@ynhcj/xiaoyi-channel 0.0.91-beta → 0.0.92-beta

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.
@@ -9,6 +9,60 @@
9
9
  // models.providers.xiaoyiprovider.models = [...]
10
10
  import { createHash } from "crypto";
11
11
  import { getCurrentSessionContext } from "./tools/session-manager.js";
12
+ // ── Retry config ──────────────────────────────────────────────
13
+ const RETRY_DELAYS_MS = [10_000, 20_000, 40_000, 60_000];
14
+ const MAX_RETRY_ATTEMPTS = 8;
15
+ /** Check if an errorMessage indicates a retryable provider error by type. */
16
+ function isRetryableProviderError(message) {
17
+ if (!message)
18
+ return false;
19
+ const lower = message.toLowerCase();
20
+ if (lower.includes("server_error"))
21
+ return true;
22
+ if (lower.includes("rate_limit_error"))
23
+ return true;
24
+ return false;
25
+ }
26
+ /** Compute retry delay in ms for the given 1-based attempt. */
27
+ function getRetryDelayMs(attempt) {
28
+ // attempt 1→10s, 2→20s, 3→40s, 4+→60s
29
+ if (attempt <= RETRY_DELAYS_MS.length)
30
+ return RETRY_DELAYS_MS[attempt - 1];
31
+ return RETRY_DELAYS_MS[RETRY_DELAYS_MS.length - 1]; // 60s cap
32
+ }
33
+ function sleep(ms) {
34
+ return new Promise((resolve) => setTimeout(resolve, ms));
35
+ }
36
+ /**
37
+ * Build a minimal EventStream-compatible object that replays a single
38
+ * done/error event. This avoids importing @mariozechner/pi-ai at runtime
39
+ * (the package is not available in the extension sandbox).
40
+ */
41
+ function buildReplayStream(result) {
42
+ let settled = false;
43
+ const queued = [
44
+ result.stopReason === "error"
45
+ ? { type: "error", reason: "error", error: result }
46
+ : { type: "done", reason: result.stopReason, message: result },
47
+ ];
48
+ return {
49
+ result: () => Promise.resolve(result),
50
+ push: () => { },
51
+ end: () => { },
52
+ [Symbol.asyncIterator]: () => {
53
+ return {
54
+ next: async () => {
55
+ if (settled || queued.length === 0) {
56
+ settled = true;
57
+ return { value: undefined, done: true };
58
+ }
59
+ settled = true;
60
+ return { value: queued.shift(), done: false };
61
+ },
62
+ };
63
+ },
64
+ };
65
+ }
12
66
  /**
13
67
  * Dynamic header keys injected via extraParams and forwarded to the HTTP request.
14
68
  * Correspond to the three fields written to .xiaoyiruntime:
@@ -73,10 +127,12 @@ export const xiaoyiProvider = {
73
127
  },
74
128
  /**
75
129
  * Wrap the stream function to inject dynamic headers into every
76
- * HTTP request to the model provider.
130
+ * HTTP request to the model provider, and retry on retryable errors
131
+ * (server_error / rate_limit_error) with backoff: 10s, 20s, 40s, 60s (cap).
77
132
  *
78
- * Reads the values injected by prepareExtraParams and adds them
79
- * as HTTP headers on the outgoing request.
133
+ * The retry loop awaits stream.result() to detect errors before deciding
134
+ * whether to retry. This keeps the agent loop waiting (no timeout risk
135
+ * since the default agent timeout is 48 hours).
80
136
  */
81
137
  wrapStreamFn: (ctx) => {
82
138
  const underlying = ctx.streamFn;
@@ -135,21 +191,51 @@ export const xiaoyiProvider = {
135
191
  if (sessionCtx?.deviceType) {
136
192
  const rawDevice = sessionCtx.deviceType;
137
193
  const displayDevice = (rawDevice === "2in1") ? "鸿蒙PC" : rawDevice;
138
- const deviceSection = `\n\n## Current User Device Context\nThe current user is using the following device: ${displayDevice}\nYou need to be aware of the users current device and provide guidance accordingly. If the response involves device-related tools or actions, you must tailor the reply based on the users current device, using device-specific references such as saved to the Notes/Calendar on your {deviceType}.\n”`;
194
+ const deviceSection = `\n\n## Current User Device Context\nThe current user is using the following device: ${displayDevice}\nYou need to be aware of the user's current device and provide guidance accordingly. If the response involves device-related tools or actions, you must tailor the reply based on the user's current device, using device-specific references such as "saved to the Notes/Calendar on your {deviceType}.\n"`;
139
195
  context.systemPrompt = (context.systemPrompt ?? "") + deviceSection;
140
196
  }
141
- const stream = await underlying(model, context, {
197
+ // ── Retry loop ─────────────────────────────────────────
198
+ for (let attempt = 0; attempt < MAX_RETRY_ATTEMPTS; attempt++) {
199
+ const stream = await underlying(model, context, {
200
+ ...options,
201
+ headers: {
202
+ ...options?.headers,
203
+ ...dynamicHeaders,
204
+ },
205
+ });
206
+ // Wait for the stream to settle (done or error) to inspect the result.
207
+ // stream.result() resolves to the final AssistantMessage (even on error).
208
+ const result = await stream.result();
209
+ // Check if this is a retryable error
210
+ if (result.stopReason === "error" && isRetryableProviderError(result.errorMessage)) {
211
+ const delayMs = getRetryDelayMs(attempt);
212
+ console.log(`[xiaoyiprovider] retryable error (attempt ${attempt + 1}/${MAX_RETRY_ATTEMPTS}): ` +
213
+ `${result.errorMessage} — retrying in ${delayMs}ms`);
214
+ await sleep(delayMs);
215
+ continue;
216
+ }
217
+ // Success or non-retryable error — log and return
218
+ if (result.stopReason === "error") {
219
+ console.log(`[xiaoyiprovider] non-retryable error: ${result.errorMessage}`);
220
+ }
221
+ else {
222
+ console.log(`[xiaoyiprovider] stream completed, usage: input=${result.usage?.input} output=${result.usage?.output}`);
223
+ }
224
+ // The original stream has already been consumed by result().
225
+ // Build a replay stream that delivers the final result.
226
+ return buildReplayStream(result);
227
+ }
228
+ // All retries exhausted — return the last attempt's real error via a new stream
229
+ console.log(`[xiaoyiprovider] all ${MAX_RETRY_ATTEMPTS} retries exhausted, surfacing last error`);
230
+ const lastStream = await underlying(model, context, {
142
231
  ...options,
143
232
  headers: {
144
233
  ...options?.headers,
145
234
  ...dynamicHeaders,
146
235
  },
147
236
  });
148
- // 异步监听输出(不阻塞 stream 返回)
149
- stream.result().then((result) => {
150
- console.log(`[xiaoyiprovider] stream completed, usage: input=${result.usage?.input} output=${result.usage?.output}`);
151
- }, (err) => console.log(`[xiaoyiprovider] stream error: ${JSON.stringify(err)}`));
152
- return stream;
237
+ const lastResult = await lastStream.result();
238
+ return buildReplayStream(lastResult);
153
239
  };
154
240
  },
155
241
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ynhcj/xiaoyi-channel",
3
- "version": "0.0.91-beta",
3
+ "version": "0.0.92-beta",
4
4
  "description": "OpenClaw Xiaoyi Channel plugin - Xiaoyi A2A protocol integration",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",