@aiyiran/myclaw 1.0.30 → 1.0.31
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/index.js +3 -4
- package/package.json +5 -3
- package/wizards/commons/gateway.steps.json +52 -0
- package/wizards/configs/weixin/bind.config.json +153 -0
- package/wizards/{weixin.config.json → configs/weixin/index.config.json} +13 -16
- package/wizards/index.js +32 -0
- package/wizards/runner/config-loader.js +89 -0
- package/wizards/runner/input-prompt.js +96 -0
- package/wizards/runner/render.js +87 -0
- package/wizards/runner/wizard-runner.js +541 -0
- package/wizards/wizard-runner.js +0 -353
package/index.js
CHANGED
|
@@ -237,13 +237,12 @@ function runNew() {
|
|
|
237
237
|
}
|
|
238
238
|
|
|
239
239
|
// ============================================================================
|
|
240
|
-
//
|
|
240
|
+
// 向导系统(统一入口)
|
|
241
241
|
// ============================================================================
|
|
242
242
|
|
|
243
243
|
function runWeixin() {
|
|
244
|
-
const {
|
|
245
|
-
|
|
246
|
-
new WizardRunner(config).run();
|
|
244
|
+
const { runWeixin } = require('./wizards/index');
|
|
245
|
+
runWeixin();
|
|
247
246
|
}
|
|
248
247
|
|
|
249
248
|
// ============================================================================
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aiyiran/myclaw",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.31",
|
|
4
4
|
"description": "",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"bin": {
|
|
@@ -15,7 +15,9 @@
|
|
|
15
15
|
"author": "",
|
|
16
16
|
"license": "ISC",
|
|
17
17
|
"dependencies": {
|
|
18
|
-
"chalk": "^4.1.2"
|
|
19
|
-
|
|
18
|
+
"chalk": "^4.1.2"
|
|
19
|
+
},
|
|
20
|
+
"engines": {
|
|
21
|
+
"node": ">=16"
|
|
20
22
|
}
|
|
21
23
|
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
{
|
|
2
|
+
"restart": {
|
|
3
|
+
"id": "restart-gateway",
|
|
4
|
+
"icon": "🔄",
|
|
5
|
+
"label": "重启 Gateway",
|
|
6
|
+
"type": "action",
|
|
7
|
+
"command": {
|
|
8
|
+
"exec": "openclaw gateway restart",
|
|
9
|
+
"display": "openclaw gateway restart"
|
|
10
|
+
},
|
|
11
|
+
"teach": {
|
|
12
|
+
"title": "重启 Gateway 使配置生效",
|
|
13
|
+
"description": "重启 OpenClaw 核心服务,让之前的所有配置变更生效。\n\nGateway 是 OpenClaw 的「大脑」,它在启动时读取配置。\n修改配置(如安装插件、启用插件)后,\n需要重启让 Gateway 加载最新配置。\n\n类比:改了系统设置后,有时候需要重启才能生效。",
|
|
14
|
+
"look_for": "重启后 Gateway 运行中,控制台地址 http://127.0.0.1:18789",
|
|
15
|
+
"tip": "⏳ 重启期间短暂不可用,请等待几秒钟再继续。"
|
|
16
|
+
}
|
|
17
|
+
},
|
|
18
|
+
|
|
19
|
+
"check-health": {
|
|
20
|
+
"id": "check-gateway-health",
|
|
21
|
+
"icon": "🔍",
|
|
22
|
+
"label": "检查 Gateway 运行状态",
|
|
23
|
+
"type": "check",
|
|
24
|
+
"command": {
|
|
25
|
+
"exec": "openclaw health",
|
|
26
|
+
"display": "openclaw health"
|
|
27
|
+
},
|
|
28
|
+
"teach": {
|
|
29
|
+
"title": "检查 Gateway 是否正在运行",
|
|
30
|
+
"description": "检查 OpenClaw Gateway 的健康状态。\n\n返回 OK 说明 Gateway 正常运行,\n返回错误说明 Gateway 未启动或出现问题。",
|
|
31
|
+
"look_for": "输出 OK 或类似健康状态提示",
|
|
32
|
+
"tip": "🔒 只读检查,可以随时运行。"
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
|
|
36
|
+
"check-status": {
|
|
37
|
+
"id": "check-openclaw-status",
|
|
38
|
+
"icon": "📊",
|
|
39
|
+
"label": "查看 OpenClaw 完整状态",
|
|
40
|
+
"type": "check",
|
|
41
|
+
"command": {
|
|
42
|
+
"exec": "openclaw status",
|
|
43
|
+
"display": "openclaw status"
|
|
44
|
+
},
|
|
45
|
+
"teach": {
|
|
46
|
+
"title": "查看 OpenClaw 完整运行状态",
|
|
47
|
+
"description": "显示 OpenClaw 所有组件的运行状态,\n包括 Gateway、已加载插件、已连接渠道等。",
|
|
48
|
+
"look_for": "查看各组件状态是否正常",
|
|
49
|
+
"tip": "🔒 只读命令,可以随时查看。"
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "weixin-bind",
|
|
3
|
+
"title": "微信多账号绑定",
|
|
4
|
+
"subtitle": "为每个微信账号绑定专属的 Agent",
|
|
5
|
+
|
|
6
|
+
"steps": [
|
|
7
|
+
{
|
|
8
|
+
"id": "check-before-bind",
|
|
9
|
+
"icon": "🔍",
|
|
10
|
+
"label": "查看当前账号列表",
|
|
11
|
+
"type": "check",
|
|
12
|
+
"command": {
|
|
13
|
+
"exec": "openclaw channels status",
|
|
14
|
+
"display": "openclaw channels status"
|
|
15
|
+
},
|
|
16
|
+
"teach": {
|
|
17
|
+
"title": "查看当前已登录的账号列表",
|
|
18
|
+
"description": "在开始绑定之前,先看看当前哪些微信账号已经登录。\n\n这条命令会列出所有已登录的渠道账号,以及它们当前\n绑定到哪个 Agent(如果已绑定的话)。\n\n从这里你可以确认账号ID(--account 参数的值),\n后续绑定步骤会用到。",
|
|
19
|
+
"look_for": "每行显示一个账号,记下账号ID(通常是一串标识符)",
|
|
20
|
+
"tip": "🔒 只读操作,可以随时运行查看最新状态。"
|
|
21
|
+
}
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
"id": "login-account-1",
|
|
25
|
+
"icon": "📱",
|
|
26
|
+
"label": "扫码登录第一个微信账号",
|
|
27
|
+
"type": "interactive",
|
|
28
|
+
"command": {
|
|
29
|
+
"exec": "openclaw channels login --channel openclaw-weixin",
|
|
30
|
+
"display": "openclaw channels login --channel openclaw-weixin"
|
|
31
|
+
},
|
|
32
|
+
"teach": {
|
|
33
|
+
"title": "扫码登录第一个微信账号",
|
|
34
|
+
"description": "每个微信账号需要单独扫码登录,系统会为它创建一个独立的账号条目。\n\n这一步登录「第一个微信账号」(比如用于课堂答疑的账号)。\n\n操作步骤:\n 1. 打开手机微信(第一个账号)\n 2. 扫描终端里的二维码\n 3. 手机上点击「确认授权」",
|
|
35
|
+
"look_for": "登录成功后,系统会显示新的账号条目(记下账号ID)",
|
|
36
|
+
"tip": "📱 确保手机是你想绑定的第一个账号,不要用错手机。"
|
|
37
|
+
}
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
"id": "login-account-2",
|
|
41
|
+
"icon": "📱",
|
|
42
|
+
"label": "扫码登录第二个微信账号",
|
|
43
|
+
"type": "interactive",
|
|
44
|
+
"command": {
|
|
45
|
+
"exec": "openclaw channels login --channel openclaw-weixin",
|
|
46
|
+
"display": "openclaw channels login --channel openclaw-weixin"
|
|
47
|
+
},
|
|
48
|
+
"teach": {
|
|
49
|
+
"title": "扫码登录第二个微信账号",
|
|
50
|
+
"description": "现在登录「第二个微信账号」(比如用于生活助手的账号)。\n\n这条命令和第一步相同,但登录的是不同的微信账号,\n系统会自动识别并创建新的独立账号条目。\n\n操作步骤:\n 1. 打开手机微信(第二个账号)\n 2. 扫描终端里的二维码\n 3. 手机上点击「确认授权」",
|
|
51
|
+
"look_for": "看到第二个账号条目出现在列表中",
|
|
52
|
+
"tip": "📱 换另一部手机(或另一个微信账号)来扫码。"
|
|
53
|
+
}
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
"id": "check-accounts",
|
|
57
|
+
"icon": "🔍",
|
|
58
|
+
"label": "查看账号列表(确认登录成功)",
|
|
59
|
+
"type": "check",
|
|
60
|
+
"capture": "accounts_output",
|
|
61
|
+
"command": {
|
|
62
|
+
"exec": "openclaw channels status",
|
|
63
|
+
"display": "openclaw channels status"
|
|
64
|
+
},
|
|
65
|
+
"teach": {
|
|
66
|
+
"title": "确认两个账号都已登录",
|
|
67
|
+
"description": "再次查看账号列表,确认两个微信账号都已成功登录。\n\n在这一步,你需要从输出中:\n 1. 找到第一个账号的「账号ID」\n 2. 找到第二个账号的「账号ID」\n\n后面的绑定步骤(第 5、6 步)的 --account 参数就填这些ID。\n\n✅ 这一步的输出会被记录,绑定时可直接参考。",
|
|
68
|
+
"look_for": "两个账号都出现在列表中,状态为 connected",
|
|
69
|
+
"tip": "📋 详细看输出,记录账号ID,避免后续填写出错。"
|
|
70
|
+
}
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
"id": "bind-account-1",
|
|
74
|
+
"icon": "🔗",
|
|
75
|
+
"label": "绑定账号1 → Agent",
|
|
76
|
+
"type": "input",
|
|
77
|
+
"requires": ["check-accounts"],
|
|
78
|
+
"show_capture": "accounts_output",
|
|
79
|
+
"command": {
|
|
80
|
+
"template": "openclaw channels bind --channel openclaw-weixin --account {{account}} --agent {{agent}}",
|
|
81
|
+
"display": "openclaw channels bind --channel openclaw-weixin --account {{account}} --agent {{agent}}"
|
|
82
|
+
},
|
|
83
|
+
"inputs": [
|
|
84
|
+
{
|
|
85
|
+
"key": "account",
|
|
86
|
+
"label": "账号1 的账号ID",
|
|
87
|
+
"hint": "从上一步(步骤4)的输出中找到第一个账号的标识符",
|
|
88
|
+
"placeholder": "例如:wx_123456789"
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
"key": "agent",
|
|
92
|
+
"label": "要绑定到的 Agent 名称",
|
|
93
|
+
"hint": "填写你已创建的 Agent 名称(使用 myclaw status 可查看)",
|
|
94
|
+
"placeholder": "例如:blog、helper、assistant"
|
|
95
|
+
}
|
|
96
|
+
],
|
|
97
|
+
"teach": {
|
|
98
|
+
"title": "将账号1绑定到指定 Agent",
|
|
99
|
+
"description": "这条命令将第一个微信账号与指定的 Agent 绑定。\n\n绑定后,从这个微信账号发来的消息,\n都会由对应的 Agent 来处理和回复。\n\n命令中的两个参数:\n --account 指定要绑定的微信账号ID\n --agent 指定负责处理消息的 Agent 名称",
|
|
100
|
+
"look_for": "看到绑定成功的提示",
|
|
101
|
+
"tip": "💡 绑定是永久的,除非你重新执行绑定命令修改。"
|
|
102
|
+
}
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
"id": "bind-account-2",
|
|
106
|
+
"icon": "🔗",
|
|
107
|
+
"label": "绑定账号2 → Agent",
|
|
108
|
+
"type": "input",
|
|
109
|
+
"requires": ["check-accounts"],
|
|
110
|
+
"show_capture": "accounts_output",
|
|
111
|
+
"command": {
|
|
112
|
+
"template": "openclaw channels bind --channel openclaw-weixin --account {{account}} --agent {{agent}}",
|
|
113
|
+
"display": "openclaw channels bind --channel openclaw-weixin --account {{account}} --agent {{agent}}"
|
|
114
|
+
},
|
|
115
|
+
"inputs": [
|
|
116
|
+
{
|
|
117
|
+
"key": "account",
|
|
118
|
+
"label": "账号2 的账号ID",
|
|
119
|
+
"hint": "从步骤4的输出中找到第二个账号的标识符",
|
|
120
|
+
"placeholder": "例如:wx_987654321"
|
|
121
|
+
},
|
|
122
|
+
{
|
|
123
|
+
"key": "agent",
|
|
124
|
+
"label": "要绑定到的 Agent 名称",
|
|
125
|
+
"hint": "填写另一个 Agent 的名称(与账号1绑定的不同)",
|
|
126
|
+
"placeholder": "例如:classroom、assistant"
|
|
127
|
+
}
|
|
128
|
+
],
|
|
129
|
+
"teach": {
|
|
130
|
+
"title": "将账号2绑定到另一个 Agent",
|
|
131
|
+
"description": "这条命令将第二个微信账号绑定到另一个 Agent。\n\n多账号绑定的典型用法:\n - 账号1 → Agent「blog」 ← 处理博客读者的问题\n - 账号2 → Agent「classroom」← 处理学生课堂问题\n\n每个微信账号有自己专属的对话上下文和 Agent 配置。",
|
|
132
|
+
"look_for": "看到绑定成功的提示",
|
|
133
|
+
"tip": "💡 两个账号绑定的 Agent 不同,这正是多账号的意义所在。"
|
|
134
|
+
}
|
|
135
|
+
},
|
|
136
|
+
{
|
|
137
|
+
"id": "verify-bindings",
|
|
138
|
+
"icon": "✅",
|
|
139
|
+
"label": "验证绑定结果",
|
|
140
|
+
"type": "check",
|
|
141
|
+
"command": {
|
|
142
|
+
"exec": "openclaw channels status",
|
|
143
|
+
"display": "openclaw channels status"
|
|
144
|
+
},
|
|
145
|
+
"teach": {
|
|
146
|
+
"title": "验证所有账号的绑定情况",
|
|
147
|
+
"description": "最后查看账号状态,确认绑定配置正确。\n\n如果配置成功,你应该能看到:\n - 账号1 → Agent 名称A\n - 账号2 → Agent 名称B\n\n如果某个账号绑定不对,重新执行对应的绑定步骤(第5或第6步)。",
|
|
148
|
+
"look_for": "每个账号后面显示了正确的 Agent 名称",
|
|
149
|
+
"tip": "🎉 绑定完成后,从不同微信账号发消息,对应 Agent 会自动回复!"
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
]
|
|
153
|
+
}
|
|
@@ -84,22 +84,7 @@
|
|
|
84
84
|
"tip": "📱 准备好手机,扫码后在手机上点击确认才算完成。"
|
|
85
85
|
}
|
|
86
86
|
},
|
|
87
|
-
{
|
|
88
|
-
"id": "restart-gateway",
|
|
89
|
-
"icon": "🔄",
|
|
90
|
-
"label": "重启 Gateway",
|
|
91
|
-
"type": "action",
|
|
92
|
-
"command": {
|
|
93
|
-
"exec": "openclaw gateway restart",
|
|
94
|
-
"display": "openclaw gateway restart"
|
|
95
|
-
},
|
|
96
|
-
"teach": {
|
|
97
|
-
"title": "重启 Gateway 使配置生效",
|
|
98
|
-
"description": "重启 OpenClaw 核心服务,让之前的所有配置变更生效。\n\nGateway 是 OpenClaw 的「大脑」,它在启动时读取配置。\n我们刚才修改了配置(安装并启用了插件),\n所以需要重启让 Gateway 加载新配置。\n\n类比:改了系统设置后,有时候需要重启电脑才生效。",
|
|
99
|
-
"look_for": "重启后 Gateway 运行中,控制台地址 http://127.0.0.1:18789",
|
|
100
|
-
"tip": "⏳ 重启期间短暂不可用,请等待几秒钟再继续。"
|
|
101
|
-
}
|
|
102
|
-
},
|
|
87
|
+
{ "$ref": "../../commons/gateway.steps.json#restart" },
|
|
103
88
|
{
|
|
104
89
|
"id": "verify",
|
|
105
90
|
"icon": "✅",
|
|
@@ -115,6 +100,18 @@
|
|
|
115
100
|
"look_for": "openclaw-weixin 的状态为 connected ✅",
|
|
116
101
|
"tip": "🎉 如果看到 connected,恭喜完成微信接入!"
|
|
117
102
|
}
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
"id": "multi-account",
|
|
106
|
+
"icon": "👥",
|
|
107
|
+
"label": "多账号绑定(进阶)",
|
|
108
|
+
"type": "submenu",
|
|
109
|
+
"submenu": "./bind.config.json",
|
|
110
|
+
"teach": {
|
|
111
|
+
"title": "微信多账号绑定",
|
|
112
|
+
"description": "如果你需要让多个微信账号分别对接不同的 Agent,\n可以进入这个子向导完成配置。\n\n适用场景:\n - 一个 Agent 负责课堂答疑(账号A)\n - 另一个 Agent 负责生活助手(账号B)\n\n普通接入不需要这一步,完成步骤 1-7 即可。",
|
|
113
|
+
"tip": "💡 按 Enter 进入子向导,按 q 随时返回此页面。"
|
|
114
|
+
}
|
|
118
115
|
}
|
|
119
116
|
]
|
|
120
117
|
}
|
package/wizards/index.js
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* wizards/index.js
|
|
5
|
+
* ────────────────────────────────────────────────────────
|
|
6
|
+
* 所有向导的统一入口
|
|
7
|
+
*
|
|
8
|
+
* 新增向导只需:
|
|
9
|
+
* 1. 在 configs/ 下建目录,写 index.config.json
|
|
10
|
+
* 2. 在这里 `startWizard(path.join(...))` 注册一个入口函数
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
const path = require('path');
|
|
14
|
+
const { WizardRunner } = require('./runner/wizard-runner');
|
|
15
|
+
const { loadConfig } = require('./runner/config-loader');
|
|
16
|
+
|
|
17
|
+
function startWizard(configPath) {
|
|
18
|
+
const config = loadConfig(configPath);
|
|
19
|
+
new WizardRunner(config).run();
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// ── 各向导入口 ────────────────────────────────────────────────
|
|
23
|
+
|
|
24
|
+
function runWeixin() {
|
|
25
|
+
startWizard(path.join(__dirname, 'configs/weixin/index.config.json'));
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// 未来扩展示例:
|
|
29
|
+
// function runGateway() { startWizard(path.join(__dirname, 'configs/gateway/index.config.json')); }
|
|
30
|
+
// function runAgent() { startWizard(path.join(__dirname, 'configs/agent/index.config.json')); }
|
|
31
|
+
|
|
32
|
+
module.exports = { runWeixin };
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* config-loader.js
|
|
5
|
+
* ──────────────────────────────────────────────────────────
|
|
6
|
+
* Wizard config 加载器
|
|
7
|
+
*
|
|
8
|
+
* 功能:
|
|
9
|
+
* 1. 加载 JSON config 文件并注入 _dir(用于解析相对路径)
|
|
10
|
+
* 2. 展开 steps 中的 $ref 引用(指向公共步骤片段)
|
|
11
|
+
*
|
|
12
|
+
* $ref 格式:
|
|
13
|
+
* { "$ref": "../../commons/gateway.steps.json#restart" }
|
|
14
|
+
* ↑ 文件路径(相对于当前 config 所在目录) ↑ 步骤 key
|
|
15
|
+
*
|
|
16
|
+
* 如果不加 #key,则将整个文件的值视为步骤(需是数组或对象)。
|
|
17
|
+
*
|
|
18
|
+
* 公共步骤文件格式(commons/xxx.steps.json):
|
|
19
|
+
* {
|
|
20
|
+
* "restart": { ...step对象... },
|
|
21
|
+
* "check-health": { ...step对象... }
|
|
22
|
+
* }
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
const path = require('path');
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* 加载并解析 wizard config
|
|
29
|
+
* @param {string} configPath - 绝对路径
|
|
30
|
+
* @returns {Object} 解析后的 config(steps 已展开 $ref)
|
|
31
|
+
*/
|
|
32
|
+
function loadConfig(configPath) {
|
|
33
|
+
// delete require cache 确保每次都重新读取(方便开发调试)
|
|
34
|
+
delete require.cache[require.resolve(configPath)];
|
|
35
|
+
|
|
36
|
+
const raw = require(configPath);
|
|
37
|
+
const baseDir = path.dirname(configPath);
|
|
38
|
+
|
|
39
|
+
const steps = _expandSteps(raw.steps || [], baseDir);
|
|
40
|
+
|
|
41
|
+
return { ...raw, steps, _dir: baseDir };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* 展开 steps 数组中的 $ref 项
|
|
46
|
+
*/
|
|
47
|
+
function _expandSteps(steps, baseDir) {
|
|
48
|
+
const result = [];
|
|
49
|
+
|
|
50
|
+
for (const step of steps) {
|
|
51
|
+
if (step.$ref) {
|
|
52
|
+
const expanded = _resolveRef(step.$ref, baseDir);
|
|
53
|
+
if (Array.isArray(expanded)) {
|
|
54
|
+
result.push(...expanded);
|
|
55
|
+
} else {
|
|
56
|
+
result.push(expanded);
|
|
57
|
+
}
|
|
58
|
+
} else {
|
|
59
|
+
result.push(step);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return result;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* 解析单个 $ref 字符串
|
|
68
|
+
* @param {string} ref - "path/to/file.json#key"
|
|
69
|
+
* @param {string} baseDir - 当前 config 所在目录
|
|
70
|
+
*/
|
|
71
|
+
function _resolveRef(ref, baseDir) {
|
|
72
|
+
const [filePart, key] = ref.split('#');
|
|
73
|
+
const absPath = path.resolve(baseDir, filePart);
|
|
74
|
+
|
|
75
|
+
// 每次都清缓存,保持公共步骤文件可热更新
|
|
76
|
+
delete require.cache[require.resolve(absPath)];
|
|
77
|
+
const file = require(absPath);
|
|
78
|
+
|
|
79
|
+
if (key) {
|
|
80
|
+
if (!file[key]) {
|
|
81
|
+
throw new Error(`[config-loader] $ref "${ref}" 中找不到 key "${key}"`);
|
|
82
|
+
}
|
|
83
|
+
return file[key];
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return file;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
module.exports = { loadConfig };
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* input-prompt.js
|
|
5
|
+
* ─────────────────────────────────────────────────────
|
|
6
|
+
* 通用「参数填写」交互模块
|
|
7
|
+
*
|
|
8
|
+
* 用于 input 类型的步骤:执行命令前需要用户填写动态参数。
|
|
9
|
+
* 暂时退出 raw mode → 用 readline 逐个收集参数 → 恢复 raw mode。
|
|
10
|
+
*
|
|
11
|
+
* 可复用于任何需要在 TUI 会话中收集用户输入的场景。
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
const readline = require('readline');
|
|
15
|
+
const chalk = require('chalk');
|
|
16
|
+
const { hr, W } = require('./render');
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* 收集一组用户输入参数
|
|
20
|
+
*
|
|
21
|
+
* @param {Array<InputDef>} inputs
|
|
22
|
+
* InputDef: { key, label, hint?, placeholder? }
|
|
23
|
+
* @returns {Promise<Object>} - { [key]: value }
|
|
24
|
+
*/
|
|
25
|
+
async function collectInputs(inputs) {
|
|
26
|
+
// 退出 raw mode,使 readline 能正常接收输入
|
|
27
|
+
process.stdin.setRawMode(false);
|
|
28
|
+
|
|
29
|
+
const values = {};
|
|
30
|
+
|
|
31
|
+
for (const input of inputs) {
|
|
32
|
+
values[input.key] = await _ask(input);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// 恢复 raw mode
|
|
36
|
+
// readline.close() 内部会 pause stdin,必须显式 resume 否则后续监听失效
|
|
37
|
+
process.stdin.setRawMode(true);
|
|
38
|
+
process.stdin.resume();
|
|
39
|
+
|
|
40
|
+
return values;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* 显示单个参数的输入提示并等待用户输入
|
|
45
|
+
*/
|
|
46
|
+
function _ask(input) {
|
|
47
|
+
return new Promise(resolve => {
|
|
48
|
+
const rl = readline.createInterface({
|
|
49
|
+
input: process.stdin,
|
|
50
|
+
output: process.stdout,
|
|
51
|
+
terminal: true,
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
// Ctrl+C 优雅处理:恢复 raw mode 后退出
|
|
55
|
+
rl.on('SIGINT', () => {
|
|
56
|
+
rl.close();
|
|
57
|
+
// 恢复 raw mode,然后退出程序
|
|
58
|
+
try { process.stdin.setRawMode(true); } catch (_) {}
|
|
59
|
+
console.log('');
|
|
60
|
+
console.log(' ' + chalk.cyan('已取消。再见!👋'));
|
|
61
|
+
console.log('');
|
|
62
|
+
process.exit(0);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
// 显示 hint
|
|
66
|
+
if (input.hint) {
|
|
67
|
+
console.log(' ' + chalk.dim('💡 ' + input.hint));
|
|
68
|
+
}
|
|
69
|
+
if (input.placeholder) {
|
|
70
|
+
console.log(' ' + chalk.dim(' 例如:' + input.placeholder));
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
rl.question(
|
|
74
|
+
' ' + chalk.bold.white(input.label) + chalk.dim(':'),
|
|
75
|
+
answer => {
|
|
76
|
+
rl.close();
|
|
77
|
+
resolve(answer.trim() || '');
|
|
78
|
+
}
|
|
79
|
+
);
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* 用参数值替换命令模板中的 {{key}} 占位符
|
|
85
|
+
*
|
|
86
|
+
* @param {string} template - 含 {{key}} 的模板字符串
|
|
87
|
+
* @param {Object} values - { key: value }
|
|
88
|
+
* @returns {string}
|
|
89
|
+
*/
|
|
90
|
+
function fillTemplate(template, values) {
|
|
91
|
+
return template.replace(/\{\{(\w+)\}\}/g, (_, key) => {
|
|
92
|
+
return values[key] !== undefined ? values[key] : `<${key}>`;
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
module.exports = { collectInputs, fillTemplate };
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* render.js
|
|
5
|
+
* ─────────────────────────────────────────
|
|
6
|
+
* 通用渲染工具
|
|
7
|
+
*
|
|
8
|
+
* W 改为动态读取终端宽度(getW),每次 render 时实时计算,
|
|
9
|
+
* 支持终端窗口 resize 后自动适配。
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const chalk = require('chalk');
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* 获取当前终端宽度(限制在 50~80 之间)
|
|
16
|
+
*/
|
|
17
|
+
function getW() {
|
|
18
|
+
const cols = process.stdout.columns;
|
|
19
|
+
if (!cols || cols < 50) return 66; // 无法获取或过窄时使用默认值
|
|
20
|
+
return Math.min(cols - 2, 80);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function hr(char = '─', w) {
|
|
24
|
+
return char.repeat(w || getW());
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function cls() {
|
|
28
|
+
process.stdout.write('\x1b[2J\x1b[H');
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* 多行文本缩进
|
|
33
|
+
*/
|
|
34
|
+
function indent(text, prefix = ' ') {
|
|
35
|
+
return text.split('\n').map(l => prefix + l).join('\n');
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* 步骤状态标签
|
|
40
|
+
*/
|
|
41
|
+
const BADGE = {
|
|
42
|
+
check: () => chalk.bgCyan.black(' 查看 '),
|
|
43
|
+
pending: () => chalk.bgGray.white(' 未执行 '),
|
|
44
|
+
done: () => chalk.bgGreen.black(' ✓ 完成 '),
|
|
45
|
+
error: () => chalk.bgRed.white(' ✗ 失败 '),
|
|
46
|
+
submenu: () => chalk.bgMagenta.white(' ▸ 子向导 '),
|
|
47
|
+
input: () => chalk.bgYellow.black(' 填参数 '),
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* 渲染命令行(自动折行)
|
|
52
|
+
*/
|
|
53
|
+
function renderCommand(display) {
|
|
54
|
+
if (!display) return;
|
|
55
|
+
const W = getW();
|
|
56
|
+
const maxLen = W - 10;
|
|
57
|
+
|
|
58
|
+
if (display.length <= maxLen) {
|
|
59
|
+
console.log(' ' + chalk.bgBlack.cyan(' $ ') + chalk.bgBlack.white(' ' + display + ' '));
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// 按空格拆词后贪心折行
|
|
64
|
+
const words = display.split(' ');
|
|
65
|
+
const lines = [];
|
|
66
|
+
let cur = '';
|
|
67
|
+
|
|
68
|
+
for (const word of words) {
|
|
69
|
+
const next = cur ? cur + ' ' + word : word;
|
|
70
|
+
if (next.length > maxLen && cur) {
|
|
71
|
+
lines.push(cur);
|
|
72
|
+
cur = word;
|
|
73
|
+
} else {
|
|
74
|
+
cur = next;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
if (cur) lines.push(cur);
|
|
78
|
+
|
|
79
|
+
lines.forEach((line, i) => {
|
|
80
|
+
const prefix = i === 0
|
|
81
|
+
? chalk.bgBlack.cyan(' $ ')
|
|
82
|
+
: chalk.bgBlack.dim(' ');
|
|
83
|
+
console.log(' ' + prefix + chalk.bgBlack.white(' ' + line + ' '));
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
module.exports = { getW, hr, cls, indent, BADGE, renderCommand };
|