feat: 将项目从nano重命名为beaver并更新相关配置

- 将所有环境变量前缀从NANO_改为BEAVER_
- 更新README.md文档内容,包括项目介绍、组件说明和快速开始指南
- 修改.gitignore文件,添加auth-portal运行时路径排除规则
- 更新app-instance镜像标签从nano/app-instance改为beaver/app-instance
- 增强技能安全检查器,支持工具前缀白名单功能
- 添加技能草稿重新检查安全性API端点
- 扩展证据选择器,收集工具调用名称用于技能学习
- 改进技能合成器,基于实际调用的工具生成工具提示
- 优化路由超时处理机制,增加重试逻辑
- 更新后端架构文档,添加可视化入口和基础概念说明
- 实现在WebSocket消息中传递工具迭代次数信息
This commit is contained in:
2026-05-20 18:01:06 +08:00
parent 3b0af173cc
commit 9d6cde2d23
63 changed files with 4894 additions and 1596 deletions

View File

@ -41,6 +41,7 @@ import {
listSkillDrafts,
listSkills,
publishSkillDraft,
recheckSkillDraftSafety,
regenerateSkillDraft,
rejectSkillDraft,
rollbackPublishedSkill,
@ -412,6 +413,11 @@ export default function SkillsPage() {
rejectSkillDraft(draft.skill_name, draft.draft_id)
)
}
onRecheckSafety={() =>
runAction(`safety:${draft.draft_id}`, () =>
recheckSkillDraftSafety(draft.skill_name, draft.draft_id)
)
}
onPublish={(confirmHighRisk) =>
runAction(`publish:${draft.draft_id}`, () =>
publishSkillDraft(draft.skill_name, draft.draft_id, '', confirmHighRisk)
@ -697,6 +703,7 @@ function DraftCard({
onSubmit,
onApprove,
onReject,
onRecheckSafety,
onPublish,
}: {
draft: SkillDraft;
@ -704,6 +711,7 @@ function DraftCard({
onSubmit: () => Promise<unknown>;
onApprove: () => Promise<unknown>;
onReject: () => Promise<unknown>;
onRecheckSafety: () => Promise<unknown>;
onPublish: (confirmHighRisk: boolean) => Promise<unknown>;
}) {
const { locale } = useAppI18n();
@ -814,6 +822,10 @@ function DraftCard({
<XCircle className="mr-2 h-4 w-4" />
{t('拒绝', 'Reject')}
</Button>
<Button variant="outline" size="sm" disabled={busy || TERMINAL_DRAFT_STATUSES.has(draft.status)} onClick={() => void onRecheckSafety()}>
<ShieldCheck className="mr-2 h-4 w-4" />
{t('复检', 'Recheck')}
</Button>
<Button size="sm" disabled={busy || publishBlocked} onClick={handlePublish}>
<Rocket className="mr-2 h-4 w-4" />
{t('发布', 'Publish')}

View File

@ -3,7 +3,7 @@
import Link from 'next/link';
import { useParams, useRouter } from 'next/navigation';
import React, { useMemo, useState } from 'react';
import { AlertCircle, ArrowLeft, Bot, CheckCircle2, Download, FileText, HelpCircle, MessageSquare, RefreshCw, Trash2, User, XCircle } from 'lucide-react';
import { AlertCircle, ArrowLeft, Bot, CheckCircle2, Download, FileText, HelpCircle, Loader2, MessageSquare, RefreshCw, ThumbsUp, Trash2, User, XCircle } from 'lucide-react';
import { TaskRuntimeStatusBadge, formatTaskRuntimeDuration, formatTaskRuntimeTime, progressPercent } from '@/components/task-runtime/TaskRuntimeShared';
import { Badge } from '@/components/ui/badge';
@ -17,6 +17,14 @@ import { buildTaskRuntimeView, type TaskRuntimeNodeView } from '@/lib/task-runti
import { useChatStore } from '@/lib/store';
import type { BackendTask, BackendTaskRun, ProcessArtifact, ProcessEvent } from '@/types';
type TaskFeedbackType = 'satisfied' | 'revise' | 'abandon';
type TaskFeedbackItem = {
feedback_type?: unknown;
comment?: unknown;
created_at?: unknown;
run_id?: unknown;
};
function taskVisibleStatus(task: TaskRuntimeNodeView, locale: 'zh-CN' | 'en-US') {
if (task.status === 'error') return pickAppText(locale, '任务失败', 'Task failed');
if (task.status === 'cancelled') return pickAppText(locale, '已取消', 'Cancelled');
@ -53,11 +61,13 @@ export default function TaskDetailPage() {
const [backendTaskLoading, setBackendTaskLoading] = useState(false);
const [selectedRunId, setSelectedRunId] = useState<string | null>(task?.rootRunId ?? null);
const [revision, setRevision] = useState('');
const [runtimeFeedback, setRuntimeFeedback] = useState<TaskFeedbackItem | null>(null);
const [actionError, setActionError] = useState<string | null>(null);
const [actionBusy, setActionBusy] = useState<string | null>(null);
React.useEffect(() => {
setSelectedRunId(task?.rootRunId ?? null);
setRuntimeFeedback(null);
}, [task?.rootRunId]);
React.useEffect(() => {
@ -138,6 +148,8 @@ export default function TaskDetailPage() {
});
};
const backendFeedbackRunId = backendTask ? pickFeedbackRunId(backendTask) : null;
if (!task && backendTask) {
const validation = backendTask.validation_result;
const accepted = Boolean(validation?.accepted);
@ -185,6 +197,26 @@ export default function TaskDetailPage() {
</CardContent>
</Card>
<TaskFeedbackPanel
sessionId={backendTask.session_id}
runId={backendFeedbackRunId}
taskStatus={backendTask.status}
feedbackItems={feedbackItems}
actionBusy={actionBusy}
onSubmit={(feedbackType, comment) =>
runAction(`backend-feedback-${feedbackType}`, async () => {
await submitChatFeedback({
sessionId: backendTask.session_id,
runId: backendFeedbackRunId!,
feedbackType,
comment,
});
const refreshed = await getBackendTask(backendTask.task_id);
setBackendTask(refreshed);
})
}
/>
<Card>
<CardHeader>
<CardTitle className="text-base">{pickAppText(locale, 'Agent 执行过程', 'Agent conversation process')}</CardTitle>
@ -424,37 +456,33 @@ export default function TaskDetailPage() {
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle className="text-base">{pickAppText(locale, '修订意见', 'Revision')}</CardTitle>
</CardHeader>
<CardContent className="space-y-3">
<Textarea
value={revision}
onChange={(event) => setRevision(event.target.value)}
placeholder={pickAppText(locale, '直接写下需要调整的地方...', 'Describe what should change...')}
/>
<Button
className="w-full"
disabled={!revision.trim() || Boolean(actionBusy)}
onClick={() =>
void runAction('revision', async () => {
updateMessageFeedback(task.rootRunId, 'revise');
await submitChatFeedback({
sessionId: task.sessionId || 'web:default',
runId: task.rootRunId,
feedbackType: 'revise',
comment: revision.trim(),
});
setRevision('');
})
}
>
<RefreshCw className="mr-2 h-4 w-4" />
{pickAppText(locale, '提交修订', 'Submit revision')}
</Button>
</CardContent>
</Card>
<TaskFeedbackPanel
sessionId={task.sessionId || 'web:default'}
runId={task.rootRunId}
taskStatus={task.status}
feedbackItems={runtimeFeedback ? [runtimeFeedback] : []}
actionBusy={actionBusy}
revision={revision}
onRevisionChange={setRevision}
onSubmit={(feedbackType, comment) =>
runAction(`runtime-feedback-${feedbackType}`, async () => {
updateMessageFeedback(task.rootRunId, feedbackType);
await submitChatFeedback({
sessionId: task.sessionId || 'web:default',
runId: task.rootRunId,
feedbackType,
comment,
});
setRuntimeFeedback({
feedback_type: feedbackType,
comment: comment || '',
created_at: new Date().toISOString(),
run_id: task.rootRunId,
});
setRevision('');
})
}
/>
<Card>
<CardHeader>
@ -521,6 +549,136 @@ function Metric({ label, value }: { label: string; value: string }) {
);
}
function TaskFeedbackPanel({
sessionId,
runId,
taskStatus,
feedbackItems,
actionBusy,
revision,
onRevisionChange,
onSubmit,
}: {
sessionId: string;
runId: string | null;
taskStatus: string;
feedbackItems: TaskFeedbackItem[];
actionBusy: string | null;
revision?: string;
onRevisionChange?: (value: string) => void;
onSubmit: (feedbackType: TaskFeedbackType, comment?: string) => Promise<unknown>;
}) {
const { locale } = useAppI18n();
const [localComment, setLocalComment] = React.useState('');
const comment = revision ?? localComment;
const setComment = onRevisionChange ?? setLocalComment;
const isFinalized = taskStatus === 'closed' || taskStatus === 'abandoned';
const recordedFeedback = feedbackForRun(feedbackItems, runId) ?? (isFinalized ? latestFeedback(feedbackItems) : null);
const canSubmit = Boolean(runId) && !recordedFeedback && !isFinalized && !actionBusy;
const submit = (feedbackType: TaskFeedbackType, nextComment?: string) => {
if (!runId || !canSubmit) return;
void onSubmit(feedbackType, nextComment);
};
return (
<Card>
<CardHeader>
<CardTitle className="text-base">{pickAppText(locale, '任务反馈', 'Task feedback')}</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
{recordedFeedback ? (
<div className="rounded-md border border-border bg-muted/25 p-3 text-sm">
<div className="flex items-center gap-2 font-medium">
<CheckCircle2 className="h-4 w-4 text-[#657162]" />
{pickAppText(locale, '已提交反馈', 'Feedback submitted')}: {humanFeedback(String(recordedFeedback.feedback_type || ''), locale)}
</div>
{recordedFeedback.comment ? (
<p className="mt-2 text-muted-foreground">{String(recordedFeedback.comment)}</p>
) : null}
{recordedFeedback.created_at ? (
<p className="mt-2 text-xs text-muted-foreground">{formatTaskRuntimeTime(String(recordedFeedback.created_at), locale)}</p>
) : null}
</div>
) : isFinalized ? (
<div className="rounded-md border border-border bg-muted/25 p-3 text-sm text-muted-foreground">
{pickAppText(locale, '任务已结束,不能再提交新的反馈。', 'This task is finalized and cannot accept new feedback.')}
</div>
) : !runId ? (
<div className="rounded-md border border-border bg-muted/25 p-3 text-sm text-muted-foreground">
{pickAppText(locale, '暂无可反馈的运行记录。', 'No run is available for feedback yet.')}
</div>
) : null}
<div className="grid gap-2 sm:grid-cols-3">
<FeedbackButton
type="satisfied"
icon={<ThumbsUp className="mr-2 h-4 w-4" />}
label={pickAppText(locale, '满意', 'Satisfied')}
actionBusy={actionBusy}
disabled={!canSubmit}
onClick={() => submit('satisfied', comment.trim() || undefined)}
/>
<FeedbackButton
type="revise"
icon={<RefreshCw className="mr-2 h-4 w-4" />}
label={pickAppText(locale, '需要修改', 'Needs revision')}
actionBusy={actionBusy}
disabled={!canSubmit || !comment.trim()}
onClick={() => submit('revise', comment.trim())}
/>
<FeedbackButton
type="abandon"
icon={<XCircle className="mr-2 h-4 w-4" />}
label={pickAppText(locale, '放弃', 'Abandon')}
actionBusy={actionBusy}
disabled={!canSubmit}
onClick={() => submit('abandon', comment.trim() || undefined)}
/>
</div>
<Textarea
value={comment}
onChange={(event) => setComment(event.target.value)}
disabled={Boolean(recordedFeedback) || isFinalized || Boolean(actionBusy)}
placeholder={pickAppText(locale, '需要修改时写下具体要求;满意或放弃可选填说明。', 'Describe requested changes; notes are optional for satisfied or abandon.')}
/>
<div className="text-xs text-muted-foreground">
{pickAppText(locale, '反馈将记录到当前任务运行:', 'Feedback will be recorded on run: ')}
<span className="font-mono">{runId || '-'}</span>
<span className="mx-1">·</span>
{pickAppText(locale, '会话:', 'Session: ')}
<span className="font-mono">{sessionId}</span>
</div>
</CardContent>
</Card>
);
}
function FeedbackButton({
type,
icon,
label,
actionBusy,
disabled,
onClick,
}: {
type: TaskFeedbackType;
icon: React.ReactNode;
label: string;
actionBusy: string | null;
disabled: boolean;
onClick: () => void;
}) {
const isBusy = Boolean(actionBusy?.endsWith(type));
return (
<Button type="button" variant="outline" className="w-full justify-center" disabled={disabled || Boolean(actionBusy)} onClick={onClick}>
{isBusy ? <Loader2 className="mr-2 h-4 w-4 animate-spin" /> : icon}
{label}
</Button>
);
}
function BackendRunConversation({ run, index }: { run: BackendTaskRun; index: number }) {
const { locale } = useAppI18n();
return (
@ -597,6 +755,24 @@ function humanFinishReason(reason: string, locale: 'zh-CN' | 'en-US') {
return reason;
}
function pickFeedbackRunId(task: BackendTask): string | null {
const runIds = task.run_ids.filter(Boolean);
if (runIds.length > 0) return runIds[runIds.length - 1];
const runs = task.runs ?? [];
if (runs.length > 0) return runs[runs.length - 1].run_id;
return null;
}
function feedbackForRun(items: TaskFeedbackItem[], runId: string | null): TaskFeedbackItem | null {
if (!runId) return null;
const ordered = [...items].reverse();
return ordered.find((item) => String(item.run_id || '') === runId) ?? null;
}
function latestFeedback(items: TaskFeedbackItem[]): TaskFeedbackItem | null {
return [...items].reverse()[0] ?? null;
}
function arrayOfStrings(value: unknown): string[] {
return Array.isArray(value) ? value.map((item) => String(item)).filter(Boolean) : [];
}