import OpenAPIClientAxios from "openapi-client-axios"; import { StoreKey } from "../constant"; import { nanoid } from "nanoid"; import { createPersistStore } from "../utils/store"; import { getClientConfig } from "../config/client"; import yaml from "js-yaml"; import { adapter, getOperationId } from "../utils"; import { useAccessStore } from "./access"; const isApp = getClientConfig()?.isApp !== false; export type Plugin = { id: string; createdAt: number; title: string; version: string; content: string; builtin: boolean; authType?: string; authLocation?: string; authHeader?: string; authToken?: string; }; export type FunctionToolItem = { type: string; function: { name: string; description?: string; parameters: Object; }; }; type FunctionToolServiceItem = { api: OpenAPIClientAxios; length: number; tools: FunctionToolItem[]; funcs: Record<string, Function>; }; export const FunctionToolService = { tools: {} as Record<string, FunctionToolServiceItem>, add(plugin: Plugin, replace = false) { if (!replace && this.tools[plugin.id]) return this.tools[plugin.id]; const headerName = ( plugin?.authType == "custom" ? plugin?.authHeader : "Authorization" ) as string; const tokenValue = plugin?.authType == "basic" ? `Basic ${plugin?.authToken}` : plugin?.authType == "bearer" ? `Bearer ${plugin?.authToken}` : plugin?.authToken; const authLocation = plugin?.authLocation || "header"; const definition = yaml.load(plugin.content) as any; const serverURL = definition?.servers?.[0]?.url; const baseURL = !isApp ? "/api/proxy" : serverURL; const headers: Record<string, string | undefined> = { "X-Base-URL": !isApp ? serverURL : undefined, }; if (authLocation == "header") { headers[headerName] = tokenValue; } // try using openaiApiKey for Dalle3 Plugin. if (!tokenValue && plugin.id === "dalle3") { const openaiApiKey = useAccessStore.getState().openaiApiKey; if (openaiApiKey) { headers[headerName] = `Bearer ${openaiApiKey}`; } } const api = new OpenAPIClientAxios({ definition: yaml.load(plugin.content) as any, axiosConfigDefaults: { adapter: (window.__TAURI__ ? adapter : ["xhr"]) as any, baseURL, headers, }, }); try { api.initSync(); } catch (e) {} const operations = api.getOperations(); return (this.tools[plugin.id] = { api, length: operations.length, tools: operations.map((o) => { // @ts-ignore const parameters = o?.requestBody?.content["application/json"] ?.schema || { type: "object", properties: {}, }; if (!parameters["required"]) { parameters["required"] = []; } if (o.parameters instanceof Array) { o.parameters.forEach((p) => { // @ts-ignore if (p?.in == "query" || p?.in == "path") { // const name = `${p.in}__${p.name}` // @ts-ignore const name = p?.name; parameters["properties"][name] = { // @ts-ignore type: p.schema.type, // @ts-ignore description: p.description, }; // @ts-ignore if (p.required) { parameters["required"].push(name); } } }); } return { type: "function", function: { name: getOperationId(o), description: o.description || o.summary, parameters: parameters, }, } as FunctionToolItem; }), funcs: operations.reduce((s, o) => { // @ts-ignore s[getOperationId(o)] = function (args) { const parameters: Record<string, any> = {}; if (o.parameters instanceof Array) { o.parameters.forEach((p) => { // @ts-ignore parameters[p?.name] = args[p?.name]; // @ts-ignore delete args[p?.name]; }); } if (authLocation == "query") { parameters[headerName] = tokenValue; } else if (authLocation == "body") { args[headerName] = tokenValue; } // @ts-ignore if o.operationId is null, then using o.path and o.method return api.client.paths[o.path][o.method]( parameters, args, api.axiosConfigDefaults, ); }; return s; }, {}), }); }, get(id: string) { return this.tools[id]; }, }; export const createEmptyPlugin = () => ({ id: nanoid(), title: "", version: "1.0.0", content: "", builtin: false, createdAt: Date.now(), }) as Plugin; export const DEFAULT_PLUGIN_STATE = { plugins: {} as Record<string, Plugin>, }; export const usePluginStore = createPersistStore( { ...DEFAULT_PLUGIN_STATE }, (set, get) => ({ create(plugin?: Partial<Plugin>) { const plugins = get().plugins; const id = plugin?.id || nanoid(); plugins[id] = { ...createEmptyPlugin(), ...plugin, id, builtin: false, }; set(() => ({ plugins })); get().markUpdate(); return plugins[id]; }, updatePlugin(id: string, updater: (plugin: Plugin) => void) { const plugins = get().plugins; const plugin = plugins[id]; if (!plugin) return; const updatePlugin = { ...plugin }; updater(updatePlugin); plugins[id] = updatePlugin; FunctionToolService.add(updatePlugin, true); set(() => ({ plugins })); get().markUpdate(); }, delete(id: string) { const plugins = get().plugins; delete plugins[id]; set(() => ({ plugins })); get().markUpdate(); }, getAsTools(ids: string[]) { const plugins = get().plugins; const selected = (ids || []) .map((id) => plugins[id]) .filter((i) => i) .map((p) => FunctionToolService.add(p)); return [ // @ts-ignore selected.reduce((s, i) => s.concat(i.tools), []), selected.reduce((s, i) => Object.assign(s, i.funcs), {}), ]; }, get(id?: string) { return get().plugins[id ?? 1145141919810]; }, getAll() { return Object.values(get().plugins).sort( (a, b) => b.createdAt - a.createdAt, ); }, }), { name: StoreKey.Plugin, version: 1, onRehydrateStorage(state) { // Skip store rehydration on server side if (typeof window === "undefined") { return; } fetch("./plugins.json") .then((res) => res.json()) .then((res) => { Promise.all( res.map((item: any) => // skip get schema state.get(item.id) ? item : fetch(item.schema) .then((res) => res.text()) .then((content) => ({ ...item, content, })) .catch((e) => item), ), ).then((builtinPlugins: any) => { builtinPlugins .filter((item: any) => item?.content) .forEach((item: any) => { const plugin = state.create(item); state.updatePlugin(plugin.id, (plugin) => { const tool = FunctionToolService.add(plugin, true); plugin.title = tool.api.definition.info.title; plugin.version = tool.api.definition.info.version; plugin.builtin = true; }); }); }); }); }, }, );