yidaconnector 2026.6.11
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/LICENSE +21 -0
- package/README.md +383 -0
- package/bin/yida.js +670 -0
- package/lib/app/form-navigation.js +58 -0
- package/lib/app/get-schema.js +538 -0
- package/lib/auth/auth.js +294 -0
- package/lib/auth/cdp-browser-login.js +390 -0
- package/lib/auth/codex-login.js +71 -0
- package/lib/auth/login.js +475 -0
- package/lib/auth/org.js +363 -0
- package/lib/auth/qr-login.js +1563 -0
- package/lib/core/chalk.js +384 -0
- package/lib/core/check-update.js +82 -0
- package/lib/core/cli-error.js +39 -0
- package/lib/core/command-manifest.js +106 -0
- package/lib/core/env-cmd.js +545 -0
- package/lib/core/env-manager.js +601 -0
- package/lib/core/env.js +287 -0
- package/lib/core/i18n.js +177 -0
- package/lib/core/locales/ar.js +805 -0
- package/lib/core/locales/de.js +805 -0
- package/lib/core/locales/en.js +1623 -0
- package/lib/core/locales/es.js +805 -0
- package/lib/core/locales/fr.js +805 -0
- package/lib/core/locales/hi.js +805 -0
- package/lib/core/locales/ja.js +1197 -0
- package/lib/core/locales/ko.js +807 -0
- package/lib/core/locales/pt.js +805 -0
- package/lib/core/locales/vi.js +805 -0
- package/lib/core/locales/zh-HK.js +1233 -0
- package/lib/core/locales/zh.js +1584 -0
- package/lib/core/query-data.js +781 -0
- package/lib/core/redact.js +100 -0
- package/lib/core/utils.js +799 -0
- package/lib/core/yida-client.js +117 -0
- package/package.json +94 -0
- package/project/config.json +4 -0
- package/project/pages/src/demo-birthday-game.oyd.jsx +832 -0
- package/project/pages/src/demo-chip-insight.oyd.jsx +983 -0
- package/project/pages/src/demo-compat-smoke.oyd.jsx +58 -0
- package/project/pages/src/demo-crm-batch-entry.oyd.jsx +805 -0
- package/project/pages/src/demo-crm-dashboard.oyd.jsx +677 -0
- package/project/pages/src/demo-future-vision-2026.oyd.jsx +1102 -0
- package/project/pages/src/demo-ppt.oyd.jsx +1192 -0
- package/project/pages/src/demo-salary-calculator.oyd.jsx +904 -0
- package/project/pages/src/yidaconnector-knowledge-doc.oyd.jsx +1714 -0
- package/project/prd/demo-birthday-game.md +39 -0
- package/project/prd/demo-crm.md +463 -0
- package/project/prd/demo-dingtalk-ai-solution-center.md +425 -0
- package/project/prd/demo-future-vision-2026.md +78 -0
- package/project/prd/demo-salary-calculator.md +101 -0
- package/scripts/build-skills-package.js +406 -0
- package/scripts/check-syntax.js +59 -0
- package/scripts/demo-dws.sh +106 -0
- package/scripts/e2e-real/cleanup.js +67 -0
- package/scripts/e2e-real/fixtures/form-fields.json +18 -0
- package/scripts/e2e-real/full-runner.js +1566 -0
- package/scripts/e2e-real/runner.js +293 -0
- package/scripts/e2e-real/skill-coverage.js +115 -0
- package/scripts/generate-command-docs.js +109 -0
- package/scripts/nightly-smoke.js +134 -0
- package/scripts/postinstall.js +545 -0
- package/scripts/solution-center-runner.js +368 -0
- package/scripts/validate-ci.sh +50 -0
- package/scripts/validate-command-manifest.js +119 -0
- package/scripts/validate-package-size.js +78 -0
- package/scripts/validate-skills.js +247 -0
- package/scripts/validate-structure.js +66 -0
- package/yida-skills/SKILL.md +163 -0
- package/yida-skills/references/yida-api.md +1309 -0
- package/yida-skills/skills/large-file-write/SKILL.md +91 -0
- package/yida-skills/skills/large-file-write/references/write-patterns.md +149 -0
- package/yida-skills/skills/large-file-write/scripts/write.js +157 -0
- package/yida-skills/skills/yida-data-management/SKILL.md +252 -0
- package/yida-skills/skills/yida-data-management/references/api-matrix.md +49 -0
- package/yida-skills/skills/yida-data-management/references/data-format-guide.md +159 -0
- package/yida-skills/skills/yida-data-management/references/verified-endpoints.md +62 -0
- package/yida-skills/skills/yida-login/SKILL.md +159 -0
- package/yida-skills/skills/yida-logout/SKILL.md +67 -0
|
@@ -0,0 +1,799 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* utils.js - 宜搭 CLI 公共工具函数
|
|
3
|
+
*
|
|
4
|
+
* 导出函数:
|
|
5
|
+
* findProjectRoot() - 查找项目根目录(兼容悟空环境)
|
|
6
|
+
* extractInfoFromCookies() - 从 Cookie 列表中提取 csrf_token / corp_id / user_id
|
|
7
|
+
* loadCookieData() - 读取 .cache/cookies.json 登录态缓存
|
|
8
|
+
* triggerLogin() - 触发登录
|
|
9
|
+
* refreshCsrfToken() - 刷新 csrf_token
|
|
10
|
+
* resolveBaseUrl() - 从 cookieData 中解析 base_url
|
|
11
|
+
* isLoginExpired() - 检测响应体是否表示登录过期
|
|
12
|
+
* isCsrfTokenExpired() - 检测响应体是否表示 csrf_token 过期
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
'use strict';
|
|
16
|
+
|
|
17
|
+
const fs = require('fs');
|
|
18
|
+
const path = require('path');
|
|
19
|
+
const os = require('os');
|
|
20
|
+
const { t } = require('./i18n');
|
|
21
|
+
const { warn } = require('./chalk');
|
|
22
|
+
|
|
23
|
+
// ── 项目根目录查找 ────────────────────────────────────
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* 检测当前活跃的 AI 工具。
|
|
27
|
+
* 优先级:环境变量 > 兜底检测
|
|
28
|
+
*
|
|
29
|
+
* 注意:只返回当前"活跃"的工具,不返回已安装但未使用的工具。
|
|
30
|
+
*
|
|
31
|
+
* @returns {{ tool: string, displayName: string, dirName: string, workspaceRoot: string }|null}
|
|
32
|
+
*/
|
|
33
|
+
function detectActiveTool() {
|
|
34
|
+
const env = process.env;
|
|
35
|
+
const cwd = process.cwd();
|
|
36
|
+
const home = os.homedir();
|
|
37
|
+
|
|
38
|
+
// 优先级1:通过环境变量检测
|
|
39
|
+
|
|
40
|
+
// QoderWork (桌面客户端,__CFBundleIdentifier=com.qoder.work 或 QODERCLI_INTEGRATION_MODE=qoder_work)
|
|
41
|
+
// 必须在 Claude Code 之前检测,因为 QoderWork 内部设置了 CLAUDE_CODE_ENTRYPOINT 会干扰后续判断
|
|
42
|
+
if (
|
|
43
|
+
env.QODERCLI_INTEGRATION_MODE === 'qoder_work' ||
|
|
44
|
+
(env.__CFBundleIdentifier || '').toLowerCase().includes('qoder')
|
|
45
|
+
) {
|
|
46
|
+
return {
|
|
47
|
+
tool: 'qoderwork',
|
|
48
|
+
displayName: 'QoderWork',
|
|
49
|
+
dirName: '.qoderwork',
|
|
50
|
+
workspaceRoot: path.join(cwd, 'project'),
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Qoder IDE / Qoder Agent(CLI 集成模式)
|
|
55
|
+
if (env.QODER_IDE || env.QODER_AGENT) {
|
|
56
|
+
return {
|
|
57
|
+
tool: 'qoder',
|
|
58
|
+
displayName: 'Qoder',
|
|
59
|
+
dirName: '.qoder',
|
|
60
|
+
workspaceRoot: path.join(cwd, 'project'),
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// 悟空(Wukong)
|
|
65
|
+
// Windows 路径可能使用反斜杠,需同时兼容正斜杠和反斜杠。
|
|
66
|
+
// AGENT_WORK_ROOT 是悟空最明确的运行时信号,优先级高于可能继承到的
|
|
67
|
+
// 外层 IDE/agent 环境变量。
|
|
68
|
+
if (env.AGENT_WORK_ROOT && (env.AGENT_WORK_ROOT.includes('.real') || env.AGENT_WORK_ROOT.includes(path.join('.real')))) {
|
|
69
|
+
return {
|
|
70
|
+
tool: 'wukong',
|
|
71
|
+
displayName: '悟空(Wukong)',
|
|
72
|
+
dirName: '.real',
|
|
73
|
+
workspaceRoot: resolveWukongWorkspaceRoot(env.AGENT_WORK_ROOT),
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// OpenAI Codex
|
|
78
|
+
if (
|
|
79
|
+
env.CODEX_SHELL ||
|
|
80
|
+
env.CODEX_CI ||
|
|
81
|
+
env.CODEX_THREAD_ID ||
|
|
82
|
+
env.CODEX_HOME ||
|
|
83
|
+
(env.__CFBundleIdentifier || '').toLowerCase().includes('codex')
|
|
84
|
+
) {
|
|
85
|
+
return {
|
|
86
|
+
tool: 'codex',
|
|
87
|
+
displayName: 'Codex',
|
|
88
|
+
dirName: '.codex',
|
|
89
|
+
workspaceRoot: path.join(cwd, 'project'),
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Claude Code
|
|
94
|
+
if (env.CLAUDE_CODE_ENTRYPOINT || env.CLAUDE_CODE) {
|
|
95
|
+
return {
|
|
96
|
+
tool: 'claude-code',
|
|
97
|
+
displayName: 'Claude Code',
|
|
98
|
+
dirName: '.claude',
|
|
99
|
+
workspaceRoot: path.join(cwd, 'project'),
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// OpenCode
|
|
104
|
+
// Windows 上配置目录为 ~/.config/opencode,macOS/Linux 为 ~/.opencode。
|
|
105
|
+
// OpenCode 当前运行时会暴露 OPENCODE_CLIENT;保留 OPENCODE 兼容旧检测。
|
|
106
|
+
if (env.OPENCODE || env.OPENCODE_CLIENT) {
|
|
107
|
+
const opencodeDirName = process.platform === 'win32'
|
|
108
|
+
? path.join('.config', 'opencode')
|
|
109
|
+
: '.opencode';
|
|
110
|
+
return {
|
|
111
|
+
tool: 'opencode',
|
|
112
|
+
displayName: 'OpenCode',
|
|
113
|
+
dirName: opencodeDirName,
|
|
114
|
+
workspaceRoot: path.join(cwd, 'project'),
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Cursor
|
|
119
|
+
if (env.CURSOR_TRACE_ID || (env.VSCODE_GIT_ASKPASS_NODE || '').includes('Cursor')) {
|
|
120
|
+
return {
|
|
121
|
+
tool: 'cursor',
|
|
122
|
+
displayName: 'Cursor',
|
|
123
|
+
dirName: '.cursor',
|
|
124
|
+
workspaceRoot: path.join(cwd, 'project'),
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// 优先级2:兜底检测
|
|
129
|
+
|
|
130
|
+
// Aone Copilot - 通过专属配置目录检测(VSCode 环境)
|
|
131
|
+
// Aone Copilot 没有独立的环境变量,但会在 home 目录创建 ~/.aone_copilot/
|
|
132
|
+
if (env.TERM_PROGRAM === 'vscode' && fs.existsSync(path.join(home, '.aone_copilot'))) {
|
|
133
|
+
return {
|
|
134
|
+
tool: 'aone-copilot',
|
|
135
|
+
displayName: 'Aone Copilot',
|
|
136
|
+
dirName: '.aone_copilot',
|
|
137
|
+
workspaceRoot: path.join(cwd, 'project'),
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// 未检测到活跃工具
|
|
142
|
+
return null;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function hasDesktopEnvironment(env = process.env, platform = process.platform) {
|
|
146
|
+
if (env.YIDACONNECTOR_FORCE_TERMINAL_QR === '1') {
|
|
147
|
+
return false;
|
|
148
|
+
}
|
|
149
|
+
if (env.YIDACONNECTOR_ASSUME_DESKTOP === '1') {
|
|
150
|
+
return true;
|
|
151
|
+
}
|
|
152
|
+
if (env.CI || env.CODEX_CI) {
|
|
153
|
+
return false;
|
|
154
|
+
}
|
|
155
|
+
if (platform === 'darwin' || platform === 'win32') {
|
|
156
|
+
return true;
|
|
157
|
+
}
|
|
158
|
+
if (platform === 'linux') {
|
|
159
|
+
return !!(
|
|
160
|
+
env.DISPLAY ||
|
|
161
|
+
env.WAYLAND_DISPLAY ||
|
|
162
|
+
env.MIR_SOCKET ||
|
|
163
|
+
['x11', 'wayland'].includes(String(env.XDG_SESSION_TYPE || '').toLowerCase())
|
|
164
|
+
);
|
|
165
|
+
}
|
|
166
|
+
return false;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* 解析悟空工作区根目录。
|
|
171
|
+
*
|
|
172
|
+
* 悟空的 AGENT_WORK_ROOT 历史上有两种形态:
|
|
173
|
+
* - ~/.real/users/{uuid}/workspace/ 直接就是工作区
|
|
174
|
+
* - ~/.real/users/{uuid}/ workspace 在其下
|
|
175
|
+
*
|
|
176
|
+
* yidaconnector copy 在空工作区会把 project/ 内容直接铺入工作区,因此这里优先
|
|
177
|
+
* 识别已经含 config.json 的目录,最后回退到 AGENT_WORK_ROOT 本身。
|
|
178
|
+
*
|
|
179
|
+
* @param {string} agentWorkRoot
|
|
180
|
+
* @returns {string}
|
|
181
|
+
*/
|
|
182
|
+
function resolveWukongWorkspaceRoot(agentWorkRoot) {
|
|
183
|
+
if (!agentWorkRoot) {
|
|
184
|
+
return path.join(os.homedir(), '.real', 'workspace');
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const candidates = [
|
|
188
|
+
agentWorkRoot,
|
|
189
|
+
path.join(agentWorkRoot, 'project'),
|
|
190
|
+
path.join(agentWorkRoot, 'workspace'),
|
|
191
|
+
path.join(agentWorkRoot, 'workspace', 'project'),
|
|
192
|
+
];
|
|
193
|
+
|
|
194
|
+
for (const candidate of candidates) {
|
|
195
|
+
if (fs.existsSync(path.join(candidate, 'config.json'))) {
|
|
196
|
+
return candidate;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
return agentWorkRoot;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* 获取悟空环境的 node bin 目录路径
|
|
205
|
+
* @returns {string|null} 悟空 node bin 目录路径,非悟空环境返回 null
|
|
206
|
+
*/
|
|
207
|
+
function getWukongNodeBinDir() {
|
|
208
|
+
const activeTool = detectActiveTool();
|
|
209
|
+
if (activeTool && activeTool.tool === 'wukong') {
|
|
210
|
+
const wukongBin = path.join(os.homedir(), '.real', '.bin', 'node', 'bin');
|
|
211
|
+
if (fs.existsSync(wukongBin)) {
|
|
212
|
+
return wukongBin;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
return null;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* 获取当前环境应使用的 npm 可执行文件路径
|
|
220
|
+
* 悟空环境优先使用悟空自带的 npm,避免权限问题
|
|
221
|
+
* @returns {string} npm 可执行文件路径或命令名
|
|
222
|
+
*/
|
|
223
|
+
function getNpmExecutable() {
|
|
224
|
+
const wukongBin = getWukongNodeBinDir();
|
|
225
|
+
if (wukongBin) {
|
|
226
|
+
const npmName = process.platform === 'win32' ? 'npm.cmd' : 'npm';
|
|
227
|
+
const npmPath = path.join(wukongBin, npmName);
|
|
228
|
+
if (fs.existsSync(npmPath)) {
|
|
229
|
+
return npmPath;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
return 'npm';
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* 获取当前环境应使用的 node 可执行文件路径
|
|
237
|
+
* 悟空环境优先使用悟空自带的 node,避免权限问题
|
|
238
|
+
* @returns {string} node 可执行文件路径或命令名
|
|
239
|
+
*/
|
|
240
|
+
function getNodeExecutable() {
|
|
241
|
+
const wukongBin = getWukongNodeBinDir();
|
|
242
|
+
if (wukongBin) {
|
|
243
|
+
const nodeName = process.platform === 'win32' ? 'node.exe' : 'node';
|
|
244
|
+
const nodePath = path.join(wukongBin, nodeName);
|
|
245
|
+
if (fs.existsSync(nodePath)) {
|
|
246
|
+
return nodePath;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
return 'node';
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* 查找项目根目录(project 工作区)。
|
|
254
|
+
*
|
|
255
|
+
* 查找策略:
|
|
256
|
+
* 1. 通过环境变量检测当前活跃的 AI 工具
|
|
257
|
+
* 2. 返回对应工具的项目根目录
|
|
258
|
+
* 3. 兜底:返回 process.cwd()
|
|
259
|
+
*
|
|
260
|
+
* @returns {string} 项目根目录的绝对路径
|
|
261
|
+
*/
|
|
262
|
+
function findProjectRoot() {
|
|
263
|
+
const activeTool = detectActiveTool();
|
|
264
|
+
|
|
265
|
+
if (activeTool) {
|
|
266
|
+
// 如果 project 目录存在,返回它;否则返回当前工作目录
|
|
267
|
+
if (fs.existsSync(activeTool.workspaceRoot)) {
|
|
268
|
+
return activeTool.workspaceRoot;
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
const cwd = process.cwd();
|
|
273
|
+
if (fs.existsSync(path.join(cwd, 'config.json'))) {
|
|
274
|
+
return cwd;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
const nestedProjectRoot = path.join(cwd, 'project');
|
|
278
|
+
if (fs.existsSync(path.join(nestedProjectRoot, 'config.json'))) {
|
|
279
|
+
return nestedProjectRoot;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// 兜底:返回当前工作目录
|
|
283
|
+
return cwd;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// ── Cookie 解析 ───────────────────────────────────────
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* 从 Cookie 列表中提取 csrf_token、corp_id、user_id。
|
|
290
|
+
*
|
|
291
|
+
* 国内宜搭(aliwork.com):corpId/userId 合并写在 `tianshu_corp_user` 里,
|
|
292
|
+
* 形如 `${corpId}_${userId}`,按最后一个下划线切分。
|
|
293
|
+
*
|
|
294
|
+
* 海外 YiDA(yidaapps.com):不写 `tianshu_corp_user`,而是单独写 `corp_id` cookie
|
|
295
|
+
* 存放 corpId 明文;userId 加密在 `pub_uid` 里客户端无法解密,留 null 接受。
|
|
296
|
+
*
|
|
297
|
+
* @param {Array} cookies
|
|
298
|
+
* @returns {{ csrfToken: string|null, corpId: string|null, userId: string|null }}
|
|
299
|
+
*/
|
|
300
|
+
function extractInfoFromCookies(cookies) {
|
|
301
|
+
let csrfToken = null;
|
|
302
|
+
let corpId = null;
|
|
303
|
+
let userId = null;
|
|
304
|
+
|
|
305
|
+
for (const cookie of cookies) {
|
|
306
|
+
if (cookie.name === 'tianshu_csrf_token') {
|
|
307
|
+
csrfToken = cookie.value;
|
|
308
|
+
} else if (cookie.name === 'tianshu_corp_user') {
|
|
309
|
+
const lastUnderscore = cookie.value.lastIndexOf('_');
|
|
310
|
+
if (lastUnderscore > 0) {
|
|
311
|
+
corpId = cookie.value.slice(0, lastUnderscore);
|
|
312
|
+
userId = cookie.value.slice(lastUnderscore + 1);
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
if (!corpId) {
|
|
318
|
+
const corpCookie = cookies.find((c) => c && c.name === 'corp_id' && c.value);
|
|
319
|
+
if (corpCookie) {
|
|
320
|
+
corpId = corpCookie.value;
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
return { csrfToken, corpId, userId };
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// ── 登录态缓存读取 ────────────────────────────────────
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* 读取登录态缓存。
|
|
331
|
+
* 优先读取当前激活环境的 Cookie 文件(环境隔离),
|
|
332
|
+
* 若不存在则兼容旧版 cookies.json(向后兼容)。
|
|
333
|
+
* @param {string} [projectRoot]
|
|
334
|
+
* @param {string} [defaultBaseUrl]
|
|
335
|
+
* @returns {object|null}
|
|
336
|
+
*/
|
|
337
|
+
function loadCookieData(projectRoot, defaultBaseUrl) {
|
|
338
|
+
const root = projectRoot || findProjectRoot();
|
|
339
|
+
const fallbackBaseUrl = defaultBaseUrl || 'https://www.aliwork.com';
|
|
340
|
+
|
|
341
|
+
// 尝试迁移旧版 cookies.json(仅在首次使用多环境功能时执行一次)
|
|
342
|
+
const { migrateOldCookieFile, getCookieFilePath } = require('./env-manager');
|
|
343
|
+
migrateOldCookieFile(root);
|
|
344
|
+
|
|
345
|
+
// 优先使用当前环境的 Cookie 文件
|
|
346
|
+
const envCookieFile = getCookieFilePath(root);
|
|
347
|
+
// 兜底:旧版 cookies.json(向后兼容)
|
|
348
|
+
const legacyCookieFile = path.join(root, '.cache', 'cookies.json');
|
|
349
|
+
|
|
350
|
+
const cookieFile = fs.existsSync(envCookieFile)
|
|
351
|
+
? envCookieFile
|
|
352
|
+
: legacyCookieFile;
|
|
353
|
+
|
|
354
|
+
if (!fs.existsSync(cookieFile)) {return null;}
|
|
355
|
+
|
|
356
|
+
try {
|
|
357
|
+
const raw = fs.readFileSync(cookieFile, 'utf-8').trim();
|
|
358
|
+
if (!raw) {return null;}
|
|
359
|
+
|
|
360
|
+
const parsed = JSON.parse(raw);
|
|
361
|
+
let cookieData;
|
|
362
|
+
|
|
363
|
+
if (Array.isArray(parsed)) {
|
|
364
|
+
cookieData = { cookies: parsed, base_url: fallbackBaseUrl };
|
|
365
|
+
} else {
|
|
366
|
+
cookieData = parsed;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
if (cookieData.cookies && cookieData.cookies.length > 0) {
|
|
370
|
+
const { csrfToken, corpId, userId } = extractInfoFromCookies(cookieData.cookies);
|
|
371
|
+
if (csrfToken) {cookieData.csrf_token = csrfToken;}
|
|
372
|
+
if (corpId) {cookieData.corp_id = corpId;}
|
|
373
|
+
if (userId) {cookieData.user_id = userId;}
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
return cookieData;
|
|
377
|
+
} catch {
|
|
378
|
+
return null;
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
// ── 登录触发 ──────────────────────────────────────────
|
|
383
|
+
|
|
384
|
+
/**
|
|
385
|
+
* 触发登录(浏览器扫码模式)。
|
|
386
|
+
* @param {object} [options]
|
|
387
|
+
* @param {boolean} [options.force=false] - 是否跳过本地缓存,强制重新登录
|
|
388
|
+
* @returns {object} loginResult
|
|
389
|
+
*/
|
|
390
|
+
function triggerLogin(options = {}) {
|
|
391
|
+
warn(t('login.trigger_login'));
|
|
392
|
+
const { ensureLogin } = require('../auth/login');
|
|
393
|
+
return ensureLogin(options);
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
/**
|
|
397
|
+
* 刷新 csrf_token(从本地缓存重新提取,无需重新扫码)。
|
|
398
|
+
* @returns {object} loginResult
|
|
399
|
+
*/
|
|
400
|
+
function refreshCsrfToken() {
|
|
401
|
+
warn(t('login.csrf_refresh'));
|
|
402
|
+
const { refreshCsrfFromCache } = require('../auth/login');
|
|
403
|
+
return refreshCsrfFromCache();
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
// ── 响应检测 ──────────────────────────────────────────
|
|
407
|
+
|
|
408
|
+
/**
|
|
409
|
+
* 检测响应体是否表示登录过期。
|
|
410
|
+
* @param {object} responseJson
|
|
411
|
+
* @returns {boolean}
|
|
412
|
+
*/
|
|
413
|
+
function isLoginExpired(responseJson) {
|
|
414
|
+
return (
|
|
415
|
+
responseJson &&
|
|
416
|
+
responseJson.success === false &&
|
|
417
|
+
(responseJson.errorCode === '307' || responseJson.errorCode === '302')
|
|
418
|
+
);
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
/**
|
|
422
|
+
* 检测响应体是否表示 csrf_token 过期。
|
|
423
|
+
* @param {object} responseJson
|
|
424
|
+
* @returns {boolean}
|
|
425
|
+
*/
|
|
426
|
+
function isCsrfTokenExpired(responseJson) {
|
|
427
|
+
return (
|
|
428
|
+
responseJson &&
|
|
429
|
+
responseJson.success === false &&
|
|
430
|
+
responseJson.errorCode === 'TIANSHU_000030'
|
|
431
|
+
);
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
function isHttpRedirectStatus(statusCode) {
|
|
435
|
+
return [301, 302, 303, 307, 308].includes(Number(statusCode));
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
// ── base_url 解析 ─────────────────────────────────────
|
|
439
|
+
|
|
440
|
+
/**
|
|
441
|
+
* 从 cookieData 中解析 base_url,支持多环境配置优先级。
|
|
442
|
+
*
|
|
443
|
+
* 优先级(高 → 低):
|
|
444
|
+
* 1. YIDACONNECTOR_ENDPOINT 环境变量
|
|
445
|
+
* 2. cookieData.base_url(登录后实际跳转域名)
|
|
446
|
+
* 3. 当前激活的私有化环境配置
|
|
447
|
+
* 4. 当前激活的环境配置(公有云默认)
|
|
448
|
+
* 5. defaultBaseUrl 参数 / 公有云兜底
|
|
449
|
+
*
|
|
450
|
+
* @param {object} cookieData
|
|
451
|
+
* @param {string} [defaultBaseUrl]
|
|
452
|
+
* @returns {string}
|
|
453
|
+
*/
|
|
454
|
+
function resolveBaseUrl(cookieData, defaultBaseUrl) {
|
|
455
|
+
const { resolveEndpoint } = require('./env-manager');
|
|
456
|
+
const resolved = resolveEndpoint(cookieData, undefined);
|
|
457
|
+
if (defaultBaseUrl && resolved === 'https://www.aliwork.com' && (!cookieData || !cookieData.base_url)) {
|
|
458
|
+
return defaultBaseUrl.replace(/\/+$/, '');
|
|
459
|
+
}
|
|
460
|
+
return resolved;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
// ── HTTP 请求工具 ─────────────────────────────────────
|
|
464
|
+
|
|
465
|
+
function collectResponseText(res, onEnd) {
|
|
466
|
+
const chunks = [];
|
|
467
|
+
res.on('data', (chunk) => {
|
|
468
|
+
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
469
|
+
});
|
|
470
|
+
res.on('end', () => {
|
|
471
|
+
onEnd(Buffer.concat(chunks).toString('utf8'));
|
|
472
|
+
});
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
/**
|
|
476
|
+
* 发送 HTTP POST 请求(application/x-www-form-urlencoded)
|
|
477
|
+
* @param {string} baseUrl
|
|
478
|
+
* @param {string} requestPath
|
|
479
|
+
* @param {string} postData - querystring 格式
|
|
480
|
+
* @param {Array} cookies
|
|
481
|
+
* @returns {Promise<object>}
|
|
482
|
+
*/
|
|
483
|
+
function httpPost(baseUrl, requestPath, postData, cookies, optionsOverride = {}) {
|
|
484
|
+
const https = require('https');
|
|
485
|
+
const http = require('http');
|
|
486
|
+
|
|
487
|
+
return new Promise((resolve, reject) => {
|
|
488
|
+
const parsedUrl = new URL(baseUrl);
|
|
489
|
+
const requestHost = parsedUrl.hostname;
|
|
490
|
+
const filteredCookies = cookies.filter(c => {
|
|
491
|
+
const cookieDomain = (c.domain || '').replace(/^\./, '');
|
|
492
|
+
return requestHost === cookieDomain || requestHost.endsWith('.' + cookieDomain);
|
|
493
|
+
});
|
|
494
|
+
// 若 domain 匹配后为空(如 cookies.json 中 domain 字段缺失),fallback 到全量 cookies
|
|
495
|
+
const effectiveCookies = filteredCookies.length > 0 ? filteredCookies : cookies;
|
|
496
|
+
const cookieHeader = effectiveCookies.map((c) => `${c.name}=${c.value}`).join('; ');
|
|
497
|
+
const isHttps = parsedUrl.protocol === 'https:';
|
|
498
|
+
const requestModule = isHttps ? https : http;
|
|
499
|
+
|
|
500
|
+
// 从 cookies 中提取 csrf_token 用于 global_csrf_token 请求头
|
|
501
|
+
const csrfCookie = effectiveCookies.find(c => c.name === 'tianshu_csrf_token');
|
|
502
|
+
const globalCsrfToken = csrfCookie ? csrfCookie.value : '';
|
|
503
|
+
|
|
504
|
+
const options = {
|
|
505
|
+
hostname: parsedUrl.hostname,
|
|
506
|
+
port: parsedUrl.port || (isHttps ? 443 : 80),
|
|
507
|
+
path: requestPath,
|
|
508
|
+
method: 'POST',
|
|
509
|
+
headers: {
|
|
510
|
+
'Content-Type': 'application/x-www-form-urlencoded',
|
|
511
|
+
'Content-Length': Buffer.byteLength(postData),
|
|
512
|
+
Accept: 'application/json, text/plain, */*',
|
|
513
|
+
Origin: baseUrl,
|
|
514
|
+
Referer: optionsOverride.referer || baseUrl + '/',
|
|
515
|
+
Cookie: cookieHeader,
|
|
516
|
+
'x-requested-with': 'XMLHttpRequest',
|
|
517
|
+
global_csrf_token: globalCsrfToken,
|
|
518
|
+
},
|
|
519
|
+
timeout: 30000,
|
|
520
|
+
};
|
|
521
|
+
|
|
522
|
+
const req = requestModule.request(options, (res) => {
|
|
523
|
+
collectResponseText(res, (data) => {
|
|
524
|
+
if (!optionsOverride.silentStatus) {
|
|
525
|
+
warn(t('common.http_status', res.statusCode));
|
|
526
|
+
}
|
|
527
|
+
if (isHttpRedirectStatus(res.statusCode)) {
|
|
528
|
+
resolve({
|
|
529
|
+
__needLogin: true,
|
|
530
|
+
__httpStatus: res.statusCode,
|
|
531
|
+
__location: res.headers.location || '',
|
|
532
|
+
});
|
|
533
|
+
return;
|
|
534
|
+
}
|
|
535
|
+
try {
|
|
536
|
+
const parsed = JSON.parse(data);
|
|
537
|
+
if (isLoginExpired(parsed)) {
|
|
538
|
+
resolve({ __needLogin: true });
|
|
539
|
+
return;
|
|
540
|
+
}
|
|
541
|
+
if (isCsrfTokenExpired(parsed)) {
|
|
542
|
+
resolve({ __csrfExpired: true });
|
|
543
|
+
return;
|
|
544
|
+
}
|
|
545
|
+
resolve(parsed);
|
|
546
|
+
} catch {
|
|
547
|
+
warn(t('common.http_response', data.substring(0, 500)));
|
|
548
|
+
resolve({ success: false, errorMsg: `HTTP ${res.statusCode}: ` + t('common.response_not_json') });
|
|
549
|
+
}
|
|
550
|
+
});
|
|
551
|
+
});
|
|
552
|
+
|
|
553
|
+
// 用标志位防止 timeout 后 req.destroy() 触发 error 事件导致双重 reject
|
|
554
|
+
let hasRejected = false;
|
|
555
|
+
req.on('timeout', () => {
|
|
556
|
+
hasRejected = true;
|
|
557
|
+
req.destroy();
|
|
558
|
+
reject(new Error(t('common.request_timeout')));
|
|
559
|
+
});
|
|
560
|
+
req.on('error', (err) => { if (!hasRejected) { reject(err); } });
|
|
561
|
+
req.write(postData);
|
|
562
|
+
req.end();
|
|
563
|
+
});
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
function httpPostJson(baseUrl, requestPath, payload, cookies, optionsOverride = {}) {
|
|
567
|
+
const https = require('https');
|
|
568
|
+
const http = require('http');
|
|
569
|
+
|
|
570
|
+
return new Promise((resolve, reject) => {
|
|
571
|
+
const parsedUrl = new URL(baseUrl);
|
|
572
|
+
const requestHost = parsedUrl.hostname;
|
|
573
|
+
const filteredCookies = cookies.filter(c => {
|
|
574
|
+
const cookieDomain = (c.domain || '').replace(/^\./, '');
|
|
575
|
+
return requestHost === cookieDomain || requestHost.endsWith('.' + cookieDomain);
|
|
576
|
+
});
|
|
577
|
+
const effectiveCookies = filteredCookies.length > 0 ? filteredCookies : cookies;
|
|
578
|
+
const cookieHeader = effectiveCookies.map((c) => `${c.name}=${c.value}`).join('; ');
|
|
579
|
+
const isHttps = parsedUrl.protocol === 'https:';
|
|
580
|
+
const requestModule = isHttps ? https : http;
|
|
581
|
+
const body = JSON.stringify(payload === undefined ? {} : payload);
|
|
582
|
+
|
|
583
|
+
const csrfCookie = effectiveCookies.find(c => c.name === 'tianshu_csrf_token');
|
|
584
|
+
const globalCsrfToken = optionsOverride.csrfToken || (csrfCookie ? csrfCookie.value : '');
|
|
585
|
+
|
|
586
|
+
const options = {
|
|
587
|
+
hostname: parsedUrl.hostname,
|
|
588
|
+
port: parsedUrl.port || (isHttps ? 443 : 80),
|
|
589
|
+
path: requestPath,
|
|
590
|
+
method: 'POST',
|
|
591
|
+
headers: {
|
|
592
|
+
'Content-Type': 'application/json',
|
|
593
|
+
'Content-Length': Buffer.byteLength(body),
|
|
594
|
+
Accept: 'application/json, text/plain, */*',
|
|
595
|
+
Origin: baseUrl,
|
|
596
|
+
Referer: optionsOverride.referer || baseUrl + '/',
|
|
597
|
+
Cookie: cookieHeader,
|
|
598
|
+
'x-requested-with': 'XMLHttpRequest',
|
|
599
|
+
global_csrf_token: globalCsrfToken,
|
|
600
|
+
},
|
|
601
|
+
timeout: optionsOverride.timeout || 30000,
|
|
602
|
+
};
|
|
603
|
+
|
|
604
|
+
const req = requestModule.request(options, (res) => {
|
|
605
|
+
collectResponseText(res, (data) => {
|
|
606
|
+
if (!optionsOverride.silentStatus) {
|
|
607
|
+
warn(t('common.http_status', res.statusCode));
|
|
608
|
+
}
|
|
609
|
+
if (isHttpRedirectStatus(res.statusCode)) {
|
|
610
|
+
resolve({
|
|
611
|
+
__needLogin: true,
|
|
612
|
+
__httpStatus: res.statusCode,
|
|
613
|
+
__location: res.headers.location || '',
|
|
614
|
+
});
|
|
615
|
+
return;
|
|
616
|
+
}
|
|
617
|
+
try {
|
|
618
|
+
const parsed = JSON.parse(data);
|
|
619
|
+
if (isLoginExpired(parsed)) {
|
|
620
|
+
resolve({ __needLogin: true });
|
|
621
|
+
return;
|
|
622
|
+
}
|
|
623
|
+
if (isCsrfTokenExpired(parsed)) {
|
|
624
|
+
resolve({ __csrfExpired: true });
|
|
625
|
+
return;
|
|
626
|
+
}
|
|
627
|
+
resolve(parsed);
|
|
628
|
+
} catch {
|
|
629
|
+
warn(t('common.http_response', data.substring(0, 500)));
|
|
630
|
+
resolve({ success: false, errorMsg: `HTTP ${res.statusCode}: ` + t('common.response_not_json') });
|
|
631
|
+
}
|
|
632
|
+
});
|
|
633
|
+
});
|
|
634
|
+
|
|
635
|
+
let hasRejected = false;
|
|
636
|
+
req.on('timeout', () => {
|
|
637
|
+
hasRejected = true;
|
|
638
|
+
req.destroy();
|
|
639
|
+
reject(new Error(t('common.request_timeout')));
|
|
640
|
+
});
|
|
641
|
+
req.on('error', (err) => { if (!hasRejected) { reject(err); } });
|
|
642
|
+
req.write(body);
|
|
643
|
+
req.end();
|
|
644
|
+
});
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
/**
|
|
648
|
+
* 发送 HTTP GET 请求
|
|
649
|
+
* @param {string} baseUrl
|
|
650
|
+
* @param {string} requestPath
|
|
651
|
+
* @param {object} queryParams
|
|
652
|
+
* @param {Array} cookies
|
|
653
|
+
* @returns {Promise<object>}
|
|
654
|
+
*/
|
|
655
|
+
function httpGet(baseUrl, requestPath, queryParams, cookies, optionsOverride = {}) {
|
|
656
|
+
const https = require('https');
|
|
657
|
+
const http = require('http');
|
|
658
|
+
const querystring = require('querystring');
|
|
659
|
+
|
|
660
|
+
return new Promise((resolve, reject) => {
|
|
661
|
+
const parsedUrl = new URL(baseUrl);
|
|
662
|
+
const requestHost = parsedUrl.hostname;
|
|
663
|
+
const filteredCookies = cookies.filter(c => {
|
|
664
|
+
const cookieDomain = (c.domain || '').replace(/^\./, '');
|
|
665
|
+
return requestHost === cookieDomain || requestHost.endsWith('.' + cookieDomain);
|
|
666
|
+
});
|
|
667
|
+
// 若 domain 匹配后为空(如 cookies.json 中 domain 字段缺失),fallback 到全量 cookies
|
|
668
|
+
const effectiveCookies = filteredCookies.length > 0 ? filteredCookies : cookies;
|
|
669
|
+
const cookieHeader = effectiveCookies.map((c) => `${c.name}=${c.value}`).join('; ');
|
|
670
|
+
const isHttps = parsedUrl.protocol === 'https:';
|
|
671
|
+
const requestModule = isHttps ? https : http;
|
|
672
|
+
const fullPath = queryParams ? `${requestPath}?${querystring.stringify(queryParams)}` : requestPath;
|
|
673
|
+
|
|
674
|
+
// 从 cookies 中提取 csrf_token 用于 global_csrf_token 请求头
|
|
675
|
+
const csrfCookie = effectiveCookies.find(c => c.name === 'tianshu_csrf_token');
|
|
676
|
+
const globalCsrfToken = csrfCookie ? csrfCookie.value : '';
|
|
677
|
+
|
|
678
|
+
const options = {
|
|
679
|
+
hostname: parsedUrl.hostname,
|
|
680
|
+
port: parsedUrl.port || (isHttps ? 443 : 80),
|
|
681
|
+
path: fullPath,
|
|
682
|
+
method: 'GET',
|
|
683
|
+
headers: {
|
|
684
|
+
Accept: 'application/json, text/plain, */*',
|
|
685
|
+
Origin: baseUrl,
|
|
686
|
+
Referer: baseUrl + '/',
|
|
687
|
+
Cookie: cookieHeader,
|
|
688
|
+
'x-requested-with': 'XMLHttpRequest',
|
|
689
|
+
global_csrf_token: globalCsrfToken,
|
|
690
|
+
},
|
|
691
|
+
timeout: 30000,
|
|
692
|
+
};
|
|
693
|
+
|
|
694
|
+
const req = requestModule.request(options, (res) => {
|
|
695
|
+
collectResponseText(res, (data) => {
|
|
696
|
+
if (!optionsOverride.silentStatus) {
|
|
697
|
+
warn(t('common.http_status', res.statusCode));
|
|
698
|
+
}
|
|
699
|
+
if (isHttpRedirectStatus(res.statusCode)) {
|
|
700
|
+
resolve({
|
|
701
|
+
__needLogin: true,
|
|
702
|
+
__httpStatus: res.statusCode,
|
|
703
|
+
__location: res.headers.location || '',
|
|
704
|
+
});
|
|
705
|
+
return;
|
|
706
|
+
}
|
|
707
|
+
try {
|
|
708
|
+
const parsed = JSON.parse(data);
|
|
709
|
+
if (isLoginExpired(parsed)) {
|
|
710
|
+
resolve({ __needLogin: true });
|
|
711
|
+
return;
|
|
712
|
+
}
|
|
713
|
+
if (isCsrfTokenExpired(parsed)) {
|
|
714
|
+
resolve({ __csrfExpired: true });
|
|
715
|
+
return;
|
|
716
|
+
}
|
|
717
|
+
resolve(parsed);
|
|
718
|
+
} catch {
|
|
719
|
+
warn(t('common.http_response', data.substring(0, 500)));
|
|
720
|
+
resolve({ success: false, errorMsg: `HTTP ${res.statusCode}: ` + t('common.response_not_json') });
|
|
721
|
+
}
|
|
722
|
+
});
|
|
723
|
+
});
|
|
724
|
+
|
|
725
|
+
// 用标志位防止 timeout 后 req.destroy() 触发 error 事件导致双重 reject
|
|
726
|
+
let hasRejected = false;
|
|
727
|
+
req.on('timeout', () => {
|
|
728
|
+
hasRejected = true;
|
|
729
|
+
req.destroy();
|
|
730
|
+
reject(new Error(t('common.request_timeout')));
|
|
731
|
+
});
|
|
732
|
+
req.on('error', (err) => { if (!hasRejected) { reject(err); } });
|
|
733
|
+
req.end();
|
|
734
|
+
});
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
/**
|
|
738
|
+
* 带自动重登录的请求封装。
|
|
739
|
+
* @param {Function} requestFn - 接受 authRef 返回 Promise 的工厂函数
|
|
740
|
+
* @param {object} authRef - { csrfToken, cookies, baseUrl, cookieData }
|
|
741
|
+
* @returns {Promise<object>}
|
|
742
|
+
*/
|
|
743
|
+
async function requestWithAutoLogin(requestFn, authRef) {
|
|
744
|
+
let result = await requestFn(authRef);
|
|
745
|
+
|
|
746
|
+
if (result && result.__csrfExpired) {
|
|
747
|
+
const refreshedData = refreshCsrfToken();
|
|
748
|
+
if (refreshedData && refreshedData.cookies && refreshedData.csrf_token) {
|
|
749
|
+
authRef.cookieData = refreshedData;
|
|
750
|
+
authRef.csrfToken = refreshedData.csrf_token;
|
|
751
|
+
authRef.cookies = refreshedData.cookies;
|
|
752
|
+
authRef.baseUrl = resolveBaseUrl(refreshedData);
|
|
753
|
+
warn(t('common.csrf_refreshed'));
|
|
754
|
+
result = await requestFn(authRef);
|
|
755
|
+
} else {
|
|
756
|
+
result = { __needLogin: true };
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
if (result && result.__needLogin) {
|
|
761
|
+
const newCookieData = triggerLogin({ force: true });
|
|
762
|
+
if (!newCookieData || !newCookieData.cookies || !newCookieData.csrf_token) {
|
|
763
|
+
return {
|
|
764
|
+
success: false,
|
|
765
|
+
__needLogin: true,
|
|
766
|
+
errorMsg: t('common.login_expired', 'yidaconnector login --qr / yidaconnector login --browser'),
|
|
767
|
+
};
|
|
768
|
+
}
|
|
769
|
+
authRef.cookieData = newCookieData;
|
|
770
|
+
authRef.csrfToken = newCookieData.csrf_token;
|
|
771
|
+
authRef.cookies = newCookieData.cookies;
|
|
772
|
+
authRef.baseUrl = resolveBaseUrl(newCookieData);
|
|
773
|
+
warn(t('common.relogin_retry'));
|
|
774
|
+
result = await requestFn(authRef);
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
return result;
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
module.exports = {
|
|
781
|
+
detectActiveTool,
|
|
782
|
+
hasDesktopEnvironment,
|
|
783
|
+
findProjectRoot,
|
|
784
|
+
extractInfoFromCookies,
|
|
785
|
+
loadCookieData,
|
|
786
|
+
triggerLogin,
|
|
787
|
+
refreshCsrfToken,
|
|
788
|
+
resolveBaseUrl,
|
|
789
|
+
isLoginExpired,
|
|
790
|
+
isCsrfTokenExpired,
|
|
791
|
+
httpPost,
|
|
792
|
+
httpPostJson,
|
|
793
|
+
httpGet,
|
|
794
|
+
requestWithAutoLogin,
|
|
795
|
+
getWukongNodeBinDir,
|
|
796
|
+
getNpmExecutable,
|
|
797
|
+
getNodeExecutable,
|
|
798
|
+
resolveWukongWorkspaceRoot,
|
|
799
|
+
};
|