dsh-form-fill-agent 0.2.2 → 0.2.3
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 +2 -2
- package/lib/brand.css +5 -0
- package/lib/diagnostics.js +47 -0
- package/lib/http.js +5 -2
- package/lib/index.js +3 -2
- package/lib/ui.html +60 -14
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -43,7 +43,7 @@ dsh plugin --profile web add dsh-form-fill-agent@latest
|
|
|
43
43
|
|
|
44
44
|
## 使用边界与数据保存
|
|
45
45
|
|
|
46
|
-
补充要求为模型可读文本;字段或工作表范围请通过向导/工作台修改,不能仅改文字后假定执行范围已同步。任务 schema 5 可读取 schema 1–5;回退旧版本请使用升级前目录副本或独立任务目录。主包为 0.2.
|
|
46
|
+
补充要求为模型可读文本;字段或工作表范围请通过向导/工作台修改,不能仅改文字后假定执行范围已同步。任务 schema 5 可读取 schema 1–5;回退旧版本请使用升级前目录副本或独立任务目录。主包为 0.2.3,共享内核和 Provider 保持 0.2.0。
|
|
47
47
|
|
|
48
48
|
支持企业完整登记名称或 18 位信用代码;简称、主体不一致或多候选不会自动猜选。仅有信用代码的表格也可作为查询锚点。连接候选检索工具时可检索简称,仍需人工确认;缺少工具时降级人工输入。字段映射采用确定性规则及人工确认,不让模型编造事实。支持普通内部公式、固定区域下拉、基础条件格式和普通表格对象;复杂 Excel 结构和 Word 暂不支持,保真边界见下方安全说明。
|
|
49
49
|
|
|
@@ -51,7 +51,7 @@ dsh plugin --profile web add dsh-form-fill-agent@latest
|
|
|
51
51
|
|
|
52
52
|
显式设置 DSH_HOME 时,任务保存至其 form-fill-tasks 目录,默认 24 小时过期;未设置时使用内存。任务保存输入表格和归一化预览,不持久化原始 MCP 响应,可主动删除任务。模型及 MCP 凭据由宿主管理。
|
|
53
53
|
|
|
54
|
-
0.2.
|
|
54
|
+
0.2.3 保存任务 schema 5,兼容读取旧 schema 1–4;回退旧版本须使用升级前任务目录副本或新目录,防止丢失已确认的字段/主体设置。
|
|
55
55
|
|
|
56
56
|
## 文档与反馈
|
|
57
57
|
|
package/lib/brand.css
CHANGED
|
@@ -13,6 +13,11 @@
|
|
|
13
13
|
.ff-ui .ff-panel-resize:hover:after,.ff-ui .ff-panel-resize:focus-visible:after,.ff-ui.ff-resizing .ff-panel-resize:after{background:var(--action)}
|
|
14
14
|
.ff-ui.ff-resizing{user-select:none;cursor:col-resize}.ff-ui.ff-resizing iframe{pointer-events:none}
|
|
15
15
|
.ff-ui.ff-panel{container-type:inline-size}
|
|
16
|
+
.ff-ui .ff-mapping-row{display:grid;gap:8px;padding:14px 0;border-bottom:1px solid var(--line);min-width:0}
|
|
17
|
+
.ff-ui .ff-mapping-row>select{width:100%}.ff-ui .ff-mapping-status{margin:0;font-size:12px;color:var(--muted)}
|
|
18
|
+
.ff-ui .ff-mapping-status[data-unmatched=true]{color:var(--review)}
|
|
19
|
+
.ff-ui .ff-mapping-filters{display:flex;gap:8px;flex-wrap:wrap}.ff-ui .ff-mapping-filters input[type=search]{flex:1 1 180px;min-width:0}.ff-ui .ff-mapping-filters select{flex:1 1 150px;max-width:100%}
|
|
20
|
+
.ff-ui .ff-mapping-suggestions{display:flex;gap:6px;align-items:center;flex-wrap:wrap;font-size:12px}.ff-ui .ff-mapping-suggestions:empty{display:none}
|
|
16
21
|
@container(max-width:480px){.ff-ui .ff-panel-head{flex-wrap:wrap;padding:10px}.ff-ui .ff-panel-actions{margin-left:auto}.ff-ui .ff-panel-head h2{font-size:14px}}
|
|
17
22
|
.ff-ui.ff-panel{position:fixed;right:0;top:0;bottom:0;z-index:1000;background:var(--surface);display:flex;flex-direction:column;border-left:1px solid var(--line)}.ff-ui .ff-panel-head{display:flex;align-items:center;justify-content:space-between;gap:8px;padding:12px 16px;border-bottom:1px solid var(--line);flex-shrink:0}.ff-ui .ff-panel-head .ff-mark{width:32px;height:32px}.ff-ui .ff-panel-head .ff-mark svg{width:20px;height:20px}.ff-ui .ff-panel-head h2{font-size:16px}.ff-ui .ff-panel-actions{display:flex;gap:6px}.ff-ui.ff-panel iframe{border:0;width:100%;flex:1;min-height:0}
|
|
18
23
|
.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)}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
export const REASON_LABELS = {
|
|
2
|
+
'missing-anchor':'缺少可用主体标识,请修正名称或信用代码映射',
|
|
3
|
+
'missing-anchor-field':'主体标识为空',
|
|
4
|
+
'unknown-field':'字段尚未映射',
|
|
5
|
+
'user-excluded':'未纳入本次字段范围或已选择不填写',
|
|
6
|
+
'provider-unavailable':'当前无可用数据来源',
|
|
7
|
+
'candidate-review-required':'需要人工确认主体候选',
|
|
8
|
+
'no-match':'数据来源未匹配到主体',
|
|
9
|
+
'no-data':'查询成功,但该字段未返回数据',
|
|
10
|
+
'provider-error':'调用失败,请检查连接或重试',
|
|
11
|
+
'cancelled':'查询已取消',
|
|
12
|
+
'not-started':'尚未执行',
|
|
13
|
+
'ambiguous-source':'存在多个数据来源,需明确来源',
|
|
14
|
+
'formula-preserved':'公式保留,不填写',
|
|
15
|
+
'error-cell':'原表错误单元格保留',
|
|
16
|
+
'hidden-or-merged':'隐藏或合并区域保留',
|
|
17
|
+
'hidden-row':'隐藏行保留',
|
|
18
|
+
'sheet-excluded':'工作表未选择',
|
|
19
|
+
'protected-source-range':'下拉来源或表格保留区域,不填写',
|
|
20
|
+
'placeholder-needs-confirmation':'原有占位内容需确认',
|
|
21
|
+
'low-confidence':'返回数据置信度不足',
|
|
22
|
+
'missing-provenance':'缺少来源或获取时间',
|
|
23
|
+
'invalid-value':'返回值类型无效',
|
|
24
|
+
'unsafe-value':'返回值未通过安全检查',
|
|
25
|
+
'validation-conflict':'返回值不符合原表下拉规则',
|
|
26
|
+
};
|
|
27
|
+
export function diagnostics(preview) {
|
|
28
|
+
const reasons=new Map(),columns=new Map();
|
|
29
|
+
for(const table of preview.analysis.tables)for(const m of table.mappings)columns.set(table.sheet+':'+m.column,{sheet:table.sheet,column:m.column,label:m.label,field:m.field,ready:0,incomplete:0});
|
|
30
|
+
for(const item of preview.changeSet.incomplete){
|
|
31
|
+
const key=item.reason;
|
|
32
|
+
if(!reasons.has(key))reasons.set(key,{code:key,label:REASON_LABELS[key]||'保留原值或需要人工核验',count:0,unit:key==='missing-anchor'?'行':'项'});
|
|
33
|
+
reasons.get(key).count++;
|
|
34
|
+
}
|
|
35
|
+
for(const [items,key] of [[preview.changeSet.changes,'ready'],[preview.changeSet.incomplete,'incomplete']])for(const item of items){
|
|
36
|
+
if(!item.cell)continue;
|
|
37
|
+
let column=0;for(const c of item.cell.match(/^[A-Z]+/)?.[0]||'')column=column*26+c.charCodeAt(0)-64;
|
|
38
|
+
const value=columns.get(item.sheet+':'+column);if(value)value[key]++;
|
|
39
|
+
}
|
|
40
|
+
return {reasons:[...reasons.values()],columns:[...columns.values()],plannedFields:[...new Set(preview.plan.calls.flatMap(c=>c.fields))]};
|
|
41
|
+
}
|
|
42
|
+
export function renderEnrichmentResult(value) {
|
|
43
|
+
const reasons=value.diagnostics?.reasons||[];
|
|
44
|
+
return 'AI填表:可填写 '+value.filled+' 格,未完成 '+value.incomplete+' 项。'
|
|
45
|
+
+(reasons.length?'原因分类:'+reasons.map(r=>r.label+' '+r.count+r.unit).join(';')+'。':'')
|
|
46
|
+
+'可填写仅代表预览,尚未写入副本;必须在工作台确认后下载。未完成包含范围排除、映射或查询问题,不可统一解释为企查查查不到。预览:'+value.previewPath;
|
|
47
|
+
}
|
package/lib/http.js
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { randomUUID, createHash } from 'node:crypto';
|
|
2
2
|
import { readFile } from 'node:fs/promises';
|
|
3
3
|
import { applyChangeSet, parseWorkbook } from 'form-fill-core';
|
|
4
|
-
import { FIELD_CATALOG, isCompleteAnchor } from 'qcc-form-fill-provider';
|
|
4
|
+
import { FIELD_CATALOG, QCC_FIELD_CATALOG, isCompleteAnchor } from 'qcc-form-fill-provider';
|
|
5
5
|
import { previewBytes } from './workflow.js';
|
|
6
6
|
import { createTaskStore } from './task-store.js';
|
|
7
7
|
import { allCandidates, selectCandidates } from './task-model.js';
|
|
8
8
|
import { gridPage } from './grid.js';
|
|
9
|
+
import { diagnostics } from './diagnostics.js';
|
|
9
10
|
const XLSX_TYPE = 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet';
|
|
10
11
|
const FIXTURES = ['客户台账', '供应商准入表', '合同主体信息表'];
|
|
11
12
|
export function createFormFillHandler({ basePath = '', getPort, now = Date.now, ttlMs = 15 * 60 * 1000, maxTasks = 10, taskDirectory, getQccStatus = () => false } = {}) {
|
|
@@ -17,12 +18,14 @@ export function createFormFillHandler({ basePath = '', getPort, now = Date.now,
|
|
|
17
18
|
const visible = (task, owner) => task && (!task.owner || task.owner === owner);
|
|
18
19
|
const metadata = (id, task) => ({ id, filename: task.filename ?? '未命名表格.xlsx', revision: task.revision ?? 1, state: task.state ?? (task.result ? 'completed' : 'preview_ready'), created: task.created, updatedAt: task.updatedAt ?? task.created, expiresAt: task.created + ttlMs, sessionId: task.sessionId, confirmed: !!task.result });
|
|
19
20
|
const settings = task => ({
|
|
21
|
+
diagnostics: diagnostics(task.preview),
|
|
20
22
|
configuration: task.configuration ?? {},
|
|
21
23
|
selectedFields: task.selectedFields,
|
|
22
24
|
progress: task.progress,
|
|
23
25
|
candidates: allCandidates(task).changes,
|
|
24
26
|
selectedIds: task.preview.changeSet.changes.map(c=>c.id),
|
|
25
27
|
catalog: FIELD_CATALOG,
|
|
28
|
+
catalogGroups: QCC_FIELD_CATALOG.map(g=>({id:g.id,label:g.label,fields:g.fields.flatMap(item=>FIELD_CATALOG.filter(f=>f.key===item.id||f.aliases?.includes(item.id)).map(f=>f.key))})),
|
|
26
29
|
structure: parseWorkbook(task.bytes).sheets.filter(s => !s.hidden).map(s => ({
|
|
27
30
|
name: s.name,
|
|
28
31
|
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 })) })),
|
|
@@ -172,7 +175,7 @@ export function createFormFillHandler({ basePath = '', getPort, now = Date.now,
|
|
|
172
175
|
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}) });
|
|
173
176
|
if (disposed || tasks.get(id) !== active) throw Error('任务已过期或被替换');
|
|
174
177
|
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 };
|
|
178
|
+
return { taskId: id, filled: active.preview.changeSet.changes.length, incomplete: active.preview.changeSet.incomplete.length, diagnostics:diagnostics(active.preview), previewPath: basePath + '/#task=' + id };
|
|
176
179
|
} catch (error) {
|
|
177
180
|
if (!disposed && tasks.get(id) === active) tasks.set(id, { ...active, state: 'failed', updatedAt: now(), revision: active.revision + 1 });
|
|
178
181
|
throw error;
|
package/lib/index.js
CHANGED
|
@@ -2,6 +2,7 @@ import { createFormFillHandler } from './http.js';
|
|
|
2
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
|
+
import { renderEnrichmentResult } from './diagnostics.js';
|
|
5
6
|
export { previewBytes, previewFile, writeCopy } from './workflow.js';
|
|
6
7
|
export const name = 'form-fill-agent';
|
|
7
8
|
export const inject = [];
|
|
@@ -18,8 +19,8 @@ export function apply(ctx, config = {}) {
|
|
|
18
19
|
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
20
|
parameters: { type: 'object', additionalProperties: false, properties: { taskId: { type: 'string' }, expectedRevision: { type: 'integer' }, mode:{type:'string',enum:['all','retry']} }, required: ['taskId'] },
|
|
20
21
|
output: {
|
|
21
|
-
schema: { type: 'object', properties: { taskId: { type: 'string' }, filled: { type: 'integer' }, incomplete: { type: 'integer' }, previewPath: { type: 'string' } }, required: ['taskId','filled','incomplete','previewPath'] },
|
|
22
|
-
render: (_args, value) => [{ type: 'text', text:
|
|
22
|
+
schema: { type: 'object', properties: { taskId: { type: 'string' }, filled: { type: 'integer' }, incomplete: { type: 'integer' }, diagnostics:{type:'object'}, previewPath: { type: 'string' } }, required: ['taskId','filled','incomplete','previewPath'] },
|
|
23
|
+
render: (_args, value) => [{ type: 'text', text: renderEnrichmentResult(value) }],
|
|
23
24
|
},
|
|
24
25
|
async execute(args, execution) {
|
|
25
26
|
if (!execution?.agent || !execution?.token) throw Error('需要 Agent-owned 工具执行上下文');
|
package/lib/ui.html
CHANGED
|
@@ -15,12 +15,12 @@
|
|
|
15
15
|
<div class="controls"><input type="file" id="file" accept=".xlsx" aria-label="选择 XLSX 文件"><button id="upload">分析表格</button></div>
|
|
16
16
|
<details><summary>体验合成模板</summary><p>下面模板只使用合成数据,不查询真实企业。</p><div class="controls" id="samples"><button class="secondary" data-name="客户台账">客户台账</button><button class="secondary" data-name="供应商准入表">供应商准入表</button><button class="secondary" data-name="合同主体信息表">合同主体信息表</button></div></details></div>
|
|
17
17
|
<section id="preview" hidden>
|
|
18
|
-
<div class="card" data-pane="rules"><h2>字段与规则</h2><p id="structure"></p><p>默认只填空白。可选择工作表、修正表头行及字段;应用设置会清除旧预览和主体确认,请重新核验。</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>
|
|
18
|
+
<div class="card" data-pane="rules"><h2>字段与规则</h2><p id="structure"></p><p id="anchor-alert" role="status"></p><p>默认只填空白。可选择工作表、修正表头行及字段;应用设置会清除旧预览和主体确认,请重新核验。</p><label>搜索字段 <input id="mapping-search" type="search" placeholder="按原表字段名称筛选"></label><p class="muted">未识别字段可手动选择;没有数据来源的字段选择“不填写”。规则只保存表头与映射,不含记录。</p><button id="mapping-save">保存字段规则</button><button id="mapping-load">复用字段规则</button><div id="mapping"></div><button id="configure" class="primary">应用字段设置</button></div>
|
|
19
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>
|
|
20
|
-
<div class="card" data-pane="preview"><h2>填写预览</h2><p id="summary"></p><p class="muted">每格显示原值、准备填写值和来源。取消勾选后点击“应用选择”,再确认新的预览。</p>
|
|
20
|
+
<div class="card" data-pane="preview"><h2>填写预览</h2><p id="summary"></p><details><summary>逐列填写结果</summary><p class="muted">按单元格统计;整行缺少主体标识的情况另见下方原因分类。</p><div class="scroll"><table><thead><tr><th>原表字段</th><th>映射字段</th><th>预览可填写格数</th><th>未完成项</th></tr></thead><tbody id="field-results"></tbody></table></div></details><p class="muted">每格显示原值、准备填写值和来源。取消勾选后点击“应用选择”,再确认新的预览。</p>
|
|
21
21
|
<div class="toolbar"><label>筛选单元格 <input id="result-search" type="search" placeholder="位置、字段或值"></label><span id="result-count" class="muted"></span></div>
|
|
22
22
|
<div class="scroll"><table><thead><tr><th>位置</th><th>字段</th><th>原值</th><th>准备填写</th><th>依据与来源</th><th>填写</th></tr></thead><tbody id="changes"></tbody></table></div><button id="selection">应用选择</button></div>
|
|
23
|
-
<div class="card" data-pane="preview"><h2>未完成项</h2><div class="scroll"><table><thead><tr><th>位置</th><th>字段</th><th>原因</th></tr></thead><tbody id="incomplete"></tbody></table></div></div>
|
|
23
|
+
<div class="card" data-pane="preview"><h2>未完成项</h2><p id="reason-summary"></p><div class="scroll"><table><thead><tr><th>位置</th><th>字段</th><th>原因</th></tr></thead><tbody id="incomplete"></tbody></table></div></div>
|
|
24
24
|
<div class="card" data-pane="download"><h2>确认并下载</h2><p>原文件不会被覆盖。下载包含全部已确认填写及完整清单,不受当前筛选影响。</p><button id="confirm" class="primary">确认这些填写,生成新副本</button><div id="downloads"></div></div>
|
|
25
25
|
<div class="controls"><button id="refresh" class="secondary">刷新任务</button><button id="discard" class="secondary">删除任务</button></div>
|
|
26
26
|
</section></main>
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
<section class="ff-wizard-step" data-wizard-pane="0"><h3>上传已有表格</h3><p>仅支持 XLSX;选择文件后执行本地识别,不调用企查查。</p><label>选择 XLSX <input id="wizard-file" type="file" accept=".xlsx"></label><p id="wizard-source"></p></section>
|
|
31
31
|
<section class="ff-wizard-step" data-wizard-pane="1"><h3>填写规则</h3><p>只填空白、保留已有内容,查询后预览,确认后生成副本。</p><p id="wizard-rules"></p><button id="wizard-edit-rules">到工作台修正字段映射</button></section>
|
|
32
32
|
<section class="ff-wizard-step" data-wizard-pane="2"><h3>企业工商字段</h3><div class="toolbar"><label>搜索 <input id="field-search" type="search" placeholder="字段名称"></label><span id="field-count"></span><button id="fields-all">选择可用字段</button><button id="fields-clear">清空可选字段</button></div><p class="muted">主体锚点固定保留;只有原表已映射的字段可选。映射修改请返回工作台。</p><div id="wizard-fields" class="ff-field-grid"></div></section>
|
|
33
|
-
<section class="ff-wizard-step" data-wizard-pane="3"><h3>确认任务描述</h3><p id="business-summary" class="ff-summary"></p><label>补充要求<textarea id="requirements" rows="4" maxlength="1000" placeholder="补充核验说明;字段或范围变更请返回前面步骤设置。"></textarea></label><details><summary>兼容执行信息</summary><p id="qcc-command" class="ff-tech"></p></details></section>
|
|
33
|
+
<section class="ff-wizard-step" data-wizard-pane="3"><h3>确认任务描述</h3><p id="business-summary" class="ff-summary"></p><p id="scope-review" class="ff-summary"></p><label id="scope-ack-label" hidden><input id="scope-ack" type="checkbox">确认本次不填写以上列</label><label>补充要求<textarea id="requirements" rows="4" maxlength="1000" placeholder="补充核验说明;字段或范围变更请返回前面步骤设置。"></textarea></label><details><summary>兼容执行信息</summary><p id="qcc-command" class="ff-tech"></p></details></section>
|
|
34
34
|
</div><p id="wizard-message" role="status"></p><footer class="dialog-footer"><button id="wizard-prev">上一步</button><button id="wizard-next" class="primary">下一步</button><button id="draft" class="primary" hidden>回填到对话框</button><button id="copy-command" hidden>复制指令</button></footer></dialog>
|
|
35
35
|
<dialog id="draft-conflict" aria-labelledby="draft-conflict-title"><header class="dialog-head"><h2 id="draft-conflict-title">保留已有输入</h2></header><div class="dialog-body">对话框已有内容,请选择替换、追加或取消。取消会保留现有输入。</div><footer class="dialog-footer"><button id="draft-cancel">取消</button><button id="draft-append">追加</button><button id="draft-replace" class="primary">替换</button></footer></dialog>
|
|
36
36
|
<script>
|
|
@@ -78,8 +78,14 @@ function navigate(target){if(target==='wizard'){openWizard();return}if(target!==
|
|
|
78
78
|
function downloads(){ $('downloads').replaceChildren();for(const [kind,label,suffix] of [['xlsx','下载已填副本','-已填副本.xlsx'],['changes','下载变更清单','-变更.json'],['incomplete','下载未完成项','-未完成.json']]){const a=document.createElement('a');Object.assign(a,{className:'download',href:basePath+'/download/'+current.id+'/'+kind,download:filename+suffix,textContent:label});$('downloads').append(a)}}
|
|
79
79
|
function businessSummary(){return current?(retryIntent?'仅重试失败、取消或未执行的部分,保留成功结果。':'')+'填写文件“'+current.filename+'”中的空白字段。工作表:'+current.analysis.tables.map(t=>t.sheet).join('、')+'。填写字段:'+current.catalog.filter(f=>wizardFields.has(f.key)).map(f=>f.label).join('、')+'。保留原值,核验预览后生成新副本。':'请先上传 XLSX 文件。'}
|
|
80
80
|
function command(){return current?businessSummary()+'\n'+$('requirements').value+'\n执行关联:调用 form_fill_enrich:taskId='+current.id+',expectedRevision='+current.revision+(retryIntent?',mode=retry':'')+'。字段范围以工作台确认设置为准。':''}
|
|
81
|
-
function
|
|
82
|
-
function
|
|
81
|
+
function mappingVersion(){return JSON.stringify({tables:current?.analysis.tables.map(t=>({sheet:t.sheet,headerRow:t.headerRow,anchors:t.anchors,mappings:t.mappings.map(m=>[m.column,m.field])})),scope:current?.selectedFields??null})}
|
|
82
|
+
function saveWizard(){if(current)sessionStorage.setItem('ff-wizard:'+current.id,JSON.stringify({fields:[...wizardFields],signature:mappingVersion(),requirements:$('requirements').value,step:wizardStep}))}
|
|
83
|
+
function initWizard(){const structure=mappingVersion();if(wizardTask===current?.id&&wizardStructure===structure)return;wizardTask=current?.id;wizardStructure=structure;const available=current?.analysis.tables.flatMap(t=>t.mappings.filter(m=>m.field&&!t.anchors.includes(m.field)).map(m=>m.field))||[];let saved;try{saved=JSON.parse(sessionStorage.getItem('ff-wizard:'+current?.id))}catch{}const valid=saved?.signature===structure;wizardFields=new Set((valid?saved.fields:current?.selectedFields||available).filter(k=>available.includes(k)));$('requirements').value=saved?.requirements||'';wizardStep=valid?saved.step||0:0;if(saved&&!valid)$('wizard-message').textContent='字段映射或执行范围已变化,已重新加载当前可填写字段,请检查后再发送。'}
|
|
84
|
+
function scopeReview(){
|
|
85
|
+
const omitted=current?.analysis.tables.flatMap(t=>t.mappings.filter(m=>!t.anchors.includes(m.field)&&(!m.field||!wizardFields.has(m.field))).map(m=>t.sheet+':'+m.label))||[];
|
|
86
|
+
const missing=current?.analysis.incomplete.filter(i=>i.reason==='missing-anchor')||[];
|
|
87
|
+
return {omitted,missing,blocked:missing.length>0&&!current?.analysis.opportunities.length};
|
|
88
|
+
}
|
|
83
89
|
function renderWizard(){
|
|
84
90
|
initWizard();
|
|
85
91
|
document.querySelectorAll('[data-wizard-pane]').forEach(p=>p.setAttribute('aria-current',String(Number(p.dataset.wizardPane)===wizardStep)));
|
|
@@ -96,13 +102,18 @@ function renderWizard(){
|
|
|
96
102
|
}
|
|
97
103
|
$('field-count').textContent='已选 '+wizardFields.size+' 个填写字段';
|
|
98
104
|
$('business-summary').textContent=businessSummary();$('qcc-command').textContent=command();
|
|
105
|
+
const review=scopeReview(),signature=JSON.stringify([current?.analysis.tables,[...wizardFields].sort()]);
|
|
106
|
+
if($('scope-ack').dataset.signature!==signature){$('scope-ack').checked=false;$('scope-ack').dataset.signature=signature}
|
|
107
|
+
$('scope-review').textContent=(review.omitted.length?'本次不填写 '+review.omitted.length+' 列:'+review.omitted.join('、')+'。':'所有已映射的非主体字段均已纳入。')+(review.missing.length?'有 '+review.missing.length+' 行缺少可用主体标识;请检查企业名称列,不能使用整列为空的信用代码定位主体。':'');
|
|
108
|
+
$('scope-ack-label').hidden=!review.omitted.length;
|
|
99
109
|
$('wizard-prev').disabled=wizardStep===0||busy;$('wizard-next').hidden=wizardStep===3;$('wizard-next').disabled=!current||busy;
|
|
100
|
-
$('draft').hidden=wizardStep!==3;$('copy-command').hidden=wizardStep!==3;$('draft').disabled=!current||!wizardFields.size||busy;$('copy-command').disabled=$('draft').disabled;
|
|
110
|
+
$('draft').hidden=wizardStep!==3;$('copy-command').hidden=wizardStep!==3;$('draft').disabled=!current||!wizardFields.size||busy||review.blocked||!!review.omitted.length&&!$('scope-ack').checked;$('copy-command').disabled=$('draft').disabled;
|
|
101
111
|
}
|
|
102
112
|
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)}}
|
|
103
113
|
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()}
|
|
104
114
|
async function applyScope(){
|
|
105
115
|
if(!current||!wizardFields.size)throw Error('请至少选择一个已映射的填写字段');
|
|
116
|
+
const review=scopeReview();if(review.blocked)throw Error('没有可用主体标识,请先到字段设置映射企业名称列。');if(review.omitted.length&&!$('scope-ack').checked)throw Error('请确认未纳入的列,或返回字段设置完善映射。');
|
|
106
117
|
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)))];
|
|
107
118
|
if(retryIntent&&JSON.stringify([...original].sort())!==JSON.stringify([...wizardFields].sort()))throw Error('重试时请保留原字段范围;如需更改,请重新生成完整核验指令。');
|
|
108
119
|
if(JSON.stringify([...original].sort())!==JSON.stringify([...wizardFields].sort()))render(await api('/scope',{id:current.id,selectedFields:[...wizardFields]}));
|
|
@@ -128,12 +139,41 @@ function renderSettings(data){
|
|
|
128
139
|
const showFields=()=>{
|
|
129
140
|
fields.replaceChildren();
|
|
130
141
|
for(const cell of sheet.rows.find(r=>r.number===Number(header.value))?.cells||[]){
|
|
131
|
-
const label=document.createElement('
|
|
142
|
+
const label=document.createElement('div');label.className='ff-mapping-row';label.dataset.sourceLabel=cell.label;
|
|
143
|
+
const title=document.createElement('strong');title.textContent=cell.label;label.append(title);
|
|
132
144
|
const select=document.createElement('select');select.dataset.column=cell.column;select.setAttribute('aria-label',sheet.name+' 字段 '+cell.label);
|
|
133
|
-
for(const field of [{key:'',label:'不填写'},...(data.catalog||[])]){const option=document.createElement('option');option.value=field.key;option.textContent=field.label;select.append(option)}
|
|
134
145
|
const override=data.configuration?.mappings?.find(m=>m.sheet===sheet.name&&m.column===cell.column);
|
|
135
|
-
|
|
136
|
-
|
|
146
|
+
const automatic=table?.headerRow===Number(header.value)?table.mappings.find(m=>m.column===cell.column)?.field||'':'';
|
|
147
|
+
let chosen=override?override.field||'':automatic,manual=!!override;
|
|
148
|
+
const normalize=s=>String(s).normalize('NFKC').toLowerCase().replace(/[\s_()()·-]/g,'');
|
|
149
|
+
const matches=(field,query)=>{const text=normalize([field.label,field.key,...(field.aliases||[])].join(' '));return query.trim().split(/\s+/).every(word=>{const q=normalize(word);if(text.includes(q))return true;if(q.length<2)return false;let i=0;for(const c of text)if(c===q[i])i++;return i===q.length})};
|
|
150
|
+
const search=document.createElement('input');search.type='search';search.placeholder='关键词,例如:电话、信用代码';search.setAttribute('aria-label',sheet.name+' 搜索目标字段 '+cell.label);
|
|
151
|
+
const dimension=document.createElement('select');dimension.setAttribute('aria-label',sheet.name+' 字段维度 '+cell.label);
|
|
152
|
+
const groups=[{id:'',label:'全部维度'},...(data.catalogGroups||[]),{id:'other',label:'其他字段'}];
|
|
153
|
+
for(const g of groups){const option=document.createElement('option');option.value=g.id;option.textContent=g.label;dimension.append(option)}
|
|
154
|
+
const status=document.createElement('p');status.className='ff-mapping-status';status.setAttribute('aria-live','polite');
|
|
155
|
+
const count=document.createElement('small');count.className='muted';
|
|
156
|
+
const suggestions=document.createElement('div');suggestions.className='ff-mapping-suggestions';
|
|
157
|
+
const updateStatus=()=>{status.textContent=chosen?(manual?'已人工选择':'已自动匹配')+':'+(data.catalog.find(f=>f.key===chosen)?.label||chosen):manual?'已选择不填写':'未匹配,请搜索或按维度选择字段;也可保留不填写';status.dataset.unmatched=String(!chosen&&!manual)};
|
|
158
|
+
const options=()=>{
|
|
159
|
+
const group=data.catalogGroups?.find(g=>g.id===dimension.value);
|
|
160
|
+
const found=(data.catalog||[]).filter(f=>matches(f,search.value)&&(!dimension.value||(dimension.value==='other'?!data.catalogGroups?.some(g=>g.fields.includes(f.key)):group?.fields.includes(f.key))));
|
|
161
|
+
select.replaceChildren();
|
|
162
|
+
const list=[{key:'',label:'不填写'},...found];
|
|
163
|
+
if(chosen&&!found.some(f=>f.key===chosen)){const field=data.catalog.find(f=>f.key===chosen);if(field)list.splice(1,0,{...field,label:field.label+'(当前选择,筛选外)'})}
|
|
164
|
+
for(const field of list){const option=document.createElement('option');option.value=field.key;option.textContent=field.label;select.append(option)}
|
|
165
|
+
select.value=chosen;count.textContent=found.length?'匹配 '+found.length+' 个字段':'没有匹配字段,请更换关键词或维度';updateStatus();
|
|
166
|
+
};
|
|
167
|
+
select.onchange=()=>{chosen=select.value;manual=true;suggestions.replaceChildren();options()};
|
|
168
|
+
search.oninput=options;dimension.onchange=options;
|
|
169
|
+
if(!chosen&&!manual){
|
|
170
|
+
const hints={'原文件导入名称':['company_name'],'单位':['company_name'],'名称':['company_name'],'网址':['contact_official_website']}[cell.label.trim()]||[];
|
|
171
|
+
const recommended=(data.catalog||[]).filter(f=>hints.includes(f.key)||matches(f,cell.label)).slice(0,4);
|
|
172
|
+
if(recommended.length){const hint=document.createElement('span');hint.textContent='推荐(确认后采用):';suggestions.append(hint)}
|
|
173
|
+
for(const field of recommended){const button=document.createElement('button');button.type='button';button.textContent=field.label;button.onclick=()=>{chosen=field.key;manual=true;suggestions.replaceChildren();options()};suggestions.append(button)}
|
|
174
|
+
}
|
|
175
|
+
const filters=document.createElement('div');filters.className='ff-mapping-filters';filters.append(search,dimension);
|
|
176
|
+
label.append(status,suggestions,filters,select,count);options();fields.append(label);
|
|
137
177
|
}
|
|
138
178
|
};
|
|
139
179
|
header.onchange=showFields;section.append(header,fields);showFields();$('mapping').append(section);
|
|
@@ -157,14 +197,19 @@ if(gridTask!==data.id){gridTask=data.id;gridPageNumber=1;gridColumn=1;$('grid-sh
|
|
|
157
197
|
if(parent!==window)parent.postMessage({type:'ff-task',taskId:data.id},location.origin);
|
|
158
198
|
$('changes').replaceChildren();$('incomplete').replaceChildren();$('downloads').replaceChildren();
|
|
159
199
|
$('task-meta').dataset.state=data.state;
|
|
160
|
-
$('task-meta').textContent=(data.filename||'表格')+' · '+({'enriching':'查询中','completed':'
|
|
200
|
+
$('task-meta').textContent=(data.filename||'表格')+' · '+({'enriching':'查询中','completed':'已生成副本'+(data.changeSet.incomplete.length?',仍有 '+data.changeSet.incomplete.length+' 项未完成':''),'interrupted':'查询中断,请重新核验','partial':'部分完成','preview_ready':'可核验'}[data.state]||data.state)+' · 有效期至 '+new Date(data.expiresAt).toLocaleString();
|
|
161
201
|
$('summary').textContent='可填写 '+data.changeSet.changes.length+' 格 · '+(data.plan.paidCalls?'企查查来源':'本地分析 / 合成示例 · 费用 0')+' · 未完成 '+data.changeSet.incomplete.length+' 项';
|
|
162
202
|
$('structure').textContent=data.analysis.tables.map(t=>t.sheet+':表头第 '+t.headerRow+' 行').join(';');
|
|
163
203
|
renderSettings(data);
|
|
164
204
|
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)}
|
|
165
|
-
for(const i of data.changeSet.incomplete)row('incomplete',[i.sheet+' '+(i.cell||''),i.label||'—',reasons[i.reason]||'此项需要人工处理'],['位置','字段','原因']);
|
|
205
|
+
for(const i of data.changeSet.incomplete)row('incomplete',[i.sheet+' '+(i.cell||('第 '+i.row+' 行')),i.label||'—',data.diagnostics?.reasons.find(r=>r.code===i.reason)?.label||reasons[i.reason]||'此项需要人工处理'],['位置','字段','原因']);
|
|
206
|
+
$('field-results').replaceChildren();
|
|
207
|
+
for(const column of data.diagnostics?.columns||[])row('field-results',[column.label,(data.catalog.find(f=>f.key===column.field)?.label||'未映射 / 不填写'),column.ready,column.incomplete],['原表字段','映射字段','预览可填写格数','未完成项']);
|
|
166
208
|
$('preview').hidden=false;if(data.confirmed)downloads();$('qcc-command').textContent=command();
|
|
167
209
|
initWizard();filterResults();syncActions();if($('wizard').open)renderWizard();
|
|
210
|
+
$('reason-summary').textContent=(data.diagnostics?.reasons||[]).map(r=>r.label+':'+r.count+r.unit).join(';')||'暂无未完成项。';
|
|
211
|
+
const missing=data.analysis.incomplete.filter(i=>i.reason==='missing-anchor');
|
|
212
|
+
$('anchor-alert').textContent=missing.length?'有 '+missing.length+' 行缺少可用主体标识。请把实际包含名称的列映射为“企业名称”,或提供非空信用代码;当前不会查询这些行。':'';
|
|
168
213
|
}
|
|
169
214
|
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}}
|
|
170
215
|
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)}}
|
|
@@ -188,12 +233,13 @@ $('wizard-prev').onclick=()=>{wizardStep=Math.max(0,wizardStep-1);saveWizard();r
|
|
|
188
233
|
$('wizard-file').onchange=async()=>{const file=$('wizard-file').files[0];if(file){await preview(await file.arrayBuffer(),file.name);renderWizard()}};
|
|
189
234
|
$('wizard-edit-rules').onclick=()=>{closeWizard();navigate('rules')};
|
|
190
235
|
$('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()};
|
|
236
|
+
$('scope-ack').onchange=renderWizard;
|
|
191
237
|
$('requirements').oninput=()=>{saveWizard();$('qcc-command').textContent=command()};
|
|
192
238
|
$('copy-command').onclick=async()=>{try{await applyScope();saveWizard();await navigator.clipboard.writeText(command());$('status').textContent='已复制,请在 DSH 对话中发送。'}catch(e){$('status').textContent=e.message}};
|
|
193
239
|
$('draft').onclick=()=>sendDraft();
|
|
194
240
|
$('draft-cancel').onclick=()=>$('draft-conflict').close();
|
|
195
241
|
for(const mode of ['append','replace'])$('draft-'+mode).onclick=()=>{$('draft-conflict').close();sendDraft(mode)};
|
|
196
|
-
$('mapping-search').oninput=()=>{const query=$('mapping-search').value.trim();$('mapping').querySelectorAll('
|
|
242
|
+
$('mapping-search').oninput=()=>{const query=$('mapping-search').value.trim().toLowerCase();$('mapping').querySelectorAll('.ff-mapping-row').forEach(row=>row.hidden=!row.dataset.sourceLabel.toLowerCase().includes(query))};
|
|
197
243
|
$('result-search').oninput=filterResults;$('changes').onchange=filterResults;
|
|
198
244
|
window.addEventListener('message',e=>{if(e.origin!==location.origin||e.source!==parent)return;if(e.data?.type==='ff-theme'&&['light','dark'].includes(e.data.theme))document.body.dataset.ffTheme=e.data.theme;if(e.data?.type==='ff-draft-conflict'){expectedDraft=e.data.draft;if(!$('draft-conflict').open)$('draft-conflict').showModal()}if(e.data?.type==='ff-draft-result'){$('status').textContent=e.data.ok?'已回填,请在对话框修改或发送。':'回填失败,任务已保留。';if(e.data.ok)closeWizard(true)}if(e.data?.type==='ff-navigate'){if(!current&&location.hash)pendingNavigation=e.data.step;if(e.data.step==='history')$('history-tab').click();else navigate(e.data.step)}});
|
|
199
245
|
api('/health').then(s=>{$('connection').textContent=s.qccAvailable?'已发现企查查工商工具,可在对话中执行。':'尚未发现企查查工商工具;请先连接 MCP。合成模板仍可体验。'}).catch(()=>{});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-form-fill-agent",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.3",
|
|
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" },
|