275 lines
7.7 KiB
TypeScript
275 lines
7.7 KiB
TypeScript
import yaml from 'js-yaml';
|
|
import { nanoid } from 'nanoid';
|
|
import OpenAPIClientAxios from 'openapi-client-axios';
|
|
import { getClientConfig } from '../config/client';
|
|
import { StoreKey } from '../constant';
|
|
import { adapter, getOperationId } from '../utils';
|
|
import { createPersistStore } from '../utils/store';
|
|
import { useAccessStore } from './access';
|
|
|
|
const isApp = getClientConfig()?.isApp !== false;
|
|
|
|
export interface Plugin {
|
|
id: string;
|
|
createdAt: number;
|
|
title: string;
|
|
version: string;
|
|
content: string;
|
|
builtin: boolean;
|
|
authType?: string;
|
|
authLocation?: string;
|
|
authHeader?: string;
|
|
authToken?: string;
|
|
}
|
|
|
|
export interface FunctionToolItem {
|
|
type: string;
|
|
function: {
|
|
name: string;
|
|
description?: string;
|
|
parameters: object;
|
|
};
|
|
}
|
|
|
|
interface 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 (Array.isArray(o.parameters)) {
|
|
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,
|
|
},
|
|
} as FunctionToolItem;
|
|
}),
|
|
funcs: operations.reduce((s, o) => {
|
|
// @ts-ignore
|
|
s[getOperationId(o)] = function (args) {
|
|
const parameters: Record<string, any> = {};
|
|
if (Array.isArray(o.parameters)) {
|
|
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 function createEmptyPlugin() {
|
|
return ({
|
|
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;
|
|
});
|
|
});
|
|
});
|
|
});
|
|
},
|
|
},
|
|
);
|