Merge remote-tracking branch 'origin/master' into fix/search-result-reveal-session

This commit is contained in:
Dudu-0223 committed 2026-09-03 15:03:50 +08:00
commit f3dab48118
41 files changed
+353 -129

No files matched your search

@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.md
2026-07-10-single-file-executable-sdk-runtime-distribution.md: ab09691b48684b8ebae269df4d585b499423e671
2026-07-10-single-file-executable-sdk-runtime-distribution.zh.md: d986105b087357bd8301251dca14736a5b427bbb
2026-07-10-single-file-executable-sdk-runtime-distribution.md: 558358167a5e7d37bc79c42003a183b0c4c40d17
2026-07-10-single-file-executable-sdk-runtime-distribution.zh.md: 0f7daec5b79e6f526dde7a01c5ef85b07e955707
@@ -46,13 +46,13 @@ The deploy root includes `@deepseek-ai/dsh-mcp-client` as an explicitly supporte
[`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.node-linker=hoisted --config.auto-install-peers=false --config.link-workspace-packages=true` **directly into** `python/sdk-runtime/src/deepseek_harness_runtime/runtime/node/` → 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 → inject pkg configuration whose bin is `node_modules/@deepseek-ai/dsh/lib/bin.js` and whose assets cover 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 into the runtime directory. 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`. All four deploy flags are grounded in measurement: `--legacy` is the mandatory path with inject-workspace-packages off; hoisted gives pkg a stable single-instance layout that the explicit materialization pass makes symlink-free; 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.
CI: [`.github/workflows/build-exe-for-python-sdk.yml`](../../../../.github/workflows/build-exe-for-python-sdk.yml) is called for all four targets by the [installed-wheel Python runtime pull-request validation](../testing/2026-08-23-installed-python-wheel-black-box-ci.md) and the [public publication workflow](../process/2026-08-11-python-publication-workflow.md); `workflow_dispatch` can still select a subset. Native builds run on linux-x64 / linux-arm64 (`ubuntu-24.04-arm`) / macos-arm64 / 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, proves their package and executable provenance, then drives the complete keyless scenario set through the public SDK and direct NDJSON JSON-RPC. Trusted pull requests additionally run a real DeepSeek two-turn tool smoke on every target; 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 verifies that the executable's deployment target fits the wheel tag. A full four-target run retains five artifacts, each containing one release file: the platform-independent SDK wheel and four 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 four native runtime wheels, then a single serialized job checks and publishes all five to the project PyPI registry. The [Windows x64 runtime decision](2026-08-23-python-sdk-windows-x64-runtime.md) owns the fourth target and the explicit exclusion of Windows arm64.
CI: [`.github/workflows/build-exe-for-python-sdk.yml`](../../../../.github/workflows/build-exe-for-python-sdk.yml) is called for all five targets by the [installed-wheel Python runtime pull-request validation](../testing/2026-08-23-installed-python-wheel-black-box-ci.md) and the [public publication workflow](../process/2026-08-11-python-publication-workflow.md); `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, proves their package and executable provenance, then drives the complete keyless scenario set through the public SDK and direct NDJSON JSON-RPC. Trusted pull requests additionally run a real DeepSeek two-turn tool smoke on every target; 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 [Windows x64 runtime decision](2026-08-23-python-sdk-windows-x64-runtime.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 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`.
[`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 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`, the conservative `py3-none-macosx_14_0_arm64` tag for the Node 24 executable's macOS 13.5 deployment target, or `py3-none-win_amd64`; the Hatch hook rejects sdists, universal tags, mixed-platform payloads, missing or extra sidecars, and unsupported platforms.
[`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 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.
@@ -46,13 +46,13 @@ exe 的 VFS 内是**构建产物形态的真实包树**(各包的 `lib/` + 真
[`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.node-linker=hoisted --config.auto-install-peers=false --config.link-workspace-packages=true` **直接写入** `python/sdk-runtime/src/deepseek_harness_runtime/runtime/node/` → 恢复 legacy deploy 遗漏的直接工作区包,并拒绝剩余的 manifest 缺口 → 将暂存依赖中的符号链接替换为目标文件内容,删除包管理器的 `.bin` 链接,并在仍有任何符号链接时失败 → 注入 pkg 配置,其中 bin 为 `node_modules/@deepseek-ai/dsh/lib/bin.js`,assets 覆盖动态读取的 profile、bundle、前端、preset、原生库与配置文件 → 暂存目标平台的 `node-pty` addon → 每个构建目标调用一次 `pkg --sea` → 将 `deepseek-harness-sdk-runtime-<platform>-<arch>` 写入 `dist-exe/` 并拷回运行时目录。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` 为 pkg 提供稳定的单实例布局,再由显式物化步骤消除符号链接;关闭对等依赖自动安装可防止未声明的对等依赖扩大闭包;`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 Python 运行时拉取请求验证](../testing/2026-08-23-installed-python-wheel-black-box-ci.zh.md)与[公开发布工作流](../process/2026-08-11-python-publication-workflow.zh.md)都会调用它构建全部四个目标;`workflow_dispatch` 仍可选择部分目标。linux-x64、linux-arm64(`ubuntu-24.04-arm`)、macos-arm64 与 win-x64(`windows-2025`)分别进行原生构建,并在适用平台缓存 `~/.pkg-cache`;macOS 的 ad-hoc 签名由 pkg 处理。每个平台都把发布形态的 SDK wheel 包与运行时 wheel 包安装到 checkout 外的干净 venv,证明包与可执行文件来源,再通过公开 SDK 与直接 NDJSON JSON-RPC 运行完整 keyless 场景。可信拉取请求还会在每个目标上运行真实 DeepSeek 双轮工具冒烟测试;fork 与 Dependabot head 不会获得密钥。Linux 会检查可执行文件和原生 addon 各自的 GLIBC 依赖,并额外运行 manylinux 2.28 冒烟测试;macOS 则验证可执行文件的部署目标符合 wheel 包标签。完整构建四个目标时保留 5 个产物,每个产物只含一个发布文件:平台无关的 SDK wheel 包与 4 个原生运行时 wheel 包;手动选择部分目标时保留 SDK wheel 与所选运行时 wheel。裸 exe 与源码包只作为测试中间输入。[`.gitlab-ci.yml`](../../../../.gitlab-ci.yml) 只接受版本与根目录 `package.json` 匹配的 `python-v<repository-version>` 标签流水线,构建一个 SDK wheel 包和 4 个原生运行时 wheel 包,再由单个串行任务校验并将这 5 个文件发布到项目的 PyPI 注册表。[Windows x64 运行时决策](2026-08-23-python-sdk-windows-x64-runtime.zh.md)负责第四个目标及对 Windows arm64 的明确排除。
CI 使用 [`.github/workflows/build-exe-for-python-sdk.yml`](../../../../.github/workflows/build-exe-for-python-sdk.yml):[安装后 wheel Python 运行时拉取请求验证](../testing/2026-08-23-installed-python-wheel-black-box-ci.zh.md)与[公开发布工作流](../process/2026-08-11-python-publication-workflow.zh.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 场景。可信拉取请求还会在每个目标上运行真实 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 注册表。[Windows x64 运行时决策](2026-08-23-python-sdk-windows-x64-runtime.zh.md)负责 Windows 目标及对 Windows arm64 的明确排除。
### Python SDK 分发:双载体,exe 用于生产,`node` 用于开发
Python SDK 位于 [`python/`](../../../../python/README.zh.md):`python/sdk` 是客户端,`python/sdk-runtime` 是运行时载体包。运行时包的数据目录包含构建注入的平台可执行文件及其必需的 `-rg` 伴随文件和可选的 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`。
[`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 及其架构匹配的 ripgrep 伴随文件,macOS wheel 包还包含与其架构匹配的 spawn helper。运行时 wheel 包使用 `py3-none-manylinux_2_28_x86_64`、`py3-none-manylinux_2_28_aarch64`、针对 Node 24 可执行文件 macOS 13.5 部署目标而保守选择的 `py3-none-macosx_14_0_arm64` 标签,或 `py3-none-win_amd64`;Hatch 钩子拒绝 sdist、通用标签、混合平台载荷、伴随文件缺失或多余,以及不支持的平台。
[`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 及其架构匹配的 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 配置项都会失败,不存在外部完整配置回退。
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-08-22-single-dsh-application-launcher.md
2026-08-22-single-dsh-application-launcher.md: feac31b3eafced8158a8d79a0e5967a8de5e2f87
2026-08-22-single-dsh-application-launcher.zh.md: 88fe4ef4d4e0436ecb450e3f5319be7acee6882b
2026-08-22-single-dsh-application-launcher.md: 068c59c1695ac41216e4df7012eaeb9f80e1e5a6
2026-08-22-single-dsh-application-launcher.zh.md: 1e082a3d6de391c70a38031caafc44781bc2c7e4
@@ -48,7 +48,7 @@ Direct SDK use follows normal Harness-home resolution: explicit `dshHome`, inher
The Python runtime wheel packages the ordinary `@deepseek-ai/dsh` CLI from `node_modules/@deepseek-ai/dsh/lib/bin.js` through the private `dsh-python-runtime-closure` deploy manifest. 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 and the separately packaged `web` application.
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. The [Python profile-runtime decision](2026-08-23-python-sdk-dsh-profile-runtime.md) owns this launch, and the [Windows x64 runtime decision](2026-08-23-python-sdk-windows-x64-runtime.md) owns the fourth carrier.
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. The [Python profile-runtime decision](2026-08-23-python-sdk-dsh-profile-runtime.md) owns this launch, and the [Windows x64 runtime decision](2026-08-23-python-sdk-windows-x64-runtime.md) owns the Windows carrier.
### Enforcement
@@ -48,7 +48,7 @@ SDK 用户通过 profile 自定义插件。`dsh plugin --profile <name> ...` 管
Python 运行时 wheel 通过私有 `dsh-python-runtime-closure` 部署 manifest,打包来自 `node_modules/@deepseek-ai/dsh/lib/bin.js` 的普通 `@deepseek-ai/dsh` CLI。Python 客户端默认选择 `dsh --profile sdk`、有序 patch 文件与显式 Harness home;`python/sdk/examples` 下的可运行示例选择 `sdk-minimal`。安装的 `dsh` 控制台命令暴露相同 profile 语法与单独打包的 `web` 应用。
可执行文件族是 `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 启动别名。[Python profile 运行时决策](2026-08-23-python-sdk-dsh-profile-runtime.zh.md)负责该启动方式,[Windows x64 运行时决策](2026-08-23-python-sdk-windows-x64-runtime.zh.md)负责第四个载体。
可执行文件族是 `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 启动别名。[Python profile 运行时决策](2026-08-23-python-sdk-dsh-profile-runtime.zh.md)负责该启动方式,[Windows x64 运行时决策](2026-08-23-python-sdk-windows-x64-runtime.zh.md)负责 Windows 载体。
### 强制校验
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-08-23-python-sdk-dsh-profile-runtime.md
2026-08-23-python-sdk-dsh-profile-runtime.md: 4af7812db6818b65c754a43ec1a7f973d1cbcbf9
2026-08-23-python-sdk-dsh-profile-runtime.zh.md: 155e7d2ae0b0ba3e4163dd85a31de90bef9d588a
2026-08-23-python-sdk-dsh-profile-runtime.md: 07e3bd522b1952a8e6257f79eb292fa0ef18e5ef
2026-08-23-python-sdk-dsh-profile-runtime.zh.md: 1b68c50d93e0fd6d2555115891772b00f95bb7d4
@@ -34,7 +34,7 @@ The zero-code deployment manifest is `dsh-python-runtime-closure`. It packages `
Plain Node profiles use symlinks in `$DSH_HOME/profiles/node_modules` to share installation packages with external plugins. An operating-system symlink cannot traverse pkg's `/snapshot` filesystem, so the packaged CLI writes small real ESM proxy packages instead. Each proxy resolves the source package's explicit ESM export map directly under Node import conditions, exposes targets that exist in the installation, and re-exports their virtual module URLs. Export rows without an ESM runtime target and executable-only or declaration-only packages produce no unusable proxy entry; malformed export maps fail startup. A complete matching generation returns without acquiring the cross-process writer lock. A missing or stale entry acquires the lock, rechecks the generation, and repairs it without exposing partial proxies; either carrier can replace the other carrier's managed entry. Loader rows and external plugin peers therefore resolve through the normal profile parent walk while retaining one Cordis and one instance of each bundled module.
The published target set is Linux x64, Linux arm64, macOS arm64, and Windows x64. Installed-wheel black-box CI owns artifact provenance, default and patched profiles, external bundle installation, native tools, MCP, direct JSON-RPC, snapshots, and trusted real-provider turns on every target. The [Windows x64 runtime decision](2026-08-23-python-sdk-windows-x64-runtime.md) owns the fourth artifact and its platform-specific shell surface.
The published target set is Linux x64, Linux arm64, macOS arm64, macOS x64, and Windows x64. Installed-wheel black-box CI owns artifact provenance, default and patched profiles, external bundle installation, native tools, MCP, direct JSON-RPC, snapshots, and trusted real-provider turns on every target. The [Windows x64 runtime decision](2026-08-23-python-sdk-windows-x64-runtime.md) owns the Windows artifact and its platform-specific shell surface.
## Existing decisions and supersession
@@ -34,7 +34,7 @@ Python SDK 分发一个私有 Node 应用,直接启动完整外部 `cordis.yml
普通 Node profile 在 `$DSH_HOME/profiles/node_modules` 中使用符号链接,让外部插件共享安装包。操作系统符号链接无法进入 pkg 的 `/snapshot` 文件系统,因此打包 CLI 改为写入小型真实 ESM 代理包。每个代理直接按 Node import 条件解析源包的显式 ESM exports map,公开安装中实际存在的目标,并重新导出其虚拟模块 URL。没有 ESM 运行时目标的 export 项以及仅含可执行入口或类型声明入口的包不会产生不可用的代理条目;格式错误的 exports map 会导致启动失败。完整且匹配的 generation 不会获取跨进程写入锁。缺失或过期的配置项会获取该锁、重新检查 generation,并在不暴露半成品代理的前提下修复;任一载体都可以替换另一载体留下的受管配置项。Loader 配置项和外部插件 peer 因而可以通过普通 profile 逐级向上查找解析,同时保留一个 Cordis 和每个内置模块的单一实例。
已发布目标集合是 Linux x64、Linux arm64、macOS arm64 与 Windows x64。Installed-wheel 黑盒 CI 在每个目标上负责产物来源、默认及 patched profile、外部 bundle 安装、原生工具、MCP、直接 JSON-RPC、快照,以及可信真实提供方轮次。[Windows x64 运行时决策](2026-08-23-python-sdk-windows-x64-runtime.zh.md)负责第四个产物及其平台专属 shell surface。
已发布目标集合是 Linux x64、Linux arm64、macOS arm64、macOS x64 与 Windows x64。Installed-wheel 黑盒 CI 在每个目标上负责产物来源、默认及 patched profile、外部 bundle 安装、原生工具、MCP、直接 JSON-RPC、快照,以及可信真实提供方轮次。[Windows x64 运行时决策](2026-08-23-python-sdk-windows-x64-runtime.zh.md)负责 Windows 产物及其平台专属 shell surface。
## 既有决策与取代关系
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-08-23-python-sdk-windows-x64-runtime.md
2026-08-23-python-sdk-windows-x64-runtime.md: 59a46d99f9e7ed411aeffbb541bbe3bb0c752078
2026-08-23-python-sdk-windows-x64-runtime.zh.md: 3ab972aabb8135c8bc6285d129ba7bc9335eb11f
2026-08-23-python-sdk-windows-x64-runtime.md: 1b59dec47036e7351b212b408a31847a1a1c9639
2026-08-23-python-sdk-windows-x64-runtime.zh.md: 75cea660c3e5cc8e0222e72dc2d07a0c802aeecd
@@ -20,7 +20,7 @@ The Python process still launches the ordinary `dsh --profile sdk` application a
The executable builder accepts `win` as a pkg platform only with x64, requires the Windows build to run under x64 Node on a Windows host, preserves `.exe` names, and copies `@vscode/ripgrep-win32-x64` as the conventional `-rg.exe` sidecar. Pnpm subprocesses use a caller-supplied JavaScript entry through `process.execPath`. When the caller exposes a `.cmd` shim, the builder resolves the installed `pnpm.mjs` or `pnpm.cjs` through `PNPM_HOME`; it fails if no JavaScript entry exists instead of spawning the shim or enabling a command shell.
The required GitHub matrix builds `node24-win-x64` on `windows-2025` beside the three existing targets. The public GitHub release and GitLab tag pipeline each publish the same four runtime wheels plus the pure SDK wheel. Windows arm64 is absent from target parsing, manifests, matrices, release contents, and documentation.
The required GitHub matrix builds `node24-win-x64` on `windows-2025` beside Linux x64, Linux arm64, macOS arm64, and macOS x64. The public GitHub release and GitLab tag pipeline each publish the same five runtime wheels plus the pure SDK wheel. Windows arm64 is absent from target parsing, manifests, matrices, release contents, and documentation.
### Installed-wheel behavior
@@ -48,4 +48,4 @@ This decision partially supersedes the Windows non-goal in the [single-file runt
## Consequences
Python installation now selects a Node-free Windows x64 runtime with the same explicit-home and profile customization model as Linux and macOS. Every pull request pays for a fourth executable, runtime wheel, full keyless blackbox, and—on trusted heads—real provider task. Release validation retains five wheels instead of four. Windows arm64 users receive an explicit unsupported-platform failure until a separate native product decision supplies and proves that carrier.
Python installation selects a Node-free Windows x64 runtime with the same explicit-home and profile customization model as Linux and macOS. Every pull request builds the Windows executable and runtime wheel as one of five native targets, runs the full keyless blackbox, and—on trusted heads—runs the real-provider task. Release validation retains six wheels. Windows arm64 users receive an explicit unsupported-platform failure until a separate native product decision supplies and proves that carrier.
@@ -20,7 +20,7 @@ Python 进程仍按 [Python profile 运行时决策](2026-08-23-python-sdk-dsh-p
可执行文件构建器仅允许 x64 使用 pkg 的 `win` 平台,并要求 Windows 构建在 Windows 宿主的 x64 Node 下运行;构建器保留 `.exe` 文件名,并把 `@vscode/ripgrep-win32-x64` 复制为常规 `-rg.exe` sidecar。Pnpm 子进程通过 `process.execPath` 执行调用方提供的 JavaScript 入口。当调用方暴露 `.cmd` shim 时,构建器会通过 `PNPM_HOME` 解析已安装的 `pnpm.mjs` 或 `pnpm.cjs`;如果不存在 JavaScript 入口,构建会失败,而不会启动 shim 或启用命令 shell。
必需 GitHub 矩阵会在 `windows-2025` 上构建 `node24-win-x64`,与现有三个目标并列。公开 GitHub 发布与 GitLab 标签流水线都会发布同一组四个运行时 wheel 加纯 SDK wheel。目标解析、manifest、矩阵、发布内容与文档均不包含 Windows arm64。
必需 GitHub 矩阵会在 `windows-2025` 上构建 `node24-win-x64`,与 Linux x64、Linux arm64、macOS arm64 和 macOS x64 并列。公开 GitHub 发布与 GitLab 标签流水线都会发布同一组五个运行时 wheel 加纯 SDK wheel。目标解析、manifest、矩阵、发布内容与文档均不包含 Windows arm64。
### Installed-wheel 行为
@@ -48,4 +48,4 @@ Windows lane 会创建干净的 Windows 虚拟环境,安装版本精确匹配
## Consequences
Python 安装现在会选择无需 Node 的 Windows x64 运行时,并与 Linux、macOS 使用同一套显式 home 与 profile 自定义模型。每个拉取请求都要承担第四个可执行文件、运行时 wheel 与完整 keyless 黑盒测试;可信 head 还要承担真实提供方任务。候选发行版验证会保留五个而不是四个 wheel。Windows arm64 用户会收到明确的不支持平台错误,直到另一项原生产品决策提供并证明该载体。
Python 安装会选择无需 Node 的 Windows x64 运行时,并与 Linux、macOS 使用同一套显式 home 与 profile 自定义模型。每个拉取请求都会把 Windows 可执行文件和运行时 wheel 作为五个原生目标之一进行构建,运行完整 keyless 黑盒测试,并在可信 head 上运行真实提供方任务。候选发行版验证会保留六个 wheel。Windows arm64 用户会收到明确的不支持平台错误,直到另一项原生产品决策提供并证明该载体。
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/process/2026-08-11-python-publication-workflow.md
2026-08-11-python-publication-workflow.md: 282bd453013da9b745c601f7b1f4be2cbd133629
2026-08-11-python-publication-workflow.zh.md: 279dc4b5798d5ceb5968f92c58a7f57c4f2c5cdd
2026-08-11-python-publication-workflow.md: afb18ee97d0708d8a79885a8f6639681e97eef72
2026-08-11-python-publication-workflow.zh.md: f2d045f686c41c3e45a3cd6739dc5e1b37c58478
@@ -6,15 +6,15 @@ English | [中文](2026-08-11-python-publication-workflow.zh.md)
## Problem
The Python SDK comprises one platform-independent client wheel and four native runtime wheels that must carry one version and become installable as a set. Public PyPI uploads expose package metadata and files immediately, cannot replace an uploaded filename, and create a temporarily unusable SDK if its exact runtime dependency has not arrived. The private repository needs to exercise the complete native build and validation sequence without publishing any artifact externally.
The Python SDK comprises one platform-independent client wheel and five native runtime wheels that must carry one version and become installable as a set. Public PyPI uploads expose package metadata and files immediately, cannot replace an uploaded filename, and create a temporarily unusable SDK if its exact runtime dependency has not arrived. The private repository needs to exercise the complete native build and validation sequence without publishing any artifact externally.
## Decision
The `Release (Python)` GitHub workflow exposes credential-free validation to manual runs with `publish=false`. The run calls the native wheel builder for all four platforms, installs the Linux release set on Python 3.10 and 3.14, downloads the five resulting artifacts, verifies their exact filenames and package metadata, enforces PyPI's default per-file size limit, records SHA-256 hashes, and retains one aggregate release candidate. These jobs have only repository read permission and no registry credential or OIDC permission, and a dry run cannot enter either publication job.
The `Release (Python)` GitHub workflow exposes credential-free validation to manual runs with `publish=false`. The run calls the native wheel builder for all five targets, installs the Linux release set on Python 3.10 and 3.14, downloads the six resulting artifacts, verifies their exact filenames and package metadata, enforces PyPI's default per-file size limit, records SHA-256 hashes, and retains one aggregate release candidate. These jobs have only repository read permission and no registry credential or OIDC permission, and a dry run cannot enter either publication job.
A run with `publish=true` must use the `python-v<repository-version>` tag in the private automation repository, match that repository's `github.repository` to its repository-scoped `PYPI_PUBLISHER_REPOSITORY` variable, find `PUBLIC_PYPI_RELEASE_ENABLED=true`, and receive approval from the `pypi-runtime` and `pypi` GitHub environments for runtime and SDK publication, respectively. The read-only public mirror supplies the package metadata URLs but does not run release Actions. Only the two publication jobs receive `id-token: write`; PyPI Trusted Publishing exchanges the private repository identity for short-lived project credentials, so the repository stores no PyPI token.
Publication consumes the aggregate artifact produced and checked in the same workflow run. Each publication job verifies the retained `SHA256SUMS` before selecting its upload set. A runtime job uploads all four platform wheels before a dependent job uploads the SDK wheel because PyPI uploads are not atomic and the SDK pins the runtime distribution at the exact same version. Neither job checks out source or rebuilds a wheel. Separating them lets GitHub's failed-job retry resume an SDK failure without attempting to replace immutable runtime files.
Publication consumes the aggregate artifact produced and checked in the same workflow run. Each publication job verifies the retained `SHA256SUMS` before selecting its upload set. A runtime job uploads all five platform wheels before a dependent job uploads the SDK wheel because PyPI uploads are not atomic and the SDK pins the runtime distribution at the exact same version. Neither job checks out source or rebuilds a wheel. Separating them lets GitHub's failed-job retry resume an SDK failure without attempting to replace immutable runtime files.
Both publication actions disable public attestations. The action still uses Trusted Publishing for authentication, while omitting provenance that would disclose the private publisher repository instead of the public source mirror.
@@ -6,15 +6,15 @@ Status: implemented
## 问题
Python SDK 由一个平台无关的客户端 wheel 包和四个原生运行时 wheel 包组成,它们必须使用同一版本,并作为一组可安装。public PyPI 上传会立即公开包元数据和文件,无法替换已上传的同名文件;如果精确版本的运行时依赖尚未到达,还会产生暂时不可用的 SDK。私有仓库需要在不向外发布任何产物的情况下,执行完整的原生构建与验证流程。
Python SDK 由一个平台无关的客户端 wheel 包和五个原生运行时 wheel 包组成,它们必须使用同一版本,并作为一组可安装。public PyPI 上传会立即公开包元数据和文件,无法替换已上传的同名文件;如果精确版本的运行时依赖尚未到达,还会产生暂时不可用的 SDK。私有仓库需要在不向外发布任何产物的情况下,执行完整的原生构建与验证流程。
## 决策
GitHub 的 `Release (Python)` 工作流为设置 `publish=false` 的手动运行提供无凭据验证。该运行会为全部四个平台调用原生 wheel 包构建器,在 Python 3.10 和 3.14 上安装 Linux 发行集合,下载所得五份产物,验证其精确文件名和包元数据,执行 PyPI 默认单文件大小限制,记录 SHA-256 哈希,并保留一份汇总候选发行版。这些作业只有仓库读取权限,没有注册表凭据或 OIDC 权限,dry-run 运行无法进入任何发布作业。
GitHub 的 `Release (Python)` 工作流为设置 `publish=false` 的手动运行提供无凭据验证。该运行会为全部五个目标调用原生 wheel 包构建器,在 Python 3.10 和 3.14 上安装 Linux 发行集合,下载所得六份产物,验证其精确文件名和包元数据,执行 PyPI 默认单文件大小限制,记录 SHA-256 哈希,并保留一份汇总候选发行版。这些作业只有仓库读取权限,没有注册表凭据或 OIDC 权限,dry-run 运行无法进入任何发布作业。
设置 `publish=true` 时,运行必须在私有自动化仓库使用 `python-v<repository-version>` 标签,将该仓库的 `github.repository` 与其仓库级 `PYPI_PUBLISHER_REPOSITORY` 变量匹配,找到 `PUBLIC_PYPI_RELEASE_ENABLED=true`,并分别获得 GitHub `pypi-runtime` 和 `pypi` 环境对运行时与 SDK 发布的批准。只读公开镜像提供包元数据 URL,但不运行发布 Actions。只有两个发布作业获得 `id-token: write`;PyPI Trusted Publishing 会把私有仓库身份换成短期项目凭据,因此仓库不保存 PyPI token。
发布过程使用同一次工作流运行中生成并检查过的汇总产物。每个发布作业都会在选择上传文件前验证保留的 `SHA256SUMS`。一个运行时作业先上传全部四个平台 wheel 包,再由依赖它的作业上传 SDK wheel 包,因为 PyPI 上传不是原子操作,而 SDK 会把运行时分发包固定到完全相同的版本。两个作业都不会检出源码,也不会重新构建 wheel 包。将它们拆开后,GitHub 的失败作业重试可以在 SDK 上传失败时继续执行,而不会尝试替换不可变的运行时文件。
发布过程使用同一次工作流运行中生成并检查过的汇总产物。每个发布作业都会在选择上传文件前验证保留的 `SHA256SUMS`。一个运行时作业先上传全部五个平台 wheel 包,再由依赖它的作业上传 SDK wheel 包,因为 PyPI 上传不是原子操作,而 SDK 会把运行时分发包固定到完全相同的版本。两个作业都不会检出源码,也不会重新构建 wheel 包。将它们拆开后,GitHub 的失败作业重试可以在 SDK 上传失败时继续执行,而不会尝试替换不可变的运行时文件。
两个发布 action 都会禁用公开 attestation。action 仍使用 Trusted Publishing 进行身份认证,同时不上传会披露私有发布仓库而非公开源码镜像的 provenance。
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/testing/2026-08-23-installed-python-wheel-black-box-ci.md
2026-08-23-installed-python-wheel-black-box-ci.md: 0ac3bc63ef391536a761ad6db9d0854a3beebe01
2026-08-23-installed-python-wheel-black-box-ci.zh.md: 365da458d3eb33dbc82dcdafaebea593cc4fe971
2026-08-23-installed-python-wheel-black-box-ci.md: a1c5d5f0a8040747caa2148f6b4990fd58828cbe
2026-08-23-installed-python-wheel-black-box-ci.zh.md: ab2b6c11a2aa49a78bad990b780019f704edef51
@@ -30,7 +30,7 @@ Fork and Dependabot pull requests never receive the repository secret. Their nat
### Required targets
The pull-request `python-runtime` job calls the reusable builder for Linux x64, Linux arm64, macOS arm64, and Windows x64. Its aggregate result remains a dependency of `all checks passed`, so a failed, cancelled, or missing native carrier blocks the required verdict. The [Windows x64 runtime decision](../architecture/2026-08-23-python-sdk-windows-x64-runtime.md) owns the fourth target and its PowerShell-specific minimal snapshot.
The pull-request `python-runtime` job calls the reusable builder for Linux x64, Linux arm64, macOS arm64, macOS x64, and Windows x64. Its aggregate result remains a dependency of `all checks passed`, so a failed, cancelled, or missing native carrier blocks the required verdict. The [Windows x64 runtime decision](../architecture/2026-08-23-python-sdk-windows-x64-runtime.md) owns the Windows target and its PowerShell-specific minimal snapshot.
## Existing decisions and supersession
@@ -38,7 +38,7 @@ This decision supersedes the single-target topology in the archived [required Py
## Alternatives considered
**Keep Linux x64 as the only required carrier.** Rejected because native addons, executable construction, wheel tags, and helper files differ across the four published targets. Release-time discovery is too late for an artifact that every Python SDK installation selects by platform.
**Keep Linux x64 as the only required carrier.** Rejected because native addons, executable construction, wheel tags, and helper files differ across the five published targets. Release-time discovery is too late for an artifact that every Python SDK installation selects by platform.
**Run full behavior before wheel construction and keep two small installed smokes.** Rejected because that proves the executable against source imports, then proves too little through the distribution users install. The clean installed environment is the stronger common location for the same scenarios.
@@ -48,4 +48,4 @@ This decision supersedes the single-target topology in the archived [required Py
## Consequences
Every pull request pays for four native executable and wheel builds plus deterministic installed-artifact scenarios. Trusted same-repository pull requests also pay for one two-turn DeepSeek task per target. In exchange, the required result describes the files Python users install, proves every published carrier before merge, and cannot pass by importing the checkout or silently skipping the real provider.
Every pull request pays for five native executable and wheel builds plus deterministic installed-artifact scenarios. Trusted same-repository pull requests also pay for one two-turn DeepSeek task per target. In exchange, the required result describes the files Python users install, proves every published carrier before merge, and cannot pass by importing the checkout or silently skipping the real provider.
@@ -30,7 +30,7 @@ Fork 与 Dependabot 拉取请求永远不会获得仓库密钥。它们的原生
### 必需目标
拉取请求的 `python-runtime` job 会针对 Linux x64、Linux arm64、macOS arm64 与 Windows x64 调用可复用构建器。其聚合结果仍是 `all checks passed` 的依赖项,因此任一原生载体失败、取消或缺失都会阻止必需判定通过。[Windows x64 运行时决策](../architecture/2026-08-23-python-sdk-windows-x64-runtime.zh.md)负责第四个目标及其 PowerShell 专属极简快照。
拉取请求的 `python-runtime` job 会针对 Linux x64、Linux arm64、macOS arm64、macOS x64 与 Windows x64 调用可复用构建器。其聚合结果仍是 `all checks passed` 的依赖项,因此任一原生载体失败、取消或缺失都会阻止必需判定通过。[Windows x64 运行时决策](../architecture/2026-08-23-python-sdk-windows-x64-runtime.zh.md)负责 Windows 目标及其 PowerShell 专属极简快照。
## Existing decisions and supersession
@@ -38,7 +38,7 @@ Fork 与 Dependabot 拉取请求永远不会获得仓库密钥。它们的原生
## Alternatives considered
**只保留 Linux x64 必需载体。** 否决:四个已发布目标的原生 addon、可执行文件构建、wheel 包标签与 helper 文件不同。等到发布时才发现问题,对每个 Python SDK 安装都会按平台选择的产物而言太晚。
**只保留 Linux x64 必需载体。** 否决:五个已发布目标的原生 addon、可执行文件构建、wheel 包标签与 helper 文件不同。等到发布时才发现问题,对每个 Python SDK 安装都会按平台选择的产物而言太晚。
**在 wheel 构建前运行完整行为,并保留两个很小的安装后冒烟测试。** 否决:这只能证明可执行文件配合源码 import 工作,再通过 distribution 证明很少的行为。干净安装环境是在同一批场景中验证用户实际安装内容的更强位置。
@@ -48,4 +48,4 @@ Fork 与 Dependabot 拉取请求永远不会获得仓库密钥。它们的原生
## Consequences
每个拉取请求都会承担四个原生可执行文件及 wheel 包构建,并运行确定性的安装后产物场景。可信的同仓库拉取请求还会在每个目标上承担一次双轮 DeepSeek 任务。相应地,必需结果描述 Python 用户实际安装的文件,在合并前证明每个已发布载体,并且不能通过导入 checkout 或静默跳过真实提供方而通过。
每个拉取请求都会承担五个原生可执行文件及 wheel 包构建,并运行确定性的安装后产物场景。可信的同仓库拉取请求还会在每个目标上承担一次双轮 DeepSeek 任务。相应地,必需结果描述 Python 用户实际安装的文件,在合并前证明每个已发布载体,并且不能通过导入 checkout 或静默跳过真实提供方而通过。
+22 -10
View File
@@ -2,7 +2,7 @@ name: Build single-exe
# Native builds for the release targets; see
# .agents/notes/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.md.
# A full target run retains one SDK wheel and four runtime wheels; subset
# A full target run retains one SDK wheel and five runtime wheels; subset
# dispatch retains the SDK wheel and selected runtime wheels. Bare executables
# and source closures are test inputs. Run manually or call it from the Python
# release workflow. There is no `pull_request` trigger: a label trigger would
@@ -12,7 +12,7 @@ on:
workflow_call:
inputs:
targets:
description: Comma-separated pkg targets to build; empty builds all four.
description: Comma-separated pkg targets to build; empty builds all five.
type: string
required: false
default: ''
@@ -36,7 +36,7 @@ on:
description: >-
Comma-separated pkg targets to build. Any subset of:
node24-linux-x64, node24-linux-arm64, node24-macos-arm64,
node24-win-x64. Empty builds all four.
node24-macos-x64, node24-win-x64. Empty builds all five.
type: string
required: false
default: ''
@@ -89,7 +89,7 @@ jobs:
id: plan
env:
# Blank dispatch inputs build all targets.
TARGETS: ${{ inputs.targets || 'node24-linux-x64,node24-linux-arm64,node24-macos-arm64,node24-win-x64' }}
TARGETS: ${{ inputs.targets || 'node24-linux-x64,node24-linux-arm64,node24-macos-arm64,node24-macos-x64,node24-win-x64' }}
run: |
set -euo pipefail
matrix='[]'
@@ -98,14 +98,15 @@ jobs:
t="$(echo "$raw" | xargs)" # trim surrounding whitespace
[ -z "$t" ] && continue
# Native-only: hosted arm64 Linux uses ubuntu-24.04-arm, while
# macos-latest is Apple Silicon.
# macos-latest is Apple Silicon; macos-15-intel is native x64.
case "$t" in
node24-linux-x64) runner=ubuntu-latest ;;
node24-linux-arm64) runner=ubuntu-24.04-arm ;;
node24-macos-arm64) runner=macos-latest ;;
node24-macos-x64) runner=macos-15-intel ;;
node24-win-x64) runner=windows-2025 ;;
*)
echo "::error::Unknown target '$t'. Supported: node24-linux-x64, node24-linux-arm64, node24-macos-arm64, node24-win-x64."
echo "::error::Unknown target '$t'. Supported: node24-linux-x64, node24-linux-arm64, node24-macos-arm64, node24-macos-x64, node24-win-x64."
exit 1
;;
esac
@@ -251,6 +252,7 @@ jobs:
linux-x64) wheel=deepseek_harness_runtime_bin-$VERSION-py3-none-manylinux_2_28_x86_64.whl ;;
linux-arm64) wheel=deepseek_harness_runtime_bin-$VERSION-py3-none-manylinux_2_28_aarch64.whl ;;
macos-arm64) wheel=deepseek_harness_runtime_bin-$VERSION-py3-none-macosx_14_0_arm64.whl ;;
macos-x64) wheel=deepseek_harness_runtime_bin-$VERSION-py3-none-macosx_14_0_x86_64.whl ;;
*) echo "::error::Unsupported runtime platform $platform"; exit 1 ;;
esac
[ -x "$exe" ] || { echo "::error::$exe missing or not executable"; exit 1; }
@@ -432,13 +434,23 @@ jobs:
exit 1
}
- name: Check macOS deployment target
- name: Check macOS payload architecture and deployment target
if: runner.os == 'macOS'
env:
EXE: ${{ steps.runtime-posix.outputs.exe }}
run: >-
python3 scripts/check-macos-deployment-target.py
"$EXE" "$EXE-spawn-helper"
PLATFORM: ${{ steps.runtime-posix.outputs.platform }}
run: |
set -euo pipefail
case "$PLATFORM" in
macos-arm64) macho_arch=arm64 ;;
macos-x64) macho_arch=x86_64 ;;
*) echo "::error::Unsupported macOS platform $PLATFORM"; exit 1 ;;
esac
for payload in "$EXE" "$EXE-rg" "$EXE-spawn-helper"; do
lipo "$payload" -verify_arch "$macho_arch"
done
python3 scripts/check-macos-deployment-target.py \
--platform "$PLATFORM" "$EXE" "$EXE-rg" "$EXE-spawn-helper"
- name: Run wheel in a manylinux 2.28 container
if: runner.os == 'Linux'
+1 -1
View File
@@ -328,7 +328,7 @@ jobs:
name: python runtime / release-shaped matrix
uses: ./.github/workflows/build-exe-for-python-sdk.yml
with:
targets: node24-linux-x64,node24-linux-arm64,node24-macos-arm64,node24-win-x64
targets: node24-linux-x64,node24-linux-arm64,node24-macos-arm64,node24-macos-x64,node24-win-x64
ci: true
secrets:
DEEPSEEK_API_KEY_EXTERNAL: ${{ secrets.DEEPSEEK_API_KEY_EXTERNAL }}
+3 -2
View File
@@ -24,10 +24,10 @@ concurrency:
jobs:
build:
name: Build five wheels
name: Build six wheels
uses: ./.github/workflows/build-exe-for-python-sdk.yml
with:
targets: node24-linux-x64,node24-linux-arm64,node24-macos-arm64,node24-win-x64
targets: node24-linux-x64,node24-linux-arm64,node24-macos-arm64,node24-macos-x64,node24-win-x64
release: true
python-compat:
@@ -149,6 +149,7 @@ jobs:
actual="$(mktemp)"
printf '%s\n' \
"deepseek_harness_runtime_bin-$VERSION-py3-none-macosx_14_0_arm64.whl" \
"deepseek_harness_runtime_bin-$VERSION-py3-none-macosx_14_0_x86_64.whl" \
"deepseek_harness_runtime_bin-$VERSION-py3-none-manylinux_2_28_aarch64.whl" \
"deepseek_harness_runtime_bin-$VERSION-py3-none-manylinux_2_28_x86_64.whl" \
"deepseek_harness_runtime_bin-$VERSION-py3-none-win_amd64.whl" \
+24 -3
View File
@@ -60,8 +60,16 @@ sdk-wheel:
docker run --rm -v "$PWD:/work" -w /work "$image" bash -euxo pipefail -c "/opt/python/cp310-cp310/bin/python -m venv /tmp/dsh-sdk && /tmp/dsh-sdk/bin/python -m pip install --find-links /work/release/$PLATFORM --find-links /work/release/sdk deepseek-harness-sdk==$DSH_WHEEL_VERSION && /tmp/dsh-sdk/bin/python /work/scripts/smoke-python-runtime.py --scenario sdk-default && /tmp/dsh-sdk/bin/python /work/scripts/smoke-python-runtime.py --scenario sdk-mcp"
fi
- |
if [ "$PLATFORM" = macos-arm64 ]; then
python3 scripts/check-macos-deployment-target.py "$EXE" "$EXE-spawn-helper"
if [ "${PLATFORM#macos-}" != "$PLATFORM" ]; then
case "$PLATFORM" in
macos-arm64) macho_arch=arm64 ;;
macos-x64) macho_arch=x86_64 ;;
*) echo "Unsupported macOS platform $PLATFORM"; exit 1 ;;
esac
for payload in "$EXE" "$EXE-rg" "$EXE-spawn-helper"; do
lipo "$payload" -verify_arch "$macho_arch"
done
python3 scripts/check-macos-deployment-target.py --platform "$PLATFORM" "$EXE" "$EXE-rg" "$EXE-spawn-helper"
fi
artifacts:
paths: [release/$PLATFORM/*.whl]
@@ -97,6 +105,16 @@ runtime-macos-arm64:
- job: sdk-wheel
artifacts: true
runtime-macos-x64:
extends: .runtime-wheel
tags: [macos-x64]
variables:
PKG_TARGET: node24-macos-x64
PLATFORM: macos-x64
needs:
- job: sdk-wheel
artifacts: true
runtime-windows-x64:
stage: build
tags: [windows-x64]
@@ -147,6 +165,8 @@ publish-python:
artifacts: true
- job: runtime-macos-arm64
artifacts: true
- job: runtime-macos-x64
artifacts: true
- job: runtime-windows-x64
artifacts: true
before_script:
@@ -157,11 +177,12 @@ publish-python:
- test "$CI_COMMIT_TAG" = "python-v$DSH_VERSION" || { echo "Tag $CI_COMMIT_TAG does not match package.json version $DSH_VERSION"; exit 1; }
- python -m pip install twine==6.2.0
script:
- test "$(find release -name '*.whl' | wc -l | tr -d ' ')" = 5
- test "$(find release -name '*.whl' | wc -l | tr -d ' ')" = 6
- test -f "release/sdk/deepseek_harness_sdk-${DSH_WHEEL_VERSION}-py3-none-any.whl"
- test -f "release/linux-x64/deepseek_harness_runtime_bin-${DSH_WHEEL_VERSION}-py3-none-manylinux_2_28_x86_64.whl"
- test -f "release/linux-arm64/deepseek_harness_runtime_bin-${DSH_WHEEL_VERSION}-py3-none-manylinux_2_28_aarch64.whl"
- test -f "release/macos-arm64/deepseek_harness_runtime_bin-${DSH_WHEEL_VERSION}-py3-none-macosx_14_0_arm64.whl"
- test -f "release/macos-x64/deepseek_harness_runtime_bin-${DSH_WHEEL_VERSION}-py3-none-macosx_14_0_x86_64.whl"
- test -f "release/win-x64/deepseek_harness_runtime_bin-${DSH_WHEEL_VERSION}-py3-none-win_amd64.whl"
- python -m twine check release/*/*.whl
- export TWINE_USERNAME=gitlab-ci-token
+2 -2
View File
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write docs/user/guide/providers.md
providers.md: 59682d38c71893c16118639cb2b24f63497a3c13
providers.zh.md: a72be6c706f7ce6d29a802bfaa7f65eedb2eb991
providers.md: 0db3280760c487c8469399bc9668b0685213e375
providers.zh.md: 3af9096f5a77ee40afe2ee788aa22a0758dd0e86
+70 -17
View File
@@ -12,21 +12,39 @@ Open **Settings → Models**. The DeepSeek card exposes one API-key field; enter
Keys are write-only. The page receives a redacted descriptor after saving, never the literal secret. The key is stored in `$DSH_HOME/.credentials.yaml`, while settings retain only its credential reference.
## Add a catalog provider
## Add a built-in provider
Choose **Add provider**, select a provider such as Anthropic or OpenAI, enter its API key, and save. The installed catalog supplies the endpoint, protocol, and model list.
Choose **Add provider** and pick a provider dsh ships with; the list shows provider ids such as `anthropic`, `openai`, `moonshotai` for Kimi, or `zai` for GLM. Enter its API key and save. The installed catalog supplies the endpoint, protocol, and model list.
Providers with native authentication need their native credentials instead. Bedrock, Vertex, Azure, and Codex use AWS credentials and a region, an ADC project, an `api-version`, and OAuth respectively; filling only the API-key field does not configure them.
Providers that sign in with OAuth, such as Codex, are not supported here yet.
## Add a custom provider
Choose **Add a custom provider** for a company gateway, self-hosted server, or provider absent from the installed catalog. Supply a lowercase Provider ID, base URL, API protocol, credential, and at least one model.
Choose **Add a custom provider** for a company gateway, self-hosted server, or provider absent from the installed catalog. Supply a lowercase Provider ID, base URL, API protocol, credential, and at least one model. The **API protocol** must be the one your gateway speaks, and the form offers three: `openai-completions` for OpenAI Chat Completions, `openai-responses` for the OpenAI Responses API, and `anthropic-messages` for the Anthropic Messages API. A provider speaks one protocol, so a gateway that serves two needs two providers.
![The custom provider form: Provider ID, display name, base URL, API protocol, and API key](providers-custom-form.png)
The Provider ID is permanent because requests, saved sessions, model defaults, and credential references use it. To rename a provider, add a new provider and delete the old one. The display name, base URL, protocol, credential, and models remain editable.
Under **Model catalog**, choose **Fetch available models** to query the base URL and credential currently shown in the form. Selecting candidates updates the draft; the provider is not stored until you save. Catalog providers use their installed catalog without a network request.
### Discover models
Under **Model catalog**, choose **Fetch available models** to ask the endpoint which models it serves. The request uses the base URL, protocol, and key currently in the form, or a saved provider's stored key, and the reply opens a searchable picker: search, tick the models you want, and choose **Add selected**. Nothing is stored until you save or create the provider.
Discovery reads the listing formats common gateways publish, but not every endpoint answers in one of them, so treat it as a convenience rather than a guarantee: when it fails or lists nothing, add the model ids by hand and they work just the same. A built-in provider is always answered from the installed catalog, even when its base URL points at a gateway, so fetch through a custom provider to see what the gateway really serves.
## Select a model
Configured providers appear in the model picker. Selecting a model also makes it the default for new sessions. A session that has already sent a request retains the model recorded in its own log.
If a saved default names a provider that was deleted, the composer displays **Select model** and blocks input until another model is selected.
## Advanced configuration
The generated [plugin configuration catalog](../../config-catalog.md) lists every supported field and default for every plugin; [`dsh-llm-pi-ai`](../../config-catalog.md#deepseek-aidsh-llm-pi-ai) is the provider section this page configures. The [`dsh-llm-pi-ai`](../../../packages/llm/llm-pi-ai/README.md) and [`dsh-llm-deepseek`](../../../packages/llm/llm-deepseek/README.md) references own direct `settings.yaml` configuration, catalog resolution, reasoning controls, credentials, and adapter errors.
::: tip The form is deliberately small
The Models page exposes only what a route needs to exist: the API key, display name, base URL, API protocol, and for each model its id, display name, context window, and max output tokens. Every other field — reasoning effort levels, image input, request-compatibility switches, headers, timeouts, retry policy — is set in `$DSH_HOME/settings.yaml`, the same document the page writes. Edit it directly, or, when the browser runs on the same machine as the server, open it with **Open configuration file** in the Settings header; the adapters re-read it on the next request, so nothing needs a restart. The subsections below cover the fields most gateways need.
:::
### Image input
@@ -64,7 +82,7 @@ llm-pi-ai:
- id: second-model
```
`defaultInput` is a fallback, not an override, and defaults to `[text]`: on a catalog provider it answers only for models the catalog does not describe, so it never removes images from a catalog model that has them. Narrow one of those with that model's own `input`. A catalog provider has no `models` list to put it in, so write it under `modelOverrides`, keyed by model id:
`defaultInput` is a fallback, not an override, and defaults to `[text]`: on a built-in provider it answers only for models its catalog does not describe, so it never removes images from a catalog model that has them. Narrow one of those with that model's own `input`. A built-in provider has no `models` list to put it in, so write it under `modelOverrides`, keyed by model id:
```yaml
llm-pi-ai:
@@ -79,6 +97,48 @@ Every list must name at least one modality except a model's own, where an empty
Both fields state a claim about your endpoint rather than checking it. A model that declares images its endpoint does not serve is not caught here; the provider rejects the request instead.
### Reasoning effort
The model picker offers an **Effort** menu for a model that declares reasoning levels. A built-in provider's models inherit their levels from the installed catalog. A model you enter by hand declares none, so the Effort entry does not appear in the menu and the endpoint's own default decides whether the model thinks. Declare the levels with `reasoningEfforts` in `$DSH_HOME/settings.yaml`:
```yaml
llm-pi-ai:
providers:
my-gateway:
apiKeyEnv: GATEWAY_API_KEY
api: openai-completions
baseURL: https://gateway.example/v1
reasoning: high
models:
- id: my-reasoner
reasoningEfforts:
off:
high: high
max: max
```
Each key is a level the menu offers, and its value is the spelling sent on the wire as `reasoning_effort`, so `max: xhigh` renames a level for a gateway with its own vocabulary. Only `off` may stay empty, because for most endpoints not thinking is the parameter's absence. The route's `reasoning` is the level used while a session has picked none; choosing an effort in the picker saves it, with the model, as the default for new sessions.
An `off` left empty sends nothing, which only stops a model that thinks on request; an `off` given a value sends that value as `reasoning_effort` instead. A model that thinks unless told not to — DeepSeek V4 behind an OpenAI-compatible gateway, for example — needs `compat.thinkingFormat: deepseek`, which makes `off` send `thinking: {type: disabled}` and every other level send `thinking: {type: enabled}` beside the effort:
```yaml
models:
- id: deepseek-v4-pro
compat:
thinkingFormat: deepseek
reasoningEfforts:
off:
high: high
max: max
```
A built-in provider's model whose gateway does not reason loses its levels with `reasoningEfforts: false` under `modelOverrides`; selecting an effort for it is then refused as `UNSUPPORTED_REASONING_EFFORT`. DeepSeek's own route needs none of this: its models already offer `off`, `low`, `high`, and `max`, and `llm-deepseek.reasoningEffort` sets the default the picker starts from:
```yaml
llm-deepseek:
reasoningEffort: max
```
### Request compatibility
A gateway can hold a working key at a reachable address and still refuse every request. pi-ai decides the shape of a request — which role carries the system prompt, which field caps the output, how a thinking level travels — from the endpoint's URL, and an address it does not recognize is addressed as though it were OpenAI itself. Most OpenAI-compatible gateways refuse at least one thing OpenAI accepts.
@@ -115,23 +175,16 @@ Each switch belongs to the protocols that declare it, so a switch valid on one `
Every switch, its accepted values, and the protocols that take it are listed under `PiAiCompatProfile` in the [generated `dsh-llm-pi-ai` configuration reference](../../config-catalog.md#deepseek-aidsh-llm-pi-ai) — which is derived from the source, so it cannot fall behind what the adapter accepts.
## Select a model
Configured providers appear in the model picker. Selecting a model also makes it the default for new sessions. A session that has already sent a request retains the model recorded in its own log.
If a saved default names a provider that was deleted, the composer displays **Select model** and blocks input until another model is selected.
## Troubleshooting
- **`MISSING_CREDENTIAL`** — Store the provider key through the Models page or supply the referenced environment variable.
- **`UNKNOWN_MODEL`** — Select a configured model or add the missing model to the custom provider.
- **Fetching available models returns 401** — Check the key. Model discovery calls the OpenAI-compatible `GET /models` endpoint; enter models manually for endpoints that do not provide it.
- **Fetching available models reports neither a `data` array nor a `models` object** — The endpoint's listing is in a format discovery does not read. Enter the models by hand.
- **The gateway refuses every request although the key and URL are right** — Its request shape differs from OpenAI's. Start with `compat.supportsDeveloperRole: false` and `compat.maxTokensField: max_tokens` on the route.
- **Only reasoning models fail** — pi-ai sends their system prompt as the `developer` role, which the gateway rejects. Set `compat.supportsDeveloperRole: false`.
- **The Effort menu does not appear for a model you entered by hand** — It declares no levels. Add `reasoningEfforts` to the model in `settings.yaml`.
- **`off` does not stop a DeepSeek model from thinking** — An empty `off` sends no reasoning field at all, and an endpoint that thinks by default keeps thinking. Set `compat.thinkingFormat: deepseek` on the model or the route.
- **A compat switch is refused as having no value** — A key written with nothing after the colon. Give it a value, or remove the key to keep the installed catalog's.
- **An image is refused before sending** — The model declares no image modality. Give a custom provider's model `input: [text, image]`; DeepSeek's own chat-completions route is text-only and cannot be configured otherwise.
- **An image is refused before sending** — The model declares no image modality. Give a custom provider's model `input: [text, image]`; on DeepSeek's own route, select `deepseek-v4-flash-vision-exp`, the model that declares images.
- **The provider rejects a request carrying an image** — The model declares images its endpoint does not actually serve. Remove `image` from whichever list granted it — the model's `input`, or the route's `defaultInput` — then start a new session: the attached image stays in the session log, so the same request repeats until the session moves off it.
## Advanced configuration
The generated [plugin configuration catalog](../../config-catalog.md) lists every supported field and default for every plugin; [`dsh-llm-pi-ai`](../../config-catalog.md#deepseek-aidsh-llm-pi-ai) is the provider section this page configures. The [`dsh-llm-pi-ai`](../../../packages/llm/llm-pi-ai/README.md) and [`dsh-llm-deepseek`](../../../packages/llm/llm-deepseek/README.md) references own direct `settings.yaml` configuration, catalog resolution, reasoning controls, credentials, and adapter errors.
+70 -17
View File
@@ -12,21 +12,39 @@
密钥是只写的。保存后,页面只会收到脱敏描述符,永远不会收到明文密钥。密钥存储在 `$DSH_HOME/.credentials.yaml` 中,settings 只保留它的凭据引用。
## 添加目录提供方
## 添加内置提供方
选择**添加提供方**,选取 Anthropic 或 OpenAI 等提供方,输入其 API 密钥并保存。已安装目录会提供端点、协议和模型列表。
选择**添加提供方**,选取 dsh 自带的提供方;列表显示的是提供方 id,例如 `anthropic`、`openai`、Kimi 对应的 `moonshotai`、GLM 对应的 `zai`。输入其 API 密钥并保存。已安装目录会提供端点、协议和模型列表。
使用原生认证的提供方需要各自的原生凭据。Bedrock、Vertex、Azure 和 Codex 分别使用 AWS 凭据与区域、ADC 项目、`api-version` 和 OAuth;只填写 API 密钥字段无法完成配置。
通过 OAuth 登录的提供方(例如 Codex)暂不支持。
## 添加自定义提供方
对于公司网关、自建服务器或已安装目录中不存在的提供方,选择**添加自定义提供方**。提供小写 Provider ID、基础 URL、API 协议、凭据和至少一个模型。
对于公司网关、自建服务器或已安装目录中不存在的提供方,选择**添加自定义提供方**。提供小写 Provider ID、基础 URL、API 协议、凭据和至少一个模型。**API 协议**必须选网关实际使用的那一种,表单提供三种:`openai-completions` 对应 OpenAI Chat Completions,`openai-responses` 对应 OpenAI Responses API,`anthropic-messages` 对应 Anthropic Messages API。一个提供方只使用一种协议,网关同时提供两种时需要建两个提供方。
![自定义提供方表单:Provider ID、显示名称、API 地址、API 协议、API 密钥](providers-custom-form.zh.png)
Provider ID 是永久的,因为请求、已保存会话、模型默认值和凭据引用都会使用它。如需重命名提供方,请添加新提供方并删除旧提供方。显示名称、基础 URL、协议、凭据和模型仍可编辑。
在**模型目录**中选择**获取可用模型**,可查询表单当前显示的基础 URL 和凭据。选择候选项只会更新草稿;保存前不会存储提供方。目录提供方使用已安装目录,不发起网络请求。
### 探测模型
在**模型目录**中选择**获取可用模型**,即可询问端点它提供哪些模型。请求使用表单当前的 API 地址、协议和密钥,已保存的提供方则用已存储的密钥;响应会打开一个可搜索的选择框,搜索、勾选想要的模型,再点**添加所选**。保存或创建提供方之前不会存储任何内容。
探测读取的是常见网关公开的列表格式,但并非每个端点都用这些格式作答,所以它只是便利手段而非保证:探测失败或列表为空时,手动添加模型 ID 即可,效果完全一样。内置提供方一律由已安装目录作答,即使其 API 地址指向网关也是如此,要查看网关实际提供的模型,请通过自定义提供方探测。
## 选择模型
已配置的提供方会出现在模型选择器中。选择模型也会将其设为新会话的默认值。已发送过请求的会话会保留自身日志中记录的模型。
如果已保存默认值指向已删除的提供方,输入框会显示**选择模型**,并在选择其他模型前阻止输入。
## 进阶配置
自动生成的[插件配置目录](../../config-catalog.zh.md)列出每个插件的所有受支持字段与默认值;[`dsh-llm-pi-ai`](../../config-catalog.zh.md#deepseek-aidsh-llm-pi-ai) 就是本页所配置的那个提供方段落。[`dsh-llm-pi-ai`](../../../packages/llm/llm-pi-ai/README.zh.md) 和 [`dsh-llm-deepseek`](../../../packages/llm/llm-deepseek/README.zh.md) 参考文档负责直接 `settings.yaml` 配置、目录解析、推理控制、凭据与适配器错误。
::: tip 表单刻意保持精简
模型页只开放让一条路由得以存在的字段:API 密钥、显示名称、API 地址、API 协议,以及每个模型的 ID、显示名称、上下文窗口和最大输出 token 数。其余所有字段——推理等级、图片输入、请求兼容性开关、请求头、超时、重试策略——都在 `$DSH_HOME/settings.yaml` 中设置,也就是模型页写入的同一份文档。可以直接编辑它;浏览器与服务器在同一台机器时,也可以点击设置页顶部的**打开配置文件**打开它。适配器会在下一次请求时重新读取,无需重启任何东西。下面各小节介绍多数网关会用到的字段。
:::
### 图片输入
@@ -64,7 +82,7 @@ llm-pi-ai:
- id: second-model
```
`defaultInput` 是回退值而不是覆盖值,默认为 `[text]`:在目录提供方上,它只为目录未描述的模型作答,因此绝不会把目录中本就具备图片能力的模型的该能力去掉。要收窄这类模型,请用它自己的 `input`。目录提供方没有可供填写的 `models` 列表,因此写在 `modelOverrides` 下,以模型 id 为键:
`defaultInput` 是回退值而不是覆盖值,默认为 `[text]`:在内置提供方上,它只为其目录未描述的模型作答,因此绝不会把目录中本就具备图片能力的模型的该能力去掉。要收窄这类模型,请用它自己的 `input`。内置提供方没有可供填写的 `models` 列表,因此写在 `modelOverrides` 下,以模型 id 为键:
```yaml
llm-pi-ai:
@@ -79,6 +97,48 @@ llm-pi-ai:
这两个字段都是对你端点的断言,而不是对它的检查。声明了端点并不提供的图片能力的模型不会在这里被拦下,改由提供方拒绝该请求。
### 推理等级
对于声明了推理等级的模型,模型选择器会提供**推理等级**菜单。内置提供方的模型从已安装目录继承其等级。手动录入的模型不声明任何等级,因此模型菜单里不会出现推理等级项,由端点自身的默认值决定模型是否思考。请在 `$DSH_HOME/settings.yaml` 中用 `reasoningEfforts` 声明等级:
```yaml
llm-pi-ai:
providers:
my-gateway:
apiKeyEnv: GATEWAY_API_KEY
api: openai-completions
baseURL: https://gateway.example/v1
reasoning: high
models:
- id: my-reasoner
reasoningEfforts:
off:
high: high
max: max
```
每个键都是菜单提供的一个等级,其值是在协议上以 `reasoning_effort` 发送的写法,因此 `max: xhigh` 可以为自有一套词汇的网关重命名某个等级。只有 `off` 可以留空,因为对多数端点来说,不思考就是不传该参数。路由的 `reasoning` 是会话尚未选择等级时采用的等级;在选择器中选定某个等级后,它会与模型一起保存为新会话的默认值。
留空的 `off` 什么都不发送,这只能让「按请求才思考」的模型停下来;给 `off` 一个值,则会把该值作为 `reasoning_effort` 发送。对于「不明确关闭就会思考」的模型——例如 OpenAI 兼容网关后面的 DeepSeek V4——需要 `compat.thinkingFormat: deepseek`:它让 `off` 发送 `thinking: {type: disabled}`,其他每个等级则在 effort 之外再发送 `thinking: {type: enabled}`:
```yaml
models:
- id: deepseek-v4-pro
compat:
thinkingFormat: deepseek
reasoningEfforts:
off:
high: high
max: max
```
网关并不提供推理能力的内置提供方模型,可在 `modelOverrides` 下用 `reasoningEfforts: false` 去掉其等级;之后再为它选择等级会被拒绝并报 `UNSUPPORTED_REASONING_EFFORT`。DeepSeek 自身的路由不需要以上任何配置:其模型已经提供 `off`、`low`、`high` 和 `max`,`llm-deepseek.reasoningEffort` 设置选择器的起始默认值:
```yaml
llm-deepseek:
reasoningEffort: max
```
### 请求兼容性
网关可能持有可用的密钥、地址也通得到,却仍然拒绝每一个请求。pi-ai 依据端点的 URL 决定请求的形状——系统提示词由哪个角色承载、输出上限写在哪个字段、思考级别如何传输——而对于它无法识别的地址,会当作 OpenAI 本身来对待。多数 OpenAI 兼容网关至少会拒绝 OpenAI 所接受的某一样东西。
@@ -115,23 +175,16 @@ llm-pi-ai:
全部开关、各自接受的取值,以及接受它们的协议,都列在[生成的 `dsh-llm-pi-ai` 配置参考](../../config-catalog.zh.md#deepseek-aidsh-llm-pi-ai)的 `PiAiCompatProfile` 之下——该参考派生自源码,因此不会落后于适配器实际接受的内容。
## 选择模型
已配置的提供方会出现在模型选择器中。选择模型也会将其设为新会话的默认值。已发送过请求的会话会保留自身日志中记录的模型。
如果已保存默认值指向已删除的提供方,输入框会显示**选择模型**,并在选择其他模型前阻止输入。
## 排错
- **`MISSING_CREDENTIAL`**:通过模型页存储提供方密钥,或提供被引用的环境变量。
- **`UNKNOWN_MODEL`**:选择已配置的模型,或向自定义提供方添加缺失的模型。
- **获取可用模型返回 401**:检查密钥。模型发现会调用 OpenAI 兼容的 `GET /models` 端点;对于不提供该端点的服务,请手动输入模型。
- **获取可用模型提示既没有 `data` 数组也没有 `models` 对象**:端点返回的列表格式不在探测的读取范围内。请手动输入模型。
- **密钥与地址都正确,网关却拒绝每一个请求**:它的请求形状与 OpenAI 不同。先在路由上设 `compat.supportsDeveloperRole: false` 与 `compat.maxTokensField: max_tokens`。
- **只有推理模型失败**:pi-ai 把它们的系统提示词以 `developer` 角色发出,而网关拒绝该角色。设 `compat.supportsDeveloperRole: false`。
- **手动录入的模型没有推理等级菜单**:该模型没有声明任何等级。在 `settings.yaml` 中给该模型加上 `reasoningEfforts`。
- **`off` 无法让 DeepSeek 模型停止思考**:留空的 `off` 不发送任何推理字段,默认思考的端点就继续思考。请在模型或路由上设置 `compat.thinkingFormat: deepseek`。
- **某个 compat 开关因没有值而被拒绝**:冒号后什么都没写。给它一个值,或删掉该键以沿用已安装 catalog 的值。
- **图片在发送前被拒绝**:该模型未声明图片模态。请给自定义提供方的模型加上 `input: [text, image]`;DeepSeek 自身的 chat-completions 路由是纯文本的,且无法通过配置改变。
- **图片在发送前被拒绝**:该模型未声明图片模态。请给自定义提供方的模型加上 `input: [text, image]`;在 DeepSeek 自身的路由上,请选择声明了图片能力的模型 `deepseek-v4-flash-vision-exp`。
- **提供方拒绝了带图片的请求**:该模型声明了其端点实际并不提供的图片能力。请从授予它图片能力的那个列表中移除 `image`——可能是模型的 `input`,也可能是路由的 `defaultInput`——然后开启新会话:附加的图片会留在会话日志里,因此在会话离开它之前,同一个请求会不断重复。
## 进阶配置
自动生成的[插件配置目录](../../config-catalog.zh.md)列出每个插件的所有受支持字段与默认值;[`dsh-llm-pi-ai`](../../config-catalog.zh.md#deepseek-aidsh-llm-pi-ai) 就是本页所配置的那个提供方段落。[`dsh-llm-pi-ai`](../../../packages/llm/llm-pi-ai/README.zh.md) 和 [`dsh-llm-deepseek`](../../../packages/llm/llm-deepseek/README.zh.md) 参考文档负责直接 `settings.yaml` 配置、目录解析、推理控制、凭据与适配器错误。
+2 -2
View File
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write python/development.md
development.md: 72acaec2b3ec2142fc176d940f1a2854e44ebad8
development.zh.md: 8bd17ad7e604b1cdc4456053bcec95c9e8e503cb
development.md: aa0144d7eaa66711d0f08316d4445da060918ca8
development.zh.md: a35f6fc8de1bdbd282fd8999a1440fde0c400b34
+3 -3
View File
@@ -13,7 +13,7 @@ pnpm install
pnpm exec tsx scripts/build-exe-for-python-sdk.ts
```
Use `--skip-build` when the required `lib/` artifacts already exist, or `--targets=node24-linux-x64,node24-linux-arm64,node24-macos-arm64,node24-win-x64` to select platforms. Build each target on its native architecture. Products land in `dist-exe/` and the script syncs the selected carriers into `python/sdk-runtime/`. Windows emits `.exe` and `-rg.exe`; macOS also syncs the matching spawn helper required by `node-pty`.
Use `--skip-build` when the required `lib/` artifacts already exist, or `--targets=node24-linux-x64,node24-linux-arm64,node24-macos-arm64,node24-macos-x64,node24-win-x64` to select platforms. Build each target on its native architecture. Products land in `dist-exe/` and the script syncs the selected carriers into `python/sdk-runtime/`. Windows emits `.exe` and `-rg.exe`; macOS also syncs the matching spawn helper required by `node-pty`.
## Validate the SDK
@@ -79,11 +79,11 @@ pip install \
"dist-python/deepseek_harness_runtime_bin-$version-py3-none-macosx_14_0_arm64.whl"
```
The runtime distribution is wheel-only. The release pipeline publishes four platform wheels with the pure SDK wheel: Linux x64, Linux arm64, macOS 14 or newer on arm64, and Windows x64 (`win_amd64`). A `python-v<repository-version>` tag is accepted only when it matches the repository version; prerelease repository versions such as `0.0.1-rc.1` use their normalized PEP 440 spelling, such as `0.0.1rc1`, inside wheel filenames and metadata.
The runtime distribution is wheel-only. The release pipeline publishes five platform wheels with the pure SDK wheel: Linux x64, Linux arm64, macOS 14 or newer on arm64 and x64, and Windows x64 (`win_amd64`). A `python-v<repository-version>` tag is accepted only when it matches the repository version; prerelease repository versions such as `0.0.1-rc.1` use their normalized PEP 440 spelling, such as `0.0.1rc1`, inside wheel filenames and metadata.
## Validate a release candidate
Manually run the GitHub `Release (Python)` workflow with `publish=false` to build all five wheels, install the Linux release set on Python 3.10 and 3.14, check exact filenames and metadata, enforce PyPI's default per-file size limit, and retain one aggregate artifact with SHA-256 hashes. The run has no registry credentials; a dry run cannot enter either publication job.
Manually run the GitHub `Release (Python)` workflow with `publish=false` to build all six wheels, install the Linux release set on Python 3.10 and 3.14, check exact filenames and metadata, enforce PyPI's default per-file size limit, and retain one aggregate artifact with SHA-256 hashes. The run has no registry credentials; a dry run cannot enter either publication job.
Public publication runs from the private automation repository. Package metadata points to the separate read-only public source mirror, which does not run release Actions. The private repository defines the repository variable `PYPI_PUBLISHER_REPOSITORY` as its own `owner/name` and keeps `PUBLIC_PYPI_RELEASE_ENABLED=false` except during an intentional release.
+3 -3
View File
@@ -13,7 +13,7 @@ pnpm install
pnpm exec tsx scripts/build-exe-for-python-sdk.ts
```
所需 `lib/` 产物已存在时使用 `--skip-build`;如需选择平台,请使用 `--targets=node24-linux-x64,node24-linux-arm64,node24-macos-arm64,node24-win-x64`。每个目标都应在其原生架构上构建。产物写入 `dist-exe/`,脚本会将所选载体同步到 `python/sdk-runtime/`。Windows 会生成 `.exe` 与 `-rg.exe`;macOS 构建还会同步 `node-pty` 所需的配套 spawn 辅助程序。
所需 `lib/` 产物已存在时使用 `--skip-build`;如需选择平台,请使用 `--targets=node24-linux-x64,node24-linux-arm64,node24-macos-arm64,node24-macos-x64,node24-win-x64`。每个目标都应在其原生架构上构建。产物写入 `dist-exe/`,脚本会将所选载体同步到 `python/sdk-runtime/`。Windows 会生成 `.exe` 与 `-rg.exe`;macOS 构建还会同步 `node-pty` 所需的配套 spawn 辅助程序。
## 验证 SDK
@@ -79,11 +79,11 @@ pip install \
"dist-python/deepseek_harness_runtime_bin-$version-py3-none-macosx_14_0_arm64.whl"
```
运行时分发包仅提供 wheel 包。发布流水线会连同纯 SDK wheel 包一起发布四个平台 wheel 包:Linux x64、Linux arm64、macOS 14 或更高版本的 arm64,以及 Windows x64(`win_amd64`)。只有与仓库版本匹配时,才接受 `python-v<repository-version>` 标签;`0.0.1-rc.1` 之类的仓库预发布版本在 wheel 包文件名和元数据中使用规范化的 PEP 440 写法,例如 `0.0.1rc1`。
运行时分发包仅提供 wheel 包。发布流水线会连同纯 SDK wheel 包一起发布五个平台 wheel 包:Linux x64、Linux arm64、macOS 14 或更高版本的 arm64 与 x64,以及 Windows x64(`win_amd64`)。只有与仓库版本匹配时,才接受 `python-v<repository-version>` 标签;`0.0.1-rc.1` 之类的仓库预发布版本在 wheel 包文件名和元数据中使用规范化的 PEP 440 写法,例如 `0.0.1rc1`。
## 验证候选发行版
手动运行 GitHub 的 `Release (Python)` 工作流并设置 `publish=false`,即可构建全部五个 wheel 包,在 Python 3.10 和 3.14 上安装 Linux 发行集合,检查精确文件名和元数据,执行 PyPI 默认单文件大小限制,并保留一份带 SHA-256 哈希的汇总产物。该运行没有注册表凭据,dry-run 运行无法进入任何发布作业。
手动运行 GitHub 的 `Release (Python)` 工作流并设置 `publish=false`,即可构建全部六个 wheel 包,在 Python 3.10 和 3.14 上安装 Linux 发行集合,检查精确文件名和元数据,执行 PyPI 默认单文件大小限制,并保留一份带 SHA-256 哈希的汇总产物。该运行没有注册表凭据,dry-run 运行无法进入任何发布作业。
公开发布从私有自动化仓库运行。包元数据指向独立的只读公开源码镜像,该镜像不运行发布 Actions。私有仓库把仓库变量 `PYPI_PUBLISHER_REPOSITORY` 定义为自身的 `owner/name`,并且只在有意发布期间把 `PUBLIC_PYPI_RELEASE_ENABLED` 从 `false` 改为 `true`。
+2 -2
View File
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write python/sdk-runtime/README.md
README.md: 28695259928a7edc6e6cf67e737f1012729df5a4
README.zh.md: f23b253cfe47d9f1ae24568b51d9db810c7a4a9f
README.md: 050ae85d9b0a38b82a3c66a84c3d8f34e137be6c
README.zh.md: 7066d7224752294c25b58cfe8fb6a94524a2013d
+1 -1
View File
@@ -8,7 +8,7 @@ Platform runtime wheel for the DeepSeek Harness Python SDK. It packages the norm
The wheel installs a `dsh` console command and the `deepseek_harness_runtime` Python module. `dsh` forwards its arguments to the bundled executable and requires a non-empty `DSH_HOME`; it never falls back to `~/.dsh`.
Production executables are named `deepseek-harness-sdk-runtime-<platform>-<arch>` under the module's `runtime/` directory; Windows uses the `.exe` suffix. Linux and macOS wheels include a target-native `-rg` sidecar, Windows includes `-rg.exe`, and macOS also includes `-spawn-helper` for `node-pty`. Published targets are Linux x64, Linux arm64, macOS arm64, and Windows x64. The wheel tag and payload must match exactly; no Windows arm64 wheel is published.
Production executables are named `deepseek-harness-sdk-runtime-<platform>-<arch>` under the module's `runtime/` directory; Windows uses the `.exe` suffix. Linux and macOS wheels include a target-native `-rg` sidecar, Windows includes `-rg.exe`, and macOS also includes `-spawn-helper` for `node-pty`. Published targets are Linux x64, Linux arm64, macOS arm64, macOS x64, and Windows x64. The wheel tag and payload must match exactly; no Windows arm64 wheel is published.
Repository builds also materialize a dev-only `runtime/node/` carrier. It runs `node runtime/node/node_modules/@deepseek-ai/dsh/lib/bin.js` on system Node 22.19 or newer. It is never selected automatically and is excluded from wheels and sdists.
+1 -1
View File
@@ -8,7 +8,7 @@ DeepSeek Harness Python SDK 的平台运行时 wheel。它把普通 `dsh` CLI
Wheel 会安装 `dsh` 控制台命令和 `deepseek_harness_runtime` Python 模块。`dsh` 将参数转发给内置可执行程序,并要求非空 `DSH_HOME`;它不会回退到 `~/.dsh`。
生产可执行程序位于模块的 `runtime/` 目录,命名为 `deepseek-harness-sdk-runtime-<platform>-<arch>`;Windows 使用 `.exe` 后缀。Linux 与 macOS wheel 包含目标平台原生的 `-rg` 伴随程序,Windows 包含 `-rg.exe`,macOS 还包含 `node-pty` 使用的 `-spawn-helper`。已发布目标是 Linux x64、Linux arm64、macOS arm64 与 Windows x64。Wheel tag 必须与载荷严格匹配;不发布 Windows arm64 wheel。
生产可执行程序位于模块的 `runtime/` 目录,命名为 `deepseek-harness-sdk-runtime-<platform>-<arch>`;Windows 使用 `.exe` 后缀。Linux 与 macOS wheel 包含目标平台原生的 `-rg` 伴随程序,Windows 包含 `-rg.exe`,macOS 还包含 `node-pty` 使用的 `-spawn-helper`。已发布目标是 Linux x64、Linux arm64、macOS arm64、macOS x64 与 Windows x64。Wheel tag 必须与载荷严格匹配;不发布 Windows arm64 wheel。
仓库构建还会物化仅限开发的 `runtime/node/` 载体。它在系统 Node 22.19 或更高版本上运行 `node runtime/node/node_modules/@deepseek-ai/dsh/lib/bin.js`。系统不会自动选择它,而且 wheel 与 sdist 均不包含它。
+4
View File
@@ -11,6 +11,10 @@
"tag": "macosx_14_0_arm64",
"executable": "deepseek-harness-sdk-runtime-macos-arm64"
},
"macos-x64": {
"tag": "macosx_14_0_x86_64",
"executable": "deepseek-harness-sdk-runtime-macos-x64"
},
"win-x64": {
"tag": "win_amd64",
"executable": "deepseek-harness-sdk-runtime-win-x64.exe"
@@ -120,12 +120,11 @@ def _current_platform_tag() -> str:
plat is None
or arch is None
or (plat == "win" and arch != "x64")
or (plat == "macos" and arch != "arm64")
):
raise FileNotFoundError(
"no bundled DeepSeek Harness SDK runtime exists for this platform "
f"(sys.platform={sys.platform!r}, machine={platform.machine()!r}); supported: "
"Linux x64/arm64, macOS arm64, and Windows x64. " + _EXE_ACQUISITION_HINT
"Linux x64/arm64, macOS x64/arm64, and Windows x64. " + _EXE_ACQUISITION_HINT
)
return f"{plat}-{arch}"
@@ -16,8 +16,15 @@ checker = SimpleNamespace(**runpy.run_path(str(SCRIPT)))
def test_otool_parser_uses_the_newest_macho_slice() -> None:
output = """
Load command 8
cmd LC_VERSION_MIN_MACOSX
cmdsize 16
version 10.7
sdk 11.1
Load command 9
cmd LC_BUILD_VERSION
minos 11.0
Load command 10
cmd LC_BUILD_VERSION
minos 13.5
"""
@@ -26,12 +33,34 @@ def test_otool_parser_uses_the_newest_macho_slice() -> None:
def test_otool_parser_requires_a_deployment_target() -> None:
with pytest.raises(ValueError, match="contains no LC_BUILD_VERSION"):
with pytest.raises(ValueError, match="contains no macOS deployment target"):
checker.parse_otool_deployment_target("Load command 0\n")
def test_otool_parser_ignores_unrelated_version_fields() -> None:
output = """
Load command 1
cmd LC_ID_DYLIB
cmdsize 48
current version 14.1.0
compatibility version 1.0.0
"""
with pytest.raises(ValueError, match="contains no macOS deployment target"):
checker.parse_otool_deployment_target(output)
def test_wheel_tag_rejects_a_newer_executable_target() -> None:
checker.ensure_compatible(Path("runtime"), (13, 5), "macosx_14_0_arm64")
checker.ensure_compatible(Path("runtime-x64"), (10, 7), "macosx_14_0_x86_64")
with pytest.raises(RuntimeError, match="requires macOS 14.1"):
checker.ensure_compatible(Path("spawn-helper"), (14, 1), "macosx_14_0_arm64")
def test_wheel_tag_accepts_only_supported_macos_architectures() -> None:
assert checker.claimed_version("macosx_14_0_arm64") == (14, 0)
assert checker.claimed_version("macosx_14_0_x86_64") == (14, 0)
with pytest.raises(ValueError, match="unsupported macOS wheel platform tag"):
checker.claimed_version("macosx_14_0_universal2")
+3 -1
View File
@@ -60,6 +60,8 @@ def test_pep440_version_spells_a_prerelease_the_python_way() -> None:
def test_macos_wheel_tag_does_not_claim_unsupported_node_platforms() -> None:
assert build_python_release.PLATFORMS["macos-arm64"][0] == "macosx_14_0_arm64"
assert build_python_release.PLATFORMS["macos-arm64"][1] == "deepseek-harness-sdk-runtime-macos-arm64"
assert build_python_release.PLATFORMS["macos-x64"][0] == "macosx_14_0_x86_64"
assert build_python_release.PLATFORMS["macos-x64"][1] == "deepseek-harness-sdk-runtime-macos-x64"
def test_windows_wheel_tag_and_payload_are_x64_only() -> None:
@@ -95,7 +97,7 @@ def test_stage_sdk_keeps_distribution_module_and_runtime_pin_distinct(tmp_path:
@pytest.mark.parametrize(
("target", "with_helper"),
[("linux-x64", False), ("macos-arm64", True), ("win-x64.exe", False)],
[("linux-x64", False), ("macos-arm64", True), ("macos-x64", True), ("win-x64.exe", False)],
)
def test_stage_runtime_copies_platform_payload(
tmp_path: Path, target: str, with_helper: bool
+2 -3
View File
@@ -79,12 +79,11 @@ def test_current_platform_supports_windows_x64_only(monkeypatch: pytest.MonkeyPa
runtime._current_platform_tag()
def test_current_platform_rejects_macos_x64(monkeypatch: pytest.MonkeyPatch) -> None:
def test_current_platform_supports_macos_x64(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setattr(runtime.sys, "platform", "darwin")
monkeypatch.setattr(runtime.platform, "machine", lambda: "x86_64")
with pytest.raises(FileNotFoundError, match="macOS arm64"):
runtime._current_platform_tag()
assert runtime._current_platform_tag() == "macos-x64"
def test_runtime_requires_ripgrep_sidecar(
+13
View File
@@ -59,6 +59,19 @@ describe('Python runtime executable builder CLI', () => {
expect(result.stdout).not.toMatch(/pnpm\.cmd/i)
})
it('accepts the macOS x64 pkg target', () => {
const result = run(
{ npm_execpath: 'C:\\tools\\pnpm.cjs' },
'--skip-build',
'--dry-run',
'--targets=node24-macos-x64',
)
expect(result.status).toBe(0)
expect(result.stdout).toContain('exec pkg')
expect(result.stdout).toContain('--sea --targets node24-macos-x64')
})
it('rejects a Windows arm64 product before any build step', () => {
const result = run(
{ npm_execpath: 'C:\\tools\\pnpm.cjs' },
+1 -1
View File
@@ -207,7 +207,7 @@ class BuildCli {
return [
'Usage: pnpm exec tsx scripts/build-exe-for-python-sdk.ts [flags]',
'',
' --targets=<t1,t2,...> pkg targets, e.g. node24-linux-x64,node24-linux-arm64,node24-macos-arm64,node24-win-x64.',
' --targets=<t1,t2,...> pkg targets, e.g. node24-linux-x64,node24-linux-arm64,node24-macos-arm64,node24-macos-x64,node24-win-x64.',
' Default: the host platform only (on node24).',
' --skip-build skip `pnpm run build` (lib/ artifacts must already exist).',
' --dry-run print every command and config patch without executing.',
+26 -10
View File
@@ -12,7 +12,11 @@ from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
RELEASE = runpy.run_path(str(ROOT / "scripts" / "build-python-release.py"))
MACOS_PLATFORM_TAG = RELEASE["PLATFORMS"]["macos-arm64"][0]
MACOS_PLATFORMS = {
name: details[0]
for name, details in RELEASE["PLATFORMS"].items()
if name.startswith("macos-")
}
def parse_version(value: str) -> tuple[int, ...]:
@@ -24,7 +28,7 @@ def parse_version(value: str) -> tuple[int, ...]:
def claimed_version(platform_tag: str) -> tuple[int, ...]:
"""Return the minimum macOS version encoded by a wheel platform tag."""
match = re.fullmatch(r"macosx_(\d+)_(\d+)_arm64", platform_tag)
match = re.fullmatch(r"macosx_(\d+)_(\d+)_(?:arm64|x86_64)", platform_tag)
if match is None:
raise ValueError(f"unsupported macOS wheel platform tag: {platform_tag!r}")
return int(match.group(1)), int(match.group(2))
@@ -32,12 +36,22 @@ def claimed_version(platform_tag: str) -> tuple[int, ...]:
def parse_otool_deployment_target(output: str) -> tuple[int, ...]:
"""Return the newest deployment target from one or more Mach-O slices."""
versions = [
parse_version(match.group(1))
for match in re.finditer(r"^\s*minos\s+(\d+(?:\.\d+)*)\s*$", output, re.MULTILINE)
]
versions: list[tuple[int, ...]] = []
command: str | None = None
for line in output.splitlines():
stripped = line.strip()
if re.fullmatch(r"Load command \d+", stripped):
command = None
elif stripped == "cmd LC_BUILD_VERSION":
command = "build"
elif stripped == "cmd LC_VERSION_MIN_MACOSX":
command = "minimum"
elif command == "build" and (match := re.fullmatch(r"minos\s+(\d+(?:\.\d+)*)", stripped)):
versions.append(parse_version(match.group(1)))
elif command == "minimum" and (match := re.fullmatch(r"version\s+(\d+(?:\.\d+)*)", stripped)):
versions.append(parse_version(match.group(1)))
if not versions:
raise ValueError("otool output contains no LC_BUILD_VERSION deployment target")
raise ValueError("otool output contains no macOS deployment target load command")
return max(versions)
@@ -73,7 +87,7 @@ def ensure_compatible(
def validate_deployment_targets(
executables: list[Path], platform_tag: str = MACOS_PLATFORM_TAG
executables: list[Path], platform_tag: str
) -> list[tuple[Path, tuple[int, ...]]]:
"""Validate every executable and return its measured deployment target."""
measured = [(executable, deployment_target(executable)) for executable in executables]
@@ -84,11 +98,13 @@ def validate_deployment_targets(
def main() -> None:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--platform", choices=tuple(MACOS_PLATFORMS), required=True)
parser.add_argument("executables", type=Path, nargs="+")
args = parser.parse_args()
for executable, version in validate_deployment_targets(args.executables):
platform_tag = MACOS_PLATFORMS[args.platform]
for executable, version in validate_deployment_targets(args.executables, platform_tag):
rendered = ".".join(str(part) for part in version)
print(f"{executable}: macOS {rendered} <= {MACOS_PLATFORM_TAG}")
print(f"{executable}: macOS {rendered} <= {platform_tag}")
if __name__ == "__main__":
+29 -8
View File
@@ -370,7 +370,7 @@ describe('CI workflow', () => {
name: 'python runtime / release-shaped matrix',
uses: './.github/workflows/build-exe-for-python-sdk.yml',
with: {
targets: 'node24-linux-x64,node24-linux-arm64,node24-macos-arm64,node24-win-x64',
targets: 'node24-linux-x64,node24-linux-arm64,node24-macos-arm64,node24-macos-x64,node24-win-x64',
ci: true,
},
secrets: {
@@ -462,7 +462,7 @@ describe('Python release workflows', () => {
expect(build).toMatchObject({
uses: './.github/workflows/build-exe-for-python-sdk.yml',
with: {
targets: 'node24-linux-x64,node24-linux-arm64,node24-macos-arm64,node24-win-x64',
targets: 'node24-linux-x64,node24-linux-arm64,node24-macos-arm64,node24-macos-x64,node24-win-x64',
release: true,
},
})
@@ -531,7 +531,7 @@ describe('Python release workflows', () => {
const buildSteps: unknown[] = build.steps
const manylinuxAddon = buildSteps.find(step => isRecord(step) && step.name === 'Rebuild Linux node-pty against manylinux 2.28')
const macosCheck = buildSteps.find(step => isRecord(step) && step.name === 'Check macOS deployment target')
const macosCheck = buildSteps.find(step => isRecord(step) && step.name === 'Check macOS payload architecture and deployment target')
const manylinuxSmoke = buildSteps.find(step => isRecord(step) && step.name === 'Run wheel in a manylinux 2.28 container')
const cleanVenvPosix = buildSteps.find(step => isRecord(step) && step.name === 'Install local SDK and runtime wheels into a clean venv (POSIX)')
const cleanVenvWindows = buildSteps.find(step => isRecord(step) && step.name === 'Install local SDK and runtime wheels into a clean venv (Windows)')
@@ -541,7 +541,8 @@ describe('Python release workflows', () => {
const realApiPreflightWindows = buildSteps.find(step => isRecord(step) && step.name === 'Preflight installed-wheel real API test (Windows)')
const installedRealApiPosix = buildSteps.find(step => isRecord(step) && step.name === 'Run installed-wheel real API black-box test (POSIX)')
const installedRealApiWindows = buildSteps.find(step => isRecord(step) && step.name === 'Run installed-wheel real API black-box test (Windows)')
if (!isRecord(cleanVenvPosix) || !isRecord(cleanVenvWindows)
if (!isRecord(macosCheck) || typeof macosCheck.run !== 'string'
|| !isRecord(cleanVenvPosix) || !isRecord(cleanVenvWindows)
|| !isRecord(installedKeylessPosix) || !isRecord(installedKeylessWindows)
|| !isRecord(realApiPreflightPosix) || !isRecord(realApiPreflightWindows)
|| !isRecord(installedRealApiPosix) || !isRecord(installedRealApiWindows)) {
@@ -564,6 +565,9 @@ describe('Python release workflows', () => {
expect(JSON.stringify(plan.steps)).toContain('pep440_version')
const workflowJson = JSON.stringify(workflow)
expect(workflowJson).toContain('macosx_14_0_arm64')
expect(workflowJson).toContain('macosx_14_0_x86_64')
expect(workflowJson).toContain('node24-macos-x64')
expect(workflowJson).toContain('macos-15-intel')
expect(workflowJson).toContain('win_amd64')
expect(workflowJson).toContain('node24-win-x64')
expect(workflowJson).toContain('windows-2025')
@@ -583,8 +587,10 @@ describe('Python release workflows', () => {
expect(JSON.stringify(manylinuxAddon)).toContain('node-pty-glibc-versions.txt')
expect(JSON.stringify(manylinuxAddon)).toContain('le 2.28')
expect(macosCheck).toMatchObject({ if: "runner.os == 'macOS'" })
expect(JSON.stringify(macosCheck)).toContain('scripts/check-macos-deployment-target.py')
expect(JSON.stringify(macosCheck)).toContain('$EXE-spawn-helper')
expect(macosCheck.run).toContain('scripts/check-macos-deployment-target.py')
expect(macosCheck.run).toContain('lipo "$payload" -verify_arch')
expect(macosCheck.run).toContain('$EXE-rg')
expect(macosCheck.run).toContain('$EXE-spawn-helper')
expect(JSON.stringify(installedKeylessPosix)).toContain('--scenario all')
expect(JSON.stringify(installedKeylessPosix)).toContain('env -u PYTHONPATH')
expect(JSON.stringify(installedKeylessWindows)).toContain('--scenario all --installed-wheel')
@@ -620,14 +626,29 @@ describe('Python release workflows', () => {
}
const runtimeScript: unknown[] = runtimeWheel.script
const macosCheck = runtimeScript.find(
step => typeof step === 'string' && step.includes('PLATFORM" = macos-arm64'),
step => typeof step === 'string' && step.includes('${PLATFORM#macos-}'),
)
if (typeof macosCheck !== 'string') {
throw new TypeError('GitLab CI must check the macOS deployment target')
}
expect(macosCheck).toContain('scripts/check-macos-deployment-target.py')
expect(macosCheck).toContain('"$EXE" "$EXE-spawn-helper"')
expect(macosCheck).toContain('lipo "$payload" -verify_arch')
expect(macosCheck).toContain('"$EXE" "$EXE-rg" "$EXE-spawn-helper"')
})
it('builds the macOS x64 wheel on the matching GitLab runner', () => {
const workflow = loadWorkflow('.gitlab-ci.yml')
const macosX64 = workflow['runtime-macos-x64']
const publish = workflow['publish-python']
if (!isRecord(macosX64) || !isRecord(publish) || !Array.isArray(publish.needs)) {
throw new TypeError('GitLab CI must define the macOS x64 runtime and publication jobs')
}
expect(macosX64.tags).toEqual(['macos-x64'])
expect(macosX64.variables).toMatchObject({ PKG_TARGET: 'node24-macos-x64', PLATFORM: 'macos-x64' })
expect(publish.needs).toContainEqual({ job: 'runtime-macos-x64', artifacts: true })
expect(JSON.stringify(publish.script)).toContain('macosx_14_0_x86_64.whl')
})
it('builds and black-box tests the Windows x64 wheel in GitLab', () => {
+4 -3
View File
@@ -21,6 +21,7 @@ const platforms = {
'linux-x64': { tag: 'manylinux_2_28_x86_64', executable: 'runtime-linux-x64' },
'linux-arm64': { tag: 'manylinux_2_28_aarch64', executable: 'runtime-linux-arm64' },
'macos-arm64': { tag: 'macosx_14_0_arm64', executable: 'runtime-macos-arm64' },
'macos-x64': { tag: 'macosx_14_0_x86_64', executable: 'runtime-macos-x64' },
'win-x64': { tag: 'win_amd64', executable: 'runtime-win-x64.exe' },
}
@@ -64,7 +65,7 @@ describe('verifyRuntimeClosure', () => {
expect(result.presetCount).toBe(1)
expect(result.failures).toEqual([
'standard preset -> @scope/linux (linux-arm64, linux-x64)',
'standard preset -> @scope/macos (macos-arm64)',
'standard preset -> @scope/macos (macos-arm64, macos-x64)',
'standard preset -> @scope/windows (win-x64)',
])
})
@@ -83,7 +84,7 @@ describe('verifyRuntimeClosure', () => {
const result = await verifyRuntimeClosure(root)
expect(result.failures).toEqual([
'standard preset -> @scope/conditional (linux-arm64, linux-x64, macos-arm64, win-x64)',
'standard preset -> @scope/conditional (linux-arm64, linux-x64, macos-arm64, macos-x64, win-x64)',
])
})
@@ -117,7 +118,7 @@ describe('verifyRuntimeClosure', () => {
const result = await verifyRuntimeClosure(root)
expect(result.failures).toEqual([
'standard preset -> @scope/plugin [runtime dependency is "1.2.3"; expected workspace:] (linux-arm64, linux-x64, macos-arm64, win-x64)',
'standard preset -> @scope/plugin [runtime dependency is "1.2.3"; expected workspace:] (linux-arm64, linux-x64, macos-arm64, macos-x64, win-x64)',
])
})