@snack-kit/porygon 0.1.0 → 0.2.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/README.md CHANGED
@@ -39,10 +39,16 @@ const porygon = createPorygon({
39
39
  backends: {
40
40
  claude: {
41
41
  model: "sonnet",
42
+ interactive: false, // false = --dangerously-skip-permissions
43
+ cliPath: "/usr/local/bin/claude", // 自定义 CLI 路径
42
44
  appendSystemPrompt: "用中文回答",
43
45
  cwd: "/path/to/project",
44
46
  proxy: { url: "http://127.0.0.1:7897" },
45
47
  },
48
+ opencode: {
49
+ serverUrl: "http://localhost:39393",
50
+ apiKey: "sk-xxx",
51
+ },
46
52
  },
47
53
  defaults: {
48
54
  timeoutMs: 300_000,
@@ -67,9 +73,14 @@ const porygon = createPorygon({
67
73
  | 字段 | 类型 | 说明 |
68
74
  |------|------|------|
69
75
  | `model` | `string` | 模型名称 |
76
+ | `interactive` | `boolean` | 是否交互模式,`false` 时 Claude 添加 `--dangerously-skip-permissions` |
77
+ | `cliPath` | `string` | CLI 可执行文件路径(如 Claude CLI 的自定义安装路径) |
78
+ | `serverUrl` | `string` | 远程服务地址(OpenCode serve 模式) |
79
+ | `apiKey` | `string` | API Key 认证 |
70
80
  | `appendSystemPrompt` | `string` | 追加系统提示词 |
71
81
  | `proxy` | `ProxyConfig` | 后端专属代理 |
72
82
  | `cwd` | `string` | 工作目录 |
83
+ | `options` | `Record<string, unknown>` | 透传给后端的额外选项(向后兼容,推荐使用上方的显式字段) |
73
84
 
74
85
  ---
75
86
 
@@ -98,13 +109,18 @@ for await (const msg of porygon.query({ prompt: "解释快速排序" })) {
98
109
  console.log("模型:", msg.model);
99
110
  break;
100
111
  case "stream_chunk":
101
- process.stdout.write(msg.text); // 实时输出
112
+ process.stdout.write(msg.text); // 增量文本,实时输出
102
113
  break;
103
114
  case "assistant":
104
- console.log("完整回复:", msg.text);
115
+ // turnComplete=true: 与前面 stream_chunk 内容重复,流式消费者应跳过
116
+ // turnComplete=false/undefined: 独立文本(如 run() 模式),需要处理
117
+ if (!msg.turnComplete) {
118
+ console.log("回复:", msg.text);
119
+ }
105
120
  break;
106
121
  case "tool_use":
107
122
  console.log("工具调用:", msg.toolName, msg.input);
123
+ if (msg.output) console.log("工具结果:", msg.output);
108
124
  break;
109
125
  case "result":
110
126
  console.log("完成", { cost: msg.costUsd, tokens: msg.inputTokens });
@@ -116,6 +132,17 @@ for await (const msg of porygon.query({ prompt: "解释快速排序" })) {
116
132
  }
117
133
  ```
118
134
 
135
+ **消息流顺序**(两个后端统一):
136
+
137
+ ```
138
+ system → stream_chunk* → assistant(turnComplete) → tool_use* → ... → result
139
+ ```
140
+
141
+ - `stream_chunk` — 增量文本片段,用于实时展示
142
+ - `assistant` — 单个 turn 的完整文本汇总。`turnComplete: true` 表示内容与 `stream_chunk` 重复
143
+ - `tool_use` — 工具调用;若带 `output` 字段则为工具执行结果
144
+ - `result` — 最终结果,包含完整文本和用量统计
145
+
119
146
  ---
120
147
 
121
148
  ### PromptRequest(请求参数)
@@ -139,16 +166,15 @@ for await (const msg of porygon.query({ prompt: "解释快速排序" })) {
139
166
  | `mcpServers` | `Record<string, McpServerConfig>` | 否 | MCP 服务器配置 |
140
167
  | `backendOptions` | `Record<string, unknown>` | 否 | 透传给后端的额外选项 |
141
168
 
142
- **McpServerConfig:**
169
+ **配置合并策略**(`mergeRequest`):
143
170
 
144
- ```ts
145
- interface McpServerConfig {
146
- command: string;
147
- args?: string[];
148
- env?: Record<string, string>;
149
- url?: string;
150
- }
151
- ```
171
+ | 字段 | 策略 |
172
+ |------|------|
173
+ | `model` | request > backendConfig > 不设置 |
174
+ | `timeoutMs` | request > defaults |
175
+ | `maxTurns` | request > defaults |
176
+ | `cwd` | request > backendConfig |
177
+ | `appendSystemPrompt` | defaults + backendConfig + request 三层追加拼接(换行分隔)。若 `systemPrompt` 已设置则忽略全部 append |
152
178
 
153
179
  ---
154
180
 
@@ -159,7 +185,7 @@ interface McpServerConfig {
159
185
  | type | 接口 | 关键字段 |
160
186
  |------|------|----------|
161
187
  | `"system"` | `AgentSystemMessage` | `model?`, `tools?`, `cwd?` |
162
- | `"assistant"` | `AgentAssistantMessage` | `text` |
188
+ | `"assistant"` | `AgentAssistantMessage` | `text`, `turnComplete?` |
163
189
  | `"tool_use"` | `AgentToolUseMessage` | `toolName`, `input`, `output?` |
164
190
  | `"stream_chunk"` | `AgentStreamChunkMessage` | `text` |
165
191
  | `"result"` | `AgentResultMessage` | `text`, `durationMs?`, `costUsd?`, `inputTokens?`, `outputTokens?` |
@@ -167,6 +193,39 @@ interface McpServerConfig {
167
193
 
168
194
  ---
169
195
 
196
+ ### `porygon.checkBackend(backend): Promise<HealthCheckResult>`
197
+
198
+ 检查单个后端的健康状态。
199
+
200
+ ```ts
201
+ const result = await porygon.checkBackend("claude");
202
+ // { available: true, version: "1.2.0", supported: true }
203
+ // { available: false, error: "command not found" }
204
+ ```
205
+
206
+ **HealthCheckResult:**
207
+
208
+ | 字段 | 类型 | 说明 |
209
+ |------|------|------|
210
+ | `available` | `boolean` | 后端是否可用 |
211
+ | `version` | `string?` | CLI 版本号 |
212
+ | `supported` | `boolean?` | 版本是否在测试范围内 |
213
+ | `warnings` | `string[]?` | 兼容性警告 |
214
+ | `error` | `string?` | 错误信息 |
215
+
216
+ ---
217
+
218
+ ### `porygon.healthCheck(): Promise<Record<string, HealthCheckResult>>`
219
+
220
+ 对所有已注册后端进行健康检查(并行执行)。
221
+
222
+ ```ts
223
+ const health = await porygon.healthCheck();
224
+ // { claude: { available: true, version: "1.2.0", supported: true }, opencode: { available: false, error: "..." } }
225
+ ```
226
+
227
+ ---
228
+
170
229
  ### `porygon.use(direction, fn): () => void`
171
230
 
172
231
  注册拦截器,返回取消注册函数。
@@ -186,17 +245,6 @@ type InterceptorFn = (
186
245
  - 返回 `false`:拒绝消息(抛出 `InterceptorRejectedError`)
187
246
  - 返回 `true` / `undefined`:不修改,传递原始文本
188
247
 
189
- **InterceptorContext:**
190
-
191
- ```ts
192
- interface InterceptorContext {
193
- direction: "input" | "output";
194
- backend: string;
195
- sessionId?: string;
196
- messageType?: string; // 仅 output 方向
197
- }
198
- ```
199
-
200
248
  **示例:**
201
249
 
202
250
  ```ts
@@ -263,20 +311,6 @@ porygon.use("output", createOutputGuard({
263
311
 
264
312
  ---
265
313
 
266
- ### `porygon.healthCheck()`
267
-
268
- 对所有已注册后端进行健康检查(并行执行)。
269
-
270
- ```ts
271
- const health = await porygon.healthCheck();
272
- // {
273
- // claude: { available: true, compatibility: { version: "1.2.0", supported: true, warnings: [] } },
274
- // opencode: { available: false, compatibility: null, error: "..." }
275
- // }
276
- ```
277
-
278
- ---
279
-
280
314
  ### `porygon.listSessions(backend?, options?): Promise<SessionInfo[]>`
281
315
 
282
316
  列出指定后端的历史会话。
@@ -324,11 +358,16 @@ const models = await porygon.listModels("claude");
324
358
  const caps = porygon.getCapabilities("claude");
325
359
  // {
326
360
  // features: Set<"streaming" | "session-resume" | "system-prompt" | "tool-restriction" | "mcp" | ...>,
361
+ // streamingMode: "delta" | "chunked",
327
362
  // outputFormats: string[],
328
363
  // testedVersionRange: string,
329
364
  // }
330
365
  ```
331
366
 
367
+ **`streamingMode`:**
368
+ - `"delta"` — 后端原生产生增量 `stream_chunk` 事件(OpenCode)
369
+ - `"chunked"` — 适配器将完整 assistant 拆分为 `stream_chunk` + `assistant`(Claude)
370
+
332
371
  ---
333
372
 
334
373
  ### `porygon.dispose(): Promise<void>`
@@ -383,6 +422,7 @@ porygon.on("health:degraded", (backend: string, warning: string) => {
383
422
  ```ts
384
423
  // 核心
385
424
  export { Porygon, createPorygon } from "@snack-kit/porygon";
425
+ export type { PorygonEvents, HealthCheckResult } from "@snack-kit/porygon";
386
426
 
387
427
  // 类型
388
428
  export type {
@@ -393,7 +433,7 @@ export type {
393
433
  AdapterCapabilities, CompatibilityResult, SessionInfo, SessionListOptions, ModelInfo,
394
434
  InterceptorFn, InterceptorDirection, InterceptorContext,
395
435
  GuardOptions, GuardAction,
396
- PorygonEvents, IAgentAdapter,
436
+ IAgentAdapter,
397
437
  } from "@snack-kit/porygon";
398
438
 
399
439
  // 错误
@@ -420,7 +460,7 @@ import { createPorygon, createInputGuard, createOutputGuard } from "@snack-kit/p
420
460
  const porygon = createPorygon({
421
461
  defaultBackend: "claude",
422
462
  backends: {
423
- claude: { model: "sonnet", appendSystemPrompt: "用中文回答" },
463
+ claude: { model: "sonnet", interactive: false },
424
464
  },
425
465
  });
426
466
 
@@ -462,6 +502,11 @@ const answer = await porygon.run({
462
502
  ### 健康检查后选择可用后端
463
503
 
464
504
  ```ts
505
+ // 检查单个后端
506
+ const result = await porygon.checkBackend("claude");
507
+ if (!result.available) console.error(result.error);
508
+
509
+ // 批量检查所有后端
465
510
  const health = await porygon.healthCheck();
466
511
  const backend = health.claude?.available ? "claude" : "opencode";
467
512
  const answer = await porygon.run({ prompt: "hello", backend });
@@ -479,12 +524,70 @@ Porygon (Facade)
479
524
  └── OpenCodeAdapter # opencode serve + REST API + SSE
480
525
  ```
481
526
 
527
+ **适配器能力对比:**
528
+
529
+ | 能力 | Claude | OpenCode |
530
+ |------|--------|----------|
531
+ | streaming | `chunked` | `delta` |
532
+ | session-resume | ✓ | ✓ |
533
+ | system-prompt | ✓ | ✓ |
534
+ | tool-restriction | ✓ | - |
535
+ | mcp | ✓ | - |
536
+ | subagents | ✓ | - |
537
+ | worktree | ✓ | - |
538
+ | serve-mode | - | ✓ |
539
+
482
540
  ## 开发
483
541
 
484
542
  ```bash
485
543
  npm install
486
- npm build # 构建
487
- npm test # 运行测试
488
- npm dev # watch 模式构建
489
- npm playground # 启动 Playground(http://localhost:3000)
544
+ npm run build # 构建(ESM + CJS + DTS)
545
+ npm test # 运行测试
546
+ npm run dev # watch 模式构建
547
+ npm run playground # 启动 Playground
490
548
  ```
549
+
550
+ ---
551
+
552
+ ## Changelog
553
+
554
+ ### v0.2.0
555
+
556
+ #### 新特性
557
+
558
+ - **`checkBackend(backend)`** — 新增单后端健康检查方法,无需检查全部后端
559
+ - **`HealthCheckResult`** — 健康检查返回扁平化结构 `{ available, version?, supported?, warnings?, error? }`
560
+ - **`BackendConfig.cliPath`** — Claude CLI 自定义路径,有类型提示
561
+ - **`BackendConfig.interactive`** — 布尔值控制是否跳过权限确认
562
+ - **`BackendConfig.serverUrl`** — OpenCode 远程服务地址(顶级字段)
563
+ - **`BackendConfig.apiKey`** — API Key 认证(顶级字段)
564
+ - **`AgentAssistantMessage.turnComplete`** — 标记 assistant 消息为 turn 完整文本汇总,流式消费者可据此跳过重复文本
565
+ - **`AdapterCapabilities.streamingMode`** — 声明后端的流式模式(`"delta"` 或 `"chunked"`)
566
+ - **`IAgentAdapter.deleteSession?`** — 可选的会话删除方法(接口已定义,适配器待实现)
567
+ - **`tool_result` 映射** — Claude 的 `tool_result` 内容块现在映射为带 `output` 字段的 `tool_use` 消息
568
+ - **`session_id` 自动提取** — Claude 原始事件中的 `session_id` 自动映射到 `AgentMessage.sessionId`
569
+ - **CJS 输出** — 同时构建 ESM 和 CJS 格式,支持 Electron 等 CJS 环境
570
+
571
+ #### 破坏性变更
572
+
573
+ - **`mapClaudeEvent()` 返回类型变更** — 从 `AgentMessage | null` 改为 `AgentMessage[]`(仅影响直接调用 mapper 的代码)
574
+ - **Claude 消息流变更** — `assistant` 事件拆分为 `stream_chunk[]` + `assistant(turnComplete: true)`。之前同时累加 `assistant` 和 `stream_chunk` 文本的代码需要调整:只累加 `stream_chunk`,或检查 `turnComplete` 标记
575
+ - **`healthCheck()` 返回类型变更** — 从 `{ available, compatibility: CompatibilityResult | null, error? }` 改为 `HealthCheckResult`(`{ available, version?, supported?, warnings?, error? }`),移除嵌套的 `compatibility` 字段
576
+ - **`AdapterCapabilities` 新增必填字段** — `streamingMode: "delta" | "chunked"` 为必填,自定义适配器需添加
577
+
578
+ #### 改进
579
+
580
+ - `mergeRequest()` 添加详细 JSDoc 注释说明配置合并策略
581
+ - `mapAssistantContent()` 支持多内容块拆分,不再丢失 tool_use 块
582
+
583
+ ### v0.1.0 — 初始版本
584
+
585
+ - 基础 Facade + Adapter 架构
586
+ - Claude Code CLI 适配器(`claude -p --output-format stream-json`)
587
+ - OpenCode 适配器(`opencode serve` + REST API + SSE)
588
+ - 输入/输出拦截器流水线
589
+ - 防护拦截器(prompt 注入检测、敏感信息过滤)
590
+ - 进程生命周期管理(EphemeralProcess / PersistentProcess)
591
+ - 会话管理与恢复
592
+ - Zod 配置校验
593
+ - ESM 输出