feat: make agent roles and skills dynamic

This commit is contained in:
2026-07-11 12:55:05 +08:00
parent 17c0536958
commit d36b00bbec
48 changed files with 1389 additions and 1749 deletions
+157
View File
@@ -0,0 +1,157 @@
import { readFile } from "node:fs/promises";
import { prisma } from "../db.js";
import { OrganizationAgentConfiguration } from "../agent/configuration.js";
import { readSkillStoreRoot, verifyStoredSkill } from "../agent/skillStore.js";
import { readSiloOrganizationId } from "./silo.js";
async function main(argv: readonly string[]): Promise<void> {
const [command, ...args] = argv;
if (command === undefined || command === "help" || command === "--help") {
printHelp();
return;
}
const options = parseOptions(args);
const organizationId = required(options, "organization");
const siloOrganizationId = readSiloOrganizationId();
if (organizationId !== siloOrganizationId) {
throw new Error(`Silo Agent configuration is restricted to ${siloOrganizationId}`);
}
const configuration = new OrganizationAgentConfiguration(prisma, readSkillStoreRoot());
switch (command) {
case "install-skill": {
const installed = await configuration.installSkill({
organizationId,
sourceDir: required(options, "source"),
version: required(options, "version"),
});
console.log(JSON.stringify(installed));
return;
}
case "upsert-role": {
const systemPromptFile = options.get("system-prompt-file");
const toolsJson = options.get("tools-json");
const tools = toolsJson === undefined
? undefined
: toolsJson === "null"
? null
: parseTools(toolsJson);
const sortOrderRaw = options.get("sort-order");
const role = await configuration.upsertRole({
organizationId,
roleId: required(options, "role"),
label: required(options, "label"),
...(options.has("model") ? { defaultModel: options.get("model") ?? null } : {}),
...(systemPromptFile !== undefined
? { systemPrompt: await readFile(systemPromptFile, "utf8") }
: {}),
...(tools !== undefined ? { tools } : {}),
...(sortOrderRaw !== undefined ? { sortOrder: integer(sortOrderRaw, "sort-order") } : {}),
});
console.log(JSON.stringify(role));
return;
}
case "set-role-skills": {
const skills = required(options, "skills").split(",").map((name) => name.trim()).filter(Boolean);
await configuration.setRoleSkills({
organizationId,
roleId: required(options, "role"),
skillNames: skills,
});
console.log(JSON.stringify({ roleId: required(options, "role"), skills }));
return;
}
case "list": {
const roles = await prisma.organizationAgentRole.findMany({
where: { organizationId },
orderBy: [{ sortOrder: "asc" }, { roleId: "asc" }],
include: {
skillBindings: {
orderBy: [{ sortOrder: "asc" }, { agentSkillId: "asc" }],
include: { skill: { select: { name: true, version: true, disabledAt: true } } },
},
},
});
console.log(JSON.stringify(roles.map((role) => ({
roleId: role.roleId,
label: role.label,
defaultModel: role.defaultModel,
systemPromptConfigured: role.systemPrompt !== null,
tools: role.tools,
disabled: role.disabledAt !== null,
skills: role.skillBindings.map((binding) => ({
name: binding.skill.name,
version: binding.skill.version,
disabled: binding.skill.disabledAt !== null,
})),
})), null, 2));
return;
}
case "verify-store": {
const skills = await prisma.organizationAgentSkill.findMany({
where: { organizationId, disabledAt: null },
select: { name: true, contentDigest: true },
});
for (const skill of skills) {
await verifyStoredSkill({ storeRoot: readSkillStoreRoot(), ...skill });
}
console.log(JSON.stringify({ verifiedSkills: skills.length }));
return;
}
default:
throw new Error(`unknown Agent configuration command: ${command}`);
}
}
function parseOptions(args: readonly string[]): Map<string, string> {
const options = new Map<string, string>();
for (let index = 0; index < args.length; index += 2) {
const flag = args[index];
const value = args[index + 1];
if (flag === undefined || !flag.startsWith("--") || value === undefined) {
throw new Error(`expected --name value, got: ${args.slice(index).join(" ")}`);
}
const name = flag.slice(2);
if (options.has(name)) throw new Error(`duplicate option: --${name}`);
options.set(name, value);
}
return options;
}
function required(options: ReadonlyMap<string, string>, name: string): string {
const value = options.get(name)?.trim();
if (value === undefined || value === "") throw new Error(`--${name} is required`);
return value;
}
function parseTools(raw: string): readonly string[] {
const parsed = JSON.parse(raw) as unknown;
if (!Array.isArray(parsed) || parsed.some((value) => typeof value !== "string")) {
throw new Error("--tools-json must be null or a JSON string array");
}
return parsed;
}
function integer(raw: string, name: string): number {
const value = Number(raw);
if (!Number.isSafeInteger(value)) throw new Error(`--${name} must be an integer`);
return value;
}
function printHelp(): void {
console.log(`Usage:
agent-config install-skill --organization ORG --source DIR --version VERSION
agent-config upsert-role --organization ORG --role ID --label LABEL [--model MODEL] [--system-prompt-file FILE] [--tools-json JSON] [--sort-order N]
agent-config set-role-skills --organization ORG --role ID --skills name,name
agent-config list --organization ORG
agent-config verify-store --organization ORG`);
}
main(process.argv.slice(2))
.catch((error) => {
console.error(error instanceof Error ? error.message : String(error));
process.exitCode = 1;
})
.finally(async () => {
await prisma.$disconnect();
});
+16
View File
@@ -226,6 +226,22 @@ async function initializeSilo(
const count = await tx.organization.count();
if (count !== 0) throw new Error(`Silo bootstrap requires an empty Organization set; found ${count}`);
await tx.organization.create({ data: { ...input.organization } });
await tx.organizationAgentRole.createMany({
data: [
{
organizationId: input.organization.id,
roleId: "draft",
label: "草稿",
sortOrder: 10,
},
{
organizationId: input.organization.id,
roleId: "review",
label: "审校",
sortOrder: 20,
},
],
});
await tx.organizationProjectSettings.create({
data: { organizationId: input.organization.id, membersCanCreateProjects: true },
});