feat(engine): 添加MCP连接管理和工具集成功能
- 集成MCP连接管理器,支持MCP服务器连接 - 添加多种内置工具:ClarifyTool、CronTool、DelegateTool、ExecuteCodeTool、 PatchFileTool、ProcessTool、SendMessageTool、SpawnTool、TerminalTool、 TodoTool、WebFetchTool、WebSearchTool、WriteFileTool等 - 实现工具注册和装配功能 - 添加技能选择上下文参数 - 支持思考模式控制参数thinking_enabled feat(coordinator): 重构任务执行计划器参数命名 - 将learning_candidate_enabled重命名为allow_candidate_generation - 更新TeamGraphScheduler中的参数传递 - 修改LocalAgentRunner中的相关参数处理 - 更新README文档中的相应描述 refactor(context): 标准化工具调用参数格式 - 添加_json导入用于参数序列化 - 实现_provider_tool_calls方法标准化OpenAI兼容的工具调用载荷 - 修复工具调用中参数非字符串类型的序列化问题 refactor(session): 优化消息历史记录过滤逻辑 - 修改get_messages_as_conversation为基于运行状态过滤消息 - 排除未完成、失败或错误结束的运行记录 - 改进对话历史的可见性控制机制 fix(store): 修复FTS索引重建逻辑 - 添加异常处理防止FTS索引创建失败 - 实现_rebuild_fts_index方法重新构建全文搜索索引 - 优化索引触发器和表的维护流程
This commit is contained in:
@ -14,7 +14,6 @@ import {
|
||||
ShieldCheck,
|
||||
Trash2,
|
||||
Upload,
|
||||
Wand2,
|
||||
X,
|
||||
XCircle,
|
||||
} from 'lucide-react';
|
||||
@ -27,11 +26,11 @@ import {
|
||||
listSkillCandidates,
|
||||
listSkillDrafts,
|
||||
listSkills,
|
||||
migrateSkills,
|
||||
publishSkillDraft,
|
||||
regenerateSkillDraft,
|
||||
rejectSkillDraft,
|
||||
rollbackPublishedSkill,
|
||||
runSkillLearningOnce,
|
||||
submitSkillDraft,
|
||||
synthesizeSkillDraft,
|
||||
uploadSkill,
|
||||
@ -52,6 +51,9 @@ import type { Skill, SkillDraft, SkillLearningCandidate } from '@/types';
|
||||
import { pickAppText } from '@/lib/i18n/core';
|
||||
import { useAppI18n } from '@/lib/i18n/provider';
|
||||
|
||||
const TERMINAL_DRAFT_STATUSES = new Set(['rejected', 'published', 'disabled', 'archived']);
|
||||
const REJECTABLE_DRAFT_STATUSES = new Set(['draft', 'in_review', 'approved']);
|
||||
|
||||
export default function SkillsPage() {
|
||||
const { locale } = useAppI18n();
|
||||
const t = (zh: string, en: string) => pickAppText(locale, zh, en);
|
||||
@ -62,7 +64,7 @@ export default function SkillsPage() {
|
||||
const [actionId, setActionId] = useState<string | null>(null);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [showUpload, setShowUpload] = useState(false);
|
||||
const [deleting, setDeleting] = useState<string | null>(null);
|
||||
const [ignoredCandidates, setIgnoredCandidates] = useState<Set<string>>(new Set());
|
||||
|
||||
const load = useCallback(async () => {
|
||||
setLoading(true);
|
||||
@ -100,12 +102,11 @@ export default function SkillsPage() {
|
||||
}
|
||||
};
|
||||
|
||||
const confirmDelete = async (name: string) => {
|
||||
await runAction(`delete:${name}`, async () => {
|
||||
await deleteSkill(name);
|
||||
setDeleting(null);
|
||||
});
|
||||
};
|
||||
const hiddenCandidateStatuses = new Set(['rejected', 'superseded', 'published']);
|
||||
const visibleCandidates = candidates.filter(
|
||||
(candidate) => !ignoredCandidates.has(candidate.candidate_id) && !hiddenCandidateStatuses.has(candidate.status)
|
||||
);
|
||||
const visibleDrafts = drafts.filter((draft) => !TERMINAL_DRAFT_STATUSES.has(draft.status));
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
@ -127,22 +128,18 @@ export default function SkillsPage() {
|
||||
<RefreshCw className="mr-2 h-4 w-4" />
|
||||
{t('刷新', 'Refresh')}
|
||||
</Button>
|
||||
<Button onClick={() => setShowUpload(true)} size="sm">
|
||||
<Upload className="mr-2 h-4 w-4" />
|
||||
{t('上传技能', 'Upload skill')}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => void runAction('learning:run-once', () => runSkillLearningOnce())}
|
||||
onClick={() => void runAction('migrate-skills', () => migrateSkills())}
|
||||
variant="outline"
|
||||
size="sm"
|
||||
disabled={Boolean(actionId)}
|
||||
>
|
||||
{actionId === 'learning:run-once' ? (
|
||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||
) : (
|
||||
<Wand2 className="mr-2 h-4 w-4" />
|
||||
)}
|
||||
{t('运行学习', 'Run learning')}
|
||||
</Button>
|
||||
<Button onClick={() => setShowUpload(true)} size="sm">
|
||||
<Upload className="mr-2 h-4 w-4" />
|
||||
{t('上传技能', 'Upload skill')}
|
||||
{actionId === 'migrate-skills' ? <Loader2 className="mr-2 h-4 w-4 animate-spin" /> : <Rocket className="mr-2 h-4 w-4" />}
|
||||
{t('迁移旧技能', 'Migrate legacy skills')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
@ -169,36 +166,18 @@ export default function SkillsPage() {
|
||||
/>
|
||||
)}
|
||||
|
||||
{deleting && (
|
||||
<Card className="border-destructive">
|
||||
<CardContent className="flex items-center justify-between gap-4 pt-6">
|
||||
<p className="text-sm">
|
||||
{t('确定删除技能', 'Delete skill')} <strong>{deleting}</strong>?
|
||||
</p>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button variant="outline" size="sm" onClick={() => setDeleting(null)}>
|
||||
{t('取消', 'Cancel')}
|
||||
</Button>
|
||||
<Button variant="destructive" size="sm" onClick={() => void confirmDelete(deleting)}>
|
||||
{t('删除', 'Delete')}
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
<Tabs defaultValue="published" className="space-y-4">
|
||||
<TabsList>
|
||||
<TabsTrigger value="published">{t('已发布', 'Published')}</TabsTrigger>
|
||||
<TabsTrigger value="candidates">{t('候选', 'Candidates')}</TabsTrigger>
|
||||
<TabsTrigger value="drafts">{t('草稿/评审', 'Drafts')}</TabsTrigger>
|
||||
<TabsTrigger value="drafts">{t('草稿评审', 'Draft review')}</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value="published">
|
||||
<PublishedSkillsTable
|
||||
skills={skills}
|
||||
onDownload={(name) => downloadSkill(name).catch((err) => setError(err.message))}
|
||||
onDelete={(name) => setDeleting(name)}
|
||||
onDelete={(name) => void runAction(`delete:${name}`, () => deleteSkill(name))}
|
||||
onDisable={(name) =>
|
||||
runAction(`disable:${name}`, () => disablePublishedSkill(name, t('人工禁用', 'Manual disable')))
|
||||
}
|
||||
@ -219,11 +198,11 @@ export default function SkillsPage() {
|
||||
<CardTitle className="text-base">{t('学习候选', 'Learning candidates')}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{candidates.length === 0 ? (
|
||||
<EmptyState icon={<Wand2 className="h-8 w-8" />} text={t('暂无学习候选', 'No learning candidates yet')} />
|
||||
{visibleCandidates.length === 0 ? (
|
||||
<EmptyState icon={<FileText className="h-8 w-8" />} text={t('暂无学习候选', 'No learning candidates yet')} />
|
||||
) : (
|
||||
<div className="space-y-3">
|
||||
{candidates.map((candidate) => (
|
||||
{visibleCandidates.map((candidate) => (
|
||||
<div key={candidate.candidate_id} className="rounded-lg border border-border p-4">
|
||||
<div className="flex flex-wrap items-start justify-between gap-3">
|
||||
<div className="min-w-0">
|
||||
@ -252,6 +231,14 @@ export default function SkillsPage() {
|
||||
)}
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
disabled={Boolean(actionId)}
|
||||
onClick={() => setIgnoredCandidates((prev) => new Set(prev).add(candidate.candidate_id))}
|
||||
>
|
||||
{t('忽略', 'Ignore')}
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
disabled={Boolean(actionId)}
|
||||
@ -299,11 +286,11 @@ export default function SkillsPage() {
|
||||
<CardTitle className="text-base">{t('草稿、评审与发布', 'Drafts, review, and publish')}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{drafts.length === 0 ? (
|
||||
{visibleDrafts.length === 0 ? (
|
||||
<EmptyState icon={<FileText className="h-8 w-8" />} text={t('暂无草稿', 'No drafts yet')} />
|
||||
) : (
|
||||
<div className="space-y-4">
|
||||
{drafts.map((draft) => (
|
||||
{visibleDrafts.map((draft) => (
|
||||
<DraftCard
|
||||
key={`${draft.skill_name}:${draft.draft_id}`}
|
||||
draft={draft}
|
||||
@ -323,15 +310,11 @@ export default function SkillsPage() {
|
||||
rejectSkillDraft(draft.skill_name, draft.draft_id)
|
||||
)
|
||||
}
|
||||
onPublish={() =>
|
||||
runAction(`publish:${draft.draft_id}`, async () => {
|
||||
const confirmHighRisk = draft.safety_report?.risk_level === 'high';
|
||||
if (confirmHighRisk && !window.confirm(t('这是高风险草稿,确认发布?', 'This is a high-risk draft. Publish anyway?'))) {
|
||||
return;
|
||||
}
|
||||
await publishSkillDraft(draft.skill_name, draft.draft_id, '', confirmHighRisk);
|
||||
})
|
||||
}
|
||||
onPublish={(confirmHighRisk) =>
|
||||
runAction(`publish:${draft.draft_id}`, () =>
|
||||
publishSkillDraft(draft.skill_name, draft.draft_id, '', confirmHighRisk)
|
||||
)
|
||||
}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
@ -442,7 +425,7 @@ function DraftCard({
|
||||
onSubmit: () => Promise<unknown>;
|
||||
onApprove: () => Promise<unknown>;
|
||||
onReject: () => Promise<unknown>;
|
||||
onPublish: () => Promise<unknown>;
|
||||
onPublish: (confirmHighRisk: boolean) => Promise<unknown>;
|
||||
}) {
|
||||
const { locale } = useAppI18n();
|
||||
const t = (zh: string, en: string) => pickAppText(locale, zh, en);
|
||||
@ -452,9 +435,27 @@ function DraftCard({
|
||||
const publishBlocked =
|
||||
draft.status !== 'approved'
|
||||
|| !safety
|
||||
|| !safety.passed
|
||||
|| safety.risk_level === 'critical'
|
||||
|| (evalReport?.status !== 'skipped_provider_unavailable' && evalReport?.passed === false);
|
||||
const isHighRisk = safety?.risk_level === 'high';
|
||||
const highRiskReason = [
|
||||
...(safety?.blocked_reasons ?? []),
|
||||
...(safety?.issues ?? []),
|
||||
safety?.suggested_fix,
|
||||
].filter(Boolean).join('\n');
|
||||
const safetyBlocksReview = Boolean(safety && (!safety.passed || safety.risk_level === 'critical'));
|
||||
const submitBlocked = draft.status !== 'draft' || safetyBlocksReview;
|
||||
const approveBlocked = draft.status !== 'in_review' || safetyBlocksReview;
|
||||
const rejectBlocked = !REJECTABLE_DRAFT_STATUSES.has(draft.status);
|
||||
const handlePublish = () => {
|
||||
if (isHighRisk) {
|
||||
const confirmed = window.confirm(
|
||||
t('该草稿被标记为高风险。确认发布?', 'This draft is marked high risk. Publish anyway?')
|
||||
);
|
||||
if (!confirmed) return;
|
||||
}
|
||||
void onPublish(isHighRisk);
|
||||
};
|
||||
return (
|
||||
<div className="rounded-lg border border-border p-4">
|
||||
<div className="flex flex-wrap items-start justify-between gap-3">
|
||||
@ -478,33 +479,47 @@ function DraftCard({
|
||||
<p className="mt-1 text-xs text-muted-foreground">
|
||||
{t('base', 'base')}: {draft.base_version || '-'}
|
||||
</p>
|
||||
{isHighRisk && (
|
||||
<div className="mt-3 rounded-md border border-destructive/30 bg-destructive/5 p-3 text-xs text-destructive">
|
||||
<div className="font-medium">{t('高风险理由来自 safety report', 'High-risk reason from safety report')}</div>
|
||||
<pre className="mt-2 whitespace-pre-wrap font-sans">{highRiskReason || t('未提供具体理由', 'No concrete reason provided')}</pre>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<Button variant="outline" size="sm" disabled={busy || draft.status !== 'draft'} onClick={() => void onSubmit()}>
|
||||
<Button variant="outline" size="sm" disabled={busy || submitBlocked} onClick={() => void onSubmit()}>
|
||||
<Send className="mr-2 h-4 w-4" />
|
||||
{t('送审', 'Submit')}
|
||||
</Button>
|
||||
<Button variant="outline" size="sm" disabled={busy || draft.status === 'published'} onClick={() => void onApprove()}>
|
||||
<Button variant="outline" size="sm" disabled={busy || approveBlocked} onClick={() => void onApprove()}>
|
||||
<Check className="mr-2 h-4 w-4" />
|
||||
{t('批准', 'Approve')}
|
||||
</Button>
|
||||
<Button variant="outline" size="sm" disabled={busy || draft.status === 'published'} onClick={() => void onReject()}>
|
||||
<Button variant="outline" size="sm" disabled={busy || rejectBlocked} onClick={() => void onReject()}>
|
||||
<XCircle className="mr-2 h-4 w-4" />
|
||||
{t('拒绝', 'Reject')}
|
||||
</Button>
|
||||
<Button size="sm" disabled={busy || publishBlocked} onClick={() => void onPublish()}>
|
||||
<Rocket className="mr-2 h-4 w-4" />
|
||||
{t('发布', 'Publish')}
|
||||
</Button>
|
||||
<Button size="sm" disabled={busy || publishBlocked} onClick={handlePublish}>
|
||||
<Rocket className="mr-2 h-4 w-4" />
|
||||
{t('发布', 'Publish')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-3 grid gap-3 md:grid-cols-2">
|
||||
<pre className="max-h-52 overflow-auto rounded-md bg-muted/50 p-3 text-xs">
|
||||
{JSON.stringify(draft.proposed_frontmatter, null, 2)}
|
||||
</pre>
|
||||
<pre className="max-h-52 overflow-auto whitespace-pre-wrap rounded-md bg-muted/50 p-3 text-xs">
|
||||
{draft.proposed_content}
|
||||
</pre>
|
||||
<div className="rounded-md border border-border bg-muted/30 p-3">
|
||||
<div className="mb-2 text-xs font-medium text-muted-foreground">{t('当前版本', 'Current version')}</div>
|
||||
<pre className="max-h-52 overflow-auto whitespace-pre-wrap text-xs">
|
||||
{draft.base_version ? `${t('基线版本', 'Base version')}: ${draft.base_version}` : t('无基线版本,视为新增技能', 'No base version, treated as a new skill')}
|
||||
</pre>
|
||||
</div>
|
||||
<div className="rounded-md border border-border bg-muted/30 p-3">
|
||||
<div className="mb-2 text-xs font-medium text-muted-foreground">{t('草稿变更', 'Draft changes')}</div>
|
||||
<pre className="max-h-52 overflow-auto whitespace-pre-wrap text-xs">
|
||||
{JSON.stringify(draft.proposed_frontmatter, null, 2)}
|
||||
{'\n\n---\n\n'}
|
||||
{draft.proposed_content}
|
||||
</pre>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-3 grid gap-3 md:grid-cols-2">
|
||||
<ReportBlock
|
||||
@ -595,6 +610,9 @@ function UploadSkillForm({
|
||||
accept=".zip"
|
||||
className="block w-full cursor-pointer text-sm text-muted-foreground file:mr-4 file:rounded-md file:border-0 file:bg-primary file:px-4 file:py-2 file:text-sm file:font-medium file:text-primary-foreground hover:file:bg-primary/90"
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{pickAppText(locale, '上传后进入草稿评审,并自动运行 safety 和 eval。', 'After upload, the skill enters draft review and runs safety and eval automatically.')}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex justify-end gap-2">
|
||||
<Button type="button" variant="outline" onClick={onCancel}>
|
||||
|
||||
Reference in New Issue
Block a user