@kc-one/smart-fill-sdk 0.0.1-beta.1

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.
Files changed (38) hide show
  1. package/README.md +204 -0
  2. package/dist/examples/vanilla/main.d.ts +2 -0
  3. package/dist/examples/vanilla/main.d.ts.map +1 -0
  4. package/dist/index.d.ts +2 -0
  5. package/dist/index.esm.js +1492 -0
  6. package/dist/index.umd.cjs +92 -0
  7. package/dist/src/adapters/native.d.ts +3 -0
  8. package/dist/src/adapters/native.d.ts.map +1 -0
  9. package/dist/src/client/gateway-client.d.ts +52 -0
  10. package/dist/src/client/gateway-client.d.ts.map +1 -0
  11. package/dist/src/config/defaults.d.ts +7 -0
  12. package/dist/src/config/defaults.d.ts.map +1 -0
  13. package/dist/src/core/errors.d.ts +13 -0
  14. package/dist/src/core/errors.d.ts.map +1 -0
  15. package/dist/src/core/instance-manager.d.ts +24 -0
  16. package/dist/src/core/instance-manager.d.ts.map +1 -0
  17. package/dist/src/core/smart-fill-instance.d.ts +105 -0
  18. package/dist/src/core/smart-fill-instance.d.ts.map +1 -0
  19. package/dist/src/core/smart-fill.d.ts +39 -0
  20. package/dist/src/core/smart-fill.d.ts.map +1 -0
  21. package/dist/src/events/event-bus.d.ts +12 -0
  22. package/dist/src/events/event-bus.d.ts.map +1 -0
  23. package/dist/src/filler/dom-filler.d.ts +32 -0
  24. package/dist/src/filler/dom-filler.d.ts.map +1 -0
  25. package/dist/src/index.d.ts +10 -0
  26. package/dist/src/index.d.ts.map +1 -0
  27. package/dist/src/rules/local-rules.d.ts +9 -0
  28. package/dist/src/rules/local-rules.d.ts.map +1 -0
  29. package/dist/src/scanner/dom-scanner.d.ts +34 -0
  30. package/dist/src/scanner/dom-scanner.d.ts.map +1 -0
  31. package/dist/src/scanner/fingerprint.d.ts +22 -0
  32. package/dist/src/scanner/fingerprint.d.ts.map +1 -0
  33. package/dist/src/types/index.d.ts +255 -0
  34. package/dist/src/types/index.d.ts.map +1 -0
  35. package/dist/src/ui/panel.d.ts +78 -0
  36. package/dist/src/ui/panel.d.ts.map +1 -0
  37. package/dist/style.css +1 -0
  38. package/package.json +45 -0
@@ -0,0 +1,1492 @@
1
+ var te = Object.defineProperty;
2
+ var ne = (n, e, t) => e in n ? te(n, e, { enumerable: !0, configurable: !0, writable: !0, value: t }) : n[e] = t;
3
+ var c = (n, e, t) => ne(n, typeof e != "symbol" ? e + "" : e, t);
4
+ const se = "https://loan.kdbank.cn";
5
+ class B extends Error {
6
+ constructor(t) {
7
+ super(t.message);
8
+ /** 结构化错误信息,含 code / stage / retryable */
9
+ c(this, "smartFillError");
10
+ this.name = "SmartFillException", this.smartFillError = t;
11
+ }
12
+ }
13
+ function y(n, e, t, s = {}) {
14
+ return new B({ code: n, message: e, stage: t, ...s });
15
+ }
16
+ function ie(n, e, t = "RECOGNIZE_FAILED") {
17
+ return n instanceof B ? n.smartFillError : {
18
+ code: t,
19
+ message: n instanceof Error ? n.message : String(n || "智能录入异常"),
20
+ stage: e,
21
+ retryable: e === "recognize"
22
+ };
23
+ }
24
+ function L(n = "sf") {
25
+ return `${n}_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 10)}`;
26
+ }
27
+ class re {
28
+ constructor(e) {
29
+ /** 会话 accessToken,写入请求头 seToken */
30
+ c(this, "seToken", "");
31
+ /** 网关根地址,见 config/defaults.ts */
32
+ c(this, "baseURL", se);
33
+ this.config = e;
34
+ }
35
+ /** 设置会话 token,后续 request 自动携带 seToken 请求头 */
36
+ setAccessToken(e) {
37
+ this.seToken = e;
38
+ }
39
+ /**
40
+ * 创建 SDK 会话。
41
+ * 校验 apiKey 格式(seKey- 前缀),当前阶段返回 mock session。
42
+ */
43
+ async createSession() {
44
+ if (!/^seKey-[A-Za-z0-9_-]{6,}$/.test(this.config.apiKey))
45
+ throw y("API_KEY_INVALID", "apiKey 格式不正确,应以 seKey- 开头。", "setup");
46
+ return {
47
+ apiKey: this.config.apiKey,
48
+ rulesVersion: "0.0.1"
49
+ };
50
+ }
51
+ /** 按 formCode 拉取远端表单配置(当前扫描流程已不再依赖) */
52
+ async getFormConfig(e) {
53
+ return e ? this.request(`/sdk/forms/${encodeURIComponent(e)}`, {
54
+ method: "GET"
55
+ }) : null;
56
+ }
57
+ /**
58
+ * 将识别输入统一为文本。
59
+ * - 图片:先走 OCR 提取文本,供本地规则和后端识别共用
60
+ * - 纯文本:直接返回 trim 后的输入
61
+ */
62
+ async resolveInputText(e) {
63
+ var r, a, o, d;
64
+ const t = (r = e.images) != null && r.length ? e.images : [], s = ((a = e.text) == null ? void 0 : a.trim()) || "";
65
+ if (!t.length)
66
+ return { text: s, usedOcr: !1 };
67
+ (o = e.onStatusChange) == null || o.call(e, "image_uploading");
68
+ const i = await this.recognizeImages(t);
69
+ return (d = e.onStatusChange) == null || d.call(e, "image_recognizing"), { text: i, usedOcr: !0 };
70
+ }
71
+ /**
72
+ * 调用后端智能识别接口。
73
+ * 将 data.fieldValues 归一化为 FieldSuggestion[],缺失字段补默认值:
74
+ * - label:扫描字段 label → fieldId
75
+ * - confidence:DEFAULT_REMOTE_CONFIDENCE
76
+ * - source:'ai'
77
+ */
78
+ async recognize(e) {
79
+ var a, o;
80
+ const t = performance.now(), s = (a = e.text) == null ? void 0 : a.trim();
81
+ (o = e.onStatusChange) == null || o.call(e, "recognizing");
82
+ const i = await this.request(
83
+ "/fcloud/flow-product/agentChat/smartEntry",
84
+ {
85
+ method: "POST",
86
+ body: JSON.stringify({
87
+ // scanToken: payload.scanToken,
88
+ formCode: e.formCode || "",
89
+ configVersion: e.configVersion || "",
90
+ formMsg: e.fields.map(({ element: d, ...l }) => l),
91
+ userInputMsg: s || ""
92
+ })
93
+ }
94
+ ), r = oe(i, e.fields, e.scanToken);
95
+ return {
96
+ scanToken: e.scanToken,
97
+ suggestions: r,
98
+ trace: i.trace || {
99
+ traceId: L("trace"),
100
+ usedOcr: !!e.usedOcr,
101
+ usedAi: !0,
102
+ durationMs: Math.round(performance.now() - t)
103
+ }
104
+ };
105
+ }
106
+ /** 图片 OCR:以 multipart/form-data 上传 files,返回提取出的 text 文本 */
107
+ async recognizeImages(e) {
108
+ const t = new FormData();
109
+ e.forEach((r) => {
110
+ t.append("image", r, r.name);
111
+ });
112
+ const s = await this.request(
113
+ "/fcloud/flow-product/agentChat/smartEntry/ocr",
114
+ {
115
+ method: "POST",
116
+ body: t
117
+ }
118
+ ), i = ce(s);
119
+ if (!i)
120
+ throw y("RECOGNIZE_FAILED", "图片识别未提取到文本内容。", "recognize");
121
+ return i;
122
+ }
123
+ /** 通用 fetch 封装:超时控制、trace 头、HTTP 错误映射为 SmartFillException */
124
+ async request(e, t) {
125
+ const s = new AbortController(), i = window.setTimeout(() => s.abort(), this.config.requestTimeoutMs ?? 3e4), r = new Headers(t.headers);
126
+ t.body && !(t.body instanceof FormData) && !r.has("Content-Type") && r.set("Content-Type", "application/json"), r.set("x-trace-id", L("trace")), this.seToken && r.set("seToken", `${this.seToken}`);
127
+ try {
128
+ const a = await fetch(`${this.baseURL}${e}`, {
129
+ ...t,
130
+ headers: r,
131
+ signal: s.signal
132
+ });
133
+ if (!a.ok)
134
+ throw y(de(a.status), await a.text(), e.includes("session") ? "setup" : "recognize", {
135
+ retryable: a.status >= 500 || a.status === 429
136
+ });
137
+ return a.json();
138
+ } catch (a) {
139
+ throw a instanceof DOMException && a.name === "AbortError" ? y("RECOGNIZE_TIMEOUT", "识别请求超时,请稍后重试。", "recognize", { retryable: !0 }) : a;
140
+ } finally {
141
+ window.clearTimeout(i);
142
+ }
143
+ }
144
+ }
145
+ const ae = 0.95;
146
+ function oe(n, e, t) {
147
+ var r;
148
+ const s = new Map(e.map((a) => [a.fieldId, a]));
149
+ return (((r = n.data) == null ? void 0 : r.fieldValues) || []).filter((a) => !!(a != null && a.fieldId)).map((a) => {
150
+ const o = s.get(a.fieldId);
151
+ return {
152
+ fieldId: a.fieldId,
153
+ scanToken: t,
154
+ label: a.label || (o == null ? void 0 : o.label) || a.fieldId,
155
+ value: a.value,
156
+ displayValue: a.value == null ? "" : String(a.value),
157
+ confidence: le(a.confidence),
158
+ source: a.source || "ai",
159
+ warnings: a.warnings
160
+ };
161
+ });
162
+ }
163
+ function le(n) {
164
+ return typeof n != "number" || Number.isNaN(n) ? ae : n < 0 ? 0 : n > 1 ? 1 : n;
165
+ }
166
+ function ce(n) {
167
+ var t;
168
+ const e = typeof n.data == "string" ? n.data : ((t = n.data) == null ? void 0 : t.text) || n.text || "";
169
+ return String(e || "").trim();
170
+ }
171
+ function de(n) {
172
+ return n === 401 ? "TOKEN_EXPIRED" : n === 403 ? "API_KEY_FORBIDDEN" : n === 404 ? "FORM_CONFIG_NOT_FOUND" : "RECOGNIZE_FAILED";
173
+ }
174
+ class Y {
175
+ constructor() {
176
+ c(this, "handlers", /* @__PURE__ */ new Map());
177
+ }
178
+ /** 订阅事件,返回 unsubscribe 函数 */
179
+ on(e, t) {
180
+ const s = this.handlers.get(e) ?? /* @__PURE__ */ new Set();
181
+ return s.add(t), this.handlers.set(e, s), () => this.off(e, t);
182
+ }
183
+ off(e, t) {
184
+ var s;
185
+ (s = this.handlers.get(e)) == null || s.delete(t);
186
+ }
187
+ emit(e, t) {
188
+ var s;
189
+ (s = this.handlers.get(e)) == null || s.forEach((i) => i(t));
190
+ }
191
+ clear() {
192
+ this.handlers.clear();
193
+ }
194
+ }
195
+ class ue {
196
+ constructor() {
197
+ /** 当前页面所有存活实例 */
198
+ c(this, "instances", /* @__PURE__ */ new Set());
199
+ /** 当前激活(面板打开)的实例 */
200
+ c(this, "active", null);
201
+ }
202
+ /** 注册新实例 */
203
+ add(e) {
204
+ this.instances.add(e);
205
+ }
206
+ /** 激活实例,自动关闭上一个实例的面板 */
207
+ activate(e) {
208
+ this.active && this.active !== e && this.active.close(), this.active = e;
209
+ }
210
+ /** 实例销毁时从管理器移除 */
211
+ remove(e) {
212
+ this.instances.delete(e), this.active === e && (this.active = null);
213
+ }
214
+ /** 销毁所有实例(apiKey 变更时调用) */
215
+ destroyAll() {
216
+ this.instances.forEach((e) => e.destroy()), this.instances.clear(), this.active = null;
217
+ }
218
+ }
219
+ const pe = [
220
+ /^(el|rc|ant|radix|headlessui|mui|chakra)-/i,
221
+ /[0-9a-f]{8,}/i,
222
+ /\d{6,}/
223
+ ];
224
+ function v(n) {
225
+ const e = String(n || "").trim();
226
+ return !e || pe.some((t) => t.test(e)) ? "" : e;
227
+ }
228
+ function $(n) {
229
+ const e = (n.options || []).slice(0, 20).map((t) => `${t.label}:${String(t.value)}`).join("|");
230
+ return w(
231
+ [
232
+ n.fieldId,
233
+ n.tagName,
234
+ n.type,
235
+ v(n.name),
236
+ v(n.id),
237
+ n.label,
238
+ n.placeholder,
239
+ n.section,
240
+ e
241
+ ].join("::")
242
+ );
243
+ }
244
+ function w(n) {
245
+ return String(n ?? "").replace(/\s+/g, " ").trim().toLowerCase();
246
+ }
247
+ const F = /* @__PURE__ */ new WeakMap();
248
+ class D {
249
+ constructor(e, t, s = []) {
250
+ this.fields = e, this.schemas = t, this.adapters = s;
251
+ }
252
+ /**
253
+ * 批量回填字段值。
254
+ * 每条 value 依次校验:字段存在 → scanToken 有效 → validate → setValue。
255
+ * 失败项记入 skipped,不中断后续字段。
256
+ */
257
+ async apply(e) {
258
+ const t = [], s = [];
259
+ for (const i of e.values) {
260
+ const r = this.fields.find((l) => l.fieldId === i.fieldId);
261
+ if (!r) {
262
+ s.push(T(i.fieldId, "", i.value, "字段不在当前扫描结果中", "FIELD_NOT_FOUND"));
263
+ continue;
264
+ }
265
+ if (r.scanToken !== e.scanToken) {
266
+ s.push(T(r.fieldId, r.label, i.value, "页面扫描已过期,请重新扫描", "SCAN_TOKEN_EXPIRED"));
267
+ continue;
268
+ }
269
+ const a = this.schemas.find((l) => l.fieldId === i.fieldId), o = a != null && a.transform ? a.transform(i.value) : i.value, d = await this.getValue(r, a);
270
+ try {
271
+ const l = a != null && a.validate ? await a.validate(o) : !0;
272
+ if (l !== !0) {
273
+ s.push(T(r.fieldId, r.label, o, typeof l == "string" ? l : "字段校验未通过", "VALIDATE_FAILED"));
274
+ continue;
275
+ }
276
+ await this.setValue(r, o, a), t.push({ fieldId: r.fieldId, label: r.label, value: o, previousValue: d });
277
+ } catch (l) {
278
+ s.push(T(r.fieldId, r.label, o, l instanceof Error ? l.message : "字段回填失败", "SET_VALUE_FAILED"));
279
+ }
280
+ }
281
+ return {
282
+ applied: t,
283
+ skipped: s,
284
+ warnings: s.length ? ["部分字段未回填,请查看跳过原因。"] : void 0
285
+ };
286
+ }
287
+ /** 读取字段当前值,优先级:schema.getValue > adapter > DOM */
288
+ async getValue(e, t) {
289
+ if (t != null && t.getValue) return t.getValue();
290
+ const s = this.matchAdapter(e);
291
+ if (s != null && s.getValue) return s.getValue(e);
292
+ const i = M(e);
293
+ if (i)
294
+ return i instanceof HTMLInputElement && i.type === "checkbox" ? i.checked : i instanceof HTMLInputElement && i.type === "radio" ? i.checked ? i.value : void 0 : i instanceof HTMLInputElement || i instanceof HTMLTextAreaElement || i instanceof HTMLSelectElement ? i.value : i.textContent;
295
+ }
296
+ /**
297
+ * 写入字段值,优先级:schema.setValue > adapter > 原生 DOM。
298
+ * 写入前校验 fingerprint 防 DOM 变化误填,写入后触发高亮。
299
+ */
300
+ async setValue(e, t, s) {
301
+ if (s != null && s.setValue) {
302
+ await s.setValue(t);
303
+ return;
304
+ }
305
+ const i = M(e);
306
+ if (!i) throw new Error("页面中未找到对应字段");
307
+ if (e.fingerprint && e.fingerprint !== $({ ...e, tagName: i.tagName.toLowerCase() }))
308
+ throw new Error("字段结构已变化,请重新扫描");
309
+ if (e.disabled || e.readonly || i.hasAttribute("disabled") || i.hasAttribute("readonly"))
310
+ throw new Error("字段不可编辑");
311
+ const r = this.matchAdapter(e);
312
+ if (r) {
313
+ await r.setValue(e, t);
314
+ return;
315
+ }
316
+ fe(i, t), he(i);
317
+ }
318
+ /** 匹配第一个适用的组件库适配器 */
319
+ matchAdapter(e) {
320
+ const t = M(e);
321
+ return t ? this.adapters.find((s) => s.match(t, e)) : void 0;
322
+ }
323
+ }
324
+ function M(n) {
325
+ var e;
326
+ return (e = n.element) != null && e.isConnected ? n.element : document.querySelector(`[data-smart-fill-id="${R(n.fieldId)}"]`);
327
+ }
328
+ function fe(n, e) {
329
+ if (n instanceof HTMLInputElement && n.type === "checkbox")
330
+ n.checked = !!e;
331
+ else if (n instanceof HTMLInputElement && n.type === "radio") {
332
+ const t = document.querySelector(`input[type="radio"][name="${R(n.name)}"][value="${R(String(e))}"]`);
333
+ (t || n).checked = !0;
334
+ } else n instanceof HTMLInputElement || n instanceof HTMLTextAreaElement || n instanceof HTMLSelectElement ? n.value = String(e ?? "") : n.isContentEditable && (n.textContent = String(e ?? ""));
335
+ n.dispatchEvent(new Event("input", { bubbles: !0 })), n.dispatchEvent(new Event("change", { bubbles: !0 }));
336
+ }
337
+ function he(n) {
338
+ var s;
339
+ n.setAttribute("data-smart-fill-highlighted", "true"), (s = F.get(n)) == null || s.abort();
340
+ const e = new AbortController(), t = (i) => {
341
+ i && "isTrusted" in i && !i.isTrusted || (n.removeAttribute("data-smart-fill-highlighted"), e.abort(), F.delete(n));
342
+ };
343
+ F.set(n, e), n.addEventListener("focus", t, { signal: e.signal }), n.addEventListener("pointerdown", t, { signal: e.signal }), n.addEventListener("keydown", t, { signal: e.signal }), n.addEventListener("input", t, { signal: e.signal }), n.addEventListener("change", t, { signal: e.signal });
344
+ }
345
+ function T(n, e, t, s, i) {
346
+ return { fieldId: n, label: e, attemptedValue: t, reason: s, reasonCode: i };
347
+ }
348
+ function R(n) {
349
+ return typeof CSS < "u" && CSS.escape ? CSS.escape(n) : n.replace(/["\\]/g, "\\$&");
350
+ }
351
+ const ge = [
352
+ { key: "mobile", pattern: new RegExp("(?<!\\d)1[3-9]\\d{9}(?!\\d)", "g"), confidence: 0.98, reason: "手机号正则命中" },
353
+ { key: "idCard", pattern: new RegExp("(?<!\\d)\\d{17}[\\dXx](?!\\d)", "g"), confidence: 0.96, reason: "身份证号正则命中" },
354
+ { key: "email", pattern: /[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}/g, confidence: 0.95, reason: "邮箱正则命中" },
355
+ { key: "bankCard", pattern: new RegExp("(?<!\\d)\\d{16,19}(?!\\d)", "g"), confidence: 0.9, reason: "银行卡号正则命中" },
356
+ { key: "amount", pattern: /(?:金额|价格|费用|合计|总计)[::\s]*([0-9]+(?:\.[0-9]{1,2})?)/g, confidence: 0.88, reason: "金额关键词命中" },
357
+ { key: "date", pattern: /\d{4}[-/.年]\d{1,2}[-/.月]\d{1,2}日?/g, confidence: 0.86, reason: "日期格式命中" }
358
+ ], P = {
359
+ legalPerson: ["法人", "法定代表人", "法定代表", "企业法人", "legal person", "legal representative"],
360
+ emergencyContact: ["紧急联系人", "紧急联络人", "emergency contact"],
361
+ contact: ["联系人", "联络人", "联系人员", "contact"],
362
+ spouse: ["配偶", "爱人", "夫妻", "spouse"]
363
+ }, h = {
364
+ mobile: ["手机", "手机号", "电话", "联系电话", "mobile", "phone"],
365
+ legalPersonMobile: ["法人手机号", "法人电话", "法定代表人手机号", "法人手机", "legal person mobile"],
366
+ emergencyContactMobile: ["紧急联系人手机号", "紧急联系人电话", "紧急联系电话", "emergency contact mobile"],
367
+ contactMobile: ["联系人手机号", "联系人电话", "联系手机", "contact mobile"],
368
+ spouseMobile: ["配偶手机号", "配偶电话", "爱人手机号", "spouse mobile"],
369
+ idCard: ["身份证", "证件号", "身份证号", "idcard", "id card"],
370
+ legalPersonIdCard: ["法人身份证", "法人身份证号", "法定代表人身份证号"],
371
+ spouseIdCard: ["配偶身份证", "配偶身份证号", "爱人身份证号"],
372
+ email: ["邮箱", "邮件", "email", "mail"],
373
+ contactEmail: ["联系人邮箱", "联系邮箱", "contact email"],
374
+ name: ["姓名", "名字", "称呼", "name"],
375
+ customerName: ["客户姓名", "申请人姓名", "用户姓名", "借款人姓名", "客户名称"],
376
+ legalPersonName: ["法人姓名", "法定代表人姓名", "企业法人姓名"],
377
+ emergencyContactName: ["紧急联系人姓名", "紧急联系人名称"],
378
+ contactName: ["联系人姓名", "联系人名称"],
379
+ spouseName: ["配偶姓名", "爱人姓名"],
380
+ companyName: ["公司名称", "企业名称", "单位名称", "商户名称", "公司", "企业"],
381
+ address: ["地址", "住址", "通讯地址", "联系地址", "现住址", "办公地址"],
382
+ detailAddress: ["详细地址", "街道地址", "门牌地址", "详细住址"],
383
+ amount: ["金额", "费用", "价格", "合计", "总计", "amount", "price"],
384
+ applyAmount: ["申请金额", "贷款金额", "借款金额", "授信金额", "申请额度"],
385
+ date: ["日期", "时间", "有效期", "date"],
386
+ applyDate: ["申请日期", "申请时间", "受理日期", "进件日期"],
387
+ bankCard: ["银行卡", "卡号", "bank", "bank card"]
388
+ }, j = {
389
+ mobile: "mobile",
390
+ legalPersonMobile: "mobile",
391
+ emergencyContactMobile: "mobile",
392
+ contactMobile: "mobile",
393
+ spouseMobile: "mobile",
394
+ idCard: "idCard",
395
+ legalPersonIdCard: "idCard",
396
+ spouseIdCard: "idCard",
397
+ email: "email",
398
+ contactEmail: "email",
399
+ name: "name",
400
+ customerName: "name",
401
+ legalPersonName: "name",
402
+ emergencyContactName: "name",
403
+ contactName: "name",
404
+ spouseName: "name",
405
+ companyName: "companyName",
406
+ address: "address",
407
+ detailAddress: "address",
408
+ amount: "amount",
409
+ applyAmount: "amount",
410
+ date: "date",
411
+ applyDate: "date",
412
+ bankCard: "bankCard"
413
+ };
414
+ class me {
415
+ /**
416
+ * 从文本中提取事实并匹配到 scan 字段。
417
+ * 按 confidence 降序分配,每个 fieldId 仅匹配一次(usedFieldIds 去重)。
418
+ */
419
+ recognize(e, t, s) {
420
+ const i = Te([...ye(e), ...be(e)]), r = /* @__PURE__ */ new Set(), a = [];
421
+ for (const o of i.sort((d, l) => l.confidence - d.confidence)) {
422
+ const d = Ee(o, t, r);
423
+ d && (a.push({
424
+ fieldId: d.fieldId,
425
+ scanToken: s,
426
+ label: d.label,
427
+ value: we(o.value, d),
428
+ displayValue: o.value,
429
+ confidence: o.confidence,
430
+ source: "local_rule",
431
+ reason: o.reason
432
+ }), r.add(d.fieldId));
433
+ }
434
+ return a;
435
+ }
436
+ }
437
+ function ye(n) {
438
+ const e = [];
439
+ for (const t of ge)
440
+ for (const s of n.matchAll(t.pattern)) {
441
+ const i = Z(s[1] || s[0]);
442
+ e.push({
443
+ key: t.key,
444
+ value: i,
445
+ confidence: t.confidence,
446
+ reason: t.reason,
447
+ baseKey: j[t.key]
448
+ });
449
+ }
450
+ return e;
451
+ }
452
+ function be(n) {
453
+ const e = [], t = n.split(/\r?\n|[;,;]/).map((s) => s.trim()).filter(Boolean);
454
+ for (const s of t) {
455
+ const i = s.match(/^[“"'`]?([^::=]{2,30})[”"'`]?[::=]\s*(.+)$/);
456
+ if (!i) continue;
457
+ const r = Ae(i[1]), a = Z(i[2]);
458
+ if (!(!r || !a))
459
+ for (const o of xe(r, a))
460
+ e.push(o);
461
+ }
462
+ return e;
463
+ }
464
+ function xe(n, e) {
465
+ const t = w(n), s = Ie(e), i = [], r = Se(t);
466
+ return f(t, h.applyDate) && i.push("applyDate"), f(t, h.applyAmount) && i.push("applyAmount"), f(t, h.companyName) && i.push("companyName"), f(t, h.detailAddress) && i.push("detailAddress"), f(t, h.address) && i.push("address"), (f(t, h.mobile) || s === "mobile") && r && N(i, r, "mobile"), (f(t, h.idCard) || s === "idCard") && r && N(i, r, "idCard"), (f(t, h.email) || s === "email") && r && N(i, r, "email"), (f(t, h.mobile) || s === "mobile") && i.push("mobile"), (f(t, h.idCard) || s === "idCard") && i.push("idCard"), (f(t, h.email) || s === "email") && i.push("email"), (f(t, h.bankCard) || s === "bankCard") && i.push("bankCard"), (f(t, h.amount) || s === "amount") && i.push("amount"), (f(t, h.date) || s === "date") && i.push("date"), Ce(i).map((a, o) => ({
467
+ key: a,
468
+ value: e,
469
+ confidence: Math.max(0.84, 0.96 - o * 0.04),
470
+ reason: r ? `键值对文本命中(${n},角色增强)` : `键值对文本命中(${n})`,
471
+ baseKey: j[a]
472
+ }));
473
+ }
474
+ function Ee(n, e, t) {
475
+ let s = null;
476
+ for (const i of e) {
477
+ if (t.has(i.fieldId)) continue;
478
+ const r = ve(n, i);
479
+ r > ((s == null ? void 0 : s.score) ?? 0) && (s = { field: i, score: r });
480
+ }
481
+ return s && s.score >= 0.45 ? s.field : null;
482
+ }
483
+ function ve(n, e) {
484
+ const t = w([e.fieldId, e.label, e.placeholder, e.name, e.id, e.section].join(" ")), s = h[n.key] || [n.key], i = h[n.baseKey] || [n.baseKey];
485
+ let r = 0;
486
+ return f(t, s) ? r += 0.95 : n.key !== n.baseKey && f(t, i) ? r += 0.68 : f(t, i) && (r += 0.82), t.includes(w(n.key)) && (r += 0.22), e.required && (r += 0.05), n.baseKey === "amount" && (e.type === "amount" || e.type === "number") && (r += 0.18), n.baseKey === "date" && e.type === "date" && (r += 0.18), n.baseKey === "address" && (e.type === "textarea" || e.type === "text") && (r += 0.12), ["name", "companyName", "email", "mobile", "idCard", "bankCard"].includes(n.baseKey) && e.type === "text" && (r += 0.08), Math.min(r, 1);
487
+ }
488
+ function we(n, e) {
489
+ var t;
490
+ return e.type === "date" ? n.replace(/[年月/.]/g, "-").replace(/日/g, "").replace(/--/g, "-") : e.type === "number" || e.type === "amount" ? ((t = n.match(/[0-9]+(?:\.[0-9]+)?/)) == null ? void 0 : t[0]) ?? n : e.name && /mobile|phone|idcard|bankcard/i.test(e.name) || e.id && /mobile|phone|idcard|bankcard/i.test(e.id) || /mobile|phone|idcard|bankcard/i.test(e.fieldId) ? n.replace(/\s+/g, "") : n;
491
+ }
492
+ function Se(n) {
493
+ const e = Object.keys(P).filter((t) => f(n, P[t]));
494
+ return e.length ? e.includes("emergencyContact") ? "emergencyContact" : e.includes("legalPerson") ? "legalPerson" : e.includes("spouse") ? "spouse" : "contact" : null;
495
+ }
496
+ function Ie(n) {
497
+ const e = n.replace(/\s+/g, "");
498
+ return /^1[3-9]\d{9}$/.test(e) ? "mobile" : /^\d{17}[\dXx]$/.test(e) ? "idCard" : /^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$/.test(n) ? "email" : /^\d{16,19}$/.test(e) ? "bankCard" : /^\d{4}[-/.年]\d{1,2}[-/.月]\d{1,2}日?$/.test(n) ? "date" : /^[0-9]+(?:\.[0-9]{1,2})?$/.test(e) ? "amount" : /[省市区县路街道号栋室乡镇]/.test(n) ? "address" : /^[\u4e00-\u9fa5a-zA-Z·]{2,20}$/.test(n) ? "name" : "unknown";
499
+ }
500
+ function N(n, e, t) {
501
+ var i;
502
+ const s = (i = ke[e]) == null ? void 0 : i[t];
503
+ s && n.push(s);
504
+ }
505
+ const ke = {
506
+ legalPerson: {
507
+ mobile: "legalPersonMobile",
508
+ idCard: "legalPersonIdCard",
509
+ name: "legalPersonName"
510
+ },
511
+ emergencyContact: {
512
+ mobile: "emergencyContactMobile",
513
+ name: "emergencyContactName"
514
+ },
515
+ contact: {
516
+ mobile: "contactMobile",
517
+ email: "contactEmail",
518
+ name: "contactName"
519
+ },
520
+ spouse: {
521
+ mobile: "spouseMobile",
522
+ idCard: "spouseIdCard",
523
+ name: "spouseName"
524
+ }
525
+ };
526
+ function Ae(n) {
527
+ return n.replace(/[“”"'`]/g, "").replace(/\s+/g, " ").trim();
528
+ }
529
+ function Z(n) {
530
+ return n.replace(/[,。;;]+$/g, "").replace(/^[“”"'`\s]+|[“”"'`\s]+$/g, "").trim();
531
+ }
532
+ function f(n, e) {
533
+ const t = w(n);
534
+ return e.some((s) => t.includes(w(s)));
535
+ }
536
+ function Te(n) {
537
+ const e = /* @__PURE__ */ new Map();
538
+ for (const t of n) {
539
+ const s = `${t.key}::${t.value}`, i = e.get(s);
540
+ (!i || i.confidence < t.confidence) && e.set(s, t);
541
+ }
542
+ return [...e.values()];
543
+ }
544
+ function Ce(n) {
545
+ return [...new Set(n)];
546
+ }
547
+ const Le = [
548
+ "input",
549
+ "textarea",
550
+ "select",
551
+ "[contenteditable]:not([contenteditable='false'])"
552
+ ].join(", "), z = [
553
+ "dialog[open]",
554
+ "[role='dialog']",
555
+ "[aria-modal='true']",
556
+ ".modal",
557
+ ".modal-dialog",
558
+ ".ant-modal",
559
+ ".ant-drawer",
560
+ ".el-dialog",
561
+ ".el-drawer",
562
+ ".n-dialog",
563
+ ".n-modal",
564
+ ".popup",
565
+ ".drawer"
566
+ ].join(", "), Fe = /* @__PURE__ */ new Set(["hidden", "submit", "button", "image", "reset", "file", "password"]);
567
+ class Me {
568
+ constructor(e = document) {
569
+ this.root = e;
570
+ }
571
+ /**
572
+ * 扫描可回填字段。
573
+ * - registered 非空:仅解析注册 Schema,不扫 DOM
574
+ * - 否则:自动扫描可见 input/textarea/select,优先弹窗内控件
575
+ */
576
+ scan(e = {}) {
577
+ var d;
578
+ const t = L("scan");
579
+ if ((d = e.registered) != null && d.length)
580
+ return {
581
+ scanToken: t,
582
+ fields: e.registered.map((l) => this.fromSchema(l, t))
583
+ };
584
+ const s = this.resolveRoot(e.scanContainer), i = this.collectVisibleFields(s), r = this.detectTopLayerContainer(i), a = i.filter((l) => r.contains(l)), o = (a.length ? a : i).slice(0, e.maxFields ?? 200);
585
+ return {
586
+ scanToken: t,
587
+ fields: o.map((l, u) => this.fromElement(l, u, t))
588
+ };
589
+ }
590
+ /** FieldSchema → FieldDescriptor,解析 element 并生成 fingerprint */
591
+ fromSchema(e, t) {
592
+ const s = De(e.element, this.root), i = {
593
+ fieldId: e.rowKey == null ? e.fieldId : `${e.fieldId}:${e.rowKey}`,
594
+ label: e.label,
595
+ type: e.type,
596
+ localRuleMode: e.localRuleMode ?? "inherit",
597
+ required: e.required,
598
+ section: e.section,
599
+ options: e.options,
600
+ source: "registered",
601
+ scanToken: t,
602
+ element: s || void 0,
603
+ fingerprint: ""
604
+ };
605
+ return i.fingerprint = $({
606
+ ...i,
607
+ tagName: s == null ? void 0 : s.tagName.toLowerCase()
608
+ }), i;
609
+ }
610
+ /** DOM 元素 → FieldDescriptor,自动推断 label/type 并写入 data-smart-fill-id */
611
+ fromElement(e, t, s) {
612
+ const i = Ne(e, t), r = $e(e), a = {
613
+ fieldId: i,
614
+ fingerprint: "",
615
+ scanToken: s,
616
+ type: Oe(e),
617
+ localRuleMode: "inherit",
618
+ label: G(e),
619
+ placeholder: X(e),
620
+ name: v(e.getAttribute("name")) || void 0,
621
+ id: v(e.id) || void 0,
622
+ section: _e(e),
623
+ options: r,
624
+ required: e.hasAttribute("required") || e.getAttribute("aria-required") === "true",
625
+ readonly: e.hasAttribute("readonly"),
626
+ disabled: e.hasAttribute("disabled"),
627
+ source: "form_scan",
628
+ element: e
629
+ };
630
+ return a.fingerprint = $({
631
+ ...a,
632
+ tagName: e.tagName.toLowerCase()
633
+ }), a;
634
+ }
635
+ /** 解析扫描根节点,scanContainer 为 CSS 选择器 */
636
+ resolveRoot(e) {
637
+ if (e) {
638
+ const t = W(this.root, e);
639
+ if (t)
640
+ return t;
641
+ }
642
+ return this.root;
643
+ }
644
+ /** 收集 root 下可见且可编辑的表单控件 */
645
+ collectVisibleFields(e) {
646
+ return Array.from(e.querySelectorAll(Le)).filter((t) => !(t instanceof HTMLElement) || !Re(t) || t instanceof HTMLInputElement && Fe.has((t.type || "").toLowerCase()) ? !1 : !t.hasAttribute("disabled") && !t.hasAttribute("readonly"));
647
+ }
648
+ /**
649
+ * 检测最应优先扫描的容器。
650
+ * 优先 activeElement 所在弹窗,否则取包含最多字段的弹窗容器。
651
+ */
652
+ detectTopLayerContainer(e) {
653
+ const t = document.activeElement instanceof HTMLElement ? document.activeElement : null, s = t == null ? void 0 : t.closest(z);
654
+ if (s instanceof HTMLElement && e.some((r) => s.contains(r)))
655
+ return s;
656
+ const i = e.map((r) => r.closest(z)).filter((r) => r instanceof HTMLElement);
657
+ return i.length ? i.sort((r, a) => e.filter((o) => a.contains(o)).length - e.filter((o) => r.contains(o)).length)[0] : document.body;
658
+ }
659
+ }
660
+ function Ne(n, e) {
661
+ const t = n.getAttribute("data-smart-fill-id");
662
+ if (t)
663
+ return t;
664
+ const i = v(n.getAttribute("data-smart-fill-key")) || v(n.getAttribute("name")) || v(n.id) || `smart-fill-${Date.now()}-${e}`;
665
+ return n.setAttribute("data-smart-fill-id", i), i;
666
+ }
667
+ function Oe(n) {
668
+ if (n instanceof HTMLTextAreaElement) return "textarea";
669
+ if (n instanceof HTMLSelectElement) return "select";
670
+ if (n instanceof HTMLInputElement) {
671
+ const e = (n.type || "text").toLowerCase();
672
+ if (e === "radio") return "radio";
673
+ if (e === "checkbox") return "checkbox";
674
+ if (e === "date" || e === "month") return "date";
675
+ if (e === "number") return "number";
676
+ }
677
+ return "text";
678
+ }
679
+ function G(n) {
680
+ const e = n.getAttribute("aria-label");
681
+ if (e) return e.trim();
682
+ if (n.id) {
683
+ const r = document.querySelector(`label[for="${J(n.id)}"]`);
684
+ if (r != null && r.textContent) return I(r.textContent);
685
+ }
686
+ const t = n.closest("label");
687
+ if (t != null && t.textContent) return I(t.textContent);
688
+ const s = n.closest(".form-item, .ant-form-item, .el-form-item, .field, .form-row"), i = s == null ? void 0 : s.querySelector("label, .ant-form-item-label, .el-form-item__label, .label");
689
+ return i != null && i.textContent ? I(i.textContent) : X(n) || n.getAttribute("name") || n.id || "未命名字段";
690
+ }
691
+ function X(n) {
692
+ return n.getAttribute("placeholder") || n.getAttribute("aria-placeholder") || void 0;
693
+ }
694
+ function _e(n) {
695
+ const e = n.closest("fieldset, section, .panel, .card, .form-section"), t = e == null ? void 0 : e.querySelector("legend, h1, h2, h3, .title, .section-title");
696
+ return t != null && t.textContent ? I(t.textContent) : void 0;
697
+ }
698
+ function $e(n) {
699
+ if (n instanceof HTMLSelectElement)
700
+ return Array.from(n.options).map((e) => ({
701
+ label: I(e.textContent || e.label),
702
+ value: e.value
703
+ }));
704
+ if (n instanceof HTMLInputElement && (n.type === "radio" || n.type === "checkbox") && n.name)
705
+ return Array.from(document.querySelectorAll(`input[name="${J(n.name)}"]`)).map((e) => ({
706
+ label: G(e),
707
+ value: e.value || !0
708
+ }));
709
+ }
710
+ function Re(n) {
711
+ const e = window.getComputedStyle(n);
712
+ return e.display === "none" || e.visibility === "hidden" || Number(e.opacity) === 0 ? !1 : !!(n.offsetWidth || n.offsetHeight || n.getClientRects().length);
713
+ }
714
+ function De(n, e) {
715
+ return n instanceof HTMLElement ? n : typeof n == "string" ? W(e, n) : null;
716
+ }
717
+ function W(n, e) {
718
+ try {
719
+ const t = n.querySelector(e);
720
+ return t instanceof HTMLElement ? t : null;
721
+ } catch {
722
+ return null;
723
+ }
724
+ }
725
+ function I(n) {
726
+ return n.replace(/[*::]/g, "").replace(/\s+/g, " ").trim();
727
+ }
728
+ function J(n) {
729
+ return typeof CSS < "u" && CSS.escape ? CSS.escape(n) : n.replace(/["\\]/g, "\\$&");
730
+ }
731
+ const O = 5, Pe = 10 * 1024 * 1024, ze = 50 * 1024 * 1024;
732
+ class Ke {
733
+ constructor(e) {
734
+ /** 面板宿主元素,挂载到 container 下 */
735
+ c(this, "host", null);
736
+ /** Shadow DOM 根或 fallback 到 host */
737
+ c(this, "root", null);
738
+ /** 识别结果候选项,供 review 区域展示 */
739
+ c(this, "autoApplyState", null);
740
+ /** 用户选择的图片文件列表 */
741
+ c(this, "selectedFiles", []);
742
+ /** 文本输入框内容缓存,render 重绘时保留 */
743
+ c(this, "inputText", "");
744
+ /** 本地优先识别开关状态 */
745
+ c(this, "localPriorityEnabled");
746
+ /** 面板展开状态:inline 默认展开,floating 默认收起 */
747
+ c(this, "isOpen", !1);
748
+ /** floating 模式拖拽状态 */
749
+ c(this, "dragState", null);
750
+ /** 拖拽结束后抑制 click 触发 open,避免误开面板 */
751
+ c(this, "suppressOpenClick", !1);
752
+ /** 鼠标是否正悬浮在图片区,供 Ctrl+V 粘贴图片触发判断 */
753
+ c(this, "isUploadHovering", !1);
754
+ c(this, "handleDragMove", (e) => {
755
+ if (!this.dragState || !this.host || e.pointerId !== this.dragState.pointerId) return;
756
+ const t = e.clientX - this.dragState.startX, s = e.clientY - this.dragState.startY;
757
+ (Math.abs(t) > 3 || Math.abs(s) > 3) && (this.dragState.moved = !0);
758
+ const i = this.host.getBoundingClientRect(), r = Math.max(8, window.innerWidth - i.width - 8), a = Math.max(8, window.innerHeight - i.height - 8), o = C(this.dragState.startLeft + t, 8, r), d = C(this.dragState.startTop + s, 8, a);
759
+ this.host.style.left = `${o}px`, this.host.style.top = `${d}px`;
760
+ });
761
+ c(this, "handleDragEnd", (e) => {
762
+ !this.dragState || e.pointerId !== this.dragState.pointerId || (this.suppressOpenClick = this.dragState.moved, this.dragState = null, document.removeEventListener("pointermove", this.handleDragMove), document.removeEventListener("pointerup", this.handleDragEnd), document.removeEventListener("pointercancel", this.handleDragEnd), this.suppressOpenClick && window.setTimeout(() => {
763
+ this.suppressOpenClick = !1;
764
+ }, 0));
765
+ });
766
+ /** 面板聚焦或鼠标悬浮在图片区时接管全局粘贴图片 */
767
+ c(this, "handleDocumentPaste", (e) => {
768
+ const t = Ve(e);
769
+ if (!t.length || !this.isOpen || !this.host)
770
+ return;
771
+ const s = document.activeElement, i = this.root instanceof ShadowRoot ? this.root.activeElement : null;
772
+ !(s === this.host || this.host.contains(s) || i) && !this.isUploadHovering || (e.preventDefault(), this.handleImages(t));
773
+ });
774
+ this.options = e, this.localPriorityEnabled = e.localPriorityEnabled ?? !1;
775
+ }
776
+ /** 挂载面板到指定容器,创建 Shadow DOM 并首次渲染 */
777
+ mount(e) {
778
+ this.host = document.createElement("div"), this.host.className = `sf-sdk-host sf-sdk-host-${this.options.mode}`, e.appendChild(this.host), this.root = this.host.attachShadow ? this.host.attachShadow({ mode: "open" }) : this.host, this.isOpen = this.options.initialOpen ?? this.options.mode === "inline", document.addEventListener("paste", this.handleDocumentPaste), this.render(this.isOpen);
779
+ }
780
+ /** 控制面板展开/收起 */
781
+ setOpen(e) {
782
+ this.isOpen = e, this.render(this.isOpen), this.keepHostInViewport();
783
+ }
784
+ /** 同步本地优先开关状态(来自其他面板/外部持久化),不触发 onLocalPriorityChange */
785
+ setLocalPriorityEnabled(e) {
786
+ this.localPriorityEnabled = e;
787
+ const t = this.query('[data-role="local-priority-toggle"]');
788
+ t && (t.checked = e);
789
+ }
790
+ /** 识别按钮 loading 状态 */
791
+ setBusy(e, t = "") {
792
+ const s = this.query('[data-role="recognize"]');
793
+ s && (s.disabled = e, s.textContent = e ? t : this.t("recognize", "智能识别"));
794
+ }
795
+ /** 更新底部状态文案 */
796
+ setStatus(e) {
797
+ const t = this.query('[data-role="status"]');
798
+ t && (t.textContent = e, t.removeAttribute("data-status"));
799
+ }
800
+ /** 展示错误状态(红色文案) */
801
+ setError(e) {
802
+ const t = this.query('[data-role="status"]');
803
+ t && (t.textContent = e, t.setAttribute("data-status", "error"));
804
+ }
805
+ /** 更新识别结果列表并刷新 review 区域 */
806
+ setAutoApplyState(e) {
807
+ this.autoApplyState = e, this.render(!0), this.keepHostInViewport(), this.setStatus(this.t("recognized", "识别完成,正在自动回填..."));
808
+ }
809
+ /** 展示回填结果统计(成功/跳过数量) */
810
+ setApplyResult(e) {
811
+ const t = this.query('[data-role="status"]');
812
+ t && (t.textContent = `已回填 ${e.applied.length} 项,跳过 ${e.skipped.length} 项。`, t.setAttribute("data-status", e.skipped.length ? "warning" : "success"));
813
+ }
814
+ /** 移除 DOM 节点并解绑拖拽监听 */
815
+ destroy() {
816
+ var e;
817
+ document.removeEventListener("pointermove", this.handleDragMove), document.removeEventListener("pointerup", this.handleDragEnd), document.removeEventListener("pointercancel", this.handleDragEnd), document.removeEventListener("paste", this.handleDocumentPaste), (e = this.host) == null || e.remove(), this.host = null, this.root = null, this.dragState = null, this.isUploadHovering = !1;
818
+ }
819
+ render(e) {
820
+ this.root && (this.root.innerHTML = `
821
+ <style>${qe}</style>
822
+ ${this.options.mode === "floating" ? `<button class="sf-float" type="button" data-role="open">${this.t("entry", "智能录入")}</button>` : ""}
823
+ <section class="sf-panel ${e ? "is-open" : ""} ${this.options.mode === "inline" ? "is-inline" : "is-floating"}" aria-label="智能录入面板">
824
+ <header class="sf-header">
825
+ <strong>${this.t("title", "智能录入")}</strong>
826
+ <button class="sf-icon-btn" type="button" data-role="close" aria-label="${this.options.mode === "inline" ? e ? this.t("collapse", "收起") : this.t("expand", "展开") : this.t("close", "关闭")}">${this.options.mode === "inline" ? e ? "∧" : "∨" : "x"}</button>
827
+ </header>
828
+ <div class="sf-body ${e ? "is-open" : ""}">
829
+ <label class="sf-toggle">
830
+ <span class="sf-toggle-main">
831
+ <input type="checkbox" data-role="local-priority-toggle" ${this.localPriorityEnabled ? "checked" : ""} />
832
+ <span class="sf-toggle-title">${this.t("localPriorityTitle", "启用本地优先识别")}</span>
833
+ </span>
834
+ <span class="sf-toggle-desc">${this.t("localPriorityDesc", "开启后优先使用前端本地规则提取手机号、证件号等;关闭后直接走后端识别流程。")}</span>
835
+ </label>
836
+ <div class="sf-entry-grid">
837
+ <div class="sf-textarea-wrap">
838
+ <textarea class="sf-textarea" data-role="text" placeholder="${this.t("placeholder", "粘贴文本,如:姓名:张三 手机号:13800000000")}">${He(this.inputText)}</textarea>
839
+ <div class="sf-textarea-actions">
840
+ <button class="sf-btn sf-btn-secondary" type="button" data-role="clear">${this.t("clear", "清空")}</button>
841
+ <button class="sf-btn sf-btn-primary" type="button" data-role="recognize">${this.t("recognize", "智能识别")}</button>
842
+ </div>
843
+ </div>
844
+ <label class="sf-upload" data-role="upload" tabindex="0">
845
+ <input data-role="file" type="file" name="files" accept="image/*" multiple hidden />
846
+ <span class="sf-upload-illustration" aria-hidden="true">
847
+ <span class="sf-upload-icon">
848
+ <svg viewBox="0 0 24 24" class="sf-upload-svg" focusable="false" aria-hidden="true">
849
+ <path d="M6 5.5h8.2L18.5 9v9.5a1 1 0 0 1-1 1h-11a1 1 0 0 1-1-1v-12a1 1 0 0 1 1-1Z" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linejoin="round"/>
850
+ <path d="M14 5.5V9h4.5" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linejoin="round"/>
851
+ <path d="m8.5 16 2.2-2.2a.9.9 0 0 1 1.3 0l1.2 1.2 1.6-1.6a.9.9 0 0 1 1.3 0L18.5 16" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"/>
852
+ <circle cx="10" cy="10.5" r="1" fill="currentColor"/>
853
+ </svg>
854
+ </span>
855
+ <span class="sf-upload-hint">${this.t("uploadHint", `点击、拖拽、Ctrl + V 粘贴图片至此(最多 ${O} 张)`)}</span>
856
+ </span>
857
+ <span class="sf-upload-btn" data-role="file-label">图片识别</span>
858
+ </label>
859
+ </div>
860
+ <div class="sf-actions">
861
+ <span class="sf-status" data-role="status">${this.t("empty", "暂无识别结果")}</span>
862
+ </div>
863
+ </div>
864
+ </section>
865
+ `, this.bindEvents());
866
+ }
867
+ bindEvents() {
868
+ var s, i, r, a, o, d, l;
869
+ const e = this.query('[data-role="open"]');
870
+ e == null || e.addEventListener("pointerdown", (u) => this.startDrag(u)), e == null || e.addEventListener("click", () => {
871
+ this.suppressOpenClick || this.options.onOpen();
872
+ }), (s = this.query(".sf-header")) == null || s.addEventListener("pointerdown", (u) => this.startDrag(u)), (i = this.query('[data-role="close"]')) == null || i.addEventListener("click", () => {
873
+ if (this.options.mode === "inline") {
874
+ this.isOpen ? this.options.onClose() : this.options.onOpen();
875
+ return;
876
+ }
877
+ this.options.onClose();
878
+ }), (r = this.query('[data-role="file"]')) == null || r.addEventListener("change", (u) => {
879
+ const g = u.target;
880
+ this.handleImages(Array.from(g.files || []));
881
+ });
882
+ const t = this.query('[data-role="upload"]');
883
+ t == null || t.addEventListener("pointerenter", () => {
884
+ this.isUploadHovering = !0;
885
+ }), t == null || t.addEventListener("pointerleave", () => {
886
+ this.isUploadHovering = !1, t.removeAttribute("data-dragover");
887
+ }), t == null || t.addEventListener("dragenter", (u) => {
888
+ u.preventDefault(), this.isUploadHovering = !0, t.setAttribute("data-dragover", "true");
889
+ }), t == null || t.addEventListener("dragover", (u) => {
890
+ u.preventDefault(), t.setAttribute("data-dragover", "true");
891
+ }), t == null || t.addEventListener("dragleave", (u) => {
892
+ u.currentTarget.contains(u.relatedTarget) || (this.isUploadHovering = !1, t.removeAttribute("data-dragover"));
893
+ }), t == null || t.addEventListener("drop", (u) => {
894
+ var g;
895
+ u.preventDefault(), this.isUploadHovering = !1, t.removeAttribute("data-dragover"), this.handleImages(Array.from(((g = u.dataTransfer) == null ? void 0 : g.files) || []));
896
+ }), (a = this.query('[data-role="text"]')) == null || a.addEventListener("input", (u) => {
897
+ this.inputText = u.target.value;
898
+ }), (o = this.query('[data-role="local-priority-toggle"]')) == null || o.addEventListener("change", (u) => {
899
+ var g, x;
900
+ this.localPriorityEnabled = u.target.checked, (x = (g = this.options).onLocalPriorityChange) == null || x.call(g, this.localPriorityEnabled);
901
+ }), (d = this.query('[data-role="clear"]')) == null || d.addEventListener("click", () => {
902
+ this.clearFormState();
903
+ }), (l = this.query('[data-role="recognize"]')) == null || l.addEventListener("click", () => {
904
+ const u = this.query('[data-role="text"]'), g = u == null ? void 0 : u.value.trim();
905
+ if (this.inputText = (u == null ? void 0 : u.value) || this.inputText, !g) {
906
+ this.setError(this.t("emptyInput", "请输入文本内容。"));
907
+ return;
908
+ }
909
+ this.options.onRecognize({ text: g });
910
+ });
911
+ }
912
+ clearFormState() {
913
+ this.inputText = "", this.resetSelectedFiles(), this.autoApplyState = null, this.render(!0), this.setStatus(this.t("empty", "暂无识别结果"));
914
+ }
915
+ /** 清空当前已选图片与上传文案,供 clear 和图片识别成功后复用 */
916
+ resetSelectedFiles() {
917
+ var t;
918
+ this.selectedFiles = [];
919
+ const e = this.query('[data-role="file"]');
920
+ e && (e.value = ""), this.isUploadHovering = !1, (t = this.query('[data-role="upload"]')) == null || t.removeAttribute("data-dragover");
921
+ }
922
+ /** 校验并触发图片识别,支持点击选择 / 拖拽 / 粘贴三种入口 */
923
+ async handleImages(e) {
924
+ const t = e.filter((r) => r.type.startsWith("image/"));
925
+ if (!t.length) {
926
+ this.resetSelectedFiles(), this.setError(this.t("invalidImageError", "请选择图片文件。"));
927
+ return;
928
+ }
929
+ if (t.length > O) {
930
+ this.resetSelectedFiles(), this.setError(this.t("maxFilesError", `最多上传 ${O} 张图片。`));
931
+ return;
932
+ }
933
+ if (t.find((r) => r.size > Pe)) {
934
+ this.resetSelectedFiles(), this.setError(this.t("maxSingleFileSizeError", "单张图片不能超过 10MB。"));
935
+ return;
936
+ }
937
+ if (t.reduce((r, a) => r + a.size, 0) > ze) {
938
+ this.resetSelectedFiles(), this.setError(this.t("maxTotalFileSizeError", "上传图片总大小不能超过 50MB。"));
939
+ return;
940
+ }
941
+ this.selectedFiles = t, this.setStatus(this.t("imageReady", `已选择 ${this.selectedFiles.length} 张图片,开始识别...`));
942
+ try {
943
+ await this.options.onRecognize({ images: [...this.selectedFiles] }), this.resetSelectedFiles();
944
+ } catch {
945
+ }
946
+ }
947
+ startDrag(e) {
948
+ var s;
949
+ if (this.options.mode !== "floating" || !this.host || e.button !== 0 || (s = e.target) != null && s.closest('[data-role="close"]')) return;
950
+ const t = this.host.getBoundingClientRect();
951
+ this.host.style.left = `${t.left}px`, this.host.style.top = `${t.top}px`, this.host.style.right = "auto", this.host.style.bottom = "auto", this.dragState = {
952
+ pointerId: e.pointerId,
953
+ startX: e.clientX,
954
+ startY: e.clientY,
955
+ startLeft: t.left,
956
+ startTop: t.top,
957
+ moved: !1
958
+ }, e.preventDefault(), document.addEventListener("pointermove", this.handleDragMove), document.addEventListener("pointerup", this.handleDragEnd), document.addEventListener("pointercancel", this.handleDragEnd);
959
+ }
960
+ keepHostInViewport() {
961
+ this.options.mode !== "floating" || !this.host || !this.host.style.left || !this.host.style.top || window.requestAnimationFrame(() => {
962
+ if (!this.host) return;
963
+ const e = this.host.getBoundingClientRect(), t = C(e.left, 8, Math.max(8, window.innerWidth - e.width - 8)), s = C(e.top, 8, Math.max(8, window.innerHeight - e.height - 8));
964
+ this.host.style.left = `${t}px`, this.host.style.top = `${s}px`;
965
+ });
966
+ }
967
+ query(e) {
968
+ var t;
969
+ return ((t = this.root) == null ? void 0 : t.querySelector(e)) || null;
970
+ }
971
+ t(e, t) {
972
+ var s;
973
+ return ((s = this.options.messages) == null ? void 0 : s[e]) || t;
974
+ }
975
+ }
976
+ function He(n) {
977
+ return n.replace(/[&<>"']/g, (e) => ({
978
+ "&": "&amp;",
979
+ "<": "&lt;",
980
+ ">": "&gt;",
981
+ '"': "&quot;",
982
+ "'": "&#39;"
983
+ })[e] || e);
984
+ }
985
+ function C(n, e, t) {
986
+ return Math.min(Math.max(n, e), t);
987
+ }
988
+ function Ve(n) {
989
+ var t;
990
+ return Array.from(((t = n.clipboardData) == null ? void 0 : t.items) || []).filter((s) => s.kind === "file" && s.type.startsWith("image/")).map((s) => s.getAsFile()).filter((s) => !!s);
991
+ }
992
+ const qe = `
993
+ :host, .sf-panel { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; }
994
+ :host(.sf-sdk-host-floating), .sf-sdk-host-floating { position: fixed; top: 24px; right: 24px; z-index: 2147483647; display: flex; flex-direction: column; align-items: flex-end; gap: 12px; width: min(420px, calc(100vw - 32px)); pointer-events: none; }
995
+ .sf-float { align-self: flex-end; border: 0; border-radius: 999px; padding: 12px 18px; color: #fff; background: #2563eb; box-shadow: 0 10px 24px rgba(37,99,235,.3); cursor: move; user-select: none; touch-action: none; pointer-events: auto; }
996
+ .sf-panel { display: none; width: 100%; box-sizing: border-box; border: 1px solid #dbe3ef; border-radius: 16px; background: #fff; color: #172033; box-shadow: 0 18px 48px rgba(15,23,42,.18); padding: 16px; pointer-events: auto; }
997
+ .sf-panel.is-open { display: block; }
998
+ .sf-panel.is-inline { display: block; }
999
+ .sf-body { display: none; }
1000
+ .sf-body.is-open { display: block; }
1001
+ .sf-header, .sf-actions { display: flex; align-items: center; justify-content: space-between; gap: 10px; }
1002
+ .sf-header { cursor: move; user-select: none; touch-action: none; }
1003
+ .sf-icon-btn {font-size: 16px; border: 0; background: transparent; cursor: pointer; color: #64748b; font-weight: 700; }
1004
+ .sf-tip, .sf-status, .sf-empty, small { color: #64748b; font-size: 12px; }
1005
+ .sf-toggle { display: grid; gap: 4px; margin: 10px 0 12px; padding: 10px 12px; border: 1px solid #dbe3ef; border-radius: 10px; background: #f8fbff; }
1006
+ .sf-toggle-main { display: flex; align-items: center; gap: 8px; color: #172033; font-size: 13px; font-weight: 600; }
1007
+ .sf-toggle-main input { margin: 0; }
1008
+ .sf-toggle-desc { color: #64748b; font-size: 12px; line-height: 1.4; }
1009
+ .sf-entry-grid { display: grid; grid-template-columns: minmax(0, 1fr) 200px; gap: 12px; align-items: stretch; margin: 10px 0 14px; }
1010
+ .sf-textarea-wrap { position: relative; }
1011
+ .sf-textarea { width: 100%; min-height: 140px; resize: vertical; border: 1px solid #cbd5e1; border-radius: 10px; padding: 10px 10px 56px; box-sizing: border-box; }
1012
+ .sf-textarea-actions { position: absolute; right: 12px; bottom: 12px; display: flex; gap: 10px; }
1013
+ .sf-upload { display: flex; flex-direction: column; justify-content: space-between; align-items: center; border: 1px solid #e4e7ee; border-radius: 10px; padding: 14px 10px 12px; text-align: center; color: #98a2b3; cursor: pointer; height: 140px; background: #f7f8fb; box-sizing: border-box; transition: border-color .18s ease, background-color .18s ease, box-shadow .18s ease; outline: none; }
1014
+ .sf-upload[data-dragover="true"] { border-color: #7aa7ff; background: #eef4ff; box-shadow: inset 0 0 0 1px rgba(78, 134, 255, .18); }
1015
+ .sf-upload:focus-visible { border-color: #7aa7ff; box-shadow: 0 0 0 3px rgba(78, 134, 255, .18); }
1016
+ .sf-upload-illustration { display: grid; justify-items: center; gap: 8px; width: 100%; }
1017
+ .sf-upload-icon { display: inline-flex; align-items: center; justify-content: center; width: 26px; height: 26px; color: #b6bdc9; }
1018
+ .sf-upload-svg { width: 24px; height: 24px; }
1019
+ .sf-upload-hint { color: #a0a7b4; font-size: 12px; line-height: 1.45; word-break: break-word; }
1020
+ .sf-upload-btn { display: inline-flex; align-items: center; justify-content: center; min-width: 92px; padding: 8px 16px; border: 1px solid #d2d6df; border-radius: 999px; background: #fff; color: #667085; font-size: 13px; line-height: 1; }
1021
+ .sf-actions { margin: 0 0 12px; }
1022
+ .sf-status { flex: 1; min-width: 0; line-height: 1.4; }
1023
+ .sf-btn { border: 1px solid #d7e3f5; border-radius: 999px; background: #fff; color: #4b5563; padding: 8px 20px; cursor: pointer; font-size: 13px; font-weight: 600; transition: all .18s ease; }
1024
+ .sf-btn:hover { transform: translateY(-1px); box-shadow: 0 8px 20px rgba(15, 23, 42, .08); }
1025
+ .sf-btn-secondary { background: #fff; color: #6b7280; border-color: #e5e7eb; }
1026
+ .sf-btn-primary { min-width: 100px; border-color: transparent; background: linear-gradient(135deg, #6aa6ff 0%, #4e86ff 45%, #3b72f6 100%); color: #fff; box-shadow: 0 12px 26px rgba(59, 114, 246, .32); }
1027
+ .sf-btn:disabled { opacity: .55; cursor: not-allowed; }
1028
+ .sf-panel.is-floating .sf-entry-grid { grid-template-columns: 1fr; gap: 10px; }
1029
+ .sf-panel.is-floating .sf-textarea { min-height: 140px; }
1030
+ .sf-panel.is-floating .sf-upload { height: 96px; padding-top: 10px; padding-bottom: 10px; }
1031
+ .sf-panel.is-floating .sf-upload-illustration { gap: 6px; }
1032
+ .sf-panel.is-floating .sf-upload-icon { width: 22px; height: 22px; }
1033
+ .sf-panel.is-floating .sf-upload-svg { width: 20px; height: 20px; }
1034
+ .sf-panel.is-floating .sf-upload-hint { font-size: 11px; line-height: 1.35; }
1035
+ [data-status="error"] { color: #dc2626; }
1036
+ [data-status="warning"] { color: #d97706; }
1037
+ [data-status="success"] { color: #16a34a; }
1038
+ `, Ue = 0.75;
1039
+ class Be {
1040
+ constructor(e, t) {
1041
+ /** 实例级事件总线,对应 instance.on(...) */
1042
+ c(this, "events", new Y());
1043
+ /** 页面字段扫描器,root 来自 SmartFill.create({ root }) */
1044
+ c(this, "scanner");
1045
+ /** 浏览器端本地规则引擎,用于文本正则/键值对提取 */
1046
+ c(this, "ruleEngine", new me());
1047
+ /** Shadow DOM 面板,mount / mountFloatingButton 后可用(指向最后挂载的面板,用于状态展示) */
1048
+ c(this, "panel", null);
1049
+ /** 已挂载的全部面板(inline / floating 可同时存在),用于本地优先开关广播与销毁 */
1050
+ c(this, "panels", []);
1051
+ /** 面板展开状态持久化 key */
1052
+ c(this, "panelStorageKeys", /* @__PURE__ */ new Map());
1053
+ /** 业务方 registerFields 注册的字段;非空时 rescan 仅扫描这些字段 */
1054
+ c(this, "registeredFields", []);
1055
+ /** 组件库回填适配器链,如 AntD / Element 自定义控件 */
1056
+ c(this, "adapters", []);
1057
+ /** 最近一次 rescan 结果;scanToken 用于识别/回填防过期校验 */
1058
+ c(this, "scanResult", null);
1059
+ /** recognize 完成后生成的自动回填候选,供面板展示与 applyAutoItems 使用 */
1060
+ c(this, "autoApplyState", null);
1061
+ /** 预留:表单配置版本号,识别请求可携带给后端 */
1062
+ c(this, "formConfigVersion");
1063
+ /** 面板「启用本地优先识别」开关状态,默认关闭(走后端识别) */
1064
+ c(this, "localPriorityEnabled", !1);
1065
+ /** 实例是否已 destroy,销毁后所有公开方法抛 INSTANCE_DESTROYED */
1066
+ c(this, "destroyed", !1);
1067
+ this.config = e, this.context = t, this.scanner = new Me(e.root || document), this.localPriorityEnabled = Je(), this.context.manager.add(this);
1068
+ }
1069
+ /** 订阅实例事件,返回取消订阅函数 */
1070
+ on(e, t) {
1071
+ return this.events.on(e, t);
1072
+ }
1073
+ /** 注册组件库回填适配器,支持链式调用 */
1074
+ useAdapter(e) {
1075
+ return this.assertAlive(), this.adapters.push(e), this;
1076
+ }
1077
+ /**
1078
+ * 显式注册字段映射(L3 模式)。
1079
+ * 注册后 rescan 不再自动扫 DOM,仅解析 element 选择器定位控件。
1080
+ * rowKey 用于明细行等同 fieldId 多行场景,不可重复。
1081
+ */
1082
+ registerFields(e) {
1083
+ this.assertAlive();
1084
+ const t = /* @__PURE__ */ new Set();
1085
+ for (const s of e) {
1086
+ const i = s.rowKey == null ? s.fieldId : `${s.fieldId}:${s.rowKey}`;
1087
+ if (t.has(i))
1088
+ throw y("UNSUPPORTED_PAGE", `字段 ${i} 重复注册。`, "scan");
1089
+ t.add(i);
1090
+ }
1091
+ return this.registeredFields = e, this;
1092
+ }
1093
+ /** 取消注册字段;不传 fieldIds 时清空全部注册 */
1094
+ unregisterFields(e) {
1095
+ this.assertAlive(), this.registeredFields = e != null && e.length ? this.registeredFields.filter((t) => !e.includes(t.fieldId)) : [];
1096
+ }
1097
+ /** 将面板嵌入指定容器(inline 模式) */
1098
+ mount(e) {
1099
+ this.assertAlive();
1100
+ const t = typeof e == "string" ? document.querySelector(e) : e;
1101
+ if (!t)
1102
+ throw y("UNSUPPORTED_PAGE", "未找到智能录入挂载点。", "ui");
1103
+ const s = H("inline", We(e, t)), i = V(s, !0);
1104
+ return this.createPanel("inline", s, i).mount(t), this;
1105
+ }
1106
+ /** 挂载右下角悬浮按钮 + 弹框(floating 模式) */
1107
+ mountFloatingButton() {
1108
+ this.assertAlive();
1109
+ const e = H("floating"), t = V(e, !1);
1110
+ return this.createPanel("floating", e, t).mount(document.body), this;
1111
+ }
1112
+ /**
1113
+ * 创建面板并登记到 panels。
1114
+ * inline / floating 共用同一份 localPriorityEnabled,开关变更经 handleLocalPriorityChange 持久化并广播。
1115
+ */
1116
+ createPanel(e, t, s) {
1117
+ let i;
1118
+ return i = new Ke({
1119
+ mode: e,
1120
+ initialOpen: s,
1121
+ messages: this.config.messages,
1122
+ localPriorityEnabled: this.localPriorityEnabled,
1123
+ onOpen: () => this.open(i),
1124
+ onClose: () => this.close(i),
1125
+ onRecognize: (r) => (this.panel = i, this.recognize(r)),
1126
+ onLocalPriorityChange: (r) => this.handleLocalPriorityChange(r, i)
1127
+ }), this.panels.push(i), this.panelStorageKeys.set(i, t), this.panel = i, i;
1128
+ }
1129
+ /** 同步本地优先开关:更新内存态、写入 localStorage,并广播到其他已挂载面板 */
1130
+ handleLocalPriorityChange(e, t) {
1131
+ this.localPriorityEnabled = e, Qe(e);
1132
+ for (const s of this.panels)
1133
+ s !== t && s.setLocalPriorityEnabled(e);
1134
+ }
1135
+ /** 打开面板并触发 rescan,同时激活当前实例(关闭其他实例面板) */
1136
+ async open(e = this.panel) {
1137
+ this.assertAlive(), e && (this.context.manager.activate(this), this.panel = e, e.setOpen(!0), q(this.panelStorageKeys.get(e), !0), await this.rescan());
1138
+ }
1139
+ /** 关闭面板(不销毁实例) */
1140
+ close(e = this.panel) {
1141
+ e && (this.panel = e, e.setOpen(!1), q(this.panelStorageKeys.get(e), !1));
1142
+ }
1143
+ /**
1144
+ * 扫描页面可回填字段。
1145
+ * 优先级:registerFields > 页面 DOM 自动扫描。
1146
+ * 扫描成功 emit scanCompleted,失败 emit error。
1147
+ */
1148
+ async rescan() {
1149
+ var e;
1150
+ this.assertAlive();
1151
+ try {
1152
+ this.formConfigVersion = void 0;
1153
+ const t = this.registeredFields.length ? this.registeredFields : void 0;
1154
+ if (this.scanResult = this.scanner.scan({
1155
+ registered: t,
1156
+ maxFields: this.config.maxFields
1157
+ }), !this.scanResult.fields.length)
1158
+ throw y("NO_FIELDS_FOUND", "当前页面未找到可回填字段。", "scan");
1159
+ return this.events.emit("scanCompleted", {
1160
+ scanToken: this.scanResult.scanToken,
1161
+ fieldCount: this.scanResult.fields.length
1162
+ }), (e = this.panel) == null || e.setStatus(`已扫描 ${this.scanResult.fields.length} 个字段。`), this.scanResult;
1163
+ } catch (t) {
1164
+ throw this.emitError(t, "scan"), t;
1165
+ }
1166
+ }
1167
+ /**
1168
+ * 识别入口:文本/图片 → 本地规则 + 后端网关 → 合并 → 自动回填。
1169
+ *
1170
+ * localRuleMode 分流:
1171
+ * - off:跳过本地规则
1172
+ * - only:跳过后端,仅本地识别
1173
+ * - inherit:跟随 localPriorityEnabled 总开关
1174
+ *
1175
+ * 后端失败时,仅在当前已启用本地规则时,降级使用本地结果继续回填。
1176
+ */
1177
+ async recognize(e) {
1178
+ var r, a, o;
1179
+ this.assertAlive();
1180
+ const t = this.scanResult || await this.rescan(), s = L("trace"), i = performance.now();
1181
+ this.events.emit("recognizing", { scanToken: t.scanToken, traceId: s }), (r = this.panel) == null || r.setBusy(!0, "识别中...");
1182
+ try {
1183
+ const d = t.fields.filter((m) => Ge(m.localRuleMode, this.localPriorityEnabled)), l = t.fields.filter((m) => m.localRuleMode !== "only"), { text: u, usedOcr: g } = await this.context.client.resolveInputText({
1184
+ text: e.text,
1185
+ images: e.images,
1186
+ onStatusChange: (m) => {
1187
+ var k, E, A;
1188
+ if (m === "image_uploading") {
1189
+ (k = this.panel) == null || k.setStatus("图片上传中...");
1190
+ return;
1191
+ }
1192
+ if (m === "image_recognizing") {
1193
+ (E = this.panel) == null || E.setStatus("图片识别中...");
1194
+ return;
1195
+ }
1196
+ (A = this.panel) == null || A.setStatus("识别中...");
1197
+ }
1198
+ }), x = u ? this.ruleEngine.recognize(u, d, t.scanToken) : [];
1199
+ let b;
1200
+ if (!l.length)
1201
+ b = K(t.scanToken, s, x, i, void 0, g);
1202
+ else
1203
+ try {
1204
+ const m = await this.context.client.recognize({
1205
+ scanToken: t.scanToken,
1206
+ formCode: this.config.formCode,
1207
+ configVersion: this.formConfigVersion,
1208
+ text: u,
1209
+ usedOcr: g,
1210
+ fields: l,
1211
+ onStatusChange: () => {
1212
+ var E;
1213
+ (E = this.panel) == null || E.setStatus("识别中...");
1214
+ }
1215
+ });
1216
+ m.trace.durationMs = m.trace.durationMs || Math.round(performance.now() - i);
1217
+ const k = Ze(
1218
+ x,
1219
+ m.suggestions.filter((E) => l.some((A) => A.fieldId === E.fieldId))
1220
+ );
1221
+ b = { ...m, suggestions: k };
1222
+ } catch (m) {
1223
+ if (!x.length)
1224
+ throw m;
1225
+ b = K(
1226
+ t.scanToken,
1227
+ s,
1228
+ x,
1229
+ i,
1230
+ [g ? "后端识别失败,已使用 OCR 文本触发本地识别继续回填。" : "后端识别失败,已启用本地识别继续回填。"],
1231
+ g
1232
+ );
1233
+ }
1234
+ return this.autoApplyState = Ye(
1235
+ b.scanToken,
1236
+ b.trace.traceId,
1237
+ b.suggestions,
1238
+ t,
1239
+ this.registeredFields
1240
+ ), this.events.emit("recognized", b), (a = this.panel) == null || a.setAutoApplyState(this.autoApplyState), await this.applyAutoItems(this.autoApplyState), b;
1241
+ } catch (d) {
1242
+ throw this.emitError(d, "recognize"), d;
1243
+ } finally {
1244
+ (o = this.panel) == null || o.setBusy(!1);
1245
+ }
1246
+ }
1247
+ /**
1248
+ * 手动回填指定字段值。
1249
+ * 通常由业务方调用;recognize 流程内的自动回填走 applyAutoItems。
1250
+ * 需传入与当前 scanResult 一致的 scanToken。
1251
+ */
1252
+ async apply(e) {
1253
+ var i;
1254
+ if (this.assertAlive(), !this.scanResult)
1255
+ throw y("SCAN_TOKEN_EXPIRED", "请先扫描字段后再回填。", "apply");
1256
+ this.events.emit("applying", { scanToken: e.scanToken, count: e.values.length });
1257
+ const s = await new D(this.scanResult.fields, this.registeredFields, this.adapters).apply(e);
1258
+ return this.events.emit("applied", s), (i = this.panel) == null || i.setApplyResult(s), s;
1259
+ }
1260
+ /**
1261
+ * 自动回填:过滤 confidence ≥ 阈值且无 warnings 的候选项,写入 DOM。
1262
+ * 被策略跳过的字段记录在 skipped 中,reasonCode 为 LOW_CONFIDENCE / AUTO_APPLY_WARNING。
1263
+ */
1264
+ async applyAutoItems(e) {
1265
+ var o, d;
1266
+ if (!this.scanResult)
1267
+ throw y("SCAN_TOKEN_EXPIRED", "请先扫描字段后再回填。", "apply");
1268
+ const t = je(e), s = e.items.filter((l) => Q(l)).map((l) => ({ fieldId: l.fieldId, value: l.value, source: l.source }));
1269
+ this.events.emit("applying", { scanToken: e.scanToken, count: s.length }), (o = this.panel) == null || o.setStatus("识别完成,正在自动回填...");
1270
+ const r = await new D(this.scanResult.fields, this.registeredFields, this.adapters).apply({ scanToken: e.scanToken, values: s }), a = {
1271
+ ...r,
1272
+ skipped: [...t, ...r.skipped],
1273
+ warnings: [
1274
+ ...r.warnings || [],
1275
+ ...t.length ? ["部分字段因置信度或风险策略被跳过。"] : []
1276
+ ]
1277
+ };
1278
+ return this.events.emit("applied", a), (d = this.panel) == null || d.setApplyResult(a), a;
1279
+ }
1280
+ /** 销毁实例:移除面板、清空事件、从 InstanceManager 注销 */
1281
+ destroy() {
1282
+ if (!this.destroyed) {
1283
+ this.destroyed = !0;
1284
+ for (const e of this.panels) e.destroy();
1285
+ this.panels.length = 0, this.panelStorageKeys.clear(), this.panel = null, this.events.clear(), this.context.manager.remove(this);
1286
+ }
1287
+ }
1288
+ assertAlive() {
1289
+ if (this.destroyed)
1290
+ throw y("INSTANCE_DESTROYED", "实例已销毁。", "ui");
1291
+ }
1292
+ emitError(e, t) {
1293
+ var i;
1294
+ const s = ie(e, t);
1295
+ this.events.emit("error", s), (i = this.panel) == null || i.setError(s.message);
1296
+ }
1297
+ }
1298
+ function Ye(n, e, t, s, i) {
1299
+ return {
1300
+ scanToken: n,
1301
+ traceId: e,
1302
+ items: t.map((r) => {
1303
+ const a = s.fields.find((l) => l.fieldId === r.fieldId), o = i.find((l) => l.fieldId === r.fieldId), d = o != null && o.getValue ? o.getValue() : Xe(a == null ? void 0 : a.element);
1304
+ return {
1305
+ applyItemId: `${r.fieldId}_${Math.random().toString(36).slice(2, 8)}`,
1306
+ fieldId: r.fieldId,
1307
+ scanToken: n,
1308
+ groupId: r.groupId,
1309
+ label: r.label,
1310
+ value: r.value,
1311
+ currentValue: d,
1312
+ displayValue: r.displayValue || String(r.value ?? ""),
1313
+ confidence: r.confidence,
1314
+ source: r.source,
1315
+ warnings: r.warnings,
1316
+ previousValue: d
1317
+ };
1318
+ })
1319
+ };
1320
+ }
1321
+ function Q(n) {
1322
+ var e;
1323
+ return n.confidence >= Ue && !((e = n.warnings) != null && e.length);
1324
+ }
1325
+ function je(n) {
1326
+ return n.items.filter((e) => !Q(e)).map((e) => {
1327
+ var t, s;
1328
+ return {
1329
+ fieldId: e.fieldId,
1330
+ label: e.label,
1331
+ attemptedValue: e.value,
1332
+ reason: ((t = e.warnings) == null ? void 0 : t.join(";")) || `置信度 ${Math.round(e.confidence * 100)}% 低于自动回填阈值`,
1333
+ reasonCode: (s = e.warnings) != null && s.length ? "AUTO_APPLY_WARNING" : "LOW_CONFIDENCE"
1334
+ };
1335
+ });
1336
+ }
1337
+ function Ze(n, e) {
1338
+ const t = /* @__PURE__ */ new Map();
1339
+ for (const s of e)
1340
+ t.set(s.fieldId, s);
1341
+ for (const s of n)
1342
+ t.has(s.fieldId) || t.set(s.fieldId, s);
1343
+ return [...t.values()];
1344
+ }
1345
+ function Ge(n, e) {
1346
+ return n === "only" ? !0 : n === "off" ? !1 : e;
1347
+ }
1348
+ function K(n, e, t, s, i, r = !1) {
1349
+ return {
1350
+ scanToken: n,
1351
+ suggestions: t,
1352
+ warnings: i,
1353
+ trace: {
1354
+ traceId: e,
1355
+ usedOcr: r,
1356
+ usedAi: !1,
1357
+ durationMs: Math.round(performance.now() - s)
1358
+ }
1359
+ };
1360
+ }
1361
+ function Xe(n) {
1362
+ if (n)
1363
+ return n instanceof HTMLInputElement && n.type === "checkbox" ? n.checked : n instanceof HTMLInputElement || n instanceof HTMLTextAreaElement || n instanceof HTMLSelectElement ? n.value : n.textContent;
1364
+ }
1365
+ function H(n, e = "default") {
1366
+ const t = typeof location < "u" ? location.pathname : "unknown-page", s = e.replace(/[^\w-]/g, "_") || "default";
1367
+ return `smart-fill:${n}:${t}:${s}:open`;
1368
+ }
1369
+ function We(n, e) {
1370
+ if (typeof n == "string") return n;
1371
+ if (e.id) return `#${e.id}`;
1372
+ const t = typeof e.className == "string" ? e.className.trim().split(/\s+/).filter(Boolean)[0] : "";
1373
+ return t ? `.${t}` : e.tagName.toLowerCase();
1374
+ }
1375
+ function V(n, e) {
1376
+ try {
1377
+ const t = window.localStorage.getItem(n);
1378
+ return t == null ? e : t === "1";
1379
+ } catch {
1380
+ return e;
1381
+ }
1382
+ }
1383
+ function q(n, e) {
1384
+ if (n)
1385
+ try {
1386
+ window.localStorage.setItem(n, e ? "1" : "0");
1387
+ } catch {
1388
+ }
1389
+ }
1390
+ function ee() {
1391
+ return `smart-fill:${typeof location < "u" ? location.pathname : "unknown-page"}:local-priority`;
1392
+ }
1393
+ function Je() {
1394
+ try {
1395
+ return window.localStorage.getItem(ee()) === "1";
1396
+ } catch {
1397
+ return !1;
1398
+ }
1399
+ }
1400
+ function Qe(n) {
1401
+ try {
1402
+ window.localStorage.setItem(ee(), n ? "1" : "0");
1403
+ } catch {
1404
+ }
1405
+ }
1406
+ const p = { status: "idle" }, _ = new Y(), U = new ue();
1407
+ class et {
1408
+ /** 初始化 SDK:校验 apiKey、创建会话、获取 accessToken */
1409
+ static async setup(e) {
1410
+ if (typeof window > "u")
1411
+ return p.status = "ready", tt();
1412
+ if (p.status === "ready" && p.apiKey === e.apiKey && p.session)
1413
+ return p.session;
1414
+ if (p.status === "loading" && p.apiKey === e.apiKey && p.promise)
1415
+ return p.promise;
1416
+ p.apiKey && p.apiKey !== e.apiKey && U.destroyAll();
1417
+ const t = new re(e);
1418
+ return p.status = "loading", p.apiKey = e.apiKey, p.client = t, p.promise = t.createSession().then((s) => (console.log("SmartFill session created:", s), t.setAccessToken(s.apiKey), p.status = "ready", p.session = s, _.emit("ready", { apiKey: e.apiKey }), s)).catch((s) => {
1419
+ throw p.status = "error", s;
1420
+ }), p.promise;
1421
+ }
1422
+ /** 创建页面实例,必须在 setup ready 后调用 */
1423
+ static create(e = {}) {
1424
+ if (typeof window > "u")
1425
+ return S();
1426
+ if (p.status !== "ready" || !p.client)
1427
+ throw y("SDK_NOT_READY", "请先 await SmartFill.setup({ apiKey })。", "setup");
1428
+ return new Be(e, {
1429
+ client: p.client,
1430
+ manager: U
1431
+ });
1432
+ }
1433
+ }
1434
+ /** 订阅全局事件(目前仅 ready) */
1435
+ c(et, "on", _.on.bind(_));
1436
+ function tt() {
1437
+ return {
1438
+ apiKey: "server-mock",
1439
+ accessToken: "server-mock",
1440
+ expiresIn: 0,
1441
+ refreshBefore: 0,
1442
+ features: { text: !1, image: !1, ai: !1, localRuleOnly: !0 },
1443
+ rulesVersion: "server"
1444
+ };
1445
+ }
1446
+ function S(n) {
1447
+ const e = () => {
1448
+ };
1449
+ return {
1450
+ on: e,
1451
+ useAdapter: () => S(),
1452
+ registerFields: () => S(),
1453
+ unregisterFields: e,
1454
+ mount: () => S(),
1455
+ mountFloatingButton: () => S(),
1456
+ open: async () => {
1457
+ },
1458
+ close: e,
1459
+ rescan: async () => ({ scanToken: "server", fields: [] }),
1460
+ recognize: async () => ({
1461
+ scanToken: "server",
1462
+ suggestions: [],
1463
+ trace: { traceId: "server", usedOcr: !1, usedAi: !1, durationMs: 0 }
1464
+ }),
1465
+ apply: async () => ({ applied: [], skipped: [] }),
1466
+ destroy: e
1467
+ };
1468
+ }
1469
+ const st = {
1470
+ name: "native",
1471
+ /** 匹配 input / textarea / select 原生控件 */
1472
+ match: (n) => n instanceof HTMLInputElement || n instanceof HTMLTextAreaElement || n instanceof HTMLSelectElement,
1473
+ getValue: (n) => {
1474
+ const e = n.element;
1475
+ if (e instanceof HTMLInputElement && e.type === "checkbox") return e.checked;
1476
+ if (e instanceof HTMLInputElement || e instanceof HTMLTextAreaElement || e instanceof HTMLSelectElement) return e.value;
1477
+ },
1478
+ setValue: (n, e) => {
1479
+ const t = n.element;
1480
+ t instanceof HTMLInputElement && t.type === "checkbox" ? t.checked = !!e : (t instanceof HTMLInputElement || t instanceof HTMLTextAreaElement || t instanceof HTMLSelectElement) && (t.value = String(e ?? "")), t == null || t.dispatchEvent(new Event("input", { bubbles: !0 })), t == null || t.dispatchEvent(new Event("change", { bubbles: !0 }));
1481
+ }
1482
+ };
1483
+ export {
1484
+ se as DEFAULT_BASE_URL,
1485
+ D as DomFiller,
1486
+ Me as DomScanner,
1487
+ Y as EventBus,
1488
+ me as LocalRuleEngine,
1489
+ st as NativeAdapter,
1490
+ et as SmartFill,
1491
+ Be as SmartFillInstance
1492
+ };