dsh-data-cleaning-agent 0.9.13 → 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;
1283
+ white-space: nowrap;
1284
+ }
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);
1161
1297
  white-space: nowrap;
1162
1298
  }
1163
- .dcAgentWbBody > .dcAgentSection[aria-label="当前任务进展"] > details {
1164
- grid-column: 3;
1165
- grid-row: 1;
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
  }
@@ -2138,7 +2477,7 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
2138
2477
  const isActive = () => generation === commandWatchGeneration;
2139
2478
  void waitForQccCommand(command.commandId, 1800, async (status) => {
2140
2479
  if (!status || status.state === 'prepared') return;
2141
- await refreshHostedTask(actions, sessionId, command.taskId, null, true).catch(() => {});
2480
+ await refreshHostedTask(actions, sessionId, command.taskId, null, 'update').catch(() => {});
2142
2481
  if (isActive() && workflowTaskBySession.get(String(sessionId))?.id === command.taskId) {
2143
2482
  sessionWorkbenchController?.revealSubmitted(sessionId, command.commandId,
2144
2483
  workflowTaskBySession.get(String(sessionId))?.stage || 'match');
@@ -2203,6 +2542,32 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
2203
2542
  draft.workflowTask = value ?? null;
2204
2543
  if (value) draft.workflowTasks = [value, ...(draft.workflowTasks || []).filter(task => task.id !== value.id)];
2205
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
+ },
2206
2571
  setWorkflowTasks: (draft, value) => {
2207
2572
  const cached = new Map((draft.workflowTasks || []).map(task => [task.id, task]));
2208
2573
  draft.workflowTasks = (Array.isArray(value) ? value : []).map(task =>
@@ -2524,8 +2889,11 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
2524
2889
  }
2525
2890
 
2526
2891
  function openWorkbench(actions, step, sessionId) {
2527
- actions.openAt(step, sessionId);
2528
- 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
+ }
2529
2897
  }
2530
2898
 
2531
2899
  const WORKBENCH_OPEN_EVENT = 'dsh:data-cleaning-workbench-open';
@@ -2677,8 +3045,68 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
2677
3045
  authorization_required: '待连接企查查', cancelled: '已取消', parse_failed: '解析失败',
2678
3046
  };
2679
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
+
2680
3103
  function workflowNavigationIssue(step, task, hasData, profile, qccRun, hasLocalResult = false) {
2681
- 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;
2682
3110
  if (step === 'download' && task?.artifacts?.length) return null;
2683
3111
  if (!hasData) return '请先在「导入与核验」载入并核对完整名单。';
2684
3112
  if (step === 'rules') return null;
@@ -3053,7 +3481,7 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
3053
3481
  if (command.taskId) {
3054
3482
  const actions = sessionWorkbenchController?.actionsFor(sessionId);
3055
3483
  if (actions) {
3056
- await refreshHostedTask(actions, sessionId, command.taskId, null, true);
3484
+ await refreshHostedTask(actions, sessionId, command.taskId, null, 'update');
3057
3485
  if (command.state === 'running') requestWorkbenchOpen(command.qccCommand ? 'match' : 'upload', sessionId);
3058
3486
  if (command.qccCommand) {
3059
3487
  const status = await requestJson(`/data-cleaning/api/g5/commands/${encodeURIComponent(command.qccCommand.commandId)}`);
@@ -3243,21 +3671,26 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
3243
3671
  removeImage(sessionId, imageAttachment);
3244
3672
  setImageAttachment(null);
3245
3673
  }
3246
- if (mode === 'text') {
3247
- requestWorkbenchDataset(entriesToDataset(config.entries), sessionId, { type: 'text', fileName: '' });
3248
- } else if (mode === 'excel' && spreadsheetData) {
3249
- requestWorkbenchDataset(spreadsheetData, sessionId, { type: spreadsheetData.fmt || 'xlsx', fileName: spreadsheetFileName });
3250
- }
3251
- requestWorkbenchDraft({
3252
- title: `企业数据清洗补全任务 · ${config.entryCount || '待解析'} 条`,
3253
- objectives: cleaningKeys,
3254
- fieldSelection: enrichmentKeys,
3255
- matchRules,
3256
- ...(mode === 'excel' ? { mappings: spreadsheetMappings } : {}),
3257
- source: { type: mode === 'excel' ? 'xlsx' : mode, fileName: config.fileName || '', rowCount: config.entryCount || 0 },
3258
- }, sessionId);
3259
3674
  setBusy(true);
3260
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);
3261
3694
  let prompt = buildTaskPrompt(config);
3262
3695
  if (mode !== 'image' || ['prepared', 'completed'].includes(imageCommand?.state)) {
3263
3696
  const command = await requestPreparedWorkflow(sessionId, config);
@@ -3338,12 +3771,14 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
3338
3771
  const receiveNewTask = (event) => {
3339
3772
  if (event.detail?.sessionId !== sessionId || busy || imageCommand?.state === 'running') return;
3340
3773
  event.preventDefault();
3774
+ const reused = event.detail?.reuseConfig || null;
3341
3775
  discardCurrentImage();
3342
3776
  setMode('text'); setRawText(''); setEntries([]); setEntryCount(0); setSpreadsheetFileName('');
3343
3777
  setSpreadsheetData(null); setSpreadsheetMappings([]);
3344
- setCleaningKeys(DEFAULT_CLEANING_KEYS); setEnrichmentKeys(DEFAULT_ENRICHMENT_KEYS);
3778
+ setCleaningKeys(reused?.objectives || DEFAULT_CLEANING_KEYS);
3779
+ setEnrichmentKeys(reused?.fieldSelection || DEFAULT_ENRICHMENT_KEYS);
3345
3780
  setAnchorKeys(['company_name', 'credit_no']);
3346
- setPromptMatchRules({ normalizeNames: true, preferCreditNo: true, deduplicate: true, manualReviewAmbiguous: true });
3781
+ setPromptMatchRules(reused?.matchRules || { normalizeNames: true, preferCreditNo: true, deduplicate: true, manualReviewAmbiguous: true });
3347
3782
  setWizardStep(1); setOpen(true); setError(null);
3348
3783
  };
3349
3784
  const receiveImage = (event) => {
@@ -3681,7 +4116,7 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
3681
4116
  }
3682
4117
 
3683
4118
  /** 上传 pane 的处理:文件 → 文本/base64 → parse。 */
3684
- async function parseFile(file) {
4119
+ async function parseFile(file, previousChecksum) {
3685
4120
  const isXlsx = /\.(xlsx|xls)$/i.test(file && file.name ? file.name : '');
3686
4121
  let content;
3687
4122
  if (isXlsx) {
@@ -3693,7 +4128,7 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
3693
4128
  } else {
3694
4129
  content = await file.text();
3695
4130
  }
3696
- return api('/data-cleaning/api/mvp/parse', { filename: file.name, content });
4131
+ return api('/data-cleaning/api/mvp/parse', { filename: file.name, content, previousChecksum });
3697
4132
  }
3698
4133
 
3699
4134
  /** 文本/JSON 直通 parse(与 web.js 内联页同构,含 JSON 数组直通)。 */
@@ -3708,7 +4143,7 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
3708
4143
  };
3709
4144
  }
3710
4145
 
3711
- async function parseText(text, actions) {
4146
+ async function parseText(text, actions, previousChecksum) {
3712
4147
  const trimmed = (text ?? '').trim();
3713
4148
  if (!trimmed) {
3714
4149
  actions.setError('请粘贴或上传数据后再解析。');
@@ -3716,18 +4151,26 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
3716
4151
  }
3717
4152
  if (trimmed.startsWith('[')) {
3718
4153
  try {
3719
- const rows = JSON.parse(trimmed);
3720
- const list = Array.isArray(rows) ? rows : [];
3721
- const headers = list.length ? Object.keys(list[0]) : [];
3722
- 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
+ });
3723
4159
  } catch (error) {
3724
4160
  actions.setError(`JSON 解析失败:${error instanceof Error ? error.message : String(error)}`);
3725
4161
  }
3726
4162
  return;
3727
4163
  }
3728
4164
  const entityList = plainEntityListDataset(trimmed);
3729
- if (entityList) return entityList;
3730
- 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 });
3731
4174
  }
3732
4175
 
3733
4176
  /** 解析成功进入 taskId runtime;不把整表写入 Host store。 */
@@ -3740,6 +4183,7 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
3740
4183
  type: source.type || result.fmt || 'csv',
3741
4184
  fileName: source.fileName || '',
3742
4185
  sizeBytes: source.sizeBytes || 0,
4186
+ checksum: result.checksum || source.checksum || '',
3743
4187
  };
3744
4188
  const mappings = guessMappings(runtime.headers);
3745
4189
  const guessedNameField = mappings.find((mapping) => mapping.targetField === 'company_name')?.sourceField
@@ -3759,6 +4203,28 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
3759
4203
  return runtime;
3760
4204
  }
3761
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
+
3762
4228
  async function persistParsedWorkflow(actions, sessionId, result, source = {}, task) {
3763
4229
  const importedMappings = guessMappings(result.headers || []);
3764
4230
  const importedDraft = { mappings: importedMappings,
@@ -3772,10 +4238,11 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
3772
4238
  target.rows = Array.isArray(result.rows) ? result.rows : [];
3773
4239
  target.sourceRows = target.rows;
3774
4240
  target.headers = Array.isArray(result.headers) ? result.headers : [];
3775
- target.source = {
3776
- type: source.type || result.fmt || 'csv',
3777
- fileName: source.fileName || '',
3778
- 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 || '',
3779
4246
  };
3780
4247
  if (['draft', 'uploaded', 'parse_failed'].includes(current.state)) {
3781
4248
  current = await workflowAction(actions, sessionId, current, 'upload', {
@@ -3786,8 +4253,9 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
3786
4253
  columnCount: Array.isArray(result.headers) ? result.headers.length : 0,
3787
4254
  headers: Array.isArray(result.headers) ? result.headers : [],
3788
4255
  sizeBytes: source.sizeBytes || 0,
4256
+ checksum: result.checksum || source.checksum || '',
3789
4257
  },
3790
- });
4258
+ }, 'import');
3791
4259
  }
3792
4260
  if (current.state === 'uploaded') current = await updateWorkflowTask(actions, sessionId, current, importedDraft);
3793
4261
  return current;
@@ -3858,6 +4326,103 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
3858
4326
  );
3859
4327
  }
3860
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
+
3861
4426
  /** Session Tab 业务内容;尺寸、停靠、关闭均由 Better Sidebar 宿主管理。 */
3862
4427
  function WorkbenchContent(props) {
3863
4428
  const { useStore, actions } = props;
@@ -3910,13 +4475,19 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
3910
4475
  requestJson('/data-cleaning/api/workflow/contract').then((response) => {
3911
4476
  if (!disposed) actions.setWorkflowContract(response.contract);
3912
4477
  }).catch(() => {});
3913
- loadWorkflowTasks(actions);
4478
+ const tasksPromise = loadWorkflowTasks(actions);
3914
4479
  if (activeSessionId) {
3915
4480
  queueWorkflowOperation(activeSessionId, async () => {
3916
4481
  const key = String(activeSessionId);
4482
+ const tasks = await tasksPromise;
3917
4483
  const saved = ownedSessionId(key) ? readTaskBindings()[key] : null;
3918
- const taskId = workflowTaskBySession.get(key)?.id || (/^dcw-[a-zA-Z0-9-]+$/.test(saved || '') ? saved : null);
3919
- 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');
3920
4491
  }).catch((taskError) => {
3921
4492
  if (!disposed) actions.setError(`状态同步异常:${taskError instanceof Error ? taskError.message : String(taskError)}。保留最近任务记录,未重新执行查询。`);
3922
4493
  });
@@ -3931,11 +4502,10 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
3931
4502
  let disposed = false, timer;
3932
4503
  const poll = async () => {
3933
4504
  try {
3934
- const task = await refreshHostedTask(actions, activeSessionId, workflowTask.id, null, false);
4505
+ const task = await refreshHostedTask(actions, activeSessionId, workflowTask.id, null, 'update');
3935
4506
  if (disposed) return;
3936
4507
  if (!task) return;
3937
4508
  if (['matching', 'enriching'].includes(task.state)) timer = setTimeout(poll, 2000);
3938
- else if (step === 'match' || step === 'enrich') actions.setStep(task.stage || step);
3939
4509
  } catch (error) { if (!disposed) actions.setError(`状态同步异常:${error.message}。保留最近任务记录,未重新执行查询。`); }
3940
4510
  };
3941
4511
  timer = setTimeout(poll, 2000);
@@ -3952,7 +4522,29 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
3952
4522
  const stagedSource = runtimeFor(`session:${activeSessionId || 'unassigned'}`, false);
3953
4523
  const sourceRows = stagedSource?.sourceRows ?? runtime.sourceRows ?? runtime.rows;
3954
4524
  const intake = intakeState(dataset, sourceRows, input, runtime.source || stagedSource?.source || cachedTask?.source);
3955
- 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';
3956
4548
  const lastCsv = runtime.lastCsv;
3957
4549
  const fieldByPattern = (pattern) => runtime.headers.find((field) => pattern.test(field)) ?? null;
3958
4550
  const phoneField = fieldByPattern(/^(phone|mobile|tel|telephone|联系电话|手机号码|手机号)$/i);
@@ -3971,7 +4563,7 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
3971
4563
  };
3972
4564
 
3973
4565
  const handleParse = async () => {
3974
- if (busy) return;
4566
+ if (busy || !canAction('import-data')) return;
3975
4567
  if (!intake.pendingText) {
3976
4568
  if (intake.available) actions.setError(null);
3977
4569
  return;
@@ -3979,7 +4571,7 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
3979
4571
  actions.setBusy(true);
3980
4572
  actions.setError(null);
3981
4573
  try {
3982
- const result = await parseText(input, actions);
4574
+ const result = await parseText(input, actions, cachedTask?.source?.checksum);
3983
4575
  if (result && result.ok !== false) {
3984
4576
  applyParsed(result, actions, runtimeKey, { type: result.fmt || 'csv' });
3985
4577
  await persistParsedWorkflow(actions, activeSessionId, result, { type: result.fmt || 'csv' }, cachedTask);
@@ -4003,7 +4595,7 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
4003
4595
  return true;
4004
4596
  };
4005
4597
  const handleFile = async (event) => {
4006
- if (busy) return;
4598
+ if (busy || !canAction('import-data')) return;
4007
4599
  const file = event.target && event.target.files && event.target.files[0];
4008
4600
  if (!file) return;
4009
4601
  if (String(file.type ?? '').startsWith('image/')) {
@@ -4014,7 +4606,7 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
4014
4606
  actions.setBusy(true);
4015
4607
  actions.setError(null);
4016
4608
  try {
4017
- const result = await parseFile(file);
4609
+ const result = await parseFile(file, cachedTask?.source?.checksum);
4018
4610
  if (result && result.ok !== false) {
4019
4611
  applyParsed(result, actions, runtimeKey, { type: result.fmt || 'csv', fileName: file.name, sizeBytes: file.size || 0 });
4020
4612
  await persistParsedWorkflow(actions, activeSessionId, result, { type: result.fmt || 'csv', fileName: file.name, sizeBytes: file.size || 0 }, cachedTask);
@@ -4029,6 +4621,49 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
4029
4621
  }
4030
4622
  };
4031
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
+
4032
4667
  const performProfile = async (task) => {
4033
4668
  const r = await api('/data-cleaning/api/mvp/profile', {
4034
4669
  rows: runtime.rows,
@@ -4042,7 +4677,7 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
4042
4677
  };
4043
4678
 
4044
4679
  const runProfile = async () => {
4045
- if (busy || !runtime.rows.length) {
4680
+ if (busy || !canAction('run-quality') || !runtime.rows.length) {
4046
4681
  if (!runtime.rows.length) actions.setError('请先上传并解析数据。');
4047
4682
  return;
4048
4683
  }
@@ -4050,7 +4685,6 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
4050
4685
  actions.setError(null);
4051
4686
  try {
4052
4687
  await performProfile(cachedTask);
4053
- actions.setStep(requiresQcc ? 'match' : 'enrich');
4054
4688
  } catch (err) {
4055
4689
  actions.setError(err instanceof Error ? err.message : String(err));
4056
4690
  } finally {
@@ -4059,7 +4693,7 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
4059
4693
  };
4060
4694
 
4061
4695
  const runClean = async () => {
4062
- if (busy || !runtime.rows.length) {
4696
+ if (busy || !canAction('run-local-clean') || !runtime.rows.length) {
4063
4697
  if (!runtime.rows.length) actions.setError('请先上传并解析数据。');
4064
4698
  return;
4065
4699
  }
@@ -4087,7 +4721,7 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
4087
4721
  };
4088
4722
 
4089
4723
  const runComplete = async () => {
4090
- if (busy || !runtime.rows.length) {
4724
+ if (busy || !canAction('run-local-complete') || !runtime.rows.length) {
4091
4725
  if (!runtime.rows.length) actions.setError('请先上传并解析数据。');
4092
4726
  return;
4093
4727
  }
@@ -4120,48 +4754,9 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
4120
4754
  runtime.resultRows.qcc = Array.isArray(run.rows) ? run.rows : null;
4121
4755
  lastCsv.qcc = { csv: run.csv ?? '', name: run.downloadName ?? 'qcc-enriched.csv' };
4122
4756
  if (run.reviewCsv) lastCsv.review = { csv: run.reviewCsv, name: run.reviewDownloadName ?? 'qcc-review.csv' };
4123
- const summary = run.summary ?? {};
4124
- const reviewRequired = Number(summary.ambiguous ?? run.reviewQueue?.length ?? 0);
4125
4757
  const currentTask = workflowTaskBySession.get(String(activeSessionId || 'unassigned')) ?? cachedTask;
4126
- let current = currentTask;
4127
- if (currentTask?.qccRunId === run.runId && ['completed', 'partial', 'review_required', 'export_ready'].includes(currentTask.state)) {
4128
- actions.setStep(currentTask.stage || 'download');
4129
- return currentTask;
4130
- }
4131
- // partial 表示上一轮仍有失败项。重试成功后可直接重进 enrichment,
4132
- // 无需伪造一次新的 match 转换;若重试产生候选,再回到人工核验。
4133
- if (currentTask?.state !== 'partial' || reviewRequired > 0) {
4134
- current = await workflowAction(actions, activeSessionId, currentTask, 'match', {
4135
- qccRunId: run.runId,
4136
- summary: {
4137
- total: Number(summary.totalRows ?? runtime.rows.length),
4138
- exact: Number(summary.enriched ?? 0) + Number(summary.fieldReview ?? 0),
4139
- candidate: reviewRequired,
4140
- confirmed: 0,
4141
- unresolved: Number(summary.unresolved ?? summary.missingName ?? 0),
4142
- failed: Number(summary.failed ?? 0),
4143
- reviewRequired,
4144
- },
4145
- });
4146
- }
4147
- if (reviewRequired > 0) {
4148
- actions.setStep('match');
4149
- return current;
4150
- }
4151
- current = await workflowAction(actions, activeSessionId, current, 'enrich-start', { fieldSelection });
4152
- current = await workflowAction(actions, activeSessionId, current, 'enrichment', {
4153
- qccRunId: run.runId,
4154
- summary: {
4155
- total: Number(summary.totalRows ?? runtime.rows.length),
4156
- completed: Number(summary.enriched ?? 0),
4157
- unchanged: Number(summary.unresolved ?? 0) + Number(summary.missingName ?? 0),
4158
- failed: Number(summary.failed ?? 0),
4159
- reviewRequired: Number(summary.fieldReview ?? 0),
4160
- callsUsed: Array.isArray(run.audit) ? run.audit.length : 0,
4161
- },
4162
- });
4163
- actions.setStep(current.stage === 'download' ? 'download' : 'enrich');
4164
- return current;
4758
+ if (!currentTask?.id) return null;
4759
+ return refreshHostedTask(actions, activeSessionId, currentTask.id, run, 'update');
4165
4760
  };
4166
4761
 
4167
4762
  const executePreparedQccCommand = async (payload) => {
@@ -4201,7 +4796,7 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
4201
4796
  };
4202
4797
 
4203
4798
  const runQcc = async () => {
4204
- if (busy || !cachedTask?.id || !hasData) return;
4799
+ if (busy || !canAction('prepare-qcc-command') || !cachedTask?.id || !hasData) return;
4205
4800
  actions.setBusy(true);
4206
4801
  actions.setError(null);
4207
4802
  try {
@@ -4223,7 +4818,7 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
4223
4818
  };
4224
4819
 
4225
4820
  const resolveCandidate = async (item, candidate) => {
4226
- if (busy || !paidConfirmed || !qccRun) return;
4821
+ if (busy || !canAction('resolve-candidate') || !paidConfirmed || !qccRun) return;
4227
4822
  actions.setBusy(true);
4228
4823
  actions.setError(null);
4229
4824
  try {
@@ -4244,7 +4839,7 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
4244
4839
  };
4245
4840
 
4246
4841
  const retryFailures = async () => {
4247
- if (busy || !paidConfirmed || !qccRun) return;
4842
+ if (busy || !canAction('retry-failed') || !paidConfirmed || !qccRun) return;
4248
4843
  const names = [...new Set((qccRun.errors || []).filter((item) => item.error && item.error.retryable).map((item) => item.companyName))];
4249
4844
  if (!names.length) return;
4250
4845
  actions.setBusy(true);
@@ -4265,7 +4860,21 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
4265
4860
  }
4266
4861
  };
4267
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
+
4268
4876
  const saveTaskSettings = async () => {
4877
+ if (!canAction('edit-rules')) return;
4269
4878
  actions.setBusy(true);
4270
4879
  actions.setError(null);
4271
4880
  try {
@@ -4283,6 +4892,7 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
4283
4892
  };
4284
4893
 
4285
4894
  const confirmRuleSettings = async () => {
4895
+ if (!canAction('confirm-rules')) return;
4286
4896
  if (!hasData) { actions.setError('请先上传并解析数据。'); return; }
4287
4897
  const anchorMappings = mappings.filter((mapping) => ['company_name', 'credit_no', 'reg_no'].includes(mapping.targetField));
4288
4898
  if (!anchorMappings.length) { actions.setError('请至少映射企业名称、统一社会信用代码或注册号。'); return; }
@@ -4302,6 +4912,7 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
4302
4912
  columnCount: dataset.headers?.length || 0,
4303
4913
  headers: dataset.headers || [],
4304
4914
  sizeBytes: runtime.source?.sizeBytes || 0,
4915
+ checksum: runtime.source?.checksum || '',
4305
4916
  },
4306
4917
  });
4307
4918
  }
@@ -4316,7 +4927,6 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
4316
4927
  ?? mappings.find((mapping) => mapping.targetField === 'credit_no')
4317
4928
  ?? mappings.find((mapping) => mapping.targetField === 'reg_no');
4318
4929
  if (primary) actions.setNameField(primary.sourceField);
4319
- actions.setStep('profile');
4320
4930
  } catch (ruleError) {
4321
4931
  actions.setError(ruleError instanceof Error ? ruleError.message : String(ruleError));
4322
4932
  } finally {
@@ -4333,9 +4943,16 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
4333
4943
  } catch (error) { actions.setError(error.message); return; }
4334
4944
  };
4335
4945
 
4336
- const startNewWorkflow = () => {
4946
+ const startNewWorkflow = (reuseConfig = false) => {
4337
4947
  const event = document.createEvent('CustomEvent');
4338
- 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
+ });
4339
4956
  if (document.dispatchEvent(event)) {
4340
4957
  actions.setError('当前会话的任务向导尚未就绪或正在解析图片,请稍后重试。');
4341
4958
  return;
@@ -4356,6 +4973,7 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
4356
4973
  const createArtifactBundle = async () => {
4357
4974
  let latest = workflowTaskBySession.get(String(activeSessionId || 'unassigned')) ?? cachedTask;
4358
4975
  if (!latest) throw new Error('请先创建并执行数据清洗补全任务。');
4976
+ if (!canAction('create-local-artifacts')) throw new Error('当前流程节点不能生成本地结果。');
4359
4977
  if (Array.isArray(latest.artifacts) && latest.artifacts.length) return latest;
4360
4978
  const rows = exportRows();
4361
4979
  if (!rows?.length) throw new Error('当前页面没有可导出的明细。若任务来自历史记录,请重新上传源文件并继续处理。');
@@ -4436,6 +5054,37 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
4436
5054
  : cachedTask?.enrichmentSummary?.reviewRequired
4437
5055
  ?? cachedTask?.matchSummary?.reviewRequired;
4438
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} 条`;
4439
5088
  const stat = (label, value, tone) => h('div', { className: 'dcAgentCard' },
4440
5089
  h('span', null, label),
4441
5090
  h('b', { className: tone ? `is-${tone}` : null }, displayStatValue(value))
@@ -4472,8 +5121,13 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
4472
5121
  h('button', {
4473
5122
  type: 'button',
4474
5123
  className: 'dcAgentButton is-primary',
4475
- onClick: startNewWorkflow,
5124
+ onClick: () => startNewWorkflow(false),
4476
5125
  }, '新建清洗任务'),
5126
+ cachedTask ? h('button', {
5127
+ type: 'button',
5128
+ className: 'dcAgentButton',
5129
+ onClick: () => startNewWorkflow(true),
5130
+ }, '新建任务并复用配置') : null,
4477
5131
  ),
4478
5132
  historyTask ? h('section', { className: 'dcAgentSection', 'aria-label': '历史任务只读详情' },
4479
5133
  h('h3', null, workflowDisplayName(historyTask)),
@@ -4509,8 +5163,48 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
4509
5163
  ),
4510
5164
  );
4511
5165
  } else if (step === 'rules') {
4512
- 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' },
4513
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,
4514
5208
  h('label', { className: 'dcAgentFormField' },
4515
5209
  h('span', null, '任务名称'),
4516
5210
  h('input', { className: 'dcAgentField', value: taskTitle, maxLength: 120, onChange: (event) => actions.setTaskTitle(event.target.value) }),
@@ -4635,12 +5329,12 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
4635
5329
  type: 'button',
4636
5330
  className: 'dcAgentButton is-primary',
4637
5331
  disabled: busy,
4638
- onClick: () => actions.setStep(requiresQcc ? 'match' : 'enrich'),
5332
+ onClick: () => navigate(requiresQcc ? 'match' : 'enrich'),
4639
5333
  }, requiresQcc ? '下一步:匹配核验' : '下一步:本地清洗补全'),
4640
5334
  ),
4641
- ) : h('div', { className: 'dcAgentProfileEmpty' },
5335
+ ) : canAction('run-quality') ? h('div', { className: 'dcAgentProfileEmpty' },
4642
5336
  h('button', { type: 'button', className: 'dcAgentButton is-primary', disabled: busy || !hasData, 'aria-label': '生成体检报告', onClick: runProfile }, busy ? '体检中…' : '生成体检报告'),
4643
- ),
5337
+ ) : h('p', { className: 'dcAgentHint' }, '当前没有可恢复的质量体检明细。'),
4644
5338
  ),
4645
5339
  );
4646
5340
  } else if (step === 'match') {
@@ -4663,19 +5357,20 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
4663
5357
  h('section', { className: 'dcAgentMatchBlock', 'aria-label': '准备任务说明' },
4664
5358
  h('div', { className: 'dcAgentMatchBlockHead' },
4665
5359
  h('h4', null, '准备任务说明'),
4666
- 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), '条记录'),
4667
5362
  ),
4668
5363
  h('p', { className: 'dcAgentHint' }, `已选择 ${fieldSelection.length} 个补全字段。Host 自动检查所需工具并计算去重主体与调用上界。`),
4669
5364
  h('p', { className: 'dcAgentHint' }, '回填后可修改;点击发送才开始查询,使用你已连接的企查查 MCP 账号及额度。'),
4670
5365
  ),
4671
5366
  ),
4672
- h('div', { className: 'dcAgentMatchActions' },
5367
+ canAction('prepare-qcc-command') ? h('div', { className: 'dcAgentMatchActions' },
4673
5368
  h('button', { type: 'button', className: 'dcAgentButton is-primary', disabled: busy || !hasData || !cachedTask?.id, onClick: runQcc }, busy ? '检查并生成中…' : '生成可编辑任务说明'),
4674
- ),
5369
+ ) : null,
4675
5370
  ),
4676
5371
  qccRun ? h('section', { className: 'dcAgentSection' },
4677
5372
  h('h3', null, `任务 ${qccRun.runId} · ${qccRun.state}`),
4678
- ((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' },
4679
5374
  h('input', { type: 'checkbox', checked: paidConfirmed, 'aria-label': '确认候选或重试的企查查调用', onChange: (event) => actions.setPaidConfirmed(event.target.checked) }),
4680
5375
  '确认主体或重试会继续查询,使用当前连接的企查查账号;额度或费用由该账号自行承担。',
4681
5376
  ) : null,
@@ -4695,10 +5390,9 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
4695
5390
  item.candidates.map((candidate) => h('div', { key: candidate.creditNo, className: 'dcAgentCandidate' },
4696
5391
  h('div', null, h('b', null, candidate.companyName || '未命名候选'), h('small', null, candidate.creditNo)),
4697
5392
  h('small', null, `${candidate.status || '状态未知'} · ${(candidate.legalRep || []).join('、') || '法人未知'}`),
4698
- 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,
4699
5394
  )),
4700
5395
  )),
4701
- (qccRun.errors || []).some((item) => item.error?.retryable) ? h('button', { type: 'button', className: 'dcAgentButton', disabled: busy || !paidConfirmed, onClick: retryFailures }, '重试可恢复失败项') : null,
4702
5396
  ) : null,
4703
5397
  );
4704
5398
  } else if (step === 'enrich') {
@@ -4710,10 +5404,10 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
4710
5404
  ),
4711
5405
  h('section', { className: 'dcAgentSection' },
4712
5406
  h('h3', null, '本地清洗预处理'),
4713
- h('div', { className: 'dcAgentRow' },
4714
- h('button', { type: 'button', className: 'dcAgentButton', disabled: busy || !hasData, 'aria-label': '执行清洗', onClick: runClean }, busy ? '处理中…' : '执行确定性清洗'),
4715
- h('button', { type: 'button', className: 'dcAgentButton', disabled: busy || !hasData, 'aria-label': '执行补全', onClick: runComplete }, busy ? '处理中…' : '本地规则补全'),
4716
- ),
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,
4717
5411
  clean ? h('div', { className: 'dcAgentGrid' },
4718
5412
  stat('总数', clean.total), stat('保留', clean.kept, 'good'), stat('剔除', clean.dropped, 'bad'),
4719
5413
  stat('缺失关键字段', clean.badMissing, clean.badMissing > 0 ? 'bad' : null),
@@ -4727,13 +5421,28 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
4727
5421
  stat('QCC 已补全', qccEnrichedCount, qccEnrichedCount > 0 ? 'good' : null),
4728
5422
  stat('待核验', qccReviewCount, qccReviewCount > 0 ? 'warn' : null),
4729
5423
  ),
4730
- 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,
4731
5428
  );
4732
5429
  } else if (step === 'download') {
4733
- 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()];
4734
5437
  const resultArtifacts = availableArtifacts.filter(artifact => artifact.kind !== 'report');
4735
5438
  const reportArtifact = availableArtifacts.find(artifact => artifact.kind === 'report');
4736
- 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;
4737
5446
  pane = h('div', { className: 'dcAgentPane' },
4738
5447
  h('p', { className: 'dcAgentHint' }, '结果文件由 Host 保存;有失败、未匹配或待核验记录时展示异常清单。任务或插件重启后,可从任务历史继续下载;无需再次调用企查查。'),
4739
5448
  h('div', { className: 'dcAgentResultSummary' }, h('div', { className: 'dcAgentGrid' },
@@ -4770,10 +5479,21 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
4770
5479
  ),
4771
5480
  !canCreateArtifacts ? h('p', { className: 'dcAgentHint' }, '请先完成规则确认与清洗补全;历史任务若尚未生成制品,需要重新上传源文件。') : null,
4772
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,
4773
5493
  );
4774
5494
  } else {
4775
5495
  pane = h('div', { className: 'dcAgentPane' },
4776
- dataset ? h('section', { className: 'dcAgentSection dcAgentIntakeSummary', 'aria-label': '当前导入数据', role: 'status' },
5496
+ dataset || cachedTask?.source ? h('section', { className: 'dcAgentSection dcAgentIntakeSummary', 'aria-label': '当前导入数据', role: 'status' },
4777
5497
  h('div', { className: 'dcAgentIntakeSummaryHead' },
4778
5498
  h('div', null,
4779
5499
  h('h3', null, intake.available ? `已解析 ${intake.count} 条数据` : '已保存导入摘要'),
@@ -4781,8 +5501,12 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
4781
5501
  ),
4782
5502
  ),
4783
5503
  h('p', { className: 'dcAgentHint' }, intake.available
4784
- ? '数据已导入。请核对下方完整名单;需要替换来源时点击“重新导入”。'
4785
- : '当前页面没有完整原始数据。请重新导入原文件;历史下载制品仍可从任务历史获取。'),
5504
+ ? canAction('import-data')
5505
+ ? '数据已导入。请核对下方完整名单;需要替换来源时点击“重新导入”。'
5506
+ : '这是已导入来源的只读摘要。更换名单请从任务历史新建任务并复用配置。'
5507
+ : canAction('import-data')
5508
+ ? '当前页面没有完整原始数据。请重新导入原文件;历史下载制品仍可从任务历史获取。'
5509
+ : '当前页面只保留 Host 来源摘要;已有制品可从结果下载或任务历史获取。'),
4786
5510
  ) : null,
4787
5511
  showIntakeEditor ? h('section', { id: 'dc-agent-intake-editor', className: 'dcAgentSection dcAgentIntakeEditor', 'aria-label': dataset ? '重新导入数据' : '导入数据' },
4788
5512
  h('header', { className: 'dcAgentIntakeEditorHeader' },
@@ -4840,16 +5564,47 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
4840
5564
  }) : null,
4841
5565
  hasData ? h('p', { className: 'dcAgentHint' }, '请核对完整名单。识别清单不代表已完成企查查匹配。核对后进入「规则与体检」,确认规则生成质量体检;再进入「主体匹配」直接生成可编辑任务说明。Host 自动检查工具和调用范围,回填中央对话框后点击发送才开始查询。') : null,
4842
5566
  h('div', { className: 'dcAgentUploadActions' },
4843
- hasData && !intakeEditorOpen ? h('button', {
5567
+ hasData && !intakeEditorOpen && canAction('import-data') ? h('button', {
4844
5568
  type: 'button', className: 'dcAgentButton', disabled: busy,
4845
5569
  'aria-expanded': false, 'aria-controls': 'dc-agent-intake-editor',
4846
5570
  onClick: () => { actions.setError(null); setIntakeEditorOpen(true); },
4847
5571
  }, '重新导入') : null,
4848
- 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,
4849
5577
  ),
4850
5578
  );
4851
5579
  }
4852
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
+
4853
5608
  return h('section', {
4854
5609
  className: 'dcAgentWorkbenchContent',
4855
5610
  'data-dsh-plugin': 'data-cleaning-agent',
@@ -4858,54 +5613,75 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
4858
5613
  },
4859
5614
  h('nav', { className: 'dcAgentManagement', 'aria-label': '工作台管理' },
4860
5615
  h('button', { type: 'button', className: 'dcAgentButton', 'aria-pressed': step !== 'history',
4861
- onClick: () => actions.setStep(cachedTask?.stage || 'upload') }, '当前任务'),
5616
+ onClick: () => navigate(cachedTask?.flow?.currentStage || cachedTask?.stage || 'upload') }, '当前任务'),
4862
5617
  h('button', { type: 'button', className: 'dcAgentButton', 'aria-pressed': step === 'history',
4863
- onClick: () => actions.setStep('history') }, '任务历史'),
5618
+ onClick: () => navigate('history') }, '任务历史'),
4864
5619
  ),
4865
5620
  step !== 'history' ? h('nav', { className: 'dcAgentStepper', 'aria-label': '清洗流程',
4866
5621
  style: { '--dc-stage-count': STEPS.length } },
4867
- STEPS.map((st) => h('button', {
4868
- key: st.key,
4869
- type: 'button',
4870
- className: `dcAgentStep${(step === 'profile' ? 'rules' : step) === st.key ? ' is-active' : ''}`,
4871
- 'aria-label': st.label,
4872
- title: st.label,
4873
- 'aria-current': (step === 'profile' ? 'rules' : step) === st.key ? 'step' : undefined,
4874
- onClick: () => actions.setStep(st.key),
4875
- }, h('span', { className: 'dcAgentStepIcon' }, h(StageIcon, { kind: st.icon })),
4876
- 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
+ }),
4877
5644
  ) : null,
4878
5645
  h('div', { className: 'dcAgentWbBody' },
4879
- cachedTask && step !== 'history' ? h('section', { className: 'dcAgentSection', 'aria-label': '当前任务进展', 'aria-live': 'polite' },
4880
- h('b', null, cachedTask.source?.type === 'image' && cachedTask.state === 'draft' ? '图片已暂存,待识别' : `任务记录:${WORKFLOW_STATE_LABELS[cachedTask.state] || '状态未知'}`),
4881
- h('h3', null, workflowDisplayName(cachedTask)),
4882
- ['matching', 'enriching'].includes(cachedTask.state) ? h('p', { className: 'dcAgentHint' }, '以上为最近同步的任务记录,不代表模型仍在运行。若会话已停止,请查看原会话的确认或错误提示;不要重复启动。') : null,
4883
- h('details', null, h('summary', null, '任务详情'), h('p', { className: 'dcAgentHint' }, `任务编号:${cachedTask.id}`)),
4884
- 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} 个补全字段`),
4885
- commandProgress?.taskId === cachedTask.id && Number.isFinite(commandProgress.totalUnique) ? h('p', null, `已处理主体 ${commandProgress.completedUnique}/${commandProgress.totalUnique}(去重后,含未匹配或失败,不代表补全成功)`) : null,
4886
- commandProgress?.taskId === cachedTask.id && Number.isFinite(commandProgress.totalUnique) && commandProgress.totalUnique > 0 ? h('progress', {
4887
- max: commandProgress.totalUnique, value: Math.min(commandProgress.totalUnique, Math.max(0, commandProgress.completedUnique || 0)),
4888
- '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'),
4889
5667
  }) : null,
4890
- h('p', { style: { color: qccFailedCount > 0 ? '#c62828' : Number(qccReviewCount) > 0 ? '#a66a00' : qccEnrichedCount > 0 ? '#16845b' : 'inherit' } },
4891
- cachedTask.source?.type === 'image' && cachedTask.state === 'draft' ? '请发送对话中的任务说明开始图片识别,识别后核验企业名单。' : Number.isFinite(qccEnrichedCount) ? `已处理并补全 ${qccEnrichedCount} 条 · 待核验 ${Number.isFinite(qccReviewCount) ? qccReviewCount : '统计中'} 条 · 失败 ${qccFailedCount} 条(按输入行统计;未披露字段保留空白)` : '结果统计中,处理结束后展示补全与待核验数量'),
4892
- ) : null,
4893
- error ? h('div', { className: 'dcAgentError', role: 'alert' }, error) : null,
4894
- step !== 'history' ? h('details', { className: 'dcAgentSection' },
4895
- h('summary', null, '使用说明与任务流程'),
4896
- h('p', { className: 'dcAgentHint' }, '导入与核验 → 规则与体检 → 主体匹配 → 字段补全 → 结果下载。菜单只切换视图,不启动调用;按页面主按钮逐步确认。'),
4897
- h('p', { className: 'dcAgentHint' }, '支持文本、Excel、JSON 和图片名单。原值保留、多候选人工核验;企查查调用使用当前用户自己的连接与额度。'),
5668
+ h(WorkflowGuide),
4898
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,
4899
5675
  ['rules', 'profile'].includes(step) ? h('nav', { className: 'dcAgentViewTabs', 'aria-label': '规则与体检视图' },
4900
- h('button', { type: 'button', className: 'dcAgentButton', 'aria-pressed': step === 'rules', onClick: () => actions.setStep('rules') }, '字段映射与规则'),
4901
- 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') }, '质量体检报告'),
4902
5678
  ) : null,
4903
- workflowNavigationIssue(step, cachedTask, hasData, profile, qccRun, Boolean((clean || complete) && exportRows()?.length))
5679
+ runtimeRecoveryPane || (workflowNavigationIssue(step, cachedTask, hasData, profile, qccRun, Boolean((clean || complete) && exportRows()?.length))
4904
5680
  ? h('section', { className: 'dcAgentSection', role: 'status' },
4905
5681
  h('p', null, workflowNavigationIssue(step, cachedTask, hasData, profile, qccRun, Boolean((clean || complete) && exportRows()?.length))),
4906
5682
  h('button', { type: 'button', className: 'dcAgentButton',
4907
- onClick: () => actions.setStep(!hasData ? 'upload' : !cachedTask || ['draft', 'uploaded'].includes(cachedTask.state) ? 'rules' : cachedTask.state === 'rules_confirmed' ? 'profile' : step === 'download' ? 'enrich' : 'match') }, '前往前置步骤'))
4908
- : pane,
5683
+ onClick: () => navigate(cachedTask?.flow?.currentStage || cachedTask?.stage || 'upload') }, '返回当前节点'))
5684
+ : pane),
4909
5685
  ),
4910
5686
  );
4911
5687
  }
@@ -5064,7 +5840,12 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
5064
5840
  react.useEffect(() => {
5065
5841
  instance.actions.openAt(null, props.scope.sessionId);
5066
5842
  return adapter.attach(props.scope, { tabId: props.tab.id, store: props.store,
5067
- 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
+ ) });
5068
5849
  }, [instance, props.scope.sessionId, props.tab.id, props.store]);
5069
5850
  return h(WorkbenchContent, { useStore, actions: instance.actions, visible: props.visible,
5070
5851
  sendSessionCommand: (sessionId, prompt) => sendQccAgentCommand(ctx, sessionId, prompt),
@@ -5139,13 +5920,29 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
5139
5920
  open(sessionId, step) {
5140
5921
  if (disposed || !sessionId) return false;
5141
5922
  const instance = instanceFor(sessionId);
5142
- 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);
5143
5929
  const sessions = typeof ctx.get === 'function' ? ctx.get('sessions') : ctx.sessions;
5144
5930
  const summary = sessions?.list?.getSnapshot?.()?.byId?.[sessionId];
5145
5931
  openingSession = sessionId;
5146
- try { return adapter.open({ sessionId, ...(summary?.cwd ? { cwd: summary.cwd } : {}) },
5147
- step || instance.getSnapshot().step); }
5932
+ let opened;
5933
+ try { opened = adapter.open({ sessionId, ...(summary?.cwd ? { cwd: summary.cwd } : {}) },
5934
+ destination.stage); }
5148
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;
5149
5946
  },
5150
5947
  };
5151
5948
  sessionWorkbenchController = controller;
@@ -5154,7 +5951,7 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
5154
5951
  [WORKBENCH_OPEN_EVENT]: event => {
5155
5952
  const detail = event.detail || {};
5156
5953
  const actions = controller.actionsFor(detail.sessionId);
5157
- if (detail.task?.id) cacheWorkflowTask(actions, detail.sessionId, detail.task);
5954
+ if (detail.task?.id) cacheWorkflowTask(actions, detail.sessionId, detail.task, 'restore');
5158
5955
  controller.open(detail.sessionId, detail.step);
5159
5956
  },
5160
5957
  [WORKBENCH_DATASET_EVENT]: event => {
@@ -5307,6 +6104,11 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
5307
6104
  visibleCatalogFields,
5308
6105
  installCapabilityMount,
5309
6106
  PromptDialog,
6107
+ normalizeViewStage,
6108
+ resolveStageAccess,
6109
+ workflowDestination,
6110
+ lockedWorkflowNotice,
6111
+ navigateWorkflow,
5310
6112
  workflowNavigationIssue,
5311
6113
  buildTaskPrompt,
5312
6114
  DatasetReview,
@@ -5320,6 +6122,7 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
5320
6122
  requestPromptImage,
5321
6123
  guessMappings,
5322
6124
  intakeState,
6125
+ restoreParsedRuntime,
5323
6126
  projectCompletionResult,
5324
6127
  mappingRecommendations,
5325
6128
  mappedOutputFields,
@@ -5340,6 +6143,7 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
5340
6143
  observeHomePhase,
5341
6144
  ensureWorkflowTask,
5342
6145
  ensureEditableWorkflowTask,
6146
+ refreshHostedTask,
5343
6147
  queueWorkflowOperation,
5344
6148
  taskBelongsToSession,
5345
6149
  workflowOriginLabel,