@h-ai/feedback 0.1.0-alpha.70 → 0.1.0-alpha.71
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 +25 -8
- package/README.ja.md +25 -8
- package/README.md +25 -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-DYEK2FNM.js → chunk-KGHZL7K6.js} +3 -6
- package/dist/chunk-KGHZL7K6.js.map +1 -0
- package/dist/{chunk-43AOXE4W.js → chunk-XQXX5WMV.js} +11 -6
- package/dist/chunk-XQXX5WMV.js.map +1 -0
- package/dist/client/index.d.ts +88 -6
- package/dist/client/index.js +325 -59
- 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,6 +1,46 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
1
|
+
import { feedbackM, HaiFeedbackError, redactFeedbackText } from '../chunk-KGHZL7K6.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
46
|
var PRIVATE_SELECTOR = 'input,textarea,select,[contenteditable]:not([contenteditable="false"]),[data-feedback-private],.rr-block,.rr-mask';
|
|
@@ -15,13 +55,15 @@ async function captureFeedbackScreenshot() {
|
|
|
15
55
|
}
|
|
16
56
|
const { default: html2canvas } = await import('html2canvas');
|
|
17
57
|
const canvas = await html2canvas(document.body, {
|
|
18
|
-
width:
|
|
19
|
-
height:
|
|
20
|
-
scale: 1,
|
|
58
|
+
width: window.innerWidth,
|
|
59
|
+
height: window.innerHeight,
|
|
60
|
+
scale: Math.min(1, 1920 / window.innerWidth, 1200 / window.innerHeight),
|
|
21
61
|
// 原生 SVG foreignObject 保留 CSS mask 图标与字体基线,避免 CanvasRenderer 裁切文本。
|
|
22
62
|
foreignObjectRendering: true,
|
|
23
|
-
|
|
24
|
-
|
|
63
|
+
x: window.scrollX,
|
|
64
|
+
y: window.scrollY,
|
|
65
|
+
scrollX: window.scrollX,
|
|
66
|
+
scrollY: window.scrollY,
|
|
25
67
|
useCORS: false,
|
|
26
68
|
allowTaint: false,
|
|
27
69
|
logging: false,
|
|
@@ -78,6 +120,11 @@ async function annotateFeedbackImage(bytes, annotation) {
|
|
|
78
120
|
try {
|
|
79
121
|
image.src = imageUrl;
|
|
80
122
|
await image.decode();
|
|
123
|
+
if (annotation.coordinateSpace === "ratio") {
|
|
124
|
+
if ([annotation.x, annotation.y, annotation.width, annotation.height].some((value) => value < 0 || value > 1))
|
|
125
|
+
throw new Error("Invalid annotation ratio");
|
|
126
|
+
annotation = { ...annotation, x: annotation.x * image.naturalWidth, y: annotation.y * image.naturalHeight, width: annotation.width * image.naturalWidth, height: annotation.height * image.naturalHeight };
|
|
127
|
+
}
|
|
81
128
|
canvas.width = image.naturalWidth;
|
|
82
129
|
canvas.height = image.naturalHeight;
|
|
83
130
|
const context = canvas.getContext("2d");
|
|
@@ -86,7 +133,9 @@ async function annotateFeedbackImage(bytes, annotation) {
|
|
|
86
133
|
context.drawImage(image, 0, 0);
|
|
87
134
|
if (annotation.mode === "mask") {
|
|
88
135
|
context.fillStyle = "#111";
|
|
89
|
-
|
|
136
|
+
const left = Math.floor(annotation.x);
|
|
137
|
+
const top = Math.floor(annotation.y);
|
|
138
|
+
context.fillRect(left, top, Math.ceil(annotation.x + annotation.width) - left, Math.ceil(annotation.y + annotation.height) - top);
|
|
90
139
|
} else {
|
|
91
140
|
context.strokeStyle = "#e11d48";
|
|
92
141
|
context.lineWidth = 3;
|
|
@@ -153,14 +202,14 @@ function sanitizeFeedbackReplay(value) {
|
|
|
153
202
|
if (key === "attributes" && item && typeof item === "object") {
|
|
154
203
|
const attributes = {};
|
|
155
204
|
for (const [attribute, content] of Object.entries(item)) {
|
|
156
|
-
if (/^on|^(?:srcdoc|action|formaction|href|srcset)$/i.test(attribute) || CREDENTIAL_NAME.test(attribute))
|
|
205
|
+
if (/^on|^(?:srcdoc|action|formaction|href|srcset|rr_src)$/i.test(attribute) || CREDENTIAL_NAME.test(attribute))
|
|
157
206
|
continue;
|
|
158
207
|
if (attribute === "src" && typeof content === "string" && !/^data:image\/(?:png|jpeg|webp|gif);base64,/i.test(content)) {
|
|
159
208
|
attributes["data-feedback-missing-resource"] = true;
|
|
160
209
|
continue;
|
|
161
210
|
}
|
|
162
|
-
if (attribute === "style" && typeof content === "string"
|
|
163
|
-
attributes
|
|
211
|
+
if ((attribute === "style" || attribute === "_cssText") && typeof content === "string") {
|
|
212
|
+
attributes[attribute] = sanitizeFeedbackText(content).replace(/(?:url|@import)\s*\([^)]*\)|@import\s+["'][^"']+["']/gi, "");
|
|
164
213
|
attributes["data-feedback-missing-resource"] = true;
|
|
165
214
|
} else {
|
|
166
215
|
attributes[attribute] = visit(content, depth + 1);
|
|
@@ -248,8 +297,36 @@ async function restoreFeedbackCheckpoint(key) {
|
|
|
248
297
|
// src/client/feedback-client-main.ts
|
|
249
298
|
function createFeedbackClient() {
|
|
250
299
|
let config;
|
|
251
|
-
let initializing = false;
|
|
252
300
|
let draft;
|
|
301
|
+
let sessionVersion = 0;
|
|
302
|
+
let policyVerified = false;
|
|
303
|
+
let preparedPage = "";
|
|
304
|
+
let budget = resolveFeedbackBudget();
|
|
305
|
+
const previews = /* @__PURE__ */ new Map();
|
|
306
|
+
let discardOnClose = false;
|
|
307
|
+
let saveController = new AbortController();
|
|
308
|
+
function releasePreviews() {
|
|
309
|
+
for (const preview of previews.values()) URL.revokeObjectURL(preview.url);
|
|
310
|
+
previews.clear();
|
|
311
|
+
}
|
|
312
|
+
function getView() {
|
|
313
|
+
const files = draft?.attachments ?? [];
|
|
314
|
+
for (const [id, preview] of previews) {
|
|
315
|
+
if (!files.some((file) => file.id === id && file.checksum === preview.checksum)) {
|
|
316
|
+
URL.revokeObjectURL(preview.url);
|
|
317
|
+
previews.delete(id);
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
return { attachments: files.map((file) => {
|
|
321
|
+
let preview = previews.get(file.id);
|
|
322
|
+
if (!preview) {
|
|
323
|
+
preview = { checksum: file.checksum, url: URL.createObjectURL(new Blob([new Uint8Array(file.bytes).buffer], { type: file.contentType })) };
|
|
324
|
+
previews.set(file.id, preview);
|
|
325
|
+
}
|
|
326
|
+
return { id: file.id, name: file.name, size: file.size, mimeType: file.contentType, url: preview.url };
|
|
327
|
+
}), evidence: draft?.evidence ?? [], missing: draft?.missing ?? [], budget: { ...budget } };
|
|
328
|
+
}
|
|
329
|
+
let initializing = false;
|
|
253
330
|
let sequence = 0;
|
|
254
331
|
let buffer = [];
|
|
255
332
|
let bufferBytes = 0;
|
|
@@ -265,6 +342,7 @@ function createFeedbackClient() {
|
|
|
265
342
|
let persistTimer;
|
|
266
343
|
let flushTimer;
|
|
267
344
|
let stopReplay;
|
|
345
|
+
let screenshotCaptures = 0;
|
|
268
346
|
let unsubscribeLogger;
|
|
269
347
|
let source = "";
|
|
270
348
|
let keyScreenshots = [];
|
|
@@ -281,10 +359,13 @@ function createFeedbackClient() {
|
|
|
281
359
|
let replayChunks = [];
|
|
282
360
|
function failure(code = "OPERATION_FAILED", cause) {
|
|
283
361
|
const key = code === "NOT_INITIALIZED" ? "feedback_notInitialized" : code === "LIMIT_EXCEEDED" ? "feedback_limitExceeded" : code === "INVALID_INPUT" ? "feedback_invalidInput" : "feedback_operationFailed";
|
|
284
|
-
|
|
362
|
+
const traceId = core.context.get().traceId ?? core.context.get().requestId ?? core.id.uuid();
|
|
363
|
+
const error = { ...HaiFeedbackError[code], traceId, message: feedbackM(key), cause };
|
|
364
|
+
core.logger[cause === void 0 ? "warn" : "error"]("Feedback client operation failed", { module: "feedback", traceId, error });
|
|
365
|
+
return err({ ...error, message: core.error.formatMessage(error) });
|
|
285
366
|
}
|
|
286
367
|
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 };
|
|
368
|
+
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
369
|
}
|
|
289
370
|
function schedulePersist() {
|
|
290
371
|
if (!config?.persistDraft || persistTimer)
|
|
@@ -350,6 +431,10 @@ function createFeedbackClient() {
|
|
|
350
431
|
missing.add("browser-error-windows-truncated");
|
|
351
432
|
}
|
|
352
433
|
schedulePersist();
|
|
434
|
+
appendToDraft(item, size);
|
|
435
|
+
}
|
|
436
|
+
function appendToDraft(item, size) {
|
|
437
|
+
const bounds = limits();
|
|
353
438
|
if (draft && !collectionFinished && !collectionSealed && (Date.now() <= deadline && afterCount < bounds.after || item.kind === "environment") && draft.evidence.length < bounds.events && draftBytes + size <= bounds.bytes) {
|
|
354
439
|
draft.evidence.push(item);
|
|
355
440
|
draftBytes += size;
|
|
@@ -374,7 +459,18 @@ function createFeedbackClient() {
|
|
|
374
459
|
const numeric = [input.maxEvents, input.maxBytes, input.maxAgeMs, input.beforeCount, input.afterCount, input.afterTimeoutMs, input.screenshotTimeoutMs, input.maxKeyScreenshots, input.keyScreenshotThrottleMs];
|
|
375
460
|
if (numeric.some((value) => value !== void 0 && (!Number.isSafeInteger(value) || value < 0)) || input.persistDraft && !input.persistenceKey)
|
|
376
461
|
return failure("INVALID_INPUT");
|
|
462
|
+
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)
|
|
463
|
+
return failure("INVALID_INPUT");
|
|
464
|
+
const initVersion = sessionVersion;
|
|
465
|
+
const policy = input.actions.policy ? await input.actions.policy().catch(() => failure("OPERATION_FAILED")) : ok(input.budget ?? resolveFeedbackBudget());
|
|
466
|
+
if (initVersion !== sessionVersion)
|
|
467
|
+
return failure("NOT_INITIALIZED");
|
|
468
|
+
policyVerified = policy.success;
|
|
469
|
+
budget = FeedbackBudgetSchema.parse(policy.success ? policy.data : input.budget ?? resolveFeedbackBudget());
|
|
470
|
+
if (!policy.success)
|
|
471
|
+
missing.add("server-policy-unavailable");
|
|
377
472
|
config = input;
|
|
473
|
+
saveController = new AbortController();
|
|
378
474
|
closing = false;
|
|
379
475
|
source = `browser:${core.id.uuid()}`;
|
|
380
476
|
storageKey = `${input.appId}:${input.persistenceKey ?? ""}`;
|
|
@@ -412,7 +508,7 @@ function createFeedbackClient() {
|
|
|
412
508
|
unsubscribeLogger = core.logger.observe((entry) => {
|
|
413
509
|
if (entry.context.module === "feedback" || entry.context.feedback === true)
|
|
414
510
|
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 } });
|
|
511
|
+
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
512
|
});
|
|
417
513
|
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
514
|
listen("unhandledrejection", (event) => record({ kind: "error", data: { reason: event.reason } }));
|
|
@@ -469,19 +565,22 @@ function createFeedbackClient() {
|
|
|
469
565
|
async function startReplay(activeConfig) {
|
|
470
566
|
try {
|
|
471
567
|
const { record: start } = await import('@rrweb/record');
|
|
472
|
-
if (closing || config !== activeConfig)
|
|
568
|
+
if (closing || config !== activeConfig || screenshotCaptures || stopReplay)
|
|
473
569
|
return;
|
|
570
|
+
let firstEvent = true;
|
|
474
571
|
stopReplay = start({
|
|
475
572
|
maskAllInputs: true,
|
|
476
573
|
maskTextSelector: "[data-feedback-private],.rr-mask",
|
|
477
574
|
blockSelector: "[data-feedback-ignore],[data-feedback-private],.rr-block,iframe,canvas,video",
|
|
478
|
-
|
|
575
|
+
// rrweb 内联跨域图像会修改原 img 的 crossOrigin,可能破坏带凭据的头像等资源。
|
|
576
|
+
inlineImages: false,
|
|
479
577
|
inlineStylesheet: true,
|
|
480
578
|
collectFonts: false,
|
|
481
579
|
checkoutEveryNms: 3e4,
|
|
482
580
|
emit: (event, isCheckout) => {
|
|
483
|
-
if (isCheckout || replayChunks.length === 0)
|
|
581
|
+
if (firstEvent || isCheckout || replayChunks.length === 0)
|
|
484
582
|
replayChunks.push([]);
|
|
583
|
+
firstEvent = false;
|
|
485
584
|
let payload;
|
|
486
585
|
try {
|
|
487
586
|
payload = sanitizeFeedbackReplay(event);
|
|
@@ -493,6 +592,7 @@ function createFeedbackClient() {
|
|
|
493
592
|
if (!safe.success)
|
|
494
593
|
return;
|
|
495
594
|
replayChunks.at(-1)?.push(safe.data);
|
|
595
|
+
appendToDraft(safe.data, new TextEncoder().encode(JSON.stringify(safe.data)).byteLength);
|
|
496
596
|
while (replayChunks.length > 2)
|
|
497
597
|
replayChunks.shift();
|
|
498
598
|
const count = replayChunks.reduce((total, chunk) => total + chunk.length, 0);
|
|
@@ -515,10 +615,10 @@ function createFeedbackClient() {
|
|
|
515
615
|
lastKeyScreenshot = Date.now();
|
|
516
616
|
keyScreenshotPromise = (async () => {
|
|
517
617
|
try {
|
|
518
|
-
const bytes = await
|
|
618
|
+
const bytes = await takeScreenshot(activeConfig);
|
|
519
619
|
if (closing || config !== activeConfig)
|
|
520
620
|
return;
|
|
521
|
-
if (bytes.byteLength >
|
|
621
|
+
if (bytes.byteLength > budget.maxAttachmentBytes) {
|
|
522
622
|
missing.add("key-screenshot-size-limit-exceeded");
|
|
523
623
|
return;
|
|
524
624
|
}
|
|
@@ -536,6 +636,21 @@ function createFeedbackClient() {
|
|
|
536
636
|
});
|
|
537
637
|
return keyScreenshotPromise;
|
|
538
638
|
}
|
|
639
|
+
function takeScreenshot(activeConfig) {
|
|
640
|
+
screenshotCaptures++;
|
|
641
|
+
stopReplay?.();
|
|
642
|
+
stopReplay = void 0;
|
|
643
|
+
const captured = (async () => {
|
|
644
|
+
try {
|
|
645
|
+
return await (activeConfig.captureScreenshot?.() ?? captureFeedbackScreenshot());
|
|
646
|
+
} finally {
|
|
647
|
+
screenshotCaptures--;
|
|
648
|
+
if (!screenshotCaptures && !closing && config === activeConfig && activeConfig.replay)
|
|
649
|
+
await startReplay(activeConfig);
|
|
650
|
+
}
|
|
651
|
+
})();
|
|
652
|
+
return bounded(captured, activeConfig.screenshotTimeoutMs ?? 3e3);
|
|
653
|
+
}
|
|
539
654
|
async function doPrepare() {
|
|
540
655
|
if (!config || closing)
|
|
541
656
|
return failure("NOT_INITIALIZED");
|
|
@@ -550,8 +665,14 @@ function createFeedbackClient() {
|
|
|
550
665
|
if (closing || config !== activeConfig)
|
|
551
666
|
return failure("NOT_INITIALIZED");
|
|
552
667
|
}
|
|
553
|
-
if (draft && !collectionFinished)
|
|
668
|
+
if (draft && !collectionFinished) {
|
|
669
|
+
if (!draft.submission && config.screenshot !== false && preparedPage !== pageIdentity()) {
|
|
670
|
+
const captured = await captureScreenshot();
|
|
671
|
+
if (!captured.success)
|
|
672
|
+
return captured;
|
|
673
|
+
}
|
|
554
674
|
return ok(draft);
|
|
675
|
+
}
|
|
555
676
|
collectionFinished = false;
|
|
556
677
|
collectionSealed = false;
|
|
557
678
|
initialIds.clear();
|
|
@@ -578,7 +699,7 @@ function createFeedbackClient() {
|
|
|
578
699
|
if (closing || config !== activeConfig)
|
|
579
700
|
return failure("NOT_INITIALIZED");
|
|
580
701
|
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:
|
|
702
|
+
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
703
|
while (draft.evidence.length > limits().events || new TextEncoder().encode(JSON.stringify(draft.evidence)).byteLength > limits().bytes) {
|
|
583
704
|
draft.evidence.shift();
|
|
584
705
|
missing.add("browser-draft-truncated");
|
|
@@ -588,30 +709,24 @@ function createFeedbackClient() {
|
|
|
588
709
|
draft.missing.push(`browser-buffer-truncated:${dropped}`);
|
|
589
710
|
afterCount = frozenWindow?.after ?? (anchor ? Math.max(0, logs.length - anchorIndex - 1) : 0);
|
|
590
711
|
deadline = (anchor?.timestamp ?? now) + (config.afterTimeoutMs ?? 3e4);
|
|
591
|
-
|
|
712
|
+
recordEnvironment();
|
|
592
713
|
if (config.screenshot !== false) {
|
|
593
714
|
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)}`);
|
|
715
|
+
const captured = await captureScreenshot(void 0, "screenshot.png");
|
|
716
|
+
if (closing || config !== activeConfig || draft !== current)
|
|
717
|
+
return failure("NOT_INITIALIZED");
|
|
718
|
+
if (!captured.success) {
|
|
719
|
+
current.missing.push(captured.error.code === HaiFeedbackError.LIMIT_EXCEEDED.code ? "screenshot-size-limit-exceeded" : "screenshot-unavailable");
|
|
720
|
+
current.missing.push(`screenshot-error:${captured.error.message.slice(0, 300)}`);
|
|
612
721
|
}
|
|
613
722
|
}
|
|
723
|
+
for (const { attachment: image } of keyScreenshots) {
|
|
724
|
+
if (image.size > budget.maxAttachmentBytes || draft.attachments.length >= budget.maxAttachmentCount || draft.attachments.reduce((sum, file) => sum + file.size, image.size) > budget.maxAttachmentsBytes)
|
|
725
|
+
draft.missing.push("key-screenshot-budget-exceeded");
|
|
726
|
+
else draft.attachments.push(image);
|
|
727
|
+
}
|
|
614
728
|
schedulePersist();
|
|
729
|
+
preparedPage = pageIdentity();
|
|
615
730
|
return ok(draft);
|
|
616
731
|
}
|
|
617
732
|
function prepare() {
|
|
@@ -620,6 +735,52 @@ function createFeedbackClient() {
|
|
|
620
735
|
});
|
|
621
736
|
return preparePromise;
|
|
622
737
|
}
|
|
738
|
+
function recordEnvironment() {
|
|
739
|
+
if (!config)
|
|
740
|
+
return;
|
|
741
|
+
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 ?? []) } });
|
|
742
|
+
}
|
|
743
|
+
function pageIdentity() {
|
|
744
|
+
return `${location.origin}${location.pathname}${location.hash.startsWith("#/") ? location.hash.split("?")[0] : ""}`;
|
|
745
|
+
}
|
|
746
|
+
async function captureScreenshot(attachmentId, name) {
|
|
747
|
+
if (!config || !draft || closing)
|
|
748
|
+
return failure("NOT_INITIALIZED");
|
|
749
|
+
const current = draft;
|
|
750
|
+
const activeConfig = config;
|
|
751
|
+
const previous = attachmentId ? current.attachments.find((item) => item.id === attachmentId) : void 0;
|
|
752
|
+
const previousChecksum = previous?.checksum;
|
|
753
|
+
if (current.submission)
|
|
754
|
+
return err(HaiFeedbackError.CONFLICT, feedbackM("feedback_conflict"));
|
|
755
|
+
if (attachmentId && (!previous || !previous.contentType.startsWith("image/")))
|
|
756
|
+
return failure("INVALID_INPUT");
|
|
757
|
+
if (!previous && current.attachments.length >= budget.maxAttachmentCount)
|
|
758
|
+
return failure("LIMIT_EXCEEDED");
|
|
759
|
+
try {
|
|
760
|
+
const bytes = await takeScreenshot(activeConfig);
|
|
761
|
+
if (bytes.byteLength > budget.maxAttachmentBytes)
|
|
762
|
+
return failure("LIMIT_EXCEEDED");
|
|
763
|
+
const previousName = previous && (/\.[^.]+$/.test(previous.name) ? previous.name.replace(/\.[^.]+$/, ".png") : `${previous.name}.png`);
|
|
764
|
+
const image = await attachment(name ?? previousName ?? `screenshot-${core.id.uuid()}.png`, "image/png", bytes);
|
|
765
|
+
if (closing || config !== activeConfig || draft !== current || current.submission || previous && (!current.attachments.includes(previous) || previous.checksum !== previousChecksum))
|
|
766
|
+
return err(HaiFeedbackError.CONFLICT, feedbackM("feedback_conflict"));
|
|
767
|
+
if (current.attachments.reduce((total, item) => total + (item === previous ? 0 : item.size), image.size) > budget.maxAttachmentsBytes)
|
|
768
|
+
return failure("LIMIT_EXCEEDED");
|
|
769
|
+
if (previous)
|
|
770
|
+
current.attachments = current.attachments.map((item) => item === previous ? { ...image, id: previous.id } : item);
|
|
771
|
+
else if (current.attachments.length < budget.maxAttachmentCount)
|
|
772
|
+
current.attachments.unshift(image);
|
|
773
|
+
else
|
|
774
|
+
return failure("LIMIT_EXCEEDED");
|
|
775
|
+
if (!name)
|
|
776
|
+
recordEnvironment();
|
|
777
|
+
preparedPage = pageIdentity();
|
|
778
|
+
schedulePersist();
|
|
779
|
+
return ok(void 0);
|
|
780
|
+
} catch (cause) {
|
|
781
|
+
return failure("OPERATION_FAILED", cause);
|
|
782
|
+
}
|
|
783
|
+
}
|
|
623
784
|
async function submit(input = {}) {
|
|
624
785
|
if (!config || closing)
|
|
625
786
|
return failure("NOT_INITIALIZED");
|
|
@@ -629,6 +790,23 @@ function createFeedbackClient() {
|
|
|
629
790
|
return prepared;
|
|
630
791
|
const current = prepared.data;
|
|
631
792
|
try {
|
|
793
|
+
if (!policyVerified && activeConfig.actions.policy) {
|
|
794
|
+
const policy = await activeConfig.actions.policy();
|
|
795
|
+
if (closing || config !== activeConfig || draft !== current)
|
|
796
|
+
return failure("NOT_INITIALIZED");
|
|
797
|
+
if (!policy.success)
|
|
798
|
+
return policy;
|
|
799
|
+
budget = FeedbackBudgetSchema.parse(policy.data);
|
|
800
|
+
policyVerified = true;
|
|
801
|
+
missing.delete("server-policy-unavailable");
|
|
802
|
+
current.missing = current.missing.filter((reason) => reason !== "server-policy-unavailable");
|
|
803
|
+
while (current.evidence.length && (current.evidence.length > limits().events || new TextEncoder().encode(JSON.stringify(current.evidence)).byteLength > limits().bytes)) {
|
|
804
|
+
current.evidence.shift();
|
|
805
|
+
if (!current.missing.includes("browser-draft-truncated"))
|
|
806
|
+
current.missing.push("browser-draft-truncated");
|
|
807
|
+
}
|
|
808
|
+
draftBytes = new TextEncoder().encode(JSON.stringify(current.evidence)).byteLength;
|
|
809
|
+
}
|
|
632
810
|
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
811
|
if (closing || config !== activeConfig || draft !== current)
|
|
634
812
|
return failure("NOT_INITIALIZED");
|
|
@@ -689,7 +867,7 @@ function createFeedbackClient() {
|
|
|
689
867
|
if (pending.some((item) => !initialIds.has(item.id)))
|
|
690
868
|
return failure();
|
|
691
869
|
drainPasses++;
|
|
692
|
-
if (drainPasses >=
|
|
870
|
+
if (drainPasses >= budget.maxAttachmentCount)
|
|
693
871
|
return ok(void 0);
|
|
694
872
|
}
|
|
695
873
|
if (collectionSealed) {
|
|
@@ -727,6 +905,8 @@ function createFeedbackClient() {
|
|
|
727
905
|
const bytes = await annotateFeedbackImage(originalBytes, mark);
|
|
728
906
|
const name = /\.[^.]+$/.test(item.name) ? item.name.replace(/\.[^.]+$/, ".png") : `${item.name}.png`;
|
|
729
907
|
const updated = await attachment(name, "image/png", bytes);
|
|
908
|
+
if (updated.size > budget.maxAttachmentBytes || current.attachments.reduce((sum, file) => sum + (file === item ? 0 : file.size), updated.size) > budget.maxAttachmentsBytes)
|
|
909
|
+
return failure("LIMIT_EXCEEDED");
|
|
730
910
|
if (closing || draft !== current || current.submission || !current.attachments.includes(item) || item.bytes !== originalBytes)
|
|
731
911
|
return err(HaiFeedbackError.CONFLICT, feedbackM("feedback_conflict"));
|
|
732
912
|
Object.assign(item, updated, { id });
|
|
@@ -736,19 +916,19 @@ function createFeedbackClient() {
|
|
|
736
916
|
return failure("OPERATION_FAILED", cause);
|
|
737
917
|
}
|
|
738
918
|
}
|
|
739
|
-
async function addFiles(files) {
|
|
919
|
+
async function addFiles(files, removedAttachmentIds = []) {
|
|
740
920
|
if (!draft)
|
|
741
921
|
return failure("NOT_INITIALIZED");
|
|
742
|
-
if (draft.submission && files.length)
|
|
922
|
+
if (draft.submission && (files.length || removedAttachmentIds.length))
|
|
743
923
|
return err(HaiFeedbackError.CONFLICT, feedbackM("feedback_retryInputChanged"));
|
|
744
924
|
const current = draft;
|
|
745
925
|
const items = [];
|
|
746
926
|
const newMissing = [];
|
|
747
927
|
try {
|
|
748
928
|
for (const file of files) {
|
|
749
|
-
if (items.length >=
|
|
929
|
+
if (items.length >= budget.maxAttachmentCount || file.size > budget.maxAttachmentBytes)
|
|
750
930
|
return failure("LIMIT_EXCEEDED");
|
|
751
|
-
if (!["image/png", "image/jpeg", "image/webp", "text/plain", "application/json"].includes(file.type))
|
|
931
|
+
if (!["image/png", "image/jpeg", "image/webp", "video/webm", "video/mp4", "text/plain", "application/json"].includes(file.type))
|
|
752
932
|
return failure("INVALID_INPUT");
|
|
753
933
|
let bytes;
|
|
754
934
|
if (file.type.startsWith("text/") || file.type === "application/json") {
|
|
@@ -759,7 +939,7 @@ function createFeedbackClient() {
|
|
|
759
939
|
} else {
|
|
760
940
|
bytes = new Uint8Array(await file.arrayBuffer());
|
|
761
941
|
}
|
|
762
|
-
if (bytes.byteLength >
|
|
942
|
+
if (bytes.byteLength > budget.maxAttachmentBytes)
|
|
763
943
|
return failure("LIMIT_EXCEEDED");
|
|
764
944
|
const item = await attachment(file.name, file.type, bytes);
|
|
765
945
|
if (!items.some((existing) => existing.name === item.name && existing.checksum === item.checksum))
|
|
@@ -767,14 +947,14 @@ function createFeedbackClient() {
|
|
|
767
947
|
}
|
|
768
948
|
if (draft !== current || closing)
|
|
769
949
|
return err(HaiFeedbackError.CONFLICT, feedbackM("feedback_conflict"));
|
|
770
|
-
if (current.submission && files.length)
|
|
950
|
+
if (current.submission && (files.length || removedAttachmentIds.length))
|
|
771
951
|
return err(HaiFeedbackError.CONFLICT, feedbackM("feedback_retryInputChanged"));
|
|
772
|
-
const merged =
|
|
952
|
+
const merged = current.attachments.filter((file) => !removedAttachmentIds.includes(file.id));
|
|
773
953
|
for (const item of items) {
|
|
774
954
|
if (!merged.some((existing) => existing.name === item.name && existing.checksum === item.checksum))
|
|
775
955
|
merged.push(item);
|
|
776
956
|
}
|
|
777
|
-
if (merged.length >
|
|
957
|
+
if (merged.length > budget.maxAttachmentCount || merged.reduce((total, item) => total + item.size, 0) > budget.maxAttachmentsBytes)
|
|
778
958
|
return failure("LIMIT_EXCEEDED");
|
|
779
959
|
current.attachments = merged;
|
|
780
960
|
current.missing.push(...newMissing);
|
|
@@ -797,24 +977,26 @@ function createFeedbackClient() {
|
|
|
797
977
|
attachments.splice(0, attachments.length, ...draft.attachments);
|
|
798
978
|
} else {
|
|
799
979
|
for (const file of input.files ?? []) {
|
|
800
|
-
if (attachments.length >=
|
|
980
|
+
if (attachments.length >= budget.maxAttachmentCount || file.size > budget.maxAttachmentBytes)
|
|
801
981
|
return failure("LIMIT_EXCEEDED");
|
|
802
|
-
if (!["image/png", "image/jpeg", "image/webp", "text/plain", "application/json"].includes(file.type))
|
|
982
|
+
if (!["image/png", "image/jpeg", "image/webp", "video/webm", "video/mp4", "text/plain", "application/json"].includes(file.type))
|
|
803
983
|
return failure("INVALID_INPUT");
|
|
804
984
|
const bytes = file.type === "application/json" ? new TextEncoder().encode(JSON.stringify(sanitizeFeedbackData(JSON.parse(await file.text())))) : file.type.startsWith("text/") ? new TextEncoder().encode(sanitizeFeedbackText(await file.text())) : new Uint8Array(await file.arrayBuffer());
|
|
805
|
-
if (bytes.byteLength >
|
|
985
|
+
if (bytes.byteLength > budget.maxAttachmentBytes)
|
|
806
986
|
return failure("LIMIT_EXCEEDED");
|
|
807
987
|
const item = await attachment(file.name, file.type, bytes);
|
|
808
988
|
if (!attachments.some((existing) => existing.name === item.name && existing.checksum === item.checksum))
|
|
809
989
|
attachments.push(item);
|
|
810
990
|
}
|
|
811
991
|
}
|
|
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"] : []])] });
|
|
992
|
+
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
993
|
} catch (cause) {
|
|
814
994
|
return failure("OPERATION_FAILED", cause);
|
|
815
995
|
}
|
|
816
996
|
}
|
|
817
|
-
function close() {
|
|
997
|
+
function close(options = {}) {
|
|
998
|
+
sessionVersion++;
|
|
999
|
+
discardOnClose ||= options.discard === true;
|
|
818
1000
|
closePromise ??= doClose().finally(() => {
|
|
819
1001
|
closePromise = void 0;
|
|
820
1002
|
});
|
|
@@ -822,6 +1004,7 @@ function createFeedbackClient() {
|
|
|
822
1004
|
}
|
|
823
1005
|
async function doClose() {
|
|
824
1006
|
closing = true;
|
|
1007
|
+
saveController.abort();
|
|
825
1008
|
unsubscribeLogger?.();
|
|
826
1009
|
unsubscribeLogger = void 0;
|
|
827
1010
|
stopReplay?.();
|
|
@@ -838,9 +1021,14 @@ function createFeedbackClient() {
|
|
|
838
1021
|
await keyScreenshotPromise;
|
|
839
1022
|
if (draft?.report)
|
|
840
1023
|
await flush();
|
|
841
|
-
if (
|
|
1024
|
+
if (discardOnClose && storageKey)
|
|
1025
|
+
await deleteFeedbackDraft(storageKey).catch(() => void 0);
|
|
1026
|
+
else if (config?.persistDraft && draft && !collectionFinished)
|
|
842
1027
|
await saveFeedbackDraft(storageKey, draft).catch(() => void 0);
|
|
1028
|
+
releasePreviews();
|
|
1029
|
+
discardOnClose = false;
|
|
843
1030
|
config = void 0;
|
|
1031
|
+
preparedPage = "";
|
|
844
1032
|
buffer = [];
|
|
845
1033
|
bufferBytes = 0;
|
|
846
1034
|
draft = void 0;
|
|
@@ -856,7 +1044,17 @@ function createFeedbackClient() {
|
|
|
856
1044
|
collectionFinished = false;
|
|
857
1045
|
collectionSealed = false;
|
|
858
1046
|
}
|
|
859
|
-
return { init, prepare, submit, flush, record, getDraft: () => draft, annotate, addFiles, exportDraft,
|
|
1047
|
+
return { init, prepare, captureScreenshot, submit, flush, record, getDraft: () => draft, getView, releasePreviews, annotate, addFiles, exportDraft, async saveDraft(input) {
|
|
1048
|
+
const session = config;
|
|
1049
|
+
const exported = await exportDraft(input);
|
|
1050
|
+
if (!exported.success)
|
|
1051
|
+
return exported;
|
|
1052
|
+
if (closing || !session || session !== config)
|
|
1053
|
+
return failure("NOT_INITIALIZED");
|
|
1054
|
+
return saveFeedbackFile(exported.data, `feedback-${draft?.submissionId ?? "local"}.zip`, "application/zip", saveController.signal);
|
|
1055
|
+
}, close, get budget() {
|
|
1056
|
+
return { ...budget };
|
|
1057
|
+
} };
|
|
860
1058
|
}
|
|
861
1059
|
var feedbackClient = createFeedbackClient();
|
|
862
1060
|
async function bounded(promise, ms) {
|
|
@@ -883,7 +1081,75 @@ function base64(bytes) {
|
|
|
883
1081
|
value += String.fromCharCode(...bytes.subarray(offset, offset + 32768));
|
|
884
1082
|
return btoa(value);
|
|
885
1083
|
}
|
|
1084
|
+
function createFeedbackReview(actions) {
|
|
1085
|
+
let controller = new AbortController();
|
|
1086
|
+
const failed = () => err(HaiFeedbackError.OPERATION_FAILED, feedbackM("feedback_operationFailed"));
|
|
1087
|
+
return {
|
|
1088
|
+
close() {
|
|
1089
|
+
controller.abort();
|
|
1090
|
+
controller = new AbortController();
|
|
1091
|
+
},
|
|
1092
|
+
async loadReplay(id) {
|
|
1093
|
+
const signal = controller.signal;
|
|
1094
|
+
const events = [];
|
|
1095
|
+
try {
|
|
1096
|
+
for (let page = 1; page <= 100; page++) {
|
|
1097
|
+
const result = await actions.getEvidence({ id, kind: "replay", page, pageSize: 100 });
|
|
1098
|
+
if (signal.aborted)
|
|
1099
|
+
return failed();
|
|
1100
|
+
if (!result.success)
|
|
1101
|
+
return result;
|
|
1102
|
+
events.push(...result.data.items);
|
|
1103
|
+
if (events.length >= result.data.total || !result.data.items.length)
|
|
1104
|
+
return ok(events);
|
|
1105
|
+
}
|
|
1106
|
+
} catch {
|
|
1107
|
+
return failed();
|
|
1108
|
+
}
|
|
1109
|
+
return failed();
|
|
1110
|
+
},
|
|
1111
|
+
async exportReport(id) {
|
|
1112
|
+
const signal = controller.signal;
|
|
1113
|
+
try {
|
|
1114
|
+
const started = await actions.export({ id });
|
|
1115
|
+
if (!started.success)
|
|
1116
|
+
return started;
|
|
1117
|
+
for (let attempt = 0; attempt < 120; attempt++) {
|
|
1118
|
+
if (signal.aborted)
|
|
1119
|
+
return failed();
|
|
1120
|
+
const detail = await actions.get({ id });
|
|
1121
|
+
if (!detail.success)
|
|
1122
|
+
return detail;
|
|
1123
|
+
if (signal.aborted)
|
|
1124
|
+
return failed();
|
|
1125
|
+
const job = detail.data.jobs.find((job2) => job2.id === started.data.id);
|
|
1126
|
+
if (!job || job.status === "failed")
|
|
1127
|
+
return failed();
|
|
1128
|
+
if (job.status === "succeeded") {
|
|
1129
|
+
const file = await actions.getExport({ id, jobId: job.id });
|
|
1130
|
+
if (signal.aborted)
|
|
1131
|
+
return failed();
|
|
1132
|
+
return file.success ? saveFeedbackFile(feedbackDataBytes(file.data), file.data.name, file.data.contentType, signal) : file;
|
|
1133
|
+
}
|
|
1134
|
+
await new Promise((resolve) => {
|
|
1135
|
+
let timer;
|
|
1136
|
+
const done = () => {
|
|
1137
|
+
clearTimeout(timer);
|
|
1138
|
+
signal.removeEventListener("abort", done);
|
|
1139
|
+
resolve();
|
|
1140
|
+
};
|
|
1141
|
+
timer = setTimeout(done, 500);
|
|
1142
|
+
signal.addEventListener("abort", done, { once: true });
|
|
1143
|
+
});
|
|
1144
|
+
}
|
|
1145
|
+
} catch {
|
|
1146
|
+
return failed();
|
|
1147
|
+
}
|
|
1148
|
+
return failed();
|
|
1149
|
+
}
|
|
1150
|
+
};
|
|
1151
|
+
}
|
|
886
1152
|
|
|
887
|
-
export { createFeedbackClient, feedbackClient };
|
|
1153
|
+
export { configureFeedbackSaving, createFeedbackClient, createFeedbackReview, feedbackClient, feedbackDataBytes, saveFeedbackFile };
|
|
888
1154
|
//# sourceMappingURL=index.js.map
|
|
889
1155
|
//# sourceMappingURL=index.js.map
|