qdmp-cli 0.1.28 → 0.1.29

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
@@ -26,12 +26,17 @@ npm install -g qdmp-cli
26
26
  {
27
27
  "appId": "your_app_id",
28
28
  "devAppId": "your_dev_app_id",
29
+ "loader": "EMP"
29
30
  }
30
31
  ```
31
32
  ### 初始化项目
32
33
  ```bash
33
- qdmp-cli create [options] <app-name>
34
+ qdmp-cli create <app-name> -t default
34
35
  ```
36
+ 新建项目使用官方 2.0(EMP)模板,创建后验证配置。`package.json.version` 是业务包版本,模板中的 `1.0.0` 不代表 1.0 小程序。保留模板的 loader,不通过改写该字段转换工程。
37
+
38
+ 关联 AppID 后执行 `qdmp-cli getMe` 核对本地与平台应用类型;上传前也会校验一致性。仅支持 2.0(EMP)项目,不再支持 SPA/effuse 工程;构建必须完成 EMP 编译。
39
+
35
40
  ### 拉取配置
36
41
  ```bash
37
42
  qdmp-cli init -a appId
package/actions.js CHANGED
@@ -7,7 +7,6 @@ import chalk from "chalk";
7
7
  import table from "table";
8
8
  import { inquirerVersionDescription } from "./utils/interactive.js";
9
9
  import { ensureInteractiveAuth, interactiveLogin } from "./utils/authFlow.js";
10
- import { agentLogin } from "./utils/agentLogin.js";
11
10
  import { getDeveloperId, getUserInfo } from "./api.js";
12
11
  import fs from "fs";
13
12
  import path from "path";
@@ -20,10 +19,11 @@ import {
20
19
  preUpload,
21
20
  cleanWorkSpaceFolder,
22
21
  } from "./utils/common.js";
22
+ import { readProjectLoader, assertMatchingLoader, assertEMPTemplate } from "./utils/projectLoader.js";
23
23
  import { assertCompilerRuntime } from "./utils/compilerRuntime.js";
24
24
  import { getAppInfo, uploadVersion, uploadApp } from "./api.js";
25
25
 
26
- function createProject(appName, template) {
26
+ async function createProject(appName, template) {
27
27
  printLogo();
28
28
  if (template) {
29
29
  console.log("\n" + chalk.dim("─".repeat(60)) + "\n");
@@ -36,7 +36,9 @@ function createProject(appName, template) {
36
36
  info(` ${chalk.dim("仓库:")} ${chalk.green(template.value)}`);
37
37
  info(`⏳ 正在创建 ${chalk.bold(appName)} 项目...`);
38
38
  console.log("\n" + chalk.dim("─".repeat(60)) + "\n");
39
- download(appName, template.value);
39
+ const targetPath = await download(appName, template.value);
40
+ assertEMPTemplate(targetPath);
41
+ success("已验证 2.0(EMP)模板;package.json.version 不代表小程序运行时版本。");
40
42
  } else {
41
43
  listAction();
42
44
  error(`未找到模板`);
@@ -51,9 +53,13 @@ export const initAction = async (appName, option) => {
51
53
  if (appName.match(invalidChars)) {
52
54
  error("名称不能包含中文及特殊字符");
53
55
  }
54
- const inputTemplate = option.template || TEMPLATES[0].name;
56
+ const inputTemplate = option.template || "default";
55
57
  const template = TEMPLATES.find((t) => t.name === inputTemplate);
56
- await createProject(appName, template);
58
+ try {
59
+ await createProject(appName, template);
60
+ } catch (e) {
61
+ error(e.message);
62
+ }
57
63
  };
58
64
 
59
65
  export const listAction = () => {
@@ -80,21 +86,12 @@ export const listAction = () => {
80
86
 
81
87
  export const loginAction = async (option) => {
82
88
  try {
83
- if (option.agent) {
84
- await agentLogin(option.env);
85
- return;
86
- }
87
89
  if (!process.stdin.isTTY || !process.stdout.isTTY) {
88
- throw new Error(`Agent 对话请执行 qdmp login --agent --env ${option.env} 获取登录链接和图片;CI 请设置 QDMP_TOKEN`);
90
+ throw new Error("qdmp login 需要在支持交互输入的终端中执行,或使用 QDMP_TOKEN");
89
91
  }
90
92
  await interactiveLogin(option.env);
91
93
  success("登录成功");
92
94
  } catch (e) {
93
- if (option.agent) {
94
- console.log(JSON.stringify({ type: "error", env: option.env, message: e.message }));
95
- process.exitCode = 1;
96
- return;
97
- }
98
95
  error(e.message);
99
96
  } finally {
100
97
  // Inquirer may leave stdin in a flowing state after the localhost callback
@@ -110,6 +107,9 @@ export const getMyInfoAction = async (option) => {
110
107
  const appInfo = await getAppInfo(appId, option.env);
111
108
  info(`当前用户:${nickname}`);
112
109
  info(`当前授权应用:${appInfo?.name} - ${appId}`);
110
+ const localLoader = readProjectLoader();
111
+ info(`小程序类型:${appInfo?.loader === "EMP" ? "2.0(EMP)" : "不支持"}`);
112
+ assertMatchingLoader(localLoader, appInfo?.loader);
113
113
  } catch (e) {
114
114
  error(e?.message);
115
115
  }
@@ -127,6 +127,7 @@ export async function uploadAction(option) {
127
127
  if (!remoteApp) {
128
128
  error("未找到应用,请检查appId是否正确");
129
129
  }
130
+ assertMatchingLoader(readProjectLoader(), remoteApp.loader);
130
131
  const description = option.description?.trim()
131
132
  ?? (process.stdin.isTTY ? await inquirerVersionDescription() : "");
132
133
  if (Array.from(description).length > 200) {
@@ -177,9 +178,12 @@ export const initConfigAction = async (option) => {
177
178
  const appId = option.appId;
178
179
  if (!appId) return error("未配置应用ID,请使用-a参数指定应用ID");
179
180
  const appInfo = await getAppInfo(appId, option.env);
181
+ if (!appInfo || appInfo.loader !== "EMP") {
182
+ throw new Error("仅支持平台 EMP 应用,未写入配置");
183
+ }
180
184
  const config = {
181
185
  name: appInfo.name,
182
- appId: appInfo.appId,
186
+ [option.env === "dev" ? "devAppId" : "appId"]: appInfo.appId,
183
187
  loader: appInfo.loader,
184
188
  latest: appInfo.latest,
185
189
  };
@@ -197,6 +201,7 @@ export const buildAction = async () => {
197
201
  return;
198
202
  }
199
203
  const config = JSON.parse(fs.readFileSync(qdmpJsonPath, "utf8"));
204
+ readProjectLoader();
200
205
  switch (config.loader) {
201
206
  case "EMP":
202
207
  await assertCompilerRuntime();
@@ -236,7 +241,7 @@ export const buildAction = async () => {
236
241
 
237
242
  default:
238
243
  error(
239
- `不支持的构建类型:${option.loader} 如果是空请检查qdmp.json配置文件是否正确`,
244
+ `不支持的构建类型:${config.loader ?? "未配置"},请检查 qdmp.json;2.0 必须使用 EMP,不能改用 H5 绕过`,
240
245
  );
241
246
  break;
242
247
  }
package/api.js CHANGED
@@ -211,10 +211,10 @@ const unwrapData = (result) => result?.data || result;
211
211
  const qrcodeLoginURL = (qrcodeId) =>
212
212
  `https://qiandao.com/login/scanning/confirm?qrcodeId=${encodeURIComponent(qrcodeId)}`;
213
213
 
214
- export const generateLoginQrcode = async (env = "prod", { signal } = {}) => {
214
+ export const generateLoginQrcode = async (env = "prod") => {
215
215
  const result = await request(
216
216
  "/signin/login/scan/qrcode/generate-id",
217
- { method: "POST", body: JSON.stringify({}), signal },
217
+ { method: "POST", body: JSON.stringify({}) },
218
218
  env,
219
219
  false,
220
220
  );
@@ -223,10 +223,10 @@ export const generateLoginQrcode = async (env = "prod", { signal } = {}) => {
223
223
  return { qrcodeId: String(data.qrcodeId), url: qrcodeLoginURL(data.qrcodeId) };
224
224
  };
225
225
 
226
- export const getLoginQrcodeStatus = async (qrcodeId, env = "prod", { signal } = {}) => {
226
+ export const getLoginQrcodeStatus = async (qrcodeId, env = "prod") => {
227
227
  const result = await request(
228
228
  "/signin/login/scan/qrcode/status",
229
- { method: "POST", body: JSON.stringify({ qrcodeId }), signal },
229
+ { method: "POST", body: JSON.stringify({ qrcodeId }) },
230
230
  env,
231
231
  false,
232
232
  );
@@ -259,17 +259,17 @@ export const loginByPlatformURL = async (env = "prod", onUpdate = () => {}) => {
259
259
  }
260
260
  };
261
261
 
262
- export const getQiandaoUser = async (token, env = "prod", { signal } = {}) => {
262
+ export const getQiandaoUser = async (token, env = "prod") => {
263
263
  const result = await request(
264
264
  "/user/users/me",
265
- { headers: { authorization: `Bearer ${token}` }, signal },
265
+ { headers: { authorization: `Bearer ${token}` } },
266
266
  env,
267
267
  false,
268
268
  );
269
269
  return unwrapData(result);
270
270
  };
271
271
 
272
- export const exchangeQiandaoLogin = async (token, user, env = "prod", { signal, persist = true } = {}) => {
272
+ export const exchangeQiandaoLogin = async (token, user, env = "prod") => {
273
273
  const userInfo = unwrapData(user) || {};
274
274
  const thirdPartyUserId = userInfo.id ?? userInfo.userId ?? userInfo.openId;
275
275
  if (thirdPartyUserId == null || String(thirdPartyUserId).trim() === "") {
@@ -279,7 +279,6 @@ export const exchangeQiandaoLogin = async (token, user, env = "prod", { signal,
279
279
  "/mp/v1/user/login/mobile",
280
280
  {
281
281
  method: "POST",
282
- signal,
283
282
  body: JSON.stringify({
284
283
  thirdPartyUserId: String(thirdPartyUserId),
285
284
  nickname: userInfo.nickName || userInfo.nickname || userInfo.name,
@@ -291,7 +290,7 @@ export const exchangeQiandaoLogin = async (token, user, env = "prod", { signal,
291
290
  );
292
291
  const platformToken = unwrapData(result)?.token;
293
292
  if (!platformToken) throw new Error("扫码登录未获取到开放平台 Token");
294
- if (persist) saveToken(platformToken, env);
293
+ saveToken(platformToken, env);
295
294
  return platformToken;
296
295
  };
297
296
 
package/constants.js CHANGED
@@ -6,18 +6,7 @@ export const TEMPLATES = [
6
6
  emoji: "🟢",
7
7
  fullUrl: true,
8
8
  },
9
- // {
10
- // name: "qdmp",
11
- // value: "frontend/miniapp-taro-template",
12
- // desc: "千岛小程序默认模版(1.0)",
13
- // emoji: "🟡",
14
- // },
15
- // {
16
- // name: "h5",
17
- // value: "frontend/h5-template",
18
- // desc: "h5模版",
19
- // emoji: "🔵",
20
- // },
9
+
21
10
  ];
22
11
 
23
12
  export const WORKSPACE_DIR = "qdmp-output";
package/index.js CHANGED
@@ -53,7 +53,6 @@ program
53
53
  .command("login")
54
54
  .description("登录")
55
55
  .option("-e, --env <env>", "登录环境:prod 或 dev", "prod")
56
- .option("--agent", "输出登录链接和 PNG 图片的 JSON 事件,并等待登录(无需终端输入)")
57
56
  .action(loginAction);
58
57
  program
59
58
  .command("getMe")
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "qdmp-cli",
3
- "version": "0.1.28",
3
+ "version": "0.1.29",
4
4
  "description": "qdmp-cli",
5
5
  "main": "index.js",
6
6
  "type": "module",
@@ -37,7 +37,6 @@
37
37
  "is-unicode-supported": "^2.1.0",
38
38
  "js-base64": "3.7.8",
39
39
  "ora": "^8.2.0",
40
- "qrcode": "^1.5.4",
41
40
  "qrcode-terminal": "^0.12.0",
42
41
  "query-string": "7.1.3",
43
42
  "shelljs": "^0.10.0",
@@ -46,9 +45,5 @@
46
45
  },
47
46
  "engines": {
48
47
  "node": "^20.19.0 || >=22.12.0"
49
- },
50
- "publishConfig": {
51
- "registry": "https://registry.npmjs.org/",
52
- "access": "public"
53
48
  }
54
49
  }
package/utils/authFlow.js CHANGED
@@ -87,7 +87,7 @@ export const ensureInteractiveAuth = async (env = "prod") => {
87
87
  } catch (cause) {
88
88
  if (!isAuthRequiredError(cause)) throw cause;
89
89
  if (!process.stdin.isTTY || !process.stdout.isTTY) {
90
- throw new Error(`当前命令需要登录。Agent 对话请执行 qdmp login --agent --env ${normalizeLoginEnv(env)} 获取登录链接和图片,登录后重试原命令;CI 请设置 QDMP_TOKEN`);
90
+ throw new Error("当前命令需要登录,请在支持交互输入的终端中重新执行,或设置 QDMP_TOKEN");
91
91
  }
92
92
  await interactiveLogin(env);
93
93
  return ensureAuth(env);
@@ -20,7 +20,7 @@ const readConfig = () => {
20
20
  }
21
21
  };
22
22
 
23
- export const saveToken = (token, env = "prod", { quiet = false } = {}) => {
23
+ export const saveToken = (token, env = "prod") => {
24
24
  normalizeLoginEnv(env);
25
25
  const configPath = getConfigPath();
26
26
  const config = readConfig();
@@ -32,7 +32,7 @@ export const saveToken = (token, env = "prod", { quiet = false } = {}) => {
32
32
  try {
33
33
  fs.writeFileSync(configPath, JSON.stringify(nextConfig, null, 2), { mode: 0o600 });
34
34
  fs.chmodSync(configPath, 0o600);
35
- if (!quiet) info(`🔑 ${env} 环境认证令牌已安全存储`);
35
+ info(`🔑 ${env} 环境认证令牌已安全存储`);
36
36
  } catch (err) {
37
37
  throw new Error(`保存 ${env} 环境认证令牌失败:${err.message}`);
38
38
  }
package/utils/common.js CHANGED
@@ -68,20 +68,6 @@ export const isUrlAccessible = async (url) => {
68
68
  }
69
69
  };
70
70
 
71
- /** 重命名文件夹
72
- * @param {*} dirname 旧文件夹名
73
- * @param {*} newname 新文件夹名
74
- */
75
- export async function renameDistFolder(dirname, newname) {
76
- try {
77
- if (fs.existsSync(newname)) return true;
78
- fs.renameSync(dirname, newname);
79
- return true;
80
- } catch (e) {
81
- error("重命名文件夹失败:", e);
82
- return false;
83
- }
84
- }
85
71
  /** 创建工作文件夹
86
72
  * @param {*} dirname 文件夹名
87
73
  */
@@ -105,69 +91,15 @@ export async function createWorkSpaceFolder(dirname = "") {
105
91
  // 压缩项目
106
92
  async function compressProject(output, dirname = "", loader, appId) {
107
93
  try {
108
- if (loader !== "EMP") {
109
- const zip = new AdmZip();
110
- const h5Path = path.join(dirname, "h5");
111
-
112
- if (!fs.existsSync(h5Path)) {
113
- throw new Error("h5 目录不存在");
114
- }
115
-
116
- // 添加文件夹时指定目标路径为 h5
117
- zip.addLocalFolder(h5Path, "h5");
118
- zip.writeZip(path.join(dirname, `${output}.zip`));
119
- } else {
120
- // 直接压缩dist
121
- const zip = new AdmZip();
122
- zip.addLocalFolder(path.join(dirname, "dist"), `${appId}`);
123
- zip.writeZip(path.join(dirname, `${output}.zip`));
124
- }
94
+ if (loader !== "EMP") throw new Error("仅支持 EMP 产物");
95
+ const zip = new AdmZip();
96
+ zip.addLocalFolder(path.join(dirname, "dist"), `${appId}`);
97
+ zip.writeZip(path.join(dirname, `${output}.zip`));
125
98
  return true;
126
99
  } catch (error) {
127
100
  return false;
128
101
  }
129
102
  }
130
- /** 复制文件夹
131
- * @param {*} dirname 文件夹名
132
- */
133
- async function copyDistFolder(dirname) {
134
- try {
135
- info("正在查找 index.html 文件...");
136
- let indexPath = null;
137
- function findIndexHtml(dir) {
138
- const files = fs.readdirSync(dir);
139
- for (const file of files) {
140
- const fullPath = path.join(dir, file);
141
- const stat = fs.statSync(fullPath);
142
- if (stat.isDirectory()) {
143
- const found = findIndexHtml(fullPath);
144
- if (found) return found;
145
- } else if (file === "index.html") {
146
- return fullPath;
147
- }
148
- }
149
- return null;
150
- }
151
- indexPath = findIndexHtml(dirname);
152
- if (!indexPath) {
153
- throw new Error("未找到index.html文件");
154
- }
155
- success("找到 index.html 文件");
156
-
157
- const parentDir = path.dirname(indexPath);
158
- const targetDir = path.join(WORKSPACE_DIR, "dist");
159
-
160
- info("正在复制文件...");
161
- // 直接复制整个目录
162
- fs.cpSync(parentDir, targetDir, { recursive: true });
163
- success("文件复制完成");
164
- return true;
165
- } catch (e) {
166
- error(e);
167
- return false;
168
- }
169
- }
170
-
171
103
  function hasCompiledMainDir(dirname) {
172
104
  try {
173
105
  return fs.statSync(path.join(dirname, "main")).isDirectory();
@@ -250,7 +182,6 @@ async function copyAndSetEmpDistFolder(appId, code) {
250
182
  * 预上传准备工作
251
183
  * 1. 创建工作空间
252
184
  * 2. 复制构建产物
253
- * 3. 重命名文件夹(非EMP构建模式)
254
185
  * 4. 压缩项目
255
186
  * @param {string} loader - 构建加载器类型
256
187
  * @param {string} targetName - 目标文件名
@@ -258,6 +189,7 @@ async function copyAndSetEmpDistFolder(appId, code) {
258
189
  */
259
190
  export async function preUpload(loader, targetName, appId, code) {
260
191
  try {
192
+ if (loader !== "EMP") throw new Error("仅支持 2.0(EMP)小程序上传");
261
193
  // 1. 创建工作空间文件夹
262
194
  if (!(await createWorkSpaceFolder(WORKSPACE_DIR))) {
263
195
  error("创建工作空间失败");
@@ -265,10 +197,7 @@ export async function preUpload(loader, targetName, appId, code) {
265
197
  }
266
198
 
267
199
  // 2. 根据构建类型复制对应产物
268
- const isCopySuccess =
269
- loader === "EMP"
270
- ? await copyAndSetEmpDistFolder(appId, code)
271
- : await copyAndRenameDistFolder();
200
+ const isCopySuccess = await copyAndSetEmpDistFolder(appId, code);
272
201
 
273
202
  if (!isCopySuccess) {
274
203
  error("复制构建产物失败");
@@ -278,7 +207,7 @@ export async function preUpload(loader, targetName, appId, code) {
278
207
  // 3. 复制 qdmp.json 到产物目录
279
208
  const qdmpJsonPath = path.resolve(process.cwd(), "qdmp.json");
280
209
  if (fs.existsSync(qdmpJsonPath)) {
281
- const distDir = loader === "EMP" ? "dist" : "h5";
210
+ const distDir = "dist";
282
211
  fs.copyFileSync(qdmpJsonPath, path.join(WORKSPACE_DIR, distDir, "qdmp.json"));
283
212
  info("已将 qdmp.json 复制到产物目录");
284
213
  }
@@ -299,23 +228,6 @@ export async function preUpload(loader, targetName, appId, code) {
299
228
  }
300
229
  }
301
230
 
302
- /**
303
- * 复制并创建标准H5文件夹结构
304
- * 对于非EMP构建模式,复制dist内容并重命名为h5
305
- * @returns {Promise<boolean>} 操作是否成功
306
- */
307
- async function copyAndRenameDistFolder() {
308
- try {
309
- if (!(await copyDistFolder("dist"))) {
310
- return false;
311
- }
312
-
313
- return renameDistFolder(`${WORKSPACE_DIR}/dist`, `${WORKSPACE_DIR}/h5`);
314
- } catch (err) {
315
- error("复制并重命名文件夹过程出错:", err);
316
- return false;
317
- }
318
- }
319
231
  /** 清理工作文件夹
320
232
  * @param {*} dirname 文件夹名
321
233
  */
package/utils/gitClone.js CHANGED
@@ -23,15 +23,16 @@ export const download = async (projectName, repo) => {
23
23
  `文件夹 ${projectName} 已存在,是否创建新名称: ${name}`
24
24
  );
25
25
  if (confirm) {
26
- clone(repo, name);
26
+ await clone(repo, name);
27
27
  } else {
28
28
  error(`文件夹 ${projectName} 已存在, 请更名后重新创建`);
29
29
  }
30
30
  } else {
31
- clone(repo, name);
31
+ await clone(repo, name);
32
32
  }
33
- } catch (error) {
34
- error(`${error.message}`);
33
+ return path.join(process.cwd(), name);
34
+ } catch (err) {
35
+ throw err;
35
36
  }
36
37
  };
37
38
 
@@ -81,5 +82,6 @@ const clone = async (repo, name) => {
81
82
  spinner.succeed(chalk.green(`模版创建成功!请进入 ${name} 目录开始开发`));
82
83
  } catch (err) {
83
84
  spinner.fail(chalk.red(`模版创建失败: ${err.message}`));
85
+ throw err;
84
86
  }
85
87
  };
@@ -0,0 +1,36 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+
4
+ export function readProjectLoader(cwd = process.cwd()) {
5
+ const config = JSON.parse(fs.readFileSync(path.join(cwd, "qdmp.json"), "utf8"));
6
+ if (config.loader !== "EMP") {
7
+ throw new Error("仅支持 2.0(EMP)小程序;qdmp.json 的 loader 必须为 EMP。不支持 SPA/effuse 工程,不能只修改 loader 转换工程。");
8
+ }
9
+ const packagePath = path.join(cwd, "package.json");
10
+ if (fs.existsSync(packagePath)) {
11
+ const pkg = JSON.parse(fs.readFileSync(packagePath, "utf8"));
12
+ for (const field of ["dependencies", "devDependencies", "optionalDependencies", "peerDependencies"]) {
13
+ for (const [name, version] of Object.entries(pkg[field] || {})) {
14
+ if (/effuse|effues/i.test(name) || /effuse|effues/i.test(String(version))) {
15
+ throw new Error(`不支持 effuse/effues 依赖:${name}。请使用官方 2.0 模板迁移业务,不要仅修改 loader。`);
16
+ }
17
+ }
18
+ }
19
+ }
20
+ return config.loader;
21
+ }
22
+
23
+ export function assertMatchingLoader(localLoader, remoteLoader) {
24
+ if (localLoader !== "EMP" || remoteLoader !== "EMP") {
25
+ throw new Error(`应用类型不一致或不受支持(仅支持 EMP):本地 ${localLoader},平台 ${remoteLoader ?? "未提供"}。请关联匹配的应用或完成工程迁移,不能仅修改 loader。`);
26
+ }
27
+ }
28
+
29
+ export function assertEMPTemplate(cwd) {
30
+ if (readProjectLoader(cwd) !== "EMP") {
31
+ throw new Error("创建结果不是 2.0(EMP)模板,请检查官方模板来源。");
32
+ }
33
+ const pkg = JSON.parse(fs.readFileSync(path.join(cwd, "package.json"), "utf8"));
34
+ if (!pkg.scripts?.build) throw new Error("模板缺少 build 脚本。");
35
+ // package.json.version 是业务包版本,不是小程序运行时版本。
36
+ }
@@ -1,106 +0,0 @@
1
- import { mkdtemp, chmod, rm } from "node:fs/promises";
2
- import { tmpdir } from "node:os";
3
- import { join } from "node:path";
4
- import { setTimeout as delay } from "node:timers/promises";
5
- import QRCode from "qrcode";
6
- import { generateLoginQrcode, getLoginQrcodeStatus, getQiandaoUser, exchangeQiandaoLogin, request } from "../api.js";
7
- import { saveToken } from "./authorized.js";
8
- import { normalizeLoginEnv } from "./loginEnvironment.js";
9
- import { startLocalhostLogin } from "./localhostLogin.js";
10
-
11
- // NDJSON keeps stdout machine-readable. The caller renders ready.markdown while
12
- // this process keeps the loopback callback and App status polling alive.
13
- export const agentLogin = async (env = "prod", {
14
- emit = event => console.log(JSON.stringify(event)),
15
- timeoutMs = 120000,
16
- pollIntervalMs = 1500,
17
- } = {}) => {
18
- normalizeLoginEnv(env);
19
- const controller = new AbortController();
20
- const { signal } = controller;
21
- const expiresAt = new Date(Date.now() + timeoutMs).toISOString();
22
- const timer = setTimeout(() => controller.abort(new Error("登录超时,请重新执行登录命令")), timeoutMs);
23
- const interrupt = () => controller.abort(new Error("已取消登录"));
24
- process.once("SIGINT", interrupt);
25
- process.once("SIGTERM", interrupt);
26
- let browserSession;
27
- let imageDirectory;
28
- let committed = false;
29
- const closeBrowser = () => browserSession?.close();
30
- signal.addEventListener("abort", closeBrowser, { once: true });
31
- const commit = token => {
32
- signal.throwIfAborted();
33
- if (committed) throw new Error("登录已完成");
34
- saveToken(token, env, { quiet: true });
35
- committed = true;
36
- };
37
- const requestSignal = () => AbortSignal.any([signal, AbortSignal.timeout(10000)]);
38
- try {
39
- browserSession = await startLocalhostLogin({
40
- env, timeoutMs,
41
- acceptToken: async (token, isActive) => {
42
- const result = await request("/mp/v1/user/me", {
43
- headers: { authorization: `Bearer ${token}` }, signal: requestSignal(),
44
- }, env, false);
45
- if (Number(result?.code || 0) !== 0) throw new Error("验证网页登录态失败");
46
- if (!isActive()) throw new Error("网页登录会话已结束");
47
- commit(token);
48
- },
49
- });
50
- signal.throwIfAborted();
51
- const qr = await generateLoginQrcode(env, { signal: requestSignal() });
52
- imageDirectory = await mkdtemp(join(tmpdir(), "qdmp-login-"));
53
- const qrImagePath = join(imageDirectory, "qrcode.png");
54
- await QRCode.toFile(qrImagePath, qr.url, { type: "png", width: 320, margin: 4 });
55
- await chmod(qrImagePath, 0o600);
56
- signal.throwIfAborted();
57
- const loginUrl = browserSession.url;
58
- await emit({
59
- type: "ready", env, loginUrl, qrImagePath, expiresAt,
60
- message: "点击链接前往浏览器登录,或用千岛 App 扫码登录。",
61
- markdown: `[点击链接前往浏览器登录](${loginUrl}),或用千岛 App 扫码登录。\n\n![千岛 App 扫码登录](<${qrImagePath}>)`,
62
- });
63
- const poll = async () => {
64
- let previousStatus;
65
- while (!signal.aborted && !committed) {
66
- const result = await getLoginQrcodeStatus(qr.qrcodeId, env, { signal: requestSignal() });
67
- signal.throwIfAborted();
68
- if (result.status === "success") {
69
- if (!result.token) throw new Error("扫码登录未返回登录态");
70
- const user = await getQiandaoUser(result.token, env, { signal: requestSignal() });
71
- const token = await exchangeQiandaoLogin(result.token, user, env, { signal: requestSignal(), persist: false });
72
- commit(token);
73
- return "qrcode";
74
- }
75
- if (["expired", "canceled", "cancelled", "error"].includes(result.status)) {
76
- throw new Error("二维码已过期或登录已取消,可继续使用浏览器链接");
77
- }
78
- if (previousStatus !== result.status) {
79
- previousStatus = result.status;
80
- await emit({ type: "status", env, method: "qrcode", status: result.status });
81
- }
82
- await delay(pollIntervalMs, undefined, { signal });
83
- }
84
- throw new Error("扫码登录会话已结束");
85
- };
86
- const lane = (promise, method) => promise.catch(async () => {
87
- if (!signal.aborted && !committed) await emit({ type: "unavailable", env, method });
88
- throw new Error(`${method} 登录未完成`);
89
- });
90
- const method = await Promise.any([
91
- lane(browserSession.result.then(() => "browser"), "browser"),
92
- lane(poll(), "qrcode"),
93
- ]).catch(() => {
94
- throw signal.reason || new Error("登录未完成,请重新执行登录命令");
95
- });
96
- await emit({ type: "success", env, method });
97
- } finally {
98
- controller.abort();
99
- clearTimeout(timer);
100
- closeBrowser();
101
- process.removeListener("SIGINT", interrupt);
102
- process.removeListener("SIGTERM", interrupt);
103
- signal.removeEventListener("abort", closeBrowser);
104
- if (imageDirectory) await rm(imageDirectory, { recursive: true, force: true });
105
- }
106
- };