chandao4 0.1.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.
Files changed (49) hide show
  1. package/.env.example +10 -0
  2. package/CHANGELOG.md +21 -0
  3. package/LICENSE +21 -0
  4. package/README.md +184 -0
  5. package/dist/commands/bug.d.ts +3 -0
  6. package/dist/commands/bug.js +373 -0
  7. package/dist/commands/config.d.ts +2 -0
  8. package/dist/commands/config.js +38 -0
  9. package/dist/commands/login.d.ts +3 -0
  10. package/dist/commands/login.js +83 -0
  11. package/dist/commands/product.d.ts +3 -0
  12. package/dist/commands/product.js +41 -0
  13. package/dist/commands/project.d.ts +3 -0
  14. package/dist/commands/project.js +70 -0
  15. package/dist/commands/task.d.ts +3 -0
  16. package/dist/commands/task.js +445 -0
  17. package/dist/config/config.d.ts +17 -0
  18. package/dist/config/config.js +216 -0
  19. package/dist/config/defaults.d.ts +5 -0
  20. package/dist/config/defaults.js +23 -0
  21. package/dist/core/api-client.d.ts +20 -0
  22. package/dist/core/api-client.js +127 -0
  23. package/dist/core/auth.d.ts +44 -0
  24. package/dist/core/auth.js +244 -0
  25. package/dist/core/errors.d.ts +17 -0
  26. package/dist/core/errors.js +61 -0
  27. package/dist/index.d.ts +2 -0
  28. package/dist/index.js +125 -0
  29. package/dist/services/bug.service.d.ts +59 -0
  30. package/dist/services/bug.service.js +232 -0
  31. package/dist/services/product.service.d.ts +12 -0
  32. package/dist/services/product.service.js +43 -0
  33. package/dist/services/project.service.d.ts +18 -0
  34. package/dist/services/project.service.js +35 -0
  35. package/dist/services/task.service.d.ts +55 -0
  36. package/dist/services/task.service.js +254 -0
  37. package/dist/types/api.d.ts +31 -0
  38. package/dist/types/api.js +3 -0
  39. package/dist/types/config.d.ts +18 -0
  40. package/dist/types/config.js +3 -0
  41. package/dist/types/models.d.ts +65 -0
  42. package/dist/types/models.js +33 -0
  43. package/dist/utils/format.d.ts +38 -0
  44. package/dist/utils/format.js +201 -0
  45. package/dist/utils/prompt.d.ts +12 -0
  46. package/dist/utils/prompt.js +154 -0
  47. package/dist/utils/validators.d.ts +14 -0
  48. package/dist/utils/validators.js +42 -0
  49. package/package.json +63 -0
@@ -0,0 +1,216 @@
1
+ "use strict";
2
+ // 配置加载器:文件 + 环境变量 + 默认值
3
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
4
+ if (k2 === undefined) k2 = k;
5
+ var desc = Object.getOwnPropertyDescriptor(m, k);
6
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
7
+ desc = { enumerable: true, get: function() { return m[k]; } };
8
+ }
9
+ Object.defineProperty(o, k2, desc);
10
+ }) : (function(o, m, k, k2) {
11
+ if (k2 === undefined) k2 = k;
12
+ o[k2] = m[k];
13
+ }));
14
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
15
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
16
+ }) : function(o, v) {
17
+ o["default"] = v;
18
+ });
19
+ var __importStar = (this && this.__importStar) || (function () {
20
+ var ownKeys = function(o) {
21
+ ownKeys = Object.getOwnPropertyNames || function (o) {
22
+ var ar = [];
23
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
24
+ return ar;
25
+ };
26
+ return ownKeys(o);
27
+ };
28
+ return function (mod) {
29
+ if (mod && mod.__esModule) return mod;
30
+ var result = {};
31
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
32
+ __setModuleDefault(result, mod);
33
+ return result;
34
+ };
35
+ })();
36
+ Object.defineProperty(exports, "__esModule", { value: true });
37
+ exports.loadRawConfig = loadRawConfig;
38
+ exports.loadConfig = loadConfig;
39
+ exports.saveUserConfig = saveUserConfig;
40
+ exports.clearUserConfig = clearUserConfig;
41
+ const fs = __importStar(require("fs"));
42
+ const path = __importStar(require("path"));
43
+ const os = __importStar(require("os"));
44
+ const zod_1 = require("zod");
45
+ const defaults_1 = require("./defaults");
46
+ // Zod schema for validation
47
+ const serverSchema = zod_1.z.object({
48
+ url: zod_1.z.string().url(),
49
+ username: zod_1.z.string().optional(),
50
+ password: zod_1.z.string().optional(),
51
+ code: zod_1.z.string().optional(),
52
+ token: zod_1.z.string().optional(),
53
+ });
54
+ const outputSchema = zod_1.z.object({
55
+ format: zod_1.z.enum(['table', 'json']).default('table'),
56
+ color: zod_1.z.boolean().default(true),
57
+ });
58
+ const configSchema = zod_1.z.object({
59
+ server: serverSchema,
60
+ output: outputSchema.optional(),
61
+ });
62
+ /**
63
+ * 加载配置文件(项目级或用户级)
64
+ */
65
+ function loadConfigFile() {
66
+ // 1. 尝试项目级配置
67
+ const cwd = process.cwd();
68
+ for (const name of defaults_1.CONFIG_FILE_NAMES) {
69
+ const filePath = path.join(cwd, name);
70
+ if (fs.existsSync(filePath)) {
71
+ try {
72
+ const content = fs.readFileSync(filePath, 'utf-8');
73
+ return JSON.parse(content);
74
+ }
75
+ catch {
76
+ // 配置文件格式错误,忽略
77
+ }
78
+ }
79
+ }
80
+ // 2. 尝试用户级配置
81
+ const userConfigPath = path.join(os.homedir(), defaults_1.USER_CONFIG_DIR, defaults_1.USER_CONFIG_FILE);
82
+ if (fs.existsSync(userConfigPath)) {
83
+ try {
84
+ const content = fs.readFileSync(userConfigPath, 'utf-8');
85
+ return JSON.parse(content);
86
+ }
87
+ catch {
88
+ // 配置文件格式错误,忽略
89
+ }
90
+ }
91
+ return null;
92
+ }
93
+ /**
94
+ * 从环境变量加载配置
95
+ */
96
+ function loadEnvConfig() {
97
+ const config = {};
98
+ const server = {};
99
+ if (process.env.ZENTAO_URL)
100
+ server.url = process.env.ZENTAO_URL;
101
+ if (process.env.ZENTAO_USERNAME)
102
+ server.username = process.env.ZENTAO_USERNAME;
103
+ if (process.env.ZENTAO_PASSWORD)
104
+ server.password = process.env.ZENTAO_PASSWORD;
105
+ if (process.env.ZENTAO_CODE)
106
+ server.code = process.env.ZENTAO_CODE;
107
+ if (process.env.ZENTAO_TOKEN)
108
+ server.token = process.env.ZENTAO_TOKEN;
109
+ if (Object.keys(server).length > 0) {
110
+ config.server = server;
111
+ }
112
+ return config;
113
+ }
114
+ /**
115
+ * 合并配置:环境变量 > 项目配置 > 用户配置 > 默认值
116
+ */
117
+ function mergeConfigs(...configs) {
118
+ const result = structuredClone(defaults_1.DEFAULTS);
119
+ for (const config of configs) {
120
+ if (!config)
121
+ continue;
122
+ if (config.server) {
123
+ Object.assign(result.server, config.server);
124
+ }
125
+ if (config.output) {
126
+ Object.assign(result.output, config.output);
127
+ }
128
+ }
129
+ return result;
130
+ }
131
+ /**
132
+ * 推断认证模式
133
+ */
134
+ function detectAuthMode(config) {
135
+ // 优先使用 API Key 模式
136
+ if (config.server.code && config.server.token) {
137
+ return 'apikey';
138
+ }
139
+ // 否则使用 Session 模式
140
+ if (config.server.username && config.server.password) {
141
+ return 'session';
142
+ }
143
+ // 只有 URL,等待运行时补充凭据
144
+ return 'session';
145
+ }
146
+ /**
147
+ * 不校验,直接读取原始配置(用于 login 之前读取已有配置)
148
+ */
149
+ function loadRawConfig() {
150
+ const fileConfig = loadConfigFile();
151
+ const envConfig = loadEnvConfig();
152
+ return mergeConfigs(fileConfig, envConfig);
153
+ }
154
+ /**
155
+ * 加载并校验完整配置
156
+ */
157
+ function loadConfig() {
158
+ const merged = loadRawConfig();
159
+ // 校验
160
+ const parsed = configSchema.safeParse(merged);
161
+ if (!parsed.success) {
162
+ console.error('配置错误:', parsed.error.issues.map(i => i.message).join(', '));
163
+ process.exit(1);
164
+ }
165
+ const finalConfig = parsed.data;
166
+ const authMode = detectAuthMode(finalConfig);
167
+ return {
168
+ ...finalConfig,
169
+ authMode,
170
+ output: finalConfig.output ?? defaults_1.DEFAULTS.output,
171
+ };
172
+ }
173
+ /**
174
+ * 保存配置到用户目录
175
+ */
176
+ function saveUserConfig(key, value) {
177
+ const configDir = path.join(os.homedir(), defaults_1.USER_CONFIG_DIR);
178
+ const configPath = path.join(configDir, defaults_1.USER_CONFIG_FILE);
179
+ if (!fs.existsSync(configDir)) {
180
+ fs.mkdirSync(configDir, { recursive: true });
181
+ }
182
+ let config = {};
183
+ if (fs.existsSync(configPath)) {
184
+ try {
185
+ config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
186
+ }
187
+ catch {
188
+ // ignore
189
+ }
190
+ }
191
+ // 支持点号路径如 'server.url'
192
+ const keys = key.split('.');
193
+ let current = config;
194
+ for (let i = 0; i < keys.length - 1; i++) {
195
+ if (!current[keys[i]]) {
196
+ current[keys[i]] = {};
197
+ }
198
+ current = current[keys[i]];
199
+ }
200
+ current[keys[keys.length - 1]] = value;
201
+ fs.writeFileSync(configPath, JSON.stringify(config, null, 2), 'utf-8');
202
+ console.log(`配置已保存: ${key} = ${value}`);
203
+ }
204
+ /**
205
+ * 清除用户配置
206
+ */
207
+ function clearUserConfig() {
208
+ const configPath = path.join(os.homedir(), defaults_1.USER_CONFIG_DIR, defaults_1.USER_CONFIG_FILE);
209
+ if (fs.existsSync(configPath)) {
210
+ fs.unlinkSync(configPath);
211
+ console.log('配置已清除');
212
+ }
213
+ else {
214
+ console.log('没有保存的配置');
215
+ }
216
+ }
@@ -0,0 +1,5 @@
1
+ import { ZentaoConfig } from '../types/config';
2
+ export declare const DEFAULTS: ZentaoConfig;
3
+ export declare const CONFIG_FILE_NAMES: string[];
4
+ export declare const USER_CONFIG_DIR = ".chandao4";
5
+ export declare const USER_CONFIG_FILE = "config.json";
@@ -0,0 +1,23 @@
1
+ "use strict";
2
+ // 默认配置
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ exports.USER_CONFIG_FILE = exports.USER_CONFIG_DIR = exports.CONFIG_FILE_NAMES = exports.DEFAULTS = void 0;
5
+ exports.DEFAULTS = {
6
+ server: {
7
+ url: '',
8
+ },
9
+ output: {
10
+ format: 'table',
11
+ color: true,
12
+ },
13
+ };
14
+ // 配置文件搜索路径
15
+ exports.CONFIG_FILE_NAMES = [
16
+ '.chandao4.config.json',
17
+ '.chandao4.json',
18
+ '.zentao.config.json',
19
+ '.zentao.json',
20
+ ];
21
+ // 用户级配置文件路径(~/.chandao4/config.json)
22
+ exports.USER_CONFIG_DIR = '.chandao4';
23
+ exports.USER_CONFIG_FILE = 'config.json';
@@ -0,0 +1,20 @@
1
+ import { AuthManager } from './auth';
2
+ import { RuntimeConfig } from '../types/config';
3
+ export declare class ApiClient {
4
+ private auth;
5
+ private baseUrl;
6
+ private debug;
7
+ constructor(config: RuntimeConfig, auth: AuthManager);
8
+ setDebug(enabled: boolean): void;
9
+ private logRequest;
10
+ private logResponse;
11
+ /** GET JSON 请求 */
12
+ getJson(url: string): Promise<any>;
13
+ /** GET HTML 请求 */
14
+ getHtml(url: string): Promise<string>;
15
+ /** POST 请求(返回原始响应) */
16
+ postHtml(url: string, data?: Record<string, string>): Promise<string>;
17
+ /** POST JSON 请求(解析返回的 JSON) */
18
+ postJson(url: string, data?: Record<string, string>): Promise<any>;
19
+ ping(): Promise<boolean>;
20
+ }
@@ -0,0 +1,127 @@
1
+ "use strict";
2
+ // API 客户端:支持 JSON 和 HTML 两种模式
3
+ var __importDefault = (this && this.__importDefault) || function (mod) {
4
+ return (mod && mod.__esModule) ? mod : { "default": mod };
5
+ };
6
+ Object.defineProperty(exports, "__esModule", { value: true });
7
+ exports.ApiClient = void 0;
8
+ const axios_1 = __importDefault(require("axios"));
9
+ const chalk_1 = __importDefault(require("chalk"));
10
+ class ApiClient {
11
+ auth;
12
+ baseUrl;
13
+ debug = false;
14
+ constructor(config, auth) {
15
+ this.auth = auth;
16
+ this.baseUrl = config.server.url.replace(/\/$/, '');
17
+ }
18
+ setDebug(enabled) {
19
+ this.debug = enabled;
20
+ }
21
+ logRequest(method, url, body) {
22
+ if (!this.debug)
23
+ return;
24
+ console.error(chalk_1.default.gray(`\n── DEBUG ──────────────────────────────`));
25
+ console.error(chalk_1.default.cyan(`→ ${method} ${url}`));
26
+ if (body && Object.keys(body).length > 0) {
27
+ console.error(chalk_1.default.gray(' Body:'), JSON.stringify(body, null, 2));
28
+ }
29
+ }
30
+ logResponse(data) {
31
+ if (!this.debug)
32
+ return;
33
+ const preview = JSON.stringify(data, null, 2);
34
+ const truncated = preview.length > 2000 ? preview.substring(0, 2000) + '\n... (truncated)' : preview;
35
+ console.error(chalk_1.default.gray('← Response:'));
36
+ console.error(chalk_1.default.gray(truncated));
37
+ console.error(chalk_1.default.gray('────────────────────────────────────────\n'));
38
+ }
39
+ /** GET JSON 请求 */
40
+ async getJson(url) {
41
+ await this.auth.ensureAuth();
42
+ const fullUrl = `${this.baseUrl}${url}`;
43
+ this.logRequest('GET', fullUrl);
44
+ const res = await axios_1.default.get(fullUrl, {
45
+ headers: {
46
+ 'Cookie': this.auth.getCookieHeader(),
47
+ 'User-Agent': 'Mozilla/5.0',
48
+ },
49
+ timeout: 15000,
50
+ });
51
+ const data = res.data;
52
+ this.logResponse(data);
53
+ // Zentao .json 响应格式: { status: "success", data: "{...}" }
54
+ if (data?.status === 'success' && typeof data.data === 'string') {
55
+ try {
56
+ return JSON.parse(data.data);
57
+ }
58
+ catch {
59
+ return data;
60
+ }
61
+ }
62
+ return data;
63
+ }
64
+ /** GET HTML 请求 */
65
+ async getHtml(url) {
66
+ await this.auth.ensureAuth();
67
+ const res = await axios_1.default.get(`${this.baseUrl}${url}`, {
68
+ headers: {
69
+ 'Cookie': this.auth.getCookieHeader(),
70
+ 'User-Agent': 'Mozilla/5.0',
71
+ },
72
+ timeout: 15000,
73
+ });
74
+ return typeof res.data === 'string' ? res.data : JSON.stringify(res.data);
75
+ }
76
+ /** POST 请求(返回原始响应) */
77
+ async postHtml(url, data) {
78
+ await this.auth.ensureAuth();
79
+ const body = data ? new URLSearchParams(data).toString() : undefined;
80
+ const fullUrl = `${this.baseUrl}${url}`;
81
+ this.logRequest('POST', fullUrl, data);
82
+ const res = await axios_1.default.post(fullUrl, body, {
83
+ headers: {
84
+ 'Cookie': this.auth.getCookieHeader(),
85
+ 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',
86
+ 'X-Requested-With': 'XMLHttpRequest',
87
+ 'Accept': 'application/json, text/plain, */*',
88
+ 'User-Agent': 'Mozilla/5.0',
89
+ },
90
+ timeout: 15000,
91
+ });
92
+ this.auth.updateCookies(res);
93
+ const raw = typeof res.data === 'string' ? res.data : JSON.stringify(res.data);
94
+ this.logResponse(raw);
95
+ return raw;
96
+ }
97
+ /** POST JSON 请求(解析返回的 JSON) */
98
+ async postJson(url, data) {
99
+ const raw = await this.postHtml(url, data);
100
+ try {
101
+ const parsed = JSON.parse(raw);
102
+ // Zentao .json 响应格式: { status: "success", data: "{...}" }
103
+ if (parsed?.status === 'success' && typeof parsed.data === 'string') {
104
+ try {
105
+ return JSON.parse(parsed.data);
106
+ }
107
+ catch {
108
+ return parsed;
109
+ }
110
+ }
111
+ return parsed;
112
+ }
113
+ catch {
114
+ return raw;
115
+ }
116
+ }
117
+ async ping() {
118
+ try {
119
+ const data = await this.getJson('/product-all.json');
120
+ return data && data.products && Object.keys(data.products).length > 0;
121
+ }
122
+ catch {
123
+ return false;
124
+ }
125
+ }
126
+ }
127
+ exports.ApiClient = ApiClient;
@@ -0,0 +1,44 @@
1
+ import { RuntimeConfig } from '../types/config';
2
+ export interface AuthState {
3
+ zentaosid: string | null;
4
+ account: string | null;
5
+ expiresAt: number | null;
6
+ /** 所有 cookie,用于后续请求 */
7
+ cookies: Record<string, string>;
8
+ }
9
+ /**
10
+ * 禅道企业版 4.1.3 (基于 Zentao 12.5.3) 认证管理器
11
+ *
12
+ * 登录流程(从页面 JS 逆向分析):
13
+ * 1. GET /user-login.html → 提取 verifyRand, referer + 所有 cookie
14
+ * 2. password = md5(md5(rawPassword) + verifyRand)
15
+ * 3. AJAX POST /user-login.html → 返回 JSON { result: "success" }
16
+ * 4. 登录成功,zentaosid 已被激活
17
+ */
18
+ export declare class AuthManager {
19
+ private state;
20
+ private config;
21
+ constructor(config: RuntimeConfig);
22
+ getSessionId(): string | null;
23
+ getApiKeyParams(): {
24
+ code: string;
25
+ token: string;
26
+ } | null;
27
+ getAuthParams(): string;
28
+ getAuthMode(): 'session' | 'apikey';
29
+ isApiKeyMode(): boolean;
30
+ /**
31
+ * 从响应更新 cookie jar
32
+ */
33
+ updateCookies(res: any): void;
34
+ /**
35
+ * 获取完整的 cookie header 字符串
36
+ */
37
+ getCookieHeader(): string;
38
+ /**
39
+ * 执行登录
40
+ */
41
+ login(): Promise<boolean>;
42
+ ensureAuth(): Promise<boolean>;
43
+ getAuthHeaders(): Record<string, string>;
44
+ }
@@ -0,0 +1,244 @@
1
+ "use strict";
2
+ // 认证管理器:Session Cookie 登录
3
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
4
+ if (k2 === undefined) k2 = k;
5
+ var desc = Object.getOwnPropertyDescriptor(m, k);
6
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
7
+ desc = { enumerable: true, get: function() { return m[k]; } };
8
+ }
9
+ Object.defineProperty(o, k2, desc);
10
+ }) : (function(o, m, k, k2) {
11
+ if (k2 === undefined) k2 = k;
12
+ o[k2] = m[k];
13
+ }));
14
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
15
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
16
+ }) : function(o, v) {
17
+ o["default"] = v;
18
+ });
19
+ var __importStar = (this && this.__importStar) || (function () {
20
+ var ownKeys = function(o) {
21
+ ownKeys = Object.getOwnPropertyNames || function (o) {
22
+ var ar = [];
23
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
24
+ return ar;
25
+ };
26
+ return ownKeys(o);
27
+ };
28
+ return function (mod) {
29
+ if (mod && mod.__esModule) return mod;
30
+ var result = {};
31
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
32
+ __setModuleDefault(result, mod);
33
+ return result;
34
+ };
35
+ })();
36
+ var __importDefault = (this && this.__importDefault) || function (mod) {
37
+ return (mod && mod.__esModule) ? mod : { "default": mod };
38
+ };
39
+ Object.defineProperty(exports, "__esModule", { value: true });
40
+ exports.AuthManager = void 0;
41
+ const axios_1 = __importDefault(require("axios"));
42
+ const crypto = __importStar(require("crypto"));
43
+ /**
44
+ * 禅道企业版 4.1.3 (基于 Zentao 12.5.3) 认证管理器
45
+ *
46
+ * 登录流程(从页面 JS 逆向分析):
47
+ * 1. GET /user-login.html → 提取 verifyRand, referer + 所有 cookie
48
+ * 2. password = md5(md5(rawPassword) + verifyRand)
49
+ * 3. AJAX POST /user-login.html → 返回 JSON { result: "success" }
50
+ * 4. 登录成功,zentaosid 已被激活
51
+ */
52
+ class AuthManager {
53
+ state = {
54
+ zentaosid: null,
55
+ account: null,
56
+ expiresAt: null,
57
+ cookies: {},
58
+ };
59
+ config;
60
+ constructor(config) {
61
+ this.config = config;
62
+ }
63
+ getSessionId() {
64
+ if (this.state.zentaosid && this.state.expiresAt && Date.now() < this.state.expiresAt) {
65
+ return this.state.zentaosid;
66
+ }
67
+ return null;
68
+ }
69
+ getApiKeyParams() {
70
+ if (this.config.server.code && this.config.server.token) {
71
+ return { code: this.config.server.code, token: this.config.server.token };
72
+ }
73
+ return null;
74
+ }
75
+ getAuthParams() {
76
+ const apiKey = this.getApiKeyParams();
77
+ if (apiKey) {
78
+ return `code=${encodeURIComponent(apiKey.code)}&token=${encodeURIComponent(apiKey.token)}`;
79
+ }
80
+ return '';
81
+ }
82
+ getAuthMode() {
83
+ return this.config.authMode;
84
+ }
85
+ isApiKeyMode() {
86
+ return this.config.authMode === 'apikey';
87
+ }
88
+ /**
89
+ * 从响应更新 cookie jar
90
+ */
91
+ updateCookies(res) {
92
+ const setCookie = res.headers['set-cookie'];
93
+ if (!setCookie)
94
+ return;
95
+ const arr = Array.isArray(setCookie) ? setCookie : [setCookie];
96
+ for (const c of arr) {
97
+ const [nv] = c.split(';');
98
+ const [name, ...rest] = nv.split('=');
99
+ const key = name.trim();
100
+ const value = rest.join('=');
101
+ this.state.cookies[key] = value;
102
+ if (key === 'zentaosid') {
103
+ this.state.zentaosid = value;
104
+ }
105
+ }
106
+ }
107
+ /**
108
+ * 获取完整的 cookie header 字符串
109
+ */
110
+ getCookieHeader() {
111
+ return Object.entries(this.state.cookies)
112
+ .map(([k, v]) => `${k}=${v}`)
113
+ .join('; ');
114
+ }
115
+ /**
116
+ * 执行登录
117
+ */
118
+ async login() {
119
+ const { url, username, password } = this.config.server;
120
+ if (!username || !password) {
121
+ console.error('错误: 需要用户名和密码。');
122
+ console.error(' 设置环境变量: ZENTAO_USERNAME=yourname ZENTAO_PASSWORD=yourpass');
123
+ return false;
124
+ }
125
+ try {
126
+ const baseUrl = url.replace(/\/$/, '');
127
+ const client = axios_1.default.create({
128
+ baseURL: baseUrl,
129
+ timeout: 15000,
130
+ maxRedirects: 5,
131
+ headers: { 'User-Agent': 'zentao-cli/1.0' },
132
+ });
133
+ // ---- Step 1: 获取登录页面 ----
134
+ const loginPageRes = await client.get('/user-login.html');
135
+ this.updateCookies(loginPageRes);
136
+ const html = typeof loginPageRes.data === 'string' ? loginPageRes.data : '';
137
+ // 提取 verifyRand
138
+ let verifyRand = '';
139
+ const randMatch = html.match(/id='verifyRand'\s+value='(\d+)'/);
140
+ if (randMatch) {
141
+ verifyRand = randMatch[1];
142
+ }
143
+ else {
144
+ const altMatch = html.match(/name='verifyRand'\s+id='verifyRand'\s+value='(\d+)'/);
145
+ if (altMatch)
146
+ verifyRand = altMatch[1];
147
+ }
148
+ // 提取 referer
149
+ let referer = '/';
150
+ const refMatch = html.match(/id='referer'\s+value='([^']*)'/);
151
+ if (refMatch)
152
+ referer = refMatch[1] || '/';
153
+ // ---- Step 2: 计算密码哈希 ----
154
+ const md5Once = crypto.createHash('md5').update(password).digest('hex');
155
+ const hashedPassword = crypto.createHash('md5').update(md5Once + verifyRand).digest('hex');
156
+ // ---- Step 3: AJAX POST 登录 ----
157
+ const loginParams = new URLSearchParams();
158
+ loginParams.append('account', username);
159
+ loginParams.append('password', hashedPassword);
160
+ loginParams.append('passwordStrength', '0');
161
+ loginParams.append('referer', referer);
162
+ loginParams.append('verifyRand', verifyRand);
163
+ loginParams.append('keepLogin', '0');
164
+ let loginResult;
165
+ try {
166
+ const loginRes = await client.post('/user-login.html', loginParams.toString(), {
167
+ headers: {
168
+ 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',
169
+ 'X-Requested-With': 'XMLHttpRequest',
170
+ 'Accept': 'application/json, text/javascript, */*; q=0.01',
171
+ 'Cookie': this.getCookieHeader(),
172
+ },
173
+ });
174
+ this.updateCookies(loginRes);
175
+ loginResult = loginRes.data;
176
+ }
177
+ catch (err) {
178
+ if (err.response?.status && err.response.status >= 300 && err.response.status < 400) {
179
+ loginResult = { result: 'success', locate: err.response.headers['location'] || '/index.php?m=my&f=index' };
180
+ if (err.response.headers['set-cookie']) {
181
+ this.updateCookies(err.response);
182
+ }
183
+ }
184
+ else {
185
+ throw err;
186
+ }
187
+ }
188
+ // 解析响应
189
+ if (typeof loginResult === 'string') {
190
+ try {
191
+ loginResult = JSON.parse(loginResult);
192
+ }
193
+ catch {
194
+ if (loginResult.includes('我的地盘') || loginResult.includes('my/index')) {
195
+ loginResult = { result: 'success' };
196
+ }
197
+ }
198
+ }
199
+ if (loginResult?.result !== 'success') {
200
+ const errMsg = loginResult?.message || '用户名或密码错误';
201
+ console.error(`登录失败: ${errMsg}`);
202
+ return false;
203
+ }
204
+ // 登录成功
205
+ if (this.state.zentaosid) {
206
+ this.state.account = username;
207
+ this.state.expiresAt = Date.now() + 24 * 60 * 60 * 1000;
208
+ return true;
209
+ }
210
+ console.error('登录失败: 未能获取会话 cookie');
211
+ return false;
212
+ }
213
+ catch (err) {
214
+ if (err.code === 'ECONNREFUSED' || err.code === 'ENOTFOUND') {
215
+ console.error(`无法连接到禅道服务器: ${this.config.server.url}`);
216
+ }
217
+ else if (err.code === 'ETIMEDOUT') {
218
+ console.error(`连接超时: ${this.config.server.url}`);
219
+ }
220
+ else {
221
+ console.error('登录错误:', err.message);
222
+ }
223
+ return false;
224
+ }
225
+ }
226
+ async ensureAuth() {
227
+ if (this.config.authMode === 'apikey' && this.getApiKeyParams()) {
228
+ return true;
229
+ }
230
+ if (this.getSessionId()) {
231
+ return true;
232
+ }
233
+ return this.login();
234
+ }
235
+ getAuthHeaders() {
236
+ const headers = {};
237
+ const cookie = this.getCookieHeader();
238
+ if (cookie) {
239
+ headers['Cookie'] = cookie;
240
+ }
241
+ return headers;
242
+ }
243
+ }
244
+ exports.AuthManager = AuthManager;