@h-ai/feedback 0.1.0-alpha.70 → 0.1.0-alpha.72
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/README.en.md +27 -8
- package/README.ja.md +27 -8
- package/README.md +27 -8
- package/dist/bundle/index.d.ts +2 -0
- package/dist/bundle/index.js +1 -1
- package/dist/capacitor.d.ts +18 -0
- package/dist/capacitor.js +35 -0
- package/dist/capacitor.js.map +1 -0
- package/dist/{chunk-43AOXE4W.js → chunk-5QWKURWU.js} +11 -6
- package/dist/chunk-5QWKURWU.js.map +1 -0
- package/dist/{chunk-DYEK2FNM.js → chunk-PYKGS35A.js} +6 -6
- package/dist/chunk-PYKGS35A.js.map +1 -0
- package/dist/client/index.d.ts +88 -6
- package/dist/client/index.js +385 -71
- package/dist/client/index.js.map +1 -1
- package/dist/feedback-client-files-B88nCOo_.d.ts +54 -0
- package/dist/index.d.ts +10 -3
- package/dist/index.js +79 -34
- package/dist/index.js.map +1 -1
- package/dist/tauri.d.ts +18 -0
- package/dist/tauri.js +15 -0
- package/dist/tauri.js.map +1 -0
- package/package.json +42 -6
- package/dist/chunk-43AOXE4W.js.map +0 -1
- package/dist/chunk-DYEK2FNM.js.map +0 -1
package/dist/client/index.js
CHANGED
|
@@ -1,9 +1,50 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
1
|
+
import { feedbackM, HaiFeedbackError, redactFeedbackText } from '../chunk-PYKGS35A.js';
|
|
2
|
+
import { ok, core, err } from '@h-ai/core';
|
|
3
|
+
import { resolveFeedbackBudget, FeedbackSubmitInputSchema, FeedbackBudgetSchema, FeedbackEvidenceInputSchema, FeedbackReportSchema } from '@h-ai/api-contract/feedback';
|
|
4
|
+
|
|
5
|
+
var nativeSaver;
|
|
6
|
+
function configureFeedbackSaving(saver) {
|
|
7
|
+
nativeSaver = saver;
|
|
8
|
+
}
|
|
9
|
+
function feedbackDataBytes(data) {
|
|
10
|
+
return Uint8Array.from(atob(data.dataBase64), (value) => value.charCodeAt(0));
|
|
11
|
+
}
|
|
12
|
+
async function saveFeedbackFile(bytes, filename, contentType = "application/zip", signal) {
|
|
13
|
+
const name = [...filename].map((character) => character.charCodeAt(0) < 32 || /[\\/:*?"<>|]/.test(character) ? "_" : character).join("").slice(0, 200) || "feedback.zip";
|
|
14
|
+
try {
|
|
15
|
+
if (signal?.aborted)
|
|
16
|
+
return ok({ status: "cancelled" });
|
|
17
|
+
if (typeof window === "undefined")
|
|
18
|
+
throw new Error("Browser required");
|
|
19
|
+
const capacitor = Reflect.get(window, "Capacitor");
|
|
20
|
+
const isNative = Reflect.get(window, "__TAURI_INTERNALS__") || capacitor && typeof capacitor === "object" && "isNativePlatform" in capacitor && typeof capacitor.isNativePlatform === "function" && capacitor.isNativePlatform();
|
|
21
|
+
if (isNative) {
|
|
22
|
+
if (!nativeSaver)
|
|
23
|
+
throw new Error("Feedback platform saving adapter is not configured");
|
|
24
|
+
return ok(await nativeSaver(bytes, name, contentType, signal));
|
|
25
|
+
}
|
|
26
|
+
const url = URL.createObjectURL(new Blob([new Uint8Array(bytes).buffer], { type: contentType }));
|
|
27
|
+
const link = document.createElement("a");
|
|
28
|
+
try {
|
|
29
|
+
link.href = url;
|
|
30
|
+
link.download = name;
|
|
31
|
+
document.body.append(link);
|
|
32
|
+
link.click();
|
|
33
|
+
} finally {
|
|
34
|
+
link.remove();
|
|
35
|
+
setTimeout(() => URL.revokeObjectURL(url), 6e4);
|
|
36
|
+
}
|
|
37
|
+
return ok({ status: "saved" });
|
|
38
|
+
} catch (cause) {
|
|
39
|
+
const error = core.error.toPublic({ ...HaiFeedbackError.OPERATION_FAILED, message: feedbackM("feedback_operationFailed") });
|
|
40
|
+
core.logger.error("Feedback file save failed", { traceId: error.traceId, error: cause });
|
|
41
|
+
return err(error);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
4
44
|
|
|
5
45
|
// src/client/feedback-client-image.ts
|
|
6
|
-
var PRIVATE_SELECTOR = 'input
|
|
46
|
+
var PRIVATE_SELECTOR = 'input[type="password"],[data-feedback-private],.rr-block,.rr-mask';
|
|
47
|
+
var CONTROL_SELECTOR = 'input,textarea,select,[contenteditable]:not([contenteditable="false"])';
|
|
7
48
|
async function captureFeedbackScreenshot() {
|
|
8
49
|
if (document.fonts) {
|
|
9
50
|
let timeout;
|
|
@@ -15,13 +56,15 @@ async function captureFeedbackScreenshot() {
|
|
|
15
56
|
}
|
|
16
57
|
const { default: html2canvas } = await import('html2canvas');
|
|
17
58
|
const canvas = await html2canvas(document.body, {
|
|
18
|
-
width:
|
|
19
|
-
height:
|
|
20
|
-
scale: 1,
|
|
59
|
+
width: window.innerWidth,
|
|
60
|
+
height: window.innerHeight,
|
|
61
|
+
scale: Math.min(1, 1920 / window.innerWidth, 1200 / window.innerHeight),
|
|
21
62
|
// 原生 SVG foreignObject 保留 CSS mask 图标与字体基线,避免 CanvasRenderer 裁切文本。
|
|
22
63
|
foreignObjectRendering: true,
|
|
23
|
-
|
|
24
|
-
|
|
64
|
+
x: window.scrollX,
|
|
65
|
+
y: window.scrollY,
|
|
66
|
+
scrollX: window.scrollX,
|
|
67
|
+
scrollY: window.scrollY,
|
|
25
68
|
useCORS: false,
|
|
26
69
|
allowTaint: false,
|
|
27
70
|
logging: false,
|
|
@@ -53,10 +96,28 @@ async function captureFeedbackScreenshot() {
|
|
|
53
96
|
}
|
|
54
97
|
}
|
|
55
98
|
}
|
|
56
|
-
for (const element of clone.querySelectorAll(PRIVATE_SELECTOR)) {
|
|
99
|
+
for (const element of clone.querySelectorAll(`${PRIVATE_SELECTOR},${CONTROL_SELECTOR}`)) {
|
|
100
|
+
if (!clone.documentElement.contains(element))
|
|
101
|
+
continue;
|
|
57
102
|
const bounds = element.getBoundingClientRect();
|
|
58
103
|
const replacement = clone.createElement("div");
|
|
59
|
-
|
|
104
|
+
const computed = clone.defaultView?.getComputedStyle(element);
|
|
105
|
+
if (computed) {
|
|
106
|
+
for (const property of Array.from(computed))
|
|
107
|
+
replacement.style.setProperty(property, computed.getPropertyValue(property), "important");
|
|
108
|
+
}
|
|
109
|
+
replacement.style.setProperty("width", `${bounds.width}px`, "important");
|
|
110
|
+
replacement.style.setProperty("height", `${bounds.height}px`, "important");
|
|
111
|
+
replacement.style.setProperty("box-sizing", "border-box", "important");
|
|
112
|
+
replacement.style.setProperty("overflow", "hidden", "important");
|
|
113
|
+
if (element.matches(PRIVATE_SELECTOR)) {
|
|
114
|
+
replacement.style.setProperty("background", "#111", "important");
|
|
115
|
+
replacement.style.setProperty("color", "transparent", "important");
|
|
116
|
+
} else {
|
|
117
|
+
const control = element;
|
|
118
|
+
const value = "value" in control ? control.value : element.textContent;
|
|
119
|
+
replacement.textContent = value ? "\u2022\u2022\u2022\u2022" : element.getAttribute("placeholder") ?? "";
|
|
120
|
+
}
|
|
60
121
|
replacement.setAttribute("aria-hidden", "true");
|
|
61
122
|
element.replaceWith(replacement);
|
|
62
123
|
}
|
|
@@ -78,6 +139,11 @@ async function annotateFeedbackImage(bytes, annotation) {
|
|
|
78
139
|
try {
|
|
79
140
|
image.src = imageUrl;
|
|
80
141
|
await image.decode();
|
|
142
|
+
if (annotation.coordinateSpace === "ratio") {
|
|
143
|
+
if ([annotation.x, annotation.y, annotation.width, annotation.height].some((value) => value < 0 || value > 1))
|
|
144
|
+
throw new Error("Invalid annotation ratio");
|
|
145
|
+
annotation = { ...annotation, x: annotation.x * image.naturalWidth, y: annotation.y * image.naturalHeight, width: annotation.width * image.naturalWidth, height: annotation.height * image.naturalHeight };
|
|
146
|
+
}
|
|
81
147
|
canvas.width = image.naturalWidth;
|
|
82
148
|
canvas.height = image.naturalHeight;
|
|
83
149
|
const context = canvas.getContext("2d");
|
|
@@ -86,7 +152,9 @@ async function annotateFeedbackImage(bytes, annotation) {
|
|
|
86
152
|
context.drawImage(image, 0, 0);
|
|
87
153
|
if (annotation.mode === "mask") {
|
|
88
154
|
context.fillStyle = "#111";
|
|
89
|
-
|
|
155
|
+
const left = Math.floor(annotation.x);
|
|
156
|
+
const top = Math.floor(annotation.y);
|
|
157
|
+
context.fillRect(left, top, Math.ceil(annotation.x + annotation.width) - left, Math.ceil(annotation.y + annotation.height) - top);
|
|
90
158
|
} else {
|
|
91
159
|
context.strokeStyle = "#e11d48";
|
|
92
160
|
context.lineWidth = 3;
|
|
@@ -153,14 +221,14 @@ function sanitizeFeedbackReplay(value) {
|
|
|
153
221
|
if (key === "attributes" && item && typeof item === "object") {
|
|
154
222
|
const attributes = {};
|
|
155
223
|
for (const [attribute, content] of Object.entries(item)) {
|
|
156
|
-
if (/^on|^(?:srcdoc|action|formaction|href|srcset)$/i.test(attribute) || CREDENTIAL_NAME.test(attribute))
|
|
224
|
+
if (/^on|^(?:srcdoc|action|formaction|href|srcset|rr_src)$/i.test(attribute) || CREDENTIAL_NAME.test(attribute))
|
|
157
225
|
continue;
|
|
158
226
|
if (attribute === "src" && typeof content === "string" && !/^data:image\/(?:png|jpeg|webp|gif);base64,/i.test(content)) {
|
|
159
227
|
attributes["data-feedback-missing-resource"] = true;
|
|
160
228
|
continue;
|
|
161
229
|
}
|
|
162
|
-
if (attribute === "style" && typeof content === "string"
|
|
163
|
-
attributes
|
|
230
|
+
if ((attribute === "style" || attribute === "_cssText") && typeof content === "string") {
|
|
231
|
+
attributes[attribute] = sanitizeFeedbackText(content).replace(/(?:url|@import)\s*\([^)]*\)|@import\s+["'][^"']+["']/gi, "");
|
|
164
232
|
attributes["data-feedback-missing-resource"] = true;
|
|
165
233
|
} else {
|
|
166
234
|
attributes[attribute] = visit(content, depth + 1);
|
|
@@ -246,10 +314,65 @@ async function restoreFeedbackCheckpoint(key) {
|
|
|
246
314
|
}
|
|
247
315
|
|
|
248
316
|
// src/client/feedback-client-main.ts
|
|
317
|
+
function feedbackFileContentType(file) {
|
|
318
|
+
const type = file.type.split(";", 1)[0]?.trim().toLowerCase() ?? "";
|
|
319
|
+
const extensions = /* @__PURE__ */ new Map([
|
|
320
|
+
["png", "image/png"],
|
|
321
|
+
["jpg", "image/jpeg"],
|
|
322
|
+
["jpeg", "image/jpeg"],
|
|
323
|
+
["webp", "image/webp"],
|
|
324
|
+
["webm", "video/webm"],
|
|
325
|
+
["mp4", "video/mp4"],
|
|
326
|
+
["txt", "text/plain"],
|
|
327
|
+
["log", "text/plain"],
|
|
328
|
+
["json", "application/json"],
|
|
329
|
+
["pdf", "application/pdf"]
|
|
330
|
+
]);
|
|
331
|
+
if (!type || type === "application/octet-stream")
|
|
332
|
+
return extensions.get(file.name.split(".").at(-1)?.toLowerCase() ?? "");
|
|
333
|
+
const aliases = /* @__PURE__ */ new Map([
|
|
334
|
+
["image/x-png", "image/png"],
|
|
335
|
+
["image/jpg", "image/jpeg"],
|
|
336
|
+
["image/pjpeg", "image/jpeg"],
|
|
337
|
+
["text/json", "application/json"],
|
|
338
|
+
["text/x-log", "text/plain"],
|
|
339
|
+
["application/x-pdf", "application/pdf"]
|
|
340
|
+
]);
|
|
341
|
+
const normalized = aliases.get(type) ?? type;
|
|
342
|
+
return [...extensions.values()].includes(normalized) ? normalized : void 0;
|
|
343
|
+
}
|
|
249
344
|
function createFeedbackClient() {
|
|
250
345
|
let config;
|
|
251
|
-
let initializing = false;
|
|
252
346
|
let draft;
|
|
347
|
+
let sessionVersion = 0;
|
|
348
|
+
let policyVerified = false;
|
|
349
|
+
let preparedPage = "";
|
|
350
|
+
let budget = resolveFeedbackBudget();
|
|
351
|
+
const previews = /* @__PURE__ */ new Map();
|
|
352
|
+
let discardOnClose = false;
|
|
353
|
+
let saveController = new AbortController();
|
|
354
|
+
function releasePreviews() {
|
|
355
|
+
for (const preview of previews.values()) URL.revokeObjectURL(preview.url);
|
|
356
|
+
previews.clear();
|
|
357
|
+
}
|
|
358
|
+
function getView() {
|
|
359
|
+
const files = draft?.attachments ?? [];
|
|
360
|
+
for (const [id, preview] of previews) {
|
|
361
|
+
if (!files.some((file) => file.id === id && file.checksum === preview.checksum)) {
|
|
362
|
+
URL.revokeObjectURL(preview.url);
|
|
363
|
+
previews.delete(id);
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
return { attachments: files.map((file) => {
|
|
367
|
+
let preview = previews.get(file.id);
|
|
368
|
+
if (!preview) {
|
|
369
|
+
preview = { checksum: file.checksum, url: URL.createObjectURL(new Blob([new Uint8Array(file.bytes).buffer], { type: file.contentType })) };
|
|
370
|
+
previews.set(file.id, preview);
|
|
371
|
+
}
|
|
372
|
+
return { id: file.id, name: file.name, size: file.size, mimeType: file.contentType, url: preview.url };
|
|
373
|
+
}), evidence: draft?.evidence ?? [], missing: draft?.missing ?? [], budget: { ...budget } };
|
|
374
|
+
}
|
|
375
|
+
let initializing = false;
|
|
253
376
|
let sequence = 0;
|
|
254
377
|
let buffer = [];
|
|
255
378
|
let bufferBytes = 0;
|
|
@@ -265,6 +388,7 @@ function createFeedbackClient() {
|
|
|
265
388
|
let persistTimer;
|
|
266
389
|
let flushTimer;
|
|
267
390
|
let stopReplay;
|
|
391
|
+
let screenshotCaptures = 0;
|
|
268
392
|
let unsubscribeLogger;
|
|
269
393
|
let source = "";
|
|
270
394
|
let keyScreenshots = [];
|
|
@@ -279,12 +403,15 @@ function createFeedbackClient() {
|
|
|
279
403
|
const cleanups = [];
|
|
280
404
|
const missing = /* @__PURE__ */ new Set();
|
|
281
405
|
let replayChunks = [];
|
|
282
|
-
function failure(code = "OPERATION_FAILED", cause) {
|
|
406
|
+
function failure(code = "OPERATION_FAILED", cause, message) {
|
|
283
407
|
const key = code === "NOT_INITIALIZED" ? "feedback_notInitialized" : code === "LIMIT_EXCEEDED" ? "feedback_limitExceeded" : code === "INVALID_INPUT" ? "feedback_invalidInput" : "feedback_operationFailed";
|
|
284
|
-
|
|
408
|
+
const traceId = core.context.get().traceId ?? core.context.get().requestId ?? core.id.uuid();
|
|
409
|
+
const error = { ...HaiFeedbackError[code], traceId, message: message ?? feedbackM(key), cause };
|
|
410
|
+
core.logger[cause === void 0 ? "warn" : "error"]("Feedback client operation failed", { module: "feedback", traceId, error });
|
|
411
|
+
return err({ ...error, message: core.error.formatMessage(error) });
|
|
285
412
|
}
|
|
286
413
|
function limits() {
|
|
287
|
-
return { events: config?.maxEvents ?? 1e3, bytes: config?.maxBytes ?? 2 * 1024 * 1024, age: config?.maxAgeMs ?? 3e5, before: config?.beforeCount ?? 100, after: config?.afterCount ?? 100 };
|
|
414
|
+
return { events: Math.min(config?.maxEvents ?? 1e3, budget.maxEvidenceCount), bytes: Math.min(config?.maxBytes ?? 2 * 1024 * 1024, budget.maxEvidenceBytes), age: config?.maxAgeMs ?? 3e5, before: config?.beforeCount ?? 100, after: config?.afterCount ?? 100 };
|
|
288
415
|
}
|
|
289
416
|
function schedulePersist() {
|
|
290
417
|
if (!config?.persistDraft || persistTimer)
|
|
@@ -350,6 +477,10 @@ function createFeedbackClient() {
|
|
|
350
477
|
missing.add("browser-error-windows-truncated");
|
|
351
478
|
}
|
|
352
479
|
schedulePersist();
|
|
480
|
+
appendToDraft(item, size);
|
|
481
|
+
}
|
|
482
|
+
function appendToDraft(item, size) {
|
|
483
|
+
const bounds = limits();
|
|
353
484
|
if (draft && !collectionFinished && !collectionSealed && (Date.now() <= deadline && afterCount < bounds.after || item.kind === "environment") && draft.evidence.length < bounds.events && draftBytes + size <= bounds.bytes) {
|
|
354
485
|
draft.evidence.push(item);
|
|
355
486
|
draftBytes += size;
|
|
@@ -374,7 +505,18 @@ function createFeedbackClient() {
|
|
|
374
505
|
const numeric = [input.maxEvents, input.maxBytes, input.maxAgeMs, input.beforeCount, input.afterCount, input.afterTimeoutMs, input.screenshotTimeoutMs, input.maxKeyScreenshots, input.keyScreenshotThrottleMs];
|
|
375
506
|
if (numeric.some((value) => value !== void 0 && (!Number.isSafeInteger(value) || value < 0)) || input.persistDraft && !input.persistenceKey)
|
|
376
507
|
return failure("INVALID_INPUT");
|
|
508
|
+
if (!FeedbackSubmitInputSchema.pick({ appId: true, appVersion: true, buildId: true, commit: true }).safeParse({ appId: input.appId, appVersion: input.appVersion, buildId: input.buildId, commit: input.commit }).success)
|
|
509
|
+
return failure("INVALID_INPUT");
|
|
510
|
+
const initVersion = sessionVersion;
|
|
511
|
+
const policy = input.actions.policy ? await input.actions.policy().catch(() => failure("OPERATION_FAILED")) : ok(input.budget ?? resolveFeedbackBudget());
|
|
512
|
+
if (initVersion !== sessionVersion)
|
|
513
|
+
return failure("NOT_INITIALIZED");
|
|
514
|
+
policyVerified = policy.success;
|
|
515
|
+
budget = FeedbackBudgetSchema.parse(policy.success ? policy.data : input.budget ?? resolveFeedbackBudget());
|
|
516
|
+
if (!policy.success)
|
|
517
|
+
missing.add("server-policy-unavailable");
|
|
377
518
|
config = input;
|
|
519
|
+
saveController = new AbortController();
|
|
378
520
|
closing = false;
|
|
379
521
|
source = `browser:${core.id.uuid()}`;
|
|
380
522
|
storageKey = `${input.appId}:${input.persistenceKey ?? ""}`;
|
|
@@ -412,7 +554,7 @@ function createFeedbackClient() {
|
|
|
412
554
|
unsubscribeLogger = core.logger.observe((entry) => {
|
|
413
555
|
if (entry.context.module === "feedback" || entry.context.feedback === true)
|
|
414
556
|
return;
|
|
415
|
-
record({ kind: ["error", "fatal"].includes(entry.level) ? "error" : "log", requestId: typeof entry.context.requestId === "string" ? entry.context.requestId : void 0, data: { level: entry.level, message: entry.message, context: entry.context } });
|
|
557
|
+
record({ kind: ["error", "fatal"].includes(entry.level) ? "error" : "log", requestId: typeof entry.context.requestId === "string" ? entry.context.requestId : typeof entry.context.traceId === "string" ? entry.context.traceId : void 0, data: { level: entry.level, message: entry.message, context: entry.context } });
|
|
416
558
|
});
|
|
417
559
|
listen("error", (event) => record({ kind: "error", data: { message: event.message, stack: event.error instanceof Error ? event.error.stack : void 0, filename: feedbackPageUrl(event.filename || location.href), line: event.lineno, column: event.colno } }));
|
|
418
560
|
listen("unhandledrejection", (event) => record({ kind: "error", data: { reason: event.reason } }));
|
|
@@ -469,19 +611,22 @@ function createFeedbackClient() {
|
|
|
469
611
|
async function startReplay(activeConfig) {
|
|
470
612
|
try {
|
|
471
613
|
const { record: start } = await import('@rrweb/record');
|
|
472
|
-
if (closing || config !== activeConfig)
|
|
614
|
+
if (closing || config !== activeConfig || screenshotCaptures || stopReplay)
|
|
473
615
|
return;
|
|
616
|
+
let firstEvent = true;
|
|
474
617
|
stopReplay = start({
|
|
475
618
|
maskAllInputs: true,
|
|
476
619
|
maskTextSelector: "[data-feedback-private],.rr-mask",
|
|
477
620
|
blockSelector: "[data-feedback-ignore],[data-feedback-private],.rr-block,iframe,canvas,video",
|
|
478
|
-
|
|
621
|
+
// rrweb 内联跨域图像会修改原 img 的 crossOrigin,可能破坏带凭据的头像等资源。
|
|
622
|
+
inlineImages: false,
|
|
479
623
|
inlineStylesheet: true,
|
|
480
624
|
collectFonts: false,
|
|
481
625
|
checkoutEveryNms: 3e4,
|
|
482
626
|
emit: (event, isCheckout) => {
|
|
483
|
-
if (isCheckout || replayChunks.length === 0)
|
|
627
|
+
if (firstEvent || isCheckout || replayChunks.length === 0)
|
|
484
628
|
replayChunks.push([]);
|
|
629
|
+
firstEvent = false;
|
|
485
630
|
let payload;
|
|
486
631
|
try {
|
|
487
632
|
payload = sanitizeFeedbackReplay(event);
|
|
@@ -493,6 +638,7 @@ function createFeedbackClient() {
|
|
|
493
638
|
if (!safe.success)
|
|
494
639
|
return;
|
|
495
640
|
replayChunks.at(-1)?.push(safe.data);
|
|
641
|
+
appendToDraft(safe.data, new TextEncoder().encode(JSON.stringify(safe.data)).byteLength);
|
|
496
642
|
while (replayChunks.length > 2)
|
|
497
643
|
replayChunks.shift();
|
|
498
644
|
const count = replayChunks.reduce((total, chunk) => total + chunk.length, 0);
|
|
@@ -515,10 +661,10 @@ function createFeedbackClient() {
|
|
|
515
661
|
lastKeyScreenshot = Date.now();
|
|
516
662
|
keyScreenshotPromise = (async () => {
|
|
517
663
|
try {
|
|
518
|
-
const bytes = await
|
|
664
|
+
const bytes = await takeScreenshot(activeConfig);
|
|
519
665
|
if (closing || config !== activeConfig)
|
|
520
666
|
return;
|
|
521
|
-
if (bytes.byteLength >
|
|
667
|
+
if (bytes.byteLength > budget.maxAttachmentBytes) {
|
|
522
668
|
missing.add("key-screenshot-size-limit-exceeded");
|
|
523
669
|
return;
|
|
524
670
|
}
|
|
@@ -536,6 +682,21 @@ function createFeedbackClient() {
|
|
|
536
682
|
});
|
|
537
683
|
return keyScreenshotPromise;
|
|
538
684
|
}
|
|
685
|
+
function takeScreenshot(activeConfig) {
|
|
686
|
+
screenshotCaptures++;
|
|
687
|
+
stopReplay?.();
|
|
688
|
+
stopReplay = void 0;
|
|
689
|
+
const captured = (async () => {
|
|
690
|
+
try {
|
|
691
|
+
return await (activeConfig.captureScreenshot?.() ?? captureFeedbackScreenshot());
|
|
692
|
+
} finally {
|
|
693
|
+
screenshotCaptures--;
|
|
694
|
+
if (!screenshotCaptures && !closing && config === activeConfig && activeConfig.replay)
|
|
695
|
+
await startReplay(activeConfig);
|
|
696
|
+
}
|
|
697
|
+
})();
|
|
698
|
+
return bounded(captured, activeConfig.screenshotTimeoutMs ?? 3e3);
|
|
699
|
+
}
|
|
539
700
|
async function doPrepare() {
|
|
540
701
|
if (!config || closing)
|
|
541
702
|
return failure("NOT_INITIALIZED");
|
|
@@ -550,8 +711,14 @@ function createFeedbackClient() {
|
|
|
550
711
|
if (closing || config !== activeConfig)
|
|
551
712
|
return failure("NOT_INITIALIZED");
|
|
552
713
|
}
|
|
553
|
-
if (draft && !collectionFinished)
|
|
714
|
+
if (draft && !collectionFinished) {
|
|
715
|
+
if (!draft.submission && config.screenshot !== false && preparedPage !== pageIdentity()) {
|
|
716
|
+
const captured = await captureScreenshot();
|
|
717
|
+
if (!captured.success)
|
|
718
|
+
return captured;
|
|
719
|
+
}
|
|
554
720
|
return ok(draft);
|
|
721
|
+
}
|
|
555
722
|
collectionFinished = false;
|
|
556
723
|
collectionSealed = false;
|
|
557
724
|
initialIds.clear();
|
|
@@ -578,7 +745,7 @@ function createFeedbackClient() {
|
|
|
578
745
|
if (closing || config !== activeConfig)
|
|
579
746
|
return failure("NOT_INITIALIZED");
|
|
580
747
|
keyScreenshots = keyScreenshots.filter((item) => item.timestamp >= now - limits().age);
|
|
581
|
-
draft = { submissionId: core.id.uuid(), anchorTime: anchor?.timestamp ?? now, anchorKind: anchor ? "error" : "feedback", errorId: anchor?.errorId, evidence: [...selected, ...other, ...replayChunks.flat()].sort((a, b) => a.sequence - b.sequence), attachments:
|
|
748
|
+
draft = { submissionId: core.id.uuid(), anchorTime: anchor?.timestamp ?? now, anchorKind: anchor ? "error" : "feedback", errorId: anchor?.errorId, evidence: [...selected, ...other, ...replayChunks.flat()].sort((a, b) => a.sequence - b.sequence), attachments: [], missing: [...missing], expiresAt: now + 864e5 };
|
|
582
749
|
while (draft.evidence.length > limits().events || new TextEncoder().encode(JSON.stringify(draft.evidence)).byteLength > limits().bytes) {
|
|
583
750
|
draft.evidence.shift();
|
|
584
751
|
missing.add("browser-draft-truncated");
|
|
@@ -588,30 +755,24 @@ function createFeedbackClient() {
|
|
|
588
755
|
draft.missing.push(`browser-buffer-truncated:${dropped}`);
|
|
589
756
|
afterCount = frozenWindow?.after ?? (anchor ? Math.max(0, logs.length - anchorIndex - 1) : 0);
|
|
590
757
|
deadline = (anchor?.timestamp ?? now) + (config.afterTimeoutMs ?? 3e4);
|
|
591
|
-
|
|
758
|
+
recordEnvironment();
|
|
592
759
|
if (config.screenshot !== false) {
|
|
593
760
|
const current = draft;
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
} else {
|
|
601
|
-
const image = await attachment("screenshot.png", "image/png", bytes);
|
|
602
|
-
if (closing || config !== activeConfig || draft !== current)
|
|
603
|
-
return failure("NOT_INITIALIZED");
|
|
604
|
-
current.attachments.push(image);
|
|
605
|
-
}
|
|
606
|
-
} catch (cause) {
|
|
607
|
-
if (closing || config !== activeConfig || draft !== current)
|
|
608
|
-
return failure("NOT_INITIALIZED");
|
|
609
|
-
current.missing.push("screenshot-unavailable");
|
|
610
|
-
if (cause instanceof Error)
|
|
611
|
-
current.missing.push(`screenshot-error:${String(sanitizeFeedbackData({ message: cause.message }).message).slice(0, 300)}`);
|
|
761
|
+
const captured = await captureScreenshot(void 0, "screenshot.png");
|
|
762
|
+
if (closing || config !== activeConfig || draft !== current)
|
|
763
|
+
return failure("NOT_INITIALIZED");
|
|
764
|
+
if (!captured.success) {
|
|
765
|
+
current.missing.push(captured.error.code === HaiFeedbackError.LIMIT_EXCEEDED.code ? "screenshot-size-limit-exceeded" : "screenshot-unavailable");
|
|
766
|
+
current.missing.push(`screenshot-error:${captured.error.message.slice(0, 300)}`);
|
|
612
767
|
}
|
|
613
768
|
}
|
|
769
|
+
for (const { attachment: image } of keyScreenshots) {
|
|
770
|
+
if (image.size > budget.maxAttachmentBytes || draft.attachments.length >= budget.maxAttachmentCount || draft.attachments.reduce((sum, file) => sum + file.size, image.size) > budget.maxAttachmentsBytes)
|
|
771
|
+
draft.missing.push("key-screenshot-budget-exceeded");
|
|
772
|
+
else draft.attachments.push(image);
|
|
773
|
+
}
|
|
614
774
|
schedulePersist();
|
|
775
|
+
preparedPage = pageIdentity();
|
|
615
776
|
return ok(draft);
|
|
616
777
|
}
|
|
617
778
|
function prepare() {
|
|
@@ -620,6 +781,52 @@ function createFeedbackClient() {
|
|
|
620
781
|
});
|
|
621
782
|
return preparePromise;
|
|
622
783
|
}
|
|
784
|
+
function recordEnvironment() {
|
|
785
|
+
if (!config)
|
|
786
|
+
return;
|
|
787
|
+
record({ kind: "environment", data: { appId: config.appId, appVersion: config.appVersion, buildId: config.buildId, commit: config.commit, url: feedbackPageUrl(location.href), browser: navigator.userAgent, language: navigator.language, timezone: new Intl.DateTimeFormat().resolvedOptions().timeZone, viewport: { width: innerWidth, height: innerHeight, pixelRatio: devicePixelRatio }, online: navigator.onLine, cookies: collectFeedbackCookies(document.cookie, config.cookieAllowlist ?? []) } });
|
|
788
|
+
}
|
|
789
|
+
function pageIdentity() {
|
|
790
|
+
return `${location.origin}${location.pathname}${location.hash.startsWith("#/") ? location.hash.split("?")[0] : ""}`;
|
|
791
|
+
}
|
|
792
|
+
async function captureScreenshot(attachmentId, name) {
|
|
793
|
+
if (!config || !draft || closing)
|
|
794
|
+
return failure("NOT_INITIALIZED");
|
|
795
|
+
const current = draft;
|
|
796
|
+
const activeConfig = config;
|
|
797
|
+
const previous = attachmentId ? current.attachments.find((item) => item.id === attachmentId) : void 0;
|
|
798
|
+
const previousChecksum = previous?.checksum;
|
|
799
|
+
if (current.submission)
|
|
800
|
+
return err(HaiFeedbackError.CONFLICT, feedbackM("feedback_conflict"));
|
|
801
|
+
if (attachmentId && (!previous || !previous.contentType.startsWith("image/")))
|
|
802
|
+
return failure("INVALID_INPUT");
|
|
803
|
+
if (!previous && current.attachments.length >= budget.maxAttachmentCount)
|
|
804
|
+
return failure("LIMIT_EXCEEDED");
|
|
805
|
+
try {
|
|
806
|
+
const bytes = await takeScreenshot(activeConfig);
|
|
807
|
+
if (bytes.byteLength > budget.maxAttachmentBytes)
|
|
808
|
+
return failure("LIMIT_EXCEEDED");
|
|
809
|
+
const previousName = previous && (/\.[^.]+$/.test(previous.name) ? previous.name.replace(/\.[^.]+$/, ".png") : `${previous.name}.png`);
|
|
810
|
+
const image = await attachment(name ?? previousName ?? `screenshot-${core.id.uuid()}.png`, "image/png", bytes);
|
|
811
|
+
if (closing || config !== activeConfig || draft !== current || current.submission || previous && (!current.attachments.includes(previous) || previous.checksum !== previousChecksum))
|
|
812
|
+
return err(HaiFeedbackError.CONFLICT, feedbackM("feedback_conflict"));
|
|
813
|
+
if (current.attachments.reduce((total, item) => total + (item === previous ? 0 : item.size), image.size) > budget.maxAttachmentsBytes)
|
|
814
|
+
return failure("LIMIT_EXCEEDED");
|
|
815
|
+
if (previous)
|
|
816
|
+
current.attachments = current.attachments.map((item) => item === previous ? { ...image, id: previous.id } : item);
|
|
817
|
+
else if (current.attachments.length < budget.maxAttachmentCount)
|
|
818
|
+
current.attachments.unshift(image);
|
|
819
|
+
else
|
|
820
|
+
return failure("LIMIT_EXCEEDED");
|
|
821
|
+
if (!name)
|
|
822
|
+
recordEnvironment();
|
|
823
|
+
preparedPage = pageIdentity();
|
|
824
|
+
schedulePersist();
|
|
825
|
+
return ok(void 0);
|
|
826
|
+
} catch (cause) {
|
|
827
|
+
return failure("OPERATION_FAILED", cause);
|
|
828
|
+
}
|
|
829
|
+
}
|
|
623
830
|
async function submit(input = {}) {
|
|
624
831
|
if (!config || closing)
|
|
625
832
|
return failure("NOT_INITIALIZED");
|
|
@@ -629,6 +836,23 @@ function createFeedbackClient() {
|
|
|
629
836
|
return prepared;
|
|
630
837
|
const current = prepared.data;
|
|
631
838
|
try {
|
|
839
|
+
if (!policyVerified && activeConfig.actions.policy) {
|
|
840
|
+
const policy = await activeConfig.actions.policy();
|
|
841
|
+
if (closing || config !== activeConfig || draft !== current)
|
|
842
|
+
return failure("NOT_INITIALIZED");
|
|
843
|
+
if (!policy.success)
|
|
844
|
+
return policy;
|
|
845
|
+
budget = FeedbackBudgetSchema.parse(policy.data);
|
|
846
|
+
policyVerified = true;
|
|
847
|
+
missing.delete("server-policy-unavailable");
|
|
848
|
+
current.missing = current.missing.filter((reason) => reason !== "server-policy-unavailable");
|
|
849
|
+
while (current.evidence.length && (current.evidence.length > limits().events || new TextEncoder().encode(JSON.stringify(current.evidence)).byteLength > limits().bytes)) {
|
|
850
|
+
current.evidence.shift();
|
|
851
|
+
if (!current.missing.includes("browser-draft-truncated"))
|
|
852
|
+
current.missing.push("browser-draft-truncated");
|
|
853
|
+
}
|
|
854
|
+
draftBytes = new TextEncoder().encode(JSON.stringify(current.evidence)).byteLength;
|
|
855
|
+
}
|
|
632
856
|
const fingerprint = await digestHex(new TextEncoder().encode(JSON.stringify({ description: sanitizeFeedbackData({ text: input.description ?? "" }).text, category: input.category ?? "", contact: sanitizeFeedbackData({ text: input.contact ?? "" }).text, files: (input.files ?? []).map((item) => ({ name: item.name, size: item.size, lastModified: item.lastModified })), removed: [...input.removedAttachmentIds ?? []].sort() })));
|
|
633
857
|
if (closing || config !== activeConfig || draft !== current)
|
|
634
858
|
return failure("NOT_INITIALIZED");
|
|
@@ -689,7 +913,7 @@ function createFeedbackClient() {
|
|
|
689
913
|
if (pending.some((item) => !initialIds.has(item.id)))
|
|
690
914
|
return failure();
|
|
691
915
|
drainPasses++;
|
|
692
|
-
if (drainPasses >=
|
|
916
|
+
if (drainPasses >= budget.maxAttachmentCount)
|
|
693
917
|
return ok(void 0);
|
|
694
918
|
}
|
|
695
919
|
if (collectionSealed) {
|
|
@@ -727,6 +951,8 @@ function createFeedbackClient() {
|
|
|
727
951
|
const bytes = await annotateFeedbackImage(originalBytes, mark);
|
|
728
952
|
const name = /\.[^.]+$/.test(item.name) ? item.name.replace(/\.[^.]+$/, ".png") : `${item.name}.png`;
|
|
729
953
|
const updated = await attachment(name, "image/png", bytes);
|
|
954
|
+
if (updated.size > budget.maxAttachmentBytes || current.attachments.reduce((sum, file) => sum + (file === item ? 0 : file.size), updated.size) > budget.maxAttachmentsBytes)
|
|
955
|
+
return failure("LIMIT_EXCEEDED");
|
|
730
956
|
if (closing || draft !== current || current.submission || !current.attachments.includes(item) || item.bytes !== originalBytes)
|
|
731
957
|
return err(HaiFeedbackError.CONFLICT, feedbackM("feedback_conflict"));
|
|
732
958
|
Object.assign(item, updated, { id });
|
|
@@ -736,45 +962,46 @@ function createFeedbackClient() {
|
|
|
736
962
|
return failure("OPERATION_FAILED", cause);
|
|
737
963
|
}
|
|
738
964
|
}
|
|
739
|
-
async function addFiles(files) {
|
|
965
|
+
async function addFiles(files, removedAttachmentIds = []) {
|
|
740
966
|
if (!draft)
|
|
741
967
|
return failure("NOT_INITIALIZED");
|
|
742
|
-
if (draft.submission && files.length)
|
|
968
|
+
if (draft.submission && (files.length || removedAttachmentIds.length))
|
|
743
969
|
return err(HaiFeedbackError.CONFLICT, feedbackM("feedback_retryInputChanged"));
|
|
744
970
|
const current = draft;
|
|
745
971
|
const items = [];
|
|
746
972
|
const newMissing = [];
|
|
747
973
|
try {
|
|
748
974
|
for (const file of files) {
|
|
749
|
-
if (items.length >=
|
|
975
|
+
if (items.length >= budget.maxAttachmentCount || file.size > budget.maxAttachmentBytes)
|
|
750
976
|
return failure("LIMIT_EXCEEDED");
|
|
751
|
-
|
|
752
|
-
|
|
977
|
+
const contentType = feedbackFileContentType(file);
|
|
978
|
+
if (!contentType)
|
|
979
|
+
return failure("INVALID_INPUT", void 0, feedbackM("feedback_unsupportedAttachmentType"));
|
|
753
980
|
let bytes;
|
|
754
|
-
if (
|
|
981
|
+
if (contentType.startsWith("text/") || contentType === "application/json") {
|
|
755
982
|
const raw = await file.text();
|
|
756
|
-
if (
|
|
983
|
+
if (contentType === "application/json" && raw.length > 8192)
|
|
757
984
|
newMissing.push(`attachment-text-truncated:${String(sanitizeFeedbackData({ text: file.name }).text).slice(0, 100)}`);
|
|
758
|
-
bytes =
|
|
985
|
+
bytes = contentType === "application/json" ? new TextEncoder().encode(JSON.stringify(sanitizeFeedbackData(JSON.parse(raw)))) : new TextEncoder().encode(sanitizeFeedbackText(raw));
|
|
759
986
|
} else {
|
|
760
987
|
bytes = new Uint8Array(await file.arrayBuffer());
|
|
761
988
|
}
|
|
762
|
-
if (bytes.byteLength >
|
|
989
|
+
if (bytes.byteLength > budget.maxAttachmentBytes)
|
|
763
990
|
return failure("LIMIT_EXCEEDED");
|
|
764
|
-
const item = await attachment(file.name,
|
|
991
|
+
const item = await attachment(file.name, contentType, bytes);
|
|
765
992
|
if (!items.some((existing) => existing.name === item.name && existing.checksum === item.checksum))
|
|
766
993
|
items.push(item);
|
|
767
994
|
}
|
|
768
995
|
if (draft !== current || closing)
|
|
769
996
|
return err(HaiFeedbackError.CONFLICT, feedbackM("feedback_conflict"));
|
|
770
|
-
if (current.submission && files.length)
|
|
997
|
+
if (current.submission && (files.length || removedAttachmentIds.length))
|
|
771
998
|
return err(HaiFeedbackError.CONFLICT, feedbackM("feedback_retryInputChanged"));
|
|
772
|
-
const merged =
|
|
999
|
+
const merged = current.attachments.filter((file) => !removedAttachmentIds.includes(file.id));
|
|
773
1000
|
for (const item of items) {
|
|
774
1001
|
if (!merged.some((existing) => existing.name === item.name && existing.checksum === item.checksum))
|
|
775
1002
|
merged.push(item);
|
|
776
1003
|
}
|
|
777
|
-
if (merged.length >
|
|
1004
|
+
if (merged.length > budget.maxAttachmentCount || merged.reduce((total, item) => total + item.size, 0) > budget.maxAttachmentsBytes)
|
|
778
1005
|
return failure("LIMIT_EXCEEDED");
|
|
779
1006
|
current.attachments = merged;
|
|
780
1007
|
current.missing.push(...newMissing);
|
|
@@ -797,24 +1024,27 @@ function createFeedbackClient() {
|
|
|
797
1024
|
attachments.splice(0, attachments.length, ...draft.attachments);
|
|
798
1025
|
} else {
|
|
799
1026
|
for (const file of input.files ?? []) {
|
|
800
|
-
if (attachments.length >=
|
|
1027
|
+
if (attachments.length >= budget.maxAttachmentCount || file.size > budget.maxAttachmentBytes)
|
|
801
1028
|
return failure("LIMIT_EXCEEDED");
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
1029
|
+
const contentType = feedbackFileContentType(file);
|
|
1030
|
+
if (!contentType)
|
|
1031
|
+
return failure("INVALID_INPUT", void 0, feedbackM("feedback_unsupportedAttachmentType"));
|
|
1032
|
+
const bytes = contentType === "application/json" ? new TextEncoder().encode(JSON.stringify(sanitizeFeedbackData(JSON.parse(await file.text())))) : contentType.startsWith("text/") ? new TextEncoder().encode(sanitizeFeedbackText(await file.text())) : new Uint8Array(await file.arrayBuffer());
|
|
1033
|
+
if (bytes.byteLength > budget.maxAttachmentBytes)
|
|
806
1034
|
return failure("LIMIT_EXCEEDED");
|
|
807
|
-
const item = await attachment(file.name,
|
|
1035
|
+
const item = await attachment(file.name, contentType, bytes);
|
|
808
1036
|
if (!attachments.some((existing) => existing.name === item.name && existing.checksum === item.checksum))
|
|
809
1037
|
attachments.push(item);
|
|
810
1038
|
}
|
|
811
1039
|
}
|
|
812
|
-
return createFeedbackBundle({ report: { ...draft.report ?? { id: draft.submissionId, appId: config?.appId, anchor: { kind: draft.anchorKind, timestamp: draft.anchorTime }, evidenceRevision: 1 }, ...sanitizeFeedbackData({ description: input.description ?? draft.report?.description ?? "", category: input.category, contact: input.contact }) }, evidence: draft.evidence, attachments: attachments.filter((item) => !input.removedAttachmentIds?.includes(item.id)), missing: [.../* @__PURE__ */ new Set([...draft.missing, ...missing, ...!draft.report ? ["server-evidence-not-obtained", "feedback-not-yet-accepted"] : []])] });
|
|
1040
|
+
return createFeedbackBundle({ maxBytes: budget.maxTotalBytes, report: { ...draft.report ?? { id: draft.submissionId, appId: config?.appId, appVersion: config?.appVersion, buildId: config?.buildId, commit: config?.commit, url: draft.evidence.filter((item) => item.kind === "environment").at(-1)?.data.url, anchor: { kind: draft.anchorKind, timestamp: draft.anchorTime }, evidenceRevision: 1 }, ...sanitizeFeedbackData({ description: input.description ?? draft.report?.description ?? "", category: input.category, contact: input.contact }) }, evidence: draft.evidence, attachments: attachments.filter((item) => !input.removedAttachmentIds?.includes(item.id)), missing: [.../* @__PURE__ */ new Set([...draft.missing, ...missing, ...!draft.report ? ["server-evidence-not-obtained", "feedback-not-yet-accepted"] : []])] });
|
|
813
1041
|
} catch (cause) {
|
|
814
1042
|
return failure("OPERATION_FAILED", cause);
|
|
815
1043
|
}
|
|
816
1044
|
}
|
|
817
|
-
function close() {
|
|
1045
|
+
function close(options = {}) {
|
|
1046
|
+
sessionVersion++;
|
|
1047
|
+
discardOnClose ||= options.discard === true;
|
|
818
1048
|
closePromise ??= doClose().finally(() => {
|
|
819
1049
|
closePromise = void 0;
|
|
820
1050
|
});
|
|
@@ -822,6 +1052,7 @@ function createFeedbackClient() {
|
|
|
822
1052
|
}
|
|
823
1053
|
async function doClose() {
|
|
824
1054
|
closing = true;
|
|
1055
|
+
saveController.abort();
|
|
825
1056
|
unsubscribeLogger?.();
|
|
826
1057
|
unsubscribeLogger = void 0;
|
|
827
1058
|
stopReplay?.();
|
|
@@ -838,9 +1069,14 @@ function createFeedbackClient() {
|
|
|
838
1069
|
await keyScreenshotPromise;
|
|
839
1070
|
if (draft?.report)
|
|
840
1071
|
await flush();
|
|
841
|
-
if (
|
|
1072
|
+
if (discardOnClose && storageKey)
|
|
1073
|
+
await deleteFeedbackDraft(storageKey).catch(() => void 0);
|
|
1074
|
+
else if (config?.persistDraft && draft && !collectionFinished)
|
|
842
1075
|
await saveFeedbackDraft(storageKey, draft).catch(() => void 0);
|
|
1076
|
+
releasePreviews();
|
|
1077
|
+
discardOnClose = false;
|
|
843
1078
|
config = void 0;
|
|
1079
|
+
preparedPage = "";
|
|
844
1080
|
buffer = [];
|
|
845
1081
|
bufferBytes = 0;
|
|
846
1082
|
draft = void 0;
|
|
@@ -856,7 +1092,17 @@ function createFeedbackClient() {
|
|
|
856
1092
|
collectionFinished = false;
|
|
857
1093
|
collectionSealed = false;
|
|
858
1094
|
}
|
|
859
|
-
return { init, prepare, submit, flush, record, getDraft: () => draft, annotate, addFiles, exportDraft,
|
|
1095
|
+
return { init, prepare, captureScreenshot, submit, flush, record, getDraft: () => draft, getView, releasePreviews, annotate, addFiles, exportDraft, async saveDraft(input) {
|
|
1096
|
+
const session = config;
|
|
1097
|
+
const exported = await exportDraft(input);
|
|
1098
|
+
if (!exported.success)
|
|
1099
|
+
return exported;
|
|
1100
|
+
if (closing || !session || session !== config)
|
|
1101
|
+
return failure("NOT_INITIALIZED");
|
|
1102
|
+
return saveFeedbackFile(exported.data, `feedback-${draft?.submissionId ?? "local"}.zip`, "application/zip", saveController.signal);
|
|
1103
|
+
}, close, get budget() {
|
|
1104
|
+
return { ...budget };
|
|
1105
|
+
} };
|
|
860
1106
|
}
|
|
861
1107
|
var feedbackClient = createFeedbackClient();
|
|
862
1108
|
async function bounded(promise, ms) {
|
|
@@ -883,7 +1129,75 @@ function base64(bytes) {
|
|
|
883
1129
|
value += String.fromCharCode(...bytes.subarray(offset, offset + 32768));
|
|
884
1130
|
return btoa(value);
|
|
885
1131
|
}
|
|
1132
|
+
function createFeedbackReview(actions) {
|
|
1133
|
+
let controller = new AbortController();
|
|
1134
|
+
const failed = () => err(HaiFeedbackError.OPERATION_FAILED, feedbackM("feedback_operationFailed"));
|
|
1135
|
+
return {
|
|
1136
|
+
close() {
|
|
1137
|
+
controller.abort();
|
|
1138
|
+
controller = new AbortController();
|
|
1139
|
+
},
|
|
1140
|
+
async loadReplay(id) {
|
|
1141
|
+
const signal = controller.signal;
|
|
1142
|
+
const events = [];
|
|
1143
|
+
try {
|
|
1144
|
+
for (let page = 1; page <= 100; page++) {
|
|
1145
|
+
const result = await actions.getEvidence({ id, kind: "replay", page, pageSize: 100 });
|
|
1146
|
+
if (signal.aborted)
|
|
1147
|
+
return failed();
|
|
1148
|
+
if (!result.success)
|
|
1149
|
+
return result;
|
|
1150
|
+
events.push(...result.data.items);
|
|
1151
|
+
if (events.length >= result.data.total || !result.data.items.length)
|
|
1152
|
+
return ok(events);
|
|
1153
|
+
}
|
|
1154
|
+
} catch {
|
|
1155
|
+
return failed();
|
|
1156
|
+
}
|
|
1157
|
+
return failed();
|
|
1158
|
+
},
|
|
1159
|
+
async exportReport(id) {
|
|
1160
|
+
const signal = controller.signal;
|
|
1161
|
+
try {
|
|
1162
|
+
const started = await actions.export({ id });
|
|
1163
|
+
if (!started.success)
|
|
1164
|
+
return started;
|
|
1165
|
+
for (let attempt = 0; attempt < 120; attempt++) {
|
|
1166
|
+
if (signal.aborted)
|
|
1167
|
+
return failed();
|
|
1168
|
+
const detail = await actions.get({ id });
|
|
1169
|
+
if (!detail.success)
|
|
1170
|
+
return detail;
|
|
1171
|
+
if (signal.aborted)
|
|
1172
|
+
return failed();
|
|
1173
|
+
const job = detail.data.jobs.find((job2) => job2.id === started.data.id);
|
|
1174
|
+
if (!job || job.status === "failed")
|
|
1175
|
+
return failed();
|
|
1176
|
+
if (job.status === "succeeded") {
|
|
1177
|
+
const file = await actions.getExport({ id, jobId: job.id });
|
|
1178
|
+
if (signal.aborted)
|
|
1179
|
+
return failed();
|
|
1180
|
+
return file.success ? saveFeedbackFile(feedbackDataBytes(file.data), file.data.name, file.data.contentType, signal) : file;
|
|
1181
|
+
}
|
|
1182
|
+
await new Promise((resolve) => {
|
|
1183
|
+
let timer;
|
|
1184
|
+
const done = () => {
|
|
1185
|
+
clearTimeout(timer);
|
|
1186
|
+
signal.removeEventListener("abort", done);
|
|
1187
|
+
resolve();
|
|
1188
|
+
};
|
|
1189
|
+
timer = setTimeout(done, 500);
|
|
1190
|
+
signal.addEventListener("abort", done, { once: true });
|
|
1191
|
+
});
|
|
1192
|
+
}
|
|
1193
|
+
} catch {
|
|
1194
|
+
return failed();
|
|
1195
|
+
}
|
|
1196
|
+
return failed();
|
|
1197
|
+
}
|
|
1198
|
+
};
|
|
1199
|
+
}
|
|
886
1200
|
|
|
887
|
-
export { createFeedbackClient, feedbackClient };
|
|
1201
|
+
export { configureFeedbackSaving, createFeedbackClient, createFeedbackReview, feedbackClient, feedbackDataBytes, saveFeedbackFile };
|
|
888
1202
|
//# sourceMappingURL=index.js.map
|
|
889
1203
|
//# sourceMappingURL=index.js.map
|