dsh-form-fill-agent 0.2.20 → 0.2.22

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.md CHANGED
@@ -1,11 +1,14 @@
1
1
  # AI填表智能体
2
2
 
3
+ > 统一侧栏适配需要 Better Sidebar `>=0.17.1 <0.19.0`。五个流程入口打开同一 Session Tab;侧栏展开、收起和关闭由宿主管理。缺少依赖时会显示安装升级提示。
4
+
3
5
  ## 安装与三分钟上手
4
6
 
5
7
  AI填表智能体:支持自动填表、表格填充、表格补全、Excel填表与 Excel回填;使用企查查 MCP 填写 XLSX 空白字段,预览确认后导出新副本。
6
8
 
7
9
  ```sh
8
- dsh plugin --profile web add dsh-form-fill-agent@0.2.20
10
+ dsh plugin --profile web add dsh-better-sidebar@0.17.1
11
+ dsh plugin --profile web add dsh-form-fill-agent@0.2.22
9
12
  ```
10
13
 
11
14
  请先满足下文的 DSH、连接器及侧边栏依赖要求;安装后完整停止并重启对应 Profile。
@@ -64,7 +67,7 @@ dsh plugin --profile web add dsh-form-fill-agent@latest
64
67
 
65
68
  ## 使用边界与数据保存
66
69
 
67
- 补充要求为模型可读文本;字段或工作表范围请通过向导/工作台修改,不能仅改文字后假定执行范围已同步。任务 schema 5 可读取 schema 1–5;回退旧版本请使用升级前目录副本或独立任务目录。主包为 0.2.20,共享内核为 0.2.10,Provider 为 0.2.2,共享字段契约为 0.1.0。
70
+ 补充要求为模型可读文本;字段或工作表范围请通过向导/工作台修改,不能仅改文字后假定执行范围已同步。任务 schema 5 可读取 schema 1–5;回退旧版本请使用升级前目录副本或独立任务目录。主包为 0.2.22,共享内核为 0.2.10,Provider 为 0.2.2,共享字段契约为 0.1.0。
68
71
 
69
72
  支持企业完整登记名称或 18 位信用代码;简称、主体不一致或多候选不会自动猜选。仅有信用代码的表格也可作为查询锚点。连接候选检索工具时可检索简称,仍需人工确认;缺少工具时降级人工输入。字段映射采用确定性规则及人工确认,不让模型编造事实。支持普通内部公式、固定区域下拉、基础条件格式和普通表格对象;复杂 Excel 结构和 Word 暂不支持,保真边界见下方安全说明。
70
73
 
@@ -72,7 +75,7 @@ dsh plugin --profile web add dsh-form-fill-agent@latest
72
75
 
73
76
  显式设置 DSH_HOME 时,任务保存至其 form-fill-tasks 目录,默认 24 小时过期;未设置时使用内存。任务保存输入表格和归一化预览,不持久化原始 MCP 响应,可主动删除任务。模型及 MCP 凭据由宿主管理。
74
77
 
75
- 0.2.20 保存任务 schema 5,兼容读取旧 schema 1–4;回退旧版本须使用升级前任务目录副本或新目录,防止丢失已确认的字段/主体设置。
78
+ 0.2.22 保存任务 schema 5,兼容读取旧 schema 1–4;回退旧版本须使用升级前任务目录副本或新目录,防止丢失已确认的字段/主体设置。
76
79
 
77
80
  ## 文档与反馈
78
81
 
package/lib/brand.css CHANGED
@@ -7,23 +7,16 @@
7
7
  .ff-ui.ff-modal-host{background:transparent;padding:0;overflow:hidden}.ff-ui.ff-modal-host> :not(dialog):not(script):not(link){visibility:hidden}html:has(>body.ff-ui.ff-modal-host){background:transparent;color-scheme:normal}.ff-ui #wizard-message{color:var(--error);padding:0 20px;margin:4px 0;flex-shrink:0}
8
8
  [data-composer-card]:has(.ff-ui .ff-prompt){padding-top:48px}.ff-ui .ff-prompt{margin:6px 10px}.ff-ui dialog>.tabs{padding:8px 20px;flex-shrink:0}
9
9
  .ff-ui .ff-launcher{display:flex;align-items:center;gap:8px;color:var(--text);padding:10px 8px;text-decoration:none;border-radius:6px;width:100%}.ff-ui .ff-launcher:hover,.ff-ui .ff-launcher[aria-current=page]{background:var(--soft);color:var(--action)}.ff-ui .ff-launcher .ff-icon{color:var(--brand)}.ff-ui.ff-top{width:100%;min-width:0;padding:4px var(--dsh-sidebar-inline-padding,12px) 4px 0}.ff-ui .ff-shortcuts{display:flex;justify-content:center;gap:6px;flex-wrap:wrap;padding:12px 0}.ff-ui .ff-shortcuts button{display:flex;flex-direction:column;align-items:center;gap:6px;border-color:transparent;background:transparent;color:var(--muted);font-size:12px;min-width:72px}.ff-ui .ff-shortcuts button[aria-pressed=true]{background:var(--soft);color:var(--action)}.ff-ui .ff-prompt{color:var(--action);background:var(--soft);border-color:transparent;display:inline-flex;align-items:center;gap:6px;font-size:13px}
10
- [data-form-fill-reserve=true]{padding-right:var(--ff-panel-inset)!important}
11
- .ff-ui .ff-panel-resize{position:absolute;left:-4px;top:0;bottom:0;width:8px;z-index:2;cursor:col-resize;touch-action:none;outline:none}
12
- .ff-ui .ff-panel-resize:after{content:"";position:absolute;left:3px;top:0;bottom:0;width:3px;background:transparent}
13
- .ff-ui .ff-panel-resize:hover:after,.ff-ui .ff-panel-resize:focus-visible:after,.ff-ui.ff-resizing .ff-panel-resize:after{background:var(--action)}
14
- .ff-ui.ff-resizing{user-select:none;cursor:col-resize}.ff-ui.ff-resizing iframe{pointer-events:none}
15
- .ff-ui.ff-panel{container-type:inline-size}
16
10
  .ff-ui .ff-mapping-row{display:grid;gap:8px;padding:14px 0;border-bottom:1px solid var(--line);min-width:0}
17
11
  .ff-ui .ff-mapping-row>select{width:100%}.ff-ui .ff-mapping-status{margin:0;font-size:12px;color:var(--muted)}
18
12
  .ff-ui .ff-mapping-status[data-unmatched=true]{color:var(--review)}
19
13
  .ff-ui .ff-mapping-filters{display:flex;gap:8px;flex-wrap:wrap}.ff-ui .ff-mapping-filters input[type=search]{flex:1 1 180px;min-width:0}.ff-ui .ff-mapping-filters select{flex:1 1 150px;max-width:100%}
20
14
  .ff-ui .ff-mapping-suggestions{display:flex;gap:6px;align-items:center;flex-wrap:wrap;font-size:12px}.ff-ui .ff-mapping-suggestions:empty{display:none}
21
- @container(max-width:480px){.ff-ui .ff-panel-head{flex-wrap:wrap;padding:10px}.ff-ui .ff-panel-actions{margin-left:auto}.ff-ui .ff-panel-head h2{font-size:14px}}
22
- .ff-ui.ff-panel{position:fixed;right:0;top:0;bottom:0;z-index:1000;background:var(--surface);display:flex;flex-direction:column;border-left:1px solid var(--line)}.ff-ui .ff-panel-head{display:flex;align-items:center;justify-content:space-between;gap:8px;padding:12px 16px;border-bottom:1px solid var(--line);flex-shrink:0}.ff-ui .ff-panel-head .ff-mark{width:32px;height:32px}.ff-ui .ff-panel-head .ff-mark svg{width:20px;height:20px}.ff-ui .ff-panel-head h2{font-size:16px}.ff-ui .ff-panel-actions{display:flex;gap:6px}.ff-ui.ff-panel iframe{border:0;width:100%;flex:1;min-height:0}
15
+
23
16
  .ff-ui.ff-workbench{margin:0;padding:20px;background:var(--surface);max-width:none}.ff-ui h1{font-size:24px}.ff-ui h2{font-size:18px}.ff-ui p{line-height:1.7}.ff-ui .muted{color:var(--muted);font-size:12px}.ff-ui .toolbar,.ff-ui .controls{display:flex;align-items:center;gap:10px;flex-wrap:wrap}.ff-ui .toolbar{justify-content:space-between}.ff-ui .tabs{display:flex;gap:10px;overflow:auto;border-bottom:1px solid var(--line);padding:8px 0}.ff-ui .tabs button{white-space:nowrap;background:transparent;border:0;border-bottom:2px solid transparent;border-radius:0;padding:8px 0;font-size:12px;color:var(--muted)}.ff-ui .tabs button[aria-current=true]{border-bottom-color:var(--action);color:var(--action)}.ff-ui .task-tabs{display:flex;gap:20px;border-bottom:1px solid var(--line)}.ff-ui .task-tabs button{border:0;border-bottom:3px solid transparent;border-radius:0;background:transparent;padding:10px 0}.ff-ui .task-tabs button[aria-pressed=true]{color:var(--action);border-bottom-color:var(--action)}.ff-ui .card{padding:16px 0;margin:0;border-bottom:1px solid var(--line)}.ff-ui #status{min-height:26px;color:var(--muted);font-size:13px}.ff-ui #status[data-error=true]{color:var(--error)}.ff-ui .badge{display:inline-block;padding:3px 8px;border-radius:4px;background:var(--soft);color:var(--action);font-size:12px}.ff-ui .badge[data-state=completed]{color:var(--success);background:var(--success-bg)}.ff-ui .badge[data-state=partial],.ff-ui .badge[data-state=interrupted]{color:var(--review);background:var(--review-bg)}.ff-ui .badge[data-state=failed]{color:var(--error)}
24
17
  .ff-ui .scroll{overflow:auto;max-height:520px;border:1px solid var(--line);border-radius:6px}.ff-ui .grid-changed{background:var(--soft);color:var(--action);font-weight:600}.ff-ui table{border-collapse:separate;border-spacing:0;width:100%;font-size:13px}.ff-ui th,.ff-ui td{padding:12px;white-space:nowrap;text-align:left;vertical-align:top;border-bottom:1px solid var(--line);border-right:1px solid var(--line);min-width:100px}.ff-ui th{position:sticky;top:0;background:var(--head);font-weight:500;z-index:1}.ff-ui tr:hover td{background:var(--head)}.ff-ui td:last-child,.ff-ui th:last-child{border-right:0}.ff-ui #downloads{display:flex;gap:10px;flex-wrap:wrap;margin-top:16px}.ff-ui fieldset{border:1px solid var(--line);border-radius:8px;padding:14px;margin:12px 0;min-width:0}.ff-ui legend{font-weight:600}.ff-ui label{overflow-wrap:anywhere}.ff-ui .history-item{padding:12px 0;border-bottom:1px solid var(--line)}.ff-ui .ff-field-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:8px;background:var(--page);padding:12px;border-radius:6px}.ff-ui .ff-field-grid label{display:flex;align-items:center;gap:8px;min-height:36px}.ff-ui .ff-wizard-step{display:none}.ff-ui .ff-wizard-step[aria-current=true]{display:block}
25
18
  .ff-ui dialog,.ff-ui.ff-confirm-dialog{border:1px solid var(--line);background:var(--surface);color:var(--text);border-radius:12px;padding:0;width:min(860px,calc(100vw - 32px));max-height:calc(100dvh - 32px);box-shadow:0 16px 60px #10243d24}.ff-ui dialog::backdrop,.ff-ui.ff-confirm-dialog::backdrop{background:#14253870}.ff-ui dialog[open]{display:flex;flex-direction:column}.ff-ui .dialog-head{padding:18px 20px;border-bottom:1px solid var(--line);flex-shrink:0;display:flex;align-items:center;justify-content:space-between;gap:12px}.ff-ui .dialog-body{padding:20px;overflow:auto;min-height:0;flex:1}.ff-ui .dialog-footer{padding:14px 20px;border-top:1px solid var(--line);flex-shrink:0;display:flex;gap:10px;align-items:center;justify-content:flex-end}.ff-ui .dialog-body textarea{width:100%}.ff-ui .ff-tech{overflow-wrap:anywhere;white-space:pre-wrap;font-size:12px}.ff-ui .ff-summary{padding:12px;background:var(--page);white-space:pre-line}.ff-ui .danger{color:var(--error)}
26
- @media(max-width:640px){.ff-ui.ff-workbench{padding:14px}.ff-ui input:not([type=checkbox]),.ff-ui select,.ff-ui textarea{font-size:16px}.ff-ui button{min-height:44px}.ff-ui.ff-hero h1{font-size:22px}.ff-ui.ff-hero .ff-mark{width:40px;height:40px}.ff-ui .ff-shortcuts{gap:2px}.ff-ui .ff-shortcuts button{min-width:0;flex:1;padding:8px 2px;font-size:11px}.ff-ui .ff-field-grid{grid-template-columns:1fr}.ff-ui .dialog-body{padding:14px}.ff-ui .dialog-footer{padding:12px;flex-wrap:wrap}.ff-ui .ff-panel-head{padding:10px}.ff-ui .ff-panel-head h2{font-size:14px}}
19
+ @media(max-width:640px){.ff-ui.ff-workbench{padding:14px}.ff-ui input:not([type=checkbox]),.ff-ui select,.ff-ui textarea{font-size:16px}.ff-ui button{min-height:44px}.ff-ui.ff-hero h1{font-size:22px}.ff-ui.ff-hero .ff-mark{width:40px;height:40px}.ff-ui .ff-shortcuts{gap:2px}.ff-ui .ff-shortcuts button{min-width:0;flex:1;padding:8px 2px;font-size:11px}.ff-ui .ff-field-grid{grid-template-columns:1fr}.ff-ui .dialog-body{padding:14px}.ff-ui .dialog-footer{padding:12px;flex-wrap:wrap}}
27
20
  .ff-ui .ff-stages{display:grid;grid-template-columns:repeat(5,minmax(0,1fr));min-width:0;width:100%;padding:0;border-bottom:1px solid var(--line);background:var(--surface)}
28
21
  .ff-ui .ff-stages button{position:relative;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:7px;min-width:0;min-height:78px;padding:9px 5px;border:0;border-right:1px solid var(--line);border-radius:0;background:transparent;color:var(--muted)}
29
22
  .ff-ui .ff-stages button:last-child{border-right:0}.ff-ui .ff-stages button:hover{background:var(--page);color:var(--text)}
@@ -37,13 +30,12 @@
37
30
  .ff-ui .ff-shortcuts button:hover:not(:disabled),.ff-ui .ff-shortcuts button:focus-visible{background:var(--soft);border-color:var(--line);color:var(--action)}
38
31
  [data-form-fill-mount]{width:100%;min-width:0;max-width:100%}[data-form-fill-mount]>.ff-ui{min-width:0;max-width:100%}
39
32
  .ff-ui.ff-workbench{padding:0;background:var(--page);min-width:0}.ff-ui .ff-workbench-head{padding:20px;background:var(--surface);border-bottom:1px solid var(--line)}.ff-ui .ff-workbench-head h1{font-size:18px}.ff-ui .ff-workbench-head p{margin:8px 0 0}.ff-ui.ff-embedded .ff-workbench-head{display:none}
40
- .ff-ui .ff-panel-head{padding:16px;background:var(--surface)}.ff-ui .ff-panel-actions{flex-shrink:0}.ff-ui .ff-panel-actions button{font-size:12px;padding:6px 8px}.ff-ui .ff-panel-head h2{font-size:16px;white-space:nowrap}
41
33
  .ff-ui .task-tabs{padding:0 20px;background:var(--surface)}.ff-ui #status{margin:0;padding:10px 20px;background:var(--head);overflow-wrap:anywhere;min-height:0;border-bottom:1px solid var(--line)}.ff-ui #status:empty,.ff-ui #task-meta:empty{display:none}
42
34
  .ff-ui #current,.ff-ui #preview{min-width:0}.ff-ui #current:not(:has(#preview:not([hidden]))) .ff-execution-toolbar{display:none}
43
35
  .ff-ui #task-meta{margin:16px 20px 0;max-width:calc(100% - 40px);white-space:normal;overflow-wrap:anywhere}.ff-ui .ff-execution-toolbar{margin:12px 20px;justify-content:flex-start}
44
36
  .ff-ui .card{margin:16px 20px;padding:20px;border:1px solid var(--line);border-radius:8px;background:var(--surface);min-width:0}.ff-ui .card h2{margin:0 0 12px;font-size:18px}.ff-ui .card p{margin:8px 0 16px}.ff-ui #grid{margin:16px 20px;padding:16px;background:var(--surface);border:1px solid var(--line);border-radius:8px;min-width:0}.ff-ui #grid summary{cursor:pointer;font-weight:600}
45
37
  .ff-ui #history{padding:20px}.ff-ui #preview>.controls{padding:0 20px 20px}.ff-ui input[type=file]{width:100%;max-width:100%;min-width:0;font-size:12px}.ff-ui input[type=file]::file-selector-button{font:inherit;border:1px solid var(--line);border-radius:6px;padding:8px 12px;margin-right:10px;background:var(--surface);color:var(--text);cursor:pointer}.ff-ui #samples{margin-top:12px}.ff-ui .history-item{background:var(--surface);padding:16px;border:1px solid var(--line);border-radius:8px;margin-top:12px}
46
- @media(max-width:640px){.ff-ui .ff-shortcuts{justify-content:flex-start;padding-inline:12px;gap:8px}.ff-ui .ff-shortcuts button{flex:0 0 auto;min-width:92px;padding:7px 10px;font-size:12px}.ff-ui .card{margin:14px;padding:16px}.ff-ui #grid{margin:14px}.ff-ui .task-tabs{padding-inline:14px}.ff-ui #status{padding-inline:14px}.ff-ui #task-meta{margin-inline:14px;max-width:calc(100% - 28px)}.ff-ui .ff-execution-toolbar{margin-inline:14px}.ff-ui .ff-panel-head{padding:12px}.ff-ui .ff-panel-head .ff-brand{gap:8px}.ff-ui .ff-panel-head h2{font-size:14px}.ff-ui .ff-panel-actions{gap:4px}.ff-ui .ff-panel-actions button{font-size:11px;padding:6px}.ff-ui .ff-workbench-head{padding:16px}}
38
+ @media(max-width:640px){.ff-ui .ff-shortcuts{justify-content:flex-start;padding-inline:12px;gap:8px}.ff-ui .ff-shortcuts button{flex:0 0 auto;min-width:92px;padding:7px 10px;font-size:12px}.ff-ui .card{margin:14px;padding:16px}.ff-ui #grid{margin:14px}.ff-ui .task-tabs{padding-inline:14px}.ff-ui #status{padding-inline:14px}.ff-ui #task-meta{margin-inline:14px;max-width:calc(100% - 28px)}.ff-ui .ff-execution-toolbar{margin-inline:14px}.ff-ui .ff-workbench-head{padding:16px}}
47
39
  .ff-ui .ff-field-picker{position:relative;width:100%;min-width:0}.ff-ui .ff-field-picker>summary{padding:10px 12px;border:1px solid var(--line);border-radius:8px;background:var(--surface);cursor:pointer;overflow-wrap:anywhere}.ff-ui .ff-field-picker[open]>summary{border-color:var(--blue)}.ff-ui .ff-field-panel{padding:12px;border:1px solid var(--line);border-radius:8px;background:var(--surface);margin-top:4px;min-width:0}.ff-ui .ff-field-panel select[data-column]{width:100%;height:auto;max-width:100%}.ff-ui .ff-field-panel optgroup{font-weight:600}.ff-ui .ff-field-panel option{padding:7px;font-weight:400}
48
40
  .ff-ui #wizard .dialog-head{align-items:flex-start;border-bottom:0;padding:24px 24px 12px}.ff-ui #wizard .dialog-head h2{margin:0;font-size:22px}.ff-ui #wizard .dialog-head p{margin:8px 0 0;line-height:1.6}.ff-ui #wizard-close{border-radius:50%;width:44px;height:44px;min-width:44px;padding:0;font-size:26px;line-height:1}.ff-ui .ff-wizard-nav{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:8px;padding:8px 24px 12px;flex-shrink:0}.ff-ui .ff-wizard-nav button{display:flex;align-items:center;gap:8px;min-height:52px;padding:10px;font-size:14px;border:1px solid var(--line);border-radius:8px;background:var(--surface);color:var(--text);text-align:left}.ff-ui .ff-step-number{display:inline-flex;align-items:center;justify-content:center;flex:0 0 28px;height:28px;border-radius:50%;background:var(--line);color:var(--muted);font-weight:600}.ff-ui .ff-wizard-nav button[aria-current=true]{border-color:var(--action);background:var(--soft);color:var(--action)}.ff-ui .ff-wizard-nav button[aria-current=true] .ff-step-number{background:var(--action);color:white}.ff-ui #wizard .dialog-body{padding:16px 24px 24px}.ff-ui #wizard h3{font-size:18px;margin:0 0 12px}.ff-ui #wizard p{line-height:1.65}.ff-ui .ff-upload-zone{padding:18px;background:var(--page);border:1px dashed var(--line);border-radius:8px}.ff-ui .ff-upload-zone label{display:grid;gap:12px;font-weight:600}.ff-ui .ff-service-note{padding:12px 16px;border-radius:8px;background:var(--page);color:var(--muted)}.ff-ui #wizard .dialog-footer{padding:16px 24px}.ff-ui #wizard .dialog-footer button{min-height:44px}
49
41
  @media(max-width:640px){.ff-ui .ff-wizard-nav{grid-template-columns:repeat(2,minmax(0,1fr));padding:8px 14px;gap:6px}.ff-ui .ff-wizard-nav button{min-height:44px;padding:6px 8px}.ff-ui #wizard .dialog-head{padding:16px 14px 8px}.ff-ui #wizard .dialog-head h2{font-size:20px}.ff-ui #wizard .dialog-head p{font-size:13px}.ff-ui #wizard .dialog-body{padding:14px}.ff-ui #wizard .dialog-footer{padding:12px 14px}}
@@ -60,3 +52,22 @@
60
52
  .ff-ui #wizard-mapping>.card>h2,.ff-ui #wizard-mapping #mapping-next{display:none}
61
53
  .ff-ui #wizard-mapping fieldset{min-width:0}
62
54
  .ff-ui #wizard-mapping .ff-mapping-row{min-width:0}
55
+ /* Progress measures processed work, independently from result quality. */
56
+ #progress-panel { padding: 12px 16px; }
57
+ #task-progress { width: 100%; height: 12px; accent-color: #087bd8; }
58
+ #task-progress[data-ended="true"] { accent-color: #138263; }
59
+ #task-progress::-webkit-progress-bar { background: #e3e9ef; border-radius: 6px; }
60
+ #task-progress::-webkit-progress-value { background: #087bd8; border-radius: 6px; }
61
+ #task-progress[data-ended="true"]::-webkit-progress-value { background: #138263; }
62
+ #progress-label { color: var(--action); font-weight: 600; }
63
+ #progress-label[data-ended="true"] { color: var(--success); }
64
+ .ff-review-count { color: var(--review); font-weight: 600; }
65
+ .ff-failure-count { color: var(--error); font-weight: 600; }
66
+ #wizard-grid { min-width: 0; margin-bottom: 16px; }
67
+ #task-meta, #task-details { overflow-wrap: anywhere; }
68
+
69
+ .ff-ui.ff-tab-content{height:100%;min-height:0;min-width:0;width:100%;display:flex;flex-direction:column;background:var(--surface);container-type:inline-size}
70
+ .ff-ui .ff-tab-head{flex-shrink:0;padding:12px 16px;border-bottom:1px solid var(--line)}
71
+ .ff-ui .ff-tab-head h2{font-size:16px;margin:0}
72
+ .ff-ui.ff-tab-content iframe{width:100%;border:0;flex:1;min-height:0}
73
+ .ff-ui .ff-sidebar-help{color:var(--error);padding:8px 12px;overflow-wrap:anywhere}
package/lib/client.js CHANGED
@@ -1,13 +1,40 @@
1
1
  window.__ModuleLoader__.load({
2
2
  id: 'dsh-form-fill-agent',
3
3
  factory(require) {
4
+ const SIDEBAR_TAB='dsh-form-fill-agent:workbench';
5
+ const SIDEBAR_HELP='工作台需要 Better Sidebar >=0.17.1 <0.19.0。请在当前 DSH profile 安装或升级 dsh-better-sidebar,重启后重试;原生对话仍可使用。';
6
+ // The only adapter allowed to reveal host panels. No geometry or business writes.
7
+ function createSidebarAdapter(service,component,icon){
8
+ const version=/^0\.(17|18)\.(\d+)$/.exec(service?.version||'');
9
+ if(!version||version[1]==='17'&&Number(version[2])<1||!Array.isArray(service.features)||!['targetedOpen','stateSubscription'].every(f=>service.features.includes(f))||!['registerTab','isTabEnabled','openTab','closeTab','getSnapshot','subscribeState'].every(k=>typeof service[k]==='function'))throw Error(SIDEBAR_HELP);
10
+ const targets=new Map(),pending=new Set(),opened=new Map();let disposed=false;
11
+ const contains=(node,id)=>node?.kind==='leaf'?node.tabs.some(t=>t.id===id):node?.children?.some(c=>contains(c,id));
12
+ const flush=()=>{
13
+ if(disposed)return;const id=service.getSnapshot().sessionId,target=targets.get(id);
14
+ if(!target||!pending.has(id))return;
15
+ pending.delete(id);
16
+ target.store.reduce(state=>{
17
+ if(state.floats?.some(f=>f.tab.id===target.tabId))return state;
18
+ if(contains(state.bottomSplits,target.tabId))return state.bottomOpen?state:{...state,bottomOpen:true};
19
+ if(contains(state.splits,target.tabId))return state.panelOpen?state:{...state,panelOpen:true};
20
+ return state;
21
+ });
22
+ };
23
+ const unregister=service.registerTab({id:SIDEBAR_TAB,title:'AI填表',icon,order:20,single:true,hidden:true,component});
24
+ const unsubscribe=service.subscribeState(flush);
25
+ return {
26
+ open(scope){if(disposed)throw Error(SIDEBAR_HELP);if(!service.isTabEnabled(SIDEBAR_TAB))throw Error('AI填表 Tab 已禁用,请在 Better Sidebar 设置中启用。');opened.set(scope.sessionId,scope);pending.add(scope.sessionId);service.openTab({type:SIDEBAR_TAB},scope);flush();},
27
+ attach(scope,store,tab){if(disposed)return()=>{};if(typeof store?.reduce!=='function')throw Error(SIDEBAR_HELP);const target={store,tabId:tab.id};targets.set(scope.sessionId,target);flush();return()=>{if(targets.get(scope.sessionId)===target)targets.delete(scope.sessionId)};},
28
+ dispose(){if(disposed)return;disposed=true;unsubscribe();pending.clear();targets.clear();for(const scope of opened.values())service.closeTab(SIDEBAR_TAB,scope);opened.clear();unregister();}
29
+ };
30
+ }
4
31
  function apply(ctx) {
5
32
  let React, portal;
6
33
  try { React=require('react');portal=require('react-dom').createPortal; } catch {}
7
34
  if(typeof React?.createElement!=='function'||!ctx.slots?.inject||!ctx.slots?.register)return;
8
35
  const h=React.createElement,prefix='session-dsh-form-fill-agent-';
9
36
  const owned=id=>typeof id==='string'&&id.startsWith(prefix);
10
- const disposers=[],sessionTasks=new Map(),lastDrafts=new Map();let panel=null,pendingStart;
37
+ const disposers=[],sessionTasks=new Map(),lastDrafts=new Map(),views=new Map(),viewListeners=new Set();let sidebar,pendingStart,activePlugin=true,sidebarError=SIDEBAR_HELP;
11
38
  const paths={table:'M3 4h18v16H3zM3 9h18M9 9v11',upload:'M12 16V3M7 8l5-5 5 5M4 14v7h16v-7',check:'M3 5h2v2H3zM9 6h12M3 11h2v2H3zM9 12h12M3 17h2v2H3zM9 18h12',search:'M17 17l5 5M19 10a8 8 0 1 1-16 0 8 8 0 0 1 16 0',history:'M3 11a9 9 0 1 1 2 7M3 4v7h7M12 7v6l4 2',spark:'M12 3l2.5 6.5L21 12l-6.5 2.5L12 21l-2.5-6.5L3 12l6.5-2.5z'};
12
39
  const icon=(name='table')=>h('svg',{className:'ff-icon',viewBox:'0 0 24 24','aria-hidden':true},h('path',{d:paths[name]}));
13
40
  const mark=()=>h('span',{className:'ff-mark'},icon());
@@ -16,8 +43,13 @@ window.__ModuleLoader__.load({
16
43
  const current=()=>ctx.sessions?.list?.getSnapshot?.().current;
17
44
  function register(name,component){ctx.slots.inject(name,()=>ctx.slots.register({name,id:'form-fill-agent',order:120},component))}
18
45
  function openPanel(id,step='import'){
19
- if(!owned(id))return;
20
- panel?.({id,step});
46
+ if(!activePlugin||!owned(id))return;
47
+ try{
48
+ if(!sidebar)throw Error(sidebarError);
49
+ const workspace=ctx.workspaces?.list?.getSnapshot?.().items?.find(w=>w.sessionIds?.includes(id));
50
+ views.set(id,{id,step});sidebar.open({sessionId:id,...(workspace?.path?{cwd:workspace.path}:{})});
51
+ for(const listener of viewListeners)listener();
52
+ }catch(error){sidebarError=error.message;for(const listener of viewListeners)listener(sidebarError)}
21
53
  }
22
54
  async function start(){
23
55
  if(pendingStart)return pendingStart;
@@ -82,7 +114,8 @@ window.__ModuleLoader__.load({
82
114
  return mount?portal(h(React.Fragment,null,h('div',{className:'ff-brand ff-hero-brand'},mark(),h('h1',null,'AI填表智能体')),h('p',null,'上传已有表格,核对字段映射,确认后生成新副本。')),mount):null;
83
115
  }
84
116
  function Menu({sessionId}){
85
- const ref=React.useRef(null),[mount,setMount]=React.useState(null);
117
+ const ref=React.useRef(null),[mount,setMount]=React.useState(null),[error,setError]=React.useState('');
118
+ React.useEffect(()=>{const report=message=>setError(message||'');viewListeners.add(report);return()=>viewListeners.delete(report)},[]);
86
119
  React.useEffect(()=>{
87
120
  if(!owned(sessionId)||!ref.current)return;
88
121
  const marker=ref.current,seat=marker.closest('[data-composer-seat]'),card=seat?.querySelector('[data-composer-card]');
@@ -95,50 +128,31 @@ window.__ModuleLoader__.load({
95
128
  if(!owned(sessionId))return null;
96
129
  const content=h('div',{className:'ff-ui','data-ff-theme':theme()},h('nav',{className:'ff-shortcuts','aria-label':'AI填表快捷菜单'},...[
97
130
  ['导入表格','import','upload'],['字段设置','rules','check'],['主体核验','identity','search'],['填写预览','preview','table'],['任务历史','history','history']
98
- ].map(([label,step,symbol])=>h('button',{key:step,type:'button',onClick:()=>openPanel(sessionId,step)},icon(symbol),h('span',{className:'ff-shortcut-label'},label)))));
131
+ ].map(([label,step,symbol])=>h('button',{key:step,type:'button',onClick:()=>openPanel(sessionId,step)},icon(symbol),h('span',{className:'ff-shortcut-label'},label)))),error?h('p',{className:'ff-sidebar-help',role:'alert'},error):null);
99
132
  return h('div',{ref,'data-form-fill-session':sessionId},h(Hero,{sessionId}),mount?portal(content,mount):content);
100
133
  }
101
- function Panel(){
102
- const [view,setView]=React.useState(null),frame=React.useRef(null),[active,setActive]=React.useState(current()),[available,setAvailable]=React.useState(innerWidth),[expanded,setExpanded]=React.useState(false),[modal,setModal]=React.useState(false);
103
- const [preferred,setPreferred]=React.useState(null),[dragging,setDragging]=React.useState(false),drag=React.useRef(null);
104
- const wide=available>=740&&innerWidth>760,maxWidth=Math.max(320,available-420);
105
- const clamp=value=>Math.round(Math.max(320,Math.min(maxWidth,value)));
106
- const panelWidth=clamp(expanded?maxWidth:preferred??Math.min(640,available*.48));
107
- const finishDrag=(cancel=false)=>{
108
- const prior=drag.current;if(!prior)return;drag.current=null;setDragging(false);
109
- if(cancel){setPreferred(prior.preferred);setExpanded(prior.expanded)}
110
- if(prior.node.hasPointerCapture(prior.id))prior.node.releasePointerCapture(prior.id);
111
- };
112
- const resizeKeys=event=>{
113
- if(event.key==='Escape'&&drag.current){event.preventDefault();event.stopPropagation();finishDrag(true);return}
114
- if(drag.current)return;
115
- const amount=event.shiftKey?48:16,value={ArrowLeft:panelWidth+amount,ArrowRight:panelWidth-amount,Home:320,End:maxWidth}[event.key];
116
- if(value===undefined)return;event.preventDefault();setExpanded(false);setPreferred(clamp(value));
117
- };
134
+ function WorkbenchTab({scope,store,tab,visible}){
135
+ const id=scope.sessionId,frame=React.useRef(null),ready=React.useRef(false);
136
+ const [view,setView]=React.useState(()=>views.get(id)||{id,step:'import'});
137
+ const [src]=React.useState(()=>'/form-fill/?session='+encodeURIComponent(id)+(sessionTasks.has(id)?'#task='+sessionTasks.get(id):''));
138
+ React.useEffect(()=>{
139
+ const update=()=>{const next=views.get(id);if(next)setView(next)};viewListeners.add(update);
140
+ const detach=sidebar?.attach(scope,store,tab);return()=>{viewListeners.delete(update);detach?.()};
141
+ },[id,store,tab.id]);
118
142
  React.useEffect(()=>{
119
- const sync=()=>{const value=theme();document.querySelectorAll('.ff-ui').forEach(n=>n.dataset.ffTheme=value);frame.current?.contentWindow?.postMessage({type:'ff-theme',theme:value},location.origin)};
143
+ const sync=()=>{frame.current?.contentWindow?.postMessage({type:'ff-theme',theme:theme()},location.origin)};
120
144
  const observer=new MutationObserver(sync);observer.observe(document.documentElement,{attributes:true,attributeFilter:['class','style','data-ds-dark-theme']});const media=matchMedia('(prefers-color-scheme:dark)');media.addEventListener('change',sync);sync();
121
145
  return()=>{observer.disconnect();media.removeEventListener('change',sync)};
122
146
  },[]);
123
- React.useEffect(()=>{
124
- const measure=()=>{const container=document.querySelector('[data-composer-card]')?.closest('[data-phase]')||document.querySelector('[data-slot="conversation"]');setAvailable(Math.max(0,innerWidth-Math.max(0,container?.getBoundingClientRect().left||0)))};
125
- const resize=()=>{finishDrag(true);measure()},blur=()=>finishDrag(true);
126
- const escape=event=>{if(event.key==='Escape'&&drag.current){event.preventDefault();event.stopImmediatePropagation();finishDrag(true)}};
127
- const observer=new ResizeObserver(measure);observer.observe(document.body);
128
- const container=document.querySelector('[data-composer-card]')?.closest('[data-phase]')||document.querySelector('[data-slot="conversation"]');if(container)observer.observe(container);
129
- measure();window.addEventListener('resize',resize);window.addEventListener('blur',blur);window.addEventListener('keydown',escape,true);
130
- return()=>{finishDrag(true);observer.disconnect();window.removeEventListener('resize',resize);window.removeEventListener('blur',blur);window.removeEventListener('keydown',escape,true)};
131
- },[view?.id,active,modal]);
132
- React.useEffect(()=>{panel=setView;const timer=setInterval(()=>setActive(current()),200);return()=>{panel=null;clearInterval(timer)}},[]);
133
- React.useEffect(()=>{if(view&&current()!==view.id)setView(null);setModal(false)},[active,view?.id]);
134
- React.useEffect(()=>{frame.current?.contentWindow?.postMessage({type:'ff-navigate',step:view?.step},location.origin)},[view]);
147
+ React.useEffect(()=>{if(ready.current)frame.current?.contentWindow?.postMessage({type:'ff-navigate',step:view.step},location.origin)},[view]);
135
148
  React.useEffect(()=>{
136
149
  const receive=async event=>{
137
- if(event.origin!==location.origin||event.source!==frame.current?.contentWindow||current()!==view?.id)return;
150
+ if(event.origin!==location.origin||event.source!==frame.current?.contentWindow)return;
138
151
  if(event.data?.type==='ff-capabilities'){frame.current.contentWindow.postMessage({type:'ff-capabilities-result',mappingDraft:2},location.origin);return}
139
- if(event.data?.type==='ff-wizard-state'){setModal(event.data.open===true);if(!event.data.open&&event.data.focusComposer)setTimeout(()=>document.querySelector('[data-composer-card] textarea, [data-composer-card] [contenteditable=true]')?.focus(),0);return}
152
+ if(event.data?.type==='ff-view'&&ready.current&&['import','rules','identity','preview','download','history'].includes(event.data.step)){views.set(view.id,{id:view.id,step:event.data.step});return}
153
+ if(event.data?.type==='ff-wizard-state'){if(!event.data.open&&event.data.focusComposer)setTimeout(()=>{if(current()===view.id)document.querySelector('[data-composer-card] textarea, [data-composer-card] [contenteditable=true]')?.focus()},0);return}
140
154
  if(event.data?.type==='ff-task'&&/^[a-f0-9-]{36}$/.test(event.data.taskId)){sessionTasks.set(view.id,event.data.taskId);return}
141
- if(event.data?.type!=='ff-draft')return;
155
+ if(event.data?.type!=='ff-draft'||current()!==view.id)return;
142
156
  const {taskId,revision,requirements}=event.data;
143
157
  if(!/^[a-f0-9-]{36}$/.test(taskId)||!Number.isSafeInteger(revision))return;
144
158
  let ok=false;
@@ -163,38 +177,20 @@ window.__ModuleLoader__.load({
163
177
  };
164
178
  window.addEventListener('message',receive);return()=>window.removeEventListener('message',receive);
165
179
  },[view?.id]);
166
- React.useEffect(()=>{
167
- if(!view||active!==view.id||!wide||modal)return;
168
- const touched=new Map();
169
- const adjust=()=>{
170
- if(current()!==view.id)return;
171
- const marker=[...document.querySelectorAll('[data-form-fill-session]')].find(n=>n.dataset.formFillSession===view.id);
172
- const container=marker?document.querySelector('[data-composer-card]')?.closest('[data-phase]')||document.querySelector('[data-slot="conversation"]'):null;
173
- for(const node of touched.keys())if(node!==container){node.removeAttribute('data-form-fill-reserve');node.style.removeProperty('--ff-panel-inset')}
174
- if(!container)return;
175
- if(!touched.has(container))touched.set(container,container.style.getPropertyValue('--ff-panel-inset'));
176
- container.setAttribute('data-form-fill-reserve','true');
177
- container.style.setProperty('--ff-panel-inset',panelWidth+'px');
178
- };
179
- adjust();const observer=new MutationObserver(adjust);observer.observe(document.body,{childList:true,subtree:true});
180
- return()=>{observer.disconnect();for(const [node,previous]of touched){node.removeAttribute('data-form-fill-reserve');if(previous)node.style.setProperty('--ff-panel-inset',previous);else node.style.removeProperty('--ff-panel-inset')}};
181
- },[view?.id,panelWidth,active,wide,modal]);
182
- if(!view||active!==view.id)return null;
183
- return portal(h('section',{className:'ff-ui ff-panel'+(dragging?' ff-resizing':''),'data-ff-theme':theme(),'aria-label':'AI填表工作台',style:{width:modal?'100%':wide?panelWidth+'px':'100%',...(modal?{background:'transparent',border:0}:{})}},
184
- wide&&!modal?h('div',{className:'ff-panel-resize',role:'separator',tabIndex:0,'aria-label':'调整工作台宽度','aria-orientation':'vertical','aria-valuemin':320,'aria-valuemax':maxWidth,'aria-valuenow':panelWidth,'aria-valuetext':panelWidth+' 像素',title:'拖动调宽;左右键微调,Home/End 最小/最大;双击恢复默认',onKeyDown:resizeKeys,
185
- onDoubleClick:()=>{finishDrag(true);setPreferred(null);setExpanded(false)},
186
- onPointerDown:event=>{if(event.button!==0||drag.current)return;event.preventDefault();event.currentTarget.focus();drag.current={id:event.pointerId,node:event.currentTarget,x:event.clientX,width:panelWidth,preferred,expanded};event.currentTarget.setPointerCapture(event.pointerId);setDragging(true)},
187
- onPointerMove:event=>{const prior=drag.current;if(prior?.id!==event.pointerId)return;setPreferred(clamp(prior.width+prior.x-event.clientX));setExpanded(false)},
188
- onPointerUp:()=>finishDrag(),onPointerCancel:()=>finishDrag(true),onLostPointerCapture:()=>finishDrag(true)
189
- }):null,
190
- h('div',{className:'ff-panel-head',hidden:modal},h('div',{className:'ff-brand'},mark(),h('h2',null,'AI填表工作台')),h('div',{className:'ff-panel-actions'},h('button',{onClick:()=>setExpanded(v=>!v)},expanded?'收起展开':'展开工作台'),h('button',{onClick:()=>setView(null)},'关闭工作台'))),
191
- h('iframe',{ref:frame,title:'AI填表任务',src:'/form-fill/?session='+encodeURIComponent(view.id)+(sessionTasks.has(view.id)?'#task='+sessionTasks.get(view.id):''),onLoad:()=>{frame.current.contentWindow.postMessage({type:'ff-theme',theme:theme()},location.origin);frame.current.contentWindow.postMessage({type:'ff-navigate',step:view.step},location.origin)}})),document.body);
180
+
181
+ if(!owned(id))return h('p',null,'请从 AI填表入口打开对应业务会话。');
182
+ return h('section',{className:'ff-ui ff-tab-content','data-ff-theme':theme(),'aria-label':'AI填表工作台'},
183
+ h('div',{className:'ff-tab-head'},h('div',{className:'ff-brand'},mark(),h('h2',null,'AI填表工作台'))),
184
+ h('iframe',{ref:frame,title:'AI填表任务',src,onLoad:()=>{ready.current=true;frame.current.contentWindow.postMessage({type:'ff-theme',theme:theme()},location.origin);frame.current.contentWindow.postMessage({type:'ff-navigate',step:(views.get(id)||view).step},location.origin)}}));
192
185
  }
186
+ const connect=scope=>{
187
+ try{const adapter=createSidebarAdapter(scope.betterSidebar,props=>h(WorkbenchTab,{...props,key:props.scope.sessionId}),icon());sidebar=adapter;sidebarError='';const disconnect=()=>{if(sidebar===adapter)sidebar=undefined;adapter.dispose()};scope.effect?.(()=>disconnect);disposers.push(disconnect)}catch(error){sidebarError=error.message}
188
+ };
189
+ if(ctx.inject)ctx.inject(['betterSidebar'],connect);else if(ctx.betterSidebar)connect(ctx);
193
190
  register('conversation.input.dock',Menu);
194
191
  register('conversation.input.overlay',props=>owned(props.sessionId)?h('div',{className:'ff-ui','data-ff-theme':theme()},h('button',{className:'ff-prompt',type:'button',onClick:()=>openPanel(props.sessionId,'wizard')},icon('spark'),'提示词生成')):null);
195
- register('shell.overlay',Panel);
196
- ctx.effect?.(()=>()=>{for(const dispose of disposers)dispose()});
192
+ ctx.effect?.(()=>()=>{activePlugin=false;for(const dispose of disposers)dispose();viewListeners.clear();views.clear();sessionTasks.clear();lastDrafts.clear()});
197
193
  }
198
- return {name:'form-fill-agent',inject:['slots','sessions','workspaces','conversation'],apply};
194
+ return {createSidebarAdapter,name:'form-fill-agent',inject:['slots','sessions','workspaces','conversation'],apply};
199
195
  }
200
196
  });
package/lib/http.js CHANGED
@@ -7,6 +7,7 @@ import { createTaskStore } from './task-store.js';
7
7
  import { allCandidates, selectCandidates } from './task-model.js';
8
8
  import { gridPage } from './grid.js';
9
9
  import { diagnostics } from './diagnostics.js';
10
+ import { taskPresentation, isResultExplanation } from './task-presentation.js';
10
11
  import { mappingRecommendations, mappingSearchAliases } from './mapping-recommendations.js';
11
12
  const XLSX_TYPE = 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet';
12
13
  const FIXTURES = ['客户台账', '供应商准入表', '合同主体信息表'];
@@ -17,10 +18,12 @@ export function createFormFillHandler({ basePath = '', getPort, now = Date.now,
17
18
  const controllers=new Map();
18
19
  const validOwner = value => typeof value === 'string' && /^[a-f0-9]{64}$/.test(value);
19
20
  const visible = (task, owner) => task && (!task.owner || task.owner === owner);
20
- const metadata = (id, task) => ({ id, filename: task.filename ?? '未命名表格.xlsx', revision: task.revision ?? 1, state: task.state ?? (task.result ? 'completed' : 'preview_ready'), created: task.created, updatedAt: task.updatedAt ?? task.created, expiresAt: task.created + ttlMs, sessionId: task.sessionId, confirmed: !!task.result });
21
+ const metadata = (id, task) => ({ id, presentation:taskPresentation(task), filename: task.filename ?? '未命名表格.xlsx', revision: task.revision ?? 1, state: task.state ?? (task.result ? 'completed' : 'preview_ready'), created: task.created, updatedAt: task.updatedAt ?? task.created, expiresAt: task.created + ttlMs, sessionId: task.sessionId, confirmed: !!task.result });
21
22
  const settings = task => ({
22
23
  mappingProtocol: 2,
23
24
  diagnostics: diagnostics(task.preview),
25
+ resultExplanations: task.preview.changeSet.incomplete.filter(isResultExplanation),
26
+ exceptions: task.preview.changeSet.incomplete.filter(i=>!isResultExplanation(i)),
24
27
  configuration: task.configuration ?? {},
25
28
  selectedFields: task.selectedFields,
26
29
  progress: task.progress,
@@ -134,7 +137,7 @@ export function createFormFillHandler({ basePath = '', getPort, now = Date.now,
134
137
  }
135
138
  const preview = await previewBytes(task.bytes, { configuration, selectedFields, ...(task.analyzeOnly !== false ? { provider: { mode: 'mock', version: '0.1.0-alpha.5', capabilities: [], lookup: async () => ({ status: 'not-found' }) } } : {}) });
136
139
  if (disposed || tasks.get(body.id) !== task || running.has(body.id)) return send(409, { message: '任务已更新,请刷新后重试' });
137
- const updated = { ...task, configuration, selectedFields, preview, baseChangeSet:undefined, revision: (task.revision ?? 1)+1, updatedAt: now(), state: 'preview_ready' };
140
+ const updated = { ...task, configuration, selectedFields, preview, progress:undefined, baseChangeSet:undefined, revision: (task.revision ?? 1)+1, updatedAt: now(), state: 'preview_ready' };
138
141
  tasks.set(body.id,updated);
139
142
  return send(200, { ...preview, ...metadata(body.id,updated), ...settings(updated) });
140
143
  }
@@ -0,0 +1,31 @@
1
+ import { parseWorkbook } from 'form-fill-core';
2
+
3
+ const retained = new Set(['no-data','user-excluded','formula-preserved','hidden-or-merged','hidden-row','hidden-sheet','sheet-excluded','protected-source-range','placeholder-needs-confirmation']);
4
+ export function taskPresentation(task) {
5
+ const items=task.preview.changeSet.incomplete;
6
+ const failures=Math.max(task.state==='failed'?1:0,items.filter(i=>i.reason==='provider-error').length);
7
+ const review=items.filter(i=>i.reason!=='provider-error'&&!retained.has(i.reason)).length;
8
+ const noData=items.filter(i=>i.reason==='no-data').length;
9
+ const running=task.state==='enriching';
10
+ const ended=!running&&(!!task.progress||!!task.result||task.state==='failed');
11
+ const interrupted=['cancelled','interrupted'].includes(task.state);
12
+ const status=running?'处理中':!ended?'待确认字段':task.state==='failed'?'执行失败':review||failures||interrupted?'处理结束(需核验)':noData?'已完成(部分字段无数据)':'已完成';
13
+ const total=task.progress?.total||0,completed=task.progress?.completed||0;
14
+ // A stopped/cancelled run must not imply that all planned items ran.
15
+ const percent=ended&&!interrupted&&task.state!=='failed'?100:total?Math.min(99,Math.floor(completed/total*100)):0;
16
+ const subjects=new Set();
17
+ const document=parseWorkbook(task.bytes);
18
+ for(const table of task.preview.analysis.tables){
19
+ const sheet=document.sheets.find(s=>s.name===table.sheet);
20
+ const cells=new Map(Object.values(sheet?.cells||{}).map(c=>[c.row+':'+c.column,c]));
21
+ const inputs=table.mappings.filter(m=>table.anchorColumns?table.anchorColumns.includes(m.column):table.anchors.includes(m.field));
22
+ for(const row of sheet?.rows||[]){
23
+ if(row.hidden||row.number<=table.headerRow)continue;
24
+ const values=inputs.map(m=>{const c=cells.get(row.number+':'+m.column);return c&&!c.formula&&!c.hidden?c.value.trim():''});
25
+ if(values.some(Boolean))subjects.add(JSON.stringify(values));
26
+ }
27
+ }
28
+ const date=new Intl.DateTimeFormat('zh-CN',{timeZone:'Asia/Shanghai',month:'2-digit',day:'2-digit',hour:'2-digit',minute:'2-digit',hour12:false}).format(new Date(task.created)).replace('/', '-');
29
+ return {title:(task.filename||'未命名表格').replace(/\.xlsx$/i,'')+'|'+subjects.size+' 家企业|'+date,status,running,ended,percent,completed,total,failures,review,noData};
30
+ }
31
+ export const isResultExplanation = item => retained.has(item.reason);
package/lib/ui.html CHANGED
@@ -9,6 +9,8 @@
9
9
  <main id="current">
10
10
  <nav class="ff-stages" aria-label="工作流程"><button data-step="import"><span class="ff-stage-icon" data-icon="upload"></span><span class="ff-stage-label">导入表格</span></button><button data-step="rules"><span class="ff-stage-icon" data-icon="settings"></span><span class="ff-stage-label">字段设置</span></button><button data-step="identity"><span class="ff-stage-icon" data-icon="identity"></span><span class="ff-stage-label">主体核验</span></button><button data-step="preview"><span class="ff-stage-icon" data-icon="table"></span><span class="ff-stage-label">填写预览</span></button><button data-step="download"><span class="ff-stage-icon" data-icon="download"></span><span class="ff-stage-label">确认下载</span></button></nav>
11
11
  <p id="task-meta" class="badge"></p>
12
+ <details><summary>任务详情</summary><p id="task-details"></p></details>
13
+ <div id="progress-panel" hidden><progress id="task-progress" max="100" aria-label="处理进度"></progress><p id="progress-label" role="status"></p><span id="review-count" class="ff-review-count"></span> <span id="failure-count" class="ff-failure-count"></span><p class="muted">100% 只代表处理结束,填写内容需在预览中核验并确认生成副本。</p></div>
12
14
  <div class="toolbar ff-execution-toolbar"><span id="execution-progress" role="status"></span><button id="cancel-run" disabled>取消查询</button><button id="retry-run" disabled>仅重试失败 / 未完成查询</button></div>
13
15
  <details id="grid" hidden><summary>查看完整原表 / 结果表格</summary><p class="muted">全量行列按页查看,隐藏行列只读标注。结果视图展示当前已选填写;下载不受分页或搜索影响。</p><div class="toolbar"><label>工作表 <select id="grid-sheet"></select></label><label>视图 <select id="grid-view"><option value="original">原表</option><option value="result">结果预览</option></select></label><label>搜索全部行 <input id="grid-query" type="search" maxlength="200"></label><button id="grid-search">搜索</button></div><p id="grid-status" role="status"></p><div class="scroll"><table><thead id="grid-head"></thead><tbody id="grid-body"></tbody></table></div><div class="toolbar"><button id="grid-prev">上一页</button><button id="grid-next">下一页</button><button id="grid-left">前 16 列</button><button id="grid-right">后 16 列</button></div></details>
14
16
  <div class="card" data-pane="import"><h2>导入表格</h2><p class="muted">支持 XLSX,最大 8 MiB。支持普通内部公式、固定文本或区域下拉、基础条件格式及普通表格对象;宏、图片、图表及复杂扩展结构暂不支持。上传仅做本地分析,不查询企查查。</p>
@@ -17,10 +19,10 @@
17
19
  <section id="preview" hidden>
18
20
  <div class="card" data-pane="rules"><h2>字段与规则</h2><p id="structure"></p><p id="anchor-alert" role="status"></p><p>默认只填空白。可选择工作表、修正表头行及字段;应用设置会清除旧预览和主体确认,请重新核验。</p><label>搜索字段 <input id="mapping-search" type="search" placeholder="按原表字段名称筛选"></label><p class="muted">未识别字段可手动选择;没有数据来源的字段选择“不填写”。绿色仅表示字段映射已确认,不代表企业已核验或已查询。多个输出位置可分别确认同一字段;定位企业的输入列需明确。规则只保存表头与映射,不含记录。</p><button id="mapping-save">保存字段规则</button><button id="mapping-load">复用字段规则</button><div id="mapping"></div><button id="configure" class="primary">应用字段设置</button><div id="mapping-next" hidden role="status"><p>设置已保存。下一步核验企业主体,确认填写范围后生成指令并发送,查询结果会进入填写预览。</p><button id="mapping-continue" class="primary">下一步:主体核验</button></div></div>
19
21
  <div class="card" data-pane="identity"><h2>主体核验与查询</h2><p id="connection"></p><p>候选不自动选择。查询返回歧义后,可选择返回候选或填写已核验的完整登记名称/信用代码;原表主体文字不覆盖,选择仅用于后续查询。</p><div id="entities"></div><div id="direct-scope" role="status"></div><details id="scope-omissions" hidden><summary>查看未纳入字段</summary><p id="scope-omissions-list"></p></details><label id="direct-ack-label" hidden><input id="direct-ack" type="checkbox">确认仅填写已纳入的原表字段</label><textarea id="direct-command" aria-label="填写指令" readonly hidden></textarea><button id="wizard-open" class="primary">生成填写指令</button><p class="muted">查询不会自动发送;回填后可在原生对话框修改并发送。</p></div>
20
- <div class="card" data-pane="preview"><h2>填写预览</h2><p id="summary"></p><details><summary>逐列填写结果</summary><p class="muted">按单元格统计;整行缺少主体标识的情况另见下方原因分类。</p><div class="scroll"><table><thead><tr><th>原表字段</th><th>映射字段</th><th>预览可填写格数</th><th>未完成项</th></tr></thead><tbody id="field-results"></tbody></table></div></details><p class="muted">每格显示原值、准备填写值和来源。取消勾选后点击“应用选择”,再确认新的预览。</p>
22
+ <div class="card" data-pane="preview"><h2>填写预览</h2><p id="summary"></p><details><summary>结果说明(未披露及保留位置)</summary><p id="result-explanations"></p></details><details><summary>逐列填写结果</summary><p class="muted">按单元格统计;整行缺少主体标识的情况另见下方原因分类。</p><div class="scroll"><table><thead><tr><th>原表字段</th><th>映射字段</th><th>预览可填写格数</th><th>未完成项</th></tr></thead><tbody id="field-results"></tbody></table></div></details><p class="muted">每格显示原值、准备填写值和来源。取消勾选后点击“应用选择”,再确认新的预览。</p>
21
23
  <div class="toolbar"><label>筛选单元格 <input id="result-search" type="search" placeholder="位置、字段或值"></label><span id="result-count" class="muted"></span></div>
22
24
  <div class="scroll"><table><thead><tr><th>位置</th><th>字段</th><th>原值</th><th>准备填写</th><th>依据与来源</th><th>填写</th></tr></thead><tbody id="changes"></tbody></table></div><button id="selection">应用选择</button></div>
23
- <div class="card" data-pane="preview"><h2>未完成项</h2><p id="reason-summary"></p><div class="scroll"><table><thead><tr><th>位置</th><th>字段</th><th>原因</th></tr></thead><tbody id="incomplete"></tbody></table></div></div>
25
+ <div class="card" data-pane="preview"><h2>待核验与失败项</h2><p id="reason-summary"></p><div class="scroll"><table><thead><tr><th>位置</th><th>字段</th><th>原因</th></tr></thead><tbody id="incomplete"></tbody></table></div></div>
24
26
  <div class="card" data-pane="download"><h2>确认并下载</h2><p>原文件不会被覆盖。下载包含全部已确认填写及完整清单,不受当前筛选影响。</p><button id="confirm" class="primary">确认这些填写,生成新副本</button><div id="downloads"></div></div>
25
27
  <details class="ff-task-more"><summary>更多任务操作</summary><div class="controls"><button id="refresh" class="secondary">刷新任务</button><button id="discard" class="secondary">删除任务</button></div></details><div class="ff-next-action" data-pane="preview" hidden><span>核验填充值后继续,下一步生成副本。</span><button id="preview-continue" class="primary">下一步:确认下载</button></div>
26
28
  </section></main>
@@ -28,7 +30,7 @@
28
30
  <nav class="ff-wizard-nav" aria-label="提示词向导步骤"><button data-wizard-step="0"><span class="ff-step-number">1</span> 数据来源</button><button data-wizard-step="1"><span class="ff-step-number">2</span> 填写规则</button><button data-wizard-step="2"><span class="ff-step-number">3</span> 填写字段</button><button data-wizard-step="3"><span class="ff-step-number">4</span> 确认描述</button></nav>
29
31
  <div class="dialog-body">
30
32
  <section class="ff-wizard-step" data-wizard-pane="0"><h3>选择需要填写的原表</h3><p class="muted">识别表头与空白字段,再由企查查 MCP 查询对应的企业信息,填写到原表位置。</p><div class="ff-upload-zone"><label>上传 Excel 表格 <input id="wizard-file" type="file" accept=".xlsx"></label><p class="muted">支持 XLSX,最大 8 MiB。保留已有内容,确认结果后生成新副本。</p></div><p id="wizard-source" class="ff-summary"></p><p class="ff-service-note">完成向导后,将任务描述回填到对话框;发送后使用已连接的企查查 MCP 执行核验与填写。</p></section>
31
- <section class="ff-wizard-step" data-wizard-pane="1"><h3>确认原表字段映射</h3><p id="wizard-rules" hidden></p><div id="wizard-mapping"></div></section>
33
+ <section class="ff-wizard-step" data-wizard-pane="1"><h3>确认原表字段映射</h3><div id="wizard-grid"></div><p id="wizard-rules" hidden></p><div id="wizard-mapping"></div></section>
32
34
  <section class="ff-wizard-step" data-wizard-pane="2"><h3>确认填写范围</h3><p>已确认映射已自动纳入,无需重复选择;下方可按需减少填写范围。</p><div class="toolbar"><label>搜索 <input id="field-search" type="search" placeholder="字段名称"></label><span id="field-count"></span><button id="fields-all">选择可用字段</button><button id="fields-clear">清空可选字段</button></div><p class="muted">主体锚点固定保留;只有原表已映射的字段可选。修改映射请返回第二步。</p><div id="wizard-fields" class="ff-field-grid"></div></section>
33
35
  <section class="ff-wizard-step" data-wizard-pane="3"><h3>确认任务描述</h3><p id="business-summary" class="ff-summary"></p><p id="scope-review" class="ff-summary"></p><details><summary>查看待补空位置与保留列</summary><pre id="wizard-positions" style="white-space:pre-wrap;max-height:240px;overflow:auto"></pre></details><label id="scope-ack-label" hidden><input id="scope-ack" type="checkbox">确认本次不填写以上列</label><label>补充要求<textarea id="requirements" rows="4" maxlength="1000" placeholder="补充核验说明;字段或范围变更请返回前面步骤设置。"></textarea></label><details><summary>兼容执行信息</summary><p id="qcc-command" class="ff-tech"></p></details></section>
34
36
  </div><p id="wizard-message" role="status"></p><footer class="dialog-footer"><button id="wizard-prev">上一步</button><button id="wizard-next" class="primary">下一步</button><button id="draft" class="primary" hidden>回填到对话框</button><button id="copy-command" hidden>复制指令</button></footer></dialog>
@@ -65,7 +67,9 @@ const reasons={'unknown-field':'暂无可用字段映射','no-capability':'尚
65
67
  function controls(value){busy=value;document.querySelectorAll('button').forEach(b=>b.disabled=value);if($('wizard').open)renderWizard();syncActions();if(!value)loadGrid()}
66
68
  function row(target,values,labels){const tr=document.createElement('tr');values.forEach((v,i)=>{const td=document.createElement('td');td.textContent=v;td.dataset.label=labels[i];tr.append(td)});$(target).append(tr);return tr}
67
69
  function syncActions(){
68
- const running=current?.state==='enriching';$('cancel-run').hidden=!running;$('cancel-run').disabled=busy||!running;
70
+ const running=current?.state==='enriching';
71
+ const p=current?.presentation;$('progress-panel').hidden=!p||(!p.running&&!p.ended);if(p){$('task-progress').value=p.percent;$('task-progress').dataset.ended=String(p.ended);$('progress-label').textContent=(p.running?'处理中':p.ended?'处理结束':'待处理')+' · '+p.percent+'% · '+p.completed+' / '+p.total+' 查询计划项';$('progress-label').dataset.ended=String(p.ended);$('review-count').textContent='待核验 '+p.review+' 项';$('failure-count').textContent='失败 '+p.failures+' 项';}
72
+ $('cancel-run').hidden=!running;$('cancel-run').disabled=busy||!running;
69
73
  $('retry-run').hidden=!current||current.confirmed||!current.changeSet.incomplete.some(i=>['provider-error','cancelled','not-started'].includes(i.reason));
70
74
  $('retry-run').disabled=busy||!current||current.confirmed||running||!current.changeSet.incomplete.some(i=>['provider-error','cancelled','not-started'].includes(i.reason));
71
75
  $('execution-progress').textContent=running?(current.progress?.total?'已处理查询计划项 '+(current.progress?.completed||0)+' / '+current.progress.total+'(含失败,不代表填写成功)':'正在准备查询…'):current?.state==='cancelled'?'已取消,成功结果保留。':current?.state==='interrupted'?'上次查询中断,已保存进度,可重试剩余部分。':'';
@@ -74,7 +78,7 @@ function syncActions(){
74
78
  $('cancel-run').onclick=async()=>{try{await api('/cancel',{id:current.id});$('status').textContent='已请求取消。已发出的请求可能已在远端执行,本地不再继续后续查询。';await restore()}catch(e){$('status').textContent=e.message}};
75
79
  $('retry-run').onclick=()=>{openWizard(true);wizardStep=3;renderWizard()};
76
80
  async function api(path,body){if(current&&body?.id===current.id)body={...body,expectedRevision:current.revision};const response=await fetch(basePath+path,{...(body===undefined?{}:{method:'POST',body:JSON.stringify(body)}),headers:{'Content-Type':'application/json','X-Form-Fill-Owner':owner}});const data=await response.json();if(!response.ok)throw Error(data.message);return data}
77
- function navigate(target){if(target==='wizard'){openWizard();return}if(target!=='import'&&!current){$('status').textContent='请先导入表格。';return}step=target;$('history').hidden=true;$('current').hidden=false;$('current-tab').setAttribute('aria-pressed','true');$('history-tab').setAttribute('aria-pressed','false');document.querySelectorAll('[data-pane]').forEach(p=>p.hidden=p.dataset.pane!==target);document.querySelectorAll('[data-step]').forEach(b=>b.setAttribute('aria-current',String(b.dataset.step===target)))}
81
+ function navigate(target){if(target==='wizard'){openWizard();return}if(target!=='import'&&!current){$('status').textContent='请先导入表格。';return}step=target;if(parent!==window)parent.postMessage({type:'ff-view',step:target},location.origin);$('history').hidden=true;$('current').hidden=false;$('current-tab').setAttribute('aria-pressed','true');$('history-tab').setAttribute('aria-pressed','false');document.querySelectorAll('[data-pane]').forEach(p=>p.hidden=p.dataset.pane!==target);document.querySelectorAll('[data-step]').forEach(b=>b.setAttribute('aria-current',String(b.dataset.step===target)))}
78
82
  function downloads(){ $('downloads').replaceChildren();for(const [kind,label,suffix] of [['xlsx','下载已填副本','-已填副本.xlsx'],['changes','下载变更清单','-变更.json'],['incomplete','下载未完成项','-未完成.json']]){if(kind==='incomplete'&&!current.changeSet.incomplete.length)continue;const a=document.createElement('a');Object.assign(a,{className:'download',href:basePath+'/download/'+current.id+'/'+kind,download:filename+suffix,textContent:label});$('downloads').append(a)}}
79
83
  function businessSummary(){return current?(retryIntent?'仅重试失败、取消或未执行的部分,保留成功结果。':'')+'填写文件“'+current.filename+'”中的空白字段。共 '+current.analysis.tables.length+' 个工作表,'+wizardFields.size+' 个填写字段,'+current.analysis.opportunities.filter(p=>wizardFields.has(p.field)).length+' 个待补空位置。字段范围以本次保存的映射为准。保留已有值、公式与未确认位置,核验预览后生成新副本。':'请先上传 XLSX 文件。'}
80
84
  function command(){return current?businessSummary()+'\n'+$('requirements').value+'\n执行关联:调用 form_fill_enrich:taskId='+current.id+',expectedRevision='+current.revision+(retryIntent?',mode=retry':'')+'。字段范围以工作台确认设置为准。':''}
@@ -99,9 +103,10 @@ function scopeReview(){
99
103
  return {omitted,missing,blocked:!!problem,problem};
100
104
  }
101
105
  let wizardMappingApplied=false;
106
+ const gridHome=document.createComment('grid home');$('grid').before(gridHome);
102
107
  const sharedMapping=document.querySelector('[data-pane="rules"]'),mappingHome=document.createComment('shared mapping home');sharedMapping.before(mappingHome);
103
108
  function renderWizard(){
104
- if($('wizard').open){$('wizard-mapping').append(sharedMapping);sharedMapping.hidden=false}
109
+ if($('wizard').open){$('wizard-mapping').append(sharedMapping);sharedMapping.hidden=false;$('wizard-grid').append($('grid'));if(wizardStep===1){$('grid').open=true;loadGrid()}}
105
110
  initWizard();
106
111
  document.querySelectorAll('[data-wizard-pane]').forEach(p=>p.setAttribute('aria-current',String(Number(p.dataset.wizardPane)===wizardStep)));
107
112
  document.querySelectorAll('[data-wizard-step]').forEach(p=>p.setAttribute('aria-current',String(Number(p.dataset.wizardStep)===wizardStep)));
@@ -128,7 +133,7 @@ function renderWizard(){
128
133
  $('draft').hidden=wizardStep!==3;$('copy-command').hidden=wizardStep!==3;$('draft').disabled=!current||!wizardFields.size||busy||review.blocked;$('copy-command').disabled=$('draft').disabled;
129
134
  }
130
135
  function openWizard(retry=false){retryIntent=retry===true;initWizard();renderWizard();if(!$('wizard').open)$('wizard').showModal();renderWizard();if(parent!==window){document.body.classList.add('ff-modal-host');parent.postMessage({type:'ff-wizard-state',open:true},location.origin)}}
131
- function closeWizard(focusComposer=false){saveWizard();$('draft-conflict').close();$('wizard').close();mappingHome.after(sharedMapping);sharedMapping.hidden=step!=='rules';document.body.classList.remove('ff-modal-host');if(parent!==window)parent.postMessage({type:'ff-wizard-state',open:false,focusComposer},location.origin);if(!focusComposer)$('wizard-open').focus()}
136
+ function closeWizard(focusComposer=false){saveWizard();$('draft-conflict').close();$('wizard').close();mappingHome.after(sharedMapping);gridHome.after($('grid'));sharedMapping.hidden=step!=='rules';document.body.classList.remove('ff-modal-host');if(parent!==window)parent.postMessage({type:'ff-wizard-state',open:false,focusComposer},location.origin);if(!focusComposer)$('wizard-open').focus()}
132
137
  async function applyScope(){
133
138
  if(!current||!wizardFields.size)throw Error('请至少选择一个已映射的填写字段');
134
139
  const review=scopeReview();if(review.blocked)throw Error('没有可用主体标识,请先到字段设置映射企业名称列。');
@@ -165,11 +170,12 @@ function highlightMappingConflicts(){
165
170
  for(const section of $('mapping').querySelectorAll('fieldset')){
166
171
  const fields=[...section.querySelectorAll('select[data-column]')];
167
172
  for(const field of fields)field.closest('.ff-mapping-row').updateMappingStatus?.();
168
- for(const field of fields){const peers=fields.filter(other=>other.value===field.value);const conflict=!!field.value&&peers.length>1&&(peers.some(other=>other.dataset.manual!=='true')||(field.dataset.primary==='true'&&peers.filter(other=>other.closest('.ff-mapping-row').querySelector('.ff-mapping-role').value!=='output').length>1));field.setAttribute('aria-invalid',String(conflict));const row=field.closest('.ff-mapping-row');row.classList.toggle('ff-mapping-conflict',conflict);if(conflict){const message=row.querySelector('.ff-mapping-status');message.dataset.state='pending';message.textContent='待人工确认:重复位置需逐一确认;企业标识需保留唯一定位列。';}}
173
+ for(const field of fields){const peers=fields.filter(other=>other.value===field.value);const conflict=!!field.value&&peers.length>1&&(field.dataset.primary==='true'&&peers.filter(other=>other.closest('.ff-mapping-row').querySelector('.ff-mapping-role').value!=='output').length>1);field.setAttribute('aria-invalid',String(conflict));const row=field.closest('.ff-mapping-row');row.classList.toggle('ff-mapping-conflict',conflict);if(conflict){const message=row.querySelector('.ff-mapping-status');message.dataset.state='pending';message.textContent='待人工确认:重复位置需逐一确认;企业标识需保留唯一定位列。';}}
169
174
  }
170
175
  }
171
176
  function filterResults(){const query=$('result-search').value.trim().toLowerCase(),rows=[...$('changes').rows];let shown=0;for(const tr of rows){tr.hidden=!!query&&!tr.textContent.toLowerCase().includes(query);if(!tr.hidden)shown++}const selected=rows.filter(tr=>tr.querySelector('input')?.checked).length;$('result-count').textContent=rows.length?'显示 '+shown+' / '+rows.length+' 格 · 已选 '+selected+' 格'+(!shown?' · 无匹配结果':''):'暂无可填写单元格'}
172
177
  function renderSettings(data){
178
+ try{const saved=JSON.parse(sessionStorage.getItem('ff-ui-mapping:'+data.id));if(saved?.base===JSON.stringify(data.configuration))data={...data,configuration:saved.configuration}}catch{}
173
179
  $('mapping').replaceChildren();$('entities').replaceChildren();
174
180
  for(const sheet of data.structure||[]){
175
181
  const section=document.createElement('fieldset');section.dataset.sheet=sheet.name;
@@ -268,19 +274,20 @@ function render(data){
268
274
  current=data;directScope();$('direct-command').hidden=true;filename=(data.filename||'已填表格.xlsx').replace(/\.xlsx$/i,'');location.hash='task='+data.id;
269
275
  if(gridTask!==data.id){gridTask=data.id;gridPageNumber=1;gridColumn=1;$('grid-sheet').replaceChildren();$('grid-query').value=''}$('grid').hidden=false;loadGrid();
270
276
  if(parent!==window)parent.postMessage({type:'ff-task',taskId:data.id},location.origin);
271
- $('incomplete').closest('.card').style.display=data.changeSet.incomplete.length?'':'none';$('changes').replaceChildren();$('incomplete').replaceChildren();$('downloads').replaceChildren();
277
+ $('incomplete').closest('.card').style.display=(data.exceptions||data.changeSet.incomplete).length?'':'none';$('changes').replaceChildren();$('incomplete').replaceChildren();$('downloads').replaceChildren();
272
278
  $('task-meta').dataset.state=data.state;
273
- $('task-meta').textContent=(data.filename||'表格')+' · '+({'enriching':'查询中','completed':'已生成副本'+(data.changeSet.incomplete.length?',仍有 '+data.changeSet.incomplete.length+' 项未完成':''),'interrupted':'查询中断,请重新核验','partial':'部分完成','preview_ready':'可核验'}[data.state]||data.state)+' · 有效期至 '+new Date(data.expiresAt).toLocaleString();
279
+ $('task-meta').textContent=(data.presentation?.title||data.filename)+' · '+(data.presentation?.status||data.state);$('task-details').textContent='任务 ID:'+data.id+' · 有效期至 '+new Date(data.expiresAt).toLocaleString();
274
280
  $('summary').textContent='可填写 '+data.changeSet.changes.length+' 格 · '+(data.plan.paidCalls?'企查查来源':'本地分析 / 合成示例 · 费用 0')+' · 未完成 '+data.changeSet.incomplete.length+' 项';
281
+ $('result-explanations').textContent=(data.resultExplanations||[]).map(i=>(i.sheet||'')+' '+(i.cell||'')+':'+(data.diagnostics?.reasons.find(r=>r.code===i.reason)?.label||i.reason)).join(';')||'暂无。';
275
282
  $('structure').textContent=data.analysis.tables.map(t=>t.sheet+':表头第 '+t.headerRow+' 行').join(';');
276
283
  renderSettings(data);
277
284
  for(const c of data.candidates||data.changeSet.changes){const tr=row('changes',[c.sheet+' ! '+c.cell,c.label,c.oldValue||'(空白)',c.newValue,(c.source.startsWith('qcc://')?'企查查 · '+c.source.slice(6):'合成示例')+' · 获取于 '+c.acquiredAt.slice(0,10)+' · 明确字段映射'],['位置','字段','原值','准备填写','来源']);const td=document.createElement('td'),check=document.createElement('input');check.type='checkbox';check.checked=(data.selectedIds||data.changeSet.changes.map(x=>x.id)).includes(c.id);check.value=c.id;check.disabled=data.confirmed;check.setAttribute('aria-label','填写 '+c.cell);td.append(check);tr.append(td)}
278
- for(const i of data.changeSet.incomplete)row('incomplete',[i.sheet+' '+(i.cell||('第 '+i.row+' 行')),i.label||'—',i.detail||data.diagnostics?.reasons.find(r=>r.code===i.reason)?.label||reasons[i.reason]||'此项需要人工处理'],['位置','字段','原因']);
285
+ for(const i of data.exceptions||data.changeSet.incomplete)row('incomplete',[i.sheet+' '+(i.cell||('第 '+i.row+' 行')),i.label||'—',i.detail||data.diagnostics?.reasons.find(r=>r.code===i.reason)?.label||reasons[i.reason]||'此项需要人工处理'],['位置','字段','原因']);
279
286
  $('field-results').replaceChildren();
280
287
  for(const column of data.diagnostics?.columns||[])row('field-results',[column.label,(data.catalog.find(f=>f.key===column.field)?.label||'未映射 / 不填写'),column.ready,column.incomplete],['原表字段','映射字段','预览可填写格数','未完成项']);
281
288
  $('preview').hidden=false;if(data.confirmed)downloads();$('qcc-command').textContent=command();
282
289
  initWizard();filterResults();syncActions();if($('wizard').open)renderWizard();
283
- $('reason-summary').textContent=(data.diagnostics?.reasons||[]).map(r=>r.label+':'+r.count+r.unit).join(';')||'暂无未完成项。';
290
+ $('reason-summary').textContent=(data.diagnostics?.reasons||[]).filter(r=>(data.exceptions||data.changeSet.incomplete).some(i=>i.reason===r.code)).map(r=>r.label+':'+r.count+r.unit).join(';')||'暂无待核验或失败项。';
284
291
  const missing=data.analysis.incomplete.filter(i=>i.reason==='missing-anchor');
285
292
  $('anchor-alert').textContent=missing.length?'有 '+missing.length+' 行缺少可用主体标识。请把实际包含名称的列映射为“企业名称”,或提供非空信用代码;当前不会查询这些行。':'';
286
293
  }
@@ -288,7 +295,7 @@ async function restore(silent=false){if(busy||!current&& !location.hash)return;c
288
295
  async function preview(buffer,name,demo=false){controls(true);try{const bytes=new Uint8Array(buffer);if(bytes.length>8388608)throw Error('文件超过 8 MiB');let binary='';for(let i=0;i<bytes.length;i+=32768)binary+=String.fromCharCode(...bytes.subarray(i,i+32768));$('grid').open=true;render(await api('/preview',{base64:btoa(binary),filename:name,sessionId,analyzeOnly:!demo}));navigate(demo?'preview':'rules');$('status').textContent='预览已准备好,请检查后确认。'}catch(e){$('status').textContent=e.message;$('wizard-message').textContent=e.message}finally{controls(false)}}
289
296
  document.querySelectorAll('[data-step]').forEach(b=>b.onclick=()=>navigate(b.dataset.step));
290
297
  $('file').onchange=async()=>{const input=$('file'),f=input.files[0];if(!f||busy)return;input.disabled=true;controls(true);$('status').textContent='正在自动解析表格…';try{await preview(await f.arrayBuffer(),f.name)}catch(e){$('status').textContent=e.message}finally{input.disabled=false;input.value='';controls(false)}};
291
- function readMapping(){const configuration={sheets:[],headers:[],mappings:[]};for(const section of $('mapping').querySelectorAll('fieldset')){if(!section.querySelector('.sheet-enabled').checked)continue;const sheet=section.dataset.sheet;configuration.sheets.push(sheet);configuration.headers.push({sheet,row:Number(section.querySelector('.header-row').value)});for(const field of section.querySelectorAll('[data-column]'))if(field.dataset.manual==='true')configuration.mappings.push({sheet,column:Number(field.dataset.column),field:field.value||null,...(field.dataset.primary==='true'?{role:field.closest('.ff-mapping-row').querySelector('.ff-mapping-role').value}:{})})}return configuration}
298
+ function readMapping(){const configuration={sheets:[],headers:[],mappings:[]};for(const section of $('mapping').querySelectorAll('fieldset')){if(!section.querySelector('.sheet-enabled').checked)continue;const sheet=section.dataset.sheet;configuration.sheets.push(sheet);configuration.headers.push({sheet,row:Number(section.querySelector('.header-row').value)});for(const field of section.querySelectorAll('[data-column]'))if(field.value||field.dataset.manual==='true')configuration.mappings.push({sheet,column:Number(field.dataset.column),field:field.value||null,...(field.dataset.primary==='true'?{role:field.closest('.ff-mapping-row').querySelector('.ff-mapping-role').value}:{})})}return configuration}
292
299
  function mappingSignature(configuration){return JSON.stringify(configuration.headers.map(h=>({sheet:h.sheet,row:h.row,cells:current.structure.find(s=>s.name===h.sheet)?.rows.find(r=>r.number===h.row)?.cells}))) }
293
300
  $('mapping-save').onclick=()=>{try{if(!current||busy)return;const configuration=readMapping();localStorage.setItem('ff-mapping:'+owner,JSON.stringify({schema:1,signature:mappingSignature(configuration),configuration}));$('status').textContent='字段规则已保存在本浏览器。'}catch{$('status').textContent='浏览器无法保存字段规则。'}};
294
301
  $('mapping-load').onclick=async()=>{if(!current||busy)return;controls(true);try{const saved=JSON.parse(localStorage.getItem('ff-mapping:'+owner)||'null');if(saved?.schema!==1||saved.signature!==mappingSignature(saved.configuration))throw Error('没有与当前工作表及表头一致的规则,请手动设置。');render(await api('/configure',{id:current.id,configuration:saved.configuration}));$('status').textContent='字段规则已复用,请检查后重新核验。'}catch(e){$('status').textContent=e.message}finally{controls(false)}};
@@ -298,7 +305,7 @@ $('confirm').onclick=async()=>{if(!current||busy)return;controls(true);try{await
298
305
  $('selection').onclick=async()=>{if(!current||busy)return;controls(true);try{render(await api('/select',{id:current.id,selectedIds:[...$('changes').querySelectorAll('input:checked')].map(x=>x.value)}));$('status').textContent='选择已应用,请核对新的预览。'}catch(e){$('status').textContent=e.message}finally{controls(false)}};
299
306
  $('refresh').onclick=()=>restore();
300
307
  $('discard').onclick=async()=>{if(!current||busy)return;controls(true);try{await api('/discard',{id:current.id});current=null;history.replaceState(null,'',location.pathname+location.search);$('preview').hidden=true;$('grid').hidden=true;gridTask=null;navigate('import');$('status').textContent='任务及保存的表格已删除。'}catch(e){$('status').textContent=e.message}finally{controls(false)}};
301
- $('history-tab').onclick=async()=>{try{const list=await api('/tasks');$('history-list').replaceChildren();for(const item of list){const div=document.createElement('div');div.className='history-item';const button=document.createElement('button');button.textContent=item.filename+' · '+item.state;button.onclick=()=>{location.hash='task='+item.id;restore()};div.append(button);$('history-list').append(div)}$('history').hidden=false;$('current').hidden=true;$('current-tab').setAttribute('aria-pressed','false');$('history-tab').setAttribute('aria-pressed','true');if(!list.length)$('history-list').textContent='暂无可恢复的任务。'}catch(e){$('status').textContent=e.message}};
308
+ $('history-tab').onclick=async()=>{try{const list=await api('/tasks');$('history-list').replaceChildren();for(const item of list){const div=document.createElement('div');div.className='history-item';const button=document.createElement('button');button.textContent=(item.presentation?.title||item.filename)+' · '+(item.presentation?.status||item.state);button.onclick=()=>{location.hash='task='+item.id;restore()};const details=document.createElement('details'),summary=document.createElement('summary');summary.textContent='任务详情';details.append(summary,document.createTextNode('任务 ID:'+item.id));div.append(button,details);$('history-list').append(div)}if(parent!==window)parent.postMessage({type:'ff-view',step:'history'},location.origin);$('history').hidden=false;$('current').hidden=true;$('current-tab').setAttribute('aria-pressed','false');$('history-tab').setAttribute('aria-pressed','true');if(!list.length)$('history-list').textContent='暂无可恢复的任务。'}catch(e){$('status').textContent=e.message}};
302
309
  $('current-tab').onclick=()=>navigate(step);
303
310
  $('wizard-open').onclick=()=>directDraft();$('wizard-close').onclick=()=>closeWizard();$('wizard').addEventListener('cancel',e=>{e.preventDefault();closeWizard()});
304
311
  document.querySelectorAll('[data-wizard-step]').forEach(b=>b.onclick=()=>{const next=Number(b.dataset.wizardStep);wizardStep=next>1&&wizardMappingApplied!==JSON.stringify([current?.id,readMapping()])?1:next;saveWizard();renderWizard()});
@@ -314,10 +321,13 @@ $('draft-cancel').onclick=()=>$('draft-conflict').close();
314
321
  for(const mode of ['append','replace'])$('draft-'+mode).onclick=()=>{$('draft-conflict').close();sendDraft(mode)};
315
322
  $('mapping-search').oninput=()=>{const query=$('mapping-search').value.trim().toLowerCase();$('mapping').querySelectorAll('.ff-mapping-row').forEach(row=>row.hidden=!row.dataset.sourceLabel.toLowerCase().includes(query))};
316
323
  $('result-search').oninput=filterResults;$('changes').onchange=filterResults;
317
- window.addEventListener('message',e=>{if(e.origin!==location.origin||e.source!==parent)return;if(e.data?.type==='ff-theme'&&['light','dark'].includes(e.data.theme))document.body.dataset.ffTheme=e.data.theme;if(e.data?.type==='ff-draft-conflict'){expectedDraft=e.data.draft;if(!$('draft-conflict').open)$('draft-conflict').showModal()}if(e.data?.type==='ff-draft-result'){$('status').textContent=e.data.ok?'已回填,请在对话框修改或发送。':'回填失败,任务已保留。';if(e.data.ok){closeWizard(true);navigate('identity');$('status').textContent='指令已回填,发送后将在此显示查询进度。'}}if(e.data?.type==='ff-navigate'){if(!current&&location.hash)pendingNavigation=e.data.step;if(e.data.step==='history')$('history-tab').click();else navigate(e.data.step)}});
324
+ window.addEventListener('message',e=>{if(e.origin!==location.origin||e.source!==parent)return;if(e.data?.type==='ff-theme'&&['light','dark'].includes(e.data.theme))document.body.dataset.ffTheme=e.data.theme;if(e.data?.type==='ff-draft-conflict'){expectedDraft=e.data.draft;if(!$('draft-conflict').open)$('draft-conflict').showModal()}if(e.data?.type==='ff-draft-result'){$('status').textContent=e.data.ok?'已回填,请在对话框修改或发送。':'回填失败,任务已保留。';if(e.data.ok){closeWizard(true);navigate('identity');$('status').textContent='指令已回填,发送后将在此显示查询进度。'}}if(e.data?.type==='ff-navigate'){if(!current&&location.hash){pendingNavigation=e.data.step;return}if(e.data.step==='history')$('history-tab').click();else navigate(e.data.step)}});
318
325
  api('/health').then(s=>{$('connection').textContent=s.qccAvailable?'已发现企查查工商工具,可在对话中执行。':'尚未发现企查查工商工具;请先连接 MCP。合成模板仍可体验。'}).catch(()=>{});
319
326
  $('preview-continue').onclick=async()=>{if(busy||!current)return;controls(true);try{const ids=[...$('changes').querySelectorAll('input:checked')].map(x=>x.value);if(!current.confirmed&&JSON.stringify([...ids].sort())!==JSON.stringify([...(current.selectedIds||[])].sort()))render(await api('/select',{id:current.id,selectedIds:ids}));navigate('download');$('confirm').scrollIntoView({block:'center'});$('confirm').focus();$('status').textContent=current.confirmed?'副本已生成,请点击下载。':'请确认填写结果,点击蓝色按钮生成新副本。'}catch(e){$('status').textContent=e.message}finally{controls(false)}};
320
327
  $('mapping-continue').onclick=()=>{if(busy||!current)return;navigate('identity');$('wizard-open').focus()};
328
+ function saveMappingDraft(){if(!current)return;sessionStorage.setItem('ff-ui-mapping:'+current.id,JSON.stringify({base:JSON.stringify(current.configuration),configuration:readMapping()}));if($('wizard').open)saveWizard()}
329
+ window.addEventListener('pagehide',saveMappingDraft);
330
+ for(const event of ['change','click'])$('mapping').addEventListener(event,saveMappingDraft);
321
331
  $('mapping').addEventListener('change',()=>{wizardMappingApplied=false;$('mapping-next').hidden=true;highlightMappingConflicts()});
322
332
  window.addEventListener('hashchange',()=>{if(new URLSearchParams(location.hash.slice(1)).get('task')!==current?.id)restore()});
323
333
  navigate('import');restore();setInterval(()=>{if(!document.hidden&&current&&!busy&&!$('wizard').open)restore(true)},2000);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-form-fill-agent",
3
- "version": "0.2.20",
3
+ "version": "0.2.22",
4
4
  "private": false,
5
5
  "description": "AI form filling and spreadsheet autofill for XLSX in DeepSeek Harness, using authorized Qichacha MCP facts with cell previews, user confirmation and export to a new copy.",
6
6
  "repository": {
@@ -35,6 +35,14 @@
35
35
  "form-fill-core": "0.2.10",
36
36
  "qcc-form-fill-provider": "0.2.2"
37
37
  },
38
+ "peerDependencies": {
39
+ "dsh-better-sidebar": ">=0.17.1 <0.19.0"
40
+ },
41
+ "peerDependenciesMeta": {
42
+ "dsh-better-sidebar": {
43
+ "optional": true
44
+ }
45
+ },
38
46
  "dsh": {
39
47
  "client": {
40
48
  "inject": [