@h-ai/feedback 1.0.0-beta.2 → 1.0.0-beta.21

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 CHANGED
@@ -4,19 +4,9 @@
4
4
 
5
5
  [Framework](../../README.en.md)
6
6
 
7
- ## Introduction
8
-
9
7
  Collect user feedback and diagnostic evidence, notify administrators, review reports and export offline bundles. Descriptions are optional. Reports and structured evidence use a relational database; screenshots and binary attachments use private object storage. A separate telemetry service is not required.
10
8
 
11
- ## Features
12
-
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
- - 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` (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
- - Browser screenshots, irreversible masking, annotations, optional rrweb recording, retries and local fallback export; `FeedbackModal`, `FeedbackList` and `FeedbackDetail` in `@h-ai/ui`.
17
- - Self-contained ZIP files, SHA-256 validation, an offline viewer and paginated evidence queries without production credentials.
18
-
19
- ## Getting started
9
+ ## Quick Start
20
10
 
21
11
  Supply `appVersion/buildId/commit` from each actual browser or server artifact. A browser must retain its own identity; one SSR build shares its buildId across client and server output. Admin Console and generated base/API/fullstack projects inline these values. `hai build` creates internal metadata and passes it through Docker build arguments. Each target reads its own package.json version; a missing owning Git commit is `unknown`. Template `build-metadata.json` contains only these three nonsecret fields and matches the inlined values.
22
12
 
@@ -35,7 +25,11 @@ const initialized = await feedback.init({
35
25
  })
36
26
  if (!initialized.success)
37
27
  return initialized
38
- const context = { scopeId: 'tenant-1', userId: 'user-1', permissions: ['feedback:create'] }
28
+ const context = {
29
+ scopeId: 'tenant-1',
30
+ userId: 'user-1',
31
+ permissions: ['feedback:create']
32
+ }
39
33
  const submitted = await feedback.submit({ appId: 'orders', submissionId: 'stable-client-operation' }, context)
40
34
  if (!submitted.success)
41
35
  return submitted
@@ -76,6 +70,61 @@ After acceptance, the SDK uploads attachments and subsequent evidence in the bac
76
70
 
77
71
  Automatic screenshots use html2canvas to render a redacted DOM clone. 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.
78
72
 
73
+ ## Features
74
+
75
+ - Bounded browser/server logs, requests, errors, interactions, versions and allowed environment fields; up to 100 log records before and after an error by default.
76
+ - Transactional intake, stable submission/evidence identifiers, checksum validation, independent processing/collection/notification state and durable leased jobs.
77
+ - `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.
78
+ - Browser screenshots, irreversible masking, annotations, optional rrweb recording, retries and local fallback export; `FeedbackModal`, `FeedbackList` and `FeedbackDetail` in `@h-ai/ui`.
79
+ - Self-contained ZIP files, SHA-256 validation, an offline viewer and paginated evidence queries without production credentials.
80
+
81
+ ### Operations and results
82
+
83
+ Use the complete initialization flow in this section first. The host validates and authorizes example business values; check success before reading data from asynchronous domain results. Here void means completion without data, null means absent; synchronous factories/tools return the listed type directly.
84
+
85
+ input.id is a report ID; attachmentId/jobId must belong to it. context comes from trusted authentication with subject/scope/permissions. Evidence, submission and base64 remain bounded and ownership-checked.
86
+
87
+ #### feedback
88
+
89
+ Accept/authorize reports: input plus trusted context yields a report; list filters/page yield a page, get ID yields details, getEvidence ID/filters yield evidence pages.
90
+
91
+ | Entry | Input parameters | Successful result | Call example |
92
+ | ---------------------- | ---------------- | --------------------------------------- | ------------------------------------------------------------------------------ |
93
+ | `feedback.submit` | input, context | `Report` | `await feedback.submit(submission, context)` |
94
+ | `feedback.list` | input, context | `Report page` | `await feedback.list({ page: 1, pageSize: 20 }, context)` |
95
+ | `feedback.get` | id, context | `Report, attachments, history and jobs` | `await feedback.get(reportId, context)` |
96
+ | `feedback.getEvidence` | input, context | `Evidence page` | `await feedback.getEvidence({ id: reportId, page: 1, pageSize: 20 }, context)` |
97
+
98
+ #### feedback
99
+
100
+ Admin handling takes ID, status/expectedVersion or retry target and trusted context; returns the report or void. Authorize first and refresh versions on conflict.
101
+
102
+ | Entry | Input parameters | Successful result | Call example |
103
+ | -------------------- | ---------------- | ----------------- | -------------------------------------------------------------------------------------------------- |
104
+ | `feedback.setStatus` | input, context | `Updated report` | `await feedback.setStatus({ id: reportId, status: 'handled', expectedVersion: version }, context)` |
105
+ | `feedback.retry` | input, context | `void` | `await feedback.retry({ id: reportId, target: 'collect' }, context)` |
106
+
107
+ #### feedback
108
+
109
+ Collection takes report ID with evidence, missing markers or attachment ID/base64; returns acknowledgements/revision, report, metadata or base64. All require trusted context and the same quota/ownership rules.
110
+
111
+ | Entry | Input parameters | Successful result | Call example |
112
+ | ----------------------------- | ---------------- | ---------------------------------------- | -------------------------------------------------------------------------------------- |
113
+ | `feedback.appendEvidence` | input, context | `Acknowledged IDs and evidence revision` | `await feedback.appendEvidence({ id: reportId, evidence }, context)` |
114
+ | `feedback.completeCollection` | input, context | `Report` | `await feedback.completeCollection({ id: reportId }, context)` |
115
+ | `feedback.uploadAttachment` | input, context | `Attachment metadata` | `await feedback.uploadAttachment({ id: reportId, attachmentId, dataBase64 }, context)` |
116
+ | `feedback.getAttachment` | input, context | `Name, type and base64` | `await feedback.getAttachment({ id: reportId, attachmentId }, context)` |
117
+
118
+ #### feedback
119
+
120
+ Export takes report ID/revision and yields a job; getExport takes report/job IDs and yields ZIP/base64, polling only bounded pending states; runJobs processes due jobs and yields processed count.
121
+
122
+ | Entry | Input parameters | Successful result | Call example |
123
+ | -------------------- | ---------------- | --------------------------- | ------------------------------------------------------------ |
124
+ | `feedback.export` | input, context | `Export job` | `await feedback.export({ id: reportId }, context)` |
125
+ | `feedback.getExport` | input, context | `ZIP name, type and base64` | `await feedback.getExport({ id: reportId, jobId }, context)` |
126
+ | `feedback.runJobs` | No arguments | `processed count` | `await feedback.runJobs()` |
127
+
79
128
  ### Feedback capture and delivery checks
80
129
 
81
130
  The current screenshot is shown first. `captureScreenshot(attachmentId?)` adds the visible page or replaces that attachment. Connect `FeedbackModal.oncapture` to it: the modal hides while the user navigates and chooses “Capture this page”, retaining form input. `FeedbackForm.evidence` exposes sanitized logs, requests, environment facts and replay events; use `evidenceSummary` for missing-material notes. Browser facts come from the actual URL (without query/hash), viewport, language and compiled build. Server environment variables include only non-credential `envAllowlist` keys; absent values are null.
@@ -110,12 +159,14 @@ async function startFeedback(config: FeedbackConfigInput) {
110
159
  collectEvidence: async ({ report, signal }) => ({
111
160
  text: `Diagnostics for ${report.number}`,
112
161
  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
- }],
162
+ attachments: [
163
+ {
164
+ id: 'business-logs',
165
+ name: 'business-logs.zip',
166
+ contentType: 'application/zip',
167
+ bytes: await readFile('diagnostics/sanitized-logs.zip', { signal }),
168
+ }
169
+ ],
119
170
  }),
120
171
  })
121
172
  if (!result.success)
@@ -180,11 +231,23 @@ Reference admin/fullstack feedback startup failures leave required business modu
180
231
 
181
232
  ### Privacy boundary of console notifications
182
233
 
183
- `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.
234
+ `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.
184
235
 
185
236
  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.
186
237
 
187
- ## Development and testing
238
+ ## Configuration
239
+
240
+ Lifecycle: init takes backend/business configuration; use the module only after success. Await close before releasing borrowed dependencies. Follow the quick-start example and public signatures for exact return values.
241
+
242
+ The host loads and validates configuration before passing it to this module. Keep secrets in private server environment files. Select the backend, capacity, deadlines and logging for the actual use case; reinitialize and check the result after changes.
243
+
244
+ [Per-option purpose, use cases and examples](../../docs/usage/configuration/operations.md).
245
+
246
+ ## Error Handling
247
+
248
+ Check result.success for domain operations. Handle failures with the module error definitions and safe messages; retain codes, correlation IDs and sanitized causes in server logs. Synchronous tools, factories, stream iteration and lifecycle calls follow their own signatures. A completed Promise alone is not business success. See each feature for recovery limits.
249
+
250
+ ## Development
188
251
 
189
252
  ```sh
190
253
  pnpm --filter @h-ai/feedback typecheck
package/README.ja.md CHANGED
@@ -4,19 +4,9 @@
4
4
 
5
5
  [フレームワーク](../../README.ja.md)
6
6
 
7
- ## 概要
8
-
9
7
  ユーザーの問題報告と診断情報の収集、管理者への通知、一覧処理、オフライン問題パッケージの出力を提供します。説明は任意です。報告と構造化された証拠は関係データベースに、スクリーンショットなどは非公開オブジェクトストレージに保存します。別の telemetry サービスは不要です。
10
8
 
11
- ## 主な機能
12
-
13
- - ブラウザーとサーバーのログ、リクエスト、操作、バージョン、許可された環境情報を上限付きで収集します。既定ではエラーの前後それぞれ最大100件です。
14
- - トランザクションによる受理、提出と証拠の冪等性、checksum 検証、処理・収集・通知状態の分離、永続ジョブとリース回復。
15
- - `notificationMode` は独立して選択できます。`disabled` は通知ジョブなし、`console` は本番でも既定でメタデータだけを出力し Reach/メール不要(consoleContent: summary で一般的なマスキング済み概要を明示)、`reach` は任意の Provider と宛先を使い、このモードのみ recipients が必要です。既定の `reach` は既存設定を維持し、生成プロジェクトは `console` を明示します。Reach DND 中は feedback 自身の outbox で待機し、二重の遅延キューや他通知の方針変更を行いません。本番 Reach Console の制限は維持します。sent はチャネル受付であり受信完了を意味しません。
16
- - スクリーンショット、復元不能なマスク、注釈、任意の rrweb 記録、再試行とローカル出力。UI は `FeedbackModal`、`FeedbackList`、`FeedbackDetail` を提供します。
17
- - 自己完結 ZIP、SHA-256 検証、オフライン表示、証拠のページ検索。運用認証情報は含めません。
18
-
19
- ## はじめに
9
+ ## クイックスタート
20
10
 
21
11
  ブラウザーとサーバーには各実成果物の `appVersion/buildId/commit` を渡します。フロント側は自分の識別情報を保持し、同じ SSR ビルドの client/server 出力は buildId を共有します。管理画面と base/API/fullstack テンプレートにはコンパイル時の識別情報を組み込んでいます。`hai build` は内部で識別情報を生成し、Docker のビルド引数にも渡します。バージョンは各対象の package.json から取得し、所属する Git コミットがない場合は `unknown` を記録します。テンプレートの `build-metadata.json` には同じ三つの非機密項目のみを保存します。
22
12
 
@@ -35,7 +25,11 @@ const initialized = await feedback.init({
35
25
  })
36
26
  if (!initialized.success)
37
27
  return initialized
38
- const context = { scopeId: 'tenant-1', userId: 'user-1', permissions: ['feedback:create'] }
28
+ const context = {
29
+ scopeId: 'tenant-1',
30
+ userId: 'user-1',
31
+ permissions: ['feedback:create']
32
+ }
39
33
  const submitted = await feedback.submit({ appId: 'orders', submissionId: 'stable-client-operation' }, context)
40
34
  if (!submitted.success)
41
35
  return submitted
@@ -76,6 +70,61 @@ return submitted
76
70
 
77
71
  自動撮影は html2canvas で機密部分を隠したクローン DOM を描画します。クロスオリジン画像、iframe/video、未準備のネットワークフォントは欠落する場合があります。ブラウザーや CSP 制限による失敗は missing として記録され、画像の手動添付も利用できます。
78
72
 
73
+ ## 機能
74
+
75
+ - ブラウザーとサーバーのログ、リクエスト、操作、バージョン、許可された環境情報を上限付きで収集します。既定ではエラーの前後それぞれ最大100件です。
76
+ - トランザクションによる受理、提出と証拠の冪等性、checksum 検証、処理・収集・通知状態の分離、永続ジョブとリース回復。
77
+ - `notificationMode` は独立して選択できます。`disabled` は通知ジョブなし、`console` は本番でも既定でメタデータだけを出力し Reach/メール不要(consoleContent: summary で一般的なマスキング済み概要を明示)、`reach` は任意の Provider と宛先を使い、このモードのみ recipients が必要です。既定は `reach`、生成プロジェクトは `console` を明示します。Reach DND 中は feedback 自身の outbox で待機し、二重の遅延キューや他通知の方針変更を行いません。本番 Reach Console の制限は維持します。sent はチャネル受付であり受信完了を意味しません。
78
+ - スクリーンショット、復元不能なマスク、注釈、任意の rrweb 記録、再試行とローカル出力。UI は `FeedbackModal`、`FeedbackList`、`FeedbackDetail` を提供します。
79
+ - 自己完結 ZIP、SHA-256 検証、オフライン表示、証拠のページ検索。運用認証情報は含めません。
80
+
81
+ ### 操作と入出力
82
+
83
+ 本節の完全な初期化手順を先に実行します。例の業務値はホストが検証・認可し、非同期領域結果は success を確認してから data を読みます。void はデータなし完了、null は不存在、同期ファクトリー/ツールは表の型を直接返します。
84
+
85
+ input.id は報告 ID、attachmentId/jobId は同じ報告に所属。context は信頼する認証の主体/範囲/権限。証拠・提出・base64 にも容量/所有権検査が適用。
86
+
87
+ #### feedback
88
+
89
+ 受付と権限付き照会。入力と信頼する context から報告、list は条件/ページから一覧、get は ID から詳細、getEvidence は ID/条件から証拠ページ。
90
+
91
+ | 入口 | 入力引数 | 成功結果 | 呼出し例 |
92
+ | ---------------------- | -------------- | -------------------------- | ------------------------------------------------------------------------------ |
93
+ | `feedback.submit` | input, context | `報告` | `await feedback.submit(submission, context)` |
94
+ | `feedback.list` | input, context | `報告ページ` | `await feedback.list({ page: 1, pageSize: 20 }, context)` |
95
+ | `feedback.get` | id, context | `報告・添付・履歴・ジョブ` | `await feedback.get(reportId, context)` |
96
+ | `feedback.getEvidence` | input, context | `証拠ページ` | `await feedback.getEvidence({ id: reportId, page: 1, pageSize: 20 }, context)` |
97
+
98
+ #### feedback
99
+
100
+ 管理処理は ID・状態/expectedVersion または retry target と信頼する context から報告/void。先に認可し、競合時は版を再取得。
101
+
102
+ | 入口 | 入力引数 | 成功結果 | 呼出し例 |
103
+ | -------------------- | -------------- | ---------- | -------------------------------------------------------------------------------------------------- |
104
+ | `feedback.setStatus` | input, context | `更新報告` | `await feedback.setStatus({ id: reportId, status: 'handled', expectedVersion: version }, context)` |
105
+ | `feedback.retry` | input, context | `void` | `await feedback.retry({ id: reportId, target: 'collect' }, context)` |
106
+
107
+ #### feedback
108
+
109
+ 追加収集は報告 ID と証拠・missing・添付 ID/base64 を入力し、確認 ID/改訂・報告・メタデータ・base64 を返す。信頼する context と容量/所有権制限が必須。
110
+
111
+ | 入口 | 入力引数 | 成功結果 | 呼出し例 |
112
+ | ----------------------------- | -------------- | ------------------------ | -------------------------------------------------------------------------------------- |
113
+ | `feedback.appendEvidence` | input, context | `確認 ID 配列と証拠改訂` | `await feedback.appendEvidence({ id: reportId, evidence }, context)` |
114
+ | `feedback.completeCollection` | input, context | `報告` | `await feedback.completeCollection({ id: reportId }, context)` |
115
+ | `feedback.uploadAttachment` | input, context | `添付メタデータ` | `await feedback.uploadAttachment({ id: reportId, attachmentId, dataBase64 }, context)` |
116
+ | `feedback.getAttachment` | input, context | `名前・種類・base64` | `await feedback.getAttachment({ id: reportId, attachmentId }, context)` |
117
+
118
+ #### feedback
119
+
120
+ export は報告 ID/改訂からジョブ、getExport は報告/ジョブ ID から ZIP/base64。生成中だけ有限ポーリング。runJobs は期限到来分を処理し processed 件数。
121
+
122
+ | 入口 | 入力引数 | 成功結果 | 呼出し例 |
123
+ | -------------------- | -------------- | -------------------------- | ------------------------------------------------------------ |
124
+ | `feedback.export` | input, context | `出力ジョブ` | `await feedback.export({ id: reportId }, context)` |
125
+ | `feedback.getExport` | input, context | `ZIP の名前・種類・base64` | `await feedback.getExport({ id: reportId, jobId }, context)` |
126
+ | `feedback.runJobs` | 引数なし | `processed 件数` | `await feedback.runJobs()` |
127
+
79
128
  ### フィードバック収集と配布確認
80
129
 
81
130
  現在のページ画像を先頭に表示します。`captureScreenshot(attachmentId?)` は現在のページを追加し、IDを渡すと既存画像を置換します。`FeedbackModal.oncapture` に接続すると、入力を保持して画面を閉じ、ページを切り替えて撮影できます。`FeedbackForm.evidence` は脱敏済みログ、リクエスト、環境と操作記録を展開表示し、`evidenceSummary` は不足理由を補足します。ブラウザー環境は実際のURL(クエリとhashを除外)、画面、言語とビルドから取得します。サーバーは `envAllowlist` の非認証情報だけを収集し、未設定値は null です。
@@ -110,12 +159,14 @@ async function startFeedback(config: FeedbackConfigInput) {
110
159
  collectEvidence: async ({ report, signal }) => ({
111
160
  text: `Diagnostics for ${report.number}`,
112
161
  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
- }],
162
+ attachments: [
163
+ {
164
+ id: 'business-logs',
165
+ name: 'business-logs.zip',
166
+ contentType: 'application/zip',
167
+ bytes: await readFile('diagnostics/sanitized-logs.zip', { signal }),
168
+ }
169
+ ],
119
170
  }),
120
171
  })
121
172
  if (!result.success)
@@ -180,10 +231,22 @@ hai feedback view feedback-FB-1.zip
180
231
 
181
232
  ### console 通知のプライバシー境界
182
233
 
183
- `consoleContent` の既定は `metadata` で、報告番号、アプリ・バージョン・ビルド情報と収集状態だけを記録します。説明・概要・添付・証拠・通知本文・宛先は含みません。一般的なマスキング済み概要が必要な場合のみ `summary` を明示します。一般的なマスキングでは全業務機密を検出できません。Reach は不要で disabled/reach の意味は変わりません。更新後の既存 console 設定も既定でメタデータだけになります。
234
+ `consoleContent` の既定は `metadata` で、報告番号、アプリ・バージョン・ビルド情報と収集状態だけを記録します。説明・概要・添付・証拠・通知本文・宛先は含みません。一般的なマスキング済み概要が必要な場合のみ `summary` を明示します。一般的なマスキングでは全業務機密を検出できません。Reach は不要で disabled/reach の意味は変わりません。
184
235
 
185
236
  サーバー環境証拠は受付トランザクションで同時に保存します。環境保存失敗時は不完全な受付を残さず、同じ submissionId で再試行できます。管理画面は `createFeedbackReview(...).retryReport(id)` を使い、復旧可能な収集と通知/Webhook失敗を再試行します。対象を重複排除し失敗を伝播し、close後は後続処理を止めます。
186
237
 
238
+ ## 設定
239
+
240
+ ライフサイクル。init にバックエンド/業務設定を渡し成功後に使用します。借用依存先の解放前に close の完了を待ち、正確な戻り値は例と公開署名に従います。
241
+
242
+ 設定はホストが読込・検証してから渡します。秘密はサーバーの非公開環境に保存し、利用場面に応じてバックエンド・容量・期限・ログを選びます。変更後は再初期化の結果を確認してください。
243
+
244
+ [項目別の用途・適用場面・例](../../docs/usage/configuration/operations.md).
245
+
246
+ ## エラー処理
247
+
248
+ 領域操作は result.success を確認し、失敗時はモジュールのエラー定義と安全な説明を使います。コード・関連 ID・秘匿した原因をサーバーログに残します。同期ツール・ファクトリー・ストリーム反復・ライフサイクルは各署名に従い、Promise 完了だけで成功と判断しません。回復条件は各機能を参照してください。
249
+
187
250
  ## 開発とテスト
188
251
 
189
252
  ```sh
package/README.md CHANGED
@@ -4,19 +4,9 @@
4
4
 
5
5
  [框架首页](../../README.md)
6
6
 
7
- ## 简介
8
-
9
7
  提供用户反馈、诊断现场、管理员处理与离线问题包。用户可以不填说明直接提交;反馈和结构化证据保存到关系型数据库,截图等附件进入私有对象存储,管理员通过邮件和管理界面处理。它不依赖通用 telemetry 服务。
10
8
 
11
- ## 核心功能清单
12
-
13
- - 有界采集前后端日志、请求、错误、操作、版本与白名单环境信息;默认错误前后各最多 100 条。
14
- - 数据库事务受理、提交/证据幂等、附件校验、独立的处理/采集/通知状态、持久化任务与租约恢复。
15
- - 通知由 `notificationMode` 独立选择:`disabled` 不创建通知任务;`console` 默认仅输出定位与采集状态元数据(consoleContent: metadata);显式 consoleContent: summary 才加入通用脱敏摘要,不依赖 Reach 或邮箱;`reach` 使用任意已配置 Provider 与接收方,仅此模式要求 recipients。默认 `reach` 保持旧配置兼容,新生成工程显式使用 `console`。Reach 免打扰期间由反馈自己的 outbox 等待,不进入 Reach 延迟队列,也不修改其他通知策略。Reach 的生产 Console Provider 限制保持不变;sent 仅表示选定通道受理,不能证明收信。
16
- - 浏览器截图、遮挡/标注、可选 rrweb 回放、失败重试与本地导出;`@h-ai/ui` 提供 `FeedbackModal`、`FeedbackList` 和 `FeedbackDetail`。
17
- - 自包含 ZIP、SHA-256 校验、离线查看与按请求/错误/时间分页读取,不包含生产凭据。
18
-
19
- ## 使用入门
9
+ ## 快速开始
20
10
 
21
11
  服务端先初始化 core、reldb 和私有 storage,再初始化 feedback;仅 reach 通知模式需先初始化 Reach。生产使用共享数据库和私有对象存储,连接凭据由宿主管理。
22
12
 
@@ -34,7 +24,11 @@ const initialized = await feedback.init({
34
24
  if (!initialized.success)
35
25
  return initialized
36
26
 
37
- const context = { scopeId: 'tenant-1', userId: 'user-1', permissions: ['feedback:create'] }
27
+ const context = {
28
+ scopeId: 'tenant-1',
29
+ userId: 'user-1',
30
+ permissions: ['feedback:create']
31
+ }
38
32
  const submitted = await feedback.submit({ appId: 'orders', submissionId: 'stable-client-operation' }, context)
39
33
  if (!submitted.success)
40
34
  return submitted
@@ -82,6 +76,61 @@ return submitted
82
76
 
83
77
  Web 客户端不解析或安装原生插件。自定义 Tauri/Capacitor 宿主分别从 @h-ai/feedback/tauri 或 @h-ai/feedback/capacitor 导入 saveTauriFeedbackFile/saveCapacitorFeedbackFile,并在启动时调用 configureFeedbackSaving(saver);生成工程已接入,业务无需处理原生文件生命周期。
84
78
 
79
+ ## 功能
80
+
81
+ - 有界采集前后端日志、请求、错误、操作、版本与白名单环境信息;默认错误前后各最多 100 条。
82
+ - 数据库事务受理、提交/证据幂等、附件校验、独立的处理/采集/通知状态、持久化任务与租约恢复。
83
+ - 通知由 `notificationMode` 独立选择:`disabled` 不创建通知任务;`console` 默认仅输出定位与采集状态元数据(consoleContent: metadata);显式 consoleContent: summary 才加入通用脱敏摘要,不依赖 Reach 或邮箱;`reach` 使用任意已配置 Provider 与接收方,仅此模式要求 recipients。默认 `reach`,生成工程显式使用 `console`。Reach 免打扰期间由反馈自己的 outbox 等待,不进入 Reach 延迟队列,也不修改其他通知策略。Reach 的生产 Console Provider 限制保持不变;sent 仅表示选定通道受理,不能证明收信。
84
+ - 浏览器截图、遮挡/标注、可选 rrweb 回放、失败重试与本地导出;`@h-ai/ui` 提供 `FeedbackModal`、`FeedbackList` 和 `FeedbackDetail`。
85
+ - 自包含 ZIP、SHA-256 校验、离线查看与按请求/错误/时间分页读取,不包含生产凭据。
86
+
87
+ ### 功能输入输出
88
+
89
+ 以下片段依赖本节完整初始化流程。示例中的业务值由宿主校验和授权后传入;异步领域结果先判断 success,再读取 data。表中 void 表示操作已完成且没有返回数据,null 表示不存在;同步工厂/工具直接返回表中类型。
90
+
91
+ input 中的 id 是反馈 ID,attachmentId/jobId 指向同一反馈;context 来自可信认证,包含当前主体/范围/权限。evidence、submission 和 base64 均受容量及归属检查。
92
+
93
+ #### feedback
94
+
95
+ 受理/受权查询:提交结构和可信 context 返回报告;list 输入筛选/页码返回分页;get 输入 ID 返回详情;getEvidence 输入 ID/筛选返回分页证据。
96
+
97
+ | 入口 | 输入参数 | 成功结果 | 调用示例 |
98
+ | ---------------------- | -------------- | ------------------------ | ------------------------------------------------------------------------------ |
99
+ | `feedback.submit` | input, context | `報告` | `await feedback.submit(submission, context)` |
100
+ | `feedback.list` | input, context | `报告分页` | `await feedback.list({ page: 1, pageSize: 20 }, context)` |
101
+ | `feedback.get` | id, context | `报告、附件、历史、任务` | `await feedback.get(reportId, context)` |
102
+ | `feedback.getEvidence` | input, context | `证据分页` | `await feedback.getEvidence({ id: reportId, page: 1, pageSize: 20 }, context)` |
103
+
104
+ #### feedback
105
+
106
+ 管理员处理输入 ID、状态/expectedVersion 或重试 target,并传可信 context;分别返回更新报告或 void。只有明确授权后调用,冲突先刷新版本。
107
+
108
+ | 入口 | 输入参数 | 成功结果 | 调用示例 |
109
+ | -------------------- | -------------- | -------------- | -------------------------------------------------------------------------------------------------- |
110
+ | `feedback.setStatus` | input, context | `更新后的报告` | `await feedback.setStatus({ id: reportId, status: 'handled', expectedVersion: version }, context)` |
111
+ | `feedback.retry` | input, context | `void` | `await feedback.retry({ id: reportId, target: 'collect' }, context)` |
112
+
113
+ #### feedback
114
+
115
+ 补采输入报告 ID 与证据、missing 或附件 ID/base64;返回确认 ID/修订、报告、附件元数据或附件 base64。所有调用携带可信 context,并受同一容量/归属限制。
116
+
117
+ | 入口 | 输入参数 | 成功结果 | 调用示例 |
118
+ | ----------------------------- | -------------- | ------------------------ | -------------------------------------------------------------------------------------- |
119
+ | `feedback.appendEvidence` | input, context | `确认 ID 数组与证据修订` | `await feedback.appendEvidence({ id: reportId, evidence }, context)` |
120
+ | `feedback.completeCollection` | input, context | `报告` | `await feedback.completeCollection({ id: reportId }, context)` |
121
+ | `feedback.uploadAttachment` | input, context | `附件元数据` | `await feedback.uploadAttachment({ id: reportId, attachmentId, dataBase64 }, context)` |
122
+ | `feedback.getAttachment` | input, context | `文件名、类型与 base64` | `await feedback.getAttachment({ id: reportId, attachmentId }, context)` |
123
+
124
+ #### feedback
125
+
126
+ 导出输入报告 ID/证据修订返回任务;getExport 输入报告与任务 ID 返回 ZIP/base64,仅对生成中有界轮询;runJobs 无参数处理到期任务返回 processed 数。
127
+
128
+ | 入口 | 输入参数 | 成功结果 | 调用示例 |
129
+ | -------------------- | -------------- | --------------------------- | ------------------------------------------------------------ |
130
+ | `feedback.export` | input, context | `导出任务` | `await feedback.export({ id: reportId }, context)` |
131
+ | `feedback.getExport` | input, context | `ZIP 文件名、类型与 base64` | `await feedback.getExport({ id: reportId, jobId }, context)` |
132
+ | `feedback.runJobs` | 无参数 | `processed 处理数` | `await feedback.runJobs()` |
133
+
85
134
  ### 反馈现场与交付检查
86
135
 
87
136
  当前页面截图优先展示;`captureScreenshot(attachmentId?)` 补充当前页面,传附件 ID 则替换原截图。`FeedbackModal` 的 `oncapture` 接入此方法,选取时隐藏弹窗,允许切换页面后点击“截取当前页面”,并保留说明。`FeedbackForm.evidence` 展开显示已脱敏的日志、请求、环境和回放事件;`evidenceSummary` 补充缺失说明。浏览器环境取实际 URL(去查询参数/hash)、视口、语言和构建标识;服务端环境仅采集 `envAllowlist` 中的非凭据键,缺失值为 null。
@@ -112,12 +161,14 @@ async function startFeedback(config: FeedbackConfigInput) {
112
161
  collectEvidence: async ({ report, signal }) => ({
113
162
  text: `Diagnostics for ${report.number}`,
114
163
  data: { businessSystem: 'orders', scopeId: report.scopeId },
115
- attachments: [{
116
- id: 'business-logs',
117
- name: 'business-logs.zip',
118
- contentType: 'application/zip',
119
- bytes: await readFile('diagnostics/sanitized-logs.zip', { signal }),
120
- }],
164
+ attachments: [
165
+ {
166
+ id: 'business-logs',
167
+ name: 'business-logs.zip',
168
+ contentType: 'application/zip',
169
+ bytes: await readFile('diagnostics/sanitized-logs.zip', { signal }),
170
+ }
171
+ ],
121
172
  }),
122
173
  })
123
174
  if (!result.success)
@@ -182,10 +233,22 @@ hai feedback view feedback-FB-1.zip
182
233
 
183
234
  ### console 通知的隐私边界
184
235
 
185
- `consoleContent` 默认为 `metadata`:仅记录反馈编号、应用/版本/构建定位与采集状态,不输出说明、summary、附件、证据、通知正文或接收地址。需要通用脱敏摘要时显式设为 `summary`;通用脱敏不能识别所有业务秘密。该选项不需要 Reach,也不改变 disabled/reach 的通道语义。旧配置的 console 日志默认随本次升级收紧。
236
+ `consoleContent` 默认为 `metadata`:仅记录反馈编号、应用/版本/构建定位与采集状态,不输出说明、summary、附件、证据、通知正文或接收地址。需要通用脱敏摘要时显式设为 `summary`;通用脱敏不能识别所有业务秘密。该选项不需要 Reach,也不改变 disabled/reach 的通道语义。
186
237
 
187
238
  服务端环境证据与反馈受理共同提交;环境写入失败不留下已受理但缺失环境的半成品,相同 submissionId 可安全重试。管理界面用 `createFeedbackReview(...).retryReport(id)` 重试可恢复材料与失败通知/Webhook,控制器去重目标并传播失败,退出调用 close 阻止迟到请求继续编排。
188
239
 
240
+ ## 配置
241
+
242
+ 生命周期:init 输入后端/业务配置,成功后才能使用;close 在释放借用依赖前等待完成。返回值以快速开始示例和公开签名为准。
243
+
244
+ 配置由宿主加载和校验,再传入本模块;私密值放服务端私有环境,不传到浏览器。按实际使用场景逐项选择后端、容量、期限和日志策略,修改后重新初始化并核验成功结果。
245
+
246
+ [逐项配置说明、适用场景和示例](../../docs/usage/configuration/operations.md).
247
+
248
+ ## 错误处理
249
+
250
+ 领域操作先判断 `result.success`,失败按本模块错误定义与安全提示处理,服务端保留错误码、关联编号和脱敏原因。同步工具、工厂、流迭代及生命周期按各自签名处理,不把 Promise 完成视为业务成功。具体失败与恢复边界见对应功能。
251
+
189
252
  ## 开发测试
190
253
 
191
254
  ```sh
@@ -198,7 +261,7 @@ pnpm --filter @h-ai/cli test:scaffold-gates
198
261
  node packages/cli/tests/feedback-offline-browser.mjs
199
262
  ```
200
263
 
201
- 验收需分别核对单元/浏览器、SQLite/PG/MySQL、真实 S3 与 SMTP、多节点恢复和断网离线查看;构建或 mock 通过不能代替真实 Provider 和用户流程。应用接入与部署流程见[使用指南](../../docs/usage/README.md)。
264
+ 验收需分别核对单元/浏览器、SQLite/PG/MySQL、真实 S3 与 SMTP、多节点恢复和断网离线查看;构建或 mock 通过不能代替真实 Provider 和用户流程。应用接入与部署流程见[使用指南](../../docs/usage/README.md).
202
265
 
203
266
  ## 许可证
204
267
 
@@ -213,4 +213,5 @@ declare function queryFeedbackBundle(bundle: FeedbackBundle, query?: FeedbackBun
213
213
  */
214
214
  declare function getFeedbackBundleViewerFiles(): Readonly<Record<string, Uint8Array>>;
215
215
 
216
- export { type FeedbackBundle, type FeedbackBundleAttachment, type FeedbackBundleEvidence, type FeedbackBundleEvidencePage, type FeedbackBundleFile, type FeedbackBundleInput, type FeedbackBundleManifest, type FeedbackBundleQuery, type FeedbackBundleReadOptions, createFeedbackBundle, getFeedbackBundleViewerFiles, queryFeedbackBundle, readFeedbackBundle, summarizeFeedbackBundle };
216
+ export { createFeedbackBundle, getFeedbackBundleViewerFiles, queryFeedbackBundle, readFeedbackBundle, summarizeFeedbackBundle };
217
+ export type { FeedbackBundle, FeedbackBundleAttachment, FeedbackBundleEvidence, FeedbackBundleEvidencePage, FeedbackBundleFile, FeedbackBundleInput, FeedbackBundleManifest, FeedbackBundleQuery, FeedbackBundleReadOptions };
@@ -1,3 +1,3 @@
1
- export { createFeedbackBundle, getFeedbackBundleViewerFiles, queryFeedbackBundle, readFeedbackBundle, summarizeFeedbackBundle } from '../chunk-KGMLTOMM.js';
1
+ export { createFeedbackBundle, getFeedbackBundleViewerFiles, queryFeedbackBundle, readFeedbackBundle, summarizeFeedbackBundle } from '../chunk-IFJHL5VK.js';
2
2
  //# sourceMappingURL=index.js.map
3
3
  //# sourceMappingURL=index.js.map
@@ -1,4 +1,4 @@
1
- import { F as FeedbackFileSaver } from './feedback-client-files-B88nCOo_.js';
1
+ import { FeedbackFileSaver } from './client/index.js';
2
2
  import '@h-ai/api-contract/feedback';
3
3
  import '@h-ai/core';
4
4
 
package/dist/capacitor.js CHANGED
@@ -4,7 +4,11 @@ import { Share } from '@capacitor/share';
4
4
  // src/client/feedback-client-capacitor.ts
5
5
  var saveCapacitorFeedbackFile = async (bytes, filename, _contentType, signal) => {
6
6
  const path = `feedback/${globalThis.crypto.randomUUID()}/${filename}`;
7
- const cleanup = () => Filesystem.rmdir({ path: path.slice(0, path.lastIndexOf("/")), directory: Directory.Cache, recursive: true }).catch(() => void 0);
7
+ const cleanup = () => Filesystem.rmdir({
8
+ path: path.slice(0, path.lastIndexOf("/")),
9
+ directory: Directory.Cache,
10
+ recursive: true
11
+ }).catch(() => void 0);
8
12
  const abort = () => {
9
13
  void cleanup();
10
14
  };
@@ -14,12 +18,22 @@ var saveCapacitorFeedbackFile = async (bytes, filename, _contentType, signal) =>
14
18
  try {
15
19
  if (signal?.aborted)
16
20
  return { status: "cancelled" };
17
- const file = await Filesystem.writeFile({ path, data: btoa(raw), directory: Directory.Cache, recursive: true });
21
+ const file = await Filesystem.writeFile({
22
+ path,
23
+ data: btoa(raw),
24
+ directory: Directory.Cache,
25
+ recursive: true
26
+ });
18
27
  if (signal?.aborted)
19
28
  return { status: "cancelled" };
20
29
  signal?.addEventListener("abort", abort, { once: true });
21
- await Share.share({ files: [file.uri], dialogTitle: filename });
22
- return { status: signal?.aborted ? "cancelled" : "shared" };
30
+ await Share.share({
31
+ files: [file.uri],
32
+ dialogTitle: filename
33
+ });
34
+ return {
35
+ status: signal?.aborted ? "cancelled" : "shared"
36
+ };
23
37
  } catch (cause) {
24
38
  if (cause instanceof Error && cause.message === "Share canceled")
25
39
  return { status: "cancelled" };
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/client/feedback-client-capacitor.ts"],"names":[],"mappings":";;;;AAcO,IAAM,yBAAA,GAA+C,OAAO,KAAA,EAAO,QAAA,EAAU,cAAc,MAAA,KAAW;AAC3G,EAAA,MAAM,OAAO,CAAA,SAAA,EAAY,UAAA,CAAW,OAAO,UAAA,EAAY,IAAI,QAAQ,CAAA,CAAA;AACnE,EAAA,MAAM,OAAA,GAAU,MAAM,UAAA,CAAW,KAAA,CAAM,EAAE,IAAA,EAAM,IAAA,CAAK,KAAA,CAAM,CAAA,EAAG,IAAA,CAAK,WAAA,CAAY,GAAG,CAAC,CAAA,EAAG,SAAA,EAAW,SAAA,CAAU,KAAA,EAAO,SAAA,EAAW,MAAM,CAAA,CAAE,KAAA,CAAM,MAAM,MAAS,CAAA;AACzJ,EAAA,MAAM,QAAQ,MAAM;AAClB,IAAA,KAAK,OAAA,EAAQ;AAAA,EACf,CAAA;AACA,EAAA,IAAI,GAAA,GAAM,EAAA;AACV,EAAA,KAAA,IAAS,MAAA,GAAS,CAAA,EAAG,MAAA,GAAS,KAAA,CAAM,QAAQ,MAAA,IAAU,KAAA;AACpD,IAAA,GAAA,IAAO,MAAA,CAAO,aAAa,GAAG,KAAA,CAAM,SAAS,MAAA,EAAQ,MAAA,GAAS,KAAK,CAAC,CAAA;AACtE,EAAA,IAAI;AACF,IAAA,IAAI,MAAA,EAAQ,OAAA;AACV,MAAA,OAAO,EAAE,QAAQ,WAAA,EAAY;AAC/B,IAAA,MAAM,IAAA,GAAO,MAAM,UAAA,CAAW,SAAA,CAAU,EAAE,IAAA,EAAM,IAAA,EAAM,IAAA,CAAK,GAAG,GAAG,SAAA,EAAW,SAAA,CAAU,KAAA,EAAO,SAAA,EAAW,MAAM,CAAA;AAC9G,IAAA,IAAI,MAAA,EAAQ,OAAA;AACV,MAAA,OAAO,EAAE,QAAQ,WAAA,EAAY;AAC/B,IAAA,MAAA,EAAQ,iBAAiB,OAAA,EAAS,KAAA,EAAO,EAAE,IAAA,EAAM,MAAM,CAAA;AACvD,IAAA,MAAM,KAAA,CAAM,KAAA,CAAM,EAAE,KAAA,EAAO,CAAC,KAAK,GAAG,CAAA,EAAG,WAAA,EAAa,QAAA,EAAU,CAAA;AAC9D,IAAA,OAAO,EAAE,MAAA,EAAQ,MAAA,EAAQ,OAAA,GAAU,cAAc,QAAA,EAAS;AAAA,EAC5D,SACO,KAAA,EAAO;AAEZ,IAAA,IAAI,KAAA,YAAiB,KAAA,IAAS,KAAA,CAAM,OAAA,KAAY,gBAAA;AAC9C,MAAA,OAAO,EAAE,QAAQ,WAAA,EAAY;AAC/B,IAAA,MAAM,KAAA;AAAA,EACR,CAAA,SACA;AACE,IAAA,MAAA,EAAQ,mBAAA,CAAoB,SAAS,KAAK,CAAA;AAC1C,IAAA,MAAM,OAAA,EAAQ;AAAA,EAChB;AACF","file":"capacitor.js","sourcesContent":["/** Capacitor 独立入口,只在 App 中导入插件,缓存目录由本次调用负责清理。 */\nimport type { FeedbackFileSaver } from './feedback-client-files.js'\nimport { Directory, Filesystem } from '@capacitor/filesystem'\nimport { Share } from '@capacitor/share'\n\n/**\n * 以原生分享保存临时诊断包;分享结束、失败或退出会话均清理缓存。\n * @example\n * ```ts\n * import { configureFeedbackSaving } from '@h-ai/feedback/client'\n * import { saveCapacitorFeedbackFile } from '@h-ai/feedback/capacitor'\n * configureFeedbackSaving(saveCapacitorFeedbackFile)\n * ```\n */\nexport const saveCapacitorFeedbackFile: FeedbackFileSaver = async (bytes, filename, _contentType, signal) => {\n const path = `feedback/${globalThis.crypto.randomUUID()}/${filename}`\n const cleanup = () => Filesystem.rmdir({ path: path.slice(0, path.lastIndexOf('/')), directory: Directory.Cache, recursive: true }).catch(() => undefined)\n const abort = () => {\n void cleanup()\n }\n let raw = ''\n for (let offset = 0; offset < bytes.length; offset += 32768)\n raw += String.fromCharCode(...bytes.subarray(offset, offset + 32768))\n try {\n if (signal?.aborted)\n return { status: 'cancelled' }\n const file = await Filesystem.writeFile({ path, data: btoa(raw), directory: Directory.Cache, recursive: true })\n if (signal?.aborted)\n return { status: 'cancelled' }\n signal?.addEventListener('abort', abort, { once: true })\n await Share.share({ files: [file.uri], dialogTitle: filename })\n return { status: signal?.aborted ? 'cancelled' : 'shared' }\n }\n catch (cause) {\n // 原生插件以固定消息表示主动取消;其余异常交给统一保存边界记录并返回失败。\n if (cause instanceof Error && cause.message === 'Share canceled')\n return { status: 'cancelled' }\n throw cause\n }\n finally {\n signal?.removeEventListener('abort', abort)\n await cleanup()\n }\n}\n"]}
1
+ {"version":3,"sources":["../src/client/feedback-client-capacitor.ts"],"names":[],"mappings":";;;;AAcO,IAAM,yBAAA,GAA+C,OAAO,KAAA,EAAO,QAAA,EAAU,cAAc,MAAA,KAAW;AAC3G,EAAA,MAAM,OAAO,CAAA,SAAA,EAAY,UAAA,CAAW,OAAO,UAAA,EAAY,IAAI,QAAQ,CAAA,CAAA;AACnE,EAAA,MAAM,OAAA,GAAU,MAAM,UAAA,CAAW,KAAA,CAAM;AAAA,IACrC,MAAM,IAAA,CAAK,KAAA,CAAM,GAAG,IAAA,CAAK,WAAA,CAAY,GAAG,CAAC,CAAA;AAAA,IACzC,WAAW,SAAA,CAAU,KAAA;AAAA,IACrB,SAAA,EAAW;AAAA,GACZ,CAAA,CAAE,KAAA,CAAM,MAAM,MAAS,CAAA;AACxB,EAAA,MAAM,QAAQ,MAAM;AAClB,IAAA,KAAK,OAAA,EAAQ;AAAA,EACf,CAAA;AACA,EAAA,IAAI,GAAA,GAAM,EAAA;AACV,EAAA,KAAA,IAAS,MAAA,GAAS,CAAA,EAAG,MAAA,GAAS,KAAA,CAAM,QAAQ,MAAA,IAAU,KAAA;AACpD,IAAA,GAAA,IAAO,MAAA,CAAO,aAAa,GAAG,KAAA,CAAM,SAAS,MAAA,EAAQ,MAAA,GAAS,KAAK,CAAC,CAAA;AACtE,EAAA,IAAI;AACF,IAAA,IAAI,MAAA,EAAQ,OAAA;AACV,MAAA,OAAO,EAAE,QAAQ,WAAA,EAAY;AAC/B,IAAA,MAAM,IAAA,GAAO,MAAM,UAAA,CAAW,SAAA,CAAU;AAAA,MACtC,IAAA;AAAA,MACA,IAAA,EAAM,KAAK,GAAG,CAAA;AAAA,MACd,WAAW,SAAA,CAAU,KAAA;AAAA,MACrB,SAAA,EAAW;AAAA,KACZ,CAAA;AACD,IAAA,IAAI,MAAA,EAAQ,OAAA;AACV,MAAA,OAAO,EAAE,QAAQ,WAAA,EAAY;AAC/B,IAAA,MAAA,EAAQ,iBAAiB,OAAA,EAAS,KAAA,EAAO,EAAE,IAAA,EAAM,MAAM,CAAA;AACvD,IAAA,MAAM,MAAM,KAAA,CAAM;AAAA,MAChB,KAAA,EAAO,CAAC,IAAA,CAAK,GAAG,CAAA;AAAA,MAChB,WAAA,EAAa;AAAA,KACd,CAAA;AACD,IAAA,OAAO;AAAA,MACL,MAAA,EAAQ,MAAA,EAAQ,OAAA,GAAU,WAAA,GAAc;AAAA,KAC1C;AAAA,EACF,SACO,KAAA,EAAO;AAEZ,IAAA,IAAI,KAAA,YAAiB,KAAA,IAAS,KAAA,CAAM,OAAA,KAAY,gBAAA;AAC9C,MAAA,OAAO,EAAE,QAAQ,WAAA,EAAY;AAC/B,IAAA,MAAM,KAAA;AAAA,EACR,CAAA,SACA;AACE,IAAA,MAAA,EAAQ,mBAAA,CAAoB,SAAS,KAAK,CAAA;AAC1C,IAAA,MAAM,OAAA,EAAQ;AAAA,EAChB;AACF","file":"capacitor.js","sourcesContent":["/** Capacitor 独立入口,只在 App 中导入插件,缓存目录由本次调用负责清理。 */\nimport type { FeedbackFileSaver } from './feedback-client-files.js'\nimport { Directory, Filesystem } from '@capacitor/filesystem'\nimport { Share } from '@capacitor/share'\n\n/**\n * 以原生分享保存临时诊断包;分享结束、失败或退出会话均清理缓存。\n * @example\n * ```ts\n * import { configureFeedbackSaving } from '@h-ai/feedback/client'\n * import { saveCapacitorFeedbackFile } from '@h-ai/feedback/capacitor'\n * configureFeedbackSaving(saveCapacitorFeedbackFile)\n * ```\n */\nexport const saveCapacitorFeedbackFile: FeedbackFileSaver = async (bytes, filename, _contentType, signal) => {\n const path = `feedback/${globalThis.crypto.randomUUID()}/${filename}`\n const cleanup = () => Filesystem.rmdir({\n path: path.slice(0, path.lastIndexOf('/')),\n directory: Directory.Cache,\n recursive: true,\n }).catch(() => undefined)\n const abort = () => {\n void cleanup()\n }\n let raw = ''\n for (let offset = 0; offset < bytes.length; offset += 32768)\n raw += String.fromCharCode(...bytes.subarray(offset, offset + 32768))\n try {\n if (signal?.aborted)\n return { status: 'cancelled' }\n const file = await Filesystem.writeFile({\n path,\n data: btoa(raw),\n directory: Directory.Cache,\n recursive: true,\n })\n if (signal?.aborted)\n return { status: 'cancelled' }\n signal?.addEventListener('abort', abort, { once: true })\n await Share.share({\n files: [file.uri],\n dialogTitle: filename,\n })\n return {\n status: signal?.aborted ? 'cancelled' : 'shared',\n }\n }\n catch (cause) {\n // 原生插件以固定消息表示主动取消;其余异常交给统一保存边界记录并返回失败。\n if (cause instanceof Error && cause.message === 'Share canceled')\n return { status: 'cancelled' }\n throw cause\n }\n finally {\n signal?.removeEventListener('abort', abort)\n await cleanup()\n }\n}\n"]}