吸收数字员工技能策略开关

This commit is contained in:
baiyanyun
2026-06-09 16:28:18 +08:00
parent 9ad7881378
commit 4d8b8e4ad5
8 changed files with 249 additions and 30 deletions

View File

@@ -40,7 +40,10 @@ import {
getDigitalEmployeeSyncStatus,
} from "../services/digitalEmployee/syncService";
import { listDigitalEmployeePlanTemplates } from "../services/digitalEmployee/planTemplateService";
import { listDigitalEmployeeSkillCapabilities } from "../services/digitalEmployee/skillCatalogService";
import {
listDigitalEmployeeSkillCapabilities,
setDigitalEmployeeSkillEnabled,
} from "../services/digitalEmployee/skillCatalogService";
import { memoryService } from "../services/memory";
export function registerDigitalEmployeeHandlers(ctx: HandlerContext): void {
@@ -75,6 +78,10 @@ export function registerDigitalEmployeeHandlers(ctx: HandlerContext): void {
});
});
ipcMain.handle("digitalEmployee:setSkillEnabled", async (_, skillId: string, enabled: boolean) => {
return setDigitalEmployeeSkillEnabled(skillId, enabled);
});
ipcMain.handle("digitalEmployee:getPlanTemplates", async () => {
return listDigitalEmployeePlanTemplates();
});

View File

@@ -102,6 +102,22 @@ const mockAgent = vi.hoisted(() => ({
],
}));
const mockSettings = vi.hoisted(() => ({
values: new Map<string, unknown>(),
}));
vi.mock("../../db", () => ({
readSetting: (key: string) => mockSettings.values.get(key) ?? null,
writeSetting: (key: string, value: unknown) => {
if (value === null || value === undefined) {
mockSettings.values.delete(key);
} else {
mockSettings.values.set(key, value);
}
return true;
},
}));
vi.mock("../packages/mcp", () => ({
mcpProxyManager: {
getStatus: () => mockMcp.status,
@@ -140,6 +156,7 @@ vi.mock("../startupPorts", () => ({
describe("digital employee skill catalog service", () => {
beforeEach(() => {
vi.resetModules();
mockSettings.values = new Map<string, unknown>();
});
it("projects MCP servers and tools with governance and recent calls", async () => {
@@ -263,4 +280,35 @@ describe("digital employee skill catalog service", () => {
}),
]));
});
it("persists local skill enabled policy and applies it to catalog governance", async () => {
const {
listDigitalEmployeeSkillCapabilities,
readDigitalEmployeeSkillPolicies,
setDigitalEmployeeSkillEnabled,
} = await import("./skillCatalogService");
const update = setDigitalEmployeeSkillEnabled("qimingclaw-computer-control", false);
expect(update).toEqual(expect.objectContaining({
ok: true,
skill_id: "qimingclaw-computer-control",
enabled: false,
}));
expect(readDigitalEmployeeSkillPolicies().skills["qimingclaw-computer-control"]).toEqual(expect.objectContaining({
enabled: false,
updatedAt: expect.any(String),
}));
const result = listDigitalEmployeeSkillCapabilities();
expect(result.skills).toEqual(expect.arrayContaining([
expect.objectContaining({
skill_id: "qimingclaw-computer-control",
enabled: false,
governance: expect.objectContaining({
effective_policy: "disabled",
}),
}),
]));
});
});

View File

@@ -5,6 +5,7 @@ import { getGuiAgentServerStatus } from "../packages/guiAgentServer";
import { getWindowsMcpStatus } from "../packages/windowsMcp";
import { isWindows } from "../system/shellEnv";
import { getConfiguredPorts } from "../startupPorts";
import { readSetting, writeSetting } from "../../db";
import { readDigitalEmployeeRuntimeRecords } from "./stateService";
export interface DigitalEmployeeSkillRecentCall {
@@ -84,6 +85,25 @@ export interface DigitalEmployeeSkillCatalogOptions {
guiServerStatus?: DigitalEmployeeLocalServiceStatus | null;
}
export interface DigitalEmployeeSkillPolicyRecord {
enabled: boolean;
updatedAt: string;
}
export interface DigitalEmployeeSkillPolicies {
version: 1;
skills: Record<string, DigitalEmployeeSkillPolicyRecord>;
}
export interface DigitalEmployeeSkillPolicyUpdateResult {
ok: boolean;
skill_id: string;
enabled: boolean;
policies: DigitalEmployeeSkillPolicies;
}
const SKILL_POLICIES_SETTING_KEY = "digital_employee_skill_policies_v1";
const ACP_SKILLS: AcpSkillDefinition[] = [
{
skill_id: "qimingclaw-acp-session",
@@ -168,6 +188,7 @@ export function listDigitalEmployeeSkillCapabilities(
generatedAt: string;
skills: DigitalEmployeeSkillCapability[];
} {
const policies = readDigitalEmployeeSkillPolicies();
const status = mcpProxyManager.getStatus();
const config = mcpProxyManager.getConfig();
const runtime = readDigitalEmployeeRuntimeRecords(100);
@@ -259,10 +280,86 @@ export function listDigitalEmployeeSkillCapabilities(
return {
generatedAt: new Date().toISOString(),
skills,
skills: applySkillPolicies(skills, policies),
};
}
export function readDigitalEmployeeSkillPolicies(): DigitalEmployeeSkillPolicies {
return normalizeSkillPolicies(readSetting(SKILL_POLICIES_SETTING_KEY));
}
export function setDigitalEmployeeSkillEnabled(
skillId: string,
enabled: boolean,
): DigitalEmployeeSkillPolicyUpdateResult {
const normalizedSkillId = normalizeSkillId(skillId);
if (!normalizedSkillId) {
throw new Error("skill_id is required");
}
const current = readDigitalEmployeeSkillPolicies();
const next: DigitalEmployeeSkillPolicies = {
version: 1,
skills: {
...current.skills,
[normalizedSkillId]: {
enabled: enabled === true,
updatedAt: new Date().toISOString(),
},
},
};
writeSetting(SKILL_POLICIES_SETTING_KEY, next);
return {
ok: true,
skill_id: normalizedSkillId,
enabled: enabled === true,
policies: next,
};
}
function applySkillPolicies(
skills: DigitalEmployeeSkillCapability[],
policies: DigitalEmployeeSkillPolicies,
): DigitalEmployeeSkillCapability[] {
return skills.map((skill) => {
const policy = policies.skills[skill.skill_id];
if (!policy) return skill;
const enabled = skill.enabled && policy.enabled !== false;
return {
...skill,
enabled,
governance: {
...skill.governance,
effective_policy: enabled ? skill.governance.effective_policy : "disabled",
},
};
});
}
function normalizeSkillPolicies(value: unknown): DigitalEmployeeSkillPolicies {
const record = objectRecord(value);
const rawSkills = objectRecord(record?.skills) ?? record;
const skills: Record<string, DigitalEmployeeSkillPolicyRecord> = {};
for (const [rawSkillId, rawPolicy] of Object.entries(rawSkills ?? {})) {
const skillId = normalizeSkillId(rawSkillId);
if (!skillId) continue;
if (typeof rawPolicy === "boolean") {
skills[skillId] = { enabled: rawPolicy, updatedAt: "" };
continue;
}
const policy = objectRecord(rawPolicy);
if (!policy || typeof policy.enabled !== "boolean") continue;
skills[skillId] = {
enabled: policy.enabled,
updatedAt: stringValue(policy.updatedAt ?? policy.updated_at) || "",
};
}
return { version: 1, skills };
}
function normalizeSkillId(value: unknown): string {
return typeof value === "string" ? value.trim() : "";
}
function buildGuiAgentSkills(
events: DigitalEmployeeRuntimeEventLike[],
inputStatus?: DigitalEmployeeLocalServiceStatus | null,

View File

@@ -104,6 +104,9 @@ contextBridge.exposeInMainWorld("QimingClawBridge", {
async getSkillCapabilities() {
return ipcRenderer.invoke("digitalEmployee:getSkillCapabilities");
},
async setSkillEnabled(skillId: string, enabled: boolean) {
return ipcRenderer.invoke("digitalEmployee:setSkillEnabled", skillId, enabled);
},
async getPlanTemplates() {
return ipcRenderer.invoke("digitalEmployee:getPlanTemplates");
},