larkway 0.3.20 → 0.3.21

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.20**
11
+ **Current release: v0.3.21**
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.20**
11
+ **当前版本:v0.3.21**
12
12
 
13
13
  ---
14
14
 
package/dist/cli/index.js CHANGED
@@ -111269,6 +111269,20 @@ var init_channelPostClient = __esm({
111269
111269
  }
111270
111270
  return { messageId };
111271
111271
  }
111272
+ async updatePost(messageId, content) {
111273
+ const res = await withPostRetry(
111274
+ "updatePost",
111275
+ () => this.channel().rawClient.im.v1.message.update({
111276
+ path: { message_id: messageId },
111277
+ data: {
111278
+ content,
111279
+ msg_type: "post"
111280
+ }
111281
+ }),
111282
+ { maxAttempts: this.maxAttempts, baseDelayMs: this.baseDelayMs }
111283
+ );
111284
+ return { messageId: res.data?.message_id ?? messageId };
111285
+ }
111272
111286
  };
111273
111287
  }
111274
111288
  });
@@ -123680,7 +123694,7 @@ function defaultResponseSurfacePrototypeConfig() {
123680
123694
  enabled: true,
123681
123695
  allowed_chats: [],
123682
123696
  allowed_threads: [],
123683
- lazy_card_creation: false,
123697
+ lazy_card_creation: true,
123684
123698
  kill_switch: false,
123685
123699
  post_outbound_enabled: true,
123686
123700
  allow_agent_mentions: true,
@@ -123697,7 +123711,7 @@ var responseSurfacePrototypeConfigDefaults = () => ({
123697
123711
  enabled: true,
123698
123712
  allowed_chats: [],
123699
123713
  allowed_threads: [],
123700
- lazy_card_creation: false,
123714
+ lazy_card_creation: true,
123701
123715
  kill_switch: false,
123702
123716
  post_outbound_enabled: true,
123703
123717
  allow_agent_mentions: true,
@@ -123764,11 +123778,11 @@ var ResponseSurfacePrototypeConfigSchema = external_exports.object({
123764
123778
  */
123765
123779
  allowed_threads: external_exports.array(external_exports.string().min(1)).default([]),
123766
123780
  /**
123767
- * Dark-launch hook for PR2. This does not enable post-only output by itself;
123768
- * the bridge still requires a visible card fallback until post outbound
123769
- * exists and is explicitly enabled in a later PR.
123781
+ * Post-first gate. Default true means the bridge starts a lightweight post
123782
+ * as the live main surface when post outbound is available, and creates a
123783
+ * card only for fallback or card-only capabilities.
123770
123784
  */
123771
- lazy_card_creation: external_exports.boolean().default(false),
123785
+ lazy_card_creation: external_exports.boolean().default(true),
123772
123786
  /**
123773
123787
  * Runtime kill switch for emergency rollback. When true, every response
123774
123788
  * surface post path is treated as disabled even if enabled/allowlists are
package/dist/main.js CHANGED
@@ -115497,6 +115497,20 @@ var ChannelPostClient = class {
115497
115497
  }
115498
115498
  return { messageId };
115499
115499
  }
115500
+ async updatePost(messageId, content) {
115501
+ const res = await withPostRetry(
115502
+ "updatePost",
115503
+ () => this.channel().rawClient.im.v1.message.update({
115504
+ path: { message_id: messageId },
115505
+ data: {
115506
+ content,
115507
+ msg_type: "post"
115508
+ }
115509
+ }),
115510
+ { maxAttempts: this.maxAttempts, baseDelayMs: this.baseDelayMs }
115511
+ );
115512
+ return { messageId: res.data?.message_id ?? messageId };
115513
+ }
115500
115514
  };
115501
115515
 
115502
115516
  // src/lark/channelClient.ts
@@ -117434,10 +117448,12 @@ function renderStateContract(stateFilePath) {
117434
117448
  const stateTarget = stateFilePath ? `\u6307\u5B9A\u8DEF\u5F84 \`${stateFilePath}\`` : "\u5DE5\u4F5C\u76EE\u5F55\u91CC\u7684 `.larkway/state.json`";
117435
117449
  return [
117436
117450
  "<state-contract>",
117437
- "\u4F60\u548C\u8FD0\u8425\u4E4B\u95F4\u7684\u754C\u9762\u662F\u98DE\u4E66\u8BDD\u9898\u91CC\u7684\u5361\u7247,\u4F46\u8FD9\u5F20\u5361\u7247\u662F\u4E00\u4E2A thin-channel \u5916\u58F3:",
117438
- "- bridge \u53EA\u8D1F\u8D23\u521B\u5EFA/\u66F4\u65B0\u98DE\u4E66\u5361\u7247\u3001\u8282\u6D41 PATCH\u3001\u663E\u793A\u901A\u7528\u5934\u90E8\u72B6\u6001\u3001\u6E32\u67D3\u6B63\u6587 markdown\u3001\u663E\u793A\u5FC5\u8981\u7684\u5DE5\u5177\u6458\u8981\u3001\u628A choices \u8F6C\u6210\u6309\u94AE\u5E76\u628A\u70B9\u51FB\u503C\u56DE\u4F20\u7ED9\u4F60\u3002",
117439
- "- \u4F60\u8D1F\u8D23\u51B3\u5B9A\u5361\u7247\u91CC\u8981\u544A\u8BC9\u8FD0\u8425\u4EC0\u4E48\u3001\u5F53\u524D\u662F\u4EC0\u4E48\u4E1A\u52A1\u72B6\u6001\u3001\u9700\u8981\u7528\u6237\u56DE\u590D\u4EC0\u4E48\u3001\u8981\u4E0D\u8981\u7ED9\u79BB\u6563\u6309\u94AE\u3002",
117440
- `\u4F60\u4E0D\u76F4\u63A5\u753B\u98DE\u4E66\u5361\u7247,\u4E5F\u4E0D\u76F4\u63A5 PATCH \u5361\u7247;\u4F60\u53EA\u5199 ${stateTarget},bridge \u8BFB\u5B83\u6765\u505A\u5B89\u5168\u6E32\u67D3\u3002`,
117451
+ "\u4F60\u548C\u8FD0\u8425\u4E4B\u95F4\u7684\u754C\u9762\u662F\u98DE\u4E66\u8BDD\u9898\u91CC\u7684 response surface,\u8FD9\u662F\u4E00\u4E2A thin-channel \u5916\u58F3:",
117452
+ "- \u9ED8\u8BA4\u4E3B\u56DE\u590D\u9762\u662F post/RichText:bridge \u8D77\u624B\u53D1\u4E00\u6761\u8F7B\u91CF\u201C\u6B63\u5728\u5904\u7406\u2026\u201Dpost,\u6267\u884C\u4E2D\u7528\u5206\u5757\u7EA7\u539F\u5730\u7F16\u8F91\u6A21\u62DF\u6D41\u5F0F\u8F93\u51FA,\u7ED3\u675F\u65F6\u7F16\u8F91\u6210\u5E72\u51C0\u7EC8\u7A3F\u3002",
117453
+ "- \u5361\u7247\u662F\u4F8B\u5916\u8865\u5145\u9762,\u53EA\u5728\u9700\u8981\u79BB\u6563\u9009\u62E9/\u6309\u94AE(`choices`)\u6216 post \u8868\u8FBE\u4E0D\u4E86\u7684\u7ED3\u6784\u5316\u5185\u5BB9(`content_blocks`/`image_blocks`,\u4F8B\u5982\u56FE\u7247\u7EC4\u3001\u9A8C\u6536\u6E05\u5355\u3001dev \u9884\u89C8)\u65F6\u624D\u8865\u4E00\u5F20\u3002",
117454
+ "- bridge \u8D1F\u8D23\u521B\u5EFA/\u7F16\u8F91 post\u3001\u5FC5\u8981\u65F6\u521B\u5EFA/\u66F4\u65B0\u98DE\u4E66\u5361\u7247\u3001\u8282\u6D41\u3001\u6E32\u67D3\u6B63\u6587\u3001\u628A choices \u8F6C\u6210\u6309\u94AE\u5E76\u628A\u70B9\u51FB\u503C\u56DE\u4F20\u7ED9\u4F60\u3002",
117455
+ "- \u4F60\u8D1F\u8D23\u628A\u6700\u7EC8\u7ED9\u8FD0\u8425\u770B\u7684\u6B63\u6587\u3001\u72B6\u6001\u3001\u4E0B\u4E00\u6B65\u95EE\u9898\u3001\u662F\u5426\u9700\u8981 choices/\u7ED3\u6784\u5316\u5361\u7247\u5199\u8FDB state.json\u3002",
117456
+ `\u4F60\u4E0D\u76F4\u63A5\u53D1/\u7F16\u8F91 bridge \u7BA1\u7406\u7684 post \u6216\u5361\u7247;\u4F60\u53EA\u5199 ${stateTarget},bridge \u8BFB\u5B83\u6765\u505A\u5B89\u5168\u6E32\u67D3\u3002`,
117441
117457
  "**\u5B8C\u6210\u672C\u6B21\u54CD\u5E94\u524D\u5FC5\u987B**\u6839\u636E\u5F53\u524D\u5B9E\u9645\u72B6\u6001\u66F4\u65B0\u8FD9\u4E2A\u6587\u4EF6(\u539F\u5B50\u5199:\u5199 .tmp \u518D mv)\u3002",
117442
117458
  "",
117443
117459
  "\u4F60\u80FD\u5199\u7684\u5B57\u6BB5:",
@@ -117448,18 +117464,19 @@ function renderStateContract(stateFilePath) {
117448
117464
  "- card_color: \u5361\u7247\u914D\u8272(\u53EF\u9009,success/failure/neutral,\u4E5F\u53EF\u76F4\u63A5\u5199 green/red/grey)",
117449
117465
  "- image_blocks: \u53EF\u9009\u56FE\u7247\u9884\u89C8\u5757\u6570\u7EC4,\u6700\u591A 4 \u4E2A\u3002\u6BCF\u9879 `{img_key, alt?, title?, mode?, preview?}`; `img_key` \u5FC5\u987B\u662F\u5DF2\u4E0A\u4F20/\u53EF\u7528\u4E8E\u5361\u7247\u7684 Feishu \u56FE\u7247 key,`alt` \u7701\u7565\u65F6 bridge \u9ED8\u8BA4\u201C\u56FE\u7247\u9884\u89C8\u201D,`mode` \u53EA\u5141\u8BB8 `crop_center`/`fit_horizontal` \u5E76\u6620\u5C04\u5230 Card JSON 2.0 `scale_type`,`preview` \u9ED8\u8BA4 true\u3002bridge \u4E0D\u8D1F\u8D23\u4E0B\u8F7D/\u4E0A\u4F20/\u9009\u62E9\u56FE\u7247;\u8FD9\u4E9B\u7531\u4F60\u7528 lark-cli \u7B49\u5DE5\u5177\u5148\u5B8C\u6210\u3002",
117450
117466
  '- content_blocks: \u53EF\u9009\u6709\u5E8F\u6B63\u6587\u5757\u6570\u7EC4,\u6700\u591A 12 \u4E2A block\u3001\u6700\u591A 4 \u4E2A image block\u3002\u53EA\u652F\u6301\u7A84 union:`{type:"markdown", content}` \u548C `{type:"image", img_key, alt?, title?, mode?, preview?}`;\u4E0D\u652F\u6301 raw card JSON\u3002\u7528\u4E8E\u6B63\u6587\u4E0E\u56FE\u7247\u4EA4\u9519\u6392\u7248,\u4F8B\u5982 markdown -> image -> markdown -> image\u3002\u82E5 `content_blocks` \u975E\u7A7A,bridge \u4EE5\u5B83\u4F5C\u4E3A\u4E3B\u6B63\u6587\u5E76\u5FFD\u7565 `last_message` + `image_blocks` \u7684\u6B63\u6587\u6E32\u67D3,\u907F\u514D\u91CD\u590D;\u82E5\u7701\u7565\u5219\u4FDD\u6301\u65E7 `last_message` + `image_blocks` \u884C\u4E3A\u3002',
117451
- '- response_surface: \u53EF\u9009\u539F\u578B\u5B57\u6BB5,\u5F62\u5982 `{mode:"card"|"post"|"hybrid", primary?:"card"|"post", post?:{mentions:[{user_id,label?}]}, card?:{compact?:boolean, capabilities?:[...]}}`\u3002\u5F53\u524D\u4EC5\u5728 bot \u914D\u7F6E\u663E\u5F0F\u542F\u7528\u4E14\u547D\u4E2D\u767D\u540D\u5355\u65F6\u624D\u53EF\u80FD\u88AB bridge \u8BFB\u53D6;PR3 \u771F\u5B9E post outbound/ledger \u672A\u5B9E\u73B0\u524D,bridge \u5FC5\u987B\u4FDD\u7559\u73B0\u6709\u53EF\u89C1 card \u515C\u5E95\u3002\u4E0D\u8981\u5199 raw Feishu post/card JSON\u3002',
117467
+ '- response_surface: \u53EF\u9009\u8986\u76D6\u5B57\u6BB5,\u5F62\u5982 `{mode:"card"|"post"|"hybrid", primary?:"card"|"post", post?:{mentions:[{user_id,label?}]}, card?:{compact?:boolean, capabilities?:[...]}}`\u3002\u9ED8\u8BA4\u53EF\u4E0D\u5199;\u65E0\u663E\u5F0F card \u610F\u56FE\u65F6 bridge \u6309 post-first \u5904\u7406\u3002\u9700\u8981\u771F\u5B9E @ \u65F6\u628A\u76EE\u6807\u5199\u5165 `post.mentions`;\u4E0D\u8981\u5199 raw Feishu post/card JSON\u3002',
117452
117468
  "- scheduled reply / daily social ops review card \u7B49\u9700\u8981\u201C\u5E73\u53F0\u6B63\u6587 + \u5339\u914D\u56FE\u7247\u201D\u540C\u6BB5\u76F8\u90BB\u5C55\u793A\u7684\u573A\u666F,\u5E94\u5148\u53D6\u5F97\u5404\u56FE\u7247 `img_key`,\u518D\u5199 `content_blocks` \u4E3A `\u5E73\u53F0 markdown -> \u5BF9\u5E94 image -> \u4E0B\u4E2A\u5E73\u53F0 markdown -> \u5BF9\u5E94 image`;\u4E0D\u8981\u7528\u5355\u72EC\u8BDD\u9898\u56FE\u7247\u6D88\u606F\u6216\u5C3E\u90E8 `image_blocks` \u4EE3\u66FF\u9A8C\u6536\u9762\u3002",
117453
117469
  "- dev_url / mr_url / \u5176\u4F59\u4E1A\u52A1\u5B57\u6BB5:\u81EA\u7531\u5199\u5165,bridge \u4E0D\u611F\u77E5\u5176\u4E1A\u52A1\u542B\u4E49;\u8981\u8BA9\u8FD0\u8425\u770B\u5230,\u8BF7\u5199\u8FDB last_message",
117454
117470
  "- updated_at: ISO 8601 timestamp",
117455
117471
  "",
117456
- "**\u7EDD\u4E0D\u81EA\u5DF1 `lark-cli api PATCH .../im/v1/messages/...` \u6539\u5361\u7247** \u2014\u2014 \u90A3\u662F bridge \u7684\u6D3B;",
117457
- "\u81EA\u5DF1 PATCH \u4F1A\u548C bridge \u51B2\u7A81,\u8FD8\u4F1A\u8BA9\u672C\u8F6E turn \u4E0D\u7ED3\u675F\u3002\u4F60\u53EA\u5199 state.json,\u5361\u7247\u5916\u58F3\u548C\u7F51\u7EDC\u66F4\u65B0\u4EA4\u7ED9 bridge\u3002",
117458
- "\u5199\u5B8C state.json \u5C31**\u5E72\u51C0\u7ED3\u675F\u672C\u8F6E**(\u4F60\u7684\u8FDB\u7A0B\u9000\u51FA \u2192 bridge \u624D finalize \u5361\u7247;",
117459
- "\u6302\u7740\u4E0D\u9000\u51FA = \u5361\u7247\u6C38\u8FDC\u5361\u5728\u300C\u5904\u7406\u4E2D\u300D)\u3002",
117472
+ "**\u7EDD\u4E0D\u81EA\u5DF1 `lark-cli api PATCH/PUT .../im/v1/messages/...` \u6539 bridge \u7BA1\u7406\u7684 post/card** \u2014\u2014 \u90A3\u662F bridge \u7684\u6D3B;",
117473
+ "\u81EA\u5DF1\u53D1/\u6539\u4E3B\u56DE\u590D\u9762\u4F1A\u548C bridge \u7684 post \u7F16\u8F91\u3001\u5361\u7247 finalize\u3001\u6309\u94AE\u56DE\u4F20\u3001\u5D29\u6E83\u6062\u590D\u51B2\u7A81\u3002\u4F60\u53EA\u5199 state.json,\u7F51\u7EDC\u66F4\u65B0\u4EA4\u7ED9 bridge\u3002",
117474
+ "\u5199\u5B8C state.json \u5C31**\u5E72\u51C0\u7ED3\u675F\u672C\u8F6E**(\u4F60\u7684\u8FDB\u7A0B\u9000\u51FA \u2192 bridge \u624D\u628A post/card \u6536\u655B\u6210\u7EC8\u6001;",
117475
+ "\u6302\u7740\u4E0D\u9000\u51FA = \u8BDD\u9898\u6C38\u8FDC\u5361\u5728\u300C\u6B63\u5728\u5904\u7406\u2026\u300D)\u3002",
117460
117476
  "",
117461
117477
  "\u5361\u7247\u5C55\u793A\u539F\u5219:",
117462
- "- bridge \u53EF\u80FD\u5728\u8FD0\u884C\u4E2D\u5C55\u793A\u5DE5\u5177\u6458\u8981,\u8FD9\u662F\u901A\u7528\u8FDB\u5EA6\u4FE1\u53F7;\u6700\u7EC8\u6210\u529F\u5361\u7247\u4EE5\u4F60\u7684 `content_blocks`(\u82E5\u975E\u7A7A)\u6216 `last_message` \u4E3A\u4E3B\u3002",
117478
+ "- \u8FD0\u884C\u4E2D\u4E3B\u9762\u662F post,bridge \u901A\u8FC7\u6574\u6761\u5BCC\u6587\u672C\u66FF\u6362\u505A\u5206\u5757\u5237\u65B0;\u5B83\u4E0D\u662F\u9010\u5B57 token streaming,\u4E0D\u8981\u4F9D\u8D56\u5DE5\u5177\u6D41/\u65E5\u5FD7\u8868\u8FBE\u4E1A\u52A1\u9636\u6BB5\u3002",
117479
+ "- \u6700\u7EC8 post \u4EE5\u4F60\u7684 `last_message` \u4E3A\u4E3B;\u82E5\u5199\u4E86 `choices`\u3001`content_blocks` \u6216 `image_blocks`,bridge \u4F1A\u5728 post \u4E4B\u540E\u8865\u5361\u7247\u627F\u8F7D\u8FD9\u4E9B\u80FD\u529B\u3002",
117463
117480
  "- \u4E0D\u8981\u4F9D\u8D56 bridge \u4ECE\u8F93\u51FA\u91CC\u89E3\u6790\u4E1A\u52A1\u9636\u6BB5\u3001MR\u3001\u9884\u89C8\u5730\u5740\u6216\u4E0B\u4E00\u6B65\u52A8\u4F5C;\u9700\u8981\u5C55\u793A\u7684\u5185\u5BB9\u4F60\u81EA\u5DF1\u5199\u8FDB last_message\u3002",
117464
117481
  "- \u4E0D\u8981\u6C42\u56FA\u5B9A\u683C\u5F0F\u3002\u6839\u636E\u4EFB\u52A1\u9009\u62E9\u6700\u6E05\u695A\u7684\u8868\u8FBE:\u77ED\u7ED3\u8BBA\u3001\u5206\u70B9\u3001\u8868\u683C\u3001\u94FE\u63A5\u3001\u4E0B\u4E00\u6B65\u95EE\u9898\u90FD\u53EF\u4EE5\u3002",
117465
117482
  "- \u5982\u679C\u672C\u8F6E\u6D89\u53CA repo / \u4EE3\u7801 / \u6587\u6863\u4FEE\u6539,last_message \u5E94\u5305\u542B\u8DB3\u591F\u8BA9\u8FD0\u8425\u9A8C\u6536\u7684\u8BC1\u636E,\u4F8B\u5982\u4F60\u5B9E\u9645\u4F7F\u7528\u7684 workspace/repo\u3001\u5173\u952E diff \u6216\u94FE\u63A5\u3001\u8FD0\u884C\u8FC7\u7684\u6D4B\u8BD5/\u68C0\u67E5\u547D\u4EE4\u548C\u7ED3\u679C\u3002\u5177\u4F53\u8BC1\u636E\u7531\u4EFB\u52A1\u51B3\u5B9A;dogfood E2E \u7684\u4E25\u683C\u6E05\u5355\u53EA\u5728 dogfood guide \u4E2D\u8981\u6C42\u3002",
@@ -118259,7 +118276,7 @@ function defaultResponseSurfacePrototypeConfig() {
118259
118276
  enabled: true,
118260
118277
  allowed_chats: [],
118261
118278
  allowed_threads: [],
118262
- lazy_card_creation: false,
118279
+ lazy_card_creation: true,
118263
118280
  kill_switch: false,
118264
118281
  post_outbound_enabled: true,
118265
118282
  allow_agent_mentions: true,
@@ -118276,7 +118293,7 @@ var responseSurfacePrototypeConfigDefaults = () => ({
118276
118293
  enabled: true,
118277
118294
  allowed_chats: [],
118278
118295
  allowed_threads: [],
118279
- lazy_card_creation: false,
118296
+ lazy_card_creation: true,
118280
118297
  kill_switch: false,
118281
118298
  post_outbound_enabled: true,
118282
118299
  allow_agent_mentions: true,
@@ -118343,11 +118360,11 @@ var ResponseSurfacePrototypeConfigSchema = external_exports.object({
118343
118360
  */
118344
118361
  allowed_threads: external_exports.array(external_exports.string().min(1)).default([]),
118345
118362
  /**
118346
- * Dark-launch hook for PR2. This does not enable post-only output by itself;
118347
- * the bridge still requires a visible card fallback until post outbound
118348
- * exists and is explicitly enabled in a later PR.
118363
+ * Post-first gate. Default true means the bridge starts a lightweight post
118364
+ * as the live main surface when post outbound is available, and creates a
118365
+ * card only for fallback or card-only capabilities.
118349
118366
  */
118350
- lazy_card_creation: external_exports.boolean().default(false),
118367
+ lazy_card_creation: external_exports.boolean().default(true),
118351
118368
  /**
118352
118369
  * Runtime kill switch for emergency rollback. When true, every response
118353
118370
  * surface post path is treated as disabled even if enabled/allowlists are
@@ -119219,6 +119236,9 @@ function fullCard(input, reason) {
119219
119236
  function hasCardOnlyPayload(state) {
119220
119237
  return !!(state?.choices?.length || state?.image_blocks?.length || state?.content_blocks?.length);
119221
119238
  }
119239
+ function hasCardOnlyPayloadIn(input) {
119240
+ return !!(hasCardOnlyPayload(input.state) || input.baseCard.choices?.length || input.baseCard.imageBlocks?.length || input.baseCard.contentBlocks?.length);
119241
+ }
119222
119242
  function compactAuditCard(input, post) {
119223
119243
  const status = input.state?.status ?? (input.baseCard.success ? "ready" : "failed");
119224
119244
  const title = input.baseCard.titleOverride ?? "Post \u5DF2\u53D1\u9001";
@@ -119288,10 +119308,18 @@ async function existingLedgerEntry(input, idempotencyKey) {
119288
119308
  return ledger?.posts.find((post) => post.idempotencyKey === idempotencyKey) ?? null;
119289
119309
  }
119290
119310
  function sentResult(input, surface, post, reason) {
119311
+ if (hasCardOnlyPayloadIn(input)) {
119312
+ return {
119313
+ card: input.baseCard,
119314
+ reason: "post-sent-card-capability-required",
119315
+ visible: true,
119316
+ post
119317
+ };
119318
+ }
119291
119319
  if (surface.mode === "hybrid" || input.cardStarted) {
119292
119320
  return {
119293
119321
  card: compactAuditCard(input, post),
119294
- reason: reason === "post-ledger-already-sent" ? "post-ledger-already-sent" : surface.mode === "hybrid" ? "hybrid-post-sent-compact-card" : "post-sent",
119322
+ reason: reason === "post-ledger-already-sent" ? "post-ledger-already-sent" : surface.mode === "hybrid" ? "hybrid-post-sent-compact-card" : reason,
119295
119323
  visible: true,
119296
119324
  post
119297
119325
  };
@@ -119334,10 +119362,11 @@ function emitSurfaceObservation(input) {
119334
119362
  );
119335
119363
  }
119336
119364
  async function dispatchResponseSurfaceInner(input) {
119337
- const surface = input.state?.response_surface;
119338
- if (!surface || surface.mode === "card" || surface.primary === "card") {
119365
+ const declaredSurface = input.state?.response_surface;
119366
+ if (declaredSurface?.mode === "card" || declaredSurface?.primary === "card") {
119339
119367
  return fullCard(input, "legacy-card-mode");
119340
119368
  }
119369
+ const surface = declaredSurface ?? { mode: "post", primary: "post" };
119341
119370
  const cfg = input.prototypeConfig;
119342
119371
  if (!cfg?.enabled) return fullCard(input, "prototype-disabled");
119343
119372
  if (cfg.kill_switch) return fullCard(input, "kill-switch-active");
@@ -119356,9 +119385,6 @@ async function dispatchResponseSurfaceInner(input) {
119356
119385
  if (!input.visibleFallbackAvailable) {
119357
119386
  return fullCard(input, "visible-fallback-unavailable");
119358
119387
  }
119359
- if (hasCardOnlyPayload(input.state)) {
119360
- return fullCard(input, "card-capability-required");
119361
- }
119362
119388
  if (!input.postLedgerAvailable || !input.worktreePath) {
119363
119389
  return fullCard(input, "post-ledger-unavailable");
119364
119390
  }
@@ -119381,6 +119407,16 @@ async function dispatchResponseSurfaceInner(input) {
119381
119407
  const now = input.now?.() ?? (/* @__PURE__ */ new Date()).toISOString();
119382
119408
  if (blockedMention) {
119383
119409
  const policyError = `mention target is not allowed by response surface policy: ${blockedMention.user_id}`;
119410
+ if (input.livePost) {
119411
+ try {
119412
+ await input.postClient.updatePost(input.livePost.messageId, buildPostContent({ text }));
119413
+ } catch (err) {
119414
+ console.warn(
119415
+ "[surface_dispatch] live post policy-blocked cleanup update failed:",
119416
+ err
119417
+ );
119418
+ }
119419
+ }
119384
119420
  await writeLedger(
119385
119421
  input,
119386
119422
  newLedgerEntry({
@@ -119415,7 +119451,7 @@ async function dispatchResponseSurfaceInner(input) {
119415
119451
  }))
119416
119452
  });
119417
119453
  const contentDigest = digestPostContent(content);
119418
- const idempotencyKey = derivePostIdempotencyKey({
119454
+ const idempotencyKey = input.livePost?.idempotencyKey ?? derivePostIdempotencyKey({
119419
119455
  botId: input.facts.botId,
119420
119456
  threadId: input.facts.threadId,
119421
119457
  triggerMessageId: input.facts.triggerMessageId,
@@ -119468,7 +119504,7 @@ async function dispatchResponseSurfaceInner(input) {
119468
119504
  }
119469
119505
  };
119470
119506
  }
119471
- const budget = input.postBudget?.reserve();
119507
+ const budget = input.livePost ? void 0 : input.postBudget?.reserve();
119472
119508
  if (budget && !budget.allowed) {
119473
119509
  return {
119474
119510
  ...fullCard(input, "post-rate-limit-exhausted"),
@@ -119502,7 +119538,7 @@ async function dispatchResponseSurfaceInner(input) {
119502
119538
  })
119503
119539
  );
119504
119540
  try {
119505
- const sent = await input.postClient.createPostReply(input.facts.replyToMessageId, content, {
119541
+ const sent = input.livePost ? await input.postClient.updatePost(input.livePost.messageId, content) : await input.postClient.createPostReply(input.facts.replyToMessageId, content, {
119506
119542
  replyInThread: input.facts.replyInThread,
119507
119543
  idempotencyKey
119508
119544
  });
@@ -119528,7 +119564,10 @@ async function dispatchResponseSurfaceInner(input) {
119528
119564
  })
119529
119565
  );
119530
119566
  const post = { idempotencyKey, messageId: sent.messageId, role };
119531
- return { ...sentResult(input, surface, post, "post-sent"), budget };
119567
+ return {
119568
+ ...sentResult(input, surface, post, input.livePost ? "post-updated" : "post-sent"),
119569
+ budget
119570
+ };
119532
119571
  } catch (err) {
119533
119572
  const failedAt = input.now?.() ?? (/* @__PURE__ */ new Date()).toISOString();
119534
119573
  const error = err instanceof Error ? err.message : String(err);
@@ -119581,6 +119620,122 @@ async function dispatchResponseSurface(input) {
119581
119620
  return result;
119582
119621
  }
119583
119622
 
119623
+ // src/bridge/postProgress.ts
119624
+ var DEFAULT_PLACEHOLDER_TEXT = "\u6B63\u5728\u5904\u7406\u2026";
119625
+ var DEFAULT_PATCH_INTERVAL_MS = 1500;
119626
+ var DEFAULT_MAX_PROGRESS_EDITS = 16;
119627
+ function normalizeText2(text) {
119628
+ return text.replace(/\r\n/g, "\n").trim();
119629
+ }
119630
+ function progressText(raw, fallback) {
119631
+ const text = normalizeText2(raw);
119632
+ if (!text) return fallback;
119633
+ return text.length > 3500 ? `${text.slice(0, 3500)}
119634
+
119635
+ _\u5904\u7406\u4E2D\u5185\u5BB9\u8F83\u957F\uFF0C\u7EC8\u7A3F\u4F1A\u6536\u655B\u3002_` : text;
119636
+ }
119637
+ var LivePostProgressHandle = class {
119638
+ messageId;
119639
+ idempotencyKey;
119640
+ role = "primary";
119641
+ postClient;
119642
+ patchIntervalMs;
119643
+ maxProgressEdits;
119644
+ initialText;
119645
+ textBuffer = "";
119646
+ lastPatchedText = "";
119647
+ progressEdits = 0;
119648
+ pendingPatch = null;
119649
+ inFlight = Promise.resolve();
119650
+ closed = false;
119651
+ constructor(opts) {
119652
+ this.postClient = opts.postClient;
119653
+ this.messageId = opts.messageId;
119654
+ this.idempotencyKey = opts.idempotencyKey;
119655
+ this.initialText = opts.initialText;
119656
+ this.lastPatchedText = opts.initialText;
119657
+ this.patchIntervalMs = opts.patchIntervalMs;
119658
+ this.maxProgressEdits = opts.maxProgressEdits;
119659
+ }
119660
+ handle(event) {
119661
+ if (this.closed) return;
119662
+ if (event.type !== "text_delta") return;
119663
+ this.textBuffer += event.text;
119664
+ this.schedulePatch();
119665
+ }
119666
+ async finalize(opts) {
119667
+ await this.drain();
119668
+ const finalText = normalizeText2(opts.text) || this.initialText;
119669
+ await this.update(finalText, opts.mentions);
119670
+ }
119671
+ async drain() {
119672
+ this.closed = true;
119673
+ if (this.pendingPatch) {
119674
+ clearTimeout(this.pendingPatch);
119675
+ this.pendingPatch = null;
119676
+ }
119677
+ await this.inFlight;
119678
+ }
119679
+ close() {
119680
+ this.closed = true;
119681
+ if (this.pendingPatch) {
119682
+ clearTimeout(this.pendingPatch);
119683
+ this.pendingPatch = null;
119684
+ }
119685
+ }
119686
+ schedulePatch() {
119687
+ if (this.pendingPatch || this.progressEdits >= this.maxProgressEdits) return;
119688
+ this.pendingPatch = setTimeout(() => {
119689
+ this.pendingPatch = null;
119690
+ void this.patchProgress();
119691
+ }, this.patchIntervalMs);
119692
+ this.pendingPatch.unref?.();
119693
+ }
119694
+ async patchProgress() {
119695
+ if (this.closed || this.progressEdits >= this.maxProgressEdits) return;
119696
+ const nextText = progressText(this.textBuffer, this.initialText);
119697
+ if (nextText === this.lastPatchedText) return;
119698
+ this.progressEdits += 1;
119699
+ this.inFlight = this.inFlight.then(() => this.update(nextText)).catch((err) => {
119700
+ console.warn("[post_progress] progress update failed (continuing):", err);
119701
+ });
119702
+ await this.inFlight;
119703
+ }
119704
+ async update(text, mentions = []) {
119705
+ const content = buildPostContent({ text, mentions });
119706
+ await this.postClient.updatePost(this.messageId, content);
119707
+ this.lastPatchedText = text;
119708
+ }
119709
+ };
119710
+ async function createPostProgressHandle(opts) {
119711
+ const role = "primary";
119712
+ const initialText = normalizeText2(opts.initialText ?? DEFAULT_PLACEHOLDER_TEXT);
119713
+ const idempotencyKey = derivePostIdempotencyKey({
119714
+ botId: opts.facts.botId,
119715
+ threadId: opts.facts.threadId,
119716
+ triggerMessageId: opts.facts.triggerMessageId,
119717
+ role,
119718
+ logicalIndex: 0,
119719
+ contentDigest: digestPostContent("live-progress-placeholder")
119720
+ });
119721
+ const sent = await opts.postClient.createPostReply(
119722
+ opts.replyToMessageId,
119723
+ buildPostContent({ text: initialText }),
119724
+ {
119725
+ replyInThread: opts.replyInThread,
119726
+ idempotencyKey
119727
+ }
119728
+ );
119729
+ return new LivePostProgressHandle({
119730
+ postClient: opts.postClient,
119731
+ messageId: sent.messageId,
119732
+ idempotencyKey,
119733
+ initialText,
119734
+ patchIntervalMs: opts.patchIntervalMs ?? DEFAULT_PATCH_INTERVAL_MS,
119735
+ maxProgressEdits: opts.maxProgressEdits ?? DEFAULT_MAX_PROGRESS_EDITS
119736
+ });
119737
+ }
119738
+
119584
119739
  // src/bridge/postBudget.ts
119585
119740
  function scopeKey(scope) {
119586
119741
  return `${scope.botId}\0${scope.chatId}\0${scope.threadId}`;
@@ -119962,6 +120117,8 @@ var BridgeHandler = class {
119962
120117
  visibleFallbackAvailable: true
119963
120118
  });
119964
120119
  let card;
120120
+ let progressPost;
120121
+ let progressPostStartFailed = false;
119965
120122
  if (surfaceController.shouldStartCardImmediately()) {
119966
120123
  try {
119967
120124
  card = await this.deps.cardRenderer.start(messageId, { replyInThread, threadId });
@@ -120125,6 +120282,55 @@ var BridgeHandler = class {
120125
120282
  } catch (err) {
120126
120283
  console.warn("[bridge.handler] ensureStateFile failed (continuing):", err);
120127
120284
  }
120285
+ if (!card && postOutboundAvailable && this.deps.postClient) {
120286
+ const budget = prototypeConfig ? this.responseSurfacePostBudget.reserve({
120287
+ scope: {
120288
+ botId: this.deps.botConfig?.id ?? "v1-default",
120289
+ chatId: parsed.chatId,
120290
+ threadId
120291
+ },
120292
+ maxPosts: prototypeConfig.max_posts_per_window,
120293
+ windowMs: prototypeConfig.post_window_ms
120294
+ }) : void 0;
120295
+ if (budget?.allowed === false) {
120296
+ console.warn("[bridge.handler] post progress budget exhausted; using card fallback");
120297
+ progressPostStartFailed = true;
120298
+ } else {
120299
+ try {
120300
+ progressPost = await createPostProgressHandle({
120301
+ postClient: this.deps.postClient,
120302
+ replyToMessageId: messageId,
120303
+ replyInThread,
120304
+ facts: {
120305
+ botId: this.deps.botConfig?.id ?? "v1-default",
120306
+ threadId,
120307
+ triggerMessageId: messageId
120308
+ },
120309
+ initialText: "\u6B63\u5728\u5904\u7406\u2026"
120310
+ });
120311
+ await this.deps.client.removeProcessingReaction?.(messageId);
120312
+ await recordEvent({
120313
+ status: "running",
120314
+ startedAt: (/* @__PURE__ */ new Date()).toISOString(),
120315
+ appendPath: "\u5DF2\u521B\u5EFA post",
120316
+ reason: "response surface \u4F7F\u7528 post \u4F5C\u4E3A\u672C\u8F6E\u4E3B\u56DE\u590D\u9762\u3002"
120317
+ });
120318
+ } catch (err) {
120319
+ progressPostStartFailed = true;
120320
+ console.warn("[bridge.handler] create progress post failed; using card fallback:", err);
120321
+ }
120322
+ }
120323
+ }
120324
+ if (!card && progressPostStartFailed) {
120325
+ card = await this.deps.cardRenderer.start(messageId, { replyInThread, threadId });
120326
+ await this.deps.client.removeProcessingReaction?.(messageId);
120327
+ await recordEvent({
120328
+ status: "running",
120329
+ startedAt: (/* @__PURE__ */ new Date()).toISOString(),
120330
+ appendPath: "post \u5931\u8D25\uFF0C\u5DF2\u521B\u5EFA\u5361\u7247",
120331
+ reason: "post \u4E3B\u9762\u521B\u5EFA\u5931\u8D25\uFF0Cbridge \u4F7F\u7528\u53EF\u89C1\u5361\u7247\u515C\u5E95\u3002"
120332
+ });
120333
+ }
120128
120334
  if (card) {
120129
120335
  try {
120130
120336
  await writeCardFile(worktreePath, {
@@ -120199,6 +120405,7 @@ var BridgeHandler = class {
120199
120405
  try {
120200
120406
  for await (const ev of handle.events) {
120201
120407
  if (card) card.handle(ev);
120408
+ else progressPost?.handle(ev);
120202
120409
  if (ev.type === "system_init") {
120203
120410
  sessionId = ev.sessionId;
120204
120411
  }
@@ -120266,6 +120473,7 @@ var BridgeHandler = class {
120266
120473
  imageBlocks: reportedState?.image_blocks,
120267
120474
  contentBlocks: reportedState?.content_blocks
120268
120475
  };
120476
+ await progressPost?.drain();
120269
120477
  const surfaceDispatch = await dispatchResponseSurface({
120270
120478
  state: reportedState,
120271
120479
  prototypeConfig,
@@ -120280,10 +120488,15 @@ var BridgeHandler = class {
120280
120488
  worktreePath,
120281
120489
  baseCard: baseCardPayload,
120282
120490
  cardStarted: !!card,
120283
- postOutboundAvailable,
120491
+ postOutboundAvailable: postOutboundAvailable && !progressPostStartFailed,
120284
120492
  postLedgerAvailable: true,
120285
120493
  visibleFallbackAvailable: true,
120286
120494
  postClient: this.deps.postClient,
120495
+ livePost: progressPost ? {
120496
+ messageId: progressPost.messageId,
120497
+ idempotencyKey: progressPost.idempotencyKey,
120498
+ role: progressPost.role
120499
+ } : void 0,
120287
120500
  postBudget: prototypeConfig ? {
120288
120501
  reserve: () => this.responseSurfacePostBudget.reserve({
120289
120502
  scope: {
@@ -120387,6 +120600,23 @@ var BridgeHandler = class {
120387
120600
  reason: String(err)
120388
120601
  });
120389
120602
  settle(false);
120603
+ if (!card && progressPost) {
120604
+ try {
120605
+ await progressPost.finalize({
120606
+ text: `\u6267\u884C\u5931\u8D25: ${String(err)}`
120607
+ });
120608
+ } catch (postFinalizeErr) {
120609
+ console.error(
120610
+ "[bridge.handler] progress post failure update failed; creating card fallback:",
120611
+ postFinalizeErr
120612
+ );
120613
+ try {
120614
+ card = await this.deps.cardRenderer.start(messageId, { replyInThread, threadId });
120615
+ } catch (cardStartErr) {
120616
+ console.error("[bridge.handler] failure card start also failed:", cardStartErr);
120617
+ }
120618
+ }
120619
+ }
120390
120620
  if (card) {
120391
120621
  try {
120392
120622
  await card.finalize({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "larkway",
3
- "version": "0.3.20",
3
+ "version": "0.3.21",
4
4
  "description": "Thin bridge: Feishu thread to local Claude Code CLI",
5
5
  "license": "MIT",
6
6
  "author": "Chuck Wu (chuckwu0)",