diff --git a/.project-docs/30-worklog/tasks/20260831-consolidate-plugin-navigation-3d9a6c71.md b/.project-docs/30-worklog/tasks/20260831-consolidate-plugin-navigation-3d9a6c71.md
new file mode 100644
index 0000000..b652731
--- /dev/null
+++ b/.project-docs/30-worklog/tasks/20260831-consolidate-plugin-navigation-3d9a6c71.md
@@ -0,0 +1,82 @@
+# Task: Consolidate plugin navigation into project configuration
+
+## Identity
+
+- Task ID: 20260831-consolidate-plugin-navigation-3d9a6c71
+- Mode: Feature
+- Branch: codex/20260831-consolidate-plugin-navigation-3d9a6c71-consolidate-plugin-navigation
+- Worktree: /Users/inmanx/Documents/makelore-consolidate-plugin-navigation-3d9a6c71
+- Base commit: 38f85f6b5e4dc4e2c5e5b9f8f4506554cfd578f5
+- Owner: codex
+- Status: Completed
+
+## Scope
+
+- Replace the separate global “插件中心” and “我的插件” navigation entries with one plugin hub that exposes “发现插件” and “我的插件” as tabs.
+- Remove the sidebar “项目插件” entry and embed project plugin enablement/configuration in the existing Project Configuration resource area beside models, skills, and knowledge.
+- Preserve account acquisition, device installation, project enablement, Agent skill assignment, runtime authorization, and billing as independent lifecycle states.
+- Keep `/my-plugins` and `/project-plugins` as compatibility redirects to the consolidated destinations.
+- Update focused unit tests, Electron E2E coverage, and README product-state documentation.
+
+## Intent And Constraints
+
+- Reduce navigation duplication without changing Main-owned plugin contracts or `.makelore/project.json` authority.
+- Reuse the existing light visual system, Radix tabs/sheets, stores, and Host API clients; add no dependency or alternate backend path.
+- Keep plugin discovery/account/device management global, while project enablement and Agent assignment remain project-scoped.
+- Do not silently acquire, download, enable, assign, authorize, or bill as a side effect of another action.
+- Preserve unavailable/removed plugin IDs and explicit status messaging rather than inventing metadata or hiding retained configuration.
+
+## Outcome
+
+- Consolidated the two global plugin destinations into one `/plugin-marketplace`
+ hub. “发现插件” and “我的插件” are URL-backed tabs, and the sidebar now exposes
+ only the single “插件中心” entry.
+- Removed the project plugin sidebar entry and added “项目插件” beside models,
+ Skills, and knowledge files in Project Configuration. The resource card reports
+ enabled/available counts and opens the existing project plugin projection in a
+ wider configuration sheet.
+- Reused the existing Marketplace, Library, device installation, project plugin,
+ and Agent Skill stores. Enabling a project plugin refreshes the Skill projection,
+ but does not acquire, download, configure a backend, or assign a Skill.
+- Preserved compatibility for saved/deep links: `/my-plugins` redirects to the
+ “我的插件” tab and `/project-plugins` redirects to the Project Configuration
+ plugin sheet. My Plugins project actions now target the consolidated routes.
+- Kept the standalone view variants available for focused rendering tests while
+ adding semantic embedded `section` variants for the tab and configuration sheet.
+- Updated README product-state copy and focused unit/Electron E2E coverage.
+
+## Verification
+
+- Exact `pnpm@10.33.4` was used for repository commands.
+- `pnpm run typecheck`: passed.
+- Focused Vitest: 2 files / 17 tests passed.
+- `pnpm test`: 212 files / 1,728 tests passed; 3 existing conditional tests skipped.
+- `pnpm run lint:check`: passed with zero errors and five pre-existing warnings
+ (`Home/index.tsx` hook dependency and four `Makelore/index.tsx` Fast Refresh warnings).
+- `pnpm run build:vite`: passed for Renderer, Electron Main, Preload, and utility
+ worker; existing dynamic-import and large-chunk warnings remain non-fatal.
+- Focused Electron E2E: 3/3 passed across unified plugin discovery/account-device
+ management and Project Configuration plugin enablement.
+- `git diff --check`: passed.
+
+## Follow-ups
+
+- Canonical `.project-docs` navigation wording should be promoted only by a later
+ Integration Gate; no plugin lifecycle, trust, billing, or backend contract changed.
+
+## Promotion Candidates
+
+- Target canonical documents: `.project-docs/30-worklog/current-state.md` and
+ `.project-docs/20-architecture/module-map.md`.
+- Proposal: record that Renderer now exposes a single global Plugin Center with
+ discovery/account-device tabs, while project enablement/configuration is embedded
+ in Project Configuration beside models, Skills, and knowledge.
+- Evidence: this task's source diff, 17 focused unit tests, the 1,728-test full unit
+ suite, three focused Electron E2E flows, typecheck, lint, and production build.
+- Future impact: new navigation, documentation, and E2E work should target
+ `/plugin-marketplace` and `/project-config?resource=plugins`; the legacy routes are
+ compatibility redirects only.
+- Semantic conflicts: none identified. The existing independent lifecycle and
+ Main-owned trust boundaries remain unchanged.
+- Human confirmation: not required for the factual promotion; the user explicitly
+ approved this consolidation, but Integration Gate ownership is still required.
diff --git a/README.md b/README.md
index c30e2fd..db2c9b3 100644
--- a/README.md
+++ b/README.md
@@ -29,7 +29,7 @@ Makelore 是一个面向软件、视觉创作、互动学习与智能机器人
- AI 绘画健壮性:Renderer 为每次命令生成稳定 operation id,网络结果未知时保留原命令供原样重试,不把未知写入当失败或创建第二次生成。Main 负责 Token 刷新、Agent Gateway REST 提交和有界 Run 查询,并将可恢复事件流投影为 Host API SSE;断线后按事件游标续接并重新读取权威 Workspace。Canvas 只使用 Works Square 云端 V2 契约,没有本地语义适配器或降级路径,上游不可用时明确报错。
- AI 绘画项目栏只展示 Workspace,不再在项目下创建独立设计会话。删除时必须完整输入项目名称;删除后项目、Living Form、任务、参考图和生成作品会从账户中隐藏且无法访问,不影响用户已另存到磁盘的副本。删除当前项目后自动打开最近更新的剩余项目,删除最后一个项目后进入空状态。
- AI 学习:主区展示服务端分页项目卡片,详情页用安全 Markdown 渲染 README;原始 HTML 被禁用,Markdown 图片节点直接加载服务端校验后的无凭据 HTTPS URL,包括 SVG 和 Electron 支持的其他图片格式,不经过服务端下载、识别、转码或镜像。下载按钮打开系统保存对话框,Main 不按 `Content-Length`、声明字节数或客户端上限阻断下载,流式校验 SHA-256 与 ZIP 签名后原子保存。客户端不提供课程生成、课程播放器、本地课程库、Agent、ASR 或课堂 runtime。运营管理与接口字段见 [`docs/learning-project-catalog-server-contract.md`](docs/learning-project-catalog-server-contract.md)。
-- 插件市场 Release A:插件中心(Marketplace)提供运营精选的免费 `skill_only` 插件;“免费获取”只写入账号 Library,“下载/更新”才写入本机 Package Store,“启用到项目”和“分配给伙伴”仍是 Project Plugins 中彼此独立的动作。我的插件(My Plugins)展示账号获取状态、本机安装/更新/删除设备包、移除后的 tombstone 和 bounded unavailable reason;Project Plugins 继续只修改项目选择,不会因获取或下载自动启用或分配。
+- 插件市场 Release A:侧栏只保留一个“插件中心”,其中“发现插件”与“我的插件”两个标签分别承载 Marketplace 浏览获取,以及账号 Library、本机安装/更新/删除设备包、移除后的 tombstone 和 bounded unavailable reason。项目插件入口位于项目配置,与模型、Skill 和知识文件并列;“免费获取”只写入账号 Library,“下载/更新”才写入本机 Package Store,“启用到项目”和“分配给伙伴”仍是彼此独立的动作,不会因获取或下载自动推进。
- 插件运行架构:Renderer 只调用 Main-owned Marketplace facade;Main 负责账号、请求 deadline、签名/摘要校验、不可变 Release、current selection 与原子回滚。下一代 Pi worker 使用同一个 effective snapshot,将每个有效 Skill 与已验证 Package Store root 成对传给 resource loader、Extension Host 和 CLI;`skill_only` 不依赖运行时 Policy,也不执行分发包中的任意代码。正式激活仍等待官方 Ed25519 公钥(production key activation HOLD);生产私钥只能来自部署 secret,测试使用注入的临时密钥。
- 提示词博物馆:只陈列经过审核的作品预览、Prompt、分类以及作者/来源/许可证信息,支持搜索、使用场景/风格/主体筛选和详情抽屉;“使用此 Prompt”只把原文带回当前 Canvas 对话草稿,不自动发送、不构成社区。列表和详情数据由服务端提供,客户端不打包数据集;服务端字段契约见 [`docs/prompt-museum-server-contract.md`](docs/prompt-museum-server-contract.md)。
- 视觉系统:单一浅色主题,品牌蓝 `#3A5578`、星火橙 `#F26A3D`、白色画布与低饱和蓝灰层级。
diff --git a/src/App.tsx b/src/App.tsx
index b991028..c3967a1 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -32,9 +32,7 @@ const Chat = lazy(() => import('./pages/Chat').then(({ Chat: component }) => ({
const Home = lazy(() => import('./pages/Home').then(({ Home: component }) => ({ default: component })));
const PreviewScene = lazy(() => import('./pages/Makelore').then(({ PreviewScene: component }) => ({ default: component })));
const ProjectConfiguration = lazy(() => import('./pages/ProjectConfiguration').then(({ ProjectConfiguration: component }) => ({ default: component })));
-const ProjectPlugins = lazy(() => import('./pages/ProjectPlugins').then(({ ProjectPlugins: component }) => ({ default: component })));
const PluginMarketplace = lazy(() => import('./pages/PluginMarketplace').then(({ PluginMarketplace: component }) => ({ default: component })));
-const MyPlugins = lazy(() => import('./pages/MyPlugins').then(({ MyPlugins: component }) => ({ default: component })));
const Workbench = lazy(() => import('./pages/Workbench').then(({ Workbench: component }) => ({ default: component })));
const ImageCanvas = lazy(() => import('./pages/ImageCanvas').then(({ ImageCanvas: component }) => ({ default: component })));
const ImagePromptMuseum = lazy(() => import('./pages/ImagePromptMuseum').then(({ ImagePromptMuseum: component }) => ({ default: component })));
@@ -446,9 +444,9 @@ function App() {
)}
>
} />
- } />
+ } />
} />
- } />
+ } />
} />
} />
} />
diff --git a/src/components/layout/Sidebar.tsx b/src/components/layout/Sidebar.tsx
index f7502cc..b123837 100644
--- a/src/components/layout/Sidebar.tsx
+++ b/src/components/layout/Sidebar.tsx
@@ -12,8 +12,6 @@ import {
FolderKanban,
LogOut,
Plus,
- Puzzle,
- Library,
Store,
Settings as SettingsIcon,
UserCircle2,
@@ -604,21 +602,6 @@ export function Sidebar({ workspaceLayout = false, sidebarPeekOpen = false, onSi
{!sidebarCollapsed ? 插件中心 : null}
-
- {activeProject ? (
-
- ) : null}
>
) : isLearningModule ? (
diff --git a/src/pages/MyPlugins/index.tsx b/src/pages/MyPlugins/index.tsx
index 34f35af..5e8c856 100644
--- a/src/pages/MyPlugins/index.tsx
+++ b/src/pages/MyPlugins/index.tsx
@@ -37,6 +37,7 @@ function actionErrorText(error?: string | null): string | null {
}
export type MyPluginsViewProps = {
+ embedded?: boolean;
library: MarketplaceLibrarySnapshot | null;
installations: Record;
state: 'idle' | 'loading' | 'ready' | 'error';
@@ -53,9 +54,10 @@ export type MyPluginsViewProps = {
export function MyPluginsView(props: MyPluginsViewProps) {
const visibleError = actionErrorText(props.error);
+ const Root = props.embedded ? 'section' : 'main';
return (
-
-
+
+
{props.library?.stale ? 正在显示上次可信的插件库;服务恢复前,在线计量或托管能力保持不可用。
: null}
{props.state === 'loading' && !props.library ? 正在读取我的插件…
: null}
{props.state === 'error' && !props.library ? 无法读取我的插件
{props.error ?? '请稍后重试。'}
: null}
@@ -87,17 +89,17 @@ export function MyPluginsView(props: MyPluginsViewProps) {
{!removed && plugin.acquisition !== 'system_included' && plugin.betaVersion && installedChannel !== 'beta' ? : null}
{!removed && plugin.acquisition !== 'system_included' ? : null}
{hasDevicePackage ? : null}
-
+
;
})}
-
+
);
}
-export function MyPlugins() {
+export function MyPlugins({ embedded = false }: { embedded?: boolean } = {}) {
const user = useAuthStore((state) => state.user);
const accountKey = user?.userId ?? user?.username ?? null;
const library = usePluginMarketplaceStore((state) => state.library);
@@ -113,5 +115,5 @@ export function MyPlugins() {
toast.error(reason instanceof Error ? reason.message : String(reason));
});
const store = pluginMarketplaceStore.getState;
- return safe(store().loadLibrary())} onInstall={(id) => safe(store().install(id))} onInstallBeta={(id) => safe(store().installBeta(id))} onUpdate={(id) => safe(store().update(id))} onUninstall={(id) => safe(store().uninstall(id))} onRemove={(id) => safe(store().remove(id))} onReacquire={(id) => safe(store().acquire(id))} />;
+ return safe(store().loadLibrary())} onInstall={(id) => safe(store().install(id))} onInstallBeta={(id) => safe(store().installBeta(id))} onUpdate={(id) => safe(store().update(id))} onUninstall={(id) => safe(store().uninstall(id))} onRemove={(id) => safe(store().remove(id))} onReacquire={(id) => safe(store().acquire(id))} />;
}
diff --git a/src/pages/PluginMarketplace/index.tsx b/src/pages/PluginMarketplace/index.tsx
index f12130e..07c5be3 100644
--- a/src/pages/PluginMarketplace/index.tsx
+++ b/src/pages/PluginMarketplace/index.tsx
@@ -1,8 +1,11 @@
import { FormEvent, useEffect, useMemo, useState } from 'react';
import { CheckCircle2, Download, Puzzle, Search, ShieldCheck } from 'lucide-react';
+import { useSearchParams } from 'react-router-dom';
import { toast } from 'sonner';
import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button';
+import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
+import { MyPlugins } from '@/pages/MyPlugins';
import type {
MarketplaceCatalogPage,
MarketplaceCatalogQuery,
@@ -20,6 +23,7 @@ const USAGE_COPY: Record = {
};
export type PluginMarketplaceViewProps = {
+ embedded?: boolean;
catalog: MarketplaceCatalogPage | null;
detail: MarketplacePluginDetail | null;
library: MarketplaceLibrarySnapshot | null;
@@ -42,12 +46,15 @@ export function PluginMarketplaceView(props: PluginMarketplaceViewProps) {
event.preventDefault();
void props.onSearch({ ...(query.trim() ? { query: query.trim() } : {}), ...(category ? { category } : {}), limit: 24 });
};
+ const Root = props.embedded ? 'section' : 'main';
return (
-
+
+
);
}
export function PluginMarketplace() {
+ const [searchParams, setSearchParams] = useSearchParams();
+ const activeTab = searchParams.get('tab') === 'mine' ? 'mine' : 'discover';
const user = useAuthStore((state) => state.user);
const accountKey = user?.userId ?? user?.username ?? null;
const catalog = usePluginMarketplaceStore((state) => state.catalog);
@@ -119,11 +128,34 @@ export function PluginMarketplace() {
const [selectedId, setSelectedId] = useState(null);
useEffect(() => {
pluginMarketplaceStore.getState().activateAccount(accountKey);
- void pluginMarketplaceStore.getState().loadCatalog({ limit: 24 }).catch(() => undefined);
+ if (activeTab === 'discover') void pluginMarketplaceStore.getState().loadCatalog({ limit: 24 }).catch(() => undefined);
if (accountKey) void pluginMarketplaceStore.getState().loadLibrary().catch(() => undefined);
- }, [accountKey]);
+ }, [accountKey, activeTab]);
const safe = (operation: Promise) => operation.catch((error) => {
toast.error(error instanceof Error ? error.message : String(error));
});
- return safe(pluginMarketplaceStore.getState().loadCatalog(input))} onSelect={(pluginId) => { setSelectedId(pluginId); return safe(pluginMarketplaceStore.getState().loadDetail(pluginId)); }} onAcquire={(pluginId) => safe(pluginMarketplaceStore.getState().acquire(pluginId))} />;
+ const handleTabChange = (value: string) => {
+ const next = new URLSearchParams(searchParams);
+ if (value === 'mine') next.set('tab', 'mine');
+ else next.delete('tab');
+ setSearchParams(next, { replace: true });
+ };
+ return
+
+
+
+ 发现插件
+ 我的插件
+
+
+ safe(pluginMarketplaceStore.getState().loadCatalog(input))} onSelect={(pluginId) => { setSelectedId(pluginId); return safe(pluginMarketplaceStore.getState().loadDetail(pluginId)); }} onAcquire={(pluginId) => safe(pluginMarketplaceStore.getState().acquire(pluginId))} />
+
+
+
+
+
+ ;
}
diff --git a/src/pages/ProjectConfiguration/index.tsx b/src/pages/ProjectConfiguration/index.tsx
index 6dcbdcc..4dbd8ad 100644
--- a/src/pages/ProjectConfiguration/index.tsx
+++ b/src/pages/ProjectConfiguration/index.tsx
@@ -1,6 +1,6 @@
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { useLocation, useNavigate } from 'react-router-dom';
-import { ArrowLeft, Blocks, Check, Cpu, FileText, Files, FolderOpen, Plus, Trash2, Upload, X } from 'lucide-react';
+import { ArrowLeft, Blocks, Check, Cpu, FileText, Files, FolderOpen, Plus, Puzzle, Trash2, Upload, X } from 'lucide-react';
import { toast } from 'sonner';
import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button';
@@ -8,6 +8,7 @@ import { ConfirmDialog } from '@/components/ui/confirm-dialog';
import { Sheet, SheetClose, SheetContent, SheetDescription, SheetTitle } from '@/components/ui/sheet';
import { AgentCreationDialog, type AgentCreationInput } from '@/components/coding/AgentCreationDialog';
import { ProjectPublishAction } from '@/components/works/ProjectPublishAction';
+import { ProjectPlugins } from '@/pages/ProjectPlugins';
import { buildCodingModelOptions, parseCodingModelKey, type CodingModelOption } from '@/lib/coding-model-options';
import { getCodingSkills } from '@/lib/coding-product-tools';
import { abortCodingConversation } from '@/lib/coding-conversations';
@@ -16,6 +17,7 @@ import { getSkillDisplayInfo } from '@/lib/skill-display';
import { getAgentAvatarSrc } from '@/lib/agent-avatars';
import { cn } from '@/lib/utils';
import { codingConversationStore } from '@/stores/coding-conversations';
+import { codingPluginsStore, useCodingPluginsStore } from '@/stores/coding-plugins';
import { useCodingWorkspaceStore } from '@/stores/coding-workspace';
import { useProjectConfigStore } from '@/stores/project-config';
import { useProviderStore } from '@/stores/providers';
@@ -23,7 +25,7 @@ import type { CodingProjectAgent, CodingProjectConfig } from '@/types/coding-pro
import type { ProjectIdentityChoice } from '../../../shared/coding-project-contracts';
const EMPTY_FILES: string[] = [];
-type DrawerMode = 'models' | 'knowledge' | 'skills' | null;
+type DrawerMode = 'models' | 'knowledge' | 'skills' | 'plugins' | null;
type ProjectIdentityKind = ProjectIdentityChoice['kind'];
type SkillStructureEntry = { path: string; type: 'file' | 'directory' };
type SkillInfo = {
@@ -141,6 +143,8 @@ export function ProjectConfiguration() {
const providerAccounts = useProviderStore((state) => state.accounts);
const providerVendors = useProviderStore((state) => state.vendors);
const refreshProviderSnapshot = useProviderStore((state) => state.refreshProviderSnapshot);
+ const pluginProjection = useCodingPluginsStore((state) => state.projection);
+ const pluginLoadState = useCodingPluginsStore((state) => state.loadState);
const [draft, setDraft] = useState(null);
const [drawerMode, setDrawerMode] = useState(null);
const [activeSkillId, setActiveSkillId] = useState(null);
@@ -185,17 +189,29 @@ export function ProjectConfiguration() {
setActiveAgentId(requestedAgentId);
setPartnerDialogOpen(true);
}, [draft, location.search]);
+ useEffect(() => {
+ if (new URLSearchParams(location.search).get('resource') === 'plugins') setDrawerMode('plugins');
+ }, [location.search]);
useEffect(() => { loadSkills(); }, [loadSkills]);
useEffect(() => {
if (drawerMode === 'skills') loadSkills();
}, [drawerMode, loadSkills]);
useEffect(() => { void refreshProviderSnapshot(); }, [refreshProviderSnapshot]);
+ useEffect(() => {
+ if (activeProject) void codingPluginsStore.getState().load(activeProject.id).catch(() => undefined);
+ }, [activeProject]);
const modelOptions = useMemo(
() => buildCodingModelOptions(providerAccounts, providerVendors),
[providerAccounts, providerVendors],
);
const activeAgent = draft?.agents.find((agent) => agent.id === activeAgentId) ?? null;
const activeSkill = skills.find((skill) => skill.id === activeSkillId) ?? null;
+ const currentPluginProjection = pluginProjection?.project.localProjectId === activeProject?.id ? pluginProjection : null;
+ const availablePluginCount = currentPluginProjection?.items.length ?? 0;
+ const enabledPluginCount = currentPluginProjection?.items.filter(({ enabled }) => enabled).length ?? 0;
+ const pluginSummary = !currentPluginProjection
+ ? pluginLoadState === 'error' ? '项目插件暂不可用' : '正在读取项目插件'
+ : `${enabledPluginCount} 个已启用 · ${availablePluginCount} 个可用`;
const playSaveConfirmation = () => undefined;
const handleBack = () => navigate(-1);
@@ -455,8 +471,8 @@ export function ProjectConfiguration() {
)}
- } title="可用模型" subtitle={`${modelOptions.length} 个可用模型`} onClick={() => setDrawerMode('models')} />} title="可用技能" subtitle={`${skills.length} 项可用技能`} onClick={() => { setActiveSkillId(null); setDrawerMode('skills'); }} />} title="知识文件" subtitle={`${knowledge.length} 个知识文件`} onClick={() => setDrawerMode('knowledge')} />
- 项目智能体
智能体配置仅属于当前项目;每个智能体可以拥有多条独立对话。
{draft.agents.filter((agent) => !agent.archivedAt).map((agent) =>
{ setActiveAgentId(agent.id); setPartnerDialogOpen(true); }} />)}{draft.agents.every((agent) => agent.archivedAt) ? 当前项目还没有智能体,点击右上角的 + 创建。
: null} {draft.agents.some((agent) => agent.archivedAt) ? 已归档
{draft.agents.filter((agent) => agent.archivedAt).map((agent) => )}
: null}
+ } title="可用模型" subtitle={`${modelOptions.length} 个可用模型`} onClick={() => setDrawerMode('models')} />} title="可用技能" subtitle={`${skills.length} 项可用技能`} onClick={() => { setActiveSkillId(null); setDrawerMode('skills'); }} />} title="知识文件" subtitle={`${knowledge.length} 个知识文件`} onClick={() => setDrawerMode('knowledge')} />} title="项目插件" subtitle={pluginSummary} onClick={() => setDrawerMode('plugins')} />
+ 项目智能体
智能体配置仅属于当前项目;每个智能体可以拥有多条独立对话。
{draft.agents.filter((agent) => !agent.archivedAt).map((agent) =>
{ setActiveAgentId(agent.id); setPartnerDialogOpen(true); }} />)}{draft.agents.every((agent) => agent.archivedAt) ? 当前项目还没有智能体,点击右上角的 + 创建。
: null} {draft.agents.some((agent) => agent.archivedAt) ? 已归档
{draft.agents.filter((agent) => agent.archivedAt).map((agent) => )}
: null}
@@ -486,11 +502,12 @@ export function ProjectConfiguration() {
title={activeAgent ? `${activeAgent.name || '未命名智能体'} · 编辑智能体` : '创建项目智能体'}
submitLabel={activeAgent ? '保存智能体' : '创建智能体'}
/>
-
{ if (!open) { setDrawerMode(null); setActiveSkillId(null); } }}>
+ { if (!open) { setDrawerMode(null); setActiveSkillId(null); } }}>
{drawerMode === 'models' ? setDrawerMode(null)}> : null}
{drawerMode === 'knowledge' ? setDrawerMode(null)}> { void handleKnowledge(event.target.files?.[0]); event.currentTarget.value = ''; }} />{knowledge.length ? knowledge.map((file) =>
{file}
) :
当前还没有知识文件。
}
: null}
{drawerMode === 'skills' && activeSkill ? setActiveSkillId(null)} closeLabel="返回技能列表" closeIcon="back"> : null}
{drawerMode === 'skills' && !activeSkill ? setDrawerMode(null)}>{skills.length > 0 ? skills.map((skill) =>
) :
当前没有可查看的技能。
}
: null}
+ {drawerMode === 'plugins' ? setDrawerMode(null)}> setDrawerMode(null)} onManageAgents={() => setDrawerMode(null)} /> : null}
;
onRemoveCollection(collection: string): void | Promise;
onRemoveProject(): void | Promise;
+ onManageAgents?(): void;
+ onResolveIdentity?(): void;
};
export function ProjectPluginsView(props: ViewProps) {
@@ -69,16 +72,25 @@ export function ProjectPluginsView(props: ViewProps) {
const pendingToggle = selected ? Boolean(props.pending[`enabled:${props.projection.project.localProjectId}:${selected.id}`]) : false;
const libraryEntry = selected ? props.marketplaceLibrary?.items.find(({ pluginId }) => pluginId === selected.id) : null;
const installation = selected ? props.installations?.[selected.id] : null;
+ const Root = props.embedded ? 'section' : 'main';
+ const manageAgents = () => {
+ if (props.onManageAgents) props.onManageAgents();
+ else navigate('/project-config');
+ };
+ const resolveIdentity = () => {
+ if (props.onResolveIdentity) props.onResolveIdentity();
+ else navigate('/project-config');
+ };
return (
-
-