Files
openclaw-config/docs/dsh-lang-check.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

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);
}