dsh-form-fill-agent 0.2.26 → 0.2.28
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 +5 -3
- package/lib/brand.css +2 -0
- package/lib/client.js +2 -1
- package/lib/http.js +24 -13
- package/lib/index.js +7 -6
- package/lib/profile-storage.js +20 -0
- package/lib/provider-outcome.js +14 -0
- package/lib/task-presentation.js +3 -2
- package/lib/task-store.js +4 -2
- package/lib/ui.html +22 -14
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# AI填表智能体
|
|
2
2
|
|
|
3
|
-
> 0.2.
|
|
3
|
+
> 0.2.28:会话隔离的只读历史、维度搜索与真实查询进度;侧栏仍为可选依赖。
|
|
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.
|
|
14
|
+
dsh plugin --profile web add dsh-form-fill-agent@0.2.28
|
|
15
15
|
```
|
|
16
16
|
|
|
17
17
|
只需 Node.js >=22、完整 DSH 与主包自动安装的内核/Provider。Better Sidebar 和 dsh-context 都是可选项,不会由基础安装强装。
|
|
@@ -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.
|
|
105
|
+
补充要求为模型可读文本;字段或工作表范围请通过向导/工作台修改,不能仅改文字后假定执行范围已同步。任务 schema 5 可读取 schema 1–5;回退旧版本请使用升级前目录副本或独立任务目录。主包为 0.2.28,共享内核为 0.2.10,Provider 为 0.2.2,共享字段契约为 0.1.0。
|
|
106
106
|
|
|
107
107
|
支持企业完整登记名称或 18 位信用代码;简称、主体不一致或多候选不会自动猜选。仅有信用代码的表格也可作为查询锚点。连接候选检索工具时可检索简称,仍需人工确认;缺少工具时降级人工输入。字段映射采用确定性规则及人工确认,不让模型编造事实。支持普通内部公式、固定区域下拉、基础条件格式和普通表格对象;复杂 Excel 结构和 Word 暂不支持,保真边界见下方安全说明。
|
|
108
108
|
|
|
@@ -125,3 +125,5 @@ dsh plugin --profile web add dsh-form-fill-agent@latest
|
|
|
125
125
|
MIT License.
|
|
126
126
|
|
|
127
127
|
任务结果报告与客户副本分开下载,采用中文状态和已记录的来源。新查询保留实际 MCP server/工具;旧记录缺少 server 时明确提示,不补造。原 JSON 制品接口继续兼容,页面默认提供 XLSX。
|
|
128
|
+
|
|
129
|
+
任务报告在确认后的摘要旁以次要文字链接下载;主结果区只展示已填副本的预览和下载,避免混淆。
|
package/lib/brand.css
CHANGED
|
@@ -74,3 +74,5 @@
|
|
|
74
74
|
|
|
75
75
|
/* Spreadsheet previews: compact rows, stationary location column. */
|
|
76
76
|
.ff-ui .scroll td{max-width:420px;overflow:hidden;text-overflow:ellipsis;padding:8px 12px}.ff-ui .scroll th:first-child,.ff-ui .scroll td:first-child{position:sticky;left:0;background:var(--head);z-index:2}.ff-ui .scroll th:first-child{z-index:3}.ff-ui #downloads{display:block}.ff-ui #downloads>.toolbar{justify-content:flex-start;margin:12px 0}
|
|
77
|
+
|
|
78
|
+
.ff-ui .ff-download-summary{display:flex;flex-wrap:wrap;align-items:center;gap:8px 16px;min-width:0}.ff-ui .ff-download-summary p{margin:8px 0}.ff-ui .ff-report-link{color:var(--muted);font-size:13px;text-decoration:underline;padding:8px 0;overflow-wrap:anywhere}.ff-ui .ff-report-link:hover{color:var(--action)}
|
package/lib/client.js
CHANGED
|
@@ -134,7 +134,7 @@ window.__ModuleLoader__.load({
|
|
|
134
134
|
function WorkbenchTab({scope,store,tab,visible}){
|
|
135
135
|
const id=scope.sessionId,frame=React.useRef(null),ready=React.useRef(false);
|
|
136
136
|
const [view,setView]=React.useState(()=>views.get(id)||{id,step:'import'});
|
|
137
|
-
const [src]=React.useState(()=>'/form-fill/?session='+encodeURIComponent(id)+(sessionTasks.has(id)?'#task='+sessionTasks.get(id):''));
|
|
137
|
+
const [src]=React.useState(()=>'/form-fill/?workspace='+encodeURIComponent(ctx.workspaces?.list?.getSnapshot?.().items?.find(w=>w.sessionIds?.includes(id))?.workspaceId||'')+'&session='+encodeURIComponent(id)+(sessionTasks.has(id)?'#task='+sessionTasks.get(id):''));
|
|
138
138
|
React.useEffect(()=>{
|
|
139
139
|
const update=()=>{const next=views.get(id);if(next)setView(next)};viewListeners.add(update);
|
|
140
140
|
const detach=sidebar?.attach(scope,store,tab);return()=>{viewListeners.delete(update);detach?.()};
|
|
@@ -148,6 +148,7 @@ window.__ModuleLoader__.load({
|
|
|
148
148
|
React.useEffect(()=>{
|
|
149
149
|
const receive=async event=>{
|
|
150
150
|
if(event.origin!==location.origin||event.source!==frame.current?.contentWindow)return;
|
|
151
|
+
if(event.data?.type==='ff-open-origin'){const target=event.data.sessionId;const known=ctx.workspaces?.list?.getSnapshot?.().items?.some(w=>w.sessionIds?.includes(target));try{if(current()!==id||!owned(target)||!known||typeof ctx.sessions.open!=='function')throw Error('来源会话不可用');await ctx.sessions.open(target)}catch{frame.current?.contentWindow?.postMessage({type:'ff-origin-unavailable'},location.origin)}return}
|
|
151
152
|
if(event.data?.type==='ff-capabilities'){frame.current.contentWindow.postMessage({type:'ff-capabilities-result',mappingDraft:2},location.origin);return}
|
|
152
153
|
if(event.data?.type==='ff-view'&&ready.current&&['import','rules','identity','preview','download','history'].includes(event.data.step)){views.set(view.id,{id:view.id,step:event.data.step});return}
|
|
153
154
|
if(event.data?.type==='ff-wizard-state'){if(!event.data.open&&event.data.focusComposer)setTimeout(()=>{if(current()===view.id)document.querySelector('[data-composer-card] textarea, [data-composer-card] [contenteditable=true]')?.focus()},0);return}
|
package/lib/http.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { providerOutcome } from './provider-outcome.js';
|
|
1
2
|
import { randomUUID, createHash } from 'node:crypto';
|
|
2
3
|
import { readFile } from 'node:fs/promises';
|
|
3
4
|
import { applyChangeSet, parseWorkbook } from 'form-fill-core';
|
|
@@ -19,7 +20,8 @@ export function createFormFillHandler({ basePath = '', getPort, now = Date.now,
|
|
|
19
20
|
const controllers=new Map();
|
|
20
21
|
const validOwner = value => typeof value === 'string' && /^[a-f0-9]{64}$/.test(value);
|
|
21
22
|
const visible = (task, owner) => task && (!task.owner || task.owner === owner);
|
|
22
|
-
const
|
|
23
|
+
const inSession=(task,session)=>task && (task.originSessionId||task.sessionId||'')===(session||'');
|
|
24
|
+
const metadata = (id, task) => ({ id, presentation:taskPresentation(task,now()), 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, originSessionId:task.originSessionId||task.sessionId||null, originWorkspaceId:task.originWorkspaceId||null, confirmed: !!task.result });
|
|
23
25
|
const settings = task => ({
|
|
24
26
|
mappingProtocol: 2,
|
|
25
27
|
diagnostics: diagnostics(task.preview),
|
|
@@ -56,20 +58,23 @@ export function createFormFillHandler({ basePath = '', getPort, now = Date.now,
|
|
|
56
58
|
const url = new URL(request.url, origin);
|
|
57
59
|
if (url.pathname !== basePath && !url.pathname.startsWith(basePath + '/')) return send(404, { message: '路径不存在' });
|
|
58
60
|
const path = url.pathname.slice(basePath.length) || '/';
|
|
61
|
+
const session=request.headers['x-form-fill-session']||url.searchParams.get('session')||'';
|
|
62
|
+
const accessible=task=>visible(task,owner)&&inSession(task,session);
|
|
59
63
|
if (request.method === 'GET' && path === '/') return send(200, await readFile(new URL('./ui.html', import.meta.url)), 'text/html; charset=utf-8');
|
|
60
64
|
if (request.method === 'GET' && path === '/brand.css') return send(200, await readFile(new URL('./brand.css', import.meta.url)), 'text/css; charset=utf-8');
|
|
61
65
|
if (request.method === 'GET' && path === '/health') return send(200, { plugin: 'form-fill-agent', product: 'AI填表', version: '0.1.0-alpha.7', provider: 'mock-only', qccAvailable: !!getQccStatus(), companionRequired: false, taskStorage: taskDirectory ? 'disk' : 'memory' });
|
|
62
|
-
if (request.method === 'GET' &&
|
|
66
|
+
if (request.method === 'GET' && ['/tasks','/history'].includes(path)) {
|
|
63
67
|
if (!validOwner(owner)) return send(403, { message: '需要本地任务访问凭据' });
|
|
64
|
-
|
|
68
|
+
const list=[...tasks].filter(([,t])=>t.owner===owner&&(path==='/history'||inSession(t,session))).map(([id,t])=>{const m=metadata(id,t);return path==='/history'?{id:m.id,filename:m.filename,title:m.presentation.title,status:m.presentation.status,created:m.created,updatedAt:m.updatedAt,expiresAt:m.expiresAt,originSessionId:m.originSessionId,originWorkspaceId:m.originWorkspaceId,confirmed:m.confirmed,readOnly:true}:m}).sort((a,b)=>b.updatedAt-a.updatedAt);
|
|
69
|
+
return send(200,list);
|
|
65
70
|
}
|
|
66
71
|
if (request.method === 'GET' && path.startsWith('/task/')) {
|
|
67
72
|
const id = path.slice(6), task = tasks.get(id);
|
|
68
|
-
if (!
|
|
73
|
+
if (!accessible(task)) return send(404, { message: '任务不存在或已过期' });
|
|
69
74
|
return send(200, { ...task.preview, ...metadata(id,task), ...settings(task) });
|
|
70
75
|
}
|
|
71
76
|
if(request.method==='GET'&&path.startsWith('/grid/')){
|
|
72
|
-
const task=tasks.get(path.slice(6));if(!
|
|
77
|
+
const task=tasks.get(path.slice(6));if(!accessible(task))return send(404,{message:'任务不存在或已过期'});
|
|
73
78
|
return send(200,gridPage(task,url.searchParams));
|
|
74
79
|
}
|
|
75
80
|
if (request.method === 'GET' && path.startsWith('/fixture/')) {
|
|
@@ -79,7 +84,7 @@ export function createFormFillHandler({ basePath = '', getPort, now = Date.now,
|
|
|
79
84
|
}
|
|
80
85
|
if (request.method === 'GET' && path.startsWith('/download/')) {
|
|
81
86
|
const parts = path.split('/'), [, , id, kind] = parts, task = tasks.get(id);
|
|
82
|
-
if (parts.length !== 4 || !
|
|
87
|
+
if (parts.length !== 4 || !accessible(task) || !task?.result) return send(404, { message: '请先确认写回,或预览已过期' });
|
|
83
88
|
if(kind==='report')return send(200,reportWorkbook(task),XLSX_TYPE);
|
|
84
89
|
if(kind==='report-preview')return send(200,{rows:reportRows(task)});
|
|
85
90
|
if (kind === 'xlsx') return send(200, task.result.bytes, XLSX_TYPE);
|
|
@@ -109,12 +114,13 @@ export function createFormFillHandler({ basePath = '', getPort, now = Date.now,
|
|
|
109
114
|
if (disposed) return send(503, { message: '插件已卸载' });
|
|
110
115
|
const filename = typeof body.filename === 'string' ? body.filename.replace(/[\\/\x00-\x1f]/g,'').slice(0,180) : '未命名表格.xlsx';
|
|
111
116
|
if (owner && !validOwner(owner)) return send(400, { message: '任务访问凭据无效' });
|
|
112
|
-
|
|
117
|
+
if((body.sessionId||'')!==session)return send(403,{message:'来源会话与当前请求不一致'});
|
|
118
|
+
const task = { originSessionId:session||undefined,originWorkspaceId:typeof body.originWorkspaceId==='string'?body.originWorkspaceId.slice(0,160):undefined, bytes, preview, analyzeOnly: !!body.analyzeOnly, created: now(), updatedAt: now(), revision: 1, state: 'preview_ready', owner, filename, sessionId: typeof body.sessionId === 'string' ? body.sessionId.slice(0,160) : undefined };
|
|
113
119
|
tasks.set(id, task);
|
|
114
120
|
return send(200, { ...preview, ...metadata(id,task), ...settings(task) });
|
|
115
121
|
}
|
|
116
122
|
const task = tasks.get(body.id);
|
|
117
|
-
if (!
|
|
123
|
+
if (!accessible(task)) return send(404, { message: '预览已过期,请重新上传' });
|
|
118
124
|
if ((task.owner || body.expectedRevision !== undefined) && body.expectedRevision !== (task.revision ?? 1)) return send(409, { code: 'REVISION_CONFLICT', message: '任务已更新,请刷新后重试' });
|
|
119
125
|
if(path==='/cancel'){
|
|
120
126
|
const controller=controllers.get(body.id);if(!controller)return send(409,{message:'任务当前未运行'});
|
|
@@ -163,29 +169,34 @@ export function createFormFillHandler({ basePath = '', getPort, now = Date.now,
|
|
|
163
169
|
};
|
|
164
170
|
return {
|
|
165
171
|
handler,
|
|
166
|
-
async enrich(id, provider, expectedRevision, {retryOnly=false}={}) {
|
|
172
|
+
async enrich(id, provider, expectedRevision, {retryOnly=false,sessionId}={}) {
|
|
167
173
|
sweep();
|
|
168
174
|
const task = tasks.get(id);
|
|
169
175
|
if (disposed || !task || task.result || running.has(id)) throw Error('任务不存在、已确认或正在执行');
|
|
176
|
+
if((task.originSessionId||task.sessionId)&&!inSession(task,sessionId))throw Error('来源会话不匹配,不能跨会话执行');
|
|
170
177
|
if ((task.owner || expectedRevision !== undefined) && expectedRevision !== (task.revision ?? 1)) throw Error('REVISION_CONFLICT:任务已更新');
|
|
171
178
|
running.add(id);
|
|
172
179
|
const controller=new AbortController();controllers.set(id,controller);
|
|
173
|
-
let active = { ...task, state: 'enriching', progress:{completed:0,total:0}, updatedAt: now(), revision: (task.revision ?? 1) + 1 };
|
|
180
|
+
let active = { ...task, state: 'enriching', progress:{completed:0,total:0,startedAt:now(),currentAction:'准备填写计划',outcomes:{},elapsedMs:0}, updatedAt: now(), revision: (task.revision ?? 1) + 1 };
|
|
174
181
|
const excluded=new Set(allCandidates(task).changes.filter(c=>!task.preview.changeSet.changes.some(s=>s.id===c.id)).map(c=>c.id));
|
|
175
182
|
const update=(preview,state,progress)=>{
|
|
176
183
|
if(disposed||tasks.get(id)!==active)throw Error('任务已过期或被替换');
|
|
177
184
|
const baseChangeSet=preview.changeSet;
|
|
178
185
|
const selected=selectCandidates(baseChangeSet,baseChangeSet.changes.filter(c=>!excluded.has(c.id)).map(c=>c.id));
|
|
179
|
-
active={...active,baseChangeSet,preview:{...preview,changeSet:selected},state,progress,updatedAt:now(),revision:active.revision+1};tasks.set(id,active);
|
|
186
|
+
active={...active,baseChangeSet,preview:{...preview,changeSet:selected},state,progress:{...active.progress,...progress,elapsedMs:Math.max(0,now()-active.progress.startedAt),...(state==='enriching'?{}:{currentAction:'处理结束',endedAt:now()})},updatedAt:now(),revision:active.revision+1};tasks.set(id,active);
|
|
180
187
|
};
|
|
181
188
|
try {
|
|
182
189
|
tasks.set(id, active);
|
|
183
|
-
const
|
|
190
|
+
const observed={...provider,lookup:async(request,options)=>{
|
|
191
|
+
active={...active,progress:{...active.progress,currentAction:'查询企业数据',recentItem:String(request.anchor?.company_name||request.anchor?.credit_no||'').slice(0,180)},updatedAt:now(),revision:active.revision+1};tasks.set(id,active);
|
|
192
|
+
let result;try{result=await provider.lookup(request,options)}catch(error){result={status:'error'};throw error}finally{if(!disposed&&tasks.get(id)===active&&active.state==='enriching'){const outcome=providerOutcome(result);active={...active,progress:{...active.progress,outcomes:{...active.progress.outcomes,[outcome]:(active.progress.outcomes[outcome]||0)+1}}};tasks.set(id,active)}}return result;
|
|
193
|
+
}};
|
|
194
|
+
const preview = await previewBytes(task.bytes, { provider:observed, 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}) });
|
|
184
195
|
if (disposed || tasks.get(id) !== active) throw Error('任务已过期或被替换');
|
|
185
196
|
update(preview,controller.signal.aborted?'cancelled':preview.changeSet.incomplete.some(i => i.reason === 'provider-error') ? 'partial' : 'preview_ready',active.progress);
|
|
186
197
|
return { taskId: id, filled: active.preview.changeSet.changes.length, incomplete: active.preview.changeSet.incomplete.length, diagnostics:diagnostics(active.preview), previewPath: basePath + '/#task=' + id };
|
|
187
198
|
} catch (error) {
|
|
188
|
-
if (!disposed && tasks.get(id) === active) tasks.set(id, { ...active, state: 'failed', updatedAt: now(), revision: active.revision + 1 });
|
|
199
|
+
if (!disposed && tasks.get(id) === active) tasks.set(id, { ...active, state: 'failed', progress:{...active.progress,currentAction:'执行失败',endedAt:now(),elapsedMs:Math.max(0,now()-active.progress.startedAt)},updatedAt: now(), revision: active.revision + 1 });
|
|
189
200
|
throw error;
|
|
190
201
|
} finally { running.delete(id);controllers.delete(id); }
|
|
191
202
|
},
|
package/lib/index.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
+
import { providerOutcome } from './provider-outcome.js';
|
|
1
2
|
import { createFormFillHandler } from './http.js';
|
|
2
3
|
import { createCatalogProvider, CATALOG_TOOL_DOMAINS, runtimeToolNames, REGISTRATION_TOOL, ENTITY_TOOL } from 'qcc-form-fill-provider';
|
|
3
4
|
import { randomUUID } from 'node:crypto';
|
|
4
|
-
import {
|
|
5
|
+
import { profileTaskDirectory } from './profile-storage.js';
|
|
5
6
|
import { renderEnrichmentResult } from './diagnostics.js';
|
|
6
7
|
export { previewBytes, previewFile, writeCopy } from './workflow.js';
|
|
7
8
|
export const name = 'form-fill-agent';
|
|
@@ -9,7 +10,7 @@ export const inject = [];
|
|
|
9
10
|
export function apply(ctx, config = {}) {
|
|
10
11
|
ctx.inject(['webServer'], scope => {
|
|
11
12
|
let toolService;
|
|
12
|
-
const taskDirectory = config.taskDirectory ?? (
|
|
13
|
+
const taskDirectory = config.taskDirectory ?? profileTaskDirectory(import.meta.url);
|
|
13
14
|
const service = createFormFillHandler({ basePath: '/form-fill', getPort: () => scope.webServer.port, taskDirectory, ttlMs: 86400000, getQccStatus: () => ['mcp__qcc-company__','mcp__company__','mcp__qcc_company__'].some(p => toolService?.get?.(p + REGISTRATION_TOOL)) });
|
|
14
15
|
scope.inject?.(['tools'], toolScope => {
|
|
15
16
|
const tools = toolScope.tools;
|
|
@@ -24,7 +25,7 @@ export function apply(ctx, config = {}) {
|
|
|
24
25
|
},
|
|
25
26
|
async execute(args, execution) {
|
|
26
27
|
if (!execution?.agent || !execution?.token) throw Error('需要 Agent-owned 工具执行上下文');
|
|
27
|
-
const runtimeSources=new Map();
|
|
28
|
+
const runtimeSources=new Map();let transportOutcome;
|
|
28
29
|
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 }) => {
|
|
29
30
|
if (!Object.hasOwn(CATALOG_TOOL_DOMAINS,name)) throw Error('不支持的 QCC 工具');
|
|
30
31
|
const names = runtimeToolNames(name);
|
|
@@ -32,10 +33,10 @@ export function apply(ctx, config = {}) {
|
|
|
32
33
|
if (!selected) throw Error('请先连接企查查企业数据 MCP');
|
|
33
34
|
runtimeSources.set(name,selected);
|
|
34
35
|
const result = await tools.execute({ name: selected, arguments: arguments_, signal, callId: randomUUID(), rootCallId: execution.rootCallId, parent: execution.token, agent: execution.agent });
|
|
35
|
-
return result?.isError ? { isError: true } :
|
|
36
|
+
const raw=result?.value;const observed=providerOutcome(raw);if(observed==='no-permission')transportOutcome=observed;return result?.isError ? { isError: true } : raw;
|
|
36
37
|
} });
|
|
37
|
-
const lookup=provider.lookup.bind(provider);provider.lookup=async(...input)=>{const result=await lookup(...input);for(const value of Object.values(result.values||{})){const match=/^qcc:\/\/([^/]+)\//.exec(value.source||'');if(match&&runtimeSources.has(match[1]))value.source=value.source.replace('qcc://'+match[1]+'/', 'qcc://'+runtimeSources.get(match[1])+'/')}return result};
|
|
38
|
-
return service.enrich(args.taskId, provider, args.expectedRevision,{retryOnly:args.mode==='retry'});
|
|
38
|
+
const lookup=provider.lookup.bind(provider);provider.lookup=async(...input)=>{transportOutcome=undefined;const result=await lookup(...input);if(transportOutcome)result.outcome=transportOutcome;for(const value of Object.values(result.values||{})){const match=/^qcc:\/\/([^/]+)\//.exec(value.source||'');if(match&&runtimeSources.has(match[1]))value.source=value.source.replace('qcc://'+match[1]+'/', 'qcc://'+runtimeSources.get(match[1])+'/')}return result};
|
|
39
|
+
return service.enrich(args.taskId, provider, args.expectedRevision,{retryOnly:args.mode==='retry',sessionId:execution.agent.session?.id});
|
|
39
40
|
},
|
|
40
41
|
});
|
|
41
42
|
toolScope.effect?.(() => () => disposeTool?.());
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { dirname, join } from 'node:path';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
|
|
5
|
+
// Resolve the installed profile manifest, never a home-wide shared task store.
|
|
6
|
+
// Linked development packages outside a profile use memory unless explicitly configured.
|
|
7
|
+
export function profileTaskDirectory(moduleUrl) {
|
|
8
|
+
let directory = dirname(fileURLToPath(moduleUrl));
|
|
9
|
+
while (true) {
|
|
10
|
+
try {
|
|
11
|
+
const manifest = JSON.parse(readFileSync(join(directory, 'package.json'), 'utf8'));
|
|
12
|
+
if (manifest.private === true && Array.isArray(manifest.dsh?.profile?.bundles)) {
|
|
13
|
+
return join(directory, 'form-fill-tasks');
|
|
14
|
+
}
|
|
15
|
+
} catch { /* Not a profile manifest. */ }
|
|
16
|
+
const parent = dirname(directory);
|
|
17
|
+
if (parent === directory) return undefined;
|
|
18
|
+
directory = parent;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
// Observation only: never turns a failed response into usable business values.
|
|
2
|
+
export function providerOutcome(result) {
|
|
3
|
+
const code=String(result?.code??result?.error?.code??'').toLowerCase();
|
|
4
|
+
if(['403','401','forbidden','unauthorized','permission-denied','no-permission'].includes(code))return 'no-permission';
|
|
5
|
+
if(result?.outcome==='no-permission')return 'no-permission';
|
|
6
|
+
if(result?.status==='not-required')return 'not-required';
|
|
7
|
+
if(result?.status==='not-found')return 'zero-records';
|
|
8
|
+
if(result?.status==='exact')return Object.keys(result.values||{}).length?'data':'zero-records';
|
|
9
|
+
if(result?.status==='ambiguous')return 'review';
|
|
10
|
+
if(result?.code==='qcc-response-invalid')return 'unknown';
|
|
11
|
+
if(result?.isError||result?.status==='error'||result?.status==='cancelled')return 'failed';
|
|
12
|
+
return 'unknown';
|
|
13
|
+
}
|
|
14
|
+
export const OUTCOME_LABELS=Object.freeze({data:'成功有数据','zero-records':'成功零记录','not-required':'无需执行','no-permission':'无权限',failed:'失败',unknown:'未知',review:'需核验'});
|
package/lib/task-presentation.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { parseWorkbook } from 'form-fill-core';
|
|
2
2
|
|
|
3
3
|
const retained = new Set(['no-data','user-excluded','formula-preserved','hidden-or-merged','hidden-row','hidden-sheet','sheet-excluded','protected-source-range','placeholder-needs-confirmation']);
|
|
4
|
-
export function taskPresentation(task) {
|
|
4
|
+
export function taskPresentation(task,now=Date.now()) {
|
|
5
5
|
const items=task.preview.changeSet.incomplete;
|
|
6
6
|
const failures=Math.max(task.state==='failed'?1:0,items.filter(i=>i.reason==='provider-error').length);
|
|
7
7
|
const review=items.filter(i=>i.reason!=='provider-error'&&!retained.has(i.reason)).length;
|
|
@@ -26,6 +26,7 @@ export function taskPresentation(task) {
|
|
|
26
26
|
}
|
|
27
27
|
}
|
|
28
28
|
const date=new Intl.DateTimeFormat('zh-CN',{timeZone:'Asia/Shanghai',month:'2-digit',day:'2-digit',hour:'2-digit',minute:'2-digit',hour12:false}).format(new Date(task.created)).replace('/', '-');
|
|
29
|
-
|
|
29
|
+
const elapsedMs=task.progress?.startedAt!==undefined?Math.max(0,(running?now:task.progress.endedAt||task.updatedAt)-task.progress.startedAt):0;
|
|
30
|
+
return {currentAction:task.progress?.currentAction||'尚未执行',recentItem:task.progress?.recentItem||'',elapsedMs,outcomes:task.progress?.outcomes||{},filled:task.preview.changeSet.changes.length,title:(task.filename||'未命名表格').replace(/\.xlsx$/i,'')+'|'+subjects.size+' 家企业|'+date,status,running,ended,percent,completed,total,failures,review,noData};
|
|
30
31
|
}
|
|
31
32
|
export const isResultExplanation = item => retained.has(item.reason);
|
package/lib/task-store.js
CHANGED
|
@@ -38,7 +38,7 @@ export function createTaskStore({ directory, maxTasks = 10, now = Date.now, ttlM
|
|
|
38
38
|
if(saved.baseChangeSet){assertChangeSet(saved.baseChangeSet);if(saved.baseChangeSet.planId!==saved.preview.plan.planId)throw Error('Candidate plan mismatch');}
|
|
39
39
|
if (saved.preview.plan.documentHash !== doc.documentHash || saved.preview.changeSet.planId !== saved.preview.plan.planId) throw Error('Persisted document mismatch');
|
|
40
40
|
}
|
|
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
|
+
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'), originWorkspaceId:saved.originWorkspaceId,originSessionId:saved.originSessionId||saved.sessionId,sessionId: saved.sessionId };
|
|
42
42
|
if (saved.confirmed && task.preview) task.result = applyChangeSet(bytes,task.preview.plan,task.preview.changeSet,{confirmChangeSetId:task.preview.changeSet.changeSetId});
|
|
43
43
|
tasks.set(id,task);
|
|
44
44
|
}
|
|
@@ -50,9 +50,11 @@ export function createTaskStore({ directory, maxTasks = 10, now = Date.now, ttlM
|
|
|
50
50
|
get: id => tasks.get(id),
|
|
51
51
|
set(id, task) {
|
|
52
52
|
if (!idPattern.test(id)) throw Error('Invalid task ID');
|
|
53
|
+
const prior=tasks.get(id);if(prior&&prior.originWorkspaceId!==task.originWorkspaceId)throw Error('任务来源工作区不可重绑定');if(prior?.result&&!task.result)throw Error('已确认终态不可回退');
|
|
54
|
+
if(prior&&(prior.originSessionId||prior.sessionId)!==(task.originSessionId||task.sessionId))throw Error('任务来源会话不可重绑定');
|
|
53
55
|
if (!tasks.has(id) && tasks.size >= maxTasks) throw Error('Task capacity exceeded');
|
|
54
56
|
if (root) {
|
|
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 });
|
|
57
|
+
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,originWorkspaceId:task.originWorkspaceId,originSessionId:task.originSessionId||task.sessionId,sessionId:task.sessionId });
|
|
56
58
|
if (Buffer.byteLength(json) > 48*1024*1024) throw Error('Task too large');
|
|
57
59
|
const temp = join(root,'.'+randomUUID()+'.tmp');
|
|
58
60
|
try { writeFileSync(temp,json,{mode:0o600,flag:'wx'});renameSync(temp,join(root,id+'.json')); }
|
package/lib/ui.html
CHANGED
|
@@ -5,12 +5,12 @@
|
|
|
5
5
|
<body class="ff-ui ff-workbench">
|
|
6
6
|
<header class="ff-workbench-head"><div class="ff-brand"><span class="ff-mark" data-icon="table"></span><h1>AI填表工作台</h1></div><p class="muted">识别空位,核验来源,确认后生成新副本。</p></header><nav class="task-tabs" aria-label="任务视图"><button id="current-tab" aria-pressed="true">当前任务</button><button id="history-tab" aria-pressed="false">任务历史</button></nav>
|
|
7
7
|
<p id="status" role="status" aria-live="polite"></p>
|
|
8
|
-
<section id="history" hidden><h2
|
|
8
|
+
<section id="history" hidden><h2>任务历史(只读)</h2><p class="muted">只显示当前存储实例内持有相同访问凭据的任务元数据。过期任务自动清理。</p><div id="history-list"></div></section>
|
|
9
9
|
<main id="current">
|
|
10
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
12
|
<details><summary>任务详情</summary><p id="task-details"></p></details>
|
|
13
|
-
<div id="progress-panel" hidden><progress id="task-progress" max="100" aria-label="处理进度"></progress><p id="progress-label" role="status"></p><span id="review-count" class="ff-review-count"></span> <span id="failure-count" class="ff-failure-count"></span><p class="muted">100% 只代表处理结束,填写内容需在预览中核验并确认生成副本。</p></div>
|
|
13
|
+
<div id="progress-panel" hidden><progress id="task-progress" max="100" aria-label="处理进度"></progress><p id="progress-label" role="status"></p><p id="progress-detail"></p><span id="review-count" class="ff-review-count"></span> <span id="failure-count" class="ff-failure-count"></span><p class="muted">100% 只代表处理结束,填写内容需在预览中核验并确认生成副本。</p></div>
|
|
14
14
|
<div class="toolbar ff-execution-toolbar"><span id="execution-progress" role="status"></span><button id="cancel-run" disabled>取消查询</button><button id="retry-run" disabled>仅重试失败 / 未完成查询</button></div>
|
|
15
15
|
<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>
|
|
16
16
|
<div class="card" data-pane="import"><h2>导入表格</h2><p class="muted">支持 XLSX,最大 8 MiB。支持普通内部公式、固定文本或区域下拉、基础条件格式及普通表格对象;宏、图片、图表及复杂扩展结构暂不支持。上传仅做本地分析,不查询企查查。</p>
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
<div class="toolbar"><label>筛选单元格 <input id="result-search" type="search" placeholder="位置、字段或值"></label><span id="result-count" class="muted"></span></div>
|
|
24
24
|
<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>
|
|
25
25
|
<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>
|
|
26
|
-
<div class="card" data-pane="download"><h2>确认并下载</h2><p>原文件不会被覆盖。下载包含全部已确认填写及完整清单,不受当前筛选影响。</p><button id="confirm" class="primary">确认这些填写,生成新副本</button><div id="
|
|
26
|
+
<div class="card" data-pane="download"><h2>确认并下载</h2><p>原文件不会被覆盖。下载包含全部已确认填写及完整清单,不受当前筛选影响。</p><button id="confirm" class="primary">确认这些填写,生成新副本</button><div class="ff-download-summary"><p id="download-summary"></p><a id="report-download" class="ff-report-link" hidden>下载任务结果报告</a></div><div id="downloads"></div></div>
|
|
27
27
|
<details class="ff-task-more"><summary>更多任务操作</summary><div class="controls"><button id="refresh" class="secondary">刷新任务</button><button id="discard" class="secondary">删除任务</button></div></details><div class="ff-next-action" data-pane="preview" hidden><span>核验填充值后继续,下一步生成副本。</span><button id="preview-continue" class="primary">下一步:确认下载</button></div>
|
|
28
28
|
</section></main>
|
|
29
29
|
<dialog id="wizard" aria-labelledby="wizard-title"><header class="dialog-head"><div><h2 id="wizard-title">生成 AI填表任务</h2><p class="muted">上传原表、确认填写范围,使用企查查 MCP 核验企业信息并填写空白单元格。</p></div><button id="wizard-close" aria-label="关闭提示词向导">×</button></header>
|
|
@@ -47,6 +47,8 @@ if(!/^[a-f0-9]{64}$/.test(owner||'')){owner=[...crypto.getRandomValues(new Uint8
|
|
|
47
47
|
document.cookie='ff_owner='+owner+'; SameSite=Strict; Path='+(basePath||'/');
|
|
48
48
|
let current,filename='已填表格',busy=false,step='import',pendingNavigation,wizardStep=0,wizardFields=new Set(),wizardTask,wizardStructure,expectedDraft,retryIntent=false;
|
|
49
49
|
const sessionId=new URLSearchParams(location.search).get('session')||undefined;
|
|
50
|
+
const originWorkspaceId=new URLSearchParams(location.search).get('workspace')||undefined;
|
|
51
|
+
const downloadUrl=(id,kind)=>basePath+'/download/'+id+'/'+kind+'?'+new URLSearchParams({session:sessionId||''});
|
|
50
52
|
let gridPageNumber=1,gridColumn=1,gridRequest=0,gridTask;
|
|
51
53
|
async function loadGrid(){
|
|
52
54
|
if(!current||!$('grid').open)return;const request=++gridRequest,id=current.id;
|
|
@@ -68,7 +70,7 @@ function controls(value){busy=value;document.querySelectorAll('button').forEach(
|
|
|
68
70
|
function row(target,values,labels){const tr=document.createElement('tr');values.forEach((v,i)=>{const td=document.createElement('td');td.textContent=v;td.title=String(v??'');td.dataset.label=labels[i];tr.append(td)});$(target).append(tr);return tr}
|
|
69
71
|
function syncActions(){
|
|
70
72
|
const running=current?.state==='enriching';
|
|
71
|
-
const p=current?.presentation;$('progress-panel').hidden=!p||(!p.running&&!p.ended);if(p){$('task-progress').value=p.percent;$('task-progress').dataset.ended=String(p.ended);$('progress-label').textContent=(p.running?'处理中':p.ended?'处理结束':'待处理')+' · '+p.percent+'% · '+p.completed+' / '+p.total+' 查询计划项';$('progress-label').dataset.ended=String(p.ended);$('review-count').textContent='待核验 '+p.review+' 项';$('failure-count').textContent='失败 '+p.failures+' 项';}
|
|
73
|
+
const p=current?.presentation;$('progress-panel').hidden=!p||(!p.running&&!p.ended);if(p){$('task-progress').value=p.percent;$('task-progress').dataset.ended=String(p.ended);$('progress-label').textContent=(p.running?'处理中':p.ended?'处理结束':'待处理')+' · '+p.percent+'% · '+p.completed+' / '+p.total+' 查询计划项';$('progress-label').dataset.ended=String(p.ended);$('review-count').textContent='待核验 '+p.review+' 项';$('failure-count').textContent='失败 '+p.failures+' 项';$('progress-detail').textContent=p.currentAction+' · 已处理 '+p.completed+' 项 · 可填 '+p.filled+' 格 · 无数据 '+p.noData+' 格 · 用时 '+Math.floor(p.elapsedMs/1000)+' 秒'+(p.recentItem?' · 最近:'+p.recentItem:'')+';查询结果:'+Object.entries(p.outcomes||{}).map(([k,v])=>({'data':'有数据','zero-records':'零记录','not-required':'无需执行','no-permission':'无权限',failed:'失败',unknown:'未知',review:'需核验'}[k]||'未知')+' '+v).join(' / ');}
|
|
72
74
|
$('cancel-run').hidden=!running;$('cancel-run').disabled=busy||!running;
|
|
73
75
|
$('retry-run').hidden=!current||current.confirmed||!current.changeSet.incomplete.some(i=>['provider-error','cancelled','not-started'].includes(i.reason));
|
|
74
76
|
$('retry-run').disabled=busy||!current||current.confirmed||running||!current.changeSet.incomplete.some(i=>['provider-error','cancelled','not-started'].includes(i.reason));
|
|
@@ -77,9 +79,13 @@ function syncActions(){
|
|
|
77
79
|
}
|
|
78
80
|
$('cancel-run').onclick=async()=>{try{await api('/cancel',{id:current.id});$('status').textContent='已请求取消。已发出的请求可能已在远端执行,本地不再继续后续查询。';await restore()}catch(e){$('status').textContent=e.message}};
|
|
79
81
|
$('retry-run').onclick=()=>{openWizard(true);wizardStep=3;renderWizard()};
|
|
80
|
-
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}
|
|
82
|
+
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,'X-Form-Fill-Session':sessionId||''}});const data=await response.json();if(!response.ok)throw Error(data.message);return data}
|
|
81
83
|
function navigate(target){if(target==='wizard'){openWizard();return}if(target!=='import'&&!current){$('status').textContent='请先导入表格。';return}step=target;if(parent!==window)parent.postMessage({type:'ff-view',step:target},location.origin);$('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)))}
|
|
82
|
-
function downloads(){
|
|
84
|
+
function downloads(){
|
|
85
|
+
$('downloads').replaceChildren();$('download-summary').textContent='已确认填写 '+current.changeSet.changes.length+' 格 · '+(current.presentation?.status||'副本已生成');
|
|
86
|
+
Object.assign($('report-download'),{hidden:false,href:downloadUrl(current.id,'report'),download:filename+'-任务结果报告.xlsx'});
|
|
87
|
+
const group=document.createElement('div');group.className='toolbar';const preview=document.createElement('button');preview.textContent='预览已填副本';preview.onclick=async()=>{navigate('preview');$('grid').open=true;$('grid-view').value='result';await loadGrid();$('grid').scrollIntoView({block:'start'})};const a=document.createElement('a');Object.assign(a,{className:'download',href:downloadUrl(current.id,'xlsx'),download:filename+'-已填副本.xlsx',textContent:'下载已填副本 XLSX'});group.append(preview,a);$('downloads').append(group)
|
|
88
|
+
}
|
|
83
89
|
function businessSummary(){return current?(retryIntent?'仅重试失败、取消或未执行的部分,保留成功结果。':'')+'填写文件“'+current.filename+'”中的空白字段。共 '+current.analysis.tables.length+' 个工作表,'+wizardFields.size+' 个填写字段,'+current.analysis.opportunities.filter(p=>wizardFields.has(p.field)).length+' 个待补空位置。字段范围以本次保存的映射为准。保留已有值、公式与未确认位置,核验预览后生成新副本。':'请先上传 XLSX 文件。'}
|
|
84
90
|
function command(){return current?businessSummary()+'\n'+$('requirements').value+'\n执行关联:调用 form_fill_enrich:taskId='+current.id+',expectedRevision='+current.revision+(retryIntent?',mode=retry':'')+'。字段范围以工作台确认设置为准。':''}
|
|
85
91
|
function mappingVersion(){return JSON.stringify({tables:current?.analysis.tables.map(t=>({sheet:t.sheet,headerRow:t.headerRow,anchors:t.anchors,mappings:t.mappings.map(m=>[m.column,m.field,m.role,m.evidence])})),scope:current?.selectedFields??null})}
|
|
@@ -221,7 +227,7 @@ function renderSettings(data){
|
|
|
221
227
|
role.onchange=()=>{manual=true;label.updateMappingStatus();highlightMappingConflicts()};
|
|
222
228
|
const options=()=>{
|
|
223
229
|
const group=data.catalogGroups?.find(g=>g.id===dimension.value);
|
|
224
|
-
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))));
|
|
230
|
+
const found=(data.catalog||[]).filter(f=>(matches(f,search.value)||(data.catalogGroups||[]).some(g=>g.fields.includes(f.key)&&matches({label:g.label,key:g.id},search.value)))&&(!dimension.value||(dimension.value==='other'?!data.catalogGroups?.some(g=>g.fields.includes(f.key)):group?.fields.includes(f.key))));
|
|
225
231
|
select.replaceChildren();
|
|
226
232
|
const list=[{key:'',label:'不填写'},...found];
|
|
227
233
|
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+'(当前选择,筛选外)'})}
|
|
@@ -235,7 +241,7 @@ function renderSettings(data){
|
|
|
235
241
|
select.onchange=choose;
|
|
236
242
|
select.ondblclick=finish;
|
|
237
243
|
select.onkeydown=event=>{if(event.key==='Enter'){event.preventDefault();finish()}};
|
|
238
|
-
search.oninput=options;dimension.onchange=options;
|
|
244
|
+
search.oninput=e=>{if(!e.isComposing)options()};search.oncompositionend=options;dimension.onchange=options;
|
|
239
245
|
if(!chosen&&!manual){
|
|
240
246
|
// Strip only a trailing date-format annotation for recommendations. Keep
|
|
241
247
|
// the original header and require confirmation; semantic qualifiers remain.
|
|
@@ -274,7 +280,7 @@ function render(data){
|
|
|
274
280
|
current=data;directScope();$('direct-command').hidden=true;filename=(data.filename||'已填表格.xlsx').replace(/\.xlsx$/i,'');location.hash='task='+data.id;
|
|
275
281
|
if(gridTask!==data.id){gridTask=data.id;gridPageNumber=1;gridColumn=1;$('grid-sheet').replaceChildren();$('grid-query').value=''}$('grid').hidden=false;loadGrid();
|
|
276
282
|
if(parent!==window)parent.postMessage({type:'ff-task',taskId:data.id},location.origin);
|
|
277
|
-
$('incomplete').closest('.card').style.display=(data.exceptions||data.changeSet.incomplete).length?'':'none';$('changes').replaceChildren();$('incomplete').replaceChildren();$('report-
|
|
283
|
+
$('incomplete').closest('.card').style.display=(data.exceptions||data.changeSet.incomplete).length?'':'none';$('changes').replaceChildren();$('incomplete').replaceChildren();$('report-download').hidden=true;$('report-download').removeAttribute('href');$('download-summary').textContent='';$('downloads').replaceChildren();
|
|
278
284
|
$('task-meta').dataset.state=data.state;
|
|
279
285
|
$('task-meta').textContent=(data.presentation?.title||data.filename)+' · '+(data.presentation?.status||data.state);$('task-details').textContent='任务 ID:'+data.id+' · 有效期至 '+new Date(data.expiresAt).toLocaleString();
|
|
280
286
|
$('summary').textContent='可填写 '+data.changeSet.changes.length+' 格 · '+(data.plan.paidCalls?'企查查来源':'本地分析 / 合成示例 · 费用 0')+' · 未完成 '+data.changeSet.incomplete.length+' 项';
|
|
@@ -291,8 +297,8 @@ initWizard();filterResults();syncActions();if($('wizard').open)renderWizard();
|
|
|
291
297
|
const missing=data.analysis.incomplete.filter(i=>i.reason==='missing-anchor');
|
|
292
298
|
$('anchor-alert').textContent=missing.length?'有 '+missing.length+' 行缺少可用主体标识。请把实际包含名称的列映射为“企业名称”,或提供非空信用代码;当前不会查询这些行。':'';
|
|
293
299
|
}
|
|
294
|
-
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.id!==current?.id||data.revision!==current?.revision){const wasRunning=current?.state==='enriching';render(data);if(silent&&data.state==='enriching'){navigate('identity');$('status').textContent=$('execution-progress').textContent}else if(silent&&wasRunning){navigate('preview');$('status').textContent='查询已结束,请检查填写预览。'}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}}
|
|
295
|
-
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));$('grid').open=true;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)}}
|
|
300
|
+
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.id===current?.id&&data.revision===current?.revision){current.presentation=data.presentation;syncActions()}if(!silent||data.id!==current?.id||data.revision!==current?.revision){const wasRunning=current?.state==='enriching';render(data);if(silent&&data.state==='enriching'){navigate('identity');$('status').textContent=$('execution-progress').textContent}else if(silent&&wasRunning){navigate('preview');$('status').textContent='查询已结束,请检查填写预览。'}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}}
|
|
301
|
+
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));$('grid').open=true;render(await api('/preview',{base64:btoa(binary),filename:name,sessionId,originWorkspaceId,analyzeOnly:!demo}));navigate(demo?'preview':'rules');$('status').textContent='预览已准备好,请检查后确认。'}catch(e){$('status').textContent=e.message;$('wizard-message').textContent=e.message}finally{controls(false)}}
|
|
296
302
|
document.querySelectorAll('[data-step]').forEach(b=>b.onclick=()=>navigate(b.dataset.step));
|
|
297
303
|
$('file').onchange=async()=>{const input=$('file'),f=input.files[0];if(!f||busy)return;input.disabled=true;controls(true);$('status').textContent='正在自动解析表格…';try{await preview(await f.arrayBuffer(),f.name)}catch(e){$('status').textContent=e.message}finally{input.disabled=false;input.value='';controls(false)}};
|
|
298
304
|
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]'))if(field.value||field.dataset.manual==='true')configuration.mappings.push({sheet,column:Number(field.dataset.column),field:field.value||null,...(field.dataset.primary==='true'?{role:field.closest('.ff-mapping-row').querySelector('.ff-mapping-role').value}:{})})}return configuration}
|
|
@@ -305,7 +311,7 @@ $('confirm').onclick=async()=>{if(!current||busy)return;controls(true);try{await
|
|
|
305
311
|
$('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)}};
|
|
306
312
|
$('refresh').onclick=()=>restore();
|
|
307
313
|
$('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)}};
|
|
308
|
-
$('history-tab').onclick=async()=>{try{const list=await api('/
|
|
314
|
+
$('history-tab').onclick=async()=>{try{const list=await api('/history');$('history-list').replaceChildren();for(const item of list){const div=document.createElement('div');div.className='history-item';const title=document.createElement('strong');title.textContent=(item.title||item.filename)+' · '+item.status;const details=document.createElement('details'),summary=document.createElement('summary');summary.textContent='查看历史元数据(只读)';const text=document.createElement('p');text.textContent='来源工作区:'+(item.originWorkspaceId||'旧任务未记录')+';来源会话:'+(item.originSessionId||'独立页面 / 旧任务未记录')+';任务 ID:'+item.id+';有效期至 '+new Date(item.expiresAt).toLocaleString();details.append(summary,text);const hint=document.createElement('p');hint.className='muted';hint.textContent='仅展示当前存储实例内同访问凭据的历史元数据,不加载原表,也不绑定到当前任务。';div.append(title,details,hint);if(parent!==window&&item.originSessionId){const open=document.createElement('button');open.textContent='打开来源会话';open.onclick=()=>parent.postMessage({type:'ff-open-origin',sessionId:item.originSessionId},location.origin);div.append(open)}else{const fallback=document.createElement('p');fallback.textContent='请从宿主历史进入来源会话;独立页面不支持切换宿主会话。';div.append(fallback)}$('history-list').append(div)}if(parent!==window)parent.postMessage({type:'ff-view',step:'history'},location.origin);$('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}};
|
|
309
315
|
$('current-tab').onclick=()=>navigate(step);
|
|
310
316
|
$('wizard-open').onclick=()=>directDraft();$('wizard-close').onclick=()=>closeWizard();$('wizard').addEventListener('cancel',e=>{e.preventDefault();closeWizard()});
|
|
311
317
|
document.querySelectorAll('[data-wizard-step]').forEach(b=>b.onclick=()=>{const next=Number(b.dataset.wizardStep);wizardStep=next>1&&wizardMappingApplied!==JSON.stringify([current?.id,readMapping()])?1:next;saveWizard();renderWizard()});
|
|
@@ -319,8 +325,9 @@ $('copy-command').onclick=async()=>{try{await applyScope();saveWizard();await na
|
|
|
319
325
|
$('draft').onclick=()=>sendDraft();
|
|
320
326
|
$('draft-cancel').onclick=()=>$('draft-conflict').close();
|
|
321
327
|
for(const mode of ['append','replace'])$('draft-'+mode).onclick=()=>{$('draft-conflict').close();sendDraft(mode)};
|
|
322
|
-
$('mapping-search').oninput=()
|
|
323
|
-
$('
|
|
328
|
+
$('mapping-search').oninput=e=>{if(e?.isComposing)return;const query=$('mapping-search').value.trim().toLowerCase();$('mapping').querySelectorAll('.ff-mapping-row').forEach(row=>row.hidden=!row.dataset.sourceLabel.toLowerCase().includes(query))};
|
|
329
|
+
$('mapping-search').oncompositionend=()=> $('mapping-search').oninput();
|
|
330
|
+
$('result-search').oninput=e=>{if(!e.isComposing)filterResults()};$('result-search').oncompositionend=filterResults;$('changes').onchange=filterResults;
|
|
324
331
|
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);navigate('identity');$('status').textContent='指令已回填,发送后将在此显示查询进度。'}}if(e.data?.type==='ff-navigate'){if(!current&&location.hash){pendingNavigation=e.data.step;return}if(e.data.step==='history')$('history-tab').click();else navigate(e.data.step)}});
|
|
325
332
|
api('/health').then(s=>{$('connection').textContent=s.qccAvailable?'已发现企查查工商工具,可在对话中执行。':'尚未发现企查查工商工具;请先连接 MCP。合成模板仍可体验。'}).catch(()=>{});
|
|
326
333
|
$('preview-continue').onclick=async()=>{if(busy||!current)return;controls(true);try{const ids=[...$('changes').querySelectorAll('input:checked')].map(x=>x.value);if(!current.confirmed&&JSON.stringify([...ids].sort())!==JSON.stringify([...(current.selectedIds||[])].sort()))render(await api('/select',{id:current.id,selectedIds:ids}));navigate('download');$('confirm').scrollIntoView({block:'center'});$('confirm').focus();$('status').textContent=current.confirmed?'副本已生成,请点击下载。':'请确认填写结果,点击蓝色按钮生成新副本。'}catch(e){$('status').textContent=e.message}finally{controls(false)}};
|
|
@@ -329,6 +336,7 @@ function saveMappingDraft(){if(!current)return;sessionStorage.setItem('ff-ui-map
|
|
|
329
336
|
window.addEventListener('pagehide',saveMappingDraft);
|
|
330
337
|
for(const event of ['change','click'])$('mapping').addEventListener(event,saveMappingDraft);
|
|
331
338
|
$('mapping').addEventListener('change',()=>{wizardMappingApplied=false;$('mapping-next').hidden=true;highlightMappingConflicts()});
|
|
339
|
+
window.addEventListener('message',e=>{if(e.origin===location.origin&&e.source===parent&&e.data?.type==='ff-origin-unavailable')$('status').textContent='宿主无法打开来源会话,请从宿主历史手动进入;当前任务未改变。'});
|
|
332
340
|
window.addEventListener('hashchange',()=>{if(new URLSearchParams(location.hash.slice(1)).get('task')!==current?.id)restore()});
|
|
333
341
|
async function restoreSessionTask(){if(location.hash||!sessionId){restore();return}try{const list=await api('/tasks');if(current||location.hash)return;const task=list.filter(item=>item.sessionId===sessionId).sort((a,b)=>(b.updatedAt||b.created)-(a.updatedAt||a.created))[0];if(task){location.hash='task='+task.id;await restore()}}catch(e){$('status').textContent='恢复任务失败,可在任务历史中重试:'+e.message}}
|
|
334
342
|
navigate('import');restoreSessionTask();setInterval(()=>{if(!document.hidden&¤t&&!busy&&!$('wizard').open)restore(true)},2000);
|
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.28",
|
|
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": {
|