@ynhcj/xiaoyi-channel 0.0.91-beta → 0.0.93-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,61 @@
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, with up to 10s jitter. */
27
+ function getRetryDelayMs(attempt) {
28
+ const base = attempt <= RETRY_DELAYS_MS.length
29
+ ? RETRY_DELAYS_MS[attempt - 1]
30
+ : RETRY_DELAYS_MS[RETRY_DELAYS_MS.length - 1];
31
+ const jitter = Math.floor(Math.random() * 10_000);
32
+ return base + jitter;
33
+ }
34
+ function sleep(ms) {
35
+ return new Promise((resolve) => setTimeout(resolve, ms));
36
+ }
37
+ /**
38
+ * Build a minimal EventStream-compatible object that replays a single
39
+ * done/error event. This avoids importing @mariozechner/pi-ai at runtime
40
+ * (the package is not available in the extension sandbox).
41
+ */
42
+ function buildReplayStream(result) {
43
+ let settled = false;
44
+ const queued = [
45
+ result.stopReason === "error"
46
+ ? { type: "error", reason: "error", error: result }
47
+ : { type: "done", reason: result.stopReason, message: result },
48
+ ];
49
+ return {
50
+ result: () => Promise.resolve(result),
51
+ push: () => { },
52
+ end: () => { },
53
+ [Symbol.asyncIterator]: () => {
54
+ return {
55
+ next: async () => {
56
+ if (settled || queued.length === 0) {
57
+ settled = true;
58
+ return { value: undefined, done: true };
59
+ }
60
+ settled = true;
61
+ return { value: queued.shift(), done: false };
62
+ },
63
+ };
64
+ },
65
+ };
66
+ }
12
67
  /**
13
68
  * Dynamic header keys injected via extraParams and forwarded to the HTTP request.
14
69
  * Correspond to the three fields written to .xiaoyiruntime:
@@ -73,10 +128,12 @@ export const xiaoyiProvider = {
73
128
  },
74
129
  /**
75
130
  * Wrap the stream function to inject dynamic headers into every
76
- * HTTP request to the model provider.
131
+ * HTTP request to the model provider, and retry on retryable errors
132
+ * (server_error / rate_limit_error) with backoff: 10s, 20s, 40s, 60s (cap).
77
133
  *
78
- * Reads the values injected by prepareExtraParams and adds them
79
- * as HTTP headers on the outgoing request.
134
+ * The retry loop awaits stream.result() to detect errors before deciding
135
+ * whether to retry. This keeps the agent loop waiting (no timeout risk
136
+ * since the default agent timeout is 48 hours).
80
137
  */
81
138
  wrapStreamFn: (ctx) => {
82
139
  const underlying = ctx.streamFn;
@@ -135,21 +192,51 @@ export const xiaoyiProvider = {
135
192
  if (sessionCtx?.deviceType) {
136
193
  const rawDevice = sessionCtx.deviceType;
137
194
  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”`;
195
+ 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
196
  context.systemPrompt = (context.systemPrompt ?? "") + deviceSection;
140
197
  }
141
- const stream = await underlying(model, context, {
198
+ // ── Retry loop ─────────────────────────────────────────
199
+ for (let attempt = 0; attempt < MAX_RETRY_ATTEMPTS; attempt++) {
200
+ const stream = await underlying(model, context, {
201
+ ...options,
202
+ headers: {
203
+ ...options?.headers,
204
+ ...dynamicHeaders,
205
+ },
206
+ });
207
+ // Wait for the stream to settle (done or error) to inspect the result.
208
+ // stream.result() resolves to the final AssistantMessage (even on error).
209
+ const result = await stream.result();
210
+ // Check if this is a retryable error
211
+ if (result.stopReason === "error" && isRetryableProviderError(result.errorMessage)) {
212
+ const delayMs = getRetryDelayMs(attempt);
213
+ console.log(`[xiaoyiprovider] retryable error (attempt ${attempt + 1}/${MAX_RETRY_ATTEMPTS}): ` +
214
+ `${result.errorMessage} — retrying in ${delayMs}ms`);
215
+ await sleep(delayMs);
216
+ continue;
217
+ }
218
+ // Success or non-retryable error — log and return
219
+ if (result.stopReason === "error") {
220
+ console.log(`[xiaoyiprovider] non-retryable error: ${result.errorMessage}`);
221
+ }
222
+ else {
223
+ console.log(`[xiaoyiprovider] stream completed, usage: input=${result.usage?.input} output=${result.usage?.output}`);
224
+ }
225
+ // The original stream has already been consumed by result().
226
+ // Build a replay stream that delivers the final result.
227
+ return buildReplayStream(result);
228
+ }
229
+ // All retries exhausted — return the last attempt's real error via a new stream
230
+ console.log(`[xiaoyiprovider] all ${MAX_RETRY_ATTEMPTS} retries exhausted, surfacing last error`);
231
+ const lastStream = await underlying(model, context, {
142
232
  ...options,
143
233
  headers: {
144
234
  ...options?.headers,
145
235
  ...dynamicHeaders,
146
236
  },
147
237
  });
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;
238
+ const lastResult = await lastStream.result();
239
+ return buildReplayStream(lastResult);
153
240
  };
154
241
  },
155
242
  };
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.93-beta",
4
4
  "description": "OpenClaw Xiaoyi Channel plugin - Xiaoyi A2A protocol integration",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",