43cd42a563
- 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。
84 lines
3.4 KiB
JavaScript
84 lines
3.4 KiB
JavaScript
// DSH ACP 语言策略验证:用【英文】提问,检查回复是否仍为中文(全局指令生效则应为中文)
|
|
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('[非 JSON 的 stdout 输出]', line.slice(0, 160)); 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') {
|
|
send({ jsonrpc: '2.0', id: msg.id, result: { outcome: { outcome: 'selected', optionId: msg.params?.options?.[0]?.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);
|
|
}
|
|
|
|
try {
|
|
await call('initialize', { protocolVersion: 1, clientCapabilities: { fs: { readTextFile: false, writeTextFile: false } }, clientInfo: { name: 'dsh-lang-check', version: '0.0.1' } });
|
|
const sess = await call('session/new', { cwd: '/home/yangxuan', mcpServers: [] });
|
|
const sid = sess.sessionId;
|
|
|
|
const prompts = [
|
|
'Answer in ONE short sentence, English only: what is 2+2?',
|
|
'Reply with a single short sentence: name one benefit of unit tests.',
|
|
];
|
|
for (const text of prompts) {
|
|
notifications.length = 0;
|
|
const before = notifications.length;
|
|
const res = await call('session/prompt', { sessionId: sid, prompt: [{ type: 'text', text }] });
|
|
const reply = notifications
|
|
.map((n) => n.params?.update)
|
|
.filter((u) => u?.sessionUpdate === 'agent_message_chunk')
|
|
.map((u) => u?.content?.text ?? '')
|
|
.join('');
|
|
const cjk = (reply.match(/[\u4e00-\u9fff]/g) || []).length;
|
|
const latin = (reply.match(/[A-Za-z]/g) || []).length;
|
|
console.log('--- 提问(英文):', text);
|
|
console.log(' 回复:', reply.trim().slice(0, 200));
|
|
console.log(' stopReason:', res.stopReason, '| 中文字符数:', cjk, '| 拉丁字母数:', latin, '| 判定:', cjk > 0 && cjk >= latin / 2 ? '中文(指令生效)' : '非中文(指令未生效?)');
|
|
}
|
|
await call('session/close', { sessionId: sid }, 60000).catch(() => {});
|
|
} catch (err) {
|
|
console.log('!! 失败:', err.message);
|
|
} finally {
|
|
child.kill('SIGTERM');
|
|
setTimeout(() => process.exit(0), 1200);
|
|
}
|