create-napukettoqq 0.1.0 → 0.2.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/dist/index.mjs +185 -211
- package/dist/index.mjs.map +1 -1
- package/package.json +5 -3
- package/templates/napuketto.toml.tmpl +78 -0
- package/templates/package.json.tmpl +13 -0
package/dist/index.mjs
CHANGED
|
@@ -1,19 +1,23 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { spawn } from "node:child_process";
|
|
3
|
-
import process from "node:process";
|
|
4
|
-
import { createInterface } from "node:readline/promises";
|
|
5
3
|
import { readFileSync } from "node:fs";
|
|
6
|
-
import
|
|
4
|
+
import path, { relative } from "node:path";
|
|
5
|
+
import process from "node:process";
|
|
6
|
+
import { cancel, confirm, intro, isCancel, log, outro, spinner, text } from "@clack/prompts";
|
|
7
|
+
import { mkdir, readFile, readdir, rm, writeFile } from "node:fs/promises";
|
|
7
8
|
import { homedir } from "node:os";
|
|
8
|
-
import path from "node:path";
|
|
9
9
|
//#region src/scaffold.ts
|
|
10
10
|
/**
|
|
11
|
-
* create-napukettoqq
|
|
11
|
+
* create-napukettoqq 脚手架核心:模板渲染 + 目标目录创建 + 文件写入。
|
|
12
12
|
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
13
|
+
* 模板为独立文件(templates/ 目录,运行时读取),不硬编码在源码里;
|
|
14
|
+
* 需要插值的占位符用 {{key}} 标记(见 renderTemplate)。
|
|
15
|
+
* 依赖极轻(node:fs/promises / node:os / node:path / node:process)。
|
|
16
|
+
* 生成的用户项目依赖发布版的 @napuketto/cli,用户 install 后经 cli → loader
|
|
17
|
+
* 自建宿主启动。
|
|
16
18
|
*/
|
|
19
|
+
/** 模板目录(相对本文件:src 与 dist 都在包根下,../templates 均指向包根/templates)。 */
|
|
20
|
+
const TEMPLATES = new URL("../templates/", import.meta.url);
|
|
17
21
|
/**
|
|
18
22
|
* 生成的用户项目所依赖的 @napuketto/cli 版本范围。
|
|
19
23
|
* 运行时从脚手架自身 package.json 读取(dependencies["@napuketto/cli"])——
|
|
@@ -27,6 +31,8 @@ function cliVersionRange() {
|
|
|
27
31
|
} catch {}
|
|
28
32
|
return "^0.0.1";
|
|
29
33
|
}
|
|
34
|
+
/** 默认部署文件夹名(用户交互回车缺省值)。 */
|
|
35
|
+
const DEFAULT_PROJECT_NAME = "NapukettoQQ";
|
|
30
36
|
/**
|
|
31
37
|
* 由 npm_config_user_agent 检测调用方包管理器(yarn create / npm create /
|
|
32
38
|
* pnpm create 都会注入该变量)。兜底 pnpm(项目生态,或直接 node 运行)。
|
|
@@ -54,176 +60,65 @@ function validateProjectName(raw) {
|
|
|
54
60
|
function derivePackageName(dirName) {
|
|
55
61
|
return dirName.toLowerCase().replace(/\s+/g, "-");
|
|
56
62
|
}
|
|
57
|
-
/**
|
|
58
|
-
function
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
63
|
+
/** 解析目标目录绝对路径(校验名称后,基于 cwd)。 */
|
|
64
|
+
function resolveTargetDir(dirName) {
|
|
65
|
+
return path.resolve(process.cwd(), validateProjectName(dirName));
|
|
66
|
+
}
|
|
67
|
+
/** 检测目标目录状态(供交互层决定是否询问清空)。 */
|
|
68
|
+
async function checkDirStatus(dir) {
|
|
69
|
+
let entries = [];
|
|
70
|
+
try {
|
|
71
|
+
entries = await readdir(dir);
|
|
72
|
+
} catch {
|
|
73
|
+
return "missing";
|
|
74
|
+
}
|
|
75
|
+
return entries.length === 0 ? "empty" : "nonempty";
|
|
76
|
+
}
|
|
77
|
+
/** 清空目录内容(保留目录本身)。 */
|
|
78
|
+
async function emptyDir(dir) {
|
|
79
|
+
for (const entry of await readdir(dir)) await rm(path.join(dir, entry), {
|
|
80
|
+
recursive: true,
|
|
81
|
+
force: true
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
/** 读取模板文件(templates/ 目录,UTF-8)。 */
|
|
85
|
+
function readTemplate(file) {
|
|
86
|
+
return readFile(new URL(file, TEMPLATES), "utf8");
|
|
69
87
|
}
|
|
70
88
|
/**
|
|
71
|
-
*
|
|
72
|
-
*
|
|
89
|
+
* 占位符渲染:把 {{key}} 替换为 vars[key]。
|
|
90
|
+
* 模板中出现未提供的占位符 → 抛错(防漏插值写出半成品)。
|
|
73
91
|
*/
|
|
74
|
-
function
|
|
75
|
-
return
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
# 优先级:--data-dir <dir> > NAPKETTO_DATA 环境变量 > ~/.napuketto
|
|
81
|
-
# 配置文件路径解析:NAPKETTO_CONFIG > 项目根探测 > cwd > 数据根兜底。
|
|
82
|
-
#
|
|
83
|
-
# 单账号启动每次读取本文件;修改后重启生效。
|
|
84
|
-
|
|
85
|
-
# ------------------------------------------------------------
|
|
86
|
-
# 基础配置
|
|
87
|
-
# ------------------------------------------------------------
|
|
88
|
-
|
|
89
|
-
# 数据根目录:账号目录/日志/缓存/QQ 数据存放位置(绝对路径)
|
|
90
|
-
dataDir = ${JSON.stringify(path.join(homedir(), ".napuketto"))}
|
|
91
|
-
|
|
92
|
-
# 多账号(supervisor)模式下,账号进程崩溃是否自动重启
|
|
93
|
-
autoRestart = true
|
|
94
|
-
|
|
95
|
-
# 崩溃后自动重启的延迟(毫秒)
|
|
96
|
-
restartDelayMs = 2000
|
|
97
|
-
|
|
98
|
-
# 多账号列表(留空 = 单账号模式:启动后自动快速登录/扫码)
|
|
99
|
-
# [[accounts]]
|
|
100
|
-
# qq = "123456" # QQ 号(必填)
|
|
101
|
-
# enabled = true # 是否启用(可选,缺省 true)
|
|
102
|
-
|
|
103
|
-
# ------------------------------------------------------------
|
|
104
|
-
# OneBot 11 协议段
|
|
105
|
-
# ------------------------------------------------------------
|
|
106
|
-
[onebot11]
|
|
107
|
-
|
|
108
|
-
# 鉴权 token(全局默认):HTTP/WS 连接必须携带。
|
|
109
|
-
# 校验方式:Authorization: Bearer <token> / 裸 token / URL 参数 ?access_token=<token>
|
|
110
|
-
# 每个网络实例可单独覆盖(见下文各实例的 token 字段)。
|
|
111
|
-
# 留空 = 不鉴权(仅本机调试推荐)。
|
|
112
|
-
# token = ""
|
|
113
|
-
|
|
114
|
-
# 心跳 meta 事件间隔(毫秒);0 = 关闭心跳事件
|
|
115
|
-
heartbeatInterval = 3000
|
|
116
|
-
|
|
117
|
-
# 是否上报机器人自己发的消息(OB11 规范默认不上报)
|
|
118
|
-
reportSelfMessage = false
|
|
119
|
-
|
|
120
|
-
# 消息内容格式:array = 消息段数组(标准 OneBot 11),string = CQ 码字符串
|
|
121
|
-
messagePostFormat = "array"
|
|
122
|
-
|
|
123
|
-
# ---- HTTP 反向服务器(接收第三方 POST 指令),可配置多个 ----
|
|
124
|
-
[[onebot11.httpServers]]
|
|
125
|
-
enabled = true
|
|
126
|
-
host = "127.0.0.1" # 监听地址;0.0.0.0 = 监听所有网卡(局域网可连)
|
|
127
|
-
port = 3000
|
|
128
|
-
# token = "abc" # 可选:覆盖全局 token
|
|
129
|
-
|
|
130
|
-
# ---- HTTP 正向上报(Webhook:把事件 POST 到第三方地址),可配置多个 ----
|
|
131
|
-
# [[onebot11.httpPostUrls]]
|
|
132
|
-
# enabled = true
|
|
133
|
-
# url = "http://127.0.0.1:8080/onebot" # 第三方接收地址
|
|
134
|
-
# token = "abc" # 可选:覆盖全局 token
|
|
135
|
-
# timeoutMs = 5000 # 可选:上报超时(毫秒),缺省不超时
|
|
136
|
-
|
|
137
|
-
# ---- WS 反向服务器(外部 WS 客户端主动连入),可配置多个 ----
|
|
138
|
-
[[onebot11.wsServers]]
|
|
139
|
-
enabled = true
|
|
140
|
-
host = "127.0.0.1"
|
|
141
|
-
port = 3001
|
|
142
|
-
# token = "abc" # 可选:覆盖全局 token
|
|
143
|
-
# heartbeatInterval = 30000 # 可选:WS ping 间隔(毫秒)
|
|
144
|
-
|
|
145
|
-
# ---- WS 正向客户端(主动连接外部 WS,双向),可配置多个 ----
|
|
146
|
-
# [[onebot11.wsReverseUrls]]
|
|
147
|
-
# enabled = true
|
|
148
|
-
# url = "ws://127.0.0.1:8081/ws" # 第三方 WS 地址(ws:// 或 wss://)
|
|
149
|
-
# token = "abc" # 可选:覆盖全局 token
|
|
150
|
-
# reconnectDelayMs = 5000 # 可选:断线重连延迟(毫秒)
|
|
151
|
-
# maxReconnectAttempts = 10 # 可选:最大重连次数(缺省无限)
|
|
152
|
-
# rejectUnauthorized = true # 可选:wss:// 是否校验证书(自签证书设 false)
|
|
153
|
-
# heartbeatInterval = 30000 # 可选:WS ping 间隔(毫秒)
|
|
154
|
-
`;
|
|
155
|
-
}
|
|
156
|
-
/** 生成 readme.md(用户项目使用说明,包管理器无关,以 pnpm 为例)。 */
|
|
157
|
-
function readmeTemplate(dirName) {
|
|
158
|
-
return `# ${dirName}
|
|
159
|
-
|
|
160
|
-
基于 **NapukettoQQ** 的 QQ 机器人项目(OneBot 11 协议),由 \`create-napukettoqq\` 生成。
|
|
161
|
-
|
|
162
|
-
## 快速开始
|
|
163
|
-
|
|
164
|
-
\`\`\`bash
|
|
165
|
-
pnpm install
|
|
166
|
-
pnpm start -q 123456 # 换成你的 QQ 号
|
|
167
|
-
\`\`\`
|
|
168
|
-
|
|
169
|
-
> 包管理器不限:\`pnpm install\` / \`npm install\` / \`yarn install\`,启动同理
|
|
170
|
-
> (\`pnpm start\` / \`npm start\` / \`yarn start\`)。以下命令均以 pnpm 为例。
|
|
171
|
-
|
|
172
|
-
启动后按终端提示快速登录或扫码;登录成功后 OneBot 11 接口生效
|
|
173
|
-
(默认 HTTP \`127.0.0.1:3000\` + WS \`127.0.0.1:3001\`,见 \`napuketto.toml\`)。
|
|
174
|
-
|
|
175
|
-
## 前置要求
|
|
176
|
-
|
|
177
|
-
- 已安装 **QQ NT**(Windows,机器人所需的 \`wrapper.node\` 来自 QQ 安装目录,
|
|
178
|
-
未自动探测到时用 \`pnpm start --qq-path <QQ安装目录>\` 指定)
|
|
179
|
-
- Node.js 20+ 与任一包管理器(pnpm / npm / yarn)
|
|
180
|
-
|
|
181
|
-
## 常用命令
|
|
182
|
-
|
|
183
|
-
| 命令 | 说明 |
|
|
184
|
-
|---|---|
|
|
185
|
-
| \`pnpm start -q <QQ号>\` | 单账号启动(走自建宿主) |
|
|
186
|
-
| \`pnpm napuketto config init\` | 重新生成默认配置 |
|
|
187
|
-
| \`pnpm napuketto supervisor\` | 多账号模式(按 \`napuketto.toml\` 的 \`accounts\`) |
|
|
188
|
-
|
|
189
|
-
> npm 用户把 \`pnpm napuketto\` 换成 \`npx napuketto\`;yarn 用户换成 \`yarn napuketto\`。
|
|
190
|
-
|
|
191
|
-
## 目录
|
|
192
|
-
|
|
193
|
-
- \`napuketto.toml\` — 全局配置(QQ 号、OneBot 端口、token 等,修改后重启生效)
|
|
194
|
-
- QQ 数据(账号目录/日志/缓存)默认在 \`~/.napuketto/\`
|
|
195
|
-
|
|
196
|
-
## 升级
|
|
197
|
-
|
|
198
|
-
\`\`\`bash
|
|
199
|
-
pnpm update @napuketto/cli # npm: npm update @napuketto/cli;yarn: yarn up @napuketto/cli
|
|
200
|
-
\`\`\`
|
|
201
|
-
`;
|
|
92
|
+
function renderTemplate(content, vars) {
|
|
93
|
+
return content.replace(/\{\{(\w+)\}\}/g, (match, key) => {
|
|
94
|
+
const value = vars[key];
|
|
95
|
+
if (value === void 0) throw new Error(`模板占位符未提供:{{${key}}}(模板 ${match} 无法渲染)`);
|
|
96
|
+
return value;
|
|
97
|
+
});
|
|
202
98
|
}
|
|
203
|
-
/** 生成 .gitignore(用户项目)。 */
|
|
204
|
-
const GITIGNORE = `node_modules/
|
|
205
|
-
dist/
|
|
206
|
-
*.log
|
|
207
|
-
napuketto-data/
|
|
208
|
-
`;
|
|
209
99
|
/**
|
|
210
100
|
* 生成 NapukettoQQ 项目骨架到当前目录下的 <dirName>/。
|
|
211
|
-
*
|
|
101
|
+
* 目标目录已存在且非空:overwrite=false → 抛错;overwrite=true → 清空后生成。
|
|
102
|
+
* 模板与写入目标一一对应(同文件名)。
|
|
212
103
|
*/
|
|
213
|
-
async function scaffoldProject(dirName) {
|
|
104
|
+
async function scaffoldProject(dirName, overwrite = false) {
|
|
214
105
|
const name = validateProjectName(dirName);
|
|
215
106
|
const packageName = derivePackageName(name);
|
|
216
|
-
const dir =
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
107
|
+
const dir = resolveTargetDir(name);
|
|
108
|
+
const status = await checkDirStatus(dir);
|
|
109
|
+
if (status === "nonempty" && !overwrite) throw new Error(`目录已存在且非空:${dir}\n请换一个文件夹名,或先清空该目录。`);
|
|
110
|
+
if (status === "nonempty") await emptyDir(dir);
|
|
111
|
+
const vars = {
|
|
112
|
+
packageName,
|
|
113
|
+
cliVersion: cliVersionRange(),
|
|
114
|
+
dataDir: JSON.stringify(path.join(homedir(), ".napuketto"))
|
|
115
|
+
};
|
|
116
|
+
const files = ["package.json", "napuketto.toml"];
|
|
222
117
|
await mkdir(dir, { recursive: true });
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
118
|
+
for (const file of files) {
|
|
119
|
+
const rendered = renderTemplate(await readTemplate(`${file}.tmpl`), vars);
|
|
120
|
+
await writeFile(path.join(dir, file), rendered, "utf8");
|
|
121
|
+
}
|
|
227
122
|
return {
|
|
228
123
|
dir,
|
|
229
124
|
packageName
|
|
@@ -232,40 +127,97 @@ async function scaffoldProject(dirName) {
|
|
|
232
127
|
//#endregion
|
|
233
128
|
//#region src/index.ts
|
|
234
129
|
/**
|
|
235
|
-
* create-napukettoqq 入口(2026-08-07
|
|
130
|
+
* create-napukettoqq 入口(2026-08-07 美化 v3,交互层用 @clack/prompts——
|
|
131
|
+
* 现代脚手架交互标准,create-vue / create-turbo 同款)。
|
|
236
132
|
*
|
|
237
133
|
* 用法:
|
|
238
|
-
* create-napukettoqq # 交互问部署文件夹名(默认 NapukettoQQ)
|
|
239
|
-
* create-napukettoqq my-bot #
|
|
134
|
+
* create-napukettoqq # intro + 交互问部署文件夹名(默认 NapukettoQQ)
|
|
135
|
+
* create-napukettoqq my-bot # 位置参数指定文件夹名,跳过命名交互
|
|
136
|
+
* create-napukettoqq my-bot -f # 目标目录非空时强制清空覆盖
|
|
137
|
+
* create-napukettoqq -y # 全默认(命名用默认值、启动询问用默认 Y)
|
|
138
|
+
* create-napukettoqq -h # 打印帮助
|
|
240
139
|
*
|
|
241
|
-
*
|
|
242
|
-
*
|
|
243
|
-
* 自动 install → 询问是否现在启动(默认 Y
|
|
140
|
+
* 流程:intro → 问文件夹名 → 目标目录准备(非空需确认清空)→ spinner 生成骨架
|
|
141
|
+
* (package.json / napuketto.toml / readme.md / .gitignore)→ 用调用方包管理器
|
|
142
|
+
* 自动 install → 询问是否现在启动(默认 Y)→ 前台运行 → outro 收尾。
|
|
244
143
|
*/
|
|
245
|
-
/**
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
144
|
+
/** 帮助文本(-h/--help)。 */
|
|
145
|
+
const HELP = ` 用法: create-napukettoqq [name] [选项]
|
|
146
|
+
|
|
147
|
+
选项:
|
|
148
|
+
-f, --forced 目标目录非空时强制清空并覆盖
|
|
149
|
+
-y, --yes 跳过所有交互提示(使用默认值)
|
|
150
|
+
-h, --help 显示本帮助
|
|
151
|
+
`;
|
|
152
|
+
/** 解析命令行参数(flag 仅三个,手写保持自包含,不引 yargs-parser)。 */
|
|
153
|
+
function parseArgs(argv) {
|
|
154
|
+
const opts = {
|
|
155
|
+
forced: false,
|
|
156
|
+
yes: false,
|
|
157
|
+
help: false
|
|
158
|
+
};
|
|
159
|
+
for (const arg of argv) if (arg === "-f" || arg === "--forced") opts.forced = true;
|
|
160
|
+
else if (arg === "-y" || arg === "--yes") opts.yes = true;
|
|
161
|
+
else if (arg === "-h" || arg === "--help") opts.help = true;
|
|
162
|
+
else if (arg.startsWith("-")) throw new Error(`未知参数:${arg}`);
|
|
163
|
+
else if (opts.name === void 0) opts.name = arg;
|
|
164
|
+
else throw new Error(`多余的位置参数:${arg}`);
|
|
165
|
+
return opts;
|
|
166
|
+
}
|
|
167
|
+
/** 读取脚手架自身版本号(intro 显示;dist 场景 import.meta.url 指向包内)。 */
|
|
168
|
+
function readVersion() {
|
|
251
169
|
try {
|
|
252
|
-
return (
|
|
253
|
-
}
|
|
254
|
-
|
|
170
|
+
return JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8")).version ?? "0.0.0";
|
|
171
|
+
} catch {
|
|
172
|
+
return "0.0.0";
|
|
255
173
|
}
|
|
256
174
|
}
|
|
257
|
-
/**
|
|
258
|
-
async function
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
175
|
+
/** 交互询问部署文件夹名(回车取默认值;-y 直接返回默认值)。 */
|
|
176
|
+
async function askProjectName(yes) {
|
|
177
|
+
if (yes) return DEFAULT_PROJECT_NAME;
|
|
178
|
+
const name = await text({
|
|
179
|
+
message: "部署文件夹名",
|
|
180
|
+
placeholder: DEFAULT_PROJECT_NAME,
|
|
181
|
+
initialValue: DEFAULT_PROJECT_NAME,
|
|
182
|
+
validate: (input) => {
|
|
183
|
+
try {
|
|
184
|
+
validateProjectName(input ?? "");
|
|
185
|
+
return;
|
|
186
|
+
} catch (err) {
|
|
187
|
+
return err instanceof Error ? err.message : String(err);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
262
190
|
});
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
} finally {
|
|
267
|
-
rl.close();
|
|
191
|
+
if (isCancel(name)) {
|
|
192
|
+
cancel("操作已取消");
|
|
193
|
+
process.exit(0);
|
|
268
194
|
}
|
|
195
|
+
return validateProjectName(name);
|
|
196
|
+
}
|
|
197
|
+
/** 询问是否移除现有文件并继续(目标目录非空时)。 */
|
|
198
|
+
async function confirmRemove(dirName) {
|
|
199
|
+
const ok = await confirm({
|
|
200
|
+
message: `目录 "${dirName}" 已存在且非空,移除现有文件并继续?`,
|
|
201
|
+
initialValue: false
|
|
202
|
+
});
|
|
203
|
+
if (isCancel(ok)) {
|
|
204
|
+
cancel("操作已取消");
|
|
205
|
+
process.exit(0);
|
|
206
|
+
}
|
|
207
|
+
return ok;
|
|
208
|
+
}
|
|
209
|
+
/** 询问是否现在启动(默认 Y;-y 直接返回 true)。 */
|
|
210
|
+
async function askStartNow(yes) {
|
|
211
|
+
if (yes) return true;
|
|
212
|
+
const ok = await confirm({
|
|
213
|
+
message: "是否现在启动?",
|
|
214
|
+
initialValue: true
|
|
215
|
+
});
|
|
216
|
+
if (isCancel(ok)) {
|
|
217
|
+
cancel("操作已取消");
|
|
218
|
+
process.exit(0);
|
|
219
|
+
}
|
|
220
|
+
return ok;
|
|
269
221
|
}
|
|
270
222
|
/** 前台执行命令(stdio 继承,用户可见进度/交互),返回退出码。 */
|
|
271
223
|
function runInteractive(bin, args, cwd) {
|
|
@@ -281,37 +233,59 @@ function runInteractive(bin, args, cwd) {
|
|
|
281
233
|
});
|
|
282
234
|
}
|
|
283
235
|
async function main() {
|
|
284
|
-
const
|
|
236
|
+
const opts = parseArgs(process.argv.slice(2));
|
|
237
|
+
if (opts.help) {
|
|
238
|
+
console.log(HELP);
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
intro(`create-napukettoqq v${readVersion()}`);
|
|
242
|
+
const dirName = opts.name ?? await askProjectName(opts.yes);
|
|
243
|
+
const targetDir = resolveTargetDir(dirName);
|
|
244
|
+
let overwrite = false;
|
|
245
|
+
if (await checkDirStatus(targetDir) === "nonempty") if (!opts.forced) {
|
|
246
|
+
log.warn(`目标目录 "${dirName}" 已存在且非空。`);
|
|
247
|
+
overwrite = await confirmRemove(dirName);
|
|
248
|
+
if (!overwrite) {
|
|
249
|
+
outro("已取消,未做任何修改。");
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
} else overwrite = true;
|
|
253
|
+
const s = spinner();
|
|
254
|
+
s.start(`正在生成项目骨架 ${dirName} ...`);
|
|
255
|
+
const result = await scaffoldProject(dirName, overwrite);
|
|
256
|
+
s.stop(`项目骨架已生成:${dirName}`);
|
|
257
|
+
log.success(`位置:${result.dir}`);
|
|
285
258
|
const pm = detectPackageManager();
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
console.log(` 包管理器:${pm}(自动检测)`);
|
|
290
|
-
console.log("========================================");
|
|
291
|
-
console.log("前置要求:本机已安装 QQ NT(wrapper.node 来自 QQ 安装目录)。");
|
|
292
|
-
console.log(`\n正在安装依赖(${pm} install)...`);
|
|
259
|
+
log.info(`包管理器:${pm}(自动检测)`);
|
|
260
|
+
log.info("前置要求:本机已安装 QQ NT(wrapper.node 来自 QQ 安装目录)。");
|
|
261
|
+
log.info(`正在安装依赖(${pm} install)...`);
|
|
293
262
|
const installCode = await runInteractive(pmBin(pm), ["install"], result.dir);
|
|
294
263
|
if (installCode !== 0) {
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
264
|
+
log.error(`依赖安装失败(退出码 ${installCode})。请手动执行:`);
|
|
265
|
+
log.step(`cd ${result.dir}`);
|
|
266
|
+
log.step(`${pm} install`);
|
|
267
|
+
outro("创建失败,请手动安装后重试。");
|
|
298
268
|
process.exitCode = 1;
|
|
299
269
|
return;
|
|
300
270
|
}
|
|
301
|
-
if (await askStartNow()) {
|
|
302
|
-
|
|
271
|
+
if (await askStartNow(opts.yes)) {
|
|
272
|
+
log.info("正在启动 NapukettoQQ(Ctrl+C 停止)...\n");
|
|
303
273
|
const startCode = await runInteractive(pmBin(pm), ["start"], result.dir);
|
|
304
274
|
if (startCode !== 0) process.exitCode = startCode;
|
|
305
275
|
} else {
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
276
|
+
log.message("你可以稍后这样启动:");
|
|
277
|
+
if (targetDir !== process.cwd()) {
|
|
278
|
+
const related = relative(process.cwd(), targetDir);
|
|
279
|
+
log.step(`cd ${related}`);
|
|
280
|
+
}
|
|
281
|
+
log.step(`${pm} start -q <你的QQ号>`);
|
|
282
|
+
log.message("配置说明见项目内 napuketto.toml。");
|
|
310
283
|
}
|
|
284
|
+
outro("完成。");
|
|
311
285
|
}
|
|
312
286
|
main().catch((err) => {
|
|
313
287
|
const message = err instanceof Error ? err.message : String(err);
|
|
314
|
-
|
|
288
|
+
log.error(`创建失败:${message}`);
|
|
315
289
|
process.exitCode = 1;
|
|
316
290
|
});
|
|
317
291
|
//#endregion
|
package/dist/index.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.mjs","names":[],"sources":["../src/scaffold.ts","../src/index.ts"],"sourcesContent":["/**\n * create-napukettoqq 脚手架核心:模板 + 目标目录创建 + 文件写入。\n *\n * 零依赖(仅 node:fs/promises / node:os / node:path / node:process),\n * 一次性工具保持自包含。生成的用户项目依赖发布版的 @napuketto/cli,\n * 用户 install 后经 cli → loader 自建宿主启动。\n */\n\nimport { readFileSync } from \"node:fs\";\nimport { mkdir, readdir, writeFile } from \"node:fs/promises\";\nimport { homedir } from \"node:os\";\nimport path from \"node:path\";\nimport process from \"node:process\";\n\n/**\n * 生成的用户项目所依赖的 @napuketto/cli 版本范围。\n * 运行时从脚手架自身 package.json 读取(dependencies[\"@napuketto/cli\"])——\n * 发版时 changesets 升版本 + pnpm publish 把 workspace:* 替换为实际版本,\n * 模板版本自动跟随,无需手动同步(比硬编码常量可靠)。\n */\nexport function cliVersionRange(): string {\n try {\n const ownPkg = JSON.parse(\n readFileSync(new URL(\"../package.json\", import.meta.url), \"utf8\"),\n ) as { dependencies?: Record<string, string> };\n const range = ownPkg.dependencies?.[\"@napuketto/cli\"];\n // workspace:* 仅在本地 monorepo 直跑脚手架时出现(发布后 pnpm 替换为实际版本),\n // 此时兜底默认范围,避免生成的用户项目 install 失败\n if (range !== undefined && range !== \"workspace:*\") {\n return range;\n }\n } catch {\n // package.json 缺失(异常环境)→ 兜底默认范围\n }\n return \"^0.0.1\";\n}\n\n/** 默认部署文件夹名(用户交互回车缺省值)。 */\nexport const DEFAULT_PROJECT_NAME = \"NapukettoQQ\";\n\n/** 用户可用的包管理器(npm_config_user_agent 检测)。 */\nexport type PackageManager = \"pnpm\" | \"yarn\" | \"npm\";\n\n/**\n * 由 npm_config_user_agent 检测调用方包管理器(yarn create / npm create /\n * pnpm create 都会注入该变量)。兜底 pnpm(项目生态,或直接 node 运行)。\n */\nexport function detectPackageManager(): PackageManager {\n // noPropertyAccessFromIndexSignature:process.env 是索引签名,必须括号访问\n const ua = process.env[\"npm_config_user_agent\"] ?? \"\";\n if (ua.startsWith(\"yarn/\")) {\n return \"yarn\";\n }\n if (ua.startsWith(\"npm/\")) {\n return \"npm\";\n }\n return \"pnpm\";\n}\n\n/** Windows 下包管理器 bin 需带 .cmd 后缀(CreateProcess 不认无扩展名 shim)。 */\nexport function pmBin(pm: PackageManager): string {\n return process.platform === \"win32\" ? `${pm}.cmd` : pm;\n}\n\n/** Windows 路径非法字符(项目文件夹名校验用)。 */\nconst ILLEGAL_NAME = /[\\\\/:*?\"<>|]/;\n\n/** 脚手架生成结果。 */\nexport interface ScaffoldResult {\n /** 目标目录绝对路径。 */\n dir: string;\n /** 派生出的 npm 包名(小写、空格 → -)。 */\n packageName: string;\n}\n\n/** 校验文件夹名:非空、非 . / ..、不含 Windows 非法字符。 */\nexport function validateProjectName(raw: string): string {\n const name = raw.trim();\n if (name === \"\" || name === \".\" || name === \"..\") {\n throw new Error(\"文件夹名不能为空或为 . / ..\");\n }\n if (ILLEGAL_NAME.test(name)) {\n throw new Error(`文件夹名含非法字符:${name}(不允许 \\\\ / : * ? \" < > |)`);\n }\n return name;\n}\n\n/** 由文件夹名派生 npm 包名(小写、空格 → -)。 */\nexport function derivePackageName(dirName: string): string {\n return dirName.toLowerCase().replace(/\\s+/g, \"-\");\n}\n\n/** 生成 package.json(用户项目,JSON.stringify 保证合法)。 */\nfunction packageJsonTemplate(packageName: string): string {\n const pkg = {\n name: packageName,\n version: \"0.0.1\",\n private: true,\n type: \"module\",\n description: \"NapukettoQQ 机器人项目(OneBot 11)\",\n scripts: {\n start: \"napuketto\",\n },\n dependencies: {\n \"@napuketto/cli\": cliVersionRange(),\n },\n };\n return `${JSON.stringify(pkg, null, 4)}\\n`;\n}\n\n/**\n * 生成 napuketto.toml:与 cli configTemplate(apps/cli/src/config-cmds.ts)同款内容,\n * dataDir 按当前用户主目录插值(JSON.stringify 转义反斜杠 → TOML 基本字符串合法写法)。\n */\nfunction tomlTemplate(): string {\n const dataDirLiteral = JSON.stringify(path.join(homedir(), \".napuketto\"));\n return `# ============================================================\n# NapukettoQQ 全局配置文件(由 create-napukettoqq 生成)\n# ============================================================\n# 本文件与项目 napuketto.toml.example 内容一致(参考模板)。\n# 数据(账号目录/日志/缓存/QQ 数据)不在此文件,按数据根组织:\n# 优先级:--data-dir <dir> > NAPKETTO_DATA 环境变量 > ~/.napuketto\n# 配置文件路径解析:NAPKETTO_CONFIG > 项目根探测 > cwd > 数据根兜底。\n#\n# 单账号启动每次读取本文件;修改后重启生效。\n\n# ------------------------------------------------------------\n# 基础配置\n# ------------------------------------------------------------\n\n# 数据根目录:账号目录/日志/缓存/QQ 数据存放位置(绝对路径)\ndataDir = ${dataDirLiteral}\n\n# 多账号(supervisor)模式下,账号进程崩溃是否自动重启\nautoRestart = true\n\n# 崩溃后自动重启的延迟(毫秒)\nrestartDelayMs = 2000\n\n# 多账号列表(留空 = 单账号模式:启动后自动快速登录/扫码)\n# [[accounts]]\n# qq = \"123456\" # QQ 号(必填)\n# enabled = true # 是否启用(可选,缺省 true)\n\n# ------------------------------------------------------------\n# OneBot 11 协议段\n# ------------------------------------------------------------\n[onebot11]\n\n# 鉴权 token(全局默认):HTTP/WS 连接必须携带。\n# 校验方式:Authorization: Bearer <token> / 裸 token / URL 参数 ?access_token=<token>\n# 每个网络实例可单独覆盖(见下文各实例的 token 字段)。\n# 留空 = 不鉴权(仅本机调试推荐)。\n# token = \"\"\n\n# 心跳 meta 事件间隔(毫秒);0 = 关闭心跳事件\nheartbeatInterval = 3000\n\n# 是否上报机器人自己发的消息(OB11 规范默认不上报)\nreportSelfMessage = false\n\n# 消息内容格式:array = 消息段数组(标准 OneBot 11),string = CQ 码字符串\nmessagePostFormat = \"array\"\n\n# ---- HTTP 反向服务器(接收第三方 POST 指令),可配置多个 ----\n[[onebot11.httpServers]]\nenabled = true\nhost = \"127.0.0.1\" # 监听地址;0.0.0.0 = 监听所有网卡(局域网可连)\nport = 3000\n# token = \"abc\" # 可选:覆盖全局 token\n\n# ---- HTTP 正向上报(Webhook:把事件 POST 到第三方地址),可配置多个 ----\n# [[onebot11.httpPostUrls]]\n# enabled = true\n# url = \"http://127.0.0.1:8080/onebot\" # 第三方接收地址\n# token = \"abc\" # 可选:覆盖全局 token\n# timeoutMs = 5000 # 可选:上报超时(毫秒),缺省不超时\n\n# ---- WS 反向服务器(外部 WS 客户端主动连入),可配置多个 ----\n[[onebot11.wsServers]]\nenabled = true\nhost = \"127.0.0.1\"\nport = 3001\n# token = \"abc\" # 可选:覆盖全局 token\n# heartbeatInterval = 30000 # 可选:WS ping 间隔(毫秒)\n\n# ---- WS 正向客户端(主动连接外部 WS,双向),可配置多个 ----\n# [[onebot11.wsReverseUrls]]\n# enabled = true\n# url = \"ws://127.0.0.1:8081/ws\" # 第三方 WS 地址(ws:// 或 wss://)\n# token = \"abc\" # 可选:覆盖全局 token\n# reconnectDelayMs = 5000 # 可选:断线重连延迟(毫秒)\n# maxReconnectAttempts = 10 # 可选:最大重连次数(缺省无限)\n# rejectUnauthorized = true # 可选:wss:// 是否校验证书(自签证书设 false)\n# heartbeatInterval = 30000 # 可选:WS ping 间隔(毫秒)\n`;\n}\n\n/** 生成 readme.md(用户项目使用说明,包管理器无关,以 pnpm 为例)。 */\nfunction readmeTemplate(dirName: string): string {\n return `# ${dirName}\n\n基于 **NapukettoQQ** 的 QQ 机器人项目(OneBot 11 协议),由 \\`create-napukettoqq\\` 生成。\n\n## 快速开始\n\n\\`\\`\\`bash\npnpm install\npnpm start -q 123456 # 换成你的 QQ 号\n\\`\\`\\`\n\n> 包管理器不限:\\`pnpm install\\` / \\`npm install\\` / \\`yarn install\\`,启动同理\n> (\\`pnpm start\\` / \\`npm start\\` / \\`yarn start\\`)。以下命令均以 pnpm 为例。\n\n启动后按终端提示快速登录或扫码;登录成功后 OneBot 11 接口生效\n(默认 HTTP \\`127.0.0.1:3000\\` + WS \\`127.0.0.1:3001\\`,见 \\`napuketto.toml\\`)。\n\n## 前置要求\n\n- 已安装 **QQ NT**(Windows,机器人所需的 \\`wrapper.node\\` 来自 QQ 安装目录,\n 未自动探测到时用 \\`pnpm start --qq-path <QQ安装目录>\\` 指定)\n- Node.js 20+ 与任一包管理器(pnpm / npm / yarn)\n\n## 常用命令\n\n| 命令 | 说明 |\n|---|---|\n| \\`pnpm start -q <QQ号>\\` | 单账号启动(走自建宿主) |\n| \\`pnpm napuketto config init\\` | 重新生成默认配置 |\n| \\`pnpm napuketto supervisor\\` | 多账号模式(按 \\`napuketto.toml\\` 的 \\`accounts\\`) |\n\n> npm 用户把 \\`pnpm napuketto\\` 换成 \\`npx napuketto\\`;yarn 用户换成 \\`yarn napuketto\\`。\n\n## 目录\n\n- \\`napuketto.toml\\` — 全局配置(QQ 号、OneBot 端口、token 等,修改后重启生效)\n- QQ 数据(账号目录/日志/缓存)默认在 \\`~/.napuketto/\\`\n\n## 升级\n\n\\`\\`\\`bash\npnpm update @napuketto/cli # npm: npm update @napuketto/cli;yarn: yarn up @napuketto/cli\n\\`\\`\\`\n`;\n}\n\n/** 生成 .gitignore(用户项目)。 */\nconst GITIGNORE = `node_modules/\ndist/\n*.log\nnapuketto-data/\n`;\n\n/**\n * 生成 NapukettoQQ 项目骨架到当前目录下的 <dirName>/。\n * 目标目录已存在且非空 → 抛错(提示换名字或先清空)。\n */\nexport async function scaffoldProject(dirName: string): Promise<ScaffoldResult> {\n const name = validateProjectName(dirName);\n const packageName = derivePackageName(name);\n const dir = path.resolve(process.cwd(), name);\n\n // 目录已存在且非空 → 拒绝(避免覆盖用户文件)\n let existing: string[] = [];\n try {\n existing = await readdir(dir);\n } catch {\n // 目录不存在,可直接创建\n }\n if (existing.length > 0) {\n throw new Error(`目录已存在且非空:${dir}\\n请换一个文件夹名,或先清空该目录。`);\n }\n\n await mkdir(dir, { recursive: true });\n await writeFile(path.join(dir, \"package.json\"), packageJsonTemplate(packageName), \"utf8\");\n await writeFile(path.join(dir, \"napuketto.toml\"), tomlTemplate(), \"utf8\");\n await writeFile(path.join(dir, \"readme.md\"), readmeTemplate(name), \"utf8\");\n await writeFile(path.join(dir, \".gitignore\"), GITIGNORE, \"utf8\");\n\n return { dir, packageName };\n}\n","#!/usr/bin/env node\n\n/**\n * create-napukettoqq 入口(2026-08-07)。\n *\n * 用法:\n * create-napukettoqq # 交互问部署文件夹名(默认 NapukettoQQ)\n * create-napukettoqq my-bot # 位置参数指定文件夹名,跳过交互\n *\n * 流程:生成项目骨架(package.json / napuketto.toml / readme.md / .gitignore)\n * → 用调用方的包管理器(npm_config_user_agent 检测,支持 pnpm/yarn/npm)\n * 自动 install → 询问是否现在启动(默认 Y),Y 则前台运行(Ctrl+C 停止)。\n */\n\nimport { spawn } from \"node:child_process\";\nimport process from \"node:process\";\nimport { createInterface } from \"node:readline/promises\";\nimport { DEFAULT_PROJECT_NAME, detectPackageManager, pmBin, scaffoldProject } from \"./scaffold.js\";\n\n/** 交互询问部署文件夹名(回车取默认值)。 */\nasync function askProjectName(): Promise<string> {\n const rl = createInterface({ input: process.stdin, output: process.stdout });\n try {\n const answer = await rl.question(`? 部署文件夹名(默认 ${DEFAULT_PROJECT_NAME}): `);\n return answer.trim() || DEFAULT_PROJECT_NAME;\n } finally {\n rl.close();\n }\n}\n\n/** 交互询问是否现在启动(默认 Y)。 */\nasync function askStartNow(): Promise<boolean> {\n const rl = createInterface({ input: process.stdin, output: process.stdout });\n try {\n const answer = (await rl.question(\"? 是否现在启动?(默认 Y/n): \")).trim().toLowerCase();\n return answer !== \"n\" && answer !== \"no\";\n } finally {\n rl.close();\n }\n}\n\n/** 前台执行命令(stdio 继承,用户可见进度/交互),返回退出码。 */\nfunction runInteractive(bin: string, args: string[], cwd: string): Promise<number> {\n return new Promise((resolve) => {\n // Windows 下 .cmd shim 需 shell:true(CreateProcess 不能直接执行 .cmd)\n const child = spawn(bin, args, {\n cwd,\n stdio: \"inherit\",\n windowsHide: false,\n shell: process.platform === \"win32\",\n });\n child.on(\"exit\", (code) => {\n resolve(code ?? 0);\n });\n });\n}\n\nasync function main(): Promise<void> {\n const dirArg = process.argv[2];\n const dirName = dirArg ?? (await askProjectName());\n const result = await scaffoldProject(dirName);\n const pm = detectPackageManager();\n\n console.log(\"\\n========================================\");\n console.log(\" NapukettoQQ 项目已生成\");\n console.log(` 位置:${result.dir}`);\n console.log(` 包管理器:${pm}(自动检测)`);\n console.log(\"========================================\");\n console.log(\"前置要求:本机已安装 QQ NT(wrapper.node 来自 QQ 安装目录)。\");\n\n // 自动安装依赖(用调用方包管理器)\n console.log(`\\n正在安装依赖(${pm} install)...`);\n const installCode = await runInteractive(pmBin(pm), [\"install\"], result.dir);\n if (installCode !== 0) {\n console.error(`\\n依赖安装失败(退出码 ${installCode})。请手动执行:`);\n console.error(` cd ${result.dir}`);\n console.error(` ${pm} install`);\n process.exitCode = 1;\n return;\n }\n\n // 询问是否现在启动(默认 Y → 前台运行)\n if (await askStartNow()) {\n console.log(`\\n正在启动 NapukettoQQ(Ctrl+C 停止)...\\n`);\n const startCode = await runInteractive(pmBin(pm), [\"start\"], result.dir);\n if (startCode !== 0) {\n process.exitCode = startCode;\n }\n } else {\n console.log(\"\\n启动方式:\");\n console.log(` cd ${result.dir}`);\n console.log(` ${pm} start -q <你的QQ号>`);\n console.log(\"详细说明见项目内 readme.md 与 napuketto.toml。\");\n }\n}\n\nmain().catch((err: unknown) => {\n const message = err instanceof Error ? err.message : String(err);\n console.error(`创建失败:${message}`);\n process.exitCode = 1;\n});\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.mjs","names":[],"sources":["../src/scaffold.ts","../src/index.ts"],"sourcesContent":["/**\n * create-napukettoqq 脚手架核心:模板渲染 + 目标目录创建 + 文件写入。\n *\n * 模板为独立文件(templates/ 目录,运行时读取),不硬编码在源码里;\n * 需要插值的占位符用 {{key}} 标记(见 renderTemplate)。\n * 依赖极轻(node:fs/promises / node:os / node:path / node:process)。\n * 生成的用户项目依赖发布版的 @napuketto/cli,用户 install 后经 cli → loader\n * 自建宿主启动。\n */\n\nimport { readFileSync } from \"node:fs\";\nimport { mkdir, readdir, readFile, rm, writeFile } from \"node:fs/promises\";\nimport { homedir } from \"node:os\";\nimport path from \"node:path\";\nimport process from \"node:process\";\n\n/** 模板目录(相对本文件:src 与 dist 都在包根下,../templates 均指向包根/templates)。 */\nconst TEMPLATES = new URL(\"../templates/\", import.meta.url);\n\n/**\n * 生成的用户项目所依赖的 @napuketto/cli 版本范围。\n * 运行时从脚手架自身 package.json 读取(dependencies[\"@napuketto/cli\"])——\n * 发版时 changesets 升版本 + pnpm publish 把 workspace:* 替换为实际版本,\n * 模板版本自动跟随,无需手动同步(比硬编码常量可靠)。\n */\nexport function cliVersionRange(): string {\n try {\n const ownPkg = JSON.parse(\n readFileSync(new URL(\"../package.json\", import.meta.url), \"utf8\"),\n ) as { dependencies?: Record<string, string> };\n const range = ownPkg.dependencies?.[\"@napuketto/cli\"];\n // workspace:* 仅在本地 monorepo 直跑脚手架时出现(发布后 pnpm 替换为实际版本),\n // 此时兜底默认范围,避免生成的用户项目 install 失败\n if (range !== undefined && range !== \"workspace:*\") {\n return range;\n }\n } catch {\n // package.json 缺失(异常环境)→ 兜底默认范围\n }\n return \"^0.0.1\";\n}\n\n/** 默认部署文件夹名(用户交互回车缺省值)。 */\nexport const DEFAULT_PROJECT_NAME = \"NapukettoQQ\";\n\n/** 用户可用的包管理器(npm_config_user_agent 检测)。 */\nexport type PackageManager = \"pnpm\" | \"yarn\" | \"npm\";\n\n/**\n * 由 npm_config_user_agent 检测调用方包管理器(yarn create / npm create /\n * pnpm create 都会注入该变量)。兜底 pnpm(项目生态,或直接 node 运行)。\n */\nexport function detectPackageManager(): PackageManager {\n // noPropertyAccessFromIndexSignature:process.env 是索引签名,必须括号访问\n const ua = process.env[\"npm_config_user_agent\"] ?? \"\";\n if (ua.startsWith(\"yarn/\")) {\n return \"yarn\";\n }\n if (ua.startsWith(\"npm/\")) {\n return \"npm\";\n }\n return \"pnpm\";\n}\n\n/** Windows 下包管理器 bin 需带 .cmd 后缀(CreateProcess 不认无扩展名 shim)。 */\nexport function pmBin(pm: PackageManager): string {\n return process.platform === \"win32\" ? `${pm}.cmd` : pm;\n}\n\n/** Windows 路径非法字符(项目文件夹名校验用)。 */\nconst ILLEGAL_NAME = /[\\\\/:*?\"<>|]/;\n\n/** 脚手架生成结果。 */\nexport interface ScaffoldResult {\n /** 目标目录绝对路径。 */\n dir: string;\n /** 派生出的 npm 包名(小写、空格 → -)。 */\n packageName: string;\n}\n\n/** 校验文件夹名:非空、非 . / ..、不含 Windows 非法字符。 */\nexport function validateProjectName(raw: string): string {\n const name = raw.trim();\n if (name === \"\" || name === \".\" || name === \"..\") {\n throw new Error(\"文件夹名不能为空或为 . / ..\");\n }\n if (ILLEGAL_NAME.test(name)) {\n throw new Error(`文件夹名含非法字符:${name}(不允许 \\\\ / : * ? \" < > |)`);\n }\n return name;\n}\n\n/** 由文件夹名派生 npm 包名(小写、空格 → -)。 */\nexport function derivePackageName(dirName: string): string {\n return dirName.toLowerCase().replace(/\\s+/g, \"-\");\n}\n\n/** 解析目标目录绝对路径(校验名称后,基于 cwd)。 */\nexport function resolveTargetDir(dirName: string): string {\n return path.resolve(process.cwd(), validateProjectName(dirName));\n}\n\n/** 目标目录状态:不存在 / 空 / 非空。 */\nexport type DirStatus = \"missing\" | \"empty\" | \"nonempty\";\n\n/** 检测目标目录状态(供交互层决定是否询问清空)。 */\nexport async function checkDirStatus(dir: string): Promise<DirStatus> {\n let entries: string[] = [];\n try {\n entries = await readdir(dir);\n } catch {\n return \"missing\";\n }\n return entries.length === 0 ? \"empty\" : \"nonempty\";\n}\n\n/** 清空目录内容(保留目录本身)。 */\nexport async function emptyDir(dir: string): Promise<void> {\n for (const entry of await readdir(dir)) {\n await rm(path.join(dir, entry), { recursive: true, force: true });\n }\n}\n\n/** 读取模板文件(templates/ 目录,UTF-8)。 */\nfunction readTemplate(file: string): Promise<string> {\n return readFile(new URL(file, TEMPLATES), \"utf8\");\n}\n\n/**\n * 占位符渲染:把 {{key}} 替换为 vars[key]。\n * 模板中出现未提供的占位符 → 抛错(防漏插值写出半成品)。\n */\nfunction renderTemplate(content: string, vars: Record<string, string>): string {\n return content.replace(/\\{\\{(\\w+)\\}\\}/g, (match, key: string) => {\n const value = vars[key];\n if (value === undefined) {\n throw new Error(`模板占位符未提供:{{${key}}}(模板 ${match} 无法渲染)`);\n }\n return value;\n });\n}\n\n/**\n * 生成 NapukettoQQ 项目骨架到当前目录下的 <dirName>/。\n * 目标目录已存在且非空:overwrite=false → 抛错;overwrite=true → 清空后生成。\n * 模板与写入目标一一对应(同文件名)。\n */\nexport async function scaffoldProject(dirName: string, overwrite = false): Promise<ScaffoldResult> {\n const name = validateProjectName(dirName);\n const packageName = derivePackageName(name);\n const dir = resolveTargetDir(name);\n\n const status = await checkDirStatus(dir);\n if (status === \"nonempty\" && !overwrite) {\n throw new Error(`目录已存在且非空:${dir}\\n请换一个文件夹名,或先清空该目录。`);\n }\n if (status === \"nonempty\") {\n await emptyDir(dir);\n }\n\n // 渲染变量:dataDir 用 JSON.stringify 转义反斜杠 → TOML 基本字符串合法写法\n const vars: Record<string, string> = {\n packageName,\n cliVersion: cliVersionRange(),\n dataDir: JSON.stringify(path.join(homedir(), \".napuketto\")),\n };\n // 用户项目是「运行壳」:不开发代码,只需 package.json(依赖 + start)+ napuketto.toml(配置)\n // 模板文件为 templates/<name>.tmpl(.tmpl 后缀避免 TOML/JSON 语言服务把占位符当非法语法)\n const files = [\"package.json\", \"napuketto.toml\"];\n\n await mkdir(dir, { recursive: true });\n for (const file of files) {\n const rendered = renderTemplate(await readTemplate(`${file}.tmpl`), vars);\n await writeFile(path.join(dir, file), rendered, \"utf8\");\n }\n\n return { dir, packageName };\n}\n","#!/usr/bin/env node\n\n/**\n * create-napukettoqq 入口(2026-08-07 美化 v3,交互层用 @clack/prompts——\n * 现代脚手架交互标准,create-vue / create-turbo 同款)。\n *\n * 用法:\n * create-napukettoqq # intro + 交互问部署文件夹名(默认 NapukettoQQ)\n * create-napukettoqq my-bot # 位置参数指定文件夹名,跳过命名交互\n * create-napukettoqq my-bot -f # 目标目录非空时强制清空覆盖\n * create-napukettoqq -y # 全默认(命名用默认值、启动询问用默认 Y)\n * create-napukettoqq -h # 打印帮助\n *\n * 流程:intro → 问文件夹名 → 目标目录准备(非空需确认清空)→ spinner 生成骨架\n * (package.json / napuketto.toml / readme.md / .gitignore)→ 用调用方包管理器\n * 自动 install → 询问是否现在启动(默认 Y)→ 前台运行 → outro 收尾。\n */\n\nimport { spawn } from \"node:child_process\";\nimport { readFileSync } from \"node:fs\";\nimport { relative } from \"node:path\";\nimport process from \"node:process\";\nimport { cancel, confirm, intro, isCancel, log, outro, spinner, text } from \"@clack/prompts\";\nimport {\n checkDirStatus,\n DEFAULT_PROJECT_NAME,\n detectPackageManager,\n pmBin,\n resolveTargetDir,\n scaffoldProject,\n validateProjectName,\n} from \"./scaffold.js\";\n\n/** CLI 参数解析结果。 */\ninterface CliOptions {\n /** 位置参数:部署文件夹名(可选)。 */\n name?: string;\n /** -f/--forced:目标目录非空时强制清空覆盖。 */\n forced: boolean;\n /** -y/--yes:跳过所有交互提示(使用默认值)。 */\n yes: boolean;\n /** -h/--help:显示帮助。 */\n help: boolean;\n}\n\n/** 帮助文本(-h/--help)。 */\nconst HELP = ` 用法: create-napukettoqq [name] [选项]\n\n 选项:\n -f, --forced 目标目录非空时强制清空并覆盖\n -y, --yes 跳过所有交互提示(使用默认值)\n -h, --help 显示本帮助\n`;\n\n/** 解析命令行参数(flag 仅三个,手写保持自包含,不引 yargs-parser)。 */\nfunction parseArgs(argv: string[]): CliOptions {\n const opts: CliOptions = { forced: false, yes: false, help: false };\n for (const arg of argv) {\n if (arg === \"-f\" || arg === \"--forced\") {\n opts.forced = true;\n } else if (arg === \"-y\" || arg === \"--yes\") {\n opts.yes = true;\n } else if (arg === \"-h\" || arg === \"--help\") {\n opts.help = true;\n } else if (arg.startsWith(\"-\")) {\n throw new Error(`未知参数:${arg}`);\n } else if (opts.name === undefined) {\n opts.name = arg;\n } else {\n throw new Error(`多余的位置参数:${arg}`);\n }\n }\n return opts;\n}\n\n/** 读取脚手架自身版本号(intro 显示;dist 场景 import.meta.url 指向包内)。 */\nfunction readVersion(): string {\n try {\n const ownPkg = JSON.parse(\n readFileSync(new URL(\"../package.json\", import.meta.url), \"utf8\"),\n ) as { version?: string };\n return ownPkg.version ?? \"0.0.0\";\n } catch {\n // package.json 缺失(异常环境)→ 兜底\n return \"0.0.0\";\n }\n}\n\n/** 交互询问部署文件夹名(回车取默认值;-y 直接返回默认值)。 */\nasync function askProjectName(yes: boolean): Promise<string> {\n if (yes) {\n return DEFAULT_PROJECT_NAME;\n }\n const name = await text({\n message: \"部署文件夹名\",\n placeholder: DEFAULT_PROJECT_NAME,\n initialValue: DEFAULT_PROJECT_NAME,\n validate: (input: string | undefined) => {\n try {\n validateProjectName(input ?? \"\");\n return undefined;\n } catch (err) {\n return err instanceof Error ? err.message : String(err);\n }\n },\n });\n if (isCancel(name)) {\n cancel(\"操作已取消\");\n process.exit(0);\n }\n return validateProjectName(name);\n}\n\n/** 询问是否移除现有文件并继续(目标目录非空时)。 */\nasync function confirmRemove(dirName: string): Promise<boolean> {\n const ok = await confirm({\n message: `目录 \"${dirName}\" 已存在且非空,移除现有文件并继续?`,\n initialValue: false,\n });\n if (isCancel(ok)) {\n cancel(\"操作已取消\");\n process.exit(0);\n }\n return ok;\n}\n\n/** 询问是否现在启动(默认 Y;-y 直接返回 true)。 */\nasync function askStartNow(yes: boolean): Promise<boolean> {\n if (yes) {\n return true;\n }\n const ok = await confirm({\n message: \"是否现在启动?\",\n initialValue: true,\n });\n if (isCancel(ok)) {\n cancel(\"操作已取消\");\n process.exit(0);\n }\n return ok;\n}\n\n/** 前台执行命令(stdio 继承,用户可见进度/交互),返回退出码。 */\nfunction runInteractive(bin: string, args: string[], cwd: string): Promise<number> {\n return new Promise((resolve) => {\n // Windows 下 .cmd shim 需 shell:true(CreateProcess 不能直接执行 .cmd)\n const child = spawn(bin, args, {\n cwd,\n stdio: \"inherit\",\n windowsHide: false,\n shell: process.platform === \"win32\",\n });\n child.on(\"exit\", (code) => {\n resolve(code ?? 0);\n });\n });\n}\n\nasync function main(): Promise<void> {\n const opts = parseArgs(process.argv.slice(2));\n if (opts.help) {\n console.log(HELP);\n return;\n }\n\n // intro:顶边框 + 名称 + 版本\n intro(`create-napukettoqq v${readVersion()}`);\n\n const dirName = opts.name ?? (await askProjectName(opts.yes));\n const targetDir = resolveTargetDir(dirName);\n\n // 目标目录准备:非空需确认清空(-f 跳过确认;-y 不隐式清空,保持默认安全语义)\n let overwrite = false;\n if ((await checkDirStatus(targetDir)) === \"nonempty\") {\n if (!opts.forced) {\n log.warn(`目标目录 \"${dirName}\" 已存在且非空。`);\n overwrite = await confirmRemove(dirName);\n if (!overwrite) {\n outro(\"已取消,未做任何修改。\");\n return;\n }\n } else {\n overwrite = true;\n }\n }\n\n // 生成骨架(spinner 进度)\n const s = spinner();\n s.start(`正在生成项目骨架 ${dirName} ...`);\n const result = await scaffoldProject(dirName, overwrite);\n s.stop(`项目骨架已生成:${dirName}`);\n log.success(`位置:${result.dir}`);\n\n // 自动安装依赖(用调用方包管理器)\n const pm = detectPackageManager();\n log.info(`包管理器:${pm}(自动检测)`);\n log.info(\"前置要求:本机已安装 QQ NT(wrapper.node 来自 QQ 安装目录)。\");\n log.info(`正在安装依赖(${pm} install)...`);\n const installCode = await runInteractive(pmBin(pm), [\"install\"], result.dir);\n if (installCode !== 0) {\n log.error(`依赖安装失败(退出码 ${installCode})。请手动执行:`);\n log.step(`cd ${result.dir}`);\n log.step(`${pm} install`);\n outro(\"创建失败,请手动安装后重试。\");\n process.exitCode = 1;\n return;\n }\n\n // 询问是否现在启动(默认 Y → 前台运行)\n if (await askStartNow(opts.yes)) {\n log.info(\"正在启动 NapukettoQQ(Ctrl+C 停止)...\\n\");\n const startCode = await runInteractive(pmBin(pm), [\"start\"], result.dir);\n if (startCode !== 0) {\n process.exitCode = startCode;\n }\n } else {\n log.message(\"你可以稍后这样启动:\");\n if (targetDir !== process.cwd()) {\n const related = relative(process.cwd(), targetDir);\n log.step(`cd ${related}`);\n }\n log.step(`${pm} start -q <你的QQ号>`);\n log.message(\"配置说明见项目内 napuketto.toml。\");\n }\n\n outro(\"完成。\");\n}\n\nmain().catch((err: unknown) => {\n const message = err instanceof Error ? err.message : String(err);\n log.error(`创建失败:${message}`);\n process.exitCode = 1;\n});\n"],"mappings":";;;;;;;;;;;;;;;;;;;AAiBA,MAAM,YAAY,IAAI,IAAI,iBAAiB,YAAY,GAAG;;;;;;;AAQ1D,SAAgB,kBAA0B;CACtC,IAAI;EAIA,MAAM,QAHS,KAAK,MAChB,aAAa,IAAI,IAAI,mBAAmB,YAAY,GAAG,GAAG,MAAM,CAEjD,CAAC,CAAC,eAAe;EAGpC,IAAI,UAAU,KAAA,KAAa,UAAU,eACjC,OAAO;CAEf,QAAQ,CAER;CACA,OAAO;AACX;;AAGA,MAAa,uBAAuB;;;;;AASpC,SAAgB,uBAAuC;CAEnD,MAAM,KAAK,QAAQ,IAAI,4BAA4B;CACnD,IAAI,GAAG,WAAW,OAAO,GACrB,OAAO;CAEX,IAAI,GAAG,WAAW,MAAM,GACpB,OAAO;CAEX,OAAO;AACX;;AAGA,SAAgB,MAAM,IAA4B;CAC9C,OAAO,QAAQ,aAAa,UAAU,GAAG,GAAG,QAAQ;AACxD;;AAGA,MAAM,eAAe;;AAWrB,SAAgB,oBAAoB,KAAqB;CACrD,MAAM,OAAO,IAAI,KAAK;CACtB,IAAI,SAAS,MAAM,SAAS,OAAO,SAAS,MACxC,MAAM,IAAI,MAAM,mBAAmB;CAEvC,IAAI,aAAa,KAAK,IAAI,GACtB,MAAM,IAAI,MAAM,aAAa,KAAK,yBAAyB;CAE/D,OAAO;AACX;;AAGA,SAAgB,kBAAkB,SAAyB;CACvD,OAAO,QAAQ,YAAY,CAAC,CAAC,QAAQ,QAAQ,GAAG;AACpD;;AAGA,SAAgB,iBAAiB,SAAyB;CACtD,OAAO,KAAK,QAAQ,QAAQ,IAAI,GAAG,oBAAoB,OAAO,CAAC;AACnE;;AAMA,eAAsB,eAAe,KAAiC;CAClE,IAAI,UAAoB,CAAC;CACzB,IAAI;EACA,UAAU,MAAM,QAAQ,GAAG;CAC/B,QAAQ;EACJ,OAAO;CACX;CACA,OAAO,QAAQ,WAAW,IAAI,UAAU;AAC5C;;AAGA,eAAsB,SAAS,KAA4B;CACvD,KAAK,MAAM,SAAS,MAAM,QAAQ,GAAG,GACjC,MAAM,GAAG,KAAK,KAAK,KAAK,KAAK,GAAG;EAAE,WAAW;EAAM,OAAO;CAAK,CAAC;AAExE;;AAGA,SAAS,aAAa,MAA+B;CACjD,OAAO,SAAS,IAAI,IAAI,MAAM,SAAS,GAAG,MAAM;AACpD;;;;;AAMA,SAAS,eAAe,SAAiB,MAAsC;CAC3E,OAAO,QAAQ,QAAQ,mBAAmB,OAAO,QAAgB;EAC7D,MAAM,QAAQ,KAAK;EACnB,IAAI,UAAU,KAAA,GACV,MAAM,IAAI,MAAM,cAAc,IAAI,QAAQ,MAAM,OAAO;EAE3D,OAAO;CACX,CAAC;AACL;;;;;;AAOA,eAAsB,gBAAgB,SAAiB,YAAY,OAAgC;CAC/F,MAAM,OAAO,oBAAoB,OAAO;CACxC,MAAM,cAAc,kBAAkB,IAAI;CAC1C,MAAM,MAAM,iBAAiB,IAAI;CAEjC,MAAM,SAAS,MAAM,eAAe,GAAG;CACvC,IAAI,WAAW,cAAc,CAAC,WAC1B,MAAM,IAAI,MAAM,YAAY,IAAI,oBAAoB;CAExD,IAAI,WAAW,YACX,MAAM,SAAS,GAAG;CAItB,MAAM,OAA+B;EACjC;EACA,YAAY,gBAAgB;EAC5B,SAAS,KAAK,UAAU,KAAK,KAAK,QAAQ,GAAG,YAAY,CAAC;CAC9D;CAGA,MAAM,QAAQ,CAAC,gBAAgB,gBAAgB;CAE/C,MAAM,MAAM,KAAK,EAAE,WAAW,KAAK,CAAC;CACpC,KAAK,MAAM,QAAQ,OAAO;EACtB,MAAM,WAAW,eAAe,MAAM,aAAa,GAAG,KAAK,MAAM,GAAG,IAAI;EACxE,MAAM,UAAU,KAAK,KAAK,KAAK,IAAI,GAAG,UAAU,MAAM;CAC1D;CAEA,OAAO;EAAE;EAAK;CAAY;AAC9B;;;;;;;;;;;;;;;;;;;ACnIA,MAAM,OAAO;;;;;;;;AASb,SAAS,UAAU,MAA4B;CAC3C,MAAM,OAAmB;EAAE,QAAQ;EAAO,KAAK;EAAO,MAAM;CAAM;CAClE,KAAK,MAAM,OAAO,MACd,IAAI,QAAQ,QAAQ,QAAQ,YACxB,KAAK,SAAS;MACX,IAAI,QAAQ,QAAQ,QAAQ,SAC/B,KAAK,MAAM;MACR,IAAI,QAAQ,QAAQ,QAAQ,UAC/B,KAAK,OAAO;MACT,IAAI,IAAI,WAAW,GAAG,GACzB,MAAM,IAAI,MAAM,QAAQ,KAAK;MAC1B,IAAI,KAAK,SAAS,KAAA,GACrB,KAAK,OAAO;MAEZ,MAAM,IAAI,MAAM,WAAW,KAAK;CAGxC,OAAO;AACX;;AAGA,SAAS,cAAsB;CAC3B,IAAI;EAIA,OAHe,KAAK,MAChB,aAAa,IAAI,IAAI,mBAAmB,YAAY,GAAG,GAAG,MAAM,CAExD,CAAC,CAAC,WAAW;CAC7B,QAAQ;EAEJ,OAAO;CACX;AACJ;;AAGA,eAAe,eAAe,KAA+B;CACzD,IAAI,KACA,OAAO;CAEX,MAAM,OAAO,MAAM,KAAK;EACpB,SAAS;EACT,aAAa;EACb,cAAc;EACd,WAAW,UAA8B;GACrC,IAAI;IACA,oBAAoB,SAAS,EAAE;IAC/B;GACJ,SAAS,KAAK;IACV,OAAO,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG;GAC1D;EACJ;CACJ,CAAC;CACD,IAAI,SAAS,IAAI,GAAG;EAChB,OAAO,OAAO;EACd,QAAQ,KAAK,CAAC;CAClB;CACA,OAAO,oBAAoB,IAAI;AACnC;;AAGA,eAAe,cAAc,SAAmC;CAC5D,MAAM,KAAK,MAAM,QAAQ;EACrB,SAAS,OAAO,QAAQ;EACxB,cAAc;CAClB,CAAC;CACD,IAAI,SAAS,EAAE,GAAG;EACd,OAAO,OAAO;EACd,QAAQ,KAAK,CAAC;CAClB;CACA,OAAO;AACX;;AAGA,eAAe,YAAY,KAAgC;CACvD,IAAI,KACA,OAAO;CAEX,MAAM,KAAK,MAAM,QAAQ;EACrB,SAAS;EACT,cAAc;CAClB,CAAC;CACD,IAAI,SAAS,EAAE,GAAG;EACd,OAAO,OAAO;EACd,QAAQ,KAAK,CAAC;CAClB;CACA,OAAO;AACX;;AAGA,SAAS,eAAe,KAAa,MAAgB,KAA8B;CAC/E,OAAO,IAAI,SAAS,YAAY;EAQ5B,MANoB,KAAK,MAAM;GAC3B;GACA,OAAO;GACP,aAAa;GACb,OAAO,QAAQ,aAAa;EAChC,CACI,CAAC,CAAC,GAAG,SAAS,SAAS;GACvB,QAAQ,QAAQ,CAAC;EACrB,CAAC;CACL,CAAC;AACL;AAEA,eAAe,OAAsB;CACjC,MAAM,OAAO,UAAU,QAAQ,KAAK,MAAM,CAAC,CAAC;CAC5C,IAAI,KAAK,MAAM;EACX,QAAQ,IAAI,IAAI;EAChB;CACJ;CAGA,MAAM,wBAAwB,YAAY,GAAG;CAE7C,MAAM,UAAU,KAAK,QAAS,MAAM,eAAe,KAAK,GAAG;CAC3D,MAAM,YAAY,iBAAiB,OAAO;CAG1C,IAAI,YAAY;CAChB,IAAK,MAAM,eAAe,SAAS,MAAO,YACtC,IAAI,CAAC,KAAK,QAAQ;EACd,IAAI,KAAK,SAAS,QAAQ,UAAU;EACpC,YAAY,MAAM,cAAc,OAAO;EACvC,IAAI,CAAC,WAAW;GACZ,MAAM,aAAa;GACnB;EACJ;CACJ,OACI,YAAY;CAKpB,MAAM,IAAI,QAAQ;CAClB,EAAE,MAAM,YAAY,QAAQ,KAAK;CACjC,MAAM,SAAS,MAAM,gBAAgB,SAAS,SAAS;CACvD,EAAE,KAAK,WAAW,SAAS;CAC3B,IAAI,QAAQ,MAAM,OAAO,KAAK;CAG9B,MAAM,KAAK,qBAAqB;CAChC,IAAI,KAAK,QAAQ,GAAG,OAAO;CAC3B,IAAI,KAAK,4CAA4C;CACrD,IAAI,KAAK,UAAU,GAAG,aAAa;CACnC,MAAM,cAAc,MAAM,eAAe,MAAM,EAAE,GAAG,CAAC,SAAS,GAAG,OAAO,GAAG;CAC3E,IAAI,gBAAgB,GAAG;EACnB,IAAI,MAAM,cAAc,YAAY,SAAS;EAC7C,IAAI,KAAK,MAAM,OAAO,KAAK;EAC3B,IAAI,KAAK,GAAG,GAAG,SAAS;EACxB,MAAM,gBAAgB;EACtB,QAAQ,WAAW;EACnB;CACJ;CAGA,IAAI,MAAM,YAAY,KAAK,GAAG,GAAG;EAC7B,IAAI,KAAK,kCAAkC;EAC3C,MAAM,YAAY,MAAM,eAAe,MAAM,EAAE,GAAG,CAAC,OAAO,GAAG,OAAO,GAAG;EACvE,IAAI,cAAc,GACd,QAAQ,WAAW;CAE3B,OAAO;EACH,IAAI,QAAQ,YAAY;EACxB,IAAI,cAAc,QAAQ,IAAI,GAAG;GAC7B,MAAM,UAAU,SAAS,QAAQ,IAAI,GAAG,SAAS;GACjD,IAAI,KAAK,MAAM,SAAS;EAC5B;EACA,IAAI,KAAK,GAAG,GAAG,kBAAkB;EACjC,IAAI,QAAQ,0BAA0B;CAC1C;CAEA,MAAM,KAAK;AACf;AAEA,KAAK,CAAC,CAAC,OAAO,QAAiB;CAC3B,MAAM,UAAU,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG;CAC/D,IAAI,MAAM,QAAQ,SAAS;CAC3B,QAAQ,WAAW;AACvB,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,17 +1,19 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-napukettoqq",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "一键生成 NapukettoQQ 机器人项目(yarn/pnpm/npm create napukettoqq)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"files": [
|
|
7
|
-
"dist"
|
|
7
|
+
"dist",
|
|
8
|
+
"templates"
|
|
8
9
|
],
|
|
9
10
|
"main": "./dist/index.mjs",
|
|
10
11
|
"bin": {
|
|
11
12
|
"create-napukettoqq": "./dist/index.mjs"
|
|
12
13
|
},
|
|
13
14
|
"dependencies": {
|
|
14
|
-
"@
|
|
15
|
+
"@clack/prompts": "^1.7.0",
|
|
16
|
+
"@napuketto/cli": "0.0.2"
|
|
15
17
|
},
|
|
16
18
|
"devDependencies": {
|
|
17
19
|
"tsdown": "^0.22.14"
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# ============================================================
|
|
2
|
+
# NapukettoQQ 全局配置文件(由 create-napukettoqq 生成)
|
|
3
|
+
# ============================================================
|
|
4
|
+
# 数据(账号目录/日志/缓存/QQ 数据)不在此文件,按数据根组织:
|
|
5
|
+
# 优先级:--data-dir <dir> > NAPKETTO_DATA 环境变量 > ~/.napuketto
|
|
6
|
+
# 配置文件路径解析:NAPKETTO_CONFIG > 项目根探测 > cwd > 数据根兜底。
|
|
7
|
+
#
|
|
8
|
+
# 单账号启动每次读取本文件;修改后重启生效。
|
|
9
|
+
|
|
10
|
+
# ------------------------------------------------------------
|
|
11
|
+
# 基础配置
|
|
12
|
+
# ------------------------------------------------------------
|
|
13
|
+
|
|
14
|
+
dataDir = {{dataDir}}
|
|
15
|
+
|
|
16
|
+
# 数据根目录:账号目录/日志/缓存/QQ 数据存放位置(绝对路径)
|
|
17
|
+
# 多账号(supervisor)模式下,账号进程崩溃是否自动重启
|
|
18
|
+
autoRestart = true
|
|
19
|
+
|
|
20
|
+
# 崩溃后自动重启的延迟(毫秒)
|
|
21
|
+
restartDelayMs = 2000
|
|
22
|
+
|
|
23
|
+
# 多账号列表(留空 = 单账号模式:启动后自动快速登录/扫码)
|
|
24
|
+
# [[accounts]]
|
|
25
|
+
# qq = "123456" # QQ 号(必填)
|
|
26
|
+
# enabled = true # 是否启用(可选,缺省 true)
|
|
27
|
+
|
|
28
|
+
# ------------------------------------------------------------
|
|
29
|
+
# OneBot 11 协议段
|
|
30
|
+
# ------------------------------------------------------------
|
|
31
|
+
[onebot11]
|
|
32
|
+
|
|
33
|
+
# 鉴权 token(全局默认):HTTP/WS 连接必须携带。
|
|
34
|
+
# 校验方式:Authorization: Bearer <token> / 裸 token / URL 参数 ?access_token=<token>
|
|
35
|
+
# 每个网络实例可单独覆盖(见下文各实例的 token 字段)。
|
|
36
|
+
# 留空 = 不鉴权(仅本机调试推荐)。
|
|
37
|
+
# token = ""
|
|
38
|
+
|
|
39
|
+
# 心跳 meta 事件间隔(毫秒);0 = 关闭心跳事件
|
|
40
|
+
heartbeatInterval = 3000
|
|
41
|
+
|
|
42
|
+
# 是否上报机器人自己发的消息(OB11 规范默认不上报)
|
|
43
|
+
reportSelfMessage = false
|
|
44
|
+
|
|
45
|
+
# 消息内容格式:array = 消息段数组(标准 OneBot 11),string = CQ 码字符串
|
|
46
|
+
messagePostFormat = "array"
|
|
47
|
+
|
|
48
|
+
# ---- HTTP 反向服务器(接收第三方 POST 指令),可配置多个 ----
|
|
49
|
+
[[onebot11.httpServers]]
|
|
50
|
+
enabled = true
|
|
51
|
+
host = "127.0.0.1" # 监听地址;0.0.0.0 = 监听所有网卡(局域网可连)
|
|
52
|
+
port = 3000
|
|
53
|
+
# token = "abc" # 可选:覆盖全局 token
|
|
54
|
+
|
|
55
|
+
# ---- HTTP 正向上报(Webhook:把事件 POST 到第三方地址),可配置多个 ----
|
|
56
|
+
# [[onebot11.httpPostUrls]]
|
|
57
|
+
# enabled = true
|
|
58
|
+
# url = "http://127.0.0.1:8080/onebot" # 第三方接收地址
|
|
59
|
+
# token = "abc" # 可选:覆盖全局 token
|
|
60
|
+
# timeoutMs = 5000 # 可选:上报超时(毫秒),缺省不超时
|
|
61
|
+
|
|
62
|
+
# ---- WS 反向服务器(外部 WS 客户端主动连入),可配置多个 ----
|
|
63
|
+
[[onebot11.wsServers]]
|
|
64
|
+
enabled = true
|
|
65
|
+
host = "127.0.0.1"
|
|
66
|
+
port = 3001
|
|
67
|
+
# token = "abc" # 可选:覆盖全局 token
|
|
68
|
+
# heartbeatInterval = 30000 # 可选:WS ping 间隔(毫秒)
|
|
69
|
+
|
|
70
|
+
# ---- WS 正向客户端(主动连接外部 WS,双向),可配置多个 ----
|
|
71
|
+
# [[onebot11.wsReverseUrls]]
|
|
72
|
+
# enabled = true
|
|
73
|
+
# url = "ws://127.0.0.1:8081/ws" # 第三方 WS 地址(ws:// 或 wss://)
|
|
74
|
+
# token = "abc" # 可选:覆盖全局 token
|
|
75
|
+
# reconnectDelayMs = 5000 # 可选:断线重连延迟(毫秒)
|
|
76
|
+
# maxReconnectAttempts = 10 # 可选:最大重连次数(缺省无限)
|
|
77
|
+
# rejectUnauthorized = true # 可选:wss:// 是否校验证书(自签证书设 false)
|
|
78
|
+
# heartbeatInterval = 30000 # 可选:WS ping 间隔(毫秒)
|