dsh-form-fill-agent 0.2.30 → 0.2.32

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # AI填表智能体
2
2
 
3
- > 0.2.30:统一简洁首页、稳定菜单与可恢复品牌标题,明确任务状态;侧栏仍为可选依赖。
3
+ > 0.2.32:新增14项地区、行业层级、主营产品和企业规模字段;修复行业对象转换。仅补原模板已确认映射的空白位置。
4
4
 
5
5
  > 统一侧栏适配需要 Better Sidebar `>=0.17.1 <0.19.0`。五个流程入口打开同一 Session Tab;侧栏展开、收起和关闭由宿主管理。缺少依赖时会显示安装升级提示。
6
6
 
@@ -11,7 +11,7 @@ AI填表智能体:支持自动填表、表格填充、表格补全、Excel填
11
11
  ### 基础安装(不需要侧栏)
12
12
 
13
13
  ```sh
14
- dsh plugin --profile web add dsh-form-fill-agent@0.2.30
14
+ dsh plugin --profile web add dsh-form-fill-agent@0.2.32
15
15
  ```
16
16
 
17
17
  只需 Node.js >=22、完整 DSH 与主包自动安装的内核/Provider。Better Sidebar 和 dsh-context 都是可选项,不会由基础安装强装。
@@ -93,7 +93,7 @@ dsh plugin --profile web add dsh-form-fill-agent@latest
93
93
  - 结果筛选显示当前/总数及已选格数,下载始终包含全部确认内容。
94
94
  - 简单 XLSX 安全解析、工作表与表头识别、企业名称或信用代码锚点识别。
95
95
  - 确定性字段语义映射、只填空白、单元格级预览与来源追溯。
96
- - 137 字段目录:工商、企业简介、联系方式、上市、开票、进出口、自身及关联风险、实际控制人。唯一完整实控人记录可填写,多名/分页/异常留空待核验。事实和确定性摘要区分来源,不生成风险结论。
96
+ - 151 字段目录:工商、企业简介、联系方式、上市、开票、进出口、自身及关联风险、实际控制人。唯一完整实控人记录可填写,多名/分页/异常留空待核验。事实和确定性摘要区分来源,不生成风险结论。
97
97
  - 五步工作台、指令回填、任务历史、可恢复单元格选择、取消/失败重试与重启恢复。
98
98
  - 可选择工作表、表头行并修正字段映射;修改后需重新预览。
99
99
  - 查询主体不一致时展示返回候选,人工选择或补充已核验的完整主体后重新查询,原表名称不覆盖。
@@ -102,7 +102,7 @@ dsh plugin --profile web add dsh-form-fill-agent@latest
102
102
 
103
103
  ## 使用边界与数据保存
104
104
 
105
- 补充要求为模型可读文本;字段或工作表范围请通过向导/工作台修改,不能仅改文字后假定执行范围已同步。任务 schema 5 可读取 schema 1–5;回退旧版本请使用升级前目录副本或独立任务目录。主包为 0.2.30,共享内核为 0.2.10,Provider 为 0.2.2,共享字段契约为 0.1.0。
105
+ 补充要求为模型可读文本;字段或工作表范围请通过向导/工作台修改,不能仅改文字后假定执行范围已同步。任务 schema 5 可读取 schema 1–5;回退旧版本请使用升级前目录副本或独立任务目录。主包为 0.2.32,共享内核为 0.2.10,Provider 为 0.2.3,共享字段契约为 0.1.0。
106
106
 
107
107
  支持企业完整登记名称或 18 位信用代码;简称、主体不一致或多候选不会自动猜选。仅有信用代码的表格也可作为查询锚点。连接候选检索工具时可检索简称,仍需人工确认;缺少工具时降级人工输入。字段映射采用确定性规则及人工确认,不让模型编造事实。支持普通内部公式、固定区域下拉、基础条件格式和普通表格对象;复杂 Excel 结构和 Word 暂不支持,保真边界见下方安全说明。
108
108
 
package/lib/client.js CHANGED
@@ -57,6 +57,57 @@ window.__ModuleLoader__.load({
57
57
  connect();const unsubscribe=sessions?.list?.subscribe?.(connect);
58
58
  return()=>{disposed=true;stop();unsubscribe?.();seen.clear()};
59
59
  }
60
+ const INITIAL_TEMPLATE_ID='dsh-initial-draft/form-fill/1';
61
+ const INITIAL_TEMPLATE_TEXT='请帮我填写企业信息表。请上传 Excel 模板,说明主体定位列和需要填写的字段;也可点击左上角「提示词生成」设置填写规则。例如:按“企业名称”定位,只填空白单元格,补充统一社会信用代码、法定代表人和注册地址,并生成新文件。';
62
+ // Full text equality is the collision-free fingerprint; never trim/normalize user edits.
63
+ function createInitialDraftController({current,shell,storage,subscribeCurrent,defer=fn=>Promise.resolve().then(fn)}){
64
+ const entries=new Map();let disposed=false;
65
+ const key=id=>INITIAL_TEMPLATE_ID+':'+id;
66
+ const persist=(id,entry)=>{try{storage.setItem(key(id),JSON.stringify({state:entry.state,templateId:INITIAL_TEMPLATE_ID,fingerprint:INITIAL_TEMPLATE_TEXT,untouched:entry.untouched}));return true}catch{return false}};
67
+ const snapshot=face=>{
68
+ const state=face?.state?.getSnapshot?.(),editor=face?.editor;
69
+ if(!state||typeof state.draft!=='string'||!Array.isArray(state.imageIds)||!Number.isSafeInteger(state.draftRev)||typeof editor?.isComposing!=='function'||typeof editor?.getRootElement!=='function'||typeof editor?.update!=='function')return null;
70
+ return {draft:state.draft,rev:state.draftRev,empty:state.draft===''&&state.imageIds.length===0&&state.phase==='plain'&&Array.isArray(state.occurrences)&&state.occurrences.length===0,ready:!!editor.getRootElement(),composing:editor.isComposing()};
71
+ };
72
+ const skip=(id,e)=>{e.state='skipped';e.untouched=false;persist(id,e)};
73
+ return {
74
+ // Called only after this entry's create succeeds, before the asynchronous open.
75
+ arm(id){
76
+ if(disposed||!id?.startsWith('session-dsh-form-fill-agent-')||entries.has(id))return;
77
+ try{if(storage.getItem(key(id)))return}catch{return}
78
+ let face;try{face=shell(id)}catch{return}
79
+ const first=snapshot(face),e={state:'unseen',face,first,untouched:false};entries.set(id,e);
80
+ if(!first||!first.empty||first.composing||typeof face?.setDraft!=='function'||typeof face.state.subscribe!=='function'||!persist(id,e)){skip(id,e);return}
81
+ let visited=current()===id;
82
+ e.sessionOff=subscribeCurrent?.(()=>{if(current()===id)visited=true;else if(visited&&e.state==='unseen')skip(id,e)});
83
+ e.off=face.state.subscribe(()=>{
84
+ const next=snapshot(face);
85
+ if(e.writing)return;
86
+ if(e.state==='unseen'&&(!next||next.rev!==first.rev||!next.empty||next.composing))skip(id,e);
87
+ if(e.state==='initialized'&&next?.rev!==e.seedRev){e.untouched=false;persist(id,e)}
88
+ });
89
+ },
90
+ async attempt(id){
91
+ const e=entries.get(id);if(disposed||e?.state!=='unseen')return false;
92
+ const before=snapshot(e.face);
93
+ if(current()!==id||!before||!before.empty||before.composing||before.rev!==e.first.rev){skip(id,e);return false}
94
+ if(!before.ready)return false;
95
+ // All asynchronous work precedes this second snapshot and synchronous public write.
96
+ return defer(()=>{
97
+ const after=snapshot(e.face);
98
+ if(disposed||e.state!=='unseen')return false;
99
+ if(current()!==id||!after?.ready||!after.empty||after.composing||after.rev!==before.rev){skip(id,e);return false}
100
+ e.state='initialized';e.untouched=true;
101
+ if(!persist(id,e)){skip(id,e);return false}
102
+ try{e.writing=true;e.face.editor.update(()=>e.face.setDraft(INITIAL_TEMPLATE_TEXT),{discrete:true,tag:'skip-dom-selection'});e.seedRev=snapshot(e.face)?.rev;if(snapshot(e.face)?.draft!==INITIAL_TEMPLATE_TEXT){skip(id,e);return false}return true}
103
+ catch{skip(id,e);return false}finally{e.writing=false}
104
+ });
105
+ },
106
+ expire(id){const e=entries.get(id);if(e?.state==='unseen')skip(id,e)},
107
+ isTemplate(id,text){const e=entries.get(id);return e?.state==='initialized'&&e.untouched&&text===INITIAL_TEMPLATE_TEXT&&snapshot(e.face)?.rev===e.seedRev},
108
+ dispose(){disposed=true;for(const [id,e] of entries){if(e.state==='unseen')skip(id,e);e.off?.();e.sessionOff?.()}entries.clear()}
109
+ };
110
+ }
60
111
  function apply(ctx) {
61
112
  let React, portal;
62
113
  try { React=require('react');portal=require('react-dom').createPortal; } catch {}
@@ -70,6 +121,9 @@ window.__ModuleLoader__.load({
70
121
  const theme=()=>document.documentElement.hasAttribute('data-ds-dark-theme')?'dark':getComputedStyle(document.documentElement).colorScheme==='light'?'light':matchMedia('(prefers-color-scheme:dark)').matches?'dark':'light';
71
122
  if(typeof document!=='undefined'&&document.head?.append){const css=document.createElement('link');css.rel='stylesheet';css.href='/form-fill/brand.css';css.dataset.formFillStyle='true';document.head.append(css);disposers.push(()=>css.remove())}
72
123
  const current=()=>ctx.sessions?.list?.getSnapshot?.().current;
124
+ let draftStorage;try{draftStorage=window.sessionStorage}catch{}
125
+ const initialDraft=createInitialDraftController({current,subscribeCurrent:fn=>ctx.sessions?.list?.subscribe?.(fn),shell:id=>ctx.conversation?.input?.shell?.(id),storage:draftStorage});
126
+ disposers.push(()=>initialDraft.dispose());
73
127
  function register(name,component){ctx.slots.inject(name,()=>ctx.slots.register({name,id:'form-fill-agent',order:120},component))}
74
128
  function openPanel(id,step='import'){
75
129
  if(!activePlugin||!owned(id))return;
@@ -91,7 +145,15 @@ window.__ModuleLoader__.load({
91
145
  const id=prefix+crypto.randomUUID();
92
146
  const result=await ctx.sessions.create({workspaceId:workspace.workspaceId,sessionId:id});
93
147
  if(result!==id)throw Error('宿主不支持独立业务会话');
148
+ initialDraft.arm(id);
94
149
  await ctx.sessions.open(id);
150
+ // Input mounting is bounded; any user edit or Session switch consumes eligibility.
151
+ if(ctx.conversation?.input?.shell)for(let i=0;i<20&&activePlugin;i++){
152
+ if(await initialDraft.attempt(id))break;
153
+ if(current()!==id)break;
154
+ await new Promise(resolve=>setTimeout(resolve,50));
155
+ }
156
+ initialDraft.expire(id);
95
157
  }
96
158
  const launcher=()=>h('a',{className:'ff-launcher',href:'/form-fill/',target:'_blank',rel:'noopener noreferrer','aria-label':'AI 填表',onClick:async e=>{
97
159
  if(!ctx.sessions?.create)return;e.preventDefault();try{await start()}catch{window.open('/form-fill/','_blank','noopener')}
@@ -211,6 +273,7 @@ window.__ModuleLoader__.load({
211
273
  let draft=readDraft();if(typeof draft!=='string')throw Error('cannot read draft');
212
274
  const mode=event.data.mode;
213
275
  const prior=lastDrafts.get(view.id),unchanged=draft===prior?.value;
276
+ if(initialDraft.isTemplate(view.id,draft))draft='';
214
277
  if(draft.trim()&&!unchanged&&(!['replace','append'].includes(mode)||event.data.expectedDraft!==draft)){
215
278
  frame.current.contentWindow.postMessage({type:'ff-draft-conflict',draft},location.origin);return;
216
279
  }
@@ -239,6 +302,6 @@ window.__ModuleLoader__.load({
239
302
  register('conversation.input.overlay',props=>owned(props.sessionId)?h('div',{className:'ff-ui','data-ff-theme':theme()},h('button',{className:'ff-prompt',type:'button',onClick:()=>openPanel(props.sessionId,'wizard')},icon('spark'),'提示词生成')):null);
240
303
  ctx.effect?.(()=>()=>{activePlugin=false;for(const dispose of disposers)dispose();viewListeners.clear();views.clear();sessionTasks.clear();lastDrafts.clear()});
241
304
  }
242
- return {createSidebarAdapter,installSubmittedWorkbenchBridge,name:'form-fill-agent',inject:['slots','sessions','workspaces','conversation'],apply};
305
+ return {createSidebarAdapter,installSubmittedWorkbenchBridge,createInitialDraftController,INITIAL_TEMPLATE_ID,INITIAL_TEMPLATE_TEXT,name:'form-fill-agent',inject:['slots','sessions','workspaces','conversation'],apply};
243
306
  }
244
307
  });
package/lib/http.js CHANGED
@@ -2,7 +2,7 @@ import { providerOutcome } from './provider-outcome.js';
2
2
  import { randomUUID, createHash } from 'node:crypto';
3
3
  import { readFile } from 'node:fs/promises';
4
4
  import { applyChangeSet, parseWorkbook } from 'form-fill-core';
5
- import { FIELD_CATALOG, QCC_FIELD_CATALOG, ACTUAL_CONTROLLER_GROUP, SNAPSHOT_GROUPS, isCompleteAnchor } from 'qcc-form-fill-provider';
5
+ import { FIELD_CATALOG, RUNTIME_FIELD_GROUPS, ACTUAL_CONTROLLER_GROUP, SNAPSHOT_GROUPS, isCompleteAnchor } from 'qcc-form-fill-provider';
6
6
  import { previewBytes } from './workflow.js';
7
7
  import { createTaskStore } from './task-store.js';
8
8
  import { allCandidates, selectCandidates } from './task-model.js';
@@ -33,7 +33,7 @@ export function createFormFillHandler({ basePath = '', getPort, now = Date.now,
33
33
  candidates: allCandidates(task).changes,
34
34
  selectedIds: task.preview.changeSet.changes.map(c=>c.id),
35
35
  catalog: FIELD_CATALOG.map(field=>({...field,searchAliases:mappingSearchAliases(field.key)})),
36
- catalogGroups: [...QCC_FIELD_CATALOG,ACTUAL_CONTROLLER_GROUP,...SNAPSHOT_GROUPS].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))})),
36
+ catalogGroups: [...RUNTIME_FIELD_GROUPS,ACTUAL_CONTROLLER_GROUP,...SNAPSHOT_GROUPS].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))})),
37
37
  structure: parseWorkbook(task.bytes).sheets.filter(s => !s.hidden).map(s => ({
38
38
  name: s.name,
39
39
  rows: s.rows.filter(r => !r.hidden).slice(0,30).map(r => ({ number: r.number, cells: Object.values(s.cells).filter(c => c.row === r.number && !c.hidden && c.value.trim()).map(c => ({ column: c.column, label: c.value, recommendations: mappingRecommendations(c.value,FIELD_CATALOG) })) })),
package/lib/index.js CHANGED
@@ -7,7 +7,16 @@ import { renderEnrichmentResult } from './diagnostics.js';
7
7
  export { previewBytes, previewFile, writeCopy } from './workflow.js';
8
8
  export const name = 'form-fill-agent';
9
9
  export const inject = [];
10
+ export const INITIAL_GUIDANCE_POLICY = 'AI填表业务边界:仅支持上传的 Excel 模板,按用户明确的主体定位列补充已确认字段;只填空白,保留公式和格式,生成新文件。首页初始引导是示例,不是已上传文件、已识别列或查询授权。若用户直接发送引导、仍含未补全的【】占位符、未上传 Excel 或未说明主体定位列,请用中文先询问缺失信息,不创建任务,不调用 form_fill_enrich、Provider、MCP、OCR 或其他业务工具。已有真实任务与已保存定位映射时可沿用任务信息,不重复询问。不得宣传 Word、覆盖源模板或外部写回。';
11
+ export function initialGuidanceSection(context) {
12
+ return context?.agent?.session?.id?.startsWith('session-dsh-form-fill-agent-') ? INITIAL_GUIDANCE_POLICY : '';
13
+ }
10
14
  export function apply(ctx, config = {}) {
15
+ ctx.inject(['systemPrompt'], scope => {
16
+ if(typeof scope.systemPrompt?.section!=='function')return;
17
+ const dispose = scope.systemPrompt.section({name:'dsh-form-fill-agent/initial-guidance',order:120,text:initialGuidanceSection});
18
+ scope.effect?.(() => () => dispose?.());
19
+ });
11
20
  ctx.inject(['webServer'], scope => {
12
21
  let toolService;
13
22
  const taskDirectory = config.taskDirectory ?? profileTaskDirectory(import.meta.url);
@@ -17,7 +26,7 @@ export function apply(ctx, config = {}) {
17
26
  toolService = tools;
18
27
  const disposeTool = tools.register({
19
28
  name: 'form_fill_enrich',
20
- 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.',
29
+ description: 'Use QCC to fill an uploaded AI填表 task. Invoke only for a real uploaded Excel task with a saved entity locating column and explicit user enrichment request. If these are missing or the message is an unfilled guidance/example, ask for the Excel template and locating column without calling tools. Returns counts and a preview link; the user confirms cell changes in the workbench.',
21
30
  parameters: { type: 'object', additionalProperties: false, properties: { taskId: { type: 'string' }, expectedRevision: { type: 'integer' }, mode:{type:'string',enum:['all','retry']} }, required: ['taskId'] },
22
31
  output: {
23
32
  schema: { type: 'object', properties: { taskId: { type: 'string' }, filled: { type: 'integer' }, incomplete: { type: 'integer' }, diagnostics:{type:'object'}, previewPath: { type: 'string' } }, required: ['taskId','filled','incomplete','previewPath'] },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-form-fill-agent",
3
- "version": "0.2.30",
3
+ "version": "0.2.32",
4
4
  "private": false,
5
5
  "description": "AI form filling and spreadsheet autofill for XLSX in DeepSeek Harness, using authorized Qichacha MCP facts with cell previews, user confirmation and export to a new copy.",
6
6
  "repository": {
@@ -34,7 +34,7 @@
34
34
  },
35
35
  "dependencies": {
36
36
  "form-fill-core": "0.2.10",
37
- "qcc-form-fill-provider": "0.2.2"
37
+ "qcc-form-fill-provider": "0.2.3"
38
38
  },
39
39
  "peerDependencies": {
40
40
  "dsh-better-sidebar": ">=0.17.1 <0.19.0"