bunnyquery 1.3.0 → 1.3.4
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/bunnyquery.css +7 -0
- package/bunnyquery.js +108 -6
- package/dist/engine.cjs +65 -3
- package/dist/engine.cjs.map +1 -1
- package/dist/engine.d.mts +23 -1
- package/dist/engine.d.ts +23 -1
- package/dist/engine.mjs +64 -4
- package/dist/engine.mjs.map +1 -1
- package/package.json +1 -1
- package/src/engine/attachments.ts +0 -0
- package/src/engine/errors.ts +49 -0
- package/src/engine/index.ts +6 -1
- package/src/engine/session.ts +17 -4
- package/src/widget.css +7 -0
package/package.json
CHANGED
|
Binary file
|
package/src/engine/errors.ts
CHANGED
|
@@ -34,6 +34,55 @@ export function isErrorResponseBody(response: any): boolean {
|
|
|
34
34
|
return false;
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
+
// A request that failed because the REQUEST ITSELF is malformed — an unknown or
|
|
38
|
+
// rejected parameter on a 400/422 — will deterministically re-fail if resent
|
|
39
|
+
// unchanged; no session refresh or retry can fix it. Detecting it lets the retry
|
|
40
|
+
// gates refuse to auto-resend (otherwise a 400 whose param name merely contains
|
|
41
|
+
// "token", e.g. `max_output_tokens`, trips the `invalid_request + token` branch
|
|
42
|
+
// of isAuthExpiredError below and loops refresh->resend forever, burning tokens).
|
|
43
|
+
// This is the `_skapi_extract` 400 class. Distinct from auth-expiry, which IS
|
|
44
|
+
// retryable after a token refresh — so 401 (auth) and 429/5xx (transient) are
|
|
45
|
+
// intentionally NOT treated as non-retryable here.
|
|
46
|
+
export function isNonRetryableRequestError(input: any): boolean {
|
|
47
|
+
if (!input || typeof input !== 'object') return false;
|
|
48
|
+
|
|
49
|
+
var status = typeof input.status_code === 'number' ? input.status_code
|
|
50
|
+
: typeof input.status === 'number' ? input.status : undefined;
|
|
51
|
+
|
|
52
|
+
// Collect the error envelope wherever it lives (response, response.body, or a
|
|
53
|
+
// thrown error), mirroring isErrorResponseBody / isAuthExpiredError.
|
|
54
|
+
var param: any = undefined;
|
|
55
|
+
var blobs: string[] = [];
|
|
56
|
+
var sources = [input.error, input.body && input.body.error, input.body, input];
|
|
57
|
+
for (var i = 0; i < sources.length; i++) {
|
|
58
|
+
var e = sources[i];
|
|
59
|
+
if (!e) continue;
|
|
60
|
+
if (typeof e === 'string') { blobs.push(e); continue; }
|
|
61
|
+
if (typeof e !== 'object') continue;
|
|
62
|
+
if (param === undefined && e.param != null) param = e.param;
|
|
63
|
+
if (typeof e.code === 'string') blobs.push(e.code);
|
|
64
|
+
if (typeof e.type === 'string') blobs.push(e.type);
|
|
65
|
+
if (typeof e.message === 'string') blobs.push(e.message);
|
|
66
|
+
}
|
|
67
|
+
var hay = blobs.join(' | ').toLowerCase();
|
|
68
|
+
|
|
69
|
+
// Explicit "unknown / unsupported parameter" is always a malformed request.
|
|
70
|
+
if (hay.indexOf('unknown_parameter') !== -1 || hay.indexOf('unknown parameter') !== -1 ||
|
|
71
|
+
hay.indexOf('unsupported_parameter') !== -1 || hay.indexOf('unsupported parameter') !== -1) {
|
|
72
|
+
return true;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// A 400/422 invalid_request that points at a specific rejected field: resending
|
|
76
|
+
// the identical body re-fails. (401 -> isAuthExpiredError; 429/5xx -> transient.)
|
|
77
|
+
var isClientReqStatus = status === 400 || status === 422;
|
|
78
|
+
if (isClientReqStatus && param != null && param !== '') return true;
|
|
79
|
+
if (isClientReqStatus && hay.indexOf('invalid_request') !== -1 &&
|
|
80
|
+
(hay.indexOf('parameter') !== -1 || hay.indexOf('param') !== -1)) {
|
|
81
|
+
return true;
|
|
82
|
+
}
|
|
83
|
+
return false;
|
|
84
|
+
}
|
|
85
|
+
|
|
37
86
|
export function isAuthExpiredError(input: any): boolean {
|
|
38
87
|
if (!input) return false;
|
|
39
88
|
var blobs: string[] = [];
|
package/src/engine/index.ts
CHANGED
|
@@ -21,11 +21,16 @@ export {
|
|
|
21
21
|
type ComposedUserMessage,
|
|
22
22
|
} from './office';
|
|
23
23
|
|
|
24
|
+
export {
|
|
25
|
+
groupAttachmentFailures,
|
|
26
|
+
type AttachmentFailureGroup,
|
|
27
|
+
} from './attachments';
|
|
28
|
+
|
|
24
29
|
export * from './prompts';
|
|
25
30
|
|
|
26
31
|
// Pure helpers (Tier-1.5): error detection, token budgeting, link/path
|
|
27
32
|
// normalization, and history mapping — shared so both consumers stay identical.
|
|
28
|
-
export { getErrorMessage, isErrorResponseBody, isAuthExpiredError } from './errors';
|
|
33
|
+
export { getErrorMessage, isErrorResponseBody, isAuthExpiredError, isNonRetryableRequestError } from './errors';
|
|
29
34
|
export * from './budget';
|
|
30
35
|
export * from './links';
|
|
31
36
|
export {
|
package/src/engine/session.ts
CHANGED
|
@@ -30,7 +30,7 @@ import {
|
|
|
30
30
|
OPENAI_RESPONSES_API_URL,
|
|
31
31
|
type BgTaskEntry,
|
|
32
32
|
} from './requests';
|
|
33
|
-
import { isErrorResponseBody, isAuthExpiredError, getErrorMessage } from './errors';
|
|
33
|
+
import { isErrorResponseBody, isAuthExpiredError, isNonRetryableRequestError, getErrorMessage } from './errors';
|
|
34
34
|
import { buildBoundedChatMessages } from './budget';
|
|
35
35
|
import { createInlineLinkRegex } from './links';
|
|
36
36
|
import { mapHistoryListToMessages, extractLastUserTextFromRequest } from './history';
|
|
@@ -116,11 +116,15 @@ export class ChatSession {
|
|
|
116
116
|
};
|
|
117
117
|
var run = sendAndPoll()
|
|
118
118
|
.catch(function (err: any) {
|
|
119
|
-
|
|
119
|
+
// Only auth-expiry is worth a refresh+resend; a malformed-request 400
|
|
120
|
+
// (e.g. the `_skapi_extract`/unknown-parameter class) re-fails identically,
|
|
121
|
+
// so never loop on it — guard against isAuthExpiredError's heuristic
|
|
122
|
+
// misfiring on a param name that merely contains "token".
|
|
123
|
+
if (isAuthExpiredError(err) && !isNonRetryableRequestError(err)) return self.host.refreshSession().then(sendAndPoll);
|
|
120
124
|
throw err;
|
|
121
125
|
})
|
|
122
126
|
.then(function (response: any) {
|
|
123
|
-
if (isErrorResponseBody(response) && isAuthExpiredError(response)) {
|
|
127
|
+
if (isErrorResponseBody(response) && isAuthExpiredError(response) && !isNonRetryableRequestError(response)) {
|
|
124
128
|
return self.host.refreshSession().then(sendAndPoll);
|
|
125
129
|
}
|
|
126
130
|
return response;
|
|
@@ -656,7 +660,7 @@ export class ChatSession {
|
|
|
656
660
|
var fetchHistory = function () { return getChatHistory({ service: serviceId, owner: owner, platform: platform }, options); };
|
|
657
661
|
|
|
658
662
|
return Promise.resolve().then(fetchHistory).catch(function (err: any) {
|
|
659
|
-
if (isAuthExpiredError(err)) return self.host.refreshSession().then(fetchHistory);
|
|
663
|
+
if (isAuthExpiredError(err) && !isNonRetryableRequestError(err)) return self.host.refreshSession().then(fetchHistory);
|
|
660
664
|
throw err;
|
|
661
665
|
}).then(function (history: any) {
|
|
662
666
|
if (token !== self.state.gateRefreshToken) return;
|
|
@@ -783,6 +787,7 @@ export class ChatSession {
|
|
|
783
787
|
var self = this;
|
|
784
788
|
var id = this.host.getIdentity();
|
|
785
789
|
att.status = 'uploading'; att.progress = 0; att.errorMessage = '';
|
|
790
|
+
att.errorCode = ''; att.errorDetail = ''; // clear any prior failure (retry)
|
|
786
791
|
this.host.renderAttachmentChips();
|
|
787
792
|
var members = (att.kind === 'folder')
|
|
788
793
|
? (att.files || []).map(function (f: any) { return { file: f.file, relPath: f.path, storagePath: self.host.storagePathFor(f.path) }; })
|
|
@@ -851,6 +856,11 @@ export class ChatSession {
|
|
|
851
856
|
}, function (e: any) {
|
|
852
857
|
console.error('[chat-engine] indexing request failed', e);
|
|
853
858
|
anyIndexFailed = true; // uploaded but not indexed → yellow
|
|
859
|
+
// Record the first index error's code/message for the report dialog.
|
|
860
|
+
if (!att.errorCode && !att.errorDetail) {
|
|
861
|
+
att.errorCode = (e && (e.code || (e.body && e.body.code))) || '';
|
|
862
|
+
att.errorDetail = (e && (e.message || (e.body && e.body.message))) || (typeof e === 'string' ? e : '');
|
|
863
|
+
}
|
|
854
864
|
});
|
|
855
865
|
});
|
|
856
866
|
});
|
|
@@ -894,6 +904,9 @@ export class ChatSession {
|
|
|
894
904
|
if (removed || aborted) return;
|
|
895
905
|
att.status = 'error';
|
|
896
906
|
att.errorMessage = 'File upload has failed';
|
|
907
|
+
// Preserve the original error code/message for the report dialog.
|
|
908
|
+
att.errorCode = (err && (err.code || (err.body && err.body.code))) || '';
|
|
909
|
+
att.errorDetail = (err && (err.message || (err.body && err.body.message))) || (typeof err === 'string' ? err : '');
|
|
897
910
|
self.host.renderAttachmentChips();
|
|
898
911
|
});
|
|
899
912
|
});
|
package/src/widget.css
CHANGED
|
@@ -823,6 +823,13 @@
|
|
|
823
823
|
.bq-modal-desc { margin: 0 0 1.25rem; font-size: 0.85rem; color: var(--bq-muted); line-height: 1.6; }
|
|
824
824
|
.bq-modal-btns { display: flex; gap: 0.75rem; justify-content: flex-end; align-items: center; }
|
|
825
825
|
|
|
826
|
+
/* upload error report dialog (failed files grouped by error) */
|
|
827
|
+
.bq-upload-error-list { display: flex; flex-direction: column; gap: 0.85rem; margin: 0 0 1.25rem; max-height: 50vh; overflow-y: auto; }
|
|
828
|
+
.bq-upload-error-group { border-left: 3px solid var(--bq-danger); padding-left: 0.7rem; }
|
|
829
|
+
.bq-upload-error-heading { margin: 0 0 0.3rem; font-size: 0.82rem; font-weight: 600; color: var(--bq-danger); word-break: break-word; }
|
|
830
|
+
.bq-upload-error-files { margin: 0; padding-left: 1.1rem; font-size: 0.8rem; color: var(--bq-ink); }
|
|
831
|
+
.bq-upload-error-files li { margin: 0.1rem 0; word-break: break-word; }
|
|
832
|
+
|
|
826
833
|
/* delete/clear (danger) modal accents */
|
|
827
834
|
.bq-modal-delete-header {
|
|
828
835
|
display: flex;
|