dsh-form-fill-agent 0.1.0-alpha.7 → 0.2.0

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
@@ -15,7 +15,7 @@ AI填表是 DeepSeek Harness(DSH)的企业表格填写插件:识别已有
15
15
  dsh plugin --profile web add dsh-form-fill-agent@latest
16
16
  ```
17
17
 
18
- 建议先使用专用测试 profile。在 DSH 设置中配置模型,并连接已授权的企查查企业工商 MCP。插件不附带模型或企查查访问凭据。
18
+ 建议先使用专用测试 profile。在 DSH 设置中配置模型,并连接已授权的企查查企业 MCP;进出口及风险字段还需对应经营、风控 MCP 工具。插件不附带模型或企查查访问凭据。
19
19
 
20
20
  ## 使用流程
21
21
 
@@ -34,8 +34,8 @@ dsh plugin --profile web add dsh-form-fill-agent@latest
34
34
  - 结果筛选显示当前/总数及已选格数,下载始终包含全部确认内容。
35
35
  - 简单 XLSX 安全解析、工作表与表头识别、企业名称或信用代码锚点识别。
36
36
  - 确定性字段语义映射、只填空白、单元格级预览与来源追溯。
37
- - 六类工商字段:统一社会信用代码、法定代表人、成立日期、注册地址、登记状态和登记机关。
38
- - 五步工作台、指令回填、任务历史、单元格排除与重启恢复。
37
+ - 129 字段目录:工商、企业简介、联系方式、上市、开票、进出口、自身及关联风险。事实和确定性摘要区分来源,不生成风险结论。
38
+ - 五步工作台、指令回填、任务历史、可恢复单元格选择、取消/失败重试与重启恢复。
39
39
  - 可选择工作表、表头行并修正字段映射;修改后需重新预览。
40
40
  - 查询主体不一致时展示返回候选,人工选择或补充已核验的完整主体后重新查询,原表名称不覆盖。
41
41
  - 内嵌固定选项下拉列表原样保留,超出选项的事实值列为未完成项。
@@ -43,15 +43,15 @@ dsh plugin --profile web add dsh-form-fill-agent@latest
43
43
 
44
44
  ## 使用边界与数据保存
45
45
 
46
- 补充要求为模型可读文本;字段或工作表范围请通过向导/工作台修改,不能仅改文字后假定执行范围已同步。任务 schema 4 可读取旧任务;回退 alpha.5 或更早版本请使用独立任务目录。共享内核和 Provider 仍为 alpha.5。
46
+ 补充要求为模型可读文本;字段或工作表范围请通过向导/工作台修改,不能仅改文字后假定执行范围已同步。任务 schema 5 可读取 schema 1–5;回退旧版本请使用升级前目录副本或独立任务目录。共享内核、Provider 和主包为 0.2.0。
47
47
 
48
- 支持企业完整登记名称或 18 位信用代码;简称、主体不一致或多候选不会自动猜选。目前没有模糊候选搜索工具。字段映射采用确定性规则及人工确认,不让模型编造事实。公式、区域引用下拉验证、复杂 Excel 结构和 Word 暂不支持,保真边界见下方安全说明。
48
+ 支持企业完整登记名称或 18 位信用代码;简称、主体不一致或多候选不会自动猜选。仅有信用代码的表格也可作为查询锚点。连接候选检索工具时可检索简称,仍需人工确认;缺少工具时降级人工输入。字段映射采用确定性规则及人工确认,不让模型编造事实。支持普通内部公式、固定区域下拉、基础条件格式和普通表格对象;复杂 Excel 结构和 Word 暂不支持,保真边界见下方安全说明。
49
49
 
50
50
  真实查询使用用户连接的企查查 MCP 账号,积分规则以该账号为准。模型服务也需单独配置。
51
51
 
52
52
  显式设置 DSH_HOME 时,任务保存至其 form-fill-tasks 目录,默认 24 小时过期;未设置时使用内存。任务保存输入表格和归一化预览,不持久化原始 MCP 响应,可主动删除任务。模型及 MCP 凭据由宿主管理。
53
53
 
54
- alpha.5 保存任务 schema 3,支持读取旧 schema 1/2;回退旧版本须使用升级前任务目录副本或新目录,防止丢失已确认的字段/主体设置。
54
+ 0.2.0 保存任务 schema 5,兼容读取旧 schema 1–4;回退旧版本须使用升级前任务目录副本或新目录,防止丢失已确认的字段/主体设置。
55
55
 
56
56
  ## 文档与反馈
57
57
 
package/lib/brand.css CHANGED
@@ -7,8 +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
+ @media(min-width:1100px){[data-form-fill-reserve=true]{padding-right:var(--ff-panel-inset)!important}}
10
11
  .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}
11
12
  .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)}
12
- .ff-ui .scroll{overflow:auto;max-height:520px;border:1px solid var(--line);border-radius:6px}.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}
13
+ .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}
13
14
  .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)}
14
15
  @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}}
16
+ .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)}
17
+ .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)}
18
+ .ff-ui .ff-stages button:last-child{border-right:0}.ff-ui .ff-stages button:hover{background:var(--page);color:var(--text)}
19
+ .ff-ui .ff-stages button[aria-current=true]{background:var(--soft);color:var(--action);font-weight:600}.ff-ui .ff-stages button[aria-current=true]:after{content:'';position:absolute;left:8px;right:8px;bottom:0;height:3px;border-radius:3px 3px 0 0;background:var(--action)}
20
+ .ff-ui .ff-stage-icon{display:grid;place-items:center;width:30px;height:30px;border:1px solid var(--line);border-radius:7px;background:var(--surface)}.ff-ui .ff-stage-icon .ff-icon{width:17px;height:17px}
21
+ .ff-ui .ff-stage-label{display:block;max-width:100%;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:12px;line-height:1.35}.ff-ui .ff-stages button:focus-visible{outline-offset:-3px}
22
+ @media(max-width:420px){.ff-ui .ff-stages button{min-height:70px;padding:8px 2px;gap:5px}.ff-ui .ff-stage-icon{width:26px;height:26px}.ff-ui .ff-stage-icon .ff-icon{width:16px;height:16px}.ff-ui .ff-stage-label{font-size:11px}}
package/lib/client.js CHANGED
@@ -121,7 +121,7 @@ window.__ModuleLoader__.load({
121
121
  try{
122
122
  const shell=ctx.conversation?.input?.shell?.(view.id);
123
123
  if(!shell?.setDraft)throw Error('missing input');
124
- const prompt=String(event.data.summary||'填写已上传表格中的空白字段,保留原值,核验后生成新副本。').slice(0,1000)+'\n'+String(requirements||'').slice(0,1000)+'\n执行关联:调用 form_fill_enrich,taskId='+taskId+',expectedRevision='+revision+'。字段范围以工作台确认设置为准。';
124
+ const prompt=String(event.data.summary||'填写已上传表格中的空白字段,保留原值,核验后生成新副本。').slice(0,1000)+'\n'+String(requirements||'').slice(0,1000)+'\n执行关联:调用 form_fill_enrich,taskId='+taskId+',expectedRevision='+revision+(event.data.retryOnly===true?',mode=retry(只重试失败、取消或未开始的查询,保留成功结果)':'')+'。字段范围以工作台确认设置为准。';
125
125
  const readDraft=()=>shell.state?.getSnapshot?.()?.draft??document.querySelector('[data-composer-card] textarea')?.value;
126
126
  let draft=readDraft();if(typeof draft!=='string')throw Error('cannot read draft');
127
127
  const mode=event.data.mode;
@@ -140,11 +140,21 @@ window.__ModuleLoader__.load({
140
140
  window.addEventListener('message',receive);return()=>window.removeEventListener('message',receive);
141
141
  },[view?.id]);
142
142
  React.useEffect(()=>{
143
- if(!view)return;
144
- const container=document.querySelector('[data-composer-card]')?.closest('[data-phase]')||document.querySelector('[data-slot="conversation"]'),previous=container?.style.paddingRight;
145
- const adjust=()=>{if(container)container.style.paddingRight=innerWidth>=1100?(expanded?'calc(min(58vw, 860px) + 1px)':'calc(min(48vw, 640px) + 1px)'):previous||''};
146
- adjust();window.addEventListener('resize',adjust);return()=>{window.removeEventListener('resize',adjust);if(container)container.style.paddingRight=previous};
147
- },[view?.id,expanded]);
143
+ if(!view||active!==view.id)return;
144
+ const touched=new Map();
145
+ const adjust=()=>{
146
+ if(current()!==view.id)return;
147
+ const marker=[...document.querySelectorAll('[data-form-fill-session]')].find(n=>n.dataset.formFillSession===view.id);
148
+ const container=marker?document.querySelector('[data-composer-card]')?.closest('[data-phase]')||document.querySelector('[data-slot="conversation"]'):null;
149
+ for(const node of touched.keys())if(node!==container){node.removeAttribute('data-form-fill-reserve');node.style.removeProperty('--ff-panel-inset')}
150
+ if(!container)return;
151
+ if(!touched.has(container))touched.set(container,container.style.getPropertyValue('--ff-panel-inset'));
152
+ container.setAttribute('data-form-fill-reserve','true');
153
+ container.style.setProperty('--ff-panel-inset',expanded?'calc(min(58vw, 860px) + 1px)':'calc(min(48vw, 640px) + 1px)');
154
+ };
155
+ adjust();const observer=new MutationObserver(adjust);observer.observe(document.body,{childList:true,subtree:true});
156
+ 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')}};
157
+ },[view?.id,expanded,active]);
148
158
  if(!view||active!==view.id)return null;
149
159
  return portal(h('section',{className:'ff-ui ff-panel','data-ff-theme':theme(),'aria-label':'AI填表工作台',style:{width:modal?'100%':wide?(expanded?'min(58vw, 860px)':'min(48vw, 640px)'):'100%',...(modal?{background:'transparent',border:0}:{})}},
150
160
  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)},'关闭工作台'))),
package/lib/grid.js ADDED
@@ -0,0 +1,17 @@
1
+ import { parseWorkbook } from 'form-fill-core';
2
+ function integer(params,key,fallback,min,max){const raw=params.get(key);const n=raw===null?fallback:Number(raw);if(!Number.isInteger(n)||n<min||n>max)throw Error('网格分页参数无效');return n}
3
+ function columnName(column){let out='';for(let n=column;n;n=Math.floor((n-1)/26))out=String.fromCharCode(65+(n-1)%26)+out;return out}
4
+ export function gridPage(task,params){
5
+ const document=parseWorkbook(task.bytes),sheetName=params.get('sheet'),sheet=sheetName?document.sheets.find(s=>s.name===sheetName):document.sheets[0];
6
+ if(!sheet)throw Error('工作表不存在');
7
+ const view=params.get('view')||'original';if(!['original','result'].includes(view))throw Error('网格视图无效');
8
+ const page=integer(params,'page',1,1,10000),pageSize=integer(params,'pageSize',50,1,100),columnStart=integer(params,'columnStart',1,1,128),columnCount=integer(params,'columnCount',16,1,32);
9
+ const query=(params.get('q')||'').trim().toLowerCase();if(query.length>200)throw Error('搜索内容过长');
10
+ const cells=Object.values(sheet.cells),maxRow=Math.max(sheet.extent?.row||0,...sheet.rows.map(r=>r.number)),maxColumn=cells.reduce((n,c)=>Math.max(n,c.column),sheet.extent?.column||0);
11
+ const changes=new Map((view==='result'?task.preview.changeSet.changes:[]).filter(c=>c.sheet===sheet.name).map(c=>[c.cell,c]));
12
+ const byRow=new Map();for(const cell of cells){if(!byRow.has(cell.row))byRow.set(cell.row,[]);byRow.get(cell.row).push(cell)}
13
+ for(const change of changes.values())if(!sheet.cells[change.cell]){const row=Number(change.cell.match(/\d+$/)[0]);if(!byRow.has(row))byRow.set(row,[]);byRow.get(row).push({ref:change.cell,value:change.newValue})}
14
+ const rowNumbers=Array.from({length:maxRow},(_,i)=>i+1).filter(row=>!query||(byRow.get(row)||[]).some(c=>String(changes.get(c.ref)?.newValue??c.value).toLowerCase().includes(query)));
15
+ const columns=Array.from({length:Math.min(columnCount,Math.max(0,maxColumn-columnStart+1))},(_,i)=>({number:columnStart+i,label:columnName(columnStart+i),hidden:sheet.hiddenColumns.some(([a,b])=>columnStart+i>=a&&columnStart+i<=b)}));
16
+ return {sheet:sheet.name,hidden:sheet.hidden,view,revision:task.revision,sheets:document.sheets.map(s=>({name:s.name,hidden:s.hidden})),page,pageSize,totalRows:maxRow,matchingRows:rowNumbers.length,totalColumns:maxColumn,columnStart,columnCount,columns,rows:rowNumbers.slice((page-1)*pageSize,page*pageSize).map(number=>({number,hidden:sheet.rows.find(r=>r.number===number)?.hidden||false,cells:columns.map(col=>{const ref=col.label+number,c=sheet.cells[ref],change=changes.get(ref);return {ref,value:change?.newValue??c?.value??'',formula:!!c?.formula,recalculationRequired:!!c?.formula&&view==='result'&&task.preview.changeSet.changes.length>0,changed:!!change,hidden:!!c?.hidden||col.hidden}})}))};
17
+ }
package/lib/http.js CHANGED
@@ -4,25 +4,31 @@ import { applyChangeSet, parseWorkbook } from 'form-fill-core';
4
4
  import { FIELD_CATALOG, isCompleteAnchor } from 'qcc-form-fill-provider';
5
5
  import { previewBytes } from './workflow.js';
6
6
  import { createTaskStore } from './task-store.js';
7
+ import { allCandidates, selectCandidates } from './task-model.js';
8
+ import { gridPage } from './grid.js';
7
9
  const XLSX_TYPE = 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet';
8
10
  const FIXTURES = ['客户台账', '供应商准入表', '合同主体信息表'];
9
11
  export function createFormFillHandler({ basePath = '', getPort, now = Date.now, ttlMs = 15 * 60 * 1000, maxTasks = 10, taskDirectory, getQccStatus = () => false } = {}) {
10
12
  const tasks = createTaskStore({ directory: taskDirectory, maxTasks, now, ttlMs });
11
13
  let inFlight = 0, disposed = false;
12
14
  const running = new Set();
15
+ const controllers=new Map();
13
16
  const validOwner = value => typeof value === 'string' && /^[a-f0-9]{64}$/.test(value);
14
17
  const visible = (task, owner) => task && (!task.owner || task.owner === owner);
15
18
  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 });
16
19
  const settings = task => ({
17
20
  configuration: task.configuration ?? {},
18
21
  selectedFields: task.selectedFields,
22
+ progress: task.progress,
23
+ candidates: allCandidates(task).changes,
24
+ selectedIds: task.preview.changeSet.changes.map(c=>c.id),
19
25
  catalog: FIELD_CATALOG,
20
26
  structure: parseWorkbook(task.bytes).sheets.filter(s => !s.hidden).map(s => ({
21
27
  name: s.name,
22
28
  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 })) })),
23
29
  })),
24
30
  });
25
- const sweep = () => { for (const [id, task] of tasks) if (now() - task.created >= ttlMs) tasks.delete(id); };
31
+ const sweep = () => { for (const [id, task] of tasks) if (now() - task.created >= ttlMs) {controllers.get(id)?.abort();tasks.delete(id);} };
26
32
  const timer = setInterval(sweep, Math.min(ttlMs, 60000)); timer.unref();
27
33
  const handler = async (request, response) => {
28
34
  const send = (status, data, type = 'application/json') => {
@@ -53,6 +59,10 @@ export function createFormFillHandler({ basePath = '', getPort, now = Date.now,
53
59
  if (!visible(task, owner)) return send(404, { message: '任务不存在或已过期' });
54
60
  return send(200, { ...task.preview, ...metadata(id,task), ...settings(task) });
55
61
  }
62
+ if(request.method==='GET'&&path.startsWith('/grid/')){
63
+ const task=tasks.get(path.slice(6));if(!visible(task,owner))return send(404,{message:'任务不存在或已过期'});
64
+ return send(200,gridPage(task,url.searchParams));
65
+ }
56
66
  if (request.method === 'GET' && path.startsWith('/fixture/')) {
57
67
  const name = decodeURIComponent(path.slice(9));
58
68
  if (!FIXTURES.includes(name)) return send(404, { message: '模板不存在' });
@@ -66,7 +76,7 @@ export function createFormFillHandler({ basePath = '', getPort, now = Date.now,
66
76
  if (kind === 'incomplete') return send(200, task.result.incomplete);
67
77
  return send(404, { message: '制品不存在' });
68
78
  }
69
- if (request.method !== 'POST' || !['/preview', '/confirm', '/discard', '/select', '/configure', '/resolve', '/scope'].includes(path)) return send(404, { message: '路径不存在' });
79
+ if (request.method !== 'POST' || !['/preview', '/confirm', '/discard', '/select', '/configure', '/resolve', '/scope', '/cancel'].includes(path)) return send(404, { message: '路径不存在' });
70
80
  if (request.headers.origin !== origin || request.headers['content-type'] !== 'application/json') return send(403, { message: '请求需来自本页' });
71
81
  if (path === '/preview') {
72
82
  if (tasks.size + inFlight >= maxTasks) return send(429, { message: '预览数量已达上限,请先释放旧预览' });
@@ -95,6 +105,10 @@ export function createFormFillHandler({ basePath = '', getPort, now = Date.now,
95
105
  const task = tasks.get(body.id);
96
106
  if (!visible(task, owner)) return send(404, { message: '预览已过期,请重新上传' });
97
107
  if ((task.owner || body.expectedRevision !== undefined) && body.expectedRevision !== (task.revision ?? 1)) return send(409, { code: 'REVISION_CONFLICT', message: '任务已更新,请刷新后重试' });
108
+ if(path==='/cancel'){
109
+ const controller=controllers.get(body.id);if(!controller)return send(409,{message:'任务当前未运行'});
110
+ controller.abort();return send(200,{cancelRequested:true});
111
+ }
98
112
  if (running.has(body.id)) return send(409, { message: '任务正在查询,请稍后再操作' });
99
113
  if (path === '/discard') { tasks.delete(body.id); return send(200, { discarded: true }); }
100
114
  if (path === '/configure' || path === '/resolve' || path === '/scope') {
@@ -109,25 +123,22 @@ export function createFormFillHandler({ basePath = '', getPort, now = Date.now,
109
123
  const item = task.preview.changeSet.incomplete.find(i => i.sheet === body.sheet && i.row === body.row && i.reason === 'candidate-review-required');
110
124
  if (!item) return send(400, { message: '当前记录不需要候选确认' });
111
125
  const candidate = item.candidates?.find(c => c.id === body.candidateId);
112
- const value = body.candidateId ? candidate?.company_name : body.value;
126
+ const value = body.candidateId ? (isCompleteAnchor(candidate?.company_name)?candidate.company_name:candidate?.credit_no) : body.value;
113
127
  if (!isCompleteAnchor(value)) return send(400, { message: '请选择当前候选或输入完整登记名称/信用代码' });
114
128
  configuration = { ...task.configuration, anchors: [...(task.configuration?.anchors ?? []).filter(x => x.sheet !== body.sheet || x.row !== body.row), { sheet: body.sheet, row: body.row, value }] };
115
129
  }
116
130
  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' }) } } : {}) });
117
131
  if (disposed || tasks.get(body.id) !== task || running.has(body.id)) return send(409, { message: '任务已更新,请刷新后重试' });
118
- const updated = { ...task, configuration, selectedFields, preview, revision: (task.revision ?? 1)+1, updatedAt: now(), state: 'preview_ready' };
132
+ const updated = { ...task, configuration, selectedFields, preview, baseChangeSet:undefined, revision: (task.revision ?? 1)+1, updatedAt: now(), state: 'preview_ready' };
119
133
  tasks.set(body.id,updated);
120
134
  return send(200, { ...preview, ...metadata(body.id,updated), ...settings(updated) });
121
135
  }
122
136
  if (path === '/select') {
123
137
  if (task.result) return send(409, { message: '已确认任务不能修改' });
124
- const original = task.preview.changeSet, selected = body.selectedIds;
138
+ const original = allCandidates(task), selected = body.selectedIds;
125
139
  if (!Array.isArray(selected) || selected.length !== new Set(selected).size || selected.some(id => !original.changes.some(c => c.id === id))) return send(400, { message: '单元格选择无效' });
126
- const { changeSetId, ...body_ } = original;
127
- body_.changes = original.changes.filter(c => selected.includes(c.id));
128
- body_.incomplete = [...original.incomplete, ...original.changes.filter(c => !selected.includes(c.id)).map(c => ({ ...c, reason: 'user-excluded' }))];
129
- const changeSet = { ...body_, changeSetId: createHash('sha256').update(JSON.stringify(body_)).digest('hex') };
130
- const updated = { ...task, preview: { ...task.preview, changeSet }, revision: (task.revision ?? 1) + 1, updatedAt: now() };
140
+ const changeSet = selectCandidates(original,selected);
141
+ const updated = { ...task, baseChangeSet:original, preview: { ...task.preview, changeSet }, revision: (task.revision ?? 1) + 1, updatedAt: now() };
131
142
  tasks.set(body.id,updated);
132
143
  return send(200, { ...updated.preview, ...metadata(body.id,updated), ...settings(updated) });
133
144
  }
@@ -141,24 +152,32 @@ export function createFormFillHandler({ basePath = '', getPort, now = Date.now,
141
152
  };
142
153
  return {
143
154
  handler,
144
- async enrich(id, provider, expectedRevision) {
155
+ async enrich(id, provider, expectedRevision, {retryOnly=false}={}) {
145
156
  sweep();
146
157
  const task = tasks.get(id);
147
158
  if (disposed || !task || task.result || running.has(id)) throw Error('任务不存在、已确认或正在执行');
148
159
  if ((task.owner || expectedRevision !== undefined) && expectedRevision !== (task.revision ?? 1)) throw Error('REVISION_CONFLICT:任务已更新');
149
160
  running.add(id);
150
- const active = { ...task, state: 'enriching', updatedAt: now(), revision: (task.revision ?? 1) + 1 };
161
+ const controller=new AbortController();controllers.set(id,controller);
162
+ let active = { ...task, state: 'enriching', progress:{completed:0,total:0}, updatedAt: now(), revision: (task.revision ?? 1) + 1 };
163
+ const excluded=new Set(allCandidates(task).changes.filter(c=>!task.preview.changeSet.changes.some(s=>s.id===c.id)).map(c=>c.id));
164
+ const update=(preview,state,progress)=>{
165
+ if(disposed||tasks.get(id)!==active)throw Error('任务已过期或被替换');
166
+ const baseChangeSet=preview.changeSet;
167
+ const selected=selectCandidates(baseChangeSet,baseChangeSet.changes.filter(c=>!excluded.has(c.id)).map(c=>c.id));
168
+ active={...active,baseChangeSet,preview:{...preview,changeSet:selected},state,progress,updatedAt:now(),revision:active.revision+1};tasks.set(id,active);
169
+ };
151
170
  try {
152
171
  tasks.set(id, active);
153
- const preview = await previewBytes(task.bytes, { provider, confirmPaidCalls: true, configuration: task.configuration, selectedFields: task.selectedFields });
172
+ const preview = await previewBytes(task.bytes, { provider, confirmPaidCalls: true, configuration: task.configuration, selectedFields: task.selectedFields,signal:controller.signal,retryOnly,previousChangeSet:retryOnly?allCandidates(task):undefined,onProgress:async({analysis,plan,changeSet,completed,total})=>update({analysis,plan,changeSet},'enriching',{completed,total}) });
154
173
  if (disposed || tasks.get(id) !== active) throw Error('任务已过期或被替换');
155
- tasks.set(id, { ...active, preview, state: preview.changeSet.incomplete.some(i => i.reason === 'provider-error') ? 'partial' : 'preview_ready', updatedAt: now(), revision: active.revision + 1 });
156
- return { taskId: id, filled: preview.changeSet.changes.length, incomplete: preview.changeSet.incomplete.length, previewPath: basePath + '/#task=' + id };
174
+ update(preview,controller.signal.aborted?'cancelled':preview.changeSet.incomplete.some(i => i.reason === 'provider-error') ? 'partial' : 'preview_ready',active.progress);
175
+ return { taskId: id, filled: active.preview.changeSet.changes.length, incomplete: active.preview.changeSet.incomplete.length, previewPath: basePath + '/#task=' + id };
157
176
  } catch (error) {
158
177
  if (!disposed && tasks.get(id) === active) tasks.set(id, { ...active, state: 'failed', updatedAt: now(), revision: active.revision + 1 });
159
178
  throw error;
160
- } finally { running.delete(id); }
179
+ } finally { running.delete(id);controllers.delete(id); }
161
180
  },
162
- dispose() { if (disposed) return; disposed = true; clearInterval(timer); tasks.close(); },
181
+ dispose() { if (disposed) return; disposed = true;for(const c of controllers.values())c.abort(); clearInterval(timer); tasks.close(); },
163
182
  };
164
183
  }
package/lib/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { createFormFillHandler } from './http.js';
2
- import { createQccProvider, REGISTRATION_TOOL } from 'qcc-form-fill-provider';
2
+ import { createCatalogProvider, CATALOG_TOOL_DOMAINS, runtimeToolNames, REGISTRATION_TOOL, ENTITY_TOOL } from 'qcc-form-fill-provider';
3
3
  import { randomUUID } from 'node:crypto';
4
4
  import { join } from 'node:path';
5
5
  export { previewBytes, previewFile, writeCopy } from './workflow.js';
@@ -16,22 +16,22 @@ export function apply(ctx, config = {}) {
16
16
  const disposeTool = tools.register({
17
17
  name: 'form_fill_enrich',
18
18
  description: 'Use QCC to fill an uploaded AI填表 task. Invoke only when the user requests QCC enrichment. Returns counts and a preview link; the user confirms cell changes in the workbench.',
19
- parameters: { type: 'object', additionalProperties: false, properties: { taskId: { type: 'string' }, expectedRevision: { type: 'integer' } }, required: ['taskId'] },
19
+ parameters: { type: 'object', additionalProperties: false, properties: { taskId: { type: 'string' }, expectedRevision: { type: 'integer' }, mode:{type:'string',enum:['all','retry']} }, required: ['taskId'] },
20
20
  output: {
21
21
  schema: { type: 'object', properties: { taskId: { type: 'string' }, filled: { type: 'integer' }, incomplete: { type: 'integer' }, previewPath: { type: 'string' } }, required: ['taskId','filled','incomplete','previewPath'] },
22
22
  render: (_args, value) => [{ type: 'text', text: 'AI填表:可填写 ' + value.filled + ' 格,未完成 ' + value.incomplete + ' 项。预览:' + value.previewPath }],
23
23
  },
24
24
  async execute(args, execution) {
25
25
  if (!execution?.agent || !execution?.token) throw Error('需要 Agent-owned 工具执行上下文');
26
- const provider = createQccProvider({ callTool: async (name, arguments_, { signal }) => {
27
- if (name !== REGISTRATION_TOOL) throw Error('不支持的 QCC 工具');
28
- const names = ['mcp__qcc-company__', 'mcp__company__', 'mcp__qcc_company__'].map(prefix => prefix + name);
26
+ const provider = createCatalogProvider({ availableTools:Object.keys(CATALOG_TOOL_DOMAINS).filter(name=>runtimeToolNames(name).some(n=>tools.get(n))), enableEntitySearch: ['mcp__qcc-company__','mcp__company__','mcp__qcc_company__'].some(p=>tools.get(p+ENTITY_TOOL)), callTool: async (name, arguments_, { signal }) => {
27
+ if (!Object.hasOwn(CATALOG_TOOL_DOMAINS,name)) throw Error('不支持的 QCC 工具');
28
+ const names = runtimeToolNames(name);
29
29
  const selected = names.find(candidate => tools.get(candidate));
30
30
  if (!selected) throw Error('请先连接企查查企业数据 MCP');
31
31
  const result = await tools.execute({ name: selected, arguments: arguments_, signal, callId: randomUUID(), rootCallId: execution.rootCallId, parent: execution.token, agent: execution.agent });
32
32
  return result?.isError ? { isError: true } : result?.value;
33
33
  } });
34
- return service.enrich(args.taskId, provider, args.expectedRevision);
34
+ return service.enrich(args.taskId, provider, args.expectedRevision,{retryOnly:args.mode==='retry'});
35
35
  },
36
36
  });
37
37
  toolScope.effect?.(() => () => disposeTool?.());
@@ -0,0 +1,13 @@
1
+ import { createHash } from 'node:crypto';
2
+ export const sealChangeSet = ({changeSetId,...body}) => ({...body,changeSetId:createHash('sha256').update(JSON.stringify(body)).digest('hex')});
3
+ // Older selections moved excluded candidates into incomplete. Recover only actual facts.
4
+ export function allCandidates(task){
5
+ if(task.baseChangeSet)return task.baseChangeSet;
6
+ const current=task.preview.changeSet,recovered=current.incomplete.filter(c=>c.reason==='user-excluded'&&c.changeType==='fill-blank'&&typeof c.newValue==='string');
7
+ return sealChangeSet({...current,changes:[...current.changes,...recovered.map(({reason,...c})=>c)],incomplete:current.incomplete.filter(c=>!recovered.includes(c))});
8
+ }
9
+ export function selectCandidates(base,ids){
10
+ if(!Array.isArray(ids)||new Set(ids).size!==ids.length||ids.some(id=>!base.changes.some(c=>c.id===id)))throw Error('单元格选择无效');
11
+ const selected=new Set(ids);
12
+ return sealChangeSet({...base,changes:base.changes.filter(c=>selected.has(c.id)),incomplete:[...base.incomplete,...base.changes.filter(c=>!selected.has(c.id)).map(c=>({...c,reason:'user-excluded'}))]});
13
+ }
package/lib/task-store.js CHANGED
@@ -28,16 +28,17 @@ export function createTaskStore({ directory, maxTasks = 10, now = Date.now, ttlM
28
28
  const id = file.slice(0,-5), path = join(root,file);
29
29
  if (!idPattern.test(id) || lstatSync(path).isSymbolicLink() || lstatSync(path).size > 48 * 1024 * 1024) throw Error('Invalid persisted task');
30
30
  const saved = JSON.parse(readFileSync(path,'utf8'));
31
- if (![1,2,3,4].includes(saved.schema) || saved.id !== id || !Number.isFinite(saved.created)) throw Error('Invalid task schema');
31
+ if (![1,2,3,4,5].includes(saved.schema) || saved.id !== id || !Number.isFinite(saved.created)) throw Error('Invalid task schema');
32
32
  if (now() - saved.created >= ttlMs) { unlinkSync(path); continue; }
33
33
  if (tasks.size >= maxTasks) throw Error('Task capacity exceeded');
34
34
  const bytes = Buffer.from(saved.base64,'base64');
35
35
  const doc = parseWorkbook(bytes);
36
36
  if (saved.preview) {
37
37
  assertPlan(saved.preview.plan); assertChangeSet(saved.preview.changeSet);
38
+ if(saved.baseChangeSet){assertChangeSet(saved.baseChangeSet);if(saved.baseChangeSet.planId!==saved.preview.plan.planId)throw Error('Candidate plan mismatch');}
38
39
  if (saved.preview.plan.documentHash !== doc.documentHash || saved.preview.changeSet.planId !== saved.preview.plan.planId) throw Error('Persisted document mismatch');
39
40
  }
40
- const task = { bytes, preview: saved.preview, configuration: saved.configuration, selectedFields:saved.selectedFields, analyzeOnly: saved.analyzeOnly, created: saved.created, revision: saved.revision ?? 1, owner: saved.owner, filename: saved.filename ?? '未命名表格.xlsx', updatedAt: saved.updatedAt ?? saved.created, state: saved.state === 'enriching' ? 'interrupted' : saved.state ?? (saved.confirmed ? 'completed' : 'preview_ready'), sessionId: saved.sessionId };
41
+ const task = { bytes, preview: saved.preview, baseChangeSet:saved.baseChangeSet, progress:saved.progress, configuration: saved.configuration, selectedFields:saved.selectedFields, analyzeOnly: saved.analyzeOnly, created: saved.created, revision: saved.revision ?? 1, owner: saved.owner, filename: saved.filename ?? '未命名表格.xlsx', updatedAt: saved.updatedAt ?? saved.created, state: saved.state === 'enriching' ? 'interrupted' : saved.state ?? (saved.confirmed ? 'completed' : 'preview_ready'), sessionId: saved.sessionId };
41
42
  if (saved.confirmed && task.preview) task.result = applyChangeSet(bytes,task.preview.plan,task.preview.changeSet,{confirmChangeSetId:task.preview.changeSet.changeSetId});
42
43
  tasks.set(id,task);
43
44
  }
@@ -51,7 +52,7 @@ export function createTaskStore({ directory, maxTasks = 10, now = Date.now, ttlM
51
52
  if (!idPattern.test(id)) throw Error('Invalid task ID');
52
53
  if (!tasks.has(id) && tasks.size >= maxTasks) throw Error('Task capacity exceeded');
53
54
  if (root) {
54
- const json = JSON.stringify({ schema:4,id,base64:task.bytes.toString('base64'),preview:task.preview,configuration:task.configuration,selectedFields:task.selectedFields,analyzeOnly:task.analyzeOnly,created:task.created,revision:task.revision ?? 1,confirmed:!!task.result,owner:task.owner,filename:task.filename,updatedAt:task.updatedAt,state:task.state,sessionId:task.sessionId });
55
+ const json = JSON.stringify({ schema:5,id,base64:task.bytes.toString('base64'),preview:task.preview,baseChangeSet:task.baseChangeSet,progress:task.progress,configuration:task.configuration,selectedFields:task.selectedFields,analyzeOnly:task.analyzeOnly,created:task.created,revision:task.revision ?? 1,confirmed:!!task.result,owner:task.owner,filename:task.filename,updatedAt:task.updatedAt,state:task.state,sessionId:task.sessionId });
55
56
  if (Buffer.byteLength(json) > 48*1024*1024) throw Error('Task too large');
56
57
  const temp = join(root,'.'+randomUUID()+'.tmp');
57
58
  try { writeFileSync(temp,json,{mode:0o600,flag:'wx'});renameSync(temp,join(root,id+'.json')); }
package/lib/ui.html CHANGED
@@ -7,13 +7,15 @@
7
7
  <p id="status" role="status" aria-live="polite"></p>
8
8
  <section id="history" hidden><h2>本浏览器的任务</h2><p class="muted">只显示持有相同访问凭据的本地任务。过期任务自动清理。</p><div id="history-list"></div></section>
9
9
  <main id="current">
10
- <nav class="tabs" aria-label="工作流程"><button data-step="import">1 导入与识别</button><button data-step="rules">2 字段与规则</button><button data-step="identity">3 主体核验</button><button data-step="preview">4 填写预览</button><button data-step="download">5 确认与下载</button></nav>
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
- <div class="card" data-pane="import"><h2>导入表格</h2><p class="muted">支持简单 XLSX,最大 8 MiB。公式、宏、图片和区域引用下拉等复杂结构暂不支持;固定文本下拉可保留。上传仅做本地分析,不查询企查查。</p>
12
+ <div class="toolbar"><span id="execution-progress" role="status"></span><button id="cancel-run" disabled>取消查询</button><button id="retry-run" disabled>仅重试失败 / 未完成查询</button></div>
13
+ <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
+ <div class="card" data-pane="import"><h2>导入表格</h2><p class="muted">支持 XLSX,最大 8 MiB。支持普通内部公式、固定文本或区域下拉、基础条件格式及普通表格对象;宏、图片、图表及复杂扩展结构暂不支持。上传仅做本地分析,不查询企查查。</p>
13
15
  <div class="controls"><input type="file" id="file" accept=".xlsx" aria-label="选择 XLSX 文件"><button id="upload">分析表格</button></div>
14
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>
15
17
  <section id="preview" hidden>
16
- <div class="card" data-pane="rules"><h2>字段与规则</h2><p id="structure"></p><p>默认只填空白。可选择工作表、修正表头行及字段;应用设置会清除旧预览和主体确认,请重新核验。</p><label>搜索字段 <input id="mapping-search" type="search" placeholder="按原表字段名称筛选"></label><div id="mapping"></div><button id="configure" class="primary">应用字段设置</button></div>
18
+ <div class="card" data-pane="rules"><h2>字段与规则</h2><p id="structure"></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>
17
19
  <div class="card" data-pane="identity"><h2>主体核验与查询</h2><p id="connection"></p><p>候选不自动选择。查询返回歧义后,可选择返回候选或填写已核验的完整登记名称/信用代码;原表主体文字不覆盖,选择仅用于后续查询。</p><div id="entities"></div><button id="wizard-open">生成填写指令</button><p class="muted">查询不会自动发送;回填后可在原生对话框修改并发送。</p></div>
18
20
  <div class="card" data-pane="preview"><h2>填写预览</h2><p id="summary"></p><p class="muted">每格显示原值、准备填写值和来源。取消勾选后点击“应用选择”,再确认新的预览。</p>
19
21
  <div class="toolbar"><label>筛选单元格 <input id="result-search" type="search" placeholder="位置、字段或值"></label><span id="result-count" class="muted"></span></div>
@@ -34,23 +36,49 @@
34
36
  <script>
35
37
  const basePath=location.pathname.startsWith('/form-fill')?'/form-fill':'';
36
38
  document.getElementById('ff-style').href=basePath+'/brand.css';
37
- document.querySelectorAll('[data-icon="table"]').forEach(node=>{const svg=document.createElementNS('http://www.w3.org/2000/svg','svg');svg.setAttribute('viewBox','0 0 24 24');svg.setAttribute('class','ff-icon');svg.setAttribute('aria-hidden','true');const path=document.createElementNS(svg.namespaceURI,'path');path.setAttribute('d','M3 4h18v16H3zM3 9h18M9 9v11');svg.append(path);node.append(svg)});
39
+ const stageIcons={table:'M3 4h18v16H3zM3 9h18M9 9v11',upload:'M12 16V3m-4 4 4-4 4 4M4 14v6h16v-6',settings:'M4 6h16M4 12h16M4 18h16M8 3v6M16 9v6M10 15v6',identity:'M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2M9 11a4 4 0 1 0 0-8 4 4 0 0 0 0 8m7 1 2 2 4-4',download:'M12 3v13m-4-4 4 4 4-4M4 17v4h16v-4'};
40
+ document.querySelectorAll('[data-icon]').forEach(node=>{const d=stageIcons[node.dataset.icon];if(!d)return;const svg=document.createElementNS('http://www.w3.org/2000/svg','svg');svg.setAttribute('viewBox','0 0 24 24');svg.setAttribute('class','ff-icon');svg.setAttribute('aria-hidden','true');const path=document.createElementNS(svg.namespaceURI,'path');path.setAttribute('d',d);svg.append(path);node.append(svg)});
38
41
  const $=id=>document.getElementById(id);
39
42
  let owner=localStorage.getItem('ff-owner');
40
43
  if(!/^[a-f0-9]{64}$/.test(owner||'')){owner=[...crypto.getRandomValues(new Uint8Array(32))].map(n=>n.toString(16).padStart(2,'0')).join('');localStorage.setItem('ff-owner',owner)}
41
44
  document.cookie='ff_owner='+owner+'; SameSite=Strict; Path='+(basePath||'/');
42
- let current,filename='已填表格',busy=false,step='import',pendingNavigation,wizardStep=0,wizardFields=new Set(),wizardTask,wizardStructure,expectedDraft;
45
+ let current,filename='已填表格',busy=false,step='import',pendingNavigation,wizardStep=0,wizardFields=new Set(),wizardTask,wizardStructure,expectedDraft,retryIntent=false;
43
46
  const sessionId=new URLSearchParams(location.search).get('session')||undefined;
44
- const reasons={'unknown-field':'暂无可用字段映射','no-capability':'尚未查询或此字段暂无来源','provider-unavailable':'尚未查询或此字段暂无来源','no-match':'未匹配到企业','candidate-review-required':'主体需要人工确认','validation-conflict':'返回值不在原表下拉选项中,请人工核验','sheet-excluded':'工作表未选择','no-data':'该字段未返回','provider-error':'查询失败或未连接,请稍后重试','user-excluded':'已按你的选择排除','unsafe-value':'值不安全,已阻止填写','placeholder-needs-confirmation':'已有占位内容,保持原样'};
45
- function controls(value){busy=value;document.querySelectorAll('button').forEach(b=>b.disabled=value);if($('wizard').open)renderWizard()}
47
+ let gridPageNumber=1,gridColumn=1,gridRequest=0,gridTask;
48
+ async function loadGrid(){
49
+ if(!current||!$('grid').open)return;const request=++gridRequest,id=current.id;
50
+ try{const p=new URLSearchParams({view:$('grid-view').value,page:gridPageNumber,columnStart:gridColumn,q:$('grid-query').value});if($('grid-sheet').value)p.set('sheet',$('grid-sheet').value);
51
+ const data=await api('/grid/'+id+'?'+p);if(request!==gridRequest||id!==current?.id)return;
52
+ $('grid-sheet').replaceChildren(...data.sheets.map(s=>{const o=document.createElement('option');o.value=s.name;o.textContent=s.name+(s.hidden?'(隐藏)':'');o.selected=s.name===data.sheet;return o}));
53
+ $('grid-head').replaceChildren();const tr=document.createElement('tr');for(const label of ['行号',...data.columns.map(c=>c.label+(c.hidden?'(隐藏)':''))]){const th=document.createElement('th');th.textContent=label;tr.append(th)}$('grid-head').append(tr);
54
+ $('grid-body').replaceChildren();for(const r of data.rows){const row=document.createElement('tr');const no=document.createElement('td');no.textContent=r.number+(r.hidden?'(隐藏)':'');row.append(no);for(const c of r.cells){const td=document.createElement('td');td.textContent=c.recalculationRequired?'待 Excel 重算':c.value;td.title=c.ref+(c.formula?' · 公式单元格':'')+(c.changed?' · 准备填写':'');if(c.changed)td.className='grid-changed';row.append(td)}$('grid-body').append(row)}
55
+ $('grid-status').textContent='共 '+data.totalRows+' 行 / '+data.totalColumns+' 列,匹配 '+data.matchingRows+' 行;第 '+data.page+' / '+Math.max(1,Math.ceil(data.matchingRows/data.pageSize))+' 页;列 '+data.columnStart+'–'+Math.min(data.totalColumns,data.columnStart+data.columnCount-1);
56
+ $('grid-prev').disabled=data.page<=1;$('grid-next').disabled=data.page*data.pageSize>=data.matchingRows;$('grid-left').disabled=gridColumn===1;$('grid-right').disabled=gridColumn+16>data.totalColumns;
57
+ }catch(e){$('grid-status').textContent=e.message}
58
+ }
59
+ $('grid').ontoggle=()=>loadGrid();$('grid-search').onclick=()=>{gridPageNumber=1;loadGrid()};$('grid-query').onkeydown=e=>{if(e.key==='Enter'&&!e.isComposing){e.preventDefault();$('grid-search').click()}};
60
+ for(const id of ['grid-sheet','grid-view'])$(id).onchange=()=>{gridPageNumber=1;gridColumn=1;loadGrid()};
61
+ for(const [id,delta] of [['grid-prev',-1],['grid-next',1]])$(id).onclick=()=>{gridPageNumber=Math.max(1,gridPageNumber+delta);loadGrid()};
62
+ for(const [id,delta] of [['grid-left',-16],['grid-right',16]])$(id).onclick=()=>{gridColumn=Math.max(1,gridColumn+delta);loadGrid()};
63
+ const reasons={'unknown-field':'暂无可用字段映射','no-capability':'尚未查询或此字段暂无来源','provider-unavailable':'尚未查询或此字段暂无来源','no-match':'未匹配到企业','candidate-review-required':'主体需要人工确认','validation-conflict':'返回值不在原表下拉选项中,请人工核验','protected-source-range':'下拉来源、表格表头或汇总区域,保持原样','sheet-excluded':'工作表未选择','no-data':'该字段未返回','provider-error':'查询失败或未连接,请稍后重试','cancelled':'查询已取消,可显式重试','not-started':'尚未执行,可显式重试','user-excluded':'已按你的选择排除','unsafe-value':'值不安全,已阻止填写','placeholder-needs-confirmation':'已有占位内容,保持原样'};
64
+ function controls(value){busy=value;document.querySelectorAll('button').forEach(b=>b.disabled=value);if($('wizard').open)renderWizard();syncActions();if(!value)loadGrid()}
46
65
  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}
66
+ function syncActions(){
67
+ const running=current?.state==='enriching';$('cancel-run').hidden=!running;$('cancel-run').disabled=busy||!running;
68
+ $('retry-run').hidden=!current||current.confirmed||!current.changeSet.incomplete.some(i=>['provider-error','cancelled','not-started'].includes(i.reason));
69
+ $('retry-run').disabled=busy||!current||current.confirmed||running||!current.changeSet.incomplete.some(i=>['provider-error','cancelled','not-started'].includes(i.reason));
70
+ $('execution-progress').textContent=running?(current.progress?.total?'正在查询 '+(current.progress?.completed||0)+' / '+current.progress.total:'正在准备查询…'):current?.state==='cancelled'?'已取消,成功结果保留。':current?.state==='interrupted'?'上次查询中断,已保存进度,可重试剩余部分。':'';
71
+ for(const id of ['confirm','selection','configure'])$(id).disabled=busy||!current||current.confirmed||running;
72
+ }
73
+ $('cancel-run').onclick=async()=>{try{await api('/cancel',{id:current.id});$('status').textContent='已请求取消。已发出的请求可能已在远端执行,本地不再继续后续查询。';await restore()}catch(e){$('status').textContent=e.message}};
74
+ $('retry-run').onclick=()=>{openWizard(true);wizardStep=3;renderWizard()};
47
75
  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}
48
76
  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)))}
49
77
  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)}}
50
- function businessSummary(){return current?'填写文件“'+current.filename+'”中的空白字段。工作表:'+current.analysis.tables.map(t=>t.sheet).join('、')+'。填写字段:'+current.catalog.filter(f=>wizardFields.has(f.key)).map(f=>f.label).join('、')+'。保留原值,核验预览后生成新副本。':'请先上传 XLSX 文件。'}
51
- function command(){return current?businessSummary()+'\n'+$('requirements').value+'\n执行关联:调用 form_fill_enrich:taskId='+current.id+',expectedRevision='+current.revision+'。字段范围以工作台确认设置为准。':''}
78
+ 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 command(){return current?businessSummary()+'\n'+$('requirements').value+'\n执行关联:调用 form_fill_enrich:taskId='+current.id+',expectedRevision='+current.revision+(retryIntent?',mode=retry':'')+'。字段范围以工作台确认设置为准。':''}
52
80
  function saveWizard(){if(current)sessionStorage.setItem('ff-wizard:'+current.id,JSON.stringify({fields:[...wizardFields],requirements:$('requirements').value,step:wizardStep}))}
53
- function initWizard(){const structure=JSON.stringify(current?.analysis.tables);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&&m.field!=='company_name').map(m=>m.field))||[];let saved;try{saved=JSON.parse(sessionStorage.getItem('ff-wizard:'+current?.id))}catch{}wizardFields=new Set((saved?.fields||current?.selectedFields||available).filter(k=>available.includes(k)));$('requirements').value=saved?.requirements||'';wizardStep=saved?.step||0}
81
+ function initWizard(){const structure=JSON.stringify(current?.analysis.tables);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{}wizardFields=new Set((saved?.fields||current?.selectedFields||available).filter(k=>available.includes(k)));$('requirements').value=saved?.requirements||'';wizardStep=saved?.step||0}
54
82
  function renderWizard(){
55
83
  initWizard();
56
84
  document.querySelectorAll('[data-wizard-pane]').forEach(p=>p.setAttribute('aria-current',String(Number(p.dataset.wizardPane)===wizardStep)));
@@ -59,28 +87,30 @@ function renderWizard(){
59
87
  $('wizard-rules').textContent=current?current.analysis.tables.map(t=>t.sheet+':第 '+t.headerRow+' 行表头').join(';'):'请先上传并识别表格。';
60
88
  $('wizard-fields').replaceChildren();
61
89
  const available=new Set(current?.analysis.tables.flatMap(t=>t.mappings.map(m=>m.field))||[]);
90
+ const fillable=new Set(current?.analysis.tables.flatMap(t=>t.mappings.filter(m=>m.field&&!t.anchors.includes(m.field)).map(m=>m.field))||[]);
62
91
  for(const field of current?.catalog||[]){
63
- const label=document.createElement('label');label.hidden=!field.label.includes($('field-search').value.trim());const input=document.createElement('input');input.type='checkbox';input.checked=field.anchor||wizardFields.has(field.key);input.disabled=field.anchor||!available.has(field.key);input.dataset.field=field.key;
92
+ 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;
64
93
  input.onchange=()=>{if(input.checked)wizardFields.add(field.key);else wizardFields.delete(field.key);saveWizard();renderWizard()};
65
- label.append(input,document.createTextNode(field.label+(field.anchor?'(固定)':!available.has(field.key)?'(原表未映射)':'')));$('wizard-fields').append(label);
94
+ label.append(input,document.createTextNode(field.label+(fixed?'(固定)':!available.has(field.key)?'(原表未映射)':'')));$('wizard-fields').append(label);
66
95
  }
67
96
  $('field-count').textContent='已选 '+wizardFields.size+' 个填写字段';
68
97
  $('business-summary').textContent=businessSummary();$('qcc-command').textContent=command();
69
98
  $('wizard-prev').disabled=wizardStep===0||busy;$('wizard-next').hidden=wizardStep===3;$('wizard-next').disabled=!current||busy;
70
99
  $('draft').hidden=wizardStep!==3;$('copy-command').hidden=wizardStep!==3;$('draft').disabled=!current||!wizardFields.size||busy;$('copy-command').disabled=$('draft').disabled;
71
100
  }
72
- function openWizard(){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)}}
101
+ 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)}}
73
102
  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()}
74
103
  async function applyScope(){
75
104
  if(!current||!wizardFields.size)throw Error('请至少选择一个已映射的填写字段');
76
- const original=current.selectedFields||[...new Set(current.analysis.tables.flatMap(t=>t.mappings.filter(m=>m.field&&m.field!=='company_name').map(m=>m.field)))];
105
+ 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)))];
106
+ if(retryIntent&&JSON.stringify([...original].sort())!==JSON.stringify([...wizardFields].sort()))throw Error('重试时请保留原字段范围;如需更改,请重新生成完整核验指令。');
77
107
  if(JSON.stringify([...original].sort())!==JSON.stringify([...wizardFields].sort()))render(await api('/scope',{id:current.id,selectedFields:[...wizardFields]}));
78
108
  }
79
109
  async function sendDraft(mode){
80
110
  if(busy)return;
81
111
  if(parent===window){$('wizard-message').textContent='独立页面请复制指令到 DSH 对话。';return}
82
112
  controls(true);$('wizard-message').textContent='';
83
- try{await applyScope();saveWizard();parent.postMessage({type:'ff-draft',taskId:current.id,revision:current.revision,requirements:$('requirements').value,summary:businessSummary(),mode,expectedDraft},location.origin)}catch(e){$('wizard-message').textContent=e.message}finally{controls(false)}
113
+ 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)}
84
114
  }
85
115
  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?' · 无匹配结果':''):'暂无可填写单元格'}
86
116
  function renderSettings(data){
@@ -110,7 +140,7 @@ function renderSettings(data){
110
140
  const seen=new Set();
111
141
  for(const item of data.changeSet.incomplete.filter(i=>i.reason==='candidate-review-required')){
112
142
  const key=JSON.stringify([item.sheet,item.row]);if(seen.has(key))continue;seen.add(key);
113
- const box=document.createElement('fieldset'),legend=document.createElement('legend');legend.textContent=item.sheet+' 第 '+item.row+' 行 · '+(item.anchor?.company_name||'');box.append(legend);
143
+ const box=document.createElement('fieldset'),legend=document.createElement('legend');legend.textContent=item.sheet+' 第 '+item.row+' 行 · '+(item.anchor?.company_name||item.anchor?.credit_no||'');box.append(legend);
114
144
  const choice=document.createElement('select');choice.setAttribute('aria-label','候选 '+item.sheet+' '+item.row);
115
145
  const blank=document.createElement('option');blank.value='';blank.textContent='请选择候选(不自动选择)';choice.append(blank);
116
146
  for(const c of item.candidates||[]){const option=document.createElement('option');option.value=c.id;option.textContent=c.company_name+' '+c.credit_no;choice.append(option)}
@@ -122,6 +152,7 @@ function renderSettings(data){
122
152
  }
123
153
  function render(data){
124
154
  current=data;filename=(data.filename||'已填表格.xlsx').replace(/\.xlsx$/i,'');location.hash='task='+data.id;
155
+ if(gridTask!==data.id){gridTask=data.id;gridPageNumber=1;gridColumn=1;$('grid-sheet').replaceChildren();$('grid-query').value=''}$('grid').hidden=false;loadGrid();
125
156
  if(parent!==window)parent.postMessage({type:'ff-task',taskId:data.id},location.origin);
126
157
  $('changes').replaceChildren();$('incomplete').replaceChildren();$('downloads').replaceChildren();
127
158
  $('task-meta').dataset.state=data.state;
@@ -129,29 +160,33 @@ $('task-meta').textContent=(data.filename||'表格')+' · '+({'enriching':'查
129
160
  $('summary').textContent='可填写 '+data.changeSet.changes.length+' 格 · '+(data.plan.paidCalls?'企查查来源':'本地分析 / 合成示例 · 费用 0')+' · 未完成 '+data.changeSet.incomplete.length+' 项';
130
161
  $('structure').textContent=data.analysis.tables.map(t=>t.sheet+':表头第 '+t.headerRow+' 行').join(';');
131
162
  renderSettings(data);
132
- for(const c of 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=true;check.value=c.id;check.disabled=data.confirmed;check.setAttribute('aria-label','填写 '+c.cell);td.append(check);tr.append(td)}
163
+ 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)}
133
164
  for(const i of data.changeSet.incomplete)row('incomplete',[i.sheet+' '+(i.cell||''),i.label||'—',reasons[i.reason]||'此项需要人工处理'],['位置','字段','原因']);
134
165
  $('preview').hidden=false;if(data.confirmed)downloads();$('qcc-command').textContent=command();
135
- initWizard();filterResults();if($('wizard').open)renderWizard();
166
+ initWizard();filterResults();syncActions();if($('wizard').open)renderWizard();
136
167
  }
137
168
  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}}
138
169
  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));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)}}
139
170
  document.querySelectorAll('[data-step]').forEach(b=>b.onclick=()=>navigate(b.dataset.step));
140
171
  $('upload').onclick=async()=>{const f=$('file').files[0];if(!f){$('status').textContent='请先选择 XLSX 文件';return}await preview(await f.arrayBuffer(),f.name)};
141
- $('configure').onclick=async()=>{if(!current||busy)return;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})}controls(true);try{render(await api('/configure',{id:current.id,configuration}));$('status').textContent='字段设置已应用,旧预览已清除,请重新核验。'}catch(e){$('status').textContent=e.message}finally{controls(false)}};
172
+ 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}
173
+ 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}))) }
174
+ $('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='浏览器无法保存字段规则。'}};
175
+ $('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)}};
176
+ $('configure').onclick=async()=>{if(!current||busy)return;const configuration=readMapping();controls(true);try{render(await api('/configure',{id:current.id,configuration}));$('status').textContent='字段设置已应用,旧预览已清除,请重新核验。'}catch(e){$('status').textContent=e.message}finally{controls(false)}};
142
177
  $('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}};
143
178
  $('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)}};
144
179
  $('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)}};
145
180
  $('refresh').onclick=()=>restore();
146
- $('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;navigate('import');$('status').textContent='任务及保存的表格已删除。'}catch(e){$('status').textContent=e.message}finally{controls(false)}};
181
+ $('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)}};
147
182
  $('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}};
148
183
  $('current-tab').onclick=()=>navigate(step);
149
- $('wizard-open').onclick=openWizard;$('wizard-close').onclick=()=>closeWizard();$('wizard').addEventListener('cancel',e=>{e.preventDefault();closeWizard()});
184
+ $('wizard-open').onclick=()=>openWizard();$('wizard-close').onclick=()=>closeWizard();$('wizard').addEventListener('cancel',e=>{e.preventDefault();closeWizard()});
150
185
  document.querySelectorAll('[data-wizard-step]').forEach(b=>b.onclick=()=>{wizardStep=Number(b.dataset.wizardStep);saveWizard();renderWizard()});
151
186
  $('wizard-prev').onclick=()=>{wizardStep=Math.max(0,wizardStep-1);saveWizard();renderWizard()};$('wizard-next').onclick=()=>{wizardStep=Math.min(3,wizardStep+1);saveWizard();renderWizard()};
152
187
  $('wizard-file').onchange=async()=>{const file=$('wizard-file').files[0];if(file){await preview(await file.arrayBuffer(),file.name);renderWizard()}};
153
188
  $('wizard-edit-rules').onclick=()=>{closeWizard();navigate('rules')};
154
- $('field-search').oninput=renderWizard;$('fields-all').onclick=()=>{wizardFields=new Set(current?.analysis.tables.flatMap(t=>t.mappings.filter(m=>m.field&&m.field!=='company_name').map(m=>m.field))||[]);saveWizard();renderWizard()};$('fields-clear').onclick=()=>{wizardFields.clear();saveWizard();renderWizard()};
189
+ $('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()};
155
190
  $('requirements').oninput=()=>{saveWizard();$('qcc-command').textContent=command()};
156
191
  $('copy-command').onclick=async()=>{try{await applyScope();saveWizard();await navigator.clipboard.writeText(command());$('status').textContent='已复制,请在 DSH 对话中发送。'}catch(e){$('status').textContent=e.message}};
157
192
  $('draft').onclick=()=>sendDraft();
package/lib/workflow.js CHANGED
@@ -3,7 +3,7 @@ import { join, resolve, dirname, basename } from 'node:path';
3
3
  import { analyzeDocument, buildFillPlan, executePlan, applyChangeSet, serialize, FillError } from 'form-fill-core';
4
4
  import { createMockProvider, FIELD_CATALOG } from 'qcc-form-fill-provider';
5
5
 
6
- export async function previewBytes(bytes, { provider = createMockProvider(), maxCalls = 100, confirmPaidCalls = false, configuration = {}, selectedFields } = {}) {
6
+ export async function previewBytes(bytes, { provider = createMockProvider(), maxCalls = 100, confirmPaidCalls = false, configuration = {}, selectedFields, signal, onProgress, previousChangeSet, retryOnly } = {}) {
7
7
  if (provider.mode !== 'mock' && (provider.mode !== 'qcc' || confirmPaidCalls !== true)) throw new FillError('REAL_PROVIDER_DISABLED', '真实来源需要调用方明确授权');
8
8
  const { analysis } = analyzeDocument(bytes, FIELD_CATALOG, configuration);
9
9
  if (selectedFields !== undefined) {
@@ -12,7 +12,7 @@ export async function previewBytes(bytes, { provider = createMockProvider(), max
12
12
  analysis.opportunities=analysis.opportunities.filter(o=>selectedFields.includes(o.field));
13
13
  }
14
14
  const plan = buildFillPlan(analysis, provider.capabilities, provider.version);
15
- const changeSet = await executePlan(plan, provider, { maxCalls, confirmPaidCalls });
15
+ const changeSet = await executePlan(plan, provider, { maxCalls, confirmPaidCalls, signal, onProgress:onProgress?p=>onProgress({...p,analysis,plan}):undefined, previousChangeSet, retryOnly });
16
16
  return { analysis, plan, changeSet };
17
17
  }
18
18
  export async function previewFile(path, options) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-form-fill-agent",
3
- "version": "0.1.0-alpha.7",
3
+ "version": "0.2.0",
4
4
  "private": false,
5
5
  "description": "AI填表智能体:XLSX blank-cell preview, QCC enrichment and persistent tasks for DSH.",
6
6
  "repository": { "type": "git", "url": "git+https://github.com/duhu2000/dsh-form-fill-agent.git", "directory": "packages/dsh-form-fill-agent" },
@@ -24,8 +24,8 @@
24
24
  "node": ">=22"
25
25
  },
26
26
  "dependencies": {
27
- "form-fill-core": "0.1.0-alpha.5",
28
- "qcc-form-fill-provider": "0.1.0-alpha.5"
27
+ "form-fill-core": "0.2.0",
28
+ "qcc-form-fill-provider": "0.2.0"
29
29
  },
30
30
  "dsh": {
31
31
  "client": { "inject": ["@deepseek-ai/dsh-client-ui-layout", "@deepseek-ai/dsh-client-ui-conversation"], "platform": "web" },