feat(external-agents): add dsh profile for DeepSeek Harness headless

This commit is contained in:
RIC
2026-08-17 15:26:59 +08:00
parent 1750c50c26
commit 82be975d28
4 changed files with 145 additions and 0 deletions
@@ -437,5 +437,20 @@ class ExternalAgentRegistry:
"history_message_count": 10,
"timeout": 900,
},
{
"name": "dsh",
"aliases": ["deepseek-harness", "dsh-headless"],
"type": "cli",
"icon_svg": "",
"enabled": False,
"description": "DeepSeek Harnessheadless 一次性任务,打印最终答案后退出)",
"command": "dsh",
"args": ["--profile", "headless", "{prompt}"],
"working_dir": "",
"inherit_env": ["DEEPSEEK_API_KEY", "DEEPSEEK_BASE_URL", "DSH_HOME"],
"session_mode": "history",
"history_message_count": 10,
"timeout": 900,
},
],
}
+87
View File
@@ -0,0 +1,87 @@
"""外部编码工具 dsh profile 回归测试。
覆盖:新工作区默认配置自动带出 dsh默认禁用dsh 走通用 CLI 适配器
history 模式、不注入 --session-id别名解析禁用态保护。
"""
import json
import sys
from pathlib import Path
import pytest
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
from backend.modules.external_agents.adapters.cli import CliExternalAgentAdapter
from backend.modules.external_agents.base import ExternalAgentRequest
from backend.modules.external_agents.conversation import (
profile_supports_native_session,
resolve_effective_session_mode,
)
from backend.modules.external_agents.registry import ExternalAgentRegistry
def _load_dsh_profile(tmp_path):
"""在新工作区创建 registry会写出默认配置返回解析后的 dsh profile。"""
registry = ExternalAgentRegistry(workspace=tmp_path)
return registry, registry._load_profiles()["dsh"]
def test_default_config_contains_dsh_profile(tmp_path):
"""新工作区首次生成的 external_coding_tools.json 自动带出 dsh默认禁用"""
registry = ExternalAgentRegistry(workspace=tmp_path)
config = json.loads(
(tmp_path / ExternalAgentRegistry.CONFIG_FILENAME).read_text(encoding="utf-8")
)
profiles = {item["name"]: item for item in config["profiles"]}
dsh = profiles.get("dsh")
assert dsh is not None
assert dsh["enabled"] is False
assert dsh["type"] == "cli"
assert dsh["command"] == "dsh"
assert dsh["args"] == ["--profile", "headless", "{prompt}"]
assert dsh["session_mode"] == "history"
assert "stdin_template" not in dsh # dsh 从 argv 读任务,不走 stdin
assert dsh["timeout"] == 900
assert dsh["inherit_env"] == ["DEEPSEEK_API_KEY", "DEEPSEEK_BASE_URL", "DSH_HOME"]
def test_dsh_profile_uses_history_session_mode(tmp_path):
"""dsh 解析为 history 模式,且不会启用 native 会话。"""
_, profile = _load_dsh_profile(tmp_path)
assert profile.type == "cli"
assert resolve_effective_session_mode(profile) == "history"
assert profile_supports_native_session(profile) is False
def test_dsh_aliases_resolve_to_canonical_name(tmp_path):
"""dsh 及其别名均可解析到规范名。"""
registry, _ = _load_dsh_profile(tmp_path)
assert registry.resolve_profile_name("dsh") == "dsh"
assert registry.resolve_profile_name("deepseek-harness") == "dsh"
assert registry.resolve_profile_name("dsh-headless") == "dsh"
def test_dsh_argv_does_not_inject_session_id(tmp_path):
"""dsh 的 argv 形如 dsh --profile headless <prompt>,不注入 --session-id。"""
registry, profile = _load_dsh_profile(tmp_path)
adapter = CliExternalAgentAdapter()
request = ExternalAgentRequest(
task="hello",
prompt="hello",
workspace=tmp_path,
working_dir=tmp_path,
session_id="session-1", # 即使携带 session_idhistory 模式也不注入
)
argv = adapter._build_argv(
"dsh", profile, request, adapter._build_template_variables(request)
)
assert argv == ["dsh", "--profile", "headless", "hello"]
assert "--session-id" not in argv
def test_disabled_dsh_profile_is_rejected(tmp_path):
"""dsh 默认禁用:未启用时 resolve_profile 应明确拒绝。"""
registry, _ = _load_dsh_profile(tmp_path)
with pytest.raises(ValueError, match="disabled"):
registry.resolve_profile("dsh")
+12
View File
@@ -447,6 +447,18 @@ POST /api/settings/external-coding-tools/check
}
```
### DeepSeek Harnessdsh配置说明
dshDeepSeek Harness是 DeepSeek 官方开源的 Agent 框架,其 headless 模式适合作为一次性编码工具接入。新工作区首次生成 `external_coding_tools.json` 时会自动带出 dsh profile默认 `enabled: false`**老工作区不会自动出现,需在设置页手动新增,或手改 json 添加**。
- 调用契约:`dsh --profile headless "<task>"``--profile headless` 必须写在最前,任务从命令行参数读取,不从 stdin 读取,因此**不要配置 `stdin_template`**。
- 行为:每次运行创建一个全新的一次性会话,任务完成后把最后一条非空 assistant 文本写到 stdout 并退出;正常完成退出码 0出错含启动失败、初始化失败、最终原因为 error退出码非 0。
- 工作目录dsh 把当前工作目录当作默认 workspace 根,直接沿用 CountBot 传入的 `working_dir`(子进程 cwd无需 `{working_dir}` 占位符。
- 会话headless 不支持续会话,`session_mode` 固定用 `history`(或 `stateless`**不要用 `native`**。
- 凭证dsh 默认从 `~/.dsh/.credentials.yaml` 读取凭证(由 `dsh-credentials-local` 提供),不依赖环境变量;`inherit_env` 里的 `DEEPSEEK_API_KEY` / `DEEPSEEK_BASE_URL` / `DSH_HOME` 只是可选的 env 替代方式,未设置不代表不可用。
- 首次运行headless 首次使用会向 `$DSH_HOME/profiles/headless` 写初始化文件,要求子进程有可写的 `HOME` / `DSH_HOME`(默认 `~/.dsh`);请确保 CountBot 与 dsh 以同一用户运行,否则会以 `EPERM` 失败。
- 前置条件:机器上 `dsh` 在 PATH 中Node CLI一般 `npm i -g @deepseek-ai/dsh`),且已有可用凭证。
## 10. 导入导出
图形化路径:
+31
View File
@@ -87,6 +87,37 @@
"success_exit_codes": [
0
]
},
{
"name": "dsh",
"aliases": [
"deepseek-harness",
"dsh-headless"
],
"type": "cli",
"icon_svg": "",
"enabled": false,
"description": "DeepSeek Harnessheadless 一次性任务,打印最终答案后退出)",
"command": "dsh",
"args": [
"--profile",
"headless",
"{prompt}"
],
"working_dir": "",
"stdin_template": null,
"env": {},
"inherit_env": [
"DEEPSEEK_API_KEY",
"DEEPSEEK_BASE_URL",
"DSH_HOME"
],
"session_mode": "history",
"history_message_count": 10,
"timeout": 900,
"success_exit_codes": [
0
]
}
]
}