botmux 2.49.0 → 2.51.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.
- package/README.en.md +48 -46
- package/README.md +44 -42
- package/dist/adapters/cli/aiden.js +3 -2
- package/dist/adapters/cli/aiden.js.map +1 -1
- package/dist/adapters/cli/antigravity.js +2 -2
- package/dist/adapters/cli/antigravity.js.map +1 -1
- package/dist/adapters/cli/claude-code.d.ts.map +1 -1
- package/dist/adapters/cli/claude-code.js +12 -10
- package/dist/adapters/cli/claude-code.js.map +1 -1
- package/dist/adapters/cli/coco.js +3 -2
- package/dist/adapters/cli/coco.js.map +1 -1
- package/dist/adapters/cli/codex.js +2 -2
- package/dist/adapters/cli/codex.js.map +1 -1
- package/dist/adapters/cli/cursor.js +2 -2
- package/dist/adapters/cli/cursor.js.map +1 -1
- package/dist/adapters/cli/gemini.js +2 -2
- package/dist/adapters/cli/gemini.js.map +1 -1
- package/dist/adapters/cli/hermes.d.ts.map +1 -1
- package/dist/adapters/cli/hermes.js +4 -2
- package/dist/adapters/cli/hermes.js.map +1 -1
- package/dist/adapters/cli/types.d.ts +2 -0
- package/dist/adapters/cli/types.d.ts.map +1 -1
- package/dist/bot-registry.d.ts +5 -0
- package/dist/bot-registry.d.ts.map +1 -1
- package/dist/bot-registry.js +1 -0
- package/dist/bot-registry.js.map +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +60 -6
- package/dist/cli.js.map +1 -1
- package/dist/core/worker-pool.d.ts.map +1 -1
- package/dist/core/worker-pool.js +2 -0
- package/dist/core/worker-pool.js.map +1 -1
- package/dist/setup/open-platform-automation.d.ts +149 -0
- package/dist/setup/open-platform-automation.d.ts.map +1 -0
- package/dist/setup/open-platform-automation.js +845 -0
- package/dist/setup/open-platform-automation.js.map +1 -0
- package/dist/types.d.ts +1 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/worker.js +1 -0
- package/dist/worker.js.map +1 -1
- package/dist/workflows/definition.d.ts +8 -8
- package/dist/workflows/events/payloads.d.ts +10 -10
- package/dist/workflows/events/schema.d.ts +28 -28
- package/package.json +1 -1
|
@@ -0,0 +1,845 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Automates the Open Platform half of `botmux setup` after the PersonalAgent
|
|
3
|
+
* app has been created by the Feishu SDK registerApp flow.
|
|
4
|
+
*
|
|
5
|
+
* Follow-up for PR review: the current end-to-end setup can still ask for two
|
|
6
|
+
* QR scans: one for SDK app creation and one for this Web session. These can be
|
|
7
|
+
* collapsed in a later iteration by making the Feishu Web session the primary
|
|
8
|
+
* path for app creation as well: Web QR login -> create/find app -> read
|
|
9
|
+
* AppID/AppSecret -> write bots.json -> configure scopes/redirect/version.
|
|
10
|
+
* With a cached ~/.botmux/feishu-session.json, that path can create another bot
|
|
11
|
+
* with no QR scan at all. Keep the current SDK creation path as the stable
|
|
12
|
+
* fallback until that flow is fully verified.
|
|
13
|
+
*/
|
|
14
|
+
import { chmodSync, existsSync, mkdirSync, readFileSync, renameSync, unlinkSync, writeFileSync } from 'node:fs';
|
|
15
|
+
import { basename, join, dirname } from 'node:path';
|
|
16
|
+
import { homedir } from 'node:os';
|
|
17
|
+
import { fileURLToPath } from 'node:url';
|
|
18
|
+
import qrcode from 'qrcode-terminal';
|
|
19
|
+
export const BOTMUX_REDIRECT_URL = 'http://127.0.0.1:9768/callback';
|
|
20
|
+
const FEISHU_ACCOUNTS_ORIGIN = 'https://accounts.feishu.cn';
|
|
21
|
+
const ASK_FEISHU_ORIGIN = 'https://ask.feishu.cn';
|
|
22
|
+
const FEISHU_APP_ID = '12';
|
|
23
|
+
const FEISHU_COMMON_HEADERS = {
|
|
24
|
+
'x-api-version': '1.0.28',
|
|
25
|
+
'x-device-info': 'device_id=0;device_name=Chrome;device_os=Mac;device_model=Chrome;lark_version=;channel=Release;package_name=feishu;tt_app_id=1658;is_dpop_support=true;is_iframe=false',
|
|
26
|
+
'x-locale': 'zh-CN',
|
|
27
|
+
'x-terminal-type': '2',
|
|
28
|
+
};
|
|
29
|
+
export function parseSetupOpenPlatformAutoFlag(argv) {
|
|
30
|
+
let enabled = true;
|
|
31
|
+
for (const arg of argv) {
|
|
32
|
+
if (arg === '--open-platform-auto')
|
|
33
|
+
enabled = true;
|
|
34
|
+
if (arg === '--no-open-platform-auto')
|
|
35
|
+
enabled = false;
|
|
36
|
+
}
|
|
37
|
+
return enabled;
|
|
38
|
+
}
|
|
39
|
+
export function botmuxFeishuSessionFilePath(configDir = join(homedir(), '.botmux')) {
|
|
40
|
+
return join(configDir, 'feishu-session.json');
|
|
41
|
+
}
|
|
42
|
+
export function bytedcliFeishuSessionFilePath(homeDir = homedir()) {
|
|
43
|
+
return join(homeDir, '.local', 'share', 'bytedcli', 'data', 'feishu_session.json');
|
|
44
|
+
}
|
|
45
|
+
export function readStoredCookiesFromSessionFile(filePath) {
|
|
46
|
+
if (!existsSync(filePath))
|
|
47
|
+
return null;
|
|
48
|
+
let parsed;
|
|
49
|
+
try {
|
|
50
|
+
parsed = JSON.parse(readFileSync(filePath, 'utf-8'));
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed))
|
|
56
|
+
return null;
|
|
57
|
+
const cookies = parsed.cookies;
|
|
58
|
+
if (!Array.isArray(cookies))
|
|
59
|
+
return null;
|
|
60
|
+
return pruneExpiredCookies(cookies.filter(isStoredCookieRecord));
|
|
61
|
+
}
|
|
62
|
+
export function readStoredCookiesFromBytedcliSession(filePath) {
|
|
63
|
+
return readStoredCookiesFromSessionFile(filePath);
|
|
64
|
+
}
|
|
65
|
+
export function writeStoredCookiesToSessionFile(filePath, cookies) {
|
|
66
|
+
const dir = dirname(filePath);
|
|
67
|
+
mkdirSync(dir, { recursive: true });
|
|
68
|
+
try {
|
|
69
|
+
chmodSync(dir, 0o700);
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
// Best-effort on non-POSIX filesystems.
|
|
73
|
+
}
|
|
74
|
+
const tmpPath = join(dir, `.${basename(filePath)}.${process.pid}.${Date.now()}.tmp`);
|
|
75
|
+
try {
|
|
76
|
+
writeFileSync(tmpPath, JSON.stringify({ cookies: pruneExpiredCookies(cookies) }, null, 2), {
|
|
77
|
+
encoding: 'utf-8',
|
|
78
|
+
mode: 0o600,
|
|
79
|
+
});
|
|
80
|
+
renameSync(tmpPath, filePath);
|
|
81
|
+
}
|
|
82
|
+
finally {
|
|
83
|
+
try {
|
|
84
|
+
unlinkSync(tmpPath);
|
|
85
|
+
}
|
|
86
|
+
catch {
|
|
87
|
+
// Ignore.
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
try {
|
|
91
|
+
chmodSync(filePath, 0o600);
|
|
92
|
+
}
|
|
93
|
+
catch {
|
|
94
|
+
// Best-effort on non-POSIX filesystems.
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
export function getCookieHeader(cookies, requestUrl) {
|
|
98
|
+
const url = new URL(requestUrl);
|
|
99
|
+
return pruneExpiredCookies(cookies)
|
|
100
|
+
.filter(cookie => {
|
|
101
|
+
if (cookie.secure && url.protocol !== 'https:')
|
|
102
|
+
return false;
|
|
103
|
+
if (!domainMatches(url.hostname, cookie))
|
|
104
|
+
return false;
|
|
105
|
+
return pathMatches(url.pathname || '/', cookie.path || '/');
|
|
106
|
+
})
|
|
107
|
+
.sort((a, b) => b.path.length - a.path.length)
|
|
108
|
+
.map(cookie => `${cookie.name}=${cookie.value}`)
|
|
109
|
+
.join('; ');
|
|
110
|
+
}
|
|
111
|
+
export function extractOpenPlatformCsrfToken(html) {
|
|
112
|
+
const match = html.match(/\bwindow\.csrfToken\s*=\s*(['"])([^'"]+)\1/) ??
|
|
113
|
+
html.match(/\bcsrfToken\s*:\s*(['"])([^'"]+)\1/);
|
|
114
|
+
return match?.[2] ?? null;
|
|
115
|
+
}
|
|
116
|
+
export function extractOpenPlatformScopeEntries(payload) {
|
|
117
|
+
const out = [];
|
|
118
|
+
collectScopeEntries(payload, undefined, out);
|
|
119
|
+
const seen = new Set();
|
|
120
|
+
return out.filter(entry => {
|
|
121
|
+
const key = `${entry.bucket ?? 'any'}:${entry.name}:${entry.id}`;
|
|
122
|
+
if (seen.has(key))
|
|
123
|
+
return false;
|
|
124
|
+
seen.add(key);
|
|
125
|
+
return true;
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
export function mapManifestScopesToOpenPlatformIds(manifest, catalog) {
|
|
129
|
+
const tenant = uniqueStrings(manifest.scopes?.tenant ?? []);
|
|
130
|
+
const user = uniqueStrings(manifest.scopes?.user ?? []);
|
|
131
|
+
return {
|
|
132
|
+
tenantScopeIds: mapScopeIds(tenant, catalog, 'tenant').ids,
|
|
133
|
+
userScopeIds: mapScopeIds(user, catalog, 'user').ids,
|
|
134
|
+
missingTenantScopes: mapScopeIds(tenant, catalog, 'tenant').missing,
|
|
135
|
+
missingUserScopes: mapScopeIds(user, catalog, 'user').missing,
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
export function buildScopeUpdatePayload(appId, mapped) {
|
|
139
|
+
return {
|
|
140
|
+
clientId: appId,
|
|
141
|
+
appScopeIDs: mapped.tenantScopeIds,
|
|
142
|
+
userScopeIDs: mapped.userScopeIds,
|
|
143
|
+
scopeIds: [],
|
|
144
|
+
operation: 'add',
|
|
145
|
+
isDeveloperPanel: true,
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
export function buildSafeSettingPayload(appId) {
|
|
149
|
+
return {
|
|
150
|
+
clientId: appId,
|
|
151
|
+
redirectURL: [BOTMUX_REDIRECT_URL],
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
export function buildAppVersionCreatePayload(appVersion, visibleMemberIds = []) {
|
|
155
|
+
return {
|
|
156
|
+
appVersion,
|
|
157
|
+
mobileDefaultAbility: 'bot',
|
|
158
|
+
pcDefaultAbility: 'bot',
|
|
159
|
+
changeLog: 'Init version',
|
|
160
|
+
visibleSuggest: {
|
|
161
|
+
departments: [],
|
|
162
|
+
members: visibleMemberIds,
|
|
163
|
+
groups: [],
|
|
164
|
+
isAll: 0,
|
|
165
|
+
},
|
|
166
|
+
applyReasonConfig: {
|
|
167
|
+
apiPrivilegeNeedReason: true,
|
|
168
|
+
contactPrivilegeNeedReason: true,
|
|
169
|
+
dataPrivilegeReasonMap: {},
|
|
170
|
+
visibleScopeNeedReason: true,
|
|
171
|
+
apiPrivilegeReasonMap: {},
|
|
172
|
+
contactPrivilegeReason: '',
|
|
173
|
+
isDataPrivilegeExpandMap: {},
|
|
174
|
+
visibleScopeReason: '',
|
|
175
|
+
dataPrivilegeNeedReason: true,
|
|
176
|
+
isAutoAudit: false,
|
|
177
|
+
isContactExpand: false,
|
|
178
|
+
},
|
|
179
|
+
b2cShareSuggest: false,
|
|
180
|
+
autoPublish: false,
|
|
181
|
+
remark: 'Personal AI assistant for self use',
|
|
182
|
+
blackVisibleSuggest: {
|
|
183
|
+
departments: [],
|
|
184
|
+
members: [],
|
|
185
|
+
groups: [],
|
|
186
|
+
isAll: 0,
|
|
187
|
+
},
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
export function buildFeishuQrPayload(token) {
|
|
191
|
+
return JSON.stringify({ qrlogin: { token } });
|
|
192
|
+
}
|
|
193
|
+
export function mapFeishuQrPollingStatus(status) {
|
|
194
|
+
if (status === 2)
|
|
195
|
+
return '已经扫码,等待手机确认';
|
|
196
|
+
if (status === 5)
|
|
197
|
+
return '二维码已过期';
|
|
198
|
+
return '等待飞书扫码';
|
|
199
|
+
}
|
|
200
|
+
export async function prepareFeishuWebSession(options = {}) {
|
|
201
|
+
const fetcher = options.fetchImpl ?? fetch;
|
|
202
|
+
const sessionFile = options.sessionFilePath ?? botmuxFeishuSessionFilePath();
|
|
203
|
+
const cached = readStoredCookiesFromSessionFile(sessionFile);
|
|
204
|
+
if (cached && cached.length > 0 && await validateFeishuWebSession(cached, fetcher)) {
|
|
205
|
+
return {
|
|
206
|
+
ok: true,
|
|
207
|
+
sessionFile,
|
|
208
|
+
source: 'botmux_cache',
|
|
209
|
+
cookies: cached,
|
|
210
|
+
cookieCount: cached.length,
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
let loginError;
|
|
214
|
+
try {
|
|
215
|
+
const loggedIn = await loginFeishuWebSession(fetcher, options);
|
|
216
|
+
writeStoredCookiesToSessionFile(sessionFile, loggedIn);
|
|
217
|
+
return {
|
|
218
|
+
ok: true,
|
|
219
|
+
sessionFile,
|
|
220
|
+
source: 'qr_login',
|
|
221
|
+
cookies: loggedIn,
|
|
222
|
+
cookieCount: loggedIn.length,
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
catch (err) {
|
|
226
|
+
loginError = err;
|
|
227
|
+
}
|
|
228
|
+
const fallbackSessionFile = options.bytedcliFallbackSessionFilePath ?? bytedcliFeishuSessionFilePath();
|
|
229
|
+
if (!options.disableBytedcliFallback) {
|
|
230
|
+
const fallback = readStoredCookiesFromBytedcliSession(fallbackSessionFile);
|
|
231
|
+
if (fallback && fallback.length > 0 && await validateFeishuWebSession(fallback, fetcher)) {
|
|
232
|
+
writeStoredCookiesToSessionFile(sessionFile, fallback);
|
|
233
|
+
return {
|
|
234
|
+
ok: true,
|
|
235
|
+
sessionFile,
|
|
236
|
+
source: 'bytedcli_fallback',
|
|
237
|
+
cookies: fallback,
|
|
238
|
+
cookieCount: fallback.length,
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
return {
|
|
243
|
+
ok: false,
|
|
244
|
+
reason: classifyFeishuLoginError(loginError),
|
|
245
|
+
message: safeErrorMessage(loginError),
|
|
246
|
+
sessionFile,
|
|
247
|
+
fallbackSessionFile: options.disableBytedcliFallback ? undefined : fallbackSessionFile,
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
export async function automateOpenPlatformSetup(options) {
|
|
251
|
+
const brand = options.brand ?? 'feishu';
|
|
252
|
+
if (brand !== 'feishu') {
|
|
253
|
+
return { ok: false, reason: 'unsupported_brand', message: '开放平台自动配置当前只支持 feishu.cn 租户' };
|
|
254
|
+
}
|
|
255
|
+
const fetcher = options.fetchImpl ?? fetch;
|
|
256
|
+
const preparedSession = await prepareFeishuWebSession({
|
|
257
|
+
sessionFilePath: options.sessionFilePath,
|
|
258
|
+
bytedcliFallbackSessionFilePath: options.bytedcliFallbackSessionFilePath,
|
|
259
|
+
disableBytedcliFallback: options.disableBytedcliFallback,
|
|
260
|
+
fetchImpl: fetcher,
|
|
261
|
+
pollIntervalMs: options.pollIntervalMs,
|
|
262
|
+
maxWaitMs: options.maxWaitMs,
|
|
263
|
+
onQrCode: options.onQrCode,
|
|
264
|
+
onStatus: options.onStatus,
|
|
265
|
+
});
|
|
266
|
+
if (!preparedSession.ok) {
|
|
267
|
+
return {
|
|
268
|
+
ok: false,
|
|
269
|
+
reason: preparedSession.reason,
|
|
270
|
+
message: `获取 Feishu Web session 失败: ${preparedSession.message}`,
|
|
271
|
+
sessionFile: preparedSession.sessionFile,
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
const sessionFile = preparedSession.sessionFile;
|
|
275
|
+
const session = new MutableCookieJar(preparedSession.cookies);
|
|
276
|
+
const defaultOrigin = 'https://open.feishu.cn';
|
|
277
|
+
const defaultAppHome = `${defaultOrigin}/app/${options.appId}`;
|
|
278
|
+
// The botmux-managed Feishu Web login yields reusable cookies, not Open
|
|
279
|
+
// Platform's page-scoped `window.csrfToken`. Load an Open Platform page with
|
|
280
|
+
// those cookies and extract CSRF from HTML before calling `/developers/v1/*`.
|
|
281
|
+
// Feishu tenants can redirect the console to open.larkoffice.com; API origin,
|
|
282
|
+
// referer, CSRF token and cookies must stay on that final origin.
|
|
283
|
+
let csrfToken = null;
|
|
284
|
+
let apiOrigin = defaultOrigin;
|
|
285
|
+
let appHome = defaultAppHome;
|
|
286
|
+
try {
|
|
287
|
+
const authPage = await session.fetchTextWithUrl(fetcher, `${defaultAppHome}/auth`);
|
|
288
|
+
apiOrigin = new URL(authPage.finalUrl).origin;
|
|
289
|
+
appHome = `${apiOrigin}/app/${options.appId}`;
|
|
290
|
+
csrfToken = extractOpenPlatformCsrfToken(authPage.text);
|
|
291
|
+
if (!csrfToken) {
|
|
292
|
+
const homePage = await session.fetchTextWithUrl(fetcher, appHome);
|
|
293
|
+
apiOrigin = new URL(homePage.finalUrl).origin;
|
|
294
|
+
appHome = `${apiOrigin}/app/${options.appId}`;
|
|
295
|
+
csrfToken = extractOpenPlatformCsrfToken(homePage.text);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
catch (err) {
|
|
299
|
+
return { ok: false, reason: 'network', message: `读取开放平台页面失败: ${safeErrorMessage(err)}`, sessionFile };
|
|
300
|
+
}
|
|
301
|
+
if (!csrfToken) {
|
|
302
|
+
return {
|
|
303
|
+
ok: false,
|
|
304
|
+
reason: 'missing_csrf',
|
|
305
|
+
message: 'Feishu session 可读取,但开放平台页面没有返回 window.csrfToken;可能需要在浏览器完成开放平台登录',
|
|
306
|
+
sessionFile,
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
const postJson = async (path, body) => {
|
|
310
|
+
const url = `${apiOrigin}${path}`;
|
|
311
|
+
const response = await session.fetchRaw(fetcher, url, {
|
|
312
|
+
method: 'POST',
|
|
313
|
+
headers: {
|
|
314
|
+
accept: 'application/json, text/plain, */*',
|
|
315
|
+
origin: apiOrigin,
|
|
316
|
+
referer: appHome,
|
|
317
|
+
'x-csrf-token': csrfToken,
|
|
318
|
+
...(body === undefined ? {} : { 'content-type': 'application/json' }),
|
|
319
|
+
},
|
|
320
|
+
body: body === undefined ? undefined : JSON.stringify(body),
|
|
321
|
+
});
|
|
322
|
+
let data;
|
|
323
|
+
try {
|
|
324
|
+
data = await response.json();
|
|
325
|
+
}
|
|
326
|
+
catch {
|
|
327
|
+
data = null;
|
|
328
|
+
}
|
|
329
|
+
if (!response.ok) {
|
|
330
|
+
throw new OpenPlatformApiError(`HTTP ${response.status} ${path}: ${summarizeOpenPlatformPayload(data)}`, data);
|
|
331
|
+
}
|
|
332
|
+
if (data && typeof data === 'object' && typeof data.code === 'number' && data.code !== 0) {
|
|
333
|
+
throw new OpenPlatformApiError(`code=${data.code} msg=${data.msg ?? data.message ?? ''}`, data);
|
|
334
|
+
}
|
|
335
|
+
return data;
|
|
336
|
+
};
|
|
337
|
+
let allScopesPayload;
|
|
338
|
+
try {
|
|
339
|
+
allScopesPayload = await postJson(`/developers/v1/scope/all/${options.appId}`);
|
|
340
|
+
}
|
|
341
|
+
catch (err) {
|
|
342
|
+
return { ok: false, reason: 'api_error', message: `读取开放平台 scope 列表失败: ${safeErrorMessage(err)}`, sessionFile };
|
|
343
|
+
}
|
|
344
|
+
const manifest = options.scopeManifest ?? readDefaultScopeManifest();
|
|
345
|
+
const catalog = extractOpenPlatformScopeEntries(allScopesPayload);
|
|
346
|
+
const mapped = mapManifestScopesToOpenPlatformIds(manifest, catalog);
|
|
347
|
+
const missing = [...mapped.missingTenantScopes, ...mapped.missingUserScopes];
|
|
348
|
+
const skippedScopeCount = missing.length;
|
|
349
|
+
if (missing.length > 0) {
|
|
350
|
+
console.warn(`Warning: ${missing.length} scopes are not present in the Open Platform catalog and will be skipped: ${missing.slice(0, 8).join(', ')}`);
|
|
351
|
+
}
|
|
352
|
+
// "部分权限即成功":有的租户目录下个别权限不可授予,整批 scope/update 会被拒。
|
|
353
|
+
// 把权限注册做成非致命——失败只告警并继续配 redirect / 建版本,不让权限问题阻塞建 bot。
|
|
354
|
+
let importedScopeCount = mapped.tenantScopeIds.length + mapped.userScopeIds.length;
|
|
355
|
+
let scopeWarning;
|
|
356
|
+
if (importedScopeCount > 0) {
|
|
357
|
+
try {
|
|
358
|
+
await postJson(`/developers/v1/scope/update/${options.appId}`, buildScopeUpdatePayload(options.appId, mapped));
|
|
359
|
+
}
|
|
360
|
+
catch (err) {
|
|
361
|
+
scopeWarning = safeErrorMessage(err);
|
|
362
|
+
importedScopeCount = 0;
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
try {
|
|
366
|
+
await postJson(`/developers/v1/safe_setting/update/${options.appId}`, buildSafeSettingPayload(options.appId));
|
|
367
|
+
const contactRange = await postJson(`/developers/v1/contact_range/${options.appId}`, {});
|
|
368
|
+
const visibleMemberIds = extractContactRangeMemberIds(contactRange);
|
|
369
|
+
const versionList = await postJson(`/developers/v1/app_version/list/${options.appId}`, {});
|
|
370
|
+
const appVersion = nextAppVersion(versionList);
|
|
371
|
+
const created = await postJson(`/developers/v1/app_version/create/${options.appId}`, buildAppVersionCreatePayload(appVersion, visibleMemberIds));
|
|
372
|
+
const versionId = extractVersionId(created);
|
|
373
|
+
if (versionId) {
|
|
374
|
+
await postJson(`/developers/v1/publish/commit/${options.appId}/${versionId}`, { clientId: options.appId });
|
|
375
|
+
}
|
|
376
|
+
return {
|
|
377
|
+
ok: true,
|
|
378
|
+
sessionFile,
|
|
379
|
+
sessionSource: preparedSession.source,
|
|
380
|
+
cookieCount: preparedSession.cookieCount,
|
|
381
|
+
scopeCount: importedScopeCount,
|
|
382
|
+
skippedScopeCount,
|
|
383
|
+
scopeWarning,
|
|
384
|
+
versionId,
|
|
385
|
+
};
|
|
386
|
+
}
|
|
387
|
+
catch (err) {
|
|
388
|
+
return { ok: false, reason: 'api_error', message: `开放平台自动配置失败: ${safeErrorMessage(err)}`, sessionFile };
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
async function validateFeishuWebSession(cookies, fetcher) {
|
|
392
|
+
if (cookies.length === 0)
|
|
393
|
+
return false;
|
|
394
|
+
const session = new MutableCookieJar(cookies);
|
|
395
|
+
try {
|
|
396
|
+
const response = await session.fetchRaw(fetcher, `${ASK_FEISHU_ORIGIN}/`, { method: 'GET' });
|
|
397
|
+
if (!response.ok)
|
|
398
|
+
return false;
|
|
399
|
+
const text = await response.text();
|
|
400
|
+
return !isFeishuLoginLikeValue(text);
|
|
401
|
+
}
|
|
402
|
+
catch {
|
|
403
|
+
return false;
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
async function loginFeishuWebSession(fetcher, options) {
|
|
407
|
+
const session = new MutableCookieJar([]);
|
|
408
|
+
const redirectUrl = `${ASK_FEISHU_ORIGIN}/`;
|
|
409
|
+
// Implements Feishu Web QR session login directly: initialize
|
|
410
|
+
// `/accounts/qrlogin/init`, poll `/accounts/qrlogin/polling`, follow the
|
|
411
|
+
// returned cross-login URI, then persist the resulting cookie jar privately.
|
|
412
|
+
const qrInit = await initFeishuQrLogin(session, fetcher, redirectUrl);
|
|
413
|
+
const qrPayload = buildFeishuQrPayload(qrInit.token);
|
|
414
|
+
const qrText = await renderTerminalQr(qrPayload);
|
|
415
|
+
const onQrCode = options.onQrCode ?? defaultPrintFeishuQrCode;
|
|
416
|
+
await onQrCode({ qrText, qrPayload });
|
|
417
|
+
const pollIntervalMs = options.pollIntervalMs ?? 1500;
|
|
418
|
+
const maxWaitMs = options.maxWaitMs ?? 120_000;
|
|
419
|
+
const start = Date.now();
|
|
420
|
+
let lastStatusMessage = '';
|
|
421
|
+
for (;;) {
|
|
422
|
+
if (Date.now() - start > maxWaitMs) {
|
|
423
|
+
throw new FeishuWebSessionError('等待飞书扫码超时', 'timeout');
|
|
424
|
+
}
|
|
425
|
+
const poll = await pollFeishuQrLogin(session, fetcher, qrInit.flowKey);
|
|
426
|
+
if (poll.nextStep === 'enter_app') {
|
|
427
|
+
if (poll.crossLoginUri) {
|
|
428
|
+
await session.fetchRaw(fetcher, poll.crossLoginUri, { method: 'GET' });
|
|
429
|
+
}
|
|
430
|
+
await session.fetchRaw(fetcher, redirectUrl, { method: 'GET' });
|
|
431
|
+
const cookies = session.toJSON();
|
|
432
|
+
if (!await validateFeishuWebSession(cookies, fetcher)) {
|
|
433
|
+
throw new FeishuWebSessionError('飞书扫码已完成,但没有拿到可复用的 Web session', 'invalid_session');
|
|
434
|
+
}
|
|
435
|
+
return cookies;
|
|
436
|
+
}
|
|
437
|
+
const statusMessage = mapFeishuQrPollingStatus(poll.status);
|
|
438
|
+
if (options.onStatus && statusMessage !== lastStatusMessage) {
|
|
439
|
+
lastStatusMessage = statusMessage;
|
|
440
|
+
await options.onStatus(statusMessage);
|
|
441
|
+
}
|
|
442
|
+
if (poll.status === 5) {
|
|
443
|
+
throw new FeishuWebSessionError('二维码已过期', 'qr_expired');
|
|
444
|
+
}
|
|
445
|
+
await sleep(pollIntervalMs);
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
async function initFeishuQrLogin(session, fetcher, authorizeUrl) {
|
|
449
|
+
const endpoint = `${FEISHU_ACCOUNTS_ORIGIN}/accounts/qrlogin/init?_r${10000 + Math.floor(Math.random() * 80000)}=${Date.now()}`;
|
|
450
|
+
const response = await session.fetchRaw(fetcher, endpoint, {
|
|
451
|
+
method: 'POST',
|
|
452
|
+
headers: {
|
|
453
|
+
...FEISHU_COMMON_HEADERS,
|
|
454
|
+
'x-app-id': FEISHU_APP_ID,
|
|
455
|
+
accept: 'application/json',
|
|
456
|
+
'content-type': 'application/json',
|
|
457
|
+
},
|
|
458
|
+
body: JSON.stringify({
|
|
459
|
+
biz_type: null,
|
|
460
|
+
redirect_uri: authorizeUrl,
|
|
461
|
+
}),
|
|
462
|
+
});
|
|
463
|
+
const data = await response.json();
|
|
464
|
+
assertFeishuApiOk(data, 'Feishu QR init failed');
|
|
465
|
+
const token = asRecord(asRecord(data).data).step_info
|
|
466
|
+
? pickString(asRecord(asRecord(asRecord(data).data).step_info), ['token'])
|
|
467
|
+
: undefined;
|
|
468
|
+
const flowKey = response.headers.get('x-flow-key') ?? '';
|
|
469
|
+
if (!flowKey || !token) {
|
|
470
|
+
throw new FeishuWebSessionError('Feishu QR init missing flow key or token', 'login_failed');
|
|
471
|
+
}
|
|
472
|
+
return { flowKey, token };
|
|
473
|
+
}
|
|
474
|
+
async function pollFeishuQrLogin(session, fetcher, flowKey) {
|
|
475
|
+
const endpoint = `${FEISHU_ACCOUNTS_ORIGIN}/accounts/qrlogin/polling?_r${10000 + Math.floor(Math.random() * 80000)}=${Date.now()}`;
|
|
476
|
+
const response = await session.fetchRaw(fetcher, endpoint, {
|
|
477
|
+
method: 'POST',
|
|
478
|
+
headers: {
|
|
479
|
+
...FEISHU_COMMON_HEADERS,
|
|
480
|
+
'x-app-id': FEISHU_APP_ID,
|
|
481
|
+
'x-flow-key': flowKey,
|
|
482
|
+
accept: 'application/json',
|
|
483
|
+
'content-type': 'application/json',
|
|
484
|
+
},
|
|
485
|
+
body: JSON.stringify({ biz_type: null }),
|
|
486
|
+
});
|
|
487
|
+
const data = await response.json();
|
|
488
|
+
assertFeishuApiOk(data, 'Feishu QR polling failed');
|
|
489
|
+
const payload = asRecord(asRecord(data).data);
|
|
490
|
+
const stepInfo = asRecord(payload.step_info);
|
|
491
|
+
return {
|
|
492
|
+
nextStep: pickString(payload, ['next_step']) ?? null,
|
|
493
|
+
status: typeof stepInfo.status === 'number' ? stepInfo.status : null,
|
|
494
|
+
crossLoginUri: pickString(stepInfo, ['cross_login_uri']) ?? null,
|
|
495
|
+
};
|
|
496
|
+
}
|
|
497
|
+
function readDefaultScopeManifest() {
|
|
498
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
499
|
+
const candidates = [
|
|
500
|
+
join(here, 'lark-scopes.json'),
|
|
501
|
+
join(here, 'setup', 'lark-scopes.json'),
|
|
502
|
+
join(here, '..', 'src', 'setup', 'lark-scopes.json'),
|
|
503
|
+
];
|
|
504
|
+
for (const candidate of candidates) {
|
|
505
|
+
if (!existsSync(candidate))
|
|
506
|
+
continue;
|
|
507
|
+
return JSON.parse(readFileSync(candidate, 'utf-8'));
|
|
508
|
+
}
|
|
509
|
+
throw new Error('找不到 botmux lark-scopes.json');
|
|
510
|
+
}
|
|
511
|
+
class MutableCookieJar {
|
|
512
|
+
cookies;
|
|
513
|
+
constructor(cookies) {
|
|
514
|
+
this.cookies = pruneExpiredCookies(cookies);
|
|
515
|
+
}
|
|
516
|
+
toJSON() {
|
|
517
|
+
this.cookies = pruneExpiredCookies(this.cookies);
|
|
518
|
+
return this.cookies.map(cookie => ({ ...cookie }));
|
|
519
|
+
}
|
|
520
|
+
async fetchText(fetcher, url) {
|
|
521
|
+
const response = await this.fetchRaw(fetcher, url, { method: 'GET' });
|
|
522
|
+
return await response.text();
|
|
523
|
+
}
|
|
524
|
+
async fetchTextWithUrl(fetcher, url) {
|
|
525
|
+
const response = await this.fetchRaw(fetcher, url, { method: 'GET' });
|
|
526
|
+
return {
|
|
527
|
+
text: await response.text(),
|
|
528
|
+
finalUrl: finalResponseUrl(response, url),
|
|
529
|
+
};
|
|
530
|
+
}
|
|
531
|
+
async fetchRaw(fetcher, url, init = {}, maxHops = 10) {
|
|
532
|
+
let current = url;
|
|
533
|
+
let referer;
|
|
534
|
+
for (let hop = 0; hop <= maxHops; hop += 1) {
|
|
535
|
+
const headers = new Headers(init.headers);
|
|
536
|
+
const cookieHeader = getCookieHeader(this.cookies, current);
|
|
537
|
+
if (cookieHeader)
|
|
538
|
+
headers.set('cookie', cookieHeader);
|
|
539
|
+
headers.set('user-agent', headers.get('user-agent') ?? DEFAULT_BROWSER_USER_AGENT);
|
|
540
|
+
if (referer && !headers.has('referer'))
|
|
541
|
+
headers.set('referer', referer);
|
|
542
|
+
const response = await fetcher(current, { ...init, headers, redirect: 'manual' });
|
|
543
|
+
this.loadFromResponse(current, response.headers);
|
|
544
|
+
if (response.status >= 300 && response.status < 400) {
|
|
545
|
+
const location = response.headers.get('location');
|
|
546
|
+
if (!location)
|
|
547
|
+
return response;
|
|
548
|
+
referer = current;
|
|
549
|
+
current = new URL(location, current).toString();
|
|
550
|
+
continue;
|
|
551
|
+
}
|
|
552
|
+
markFinalResponseUrl(response, current);
|
|
553
|
+
return response;
|
|
554
|
+
}
|
|
555
|
+
throw new Error('Too many redirects while accessing open platform');
|
|
556
|
+
}
|
|
557
|
+
loadFromResponse(responseUrl, headers) {
|
|
558
|
+
const rawSetCookies = typeof headers.getSetCookie === 'function'
|
|
559
|
+
? headers.getSetCookie()
|
|
560
|
+
: splitSetCookieHeader(headers.get('set-cookie'));
|
|
561
|
+
for (const raw of rawSetCookies) {
|
|
562
|
+
const cookie = parseSetCookie(responseUrl, raw);
|
|
563
|
+
if (!cookie)
|
|
564
|
+
continue;
|
|
565
|
+
const idx = this.cookies.findIndex(item => item.name === cookie.name && item.domain === cookie.domain && item.path === cookie.path);
|
|
566
|
+
if (cookie.expiresAt !== undefined && cookie.expiresAt <= Date.now()) {
|
|
567
|
+
if (idx >= 0)
|
|
568
|
+
this.cookies.splice(idx, 1);
|
|
569
|
+
continue;
|
|
570
|
+
}
|
|
571
|
+
if (idx >= 0)
|
|
572
|
+
this.cookies[idx] = cookie;
|
|
573
|
+
else
|
|
574
|
+
this.cookies.push(cookie);
|
|
575
|
+
}
|
|
576
|
+
this.cookies = pruneExpiredCookies(this.cookies);
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
class OpenPlatformApiError extends Error {
|
|
580
|
+
payload;
|
|
581
|
+
constructor(message, payload) {
|
|
582
|
+
super(message);
|
|
583
|
+
this.payload = payload;
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
class FeishuWebSessionError extends Error {
|
|
587
|
+
reason;
|
|
588
|
+
constructor(message, reason) {
|
|
589
|
+
super(message);
|
|
590
|
+
this.reason = reason;
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
const DEFAULT_BROWSER_USER_AGENT = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/145.0.0.0 Safari/537.36';
|
|
594
|
+
function defaultPrintFeishuQrCode(info) {
|
|
595
|
+
process.stderr.write('\n请用飞书 App 扫码完成开放平台自动配置登录:\n\n');
|
|
596
|
+
process.stderr.write(`${info.qrText}\n`);
|
|
597
|
+
process.stderr.write('如果当前环境无法扫码,可重新运行 `botmux setup --no-open-platform-auto` 跳过自动配置。\n\n');
|
|
598
|
+
}
|
|
599
|
+
async function renderTerminalQr(payload) {
|
|
600
|
+
return await new Promise((resolve) => qrcode.generate(payload, { small: true }, qr => resolve(qr)));
|
|
601
|
+
}
|
|
602
|
+
function sleep(ms) {
|
|
603
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
604
|
+
}
|
|
605
|
+
function assertFeishuApiOk(payload, message) {
|
|
606
|
+
const record = asRecord(payload);
|
|
607
|
+
if (record.code === 0)
|
|
608
|
+
return;
|
|
609
|
+
const msg = pickString(record, ['message', 'msg']) ?? 'unknown error';
|
|
610
|
+
throw new FeishuWebSessionError(`${message}: ${msg}`, 'login_failed');
|
|
611
|
+
}
|
|
612
|
+
function isFeishuLoginLikeValue(value) {
|
|
613
|
+
const normalized = value.toLowerCase();
|
|
614
|
+
return normalized.includes('/accounts/') || normalized.includes('/login') || normalized.includes('qrlogin');
|
|
615
|
+
}
|
|
616
|
+
function classifyFeishuLoginError(err) {
|
|
617
|
+
if (err instanceof FeishuWebSessionError)
|
|
618
|
+
return err.reason;
|
|
619
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
620
|
+
if (/timeout|timed out|超时/i.test(message))
|
|
621
|
+
return 'timeout';
|
|
622
|
+
if (/expired|过期/i.test(message))
|
|
623
|
+
return 'qr_expired';
|
|
624
|
+
if (/ETIMEDOUT|ECONNREFUSED|ENOTFOUND|ECONNRESET|fetch failed|network/i.test(message))
|
|
625
|
+
return 'network';
|
|
626
|
+
return 'login_failed';
|
|
627
|
+
}
|
|
628
|
+
function collectScopeEntries(value, bucket, out) {
|
|
629
|
+
if (Array.isArray(value)) {
|
|
630
|
+
for (const item of value)
|
|
631
|
+
collectScopeEntries(item, bucket, out);
|
|
632
|
+
return;
|
|
633
|
+
}
|
|
634
|
+
if (!value || typeof value !== 'object')
|
|
635
|
+
return;
|
|
636
|
+
const record = value;
|
|
637
|
+
const name = pickString(record, ['scope_name', 'scopeName', 'name', 'key', 'scopeKey']);
|
|
638
|
+
const id = pickString(record, ['id', 'scope_id', 'scopeId', 'scopeID']);
|
|
639
|
+
if (name && id)
|
|
640
|
+
out.push({ name, id, bucket });
|
|
641
|
+
for (const [key, child] of Object.entries(record)) {
|
|
642
|
+
const nextBucket = /user/i.test(key)
|
|
643
|
+
? 'user'
|
|
644
|
+
: /app|client|tenant/i.test(key)
|
|
645
|
+
? 'tenant'
|
|
646
|
+
: bucket;
|
|
647
|
+
if (child && typeof child === 'object')
|
|
648
|
+
collectScopeEntries(child, nextBucket, out);
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
function mapScopeIds(scopeNames, catalog, bucket) {
|
|
652
|
+
const ids = [];
|
|
653
|
+
const missing = [];
|
|
654
|
+
for (const scopeName of scopeNames) {
|
|
655
|
+
const matched = catalog.find(entry => entry.name === scopeName && entry.bucket === bucket) ??
|
|
656
|
+
catalog.find(entry => entry.name === scopeName && entry.bucket === undefined) ??
|
|
657
|
+
catalog.find(entry => entry.name === scopeName);
|
|
658
|
+
if (matched)
|
|
659
|
+
ids.push(matched.id);
|
|
660
|
+
else
|
|
661
|
+
missing.push(scopeName);
|
|
662
|
+
}
|
|
663
|
+
return { ids: uniqueStrings(ids), missing };
|
|
664
|
+
}
|
|
665
|
+
function nextAppVersion(payload) {
|
|
666
|
+
const data = asRecord(asRecord(payload).data);
|
|
667
|
+
const versions = Array.isArray(data.versions) ? data.versions : [];
|
|
668
|
+
const published = versions
|
|
669
|
+
.map(item => asRecord(item))
|
|
670
|
+
.filter(item => item.versionStatus === 2)
|
|
671
|
+
.map(item => pickString(item, ['appVersion']))
|
|
672
|
+
.filter((version) => Boolean(version));
|
|
673
|
+
if (published.length === 0)
|
|
674
|
+
return '0.0.1';
|
|
675
|
+
const latest = published[0];
|
|
676
|
+
const parts = latest.split('.').map(part => Number.parseInt(part, 10));
|
|
677
|
+
if (parts.length < 3 || parts.some(part => !Number.isFinite(part)))
|
|
678
|
+
return '0.0.1';
|
|
679
|
+
parts[parts.length - 1] += 1;
|
|
680
|
+
return parts.join('.');
|
|
681
|
+
}
|
|
682
|
+
function extractContactRangeMemberIds(payload) {
|
|
683
|
+
const data = asRecord(asRecord(payload).data);
|
|
684
|
+
const detail = asRecord(data.contactRangeDetail);
|
|
685
|
+
const members = Array.isArray(detail.members) ? detail.members : [];
|
|
686
|
+
return uniqueStrings(members
|
|
687
|
+
.map(item => pickString(asRecord(item), ['id']))
|
|
688
|
+
.filter((id) => Boolean(id)));
|
|
689
|
+
}
|
|
690
|
+
function extractVersionId(payload) {
|
|
691
|
+
const direct = pickString(asRecord(payload), ['versionId', 'version_id', 'id']);
|
|
692
|
+
if (direct)
|
|
693
|
+
return direct;
|
|
694
|
+
const data = asRecord(asRecord(payload).data);
|
|
695
|
+
return pickString(data, ['versionId', 'version_id', 'id']) ?? pickString(asRecord(data.appVersion), ['versionId', 'version_id', 'id']);
|
|
696
|
+
}
|
|
697
|
+
function pickString(record, keys) {
|
|
698
|
+
for (const key of keys) {
|
|
699
|
+
const value = record[key];
|
|
700
|
+
if (typeof value === 'string' && value)
|
|
701
|
+
return value;
|
|
702
|
+
if (typeof value === 'number' && Number.isFinite(value))
|
|
703
|
+
return String(value);
|
|
704
|
+
}
|
|
705
|
+
return undefined;
|
|
706
|
+
}
|
|
707
|
+
function asRecord(value) {
|
|
708
|
+
return value && typeof value === 'object' && !Array.isArray(value) ? value : {};
|
|
709
|
+
}
|
|
710
|
+
function uniqueStrings(values) {
|
|
711
|
+
return [...new Set(values.filter(Boolean))];
|
|
712
|
+
}
|
|
713
|
+
function isStoredCookieRecord(value) {
|
|
714
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
715
|
+
return false;
|
|
716
|
+
const cookie = value;
|
|
717
|
+
return typeof cookie.name === 'string'
|
|
718
|
+
&& typeof cookie.value === 'string'
|
|
719
|
+
&& typeof cookie.domain === 'string'
|
|
720
|
+
&& typeof cookie.path === 'string'
|
|
721
|
+
&& typeof cookie.secure === 'boolean'
|
|
722
|
+
&& typeof cookie.httpOnly === 'boolean'
|
|
723
|
+
&& typeof cookie.hostOnly === 'boolean';
|
|
724
|
+
}
|
|
725
|
+
function pruneExpiredCookies(cookies) {
|
|
726
|
+
const now = Date.now();
|
|
727
|
+
return cookies.filter(cookie => cookie.expiresAt === undefined || cookie.expiresAt > now);
|
|
728
|
+
}
|
|
729
|
+
function domainMatches(hostname, cookie) {
|
|
730
|
+
const host = hostname.toLowerCase();
|
|
731
|
+
const domain = cookie.domain.replace(/^\./, '').toLowerCase();
|
|
732
|
+
if (cookie.hostOnly)
|
|
733
|
+
return host === domain;
|
|
734
|
+
return host === domain || host.endsWith(`.${domain}`);
|
|
735
|
+
}
|
|
736
|
+
function pathMatches(requestPath, cookiePath) {
|
|
737
|
+
if (requestPath === cookiePath)
|
|
738
|
+
return true;
|
|
739
|
+
if (!requestPath.startsWith(cookiePath))
|
|
740
|
+
return false;
|
|
741
|
+
return cookiePath.endsWith('/') || requestPath[cookiePath.length] === '/';
|
|
742
|
+
}
|
|
743
|
+
function splitSetCookieHeader(header) {
|
|
744
|
+
if (!header)
|
|
745
|
+
return [];
|
|
746
|
+
const parts = [];
|
|
747
|
+
let start = 0;
|
|
748
|
+
let inExpires = false;
|
|
749
|
+
for (let i = 0; i < header.length; i += 1) {
|
|
750
|
+
const slice = header.slice(Math.max(0, i - 8), i + 1).toLowerCase();
|
|
751
|
+
if (slice.endsWith('expires='))
|
|
752
|
+
inExpires = true;
|
|
753
|
+
if (inExpires && header[i] === ';')
|
|
754
|
+
inExpires = false;
|
|
755
|
+
if (!inExpires && header[i] === ',') {
|
|
756
|
+
parts.push(header.slice(start, i).trim());
|
|
757
|
+
start = i + 1;
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
parts.push(header.slice(start).trim());
|
|
761
|
+
return parts.filter(Boolean);
|
|
762
|
+
}
|
|
763
|
+
function parseSetCookie(responseUrl, header) {
|
|
764
|
+
const url = new URL(responseUrl);
|
|
765
|
+
const parts = header.split(';').map(part => part.trim()).filter(Boolean);
|
|
766
|
+
const first = parts.shift();
|
|
767
|
+
if (!first)
|
|
768
|
+
return null;
|
|
769
|
+
const eq = first.indexOf('=');
|
|
770
|
+
if (eq <= 0)
|
|
771
|
+
return null;
|
|
772
|
+
const cookie = {
|
|
773
|
+
name: first.slice(0, eq),
|
|
774
|
+
value: first.slice(eq + 1),
|
|
775
|
+
domain: url.hostname,
|
|
776
|
+
path: '/',
|
|
777
|
+
secure: false,
|
|
778
|
+
httpOnly: false,
|
|
779
|
+
hostOnly: true,
|
|
780
|
+
};
|
|
781
|
+
for (const part of parts) {
|
|
782
|
+
const partEq = part.indexOf('=');
|
|
783
|
+
const key = (partEq >= 0 ? part.slice(0, partEq) : part).trim().toLowerCase();
|
|
784
|
+
const value = partEq >= 0 ? part.slice(partEq + 1).trim() : '';
|
|
785
|
+
if (key === 'domain' && value) {
|
|
786
|
+
cookie.domain = value.toLowerCase();
|
|
787
|
+
cookie.hostOnly = false;
|
|
788
|
+
}
|
|
789
|
+
else if (key === 'path' && value) {
|
|
790
|
+
cookie.path = value;
|
|
791
|
+
}
|
|
792
|
+
else if (key === 'secure') {
|
|
793
|
+
cookie.secure = true;
|
|
794
|
+
}
|
|
795
|
+
else if (key === 'httponly') {
|
|
796
|
+
cookie.httpOnly = true;
|
|
797
|
+
}
|
|
798
|
+
else if (key === 'expires' && value) {
|
|
799
|
+
const parsed = Date.parse(value);
|
|
800
|
+
if (Number.isFinite(parsed))
|
|
801
|
+
cookie.expiresAt = parsed;
|
|
802
|
+
}
|
|
803
|
+
else if (key === 'max-age' && value) {
|
|
804
|
+
const seconds = Number(value);
|
|
805
|
+
if (Number.isFinite(seconds))
|
|
806
|
+
cookie.expiresAt = Date.now() + seconds * 1000;
|
|
807
|
+
}
|
|
808
|
+
else if (key === 'samesite' && value) {
|
|
809
|
+
cookie.sameSite = value;
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
return cookie;
|
|
813
|
+
}
|
|
814
|
+
function summarizeOpenPlatformPayload(payload) {
|
|
815
|
+
if (!payload || typeof payload !== 'object')
|
|
816
|
+
return String(payload);
|
|
817
|
+
const record = payload;
|
|
818
|
+
const summary = {};
|
|
819
|
+
for (const key of ['code', 'msg', 'message', 'error', 'error_msg']) {
|
|
820
|
+
if (record[key] !== undefined)
|
|
821
|
+
summary[key] = record[key];
|
|
822
|
+
}
|
|
823
|
+
return JSON.stringify(summary).slice(0, 500);
|
|
824
|
+
}
|
|
825
|
+
function safeErrorMessage(err) {
|
|
826
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
827
|
+
return message.replace(/[A-Za-z0-9_=-]{24,}/g, '***');
|
|
828
|
+
}
|
|
829
|
+
function markFinalResponseUrl(response, finalUrl) {
|
|
830
|
+
try {
|
|
831
|
+
Object.defineProperty(response, 'botmuxFinalUrl', {
|
|
832
|
+
value: finalUrl,
|
|
833
|
+
configurable: true,
|
|
834
|
+
});
|
|
835
|
+
}
|
|
836
|
+
catch {
|
|
837
|
+
// Response can be non-extensible in some runtimes; fall back to response.url.
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
function finalResponseUrl(response, fallbackUrl) {
|
|
841
|
+
return typeof response.botmuxFinalUrl === 'string'
|
|
842
|
+
? response.botmuxFinalUrl
|
|
843
|
+
: response.url || fallbackUrl;
|
|
844
|
+
}
|
|
845
|
+
//# sourceMappingURL=open-platform-automation.js.map
|