dsh-data-cleaning-agent 0.8.19 → 0.8.21

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/CHANGELOG.md CHANGED
@@ -4,6 +4,21 @@
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [0.8.21] - 2026-09-09
8
+
9
+ - 从实际 Host 请求获取文件地址的协议和主机;修复本地 HTTP 链接被补成 HTTPS 的问题。
10
+ - 工具卡片支持同源完整文件地址,继续拒绝非同源链接。
11
+ - 已确认任务开始执行时自动展开工作台一次并跟随处理步骤;生成说明不触发,关闭后不反复弹出。
12
+ - 246 项测试及 10 组浏览器回归通过,无付费查询。
13
+
14
+ ## [0.8.20] - 2026-09-09
15
+
16
+ - 普通无记录/未披露字段保留为无数据说明,尊重共享契约 reviewRequired=false,不再误计为待核验。
17
+ - 当前任务刷新同步任务历史,拒绝旧版本历史摘要覆盖新状态。
18
+ - 增加彩色主体处理进度条;结果计数未产生时显示统计中,避免 undefined。
19
+ - 当前任务和历史复用文件名、行数、创建时间生成的展示名;任务编号收纳至详情。
20
+ - 246 项测试与 10 组浏览器回归通过。旧任务结果不自动改写,不重复调用 QCC。
21
+
7
22
  ## [0.8.19] - 2026-09-09
8
23
 
9
24
  - 受益人与主营业务收入增加待确认候选,明确首条限制及财务口径差异,不自动替换。
package/README.en.md CHANGED
@@ -8,7 +8,7 @@ Related agents: [数据清洗补全](https://github.com/duhu2000/dsh-data-cleani
8
8
 
9
9
  > A data cleaning & completion agent plugin for DeepSeek Harness: local CSV/XLSX/JSON engine plus optional Qichacha (QCC) MCP enterprise-data enrichment. Initiated and maintained by the Qichacha (QCC) team.
10
10
  >
11
- > Current source version / 当前源码版本: **0.8.19** (stable release)
11
+ > Current source version / 当前源码版本: **0.8.21** (stable release)
12
12
 
13
13
  This release unifies spreadsheet previews, column mapping, and enrichment scope across the prompt wizard and workbench. Mapping statuses use color and text; mapped fields automatically enter the scope, with optional extra fields collapsed. Multiple confirmed output columns may share a non-identity field while preserving non-empty values. It fixes saved drafts resetting the scope to five defaults and detects stale Host versions. Preparing a draft does not query QCC; sending starts execution. Fully restart DSH after upgrading. Existing download artifacts are not rewritten.
14
14
 
package/README.md CHANGED
@@ -1,13 +1,13 @@
1
1
  # dsh-data-cleaning-agent
2
2
 
3
- 本版统一向导与工作台的 Host 执行闭环:发送后自动生成新 Excel,原文件不修改,工作台入口可恢复;发布记录见 [发布说明](docs/RELEASE-0.8.19.md)。
3
+ 本版统一向导与工作台的 Host 执行闭环:发送后自动生成新 Excel,原文件不修改,工作台入口可恢复;发布记录见 [发布说明](docs/RELEASE-0.8.21.md)。
4
4
 
5
5
  ## 安装与三分钟上手
6
6
 
7
7
  数据清洗补全智能体:面向 Excel/CSV/JSON 企业名单,提供数据清洗、表格清洗、清洗补全、去重、企业数据补全与字段补全,支持企查查 MCP 和结果导出。
8
8
 
9
9
  ```sh
10
- dsh plugin --profile web add dsh-data-cleaning-agent@0.8.19
10
+ dsh plugin --profile web add dsh-data-cleaning-agent@0.8.21
11
11
  ```
12
12
 
13
13
  请先满足下文的 DSH、连接器及侧边栏依赖要求;安装后完整停止并重启对应 Profile。
@@ -25,7 +25,7 @@ dsh plugin --profile web add dsh-data-cleaning-agent@0.8.19
25
25
 
26
26
  > 在 DeepSeek Harness 中清洗、补全、画像企业名单数据的智能体插件:本地 CSV/XLSX/JSON 引擎 + 可选企查查(Qichacha/QCC)MCP 企业数据补全,由企查查(Qichacha/QCC)团队发起并维护。
27
27
  >
28
- > 当前源码版本 / Current source version: **0.8.19**(正式版本)
28
+ > 当前源码版本 / Current source version: **0.8.21**(正式版本)
29
29
 
30
30
  本版上传表格后展示完整清单及空白数量,用绿色、琥珀色、红色及文字区分已通过、待确认和未匹配映射。已映射字段自动纳入补全范围,额外字段默认折叠;多个原列可经确认使用同一非主体字段,分别补空并保留非空原值。修复 Host 草稿重置为默认五字段的问题,增加前后端版本错配提示。生成说明不查询,发送才执行;升级后需完整重启 DSH,历史下载文件不自动改写。
31
31
 
@@ -0,0 +1,9 @@
1
+ # 0.8.21 发布说明
2
+
3
+ 文件预览与下载地址使用实际请求的协议、主机,不硬编码 HTTPS 或端口;工具结果卡片接受同源完整地址。
4
+
5
+ 检测已暂存任务开始执行后自动展开右侧工作台一次,切到当前处理步骤。生成说明不会提前打开,用户关闭后同一监听不会重复打开。
6
+
7
+ 验证:246 项测试及 10 组浏览器回归。未重新执行付费查询,未修改上传文件。
8
+
9
+ 升级后完整重启 DSH,并重新生成后续任务说明。历史对话中的错误 HTTPS 文本链接不会被改写,可通过工作台打开历史文件。
package/lib/artifacts.js CHANGED
@@ -22,6 +22,7 @@ const SYSTEM_FIELD_LABELS = Object.freeze({
22
22
  risk_tags: '风险标签',
23
23
  qcc_match_status: '匹配状态',
24
24
  qcc_field_issues: '字段补全待核验原因',
25
+ qcc_field_notes: '字段无数据说明',
25
26
  qcc_source: '数据来源',
26
27
  qcc_snapshot_note: '取值口径与报告期',
27
28
  qcc_error: '错误原因',
package/lib/client.js CHANGED
@@ -1703,6 +1703,13 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
1703
1703
  throw error;
1704
1704
  }
1705
1705
 
1706
+ function workflowDisplayName(task) {
1707
+ if (task.title && !task.title.startsWith('企业数据清洗补全任务')) return task.title;
1708
+ const base = task.source?.fileName?.replace(/\.[^.]+$/, '') || (task.source?.type === 'image' ? '图片名单' : '粘贴名单');
1709
+ const date = task.createdAt ? new Date(task.createdAt) : null;
1710
+ const time = date && Number.isFinite(date.getTime()) ? date.toLocaleString() : '';
1711
+ return `${base}|${task.source?.rowCount ?? 0} 条${time ? '|' + time : ''}`;
1712
+ }
1706
1713
  function cacheWorkflowTask(actions, sessionId, task) {
1707
1714
  if (!task) return null;
1708
1715
  if (sessionId) workflowTaskBySession.set(String(sessionId), task);
@@ -1855,9 +1862,14 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
1855
1862
  return response.task;
1856
1863
  }
1857
1864
  function watchHostedCommand(actions, sessionId, command) {
1865
+ let openedForExecution = false;
1858
1866
  void waitForQccCommand(command.commandId, 1800, async (status) => {
1859
1867
  if (!status || status.state === 'prepared') return;
1860
1868
  await refreshHostedTask(actions, sessionId, command.taskId, null, true).catch(() => {});
1869
+ if (!openedForExecution && isCleaningSession(sessionId) && workflowTaskBySession.get(String(sessionId))?.id === command.taskId) {
1870
+ openedForExecution = true;
1871
+ openWorkbench(actions, workflowTaskBySession.get(String(sessionId))?.stage || 'match', sessionId);
1872
+ }
1861
1873
  if (isCleaningSession(sessionId) && workflowTaskBySession.get(String(sessionId))?.id === command.taskId) actions.setCommandProgress?.({ taskId: command.taskId, ...status.progress });
1862
1874
  }).then(async (completed) => {
1863
1875
  await refreshHostedTask(actions, sessionId, command.taskId, completed.run);
@@ -1919,8 +1931,15 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
1919
1931
  setInput: (draft, input) => { draft.input = input; },
1920
1932
  setWorkflowContract: (draft, value) => { draft.workflowContract = value; },
1921
1933
  setCommandProgress: (draft, value) => { draft.commandProgress = value; },
1922
- setWorkflowTask: (draft, value) => { draft.workflowTask = value ?? null; },
1923
- setWorkflowTasks: (draft, value) => { draft.workflowTasks = Array.isArray(value) ? value : []; },
1934
+ setWorkflowTask: (draft, value) => {
1935
+ draft.workflowTask = value ?? null;
1936
+ if (value) draft.workflowTasks = [value, ...(draft.workflowTasks || []).filter(task => task.id !== value.id)];
1937
+ },
1938
+ setWorkflowTasks: (draft, value) => {
1939
+ const cached = new Map((draft.workflowTasks || []).map(task => [task.id, task]));
1940
+ draft.workflowTasks = (Array.isArray(value) ? value : []).map(task =>
1941
+ (cached.get(task.id)?.revision ?? -1) > (task.revision ?? -1) ? cached.get(task.id) : task);
1942
+ },
1924
1943
  setTaskTitle: (draft, value) => { draft.taskTitle = String(value ?? ''); },
1925
1944
  setMappings: (draft, value) => {
1926
1945
  const next = Array.isArray(value) ? value : [];
@@ -2297,7 +2316,7 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
2297
2316
  const WORKFLOW_STATE_LABELS = {
2298
2317
  draft: '待上传', uploaded: '待确认规则', rules_confirmed: '规则已确认', diagnosed: '体检完成',
2299
2318
  matching: '匹配中', review_required: '待人工核验', matched: '匹配完成', enriching: '补全中',
2300
- export_ready: '可下载', completed: '已完成', partial: '部分完成', failed: '失败',
2319
+ export_ready: '可下载', completed: '已完成', partial: '处理结束(需核验)', failed: '执行失败',
2301
2320
  authorization_required: '待连接企查查', cancelled: '已取消', parse_failed: '解析失败',
2302
2321
  };
2303
2322
 
@@ -3214,7 +3233,7 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
3214
3233
  meta.hint ? h('div', { className: 'dcAgentToolCardHint' }, meta.hint) : null,
3215
3234
  summary ? h('div', { className: 'dcAgentToolCardBody' }, summary) : null,
3216
3235
  toolName === 'data_cleaning_qcc_run' ? h('div', { className: 'dcAgentArtifactList' },
3217
- [...summary.matchAll(/\[([^\]]+)\]\((\/data-cleaning\/api\/workflow\/tasks\/[a-zA-Z0-9%_-]+\/artifacts\/[a-zA-Z0-9%_-]+(?:\?preview=1)?)\)/g)].map((match, index) =>
3236
+ [...summary.matchAll(/\[([^\]]+)\]\(((?:https?:\/\/[a-zA-Z0-9.:[\]-]+)?\/data-cleaning\/api\/workflow\/tasks\/[a-zA-Z0-9%_-]+\/artifacts\/[a-zA-Z0-9%_-]+(?:\?preview=1)?)\)/g)].filter(match => match[2].startsWith('/') || (typeof location !== 'undefined' && new URL(match[2]).origin === location.origin)).map((match, index) =>
3218
3237
  h('a', { key: index, className: 'dcAgentButton', href: match[2], target: '_blank', rel: 'noopener noreferrer' }, match[1]))) : null,
3219
3238
  );
3220
3239
  }
@@ -4165,7 +4184,7 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
4165
4184
  ),
4166
4185
  workflowTasks.length ? h('div', { className: 'dcAgentPane', 'aria-label': '任务历史列表' },
4167
4186
  workflowTasks.map((task) => h('button', { key: task.id, type: 'button', className: 'dcAgentHistoryTask', onClick: () => resumeWorkflowTask(task) },
4168
- h('h3', null, task.title || task.id || '数据清洗任务'),
4187
+ h('h3', null, workflowDisplayName(task)),
4169
4188
  h('div', { className: 'dcAgentRow' },
4170
4189
  h('span', { className: 'dcAgentJobsPill', 'data-state': task.state ?? 'idle' }, WORKFLOW_STATE_LABELS[task.state] ?? task.state ?? '未知'),
4171
4190
  h('span', { className: 'dcAgentHint' }, `${task.source?.rowCount ?? 0} 行 · ${task.updatedAt ? new Date(task.updatedAt).toLocaleString() : ''}`),
@@ -4533,10 +4552,16 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
4533
4552
  h('div', { className: 'dcAgentWbBody' },
4534
4553
  cachedTask && step !== 'history' ? h('section', { className: 'dcAgentSection', 'aria-label': '当前任务进展', 'aria-live': 'polite' },
4535
4554
  h('b', null, WORKFLOW_STATE_LABELS[cachedTask.state] || cachedTask.state),
4536
- h('p', { className: 'dcAgentHint' }, `任务:${cachedTask.id}`),
4555
+ h('h3', null, workflowDisplayName(cachedTask)),
4556
+ h('details', null, h('summary', null, '任务详情'), h('p', { className: 'dcAgentHint' }, `任务编号:${cachedTask.id}`)),
4537
4557
  h('p', null, `${cachedTask.source?.type === 'image' ? '图片识别' : cachedTask.source?.type === 'text' ? '粘贴名单' : '上传表格'} · ${cachedTask.source?.fileName || ''} · ${cachedTask.source?.rowCount || 0} 条 · ${cachedTask.fieldSelection?.length || 0} 个补全字段`),
4538
4558
  commandProgress?.taskId === cachedTask.id && Number.isFinite(commandProgress.totalUnique) ? h('p', null, `已处理主体 ${commandProgress.completedUnique}/${commandProgress.totalUnique}(去重后,含未匹配或失败,不代表补全成功)`) : null,
4539
- h('p', null, `已补全 ${qccEnrichedCount} 条 · 待核验 ${qccReviewCount} 条(按输入行统计)`),
4559
+ commandProgress?.taskId === cachedTask.id && Number.isFinite(commandProgress.totalUnique) && commandProgress.totalUnique > 0 ? h('progress', {
4560
+ max: commandProgress.totalUnique, value: Math.min(commandProgress.totalUnique, Math.max(0, commandProgress.completedUnique || 0)),
4561
+ 'aria-label': '主体处理进度', style: { width: '100%', accentColor: cachedTask.state === 'completed' ? '#16845b' : cachedTask.state === 'failed' ? '#c62828' : cachedTask.state === 'partial' ? '#a66a00' : '#087dcc' },
4562
+ }) : null,
4563
+ h('p', { style: { color: Number(qccReviewCount) > 0 ? '#a66a00' : Number.isFinite(qccEnrichedCount) ? '#16845b' : 'inherit' } },
4564
+ Number.isFinite(qccEnrichedCount) ? `已处理并补全 ${qccEnrichedCount} 条 · 待核验 ${Number.isFinite(qccReviewCount) ? qccReviewCount : '统计中'} 条(按输入行统计;未披露字段保留空白)` : '结果统计中,处理结束后展示补全与待核验数量'),
4540
4565
  ) : null,
4541
4566
  error ? h('div', { className: 'dcAgentError', role: 'alert' }, error) : null,
4542
4567
  step !== 'history' ? h('details', { className: 'dcAgentSection' },
@@ -138,7 +138,7 @@ export class QccCommandStore {
138
138
  }
139
139
  }
140
140
 
141
- prepare(input) {
141
+ prepare(input, { artifactOrigin = '' } = {}) {
142
142
  this.cleanup(true);
143
143
  const normalized = normalizedInput(input);
144
144
  const estimate = normalized.kind === 'enrich'
@@ -148,6 +148,7 @@ export class QccCommandStore {
148
148
  const at = new Date(this.clock()).toISOString();
149
149
  const record = {
150
150
  commandId,
151
+ artifactOrigin,
151
152
  taskId: normalized.taskId,
152
153
  kind: normalized.kind,
153
154
  state: 'prepared',
@@ -265,7 +266,7 @@ export class QccCommandStore {
265
266
  state: run.state,
266
267
  summary: run.summary,
267
268
  ...(record.delivery ? { deliveryState: record.delivery.state, artifactCount: record.delivery.artifacts.length,
268
- artifacts: record.delivery.artifacts.filter(a => a.format === 'xlsx' && (a.kind !== 'review' || a.rowCount > 0)).map(a => ({ fileName: a.fileName, url: `/data-cleaning/api/workflow/tasks/${encodeURIComponent(record.taskId)}/artifacts/${encodeURIComponent(a.id)}` })) } : {}),
269
+ artifacts: record.delivery.artifacts.filter(a => a.format === 'xlsx' && (a.kind !== 'review' || a.rowCount > 0)).map(a => ({ fileName: a.fileName, url: `${record.artifactOrigin || ''}/data-cleaning/api/workflow/tasks/${encodeURIComponent(record.taskId)}/artifacts/${encodeURIComponent(a.id)}` })) } : {}),
269
270
  });
270
271
  }
271
272
  }
package/lib/qcc.js CHANGED
@@ -1070,7 +1070,8 @@ export class QccHostBridge {
1070
1070
  qcc_match_status: 'enriched',
1071
1071
  qcc_source: 'qcc-mcp',
1072
1072
  ...(result.snapshotNote ? { qcc_snapshot_note: result.snapshotNote } : {}),
1073
- ...(row.qcc_field_issues || result.fieldIssues ? { qcc_field_issues: Object.entries(result.fieldIssues || {}).map(([field, issue]) => field + ': ' + issue.message).join(';') } : {}),
1073
+ ...(row.qcc_field_issues || result.fieldIssues ? { qcc_field_issues: Object.entries(result.fieldIssues || {}).filter(([, issue]) => issue.reviewRequired !== false).map(([field, issue]) => field + ': ' + issue.message).join(';') } : {}),
1074
+ ...(result.fieldIssues ? { qcc_field_notes: Object.entries(result.fieldIssues).filter(([, issue]) => issue.reviewRequired === false).map(([field, issue]) => field + ': ' + issue.message).join(';') } : {}),
1074
1075
  };
1075
1076
  }
1076
1077
  return { ...row, qcc_match_status: result?.status ?? 'failed' };
package/lib/web.js CHANGED
@@ -659,11 +659,20 @@ export function mountWebRoutes(wctx, { logger, report, TOOL_NAME, SKILL_NAME })
659
659
  }
660
660
  const payload = JSON.parse((await readBody(req)).toString('utf8'));
661
661
  if (payload.kind && payload.kind !== 'enrich') requirePaidConfirmation(payload);
662
+ // Resolve from the actual browser request, never from model/task input.
663
+ let artifactOrigin = '';
664
+ const requestHost = req.headers?.host;
665
+ if (typeof requestHost === 'string' && /^[a-zA-Z0-9.:[\]-]+$/.test(requestHost)) {
666
+ try {
667
+ const origin = new URL(req.headers.origin || `${req.socket?.encrypted ? 'https' : 'http'}://${requestHost}`);
668
+ if (['http:', 'https:'].includes(origin.protocol) && origin.host === requestHost) artifactOrigin = origin.origin;
669
+ } catch { /* Relative links remain available for hosts without an origin. */ }
670
+ }
662
671
  return writeJson(res, 201, {
663
672
  ok: true,
664
673
  marker: 'g5-agent-command',
665
674
  paidCalls: false,
666
- command: qccCommands.prepare(payload.workflowOwned ? await execution.prepare(payload) : payload),
675
+ command: qccCommands.prepare(payload.workflowOwned ? await execution.prepare(payload) : payload, { artifactOrigin }),
667
676
  });
668
677
  }
669
678
  if (rest.length === 1 && req.method === 'GET') {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-data-cleaning-agent",
3
- "version": "0.8.19",
3
+ "version": "0.8.21",
4
4
  "description": "Data cleaning and data enrichment for CSV/XLSX/JSON enterprise lists in DeepSeek Harness, including spreadsheet cleaning, deduplication, profiling, optional Qichacha MCP and exports.",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
@@ -40,7 +40,7 @@
40
40
  "docs/RELEASE-0.8.1.md",
41
41
  "docs/RELEASE-0.8.16.md",
42
42
  "docs/RELEASE-0.8.17.md",
43
- "docs/RELEASE-0.8.19.md",
43
+ "docs/RELEASE-0.8.21.md",
44
44
  "docs/TEMPLATE-MAPPING-ACCEPTANCE.md",
45
45
  "docs/UI-WORKFLOW-V2.md",
46
46
  "docs/UI-WORKFLOW-V2-MIGRATION.md",
@@ -1,13 +0,0 @@
1
- # 0.8.19 发布说明
2
-
3
- 本版修复字段候选、搜索展示和结果交付体验。
4
-
5
- - “受益人”推荐首条受益所有人;“主营业务收入”推荐营业总收入并提示口径不同,均需人工确认。
6
- - 映射选择器按当前映射、搜索、大类、字段清单展示;筛选不修改已选映射。
7
- - 已处理主体数包含未匹配或失败,不代表已补全;同时展示按输入行统计的补全与待核验数。
8
- - QCC 工具结果卡片与工作台增加原生文件链接,不只依赖模型生成 Markdown。
9
- - 空异常清单不展示;实际存在异常时保留。
10
-
11
- 验证:245 项自动测试、10 组明暗色与响应式浏览器回归通过。未发起付费 QCC 查询,未修改上传源文件。实际 DSH 宿主中的文件点击仍需升级后验收。
12
-
13
- 升级至 0.8.19 后完整重启 DSH。历史对话文本不会自动重写;历史制品可从工作台获取。