mm_toolkit 0.0.0-stage → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +111 -2
- package/config.tpl.json +30 -0
- package/file.md +2 -0
- package/index.js +30 -0
- package/lib/manager.js +279 -0
- package/lib/tool.js +555 -0
- package/package.json +23 -4
- package/script.tpl.js +44 -0
- package/test.js +126 -0
package/README.md
CHANGED
|
@@ -1,3 +1,112 @@
|
|
|
1
|
-
#
|
|
1
|
+
# mm_tools 工具模块
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
工具是服务端暴露的可执行函数,让 AI 模型能够执行具体操作(如查询天气、发送消息、计算等)。本模块负责工具的参数转换(OpenAI function calling 格式)、脚本装载与执行,基于 `mm_mod` 生命周期,无全局对象与原型扩展依赖。
|
|
4
|
+
|
|
5
|
+
## 安装与引用
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install mm_tools
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
```javascript
|
|
12
|
+
const { Tool, ToolManager, toolsAdmin } = require('mm_tools');
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## 快速使用
|
|
16
|
+
|
|
17
|
+
```javascript
|
|
18
|
+
const { Manager } = require('mm_tools');
|
|
19
|
+
|
|
20
|
+
const manager = new Manager({ dir: './ai/tool' });
|
|
21
|
+
|
|
22
|
+
// 目录加载:每个子目录一个 tool.json(支持多层分组目录)
|
|
23
|
+
const tools = await manager.load('./ai/tool');
|
|
24
|
+
|
|
25
|
+
// 批量初始化,注入服务器/事件/日志
|
|
26
|
+
await manager.runAll('init', server, eventer, logger);
|
|
27
|
+
|
|
28
|
+
const tool = manager.getMod('read_file');
|
|
29
|
+
console.log(tool.toFunctionCall());
|
|
30
|
+
|
|
31
|
+
// 装载脚本并执行
|
|
32
|
+
tool.loadScript();
|
|
33
|
+
const ret = await tool.main({ file: './README.md', encoding: 'utf-8' });
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
```javascript
|
|
37
|
+
// 手动登记并初始化
|
|
38
|
+
const tool = await manager.create(
|
|
39
|
+
{
|
|
40
|
+
name: 'sum',
|
|
41
|
+
title: '求和',
|
|
42
|
+
params: [
|
|
43
|
+
{ name: 'a', type: 'number', required: true },
|
|
44
|
+
{ name: 'b', type: 'number', required: true }
|
|
45
|
+
]
|
|
46
|
+
},
|
|
47
|
+
server, eventer, logger
|
|
48
|
+
);
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## 工具目录结构
|
|
52
|
+
|
|
53
|
+
```text
|
|
54
|
+
tool/
|
|
55
|
+
└── group/
|
|
56
|
+
└── read_file/
|
|
57
|
+
├── tool.json # 工具配置
|
|
58
|
+
└── index.js # 工具脚本,导出 main 及子函数
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## 工具脚本规范
|
|
62
|
+
|
|
63
|
+
脚本导出对象中的函数会被绑定到工具实例,`main` 内部可通过 `this.xxx()` 复用子函数。
|
|
64
|
+
|
|
65
|
+
```javascript
|
|
66
|
+
module.exports = {
|
|
67
|
+
async calc(params) {
|
|
68
|
+
return params.a + params.b;
|
|
69
|
+
},
|
|
70
|
+
async main(params) {
|
|
71
|
+
try {
|
|
72
|
+
const data = await this.calc(params);
|
|
73
|
+
return { code: 0, msg: '执行成功', data };
|
|
74
|
+
} catch (error) {
|
|
75
|
+
return { code: 10000, msg: '执行失败!原因:' + error.message };
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
## 主要接口
|
|
82
|
+
|
|
83
|
+
| 接口 | 说明 |
|
|
84
|
+
| --- | --- |
|
|
85
|
+
| `manager.add(config)` | 登记工具,不初始化 |
|
|
86
|
+
| `manager.create(config, server, eventer, logger)` | 登记并初始化工具 |
|
|
87
|
+
| `manager.load(dir)` | 递归扫描目录加载全部工具 |
|
|
88
|
+
| `manager.runAll('init', server, eventer, logger)` | 批量初始化并注入依赖 |
|
|
89
|
+
| `manager.del(name)` | 注销并释放工具 |
|
|
90
|
+
| `manager.reload(name)` | 重新读取配置并注册 |
|
|
91
|
+
| `tool.toFunctionCall()` | 转换为 OpenAI function calling 格式 |
|
|
92
|
+
| `tool.loadScript()` | 装载工具脚本并绑定导出函数 |
|
|
93
|
+
| `tool.isLoaded()` | 脚本是否已装载 |
|
|
94
|
+
| `tool.main(params)` | 执行工具(由脚本覆盖) |
|
|
95
|
+
| `tool.getDetail()` | 获取工具详情(目录、配置文件、脚本文件) |
|
|
96
|
+
| `tool.getAgent/getProject/getTask/getSkill/getTool/getLLM` | 通过服务器获取关联对象 |
|
|
97
|
+
|
|
98
|
+
## 支持处理的文件
|
|
99
|
+
|
|
100
|
+
- [文件工具](file.md)
|
|
101
|
+
|
|
102
|
+
## 依赖
|
|
103
|
+
|
|
104
|
+
- `mm_mod`:生命周期基类与管理器
|
|
105
|
+
- `mm_corejs`:Admin 工厂
|
|
106
|
+
- 原生 `fs` / `path`:替代 `.fullname()/.addDir()/.saveText()/.loadText()` 等原型扩展
|
|
107
|
+
|
|
108
|
+
## 测试
|
|
109
|
+
|
|
110
|
+
```bash
|
|
111
|
+
npm test
|
|
112
|
+
```
|
package/config.tpl.json
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
{
|
|
2
|
+
// 工具名称
|
|
3
|
+
"name": "${name}",
|
|
4
|
+
// 工具标题(显示的中文名)
|
|
5
|
+
"title": "${title}",
|
|
6
|
+
// 工具描述
|
|
7
|
+
"description": "${description}",
|
|
8
|
+
// 工具分组
|
|
9
|
+
"group": "${group}",
|
|
10
|
+
// 工具类型, 默认为"function"
|
|
11
|
+
"type": "function",
|
|
12
|
+
// 工具主文件路径
|
|
13
|
+
"main": "./index.js",
|
|
14
|
+
// 工具是否严格模式参数校验
|
|
15
|
+
"strict": true,
|
|
16
|
+
// 工具参数是否允许额外字段
|
|
17
|
+
"add_params": false,
|
|
18
|
+
// 工具参数
|
|
19
|
+
"params": [],
|
|
20
|
+
// 工具返回值
|
|
21
|
+
"returns": [],
|
|
22
|
+
// 工具排序
|
|
23
|
+
"sort": 10,
|
|
24
|
+
// 工具状态, 0表示未启用, 1表示启用
|
|
25
|
+
"state": 1,
|
|
26
|
+
// 工具显示, 0表示不显示, 1表示显示
|
|
27
|
+
"show": 0,
|
|
28
|
+
// 是否结束
|
|
29
|
+
"end": false
|
|
30
|
+
}
|
package/file.md
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
## 支持的文件类型
|
|
2
|
+
.epub,.mobi,.azw3,.pdf,.png,.jpg,.jpeg,.svg,.svgz,.bmp,.gif,.webp,.ico,.xbm,.dib,.pjp,.tif,.pjpeg,.avif,.apng,.tiff,.jfif,.txt,.md,.csv,.tsv,.html,.json,.log,.dot,.go,.h,.c,.cpp,.cxx,.cc,.cs,.java,.js,.css,.jsp,.php,.py,.py3,.asp,.yaml,.yml,.ini,.conf,.ts,.tsx,.doc,.docx,.ppt,.pptx,.xls,.xlsx,.abap,.asc,.ash,.ampl,.mod,.g4,.apib,.apl,.dyalog,.asax,.ascx,.ashx,.asmx,.aspx,.axd,.dats,.hats,.sats,.as,.adb,.ada,.ads,.agda,.als,.apacheconf,.vhost,.cls,.applescript,.scpt,.arc,.ino,.asciidoc,.adoc,.aj,.asm,.a51,.inc,.nasm,.aug,.ahk,.ahkl,.au3,.awk,.auk,.gawk,.mawk,.nawk,.bat,.cmd,.befunge,.bison,.bb,.decls,.bmx,.bsv,.boo,.b,.bf,.brs,.bro,.cats,.idc,.w,.cake,.cshtml,.csx,.c++,.cp,.h++,.hh,.hpp,.hxx,.inl,.ipp,.tcc,.tpp,.c-objdump,.chs,.clp,.cmake,.in,.cob,.cbl,.ccp,.cobol,.cpy,.capnp,.mss,.ceylon,.chpl,.ch,.ck,.cirru,.clw,.icl,.dcl,.click,.clj,.boot,.cl2,.cljc,.cljs,.hl,.cljscm,.cljx,.hic,.coffee,._coffee,.cjsx,.cson,.iced,.cfm,.cfml,.cfc,.lisp,.asd,.cl,.l,.lsp,.ny,.podsl,.sexp,.cps,.coq,.v,.cppobjdump,.c++-objdump,.c++objdump,.cpp-objdump,.cxx-objdump,.creole,.cr,.feature,.cu,.cuh,.cy,.pyx,.pxd,.pxi,.d,.di,.d-objdump,.com,.dm,.zone,.arpa,.darcspatch,.dpatch,.dart,.diff,.patch,.dockerfile,.djs,.dylan,.dyl,.intr,.lid,.E,.ecl,.eclxml,.sch,.brd,.epj,.e,.ex,.exs,.elm,.el,.emacs,.desktop,.em,.emberscript,.erl,.es,.escript,.hrl,.xrl,.yrl,.fs,.fsi,.fsx,.fx,.flux,.f90,.f,.f03,.f08,.f77,.f95,.for,.fpp,.factor,.fy,.fancypack,.fan,.fth,.4th,.forth,.fr,.frt,.ftl,.g,.gco,.gcode,.gms,.gap,.gd,.gi,.tst,.s,.ms,.glsl,.fp,.frag,.frg,.fsh,.fshader,.geo,.geom,.glslv,.gshader,.shader,.vert,.vrx,.vsh,.vshader,.gml,.kid,.ebuild,.eclass,.po,.pot,.glf,.gp,.gnu,.gnuplot,.plot,.plt,.golo,.gs,.gst,.gsx,.vark,.grace,.gradle,.gf,.graphql,.gv,.man,.1in,.1m,.1x,.3in,.3m,.3qt,.3x,.me,.n,.rno,.roff,.groovy,.grt,.gtpl,.gvy,.gsp,.hcl,.tf,.hlsl,.fxh,.hlsli,.htm,.st,.xht,.xhtml,.mustache,.jinja,.eex,.erb,.deface,.phtml,.http,.haml,.handlebars,.hbs,.hb,.hs,.hsc,.hx,.hxsl,.hy,.pro,.dlm,.ipf,.cfg,.prefs,.properties,.irclog,.weechatlog,.idr,.lidr,.ni,.i7x,.iss,.io,.ik,.thy,.ijs,.flex,.jflex,.geojson,.lock,.topojson,.json5,.jsonld,.jq,.jsx,.jade,.j,._js,.bones,.es6,.jake,.jsb,.jscad,.jsfl,.jsm,.jss,.njs,.pac,.sjs,.ssjs,.sublime-build,.sublime-commands,.sublime-completions,.sublime-keymap,.sublime-macro,.sublime-menu,.sublime-mousemap,.sublime-project,.sublime-settings,.sublime-theme,.sublime-workspace,.sublime_metrics,.sublime_session,.xsjs,.xsjslib,.jl,.ipynb,.krl,.kicad_pcb,.kit,.kt,.ktm,.kts,.lfe,.ll,.lol,.lsl,.lslp,.lvproj,.lasso,.las,.lasso8,.lasso9,.ldml,.latte,.lean,.hlean,.less,.lex,.ly,.ily,.m,.ld,.lds,.liquid,.lagda,.litcoffee,.lhs,.ls,._ls,.xm,.x,.xi,.lgt,.logtalk,.lookml,.lua,.fcgi,.nse,.pd_lua,.rbxs,.wlua,.mumps,.m4,.mcr,.mtml,.muf,.mak,.mk,.mkfile,.mako,.mao,.markdown,.mkd,.mkdn,.mkdown,.ron,.mask,.mathematica,.cdf,.ma,.map,.mt,.nb,.nbp,.wl,.wlt,.matlab,.maxpat,.maxhelp,.maxproj,.mxt,.pat,.mediawiki,.wiki,.moo,.metal,.minid,.druby,.duby,.mir,.mirah,.mo,.mms,.mmk,.monkey,.moon,.myt,.ncl,.nl,.nsi,.nsh,.axs,.axi,.nlogo,.nginxconf,.nim,.nimrod,.ninja,.nit,.nix,.nu,.numpy,.numpyw,.numsc,.ml,.eliom,.eliomi,.ml4,.mli,.mll,.mly,.objdump,.mm,.sj,.omgrofl,.opa,.opal,.opencl,.p,.scad,.org,.ox,.oxh,.oxo,.oxygene,.oz,.pwn,.aw,.ctp,.php3,.php4,.php5,.php6,.php7,.php8,.phps,.phpt,.pls,.pck,.pkb,.pks,.plb,.plsql,.sql,.pov,.pan,.psc,.parrot,.pasm,.pir,.pas,.dfm,.dpr,.lpr,.pp,.pl,.al,.cgi,.perl,.ph,.plx,.pm,.pod,.psgi,.t,.6pl,.6pm,.nqp,.p6,.p6l,.p6m,.pl6,.pm6,.pkl,.pig,.pike,.pmod,.pogo,.pony,.ps,.eps,.ps1,.psd1,.psm1,.pde,.prolog,.yap,.spin,.proto,.pub,.pd,.pb,.pbi,.purs,.bzl,.gyp,.lmi,.pyde,.pyi,.pyp,.pyt,.pyw,.rpy,.tac,.wsgi,.xpy,.pytb,.qml,.qbs,.pri,.r,.rd,.rsx,.raml,.rdoc,.rbbas,.rbfrm,.rbmnu,.rbres,.rbtbar,.rbuistate,.rhtml,.rmd,.rkt,.rktd,.rktl,.scrbl,.rl,.raw,.reb,.r2,.r3,.rebol,.red,.reds,.cw,.rs,.rsh,.robot,.rg,.rb,.builder,.gemspec,.god,.irbrc,.jbuilder,.mspec,.pluginspec,.podspec,.rabl,.rake,.rbuild,.rbw,.rbx,.ru,.ruby,.thor,.watchr,.sas,.scss,.smt2,.smt,.sparql,.rq,.sqf,.hqf,.cql,.ddl,.prc,.tab,.udf,.viw,.db2,.ston,.sage,.sagews,.sls,.sass,.scala,.sbt,.sc,.scaml,.scm,.sld,.sps,.ss,.sci,.sce,.self,.sh,.bash,.bats,.command,.ksh,.tmux,.tool,.zsh,.sh-session,.shen,.sl,.slim,.smali,.tpl,.sp,.sma,.nut,.stan,.ML,.fun,.sig,.sml,.do,.ado,.doh,.ihlp,.mata,.matah,.sthlp,.styl,.scd,.swift,.sv,.svh,.vh,.toml,.txl,.tcl,.adp,.tm,.tcsh,.csh,.tex,.aux,.bbx,.bib,.cbx,.dtx,.ins,.lbx,.ltx,.mkii,.mkiv,.mkvi,.sty,.toc,.tea,.no,.textile,.thrift,.tu,.ttl,.twig,.upc,.anim,.asset,.mat,.meta,.prefab,.unity,.uno,.uc,.ur,.urs,.vcl,.vhdl,.vhd,.vhf,.vhi,.vho,.vhs,.vht,.vhw,.vala,.vapi,.veo,.vim,.vb,.bas,.frm,.frx,.vba,.vbhtml,.vbs,.volt,.vue,.owl,.webidl,.x10,.xc,.xml,.ant,.axml,.ccxml,.clixml,.cproject,.csl,.csproj,.ct,.dita,.ditamap,.ditaval,.config,.dotsettings,.filters,.fsproj,.fxml,.glade,.grxml,.iml,.ivy,.jelly,.jsproj,.kml,.launch,.mdpolicy,.mxml,.nproj,.nuspec,.odd,.osm,.plist,.props,.ps1xml,.psc1,.pt,.rdf,.rss,.scxml,.srdf,.storyboard,.stTheme,.sublime-snippet,.targets,.tmCommand,.tml,.tmLanguage,.tmPreferences,.tmSnippet,.tmTheme,.ui,.urdf,.ux,.vbproj,.vcxproj,.vssettings,.vxml,.wsdl,.wsf,.wxi,.wxl,.wxs,.x3d,.xacro,.xaml,.xib,.xlf,.xliff,.xmi,.dist,.xproj,.xsd,.xul,.zcml,.xsp-config,.metadata,.xpl,.xproc,.xquery,.xq,.xql,.xqm,.xqy,.xs,.xslt,.xsl,.xojo_code,.xojo_menu,.xojo_report,.xojo_script,.xojo_toolbar,.xojo_window,.xtend,.reek,.rviz,.sublime-syntax,.syntax,.yaml-tmlanguage,.yang,.y,.yacc,.yy,.zep,.zimpl,.zmpl,.zpl,.ec,.eh,.edn,.fish,.mu,.nc,.ooc,.rst,.rest,.wisp,.prg,.prw,.gitignore,.gitkeep,.gitmodules,.example,.avifs,.blp,.bufr,.bw,.cur,.dcx,.dds,.emf,.fit,.fits,.flc,.fli,.ftc,.ftu,.gbr,.grib,.h5,.hdf,.icb,.icns,.iim,.im,.j2c,.j2k,.jp2,.jpc,.jpe,.jpf,.jpx,.mpeg,.mpg,.msp,.pbm,.pcd,.pcx,.pfm,.pgm,.pnm,.ppm,.psd,.pxr,.qoi,.ras,.rgb,.rgba,.sgi,.tga,.vda,.vst,.wmf,.xpm
|
package/index.js
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const ToolManager = require('./lib/manager');
|
|
4
|
+
const Tool = require('./lib/tool');
|
|
5
|
+
const Admin = require('mm_corejs').Admin;
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* 工具管理器实例(Admin 工厂)
|
|
9
|
+
* 支持:require('mm_tools').admin.setServer(server) -> admin.get('name', config)
|
|
10
|
+
*/
|
|
11
|
+
const admin = new Admin();
|
|
12
|
+
admin.setClass(ToolManager);
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* 获取指定作用域的工具管理器
|
|
16
|
+
* @param {string} scope 作用域名称
|
|
17
|
+
* @param {object} config 管理器配置
|
|
18
|
+
* @returns {ToolManager} 工具管理器实例
|
|
19
|
+
*/
|
|
20
|
+
function toolsAdmin(scope, config) {
|
|
21
|
+
return admin.get(scope || 'os', config);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
module.exports = {
|
|
25
|
+
Tool,
|
|
26
|
+
ToolManager,
|
|
27
|
+
Manager: ToolManager,
|
|
28
|
+
admin,
|
|
29
|
+
toolsAdmin
|
|
30
|
+
};
|
package/lib/manager.js
ADDED
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const Manager = require('mm_mod').Manager;
|
|
6
|
+
const { loadJson } = require('mm_helper');
|
|
7
|
+
const Tool = require('./tool');
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* 工具管理器
|
|
11
|
+
* 手动登记与目录加载两种方式,均不依赖全局对象与原型扩展。
|
|
12
|
+
* @augments {Manager}
|
|
13
|
+
*/
|
|
14
|
+
class ToolManager extends Manager {
|
|
15
|
+
/**
|
|
16
|
+
* 配置参数
|
|
17
|
+
* @type {object}
|
|
18
|
+
*/
|
|
19
|
+
static config = {
|
|
20
|
+
// 名称
|
|
21
|
+
name: 'tool',
|
|
22
|
+
// 标题
|
|
23
|
+
title: '工具管理',
|
|
24
|
+
// 描述
|
|
25
|
+
description: '这是工具管理器',
|
|
26
|
+
// 工具配置文件名
|
|
27
|
+
filename: 'tool.json',
|
|
28
|
+
// 工具根目录,缺省为模块根目录
|
|
29
|
+
dir: ''
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* 构造函数
|
|
34
|
+
* @param {object} config 配置参数
|
|
35
|
+
* @param {object} parent 父级模块
|
|
36
|
+
*/
|
|
37
|
+
constructor(config, parent) {
|
|
38
|
+
super({ ...ToolManager.config, ...config }, parent);
|
|
39
|
+
// 受管驱动类
|
|
40
|
+
this.setClass(Tool);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* 获取父级模块
|
|
46
|
+
* @returns {object|null} 父级模块
|
|
47
|
+
*/
|
|
48
|
+
ToolManager.prototype.getParent = function () {
|
|
49
|
+
return this.getServer() || null;
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* 获取工具根目录
|
|
54
|
+
* @returns {string} 根目录绝对路径
|
|
55
|
+
*/
|
|
56
|
+
ToolManager.prototype.getDir = function () {
|
|
57
|
+
if (this.config.dir) {
|
|
58
|
+
return path.resolve(this.config.dir);
|
|
59
|
+
}
|
|
60
|
+
return path.join(__dirname, '..');
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* 获取工具基础目录
|
|
65
|
+
* @returns {string} 基础目录绝对路径
|
|
66
|
+
*/
|
|
67
|
+
ToolManager.prototype.getBaseDir = function () {
|
|
68
|
+
if (this.config.base_dir) {
|
|
69
|
+
return path.resolve(this.config.base_dir);
|
|
70
|
+
}
|
|
71
|
+
return this.getDir();
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* 获取模板目录
|
|
76
|
+
* @returns {string} 模板目录绝对路径
|
|
77
|
+
*/
|
|
78
|
+
ToolManager.prototype.getTplDir = function () {
|
|
79
|
+
if (this.config.tpl_dir) {
|
|
80
|
+
return path.resolve(this.config.tpl_dir);
|
|
81
|
+
}
|
|
82
|
+
return path.join(__dirname, '..');
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* 创建受管工具实例(唯一实例化入口,父级指向本管理器)
|
|
87
|
+
* @param {object} config 工具配置(必须包含 name)
|
|
88
|
+
* @returns {Tool} 工具实例
|
|
89
|
+
* @private
|
|
90
|
+
*/
|
|
91
|
+
ToolManager.prototype._createDrive = function (config) {
|
|
92
|
+
return new this.Cs(config, this);
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* 创建并初始化一个工具
|
|
97
|
+
* @param {object} config 工具配置(必须包含 name)
|
|
98
|
+
* @param {object} server 服务器实例
|
|
99
|
+
* @param {object} eventer 事件对象
|
|
100
|
+
* @param {object} logger 日志对象
|
|
101
|
+
* @returns {Promise<Tool>} 工具实例
|
|
102
|
+
*/
|
|
103
|
+
ToolManager.prototype.create = async function (config, server, eventer, logger) {
|
|
104
|
+
const drive = this.add(config);
|
|
105
|
+
await drive._initCore(server, eventer, logger);
|
|
106
|
+
await this._initDrive(drive);
|
|
107
|
+
return drive;
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* 重新加载一个工具
|
|
112
|
+
* @param {string} name 工具名称
|
|
113
|
+
* @returns {Promise<boolean>} 是否执行成功
|
|
114
|
+
*/
|
|
115
|
+
ToolManager.prototype.reload = async function (name) {
|
|
116
|
+
const drive = this.get(name);
|
|
117
|
+
if (!drive) {
|
|
118
|
+
this.log('warn', '工具不存在,无法重新加载', name);
|
|
119
|
+
return false;
|
|
120
|
+
}
|
|
121
|
+
try {
|
|
122
|
+
const file = drive.config.config_file || path.join(drive.getDir(), this.config.filename);
|
|
123
|
+
await this.unregister(name);
|
|
124
|
+
this._loadConfigFile(file);
|
|
125
|
+
return true;
|
|
126
|
+
} catch (error) {
|
|
127
|
+
this.log('warn', '工具重新加载失败', name, error);
|
|
128
|
+
return false;
|
|
129
|
+
}
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* 加载目录下的全部工具
|
|
134
|
+
* @param {string} dir 工具根目录(可选,缺省使用配置目录)
|
|
135
|
+
* @returns {Promise<Array<Tool>>} 工具实例列表
|
|
136
|
+
*/
|
|
137
|
+
ToolManager.prototype.load = async function (dir) {
|
|
138
|
+
const target = path.resolve(dir || this.getDir());
|
|
139
|
+
this.config.dir = target;
|
|
140
|
+
const files = this._findFiles(target);
|
|
141
|
+
for (const file of files) {
|
|
142
|
+
try {
|
|
143
|
+
this._loadConfigFile(file);
|
|
144
|
+
} catch (error) {
|
|
145
|
+
this.log('error', '加载工具配置失败', file, error);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
return this.list();
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* 清空全部工具(交由管理器统一释放并重置集合)
|
|
153
|
+
* @returns {Promise<ToolManager>} 当前实例
|
|
154
|
+
*/
|
|
155
|
+
ToolManager.prototype.clearMods = async function () {
|
|
156
|
+
await this.clear();
|
|
157
|
+
return this;
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* 获取工具实例
|
|
162
|
+
* @param {string} name 工具名称
|
|
163
|
+
* @returns {Tool|undefined} 工具实例
|
|
164
|
+
*/
|
|
165
|
+
ToolManager.prototype.getMod = function (name) {
|
|
166
|
+
return this.get(name);
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* 获取工具实例列表
|
|
171
|
+
* @returns {Array<Tool>} 工具实例数组
|
|
172
|
+
*/
|
|
173
|
+
ToolManager.prototype.getMods = function () {
|
|
174
|
+
return this.list();
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* 获取工具配置列表
|
|
179
|
+
* @param {string} keyword 名称关键词(可选)
|
|
180
|
+
* @returns {Array<object>} 工具配置数组
|
|
181
|
+
*/
|
|
182
|
+
ToolManager.prototype.getList = function (keyword) {
|
|
183
|
+
const infos = this.values().map((drive) => drive.config);
|
|
184
|
+
if (!keyword) {
|
|
185
|
+
return infos;
|
|
186
|
+
}
|
|
187
|
+
return infos.filter((info) => info.name && info.name.indexOf(keyword) !== -1);
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* 批量执行驱动的生命周期方法
|
|
192
|
+
* @param {string} method 方法名称,init 时转调 _initCore 注入依赖
|
|
193
|
+
* @param {...*} args 方法参数
|
|
194
|
+
* @returns {Promise<void>} 执行完成
|
|
195
|
+
*/
|
|
196
|
+
ToolManager.prototype.runAll = async function (method, ...args) {
|
|
197
|
+
const drives = this.list();
|
|
198
|
+
for (const drive of drives) {
|
|
199
|
+
try {
|
|
200
|
+
if (method === 'init') {
|
|
201
|
+
await drive._initCore(...args);
|
|
202
|
+
await this._initDrive(drive);
|
|
203
|
+
} else {
|
|
204
|
+
await drive[method](...args);
|
|
205
|
+
}
|
|
206
|
+
} catch (error) {
|
|
207
|
+
this.log('error', `${drive.config.name} 执行 ${method} 失败`, error);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* 初始化驱动实例(仅首次初始化,运行状态由管理器记录)
|
|
214
|
+
* @param {Tool} drive 工具实例
|
|
215
|
+
* @returns {Promise<void>} 初始化完成
|
|
216
|
+
* @private
|
|
217
|
+
*/
|
|
218
|
+
ToolManager.prototype._initDrive = async function (drive) {
|
|
219
|
+
const name = drive.config.name;
|
|
220
|
+
if (await this.isState(name, 'inited')) {
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
await drive.init();
|
|
224
|
+
await this.setState(name, 'inited');
|
|
225
|
+
};
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* 查找目录下的全部工具配置文件
|
|
229
|
+
* @param {string} dir 根目录
|
|
230
|
+
* @returns {Array<string>} 配置文件绝对路径数组
|
|
231
|
+
* @private
|
|
232
|
+
*/
|
|
233
|
+
ToolManager.prototype._findFiles = function (dir) {
|
|
234
|
+
const files = [];
|
|
235
|
+
this._walkDir(path.resolve(dir), files);
|
|
236
|
+
return files;
|
|
237
|
+
};
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* 递归遍历目录,收集匹配的配置文件
|
|
241
|
+
* @param {string} dir 当前目录
|
|
242
|
+
* @param {Array<string>} files 结果集合
|
|
243
|
+
* @private
|
|
244
|
+
*/
|
|
245
|
+
ToolManager.prototype._walkDir = function (dir, files) {
|
|
246
|
+
if (!fs.existsSync(dir)) {
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
250
|
+
for (const entry of entries) {
|
|
251
|
+
const full = path.join(dir, entry.name);
|
|
252
|
+
if (entry.isDirectory()) {
|
|
253
|
+
this._walkDir(full, files);
|
|
254
|
+
} else if (entry.isFile() && entry.name === this.config.filename) {
|
|
255
|
+
files.push(full);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
};
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* 读取配置文件并注册工具实例
|
|
262
|
+
* @param {string} file 配置文件绝对路径
|
|
263
|
+
* @returns {Tool} 工具实例
|
|
264
|
+
* @throws {Error} 配置文件不存在或内容非法时
|
|
265
|
+
* @private
|
|
266
|
+
*/
|
|
267
|
+
ToolManager.prototype._loadConfigFile = function (file) {
|
|
268
|
+
const target = path.resolve(file);
|
|
269
|
+
if (!fs.existsSync(target)) {
|
|
270
|
+
throw new Error(`工具配置文件不存在: ${target}`);
|
|
271
|
+
}
|
|
272
|
+
const config = loadJson(target);
|
|
273
|
+
if (!config.name) {
|
|
274
|
+
config.name = path.basename(path.dirname(target));
|
|
275
|
+
}
|
|
276
|
+
return this.add({ ...config, config_file: target });
|
|
277
|
+
};
|
|
278
|
+
|
|
279
|
+
module.exports = ToolManager;
|
package/lib/tool.js
ADDED
|
@@ -0,0 +1,555 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const Base = require('mm_mod').Base;
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* 工具
|
|
9
|
+
* 面向 OpenAI function calling 的函数单元,负责参数转换与脚本装载。
|
|
10
|
+
* @augments {Base}
|
|
11
|
+
*/
|
|
12
|
+
class Tool extends Base {
|
|
13
|
+
/**
|
|
14
|
+
* 工具配置对象
|
|
15
|
+
* @type {object}
|
|
16
|
+
*/
|
|
17
|
+
static config = {
|
|
18
|
+
// 工具名称
|
|
19
|
+
name: '',
|
|
20
|
+
// 工具标题(显示的中文名)
|
|
21
|
+
title: '',
|
|
22
|
+
// 工具描述
|
|
23
|
+
description: '',
|
|
24
|
+
// 工具示例
|
|
25
|
+
example: '',
|
|
26
|
+
// 工具类型, 默认为'function'
|
|
27
|
+
type: 'function',
|
|
28
|
+
// 工具分类
|
|
29
|
+
category: '物联网',
|
|
30
|
+
// 工具分组
|
|
31
|
+
group: 'web',
|
|
32
|
+
// 工具主文件路径
|
|
33
|
+
main: './index.js',
|
|
34
|
+
// 工具是否严格模式参数校验。严格模式下,参数必须严格符合定义,否则会抛出错误
|
|
35
|
+
strict: true,
|
|
36
|
+
// 工具参数是否允许额外字段。默认为false,即不允许额外字段
|
|
37
|
+
add_params: false,
|
|
38
|
+
// 工具参数
|
|
39
|
+
params: [],
|
|
40
|
+
// 结果
|
|
41
|
+
returns: [],
|
|
42
|
+
// 请求参数
|
|
43
|
+
request: [],
|
|
44
|
+
// 工具响应,默认不变
|
|
45
|
+
response: [
|
|
46
|
+
{
|
|
47
|
+
name: 'result',
|
|
48
|
+
type: 'object',
|
|
49
|
+
description: '操作结果',
|
|
50
|
+
example: '{}',
|
|
51
|
+
default: null
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
name: 'error',
|
|
55
|
+
type: 'object',
|
|
56
|
+
description: '错误信息',
|
|
57
|
+
example: '{"code":10000,"message":"执行失败"}',
|
|
58
|
+
default: null,
|
|
59
|
+
props: [
|
|
60
|
+
{
|
|
61
|
+
name: 'code',
|
|
62
|
+
description: '错误码',
|
|
63
|
+
example: '10000',
|
|
64
|
+
type: 'number',
|
|
65
|
+
default: 10000
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
name: 'message',
|
|
69
|
+
description: '错误信息',
|
|
70
|
+
example: '执行失败',
|
|
71
|
+
type: 'string',
|
|
72
|
+
default: ''
|
|
73
|
+
}
|
|
74
|
+
]
|
|
75
|
+
}
|
|
76
|
+
],
|
|
77
|
+
// 工具目录,缺省时按配置文件目录或管理器目录 + 工具名称推导
|
|
78
|
+
dir: ''
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* 构造函数
|
|
83
|
+
* @param {object} config 工具配置对象
|
|
84
|
+
* @param {object} parent 父级管理器
|
|
85
|
+
*/
|
|
86
|
+
constructor(config, parent) {
|
|
87
|
+
super({ ...Tool.config, ...config }, parent);
|
|
88
|
+
// 事件触发器
|
|
89
|
+
this._eventer = null;
|
|
90
|
+
// 脚本是否已装载
|
|
91
|
+
this._loaded = false;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* 获取父级管理器
|
|
97
|
+
* @returns {object|null} 父级管理器
|
|
98
|
+
*/
|
|
99
|
+
Tool.prototype.getParent = function () {
|
|
100
|
+
return this.getServer() || null;
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* 获取工具目录
|
|
105
|
+
* 取值顺序:配置目录 -> 配置文件所在目录 -> 父级目录 + 工具名称 -> 模块根目录
|
|
106
|
+
* @returns {string} 工具目录绝对路径
|
|
107
|
+
*/
|
|
108
|
+
Tool.prototype.getDir = function () {
|
|
109
|
+
if (this.config.dir) {
|
|
110
|
+
return path.resolve(this.config.dir);
|
|
111
|
+
}
|
|
112
|
+
if (this.config.config_file) {
|
|
113
|
+
return path.dirname(path.resolve(this.config.config_file));
|
|
114
|
+
}
|
|
115
|
+
const parent = this.getParent();
|
|
116
|
+
if (parent && typeof parent.getDir === 'function') {
|
|
117
|
+
return path.join(parent.getDir(), this.config.name);
|
|
118
|
+
}
|
|
119
|
+
return path.join(__dirname, '..');
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* 设置事件触发器
|
|
124
|
+
* @param {object} eventer 事件触发器
|
|
125
|
+
* @returns {Tool} 当前实例
|
|
126
|
+
*/
|
|
127
|
+
Tool.prototype.setEventer = function (eventer) {
|
|
128
|
+
this._eventer = eventer || null;
|
|
129
|
+
return this;
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* 获取事件触发器
|
|
134
|
+
* @returns {object|null} 事件触发器
|
|
135
|
+
*/
|
|
136
|
+
Tool.prototype.getEventer = function () {
|
|
137
|
+
if (this._eventer) {
|
|
138
|
+
return this._eventer;
|
|
139
|
+
}
|
|
140
|
+
const parent = this.getParent();
|
|
141
|
+
if (parent && typeof parent.getEventer === 'function') {
|
|
142
|
+
return parent.getEventer();
|
|
143
|
+
}
|
|
144
|
+
return null;
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* 触发事件
|
|
149
|
+
* @param {string} event 事件名称
|
|
150
|
+
* @param {...*} args 事件参数
|
|
151
|
+
* @returns {boolean} 是否触发成功
|
|
152
|
+
*/
|
|
153
|
+
Tool.prototype.emitEvent = function (event, ...args) {
|
|
154
|
+
const eventer = this.getEventer();
|
|
155
|
+
if (!eventer || typeof eventer.emit !== 'function') {
|
|
156
|
+
return false;
|
|
157
|
+
}
|
|
158
|
+
eventer.emit(event, ...args);
|
|
159
|
+
return true;
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* 初始化工具核心
|
|
164
|
+
* @param {object} server 服务器实例
|
|
165
|
+
* @param {object} eventer 事件触发器
|
|
166
|
+
* @param {object} logger 日志记录器
|
|
167
|
+
* @returns {Promise<Tool>} 当前实例
|
|
168
|
+
*/
|
|
169
|
+
Tool.prototype._initCore = async function (server, eventer, logger) {
|
|
170
|
+
if (server) {
|
|
171
|
+
this.setServer(server);
|
|
172
|
+
}
|
|
173
|
+
if (eventer) {
|
|
174
|
+
this.setEventer(eventer);
|
|
175
|
+
}
|
|
176
|
+
if (logger) {
|
|
177
|
+
this.setLogger(logger);
|
|
178
|
+
}
|
|
179
|
+
return this;
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* 装载工具脚本
|
|
184
|
+
* 将脚本导出的函数绑定到当前实例,供 main 内部以 this.xxx 方式复用。
|
|
185
|
+
* @returns {Tool} 当前实例
|
|
186
|
+
* @throws {Error} 脚本文件不存在时
|
|
187
|
+
*/
|
|
188
|
+
Tool.prototype.loadScript = function () {
|
|
189
|
+
const file = this.getScriptFile();
|
|
190
|
+
if (!file || !fs.existsSync(file)) {
|
|
191
|
+
throw new Error(`工具脚本文件不存在: ${file}`);
|
|
192
|
+
}
|
|
193
|
+
const resolved = require.resolve(file);
|
|
194
|
+
delete require.cache[resolved];
|
|
195
|
+
this._applyScript(require(resolved));
|
|
196
|
+
this._loaded = true;
|
|
197
|
+
return this;
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* 将脚本导出项绑定到实例
|
|
202
|
+
* @param {object} script 脚本导出对象
|
|
203
|
+
* @private
|
|
204
|
+
*/
|
|
205
|
+
Tool.prototype._applyScript = function (script) {
|
|
206
|
+
if (!script || typeof script !== 'object') {
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
for (const key of Object.keys(script)) {
|
|
210
|
+
const value = script[key];
|
|
211
|
+
this[key] = typeof value === 'function' ? value.bind(this) : value;
|
|
212
|
+
}
|
|
213
|
+
};
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* 脚本是否已装载
|
|
217
|
+
* @returns {boolean} 是否已装载
|
|
218
|
+
*/
|
|
219
|
+
Tool.prototype.isLoaded = function () {
|
|
220
|
+
return this._loaded;
|
|
221
|
+
};
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* 转为参数 - 用于兼容openAI的工具
|
|
225
|
+
* @param {object} params 参数配置对象
|
|
226
|
+
* @returns {object} 参数对象
|
|
227
|
+
* @private
|
|
228
|
+
*/
|
|
229
|
+
Tool.prototype._toParam = function (params) {
|
|
230
|
+
const config = params || {};
|
|
231
|
+
const type = config.type || 'object';
|
|
232
|
+
let properties;
|
|
233
|
+
let required = [];
|
|
234
|
+
let additionalProperties;
|
|
235
|
+
if (type === 'object') {
|
|
236
|
+
properties = {};
|
|
237
|
+
if (Array.isArray(config)) {
|
|
238
|
+
for (const item of config) {
|
|
239
|
+
this._getProps(properties, required, item);
|
|
240
|
+
}
|
|
241
|
+
if (this.config.add_params !== undefined) {
|
|
242
|
+
additionalProperties = this.config.add_params;
|
|
243
|
+
}
|
|
244
|
+
} else {
|
|
245
|
+
additionalProperties = this._toParamByObj(properties, required, config);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
const parameters = { type };
|
|
249
|
+
if (Object.keys(properties).length > 0) {
|
|
250
|
+
parameters.properties = properties;
|
|
251
|
+
if (required.length > 0) {
|
|
252
|
+
parameters.required = required;
|
|
253
|
+
}
|
|
254
|
+
if (additionalProperties !== undefined) {
|
|
255
|
+
parameters.additionalProperties = additionalProperties;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
return parameters;
|
|
259
|
+
};
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* 按对象结构转为参数
|
|
263
|
+
* @param {object} properties 参数属性集合
|
|
264
|
+
* @param {Array<string>} required 必填参数集合
|
|
265
|
+
* @param {object} params 参数配置对象
|
|
266
|
+
* @returns {boolean|undefined} 是否允许额外字段
|
|
267
|
+
* @private
|
|
268
|
+
*/
|
|
269
|
+
Tool.prototype._toParamByObj = function (properties, required, params) {
|
|
270
|
+
let additionalProperties;
|
|
271
|
+
if (params.type) {
|
|
272
|
+
if (params.required) {
|
|
273
|
+
for (const name of params.required) {
|
|
274
|
+
if (required.indexOf(name) === -1) {
|
|
275
|
+
required.push(name);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
const props = params.props || params.properties;
|
|
280
|
+
if (props) {
|
|
281
|
+
if (Array.isArray(props)) {
|
|
282
|
+
this._getPropsByArr(properties, required, props);
|
|
283
|
+
} else {
|
|
284
|
+
this._getPropsByObj(properties, required, props);
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
if (params.additionalProperties !== undefined) {
|
|
288
|
+
additionalProperties = params.additionalProperties;
|
|
289
|
+
}
|
|
290
|
+
} else {
|
|
291
|
+
this._getProps(properties, required, params);
|
|
292
|
+
if (this.config.add_params !== undefined) {
|
|
293
|
+
additionalProperties = this.config.add_params;
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
return additionalProperties;
|
|
297
|
+
};
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* 按数组结构转换参数
|
|
301
|
+
* @param {object} properties 参数属性集合
|
|
302
|
+
* @param {Array<string>} required 必填参数集合
|
|
303
|
+
* @param {Array<object>} params 参数配置数组
|
|
304
|
+
* @private
|
|
305
|
+
*/
|
|
306
|
+
Tool.prototype._getPropsByArr = function (properties, required, params) {
|
|
307
|
+
for (const param of params) {
|
|
308
|
+
this._getProps(properties, required, param);
|
|
309
|
+
}
|
|
310
|
+
};
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* 按对象结构转换参数
|
|
314
|
+
* @param {object} properties 参数属性集合
|
|
315
|
+
* @param {Array<string>} required 必填参数集合
|
|
316
|
+
* @param {object} params 参数配置对象
|
|
317
|
+
* @private
|
|
318
|
+
*/
|
|
319
|
+
Tool.prototype._getPropsByObj = function (properties, required, params) {
|
|
320
|
+
for (const key in params) {
|
|
321
|
+
this._getProps(properties, required, params[key], key);
|
|
322
|
+
}
|
|
323
|
+
};
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* 获取参数属性
|
|
327
|
+
* @param {object} properties 参数属性集合
|
|
328
|
+
* @param {Array<string>} required 必填参数集合
|
|
329
|
+
* @param {object} param 参数配置对象
|
|
330
|
+
* @param {string} key 参数名称
|
|
331
|
+
* @private
|
|
332
|
+
*/
|
|
333
|
+
Tool.prototype._getProps = function (properties, required, param, key) {
|
|
334
|
+
const key_name = key || param.name;
|
|
335
|
+
const pm = { ...param };
|
|
336
|
+
delete pm.name;
|
|
337
|
+
if (param.required) {
|
|
338
|
+
required.push(key_name);
|
|
339
|
+
}
|
|
340
|
+
delete pm.required;
|
|
341
|
+
let description = '';
|
|
342
|
+
if (param.title) {
|
|
343
|
+
description += param.title + ': ';
|
|
344
|
+
}
|
|
345
|
+
delete pm.title;
|
|
346
|
+
if (!description) {
|
|
347
|
+
description += param.description;
|
|
348
|
+
}
|
|
349
|
+
if (param.default !== undefined) {
|
|
350
|
+
description += ' 默认值为' + param.default;
|
|
351
|
+
}
|
|
352
|
+
if (param.example) {
|
|
353
|
+
description += ' 示例:' + param.example;
|
|
354
|
+
}
|
|
355
|
+
delete pm.example;
|
|
356
|
+
if (param.items) {
|
|
357
|
+
pm.items = this._toItems(param.items);
|
|
358
|
+
}
|
|
359
|
+
const props = param.properties || param.props;
|
|
360
|
+
if (props) {
|
|
361
|
+
const _properties = {};
|
|
362
|
+
const _required = [];
|
|
363
|
+
if (Array.isArray(props)) {
|
|
364
|
+
this._getPropsByArr(_properties, _required, props);
|
|
365
|
+
} else {
|
|
366
|
+
this._getPropsByObj(_properties, _required, props);
|
|
367
|
+
}
|
|
368
|
+
if (Object.keys(_properties).length > 0) {
|
|
369
|
+
pm.properties = _properties;
|
|
370
|
+
}
|
|
371
|
+
if (_required.length > 0) {
|
|
372
|
+
pm.required = _required;
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
properties[key_name] = {
|
|
376
|
+
...pm,
|
|
377
|
+
description
|
|
378
|
+
};
|
|
379
|
+
};
|
|
380
|
+
|
|
381
|
+
/**
|
|
382
|
+
* 转换数组项参数
|
|
383
|
+
* @param {object|Array<object>} items 数组项配置
|
|
384
|
+
* @returns {object} 参数对象
|
|
385
|
+
* @private
|
|
386
|
+
*/
|
|
387
|
+
Tool.prototype._toItems = function (items) {
|
|
388
|
+
const type = items.type || 'object';
|
|
389
|
+
const properties = {};
|
|
390
|
+
const required = [];
|
|
391
|
+
if (Array.isArray(items)) {
|
|
392
|
+
for (const item of items) {
|
|
393
|
+
this._getProps(properties, required, item);
|
|
394
|
+
}
|
|
395
|
+
} else if (type === 'object') {
|
|
396
|
+
const props = items.properties || items.props || {};
|
|
397
|
+
for (const key in props) {
|
|
398
|
+
this._getProps(properties, required, props[key], key);
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
const parameters = { type };
|
|
402
|
+
if (Object.keys(properties).length > 0) {
|
|
403
|
+
parameters.properties = properties;
|
|
404
|
+
if (required.length > 0) {
|
|
405
|
+
parameters.required = required;
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
return parameters;
|
|
409
|
+
};
|
|
410
|
+
|
|
411
|
+
/**
|
|
412
|
+
* 转为openAI的调用函数
|
|
413
|
+
* @returns {object} 调用函数对象
|
|
414
|
+
*/
|
|
415
|
+
Tool.prototype.toFunctionCall = function () {
|
|
416
|
+
const cg = this.config;
|
|
417
|
+
const { name, title, type = 'function', description, example = '' } = cg;
|
|
418
|
+
let desc = title || '';
|
|
419
|
+
if (description) {
|
|
420
|
+
desc += ': ' + description;
|
|
421
|
+
}
|
|
422
|
+
const info = { type };
|
|
423
|
+
if (type === 'function') {
|
|
424
|
+
if (example) {
|
|
425
|
+
desc += ' 示例:' + example;
|
|
426
|
+
}
|
|
427
|
+
const parameters = this._toParam(cg.parameters || cg.params || {});
|
|
428
|
+
info.function = {
|
|
429
|
+
name,
|
|
430
|
+
description: desc
|
|
431
|
+
};
|
|
432
|
+
if (parameters && Object.keys(parameters).length > 0) {
|
|
433
|
+
info.function.parameters = parameters;
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
return info;
|
|
437
|
+
};
|
|
438
|
+
|
|
439
|
+
/**
|
|
440
|
+
* 获取工具配置文件
|
|
441
|
+
* @returns {string} 配置文件绝对路径,未配置时为空字符串
|
|
442
|
+
*/
|
|
443
|
+
Tool.prototype.getConfigFile = function () {
|
|
444
|
+
const file = this.config.config_file;
|
|
445
|
+
return file ? path.resolve(this.getDir(), file) : '';
|
|
446
|
+
};
|
|
447
|
+
|
|
448
|
+
/**
|
|
449
|
+
* 获取工具脚本文件
|
|
450
|
+
* @returns {string} 脚本文件绝对路径,未配置时为空字符串
|
|
451
|
+
*/
|
|
452
|
+
Tool.prototype.getScriptFile = function () {
|
|
453
|
+
const file = this.config.script_file || this.config.main;
|
|
454
|
+
return file ? path.resolve(this.getDir(), file) : '';
|
|
455
|
+
};
|
|
456
|
+
|
|
457
|
+
/**
|
|
458
|
+
* 获取工具明细
|
|
459
|
+
* @returns {object} 工具明细对象
|
|
460
|
+
*/
|
|
461
|
+
Tool.prototype.getDetail = function () {
|
|
462
|
+
return {
|
|
463
|
+
...this.config,
|
|
464
|
+
dir: this.getDir(),
|
|
465
|
+
config_file: this.getConfigFile(),
|
|
466
|
+
script_file: this.getScriptFile()
|
|
467
|
+
};
|
|
468
|
+
};
|
|
469
|
+
|
|
470
|
+
/**
|
|
471
|
+
* 调用工具
|
|
472
|
+
* @param {object} params 参数
|
|
473
|
+
* @returns {object} 执行结果
|
|
474
|
+
*/
|
|
475
|
+
Tool.prototype.main = function (params) {
|
|
476
|
+
return params;
|
|
477
|
+
};
|
|
478
|
+
|
|
479
|
+
/**
|
|
480
|
+
* 获取智能体
|
|
481
|
+
* @param {string} agent_name 智能体名称(英文名,唯一标识)
|
|
482
|
+
* @returns {object} 智能体对象
|
|
483
|
+
*/
|
|
484
|
+
Tool.prototype.getAgent = function (agent_name) {
|
|
485
|
+
if (typeof agent_name !== 'string' || !agent_name.trim()) {
|
|
486
|
+
throw new TypeError('智能体名称必须是有效的字符串');
|
|
487
|
+
}
|
|
488
|
+
return this.getServer().getAgent(agent_name);
|
|
489
|
+
};
|
|
490
|
+
|
|
491
|
+
/**
|
|
492
|
+
* 获取项目
|
|
493
|
+
* @param {string} project_name 项目名称(英文名,唯一标识)
|
|
494
|
+
* @returns {object} 项目对象
|
|
495
|
+
*/
|
|
496
|
+
Tool.prototype.getProject = function (project_name) {
|
|
497
|
+
if (typeof project_name !== 'string' || !project_name.trim()) {
|
|
498
|
+
throw new TypeError('项目名称必须是有效的字符串');
|
|
499
|
+
}
|
|
500
|
+
return this.getServer().getProject(project_name);
|
|
501
|
+
};
|
|
502
|
+
|
|
503
|
+
/**
|
|
504
|
+
* 获取任务
|
|
505
|
+
* @param {string} project_name 项目名称(英文名,唯一标识)
|
|
506
|
+
* @param {string} task_name 任务名称(英文名,唯一标识)
|
|
507
|
+
* @returns {object} 任务对象
|
|
508
|
+
*/
|
|
509
|
+
Tool.prototype.getTask = function (project_name, task_name) {
|
|
510
|
+
if (typeof project_name !== 'string' || !project_name.trim()) {
|
|
511
|
+
throw new TypeError('项目名称必须是有效的字符串');
|
|
512
|
+
}
|
|
513
|
+
if (typeof task_name !== 'string' || !task_name.trim()) {
|
|
514
|
+
throw new TypeError('任务名称必须是有效的字符串');
|
|
515
|
+
}
|
|
516
|
+
return this.getServer().getTask(project_name, task_name);
|
|
517
|
+
};
|
|
518
|
+
|
|
519
|
+
/**
|
|
520
|
+
* 获取技能
|
|
521
|
+
* @param {string} skill_name 技能名称(英文名,唯一标识)
|
|
522
|
+
* @returns {object} 技能对象
|
|
523
|
+
*/
|
|
524
|
+
Tool.prototype.getSkill = function (skill_name) {
|
|
525
|
+
if (typeof skill_name !== 'string' || !skill_name.trim()) {
|
|
526
|
+
throw new TypeError('技能名称必须是有效的字符串');
|
|
527
|
+
}
|
|
528
|
+
return this.getServer().getSkill(skill_name);
|
|
529
|
+
};
|
|
530
|
+
|
|
531
|
+
/**
|
|
532
|
+
* 获取工具
|
|
533
|
+
* @param {string} tool_name 工具名称(英文名,唯一标识)
|
|
534
|
+
* @returns {object} 工具对象
|
|
535
|
+
*/
|
|
536
|
+
Tool.prototype.getTool = function (tool_name) {
|
|
537
|
+
if (typeof tool_name !== 'string' || !tool_name.trim()) {
|
|
538
|
+
throw new TypeError('工具名称必须是有效的字符串');
|
|
539
|
+
}
|
|
540
|
+
return this.getServer().getTool(tool_name);
|
|
541
|
+
};
|
|
542
|
+
|
|
543
|
+
/**
|
|
544
|
+
* 获取LLM模型
|
|
545
|
+
* @param {string} llm_name LLM模型名称
|
|
546
|
+
* @returns {object} LLM模型对象
|
|
547
|
+
*/
|
|
548
|
+
Tool.prototype.getLLM = function (llm_name) {
|
|
549
|
+
if (typeof llm_name !== 'string' || !llm_name.trim()) {
|
|
550
|
+
throw new TypeError('LLM模型名称必须是有效的字符串');
|
|
551
|
+
}
|
|
552
|
+
return this.getServer().getLLM(llm_name);
|
|
553
|
+
};
|
|
554
|
+
|
|
555
|
+
module.exports = Tool;
|
package/package.json
CHANGED
|
@@ -1,6 +1,25 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mm_toolkit",
|
|
3
|
-
"version": "
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "工具模块,面向 OpenAI function calling 的函数单元,负责参数转换与脚本装载,基于 mm_mod 生命周期,无全局对象与原型扩展依赖",
|
|
5
|
+
"main": "index.js",
|
|
6
|
+
"keywords": [
|
|
7
|
+
"mm_mod",
|
|
8
|
+
"mm_domain",
|
|
9
|
+
"mm_agent",
|
|
10
|
+
"tool",
|
|
11
|
+
"工具"
|
|
12
|
+
],
|
|
13
|
+
"layer": "domain",
|
|
14
|
+
"group": "agent",
|
|
15
|
+
"author": "邱文武",
|
|
16
|
+
"license": "ISC",
|
|
17
|
+
"dependencies": {
|
|
18
|
+
"mm_corejs": "^1.1.4",
|
|
19
|
+
"mm_helper": "^1.0.0",
|
|
20
|
+
"mm_mod": "^1.0.1"
|
|
21
|
+
},
|
|
22
|
+
"scripts": {
|
|
23
|
+
"test": "node test.js"
|
|
24
|
+
}
|
|
25
|
+
}
|
package/script.tpl.js
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
module.exports = {
|
|
2
|
+
/**
|
|
3
|
+
* 主要逻辑
|
|
4
|
+
* @param {object} params 参数
|
|
5
|
+
* @param {string} params.client_id 客户端ID
|
|
6
|
+
* @param {string} params.id 指令ID
|
|
7
|
+
* @param {string} params.method 方法类型
|
|
8
|
+
* @param {object} params.data 数据参数
|
|
9
|
+
*/
|
|
10
|
+
async xxx(params) {
|
|
11
|
+
// 主要代码写在这
|
|
12
|
+
if (!params.client_id) {
|
|
13
|
+
throw new Error('客户端ID不能为空');
|
|
14
|
+
}
|
|
15
|
+
if (!params.id) {
|
|
16
|
+
throw new Error('指令ID不能为空');
|
|
17
|
+
}
|
|
18
|
+
if (!params.method) {
|
|
19
|
+
throw new Error('方法类型不能为空');
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
/**
|
|
23
|
+
* 主要逻辑
|
|
24
|
+
* @param {object} params 参数
|
|
25
|
+
* @returns {object} 执行结果对象
|
|
26
|
+
*/
|
|
27
|
+
async main(params) {
|
|
28
|
+
let ret;
|
|
29
|
+
try {
|
|
30
|
+
let data = await this.xxx(params);
|
|
31
|
+
ret = {
|
|
32
|
+
code: 0,
|
|
33
|
+
msg: '执行成功',
|
|
34
|
+
data
|
|
35
|
+
};
|
|
36
|
+
} catch (error) {
|
|
37
|
+
ret = {
|
|
38
|
+
code: 10000,
|
|
39
|
+
msg: '执行失败!原因:' + error.message
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
return ret;
|
|
43
|
+
},
|
|
44
|
+
};
|
package/test.js
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const assert = require('assert');
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
const os = require('os');
|
|
6
|
+
const path = require('path');
|
|
7
|
+
const mod = require('./index.js');
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* 测试导出结构
|
|
11
|
+
*/
|
|
12
|
+
function testExports() {
|
|
13
|
+
assert.ok(mod.Tool, '应导出 Tool');
|
|
14
|
+
assert.ok(mod.Manager, '应导出 Manager');
|
|
15
|
+
assert.ok(mod.admin, '应导出 admin');
|
|
16
|
+
assert.strictEqual(typeof mod.toolsAdmin, 'function', '应导出 toolsAdmin 函数');
|
|
17
|
+
const instance = mod.toolsAdmin('test');
|
|
18
|
+
assert.ok(instance instanceof mod.Manager, 'toolsAdmin 返回的实例应为 Manager 类型');
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* 测试参数转换
|
|
23
|
+
* @param {string} dir 临时目录
|
|
24
|
+
*/
|
|
25
|
+
async function testFunctionCall(dir) {
|
|
26
|
+
const manager = new mod.Manager({ name: 'function', dir });
|
|
27
|
+
const tool = await manager.create(
|
|
28
|
+
{
|
|
29
|
+
name: 'sum',
|
|
30
|
+
title: '求和',
|
|
31
|
+
description: '计算两个数字之和',
|
|
32
|
+
dir: path.join(dir, 'sum'),
|
|
33
|
+
params: [
|
|
34
|
+
{ name: 'a', description: '第一个数字', type: 'number', required: true },
|
|
35
|
+
{ name: 'b', description: '第二个数字', type: 'number', required: true }
|
|
36
|
+
]
|
|
37
|
+
},
|
|
38
|
+
{ name: 'server' }
|
|
39
|
+
);
|
|
40
|
+
assert.ok(tool instanceof mod.Tool, 'create 返回的实例应为 Tool 类型');
|
|
41
|
+
assert.ok(await manager.isState('sum', 'inited'), '工具创建后应处于 inited 状态');
|
|
42
|
+
|
|
43
|
+
const info = tool.toFunctionCall();
|
|
44
|
+
assert.strictEqual(info.type, 'function', '类型应为 function');
|
|
45
|
+
assert.strictEqual(info.function.name, 'sum', '函数名应一致');
|
|
46
|
+
assert.ok(info.function.description.indexOf('计算两个数字之和') !== -1, '描述应包含工具描述');
|
|
47
|
+
assert.deepStrictEqual(info.function.parameters.required, ['a', 'b'], '必填参数应一致');
|
|
48
|
+
assert.strictEqual(info.function.parameters.properties.a.type, 'number', '参数类型应一致');
|
|
49
|
+
assert.ok(info.function.parameters.properties.a.description.indexOf('默认值') === -1, '无默认值时不追加默认值说明');
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* 测试脚本装载与执行
|
|
54
|
+
* @param {string} dir 临时目录
|
|
55
|
+
*/
|
|
56
|
+
async function testScript(dir) {
|
|
57
|
+
const target = path.join(dir, 'load');
|
|
58
|
+
const item = path.join(target, 'file', 'add');
|
|
59
|
+
fs.mkdirSync(item, { recursive: true });
|
|
60
|
+
const config = {
|
|
61
|
+
name: 'add',
|
|
62
|
+
title: '加法',
|
|
63
|
+
description: '两个数相加',
|
|
64
|
+
group: 'math',
|
|
65
|
+
main: './index.js',
|
|
66
|
+
params: [{ name: 'a', type: 'number', required: true }, { name: 'b', type: 'number', required: true }]
|
|
67
|
+
};
|
|
68
|
+
fs.writeFileSync(path.join(item, 'tool.json'), JSON.stringify(config), 'utf8');
|
|
69
|
+
fs.writeFileSync(
|
|
70
|
+
path.join(item, 'index.js'),
|
|
71
|
+
[
|
|
72
|
+
'module.exports = {',
|
|
73
|
+
' async calc(params) {',
|
|
74
|
+
' return params.a + params.b;',
|
|
75
|
+
' },',
|
|
76
|
+
' async main(params) {',
|
|
77
|
+
' const data = await this.calc(params);',
|
|
78
|
+
' return { code: 0, msg: "执行成功", data };',
|
|
79
|
+
' }',
|
|
80
|
+
'};',
|
|
81
|
+
''
|
|
82
|
+
].join('\n'),
|
|
83
|
+
'utf8'
|
|
84
|
+
);
|
|
85
|
+
|
|
86
|
+
const manager = new mod.Manager({ name: 'load' });
|
|
87
|
+
const list = await manager.load(target);
|
|
88
|
+
assert.strictEqual(list.length, 1, '应加载 1 个工具');
|
|
89
|
+
|
|
90
|
+
const tool = list[0];
|
|
91
|
+
assert.strictEqual(tool.getDir(), item, '工具目录应为配置文件所在目录');
|
|
92
|
+
assert.strictEqual(tool.getScriptFile(), path.join(item, 'index.js'), '脚本文件应指向 main');
|
|
93
|
+
|
|
94
|
+
await tool._initCore({ name: 'server' });
|
|
95
|
+
assert.strictEqual(tool.isLoaded(), false, '装载前应为未装载状态');
|
|
96
|
+
tool.loadScript();
|
|
97
|
+
assert.strictEqual(tool.isLoaded(), true, '装载后应为已装载状态');
|
|
98
|
+
|
|
99
|
+
const ret = await tool.main({ a: 1, b: 2 });
|
|
100
|
+
assert.strictEqual(ret.code, 0, '执行应成功');
|
|
101
|
+
assert.strictEqual(ret.data, 3, '脚本内部应能通过 this 调用子函数');
|
|
102
|
+
|
|
103
|
+
const detail = tool.getDetail();
|
|
104
|
+
assert.strictEqual(detail.config_file, path.join(item, 'tool.json'), '详情应包含配置文件');
|
|
105
|
+
assert.strictEqual(detail.script_file, path.join(item, 'index.js'), '详情应包含脚本文件');
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* 主测试入口
|
|
110
|
+
*/
|
|
111
|
+
async function main() {
|
|
112
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'mm_tools_'));
|
|
113
|
+
try {
|
|
114
|
+
testExports();
|
|
115
|
+
await testFunctionCall(dir);
|
|
116
|
+
await testScript(dir);
|
|
117
|
+
console.log('mm_tools 模块测试通过');
|
|
118
|
+
} finally {
|
|
119
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
main().catch((error) => {
|
|
124
|
+
console.error(error);
|
|
125
|
+
process.exit(1);
|
|
126
|
+
});
|