larkway 0.3.34 → 0.3.35

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.md CHANGED
@@ -8,7 +8,7 @@
8
8
 
9
9
  You @ the bot in a Feishu thread. It runs on your machine — reading your real codebase, executing commands, opening MRs — and posts the result back. You define what the agent knows and what it can do. Larkway just carries the messages.
10
10
 
11
- **Current release: v0.3.34**
11
+ **Current release: v0.3.35**
12
12
 
13
13
  ---
14
14
 
package/README.zh.md CHANGED
@@ -8,7 +8,7 @@
8
8
 
9
9
  你在飞书话题里 @ bot,它在你的机器上运行——读真实代码库、执行命令、开 MR——把结果贴回飞书。你定义 agent 知道什么、能做什么。Larkway 只负责传递消息。
10
10
 
11
- **当前版本:v0.3.34**
11
+ **当前版本:v0.3.35**
12
12
 
13
13
  ---
14
14
 
package/dist/cli/index.js CHANGED
@@ -112381,6 +112381,125 @@ var init_channelPostClient = __esm({
112381
112381
  }
112382
112382
  });
112383
112383
 
112384
+ // src/lark/channelCotClient.ts
112385
+ async function withTimeout(p, ms, label) {
112386
+ let timer;
112387
+ const timeout = new Promise((_resolve, reject) => {
112388
+ timer = setTimeout(
112389
+ () => reject(new Error(`[channel.cot] ${label} timed out after ${ms}ms`)),
112390
+ ms
112391
+ );
112392
+ timer.unref?.();
112393
+ });
112394
+ try {
112395
+ return await Promise.race([p, timeout]);
112396
+ } finally {
112397
+ if (timer) clearTimeout(timer);
112398
+ }
112399
+ }
112400
+ function assertOk(res, label) {
112401
+ if (res.code !== void 0 && res.code !== 0) {
112402
+ throw new Error(
112403
+ `[channel.cot] ${label} failed: code=${res.code} msg=${res.msg ?? "<no msg>"}`
112404
+ );
112405
+ }
112406
+ }
112407
+ function stringField(data, key) {
112408
+ const value = data?.[key];
112409
+ return typeof value === "string" ? value : void 0;
112410
+ }
112411
+ var COT_REQUEST_TIMEOUT_MS, ChannelCotClient;
112412
+ var init_channelCotClient = __esm({
112413
+ "src/lark/channelCotClient.ts"() {
112414
+ "use strict";
112415
+ COT_REQUEST_TIMEOUT_MS = 8e3;
112416
+ ChannelCotClient = class {
112417
+ resolveChannel;
112418
+ constructor(opts) {
112419
+ this.resolveChannel = opts.resolveChannel;
112420
+ }
112421
+ channel() {
112422
+ const ch = this.resolveChannel();
112423
+ if (!ch) {
112424
+ throw new Error("[channel.cot] outbound called before the Channel SDK connected");
112425
+ }
112426
+ return ch;
112427
+ }
112428
+ /** Every COT call goes through here, so all get the same hard timeout. */
112429
+ request(opts) {
112430
+ return withTimeout(
112431
+ this.channel().rawClient.request(opts),
112432
+ COT_REQUEST_TIMEOUT_MS,
112433
+ `${opts.method} ${opts.url}`
112434
+ );
112435
+ }
112436
+ async create(target) {
112437
+ const receiveIdType = target.threadId ? "thread_id" : "chat_id";
112438
+ const receiveId = target.threadId ?? target.chatId;
112439
+ const res = await this.request({
112440
+ url: "/open-apis/im/v1/message_cot",
112441
+ method: "POST",
112442
+ params: { receive_id_type: receiveIdType },
112443
+ data: {
112444
+ receive_id: receiveId,
112445
+ // origin_message_id only anchors non-thread bubbles; harmless if set,
112446
+ // but omitted for topic threads where the thread id already anchors.
112447
+ ...!target.threadId && target.originMessageId ? { origin_message_id: target.originMessageId } : {}
112448
+ }
112449
+ });
112450
+ assertOk(res, "create");
112451
+ const cotId = stringField(res.data, "cot_id");
112452
+ const messageId = stringField(res.data, "message_id");
112453
+ if (!cotId || !messageId) {
112454
+ throw new Error(
112455
+ `[channel.cot] create returned no cot_id/message_id (${JSON.stringify(res.data ?? {}).slice(0, 200)})`
112456
+ );
112457
+ }
112458
+ return { cotId, messageId };
112459
+ }
112460
+ async update(ref, events) {
112461
+ if (events.length === 0) return;
112462
+ const res = await this.request({
112463
+ url: "/open-apis/im/v1/message_cot",
112464
+ method: "PUT",
112465
+ data: { cot_id: ref.cotId, message_id: ref.messageId, events: [...events] }
112466
+ });
112467
+ assertOk(res, "update");
112468
+ }
112469
+ async complete(ref, reason) {
112470
+ const res = await this.request({
112471
+ url: `/open-apis/im/v1/message_cot/complete/${encodeURIComponent(ref.cotId)}`,
112472
+ method: "POST",
112473
+ params: { message_id: ref.messageId, reason }
112474
+ });
112475
+ assertOk(res, "complete");
112476
+ }
112477
+ async resolveThreadId(messageId) {
112478
+ try {
112479
+ const res = await this.request({
112480
+ url: `/open-apis/im/v1/messages/${encodeURIComponent(messageId)}`,
112481
+ method: "GET"
112482
+ });
112483
+ if (res.code !== void 0 && res.code !== 0) return void 0;
112484
+ const items = res.data?.["items"];
112485
+ if (!Array.isArray(items)) return void 0;
112486
+ for (const item of items) {
112487
+ if (item && typeof item === "object") {
112488
+ const threadId = item["thread_id"];
112489
+ if (typeof threadId === "string" && threadId.startsWith("omt_")) {
112490
+ return threadId;
112491
+ }
112492
+ }
112493
+ }
112494
+ return void 0;
112495
+ } catch {
112496
+ return void 0;
112497
+ }
112498
+ }
112499
+ };
112500
+ }
112501
+ });
112502
+
112384
112503
  // src/lark/channelClient.ts
112385
112504
  var channelClient_exports = {};
112386
112505
  __export(channelClient_exports, {
@@ -112431,13 +112550,13 @@ function arrayField(obj, key) {
112431
112550
  const value = obj[key];
112432
112551
  return Array.isArray(value) ? value : null;
112433
112552
  }
112434
- function stringField(obj, key) {
112553
+ function stringField2(obj, key) {
112435
112554
  if (!obj || typeof obj !== "object") return void 0;
112436
112555
  const value = obj[key];
112437
112556
  return typeof value === "string" ? value : void 0;
112438
112557
  }
112439
112558
  function nonEmptyStringField(obj, key) {
112440
- const value = stringField(obj, key);
112559
+ const value = stringField2(obj, key);
112441
112560
  return value && value.length > 0 ? value : void 0;
112442
112561
  }
112443
112562
  function parseLarkCliMessages(stdout2) {
@@ -112540,6 +112659,7 @@ var init_channelClient = __esm({
112540
112659
  init_channelCardClient();
112541
112660
  init_channelCardKitClient();
112542
112661
  init_channelPostClient();
112662
+ init_channelCotClient();
112543
112663
  execFile4 = promisify4(execFileCallback);
112544
112664
  LEARNED_CHATS_LIMIT = 100;
112545
112665
  SEEN_MESSAGES_LIMIT = 1e3;
@@ -112675,6 +112795,8 @@ var init_channelClient = __esm({
112675
112795
  cardKitClient = null;
112676
112796
  /** Lazily built and only requested by main.ts when post outbound gates are configured. */
112677
112797
  postClient = null;
112798
+ /** Lazily built COT (思维链) client; only requested when a bot enables cot != off. */
112799
+ cotClient = null;
112678
112800
  constructor(opts) {
112679
112801
  if (!opts.appId || !opts.appSecret) {
112680
112802
  throw new Error(
@@ -112940,6 +113062,22 @@ var init_channelClient = __esm({
112940
113062
  }
112941
113063
  return this.cardKitClient;
112942
113064
  }
113065
+ /**
113066
+ * Return an OutboundCotClient bound to this client's channel handle.
113067
+ *
113068
+ * main.ts only calls this when the bot's `cot` config is not "off". The COT
113069
+ * bubble uses the SDK's generic rawClient.request() (tenant token auto-
113070
+ * injected via the same token manager as every other outbound call), so it
113071
+ * adds no auth surface. Resolves the live channel lazily at call time.
113072
+ */
113073
+ outboundCotClient() {
113074
+ if (!this.cotClient) {
113075
+ this.cotClient = new ChannelCotClient({
113076
+ resolveChannel: () => this.channel
113077
+ });
113078
+ }
113079
+ return this.cotClient;
113080
+ }
112943
113081
  /**
112944
113082
  * Return an OutboundPostClient bound to this client's channel handle.
112945
113083
  *
@@ -113243,7 +113381,7 @@ var init_channelClient = __esm({
113243
113381
  const chats = parseLarkCliMessages(stdout2) ?? [];
113244
113382
  fetched += chats.length;
113245
113383
  for (const raw of chats) {
113246
- const chatId = stringField(raw, "chat_id");
113384
+ const chatId = stringField2(raw, "chat_id");
113247
113385
  if (!chatId?.startsWith("oc_")) continue;
113248
113386
  discoveredChatIds.add(chatId);
113249
113387
  const before = this.recentlySeenChatIds.size;
@@ -113257,7 +113395,7 @@ var init_channelClient = __esm({
113257
113395
  const hasMore = Boolean(
113258
113396
  (data && typeof data === "object" && data["has_more"]) ?? (parsed && typeof parsed === "object" && parsed["has_more"])
113259
113397
  );
113260
- pageToken = stringField(data, "page_token") ?? stringField(parsed, "page_token") ?? "";
113398
+ pageToken = stringField2(data, "page_token") ?? stringField2(parsed, "page_token") ?? "";
113261
113399
  if (!hasMore || !pageToken) break;
113262
113400
  }
113263
113401
  if (newlyLearned > 0) {
@@ -113571,7 +113709,7 @@ function isTaskRequestTimeoutError(err2) {
113571
113709
  if (err2 instanceof TaskApiError && err2.cause instanceof TaskRequestTimeoutError) return true;
113572
113710
  return false;
113573
113711
  }
113574
- function withTimeout(promise, ms, label) {
113712
+ function withTimeout2(promise, ms, label) {
113575
113713
  let timer;
113576
113714
  const timeout = new Promise((_, reject) => {
113577
113715
  timer = setTimeout(() => reject(new TaskRequestTimeoutError(`${label} timed out after ${ms}ms`)), ms);
@@ -113615,7 +113753,7 @@ var init_client = __esm({
113615
113753
  }
113616
113754
  /** Single choke point for every outbound call — applies the timeout race uniformly. */
113617
113755
  #request(config, label) {
113618
- return withTimeout(this.#requester.request(config), this.#timeoutMs, label);
113756
+ return withTimeout2(this.#requester.request(config), this.#timeoutMs, label);
113619
113757
  }
113620
113758
  async getTask(taskGuid) {
113621
113759
  let resp;
@@ -125718,6 +125856,31 @@ var BotConfigSchema = external_exports.object({
125718
125856
  * @default "claude"
125719
125857
  */
125720
125858
  backend: external_exports.string().min(1).default("claude"),
125859
+ /**
125860
+ * COT (思维链) 气泡:把 agent 的 thinking 思考过程 + 工具调用摘要实时推到飞书
125861
+ * 原生的可折叠思维链气泡(与最终答案卡片互不干扰,最终答案永远只走卡片)。
125862
+ *
125863
+ * - "off":完全不调用 message_cot API(零网络、零副作用)。
125864
+ * - "brief"(默认):思考过程 + 工具名摘要。
125865
+ * - "detailed":额外推工具入参(TOOL_CALL_ARGS)与截断后的工具结果。
125866
+ *
125867
+ * message_cot 是无公开文档的 API —— 任何一步失败都会在本次会话内自动降级、
125868
+ * 只记 warn,绝不影响卡片和最终答案(见 src/bridge/cotProgress.ts)。
125869
+ */
125870
+ cot: external_exports.enum(["off", "brief", "detailed"]).default("brief"),
125871
+ /**
125872
+ * COT 展示形态。`cot` 管密度(off/brief/detailed),这个管落点:
125873
+ * - "bubble"(默认):走飞书原生 message_cot 思维链气泡(src/bridge/cotProgress.ts)——
125874
+ * 客户端原生渲染(工具图标列表 + 可折叠头),真机体验最好。已知限制:话题内
125875
+ * 锚定不可用(气泡落群顶层,本租户 thread 通道判死),收尾 bug 已在 main 修复。
125876
+ * - "card":思考过程内嵌进答案卡片里的 collapsible_panel(懒创建、答案上方)。
125877
+ * ⚠️ 实验选项 —— 2026-07-05 真机实测:飞书客户端**不渲染 collapsible_panel 的
125878
+ * 折叠交互**(API 全 code=0,但面板被拍平成普通文本、无折叠箭头、终态也不可折叠;
125879
+ * CardKit 无 GET 读回、只能真机人眼验)。故 card 形态目前仅作纯文本内嵌展示用,
125880
+ * 体验不如气泡,不建议作默认。见 memory feishu-message-cot-api。
125881
+ * cot="off" 时本字段无意义(两种形态都不产出)。
125882
+ */
125883
+ cotSurface: external_exports.enum(["card", "bubble"]).default("bubble"),
125721
125884
  /**
125722
125885
  * 话题 ↔ 飞书任务句柄(docs/task-handle.md,v2)。省略此字段不代表关闭,但也
125723
125886
  * 不会触发任何自动建清单——main.ts 在 startup 时只做只读解析(yaml 里的
@@ -127196,6 +127359,8 @@ async function runCreateBot(ctx, creds, basics) {
127196
127359
  response_surface_prototype: DEFAULT_RESPONSE_SURFACE_PROTOTYPE,
127197
127360
  runtime: "agent_workspace",
127198
127361
  backend: basics.backend,
127362
+ cot: "brief",
127363
+ cotSurface: "bubble",
127199
127364
  memory_file: memoryFile,
127200
127365
  ...basics.gitlabTokenEnv ? { gitlab_token_env: basics.gitlabTokenEnv } : {},
127201
127366
  ...basics.gitName && basics.gitEmail ? { git_identity: { name: basics.gitName, email: basics.gitEmail } } : {}
@@ -130650,6 +130815,8 @@ async function createBotFromCreds(opts) {
130650
130815
  response_surface_prototype: DEFAULT_RESPONSE_SURFACE_PROTOTYPE,
130651
130816
  runtime: "agent_workspace",
130652
130817
  backend: form.backend && form.backend.trim() ? form.backend.trim() : "codex",
130818
+ cot: "brief",
130819
+ cotSurface: "bubble",
130653
130820
  // Persist the Feishu avatar URL so the Web 管理面 can show an avatar before
130654
130821
  // the bridge writes status.json (pre-bridge / central roster). Best-effort:
130655
130822
  // only set when resolveBotIdentity returned a valid url.
@@ -131182,8 +131349,8 @@ async function loadChatNameMap(profile) {
131182
131349
  try {
131183
131350
  const { stdout: stdout2 } = await eventExecFile("lark-cli", args, { timeout: 8e3 });
131184
131351
  for (const chat of extractLarkCliChats(stdout2)) {
131185
- const chatId = stringField2(chat, "chat_id");
131186
- const name = stringField2(chat, "name");
131352
+ const chatId = stringField3(chat, "chat_id");
131353
+ const name = stringField3(chat, "name");
131187
131354
  if (chatId && name) names.set(chatId, name);
131188
131355
  }
131189
131356
  } catch {
@@ -131210,7 +131377,7 @@ function extractLarkCliChats(stdout2) {
131210
131377
  function isLarkOpenChatId(value) {
131211
131378
  return typeof value === "string" && value.startsWith("oc_");
131212
131379
  }
131213
- function stringField2(value, key) {
131380
+ function stringField3(value, key) {
131214
131381
  if (!value || typeof value !== "object") return void 0;
131215
131382
  const out = value[key];
131216
131383
  return typeof out === "string" ? out : void 0;