From 450700c211909bcd372e56a60a5cedf20e500506 Mon Sep 17 00:00:00 2001 From: cool Date: Tue, 28 May 2024 15:22:15 +0800 Subject: [PATCH] =?UTF-8?q?=E6=96=B0=E5=A2=9E=E6=8F=92=E4=BB=B6=E8=B0=83?= =?UTF-8?q?=E7=94=A8=E7=B1=BB=E5=9E=8B=E6=8F=90=E7=A4=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 1 + src/modules/demo/controller/open/plugin.ts | 64 ++---- src/modules/demo/controller/open/sse.ts | 2 +- src/modules/plugin/entity/info.ts | 6 + src/modules/plugin/event/app.ts | 5 + src/modules/plugin/hooks/upload/index.ts | 4 +- src/modules/plugin/service/info.ts | 42 +++- src/modules/plugin/service/types.ts | 231 +++++++++++++++++++++ tsconfig.json | 11 +- typings/plugin.d.ts | 12 ++ typings/uniphone.d.ts | 14 ++ typings/upload.d.ts | 56 +++++ typings/wx.d.ts | 58 ++++++ 13 files changed, 442 insertions(+), 64 deletions(-) create mode 100644 src/modules/plugin/service/types.ts create mode 100644 typings/plugin.d.ts create mode 100644 typings/uniphone.d.ts create mode 100644 typings/upload.d.ts create mode 100644 typings/wx.d.ts diff --git a/package.json b/package.json index fcaabcc..2272c64 100644 --- a/package.json +++ b/package.json @@ -47,6 +47,7 @@ "jest": "^29.7.0", "mwts": "^1.3.0", "mwtsc": "^1.10.0", + "prettier": "^3.2.5", "ts-jest": "^29.1.2", "typescript": "~5.4.5" }, diff --git a/src/modules/demo/controller/open/plugin.ts b/src/modules/demo/controller/open/plugin.ts index 362af90..c2d3310 100644 --- a/src/modules/demo/controller/open/plugin.ts +++ b/src/modules/demo/controller/open/plugin.ts @@ -12,58 +12,18 @@ export class OpenDemoPluginController extends BaseController { @Get('/invoke', { summary: '调用插件' }) async invoke() { - const plugin = await this.pluginService.getInstance('pay-ali'); - - // 获得插件配置 - const config = await plugin['getConfig'](); - - // 生成订单号 - const orderNum = plugin['createOrderNum'](); - - // 获得支付SDK实例 - const instance = await plugin.getInstance(); - - // 调用支付接口 - const result = instance.pageExec('alipay.trade.page.pay', { - notify_url: config.notifyUrl, - bizContent: { - out_trade_no: orderNum, - total_amount: '0.01', - subject: '测试', - product_code: 'FAST_INSTANT_TRADE_PAY', - body: '测试订单', - qr_pay_mode: '2', - }, - }); - - console.log(result); - - // 获得插件实例 - // const plugin = await this.pluginService.getInstance('pay-wx'); - - // // 获得插件配置 - // const config = await plugin['getConfig'](); - - // // 生成订单号 - // const orderNum = plugin['createOrderNum'](); - - // // 获得微信支付 SDK 实例 - // const instance = await plugin['getInstance'](); - - // // Native,返回的信息可以直接生成二维码,用户扫码支付 - // const params = { - // description: '测试', - // out_trade_no: orderNum, - // notify_url: config.notify_url, - // amount: { - // total: 1, - // }, - // scene_info: { - // payer_client_ip: 'ip', - // }, - // }; - // const result = await instance.transactions_native(params); - // console.log(result); + // 获取插件实例 + const instance: any = await this.pluginService.getInstance('ollama'); + // 调用chat + const messages = [ + { role: 'system', content: '你叫小酷,是一个智能助理' }, + { role: 'user', content: '写一个1000字的关于春天的文章' }, + ]; + for (let i = 0; i < 3; i++) { + instance.chat(messages, { stream: true }, res => { + console.log(i, res.content); + }); + } return this.ok(); } } diff --git a/src/modules/demo/controller/open/sse.ts b/src/modules/demo/controller/open/sse.ts index 7b32a4b..9733dd2 100644 --- a/src/modules/demo/controller/open/sse.ts +++ b/src/modules/demo/controller/open/sse.ts @@ -30,7 +30,7 @@ export class OpenDemoSSEController extends BaseController { }; // 获取插件实例 - const instance = await this.pluginService.getInstance('ollama'); + const instance: any = await this.pluginService.getInstance('ollama'); // 调用chat const messages = [ { role: 'system', content: '你叫小酷,是个编程助手' }, diff --git a/src/modules/plugin/entity/info.ts b/src/modules/plugin/entity/info.ts index fddebc8..3a7eaeb 100644 --- a/src/modules/plugin/entity/info.ts +++ b/src/modules/plugin/entity/info.ts @@ -42,6 +42,12 @@ export class PluginInfoEntity extends BaseEntity { data: string; }; + @Column({ comment: 'ts内容', type: 'json' }) + tsContent: { + type: 'ts'; + data: string; + }; + @Column({ comment: '插件的plugin.json', type: 'json', nullable: true }) pluginJson: any; diff --git a/src/modules/plugin/event/app.ts b/src/modules/plugin/event/app.ts index bf8d80c..984fbcf 100644 --- a/src/modules/plugin/event/app.ts +++ b/src/modules/plugin/event/app.ts @@ -10,6 +10,7 @@ import { import { IMidwayKoaApplication } from '@midwayjs/koa'; import { PLUGIN_CACHE_KEY, PluginCenterService } from '../service/center'; import { CachingFactory, MidwayCache } from '@midwayjs/cache-manager'; +import { PluginTypesService } from '../service/types'; /** * 插件事件 @@ -31,9 +32,13 @@ export class PluginAppEvent { @Inject() pluginCenterService: PluginCenterService; + @Inject() + pluginTypesService: PluginTypesService; + @Event('onServerReady') async onServerReady() { await this.midwayCache.set(PLUGIN_CACHE_KEY, []); this.pluginCenterService.init(); + this.pluginTypesService.reGenerate(); } } diff --git a/src/modules/plugin/hooks/upload/index.ts b/src/modules/plugin/hooks/upload/index.ts index 003bec7..a29df88 100644 --- a/src/modules/plugin/hooks/upload/index.ts +++ b/src/modules/plugin/hooks/upload/index.ts @@ -10,7 +10,7 @@ import * as _ from 'lodash'; /** * 文件上传 */ -export class PluginUpload extends BasePluginHook implements BaseUpload { +export class CoolPlugin extends BasePluginHook implements BaseUpload { /** * 获得上传模式 * @returns @@ -119,4 +119,4 @@ export class PluginUpload extends BasePluginHook implements BaseUpload { } // 导出插件实例, Plugin名称不可修改 -export const Plugin = PluginUpload; +export const Plugin = CoolPlugin; diff --git a/src/modules/plugin/service/info.ts b/src/modules/plugin/service/info.ts index 2b48423..75cc9f7 100644 --- a/src/modules/plugin/service/info.ts +++ b/src/modules/plugin/service/info.ts @@ -21,6 +21,8 @@ import { GLOBAL_EVENT_PLUGIN_INIT, GLOBAL_EVENT_PLUGIN_REMOVE, } from '../event/init'; +import { PluginMap, AnyString } from '../../../../typings/plugin'; +import { PluginTypesService } from './types'; /** * 插件信息 @@ -48,6 +50,9 @@ export class PluginService extends BaseService { @Inject() coolEventManager: CoolEventManager; + @Inject() + pluginTypesService: PluginTypesService; + /** * 新增或更新 * @param param @@ -84,6 +89,7 @@ export class PluginService extends BaseService { keyName, isHook ); + this.pluginTypesService.deleteDtsFile(keyName); } /** @@ -132,7 +138,11 @@ export class PluginService extends BaseService { * @param params 参数 * @returns */ - async invoke(key: string, method: string, ...params) { + async invoke( + key: K | AnyString, + method: string, + ...params + ) { // 实例 const instance = await this.getInstance(key); return await instance[method](...params); @@ -143,7 +153,9 @@ export class PluginService extends BaseService { * @param key * @returns */ - async getInstance(key: string) { + async getInstance( + key: K | AnyString + ): Promise { const check = await this.checkStatus(key); if (!check) throw new CoolCommException(`插件[${key}]不存在或已禁用`); let instance; @@ -223,13 +235,17 @@ export class PluginService extends BaseService { readme: string; logo: string; content: string; + tsContent: string; errorData: string; }> { - // const plugin = await download(encodeURI(url)); const decompress = require('decompress'); const files = await decompress(filePath); let errorData; - let pluginJson: PluginInfo, readme: string, logo: string, content: string; + let pluginJson: PluginInfo, + readme: string, + logo: string, + content: string, + tsContent: string; try { errorData = 'plugin.json'; pluginJson = JSON.parse( @@ -249,6 +265,11 @@ export class PluginService extends BaseService { path: 'src/index.js', type: 'file', }).data.toString(); + tsContent = + _.find(files, { + path: 'source/index.ts', + type: 'file', + })?.data?.toString() || ''; } catch (e) { throw new CoolCommException('插件信息不完整'); } @@ -257,6 +278,7 @@ export class PluginService extends BaseService { readme, logo, content, + tsContent, errorData, }; } @@ -272,7 +294,12 @@ export class PluginService extends BaseService { if (checkResult.type != 3 && !forceBool) { return checkResult; } - const { pluginJson, readme, logo, content } = await this.data(filePath); + const { pluginJson, readme, logo, content, tsContent } = await this.data( + filePath + ); + if (pluginJson.key == 'plugin') { + throw new CoolCommException('插件key不能为plugin,请更换其他key'); + } const check = await this.pluginInfoEntity.findOne({ where: { keyName: Equal(pluginJson.key) }, select: ['id', 'status', 'config'], @@ -289,6 +316,10 @@ export class PluginService extends BaseService { type: 'comm', data: content, }, + tsContent: { + type: 'ts', + data: tsContent, + }, description: pluginJson.description, pluginJson, config: pluginJson.config, @@ -308,6 +339,7 @@ export class PluginService extends BaseService { // 全新安装 await this.pluginInfoEntity.insert(data); } + this.pluginTypesService.generateDtsFile(pluginJson.key, tsContent); // 初始化插件 await this.reInit(pluginJson.key); } diff --git a/src/modules/plugin/service/types.ts b/src/modules/plugin/service/types.ts new file mode 100644 index 0000000..bd75da2 --- /dev/null +++ b/src/modules/plugin/service/types.ts @@ -0,0 +1,231 @@ +import { App, Provide } from '@midwayjs/decorator'; +import { BaseService } from '@cool-midway/core'; +import * as ts from 'typescript'; +import { IMidwayApplication } from '@midwayjs/core'; +import * as path from 'path'; +import * as fs from 'fs'; +import { InjectEntityModel } from '@midwayjs/typeorm'; +import { Repository } from 'typeorm'; +import { PluginInfoEntity } from '../entity/info'; +import * as prettier from 'prettier'; + +/** + * 插件类型服务 + */ +@Provide() +export class PluginTypesService extends BaseService { + @App() + app: IMidwayApplication; + + @InjectEntityModel(PluginInfoEntity) + pluginInfoEntity: Repository; + + /** + * 生成d.ts文件 + * @param tsContent + * @returns + */ + async dtsContent(tsContent: string) { + let output = ''; + + const compilerHost: ts.CompilerHost = { + fileExists: ts.sys.fileExists, + getCanonicalFileName: ts.sys.useCaseSensitiveFileNames + ? s => s + : s => s.toLowerCase(), + getCurrentDirectory: ts.sys.getCurrentDirectory, + getDefaultLibFileName: options => ts.getDefaultLibFilePath(options), + getDirectories: ts.sys.getDirectories, + getNewLine: () => ts.sys.newLine, + getSourceFile: (fileName, languageVersion) => { + if (fileName === 'file.ts') { + return ts.createSourceFile( + fileName, + tsContent, + languageVersion, + true + ); + } + const filePath = ts.sys.resolvePath(fileName); + return ts.sys.readFile(filePath) + ? ts.createSourceFile( + filePath, + ts.sys.readFile(filePath)!, + languageVersion, + true + ) + : undefined; + }, + readFile: ts.sys.readFile, + useCaseSensitiveFileNames: () => ts.sys.useCaseSensitiveFileNames, + writeFile: (fileName, content) => { + if (fileName.includes('file.d.ts')) { + output = content; + } + }, + }; + + const options: ts.CompilerOptions = { + declaration: true, + emitDeclarationOnly: true, + outDir: './', + }; + + const program = ts.createProgram(['file.ts'], options, compilerHost); + program.emit(); + + return output; + } + + /** + * 生成d.ts文件 + * @param key + * @param tsContent + * @returns + */ + async generateDtsFile(key: string, tsContent: string) { + const env = this.app.getEnv(); + // 不是本地开发环境不生成d.ts文件 + if (env != 'local' || !tsContent) { + return; + } + // 基础路径 + const basePath = path.join(this.app.getBaseDir(), '..', 'typings'); + // pluginDts文件路径 + const pluginDtsPath = path.join(basePath, 'plugin.d.ts'); + // plugin文件夹路径 + const pluginPath = path.join(basePath, `${key}.d.ts`); + // 生成d.ts文件 + const dtsContent = await this.dtsContent(tsContent); + + // 读取plugin.d.ts文件内容 + let pluginDtsContent = fs.readFileSync(pluginDtsPath, 'utf-8'); + + // 根据key判断是否在PluginMap中存在 + const keyWithHyphen = key.includes('-'); + const importStatement = keyWithHyphen + ? `import * as ${key.replace(/-/g, '_')} from './${key}';` + : `import * as ${key} from './${key}';`; + const pluginMapEntry = keyWithHyphen + ? `'${key}': ${key.replace(/-/g, '_')}.CoolPlugin;` + : `${key}: ${key}.CoolPlugin;`; + + // 检查import语句是否已经存在,若不存在则添加 + if (!pluginDtsContent.includes(importStatement)) { + pluginDtsContent = `${importStatement}\n${pluginDtsContent}`; + } + + // 检查PluginMap中的键是否存在,若不存在则添加 + if (pluginDtsContent.includes(pluginMapEntry)) { + // 键存在则覆盖 + const regex = new RegExp( + `(\\s*${keyWithHyphen ? `'${key}'` : key}:\\s*[^;]+;)` + ); + pluginDtsContent = pluginDtsContent.replace(regex, pluginMapEntry); + } else { + // 键不存在则追加 + const pluginMapRegex = /interface\s+PluginMap\s*{([^}]*)}/; + pluginDtsContent = pluginDtsContent.replace( + pluginMapRegex, + (match, p1) => { + return match.replace(p1, `${p1.trim()}\n ${pluginMapEntry}`); + } + ); + } + + // 格式化内容 + pluginDtsContent = await this.formatContent(pluginDtsContent); + + // 延迟2秒写入文件 + setTimeout(async () => { + // 写入d.ts文件,如果存在则覆盖 + fs.writeFile(pluginPath, await this.formatContent(dtsContent), () => {}); + + // 写入plugin.d.ts文件 + fs.writeFile(pluginDtsPath, pluginDtsContent, () => {}); + }, 2000); + } + + /** + * 删除d.ts文件中的指定key + * @param key + */ + async deleteDtsFile(key: string) { + const env = this.app.getEnv(); + // 不是本地开发环境不删除d.ts文件 + if (env != 'local') { + return; + } + // 基础路径 + const basePath = path.join(this.app.getBaseDir(), '..', 'typings'); + // pluginDts文件路径 + const pluginDtsPath = path.join(basePath, 'plugin.d.ts'); + // plugin文件夹路径 + const pluginPath = path.join(basePath, `${key}.d.ts`); + + // 读取plugin.d.ts文件内容 + let pluginDtsContent = fs.readFileSync(pluginDtsPath, 'utf-8'); + + // 根据key判断是否在PluginMap中存在 + const keyWithHyphen = key.includes('-'); + const importStatement = keyWithHyphen + ? `import \\* as ${key.replace(/-/g, '_')} from '\\./${key}';` + : `import \\* as ${key} from '\\./${key}';`; + const pluginMapEntry = keyWithHyphen + ? `'${key}': ${key.replace(/-/g, '_')}.CoolPlugin;` + : `${key}: ${key}.CoolPlugin;`; + + // 删除import语句 + const importRegex = new RegExp(`${importStatement}\\n`, 'g'); + pluginDtsContent = pluginDtsContent.replace(importRegex, ''); + + // 删除PluginMap中的键 + const pluginMapRegex = new RegExp(`\\s*${pluginMapEntry}`, 'g'); + pluginDtsContent = pluginDtsContent.replace(pluginMapRegex, ''); + + // 格式化内容 + pluginDtsContent = await this.formatContent(pluginDtsContent); + + // 延迟2秒写入文件 + setTimeout(async () => { + // 删除插件d.ts文件 + if (fs.existsSync(pluginPath)) { + fs.unlink(pluginPath, () => {}); + } + // 写入plugin.d.ts文件 + fs.writeFile(pluginDtsPath, pluginDtsContent, () => {}); + }, 2000); + } + + /** + * 格式化内容 + * @param content + */ + async formatContent(content: string) { + // 使用prettier格式化内容 + return prettier.format(content, { + parser: 'typescript', + singleQuote: true, + trailingComma: 'all', + bracketSpacing: true, + arrowParens: 'avoid', + printWidth: 80, + }); + } + + /** + * 重新生成d.ts文件 + */ + async reGenerate() { + const pluginInfos = await this.pluginInfoEntity + .createQueryBuilder('a') + .select(['a.id', 'a.status', 'a.tsContent', 'a.keyName']) + .getMany(); + for (const pluginInfo of pluginInfos) { + const tsContent = pluginInfo.tsContent?.data; + if (tsContent) { + await this.generateDtsFile(pluginInfo.keyName, tsContent); + } + } + } +} diff --git a/tsconfig.json b/tsconfig.json index eb4731b..f25c9ca 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -6,16 +6,19 @@ "moduleResolution": "node", "experimentalDecorators": true, "emitDecoratorMetadata": true, - "inlineSourceMap":true, + "inlineSourceMap": false, "noImplicitThis": true, "noUnusedLocals": false, "stripInternal": true, "skipLibCheck": true, "resolveJsonModule": true, "pretty": true, - "declaration": true, + "declaration": false, "noImplicitAny": false, - "typeRoots": [ "./typings", "./node_modules/@types"], + "typeRoots": [ + "typings", + "./node_modules/@types" + ], "outDir": "dist", "rootDir": "src" }, @@ -24,4 +27,4 @@ "node_modules", "test" ] -} +} \ No newline at end of file diff --git a/typings/plugin.d.ts b/typings/plugin.d.ts new file mode 100644 index 0000000..55178be --- /dev/null +++ b/typings/plugin.d.ts @@ -0,0 +1,12 @@ +import * as wx from './wx'; +import * as uniphone from './uniphone'; +import { BaseUpload, MODETYPE } from './upload'; +type AnyString = string & {}; +/** + * 插件类型声明 + */ +interface PluginMap { + wx: wx.CoolPlugin; + upload: BaseUpload; + uniphone: uniphone.CoolPlugin; +} diff --git a/typings/uniphone.d.ts b/typings/uniphone.d.ts new file mode 100644 index 0000000..35734c5 --- /dev/null +++ b/typings/uniphone.d.ts @@ -0,0 +1,14 @@ +import { BasePlugin } from '@cool-midway/plugin-cli'; +/** + * 一键手机号登录 + */ +export declare class CoolPlugin extends BasePlugin { + /** + * 获得手机号 + * @param access_token 前端获取的access_token + * @param openid 前端获取的openid + * @param appId 前端获取的appId + */ + getPhone(access_token: string, openid: string, appId: string): Promise; +} +export declare const Plugin: typeof CoolPlugin; diff --git a/typings/upload.d.ts b/typings/upload.d.ts new file mode 100644 index 0000000..4f0cdfc --- /dev/null +++ b/typings/upload.d.ts @@ -0,0 +1,56 @@ +// 模式 +export enum MODETYPE { + // 本地 + LOCAL = 'local', + // 云存储 + CLOUD = 'cloud', + // 其他 + OTHER = 'other', +} + +/** + * 上传模式 + */ +export interface Mode { + // 模式 + mode: MODETYPE; + // 类型 + type: string; +} + +/** + * 文件上传 + */ +export interface BaseUpload { + /** + * 获得上传模式 + */ + getMode(): Promise; + + /** + * 获得原始操作对象 + * @returns + */ + getMetaFileObj(): Promise; + + /** + * 下载并上传 + * @param url + * @param fileName 文件名 + */ + downAndUpload(url: string, fileName?: string): Promise; + + /** + * 指定Key(路径)上传,本地文件上传到存储服务 + * @param filePath 文件路径 + * @param key 路径一致会覆盖源文件 + */ + uploadWithKey(filePath, key): Promise; + + /** + * 上传文件 + * @param ctx + * @param key 文件路径 + */ + upload(ctx): Promise; +} diff --git a/typings/wx.d.ts b/typings/wx.d.ts new file mode 100644 index 0000000..cb375e7 --- /dev/null +++ b/typings/wx.d.ts @@ -0,0 +1,58 @@ +import { BasePlugin } from '@cool-midway/plugin-cli'; +import { + OfficialAccount, + MiniApp, + Pay, + OpenPlatform, + Work, + OpenWork, +} from 'node-easywechat'; +/** + * 微信 + */ +export declare class CoolPlugin extends BasePlugin { + /** + * 公众号 + * @param config + * @returns + */ + OfficialAccount(config?: any): Promise; + /** + * 小程序 + * @param config + * @returns + */ + MiniApp(config?: any): Promise; + /** + * 支付 + * @param config + * @returns + */ + Pay(config?: any): Promise; + /** + * 开放平台 + * @param config + * @returns + */ + OpenPlatform(config?: any): Promise; + /** + * 企业微信 + * @param config + * @returns + */ + Work(config?: any): Promise; + /** + * 企业微信开放平台 + * @param config + * @returns + */ + OpenWork(config?: any): Promise; + /** + * 设置缓存 + * @param app + */ + setCache( + app: OfficialAccount | MiniApp | Pay | OpenPlatform | Work | OpenWork + ): Promise; +} +export declare const Plugin: typeof CoolPlugin;