forked from EduCraft/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:
@@ -25,6 +25,7 @@
|
||||
{ key: 'projects', label: '项目', icon: 'projects' as const },
|
||||
{ key: 'capacity', label: '容量', icon: 'overview' as const },
|
||||
{ key: 'provider', label: '供应方', icon: 'provider' as const },
|
||||
{ key: 'roles', label: '角色', icon: 'roles' as const },
|
||||
{ key: 'feishu', label: '飞书', icon: 'feishu' as const },
|
||||
];
|
||||
|
||||
@@ -310,11 +311,7 @@
|
||||
<div class="saas-shell">
|
||||
<div class="saas-main">
|
||||
<header class="saas-topbar">
|
||||
<a
|
||||
href={`/admin/org/${org.slug}/projects`}
|
||||
class="saas-btn-ghost px-2!"
|
||||
aria-label="返回项目列表"
|
||||
>
|
||||
<a href={`/admin/org/${org.slug}/projects`} class="saas-btn-ghost px-2!" aria-label="返回项目列表">
|
||||
<Icon name="menu" class="h-5 w-5" />
|
||||
</a>
|
||||
<div class="min-w-0">
|
||||
|
||||
@@ -0,0 +1,124 @@
|
||||
<script lang="ts">
|
||||
import { page } from '$app/state';
|
||||
import { api, type AgentRoleRow, type AgentModelRow, type AgentSkillRow } from '$lib/api';
|
||||
import PageHeader from '$lib/components/PageHeader.svelte';
|
||||
import LoadingState from '$lib/components/LoadingState.svelte';
|
||||
import ErrorBanner from '$lib/components/ErrorBanner.svelte';
|
||||
import EmptyState from '$lib/components/EmptyState.svelte';
|
||||
import RoleCard from '$lib/components/RoleCard.svelte';
|
||||
import { toastError, toastSuccess } from '$lib/toast';
|
||||
|
||||
const slug = $derived(page.params.slug ?? '');
|
||||
|
||||
let roles = $state<AgentRoleRow[]>([]);
|
||||
let models = $state<AgentModelRow[]>([]);
|
||||
let skills = $state<AgentSkillRow[]>([]);
|
||||
let loading = $state(true);
|
||||
let error = $state<string | null>(null);
|
||||
|
||||
let newRoleId = $state('');
|
||||
let newLabel = $state('');
|
||||
let adding = $state(false);
|
||||
|
||||
async function load() {
|
||||
loading = true;
|
||||
error = null;
|
||||
try {
|
||||
const [r, m, s] = await Promise.all([api.agentRoles(slug), api.agentModels(slug), api.agentSkills(slug)]);
|
||||
roles = r.roles;
|
||||
models = m.models;
|
||||
skills = s.skills;
|
||||
} catch (err) {
|
||||
error = err instanceof Error ? err.message : String(err);
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function add() {
|
||||
const roleId = newRoleId.trim();
|
||||
const label = newLabel.trim();
|
||||
if (roleId === '' || label === '') {
|
||||
toastError('角色 ID 与显示名均为必填');
|
||||
return;
|
||||
}
|
||||
if (roles.some((r) => r.roleId === roleId)) {
|
||||
toastError(`角色 ID 已存在:${roleId}`);
|
||||
return;
|
||||
}
|
||||
adding = true;
|
||||
try {
|
||||
const created = await api.upsertAgentRole(slug, roleId, { label });
|
||||
roles = [...roles, created];
|
||||
newRoleId = '';
|
||||
newLabel = '';
|
||||
toastSuccess('角色已创建');
|
||||
} catch (err) {
|
||||
toastError(err instanceof Error ? err.message : String(err));
|
||||
} finally {
|
||||
adding = false;
|
||||
}
|
||||
}
|
||||
|
||||
function onRoleUpdated(updated: AgentRoleRow) {
|
||||
roles = roles.map((x) => (x.roleId === updated.roleId ? { ...updated, skillNames: x.skillNames } : x));
|
||||
if (updated.isDefault) {
|
||||
roles = roles.map((x) => (x.roleId === updated.roleId ? x : { ...x, isDefault: false }));
|
||||
}
|
||||
}
|
||||
|
||||
function onRoleSkillsChanged(roleId: string, skillNames: string[]) {
|
||||
roles = roles.map((x) => (x.roleId === roleId ? { ...x, skillNames } : x));
|
||||
}
|
||||
|
||||
$effect(() => {
|
||||
if (slug) load();
|
||||
});
|
||||
</script>
|
||||
|
||||
<PageHeader
|
||||
title="角色"
|
||||
description="角色是组织级数据:组合默认模型、系统提示词、工具白名单与已绑定技能。角色 ID 即飞书斜杠命令(如 /draft)。"
|
||||
/>
|
||||
|
||||
{#if loading}
|
||||
<LoadingState />
|
||||
{:else if error}
|
||||
<ErrorBanner message={error} onretry={load} />
|
||||
{:else}
|
||||
<div class="saas-card-pad mb-6">
|
||||
<h2 class="saas-section-title mb-4">新建角色</h2>
|
||||
<div class="grid gap-3 sm:grid-cols-[10rem_1fr_auto]">
|
||||
<input
|
||||
class="saas-input font-mono text-sm"
|
||||
placeholder="角色 ID(如 draft)"
|
||||
bind:value={newRoleId}
|
||||
onkeydown={(e) => {
|
||||
if (e.key === 'Enter') add();
|
||||
}}
|
||||
/>
|
||||
<input
|
||||
class="saas-input"
|
||||
placeholder="显示名(如 草稿)"
|
||||
bind:value={newLabel}
|
||||
onkeydown={(e) => {
|
||||
if (e.key === 'Enter') add();
|
||||
}}
|
||||
/>
|
||||
<button class="saas-btn-primary" onclick={add} disabled={adding}>新建</button>
|
||||
</div>
|
||||
<p class="mt-2 text-xs text-surface-600">角色 ID 仅允许小写字母、数字、下划线与连字符,且以字母或数字开头。</p>
|
||||
</div>
|
||||
|
||||
{#if roles.length === 0}
|
||||
<div class="saas-card">
|
||||
<EmptyState title="暂无角色" description="组织必须且只能有一个启用中的默认角色;新建第一个角色将自动成为默认。" />
|
||||
</div>
|
||||
{:else}
|
||||
<div class="space-y-4">
|
||||
{#each roles as r (r.roleId)}
|
||||
<RoleCard {r} {models} {skills} {slug} onupdated={onRoleUpdated} onskillschanged={onRoleSkillsChanged} />
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
Reference in New Issue
Block a user