吸收数字员工管理端记忆技能审计预览

This commit is contained in:
baiyanyun
2026-06-13 14:21:16 +08:00
parent b6b0a9dcb3
commit de501c0252
7 changed files with 318 additions and 5 deletions

View File

@@ -45,6 +45,16 @@ public class DigitalEmployeeAdminWorkbenchRowDto {
private String routeDecisionId;
@JsonProperty("service_id")
private String serviceId;
@JsonProperty("memory_id")
private String memoryId;
@JsonProperty("memory_key")
private String memoryKey;
@JsonProperty("skill_id")
private String skillId;
@JsonProperty("tool_name")
private String toolName;
@JsonProperty("server_id")
private String serverId;
@JsonProperty("occurred_at")
private String occurredAt;
@JsonProperty("synced_at")

View File

@@ -70,6 +70,8 @@ public class DigitalEmployeeSyncApplicationServiceImpl implements DigitalEmploye
"notification:mark_read", "notification:dismiss",
"artifact:mark_keep", "artifact:mark_expire", "artifact:mark_reviewed",
"daily_report:mark_delivered", "daily_report:mark_confirmed", "daily_report:request_approval",
"memory:archive_memory", "memory:restore_memory", "memory:mark_reviewed",
"skill:mark_reviewed", "skill:dismiss",
"audit:mark_reviewed", "audit:dismiss",
"event:mark_reviewed", "event:dismiss"
);
@@ -980,6 +982,11 @@ public class DigitalEmployeeSyncApplicationServiceImpl implements DigitalEmploye
.reportId(firstString(businessView.get("report_id"), payload.get("report_id")))
.routeDecisionId(firstString(businessView.get("route_decision_id"), payload.get("route_decision_id")))
.serviceId(firstString(businessView.get("service_id"), businessView.get("serviceId"), payload.get("service_id"), payload.get("serviceId")))
.memoryId(firstString(businessView.get("memory_id"), businessView.get("memoryId"), payload.get("memory_id"), payload.get("memoryId"), "memory".equals(record.getEntityType()) ? record.getEntityId() : null))
.memoryKey(firstString(businessView.get("memory_key"), businessView.get("memoryKey"), businessView.get("key"), payload.get("memory_key"), payload.get("memoryKey"), payload.get("key")))
.skillId(firstString(businessView.get("skill_id"), businessView.get("skillId"), payload.get("skill_id"), payload.get("skillId")))
.toolName(firstString(businessView.get("tool_name"), businessView.get("toolName"), payload.get("tool_name"), payload.get("toolName")))
.serverId(firstString(businessView.get("server_id"), businessView.get("serverId"), payload.get("server_id"), payload.get("serverId")))
.occurredAt(businessRow.getOccurredAt())
.syncedAt(record.getSyncedAt())
.clientKeyMasked(maskClientKey(record.getClientKey()))
@@ -1005,6 +1012,8 @@ public class DigitalEmployeeSyncApplicationServiceImpl implements DigitalEmploye
case "daily_reports" -> isDailyReportRow(row);
case "governance_commands" -> isGovernanceCommandRow(row);
case "route_governance" -> isRouteGovernanceRow(row);
case "memory_governance" -> isMemoryGovernanceRow(row);
case "skill_audits" -> isSkillAuditRow(row);
default -> true;
};
}
@@ -1087,6 +1096,31 @@ public class DigitalEmployeeSyncApplicationServiceImpl implements DigitalEmploye
|| StringUtils.isNotBlank(row.getRouteDecisionId());
}
private boolean isMemoryGovernanceRow(DigitalEmployeeAdminWorkbenchRowDto row) {
String category = firstString(row.getCategory());
String kind = firstString(row.getKind());
return StringUtils.equals(row.getEntityType(), "memory")
|| StringUtils.equals(category, "memory")
|| StringUtils.equals(category, "memory_governance")
|| startsWithAny(kind, "memory_")
|| StringUtils.isNotBlank(row.getMemoryId())
|| StringUtils.isNotBlank(row.getMemoryKey());
}
private boolean isSkillAuditRow(DigitalEmployeeAdminWorkbenchRowDto row) {
String category = firstString(row.getCategory());
String kind = firstString(row.getKind());
return StringUtils.equals(category, "skill")
|| StringUtils.equals(category, "skill_call_audit")
|| startsWithAny(kind, "skill_call_")
|| StringUtils.equals(category, "tool_call_audit")
|| StringUtils.equals(category, "sandbox_audit")
|| StringUtils.equals(category, "token_cost")
|| StringUtils.isNotBlank(row.getSkillId())
|| StringUtils.isNotBlank(row.getToolName())
|| StringUtils.isNotBlank(row.getServerId());
}
private String adminCategory(DigitalEmployeeBusinessViewRowDto row, Map<String, Object> businessView, Map<String, Object> payload) {
if (booleanValue(businessView.get("approval_conflict_active"))) return "approval_conflict";
String kind = firstString(row.getKind(), businessView.get("kind"), payload.get("kind"));
@@ -1104,6 +1138,8 @@ public class DigitalEmployeeSyncApplicationServiceImpl implements DigitalEmploye
}
if (startsWithAny(kind, "artifact_")) return "artifact_lifecycle";
if (startsWithAny(kind, "daily_report")) return "daily_report";
if (startsWithAny(kind, "memory_")) return "memory_governance";
if (startsWithAny(kind, "skill_call_")) return "skill_call_audit";
if (startsWithAny(kind, "governance_")) return "governance";
if (contains(kind, "audit")) return kind;
return row.getEntityType();

View File

@@ -349,6 +349,118 @@ class DigitalEmployeeSyncApplicationServiceImplTest {
assertThat(result.getRecords().get(3).getApprovalId()).isEqualTo("approval-1");
}
@Test
void queryAdminWorkbenchMemoryGovernanceReturnsMemoryRowsOnly() {
when(repository.queryBusinessRecords(eq(101L), eq(null), eq(null), eq(null), eq(null), eq(null), eq(null), eq(null), eq(1L), eq(20L)))
.thenReturn(page(List.of(
record("outbox-memory", "memory", "memory-1", 101L, new Date(10_000),
Map.of(
"entity_type", "memory",
"title", "客户偏好记忆",
"status", "active",
"memory_id", "memory-1",
"memory_key", "customer.preference",
"governance_status", "active",
"merge_candidate_count", 2,
"duplicate_memory_ids", List.of("memory-2")
),
Map.of("id", "memory-1", "key", "customer.preference")),
record("outbox-memory-archive", "event", "event-memory-1", 101L, new Date(11_000),
Map.of(
"entity_type", "event",
"category", "memory_governance",
"kind", "memory_archived",
"status", "recorded",
"title", "记忆已归档",
"memory_id", "memory-3",
"memory_key", "customer.old",
"last_governance_action", "archive"
),
Map.of("id", "event-memory-1", "kind", "memory_archived")),
record("outbox-task", "task", "task-1", 101L, new Date(12_000),
Map.of("entity_type", "task", "title", "普通任务", "status", "running"),
Map.of("id", "task-1", "status", "running")),
record("outbox-audit", "event", "event-audit-1", 101L, new Date(13_000),
Map.of("entity_type", "event", "category", "tool_call_audit", "title", "工具审计", "status", "warn"),
Map.of("id", "event-audit-1", "status", "warn"))
)));
DigitalEmployeeAdminWorkbenchPageDto result = service.queryAdminWorkbench(
"memory_governance", null, null, null, null, null, null, null, null, null, null, null, 1L, 20L
);
assertThat(result.getTotal()).isEqualTo(2);
assertThat(result.getRecords()).extracting(DigitalEmployeeAdminWorkbenchRowDto::getEntityId)
.containsExactly("memory-1", "event-memory-1");
assertThat(result.getRecords().get(0).getView()).isEqualTo("memory_governance");
assertThat(result.getRecords().get(0).getMemoryId()).isEqualTo("memory-1");
assertThat(result.getRecords().get(0).getMemoryKey()).isEqualTo("customer.preference");
assertThat(result.getRecords().get(0).getBusinessView()).containsEntry("merge_candidate_count", 2);
assertThat(result.getRecords().get(1).getCategory()).isEqualTo("memory_governance");
}
@Test
void queryAdminWorkbenchSkillAuditsReturnsSkillAndToolAuditRowsOnly() {
when(repository.queryBusinessRecords(eq(101L), eq(null), eq(null), eq(null), eq(null), eq(null), eq(null), eq(null), eq(1L), eq(20L)))
.thenReturn(page(List.of(
record("outbox-skill-allowed", "event", "event-skill-1", 101L, new Date(10_000),
Map.of(
"entity_type", "event",
"category", "skill",
"kind", "skill_call_allowed",
"status", "allowed",
"title", "技能调用允许",
"skill_id", "skill-web",
"tool_name", "browser.open",
"server_id", "mcp-browser",
"decision", "allowed",
"risk_level", "low"
),
Map.of("id", "event-skill-1", "kind", "skill_call_allowed")),
record("outbox-tool-audit", "event", "event-skill-2", 101L, new Date(11_000),
Map.of(
"entity_type", "event",
"category", "tool_call_audit",
"kind", "tool_call_audit",
"status", "rejected",
"title", "工具调用拒绝",
"tool_name", "shell",
"server_id", "local",
"decision", "rejected"
),
Map.of("id", "event-skill-2", "kind", "tool_call_audit")),
record("outbox-token-cost", "event", "event-skill-3", 101L, new Date(12_000),
Map.of(
"entity_type", "event",
"category", "token_cost",
"kind", "token_cost_recorded",
"status", "recorded",
"title", "Token 成本估算",
"model", "gpt-4.1",
"total_tokens", 180,
"estimated_cost_usd", 0.012
),
Map.of("id", "event-skill-3", "kind", "token_cost_recorded")),
record("outbox-notification", "notification", "notification-1", 101L, new Date(13_000),
Map.of("entity_type", "notification", "title", "普通通知", "status", "unread"),
Map.of("id", "notification-1", "status", "unread"))
)));
DigitalEmployeeAdminWorkbenchPageDto result = service.queryAdminWorkbench(
"skill_audits", null, null, null, null, null, null, null, null, null, null, null, 1L, 20L
);
assertThat(result.getTotal()).isEqualTo(3);
assertThat(result.getRecords()).extracting(DigitalEmployeeAdminWorkbenchRowDto::getEntityId)
.containsExactly("event-skill-1", "event-skill-2", "event-skill-3");
assertThat(result.getRecords().get(0).getView()).isEqualTo("skill_audits");
assertThat(result.getRecords().get(0).getSkillId()).isEqualTo("skill-web");
assertThat(result.getRecords().get(0).getToolName()).isEqualTo("browser.open");
assertThat(result.getRecords().get(0).getServerId()).isEqualTo("mcp-browser");
assertThat(result.getRecords().get(1).getCategory()).isEqualTo("tool_call_audit");
assertThat(result.getRecords().get(2).getBusinessView()).containsEntry("total_tokens", 180);
}
@Test
void queryAdminWorkbenchServiceOperationsReturnsServiceHealthAndRestartRowsOnly() {
when(repository.queryBusinessRecords(eq(101L), eq(null), eq(null), eq(null), eq(null), eq(null), eq(null), eq(null), eq(1L), eq(20L)))

View File

@@ -47,6 +47,8 @@ const WORKBENCH_TABS: Array<{
{ key: 'governance_commands', label: '治理命令' },
{ key: 'route_governance', label: '路由治理' },
{ key: 'policy_conflicts', label: '策略冲突' },
{ key: 'memory_governance', label: '记忆治理' },
{ key: 'skill_audits', label: '技能审计' },
];
const ENTITY_TYPE_OPTIONS = {
@@ -107,6 +109,9 @@ function renderSummary(record: DigitalEmployeeAdminWorkbenchRow) {
record.artifact_id && `Artifact ${record.artifact_id}`,
record.route_decision_id && `Route ${record.route_decision_id}`,
record.policy_kind && `Policy ${record.policy_kind}`,
record.memory_id && `Memory ${record.memory_id}`,
record.skill_id && `Skill ${record.skill_id}`,
record.tool_name && `Tool ${record.tool_name}`,
].filter((item): item is string => Boolean(item));
return (
@@ -178,6 +183,12 @@ function actionTarget(record: DigitalEmployeeAdminWorkbenchRow, view: DigitalEmp
entityId: record.policy_key || record.policy_kind,
};
}
if (view === 'memory_governance') {
return { entityType: 'memory', entityId: record.memory_id || record.entity_id };
}
if (view === 'skill_audits') {
return { entityType: 'skill', entityId: record.skill_id || record.tool_name || record.entity_id };
}
return { entityType: record.entity_type, entityId: record.entity_id };
}
@@ -252,6 +263,19 @@ function allowedActions(record: DigitalEmployeeAdminWorkbenchRow, view: DigitalE
{ action: 'dismiss', label: '忽略' },
];
}
if (view === 'memory_governance') {
return [
{ action: 'archive_memory', label: '批量归档' },
{ action: 'restore_memory', label: '批量恢复' },
{ action: 'mark_reviewed', label: '标记复核' },
];
}
if (view === 'skill_audits') {
return [
{ action: 'mark_reviewed', label: '标记复核' },
{ action: 'dismiss', label: '忽略' },
];
}
return [];
}
@@ -715,6 +739,11 @@ const DigitalEmployeeOps: React.FC = () => {
detailRecord.route_decision_id &&
`Route ${detailRecord.route_decision_id}`,
detailRecord.policy_kind && `Policy ${detailRecord.policy_kind}`,
detailRecord.memory_id && `Memory ${detailRecord.memory_id}`,
detailRecord.memory_key && `MemoryKey ${detailRecord.memory_key}`,
detailRecord.skill_id && `Skill ${detailRecord.skill_id}`,
detailRecord.tool_name && `Tool ${detailRecord.tool_name}`,
detailRecord.server_id && `Server ${detailRecord.server_id}`,
]
.filter(Boolean)
.join(' / ') || '--'}
@@ -770,6 +799,28 @@ const DigitalEmployeeOps: React.FC = () => {
</Typography.Text>
</Typography.Paragraph>
)}
{activeView === 'memory_governance' && (
<Typography.Paragraph>
<Typography.Text strong></Typography.Text>
{detailRecord.memory_key || detailRecord.memory_id || detailRecord.entity_id || '--'}
{detailRecord.status && ` · ${detailRecord.status}`}
<Typography.Text type="secondary">
{' '}
· /
</Typography.Text>
</Typography.Paragraph>
)}
{activeView === 'skill_audits' && (
<Typography.Paragraph>
<Typography.Text strong></Typography.Text>
{detailRecord.skill_id || detailRecord.tool_name || detailRecord.category || '--'}
{detailRecord.status && ` · ${detailRecord.status}`}
<Typography.Text type="secondary">
{' '}
·
</Typography.Text>
</Typography.Paragraph>
)}
{allowedActions(detailRecord, activeView).length > 0 && (
<div style={{ marginBottom: 16 }}>
<Typography.Title level={5}></Typography.Title>

View File

@@ -567,7 +567,9 @@ export type DigitalEmployeeAdminWorkbenchView =
| 'daily_reports'
| 'governance_commands'
| 'route_governance'
| 'policy_conflicts';
| 'policy_conflicts'
| 'memory_governance'
| 'skill_audits';
export interface DigitalEmployeeAdminWorkbenchRow {
view: DigitalEmployeeAdminWorkbenchView | string;
@@ -592,6 +594,11 @@ export interface DigitalEmployeeAdminWorkbenchRow {
report_id?: string;
route_decision_id?: string;
service_id?: string;
memory_id?: string;
memory_key?: string;
skill_id?: string;
tool_name?: string;
server_id?: string;
policy_kind?: string;
policy_key?: string;
conflict_keys?: string[];

View File

@@ -1449,6 +1449,102 @@ function checkDigitalAdminApprovalGovernanceWorkbench() {
console.log("[DigitalEmployeeCheck] admin approval governance workbench hooks OK");
}
function checkDigitalAdminMemorySkillWorkbench() {
console.log("\n[DigitalEmployeeCheck] Verify admin memory governance and skill audit workbench hooks");
const backendRoot = path.resolve(packageRoot, "..", "..", "..", "qiming-backend");
const qimingRoot = path.resolve(packageRoot, "..", "..", "..", "qiming");
const servicePath = 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 dtoPath = path.join(
backendRoot,
"app-platform-modules",
"app-platform-agent",
"app-platform-agent-core-adapter",
"src",
"main",
"java",
"com",
"xspaceagi",
"agent",
"core",
"adapter",
"dto",
"digital",
"DigitalEmployeeAdminWorkbenchRowDto.java",
);
const testPath = 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 qimingOpsPath = path.join(qimingRoot, "src", "pages", "SystemManagement", "Content", "DigitalEmployeeOps", "index.tsx");
const qimingTypesPath = path.join(qimingRoot, "src", "types", "interfaces", "systemManage.ts");
const docsPath = path.resolve(packageRoot, "..", "..", "docs", "digital-employee-frontend-integration-plan.md");
const service = fs.readFileSync(servicePath, "utf8");
const dto = fs.readFileSync(dtoPath, "utf8");
const test = fs.readFileSync(testPath, "utf8");
const qimingOps = fs.readFileSync(qimingOpsPath, "utf8");
const qimingTypes = fs.readFileSync(qimingTypesPath, "utf8");
const docs = fs.readFileSync(docsPath, "utf8");
const requiredSnippets = [
[service, "memory_governance", "backend memory governance view"],
[service, "skill_audits", "backend skill audits view"],
[service, "isMemoryGovernanceRow", "backend memory governance matcher"],
[service, "isSkillAuditRow", "backend skill audit matcher"],
[service, "skill_call_", "backend skill call classifier"],
[service, "tool_call_audit", "backend tool audit classifier"],
[service, "token_cost", "backend token cost classifier"],
[dto, "memory_id", "backend memory id row field"],
[dto, "memory_key", "backend memory key row field"],
[dto, "skill_id", "backend skill id row field"],
[dto, "tool_name", "backend tool name row field"],
[dto, "server_id", "backend server id row field"],
[test, "queryAdminWorkbenchMemoryGovernanceReturnsMemoryRowsOnly", "backend memory governance test"],
[test, "queryAdminWorkbenchSkillAuditsReturnsSkillAndToolAuditRowsOnly", "backend skill audits test"],
[qimingTypes, "memory_governance", "qiming memory governance view type"],
[qimingTypes, "skill_audits", "qiming skill audits view type"],
[qimingOps, "记忆治理", "qiming memory governance tab"],
[qimingOps, "技能审计", "qiming skill audits tab"],
[qimingOps, "批量归档", "qiming memory archive action"],
[qimingOps, "批量恢复", "qiming memory restore action"],
[qimingOps, "重复候选", "qiming memory duplicate copy"],
[docs, "正式管理端记忆治理预览", "docs memory governance absorption"],
[docs, "正式管理端技能审计预览", "docs skill audit absorption"],
];
const missing = requiredSnippets
.filter(([content, snippet]) => !content.includes(snippet))
.map(([, , label]) => label);
if (missing.length > 0) {
throw new Error(`Missing admin memory governance and skill audit hooks: ${missing.join(", ")}`);
}
console.log("[DigitalEmployeeCheck] admin memory governance and skill audit hooks OK");
}
function checkDigitalAdminServiceOperationsWorkbench() {
console.log("\n[DigitalEmployeeCheck] Verify admin service operations workbench hooks");
const backendRoot = path.resolve(packageRoot, "..", "..", "..", "qiming-backend");
@@ -2053,6 +2149,7 @@ function main() {
checkDigitalBackendMaterializedFacts();
checkDigitalAdminRecordBillingArchiveShell();
checkDigitalAdminApprovalGovernanceWorkbench();
checkDigitalAdminMemorySkillWorkbench();
checkDigitalAdminServiceOperationsWorkbench();
checkDigitalAdminRouteGovernanceWorkbench();
checkDigitalAdminNotificationWorkbench();

View File

@@ -562,8 +562,8 @@ GET /api/digital-employee/approvals
3. **MemoryEntry / 长期记忆(已开始)**
- 已吸收qimingclaw `memoryService` 已投射为数字员工本地 `digital_memories`,支持从主客户端记忆列表同步、桥接新增和软删除;本地兜底记忆仍可在 memory 服务未初始化时工作。
- 当前能力embedded `/api/memory` 已由 qimingclaw adapter 接管GET / POST / DELETE 会通过 preload bridge 读写 `digital_memories`;记忆新增、更新和删除会进入 `digital_sync_outbox`entity type 为 `memory`,同步业务视图会提炼 key、category、source、status 和 content preview。记忆归档/恢复 UI、管理端长期记忆业务视图和记忆治理审计链已开始吸收embedded 新增“记忆治理”入口,可从 runtime memories 派生 key/category/source/status/content preview、重复候选和同步状态并通过本地 overlay/audit 批量归档、恢复或标记复核。
- 后续缺口:真实合并执行、远端记忆策略下发和外部管理端长期记忆工作台仍待吸收。
- 当前能力embedded `/api/memory` 已由 qimingclaw adapter 接管GET / POST / DELETE 会通过 preload bridge 读写 `digital_memories`;记忆新增、更新和删除会进入 `digital_sync_outbox`entity type 为 `memory`,同步业务视图会提炼 key、category、source、status 和 content preview。记忆归档/恢复 UI、管理端长期记忆业务视图和记忆治理审计链已开始吸收embedded 新增“记忆治理”入口,可从 runtime memories 派生 key/category/source/status/content preview、重复候选和同步状态并通过本地 overlay/audit 批量归档、恢复或标记复核。正式管理端记忆治理预览已开始吸收qiming-backend 新增 `memory_governance` 运营台视图聚合 memory fact、归档/恢复/复核事件和重复候选摘要qiming“数字员工运营台”的“记忆治理”tab 可记录批量归档、批量恢复和标记复核等低风险动作意图。
- 后续缺口:真实合并执行、远端记忆策略下发和更完整的外部管理端长期记忆策略工作台仍待吸收。
- 建议:下一轮围绕管理端视图和记忆治理,把长期记忆从本地事实继续扩展到人工可审计、可归档、可合并的运营闭环。
4. **治理命令完整生命周期**
@@ -578,8 +578,8 @@ GET /api/digital-employee/approvals
- 建议:先沉淀安全重启审计和人工确认体验,再评估更细粒度的服务恢复策略。
6. **技能生命周期与策略**
- 已吸收ACP / MCP / GUI / file server 能力已投射到 Skill Catalog技能库不再使用 demo fallbackSkill Library 启用/禁用已写入 qimingclaw 本地 `digital_employee_skill_policies_v1.local_skills`,管理端下发策略写入 `remote_skills`远端禁用优先生效且不会被本地按钮覆盖入口路由候选技能会按有效策略过滤MCP 注入 Agent 前会应用有效技能策略ACP permission / tool update 会写入 `skill_call_allowed` / `skill_call_rejected` / `skill_call_observed` 审计事件,`policy_snapshot` 会包含本地/远端策略命中字段embedded 已提供 `/api/digital-employee/skill-call-audits` 只读投影、`POST /api/skill/policies/pull` 手动拉取入口技能调用策略与审计已开始闭环。管理端调用审计视图已开始吸收embedded 新增“技能审计”入口,将 skill call audit、tool_call_audit、sandbox_audit 和 token_cost 统一成可筛选列表,技能库可按 skill id 跳转聚焦。
- 缺口:仍缺少技能安装、版本管理细粒度权限范围和正式外部管理端调用审计工作台
- 已吸收ACP / MCP / GUI / file server 能力已投射到 Skill Catalog技能库不再使用 demo fallbackSkill Library 启用/禁用已写入 qimingclaw 本地 `digital_employee_skill_policies_v1.local_skills`,管理端下发策略写入 `remote_skills`远端禁用优先生效且不会被本地按钮覆盖入口路由候选技能会按有效策略过滤MCP 注入 Agent 前会应用有效技能策略ACP permission / tool update 会写入 `skill_call_allowed` / `skill_call_rejected` / `skill_call_observed` 审计事件,`policy_snapshot` 会包含本地/远端策略命中字段embedded 已提供 `/api/digital-employee/skill-call-audits` 只读投影、`POST /api/skill/policies/pull` 手动拉取入口技能调用策略与审计已开始闭环。管理端调用审计视图已开始吸收embedded 新增“技能审计”入口,将 skill call audit、tool_call_audit、sandbox_audit 和 token_cost 统一成可筛选列表,技能库可按 skill id 跳转聚焦。正式管理端技能审计预览已开始吸收qiming-backend 新增 `skill_audits` 运营台视图聚合 skill call、tool、sandbox 和 token/cost 安全摘要qiming“数字员工运营台”的“技能审计”tab 只开放标记复核和忽略意图,不安装技能、不变更权限范围。
- 缺口:仍缺少技能安装、版本管理细粒度权限范围。
- 建议:下一轮围绕管理端审计消费、技能安装和权限范围,把本地调用审计推进到更完整的跨端治理闭环。
7. **产物交付闭环**