dsh-data-cleaning-agent 0.8.20 → 0.8.22

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.22] - 2026-09-09
8
+
9
+ - 表格预览保留可读列宽、横向滚动和固定表头,避免多列挤压成竖排。
10
+ - 结果文件操作分行并留白,长文件名换行;失败计数单独展示,避免零补全被标绿。
11
+ - 失败行导出保留规范化错误码和工具名,不输出原始上游响应或凭据。
12
+ - 修复自动展开后关闭/切换会话时,排队中的尺寸回调重新写入布局留白的竞态。
13
+ - 延续 0.8.21 的实际 Host 文件地址和执行自动展开功能。
14
+
15
+ ## [0.8.21] - 2026-09-09
16
+
17
+ - 从实际 Host 请求获取文件地址的协议和主机;修复本地 HTTP 链接被补成 HTTPS 的问题。
18
+ - 工具卡片支持同源完整文件地址,继续拒绝非同源链接。
19
+ - 已确认任务开始执行时自动展开工作台一次并跟随处理步骤;生成说明不触发,关闭后不反复弹出。
20
+ - 246 项测试及 10 组浏览器回归通过,无付费查询。
21
+
7
22
  ## [0.8.20] - 2026-09-09
8
23
 
9
24
  - 普通无记录/未披露字段保留为无数据说明,尊重共享契约 reviewRequired=false,不再误计为待核验。
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.20** (stable release)
11
+ > Current source version / 当前源码版本: **0.8.22** (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.20.md)。
3
+ 本版统一向导与工作台的 Host 执行闭环:发送后自动生成新 Excel,原文件不修改,工作台入口可恢复;发布记录见 [发布说明](docs/RELEASE-0.8.22.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.20
10
+ dsh plugin --profile web add dsh-data-cleaning-agent@0.8.22
11
11
  ```
12
12
 
13
13
  请先满足下文的 DSH、连接器及侧边栏依赖要求;安装后完整停止并重启对应 Profile。
@@ -25,7 +25,7 @@ dsh plugin --profile web add dsh-data-cleaning-agent@0.8.20
25
25
 
26
26
  > 在 DeepSeek Harness 中清洗、补全、画像企业名单数据的智能体插件:本地 CSV/XLSX/JSON 引擎 + 可选企查查(Qichacha/QCC)MCP 企业数据补全,由企查查(Qichacha/QCC)团队发起并维护。
27
27
  >
28
- > 当前源码版本 / Current source version: **0.8.20**(正式版本)
28
+ > 当前源码版本 / Current source version: **0.8.22**(正式版本)
29
29
 
30
30
  本版上传表格后展示完整清单及空白数量,用绿色、琥珀色、红色及文字区分已通过、待确认和未匹配映射。已映射字段自动纳入补全范围,额外字段默认折叠;多个原列可经确认使用同一非主体字段,分别补空并保留非空原值。修复 Host 草稿重置为默认五字段的问题,增加前后端版本错配提示。生成说明不查询,发送才执行;升级后需完整重启 DSH,历史下载文件不自动改写。
31
31
 
@@ -0,0 +1,11 @@
1
+ # 0.8.22 发布说明
2
+
3
+ 包含 0.8.21 的实际 Host 协议文件链接与执行自动展开功能;额外修复尺寸回调在工作台关闭后写回留白的竞态。
4
+
5
+ - 表格不压缩列宽,支持左右滚动;纵向滚动固定表头。
6
+ - 预览和下载操作分行留白,长文件名不再挤压按钮。
7
+ - 单独显示失败数量;失败行在异常清单保留规范化错误码和工具名。
8
+
9
+ 本次不改变企查查查询范围,不自动重跑历史任务;历史异常文件不会补写已遗漏的错误信息。
10
+
11
+ 升级后完整重启 DSH。历史错误链接文本不会重写,可从工作台访问原有文件,不需要重新查询。
package/lib/client.js CHANGED
@@ -89,19 +89,27 @@ window.__ModuleLoader__.load({
89
89
  const handle = drawer.querySelector('.dcAgentResizeHandle');
90
90
  if (!handle || !view) return () => {};
91
91
  const reservations = new Map();
92
+ const registryKey = Symbol.for('dsh.data-cleaning.sizing-reservations');
93
+ const registry = doc[registryKey] || (doc[registryKey] = new WeakMap());
94
+ let disposed = false;
92
95
  let requested = expanded ? 980 : preferredWidth;
93
96
  let current = 0;
94
97
  let drag = null;
95
98
  const boundsForView = () => workbenchWidthBounds(view.innerWidth,
96
99
  doc.querySelector('[data-conversation-scroll]')?.getBoundingClientRect().width || view.innerWidth);
97
100
  const reserve = () => {
101
+ if (disposed) return;
98
102
  const width = boundsForView().mobile ? 0 : drawer.getBoundingClientRect().width;
99
103
  for (const el of doc.querySelectorAll('[data-conversation-scroll]')) {
100
104
  if (!reservations.has(el)) {
101
- reservations.set(el, {
105
+ const previous = registry.get(el) || {
102
106
  value: el.style.getPropertyValue('--dc-agent-workbench-reserve'),
103
107
  priority: el.style.getPropertyPriority('--dc-agent-workbench-reserve'),
104
- });
108
+ owners: 0,
109
+ };
110
+ previous.owners += 1;
111
+ registry.set(el, previous);
112
+ reservations.set(el, previous);
105
113
  sizeObserver?.observe(el, { box: 'border-box' });
106
114
  }
107
115
  if (el.style.getPropertyValue('--dc-agent-workbench-reserve') !== `${width}px`) {
@@ -110,6 +118,7 @@ window.__ModuleLoader__.load({
110
118
  }
111
119
  };
112
120
  const apply = (value) => {
121
+ if (disposed) return;
113
122
  const bounds = boundsForView();
114
123
  const fallback = view.innerWidth <= 1040 ? Math.min(420, view.innerWidth * .48) : Math.min(460, view.innerWidth * .42);
115
124
  current = Math.round(Math.min(bounds.max, Math.max(bounds.min, Number.isFinite(value) ? value : fallback)));
@@ -173,6 +182,7 @@ window.__ModuleLoader__.load({
173
182
  observer?.observe(doc.body, { childList: true, subtree: true });
174
183
  apply(requested);
175
184
  return () => {
185
+ disposed = true;
176
186
  cancel();
177
187
  observer?.disconnect();
178
188
  sizeObserver?.disconnect();
@@ -188,6 +198,9 @@ window.__ModuleLoader__.load({
188
198
  drawer.style.removeProperty('--dc-agent-panel-width');
189
199
  delete drawer.dataset.narrow;
190
200
  for (const [el, previous] of reservations) {
201
+ previous.owners -= 1;
202
+ if (previous.owners > 0) continue;
203
+ registry.delete(el);
191
204
  if (previous.value) el.style.setProperty('--dc-agent-workbench-reserve', previous.value, previous.priority);
192
205
  else el.style.removeProperty('--dc-agent-workbench-reserve');
193
206
  }
@@ -970,6 +983,25 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
970
983
  }
971
984
  .dcAgentFieldAction:hover, .dcAgentFieldAction:focus-visible { background: var(--dc-selected); }
972
985
  .dcAgentPreviewTable { margin-top: 12px; overflow: auto; border: 1px solid var(--dc-border); border-radius: 8px; }
986
+ .dcAgentArtifactList { display: flex; flex-wrap: wrap; gap: 10px; margin-top: 16px; min-width: 0; }
987
+ .dcAgentArtifactList > .dcAgentButton { display: inline-flex; align-items: center; box-sizing: border-box; max-width: 100%; min-width: 0; height: auto; white-space: normal; overflow-wrap: anywhere; line-height: 1.5; text-decoration: none; }
988
+ .dcAgentArtifactList > a.dcAgentButton { flex-basis: 100%; }
989
+ .dcAgentPreviewTable { min-width: 0; max-width: 100%; scrollbar-gutter: stable; }
990
+ .dcAgentPreviewTable > .dcAgentTable {
991
+ width: max-content; min-width: 100%; max-width: none;
992
+ table-layout: auto; border-collapse: separate; border-spacing: 0;
993
+ }
994
+ .dcAgentPreviewTable > .dcAgentTable th,
995
+ .dcAgentPreviewTable > .dcAgentTable td {
996
+ min-width: 140px; max-width: none; white-space: nowrap;
997
+ word-break: normal; overflow-wrap: normal;
998
+ border-right: 1px solid var(--dc-border);
999
+ }
1000
+ .dcAgentPreviewTable > .dcAgentTable th:first-child,
1001
+ .dcAgentPreviewTable > .dcAgentTable td:first-child { min-width: 64px; }
1002
+ .dcAgentPreviewTable > .dcAgentTable th {
1003
+ position: sticky; top: 0; z-index: 2; background: var(--dc-table-head);
1004
+ }
973
1005
  .dcAgentHistoryTask { display: block; width: 100%; padding: 10px; text-align: left; }
974
1006
  .dcAgentHistoryTask h3 { margin: 0; font-size: 12px; }
975
1007
  /*
@@ -1862,9 +1894,15 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
1862
1894
  return response.task;
1863
1895
  }
1864
1896
  function watchHostedCommand(actions, sessionId, command) {
1897
+ let openedForExecution = false;
1898
+ const ownerVisible = () => typeof document === 'undefined' || [...document.querySelectorAll('.dcAgentExperience')].some(node => node.dataset.sessionId === sessionId);
1865
1899
  void waitForQccCommand(command.commandId, 1800, async (status) => {
1866
1900
  if (!status || status.state === 'prepared') return;
1867
1901
  await refreshHostedTask(actions, sessionId, command.taskId, null, true).catch(() => {});
1902
+ if (!openedForExecution && ownerVisible() && isCleaningSession(sessionId) && workflowTaskBySession.get(String(sessionId))?.id === command.taskId) {
1903
+ openedForExecution = true;
1904
+ openWorkbench(actions, workflowTaskBySession.get(String(sessionId))?.stage || 'match', sessionId);
1905
+ }
1868
1906
  if (isCleaningSession(sessionId) && workflowTaskBySession.get(String(sessionId))?.id === command.taskId) actions.setCommandProgress?.({ taskId: command.taskId, ...status.progress });
1869
1907
  }).then(async (completed) => {
1870
1908
  await refreshHostedTask(actions, sessionId, command.taskId, completed.run);
@@ -2180,6 +2218,7 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
2180
2218
  const WORKBENCH_DATASET_EVENT = 'dsh:data-cleaning-workbench-dataset';
2181
2219
  const WORKBENCH_DRAFT_EVENT = 'dsh:data-cleaning-workbench-draft';
2182
2220
  const WORKBENCH_PREPARE_EVENT = 'dsh:data-cleaning-workbench-prepare';
2221
+ const WORKBENCH_OWNER_LEFT_EVENT = 'dsh:data-cleaning-workbench-owner-left';
2183
2222
 
2184
2223
  // session scope 不能复用 root scope 的 store handle。DSH 当前会分别物化 root/session
2185
2224
  // 插槽组件,因此以 document 事件跨 scope 通知;闭包引用仅作为无 DOM 测试降级。
@@ -2542,6 +2581,10 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
2542
2581
  function DataCleaningExperience(props) {
2543
2582
  const { sessionId, session: ownerSession } = props;
2544
2583
  const enabled = useCleaningSession(sessionId);
2584
+ react.useEffect(() => {
2585
+ if (!enabled) return undefined;
2586
+ return () => document.dispatchEvent(new CustomEvent(WORKBENCH_OWNER_LEFT_EVENT, { detail: { sessionId } }));
2587
+ }, [enabled, sessionId]);
2545
2588
  const hero = ownerSession?.composerPhase === 'blank';
2546
2589
  const [menuMount, setMenuMount] = react.useState(null);
2547
2590
  react.useEffect(() => {
@@ -3228,7 +3271,7 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
3228
3271
  meta.hint ? h('div', { className: 'dcAgentToolCardHint' }, meta.hint) : null,
3229
3272
  summary ? h('div', { className: 'dcAgentToolCardBody' }, summary) : null,
3230
3273
  toolName === 'data_cleaning_qcc_run' ? h('div', { className: 'dcAgentArtifactList' },
3231
- [...summary.matchAll(/\[([^\]]+)\]\((\/data-cleaning\/api\/workflow\/tasks\/[a-zA-Z0-9%_-]+\/artifacts\/[a-zA-Z0-9%_-]+(?:\?preview=1)?)\)/g)].map((match, index) =>
3274
+ [...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) =>
3232
3275
  h('a', { key: index, className: 'dcAgentButton', href: match[2], target: '_blank', rel: 'noopener noreferrer' }, match[1]))) : null,
3233
3276
  );
3234
3277
  }
@@ -3423,7 +3466,7 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
3423
3466
  h('p', { className: 'dcAgentHint', role: 'status' }, complete
3424
3467
  ? `共 ${total} 条,当前显示 ${total ? start + 1 : 0}–${Math.min(start + pageSize, total)} 条;共 ${columns.length} 列,可横向滚动。`
3425
3468
  : `完整清单尚未就绪:预期 ${expectedCount} 条,当前载入 ${total} 条。请等待同步;刷新后若仍缺失,请重新上传源文件。`),
3426
- h('div', { className: 'dcAgentPreviewTable', style: { overflow: 'auto', maxHeight: '420px' } },
3469
+ h('div', { className: 'dcAgentPreviewTable', tabIndex: 0, role: 'region', 'aria-label': '表格预览,可横向和纵向滚动,表头固定', style: { overflow: 'auto', maxHeight: '420px' } },
3427
3470
  h('table', { className: 'dcAgentTable' },
3428
3471
  h('thead', null, h('tr', null, h('th', null, '序号'), columns.map((key) => h('th', { key }, resultFieldLabel(key))))),
3429
3472
  h('tbody', null, rows.slice(start, start + pageSize).map((row, index) => h('tr', { key: start + index },
@@ -3488,6 +3531,7 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
3488
3531
 
3489
3532
  react.useEffect(() => {
3490
3533
  rootWorkbenchActions = actions;
3534
+ const handleOwnerLeft = () => actions.close();
3491
3535
  const handleOpen = (event) => {
3492
3536
  event?.preventDefault?.();
3493
3537
  const detail = event?.detail ?? {};
@@ -3540,6 +3584,7 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
3540
3584
  };
3541
3585
  if (typeof document !== 'undefined' && typeof document.addEventListener === 'function') {
3542
3586
  document.addEventListener(WORKBENCH_OPEN_EVENT, handleOpen);
3587
+ document.addEventListener(WORKBENCH_OWNER_LEFT_EVENT, handleOwnerLeft);
3543
3588
  document.addEventListener(WORKBENCH_DATASET_EVENT, handleDataset);
3544
3589
  document.addEventListener(WORKBENCH_DRAFT_EVENT, handleDraft);
3545
3590
  document.addEventListener(WORKBENCH_PREPARE_EVENT, handlePrepare);
@@ -3547,6 +3592,7 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
3547
3592
  return () => {
3548
3593
  if (typeof document !== 'undefined' && typeof document.removeEventListener === 'function') {
3549
3594
  document.removeEventListener(WORKBENCH_OPEN_EVENT, handleOpen);
3595
+ document.removeEventListener(WORKBENCH_OWNER_LEFT_EVENT, handleOwnerLeft);
3550
3596
  document.removeEventListener(WORKBENCH_DATASET_EVENT, handleDataset);
3551
3597
  document.removeEventListener(WORKBENCH_DRAFT_EVENT, handleDraft);
3552
3598
  document.removeEventListener(WORKBENCH_PREPARE_EVENT, handlePrepare);
@@ -4138,6 +4184,7 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
4138
4184
  ? (qccRun.summary?.ambiguous ?? 0) + (qccRun.summary?.fieldReview ?? 0)
4139
4185
  : cachedTask?.enrichmentSummary?.reviewRequired
4140
4186
  ?? cachedTask?.matchSummary?.reviewRequired;
4187
+ const qccFailedCount = qccRun?.summary?.failed ?? cachedTask?.enrichmentSummary?.failed ?? cachedTask?.matchSummary?.failed ?? 0;
4141
4188
  const stat = (label, value, tone) => h('div', { className: 'dcAgentCard' },
4142
4189
  h('span', null, label),
4143
4190
  h('b', { className: tone ? `is-${tone}` : null }, displayStatValue(value))
@@ -4373,12 +4420,13 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
4373
4420
  const availableArtifacts = (cachedTask?.artifacts || []).filter(artifact => artifact.kind !== 'review' || artifact.rowCount > 0);
4374
4421
  const canCreateArtifacts = Boolean(exportRows()?.length) && ['rules_confirmed', 'diagnosed', 'export_ready', 'partial'].includes(cachedTask?.state);
4375
4422
  pane = h('div', { className: 'dcAgentPane' },
4376
- h('p', { className: 'dcAgentHint' }, '结果文件由 Host 保存;仅在有待核验记录时展示异常清单。任务或插件重启后,可从任务历史继续下载;无需再次调用企查查。'),
4423
+ h('p', { className: 'dcAgentHint' }, '结果文件由 Host 保存;有失败、未匹配或待核验记录时展示异常清单。任务或插件重启后,可从任务历史继续下载;无需再次调用企查查。'),
4377
4424
  h('div', { className: 'dcAgentGrid' },
4378
4425
  stat('输入行数', dataset ? dataset.rowCount : workflowTask?.source?.rowCount ?? '—'),
4379
4426
  stat('匹配补全', qccEnrichedCount, qccEnrichedCount > 0 ? 'good' : null),
4380
4427
  stat('待核验', qccReviewCount, qccReviewCount > 0 ? 'warn' : null),
4381
4428
  stat('任务状态', WORKFLOW_STATE_LABELS[cachedTask?.state] || cachedTask?.state || '进行中'),
4429
+ stat('失败', qccFailedCount, qccFailedCount > 0 ? 'bad' : null),
4382
4430
  ),
4383
4431
  availableArtifacts.length ? h('div', { className: 'dcAgentArtifactList' },
4384
4432
  availableArtifacts.filter(artifact => artifact.format === 'xlsx').map(artifact => h('a', {
@@ -4555,8 +4603,8 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
4555
4603
  max: commandProgress.totalUnique, value: Math.min(commandProgress.totalUnique, Math.max(0, commandProgress.completedUnique || 0)),
4556
4604
  'aria-label': '主体处理进度', style: { width: '100%', accentColor: cachedTask.state === 'completed' ? '#16845b' : cachedTask.state === 'failed' ? '#c62828' : cachedTask.state === 'partial' ? '#a66a00' : '#087dcc' },
4557
4605
  }) : null,
4558
- h('p', { style: { color: Number(qccReviewCount) > 0 ? '#a66a00' : Number.isFinite(qccEnrichedCount) ? '#16845b' : 'inherit' } },
4559
- Number.isFinite(qccEnrichedCount) ? `已处理并补全 ${qccEnrichedCount} 条 · 待核验 ${Number.isFinite(qccReviewCount) ? qccReviewCount : '统计中'} 条(按输入行统计;未披露字段保留空白)` : '结果统计中,处理结束后展示补全与待核验数量'),
4606
+ h('p', { style: { color: qccFailedCount > 0 ? '#c62828' : Number(qccReviewCount) > 0 ? '#a66a00' : qccEnrichedCount > 0 ? '#16845b' : 'inherit' } },
4607
+ Number.isFinite(qccEnrichedCount) ? `已处理并补全 ${qccEnrichedCount} 条 · 待核验 ${Number.isFinite(qccReviewCount) ? qccReviewCount : '统计中'} 条 · 失败 ${qccFailedCount} 条(按输入行统计;未披露字段保留空白)` : '结果统计中,处理结束后展示补全与待核验数量'),
4560
4608
  ) : null,
4561
4609
  error ? h('div', { className: 'dcAgentError', role: 'alert' }, error) : null,
4562
4610
  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
@@ -1074,7 +1074,11 @@ export class QccHostBridge {
1074
1074
  ...(result.fieldIssues ? { qcc_field_notes: Object.entries(result.fieldIssues).filter(([, issue]) => issue.reviewRequired === false).map(([field, issue]) => field + ': ' + issue.message).join(';') } : {}),
1075
1075
  };
1076
1076
  }
1077
- return { ...row, qcc_match_status: result?.status ?? 'failed' };
1077
+ return {
1078
+ ...row, qcc_match_status: result?.status ?? 'failed',
1079
+ // Only export normalized diagnostic identifiers, never raw upstream messages or credentials.
1080
+ ...(result?.error ? { qcc_error: [result.error.code, result.error.toolName, result.error.upstreamCode].filter(Boolean).join(' · ') } : {}),
1081
+ };
1078
1082
  });
1079
1083
 
1080
1084
  const indexesFor = (name) => normalized.filter((item) => item.companyName === name).map((item) => item.index);
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.20",
3
+ "version": "0.8.22",
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.20.md",
43
+ "docs/RELEASE-0.8.22.md",
44
44
  "docs/TEMPLATE-MAPPING-ACCEPTANCE.md",
45
45
  "docs/UI-WORKFLOW-V2.md",
46
46
  "docs/UI-WORKFLOW-V2-MIGRATION.md",
@@ -1,11 +0,0 @@
1
- # 0.8.20 发布说明
2
-
3
- 修复未披露数据被误判为人工核验;保留未披露说明和空白值,不伪造数据。真正的多候选或无法判定的问题继续进入待核验。
4
-
5
- 当前页和历史列表同步最新任务状态。处理进度条区分处理中、完成、需核验和失败;100% 仅表示主体处理结束。摘要尚未生成时显示“结果统计中”。
6
-
7
- 任务展示名优先尊重自定义名称,否则由源文件名、行数和创建时间生成;当前页与历史一致。
8
-
9
- 验证:246 项自动测试、10 组浏览器回归。无付费查询,源文件未修改。
10
-
11
- 升级并完整重启 DSH 后生效;历史任务的旧异常文件和统计不会自动重算,不应仅为更新展示而重复查询。