cardbuddy 0.22.0 → 0.23.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +11 -11
- package/dist/cli.js +2 -2
- package/dist/config.js +3 -3
- package/dist/init.js +13 -13
- package/dist/mcp.js +1 -1
- package/dist/preview-editor/assets/{index-CU8zVA2P.css → index-7RzNd7tf.css} +1 -1
- package/dist/preview-editor/assets/{index-CPpLZZyi.js → index-C7N5n_qW.js} +80 -80
- package/dist/preview-editor/index.html +2 -2
- package/dist/skills-upgrade.js +1 -1
- package/dist/standalone.js +2 -2
- package/dist/vendored/card-editor/runtime/adapter.js +10 -11
- package/dist/vendored/mcp/client.mjs +50 -13
- package/dist/vendored/mcp/tools.mjs +13 -7
- package/dist/vendored/server/core/ai-chat.js +540 -183
- package/dist/vendored/server/core/assetProxy.js +1 -1
- package/dist/vendored/server/core/browser.js +1 -1
- package/dist/vendored/server/core/cardDigest.js +324 -0
- package/dist/vendored/server/core/cardExport.js +5 -5
- package/dist/vendored/server/core/cardFiles.js +1 -1
- package/dist/vendored/server/core/cardHistoryRemote.js +1 -1
- package/dist/vendored/server/core/cardImportMeta.js +1 -1
- package/dist/vendored/server/core/cardListCache.js +92 -10
- package/dist/vendored/server/core/cardPublish.js +11 -6
- package/dist/vendored/server/core/cardPublishVersions.js +8 -8
- package/dist/vendored/server/core/cardSave.js +14 -10
- package/dist/vendored/server/core/cardSidecar.js +3 -3
- package/dist/vendored/server/core/cardSync.js +35 -23
- package/dist/vendored/server/core/cardVersion.js +1 -1
- package/dist/vendored/server/core/index.js +6 -6
- package/dist/vendored/server/core/logger.js +1 -1
- package/dist/vendored/server/core/remoteCard.js +5 -5
- package/dist/vendored/server/core/skillPaths.js +1 -1
- package/dist/vendored/server/core/sliceUpload.js +4 -4
- package/dist/vendored/skills/README.md +13 -13
- package/dist/vendored/skills/card-doctor/SKILL.md +2 -2
- package/dist/vendored/skills/dsl-card-ai/SKILL.md +4 -4
- package/dist/vendored/skills/dsl-card-generator/SKILL.md +2 -2
- package/dist/vendored/skills/dsl-card-generator/references/amount_formatting.md +1 -1
- package/dist/vendored/skills/dsl-card-generator/references/build_protocol.md +54 -0
- package/dist/vendored/skills/dsl-card-generator/references/component_rules.md +2 -2
- package/dist/vendored/skills/dsl-card-generator/references/converge_protocol.md +36 -0
- package/dist/vendored/skills/dsl-card-generator/references/event_patterns.md +1 -1
- package/dist/vendored/skills/dsl-card-generator/references/generation_pitfalls.md +1 -1
- package/dist/vendored/skills/dsl-card-generator/references/image_to_card.md +1 -1
- package/dist/vendored/skills/dsl-card-generator/references/quickref.md +1 -1
- package/dist/vendored/skills/dsl-card-generator/references/recipes.md +4 -4
- package/dist/vendored/skills/dsl-card-generator/references/rules.md +1 -1
- package/dist/vendored/skills/dsl-card-generator/references/slices_convention.md +6 -6
- package/dist/vendored/skills/dsl-card-generator/scripts/inventory_schemas.py +1 -1
- package/dist/vendored/skills/dsl-card-generator/scripts/lib/scaffold.mjs +1 -1
- package/dist/vendored/skills/dsl-card-generator/scripts/lib/slice_dedupe.mjs +2 -2
- package/dist/vendored/skills/dsl-card-generator/vendor/preview/assets/standalone-CeVX_y-M.js +137 -0
- package/dist/vendored/skills/dsl-card-generator/vendor/preview/assets/standalone-DiEiY59H.css +1 -0
- package/dist/vendored/skills/dsl-card-generator/vendor/preview/standalone.html +2 -2
- package/dist/vendored/skills/{finmall-card-sync → platform-card-sync}/SKILL.md +26 -26
- package/dist/vendored/skills/{finmall-card-sync → platform-card-sync}/scripts/api_batch_sync.mjs +184 -162
- package/dist/vendored/skills/{finmall-card-sync → platform-card-sync}/scripts/slice_upload.mjs +48 -29
- package/dist/vendored/skills/{finmall-session-navigator → platform-session-navigator}/SKILL.md +13 -13
- package/dist/vendored/skills/{finmall-session-navigator → platform-session-navigator}/scripts/package-lock.json +2 -2
- package/dist/vendored/skills/{finmall-session-navigator → platform-session-navigator}/scripts/package.json +2 -2
- package/dist/vendored/skills/{finmall-session-navigator/scripts/finmall_login.mjs → platform-session-navigator/scripts/platform_login.mjs} +41 -31
- package/dist/vendored/skills/{finmall-session-navigator → platform-session-navigator}/scripts/session-manager.mjs +17 -17
- package/dist/vendored/skills/shared/README.md +21 -21
- package/dist/vendored/skills/shared/auth-model.md +8 -10
- package/dist/vendored/skills/shared/{finmall-config.mjs → platform-config.mjs} +10 -10
- package/dist/vendored/tools/{finmall-env.mjs → platform-env.mjs} +13 -13
- package/package.json +1 -1
- package/dist/vendored/skills/dsl-card-generator/vendor/preview/assets/standalone-BCLshIzC.css +0 -1
- package/dist/vendored/skills/dsl-card-generator/vendor/preview/assets/standalone-pP5kTKiI.js +0 -88
package/dist/vendored/skills/{finmall-card-sync → platform-card-sync}/scripts/slice_upload.mjs
RENAMED
|
@@ -2,14 +2,14 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* slice_upload —— 切图上传(slices_convention.md 的「③ 远程同步」阶段实现)。
|
|
4
4
|
*
|
|
5
|
-
* 把 `src/slices/`
|
|
5
|
+
* 把 `src/slices/` 下被卡片引用的切图上传到远端图片库(`CC0000.do`,multipart:
|
|
6
6
|
* `fileName` + `file`),拿到线上地址(形如 `@filestore/dzqd-download/<ts>_<name>.png`),
|
|
7
7
|
* 然后做两件事:
|
|
8
8
|
* 1. 写「本地文件 ↔ 线上地址」映射:`src/slices/manifest.json`;
|
|
9
9
|
* 2. 把卡片 `schema.json` / `data.json` 里的 `"/slices/<name>"` 引用改写为该线上地址
|
|
10
10
|
* (运行时 `adapter.doReplaceUrl` 再把 `@filestore/x` 解析成 `/minio/x`,预览链路无需改动)。
|
|
11
11
|
*
|
|
12
|
-
* 鉴权与会话复用与 api_batch_sync.mjs 完全一致:
|
|
12
|
+
* 鉴权与会话复用与 api_batch_sync.mjs 完全一致:ensurePlatformSession 拉起/复用后台无界面
|
|
13
13
|
* Chrome,token(localStorage `_sessionId`)只在页面上下文内读取并直接用于请求——
|
|
14
14
|
* **绝不回传 Node 侧、不打印、不落盘**(见 shared/auth-model.md §1.5)。
|
|
15
15
|
* 命中会话/权限失效(BLEC0006 等)时自动清 session 重登并重试一次。
|
|
@@ -22,16 +22,35 @@
|
|
|
22
22
|
* 避免卡片继续指向过期的线上图。
|
|
23
23
|
*
|
|
24
24
|
* 用法:
|
|
25
|
-
* node skills/
|
|
26
|
-
* node skills/
|
|
27
|
-
* node skills/
|
|
28
|
-
* node skills/
|
|
25
|
+
* node skills/platform-card-sync/scripts/slice_upload.mjs
|
|
26
|
+
* node skills/platform-card-sync/scripts/slice_upload.mjs --only=arrowRightGray.png,icon.png
|
|
27
|
+
* node skills/platform-card-sync/scripts/slice_upload.mjs --all --dry-run
|
|
28
|
+
* node skills/platform-card-sync/scripts/slice_upload.mjs --force --no-rewrite
|
|
29
29
|
*/
|
|
30
30
|
import { existsSync, readFileSync, readdirSync, statSync, writeFileSync, renameSync, mkdirSync } from 'node:fs';
|
|
31
31
|
import { createHash } from 'node:crypto';
|
|
32
32
|
import path from 'node:path';
|
|
33
33
|
import { fileURLToPath } from 'node:url';
|
|
34
|
-
import {
|
|
34
|
+
import { platformConfig } from '../../shared/platform-config.mjs';
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* 对端站点 host(从配置派生,不写死域名),用于在多个标签页里认出已登录的那个。
|
|
38
|
+
* 配置缺失时返回空串 → 判定一律 false,调用方回落到「取第一个页 / 新建页」。
|
|
39
|
+
*/
|
|
40
|
+
const PLATFORM_HOST = (() => {
|
|
41
|
+
try {
|
|
42
|
+
return new URL(platformConfig.baseUrl).host;
|
|
43
|
+
} catch {
|
|
44
|
+
return '';
|
|
45
|
+
}
|
|
46
|
+
})();
|
|
47
|
+
function isPlatformPage(page) {
|
|
48
|
+
try {
|
|
49
|
+
return Boolean(PLATFORM_HOST) && page.url().includes(PLATFORM_HOST);
|
|
50
|
+
} catch {
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
35
54
|
|
|
36
55
|
const scriptDir = path.dirname(fileURLToPath(import.meta.url));
|
|
37
56
|
const skillDir = path.resolve(scriptDir, '..');
|
|
@@ -55,13 +74,13 @@ try {
|
|
|
55
74
|
};
|
|
56
75
|
}
|
|
57
76
|
|
|
58
|
-
import {
|
|
77
|
+
import { ensurePlatformSession } from '../../platform-session-navigator/scripts/session-manager.mjs';
|
|
59
78
|
import {
|
|
60
79
|
performLogin,
|
|
61
80
|
clearTimeoutPrompts,
|
|
62
81
|
waitForAuthenticatedPage,
|
|
63
82
|
loadCredentials,
|
|
64
|
-
} from '../../
|
|
83
|
+
} from '../../platform-session-navigator/scripts/platform_login.mjs';
|
|
65
84
|
|
|
66
85
|
// 本机设了 HTTP_PROXY 时,Node→Chrome 的 CDP 连接(127.0.0.1)必须排除在代理外。
|
|
67
86
|
for (const key of ['NO_PROXY', 'no_proxy']) {
|
|
@@ -166,7 +185,7 @@ async function postUpload(page, { fileName, mime, base64, appId, workspaceId, en
|
|
|
166
185
|
};
|
|
167
186
|
const sessionToken = readSessionToken();
|
|
168
187
|
if (!sessionToken) {
|
|
169
|
-
throw new Error('页面缺少会话凭据(localStorage._sessionId 为空),请先运行
|
|
188
|
+
throw new Error('页面缺少会话凭据(localStorage._sessionId 为空),请先运行 platform-session-navigator 登录。');
|
|
170
189
|
}
|
|
171
190
|
const binary = atob(base64);
|
|
172
191
|
const bytes = new Uint8Array(binary.length);
|
|
@@ -212,7 +231,7 @@ function parseUploadResponse(text, fileName) {
|
|
|
212
231
|
const body = (parsed && parsed.body) || {};
|
|
213
232
|
const errorCode = header.errorCode || body.errorCode;
|
|
214
233
|
if (errorCode && errorCode !== '0') {
|
|
215
|
-
throw new Error(
|
|
234
|
+
throw new Error(`远端上传失败 [errorCode=${errorCode}] ${header.errorMsg || body.errorMsg || ''}`);
|
|
216
235
|
}
|
|
217
236
|
const list = Array.isArray(body.uploadFileList)
|
|
218
237
|
? body.uploadFileList
|
|
@@ -246,7 +265,7 @@ async function uploadWithHeal(page, payload) {
|
|
|
246
265
|
}
|
|
247
266
|
|
|
248
267
|
/**
|
|
249
|
-
*
|
|
268
|
+
* 上传单张图片到远端图片库(CC0000.do),返回 `@filestore/...` 站内地址。
|
|
250
269
|
*
|
|
251
270
|
* 与切图上传是同一条链路(同接口、同会话自愈、同响应解析),差异只在用途:
|
|
252
271
|
* 这里不写 manifest、不改写 schema —— 新建/编辑卡片抽屉只需要一个能写进 `picUrl` 的地址。
|
|
@@ -260,15 +279,15 @@ export async function uploadCardImage(input = {}) {
|
|
|
260
279
|
if (!fileName) throw new Error('缺少文件名,无法上传封面');
|
|
261
280
|
if (!input.base64) throw new Error('缺少图片内容,无法上传封面');
|
|
262
281
|
const mime = input.mime || IMAGE_MIME[path.extname(fileName).toLowerCase()] || 'image/png';
|
|
263
|
-
const appId = input.appId ||
|
|
264
|
-
const workspaceId = input.workspaceId ||
|
|
282
|
+
const appId = input.appId || platformConfig.appId;
|
|
283
|
+
const workspaceId = input.workspaceId || platformConfig.workspaceId;
|
|
265
284
|
|
|
266
|
-
const session = await
|
|
285
|
+
const session = await ensurePlatformSession();
|
|
267
286
|
const browser = session.browser;
|
|
268
287
|
try {
|
|
269
288
|
const context = browser.contexts()[0];
|
|
270
|
-
const page = context?.pages().find((p) => p
|
|
271
|
-
if (!page) throw new Error('未能在后台 Chrome
|
|
289
|
+
const page = context?.pages().find((p) => isPlatformPage(p)) || context?.pages()[0];
|
|
290
|
+
if (!page) throw new Error('未能在后台 Chrome 中找到远端页面,请检查凭据或重新触发上传。');
|
|
272
291
|
await page.bringToFront();
|
|
273
292
|
const result = await uploadWithHeal(page, {
|
|
274
293
|
fileName,
|
|
@@ -276,7 +295,7 @@ export async function uploadCardImage(input = {}) {
|
|
|
276
295
|
base64: input.base64,
|
|
277
296
|
appId,
|
|
278
297
|
workspaceId,
|
|
279
|
-
endpoint:
|
|
298
|
+
endpoint: platformConfig.uploadImgEndpoint,
|
|
280
299
|
});
|
|
281
300
|
return { url: result.url, dimensions: result.dimensions || '', fileName };
|
|
282
301
|
} finally {
|
|
@@ -466,7 +485,7 @@ function rewriteCardRefs(cardFiles, root, mapping) {
|
|
|
466
485
|
* @param {boolean} [opts.force] true=忽略 sha256 判重,强制重新上传
|
|
467
486
|
* @param {boolean} [opts.dryRun] true=只报告计划,不上传、不写盘
|
|
468
487
|
* @param {boolean} [opts.noRewrite] true=只上传 + 写映射,不改写卡片引用
|
|
469
|
-
* @param {string} [opts.appId] 覆盖 sassAppId(默认取 .env.cb 的
|
|
488
|
+
* @param {string} [opts.appId] 覆盖 sassAppId(默认取 .env.cb 的 PLATFORM_APP_ID)
|
|
470
489
|
* @param {string} [opts.workspaceId] 覆盖 sassWorkspaceId
|
|
471
490
|
* @param {string} [opts.slicesRoot] 切图目录(默认 <repo>/src/slices)
|
|
472
491
|
* @param {string} [opts.cardsRoot] 卡片目录(默认 <repo>/src/cards)
|
|
@@ -477,8 +496,8 @@ export async function uploadSlices(opts = {}) {
|
|
|
477
496
|
const slicesRoot = path.resolve(opts.slicesRoot || path.join(process.cwd(), 'src', 'slices'));
|
|
478
497
|
const cardsRoot = path.resolve(opts.cardsRoot || path.join(process.cwd(), 'src', 'cards'));
|
|
479
498
|
const manifestPath = path.join(slicesRoot, MANIFEST_FILE);
|
|
480
|
-
const appId = String(opts.appId ||
|
|
481
|
-
const workspaceId = String(opts.workspaceId ||
|
|
499
|
+
const appId = String(opts.appId || platformConfig.appId);
|
|
500
|
+
const workspaceId = String(opts.workspaceId || platformConfig.workspaceId);
|
|
482
501
|
const dryRun = !!opts.dryRun;
|
|
483
502
|
const force = !!opts.force;
|
|
484
503
|
const cardId = opts.cardId ? String(opts.cardId) : '';
|
|
@@ -550,7 +569,7 @@ export async function uploadSlices(opts = {}) {
|
|
|
550
569
|
const writeReusedEntry = (slice, fromName, fromEntry) => {
|
|
551
570
|
const reused = {
|
|
552
571
|
url: fromEntry.url,
|
|
553
|
-
httpUrl: fromEntry.httpUrl ||
|
|
572
|
+
httpUrl: fromEntry.httpUrl || platformConfig.filestoreUrl(fromEntry.url),
|
|
554
573
|
sha256: slice.sha256,
|
|
555
574
|
size: slice.size,
|
|
556
575
|
reusedFrom: fromName,
|
|
@@ -603,12 +622,12 @@ export async function uploadSlices(opts = {}) {
|
|
|
603
622
|
|
|
604
623
|
if (pending.length) {
|
|
605
624
|
log.info('待上传切图', { count: pending.length, appId, workspaceId });
|
|
606
|
-
const session = await
|
|
625
|
+
const session = await ensurePlatformSession();
|
|
607
626
|
const browser = session.browser;
|
|
608
627
|
try {
|
|
609
628
|
const context = browser.contexts()[0];
|
|
610
|
-
const page = context?.pages().find((p) => p
|
|
611
|
-
if (!page) throw new Error('未能在后台 Chrome
|
|
629
|
+
const page = context?.pages().find((p) => isPlatformPage(p)) || context?.pages()[0];
|
|
630
|
+
if (!page) throw new Error('未能在后台 Chrome 中找到远端页面,请检查凭据或重新触发上传。');
|
|
612
631
|
await page.bringToFront();
|
|
613
632
|
|
|
614
633
|
for (const slice of pending) {
|
|
@@ -619,7 +638,7 @@ export async function uploadSlices(opts = {}) {
|
|
|
619
638
|
base64: slice.base64,
|
|
620
639
|
appId,
|
|
621
640
|
workspaceId,
|
|
622
|
-
endpoint:
|
|
641
|
+
endpoint: platformConfig.uploadImgEndpoint,
|
|
623
642
|
});
|
|
624
643
|
const prev = manifest.files[slice.name];
|
|
625
644
|
// 内容变了会拿到新地址:旧地址进 superseded(上限 5),改写阶段一并替换,
|
|
@@ -630,7 +649,7 @@ export async function uploadSlices(opts = {}) {
|
|
|
630
649
|
: (prev && prev.superseded) || [];
|
|
631
650
|
const entry = {
|
|
632
651
|
url: result.url,
|
|
633
|
-
httpUrl:
|
|
652
|
+
httpUrl: platformConfig.filestoreUrl(result.url),
|
|
634
653
|
sha256: slice.sha256,
|
|
635
654
|
size: slice.size,
|
|
636
655
|
uploadedAt: new Date().toISOString(),
|
|
@@ -733,8 +752,8 @@ if (isMain) {
|
|
|
733
752
|
...(summary.dangling.length ? [`悬空引用(schema 引用但 src/slices 无此文件):${summary.dangling.join('、')}`] : []),
|
|
734
753
|
];
|
|
735
754
|
log.box({ title: '切图上传结果', level: summary.success ? 'ok' : 'warn', lines });
|
|
736
|
-
// 显式退出:
|
|
755
|
+
// 显式退出:ensurePlatformSession 内部的登录流程会另开一条 CDP 连接且按约定不关闭
|
|
737
756
|
// (后台 Chrome 要常驻复用),残留的 WebSocket 会让事件循环一直不空、进程挂住不退出。
|
|
738
|
-
// 与
|
|
757
|
+
// 与 platform_login.mjs CLI 同款处理——detached 的 Chrome 进程不受影响,继续运行。
|
|
739
758
|
process.exit(summary.success ? 0 : 1);
|
|
740
759
|
}
|
package/dist/vendored/skills/{finmall-session-navigator → platform-session-navigator}/SKILL.md
RENAMED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
---
|
|
2
|
-
name:
|
|
3
|
-
description: 使用 Playwright 独立启动 Chrome
|
|
2
|
+
name: platform-session-navigator
|
|
3
|
+
description: 使用 Playwright 独立启动 Chrome,自动登录远端 UAT 并打开卡片列表页,恢复超时会话并验证数据列表。不依赖 Codex 或任何浏览器插件,用户无需附加 @Chrome chip。
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
-
#
|
|
6
|
+
# 远端会话导航
|
|
7
7
|
|
|
8
|
-
通过 Playwright 独立启动 Chrome
|
|
8
|
+
通过 Playwright 独立启动 Chrome 浏览器,自动登录远端并打开卡片列表页。
|
|
9
9
|
|
|
10
10
|
## 环境配置
|
|
11
11
|
|
|
@@ -20,9 +20,9 @@ description: 使用 Playwright 独立启动 Chrome,自动登录 FinMall UAT
|
|
|
20
20
|
|
|
21
21
|
| 变量名 | 说明 | 默认值 |
|
|
22
22
|
|---------|------|--------|
|
|
23
|
-
| `
|
|
24
|
-
| `
|
|
25
|
-
| `
|
|
23
|
+
| `PLATFORM_BASE_URL` | 服务域名 | `https://uat-pkg.finmall.com` |
|
|
24
|
+
| `PLATFORM_APP_ID` | 应用 ID | `100042` |
|
|
25
|
+
| `PLATFORM_WORKSPACE_ID` | 工作空间 ID | `dev` |
|
|
26
26
|
| `CARDBUDDY_CDP_PORT` | Chrome 调试端口 | `9222` |
|
|
27
27
|
|
|
28
28
|
切换环境(如生产/测试)只需修改 `.env.cb.local`,无需改动代码。
|
|
@@ -33,7 +33,7 @@ description: 使用 Playwright 独立启动 Chrome,自动登录 FinMall UAT
|
|
|
33
33
|
|
|
34
34
|
1. 以独立进程启动系统 Chrome(或 Playwright 内置 Chromium),开启 `--remote-debugging-port=9222`。
|
|
35
35
|
2. 使用 Playwright `connectOverCDP` 连接到该 Chrome 实例。
|
|
36
|
-
3.
|
|
36
|
+
3. 在远端同源页面上下文中通过 `page.evaluate()` 注入登录请求。
|
|
37
37
|
4. 登录后刷新页面、验证列表渲染完成。
|
|
38
38
|
5. 脚本退出后 Chrome 进程继续运行,用户可直接操作页面。
|
|
39
39
|
|
|
@@ -51,7 +51,7 @@ Chrome 使用独立持久化用户数据目录(`.local/browser-data/`),loc
|
|
|
51
51
|
|
|
52
52
|
- 不依赖 Codex、Chrome 插件、`@Chrome` chip、`mcp__node_repl__js` 或任何外部浏览器控制工具。
|
|
53
53
|
- 不使用终端 curl 代替浏览器内请求——curl 的 Cookie 会话不会同步回 Chrome。
|
|
54
|
-
-
|
|
54
|
+
- 登录请求必须在当前远端 Chrome 页面的同源上下文中执行,确保浏览器 Cookie 和会话状态被更新。
|
|
55
55
|
- 不读取 Chrome Cookie、密码库或用户配置文件。
|
|
56
56
|
- localStorage 中的 `_sessionId` 只允许在页面上下文内读取,用途仅限两项:判断会话是否存在、作为 `token` 请求头发出。**禁止**把它回传到 Node 侧、打印到日志、写入文件或硬编码。
|
|
57
57
|
- 不输出账号、密码哈希、会话 token 或登录接口完整响应。
|
|
@@ -64,7 +64,7 @@ Chrome 使用独立持久化用户数据目录(`.local/browser-data/`),loc
|
|
|
64
64
|
1. 检查 `scripts/node_modules/playwright` 是否存在。
|
|
65
65
|
2. 若不存在,在 `scripts/` 目录下执行安装:
|
|
66
66
|
```bash
|
|
67
|
-
cd skills/
|
|
67
|
+
cd skills/platform-session-navigator/scripts
|
|
68
68
|
npm install
|
|
69
69
|
```
|
|
70
70
|
3. 若系统无 Chrome 且需要内置 Chromium,执行:
|
|
@@ -79,7 +79,7 @@ Chrome 使用独立持久化用户数据目录(`.local/browser-data/`),loc
|
|
|
79
79
|
一条命令完成全部操作:
|
|
80
80
|
|
|
81
81
|
```bash
|
|
82
|
-
node skills/
|
|
82
|
+
node skills/platform-session-navigator/scripts/platform_login.mjs
|
|
83
83
|
```
|
|
84
84
|
|
|
85
85
|
脚本内部执行:
|
|
@@ -92,7 +92,7 @@ node skills/finmall-session-navigator/scripts/finmall_login.mjs
|
|
|
92
92
|
- 关闭超时弹框(原生 JS 对话框通过 `page.on("dialog")` 自动 accept + 页面内含「会话超时」「登录超时」「登录已过期」的可见模态框)。
|
|
93
93
|
- 在页面同源上下文中通过 `page.evaluate(fetch(...))` 发起 `IM01001.do` 登录请求;请求体含 `loginName`、`loginPassword`(MD5 小写)、`requestTime`(当前毫秒时间戳,每次生成);**登录请求不需要 `token` 请求头**。
|
|
94
94
|
- 注入脚本只显式设置 `Accept: application/json, text/plain, */*` 和 `Content-Type: application/json;charset=UTF-8`;`Origin` / `Referer` / `User-Agent` / `Sec-Fetch-*` / `sec-ch-ua*` / `Connection` 属于浏览器管理或受限请求头,由 Chrome 在同源上下文自动生成,不要在 page.evaluate 里手设。
|
|
95
|
-
- 读取响应头中的新会话 token
|
|
95
|
+
- 读取响应头中的新会话 token,按远端前端格式保存为 `_sessionId`(双层编码 `{"value":"\"<uuid>\""}`),同时保存 `_userInfo`。
|
|
96
96
|
- 调用 `IM01002.do` 获取权限菜单,写入 `__menuList`、`__menuItemList`。
|
|
97
97
|
- 刷新页面,等待应用重新初始化并验证列表渲染完成。
|
|
98
98
|
6. 失败后间隔 1 秒重试,最多执行 3 次。
|
|
@@ -162,7 +162,7 @@ node skills/finmall-session-navigator/scripts/finmall_login.mjs
|
|
|
162
162
|
|
|
163
163
|
## 资源
|
|
164
164
|
|
|
165
|
-
- `scripts/
|
|
165
|
+
- `scripts/platform_login.mjs`:主脚本——读取凭据、启动/连接 Chrome、注入登录请求、处理弹框、重试并验证列表。
|
|
166
166
|
- `scripts/session-manager.mjs`:会话管理辅助脚本(如需要复用同一 Chrome 实例多次评估页面状态可用)。
|
|
167
167
|
- `scripts/package.json`:Playwright 依赖声明。
|
|
168
168
|
- `.local/credentials.json`:登录凭据(被 Git 忽略)。
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
{
|
|
2
|
-
"name": "
|
|
2
|
+
"name": "platform-session-navigator-scripts",
|
|
3
3
|
"version": "1.0.0",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
|
-
"name": "
|
|
8
|
+
"name": "platform-session-navigator-scripts",
|
|
9
9
|
"version": "1.0.0",
|
|
10
10
|
"dependencies": {
|
|
11
11
|
"playwright": "^1.62.1"
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
|
-
"name": "
|
|
2
|
+
"name": "platform-session-navigator-scripts",
|
|
3
3
|
"version": "1.0.0",
|
|
4
4
|
"private": true,
|
|
5
5
|
"type": "module",
|
|
6
|
-
"description": "Playwright-based
|
|
6
|
+
"description": "Playwright-based 远端 UAT auto-login scripts",
|
|
7
7
|
"dependencies": {
|
|
8
8
|
"playwright": "^1.62.1"
|
|
9
9
|
}
|
|
@@ -7,13 +7,26 @@ import { fileURLToPath } from "node:url";
|
|
|
7
7
|
import { createHash } from "node:crypto";
|
|
8
8
|
import * as readline from "node:readline/promises";
|
|
9
9
|
import { stdin as rlInput, stdout as rlOutput } from "node:process";
|
|
10
|
-
import {
|
|
10
|
+
import { platformConfig } from "../../shared/platform-config.mjs";
|
|
11
|
+
|
|
12
|
+
/** 对端站点 host(从配置派生,不写死域名);配置缺失时为空串,判定一律 false。 */
|
|
13
|
+
const PLATFORM_HOST = (() => {
|
|
14
|
+
try {
|
|
15
|
+
return new URL(platformConfig.baseUrl).host;
|
|
16
|
+
} catch {
|
|
17
|
+
return "";
|
|
18
|
+
}
|
|
19
|
+
})();
|
|
20
|
+
/** 目标页是否为「卡片列表页」(host + 路径双条件) */
|
|
21
|
+
function isPlatformCardListPage(url) {
|
|
22
|
+
return Boolean(PLATFORM_HOST) && url.includes(PLATFORM_HOST) && url.includes("card/list");
|
|
23
|
+
}
|
|
11
24
|
|
|
12
25
|
// ---------------------------------------------------------------------------
|
|
13
26
|
// 代理处理:本机环境设了 HTTP_PROXY,但 Chrome 远程调试端口在 127.0.0.1,
|
|
14
27
|
// 若让 Node(undici)也走代理会导致 connectOverCDP 拿到 502 / ECONNREFUSED。
|
|
15
28
|
// 这里把 localhost 排除在代理之外,仅影响 Node 侧 CDP 连接;Chrome 进程在
|
|
16
|
-
// macOS 走系统代理、不受此 env
|
|
29
|
+
// macOS 走系统代理、不受此 env 影响,因此浏览器内访问对端站点仍走代理。
|
|
17
30
|
// ---------------------------------------------------------------------------
|
|
18
31
|
for (const key of ["NO_PROXY", "no_proxy"]) {
|
|
19
32
|
const cur = process.env[key];
|
|
@@ -30,13 +43,13 @@ const SKILL_DIR = path.resolve(SCRIPT_DIR, "..");
|
|
|
30
43
|
const LOCAL_DIR = path.join(SKILL_DIR, ".local");
|
|
31
44
|
const BROWSER_DATA_DIR = path.join(LOCAL_DIR, "browser-data");
|
|
32
45
|
export const DEFAULT_CREDENTIALS_PATH = path.join(LOCAL_DIR, "credentials.json");
|
|
33
|
-
const DEBUG_PORT =
|
|
46
|
+
const DEBUG_PORT = platformConfig.cdpPort;
|
|
34
47
|
|
|
35
|
-
export const TARGET_URL =
|
|
48
|
+
export const TARGET_URL = platformConfig.targetUrl;
|
|
36
49
|
|
|
37
|
-
export const LOGIN_ENDPOINT =
|
|
50
|
+
export const LOGIN_ENDPOINT = platformConfig.loginEndpoint;
|
|
38
51
|
|
|
39
|
-
const MENU_ENDPOINT =
|
|
52
|
+
const MENU_ENDPOINT = platformConfig.menuEndpoint;
|
|
40
53
|
|
|
41
54
|
// 系统级 Chrome 路径(按优先级排列),找不到时回退到 Playwright 内置 Chromium
|
|
42
55
|
const CHROME_CANDIDATES = [
|
|
@@ -54,8 +67,8 @@ const CHROME_CANDIDATES = [
|
|
|
54
67
|
|
|
55
68
|
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
56
69
|
|
|
57
|
-
// page.evaluate
|
|
58
|
-
// responseStatus=0 的常驻请求)时,裸 evaluate 永远不返回,连带
|
|
70
|
+
// page.evaluate 没有默认超时:远端页面主线程被占 / 页面内 fetch 挂起(实测该环境确有
|
|
71
|
+
// responseStatus=0 的常驻请求)时,裸 evaluate 永远不返回,连带 ensurePlatformSession 与
|
|
59
72
|
// 整个同步任务永久挂起——同步中间件是单飞制,一个挂死的任务会让之后每次「远端同步」都拿到
|
|
60
73
|
// 409「已有同步任务正在进行」,用户侧就是「点了同步、服务端再也不响应」。
|
|
61
74
|
// 这里给每次 evaluate 套硬超时:Node 侧停止等待并抛明确错误(页面里仍在跑的那个 evaluate
|
|
@@ -113,7 +126,7 @@ export async function loadCredentials(
|
|
|
113
126
|
raw = await readFile(credentialsPath, "utf8");
|
|
114
127
|
} catch (e) {
|
|
115
128
|
const err = new Error(
|
|
116
|
-
|
|
129
|
+
`远端登录凭据不存在或不可读:${credentialsPath}。请运行登录流程,或在预览界面点「远程同步」时按提示填写账号密码。`,
|
|
117
130
|
);
|
|
118
131
|
err.code = "NO_CREDENTIALS";
|
|
119
132
|
throw err;
|
|
@@ -183,8 +196,8 @@ async function promptAndSaveCredentials(
|
|
|
183
196
|
}
|
|
184
197
|
const rl = readline.createInterface({ input: rlInput, output: rlOutput });
|
|
185
198
|
try {
|
|
186
|
-
const loginName = (await rl.question("
|
|
187
|
-
const rawPassword = (await rl.question("
|
|
199
|
+
const loginName = (await rl.question("远端登录账号:")).trim();
|
|
200
|
+
const rawPassword = (await rl.question("远端登录密码:")).trim();
|
|
188
201
|
return await saveCredentials(loginName, rawPassword, credentialsPath);
|
|
189
202
|
} finally {
|
|
190
203
|
rl.close();
|
|
@@ -268,7 +281,7 @@ export async function tryConnectBrowser(port) {
|
|
|
268
281
|
* 使用 page.evaluate 在浏览器中执行,返回 JSON 可序列化结果。
|
|
269
282
|
* 包含重试逻辑以处理 SPA 客户端导航导致的执行上下文销毁。
|
|
270
283
|
*
|
|
271
|
-
*
|
|
284
|
+
* 重要:远端不使用 Cookie 鉴权(document.cookie 恒为空),会话凭据存放在
|
|
272
285
|
* localStorage 的 `_sessionId` 中,由前端以 `token` 请求头透传。而卡片列表接口
|
|
273
286
|
* IDC10001.do 本身不校验鉴权,因此「列表渲染出来了」并不代表会话有效——只看 DOM
|
|
274
287
|
* 会把过期会话误判成已登录,导致后续需要鉴权的接口(如 IDC10012.do)返回
|
|
@@ -457,7 +470,7 @@ export async function waitForAuthenticatedPage(page, timeoutMs = 20000) {
|
|
|
457
470
|
|
|
458
471
|
/**
|
|
459
472
|
* 在页面上下文中执行登录请求(IM01001 + IM01002)。
|
|
460
|
-
*
|
|
473
|
+
* 请求在远端同源页面内发起,浏览器自动携带 Cookie 和管理请求头。
|
|
461
474
|
*/
|
|
462
475
|
export async function performLogin(page, credentials) {
|
|
463
476
|
const payload = buildPayload(
|
|
@@ -468,7 +481,7 @@ export async function performLogin(page, credentials) {
|
|
|
468
481
|
page,
|
|
469
482
|
async (input) => {
|
|
470
483
|
// 页面内 fetch 也必须自带超时:IM01001 / IM01002 挂起时(该环境实测有常驻不返回的请求),
|
|
471
|
-
// 没有 AbortController 就会把 performLogin →
|
|
484
|
+
// 没有 AbortController 就会把 performLogin → runPlatformLogin → ensurePlatformSession
|
|
472
485
|
// 整条链永久挂住。这里内层 abort + 外层 evaluateWithRetry 的硬超时双重设防。
|
|
473
486
|
const ctl = new AbortController();
|
|
474
487
|
const timer = setTimeout(() => ctl.abort(), input.timeoutMs);
|
|
@@ -526,15 +539,15 @@ export async function performLogin(page, credentials) {
|
|
|
526
539
|
usrDeptName: body.usrDeptName,
|
|
527
540
|
cstNo: body.cstNo,
|
|
528
541
|
};
|
|
529
|
-
const
|
|
542
|
+
const setPlatformStorage = (key, value) => {
|
|
530
543
|
if (globalThis.app?.api?.setLocalStorage) {
|
|
531
544
|
globalThis.app.api.setLocalStorage(key, value);
|
|
532
545
|
return;
|
|
533
546
|
}
|
|
534
547
|
localStorage.setItem(key, JSON.stringify({ value }));
|
|
535
548
|
};
|
|
536
|
-
|
|
537
|
-
|
|
549
|
+
setPlatformStorage("_sessionId", JSON.stringify(sessionId));
|
|
550
|
+
setPlatformStorage("_userInfo", JSON.stringify(userInfo));
|
|
538
551
|
|
|
539
552
|
// 获取权限菜单
|
|
540
553
|
const menuPayload = {
|
|
@@ -616,9 +629,9 @@ export async function performLogin(page, credentials) {
|
|
|
616
629
|
],
|
|
617
630
|
};
|
|
618
631
|
menuTree.unshift(welcome);
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
632
|
+
setPlatformStorage("__menuList", JSON.stringify(menuTree));
|
|
633
|
+
setPlatformStorage("__menuItemList", JSON.stringify(menuItemList));
|
|
634
|
+
setPlatformStorage("_chatLogin", true);
|
|
622
635
|
if (menuResponseBody?.header?.errorCode !== "0" || menuList.length === 0) {
|
|
623
636
|
return {
|
|
624
637
|
httpOk: response.ok,
|
|
@@ -652,7 +665,7 @@ export async function performLogin(page, credentials) {
|
|
|
652
665
|
// ---------------------------------------------------------------------------
|
|
653
666
|
|
|
654
667
|
/**
|
|
655
|
-
*
|
|
668
|
+
* 自动登录远端 UAT 并打开卡片列表页。
|
|
656
669
|
*
|
|
657
670
|
* 流程:
|
|
658
671
|
* 1. 尝试连接已运行的 Chrome 调试实例(端口 9222)。
|
|
@@ -671,7 +684,7 @@ export async function performLogin(page, credentials) {
|
|
|
671
684
|
* @param {boolean} [options.closeBrowser=false] 完成后是否关闭浏览器
|
|
672
685
|
* @returns {Promise<object>} 登录结果
|
|
673
686
|
*/
|
|
674
|
-
export async function
|
|
687
|
+
export async function runPlatformLogin(options = {}) {
|
|
675
688
|
const {
|
|
676
689
|
credentialsPath = DEFAULT_CREDENTIALS_PATH,
|
|
677
690
|
maxAttempts = 3,
|
|
@@ -689,7 +702,7 @@ export async function runFinmallLogin(options = {}) {
|
|
|
689
702
|
} catch (e) {
|
|
690
703
|
// 凭据缺失且处于交互终端:引导用户输入账号密码并落盘,再继续登录。
|
|
691
704
|
if (e && e.code === "NO_CREDENTIALS" && process.stdout.isTTY) {
|
|
692
|
-
console.error("
|
|
705
|
+
console.error("本地远端凭据缺失,进入交互式填写…");
|
|
693
706
|
credentials = await promptAndSaveCredentials(credentialsPath);
|
|
694
707
|
} else {
|
|
695
708
|
throw e;
|
|
@@ -723,7 +736,7 @@ export async function runFinmallLogin(options = {}) {
|
|
|
723
736
|
.pages()
|
|
724
737
|
.find((p) => {
|
|
725
738
|
try {
|
|
726
|
-
return p.url().includes(
|
|
739
|
+
return PLATFORM_HOST ? p.url().includes(PLATFORM_HOST) : false;
|
|
727
740
|
} catch {
|
|
728
741
|
return false;
|
|
729
742
|
}
|
|
@@ -739,10 +752,7 @@ export async function runFinmallLogin(options = {}) {
|
|
|
739
752
|
// 导航到目标页面
|
|
740
753
|
try {
|
|
741
754
|
const currentUrl = page.url();
|
|
742
|
-
if (
|
|
743
|
-
!currentUrl.includes("finmall.com") ||
|
|
744
|
-
!currentUrl.includes("card/list")
|
|
745
|
-
) {
|
|
755
|
+
if (!isPlatformCardListPage(currentUrl)) {
|
|
746
756
|
await page.goto(TARGET_URL, {
|
|
747
757
|
waitUntil: "domcontentloaded",
|
|
748
758
|
timeout: 30000,
|
|
@@ -832,7 +842,7 @@ export async function runFinmallLogin(options = {}) {
|
|
|
832
842
|
// 不消耗固定的重试次数(避免一次性输错就把次数吃光)。
|
|
833
843
|
credentialPrompts += 1;
|
|
834
844
|
console.error(
|
|
835
|
-
`✖
|
|
845
|
+
`✖ 登录失败:账号或密码错误。请重新输入远端凭据(${credentialPrompts}/${maxCredentialPrompts})`,
|
|
836
846
|
);
|
|
837
847
|
try {
|
|
838
848
|
credentials = await promptAndSaveCredentials(credentialsPath);
|
|
@@ -915,7 +925,7 @@ async function selfTest() {
|
|
|
915
925
|
throw new Error("请求体生成测试失败");
|
|
916
926
|
}
|
|
917
927
|
console.log(
|
|
918
|
-
JSON.stringify({ success: true, message: "
|
|
928
|
+
JSON.stringify({ success: true, message: "远端登录脚本自检通过" }),
|
|
919
929
|
);
|
|
920
930
|
}
|
|
921
931
|
|
|
@@ -935,7 +945,7 @@ if (globalThis.process?.argv?.[1]) {
|
|
|
935
945
|
process.exit(1);
|
|
936
946
|
});
|
|
937
947
|
} else {
|
|
938
|
-
|
|
948
|
+
runPlatformLogin({ closeBrowser })
|
|
939
949
|
.then((result) => {
|
|
940
950
|
console.log(JSON.stringify(result, null, 2));
|
|
941
951
|
// 显式退出,避免 Playwright WebSocket 连接阻止进程退出。
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
2
|
import { fileURLToPath } from "node:url";
|
|
3
|
-
import {
|
|
3
|
+
import { platformConfig } from "../../shared/platform-config.mjs";
|
|
4
4
|
import {
|
|
5
|
-
|
|
5
|
+
runPlatformLogin,
|
|
6
6
|
tryConnectBrowser,
|
|
7
7
|
waitForDebugPort,
|
|
8
8
|
findChromePath,
|
|
9
9
|
launchChromeDetached,
|
|
10
10
|
TARGET_URL,
|
|
11
|
-
} from "./
|
|
11
|
+
} from "./platform_login.mjs";
|
|
12
12
|
|
|
13
13
|
const SCRIPT_DIR = path.dirname(fileURLToPath(import.meta.url));
|
|
14
14
|
const LOCAL_DIR = path.resolve(SCRIPT_DIR, "..", ".local");
|
|
@@ -17,10 +17,10 @@ const LOCAL_DIR = path.resolve(SCRIPT_DIR, "..", ".local");
|
|
|
17
17
|
const SYNC_BROWSER_DATA_DIR = path.join(LOCAL_DIR, "sync-browser-data");
|
|
18
18
|
// 后台会话专用调试端口,与手动导航器端口隔离(手动可见、自动无界面互不干扰)。
|
|
19
19
|
// 取值走统一配置链:process.env CARDBUDDY_SYNC_CDP_PORT > .env.cb(.local) > 内置默认。
|
|
20
|
-
const SYNC_PORT =
|
|
20
|
+
const SYNC_PORT = platformConfig.syncCdpPort;
|
|
21
21
|
|
|
22
22
|
// 排除 localhost 代理:本机若设了 HTTP_PROXY,Node 侧 connectOverCDP 走代理会
|
|
23
|
-
// 拿到 502 / ECONNREFUSED。仅影响 Node→Chrome 的 CDP 连接;浏览器内访问
|
|
23
|
+
// 拿到 502 / ECONNREFUSED。仅影响 Node→Chrome 的 CDP 连接;浏览器内访问 platform
|
|
24
24
|
// 仍走系统代理(Chrome 进程不受此 env 影响)。
|
|
25
25
|
for (const key of ["NO_PROXY", "no_proxy"]) {
|
|
26
26
|
const cur = process.env[key];
|
|
@@ -28,26 +28,26 @@ for (const key of ["NO_PROXY", "no_proxy"]) {
|
|
|
28
28
|
process.env[key] = cur && cur.length ? `${cur},${extra}` : extra;
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
-
// 进程内会话缓存:
|
|
32
|
-
// 而同步/保存/历史查询等高频只读操作都会经过
|
|
33
|
-
// 直接复用,跳过重复登录校验;服务端会话真过期时由
|
|
31
|
+
// 进程内会话缓存:runPlatformLogin 热路径固定消耗导航 + networkidle + sleep(2000)(实测 ~2.6s),
|
|
32
|
+
// 而同步/保存/历史查询等高频只读操作都会经过 ensurePlatformSession。TTL 内且 CDP 仍连通时
|
|
33
|
+
// 直接复用,跳过重复登录校验;服务端会话真过期时由 platformPostWithHeal 的 SESSION_INVALID
|
|
34
34
|
// 自愈(清 session 重登重试)兜底,故此处缓存不削弱正确性。
|
|
35
|
-
const SESSION_CACHE_TTL_MS = Number(process.env.
|
|
35
|
+
const SESSION_CACHE_TTL_MS = Number(process.env.PLATFORM_SESSION_CACHE_TTL_MS || 60000);
|
|
36
36
|
let _sessionCache = null; // { browser, login, at }
|
|
37
37
|
|
|
38
38
|
/**
|
|
39
|
-
*
|
|
39
|
+
* 确保存在一个已登录的远端后台浏览器会话(无界面 headless,端口 SYNC_PORT)。
|
|
40
40
|
*
|
|
41
41
|
* 行为:
|
|
42
42
|
* - 若 SYNC_PORT 未启动(含上次 dev server 退出后该 detached Chrome 也已崩溃的情况):
|
|
43
43
|
* 以 headless 模式启动一个独立(detached)Chrome,并用保存的凭据自动登录。
|
|
44
44
|
* - 若 SYNC_PORT 已启动(含跨 dev server 重启复用的上一实例):直接连上并复用,
|
|
45
|
-
* 仅在 _sessionId 缺失时由
|
|
45
|
+
* 仅在 _sessionId 缺失时由 runPlatformLogin 自动登录。
|
|
46
46
|
* - 进程内热路径:距上次成功 ensure 不足 TTL 且 CDP 仍连通(isConnected)时直接返回缓存,
|
|
47
47
|
* 省去重复导航与固定等待;调用方 finally 的 browser.disconnect() 会令 isConnected()
|
|
48
48
|
* 为 false,缓存随之作废走完整流程,与原先行为一致。
|
|
49
49
|
*
|
|
50
|
-
* 注意:
|
|
50
|
+
* 注意:runPlatformLogin 只校验 _sessionId 是否存在,无法发现「服务端会话已过期」
|
|
51
51
|
* (IDC10001 列表接口不鉴权,会骗过它)。最终的服务端过期由 syncCards 以需鉴权的
|
|
52
52
|
* IDC10012 返回 BLEC0006 为准,并在那里自动重登。因此本函数只保证「有 Chrome +
|
|
53
53
|
* 页面 + 本地凭据存在」,不保证服务端一定有效。
|
|
@@ -60,8 +60,8 @@ let _sessionCache = null; // { browser, login, at }
|
|
|
60
60
|
* @param {boolean} [options.headless=true]
|
|
61
61
|
* @returns {Promise<{ browser: import('playwright').Browser, login: object }>}
|
|
62
62
|
*/
|
|
63
|
-
export async function
|
|
64
|
-
// 热路径:进程内缓存未过期且 CDP 仍连通 → 直接复用(跳过
|
|
63
|
+
export async function ensurePlatformSession({ debugPort = SYNC_PORT, headless = true } = {}) {
|
|
64
|
+
// 热路径:进程内缓存未过期且 CDP 仍连通 → 直接复用(跳过 runPlatformLogin 的
|
|
65
65
|
// 导航 + networkidle + sleep(2000),实测可从 ~2.6s 降到毫秒级)。
|
|
66
66
|
if (_sessionCache && Date.now() - _sessionCache.at < SESSION_CACHE_TTL_MS) {
|
|
67
67
|
try {
|
|
@@ -98,9 +98,9 @@ export async function ensureFinMallSession({ debugPort = SYNC_PORT, headless = t
|
|
|
98
98
|
}
|
|
99
99
|
}
|
|
100
100
|
|
|
101
|
-
//
|
|
101
|
+
// runPlatformLogin 内部会连上该端口、导航到卡片列表页、并在需要时自动登录
|
|
102
102
|
// (使用 .local/credentials.json 中的保存凭据,全程在页面上下文内完成)。
|
|
103
|
-
const login = await
|
|
103
|
+
const login = await runPlatformLogin({
|
|
104
104
|
debugPort,
|
|
105
105
|
headless,
|
|
106
106
|
browserDataDir: SYNC_BROWSER_DATA_DIR,
|
|
@@ -110,7 +110,7 @@ export async function ensureFinMallSession({ debugPort = SYNC_PORT, headless = t
|
|
|
110
110
|
if (!login.success) {
|
|
111
111
|
const reason = login.reason || "未知原因";
|
|
112
112
|
const captchaNote = login.captchaSignal ? "(页面要求验证码,需人工处理)" : "";
|
|
113
|
-
throw new Error(
|
|
113
|
+
throw new Error(`后台自动登录远端失败:${reason}${captchaNote}`);
|
|
114
114
|
}
|
|
115
115
|
|
|
116
116
|
_sessionCache = { browser, login, at: Date.now() };
|