dsh-form-fill-agent 0.2.15 → 0.2.17

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
@@ -5,7 +5,7 @@
5
5
  AI填表智能体:支持自动填表、表格填充、表格补全、Excel填表与 Excel回填;使用企查查 MCP 填写 XLSX 空白字段,预览确认后导出新副本。
6
6
 
7
7
  ```sh
8
- dsh plugin --profile web add dsh-form-fill-agent@0.2.15
8
+ dsh plugin --profile web add dsh-form-fill-agent@0.2.17
9
9
  ```
10
10
 
11
11
  请先满足下文的 DSH、连接器及侧边栏依赖要求;安装后完整停止并重启对应 Profile。
@@ -64,7 +64,7 @@ dsh plugin --profile web add dsh-form-fill-agent@latest
64
64
 
65
65
  ## 使用边界与数据保存
66
66
 
67
- 补充要求为模型可读文本;字段或工作表范围请通过向导/工作台修改,不能仅改文字后假定执行范围已同步。任务 schema 5 可读取 schema 1–5;回退旧版本请使用升级前目录副本或独立任务目录。主包为 0.2.14,共享内核为 0.2.9,Provider 为 0.2.1,共享字段契约为 0.1.0。
67
+ 补充要求为模型可读文本;字段或工作表范围请通过向导/工作台修改,不能仅改文字后假定执行范围已同步。任务 schema 5 可读取 schema 1–5;回退旧版本请使用升级前目录副本或独立任务目录。主包为 0.2.17,共享内核为 0.2.10,Provider 为 0.2.1,共享字段契约为 0.1.0。
68
68
 
69
69
  支持企业完整登记名称或 18 位信用代码;简称、主体不一致或多候选不会自动猜选。仅有信用代码的表格也可作为查询锚点。连接候选检索工具时可检索简称,仍需人工确认;缺少工具时降级人工输入。字段映射采用确定性规则及人工确认,不让模型编造事实。支持普通内部公式、固定区域下拉、基础条件格式和普通表格对象;复杂 Excel 结构和 Word 暂不支持,保真边界见下方安全说明。
70
70
 
@@ -72,7 +72,7 @@ dsh plugin --profile web add dsh-form-fill-agent@latest
72
72
 
73
73
  显式设置 DSH_HOME 时,任务保存至其 form-fill-tasks 目录,默认 24 小时过期;未设置时使用内存。任务保存输入表格和归一化预览,不持久化原始 MCP 响应,可主动删除任务。模型及 MCP 凭据由宿主管理。
74
74
 
75
- 0.2.14 保存任务 schema 5,兼容读取旧 schema 1–4;回退旧版本须使用升级前任务目录副本或新目录,防止丢失已确认的字段/主体设置。
75
+ 0.2.17 保存任务 schema 5,兼容读取旧 schema 1–4;回退旧版本须使用升级前任务目录副本或新目录,防止丢失已确认的字段/主体设置。
76
76
 
77
77
  ## 文档与反馈
78
78
 
package/lib/brand.css CHANGED
@@ -49,3 +49,14 @@
49
49
  @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}}
50
50
  .ff-ui .ff-next-action{position:fixed;bottom:0;left:0;right:0;z-index:20;display:flex;align-items:center;justify-content:space-between;gap:12px;padding:14px 20px;background:var(--surface);border-top:1px solid var(--line);box-shadow:0 -3px 12px #0001}.ff-ui .ff-next-action[hidden]{display:none}.ff-ui main{padding-bottom:100px}.ff-ui #direct-ack-label{display:block;padding:14px;margin:16px 0;background:var(--soft);border:1px solid var(--action);border-radius:8px}.ff-ui #direct-ack-label[hidden]{display:none}.ff-ui #wizard-open{display:block;min-height:44px;margin-top:12px}.ff-ui #scope-omissions-list{max-height:180px;overflow:auto;overflow-wrap:anywhere}.ff-ui .ff-task-more{margin:20px;color:var(--muted)}@media(max-width:480px){.ff-ui .ff-next-action{flex-direction:column;align-items:stretch;padding:10px 14px}.ff-ui .ff-next-action span{font-size:12px}}
51
51
  .ff-ui #anchor-alert:not(:empty){padding:16px;border:1px solid var(--error);border-left:5px solid var(--error);border-radius:8px;background:color-mix(in srgb,var(--error) 10%,var(--surface));color:var(--error);font-weight:600;overflow-wrap:anywhere}.ff-ui .ff-mapping-conflict{border:2px solid var(--error);border-radius:8px;padding:12px;background:color-mix(in srgb,var(--error) 6%,var(--surface))}.ff-ui .ff-mapping-conflict .ff-mapping-status{color:var(--error);font-weight:600}
52
+ .ff-ui .ff-mapping-status{padding:8px 12px;border-radius:6px;overflow-wrap:anywhere}
53
+ .ff-ui .ff-mapping-status[data-state=automatic],.ff-ui .ff-mapping-status[data-state=confirmed]{color:var(--success);background:var(--success-bg)}
54
+ .ff-ui .ff-mapping-status[data-state=pending]{color:var(--review);background:var(--review-bg)}
55
+ .ff-ui .ff-mapping-status[data-state=unmatched]{color:var(--error);background:var(--error-bg)}
56
+ .ff-ui .ff-mapping-status[data-state=skipped]{color:var(--muted);background:var(--surface)}
57
+ .ff-ui .ff-mapping-role{margin-top:8px;max-width:100%}
58
+ .ff-ui .ff-mapping-row.ff-mapping-conflict{border-color:var(--review);background:var(--review-bg)}
59
+ .ff-ui #wizard-mapping>.card{margin:0;padding:0;border:0;background:transparent;box-shadow:none}
60
+ .ff-ui #wizard-mapping>.card>h2,.ff-ui #wizard-mapping #mapping-next{display:none}
61
+ .ff-ui #wizard-mapping fieldset{min-width:0}
62
+ .ff-ui #wizard-mapping .ff-mapping-row{min-width:0}
package/lib/client.js CHANGED
@@ -135,6 +135,7 @@ window.__ModuleLoader__.load({
135
135
  React.useEffect(()=>{
136
136
  const receive=async event=>{
137
137
  if(event.origin!==location.origin||event.source!==frame.current?.contentWindow||current()!==view?.id)return;
138
+ if(event.data?.type==='ff-capabilities'){frame.current.contentWindow.postMessage({type:'ff-capabilities-result',mappingDraft:2},location.origin);return}
138
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}
139
140
  if(event.data?.type==='ff-task'&&/^[a-f0-9-]{36}$/.test(event.data.taskId)){sessionTasks.set(view.id,event.data.taskId);return}
140
141
  if(event.data?.type!=='ff-draft')return;
package/lib/http.js CHANGED
@@ -18,6 +18,7 @@ export function createFormFillHandler({ basePath = '', getPort, now = Date.now,
18
18
  const visible = (task, owner) => task && (!task.owner || task.owner === owner);
19
19
  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 });
20
20
  const settings = task => ({
21
+ mappingProtocol: 2,
21
22
  diagnostics: diagnostics(task.preview),
22
23
  configuration: task.configuration ?? {},
23
24
  selectedFields: task.selectedFields,
package/lib/ui.html CHANGED
@@ -15,7 +15,7 @@
15
15
  <div class="controls"><input type="file" id="file" accept=".xlsx" aria-label="选择 XLSX 文件"><span class="muted">选择文件后自动解析,核对结果后设置填写字段。</span></div>
16
16
  <details><summary>体验合成模板</summary><p>下面模板只使用合成数据,不查询真实企业。</p><div class="controls" id="samples"><button class="secondary" data-name="客户台账">客户台账</button><button class="secondary" data-name="供应商准入表">供应商准入表</button><button class="secondary" data-name="合同主体信息表">合同主体信息表</button></div></details></div>
17
17
  <section id="preview" hidden>
18
- <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>
18
+ <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
19
  <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
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>
21
21
  <div class="toolbar"><label>筛选单元格 <input id="result-search" type="search" placeholder="位置、字段或值"></label><span id="result-count" class="muted"></span></div>
@@ -28,9 +28,9 @@
28
28
  <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
29
  <div class="dialog-body">
30
30
  <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>只填空白、保留已有内容,查询后预览,确认后生成副本。</p><p id="wizard-rules"></p><button id="wizard-edit-rules">到工作台修正字段映射</button></section>
32
- <section class="ff-wizard-step" data-wizard-pane="2"><h3>企业工商字段</h3><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
- <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><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>
31
+ <section class="ff-wizard-step" data-wizard-pane="1"><h3>确认原表字段映射</h3><p id="wizard-rules" hidden></p><div id="wizard-mapping"></div></section>
32
+ <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
+ <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
34
  </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>
35
35
  <dialog id="draft-conflict" aria-labelledby="draft-conflict-title"><header class="dialog-head"><h2 id="draft-conflict-title">保留已有输入</h2></header><div class="dialog-body">对话框已有内容,请选择替换、追加或取消。取消会保留现有输入。</div><footer class="dialog-footer"><button id="draft-cancel">取消</button><button id="draft-append">追加</button><button id="draft-replace" class="primary">替换</button></footer></dialog>
36
36
  <script>
@@ -76,13 +76,15 @@ $('retry-run').onclick=()=>{openWizard(true);wizardStep=3;renderWizard()};
76
76
  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
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)))}
78
78
  function downloads(){ $('downloads').replaceChildren();for(const [kind,label,suffix] of [['xlsx','下载已填副本','-已填副本.xlsx'],['changes','下载变更清单','-变更.json'],['incomplete','下载未完成项','-未完成.json']]){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
- function businessSummary(){return current?(retryIntent?'仅重试失败、取消或未执行的部分,保留成功结果。':'')+'填写文件“'+current.filename+'”中的空白字段。工作表:'+current.analysis.tables.map(t=>t.sheet).join('、')+'。填写字段:'+current.catalog.filter(f=>wizardFields.has(f.key)).map(f=>f.label).join('、')+'。保留原值,核验预览后生成新副本。':'请先上传 XLSX 文件。'}
79
+ 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
80
  function command(){return current?businessSummary()+'\n'+$('requirements').value+'\n执行关联:调用 form_fill_enrich:taskId='+current.id+',expectedRevision='+current.revision+(retryIntent?',mode=retry':'')+'。字段范围以工作台确认设置为准。':''}
81
- function mappingVersion(){return JSON.stringify({tables:current?.analysis.tables.map(t=>({sheet:t.sheet,headerRow:t.headerRow,anchors:t.anchors,mappings:t.mappings.map(m=>[m.column,m.field])})),scope:current?.selectedFields??null})}
81
+ function mappingVersion(){return JSON.stringify({tables:current?.analysis.tables.map(t=>({sheet:t.sheet,headerRow:t.headerRow,anchors:t.anchors,mappings:t.mappings.map(m=>[m.column,m.field,m.role,m.evidence])})),scope:current?.selectedFields??null})}
82
82
  function saveWizard(){if(current)sessionStorage.setItem('ff-wizard:'+current.id,JSON.stringify({fields:[...wizardFields],signature:mappingVersion(),requirements:$('requirements').value,step:wizardStep}))}
83
- function initWizard(){const structure=mappingVersion();if(wizardTask===current?.id&&wizardStructure===structure)return;wizardTask=current?.id;wizardStructure=structure;const available=current?.analysis.tables.flatMap(t=>t.mappings.filter(m=>m.field&&!t.anchors.includes(m.field)).map(m=>m.field))||[];let saved;try{saved=JSON.parse(sessionStorage.getItem('ff-wizard:'+current?.id))}catch{}const valid=saved?.signature===structure;wizardFields=new Set((valid?saved.fields:current?.selectedFields||available).filter(k=>available.includes(k)));$('requirements').value=saved?.requirements||'';wizardStep=valid?saved.step||0:0;if(saved&&!valid)$('wizard-message').textContent='字段映射或执行范围已变化,已重新加载当前可填写字段,请检查后再发送。'}
83
+ async function requireMappingHost(){if(current?.mappingProtocol!==2)throw Error('页面与服务版本不一致,请保存任务后更新并重新打开 DSH。');await new Promise((resolve,reject)=>{const timer=setTimeout(()=>{window.removeEventListener('message',receive);reject(Error('当前 Host 不支持此映射草稿协议,请保存任务后更新并重新打开 DSH。'))},1500);function receive(e){if(e.origin!==location.origin||e.source!==parent||e.data?.type!=='ff-capabilities-result')return;clearTimeout(timer);window.removeEventListener('message',receive);e.data.mappingDraft===2?resolve():reject(Error('Host 映射协议不兼容,请更新 DSH 插件。'))}window.addEventListener('message',receive);parent.postMessage({type:'ff-capabilities'},location.origin)})}
84
+ function isInput(t,m){return t.anchorColumns?t.anchorColumns.includes(m.column):t.anchors.includes(m.field)}
85
+ function initWizard(){const structure=mappingVersion();if(wizardTask===current?.id&&wizardStructure===structure)return;wizardTask=current?.id;wizardStructure=structure;const available=current?.analysis.tables.flatMap(t=>t.mappings.filter(m=>m.field&&!isInput(t,m)).map(m=>m.field))||[];let saved;try{saved=JSON.parse(sessionStorage.getItem('ff-wizard:'+current?.id))}catch{}const valid=saved?.signature===structure;wizardFields=new Set((valid?saved.fields:current?.selectedFields||available).filter(k=>available.includes(k)));$('requirements').value=saved?.requirements||'';wizardStep=valid?saved.step||0:0;if(saved&&!valid)$('wizard-message').textContent='字段映射或执行范围已变化,已重新加载当前可填写字段,请检查后再发送。'}
84
86
  function scopeReview(){
85
- const omitted=current?.analysis.tables.flatMap(t=>t.mappings.filter(m=>!t.anchors.includes(m.field)&&(!m.field||!wizardFields.has(m.field))).map(m=>t.sheet+':'+m.label))||[];
87
+ const omitted=current?.analysis.tables.flatMap(t=>t.mappings.filter(m=>!isInput(t,m)&&(!m.field||!wizardFields.has(m.field))).map(m=>t.sheet+':'+m.label))||[];
86
88
  const missing=current?.analysis.incomplete.filter(i=>i.reason==='missing-anchor')||[];
87
89
  let problem='';
88
90
  const tables=current?.analysis.tables||[],issues=current?.analysis.incomplete||[];
@@ -90,13 +92,16 @@ function scopeReview(){
90
92
  const conflicts=issues.filter(i=>i.reason==='ambiguous-mapping');
91
93
  if(conflicts.length){
92
94
  const details=conflicts.map(i=>{const mappings=current.configuration?.mappings?.filter(m=>m.sheet===i.sheet&&m.field)||[];const repeated=[...new Set(mappings.filter(m=>mappings.some(n=>n.column!==m.column&&n.field===m.field)).map(m=>current.catalog.find(f=>f.key===m.field)?.label||m.field))];return i.sheet+(repeated.length?':'+repeated.join('、'):'')});
93
- problem='字段映射存在重复或冲突('+details.join(';')+')。请返回字段设置,同一目标字段保留一列,其他列选择不填写。';
95
+ problem='字段映射存在重复或冲突('+details.join(';')+')。请返回字段设置,请逐一确认重复输出位置;重复企业标识仅保留一个定位列,其余设为仅接收填写。';
94
96
  }else problem='未识别到可执行的表头。请在字段设置确认表头行,保留企业名称或有值的信用代码,并至少映射一个需要填写的字段。';
95
97
  }else if(!wizardFields.size)problem='尚未选择填写字段。请返回字段设置,为原表中至少一个待填列选择对应字段;企业名称仅用于定位主体。';
96
98
  else if(missing.length&&!current.analysis.opportunities.length)problem='待填行缺少可用企业标识。请返回字段设置映射有值的企业名称列,或检查原表主体内容。';
97
99
  return {omitted,missing,blocked:!!problem,problem};
98
100
  }
101
+ let wizardMappingApplied=false;
102
+ const sharedMapping=document.querySelector('[data-pane="rules"]'),mappingHome=document.createComment('shared mapping home');sharedMapping.before(mappingHome);
99
103
  function renderWizard(){
104
+ if($('wizard').open){$('wizard-mapping').append(sharedMapping);sharedMapping.hidden=false}
100
105
  initWizard();
101
106
  document.querySelectorAll('[data-wizard-pane]').forEach(p=>p.setAttribute('aria-current',String(Number(p.dataset.wizardPane)===wizardStep)));
102
107
  document.querySelectorAll('[data-wizard-step]').forEach(p=>p.setAttribute('aria-current',String(Number(p.dataset.wizardStep)===wizardStep)));
@@ -104,9 +109,9 @@ function renderWizard(){
104
109
  $('wizard-rules').textContent=current?current.analysis.tables.map(t=>t.sheet+':第 '+t.headerRow+' 行表头').join(';'):'请先上传并识别表格。';
105
110
  $('wizard-fields').replaceChildren();
106
111
  const available=new Set(current?.analysis.tables.flatMap(t=>t.mappings.map(m=>m.field))||[]);
107
- const fillable=new Set(current?.analysis.tables.flatMap(t=>t.mappings.filter(m=>m.field&&!t.anchors.includes(m.field)).map(m=>m.field))||[]);
112
+ const fillable=new Set(current?.analysis.tables.flatMap(t=>t.mappings.filter(m=>m.field&&!isInput(t,m)).map(m=>m.field))||[]);
108
113
  for(const field of (current?.catalog||[]).filter(f=>available.has(f.key))){
109
- const label=document.createElement('label');label.hidden=!field.label.includes($('field-search').value.trim());const input=document.createElement('input');input.type='checkbox';const fixed=field.anchor||available.has(field.key)&&!fillable.has(field.key);input.checked=fixed||wizardFields.has(field.key);input.disabled=fixed||!available.has(field.key);input.dataset.field=field.key;
114
+ const label=document.createElement('label');label.hidden=!field.label.includes($('field-search').value.trim());const input=document.createElement('input');input.type='checkbox';const fixed=available.has(field.key)&&!fillable.has(field.key);input.checked=fixed||wizardFields.has(field.key);input.disabled=fixed||!available.has(field.key);input.dataset.field=field.key;
110
115
  input.onchange=()=>{if(input.checked)wizardFields.add(field.key);else wizardFields.delete(field.key);saveWizard();renderWizard()};
111
116
  label.append(input,document.createTextNode(field.label+(fixed?'(固定)':!available.has(field.key)?'(原表未映射)':'')));$('wizard-fields').append(label);
112
117
  }
@@ -114,17 +119,20 @@ function renderWizard(){
114
119
  $('business-summary').textContent=businessSummary();$('qcc-command').textContent=command();
115
120
  const review=scopeReview(),signature=JSON.stringify([current?.analysis.tables,[...wizardFields].sort()]);
116
121
  if($('scope-ack').dataset.signature!==signature){$('scope-ack').checked=false;$('scope-ack').dataset.signature=signature}
117
- $('scope-review').textContent=(review.omitted.length?'本次不填写 '+review.omitted.length+' 列:'+review.omitted.join('、')+'。':'所有已映射的非主体字段均已纳入。')+(review.missing.length?'有 '+review.missing.length+' 行缺少可用主体标识;请检查企业名称列,不能使用整列为空的信用代码定位主体。':'');
122
+ $('scope-review').textContent=(review.omitted.length?'本次不填写 '+review.omitted.length+' 列:'+review.omitted.slice(0,3).join('、')+(review.omitted.length>3?'等,详见明细':'')+'。':'所有已映射的非主体字段均已纳入。')+(review.missing.length?'有 '+review.missing.length+' 行缺少可用主体标识;请检查企业名称列,不能使用整列为空的信用代码定位主体。':'');
123
+ $('wizard-positions').textContent=(current?.analysis.opportunities||[]).filter(p=>wizardFields.has(p.field)).slice(0,200).map(p=>p.sheet+'!'+p.cell+':'+p.label).join('\n')+'\n最多展示前 200 个位置;完整计划保存在任务中。\n保留列:'+review.omitted.join('、');
124
+ $('wizard-message').textContent=review.blocked?review.problem:review.omitted.length&&!$('scope-ack').checked?'请在第四步核对并确认本次保留列。':'';
118
125
  $('scope-ack-label').hidden=!review.omitted.length;
126
+ $('wizard-next').textContent=wizardStep===1?'应用映射并继续':'下一步';
119
127
  $('wizard-prev').disabled=wizardStep===0||busy;$('wizard-next').hidden=wizardStep===3;$('wizard-next').disabled=!current||busy;
120
128
  $('draft').hidden=wizardStep!==3;$('copy-command').hidden=wizardStep!==3;$('draft').disabled=!current||!wizardFields.size||busy||review.blocked||!!review.omitted.length&&!$('scope-ack').checked;$('copy-command').disabled=$('draft').disabled;
121
129
  }
122
- function openWizard(retry=false){retryIntent=retry===true;initWizard();renderWizard();if(!$('wizard').open)$('wizard').showModal();if(parent!==window){document.body.classList.add('ff-modal-host');parent.postMessage({type:'ff-wizard-state',open:true},location.origin)}}
123
- function closeWizard(focusComposer=false){saveWizard();$('draft-conflict').close();$('wizard').close();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()}
130
+ 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()}
124
132
  async function applyScope(){
125
133
  if(!current||!wizardFields.size)throw Error('请至少选择一个已映射的填写字段');
126
134
  const review=scopeReview();if(review.blocked)throw Error('没有可用主体标识,请先到字段设置映射企业名称列。');if(review.omitted.length&&!$('scope-ack').checked)throw Error('请确认未纳入的列,或返回字段设置完善映射。');
127
- const original=current.selectedFields||[...new Set(current.analysis.tables.flatMap(t=>t.mappings.filter(m=>m.field&&!t.anchors.includes(m.field)).map(m=>m.field)))];
135
+ const original=current.selectedFields||[...new Set(current.analysis.tables.flatMap(t=>t.mappings.filter(m=>m.field&&!isInput(t,m)).map(m=>m.field)))];
128
136
  if(retryIntent&&JSON.stringify([...original].sort())!==JSON.stringify([...wizardFields].sort()))throw Error('重试时请保留原字段范围;如需更改,请重新生成完整核验指令。');
129
137
  if(JSON.stringify([...original].sort())!==JSON.stringify([...wizardFields].sort()))render(await api('/scope',{id:current.id,selectedFields:[...wizardFields]}));
130
138
  }
@@ -132,15 +140,15 @@ async function sendDraft(mode){
132
140
  if(busy)return;
133
141
  if(parent===window){$('wizard-message').textContent='独立页面请复制指令到 DSH 对话。';return}
134
142
  controls(true);$('wizard-message').textContent='';
135
- try{await applyScope();saveWizard();parent.postMessage({type:'ff-draft',taskId:current.id,revision:current.revision,requirements:$('requirements').value,summary:businessSummary(),retryOnly:retryIntent,mode,expectedDraft},location.origin)}catch(e){$('wizard-message').textContent=e.message}finally{controls(false)}
143
+ try{await requireMappingHost();await applyScope();saveWizard();parent.postMessage({type:'ff-draft',taskId:current.id,revision:current.revision,requirements:$('requirements').value,summary:businessSummary(),retryOnly:retryIntent,mode,expectedDraft},location.origin)}catch(e){$('wizard-message').textContent=e.message;$('status').textContent=e.message}finally{controls(false)}
136
144
  }
137
145
  function directScope(){
138
146
  initWizard();retryIntent=false;
139
- wizardFields=new Set(current?.selectedFields||current?.analysis.tables.flatMap(t=>t.mappings.filter(m=>m.field&&!t.anchors.includes(m.field)).map(m=>m.field))||[]);
147
+ wizardFields=new Set(current?.selectedFields||current?.analysis.tables.flatMap(t=>t.mappings.filter(m=>m.field&&!isInput(t,m)).map(m=>m.field))||[]);
140
148
  const review=scopeReview(),signature=mappingVersion();
141
149
  if($('direct-ack').dataset.signature!==signature){$('direct-ack').checked=false;$('direct-ack').dataset.signature=signature}
142
150
  $('direct-scope').textContent=review.problem||'将填写 '+wizardFields.size+' 个已确认字段。'+(review.omitted.length?'另有 '+review.omitted.length+' 列未纳入,可展开查看。':'');
143
- $('scope-omissions').hidden=!review.omitted.length;$('scope-omissions-list').textContent=review.omitted.join('、');
151
+ $('scope-omissions').hidden=!review.omitted.length;$('scope-omissions-list').textContent=review.omitted.slice(0,3).join('、')+(review.omitted.length>3?'等,详见明细':'');
144
152
  $('direct-ack-label').hidden=!review.omitted.length;
145
153
  return review;
146
154
  }
@@ -156,7 +164,8 @@ async function directDraft(){
156
164
  function highlightMappingConflicts(){
157
165
  for(const section of $('mapping').querySelectorAll('fieldset')){
158
166
  const fields=[...section.querySelectorAll('select[data-column]')];
159
- for(const field of fields){const conflict=!!field.value&&fields.some(other=>other!==field&&other.value===field.value);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.textContent='映射冲突:另一列也选择了此字段。请保留其中一列,或为本列选择其他字段。';}}
167
+ 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='待人工确认:重复位置需逐一确认;企业标识需保留唯一定位列。';}}
160
169
  }
161
170
  }
162
171
  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?' · 无匹配结果':''):'暂无可填写单元格'}
@@ -178,8 +187,14 @@ function renderSettings(data){
178
187
  const title=document.createElement('strong');title.textContent=cell.label;label.append(title);
179
188
  const select=document.createElement('select');select.dataset.column=cell.column;select.setAttribute('aria-label',sheet.name+' 字段 '+cell.label);
180
189
  const override=data.configuration?.mappings?.find(m=>m.sheet===sheet.name&&m.column===cell.column);
181
- const automatic=table?.headerRow===Number(header.value)?table.mappings.find(m=>m.column===cell.column)?.field||'':'';
190
+ const exactNormalize=s=>String(s??'').trim().replace(/\s+/g,'').toLowerCase();
191
+ const exact=data.catalog.filter(f=>[f.label,f.key,...(f.aliases||[])].some(a=>exactNormalize(a)===exactNormalize(cell.label)));
192
+ const automatic=exact.length===1&&exactNormalize(cell.label)!=='地址'?exact[0].key:'';
182
193
  let chosen=override?override.field||'':automatic,manual=!!override;
194
+ select.dataset.manual=String(manual);
195
+ const role=document.createElement('select');role.className='ff-mapping-role';role.setAttribute('aria-label',sheet.name+' 字段用途 '+cell.label);
196
+ for(const [value,text] of [['input','用于定位企业'],['output','仅接收填写']]){const o=document.createElement('option');o.value=value;o.textContent=text;role.append(o)}
197
+ role.value=override?.role||'input';
183
198
  const normalize=s=>String(s).normalize('NFKC').toLowerCase().replace(/[\s_()()·-]/g,'');
184
199
  const matches=(field,query)=>{const text=normalize([field.label,field.key,...(field.aliases||[])].join(' '));return query.trim().split(/\s+/).every(word=>{const q=normalize(word);if(text.includes(q))return true;if(q.length<2)return false;let i=0;for(const c of text)if(c===q[i])i++;return i===q.length})};
185
200
  const search=document.createElement('input');search.type='search';search.placeholder='关键词,例如:电话、信用代码';search.setAttribute('aria-label',sheet.name+' 搜索目标字段 '+cell.label);
@@ -193,7 +208,9 @@ function renderSettings(data){
193
208
  const status=document.createElement('p');status.className='ff-mapping-status';status.setAttribute('aria-live','polite');
194
209
  const count=document.createElement('small');count.className='muted';
195
210
  const suggestions=document.createElement('div');suggestions.className='ff-mapping-suggestions';
196
- const updateStatus=()=>{status.textContent=chosen?(manual?'已人工选择':'已自动匹配')+':'+(data.catalog.find(f=>f.key===chosen)?.label||chosen):manual?'已选择不填写':'未匹配,请搜索或按维度选择字段;也可保留不填写';status.dataset.unmatched=String(!chosen&&!manual)};
211
+ const updateStatus=()=>{const f=data.catalog.find(f=>f.key===chosen);role.hidden=!(f?.anchor||f?.anchorFallback);select.dataset.manual=String(manual);select.dataset.primary=String(!role.hidden);status.dataset.state=chosen?(manual?'confirmed':'automatic'):manual?'skipped':suggestions.childElementCount?'pending':'unmatched';status.textContent=chosen?(manual?'已确认':'自动通过')+':'+(f?.label||chosen):manual?'已跳过:保留原值,不填写':suggestions.childElementCount?'待人工确认:请选择推荐字段':'未匹配:保留原值,不填写;可搜索字段';};
212
+ label.updateMappingStatus=updateStatus;
213
+ role.onchange=()=>{manual=true;updateStatus();highlightMappingConflicts()};
197
214
  const options=()=>{
198
215
  const group=data.catalogGroups?.find(g=>g.id===dimension.value);
199
216
  const found=(data.catalog||[]).filter(f=>matches(f,search.value)&&(!dimension.value||(dimension.value==='other'?!data.catalogGroups?.some(g=>g.fields.includes(f.key)):group?.fields.includes(f.key))));
@@ -206,7 +223,7 @@ function renderSettings(data){
206
223
  select.value=chosen;count.textContent=found.length?'匹配 '+found.length+' 个字段':'没有匹配字段,请更换关键词或维度';updateStatus();
207
224
  };
208
225
  const choose=()=>{chosen=select.value;manual=true;suggestions.replaceChildren();trigger.textContent=chosen?(data.catalog.find(f=>f.key===chosen)?.label||chosen):'不填写';updateStatus();$('mapping-next').hidden=true};
209
- const finish=()=>{choose();picker.open=false;trigger.focus()};
226
+ const finish=()=>{choose();highlightMappingConflicts();picker.open=false;trigger.focus()};
210
227
  select.onchange=choose;
211
228
  select.ondblclick=finish;
212
229
  select.onkeydown=event=>{if(event.key==='Enter'){event.preventDefault();finish()}};
@@ -215,17 +232,18 @@ function renderSettings(data){
215
232
  const hints={'原文件导入名称':['company_name'],'单位':['company_name'],'名称':['company_name'],'法定代表人':['legal_person'],'登记状态':['business_status'],'地址':['registered_address','mailing_address','invoice_address'],'网址':['contact_official_website']}[cell.label.trim()]||[];
216
233
  const recommended=[...hints.map(key=>data.catalog.find(f=>f.key===key)).filter(Boolean),...(data.catalog||[]).filter(f=>!hints.includes(f.key)&&matches(f,cell.label))].slice(0,4);
217
234
  if(recommended.length){const hint=document.createElement('span');hint.textContent='推荐(确认后采用):';suggestions.append(hint)}
218
- for(const field of recommended){const button=document.createElement('button');button.type='button';button.textContent='推荐:'+field.label;button.onclick=()=>{chosen=field.key;manual=true;suggestions.replaceChildren();options();$('mapping-next').hidden=true;picker.open=false;trigger.focus()};suggestions.append(button)}
235
+ for(const field of recommended){const button=document.createElement('button');button.type='button';button.textContent='推荐:'+field.label;button.title=data.catalogGroups?.find(g=>g.fields.includes(field.key))?.label||'字段目录';button.onclick=()=>{chosen=field.key;manual=true;suggestions.replaceChildren();options();$('mapping-next').hidden=true;picker.open=false;trigger.focus()};suggestions.append(button)}
219
236
  }
220
237
  const filters=document.createElement('div');filters.className='ff-mapping-filters';filters.append(search,dimension);
221
238
  select.size=8;
222
239
  const done=document.createElement('button');done.type='button';done.textContent='确认选择';done.onclick=finish;
223
240
  panel.append(filters,select,count,done);
224
- label.append(status,suggestions,picker);options();fields.append(label);
241
+ label.append(status,suggestions,picker,role);options();fields.append(label);
225
242
  }
226
243
  };
227
- header.onchange=showFields;section.append(header,fields);showFields();$('mapping').append(section);
244
+ header.onchange=()=>{showFields();highlightMappingConflicts()};section.append(header,fields);showFields();$('mapping').append(section);
228
245
  }
246
+ highlightMappingConflicts();
229
247
  const seen=new Set();
230
248
  for(const item of data.changeSet.incomplete.filter(i=>i.reason==='candidate-review-required')){
231
249
  const key=JSON.stringify([item.sheet,item.row]);if(seen.has(key))continue;seen.add(key);
@@ -260,15 +278,15 @@ initWizard();filterResults();syncActions();if($('wizard').open)renderWizard();
260
278
  const missing=data.analysis.incomplete.filter(i=>i.reason==='missing-anchor');
261
279
  $('anchor-alert').textContent=missing.length?'有 '+missing.length+' 行缺少可用主体标识。请把实际包含名称的列映射为“企业名称”,或提供非空信用代码;当前不会查询这些行。':'';
262
280
  }
263
- async function restore(silent=false){if(busy||!current&& !location.hash)return;const id=new URLSearchParams(location.hash.slice(1)).get('task');if(!id)return;try{const data=await api('/task/'+encodeURIComponent(id));if(!silent||data.revision!==current?.revision){render(data);if(!silent){const target=pendingNavigation||(data.confirmed?'download':'preview');pendingNavigation=undefined;if(target==='history')$('history-tab').click();else navigate(target)}}}catch(e){if(!silent)$('status').textContent=e.message}}
281
+ async function restore(silent=false){if(busy||!current&& !location.hash)return;const id=new URLSearchParams(location.hash.slice(1)).get('task');if(!id)return;try{const data=await api('/task/'+encodeURIComponent(id));if(!silent||data.id!==current?.id||data.revision!==current?.revision){render(data);if(!silent){const target=pendingNavigation||(data.confirmed?'download':'preview');pendingNavigation=undefined;if(target==='history')$('history-tab').click();else navigate(target)}}}catch(e){if(!silent)$('status').textContent=e.message}}
264
282
  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)}}
265
283
  document.querySelectorAll('[data-step]').forEach(b=>b.onclick=()=>navigate(b.dataset.step));
266
284
  $('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)}};
267
- 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]'))configuration.mappings.push({sheet,column:Number(field.dataset.column),field:field.value||null})}return configuration}
285
+ 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}
268
286
  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}))) }
269
287
  $('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='浏览器无法保存字段规则。'}};
270
288
  $('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)}};
271
- $('configure').onclick=async()=>{if(!current||busy)return;const configuration=readMapping();controls(true);$('mapping-next').hidden=true;try{render(await api('/configure',{id:current.id,configuration}));const review=directScope();$('status').textContent=review.blocked?'设置已保存,但尚不能执行。'+review.problem:'字段设置已应用,旧预览已清除,请重新核验。';if(review.blocked){$('anchor-alert').textContent='请完善字段设置:'+review.problem;highlightMappingConflicts();$('anchor-alert').scrollIntoView({block:'center'})}else{navigate('identity');$('wizard-open').scrollIntoView({block:'center'});$('wizard-open').focus()}}catch(e){$('status').textContent=e.message;$('anchor-alert').textContent='字段设置未能应用:'+e.message;$('anchor-alert').scrollIntoView({block:'center'})}finally{controls(false)}};
289
+ $('configure').onclick=async()=>{if(!current||busy)return;const configuration=readMapping();controls(true);$('mapping-next').hidden=true;try{render(await api('/configure',{id:current.id,configuration}));const review=directScope();wizardMappingApplied=!review.blocked?JSON.stringify([current.id,readMapping()]):false;$('status').textContent=review.blocked?'设置已保存,但尚不能执行。'+review.problem:'字段设置已应用,旧预览已清除,请重新核验。';if(review.blocked){$('anchor-alert').textContent='请完善字段设置:'+review.problem;highlightMappingConflicts();$('anchor-alert').scrollIntoView({block:'center'})}else if($('wizard').open){wizardStep=2;saveWizard();renderWizard()}else{navigate('identity');$('wizard-open').scrollIntoView({block:'center'});$('wizard-open').focus()}}catch(e){$('status').textContent=e.message;$('anchor-alert').textContent='字段设置未能应用:'+e.message;$('anchor-alert').scrollIntoView({block:'center'})}finally{controls(false)}};
272
290
  $('samples').onclick=async e=>{if(!e.target.dataset.name||busy)return;const name=e.target.dataset.name;try{const response=await fetch(basePath+'/fixture/'+encodeURIComponent(name));if(!response.ok)throw Error('模板读取失败');await preview(await response.arrayBuffer(),name+'.xlsx',true)}catch(e){$('status').textContent=e.message}};
273
291
  $('confirm').onclick=async()=>{if(!current||busy)return;controls(true);try{await api('/confirm',{id:current.id,confirmChangeSetId:current.changeSet.changeSetId});current=await api('/task/'+current.id);render(current);downloads();$('status').textContent='副本已生成,原文件保持不变。'}catch(e){$('status').textContent=e.message}finally{controls(false)}};
274
292
  $('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)}};
@@ -277,11 +295,11 @@ $('discard').onclick=async()=>{if(!current||busy)return;controls(true);try{await
277
295
  $('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}};
278
296
  $('current-tab').onclick=()=>navigate(step);
279
297
  $('wizard-open').onclick=()=>directDraft();$('wizard-close').onclick=()=>closeWizard();$('wizard').addEventListener('cancel',e=>{e.preventDefault();closeWizard()});
280
- document.querySelectorAll('[data-wizard-step]').forEach(b=>b.onclick=()=>{wizardStep=Number(b.dataset.wizardStep);saveWizard();renderWizard()});
281
- $('wizard-prev').onclick=()=>{wizardStep=Math.max(0,wizardStep-1);saveWizard();renderWizard()};$('wizard-next').onclick=()=>{wizardStep=Math.min(3,wizardStep+1);saveWizard();renderWizard()};
282
- $('wizard-file').onchange=async()=>{const file=$('wizard-file').files[0];if(file){await preview(await file.arrayBuffer(),file.name);renderWizard()}};
283
- $('wizard-edit-rules').onclick=()=>{closeWizard();navigate('rules')};
284
- $('field-search').oninput=renderWizard;$('fields-all').onclick=()=>{wizardFields=new Set(current?.analysis.tables.flatMap(t=>t.mappings.filter(m=>m.field&&!t.anchors.includes(m.field)).map(m=>m.field))||[]);saveWizard();renderWizard()};$('fields-clear').onclick=()=>{wizardFields.clear();saveWizard();renderWizard()};
298
+ 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()});
299
+ $('wizard-prev').onclick=()=>{wizardStep=Math.max(0,wizardStep-1);saveWizard();renderWizard()};$('wizard-next').onclick=async()=>{if(wizardStep===1){await $('configure').onclick();return}wizardStep=Math.min(3,wizardStep+1);saveWizard();renderWizard()};
300
+ $('wizard-file').onchange=async()=>{const file=$('wizard-file').files[0];if(file){wizardMappingApplied=false;await preview(await file.arrayBuffer(),file.name);renderWizard()}};
301
+
302
+ $('field-search').oninput=renderWizard;$('fields-all').onclick=()=>{wizardFields=new Set(current?.analysis.tables.flatMap(t=>t.mappings.filter(m=>m.field&&!isInput(t,m)).map(m=>m.field))||[]);saveWizard();renderWizard()};$('fields-clear').onclick=()=>{wizardFields.clear();saveWizard();renderWizard()};
285
303
  $('scope-ack').onchange=renderWizard;
286
304
  $('requirements').oninput=()=>{saveWizard();$('qcc-command').textContent=command()};
287
305
  $('copy-command').onclick=async()=>{try{await applyScope();saveWizard();await navigator.clipboard.writeText(command());$('status').textContent='已复制,请在 DSH 对话中发送。'}catch(e){$('status').textContent=e.message}};
@@ -294,6 +312,7 @@ window.addEventListener('message',e=>{if(e.origin!==location.origin||e.source!==
294
312
  api('/health').then(s=>{$('connection').textContent=s.qccAvailable?'已发现企查查工商工具,可在对话中执行。':'尚未发现企查查工商工具;请先连接 MCP。合成模板仍可体验。'}).catch(()=>{});
295
313
  $('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)}};
296
314
  $('mapping-continue').onclick=()=>{if(busy||!current)return;navigate('identity');$('wizard-open').focus()};
297
- $('mapping').addEventListener('change',()=>{$('mapping-next').hidden=true});
315
+ $('mapping').addEventListener('change',()=>{wizardMappingApplied=false;$('mapping-next').hidden=true;highlightMappingConflicts()});
316
+ window.addEventListener('hashchange',()=>{if(new URLSearchParams(location.hash.slice(1)).get('task')!==current?.id)restore()});
298
317
  navigate('import');restore();setInterval(()=>{if(!document.hidden&&current&&!busy&&!$('wizard').open)restore(true)},2000);
299
318
  </script></html>
package/lib/workflow.js CHANGED
@@ -12,7 +12,7 @@ export async function previewBytes(bytes, { provider = createMockProvider(), max
12
12
  if (provider.mode !== 'mock' && (provider.mode !== 'qcc' || confirmPaidCalls !== true)) throw new FillError('REAL_PROVIDER_DISABLED', '真实来源需要调用方明确授权');
13
13
  const { analysis } = analyzeDocument(bytes, FIELD_CATALOG, configuration);
14
14
  if (selectedFields !== undefined) {
15
- if (!Array.isArray(selectedFields) || new Set(selectedFields).size !== selectedFields.length || selectedFields.some(key=>!FIELD_CATALOG.some(f=>f.key===key&&!f.anchor))) throw new FillError('FIELD_SCOPE','填写字段范围无效');
15
+ if (!Array.isArray(selectedFields) || new Set(selectedFields).size !== selectedFields.length || selectedFields.some(key=>!FIELD_CATALOG.some(f=>f.key===key&&(!f.anchor||analysis.tables.some(t=>t.mappings.some(m=>m.field===key&&m.role==='output')))))) throw new FillError('FIELD_SCOPE','填写字段范围无效');
16
16
  analysis.incomplete.push(...analysis.opportunities.filter(o=>!selectedFields.includes(o.field)).map(o=>({...o,reason:'user-excluded'})));
17
17
  analysis.opportunities=analysis.opportunities.filter(o=>selectedFields.includes(o.field));
18
18
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-form-fill-agent",
3
- "version": "0.2.15",
3
+ "version": "0.2.17",
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": {
@@ -32,7 +32,7 @@
32
32
  "node": ">=22"
33
33
  },
34
34
  "dependencies": {
35
- "form-fill-core": "0.2.9",
35
+ "form-fill-core": "0.2.10",
36
36
  "qcc-form-fill-provider": "0.2.1"
37
37
  },
38
38
  "dsh": {