diff --git a/README.md b/README.md index 315aa7466..7414f4d43 100644 --- a/README.md +++ b/README.md @@ -56,8 +56,8 @@ 优先级:`SerpAPI > BingSerpAPI > DuckDuckGo` -- [ ] 插件列表页面开发 -- [ ] 支持开关指定插件 +- [x] 插件列表页面开发 +- [x] 支持开关指定插件 - [ ] 支持添加自定义插件 - [x] 支持 Agent 参数配置( ~~agentType~~, maxIterations, returnIntermediateSteps 等) - [x] 支持 ChatSession 级别插件功能开关 diff --git a/app/api/langchain/tool/agent/route.ts b/app/api/langchain/tool/agent/route.ts index dd92d4106..2fd163428 100644 --- a/app/api/langchain/tool/agent/route.ts +++ b/app/api/langchain/tool/agent/route.ts @@ -43,6 +43,7 @@ interface RequestBody { apiKey?: string; maxIterations: number; returnIntermediateSteps: boolean; + useTools: (undefined | string)[]; } class ResponseBody { @@ -207,12 +208,16 @@ async function handle(req: NextRequest) { ); const tools = [ - searchTool, - new WebBrowser({ model, embeddings }), // new RequestsGetTool(), // new RequestsPostTool(), - new Calculator(), ]; + const webBrowserTool = new WebBrowser({ model, embeddings }); + const calculatorTool = new Calculator(); + if (reqBody.useTools.includes("web-search")) tools.push(searchTool); + if (reqBody.useTools.includes(webBrowserTool.name)) + tools.push(webBrowserTool); + if (reqBody.useTools.includes(calculatorTool.name)) + tools.push(calculatorTool); const pastMessages = new Array(); diff --git a/app/client/api.ts b/app/client/api.ts index 1bf4a1e6a..677d75f68 100644 --- a/app/client/api.ts +++ b/app/client/api.ts @@ -26,6 +26,7 @@ export interface LLMConfig { export interface LLMAgentConfig { maxIterations: number; returnIntermediateSteps: boolean; + useTools?: (string | undefined)[]; } export interface ChatOptions { diff --git a/app/client/platforms/openai.ts b/app/client/platforms/openai.ts index 9721dcd07..095416645 100644 --- a/app/client/platforms/openai.ts +++ b/app/client/platforms/openai.ts @@ -220,6 +220,7 @@ export class ChatGPTApi implements LLMApi { baseUrl: useAccessStore.getState().openaiUrl, maxIterations: options.agentConfig.maxIterations, returnIntermediateSteps: options.agentConfig.returnIntermediateSteps, + useTools: options.agentConfig.useTools, }; console.log("[Request] openai payload: ", requestPayload); diff --git a/app/components/home.tsx b/app/components/home.tsx index 745298d56..7f948f57a 100644 --- a/app/components/home.tsx +++ b/app/components/home.tsx @@ -55,6 +55,10 @@ const MaskPage = dynamic(async () => (await import("./mask")).MaskPage, { loading: () => , }); +const Plugins = dynamic(async () => (await import("./plugin")).PluginPage, { + loading: () => , +}); + export function useSwitchTheme() { const config = useAppConfig(); @@ -154,6 +158,7 @@ function Screen() { } /> } /> } /> + } /> } /> } /> diff --git a/app/components/plugin.module.scss b/app/components/plugin.module.scss new file mode 100644 index 000000000..6812b4087 --- /dev/null +++ b/app/components/plugin.module.scss @@ -0,0 +1,110 @@ +@import "../styles/animation.scss"; +.plugin-page { + height: 100%; + display: flex; + flex-direction: column; + + .plugin-page-body { + padding: 20px; + overflow-y: auto; + + .plugin-filter { + width: 100%; + max-width: 100%; + margin-bottom: 20px; + animation: slide-in ease 0.3s; + height: 40px; + + display: flex; + + .search-bar { + flex-grow: 1; + max-width: 100%; + min-width: 0; + } + + .plugin-filter-lang { + height: 100%; + margin-left: 10px; + } + + .plugin-create { + height: 100%; + margin-left: 10px; + box-sizing: border-box; + min-width: 80px; + } + } + + .plugin-item { + display: flex; + justify-content: space-between; + padding: 20px; + border: var(--border-in-light); + animation: slide-in ease 0.3s; + + &:not(:last-child) { + border-bottom: 0; + } + + &:first-child { + border-top-left-radius: 10px; + border-top-right-radius: 10px; + } + + &:last-child { + border-bottom-left-radius: 10px; + border-bottom-right-radius: 10px; + } + + .plugin-header { + display: flex; + align-items: center; + + .plugin-icon { + display: flex; + align-items: center; + justify-content: center; + margin-right: 10px; + } + + .plugin-title { + .plugin-name { + font-size: 14px; + font-weight: bold; + } + .plugin-info { + font-size: 12px; + } + } + } + + .plugin-actions { + display: flex; + flex-wrap: nowrap; + transition: all ease 0.3s; + justify-content: center; + align-items: center; + } + + @media screen and (max-width: 600px) { + display: flex; + flex-direction: column; + padding-bottom: 10px; + border-radius: 10px; + margin-bottom: 20px; + box-shadow: var(--card-shadow); + + &:not(:last-child) { + border-bottom: var(--border-in-light); + } + + .plugin-actions { + width: 100%; + justify-content: space-between; + padding-top: 10px; + } + } + } + } +} diff --git a/app/components/plugin.tsx b/app/components/plugin.tsx new file mode 100644 index 000000000..32fb0dbd6 --- /dev/null +++ b/app/components/plugin.tsx @@ -0,0 +1,496 @@ +import { IconButton } from "./button"; +import { ErrorBoundary } from "./error"; + +import styles from "./plugin.module.scss"; + +import DownloadIcon from "../icons/download.svg"; +import UploadIcon from "../icons/upload.svg"; +import EditIcon from "../icons/edit.svg"; +import AddIcon from "../icons/add.svg"; +import CloseIcon from "../icons/close.svg"; +import DeleteIcon from "../icons/delete.svg"; +import EyeIcon from "../icons/eye.svg"; +import CopyIcon from "../icons/copy.svg"; +import LeftIcon from "../icons/left.svg"; + +import { Plugin, usePluginStore } from "../store/plugin"; +import { + ChatMessage, + createMessage, + ModelConfig, + useAppConfig, + useChatStore, +} from "../store"; +import { ROLES } from "../client/api"; +import { + Input, + List, + ListItem, + Modal, + Popover, + Select, + showConfirm, +} from "./ui-lib"; +import { Avatar, AvatarPicker } from "./emoji"; +import Locale, { AllLangs, ALL_LANG_OPTIONS, Lang } from "../locales"; +import { useLocation, useNavigate } from "react-router-dom"; + +import chatStyle from "./chat.module.scss"; +import { useEffect, useState } from "react"; +import { copyToClipboard, downloadAs, readFromFile } from "../utils"; +import { Updater } from "../typing"; +import { ModelConfigList } from "./model-config"; +import { FileName, Path } from "../constant"; +import { BUILTIN_PLUGIN_STORE } from "../plugins"; +import { nanoid } from "nanoid"; +import { getISOLang, getLang } from "../locales"; + +// export function PluginConfig(props: { +// plugin: Plugin; +// updateMask: Updater; +// extraListItems?: JSX.Element; +// readonly?: boolean; +// shouldSyncFromGlobal?: boolean; +// }) { +// const [showPicker, setShowPicker] = useState(false); + +// const updateConfig = (updater: (config: ModelConfig) => void) => { +// if (props.readonly) return; + +// // const config = { ...props.mask.modelConfig }; +// // updater(config); +// props.updateMask((mask) => { +// // mask.modelConfig = config; +// // // if user changed current session mask, it will disable auto sync +// // mask.syncGlobalConfig = false; +// }); +// }; + +// const globalConfig = useAppConfig(); + +// return ( +// <> +// { +// const context = props.mask.context.slice(); +// updater(context); +// props.updateMask((mask) => (mask.context = context)); +// }} +// /> + +// +// +// { +// props.updateMask((mask) => (mask.avatar = emoji)); +// setShowPicker(false); +// }} +// > +// } +// open={showPicker} +// onClose={() => setShowPicker(false)} +// > +//
setShowPicker(true)} +// style={{ cursor: "pointer" }} +// > +//
+//
+//
+// +// +// props.updateMask((mask) => { +// mask.name = e.currentTarget.value; +// }) +// } +// > +// +// +// { +// props.updateMask((mask) => { +// mask.hideContext = e.currentTarget.checked; +// }); +// }} +// > +// + +// {!props.shouldSyncFromGlobal ? ( +// +// } +// text={Locale.Mask.Config.Share.Action} +// onClick={copyMaskLink} +// /> +// +// ) : null} + +// {props.shouldSyncFromGlobal ? ( +// +// { +// const checked = e.currentTarget.checked; +// if ( +// checked && +// (await showConfirm(Locale.Mask.Config.Sync.Confirm)) +// ) { +// props.updateMask((mask) => { +// mask.syncGlobalConfig = checked; +// mask.modelConfig = { ...globalConfig.modelConfig }; +// }); +// } else if (!checked) { +// props.updateMask((mask) => { +// mask.syncGlobalConfig = checked; +// }); +// } +// }} +// > +// +// ) : null} +//
+ +// +// +// {props.extraListItems} +// +// +// ); +// } + +function ContextPromptItem(props: { + prompt: ChatMessage; + update: (prompt: ChatMessage) => void; + remove: () => void; +}) { + const [focusingInput, setFocusingInput] = useState(false); + + return ( +
+ {!focusingInput && ( + + )} + setFocusingInput(true)} + onBlur={() => { + setFocusingInput(false); + // If the selection is not removed when the user loses focus, some + // extensions like "Translate" will always display a floating bar + window?.getSelection()?.removeAllRanges(); + }} + onInput={(e) => + props.update({ + ...props.prompt, + content: e.currentTarget.value as any, + }) + } + /> + {!focusingInput && ( + } + className={chatStyle["context-delete-button"]} + onClick={() => props.remove()} + bordered + /> + )} +
+ ); +} + +export function ContextPrompts(props: { + context: ChatMessage[]; + updateContext: (updater: (context: ChatMessage[]) => void) => void; +}) { + const context = props.context; + + const addContextPrompt = (prompt: ChatMessage) => { + props.updateContext((context) => context.push(prompt)); + }; + + const removeContextPrompt = (i: number) => { + props.updateContext((context) => context.splice(i, 1)); + }; + + const updateContextPrompt = (i: number, prompt: ChatMessage) => { + props.updateContext((context) => (context[i] = prompt)); + }; + + return ( + <> +
+ {context.map((c, i) => ( + updateContextPrompt(i, prompt)} + remove={() => removeContextPrompt(i)} + /> + ))} + +
+ } + text={Locale.Context.Add} + bordered + className={chatStyle["context-prompt-button"]} + onClick={() => + addContextPrompt( + createMessage({ + role: "user", + content: "", + date: "", + }), + ) + } + /> +
+
+ + ); +} + +export function PluginPage() { + const navigate = useNavigate(); + + const pluginStore = usePluginStore(); + const chatStore = useChatStore(); + + const allPlugins = pluginStore + .getAll() + .filter((m) => !getLang() || m.lang === getLang()); + + const [searchPlugins, setSearchPlugins] = useState([]); + const [searchText, setSearchText] = useState(""); + const plugins = searchText.length > 0 ? searchPlugins : allPlugins; + + // simple search, will refactor later + const onSearch = (text: string) => { + setSearchText(text); + if (text.length > 0) { + const result = allPlugins.filter((m) => m.name.includes(text)); + setSearchPlugins(result); + } else { + setSearchPlugins(allPlugins); + } + }; + + const [editingPluginId, setEditingPluginId] = useState(); + const editingPlugin = + pluginStore.get(editingPluginId) ?? + BUILTIN_PLUGIN_STORE.get(editingPluginId); + const closePluginModal = () => setEditingPluginId(undefined); + + const downloadAll = () => { + downloadAs(JSON.stringify(plugins), FileName.Plugins); + }; + + const updatePluginEnableStatus = (id: string, enable: boolean) => { + console.log(enable); + if (enable) pluginStore.enable(id); + else pluginStore.disable(id); + }; + + const importFromFile = () => { + readFromFile().then((content) => { + try { + const importPlugins = JSON.parse(content); + if (Array.isArray(importPlugins)) { + for (const plugin of importPlugins) { + if (plugin.name) { + pluginStore.create(plugin); + } + } + return; + } + if (importPlugins.name) { + pluginStore.create(importPlugins); + } + } catch {} + }); + }; + + return ( + +
+
+ } + text={Locale.NewChat.Return} + onClick={() => navigate(Path.Home)} + > +
+
+
+
+ {Locale.Plugin.Page.Title} +
+
+ {Locale.Plugin.Page.SubTitle(allPlugins.length)} +
+
+ +
+ {/*
+ } + bordered + onClick={downloadAll} + /> +
+
+ } + bordered + onClick={() => importFromFile()} + /> +
*/} +
+
+ +
+
+ onSearch(e.currentTarget.value)} + /> + + {/* } + text={Locale.Mask.Page.Create} + bordered + onClick={() => { + const createdMask = pluginStore.create(); + setEditingMaskId(createdMask.id); + }} + /> */} +
+ +
+ {plugins.map((m) => ( +
+
+
+
{m.name}
+ {/* 描述 */} +
+ {`${m.description}`} +
+
+
+ {/* 操作按钮 */} +
+ { + updatePluginEnableStatus(m.id, e.currentTarget.checked); + }} + > + {/* {m.builtin ? ( + } + text={Locale.Mask.Item.View} + onClick={() => setEditingMaskId(m.id)} + /> + ) : ( + } + text={Locale.Mask.Item.Edit} + onClick={() => setEditingMaskId(m.id)} + /> + )} + {!m.builtin && ( + } + text={Locale.Mask.Item.Delete} + onClick={async () => { + if (await showConfirm(Locale.Mask.Item.DeleteConfirm)) { + maskStore.delete(m.id); + } + }} + /> + )} */} +
+
+ ))} +
+
+
+ + {editingPlugin && ( +
+ } + text={Locale.Plugin.EditModal.Download} + key="export" + bordered + onClick={() => + downloadAs( + JSON.stringify(editingPlugin), + `${editingPlugin.name}.json`, + ) + } + />, + ]} + > + {/* + pluginStore.update(editingPluginId!, updater) + } + readonly={editingPlugin.builtin} + /> */} + +
+ )} +
+ ); +} diff --git a/app/components/sidebar.tsx b/app/components/sidebar.tsx index 634639f1d..b0ffeb8cf 100644 --- a/app/components/sidebar.tsx +++ b/app/components/sidebar.tsx @@ -140,7 +140,7 @@ export function SideBar(props: { className?: string }) { icon={} text={shouldNarrow ? undefined : Locale.Plugin.Name} className={styles["sidebar-bar-button"]} - onClick={() => showToast(Locale.WIP)} + onClick={() => navigate(Path.Plugins, { state: { fromHome: true } })} shadow /> diff --git a/app/constant.ts b/app/constant.ts index ba0b22c7f..8434e2cbd 100644 --- a/app/constant.ts +++ b/app/constant.ts @@ -15,6 +15,7 @@ export enum Path { Settings = "/settings", NewChat = "/new-chat", Masks = "/masks", + Plugins = "/plugins", Auth = "/auth", } @@ -24,6 +25,7 @@ export enum SlotID { export enum FileName { Masks = "masks.json", + Plugins = "plugins.json", Prompts = "prompts.json", } @@ -32,6 +34,7 @@ export enum StoreKey { Access = "access-control", Config = "app-config", Mask = "mask-store", + Plugin = "plugin-store", Prompt = "prompt-store", Update = "chat-update", Sync = "sync", diff --git a/app/locales/cn.ts b/app/locales/cn.ts index 5434b926f..9e9352ac4 100644 --- a/app/locales/cn.ts +++ b/app/locales/cn.ts @@ -305,6 +305,24 @@ const cn = { }, Plugin: { Name: "插件", + Page: { + Title: "预设插件", + SubTitle: (count: number) => `${count} 个预设插件`, + Search: "搜索插件", + Create: "新建", + }, + Item: { + View: "查看", + Edit: "编辑", + Delete: "删除", + DeleteConfirm: "确认删除?", + }, + EditModal: { + Title: (readonly: boolean) => + `编辑预设插件 ${readonly ? "(只读)" : ""}`, + Download: "下载预设", + Clone: "克隆预设", + }, }, FineTuned: { Sysmessage: "你是一个助手", diff --git a/app/locales/en.ts b/app/locales/en.ts index cf8e5981a..15279ef60 100644 --- a/app/locales/en.ts +++ b/app/locales/en.ts @@ -310,6 +310,24 @@ const en: LocaleType = { }, Plugin: { Name: "Plugin", + Page: { + Title: "Plugin Template", + SubTitle: (count: number) => `${count} plugin templates`, + Search: "Search Templates", + Create: "Create", + }, + Item: { + View: "View", + Edit: "Edit", + Delete: "Delete", + DeleteConfirm: "Confirm to delete?", + }, + EditModal: { + Title: (readonly: boolean) => + `Edit Plugin Template ${readonly ? "(readonly)" : ""}`, + Download: "Download", + Clone: "Clone", + }, }, FineTuned: { Sysmessage: "You are an assistant that", diff --git a/app/plugins/cn.ts b/app/plugins/cn.ts new file mode 100644 index 000000000..25b0cfe07 --- /dev/null +++ b/app/plugins/cn.ts @@ -0,0 +1,32 @@ +import { BuiltinPlugin } from "./typing"; + +export const CN_PLUGINS: BuiltinPlugin[] = [ + { + name: "搜索引擎", + toolName: "web-search", + lang: "cn", + description: "搜索引擎的网络搜索功能工具。", + builtin: true, + createdAt: 1693744292000, + enable: true, + }, + { + name: "计算器", + toolName: "calculator", + lang: "cn", + description: "计算器是一个用于计算数学表达式的工具。", + builtin: true, + createdAt: 1693744292000, + enable: true, + }, + { + name: "网页浏览器", + toolName: "web-browser", + lang: "cn", + description: + "一个用于与网页进行交互的工具,可以从网页中提取信息或总结其内容。", + builtin: true, + createdAt: 1693744292000, + enable: true, + }, +]; diff --git a/app/plugins/en.ts b/app/plugins/en.ts new file mode 100644 index 000000000..6cd8f5f20 --- /dev/null +++ b/app/plugins/en.ts @@ -0,0 +1,33 @@ +import { BuiltinPlugin } from "./typing"; + +export const EN_PLUGINS: BuiltinPlugin[] = [ + { + name: "WebSearch", + toolName: "web-search", + lang: "en", + description: "Web search function tool for search engines.", + builtin: true, + createdAt: 1693744292000, + enable: true, + }, + { + name: "Calculator", + toolName: "calculator", + lang: "en", + description: + "The Calculator class is a tool used to evaluate mathematical expressions. It extends the base Tool class.", + builtin: true, + createdAt: 1693744292000, + enable: true, + }, + { + name: "WebBrowser", + toolName: "web-browser", + lang: "en", + description: + "A class designed to interact with web pages, either to extract information from them or to summarize their content.", + builtin: true, + createdAt: 1693744292000, + enable: true, + }, +]; diff --git a/app/plugins/index.ts b/app/plugins/index.ts new file mode 100644 index 000000000..5331eb11c --- /dev/null +++ b/app/plugins/index.ts @@ -0,0 +1,27 @@ +import { Plugin } from "../store/plugin"; +import { CN_PLUGINS } from "./cn"; +import { EN_PLUGINS } from "./en"; + +import { type BuiltinPlugin } from "./typing"; +export { type BuiltinPlugin } from "./typing"; + +export const BUILTIN_PLUGIN_ID = 100000; + +export const BUILTIN_PLUGIN_STORE = { + buildinId: BUILTIN_PLUGIN_ID, + plugins: {} as Record, + get(id?: string) { + if (!id) return undefined; + return this.plugins[id] as Plugin | undefined; + }, + add(m: BuiltinPlugin) { + const plugin = { ...m, id: this.buildinId++, builtin: true }; + this.plugins[plugin.id] = plugin; + return plugin; + }, +}; + +export const BUILTIN_PLUGINS: BuiltinPlugin[] = [ + ...CN_PLUGINS, + ...EN_PLUGINS, +].map((m) => BUILTIN_PLUGIN_STORE.add(m)); diff --git a/app/plugins/typing.ts b/app/plugins/typing.ts new file mode 100644 index 000000000..941dd5b84 --- /dev/null +++ b/app/plugins/typing.ts @@ -0,0 +1,5 @@ +import { type Plugin } from "../store/plugin"; + +export type BuiltinPlugin = Omit & { + builtin: Boolean; +}; diff --git a/app/store/chat.ts b/app/store/chat.ts index 021fb0fbe..25658fb51 100644 --- a/app/store/chat.ts +++ b/app/store/chat.ts @@ -18,6 +18,7 @@ import { ChatControllerPool } from "../client/controller"; import { prettyObject } from "../utils/format"; import { estimateTokenLength } from "../utils/token"; import { nanoid } from "nanoid"; +import { Plugin, usePluginStore } from "../store/plugin"; export interface ChatToolMessage { toolName: string; @@ -313,6 +314,10 @@ export const useChatStore = create()( const config = useAppConfig.getState(); const pluginConfig = useAppConfig.getState().pluginConfig; + const pluginStore = usePluginStore.getState(); + const allPlugins = pluginStore + .getAll() + .filter((m) => (!getLang() || m.lang === getLang()) && m.enable); // save user's and bot's message get().updateCurrentSession((session) => { @@ -324,12 +329,17 @@ export const useChatStore = create()( session.messages.push(botMessage); }); - if (config.pluginConfig.enable && session.mask.usePlugins) { + if ( + config.pluginConfig.enable && + session.mask.usePlugins && + allPlugins.length > 0 + ) { console.log("[ToolAgent] start"); + const pluginToolNames = allPlugins.map((m) => m.toolName); api.llm.toolAgentChat({ messages: sendMessages, config: { ...modelConfig, stream: true }, - agentConfig: { ...pluginConfig }, + agentConfig: { ...pluginConfig, useTools: pluginToolNames }, onUpdate(message) { botMessage.streaming = true; if (message) { diff --git a/app/store/plugin.ts b/app/store/plugin.ts new file mode 100644 index 000000000..877942f23 --- /dev/null +++ b/app/store/plugin.ts @@ -0,0 +1,137 @@ +import { create } from "zustand"; +import { persist } from "zustand/middleware"; +import { BUILTIN_PLUGIN_STORE, BUILTIN_PLUGINS } from "../plugins"; +import { getLang, Lang } from "../locales"; +import { DEFAULT_TOPIC, ChatMessage } from "./chat"; +import { ModelConfig, useAppConfig } from "./config"; +import { StoreKey } from "../constant"; +import { nanoid } from "nanoid"; + +export type Plugin = { + id: string; + createdAt: number; + name: string; + toolName?: string; + lang: string; + description: string; + builtin: boolean; + enable: boolean; +}; + +export const DEFAULT_PLUGIN_STATE = { + plugins: {} as Record, + pluginStatuses: {} as Record, +}; + +export type PluginState = typeof DEFAULT_PLUGIN_STATE; +type PluginStore = PluginState & { + create: (plugin?: Partial) => Plugin; + update: (id: string, updater: (plugin: Plugin) => void) => void; + enable: (id: string) => void; + disable: (id: string) => void; + delete: (id: string) => void; + search: (text: string) => Plugin[]; + get: (id?: string) => Plugin | null; + getAll: () => Plugin[]; +}; + +export const createEmptyPlugin = () => + ({ + id: nanoid(), + name: DEFAULT_TOPIC, + lang: getLang(), + builtin: false, + createdAt: Date.now(), + enable: true, + }) as Plugin; + +export const usePluginStore = create()( + persist( + (set, get) => ({ + ...DEFAULT_PLUGIN_STATE, + + create(plugin) { + const plugins = get().plugins; + const id = nanoid(); + plugins[id] = { + ...createEmptyPlugin(), + ...plugin, + id, + builtin: false, + }; + + set(() => ({ plugins })); + + return plugins[id]; + }, + update(id, updater) { + const plugins = get().plugins; + const plugin = plugins[id]; + if (!plugin) return; + const updatePlugin = { ...plugin }; + updater(updatePlugin); + plugins[id] = updatePlugin; + set(() => ({ plugins })); + }, + delete(id) { + const plugins = get().plugins; + delete plugins[id]; + set(() => ({ plugins })); + }, + enable(id) { + const pluginStatuses = get().pluginStatuses; + pluginStatuses[id] = true; + set(() => ({ pluginStatuses })); + }, + disable(id) { + const pluginStatuses = get().pluginStatuses; + pluginStatuses[id] = false; + set(() => ({ pluginStatuses })); + }, + get(id) { + return get().plugins[id ?? 1145141919810]; + }, + getAll() { + const userPlugins = Object.values(get().plugins).sort( + (a, b) => b.createdAt - a.createdAt, + ); + const buildinPlugins = BUILTIN_PLUGINS.map( + (m) => + ({ + ...m, + }) as Plugin, + ); + const pluginStatuses = get().pluginStatuses; + return userPlugins.concat(buildinPlugins).map((e) => { + e.enable = pluginStatuses[e.id] ?? true; + return e; + }); + }, + search(text) { + return Object.values(get().plugins); + }, + }), + { + name: StoreKey.Plugin, + version: 3.1, + + migrate(state, version) { + const newState = JSON.parse(JSON.stringify(state)) as PluginState; + + if (version < 3) { + Object.values(newState.plugins).forEach((m) => (m.id = nanoid())); + } + + if (version < 3.1) { + const updatedPlugins: Record = {}; + Object.values(newState.plugins).forEach((m) => { + updatedPlugins[m.id] = m; + }); + newState.plugins = updatedPlugins; + } + + return newState as any; + }, + }, + ), +);