daodou-command 1.4.6 → 1.4.8

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.
@@ -3,6 +3,8 @@ const chalk = require('chalk');
3
3
  const fs = require('fs');
4
4
  const path = require('path');
5
5
  const os = require('os');
6
+ const axios = require('axios');
7
+ const ora = require('ora');
6
8
  const { ConfigManager } = require('./config');
7
9
 
8
10
  // 配置管理器
@@ -14,24 +16,23 @@ class BrowserAuth {
14
16
  this.jenkinsUrl = this.config.jenkinsUrl;
15
17
  this.username = this.config.jenkinsUsername;
16
18
  this.password = this.config.jenkinsPassword;
17
- this.cookies = null;
19
+ this.cookies = null; // Jenkins 域名的 cookies
20
+ this.allCookies = null; // 所有域名的 cookies(含 Casdoor)
18
21
  }
19
22
 
20
23
  /**
21
24
  * 启动浏览器并登录Jenkins
22
25
  */
23
26
  async login() {
27
+ const spinner = ora({ text: '正在登录 Jenkins...', indent: 2 }).start();
24
28
  try {
25
- console.log(chalk.blue('🌐 启动浏览器...'));
26
-
27
29
  // 验证 Jenkins URL
28
30
  if (!this.jenkinsUrl || this.jenkinsUrl === 'your-jenkins-url') {
29
31
  throw new Error('Jenkins URL 未配置或为模板值,请检查 .daodourc 文件');
30
32
  }
31
-
32
- this.browser = await puppeteer.launch({
33
+
34
+ this.browser = await puppeteer.launch({
33
35
  headless: true,
34
- // 移除硬编码路径,让 Puppeteer 自动查找
35
36
  args: [
36
37
  '--no-sandbox',
37
38
  '--disable-setuid-sandbox',
@@ -44,44 +45,53 @@ class BrowserAuth {
44
45
  });
45
46
  this.page = await this.browser.newPage();
46
47
  await this.page.setUserAgent('Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36');
47
- console.log(chalk.blue('🔗 访问Jenkins首页...'));
48
- console.log(chalk.gray(` 目标URL: ${this.jenkinsUrl}`));
48
+
49
+ spinner.text = '正在访问 Jenkins...';
49
50
  await this.page.goto(this.jenkinsUrl, { waitUntil: 'networkidle2', timeout: 30000 });
50
- console.log(chalk.green(`✅ 当前页面: ${this.page.url()}`));
51
51
 
52
52
  // 检查是否跳转到Casdoor登录页
53
53
  if (this.page.url().includes('casdoor')) {
54
- console.log(chalk.yellow('📋 检测到Casdoor登录页,自动填写登录信息...'));
55
- // 等待用户名输入框
54
+ spinner.text = '正在通过 Casdoor 认证...';
56
55
  await this.page.waitForSelector('input[name="username"], input[type="text"]', { timeout: 10000 });
57
- // 填写用户名
58
56
  await this.page.type('input[name="username"], input[type="text"]', this.username, {delay: 50});
59
- // 填写密码
60
57
  await this.page.type('input[name="password"], input[type="password"]', this.password, {delay: 50});
61
- // 点击登录按钮
58
+ // 勾选"记住我",延长 session 有效期
59
+ try {
60
+ const rememberCheckbox = await this.page.$('input[name="autoSignin"], input[name="remember"], input[name="rememberMe"], .ant-checkbox-input, input[type="checkbox"]');
61
+ if (rememberCheckbox) {
62
+ const isChecked = await rememberCheckbox.evaluate(el => el.checked);
63
+ if (!isChecked) {
64
+ await rememberCheckbox.click();
65
+ }
66
+ }
67
+ } catch (e) {
68
+ // 找不到记住我选项不影响登录
69
+ }
62
70
  await Promise.all([
63
71
  this.page.click('button[type="submit"], input[type="submit"], .login-button'),
64
72
  this.page.waitForNavigation({ waitUntil: 'networkidle2', timeout: 30000 })
65
73
  ]);
66
- console.log(chalk.green(`✅ 登录表单已提交,当前页面: ${this.page.url()}`));
67
74
  }
68
75
 
69
76
  // 检查是否成功跳转到Jenkins
70
77
  if (this.page.url().includes(this.jenkinsUrl)) {
71
- console.log(chalk.green('🎉 Jenkins登录成功!'));
72
- // 获取cookies
73
- this.cookies = await this.page.cookies();
78
+ // 通过 CDP 获取所有域名的 cookies(含 Casdoor 的长期 cookie)
79
+ const client = await this.page.target().createCDPSession();
80
+ const { cookies: allCookies } = await client.send('Network.getAllCookies');
81
+ this.allCookies = allCookies;
82
+ const jenkinsHost = new URL(this.jenkinsUrl).hostname;
83
+ this.cookies = allCookies.filter(c => c.domain.includes(jenkinsHost));
74
84
  await this.saveCookies();
75
85
  await this.browser.close();
86
+ spinner.succeed('登录成功');
76
87
  return true;
77
88
  } else {
78
- console.log(chalk.red('❌ 登录失败,当前页面:'), this.page.url());
79
89
  await this.browser.close();
80
- throw new Error('自动登录失败,未跳转回Jenkins');
90
+ throw new Error('登录失败,未跳转回 Jenkins');
81
91
  }
82
92
  } catch (error) {
83
- console.error(chalk.red('Jenkins自动登录失败:'), error.message);
84
93
  if (this.browser) await this.browser.close();
94
+ spinner.fail('登录失败 ' + chalk.dim(error.message));
85
95
  throw error;
86
96
  }
87
97
  }
@@ -92,8 +102,12 @@ class BrowserAuth {
92
102
  async saveCookies() {
93
103
  const dir = path.join(os.homedir(), '.daodou');
94
104
  if (!fs.existsSync(dir)) fs.mkdirSync(dir);
105
+ // 保存所有域名的 cookies
95
106
  const file = path.join(dir, 'cookies.json');
96
- fs.writeFileSync(file, JSON.stringify(this.cookies, null, 2));
107
+ fs.writeFileSync(file, JSON.stringify({
108
+ jenkins: this.cookies,
109
+ all: this.allCookies || this.cookies
110
+ }, null, 2));
97
111
  }
98
112
 
99
113
  /**
@@ -102,18 +116,215 @@ class BrowserAuth {
102
116
  loadCookies() {
103
117
  const file = path.join(os.homedir(), '.daodou', 'cookies.json');
104
118
  if (fs.existsSync(file)) {
105
- this.cookies = JSON.parse(fs.readFileSync(file, 'utf-8'));
119
+ const data = JSON.parse(fs.readFileSync(file, 'utf-8'));
120
+ // 兼容旧格式(纯数组)
121
+ if (Array.isArray(data)) {
122
+ this.cookies = data;
123
+ this.allCookies = data;
124
+ } else {
125
+ this.cookies = data.jenkins || [];
126
+ this.allCookies = data.all || data.jenkins || [];
127
+ }
106
128
  return true;
107
129
  }
108
130
  return false;
109
131
  }
110
132
 
111
133
  /**
112
- * 确保已登录并有可用 cookies
134
+ * set-cookie 更新 cookies
135
+ * @param {Array<string>} setCookies 响应头中的 set-cookie 数组
136
+ */
137
+ async updateCookiesFromSetCookie(setCookies) {
138
+ if (!setCookies || !Array.isArray(setCookies)) return;
139
+
140
+ if (!this.cookies) this.cookies = [];
141
+
142
+ let changed = false;
143
+ const jenkinsHost = new URL(this.jenkinsUrl).hostname;
144
+ setCookies.forEach(str => {
145
+ // 简单解析:取第一个分号前的部分作为 name=value
146
+ const firstPart = str.split(';')[0];
147
+ const [rawName, ...valueParts] = firstPart.split('=');
148
+ const name = rawName.trim();
149
+ const value = valueParts.join('=').trim();
150
+
151
+ if (name && value) {
152
+ // 更新 jenkins cookies
153
+ const index = this.cookies.findIndex(c => c.name === name);
154
+ if (index !== -1) {
155
+ if (this.cookies[index].value !== value) {
156
+ this.cookies[index].value = value;
157
+ changed = true;
158
+ }
159
+ } else {
160
+ this.cookies.push({ name, value, domain: jenkinsHost, path: '/' });
161
+ changed = true;
162
+ }
163
+ // 同步更新 allCookies 中对应的 Jenkins cookie
164
+ if (this.allCookies) {
165
+ const allIdx = this.allCookies.findIndex(c => c.name === name && c.domain.includes(jenkinsHost));
166
+ if (allIdx !== -1) {
167
+ this.allCookies[allIdx].value = value;
168
+ } else {
169
+ this.allCookies.push({ name, value, domain: jenkinsHost, path: '/' });
170
+ }
171
+ }
172
+ }
173
+ });
174
+
175
+ if (changed) {
176
+ await this.saveCookies();
177
+ }
178
+ }
179
+
180
+ /**
181
+ * 获取 Cookie 字符串(Jenkins 域名)
182
+ */
183
+ getCookieString() {
184
+ if (!this.cookies) return '';
185
+ return this.cookies.map(c => `${c.name}=${c.value}`).join('; ');
186
+ }
187
+
188
+ /**
189
+ * 获取指定域名的 Cookie 字符串
190
+ */
191
+ getCookieStringForDomain(domain) {
192
+ if (!this.allCookies) return '';
193
+ return this.allCookies
194
+ .filter(c => domain.includes(c.domain.replace(/^\./, '')))
195
+ .map(c => `${c.name}=${c.value}`)
196
+ .join('; ');
197
+ }
198
+
199
+ /**
200
+ * 用 Casdoor cookie 走 SSO 重定向刷新 Jenkins session(无需启动浏览器)
201
+ * @returns {boolean} 是否刷新成功
202
+ */
203
+ async refreshSessionViaCasdoor() {
204
+ if (!this.allCookies || this.allCookies.length === 0) return false;
205
+
206
+ try {
207
+ // 1. 访问 Jenkins,期望 302 跳转到 Casdoor
208
+ const resp1 = await axios.get(this.jenkinsUrl, {
209
+ maxRedirects: 0,
210
+ validateStatus: s => s === 302 || s === 301,
211
+ headers: {
212
+ Cookie: this.getCookieString(),
213
+ 'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36'
214
+ }
215
+ }).catch(e => e.response);
216
+
217
+ if (!resp1 || !resp1.headers.location) return false;
218
+ const casdoorUrl = resp1.headers.location;
219
+ if (!casdoorUrl.includes('casdoor')) return false;
220
+
221
+ // 2. 带 Casdoor cookie 访问授权页,期望自动跳回 Jenkins
222
+ const casdoorDomain = new URL(casdoorUrl).hostname;
223
+ const casdoorCookieStr = this.getCookieStringForDomain(casdoorDomain);
224
+ if (!casdoorCookieStr) return false;
225
+
226
+ const resp2 = await axios.get(casdoorUrl, {
227
+ maxRedirects: 0,
228
+ validateStatus: s => s === 302 || s === 301,
229
+ headers: {
230
+ Cookie: casdoorCookieStr,
231
+ 'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36'
232
+ }
233
+ }).catch(e => e.response);
234
+
235
+ if (!resp2 || !resp2.headers.location) return false;
236
+ const callbackUrl = resp2.headers.location;
237
+
238
+ // 收集 Casdoor 返回的 set-cookie
239
+ const casdoorSetCookies = resp2.headers['set-cookie'] || [];
240
+
241
+ // 3. 跟随回调 URL 到 Jenkins,收集所有重定向中的 Set-Cookie
242
+ let currentUrl = callbackUrl;
243
+ let allSetCookies = [];
244
+ for (let i = 0; i < 5; i++) { // 最多跟随 5 次重定向
245
+ const resp = await axios.get(currentUrl, {
246
+ maxRedirects: 0,
247
+ validateStatus: s => s < 400 || s === 302 || s === 301,
248
+ headers: {
249
+ Cookie: this.getCookieString(),
250
+ 'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36'
251
+ }
252
+ }).catch(e => e.response);
253
+
254
+ if (!resp) return false;
255
+ if (resp.headers['set-cookie']) {
256
+ allSetCookies = allSetCookies.concat(resp.headers['set-cookie']);
257
+ // 实时更新 cookies,后续重定向用新 cookie
258
+ await this.updateCookiesFromSetCookie(resp.headers['set-cookie']);
259
+ }
260
+ if ((resp.status === 302 || resp.status === 301) && resp.headers.location) {
261
+ currentUrl = resp.headers.location;
262
+ // 相对路径补全
263
+ if (currentUrl.startsWith('/')) {
264
+ const base = new URL(this.jenkinsUrl);
265
+ currentUrl = `${base.protocol}//${base.host}${currentUrl}`;
266
+ }
267
+ } else {
268
+ break; // 非重定向,结束
269
+ }
270
+ }
271
+
272
+ if (allSetCookies.length === 0) return false;
273
+
274
+ // 4. 更新 Casdoor cookies
275
+ if (casdoorSetCookies.length > 0) {
276
+ this._updateAllCookiesFromSetCookie(casdoorSetCookies, casdoorDomain);
277
+ }
278
+ // 同步 Jenkins cookie 到 allCookies 并持久化
279
+ await this._syncAndSave();
280
+
281
+ return true;
282
+ } catch (e) {
283
+ return false;
284
+ }
285
+ }
286
+
287
+ /**
288
+ * 更新 allCookies 中指定域名的 cookie
113
289
  */
290
+ _updateAllCookiesFromSetCookie(setCookies, domain) {
291
+ if (!setCookies || !this.allCookies) return;
292
+ setCookies.forEach(str => {
293
+ const firstPart = str.split(';')[0];
294
+ const [rawName, ...valueParts] = firstPart.split('=');
295
+ const name = rawName.trim();
296
+ const value = valueParts.join('=').trim();
297
+ if (name && value) {
298
+ const index = this.allCookies.findIndex(c => c.name === name && c.domain.includes(domain));
299
+ if (index !== -1) {
300
+ this.allCookies[index].value = value;
301
+ } else {
302
+ this.allCookies.push({ name, value, domain, path: '/' });
303
+ }
304
+ }
305
+ });
306
+ }
307
+
308
+ /**
309
+ * 将 jenkins cookies 同步到 allCookies 并持久化
310
+ */
311
+ async _syncAndSave() {
312
+ if (!this.allCookies) this.allCookies = [];
313
+ const jenkinsHost = new URL(this.jenkinsUrl).hostname;
314
+ // 用最新的 jenkins cookies 覆盖 allCookies 中对应的条目
315
+ for (const jc of this.cookies) {
316
+ const idx = this.allCookies.findIndex(c => c.name === jc.name && c.domain.includes(jenkinsHost));
317
+ if (idx !== -1) {
318
+ this.allCookies[idx] = jc;
319
+ } else {
320
+ this.allCookies.push(jc);
321
+ }
322
+ }
323
+ await this.saveCookies();
324
+ }
325
+
114
326
  async ensureLogin() {
115
327
  if (this.loadCookies()) {
116
- console.log(chalk.green('✅ 已加载保存的cookies'));
117
328
  return;
118
329
  }
119
330
  await this.login();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "daodou-command",
3
- "version": "1.4.6",
3
+ "version": "1.4.8",
4
4
  "description": "刀豆命令行工具 - 自动化构建和部署",
5
5
  "main": "index.js",
6
6
  "bin": {
@@ -1,20 +0,0 @@
1
- {
2
- "permissions": {
3
- "allow": [
4
- "Bash(tree:*)",
5
- "Bash(git add:*)",
6
- "Bash(git commit:*)",
7
- "Bash(npm whoami:*)",
8
- "Bash(npm publish)",
9
- "Bash(git push:*)",
10
- "Bash(npm view:*)",
11
- "Bash(npm deprecate:*)",
12
- "Bash(npm unlink:*)",
13
- "Bash(npm install:*)",
14
- "Bash(dao:*)",
15
- "Bash(npm link)"
16
- ],
17
- "deny": [],
18
- "ask": []
19
- }
20
- }
@@ -1,10 +0,0 @@
1
- <?xml version="1.0" encoding="UTF-8"?>
2
- <project version="4">
3
- <component name="cn.fjdmy.uniapp.UniappProjectDataService">
4
- <option name="generalBasePath" value="$PROJECT_DIR$" />
5
- <option name="manifestPath" value="$PROJECT_DIR$/manifest.json" />
6
- <option name="pagesPath" value="$PROJECT_DIR$/pages.json" />
7
- <option name="scanNum" value="1" />
8
- <option name="type" value="store" />
9
- </component>
10
- </project>
@@ -1,6 +0,0 @@
1
- <?xml version="1.0" encoding="UTF-8"?>
2
- <project version="4">
3
- <component name="AgentMigrationStateService">
4
- <option name="migrationStatus" value="COMPLETED" />
5
- </component>
6
- </project>
@@ -1,6 +0,0 @@
1
- <?xml version="1.0" encoding="UTF-8"?>
2
- <project version="4">
3
- <component name="AskMigrationStateService">
4
- <option name="migrationStatus" value="COMPLETED" />
5
- </component>
6
- </project>
@@ -1,6 +0,0 @@
1
- <?xml version="1.0" encoding="UTF-8"?>
2
- <project version="4">
3
- <component name="Ask2AgentMigrationStateService">
4
- <option name="migrationStatus" value="COMPLETED" />
5
- </component>
6
- </project>
@@ -1,6 +0,0 @@
1
- <?xml version="1.0" encoding="UTF-8"?>
2
- <project version="4">
3
- <component name="EditMigrationStateService">
4
- <option name="migrationStatus" value="COMPLETED" />
5
- </component>
6
- </project>
@@ -1,12 +0,0 @@
1
- <?xml version="1.0" encoding="UTF-8"?>
2
- <module type="WEB_MODULE" version="4">
3
- <component name="NewModuleRootManager">
4
- <content url="file://$MODULE_DIR$">
5
- <excludeFolder url="file://$MODULE_DIR$/.tmp" />
6
- <excludeFolder url="file://$MODULE_DIR$/temp" />
7
- <excludeFolder url="file://$MODULE_DIR$/tmp" />
8
- </content>
9
- <orderEntry type="inheritedJdk" />
10
- <orderEntry type="sourceFolder" forTests="false" />
11
- </component>
12
- </module>
@@ -1,6 +0,0 @@
1
- <?xml version="1.0" encoding="UTF-8"?>
2
- <project version="4">
3
- <component name="com.github.wanniwa.editorjumper.settings.EditorJumperProjectSettings">
4
- <option name="projectEditorType" value="Cursor" />
5
- </component>
6
- </project>
package/.idea/modules.xml DELETED
@@ -1,8 +0,0 @@
1
- <?xml version="1.0" encoding="UTF-8"?>
2
- <project version="4">
3
- <component name="ProjectModuleManager">
4
- <modules>
5
- <module fileurl="file://$PROJECT_DIR$/.idea/daodou-command.iml" filepath="$PROJECT_DIR$/.idea/daodou-command.iml" />
6
- </modules>
7
- </component>
8
- </project>
package/.idea/vcs.xml DELETED
@@ -1,6 +0,0 @@
1
- <?xml version="1.0" encoding="UTF-8"?>
2
- <project version="4">
3
- <component name="VcsDirectoryMappings">
4
- <mapping directory="" vcs="Git" />
5
- </component>
6
- </project>
@@ -1,84 +0,0 @@
1
- <?xml version="1.0" encoding="UTF-8"?>
2
- <project version="4">
3
- <component name="AutoImportSettings">
4
- <option name="autoReloadType" value="SELECTIVE" />
5
- </component>
6
- <component name="ChangeListManager">
7
- <list default="true" id="1537cb23-f918-4011-a0ed-24da46bf53bc" name="更改" comment="" />
8
- <option name="SHOW_DIALOG" value="false" />
9
- <option name="HIGHLIGHT_CONFLICTS" value="true" />
10
- <option name="HIGHLIGHT_NON_ACTIVE_CHANGELIST" value="false" />
11
- <option name="LAST_RESOLUTION" value="IGNORE" />
12
- </component>
13
- <component name="Git.Settings">
14
- <option name="RECENT_GIT_ROOT_PATH" value="$PROJECT_DIR$" />
15
- </component>
16
- <component name="GitHubPullRequestSearchHistory">{
17
- &quot;lastFilter&quot;: {
18
- &quot;state&quot;: &quot;OPEN&quot;,
19
- &quot;assignee&quot;: &quot;h025&quot;
20
- }
21
- }</component>
22
- <component name="GithubPullRequestsUISettings">{
23
- &quot;selectedUrlAndAccountId&quot;: {
24
- &quot;url&quot;: &quot;https://github.com/h025/daodou-command.git&quot;,
25
- &quot;accountId&quot;: &quot;c8b4f74c-2e36-4aad-b608-940f73fc88db&quot;
26
- }
27
- }</component>
28
- <component name="ProjectColorInfo">{
29
- &quot;associatedIndex&quot;: 6
30
- }</component>
31
- <component name="ProjectId" id="33EP70DuHXVnueD9LcTKpn8t7Vr" />
32
- <component name="ProjectViewState">
33
- <option name="hideEmptyMiddlePackages" value="true" />
34
- <option name="showLibraryContents" value="true" />
35
- </component>
36
- <component name="PropertiesComponent">{
37
- &quot;keyToString&quot;: {
38
- &quot;ModuleVcsDetector.initialDetectionPerformed&quot;: &quot;true&quot;,
39
- &quot;RunOnceActivity.ShowReadmeOnStart&quot;: &quot;true&quot;,
40
- &quot;RunOnceActivity.git.unshallow&quot;: &quot;true&quot;,
41
- &quot;git-widget-placeholder&quot;: &quot;main&quot;,
42
- &quot;last_opened_file_path&quot;: &quot;/Users/summermr/daodou/daodou-command&quot;,
43
- &quot;node.js.detected.package.eslint&quot;: &quot;true&quot;,
44
- &quot;node.js.detected.package.tslint&quot;: &quot;true&quot;,
45
- &quot;node.js.selected.package.eslint&quot;: &quot;(autodetect)&quot;,
46
- &quot;node.js.selected.package.tslint&quot;: &quot;(autodetect)&quot;,
47
- &quot;nodejs_package_manager_path&quot;: &quot;npm&quot;,
48
- &quot;vue.rearranger.settings.migration&quot;: &quot;true&quot;
49
- }
50
- }</component>
51
- <component name="SharedIndexes">
52
- <attachedChunks>
53
- <set>
54
- <option value="bundled-js-predefined-d6986cc7102b-3aa1da707db6-JavaScript-WS-252.27397.92" />
55
- </set>
56
- </attachedChunks>
57
- </component>
58
- <component name="TaskManager">
59
- <task active="true" id="Default" summary="默认任务">
60
- <changelist id="1537cb23-f918-4011-a0ed-24da46bf53bc" name="更改" comment="" />
61
- <created>1758879086844</created>
62
- <option name="number" value="Default" />
63
- <option name="presentableId" value="Default" />
64
- <updated>1758879086844</updated>
65
- <workItem from="1758879088110" duration="1336000" />
66
- <workItem from="1763003382850" duration="4466000" />
67
- </task>
68
- <servers />
69
- </component>
70
- <component name="TypeScriptGeneratedFilesManager">
71
- <option name="version" value="3" />
72
- </component>
73
- <component name="Vcs.Log.Tabs.Properties">
74
- <option name="TAB_STATES">
75
- <map>
76
- <entry key="MAIN">
77
- <value>
78
- <State />
79
- </value>
80
- </entry>
81
- </map>
82
- </option>
83
- </component>
84
- </project>