dsh-form-fill-agent 0.2.16 → 0.2.18

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.16
8
+ dsh plugin --profile web add dsh-form-fill-agent@0.2.18
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.16,共享内核为 0.2.10,Provider 为 0.2.1,共享字段契约为 0.1.0。
67
+ 补充要求为模型可读文本;字段或工作表范围请通过向导/工作台修改,不能仅改文字后假定执行范围已同步。任务 schema 5 可读取 schema 1–5;回退旧版本请使用升级前目录副本或独立任务目录。主包为 0.2.18,共享内核为 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.16 保存任务 schema 5,兼容读取旧 schema 1–4;回退旧版本须使用升级前任务目录副本或新目录,防止丢失已确认的字段/主体设置。
75
+ 0.2.18 保存任务 schema 5,兼容读取旧 schema 1–4;回退旧版本须使用升级前任务目录副本或新目录,防止丢失已确认的字段/主体设置。
76
76
 
77
77
  ## 文档与反馈
78
78
 
package/lib/brand.css CHANGED
@@ -56,3 +56,7 @@
56
56
  .ff-ui .ff-mapping-status[data-state=skipped]{color:var(--muted);background:var(--surface)}
57
57
  .ff-ui .ff-mapping-role{margin-top:8px;max-width:100%}
58
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/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 { mappingRecommendations } from './mapping-recommendations.js';
10
11
  const XLSX_TYPE = 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet';
11
12
  const FIXTURES = ['客户台账', '供应商准入表', '合同主体信息表'];
12
13
  export function createFormFillHandler({ basePath = '', getPort, now = Date.now, ttlMs = 15 * 60 * 1000, maxTasks = 10, taskDirectory, getQccStatus = () => false } = {}) {
@@ -29,7 +30,7 @@ export function createFormFillHandler({ basePath = '', getPort, now = Date.now,
29
30
  catalogGroups: [...QCC_FIELD_CATALOG,ACTUAL_CONTROLLER_GROUP].map(g=>({id:g.id,label:g.label,fields:g.fields.flatMap(item=>FIELD_CATALOG.filter(f=>f.key===item.id||f.aliases?.includes(item.id)).map(f=>f.key))})),
30
31
  structure: parseWorkbook(task.bytes).sheets.filter(s => !s.hidden).map(s => ({
31
32
  name: s.name,
32
- rows: s.rows.filter(r => !r.hidden).slice(0,30).map(r => ({ number: r.number, cells: Object.values(s.cells).filter(c => c.row === r.number && !c.hidden && c.value.trim()).map(c => ({ column: c.column, label: c.value })) })),
33
+ rows: s.rows.filter(r => !r.hidden).slice(0,30).map(r => ({ number: r.number, cells: Object.values(s.cells).filter(c => c.row === r.number && !c.hidden && c.value.trim()).map(c => ({ column: c.column, label: c.value, recommendations: mappingRecommendations(c.value,FIELD_CATALOG) })) })),
33
34
  })),
34
35
  });
35
36
  const sweep = () => { for (const [id, task] of tasks) if (now() - task.created >= ttlMs) {controllers.get(id)?.abort();tasks.delete(id);} };
@@ -0,0 +1,37 @@
1
+ // Display recommendations only: adopting a candidate remains an explicit UI action.
2
+ // Cleaning-side keys are adapted to the form-fill catalog, not its output schema.
3
+ const aliases = {
4
+ company_name:['name','company','公司名称','单位名称','企业全称','公司全称','主体标识','原文件导入名称','单位','名称'],
5
+ credit_no:['CreditCode','unified_credit_code','unifiedSocialCreditCode','信用代码','社会信用代码','统一信用代码','USCC'],
6
+ reg_no:['registration_no','工商注册号'],
7
+ legal_person:['法定代表','法人代表','legalRepresentative','legal_rep'],
8
+ reg_capital:['注册资金'],established_date:['成立时间','企业成立日期','establishmentDate','establish_date'],
9
+ business_status:['工商登记状态','企业状态','reg_status'],
10
+ registered_address:['企业注册地址','公司注册地址'],
11
+ contact_preferred_phone:['mobile','tel','telephone','电话','手机号码','手机号','phone'],
12
+ contact_official_website:['官网','企业官网','官方网站地址','网址','website'],
13
+ contact_preferred_email:['首选电子邮箱','邮箱','email'],
14
+ actual_controller_name:['实控人','实际控制人','企业实控人名称','企业实际控制人','企业实控人名称(自然人请填写姓名)'],
15
+ actual_controller_direct_ratio:['实际控制人直接持股比例'],
16
+ actual_controller_total_ratio:['实际控制人总持股比例'],
17
+ actual_controller_voting_ratio:['实际控制人表决权比例'],
18
+ };
19
+ const ambiguous = {
20
+ 地址:['registered_address','mailing_address','invoice_address'],
21
+ address:['registered_address','mailing_address'],企业地址:['registered_address','mailing_address'],公司地址:['registered_address','mailing_address'],
22
+ 开业时间:['established_date'],开业日期:['established_date'],
23
+ 所属行业:['industry_category','qcc_industry'],行业:['industry_category','qcc_industry'],法人:['legal_person'],
24
+ };
25
+ const normalize=value=>String(value??'').normalize('NFKC').toLowerCase().replace(/[\s_\-·]/g,'');
26
+ export function mappingRecommendations(header,catalog){
27
+ const label=normalize(String(header??'').normalize('NFKC').replace(/\s*\(?\s*YYYY[-/]MM[-/]DD\s*\)?\s*$/i,''));
28
+ if(!label)return [];
29
+ const available=new Set(catalog.map(f=>f.key));
30
+ const hinted=Object.entries(ambiguous).find(([name])=>normalize(name)===label)?.[1];
31
+ if(hinted)return hinted.filter(key=>available.has(key));
32
+ const exact=catalog.filter(f=>[f.key,f.label,...(f.aliases||[]),...(aliases[f.key]||[])].some(name=>normalize(name)===label));
33
+ if(exact.length)return exact.map(f=>f.key);
34
+ // Conservative partial recommendations only. Do not discard units or semantic
35
+ // qualifiers (industry definitions, staff populations, financial periods).
36
+ return catalog.filter(f=>[f.key,f.label,...(f.aliases||[])].some(name=>label.length>=2&&normalize(name).includes(label))).slice(0,4).map(f=>f.key);
37
+ }
package/lib/ui.html CHANGED
@@ -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,7 +76,7 @@ $('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('、')+'。待补空位置:'+current.analysis.opportunities.filter(p=>wizardFields.has(p.field)).map(p=>p.sheet+'!'+p.cell+'('+p.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
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}))}
@@ -98,7 +98,10 @@ function scopeReview(){
98
98
  else if(missing.length&&!current.analysis.opportunities.length)problem='待填行缺少可用企业标识。请返回字段设置映射有值的企业名称列,或检查原表主体内容。';
99
99
  return {omitted,missing,blocked:!!problem,problem};
100
100
  }
101
+ let wizardMappingApplied=false;
102
+ const sharedMapping=document.querySelector('[data-pane="rules"]'),mappingHome=document.createComment('shared mapping home');sharedMapping.before(mappingHome);
101
103
  function renderWizard(){
104
+ if($('wizard').open){$('wizard-mapping').append(sharedMapping);sharedMapping.hidden=false}
102
105
  initWizard();
103
106
  document.querySelectorAll('[data-wizard-pane]').forEach(p=>p.setAttribute('aria-current',String(Number(p.dataset.wizardPane)===wizardStep)));
104
107
  document.querySelectorAll('[data-wizard-step]').forEach(p=>p.setAttribute('aria-current',String(Number(p.dataset.wizardStep)===wizardStep)));
@@ -116,13 +119,16 @@ function renderWizard(){
116
119
  $('business-summary').textContent=businessSummary();$('qcc-command').textContent=command();
117
120
  const review=scopeReview(),signature=JSON.stringify([current?.analysis.tables,[...wizardFields].sort()]);
118
121
  if($('scope-ack').dataset.signature!==signature){$('scope-ack').checked=false;$('scope-ack').dataset.signature=signature}
119
- $('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?'请在第四步核对并确认本次保留列。':'';
120
125
  $('scope-ack-label').hidden=!review.omitted.length;
126
+ $('wizard-next').textContent=wizardStep===1?'应用映射并继续':'下一步';
121
127
  $('wizard-prev').disabled=wizardStep===0||busy;$('wizard-next').hidden=wizardStep===3;$('wizard-next').disabled=!current||busy;
122
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;
123
129
  }
124
- 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)}}
125
- 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()}
126
132
  async function applyScope(){
127
133
  if(!current||!wizardFields.size)throw Error('请至少选择一个已映射的填写字段');
128
134
  const review=scopeReview();if(review.blocked)throw Error('没有可用主体标识,请先到字段设置映射企业名称列。');if(review.omitted.length&&!$('scope-ack').checked)throw Error('请确认未纳入的列,或返回字段设置完善映射。');
@@ -142,7 +148,7 @@ function directScope(){
142
148
  const review=scopeReview(),signature=mappingVersion();
143
149
  if($('direct-ack').dataset.signature!==signature){$('direct-ack').checked=false;$('direct-ack').dataset.signature=signature}
144
150
  $('direct-scope').textContent=review.problem||'将填写 '+wizardFields.size+' 个已确认字段。'+(review.omitted.length?'另有 '+review.omitted.length+' 列未纳入,可展开查看。':'');
145
- $('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?'等,详见明细':'');
146
152
  $('direct-ack-label').hidden=!review.omitted.length;
147
153
  return review;
148
154
  }
@@ -223,8 +229,12 @@ function renderSettings(data){
223
229
  select.onkeydown=event=>{if(event.key==='Enter'){event.preventDefault();finish()}};
224
230
  search.oninput=options;dimension.onchange=options;
225
231
  if(!chosen&&!manual){
226
- const hints={'原文件导入名称':['company_name'],'单位':['company_name'],'名称':['company_name'],'法定代表人':['legal_person'],'登记状态':['business_status'],'地址':['registered_address','mailing_address','invoice_address'],'网址':['contact_official_website']}[cell.label.trim()]||[];
227
- 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);
232
+ // Strip only a trailing date-format annotation for recommendations. Keep
233
+ // the original header and require confirmation; semantic qualifiers remain.
234
+ const candidateLabel=String(cell.label).normalize('NFKC').replace(/\s*\(?\s*YYYY[-/]MM[-/]DD\s*\)?\s*$/i,'').trim();
235
+ const hintMap={'原文件导入名称':['company_name'],'单位':['company_name'],'名称':['company_name'],'法定代表人':['legal_person'],'登记状态':['business_status'],'企业状态':['business_status'],'creditcode':['credit_no'],'unifiedsocialcreditcode':['credit_no'],'地址':['registered_address','mailing_address','invoice_address'],'网址':['contact_official_website']};
236
+ const hints=hintMap[normalize(candidateLabel)]||[];
237
+ const recommended=Array.isArray(cell.recommendations)?cell.recommendations.map(key=>data.catalog.find(f=>f.key===key)).filter(Boolean):candidateLabel?[...hints.map(key=>data.catalog.find(f=>f.key===key)).filter(Boolean),...(data.catalog||[]).filter(f=>!hints.includes(f.key)&&matches(f,candidateLabel))].slice(0,4):[];
228
238
  if(recommended.length){const hint=document.createElement('span');hint.textContent='推荐(确认后采用):';suggestions.append(hint)}
229
239
  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)}
230
240
  }
@@ -280,7 +290,7 @@ function readMapping(){const configuration={sheets:[],headers:[],mappings:[]};fo
280
290
  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}))) }
281
291
  $('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='浏览器无法保存字段规则。'}};
282
292
  $('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)}};
283
- $('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)}};
293
+ $('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)}};
284
294
  $('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}};
285
295
  $('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)}};
286
296
  $('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)}};
@@ -289,10 +299,10 @@ $('discard').onclick=async()=>{if(!current||busy)return;controls(true);try{await
289
299
  $('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}};
290
300
  $('current-tab').onclick=()=>navigate(step);
291
301
  $('wizard-open').onclick=()=>directDraft();$('wizard-close').onclick=()=>closeWizard();$('wizard').addEventListener('cancel',e=>{e.preventDefault();closeWizard()});
292
- document.querySelectorAll('[data-wizard-step]').forEach(b=>b.onclick=()=>{wizardStep=Number(b.dataset.wizardStep);saveWizard();renderWizard()});
293
- $('wizard-prev').onclick=()=>{wizardStep=Math.max(0,wizardStep-1);saveWizard();renderWizard()};$('wizard-next').onclick=()=>{wizardStep=Math.min(3,wizardStep+1);saveWizard();renderWizard()};
294
- $('wizard-file').onchange=async()=>{const file=$('wizard-file').files[0];if(file){await preview(await file.arrayBuffer(),file.name);renderWizard()}};
295
- $('wizard-edit-rules').onclick=()=>{closeWizard();navigate('rules')};
302
+ 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()});
303
+ $('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()};
304
+ $('wizard-file').onchange=async()=>{const file=$('wizard-file').files[0];if(file){wizardMappingApplied=false;await preview(await file.arrayBuffer(),file.name);renderWizard()}};
305
+
296
306
  $('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()};
297
307
  $('scope-ack').onchange=renderWizard;
298
308
  $('requirements').oninput=()=>{saveWizard();$('qcc-command').textContent=command()};
@@ -306,7 +316,7 @@ window.addEventListener('message',e=>{if(e.origin!==location.origin||e.source!==
306
316
  api('/health').then(s=>{$('connection').textContent=s.qccAvailable?'已发现企查查工商工具,可在对话中执行。':'尚未发现企查查工商工具;请先连接 MCP。合成模板仍可体验。'}).catch(()=>{});
307
317
  $('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)}};
308
318
  $('mapping-continue').onclick=()=>{if(busy||!current)return;navigate('identity');$('wizard-open').focus()};
309
- $('mapping').addEventListener('change',()=>{$('mapping-next').hidden=true;highlightMappingConflicts()});
319
+ $('mapping').addEventListener('change',()=>{wizardMappingApplied=false;$('mapping-next').hidden=true;highlightMappingConflicts()});
310
320
  window.addEventListener('hashchange',()=>{if(new URLSearchParams(location.hash.slice(1)).get('task')!==current?.id)restore()});
311
321
  navigate('import');restore();setInterval(()=>{if(!document.hidden&&current&&!busy&&!$('wizard').open)restore(true)},2000);
312
322
  </script></html>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-form-fill-agent",
3
- "version": "0.2.16",
3
+ "version": "0.2.18",
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": {