新增插件调用类型提示

This commit is contained in:
cool 2024-05-28 15:22:15 +08:00
parent 8d7e740484
commit 450700c211
13 changed files with 442 additions and 64 deletions

View File

@ -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"
},

View File

@ -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();
}
}

View File

@ -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: '你叫小酷,是个编程助手' },

View File

@ -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;

View File

@ -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();
}
}

View File

@ -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;

View File

@ -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<K extends keyof PluginMap>(
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<K extends keyof PluginMap>(
key: K | AnyString
): Promise<K extends keyof PluginMap ? PluginMap[K] : any> {
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);
}

View File

@ -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<PluginInfoEntity>;
/**
* 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);
}
}
}
}

View File

@ -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"
]
}
}

12
typings/plugin.d.ts vendored Normal file
View File

@ -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;
}

14
typings/uniphone.d.ts vendored Normal file
View File

@ -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<any>;
}
export declare const Plugin: typeof CoolPlugin;

56
typings/upload.d.ts vendored Normal file
View File

@ -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<Mode>;
/**
*
* @returns
*/
getMetaFileObj(): Promise<any>;
/**
*
* @param url
* @param fileName
*/
downAndUpload(url: string, fileName?: string): Promise<string>;
/**
* Key()
* @param filePath
* @param key
*/
uploadWithKey(filePath, key): Promise<string>;
/**
*
* @param ctx
* @param key
*/
upload(ctx): Promise<string>;
}

58
typings/wx.d.ts vendored Normal file
View File

@ -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<any>;
/**
*
* @param config
* @returns
*/
MiniApp(config?: any): Promise<any>;
/**
*
* @param config
* @returns
*/
Pay(config?: any): Promise<any>;
/**
*
* @param config
* @returns
*/
OpenPlatform(config?: any): Promise<any>;
/**
*
* @param config
* @returns
*/
Work(config?: any): Promise<any>;
/**
*
* @param config
* @returns
*/
OpenWork(config?: any): Promise<any>;
/**
*
* @param app
*/
setCache(
app: OfficialAccount | MiniApp | Pay | OpenPlatform | Work | OpenWork
): Promise<void>;
}
export declare const Plugin: typeof CoolPlugin;