Files
openclaw-config/docs/dsh-acp-smoke.mjs
T
yangxuan 43cd42a563 feat(acp): 接入 DSH 作为 ACP harness,打通 微信→OpenClaw→DSH 派发链路
- openclaw.json
  - plugins.allow 放行 acpx(限制性白名单,缺了后端不会加载)
  - 新增 acp 策略段:enabled/dispatch、backend=acpx、defaultAgent=dsh、
    allowedAgents=[dsh]、stream.deliveryMode=live
  - 新增 plugins.entries.acpx.config:permissionMode=approve-all、
    timeoutSeconds=900、cwd、agents.dsh = dsh --profile acp(绝对路径)
  - 新增 OpenClaw agent dsh(runtime.type=acp → harness dsh),否则
    sessions_spawn 会报 dispatch_failed: Unknown agent id "dsh"
- workspace-dsh/:该 agent 的身份文件(AGENTS/SOUL/IDENTITY/USER/BOOTSTRAP.md)。
  其中的空 .git 由 `openclaw agents add` 的标准 provisioning 生成(非任何 agent 自建),
  会让父仓库把它当 gitlink,已移除后纳入版本控制
- docs/bk02-dsh-openclaw-ACP集成.md:部署/配置/验证证据/排错/回滚全文
- docs/dsh-acp-smoke.mjs:ACP 独立冒烟(initialize→session/new→prompt→close)
- docs/dsh-lang-check.mjs:全局中文指令验证(英文提问看是否回中文)
- skills/delegate-to-dsh:让「交给 dsh」稳定走 ACP 派发;缺此技能时模型会
  静默 fallback 到内嵌 subagent(已复现「假成功」并写入判据)
- agents/main/agent/workshop-skills/acp-backend-triage:ACP 后端排查技能
- plugin-skills/acp-router:acpx 插件自带技能(symlink,与既有渠道一致)
- .gitignore:workspace-dsh 沿用 workspace/ 白名单(只版本化顶层 *.md);
  新增忽略 acpx/ 插件运行产物

验证:ACP 三条途径均通过(独立冒烟、显式 sessions_spawn、自然语言「转给 dsh」),
DSH 侧会话留痕与产物落地见 docs 文档 §5 与 §7。
2026-09-16 17:40:53 +08:00

97 lines
3.5 KiB
JavaScript

import { spawn } from 'node:child_process';
const child = spawn('dsh', ['--profile', 'acp'], {
stdio: ['pipe', 'pipe', 'pipe'],
env: { ...process.env, PATH: `/home/yangxuan/.nvm/versions/node/v26.8.2/bin:${process.env.PATH}` },
cwd: '/home/yangxuan',
});
let buf = '';
const pending = new Map();
let nextId = 0;
const notifications = [];
child.stdout.on('data', (d) => {
buf += d.toString();
let i;
while ((i = buf.indexOf('\n')) >= 0) {
const line = buf.slice(0, i).trim();
buf = buf.slice(i + 1);
if (!line) continue;
let msg;
try { msg = JSON.parse(line); } catch { console.log('[non-json stdout]', line.slice(0, 200)); continue; }
handle(msg);
}
});
child.stderr.on('data', (d) => process.stderr.write('[stderr] ' + d.toString()));
function send(msg) { child.stdin.write(JSON.stringify(msg) + '\n'); }
function call(method, params, timeoutMs = 180000) {
const id = ++nextId;
return new Promise((resolve, reject) => {
const t = setTimeout(() => { pending.delete(id); reject(new Error(`timeout: ${method}`)); }, timeoutMs);
pending.set(id, { resolve: (v) => { clearTimeout(t); resolve(v); }, reject: (e) => { clearTimeout(t); reject(e); } });
send({ jsonrpc: '2.0', id, method, params });
});
}
function handle(msg) {
if (msg.id !== undefined && msg.method === 'session/request_permission') {
const opt = msg.params?.options?.[0];
console.log('[permission request]', JSON.stringify(msg.params?.toolCall ?? {}).slice(0, 200));
send({ jsonrpc: '2.0', id: msg.id, result: { outcome: { outcome: 'selected', optionId: opt?.optionId } } });
return;
}
if (msg.id !== undefined && (msg.result !== undefined || msg.error !== undefined)) {
const p = pending.get(msg.id);
if (!p) return;
pending.delete(msg.id);
msg.error ? p.reject(new Error(JSON.stringify(msg.error))) : p.resolve(msg.result);
return;
}
if (msg.method) {
notifications.push(msg);
const u = msg.params?.update;
const kind = u?.sessionUpdate ?? '';
const text = u?.content?.text ?? u?.text ?? u?.title ?? '';
console.log(`[notify] ${msg.method} ${kind} ${String(text).slice(0, 120)}`);
}
}
try {
const init = await call('initialize', {
protocolVersion: 1,
clientCapabilities: { fs: { readTextFile: false, writeTextFile: false } },
clientInfo: { name: 'dsh-acp-smoke', version: '0.0.1' },
});
console.log('== initialize ==');
console.log(JSON.stringify(init).slice(0, 800));
const sess = await call('session/new', { cwd: '/home/yangxuan', mcpServers: [] });
console.log('== session/new ==');
console.log(JSON.stringify(sess).slice(0, 800));
const sid = sess.sessionId;
const res = await call('session/prompt', {
sessionId: sid,
prompt: [{ type: 'text', text: '只回答一句话:1+1 等于几?不要使用任何工具。' }],
});
console.log('== session/prompt ==');
console.log(JSON.stringify(res).slice(0, 500));
const assistant = notifications
.map((n) => n.params?.update)
.filter((u) => u?.sessionUpdate === 'agent_message_chunk')
.map((u) => u?.content?.text ?? '')
.join('');
console.log('== 助手全文 ==');
console.log(assistant.slice(0, 500));
console.log('== 通知统计 ==', notifications.length, '条');
await call('session/close', { sessionId: sid }, 60000).then((r) => console.log('== session/close ==', JSON.stringify(r))).catch((e) => console.log('close 失败:', e.message));
} catch (err) {
console.log('!! 失败:', err.message);
} finally {
child.kill('SIGTERM');
setTimeout(() => process.exit(0), 1500);
}