dsh-data-cleaning-agent 0.9.12 → 0.9.15

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/lib/client.js CHANGED
@@ -473,6 +473,15 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
473
473
  }
474
474
  .dcAgentStep:last-child { border-right: 0; }
475
475
  .dcAgentStep:hover { background: var(--dc-page); }
476
+ .dcAgentStep[data-access="locked"] {
477
+ color: light-dark(#8491a1, #8291a0);
478
+ cursor: not-allowed;
479
+ }
480
+ .dcAgentStep[data-access="locked"]:hover,
481
+ .dcAgentStep[data-access="locked"]:focus-visible {
482
+ background: var(--dc-page);
483
+ color: var(--dc-muted);
484
+ }
476
485
  .dcAgentStep.is-active {
477
486
  color: var(--dc-action);
478
487
  background: var(--dc-selected);
@@ -488,6 +497,7 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
488
497
  background: var(--dc-action);
489
498
  }
490
499
  .dcAgentStepIcon {
500
+ position: relative;
491
501
  display: grid;
492
502
  place-items: center;
493
503
  width: 30px;
@@ -497,6 +507,30 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
497
507
  border-radius: 8px;
498
508
  }
499
509
  .dcAgentStepIcon svg { display: block; }
510
+ .dcAgentStep[data-access="locked"] .dcAgentStepIcon::before {
511
+ content: '';
512
+ position: absolute;
513
+ right: -4px;
514
+ bottom: -3px;
515
+ box-sizing: border-box;
516
+ width: 11px;
517
+ height: 9px;
518
+ border: 1px solid var(--dc-border);
519
+ border-radius: 2px;
520
+ background: var(--dc-surface);
521
+ }
522
+ .dcAgentStep[data-access="locked"] .dcAgentStepIcon::after {
523
+ content: '';
524
+ position: absolute;
525
+ right: -2px;
526
+ bottom: 5px;
527
+ box-sizing: border-box;
528
+ width: 7px;
529
+ height: 6px;
530
+ border: 1px solid var(--dc-muted);
531
+ border-bottom: 0;
532
+ border-radius: 5px 5px 0 0;
533
+ }
500
534
  .dcAgentStepLabel {
501
535
  display: block;
502
536
  width: 100%;
@@ -611,6 +645,34 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
611
645
  color: light-dark(#d2454f, #ff838b);
612
646
  font-size: 12px;
613
647
  }
648
+ .dcAgentNavigationNotice {
649
+ position: relative;
650
+ flex: none;
651
+ padding: 9px 12px 9px 34px;
652
+ border: 1px solid light-dark(#b8dbef, #315a72);
653
+ border-radius: 6px;
654
+ background: var(--dc-table-head);
655
+ color: var(--dc-muted);
656
+ font-size: 12px;
657
+ line-height: 18px;
658
+ }
659
+ .dcAgentNavigationNotice::before {
660
+ content: 'i';
661
+ position: absolute;
662
+ top: 10px;
663
+ left: 12px;
664
+ box-sizing: border-box;
665
+ width: 14px;
666
+ height: 14px;
667
+ display: grid;
668
+ place-items: center;
669
+ border: 1px solid light-dark(#8fc4e4, #4f829f);
670
+ border-radius: 50%;
671
+ color: var(--dc-action);
672
+ font-size: 9px;
673
+ font-weight: 700;
674
+ line-height: 1;
675
+ }
614
676
 
615
677
  .dcAgentChips { display: flex; flex-wrap: wrap; gap: 6px; }
616
678
  .dcAgentChip {
@@ -1133,41 +1195,288 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
1133
1195
  .dcAgentTable th, .dcAgentTable td { height: 44px; box-sizing: border-box; font-size: 13px; }
1134
1196
  .dcAgentTable tbody tr:hover { background: var(--dc-selected); }
1135
1197
  .dcAgentSection, .dcAgentCard { background: var(--dc-surface); }
1136
- .dcAgentWbBody > .dcAgentSection[aria-label="当前任务进展"] {
1198
+ .dcAgentTaskContext {
1199
+ flex: none;
1200
+ margin: -18px -18px 0;
1201
+ border-bottom: 1px solid var(--dc-border);
1202
+ background: var(--dc-surface);
1203
+ }
1204
+ .dcAgentTaskStatus {
1205
+ position: relative;
1206
+ padding: 11px 18px 10px;
1207
+ border-bottom: 1px solid var(--dc-border);
1208
+ background: var(--dc-table-head);
1209
+ }
1210
+ .dcAgentTaskStatusRow {
1211
+ min-height: 46px;
1137
1212
  display: grid;
1138
- grid-template-columns: minmax(0, 1fr) auto auto;
1213
+ grid-template-columns: minmax(0, 1fr) auto auto auto;
1139
1214
  align-items: center;
1140
- gap: 6px 10px;
1141
- padding: 14px 16px;
1215
+ gap: 10px;
1216
+ color: var(--dc-muted);
1217
+ font-size: 12px;
1142
1218
  }
1143
- .dcAgentWbBody > .dcAgentSection[aria-label="当前任务进展"] > h3 {
1144
- grid-column: 1;
1145
- grid-row: 1;
1219
+ .dcAgentTaskStage,
1220
+ .dcAgentTaskCurrent {
1146
1221
  min-width: 0;
1147
- margin: 0;
1148
- overflow-wrap: anywhere;
1149
- font-size: 14px;
1222
+ display: flex;
1223
+ align-items: center;
1224
+ gap: 7px;
1150
1225
  }
1151
- .dcAgentWbBody > .dcAgentSection[aria-label="当前任务进展"] > b {
1152
- grid-column: 2;
1153
- grid-row: 1;
1154
- padding: 2px 8px;
1155
- border: 1px solid var(--dc-border);
1226
+ .dcAgentTaskDoneMark {
1227
+ box-sizing: border-box;
1228
+ width: 17px;
1229
+ height: 17px;
1230
+ display: grid;
1231
+ place-items: center;
1232
+ flex: none;
1233
+ border: 1px solid light-dark(#91cdb2, #3f7861);
1234
+ border-radius: 50%;
1235
+ color: light-dark(#16845b, #5ed29f);
1236
+ font-size: 11px;
1237
+ line-height: 1;
1238
+ }
1239
+ .dcAgentTaskDoneMark[data-complete="false"] {
1240
+ border-color: light-dark(#9bcceb, #477894);
1241
+ background: var(--dc-selected);
1242
+ color: var(--dc-action);
1243
+ }
1244
+ .dcAgentTaskStageCopy { min-width: 0; }
1245
+ .dcAgentTaskStageTitle {
1246
+ display: flex;
1247
+ align-items: center;
1248
+ gap: 7px;
1249
+ min-width: 0;
1250
+ }
1251
+ .dcAgentTaskStageTitle strong {
1252
+ overflow: hidden;
1253
+ color: var(--dc-text);
1254
+ font-size: 13px;
1255
+ font-weight: 650;
1256
+ text-overflow: ellipsis;
1257
+ white-space: nowrap;
1258
+ }
1259
+ .dcAgentTaskState {
1260
+ flex: none;
1261
+ padding: 1px 6px;
1262
+ border: 1px solid light-dark(#b9ddcc, #37634f);
1156
1263
  border-radius: 4px;
1157
- background: var(--dc-page);
1264
+ background: light-dark(#eaf7f1, #173529);
1265
+ color: light-dark(#16845b, #5ed29f);
1266
+ font-size: 10px;
1267
+ font-weight: 650;
1268
+ white-space: nowrap;
1269
+ }
1270
+ .dcAgentTaskState[data-complete="false"] {
1271
+ border-color: light-dark(#b5d8ef, #426c83);
1272
+ background: var(--dc-selected);
1273
+ color: var(--dc-action);
1274
+ }
1275
+ .dcAgentTaskMeta {
1276
+ display: flex;
1277
+ min-width: 0;
1278
+ margin-top: 2px;
1279
+ overflow: hidden;
1158
1280
  color: var(--dc-muted);
1159
- font-size: 12px;
1160
- font-weight: 600;
1281
+ font-size: 11px;
1282
+ line-height: 16px;
1161
1283
  white-space: nowrap;
1162
1284
  }
1163
- .dcAgentWbBody > .dcAgentSection[aria-label="当前任务进展"] > details {
1164
- grid-column: 3;
1165
- grid-row: 1;
1285
+ .dcAgentTaskMeta > span {
1286
+ overflow: hidden;
1287
+ text-overflow: ellipsis;
1288
+ }
1289
+ .dcAgentTaskMeta > span + span::before {
1290
+ content: "·";
1291
+ margin: 0 6px;
1292
+ color: var(--dc-border);
1293
+ }
1294
+ .dcAgentTaskCurrent {
1295
+ padding-left: 10px;
1296
+ border-left: 1px solid var(--dc-border);
1297
+ white-space: nowrap;
1298
+ }
1299
+ .dcAgentTaskCurrent span { color: var(--dc-muted); font-size: 11px; }
1300
+ .dcAgentTaskCurrent strong { color: var(--dc-action); font-size: 12px; }
1301
+ .dcAgentTaskReturn {
1302
+ min-height: 28px;
1303
+ padding: 3px 7px;
1304
+ border: 0;
1305
+ border-radius: 5px;
1306
+ background: transparent;
1307
+ color: var(--dc-action);
1308
+ cursor: pointer;
1309
+ white-space: nowrap;
1310
+ }
1311
+ .dcAgentTaskReturn:hover { background: var(--dc-selected); }
1312
+ .dcAgentTaskDetails { position: relative; }
1313
+ .dcAgentTaskDetails > summary {
1314
+ box-sizing: border-box;
1315
+ width: 28px;
1316
+ height: 28px;
1317
+ display: grid;
1318
+ place-items: center;
1319
+ border: 1px solid transparent;
1320
+ border-radius: 6px;
1321
+ color: var(--dc-muted);
1322
+ cursor: pointer;
1323
+ font-size: 12px;
1324
+ font-weight: 700;
1325
+ list-style: none;
1326
+ }
1327
+ .dcAgentTaskDetails > summary::-webkit-details-marker { display: none; }
1328
+ .dcAgentTaskDetails > summary:hover {
1329
+ border-color: var(--dc-border);
1330
+ background: var(--dc-surface);
1331
+ color: var(--dc-action);
1332
+ }
1333
+ .dcAgentTaskDetailsBody {
1334
+ position: absolute;
1335
+ z-index: 6;
1336
+ top: 32px;
1337
+ right: 0;
1338
+ width: min(310px, calc(100vw - 48px));
1339
+ padding: 12px;
1340
+ border: 1px solid var(--dc-border);
1341
+ border-radius: 8px;
1342
+ background: var(--dc-surface);
1343
+ box-shadow: 0 12px 30px light-dark(rgb(26 74 102 / 10%), rgb(0 0 0 / 34%));
1344
+ color: var(--dc-muted);
1345
+ font-size: 12px;
1346
+ }
1347
+ .dcAgentTaskDetailsBody p { margin: 4px 0; overflow-wrap: anywhere; }
1348
+ .dcAgentTaskSyncNote {
1349
+ margin: 6px 0 0 24px;
1350
+ color: var(--dc-muted);
1351
+ font-size: 11px;
1352
+ }
1353
+ .dcAgentTaskExecution {
1354
+ display: grid;
1355
+ grid-template-columns: auto minmax(100px, 1fr);
1356
+ align-items: center;
1357
+ gap: 8px;
1358
+ margin-top: 6px;
1359
+ color: var(--dc-muted);
1360
+ font-size: 11px;
1361
+ }
1362
+ .dcAgentTaskExecution progress { width: 100%; height: 6px; accent-color: var(--dc-action); }
1363
+ .dcAgentTaskMetrics {
1364
+ display: flex;
1365
+ align-items: center;
1366
+ gap: 16px;
1367
+ margin-top: 7px;
1368
+ padding-top: 8px;
1369
+ border-top: 1px solid var(--dc-border);
1370
+ color: var(--dc-muted);
1371
+ font-size: 11px;
1372
+ }
1373
+ .dcAgentTaskMetricGroup {
1374
+ min-width: 0;
1375
+ display: flex;
1376
+ align-items: center;
1377
+ gap: 10px;
1378
+ }
1379
+ .dcAgentTaskMetricGroup + .dcAgentTaskMetricGroup {
1380
+ flex: 1;
1381
+ padding-left: 16px;
1382
+ border-left: 1px solid var(--dc-border);
1383
+ }
1384
+ .dcAgentTaskMetricLabel { flex: none; color: var(--dc-muted); font-weight: 600; }
1385
+ .dcAgentTaskMetric { color: var(--dc-text); white-space: nowrap; }
1386
+ .dcAgentTaskMetric b { margin-left: 3px; font-size: 12px; font-weight: 650; }
1387
+ .dcAgentTaskMetric.is-good,
1388
+ .dcAgentTaskMetric.is-good b { color: light-dark(#16845b, #5ed29f); }
1389
+ .dcAgentWorkflowGuide { background: var(--dc-surface); }
1390
+ .dcAgentWorkflowGuide > summary {
1391
+ box-sizing: border-box;
1392
+ min-height: 40px;
1393
+ display: flex;
1394
+ align-items: center;
1395
+ gap: 7px;
1396
+ padding: 8px 18px;
1166
1397
  color: var(--dc-muted);
1167
1398
  font-size: 12px;
1399
+ cursor: pointer;
1400
+ list-style: none;
1401
+ }
1402
+ .dcAgentWorkflowGuide > summary::-webkit-details-marker { display: none; }
1403
+ .dcAgentWorkflowGuide > summary:hover {
1404
+ color: var(--dc-action);
1405
+ background: var(--dc-table-head);
1406
+ }
1407
+ .dcAgentWorkflowGuide > summary::before {
1408
+ content: "?";
1409
+ width: 16px;
1410
+ height: 16px;
1411
+ display: grid;
1412
+ place-items: center;
1413
+ border: 1px solid var(--dc-border);
1414
+ border-radius: 50%;
1415
+ font-size: 10px;
1416
+ }
1417
+ .dcAgentWorkflowGuide > summary::after {
1418
+ content: "›";
1419
+ margin-left: auto;
1420
+ font-size: 17px;
1421
+ transition: transform 120ms ease;
1422
+ }
1423
+ .dcAgentWorkflowGuide[open] > summary::after { transform: rotate(90deg); }
1424
+ .dcAgentWorkflowGuideContent { padding: 0 18px 16px; }
1425
+ .dcAgentWorkflowGuideIntro {
1426
+ margin: 0 0 5px;
1427
+ color: var(--dc-muted);
1428
+ font-size: 12px;
1429
+ line-height: 18px;
1430
+ }
1431
+ .dcAgentWorkflowGuideList {
1432
+ display: grid;
1433
+ grid-template-columns: repeat(2, minmax(0, 1fr));
1434
+ gap: 0 24px;
1435
+ margin-top: 11px;
1436
+ }
1437
+ .dcAgentWorkflowGuideStep {
1438
+ min-width: 0;
1439
+ display: grid;
1440
+ grid-template-columns: auto minmax(0, 1fr);
1441
+ gap: 9px;
1442
+ padding: 10px 0;
1443
+ border-top: 1px solid var(--dc-border);
1444
+ }
1445
+ .dcAgentWorkflowGuideStep:last-child:nth-child(odd) { grid-column: 1 / -1; }
1446
+ .dcAgentWorkflowGuideIndex {
1447
+ box-sizing: border-box;
1448
+ width: 22px;
1449
+ height: 22px;
1450
+ display: grid;
1451
+ place-items: center;
1452
+ border: 1px solid var(--dc-border);
1453
+ border-radius: 50%;
1454
+ background: var(--dc-page);
1455
+ color: var(--dc-action);
1456
+ font-size: 10px;
1457
+ font-weight: 700;
1458
+ }
1459
+ .dcAgentWorkflowGuideStep strong {
1460
+ display: block;
1461
+ margin-bottom: 2px;
1462
+ color: var(--dc-text);
1463
+ font-size: 12px;
1464
+ }
1465
+ .dcAgentWorkflowGuideStep p {
1466
+ margin: 0;
1467
+ color: var(--dc-muted);
1468
+ font-size: 11px;
1469
+ line-height: 17px;
1470
+ }
1471
+ .dcAgentWorkflowGuideBoundary {
1472
+ margin-top: 8px;
1473
+ padding: 9px 10px;
1474
+ border-left: 2px solid var(--dc-action);
1475
+ background: var(--dc-table-head);
1476
+ color: var(--dc-muted);
1477
+ font-size: 11px;
1478
+ line-height: 17px;
1168
1479
  }
1169
- .dcAgentWbBody > .dcAgentSection[aria-label="当前任务进展"] > :is(p, progress) { grid-column: 1 / -1; }
1170
- .dcAgentWbBody > .dcAgentSection[aria-label="当前任务进展"] > p { margin: 0; }
1171
1480
  .dcAgentWbBody > .dcAgentSection[role="status"]:not([aria-label]) {
1172
1481
  min-height: 156px;
1173
1482
  display: flex;
@@ -1222,10 +1531,32 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
1222
1531
  -webkit-box-orient: vertical;
1223
1532
  -webkit-line-clamp: 2;
1224
1533
  }
1225
- .dcAgentWbBody > .dcAgentSection[aria-label="当前任务进展"] { grid-template-columns: minmax(0, 1fr) auto; }
1226
- .dcAgentWbBody > .dcAgentSection[aria-label="当前任务进展"] > h3 { grid-column: 1 / -1; }
1227
- .dcAgentWbBody > .dcAgentSection[aria-label="当前任务进展"] > b { grid-column: 1; grid-row: 2; justify-self: start; }
1228
- .dcAgentWbBody > .dcAgentSection[aria-label="当前任务进展"] > details { grid-column: 2; grid-row: 2; }
1534
+ .dcAgentTaskStatus { padding-inline: 10px; }
1535
+ .dcAgentTaskStatusRow { grid-template-columns: minmax(0, 1fr) auto; gap: 4px 8px; }
1536
+ .dcAgentTaskStage { grid-column: 1; grid-row: 1; }
1537
+ .dcAgentTaskCurrent {
1538
+ grid-column: 1;
1539
+ grid-row: 2;
1540
+ justify-self: start;
1541
+ margin-left: 24px;
1542
+ padding-left: 0;
1543
+ border-left: 0;
1544
+ }
1545
+ .dcAgentTaskReturn { grid-column: 2; grid-row: 2; }
1546
+ .dcAgentTaskDetails { grid-column: 2; grid-row: 1; justify-self: end; }
1547
+ .dcAgentTaskMetrics { align-items: flex-start; flex-direction: column; gap: 5px; }
1548
+ .dcAgentTaskMetricGroup { width: 100%; gap: 8px; }
1549
+ .dcAgentTaskMetricGroup + .dcAgentTaskMetricGroup {
1550
+ padding-top: 5px;
1551
+ padding-left: 0;
1552
+ border-top: 1px solid var(--dc-border);
1553
+ border-left: 0;
1554
+ }
1555
+ .dcAgentTaskMetricLabel { min-width: 48px; }
1556
+ .dcAgentWorkflowGuide > summary { padding-inline: 10px; }
1557
+ .dcAgentWorkflowGuideContent { padding-inline: 10px; }
1558
+ .dcAgentWorkflowGuideList { grid-template-columns: 1fr; }
1559
+ .dcAgentWorkflowGuideStep:last-child:nth-child(odd) { grid-column: auto; }
1229
1560
  .dcAgentDatasetHeader { display: grid; }
1230
1561
  .dcAgentDatasetHeader .dcAgentHint { text-align: left; }
1231
1562
  .dcAgentDatasetFooter { grid-template-columns: 1fr; }
@@ -1316,6 +1647,14 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
1316
1647
  { key: 'download', label: '结果下载', icon: 'table' },
1317
1648
  ];
1318
1649
 
1650
+ const WORKFLOW_GUIDE_STEPS = [
1651
+ ['导入与核验', '导入文本、Excel、JSON 或图片名单,并核对完整原始数据。'],
1652
+ ['规则与体检', '确认字段映射、匹配规则和补全范围;质量体检在本地完成。'],
1653
+ ['主体匹配', '优先使用统一社会信用代码等强标识;名称不明确时进入人工候选核验。'],
1654
+ ['字段补全', '只补空值并保留非空原值;外部字段以企查查真实返回为准。'],
1655
+ ['结果下载', 'Host 保存结果、异常清单与任务报告,可在任务历史中继续下载。'],
1656
+ ];
1657
+
1319
1658
  const CAPABILITIES = [
1320
1659
  { key: 'upload', label: '导入名单', icon: IconPaperclipOutline16, fallback: '+' },
1321
1660
  { key: 'profile', label: '质量体检', icon: IconChecklistOutline14, fallback: '✓' },
@@ -1952,14 +2291,18 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
1952
2291
  if (!task?.originSessionId) return '历史版本未保存来源';
1953
2292
  return `工作区:${task.originWorkspaceName || task.originWorkspaceId || '未取得来源工作区'} · 会话:${task.originSessionName || task.originSessionId}`;
1954
2293
  }
1955
- function cacheWorkflowTask(actions, sessionId, task) {
2294
+ function cacheWorkflowTask(actions, sessionId, task, reason = 'update') {
1956
2295
  if (!task) return null;
1957
2296
  if (!taskBelongsToSession(task, sessionId)) throw new Error('此任务不属于当前会话;请在任务历史中只读查看或打开来源会话。');
1958
- const previous = workflowTaskBySession.get(String(sessionId));
1959
- if (previous?.id === task.id && previous.revision > task.revision) return previous;
1960
- if (sessionId) workflowTaskBySession.set(String(sessionId), task);
2297
+ const key = String(sessionId || 'unassigned');
2298
+ const current = workflowTaskBySession.get(key);
2299
+ const explicitSwitch = ['create', 'restore'].includes(reason);
2300
+ if (current && current.id !== task.id && !explicitSwitch) return current;
2301
+ if (current?.id === task.id && Number(current.revision ?? -1) > Number(task.revision ?? -1)) return current;
2302
+ workflowTaskBySession.set(key, task);
1961
2303
  bindTask(sessionId, task.id);
1962
- actions.setWorkflowTask(task);
2304
+ if (typeof actions.acceptWorkflowTask === 'function') actions.acceptWorkflowTask(task, reason);
2305
+ else actions.setWorkflowTask(task);
1963
2306
  if (task.title) actions.setTaskTitle(task.title);
1964
2307
  if (Array.isArray(task.objectives)) actions.setObjectives(task.objectives);
1965
2308
  if (Array.isArray(task.mappings)) actions.setMappings(task.mappings);
@@ -1991,7 +2334,7 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
1991
2334
  }
1992
2335
  const response = await pending;
1993
2336
  moveRuntime(`session:${key}`, response.task.id);
1994
- return cacheWorkflowTask(actions, sessionId, response.task);
2337
+ return cacheWorkflowTask(actions, sessionId, response.task, 'create');
1995
2338
  }
1996
2339
 
1997
2340
  async function ensureWorkflowTask(actions, sessionId, draft = {}, restoreStage = true) {
@@ -2002,9 +2345,7 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
2002
2345
  if (ownedSessionId(key) && /^dcw-[a-zA-Z0-9-]+$/.test(saved || '')) {
2003
2346
  const response = await requestJson(`/data-cleaning/api/workflow/tasks/${encodeURIComponent(saved)}`);
2004
2347
  if (taskBelongsToSession(response.task, sessionId)) {
2005
- const task = cacheWorkflowTask(actions, sessionId, response.task);
2006
- if (restoreStage) actions.setStep(task.stage || 'upload');
2007
- return task;
2348
+ return cacheWorkflowTask(actions, sessionId, response.task, restoreStage ? 'restore' : 'update');
2008
2349
  }
2009
2350
  bindTask(sessionId, null);
2010
2351
  }
@@ -2053,7 +2394,7 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
2053
2394
  return cacheWorkflowTask(actions, sessionId, response.task);
2054
2395
  }
2055
2396
 
2056
- async function workflowAction(actions, sessionId, task, action, input = {}) {
2397
+ async function workflowAction(actions, sessionId, task, action, input = {}, reason = 'update') {
2057
2398
  const latest = workflowTaskBySession.get(String(sessionId || 'unassigned')) ?? task;
2058
2399
  if (!latest) throw new Error('请先创建数据清洗补全任务。');
2059
2400
  if (!taskBelongsToSession(latest, sessionId)) throw new Error('此任务不属于当前会话。');
@@ -2062,7 +2403,7 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
2062
2403
  originSessionId: sessionId, originWorkspaceId: latest.originWorkspaceId,
2063
2404
  expectedRevision: latest.revision,
2064
2405
  });
2065
- return cacheWorkflowTask(actions, sessionId, response.task);
2406
+ return cacheWorkflowTask(actions, sessionId, response.task, reason);
2066
2407
  }
2067
2408
 
2068
2409
  async function stageConfirmedWorkflow(actions, sessionId, expected = null) {
@@ -2104,7 +2445,7 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
2104
2445
  return response.command;
2105
2446
  }
2106
2447
 
2107
- async function refreshHostedTask(actions, sessionId, taskId, run, navigate = true) {
2448
+ async function refreshHostedTask(actions, sessionId, taskId, run, reason = 'update') {
2108
2449
  const response = await requestJson(`/data-cleaning/api/workflow/tasks/${encodeURIComponent(taskId)}`);
2109
2450
  if (!taskBelongsToSession(response.task, sessionId)) {
2110
2451
  if (readTaskBindings()[sessionId] === taskId) bindTask(sessionId, null);
@@ -2113,22 +2454,20 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
2113
2454
  const selectedTask = workflowTaskBySession.get(String(sessionId));
2114
2455
  if (selectedTask && selectedTask.id !== taskId) return response.task;
2115
2456
  if (selectedTask && selectedTask.revision > response.task.revision) return selectedTask;
2116
- workflowTaskBySession.set(String(sessionId), response.task);
2117
- bindTask(sessionId, taskId);
2118
2457
  if (isCleaningSession(sessionId)) {
2119
2458
  if (!run && response.task.qccRunId && ['review_required', 'partial'].includes(response.task.state)) {
2120
2459
  try { run = await requestJson(`/data-cleaning/api/g5/run/${encodeURIComponent(response.task.qccRunId)}`); }
2121
2460
  catch { actions.setError('Host 中的候选或重试数据已失效。已有结果仍可下载;重新查询需新建任务,不会自动重复扣量。'); }
2122
2461
  }
2123
- if (!isCleaningSession(sessionId) || workflowTaskBySession.get(String(sessionId))?.id !== taskId) return response.task;
2124
- cacheWorkflowTask(actions, sessionId, response.task);
2462
+ const latestSelectedTask = workflowTaskBySession.get(String(sessionId));
2463
+ if (!isCleaningSession(sessionId) || (latestSelectedTask && latestSelectedTask.id !== taskId)) return response.task;
2464
+ cacheWorkflowTask(actions, sessionId, response.task, reason);
2125
2465
  if (run) {
2126
2466
  actions.setQccRun(run);
2127
2467
  const runtime = runtimeFor(taskId);
2128
2468
  runtime.resultRows.qcc = run.rows;
2129
2469
  if (!runtime.rows.length) { runtime.rows = run.rows; runtime.headers = response.task.source?.headers || run.headers || []; }
2130
2470
  }
2131
- if (navigate) actions.setStep(response.task.stage || 'upload');
2132
2471
  }
2133
2472
  return response.task;
2134
2473
  }
@@ -2136,14 +2475,12 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
2136
2475
  function watchHostedCommand(actions, sessionId, command) {
2137
2476
  const generation = commandWatchGeneration;
2138
2477
  const isActive = () => generation === commandWatchGeneration;
2139
- let openedForExecution = false;
2140
- const ownerVisible = () => typeof document === 'undefined' || [...document.querySelectorAll('.dcAgentExperience')].some(node => node.dataset.sessionId === sessionId);
2141
2478
  void waitForQccCommand(command.commandId, 1800, async (status) => {
2142
2479
  if (!status || status.state === 'prepared') return;
2143
- await refreshHostedTask(actions, sessionId, command.taskId, null, true).catch(() => {});
2144
- if (!openedForExecution && ownerVisible() && isCleaningSession(sessionId) && workflowTaskBySession.get(String(sessionId))?.id === command.taskId) {
2145
- openedForExecution = true;
2146
- openWorkbench(actions, workflowTaskBySession.get(String(sessionId))?.stage || 'match', sessionId);
2480
+ await refreshHostedTask(actions, sessionId, command.taskId, null, 'update').catch(() => {});
2481
+ if (isActive() && workflowTaskBySession.get(String(sessionId))?.id === command.taskId) {
2482
+ sessionWorkbenchController?.revealSubmitted(sessionId, command.commandId,
2483
+ workflowTaskBySession.get(String(sessionId))?.stage || 'match');
2147
2484
  }
2148
2485
  if (isCleaningSession(sessionId) && workflowTaskBySession.get(String(sessionId))?.id === command.taskId) actions.setCommandProgress?.({ taskId: command.taskId, ...status.progress });
2149
2486
  }, isActive).then(async (completed) => {
@@ -2205,6 +2542,32 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
2205
2542
  draft.workflowTask = value ?? null;
2206
2543
  if (value) draft.workflowTasks = [value, ...(draft.workflowTasks || []).filter(task => task.id !== value.id)];
2207
2544
  },
2545
+ acceptWorkflowTask: (draft, value, reason = 'update') => {
2546
+ if (!value) return;
2547
+ const previous = draft.workflowTask;
2548
+ if (previous?.id === value.id && Number(previous.revision ?? -1) > Number(value.revision ?? -1)) return;
2549
+ const previousStage = normalizeViewStage(draft.step);
2550
+ const followedCurrent = previous?.flow?.flowVersion === SUPPORTED_FLOW_VERSION
2551
+ && resolveStageAccess(previousStage, previous.flow) === 'current';
2552
+ draft.workflowTask = value;
2553
+ draft.workflowTasks = [value, ...(draft.workflowTasks || []).filter(task => task.id !== value.id)];
2554
+ if (value.flow?.flowVersion !== SUPPORTED_FLOW_VERSION) {
2555
+ draft.step = value.stage || 'upload';
2556
+ return;
2557
+ }
2558
+ if (draft.step === 'history' && reason !== 'restore') return;
2559
+ if (reason === 'import' && previousStage === 'upload'
2560
+ && resolveStageAccess('upload', value.flow) === 'read'
2561
+ && value.flow.currentStage === 'rules') {
2562
+ draft.step = 'upload';
2563
+ return;
2564
+ }
2565
+ if (!previous || previous.id !== value.id || ['create', 'restore'].includes(reason) || followedCurrent) {
2566
+ draft.step = value.flow.currentStage;
2567
+ return;
2568
+ }
2569
+ if (resolveStageAccess(previousStage, value.flow) === 'locked') draft.step = value.flow.currentStage;
2570
+ },
2208
2571
  setWorkflowTasks: (draft, value) => {
2209
2572
  const cached = new Map((draft.workflowTasks || []).map(task => [task.id, task]));
2210
2573
  draft.workflowTasks = (Array.isArray(value) ? value : []).map(task =>
@@ -2438,7 +2801,73 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
2438
2801
  if (!sessionConversation || typeof sessionConversation.send !== 'function') {
2439
2802
  throw new Error('当前 DSH 版本没有可用的 Session conversation.send 能力');
2440
2803
  }
2441
- await sessionConversation.send(prompt);
2804
+ const result = await sessionConversation.send(prompt);
2805
+ if (result !== false) sessionWorkbenchController?.revealSubmitted(sessionId, submittedCommandId(prompt));
2806
+ }
2807
+
2808
+ function submittedCommandId(prompt) {
2809
+ return String(prompt || '').match(/\bdc[qi]-[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\b/i)?.[0] || null;
2810
+ }
2811
+
2812
+ /** Observe the public admission settlement; drafts and failed sends never reveal a Tab. */
2813
+ function installSubmittedWorkbenchBridge(ctx, controller) {
2814
+ const sessions = typeof ctx.get === 'function' ? ctx.get('sessions') : ctx.sessions;
2815
+ let stopSession = () => {};
2816
+ let current = null;
2817
+ let currentFace = null;
2818
+ const connect = () => {
2819
+ const sessionId = sessions?.list?.getSnapshot?.()?.current;
2820
+ const face = sessionId ? sessions?.binding?.(sessionId)?.session : null;
2821
+ if (sessionId === current && face === currentFace) return;
2822
+ stopSession(); stopSession = () => {}; current = sessionId; currentFace = face;
2823
+ if (!sessionId) return;
2824
+ if (typeof face?.beginSubmission === 'function') {
2825
+ const original = face.beginSubmission;
2826
+ const wrapped = function(input) {
2827
+ if (!isCleaningSession(sessionId)) return original.call(this, input);
2828
+ let observed = false;
2829
+ return original.call(this, { ...input, onRetire: settlement => {
2830
+ try { input.onRetire?.(settlement); }
2831
+ finally {
2832
+ if (settlement.reason === 'observed' && !observed) {
2833
+ observed = true;
2834
+ controller.revealSubmitted(sessionId, submittedCommandId(input.text));
2835
+ }
2836
+ }
2837
+ } });
2838
+ };
2839
+ face.beginSubmission = wrapped;
2840
+ stopSession = () => { if (face.beginSubmission === wrapped) face.beginSubmission = original; };
2841
+ return;
2842
+ }
2843
+ // Older DSH versions publish committed nodes on the Session read face.
2844
+ if (!face?.subscribe || !face?.getSnapshot) return;
2845
+ let previous = face.getSnapshot();
2846
+ let baseline = previous.nodes?.length ?? 0;
2847
+ let highestSeq = Math.max(0, ...(previous.nodes || []).map(node => Number(node.seq) || 0));
2848
+ stopSession = face.subscribe(() => {
2849
+ const snapshot = face.getSnapshot();
2850
+ const nodes = snapshot.nodes || [];
2851
+ const appended = highestSeq ? nodes.filter(node => node.seq > highestSeq) : nodes.slice(baseline);
2852
+ highestSeq = Math.max(highestSeq, ...nodes.map(node => Number(node.seq) || 0));
2853
+ baseline = nodes.length;
2854
+ const restoring = (previous.openState && previous.openState !== 'open') || snapshot.loadingOlder;
2855
+ previous = snapshot;
2856
+ if (restoring) return;
2857
+ if (!isCleaningSession(sessionId)) return;
2858
+ for (const node of appended) {
2859
+ if (node.kind !== 'user' && node.role !== 'user' && node.message?.role !== 'user') continue;
2860
+ const content = node.message?.content ?? node.content ?? node.blocks ?? node.parts;
2861
+ const prompt = node.text || (typeof content === 'string' ? content
2862
+ : Array.isArray(content) ? content.map(part => typeof part === 'string' ? part : part.text || '').join('\n') : '');
2863
+ controller.revealSubmitted(sessionId, submittedCommandId(prompt));
2864
+ }
2865
+ });
2866
+ };
2867
+ connect();
2868
+ const stopList = sessions?.list?.subscribe?.(connect);
2869
+ connect();
2870
+ return () => { stopSession(); stopList?.(); };
2442
2871
  }
2443
2872
 
2444
2873
  function setQccAgentDraft(ctx, sessionId, prompt) {
@@ -2460,8 +2889,11 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
2460
2889
  }
2461
2890
 
2462
2891
  function openWorkbench(actions, step, sessionId) {
2463
- actions.openAt(step, sessionId);
2464
- sessionWorkbenchController?.open(sessionId, step);
2892
+ if (sessionWorkbenchController) sessionWorkbenchController.open(sessionId, step);
2893
+ else {
2894
+ const destination = navigateWorkflow(actions, sessionId, step);
2895
+ actions.openAt(destination.stage, sessionId);
2896
+ }
2465
2897
  }
2466
2898
 
2467
2899
  const WORKBENCH_OPEN_EVENT = 'dsh:data-cleaning-workbench-open';
@@ -2613,8 +3045,68 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
2613
3045
  authorization_required: '待连接企查查', cancelled: '已取消', parse_failed: '解析失败',
2614
3046
  };
2615
3047
 
3048
+ const SUPPORTED_FLOW_VERSION = 1;
3049
+ function normalizeViewStage(stage) {
3050
+ return stage === 'profile' ? 'rules' : stage;
3051
+ }
3052
+ function resolveStageAccess(stage, flow) {
3053
+ const normalized = normalizeViewStage(stage);
3054
+ if (flow?.flowVersion !== SUPPORTED_FLOW_VERSION) return 'locked';
3055
+ return flow.stageAccess?.[normalized] || 'locked';
3056
+ }
3057
+ function workflowActionAllowed(task, action) {
3058
+ return task?.flow?.flowVersion === SUPPORTED_FLOW_VERSION
3059
+ && Array.isArray(task.flow.allowedActions)
3060
+ && task.flow.allowedActions.includes(action);
3061
+ }
3062
+
3063
+ function workflowDestination(requestedStage, task, currentView = 'upload') {
3064
+ if (requestedStage === 'history') return { stage: 'history', access: 'read' };
3065
+ if (!task) {
3066
+ return requestedStage === 'upload' || !requestedStage
3067
+ ? { stage: 'upload', access: 'current' }
3068
+ : { stage: 'upload', access: 'locked' };
3069
+ }
3070
+ if (task.flow?.flowVersion !== SUPPORTED_FLOW_VERSION) {
3071
+ return { stage: task.stage || 'upload', access: 'read', compatibility: true };
3072
+ }
3073
+ const requested = requestedStage || currentView || task.flow.currentStage;
3074
+ const access = resolveStageAccess(requested, task.flow);
3075
+ if (access === 'locked') return { stage: task.flow.currentStage, access };
3076
+ return { stage: requested, access };
3077
+ }
3078
+
3079
+ function workflowStageLabel(stage) {
3080
+ const normalized = normalizeViewStage(stage);
3081
+ return STEPS.find((item) => item.key === normalized)?.label ?? normalized ?? '当前节点';
3082
+ }
3083
+
3084
+ function lockedWorkflowNotice(requestedStage, task, fallbackStage = 'upload') {
3085
+ const currentStage = task?.flow?.currentStage || task?.stage || fallbackStage;
3086
+ return `流程提示:“${workflowStageLabel(requestedStage)}”尚未开放,请先完成“${workflowStageLabel(currentStage)}”。`;
3087
+ }
3088
+
3089
+ function navigateWorkflow(actions, sessionId, requestedStage, task) {
3090
+ const current = task ?? workflowTaskBySession.get(String(sessionId || 'unassigned')) ?? null;
3091
+ const destination = workflowDestination(requestedStage, current);
3092
+ actions.setStep(destination.stage);
3093
+ if (destination.compatibility) {
3094
+ actions.setError('页面与 Host 流程版本不一致,当前任务已切换为只读模式;请完整重启并升级 Host。');
3095
+ } else if (destination.access === 'locked') {
3096
+ actions.setError(lockedWorkflowNotice(requestedStage, current, destination.stage));
3097
+ } else {
3098
+ actions.setError(null);
3099
+ }
3100
+ return destination;
3101
+ }
3102
+
2616
3103
  function workflowNavigationIssue(step, task, hasData, profile, qccRun, hasLocalResult = false) {
2617
- if (['upload', 'history'].includes(step)) return null;
3104
+ if (step === 'history') return null;
3105
+ if (task?.flow?.flowVersion === SUPPORTED_FLOW_VERSION) {
3106
+ const access = resolveStageAccess(step, task.flow);
3107
+ return access === 'locked' ? '该步骤尚未开放,请先完成当前流程节点。' : null;
3108
+ }
3109
+ if (step === 'upload') return null;
2618
3110
  if (step === 'download' && task?.artifacts?.length) return null;
2619
3111
  if (!hasData) return '请先在「导入与核验」载入并核对完整名单。';
2620
3112
  if (step === 'rules') return null;
@@ -2989,7 +3481,7 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
2989
3481
  if (command.taskId) {
2990
3482
  const actions = sessionWorkbenchController?.actionsFor(sessionId);
2991
3483
  if (actions) {
2992
- await refreshHostedTask(actions, sessionId, command.taskId, null, true);
3484
+ await refreshHostedTask(actions, sessionId, command.taskId, null, 'update');
2993
3485
  if (command.state === 'running') requestWorkbenchOpen(command.qccCommand ? 'match' : 'upload', sessionId);
2994
3486
  if (command.qccCommand) {
2995
3487
  const status = await requestJson(`/data-cleaning/api/g5/commands/${encodeURIComponent(command.qccCommand.commandId)}`);
@@ -3179,21 +3671,26 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
3179
3671
  removeImage(sessionId, imageAttachment);
3180
3672
  setImageAttachment(null);
3181
3673
  }
3182
- if (mode === 'text') {
3183
- requestWorkbenchDataset(entriesToDataset(config.entries), sessionId, { type: 'text', fileName: '' });
3184
- } else if (mode === 'excel' && spreadsheetData) {
3185
- requestWorkbenchDataset(spreadsheetData, sessionId, { type: spreadsheetData.fmt || 'xlsx', fileName: spreadsheetFileName });
3186
- }
3187
- requestWorkbenchDraft({
3188
- title: `企业数据清洗补全任务 · ${config.entryCount || '待解析'} 条`,
3189
- objectives: cleaningKeys,
3190
- fieldSelection: enrichmentKeys,
3191
- matchRules,
3192
- ...(mode === 'excel' ? { mappings: spreadsheetMappings } : {}),
3193
- source: { type: mode === 'excel' ? 'xlsx' : mode, fileName: config.fileName || '', rowCount: config.entryCount || 0 },
3194
- }, sessionId);
3195
3674
  setBusy(true);
3196
3675
  try {
3676
+ if (mode === 'text') {
3677
+ const local = entriesToDataset(config.entries);
3678
+ const verified = await api('/data-cleaning/api/mvp/parse', {
3679
+ filename: 'data.json',
3680
+ content: JSON.stringify(local.rows),
3681
+ });
3682
+ requestWorkbenchDataset({ ...verified, fmt: 'text' }, sessionId, { type: 'text', fileName: '' });
3683
+ } else if (mode === 'excel' && spreadsheetData) {
3684
+ requestWorkbenchDataset(spreadsheetData, sessionId, { type: spreadsheetData.fmt || 'xlsx', fileName: spreadsheetFileName });
3685
+ }
3686
+ requestWorkbenchDraft({
3687
+ title: `企业数据清洗补全任务 · ${config.entryCount || '待解析'} 条`,
3688
+ objectives: cleaningKeys,
3689
+ fieldSelection: enrichmentKeys,
3690
+ matchRules,
3691
+ ...(mode === 'excel' ? { mappings: spreadsheetMappings } : {}),
3692
+ source: { type: mode === 'excel' ? 'xlsx' : mode, fileName: config.fileName || '', rowCount: config.entryCount || 0 },
3693
+ }, sessionId);
3197
3694
  let prompt = buildTaskPrompt(config);
3198
3695
  if (mode !== 'image' || ['prepared', 'completed'].includes(imageCommand?.state)) {
3199
3696
  const command = await requestPreparedWorkflow(sessionId, config);
@@ -3274,12 +3771,14 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
3274
3771
  const receiveNewTask = (event) => {
3275
3772
  if (event.detail?.sessionId !== sessionId || busy || imageCommand?.state === 'running') return;
3276
3773
  event.preventDefault();
3774
+ const reused = event.detail?.reuseConfig || null;
3277
3775
  discardCurrentImage();
3278
3776
  setMode('text'); setRawText(''); setEntries([]); setEntryCount(0); setSpreadsheetFileName('');
3279
3777
  setSpreadsheetData(null); setSpreadsheetMappings([]);
3280
- setCleaningKeys(DEFAULT_CLEANING_KEYS); setEnrichmentKeys(DEFAULT_ENRICHMENT_KEYS);
3778
+ setCleaningKeys(reused?.objectives || DEFAULT_CLEANING_KEYS);
3779
+ setEnrichmentKeys(reused?.fieldSelection || DEFAULT_ENRICHMENT_KEYS);
3281
3780
  setAnchorKeys(['company_name', 'credit_no']);
3282
- setPromptMatchRules({ normalizeNames: true, preferCreditNo: true, deduplicate: true, manualReviewAmbiguous: true });
3781
+ setPromptMatchRules(reused?.matchRules || { normalizeNames: true, preferCreditNo: true, deduplicate: true, manualReviewAmbiguous: true });
3283
3782
  setWizardStep(1); setOpen(true); setError(null);
3284
3783
  };
3285
3784
  const receiveImage = (event) => {
@@ -3617,7 +4116,7 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
3617
4116
  }
3618
4117
 
3619
4118
  /** 上传 pane 的处理:文件 → 文本/base64 → parse。 */
3620
- async function parseFile(file) {
4119
+ async function parseFile(file, previousChecksum) {
3621
4120
  const isXlsx = /\.(xlsx|xls)$/i.test(file && file.name ? file.name : '');
3622
4121
  let content;
3623
4122
  if (isXlsx) {
@@ -3629,7 +4128,7 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
3629
4128
  } else {
3630
4129
  content = await file.text();
3631
4130
  }
3632
- return api('/data-cleaning/api/mvp/parse', { filename: file.name, content });
4131
+ return api('/data-cleaning/api/mvp/parse', { filename: file.name, content, previousChecksum });
3633
4132
  }
3634
4133
 
3635
4134
  /** 文本/JSON 直通 parse(与 web.js 内联页同构,含 JSON 数组直通)。 */
@@ -3644,7 +4143,7 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
3644
4143
  };
3645
4144
  }
3646
4145
 
3647
- async function parseText(text, actions) {
4146
+ async function parseText(text, actions, previousChecksum) {
3648
4147
  const trimmed = (text ?? '').trim();
3649
4148
  if (!trimmed) {
3650
4149
  actions.setError('请粘贴或上传数据后再解析。');
@@ -3652,18 +4151,26 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
3652
4151
  }
3653
4152
  if (trimmed.startsWith('[')) {
3654
4153
  try {
3655
- const rows = JSON.parse(trimmed);
3656
- const list = Array.isArray(rows) ? rows : [];
3657
- const headers = list.length ? Object.keys(list[0]) : [];
3658
- return { ok: true, fmt: 'json', headers, rowCount: list.length, rows: list, preview: list.slice(0, 5) };
4154
+ return api('/data-cleaning/api/mvp/parse', {
4155
+ filename: 'data.json',
4156
+ content: trimmed,
4157
+ previousChecksum,
4158
+ });
3659
4159
  } catch (error) {
3660
4160
  actions.setError(`JSON 解析失败:${error instanceof Error ? error.message : String(error)}`);
3661
4161
  }
3662
4162
  return;
3663
4163
  }
3664
4164
  const entityList = plainEntityListDataset(trimmed);
3665
- if (entityList) return entityList;
3666
- return api('/data-cleaning/api/mvp/parse', { filename: 'data.csv', content: trimmed });
4165
+ if (entityList) {
4166
+ const parsed = await api('/data-cleaning/api/mvp/parse', {
4167
+ filename: 'data.json',
4168
+ content: JSON.stringify(entityList.rows),
4169
+ previousChecksum,
4170
+ });
4171
+ return { ...parsed, fmt: 'text' };
4172
+ }
4173
+ return api('/data-cleaning/api/mvp/parse', { filename: 'data.csv', content: trimmed, previousChecksum });
3667
4174
  }
3668
4175
 
3669
4176
  /** 解析成功进入 taskId runtime;不把整表写入 Host store。 */
@@ -3676,6 +4183,7 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
3676
4183
  type: source.type || result.fmt || 'csv',
3677
4184
  fileName: source.fileName || '',
3678
4185
  sizeBytes: source.sizeBytes || 0,
4186
+ checksum: result.checksum || source.checksum || '',
3679
4187
  };
3680
4188
  const mappings = guessMappings(runtime.headers);
3681
4189
  const guessedNameField = mappings.find((mapping) => mapping.targetField === 'company_name')?.sourceField
@@ -3695,6 +4203,28 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
3695
4203
  return runtime;
3696
4204
  }
3697
4205
 
4206
+ function restoreParsedRuntime(result, actions, task, source = {}) {
4207
+ const runtime = runtimeFor(task.id);
4208
+ runtime.rows = Array.isArray(result.rows) ? result.rows : [];
4209
+ runtime.sourceRows = runtime.rows;
4210
+ runtime.headers = Array.isArray(result.headers) ? result.headers : [];
4211
+ runtime.source = {
4212
+ type: source.type || result.fmt || task.source?.type || 'csv',
4213
+ fileName: source.fileName || task.source?.fileName || '',
4214
+ sizeBytes: source.sizeBytes || task.source?.sizeBytes || 0,
4215
+ checksum: result.checksum || '',
4216
+ };
4217
+ actions.setDataset({
4218
+ fmt: result.fmt ?? task.source?.type ?? 'csv',
4219
+ headers: runtime.headers,
4220
+ rowCount: typeof result.rowCount === 'number' ? result.rowCount : runtime.rows.length,
4221
+ preview: Array.isArray(result.preview) ? result.preview : runtime.rows.slice(0, 5),
4222
+ });
4223
+ const primary = (task.mappings || []).find((mapping) => ['company_name', 'credit_no', 'reg_no'].includes(mapping.targetField));
4224
+ if (primary) actions.setNameField(primary.sourceField);
4225
+ return runtime;
4226
+ }
4227
+
3698
4228
  async function persistParsedWorkflow(actions, sessionId, result, source = {}, task) {
3699
4229
  const importedMappings = guessMappings(result.headers || []);
3700
4230
  const importedDraft = { mappings: importedMappings,
@@ -3708,10 +4238,11 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
3708
4238
  target.rows = Array.isArray(result.rows) ? result.rows : [];
3709
4239
  target.sourceRows = target.rows;
3710
4240
  target.headers = Array.isArray(result.headers) ? result.headers : [];
3711
- target.source = {
3712
- type: source.type || result.fmt || 'csv',
3713
- fileName: source.fileName || '',
3714
- sizeBytes: source.sizeBytes || 0,
4241
+ target.source = {
4242
+ type: source.type || result.fmt || 'csv',
4243
+ fileName: source.fileName || '',
4244
+ sizeBytes: source.sizeBytes || 0,
4245
+ checksum: result.checksum || source.checksum || '',
3715
4246
  };
3716
4247
  if (['draft', 'uploaded', 'parse_failed'].includes(current.state)) {
3717
4248
  current = await workflowAction(actions, sessionId, current, 'upload', {
@@ -3722,8 +4253,9 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
3722
4253
  columnCount: Array.isArray(result.headers) ? result.headers.length : 0,
3723
4254
  headers: Array.isArray(result.headers) ? result.headers : [],
3724
4255
  sizeBytes: source.sizeBytes || 0,
4256
+ checksum: result.checksum || source.checksum || '',
3725
4257
  },
3726
- });
4258
+ }, 'import');
3727
4259
  }
3728
4260
  if (current.state === 'uploaded') current = await updateWorkflowTask(actions, sessionId, current, importedDraft);
3729
4261
  return current;
@@ -3794,6 +4326,103 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
3794
4326
  );
3795
4327
  }
3796
4328
 
4329
+ function WorkflowTaskContext(props) {
4330
+ const stageCompleted = props.stageCompleted;
4331
+ const progressVisible = props.commandProgress?.taskId === props.task.id
4332
+ && Number.isFinite(props.commandProgress.totalUnique);
4333
+ const completedUnique = Math.min(
4334
+ props.commandProgress?.totalUnique || 0,
4335
+ Math.max(0, props.commandProgress?.completedUnique || 0),
4336
+ );
4337
+ return h('div', { className: 'dcAgentTaskStatus', 'aria-live': 'polite' },
4338
+ h('div', { className: 'dcAgentTaskStatusRow' },
4339
+ h('div', { className: 'dcAgentTaskStage' },
4340
+ h('span', {
4341
+ className: 'dcAgentTaskDoneMark',
4342
+ 'data-complete': stageCompleted,
4343
+ 'aria-hidden': true,
4344
+ }, stageCompleted ? '✓' : '•'),
4345
+ h('div', { className: 'dcAgentTaskStageCopy' },
4346
+ h('div', { className: 'dcAgentTaskStageTitle' },
4347
+ h('strong', null, props.stageLabel),
4348
+ h('span', {
4349
+ className: 'dcAgentTaskState',
4350
+ 'data-complete': stageCompleted,
4351
+ }, props.statusLabel),
4352
+ ),
4353
+ props.timeLabel || props.sourceLabel ? h('div', { className: 'dcAgentTaskMeta' },
4354
+ props.timeLabel ? h('span', null,
4355
+ h('time', { dateTime: props.timeValue || undefined }, props.timeLabel)) : null,
4356
+ props.sourceLabel ? h('span', null, props.sourceLabel) : null,
4357
+ ) : null,
4358
+ ),
4359
+ ),
4360
+ props.viewAccess === 'read' ? h('div', { className: 'dcAgentTaskCurrent' },
4361
+ h('span', null, '当前节点'),
4362
+ h('strong', null, props.currentStageLabel),
4363
+ ) : null,
4364
+ props.viewAccess === 'read' && props.onReturn ? h('button', {
4365
+ type: 'button',
4366
+ className: 'dcAgentTaskReturn',
4367
+ onClick: props.onReturn,
4368
+ }, '回到当前节点') : null,
4369
+ h('details', { className: 'dcAgentTaskDetails' },
4370
+ h('summary', { 'aria-label': '任务详情', title: '任务详情' }, 'i'),
4371
+ h('div', { className: 'dcAgentTaskDetailsBody' },
4372
+ h('strong', null, `${props.sourceLabel || '当前任务'} · ${props.statusLabel}`),
4373
+ h('p', null, `任务编号:${props.task.id}`),
4374
+ h('p', null, workflowOriginLabel(props.task)),
4375
+ h('p', null, `${props.rowCount} 条企业记录 · ${props.fieldCount} 个补全字段`),
4376
+ ),
4377
+ ),
4378
+ ),
4379
+ ['matching', 'enriching'].includes(props.task.state) ? h('p', { className: 'dcAgentTaskSyncNote' },
4380
+ '这是最近同步的任务记录;若原会话已停止,请先查看确认或错误提示,不要重复启动。') : null,
4381
+ progressVisible ? h('div', { className: 'dcAgentTaskExecution' },
4382
+ h('span', null, `已处理主体 ${completedUnique}/${props.commandProgress.totalUnique}`),
4383
+ h('progress', {
4384
+ max: props.commandProgress.totalUnique,
4385
+ value: completedUnique,
4386
+ 'aria-label': '主体处理进度',
4387
+ }),
4388
+ ) : null,
4389
+ h('div', { className: 'dcAgentTaskMetrics', 'aria-label': '任务摘要' },
4390
+ h('div', { className: 'dcAgentTaskMetricGroup' },
4391
+ h('span', { className: 'dcAgentTaskMetricLabel' }, '任务范围'),
4392
+ h('span', { className: 'dcAgentTaskMetric' }, '企业 ', h('b', null, props.rowCount), ' 家'),
4393
+ h('span', { className: 'dcAgentTaskMetric' }, '字段 ', h('b', null, props.fieldCount), ' 个'),
4394
+ ),
4395
+ props.hasResultSummary ? h('div', { className: 'dcAgentTaskMetricGroup' },
4396
+ h('span', { className: 'dcAgentTaskMetricLabel' }, '处理结果'),
4397
+ h('span', { className: 'dcAgentTaskMetric is-good' }, '已补全 ', h('b', null, props.completedMetric)),
4398
+ h('span', { className: 'dcAgentTaskMetric' }, '待核验 ', h('b', null, props.reviewMetric)),
4399
+ h('span', { className: 'dcAgentTaskMetric' }, '失败 ', h('b', null, props.failedMetric)),
4400
+ ) : null,
4401
+ ),
4402
+ );
4403
+ }
4404
+
4405
+ function WorkflowGuide() {
4406
+ return h('details', { className: 'dcAgentWorkflowGuide' },
4407
+ h('summary', null, '使用说明和任务流程'),
4408
+ h('div', { className: 'dcAgentWorkflowGuideContent' },
4409
+ h('p', { className: 'dcAgentWorkflowGuideIntro' },
4410
+ '导入与核验 → 规则与体检 → 主体匹配 → 字段补全 → 结果下载。菜单只切换视图,不启动调用;按页面主按钮逐步确认。'),
4411
+ h('p', { className: 'dcAgentWorkflowGuideIntro' },
4412
+ '支持文本、Excel、JSON 和图片名单。原值保留、多候选人工核验;企查查调用使用当前用户自己的连接与额度。'),
4413
+ h('div', { className: 'dcAgentWorkflowGuideList' },
4414
+ WORKFLOW_GUIDE_STEPS.map(([title, description], index) => h('div', {
4415
+ key: title,
4416
+ className: 'dcAgentWorkflowGuideStep',
4417
+ },
4418
+ h('span', { className: 'dcAgentWorkflowGuideIndex' }, String(index + 1)),
4419
+ h('div', null, h('strong', null, title), h('p', null, description))))),
4420
+ h('div', { className: 'dcAgentWorkflowGuideBoundary' },
4421
+ '生成任务说明不会查询;发送说明或显式重试时,才使用当前用户连接的企查查账号及额度。'),
4422
+ ),
4423
+ );
4424
+ }
4425
+
3797
4426
  /** Session Tab 业务内容;尺寸、停靠、关闭均由 Better Sidebar 宿主管理。 */
3798
4427
  function WorkbenchContent(props) {
3799
4428
  const { useStore, actions } = props;
@@ -3846,13 +4475,19 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
3846
4475
  requestJson('/data-cleaning/api/workflow/contract').then((response) => {
3847
4476
  if (!disposed) actions.setWorkflowContract(response.contract);
3848
4477
  }).catch(() => {});
3849
- loadWorkflowTasks(actions);
4478
+ const tasksPromise = loadWorkflowTasks(actions);
3850
4479
  if (activeSessionId) {
3851
4480
  queueWorkflowOperation(activeSessionId, async () => {
3852
4481
  const key = String(activeSessionId);
4482
+ const tasks = await tasksPromise;
3853
4483
  const saved = ownedSessionId(key) ? readTaskBindings()[key] : null;
3854
- const taskId = workflowTaskBySession.get(key)?.id || (/^dcw-[a-zA-Z0-9-]+$/.test(saved || '') ? saved : null);
3855
- if (taskId) await refreshHostedTask(actions, activeSessionId, taskId, null, false);
4484
+ const hosted = tasks
4485
+ .filter((task) => taskBelongsToSession(task, activeSessionId))
4486
+ .sort((left, right) => String(right.createdAt).localeCompare(String(left.createdAt)))[0]?.id;
4487
+ const taskId = workflowTaskBySession.get(key)?.id
4488
+ || hosted
4489
+ || (/^dcw-[a-zA-Z0-9-]+$/.test(saved || '') ? saved : null);
4490
+ if (taskId) await refreshHostedTask(actions, activeSessionId, taskId, null, 'update');
3856
4491
  }).catch((taskError) => {
3857
4492
  if (!disposed) actions.setError(`状态同步异常:${taskError instanceof Error ? taskError.message : String(taskError)}。保留最近任务记录,未重新执行查询。`);
3858
4493
  });
@@ -3867,11 +4502,10 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
3867
4502
  let disposed = false, timer;
3868
4503
  const poll = async () => {
3869
4504
  try {
3870
- const task = await refreshHostedTask(actions, activeSessionId, workflowTask.id, null, false);
4505
+ const task = await refreshHostedTask(actions, activeSessionId, workflowTask.id, null, 'update');
3871
4506
  if (disposed) return;
3872
4507
  if (!task) return;
3873
4508
  if (['matching', 'enriching'].includes(task.state)) timer = setTimeout(poll, 2000);
3874
- else if (step === 'match' || step === 'enrich') actions.setStep(task.stage || step);
3875
4509
  } catch (error) { if (!disposed) actions.setError(`状态同步异常:${error.message}。保留最近任务记录,未重新执行查询。`); }
3876
4510
  };
3877
4511
  timer = setTimeout(poll, 2000);
@@ -3888,7 +4522,29 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
3888
4522
  const stagedSource = runtimeFor(`session:${activeSessionId || 'unassigned'}`, false);
3889
4523
  const sourceRows = stagedSource?.sourceRows ?? runtime.sourceRows ?? runtime.rows;
3890
4524
  const intake = intakeState(dataset, sourceRows, input, runtime.source || stagedSource?.source || cachedTask?.source);
3891
- const showIntakeEditor = !dataset || intakeEditorOpen;
4525
+ const normalizedStage = normalizeViewStage(step);
4526
+ const flowCompatible = cachedTask?.flow?.flowVersion === SUPPORTED_FLOW_VERSION;
4527
+ const viewAccess = step === 'history'
4528
+ ? 'read'
4529
+ : cachedTask
4530
+ ? resolveStageAccess(normalizedStage, cachedTask.flow)
4531
+ : normalizedStage === 'upload' ? 'current' : 'locked';
4532
+ const allowedActions = new Set(flowCompatible ? cachedTask.flow.allowedActions || [] : []);
4533
+ const isCurrentView = viewAccess === 'current';
4534
+ const canAction = (action) => isCurrentView && (
4535
+ cachedTask ? allowedActions.has(action) : action === 'import-data'
4536
+ );
4537
+ const reviewingImportedDataset = normalizedStage === 'upload'
4538
+ && cachedTask?.state === 'uploaded'
4539
+ && cachedTask.flow?.currentStage === 'rules'
4540
+ && viewAccess === 'read';
4541
+ const navigate = (destination) => navigateWorkflow(actions, activeSessionId, destination, cachedTask);
4542
+ const showIntakeEditor = canAction('import-data') && (!dataset || intakeEditorOpen);
4543
+ const runtimeRecoveryNeeded = Boolean(cachedTask?.source?.rowCount) && runtime.rows.length === 0;
4544
+ const runtimeRecoveryAllowed = runtimeRecoveryNeeded
4545
+ && isCurrentView
4546
+ && ['uploaded', 'rules_confirmed', 'diagnosed'].includes(cachedTask?.state)
4547
+ && cachedTask?.source?.type !== 'image';
3892
4548
  const lastCsv = runtime.lastCsv;
3893
4549
  const fieldByPattern = (pattern) => runtime.headers.find((field) => pattern.test(field)) ?? null;
3894
4550
  const phoneField = fieldByPattern(/^(phone|mobile|tel|telephone|联系电话|手机号码|手机号)$/i);
@@ -3907,7 +4563,7 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
3907
4563
  };
3908
4564
 
3909
4565
  const handleParse = async () => {
3910
- if (busy) return;
4566
+ if (busy || !canAction('import-data')) return;
3911
4567
  if (!intake.pendingText) {
3912
4568
  if (intake.available) actions.setError(null);
3913
4569
  return;
@@ -3915,7 +4571,7 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
3915
4571
  actions.setBusy(true);
3916
4572
  actions.setError(null);
3917
4573
  try {
3918
- const result = await parseText(input, actions);
4574
+ const result = await parseText(input, actions, cachedTask?.source?.checksum);
3919
4575
  if (result && result.ok !== false) {
3920
4576
  applyParsed(result, actions, runtimeKey, { type: result.fmt || 'csv' });
3921
4577
  await persistParsedWorkflow(actions, activeSessionId, result, { type: result.fmt || 'csv' }, cachedTask);
@@ -3939,7 +4595,7 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
3939
4595
  return true;
3940
4596
  };
3941
4597
  const handleFile = async (event) => {
3942
- if (busy) return;
4598
+ if (busy || !canAction('import-data')) return;
3943
4599
  const file = event.target && event.target.files && event.target.files[0];
3944
4600
  if (!file) return;
3945
4601
  if (String(file.type ?? '').startsWith('image/')) {
@@ -3950,7 +4606,7 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
3950
4606
  actions.setBusy(true);
3951
4607
  actions.setError(null);
3952
4608
  try {
3953
- const result = await parseFile(file);
4609
+ const result = await parseFile(file, cachedTask?.source?.checksum);
3954
4610
  if (result && result.ok !== false) {
3955
4611
  applyParsed(result, actions, runtimeKey, { type: result.fmt || 'csv', fileName: file.name, sizeBytes: file.size || 0 });
3956
4612
  await persistParsedWorkflow(actions, activeSessionId, result, { type: result.fmt || 'csv', fileName: file.name, sizeBytes: file.size || 0 }, cachedTask);
@@ -3965,6 +4621,49 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
3965
4621
  }
3966
4622
  };
3967
4623
 
4624
+ const acceptRuntimeRecovery = (result, source) => {
4625
+ if (!cachedTask?.source?.checksum) {
4626
+ throw new Error('该旧任务缺少同源校验信息,不能恢复写操作。请新建任务并复用配置。');
4627
+ }
4628
+ if (!result?.checksum || result.checksum !== cachedTask.source.checksum) {
4629
+ throw new Error('重新载入的数据与原任务不一致,未修改当前任务。请新建任务处理其他名单。');
4630
+ }
4631
+ restoreParsedRuntime(result, actions, cachedTask, source);
4632
+ actions.setInput('');
4633
+ actions.setError(null);
4634
+ };
4635
+
4636
+ const recoverRuntimeFile = async (event) => {
4637
+ if (busy) return;
4638
+ const file = event.target?.files?.[0];
4639
+ if (!file) return;
4640
+ actions.setBusy(true);
4641
+ actions.setError(null);
4642
+ try {
4643
+ const result = await parseFile(file, cachedTask?.source?.checksum);
4644
+ acceptRuntimeRecovery(result, { type: result.fmt, fileName: file.name, sizeBytes: file.size || 0 });
4645
+ } catch (err) {
4646
+ actions.setError(err instanceof Error ? err.message : String(err));
4647
+ } finally {
4648
+ event.target.value = '';
4649
+ actions.setBusy(false);
4650
+ }
4651
+ };
4652
+
4653
+ const recoverRuntimeText = async () => {
4654
+ if (busy || !String(input || '').trim()) return;
4655
+ actions.setBusy(true);
4656
+ actions.setError(null);
4657
+ try {
4658
+ const result = await parseText(input, actions, cachedTask?.source?.checksum);
4659
+ acceptRuntimeRecovery(result, { type: result.fmt, fileName: cachedTask?.source?.fileName || '' });
4660
+ } catch (err) {
4661
+ actions.setError(err instanceof Error ? err.message : String(err));
4662
+ } finally {
4663
+ actions.setBusy(false);
4664
+ }
4665
+ };
4666
+
3968
4667
  const performProfile = async (task) => {
3969
4668
  const r = await api('/data-cleaning/api/mvp/profile', {
3970
4669
  rows: runtime.rows,
@@ -3978,7 +4677,7 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
3978
4677
  };
3979
4678
 
3980
4679
  const runProfile = async () => {
3981
- if (busy || !runtime.rows.length) {
4680
+ if (busy || !canAction('run-quality') || !runtime.rows.length) {
3982
4681
  if (!runtime.rows.length) actions.setError('请先上传并解析数据。');
3983
4682
  return;
3984
4683
  }
@@ -3986,7 +4685,6 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
3986
4685
  actions.setError(null);
3987
4686
  try {
3988
4687
  await performProfile(cachedTask);
3989
- actions.setStep(requiresQcc ? 'match' : 'enrich');
3990
4688
  } catch (err) {
3991
4689
  actions.setError(err instanceof Error ? err.message : String(err));
3992
4690
  } finally {
@@ -3995,7 +4693,7 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
3995
4693
  };
3996
4694
 
3997
4695
  const runClean = async () => {
3998
- if (busy || !runtime.rows.length) {
4696
+ if (busy || !canAction('run-local-clean') || !runtime.rows.length) {
3999
4697
  if (!runtime.rows.length) actions.setError('请先上传并解析数据。');
4000
4698
  return;
4001
4699
  }
@@ -4023,7 +4721,7 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
4023
4721
  };
4024
4722
 
4025
4723
  const runComplete = async () => {
4026
- if (busy || !runtime.rows.length) {
4724
+ if (busy || !canAction('run-local-complete') || !runtime.rows.length) {
4027
4725
  if (!runtime.rows.length) actions.setError('请先上传并解析数据。');
4028
4726
  return;
4029
4727
  }
@@ -4056,48 +4754,9 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
4056
4754
  runtime.resultRows.qcc = Array.isArray(run.rows) ? run.rows : null;
4057
4755
  lastCsv.qcc = { csv: run.csv ?? '', name: run.downloadName ?? 'qcc-enriched.csv' };
4058
4756
  if (run.reviewCsv) lastCsv.review = { csv: run.reviewCsv, name: run.reviewDownloadName ?? 'qcc-review.csv' };
4059
- const summary = run.summary ?? {};
4060
- const reviewRequired = Number(summary.ambiguous ?? run.reviewQueue?.length ?? 0);
4061
4757
  const currentTask = workflowTaskBySession.get(String(activeSessionId || 'unassigned')) ?? cachedTask;
4062
- let current = currentTask;
4063
- if (currentTask?.qccRunId === run.runId && ['completed', 'partial', 'review_required', 'export_ready'].includes(currentTask.state)) {
4064
- actions.setStep(currentTask.stage || 'download');
4065
- return currentTask;
4066
- }
4067
- // partial 表示上一轮仍有失败项。重试成功后可直接重进 enrichment,
4068
- // 无需伪造一次新的 match 转换;若重试产生候选,再回到人工核验。
4069
- if (currentTask?.state !== 'partial' || reviewRequired > 0) {
4070
- current = await workflowAction(actions, activeSessionId, currentTask, 'match', {
4071
- qccRunId: run.runId,
4072
- summary: {
4073
- total: Number(summary.totalRows ?? runtime.rows.length),
4074
- exact: Number(summary.enriched ?? 0) + Number(summary.fieldReview ?? 0),
4075
- candidate: reviewRequired,
4076
- confirmed: 0,
4077
- unresolved: Number(summary.unresolved ?? summary.missingName ?? 0),
4078
- failed: Number(summary.failed ?? 0),
4079
- reviewRequired,
4080
- },
4081
- });
4082
- }
4083
- if (reviewRequired > 0) {
4084
- actions.setStep('match');
4085
- return current;
4086
- }
4087
- current = await workflowAction(actions, activeSessionId, current, 'enrich-start', { fieldSelection });
4088
- current = await workflowAction(actions, activeSessionId, current, 'enrichment', {
4089
- qccRunId: run.runId,
4090
- summary: {
4091
- total: Number(summary.totalRows ?? runtime.rows.length),
4092
- completed: Number(summary.enriched ?? 0),
4093
- unchanged: Number(summary.unresolved ?? 0) + Number(summary.missingName ?? 0),
4094
- failed: Number(summary.failed ?? 0),
4095
- reviewRequired: Number(summary.fieldReview ?? 0),
4096
- callsUsed: Array.isArray(run.audit) ? run.audit.length : 0,
4097
- },
4098
- });
4099
- actions.setStep(current.stage === 'download' ? 'download' : 'enrich');
4100
- return current;
4758
+ if (!currentTask?.id) return null;
4759
+ return refreshHostedTask(actions, activeSessionId, currentTask.id, run, 'update');
4101
4760
  };
4102
4761
 
4103
4762
  const executePreparedQccCommand = async (payload) => {
@@ -4137,7 +4796,7 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
4137
4796
  };
4138
4797
 
4139
4798
  const runQcc = async () => {
4140
- if (busy || !cachedTask?.id || !hasData) return;
4799
+ if (busy || !canAction('prepare-qcc-command') || !cachedTask?.id || !hasData) return;
4141
4800
  actions.setBusy(true);
4142
4801
  actions.setError(null);
4143
4802
  try {
@@ -4159,7 +4818,7 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
4159
4818
  };
4160
4819
 
4161
4820
  const resolveCandidate = async (item, candidate) => {
4162
- if (busy || !paidConfirmed || !qccRun) return;
4821
+ if (busy || !canAction('resolve-candidate') || !paidConfirmed || !qccRun) return;
4163
4822
  actions.setBusy(true);
4164
4823
  actions.setError(null);
4165
4824
  try {
@@ -4180,7 +4839,7 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
4180
4839
  };
4181
4840
 
4182
4841
  const retryFailures = async () => {
4183
- if (busy || !paidConfirmed || !qccRun) return;
4842
+ if (busy || !canAction('retry-failed') || !paidConfirmed || !qccRun) return;
4184
4843
  const names = [...new Set((qccRun.errors || []).filter((item) => item.error && item.error.retryable).map((item) => item.companyName))];
4185
4844
  if (!names.length) return;
4186
4845
  actions.setBusy(true);
@@ -4201,7 +4860,21 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
4201
4860
  }
4202
4861
  };
4203
4862
 
4863
+ const retryDelivery = async () => {
4864
+ if (busy || !canAction('retry-delivery') || !cachedTask?.id) return;
4865
+ actions.setBusy(true);
4866
+ actions.setError(null);
4867
+ try {
4868
+ await workflowAction(actions, activeSessionId, cachedTask, 'retry-delivery');
4869
+ } catch (err) {
4870
+ actions.setError(err instanceof Error ? err.message : String(err));
4871
+ } finally {
4872
+ actions.setBusy(false);
4873
+ }
4874
+ };
4875
+
4204
4876
  const saveTaskSettings = async () => {
4877
+ if (!canAction('edit-rules')) return;
4205
4878
  actions.setBusy(true);
4206
4879
  actions.setError(null);
4207
4880
  try {
@@ -4219,6 +4892,7 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
4219
4892
  };
4220
4893
 
4221
4894
  const confirmRuleSettings = async () => {
4895
+ if (!canAction('confirm-rules')) return;
4222
4896
  if (!hasData) { actions.setError('请先上传并解析数据。'); return; }
4223
4897
  const anchorMappings = mappings.filter((mapping) => ['company_name', 'credit_no', 'reg_no'].includes(mapping.targetField));
4224
4898
  if (!anchorMappings.length) { actions.setError('请至少映射企业名称、统一社会信用代码或注册号。'); return; }
@@ -4238,6 +4912,7 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
4238
4912
  columnCount: dataset.headers?.length || 0,
4239
4913
  headers: dataset.headers || [],
4240
4914
  sizeBytes: runtime.source?.sizeBytes || 0,
4915
+ checksum: runtime.source?.checksum || '',
4241
4916
  },
4242
4917
  });
4243
4918
  }
@@ -4252,7 +4927,6 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
4252
4927
  ?? mappings.find((mapping) => mapping.targetField === 'credit_no')
4253
4928
  ?? mappings.find((mapping) => mapping.targetField === 'reg_no');
4254
4929
  if (primary) actions.setNameField(primary.sourceField);
4255
- actions.setStep('profile');
4256
4930
  } catch (ruleError) {
4257
4931
  actions.setError(ruleError instanceof Error ? ruleError.message : String(ruleError));
4258
4932
  } finally {
@@ -4269,9 +4943,16 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
4269
4943
  } catch (error) { actions.setError(error.message); return; }
4270
4944
  };
4271
4945
 
4272
- const startNewWorkflow = () => {
4946
+ const startNewWorkflow = (reuseConfig = false) => {
4273
4947
  const event = document.createEvent('CustomEvent');
4274
- event.initCustomEvent('dsh:data-cleaning-prompt-new', false, true, { sessionId: activeSessionId });
4948
+ event.initCustomEvent('dsh:data-cleaning-prompt-new', false, true, {
4949
+ sessionId: activeSessionId,
4950
+ ...(reuseConfig && cachedTask ? { reuseConfig: {
4951
+ objectives: cachedTask.objectives || [],
4952
+ fieldSelection: cachedTask.fieldSelection || [],
4953
+ matchRules: cachedTask.matchRules || {},
4954
+ } } : {}),
4955
+ });
4275
4956
  if (document.dispatchEvent(event)) {
4276
4957
  actions.setError('当前会话的任务向导尚未就绪或正在解析图片,请稍后重试。');
4277
4958
  return;
@@ -4292,6 +4973,7 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
4292
4973
  const createArtifactBundle = async () => {
4293
4974
  let latest = workflowTaskBySession.get(String(activeSessionId || 'unassigned')) ?? cachedTask;
4294
4975
  if (!latest) throw new Error('请先创建并执行数据清洗补全任务。');
4976
+ if (!canAction('create-local-artifacts')) throw new Error('当前流程节点不能生成本地结果。');
4295
4977
  if (Array.isArray(latest.artifacts) && latest.artifacts.length) return latest;
4296
4978
  const rows = exportRows();
4297
4979
  if (!rows?.length) throw new Error('当前页面没有可导出的明细。若任务来自历史记录,请重新上传源文件并继续处理。');
@@ -4372,6 +5054,37 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
4372
5054
  : cachedTask?.enrichmentSummary?.reviewRequired
4373
5055
  ?? cachedTask?.matchSummary?.reviewRequired;
4374
5056
  const qccFailedCount = qccRun?.summary?.failed ?? cachedTask?.enrichmentSummary?.failed ?? cachedTask?.matchSummary?.failed ?? 0;
5057
+ const viewedStageLabel = workflowStageLabel(normalizedStage);
5058
+ const currentStageLabel = workflowStageLabel(cachedTask?.flow?.currentStage || cachedTask?.stage || 'upload');
5059
+ const taskStatusLabel = viewAccess === 'read'
5060
+ ? '已完成'
5061
+ : cachedTask?.source?.type === 'image' && cachedTask?.state === 'draft'
5062
+ ? '图片已暂存,待识别'
5063
+ : WORKFLOW_STATE_LABELS[cachedTask?.state] || cachedTask?.state || '待开始';
5064
+ const viewedStageCompleted = viewAccess === 'read' || cachedTask?.state === 'completed';
5065
+ const taskTimestamp = cachedTask?.updatedAt || cachedTask?.createdAt;
5066
+ const taskTimeLabel = taskTimestamp && Number.isFinite(new Date(taskTimestamp).getTime())
5067
+ ? new Date(taskTimestamp).toLocaleString('zh-CN', {
5068
+ year: 'numeric', month: 'numeric', day: 'numeric',
5069
+ hour: '2-digit', minute: '2-digit', hour12: false,
5070
+ })
5071
+ : '';
5072
+ const taskFileName = cachedTask?.source?.fileName || '';
5073
+ const taskSourceLabel = taskFileName
5074
+ || (cachedTask?.source?.type === 'image'
5075
+ ? '图片名单'
5076
+ : cachedTask?.source
5077
+ ? '文本粘贴'
5078
+ : '');
5079
+ const taskRowCount = cachedTask?.source?.rowCount ?? dataset?.rowCount ?? 0;
5080
+ const taskFieldCount = cachedTask?.fieldSelection?.length ?? fieldSelection.length;
5081
+ const hasTaskResultSummary = Boolean(qccRun || cachedTask?.enrichmentSummary || cachedTask?.matchSummary);
5082
+ const taskCompletedCount = Number.isFinite(qccEnrichedCount) ? qccEnrichedCount : 0;
5083
+ const taskReviewCount = Number.isFinite(qccReviewCount) ? qccReviewCount : 0;
5084
+ const taskFailedCount = Number.isFinite(qccFailedCount) ? qccFailedCount : 0;
5085
+ const taskCompletedMetric = `${taskCompletedCount}/${taskRowCount || taskCompletedCount} 条`;
5086
+ const taskReviewMetric = `${taskReviewCount} 条`;
5087
+ const taskFailedMetric = `${taskFailedCount} 条`;
4375
5088
  const stat = (label, value, tone) => h('div', { className: 'dcAgentCard' },
4376
5089
  h('span', null, label),
4377
5090
  h('b', { className: tone ? `is-${tone}` : null }, displayStatValue(value))
@@ -4408,8 +5121,13 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
4408
5121
  h('button', {
4409
5122
  type: 'button',
4410
5123
  className: 'dcAgentButton is-primary',
4411
- onClick: startNewWorkflow,
5124
+ onClick: () => startNewWorkflow(false),
4412
5125
  }, '新建清洗任务'),
5126
+ cachedTask ? h('button', {
5127
+ type: 'button',
5128
+ className: 'dcAgentButton',
5129
+ onClick: () => startNewWorkflow(true),
5130
+ }, '新建任务并复用配置') : null,
4413
5131
  ),
4414
5132
  historyTask ? h('section', { className: 'dcAgentSection', 'aria-label': '历史任务只读详情' },
4415
5133
  h('h3', null, workflowDisplayName(historyTask)),
@@ -4445,8 +5163,48 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
4445
5163
  ),
4446
5164
  );
4447
5165
  } else if (step === 'rules') {
4448
- pane = h('div', { className: 'dcAgentPane' },
5166
+ if (!canAction('edit-rules') && !canAction('confirm-rules')) {
5167
+ pane = h('div', { className: 'dcAgentPane' },
5168
+ h('p', { className: 'dcAgentHint' }, isCurrentView
5169
+ ? '规则已经确认,当前只等待生成质量体检。'
5170
+ : '这是已确认规则的只读摘要。'),
5171
+ h('section', { className: 'dcAgentSection' },
5172
+ h('h3', null, taskTitle),
5173
+ h('div', { className: 'dcAgentChips' },
5174
+ mappings.map((mapping) => h('span', { key: mapping.sourceField, className: 'dcAgentChip' },
5175
+ `${mapping.sourceField} → ${resultFieldLabel(mapping.targetField)}`))),
5176
+ h('p', { className: 'dcAgentHint' }, `任务目标 ${objectives.length} 项 · 补全字段 ${fieldSelection.length} 项`),
5177
+ ),
5178
+ canAction('run-quality') ? h('button', {
5179
+ type: 'button', className: 'dcAgentButton is-primary', disabled: busy || !runtime.rows.length,
5180
+ onClick: runProfile,
5181
+ }, busy ? '体检中…' : '生成质量体检报告') : null,
5182
+ );
5183
+ } else pane = h('div', { className: 'dcAgentPane' },
4449
5184
  h('p', { className: 'dcAgentHint' }, '确认任务名称、字段映射、匹配规则和输出字段。保存草稿不会调用企查查;确认规则后才可进入质量体检。'),
5185
+ canAction('import-data') ? h('section', { className: 'dcAgentSection' },
5186
+ h('div', { className: 'dcAgentRow' },
5187
+ h('h3', null, '当前来源'),
5188
+ !intakeEditorOpen ? h('button', { type: 'button', className: 'dcAgentButton', disabled: busy,
5189
+ onClick: () => { actions.setError(null); setIntakeEditorOpen(true); } }, '重新导入') : null,
5190
+ ),
5191
+ h('p', { className: 'dcAgentHint' }, `${intake.fileName} · ${dataset?.rowCount || 0} 行`),
5192
+ showIntakeEditor ? h('div', { className: 'dcAgentIntakeEditor' },
5193
+ h('div', { className: 'dcAgentIntakeSourceActions' },
5194
+ h('button', { type: 'button', className: 'dcAgentButton', disabled: busy,
5195
+ onClick: (event) => event.currentTarget.nextElementSibling?.click() }, '选择文件(自动解析)'),
5196
+ h('input', { type: 'file', accept: '.csv,.json,.xlsx,.xls,image/png,image/jpeg,image/webp',
5197
+ style: { display: 'none' }, disabled: busy, 'aria-label': '选择数据文件', onChange: handleFile }),
5198
+ h('button', { type: 'button', className: 'dcAgentButton', disabled: busy,
5199
+ onClick: () => openImageIntake() }, '图片识别(粘贴 / 选择)'),
5200
+ ),
5201
+ h('textarea', { className: 'dcAgentTextarea', value: input, disabled: busy,
5202
+ 'aria-label': '粘贴数据', placeholder: '粘贴 CSV、JSON 或逐行企业名单',
5203
+ onInput: (event) => { actions.setInput(event.target.value); actions.setError(null); } }),
5204
+ h('button', { type: 'button', className: 'dcAgentButton', disabled: busy || !intake.pendingText,
5205
+ 'aria-label': '解析数据', onClick: handleParse }, busy ? '解析中…' : '解析粘贴内容'),
5206
+ ) : null,
5207
+ ) : null,
4450
5208
  h('label', { className: 'dcAgentFormField' },
4451
5209
  h('span', null, '任务名称'),
4452
5210
  h('input', { className: 'dcAgentField', value: taskTitle, maxLength: 120, onChange: (event) => actions.setTaskTitle(event.target.value) }),
@@ -4571,12 +5329,12 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
4571
5329
  type: 'button',
4572
5330
  className: 'dcAgentButton is-primary',
4573
5331
  disabled: busy,
4574
- onClick: () => actions.setStep(requiresQcc ? 'match' : 'enrich'),
5332
+ onClick: () => navigate(requiresQcc ? 'match' : 'enrich'),
4575
5333
  }, requiresQcc ? '下一步:匹配核验' : '下一步:本地清洗补全'),
4576
5334
  ),
4577
- ) : h('div', { className: 'dcAgentProfileEmpty' },
5335
+ ) : canAction('run-quality') ? h('div', { className: 'dcAgentProfileEmpty' },
4578
5336
  h('button', { type: 'button', className: 'dcAgentButton is-primary', disabled: busy || !hasData, 'aria-label': '生成体检报告', onClick: runProfile }, busy ? '体检中…' : '生成体检报告'),
4579
- ),
5337
+ ) : h('p', { className: 'dcAgentHint' }, '当前没有可恢复的质量体检明细。'),
4580
5338
  ),
4581
5339
  );
4582
5340
  } else if (step === 'match') {
@@ -4599,19 +5357,20 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
4599
5357
  h('section', { className: 'dcAgentMatchBlock', 'aria-label': '准备任务说明' },
4600
5358
  h('div', { className: 'dcAgentMatchBlockHead' },
4601
5359
  h('h4', null, '准备任务说明'),
4602
- h('span', { className: 'dcAgentMatchScope' }, h('b', null, dataset?.rowCount ?? runtime.rows.length), '条记录'),
5360
+ h('span', { className: 'dcAgentMatchScope' }, h('b', null,
5361
+ dataset?.rowCount ?? cachedTask?.source?.rowCount ?? runtime.rows.length), '条记录'),
4603
5362
  ),
4604
5363
  h('p', { className: 'dcAgentHint' }, `已选择 ${fieldSelection.length} 个补全字段。Host 自动检查所需工具并计算去重主体与调用上界。`),
4605
5364
  h('p', { className: 'dcAgentHint' }, '回填后可修改;点击发送才开始查询,使用你已连接的企查查 MCP 账号及额度。'),
4606
5365
  ),
4607
5366
  ),
4608
- h('div', { className: 'dcAgentMatchActions' },
5367
+ canAction('prepare-qcc-command') ? h('div', { className: 'dcAgentMatchActions' },
4609
5368
  h('button', { type: 'button', className: 'dcAgentButton is-primary', disabled: busy || !hasData || !cachedTask?.id, onClick: runQcc }, busy ? '检查并生成中…' : '生成可编辑任务说明'),
4610
- ),
5369
+ ) : null,
4611
5370
  ),
4612
5371
  qccRun ? h('section', { className: 'dcAgentSection' },
4613
5372
  h('h3', null, `任务 ${qccRun.runId} · ${qccRun.state}`),
4614
- ((qccRun.reviewQueue || []).length || (qccRun.errors || []).some((item) => item.error?.retryable)) ? h('label', { className: 'dcAgentCheck' },
5373
+ canAction('resolve-candidate') && (qccRun.reviewQueue || []).length ? h('label', { className: 'dcAgentCheck' },
4615
5374
  h('input', { type: 'checkbox', checked: paidConfirmed, 'aria-label': '确认候选或重试的企查查调用', onChange: (event) => actions.setPaidConfirmed(event.target.checked) }),
4616
5375
  '确认主体或重试会继续查询,使用当前连接的企查查账号;额度或费用由该账号自行承担。',
4617
5376
  ) : null,
@@ -4631,10 +5390,9 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
4631
5390
  item.candidates.map((candidate) => h('div', { key: candidate.creditNo, className: 'dcAgentCandidate' },
4632
5391
  h('div', null, h('b', null, candidate.companyName || '未命名候选'), h('small', null, candidate.creditNo)),
4633
5392
  h('small', null, `${candidate.status || '状态未知'} · ${(candidate.legalRep || []).join('、') || '法人未知'}`),
4634
- h('button', { type: 'button', className: 'dcAgentButton', disabled: busy || !paidConfirmed, onClick: () => resolveCandidate(item, candidate) }, '确认此主体'),
5393
+ canAction('resolve-candidate') ? h('button', { type: 'button', className: 'dcAgentButton', disabled: busy || !paidConfirmed, onClick: () => resolveCandidate(item, candidate) }, '确认此主体') : null,
4635
5394
  )),
4636
5395
  )),
4637
- (qccRun.errors || []).some((item) => item.error?.retryable) ? h('button', { type: 'button', className: 'dcAgentButton', disabled: busy || !paidConfirmed, onClick: retryFailures }, '重试可恢复失败项') : null,
4638
5396
  ) : null,
4639
5397
  );
4640
5398
  } else if (step === 'enrich') {
@@ -4646,10 +5404,10 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
4646
5404
  ),
4647
5405
  h('section', { className: 'dcAgentSection' },
4648
5406
  h('h3', null, '本地清洗预处理'),
4649
- h('div', { className: 'dcAgentRow' },
4650
- h('button', { type: 'button', className: 'dcAgentButton', disabled: busy || !hasData, 'aria-label': '执行清洗', onClick: runClean }, busy ? '处理中…' : '执行确定性清洗'),
4651
- h('button', { type: 'button', className: 'dcAgentButton', disabled: busy || !hasData, 'aria-label': '执行补全', onClick: runComplete }, busy ? '处理中…' : '本地规则补全'),
4652
- ),
5407
+ canAction('run-local-clean') || canAction('run-local-complete') ? h('div', { className: 'dcAgentRow' },
5408
+ canAction('run-local-clean') ? h('button', { type: 'button', className: 'dcAgentButton', disabled: busy || !hasData, 'aria-label': '执行清洗', onClick: runClean }, busy ? '处理中…' : '执行确定性清洗') : null,
5409
+ canAction('run-local-complete') ? h('button', { type: 'button', className: 'dcAgentButton', disabled: busy || !hasData, 'aria-label': '执行补全', onClick: runComplete }, busy ? '处理中…' : '本地规则补全') : null,
5410
+ ) : null,
4653
5411
  clean ? h('div', { className: 'dcAgentGrid' },
4654
5412
  stat('总数', clean.total), stat('保留', clean.kept, 'good'), stat('剔除', clean.dropped, 'bad'),
4655
5413
  stat('缺失关键字段', clean.badMissing, clean.badMissing > 0 ? 'bad' : null),
@@ -4663,13 +5421,28 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
4663
5421
  stat('QCC 已补全', qccEnrichedCount, qccEnrichedCount > 0 ? 'good' : null),
4664
5422
  stat('待核验', qccReviewCount, qccReviewCount > 0 ? 'warn' : null),
4665
5423
  ),
4666
- h('div', { className: 'dcAgentRow' }, h('button', { type: 'button', className: 'dcAgentButton is-primary', onClick: () => actions.setStep('download') }, '进入下载数据')),
5424
+ canAction('create-local-artifacts') ? h('div', { className: 'dcAgentRow' }, h('button', {
5425
+ type: 'button', className: 'dcAgentButton is-primary', disabled: busy || !exportRows()?.length,
5426
+ onClick: () => createAndDownload('complete', 'xlsx'),
5427
+ }, '生成结果')) : null,
4667
5428
  );
4668
5429
  } else if (step === 'download') {
4669
- const availableArtifacts = (cachedTask?.artifacts || []).filter(artifact => artifact.format === 'xlsx' && (artifact.kind !== 'review' || artifact.rowCount > 0));
5430
+ const artifactsByKind = new Map();
5431
+ for (const artifact of cachedTask?.artifacts || []) {
5432
+ if (artifact.format === 'xlsx' && (artifact.kind !== 'review' || artifact.rowCount > 0)) {
5433
+ artifactsByKind.set(artifact.kind, artifact);
5434
+ }
5435
+ }
5436
+ const availableArtifacts = [...artifactsByKind.values()];
4670
5437
  const resultArtifacts = availableArtifacts.filter(artifact => artifact.kind !== 'report');
4671
5438
  const reportArtifact = availableArtifacts.find(artifact => artifact.kind === 'report');
4672
- const canCreateArtifacts = Boolean(exportRows()?.length) && ['rules_confirmed', 'diagnosed', 'export_ready', 'partial'].includes(cachedTask?.state);
5439
+ const canCreateArtifacts = Boolean(exportRows()?.length) && canAction('create-local-artifacts');
5440
+ const retryableErrors = (qccRun?.errors || []).filter((item) => item.error?.retryable);
5441
+ const canRetryFailures = canAction('retry-failed')
5442
+ && cachedTask?.runtimeCapabilities?.liveRunAvailable
5443
+ && retryableErrors.length > 0;
5444
+ const canRetryDelivery = canAction('retry-delivery')
5445
+ && cachedTask?.runtimeCapabilities?.liveRunAvailable;
4673
5446
  pane = h('div', { className: 'dcAgentPane' },
4674
5447
  h('p', { className: 'dcAgentHint' }, '结果文件由 Host 保存;有失败、未匹配或待核验记录时展示异常清单。任务或插件重启后,可从任务历史继续下载;无需再次调用企查查。'),
4675
5448
  h('div', { className: 'dcAgentResultSummary' }, h('div', { className: 'dcAgentGrid' },
@@ -4706,10 +5479,21 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
4706
5479
  ),
4707
5480
  !canCreateArtifacts ? h('p', { className: 'dcAgentHint' }, '请先完成规则确认与清洗补全;历史任务若尚未生成制品,需要重新上传源文件。') : null,
4708
5481
  ),
5482
+ canRetryFailures ? h('section', { className: 'dcAgentSection' },
5483
+ h('label', { className: 'dcAgentCheck' },
5484
+ h('input', { type: 'checkbox', checked: paidConfirmed, 'aria-label': '确认重试的企查查调用', onChange: (event) => actions.setPaidConfirmed(event.target.checked) }),
5485
+ '重试会继续查询,使用当前连接的企查查账号;额度或费用由该账号自行承担。',
5486
+ ),
5487
+ h('button', { type: 'button', className: 'dcAgentButton', disabled: busy || !paidConfirmed, onClick: retryFailures },
5488
+ `重试可恢复失败项 · ${retryableErrors.length}`),
5489
+ ) : null,
5490
+ canRetryDelivery ? h('button', {
5491
+ type: 'button', className: 'dcAgentButton', disabled: busy, onClick: retryDelivery,
5492
+ }, '重新生成结果文件') : null,
4709
5493
  );
4710
5494
  } else {
4711
5495
  pane = h('div', { className: 'dcAgentPane' },
4712
- dataset ? h('section', { className: 'dcAgentSection dcAgentIntakeSummary', 'aria-label': '当前导入数据', role: 'status' },
5496
+ dataset || cachedTask?.source ? h('section', { className: 'dcAgentSection dcAgentIntakeSummary', 'aria-label': '当前导入数据', role: 'status' },
4713
5497
  h('div', { className: 'dcAgentIntakeSummaryHead' },
4714
5498
  h('div', null,
4715
5499
  h('h3', null, intake.available ? `已解析 ${intake.count} 条数据` : '已保存导入摘要'),
@@ -4717,8 +5501,12 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
4717
5501
  ),
4718
5502
  ),
4719
5503
  h('p', { className: 'dcAgentHint' }, intake.available
4720
- ? '数据已导入。请核对下方完整名单;需要替换来源时点击“重新导入”。'
4721
- : '当前页面没有完整原始数据。请重新导入原文件;历史下载制品仍可从任务历史获取。'),
5504
+ ? canAction('import-data')
5505
+ ? '数据已导入。请核对下方完整名单;需要替换来源时点击“重新导入”。'
5506
+ : '这是已导入来源的只读摘要。更换名单请从任务历史新建任务并复用配置。'
5507
+ : canAction('import-data')
5508
+ ? '当前页面没有完整原始数据。请重新导入原文件;历史下载制品仍可从任务历史获取。'
5509
+ : '当前页面只保留 Host 来源摘要;已有制品可从结果下载或任务历史获取。'),
4722
5510
  ) : null,
4723
5511
  showIntakeEditor ? h('section', { id: 'dc-agent-intake-editor', className: 'dcAgentSection dcAgentIntakeEditor', 'aria-label': dataset ? '重新导入数据' : '导入数据' },
4724
5512
  h('header', { className: 'dcAgentIntakeEditorHeader' },
@@ -4776,16 +5564,47 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
4776
5564
  }) : null,
4777
5565
  hasData ? h('p', { className: 'dcAgentHint' }, '请核对完整名单。识别清单不代表已完成企查查匹配。核对后进入「规则与体检」,确认规则生成质量体检;再进入「主体匹配」直接生成可编辑任务说明。Host 自动检查工具和调用范围,回填中央对话框后点击发送才开始查询。') : null,
4778
5566
  h('div', { className: 'dcAgentUploadActions' },
4779
- hasData && !intakeEditorOpen ? h('button', {
5567
+ hasData && !intakeEditorOpen && canAction('import-data') ? h('button', {
4780
5568
  type: 'button', className: 'dcAgentButton', disabled: busy,
4781
5569
  'aria-expanded': false, 'aria-controls': 'dc-agent-intake-editor',
4782
5570
  onClick: () => { actions.setError(null); setIntakeEditorOpen(true); },
4783
5571
  }, '重新导入') : null,
4784
- hasData ? h('button', { type: 'button', className: 'dcAgentButton is-primary', disabled: busy || !intake.available || intake.pendingText, onClick: () => { actions.setError(null); actions.setStep('rules'); } }, '已核对清单,下一步:字段映射与规则') : null,
5572
+ hasData && (canAction('import-data') || reviewingImportedDataset) ? h('button', {
5573
+ type: 'button', className: 'dcAgentButton is-primary',
5574
+ disabled: busy || !intake.available || intake.pendingText,
5575
+ onClick: () => navigate('rules'),
5576
+ }, reviewingImportedDataset ? '确认名单,进入规则与体检' : '已核对清单,下一步:字段映射与规则') : null,
4785
5577
  ),
4786
5578
  );
4787
5579
  }
4788
5580
 
5581
+ const runtimeRecoveryPane = runtimeRecoveryAllowed ? h('div', { className: 'dcAgentPane' },
5582
+ h('section', { className: 'dcAgentSection', role: 'status' },
5583
+ h('h3', null, '重新载入原任务数据'),
5584
+ h('p', { className: 'dcAgentHint' }, cachedTask.source?.checksum
5585
+ ? '原始名单未持久化。请选择或粘贴同一份数据;Host 校验一致后只恢复本页运行时数据,不改变任务状态。'
5586
+ : '该旧任务缺少同源校验信息,不能恢复写操作。'),
5587
+ cachedTask.source?.checksum ? h('div', { className: 'dcAgentRow' },
5588
+ h('button', { type: 'button', className: 'dcAgentButton', disabled: busy,
5589
+ onClick: (event) => event.currentTarget.nextElementSibling?.click() }, '选择原文件'),
5590
+ h('input', { type: 'file', accept: '.csv,.json,.xlsx,.xls', style: { display: 'none' },
5591
+ disabled: busy, 'aria-label': '重新载入原任务文件', onChange: recoverRuntimeFile }),
5592
+ ) : null,
5593
+ cachedTask.source?.checksum ? h('textarea', {
5594
+ className: 'dcAgentTextarea', value: input, disabled: busy,
5595
+ placeholder: '也可以粘贴原 CSV、JSON 或逐行企业名单',
5596
+ 'aria-label': '重新载入原任务文本',
5597
+ onInput: (event) => { actions.setInput(event.target.value); actions.setError(null); },
5598
+ }) : null,
5599
+ cachedTask.source?.checksum ? h('button', {
5600
+ type: 'button', className: 'dcAgentButton', disabled: busy || !String(input || '').trim(),
5601
+ onClick: recoverRuntimeText,
5602
+ }, '校验并恢复') : null,
5603
+ h('button', { type: 'button', className: 'dcAgentButton', onClick: () => startNewWorkflow(true) },
5604
+ '新建任务并复用配置'),
5605
+ ),
5606
+ ) : null;
5607
+
4789
5608
  return h('section', {
4790
5609
  className: 'dcAgentWorkbenchContent',
4791
5610
  'data-dsh-plugin': 'data-cleaning-agent',
@@ -4794,54 +5613,75 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
4794
5613
  },
4795
5614
  h('nav', { className: 'dcAgentManagement', 'aria-label': '工作台管理' },
4796
5615
  h('button', { type: 'button', className: 'dcAgentButton', 'aria-pressed': step !== 'history',
4797
- onClick: () => actions.setStep(cachedTask?.stage || 'upload') }, '当前任务'),
5616
+ onClick: () => navigate(cachedTask?.flow?.currentStage || cachedTask?.stage || 'upload') }, '当前任务'),
4798
5617
  h('button', { type: 'button', className: 'dcAgentButton', 'aria-pressed': step === 'history',
4799
- onClick: () => actions.setStep('history') }, '任务历史'),
5618
+ onClick: () => navigate('history') }, '任务历史'),
4800
5619
  ),
4801
5620
  step !== 'history' ? h('nav', { className: 'dcAgentStepper', 'aria-label': '清洗流程',
4802
5621
  style: { '--dc-stage-count': STEPS.length } },
4803
- STEPS.map((st) => h('button', {
4804
- key: st.key,
4805
- type: 'button',
4806
- className: `dcAgentStep${(step === 'profile' ? 'rules' : step) === st.key ? ' is-active' : ''}`,
4807
- 'aria-label': st.label,
4808
- title: st.label,
4809
- 'aria-current': (step === 'profile' ? 'rules' : step) === st.key ? 'step' : undefined,
4810
- onClick: () => actions.setStep(st.key),
4811
- }, h('span', { className: 'dcAgentStepIcon' }, h(StageIcon, { kind: st.icon })),
4812
- h('span', { className: 'dcAgentStepLabel' }, st.label))),
5622
+ STEPS.map((st) => {
5623
+ const access = cachedTask
5624
+ ? resolveStageAccess(st.key, cachedTask.flow)
5625
+ : st.key === 'upload' ? 'current' : 'locked';
5626
+ const locked = access === 'locked';
5627
+ const lockHint = locked
5628
+ ? `尚未开放,请先完成“${currentStageLabel}”`
5629
+ : st.label;
5630
+ return h('button', {
5631
+ key: st.key,
5632
+ type: 'button',
5633
+ className: `dcAgentStep${(step === 'profile' ? 'rules' : step) === st.key ? ' is-active' : ''}`,
5634
+ 'aria-label': st.label,
5635
+ title: lockHint,
5636
+ 'aria-current': (step === 'profile' ? 'rules' : step) === st.key ? 'step' : undefined,
5637
+ 'aria-disabled': locked ? 'true' : undefined,
5638
+ 'aria-description': locked ? lockHint : undefined,
5639
+ 'data-access': access,
5640
+ onClick: () => navigate(st.key),
5641
+ }, h('span', { className: 'dcAgentStepIcon' }, h(StageIcon, { kind: st.icon })),
5642
+ h('span', { className: 'dcAgentStepLabel' }, st.label));
5643
+ }),
4813
5644
  ) : null,
4814
5645
  h('div', { className: 'dcAgentWbBody' },
4815
- cachedTask && step !== 'history' ? h('section', { className: 'dcAgentSection', 'aria-label': '当前任务进展', 'aria-live': 'polite' },
4816
- h('b', null, cachedTask.source?.type === 'image' && cachedTask.state === 'draft' ? '图片已暂存,待识别' : `任务记录:${WORKFLOW_STATE_LABELS[cachedTask.state] || '状态未知'}`),
4817
- h('h3', null, workflowDisplayName(cachedTask)),
4818
- ['matching', 'enriching'].includes(cachedTask.state) ? h('p', { className: 'dcAgentHint' }, '以上为最近同步的任务记录,不代表模型仍在运行。若会话已停止,请查看原会话的确认或错误提示;不要重复启动。') : null,
4819
- h('details', null, h('summary', null, '任务详情'), h('p', { className: 'dcAgentHint' }, `任务编号:${cachedTask.id}`)),
4820
- h('p', null, `${cachedTask.source?.type === 'image' ? '图片识别' : cachedTask.source?.type === 'text' ? '粘贴名单' : cachedTask.source ? '上传表格' : '待选择数据来源'}${cachedTask.source?.fileName ? ' · ' + cachedTask.source.fileName : ''} · ${cachedTask.source?.type === 'image' && cachedTask.state === 'draft' ? '条目数待识别' : `${cachedTask.source?.rowCount || 0} 条`} · ${cachedTask.fieldSelection?.length || 0} 个补全字段`),
4821
- commandProgress?.taskId === cachedTask.id && Number.isFinite(commandProgress.totalUnique) ? h('p', null, `已处理主体 ${commandProgress.completedUnique}/${commandProgress.totalUnique}(去重后,含未匹配或失败,不代表补全成功)`) : null,
4822
- commandProgress?.taskId === cachedTask.id && Number.isFinite(commandProgress.totalUnique) && commandProgress.totalUnique > 0 ? h('progress', {
4823
- max: commandProgress.totalUnique, value: Math.min(commandProgress.totalUnique, Math.max(0, commandProgress.completedUnique || 0)),
4824
- 'aria-label': '主体处理进度', style: { width: '100%', accentColor: cachedTask.state === 'completed' ? '#16845b' : cachedTask.state === 'failed' ? '#c62828' : cachedTask.state === 'partial' ? '#a66a00' : '#087dcc' },
5646
+ step !== 'history' ? h('section', { className: 'dcAgentTaskContext', 'aria-label': '任务状态与流程说明' },
5647
+ cachedTask ? h(WorkflowTaskContext, {
5648
+ task: cachedTask,
5649
+ stageLabel: viewedStageLabel,
5650
+ viewAccess,
5651
+ currentStageLabel,
5652
+ statusLabel: taskStatusLabel,
5653
+ stageCompleted: viewedStageCompleted,
5654
+ timeLabel: taskTimeLabel,
5655
+ timeValue: taskTimestamp,
5656
+ sourceLabel: taskSourceLabel,
5657
+ rowCount: taskRowCount,
5658
+ fieldCount: taskFieldCount,
5659
+ hasResultSummary: hasTaskResultSummary,
5660
+ completedMetric: taskCompletedMetric,
5661
+ reviewMetric: taskReviewMetric,
5662
+ failedMetric: taskFailedMetric,
5663
+ commandProgress,
5664
+ onReturn: reviewingImportedDataset
5665
+ ? null
5666
+ : () => navigate(cachedTask?.flow?.currentStage || cachedTask?.stage || 'upload'),
4825
5667
  }) : null,
4826
- h('p', { style: { color: qccFailedCount > 0 ? '#c62828' : Number(qccReviewCount) > 0 ? '#a66a00' : qccEnrichedCount > 0 ? '#16845b' : 'inherit' } },
4827
- cachedTask.source?.type === 'image' && cachedTask.state === 'draft' ? '请发送对话中的任务说明开始图片识别,识别后核验企业名单。' : Number.isFinite(qccEnrichedCount) ? `已处理并补全 ${qccEnrichedCount} 条 · 待核验 ${Number.isFinite(qccReviewCount) ? qccReviewCount : '统计中'} 条 · 失败 ${qccFailedCount} 条(按输入行统计;未披露字段保留空白)` : '结果统计中,处理结束后展示补全与待核验数量'),
4828
- ) : null,
4829
- error ? h('div', { className: 'dcAgentError', role: 'alert' }, error) : null,
4830
- step !== 'history' ? h('details', { className: 'dcAgentSection' },
4831
- h('summary', null, '使用说明与任务流程'),
4832
- h('p', { className: 'dcAgentHint' }, '导入与核验 → 规则与体检 → 主体匹配 → 字段补全 → 结果下载。菜单只切换视图,不启动调用;按页面主按钮逐步确认。'),
4833
- h('p', { className: 'dcAgentHint' }, '支持文本、Excel、JSON 和图片名单。原值保留、多候选人工核验;企查查调用使用当前用户自己的连接与额度。'),
5668
+ h(WorkflowGuide),
4834
5669
  ) : null,
5670
+ error ? h('div', {
5671
+ className: error.startsWith('流程提示:') ? 'dcAgentNavigationNotice' : 'dcAgentError',
5672
+ role: error.startsWith('流程提示:') ? 'status' : 'alert',
5673
+ 'aria-live': 'polite',
5674
+ }, error) : null,
4835
5675
  ['rules', 'profile'].includes(step) ? h('nav', { className: 'dcAgentViewTabs', 'aria-label': '规则与体检视图' },
4836
- h('button', { type: 'button', className: 'dcAgentButton', 'aria-pressed': step === 'rules', onClick: () => actions.setStep('rules') }, '字段映射与规则'),
4837
- h('button', { type: 'button', className: 'dcAgentButton', 'aria-pressed': step === 'profile', onClick: () => actions.setStep('profile') }, '质量体检报告'),
5676
+ h('button', { type: 'button', className: 'dcAgentButton', 'aria-pressed': step === 'rules', onClick: () => navigate('rules') }, '字段映射与规则'),
5677
+ h('button', { type: 'button', className: 'dcAgentButton', 'aria-pressed': step === 'profile', onClick: () => navigate('profile') }, '质量体检报告'),
4838
5678
  ) : null,
4839
- workflowNavigationIssue(step, cachedTask, hasData, profile, qccRun, Boolean((clean || complete) && exportRows()?.length))
5679
+ runtimeRecoveryPane || (workflowNavigationIssue(step, cachedTask, hasData, profile, qccRun, Boolean((clean || complete) && exportRows()?.length))
4840
5680
  ? h('section', { className: 'dcAgentSection', role: 'status' },
4841
5681
  h('p', null, workflowNavigationIssue(step, cachedTask, hasData, profile, qccRun, Boolean((clean || complete) && exportRows()?.length))),
4842
5682
  h('button', { type: 'button', className: 'dcAgentButton',
4843
- onClick: () => actions.setStep(!hasData ? 'upload' : !cachedTask || ['draft', 'uploaded'].includes(cachedTask.state) ? 'rules' : cachedTask.state === 'rules_confirmed' ? 'profile' : step === 'download' ? 'enrich' : 'match') }, '前往前置步骤'))
4844
- : pane,
5683
+ onClick: () => navigate(cachedTask?.flow?.currentStage || cachedTask?.stage || 'upload') }, '返回当前节点'))
5684
+ : pane),
4845
5685
  ),
4846
5686
  );
4847
5687
  }
@@ -4940,6 +5780,7 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
4940
5780
  }
4941
5781
  let disposed = false;
4942
5782
  const instances = new Map();
5783
+ const revealedCommands = new Set();
4943
5784
  const spec = createWorkbenchStore();
4944
5785
  const instanceFor = sessionId => {
4945
5786
  if (!sessionId) throw new Error('请先打开数据清洗会话。');
@@ -4999,7 +5840,12 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
4999
5840
  react.useEffect(() => {
5000
5841
  instance.actions.openAt(null, props.scope.sessionId);
5001
5842
  return adapter.attach(props.scope, { tabId: props.tab.id, store: props.store,
5002
- navigate: destination => instance.actions.setStep(destination || instance.getSnapshot().step) });
5843
+ navigate: destination => navigateWorkflow(
5844
+ instance.actions,
5845
+ props.scope.sessionId,
5846
+ destination || instance.getSnapshot().step,
5847
+ instance.getSnapshot().workflowTask,
5848
+ ) });
5003
5849
  }, [instance, props.scope.sessionId, props.tab.id, props.store]);
5004
5850
  return h(WorkbenchContent, { useStore, actions: instance.actions, visible: props.visible,
5005
5851
  sendSessionCommand: (sessionId, prompt) => sendQccAgentCommand(ctx, sessionId, prompt),
@@ -5054,26 +5900,58 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
5054
5900
  sessions.open(task.originSessionId);
5055
5901
  // Navigation only: no task binding, command preparation, or draft writes.
5056
5902
  },
5903
+ revealSubmitted(sessionId, commandId, step) {
5904
+ if (disposed || !sessionId) return false;
5905
+ const key = commandId && `${sessionId}:${commandId}`;
5906
+ if (key && revealedCommands.has(key)) return false;
5907
+ // Consume the event even if its owner is no longer visible. Polling must
5908
+ // not pull open an old task when the user later returns to its Session.
5909
+ if (key) revealedCommands.add(key);
5910
+ const sessions = typeof ctx.get === 'function' ? ctx.get('sessions') : ctx.sessions;
5911
+ if (sessions?.list?.getSnapshot?.()?.current !== sessionId || !isCleaningSession(sessionId)) return false;
5912
+ try { return controller.open(sessionId, step); }
5913
+ catch (error) {
5914
+ instanceFor(sessionId).actions.setError(error.message || String(error));
5915
+ return false;
5916
+ }
5917
+ },
5057
5918
  storeFor: instanceFor,
5058
5919
  actionsFor: sessionId => instanceFor(sessionId).actions,
5059
5920
  open(sessionId, step) {
5060
5921
  if (disposed || !sessionId) return false;
5061
5922
  const instance = instanceFor(sessionId);
5062
- instance.actions.openAt(step, sessionId);
5923
+ const destination = workflowDestination(
5924
+ step,
5925
+ workflowTaskBySession.get(String(sessionId)) ?? instance.getSnapshot().workflowTask,
5926
+ instance.getSnapshot().step,
5927
+ );
5928
+ instance.actions.openAt(destination.stage, sessionId);
5063
5929
  const sessions = typeof ctx.get === 'function' ? ctx.get('sessions') : ctx.sessions;
5064
5930
  const summary = sessions?.list?.getSnapshot?.()?.byId?.[sessionId];
5065
5931
  openingSession = sessionId;
5066
- try { return adapter.open({ sessionId, ...(summary?.cwd ? { cwd: summary.cwd } : {}) },
5067
- step || instance.getSnapshot().step); }
5932
+ let opened;
5933
+ try { opened = adapter.open({ sessionId, ...(summary?.cwd ? { cwd: summary.cwd } : {}) },
5934
+ destination.stage); }
5068
5935
  finally { openingSession = null; }
5936
+ if (destination.compatibility) {
5937
+ instance.actions.setError('页面与 Host 流程版本不一致,当前任务已切换为只读模式;请完整重启并升级 Host。');
5938
+ } else if (destination.access === 'locked') {
5939
+ instance.actions.setError(lockedWorkflowNotice(
5940
+ step,
5941
+ workflowTaskBySession.get(String(sessionId)) ?? instance.getSnapshot().workflowTask,
5942
+ destination.stage,
5943
+ ));
5944
+ }
5945
+ return opened;
5069
5946
  },
5070
5947
  };
5071
5948
  sessionWorkbenchController = controller;
5949
+ const stopSubmittedBridge = installSubmittedWorkbenchBridge(ctx, controller);
5072
5950
  const listeners = {
5073
5951
  [WORKBENCH_OPEN_EVENT]: event => {
5074
5952
  const detail = event.detail || {};
5075
5953
  const actions = controller.actionsFor(detail.sessionId);
5076
- if (detail.task?.id) cacheWorkflowTask(actions, detail.sessionId, detail.task);
5954
+ if (detail.task?.id) cacheWorkflowTask(actions, detail.sessionId, detail.task, 'restore');
5077
5955
  controller.open(detail.sessionId, detail.step);
5078
5956
  },
5079
5957
  [WORKBENCH_DATASET_EVENT]: event => {
@@ -5116,6 +5994,7 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
5116
5994
  const shared = { controller, references: 1, release() {
5117
5995
  if (--shared.references > 0) return;
5118
5996
  disposed = true;
5997
+ stopSubmittedBridge();
5119
5998
  commandWatchGeneration += 1;
5120
5999
  for (const remove of bindings) remove();
5121
6000
  host.removeEventListener?.('click', previewClick, true);
@@ -5124,6 +6003,7 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
5124
6003
  disconnectFixture();
5125
6004
  void providerFiber?.dispose();
5126
6005
  instances.clear();
6006
+ revealedCommands.clear();
5127
6007
  if (host[CONTROLLER_KEY] === shared) delete host[CONTROLLER_KEY];
5128
6008
  if (sessionWorkbenchController === controller) sessionWorkbenchController = null;
5129
6009
  } };
@@ -5218,10 +6098,17 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
5218
6098
  createWorkbenchStore,
5219
6099
  WorkbenchContent,
5220
6100
  installSessionWorkbench,
6101
+ sendQccAgentCommand,
6102
+ installSubmittedWorkbenchBridge,
5221
6103
  DatabaseLogo,
5222
6104
  visibleCatalogFields,
5223
6105
  installCapabilityMount,
5224
6106
  PromptDialog,
6107
+ normalizeViewStage,
6108
+ resolveStageAccess,
6109
+ workflowDestination,
6110
+ lockedWorkflowNotice,
6111
+ navigateWorkflow,
5225
6112
  workflowNavigationIssue,
5226
6113
  buildTaskPrompt,
5227
6114
  DatasetReview,
@@ -5235,6 +6122,7 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
5235
6122
  requestPromptImage,
5236
6123
  guessMappings,
5237
6124
  intakeState,
6125
+ restoreParsedRuntime,
5238
6126
  projectCompletionResult,
5239
6127
  mappingRecommendations,
5240
6128
  mappedOutputFields,
@@ -5255,6 +6143,7 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
5255
6143
  observeHomePhase,
5256
6144
  ensureWorkflowTask,
5257
6145
  ensureEditableWorkflowTask,
6146
+ refreshHostedTask,
5258
6147
  queueWorkflowOperation,
5259
6148
  taskBelongsToSession,
5260
6149
  workflowOriginLabel,