@h-ai/feedback 0.1.0-alpha.73 → 0.1.0-alpha.75
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 +72 -5
- package/README.ja.md +72 -5
- package/README.md +72 -5
- package/dist/bundle/index.d.ts +2 -0
- package/dist/bundle/index.js +1 -1
- package/dist/{chunk-PYKGS35A.js → chunk-7W47WZXY.js} +13 -9
- package/dist/chunk-7W47WZXY.js.map +1 -0
- package/dist/{chunk-5QWKURWU.js → chunk-KGMLTOMM.js} +5 -3
- package/dist/chunk-KGMLTOMM.js.map +1 -0
- package/dist/client/index.d.ts +12 -2
- package/dist/client/index.js +117 -19
- package/dist/client/index.js.map +1 -1
- package/dist/index.d.ts +82 -3
- package/dist/index.js +480 -75
- package/dist/index.js.map +1 -1
- package/package.json +7 -7
- package/dist/chunk-5QWKURWU.js.map +0 -1
- package/dist/chunk-PYKGS35A.js.map +0 -1
package/README.en.md
CHANGED
|
@@ -12,7 +12,7 @@ Collect user feedback and diagnostic evidence, notify administrators, review rep
|
|
|
12
12
|
|
|
13
13
|
- Bounded browser/server logs, requests, errors, interactions, versions and allowed environment fields; up to 100 log records before and after an error by default.
|
|
14
14
|
- Transactional intake, stable submission/evidence identifiers, checksum validation, independent processing/collection/notification state and durable leased jobs.
|
|
15
|
-
- `notificationMode` independently selects `disabled` (no notification jobs), `console` (
|
|
15
|
+
- `notificationMode` independently selects `disabled` (no notification jobs), `console` (metadata only by default, including production, without Reach/email; consoleContent: summary explicitly enables sanitized summaries), or `reach` (any configured provider and destinations; only this mode requires recipients). The default remains `reach` for existing configurations; generated projects explicitly select `console`. During Reach DND, the feedback outbox waits without adding a second delayed queue or changing other notification policy. Production Reach Console restrictions remain enforced. Sent means channel acceptance, not inbox delivery.
|
|
16
16
|
- Browser screenshots, irreversible masking, annotations, optional rrweb recording, retries and local fallback export; `FeedbackModal`, `FeedbackList` and `FeedbackDetail` in `@h-ai/ui`.
|
|
17
17
|
- Self-contained ZIP files, SHA-256 validation, an offline viewer and paginated evidence queries without production credentials.
|
|
18
18
|
|
|
@@ -54,11 +54,11 @@ Browser and server text redaction share rules for common credential keys, JSON/e
|
|
|
54
54
|
|
|
55
55
|
`text/plain` attachments are redacted in full within the 5 MiB file limit, preserving safe content and trailing log entries without structured evidence's per-field truncation. Oversized original files are rejected before reading, and redacted output remains subject to the same attachment byte limit. JSON attachments and structured evidence retain field length, depth and count limits and may be trimmed; use plain text for complete logs. The same rules apply to pending attachments and files added to a local export after the first submission envelope is frozen.
|
|
56
56
|
|
|
57
|
-
The complete client exposes `init(config)`, `prepare/captureScreenshot(attachmentId?)/submit/flush/record/getDraft/getView/annotate/addFiles/exportDraft/saveDraft/close`. Connect form `onfileschange` to `addFiles` and refresh draft attachment previews so new images can be marked, masked or removed before submission. Use `data-feedback-action` for key interactions, `data-feedback-private` for sensitive content and `data-feedback-ignore` for the feedback UI itself. The first transmitted envelope is frozen; changing input during a retry returns a conflict and preserves the input for local export.
|
|
57
|
+
The complete client exposes `init(config)`, `prepare/captureScreenshot(attachmentId?)/submit/flush/record/getDraft/getView/annotate/addFiles/exportDraft/saveDraft/pause/close`. Connect form `onfileschange` to `addFiles` and refresh draft attachment previews so new images can be marked, masked or removed before submission. Use `data-feedback-action` for key interactions, `data-feedback-private` for sensitive content and `data-feedback-ignore` for the feedback UI itself. The first transmitted envelope is frozen; changing input during a retry returns a conflict and preserves the input for local export.
|
|
58
58
|
|
|
59
59
|
`addFiles` adds each batch atomically: all files must pass validation before merging. Any failure preserves existing attachments and missing markers without saving a partial batch. Successful concurrent batches preserve each other's files, with at most 20 merged attachments. If the draft changes or the first envelope freezes during reading, it returns `CONFLICT` without adding hidden uploads outside that envelope.
|
|
60
60
|
|
|
61
|
-
Local attachments support PNG/JPEG/WebP, WebM/MP4, TXT/LOG, JSON and
|
|
61
|
+
Local attachments support PNG/JPEG/WebP, WebM/MP4, TXT/LOG, JSON, PDF and ZIP/GZIP. Missing MIME types and `application/octet-stream` are inferred from supported extensions; common MIME aliases are normalized. The server still validates file bytes and checksums. Unsupported formats show a specific message. Screenshots preserve ordinary controls' backgrounds and borders while replacing their values with fixed mask characters. Passwords, `data-feedback-private`, `.rr-block` and `.rr-mask` remain fully covered. Only the capture clone changes; the original page and values remain intact.
|
|
62
62
|
|
|
63
63
|
`annotate(id, { ...rect, mode, coordinateSpace: 'ratio' })` accepts UI 0–1 coordinates directly. The SDK converts decoded intrinsic dimensions, including fractional/tiny selections, reverse dragging, edges and CSS scaling. Omitting coordinateSpace retains pixel coordinates. `getView()` owns and reuses preview URLs, releasing them on replacement, removal and close. Hosts need no image decoding or object URL bookkeeping.
|
|
64
64
|
|
|
@@ -66,9 +66,11 @@ Concurrent `init` returns `INIT_IN_PROGRESS`; `close` drains bounded screenshots
|
|
|
66
66
|
|
|
67
67
|
The SDK owns the draft and updates it through methods such as `annotate` and `addFiles`. Svelte hosts should keep this reference in `$state.raw<FeedbackDraft>()` and refresh attachment views from `getDraft()` after successful operations. A deep `$state` proxy can retain cached image bytes after the SDK replaces them on the original draft.
|
|
68
68
|
|
|
69
|
-
`maxTotalBytes` defaults to 32 MiB (4–40 MiB). `feedback.budget` derives individual/total attachment, duplicate evidence and Base64/encrypted transport budgets from it. Defaults are 5 MiB per file, 22 MiB for attachments, 4 MiB/2000 evidence records, plus 2 MiB for history, metadata and the viewer. Submission validates declared totals; evidence updates enforce quotas atomically; processing history is limited to 256 KiB. The authenticated `reports.policy({})` supplies effective limits to the client and form.
|
|
69
|
+
`maxTotalBytes` defaults to 32 MiB (4–40 MiB). `feedback.budget` derives individual/total attachment, duplicate evidence and Base64/encrypted transport budgets from it. Defaults are 5 MiB per file, 22 MiB for attachments, 4 MiB/2000 evidence records, plus 2 MiB for history, metadata and the viewer. Submission validates declared totals; evidence updates enforce quotas atomically; processing history is limited to 256 KiB. The authenticated `reports.policy({})` supplies effective limits to the client and form.
|
|
70
70
|
|
|
71
|
-
|
|
71
|
+
Ordinary routes retain `transport.maxPlaintextBytes` (auto/omitted: 1 MiB). Declare Feedback report intake, evidence, attachments and ZIP download capacity separately using `routePlaintextBytes`. At startup, derive capacity with `resolveFeedbackBudget(validatedFeedbackConfig).maxTransportBytes` from `@h-ai/api-contract/feedback`, independently of optional Feedback initialization/retries; `*` matches one complete path segment and explicit host route values take precedence. Handshakes, adapter-node and proxies use the maximum route capacity. HTTP adapters admit at most 2 explicitly declared material requests above 1 MiB and a separate 16 ordinary business requests, releasing admission after the entire response or failure. Busy admission returns 503 / `hai:crypto:072`; business writes are not automatically replayed.
|
|
72
|
+
|
|
73
|
+
`keyScreenshots` defaults to false; automatic key screenshots require opt-in, while manual `prepare()` can still capture. Timeouts propagate AbortSignal and actual unfinished rendering retains the single-flight lock. The default renderer caps DOM traversal at 10000 nodes alongside its canvas pixel budget. With `replay:true`, `replayStart` defaults to `prepare`; pre-recording requires explicit `replayStart: init`. Call `pause()` when closing feedback to stop DOM replay/screenshots while retaining lightweight logs, drafts and uploads. Identity exit still calls `close({ discard:true })`. Custom `captureScreenshot({ signal })` should honor cancellation.
|
|
72
74
|
|
|
73
75
|
Automatic screenshots reconstruct the visible page from a redacted DOM clone through SVG foreignObject. Cross-origin images, iframe/video content and network fonts that are not ready may be missing. Browser or CSP failures are recorded as missing, and users can still attach images manually.
|
|
74
76
|
|
|
@@ -90,6 +92,57 @@ Console/disabled do not require the Reach package. Reach mode explicitly install
|
|
|
90
92
|
|
|
91
93
|
Web clients never resolve or install native plugins. Custom hosts import saveTauriFeedbackFile from @h-ai/feedback/tauri or saveCapacitorFeedbackFile from @h-ai/feedback/capacitor and call configureFeedbackSaving(saver) at startup. Generated hosts already configure this; business code does not handle native file lifecycles.
|
|
92
94
|
|
|
95
|
+
### Business evidence extension
|
|
96
|
+
|
|
97
|
+
Register a server callback with `feedback.init(config, { collectEvidence })`. A durable `collect` job invokes it after intake commits and awaits synchronous or asynchronous results without holding up submission. All instances handling the same appId must register the same callback so jobs can recover after restart or takeover.
|
|
98
|
+
|
|
99
|
+
This example runs after dependencies initialize. The host supplies `FeedbackConfigInput` and prepares a sanitized `diagnostics/sanitized-logs.zip`; the callback can instead await the application's own diagnostic service:
|
|
100
|
+
|
|
101
|
+
```ts
|
|
102
|
+
import type { FeedbackConfigInput } from '@h-ai/feedback'
|
|
103
|
+
import { readFile } from 'node:fs/promises'
|
|
104
|
+
import { feedback } from '@h-ai/feedback'
|
|
105
|
+
|
|
106
|
+
async function startFeedback(config: FeedbackConfigInput) {
|
|
107
|
+
const result = await feedback.init(config, {
|
|
108
|
+
collectEvidence: async ({ report, signal }) => ({
|
|
109
|
+
text: `Diagnostics for ${report.number}`,
|
|
110
|
+
data: { businessSystem: 'orders', scopeId: report.scopeId },
|
|
111
|
+
attachments: [{
|
|
112
|
+
id: 'business-logs',
|
|
113
|
+
name: 'business-logs.zip',
|
|
114
|
+
contentType: 'application/zip',
|
|
115
|
+
bytes: await readFile('diagnostics/sanitized-logs.zip', { signal }),
|
|
116
|
+
}],
|
|
117
|
+
}),
|
|
118
|
+
})
|
|
119
|
+
if (!result.success)
|
|
120
|
+
return result
|
|
121
|
+
return result
|
|
122
|
+
}
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
Text and structured data become `source: server:business`, `kind: business` evidence, paginated in the Business evidence tab. Associated files are downloadable and included in offline exports. Existing sanitization, field truncation and evidence budgets apply; use `text/plain` attachments for complete logs with full-text redaction. ZIP/GZIP bytes are stored unchanged after signature, size and SHA-256 checks; archives are never unpacked, executed or internally redacted. The application must sanitize their contents before compression. Browser `addFiles` also accepts ZIP, GZ and TGZ.
|
|
126
|
+
|
|
127
|
+
`businessCollectionTimeoutMs` defaults to 30000ms (1–300000). Timeout and shutdown abort the supplied signal and stop waiting; pass it into asynchronous work. Throws, invalid files, capacity limits and storage failures retry the collect job up to `maxAttempts`, then expose `collectionStatus: failed`; administrators can call `retry({ id, target: 'collect' }, context)`. Callbacks may run again, so use report.id for external idempotency. Files are stored first; evidence and attachment metadata publish in one leased transaction, and a committed success marker prevents recollection. Stable attachment IDs contain 1–100 characters and receive a `server:business:` prefix. All files share the existing 20-item, per-file and total byte budgets.
|
|
128
|
+
|
|
129
|
+
### Webhook callback
|
|
130
|
+
|
|
131
|
+
A trusted `webhookUrl` creates a separate durable webhook job in the intake transaction. It waits for collection to reach complete/partial/failed, then sends `POST application/json` with `feedback.created`, including when collection failed. It is independent of Reach, notificationMode and DND.
|
|
132
|
+
|
|
133
|
+
```yaml
|
|
134
|
+
businessCollectionTimeoutMs: 30000
|
|
135
|
+
webhookUrl: ${HAI_FEEDBACK_WEBHOOKURL:}
|
|
136
|
+
webhookSecret: ${HAI_FEEDBACK_WEBHOOKSECRET:}
|
|
137
|
+
webhookTimeoutMs: ${HAI_FEEDBACK_WEBHOOKTIMEOUTMS:10000}
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
An empty URL disables delivery. Only HTTP(S) URLs without user credentials or fragments are accepted, and redirects are not followed. Configure destinations through trusted server deployment settings; internal URLs support internal integrations. Keep secrets in private server environment settings, never PUBLIC_ variables. The request timeout defaults to 10000ms (1–60000). Non-2xx responses, redirects, network errors and timeouts retry with exponential backoff from 1s to 60s, up to maxAttempts (default 5).
|
|
141
|
+
|
|
142
|
+
`FeedbackWebhookEvent` contains `event/eventId/createdAt/report/evidence/detailUrl?`. Evidence contains its revision, business collection success and attachment metadata, without file bytes, storage keys, configuration secrets or full logs. Read contents through authenticated APIs or an offline bundle. The first delivery freezes the payload; retries and restarts reuse its body and `Idempotency-Key: eventId`. Receivers should persist and deduplicate eventId before returning 2xx. Delivery is at least once: lost acknowledgements can cause duplicates. Changing the configured URL causes older jobs to fail rather than redirect to a different destination; drain them before switching.
|
|
143
|
+
|
|
144
|
+
Requests include `X-Hai-Feedback-Event` and `X-Hai-Feedback-Timestamp` (Unix seconds). With a secret, `X-Hai-Feedback-Signature: sha256=<hex>` is HMAC-SHA256 of `timestamp + '.' + raw request body`. Verify raw bytes, compare in constant time and enforce timestamp tolerance plus eventId deduplication. `get().jobs` exposes status, attempts and safe lastError codes. Admin Console and Fullstack details show Webhook status and administrator retry; programmatic retry uses `retry({ id, target: 'webhook' }, context)`. It is tracked separately from notificationStatus; success means the endpoint returned 2xx.
|
|
145
|
+
|
|
93
146
|
### Reviewing and exporting
|
|
94
147
|
|
|
95
148
|
`list/get/getEvidence` return scoped reports, details and paginated evidence. `setStatus({ id, status, expectedVersion }, context)` protects processing/reopening against concurrent overwrites. `retry` queues recoverable operations. `export` fixes an evidence revision; `getExport` downloads the completed ZIP. Missing/truncated evidence is explicit. Replay displays recorded state and does not reexecute business behavior.
|
|
@@ -115,6 +168,20 @@ The viewer binds only to loopback, uses a random URL and trusted built-in viewer
|
|
|
115
168
|
|
|
116
169
|
Check `success` for every domain result. `HaiFeedbackError` covers initialization, configuration, scope, missing records, conflicts, quota and checksum failures. HTTP 200 and SMTP acceptance do not prove final business success or inbox delivery; uncertain sends can be repeated.
|
|
117
170
|
|
|
171
|
+
### Resource limits and optional availability
|
|
172
|
+
|
|
173
|
+
Server error-window pending evidence is bounded by 3000 records, 8 MiB and five minutes. Idempotent batches contain at most 32 records/256 KiB; database failures back off to 30 seconds and missing markers describe loss. Idle workers back off to 30 seconds; local submit/retry/export wakes them. `workerIntervalMs:0` requires a dedicated driver. Temporary evidence cleanup uses appId, an expiry index and at most 100 rows per batch.
|
|
174
|
+
|
|
175
|
+
Defaults: `maxReportsPerUserDay=20`, `maxBytesPerUserDay=2 GiB`, `maxStoredBytesPerScope=10 GiB`, `maxExportsPerReport=5`, `exportRetentionHours=24`. One shared DB transaction reserves `maxTotalBytes * (maxExportsPerReport + 1)` bytes per report for its materials and historical ZIPs, using the database UTC day and app/scope/user. Report capacity and export count are fixed at intake; raising configuration cannot exceed existing reservations. This is conservative reservation, not live disk accounting. Idempotent retries do not consume another quota; rejection returns `QUOTA_EXCEEDED` (429) without a report/object. Successful report retention cleanup releases scope reservation; daily totals are retained. Expired exports cannot be downloaded and objects are deleted in bounded batches. Frozen snapshots/timestamps and conditional writes reuse one ZIP; business collection freezes its first file manifest before upload. Monitor period=0 scope rows in `hai_feedback_quotas` and pending expiry cleanup jobs.
|
|
176
|
+
|
|
177
|
+
Reference admin/fullstack feedback startup failures leave required business modules ready, retry independently every 30 seconds and suppress unchanged warning spam; policy returns 503 until ready. Required DB/cache/IAM/audit retain their existing failure policy. Do not catch the entire failed runtime.start and continue after its rollback. Stop feedback and its retry before closing borrowed dependencies.
|
|
178
|
+
|
|
179
|
+
### Privacy boundary of console notifications
|
|
180
|
+
|
|
181
|
+
`consoleContent` defaults to `metadata`: report number, application/version/build identity and collection status only. Descriptions, summaries, attachments, evidence, notification bodies and destinations are excluded. Explicit `summary` enables a generically sanitized summary; generic redaction cannot identify every business secret. Reach is not required and disabled/reach channel semantics are unchanged. Existing console configurations become metadata-only by default on upgrade.
|
|
182
|
+
|
|
183
|
+
Server environment evidence commits in the intake transaction. A failed environment write leaves no accepted partial report; retry with the same submissionId. Management screens use `createFeedbackReview(...).retryReport(id)` for recoverable collection and failed notification/Webhook jobs. It deduplicates targets, propagates failures, and stops further orchestration after close.
|
|
184
|
+
|
|
118
185
|
## Development and testing
|
|
119
186
|
|
|
120
187
|
```sh
|
package/README.ja.md
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
|
|
13
13
|
- ブラウザーとサーバーのログ、リクエスト、操作、バージョン、許可された環境情報を上限付きで収集します。既定ではエラーの前後それぞれ最大100件です。
|
|
14
14
|
- トランザクションによる受理、提出と証拠の冪等性、checksum 検証、処理・収集・通知状態の分離、永続ジョブとリース回復。
|
|
15
|
-
- `notificationMode` は独立して選択できます。`disabled` は通知ジョブなし、`console`
|
|
15
|
+
- `notificationMode` は独立して選択できます。`disabled` は通知ジョブなし、`console` は本番でも既定でメタデータだけを出力し Reach/メール不要(consoleContent: summary で一般的なマスキング済み概要を明示)、`reach` は任意の Provider と宛先を使い、このモードのみ recipients が必要です。既定の `reach` は既存設定を維持し、生成プロジェクトは `console` を明示します。Reach DND 中は feedback 自身の outbox で待機し、二重の遅延キューや他通知の方針変更を行いません。本番 Reach Console の制限は維持します。sent はチャネル受付であり受信完了を意味しません。
|
|
16
16
|
- スクリーンショット、復元不能なマスク、注釈、任意の rrweb 記録、再試行とローカル出力。UI は `FeedbackModal`、`FeedbackList`、`FeedbackDetail` を提供します。
|
|
17
17
|
- 自己完結 ZIP、SHA-256 検証、オフライン表示、証拠のページ検索。運用認証情報は含めません。
|
|
18
18
|
|
|
@@ -54,11 +54,11 @@ return submitted
|
|
|
54
54
|
|
|
55
55
|
`text/plain` 添付は 5 MiB のファイル上限内で全文を秘匿処理し、安全な本文と末尾のログを保持します。構造化証拠のフィールド切り捨ては適用しません。上限を超える元ファイルは読取前に拒否し、秘匿処理後も同じ添付バイト上限を適用します。JSON 添付と構造化証拠には文字列長・深さ・件数の上限が残り、切り捨てられる場合があります。完全なログにはプレーンテキスト添付を使用してください。送信前の添付と、初回送信内容の固定後にローカル出力へ追加したファイルに同じ規則を適用します。
|
|
56
56
|
|
|
57
|
-
クライアントの全操作は `init(config)`、`prepare/captureScreenshot(attachmentId?)/submit/flush/record/getDraft/getView/annotate/addFiles/exportDraft/saveDraft/close` です。フォームの `onfileschange` を `addFiles` に接続し、下書きの添付プレビューを更新すると、追加画像を送信前に注釈・遮蔽・削除できます。重要操作は `data-feedback-action`、機密部分は `data-feedback-private`、フィードバック画面自体の除外は `data-feedback-ignore` で指定します。初回送信後の内容は固定され、再試行中の入力変更は競合として入力を保持し、ローカル保存できます。
|
|
57
|
+
クライアントの全操作は `init(config)`、`prepare/captureScreenshot(attachmentId?)/submit/flush/record/getDraft/getView/annotate/addFiles/exportDraft/saveDraft/pause/close` です。フォームの `onfileschange` を `addFiles` に接続し、下書きの添付プレビューを更新すると、追加画像を送信前に注釈・遮蔽・削除できます。重要操作は `data-feedback-action`、機密部分は `data-feedback-private`、フィードバック画面自体の除外は `data-feedback-ignore` で指定します。初回送信後の内容は固定され、再試行中の入力変更は競合として入力を保持し、ローカル保存できます。
|
|
58
58
|
|
|
59
59
|
`addFiles` は一括追加を原子的に行い、全ファイルの検証に成功してから統合します。一つでも失敗した場合、既存の添付と欠落マーカーを維持し、一部だけを保存しません。並行して成功した追加を上書きせず、統合後も最大 20 件です。読取中に下書きが変わるか初回送信内容が固定された場合は `CONFLICT` を返し、送信内容に含まれない添付を後から隠れてアップロードしません。
|
|
60
60
|
|
|
61
|
-
ローカル添付は PNG/JPEG/WebP、WebM/MP4、TXT/LOG、JSON、PDF に対応します。MIME が空または `application/octet-stream` の場合は対応する拡張子で補い、一般的な MIME 別名も正規化します。サーバーは実際のバイトとチェックサムを引き続き検証し、未対応形式には具体的な案内を表示します。通常の入力欄は背景と枠を保持し、値だけを固定マスク文字に置き換えます。パスワード、`data-feedback-private`、`.rr-block`、`.rr-mask` は全面を遮蔽します。撮影用クローンだけを変更し、元の画面と入力値は保持します。
|
|
61
|
+
ローカル添付は PNG/JPEG/WebP、WebM/MP4、TXT/LOG、JSON、PDF、ZIP/GZIP に対応します。MIME が空または `application/octet-stream` の場合は対応する拡張子で補い、一般的な MIME 別名も正規化します。サーバーは実際のバイトとチェックサムを引き続き検証し、未対応形式には具体的な案内を表示します。通常の入力欄は背景と枠を保持し、値だけを固定マスク文字に置き換えます。パスワード、`data-feedback-private`、`.rr-block`、`.rr-mask` は全面を遮蔽します。撮影用クローンだけを変更し、元の画面と入力値は保持します。
|
|
62
62
|
|
|
63
63
|
`annotate(id, { ...rect, mode, coordinateSpace: 'ratio' })` は UI の 0–1 座標を直接受け取り、SDK がデコード後の元画像サイズで換算します。小数、1% 未満、逆方向、端や CSS 拡大縮小に対応します。省略時は従来のピクセル座標です。`getView()` がプレビュー URL を管理し、置換・削除・終了時に解放するため、ホストの画像デコードや URL 管理は不要です。
|
|
64
64
|
|
|
@@ -66,9 +66,11 @@ return submitted
|
|
|
66
66
|
|
|
67
67
|
下書きは SDK が保持し、`annotate` や `addFiles` で更新します。Svelte 側では参照を `$state.raw<FeedbackDraft>()` に保持し、操作成功後に `getDraft()` から添付ビューを更新してください。深い `$state` プロキシに格納すると、SDK が元の下書きの画像バイトを置き換えても、キャッシュされた古いバイトが返る場合があります。
|
|
68
68
|
|
|
69
|
-
`maxTotalBytes` の既定値は 32 MiB(4–40 MiB)です。`feedback.budget` から単一・合計添付、二重保存する証拠と Base64/暗号化転送の上限を導出します。既定は各 5 MiB、添付合計 22 MiB、証拠 4 MiB/2000 件、履歴・メタデータ・ビューアー用 2 MiB です。受理前に添付宣言の合計を検証し、証拠更新は原子的に制限、処理履歴は 256 KiB までです。認証済み `reports.policy({})`
|
|
69
|
+
`maxTotalBytes` の既定値は 32 MiB(4–40 MiB)です。`feedback.budget` から単一・合計添付、二重保存する証拠と Base64/暗号化転送の上限を導出します。既定は各 5 MiB、添付合計 22 MiB、証拠 4 MiB/2000 件、履歴・メタデータ・ビューアー用 2 MiB です。受理前に添付宣言の合計を検証し、証拠更新は原子的に制限、処理履歴は 256 KiB までです。認証済み `reports.policy({})` でクライアントとフォームに有効値を渡します。
|
|
70
70
|
|
|
71
|
-
`
|
|
71
|
+
通常業務は `transport.maxPlaintextBytes`(auto/省略は 1 MiB)を維持します。Feedback の受理・証拠・添付・ZIP ダウンロードは `routePlaintextBytes` で別途宣言します。起動時は `@h-ai/api-contract/feedback` の `resolveFeedbackBudget(検証済み設定).maxTransportBytes` を使い、任意の Feedback 初期化・再試行状態に依存しません。`*` は完全な1パス区間のみ一致し、ホストの明示値が優先です。ハンドシェイク、adapter-node、プロキシは経路の最大値を使用します。HTTP アダプターは個別宣言された 1 MiB 超の資料経路を最大2件、通常業務を別枠で16件まで許可し、応答完了・失敗時に解放します。混雑は503 / `hai:crypto:072` を返し、業務書込は自動再送しません。
|
|
72
|
+
|
|
73
|
+
`keyScreenshots` の既定値は false で、自動撮影は明示的に有効化します。手動 `prepare()` は撮影可能です。タイムアウトは AbortSignal を伝え、実処理の終了まで重複撮影を禁止します。既定レンダラーは10000 DOMノードとキャンバス画素予算を制限します。`replay:true` の `replayStart` は既定 `prepare` です。事前記録には `replayStart: init` を明示します。画面を閉じる際の `pause()` はDOM記録と撮影を止め、軽量ログ・下書き・アップロードを維持します。ユーザー切替時は引き続き `close({ discard:true })` を呼び、独自撮影関数は `{ signal }` を尊重します。
|
|
72
74
|
|
|
73
75
|
自動画像は機密部分を隠したクローン DOM から SVG foreignObject を通して可視ページを再構築します。クロスオリジン画像、iframe/video、未準備のネットワークフォントは欠落する場合があります。ブラウザーや CSP 制限による失敗は missing として記録され、画像の手動添付も利用できます。
|
|
74
76
|
|
|
@@ -90,6 +92,57 @@ console/disabled は Reach パッケージ不要です。reach モードは opti
|
|
|
90
92
|
|
|
91
93
|
Web クライアントは原生プラグインを解決・導入しません。独自ホストは @h-ai/feedback/tauri の saveTauriFeedbackFile または @h-ai/feedback/capacitor の saveCapacitorFeedbackFile を読み込み、起動時に configureFeedbackSaving(saver) を呼びます。生成工程には接続済みで、業務コードはネイティブファイルの管理を行いません。
|
|
92
94
|
|
|
95
|
+
### 業務証拠の拡張
|
|
96
|
+
|
|
97
|
+
`feedback.init(config, { collectEvidence })` にサーバー側の収集関数を登録します。受付コミット後に永続 collect タスクが同期・非同期の戻り値を待機するため、送信リクエストは業務ログの生成を待ちません。同じ appId を処理する全インスタンスに同じ関数を登録し、再起動・引き継ぎ後も復旧できるようにします。
|
|
98
|
+
|
|
99
|
+
以下は依存モジュール初期化後の例です。config はホストが読み込んだ FeedbackConfigInput、diagnostics/sanitized-logs.zip は業務側で事前に生成・秘匿化したファイルです。実際には関数内で独自のログ収集処理を await できます。
|
|
100
|
+
|
|
101
|
+
```ts
|
|
102
|
+
import type { FeedbackConfigInput } from '@h-ai/feedback'
|
|
103
|
+
import { readFile } from 'node:fs/promises'
|
|
104
|
+
import { feedback } from '@h-ai/feedback'
|
|
105
|
+
|
|
106
|
+
async function startFeedback(config: FeedbackConfigInput) {
|
|
107
|
+
const result = await feedback.init(config, {
|
|
108
|
+
collectEvidence: async ({ report, signal }) => ({
|
|
109
|
+
text: `Diagnostics for ${report.number}`,
|
|
110
|
+
data: { businessSystem: 'orders', scopeId: report.scopeId },
|
|
111
|
+
attachments: [{
|
|
112
|
+
id: 'business-logs',
|
|
113
|
+
name: 'business-logs.zip',
|
|
114
|
+
contentType: 'application/zip',
|
|
115
|
+
bytes: await readFile('diagnostics/sanitized-logs.zip', { signal }),
|
|
116
|
+
}],
|
|
117
|
+
}),
|
|
118
|
+
})
|
|
119
|
+
if (!result.success)
|
|
120
|
+
return result
|
|
121
|
+
return result
|
|
122
|
+
}
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
text/data は source: server:business、kind: business の証拠として詳細の「業務情報」タブにページ表示され、添付ファイルは認可付きダウンロードとオフライン出力に含まれます。短文・JSON は既存の秘匿化、切り詰め、証拠容量制限に従います。完全なログには全文を秘匿化する text/plain 添付を使用します。ZIP/GZIP は形式マーカー・サイズ・SHA-256 を確認してそのまま保存し、展開・実行・内部秘匿化は行いません。業務側で圧縮前に機密情報を除去してください。ブラウザーの addFiles も ZIP/GZ/TGZ に対応します。
|
|
126
|
+
|
|
127
|
+
businessCollectionTimeoutMs は既定 30000ms(1–300000)。タイムアウトと終了時に signal を中止して待機を終了するため、非同期処理にも signal を渡します。例外、不正ファイル、容量超過、保存失敗は maxAttempts まで再試行し、最終失敗は collectionStatus: failed になります。管理者は retry({ id, target: 'collect' }, context) で再実行できます。関数は再実行される可能性があるため report.id で外部処理を冪等化します。全ファイル保存後、証拠と索引をリース付きの同一トランザクションで公開し、成功マーカー保存後は再収集しません。添付 ID は安定した 1–100 文字とし、server:business: が自動付加されます。全添付で既存の 20 件・単一/合計容量制限を共有します。
|
|
128
|
+
|
|
129
|
+
### Webhook コールバック
|
|
130
|
+
|
|
131
|
+
信頼するサーバー設定 webhookUrl を指定すると、受付トランザクションで独立した永続 webhook タスクを作成します。収集が complete/partial/failed になった後、feedback.created を POST application/json で送信します。収集失敗時も送信し、Reach、notificationMode、通知抑止とは独立します。
|
|
132
|
+
|
|
133
|
+
```yaml
|
|
134
|
+
businessCollectionTimeoutMs: 30000
|
|
135
|
+
webhookUrl: ${HAI_FEEDBACK_WEBHOOKURL:}
|
|
136
|
+
webhookSecret: ${HAI_FEEDBACK_WEBHOOKSECRET:}
|
|
137
|
+
webhookTimeoutMs: ${HAI_FEEDBACK_WEBHOOKTIMEOUTMS:10000}
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
URL の空文字は無効化です。HTTP(S) のみ許可し、URL のユーザー資格情報・fragment は禁止、リダイレクトは追従しません。宛先は信頼できる配布設定で管理し、内部連携には内部 URL も使用できます。署名鍵はサーバーの非公開環境に保存し、PUBLIC_ 変数に含めません。webhookTimeoutMs は既定 10000ms(1–60000)。非 2xx、リダイレクト、通信障害、タイムアウトは 1s から最大 60s の指数バックオフで maxAttempts(既定 5)まで再試行します。
|
|
141
|
+
|
|
142
|
+
公開型 FeedbackWebhookEvent は event/eventId/createdAt/report/evidence/detailUrl? を含みます。evidence は修訂、業務証拠の収集成功、添付メタデータを持ち、本文・保存キー・設定秘密・全ログは送信しません。内容は認可付き API またはオフライン ZIP から取得します。初回送信前に本文を固定し、再試行・再起動でも本文と Idempotency-Key: eventId を維持します。受信側は eventId を永続化・重複排除した後に 2xx を返してください。配信は少なくとも 1 回で、応答消失による重複があります。URL 変更後の旧タスクは別の宛先に転送せず失敗するため、切替前に処理を完了します。
|
|
143
|
+
|
|
144
|
+
X-Hai-Feedback-Event と X-Hai-Feedback-Timestamp(Unix 秒)を送信します。鍵設定時は X-Hai-Feedback-Signature: sha256=<hex> を付加し、HMAC-SHA256(secret, timestamp + '.' + 元の本文) を計算します。受信側は元のバイトで検証し、定数時間比較、時刻許容幅、eventId による重複排除を実装します。get().jobs で状態、試行回数、安全な lastError コードを確認でき、管理台/Fullstack の詳細にも状態と再試行を表示します。管理者は retry({ id, target: 'webhook' }, context) を使用できます。notificationStatus とは別管理で、成功は受信先が 2xx を返したことを示します。
|
|
145
|
+
|
|
93
146
|
### 一覧、処理、出力
|
|
94
147
|
|
|
95
148
|
`list/get/getEvidence` は認可された報告・詳細・証拠ページを返します。`setStatus({ id, status, expectedVersion }, context)` で処理と取り消しの競合を防ぎます。`retry` は回復可能な操作のみ再登録します。`export` は証拠の版を固定し、`getExport` は完成済みZIPを返します。欠落や切り捨ては明示され、ページ再生は業務処理の再実行ではありません。
|
|
@@ -115,6 +168,20 @@ hai feedback view feedback-FB-1.zip
|
|
|
115
168
|
|
|
116
169
|
各業務結果の `success` を確認してください。`HaiFeedbackError` は初期化、設定、権限、存在しない記録、競合、容量、checksum エラーを表します。HTTP 200やSMTP受理だけでは、業務成功や受信箱への配信を証明できません。不確実な送信は重複する可能性があります。
|
|
117
170
|
|
|
171
|
+
### リソース、クォータと補助機能の可用性
|
|
172
|
+
|
|
173
|
+
サーバーエラー証拠の待機列は3000件・8 MiB・5分を上限とし、最大32件/256 KiBの冪等バッチで保存します。DB障害は最大30秒までバックオフし、欠落理由を保存します。空のworkerも最大30秒まで待機し、同じノードでの送信・再試行・出力が起動します。`workerIntervalMs:0` には専用実行元が必要です。仮証拠はappId・期限索引を使って最大100件ずつ削除します。
|
|
174
|
+
|
|
175
|
+
既定値は `maxReportsPerUserDay=20`、`maxBytesPerUserDay=2 GiB`、`maxStoredBytesPerScope=10 GiB`、`maxExportsPerReport=5`、`exportRetentionHours=24` です。共有DBの同一トランザクションで、DBのUTC日とapp/scope/user別に `maxTotalBytes * (maxExportsPerReport + 1)` バイトを予約します。報告容量と出力数は受理時に固定し、設定を増やしても既存予約を超えません。これは保守的な予約量で実ディスク計測ではありません。冪等再送は再計上せず、超過は429 `QUOTA_EXCEEDED` とし報告・オブジェクトを作りません。報告の期限削除成功でscope予約を解放し、日次累計は戻しません。期限切れ出力はダウンロード不可で段階削除します。固定スナップショット・作成日時と条件付書込でZIPを再利用し、業務証拠のファイル一覧は最初のアップロード前に固定します。`hai_feedback_quotas` のperiod=0と期限cleanupの滞留を監視し。
|
|
176
|
+
|
|
177
|
+
参考admin/fullstackはfeedback起動失敗を業務の起動失敗にせず、30秒ごとに個別再試行し、同じ警告を繰り返しません。未準備のpolicyも503です。必須のDB/cache/IAM/auditは従来の障害方針を維持します。runtime.start全体の失敗を握りつぶしてロールバック済みのモジュールを使わないでください。終了時はfeedbackと再試行を停止してから依存を解放します。
|
|
178
|
+
|
|
179
|
+
### console 通知のプライバシー境界
|
|
180
|
+
|
|
181
|
+
`consoleContent` の既定は `metadata` で、報告番号、アプリ・バージョン・ビルド情報と収集状態だけを記録します。説明・概要・添付・証拠・通知本文・宛先は含みません。一般的なマスキング済み概要が必要な場合のみ `summary` を明示します。一般的なマスキングでは全業務機密を検出できません。Reach は不要で disabled/reach の意味は変わりません。更新後の既存 console 設定も既定でメタデータだけになります。
|
|
182
|
+
|
|
183
|
+
サーバー環境証拠は受付トランザクションで同時に保存します。環境保存失敗時は不完全な受付を残さず、同じ submissionId で再試行できます。管理画面は `createFeedbackReview(...).retryReport(id)` を使い、復旧可能な収集と通知/Webhook失敗を再試行します。対象を重複排除し失敗を伝播し、close後は後続処理を止めます。
|
|
184
|
+
|
|
118
185
|
## 開発とテスト
|
|
119
186
|
|
|
120
187
|
```sh
|
package/README.md
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
|
|
13
13
|
- 有界采集前后端日志、请求、错误、操作、版本与白名单环境信息;默认错误前后各最多 100 条。
|
|
14
14
|
- 数据库事务受理、提交/证据幂等、附件校验、独立的处理/采集/通知状态、持久化任务与租约恢复。
|
|
15
|
-
- 通知由 `notificationMode` 独立选择:`disabled` 不创建通知任务;`console`
|
|
15
|
+
- 通知由 `notificationMode` 独立选择:`disabled` 不创建通知任务;`console` 默认仅输出定位与采集状态元数据(consoleContent: metadata);显式 consoleContent: summary 才加入通用脱敏摘要,不依赖 Reach 或邮箱;`reach` 使用任意已配置 Provider 与接收方,仅此模式要求 recipients。默认 `reach` 保持旧配置兼容,新生成工程显式使用 `console`。Reach 免打扰期间由反馈自己的 outbox 等待,不进入 Reach 延迟队列,也不修改其他通知策略。Reach 的生产 Console Provider 限制保持不变;sent 仅表示选定通道受理,不能证明收信。
|
|
16
16
|
- 浏览器截图、遮挡/标注、可选 rrweb 回放、失败重试与本地导出;`@h-ai/ui` 提供 `FeedbackModal`、`FeedbackList` 和 `FeedbackDetail`。
|
|
17
17
|
- 自包含 ZIP、SHA-256 校验、离线查看与按请求/错误/时间分页读取,不包含生产凭据。
|
|
18
18
|
|
|
@@ -56,11 +56,11 @@ return submitted
|
|
|
56
56
|
|
|
57
57
|
`text/plain` 附件在 5 MiB 文件上限内全文脱敏,保留安全正文和末尾日志,不套用结构化证据的单字段截断。超过上限的原文件在读取前拒绝,脱敏结果仍受同一附件字节上限约束。JSON 附件和结构化证据继续限制字段长度、深度及数量,可能被裁剪;需要完整日志时使用纯文本附件。待提交附件与首次包络冻结后本地导出的新增附件使用相同规则。
|
|
58
58
|
|
|
59
|
-
完整客户端方法为 `init(config)`、`prepare/captureScreenshot(attachmentId?)/submit/flush/record/getDraft/getView/annotate/addFiles/exportDraft/saveDraft/close`。将表单 `onfileschange` 连接 `addFiles` 后刷新草稿附件预览,用户便可在提交前标注、遮挡或删除新增图片。`data-feedback-action` 标记关键操作,`data-feedback-private` 遮挡敏感内容,`data-feedback-ignore` 排除反馈界面自身。首次发送后包络冻结;同一提交重试时修改输入会返回冲突并保留输入,可先本地导出。
|
|
59
|
+
完整客户端方法为 `init(config)`、`prepare/captureScreenshot(attachmentId?)/submit/flush/record/getDraft/getView/annotate/addFiles/exportDraft/saveDraft/pause/close`。将表单 `onfileschange` 连接 `addFiles` 后刷新草稿附件预览,用户便可在提交前标注、遮挡或删除新增图片。`data-feedback-action` 标记关键操作,`data-feedback-private` 遮挡敏感内容,`data-feedback-ignore` 排除反馈界面自身。首次发送后包络冻结;同一提交重试时修改输入会返回冲突并保留输入,可先本地导出。
|
|
60
60
|
|
|
61
61
|
`addFiles` 按批次原子添加:全部文件通过校验才合并,任一失败均保留原附件和缺失标记,不保存半批文件。并发成功批次不会相互覆盖,合并后仍最多 20 项;读取期间草稿切换或首次包络冻结时返回 `CONFLICT`,不会补入包络之外的隐藏上传。
|
|
62
62
|
|
|
63
|
-
本地附件支持 PNG/JPEG/WebP、WebM/MP4、TXT/LOG、JSON 和
|
|
63
|
+
本地附件支持 PNG/JPEG/WebP、WebM/MP4、TXT/LOG、JSON、PDF 和 ZIP/GZIP。浏览器未提供 MIME 或只提供 `application/octet-stream` 时按受支持扩展名补齐,常见 MIME 别名会规范化;服务端仍校验真实文件字节和 checksum。不支持的格式显示具体提示。普通输入控件的截图保留底色与边框,内容替换为固定遮罩字符;密码框、`data-feedback-private`、`.rr-block` 和 `.rr-mask` 继续整块遮挡。仅修改截图克隆,原页面和输入值不变。
|
|
64
64
|
|
|
65
65
|
`annotate(id, { ...rect, mode, coordinateSpace: 'ratio' })` 直接接受 UI 的 0–1 坐标,由 SDK 按解码后的原图换算,支持小数、小于 1% 的区域、反向拖动、边缘与 CSS 缩放。省略 coordinateSpace 仍使用原图像素。`getView()` 管理并复用预览 URL;替换、删除和关闭时释放。业务无需 Image.decode 或 URL.createObjectURL。
|
|
66
66
|
|
|
@@ -68,9 +68,11 @@ return submitted
|
|
|
68
68
|
|
|
69
69
|
草稿由 SDK 持有并通过 `annotate`、`addFiles` 等方法更新。Svelte 宿主保存该引用时使用 `$state.raw<FeedbackDraft>()`,操作成功后从 `getDraft()` 刷新附件视图;不要将 SDK 草稿放入深度 `$state` 代理,否则 SDK 替换图片字节后,代理可能继续返回已缓存的旧字节。
|
|
70
70
|
|
|
71
|
-
`maxTotalBytes` 默认 32 MiB(4–40 MiB)。`feedback.budget` 从同一总量推导单附件、附件总量、双份证据与 Base64/加密传输预算;默认单附件 5 MiB、附件合计 22 MiB、证据 4 MiB/2000 条,并预留 2 MiB 给历史、元数据和查看器。受理前检查附件声明总量,补采原子检查证据预算,处理历史限制 256 KiB,避免成功受理后因容量永久无法导出。客户端通过受权 `reports.policy({})`
|
|
71
|
+
`maxTotalBytes` 默认 32 MiB(4–40 MiB)。`feedback.budget` 从同一总量推导单附件、附件总量、双份证据与 Base64/加密传输预算;默认单附件 5 MiB、附件合计 22 MiB、证据 4 MiB/2000 条,并预留 2 MiB 给历史、元数据和查看器。受理前检查附件声明总量,补采原子检查证据预算,处理历史限制 256 KiB,避免成功受理后因容量永久无法导出。客户端通过受权 `reports.policy({})` 取得有效值,表单展示容量;
|
|
72
72
|
|
|
73
|
-
`
|
|
73
|
+
普通业务仍使用 `transport.maxPlaintextBytes`(auto/省略为 1 MiB);Feedback 的报告受理、证据、附件及 ZIP 下载路由通过 `routePlaintextBytes` 单独声明容量。启动时用 `@h-ai/api-contract/feedback` 的 `resolveFeedbackBudget(已校验的反馈配置).maxTransportBytes` 预计算,避免可选反馈故障/重试期间缓存模块默认预算。路径中的 `*` 仅匹配一个完整路径段;宿主显式路由值优先。握手、adapter-node 与代理使用各路由最大容量,普通业务不会随材料预算扩大。HTTP 适配器为单独声明且大于 1 MiB 的材料路由保留最多 2 个槽,普通业务另有 16 个槽,完整响应/失败后释放;忙碌返回 503 / `hai:crypto:072`,不自动重放业务写入。
|
|
74
|
+
|
|
75
|
+
`keyScreenshots` 默认 false,需显式开启自动关键截图;手动 `prepare()` 仍可截图。截图超时会传递 AbortSignal,底层尚未结束时不允许重叠截图。默认渲染最多 10000 DOM 节点,继续受画布像素预算限制。`replay:true` 的默认 `replayStart` 为 `prepare`,打开反馈时才录制;明确需要预录时配置 `replayStart: init`。关闭反馈界面调用 `pause()` 停止 DOM 回放和截图,保留轻量日志、草稿及上传;退出身份仍调用 `close({ discard:true })`。自定义 `captureScreenshot({ signal })` 应尊重取消信号。
|
|
74
76
|
|
|
75
77
|
自动截图由脱敏克隆 DOM 经 SVG foreignObject 重建可见页面。跨域图片、iframe、video 和尚未就绪的网络字体可能缺失;浏览器或 CSP 限制导致失败时明确标记 missing,用户仍可手动添加附件。
|
|
76
78
|
|
|
@@ -92,6 +94,57 @@ Web 客户端不解析或安装原生插件。自定义 Tauri/Capacitor 宿主
|
|
|
92
94
|
|
|
93
95
|
console/disabled 无 Reach npm 依赖;选 reach 模式需显式安装 @h-ai/reach(optional peer)并由宿主初始化。凭据通知只返回生成的 receiptId,不保存或回传可能携带载荷的 Provider messageId。
|
|
94
96
|
|
|
97
|
+
### 业务证据扩展
|
|
98
|
+
|
|
99
|
+
业务系统通过 `feedback.init(config, { collectEvidence })` 注册一个服务端采集函数。它在反馈可靠入库后由持久 `collect` 任务调用,支持直接返回或 `async/await`;提交请求无需等待业务日志生成。相比进程内事件订阅,任务在重启或切换实例后仍可恢复。所有处理同一 appId 的实例必须注册相同采集函数。
|
|
100
|
+
|
|
101
|
+
下例运行在依赖已初始化的服务端;`config` 为宿主加载的 `FeedbackConfigInput`,业务预先生成并脱敏 `diagnostics/sanitized-logs.zip`,生产中可在回调内等待自己的日志收集服务:
|
|
102
|
+
|
|
103
|
+
```ts
|
|
104
|
+
import type { FeedbackConfigInput } from '@h-ai/feedback'
|
|
105
|
+
import { readFile } from 'node:fs/promises'
|
|
106
|
+
import { feedback } from '@h-ai/feedback'
|
|
107
|
+
|
|
108
|
+
async function startFeedback(config: FeedbackConfigInput) {
|
|
109
|
+
const result = await feedback.init(config, {
|
|
110
|
+
collectEvidence: async ({ report, signal }) => ({
|
|
111
|
+
text: `Diagnostics for ${report.number}`,
|
|
112
|
+
data: { businessSystem: 'orders', scopeId: report.scopeId },
|
|
113
|
+
attachments: [{
|
|
114
|
+
id: 'business-logs',
|
|
115
|
+
name: 'business-logs.zip',
|
|
116
|
+
contentType: 'application/zip',
|
|
117
|
+
bytes: await readFile('diagnostics/sanitized-logs.zip', { signal }),
|
|
118
|
+
}],
|
|
119
|
+
}),
|
|
120
|
+
})
|
|
121
|
+
if (!result.success)
|
|
122
|
+
return result
|
|
123
|
+
return result
|
|
124
|
+
}
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
`text/data` 以 `source: server:business`、`kind: business` 写入证据,详情“业务信息”页签可分页查询;附件关联同一反馈,详情下载和离线导出均包含。短文本/JSON 遵循已有脱敏、字段截断和证据预算;完整日志使用 `text/plain` 附件,会全文脱敏。ZIP/GZIP 原样存储,仅校验类型标记、字节数与 SHA-256,不解压、不执行、不自动脱敏内部文件;业务须在压缩前清理凭据和个人敏感内容。浏览器 `addFiles` 同样支持 ZIP、GZ、TGZ。
|
|
128
|
+
|
|
129
|
+
回调最多等待 `businessCollectionTimeoutMs`(默认 30000ms,1–300000);超时或关闭触发 `signal`,框架停止等待,宿主必须将 signal 传给异步操作。抛错、超限、非法文件或存储失败均记录采集任务失败并按 `maxAttempts` 重试;耗尽后 `collectionStatus: failed`,管理员可 `retry({ id, target: 'collect' }, context)`。回调可能重新执行,使用 report.id 实现外部操作幂等。附件全部存储完成后,证据和附件索引在持有租约的同一事务发布;成功标记入库后不再调用回调。附件 ID 稳定且为 1–100 字符,框架自动加 `server:business:` 前缀;所有附件共享 20 项、单项/合计预算,不另开无限容量。
|
|
130
|
+
|
|
131
|
+
### Webhook 回调
|
|
132
|
+
|
|
133
|
+
配置可信服务端 `webhookUrl` 后,每次新反馈在受理事务中创建一个独立 `webhook` 任务。任务等待采集进入 complete/partial/failed,再发送 `POST application/json` 的 `feedback.created`;采集失败仍会回调。Webhook 不依赖 Reach,也不受 notificationMode 或 Reach 免打扰影响。
|
|
134
|
+
|
|
135
|
+
```yaml
|
|
136
|
+
businessCollectionTimeoutMs: 30000
|
|
137
|
+
webhookUrl: ${HAI_FEEDBACK_WEBHOOKURL:}
|
|
138
|
+
webhookSecret: ${HAI_FEEDBACK_WEBHOOKSECRET:}
|
|
139
|
+
webhookTimeoutMs: ${HAI_FEEDBACK_WEBHOOKTIMEOUTMS:10000}
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
地址默认空字符串(禁用),只允许 HTTP(S),禁止 URL 用户凭据/fragment,不跟随重定向。地址只能由可信部署配置提供;内网地址可用于内部系统回调。签名密钥仅放服务端 `.env.local` 或部署私有环境,不使用 `PUBLIC_`。`webhookTimeoutMs` 默认 10000ms(1–60000),非 2xx、重定向、网络错误及超时均进入指数退避重试(1s 起,最多 60s),最多 `maxAttempts`(默认 5)次。
|
|
143
|
+
|
|
144
|
+
公开载荷类型为 `FeedbackWebhookEvent`,包含 `event/eventId/createdAt/report/evidence/detailUrl?`。evidence 提供修订、业务材料是否成功和附件元数据;不发送附件正文、存储 key、配置密钥或全部日志。需要正文时使用现有受权 API 或离线包。首次发送前固定载荷,重试/重启保持相同正文和 `Idempotency-Key: eventId`。接收方应先持久化并按 eventId 去重,再返回 2xx;HTTP 副作用采用至少一次投递,响应丢失仍可能重发。修改回调地址后旧任务会失败,不会自动转投新地址;待旧任务处理完再切换。
|
|
145
|
+
|
|
146
|
+
每次请求发送 `X-Hai-Feedback-Event`、`X-Hai-Feedback-Timestamp`(Unix 秒);配置密钥时额外发送 `X-Hai-Feedback-Signature: sha256=<hex>`,计算方式为 `HMAC-SHA256(secret, timestamp + '.' + 原始请求正文)`。接收方用原始字节验签、恒定时间比较,并限制时间偏差及按 eventId 防重放。任务终态和尝试次数见 `get().jobs`,`lastError` 仅为安全错误码;管理台/Fullstack 详情展示 Webhook 状态,管理员可重试或调用 `retry({ id, target: 'webhook' }, context)`。它与 `notificationStatus` 分开记录,发送成功表示接收端返回 2xx。
|
|
147
|
+
|
|
95
148
|
### 查询、处理与导出
|
|
96
149
|
|
|
97
150
|
`list/get/getEvidence` 返回受权记录、详情与分页证据;`setStatus({ id, status, expectedVersion }, context)` 以版本检查执行处理/撤销。`retry` 仅重排可恢复项。`export` 固定证据修订并创建任务,通过 `getExport` 获取已生成的 ZIP;导出未完成不伪装成功。材料缺失和截断随包保留,回放不是重新执行业务。
|
|
@@ -117,6 +170,20 @@ hai feedback view feedback-FB-1.zip
|
|
|
117
170
|
|
|
118
171
|
领域错误使用 `HaiFeedbackError`,包括未初始化、配置、权限、找不到、冲突、容量及 checksum 失败。所有领域 API 先检查 `success`;HTTP 200 或 Promise 完成不代表反馈、上传或邮件成功。SMTP 受理不等于管理员收件箱已投递;不确定发送存在重发边界。
|
|
119
172
|
|
|
173
|
+
### 资源、配额与辅助服务故障
|
|
174
|
+
|
|
175
|
+
服务端错误窗口待写材料同时限制 3000 条、8 MiB 和 5 分钟;按最多 32 条/256 KiB 批量幂等写入,数据库失败指数退避到 30 秒,丢失保留 missing 原因。worker 空闲指数退避到 30 秒,本节点提交/重试/导出会唤醒;`workerIntervalMs:0` 由专用 worker 驱动。临时证据按 appId 每批最多 100 条清理,使用复合过期索引。
|
|
176
|
+
|
|
177
|
+
`maxReportsPerUserDay` 默认 20,`maxBytesPerUserDay` 默认 2 GiB,`maxStoredBytesPerScope` 默认 10 GiB,`maxExportsPerReport` 默认 5,`exportRetentionHours` 默认 24。受理在同一共享数据库事务中按数据库 UTC 日、app/scope/user 预留 `maxTotalBytes * (maxExportsPerReport + 1)` 字节,覆盖报告及历史 ZIP;受理时固定报告容量与导出数量,调高配置不突破既有预留;是保守容量预留,非磁盘实时计量。重复 submissionId 不重复计费,超限返回 `QUOTA_EXCEEDED`(429)且不创建报告或对象。成功到期删除报告才释放 scope 预留;日累计计数不退还。导出到期禁止下载并限批删对象,固定快照/创建时间与条件写复用同一个 ZIP,业务补采首次上传前冻结文件清单,重试不能变化 ID/内容制造无限对象。运维可查看 `hai_feedback_quotas` 的 scope 行(period=0)已预留量及到期 cleanup 任务积压;它不等于云存储账单。
|
|
178
|
+
|
|
179
|
+
参考管理台与 fullstack 的 feedback 启动失败仅使反馈不可用,30 秒独立重试且同类故障不重复输出告警;策略接口也返回 503。业务必需的数据库、缓存、IAM 和审计仍遵守原失败策略;不能捕获整个 runtime.start 后继续使用已经回滚的模块。关闭先停止反馈与重试任务,再释放借用依赖。
|
|
180
|
+
|
|
181
|
+
### console 通知的隐私边界
|
|
182
|
+
|
|
183
|
+
`consoleContent` 默认为 `metadata`:仅记录反馈编号、应用/版本/构建定位与采集状态,不输出说明、summary、附件、证据、通知正文或接收地址。需要通用脱敏摘要时显式设为 `summary`;通用脱敏不能识别所有业务秘密。该选项不需要 Reach,也不改变 disabled/reach 的通道语义。旧配置的 console 日志默认随本次升级收紧。
|
|
184
|
+
|
|
185
|
+
服务端环境证据与反馈受理共同提交;环境写入失败不留下已受理但缺失环境的半成品,相同 submissionId 可安全重试。管理界面用 `createFeedbackReview(...).retryReport(id)` 重试可恢复材料与失败通知/Webhook,控制器去重目标并传播失败,退出调用 close 阻止迟到请求继续编排。
|
|
186
|
+
|
|
120
187
|
## 开发测试
|
|
121
188
|
|
|
122
189
|
```sh
|
package/dist/bundle/index.d.ts
CHANGED
|
@@ -49,6 +49,8 @@ interface FeedbackBundleInput<TReport extends {
|
|
|
49
49
|
}> {
|
|
50
50
|
/** 有效总预算;不得高于通用离线格式的 64 MiB 防护上限。 */
|
|
51
51
|
maxBytes?: number;
|
|
52
|
+
/** 固定归档创建时间(Unix 毫秒),服务端幂等导出使用任务创建时间;默认当前时间。 */
|
|
53
|
+
createdAt?: number;
|
|
52
54
|
report: TReport;
|
|
53
55
|
evidence: readonly FeedbackBundleEvidence[];
|
|
54
56
|
history?: readonly object[];
|
package/dist/bundle/index.js
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
export { createFeedbackBundle, getFeedbackBundleViewerFiles, queryFeedbackBundle, readFeedbackBundle, summarizeFeedbackBundle } from '../chunk-
|
|
1
|
+
export { createFeedbackBundle, getFeedbackBundleViewerFiles, queryFeedbackBundle, readFeedbackBundle, summarizeFeedbackBundle } from '../chunk-KGMLTOMM.js';
|
|
2
2
|
//# sourceMappingURL=index.js.map
|
|
3
3
|
//# sourceMappingURL=index.js.map
|
|
@@ -11,7 +11,8 @@ var FeedbackErrorInfo = {
|
|
|
11
11
|
CONFLICT: "023:409",
|
|
12
12
|
LIMIT_EXCEEDED: "024:413",
|
|
13
13
|
INVALID_INPUT: "025:400",
|
|
14
|
-
CHECKSUM_MISMATCH: "026:422"
|
|
14
|
+
CHECKSUM_MISMATCH: "026:422",
|
|
15
|
+
QUOTA_EXCEEDED: "027:429"
|
|
15
16
|
};
|
|
16
17
|
var HaiFeedbackError = core.error.buildHaiErrorsDef("feedback", FeedbackErrorInfo);
|
|
17
18
|
|
|
@@ -27,7 +28,7 @@ var en_US_default = {
|
|
|
27
28
|
feedback_exportPending: "The feedback bundle is being generated. Please retry shortly",
|
|
28
29
|
feedback_limitExceeded: "Feedback evidence exceeds the allowed capacity",
|
|
29
30
|
feedback_invalidInput: "Invalid feedback input",
|
|
30
|
-
feedback_unsupportedAttachmentType: "Unsupported attachment format. Choose a PNG, JPEG, WebP, WebM, MP4, TXT, LOG, JSON or
|
|
31
|
+
feedback_unsupportedAttachmentType: "Unsupported attachment format. Choose a PNG, JPEG, WebP, WebM, MP4, TXT, LOG, JSON, PDF, ZIP or GZIP file",
|
|
31
32
|
feedback_checksumMismatch: "Attachment integrity verification failed",
|
|
32
33
|
feedback_received: "New feedback received",
|
|
33
34
|
feedback_submittedAt: "Submitted at (UTC)",
|
|
@@ -37,7 +38,8 @@ var en_US_default = {
|
|
|
37
38
|
feedback_materialPartial: "Partially missing",
|
|
38
39
|
feedback_materialFailed: "Collection failed",
|
|
39
40
|
feedback_defaultSummary: "User feedback",
|
|
40
|
-
feedback_retryInputChanged: "The first submission has already been sent. Restore the original text and attachments to retry, then open a new feedback."
|
|
41
|
+
feedback_retryInputChanged: "The first submission has already been sent. Restore the original text and attachments to retry, then open a new feedback.",
|
|
42
|
+
feedback_quotaExceeded: "Feedback quota reached. Try later or contact an administrator."
|
|
41
43
|
};
|
|
42
44
|
|
|
43
45
|
// messages/ja-JP.json
|
|
@@ -52,7 +54,7 @@ var ja_JP_default = {
|
|
|
52
54
|
feedback_exportPending: "\u554F\u984C\u30D1\u30C3\u30B1\u30FC\u30B8\u3092\u751F\u6210\u3057\u3066\u3044\u307E\u3059\u3002\u3057\u3070\u3089\u304F\u3057\u3066\u304B\u3089\u518D\u8A66\u884C\u3057\u3066\u304F\u3060\u3055\u3044",
|
|
53
55
|
feedback_limitExceeded: "\u8CC7\u6599\u304C\u8A31\u5BB9\u5BB9\u91CF\u3092\u8D85\u3048\u3066\u3044\u307E\u3059",
|
|
54
56
|
feedback_invalidInput: "\u5165\u529B\u304C\u7121\u52B9\u3067\u3059",
|
|
55
|
-
feedback_unsupportedAttachmentType: "\u672A\u5BFE\u5FDC\u306E\u6DFB\u4ED8\u5F62\u5F0F\u3067\u3059\u3002PNG\u3001JPEG\u3001WebP\u3001WebM\u3001MP4\u3001TXT\u3001LOG\u3001JSON\u3001PDF \u3092\u9078\u629E\u3057\u3066\u304F\u3060\u3055\u3044",
|
|
57
|
+
feedback_unsupportedAttachmentType: "\u672A\u5BFE\u5FDC\u306E\u6DFB\u4ED8\u5F62\u5F0F\u3067\u3059\u3002PNG\u3001JPEG\u3001WebP\u3001WebM\u3001MP4\u3001TXT\u3001LOG\u3001JSON\u3001PDF\u3001ZIP\u3001GZIP \u3092\u9078\u629E\u3057\u3066\u304F\u3060\u3055\u3044",
|
|
56
58
|
feedback_checksumMismatch: "\u6DFB\u4ED8\u30D5\u30A1\u30A4\u30EB\u306E\u6574\u5408\u6027\u3092\u78BA\u8A8D\u3067\u304D\u307E\u305B\u3093",
|
|
57
59
|
feedback_received: "\u65B0\u3057\u3044\u30D5\u30A3\u30FC\u30C9\u30D0\u30C3\u30AF\u3092\u53D7\u4FE1\u3057\u307E\u3057\u305F",
|
|
58
60
|
feedback_submittedAt: "\u5831\u544A\u65E5\u6642\uFF08UTC\uFF09",
|
|
@@ -62,7 +64,8 @@ var ja_JP_default = {
|
|
|
62
64
|
feedback_materialPartial: "\u4E00\u90E8\u4E0D\u8DB3",
|
|
63
65
|
feedback_materialFailed: "\u53CE\u96C6\u5931\u6557",
|
|
64
66
|
feedback_defaultSummary: "\u30E6\u30FC\u30B6\u30FC\u306E\u30D5\u30A3\u30FC\u30C9\u30D0\u30C3\u30AF",
|
|
65
|
-
feedback_retryInputChanged: "\u6700\u521D\u306E\u9001\u4FE1\u306F\u958B\u59CB\u3055\u308C\u3066\u3044\u307E\u3059\u3002\u5143\u306E\u8AAC\u660E\u3068\u6DFB\u4ED8\u30D5\u30A1\u30A4\u30EB\u306B\u623B\u3057\u3066\u9001\u4FE1\u7D50\u679C\u3092\u78BA\u8A8D\u3057\u3066\u304B\u3089\u3001\u65B0\u3057\u3044\u30D5\u30A3\u30FC\u30C9\u30D0\u30C3\u30AF\u3092\u4F5C\u6210\u3057\u3066\u304F\u3060\u3055\u3044\u3002"
|
|
67
|
+
feedback_retryInputChanged: "\u6700\u521D\u306E\u9001\u4FE1\u306F\u958B\u59CB\u3055\u308C\u3066\u3044\u307E\u3059\u3002\u5143\u306E\u8AAC\u660E\u3068\u6DFB\u4ED8\u30D5\u30A1\u30A4\u30EB\u306B\u623B\u3057\u3066\u9001\u4FE1\u7D50\u679C\u3092\u78BA\u8A8D\u3057\u3066\u304B\u3089\u3001\u65B0\u3057\u3044\u30D5\u30A3\u30FC\u30C9\u30D0\u30C3\u30AF\u3092\u4F5C\u6210\u3057\u3066\u304F\u3060\u3055\u3044\u3002",
|
|
68
|
+
feedback_quotaExceeded: "\u30D5\u30A3\u30FC\u30C9\u30D0\u30C3\u30AF\u306E\u4E0A\u9650\u306B\u9054\u3057\u307E\u3057\u305F\u3002\u5F8C\u3067\u518D\u8A66\u884C\u3059\u308B\u304B\u7BA1\u7406\u8005\u306B\u9023\u7D61\u3057\u3066\u304F\u3060\u3055\u3044\u3002"
|
|
66
69
|
};
|
|
67
70
|
|
|
68
71
|
// messages/zh-CN.json
|
|
@@ -77,7 +80,7 @@ var zh_CN_default = {
|
|
|
77
80
|
feedback_exportPending: "\u95EE\u9898\u5305\u6B63\u5728\u751F\u6210\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5",
|
|
78
81
|
feedback_limitExceeded: "\u53CD\u9988\u6750\u6599\u8D85\u8FC7\u5141\u8BB8\u7684\u5BB9\u91CF",
|
|
79
82
|
feedback_invalidInput: "\u53CD\u9988\u53C2\u6570\u65E0\u6548",
|
|
80
|
-
feedback_unsupportedAttachmentType: "\u4E0D\u652F\u6301\u6B64\u9644\u4EF6\u683C\u5F0F\uFF0C\u8BF7\u9009\u62E9 PNG\u3001JPEG\u3001WebP\u3001WebM\u3001MP4\u3001TXT\u3001LOG\u3001JSON \u6216
|
|
83
|
+
feedback_unsupportedAttachmentType: "\u4E0D\u652F\u6301\u6B64\u9644\u4EF6\u683C\u5F0F\uFF0C\u8BF7\u9009\u62E9 PNG\u3001JPEG\u3001WebP\u3001WebM\u3001MP4\u3001TXT\u3001LOG\u3001JSON\u3001PDF\u3001ZIP \u6216 GZIP \u6587\u4EF6",
|
|
81
84
|
feedback_checksumMismatch: "\u9644\u4EF6\u5B8C\u6574\u6027\u6821\u9A8C\u5931\u8D25",
|
|
82
85
|
feedback_received: "\u6536\u5230\u65B0\u7684\u95EE\u9898\u53CD\u9988",
|
|
83
86
|
feedback_submittedAt: "\u53CD\u9988\u65F6\u95F4\uFF08UTC\uFF09",
|
|
@@ -87,7 +90,8 @@ var zh_CN_default = {
|
|
|
87
90
|
feedback_materialPartial: "\u90E8\u5206\u7F3A\u5931",
|
|
88
91
|
feedback_materialFailed: "\u91C7\u96C6\u5931\u8D25",
|
|
89
92
|
feedback_defaultSummary: "\u7528\u6237\u63D0\u4EA4\u7684\u95EE\u9898\u53CD\u9988",
|
|
90
|
-
feedback_retryInputChanged: "\u9996\u6B21\u63D0\u4EA4\u5DF2\u53D1\u51FA\uFF0C\u91CD\u8BD5\u9700\u4FDD\u6301\u539F\u8BF4\u660E\u548C\u9644\u4EF6\u3002\u8BF7\u6062\u590D\u539F\u8F93\u5165\u5B8C\u6210\u786E\u8BA4\u540E\uFF0C\u518D\u53D1\u8D77\u65B0\u53CD\u9988\u3002"
|
|
93
|
+
feedback_retryInputChanged: "\u9996\u6B21\u63D0\u4EA4\u5DF2\u53D1\u51FA\uFF0C\u91CD\u8BD5\u9700\u4FDD\u6301\u539F\u8BF4\u660E\u548C\u9644\u4EF6\u3002\u8BF7\u6062\u590D\u539F\u8F93\u5165\u5B8C\u6210\u786E\u8BA4\u540E\uFF0C\u518D\u53D1\u8D77\u65B0\u53CD\u9988\u3002",
|
|
94
|
+
feedback_quotaExceeded: "\u53CD\u9988\u914D\u989D\u5DF2\u7528\u5B8C\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5\u6216\u8054\u7CFB\u7BA1\u7406\u5458"
|
|
91
95
|
};
|
|
92
96
|
|
|
93
97
|
// src/feedback-i18n.ts
|
|
@@ -97,5 +101,5 @@ function redactFeedbackText(text) {
|
|
|
97
101
|
}
|
|
98
102
|
|
|
99
103
|
export { HaiFeedbackError, feedbackM, redactFeedbackText };
|
|
100
|
-
//# sourceMappingURL=chunk-
|
|
101
|
-
//# sourceMappingURL=chunk-
|
|
104
|
+
//# sourceMappingURL=chunk-7W47WZXY.js.map
|
|
105
|
+
//# sourceMappingURL=chunk-7W47WZXY.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/feedback-types.ts","../messages/en-US.json","../messages/ja-JP.json","../messages/zh-CN.json","../src/feedback-i18n.ts","../src/feedback-sanitize-text.ts"],"names":["core"],"mappings":";;;AAuBA,IAAM,iBAAA,GAAoB;AAAA,EACxB,eAAA,EAAiB,SAAA;AAAA,EACjB,gBAAA,EAAkB,SAAA;AAAA,EAClB,YAAA,EAAc,SAAA;AAAA,EACd,gBAAA,EAAkB,SAAA;AAAA,EAClB,SAAA,EAAW,SAAA;AAAA,EACX,SAAA,EAAW,SAAA;AAAA,EACX,QAAA,EAAU,SAAA;AAAA,EACV,cAAA,EAAgB,SAAA;AAAA,EAChB,aAAA,EAAe,SAAA;AAAA,EACf,iBAAA,EAAmB,SAAA;AAAA,EACnB,cAAA,EAAgB;AAClB,CAAA;AAWO,IAAM,gBAAA,GAAmB,IAAA,CAAK,KAAA,CAAM,iBAAA,CAAkB,YAAY,iBAAiB;;;AC9C1F,IAAA,aAAA,GAAA;AAAA,EACE,uBAAA,EAA2B,mCAAA;AAAA,EAC3B,uBAAA,EAA2B,wCAAA;AAAA,EAC3B,oBAAA,EAAwB,0DAAA;AAAA,EACxB,wBAAA,EAA4B,yCAAA;AAAA,EAC5B,iBAAA,EAAqB,oCAAA;AAAA,EACrB,kBAAA,EAAsB,mCAAA;AAAA,EACtB,iBAAA,EAAqB,0EAAA;AAAA,EACrB,sBAAA,EAA0B,8DAAA;AAAA,EAC1B,sBAAA,EAA0B,gDAAA;AAAA,EAC1B,qBAAA,EAAyB,wBAAA;AAAA,EACzB,kCAAA,EAAsC,2GAAA;AAAA,EACtC,yBAAA,EAA6B,0CAAA;AAAA,EAC7B,iBAAA,EAAqB,uBAAA;AAAA,EACrB,oBAAA,EAAwB,oBAAA;AAAA,EACxB,uBAAA,EAA2B,iBAAA;AAAA,EAC3B,2BAAA,EAA+B,YAAA;AAAA,EAC/B,yBAAA,EAA6B,UAAA;AAAA,EAC7B,wBAAA,EAA4B,mBAAA;AAAA,EAC5B,uBAAA,EAA2B,mBAAA;AAAA,EAC3B,uBAAA,EAA2B,eAAA;AAAA,EAC3B,0BAAA,EAA8B,2HAAA;AAAA,EAC9B,sBAAA,EAA0B;AAC5B,CAAA;;;ACvBA,IAAA,aAAA,GAAA;AAAA,EACE,uBAAA,EAA2B,8GAAA;AAAA,EAC3B,uBAAA,EAA2B,kGAAA;AAAA,EAC3B,oBAAA,EAAwB,4IAAA;AAAA,EACxB,wBAAA,EAA4B,oHAAA;AAAA,EAC5B,iBAAA,EAAqB,0HAAA;AAAA,EACrB,kBAAA,EAAsB,4IAAA;AAAA,EACtB,iBAAA,EAAqB,gOAAA;AAAA,EACrB,sBAAA,EAA0B,wMAAA;AAAA,EAC1B,sBAAA,EAA0B,sFAAA;AAAA,EAC1B,qBAAA,EAAyB,4CAAA;AAAA,EACzB,kCAAA,EAAsC,6NAAA;AAAA,EACtC,yBAAA,EAA6B,8GAAA;AAAA,EAC7B,iBAAA,EAAqB,wGAAA;AAAA,EACrB,oBAAA,EAAwB,yCAAA;AAAA,EACxB,uBAAA,EAA2B,gCAAA;AAAA,EAC3B,2BAAA,EAA+B,oBAAA;AAAA,EAC/B,yBAAA,EAA6B,cAAA;AAAA,EAC7B,wBAAA,EAA4B,0BAAA;AAAA,EAC5B,uBAAA,EAA2B,0BAAA;AAAA,EAC3B,uBAAA,EAA2B,0EAAA;AAAA,EAC3B,0BAAA,EAA8B,sXAAA;AAAA,EAC9B,sBAAA,EAA0B;AAC5B,CAAA;;;ACvBA,IAAA,aAAA,GAAA;AAAA,EACE,uBAAA,EAA2B,wDAAA;AAAA,EAC3B,uBAAA,EAA2B,wDAAA;AAAA,EAC3B,oBAAA,EAAwB,sFAAA;AAAA,EACxB,wBAAA,EAA4B,8DAAA;AAAA,EAC5B,iBAAA,EAAqB,kDAAA;AAAA,EACrB,kBAAA,EAAsB,8DAAA;AAAA,EACtB,iBAAA,EAAqB,oHAAA;AAAA,EACrB,sBAAA,EAA0B,gFAAA;AAAA,EAC1B,sBAAA,EAA0B,oEAAA;AAAA,EAC1B,qBAAA,EAAyB,sCAAA;AAAA,EACzB,kCAAA,EAAsC,4LAAA;AAAA,EACtC,yBAAA,EAA6B,wDAAA;AAAA,EAC7B,iBAAA,EAAqB,kDAAA;AAAA,EACrB,oBAAA,EAAwB,yCAAA;AAAA,EACxB,uBAAA,EAA2B,0BAAA;AAAA,EAC3B,2BAAA,EAA+B,oBAAA;AAAA,EAC/B,yBAAA,EAA6B,cAAA;AAAA,EAC7B,wBAAA,EAA4B,0BAAA;AAAA,EAC5B,uBAAA,EAA2B,0BAAA;AAAA,EAC3B,uBAAA,EAA2B,wDAAA;AAAA,EAC3B,0BAAA,EAA8B,4OAAA;AAAA,EAC9B,sBAAA,EAA0B;AAC5B,CAAA;;;ACVO,IAAM,SAAA,GAAYA,IAAAA,CAAK,IAAA,CAAK,mBAAA,CAAqC,EAAE,OAAA,EAAS,aAAA,EAAI,OAAA,EAAS,aAAA,EAAI,OAAA,EAAS,aAAA,EAAI;ACH1G,SAAS,mBAAmB,IAAA,EAAsB;AACvD,EAAA,OAAOA,IAAAA,CAAK,QAAA,CAAS,YAAA,CAAa,IAAI,CAAA;AACxC","file":"chunk-7W47WZXY.js","sourcesContent":["import type {\n FeedbackAppendEvidenceInput,\n FeedbackAttachment,\n FeedbackAttachmentData,\n FeedbackBudget,\n FeedbackCompleteInput,\n FeedbackDetail,\n FeedbackEvidence,\n FeedbackEvidenceInput,\n FeedbackEvidenceQuery,\n FeedbackExportInput,\n FeedbackJob,\n FeedbackListInput,\n FeedbackReport,\n FeedbackRetryInput,\n FeedbackSetStatusInput,\n FeedbackSubmitInput,\n FeedbackUploadInput,\n} from '@h-ai/api-contract/feedback'\nimport type { ErrorInfo, HaiResult, PaginatedResult } from '@h-ai/core'\nimport type { FeedbackConfig, FeedbackConfigInput } from './feedback-config.js'\nimport { core } from '@h-ai/core'\n\nconst FeedbackErrorInfo = {\n NOT_INITIALIZED: '010:503',\n INIT_IN_PROGRESS: '011:409',\n CONFIG_ERROR: '013:500',\n OPERATION_FAILED: '020:500',\n NOT_FOUND: '021:404',\n FORBIDDEN: '022:403',\n CONFLICT: '023:409',\n LIMIT_EXCEEDED: '024:413',\n INVALID_INPUT: '025:400',\n CHECKSUM_MISMATCH: '026:422',\n QUOTA_EXCEEDED: '027:429',\n} satisfies ErrorInfo\n\n/**\n * Feedback 错误定义。\n * @example\n * ```ts\n * import { HaiFeedbackError } from '@h-ai/feedback'\n * const code = HaiFeedbackError.NOT_FOUND.code\n * void code\n * ```\n */\nexport const HaiFeedbackError = core.error.buildHaiErrorsDef('feedback', FeedbackErrorInfo)\n\n/**\n * 由可信服务端认证与授权策略生成的范围;HTTP 输入不能指定此对象。\n * @example\n * ```ts\n * import type { FeedbackContext } from '@h-ai/feedback'\n * const context: FeedbackContext = { scopeId: 'tenant-1', userId: 'user-1', permissions: ['feedback:create'] }\n * void context\n * ```\n */\nexport interface FeedbackContext {\n scopeId: string\n userId: string\n permissions?: readonly string[]\n}\n\n/**\n * 业务采集附件;ID 在同一反馈内稳定,正文由框架计算校验和并写入私有存储。\n * @example\n * ```ts\n * import type { FeedbackCollectedAttachment } from '@h-ai/feedback'\n * const file: FeedbackCollectedAttachment = { id: 'logs', name: 'logs.txt', contentType: 'text/plain', bytes: new TextEncoder().encode('diagnostics') }\n * void file\n * ```\n */\nexport interface FeedbackCollectedAttachment {\n id: string\n name: string\n contentType: string\n bytes: Uint8Array\n}\n\n/**\n * 业务补充材料;短文本/结构化信息出现在 business 证据,完整日志使用附件。\n * @example\n * ```ts\n * import type { FeedbackCollectedEvidence } from '@h-ai/feedback'\n * const evidence: FeedbackCollectedEvidence = { text: 'Order diagnostics', data: { orderId: 'order-1' } }\n * void evidence\n * ```\n */\nexport interface FeedbackCollectedEvidence {\n text?: string\n data?: FeedbackEvidenceInput['data']\n attachments?: FeedbackCollectedAttachment[]\n}\n\n/**\n * 采集扩展在持久任务中执行并等待异步结果;可能重试,宿主应按 report.id 幂等并响应 signal。\n * @example\n * ```ts\n * import type { FeedbackInitOptions } from '@h-ai/feedback'\n * const options: FeedbackInitOptions = { collectEvidence: async ({ report, signal }) => { signal.throwIfAborted(); return { text: report.number } } }\n * void options\n * ```\n */\nexport interface FeedbackInitOptions {\n collectEvidence?: (context: { report: Readonly<FeedbackReport>, signal: AbortSignal }) => FeedbackCollectedEvidence | Promise<FeedbackCollectedEvidence>\n}\n\n/**\n * 采集结束后发送的 feedback.created 事件;eventId 跨重试稳定,附件正文通过受权 API 读取。\n * @example\n * ```ts\n * import type { FeedbackWebhookEvent } from '@h-ai/feedback'\n * function eventKey(event: FeedbackWebhookEvent) { return event.eventId }\n * void eventKey\n * ```\n */\nexport interface FeedbackWebhookEvent {\n event: 'feedback.created'\n eventId: string\n createdAt: number\n report: FeedbackReport\n evidence: { revision: number, business: boolean, attachments: FeedbackAttachment[] }\n detailUrl?: string\n}\n\n/**\n * 反馈服务对象;领域操作返回 HaiResult,close 不释放宿主依赖连接。\n * @example\n * ```ts\n * import type { FeedbackContext, FeedbackFunctions } from '@h-ai/feedback'\n * async function submit(service: FeedbackFunctions, context: FeedbackContext) {\n * const result = await service.submit({ appId: 'orders', submissionId: 'stable-operation-id' }, context)\n * return result.success ? result.data.id : result.error.code\n * }\n * void submit\n * ```\n */\nexport interface FeedbackFunctions {\n init: (config: FeedbackConfigInput, options?: FeedbackInitOptions) => Promise<HaiResult<void>>\n close: () => Promise<HaiResult<void>>\n readonly isInitialized: boolean\n readonly config: FeedbackConfig | null\n readonly budget: FeedbackBudget\n submit: (input: FeedbackSubmitInput, context: FeedbackContext) => Promise<HaiResult<FeedbackReport>>\n list: (input: FeedbackListInput, context: FeedbackContext) => Promise<HaiResult<PaginatedResult<FeedbackReport>>>\n get: (id: string, context: FeedbackContext) => Promise<HaiResult<FeedbackDetail>>\n getEvidence: (input: FeedbackEvidenceQuery, context: FeedbackContext) => Promise<HaiResult<PaginatedResult<FeedbackEvidence>>>\n setStatus: (input: FeedbackSetStatusInput, context: FeedbackContext) => Promise<HaiResult<FeedbackReport>>\n appendEvidence: (input: FeedbackAppendEvidenceInput, context: FeedbackContext) => Promise<HaiResult<{ acknowledged: string[], evidenceRevision: number }>>\n completeCollection: (input: FeedbackCompleteInput, context: FeedbackContext) => Promise<HaiResult<FeedbackReport>>\n uploadAttachment: (input: FeedbackUploadInput, context: FeedbackContext) => Promise<HaiResult<FeedbackAttachment>>\n getAttachment: (input: { id: string, attachmentId: string }, context: FeedbackContext) => Promise<HaiResult<FeedbackAttachmentData>>\n retry: (input: FeedbackRetryInput, context: FeedbackContext) => Promise<HaiResult<void>>\n export: (input: FeedbackExportInput, context: FeedbackContext) => Promise<HaiResult<FeedbackJob>>\n getExport: (input: { id: string, jobId: string }, context: FeedbackContext) => Promise<HaiResult<FeedbackAttachmentData>>\n runJobs: () => Promise<HaiResult<{ processed: number }>>\n}\n\nexport type {\n FeedbackAppendEvidenceInput,\n FeedbackAttachment,\n FeedbackAttachmentData,\n FeedbackCompleteInput,\n FeedbackDetail,\n FeedbackEvidence,\n FeedbackEvidenceQuery,\n FeedbackExportInput,\n FeedbackJob,\n FeedbackListInput,\n FeedbackReport,\n FeedbackRetryInput,\n FeedbackSetStatusInput,\n FeedbackSubmitInput,\n FeedbackUploadInput,\n} from '@h-ai/api-contract/feedback'\n","{\n \"feedback_notInitialized\": \"Feedback has not been initialized\",\n \"feedback_initInProgress\": \"Feedback initialization is in progress\",\n \"feedback_configError\": \"Invalid feedback configuration or unavailable dependency\",\n \"feedback_operationFailed\": \"Feedback operation failed. Please retry\",\n \"feedback_notFound\": \"Feedback or evidence was not found\",\n \"feedback_forbidden\": \"Access to this feedback is denied\",\n \"feedback_conflict\": \"Feedback changed or an operation identifier conflicts. Refresh and retry\",\n \"feedback_exportPending\": \"The feedback bundle is being generated. Please retry shortly\",\n \"feedback_limitExceeded\": \"Feedback evidence exceeds the allowed capacity\",\n \"feedback_invalidInput\": \"Invalid feedback input\",\n \"feedback_unsupportedAttachmentType\": \"Unsupported attachment format. Choose a PNG, JPEG, WebP, WebM, MP4, TXT, LOG, JSON, PDF, ZIP or GZIP file\",\n \"feedback_checksumMismatch\": \"Attachment integrity verification failed\",\n \"feedback_received\": \"New feedback received\",\n \"feedback_submittedAt\": \"Submitted at (UTC)\",\n \"feedback_materialStatus\": \"Material status\",\n \"feedback_materialCollecting\": \"Collecting\",\n \"feedback_materialComplete\": \"Complete\",\n \"feedback_materialPartial\": \"Partially missing\",\n \"feedback_materialFailed\": \"Collection failed\",\n \"feedback_defaultSummary\": \"User feedback\",\n \"feedback_retryInputChanged\": \"The first submission has already been sent. Restore the original text and attachments to retry, then open a new feedback.\",\n \"feedback_quotaExceeded\": \"Feedback quota reached. Try later or contact an administrator.\"\n}\n","{\n \"feedback_notInitialized\": \"フィードバックが初期化されていません\",\n \"feedback_initInProgress\": \"フィードバックを初期化しています\",\n \"feedback_configError\": \"設定が無効、または依存サービスが利用できません\",\n \"feedback_operationFailed\": \"操作に失敗しました。再試行してください\",\n \"feedback_notFound\": \"フィードバックまたは資料が見つかりません\",\n \"feedback_forbidden\": \"このフィードバックへのアクセス権限がありません\",\n \"feedback_conflict\": \"フィードバックが更新されたか操作識別子が重複しています。再読込してください\",\n \"feedback_exportPending\": \"問題パッケージを生成しています。しばらくしてから再試行してください\",\n \"feedback_limitExceeded\": \"資料が許容容量を超えています\",\n \"feedback_invalidInput\": \"入力が無効です\",\n \"feedback_unsupportedAttachmentType\": \"未対応の添付形式です。PNG、JPEG、WebP、WebM、MP4、TXT、LOG、JSON、PDF、ZIP、GZIP を選択してください\",\n \"feedback_checksumMismatch\": \"添付ファイルの整合性を確認できません\",\n \"feedback_received\": \"新しいフィードバックを受信しました\",\n \"feedback_submittedAt\": \"報告日時(UTC)\",\n \"feedback_materialStatus\": \"資料の状態\",\n \"feedback_materialCollecting\": \"収集中\",\n \"feedback_materialComplete\": \"完全\",\n \"feedback_materialPartial\": \"一部不足\",\n \"feedback_materialFailed\": \"収集失敗\",\n \"feedback_defaultSummary\": \"ユーザーのフィードバック\",\n \"feedback_retryInputChanged\": \"最初の送信は開始されています。元の説明と添付ファイルに戻して送信結果を確認してから、新しいフィードバックを作成してください。\",\n \"feedback_quotaExceeded\": \"フィードバックの上限に達しました。後で再試行するか管理者に連絡してください。\"\n}\n","{\n \"feedback_notInitialized\": \"反馈模块尚未初始化\",\n \"feedback_initInProgress\": \"反馈模块正在初始化\",\n \"feedback_configError\": \"反馈配置无效或前置服务不可用\",\n \"feedback_operationFailed\": \"反馈操作失败,请重试\",\n \"feedback_notFound\": \"反馈或材料不存在\",\n \"feedback_forbidden\": \"没有访问该反馈的权限\",\n \"feedback_conflict\": \"反馈已更新或操作标识冲突,请刷新后重试\",\n \"feedback_exportPending\": \"问题包正在生成,请稍后重试\",\n \"feedback_limitExceeded\": \"反馈材料超过允许的容量\",\n \"feedback_invalidInput\": \"反馈参数无效\",\n \"feedback_unsupportedAttachmentType\": \"不支持此附件格式,请选择 PNG、JPEG、WebP、WebM、MP4、TXT、LOG、JSON、PDF、ZIP 或 GZIP 文件\",\n \"feedback_checksumMismatch\": \"附件完整性校验失败\",\n \"feedback_received\": \"收到新的问题反馈\",\n \"feedback_submittedAt\": \"反馈时间(UTC)\",\n \"feedback_materialStatus\": \"材料状态\",\n \"feedback_materialCollecting\": \"收集中\",\n \"feedback_materialComplete\": \"完整\",\n \"feedback_materialPartial\": \"部分缺失\",\n \"feedback_materialFailed\": \"采集失败\",\n \"feedback_defaultSummary\": \"用户提交的问题反馈\",\n \"feedback_retryInputChanged\": \"首次提交已发出,重试需保持原说明和附件。请恢复原输入完成确认后,再发起新反馈。\",\n \"feedback_quotaExceeded\": \"反馈配额已用完,请稍后重试或联系管理员\"\n}\n","import { core } from '@h-ai/core'\nimport en from '../messages/en-US.json'\nimport ja from '../messages/ja-JP.json'\nimport zh from '../messages/zh-CN.json'\n\n/**\n * 模块内消息获取器。\n * @example 模块内部上下文\n * ```ts\n * const message = feedbackM('feedback_notFound')\n * void message\n * ```\n */\nexport const feedbackM = core.i18n.createMessageGetter<keyof typeof zh>({ 'zh-CN': zh, 'en-US': en, 'ja-JP': ja })\n","import { core } from '@h-ai/core'\n\n/**\n * Node 与 Browser 共用的自由文本凭据脱敏;URL 与结构化字段仍复用 core。\n * @example 模块内部上下文\n * ```ts\n * const safeText = redactFeedbackText('{\"password\":\"example-only\"}')\n * void safeText\n * ```\n */\nexport function redactFeedbackText(text: string): string {\n return core.sanitize.sanitizeText(text)\n}\n"]}
|
|
@@ -204,6 +204,8 @@ function parseManifest(value) {
|
|
|
204
204
|
}
|
|
205
205
|
async function createFeedbackBundle(input) {
|
|
206
206
|
try {
|
|
207
|
+
if (input.createdAt !== void 0 && (!Number.isSafeInteger(input.createdAt) || input.createdAt < 0))
|
|
208
|
+
throw new Error("Invalid bundle creation time");
|
|
207
209
|
const report = jsonObject(input.report);
|
|
208
210
|
if (typeof report.id !== "string" || !SAFE_ID.test(report.id))
|
|
209
211
|
throw new Error("Invalid report");
|
|
@@ -237,7 +239,7 @@ async function createFeedbackBundle(input) {
|
|
|
237
239
|
files[path] = new Uint8Array(attachment.bytes);
|
|
238
240
|
manifestFiles.push({ path, size: attachment.bytes.byteLength, sha256: hash, kind: "attachment", attachmentId: attachment.id, name: attachment.name, contentType: attachment.contentType });
|
|
239
241
|
}
|
|
240
|
-
const manifest = { format: "hai-feedback", version: 1, reportId: report.id, evidenceRevision: typeof report.evidenceRevision === "number" ? report.evidenceRevision : 0, createdAt: Date.now(), missing, files: manifestFiles };
|
|
242
|
+
const manifest = { format: "hai-feedback", version: 1, reportId: report.id, evidenceRevision: typeof report.evidenceRevision === "number" ? report.evidenceRevision : 0, createdAt: input.createdAt ?? Date.now(), missing, files: manifestFiles };
|
|
241
243
|
const temporary = { report: { ...report, id: report.id }, evidence, history, manifest, files };
|
|
242
244
|
files["summary.md"] = strToU8(summarizeFeedbackBundle(temporary));
|
|
243
245
|
for (const [path, bytes] of Object.entries(files)) {
|
|
@@ -334,5 +336,5 @@ function getFeedbackBundleViewerFiles() {
|
|
|
334
336
|
}
|
|
335
337
|
|
|
336
338
|
export { createFeedbackBundle, getFeedbackBundleViewerFiles, queryFeedbackBundle, readFeedbackBundle, summarizeFeedbackBundle };
|
|
337
|
-
//# sourceMappingURL=chunk-
|
|
338
|
-
//# sourceMappingURL=chunk-
|
|
339
|
+
//# sourceMappingURL=chunk-KGMLTOMM.js.map
|
|
340
|
+
//# sourceMappingURL=chunk-KGMLTOMM.js.map
|