dsh-any-agent 0.3.9 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +153 -1
- package/README.md +75 -634
- package/README.zh.md +64 -649
- package/bin/dsh-any-agent.mjs +59 -0
- package/demo/mcp-client.mjs +130 -0
- package/demo/run.mjs +326 -0
- package/demo/transcript.json +47 -0
- package/docs/brand/README.md +58 -0
- package/docs/brand/header.jpg +0 -0
- package/docs/en/architecture.md +43 -0
- package/docs/en/compatibility.md +51 -0
- package/docs/en/demo.md +90 -0
- package/docs/en/install.md +257 -0
- package/docs/en/limits.md +27 -0
- package/docs/en/locking.md +24 -0
- package/docs/en/reference.md +40 -0
- package/docs/en/release.md +33 -0
- package/docs/en/security.md +59 -0
- package/docs/en/team.md +74 -0
- package/docs/en/tests.md +96 -0
- package/docs/en/usage.md +71 -0
- package/docs/zh/architecture.md +43 -0
- package/docs/zh/compatibility.md +49 -0
- package/docs/zh/demo.md +90 -0
- package/docs/zh/install.md +274 -0
- package/docs/zh/limits.md +30 -0
- package/docs/zh/locking.md +24 -0
- package/docs/zh/reference.md +40 -0
- package/docs/zh/release.md +38 -0
- package/docs/zh/security.md +59 -0
- package/docs/zh/team.md +71 -0
- package/docs/zh/tests.md +95 -0
- package/docs/zh/usage.md +69 -0
- package/package.json +5 -1
- package/scripts/check-release.mjs +73 -0
- package/src/acp.mjs +31 -3
- package/src/audit.mjs +32 -0
- package/src/bootstrap.mjs +26 -1
- package/src/config.mjs +178 -9
- package/src/host.mjs +148 -0
- package/src/hub.mjs +494 -73
- package/src/procstat.mjs +72 -0
- package/src/tools.mjs +561 -27
package/bin/dsh-any-agent.mjs
CHANGED
|
@@ -34,6 +34,8 @@ if (argv.includes('--help') || argv.includes('-h')) {
|
|
|
34
34
|
'Runs as an MCP stdio server; normally launched by your MCP client, not by hand.\n\n' +
|
|
35
35
|
'Usage:\n' +
|
|
36
36
|
' dsh-any-agent run the MCP server on stdio\n' +
|
|
37
|
+
' dsh-any-agent team on|off enable/disable Agent Teams (writes settings.json)\n' +
|
|
38
|
+
' dsh-any-agent team status show the effective team mode\n' +
|
|
37
39
|
' dsh-any-agent --version print the version and exit\n' +
|
|
38
40
|
' dsh-any-agent --help this text\n\n' +
|
|
39
41
|
'Register it with your client:\n' +
|
|
@@ -50,6 +52,63 @@ if (argv.includes('--help') || argv.includes('-h')) {
|
|
|
50
52
|
process.exit(0);
|
|
51
53
|
}
|
|
52
54
|
|
|
55
|
+
/**
|
|
56
|
+
* `dsh-any-agent team on|off|status`
|
|
57
|
+
*
|
|
58
|
+
* 团队开关写 `<DSH_HOME>/.dsh-any-agent/settings.json`,不改客户端配置。
|
|
59
|
+
* 优先级:环境变量 DSH_MCP_TEAM > 该文件 > 默认关。
|
|
60
|
+
*/
|
|
61
|
+
if (argv[0] === 'team') {
|
|
62
|
+
const fs = await import('node:fs');
|
|
63
|
+
const nodePath = await import('node:path');
|
|
64
|
+
const cfg = await import('../src/config.mjs');
|
|
65
|
+
const readSettings = () => {
|
|
66
|
+
try {
|
|
67
|
+
return JSON.parse(fs.readFileSync(cfg.TEAM_SETTINGS_FILE, 'utf8'));
|
|
68
|
+
} catch {
|
|
69
|
+
return {};
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
const writeSettings = (obj) => {
|
|
73
|
+
fs.mkdirSync(nodePath.dirname(cfg.TEAM_SETTINGS_FILE), { recursive: true });
|
|
74
|
+
const tmp = `${cfg.TEAM_SETTINGS_FILE}.tmp.${process.pid}`;
|
|
75
|
+
fs.writeFileSync(tmp, JSON.stringify(obj, null, 2) + '\n', 'utf8');
|
|
76
|
+
fs.renameSync(tmp, cfg.TEAM_SETTINGS_FILE);
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
const action = argv[1] ?? 'status';
|
|
80
|
+
if (action === 'on' || action === 'off') {
|
|
81
|
+
const cur = readSettings();
|
|
82
|
+
cur.team = action === 'on';
|
|
83
|
+
writeSettings(cur);
|
|
84
|
+
process.stdout.write(
|
|
85
|
+
`团队模式已${action === 'on' ? '开启' : '关闭'}(${cfg.TEAM_SETTINGS_FILE})。\n` +
|
|
86
|
+
`DSH 版本: ${cfg.DSH_VERSION ?? '未检测到'}(团队需要 >= 0.1.7-0)。\n`,
|
|
87
|
+
);
|
|
88
|
+
if (cfg.TEAM_MODE_SOURCE === 'env') {
|
|
89
|
+
process.stdout.write(
|
|
90
|
+
'⚠️ 环境变量 DSH_MCP_TEAM 优先级更高;请同时删除它,否则本次设置不会生效。\n',
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
process.stdout.write('重新连接 MCP 服务(或重启客户端)后生效。\n');
|
|
94
|
+
process.exit(0);
|
|
95
|
+
}
|
|
96
|
+
if (action === 'status') {
|
|
97
|
+
const file = readSettings();
|
|
98
|
+
process.stdout.write(
|
|
99
|
+
'团队模式状态:\n' +
|
|
100
|
+
` 环境变量 DSH_MCP_TEAM: ${process.env.DSH_MCP_TEAM ?? '(未设置)'}\n` +
|
|
101
|
+
` 设置文件: ${cfg.TEAM_SETTINGS_FILE}(team=${file.team === undefined ? '未设置' : file.team})\n` +
|
|
102
|
+
` 生效值: ${cfg.TEAM_MODE ? '开' : '关'}(来源: ${cfg.TEAM_MODE_SOURCE})\n` +
|
|
103
|
+
` profile: ${cfg.DSH_PROFILE}\n` +
|
|
104
|
+
` 团队能力可用: ${cfg.TEAM_ACTIVE ? '是' : '否'}\n`,
|
|
105
|
+
);
|
|
106
|
+
process.exit(0);
|
|
107
|
+
}
|
|
108
|
+
process.stderr.write('用法: dsh-any-agent team on|off|status\n');
|
|
109
|
+
process.exit(2);
|
|
110
|
+
}
|
|
111
|
+
|
|
53
112
|
const { createLogger } = await import('../src/config.mjs');
|
|
54
113
|
const { Hub } = await import('../src/hub.mjs');
|
|
55
114
|
const { createMcpServer } = await import('../src/mcp.mjs');
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 极简 MCP 客户端(demo 用)。
|
|
3
|
+
*
|
|
4
|
+
* 与 `test/mcp-client.mjs` 是同一套换行分隔 JSON-RPC 协议,但这里刻意独立一份:
|
|
5
|
+
* `test/` 不进 npm 发布包,而 demo 要随包分发 —— 依赖它会让 `npm run demo`
|
|
6
|
+
* 在装好的包里直接崩。
|
|
7
|
+
*
|
|
8
|
+
* 只做四件事:拉起服务端、initialize、tools/list、tools/call。
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { spawn } from 'node:child_process';
|
|
12
|
+
import { join } from 'node:path';
|
|
13
|
+
|
|
14
|
+
export class McpClient {
|
|
15
|
+
#child = null;
|
|
16
|
+
#nextId = 1;
|
|
17
|
+
#pending = new Map();
|
|
18
|
+
#buf = '';
|
|
19
|
+
|
|
20
|
+
constructor({ pkgRoot, env = {} }) {
|
|
21
|
+
this.pkgRoot = pkgRoot;
|
|
22
|
+
this.env = env;
|
|
23
|
+
/** 服务端的 stderr 行(demo 默认静默,只有 -v 时才值得看)。 */
|
|
24
|
+
this.stderr = [];
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
get pid() {
|
|
28
|
+
return this.#child?.pid ?? null;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
start() {
|
|
32
|
+
this.#child = spawn(process.execPath, [join(this.pkgRoot, 'bin', 'dsh-any-agent.mjs')], {
|
|
33
|
+
env: { ...process.env, ...this.env },
|
|
34
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
35
|
+
});
|
|
36
|
+
this.#child.stdout.setEncoding('utf8');
|
|
37
|
+
this.#child.stderr.setEncoding('utf8');
|
|
38
|
+
this.#child.stdout.on('data', (d) => this.#onData(d));
|
|
39
|
+
this.#child.stderr.on('data', (d) => {
|
|
40
|
+
for (const line of d.split(/\r?\n/)) if (line.trim()) this.stderr.push(line.trim());
|
|
41
|
+
});
|
|
42
|
+
this.#child.on('exit', (code, signal) => {
|
|
43
|
+
const why = `服务端退出(code=${code}${signal ? `, signal=${signal}` : ''})`;
|
|
44
|
+
for (const [, p] of this.#pending) p.reject(new Error(why));
|
|
45
|
+
this.#pending.clear();
|
|
46
|
+
});
|
|
47
|
+
return this;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
#onData(chunk) {
|
|
51
|
+
this.#buf += chunk;
|
|
52
|
+
let i;
|
|
53
|
+
while ((i = this.#buf.indexOf('\n')) !== -1) {
|
|
54
|
+
const line = this.#buf.slice(0, i);
|
|
55
|
+
this.#buf = this.#buf.slice(i + 1);
|
|
56
|
+
if (!line.trim()) continue;
|
|
57
|
+
let msg;
|
|
58
|
+
try {
|
|
59
|
+
msg = JSON.parse(line);
|
|
60
|
+
} catch {
|
|
61
|
+
throw new Error(`服务端输出了非 JSON 内容: ${line.slice(0, 300)}`);
|
|
62
|
+
}
|
|
63
|
+
if (msg.id === undefined) continue;
|
|
64
|
+
const p = this.#pending.get(msg.id);
|
|
65
|
+
if (!p) continue;
|
|
66
|
+
this.#pending.delete(msg.id);
|
|
67
|
+
clearTimeout(p.timer);
|
|
68
|
+
msg.error ? p.reject(new Error(msg.error.message)) : p.resolve(msg.result);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
request(method, params, timeoutMs = 120_000) {
|
|
73
|
+
const id = this.#nextId++;
|
|
74
|
+
return new Promise((resolve, reject) => {
|
|
75
|
+
const timer = setTimeout(() => {
|
|
76
|
+
if (this.#pending.delete(id)) reject(new Error(`请求超时: ${method}`));
|
|
77
|
+
}, timeoutMs);
|
|
78
|
+
this.#pending.set(id, { resolve, reject, timer });
|
|
79
|
+
this.#child.stdin.write(JSON.stringify({ jsonrpc: '2.0', id, method, params }) + '\n');
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
notify(method, params) {
|
|
84
|
+
this.#child.stdin.write(JSON.stringify({ jsonrpc: '2.0', method, params }) + '\n');
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
async initialize() {
|
|
88
|
+
const r = await this.request('initialize', {
|
|
89
|
+
protocolVersion: '2025-06-18',
|
|
90
|
+
capabilities: {},
|
|
91
|
+
clientInfo: { name: 'dsh-any-agent-demo', version: '1.0.0' },
|
|
92
|
+
});
|
|
93
|
+
this.notify('notifications/initialized', {});
|
|
94
|
+
return r;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
async listTools() {
|
|
98
|
+
return (await this.request('tools/list', {})).tools;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
async callTool(name, args = {}, timeoutMs) {
|
|
102
|
+
return await this.request('tools/call', { name, arguments: args }, timeoutMs);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** 取工具结果里的纯文本。 */
|
|
106
|
+
static text(result) {
|
|
107
|
+
return (result?.content ?? [])
|
|
108
|
+
.filter((c) => c.type === 'text')
|
|
109
|
+
.map((c) => c.text)
|
|
110
|
+
.join('\n');
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
async close() {
|
|
114
|
+
try {
|
|
115
|
+
this.#child.stdin.end();
|
|
116
|
+
} catch {}
|
|
117
|
+
await new Promise((resolve) => {
|
|
118
|
+
const timer = setTimeout(() => {
|
|
119
|
+
try {
|
|
120
|
+
this.#child?.kill();
|
|
121
|
+
} catch {}
|
|
122
|
+
resolve();
|
|
123
|
+
}, 6000);
|
|
124
|
+
this.#child?.once('exit', () => {
|
|
125
|
+
clearTimeout(timer);
|
|
126
|
+
resolve();
|
|
127
|
+
});
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
}
|
package/demo/run.mjs
ADDED
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* 5 分钟真实案例:把一件活派给 DSH,中途插两次话,再回头验收。
|
|
4
|
+
*
|
|
5
|
+
* 这个脚本只用 MCP 协议本身(`initialize` / `tools/list` / `tools/call`),
|
|
6
|
+
* 不 import 服务端的任何内部模块 —— 它就是你自己的客户端会做的事,
|
|
7
|
+
* 只不过换成了命令行。
|
|
8
|
+
*
|
|
9
|
+
* 用法:
|
|
10
|
+
* node demo/run.mjs 真实跑一遍(需要本机 DSH + 可用 provider)
|
|
11
|
+
* node demo/run.mjs --replay 回放上一次录制(零成本、不需要 DSH)
|
|
12
|
+
* node demo/run.mjs --keep 保留临时工作区,方便自己翻结果
|
|
13
|
+
* node demo/run.mjs --save 跑完把这份记录写成 demo/transcript.json
|
|
14
|
+
* node demo/run.mjs --timeout 240 单个回合的最长等待秒数(默认 240)
|
|
15
|
+
* node demo/run.mjs --prompt "…" 换一个任务(会跳过内置的两条插话)
|
|
16
|
+
*
|
|
17
|
+
* 隔离:注册表 / 哨兵 / 写锁登记全部指向临时目录,不会动你日常的
|
|
18
|
+
* `~/.dsh/.dsh-any-agent`。DSH 自己的会话仍落在 `~/.dsh/sessions`(凭据与
|
|
19
|
+
* provider 在那里),脚本结束时会 `dsh_release(forget=true)` 把它移出注册表。
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import { spawnSync } from 'node:child_process';
|
|
23
|
+
import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
|
24
|
+
import { tmpdir } from 'node:os';
|
|
25
|
+
import { dirname, join } from 'node:path';
|
|
26
|
+
import { fileURLToPath } from 'node:url';
|
|
27
|
+
|
|
28
|
+
import { McpClient } from './mcp-client.mjs';
|
|
29
|
+
|
|
30
|
+
const PKG_ROOT = join(dirname(fileURLToPath(import.meta.url)), '..');
|
|
31
|
+
const TRANSCRIPT = join(PKG_ROOT, 'demo', 'transcript.json');
|
|
32
|
+
|
|
33
|
+
// ── 参数 ────────────────────────────────────────────────────────────
|
|
34
|
+
const argv = process.argv.slice(2);
|
|
35
|
+
const flag = (name) => argv.includes(name);
|
|
36
|
+
const opt = (name, fallback) => {
|
|
37
|
+
const i = argv.indexOf(name);
|
|
38
|
+
return i !== -1 && argv[i + 1] && !argv[i + 1].startsWith('-') ? argv[i + 1] : fallback;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
if (flag('-h') || flag('--help')) {
|
|
42
|
+
console.log(
|
|
43
|
+
[
|
|
44
|
+
'dsh-any-agent demo —— 派活 → 插话 → 验收',
|
|
45
|
+
'',
|
|
46
|
+
' node demo/run.mjs real run (needs a local DSH + a working provider)',
|
|
47
|
+
' node demo/run.mjs --replay replay the last recorded run (no DSH, no tokens)',
|
|
48
|
+
' node demo/run.mjs --keep keep the temp workspace',
|
|
49
|
+
' node demo/run.mjs --save write demo/transcript.json from this run',
|
|
50
|
+
' node demo/run.mjs --timeout 240 seconds to wait per turn (default 240)',
|
|
51
|
+
' node demo/run.mjs --prompt "…" use your own task instead of the built-in one',
|
|
52
|
+
'',
|
|
53
|
+
'Env: DSH_BIN, DSH_HOME, DSH_MCP_PROFILE (same as the server).',
|
|
54
|
+
].join('\n'),
|
|
55
|
+
);
|
|
56
|
+
process.exit(0);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const TURN_TIMEOUT_S = Number(opt('--timeout', '240')) || 240;
|
|
60
|
+
const KEEP = flag('--keep');
|
|
61
|
+
const SAVE = flag('--save');
|
|
62
|
+
|
|
63
|
+
// ── 记录器:打印 + 收集,--save 时落盘 ──────────────────────────────
|
|
64
|
+
const log = [];
|
|
65
|
+
function say(line = '') {
|
|
66
|
+
process.stdout.write(line + '\n');
|
|
67
|
+
log.push(line);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// ── --replay:零成本回放 ────────────────────────────────────────────
|
|
71
|
+
if (flag('--replay')) {
|
|
72
|
+
if (!existsSync(TRANSCRIPT)) {
|
|
73
|
+
console.error(`还没有录制过:${TRANSCRIPT} 不存在。\n先真实跑一次:node demo/run.mjs --save`);
|
|
74
|
+
process.exit(1);
|
|
75
|
+
}
|
|
76
|
+
const doc = JSON.parse(readFileSync(TRANSCRIPT, 'utf8'));
|
|
77
|
+
say(`(回放:${doc.recorded_at} 在本机录制的真实运行,不是现场跑的)`);
|
|
78
|
+
say(` 任务:${doc.prompt}`);
|
|
79
|
+
say('');
|
|
80
|
+
for (const line of doc.lines) say(line);
|
|
81
|
+
process.exit(0);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// ── 真实运行 ────────────────────────────────────────────────────────
|
|
85
|
+
const DEFAULT_PROMPT = [
|
|
86
|
+
'这是一个自动化演示,请直接动手,不要做架构/影响面分析,不要创建 .birdview/ 之类的产物,不要使用技能。',
|
|
87
|
+
'',
|
|
88
|
+
'在这个空目录里做一件小事,并自己验证:',
|
|
89
|
+
'',
|
|
90
|
+
'1. 写 fib.mjs:ESM 导出 fib(n),fib(0)=0、fib(1)=1,用迭代实现,不要递归。',
|
|
91
|
+
'2. 写 test-fib.mjs:用 node:test + node:assert 覆盖 fib(0)、fib(1)、fib(10)=55。',
|
|
92
|
+
'3. 运行 `node --test test-fib.mjs`,确认全部通过。',
|
|
93
|
+
'',
|
|
94
|
+
'只在当前目录里建这两个文件,不要建别的文件,不要安装任何依赖。',
|
|
95
|
+
].join('\n');
|
|
96
|
+
|
|
97
|
+
const PROMPT = opt('--prompt', DEFAULT_PROMPT);
|
|
98
|
+
const CUSTOM = PROMPT !== DEFAULT_PROMPT;
|
|
99
|
+
|
|
100
|
+
const INTERJECTS = [
|
|
101
|
+
'补充一条要求:fib(n) 收到负数要抛 RangeError("n must be >= 0"),测试里补一条对应断言。',
|
|
102
|
+
'再补一条:在 fib.mjs 顶上加一行注释,说明时间复杂度是 O(n)。其它别动。',
|
|
103
|
+
];
|
|
104
|
+
|
|
105
|
+
const t0 = Date.now();
|
|
106
|
+
const since = () => `+${((Date.now() - t0) / 1000).toFixed(1)}s`;
|
|
107
|
+
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
108
|
+
|
|
109
|
+
// 隔离目录:注册表 / 哨兵 / 锁登记都放这里
|
|
110
|
+
const base = mkdtempSync(join(tmpdir(), 'dsh-any-agent-demo-'));
|
|
111
|
+
const stateDir = join(base, 'state');
|
|
112
|
+
const workspace = join(base, 'workspace');
|
|
113
|
+
mkdirSync(stateDir, { recursive: true });
|
|
114
|
+
mkdirSync(workspace, { recursive: true });
|
|
115
|
+
|
|
116
|
+
const env = {
|
|
117
|
+
DSH_MCP_STATE: join(stateDir, 'conversations.json'),
|
|
118
|
+
DSH_MCP_RUNS_DIR: join(stateDir, 'runs'),
|
|
119
|
+
DSH_MCP_LOCKS_DIR: join(stateDir, 'locks'),
|
|
120
|
+
DSH_MCP_LOG: 'silent',
|
|
121
|
+
DSH_MCP_PERMISSION: 'workspace-write',
|
|
122
|
+
DSH_MCP_APPROVAL: 'auto-allow',
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
say('dsh-any-agent · 5 分钟真实案例');
|
|
126
|
+
say('='.repeat(64));
|
|
127
|
+
say(`工作区:${workspace}`);
|
|
128
|
+
say(`注册表:${env.DSH_MCP_STATE}(隔离,不动你的日常状态)`);
|
|
129
|
+
say('');
|
|
130
|
+
|
|
131
|
+
// 前置检查:Node 版本 + DSH 是否可用。早失败 > 跑一半失败。
|
|
132
|
+
const [major, minor] = process.versions.node.split('.').map(Number);
|
|
133
|
+
if (major < 22 || (major === 22 && minor < 15)) {
|
|
134
|
+
console.error(
|
|
135
|
+
`Node ${process.versions.node} 太旧:服务端要用 node:zlib 的 zstdDecompressSync 读会话头,需要 >= 22.15。`,
|
|
136
|
+
);
|
|
137
|
+
process.exit(1);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
let resolveDshBin;
|
|
141
|
+
try {
|
|
142
|
+
({ resolveDshBin } = await import('../src/config.mjs'));
|
|
143
|
+
} catch (e) {
|
|
144
|
+
console.error(`加载配置失败:${e.message}`);
|
|
145
|
+
process.exit(1);
|
|
146
|
+
}
|
|
147
|
+
try {
|
|
148
|
+
say(`DSH:${resolveDshBin()}`);
|
|
149
|
+
} catch (e) {
|
|
150
|
+
console.error(
|
|
151
|
+
[
|
|
152
|
+
'找不到可用的 DSH。这个 demo 演示的是「让 DSH 干活」,不是「不装 DSH 也能跑」。',
|
|
153
|
+
' 装一份:npm i -g @deepseek-ai/dsh (或设置 DSH_BIN 指向它的 lib/bin.js)',
|
|
154
|
+
` 原因:${e.message}`,
|
|
155
|
+
].join('\n'),
|
|
156
|
+
);
|
|
157
|
+
process.exit(1);
|
|
158
|
+
}
|
|
159
|
+
say('');
|
|
160
|
+
|
|
161
|
+
const client = new McpClient({ pkgRoot: PKG_ROOT, env }).start();
|
|
162
|
+
let exitCode = 0;
|
|
163
|
+
|
|
164
|
+
/** 从 tools/call 的返回里同时拿结构化字段和可读文本。 */
|
|
165
|
+
const info = (res) => res?.structuredContent ?? {};
|
|
166
|
+
const text = (res) => McpClient.text(res);
|
|
167
|
+
const sentinelStatus = (file) => {
|
|
168
|
+
try {
|
|
169
|
+
return JSON.parse(readFileSync(file, 'utf8'));
|
|
170
|
+
} catch {
|
|
171
|
+
return null;
|
|
172
|
+
}
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
try {
|
|
176
|
+
// 1. 握手
|
|
177
|
+
say(`[1/6] ${since()} initialize —— 服务端只要求 4 个标准方法`);
|
|
178
|
+
await client.initialize();
|
|
179
|
+
const tools = await client.listTools();
|
|
180
|
+
say(` 已连接,暴露 ${tools.length} 个工具:${tools.map((t) => t.name).join(', ')}`);
|
|
181
|
+
|
|
182
|
+
// 2. 建会话
|
|
183
|
+
say('');
|
|
184
|
+
say(`[2/6] ${since()} dsh_start —— 建一个长驻会话`);
|
|
185
|
+
const started = await client.callTool('dsh_start', {
|
|
186
|
+
cwd: workspace,
|
|
187
|
+
title_hint: 'demo: fib + test',
|
|
188
|
+
permission: 'workspace-write',
|
|
189
|
+
});
|
|
190
|
+
const convId = info(started).conversation_id;
|
|
191
|
+
if (!convId) throw new Error(`dsh_start 没返回 conversation_id:\n${text(started)}`);
|
|
192
|
+
say(` 会话 ${convId}`);
|
|
193
|
+
|
|
194
|
+
// 3. 派活(不等)
|
|
195
|
+
say('');
|
|
196
|
+
say(`[3/6] ${since()} dsh_send(wait=false) —— 派完立刻返回,回合在后台跑`);
|
|
197
|
+
const sent = await client.callTool('dsh_send', {
|
|
198
|
+
conversation_id: convId,
|
|
199
|
+
prompt: PROMPT,
|
|
200
|
+
wait: false,
|
|
201
|
+
});
|
|
202
|
+
const sendInfo = info(sent);
|
|
203
|
+
if (!sendInfo.run_id) throw new Error(`dsh_send 没返回 run_id:\n${text(sent)}`);
|
|
204
|
+
const firstRun = sendInfo.run_id;
|
|
205
|
+
const firstSentinel = sendInfo.sentinel_file;
|
|
206
|
+
say(` run_id=${firstRun}`);
|
|
207
|
+
say(` 哨兵=${firstSentinel} ← 跑完由它通知,不用轮询`);
|
|
208
|
+
|
|
209
|
+
// 4. 两次插话。注意:dsh_interject 会一直等到"改口后的那一轮"结束才返回,
|
|
210
|
+
// 所以这一步同时也是"第二轮的验收点"。
|
|
211
|
+
say('');
|
|
212
|
+
say(`[4/6] ${since()} 中途插话 —— 打断当前回合并改口,已做的工作不丢`);
|
|
213
|
+
let last = null;
|
|
214
|
+
let n = 0;
|
|
215
|
+
for (const message of INTERJECTS) {
|
|
216
|
+
if (CUSTOM) break;
|
|
217
|
+
await sleep(4000);
|
|
218
|
+
const res = await client.callTool(
|
|
219
|
+
'dsh_interject',
|
|
220
|
+
{ conversation_id: convId, message, timeout_ms: TURN_TIMEOUT_S * 1000 },
|
|
221
|
+
(TURN_TIMEOUT_S + 60) * 1000,
|
|
222
|
+
);
|
|
223
|
+
const r = info(res);
|
|
224
|
+
n += 1;
|
|
225
|
+
// interject 的返回是"改口后那一轮"的结果:有的版本带 status,有的只带 stop_reason。
|
|
226
|
+
// 两个都认,别把一次正常结束报成 undefined。
|
|
227
|
+
const state = r.status ?? (r.error ? 'error' : r.stop_reason === 'end_turn' ? 'done' : r.stop_reason);
|
|
228
|
+
if (r.interjected === 'idle') {
|
|
229
|
+
say(` 插话 #${n}:会话当时已空闲,按普通回合执行;status=${state}(${since()})`);
|
|
230
|
+
} else {
|
|
231
|
+
say(
|
|
232
|
+
` 插话 #${n}:已打断上一轮(上一轮跑了 ${((r.waited_ms ?? 0) / 1000).toFixed(1)}s),` +
|
|
233
|
+
`新一轮 status=${state}(${since()})`,
|
|
234
|
+
);
|
|
235
|
+
}
|
|
236
|
+
last = r;
|
|
237
|
+
if (state !== 'done') say(` 本轮未正常结束:${r.error ?? state}`);
|
|
238
|
+
}
|
|
239
|
+
if (CUSTOM) say(' (--prompt 自定义任务,跳过内置插话)');
|
|
240
|
+
|
|
241
|
+
// 5. 收活:先看被打断那一轮的哨兵,再看最后一轮
|
|
242
|
+
say('');
|
|
243
|
+
say(`[5/6] ${since()} 收活 —— 一个回合 = 一个哨兵,被打断的也算`);
|
|
244
|
+
const cancelled = firstSentinel ? sentinelStatus(firstSentinel) : null;
|
|
245
|
+
if (cancelled) {
|
|
246
|
+
say(` 第 1 轮(被打断)哨兵:status=${cancelled.status} ← 没被丢掉,照样落地`);
|
|
247
|
+
}
|
|
248
|
+
if (last?.sentinel_file) {
|
|
249
|
+
const fin = sentinelStatus(last.sentinel_file);
|
|
250
|
+
say(` 最后一轮哨兵:status=${fin?.status ?? '(尚未落地)'} ${last.sentinel_file}`);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// 6. 验收:用 run_id 取正文,然后自己复核磁盘
|
|
254
|
+
say('');
|
|
255
|
+
say(`[6/6] ${since()} dsh_get —— 用 run_id 取回最终结果`);
|
|
256
|
+
const finalRun = last?.run_id ?? firstRun;
|
|
257
|
+
const got = await client.callTool('dsh_get', { conversation_id: convId, run_id: finalRun });
|
|
258
|
+
const answer = info(got).run?.result?.answer ?? '';
|
|
259
|
+
say(
|
|
260
|
+
(answer || text(got))
|
|
261
|
+
.split('\n')
|
|
262
|
+
.filter((l) => l.trim())
|
|
263
|
+
.slice(0, 10)
|
|
264
|
+
.map((l) => ` ${l}`)
|
|
265
|
+
.join('\n'),
|
|
266
|
+
);
|
|
267
|
+
|
|
268
|
+
say('');
|
|
269
|
+
say('—'.repeat(64));
|
|
270
|
+
say('复核(不看它说了什么,看磁盘上有什么)');
|
|
271
|
+
const files = ['fib.mjs', 'test-fib.mjs'].filter((f) => existsSync(join(workspace, f)));
|
|
272
|
+
say(` 产出文件:${files.length ? files.join(', ') : '(无)'}`);
|
|
273
|
+
if (files.includes('test-fib.mjs')) {
|
|
274
|
+
const test = spawnSync(process.execPath, ['--test', 'test-fib.mjs'], {
|
|
275
|
+
cwd: workspace,
|
|
276
|
+
encoding: 'utf8',
|
|
277
|
+
timeout: 60_000,
|
|
278
|
+
});
|
|
279
|
+
say(` node --test:exit=${test.status}`);
|
|
280
|
+
for (const line of (test.stdout ?? '').trim().split('\n').filter(Boolean).slice(-3)) {
|
|
281
|
+
say(` ${line.trim()}`);
|
|
282
|
+
}
|
|
283
|
+
if (test.status !== 0) exitCode = 1;
|
|
284
|
+
} else {
|
|
285
|
+
exitCode = 1;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// 状态快照:锁在谁手上、内存多少、是否共享进程
|
|
289
|
+
const status = await client.callTool('dsh_status', { conversation_id: convId }).catch(() => null);
|
|
290
|
+
if (status) {
|
|
291
|
+
say('');
|
|
292
|
+
say('状态快照(dsh_status)');
|
|
293
|
+
for (const line of text(status).split('\n').slice(0, 8)) say(` ${line}`);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
await client.callTool('dsh_release', { conversation_id: convId, forget: true }).catch(() => null);
|
|
297
|
+
} catch (e) {
|
|
298
|
+
exitCode = 1;
|
|
299
|
+
say('');
|
|
300
|
+
say(`!! 失败:${e.message}`);
|
|
301
|
+
if (client.stderr.length) {
|
|
302
|
+
say('服务端 stderr(最后 10 行):');
|
|
303
|
+
for (const l of client.stderr.slice(-10)) say(` ${l}`);
|
|
304
|
+
}
|
|
305
|
+
} finally {
|
|
306
|
+
await client.close();
|
|
307
|
+
if (!KEEP) rmSync(base, { recursive: true, force: true });
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
say('');
|
|
311
|
+
say('='.repeat(64));
|
|
312
|
+
say(
|
|
313
|
+
`${exitCode === 0 ? '完成' : '未通过'} · 总耗时 ${((Date.now() - t0) / 1000).toFixed(1)}s` +
|
|
314
|
+
(KEEP ? ` · 工作区保留在 ${workspace}` : ''),
|
|
315
|
+
);
|
|
316
|
+
|
|
317
|
+
if (SAVE && exitCode === 0) {
|
|
318
|
+
writeFileSync(
|
|
319
|
+
TRANSCRIPT,
|
|
320
|
+
JSON.stringify({ recorded_at: new Date().toISOString(), prompt: PROMPT, lines: log }, null, 2) + '\n',
|
|
321
|
+
'utf8',
|
|
322
|
+
);
|
|
323
|
+
say(`已录制:${TRANSCRIPT}(之后可用 --replay 零成本回放)`);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
process.exit(exitCode);
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
{
|
|
2
|
+
"recorded_at": "2026-11-03T08:16:28.187Z",
|
|
3
|
+
"prompt": "这是一个自动化演示,请直接动手,不要做架构/影响面分析,不要创建 .birdview/ 之类的产物,不要使用技能。\n\n在这个空目录里做一件小事,并自己验证:\n\n1. 写 fib.mjs:ESM 导出 fib(n),fib(0)=0、fib(1)=1,用迭代实现,不要递归。\n2. 写 test-fib.mjs:用 node:test + node:assert 覆盖 fib(0)、fib(1)、fib(10)=55。\n3. 运行 `node --test test-fib.mjs`,确认全部通过。\n\n只在当前目录里建这两个文件,不要建别的文件,不要安装任何依赖。",
|
|
4
|
+
"lines": [
|
|
5
|
+
"dsh-any-agent · 5 分钟真实案例",
|
|
6
|
+
"================================================================",
|
|
7
|
+
"工作区:C:\\Users\\xmtlz\\AppData\\Local\\Temp\\dsh-any-agent-demo-R2f2pV\\workspace",
|
|
8
|
+
"注册表:C:\\Users\\xmtlz\\AppData\\Local\\Temp\\dsh-any-agent-demo-R2f2pV\\state\\conversations.json(隔离,不动你的日常状态)",
|
|
9
|
+
"",
|
|
10
|
+
"DSH:C:\\Users\\xmtlz\\AppData\\Local\\npm-cache\\_npx\\57331acab1a6b93f\\node_modules\\@deepseek-ai\\dsh\\lib\\bin.js",
|
|
11
|
+
"",
|
|
12
|
+
"[1/6] +0.1s initialize —— 服务端只要求 4 个标准方法",
|
|
13
|
+
" 已连接,暴露 14 个工具:dsh_start, dsh_send, dsh_list, dsh_read, dsh_get, dsh_interject, dsh_interrupt, dsh_takeover, dsh_release, dsh_approval_decide, dsh_status, dsh_inbox, dsh_queue, dsh_doctor",
|
|
14
|
+
"",
|
|
15
|
+
"[2/6] +0.2s dsh_start —— 建一个长驻会话",
|
|
16
|
+
" 会话 cfc31c11-b407-4917-86d9-ccf851f06228",
|
|
17
|
+
"",
|
|
18
|
+
"[3/6] +7.6s dsh_send(wait=false) —— 派完立刻返回,回合在后台跑",
|
|
19
|
+
" run_id=run-mw0exu3d-1",
|
|
20
|
+
" 哨兵=C:\\Users\\xmtlz\\AppData\\Local\\Temp\\dsh-any-agent-demo-R2f2pV\\state\\runs\\cfc31c11-b407-4917-86d9-ccf851f06228\\run-mw0exu3d-1.json ← 跑完由它通知,不用轮询",
|
|
21
|
+
"",
|
|
22
|
+
"[4/6] +7.7s 中途插话 —— 打断当前回合并改口,已做的工作不丢",
|
|
23
|
+
" 插话 #1:已打断上一轮(上一轮跑了 0.1s),新一轮 status=done(+38.7s)",
|
|
24
|
+
" 插话 #2:会话当时已空闲,按普通回合执行;status=done(+56.1s)",
|
|
25
|
+
"",
|
|
26
|
+
"[5/6] +56.1s 收活 —— 一个回合 = 一个哨兵,被打断的也算",
|
|
27
|
+
" 第 1 轮(被打断)哨兵:status=cancelled ← 没被丢掉,照样落地",
|
|
28
|
+
" 最后一轮哨兵:status=done C:\\Users\\xmtlz\\AppData\\Local\\Temp\\dsh-any-agent-demo-R2f2pV\\state\\runs\\cfc31c11-b407-4917-86d9-ccf851f06228\\run-mw0eyl8k-3.json",
|
|
29
|
+
"",
|
|
30
|
+
"[6/6] +56.1s dsh_get —— 用 run_id 取回最终结果",
|
|
31
|
+
" 已在 `fib.mjs` 第 1 行加上注释 `// Time complexity: O(n)`,其余内容一字未动,仍只有这两个文件、无依赖。重跑 `node --test test-fib.mjs` 依旧 4/4 通过。",
|
|
32
|
+
"",
|
|
33
|
+
"————————————————————————————————————————————————————————————————",
|
|
34
|
+
"复核(不看它说了什么,看磁盘上有什么)",
|
|
35
|
+
" 产出文件:fib.mjs, test-fib.mjs",
|
|
36
|
+
" node --test:exit=0",
|
|
37
|
+
" ℹ skipped 0",
|
|
38
|
+
" ℹ todo 0",
|
|
39
|
+
" ℹ duration_ms 131.9934",
|
|
40
|
+
"",
|
|
41
|
+
"状态快照(dsh_status)",
|
|
42
|
+
" 状态=idle | 轮数=3 | 最近回合=run-mw0eyl8k-3(done) | 待决审批=0 | rss=214MB",
|
|
43
|
+
"",
|
|
44
|
+
"================================================================",
|
|
45
|
+
"完成 · 总耗时 57.6s"
|
|
46
|
+
]
|
|
47
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# README artwork
|
|
2
|
+
|
|
3
|
+
`header.jpg` is the shared hero for the English and Chinese READMEs: a blue-haired chibi whale-girl enjoying white rice at a coding desk, with subtle terminal connections behind her.
|
|
4
|
+
|
|
5
|
+
- Generated and refined with the built-in GPT Image tool on 2026-11-02.
|
|
6
|
+
- The selected version includes the requested slimmer cheeks and leg proportions.
|
|
7
|
+
- Exported as an optimized progressive JPEG, 1983 × 793 pixels, 282,513 bytes.
|
|
8
|
+
- The READMEs use the public raw GitHub URL so the same image works on GitHub and npm.
|
|
9
|
+
- Character reference: the community-created [rice-eating DeepSeek whale-girl meme](https://www.vgover.com/news/227900).
|
|
10
|
+
- Visual direction: the dark desktop and blue/amber lighting in [dsh-notify-me's header](https://github.com/chromoany/dsh-notify-me/blob/main/docs/brand/header.jpg).
|
|
11
|
+
|
|
12
|
+
## Generation and edit prompts
|
|
13
|
+
|
|
14
|
+
<details>
|
|
15
|
+
<summary>Original generation</summary>
|
|
16
|
+
|
|
17
|
+
```text
|
|
18
|
+
Use case: ads-marketing.
|
|
19
|
+
Asset type: preview concept for a GitHub README hero banner for dsh-any-agent.
|
|
20
|
+
Primary request: Create an adorable, polished, wide illustrated banner based on the Chinese DeepSeek community fan-character meme "吃白饭的蓝色大肥鱼", a blue-haired chibi whale-girl happily eating plain white rice. This is the humanlike whale-girl character, NOT an ordinary whale animal. Create a fresh scene and composition, not a reproduction of a meme panel.
|
|
21
|
+
Canvas/composition: Wide landscape banner, about 2.5:1, around 2000 x 800. The left half has beautifully typeset project text with ample breathing room. The right half features one large charming chibi whale-girl seated comfortably at a little developer desk, holding a bowl of fluffy steaming white rice and eating a bite with chopsticks. Character and rice bowl should be large and instantly readable. Her whole head, whale fins and tail must fit comfortably within frame. Balance a clean left typography area with a delightful illustrated right area.
|
|
22
|
+
Character appearance: Big round face, very short cuddly chibi proportions around 2.5 heads tall, round rosy cheeks puffed with a mouthful of rice, large expressive blue eyes with a delighted slightly mischievous look; indigo-blue voluminous long wavy hair shading to pale sky-blue tips, a single curved ahoge on top; blue whale-fin-shaped ears with pale scalloped undersides, small blue hair ribbon, white frilled maid headband. Fully clothed in a cute modest navy-blue and white long-sleeved maid dress with a white apron, small navy neck bow, round-toed dark shoes. One blue whale tail emerges naturally behind her and curves out beside her. Keep the character round and soft, not a tall fashion illustration. A tiny rice grain at the corner of her mouth is a sweet detail. A small blue whale mark decorates the rice bowl.
|
|
23
|
+
Scene/backdrop: Cozy midnight-blue coding workspace. A small open laptop beside her shows a few abstract quiet code lines; it stays secondary. Faint elegant terminal-window outlines and fine blue connecting paths in the distant background hint that the project bridges multiple MCP clients to one persistent agent. Minimal desk surface along bottom. Soft warm amber desk-lamp light and cool blue edge light create a welcoming late-night coding mood. Leave the left typography backdrop clean and dark, with no busy objects behind letters.
|
|
24
|
+
Style/medium: Excellent hand-drawn anime chibi editorial illustration, crisp confident outlines, soft cel shading with delicate painterly light, rich restrained blue palette, warm white rice and soft peach cheeks, cute and premium rather than photorealistic or glossy 3D. All parts of scene share one coherent illustration style. The character eating rice is the visual focal point; terminal elements remain very subtle.
|
|
25
|
+
Text (verbatim): "dsh-any-agent"
|
|
26
|
+
Subtitle (verbatim): "A coding buddy that sticks around."
|
|
27
|
+
Typography: Large very legible off-white bold rounded modern sans-serif title on left, all lower case, hyphens precisely correct. Smaller muted light-blue subtitle directly beneath. Title must remain readable at README width. Use only these two pieces of text.
|
|
28
|
+
Constraints: One character, one bowl of clearly visible plain white rice, one anatomically coherent pair of hands holding bowl and chopsticks naturally. Correct fin ears, long gradient blue hair, chibi face, maid headband, single whale tail. No additional people, no animal-whale substitute, no comic panels, no speech bubbles, no other text, no watermark, no badges, no screenshots, no random typography. Opaque full-bleed dark-blue background, generous safe margins, lovely expressive face and readable rice-eating action.
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
</details>
|
|
32
|
+
|
|
33
|
+
<details>
|
|
34
|
+
<summary>Face refinement</summary>
|
|
35
|
+
|
|
36
|
+
```text
|
|
37
|
+
Use case: precise-object-edit.
|
|
38
|
+
Image 1 is the EDIT TARGET: the existing wide dsh-any-agent rice-eating whale-girl banner.
|
|
39
|
+
Image 2 is a CHARACTER FACE REFERENCE ONLY: the original community meme character. Do not copy its text, pose, white background, pointing hand or empty bowl.
|
|
40
|
+
User request: The banner character's face is too chubby. Make the face slightly slimmer and closer to the original reference, while preserving her cute chibi identity.
|
|
41
|
+
Change ONLY the face and the tiny adjoining hair/neck boundaries needed for a natural contour. Reduce the exaggerated sideways cheek bulges and puffed-cheek inflation. Narrow the visible facial contour subtly, approximately 10 to 15 percent, particularly the lower cheeks. Give her the softly rounded, gently tapered cheeks and neat small rounded chin seen in Image 2. Keep a cute round face, NOT a sharp V-shaped chin or elongated adult face. Maintain approximately the same eye size, blue irises and eye style; make the expression a gentle pleased little closed-mouth smile while eating, with only mild fullness in the cheeks. Preserve the tiny rice grain and soft blush.
|
|
42
|
+
Invariants: Keep Image 1's exact wide canvas and composition, project name "dsh-any-agent", subtitle "A coding buddy that sticks around.", exact typography and text placement, all background and terminal windows, desk and laptop, hands/chopsticks/rice bowl, outfit and maid headband, blue gradient long hair, fin ears, whale tail, pose, color palette and lighting. Do not redraw or restyle the rest of the banner. Only a subtle face slimming toward the character reference. No new text or objects.
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
</details>
|
|
46
|
+
|
|
47
|
+
<details>
|
|
48
|
+
<summary>Leg proportions</summary>
|
|
49
|
+
|
|
50
|
+
```text
|
|
51
|
+
Use case: precise-object-edit.
|
|
52
|
+
Edit target: the supplied wide dsh-any-agent whale-girl banner.
|
|
53
|
+
User request: her legs look a little too thick; make them slightly slimmer and more naturally proportioned while retaining the cute chibi style.
|
|
54
|
+
Change ONLY the small lower-body region at the bottom center-right, where the seated character's knees, short legs and navy shoes appear immediately above the desk edge. Subtly reduce the widths of the visible thighs, knees and lower legs by about 15–20 percent so they look lighter and less bulky. Refine the knee contours into a soft natural shape. Reduce oversized shoe bulk just enough to match the narrower legs. Keep the legs short and softly rounded to suit the existing chibi proportions; maintain the same seated posture, clothing coverage, skirt/stocking/shoe design, and modest framing. Adjust only adjoining skirt folds or the tiny surrounding chair area as strictly necessary for a coherent silhouette.
|
|
55
|
+
Invariants: Preserve the already corrected slimmer face EXACTLY, facial expression, eye size, hair, headband, fin ears and tail, all upper-body proportions, hands, rice bowl, chopsticks, rice, laptop, desk, chair, background terminals, lighting, colors and existing illustration style. Preserve the exact canvas, image framing and all text: "dsh-any-agent" and "A coding buddy that sticks around." Keep typography and placement unchanged. This is a localized subtle leg-proportion correction, not a redesign. No additional objects or text.
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
</details>
|
|
Binary file
|