@dreamor/atlas-cli 0.7.23 → 0.7.25
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 +1 -59
- 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 -4
- package/dist/adapters/atlas/auth/login.js +0 -187
- package/dist/adapters/atlas/auth/portable.js +0 -193
- package/dist/adapters/atlas/auth/refresh.js +0 -138
- package/dist/adapters/atlas/auth/session.js +0 -167
- package/dist/adapters/atlas/cli.js +0 -561
- 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 -145
- 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/cidr.js +0 -114
- package/dist/adapters/atlas/util/constants.js +0 -4
- package/dist/adapters/atlas/util/env.js +0 -56
- package/dist/adapters/atlas/util/environment.js +0 -152
- 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/portable-store.js +0 -153
- 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.25",
|
|
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,4 +0,0 @@
|
|
|
1
|
-
export { loginCmd, statusCmd, sandboxLoginGuide } from './login.js';
|
|
2
|
-
export { refreshCmd, refreshSession } from './refresh.js';
|
|
3
|
-
export { getSessionToken, getBanmaIdentity, readBanmaIdentity } from './session.js';
|
|
4
|
-
export { exportCmd, importCmd, doctorCmd, recommendPath } from './portable.js';
|
|
@@ -1,187 +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
|
-
/**
|
|
115
|
-
* 沙盒登录降级文案(对齐 dws 三条路径)。
|
|
116
|
-
* 纯函数,便于测试。
|
|
117
|
-
*/
|
|
118
|
-
export function sandboxLoginGuide(agentCode, reason) {
|
|
119
|
-
const head = reason ?? '无法拉起浏览器完成 SSO';
|
|
120
|
-
const agent = agentCode ? `(agent=${agentCode}, no DISPLAY/TTY)` : '(无浏览器环境)';
|
|
121
|
-
return `检测到当前是沙盒环境${agent},${head}。请任选一种方式完成登录:
|
|
122
|
-
|
|
123
|
-
[推荐] 从本地宿主机导出登录态:
|
|
124
|
-
(本地) atlas auth export --base64
|
|
125
|
-
(沙盒) export ATLAS_COOKIES_B64=<粘贴上一步输出>
|
|
126
|
-
atlas auth doctor # 确认识别成功
|
|
127
|
-
|
|
128
|
-
[网络可达宿主机] 复用 daemon:
|
|
129
|
-
(本地) atlas daemon
|
|
130
|
-
(沙盒) export ATLAS_DAEMON_URL=http://host.docker.internal:8765
|
|
131
|
-
export ATLAS_DAEMON_TOKEN=<~/.atlas/daemon.token 内容>
|
|
132
|
-
atlas auth status # 走 daemon 透明鉴权
|
|
133
|
-
|
|
134
|
-
[文件传输] 直接拷贝 bundle:
|
|
135
|
-
(本地) atlas auth export -o /tmp/atlas-auth.tar.gz
|
|
136
|
-
(沙盒) atlas auth import -i /tmp/atlas-auth.tar.gz
|
|
137
|
-
`;
|
|
138
|
-
}
|
|
139
|
-
export async function loginCmd(opts) {
|
|
140
|
-
const { detectEnvironment } = await import('../util/environment.js');
|
|
141
|
-
const env = detectEnvironment();
|
|
142
|
-
// 优先复用 daemon(已存在逻辑)
|
|
143
|
-
const daemonCookies = await fetchCookiesFromDaemon();
|
|
144
|
-
if (daemonCookies && daemonCookies.length > 0) {
|
|
145
|
-
await writeCookies(daemonCookies);
|
|
146
|
-
log('已从 daemon 获取 cookies');
|
|
147
|
-
if (opts.json || isJsonMode()) {
|
|
148
|
-
jsonOk({ status: 'logged_in', via: 'daemon' });
|
|
149
|
-
}
|
|
150
|
-
return;
|
|
151
|
-
}
|
|
152
|
-
// 沙盒环境:直接走引导路径,不尝试拉起 Firefox
|
|
153
|
-
if (!env.canLaunchBrowser) {
|
|
154
|
-
throw new AtlasError(sandboxLoginGuide(env.agentCode, env.isContainer ? '容器环境无法拉起 GUI' : '无 TTY/DISPLAY'), 'INTERACTIVE_REQUIRED');
|
|
155
|
-
}
|
|
156
|
-
// 本地环境:尝试拉起浏览器;失败降级为引导提示而非硬崩
|
|
157
|
-
try {
|
|
158
|
-
await login();
|
|
159
|
-
}
|
|
160
|
-
catch (e) {
|
|
161
|
-
log(sandboxLoginGuide(env.agentCode, '浏览器拉起失败'));
|
|
162
|
-
if (opts.json || isJsonMode()) {
|
|
163
|
-
// 已在 stderr 给出引导,仍抛出错误让上层以 INTERACTIVE_REQUIRED 退出
|
|
164
|
-
}
|
|
165
|
-
throw e instanceof Error
|
|
166
|
-
? new AtlasError(`浏览器登录失败:${e.message}\n${sandboxLoginGuide(env.agentCode, '浏览器拉起失败')}`, 'INTERACTIVE_REQUIRED')
|
|
167
|
-
: e;
|
|
168
|
-
}
|
|
169
|
-
if (opts.json || isJsonMode()) {
|
|
170
|
-
jsonOk({ status: 'logged_in' });
|
|
171
|
-
}
|
|
172
|
-
}
|
|
173
|
-
export async function statusCmd(opts) {
|
|
174
|
-
const status = await loginStatus();
|
|
175
|
-
if (opts.json || isJsonMode()) {
|
|
176
|
-
jsonOk(status);
|
|
177
|
-
return;
|
|
178
|
-
}
|
|
179
|
-
if (status.loggedIn) {
|
|
180
|
-
log(`已登录 (${status.mode === 'daemon' ? 'daemon' : 'local cookies'})`);
|
|
181
|
-
if (status.account)
|
|
182
|
-
log(`账号: ${status.account}`);
|
|
183
|
-
}
|
|
184
|
-
else {
|
|
185
|
-
log('未登录,请执行 atlas auth login');
|
|
186
|
-
}
|
|
187
|
-
}
|
|
@@ -1,193 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* atlas auth export / import / doctor 命令。
|
|
3
|
-
*
|
|
4
|
-
* 完全对齐 dws 参数:
|
|
5
|
-
* atlas auth export [-o <path>] [--base64] [--refresh-only] [--include-access-token]
|
|
6
|
-
* atlas auth import [-i <path>] [--base64] [--force] [--persist]
|
|
7
|
-
* atlas auth doctor
|
|
8
|
-
*
|
|
9
|
-
* 安全护栏详见 docs/sandbox-login-plan.md §4.3/§4.4 与 §8。
|
|
10
|
-
*/
|
|
11
|
-
import { readFileSync } from 'fs';
|
|
12
|
-
import { buildBundle, serializeBundleBase64, writeBundleToFile, parseBundleBase64, parseBundleFile, } from '../util/portable-store.js';
|
|
13
|
-
import { detectEnvironment, resolveDaemonConfig, probeDaemonReachable, } from '../util/environment.js';
|
|
14
|
-
import { readCookies, writeCookies } from './session.js';
|
|
15
|
-
import { isJsonMode, jsonOk, log, printError } from '../util/output.js';
|
|
16
|
-
import { ConfigError } from '../util/errors.js';
|
|
17
|
-
import { getCookieFile } from '../util/paths.js';
|
|
18
|
-
const REFRESH_TOKEN_WARNING = '⚠️ 即将输出的 refresh token 是长期凭证,请仅在受信任的环境粘贴,且仅用于一次性导入。';
|
|
19
|
-
/** 当前 ISO 时间戳(命令层调用,纯逻辑层不依赖) */
|
|
20
|
-
function nowIso() {
|
|
21
|
-
return new Date().toISOString();
|
|
22
|
-
}
|
|
23
|
-
export async function exportCmd(opts) {
|
|
24
|
-
// refresh-only 默认开启;--include-access-token 显式关闭
|
|
25
|
-
const refreshOnly = opts.includeAccessToken ? false : opts.refreshOnly !== false;
|
|
26
|
-
const cookies = await readCookies();
|
|
27
|
-
if (!cookies || cookies.length === 0) {
|
|
28
|
-
throw new ConfigError('本地无登录态,无法导出。请先执行:atlas auth login');
|
|
29
|
-
}
|
|
30
|
-
const bundle = buildBundle(cookies, {
|
|
31
|
-
refreshOnly,
|
|
32
|
-
exportedAt: nowIso(),
|
|
33
|
-
});
|
|
34
|
-
// 输出到 stdout(base64 模式无 -o,明显是给 agent 管道用的,先打 stderr 警告)
|
|
35
|
-
if (opts.base64 && !opts.out) {
|
|
36
|
-
log(REFRESH_TOKEN_WARNING);
|
|
37
|
-
const b64 = await serializeBundleBase64(bundle);
|
|
38
|
-
process.stdout.write(b64 + '\n');
|
|
39
|
-
if (opts.json || isJsonMode()) {
|
|
40
|
-
// JSON 模式下也输出信封,但 stdout 已被 b64 占用——只在 stderr 给提示
|
|
41
|
-
log('(JSON 模式下 base64 优先写 stdout;信封省略)');
|
|
42
|
-
}
|
|
43
|
-
return;
|
|
44
|
-
}
|
|
45
|
-
// base64 模式 + -o:写到文件
|
|
46
|
-
if (opts.base64 && opts.out) {
|
|
47
|
-
const resolved = await writeBundleToFile(bundle, opts.out, true);
|
|
48
|
-
if (refreshOnly)
|
|
49
|
-
log(REFRESH_TOKEN_WARNING);
|
|
50
|
-
log(`已导出到 ${resolved}(refresh-only=${refreshOnly},base64 文本)`);
|
|
51
|
-
if (opts.json || isJsonMode()) {
|
|
52
|
-
jsonOk({ exported: true, path: resolved, refreshOnly, base64: true, count: bundle.cookies.length });
|
|
53
|
-
}
|
|
54
|
-
return;
|
|
55
|
-
}
|
|
56
|
-
// 无 base64 写二进制 gzip 文件(需 -o)
|
|
57
|
-
if (opts.out) {
|
|
58
|
-
const resolved = await writeBundleToFile(bundle, opts.out, false);
|
|
59
|
-
if (refreshOnly)
|
|
60
|
-
log(REFRESH_TOKEN_WARNING);
|
|
61
|
-
log(`已导出到 ${resolved}(refresh-only=${refreshOnly},gzip 二进制)`);
|
|
62
|
-
if (opts.json || isJsonMode()) {
|
|
63
|
-
jsonOk({ exported: true, path: resolved, refreshOnly, base64: false, count: bundle.cookies.length });
|
|
64
|
-
}
|
|
65
|
-
return;
|
|
66
|
-
}
|
|
67
|
-
// 既无 base64 也无 -o:默认走 base64 stdout(对齐 dws `export` 无参行为)
|
|
68
|
-
log(REFRESH_TOKEN_WARNING);
|
|
69
|
-
const b64 = await serializeBundleBase64(bundle);
|
|
70
|
-
process.stdout.write(b64 + '\n');
|
|
71
|
-
}
|
|
72
|
-
export async function importCmd(opts) {
|
|
73
|
-
const persist = opts.persist !== false; // 默认落盘
|
|
74
|
-
let bundle;
|
|
75
|
-
let osMismatch = false;
|
|
76
|
-
// 从 stdin 读 base64:--base64 且无 -i
|
|
77
|
-
if (opts.base64 && !opts.input) {
|
|
78
|
-
const b64 = readFileSync(0, 'utf-8').trim(); // fd 0 = stdin
|
|
79
|
-
const result = await parseBundleBase64(b64);
|
|
80
|
-
bundle = result.bundle;
|
|
81
|
-
osMismatch = result.warning.osMismatch;
|
|
82
|
-
}
|
|
83
|
-
else if (opts.input) {
|
|
84
|
-
const result = await parseBundleFile(opts.input);
|
|
85
|
-
bundle = result.bundle;
|
|
86
|
-
osMismatch = result.warning.osMismatch;
|
|
87
|
-
}
|
|
88
|
-
else {
|
|
89
|
-
throw new ConfigError('请指定导入源:-i <文件> 或 --base64 从 stdin 读取(如 ATLAS_COOKIES_B64 环境变量自动导入请使用环境变量方式)');
|
|
90
|
-
}
|
|
91
|
-
if (bundle.cookies.length === 0) {
|
|
92
|
-
throw new ConfigError('导入的 bundle 不含任何 cookies,请确认导出源已登录');
|
|
93
|
-
}
|
|
94
|
-
// 跨 OS 警告(不阻断)
|
|
95
|
-
if (osMismatch) {
|
|
96
|
-
log(`⚠️ bundle 在 ${bundle.manifest.os} 导出,当前为 ${process.platform},跨平台 cookies 可能失效`);
|
|
97
|
-
}
|
|
98
|
-
// 已有 cookies 且未 --force 时拒绝覆盖
|
|
99
|
-
const existing = await readCookies();
|
|
100
|
-
if (existing && existing.length > 0 && !opts.force) {
|
|
101
|
-
throw new ConfigError('本地已存在登录态,拒绝覆盖。使用 --force 强制覆盖(将丢失旧 cookies)');
|
|
102
|
-
}
|
|
103
|
-
if (persist) {
|
|
104
|
-
await writeCookies(bundle.cookies);
|
|
105
|
-
log(`已导入 ${bundle.cookies.length} 个 cookies 到 ${getCookieFile()}`);
|
|
106
|
-
if (opts.json || isJsonMode()) {
|
|
107
|
-
jsonOk({
|
|
108
|
-
imported: true,
|
|
109
|
-
count: bundle.cookies.length,
|
|
110
|
-
osMismatch,
|
|
111
|
-
refreshOnly: bundle.manifest.refreshOnly,
|
|
112
|
-
persisted: true,
|
|
113
|
-
});
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
else {
|
|
117
|
-
// 仅内存缓存(不落盘),用于一次性会话
|
|
118
|
-
// readCookies 走 cachedCookies,需手动注入缓存
|
|
119
|
-
const { __setCachedCookiesForImport } = await import('./session.js');
|
|
120
|
-
__setCachedCookiesForImport(bundle.cookies);
|
|
121
|
-
log(`已内存导入 ${bundle.cookies.length} 个 cookies(未落盘)`);
|
|
122
|
-
if (opts.json || isJsonMode()) {
|
|
123
|
-
jsonOk({
|
|
124
|
-
imported: true,
|
|
125
|
-
count: bundle.cookies.length,
|
|
126
|
-
osMismatch,
|
|
127
|
-
refreshOnly: bundle.manifest.refreshOnly,
|
|
128
|
-
persisted: false,
|
|
129
|
-
});
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
/** 推导 recommendedPath(纯函数,便于测试) */
|
|
134
|
-
export function recommendPath(opts) {
|
|
135
|
-
if (opts.canLaunchBrowser)
|
|
136
|
-
return 'browser';
|
|
137
|
-
if (opts.daemonReachable)
|
|
138
|
-
return 'daemon';
|
|
139
|
-
if (opts.cookiesReady)
|
|
140
|
-
return 'manual_export_hint';
|
|
141
|
-
return 'import';
|
|
142
|
-
}
|
|
143
|
-
export async function doctorCmd(opts) {
|
|
144
|
-
const env = detectEnvironment();
|
|
145
|
-
const daemonCfg = resolveDaemonConfig();
|
|
146
|
-
const daemonReachable = await probeDaemonReachable(daemonCfg.url ?? undefined, daemonCfg.token ?? undefined);
|
|
147
|
-
const recommendedPath = recommendPath({
|
|
148
|
-
canLaunchBrowser: env.canLaunchBrowser,
|
|
149
|
-
cookiesReady: env.cookiesReady,
|
|
150
|
-
daemonReachable,
|
|
151
|
-
daemonVia: daemonCfg.via,
|
|
152
|
-
});
|
|
153
|
-
const result = {
|
|
154
|
-
agentCode: env.agentCode,
|
|
155
|
-
agentSignal: env.agentSignal,
|
|
156
|
-
sandbox: !env.canLaunchBrowser,
|
|
157
|
-
canLaunchBrowser: env.canLaunchBrowser,
|
|
158
|
-
cookies: { path: getCookieFile(), present: env.cookiesReady, expired: null },
|
|
159
|
-
daemon: {
|
|
160
|
-
url: daemonCfg.url,
|
|
161
|
-
reachable: daemonReachable,
|
|
162
|
-
via: daemonCfg.via,
|
|
163
|
-
},
|
|
164
|
-
recommendedPath,
|
|
165
|
-
};
|
|
166
|
-
if (opts.json || isJsonMode()) {
|
|
167
|
-
jsonOk(result);
|
|
168
|
-
return;
|
|
169
|
-
}
|
|
170
|
-
// 人类可读输出
|
|
171
|
-
log('Atlas auth 诊断');
|
|
172
|
-
log(` agent: ${env.agentCode || '(未识别)'} ${env.agentSignal ? `[${env.agentSignal}]` : ''}`);
|
|
173
|
-
log(` 沙盒环境: ${result.sandbox ? '是' : '否'}`);
|
|
174
|
-
log(` 可拉起浏览器: ${env.canLaunchBrowser ? '是' : '否'} (TTY=${env.hasTTY}, DISPLAY=${env.hasDisplay}, container=${env.isContainer})`);
|
|
175
|
-
log(` 本地 cookies: ${env.cookiesReady ? '已就绪' : '缺失'} (${getCookieFile()})`);
|
|
176
|
-
log(` Daemon: ${daemonCfg.url ? `${daemonCfg.url} (${daemonCfg.via})` : '未配置'} - ${daemonReachable ? '可达' : '不可达'}`);
|
|
177
|
-
log(` 推荐路径: ${recommendedPath}`);
|
|
178
|
-
if (result.sandbox && !env.cookiesReady && !daemonReachable) {
|
|
179
|
-
log('');
|
|
180
|
-
log('沙盒登录引导:');
|
|
181
|
-
log(' [推荐] 从本地导出 → 沙盒导入:');
|
|
182
|
-
log(' (本地) atlas auth export --base64');
|
|
183
|
-
log(' (沙盒) export ATLAS_COOKIES_B64=<粘贴上一步输出> && atlas auth doctor');
|
|
184
|
-
log(' [或] 复用宿主机 daemon:');
|
|
185
|
-
log(' (本地) atlas daemon');
|
|
186
|
-
log(' (沙盒) export ATLAS_DAEMON_URL=http://host.docker.internal:8765');
|
|
187
|
-
log(' export ATLAS_DAEMON_TOKEN=<~/.atlas/daemon.token 内容>');
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
|
-
/** 兼容错误打印 helper(cli.ts handleError 用不到,保留给子命令内部用) */
|
|
191
|
-
export function reportError(err, json) {
|
|
192
|
-
printError(err, { json });
|
|
193
|
-
}
|
|
@@ -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
|
-
}
|