open-claude-p 1.0.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/LICENSE +21 -0
- package/README.ja.md +708 -0
- package/README.ko.md +713 -0
- package/README.md +850 -0
- package/README.zh.md +708 -0
- package/bin/cli.js +782 -0
- package/package.json +68 -0
- package/scripts/postinstall.js +60 -0
- package/src/chat/event-filters.js +116 -0
- package/src/chat/index.js +1225 -0
- package/src/completion/detector.js +163 -0
- package/src/daemon/client.js +172 -0
- package/src/daemon/server.js +267 -0
- package/src/daemon/socket.js +78 -0
- package/src/index.js +908 -0
- package/src/options/index.js +4 -0
- package/src/options/parse-argv.js +214 -0
- package/src/options/spec.js +519 -0
- package/src/options/validate.js +104 -0
- package/src/output/index.js +8 -0
- package/src/output/json.js +83 -0
- package/src/output/registry.js +35 -0
- package/src/output/stream-json.js +111 -0
- package/src/output/text.js +94 -0
- package/src/parsers/ansi-strip.js +94 -0
- package/src/parsers/index.js +8 -0
- package/src/parsers/pipeline.js +50 -0
- package/src/parsers/registry.js +43 -0
- package/src/parsers/sentinel.js +41 -0
- package/src/parsers/tui-frame.js +256 -0
- package/src/print-mode.js +214 -0
- package/src/pty/index.js +3 -0
- package/src/pty/pool.js +127 -0
- package/src/pty/session.js +88 -0
- package/src/session-log.js +124 -0
package/README.zh.md
ADDED
|
@@ -0,0 +1,708 @@
|
|
|
1
|
+
[English](README.md) · [한국어](README.ko.md) · **中文** · [日本語](README.ja.md)
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/open-claude-p)
|
|
4
|
+
[](https://www.npmjs.com/package/open-claude-p)
|
|
5
|
+
[](https://github.com/empty-user77/open-claude-p/stargazers)
|
|
6
|
+
[](https://github.com/empty-user77/open-claude-p/blob/main/LICENSE)
|
|
7
|
+
[](https://github.com/sponsors/empty-user77)
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
# open-claude-p (ocp)
|
|
12
|
+
|
|
13
|
+
一个基于 PTY 的兼容层,通过 **node-pty** 直接驱动交互式 `claude` CLI,在无法使用 `claude -p`(无头打印模式)的环境中提供相同的功能。
|
|
14
|
+
|
|
15
|
+
> **核心区别**:`claude -p` 是 Claude Code 的非交互模式,通过内部 API 运行,但在特定订阅/环境中不可用。`open-claude-p` 通过 PTY 运行实际的 TUI 客户端,并解析输出流以获得相同的结果。
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## 目录
|
|
20
|
+
|
|
21
|
+
- [安装](#安装)
|
|
22
|
+
- [CLI 用法](#cli-用法)
|
|
23
|
+
- [守护进程(会话持久化)](#守护进程会话持久化)
|
|
24
|
+
- [库 API](#库-api)
|
|
25
|
+
- [createDriver()](#createdriveropts)
|
|
26
|
+
- [runOneShot()](#runoneshotreq)
|
|
27
|
+
- [返回值:OneShotResult](#返回值oneshotresult)
|
|
28
|
+
- [事件类型(onEvent 回调)](#事件类型onevent-回调)
|
|
29
|
+
- [会话管理](#会话管理)
|
|
30
|
+
- [使用 JSONL 会话文件](#使用-jsonl-会话文件)
|
|
31
|
+
- [关于输出解析](#关于输出解析)
|
|
32
|
+
- [环境变量](#环境变量)
|
|
33
|
+
- [完整选项参考](#完整选项参考)
|
|
34
|
+
- [示例应用](#示例应用)
|
|
35
|
+
|
|
36
|
+
---
|
|
37
|
+
|
|
38
|
+
## 安装
|
|
39
|
+
|
|
40
|
+
### npm
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
# 作为项目依赖安装
|
|
44
|
+
npm install open-claude-p
|
|
45
|
+
|
|
46
|
+
# 或全局安装,以便在任意位置使用 `ocp` CLI
|
|
47
|
+
npm install -g open-claude-p
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
### 从源码安装(开发用)
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
# clone 后在项目根目录创建符号链接
|
|
54
|
+
git clone https://github.com/empty-user77/open-claude-p.git
|
|
55
|
+
cd open-claude-p
|
|
56
|
+
npm link
|
|
57
|
+
|
|
58
|
+
# 或从其他项目通过本地路径安装
|
|
59
|
+
npm install /path/to/open-claude-p
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
**前提条件**:`claude` CLI 必须已安装并在 `PATH` 中可用。
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
# 验证 Claude Code CLI 安装
|
|
66
|
+
claude --version
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
---
|
|
70
|
+
|
|
71
|
+
## CLI 用法
|
|
72
|
+
|
|
73
|
+
该包安装单个二进制文件 **`ocp`**。
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
# 基本用法
|
|
77
|
+
ocp "你好"
|
|
78
|
+
|
|
79
|
+
# 支持与 claude -p 相同的 argv 格式(-p 标志为兼容性被忽略)
|
|
80
|
+
ocp -p "你好"
|
|
81
|
+
|
|
82
|
+
# 从 stdin 读取提示
|
|
83
|
+
echo "北京今天天气怎么样?" | ocp
|
|
84
|
+
|
|
85
|
+
# 指定输出格式
|
|
86
|
+
ocp --output-format json "一个词回答:苹果"
|
|
87
|
+
ocp --output-format stream-json "你好"
|
|
88
|
+
|
|
89
|
+
# 指定模型
|
|
90
|
+
ocp --model sonnet "复杂问题..."
|
|
91
|
+
ocp --model claude-opus-4-7 "架构评审..."
|
|
92
|
+
|
|
93
|
+
# 添加系统提示
|
|
94
|
+
ocp --append-system-prompt "始终用中文回答" "what's the weather?"
|
|
95
|
+
|
|
96
|
+
# 恢复会话 — sessionId 会打印到 stderr
|
|
97
|
+
SID=$(ocp "只说奇异果" 2>&1 >/dev/null | grep sessionId | grep -oE '[0-9a-f-]{36}')
|
|
98
|
+
ocp --resume "$SID" "你刚才说了什么?"
|
|
99
|
+
|
|
100
|
+
# 或自动继续最近的会话
|
|
101
|
+
ocp --continue "你刚才说了什么?"
|
|
102
|
+
|
|
103
|
+
# 跳过权限检查(用于自动化环境)
|
|
104
|
+
ocp --dangerously-skip-permissions "读取并分析文件"
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
### 输出格式
|
|
108
|
+
|
|
109
|
+
#### `text`(默认)
|
|
110
|
+
|
|
111
|
+
```
|
|
112
|
+
你好!有什么我可以帮助您的?
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
#### `json`
|
|
116
|
+
|
|
117
|
+
```json
|
|
118
|
+
{
|
|
119
|
+
"result": "你好!有什么我可以帮助您的?",
|
|
120
|
+
"session_id": "a1b2c3d4-...",
|
|
121
|
+
"is_error": false,
|
|
122
|
+
"cost_usd": null,
|
|
123
|
+
"duration_ms": 4200,
|
|
124
|
+
"num_turns": 1
|
|
125
|
+
}
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
#### `stream-json`(NDJSON)
|
|
129
|
+
|
|
130
|
+
响应逐行流式传输:
|
|
131
|
+
|
|
132
|
+
```jsonl
|
|
133
|
+
{"type":"system","subtype":"init","session_id":"a1b2c3d4-...","tools":[],"mcp_servers":[]}
|
|
134
|
+
{"type":"assistant","session_id":"a1b2c3d4-...","message":{"role":"assistant","content":[{"type":"text","text":"你好!"}]}}
|
|
135
|
+
{"type":"result","subtype":"success","session_id":"a1b2c3d4-...","is_error":false,"duration_ms":4200}
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
---
|
|
139
|
+
|
|
140
|
+
## 守护进程(会话持久化)
|
|
141
|
+
|
|
142
|
+
`ocp` CLI 默认通过**后台守护进程**保持 PTY 会话存活。
|
|
143
|
+
这意味着在同一目录中重复调用时无需等待 2.5 秒的预热,对话上下文会自动保留。
|
|
144
|
+
|
|
145
|
+
```
|
|
146
|
+
ocp "第一个问题" → 如果没有守护进程则启动新的,有则复用
|
|
147
|
+
ocp "第二个问题" → 连接到同一守护进程,上下文保留
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
守护进程 socket 存储在 `~/.ocp/` 下,**每个工作目录一个**。
|
|
151
|
+
|
|
152
|
+
### 禁用守护进程
|
|
153
|
+
|
|
154
|
+
```bash
|
|
155
|
+
OCP_NO_DAEMON=1 ocp "只运行一次" # 直接启动 PTY,不使用守护进程
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
应跳过守护进程的情况:
|
|
159
|
+
- 使用 `--resume`、`--continue` 或 `--fork-session` 标志时(自动切换到直接模式)
|
|
160
|
+
- 使用 `--input-format=stream-json` 时
|
|
161
|
+
- 在 CI/CD 等隔离环境中单次执行时
|
|
162
|
+
|
|
163
|
+
### 守护进程环境变量
|
|
164
|
+
|
|
165
|
+
| 变量 | 说明 | 默认值 |
|
|
166
|
+
|------|------|--------|
|
|
167
|
+
| `OCP_NO_DAEMON` | 设为 `1` 时禁用守护进程 | — |
|
|
168
|
+
| `OCP_DAEMON_IDLE_MS` | 空闲此时间后自动终止守护进程 | `600000`(10 分钟) |
|
|
169
|
+
| `OCP_MAX_DAEMONS` | 同时保持的最大守护进程数 | `30` |
|
|
170
|
+
|
|
171
|
+
---
|
|
172
|
+
|
|
173
|
+
## 库 API
|
|
174
|
+
|
|
175
|
+
### `createDriver(opts?)`
|
|
176
|
+
|
|
177
|
+
创建驱动器。在整个应用程序中共享单个驱动器实例。
|
|
178
|
+
|
|
179
|
+
```js
|
|
180
|
+
import { createDriver } from 'open-claude-p';
|
|
181
|
+
|
|
182
|
+
const driver = createDriver({
|
|
183
|
+
claudeBin: 'claude', // claude 二进制路径(默认:PATH 中的 claude)
|
|
184
|
+
warmupMs: 2500, // PTY 初始化等待时间(ms)
|
|
185
|
+
reuseWarmupMs: 200, // 从池中复用时的等待时间(ms)
|
|
186
|
+
idleMs: 1500, // 响应完成后的静默等待(ms)
|
|
187
|
+
preIdleMs: 8000, // sentinel 匹配前的最小等待(ms)
|
|
188
|
+
maxResponseMs: 60_000, // 最大响应等待时间(ms),超时则结束
|
|
189
|
+
poolSize: 0, // PTY 池大小(0=禁用,N>0=保持 N 个预热)
|
|
190
|
+
poolMaxAgeMs: 600_000, // 池会话最大生命周期(ms)
|
|
191
|
+
cwd: process.cwd(), // 工作目录
|
|
192
|
+
env: {}, // 附加环境变量
|
|
193
|
+
debug: false, // 将调试日志打印到 stderr
|
|
194
|
+
});
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
### `runOneShot(req)`
|
|
198
|
+
|
|
199
|
+
向 Claude 发送单个提示并等待响应。
|
|
200
|
+
|
|
201
|
+
```js
|
|
202
|
+
const result = await driver.runOneShot({
|
|
203
|
+
prompt: '北京现在的天气怎么样?',
|
|
204
|
+
|
|
205
|
+
// ── 模型 / 行为 ──────────────────────────────
|
|
206
|
+
model: 'sonnet', // 模型名称
|
|
207
|
+
effort: 'high', // 'low' | 'medium' | 'high' | 'max'
|
|
208
|
+
thinking: 'adaptive', // 'enabled' | 'adaptive' | 'disabled'
|
|
209
|
+
maxTurns: 5, // 最大代理轮次(shim 强制)
|
|
210
|
+
|
|
211
|
+
// ── 系统提示 ──────────────────────────────────
|
|
212
|
+
systemPrompt: '你是天气专家', // 替换整个系统提示
|
|
213
|
+
appendSystemPrompt: '始终用中文回答', // 追加到默认提示
|
|
214
|
+
|
|
215
|
+
// ── 权限 / 工具 ──────────────────────────────
|
|
216
|
+
dangerouslySkipPermissions: true, // 跳过权限检查
|
|
217
|
+
allowedTools: ['WebSearch', 'Read'], // 工具白名单
|
|
218
|
+
disallowedTools: ['Bash'], // 工具黑名单
|
|
219
|
+
|
|
220
|
+
// ── 会话 ──────────────────────────────────────
|
|
221
|
+
resume: 'a1b2c3d4-...', // 从之前的会话 UUID 恢复
|
|
222
|
+
continue: false, // 继续最近的会话
|
|
223
|
+
forkSession: false, // 恢复时创建新的会话 ID
|
|
224
|
+
|
|
225
|
+
// ── 工作目录 ──────────────────────────────────
|
|
226
|
+
cwd: '/path/to/project',
|
|
227
|
+
|
|
228
|
+
// ── 取消 ──────────────────────────────────────
|
|
229
|
+
abortSignal: controller.signal,
|
|
230
|
+
|
|
231
|
+
// ── 实时事件回调 ──────────────────────────────
|
|
232
|
+
onEvent(ev) {
|
|
233
|
+
// 响应生成时实时调用
|
|
234
|
+
// 参见下方"事件类型"章节
|
|
235
|
+
if (ev.type === 'assistant-text') {
|
|
236
|
+
process.stdout.write(ev.text);
|
|
237
|
+
}
|
|
238
|
+
},
|
|
239
|
+
});
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
### 返回值:OneShotResult
|
|
243
|
+
|
|
244
|
+
`runOneShot()` resolve 时返回如下结构的对象:
|
|
245
|
+
|
|
246
|
+
```ts
|
|
247
|
+
{
|
|
248
|
+
// ── 核心结果 ────────────────────────────────────────────────────
|
|
249
|
+
text: string,
|
|
250
|
+
// Claude 的最终响应文本(已去除 TUI 残留)。
|
|
251
|
+
// Claude 生成的原始文本——markdown、HTML、代码块等。
|
|
252
|
+
// 渲染/解析由调用方负责。
|
|
253
|
+
|
|
254
|
+
sessionId: string | null,
|
|
255
|
+
// 本次请求对应的 Claude 会话 UUID。
|
|
256
|
+
// 使用 --resume <sessionId> 可继续对话。
|
|
257
|
+
// 如果 banner 捕获失败,将回退到 ~/.claude/projects/ 文件系统扫描。
|
|
258
|
+
|
|
259
|
+
isError: boolean,
|
|
260
|
+
// true = 因错误或超时完成
|
|
261
|
+
|
|
262
|
+
completionReason: string,
|
|
263
|
+
// 完成原因:
|
|
264
|
+
// 'sentinel' 正常完成(检测到 sentinel 字符串)
|
|
265
|
+
// 'idle' 响应后静默超时
|
|
266
|
+
// 'prompt-box' TUI 输入框重新出现
|
|
267
|
+
// 'timeout' 超过 maxResponseMs
|
|
268
|
+
// 'max-turns' 达到 maxTurns 限制
|
|
269
|
+
// 'upstream-exited' claude 进程先退出
|
|
270
|
+
// 'write-failed' PTY 写入失败
|
|
271
|
+
// 'cancelled' 通过 AbortSignal 取消
|
|
272
|
+
|
|
273
|
+
exitCode: number,
|
|
274
|
+
// 0 = 成功,1 = 错误
|
|
275
|
+
|
|
276
|
+
// ── 事件数组 ────────────────────────────────────────────────────
|
|
277
|
+
events: Array<object>,
|
|
278
|
+
// 管道产生的所有事件(与 onEvent 回调相同的对象)。
|
|
279
|
+
// 参见下方"事件类型"章节。
|
|
280
|
+
|
|
281
|
+
// ── 性能指标 ────────────────────────────────────────────────────
|
|
282
|
+
durationMs: number,
|
|
283
|
+
// 总耗时(ms)
|
|
284
|
+
|
|
285
|
+
cost: { totalUsd: number | null, numTurns: number | null },
|
|
286
|
+
// 目前为 null(无法从 PTY 直接获取费用信息)。
|
|
287
|
+
// 准确的令牌/费用数据请读取 JSONL 会话文件(见下文)。
|
|
288
|
+
|
|
289
|
+
diagnostics: { rawBytes: number, strippedBytes: number },
|
|
290
|
+
// 从 PTY 接收的原始字节数 / ANSI 去除后的字节数
|
|
291
|
+
}
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
### 事件类型(onEvent 回调)
|
|
295
|
+
|
|
296
|
+
`onEvent` 回调和 `result.events` 数组包含以下类型的事件:
|
|
297
|
+
|
|
298
|
+
```ts
|
|
299
|
+
// Claude 开始响应时(检测到 ⏺ 标记)
|
|
300
|
+
{ type: 'assistant-region-entered', n: number }
|
|
301
|
+
|
|
302
|
+
// 响应区域关闭时(检测到 hr 或 sentinel)
|
|
303
|
+
{ type: 'assistant-region-exited', n: number }
|
|
304
|
+
|
|
305
|
+
// 一行响应文本(实时流式传输)
|
|
306
|
+
{
|
|
307
|
+
type: 'assistant-text',
|
|
308
|
+
text: string, // 一行文本(原始 markdown)
|
|
309
|
+
region: number // 第几个响应区域(编号越大越新,恢复时有用)
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// 检测到 Claude 会话 UUID(来自 banner 或退出消息)
|
|
313
|
+
{ type: 'session-id', id: string }
|
|
314
|
+
|
|
315
|
+
// TUI 进度条(表示 Claude 正在工作)
|
|
316
|
+
// label: "Searching the web...", "Reading file...", "Cogitated for 25s" 等
|
|
317
|
+
{ type: 'spinner', label: string }
|
|
318
|
+
|
|
319
|
+
// TUI 输入框出现在屏幕上(完成信号之一)
|
|
320
|
+
{ type: 'prompt-box-shown' }
|
|
321
|
+
|
|
322
|
+
// 检测到 sentinel 字符串(正常完成)
|
|
323
|
+
{ type: 'sentinel' }
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
#### 事件使用示例
|
|
327
|
+
|
|
328
|
+
```js
|
|
329
|
+
const result = await driver.runOneShot({
|
|
330
|
+
prompt: '分析这篇长文档',
|
|
331
|
+
onEvent(ev) {
|
|
332
|
+
switch (ev.type) {
|
|
333
|
+
case 'assistant-text':
|
|
334
|
+
// 实时流式传输——逐行打印
|
|
335
|
+
process.stdout.write(ev.text + '\n');
|
|
336
|
+
break;
|
|
337
|
+
|
|
338
|
+
case 'spinner':
|
|
339
|
+
// 进度条标签——工具使用中显示(如 "Searching the web...")
|
|
340
|
+
process.stderr.write(`\r⏳ ${ev.label} `);
|
|
341
|
+
break;
|
|
342
|
+
|
|
343
|
+
case 'session-id':
|
|
344
|
+
// 提前保存会话 ID,超时也能恢复
|
|
345
|
+
saveSessionId(ev.id);
|
|
346
|
+
break;
|
|
347
|
+
}
|
|
348
|
+
},
|
|
349
|
+
});
|
|
350
|
+
```
|
|
351
|
+
|
|
352
|
+
---
|
|
353
|
+
|
|
354
|
+
## 会话管理
|
|
355
|
+
|
|
356
|
+
Claude 用 UUID 标识每个会话,可用其恢复之前的对话。
|
|
357
|
+
|
|
358
|
+
```js
|
|
359
|
+
// 1. 第一次请求 — 开始新会话
|
|
360
|
+
const result1 = await driver.runOneShot({
|
|
361
|
+
prompt: '用 Python 实现斐波那契数列',
|
|
362
|
+
});
|
|
363
|
+
console.log('会话 ID:', result1.sessionId);
|
|
364
|
+
// → "a1b2c3d4-5678-..."
|
|
365
|
+
|
|
366
|
+
// 2. 恢复会话 — 保留之前的对话上下文
|
|
367
|
+
const result2 = await driver.runOneShot({
|
|
368
|
+
prompt: '现在用迭代而非递归改写',
|
|
369
|
+
resume: result1.sessionId,
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
// 3. 分叉会话 — 保留原始会话,探索不同方向
|
|
373
|
+
const result3 = await driver.runOneShot({
|
|
374
|
+
prompt: '改成生成器版本',
|
|
375
|
+
resume: result1.sessionId,
|
|
376
|
+
forkSession: true, // 分配新 UUID,原始会话保留
|
|
377
|
+
});
|
|
378
|
+
```
|
|
379
|
+
|
|
380
|
+
---
|
|
381
|
+
|
|
382
|
+
## 使用 JSONL 会话文件
|
|
383
|
+
|
|
384
|
+
Claude CLI 将每个会话保存为 JSONL 文件:
|
|
385
|
+
|
|
386
|
+
```
|
|
387
|
+
~/.claude/projects/<cwd 编码路径>/<session-uuid>.jsonl
|
|
388
|
+
```
|
|
389
|
+
|
|
390
|
+
例如,`cwd` 为 `/Users/alice/myproject` 时:
|
|
391
|
+
→ `~/.claude/projects/-Users-alice-myproject/<uuid>.jsonl`
|
|
392
|
+
|
|
393
|
+
这些文件包含 PTY 输出中不可用的**令牌用量、费用和工具使用元数据**。
|
|
394
|
+
|
|
395
|
+
```js
|
|
396
|
+
import { readFile } from 'node:fs/promises';
|
|
397
|
+
import path from 'node:path';
|
|
398
|
+
import os from 'node:os';
|
|
399
|
+
|
|
400
|
+
async function readSessionMeta(sessionId, cwd = process.cwd()) {
|
|
401
|
+
const key = path.resolve(cwd).replace(/\//g, '-');
|
|
402
|
+
const filePath = path.join(os.homedir(), '.claude', 'projects', key, `${sessionId}.jsonl`);
|
|
403
|
+
const lines = (await readFile(filePath, 'utf8')).split('\n').filter(Boolean);
|
|
404
|
+
|
|
405
|
+
// 从最后一条 assistant 消息中提取 usage
|
|
406
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
407
|
+
try {
|
|
408
|
+
const ev = JSON.parse(lines[i]);
|
|
409
|
+
if (ev.message?.role === 'assistant') {
|
|
410
|
+
const textBlock = ev.message.content?.find(c => c.type === 'text');
|
|
411
|
+
return {
|
|
412
|
+
text: textBlock?.text, // 干净的 markdown 文本(无 TUI 残留)
|
|
413
|
+
usage: ev.message.usage, // { input_tokens, output_tokens, cache_read_input_tokens, ... }
|
|
414
|
+
timestamp: ev.timestamp,
|
|
415
|
+
};
|
|
416
|
+
}
|
|
417
|
+
} catch {}
|
|
418
|
+
}
|
|
419
|
+
return null;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
const meta = await readSessionMeta(result.sessionId);
|
|
423
|
+
// meta.usage.input_tokens → 输入令牌数
|
|
424
|
+
// meta.usage.output_tokens → 输出令牌数
|
|
425
|
+
// meta.usage.cache_read_input_tokens → 缓存读取令牌数
|
|
426
|
+
// meta.usage.server_tool_use.web_search_requests → 网络搜索次数
|
|
427
|
+
```
|
|
428
|
+
|
|
429
|
+
### JSONL 可获取的内容
|
|
430
|
+
|
|
431
|
+
| 项目 | PTY result.text | JSONL |
|
|
432
|
+
|------|----------------|-------|
|
|
433
|
+
| 响应文本 | ✅(可能含 TUI 残留) | ✅(干净 markdown) |
|
|
434
|
+
| 输入令牌数 | ❌ | ✅ |
|
|
435
|
+
| 输出令牌数 | ❌ | ✅ |
|
|
436
|
+
| 缓存令牌数 | ❌ | ✅ |
|
|
437
|
+
| 费用计算 | ❌ | ✅(令牌 × 单价) |
|
|
438
|
+
| 网络搜索次数 | ❌ | ✅ |
|
|
439
|
+
| 时间戳 | ❌ | ✅ |
|
|
440
|
+
| 工具使用详情 | 部分(事件) | ✅ |
|
|
441
|
+
|
|
442
|
+
---
|
|
443
|
+
|
|
444
|
+
## 关于输出解析
|
|
445
|
+
|
|
446
|
+
**`result.text` 是 Claude 生成的原始 markdown/文本。**
|
|
447
|
+
这是开放格式——渲染、解析和显示方式由您自行实现。
|
|
448
|
+
|
|
449
|
+
```
|
|
450
|
+
result.text 示例:
|
|
451
|
+
─────────────────────────────────────
|
|
452
|
+
# 斐波那契数列
|
|
453
|
+
|
|
454
|
+
以下是用 Python 实现斐波那契数列的方法:
|
|
455
|
+
|
|
456
|
+
```python
|
|
457
|
+
def fib(n):
|
|
458
|
+
a, b = 0, 1
|
|
459
|
+
for _ in range(n):
|
|
460
|
+
a, b = b, a + b
|
|
461
|
+
return a
|
|
462
|
+
```
|
|
463
|
+
|
|
464
|
+
- 时间复杂度:O(n)
|
|
465
|
+
- 空间复杂度:O(1)
|
|
466
|
+
─────────────────────────────────────
|
|
467
|
+
```
|
|
468
|
+
|
|
469
|
+
### 解析实现参考
|
|
470
|
+
|
|
471
|
+
`sample/public/app.js` 中的 `renderMarkdown()` 函数是 Web UI 的解析示例。
|
|
472
|
+
请根据您的目标环境自行实现:
|
|
473
|
+
|
|
474
|
+
```js
|
|
475
|
+
// Web UI → HTML 渲染(示例)
|
|
476
|
+
import { marked } from 'marked';
|
|
477
|
+
const html = marked.parse(result.text);
|
|
478
|
+
|
|
479
|
+
// 终端 → ANSI 颜色渲染(示例)
|
|
480
|
+
import { renderMarkdown } from 'cli-markdown';
|
|
481
|
+
console.log(renderMarkdown(result.text));
|
|
482
|
+
|
|
483
|
+
// 传递给其他 LLM → 直接使用
|
|
484
|
+
const nextPrompt = `之前的响应:${result.text}\n\n现在继续下一步`;
|
|
485
|
+
```
|
|
486
|
+
|
|
487
|
+
### 关于 TUI 残留
|
|
488
|
+
|
|
489
|
+
`result.text` 已由 ocp 尽可能去除 TUI 渲染残留,但可能不完美。
|
|
490
|
+
如需更干净的文本,建议从 **JSONL 会话文件**中读取(见上文)。
|
|
491
|
+
|
|
492
|
+
---
|
|
493
|
+
|
|
494
|
+
## 环境变量
|
|
495
|
+
|
|
496
|
+
### 驱动器选项
|
|
497
|
+
|
|
498
|
+
| 变量 | 对应选项 | 默认值 |
|
|
499
|
+
|------|---------|--------|
|
|
500
|
+
| `OCP_CLAUDE_BIN` | `claudeBin` | `'claude'` |
|
|
501
|
+
| `OCP_WARMUP_MS` | `warmupMs` | `2500` |
|
|
502
|
+
| `OCP_REUSE_WARMUP_MS` | `reuseWarmupMs` | `200` |
|
|
503
|
+
| `OCP_IDLE_MS` | `idleMs` | `1500` |
|
|
504
|
+
| `OCP_PRE_IDLE_MS` | `preIdleMs` | `8000` |
|
|
505
|
+
| `OCP_MAX_RESPONSE_MS` | `maxResponseMs` | `60000` |
|
|
506
|
+
| `OCP_POOL_SIZE` | `poolSize` | `0` |
|
|
507
|
+
| `OCP_POOL_MAX_AGE_MS` | `poolMaxAgeMs` | `600000` |
|
|
508
|
+
|
|
509
|
+
### 守护进程(仅 CLI)
|
|
510
|
+
|
|
511
|
+
| 变量 | 说明 | 默认值 |
|
|
512
|
+
|------|------|--------|
|
|
513
|
+
| `OCP_NO_DAEMON` | 设为 `1` 时禁用守护进程,直接运行 PTY | — |
|
|
514
|
+
| `OCP_DAEMON_IDLE_MS` | 空闲此时间后自动终止守护进程 | `600000` |
|
|
515
|
+
| `OCP_MAX_DAEMONS` | 同时保持的最大守护进程数 | `30` |
|
|
516
|
+
|
|
517
|
+
```bash
|
|
518
|
+
# 将响应超时增加到 10 分钟
|
|
519
|
+
OCP_MAX_RESPONSE_MS=600000 ocp "复杂任务..."
|
|
520
|
+
|
|
521
|
+
# 不使用守护进程单次运行
|
|
522
|
+
OCP_NO_DAEMON=1 ocp "只运行一次"
|
|
523
|
+
```
|
|
524
|
+
|
|
525
|
+
---
|
|
526
|
+
|
|
527
|
+
## 完整选项参考
|
|
528
|
+
|
|
529
|
+
`runOneShot(req)` 请求字段与 CLI 标志的对应关系:
|
|
530
|
+
|
|
531
|
+
| req 字段 | CLI 标志 | 类型 | 说明 |
|
|
532
|
+
|---------|---------|------|------|
|
|
533
|
+
| `model` | `--model` | string | 模型名称(如 `sonnet`、`claude-sonnet-4-6`) |
|
|
534
|
+
| `systemPrompt` | `--system-prompt` | string | 替换整个系统提示 |
|
|
535
|
+
| `appendSystemPrompt` | `--append-system-prompt` | string | 追加到默认系统提示 |
|
|
536
|
+
| `dangerouslySkipPermissions` | `--dangerously-skip-permissions` | boolean | 跳过权限检查 |
|
|
537
|
+
| `allowedTools` | `--allowed-tools` | string[] | 工具白名单 |
|
|
538
|
+
| `disallowedTools` | `--disallowed-tools` | string[] | 工具黑名单 |
|
|
539
|
+
| `resume` | `--resume` / `-r` | string | 从会话 UUID 恢复 |
|
|
540
|
+
| `continue` | `--continue` / `-c` | boolean | 继续最近的会话 |
|
|
541
|
+
| `forkSession` | `--fork-session` | boolean | 恢复时创建新的会话 ID |
|
|
542
|
+
| `sessionId` | `--session-id` | string | 为新会话指定特定 UUID |
|
|
543
|
+
| `noSessionPersistence` | `--no-session-persistence` | boolean | 禁用会话保存 |
|
|
544
|
+
| `effort` | `--effort` | enum | `low` \| `medium` \| `high` \| `max` |
|
|
545
|
+
| `thinking` | `--thinking` | enum | `enabled` \| `adaptive` \| `disabled` |
|
|
546
|
+
| `maxTurns` | `--max-turns` | number | 最大代理轮次 |
|
|
547
|
+
| `fallbackModel` | `--fallback-model` | string | 主模型过载时的备用模型 |
|
|
548
|
+
| `permissionMode` | `--permission-mode` | string | `default` \| `plan` \| `acceptEdits` \| `bypassPermissions` |
|
|
549
|
+
| `mcpConfig` | `--mcp-config` | string[] | MCP 配置路径 |
|
|
550
|
+
| `addDir` | `--add-dir` | string[] | 工具可访问的附加目录 |
|
|
551
|
+
| `bare` | `--bare` | boolean | 最小模式(禁用 hooks、LSP、插件等) |
|
|
552
|
+
| `debug` | `--debug` | boolean | 将调试日志打印到 stderr |
|
|
553
|
+
| `verbose` | `--verbose` | boolean | 详细输出 |
|
|
554
|
+
| `cwd` | `--cwd` | string | PTY 进程工作目录 |
|
|
555
|
+
| `abortSignal` | — | AbortSignal | 取消信号 |
|
|
556
|
+
| `onEvent` | — | function | 实时事件回调 |
|
|
557
|
+
| `passThroughArgv` | — | string[] | 直接传递给 claude 的附加 argv |
|
|
558
|
+
|
|
559
|
+
---
|
|
560
|
+
|
|
561
|
+
## 示例应用
|
|
562
|
+
|
|
563
|
+
`sample/` 目录包含一个基于 ocp 构建的 Web 聊天 UI。
|
|
564
|
+
|
|
565
|
+
### 运行
|
|
566
|
+
|
|
567
|
+
```bash
|
|
568
|
+
cd sample
|
|
569
|
+
node server.js
|
|
570
|
+
# → http://localhost:3000
|
|
571
|
+
```
|
|
572
|
+
|
|
573
|
+
### 示例应用结构
|
|
574
|
+
|
|
575
|
+
```
|
|
576
|
+
sample/
|
|
577
|
+
server.js Express 服务器 — 封装 ocp 驱动器,SSE 流式传输
|
|
578
|
+
data/
|
|
579
|
+
conversations.json 对话历史(自动生成)
|
|
580
|
+
public/
|
|
581
|
+
index.html 聊天 UI
|
|
582
|
+
app.js 客户端 JavaScript
|
|
583
|
+
style.css 样式表
|
|
584
|
+
```
|
|
585
|
+
|
|
586
|
+
### 示例服务器 API
|
|
587
|
+
|
|
588
|
+
| 端点 | 方法 | 说明 |
|
|
589
|
+
|------|------|------|
|
|
590
|
+
| `/api/conversations` | GET | 对话列表 |
|
|
591
|
+
| `/api/conversations/:id` | GET | 对话详情(所有消息) |
|
|
592
|
+
| `/api/conversations/:id` | DELETE | 删除对话 |
|
|
593
|
+
| `/api/chat` | POST | 发送消息(SSE 流式传输) |
|
|
594
|
+
| `/api/monitor` | GET | PTY 事件监控(SSE) |
|
|
595
|
+
| `/api/skills` | GET | `~/.claude/skills/` 技能列表 |
|
|
596
|
+
| `/api/processes` | GET | 进行中的请求列表(`id`、`prompt`、`elapsedMs`) |
|
|
597
|
+
| `/api/processes/:id` | DELETE | 中止特定请求(`all` 中止全部) |
|
|
598
|
+
|
|
599
|
+
### `/api/chat` SSE 事件
|
|
600
|
+
|
|
601
|
+
聊天请求(`POST /api/chat`)以 Server-Sent Events 形式流式传输响应:
|
|
602
|
+
|
|
603
|
+
```js
|
|
604
|
+
// 客户端请求
|
|
605
|
+
const resp = await fetch('/api/chat', {
|
|
606
|
+
method: 'POST',
|
|
607
|
+
headers: { 'Content-Type': 'application/json' },
|
|
608
|
+
body: JSON.stringify({
|
|
609
|
+
message: '北京今天天气怎么样?',
|
|
610
|
+
conversationId: null, // null 开始新对话
|
|
611
|
+
skillName: 'my-skill', // 可选:~/.claude/skills/ 中的技能名称
|
|
612
|
+
}),
|
|
613
|
+
});
|
|
614
|
+
|
|
615
|
+
// SSE 事件类型
|
|
616
|
+
{ type: 'spinner', label: 'Searching the web...' } // 工作中状态
|
|
617
|
+
{ type: 'text', text: '你好...' } // 流式文本(片段)
|
|
618
|
+
{ type: 'error', error: '错误消息' } // 错误
|
|
619
|
+
{
|
|
620
|
+
type: 'done',
|
|
621
|
+
conversationId: 'uuid', // 对话 ID(已保存)
|
|
622
|
+
text: '完整最终响应', // 完整最终文本(来自 JSONL 的干净 markdown)
|
|
623
|
+
isNew: true, // 是否为新对话
|
|
624
|
+
meta: {
|
|
625
|
+
elapsedMs: 4200, // 耗时(ms)
|
|
626
|
+
inputTokens: 1500, // 输入令牌(含缓存)
|
|
627
|
+
outputTokens: 320, // 输出令牌
|
|
628
|
+
costUsd: 0.0042, // 费用(USD)
|
|
629
|
+
tools: ['WebSearch'], // 使用的工具
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
```
|
|
633
|
+
|
|
634
|
+
### 示例中的 Markdown 解析
|
|
635
|
+
|
|
636
|
+
示例应用(`sample/public/app.js`)通过 `renderMarkdown()` 函数将 `result.text` 转换为 HTML。
|
|
637
|
+
|
|
638
|
+
**此解析代码仅供示例使用。** 实际项目请使用:
|
|
639
|
+
- Web:`marked`、`markdown-it` 等
|
|
640
|
+
- 终端:`cli-markdown`、`terminal-link` 等
|
|
641
|
+
- React:`react-markdown`
|
|
642
|
+
- LLM 输入:直接使用
|
|
643
|
+
|
|
644
|
+
### 进程管理器(`ocp-ps`)
|
|
645
|
+
|
|
646
|
+
示例应用附带一个 CLI 工具,使用 `/api/processes` API 列出和取消进行中的请求。
|
|
647
|
+
|
|
648
|
+
```bash
|
|
649
|
+
cd sample
|
|
650
|
+
|
|
651
|
+
node ocp-ps.js # 列出进行中的请求
|
|
652
|
+
node ocp-ps.js kill <id> # 中止特定请求
|
|
653
|
+
node ocp-ps.js kill all # 中止全部
|
|
654
|
+
node ocp-ps.js watch # 每秒自动刷新
|
|
655
|
+
```
|
|
656
|
+
|
|
657
|
+
> **注意**:`ocp-ps` 是使用示例应用 HTTP API(`/api/processes`)的示例实现。
|
|
658
|
+
> 使用 ocp 库构建自己的服务器时,可以按相同模式实现进程管理 API。
|
|
659
|
+
|
|
660
|
+
### 技能调用(`/技能名`)
|
|
661
|
+
|
|
662
|
+
在聊天输入框中输入 `/` 会显示 `~/.claude/skills/` 中的技能下拉列表。
|
|
663
|
+
|
|
664
|
+
```
|
|
665
|
+
用户输入:/my-skill 分析这份 PRD,找出相关仓库
|
|
666
|
+
↓
|
|
667
|
+
服务器:将 SKILL.md 内容作为 appendSystemPrompt 注入
|
|
668
|
+
↓
|
|
669
|
+
Claude:按技能指令执行
|
|
670
|
+
```
|
|
671
|
+
|
|
672
|
+
---
|
|
673
|
+
|
|
674
|
+
## 模块结构
|
|
675
|
+
|
|
676
|
+
```
|
|
677
|
+
src/
|
|
678
|
+
index.js 库公共 API(createDriver、runOneShot)
|
|
679
|
+
options/
|
|
680
|
+
spec.js 所有选项定义(单一来源)
|
|
681
|
+
parse-argv.js CLI argv 解析器
|
|
682
|
+
validate.js 跨选项验证
|
|
683
|
+
parsers/
|
|
684
|
+
ansi-strip.js ANSI 转义去除
|
|
685
|
+
tui-frame.js TUI 帧解析器(事件生成)
|
|
686
|
+
sentinel.js 完成 sentinel 检测
|
|
687
|
+
pipeline.js 解析器管道组合
|
|
688
|
+
output/
|
|
689
|
+
text.js --output-format text 适配器
|
|
690
|
+
json.js --output-format json 适配器
|
|
691
|
+
stream-json.js --output-format stream-json 适配器
|
|
692
|
+
pty/
|
|
693
|
+
session.js 单个 PTY 会话生命周期
|
|
694
|
+
pool.js 预热 PTY 池
|
|
695
|
+
completion/
|
|
696
|
+
detector.js 完成检测(sentinel + idle + prompt-box)
|
|
697
|
+
bin/
|
|
698
|
+
cli.js ocp CLI 入口点
|
|
699
|
+
sample/
|
|
700
|
+
server.js 示例 Web 服务器
|
|
701
|
+
public/ 聊天 UI
|
|
702
|
+
```
|
|
703
|
+
|
|
704
|
+
---
|
|
705
|
+
|
|
706
|
+
## 许可证
|
|
707
|
+
|
|
708
|
+
MIT
|