Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 33 additions & 2 deletions docs/en/docs/integrations/dsh.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ description: Install the PowerContext DeepSeek Harness plugin and control its lo

## Install matching Server and plugin versions

Install DeepSeek Harness and make sure its Web profile is available. The real-host acceptance suite pins DSH
Install DeepSeek Harness Web or Desktop. The real-host acceptance suite pins DSH
0.1.2-rc.1. Choose one PowerContext installation path and keep the Server and plugin together.

For this guided-setup build:
Expand All @@ -36,7 +36,38 @@ commands. A local source must contain the checked-in built `lib/index.js`.
`setup dsh --source oceanbase/powercontext --ref master` reuses a valid cached checkout without fetching:
repeating that command does not update a moving branch. A broken checkout is replaced.

`setup dsh` calls `dsh plugin --profile web add`; it does not start the Server. Restart DSH after installation.
`powercontext setup dsh` asks for `web` or `desktop` in an interactive terminal. Without a terminal, or with
`--json`, an omitted profile defaults to `web`. Explicit selection never prompts:

```bash
powercontext setup dsh --profile web
powercontext setup dsh --profile desktop
powercontext doctor dsh --profile desktop
```

Web installs under `$DSH_HOME/profiles/web`; Desktop installs under `$DSH_HOME/profiles/desktop`
(`DSH_HOME` defaults to `~/.dsh`). Setup calls `dsh plugin --profile <profile> add`, which installs the package
and enables its bundle in that profile's `package.json`. The bundle supplies its own patch. Setup preserves
both profiles' user `cordis.patch.yml` files and does not enable the plugin in the other profile.

Desktop requires its **Desktop-installed command**, not npm/pnpm dsh. Open Desktop once to initialize its
profile, use **Manage dsh Command…** to install the command, open a new terminal, then fully quit Desktop
before setup. Closing the window may only hide it. Setup searches PATH for the Desktop carrier even when
an npm shim comes first. If it is not registered on PATH, pass its installed launcher explicitly:

```powershell
powercontext setup dsh --profile desktop --dsh-command "D:\Apps\DeepSeek Harness\resources\runtime\cli\bin\dsh.cmd"
```

On macOS the launcher is `DeepSeek Harness.app/Contents/Resources/runtime/cli/bin/dsh`.
`--dsh-command` also works with `doctor dsh`. Packaged Windows/macOS Desktop layouts are supported;
unpackaged development launchers are not. Desktop inspection uses the application's Electron runtime and
configuration APIs without booting plugins. Registration success does not establish running-host readiness:
reopen Desktop and run `/pc doctor`. Setup does not start the Server or quit Desktop for you.

Saved `hosts.dsh` connection settings and `$DSH_HOME/powercontext/credentials.json` are shared between Web
and Desktop. Use the same Server for both; changing the shared URL affects either profile that has no native
override. Credentials remain bound to their saved Server URL. Install into each profile separately if you use both.

Existing UI and model patches can remain in place. Setup checks the composed PowerContext connection settings;
see [remote connection configuration](../operate/connect-remote-server.md) for conflicts and dynamic configuration limits.
Expand Down
34 changes: 32 additions & 2 deletions docs/zh/docs/integrations/dsh.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ description: 安装 PowerContext DeepSeek Harness 插件并控制其本地行为

## 安装匹配的 Server 和插件

先安装 DeepSeek Harness,并确保 Web profile 可用。真实宿主验收固定使用 DSH 0.1.2-rc.1。
先安装 DeepSeek Harness Web 或 Desktop。真实宿主验收固定使用 DSH 0.1.2-rc.1。
选择以下一种 PowerContext 安装方式,让 Server 和插件保持匹配。

使用本站对应的配置向导版本:
Expand All @@ -36,7 +36,37 @@ powercontext setup dsh --source ./powercontext-dsh-dev
`setup dsh --source oceanbase/powercontext --ref master` 会直接复用有效缓存 checkout,不会 fetch;
重复运行不代表更新了移动分支。只有残缺 checkout 会被替换。

`setup dsh` 调用 `dsh plugin --profile web add`,不会启动 Server。安装完成后重启 DSH。
在交互终端中运行 `powercontext setup dsh` 会询问安装到 `web` 还是 `desktop`。
非交互模式或使用 `--json` 时,未指定 profile 默认安装到 `web`。显式指定目标不会询问:

```bash
powercontext setup dsh --profile web
powercontext setup dsh --profile desktop
powercontext doctor dsh --profile desktop
```

Web 安装目录为 `$DSH_HOME/profiles/web`,Desktop 为 `$DSH_HOME/profiles/desktop`;
`DSH_HOME` 默认是 `~/.dsh`。Setup 调用 `dsh plugin --profile <profile> add`,在对应 profile 的
`package.json` 中安装依赖并启用 bundle,再加载插件包自带的 patch。两个 profile 的用户
`cordis.patch.yml` 均会保留,安装不会改变另一个 profile 的插件启用状态。

Desktop 必须使用**桌面版自带的 dsh 命令**,npm/pnpm 版 dsh 无权管理 desktop profile。
请先启动一次 Desktop 初始化配置,通过 **Manage dsh Command…** 菜单安装命令并打开新终端,
然后完全退出 Desktop 再安装插件;关闭窗口可能只是隐藏。即使 npm 命令排在前面,Setup 也会继续
在 PATH 中查找 Desktop 启动器。未注册 PATH 时可以显式指定:

```powershell
powercontext setup dsh --profile desktop --dsh-command "D:\Apps\DeepSeek Harness\resources\runtime\cli\bin\dsh.cmd"
```

macOS 启动器位于 `DeepSeek Harness.app/Contents/Resources/runtime/cli/bin/dsh`。
`doctor dsh` 同样支持 `--dsh-command`。支持 Windows/macOS 的打包桌面版布局,不支持未打包的开发启动器。
配置检查使用桌面版自带的 Electron 和配置解析器,不启动插件。注册成功不代表运行中的宿主和 Server
已经就绪;安装后重新打开 Desktop,在会话中执行 `/pc doctor`。Setup 不会替你启动 Server 或退出 Desktop。

Web 和 Desktop 共用已保存的 `hosts.dsh` 连接设置及 `$DSH_HOME/powercontext/credentials.json`。
建议两者连接同一个 Server;修改共享 URL 会影响没有原生覆盖配置的 profile,凭据仍绑定原来的 Server URL。
同时使用 Web 和 Desktop 时,分别执行两个 profile 的安装命令。

已有界面和模型 patch 可以保留。setup 检查合成后的 PowerContext 连接设置;
冲突处理及动态配置限制见[远程连接配置](../operate/connect-remote-server.md)。
Expand Down
10 changes: 10 additions & 0 deletions integrations/dsh/plugins/powercontext/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,3 +79,13 @@ Its displayed text is the same untrusted, request-specific context sent to the m

See [runtime acceptance tests](tests/runtime/README.md) for the pinned real DSH host, deterministic CI scenarios,
and the separate real-model and Web acceptance procedure.

## Web and Desktop setup targets

`powercontext setup dsh` prompts for Web or Desktop on a TTY; `--json` and non-interactive invocations default to Web.
Use `--profile web` or `--profile desktop` explicitly in scripts. Each profile has independent package dependencies
and bundle activation. Desktop requires its installed dsh launcher, an initialized profile, and a fully quit application.
Use Desktop's **Manage dsh Command…** menu or pass `--dsh-command` with its installed launcher path.
`powercontext doctor dsh --profile desktop` checks registration without launching the reserved profile; reopen
Desktop and run `/pc doctor` to inspect the active session. Connection settings and URL-bound credentials remain shared.
See [the installation guide](../../../../docs/en/docs/integrations/dsh.md) for paths and complete examples.
61 changes: 51 additions & 10 deletions src/powercontext/cli/dsh.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
from pathlib import Path
from shutil import which

from powercontext.cli.dsh_runtime import DshProfile, DshTarget, dsh_home
from powercontext.cli.git_source import (
InvalidGitHubSourceError,
clone_github_source,
Expand All @@ -49,14 +50,22 @@ class DshSetupResult:
plugin_path: str
data_dir: str
authorization_state: str = "not_attempted"
profile: str = "web"
profile_dir: str = ""


def install_dsh_plugin(
*, source: str, ref: str, server_url: str = "http://127.0.0.1:8000", allow_insecure_http: bool = False
*,
source: str,
ref: str,
server_url: str = "http://127.0.0.1:8000",
allow_insecure_http: bool = False,
target: DshTarget | None = None,
) -> DshSetupResult:
"""Install the plugin from a PowerContext checkout or Git source."""

dsh_executable()
profile = target.profile if target else DshProfile.WEB
executable = target.command if target and target.command else dsh_executable()
data_dir = powercontext_data_dir()
try:
data_dir.mkdir(parents=True, exist_ok=True)
Expand All @@ -67,13 +76,13 @@ def install_dsh_plugin(
from powercontext.cli.dsh_transport import read_dsh_settings, validate_dsh_setup_transport

try:
settings = read_dsh_settings(profile=DSH_PROFILE, candidate=plugin_dir, prospective=True)
settings = read_dsh_settings(profile=profile, candidate=plugin_dir, prospective=True, executable=executable)
validate_dsh_setup_transport(settings, server_url, allow_insecure_http)
except ValueError as error:
raise SetupError(str(error)) from error
_run_dsh("plugin", "--profile", DSH_PROFILE, "add", str(plugin_dir))
_run_dsh("plugin", "--profile", profile, "add", str(plugin_dir), **({"executable": executable} if target else {}))
try:
settings = read_dsh_settings(profile=DSH_PROFILE, require_installed=True)
settings = read_dsh_settings(profile=profile, executable=executable, require_installed=True)
validate_dsh_setup_transport(settings, server_url, allow_insecure_http)
except ValueError as error:
raise SetupError( # noqa: TRY003 - actionable host-specific readback failure.
Expand All @@ -89,6 +98,8 @@ def install_dsh_plugin(
plugin=DSH_PLUGIN_NAME,
plugin_path=str(plugin_dir),
data_dir=str(data_dir),
profile=profile,
profile_dir=str(dsh_home() / "profiles" / profile),
authorization_state=configure_stored_authorization(
"dsh",
server_url=server_url,
Expand Down Expand Up @@ -123,11 +134,11 @@ def require_built_plugin(path: Path) -> None:
raise SetupError.unbuilt_dsh_plugin(path)


def run_dsh_diagnostics() -> dict[str, Diagnostic]:
def run_dsh_diagnostics(*, target: DshTarget | None = None) -> dict[str, Diagnostic]:
"""Collect diagnostics for the optional DeepSeek Harness integration."""

try:
executable = dsh_executable()
executable = target.command if target and target.command else dsh_executable()
except SetupError:
return {
"dsh": Diagnostic(
Expand All @@ -139,8 +150,10 @@ def run_dsh_diagnostics() -> dict[str, Diagnostic]:
detail="not checked because DeepSeek Harness CLI is unavailable",
),
}
if target and target.profile == DshProfile.DESKTOP:
return _desktop_diagnostics(target)
try:
output = _run_dsh("--profile", DSH_PROFILE, "--dump-config")
output = _run_dsh("--profile", DSH_PROFILE, "--dump-config", **({"executable": executable} if target else {}))
except SetupError as error:
cause = error.__cause__
checks = {"dump_config": "failed"}
Expand Down Expand Up @@ -188,6 +201,34 @@ def run_dsh_diagnostics() -> dict[str, Diagnostic]:
}


def _desktop_diagnostics(target: DshTarget) -> dict[str, Diagnostic]:
"""Compose registration with Desktop's parser without booting its reserved profile."""
from powercontext.cli.dsh_transport import read_dsh_settings

try:
read_dsh_settings(profile=target.profile, executable=target.command, require_installed=True)
except ValueError as error:
plugin = Diagnostic(status=DiagnosticStatus.FAILED, detail=str(error))
else:
plugin = Diagnostic(
status=DiagnosticStatus.OK,
detail="powercontext-dsh is registered in the Desktop profile. Reopen Desktop and run /pc doctor "
"inside that session to check the running host and Server.",
checks={
"registration": "present",
"running_host_configuration": "not_observed",
"server_liveness": "not_checked",
"server_readiness": "not_checked",
"route_compatibility": "not_checked",
"session_scope": "not_checked",
},
)
return {
"dsh": Diagnostic(status=DiagnosticStatus.OK, detail=target.command or "Desktop-installed dsh"),
"plugin": plugin,
}


def dsh_executable() -> str:
"""Return a subprocess-launchable DeepSeek Harness CLI path."""

Expand Down Expand Up @@ -270,8 +311,8 @@ def _clone_github_source(source: str, ref: str, target: Path) -> None:
raise SetupError.invalid_dsh_source() from None


def _run_dsh(*arguments: str) -> str:
command = [dsh_executable(), *arguments]
def _run_dsh(*arguments: str, executable: str | None = None) -> str:
command = [executable or dsh_executable(), *arguments]
try:
completed = subprocess.run( # noqa: S603 - arguments are passed directly to the fixed dsh executable.
command,
Expand Down
12 changes: 8 additions & 4 deletions src/powercontext/cli/dsh_config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -64,8 +64,12 @@ function installation(executable) {
fail('Cannot locate the installed DSH package from its CLI; use a supported npm/pnpm DSH installation')
}

async function inspect(executable, home, profile, candidate, prospective, requireInstalled) {
const anchor = installation(executable)
async function inspect(executable, home, profile, candidate, prospective, desktopAnchor, requireInstalled) {
const anchor = desktopAnchor || installation(executable)
if (desktopAnchor) {
const manifest = read(anchor, () => JSON.parse(readFileSync(anchor, 'utf8')))
if (manifest.name !== '@deepseek-ai/dsh') fail('Invalid Desktop runtime manifest', anchor)
}
const require = createRequire(anchor)
// These are the selected host's APIs, not dependencies of the plugin being installed.
// Resolving a different copy from PowerContext would inspect a different host contract.
Expand Down Expand Up @@ -288,8 +292,8 @@ async function inspect(executable, home, profile, candidate, prospective, requir
}

try {
const [executable, home, profile, candidate, prospective, requireInstalled] = process.argv.slice(2)
const result = await inspect(resolve(executable), resolve(home), profile, candidate || undefined, prospective === 'true', requireInstalled === 'true')
const [executable, home, profile, candidate, prospective, desktopAnchor, requireInstalled] = process.argv.slice(2)
const result = await inspect(resolve(executable), resolve(home), profile, candidate || undefined, prospective === 'true', desktopAnchor, requireInstalled === 'true')
process.stdout.write(JSON.stringify(result))
} catch (error) {
// Native parser messages and stacks can include credentials or whole YAML rows.
Expand Down
83 changes: 82 additions & 1 deletion src/powercontext/cli/dsh_runtime.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,15 +12,96 @@
# See the License for the specific language governing permissions and
# limitations under the License.

"""Locate DSH's shared configuration and credential home."""
"""Select a DSH profile and its installation-owned command/runtime."""

from __future__ import annotations

import os
from dataclasses import dataclass
from enum import StrEnum
from pathlib import Path


class DshProfile(StrEnum):
WEB = "web"
DESKTOP = "desktop"


def dsh_home() -> Path:
"""Use the same home for profile configuration and URL-bound credentials."""
configured = os.environ.get("DSH_HOME", "")
return Path(configured if configured.strip() else Path.home() / ".dsh").expanduser()


@dataclass(frozen=True)
class DesktopRuntime:
"""The signed application supplies both Electron and the ASAR package tree."""

electron: Path
anchor: Path


def desktop_runtime(command: str) -> DesktopRuntime | None:
"""Recognize installed Windows/macOS launchers, including command symlinks.

Do not execute launcher contents or substitute an npm runtime for Desktop.
Python cannot stat files inside ASAR; the Electron inspection helper validates
the manifest before importing the installed configuration APIs.
"""
path = Path(command).expanduser().resolve()
if not path.is_file() or path.name not in {"dsh", "dsh.cmd"}:
return None
if tuple(part.lower() for part in path.parts[-5:-1]) != ("resources", "runtime", "cli", "bin"):
return None
resources = path.parents[3]
electron = (
resources.parent / "DeepSeek Harness.exe"
if path.suffix == ".cmd"
else resources.parent / "MacOS" / "DeepSeek Harness"
)
archive = resources / "app.asar"
if not electron.is_file() or not archive.is_file():
return None
return DesktopRuntime(electron, archive / "dsh/node_modules/@deepseek-ai/dsh/package.json")


@dataclass(frozen=True)
class DshTarget:
profile: DshProfile
command: str | None = None

@property
def directory(self) -> Path:
return dsh_home() / "profiles" / self.profile


def resolve_dsh_target(profile: DshProfile, command: Path | None = None) -> DshTarget:
"""Resolve an explicit command or find the selected carrier on PATH."""
from powercontext.cli.system import SetupError

if command is not None:
selected = str(command.expanduser().resolve())
if not Path(selected).is_file():
raise SetupError( # noqa: TRY003 - actionable host-specific setup failures.
f"DSH command does not exist: {selected}"
)
elif profile == DshProfile.WEB:
# Preserve Web's transport preflight before requiring a local CLI.
selected = None
else:
filename = "dsh.cmd" if os.name == "nt" else "dsh"
candidates = [str(Path(directory) / filename) for directory in os.get_exec_path() if directory]
selected = next((candidate for candidate in candidates if desktop_runtime(candidate)), "")
if profile == DshProfile.DESKTOP and not desktop_runtime(selected or ""):
raise SetupError( # noqa: TRY003 - actionable host-specific setup failures.
"Desktop setup requires the Desktop-installed dsh command; npm/pnpm dsh cannot manage its profile. "
"Use Desktop's Manage dsh Command menu, open a new terminal, or pass "
"--dsh-command /path/to/Desktop/resources/runtime/cli/bin/dsh (dsh.cmd on Windows)."
)
target = DshTarget(profile, selected)
if profile == DshProfile.DESKTOP and not (target.directory / "package.json").is_file():
raise SetupError( # noqa: TRY003 - actionable host-specific setup failures.
f"Desktop profile is not initialized: {target.directory}. "
"Open Desktop once, then fully quit it before rerunning setup."
)
return target
Loading
Loading