@ryantest/openclaw-qqbot 1.6.6-alpha.0 → 1.6.6-alpha.1
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/src/api.js +23 -2
- package/dist/src/streaming.d.ts +6 -0
- package/dist/src/streaming.js +16 -9
- package/package.json +2 -2
- package/src/api.ts +33 -2
- package/src/streaming.ts +15 -9
package/dist/src/api.js
CHANGED
|
@@ -332,6 +332,27 @@ async function completeUploadWithRetry(accessToken, method, path, body) {
|
|
|
332
332
|
}
|
|
333
333
|
throw lastError;
|
|
334
334
|
}
|
|
335
|
+
// ============ 分片完成重试(无条件,与 completeUpload 策略一致) ============
|
|
336
|
+
const PART_FINISH_MAX_RETRIES = 2;
|
|
337
|
+
const PART_FINISH_BASE_DELAY_MS = 1000;
|
|
338
|
+
async function partFinishWithRetry(accessToken, method, path, body) {
|
|
339
|
+
let lastError = null;
|
|
340
|
+
for (let attempt = 0; attempt <= PART_FINISH_MAX_RETRIES; attempt++) {
|
|
341
|
+
try {
|
|
342
|
+
await apiRequest(accessToken, method, path, body);
|
|
343
|
+
return;
|
|
344
|
+
}
|
|
345
|
+
catch (err) {
|
|
346
|
+
lastError = err instanceof Error ? err : new Error(String(err));
|
|
347
|
+
if (attempt < PART_FINISH_MAX_RETRIES) {
|
|
348
|
+
const delay = PART_FINISH_BASE_DELAY_MS * Math.pow(2, attempt);
|
|
349
|
+
console.warn(`[qqbot-api] PartFinish attempt ${attempt + 1} failed, retrying in ${delay}ms: ${lastError.message.slice(0, 200)}`);
|
|
350
|
+
await new Promise(resolve => setTimeout(resolve, delay));
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
throw lastError;
|
|
355
|
+
}
|
|
335
356
|
export async function getGatewayUrl(accessToken) {
|
|
336
357
|
const data = await apiRequest(accessToken, "GET", "/gateway");
|
|
337
358
|
return data.url;
|
|
@@ -467,7 +488,7 @@ export async function c2cUploadPrepare(accessToken, userId, fileType, fileName,
|
|
|
467
488
|
* @param md5 - 分片数据的 MD5(十六进制)
|
|
468
489
|
*/
|
|
469
490
|
export async function c2cUploadPartFinish(accessToken, userId, uploadId, partIndex, blockSize, md5) {
|
|
470
|
-
await
|
|
491
|
+
await partFinishWithRetry(accessToken, "POST", `/v2/users/${userId}/upload_part_finish`, { upload_id: uploadId, part_index: partIndex, block_size: blockSize, md5 });
|
|
471
492
|
}
|
|
472
493
|
/**
|
|
473
494
|
* 完成文件上传(C2C)
|
|
@@ -493,7 +514,7 @@ export async function groupUploadPrepare(accessToken, groupId, fileType, fileNam
|
|
|
493
514
|
* POST /v2/groups/{group_id}/upload_part_finish
|
|
494
515
|
*/
|
|
495
516
|
export async function groupUploadPartFinish(accessToken, groupId, uploadId, partIndex, blockSize, md5) {
|
|
496
|
-
await
|
|
517
|
+
await partFinishWithRetry(accessToken, "POST", `/v2/groups/${groupId}/upload_part_finish`, { upload_id: uploadId, part_index: partIndex, block_size: blockSize, md5 });
|
|
497
518
|
}
|
|
498
519
|
/**
|
|
499
520
|
* 完成文件上传(Group)
|
package/dist/src/streaming.d.ts
CHANGED
|
@@ -73,6 +73,12 @@ export declare class StreamingController {
|
|
|
73
73
|
* - onIdle 校验时用于前缀匹配
|
|
74
74
|
*/
|
|
75
75
|
private lastNormalizedFull;
|
|
76
|
+
/**
|
|
77
|
+
* 最后一次收到的完整原始文本(未经 normalize)。
|
|
78
|
+
* 仅用于回复边界检测——原始文本在 partial reply 过程中是稳定递增的,
|
|
79
|
+
* 不会因为 normalizeMediaTags 对未闭合标签的处理差异导致前缀不匹配。
|
|
80
|
+
*/
|
|
81
|
+
private lastRawFull;
|
|
76
82
|
/**
|
|
77
83
|
* 在 lastNormalizedFull 中已经"消费"到的位置。
|
|
78
84
|
* "消费"包括:已通过流式发送并终结的文本段、已处理的媒体标签。
|
package/dist/src/streaming.js
CHANGED
|
@@ -179,7 +179,7 @@ class FlushController {
|
|
|
179
179
|
export class StreamingController {
|
|
180
180
|
// ---- 状态机 ----
|
|
181
181
|
phase = "idle";
|
|
182
|
-
// ----
|
|
182
|
+
// ---- 核心文本状态 ----
|
|
183
183
|
/**
|
|
184
184
|
* 最后一次收到的完整 normalized 全量文本。
|
|
185
185
|
* - onPartialReply 每次更新(回复边界时会拼接前缀)
|
|
@@ -187,6 +187,12 @@ export class StreamingController {
|
|
|
187
187
|
* - onIdle 校验时用于前缀匹配
|
|
188
188
|
*/
|
|
189
189
|
lastNormalizedFull = "";
|
|
190
|
+
/**
|
|
191
|
+
* 最后一次收到的完整原始文本(未经 normalize)。
|
|
192
|
+
* 仅用于回复边界检测——原始文本在 partial reply 过程中是稳定递增的,
|
|
193
|
+
* 不会因为 normalizeMediaTags 对未闭合标签的处理差异导致前缀不匹配。
|
|
194
|
+
*/
|
|
195
|
+
lastRawFull = "";
|
|
190
196
|
/**
|
|
191
197
|
* 在 lastNormalizedFull 中已经"消费"到的位置。
|
|
192
198
|
* "消费"包括:已通过流式发送并终结的文本段、已处理的媒体标签。
|
|
@@ -352,12 +358,11 @@ export class StreamingController {
|
|
|
352
358
|
this.logDebug(`onPartialReply: skipped (empty text)`);
|
|
353
359
|
return;
|
|
354
360
|
}
|
|
355
|
-
// ★
|
|
356
|
-
//
|
|
357
|
-
//
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
this.logInfo(`onPartialReply: reply boundary detected — prefix mismatch (new len=${normalized.length}, prev len=${this.lastNormalizedFull.length}), finalizing current controller`);
|
|
361
|
+
// ★ 回复边界检测:用原始文本做前缀比较,避免 normalizeMediaTags 对未闭合标签
|
|
362
|
+
// 的不稳定处理导致误判(normalize 后的文本在 partial reply 的不同阶段可能产生
|
|
363
|
+
// 完全不同的结果,从而使 startsWith 始终失败,导致 boundary 被反复触发)
|
|
364
|
+
if (this.lastRawFull && text.length > 0 && !text.startsWith(this.lastRawFull)) {
|
|
365
|
+
this.logInfo(`onPartialReply: reply boundary detected — raw prefix mismatch (new len=${text.length}, prev len=${this.lastRawFull.length}), finalizing current controller`);
|
|
361
366
|
// 终结当前流式会话,处理完当前内容(包括可能的未闭合媒体标签)
|
|
362
367
|
this.dispatchFullyComplete = true;
|
|
363
368
|
await this.finalizeOnIdle();
|
|
@@ -371,8 +376,10 @@ export class StreamingController {
|
|
|
371
376
|
}
|
|
372
377
|
return;
|
|
373
378
|
}
|
|
374
|
-
//
|
|
375
|
-
this.
|
|
379
|
+
// 正常增长:更新原始文本和 normalize 后的文本
|
|
380
|
+
this.lastRawFull = text;
|
|
381
|
+
this.lastNormalizedFull = normalizeMediaTags(text);
|
|
382
|
+
;
|
|
376
383
|
// ★ 核心:从 sentIndex 开始,处理增量文本(串行队列保证不会并发进入)
|
|
377
384
|
await this.processMediaTags(this.lastNormalizedFull);
|
|
378
385
|
}
|
package/package.json
CHANGED
package/src/api.ts
CHANGED
|
@@ -413,6 +413,37 @@ async function completeUploadWithRetry(
|
|
|
413
413
|
throw lastError!;
|
|
414
414
|
}
|
|
415
415
|
|
|
416
|
+
// ============ 分片完成重试(无条件,与 completeUpload 策略一致) ============
|
|
417
|
+
|
|
418
|
+
const PART_FINISH_MAX_RETRIES = 2;
|
|
419
|
+
const PART_FINISH_BASE_DELAY_MS = 1000;
|
|
420
|
+
|
|
421
|
+
async function partFinishWithRetry(
|
|
422
|
+
accessToken: string,
|
|
423
|
+
method: string,
|
|
424
|
+
path: string,
|
|
425
|
+
body?: unknown,
|
|
426
|
+
): Promise<void> {
|
|
427
|
+
let lastError: Error | null = null;
|
|
428
|
+
|
|
429
|
+
for (let attempt = 0; attempt <= PART_FINISH_MAX_RETRIES; attempt++) {
|
|
430
|
+
try {
|
|
431
|
+
await apiRequest<Record<string, unknown>>(accessToken, method, path, body);
|
|
432
|
+
return;
|
|
433
|
+
} catch (err) {
|
|
434
|
+
lastError = err instanceof Error ? err : new Error(String(err));
|
|
435
|
+
|
|
436
|
+
if (attempt < PART_FINISH_MAX_RETRIES) {
|
|
437
|
+
const delay = PART_FINISH_BASE_DELAY_MS * Math.pow(2, attempt);
|
|
438
|
+
console.warn(`[qqbot-api] PartFinish attempt ${attempt + 1} failed, retrying in ${delay}ms: ${lastError.message.slice(0, 200)}`);
|
|
439
|
+
await new Promise(resolve => setTimeout(resolve, delay));
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
throw lastError!;
|
|
445
|
+
}
|
|
446
|
+
|
|
416
447
|
export async function getGatewayUrl(accessToken: string): Promise<string> {
|
|
417
448
|
const data = await apiRequest<{ url: string }>(accessToken, "GET", "/gateway");
|
|
418
449
|
return data.url;
|
|
@@ -680,7 +711,7 @@ export async function c2cUploadPartFinish(
|
|
|
680
711
|
blockSize: number,
|
|
681
712
|
md5: string,
|
|
682
713
|
): Promise<void> {
|
|
683
|
-
await
|
|
714
|
+
await partFinishWithRetry(
|
|
684
715
|
accessToken, "POST", `/v2/users/${userId}/upload_part_finish`,
|
|
685
716
|
{ upload_id: uploadId, part_index: partIndex, block_size: blockSize, md5 },
|
|
686
717
|
);
|
|
@@ -736,7 +767,7 @@ export async function groupUploadPartFinish(
|
|
|
736
767
|
blockSize: number,
|
|
737
768
|
md5: string,
|
|
738
769
|
): Promise<void> {
|
|
739
|
-
await
|
|
770
|
+
await partFinishWithRetry(
|
|
740
771
|
accessToken, "POST", `/v2/groups/${groupId}/upload_part_finish`,
|
|
741
772
|
{ upload_id: uploadId, part_index: partIndex, block_size: blockSize, md5 },
|
|
742
773
|
);
|
package/src/streaming.ts
CHANGED
|
@@ -245,7 +245,7 @@ export class StreamingController {
|
|
|
245
245
|
// ---- 状态机 ----
|
|
246
246
|
private phase: StreamingPhase = "idle";
|
|
247
247
|
|
|
248
|
-
// ----
|
|
248
|
+
// ---- 核心文本状态 ----
|
|
249
249
|
/**
|
|
250
250
|
* 最后一次收到的完整 normalized 全量文本。
|
|
251
251
|
* - onPartialReply 每次更新(回复边界时会拼接前缀)
|
|
@@ -253,6 +253,12 @@ export class StreamingController {
|
|
|
253
253
|
* - onIdle 校验时用于前缀匹配
|
|
254
254
|
*/
|
|
255
255
|
private lastNormalizedFull = "";
|
|
256
|
+
/**
|
|
257
|
+
* 最后一次收到的完整原始文本(未经 normalize)。
|
|
258
|
+
* 仅用于回复边界检测——原始文本在 partial reply 过程中是稳定递增的,
|
|
259
|
+
* 不会因为 normalizeMediaTags 对未闭合标签的处理差异导致前缀不匹配。
|
|
260
|
+
*/
|
|
261
|
+
private lastRawFull = "";
|
|
256
262
|
/**
|
|
257
263
|
* 在 lastNormalizedFull 中已经"消费"到的位置。
|
|
258
264
|
* "消费"包括:已通过流式发送并终结的文本段、已处理的媒体标签。
|
|
@@ -444,12 +450,11 @@ export class StreamingController {
|
|
|
444
450
|
return;
|
|
445
451
|
}
|
|
446
452
|
|
|
447
|
-
// ★
|
|
448
|
-
//
|
|
449
|
-
//
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
this.logInfo(`onPartialReply: reply boundary detected — prefix mismatch (new len=${normalized.length}, prev len=${this.lastNormalizedFull.length}), finalizing current controller`);
|
|
453
|
+
// ★ 回复边界检测:用原始文本做前缀比较,避免 normalizeMediaTags 对未闭合标签
|
|
454
|
+
// 的不稳定处理导致误判(normalize 后的文本在 partial reply 的不同阶段可能产生
|
|
455
|
+
// 完全不同的结果,从而使 startsWith 始终失败,导致 boundary 被反复触发)
|
|
456
|
+
if (this.lastRawFull && text.length > 0 && !text.startsWith(this.lastRawFull)) {
|
|
457
|
+
this.logInfo(`onPartialReply: reply boundary detected — raw prefix mismatch (new len=${text.length}, prev len=${this.lastRawFull.length}), finalizing current controller`);
|
|
453
458
|
|
|
454
459
|
// 终结当前流式会话,处理完当前内容(包括可能的未闭合媒体标签)
|
|
455
460
|
this.dispatchFullyComplete = true;
|
|
@@ -465,8 +470,9 @@ export class StreamingController {
|
|
|
465
470
|
return;
|
|
466
471
|
}
|
|
467
472
|
|
|
468
|
-
//
|
|
469
|
-
this.
|
|
473
|
+
// 正常增长:更新原始文本和 normalize 后的文本
|
|
474
|
+
this.lastRawFull = text;
|
|
475
|
+
this.lastNormalizedFull = normalizeMediaTags(text);;
|
|
470
476
|
|
|
471
477
|
// ★ 核心:从 sentIndex 开始,处理增量文本(串行队列保证不会并发进入)
|
|
472
478
|
await this.processMediaTags(this.lastNormalizedFull);
|