dsh-any-agent 0.3.8 → 0.4.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 +175 -2
- package/DESIGN.md +6 -6
- package/README.md +105 -27
- package/README.zh.md +90 -27
- package/USAGE-MIGRATION.en.md +7 -5
- package/USAGE-MIGRATION.md +3 -3
- package/USAGE-NOTES.en.md +4 -4
- package/USAGE-NOTES.md +9 -9
- package/bin/restore.mjs +6 -2
- package/bin/setup.mjs +6 -0
- package/package.json +1 -1
- package/src/acp.mjs +39 -13
- package/src/audit.mjs +32 -0
- package/src/bootstrap.mjs +26 -1
- package/src/clients.mjs +13 -2
- package/src/config.mjs +137 -6
- package/src/host.mjs +148 -0
- package/src/hub.mjs +508 -92
- package/src/line-decoder.mjs +41 -0
- package/src/locks.mjs +6 -1
- package/src/mcp.mjs +3 -6
- package/src/procstat.mjs +72 -0
- package/src/tools.mjs +546 -29
- package/src/workspace.mjs +18 -7
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 行式 UTF-8 解码器。
|
|
3
|
+
*
|
|
4
|
+
* Node 的 stream 回调给的是 Buffer;直接对每个 chunk 调 `toString('utf8')`
|
|
5
|
+
* 会在 chunk 边界把一个多字节字符(中文/emoji)截成 U+FFFD,JSON 仍然合法,
|
|
6
|
+
* 但内容被静默改写。StringDecoder 会跨 chunk 保留未完成的多字节序列。
|
|
7
|
+
*
|
|
8
|
+
* 同时负责按 `\n` 切行;行内保留 `\r`,由调用方按需 trim。
|
|
9
|
+
*/
|
|
10
|
+
import { StringDecoder } from 'node:string_decoder';
|
|
11
|
+
|
|
12
|
+
export class LineDecoder {
|
|
13
|
+
#decoder = new StringDecoder('utf8');
|
|
14
|
+
#buf = '';
|
|
15
|
+
|
|
16
|
+
/** 喂一个 Buffer;返回本轮切出的完整行(不含换行符)。 */
|
|
17
|
+
push(chunk) {
|
|
18
|
+
this.#buf += this.#decoder.write(chunk);
|
|
19
|
+
return this.#drain(false);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** EOF:把 decoder 里残留的半个字符和最后一行一起吐出来。 */
|
|
23
|
+
end() {
|
|
24
|
+
this.#buf += this.#decoder.end();
|
|
25
|
+
return this.#drain(true);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
#drain(flush) {
|
|
29
|
+
const lines = [];
|
|
30
|
+
let i;
|
|
31
|
+
while ((i = this.#buf.indexOf('\n')) !== -1) {
|
|
32
|
+
lines.push(this.#buf.slice(0, i));
|
|
33
|
+
this.#buf = this.#buf.slice(i + 1);
|
|
34
|
+
}
|
|
35
|
+
if (flush && this.#buf) {
|
|
36
|
+
lines.push(this.#buf);
|
|
37
|
+
this.#buf = '';
|
|
38
|
+
}
|
|
39
|
+
return lines;
|
|
40
|
+
}
|
|
41
|
+
}
|
package/src/locks.mjs
CHANGED
|
@@ -106,7 +106,12 @@ export function writeMarker(conversationId, { childPid, cwd }) {
|
|
|
106
106
|
mcp_token: MCP_TOKEN,
|
|
107
107
|
child_pid: childPid ?? null,
|
|
108
108
|
cwd: cwd ?? null,
|
|
109
|
-
|
|
109
|
+
// 只有同一个子进程才沿用旧启动时间;子进程换了(PID 变化)必须重置,
|
|
110
|
+
// 否则 PID 复用保护会把新进程误判成“复用旧 PID”而拒绝 takeover。
|
|
111
|
+
spawned_at:
|
|
112
|
+
prev && prev.child_pid === childPid && Number.isFinite(prev.spawned_at)
|
|
113
|
+
? prev.spawned_at
|
|
114
|
+
: now,
|
|
110
115
|
heartbeat_at: now,
|
|
111
116
|
});
|
|
112
117
|
return true;
|
package/src/mcp.mjs
CHANGED
|
@@ -10,6 +10,7 @@ import { dirname, join } from 'node:path';
|
|
|
10
10
|
import { fileURLToPath } from 'node:url';
|
|
11
11
|
import { createHandlers, TOOL_DEFS } from './tools.mjs';
|
|
12
12
|
import { DEFAULT_PERMISSION } from './config.mjs';
|
|
13
|
+
import { LineDecoder } from './line-decoder.mjs';
|
|
13
14
|
|
|
14
15
|
const SERVER_NAME = 'dsh-any-agent';
|
|
15
16
|
/**
|
|
@@ -117,7 +118,7 @@ function fileURLToPathSafe(uri) {
|
|
|
117
118
|
|
|
118
119
|
export function createMcpServer({ hub, toolDefs = TOOL_DEFS, handlers = createHandlers(hub), log }) {
|
|
119
120
|
const write = (obj) => process.stdout.write(JSON.stringify(obj) + '\n');
|
|
120
|
-
|
|
121
|
+
const lineDecoder = new LineDecoder();
|
|
121
122
|
let initialized = false;
|
|
122
123
|
let clientCapabilities = {};
|
|
123
124
|
let rootsSeq = 0;
|
|
@@ -251,11 +252,7 @@ export function createMcpServer({ hub, toolDefs = TOOL_DEFS, handlers = createHa
|
|
|
251
252
|
}
|
|
252
253
|
|
|
253
254
|
function onData(chunk) {
|
|
254
|
-
|
|
255
|
-
let idx;
|
|
256
|
-
while ((idx = buffer.indexOf('\n')) !== -1) {
|
|
257
|
-
const line = buffer.slice(0, idx);
|
|
258
|
-
buffer = buffer.slice(idx + 1);
|
|
255
|
+
for (const line of lineDecoder.push(chunk)) {
|
|
259
256
|
if (!line.trim()) continue;
|
|
260
257
|
let msg;
|
|
261
258
|
try {
|
package/src/procstat.mjs
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 子进程 RSS 读取(只用于观测与内存预算,不参与任何协议)。
|
|
3
|
+
*
|
|
4
|
+
* 平台差异:
|
|
5
|
+
* - Windows:`tasklist /FO CSV /NH` 第五列是 "12,345 K"
|
|
6
|
+
* - Unix:`ps -axo pid=,rss=` 第二列是 KB
|
|
7
|
+
*
|
|
8
|
+
* 读取失败一律返回空 Map / null —— 量不到内存不是故障,绝不能因此回收会话。
|
|
9
|
+
* 结果缓存 5 秒,避免 reaper(默认 30 秒一轮)和 dsh_list 反复拉起进程。
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { spawnSync } from 'node:child_process';
|
|
13
|
+
|
|
14
|
+
const CACHE_MS = 5000;
|
|
15
|
+
let cache = { at: 0, map: new Map() };
|
|
16
|
+
|
|
17
|
+
function parseTasklist(out) {
|
|
18
|
+
const map = new Map();
|
|
19
|
+
for (const line of out.split('\n')) {
|
|
20
|
+
const cols = line.match(/"[^"]*"/g);
|
|
21
|
+
if (!cols || cols.length < 5) continue;
|
|
22
|
+
const pid = Number(cols[1].slice(1, -1));
|
|
23
|
+
const kb = /([\d,]+)\s*K/i.exec(cols[4]);
|
|
24
|
+
if (Number.isFinite(pid) && kb) map.set(pid, Number(kb[1].replace(/,/g, '')) / 1024);
|
|
25
|
+
}
|
|
26
|
+
return map;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function parsePs(out) {
|
|
30
|
+
const map = new Map();
|
|
31
|
+
for (const line of out.split('\n')) {
|
|
32
|
+
const m = /^\s*(\d+)\s+(\d+)\s*$/.exec(line);
|
|
33
|
+
if (m) map.set(Number(m[1]), Number(m[2]) / 1024);
|
|
34
|
+
}
|
|
35
|
+
return map;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** @returns {Map<number, number>} pid → MB */
|
|
39
|
+
export function readRssMap({ force = false } = {}) {
|
|
40
|
+
const now = Date.now();
|
|
41
|
+
if (!force && now - cache.at < CACHE_MS) return cache.map;
|
|
42
|
+
let map = new Map();
|
|
43
|
+
try {
|
|
44
|
+
const r =
|
|
45
|
+
process.platform === 'win32'
|
|
46
|
+
? spawnSync('tasklist', ['/FO', 'CSV', '/NH'], {
|
|
47
|
+
encoding: 'utf8',
|
|
48
|
+
timeout: 3000,
|
|
49
|
+
windowsHide: true,
|
|
50
|
+
maxBuffer: 8 << 20,
|
|
51
|
+
})
|
|
52
|
+
: spawnSync('ps', ['-axo', 'pid=,rss='], {
|
|
53
|
+
encoding: 'utf8',
|
|
54
|
+
timeout: 3000,
|
|
55
|
+
maxBuffer: 8 << 20,
|
|
56
|
+
});
|
|
57
|
+
if (r.status === 0 && r.stdout) {
|
|
58
|
+
map = process.platform === 'win32' ? parseTasklist(r.stdout) : parsePs(r.stdout);
|
|
59
|
+
}
|
|
60
|
+
} catch {
|
|
61
|
+
// 读不到就当作未知
|
|
62
|
+
}
|
|
63
|
+
cache = { at: now, map };
|
|
64
|
+
return map;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** @returns {number|null} pid 的 RSS(MB,四舍五入);未知返回 null */
|
|
68
|
+
export function rssMb(pid) {
|
|
69
|
+
if (!pid) return null;
|
|
70
|
+
const v = readRssMap().get(pid);
|
|
71
|
+
return v === undefined ? null : Math.round(v);
|
|
72
|
+
}
|