mm_cmd 0.0.0-stage → 1.1.1

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 CHANGED
@@ -1,3 +1,11 @@
1
- # Temporary Holding Version
2
-
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
1
+ ## 指令框架
2
+ 用于以聊天的方式操作程序。
3
+ 前端发送字符串,后端通过字符串判断是否为指令进而执行程序。提高工作效率。
4
+
5
+
6
+ ## 流程
7
+ 1. 前端发送消息,后端接收消息。
8
+ 2. 后端先进行基础的指令判断(内容是否匹配有指令关键词),判断消息是否为指令。
9
+ 3. 如果是指令,则执行指令处理。
10
+ 4. 如果不是指令,再交由AI判断是否为指令。
11
+ 5. 如果AI判断为指令,则执行指令处理。
package/com.json ADDED
@@ -0,0 +1,5 @@
1
+ {
2
+ "name": "Cmd",
3
+ "title": "指令管理器",
4
+ "state": 1
5
+ }
@@ -0,0 +1,48 @@
1
+ {
2
+ // 指令名称, 用于动态增删改配置
3
+ "name": "{name || 'demo'}",
4
+ // 标题, 用于查询指令时
5
+ "title": "{title || '指令标题'}",
6
+ // 描述, 用于介绍该指令的作用
7
+ "description": "{description || '暂无描述'}",
8
+ // 文件名, 用于驱动脚本, 实现业务
9
+ "main": "{main || './index.js'}",
10
+ // 指令状态, 1为启用, 0为禁用
11
+ "state": 1,
12
+ // 时态, before之前、main主要、after之后
13
+ "tense": "{tense || 'main'}",
14
+ // 匹配关键词组, 消息内容包含任一关键词则触发指令
15
+ "match": ["触发词1", "触发词2"],
16
+ // 排除关键词组, 包含任一排除词则不触发指令
17
+ "not": [],
18
+ // 执行顺序, 数值越小越优先执行
19
+ "sort": 100,
20
+ // 结束标志: 1为匹配后不再执行后续指令, 0为继续执行
21
+ "end": 1,
22
+ // 指令参数, 类似于驱动脚本中使用 openAI 工具时传递参数
23
+ "params": {
24
+ "properties": {
25
+ "tracking_no": {
26
+ "type": "string",
27
+ "description": "快递单号",
28
+ "example": "1234567890",
29
+ "question": "快递单号多少?",
30
+ "error": "请输入正确的快递单号",
31
+ "match": "{数字}{英文}",
32
+ "default": null
33
+ },
34
+ "company": {
35
+ "type": "string",
36
+ "description": "快递公司",
37
+ "example": "顺丰",
38
+ "question": "快递公司多少?",
39
+ "error": "请输入正确的快递公司",
40
+ "match": "{中文}快递",
41
+ "enum": ["顺丰", "圆通", "中通", "韵达", "申通"],
42
+ "remove": ["快递"],
43
+ "default": null
44
+ }
45
+ },
46
+ "required": []
47
+ }
48
+ }
@@ -0,0 +1,76 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * mm_cmd 指令框架模块示例
5
+ * 演示:注册一条"打招呼"指令,加载脚本后用字符串消息触发匹配与执行。
6
+ * 注:管理器 add() 存在未定义 Drive 的缺陷,此处直接实例化 Cmd 驱动演示核心 API。
7
+ * 运行:node example/index.js
8
+ */
9
+
10
+ const fs = require('fs');
11
+ const os = require('os');
12
+ const path = require('path');
13
+ const { cmdAdmin, Cmd } = require('../index.js');
14
+
15
+ /**
16
+ * 生成临时指令脚本(导出 main 方法)
17
+ * @returns {string} 脚本文件路径
18
+ */
19
+ function createScript() {
20
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'mm_cmd-'));
21
+ const file = path.join(dir, 'hello.js');
22
+ fs.writeFileSync(
23
+ file,
24
+ `'use strict';
25
+ module.exports = {
26
+ /**
27
+ * 指令主函数
28
+ * @param {object} msg 消息对象
29
+ * @returns {string} 回复内容
30
+ */
31
+ main: async function (msg) {
32
+ return '你好,我是机器人!';
33
+ }
34
+ };
35
+ `,
36
+ 'utf8'
37
+ );
38
+ return file;
39
+ }
40
+
41
+ /**
42
+ * 主函数
43
+ * @returns {Promise<void>} 无返回值
44
+ */
45
+ async function main() {
46
+ // 1. 获取指令管理器(同 scope 复用实例)
47
+ const manager = cmdAdmin('demo', { title: '示例指令管理器' });
48
+ const manager2 = cmdAdmin('demo');
49
+ console.log('管理器实例复用:', manager === manager2);
50
+
51
+ // 2. 创建指令驱动(匹配"你好"开头的消息)
52
+ const drive = new Cmd(
53
+ {
54
+ name: 'hello',
55
+ title: '打招呼',
56
+ match: ['/你好/'],
57
+ main: createScript(),
58
+ func_name: 'main'
59
+ },
60
+ manager
61
+ );
62
+ console.log('驱动类型:', drive instanceof Cmd);
63
+
64
+ // 3. 匹配并执行
65
+ const ret = await drive.run({ content: '你好', from_user: 'u1', to_user: 'bot', chatid: 'c1' }, {});
66
+ console.log('执行结果:', JSON.stringify(ret));
67
+
68
+ // 4. 不匹配的内容返回空
69
+ const no_match = await drive.run({ content: '再见' }, {});
70
+ console.log('不匹配时结果:', no_match);
71
+ }
72
+
73
+ main().catch((err) => {
74
+ console.error('示例执行失败:', err);
75
+ process.exit(1);
76
+ });
package/index.js ADDED
@@ -0,0 +1,17 @@
1
+ const Admin = require('mm_corejs').Admin;
2
+ const Manager = require('./lib/manager');
3
+
4
+ let admin = new Admin();
5
+ admin.setClass(Manager);
6
+
7
+ module.exports = {
8
+ Cmd: require('./lib/cmd'),
9
+ Manager,
10
+ admin,
11
+ cmdAdmin(scope, config) {
12
+ if (!scope) {
13
+ scope = 'os';
14
+ }
15
+ return admin.get(scope, config);
16
+ }
17
+ };
@@ -0,0 +1,66 @@
1
+ {
2
+ "name": "job",
3
+ "title": "找工作",
4
+ "description": "用户输入求职意向, 登记信息并搜索匹配岗位",
5
+ "main": "./index.js",
6
+ "state": 1,
7
+ "tense": "main",
8
+ "match": ["找工作", "求职", "应聘", "找工作"],
9
+ "not": [],
10
+ "sort": 100,
11
+ "end": 1,
12
+ "params": {
13
+ "properties": {
14
+ "position": {
15
+ "type": "string",
16
+ "description": "意向岗位",
17
+ "example": "前端开发",
18
+ "question": "你想找什么岗位的工作?比如: 前端开发、后端开发、UI设计",
19
+ "match": "{中文}",
20
+ "default": null
21
+ },
22
+ "name": {
23
+ "type": "string",
24
+ "description": "姓名",
25
+ "example": "张三",
26
+ "match": "{中文}",
27
+ "default": null
28
+ },
29
+ "phone": {
30
+ "type": "string",
31
+ "description": "手机号码",
32
+ "example": "13800138000",
33
+ "match": "{手机号码}",
34
+ "default": null
35
+ },
36
+ "location": {
37
+ "type": "string",
38
+ "description": "工作地点",
39
+ "example": "深圳",
40
+ "question": "想在哪个城市工作?",
41
+ "match": "{中文}",
42
+ "enum": [
43
+ "北京",
44
+ "上海",
45
+ "广州",
46
+ "深圳",
47
+ "杭州",
48
+ "成都",
49
+ "武汉",
50
+ "南京"
51
+ ],
52
+ "default": null
53
+ },
54
+ "salary": {
55
+ "type": "string",
56
+ "description": "期望薪资",
57
+ "example": "15k",
58
+ "question": "期望薪资多少?比如: 10k-15k",
59
+ "match": "{数字}k",
60
+ "remove": ["k"],
61
+ "default": null
62
+ }
63
+ },
64
+ "required": ["position"]
65
+ }
66
+ }
package/job/index.js ADDED
@@ -0,0 +1,241 @@
1
+ /**
2
+ * 模拟岗位库
3
+ */
4
+ var jobPool = [
5
+ {
6
+ title: '前端开发工程师',
7
+ company: '科技公司A',
8
+ location: '深圳',
9
+ salary: '12k-18k',
10
+ },
11
+ {
12
+ title: '高级前端开发',
13
+ company: '互联网公司B',
14
+ location: '北京',
15
+ salary: '18k-25k',
16
+ },
17
+ {
18
+ title: '前端架构师',
19
+ company: '金融科技C',
20
+ location: '上海',
21
+ salary: '25k-35k',
22
+ },
23
+ {
24
+ title: 'Web前端开发',
25
+ company: '电商平台D',
26
+ location: '广州',
27
+ salary: '10k-15k',
28
+ },
29
+ {
30
+ title: 'React前端工程师',
31
+ company: '游戏公司E',
32
+ location: '杭州',
33
+ salary: '15k-22k',
34
+ },
35
+ {
36
+ title: 'Vue前端开发',
37
+ company: '教育科技F',
38
+ location: '成都',
39
+ salary: '10k-16k',
40
+ },
41
+ {
42
+ title: '前端负责人',
43
+ company: 'AI公司G',
44
+ location: '深圳',
45
+ salary: '20k-30k',
46
+ },
47
+ {
48
+ title: '全栈开发',
49
+ company: '创业公司H',
50
+ location: '北京',
51
+ salary: '15k-25k',
52
+ },
53
+ {
54
+ title: '后端开发工程师',
55
+ company: '云计算I',
56
+ location: '深圳',
57
+ salary: '15k-22k',
58
+ },
59
+ {
60
+ title: 'Java开发',
61
+ company: '银行J',
62
+ location: '上海',
63
+ salary: '14k-20k',
64
+ },
65
+ {
66
+ title: 'Go开发工程师',
67
+ company: '区块链K',
68
+ location: '杭州',
69
+ salary: '20k-30k',
70
+ },
71
+ {
72
+ title: 'Python开发',
73
+ company: 'AI公司L',
74
+ location: '北京',
75
+ salary: '18k-28k',
76
+ },
77
+ {
78
+ title: 'UI设计师',
79
+ company: '设计公司M',
80
+ location: '上海',
81
+ salary: '12k-18k',
82
+ },
83
+ {
84
+ title: '产品经理',
85
+ company: '互联网公司N',
86
+ location: '深圳',
87
+ salary: '15k-25k',
88
+ },
89
+ {
90
+ title: '测试工程师',
91
+ company: '软件公司O',
92
+ location: '武汉',
93
+ salary: '10k-15k',
94
+ },
95
+ {
96
+ title: '运维工程师',
97
+ company: '数据中心P',
98
+ location: '南京',
99
+ salary: '12k-18k',
100
+ },
101
+ ];
102
+
103
+ /**
104
+ * 解析薪资范围取中间值
105
+ * @param {string} range 薪资范围字符串, 如 "12k-18k"
106
+ * @returns {number} 中间值
107
+ */
108
+ function getAvg(range) {
109
+ var parts = range.replace(/k/g, '').split('-');
110
+ return (parseInt(parts[0], 10) + parseInt(parts[1], 10)) / 2;
111
+ }
112
+
113
+ /**
114
+ * 按关键词过滤岗位
115
+ * @param {Array} lt 岗位列表
116
+ * @param {string} kw 关键词
117
+ * @returns {Array} 过滤结果
118
+ */
119
+ function filterByKw(lt, kw) {
120
+ var ret = lt.filter((j) => {
121
+ return j.title.indexOf(kw) !== -1 || j.title.indexOf(kw.replace('开发', '')) !== -1;
122
+ });
123
+ return ret.length > 0 ? ret : jobPool.slice(0, 5);
124
+ }
125
+
126
+ /**
127
+ * 按城市排序
128
+ * @param {Array} lt 岗位列表
129
+ * @param {string} loc 目标城市
130
+ */
131
+ function sortByLocation(lt, loc) {
132
+ lt.sort((a, b) => {
133
+ if (a.location === loc && b.location !== loc) return -1;
134
+ if (b.location === loc && a.location !== loc) return 1;
135
+ return 0;
136
+ });
137
+ }
138
+
139
+ /**
140
+ * 按薪资接近度排序
141
+ * @param {Array} lt 岗位列表
142
+ * @param {string} salary 期望薪资
143
+ */
144
+ function sortBySalary(lt, salary) {
145
+ var target = parseInt(salary, 10) || 15;
146
+ lt.sort((a, b) => {
147
+ return Math.abs(getAvg(a.salary) - target) - Math.abs(getAvg(b.salary) - target);
148
+ });
149
+ }
150
+
151
+ /**
152
+ * 格式化岗位列表为文本
153
+ * @param {Array} lt 岗位列表
154
+ * @returns {string} 格式化文本
155
+ */
156
+ function formatList(lt) {
157
+ var list = lt.slice(0, 4).map((j, i) => {
158
+ return (i + 1) + '. ' + j.title + ' | ' + j.company + ' | ' + j.location + ' | ' + j.salary;
159
+ });
160
+ return '为你找到以下岗位:\n\n' + list.join('\n') + '\n\n已登记求职信息, 有合适岗位将推送通知';
161
+ }
162
+
163
+ /**
164
+ * 搜索岗位
165
+ * @param {object} form 用户表单
166
+ * @returns {string} 搜索结果文本
167
+ */
168
+ function search(form) {
169
+ var results = jobPool.slice();
170
+ if (form.position) {
171
+ results = filterByKw(results, form.position);
172
+ }
173
+ if (form.location) {
174
+ sortByLocation(results, form.location);
175
+ }
176
+ if (form.salary) {
177
+ sortBySalary(results, form.salary);
178
+ }
179
+ return formatList(results);
180
+ }
181
+
182
+ /**
183
+ * 收集缺失的可选信息
184
+ * @param {object} form 用户表单
185
+ * @returns {Array} 缺失信息列表
186
+ */
187
+ function getMissing(form) {
188
+ var missing = [];
189
+ if (!form.name) missing.push('姓名');
190
+ if (!form.phone) missing.push('手机号');
191
+ if (!form.location) missing.push('工作城市');
192
+ if (!form.salary) missing.push('期望薪资(如: 15k)');
193
+ return missing;
194
+ }
195
+
196
+ /**
197
+ * 构建已收集信息摘要
198
+ * @param {object} form 用户表单
199
+ * @returns {Array} 摘要行
200
+ */
201
+ function buildSummary(form) {
202
+ var has = ['岗位: ' + form.position];
203
+ if (form.location) has.push('城市: ' + form.location);
204
+ if (form.salary) has.push('薪资: ' + form.salary + 'k');
205
+ if (form.name) has.push('姓名: ' + form.name);
206
+ return has;
207
+ }
208
+
209
+ module.exports = {
210
+ /**
211
+ * 执行指令主函数
212
+ * @param {object} msg 消息对象
213
+ * @param {object} db 数据管理器
214
+ * @returns {object|string} 执行结果
215
+ */
216
+ async main(msg, db) {
217
+ var form = msg.form || {};
218
+ if (msg.content === '搜索' || msg.content === '好了' || msg.content === '跳过') {
219
+ return search(form);
220
+ }
221
+ var missing = getMissing(form);
222
+ if (missing.length > 0) {
223
+ msg.end = 0;
224
+ return (
225
+ '已收集:\n' +
226
+ buildSummary(form).join('\n') +
227
+ '\n\n还需要: ' +
228
+ missing.join('、') +
229
+ '\n请回复补充, 或回复"搜索"直接查找'
230
+ );
231
+ }
232
+ if (db && db.table) {
233
+ try {
234
+ await db.table('job_candidate').addObj(form);
235
+ } catch {
236
+ // 登记失败不影响搜索
237
+ }
238
+ }
239
+ return search(form);
240
+ },
241
+ };
package/job/test.js ADDED
@@ -0,0 +1,188 @@
1
+ /**
2
+ * 模拟"找工作"完整对话流程测试
3
+ *
4
+ * 测试 Drive 核心管线: 关键字匹配 → 槽位填充 → 会话恢复 → 结果返回
5
+ * 手动管理会话状态模拟多轮 NPC 式对话
6
+ */
7
+ // 指令匹配依赖 mm_matchs 注入的 String.prototype.matchs(需 mm_matchs 环境)
8
+ require('mm_matchs');
9
+
10
+ var Drive = require('../drive');
11
+ var job_config = require('./config.json');
12
+ var job_script = require('./index');
13
+
14
+ // 创建模拟 mod: 绑定 Drive 原型方法 + job 配置 + job 脚本
15
+ var mod = {
16
+ name: 'job',
17
+ config: Object.assign({}, Drive.config, job_config),
18
+ call: function (name, msg, db) {
19
+ return job_script.main(msg, db);
20
+ },
21
+ };
22
+
23
+ mod.match = Drive.prototype.match.bind(mod);
24
+ mod.run = Drive.prototype.run.bind(mod);
25
+ mod._fillParams = Drive.prototype._fillParams.bind(mod);
26
+ mod._tryExtractParam = Drive.prototype._tryExtractParam.bind(mod);
27
+ mod._cleanVal = Drive.prototype._cleanVal.bind(mod);
28
+ mod.log = function () {};
29
+
30
+ /**
31
+ * 发送消息并获取回复
32
+ * @param {object} msg 消息对象(会被修改)
33
+ * @param {string} content 用户输入
34
+ * @param {boolean} is_session 是否为会话继续(跳过match)
35
+ * @returns {object|null} AI回复
36
+ */
37
+ async function send(msg, content, is_session) {
38
+ if (!is_session) {
39
+ delete msg.cmd;
40
+ delete msg.form;
41
+ delete msg.stage;
42
+ }
43
+ delete msg.end;
44
+ msg.content = content;
45
+ return await mod.run(msg, {});
46
+ }
47
+
48
+ /**
49
+ * 打印分隔线
50
+ */
51
+ function hr(title) {
52
+ console.log('\n' + '─'.repeat(55));
53
+ console.log(' ' + title);
54
+ console.log('─'.repeat(55));
55
+ }
56
+
57
+ /**
58
+ * 打印对话
59
+ */
60
+ function chat(role, text) {
61
+ if (role === '用户') {
62
+ console.log('\n 👤 用户: ' + text);
63
+ } else {
64
+ console.log(' 🤖 系统: ' + text);
65
+ }
66
+ }
67
+
68
+ /**
69
+ * 打印内部状态
70
+ */
71
+ function debug(msg) {
72
+ console.log('\n [调试]');
73
+ console.log(' msg.cmd : ' + (msg.cmd || '(无)'));
74
+ console.log(' msg.end : ' + (msg.end === 0 ? '0 (待续)' : msg.end || '(无)'));
75
+ console.log(' msg.form: ' + JSON.stringify(msg.form || {}));
76
+ }
77
+
78
+ // ===== 执行完整对话测试 =====
79
+ async function main() {
80
+ var pass = 0;
81
+ var fail = 0;
82
+
83
+ function check(name, actual, expected) {
84
+ if (actual === expected) {
85
+ pass++;
86
+ } else {
87
+ fail++;
88
+ console.log(' ❌ ' + name + ' 期望: "' + expected + '", 实际: "' + actual + '"');
89
+ }
90
+ }
91
+
92
+ var msg = { from_user: 'user_001', to_user: 'bot', chatid: 'chat_001' };
93
+
94
+ // ===== Round 1: 触发指令 =====
95
+ hr('第1轮: 关键词触发');
96
+ chat('用户', '找工作');
97
+
98
+ var ret = await send(msg, '找工作');
99
+ chat('系统', ret.content);
100
+ debug(msg);
101
+
102
+ console.log('\n [验证]');
103
+ check('cmd匹配', msg.cmd, 'job');
104
+ check('end状态', msg.end, 0);
105
+ check('position被过滤', msg.form.position, undefined);
106
+ check('返回追问', ret.content.indexOf('什么岗位') > -1, true);
107
+
108
+ // ===== Round 2: 填写岗位 =====
109
+ hr('第2轮: 填写意向岗位');
110
+ chat('用户', '前端开发');
111
+
112
+ ret = await send(msg, '前端开发', true);
113
+ chat('系统', ret.content);
114
+ debug(msg);
115
+
116
+ console.log('\n [验证]');
117
+ check('position填充', msg.form.position, '前端开发');
118
+ check('end状态', msg.end, 0);
119
+ check('返回追问', ret.content.indexOf('还需要') > -1, true);
120
+
121
+ // ===== Round 3: 填写城市和薪资 =====
122
+ hr('第3轮: 补充城市和薪资');
123
+ chat('用户', '深圳 15k');
124
+
125
+ ret = await send(msg, '深圳 15k', true);
126
+ chat('系统', ret.content);
127
+ debug(msg);
128
+
129
+ console.log('\n [验证]');
130
+ check('location填充', msg.form.location, '深圳');
131
+ check('salary填充', msg.form.salary, '15');
132
+ check('end状态', msg.end, 0);
133
+ check('追问手机号', ret.content.indexOf('手机号') > -1, true);
134
+
135
+ // ===== Round 4: 填写手机号 =====
136
+ hr('第4轮: 填写手机号');
137
+ chat('用户', '13800138000');
138
+
139
+ ret = await send(msg, '13800138000', true);
140
+ chat('系统', ret.content);
141
+ debug(msg);
142
+
143
+ console.log('\n [验证]');
144
+ check('phone填充', msg.form.phone, '13800138000');
145
+ check('end状态', msg.end, undefined);
146
+ check('返回岗位结果', ret.content.indexOf('为你找到以下岗位') > -1, true);
147
+
148
+ // ===== Round 5: 搜索岗位(覆盖全部信息) =====
149
+ hr('第5轮: 搜索岗位(补充姓名后)');
150
+ msg.form.name = '张三';
151
+ chat('用户', '张三');
152
+
153
+ ret = await send(msg, '张三', true);
154
+ chat('系统', ret.content);
155
+ debug(msg);
156
+
157
+ console.log('\n [验证]');
158
+ check('name填充', msg.form.name, '张三');
159
+ check('end完成', msg.end, undefined);
160
+ check('返回岗位', ret.content.indexOf('为你找到以下岗位') > -1, true);
161
+ check('无追问', ret.content.indexOf('还需要') > -1, false);
162
+
163
+ // ===== Round 6: 新会话重新找工作 =====
164
+ hr('第6轮: 新会话触发(验证状态隔离)');
165
+ var msg2 = { from_user: 'user_002', to_user: 'bot', chatid: 'chat_002' };
166
+ chat('用户', '应聘');
167
+ ret = await send(msg2, '应聘');
168
+ chat('系统', ret.content);
169
+ debug(msg2);
170
+
171
+ console.log('\n [验证]');
172
+ check('新会话cmd', msg2.cmd, 'job');
173
+ check('form为空', JSON.stringify(msg2.form), '{}');
174
+
175
+ // ===== 打印结果汇总 =====
176
+ hr('测试结果汇总');
177
+ var total = pass + fail;
178
+ console.log('\n 总计: ' + total + ' 项');
179
+ console.log(' 通过: ' + pass + ' 项 ✅');
180
+ if (fail > 0) {
181
+ console.log(' 失败: ' + fail + ' 项 ❌');
182
+ }
183
+ console.log('\n ' + (fail === 0 ? '全部通过! 🎉' : '存在失败项, 请检查'));
184
+ }
185
+
186
+ main().catch(function (err) {
187
+ console.error('测试异常:', err);
188
+ });