From 6098cf678acde8cbcccde62b11cb6e76e06825b6 Mon Sep 17 00:00:00 2001 From: Rick Ye Date: Fri, 11 Sep 2026 11:11:40 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=8F=92=E4=BB=B6=E5=9B=BE=E6=A0=87?= =?UTF-8?q?=E8=A7=84=E8=8C=83=E5=8C=96=E2=80=94=E2=80=94manifest/=E8=83=B6?= =?UTF-8?q?=E5=9B=8A=20icon=20=E7=BB=9F=E4=B8=80=20lucide=20=E5=9B=BE?= =?UTF-8?q?=E6=A0=87=E5=90=8D=EF=BC=88=E7=99=BD=E5=90=8D=E5=8D=95=20101=20?= =?UTF-8?q?=E4=B8=AA=EF=BC=89=EF=BC=8C=E6=9C=AA=E7=9F=A5=E5=90=8D=E5=9B=9E?= =?UTF-8?q?=E8=90=BD=E6=8B=BC=E5=9B=BE=E5=9B=BE=E6=A0=87=EF=BC=9B=E5=86=85?= =?UTF-8?q?=E8=81=94=20svg=20=E4=BB=85=E6=97=A7=E5=85=BC=E5=AE=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/desktop/src/renderer/src/App.tsx | 7 +- .../renderer/src/components/SettingsModal.tsx | 12 +- .../renderer/src/components/SidebarNav.tsx | 4 +- .../src/plugins/PluginStatusCapsules.tsx | 6 +- .../src/renderer/src/plugins/plugin-icons.tsx | 242 ++++++++++++++++++ docs/plugins.md | 2 +- docs/plugins/DESIGN.md | 2 +- packages/plugin-sdk/src/manifest.ts | 2 +- packages/plugin-template/manifest.json | 2 +- 9 files changed, 263 insertions(+), 16 deletions(-) create mode 100644 apps/desktop/src/renderer/src/plugins/plugin-icons.tsx diff --git a/apps/desktop/src/renderer/src/App.tsx b/apps/desktop/src/renderer/src/App.tsx index 2fd4c6d..2fb1c14 100644 --- a/apps/desktop/src/renderer/src/App.tsx +++ b/apps/desktop/src/renderer/src/App.tsx @@ -12,7 +12,8 @@ import { SettingsModal } from './components/SettingsModal' import { Search } from './components/Search' import { BrowserPanel } from './components/browser/BrowserPanel' import { SidePanel, type PanelTabIcon } from './components/SidePanel' -import { PluginPageFrame, PluginIcon } from './plugins/PluginPageFrame' +import { PluginPageFrame } from './plugins/PluginPageFrame' +import { PluginIconValue } from './plugins/plugin-icons' import { useExternalPluginComponents, usePluginRendererLoader, @@ -316,9 +317,9 @@ export default function App(): JSX.Element { const pluginTabIcons = useMemo(() => { const map = new Map() for (const p of panelPlugins) { - const svg = p.manifest.icon + const icon = p.manifest.icon const Icon = ({ size, className }: { size?: number; className?: string }): JSX.Element => ( - + ) map.set(p.manifest.id, Icon) } diff --git a/apps/desktop/src/renderer/src/components/SettingsModal.tsx b/apps/desktop/src/renderer/src/components/SettingsModal.tsx index ea0a5b2..d9a4404 100644 --- a/apps/desktop/src/renderer/src/components/SettingsModal.tsx +++ b/apps/desktop/src/renderer/src/components/SettingsModal.tsx @@ -31,7 +31,7 @@ import type { import type { PluginSettingsSchema, PluginSettingsSchemas, PluginSettingValues, PluginSettingsValuesMap } from '@aso/plugin-sdk' import { THEME_MODES, THEME_PRESETS, applyThemeColor } from '../lib/theme' import { usePluginList } from '../plugins/store' -import { PluginIcon } from '../plugins/PluginPageFrame' +import { PluginIconValue } from '../plugins/plugin-icons' type PluginSettingsTab = `plugin:${string}` type Tab = 'general' | 'model' | 'prompt' | 'plugins' | 'skills' | 'commands' | PluginSettingsTab @@ -818,12 +818,16 @@ function PluginsPanel({ // 插件设置导航图标(svg 字符串 → 组件;按 id+svg 缓存保证身份稳定,避免导航项重挂载) const pluginSettingsIconCache = new Map() -function pluginSettingsIcon(id: string, svg: string | undefined): IconType { - const key = `${id}:${svg ?? ''}` +function pluginSettingsIcon(id: string, icon: string | undefined): IconType { + const key = `${id}:${icon ?? ''}` const cached = pluginSettingsIconCache.get(key) if (cached) return cached const Icon: IconType = (props) => ( - + ) pluginSettingsIconCache.set(key, Icon) return Icon diff --git a/apps/desktop/src/renderer/src/components/SidebarNav.tsx b/apps/desktop/src/renderer/src/components/SidebarNav.tsx index 2bceb2c..8493df6 100644 --- a/apps/desktop/src/renderer/src/components/SidebarNav.tsx +++ b/apps/desktop/src/renderer/src/components/SidebarNav.tsx @@ -15,7 +15,7 @@ import { import { LuPanelLeft } from 'react-icons/lu' import { Button } from '@/components/atoms/Button' import { GlideMenu } from '@/components/GlideMenu' -import { PluginIcon } from '@/plugins/PluginPageFrame' +import { PluginIconValue } from '@/plugins/plugin-icons' import type { PluginInfo, SessionMeta, SessionState, WorkspaceMeta } from '@shared/types' /** 可折叠工作区导航(beautifului.dev Sidebar Nav)。 @@ -445,7 +445,7 @@ export function SidebarNav({ className={`${navRowClass} ${active ? 'bg-hover-2 text-ink' : ''}`} > - + {plugin.manifest.name} diff --git a/apps/desktop/src/renderer/src/plugins/PluginStatusCapsules.tsx b/apps/desktop/src/renderer/src/plugins/PluginStatusCapsules.tsx index 4749160..7f25779 100644 --- a/apps/desktop/src/renderer/src/plugins/PluginStatusCapsules.tsx +++ b/apps/desktop/src/renderer/src/plugins/PluginStatusCapsules.tsx @@ -9,7 +9,7 @@ import { useEffect, useRef, useState } from 'react' import type { ReactNode } from 'react' import type { PluginStatusEntry } from '@shared/types' -import { PluginIcon } from './PluginPageFrame' +import { PluginIconValue } from './plugin-icons' const TONE_CLASS: Record = { default: 'border-line text-ink-2', @@ -39,7 +39,7 @@ function StatusPopover({ entry, onOpenView }: { entry: PluginStatusEntry; onOpen const clickable = Boolean(row.openView) const content = ( <> - {row.icon && } + {row.icon && } {row.label} {row.detail && {row.detail}} @@ -130,7 +130,7 @@ export function PluginStatusCapsules({ TONE_CLASS[tone] ?? TONE_CLASS.default } ${clickable ? 'hover:opacity-80 active:scale-[0.97]' : 'cursor-default'} ${open ? 'opacity-80' : ''}`} > - {item.icon && } + {item.icon && } {item.text} {open && item.detail && } diff --git a/apps/desktop/src/renderer/src/plugins/plugin-icons.tsx b/apps/desktop/src/renderer/src/plugins/plugin-icons.tsx new file mode 100644 index 0000000..2ea367c --- /dev/null +++ b/apps/desktop/src/renderer/src/plugins/plugin-icons.tsx @@ -0,0 +1,242 @@ +/** + * 插件图标规范:manifest.icon / 状态胶囊 icon 统一填 **lucide 图标名** + * (PascalCase 如 "Globe",kebab-case "layout-grid" 亦可),宿主经 react-icons/lu + * 白名单渲染——线条风格、粗细、圆角与宿主完全一致(DESIGN.md 图标规范)。 + * 旧版内联 `` 字符串仍兼容(sanitizeSvg 防线继续生效); + * 名单之外或缺失的名字一律回落拼图图标,杜绝来历不明的自绘图形。 + */ +import type { ReactNode } from 'react' +import type { IconType } from 'react-icons' +import { + LuActivity, + LuAppWindow, + LuArchive, + LuAtom, + LuAward, + LuBadgeCheck, + LuBan, + LuBell, + LuBlocks, + LuBookOpen, + LuBot, + LuBoxes, + LuBriefcase, + LuCamera, + LuCalendar, + LuChartPie, + LuChartBar, + LuCheck, + LuCircleCheck, + LuCircle, + LuCircleDot, + LuClock, + LuCloud, + LuCode, + LuCompass, + LuCpu, + LuDatabase, + LuHardDrive, + LuDownload, + LuEye, + LuEyeOff, + LuFile, + LuFileText, + LuFilter, + LuFlag, + LuFolder, + LuFolderOpen, + LuGitBranch, + LuGitCommitHorizontal, + LuGithub, + LuGitPullRequest, + LuGlobe, + LuHammer, + LuHash, + LuHeadphones, + LuHouse, + LuImage, + LuInbox, + LuKeyboard, + LuLayers, + LuLayoutGrid, + LuLink, + LuListChecks, + LuListTodo, + LuLock, + LuMail, + LuMap, + LuMessageCircle, + LuMic, + LuMonitor, + LuMoon, + LuMusic, + LuPackage, + LuPalette, + LuPaperclip, + LuPenLine, + LuPhone, + LuPlay, + LuPlug, + LuPlugZap, + LuPuzzle, + LuRocket, + LuSave, + LuScan, + LuSearch, + LuServer, + LuSettings, + LuSettings2, + LuShare2, + LuShield, + LuShoppingCart, + LuSlidersHorizontal, + LuSparkles, + LuSquareStack, + LuStar, + LuSun, + LuTable, + LuTag, + LuTarget, + LuTerminal, + LuThumbsUp, + LuTimer, + LuTrash2, + LuTrendingUp, + LuUpload, + LuUser, + LuUsers, + LuVideo, + LuWallet, + LuWebhook, + LuWrench, + LuZap +} from 'react-icons/lu' +import { PluginIcon } from './PluginPageFrame' + +/** 宿主核准的 lucide 图标白名单(manifest.icon / 胶囊 icon 可填的名字) */ +export const PLUGIN_LUCIDE_ICONS = { + Activity: LuActivity, + AppWindow: LuAppWindow, + Archive: LuArchive, + Atom: LuAtom, + Award: LuAward, + BadgeCheck: LuBadgeCheck, + Ban: LuBan, + Bell: LuBell, + Blocks: LuBlocks, + BookOpen: LuBookOpen, + Bot: LuBot, + Boxes: LuBoxes, + Briefcase: LuBriefcase, + Camera: LuCamera, + Calendar: LuCalendar, + ChartPie: LuChartPie, + ChartBar: LuChartBar, + Check: LuCheck, + CircleCheck: LuCircleCheck, + Circle: LuCircle, + CircleDot: LuCircleDot, + Clock: LuClock, + Cloud: LuCloud, + Code: LuCode, + Compass: LuCompass, + Cpu: LuCpu, + Database: LuDatabase, + HardDrive: LuHardDrive, + Download: LuDownload, + Eye: LuEye, + EyeOff: LuEyeOff, + File: LuFile, + FileText: LuFileText, + Filter: LuFilter, + Flag: LuFlag, + Folder: LuFolder, + FolderOpen: LuFolderOpen, + GitBranch: LuGitBranch, + GitCommit: LuGitCommitHorizontal, + Github: LuGithub, + GitPullRequest: LuGitPullRequest, + Globe: LuGlobe, + Hammer: LuHammer, + Hash: LuHash, + Headphones: LuHeadphones, + House: LuHouse, + Image: LuImage, + Inbox: LuInbox, + Keyboard: LuKeyboard, + Layers: LuLayers, + LayoutGrid: LuLayoutGrid, + Link: LuLink, + ListChecks: LuListChecks, + ListTodo: LuListTodo, + Lock: LuLock, + Mail: LuMail, + Map: LuMap, + MessageCircle: LuMessageCircle, + Mic: LuMic, + Monitor: LuMonitor, + Moon: LuMoon, + Music: LuMusic, + Package: LuPackage, + Palette: LuPalette, + Paperclip: LuPaperclip, + PenLine: LuPenLine, + Phone: LuPhone, + Play: LuPlay, + Plug: LuPlug, + PlugZap: LuPlugZap, + Puzzle: LuPuzzle, + Rocket: LuRocket, + Save: LuSave, + Scan: LuScan, + Search: LuSearch, + Server: LuServer, + Settings: LuSettings, + Settings2: LuSettings2, + Share2: LuShare2, + Shield: LuShield, + ShoppingCart: LuShoppingCart, + SlidersHorizontal: LuSlidersHorizontal, + Sparkles: LuSparkles, + SquareStack: LuSquareStack, + Star: LuStar, + Sun: LuSun, + Table: LuTable, + Tag: LuTag, + Target: LuTarget, + Terminal: LuTerminal, + ThumbsUp: LuThumbsUp, + Timer: LuTimer, + Trash2: LuTrash2, + TrendingUp: LuTrendingUp, + Upload: LuUpload, + User: LuUser, + Users: LuUsers, + Video: LuVideo, + Wallet: LuWallet, + Webhook: LuWebhook, + Wrench: LuWrench, + Zap: LuZap +} satisfies Record + +/** kebab/snake/全小写 → PascalCase;已是 Pascal 的名字原样通过 */ +function toPascal(name: string): string { + return name + .trim() + .replace(/[-_\s]+(.)/g, (_, c: string) => c.toUpperCase()) + .replace(/^./, (c) => c.toUpperCase()) +} + +/** 按名字查白名单图标;未命中返回 null */ +export function lucideIconByName(name: string): IconType | null { + const key = toPascal(name) + return (PLUGIN_LUCIDE_ICONS as Record)[key] ?? null +} + +/** 图标槽位统一渲染:lucide 名(规范用法)/ 内联 svg(旧兼容)/ 缺省与未知名回落拼图图标 */ +export function PluginIconValue({ icon, size = 16, className }: { icon?: string; size?: number; className?: string }): ReactNode { + const v = icon?.trim() + if (v && v.startsWith(' + const Icon = (v ? lucideIconByName(v) : null) ?? LuPuzzle + return +} diff --git a/docs/plugins.md b/docs/plugins.md index bc8420f..e05be6f 100644 --- a/docs/plugins.md +++ b/docs/plugins.md @@ -27,7 +27,7 @@ "version": "1.2.0", // semver(x.y.z) "minContract": "0.1.0", "description": "…", - "icon": "", // 侧栏/面板图标(svg 字符串;宿主清除事件属性与 script) + "icon": "Globe", // 侧栏/面板/胶囊图标:lucide 图标名(白名单见 DESIGN.md;kebab 亦可);兼容旧 svg 字符串;未知名回落拼图图标 "order": 10, // 侧栏排序(只在插件间;核心入口恒在前) "panel": true, // 在右侧 SidePanel 出现一个 tab "entry": { "page": "index.html", "main": "main.mjs", "renderer": "renderer.mjs" }, // 内置插件省略 entry diff --git a/docs/plugins/DESIGN.md b/docs/plugins/DESIGN.md index a3c51a1..bed7928 100644 --- a/docs/plugins/DESIGN.md +++ b/docs/plugins/DESIGN.md @@ -64,7 +64,7 @@ - 字号阶:`10px`(徽标)/ `11px`(辅助按钮)/ `11.5px`(次要按钮、说明)/ `12px`(正文说明) / `12.5px`(行标题/正文)/ `16px`(页面标题)。**不使用其他字号**;行高 `leading-4/5`。 - 间距基数为 4px:卡片内边距 `px-3 py-2.5`,区块间 `space-y-1.5`,小节间 `pt-3`。 -- 图标 12–16px,线条风格,`currentColor`。 +- 图标一律 **lucide**(宿主白名单 `plugin-icons.tsx`,约 100 个常用名):`manifest.icon` / 状态胶囊 `icon` 填 lucide 图标名(如 `"Globe"`,kebab-case 亦可),12–16px、`currentColor` 由宿主保证;**禁止自绘 svg**(旧内联 svg 仅对存量插件兼容),未知名回落拼图图标 ## 4. 标准模式(复制即用) diff --git a/packages/plugin-sdk/src/manifest.ts b/packages/plugin-sdk/src/manifest.ts index 66f1cc4..a7474f8 100644 --- a/packages/plugin-sdk/src/manifest.ts +++ b/packages/plugin-sdk/src/manifest.ts @@ -15,7 +15,7 @@ export interface PluginManifest { /** 最低 harness 契约版本(semver),不满足拒载 */ minContract: string description?: string - /** 侧栏入口图标:svg 字符串;缺省用宿主默认字形 */ + /** 侧栏入口图标:lucide 图标名(如 "Globe",白名单见 docs/plugins/DESIGN.md);兼容旧内联 svg 字符串;缺省/未知名回落拼图图标 */ icon?: string /** 侧栏入口排序(升序,只在插件之间排序;核心入口恒在前) */ order?: number diff --git a/packages/plugin-template/manifest.json b/packages/plugin-template/manifest.json index 5fbd322..280b0e1 100644 --- a/packages/plugin-template/manifest.json +++ b/packages/plugin-template/manifest.json @@ -6,7 +6,7 @@ "description": "TODO:一句话描述插件做什么", "order": 50, "panel": true, - "icon": "", + "icon": "Puzzle", "entry": { "main": "main.mjs", "renderer": "renderer.mjs"