@vibe-lark/larkpal 0.1.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/larkpal.js +2 -0
- package/dist/cli.mjs +85 -0
- package/dist/interactive-init-CEVq3afY.mjs +292 -0
- package/dist/lark-cli-provider-CdgwmqSz.mjs +562 -0
- package/dist/lark-logger-D7_pEVQc.mjs +143 -0
- package/dist/main.mjs +12112 -0
- package/dist/preview-proxy-KMPQK_j4.mjs +505 -0
- package/package.json +64 -0
|
@@ -0,0 +1,562 @@
|
|
|
1
|
+
import { t as larkLogger } from "./lark-logger-D7_pEVQc.mjs";
|
|
2
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { homedir } from "node:os";
|
|
5
|
+
import { execSync } from "node:child_process";
|
|
6
|
+
import { createHash } from "node:crypto";
|
|
7
|
+
//#region \0rolldown/runtime.js
|
|
8
|
+
var __defProp = Object.defineProperty;
|
|
9
|
+
var __exportAll = (all, no_symbols) => {
|
|
10
|
+
let target = {};
|
|
11
|
+
for (var name in all) __defProp(target, name, {
|
|
12
|
+
get: all[name],
|
|
13
|
+
enumerable: true
|
|
14
|
+
});
|
|
15
|
+
if (!no_symbols) __defProp(target, Symbol.toStringTag, { value: "Module" });
|
|
16
|
+
return target;
|
|
17
|
+
};
|
|
18
|
+
//#endregion
|
|
19
|
+
//#region src/credentials/lark-cli-provider.ts
|
|
20
|
+
/**
|
|
21
|
+
* 飞书应用凭证提供者
|
|
22
|
+
*
|
|
23
|
+
* 凭证来源优先级:
|
|
24
|
+
* 1. 环境变量 LARK_APP_ID / LARK_APP_SECRET(最高优先)
|
|
25
|
+
* 2. LarkPal 自身凭证文件: ~/.larkpal/credentials.json(由 larkpal init 写入)
|
|
26
|
+
* 3. 新版 lark-cli (≥1.0.x) 配置 + keychain
|
|
27
|
+
* 4. 旧版 lark-cli 配置文件: ~/.config/lark/config.json
|
|
28
|
+
*
|
|
29
|
+
* lark-cli 是可插拔依赖——没有 lark-cli 也能通过环境变量启动。
|
|
30
|
+
* 有 lark-cli 时可以额外获取 tenant_access_token。
|
|
31
|
+
*
|
|
32
|
+
* lark-cli 安装方式:
|
|
33
|
+
* npm install -g @larksuite/cli
|
|
34
|
+
* npx skills add larksuite/cli -y -g
|
|
35
|
+
* lark-cli config init
|
|
36
|
+
* lark-cli auth login --recommend
|
|
37
|
+
*/
|
|
38
|
+
var lark_cli_provider_exports = /* @__PURE__ */ __exportAll({ LarkCliCredentialProvider: () => LarkCliCredentialProvider });
|
|
39
|
+
const log = larkLogger("credentials/lark-cli-provider");
|
|
40
|
+
/**
|
|
41
|
+
* 探测 lark-cli 可执行文件路径。
|
|
42
|
+
*/
|
|
43
|
+
function findLarkCli() {
|
|
44
|
+
try {
|
|
45
|
+
const cliPath = execSync("which lark-cli", {
|
|
46
|
+
encoding: "utf-8",
|
|
47
|
+
timeout: 5e3
|
|
48
|
+
}).trim();
|
|
49
|
+
if (!cliPath) return null;
|
|
50
|
+
let version = "unknown";
|
|
51
|
+
try {
|
|
52
|
+
version = execSync(`${cliPath} --version`, {
|
|
53
|
+
encoding: "utf-8",
|
|
54
|
+
timeout: 5e3
|
|
55
|
+
}).trim();
|
|
56
|
+
} catch {}
|
|
57
|
+
return {
|
|
58
|
+
path: cliPath,
|
|
59
|
+
version
|
|
60
|
+
};
|
|
61
|
+
} catch {
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* 通过环境变量 + lark-cli 获取凭证的提供者实现。
|
|
67
|
+
*
|
|
68
|
+
* - 环境变量是主要凭证来源,确保 lark-cli 是可插拔的
|
|
69
|
+
* - lark-cli 提供额外能力(token 缓存、刷新)
|
|
70
|
+
* - lark-cli 版本和路径在启动时记录,方便排查
|
|
71
|
+
*/
|
|
72
|
+
var LarkCliCredentialProvider = class LarkCliCredentialProvider {
|
|
73
|
+
appId;
|
|
74
|
+
appSecret;
|
|
75
|
+
tenantAccessToken;
|
|
76
|
+
userAccessToken;
|
|
77
|
+
/** lark-cli 可执行文件路径,null 表示不可用 */
|
|
78
|
+
larkCliPath;
|
|
79
|
+
/** lark-cli 版本号 */
|
|
80
|
+
larkCliVersion;
|
|
81
|
+
/**
|
|
82
|
+
* 检查凭证是否已配置(不抛出异常)。
|
|
83
|
+
*
|
|
84
|
+
* 检测来源优先级:
|
|
85
|
+
* 1. 环境变量 LARK_APP_ID + LARK_APP_SECRET
|
|
86
|
+
* 2. LarkPal 凭证文件: ~/.larkpal/credentials.json
|
|
87
|
+
* 3. 旧版 lark-cli 配置: ~/.config/lark/config.json
|
|
88
|
+
* 4. 新版 lark-cli 配置: ~/.lark-cli/config.json(apps 数组 + keychain)
|
|
89
|
+
*
|
|
90
|
+
* 用于 CLI 入口在启动服务前判断是否需要先执行初始化流程。
|
|
91
|
+
*/
|
|
92
|
+
static isConfigured() {
|
|
93
|
+
if (process.env.LARK_APP_ID && process.env.LARK_APP_SECRET) return true;
|
|
94
|
+
const larkpalCredsPath = join(homedir(), ".larkpal", "credentials.json");
|
|
95
|
+
if (existsSync(larkpalCredsPath)) try {
|
|
96
|
+
const raw = readFileSync(larkpalCredsPath, "utf-8");
|
|
97
|
+
const creds = JSON.parse(raw);
|
|
98
|
+
if (creds.app_id && creds.app_secret) return true;
|
|
99
|
+
} catch {}
|
|
100
|
+
const legacyConfigPath = join(homedir(), ".config", "lark", "config.json");
|
|
101
|
+
if (existsSync(legacyConfigPath)) try {
|
|
102
|
+
const raw = readFileSync(legacyConfigPath, "utf-8");
|
|
103
|
+
const config = JSON.parse(raw);
|
|
104
|
+
if (config.app_id && (config.app_secret || config.app_secret_in_keyring)) return true;
|
|
105
|
+
} catch {}
|
|
106
|
+
const newConfigPath = join(homedir(), ".lark-cli", "config.json");
|
|
107
|
+
if (existsSync(newConfigPath)) try {
|
|
108
|
+
const raw = readFileSync(newConfigPath, "utf-8");
|
|
109
|
+
if (JSON.parse(raw).apps?.some((app) => app.appId && app.appSecret)) return true;
|
|
110
|
+
} catch {}
|
|
111
|
+
return false;
|
|
112
|
+
}
|
|
113
|
+
constructor() {
|
|
114
|
+
const cli = findLarkCli();
|
|
115
|
+
this.larkCliPath = cli?.path ?? null;
|
|
116
|
+
this.larkCliVersion = cli?.version ?? null;
|
|
117
|
+
if (cli) log.info("lark-cli 已探测到", {
|
|
118
|
+
path: cli.path,
|
|
119
|
+
version: cli.version
|
|
120
|
+
});
|
|
121
|
+
else log.warn("未找到 lark-cli(应由 preflight 检查保证安装)");
|
|
122
|
+
const envAppId = process.env.LARK_APP_ID;
|
|
123
|
+
const envAppSecret = process.env.LARK_APP_SECRET;
|
|
124
|
+
let cliConfig = {};
|
|
125
|
+
const larkpalCreds = this.loadFromLarkPalCredentials();
|
|
126
|
+
if (larkpalCreds?.appId && larkpalCreds?.appSecret) cliConfig = larkpalCreds;
|
|
127
|
+
else if (this.larkCliPath) try {
|
|
128
|
+
cliConfig = this.loadFromCli();
|
|
129
|
+
} catch (err) {
|
|
130
|
+
log.warn("从 lark-cli 读取配置失败,将使用环境变量", { error: err instanceof Error ? err.message : String(err) });
|
|
131
|
+
}
|
|
132
|
+
this.appId = envAppId ?? cliConfig.appId ?? "";
|
|
133
|
+
this.appSecret = envAppSecret ?? cliConfig.appSecret ?? "";
|
|
134
|
+
this.tenantAccessToken = cliConfig.tenant_access_token ?? "";
|
|
135
|
+
this.userAccessToken = cliConfig.user_access_token ?? "";
|
|
136
|
+
if (!this.appId) {
|
|
137
|
+
const msg = [
|
|
138
|
+
"无法获取飞书应用凭证 (app_id)",
|
|
139
|
+
"",
|
|
140
|
+
"请通过以下任一方式提供:",
|
|
141
|
+
" 方式 1: 设置环境变量 LARK_APP_ID + LARK_APP_SECRET",
|
|
142
|
+
" 方式 2: 安装并配置 lark-cli:",
|
|
143
|
+
" npm install -g @larksuite/cli",
|
|
144
|
+
" lark-cli config init"
|
|
145
|
+
].join("\n");
|
|
146
|
+
log.error(msg);
|
|
147
|
+
throw new Error(msg);
|
|
148
|
+
}
|
|
149
|
+
if (!this.appSecret) {
|
|
150
|
+
const msg = [
|
|
151
|
+
"无法获取飞书应用 Secret (app_secret)",
|
|
152
|
+
"",
|
|
153
|
+
"请通过以下任一方式提供:",
|
|
154
|
+
" 方式 1: 设置环境变量 LARK_APP_SECRET",
|
|
155
|
+
" 方式 2: 重新配置 lark-cli: lark-cli config init"
|
|
156
|
+
].join("\n");
|
|
157
|
+
log.error(msg);
|
|
158
|
+
throw new Error(msg);
|
|
159
|
+
}
|
|
160
|
+
log.info("凭证加载成功", {
|
|
161
|
+
appId: this.appId,
|
|
162
|
+
source: envAppId ? "env" : "lark-cli",
|
|
163
|
+
hasTenantToken: !!this.tenantAccessToken,
|
|
164
|
+
hasUserToken: !!this.userAccessToken,
|
|
165
|
+
larkCliAvailable: !!this.larkCliPath,
|
|
166
|
+
larkCliVersion: this.larkCliVersion
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
getAppId() {
|
|
170
|
+
return this.appId;
|
|
171
|
+
}
|
|
172
|
+
getAppSecret() {
|
|
173
|
+
return this.appSecret;
|
|
174
|
+
}
|
|
175
|
+
async getTenantAccessToken() {
|
|
176
|
+
if (!this.tenantAccessToken) log.warn("tenant_access_token 为空,Lark SDK 将自动获取");
|
|
177
|
+
return this.tenantAccessToken;
|
|
178
|
+
}
|
|
179
|
+
async getUserAccessToken() {
|
|
180
|
+
if (!this.userAccessToken) log.warn("user_access_token 为空");
|
|
181
|
+
return this.userAccessToken;
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* 刷新 token。
|
|
185
|
+
* 如果 lark-cli 可用,调用 `lark-cli auth tenant` 刷新。
|
|
186
|
+
* 否则跳过(Lark SDK 会用 appId+appSecret 自动获取)。
|
|
187
|
+
*/
|
|
188
|
+
async refreshTokens() {
|
|
189
|
+
if (!this.larkCliPath) {
|
|
190
|
+
log.info("lark-cli 不可用,跳过 token 刷新(Lark SDK 将自动管理)");
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
log.info("通过 lark-cli 刷新 token");
|
|
194
|
+
try {
|
|
195
|
+
const result = execSync(`${this.larkCliPath} auth tenant --json --no-input`, {
|
|
196
|
+
encoding: "utf-8",
|
|
197
|
+
timeout: 3e4
|
|
198
|
+
});
|
|
199
|
+
log.info("lark-cli auth tenant 执行成功", { output: result.trim().substring(0, 200) });
|
|
200
|
+
} catch (err) {
|
|
201
|
+
log.warn("lark-cli auth tenant 刷新失败", { error: err instanceof Error ? err.message : String(err) });
|
|
202
|
+
}
|
|
203
|
+
try {
|
|
204
|
+
const config = this.loadFromCli();
|
|
205
|
+
this.tenantAccessToken = config.tenant_access_token ?? "";
|
|
206
|
+
this.userAccessToken = config.user_access_token ?? "";
|
|
207
|
+
} catch {}
|
|
208
|
+
log.info("token 刷新完成", {
|
|
209
|
+
hasTenantToken: !!this.tenantAccessToken,
|
|
210
|
+
hasUserToken: !!this.userAccessToken
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
/** 获取 lark-cli 版本号,null 表示不可用 */
|
|
214
|
+
getLarkCliVersion() {
|
|
215
|
+
return this.larkCliVersion;
|
|
216
|
+
}
|
|
217
|
+
/** lark-cli 是否可用 */
|
|
218
|
+
isLarkCliAvailable() {
|
|
219
|
+
return this.larkCliPath !== null;
|
|
220
|
+
}
|
|
221
|
+
/** LarkPal 自身凭证文件路径(由 larkpal init 写入) */
|
|
222
|
+
static LARKPAL_CREDENTIALS_PATH = join(homedir(), ".larkpal", "credentials.json");
|
|
223
|
+
/** 新版 lark-cli (≥1.0.x) 配置路径 */
|
|
224
|
+
static NEW_CONFIG_PATH = join(homedir(), ".lark-cli", "config.json");
|
|
225
|
+
/** 旧版 lark-cli 配置路径 */
|
|
226
|
+
static LEGACY_CONFIG_PATH = join(homedir(), ".config", "lark", "config.json");
|
|
227
|
+
/**
|
|
228
|
+
* 读取 LarkPal 自身凭证文件 ~/.larkpal/credentials.json
|
|
229
|
+
* 由 `larkpal init` 写入,格式:{ "app_id": "...", "app_secret": "..." }
|
|
230
|
+
*/
|
|
231
|
+
loadFromLarkPalCredentials() {
|
|
232
|
+
const credPath = LarkCliCredentialProvider.LARKPAL_CREDENTIALS_PATH;
|
|
233
|
+
if (!existsSync(credPath)) {
|
|
234
|
+
log.info("LarkPal 凭证文件不存在", { path: credPath });
|
|
235
|
+
return null;
|
|
236
|
+
}
|
|
237
|
+
try {
|
|
238
|
+
const raw = readFileSync(credPath, "utf-8");
|
|
239
|
+
const creds = JSON.parse(raw);
|
|
240
|
+
if (creds.app_id && creds.app_secret) {
|
|
241
|
+
log.info("从 LarkPal 凭证文件读取成功", {
|
|
242
|
+
path: credPath,
|
|
243
|
+
appId: creds.app_id
|
|
244
|
+
});
|
|
245
|
+
return {
|
|
246
|
+
appId: creds.app_id,
|
|
247
|
+
appSecret: creds.app_secret
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
log.warn("LarkPal 凭证文件内容不完整", { path: credPath });
|
|
251
|
+
return null;
|
|
252
|
+
} catch (err) {
|
|
253
|
+
log.warn("读取 LarkPal 凭证文件失败", {
|
|
254
|
+
path: credPath,
|
|
255
|
+
error: err instanceof Error ? err.message : String(err)
|
|
256
|
+
});
|
|
257
|
+
return null;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
/**
|
|
261
|
+
* 从 lark-cli 读取配置。
|
|
262
|
+
*
|
|
263
|
+
* 按优先级尝试多种来源:
|
|
264
|
+
* 1. 新版配置文件 ~/.lark-cli/config.json + `lark-cli config show`
|
|
265
|
+
* 2. 旧版配置文件 ~/.config/lark/config.json(直接读取 + keyring)
|
|
266
|
+
* 3. 旧版命令 `lark-cli --json config info`
|
|
267
|
+
*/
|
|
268
|
+
loadFromCli() {
|
|
269
|
+
const showResult = this.loadFromConfigShow();
|
|
270
|
+
const newFileResult = this.loadFromNewConfigFile();
|
|
271
|
+
const appIdFromNew = showResult?.appId || newFileResult?.appId;
|
|
272
|
+
if (appIdFromNew) {
|
|
273
|
+
const legacyResult = this.loadFromLegacyConfigFile();
|
|
274
|
+
if (legacyResult?.appId === appIdFromNew && legacyResult?.appSecret) {
|
|
275
|
+
log.info("从旧版兼容路径获取到 appSecret", { appId: appIdFromNew });
|
|
276
|
+
return {
|
|
277
|
+
...legacyResult,
|
|
278
|
+
appId: appIdFromNew
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
const keychainSecret = this.resolveKeychainSecret(appIdFromNew);
|
|
282
|
+
if (keychainSecret) {
|
|
283
|
+
log.info("从系统 keychain 获取到 appSecret", { appId: appIdFromNew });
|
|
284
|
+
return {
|
|
285
|
+
appId: appIdFromNew,
|
|
286
|
+
appSecret: keychainSecret
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
if (!appIdFromNew) {
|
|
291
|
+
const legacyResult = this.loadFromLegacyConfigFile();
|
|
292
|
+
if (legacyResult?.appId && legacyResult?.appSecret) return legacyResult;
|
|
293
|
+
}
|
|
294
|
+
return this.loadFromLegacyCliCommand();
|
|
295
|
+
}
|
|
296
|
+
/**
|
|
297
|
+
* 通过 `lark-cli config show` 获取配置(新版 lark-cli)。
|
|
298
|
+
*
|
|
299
|
+
* 此命令输出 JSON,包含 appId、appSecret(脱敏)、brand 等。
|
|
300
|
+
* 注意:appSecret 在 show 输出中是 "****",无法获取明文。
|
|
301
|
+
* 但旧版兼容路径 ~/.config/lark/config.json 通常有明文 secret。
|
|
302
|
+
*/
|
|
303
|
+
loadFromConfigShow() {
|
|
304
|
+
if (!this.larkCliPath) return null;
|
|
305
|
+
try {
|
|
306
|
+
log.info("通过 lark-cli config show 读取配置");
|
|
307
|
+
const jsonMatch = execSync(`${this.larkCliPath} config show`, {
|
|
308
|
+
encoding: "utf-8",
|
|
309
|
+
timeout: 1e4,
|
|
310
|
+
env: {
|
|
311
|
+
...process.env,
|
|
312
|
+
NO_COLOR: "1"
|
|
313
|
+
},
|
|
314
|
+
stdio: [
|
|
315
|
+
"pipe",
|
|
316
|
+
"pipe",
|
|
317
|
+
"pipe"
|
|
318
|
+
]
|
|
319
|
+
}).match(/\{[\s\S]*\}/);
|
|
320
|
+
if (!jsonMatch) {
|
|
321
|
+
log.warn("lark-cli config show 输出无法解析为 JSON");
|
|
322
|
+
return null;
|
|
323
|
+
}
|
|
324
|
+
const config = JSON.parse(jsonMatch[0]);
|
|
325
|
+
log.info("lark-cli config show 读取成功", {
|
|
326
|
+
appId: config.appId,
|
|
327
|
+
hasAppSecret: !!config.appSecret && config.appSecret !== "****",
|
|
328
|
+
brand: config.brand
|
|
329
|
+
});
|
|
330
|
+
const result = { appId: config.appId };
|
|
331
|
+
if (config.appSecret && config.appSecret !== "****") result.appSecret = config.appSecret;
|
|
332
|
+
return result;
|
|
333
|
+
} catch (err) {
|
|
334
|
+
log.info("lark-cli config show 失败,尝试其他方式", { error: err instanceof Error ? err.message : String(err) });
|
|
335
|
+
return null;
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
/**
|
|
339
|
+
* 读取新版 lark-cli 配置文件 ~/.lark-cli/config.json
|
|
340
|
+
*
|
|
341
|
+
* 新版配置结构:
|
|
342
|
+
* {
|
|
343
|
+
* "apps": [{
|
|
344
|
+
* "appId": "cli_xxx",
|
|
345
|
+
* "appSecret": { "source": "keychain", "id": "appsecret:cli_xxx" },
|
|
346
|
+
* "brand": "feishu",
|
|
347
|
+
* "lang": "zh"
|
|
348
|
+
* }]
|
|
349
|
+
* }
|
|
350
|
+
*
|
|
351
|
+
* appSecret 通过 keychain 存储,无法直接从此文件获取明文。
|
|
352
|
+
* 但旧版兼容路径通常有明文 secret(keyring file 后端)。
|
|
353
|
+
*/
|
|
354
|
+
loadFromNewConfigFile() {
|
|
355
|
+
const configPath = LarkCliCredentialProvider.NEW_CONFIG_PATH;
|
|
356
|
+
if (!existsSync(configPath)) {
|
|
357
|
+
log.info("新版 lark-cli 配置文件不存在", { path: configPath });
|
|
358
|
+
return null;
|
|
359
|
+
}
|
|
360
|
+
try {
|
|
361
|
+
const raw = readFileSync(configPath, "utf-8");
|
|
362
|
+
const app = JSON.parse(raw).apps?.[0];
|
|
363
|
+
if (!app?.appId) {
|
|
364
|
+
log.info("新版配置文件中无有效 app", { path: configPath });
|
|
365
|
+
return null;
|
|
366
|
+
}
|
|
367
|
+
log.info("读取新版 lark-cli 配置", {
|
|
368
|
+
path: configPath,
|
|
369
|
+
appId: app.appId,
|
|
370
|
+
secretType: typeof app.appSecret === "string" ? "string" : app.appSecret?.source
|
|
371
|
+
});
|
|
372
|
+
const result = { appId: app.appId };
|
|
373
|
+
if (typeof app.appSecret === "string") result.appSecret = app.appSecret;
|
|
374
|
+
return result;
|
|
375
|
+
} catch (err) {
|
|
376
|
+
log.warn("读取新版 lark-cli 配置文件失败", {
|
|
377
|
+
path: configPath,
|
|
378
|
+
error: err instanceof Error ? err.message : String(err)
|
|
379
|
+
});
|
|
380
|
+
return null;
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
/**
|
|
384
|
+
* 读取旧版 lark-cli 配置文件 ~/.config/lark/config.json
|
|
385
|
+
*
|
|
386
|
+
* 配置结构(snake_case):
|
|
387
|
+
* {
|
|
388
|
+
* "app_id": "cli_xxx",
|
|
389
|
+
* "app_secret": "明文(keyring_backend=file 时)",
|
|
390
|
+
* "app_secret_in_keyring": false,
|
|
391
|
+
* "base_url": "https://open.feishu.cn",
|
|
392
|
+
* "tenant_access_token": "t-xxx",
|
|
393
|
+
* "tenant_access_token_expires_at": 1234567890
|
|
394
|
+
* }
|
|
395
|
+
*
|
|
396
|
+
* 注意:新版 lark-cli 使用 keyring file 后端时,也会在此路径存储明文 secret。
|
|
397
|
+
*/
|
|
398
|
+
loadFromLegacyConfigFile() {
|
|
399
|
+
const configPath = LarkCliCredentialProvider.LEGACY_CONFIG_PATH;
|
|
400
|
+
if (!existsSync(configPath)) {
|
|
401
|
+
log.info("旧版 lark-cli 配置文件不存在", { path: configPath });
|
|
402
|
+
return null;
|
|
403
|
+
}
|
|
404
|
+
try {
|
|
405
|
+
const raw = readFileSync(configPath, "utf-8");
|
|
406
|
+
const config = JSON.parse(raw);
|
|
407
|
+
log.info("读取旧版 lark-cli 配置文件", {
|
|
408
|
+
path: configPath,
|
|
409
|
+
appId: config.app_id,
|
|
410
|
+
hasAppSecret: !!config.app_secret,
|
|
411
|
+
secretInKeyring: !!config.app_secret_in_keyring
|
|
412
|
+
});
|
|
413
|
+
const result = { appId: config.app_id };
|
|
414
|
+
if (config.app_secret_in_keyring) {
|
|
415
|
+
const secret = this.resolveKeychainSecret(config.app_id, config.base_url);
|
|
416
|
+
if (secret) {
|
|
417
|
+
result.appSecret = secret;
|
|
418
|
+
log.info("从系统 keyring 获取 appSecret 成功");
|
|
419
|
+
} else log.warn("appSecret 存储在 keyring 中但无法获取,需通过环境变量 LARK_APP_SECRET 提供");
|
|
420
|
+
} else if (config.app_secret) result.appSecret = config.app_secret;
|
|
421
|
+
if (config.tenant_access_token) if ((config.tenant_access_token_expires_at ?? 0) > Date.now() / 1e3) result.tenant_access_token = config.tenant_access_token;
|
|
422
|
+
else log.info("旧版配置缓存的 tenant_access_token 已过期,忽略");
|
|
423
|
+
result.user_access_token = config.user_access_token;
|
|
424
|
+
log.info("旧版配置读取成功", {
|
|
425
|
+
source: "legacy-config-file",
|
|
426
|
+
hasAppId: !!result.appId,
|
|
427
|
+
hasAppSecret: !!result.appSecret,
|
|
428
|
+
hasTenantToken: !!result.tenant_access_token
|
|
429
|
+
});
|
|
430
|
+
return result;
|
|
431
|
+
} catch (err) {
|
|
432
|
+
log.warn("读取旧版 lark-cli 配置文件失败", {
|
|
433
|
+
path: configPath,
|
|
434
|
+
error: err instanceof Error ? err.message : String(err)
|
|
435
|
+
});
|
|
436
|
+
return null;
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
/**
|
|
440
|
+
* 通过旧版 `lark-cli --json config info` 获取配置(fallback 方案)
|
|
441
|
+
*/
|
|
442
|
+
loadFromLegacyCliCommand() {
|
|
443
|
+
if (!this.larkCliPath) return {};
|
|
444
|
+
const cmd = `${this.larkCliPath} --json config info`;
|
|
445
|
+
try {
|
|
446
|
+
log.info("通过旧版 lark-cli 命令读取配置", { cmd });
|
|
447
|
+
const output = execSync(cmd, {
|
|
448
|
+
encoding: "utf-8",
|
|
449
|
+
timeout: 1e4,
|
|
450
|
+
env: {
|
|
451
|
+
...process.env,
|
|
452
|
+
NO_COLOR: "1"
|
|
453
|
+
}
|
|
454
|
+
});
|
|
455
|
+
const config = JSON.parse(output);
|
|
456
|
+
const result = {
|
|
457
|
+
appId: config.app_id,
|
|
458
|
+
appSecret: config.app_secret || void 0,
|
|
459
|
+
tenant_access_token: config.tenant_access_token,
|
|
460
|
+
user_access_token: config.user_access_token
|
|
461
|
+
};
|
|
462
|
+
log.info("旧版 lark-cli 命令读取成功", {
|
|
463
|
+
source: "legacy-cli-command",
|
|
464
|
+
hasAppId: !!result.appId,
|
|
465
|
+
hasAppSecret: !!result.appSecret,
|
|
466
|
+
hasTenantToken: !!result.tenant_access_token
|
|
467
|
+
});
|
|
468
|
+
return result;
|
|
469
|
+
} catch (err) {
|
|
470
|
+
log.info("旧版 lark-cli --json config info 不可用", { error: err instanceof Error ? err.message : String(err) });
|
|
471
|
+
return {};
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
/**
|
|
475
|
+
* 从系统 keyring/keychain 获取 appSecret。
|
|
476
|
+
*
|
|
477
|
+
* 支持两种存储格式:
|
|
478
|
+
*
|
|
479
|
+
* 1. 新版 lark-cli (≥1.0.x) — 使用 zalando/go-keyring:
|
|
480
|
+
* macOS Keychain: service="lark-cli", account="appsecret:{appId}"
|
|
481
|
+
* Linux D-Bus: service="lark-cli", username="{bucketHash}:app-secret"
|
|
482
|
+
*
|
|
483
|
+
* 2. 旧版 lark-cli — 使用 D-Bus Secret Service:
|
|
484
|
+
* service="lark-cli", username="{SHA256(profile\nbaseURL\nappID)}:app-secret"
|
|
485
|
+
*/
|
|
486
|
+
resolveKeychainSecret(appId, baseUrl) {
|
|
487
|
+
if (!appId) return null;
|
|
488
|
+
log.info("尝试从系统 keyring/keychain 获取 appSecret", { appId });
|
|
489
|
+
try {
|
|
490
|
+
const secret = execSync(`security find-generic-password -s "lark-cli" -a "appsecret:${appId}" -w 2>/dev/null`, {
|
|
491
|
+
encoding: "utf-8",
|
|
492
|
+
timeout: 5e3,
|
|
493
|
+
shell: "/bin/sh",
|
|
494
|
+
stdio: [
|
|
495
|
+
"pipe",
|
|
496
|
+
"pipe",
|
|
497
|
+
"pipe"
|
|
498
|
+
]
|
|
499
|
+
}).trim();
|
|
500
|
+
if (secret) {
|
|
501
|
+
log.info("从 macOS Keychain 获取 appSecret 成功(新版格式)");
|
|
502
|
+
return secret;
|
|
503
|
+
}
|
|
504
|
+
} catch {}
|
|
505
|
+
const seed = [
|
|
506
|
+
"default",
|
|
507
|
+
(baseUrl || "https://open.feishu.cn").toLowerCase().replace(/\/+$/, "").replace(/\/open-apis$/, ""),
|
|
508
|
+
appId.trim()
|
|
509
|
+
].join("\n");
|
|
510
|
+
const username = `${createHash("sha256").update(seed).digest("hex")}:app-secret`;
|
|
511
|
+
try {
|
|
512
|
+
const secret = execSync(`security find-generic-password -s "lark-cli" -a "${username}" -w 2>/dev/null`, {
|
|
513
|
+
encoding: "utf-8",
|
|
514
|
+
timeout: 5e3,
|
|
515
|
+
shell: "/bin/sh",
|
|
516
|
+
stdio: [
|
|
517
|
+
"pipe",
|
|
518
|
+
"pipe",
|
|
519
|
+
"pipe"
|
|
520
|
+
]
|
|
521
|
+
}).trim();
|
|
522
|
+
if (secret) {
|
|
523
|
+
log.info("从 macOS Keychain 获取 appSecret 成功(旧版格式)");
|
|
524
|
+
return secret;
|
|
525
|
+
}
|
|
526
|
+
} catch {}
|
|
527
|
+
try {
|
|
528
|
+
const secret = execSync(`secret-tool lookup service "lark-cli" username "${username}"`, {
|
|
529
|
+
encoding: "utf-8",
|
|
530
|
+
timeout: 5e3,
|
|
531
|
+
stdio: [
|
|
532
|
+
"pipe",
|
|
533
|
+
"pipe",
|
|
534
|
+
"pipe"
|
|
535
|
+
]
|
|
536
|
+
}).trim();
|
|
537
|
+
if (secret) {
|
|
538
|
+
log.info("从 Linux D-Bus Secret Service 获取 appSecret 成功");
|
|
539
|
+
return secret;
|
|
540
|
+
}
|
|
541
|
+
} catch {}
|
|
542
|
+
try {
|
|
543
|
+
const secret = execSync(`secret-tool lookup service "lark-cli" username "appsecret:${appId}"`, {
|
|
544
|
+
encoding: "utf-8",
|
|
545
|
+
timeout: 5e3,
|
|
546
|
+
stdio: [
|
|
547
|
+
"pipe",
|
|
548
|
+
"pipe",
|
|
549
|
+
"pipe"
|
|
550
|
+
]
|
|
551
|
+
}).trim();
|
|
552
|
+
if (secret) {
|
|
553
|
+
log.info("从 Linux D-Bus Secret Service 获取 appSecret 成功(新版格式)");
|
|
554
|
+
return secret;
|
|
555
|
+
}
|
|
556
|
+
} catch {}
|
|
557
|
+
log.warn("系统 keyring/keychain 获取 appSecret 失败");
|
|
558
|
+
return null;
|
|
559
|
+
}
|
|
560
|
+
};
|
|
561
|
+
//#endregion
|
|
562
|
+
export { lark_cli_provider_exports as n, LarkCliCredentialProvider as t };
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
2
|
+
//#region src/core/lark-ticket.ts
|
|
3
|
+
/**
|
|
4
|
+
* Copyright (c) 2026 ByteDance Ltd. and/or its affiliates
|
|
5
|
+
* SPDX-License-Identifier: MIT
|
|
6
|
+
*
|
|
7
|
+
* Request-level ticket for the Feishu plugin.
|
|
8
|
+
*
|
|
9
|
+
* Uses Node.js AsyncLocalStorage to propagate a ticket (message_id,
|
|
10
|
+
* chat_id, account_id) through the entire async call chain without passing
|
|
11
|
+
* parameters explicitly. Call {@link withTicket} at the event entry point
|
|
12
|
+
* (monitor.ts) and use {@link getTicket} anywhere downstream.
|
|
13
|
+
*/
|
|
14
|
+
const store = new AsyncLocalStorage();
|
|
15
|
+
/** Return the current ticket, or `undefined` if not inside withTicket. */
|
|
16
|
+
function getTicket() {
|
|
17
|
+
return store.getStore();
|
|
18
|
+
}
|
|
19
|
+
//#endregion
|
|
20
|
+
//#region src/core/runtime-store.ts
|
|
21
|
+
const RUNTIME_NOT_INITIALIZED_ERROR = "Feishu plugin runtime has not been initialised. Ensure LarkClient.setRuntime() is called during plugin activation.";
|
|
22
|
+
let runtime = null;
|
|
23
|
+
function setLarkRuntime(nextRuntime) {
|
|
24
|
+
runtime = nextRuntime;
|
|
25
|
+
}
|
|
26
|
+
function tryGetLarkRuntime() {
|
|
27
|
+
return runtime;
|
|
28
|
+
}
|
|
29
|
+
function getLarkRuntime() {
|
|
30
|
+
if (!runtime) throw new Error(RUNTIME_NOT_INITIALIZED_ERROR);
|
|
31
|
+
return runtime;
|
|
32
|
+
}
|
|
33
|
+
//#endregion
|
|
34
|
+
//#region src/core/lark-logger.ts
|
|
35
|
+
const CYAN = "\x1B[36m";
|
|
36
|
+
const YELLOW = "\x1B[33m";
|
|
37
|
+
const RED = "\x1B[31m";
|
|
38
|
+
const GRAY = "\x1B[90m";
|
|
39
|
+
const RESET = "\x1B[0m";
|
|
40
|
+
function consoleFallback(subsystem) {
|
|
41
|
+
const tag = `feishu/${subsystem}`;
|
|
42
|
+
return {
|
|
43
|
+
debug: (msg, meta) => console.debug(`${GRAY}[${tag}]${RESET}`, msg, ...meta ? [meta] : []),
|
|
44
|
+
info: (msg, meta) => console.log(`${CYAN}[${tag}]${RESET}`, msg, ...meta ? [meta] : []),
|
|
45
|
+
warn: (msg, meta) => console.warn(`${YELLOW}[${tag}]${RESET}`, msg, ...meta ? [meta] : []),
|
|
46
|
+
error: (msg, meta) => console.error(`${RED}[${tag}]${RESET}`, msg, ...meta ? [meta] : [])
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
function resolveRuntimeLogger(subsystem) {
|
|
50
|
+
try {
|
|
51
|
+
const runtime = tryGetLarkRuntime();
|
|
52
|
+
if (!runtime) return null;
|
|
53
|
+
return runtime.logging.getChildLogger({ subsystem: `feishu/${subsystem}` });
|
|
54
|
+
} catch {
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
function getTraceMeta() {
|
|
59
|
+
const ctx = getTicket();
|
|
60
|
+
if (!ctx) return null;
|
|
61
|
+
const trace = {
|
|
62
|
+
accountId: ctx.accountId,
|
|
63
|
+
messageId: ctx.messageId,
|
|
64
|
+
chatId: ctx.chatId
|
|
65
|
+
};
|
|
66
|
+
if (ctx.senderOpenId) trace.senderOpenId = ctx.senderOpenId;
|
|
67
|
+
return trace;
|
|
68
|
+
}
|
|
69
|
+
function enrichMeta(meta) {
|
|
70
|
+
const trace = getTraceMeta();
|
|
71
|
+
if (!trace) return meta ?? {};
|
|
72
|
+
return meta ? {
|
|
73
|
+
...trace,
|
|
74
|
+
...meta
|
|
75
|
+
} : trace;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Build a trace-aware prefix like `feishu[default][msg:om_xxx]:`.
|
|
79
|
+
*
|
|
80
|
+
* Mirrors the format used by `trace.ts` so log lines are consistent
|
|
81
|
+
* across the old and new logging systems.
|
|
82
|
+
*/
|
|
83
|
+
function buildTracePrefix() {
|
|
84
|
+
const ctx = getTicket();
|
|
85
|
+
if (!ctx) return "feishu:";
|
|
86
|
+
return `feishu[${ctx.accountId}][msg:${ctx.messageId}]:`;
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Format message with inline meta for text-based log output.
|
|
90
|
+
*
|
|
91
|
+
* RuntimeLogger implementations typically ignore the `meta` parameter in
|
|
92
|
+
* their text output (gateway.log / console). To ensure meta is always
|
|
93
|
+
* visible, we serialize user-supplied meta into the message string and
|
|
94
|
+
* prepend the trace context prefix (accountId + messageId).
|
|
95
|
+
*
|
|
96
|
+
* Example:
|
|
97
|
+
* formatMessage("card.create response", { code: 0, cardId: "c_xxx" })
|
|
98
|
+
* → "feishu[default][msg:om_xxx]: card.create response (code=0, cardId=c_xxx)"
|
|
99
|
+
*/
|
|
100
|
+
function formatMessage(message, meta) {
|
|
101
|
+
const prefix = buildTracePrefix();
|
|
102
|
+
if (!meta || Object.keys(meta).length === 0) return `${prefix} ${message}`;
|
|
103
|
+
const parts = Object.entries(meta).map(([k, v]) => {
|
|
104
|
+
if (v === void 0 || v == null) return null;
|
|
105
|
+
if (typeof v === "object") return `${k}=${JSON.stringify(v)}`;
|
|
106
|
+
return `${k}=${v}`;
|
|
107
|
+
}).filter(Boolean);
|
|
108
|
+
return parts.length > 0 ? `${prefix} ${message} (${parts.join(", ")})` : `${prefix} ${message}`;
|
|
109
|
+
}
|
|
110
|
+
function createLarkLogger(subsystem) {
|
|
111
|
+
let cachedLogger = null;
|
|
112
|
+
let resolved = false;
|
|
113
|
+
function getLogger() {
|
|
114
|
+
if (!resolved) {
|
|
115
|
+
cachedLogger = resolveRuntimeLogger(subsystem);
|
|
116
|
+
if (cachedLogger) resolved = true;
|
|
117
|
+
}
|
|
118
|
+
return cachedLogger ?? consoleFallback(subsystem);
|
|
119
|
+
}
|
|
120
|
+
return {
|
|
121
|
+
subsystem,
|
|
122
|
+
debug(message, meta) {
|
|
123
|
+
getLogger().debug?.(formatMessage(message, meta), enrichMeta(meta));
|
|
124
|
+
},
|
|
125
|
+
info(message, meta) {
|
|
126
|
+
getLogger().info(formatMessage(message, meta), enrichMeta(meta));
|
|
127
|
+
},
|
|
128
|
+
warn(message, meta) {
|
|
129
|
+
getLogger().warn(formatMessage(message, meta), enrichMeta(meta));
|
|
130
|
+
},
|
|
131
|
+
error(message, meta) {
|
|
132
|
+
getLogger().error(formatMessage(message, meta), enrichMeta(meta));
|
|
133
|
+
},
|
|
134
|
+
child(name) {
|
|
135
|
+
return createLarkLogger(`${subsystem}/${name}`);
|
|
136
|
+
}
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
function larkLogger(subsystem) {
|
|
140
|
+
return createLarkLogger(subsystem);
|
|
141
|
+
}
|
|
142
|
+
//#endregion
|
|
143
|
+
export { getLarkRuntime as n, setLarkRuntime as r, larkLogger as t };
|