@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.
Files changed (2) hide show
  1. package/dist/main.mjs +345 -21
  2. 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$29 = larkLogger("preflight");
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$29.info("开始前置环境检查...");
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$29.info(` ${status(claude)}`);
82
- log$29.info(` ${status(larkCli)}`);
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$29.error(msg);
112
+ log$30.error(msg);
113
113
  throw new Error("前置环境检查未通过,缺少必要依赖");
114
114
  }
115
- log$29.info("前置环境检查通过 ✓");
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
- const isResuming = this.ccSessionExists(cwd, claudeSessionId);
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 (!data || typeof data !== "object") return null;
8737
- const feishuErr = data;
8738
- if (feishuErr.code !== LARK_ERROR.APP_SCOPE_MISSING) return null;
8739
- const msg = feishuErr.msg ?? "";
8740
- const grantUrl = extractPermissionGrantUrl(msg);
8741
- if (!grantUrl) return null;
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: feishuErr.code,
8744
- message: msg,
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();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vibe-lark/larkpal",
3
- "version": "0.1.8",
3
+ "version": "0.1.9",
4
4
  "description": "LarkPal - Lark/Feishu bot service",
5
5
  "type": "module",
6
6
  "main": "./dist/main.mjs",