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.
Files changed (79) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +383 -0
  3. package/bin/yida.js +670 -0
  4. package/lib/app/form-navigation.js +58 -0
  5. package/lib/app/get-schema.js +538 -0
  6. package/lib/auth/auth.js +294 -0
  7. package/lib/auth/cdp-browser-login.js +390 -0
  8. package/lib/auth/codex-login.js +71 -0
  9. package/lib/auth/login.js +475 -0
  10. package/lib/auth/org.js +363 -0
  11. package/lib/auth/qr-login.js +1563 -0
  12. package/lib/core/chalk.js +384 -0
  13. package/lib/core/check-update.js +82 -0
  14. package/lib/core/cli-error.js +39 -0
  15. package/lib/core/command-manifest.js +106 -0
  16. package/lib/core/env-cmd.js +545 -0
  17. package/lib/core/env-manager.js +601 -0
  18. package/lib/core/env.js +287 -0
  19. package/lib/core/i18n.js +177 -0
  20. package/lib/core/locales/ar.js +805 -0
  21. package/lib/core/locales/de.js +805 -0
  22. package/lib/core/locales/en.js +1623 -0
  23. package/lib/core/locales/es.js +805 -0
  24. package/lib/core/locales/fr.js +805 -0
  25. package/lib/core/locales/hi.js +805 -0
  26. package/lib/core/locales/ja.js +1197 -0
  27. package/lib/core/locales/ko.js +807 -0
  28. package/lib/core/locales/pt.js +805 -0
  29. package/lib/core/locales/vi.js +805 -0
  30. package/lib/core/locales/zh-HK.js +1233 -0
  31. package/lib/core/locales/zh.js +1584 -0
  32. package/lib/core/query-data.js +781 -0
  33. package/lib/core/redact.js +100 -0
  34. package/lib/core/utils.js +799 -0
  35. package/lib/core/yida-client.js +117 -0
  36. package/package.json +94 -0
  37. package/project/config.json +4 -0
  38. package/project/pages/src/demo-birthday-game.oyd.jsx +832 -0
  39. package/project/pages/src/demo-chip-insight.oyd.jsx +983 -0
  40. package/project/pages/src/demo-compat-smoke.oyd.jsx +58 -0
  41. package/project/pages/src/demo-crm-batch-entry.oyd.jsx +805 -0
  42. package/project/pages/src/demo-crm-dashboard.oyd.jsx +677 -0
  43. package/project/pages/src/demo-future-vision-2026.oyd.jsx +1102 -0
  44. package/project/pages/src/demo-ppt.oyd.jsx +1192 -0
  45. package/project/pages/src/demo-salary-calculator.oyd.jsx +904 -0
  46. package/project/pages/src/yidaconnector-knowledge-doc.oyd.jsx +1714 -0
  47. package/project/prd/demo-birthday-game.md +39 -0
  48. package/project/prd/demo-crm.md +463 -0
  49. package/project/prd/demo-dingtalk-ai-solution-center.md +425 -0
  50. package/project/prd/demo-future-vision-2026.md +78 -0
  51. package/project/prd/demo-salary-calculator.md +101 -0
  52. package/scripts/build-skills-package.js +406 -0
  53. package/scripts/check-syntax.js +59 -0
  54. package/scripts/demo-dws.sh +106 -0
  55. package/scripts/e2e-real/cleanup.js +67 -0
  56. package/scripts/e2e-real/fixtures/form-fields.json +18 -0
  57. package/scripts/e2e-real/full-runner.js +1566 -0
  58. package/scripts/e2e-real/runner.js +293 -0
  59. package/scripts/e2e-real/skill-coverage.js +115 -0
  60. package/scripts/generate-command-docs.js +109 -0
  61. package/scripts/nightly-smoke.js +134 -0
  62. package/scripts/postinstall.js +545 -0
  63. package/scripts/solution-center-runner.js +368 -0
  64. package/scripts/validate-ci.sh +50 -0
  65. package/scripts/validate-command-manifest.js +119 -0
  66. package/scripts/validate-package-size.js +78 -0
  67. package/scripts/validate-skills.js +247 -0
  68. package/scripts/validate-structure.js +66 -0
  69. package/yida-skills/SKILL.md +163 -0
  70. package/yida-skills/references/yida-api.md +1309 -0
  71. package/yida-skills/skills/large-file-write/SKILL.md +91 -0
  72. package/yida-skills/skills/large-file-write/references/write-patterns.md +149 -0
  73. package/yida-skills/skills/large-file-write/scripts/write.js +157 -0
  74. package/yida-skills/skills/yida-data-management/SKILL.md +252 -0
  75. package/yida-skills/skills/yida-data-management/references/api-matrix.md +49 -0
  76. package/yida-skills/skills/yida-data-management/references/data-format-guide.md +159 -0
  77. package/yida-skills/skills/yida-data-management/references/verified-endpoints.md +62 -0
  78. package/yida-skills/skills/yida-login/SKILL.md +159 -0
  79. package/yida-skills/skills/yida-logout/SKILL.md +67 -0
@@ -0,0 +1,294 @@
1
+ /**
2
+ * auth.js - 登录态管理模块
3
+ *
4
+ * 提供统一的登录态管理能力,支持:
5
+ * - 登录态状态查询
6
+ * - 钉钉扫码登录
7
+ * - 登录态刷新
8
+ * - 安全退出
9
+ *
10
+ * 导出函数:
11
+ * authStatus() - 查询当前登录状态
12
+ * authLogin() - 执行登录(扫码/钉钉自动登录)
13
+ * authRefresh() - 刷新登录态
14
+ * authLogout() - 退出登录
15
+ */
16
+
17
+ 'use strict';
18
+
19
+ const fs = require('fs');
20
+ const path = require('path');
21
+ const { findProjectRoot, loadCookieData, extractInfoFromCookies, resolveBaseUrl, detectActiveTool, hasDesktopEnvironment } = require('../core/utils');
22
+ const { logout, checkLoginOnly, interactiveLogin } = require('./login');
23
+ const { t } = require('../core/i18n');
24
+
25
+ const AUTH_CACHE_FILE = '.cache/auth.json';
26
+
27
+ // ── 配置读取 ──────────────────────────────────────────
28
+
29
+ function loadAuthConfig() {
30
+ const projectRoot = findProjectRoot();
31
+ const authConfigPath = path.join(projectRoot, AUTH_CACHE_FILE);
32
+
33
+ if (fs.existsSync(authConfigPath)) {
34
+ try {
35
+ const content = fs.readFileSync(authConfigPath, 'utf-8').trim();
36
+ if (content) {
37
+ return JSON.parse(content);
38
+ }
39
+ } catch {
40
+ // ignore
41
+ }
42
+ }
43
+ return null;
44
+ }
45
+
46
+ function saveAuthConfig(config) {
47
+ const projectRoot = findProjectRoot();
48
+ const cacheDir = path.join(projectRoot, '.cache');
49
+ const authConfigPath = path.join(cacheDir, 'auth.json');
50
+
51
+ fs.mkdirSync(cacheDir, { recursive: true });
52
+ fs.writeFileSync(authConfigPath, JSON.stringify(config, null, 2), 'utf-8');
53
+ }
54
+
55
+ // ── 登录态状态查询 ────────────────────────────────────
56
+
57
+ /**
58
+ * 查询当前登录状态
59
+ * @returns {object} 登录状态信息
60
+ */
61
+ function authStatus() {
62
+ const { banner, warn, success: chalkSuccess, label, sep, hint } = require('../core/chalk');
63
+
64
+ banner(t('auth.status_title'), { stderr: false });
65
+
66
+ // 读取 Cookie 缓存
67
+ const cookieData = loadCookieData();
68
+
69
+ if (!cookieData || !cookieData.cookies || cookieData.cookies.length === 0) {
70
+ warn(t('auth.not_logged_in'), false);
71
+ hint(t('auth.login_hint'), false);
72
+ console.log(` ${sep()}\n`);
73
+ return {
74
+ status: 'not_logged_in',
75
+ canAutoUse: false,
76
+ };
77
+ }
78
+
79
+ const { csrfToken, corpId, userId } = extractInfoFromCookies(cookieData.cookies);
80
+ const baseUrl = resolveBaseUrl(cookieData);
81
+ const authConfig = loadAuthConfig();
82
+
83
+ if (!csrfToken) {
84
+ warn(t('auth.no_csrf_token'), false);
85
+ hint(t('auth.relogin_hint'), false);
86
+ console.log(` ${sep()}\n`);
87
+ return {
88
+ status: 'invalid',
89
+ canAutoUse: false,
90
+ };
91
+ }
92
+
93
+ // 登录态有效
94
+ chalkSuccess(t('auth.logged_in'), false);
95
+ label('Base URL', baseUrl, { stderr: false });
96
+ if (corpId) {
97
+ label('Corp ID', corpId, { stderr: false });
98
+ }
99
+ if (userId) {
100
+ label('User ID', userId, { stderr: false });
101
+ }
102
+ label('CSRF', `${csrfToken.slice(0, 16)}…`, { stderr: false });
103
+
104
+ // 显示登录信息
105
+ if (authConfig) {
106
+ if (authConfig.loginType) {
107
+ label('Login Type', authConfig.loginType, { stderr: false });
108
+ }
109
+ if (authConfig.loginTime) {
110
+ label('Login Time', authConfig.loginTime, { stderr: false });
111
+ }
112
+ }
113
+
114
+ console.log(` ${sep()}\n`);
115
+
116
+ return {
117
+ status: 'ok',
118
+ canAutoUse: true,
119
+ csrfToken,
120
+ corpId,
121
+ userId,
122
+ baseUrl,
123
+ loginType: authConfig?.loginType,
124
+ loginTime: authConfig?.loginTime,
125
+ };
126
+ }
127
+
128
+ // ── 执行登录 ──────────────────────────────────────────
129
+
130
+ /**
131
+ * 执行登录
132
+ * @param {object} options - 登录选项
133
+ * @param {string} options.type - 登录类型:'qrcode' | 'dingtalk' | 'browser' | 'codex' | 'qoder' | 'wukong'
134
+ * @param {string} [options.corpId] - 多组织登录时指定目标组织
135
+ * @returns {Promise<object>|object} 登录结果
136
+ */
137
+ async function authLogin(options = {}) {
138
+ const { type = 'qrcode', corpId, forceTerminalQr = false } = options;
139
+
140
+ const { info, success: chalkSuccess, label } = require('../core/chalk');
141
+ info(t('auth.login_start', type));
142
+
143
+ // 调用现有的登录逻辑
144
+ let result;
145
+ const cachedResult = checkLoginOnly({ includeSecrets: true });
146
+ if (cachedResult.status === 'ok') {
147
+ result = cachedResult;
148
+ } else if (type === 'browser') {
149
+ result = interactiveLogin({ playwrightFallback: true });
150
+ } else if (type === 'codex' || type === 'qoder' || type === 'wukong') {
151
+ result = await require('./codex-login').codexLogin({
152
+ tool: type,
153
+ });
154
+ } else if (type === 'qrcode' && !forceTerminalQr && isAgentConversationEnvironment()) {
155
+ result = await loginInAgentConversation({ corpId });
156
+ } else if (type === 'qrcode' && !forceTerminalQr && hasDesktopEnvironment()) {
157
+ result = interactiveLogin({ playwrightFallback: true }) ||
158
+ await require('./qr-login').qrLogin({ corpId });
159
+ } else {
160
+ result = await require('./qr-login').qrLogin({ corpId });
161
+ }
162
+
163
+ if (!result) {
164
+ return result;
165
+ }
166
+
167
+ if (!result.csrf_token || !result.cookies) {
168
+ return result;
169
+ }
170
+
171
+ // 保存登录信息
172
+ const authConfig = {
173
+ loginType: type,
174
+ loginTime: new Date().toISOString(),
175
+ corpId: result.corp_id,
176
+ userId: result.user_id,
177
+ };
178
+ saveAuthConfig(authConfig);
179
+
180
+ chalkSuccess(t('auth.login_success'));
181
+ if (result.corp_id) {
182
+ label('Corp ID', result.corp_id);
183
+ }
184
+
185
+ return result;
186
+ }
187
+
188
+ function isAgentConversationEnvironment() {
189
+ return !!detectActiveTool() || process.env.YIDACONNECTOR_AGENT_MODE === '1';
190
+ }
191
+
192
+ function shouldUsePlaywrightFallbackInAgentLogin() {
193
+ return hasDesktopEnvironment() || process.env.YIDACONNECTOR_AGENT_PLAYWRIGHT_FALLBACK === '1';
194
+ }
195
+
196
+ async function loginInAgentConversation(options = {}) {
197
+ const activeTool = detectActiveTool();
198
+
199
+ const browserResult = interactiveLogin({
200
+ playwrightFallback: shouldUsePlaywrightFallbackInAgentLogin(),
201
+ });
202
+ if (browserResult) {
203
+ return browserResult;
204
+ }
205
+
206
+ if (activeTool && (activeTool.tool === 'wukong' || activeTool.tool === 'qoderwork')) {
207
+ return require('./codex-login').codexLogin({ tool: activeTool.tool });
208
+ }
209
+
210
+ return require('./qr-login').startCodexQrLogin({ corpId: options.corpId });
211
+ }
212
+
213
+ // ── 刷新登录态 ────────────────────────────────────────
214
+
215
+ /**
216
+ * 刷新登录态(从本地缓存重新提取 csrf_token)
217
+ * @returns {object} 刷新结果
218
+ */
219
+ function authRefresh() {
220
+ const { info: chalkInfo, fail: chalkFail, success: chalkSuccess2, label: chalkLabel } = require('../core/chalk');
221
+ chalkInfo(t('auth.refresh_start'));
222
+
223
+ const cookieData = loadCookieData();
224
+
225
+ if (!cookieData || !cookieData.cookies) {
226
+ chalkFail(t('auth.no_cookie_cache'), { exit: false });
227
+ return {
228
+ status: 'error',
229
+ message: 'No cookie cache',
230
+ };
231
+ }
232
+
233
+ const { csrfToken, corpId, userId } = extractInfoFromCookies(cookieData.cookies);
234
+
235
+ if (!csrfToken) {
236
+ chalkFail(t('auth.no_csrf_in_cache'), { exit: false });
237
+ return {
238
+ status: 'error',
239
+ message: 'No csrf_token in cache',
240
+ };
241
+ }
242
+
243
+ const baseUrl = resolveBaseUrl(cookieData);
244
+
245
+ // 更新登录时间
246
+ const authConfig = loadAuthConfig() || {};
247
+ authConfig.refreshTime = new Date().toISOString();
248
+ authConfig.corpId = corpId;
249
+ authConfig.userId = userId;
250
+ saveAuthConfig(authConfig);
251
+
252
+ chalkSuccess2(t('auth.refresh_success'));
253
+ chalkLabel('CSRF', `${csrfToken.slice(0, 16)}…`);
254
+
255
+ return {
256
+ status: 'ok',
257
+ csrfToken,
258
+ corpId,
259
+ userId,
260
+ baseUrl,
261
+ };
262
+ }
263
+
264
+ // ── 退出登录 ──────────────────────────────────────────
265
+
266
+ /**
267
+ * 退出登录
268
+ */
269
+ function authLogout() {
270
+ // 调用现有的退出逻辑
271
+ logout();
272
+
273
+ // 清空 auth 配置
274
+ const projectRoot = findProjectRoot();
275
+ const authConfigPath = path.join(projectRoot, AUTH_CACHE_FILE);
276
+
277
+ // 确保目录存在
278
+ const authConfigDir = path.dirname(authConfigPath);
279
+ if (!fs.existsSync(authConfigDir)) {
280
+ fs.mkdirSync(authConfigDir, { recursive: true });
281
+ }
282
+ fs.writeFileSync(authConfigPath, '{}', 'utf-8');
283
+ const { success: chalkSuccess3 } = require('../core/chalk');
284
+ chalkSuccess3(t('auth.auth_config_cleared'));
285
+ }
286
+
287
+ module.exports = {
288
+ authStatus,
289
+ authLogin,
290
+ authRefresh,
291
+ authLogout,
292
+ loadAuthConfig,
293
+ saveAuthConfig,
294
+ };
@@ -0,0 +1,390 @@
1
+ /**
2
+ * cdp-browser-login.js - dependency-free local browser login via Chrome DevTools Protocol
3
+ *
4
+ * Opens an isolated Chrome/Edge/Chromium profile, waits for Yida login cookies,
5
+ * prints the cookies as JSON to the parent process, then closes the browser.
6
+ */
7
+
8
+ 'use strict';
9
+
10
+ const fs = require('fs');
11
+ const os = require('os');
12
+ const path = require('path');
13
+ const net = require('net');
14
+ const crypto = require('crypto');
15
+ const { execFileSync, spawn } = require('child_process');
16
+ const { deriveBaseUrlFromCookies, deriveBaseUrlFromLoginState } = require('../core/env-manager');
17
+
18
+ function findBrowserExecutable() {
19
+ if (process.env.YIDACONNECTOR_CHROME_PATH && fs.existsSync(process.env.YIDACONNECTOR_CHROME_PATH)) {
20
+ return process.env.YIDACONNECTOR_CHROME_PATH;
21
+ }
22
+
23
+ const candidates = process.platform === 'darwin'
24
+ ? [
25
+ '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
26
+ '/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge',
27
+ '/Applications/Chromium.app/Contents/MacOS/Chromium',
28
+ ]
29
+ : process.platform === 'win32'
30
+ ? [
31
+ path.join(process.env.PROGRAMFILES || 'C:\\Program Files', 'Google', 'Chrome', 'Application', 'chrome.exe'),
32
+ path.join(process.env['PROGRAMFILES(X86)'] || 'C:\\Program Files (x86)', 'Google', 'Chrome', 'Application', 'chrome.exe'),
33
+ path.join(process.env.PROGRAMFILES || 'C:\\Program Files', 'Microsoft', 'Edge', 'Application', 'msedge.exe'),
34
+ ]
35
+ : [
36
+ '/usr/bin/google-chrome',
37
+ '/usr/bin/google-chrome-stable',
38
+ '/usr/bin/chromium',
39
+ '/usr/bin/chromium-browser',
40
+ '/usr/bin/microsoft-edge',
41
+ ];
42
+
43
+ return candidates.find((candidate) => fs.existsSync(candidate)) || null;
44
+ }
45
+
46
+ function getFreePort() {
47
+ return new Promise((resolve, reject) => {
48
+ const server = net.createServer();
49
+ server.listen(0, '127.0.0.1', () => {
50
+ const { port } = server.address();
51
+ server.close(() => resolve(port));
52
+ });
53
+ server.on('error', reject);
54
+ });
55
+ }
56
+
57
+ async function waitForJson(url, timeoutMs = 30000) {
58
+ const deadline = Date.now() + timeoutMs;
59
+ let lastError = null;
60
+ while (Date.now() < deadline) {
61
+ try {
62
+ const response = await fetch(url);
63
+ if (response.ok) {
64
+ return response.json();
65
+ }
66
+ lastError = new Error(`HTTP ${response.status}`);
67
+ } catch (err) {
68
+ lastError = err;
69
+ }
70
+ await new Promise((resolve) => setTimeout(resolve, 300));
71
+ }
72
+ throw lastError || new Error(`Timed out waiting for ${url}`);
73
+ }
74
+
75
+ function encodeClientFrame(text, opcode = 1) {
76
+ const payload = Buffer.from(text);
77
+ const mask = crypto.randomBytes(4);
78
+ let header;
79
+
80
+ if (payload.length < 126) {
81
+ header = Buffer.alloc(2);
82
+ header[1] = 0x80 | payload.length;
83
+ } else if (payload.length <= 0xffff) {
84
+ header = Buffer.alloc(4);
85
+ header[1] = 0x80 | 126;
86
+ header.writeUInt16BE(payload.length, 2);
87
+ } else {
88
+ header = Buffer.alloc(10);
89
+ header[1] = 0x80 | 127;
90
+ header.writeBigUInt64BE(BigInt(payload.length), 2);
91
+ }
92
+
93
+ header[0] = 0x80 | opcode;
94
+ const maskedPayload = Buffer.alloc(payload.length);
95
+ for (let index = 0; index < payload.length; index++) {
96
+ maskedPayload[index] = payload[index] ^ mask[index % 4];
97
+ }
98
+
99
+ return Buffer.concat([header, mask, maskedPayload]);
100
+ }
101
+
102
+ function tryDecodeFrame(buffer) {
103
+ if (buffer.length < 2) {return null;}
104
+
105
+ const firstByte = buffer[0];
106
+ const secondByte = buffer[1];
107
+ const opcode = firstByte & 0x0f;
108
+ const masked = (secondByte & 0x80) !== 0;
109
+ let length = secondByte & 0x7f;
110
+ let offset = 2;
111
+
112
+ if (length === 126) {
113
+ if (buffer.length < offset + 2) {return null;}
114
+ length = buffer.readUInt16BE(offset);
115
+ offset += 2;
116
+ } else if (length === 127) {
117
+ if (buffer.length < offset + 8) {return null;}
118
+ length = Number(buffer.readBigUInt64BE(offset));
119
+ offset += 8;
120
+ }
121
+
122
+ let mask = null;
123
+ if (masked) {
124
+ if (buffer.length < offset + 4) {return null;}
125
+ mask = buffer.subarray(offset, offset + 4);
126
+ offset += 4;
127
+ }
128
+
129
+ if (buffer.length < offset + length) {return null;}
130
+
131
+ const payload = Buffer.from(buffer.subarray(offset, offset + length));
132
+ if (mask) {
133
+ for (let index = 0; index < payload.length; index++) {
134
+ payload[index] ^= mask[index % 4];
135
+ }
136
+ }
137
+
138
+ return {
139
+ opcode,
140
+ payload,
141
+ remaining: buffer.subarray(offset + length),
142
+ };
143
+ }
144
+
145
+ function createCdpClient(wsUrl) {
146
+ return new Promise((resolve, reject) => {
147
+ const parsedUrl = new URL(wsUrl);
148
+ if (parsedUrl.protocol !== 'ws:') {
149
+ reject(new Error(`Unsupported CDP WebSocket protocol: ${parsedUrl.protocol}`));
150
+ return;
151
+ }
152
+
153
+ const key = crypto.randomBytes(16).toString('base64');
154
+ const socket = net.createConnection({
155
+ host: parsedUrl.hostname,
156
+ port: Number(parsedUrl.port || 80),
157
+ });
158
+ const pending = new Map();
159
+ let nextId = 1;
160
+ let handshakeDone = false;
161
+ let buffer = Buffer.alloc(0);
162
+ let settled = false;
163
+
164
+ function settleReject(err) {
165
+ if (!settled) {
166
+ settled = true;
167
+ reject(err);
168
+ }
169
+ }
170
+
171
+ function sendJson(payload) {
172
+ socket.write(encodeClientFrame(JSON.stringify(payload)));
173
+ }
174
+
175
+ function closePending(err) {
176
+ for (const { reject: rejectPending } of pending.values()) {
177
+ rejectPending(err);
178
+ }
179
+ pending.clear();
180
+ }
181
+
182
+ socket.on('connect', () => {
183
+ const requestPath = `${parsedUrl.pathname}${parsedUrl.search}`;
184
+ socket.write([
185
+ `GET ${requestPath} HTTP/1.1`,
186
+ `Host: ${parsedUrl.host}`,
187
+ 'Upgrade: websocket',
188
+ 'Connection: Upgrade',
189
+ `Sec-WebSocket-Key: ${key}`,
190
+ 'Sec-WebSocket-Version: 13',
191
+ '',
192
+ '',
193
+ ].join('\r\n'));
194
+ });
195
+
196
+ socket.on('data', (chunk) => {
197
+ buffer = Buffer.concat([buffer, chunk]);
198
+
199
+ if (!handshakeDone) {
200
+ const headerEnd = buffer.indexOf('\r\n\r\n');
201
+ if (headerEnd === -1) {return;}
202
+ const header = buffer.subarray(0, headerEnd).toString('utf8');
203
+ if (!/^HTTP\/1\.1 101\b/i.test(header)) {
204
+ settleReject(new Error(`CDP WebSocket handshake failed: ${header.split('\r\n')[0]}`));
205
+ socket.destroy();
206
+ return;
207
+ }
208
+ handshakeDone = true;
209
+ buffer = buffer.subarray(headerEnd + 4);
210
+ if (!settled) {
211
+ settled = true;
212
+ resolve({
213
+ send(method, params = {}) {
214
+ const id = nextId++;
215
+ sendJson({ id, method, params });
216
+ return new Promise((resolvePending, rejectPending) => {
217
+ pending.set(id, { resolve: resolvePending, reject: rejectPending });
218
+ });
219
+ },
220
+ close() {
221
+ try { socket.end(encodeClientFrame('', 8)); } catch { /* ignore */ }
222
+ },
223
+ });
224
+ }
225
+ }
226
+
227
+ let decoded;
228
+ while ((decoded = tryDecodeFrame(buffer))) {
229
+ buffer = decoded.remaining;
230
+
231
+ if (decoded.opcode === 8) {
232
+ closePending(new Error('CDP WebSocket closed'));
233
+ socket.end();
234
+ return;
235
+ }
236
+
237
+ if (decoded.opcode === 9) {
238
+ socket.write(encodeClientFrame(decoded.payload.toString('utf8'), 10));
239
+ continue;
240
+ }
241
+
242
+ if (decoded.opcode !== 1) {continue;}
243
+
244
+ let message;
245
+ try {
246
+ message = JSON.parse(decoded.payload.toString('utf8'));
247
+ } catch {
248
+ continue;
249
+ }
250
+
251
+ if (!message.id || !pending.has(message.id)) {continue;}
252
+ const { resolve: resolvePending, reject: rejectPending } = pending.get(message.id);
253
+ pending.delete(message.id);
254
+ if (message.error) {
255
+ rejectPending(new Error(message.error.message || JSON.stringify(message.error)));
256
+ } else {
257
+ resolvePending(message.result || {});
258
+ }
259
+ }
260
+ });
261
+
262
+ socket.on('error', (err) => {
263
+ settleReject(err);
264
+ closePending(err);
265
+ });
266
+ socket.on('close', () => closePending(new Error('CDP WebSocket closed')));
267
+ });
268
+ }
269
+
270
+ function deriveBaseUrl(cookies, fallbackUrl) {
271
+ return deriveBaseUrlFromCookies(cookies, fallbackUrl);
272
+ }
273
+
274
+ async function getCurrentPageUrl(client, fallbackUrl) {
275
+ try {
276
+ const result = await client.send('Runtime.evaluate', {
277
+ expression: 'window.location.href',
278
+ returnByValue: true,
279
+ });
280
+ const value = result && result.result && result.result.value;
281
+ return typeof value === 'string' && value ? value : fallbackUrl;
282
+ } catch {
283
+ return fallbackUrl;
284
+ }
285
+ }
286
+
287
+ function deriveBaseUrlFromBrowserState(cookies, loginUrl, currentPageUrl) {
288
+ return deriveBaseUrlFromLoginState(cookies, loginUrl, currentPageUrl);
289
+ }
290
+
291
+ async function runCdpBrowserLogin(options = {}) {
292
+ const browserPath = options.browserPath || findBrowserExecutable();
293
+ if (!browserPath) {
294
+ throw new Error('No Chrome, Edge, or Chromium executable found');
295
+ }
296
+
297
+ const loginUrl = options.loginUrl || 'https://www.aliwork.com/workPlatform';
298
+ const profileDir = fs.mkdtempSync(path.join(os.tmpdir(), 'yidaconnector-browser-login-'));
299
+ const port = await getFreePort();
300
+ const timeoutMs = options.timeoutMs || 10 * 60 * 1000;
301
+ const child = spawn(browserPath, [
302
+ `--user-data-dir=${profileDir}`,
303
+ `--remote-debugging-port=${port}`,
304
+ '--no-first-run',
305
+ '--no-default-browser-check',
306
+ '--disable-features=Translate',
307
+ loginUrl,
308
+ ], {
309
+ stdio: ['ignore', 'ignore', 'pipe'],
310
+ });
311
+ child.stderr.on('data', () => {});
312
+
313
+ let client = null;
314
+ try {
315
+ await waitForJson(`http://127.0.0.1:${port}/json/version`, 30000);
316
+
317
+ let target = null;
318
+ const targetDeadline = Date.now() + 30000;
319
+ while (Date.now() < targetDeadline) {
320
+ const targets = await waitForJson(`http://127.0.0.1:${port}/json/list`, 5000);
321
+ target = targets.find((item) => item.type === 'page' && item.webSocketDebuggerUrl) || null;
322
+ if (target) {break;}
323
+ await new Promise((resolve) => setTimeout(resolve, 300));
324
+ }
325
+ if (!target) {
326
+ throw new Error('No debuggable browser page found');
327
+ }
328
+
329
+ client = await createCdpClient(target.webSocketDebuggerUrl);
330
+ await client.send('Network.enable').catch(() => {});
331
+
332
+ const deadline = Date.now() + timeoutMs;
333
+ while (Date.now() < deadline) {
334
+ const result = await client.send('Network.getAllCookies')
335
+ .catch(() => client.send('Storage.getCookies'));
336
+ const cookies = Array.isArray(result.cookies) ? result.cookies : [];
337
+ if (cookies.some((cookie) => cookie.name === 'tianshu_csrf_token' && cookie.value)) {
338
+ const currentPageUrl = await getCurrentPageUrl(client, target.url || loginUrl);
339
+ return {
340
+ cookies,
341
+ base_url: deriveBaseUrlFromBrowserState(cookies, loginUrl, currentPageUrl),
342
+ };
343
+ }
344
+ await new Promise((resolve) => setTimeout(resolve, 2000));
345
+ }
346
+
347
+ throw new Error('Login timed out before tianshu_csrf_token appeared');
348
+ } finally {
349
+ if (client) {client.close();}
350
+ if (!child.killed) {child.kill('SIGTERM');}
351
+ try { fs.rmSync(profileDir, { recursive: true, force: true }); } catch { /* ignore */ }
352
+ }
353
+ }
354
+
355
+ function cdpBrowserLogin(options = {}) {
356
+ const scriptPath = path.join(os.tmpdir(), `yidaconnector-cdp-login-${Date.now()}.js`);
357
+ const modulePath = __filename;
358
+ fs.writeFileSync(scriptPath, `
359
+ const { runCdpBrowserLogin } = require(${JSON.stringify(modulePath)});
360
+ runCdpBrowserLogin(${JSON.stringify(options)})
361
+ .then((result) => {
362
+ console.log(JSON.stringify(result));
363
+ })
364
+ .catch((err) => {
365
+ console.error(err && err.message ? err.message : String(err));
366
+ process.exit(1);
367
+ });
368
+ `, 'utf8');
369
+
370
+ try {
371
+ const stdout = execFileSync(process.execPath, [scriptPath], {
372
+ encoding: 'utf8',
373
+ stdio: ['inherit', 'pipe', 'inherit'],
374
+ timeout: (options.timeoutMs || 10 * 60 * 1000) + 60000,
375
+ });
376
+ const lines = stdout.trim().split('\n').filter(Boolean);
377
+ return JSON.parse(lines[lines.length - 1]);
378
+ } finally {
379
+ try { fs.unlinkSync(scriptPath); } catch { /* ignore */ }
380
+ }
381
+ }
382
+
383
+ module.exports = {
384
+ cdpBrowserLogin,
385
+ createCdpClient,
386
+ deriveBaseUrl,
387
+ deriveBaseUrlFromBrowserState,
388
+ findBrowserExecutable,
389
+ runCdpBrowserLogin,
390
+ };