@snack-kit/porygon 0.1.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 +490 -0
- package/dist/index.d.ts +715 -0
- package/dist/index.js +1963 -0
- package/dist/index.js.map +1 -0
- package/package.json +41 -0
package/README.md
ADDED
|
@@ -0,0 +1,490 @@
|
|
|
1
|
+
# Porygon
|
|
2
|
+
|
|
3
|
+
统一的 LLM Agent CLI 编程接口框架。通过 Facade + Adapter 模式,将 Claude Code、OpenCode 等 CLI 工具封装为一致的 TypeScript API。
|
|
4
|
+
|
|
5
|
+
## 安装
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm add @snack-kit/porygon
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
**前置条件**:需要安装至少一个 CLI 后端:
|
|
12
|
+
- [Claude Code](https://docs.anthropic.com/en/docs/claude-code) (`claude` 命令)
|
|
13
|
+
- [OpenCode](https://github.com/opencode-ai/opencode) (`opencode` 命令)
|
|
14
|
+
|
|
15
|
+
## 快速上手
|
|
16
|
+
|
|
17
|
+
```ts
|
|
18
|
+
import { createPorygon } from "@snack-kit/porygon";
|
|
19
|
+
|
|
20
|
+
const porygon = createPorygon();
|
|
21
|
+
|
|
22
|
+
// 简单调用:发送 prompt,返回最终文本
|
|
23
|
+
const answer = await porygon.run({ prompt: "什么是闭包?" });
|
|
24
|
+
console.log(answer);
|
|
25
|
+
|
|
26
|
+
// 释放资源
|
|
27
|
+
await porygon.dispose();
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## API 速查
|
|
31
|
+
|
|
32
|
+
### `createPorygon(config?): Porygon`
|
|
33
|
+
|
|
34
|
+
工厂函数,创建 Porygon 实例。
|
|
35
|
+
|
|
36
|
+
```ts
|
|
37
|
+
const porygon = createPorygon({
|
|
38
|
+
defaultBackend: "claude", // "claude" | "opencode"
|
|
39
|
+
backends: {
|
|
40
|
+
claude: {
|
|
41
|
+
model: "sonnet",
|
|
42
|
+
appendSystemPrompt: "用中文回答",
|
|
43
|
+
cwd: "/path/to/project",
|
|
44
|
+
proxy: { url: "http://127.0.0.1:7897" },
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
defaults: {
|
|
48
|
+
timeoutMs: 300_000,
|
|
49
|
+
maxTurns: 50,
|
|
50
|
+
appendSystemPrompt: "请简洁回答",
|
|
51
|
+
},
|
|
52
|
+
proxy: { url: "http://proxy:8080", noProxy: "localhost" },
|
|
53
|
+
});
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
**PorygonConfig 字段:**
|
|
57
|
+
|
|
58
|
+
| 字段 | 类型 | 说明 |
|
|
59
|
+
|------|------|------|
|
|
60
|
+
| `defaultBackend` | `string` | 默认后端名称 |
|
|
61
|
+
| `backends` | `Record<string, BackendConfig>` | 各后端独立配置 |
|
|
62
|
+
| `defaults` | `{ appendSystemPrompt?, timeoutMs?, maxTurns? }` | 全局默认参数 |
|
|
63
|
+
| `proxy` | `{ url: string, noProxy?: string }` | 全局代理(后端级 proxy 优先) |
|
|
64
|
+
|
|
65
|
+
**BackendConfig 字段:**
|
|
66
|
+
|
|
67
|
+
| 字段 | 类型 | 说明 |
|
|
68
|
+
|------|------|------|
|
|
69
|
+
| `model` | `string` | 模型名称 |
|
|
70
|
+
| `appendSystemPrompt` | `string` | 追加系统提示词 |
|
|
71
|
+
| `proxy` | `ProxyConfig` | 后端专属代理 |
|
|
72
|
+
| `cwd` | `string` | 工作目录 |
|
|
73
|
+
|
|
74
|
+
---
|
|
75
|
+
|
|
76
|
+
### `porygon.run(request): Promise<string>`
|
|
77
|
+
|
|
78
|
+
发送 prompt,等待完成,返回最终结果文本。适合简单的一次性调用。
|
|
79
|
+
|
|
80
|
+
```ts
|
|
81
|
+
const result = await porygon.run({
|
|
82
|
+
prompt: "写一个 HTTP 服务器",
|
|
83
|
+
backend: "claude",
|
|
84
|
+
model: "opus",
|
|
85
|
+
});
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
---
|
|
89
|
+
|
|
90
|
+
### `porygon.query(request): AsyncGenerator<AgentMessage>`
|
|
91
|
+
|
|
92
|
+
流式查询,逐条 yield `AgentMessage`。适合需要实时输出或监控工具调用的场景。
|
|
93
|
+
|
|
94
|
+
```ts
|
|
95
|
+
for await (const msg of porygon.query({ prompt: "解释快速排序" })) {
|
|
96
|
+
switch (msg.type) {
|
|
97
|
+
case "system":
|
|
98
|
+
console.log("模型:", msg.model);
|
|
99
|
+
break;
|
|
100
|
+
case "stream_chunk":
|
|
101
|
+
process.stdout.write(msg.text); // 实时输出
|
|
102
|
+
break;
|
|
103
|
+
case "assistant":
|
|
104
|
+
console.log("完整回复:", msg.text);
|
|
105
|
+
break;
|
|
106
|
+
case "tool_use":
|
|
107
|
+
console.log("工具调用:", msg.toolName, msg.input);
|
|
108
|
+
break;
|
|
109
|
+
case "result":
|
|
110
|
+
console.log("完成", { cost: msg.costUsd, tokens: msg.inputTokens });
|
|
111
|
+
break;
|
|
112
|
+
case "error":
|
|
113
|
+
console.error("错误:", msg.message);
|
|
114
|
+
break;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
---
|
|
120
|
+
|
|
121
|
+
### PromptRequest(请求参数)
|
|
122
|
+
|
|
123
|
+
`run()` 和 `query()` 共用同一参数类型:
|
|
124
|
+
|
|
125
|
+
| 字段 | 类型 | 必填 | 说明 |
|
|
126
|
+
|------|------|------|------|
|
|
127
|
+
| `prompt` | `string` | 是 | 提示词 |
|
|
128
|
+
| `backend` | `string` | 否 | 后端名称(默认取 config.defaultBackend) |
|
|
129
|
+
| `model` | `string` | 否 | 模型名称 |
|
|
130
|
+
| `resume` | `string` | 否 | 恢复会话的 session ID |
|
|
131
|
+
| `systemPrompt` | `string` | 否 | 系统提示词(替换模式,设置后 appendSystemPrompt 失效) |
|
|
132
|
+
| `appendSystemPrompt` | `string` | 否 | 系统提示词(追加模式,与 config 中的追加内容合并) |
|
|
133
|
+
| `allowedTools` | `string[]` | 否 | 允许的工具白名单 |
|
|
134
|
+
| `disallowedTools` | `string[]` | 否 | 禁用的工具黑名单 |
|
|
135
|
+
| `maxTurns` | `number` | 否 | 最大对话轮次 |
|
|
136
|
+
| `timeoutMs` | `number` | 否 | 超时毫秒数 |
|
|
137
|
+
| `cwd` | `string` | 否 | 工作目录 |
|
|
138
|
+
| `envVars` | `Record<string, string>` | 否 | 注入子进程的环境变量 |
|
|
139
|
+
| `mcpServers` | `Record<string, McpServerConfig>` | 否 | MCP 服务器配置 |
|
|
140
|
+
| `backendOptions` | `Record<string, unknown>` | 否 | 透传给后端的额外选项 |
|
|
141
|
+
|
|
142
|
+
**McpServerConfig:**
|
|
143
|
+
|
|
144
|
+
```ts
|
|
145
|
+
interface McpServerConfig {
|
|
146
|
+
command: string;
|
|
147
|
+
args?: string[];
|
|
148
|
+
env?: Record<string, string>;
|
|
149
|
+
url?: string;
|
|
150
|
+
}
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
---
|
|
154
|
+
|
|
155
|
+
### AgentMessage(消息类型)
|
|
156
|
+
|
|
157
|
+
`porygon.query()` 返回的 `AgentMessage` 是一个联合类型。所有消息均包含 `timestamp: number`,可选 `sessionId?: string` 和 `raw?: unknown`(后端原始数据)。
|
|
158
|
+
|
|
159
|
+
| type | 接口 | 关键字段 |
|
|
160
|
+
|------|------|----------|
|
|
161
|
+
| `"system"` | `AgentSystemMessage` | `model?`, `tools?`, `cwd?` |
|
|
162
|
+
| `"assistant"` | `AgentAssistantMessage` | `text` |
|
|
163
|
+
| `"tool_use"` | `AgentToolUseMessage` | `toolName`, `input`, `output?` |
|
|
164
|
+
| `"stream_chunk"` | `AgentStreamChunkMessage` | `text` |
|
|
165
|
+
| `"result"` | `AgentResultMessage` | `text`, `durationMs?`, `costUsd?`, `inputTokens?`, `outputTokens?` |
|
|
166
|
+
| `"error"` | `AgentErrorMessage` | `message`, `code?` |
|
|
167
|
+
|
|
168
|
+
---
|
|
169
|
+
|
|
170
|
+
### `porygon.use(direction, fn): () => void`
|
|
171
|
+
|
|
172
|
+
注册拦截器,返回取消注册函数。
|
|
173
|
+
|
|
174
|
+
**direction**: `"input"` | `"output"`
|
|
175
|
+
|
|
176
|
+
**InterceptorFn 签名:**
|
|
177
|
+
|
|
178
|
+
```ts
|
|
179
|
+
type InterceptorFn = (
|
|
180
|
+
text: string,
|
|
181
|
+
context: InterceptorContext
|
|
182
|
+
) => string | boolean | undefined | Promise<string | boolean | undefined>;
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
- 返回 `string`:替换文本
|
|
186
|
+
- 返回 `false`:拒绝消息(抛出 `InterceptorRejectedError`)
|
|
187
|
+
- 返回 `true` / `undefined`:不修改,传递原始文本
|
|
188
|
+
|
|
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
|
+
**示例:**
|
|
201
|
+
|
|
202
|
+
```ts
|
|
203
|
+
// 输入拦截:追加指令
|
|
204
|
+
const unuse = porygon.use("input", (text) => text + "\n请用 Markdown 格式回答");
|
|
205
|
+
|
|
206
|
+
// 输出拦截:过滤敏感信息
|
|
207
|
+
porygon.use("output", (text) => text.replace(/sk-[a-zA-Z0-9]{48}/g, "[REDACTED]"));
|
|
208
|
+
|
|
209
|
+
// 拒绝过长输入
|
|
210
|
+
porygon.use("input", (text) => { if (text.length > 10000) return false; });
|
|
211
|
+
|
|
212
|
+
// 取消注册
|
|
213
|
+
unuse();
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
---
|
|
217
|
+
|
|
218
|
+
### 防护拦截器
|
|
219
|
+
|
|
220
|
+
内置的 prompt 注入防护工具函数。
|
|
221
|
+
|
|
222
|
+
#### `createInputGuard(options?): InterceptorFn`
|
|
223
|
+
|
|
224
|
+
检测并阻止常见 prompt 注入攻击(内置 13 条中英文模式)。
|
|
225
|
+
|
|
226
|
+
```ts
|
|
227
|
+
import { createInputGuard } from "@snack-kit/porygon";
|
|
228
|
+
|
|
229
|
+
// 使用内置规则
|
|
230
|
+
porygon.use("input", createInputGuard());
|
|
231
|
+
|
|
232
|
+
// 自定义配置
|
|
233
|
+
porygon.use("input", createInputGuard({
|
|
234
|
+
blockedPatterns: [/我是管理员/, /sudo mode/i],
|
|
235
|
+
action: "reject", // "reject"(拒绝)| "redact"(替换为 [REDACTED])
|
|
236
|
+
backends: ["claude"], // 仅对指定后端生效
|
|
237
|
+
customCheck: (text) => text.includes("bypass"),
|
|
238
|
+
}));
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
#### `createOutputGuard(options?): InterceptorFn`
|
|
242
|
+
|
|
243
|
+
检测输出中的敏感关键词泄露。
|
|
244
|
+
|
|
245
|
+
```ts
|
|
246
|
+
import { createOutputGuard } from "@snack-kit/porygon";
|
|
247
|
+
|
|
248
|
+
porygon.use("output", createOutputGuard({
|
|
249
|
+
sensitiveKeywords: ["内部API密钥", "sk-xxxx"],
|
|
250
|
+
action: "redact",
|
|
251
|
+
}));
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
**GuardOptions:**
|
|
255
|
+
|
|
256
|
+
| 字段 | 类型 | 说明 |
|
|
257
|
+
|------|------|------|
|
|
258
|
+
| `blockedPatterns` | `RegExp[]` | 额外阻止模式(输入侧) |
|
|
259
|
+
| `sensitiveKeywords` | `string[]` | 敏感关键词列表(输出侧) |
|
|
260
|
+
| `action` | `"reject"` \| `"redact"` | 触发时的处理动作,输入默认 reject,输出默认 redact |
|
|
261
|
+
| `customCheck` | `(text, context) => boolean` | 自定义判定函数 |
|
|
262
|
+
| `backends` | `string[]` | 仅对指定后端生效 |
|
|
263
|
+
|
|
264
|
+
---
|
|
265
|
+
|
|
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
|
+
### `porygon.listSessions(backend?, options?): Promise<SessionInfo[]>`
|
|
281
|
+
|
|
282
|
+
列出指定后端的历史会话。
|
|
283
|
+
|
|
284
|
+
```ts
|
|
285
|
+
const sessions = await porygon.listSessions("claude", { limit: 10 });
|
|
286
|
+
// SessionInfo: { sessionId, backend, summary?, lastModified, cwd?, metadata? }
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
---
|
|
290
|
+
|
|
291
|
+
### `porygon.abort(backend, sessionId): void`
|
|
292
|
+
|
|
293
|
+
中止正在运行的查询。
|
|
294
|
+
|
|
295
|
+
---
|
|
296
|
+
|
|
297
|
+
### `porygon.settings(backend, newSettings?): Promise<Record<string, unknown>>`
|
|
298
|
+
|
|
299
|
+
读取或更新后端设置。
|
|
300
|
+
|
|
301
|
+
```ts
|
|
302
|
+
const settings = await porygon.settings("claude");
|
|
303
|
+
await porygon.settings("claude", { model: "opus" });
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
---
|
|
307
|
+
|
|
308
|
+
### `porygon.listModels(backend?): Promise<ModelInfo[]>`
|
|
309
|
+
|
|
310
|
+
获取后端可用模型列表。
|
|
311
|
+
|
|
312
|
+
```ts
|
|
313
|
+
const models = await porygon.listModels("claude");
|
|
314
|
+
// ModelInfo: { id: string, name?: string, provider?: string }
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
---
|
|
318
|
+
|
|
319
|
+
### `porygon.getCapabilities(backend?): AdapterCapabilities`
|
|
320
|
+
|
|
321
|
+
获取后端能力声明。
|
|
322
|
+
|
|
323
|
+
```ts
|
|
324
|
+
const caps = porygon.getCapabilities("claude");
|
|
325
|
+
// {
|
|
326
|
+
// features: Set<"streaming" | "session-resume" | "system-prompt" | "tool-restriction" | "mcp" | ...>,
|
|
327
|
+
// outputFormats: string[],
|
|
328
|
+
// testedVersionRange: string,
|
|
329
|
+
// }
|
|
330
|
+
```
|
|
331
|
+
|
|
332
|
+
---
|
|
333
|
+
|
|
334
|
+
### `porygon.dispose(): Promise<void>`
|
|
335
|
+
|
|
336
|
+
终止所有进程、中止所有查询、清空缓存。使用完毕后必须调用。
|
|
337
|
+
|
|
338
|
+
---
|
|
339
|
+
|
|
340
|
+
## 错误处理
|
|
341
|
+
|
|
342
|
+
所有错误继承自 `PorygonError`,包含 `code` 字段:
|
|
343
|
+
|
|
344
|
+
```ts
|
|
345
|
+
import { PorygonError, AdapterNotFoundError } from "@snack-kit/porygon";
|
|
346
|
+
|
|
347
|
+
try {
|
|
348
|
+
await porygon.run({ prompt: "hi", backend: "nonexistent" });
|
|
349
|
+
} catch (err) {
|
|
350
|
+
if (err instanceof AdapterNotFoundError) {
|
|
351
|
+
console.log(err.code); // "ADAPTER_NOT_FOUND"
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
```
|
|
355
|
+
|
|
356
|
+
| 错误类 | code | 场景 |
|
|
357
|
+
|--------|------|------|
|
|
358
|
+
| `AdapterNotFoundError` | `ADAPTER_NOT_FOUND` | 后端未注册 |
|
|
359
|
+
| `AdapterNotAvailableError` | `ADAPTER_NOT_AVAILABLE` | CLI 不可用 |
|
|
360
|
+
| `AdapterIncompatibleError` | `ADAPTER_INCOMPATIBLE` | 版本不兼容 |
|
|
361
|
+
| `SessionNotFoundError` | `SESSION_NOT_FOUND` | 会话不存在 |
|
|
362
|
+
| `InterceptorRejectedError` | `INTERCEPTOR_REJECTED` | 拦截器拒绝 |
|
|
363
|
+
| `AgentExecutionError` | `AGENT_EXECUTION_ERROR` | 执行异常 |
|
|
364
|
+
| `AgentTimeoutError` | `AGENT_TIMEOUT` | 超时 |
|
|
365
|
+
| `ConfigValidationError` | `CONFIG_VALIDATION_ERROR` | 配置校验失败 |
|
|
366
|
+
|
|
367
|
+
## 事件
|
|
368
|
+
|
|
369
|
+
Porygon 继承自 EventEmitter:
|
|
370
|
+
|
|
371
|
+
```ts
|
|
372
|
+
porygon.on("health:degraded", (backend: string, warning: string) => {
|
|
373
|
+
console.warn(`${backend} 降级: ${warning}`);
|
|
374
|
+
});
|
|
375
|
+
```
|
|
376
|
+
|
|
377
|
+
| 事件 | 参数 | 说明 |
|
|
378
|
+
|------|------|------|
|
|
379
|
+
| `health:degraded` | `(backend, warning)` | 后端版本不兼容时触发 |
|
|
380
|
+
|
|
381
|
+
## 完整导出列表
|
|
382
|
+
|
|
383
|
+
```ts
|
|
384
|
+
// 核心
|
|
385
|
+
export { Porygon, createPorygon } from "@snack-kit/porygon";
|
|
386
|
+
|
|
387
|
+
// 类型
|
|
388
|
+
export type {
|
|
389
|
+
PorygonConfig, BackendConfig, ProxyConfig,
|
|
390
|
+
PromptRequest, McpServerConfig,
|
|
391
|
+
AgentMessage, AgentSystemMessage, AgentAssistantMessage,
|
|
392
|
+
AgentToolUseMessage, AgentStreamChunkMessage, AgentResultMessage, AgentErrorMessage,
|
|
393
|
+
AdapterCapabilities, CompatibilityResult, SessionInfo, SessionListOptions, ModelInfo,
|
|
394
|
+
InterceptorFn, InterceptorDirection, InterceptorContext,
|
|
395
|
+
GuardOptions, GuardAction,
|
|
396
|
+
PorygonEvents, IAgentAdapter,
|
|
397
|
+
} from "@snack-kit/porygon";
|
|
398
|
+
|
|
399
|
+
// 错误
|
|
400
|
+
export {
|
|
401
|
+
PorygonError, AdapterNotFoundError, AdapterNotAvailableError,
|
|
402
|
+
AdapterIncompatibleError, SessionNotFoundError, InterceptorRejectedError,
|
|
403
|
+
AgentExecutionError, AgentTimeoutError, ConfigValidationError,
|
|
404
|
+
} from "@snack-kit/porygon";
|
|
405
|
+
|
|
406
|
+
// 防护拦截器
|
|
407
|
+
export { createInputGuard, createOutputGuard } from "@snack-kit/porygon";
|
|
408
|
+
|
|
409
|
+
// 适配器(自定义扩展用)
|
|
410
|
+
export { AbstractAgentAdapter, ClaudeAdapter, OpenCodeAdapter } from "@snack-kit/porygon";
|
|
411
|
+
```
|
|
412
|
+
|
|
413
|
+
## 典型使用模式
|
|
414
|
+
|
|
415
|
+
### 带防护的流式调用
|
|
416
|
+
|
|
417
|
+
```ts
|
|
418
|
+
import { createPorygon, createInputGuard, createOutputGuard } from "@snack-kit/porygon";
|
|
419
|
+
|
|
420
|
+
const porygon = createPorygon({
|
|
421
|
+
defaultBackend: "claude",
|
|
422
|
+
backends: {
|
|
423
|
+
claude: { model: "sonnet", appendSystemPrompt: "用中文回答" },
|
|
424
|
+
},
|
|
425
|
+
});
|
|
426
|
+
|
|
427
|
+
porygon.use("input", createInputGuard());
|
|
428
|
+
porygon.use("output", createOutputGuard({ sensitiveKeywords: ["SECRET"] }));
|
|
429
|
+
|
|
430
|
+
for await (const msg of porygon.query({ prompt: "解释 async/await" })) {
|
|
431
|
+
if (msg.type === "stream_chunk") process.stdout.write(msg.text);
|
|
432
|
+
if (msg.type === "result") console.log("\n完成", msg.costUsd);
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
await porygon.dispose();
|
|
436
|
+
```
|
|
437
|
+
|
|
438
|
+
### 恢复会话
|
|
439
|
+
|
|
440
|
+
```ts
|
|
441
|
+
const sessions = await porygon.listSessions("claude", { limit: 1 });
|
|
442
|
+
if (sessions.length > 0) {
|
|
443
|
+
const answer = await porygon.run({
|
|
444
|
+
prompt: "继续上次的工作",
|
|
445
|
+
resume: sessions[0].sessionId,
|
|
446
|
+
});
|
|
447
|
+
}
|
|
448
|
+
```
|
|
449
|
+
|
|
450
|
+
### 注入环境变量与 MCP 服务器
|
|
451
|
+
|
|
452
|
+
```ts
|
|
453
|
+
const answer = await porygon.run({
|
|
454
|
+
prompt: "查找相关文档后回答",
|
|
455
|
+
envVars: { CLAUDE_CODE_OAUTH_TOKEN: "token-xxx" },
|
|
456
|
+
mcpServers: {
|
|
457
|
+
"context7": { command: "npx", args: ["-y", "@context7/mcp"] },
|
|
458
|
+
},
|
|
459
|
+
});
|
|
460
|
+
```
|
|
461
|
+
|
|
462
|
+
### 健康检查后选择可用后端
|
|
463
|
+
|
|
464
|
+
```ts
|
|
465
|
+
const health = await porygon.healthCheck();
|
|
466
|
+
const backend = health.claude?.available ? "claude" : "opencode";
|
|
467
|
+
const answer = await porygon.run({ prompt: "hello", backend });
|
|
468
|
+
```
|
|
469
|
+
|
|
470
|
+
## 架构
|
|
471
|
+
|
|
472
|
+
```
|
|
473
|
+
Porygon (Facade)
|
|
474
|
+
├── InterceptorManager # 输入/输出拦截器流水线
|
|
475
|
+
├── ProcessManager # 子进程生命周期管理
|
|
476
|
+
├── SessionManager # 会话缓存与委托
|
|
477
|
+
└── Adapters
|
|
478
|
+
├── ClaudeAdapter # claude -p --output-format stream-json
|
|
479
|
+
└── OpenCodeAdapter # opencode serve + REST API + SSE
|
|
480
|
+
```
|
|
481
|
+
|
|
482
|
+
## 开发
|
|
483
|
+
|
|
484
|
+
```bash
|
|
485
|
+
npm install
|
|
486
|
+
npm build # 构建
|
|
487
|
+
npm test # 运行测试
|
|
488
|
+
npm dev # watch 模式构建
|
|
489
|
+
npm playground # 启动 Playground(http://localhost:3000)
|
|
490
|
+
```
|