forked from bai/curriculum-project-hub
feat(admin): restore org Agent role/skill management and fix 404 after project create
- explorer POST /projects now returns {id,name} matching the SPA contract
(previously returned ProjectOnboardingResult.projectId, so res.id was
undefined and the redirect to /projects/undefined 404'd)
- add OrganizationAgentConfiguration.listRoles/listSkills + AgentRoleRow/
AgentSkillRow exports; upsertRole now returns the full row
- new agentConfigRoutes: GET/PUT /agent-roles, PUT /agent-roles/:id/skills,
GET /agent-skills, GET /agent-models (env-default picker)
- restore admin-web roles page + RoleCard rewired to ADR-0017/0018 backend
(label, defaultModel, tools whitelist, skill binding, systemPrompt,
sortOrder, default toggle); add 角色 nav item + roles icon
- skill installation stays out-of-band (CLI/seed) per spec; the surface only
lists installed skills and binds them to roles
This commit is contained in:
@@ -0,0 +1,129 @@
|
||||
/**
|
||||
* Org-admin Agent configuration routes (ADR-0017 / ADR-0018).
|
||||
*
|
||||
* Surface for browsing and editing Organization-scoped Agent roles and the
|
||||
* skills bound to them. Roles are org-owned data; the default-model picker is
|
||||
* constrained to the env-default model registry (ADR-0017: there is no
|
||||
* org-scoped model list — `OrganizationAgentRole.defaultModel` selects from the
|
||||
* platform-enabled set). Skill *installation* is out of band (CLI / seed) per
|
||||
* spec `Skill 管理是 org-scoped; 存储机制 OPEN`; this route only lists installed
|
||||
* skills and binds them to roles.
|
||||
*/
|
||||
import type { PrismaClient } from "@prisma/client";
|
||||
import type { FastifyInstance } from "fastify";
|
||||
import { OrganizationAgentConfiguration } from "../../agent/configuration.js";
|
||||
import { createDefaultModelRegistry } from "../../settings/runtime.js";
|
||||
import { requireOrgRole, type GuardDeps } from "../auth/guards.js";
|
||||
import { handleRouteError } from "../errors.js";
|
||||
|
||||
export interface AgentConfigRouteConfig {
|
||||
readonly prisma: PrismaClient;
|
||||
readonly sessionSecret: string;
|
||||
}
|
||||
|
||||
export async function registerAgentConfigRoutes(
|
||||
app: FastifyInstance,
|
||||
config: AgentConfigRouteConfig,
|
||||
): Promise<void> {
|
||||
const guardDeps: GuardDeps = { prisma: config.prisma, sessionSecret: config.sessionSecret };
|
||||
// skillStoreRoot is null: this surface does not install skills (see header).
|
||||
const agentConfig = new OrganizationAgentConfiguration(config.prisma, null);
|
||||
|
||||
app.get("/api/org/:orgSlug/agent-roles", async (request, reply) => {
|
||||
try {
|
||||
const { orgSlug } = request.params as { orgSlug: string };
|
||||
const auth = await requireOrgRole(request, reply, guardDeps, { orgSlug });
|
||||
if (auth === null) return;
|
||||
const roles = await agentConfig.listRoles({ organizationId: auth.organization.id });
|
||||
return { roles };
|
||||
} catch (err) {
|
||||
return handleRouteError(reply, err);
|
||||
}
|
||||
});
|
||||
|
||||
app.put("/api/org/:orgSlug/agent-roles/:roleId", async (request, reply) => {
|
||||
try {
|
||||
const { orgSlug, roleId } = request.params as { orgSlug: string; roleId: string };
|
||||
const auth = await requireOrgRole(request, reply, guardDeps, { orgSlug });
|
||||
if (auth === null) return;
|
||||
const body = request.body as {
|
||||
label?: unknown;
|
||||
defaultModel?: unknown;
|
||||
systemPrompt?: unknown;
|
||||
tools?: unknown;
|
||||
sortOrder?: unknown;
|
||||
isDefault?: unknown;
|
||||
};
|
||||
if (typeof body.label !== "string" || body.label.trim() === "") {
|
||||
return reply.status(400).send({
|
||||
error: { code: "bad_request", message: "label is required" },
|
||||
});
|
||||
}
|
||||
const role = await agentConfig.upsertRole({
|
||||
organizationId: auth.organization.id,
|
||||
roleId,
|
||||
label: body.label,
|
||||
...(body.defaultModel === null || typeof body.defaultModel === "string"
|
||||
? { defaultModel: body.defaultModel as string | null }
|
||||
: {}),
|
||||
...(body.systemPrompt === null || typeof body.systemPrompt === "string"
|
||||
? { systemPrompt: body.systemPrompt as string | null }
|
||||
: {}),
|
||||
...(body.tools === null || Array.isArray(body.tools)
|
||||
? { tools: body.tools as readonly string[] | null }
|
||||
: {}),
|
||||
...(typeof body.sortOrder === "number" ? { sortOrder: body.sortOrder } : {}),
|
||||
...(typeof body.isDefault === "boolean" ? { isDefault: body.isDefault } : {}),
|
||||
});
|
||||
return role;
|
||||
} catch (err) {
|
||||
return handleRouteError(reply, err);
|
||||
}
|
||||
});
|
||||
|
||||
app.put("/api/org/:orgSlug/agent-roles/:roleId/skills", async (request, reply) => {
|
||||
try {
|
||||
const { orgSlug, roleId } = request.params as { orgSlug: string; roleId: string };
|
||||
const auth = await requireOrgRole(request, reply, guardDeps, { orgSlug });
|
||||
if (auth === null) return;
|
||||
const body = request.body as { skillNames?: unknown };
|
||||
if (!Array.isArray(body.skillNames) || body.skillNames.some((n) => typeof n !== "string")) {
|
||||
return reply.status(400).send({
|
||||
error: { code: "bad_request", message: "skillNames must be a string array" },
|
||||
});
|
||||
}
|
||||
await agentConfig.setRoleSkills({
|
||||
organizationId: auth.organization.id,
|
||||
roleId,
|
||||
skillNames: body.skillNames as readonly string[],
|
||||
});
|
||||
return { skillNames: body.skillNames as string[] };
|
||||
} catch (err) {
|
||||
return handleRouteError(reply, err);
|
||||
}
|
||||
});
|
||||
|
||||
app.get("/api/org/:orgSlug/agent-skills", async (request, reply) => {
|
||||
try {
|
||||
const { orgSlug } = request.params as { orgSlug: string };
|
||||
const auth = await requireOrgRole(request, reply, guardDeps, { orgSlug });
|
||||
if (auth === null) return;
|
||||
const skills = await agentConfig.listSkills({ organizationId: auth.organization.id });
|
||||
return { skills };
|
||||
} catch (err) {
|
||||
return handleRouteError(reply, err);
|
||||
}
|
||||
});
|
||||
|
||||
app.get("/api/org/:orgSlug/agent-models", async (request, reply) => {
|
||||
try {
|
||||
const { orgSlug } = request.params as { orgSlug: string };
|
||||
const auth = await requireOrgRole(request, reply, guardDeps, { orgSlug });
|
||||
if (auth === null) return;
|
||||
const registry = createDefaultModelRegistry(process.env);
|
||||
return { models: registry.listModels() };
|
||||
} catch (err) {
|
||||
return handleRouteError(reply, err);
|
||||
}
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user