import { useEffect, useState, type ReactNode } from "react"; import { Button, Checkbox, Input } from "antd"; import { ArrowLeft, BookMarked, Check, ChevronRight, Cpu, Gauge, LockKeyhole, PlugZap, Search, ShieldCheck, Sparkles, Wrench } from "lucide-react"; import { ModelPicker } from "@/components/model-picker"; import type { CanvasTheme } from "@/lib/canvas-theme"; import AgentMemoryPane from "@/pages/settings/agent-memory-pane"; import type { AgentPermissionMode, AgentProfileLayer, AgentProfileScope, AgentProfileView, AgentReasoningMode } from "@/services/api/agent"; import type { Skill } from "@/services/api/skills"; import type { AiConfig } from "@/stores/use-config-store"; export type AgentContextKey = "canvas" | "resources" | "generation_history" | "skills" | "project"; type SettingsSection = "home" | "profile" | "memories" | "skills" | "mcp" | "context" | "budget"; type AgentSettingsProps = { theme: CanvasTheme; config: AiConfig; selectedModel: string; permissionMode: AgentPermissionMode; reasoningMode: AgentReasoningMode; onReasoningModeChange: (value: AgentReasoningMode) => void; profileView: AgentProfileView | null; profileLoading: boolean; profileSaving: boolean; profileError?: string; projectId?: string; canvasId: string; onReloadProfile: () => Promise; onSaveProfile: (input: { scope: AgentProfileScope; projectId?: string; canvasId?: string; content: string; revision: number }) => Promise; contextScope: AgentContextKey[]; nodeCount: number; installedSkills: Skill[]; marketSkills: Skill[]; selectedSkillIds: string[]; skillSearch: string; skillsLoading: boolean; skillHasMore: boolean; maxCredits: string; maxGenerationTasks: string; maxVideoSeconds: string; onBack: () => void; onModelChange: (model: string) => void; onPermissionChange: (mode: AgentPermissionMode) => void; onContextToggle: (value: AgentContextKey) => void; onSkillSearch: (value: string) => void; onSkillToggle: (skillId: string) => void; onSkillInstall: (skill: Skill) => Promise; onLoadMoreSkills: () => Promise; onMaxCreditsChange: (value: string) => void; onMaxGenerationTasksChange: (value: string) => void; onMaxVideoSecondsChange: (value: string) => void; }; const permissionOptions: Array<{ value: AgentPermissionMode; label: string; description: string; icon: typeof ShieldCheck; color: string }> = [ { value: "read_only", label: "只读", description: "只分析和建议", icon: LockKeyhole, color: "#4f7cff" }, { value: "request_approval", label: "请求审批", description: "写入和生成前确认", icon: ShieldCheck, color: "#b58336" }, { value: "auto", label: "自动执行", description: "预算内执行已授权工具", icon: Sparkles, color: "#429477" }, ]; const contextOptions: Array<{ value: AgentContextKey; label: string; description: string }> = [ { value: "canvas", label: "已保存画布摘要", description: "最多 80 个节点的标题与文本片段,不含媒体正文或未同步修改" }, ]; const reasoningOptions: Array<{ value: AgentReasoningMode; label: string; description: string }> = [ { value: "off", label: "关闭", description: "直接执行" }, { value: "auto", label: "自动", description: "按任务决定" }, { value: "deep", label: "深入", description: "复杂任务优先规划" }, ]; export function CanvasCloudAgentSettings(props: AgentSettingsProps) { const [section, setSection] = useState("home"); const [skillTab, setSkillTab] = useState<"installed" | "market">("installed"); const { theme } = props; const title = section === "home" ? "Agent 设置" : sectionTitle(section); const goHome = () => setSection("home"); return (
{section === "home" ? : null} {section === "profile" ? : null} {section === "memories" ? : null} {section === "skills" ? : null} {section === "mcp" ? : null} {section === "context" ? : null} {section === "budget" ? : null}
); } function SettingsHome({ props, theme, onOpen }: { props: AgentSettingsProps; theme: CanvasTheme; onOpen: (section: SettingsSection) => void }) { return (
{reasoningOptions.map((option) => )}

推理只用于目标拆解、缺口判断和下一步工具选择,不会把内部思考过程展示给用户。

{permissionOptions.map((option) => { const Icon = option.icon; const active = option.value === props.permissionMode; return ( ); })}

默认逐项审批。自动模式可修改已授权的画布内容;图片、视频始终先创建草稿,再经独立审批才提交生成任务。

} title="上下文" summary={`${props.nodeCount} 个节点 · ${props.contextScope.length} 个范围`} onClick={() => onOpen("context")} /> } title="长期偏好" summary={profileSummary(props.profileView)} onClick={() => onOpen("profile")} /> } title="个人记忆" summary="批准、添加、导出导入;只影响你的会话" onClick={() => onOpen("memories")} /> } title="Skills · 用户技能库" summary={`${props.installedSkills.length} 个已安装 · 本轮启用 ${props.selectedSkillIds.length} 个`} onClick={() => onOpen("skills")} /> } title="工具与连接" summary="画布、技能参考文件、生成任务" onClick={() => onOpen("mcp")} /> } title="预算" summary={`每轮最多 ${props.maxCredits || "未设置"} 积分 · 固定计价模型`} onClick={() => onOpen("budget")} />
); } function MemoriesWorkspace() { return (
); } function SkillsWorkspace({ props, theme, tab, onTabChange }: { props: AgentSettingsProps; theme: CanvasTheme; tab: "installed" | "market"; onTabChange: (tab: "installed" | "market") => void }) { const query = props.skillSearch.trim().toLocaleLowerCase(); const source = tab === "installed" ? props.installedSkills.filter((skill) => `${skill.skillName} ${skill.description || ""}`.toLocaleLowerCase().includes(query)) : props.marketSkills; const allVisibleSelected = tab === "installed" && source.length > 0 && source.every((skill) => props.selectedSkillIds.includes(skill.skillId)); const toggleAll = () => source.forEach((skill) => { const selected = props.selectedSkillIds.includes(skill.skillId); if (tab === "installed" && selected === allVisibleSelected) props.onSkillToggle(skill.skillId); }); return (
onTabChange("installed")} /> onTabChange("market")} />
} value={props.skillSearch} onChange={(event) => props.onSkillSearch(event.target.value)} placeholder={tab === "market" ? "搜索 Skill 市场" : "筛选已安装 Skill"} aria-label="搜索 Skills" className="mt-3 !h-10 !rounded-xl" />
{tab === "market" ? "发现适合当前创作的技能" : `${source.length} 个匹配的 Skill`} {tab === "installed" && source.length > 0 ? : null}
{props.skillsLoading && !source.length ?
正在读取技能…
: null} {source.map((skill) => props.onSkillToggle(skill.skillId)} onInstall={() => void props.onSkillInstall(skill)} />)} {!props.skillsLoading && !source.length ?
没有匹配结果,试试其他关键词
: null} {tab === "market" && props.skillHasMore ? : null}
); } type ProfileDraft = Pick & { dirty: boolean }; function ProfileWorkspace({ props, theme }: { props: AgentSettingsProps; theme: CanvasTheme }) { const [scope, setScope] = useState("user"); const [drafts, setDrafts] = useState>({}); const [error, setError] = useState(""); const layer = profileLayer(props.profileView, scope, props.projectId, props.canvasId); const draftKey = profileDraftKey(scope, props.projectId, props.canvasId); const serverDraft: ProfileDraft = { content: layer.content, revision: layer.revision, hash: layer.hash, dirty: false }; const draft = drafts[draftKey] || serverDraft; const profileUnavailable = props.profileLoading || !props.profileView || Boolean(props.profileError); useEffect(() => { setDrafts((current) => { const existing = current[draftKey]; if (existing?.dirty) return current; if (existing && existing.revision === layer.revision && existing.hash === layer.hash && existing.content === layer.content) return current; return { ...current, [draftKey]: { content: layer.content, revision: layer.revision, hash: layer.hash, dirty: false } }; }); setError(""); }, [draftKey, layer.content, layer.hash, layer.revision]); const updateDraft = (content: string) => { setDrafts((current) => { const base = current[draftKey] || serverDraft; return { ...current, [draftKey]: { ...base, content, dirty: true } }; }); }; const save = async () => { setError(""); try { const result = await props.onSaveProfile({ scope, projectId: scope === "project" ? props.projectId : undefined, canvasId: scope === "canvas" ? props.canvasId : undefined, content: draft.content, revision: draft.revision, }); const savedLayer = profileLayer(result, scope, props.projectId, props.canvasId); setDrafts((current) => ({ ...current, [draftKey]: { content: savedLayer.content, revision: savedLayer.revision, hash: savedLayer.hash, dirty: false }, })); } catch (cause) { setError(cause instanceof Error ? cause.message : String(cause)); } }; const disabled = profileUnavailable || props.profileSaving || (scope === "project" && !props.projectId); return (
长期偏好文档

这里定义 Agent 的长期工作习惯、语气和输出结构。它不会增加工具、节点、预算或审批权限;安全契约始终由服务端代码控制。

setScope("user")} /> setScope("project")} /> setScope("canvas")} />
{scope === "project" && !props.projectId ?

当前画布尚未关联项目,项目偏好暂不可用。

: null} {props.profileLoading ?

正在读取偏好快照…

: null} {props.profileError ? : null}
保存后只影响下一次新运行。正在运行的 Agent 会固定原快照。
{error ?
{error}
: null}
); } function profileDraftKey(scope: AgentProfileScope, projectId: string | undefined, canvasId: string) { if (scope === "project") return `project:${projectId || "unbound"}`; if (scope === "canvas") return `canvas:${canvasId}`; return "user"; } function profileLayer(view: AgentProfileView | null, scope: AgentProfileScope, projectId: string | undefined, canvasId: string): AgentProfileLayer { return view?.layers.find((item) => item.scope === scope) || { scope, projectId: scope === "project" ? projectId : undefined, canvasId: scope === "canvas" ? canvasId : undefined, content: "", revision: 0, hash: "" }; } function profileSummary(view: AgentProfileView | null) { const count = view?.layers.filter((layer) => layer.content.trim()).length || 0; return count ? `${count} 层已配置 · 仅影响行为偏好` : "未配置 · 仅影响行为偏好"; } function scopeLabel(scope: AgentProfileScope) { return scope === "user" ? "用户偏好" : scope === "project" ? "项目偏好" : "当前画布偏好"; } function ProfileError({ message, onRetry, theme }: { message: string; onRetry: () => Promise; theme: CanvasTheme }) { return
{message}
; } function SkillRow({ skill, theme, selected, installed, selectable, onToggle, onInstall }: { skill: Skill; theme: CanvasTheme; selected: boolean; installed: boolean; selectable: boolean; onToggle: () => void; onInstall: () => void }) { return (
{selectable ? : }
{skill.skillName}{skill.version ? v{skill.version} : null}

{skill.description || "暂无技能说明"}

{!selectable ? : null}
); } function McpWorkspace({ theme }: { theme: CanvasTheme }) { return (
服务端内置工具

按本轮权限开放,实际能力在发送前向后端确认。Skills 提供操作知识,不会提升工具权限。

{["已保存画布读取", "技能参考文件(固定版本)", "文本 / Markdown 节点写入", "系统模型媒体生成与任务查询"].map((tool) =>
{tool}受权限约束
)}

自定义 MCP 暂未开放。实际工具权限、密钥和调用配额需由 Agent 后端校验,不能在浏览器中假设已启用。

); } function ContextWorkspace({ props, theme }: { props: AgentSettingsProps; theme: CanvasTheme }) { return
Agent 可以读取什么
只影响本次新运行的上下文范围
{contextOptions.map((option) => { const active = props.contextScope.includes(option.value); return ; })}
; } function BudgetWorkspace({ props, theme }: { props: AgentSettingsProps; theme: CanvasTheme }) { return
本轮累计预算

积分预算用于费用保护;生成任务和视频秒数填 0 表示不设该项上限。模型循环由预算和运行状态控制,不再用固定次数截断。

; } function SettingRow({ theme, icon, title, summary, onClick }: { theme: CanvasTheme; icon: ReactNode; title: string; summary: string; onClick: () => void }) { return ; } function TabButton({ active, label, onClick, theme }: { active: boolean; label: string; onClick: () => void; theme: CanvasTheme }) { return ; } function SettingLabel({ label, hint }: { label: string; hint?: string }) { return
{label}{hint ? {hint} : null}
; } function BudgetInput({ label, hint, value, onChange, theme }: { label: string; hint: string; value: string; onChange: (value: string) => void; theme: CanvasTheme }) { return ; } function sectionTitle(section: SettingsSection) { return section === "profile" ? "长期偏好" : section === "memories" ? "个人记忆" : section === "skills" ? "Skills" : section === "mcp" ? "MCP 与工具" : section === "context" ? "上下文" : "预算"; } function sectionSubtitle(section: SettingsSection) { return section === "profile" ? "用户、项目和画布的长期行为偏好" : section === "memories" ? "只属于你,批准后才会注入会话" : section === "skills" ? "搜索、安装并选择本轮技能" : section === "mcp" ? "云端工具与连接状态" : section === "context" ? "控制 Agent 能读取的范围" : "控制本轮积分与生成消耗"; } export function agentPermissionLabel(mode: AgentPermissionMode) { return permissionOptions.find((option) => option.value === mode)?.label || "请求审批"; } export function agentPermissionVisual(mode: AgentPermissionMode) { const option = permissionOptions.find((item) => item.value === mode) || permissionOptions[0]; return { color: option.color, soft: `${option.color}1f`, icon: option.icon }; } export function agentPermissionMenuItems(mode: AgentPermissionMode, onChange: (mode: AgentPermissionMode) => void) { return permissionOptions.map((option) => ({ key: option.value, label: {option.label}, icon: mode === option.value ? : , onClick: () => onChange(option.value) })); }