@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/dist/index.d.ts CHANGED
@@ -15,7 +15,15 @@ interface BackendConfig {
15
15
  appendSystemPrompt?: string;
16
16
  proxy?: ProxyConfig;
17
17
  cwd?: string;
18
- /** 透传给 CLI 的后端特定选项 */
18
+ /** 远程服务地址(OpenCode serve 模式) */
19
+ serverUrl?: string;
20
+ /** API Key 认证 */
21
+ apiKey?: string;
22
+ /** 是否为交互模式,false 时 Claude 会添加 --dangerously-skip-permissions */
23
+ interactive?: boolean;
24
+ /** CLI 可执行文件路径(如 Claude CLI 的自定义安装路径) */
25
+ cliPath?: string;
26
+ /** 透传给 CLI 的后端特定选项(向后兼容,推荐使用上方的显式字段) */
19
27
  options?: Record<string, unknown>;
20
28
  }
21
29
  /**
@@ -84,6 +92,13 @@ interface AgentSystemMessage extends BaseAgentMessage {
84
92
  interface AgentAssistantMessage extends BaseAgentMessage {
85
93
  type: "assistant";
86
94
  text: string;
95
+ /**
96
+ * 标记此消息为一个 turn 的完整文本汇总。
97
+ * 当为 true 时,text 内容与之前发出的 stream_chunk 消息完全重复,
98
+ * 调用方如果已通过 stream_chunk 累加文本,应忽略此消息的 text 以避免重复。
99
+ * run() 和拦截器仍会使用此消息。
100
+ */
101
+ turnComplete?: boolean;
87
102
  }
88
103
  interface AgentToolUseMessage extends BaseAgentMessage {
89
104
  type: "tool_use";
@@ -114,6 +129,12 @@ interface AgentErrorMessage extends BaseAgentMessage {
114
129
  */
115
130
  interface AdapterCapabilities {
116
131
  features: Set<"streaming" | "session-resume" | "system-prompt" | "tool-restriction" | "mcp" | "subagents" | "worktree" | "budget-limit" | "serve-mode">;
132
+ /**
133
+ * 流式输出模式:
134
+ * - 'delta': 后端原生产生增量 stream_chunk 事件(如 OpenCode)
135
+ * - 'chunked': 适配器将完整 assistant 消息拆分为 stream_chunk + assistant(如 Claude)
136
+ */
137
+ streamingMode: "delta" | "chunked";
117
138
  outputFormats: string[];
118
139
  testedVersionRange: string;
119
140
  }
@@ -159,6 +180,8 @@ interface IAgentAdapter {
159
180
  /** 获取可用模型列表 */
160
181
  listModels(): Promise<ModelInfo[]>;
161
182
  abort(sessionId: string): void;
183
+ /** 删除会话及其资源(可选实现) */
184
+ deleteSession?(sessionId: string): Promise<void>;
162
185
  dispose(): Promise<void>;
163
186
  }
164
187
  /**
@@ -186,6 +209,16 @@ interface InterceptorContext {
186
209
  */
187
210
  type InterceptorFn = (text: string, context: InterceptorContext) => string | boolean | undefined | Promise<string | boolean | undefined>;
188
211
 
212
+ /**
213
+ * 健康检查结果(扁平化结构)
214
+ */
215
+ interface HealthCheckResult {
216
+ available: boolean;
217
+ version?: string;
218
+ supported?: boolean;
219
+ warnings?: string[];
220
+ error?: string;
221
+ }
189
222
  /**
190
223
  * Porygon 事件类型定义
191
224
  */
@@ -255,13 +288,15 @@ declare class Porygon extends EventEmitter {
255
288
  */
256
289
  listModels(backend?: string): Promise<ModelInfo[]>;
257
290
  /**
258
- * 对所有已注册后端进行健康检查
291
+ * 检查单个后端的健康状态
292
+ * @param backend 后端名称
259
293
  */
260
- healthCheck(): Promise<Record<string, {
261
- available: boolean;
262
- compatibility: CompatibilityResult | null;
263
- error?: string;
264
- }>>;
294
+ checkBackend(backend: string): Promise<HealthCheckResult>;
295
+ /**
296
+ * 对所有已注册后端进行健康检查。
297
+ * 返回扁平化结构,包含 version/supported/warnings 等字段。
298
+ */
299
+ healthCheck(): Promise<Record<string, HealthCheckResult>>;
265
300
  /**
266
301
  * 读取或更新后端设置
267
302
  * @param backend 后端名称
@@ -286,7 +321,16 @@ declare class Porygon extends EventEmitter {
286
321
  */
287
322
  dispose(): Promise<void>;
288
323
  /**
289
- * 合并请求参数与配置默认值
324
+ * 合并请求参数与配置默认值。
325
+ *
326
+ * 合并策略:
327
+ * - model: request > backendConfig > 不设置(后端使用自身默认值)
328
+ * - timeoutMs: request > defaults
329
+ * - maxTurns: request > defaults
330
+ * - cwd: request > backendConfig
331
+ * - appendSystemPrompt: **追加模式** — defaults + backendConfig + request 三层拼接(换行分隔)
332
+ * 但如果 request.systemPrompt 已设置(替换模式),则忽略所有 appendSystemPrompt
333
+ *
290
334
  * @param request 原始请求
291
335
  * @param backend 后端名称
292
336
  * @returns 合并后的请求
@@ -588,6 +632,8 @@ declare abstract class AbstractAgentAdapter implements IAgentAdapter {
588
632
  */
589
633
  declare class ClaudeAdapter extends AbstractAgentAdapter {
590
634
  readonly backend = "claude";
635
+ /** CLI 命令名或路径 */
636
+ private get cliCommand();
591
637
  /**
592
638
  * 检查 Claude CLI 是否可用
593
639
  */
@@ -646,11 +692,18 @@ declare class OpenCodeAdapter extends AbstractAgentAdapter {
646
692
  readonly backend = "opencode";
647
693
  private apiClient;
648
694
  private servePort;
695
+ /** 远程服务器 URL(提供时跳过自启 serve) */
696
+ private remoteServerUrl?;
697
+ /** API Key(用于 Basic Auth) */
698
+ private apiKey?;
649
699
  /**
650
700
  * @param processManager 进程管理器实例
651
- * @param servePort 可选的 serve 服务端口
701
+ * @param config 可选的后端配置
702
+ * - serverUrl: 远程 OpenCode 服务地址,提供时跳过自启 serve
703
+ * - apiKey: API Key,用于 Basic Auth 认证
704
+ * - options.servePort: 本地 serve 端口(仅自启模式)
652
705
  */
653
- constructor(processManager: ProcessManager, servePort?: number);
706
+ constructor(processManager: ProcessManager, config?: BackendConfig);
654
707
  /**
655
708
  * 检查 opencode 命令是否可用
656
709
  */
@@ -712,4 +765,4 @@ declare class OpenCodeAdapter extends AbstractAgentAdapter {
712
765
  private getConfigPath;
713
766
  }
714
767
 
715
- export { AbstractAgentAdapter, type AdapterCapabilities, AdapterIncompatibleError, AdapterNotAvailableError, AdapterNotFoundError, type AgentAssistantMessage, type AgentErrorMessage, AgentExecutionError, type AgentMessage, type AgentResultMessage, type AgentStreamChunkMessage, type AgentSystemMessage, AgentTimeoutError, type AgentToolUseMessage, type BackendConfig, ClaudeAdapter, type CompatibilityResult, ConfigValidationError, type GuardAction, type GuardOptions, type IAgentAdapter, type InterceptorContext, type InterceptorDirection, type InterceptorFn, InterceptorRejectedError, type McpServerConfig, type ModelInfo, OpenCodeAdapter, Porygon, type PorygonConfig, PorygonError, type PorygonEvents, type PromptRequest, type ProxyConfig, type SessionInfo, type SessionListOptions, SessionNotFoundError, createInputGuard, createOutputGuard, createPorygon };
768
+ export { AbstractAgentAdapter, type AdapterCapabilities, AdapterIncompatibleError, AdapterNotAvailableError, AdapterNotFoundError, type AgentAssistantMessage, type AgentErrorMessage, AgentExecutionError, type AgentMessage, type AgentResultMessage, type AgentStreamChunkMessage, type AgentSystemMessage, AgentTimeoutError, type AgentToolUseMessage, type BackendConfig, ClaudeAdapter, type CompatibilityResult, ConfigValidationError, type GuardAction, type GuardOptions, type HealthCheckResult, type IAgentAdapter, type InterceptorContext, type InterceptorDirection, type InterceptorFn, InterceptorRejectedError, type McpServerConfig, type ModelInfo, OpenCodeAdapter, Porygon, type PorygonConfig, PorygonError, type PorygonEvents, type PromptRequest, type ProxyConfig, type SessionInfo, type SessionListOptions, SessionNotFoundError, createInputGuard, createOutputGuard, createPorygon };
package/dist/index.js CHANGED
@@ -88,6 +88,10 @@ var BackendConfigSchema = z.object({
88
88
  appendSystemPrompt: z.string().optional(),
89
89
  proxy: ProxyConfigSchema.optional(),
90
90
  cwd: z.string().optional(),
91
+ serverUrl: z.string().optional(),
92
+ apiKey: z.string().optional(),
93
+ interactive: z.boolean().optional(),
94
+ cliPath: z.string().optional(),
91
95
  options: z.record(z.string(), z.unknown()).optional()
92
96
  });
93
97
  var PorygonConfigSchema = z.object({
@@ -780,15 +784,16 @@ function isResultEvent(event) {
780
784
  // src/adapters/claude/message-mapper.ts
781
785
  function mapClaudeEvent(event, sessionId) {
782
786
  const timestamp = Date.now();
783
- const baseFields = { timestamp, sessionId, raw: event };
787
+ const resolvedSessionId = event.session_id ?? sessionId;
788
+ const baseFields = { timestamp, sessionId: resolvedSessionId, raw: event };
784
789
  if (isSystemEvent(event)) {
785
- return {
790
+ return [{
786
791
  ...baseFields,
787
792
  type: "system",
788
793
  model: event.model,
789
794
  tools: event.tools,
790
795
  cwd: event.cwd
791
- };
796
+ }];
792
797
  }
793
798
  if (isAssistantEvent(event)) {
794
799
  const content = event.message.content;
@@ -797,16 +802,16 @@ function mapClaudeEvent(event, sessionId) {
797
802
  if (isStreamEvent(event)) {
798
803
  const delta = event.event.delta;
799
804
  if (delta?.type === "text_delta" && delta.text) {
800
- return {
805
+ return [{
801
806
  ...baseFields,
802
807
  type: "stream_chunk",
803
808
  text: delta.text
804
- };
809
+ }];
805
810
  }
806
- return null;
811
+ return [];
807
812
  }
808
813
  if (isResultEvent(event)) {
809
- return {
814
+ return [{
810
815
  ...baseFields,
811
816
  type: "result",
812
817
  text: event.result,
@@ -814,38 +819,52 @@ function mapClaudeEvent(event, sessionId) {
814
819
  durationMs: event.duration_ms,
815
820
  inputTokens: event.input_tokens,
816
821
  outputTokens: event.output_tokens
817
- };
822
+ }];
818
823
  }
819
- return null;
824
+ return [];
820
825
  }
821
826
  function mapAssistantContent(blocks, baseFields) {
822
- if (!blocks || blocks.length === 0) return null;
827
+ if (!blocks || blocks.length === 0) return [];
828
+ const messages = [];
823
829
  const textParts = [];
824
- let firstToolUse = null;
825
830
  for (const block of blocks) {
826
831
  if (block.type === "text" && block.text) {
827
832
  textParts.push(block.text);
828
- }
829
- if (block.type === "tool_use" && !firstToolUse) {
830
- firstToolUse = { name: block.name, input: block.input };
833
+ messages.push({
834
+ ...baseFields,
835
+ type: "stream_chunk",
836
+ text: block.text
837
+ });
831
838
  }
832
839
  }
833
840
  if (textParts.length > 0) {
834
- return {
841
+ messages.push({
835
842
  ...baseFields,
836
843
  type: "assistant",
837
- text: textParts.join("\n")
838
- };
844
+ text: textParts.join("\n"),
845
+ turnComplete: true
846
+ });
839
847
  }
840
- if (firstToolUse) {
841
- return {
842
- ...baseFields,
843
- type: "tool_use",
844
- toolName: firstToolUse.name,
845
- input: firstToolUse.input
846
- };
848
+ for (const block of blocks) {
849
+ if (block.type === "tool_use") {
850
+ messages.push({
851
+ ...baseFields,
852
+ type: "tool_use",
853
+ toolName: block.name,
854
+ input: block.input
855
+ });
856
+ }
857
+ if (block.type === "tool_result") {
858
+ messages.push({
859
+ ...baseFields,
860
+ type: "tool_use",
861
+ toolName: block.tool_use_id,
862
+ input: {},
863
+ output: block.content
864
+ });
865
+ }
847
866
  }
848
- return null;
867
+ return messages;
849
868
  }
850
869
 
851
870
  // src/adapters/claude/index.ts
@@ -857,6 +876,10 @@ var CLAUDE_MODELS = [
857
876
  ];
858
877
  var ClaudeAdapter = class extends AbstractAgentAdapter {
859
878
  backend = "claude";
879
+ /** CLI 命令名或路径 */
880
+ get cliCommand() {
881
+ return this.config?.cliPath ?? "claude";
882
+ }
860
883
  /**
861
884
  * 检查 Claude CLI 是否可用
862
885
  */
@@ -865,7 +888,7 @@ var ClaudeAdapter = class extends AbstractAgentAdapter {
865
888
  const proc = new EphemeralProcess();
866
889
  const result = await proc.execute({
867
890
  command: "which",
868
- args: ["claude"]
891
+ args: [this.cliCommand]
869
892
  });
870
893
  return result.exitCode === 0;
871
894
  } catch {
@@ -878,7 +901,7 @@ var ClaudeAdapter = class extends AbstractAgentAdapter {
878
901
  async getVersion() {
879
902
  const proc = new EphemeralProcess();
880
903
  const result = await proc.execute({
881
- command: "claude",
904
+ command: this.cliCommand,
882
905
  args: ["--version"]
883
906
  });
884
907
  if (result.exitCode !== 0) {
@@ -905,6 +928,7 @@ var ClaudeAdapter = class extends AbstractAgentAdapter {
905
928
  "subagents",
906
929
  "worktree"
907
930
  ]),
931
+ streamingMode: "chunked",
908
932
  outputFormats: ["text", "json", "stream-json"],
909
933
  testedVersionRange: ">=1.0.0"
910
934
  };
@@ -936,13 +960,13 @@ var ClaudeAdapter = class extends AbstractAgentAdapter {
936
960
  }
937
961
  }
938
962
  const streamOptions = {
939
- command: "claude",
963
+ command: this.cliCommand,
940
964
  args,
941
965
  ...cwd ? { cwd } : {},
942
966
  env: cleanEnv,
943
967
  timeoutMs: request.timeoutMs
944
968
  };
945
- const cmdStr = ["claude", ...args.map((a) => /[\s"']/.test(a) ? JSON.stringify(a) : a)].join(" ");
969
+ const cmdStr = [this.cliCommand, ...args.map((a) => /[\s"']/.test(a) ? JSON.stringify(a) : a)].join(" ");
946
970
  const debugCmd = cwd ? `cd ${JSON.stringify(cwd)} && ${cmdStr}` : cmdStr;
947
971
  yield {
948
972
  type: "system",
@@ -963,8 +987,8 @@ var ClaudeAdapter = class extends AbstractAgentAdapter {
963
987
  } catch {
964
988
  continue;
965
989
  }
966
- const message = mapClaudeEvent(parsed, sessionId);
967
- if (message) {
990
+ const messages = mapClaudeEvent(parsed, sessionId);
991
+ for (const message of messages) {
968
992
  yield message;
969
993
  }
970
994
  }
@@ -984,7 +1008,7 @@ var ClaudeAdapter = class extends AbstractAgentAdapter {
984
1008
  args.push("--limit", String(options.limit));
985
1009
  }
986
1010
  const result = await proc.execute({
987
- command: "claude",
1011
+ command: this.cliCommand,
988
1012
  args,
989
1013
  cwd: options?.cwd
990
1014
  });
@@ -1108,6 +1132,11 @@ var ClaudeAdapter = class extends AbstractAgentAdapter {
1108
1132
  if (request.maxTurns !== void 0) {
1109
1133
  args.push("--max-turns", String(request.maxTurns));
1110
1134
  }
1135
+ const interactive = this.config?.interactive;
1136
+ const skipPerms = interactive === false || request.backendOptions?.dangerouslySkipPermissions || this.config?.options?.dangerouslySkipPermissions;
1137
+ if (skipPerms) {
1138
+ args.push("--dangerously-skip-permissions");
1139
+ }
1111
1140
  if (request.mcpServers) {
1112
1141
  for (const [name, config] of Object.entries(request.mcpServers)) {
1113
1142
  const serverSpec = {
@@ -1144,11 +1173,14 @@ var OpenCodeApiError = class extends Error {
1144
1173
  var DEFAULT_TIMEOUT_MS = 3e4;
1145
1174
  var OpenCodeApiClient = class {
1146
1175
  baseUrl;
1176
+ apiKey;
1147
1177
  /**
1148
1178
  * @param baseUrl opencode serve 服务的基础 URL,例如 http://localhost:39393
1179
+ * @param apiKey 可选的 API Key,用于 Basic Auth 认证(username: opencode, password: apiKey)
1149
1180
  */
1150
- constructor(baseUrl) {
1181
+ constructor(baseUrl, apiKey) {
1151
1182
  this.baseUrl = baseUrl.replace(/\/+$/, "");
1183
+ this.apiKey = apiKey;
1152
1184
  }
1153
1185
  /**
1154
1186
  * 创建新会话
@@ -1206,7 +1238,8 @@ var OpenCodeApiClient = class {
1206
1238
  method: "GET",
1207
1239
  headers: {
1208
1240
  Accept: "text/event-stream",
1209
- "Cache-Control": "no-cache"
1241
+ "Cache-Control": "no-cache",
1242
+ ...this.buildAuthHeaders()
1210
1243
  },
1211
1244
  signal: abortSignal
1212
1245
  });
@@ -1257,6 +1290,7 @@ var OpenCodeApiClient = class {
1257
1290
  const timer = setTimeout(() => controller.abort(), 5e3);
1258
1291
  const response = await fetch(`${this.baseUrl}/api/sessions`, {
1259
1292
  method: "GET",
1293
+ headers: this.buildAuthHeaders(),
1260
1294
  signal: controller.signal
1261
1295
  });
1262
1296
  clearTimeout(timer);
@@ -1294,6 +1328,17 @@ var OpenCodeApiClient = class {
1294
1328
  * @param init fetch 请求选项
1295
1329
  * @returns 解析后的 JSON 响应
1296
1330
  */
1331
+ /**
1332
+ * 构建包含认证信息的请求 headers
1333
+ */
1334
+ buildAuthHeaders() {
1335
+ const headers = {};
1336
+ if (this.apiKey) {
1337
+ const credentials = Buffer.from(`opencode:${this.apiKey}`).toString("base64");
1338
+ headers["Authorization"] = `Basic ${credentials}`;
1339
+ }
1340
+ return headers;
1341
+ }
1297
1342
  async request(path, init) {
1298
1343
  const controller = new AbortController();
1299
1344
  const timer = setTimeout(() => controller.abort(), DEFAULT_TIMEOUT_MS);
@@ -1302,6 +1347,7 @@ var OpenCodeApiClient = class {
1302
1347
  ...init,
1303
1348
  headers: {
1304
1349
  "Content-Type": "application/json",
1350
+ ...this.buildAuthHeaders(),
1305
1351
  ...init.headers
1306
1352
  },
1307
1353
  signal: controller.signal
@@ -1395,18 +1441,36 @@ var OpenCodeAdapter = class extends AbstractAgentAdapter {
1395
1441
  backend = "opencode";
1396
1442
  apiClient = null;
1397
1443
  servePort;
1444
+ /** 远程服务器 URL(提供时跳过自启 serve) */
1445
+ remoteServerUrl;
1446
+ /** API Key(用于 Basic Auth) */
1447
+ apiKey;
1398
1448
  /**
1399
1449
  * @param processManager 进程管理器实例
1400
- * @param servePort 可选的 serve 服务端口
1450
+ * @param config 可选的后端配置
1451
+ * - serverUrl: 远程 OpenCode 服务地址,提供时跳过自启 serve
1452
+ * - apiKey: API Key,用于 Basic Auth 认证
1453
+ * - options.servePort: 本地 serve 端口(仅自启模式)
1401
1454
  */
1402
- constructor(processManager, servePort) {
1403
- super(processManager);
1404
- this.servePort = servePort ?? DEFAULT_SERVE_PORT;
1455
+ constructor(processManager, config) {
1456
+ super(processManager, config);
1457
+ const opts = config?.options ?? {};
1458
+ this.remoteServerUrl = config?.serverUrl ?? opts.serverUrl;
1459
+ this.apiKey = config?.apiKey ?? opts.apiKey;
1460
+ this.servePort = opts.servePort ?? DEFAULT_SERVE_PORT;
1405
1461
  }
1406
1462
  /**
1407
1463
  * 检查 opencode 命令是否可用
1408
1464
  */
1409
1465
  async isAvailable() {
1466
+ if (this.remoteServerUrl) {
1467
+ try {
1468
+ const client = new OpenCodeApiClient(this.remoteServerUrl, this.apiKey);
1469
+ return await client.healthCheck();
1470
+ } catch {
1471
+ return false;
1472
+ }
1473
+ }
1410
1474
  try {
1411
1475
  const proc = new EphemeralProcess();
1412
1476
  const result = await proc.execute({
@@ -1445,6 +1509,7 @@ var OpenCodeAdapter = class extends AbstractAgentAdapter {
1445
1509
  "system-prompt",
1446
1510
  "serve-mode"
1447
1511
  ]),
1512
+ streamingMode: "delta",
1448
1513
  outputFormats: ["text"],
1449
1514
  testedVersionRange: TESTED_VERSION_RANGE
1450
1515
  };
@@ -1582,8 +1647,20 @@ var OpenCodeAdapter = class extends AbstractAgentAdapter {
1582
1647
  const healthy = await this.apiClient.healthCheck();
1583
1648
  if (healthy) return this.apiClient;
1584
1649
  }
1650
+ if (this.remoteServerUrl) {
1651
+ const client2 = new OpenCodeApiClient(this.remoteServerUrl, this.apiKey);
1652
+ const healthy = await client2.healthCheck();
1653
+ if (!healthy) {
1654
+ throw new Error(
1655
+ `\u65E0\u6CD5\u8FDE\u63A5\u8FDC\u7A0B OpenCode \u670D\u52A1: ${this.remoteServerUrl}`
1656
+ );
1657
+ }
1658
+ this.apiClient = client2;
1659
+ return client2;
1660
+ }
1585
1661
  const client = new OpenCodeApiClient(
1586
- `http://localhost:${this.servePort}`
1662
+ `http://localhost:${this.servePort}`,
1663
+ this.apiKey
1587
1664
  );
1588
1665
  const alreadyRunning = await client.healthCheck();
1589
1666
  if (alreadyRunning) {
@@ -1661,7 +1738,12 @@ var Porygon = class extends EventEmitter2 {
1661
1738
  };
1662
1739
  const claudeAdapter = new ClaudeAdapter(this.processManager, mergedClaudeConfig);
1663
1740
  this.registerAdapter(claudeAdapter);
1664
- const opencodeAdapter = new OpenCodeAdapter(this.processManager);
1741
+ const opencodeConfig = this.config.backends?.["opencode"];
1742
+ const mergedOpencodeConfig = {
1743
+ ...opencodeConfig,
1744
+ proxy: opencodeConfig?.proxy ?? this.config.proxy
1745
+ };
1746
+ const opencodeAdapter = new OpenCodeAdapter(this.processManager, mergedOpencodeConfig);
1665
1747
  this.registerAdapter(opencodeAdapter);
1666
1748
  }
1667
1749
  /**
@@ -1753,29 +1835,45 @@ var Porygon = class extends EventEmitter2 {
1753
1835
  return this.getAdapter(backend).listModels();
1754
1836
  }
1755
1837
  /**
1756
- * 对所有已注册后端进行健康检查
1838
+ * 检查单个后端的健康状态
1839
+ * @param backend 后端名称
1757
1840
  */
1758
- async healthCheck() {
1759
- const entries = Array.from(this.adapters.entries());
1760
- const checks = entries.map(async ([name, adapter]) => {
1761
- try {
1762
- const available = await adapter.isAvailable();
1763
- let compatibility = null;
1764
- if (available) {
1765
- compatibility = await adapter.checkCompatibility();
1766
- if (!compatibility.supported) {
1767
- this.emit(
1768
- "health:degraded",
1769
- name,
1770
- compatibility.warnings.join("; ")
1771
- );
1772
- }
1773
- }
1774
- return [name, { available, compatibility }];
1775
- } catch (err) {
1776
- const errorMsg = err instanceof Error ? err.message : String(err);
1777
- return [name, { available: false, compatibility: null, error: errorMsg }];
1841
+ async checkBackend(backend) {
1842
+ const adapter = this.getAdapter(backend);
1843
+ try {
1844
+ const available = await adapter.isAvailable();
1845
+ if (!available) {
1846
+ return { available: false };
1847
+ }
1848
+ const compat = await adapter.checkCompatibility();
1849
+ const result = {
1850
+ available: true,
1851
+ version: compat.version,
1852
+ supported: compat.supported
1853
+ };
1854
+ if (compat.warnings.length > 0) {
1855
+ result.warnings = compat.warnings;
1856
+ }
1857
+ if (!compat.supported) {
1858
+ this.emit("health:degraded", backend, compat.warnings.join("; "));
1778
1859
  }
1860
+ return result;
1861
+ } catch (err) {
1862
+ return {
1863
+ available: false,
1864
+ error: err instanceof Error ? err.message : String(err)
1865
+ };
1866
+ }
1867
+ }
1868
+ /**
1869
+ * 对所有已注册后端进行健康检查。
1870
+ * 返回扁平化结构,包含 version/supported/warnings 等字段。
1871
+ */
1872
+ async healthCheck() {
1873
+ const entries = Array.from(this.adapters.keys());
1874
+ const checks = entries.map(async (name) => {
1875
+ const result = await this.checkBackend(name);
1876
+ return [name, result];
1779
1877
  });
1780
1878
  const settled = await Promise.allSettled(checks);
1781
1879
  const results = {};
@@ -1831,7 +1929,16 @@ var Porygon = class extends EventEmitter2 {
1831
1929
  this.sessionManager.clearCache();
1832
1930
  }
1833
1931
  /**
1834
- * 合并请求参数与配置默认值
1932
+ * 合并请求参数与配置默认值。
1933
+ *
1934
+ * 合并策略:
1935
+ * - model: request > backendConfig > 不设置(后端使用自身默认值)
1936
+ * - timeoutMs: request > defaults
1937
+ * - maxTurns: request > defaults
1938
+ * - cwd: request > backendConfig
1939
+ * - appendSystemPrompt: **追加模式** — defaults + backendConfig + request 三层拼接(换行分隔)
1940
+ * 但如果 request.systemPrompt 已设置(替换模式),则忽略所有 appendSystemPrompt
1941
+ *
1835
1942
  * @param request 原始请求
1836
1943
  * @param backend 后端名称
1837
1944
  * @returns 合并后的请求