zhuye 0.1.1 → 0.1.6
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/README.md +11 -2
- package/dist/agent.d.ts +11 -1
- package/dist/agent.js +97 -8
- package/dist/binary.d.ts +16 -0
- package/dist/binary.js +49 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/package.json +9 -2
package/README.md
CHANGED
|
@@ -8,7 +8,16 @@
|
|
|
8
8
|
npm install zhuye
|
|
9
9
|
```
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
安装即用:主包通过 optional dependencies 自动安装当前平台的平台子包
|
|
12
|
+
(`zhuye-darwin-arm64` / `zhuye-darwin-x64` / `zhuye-linux-x64` /
|
|
13
|
+
`zhuye-linux-arm64` / `zhuye-win32-x64`),内嵌对应平台的 `zhuye`
|
|
14
|
+
二进制,SDK 优先解析它 —— 无需自装二进制、无需配环境变量。
|
|
15
|
+
|
|
16
|
+
二进制解析优先级:显式 `options.bin` → `ZHUYE_BIN` → 平台子包内嵌
|
|
17
|
+
二进制 → PATH 上的 `zhuye`。当前平台不在支持矩阵(如 musl/Alpine)
|
|
18
|
+
时回退 PATH:从 [GitHub Releases](https://github.com/Asketisch/ZhuYe/releases)
|
|
19
|
+
下载,或从本仓库 `make build` 产出 `target/release/zhuye` 后以
|
|
20
|
+
`ZHUYE_BIN` 指向。
|
|
12
21
|
|
|
13
22
|
## 用法
|
|
14
23
|
|
|
@@ -62,7 +71,7 @@ const handshake = await agent.nextGlobalEvent({ timeoutMs: 5000 });
|
|
|
62
71
|
|
|
63
72
|
| 变量 | 作用 |
|
|
64
73
|
|------|------|
|
|
65
|
-
| `ZHUYE_BIN` | `zhuye` 二进制路径(
|
|
74
|
+
| `ZHUYE_BIN` | `zhuye` 二进制路径(最高优先级覆盖;缺省用平台子包内嵌二进制)。 |
|
|
66
75
|
| `ZHUYE_MODEL` | 模型名(spec,例如 `mock/mock-1` 用于离线 SDK 测试)。 |
|
|
67
76
|
| `ZHUYE_REMOTE_TOOL_TIMEOUT_SECS` | 远程工具等待客户端回执的上限,默认 120s。 |
|
|
68
77
|
|
package/dist/agent.d.ts
CHANGED
|
@@ -5,7 +5,11 @@ import { Event, Op, ReviewDecision, ToolOutput } from './protocol.js';
|
|
|
5
5
|
export type ToolHandler = (args: Record<string, unknown>) => Promise<ToolOutput> | ToolOutput;
|
|
6
6
|
/** ZhuyeAgent 启动选项。 */
|
|
7
7
|
export interface ZhuyeAgentOptions {
|
|
8
|
-
/**
|
|
8
|
+
/**
|
|
9
|
+
* `zhuye` 二进制路径。缺省按优先级解析:env `ZHUYE_BIN` → 随包安装
|
|
10
|
+
* 的平台子包内嵌二进制(`zhuye-<平台>` optional dependency)→
|
|
11
|
+
* PATH 上的 `zhuye`。
|
|
12
|
+
*/
|
|
9
13
|
bin?: string;
|
|
10
14
|
/** 工作目录(传给 serve 的 env)。 */
|
|
11
15
|
cwd?: string;
|
|
@@ -15,6 +19,12 @@ export interface ZhuyeAgentOptions {
|
|
|
15
19
|
spawnOptions?: Omit<SpawnOptions, 'bin' | 'args' | 'env' | 'cwd'>;
|
|
16
20
|
/** `session_configured` 握手超时(ms)。默认 30s。 */
|
|
17
21
|
handshakeTimeoutMs?: number;
|
|
22
|
+
/**
|
|
23
|
+
* 子进程 stderr 接管回调。提供后每个原始块交给回调(落文件 /
|
|
24
|
+
* 送日志系统 / 过滤降噪),不再硬编码直通宿主 stderr。
|
|
25
|
+
* 未提供时维持旧行为(透传宿主 stderr)。
|
|
26
|
+
*/
|
|
27
|
+
onStderr?: (chunk: Buffer) => void;
|
|
18
28
|
}
|
|
19
29
|
interface SpawnOptions {
|
|
20
30
|
bin: string;
|
package/dist/agent.js
CHANGED
|
@@ -13,10 +13,20 @@
|
|
|
13
13
|
//! ```
|
|
14
14
|
import { spawn } from 'node:child_process';
|
|
15
15
|
import { randomUUID } from 'node:crypto';
|
|
16
|
+
import { resolveBundledBinary } from './binary.js';
|
|
16
17
|
const ZHUYE_BIN_ENV = 'ZHUYE_BIN';
|
|
17
18
|
const DEFAULT_BIN = 'zhuye';
|
|
18
19
|
const READLINE_LIMIT = 16 * 1024 * 1024;
|
|
19
20
|
const STDIN_HANDSHAKE_TIMEOUT_MS = 30_000;
|
|
21
|
+
/** 握手失败时随异常携带的 stderr 尾部块数上限。 */
|
|
22
|
+
const STDERR_TAIL_LINES = 32;
|
|
23
|
+
/** 把 stderr 尾部 ring buffer 拼成诊断文本(解码容错)。 */
|
|
24
|
+
function stderrTailText(tail) {
|
|
25
|
+
return tail
|
|
26
|
+
.map((c) => c.toString('utf-8').trim())
|
|
27
|
+
.filter((s) => s.length > 0)
|
|
28
|
+
.join(' | ');
|
|
29
|
+
}
|
|
20
30
|
/**
|
|
21
31
|
* 主类:启动并维护一个常驻 `zhuye serve` 子进程,把 JSONL 协议包装
|
|
22
32
|
* 成 Promise / AsyncIterable 给 TS 用户。
|
|
@@ -46,7 +56,12 @@ export class ZhuyeAgent {
|
|
|
46
56
|
// config 目录都定位不到),options.env 只做增量覆盖。
|
|
47
57
|
const env = { ...process.env, ...options.env };
|
|
48
58
|
const spawnOpts = {
|
|
49
|
-
|
|
59
|
+
// 解析优先级:显式指定 → 环境变量 → 随平台子包内嵌的二进制 →
|
|
60
|
+
// PATH 上的 `zhuye`。内嵌优先于 PATH:它与 SDK 版本严格配套。
|
|
61
|
+
bin: options.bin ??
|
|
62
|
+
process.env[ZHUYE_BIN_ENV] ??
|
|
63
|
+
resolveBundledBinary() ??
|
|
64
|
+
DEFAULT_BIN,
|
|
50
65
|
args: ['serve'],
|
|
51
66
|
cwd: options.cwd,
|
|
52
67
|
env,
|
|
@@ -56,9 +71,20 @@ export class ZhuyeAgent {
|
|
|
56
71
|
env: spawnOpts.env,
|
|
57
72
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
58
73
|
});
|
|
59
|
-
// 子进程 stderr →
|
|
74
|
+
// 子进程 stderr → 提供 onStderr 回调时交给回调;否则透传
|
|
75
|
+
// 当前进程 stderr。无论哪种模式,尾部都保留在 ring buffer 里,
|
|
76
|
+
// 握手期子进程死亡时把真实原因带进异常。
|
|
77
|
+
const stderrTail = [];
|
|
60
78
|
child.stderr.on('data', (chunk) => {
|
|
61
|
-
|
|
79
|
+
stderrTail.push(chunk);
|
|
80
|
+
if (stderrTail.length > STDERR_TAIL_LINES)
|
|
81
|
+
stderrTail.shift();
|
|
82
|
+
if (options.onStderr) {
|
|
83
|
+
options.onStderr(chunk);
|
|
84
|
+
}
|
|
85
|
+
else {
|
|
86
|
+
process.stderr.write(chunk);
|
|
87
|
+
}
|
|
62
88
|
});
|
|
63
89
|
const proc = {
|
|
64
90
|
child,
|
|
@@ -72,29 +98,83 @@ export class ZhuyeAgent {
|
|
|
72
98
|
readyResolve = res;
|
|
73
99
|
readyReject = rej;
|
|
74
100
|
});
|
|
101
|
+
// 握手期死亡时 ready 被拒绝;若调用方从未 await 它(spawn 后
|
|
102
|
+
// 直接 close / 只用全局事件),挂一个 no-op 兜底标记已处理,
|
|
103
|
+
// 避免 unhandled rejection 打爆宿主。后续 await 仍能拿到该错误。
|
|
104
|
+
ready.catch(() => { });
|
|
75
105
|
const agent = new ZhuyeAgent(proc, ready);
|
|
106
|
+
// 握手是否仍在进行 —— 子进程在握手期退出时,ready 立刻拒绝
|
|
107
|
+
// (带 stderr 尾部),不再傻等满握手超时后报泛化的 timed out。
|
|
108
|
+
let handshakeSettled = false;
|
|
76
109
|
// 子进程意外退出 → 通知所有 close 监听者(submit / prompt 等
|
|
77
110
|
// 仍 await 中的 promise 借此抛错 / 收尾)。
|
|
78
111
|
child.on('exit', (code, signal) => {
|
|
79
|
-
|
|
112
|
+
// 阶段标记必须在 exit 时刻捕获:等待 stderr 排空期间握手状态
|
|
113
|
+
// 可能被翻转,措辞判定要以死亡时的阶段为准。
|
|
114
|
+
const wasHandshake = !handshakeSettled;
|
|
115
|
+
// 握手期死亡与运行期死亡措辞区分,方便排障定位。
|
|
116
|
+
const buildErr = () => {
|
|
117
|
+
const tail = stderrTail.length
|
|
118
|
+
? ` stderr tail: ${stderrTailText(stderrTail)}`
|
|
119
|
+
: '';
|
|
120
|
+
return wasHandshake
|
|
121
|
+
? new Error(`zhuye serve exited during handshake (code=${code} signal=${signal ?? 'none'})${tail}`)
|
|
122
|
+
: new Error(`zhuye serve exited unexpectedly (code=${code} signal=${signal ?? 'none'})${tail}`);
|
|
123
|
+
};
|
|
124
|
+
if (!wasHandshake) {
|
|
125
|
+
agent.handleServeExit(buildErr());
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
// 竞态:子进程写 stderr 后立刻退出时,`exit` 事件可能先于
|
|
129
|
+
// stderr 的 `data` 事件投递(管道排空晚于进程退出),此刻同步读
|
|
130
|
+
// stderrTail 会拿到空尾、丢失真实原因。握手失败不在毫秒级关键
|
|
131
|
+
// 路径上 —— 等 `close`(stdio 全部关闭,stderr 必已投递)或至多
|
|
132
|
+
// 500ms 兜底,再构建错误。
|
|
133
|
+
let drainSettled = false;
|
|
134
|
+
const finish = () => {
|
|
135
|
+
if (drainSettled)
|
|
136
|
+
return;
|
|
137
|
+
drainSettled = true;
|
|
138
|
+
// 等待期间 stdout 缓冲可能投递了 session_configured:握手实际
|
|
139
|
+
// 成功则让路,不再以失败拒绝。
|
|
140
|
+
if (handshakeSettled)
|
|
141
|
+
return;
|
|
142
|
+
handshakeSettled = true;
|
|
143
|
+
const err = buildErr();
|
|
144
|
+
readyReject(err);
|
|
145
|
+
agent.handleServeExit(err);
|
|
146
|
+
};
|
|
147
|
+
child.once('close', finish);
|
|
148
|
+
const drainTimer = setTimeout(finish, 500);
|
|
149
|
+
drainTimer.unref?.();
|
|
80
150
|
});
|
|
81
151
|
// spawn 失败(典型:二进制路径不存在,ENOENT):ChildProcess 的
|
|
82
152
|
// error 事件没有监听者会以 uncaught exception 打爆宿主进程;这里
|
|
83
153
|
// 转为握手失败(ready 尚未 settle 时立即拒绝,否则等握手超时)。
|
|
84
154
|
child.on('error', (err) => {
|
|
85
155
|
const failure = new Error(`zhuye serve failed to start: ${err.message}`);
|
|
86
|
-
|
|
156
|
+
if (!handshakeSettled) {
|
|
157
|
+
handshakeSettled = true;
|
|
158
|
+
readyReject(failure);
|
|
159
|
+
}
|
|
87
160
|
agent.handleServeExit(failure);
|
|
88
161
|
});
|
|
89
162
|
const handshakeMs = options.handshakeTimeoutMs ?? STDIN_HANDSHAKE_TIMEOUT_MS;
|
|
90
163
|
const timer = setTimeout(() => {
|
|
91
|
-
|
|
164
|
+
if (handshakeSettled)
|
|
165
|
+
return;
|
|
166
|
+
handshakeSettled = true;
|
|
167
|
+
readyReject(new Error(`zhuye serve handshake timed out after ${handshakeMs}ms` +
|
|
168
|
+
(stderrTail.length
|
|
169
|
+
? ` stderr tail: ${stderrTailText(stderrTail)}`
|
|
170
|
+
: '')));
|
|
92
171
|
}, handshakeMs);
|
|
93
172
|
timer.unref?.();
|
|
94
173
|
// 在握手阶段消费 stdout,直到见到 session_configured 后再切给
|
|
95
174
|
// 业务消费者 —— 简单做法:让后台 consumer 自己处理,ready 在
|
|
96
175
|
// session_configured 时 resolve。
|
|
97
176
|
agent.onceSessionConfigured = () => {
|
|
177
|
+
handshakeSettled = true;
|
|
98
178
|
clearTimeout(timer);
|
|
99
179
|
readyResolve();
|
|
100
180
|
};
|
|
@@ -114,6 +194,9 @@ export class ZhuyeAgent {
|
|
|
114
194
|
readyResolve = res;
|
|
115
195
|
readyReject = rej;
|
|
116
196
|
});
|
|
197
|
+
// 与 spawn 同款兜底:握手超时拒绝而调用方从未 await 时,
|
|
198
|
+
// 不让 unhandled rejection 打爆宿主;后续 await 仍能拿到该错误。
|
|
199
|
+
ready.catch(() => { });
|
|
117
200
|
const agent = new ZhuyeAgent(proc, ready);
|
|
118
201
|
const timer = setTimeout(() => {
|
|
119
202
|
readyReject(new Error(`fromStdio handshake timed out after ${handshakeTimeoutMs}ms`));
|
|
@@ -156,8 +239,14 @@ export class ZhuyeAgent {
|
|
|
156
239
|
const msg = ev.msg;
|
|
157
240
|
if (msg.type === 'agent_message_delta')
|
|
158
241
|
result += msg.delta;
|
|
159
|
-
else if (msg.type === 'agent_message')
|
|
160
|
-
|
|
242
|
+
else if (msg.type === 'agent_message') {
|
|
243
|
+
// 全量消息是权威快照 —— 覆盖(而非追加)已累计的 delta,
|
|
244
|
+
// 协议允许同轮混合下发时不会产生重复文本。text 经 typeof 守卫:
|
|
245
|
+
// EventMsg 联合含开放成员(UnknownEventMsg),窄化后仍是 unknown。
|
|
246
|
+
const { text } = msg;
|
|
247
|
+
if (typeof text === 'string')
|
|
248
|
+
result = text;
|
|
249
|
+
}
|
|
161
250
|
else if (msg.type === 'turn_complete')
|
|
162
251
|
break;
|
|
163
252
|
else if (msg.type === 'turn_aborted') {
|
package/dist/binary.d.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/** 当前平台对应的 npm 平台子包名;不在支持矩阵时返回 null。 */
|
|
2
|
+
export declare function platformPackageName(platform?: string, arch?: string): string | null;
|
|
3
|
+
/** 由平台子包目录推导二进制路径(纯函数拆分,便于单测)。 */
|
|
4
|
+
export declare function binaryPathFromPackageDir(pkgDir: string): string;
|
|
5
|
+
/**
|
|
6
|
+
* 解析随包安装的内嵌 `zhuye` 二进制的绝对路径。
|
|
7
|
+
*
|
|
8
|
+
* 平台子包未落地(不支持的平台,或 optionalDependencies 安装失败 ——
|
|
9
|
+
* npm 对 optional 依赖失败静默容忍)时返回 null,由 spawn 链回退到
|
|
10
|
+
* PATH 上的 `zhuye`。
|
|
11
|
+
*
|
|
12
|
+
* 用 createRequire 而非 import.meta.resolve:后者在旧 Node 需要
|
|
13
|
+
* experimental 标志;createRequire 自 Node 12.2 稳定,且天然解析
|
|
14
|
+
* node_modules 的真实落盘路径。
|
|
15
|
+
*/
|
|
16
|
+
export declare function resolveBundledBinary(): string | null;
|
package/dist/binary.js
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
//! 内嵌二进制解析:定位随平台子包(optionalDependencies)安装的
|
|
2
|
+
//! `zhuye` 可执行文件,让 `npm install zhuye` 后开箱即用。
|
|
3
|
+
//!
|
|
4
|
+
//! 解析优先级见 `agent.ts` 的 spawn 链:options.bin → ZHUYE_BIN →
|
|
5
|
+
//! 本模块 → PATH 上的 `zhuye`。
|
|
6
|
+
import { accessSync, constants } from 'node:fs';
|
|
7
|
+
import { createRequire } from 'node:module';
|
|
8
|
+
import { join } from 'node:path';
|
|
9
|
+
/** process.platform → process.arch → npm 平台子包名(与发布矩阵一一对应)。 */
|
|
10
|
+
const PLATFORM_PACKAGES = {
|
|
11
|
+
darwin: { arm64: 'zhuye-darwin-arm64', x64: 'zhuye-darwin-x64' },
|
|
12
|
+
linux: { arm64: 'zhuye-linux-arm64', x64: 'zhuye-linux-x64' },
|
|
13
|
+
win32: { x64: 'zhuye-win32-x64' },
|
|
14
|
+
};
|
|
15
|
+
const EXE_SUFFIX = process.platform === 'win32' ? '.exe' : '';
|
|
16
|
+
/** 当前平台对应的 npm 平台子包名;不在支持矩阵时返回 null。 */
|
|
17
|
+
export function platformPackageName(platform = process.platform, arch = process.arch) {
|
|
18
|
+
return PLATFORM_PACKAGES[platform]?.[arch] ?? null;
|
|
19
|
+
}
|
|
20
|
+
/** 由平台子包目录推导二进制路径(纯函数拆分,便于单测)。 */
|
|
21
|
+
export function binaryPathFromPackageDir(pkgDir) {
|
|
22
|
+
return join(pkgDir, 'bin', `zhuye${EXE_SUFFIX}`);
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* 解析随包安装的内嵌 `zhuye` 二进制的绝对路径。
|
|
26
|
+
*
|
|
27
|
+
* 平台子包未落地(不支持的平台,或 optionalDependencies 安装失败 ——
|
|
28
|
+
* npm 对 optional 依赖失败静默容忍)时返回 null,由 spawn 链回退到
|
|
29
|
+
* PATH 上的 `zhuye`。
|
|
30
|
+
*
|
|
31
|
+
* 用 createRequire 而非 import.meta.resolve:后者在旧 Node 需要
|
|
32
|
+
* experimental 标志;createRequire 自 Node 12.2 稳定,且天然解析
|
|
33
|
+
* node_modules 的真实落盘路径。
|
|
34
|
+
*/
|
|
35
|
+
export function resolveBundledBinary() {
|
|
36
|
+
const pkg = platformPackageName();
|
|
37
|
+
if (pkg === null)
|
|
38
|
+
return null;
|
|
39
|
+
try {
|
|
40
|
+
const require = createRequire(import.meta.url);
|
|
41
|
+
const manifest = require.resolve(`${pkg}/package.json`);
|
|
42
|
+
const bin = binaryPathFromPackageDir(join(manifest, '..'));
|
|
43
|
+
accessSync(bin, constants.X_OK);
|
|
44
|
+
return bin;
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
}
|
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "zhuye",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.6",
|
|
4
4
|
"description": "ZhuYe Agent SDK for TypeScript: spawn `zhuye serve` and drive it over JSONL stdio.",
|
|
5
5
|
"author": "ZhuYe Agent Contributors",
|
|
6
6
|
"homepage": "https://github.com/Asketisch/ZhuYe#readme",
|
|
@@ -35,5 +35,12 @@
|
|
|
35
35
|
"@types/node": "^20.11.0",
|
|
36
36
|
"typescript": "^5.4.0",
|
|
37
37
|
"vitest": "^1.6.0"
|
|
38
|
+
},
|
|
39
|
+
"optionalDependencies": {
|
|
40
|
+
"zhuye-darwin-arm64": "0.1.6",
|
|
41
|
+
"zhuye-darwin-x64": "0.1.6",
|
|
42
|
+
"zhuye-linux-x64": "0.1.6",
|
|
43
|
+
"zhuye-linux-arm64": "0.1.6",
|
|
44
|
+
"zhuye-win32-x64": "0.1.6"
|
|
38
45
|
}
|
|
39
|
-
}
|
|
46
|
+
}
|