From eaf26bd3450dacfb1a25fc24be7db07ab50c4dc9 Mon Sep 17 00:00:00 2001 From: arvinxx Date: Wed, 16 Aug 2023 23:00:54 +0800 Subject: [PATCH] =?UTF-8?q?:sparkles:=20feat:=20=E5=85=88=E4=BD=BF?= =?UTF-8?q?=E7=94=A8=E6=9C=80=E7=AE=80=E5=8D=95=E7=9A=84=E6=96=B9=E5=BC=8F?= =?UTF-8?q?=E8=BF=81=E7=A7=BB=E6=8F=92=E4=BB=B6=E6=9C=8D=E5=8A=A1=E7=AB=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- api/v1/runner.ts | 3 +- package.json | 3 ++ plugins/index.ts | 10 +++++ plugins/searchEngine/index.ts | 27 +++++++++++ plugins/searchEngine/runner.ts | 40 +++++++++++++++++ plugins/searchEngine/type.ts | 82 ++++++++++++++++++++++++++++++++++ plugins/weather/index.ts | 27 +++++++++++ plugins/weather/runner.ts | 34 ++++++++++++++ plugins/weather/type.ts | 36 +++++++++++++++ plugins/webCrawler/index.ts | 22 +++++++++ plugins/webCrawler/runner.ts | 45 +++++++++++++++++++ plugins/webCrawler/type.ts | 35 +++++++++++++++ public/index.html | 2 +- tsconfig.json | 1 - types/pluginItem.ts | 36 +++++++++++++++ types/schema.ts | 22 +++++++++ 16 files changed, 422 insertions(+), 3 deletions(-) create mode 100644 plugins/index.ts create mode 100644 plugins/searchEngine/index.ts create mode 100644 plugins/searchEngine/runner.ts create mode 100644 plugins/searchEngine/type.ts create mode 100644 plugins/weather/index.ts create mode 100644 plugins/weather/runner.ts create mode 100644 plugins/weather/type.ts create mode 100644 plugins/webCrawler/index.ts create mode 100644 plugins/webCrawler/runner.ts create mode 100644 plugins/webCrawler/type.ts create mode 100644 types/pluginItem.ts create mode 100644 types/schema.ts diff --git a/api/v1/runner.ts b/api/v1/runner.ts index 8939e57..00ffe7b 100644 --- a/api/v1/runner.ts +++ b/api/v1/runner.ts @@ -1,3 +1,4 @@ +import { PluginsMap } from '../../plugins'; import { OpenAIPluginPayload } from '../../types/plugins'; export const runtime = 'edge'; @@ -9,7 +10,7 @@ export default async (req: Request) => { console.log(`检测到 functionCall: ${name}`); - const func = { runner: (params: any) => params }; + const func = PluginsMap[name]; if (func) { const data = JSON.parse(args); diff --git a/package.json b/package.json index 46577b7..c4bdeaf 100644 --- a/package.json +++ b/package.json @@ -30,6 +30,9 @@ "last 2 versions", "not ie <= 10" ], + "dependencies": { + "query-string": "^8" + }, "devDependencies": { "@lobehub/lint": "latest", "@vercel/node": "^2", diff --git a/plugins/index.ts b/plugins/index.ts new file mode 100644 index 0000000..f4ffe6c --- /dev/null +++ b/plugins/index.ts @@ -0,0 +1,10 @@ +import { PluginItem } from '../types/pluginItem'; +import searchEngine from './searchEngine'; +import getWeather from './weather'; +import webCrawler from './webCrawler'; + +export const PluginsMap: Record = { + [getWeather.name]: getWeather, + [searchEngine.name]: searchEngine, + [webCrawler.name]: webCrawler, +}; diff --git a/plugins/searchEngine/index.ts b/plugins/searchEngine/index.ts new file mode 100644 index 0000000..23ff8aa --- /dev/null +++ b/plugins/searchEngine/index.ts @@ -0,0 +1,27 @@ +import { PluginItem } from '../../types/pluginItem'; +import runner from './runner'; +import { Result } from './type'; + +const schema: PluginItem['schema'] = { + description: '查询搜索引擎获取信息', + name: 'searchEngine', + parameters: { + properties: { + keywords: { + description: '关键词', + type: 'string', + }, + }, + required: ['keywords'], + type: 'object', + }, +}; + +const searchEngine: PluginItem = { + avatar: '🔍', + name: 'searchEngine', + runner, + schema, +}; + +export default searchEngine; diff --git a/plugins/searchEngine/runner.ts b/plugins/searchEngine/runner.ts new file mode 100644 index 0000000..fce1de6 --- /dev/null +++ b/plugins/searchEngine/runner.ts @@ -0,0 +1,40 @@ +import querystring from 'query-string'; + +import { PluginRunner } from '../../types/pluginItem'; +import { OrganicResults, Result } from './type'; + +const BASE_URL = 'https://serpapi.com/search'; + +const API_KEY = process.env.SERPAI_API_KEY; + +const fetchResult: PluginRunner<{ keywords: string }, Result> = async ({ keywords }) => { + const params = { + api_key: API_KEY, + engine: 'google', + gl: 'cn', + google_domain: 'google.com', + hl: 'zh-cn', + location: 'China', + q: keywords, + }; + + const query = querystring.stringify(params); + + const res = await fetch(`${BASE_URL}?${query}`); + + const data = await res.json(); + + const results = data.organic_results as OrganicResults; + + return results.map((r) => ({ + content: r.snippet, + date: r.date, + displayed_link: r.displayed_link, + favicon: r.favicon, + link: r.link, + source: r.source, + title: r.title, + })); +}; + +export default fetchResult; diff --git a/plugins/searchEngine/type.ts b/plugins/searchEngine/type.ts new file mode 100644 index 0000000..0e6555c --- /dev/null +++ b/plugins/searchEngine/type.ts @@ -0,0 +1,82 @@ +export type OrganicResults = OrganicResult[]; + +export interface SearchItem { + content: string; + date?: string; + displayed_link?: string; + favicon?: string; + link: string; + source?: string; + title: string; +} +export type Result = SearchItem[]; + +interface OrganicResult { + about_page_link: string; + about_page_serpapi_link: string; + about_this_result: AboutThisResult; + cached_page_link?: string; + date?: string; + displayed_link: string; + favicon?: string; + link: string; + position: number; + related_results?: RelatedResult[]; + rich_snippet?: RichSnippet; + snippet: string; + snippet_highlighted_words?: string[]; + source: string; + thumbnail?: string; + title: string; +} + +interface AboutThisResult { + languages: string[]; + regions: string[]; + source: Source; +} + +interface Source { + description: string; + icon: string; + security?: string; + source_info_link?: string; +} + +interface RelatedResult { + about_page_link: string; + about_page_serpapi_link: string; + about_this_result: AboutThisResult2; + cached_page_link: string; + date: string; + displayed_link: string; + link: string; + position: number; + snippet: string; + snippet_highlighted_words: string[]; + title: string; +} + +interface AboutThisResult2 { + languages: string[]; + regions: string[]; + source: Source2; +} + +interface Source2 { + description: string; + icon: string; +} + +interface RichSnippet { + top: Top; +} + +interface Top { + detected_extensions: DetectedExtensions; + extensions: string[]; +} + +interface DetectedExtensions { + month_ago: number; +} diff --git a/plugins/weather/index.ts b/plugins/weather/index.ts new file mode 100644 index 0000000..9f5b25d --- /dev/null +++ b/plugins/weather/index.ts @@ -0,0 +1,27 @@ +import { PluginItem } from '../../types/pluginItem'; +import runner from './runner'; +import { WeatherResult } from './type'; + +const schema: PluginItem['schema'] = { + description: '获取当前天气情况', + name: 'realtimeWeather', + parameters: { + properties: { + city: { + description: '城市名称', + type: 'string', + }, + }, + required: ['city'], + type: 'object', + }, +}; + +const getWeather: PluginItem = { + avatar: '☂️', + name: 'realtimeWeather', + runner, + schema, +}; + +export default getWeather; diff --git a/plugins/weather/runner.ts b/plugins/weather/runner.ts new file mode 100644 index 0000000..a0736e4 --- /dev/null +++ b/plugins/weather/runner.ts @@ -0,0 +1,34 @@ +import { PluginRunner } from '../../types/pluginItem'; +import { Response, WeatherParams, WeatherResult } from './type'; + +const weatherBaseURL = 'https://restapi.amap.com/v3/weather/weatherInfo'; + +const citySearchURL = 'https://restapi.amap.com/v3/config/district'; + +const KEY = process.env.GAODE_WEATHER_KEY; + +const fetchCityCode = async (keywords: string): Promise => { + const URL = `${citySearchURL}?keywords=${keywords}&subdistrict=0&extensions=base&key=${KEY}`; + const res = await fetch(URL); + + const data = await res.json(); + console.log(data); + + return data.districts[0].adcode; +}; + +const fetchWeather: PluginRunner = async ({ + city, + extensions = 'all', +}) => { + const cityCode = await fetchCityCode(city); + + const URL = `${weatherBaseURL}?city=${cityCode}&extensions=${extensions}&key=${KEY}`; + const res = await fetch(URL); + + const data: Response = await res.json(); + + return data.forecasts; +}; + +export default fetchWeather; diff --git a/plugins/weather/type.ts b/plugins/weather/type.ts new file mode 100644 index 0000000..2384bdd --- /dev/null +++ b/plugins/weather/type.ts @@ -0,0 +1,36 @@ +export interface WeatherParams { + city: string; + extensions?: 'base' | 'all'; +} +export type WeatherResult = Forecast[]; + +export interface Response { + count: string; + forecasts: Forecast[]; + info: string; + infocode: string; + status: string; +} + +export interface Forecast { + adcode: string; + casts: Cast[]; + city: string; + province: string; + reporttime: string; +} + +export interface Cast { + date: string; + daypower: string; + daytemp: string; + daytemp_float: string; + dayweather: string; + daywind: string; + nightpower: string; + nighttemp: string; + nighttemp_float: string; + nightweather: string; + nightwind: string; + week: string; +} diff --git a/plugins/webCrawler/index.ts b/plugins/webCrawler/index.ts new file mode 100644 index 0000000..55e6e6a --- /dev/null +++ b/plugins/webCrawler/index.ts @@ -0,0 +1,22 @@ +import { PluginItem } from '../../types/pluginItem'; +import runner from './runner'; +import { Result } from './type'; + +const schema = { + description: '提取网页内容并总结', + name: 'websiteCrawler', + parameters: { + properties: { + url: { + description: '网页内容', + type: 'string', + }, + }, + required: ['url'], + type: 'object', + }, +}; + +const getWeather: PluginItem = { avatar: '🕸', name: 'websiteCrawler', runner, schema }; + +export default getWeather; diff --git a/plugins/webCrawler/runner.ts b/plugins/webCrawler/runner.ts new file mode 100644 index 0000000..084458c --- /dev/null +++ b/plugins/webCrawler/runner.ts @@ -0,0 +1,45 @@ +import { PluginRunner } from '../../types/pluginItem'; +import { ParserResponse, Result } from './type'; + +const BASE_URL = process.env.BROWSERLESS_URL ?? 'https://chrome.browserless.io'; +const BROWSERLESS_TOKEN = process.env.BROWSERLESS_TOKEN; + +// service from: https://github.com/lobehub/html-parser/tree/master +const HTML_PARSER_URL = process.env.HTML_PARSER_URL; + +const runner: PluginRunner<{ url: string }, Result> = async ({ url }) => { + const input = { + gotoOptions: { waitUntil: 'networkidle2' }, + url, + }; + + try { + const res = await fetch(`${BASE_URL}/content?token=${BROWSERLESS_TOKEN}`, { + body: JSON.stringify(input), + headers: { + 'Content-Type': 'application/json', + }, + method: 'POST', + }); + const html = await res.text(); + + const parserBody = { html, url }; + + const parseRes = await fetch(`${HTML_PARSER_URL}`, { + body: JSON.stringify(parserBody), + headers: { + 'Content-Type': 'application/json', + }, + method: 'POST', + }); + + const { title, textContent, siteName } = (await parseRes.json()) as ParserResponse; + + return { content: textContent, title, url, website: siteName }; + } catch (error) { + console.error(error); + return { content: '抓取失败', errorMessage: (error as any).message, url }; + } +}; + +export default runner; diff --git a/plugins/webCrawler/type.ts b/plugins/webCrawler/type.ts new file mode 100644 index 0000000..946fb61 --- /dev/null +++ b/plugins/webCrawler/type.ts @@ -0,0 +1,35 @@ +export type Result = { + content: string; + title?: string; + url: string; + website?: string; +}; + +export interface ParserResponse { + /** author metadata */ + byline: string; + + /** HTML string of processed article content */ + content: string; + + /** content direction */ + dir: string; + + /** article description, or short excerpt from the content */ + excerpt: string; + + /** content language */ + lang: string; + + /** length of an article, in characters */ + length: number; + + /** name of the site */ + siteName: string; + + /** text content of the article, with all the HTML tags removed */ + textContent: string; + + /** article title */ + title: string; +} diff --git a/public/index.html b/public/index.html index 75bf185..0588f10 100644 --- a/public/index.html +++ b/public/index.html @@ -2,7 +2,7 @@ - Vercel Serveless Api Template + Lobe Chat Plugin Market diff --git a/tsconfig.json b/tsconfig.json index f84f3fc..ca3c20f 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -9,7 +9,6 @@ "moduleResolution": "node", "forceConsistentCasingInFileNames": true, "noImplicitReturns": true, - "suppressImplicitAnyIndexErrors": true, "noUnusedLocals": true, "skipLibCheck": true, "strict": true diff --git a/types/pluginItem.ts b/types/pluginItem.ts new file mode 100644 index 0000000..259b1a2 --- /dev/null +++ b/types/pluginItem.ts @@ -0,0 +1,36 @@ +import { ChatCompletionFunctions } from './schema'; + +/** + * 插件项 + * @template Result - 结果类型,默认为 any + * @template RunnerParams - 运行参数类型,默认为 any + */ +export interface PluginItem { + /** + * 头像 + */ + avatar: string; + /** + * 名称 + */ + name: string; + /** + * 运行器 + * @param params - 运行参数 + * @returns 运行结果的 Promise + */ + runner: PluginRunner; + /** + * 聊天完成函数的模式 + */ + schema: ChatCompletionFunctions; +} + +/** + * 插件运行器 + * @template Params - 参数类型,默认为 object + * @template Result - 结果类型,默认为 any + * @param params - 运行参数 + * @returns 运行结果的 Promise + */ +export type PluginRunner = (params: Params) => Promise; diff --git a/types/schema.ts b/types/schema.ts new file mode 100644 index 0000000..031b7a2 --- /dev/null +++ b/types/schema.ts @@ -0,0 +1,22 @@ +export interface ChatCompletionFunctions { + /** + * The description of what the function does. + * @type {string} + * @memberof ChatCompletionFunctions + */ + description?: string; + /** + * The name of the function to be called. Must be a-z, A-Z, 0-9, or contain underscores and dashes, with a maximum length of 64. + * @type {string} + * @memberof ChatCompletionFunctions + */ + name: string; + /** + * The parameters the functions accepts, described as a JSON Schema object. See the [guide](/docs/guides/gpt/function-calling) for examples, and the [JSON Schema reference](https://json-schema.org/understanding-json-schema/) for documentation about the format. + * @type {{ [key: string]: any }} + * @memberof ChatCompletionFunctions + */ + parameters?: { + [key: string]: any; + }; +}