@dreamor/atlas-cli 0.7.22 → 0.7.24
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.github.md +230 -0
- package/README.md +1 -55
- package/atlas.cjs +226 -0
- package/package.json +7 -9
- package/atlas.js +0 -5
- package/dist/adapters/atlas/auth/browser.js +0 -37
- package/dist/adapters/atlas/auth/index.js +0 -3
- package/dist/adapters/atlas/auth/login.js +0 -147
- package/dist/adapters/atlas/auth/refresh.js +0 -138
- package/dist/adapters/atlas/auth/session.js +0 -115
- package/dist/adapters/atlas/cli.js +0 -512
- package/dist/adapters/atlas/commands/_output_schema.js +0 -379
- package/dist/adapters/atlas/commands/actual/_logic.js +0 -116
- package/dist/adapters/atlas/commands/actual/index.js +0 -138
- package/dist/adapters/atlas/commands/auth.js +0 -1
- package/dist/adapters/atlas/commands/baseline/index.js +0 -137
- package/dist/adapters/atlas/commands/compare/_logic.js +0 -39
- package/dist/adapters/atlas/commands/compare/index.js +0 -89
- package/dist/adapters/atlas/commands/exec.js +0 -81
- package/dist/adapters/atlas/commands/project/index.js +0 -218
- package/dist/adapters/atlas/commands/schema.js +0 -25
- package/dist/adapters/atlas/commands/suggest.js +0 -83
- package/dist/adapters/atlas/commands/update.js +0 -104
- package/dist/adapters/atlas/daemon/index.js +0 -64
- package/dist/adapters/atlas/dict/index.js +0 -41
- package/dist/adapters/atlas/http/client.js +0 -200
- package/dist/adapters/atlas/http/index.js +0 -1
- package/dist/adapters/atlas/schema/actual.js +0 -16
- package/dist/adapters/atlas/schema/baseline.js +0 -34
- package/dist/adapters/atlas/schema/department.js +0 -11
- package/dist/adapters/atlas/schema/index.js +0 -4
- package/dist/adapters/atlas/schema/project.js +0 -13
- package/dist/adapters/atlas/util/constants.js +0 -4
- package/dist/adapters/atlas/util/env.js +0 -56
- package/dist/adapters/atlas/util/errors.js +0 -49
- package/dist/adapters/atlas/util/helpers.js +0 -17
- package/dist/adapters/atlas/util/months.js +0 -65
- package/dist/adapters/atlas/util/output-limit.js +0 -21
- package/dist/adapters/atlas/util/output.js +0 -70
- package/dist/adapters/atlas/util/paths.js +0 -40
- package/dist/adapters/atlas/util/secure-fs.js +0 -41
- package/dist/adapters/atlas/util/time.js +0 -17
- package/dist/adapters/atlas/util/version.js +0 -1
package/package.json
CHANGED
|
@@ -1,19 +1,16 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dreamor/atlas-cli",
|
|
3
|
-
"version": "0.7.
|
|
4
|
-
"description": "Atlas CLI
|
|
3
|
+
"version": "0.7.24",
|
|
4
|
+
"description": "Atlas CLI",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
|
-
"atlas": "./atlas.
|
|
7
|
+
"atlas": "./atlas.cjs"
|
|
8
8
|
},
|
|
9
9
|
"engines": {
|
|
10
10
|
"node": ">=20"
|
|
11
11
|
},
|
|
12
12
|
"files": [
|
|
13
|
-
"atlas.
|
|
14
|
-
"dist/adapters/**/*.js",
|
|
15
|
-
"!dist/adapters/atlas/tests/**",
|
|
16
|
-
"!dist/adapters/**/tests/**",
|
|
13
|
+
"atlas.cjs",
|
|
17
14
|
"README.md"
|
|
18
15
|
],
|
|
19
16
|
"publishConfig": {
|
|
@@ -25,12 +22,13 @@
|
|
|
25
22
|
"url": "git+https://github.com/dreamor/atlas-cli.git"
|
|
26
23
|
},
|
|
27
24
|
"scripts": {
|
|
28
|
-
"build": "
|
|
25
|
+
"build": "node scripts/build.mjs",
|
|
26
|
+
"build:dev": "node scripts/build.mjs --dev",
|
|
29
27
|
"lint": "tsc --noEmit --pretty false",
|
|
30
28
|
"lint:watch": "tsc --noEmit --pretty false --watch",
|
|
31
29
|
"test": "vitest run",
|
|
32
30
|
"test:watch": "vitest",
|
|
33
|
-
"verify": "node
|
|
31
|
+
"verify": "node atlas.cjs --help",
|
|
34
32
|
"auth:login": "node --import tsx ./adapters/atlas/cli.ts auth login",
|
|
35
33
|
"generate-skill-docs": "node scripts/generate-skill-docs.mjs"
|
|
36
34
|
},
|
package/atlas.js
DELETED
|
@@ -1,37 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Playwright 加载与 Firefox 二进制安装 —— login 和 refresh 命令共用。
|
|
3
|
-
*
|
|
4
|
-
* 保持动态 import + try/catch 降级:顶层静态 import 会让单文件 bundle
|
|
5
|
-
* 在未安装 playwright 的环境下无法启动(连 --version 都 require 失败)。
|
|
6
|
-
* 只在真正调用 auth login / auth refresh 时才尝试加载 playwright。
|
|
7
|
-
*/
|
|
8
|
-
import { execSync } from 'child_process';
|
|
9
|
-
import { createRequire } from 'module';
|
|
10
|
-
import { log } from '../util/output.js';
|
|
11
|
-
const _require = createRequire(import.meta.url);
|
|
12
|
-
export async function loadFirefox() {
|
|
13
|
-
try {
|
|
14
|
-
const pw = await import('playwright');
|
|
15
|
-
return pw.firefox;
|
|
16
|
-
}
|
|
17
|
-
catch {
|
|
18
|
-
throw new Error("未安装 playwright。请执行: npm i -g playwright && npx playwright install");
|
|
19
|
-
}
|
|
20
|
-
}
|
|
21
|
-
/**
|
|
22
|
-
* 检测 launch 错误是否为"浏览器二进制未安装",自动用内置 playwright 下载。
|
|
23
|
-
*/
|
|
24
|
-
export async function ensureFirefoxInstalled() {
|
|
25
|
-
const pwPath = _require.resolve('playwright/package.json');
|
|
26
|
-
const cliJs = pwPath.replace(/package\.json$/, 'cli.js');
|
|
27
|
-
log('首次运行,正在自动安装 Firefox 浏览器...');
|
|
28
|
-
try {
|
|
29
|
-
execSync(`node "${cliJs}" install firefox`, {
|
|
30
|
-
stdio: 'inherit',
|
|
31
|
-
timeout: 300_000,
|
|
32
|
-
});
|
|
33
|
-
}
|
|
34
|
-
catch {
|
|
35
|
-
throw new Error('Firefox 浏览器自动安装失败。请手动执行: npx playwright install firefox');
|
|
36
|
-
}
|
|
37
|
-
}
|
|
@@ -1,147 +0,0 @@
|
|
|
1
|
-
import { writeCookies, readCookies, fetchCookiesFromDaemon } from './session.js';
|
|
2
|
-
import { loadFirefox, ensureFirefoxInstalled } from './browser.js';
|
|
3
|
-
import { isJsonMode, jsonOk, log } from '../util/output.js';
|
|
4
|
-
import { AtlasError } from '../util/errors.js';
|
|
5
|
-
const BANMA_HOST = 'banma-yuntu.alibaba-inc.com';
|
|
6
|
-
export async function login(_port) {
|
|
7
|
-
log('正在打开浏览器进行 Banma SSO 登录...');
|
|
8
|
-
const firefox = await loadFirefox();
|
|
9
|
-
let browser;
|
|
10
|
-
try {
|
|
11
|
-
browser = await firefox.launch({
|
|
12
|
-
headless: false,
|
|
13
|
-
});
|
|
14
|
-
}
|
|
15
|
-
catch (e) {
|
|
16
|
-
if (e instanceof Error && e.message.includes('Executable doesn\'t exist')) {
|
|
17
|
-
await ensureFirefoxInstalled();
|
|
18
|
-
browser = await firefox.launch({ headless: false });
|
|
19
|
-
}
|
|
20
|
-
else {
|
|
21
|
-
throw e;
|
|
22
|
-
}
|
|
23
|
-
}
|
|
24
|
-
const page = await browser.newPage();
|
|
25
|
-
log('请在浏览器中完成 SSO 登录...');
|
|
26
|
-
log('登录成功后页面会自动跳转,CLI 会自动捕获 cookies');
|
|
27
|
-
await page.goto(`https://${BANMA_HOST}/`, { waitUntil: 'domcontentloaded', timeout: 60000 });
|
|
28
|
-
// 等待 access_token cookie 出现(最多 120s)。
|
|
29
|
-
// 注意 access_token 是 HttpOnly cookie,不能用 document.cookie 检测,
|
|
30
|
-
// 必须走 page.context().cookies() 才能读到。
|
|
31
|
-
const MAX_WAIT = 120_000;
|
|
32
|
-
const POLL_INTERVAL = 500;
|
|
33
|
-
let foundToken = false;
|
|
34
|
-
for (let elapsed = 0; elapsed < MAX_WAIT; elapsed += POLL_INTERVAL) {
|
|
35
|
-
const allCookies = await page.context().cookies();
|
|
36
|
-
if (allCookies.some((c) => c.name === 'access_token' && c.value)) {
|
|
37
|
-
foundToken = true;
|
|
38
|
-
break;
|
|
39
|
-
}
|
|
40
|
-
await page.waitForTimeout(POLL_INTERVAL);
|
|
41
|
-
}
|
|
42
|
-
if (!foundToken) {
|
|
43
|
-
log('SSO 登录检测超时。如果您已完成登录,请按 Ctrl+C 重试或确认账号权限。');
|
|
44
|
-
}
|
|
45
|
-
// 额外等待确保残留 cookie 写入完毕
|
|
46
|
-
await page.waitForTimeout(500);
|
|
47
|
-
// Extract cookies — 保存 banma-yuntu 域下所有 cookies(API 网关需要完整会话链)
|
|
48
|
-
const cookies = await page.context().cookies();
|
|
49
|
-
const atlasCookies = cookies
|
|
50
|
-
.filter((c) => c.domain?.includes('banma-yuntu') || c.domain?.includes('alibaba-inc'))
|
|
51
|
-
.map((c) => ({
|
|
52
|
-
name: c.name,
|
|
53
|
-
value: c.value,
|
|
54
|
-
domain: c.domain,
|
|
55
|
-
}));
|
|
56
|
-
await writeCookies(atlasCookies);
|
|
57
|
-
await browser.close();
|
|
58
|
-
log('SSO 登录成功!');
|
|
59
|
-
log(`已保存 ${atlasCookies.length} 个 cookies`);
|
|
60
|
-
}
|
|
61
|
-
export async function loginStatus() {
|
|
62
|
-
// Try daemon first
|
|
63
|
-
try {
|
|
64
|
-
const daemonCookies = await fetchCookiesFromDaemon();
|
|
65
|
-
if (daemonCookies && daemonCookies.length > 0) {
|
|
66
|
-
const userinfoCookie = daemonCookies.find((c) => c.name === 'buc_userinfo');
|
|
67
|
-
let displayName = 'via-daemon';
|
|
68
|
-
if (userinfoCookie) {
|
|
69
|
-
try {
|
|
70
|
-
const decoded = JSON.parse(Buffer.from(userinfoCookie.value, 'base64').toString('utf-8'));
|
|
71
|
-
displayName = decoded.name || decoded.account || `工号 ${decoded.emp_id}`;
|
|
72
|
-
}
|
|
73
|
-
catch {
|
|
74
|
-
// fallback
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
return {
|
|
78
|
-
loggedIn: true,
|
|
79
|
-
account: displayName,
|
|
80
|
-
mode: 'daemon',
|
|
81
|
-
};
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
catch {
|
|
85
|
-
// Daemon not running
|
|
86
|
-
}
|
|
87
|
-
// Check local cookies
|
|
88
|
-
const cookies = await readCookies();
|
|
89
|
-
if (cookies && cookies.length > 0) {
|
|
90
|
-
const userinfoCookie = cookies.find((c) => c.name === 'buc_userinfo');
|
|
91
|
-
let displayName = '已登录';
|
|
92
|
-
if (userinfoCookie) {
|
|
93
|
-
try {
|
|
94
|
-
const decoded = JSON.parse(Buffer.from(userinfoCookie.value, 'base64').toString('utf-8'));
|
|
95
|
-
displayName = decoded.name || decoded.account || `工号 ${decoded.emp_id}`;
|
|
96
|
-
}
|
|
97
|
-
catch {
|
|
98
|
-
// fallback below
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
if (displayName === '已登录') {
|
|
102
|
-
const bucUser = cookies.find((c) => c.name === 'buc_username');
|
|
103
|
-
if (bucUser)
|
|
104
|
-
displayName = bucUser.value;
|
|
105
|
-
}
|
|
106
|
-
return {
|
|
107
|
-
loggedIn: true,
|
|
108
|
-
account: displayName,
|
|
109
|
-
mode: 'local',
|
|
110
|
-
};
|
|
111
|
-
}
|
|
112
|
-
return { loggedIn: false };
|
|
113
|
-
}
|
|
114
|
-
export async function loginCmd(opts) {
|
|
115
|
-
// Try daemon first if cookies available
|
|
116
|
-
const daemonCookies = await fetchCookiesFromDaemon();
|
|
117
|
-
if (daemonCookies && daemonCookies.length > 0) {
|
|
118
|
-
await writeCookies(daemonCookies);
|
|
119
|
-
log('已从 daemon 获取 cookies');
|
|
120
|
-
if (opts.json || isJsonMode()) {
|
|
121
|
-
jsonOk({ status: 'logged_in', via: 'daemon' });
|
|
122
|
-
}
|
|
123
|
-
return;
|
|
124
|
-
}
|
|
125
|
-
if (!process.stdout.isTTY) {
|
|
126
|
-
throw new AtlasError('atlas auth login 需要在终端中交互完成 SSO,当前为非终端环境。请手动在终端执行 `atlas auth login` 后重试', 'INTERACTIVE_REQUIRED');
|
|
127
|
-
}
|
|
128
|
-
await login();
|
|
129
|
-
if (opts.json || isJsonMode()) {
|
|
130
|
-
jsonOk({ status: 'logged_in' });
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
export async function statusCmd(opts) {
|
|
134
|
-
const status = await loginStatus();
|
|
135
|
-
if (opts.json || isJsonMode()) {
|
|
136
|
-
jsonOk(status);
|
|
137
|
-
return;
|
|
138
|
-
}
|
|
139
|
-
if (status.loggedIn) {
|
|
140
|
-
log(`已登录 (${status.mode === 'daemon' ? 'daemon' : 'local cookies'})`);
|
|
141
|
-
if (status.account)
|
|
142
|
-
log(`账号: ${status.account}`);
|
|
143
|
-
}
|
|
144
|
-
else {
|
|
145
|
-
log('未登录,请执行 atlas auth login');
|
|
146
|
-
}
|
|
147
|
-
}
|
|
@@ -1,138 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* atlas auth refresh —— headless 静默换发 access_token。
|
|
3
|
-
*
|
|
4
|
-
* 原理:斑马 banma-yuntu 首页在 access_token 缺失/过期时会被 SSO
|
|
5
|
-
* 拦截并走 302 重定向链换发新 token。已通过 spike 验证 headless 模式下
|
|
6
|
-
* SSO_REFRESH_TOKEN 足以完成整个链路,无需人工交互。
|
|
7
|
-
*
|
|
8
|
-
* 失败降级:抛 SessionExpiredError 引导用户 atlas auth login。
|
|
9
|
-
*/
|
|
10
|
-
import { readCookies, writeCookies } from './session.js';
|
|
11
|
-
import { loadFirefox, ensureFirefoxInstalled } from './browser.js';
|
|
12
|
-
import { isJsonMode, jsonOk, log } from '../util/output.js';
|
|
13
|
-
import { AtlasError, SessionExpiredError } from '../util/errors.js';
|
|
14
|
-
const BANMA_HOST = 'banma-yuntu.alibaba-inc.com';
|
|
15
|
-
const REFRESH_TIMEOUT_MS = 15_000;
|
|
16
|
-
const POLL_INTERVAL_MS = 500;
|
|
17
|
-
/**
|
|
18
|
-
* 执行一次静默 SSO 刷新。
|
|
19
|
-
*
|
|
20
|
-
* 前置条件:本地存在 cookies.json 且含有 SSO_REFRESH_TOKEN 等长期凭证。
|
|
21
|
-
* 成功:新 access_token 落库;返回 { refreshed: true, account }。
|
|
22
|
-
* 失败:抛 SessionExpiredError 引导 atlas auth login。
|
|
23
|
-
*/
|
|
24
|
-
export async function refreshSession(opts = {}) {
|
|
25
|
-
const timeoutMs = opts.timeoutMs ?? REFRESH_TIMEOUT_MS;
|
|
26
|
-
const pollIntervalMs = opts.pollIntervalMs ?? POLL_INTERVAL_MS;
|
|
27
|
-
const existing = await readCookies();
|
|
28
|
-
if (!existing || existing.length === 0) {
|
|
29
|
-
throw new AtlasError('未检测到本地登录态,请先执行:atlas auth login', 'INTERACTIVE_REQUIRED');
|
|
30
|
-
}
|
|
31
|
-
const originalAccessToken = existing.find((c) => c.name === 'access_token')?.value ?? '';
|
|
32
|
-
const firefox = await loadFirefox();
|
|
33
|
-
let browser;
|
|
34
|
-
try {
|
|
35
|
-
browser = await firefox.launch({ headless: true });
|
|
36
|
-
}
|
|
37
|
-
catch (e) {
|
|
38
|
-
if (e instanceof Error && e.message.includes("Executable doesn't exist")) {
|
|
39
|
-
await ensureFirefoxInstalled();
|
|
40
|
-
browser = await firefox.launch({ headless: true });
|
|
41
|
-
}
|
|
42
|
-
else {
|
|
43
|
-
throw e;
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
try {
|
|
47
|
-
const ctx = await browser.newContext();
|
|
48
|
-
// 灌入已有 cookie,但剥离失效的 access_token 及相关签名 cookies
|
|
49
|
-
// (access_token.sig / refresh_token / refresh_token.sig)—— 强制
|
|
50
|
-
// SSO 用 SSO_REFRESH_TOKEN 换发全套新 token,防止 still_valid 假阳性。
|
|
51
|
-
// 注:实际工时 API 会校验 token + signature 匹配,只换 access_token
|
|
52
|
-
// 不换 sig 同样会被拒绝。
|
|
53
|
-
// playwright 严格校验 cookie 字段,需要补齐 path/secure 等。
|
|
54
|
-
const tokenNames = new Set(['access_token', 'access_token.sig', 'refresh_token', 'refresh_token.sig']);
|
|
55
|
-
const cookiesForContext = existing
|
|
56
|
-
.filter((c) => !tokenNames.has(c.name))
|
|
57
|
-
.map((c) => ({
|
|
58
|
-
name: c.name,
|
|
59
|
-
value: c.value,
|
|
60
|
-
domain: c.domain ?? '.alibaba-inc.com',
|
|
61
|
-
path: '/',
|
|
62
|
-
httpOnly: false,
|
|
63
|
-
secure: true,
|
|
64
|
-
sameSite: 'Lax',
|
|
65
|
-
}));
|
|
66
|
-
await ctx.addCookies(cookiesForContext);
|
|
67
|
-
const page = await ctx.newPage();
|
|
68
|
-
await page.goto(`https://${BANMA_HOST}/`, {
|
|
69
|
-
waitUntil: 'domcontentloaded',
|
|
70
|
-
timeout: timeoutMs,
|
|
71
|
-
});
|
|
72
|
-
// 轮询 access_token:goto 完成后如果 SSO 302 链跑通,浏览器 context 里
|
|
73
|
-
// 就会有 access_token(可能是原值——本来就没过期;也可能是新值——SSO 补发)。
|
|
74
|
-
// 两种情况都视为"session 依然可用",回写 cookies 即可;只有拿不到才算失败。
|
|
75
|
-
let finalToken = null;
|
|
76
|
-
let tokenChanged = false;
|
|
77
|
-
const startedAt = Date.now();
|
|
78
|
-
while (Date.now() - startedAt < timeoutMs) {
|
|
79
|
-
const all = await ctx.cookies();
|
|
80
|
-
const cur = all.find((c) => c.name === 'access_token');
|
|
81
|
-
if (cur && cur.value) {
|
|
82
|
-
finalToken = cur.value;
|
|
83
|
-
tokenChanged = cur.value !== originalAccessToken;
|
|
84
|
-
// 如果 token 变了(真的刷新过),马上确认,避免过早退出
|
|
85
|
-
if (tokenChanged || !originalAccessToken)
|
|
86
|
-
break;
|
|
87
|
-
// 如果 token 未变,多轮询几次给 SSO 一点补发机会,但 goto 完成后
|
|
88
|
-
// 通常几百 ms 内就有结论;等一轮就够了。
|
|
89
|
-
await page.waitForTimeout(pollIntervalMs);
|
|
90
|
-
const again = (await ctx.cookies()).find((c) => c.name === 'access_token');
|
|
91
|
-
if (again && again.value !== originalAccessToken) {
|
|
92
|
-
finalToken = again.value;
|
|
93
|
-
tokenChanged = true;
|
|
94
|
-
}
|
|
95
|
-
break;
|
|
96
|
-
}
|
|
97
|
-
await page.waitForTimeout(pollIntervalMs);
|
|
98
|
-
}
|
|
99
|
-
if (!finalToken) {
|
|
100
|
-
throw new SessionExpiredError('自动刷新失败(未获取到 access_token),请重新登录:atlas auth login');
|
|
101
|
-
}
|
|
102
|
-
// 抓 banma-yuntu / alibaba-inc 域下所有 cookies,回写持久化
|
|
103
|
-
const all = await ctx.cookies();
|
|
104
|
-
const filtered = all
|
|
105
|
-
.filter((c) => c.domain?.includes('banma-yuntu') || c.domain?.includes('alibaba-inc'))
|
|
106
|
-
.map((c) => ({ name: c.name, value: c.value, domain: c.domain }));
|
|
107
|
-
await writeCookies(filtered);
|
|
108
|
-
const account = deriveAccountName(filtered);
|
|
109
|
-
return { refreshed: tokenChanged, account };
|
|
110
|
-
}
|
|
111
|
-
finally {
|
|
112
|
-
await browser.close();
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
function deriveAccountName(cookies) {
|
|
116
|
-
const userinfo = cookies.find((c) => c.name === 'buc_userinfo');
|
|
117
|
-
if (userinfo) {
|
|
118
|
-
try {
|
|
119
|
-
const decoded = JSON.parse(Buffer.from(userinfo.value, 'base64').toString('utf-8'));
|
|
120
|
-
return decoded.name || decoded.account || (decoded.emp_id ? `工号 ${decoded.emp_id}` : undefined);
|
|
121
|
-
}
|
|
122
|
-
catch {
|
|
123
|
-
// 解码失败降级到 buc_username
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
return cookies.find((c) => c.name === 'buc_username')?.value;
|
|
127
|
-
}
|
|
128
|
-
export async function refreshCmd(opts) {
|
|
129
|
-
const result = await refreshSession();
|
|
130
|
-
const status = result.refreshed ? 'refreshed' : 'still_valid';
|
|
131
|
-
if (opts.json || isJsonMode()) {
|
|
132
|
-
jsonOk({ status, account: result.account });
|
|
133
|
-
return;
|
|
134
|
-
}
|
|
135
|
-
log(result.refreshed ? '会话已刷新' : '会话仍然有效,无需刷新');
|
|
136
|
-
if (result.account)
|
|
137
|
-
log(`账号: ${result.account}`);
|
|
138
|
-
}
|
|
@@ -1,115 +0,0 @@
|
|
|
1
|
-
import { readFile, unlink } from 'fs/promises';
|
|
2
|
-
import { existsSync } from 'fs';
|
|
3
|
-
import { secureMkdir, secureWriteFile } from '../util/secure-fs.js';
|
|
4
|
-
import { getAtlasHome, getCookieFile } from '../util/paths.js';
|
|
5
|
-
const SESSION_DIR = getAtlasHome();
|
|
6
|
-
const COOKIE_FILE = getCookieFile();
|
|
7
|
-
let cachedCookies = null;
|
|
8
|
-
/**
|
|
9
|
-
* 读取持久化的 cookies(从本地文件)
|
|
10
|
-
* Playwright SSO 登录后将 cookies 写入此文件
|
|
11
|
-
*/
|
|
12
|
-
export async function readCookies() {
|
|
13
|
-
if (cachedCookies)
|
|
14
|
-
return cachedCookies;
|
|
15
|
-
try {
|
|
16
|
-
if (!existsSync(COOKIE_FILE))
|
|
17
|
-
return null;
|
|
18
|
-
const data = await readFile(COOKIE_FILE, 'utf-8');
|
|
19
|
-
const cookies = JSON.parse(data);
|
|
20
|
-
cachedCookies = cookies;
|
|
21
|
-
return cookies;
|
|
22
|
-
}
|
|
23
|
-
catch {
|
|
24
|
-
return null;
|
|
25
|
-
}
|
|
26
|
-
}
|
|
27
|
-
export async function writeCookies(cookies) {
|
|
28
|
-
cachedCookies = cookies;
|
|
29
|
-
await secureMkdir(SESSION_DIR, { recursive: true });
|
|
30
|
-
await secureWriteFile(COOKIE_FILE, JSON.stringify(cookies, null, 2));
|
|
31
|
-
}
|
|
32
|
-
export async function clearCookies() {
|
|
33
|
-
cachedCookies = null;
|
|
34
|
-
try {
|
|
35
|
-
await unlink(COOKIE_FILE);
|
|
36
|
-
}
|
|
37
|
-
catch {
|
|
38
|
-
// ignore
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
/**
|
|
42
|
-
* 从 daemon 获取 cookies(附带 daemon.token 鉴权)
|
|
43
|
-
*/
|
|
44
|
-
export async function fetchCookiesFromDaemon(port = 8765) {
|
|
45
|
-
try {
|
|
46
|
-
// 读取 daemon token 用于 Authorization header
|
|
47
|
-
const { getDaemonTokenFile } = await import('../util/paths.js');
|
|
48
|
-
const tokenFile = getDaemonTokenFile();
|
|
49
|
-
let token = '';
|
|
50
|
-
if (existsSync(tokenFile)) {
|
|
51
|
-
token = (await readFile(tokenFile, 'utf-8')).trim();
|
|
52
|
-
}
|
|
53
|
-
const { request } = await import('undici');
|
|
54
|
-
const headers = {};
|
|
55
|
-
if (token)
|
|
56
|
-
headers['Authorization'] = `Bearer ${token}`;
|
|
57
|
-
const resp = await request(`http://localhost:${port}/api/cookies`, {
|
|
58
|
-
headers,
|
|
59
|
-
headersTimeout: 5000,
|
|
60
|
-
bodyTimeout: 5000,
|
|
61
|
-
});
|
|
62
|
-
const text = await resp.body.text();
|
|
63
|
-
const json = JSON.parse(text);
|
|
64
|
-
if (json?.cookies && Array.isArray(json.cookies)) {
|
|
65
|
-
return json.cookies;
|
|
66
|
-
}
|
|
67
|
-
return null;
|
|
68
|
-
}
|
|
69
|
-
catch {
|
|
70
|
-
return null;
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
/** 兼容旧接口:从 cookies 中提取第一个可用的 token 类值 */
|
|
74
|
-
export async function getSessionToken() {
|
|
75
|
-
const cookies = await readCookies();
|
|
76
|
-
if (!cookies || cookies.length === 0)
|
|
77
|
-
return null;
|
|
78
|
-
// Try to find an auth token cookie
|
|
79
|
-
const tokenCookie = cookies.find((c) => c.name.includes('token') ||
|
|
80
|
-
c.name.includes('sid') ||
|
|
81
|
-
c.name.includes('session') ||
|
|
82
|
-
c.name.includes('SESSION'));
|
|
83
|
-
// 没找到 token cookie 时返回 null,避免返回无效 cookie 导致上层误判为"已登录"
|
|
84
|
-
return tokenCookie?.value ?? null;
|
|
85
|
-
}
|
|
86
|
-
const findCookie = (cookies, name) => cookies.find((c) => c.name === name);
|
|
87
|
-
/**
|
|
88
|
-
* 从已有 cookies 派生 Banma 身份。纯函数,无副作用,便于测试。
|
|
89
|
-
* 任一来源缺失对应字段返回空字符串;cookies 为空数组返回 null。
|
|
90
|
-
*/
|
|
91
|
-
export function getBanmaIdentity(cookies) {
|
|
92
|
-
if (!cookies || cookies.length === 0)
|
|
93
|
-
return null;
|
|
94
|
-
const token = findCookie(cookies, 'access_token')?.value ?? '';
|
|
95
|
-
const user = findCookie(cookies, 'buc_username')?.value ?? '';
|
|
96
|
-
// buc_userinfo 是 base64 编码的 JSON,含 emp_id / account / name 等
|
|
97
|
-
let staffId = '';
|
|
98
|
-
const userinfoValue = findCookie(cookies, 'buc_userinfo')?.value;
|
|
99
|
-
if (userinfoValue) {
|
|
100
|
-
try {
|
|
101
|
-
const decoded = JSON.parse(Buffer.from(userinfoValue, 'base64').toString('utf-8'));
|
|
102
|
-
if (decoded && decoded.emp_id != null)
|
|
103
|
-
staffId = String(decoded.emp_id);
|
|
104
|
-
}
|
|
105
|
-
catch {
|
|
106
|
-
// 解码或解析失败:保持空字符串,不阻断请求
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
return { token, user, staffId };
|
|
110
|
-
}
|
|
111
|
-
/** 读取持久化 cookies 并派生 Banma 身份,供 HttpClient 使用 */
|
|
112
|
-
export async function readBanmaIdentity() {
|
|
113
|
-
const cookies = await readCookies();
|
|
114
|
-
return getBanmaIdentity(cookies);
|
|
115
|
-
}
|