@vibe-lark/larkpal 0.1.8 → 0.1.9
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/dist/main.mjs +345 -21
- package/package.json +1 -1
package/dist/main.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { t as LarkCliCredentialProvider } from "./lark-cli-provider-CdgwmqSz.mjs";
|
|
2
2
|
import { n as getLarkRuntime, r as setLarkRuntime, t as larkLogger } from "./lark-logger-D7_pEVQc.mjs";
|
|
3
3
|
import * as fs from "node:fs";
|
|
4
|
-
import { existsSync, readFileSync } from "node:fs";
|
|
4
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
5
5
|
import * as path$1 from "node:path";
|
|
6
6
|
import { basename, dirname, join } from "node:path";
|
|
7
7
|
import { fileURLToPath } from "node:url";
|
|
@@ -36,7 +36,7 @@ import { Readable } from "node:stream";
|
|
|
36
36
|
*
|
|
37
37
|
* 本模块在启动时检测它们是否已安装,缺失时给出明确的安装指引。
|
|
38
38
|
*/
|
|
39
|
-
const log$
|
|
39
|
+
const log$30 = larkLogger("preflight");
|
|
40
40
|
function detectCli(name) {
|
|
41
41
|
try {
|
|
42
42
|
const cliPath = execSync(`which ${name}`, {
|
|
@@ -74,12 +74,12 @@ function detectCli(name) {
|
|
|
74
74
|
* 缺失任一依赖时打印安装指引并抛出错误,阻止启动。
|
|
75
75
|
*/
|
|
76
76
|
function preflight() {
|
|
77
|
-
log$
|
|
77
|
+
log$30.info("开始前置环境检查...");
|
|
78
78
|
const claude = detectCli("claude");
|
|
79
79
|
const larkCli = detectCli("lark-cli");
|
|
80
80
|
const status = (dep) => dep.installed ? `✅ ${dep.name} (${dep.version ?? "unknown"}) → ${dep.path}` : `❌ ${dep.name} 未安装`;
|
|
81
|
-
log$
|
|
82
|
-
log$
|
|
81
|
+
log$30.info(` ${status(claude)}`);
|
|
82
|
+
log$30.info(` ${status(larkCli)}`);
|
|
83
83
|
const missing = [];
|
|
84
84
|
if (!claude.installed) missing.push([
|
|
85
85
|
"❌ 未找到 Claude Code (claude)",
|
|
@@ -109,10 +109,10 @@ function preflight() {
|
|
|
109
109
|
"",
|
|
110
110
|
"请安装缺失的依赖后重新启动 LarkPal。"
|
|
111
111
|
].join("\n");
|
|
112
|
-
log$
|
|
112
|
+
log$30.error(msg);
|
|
113
113
|
throw new Error("前置环境检查未通过,缺少必要依赖");
|
|
114
114
|
}
|
|
115
|
-
log$
|
|
115
|
+
log$30.info("前置环境检查通过 ✓");
|
|
116
116
|
return {
|
|
117
117
|
claude,
|
|
118
118
|
larkCli,
|
|
@@ -120,6 +120,88 @@ function preflight() {
|
|
|
120
120
|
};
|
|
121
121
|
}
|
|
122
122
|
//#endregion
|
|
123
|
+
//#region src/config/ensure-lark-cli-config.ts
|
|
124
|
+
/**
|
|
125
|
+
* 确保 lark-cli 配置文件存在(同步环境变量凭证 → lark-cli 配置文件)
|
|
126
|
+
*
|
|
127
|
+
* 背景:
|
|
128
|
+
* CC 运行时会调用 `lark-cli config show` 来检测 lark-cli 是否已初始化。
|
|
129
|
+
* 在服务端部署中,凭证通过环境变量注入,lark-cli 配置文件不存在,
|
|
130
|
+
* 导致 CC 认为 lark-cli "未配置",影响 CC 内部对 lark-cli 功能的调用。
|
|
131
|
+
*
|
|
132
|
+
* 策略:
|
|
133
|
+
* 如果环境变量 LARK_APP_ID + LARK_APP_SECRET 已设置,且 lark-cli 配置文件
|
|
134
|
+
* 不存在,则主动生成配置文件,使 lark-cli 命令行也能识别为已配置状态。
|
|
135
|
+
*
|
|
136
|
+
* 同时生成新版 (~/.lark-cli/config.json) 和旧版 (~/.config/lark/config.json)
|
|
137
|
+
* 格式的配置文件,保证 lark-cli 无论通过哪种路径读取都能正常工作。
|
|
138
|
+
*/
|
|
139
|
+
const log$29 = larkLogger("config/ensure-lark-cli");
|
|
140
|
+
/** 新版 lark-cli 配置路径 */
|
|
141
|
+
const NEW_CONFIG_DIR = join(homedir(), ".lark-cli");
|
|
142
|
+
const NEW_CONFIG_PATH = join(NEW_CONFIG_DIR, "config.json");
|
|
143
|
+
/** 旧版 lark-cli 配置路径(lark-cli file backend 兼容路径) */
|
|
144
|
+
const LEGACY_CONFIG_DIR = join(homedir(), ".config", "lark");
|
|
145
|
+
const LEGACY_CONFIG_PATH = join(LEGACY_CONFIG_DIR, "config.json");
|
|
146
|
+
/**
|
|
147
|
+
* 确保 lark-cli 配置文件存在。
|
|
148
|
+
*
|
|
149
|
+
* 当环境变量提供了凭证但 lark-cli 配置文件缺失时,主动写入配置文件,
|
|
150
|
+
* 使 `lark-cli config show` 能正确返回已配置状态。
|
|
151
|
+
*/
|
|
152
|
+
function ensureLarkCliConfig() {
|
|
153
|
+
const appId = process.env.LARK_APP_ID;
|
|
154
|
+
const appSecret = process.env.LARK_APP_SECRET;
|
|
155
|
+
if (!appId || !appSecret) {
|
|
156
|
+
log$29.info("环境变量凭证未设置,跳过 lark-cli 配置同步");
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
const hasNewConfig = existsSync(NEW_CONFIG_PATH);
|
|
160
|
+
const hasLegacyConfig = existsSync(LEGACY_CONFIG_PATH);
|
|
161
|
+
if (hasNewConfig && hasLegacyConfig) {
|
|
162
|
+
log$29.info("lark-cli 配置文件已存在,跳过同步", {
|
|
163
|
+
newConfig: NEW_CONFIG_PATH,
|
|
164
|
+
legacyConfig: LEGACY_CONFIG_PATH
|
|
165
|
+
});
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
log$29.info("检测到环境变量凭证但 lark-cli 配置文件缺失,开始同步", {
|
|
169
|
+
appId,
|
|
170
|
+
hasNewConfig,
|
|
171
|
+
hasLegacyConfig
|
|
172
|
+
});
|
|
173
|
+
if (!hasNewConfig) try {
|
|
174
|
+
mkdirSync(NEW_CONFIG_DIR, { recursive: true });
|
|
175
|
+
writeFileSync(NEW_CONFIG_PATH, JSON.stringify({ apps: [{
|
|
176
|
+
appId,
|
|
177
|
+
appSecret,
|
|
178
|
+
brand: "feishu",
|
|
179
|
+
lang: "zh"
|
|
180
|
+
}] }, null, 2), "utf-8");
|
|
181
|
+
log$29.info("已生成新版 lark-cli 配置文件", { path: NEW_CONFIG_PATH });
|
|
182
|
+
} catch (err) {
|
|
183
|
+
log$29.warn("生成新版 lark-cli 配置文件失败", {
|
|
184
|
+
path: NEW_CONFIG_PATH,
|
|
185
|
+
error: err instanceof Error ? err.message : String(err)
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
if (!hasLegacyConfig) try {
|
|
189
|
+
mkdirSync(LEGACY_CONFIG_DIR, { recursive: true });
|
|
190
|
+
writeFileSync(LEGACY_CONFIG_PATH, JSON.stringify({
|
|
191
|
+
app_id: appId,
|
|
192
|
+
app_secret: appSecret,
|
|
193
|
+
app_secret_in_keyring: false,
|
|
194
|
+
base_url: "https://open.feishu.cn"
|
|
195
|
+
}, null, 2), "utf-8");
|
|
196
|
+
log$29.info("已生成旧版 lark-cli 配置文件", { path: LEGACY_CONFIG_PATH });
|
|
197
|
+
} catch (err) {
|
|
198
|
+
log$29.warn("生成旧版 lark-cli 配置文件失败", {
|
|
199
|
+
path: LEGACY_CONFIG_PATH,
|
|
200
|
+
error: err instanceof Error ? err.message : String(err)
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
//#endregion
|
|
123
205
|
//#region src/config/defaults.ts
|
|
124
206
|
/**
|
|
125
207
|
* 默认配置生成器
|
|
@@ -861,7 +943,7 @@ var SessionProcessManager = class {
|
|
|
861
943
|
async startPersistentProcess(config, opts) {
|
|
862
944
|
const { sessionId, cwd, prompt } = config;
|
|
863
945
|
const claudeSessionId = v5(sessionId, LARKPAL_SESSION_NAMESPACE);
|
|
864
|
-
|
|
946
|
+
let isResuming = opts?._forceResume || this.ccSessionExists(cwd, claudeSessionId);
|
|
865
947
|
const args = [
|
|
866
948
|
"-p",
|
|
867
949
|
"--input-format",
|
|
@@ -952,6 +1034,7 @@ var SessionProcessManager = class {
|
|
|
952
1034
|
this.sessionCwdCache.set(sessionId, cwd);
|
|
953
1035
|
const parser = new CCStreamParser();
|
|
954
1036
|
this.bindParserToCallbackProxy(parser, sessionId);
|
|
1037
|
+
let stderrSessionInUse = false;
|
|
955
1038
|
if (child.stdout) createInterface({ input: child.stdout }).on("line", (line) => {
|
|
956
1039
|
ccProcess.lastActiveAt = /* @__PURE__ */ new Date();
|
|
957
1040
|
this.resetIdleTimer(sessionId);
|
|
@@ -978,14 +1061,35 @@ var SessionProcessManager = class {
|
|
|
978
1061
|
});
|
|
979
1062
|
if (child.stderr) createInterface({ input: child.stderr }).on("line", (line) => {
|
|
980
1063
|
if (line.includes("no stdin data received")) return;
|
|
1064
|
+
if (line.toLowerCase().includes("already in use")) stderrSessionInUse = true;
|
|
981
1065
|
log$25.warn("CC 进程 stderr", {
|
|
982
1066
|
sessionId,
|
|
983
1067
|
line: line.slice(0, 500)
|
|
984
1068
|
});
|
|
985
1069
|
});
|
|
986
|
-
child.on("close", (code, signal) => {
|
|
1070
|
+
child.on("close", async (code, signal) => {
|
|
987
1071
|
const exitCode = code ?? -1;
|
|
988
1072
|
const exitSignal = signal ?? "none";
|
|
1073
|
+
if (exitCode === 1 && stderrSessionInUse && !isResuming) {
|
|
1074
|
+
log$25.info("Session ID 已被占用,准备使用 --resume 模式重试", {
|
|
1075
|
+
sessionId,
|
|
1076
|
+
claudeSessionId
|
|
1077
|
+
});
|
|
1078
|
+
this.processes.delete(sessionId);
|
|
1079
|
+
this.sessionCwdCache.delete(sessionId);
|
|
1080
|
+
try {
|
|
1081
|
+
await this.startPersistentProcess(config, {
|
|
1082
|
+
skipInitialMessage: opts?.skipInitialMessage,
|
|
1083
|
+
_forceResume: true
|
|
1084
|
+
});
|
|
1085
|
+
return;
|
|
1086
|
+
} catch (retryErr) {
|
|
1087
|
+
log$25.error("Session ID 重试失败", {
|
|
1088
|
+
sessionId,
|
|
1089
|
+
error: retryErr instanceof Error ? retryErr.message : String(retryErr)
|
|
1090
|
+
});
|
|
1091
|
+
}
|
|
1092
|
+
}
|
|
989
1093
|
if (exitCode === 0) {
|
|
990
1094
|
log$25.info("CC 常驻进程正常退出", {
|
|
991
1095
|
sessionId,
|
|
@@ -4522,6 +4626,8 @@ const LARK_ERROR = {
|
|
|
4522
4626
|
APP_SCOPE_MISSING: 99991672,
|
|
4523
4627
|
/** 用户 token scope 不足 */
|
|
4524
4628
|
USER_SCOPE_INSUFFICIENT: 99991679,
|
|
4629
|
+
/** 无用户数据权限(如 contact:user.base:readonly 未开通) */
|
|
4630
|
+
NO_USER_AUTHORITY: 41050,
|
|
4525
4631
|
/** access_token 无效 */
|
|
4526
4632
|
TOKEN_INVALID: 99991668,
|
|
4527
4633
|
/** access_token 已过期 */
|
|
@@ -8375,6 +8481,127 @@ async function dispatchToCC(params) {
|
|
|
8375
8481
|
});
|
|
8376
8482
|
}
|
|
8377
8483
|
}
|
|
8484
|
+
const attachmentResources = ctx.resources.filter((r) => r.type === "file" || r.type === "audio" || r.type === "video" || r.type === "sticker");
|
|
8485
|
+
if (attachmentResources.length > 0) {
|
|
8486
|
+
const filesDir = path.join(route.cwd, "files");
|
|
8487
|
+
const { writeFile: fsWriteFile, mkdir: fsMkdir } = await import("fs/promises");
|
|
8488
|
+
await fsMkdir(filesDir, { recursive: true });
|
|
8489
|
+
let updatedTextPrompt = typeof prompt === "string" ? prompt : prompt[0].text;
|
|
8490
|
+
for (const res of attachmentResources) try {
|
|
8491
|
+
log$11.info("开始下载非图片附件", {
|
|
8492
|
+
type: res.type,
|
|
8493
|
+
fileKey: res.fileKey,
|
|
8494
|
+
fileName: res.fileName,
|
|
8495
|
+
duration: res.duration,
|
|
8496
|
+
messageId: ctx.messageId
|
|
8497
|
+
});
|
|
8498
|
+
const response = await LarkClient.fromAccount(account).sdk.im.messageResource.get({
|
|
8499
|
+
path: {
|
|
8500
|
+
message_id: ctx.messageId,
|
|
8501
|
+
file_key: res.fileKey
|
|
8502
|
+
},
|
|
8503
|
+
params: { type: "file" }
|
|
8504
|
+
});
|
|
8505
|
+
log$11.info("非图片附件 API 响应已收到", {
|
|
8506
|
+
type: res.type,
|
|
8507
|
+
fileKey: res.fileKey,
|
|
8508
|
+
responseType: typeof response,
|
|
8509
|
+
hasData: response != null
|
|
8510
|
+
});
|
|
8511
|
+
let buffer;
|
|
8512
|
+
if (response instanceof ArrayBuffer) buffer = Buffer.from(response);
|
|
8513
|
+
else if (Buffer.isBuffer(response)) buffer = response;
|
|
8514
|
+
else if (response && typeof response === "object") {
|
|
8515
|
+
const resp = response;
|
|
8516
|
+
if (resp.data instanceof ArrayBuffer) buffer = Buffer.from(resp.data);
|
|
8517
|
+
else if (Buffer.isBuffer(resp.data)) buffer = resp.data;
|
|
8518
|
+
else if (resp.writeFile) {
|
|
8519
|
+
const chunks = [];
|
|
8520
|
+
const readable = resp.getReadableStream();
|
|
8521
|
+
for await (const chunk of readable) chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
8522
|
+
buffer = Buffer.concat(chunks);
|
|
8523
|
+
} else {
|
|
8524
|
+
log$11.warn("非图片附件下载返回未知格式,跳过", {
|
|
8525
|
+
type: res.type,
|
|
8526
|
+
fileKey: res.fileKey,
|
|
8527
|
+
responseType: typeof response
|
|
8528
|
+
});
|
|
8529
|
+
continue;
|
|
8530
|
+
}
|
|
8531
|
+
} else {
|
|
8532
|
+
log$11.warn("非图片附件下载返回空,跳过", {
|
|
8533
|
+
type: res.type,
|
|
8534
|
+
fileKey: res.fileKey
|
|
8535
|
+
});
|
|
8536
|
+
continue;
|
|
8537
|
+
}
|
|
8538
|
+
let savedFileName = "";
|
|
8539
|
+
switch (res.type) {
|
|
8540
|
+
case "file":
|
|
8541
|
+
savedFileName = res.fileName ? `${res.fileKey}_${res.fileName}` : `${res.fileKey}_file`;
|
|
8542
|
+
break;
|
|
8543
|
+
case "audio":
|
|
8544
|
+
savedFileName = `${res.fileKey}.ogg`;
|
|
8545
|
+
break;
|
|
8546
|
+
case "video":
|
|
8547
|
+
savedFileName = res.fileName ? `${res.fileKey}_${res.fileName}` : `${res.fileKey}_video`;
|
|
8548
|
+
break;
|
|
8549
|
+
case "sticker":
|
|
8550
|
+
savedFileName = `${res.fileKey}.png`;
|
|
8551
|
+
break;
|
|
8552
|
+
}
|
|
8553
|
+
const filePath = path.join(filesDir, savedFileName);
|
|
8554
|
+
await fsWriteFile(filePath, buffer);
|
|
8555
|
+
log$11.info("非图片附件已保存到工作目录", {
|
|
8556
|
+
type: res.type,
|
|
8557
|
+
fileKey: res.fileKey,
|
|
8558
|
+
savedFileName,
|
|
8559
|
+
path: filePath,
|
|
8560
|
+
sizeBytes: buffer.length
|
|
8561
|
+
});
|
|
8562
|
+
switch (res.type) {
|
|
8563
|
+
case "file": {
|
|
8564
|
+
const nameDisplay = res.fileName || savedFileName;
|
|
8565
|
+
const fileRegex = new RegExp(`<file\\s+key="${res.fileKey}"[^/]*/>`, "g");
|
|
8566
|
+
updatedTextPrompt = updatedTextPrompt.replace(fileRegex, `[文件: ${nameDisplay}] (已下载到 files/${savedFileName})`);
|
|
8567
|
+
break;
|
|
8568
|
+
}
|
|
8569
|
+
case "audio": {
|
|
8570
|
+
const audioRegex = new RegExp(`<audio\\s+key="${res.fileKey}"[^/]*/>`, "g");
|
|
8571
|
+
const durationStr = res.duration != null ? String(res.duration) : "未知";
|
|
8572
|
+
updatedTextPrompt = updatedTextPrompt.replace(audioRegex, `[语音: files/${savedFileName}] (时长: ${durationStr})`);
|
|
8573
|
+
break;
|
|
8574
|
+
}
|
|
8575
|
+
case "video": {
|
|
8576
|
+
const videoRegex = new RegExp(`<video\\s+key="${res.fileKey}"[^/]*/>`, "g");
|
|
8577
|
+
const videoName = res.fileName || "未知";
|
|
8578
|
+
const videoDuration = res.duration != null ? String(res.duration) : "未知";
|
|
8579
|
+
updatedTextPrompt = updatedTextPrompt.replace(videoRegex, `[视频: files/${savedFileName}] (文件名: ${videoName}, 时长: ${videoDuration})`);
|
|
8580
|
+
break;
|
|
8581
|
+
}
|
|
8582
|
+
case "sticker": {
|
|
8583
|
+
const stickerRegex = new RegExp(`<sticker\\s+key="${res.fileKey}"[^/]*/>`, "g");
|
|
8584
|
+
updatedTextPrompt = updatedTextPrompt.replace(stickerRegex, `[表情贴纸: files/${savedFileName}]`);
|
|
8585
|
+
break;
|
|
8586
|
+
}
|
|
8587
|
+
}
|
|
8588
|
+
} catch (err) {
|
|
8589
|
+
log$11.warn("非图片附件下载失败,保留原始占位符", {
|
|
8590
|
+
type: res.type,
|
|
8591
|
+
fileKey: res.fileKey,
|
|
8592
|
+
error: err instanceof Error ? err.message : String(err)
|
|
8593
|
+
});
|
|
8594
|
+
}
|
|
8595
|
+
if (typeof prompt === "string") prompt = updatedTextPrompt;
|
|
8596
|
+
else {
|
|
8597
|
+
const textBlock = prompt.find((b) => b.type === "text");
|
|
8598
|
+
if (textBlock) textBlock.text = updatedTextPrompt;
|
|
8599
|
+
}
|
|
8600
|
+
log$11.info("非图片附件处理完成", {
|
|
8601
|
+
totalAttachments: attachmentResources.length,
|
|
8602
|
+
promptLength: typeof prompt === "string" ? prompt.length : prompt.find((b) => b.type === "text")?.text?.length
|
|
8603
|
+
});
|
|
8604
|
+
}
|
|
8378
8605
|
log$11.info("消息格式化完成", {
|
|
8379
8606
|
promptLength: typeof prompt === "string" ? prompt.length : prompt.length,
|
|
8380
8607
|
isMultimodal: Array.isArray(prompt),
|
|
@@ -8730,21 +8957,55 @@ async function dispatchTeammateEval(params) {
|
|
|
8730
8957
|
* Extracted from bot.ts: PermissionError type, extractPermissionError,
|
|
8731
8958
|
* PERMISSION_ERROR_COOLDOWN_MS, permissionErrorNotifiedAt.
|
|
8732
8959
|
*/
|
|
8960
|
+
/** 所有表示权限不足的飞书 API 错误码 */
|
|
8961
|
+
const PERMISSION_ERROR_CODES = new Set([LARK_ERROR.APP_SCOPE_MISSING, LARK_ERROR.NO_USER_AUTHORITY]);
|
|
8962
|
+
/**
|
|
8963
|
+
* 从飞书 API 错误对象中提取权限错误信息。
|
|
8964
|
+
*
|
|
8965
|
+
* 支持两种错误结构:
|
|
8966
|
+
* 1. Axios 风格:err.response.data.{code, msg}(如 99991672)
|
|
8967
|
+
* 2. Lark SDK 直接抛出:err.{code, msg}(如 41050)
|
|
8968
|
+
*/
|
|
8733
8969
|
function extractPermissionError(err) {
|
|
8734
8970
|
if (!err || typeof err !== "object") return null;
|
|
8735
8971
|
const data = err.response?.data;
|
|
8736
|
-
if (
|
|
8737
|
-
|
|
8738
|
-
|
|
8739
|
-
|
|
8740
|
-
|
|
8741
|
-
|
|
8972
|
+
if (data && typeof data === "object") {
|
|
8973
|
+
const feishuErr = data;
|
|
8974
|
+
const result = matchPermissionError(feishuErr.code, feishuErr.msg);
|
|
8975
|
+
if (result) return result;
|
|
8976
|
+
}
|
|
8977
|
+
const directErr = err;
|
|
8978
|
+
const directCode = typeof directErr.code === "number" ? directErr.code : void 0;
|
|
8979
|
+
const directMsg = directErr.msg ?? directErr.message ?? "";
|
|
8980
|
+
if (directCode !== void 0) {
|
|
8981
|
+
const result = matchPermissionError(directCode, directMsg);
|
|
8982
|
+
if (result) return result;
|
|
8983
|
+
}
|
|
8984
|
+
return null;
|
|
8985
|
+
}
|
|
8986
|
+
/**
|
|
8987
|
+
* 根据错误码和错误消息,匹配并构造 PermissionError。
|
|
8988
|
+
*/
|
|
8989
|
+
function matchPermissionError(code, msg) {
|
|
8990
|
+
if (code === void 0 || !PERMISSION_ERROR_CODES.has(code)) return null;
|
|
8991
|
+
const message = msg ?? "";
|
|
8742
8992
|
return {
|
|
8743
|
-
code
|
|
8744
|
-
message
|
|
8745
|
-
grantUrl
|
|
8993
|
+
code,
|
|
8994
|
+
message,
|
|
8995
|
+
grantUrl: extractPermissionGrantUrl(message) || void 0,
|
|
8996
|
+
scopes: extractScopesFromMessage(message)
|
|
8746
8997
|
};
|
|
8747
8998
|
}
|
|
8999
|
+
/**
|
|
9000
|
+
* 从错误消息中提取权限 scope 列表。
|
|
9001
|
+
* 飞书错误消息中常见格式:[scope1,scope2,...] 或 scope: xxx
|
|
9002
|
+
*/
|
|
9003
|
+
function extractScopesFromMessage(msg) {
|
|
9004
|
+
const bracketMatch = msg.match(/\[([^\]]+)\]/);
|
|
9005
|
+
if (bracketMatch?.[1]) return bracketMatch[1].split(",").map((s) => s.trim()).filter(Boolean);
|
|
9006
|
+
const scopeMatch = msg.match(/(?:scope[s]?[=:]\s*)([a-z][a-z0-9_.:-]+(?:,\s*[a-z][a-z0-9_.:-]+)*)/i);
|
|
9007
|
+
if (scopeMatch?.[1]) return scopeMatch[1].split(",").map((s) => s.trim()).filter(Boolean);
|
|
9008
|
+
}
|
|
8748
9009
|
//#endregion
|
|
8749
9010
|
//#region src/messaging/inbound/user-name-cache.ts
|
|
8750
9011
|
/** Max user_ids per API call (Feishu limit). */
|
|
@@ -11059,7 +11320,8 @@ const DEFAULT_CONFIG = {
|
|
|
11059
11320
|
globalDefault: true,
|
|
11060
11321
|
groupOverrides: {},
|
|
11061
11322
|
bufferSize: 5,
|
|
11062
|
-
bufferTimeoutSec: 30
|
|
11323
|
+
bufferTimeoutSec: 30,
|
|
11324
|
+
maxHourlyInterventions: 10
|
|
11063
11325
|
};
|
|
11064
11326
|
var TeammateConfig = class {
|
|
11065
11327
|
data;
|
|
@@ -11130,6 +11392,9 @@ var TeammateConfig = class {
|
|
|
11130
11392
|
getBufferTimeoutSec() {
|
|
11131
11393
|
return this.data.bufferTimeoutSec;
|
|
11132
11394
|
}
|
|
11395
|
+
getMaxHourlyInterventions() {
|
|
11396
|
+
return this.data.maxHourlyInterventions;
|
|
11397
|
+
}
|
|
11133
11398
|
/** 获取完整配置数据(用于 API 展示) */
|
|
11134
11399
|
getData() {
|
|
11135
11400
|
return this.data;
|
|
@@ -11154,18 +11419,23 @@ const log$1 = larkLogger("messaging/teammate-buffer");
|
|
|
11154
11419
|
var TeammateBuffer = class {
|
|
11155
11420
|
/** 每群独立缓冲区 */
|
|
11156
11421
|
buffers = /* @__PURE__ */ new Map();
|
|
11422
|
+
/** 防失控兜底:每群每小时主动发言计数器 */
|
|
11423
|
+
hourlyCounters = /* @__PURE__ */ new Map();
|
|
11157
11424
|
/** 触发评估时的回调 */
|
|
11158
11425
|
evalCallback;
|
|
11159
11426
|
/** 配置参数 */
|
|
11160
11427
|
bufferSize;
|
|
11161
11428
|
bufferTimeoutMs;
|
|
11429
|
+
maxHourlyInterventions;
|
|
11162
11430
|
constructor(opts) {
|
|
11163
11431
|
this.evalCallback = opts.onEval;
|
|
11164
11432
|
this.bufferSize = opts.bufferSize ?? 5;
|
|
11165
11433
|
this.bufferTimeoutMs = (opts.bufferTimeoutSec ?? 30) * 1e3;
|
|
11434
|
+
this.maxHourlyInterventions = opts.maxHourlyInterventions ?? 10;
|
|
11166
11435
|
log$1.info("TeammateBuffer 初始化", {
|
|
11167
11436
|
bufferSize: this.bufferSize,
|
|
11168
|
-
bufferTimeoutMs: this.bufferTimeoutMs
|
|
11437
|
+
bufferTimeoutMs: this.bufferTimeoutMs,
|
|
11438
|
+
maxHourlyInterventions: this.maxHourlyInterventions
|
|
11169
11439
|
});
|
|
11170
11440
|
}
|
|
11171
11441
|
/**
|
|
@@ -11201,11 +11471,51 @@ var TeammateBuffer = class {
|
|
|
11201
11471
|
});
|
|
11202
11472
|
buf.evaluating = null;
|
|
11203
11473
|
buf.isEvaluating = false;
|
|
11474
|
+
if (replied) this.incrementHourlyCounter(chatId);
|
|
11204
11475
|
if (buf.active.length > 0) {
|
|
11205
11476
|
if (buf.active.length >= this.bufferSize) this.tryTrigger(chatId, buf);
|
|
11206
11477
|
else if (!buf.timer) this.startTimer(chatId, buf);
|
|
11207
11478
|
}
|
|
11208
11479
|
}
|
|
11480
|
+
/**
|
|
11481
|
+
* 检查指定群聊是否触发了防失控限流。
|
|
11482
|
+
* 如果当前小时窗口已达到上限,返回 true。
|
|
11483
|
+
*/
|
|
11484
|
+
isRateLimited(chatId) {
|
|
11485
|
+
const counter = this.hourlyCounters.get(chatId);
|
|
11486
|
+
if (!counter) return false;
|
|
11487
|
+
if (Date.now() - counter.windowStart >= 3600 * 1e3) {
|
|
11488
|
+
this.hourlyCounters.delete(chatId);
|
|
11489
|
+
log$1.info("teammate 防失控计数器已重置(时间窗口过期)", { chatId });
|
|
11490
|
+
return false;
|
|
11491
|
+
}
|
|
11492
|
+
return counter.count >= this.maxHourlyInterventions;
|
|
11493
|
+
}
|
|
11494
|
+
/**
|
|
11495
|
+
* 增加指定群聊的每小时主动发言计数。
|
|
11496
|
+
*/
|
|
11497
|
+
incrementHourlyCounter(chatId) {
|
|
11498
|
+
const now = Date.now();
|
|
11499
|
+
const oneHour = 3600 * 1e3;
|
|
11500
|
+
const counter = this.hourlyCounters.get(chatId);
|
|
11501
|
+
if (!counter || now - counter.windowStart >= oneHour) {
|
|
11502
|
+
this.hourlyCounters.set(chatId, {
|
|
11503
|
+
count: 1,
|
|
11504
|
+
windowStart: now
|
|
11505
|
+
});
|
|
11506
|
+
log$1.info("teammate 防失控计数器开始新窗口", {
|
|
11507
|
+
chatId,
|
|
11508
|
+
count: 1
|
|
11509
|
+
});
|
|
11510
|
+
} else {
|
|
11511
|
+
counter.count++;
|
|
11512
|
+
log$1.info("teammate 防失控计数器更新", {
|
|
11513
|
+
chatId,
|
|
11514
|
+
count: counter.count,
|
|
11515
|
+
limit: this.maxHourlyInterventions
|
|
11516
|
+
});
|
|
11517
|
+
}
|
|
11518
|
+
}
|
|
11209
11519
|
/** 清理所有缓冲区和定时器(优雅退出时调用) */
|
|
11210
11520
|
dispose() {
|
|
11211
11521
|
for (const [chatId, buf] of this.buffers) {
|
|
@@ -11249,6 +11559,18 @@ var TeammateBuffer = class {
|
|
|
11249
11559
|
return;
|
|
11250
11560
|
}
|
|
11251
11561
|
if (buf.active.length === 0) return;
|
|
11562
|
+
if (this.isRateLimited(chatId)) {
|
|
11563
|
+
log$1.warn("teammate 防失控兜底生效, 该群本小时已达上限", {
|
|
11564
|
+
chatId,
|
|
11565
|
+
limit: this.maxHourlyInterventions
|
|
11566
|
+
});
|
|
11567
|
+
if (buf.timer) {
|
|
11568
|
+
clearTimeout(buf.timer);
|
|
11569
|
+
buf.timer = null;
|
|
11570
|
+
}
|
|
11571
|
+
buf.active = [];
|
|
11572
|
+
return;
|
|
11573
|
+
}
|
|
11252
11574
|
if (buf.timer) {
|
|
11253
11575
|
clearTimeout(buf.timer);
|
|
11254
11576
|
buf.timer = null;
|
|
@@ -11725,6 +12047,7 @@ async function main() {
|
|
|
11725
12047
|
}
|
|
11726
12048
|
} catch {}
|
|
11727
12049
|
preflight();
|
|
12050
|
+
ensureLarkCliConfig();
|
|
11728
12051
|
const credentialProvider = new LarkCliCredentialProvider();
|
|
11729
12052
|
const appId = credentialProvider.getAppId();
|
|
11730
12053
|
logger.info("凭证加载完成", { appId });
|
|
@@ -11828,7 +12151,8 @@ async function main() {
|
|
|
11828
12151
|
});
|
|
11829
12152
|
},
|
|
11830
12153
|
bufferSize: teammateConfig.getBufferSize(),
|
|
11831
|
-
bufferTimeoutSec: teammateConfig.getBufferTimeoutSec()
|
|
12154
|
+
bufferTimeoutSec: teammateConfig.getBufferTimeoutSec(),
|
|
12155
|
+
maxHourlyInterventions: teammateConfig.getMaxHourlyInterventions()
|
|
11832
12156
|
});
|
|
11833
12157
|
logger.info("TeammateBuffer 初始化完成");
|
|
11834
12158
|
const wsAbortController = new AbortController();
|