吸收数字员工跨设备路由干预冲突预览

This commit is contained in:
baiyanyun
2026-06-13 15:58:10 +08:00
parent 5ab6bc0fd0
commit 7ea963dc9a
7 changed files with 258 additions and 6 deletions

View File

@@ -27,6 +27,18 @@ public class DigitalEmployeePolicySnapshotRowDto {
private Date latestSyncedAt; private Date latestSyncedAt;
@JsonProperty("recommended_resolution") @JsonProperty("recommended_resolution")
private String recommendedResolution; private String recommendedResolution;
@JsonProperty("route_decision_count")
private Integer routeDecisionCount;
@JsonProperty("blocked_count")
private Integer blockedCount;
@JsonProperty("approval_required_count")
private Integer approvalRequiredCount;
@JsonProperty("missing_context_count")
private Integer missingContextCount;
@JsonProperty("not_mapped_count")
private Integer notMappedCount;
@JsonProperty("intervention_conflict_status")
private String interventionConflictStatus;
@JsonProperty("sync_record_url") @JsonProperty("sync_record_url")
private String syncRecordUrl; private String syncRecordUrl;
@JsonProperty("client_key_masked") @JsonProperty("client_key_masked")

View File

@@ -546,6 +546,9 @@ public class DigitalEmployeeSyncApplicationServiceImpl implements DigitalEmploye
for (DigitalEmployeeSyncRecord record : page.getRecords()) { for (DigitalEmployeeSyncRecord record : page.getRecords()) {
Map<String, Object> view = readJsonObject(record.getBusinessView()); Map<String, Object> view = readJsonObject(record.getBusinessView());
String kind = firstString(view.get("policy_kind")); String kind = firstString(view.get("policy_kind"));
if (StringUtils.isBlank(kind) && isRouteInterventionPolicySnapshotView(view)) {
kind = "route_decision";
}
if (StringUtils.isBlank(kind)) continue; if (StringUtils.isBlank(kind)) continue;
if (StringUtils.isNotBlank(policyKind) && !StringUtils.equals(kind, policyKind)) continue; if (StringUtils.isNotBlank(policyKind) && !StringUtils.equals(kind, policyKind)) continue;
grouped.computeIfAbsent(kind, ignored -> new ArrayList<>()).add(record); grouped.computeIfAbsent(kind, ignored -> new ArrayList<>()).add(record);
@@ -570,22 +573,40 @@ public class DigitalEmployeeSyncApplicationServiceImpl implements DigitalEmploye
private DigitalEmployeePolicySnapshotRowDto toPolicySnapshotRow(String policyKind, List<DigitalEmployeeSyncRecord> records) { private DigitalEmployeePolicySnapshotRowDto toPolicySnapshotRow(String policyKind, List<DigitalEmployeeSyncRecord> records) {
List<Map<String, Object>> views = records.stream().map(record -> readJsonObject(record.getBusinessView())).toList(); List<Map<String, Object>> views = records.stream().map(record -> readJsonObject(record.getBusinessView())).toList();
List<String> signatures = views.stream().map(this::policySignature).distinct().toList(); List<Map<String, Object>> policyViews = views.stream()
.filter(view -> StringUtils.isNotBlank(firstString(view.get("policy_kind"))))
.toList();
List<Map<String, Object>> routeInterventionViews = views.stream()
.filter(this::isRouteInterventionPolicySnapshotView)
.toList();
List<String> signatures = policyViews.stream().map(this::policySignature).distinct().toList();
DigitalEmployeeSyncRecord latest = records.stream() DigitalEmployeeSyncRecord latest = records.stream()
.max(Comparator.comparing(DigitalEmployeeSyncRecord::getSyncedAt, Comparator.nullsLast(Comparator.naturalOrder()))) .max(Comparator.comparing(DigitalEmployeeSyncRecord::getSyncedAt, Comparator.nullsLast(Comparator.naturalOrder())))
.orElse(records.get(0)); .orElse(records.get(0));
Map<String, Object> latestView = readJsonObject(latest.getBusinessView()); Map<String, Object> latestView = readJsonObject(latest.getBusinessView());
String status = signatures.size() > 1 ? "conflict" : "consistent"; String interventionConflictStatus = routeInterventionConflictStatus(routeInterventionViews);
boolean routeConflict = StringUtils.equals(interventionConflictStatus, "conflict");
String status = signatures.size() > 1 || routeConflict ? "conflict" : "consistent";
List<String> conflictKeys = new ArrayList<>("conflict".equals(status) ? policyConflictKeys(policyViews) : List.of());
if (routeConflict && !conflictKeys.contains("route_intervention_state")) {
conflictKeys.add("route_intervention_state");
}
return DigitalEmployeePolicySnapshotRowDto.builder() return DigitalEmployeePolicySnapshotRowDto.builder()
.policyKind(policyKind) .policyKind(policyKind)
.policyKey(firstString(latestView.get("policy_key"), policyKind)) .policyKey(firstString(latestView.get("policy_key"), policyKind))
.status(status) .status(status)
.summary(firstString(latestView.get("summary"), latestView.get("title"), policyKind)) .summary(routeConflict ? "跨设备路由干预冲突" : firstString(latestView.get("summary"), latestView.get("title"), policyKind))
.clientCount((int) records.stream().map(DigitalEmployeeSyncRecord::getClientKey).filter(StringUtils::isNotBlank).distinct().count()) .clientCount((int) records.stream().map(DigitalEmployeeSyncRecord::getClientKey).filter(StringUtils::isNotBlank).distinct().count())
.deviceCount((int) records.stream().map(DigitalEmployeeSyncRecord::getDeviceId).filter(StringUtils::isNotBlank).distinct().count()) .deviceCount((int) records.stream().map(DigitalEmployeeSyncRecord::getDeviceId).filter(StringUtils::isNotBlank).distinct().count())
.conflictKeys("conflict".equals(status) ? policyConflictKeys(views) : List.of()) .conflictKeys(conflictKeys)
.latestSyncedAt(latest.getSyncedAt()) .latestSyncedAt(latest.getSyncedAt())
.recommendedResolution("conflict".equals(status) ? "review_latest_policy_snapshot" : "keep_current") .recommendedResolution("conflict".equals(status) ? "review_latest_policy_snapshot" : "keep_current")
.routeDecisionCount(routeInterventionViews.size())
.blockedCount(routeInterventionCount(routeInterventionViews, "blocked"))
.approvalRequiredCount(routeInterventionCount(routeInterventionViews, "approval_required"))
.missingContextCount(routeInterventionCount(routeInterventionViews, "missing_context"))
.notMappedCount(routeInterventionCount(routeInterventionViews, "not_mapped"))
.interventionConflictStatus(interventionConflictStatus)
.syncRecordUrl(syncRecordUrl(latest.getOutboxId())) .syncRecordUrl(syncRecordUrl(latest.getOutboxId()))
.clientKeyMasked(maskClientKey(latest.getClientKey())) .clientKeyMasked(maskClientKey(latest.getClientKey()))
.deviceId(latest.getDeviceId()) .deviceId(latest.getDeviceId())
@@ -593,6 +614,47 @@ public class DigitalEmployeeSyncApplicationServiceImpl implements DigitalEmploye
.build(); .build();
} }
private boolean isRouteInterventionPolicySnapshotView(Map<String, Object> view) {
String category = firstString(view.get("category"));
String kind = firstString(view.get("kind"));
return StringUtils.equals(category, "route_intervention")
|| StringUtils.equals(category, "route_governance")
|| contains(kind, "route_decision_blocked")
|| contains(kind, "route_decision_approval_required")
|| contains(kind, "route_action_policy_blocked")
|| contains(kind, "route_action_missing_context")
|| contains(kind, "route_action_not_mapped")
|| StringUtils.isNotBlank(firstString(view.get("route_decision_id"), view.get("routeDecisionId")));
}
private String routeInterventionConflictStatus(List<Map<String, Object>> views) {
if (views.isEmpty()) return "none";
return views.stream().map(this::routeInterventionState).distinct().count() > 1 ? "conflict" : "consistent";
}
private int routeInterventionCount(List<Map<String, Object>> views, String state) {
return (int) views.stream().filter(view -> StringUtils.equals(routeInterventionState(view), state)).count();
}
private String routeInterventionState(Map<String, Object> view) {
String kind = firstString(view.get("kind"));
String policyResult = firstString(view.get("policy_result"), view.get("policyResult"));
String status = firstString(view.get("status"));
if (contains(kind, "route_decision_approval_required") || contains(policyResult, "approval_required") || contains(status, "approval_required")) {
return "approval_required";
}
if (contains(kind, "route_action_missing_context") || contains(status, "missing_context")) {
return "missing_context";
}
if (contains(kind, "route_action_not_mapped") || contains(status, "not_mapped")) {
return "not_mapped";
}
if (contains(kind, "route_decision_blocked") || contains(kind, "route_action_policy_blocked") || contains(policyResult, "blocked") || contains(status, "blocked")) {
return "blocked";
}
return firstString(policyResult, status, "observed");
}
private String policySignature(Map<String, Object> view) { private String policySignature(Map<String, Object> view) {
Map<String, Object> signature = new LinkedHashMap<>(); Map<String, Object> signature = new LinkedHashMap<>();
for (String key : policyConflictCandidateKeys()) { for (String key : policyConflictCandidateKeys()) {

View File

@@ -1460,6 +1460,86 @@ class DigitalEmployeeSyncApplicationServiceImplTest {
assertThat(row.getRecommendedResolution()).isEqualTo("review_latest_policy_snapshot"); assertThat(row.getRecommendedResolution()).isEqualTo("review_latest_policy_snapshot");
} }
@Test
void queryAdminPolicySnapshotsSummarizesRouteInterventionConflicts() {
DigitalEmployeeSyncRecord routeBlocked = record("outbox-route-blocked", "event", "event-route-1", 101L, new Date(10_000),
Map.of(
"entity_type", "event",
"category", "route_intervention",
"kind", "route_decision_blocked",
"status", "blocked",
"title", "入口路由被阻断",
"route_decision_id", "route-1",
"policy_result", "blocked",
"blocked_reason", "sensitive_action"
),
Map.of("id", "event-route-1", "kind", "route_decision_blocked"));
routeBlocked.setClientKey("client-key-a");
routeBlocked.setDeviceId("device-a");
DigitalEmployeeSyncRecord routeApproval = record("outbox-route-approval", "event", "event-route-2", 101L, new Date(11_000),
Map.of(
"entity_type", "event",
"category", "route_intervention",
"kind", "route_decision_approval_required",
"status", "approval_required",
"title", "入口路由待审批",
"route_decision_id", "route-2",
"policy_result", "approval_required",
"approval_id", "approval-1"
),
Map.of("id", "event-route-2", "kind", "route_decision_approval_required"));
routeApproval.setClientKey("client-key-b");
routeApproval.setDeviceId("device-b");
DigitalEmployeeSyncRecord routeMissingContext = record("outbox-route-missing-context", "event", "event-route-3", 101L, new Date(12_000),
Map.of(
"entity_type", "event",
"category", "route_governance",
"kind", "route_action_missing_context",
"status", "missing_context",
"title", "路由动作缺少上下文",
"route_decision_id", "route-3"
),
Map.of("id", "event-route-3", "kind", "route_action_missing_context"));
routeMissingContext.setClientKey("client-key-c");
routeMissingContext.setDeviceId("device-c");
when(repository.queryBusinessRecords(eq(101L), eq(null), eq(null), eq("event"), eq(null), eq(null), eq(null), eq(null), eq(1L), eq(100L)))
.thenReturn(page(List.of(
routeBlocked,
routeApproval,
routeMissingContext,
record("outbox-policy", "event", "event-policy-1", 101L, new Date(13_000),
Map.of(
"entity_type", "event",
"category", "policy_snapshot",
"kind", "digital_workday_pull_route_decision_policy",
"policy_kind", "route_decision",
"policy_key", "route_decision",
"enabled", true,
"auto_create_governance_commands", true,
"title", "入口路由策略"
),
Map.of("kind", "digital_workday_pull_route_decision_policy")),
record("outbox-skill", "event", "event-skill-1", 101L, new Date(14_000),
Map.of("entity_type", "event", "category", "skill_call_audit", "title", "技能审计", "status", "allowed"),
Map.of("id", "event-skill-1", "status", "allowed"))
)));
DigitalEmployeePolicySnapshotPageDto result = service.queryAdminPolicySnapshots(null, null, "conflict", "route_decision", null, null, 1L, 20L);
assertThat(result.getTotal()).isEqualTo(1);
DigitalEmployeePolicySnapshotRowDto row = result.getRecords().get(0);
assertThat(row.getPolicyKind()).isEqualTo("route_decision");
assertThat(row.getStatus()).isEqualTo("conflict");
assertThat(row.getRouteDecisionCount()).isEqualTo(3);
assertThat(row.getBlockedCount()).isEqualTo(1);
assertThat(row.getApprovalRequiredCount()).isEqualTo(1);
assertThat(row.getMissingContextCount()).isEqualTo(1);
assertThat(row.getNotMappedCount()).isZero();
assertThat(row.getInterventionConflictStatus()).isEqualTo("conflict");
assertThat(row.getConflictKeys()).contains("route_intervention_state");
}
@Test @Test
void recordDailyReportSendResultStoresRecordedObservation() { void recordDailyReportSendResultStoresRecordedObservation() {
DigitalEmployeeAdminRecordRequestDto request = adminRecordRequest("daily_report", "report-1"); DigitalEmployeeAdminRecordRequestDto request = adminRecordRequest("daily_report", "report-1");

View File

@@ -119,6 +119,7 @@ function renderSummary(record: DigitalEmployeeAdminWorkbenchRow) {
record.cleanup_request_id && `Cleanup ${record.cleanup_request_id}`, record.cleanup_request_id && `Cleanup ${record.cleanup_request_id}`,
record.route_decision_id && `Route ${record.route_decision_id}`, record.route_decision_id && `Route ${record.route_decision_id}`,
record.policy_kind && `Policy ${record.policy_kind}`, record.policy_kind && `Policy ${record.policy_kind}`,
record.intervention_conflict_status && `Route ${record.intervention_conflict_status}`,
record.subscription_policy_id && `Subscription ${record.subscription_policy_id}`, record.subscription_policy_id && `Subscription ${record.subscription_policy_id}`,
record.daily_report_fact_id && `ReportFact ${record.daily_report_fact_id}`, record.daily_report_fact_id && `ReportFact ${record.daily_report_fact_id}`,
record.memory_id && `Memory ${record.memory_id}`, record.memory_id && `Memory ${record.memory_id}`,
@@ -675,6 +676,12 @@ const DigitalEmployeeOps: React.FC = () => {
policy_key: record.policy_key, policy_key: record.policy_key,
conflict_keys: record.conflict_keys, conflict_keys: record.conflict_keys,
recommended_resolution: record.recommended_resolution, recommended_resolution: record.recommended_resolution,
route_decision_count: record.route_decision_count,
blocked_count: record.blocked_count,
approval_required_count: record.approval_required_count,
missing_context_count: record.missing_context_count,
not_mapped_count: record.not_mapped_count,
intervention_conflict_status: record.intervention_conflict_status,
latest_synced_at: record.latest_synced_at, latest_synced_at: record.latest_synced_at,
synced_at: record.latest_synced_at, synced_at: record.latest_synced_at,
client_key_masked: record.client_key_masked, client_key_masked: record.client_key_masked,
@@ -833,12 +840,28 @@ const DigitalEmployeeOps: React.FC = () => {
{detailRecord.conflict_keys?.length {detailRecord.conflict_keys?.length
? detailRecord.conflict_keys.join(' / ') ? detailRecord.conflict_keys.join(' / ')
: '暂无冲突字段'} : '暂无冲突字段'}
{detailRecord.intervention_conflict_status &&
` · 跨设备路由干预冲突 ${detailRecord.intervention_conflict_status}`}
{detailRecord.route_decision_count != null &&
` · 路由 ${detailRecord.route_decision_count}`}
{detailRecord.blocked_count != null &&
` · 阻断 ${detailRecord.blocked_count}`}
{detailRecord.approval_required_count != null &&
` · 待审批 ${detailRecord.approval_required_count}`}
{detailRecord.missing_context_count != null &&
` · 缺上下文 ${detailRecord.missing_context_count}`}
{detailRecord.not_mapped_count != null &&
` · 未映射 ${detailRecord.not_mapped_count}`}
{detailRecord.recommended_resolution && ( {detailRecord.recommended_resolution && (
<Typography.Text type="secondary"> <Typography.Text type="secondary">
{' '} {' '}
· {detailRecord.recommended_resolution} · {detailRecord.recommended_resolution}
</Typography.Text> </Typography.Text>
)} )}
<Typography.Text type="secondary">
{' '}
·
</Typography.Text>
</Typography.Paragraph> </Typography.Paragraph>
)} )}
{activeView === 'approval_governance' && ( {activeView === 'approval_governance' && (

View File

@@ -629,6 +629,12 @@ export interface DigitalEmployeeAdminWorkbenchRow {
revision_id?: string; revision_id?: string;
conflict_keys?: string[]; conflict_keys?: string[];
recommended_resolution?: string; recommended_resolution?: string;
route_decision_count?: number;
blocked_count?: number;
approval_required_count?: number;
missing_context_count?: number;
not_mapped_count?: number;
intervention_conflict_status?: string;
occurred_at?: string; occurred_at?: string;
synced_at?: string; synced_at?: string;
latest_synced_at?: string; latest_synced_at?: string;
@@ -672,6 +678,12 @@ export interface DigitalEmployeePolicySnapshotRow {
conflict_keys?: string[]; conflict_keys?: string[];
latest_synced_at?: string; latest_synced_at?: string;
recommended_resolution?: string; recommended_resolution?: string;
route_decision_count?: number;
blocked_count?: number;
approval_required_count?: number;
missing_context_count?: number;
not_mapped_count?: number;
intervention_conflict_status?: string;
sync_record_url?: string; sync_record_url?: string;
client_key_masked?: string; client_key_masked?: string;
device_id?: string; device_id?: string;

View File

@@ -1191,6 +1191,55 @@ function checkDigitalAdminPolicySnapshots() {
const qimingOpsPath = path.join(qimingRoot, "src", "pages", "SystemManagement", "Content", "DigitalEmployeeOps", "index.tsx"); const qimingOpsPath = path.join(qimingRoot, "src", "pages", "SystemManagement", "Content", "DigitalEmployeeOps", "index.tsx");
const qimingServicePath = path.join(qimingRoot, "src", "services", "systemManage.ts"); const qimingServicePath = path.join(qimingRoot, "src", "services", "systemManage.ts");
const qimingTypesPath = path.join(qimingRoot, "src", "types", "interfaces", "systemManage.ts"); const qimingTypesPath = path.join(qimingRoot, "src", "types", "interfaces", "systemManage.ts");
const backendServicePath = path.join(
backendRoot,
"app-platform-modules",
"app-platform-agent",
"app-platform-agent-core-application",
"src",
"main",
"java",
"com",
"xspaceagi",
"agent",
"core",
"application",
"service",
"DigitalEmployeeSyncApplicationServiceImpl.java",
);
const backendDtoPath = path.join(
backendRoot,
"app-platform-modules",
"app-platform-agent",
"app-platform-agent-core-adapter",
"src",
"main",
"java",
"com",
"xspaceagi",
"agent",
"core",
"adapter",
"dto",
"digital",
"DigitalEmployeePolicySnapshotRowDto.java",
);
const backendTestPath = path.join(
backendRoot,
"app-platform-modules",
"app-platform-agent",
"app-platform-agent-core-application",
"src",
"test",
"java",
"com",
"xspaceagi",
"agent",
"core",
"application",
"service",
"DigitalEmployeeSyncApplicationServiceImplTest.java",
);
const backendControllerPath = path.join( const backendControllerPath = path.join(
backendRoot, backendRoot,
"app-platform-modules", "app-platform-modules",
@@ -1213,6 +1262,9 @@ function checkDigitalAdminPolicySnapshots() {
const qimingOps = fs.readFileSync(qimingOpsPath, "utf8"); const qimingOps = fs.readFileSync(qimingOpsPath, "utf8");
const qimingService = fs.readFileSync(qimingServicePath, "utf8"); const qimingService = fs.readFileSync(qimingServicePath, "utf8");
const qimingTypes = fs.readFileSync(qimingTypesPath, "utf8"); const qimingTypes = fs.readFileSync(qimingTypesPath, "utf8");
const backendService = fs.readFileSync(backendServicePath, "utf8");
const backendDto = fs.readFileSync(backendDtoPath, "utf8");
const backendTest = fs.readFileSync(backendTestPath, "utf8");
const backendController = fs.readFileSync(backendControllerPath, "utf8"); const backendController = fs.readFileSync(backendControllerPath, "utf8");
const requiredSnippets = [ const requiredSnippets = [
[syncService, "policySnapshotBusinessView", "client policy snapshot business view"], [syncService, "policySnapshotBusinessView", "client policy snapshot business view"],
@@ -1224,13 +1276,24 @@ function checkDigitalAdminPolicySnapshots() {
[syncTest, "event-route-policy-pull", "route policy snapshot sync test"], [syncTest, "event-route-policy-pull", "route policy snapshot sync test"],
[syncTest, "event-risk-policy-pull", "risk policy snapshot sync test"], [syncTest, "event-risk-policy-pull", "risk policy snapshot sync test"],
[backendController, "/snapshots", "backend policy snapshot endpoint"], [backendController, "/snapshots", "backend policy snapshot endpoint"],
[backendService, "routeInterventionConflictStatus", "backend route intervention conflict aggregation"],
[backendService, "route_intervention_state", "backend route intervention conflict key"],
[backendDto, "intervention_conflict_status", "backend route intervention conflict status field"],
[backendDto, "route_decision_count", "backend route decision count field"],
[backendTest, "queryAdminPolicySnapshotsSummarizesRouteInterventionConflicts", "backend route intervention conflict test"],
[qimingService, "apiDigitalEmployeeAdminPolicySnapshots", "qiming policy snapshot service"], [qimingService, "apiDigitalEmployeeAdminPolicySnapshots", "qiming policy snapshot service"],
[qimingService, "/api/digital-employee/admin/policies/snapshots", "qiming policy snapshot endpoint"], [qimingService, "/api/digital-employee/admin/policies/snapshots", "qiming policy snapshot endpoint"],
[qimingTypes, "DigitalEmployeePolicySnapshotRow", "qiming policy snapshot row type"], [qimingTypes, "DigitalEmployeePolicySnapshotRow", "qiming policy snapshot row type"],
[qimingTypes, "policy_conflicts", "qiming policy conflicts view type"], [qimingTypes, "policy_conflicts", "qiming policy conflicts view type"],
[qimingTypes, "intervention_conflict_status", "qiming route intervention conflict type field"],
[qimingOps, "策略冲突", "qiming policy conflict tab text"], [qimingOps, "策略冲突", "qiming policy conflict tab text"],
[qimingOps, "conflict_keys", "qiming conflict key display"], [qimingOps, "conflict_keys", "qiming conflict key display"],
[qimingOps, "跨设备路由干预冲突", "qiming route intervention conflict copy"],
[qimingOps, "不恢复执行", "qiming route recovery guard copy"],
[qimingOps, "不绕过审批", "qiming approval guard copy"],
[qimingOps, "不远程改策略", "qiming remote policy edit guard copy"],
[docs, "跨设备策略冲突视图", "docs policy conflict absorption"], [docs, "跨设备策略冲突视图", "docs policy conflict absorption"],
[docs, "跨设备路由干预冲突可视化", "docs route intervention conflict absorption"],
]; ];
const missing = requiredSnippets const missing = requiredSnippets
.filter(([content, snippet]) => !content.includes(snippet)) .filter(([content, snippet]) => !content.includes(snippet))

View File

@@ -599,8 +599,8 @@ GET /api/digital-employee/approvals
10. **入口路由 / 任务分流(路由业务视图与通知联动已开始)** 10. **入口路由 / 任务分流(路由业务视图与通知联动已开始)**
- 已吸收:管理端通过 `/computer/chat` 进入 qimingclaw 后会生成本地任务事实embedded `/api/ingress/route``/api/route-decisions``/api/digital-employee/route-decisions` 已由 qimingclaw adapter 接管。 - 已吸收:管理端通过 `/computer/chat` 进入 qimingclaw 后会生成本地任务事实embedded `/api/ingress/route``/api/route-decisions``/api/digital-employee/route-decisions` 已由 qimingclaw adapter 接管。
- 当前能力入口请求会按上下文、ready PlanStep、调度、查询和控制动作生成 route decision并写入 `digital_events.kind = route_decision` 与现有 event outbox路由时间线优先从 qimingclaw 本地事件读取;低风险 route decision 已开始映射为明确的本地 governance command并把 `created_command_id` 回填到 route decision自动映射前会执行低风险/受保护高风险 action guard未允许的控制动作会记录 `route_action_policy_blocked``route_action_missing_context`,避免静默创建命令;入口路由策略可从管理端远端下发,支持按 intent 阻断、按 intent 要求审批、限制自动创建 governance command并在策略干预时先记录 route decision 再停止本地命令执行;首页已展示路由干预队列和治理审计摘要,审批通过/拒绝会写入本地 approval decision 并回写管理端,管理端审批更新拉取后会自动扫描已批准 route approval 并按幂等规则恢复执行scheduler check 已接入 ready PlanStep 安全派发,首页立即执行后会触发一次派发 sweep 并展示派发结果;路由决策本地业务视图、首页摘要、治理审计、策略来源/拉取错误、阻断/待审/待干预/已恢复计数和通知联动已开始闭环。管理端正式路由 UI 预览已开始吸收“策略中心”会把入口路由策略纳入统一拉取与状态展示。正式管理端策略中心预览已开始吸收qiming 运营台可从 `policy_center` 查看入口路由策略快照和拉取状态。正式管理端路由治理预览已开始吸收qiming-backend 的 `route_governance` 运营台视图会聚合 route blocked / approval required / missing context / not mapped / governance auditqiming“数字员工运营台”的“路由治理”tab 只开放标记已看、忽略和重试策略检查等低风险动作意图。 - 当前能力入口请求会按上下文、ready PlanStep、调度、查询和控制动作生成 route decision并写入 `digital_events.kind = route_decision` 与现有 event outbox路由时间线优先从 qimingclaw 本地事件读取;低风险 route decision 已开始映射为明确的本地 governance command并把 `created_command_id` 回填到 route decision自动映射前会执行低风险/受保护高风险 action guard未允许的控制动作会记录 `route_action_policy_blocked``route_action_missing_context`,避免静默创建命令;入口路由策略可从管理端远端下发,支持按 intent 阻断、按 intent 要求审批、限制自动创建 governance command并在策略干预时先记录 route decision 再停止本地命令执行;首页已展示路由干预队列和治理审计摘要,审批通过/拒绝会写入本地 approval decision 并回写管理端,管理端审批更新拉取后会自动扫描已批准 route approval 并按幂等规则恢复执行scheduler check 已接入 ready PlanStep 安全派发,首页立即执行后会触发一次派发 sweep 并展示派发结果;路由决策本地业务视图、首页摘要、治理审计、策略来源/拉取错误、阻断/待审/待干预/已恢复计数和通知联动已开始闭环。管理端正式路由 UI 预览已开始吸收“策略中心”会把入口路由策略纳入统一拉取与状态展示。正式管理端策略中心预览已开始吸收qiming 运营台可从 `policy_center` 查看入口路由策略快照和拉取状态。正式管理端路由治理预览已开始吸收qiming-backend 的 `route_governance` 运营台视图会聚合 route blocked / approval required / missing context / not mapped / governance auditqiming“数字员工运营台”的“路由治理”tab 只开放标记已看、忽略和重试策略检查等低风险动作意图。跨设备路由干预冲突可视化已开始吸收qiming-backend 的策略快照读模型会把 route blocked / approval required / missing context / not mapped 汇总为 `intervention_conflict_status` 和干预计数qiming“策略冲突”tab 可展示跨设备路由干预冲突,但不恢复执行、不绕过审批、不远程改策略。
- 后续缺口:路由策略编辑、跨设备路由干预冲突可视化和高风险恢复执行策略 UI 仍待吸收。 - 后续缺口:路由策略编辑和高风险恢复执行策略 UI 仍待吸收。
- 建议:下一轮围绕管理端正式路由 UI 和跨设备冲突可视化,把 embedded 批量干预入口推进到管理端运营台。 - 建议:下一轮围绕管理端正式路由 UI 和跨设备冲突可视化,把 embedded 批量干预入口推进到管理端运营台。
11. **诊断 / 成本 / 审计视图** 11. **诊断 / 成本 / 审计视图**