🎁 福利专区全网大模型免费应用 + 新用户福利 + 注册活动入口,低成本玩转 AI

Obsidian ACP 桥接 SilenZerOrz/obsidian-dsh-acp

DeepSeek Harnessv0.3.7
SilenZerOrz1 天前有提交已提供中文文档扫描:含敏感能力

版本历史见 CHANGELOG.md。

36.2/100
综合分
4
GitHub 星标
463
npm 周下载
—/5
用户评分

数据截至 2026/11/3 · 周下载取自 npm 全网(含 CI / 镜像),非本站安装数;综合分由星标、活跃度与兼容性检查合成。

安装命令 · 需先装 dsh CLI 引擎
dsh plugin --profile web add obsidian-dsh-acp
npm 源 · obsidian-dsh-acp 的发布者账号已校验与本仓库一致

状态速览

判定依据:实装验证 · 静态安装检查 · 自动扫描 · 仓库维护状态
信任档位:已验证
是什么
dsh 原生插件 · other
装得上吗
本站已在 dsh 0.2.1-rc.5 下真实安装成功(非静态推断);本次验证针对插件 v0.3.8,当前版本为 v0.3.7
L5 · 运行时冒烟等级说明 →
安全吗
本站尚未对该插件做风险分级
暂未覆盖,不等同于无风险
还在维护吗
活跃:最近一次提交在 1 天前
档位由哪些信号合成

本站实装验证(真实安装 L4 覆盖全站;L5 为运行时冒烟或实机截图,覆盖范围小于 L4) · 验证所用 dsh 版本 · 静态安装检查 · 风险分级 · 仓库维护状态。 下方各区块是它的证据明细。

验证判据与等级说明 →
社区实装还没有人打卡 · 装过的第一个就是你
安装兼容性检查✓ 自动检查通过
展开查看检查明细 ↓

以下结论由程序自动检查 npm 包、engines 声明与入口文件得出,未做人工实机验证——能装不等于用着没问题。

✓npm 包obsidian-dsh-acp @ 0.3.7
✓Node 引擎要求 >=22.13 · 基线 Node 22.19 满足
✓dsh CLI 依赖未声明 dsh 版本约束
✓入口文件main/exports/bin 已声明

验证方式:npm registry 存在性 + package.json 静态校验 · 最后检查 2026/10/31 19:59:45

安全扫描自动 · 静态分析源码
v2.7.0
🔵 扫描:含敏感能力尚无人工评估
展开查看扫描说明与证据 ↓

静态扫描发现插件具备触达本地系统的能力:执行外部命令、读写/删除本地文件、读取剪贴板或密钥类环境变量(至少其一)。这是能力清单,不是危险判定——具体能力项与证据见下方,安装前请自行判断是否需要。

可执行外部命令读写/删除本地文件写入剪贴板(不计风险)发起外部网络请求
查看扫描证据(共 110 处,展示前 82 条)
  • 写入本地文件
    index.mjs:42
    appendFileSync("/tmp/dsh-acp-debug.log", `${new Date().toISOString()} [${level}] ${line}\n`);
  • 执行外部命令来自配置/变量
    index.mjs:235
    const child = spawn(process.execPath, [this.adapterPath()], {
  • 读取本地文件
    dsh-acp.mjs:241
    try { size = statSync(logFile).size; } catch { return; }
  • 删除本地文件
    dsh-acp.mjs:250
    try { rmSync(`${logFile}.${LOG_KEEP + 1}`, { force: true }); } catch {}
  • 写入本地文件
    dsh-acp.mjs:350
    writeFileSync(file, [
  • 删除本地文件递归
    dsh-acp.mjs:360
    return { file, cleanup: () => { try { rmSync(dir, { recursive: true, force: true }); } catch {} } };
  • 读取本地文件
    dsh-acp.mjs:425
    const st = statSync(effectiveCwd);
  • 执行外部命令来自配置/变量
    dsh-acp.mjs:435
    const child = spawn(DSH_SPAWN.cmd, [...DSH_SPAWN.prefixArgs, ...args], {
  • 发起网络请求
    dsh-acp.mjs:1095
    return await ctx.client.request(method, p);
  • 发起网络请求
    dsh-acp.mjs:1180
    ? { request: (m, p) => ctx.client.request(m, p) }
  • 发起网络请求
    dsh-acp.mjs:1488
    .request({ method: methods.agent.initialize, params: permissiveAny }, (ctx) => h.initialize(ctx.params))
  • 发起网络请求
    dsh-acp.mjs:1489
    .request({ method: methods.agent.session.new, params: permissiveNewSession }, (ctx) => h.newSession(ctx.params))
  • 发起网络请求
    dsh-acp.mjs:1490
    .request({ method: methods.agent.session.load, params: permissiveLoadSession }, (ctx) => h.loadSession(ctx.params))
  • 发起网络请求
    dsh-acp.mjs:1491
    .request({ method: methods.agent.session.list, params: permissiveAny }, (ctx) => h.listSessions(ctx.params))
  • 发起网络请求
    lib/client.js:307
    const resp = await fetch("/api-session/" + path, {
  • 写入剪贴板
    lib/client.js:531
    await navigator.clipboard.writeText(text);
  • 写入剪贴板
    lib/client.js:686
    await navigator.clipboard.writeText(id);
  • 写入剪贴板
    lib/client.js:872
    await navigator.clipboard.writeText(id);
  • 读取本地文件
    lib/http-gateway.mjs:534
    const text = readFileSync(file, "utf8").replace(/^ FEFF/, "");
  • 读取本地文件
    lib/version-detect.mjs:104
    const raw = readFileSync(candidate, "utf8");
  • 读取本地文件
    lib/version-detect.mjs:224
    const src = readFileSync(path, "utf8");
  • 执行外部命令固定命令
    lib/version-detect.mjs:335
    const r = spawnSync("dsh", ["--profile", key, "--json", "--help"], {
  • 读取本地文件
    archive-store.mjs:93
    if (existsSync(storeFile())) {
  • 读取本地文件
    archive-store.mjs:94
    cache = JSON.parse(readFileSync(storeFile(), "utf8"));
  • 读取本地文件
    archive-store.mjs:112
    if (!existsSync(storeFile())) return;
  • 读取本地文件
    archive-store.mjs:113
    const disk = JSON.parse(readFileSync(storeFile(), "utf8"));
  • 写入本地文件
    archive-store.mjs:141
    mkdirSync(storeRoot(), { recursive: true });
  • 写入本地文件
    archive-store.mjs:148
    writeFileSync(tmp, JSON.stringify(idx, null, 2), "utf8");
  • 写入本地文件
    archive-store.mjs:149
    renameSync(tmp, storeFile());
  • 读取本地文件
    archive-store.mjs:299
    if (existsSync(oldDir) && !existsSync(newDir)) {
  • 写入本地文件
    archive-store.mjs:300
    try { mkdirSync(newDir, { recursive: true }); renameSync(oldDir, newDir); } catch (e) {
  • 写入本地文件
    archive-store.mjs:300
    try { mkdirSync(newDir, { recursive: true }); renameSync(oldDir, newDir); } catch (e) {
  • 删除本地文件递归
    archive-store.mjs:372
    rmSync(join(root, enc, rec.archive), { recursive: true, force: true });
  • 写入本地文件
    archive-store.mjs:411
    mkdirSync(join(logPath, ".."), { recursive: true });
  • 读取本地文件
    archive-store.mjs:414
    const firstWrite = !existsSync(logPath);
  • 读取本地文件
    gc.mjs:52
    (() => { try { return existsSync(p); } catch { return false; } })()
  • 读取本地文件
    gc.mjs:73
    try { entries = readdirSync(root); } catch { continue; }
  • 读取本地文件
    gc.mjs:87
    try { entries = readdirSync(dir); } catch { continue; }
  • 读取本地文件
    gc.mjs:92
    const j = JSON.parse(readFileSync(join(dir, f), "utf8"));
  • 读取本地文件
    gc.mjs:106
    (() => { try { return existsSync(join(root, "dsh-acp-archives", enc, name)); } catch { return false; } })() ||
  • 读取本地文件
    gc.mjs:107
    (() => { try { return existsSync(join(root, "sessions", enc, name)); } catch { return false; } })()
  • 读取本地文件
    doctor.mjs:24
    return readFileSync(file, "utf8").replace(/^ FEFF/, "");
  • 读取本地文件
    doctor.mjs:46
    try { accessSync(p, fsConstants.X_OK); return p; } catch { }
  • 读取本地文件
    doctor.mjs:59
    try { accessSync(p, fsConstants.X_OK); } catch { continue; }
  • 读取本地文件
    doctor.mjs:106
    try { accessSync(p, fsConstants.X_OK); return p; } catch { }
  • 执行外部命令来自配置/变量
    doctor.mjs:127
    const r = spawnSync(spec.cmd, [...spec.prefixArgs, "--version"], { encoding: "utf8", timeout: 8000 });
  • 执行外部命令固定命令
    doctor.mjs:232
    const hasDsh = (() => { try { const r = spawnSync("dsh", ["--version"], { timeout: 5000 }); return r.status === 0; } catch { return false; } })();
  • 读取本地文件
    doctor.mjs:298
    if (!existsSync(profileDir)) {
  • 读取本地文件
    doctor.mjs:318
    if (existsSync(cordisYml)) {
  • 写入本地文件
    doctor.mjs:997
    const write = opts.writeFile ?? ((f, d) => writeFileSync(f, JSON.stringify(d, null, 2)));
  • 写入本地文件
    doctor.mjs:998
    writeFileSync(backupPath, JSON.stringify(current, null, 2));
  • 读取本地文件
    lib/settings-provider-catalog.mjs:95
    try { return readFileSync(p, "utf8"); } catch { return null; }
  • 读取本地文件
    lib/cwd-validator.mjs:69
    st = statSync(cwd);
  • 发起网络请求
    lib/permission-gate.mjs:278
    () => acpRequester.request(toolCallId, title, options),
  • 发起网络请求
    lib/proxy-mode.mjs:82
    const res = await fetch(`${baseUrl}/acp/proxy/probe`, {
  • 发起网络请求
    lib/proxy-mode.mjs:188
    const r = await fetch(`${baseUrl}/acp/proxy/session/prompt`, {
  • 发起网络请求
    lib/proxy-mode.mjs:318
    await fetch(`${baseUrl}/acp/proxy/permission-response`, {
  • 读取本地文件
    web/obsidian-import.mjs:38
    try { idx = JSON.parse(readFileSync(file, "utf8") || "{}"); } catch { idx = {}; }
  • 写入本地文件
    web/obsidian-import.mjs:41
    writeFileSync(file, JSON.stringify(idx, null, 2));
  • 读取本地文件
    web/obsidian-import.mjs:53
    const idx = JSON.parse(readFileSync(obsidianImportedIndexPath(), "utf8") || "{}");
  • 读取本地文件
    web/obsidian-import.mjs:82
    if (existsSync(sp)) candidates.add(p);
  • 读取本地文件
    web/obsidian-import.mjs:87
    try { entries = readdirSync(home, { withFileTypes: true }); } catch { entries = []; }
  • 读取本地文件
    web/obsidian-import.mjs:93
    if (existsSync(sp)) candidates.add(join(home, n));
  • 读取本地文件
    web/obsidian-import.mjs:110
    if (!existsSync(sp)) continue;
  • 读取本地文件
    import-session.mjs:88
    if (!existsSync(abs)) throw new Error(`file not found: ${abs}`);
  • 读取本地文件
    import-session.mjs:89
    const data = JSON.parse(readFileSync(abs, "utf8"));
  • 发起网络请求
    lib/long-runtime.mjs:315
    const res = await live.request(ACP_METHOD_REQUEST_PERMISSION, {
  • 读取本地文件
    lib/selfcheck/audit.mjs:529
    try { return JSON.parse(readFileSync(p, "utf8")); } catch { return null; }
  • 读取本地文件
    lib/selfcheck/audit.mjs:533
    try { return JSON.parse(readFileSync(p, "utf8")); } catch { return null; }
  • 读取本地文件
    lib/selfcheck/cross-config-sync.mjs:193
    for (const name of readdirSync(profilesDir)) {
  • 读取本地文件
    lib/selfcheck/paths.mjs:15
    try { return statSync(p).isDirectory(); } catch { return false; }
  • 读取本地文件
    lib/selfcheck/paths.mjs:20
    try { return statSync(p).isFile(); } catch { return false; }
  • 读取本地文件
    lib/selfcheck/paths.mjs:25
    try { return readFileSync(p, "utf8"); } catch { return null; }
  • 执行外部命令来自配置/变量
    lib/selfcheck/platform.mjs:25
    const child = spawn(cmd, args, {
  • 读取本地文件
    lib/selfcheck/platform.mjs:69
    if (existsSync(p)) return p;
  • 读取本地文件
    lib/selfcheck/platform.mjs:82
    if (existsSync(p)) return p;
  • 发起网络请求
    lib/selfcheck/platform.mjs:151
    const req = lib.request({
  • 写入本地文件
    lib/selfcheck/repair.mjs:159
    mkdirSync(dirname(target), { recursive: true });
  • 读取本地文件
    lib/selfcheck/repair.mjs:161
    if (existsSync(target)) {
  • 写入本地文件
    lib/selfcheck/repair.mjs:163
    copyFileSync(target, backupPath);
  • 写入本地文件
    lib/selfcheck/repair.mjs:165
    writeFileSync(target, content);
  • 读取本地文件
    lib/selfcheck/repair.mjs:200
    try { cur = JSON.parse(readFileSync(target, "utf8")); } catch { cur = {}; }
扫描于 2026/11/3(0 天前) · 范围 入口闭包 + 全包扫描 · 来源 npm 发布包

静态扫描只反映源码中出现的调用,不构成安全背书;人工评估与自动扫描相互独立、互不覆盖。

依赖的 DSH / Cordis 模块
@deepseek-ai/schemastery@deepseek-ai/cordis@deepseek-ai/dsh-acp
给这个插件打分还没有人投票↓
用户评分
还没有人投票,来当第一个

界面预览

3 张 · 点击查看大图
v0.3.82026-10-31
· Added · ACP stdout 协议纯净性守卫(lib/acp-stdout-guard.mjs,P0) - ACP 走 stdio 的 NDJSON 帧协议,stdout 是协议信道;adapter 内任何 console.log 都会往 stdout 写非协议文本 → 客户端帧解析失败 - installAcpStdoutGuard({ logFile }):把 console.log/info/debug 重定向到 stderr; 幂等(Symbol.for("obsidian-dsh-acp.stdout-guard") 打标);已有 logFile 重定向时跳过 - dsh-acp.mjs 两处 .catch((e) => console.log(...)) → diagNotifyError(e, ...); 默认 CLI 分支在 runAcpDualMode() 之前安装守卫 · 防退化断言 test/acp-stdout-purity.test.mjs(7/7) - 5 项行为测试 + 2 项源码门(静态断言 installAcpStdoutGuard( 在 runAcpDualMode().catch( 之前; 文件内 console.log( 调用点数符合约束) · doctor 第 6 层:headless 凭据诊断 - 正则提取 headless profile cordis.patch.yml 的 apiKeyEnv: 名单,若某名字既不在 process.env 也不在 $DSH_HOME/.env → 报 headless-credential-env(severity high) - 提示里只列键名,不含值;dsh 版本不可判定或 =0.1.6-alpha.1 600s 挂死 → 17s,cancelled: 3 → 0 · Known issues (not fixed in 0.3.8) · C1:official-acp-bridge 测试断言 missingServices.includes("subagents"), 实现已把 subagents 移入 advisoryServices → 测试滞后实现(改断言即可,非实现缺陷) · C2:http-gateway-tool-translation(-concurrent) 测试断言「必须翻译成 tool_call_update」, 实现自 P0-1 起 PASS THROUGH tool_call → 同上 按 D10-a 口径,门槛 3 = cancelled = 0(已达成);C1/C2 的 3 个 fail 不计入 0.3.8 门槛, 登记为 0.3.9 / 0.4 独立项。详见 docs/计划文档.md §8.4 / §8.5。 · Tests · test:unit 388/388/0 · test:protocol / test:long 各 37/0 · test:officials 27 tests / 15 pass / 3 fail(C1/C2) / 0 cancelled / 9 skipped ---
v0.3.72026-10-19
0.3.7:入口守卫改用 realpath + DSH_ACP_CONTEXT_WINDOW 文档 · doctor.mjs 入口守卫改用 realpathSync 规范化(修复 /tmp 符号链接导致的静默退出) · README 三处 + CHANGELOG:补充 DSH_ACP_CONTEXT_WINDOW 环境变量说明 · test/entry-guard.test.mjs(新增,已纳入版本跟踪) · test:unit 379 -> 381/381/0 · 包含后续提交,把测试文件纳入跟踪(上一个提交 bd02541 因 .gitignore 否定列表有缺口而漏掉了它)
v0.3.62026-10-19
0.3.6:修复 0.3.4/0.3.5 界面静默失效的紧急补丁 · _notifyUpdate 读取 _liveAcpClient(与 request() 保持一致) · ensureLongRuntime 把启动占位重新指向桥接通知器 · resolveContextWindow():上游 -> DSH_ACP_CONTEXT_WINDOW -> 0 · 回归测试:H1 初始化前顺序 -> >=1 次 sessionUpdate · P0 真机 6/6;P1 架构缺口(0.4 范围,见 CHANGELOG)
数据来自仓库的 GitHub Release,可能与插件实际版本不一致。

README

由 DeepSeek 最新模型翻译生成
obsidian-dsh-acp English · 简体中文 · Русский 版本历史见 CHANGELOG.md。 node 在 [Obsidian 里直接使用 DeepSeek Harness(DSH) ——一个把 Obsidian 变成完整 DSH 客户端的 ACP 适配器。 这是什么? obsidian-dsh-acp 是一个 ACP(Agent Client Protocol) 服务器,把 DSH 接入 Obsidian。把它配置为 Obsidian Agent Client 社区插件里的 Custom Agent(或作为 cordis 插件装进 DSH profile),就能在 Obsidian 界面里驱动 DeepSeek Harness —— 对话、工具调用、会话历史 —— 全程不用切出 Obsidian。
展开完整文档(README 剩余 1 节)
obsidian-dsh-acp

English · 简体中文 · Русский

版本历史见 CHANGELOG.md。

node

在 [Obsidian 里直接使用
DeepSeek Harness(DSH) ——一个把 Obsidian 变成完整 DSH 客户端的 ACP 适配器。

这是什么?

obsidian-dsh-acp 是一个 ACP(Agent Client Protocol) 服务器,把 DSH 接入 Obsidian。把它配置为 Obsidian Agent Client 社区插件里的 Custom Agent(或作为 cordis 插件装进 DSH profile),就能在 Obsidian 界面里驱动 DeepSeek Harness —— 对话、工具调用、会话历史 —— 全程不用切出 Obsidian。

Obsidian(Agent Client 插件) │ ① 作为 Custom Agent 通过 ACP 启动它 ▼ obsidian-dsh-acp(ACP 服务器,v1 over stdin/stdout) │ ② 每个 turn 一条 prompt ▼
dsh (headless 一次性任务,或长驻的 dsh web gateway)

它的工作方式与 claude-agent-acp 包装 Claude Code 类似:每个 turn 把 DSH 输出以 agent_message_chunk 增量流回客户端,结束时返回 end_turn。

功能

- 💬 在 Obsidian 里对话 —— 从 Agent Client 侧边栏直接向 DSH 发 prompt。
- 🧰 工具调用卡片 —— 客户端渲染工具卡片(in_progress → completed),每步显示 token 用量。(需要支持工具的模型)
- 🗂️ 会话管理 —— 持久会话列表、fork、resume、永久删除;适配器重启后仍在。
- 🔀 按会话切换模型 —— 客户端里的模型下拉框;所选模型只作用于该会话(绝不改动共享的 profile 配置)。
- 🧠 推理与实时流 —— long 模式(在 dsh web profile 内)显示实时推理 / 执行过程。
- 📥 导入外部会话 —— 把其它 ACP agent(如 Claude / Agent Client)导出的会话导入本地 DSH 会话库。
- 🩺 自检与一键修复 —— dsh-acp doctor 诊断环境漂移,并能自动修复大部分问题。
- 🧹 自动清理 —— 每次 session/list 与 Obsidian 自身的会话列表对账清理孤儿(保守策略;仍在 Obsidian 里的会话永不删除)。

截图

在 Obsidian Agent Client 里注册为自定义 agent、运行一次对话、浏览会话历史:

Agent settings
Chat panel
Session history

环境要求

- Node.js >= 22.13
- 一个能启动的 dsh 后端(见 Headless profile 引导)
- Obsidian + Agent Client 社区插件(走 Obsidian 路径时)

安装

一键安装(推荐)

安装脚本会(a)通过官方的 dsh plugin add 把插件装进某个 DSH profile,并(b)把一个 Obsidian Agent Client 自定义 agent 接到 ACP 服务器上。它幂等、改动文件前先备份、支持任意 Obsidian vault,可用 --dry-run 预览。

先预览(不改动任何东西) ./install.sh --obsidian-vault /path/to/any/vault --dry-run

装进 "web" profile 并接好 Obsidian ./install.sh --obsidian-vault /path/to/any/vault

装进其它 profile / 跳过 Obsidian ./install.sh --profile headless --obsidian-vault /path/to/any/vault ./install.sh --no-obsidian

全部选项见 ./install.sh --help(--profile、--dsh-home、--package、 --node-bin、--dry-run、--uninstall 等)。

Windows: 改用 PowerShell 版 —— .\install.ps1 -ObsidianVault 'D:\path\to\vault' (加 -DryRun 预览,-Uninstall 还原)。它会写入指向 dsh.cmd 垫片的 DSH_BIN,并把「插件目标 profile」与「适配器运行时 profile」分开。

手动配置(Obsidian Agent Client)

1. 安装并启用 Agent Client 社区插件(设置 → 第三方插件 → 浏览 → 搜索 "Agent Client")。
2. 设置 → Custom Agents → Add,填写:
- ID:dsh-acp
- Display name:DeepSeek Harness (ACP)
- Command:本包 dsh-acp.mjs 的绝对路径
- Args:(留空)
3. 把插件的 nodePath 指向一个真实的 node(>= 22.13)。
4. 重载 Obsidian(Cmd/Ctrl-R),在 agent 选择器里选 DeepSeek Harness (ACP)。

customAgent.env 是 { key, value } 对象数组 —— 不是 "KEY=VALUE" 字符串。
Agent Client 从该数组构建子进程环境;纯字符串条目会被静默忽略。

快速上手

1. 首次发 prompt 前,在会话面板里选一个支持工具的模型(如 gemini-2.5-pro),并确保它配好了有效的 API key。工具卡片只在模型支持工具调用时出现。
2. 发一条 prompt。DSH 跑完这个 turn,把回答流回 Obsidian。

如果首轮报 AUTH: Authentication Fails 或什么都不返回,说明默认模型要么不支持工具、要么没配凭据 —— 在模型选择器里换一个配好的模型重试。

用法

dsh-acp # 启动 ACP 服务器(默认) dsh-acp doctor # 健康检查 + 一键修复指引 dsh-acp import <file> # 导入外部 ACP 会话 JSON dsh-acp manage <verb> ... # list / export / archive / move / import 会话 dsh-acp --help # 完整帮助

健康检查与修复(doctor)

当 DSH 或 Obsidian 报告连接问题("ACP connection closed"、"dsh exited 1"、 MISSING_CREDENTIAL 等)时,运行:

dsh-acp doctor # 诊断,打印可直接复制的修复命令 dsh-acp doctor --repair # 自检全量修复 —— 预览(零写盘) dsh-acp doctor --repair --apply # 执行修复(每个文件先自动备份)

doctor 与版本无关:只做通用检查(dsh 二进制、dsh 版本、凭据、headless profile 健康度),不依赖任何特定 dsh 版本的内部 API。

工具轮无文字兜底

推理型模型有时只调工具、不产出面向用户的文字(答案全留在了推理流里)。若不处理,客户端只会看到一张工具卡片 + 空白回复。为此 dsh-acp 会合成一条如实提示,保证这一轮始终有可见输出:

[模型请求调用工具:<工具名>;当前运行模式不执行工具,本轮无结果与文字]

这是纯显示——不改协议、不改归档历史。可用 DSH_ACP_TOOL_ONLY_FALLBACK=off 关闭。

proxy/long 工具门控(默认关)。 proxy/long 运行时路径只调用一次
ctx.llm.stream(),没有工具执行器。在该路径暴露工具 schema 会让模型请求根本
不会执行的工具、然后无正文结束。因此该路径默认不暴露工具:

DSH_ACP_LONG_TOOLS=1 dsh-acp ... # 开启(须设在 dsh web 进程环境)

需要真正执行工具?走 spawn 路径(去掉 DSH_ACP_HTTP_GATEWAY_URL、设 DSH_ACP_PROXY_MODE=false)—— 真 dsh 后端自带 agent loop。

可选的收尾句提示(默认关)。 开启后引导模型在调工具后补一句收尾:

DSH_ACP_CLOSING_REPLY_NUDGE=1 dsh-acp ...

开启后,发给模型的 prompt 会前置一句简短指令("调完工具后告诉用户你做了什么")。
该提示仅模型可见,绝不显示为用户文字。

⚠️ 开启后每个工具轮都会改变 prompt 前缀 → prompt 缓存失效 → 成本上升。
除非清楚取舍,否则保持默认关闭。文案可用 DSH_ACP_CLOSING_REPLY_NUDGE_TEXT 覆盖。

proxy 路径下记忆工具不执行(无执行器),且 headless profile 未加载 ui-obsidian-memory。同 session 回忆正常(靠会话历史);跨 session 记忆不可用。
详见 docs/known-gaps.md。

会话清理(gc,自动)

Obsidian 的删除按钮只删它自己的本地会话文件,从不发 ACP session/delete —— 所以本适配器的持久索引会变陈旧、已删会话可能「复活」。适配器在每次 session/list 时自动对账(保守策略 —— 仍在 Obsidian 里的会话永不删除)。

dsh-acp doctor # 显示探测到的 Obsidian 会话目录 + 孤儿数量 dsh-acp doctor --gc # 立即执行清理

| 环境变量 | 默认 | 含义 |
| DSH_ACP_GC | on | off 关闭自动 GC |
| DSH_ACP_GC_REPORT_ONLY | 0 | 1 = 只报告不删除(dry-run) |

配置

Obsidian ↔ 适配器传输(proxy vs spawn)

适配器按 prompt 选择传输方式:

- proxy (推荐,当有 dsh web gateway 在跑时) —— 把 turn 通过 HTTP/SSE 转发给已在运行的 gateway,一个长驻进程服务模型;
- spawn (兜底) —— 自己起一个 dsh --profile headless "<prompt>" 子进程。

建议的自定义 agent env 条目:

| key | value | 作用 |
| DSH_ACP_HTTP_GATEWAY_URL | http://127.0.0.1:3080 | 指向 dsh web gateway |
| DSH_ACP_PROXY_MODE | true | 请求 proxy 传输 |

两者都不设时,适配器用 spawn(headless) —— 需要系统上有 dsh CLI (DSH_ACP_SPAWN_PROFILE 可覆盖 profile,默认 headless,也就是那个能接收 prompt 的 profile)。

常用环境变量

| 变量 | 含义 | 默认 |
| DSH_BIN | dsh 可执行文件 | PATH 上的 dsh |
| DSH_ACP_SPAWN_PROFILE | spawn 传输用的 profile | headless |
| DSH_ACP_DEFAULT_MODEL | 新会话默认 provider/model | (profile) |
| DSH_ACP_MODELS | 覆盖模型下拉框(逗号分隔 id(显示名)) | (catalog) |
| DSH_ACP_PERMISSION_MODE | default / acceptEdits / dontAsk / bypassPermissions | default |
| DSH_ACP_LOG_DIR | 运行时日志目录 | (关闭) |
| DSH_ACP_STORE_DIR | 持久会话 JSON 索引目录 | ~/.dsh-acp |

进阶 → 完整适配器环境变量表

| 变量 | 含义 | 默认 |
| DSH_PROFILE | 启动的 profile(其它组件用) | — |
| DSH_ARGS | prompt 之前的额外参数(空格分隔) | (无) |
| DSH_ACP_LOG_MAX_BYTES | 日志轮转大小上限(字节) | 5242880(5 MB) |
| DSH_ACP_LOG_KEEP | 保留的轮转日志数 | 2 |
| DSH_ACP_PERSIST_DEBOUNCE_MS | 索引写入的防抖窗口 | 100 |
| DSH_ACP_PERMISSION_TIMEOUT_MS | 审批自动拒绝超时 | 300000 |
| DSH_ACP_PERMISSION_EDIT_TOOLS | 视作编辑类的工具 | Edit,Write,MultiEdit,NotebookEdit |
| DSH_ACP_RUNTIME_MODE | long / spawn 显式覆盖 | (自动) |
| DSH_ACP_SPAWN_FALLBACK | long 初始化失败时回退 spawn | true |
| DSH_ACP_USE_OFFICIAL_BRIDGE | 尝试官方 dsh bridge | 0 |
| DSH_ACP_ARCHIVE_IN_MAIN | 把 turn 归档放进 sessions/ | 0 |

Headless profile 引导

dsh --profile headless 需要一个它能解析的默认模型 provider。用一个直接的 llm-pi-ai patch 条目给 headless profile 自己的设置:

- ~/.dsh/profiles/headless/cordis.patch.yml —— 一个 llm-pi-ai provider 块 + 把 agent-default-model 设成你选的 provider/model。
- ~/.dsh/profiles/headless/package.json —— dsh-llm、dsh-agent-loop、 dsh-acp、dsh-base、dsh-headless 固定到与全局 dsh 相同版本。
- ~/.dsh/profiles/headless/pnpm-workspace.yaml —— overrides: 固定 @deepseek-ai/dsh- 核心包 + allowBuilds: 放行原生模块。

用 dsh-acp doctor 验证 —— 它会报告 headless profile 的配置错误并给出可直接复制的修复命令。

已知限制

- spawn 路径无权限弹窗 —— 该路径下 bash / 写文件等工具不经确认直接执行。需要审批时用 DSH_ACP_PERMISSION_MODE(仅在 long-runtime 路径生效)。
- 官方桥为实验性(0.3.8) —— 默认走 legacy* 路径。启用官方桥需三件套 env 同设(见下方「启用官方桥」);经 launchd 启动需写入 plist EnvironmentVariables 或 launchctl setenv,否则重启即回 legacy。
- 权限路径挂起(0.3.8 仍未修) —— 官方桥 + 内部 router 组合下,触发审批的工具(session/request_permission)会挂起。0.3.8 默认 web profile 不暴露 bash/write 工具,用户实际碰不到。
- headless 凭据(dsh CLI ≥ 0.2.1-rc.2) —— headless profile 禁用托管凭据存储(不读 ~/.dsh/.credentials.yaml);API-key 回退链:process.env → <cwd>/.env → $DSH_HOME/.env。详见下方「headless 凭据设置」。

headless 凭据设置

自 @deepseek-ai/dsh 0.2.1-rc.2 起,headless profile 禁用托管凭据存储(不读 ~/.dsh/.credentials.yaml)。API-key 回退链:process.env → <调用目录>/.env → $DSH_HOME/.env。

若报 MISSING_CREDENTIAL: llm-pi-ai: no credential for provider route "<name>":

1. 从 ~/.dsh/profiles/<profile>/cordis.patch.yml 找 provider 的 apiKeyEnv 字段(如 JLTOKEN_API_KEY;注意可能与 .credentials.yaml 里的 ref 名不同)。
2. 写入 ~/.dsh/.env:
JLTOKEN_API_KEY=<你的密钥>

3. Windows 收紧权限: icacls "$env:USERPROFILE\.dsh\.env" /inheritance:r /grant:r "${env:USERNAME}:R"

4. 重启 DSH Desktop / Obsidian。

验证:dsh --profile headless echo hi 应无报错。

上下文窗口(usage 百分比)

usage_update 帧含 size 字段(模型的上下文窗口)。当上游模型不报告该值时, dsh-acp 默认 size: 0,客户端会显示 used/0 tokens + Infinity%。

要提供真实上下文窗口,设置环境变量:

DSH_ACP_CONTEXT_WINDOW=200000 # 或你模型的实际上限

写入 ~/.dsh/.env(与 API key 一起)或 dsh web 进程环境。仅在上游模型不报告时作为回退。

启用官方桥(实验性)

0.3.8 默认 legacy。若需启用官方桥:

方式 1:临时 env(仅本次 dsh web 进程)
DSH_ACP_USE_OFFICIAL_BRIDGE=1 \
DSH_ACP_OFFICIAL_PROVIDER=<provider> \
DSH_ACP_OFFICIAL_MODEL=<model> \
dsh --profile web --port 3080 --no-open

方式 2:launchd 常驻(推荐,永久生效) launchctl setenv DSH_ACP_USE_OFFICIAL_BRIDGE 1 launchctl setenv DSH_ACP_OFFICIAL_PROVIDER <provider> launchctl setenv DSH_ACP_OFFICIAL_MODEL <model> launchctl kickstart -k gui/$(id -u)/com.deepseek.harness.web 回滚:launchctl unsetenv ... + kickstart

验证:curl -s http://127.0.0.1:3080/acp/proxy/probe 应返回 official.envSwitchOn:true + servicesReady:true。

开发

npm run test:unit # 单元测试(node --test)
npm run test:protocol # ACP 协议测试(spawn)
npm run test:long # long-runtime 测试
npm run test:all # unit + protocol + long

License

MIT

[acp]: https://github.com/evalstate/agent-client-protocol
[cordis]: https://github.com/cordiverse/cordis
预览分享卡片长什么样 ↓SilenZerOrz/obsidian-dsh-acp
分享 · DeepSeek Harness Hub
SilenZerOrz/obsidian-dsh-acp

版本历史见 CHANGELOG.md。

回到顶部