You cannot select more than 25 topics
			Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
		
		
		
		
		
			
		
			
				
	
	
		
			272 lines
		
	
	
		
			7.7 KiB
		
	
	
	
		
			TypeScript
		
	
			
		
		
	
	
			272 lines
		
	
	
		
			7.7 KiB
		
	
	
	
		
			TypeScript
		
	
| 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;
 | |
|                 });
 | |
|               });
 | |
|           });
 | |
|         });
 | |
|     },
 | |
|   },
 | |
| );
 |