From 82be975d28b2335b6b62a00a659f4c624fbf25c8 Mon Sep 17 00:00:00 2001 From: RIC <80094642+ddggkkcc@users.noreply.github.com> Date: Mon, 17 Aug 2026 15:26:59 +0800 Subject: [PATCH] feat(external-agents): add dsh profile for DeepSeek Harness headless --- backend/modules/external_agents/registry.py | 15 ++++ tests/test_external_coding_dsh_profile.py | 87 +++++++++++++++++++++ workspace/AI_QUICK_REFERENCE.md | 12 +++ workspace/external_coding_tools.json | 31 ++++++++ 4 files changed, 145 insertions(+) create mode 100644 tests/test_external_coding_dsh_profile.py diff --git a/backend/modules/external_agents/registry.py b/backend/modules/external_agents/registry.py index 65cbcbd..19bea13 100644 --- a/backend/modules/external_agents/registry.py +++ b/backend/modules/external_agents/registry.py @@ -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 Harness(headless 一次性任务,打印最终答案后退出)", + "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, + }, ], } diff --git a/tests/test_external_coding_dsh_profile.py b/tests/test_external_coding_dsh_profile.py new file mode 100644 index 0000000..84c3627 --- /dev/null +++ b/tests/test_external_coding_dsh_profile.py @@ -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 ,不注入 --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_id,history 模式也不注入 + ) + 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") diff --git a/workspace/AI_QUICK_REFERENCE.md b/workspace/AI_QUICK_REFERENCE.md index a354299..e490c62 100644 --- a/workspace/AI_QUICK_REFERENCE.md +++ b/workspace/AI_QUICK_REFERENCE.md @@ -447,6 +447,18 @@ POST /api/settings/external-coding-tools/check } ``` +### DeepSeek Harness(dsh)配置说明 + +dsh(DeepSeek Harness)是 DeepSeek 官方开源的 Agent 框架,其 headless 模式适合作为一次性编码工具接入。新工作区首次生成 `external_coding_tools.json` 时会自动带出 dsh profile(默认 `enabled: false`);**老工作区不会自动出现,需在设置页手动新增,或手改 json 添加**。 + +- 调用契约:`dsh --profile headless ""`;`--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. 导入导出 图形化路径: diff --git a/workspace/external_coding_tools.json b/workspace/external_coding_tools.json index 13ca47a..9a96893 100644 --- a/workspace/external_coding_tools.json +++ b/workspace/external_coding_tools.json @@ -87,6 +87,37 @@ "success_exit_codes": [ 0 ] + }, + { + "name": "dsh", + "aliases": [ + "deepseek-harness", + "dsh-headless" + ], + "type": "cli", + "icon_svg": "", + "enabled": false, + "description": "DeepSeek Harness(headless 一次性任务,打印最终答案后退出)", + "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 + ] } ] }