ChatGPT-Next-Web/app/store/plugin.ts

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;
});
});
});
});
},
},
);