吸收数字员工跨设备路由干预冲突预览
This commit is contained in:
@@ -27,6 +27,18 @@ public class DigitalEmployeePolicySnapshotRowDto {
|
||||
private Date latestSyncedAt;
|
||||
@JsonProperty("recommended_resolution")
|
||||
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")
|
||||
private String syncRecordUrl;
|
||||
@JsonProperty("client_key_masked")
|
||||
|
||||
@@ -546,6 +546,9 @@ public class DigitalEmployeeSyncApplicationServiceImpl implements DigitalEmploye
|
||||
for (DigitalEmployeeSyncRecord record : page.getRecords()) {
|
||||
Map<String, Object> view = readJsonObject(record.getBusinessView());
|
||||
String kind = firstString(view.get("policy_kind"));
|
||||
if (StringUtils.isBlank(kind) && isRouteInterventionPolicySnapshotView(view)) {
|
||||
kind = "route_decision";
|
||||
}
|
||||
if (StringUtils.isBlank(kind)) continue;
|
||||
if (StringUtils.isNotBlank(policyKind) && !StringUtils.equals(kind, policyKind)) continue;
|
||||
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) {
|
||||
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()
|
||||
.max(Comparator.comparing(DigitalEmployeeSyncRecord::getSyncedAt, Comparator.nullsLast(Comparator.naturalOrder())))
|
||||
.orElse(records.get(0));
|
||||
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()
|
||||
.policyKind(policyKind)
|
||||
.policyKey(firstString(latestView.get("policy_key"), policyKind))
|
||||
.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())
|
||||
.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())
|
||||
.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()))
|
||||
.clientKeyMasked(maskClientKey(latest.getClientKey()))
|
||||
.deviceId(latest.getDeviceId())
|
||||
@@ -593,6 +614,47 @@ public class DigitalEmployeeSyncApplicationServiceImpl implements DigitalEmploye
|
||||
.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) {
|
||||
Map<String, Object> signature = new LinkedHashMap<>();
|
||||
for (String key : policyConflictCandidateKeys()) {
|
||||
|
||||
@@ -1460,6 +1460,86 @@ class DigitalEmployeeSyncApplicationServiceImplTest {
|
||||
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
|
||||
void recordDailyReportSendResultStoresRecordedObservation() {
|
||||
DigitalEmployeeAdminRecordRequestDto request = adminRecordRequest("daily_report", "report-1");
|
||||
|
||||
@@ -119,6 +119,7 @@ function renderSummary(record: DigitalEmployeeAdminWorkbenchRow) {
|
||||
record.cleanup_request_id && `Cleanup ${record.cleanup_request_id}`,
|
||||
record.route_decision_id && `Route ${record.route_decision_id}`,
|
||||
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.daily_report_fact_id && `ReportFact ${record.daily_report_fact_id}`,
|
||||
record.memory_id && `Memory ${record.memory_id}`,
|
||||
@@ -675,6 +676,12 @@ const DigitalEmployeeOps: React.FC = () => {
|
||||
policy_key: record.policy_key,
|
||||
conflict_keys: record.conflict_keys,
|
||||
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,
|
||||
synced_at: record.latest_synced_at,
|
||||
client_key_masked: record.client_key_masked,
|
||||
@@ -833,12 +840,28 @@ const DigitalEmployeeOps: React.FC = () => {
|
||||
{detailRecord.conflict_keys?.length
|
||||
? 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 && (
|
||||
<Typography.Text type="secondary">
|
||||
{' '}
|
||||
· 建议:{detailRecord.recommended_resolution}
|
||||
</Typography.Text>
|
||||
)}
|
||||
<Typography.Text type="secondary">
|
||||
{' '}
|
||||
· 不恢复执行、不绕过审批、不远程改策略。
|
||||
</Typography.Text>
|
||||
</Typography.Paragraph>
|
||||
)}
|
||||
{activeView === 'approval_governance' && (
|
||||
|
||||
@@ -629,6 +629,12 @@ export interface DigitalEmployeeAdminWorkbenchRow {
|
||||
revision_id?: string;
|
||||
conflict_keys?: 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;
|
||||
synced_at?: string;
|
||||
latest_synced_at?: string;
|
||||
@@ -672,6 +678,12 @@ export interface DigitalEmployeePolicySnapshotRow {
|
||||
conflict_keys?: string[];
|
||||
latest_synced_at?: 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;
|
||||
client_key_masked?: string;
|
||||
device_id?: string;
|
||||
|
||||
@@ -1191,6 +1191,55 @@ function checkDigitalAdminPolicySnapshots() {
|
||||
const qimingOpsPath = path.join(qimingRoot, "src", "pages", "SystemManagement", "Content", "DigitalEmployeeOps", "index.tsx");
|
||||
const qimingServicePath = path.join(qimingRoot, "src", "services", "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(
|
||||
backendRoot,
|
||||
"app-platform-modules",
|
||||
@@ -1213,6 +1262,9 @@ function checkDigitalAdminPolicySnapshots() {
|
||||
const qimingOps = fs.readFileSync(qimingOpsPath, "utf8");
|
||||
const qimingService = fs.readFileSync(qimingServicePath, "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 requiredSnippets = [
|
||||
[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-risk-policy-pull", "risk policy snapshot sync test"],
|
||||
[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, "/api/digital-employee/admin/policies/snapshots", "qiming policy snapshot endpoint"],
|
||||
[qimingTypes, "DigitalEmployeePolicySnapshotRow", "qiming policy snapshot row 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, "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 route intervention conflict absorption"],
|
||||
];
|
||||
const missing = requiredSnippets
|
||||
.filter(([content, snippet]) => !content.includes(snippet))
|
||||
|
||||
@@ -599,8 +599,8 @@ GET /api/digital-employee/approvals
|
||||
|
||||
10. **入口路由 / 任务分流(路由业务视图与通知联动已开始)**
|
||||
- 已吸收:管理端通过 `/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 audit,qiming“数字员工运营台”的“路由治理”tab 只开放标记已看、忽略和重试策略检查等低风险动作意图。
|
||||
- 后续缺口:路由策略编辑、跨设备路由干预冲突可视化和高风险恢复执行策略 UI 仍待吸收。
|
||||
- 当前能力:入口请求会按上下文、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 audit,qiming“数字员工运营台”的“路由治理”tab 只开放标记已看、忽略和重试策略检查等低风险动作意图。跨设备路由干预冲突可视化已开始吸收,qiming-backend 的策略快照读模型会把 route blocked / approval required / missing context / not mapped 汇总为 `intervention_conflict_status` 和干预计数,qiming“策略冲突”tab 可展示跨设备路由干预冲突,但不恢复执行、不绕过审批、不远程改策略。
|
||||
- 后续缺口:路由策略编辑和高风险恢复执行策略 UI 仍待吸收。
|
||||
- 建议:下一轮围绕管理端正式路由 UI 和跨设备冲突可视化,把 embedded 批量干预入口推进到管理端运营台。
|
||||
|
||||
11. **诊断 / 成本 / 审计视图**
|
||||
|
||||
Reference in New Issue
Block a user