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 +6 -6
- package/lib/brand.css +9 -1
- package/lib/client.js +16 -6
- package/lib/grid.js +17 -0
- package/lib/http.js +36 -17
- package/lib/index.js +6 -6
- package/lib/task-model.js +13 -0
- package/lib/task-store.js +4 -3
- package/lib/ui.html +57 -22
- package/lib/workflow.js +2 -2
- package/package.json +3 -3
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
|
|
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
|
|
46
|
+
补充要求为模型可读文本;字段或工作表范围请通过向导/工作台修改,不能仅改文字后假定执行范围已同步。任务 schema 5 可读取 schema 1–5;回退旧版本请使用升级前目录副本或独立任务目录。共享内核、Provider 和主包为 0.2.0。
|
|
47
47
|
|
|
48
|
-
支持企业完整登记名称或 18
|
|
48
|
+
支持企业完整登记名称或 18 位信用代码;简称、主体不一致或多候选不会自动猜选。仅有信用代码的表格也可作为查询锚点。连接候选检索工具时可检索简称,仍需人工确认;缺少工具时降级人工输入。字段映射采用确定性规则及人工确认,不让模型编造事实。支持普通内部公式、固定区域下拉、基础条件格式和普通表格对象;复杂 Excel 结构和 Word 暂不支持,保真边界见下方安全说明。
|
|
49
49
|
|
|
50
50
|
真实查询使用用户连接的企查查 MCP 账号,积分规则以该账号为准。模型服务也需单独配置。
|
|
51
51
|
|
|
52
52
|
显式设置 DSH_HOME 时,任务保存至其 form-fill-tasks 目录,默认 24 小时过期;未设置时使用内存。任务保存输入表格和归一化预览,不持久化原始 MCP 响应,可主动删除任务。模型及 MCP 凭据由宿主管理。
|
|
53
53
|
|
|
54
|
-
|
|
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
|
|
145
|
-
const adjust=()=>{
|
|
146
|
-
|
|
147
|
-
|
|
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
|
|
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
|
|
127
|
-
|
|
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
|
|
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
|
-
|
|
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 {
|
|
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 =
|
|
27
|
-
if (name
|
|
28
|
-
const names =
|
|
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:
|
|
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="
|
|
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="
|
|
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
|
-
|
|
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
|
-
|
|
45
|
-
function
|
|
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
|
|
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';
|
|
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+(
|
|
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
|
|
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=
|
|
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
|
-
|
|
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
|
|
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.
|
|
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.
|
|
28
|
-
"qcc-form-fill-provider": "0.
|
|
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" },
|