opencode-feishu 1.1.0 → 1.3.0

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/index.js CHANGED
@@ -1,8 +1,8 @@
1
1
  import fs, { existsSync, readFileSync } from 'fs';
2
2
  import { join } from 'path';
3
+ import url, { fileURLToPath } from 'url';
3
4
  import { homedir } from 'os';
4
- import crypto2 from 'crypto';
5
- import url from 'url';
5
+ import crypto2, { randomUUID } from 'crypto';
6
6
  import http from 'http';
7
7
  import https from 'https';
8
8
  import http2 from 'http2';
@@ -13,6 +13,8 @@ import { EventEmitter } from 'events';
13
13
  import qs from 'querystring';
14
14
  import WebSocket from 'ws';
15
15
  import { HttpsProxyAgent } from 'https-proxy-agent';
16
+ import { tool } from '@opencode-ai/plugin';
17
+ import { createOpencodeClient } from '@opencode-ai/sdk/v2/client';
16
18
 
17
19
  var __create = Object.create;
18
20
  var __defProp = Object.defineProperty;
@@ -11587,7 +11589,7 @@ var require_object_inspect = __commonJS({
11587
11589
  "double": /(["\\])/g,
11588
11590
  single: /(['\\])/g
11589
11591
  };
11590
- module2.exports = function inspect_(obj, options, depth, seen2) {
11592
+ module2.exports = function inspect_(obj, options, depth, seen) {
11591
11593
  var opts = options || {};
11592
11594
  if (has(opts, "quoteStyle") && !has(quotes, opts.quoteStyle)) {
11593
11595
  throw new TypeError('option "quoteStyle" must be "single" or "double"');
@@ -11637,15 +11639,15 @@ var require_object_inspect = __commonJS({
11637
11639
  return isArray2(obj) ? "[Array]" : "[Object]";
11638
11640
  }
11639
11641
  var indent = getIndent(opts, depth);
11640
- if (typeof seen2 === "undefined") {
11641
- seen2 = [];
11642
- } else if (indexOf(seen2, obj) >= 0) {
11642
+ if (typeof seen === "undefined") {
11643
+ seen = [];
11644
+ } else if (indexOf(seen, obj) >= 0) {
11643
11645
  return "[Circular]";
11644
11646
  }
11645
11647
  function inspect(value, from, noIndent) {
11646
11648
  if (from) {
11647
- seen2 = $arrSlice.call(seen2);
11648
- seen2.push(from);
11649
+ seen = $arrSlice.call(seen);
11650
+ seen.push(from);
11649
11651
  }
11650
11652
  if (noIndent) {
11651
11653
  var newOpts = {
@@ -11654,9 +11656,9 @@ var require_object_inspect = __commonJS({
11654
11656
  if (has(opts, "quoteStyle")) {
11655
11657
  newOpts.quoteStyle = opts.quoteStyle;
11656
11658
  }
11657
- return inspect_(value, newOpts, depth + 1, seen2);
11659
+ return inspect_(value, newOpts, depth + 1, seen);
11658
11660
  }
11659
- return inspect_(value, opts, depth + 1, seen2);
11661
+ return inspect_(value, opts, depth + 1, seen);
11660
11662
  }
11661
11663
  if (typeof obj === "function" && !isRegExp2(obj)) {
11662
11664
  var name = nameOf(obj);
@@ -13718,7 +13720,7 @@ var require_lodash2 = __commonJS({
13718
13720
  if (stacked && stack.get(other)) {
13719
13721
  return stacked == other;
13720
13722
  }
13721
- var index = -1, result = true, seen2 = bitmask & UNORDERED_COMPARE_FLAG ? new SetCache() : void 0;
13723
+ var index = -1, result = true, seen = bitmask & UNORDERED_COMPARE_FLAG ? new SetCache() : void 0;
13722
13724
  stack.set(array2, other);
13723
13725
  stack.set(other, array2);
13724
13726
  while (++index < arrLength) {
@@ -13733,10 +13735,10 @@ var require_lodash2 = __commonJS({
13733
13735
  result = false;
13734
13736
  break;
13735
13737
  }
13736
- if (seen2) {
13738
+ if (seen) {
13737
13739
  if (!arraySome(other, function(othValue2, othIndex) {
13738
- if (!seen2.has(othIndex) && (arrValue === othValue2 || equalFunc(arrValue, othValue2, customizer, bitmask, stack))) {
13739
- return seen2.add(othIndex);
13740
+ if (!seen.has(othIndex) && (arrValue === othValue2 || equalFunc(arrValue, othValue2, customizer, bitmask, stack))) {
13741
+ return seen.add(othIndex);
13740
13742
  }
13741
13743
  })) {
13742
13744
  result = false;
@@ -109648,14 +109650,14 @@ function initializeContext(params) {
109648
109650
  function process2(schema, ctx, _params = { path: [], schemaPath: [] }) {
109649
109651
  var _a2;
109650
109652
  const def = schema._zod.def;
109651
- const seen2 = ctx.seen.get(schema);
109652
- if (seen2) {
109653
- seen2.count++;
109653
+ const seen = ctx.seen.get(schema);
109654
+ if (seen) {
109655
+ seen.count++;
109654
109656
  const isCycle = _params.schemaPath.includes(schema);
109655
109657
  if (isCycle) {
109656
- seen2.cycle = _params.path;
109658
+ seen.cycle = _params.path;
109657
109659
  }
109658
- return seen2.schema;
109660
+ return seen.schema;
109659
109661
  }
109660
109662
  const result = { schema: {}, count: 1, cycle: void 0, path: _params.path };
109661
109663
  ctx.seen.set(schema, result);
@@ -109738,12 +109740,12 @@ function extractDefs(ctx, schema) {
109738
109740
  if (entry[1].schema.$ref) {
109739
109741
  return;
109740
109742
  }
109741
- const seen2 = entry[1];
109743
+ const seen = entry[1];
109742
109744
  const { ref, defId } = makeURI(entry);
109743
- seen2.def = { ...seen2.schema };
109745
+ seen.def = { ...seen.schema };
109744
109746
  if (defId)
109745
- seen2.defId = defId;
109746
- const schema2 = seen2.schema;
109747
+ seen.defId = defId;
109748
+ const schema2 = seen.schema;
109747
109749
  for (const key in schema2) {
109748
109750
  delete schema2[key];
109749
109751
  }
@@ -109751,16 +109753,16 @@ function extractDefs(ctx, schema) {
109751
109753
  };
109752
109754
  if (ctx.cycles === "throw") {
109753
109755
  for (const entry of ctx.seen.entries()) {
109754
- const seen2 = entry[1];
109755
- if (seen2.cycle) {
109756
- throw new Error(`Cycle detected: #/${seen2.cycle?.join("/")}/<root>
109756
+ const seen = entry[1];
109757
+ if (seen.cycle) {
109758
+ throw new Error(`Cycle detected: #/${seen.cycle?.join("/")}/<root>
109757
109759
 
109758
109760
  Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.`);
109759
109761
  }
109760
109762
  }
109761
109763
  }
109762
109764
  for (const entry of ctx.seen.entries()) {
109763
- const seen2 = entry[1];
109765
+ const seen = entry[1];
109764
109766
  if (schema === entry[0]) {
109765
109767
  extractToDef(entry);
109766
109768
  continue;
@@ -109777,11 +109779,11 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
109777
109779
  extractToDef(entry);
109778
109780
  continue;
109779
109781
  }
109780
- if (seen2.cycle) {
109782
+ if (seen.cycle) {
109781
109783
  extractToDef(entry);
109782
109784
  continue;
109783
109785
  }
109784
- if (seen2.count > 1) {
109786
+ if (seen.count > 1) {
109785
109787
  if (ctx.reused === "ref") {
109786
109788
  extractToDef(entry);
109787
109789
  continue;
@@ -109794,13 +109796,13 @@ function finalize(ctx, schema) {
109794
109796
  if (!root)
109795
109797
  throw new Error("Unprocessed schema. This is a bug in Zod.");
109796
109798
  const flattenRef = (zodSchema) => {
109797
- const seen2 = ctx.seen.get(zodSchema);
109798
- if (seen2.ref === null)
109799
+ const seen = ctx.seen.get(zodSchema);
109800
+ if (seen.ref === null)
109799
109801
  return;
109800
- const schema2 = seen2.def ?? seen2.schema;
109802
+ const schema2 = seen.def ?? seen.schema;
109801
109803
  const _cached = { ...schema2 };
109802
- const ref = seen2.ref;
109803
- seen2.ref = null;
109804
+ const ref = seen.ref;
109805
+ seen.ref = null;
109804
109806
  if (ref) {
109805
109807
  flattenRef(ref);
109806
109808
  const refSeen = ctx.seen.get(ref);
@@ -109852,7 +109854,7 @@ function finalize(ctx, schema) {
109852
109854
  ctx.override({
109853
109855
  zodSchema,
109854
109856
  jsonSchema: schema2,
109855
- path: seen2.path ?? []
109857
+ path: seen.path ?? []
109856
109858
  });
109857
109859
  };
109858
109860
  for (const entry of [...ctx.seen.entries()].reverse()) {
@@ -109875,9 +109877,9 @@ function finalize(ctx, schema) {
109875
109877
  Object.assign(result, root.def ?? root.schema);
109876
109878
  const defs = ctx.external?.defs ?? {};
109877
109879
  for (const entry of ctx.seen.entries()) {
109878
- const seen2 = entry[1];
109879
- if (seen2.def && seen2.defId) {
109880
- defs[seen2.defId] = seen2.def;
109880
+ const seen = entry[1];
109881
+ if (seen.def && seen.defId) {
109882
+ defs[seen.defId] = seen.def;
109881
109883
  }
109882
109884
  }
109883
109885
  if (ctx.external) ; else {
@@ -110371,9 +110373,9 @@ var recordProcessor = (schema, ctx, _json, params) => {
110371
110373
  var nullableProcessor = (schema, ctx, json2, params) => {
110372
110374
  const def = schema._zod.def;
110373
110375
  const inner = process2(def.innerType, ctx, params);
110374
- const seen2 = ctx.seen.get(schema);
110376
+ const seen = ctx.seen.get(schema);
110375
110377
  if (ctx.target === "openapi-3.0") {
110376
- seen2.ref = def.innerType;
110378
+ seen.ref = def.innerType;
110377
110379
  json2.nullable = true;
110378
110380
  } else {
110379
110381
  json2.anyOf = [inner, { type: "null" }];
@@ -110382,29 +110384,29 @@ var nullableProcessor = (schema, ctx, json2, params) => {
110382
110384
  var nonoptionalProcessor = (schema, ctx, _json, params) => {
110383
110385
  const def = schema._zod.def;
110384
110386
  process2(def.innerType, ctx, params);
110385
- const seen2 = ctx.seen.get(schema);
110386
- seen2.ref = def.innerType;
110387
+ const seen = ctx.seen.get(schema);
110388
+ seen.ref = def.innerType;
110387
110389
  };
110388
110390
  var defaultProcessor = (schema, ctx, json2, params) => {
110389
110391
  const def = schema._zod.def;
110390
110392
  process2(def.innerType, ctx, params);
110391
- const seen2 = ctx.seen.get(schema);
110392
- seen2.ref = def.innerType;
110393
+ const seen = ctx.seen.get(schema);
110394
+ seen.ref = def.innerType;
110393
110395
  json2.default = JSON.parse(JSON.stringify(def.defaultValue));
110394
110396
  };
110395
110397
  var prefaultProcessor = (schema, ctx, json2, params) => {
110396
110398
  const def = schema._zod.def;
110397
110399
  process2(def.innerType, ctx, params);
110398
- const seen2 = ctx.seen.get(schema);
110399
- seen2.ref = def.innerType;
110400
+ const seen = ctx.seen.get(schema);
110401
+ seen.ref = def.innerType;
110400
110402
  if (ctx.io === "input")
110401
110403
  json2._prefault = JSON.parse(JSON.stringify(def.defaultValue));
110402
110404
  };
110403
110405
  var catchProcessor = (schema, ctx, json2, params) => {
110404
110406
  const def = schema._zod.def;
110405
110407
  process2(def.innerType, ctx, params);
110406
- const seen2 = ctx.seen.get(schema);
110407
- seen2.ref = def.innerType;
110408
+ const seen = ctx.seen.get(schema);
110409
+ seen.ref = def.innerType;
110408
110410
  let catchValue;
110409
110411
  try {
110410
110412
  catchValue = def.catchValue(void 0);
@@ -110417,33 +110419,33 @@ var pipeProcessor = (schema, ctx, _json, params) => {
110417
110419
  const def = schema._zod.def;
110418
110420
  const innerType = ctx.io === "input" ? def.in._zod.def.type === "transform" ? def.out : def.in : def.out;
110419
110421
  process2(innerType, ctx, params);
110420
- const seen2 = ctx.seen.get(schema);
110421
- seen2.ref = innerType;
110422
+ const seen = ctx.seen.get(schema);
110423
+ seen.ref = innerType;
110422
110424
  };
110423
110425
  var readonlyProcessor = (schema, ctx, json2, params) => {
110424
110426
  const def = schema._zod.def;
110425
110427
  process2(def.innerType, ctx, params);
110426
- const seen2 = ctx.seen.get(schema);
110427
- seen2.ref = def.innerType;
110428
+ const seen = ctx.seen.get(schema);
110429
+ seen.ref = def.innerType;
110428
110430
  json2.readOnly = true;
110429
110431
  };
110430
110432
  var promiseProcessor = (schema, ctx, _json, params) => {
110431
110433
  const def = schema._zod.def;
110432
110434
  process2(def.innerType, ctx, params);
110433
- const seen2 = ctx.seen.get(schema);
110434
- seen2.ref = def.innerType;
110435
+ const seen = ctx.seen.get(schema);
110436
+ seen.ref = def.innerType;
110435
110437
  };
110436
110438
  var optionalProcessor = (schema, ctx, _json, params) => {
110437
110439
  const def = schema._zod.def;
110438
110440
  process2(def.innerType, ctx, params);
110439
- const seen2 = ctx.seen.get(schema);
110440
- seen2.ref = def.innerType;
110441
+ const seen = ctx.seen.get(schema);
110442
+ seen.ref = def.innerType;
110441
110443
  };
110442
110444
  var lazyProcessor = (schema, ctx, _json, params) => {
110443
110445
  const innerType = schema._zod.innerType;
110444
110446
  process2(innerType, ctx, params);
110445
- const seen2 = ctx.seen.get(schema);
110446
- seen2.ref = innerType;
110447
+ const seen = ctx.seen.get(schema);
110448
+ seen.ref = innerType;
110447
110449
  };
110448
110450
  var allProcessors = {
110449
110451
  string: stringProcessor,
@@ -112507,8 +112509,8 @@ config(en_default());
112507
112509
  // src/types.ts
112508
112510
  var AutoPromptSchema = external_exports.object({
112509
112511
  enabled: external_exports.boolean().default(false),
112510
- intervalSeconds: external_exports.number().int().positive().default(30),
112511
- maxIterations: external_exports.number().int().positive().default(10),
112512
+ intervalSeconds: external_exports.number().int().positive().max(300).default(30),
112513
+ maxIterations: external_exports.number().int().positive().max(100).default(10),
112512
112514
  message: external_exports.string().min(1).default("\u8BF7\u540C\u6B65\u5F53\u524D\u8FDB\u5EA6\uFF0C\u5982\u9700\u5E2E\u52A9\u8BF7\u8BF4\u660E"),
112513
112515
  idleThreshold: external_exports.number().int().min(1).default(2),
112514
112516
  idleMaxLength: external_exports.number().int().min(10).default(50)
@@ -112516,10 +112518,10 @@ var AutoPromptSchema = external_exports.object({
112516
112518
  var FeishuConfigSchema = external_exports.object({
112517
112519
  appId: external_exports.string().min(1, "appId \u4E0D\u80FD\u4E3A\u7A7A"),
112518
112520
  appSecret: external_exports.string().min(1, "appSecret \u4E0D\u80FD\u4E3A\u7A7A"),
112519
- timeout: external_exports.number().int().positive().default(12e4),
112521
+ timeout: external_exports.number().int().positive().max(6e5).default(12e4),
112520
112522
  thinkingDelay: external_exports.number().int().nonnegative().default(2500),
112521
112523
  logLevel: external_exports.enum(["fatal", "error", "warn", "info", "debug", "trace"]).default("info"),
112522
- maxHistoryMessages: external_exports.number().int().positive().default(200),
112524
+ maxHistoryMessages: external_exports.number().int().positive().max(500).default(200),
112523
112525
  pollInterval: external_exports.number().int().positive().default(1e3),
112524
112526
  stablePolls: external_exports.number().int().positive().default(3),
112525
112527
  dedupTtl: external_exports.number().int().positive().default(10 * 60 * 1e3),
@@ -112623,128 +112625,86 @@ var CardKitClient = class {
112623
112625
  }
112624
112626
  };
112625
112627
 
112626
- // src/feishu/card-builder.ts
112627
- function buildPermissionCard(request) {
112628
- const permission = String(request.permission ?? "unknown");
112629
- const patterns = Array.isArray(request.patterns) ? request.patterns.map(String) : [];
112630
- const requestId = String(request.id ?? "");
112631
- const patternsText = patterns.length > 0 ? patterns.map((p) => `- ${p}`).join("\n") : "\uFF08\u65E0\u5177\u4F53\u8DEF\u5F84\uFF09";
112632
- return {
112633
- type: "card_kit",
112634
- data: {
112635
- schema: "2.0",
112636
- config: { wide_screen_mode: true },
112637
- header: {
112638
- title: { tag: "plain_text", content: `\u{1F510} \u6743\u9650\u8BF7\u6C42: ${permission}` },
112639
- template: "orange"
112640
- },
112641
- body: {
112642
- elements: [
112643
- {
112644
- tag: "markdown",
112645
- content: `AI \u8BF7\u6C42\u4EE5\u4E0B\u6743\u9650:
112646
-
112647
- ${patternsText}`
112648
- },
112649
- {
112650
- tag: "action",
112651
- actions: [
112652
- {
112653
- tag: "button",
112654
- text: { tag: "plain_text", content: "\u2705 \u5141\u8BB8\u4E00\u6B21" },
112655
- type: "primary",
112656
- value: JSON.stringify({ action: "permission_reply", requestId, reply: "once" })
112657
- },
112658
- {
112659
- tag: "button",
112660
- text: { tag: "plain_text", content: "\u{1F513} \u59CB\u7EC8\u5141\u8BB8" },
112661
- type: "default",
112662
- value: JSON.stringify({ action: "permission_reply", requestId, reply: "always" })
112663
- },
112664
- {
112665
- tag: "button",
112666
- text: { tag: "plain_text", content: "\u274C \u62D2\u7EDD" },
112667
- type: "danger",
112668
- value: JSON.stringify({ action: "permission_reply", requestId, reply: "reject" })
112669
- }
112670
- ]
112671
- }
112672
- ]
112673
- }
112628
+ // src/utils/ttl-map.ts
112629
+ var TtlMap = class {
112630
+ constructor(defaultTtlMs) {
112631
+ this.defaultTtlMs = defaultTtlMs;
112632
+ }
112633
+ data = /* @__PURE__ */ new Map();
112634
+ timers = /* @__PURE__ */ new Map();
112635
+ get(key) {
112636
+ return this.data.get(key);
112637
+ }
112638
+ has(key) {
112639
+ return this.data.has(key);
112640
+ }
112641
+ set(key, value, ttlMs) {
112642
+ this.delete(key);
112643
+ this.data.set(key, value);
112644
+ const timer = setTimeout(() => {
112645
+ this.data.delete(key);
112646
+ this.timers.delete(key);
112647
+ }, ttlMs ?? this.defaultTtlMs);
112648
+ timer.unref();
112649
+ this.timers.set(key, timer);
112650
+ }
112651
+ delete(key) {
112652
+ const timer = this.timers.get(key);
112653
+ if (timer) {
112654
+ clearTimeout(timer);
112655
+ this.timers.delete(key);
112674
112656
  }
112675
- };
112657
+ this.data.delete(key);
112658
+ }
112659
+ };
112660
+
112661
+ // src/feishu/session-chat-map.ts
112662
+ var sessionToChat = new TtlMap(24 * 60 * 60 * 1e3);
112663
+ function registerSessionChat(sessionId, chatId, chatType) {
112664
+ sessionToChat.set(sessionId, { chatId, chatType });
112676
112665
  }
112677
- function buildQuestionCard(request) {
112678
- const questions = Array.isArray(request.questions) ? request.questions : [];
112679
- const requestId = String(request.id ?? "");
112680
- const q = questions[0];
112681
- const header = String(q?.header ?? "AI \u63D0\u95EE");
112682
- const questionText = String(q?.question ?? "\u8BF7\u9009\u62E9");
112683
- const options = Array.isArray(q?.options) ? q.options : [];
112684
- const buttons = options.map((opt, idx) => ({
112685
- tag: "button",
112686
- text: { tag: "plain_text", content: String(opt.label ?? opt.value ?? `\u9009\u9879 ${idx + 1}`) },
112687
- type: idx === 0 ? "primary" : "default",
112688
- value: JSON.stringify({
112689
- action: "question_reply",
112690
- requestId,
112691
- answers: [[String(opt.value ?? opt.label ?? "")]]
112692
- })
112693
- }));
112694
- return {
112695
- type: "card_kit",
112696
- data: {
112697
- schema: "2.0",
112698
- config: { wide_screen_mode: true },
112699
- header: {
112700
- title: { tag: "plain_text", content: header },
112701
- template: "blue"
112702
- },
112703
- body: {
112704
- elements: [
112705
- {
112706
- tag: "markdown",
112707
- content: questionText
112708
- },
112709
- ...buttons.length > 0 ? [{ tag: "action", actions: buttons }] : []
112710
- ]
112711
- }
112712
- }
112713
- };
112666
+ function getChatIdBySession(sessionId) {
112667
+ return sessionToChat.get(sessionId)?.chatId;
112668
+ }
112669
+ function getChatInfoBySession(sessionId) {
112670
+ return sessionToChat.get(sessionId);
112714
112671
  }
112715
112672
 
112716
112673
  // src/feishu/sender.ts
112674
+ async function wrapSendCall(fn, idExtractor = (res) => res?.data?.message_id ?? "") {
112675
+ try {
112676
+ const res = await fn();
112677
+ return { ok: true, messageId: idExtractor(res) };
112678
+ } catch (err) {
112679
+ return { ok: false, error: err instanceof Error ? err.message : String(err) };
112680
+ }
112681
+ }
112717
112682
  async function sendTextMessage(client, chatId, text) {
112718
112683
  if (!chatId?.trim()) {
112719
112684
  return { ok: false, error: "No chat_id provided" };
112720
112685
  }
112721
- try {
112722
- const res = await client.im.message.create({
112686
+ return wrapSendCall(
112687
+ () => client.im.message.create({
112723
112688
  params: { receive_id_type: "chat_id" },
112724
112689
  data: {
112725
112690
  receive_id: chatId.trim(),
112726
112691
  msg_type: "text",
112727
112692
  content: JSON.stringify({ text })
112728
112693
  }
112729
- });
112730
- return { ok: true, messageId: res?.data?.message_id ?? "" };
112731
- } catch (err) {
112732
- return { ok: false, error: err instanceof Error ? err.message : String(err) };
112733
- }
112694
+ })
112695
+ );
112734
112696
  }
112735
112697
  async function updateMessage(client, messageId, text) {
112736
- try {
112737
- await client.im.message.update({
112698
+ return wrapSendCall(
112699
+ () => client.im.message.update({
112738
112700
  path: { message_id: messageId },
112739
112701
  data: {
112740
112702
  msg_type: "text",
112741
112703
  content: JSON.stringify({ text })
112742
112704
  }
112743
- });
112744
- return { ok: true, messageId };
112745
- } catch (err) {
112746
- return { ok: false, error: err instanceof Error ? err.message : String(err) };
112747
- }
112705
+ }),
112706
+ () => messageId
112707
+ );
112748
112708
  }
112749
112709
  async function deleteMessage(client, messageId) {
112750
112710
  try {
@@ -112756,59 +112716,193 @@ async function sendInteractiveCard(client, chatId, card) {
112756
112716
  if (!chatId?.trim()) {
112757
112717
  return { ok: false, error: "No chat_id provided" };
112758
112718
  }
112759
- try {
112760
- const res = await client.im.message.create({
112719
+ return wrapSendCall(
112720
+ () => client.im.message.create({
112761
112721
  params: { receive_id_type: "chat_id" },
112762
112722
  data: {
112763
112723
  receive_id: chatId.trim(),
112764
112724
  msg_type: "interactive",
112765
112725
  content: JSON.stringify(card)
112766
112726
  }
112767
- });
112768
- return { ok: true, messageId: res?.data?.message_id ?? "" };
112769
- } catch (err) {
112770
- return { ok: false, error: err instanceof Error ? err.message : String(err) };
112771
- }
112727
+ })
112728
+ );
112772
112729
  }
112773
112730
  async function sendCardMessage(client, chatId, cardId) {
112774
112731
  if (!chatId?.trim()) {
112775
112732
  return { ok: false, error: "No chat_id provided" };
112776
112733
  }
112777
- try {
112778
- const res = await client.im.message.create({
112734
+ return wrapSendCall(
112735
+ () => client.im.message.create({
112779
112736
  params: { receive_id_type: "chat_id" },
112780
112737
  data: {
112781
112738
  receive_id: chatId.trim(),
112782
112739
  msg_type: "interactive",
112783
112740
  content: JSON.stringify({ type: "card_kit", data: { card_id: cardId } })
112784
112741
  }
112785
- });
112786
- return { ok: true, messageId: res?.data?.message_id ?? "" };
112787
- } catch (err) {
112788
- return { ok: false, error: err instanceof Error ? err.message : String(err) };
112742
+ })
112743
+ );
112744
+ }
112745
+
112746
+ // src/feishu/markdown.ts
112747
+ var MAX_CARD_BYTES = 28 * 1024;
112748
+ var TRUNCATION_SUFFIX = "\n\n*\u5185\u5BB9\u8FC7\u957F\uFF0C\u5DF2\u622A\u65AD*";
112749
+ var TRUNCATION_SUFFIX_BYTES = new TextEncoder().encode(TRUNCATION_SUFFIX).length;
112750
+ var CODE_FENCE_BYTES = 4;
112751
+ var HTML_TAG_RE = /<\/?\w+(?:\s[^>]*)?\/?>/g;
112752
+ function cleanMarkdown(text) {
112753
+ let result = text.replace(/<br\s*\/?>/gi, "\n");
112754
+ const { segments, codeBlocks } = extractCodeBlocks(result);
112755
+ result = segments.map((seg) => seg.replace(HTML_TAG_RE, "")).join("\0");
112756
+ let idx = 0;
112757
+ result = result.replace(/\0/g, () => codeBlocks[idx++] ?? "");
112758
+ result = closeCodeBlocks(result);
112759
+ return result;
112760
+ }
112761
+ function truncateMarkdown(text, limit = MAX_CARD_BYTES) {
112762
+ const bytes = new TextEncoder().encode(text);
112763
+ if (bytes.length <= limit) return text;
112764
+ const effectiveLimit = limit - TRUNCATION_SUFFIX_BYTES - CODE_FENCE_BYTES;
112765
+ if (effectiveLimit <= 0) return TRUNCATION_SUFFIX;
112766
+ const truncated = new TextDecoder().decode(bytes.slice(0, effectiveLimit));
112767
+ const lastNewline = truncated.lastIndexOf("\n");
112768
+ const cutPoint = lastNewline > effectiveLimit * 0.8 ? lastNewline : truncated.length;
112769
+ let result = truncated.slice(0, cutPoint);
112770
+ result = closeCodeBlocks(result);
112771
+ return result + TRUNCATION_SUFFIX;
112772
+ }
112773
+ function extractCodeBlocks(text) {
112774
+ const segments = [];
112775
+ const codeBlocks = [];
112776
+ const re = /```[\s\S]*?```/g;
112777
+ let lastIndex = 0;
112778
+ let match;
112779
+ while ((match = re.exec(text)) !== null) {
112780
+ segments.push(text.slice(lastIndex, match.index));
112781
+ codeBlocks.push(match[0]);
112782
+ lastIndex = match.index + match[0].length;
112789
112783
  }
112784
+ segments.push(text.slice(lastIndex));
112785
+ return { segments, codeBlocks };
112786
+ }
112787
+ function closeCodeBlocks(text) {
112788
+ const matches = text.match(/```/g);
112789
+ if (matches && matches.length % 2 !== 0) {
112790
+ return text + "\n```";
112791
+ }
112792
+ return text;
112793
+ }
112794
+
112795
+ // src/tools/send-card.ts
112796
+ var z2 = tool.schema;
112797
+ var TEMPLATE_COLORS = ["blue", "green", "orange", "red", "purple", "grey"];
112798
+ function createSendCardTool(deps) {
112799
+ return tool({
112800
+ description: "\u53D1\u9001\u683C\u5F0F\u5316\u5361\u7247\u6D88\u606F\u5230\u5F53\u524D\u98DE\u4E66\u4F1A\u8BDD\u3002\u652F\u6301 markdown \u6B63\u6587\u3001\u5206\u5272\u7EBF\u3001\u5907\u6CE8\u548C\u4EA4\u4E92\u6309\u94AE\u3002\u6309\u94AE\u70B9\u51FB\u7B49\u540C\u7528\u6237\u53D1\u9001\u6D88\u606F\u3002\u5361\u7247\u4F5C\u4E3A\u72EC\u7ACB\u6D88\u606F\u53D1\u9001\uFF0C\u4E0D\u5F71\u54CD\u6D41\u5F0F\u56DE\u590D\u3002",
112801
+ args: {
112802
+ title: z2.string().describe("\u5361\u7247\u6807\u9898"),
112803
+ template: z2.enum(TEMPLATE_COLORS).default("blue").describe("\u6807\u9898\u989C\u8272\u4E3B\u9898"),
112804
+ sections: z2.array(
112805
+ z2.object({
112806
+ type: z2.enum(["markdown", "divider", "note", "actions"]).default("markdown").describe("\u533A\u5757\u7C7B\u578B\uFF1Amarkdown\uFF08\u6B63\u6587\uFF09\u3001divider\uFF08\u5206\u5272\u7EBF\uFF09\u3001note\uFF08\u5907\u6CE8\uFF09\u3001actions\uFF08\u6309\u94AE\u7EC4\uFF09"),
112807
+ content: z2.string().optional().describe("\u533A\u5757\u5185\u5BB9\uFF08markdown \u683C\u5F0F\uFF0Cdivider/actions \u7C7B\u578B\u65E0\u9700\u6B64\u5B57\u6BB5\uFF09"),
112808
+ buttons: z2.array(
112809
+ z2.object({
112810
+ text: z2.string().describe("\u6309\u94AE\u663E\u793A\u6587\u672C\uFF082-6\u5B57\uFF09"),
112811
+ value: z2.string().describe("\u70B9\u51FB\u540E\u4F5C\u4E3A\u7528\u6237\u6D88\u606F\u53D1\u9001\u7684\u5185\u5BB9"),
112812
+ style: z2.enum(["primary", "default", "danger"]).default("default").describe("\u6309\u94AE\u6837\u5F0F")
112813
+ })
112814
+ ).optional().describe("\u6309\u94AE\u5217\u8868\uFF08\u4EC5 actions \u7C7B\u578B\u4F7F\u7528\uFF09")
112815
+ })
112816
+ ).min(1).describe("\u5361\u7247\u6B63\u6587\u533A\u5757\u5217\u8868")
112817
+ },
112818
+ async execute(args, context) {
112819
+ const chatId = getChatIdBySession(context.sessionID);
112820
+ if (!chatId) {
112821
+ return "\u9519\u8BEF\uFF1A\u5F53\u524D\u4F1A\u8BDD\u4E0D\u5173\u8054\u98DE\u4E66\u804A\u5929\uFF0C\u65E0\u6CD5\u53D1\u9001\u5361\u7247";
112822
+ }
112823
+ const chatInfo = getChatInfoBySession(context.sessionID);
112824
+ const card = buildCardFromDSL(args, chatId, chatInfo?.chatType ?? "p2p");
112825
+ const result = await sendInteractiveCard(deps.feishuClient, chatId, card);
112826
+ if (result.ok) {
112827
+ deps.log("info", "Agent \u5361\u7247\u5DF2\u53D1\u9001", {
112828
+ sessionId: context.sessionID,
112829
+ chatId,
112830
+ title: args.title,
112831
+ messageId: result.messageId
112832
+ });
112833
+ return `\u5361\u7247\u5DF2\u53D1\u9001\uFF1A\u300C${args.title}\u300D`;
112834
+ }
112835
+ deps.log("warn", "Agent \u5361\u7247\u53D1\u9001\u5931\u8D25", {
112836
+ sessionId: context.sessionID,
112837
+ chatId,
112838
+ title: args.title,
112839
+ error: result.error
112840
+ });
112841
+ return `\u5361\u7247\u53D1\u9001\u5931\u8D25\uFF1A${result.error}`;
112842
+ }
112843
+ });
112844
+ }
112845
+ function buildCardFromDSL(args, chatId, chatType) {
112846
+ return {
112847
+ schema: "2.0",
112848
+ config: { wide_screen_mode: true },
112849
+ header: {
112850
+ title: { tag: "plain_text", content: args.title },
112851
+ template: args.template
112852
+ },
112853
+ body: {
112854
+ elements: args.sections.map((s) => {
112855
+ switch (s.type) {
112856
+ case "divider":
112857
+ return { tag: "hr" };
112858
+ case "note":
112859
+ return {
112860
+ tag: "note",
112861
+ elements: [{ tag: "plain_text", content: s.content ?? "" }]
112862
+ };
112863
+ case "actions":
112864
+ if (!s.buttons?.length) return null;
112865
+ return {
112866
+ tag: "action",
112867
+ actions: s.buttons.map((btn) => ({
112868
+ tag: "button",
112869
+ text: { tag: "plain_text", content: btn.text },
112870
+ type: btn.style,
112871
+ value: JSON.stringify(btn.actionPayload ?? {
112872
+ action: "send_message",
112873
+ chatId,
112874
+ chatType,
112875
+ text: btn.value
112876
+ })
112877
+ }))
112878
+ };
112879
+ case "markdown":
112880
+ default:
112881
+ return {
112882
+ tag: "markdown",
112883
+ content: truncateMarkdown(s.content ?? "", 28e3)
112884
+ };
112885
+ }
112886
+ }).filter(Boolean)
112887
+ }
112888
+ };
112790
112889
  }
112791
112890
 
112792
112891
  // src/handler/interactive.ts
112793
- var SEEN_TTL_MS = 10 * 60 * 1e3;
112794
- var seenRequestIds = /* @__PURE__ */ new Map();
112892
+ var seenIds = new TtlMap(10 * 60 * 1e3);
112795
112893
  function markSeen(requestId) {
112796
- const now = Date.now();
112797
- for (const [id, ts] of seenRequestIds) {
112798
- if (now - ts > SEEN_TTL_MS) seenRequestIds.delete(id);
112799
- }
112800
- if (seenRequestIds.has(requestId)) return false;
112801
- seenRequestIds.set(requestId, now);
112894
+ if (seenIds.has(requestId)) return false;
112895
+ seenIds.set(requestId, true);
112802
112896
  return true;
112803
112897
  }
112804
- function handlePermissionRequested(request, chatId, deps) {
112898
+ function handlePermissionRequested(request, chatId, deps, chatType = "p2p") {
112805
112899
  if (!deps.v2Client) {
112806
112900
  deps.log("warn", "v2Client \u672A\u914D\u7F6E\uFF0C\u8DF3\u8FC7\u6743\u9650\u5361\u7247\u53D1\u9001", { requestId: String(request.id ?? "") });
112807
112901
  return;
112808
112902
  }
112809
112903
  const requestId = String(request.id ?? "");
112810
112904
  if (!requestId || !markSeen(requestId)) return;
112811
- const card = buildPermissionCard(request);
112905
+ const card = buildPermissionCardDSL(request, chatId, chatType);
112812
112906
  sendInteractiveCard(deps.feishuClient, chatId, card).catch((err) => {
112813
112907
  deps.log("warn", "\u53D1\u9001\u6743\u9650\u5361\u7247\u5931\u8D25", {
112814
112908
  requestId,
@@ -112816,14 +112910,14 @@ function handlePermissionRequested(request, chatId, deps) {
112816
112910
  });
112817
112911
  });
112818
112912
  }
112819
- function handleQuestionRequested(request, chatId, deps) {
112913
+ function handleQuestionRequested(request, chatId, deps, chatType = "p2p") {
112820
112914
  if (!deps.v2Client) {
112821
112915
  deps.log("warn", "v2Client \u672A\u914D\u7F6E\uFF0C\u8DF3\u8FC7\u95EE\u7B54\u5361\u7247\u53D1\u9001", { requestId: String(request.id ?? "") });
112822
112916
  return;
112823
112917
  }
112824
112918
  const requestId = String(request.id ?? "");
112825
112919
  if (!requestId || !markSeen(requestId)) return;
112826
- const card = buildQuestionCard(request);
112920
+ const card = buildQuestionCardDSL(request, chatId, chatType);
112827
112921
  sendInteractiveCard(deps.feishuClient, chatId, card).catch((err) => {
112828
112922
  deps.log("warn", "\u53D1\u9001\u95EE\u7B54\u5361\u7247\u5931\u8D25", {
112829
112923
  requestId,
@@ -112833,12 +112927,39 @@ function handleQuestionRequested(request, chatId, deps) {
112833
112927
  }
112834
112928
  async function handleCardAction(action, deps) {
112835
112929
  if (!action.actionValue) return;
112836
- {
112930
+ if (!deps.v2Client) {
112837
112931
  deps.log("warn", "v2Client \u672A\u914D\u7F6E\uFF0C\u4EA4\u4E92\u56DE\u8C03\u88AB\u5FFD\u7565\uFF08\u6309\u94AE\u70B9\u51FB\u4E0D\u4F1A\u8F6C\u53D1\u5230 OpenCode\uFF09", {
112838
112932
  actionValue: action.actionValue
112839
112933
  });
112840
112934
  return;
112841
112935
  }
112936
+ let value;
112937
+ try {
112938
+ value = JSON.parse(action.actionValue);
112939
+ } catch {
112940
+ return;
112941
+ }
112942
+ const requestId = value.requestId;
112943
+ if (!requestId) return;
112944
+ try {
112945
+ if (value.action === "permission_reply" && "reply" in value) {
112946
+ await deps.v2Client.permission.reply({
112947
+ requestID: requestId,
112948
+ reply: value.reply
112949
+ });
112950
+ } else if (value.action === "question_reply" && "answers" in value) {
112951
+ await deps.v2Client.question.reply({
112952
+ requestID: requestId,
112953
+ answers: value.answers
112954
+ });
112955
+ }
112956
+ } catch (err) {
112957
+ deps.log("error", "\u4EA4\u4E92\u56DE\u8C03\u5904\u7406\u5931\u8D25", {
112958
+ action: value.action,
112959
+ requestId,
112960
+ error: err instanceof Error ? err.message : String(err)
112961
+ });
112962
+ }
112842
112963
  }
112843
112964
  function buildCallbackResponse(action) {
112844
112965
  if (!action.actionValue) return {};
@@ -112862,23 +112983,81 @@ function buildCallbackResponse(action) {
112862
112983
  toast: { type: "success", content: "\u2705 \u5DF2\u56DE\u7B54" }
112863
112984
  };
112864
112985
  }
112986
+ if (value.action === "send_message") {
112987
+ return {
112988
+ toast: { type: "info", content: "\u{1F4E8} \u5DF2\u53D1\u9001" }
112989
+ };
112990
+ }
112865
112991
  return {};
112866
112992
  }
112993
+ function buildPermissionCardDSL(request, chatId, chatType) {
112994
+ const permission = String(request.permission ?? "unknown");
112995
+ const patterns = Array.isArray(request.patterns) ? request.patterns.map(String) : [];
112996
+ const requestId = String(request.id ?? "");
112997
+ const patternsText = patterns.length > 0 ? patterns.map((p) => `- \`${p}\``).join("\n") : "\uFF08\u65E0\u5177\u4F53\u8DEF\u5F84\uFF09";
112998
+ const buttons = [
112999
+ {
113000
+ text: "\u2705 \u5141\u8BB8\u4E00\u6B21",
113001
+ value: "",
113002
+ style: "primary",
113003
+ actionPayload: { action: "permission_reply", requestId, reply: "once" }
113004
+ },
113005
+ {
113006
+ text: "\u{1F513} \u59CB\u7EC8\u5141\u8BB8",
113007
+ value: "",
113008
+ style: "default",
113009
+ actionPayload: { action: "permission_reply", requestId, reply: "always" }
113010
+ },
113011
+ {
113012
+ text: "\u274C \u62D2\u7EDD",
113013
+ value: "",
113014
+ style: "danger",
113015
+ actionPayload: { action: "permission_reply", requestId, reply: "reject" }
113016
+ }
113017
+ ];
113018
+ const sections = [
113019
+ { type: "markdown", content: `AI \u8BF7\u6C42\u4EE5\u4E0B\u6743\u9650:
113020
+
113021
+ ${patternsText}` },
113022
+ { type: "actions", buttons }
113023
+ ];
113024
+ const dsl = { title: `\u{1F510} \u6743\u9650\u8BF7\u6C42: ${permission}`, template: "orange", sections };
113025
+ return { type: "card_kit", data: buildCardFromDSL(dsl, chatId, chatType) };
113026
+ }
113027
+ function buildQuestionCardDSL(request, chatId, chatType) {
113028
+ const questions = request.questions ?? [];
113029
+ const requestId = String(request.id ?? "");
113030
+ const q = questions[0];
113031
+ const header = String(q?.header ?? "AI \u63D0\u95EE");
113032
+ const questionText = String(q?.question ?? "\u8BF7\u9009\u62E9");
113033
+ const options = Array.isArray(q?.options) ? q.options : [];
113034
+ const buttons = options.map((opt, idx) => ({
113035
+ text: String(opt.label ?? opt.value ?? `\u9009\u9879 ${idx + 1}`),
113036
+ value: "",
113037
+ style: idx === 0 ? "primary" : "default",
113038
+ actionPayload: {
113039
+ action: "question_reply",
113040
+ requestId,
113041
+ answers: [[String(opt.value ?? opt.label ?? "")]]
113042
+ }
113043
+ }));
113044
+ const sections = [
113045
+ { type: "markdown", content: questionText },
113046
+ ...buttons.length > 0 ? [{ type: "actions", buttons }] : []
113047
+ ];
113048
+ const dsl = { title: header, template: "blue", sections };
113049
+ return { type: "card_kit", data: buildCardFromDSL(dsl, chatId, chatType) };
113050
+ }
112867
113051
 
112868
113052
  // src/feishu/dedup.ts
112869
- var seenTtlMs = 10 * 60 * 1e3;
112870
- var seen = /* @__PURE__ */ new Map();
113053
+ var dedup = new TtlMap(10 * 60 * 1e3);
112871
113054
  function initDedup(ttl) {
112872
- seenTtlMs = ttl;
113055
+ dedup = new TtlMap(ttl);
112873
113056
  }
112874
113057
  function isDuplicate(messageId) {
112875
- const now = Date.now();
112876
- for (const [k, ts] of seen) {
112877
- if (now - ts > seenTtlMs) seen.delete(k);
112878
- }
112879
113058
  if (!messageId) return false;
112880
- if (seen.has(messageId)) return true;
112881
- seen.set(messageId, now);
113059
+ if (dedup.has(messageId)) return true;
113060
+ dedup.set(messageId, true);
112882
113061
  return false;
112883
113062
  }
112884
113063
 
@@ -113235,6 +113414,25 @@ function startFeishuGateway(options) {
113235
113414
  chatId: String(evt.context?.open_chat_id ?? evt.open_chat_id ?? ""),
113236
113415
  operatorId: String(evt.operator?.open_id ?? "")
113237
113416
  };
113417
+ const sendMsg = parseSendMessageAction(action);
113418
+ if (sendMsg) {
113419
+ const syntheticCtx = {
113420
+ chatId: sendMsg.chatId,
113421
+ messageId: `btn-${randomUUID()}`,
113422
+ messageType: "text",
113423
+ content: sendMsg.text,
113424
+ rawContent: JSON.stringify({ text: sendMsg.text }),
113425
+ chatType: sendMsg.chatType,
113426
+ senderId: action.operatorId ?? "",
113427
+ shouldReply: true
113428
+ };
113429
+ void Promise.resolve(onMessage(syntheticCtx)).catch((err) => {
113430
+ log("error", "send_message \u6309\u94AE\u5904\u7406\u5931\u8D25", {
113431
+ error: err instanceof Error ? err.message : String(err)
113432
+ });
113433
+ });
113434
+ return buildCallbackResponse(action);
113435
+ }
113238
113436
  if (onCardAction) {
113239
113437
  void onCardAction(action).catch((err) => {
113240
113438
  log("error", "card action \u5904\u7406\u5931\u8D25", {
@@ -113282,6 +113480,20 @@ function startFeishuGateway(options) {
113282
113480
  };
113283
113481
  return { client: larkClient, stop };
113284
113482
  }
113483
+ function parseSendMessageAction(action) {
113484
+ if (!action.actionValue) return void 0;
113485
+ try {
113486
+ const value = JSON.parse(action.actionValue);
113487
+ if (value.action !== "send_message") return void 0;
113488
+ const text = typeof value.text === "string" ? value.text : "";
113489
+ const chatId = typeof value.chatId === "string" ? value.chatId : "";
113490
+ if (!text || !chatId) return void 0;
113491
+ const chatType = value.chatType === "group" ? "group" : "p2p";
113492
+ return { chatId, chatType, text };
113493
+ } catch {
113494
+ return void 0;
113495
+ }
113496
+ }
113285
113497
 
113286
113498
  // src/handler/action-bus.ts
113287
113499
  var subscribers = /* @__PURE__ */ new Map();
@@ -113316,57 +113528,24 @@ function emit(sessionId, action) {
113316
113528
 
113317
113529
  // src/handler/event.ts
113318
113530
  var pendingBySession = /* @__PURE__ */ new Map();
113319
- var sessionErrors = /* @__PURE__ */ new Map();
113320
- var sessionErrorTimeouts = /* @__PURE__ */ new Map();
113321
- var SESSION_ERROR_TTL_MS = 3e4;
113322
- var retryAttempts = /* @__PURE__ */ new Map();
113323
- var retryAttemptTimeouts = /* @__PURE__ */ new Map();
113531
+ var sessionErrors = new TtlMap(3e4);
113532
+ var retryAttempts = new TtlMap(36e5);
113324
113533
  var MAX_RETRY_ATTEMPTS = 2;
113325
- var RETRY_ATTEMPTS_TTL_MS = 36e5;
113326
113534
  function clearRetryAttempts(sessionKey) {
113327
113535
  retryAttempts.delete(sessionKey);
113328
- const timer = retryAttemptTimeouts.get(sessionKey);
113329
- if (timer) {
113330
- clearTimeout(timer);
113331
- retryAttemptTimeouts.delete(sessionKey);
113332
- }
113333
113536
  }
113334
113537
  function getRetryAttempts(sessionKey) {
113335
113538
  return retryAttempts.get(sessionKey) ?? 0;
113336
113539
  }
113337
113540
  function setRetryAttempts(sessionKey, count) {
113338
113541
  retryAttempts.set(sessionKey, count);
113339
- const existing = retryAttemptTimeouts.get(sessionKey);
113340
- if (existing) clearTimeout(existing);
113341
- const timeoutId = setTimeout(() => {
113342
- retryAttempts.delete(sessionKey);
113343
- retryAttemptTimeouts.delete(sessionKey);
113344
- }, RETRY_ATTEMPTS_TTL_MS);
113345
- retryAttemptTimeouts.set(sessionKey, timeoutId);
113346
113542
  }
113347
113543
  function getSessionError(sessionId) {
113348
113544
  return sessionErrors.get(sessionId);
113349
113545
  }
113350
113546
  function clearSessionError(sessionId) {
113351
- const timer = sessionErrorTimeouts.get(sessionId);
113352
- if (timer) {
113353
- clearTimeout(timer);
113354
- sessionErrorTimeouts.delete(sessionId);
113355
- }
113356
113547
  sessionErrors.delete(sessionId);
113357
113548
  }
113358
- function setSessionError(sessionId, message, fields) {
113359
- const existing = sessionErrorTimeouts.get(sessionId);
113360
- if (existing) {
113361
- clearTimeout(existing);
113362
- }
113363
- sessionErrors.set(sessionId, { message, fields });
113364
- const timeoutId = setTimeout(() => {
113365
- sessionErrors.delete(sessionId);
113366
- sessionErrorTimeouts.delete(sessionId);
113367
- }, SESSION_ERROR_TTL_MS);
113368
- sessionErrorTimeouts.set(sessionId, timeoutId);
113369
- }
113370
113549
  function registerPending(sessionId, payload) {
113371
113550
  pendingBySession.set(sessionId, { ...payload, textBuffer: "", expectedMessageId: void 0 });
113372
113551
  }
@@ -113417,112 +113596,116 @@ async function handleEvent(event, deps) {
113417
113596
  switch (event.type) {
113418
113597
  case "message.part.updated": {
113419
113598
  const part = event.properties.part;
113420
- if (!part) break;
113421
- const sessionId = part.sessionID;
113422
- if (!sessionId) break;
113423
- const payload = pendingBySession.get(sessionId);
113599
+ if (!part?.sessionID) break;
113600
+ const payload = pendingBySession.get(part.sessionID);
113424
113601
  if (!payload) break;
113425
- const messageId = part.messageID;
113426
- if (messageId) {
113427
- if (!payload.expectedMessageId) {
113428
- payload.expectedMessageId = messageId;
113429
- } else if (payload.expectedMessageId !== messageId) {
113430
- break;
113431
- }
113432
- } else if (payload.expectedMessageId) {
113433
- break;
113434
- }
113435
- const partSessionId = part.sessionID;
113436
- if (part.type === "tool") {
113437
- const p = part;
113438
- const toolName = String(p.toolName ?? p.name ?? "unknown");
113439
- const callID = String(p.toolCallID ?? p.id ?? "");
113440
- const hasError = p.error !== void 0 && p.error !== null;
113441
- const rawState = p.state != null ? String(p.state) : hasError ? "error" : "running";
113442
- const toolState = rawState === "completed" || rawState === "error" ? rawState : "running";
113443
- if (partSessionId) {
113444
- emit(partSessionId, {
113445
- type: "tool-state-changed",
113446
- sessionId: partSessionId,
113447
- callID,
113448
- tool: toolName,
113449
- state: toolState
113450
- });
113451
- }
113452
- break;
113453
- }
113454
- const delta = event.properties.delta;
113455
- if (delta) {
113456
- payload.textBuffer += delta;
113457
- } else {
113458
- const fullText = extractPartText(part);
113459
- if (fullText) {
113460
- payload.textBuffer = fullText;
113461
- }
113462
- }
113463
- if (payload.textBuffer) {
113464
- const res = await updateMessage(payload.feishuClient, payload.placeholderId, payload.textBuffer.trim());
113465
- if (!res.ok) ;
113466
- }
113467
- if (partSessionId) {
113468
- emit(partSessionId, {
113469
- type: "text-updated",
113470
- sessionId: partSessionId,
113471
- messageId: part.messageID,
113472
- delta: delta ?? void 0,
113473
- fullText: payload.textBuffer
113474
- });
113475
- }
113602
+ await handleMessagePartUpdated(event, part, payload);
113476
113603
  break;
113477
113604
  }
113478
- case "session.error": {
113479
- const props = event.properties;
113480
- const sessionId = props.sessionID;
113481
- if (!sessionId) break;
113482
- const error48 = props.error;
113483
- let errMsg;
113484
- if (typeof error48 === "string") {
113485
- errMsg = error48;
113486
- } else if (error48 && typeof error48 === "object") {
113487
- const e = error48;
113488
- const rawDataMsg = e.data && typeof e.data === "object" && "message" in e.data ? e.data.message : void 0;
113489
- const dataMsg = rawDataMsg != null ? String(rawDataMsg) : void 0;
113490
- errMsg = String(e.message ?? dataMsg ?? e.type ?? e.name ?? "An unexpected error occurred");
113491
- } else {
113492
- errMsg = String(error48);
113493
- }
113494
- const fields = extractErrorFields(error48);
113495
- deps.log("warn", "\u6536\u5230 session.error \u4E8B\u4EF6", { sessionId, errMsg });
113496
- setSessionError(sessionId, errMsg, fields);
113605
+ case "session.error":
113606
+ handleSessionErrorEvent(event, deps);
113497
113607
  break;
113498
- }
113499
- default: {
113500
- const evtType = event.type;
113501
- const evtProps = event.properties ?? {};
113502
- const evtSessionId = evtProps.sessionID;
113503
- if (evtType === "permission.asked" && evtSessionId) {
113504
- emit(evtSessionId, {
113505
- type: "permission-requested",
113506
- sessionId: evtSessionId,
113507
- request: evtProps
113508
- });
113509
- deps.log("info", "permission.asked \u4E8B\u4EF6\u5DF2\u5206\u53D1", { sessionId: evtSessionId });
113510
- } else if (evtType === "question.asked" && evtSessionId) {
113511
- emit(evtSessionId, {
113512
- type: "question-requested",
113513
- sessionId: evtSessionId,
113514
- request: evtProps
113515
- });
113516
- deps.log("info", "question.asked \u4E8B\u4EF6\u5DF2\u5206\u53D1", { sessionId: evtSessionId });
113517
- } else if (evtType === "session.idle" && evtSessionId) {
113518
- emit(evtSessionId, {
113519
- type: "session-idle",
113520
- sessionId: evtSessionId
113521
- });
113522
- }
113608
+ default:
113609
+ handleV2Event(event, deps);
113523
113610
  break;
113611
+ }
113612
+ }
113613
+ async function handleMessagePartUpdated(event, part, payload) {
113614
+ const messageId = part.messageID;
113615
+ if (messageId) {
113616
+ if (!payload.expectedMessageId) {
113617
+ payload.expectedMessageId = messageId;
113618
+ } else if (payload.expectedMessageId !== messageId) {
113619
+ return;
113620
+ }
113621
+ } else if (payload.expectedMessageId) {
113622
+ return;
113623
+ }
113624
+ const partSessionId = part.sessionID;
113625
+ if (part.type === "tool") {
113626
+ const p = part;
113627
+ const toolName = String(p.toolName ?? p.name ?? "unknown");
113628
+ const callID = String(p.toolCallID ?? p.id ?? "");
113629
+ const hasError = p.error !== void 0 && p.error !== null;
113630
+ const rawState = p.state != null ? String(p.state) : hasError ? "error" : "running";
113631
+ const toolState = rawState === "completed" || rawState === "error" ? rawState : "running";
113632
+ if (partSessionId) {
113633
+ emit(partSessionId, {
113634
+ type: "tool-state-changed",
113635
+ sessionId: partSessionId,
113636
+ callID,
113637
+ tool: toolName,
113638
+ state: toolState
113639
+ });
113640
+ }
113641
+ return;
113642
+ }
113643
+ const delta = event.properties.delta;
113644
+ if (delta) {
113645
+ payload.textBuffer += delta;
113646
+ } else {
113647
+ const fullText = extractPartText(part);
113648
+ if (fullText) {
113649
+ payload.textBuffer = fullText;
113524
113650
  }
113525
113651
  }
113652
+ if (payload.textBuffer) {
113653
+ await updateMessage(payload.feishuClient, payload.placeholderId, payload.textBuffer.trim());
113654
+ }
113655
+ if (partSessionId) {
113656
+ emit(partSessionId, {
113657
+ type: "text-updated",
113658
+ sessionId: partSessionId,
113659
+ messageId: part.messageID,
113660
+ delta: delta ?? void 0,
113661
+ fullText: payload.textBuffer
113662
+ });
113663
+ }
113664
+ }
113665
+ function handleSessionErrorEvent(event, deps) {
113666
+ const props = event.properties;
113667
+ const sessionId = props.sessionID;
113668
+ if (!sessionId) return;
113669
+ const error48 = props.error;
113670
+ let errMsg;
113671
+ if (typeof error48 === "string") {
113672
+ errMsg = error48;
113673
+ } else if (error48 && typeof error48 === "object") {
113674
+ const e = error48;
113675
+ const asStr = (v) => typeof v === "string" && v.trim().length > 0 ? v : void 0;
113676
+ const rawDataMsg = e.data && typeof e.data === "object" && "message" in e.data ? e.data.message : void 0;
113677
+ errMsg = asStr(e.message) ?? asStr(rawDataMsg) ?? asStr(e.type) ?? asStr(e.name) ?? "An unexpected error occurred";
113678
+ } else {
113679
+ errMsg = String(error48);
113680
+ }
113681
+ const fields = extractErrorFields(error48);
113682
+ deps.log("warn", "\u6536\u5230 session.error \u4E8B\u4EF6", { sessionId, errMsg });
113683
+ sessionErrors.set(sessionId, { message: errMsg, fields });
113684
+ }
113685
+ function handleV2Event(event, deps) {
113686
+ const evtType = event.type;
113687
+ const evtProps = event.properties ?? {};
113688
+ const evtSessionId = evtProps.sessionID;
113689
+ if (evtType === "permission.asked" && evtSessionId) {
113690
+ emit(evtSessionId, {
113691
+ type: "permission-requested",
113692
+ sessionId: evtSessionId,
113693
+ request: evtProps
113694
+ });
113695
+ deps.log("info", "permission.asked \u4E8B\u4EF6\u5DF2\u5206\u53D1", { sessionId: evtSessionId });
113696
+ } else if (evtType === "question.asked" && evtSessionId) {
113697
+ emit(evtSessionId, {
113698
+ type: "question-requested",
113699
+ sessionId: evtSessionId,
113700
+ request: evtProps
113701
+ });
113702
+ deps.log("info", "question.asked \u4E8B\u4EF6\u5DF2\u5206\u53D1", { sessionId: evtSessionId });
113703
+ } else if (evtType === "session.idle" && evtSessionId) {
113704
+ emit(evtSessionId, {
113705
+ type: "session-idle",
113706
+ sessionId: evtSessionId
113707
+ });
113708
+ }
113526
113709
  }
113527
113710
  function extractPartText(part) {
113528
113711
  if (part.type === "text") return part.text ?? "";
@@ -113532,6 +113715,128 @@ function extractPartText(part) {
113532
113715
  return "";
113533
113716
  }
113534
113717
 
113718
+ // src/handler/error-recovery.ts
113719
+ var SessionErrorDetected = class extends Error {
113720
+ constructor(sessionError) {
113721
+ super(sessionError.message);
113722
+ this.sessionError = sessionError;
113723
+ this.name = "SessionErrorDetected";
113724
+ }
113725
+ };
113726
+ async function getGlobalDefaultModel(client, directory) {
113727
+ const query = directory ? { directory } : void 0;
113728
+ const { data: config2 } = await client.config.get({ query });
113729
+ const model = config2?.model;
113730
+ if (!model || !model.includes("/")) return void 0;
113731
+ const slash = model.indexOf("/");
113732
+ const providerID = model.slice(0, slash).trim();
113733
+ const modelID = model.slice(slash + 1).trim();
113734
+ if (!providerID || !modelID) return void 0;
113735
+ return { providerID, modelID };
113736
+ }
113737
+ function extractSessionError(err, sessionId) {
113738
+ const result = err instanceof SessionErrorDetected ? err.sessionError : getSessionError(sessionId);
113739
+ clearSessionError(sessionId);
113740
+ return result;
113741
+ }
113742
+ async function tryModelRecovery(params) {
113743
+ const {
113744
+ sessionError,
113745
+ sessionId,
113746
+ sessionKey,
113747
+ client,
113748
+ directory,
113749
+ parts,
113750
+ timeout,
113751
+ pollInterval,
113752
+ stablePolls,
113753
+ query,
113754
+ signal,
113755
+ log,
113756
+ poll
113757
+ } = params;
113758
+ log("info", "\u9519\u8BEF\u5B57\u6BB5\u68C0\u67E5", {
113759
+ sessionKey,
113760
+ fields: sessionError.fields,
113761
+ isModel: isModelError(sessionError.fields)
113762
+ });
113763
+ if (!isModelError(sessionError.fields)) {
113764
+ return { recovered: false, sessionError };
113765
+ }
113766
+ const attempts = getRetryAttempts(sessionKey);
113767
+ if (attempts >= MAX_RETRY_ATTEMPTS) {
113768
+ log("warn", "\u5DF2\u8FBE\u91CD\u8BD5\u4E0A\u9650\uFF0C\u653E\u5F03\u6062\u590D", { sessionKey, attempts });
113769
+ return { recovered: false, sessionError };
113770
+ }
113771
+ try {
113772
+ let modelOverride;
113773
+ try {
113774
+ modelOverride = await getGlobalDefaultModel(client, directory);
113775
+ } catch (configErr) {
113776
+ log("warn", "\u8BFB\u53D6\u5168\u5C40\u6A21\u578B\u914D\u7F6E\u5931\u8D25", {
113777
+ sessionKey,
113778
+ error: configErr instanceof Error ? configErr.message : String(configErr)
113779
+ });
113780
+ }
113781
+ if (!modelOverride) {
113782
+ log("warn", "\u5168\u5C40\u9ED8\u8BA4\u6A21\u578B\u672A\u914D\u7F6E\uFF0C\u653E\u5F03\u6062\u590D", { sessionKey });
113783
+ return { recovered: false, sessionError };
113784
+ }
113785
+ setRetryAttempts(sessionKey, attempts + 1);
113786
+ log("info", "\u4F7F\u7528\u5168\u5C40\u9ED8\u8BA4\u6A21\u578B\u6062\u590D", {
113787
+ sessionKey,
113788
+ providerID: modelOverride.providerID,
113789
+ modelID: modelOverride.modelID
113790
+ });
113791
+ clearSessionError(sessionId);
113792
+ await client.session.promptAsync({
113793
+ path: { id: sessionId },
113794
+ query,
113795
+ body: { parts: [...parts], model: modelOverride }
113796
+ });
113797
+ const finalText = await poll(client, sessionId, {
113798
+ timeout,
113799
+ pollInterval,
113800
+ stablePolls,
113801
+ query,
113802
+ signal
113803
+ });
113804
+ log("info", "\u6A21\u578B\u6062\u590D\u540E\u54CD\u5E94\u5B8C\u6210", {
113805
+ sessionKey,
113806
+ sessionId,
113807
+ output: finalText || "(empty)"
113808
+ });
113809
+ clearRetryAttempts(sessionKey);
113810
+ log("info", "\u6A21\u578B\u4E0D\u517C\u5BB9\u6062\u590D\u6210\u529F", {
113811
+ sessionId,
113812
+ sessionKey,
113813
+ model: `${modelOverride.providerID}/${modelOverride.modelID}`,
113814
+ attempt: attempts + 1
113815
+ });
113816
+ return { recovered: true, text: finalText };
113817
+ } catch (recoveryErr) {
113818
+ if (recoveryErr instanceof Error && recoveryErr.name === "AbortError") {
113819
+ throw recoveryErr;
113820
+ }
113821
+ const errMsg = recoveryErr instanceof Error ? recoveryErr.message : String(recoveryErr);
113822
+ let updatedError;
113823
+ if (recoveryErr instanceof SessionErrorDetected) {
113824
+ updatedError = recoveryErr.sessionError;
113825
+ clearSessionError(sessionId);
113826
+ } else {
113827
+ const sseError = getSessionError(sessionId);
113828
+ if (sseError) {
113829
+ updatedError = sseError;
113830
+ clearSessionError(sessionId);
113831
+ } else {
113832
+ updatedError = { message: errMsg, fields: [] };
113833
+ }
113834
+ }
113835
+ log("error", "\u6A21\u578B\u6062\u590D\u5931\u8D25", { sessionId, sessionKey, error: errMsg });
113836
+ return { recovered: false, sessionError: updatedError };
113837
+ }
113838
+ }
113839
+
113535
113840
  // src/session.ts
113536
113841
  var SESSION_KEY_PREFIX = "feishu";
113537
113842
  var TITLE_PREFIX = "Feishu";
@@ -113585,55 +113890,6 @@ async function getOrCreateSession(client, sessionKey, directory) {
113585
113890
  return session;
113586
113891
  }
113587
113892
 
113588
- // src/feishu/markdown.ts
113589
- var MAX_CARD_BYTES = 28 * 1024;
113590
- var TRUNCATION_SUFFIX = "\n\n*\u5185\u5BB9\u8FC7\u957F\uFF0C\u5DF2\u622A\u65AD*";
113591
- var TRUNCATION_SUFFIX_BYTES = new TextEncoder().encode(TRUNCATION_SUFFIX).length;
113592
- var CODE_FENCE_BYTES = 4;
113593
- var HTML_TAG_RE = /<\/?\w+(?:\s[^>]*)?\/?>/g;
113594
- function cleanMarkdown(text) {
113595
- let result = text.replace(/<br\s*\/?>/gi, "\n");
113596
- const { segments, codeBlocks } = extractCodeBlocks(result);
113597
- result = segments.map((seg) => seg.replace(HTML_TAG_RE, "")).join("\0");
113598
- let idx = 0;
113599
- result = result.replace(/\0/g, () => codeBlocks[idx++] ?? "");
113600
- result = closeCodeBlocks(result);
113601
- return result;
113602
- }
113603
- function truncateMarkdown(text, limit = MAX_CARD_BYTES) {
113604
- const bytes = new TextEncoder().encode(text);
113605
- if (bytes.length <= limit) return text;
113606
- const effectiveLimit = limit - TRUNCATION_SUFFIX_BYTES - CODE_FENCE_BYTES;
113607
- if (effectiveLimit <= 0) return TRUNCATION_SUFFIX;
113608
- const truncated = new TextDecoder().decode(bytes.slice(0, effectiveLimit));
113609
- const lastNewline = truncated.lastIndexOf("\n");
113610
- const cutPoint = lastNewline > effectiveLimit * 0.8 ? lastNewline : truncated.length;
113611
- let result = truncated.slice(0, cutPoint);
113612
- result = closeCodeBlocks(result);
113613
- return result + TRUNCATION_SUFFIX;
113614
- }
113615
- function extractCodeBlocks(text) {
113616
- const segments = [];
113617
- const codeBlocks = [];
113618
- const re = /```[\s\S]*?```/g;
113619
- let lastIndex = 0;
113620
- let match;
113621
- while ((match = re.exec(text)) !== null) {
113622
- segments.push(text.slice(lastIndex, match.index));
113623
- codeBlocks.push(match[0]);
113624
- lastIndex = match.index + match[0].length;
113625
- }
113626
- segments.push(text.slice(lastIndex));
113627
- return { segments, codeBlocks };
113628
- }
113629
- function closeCodeBlocks(text) {
113630
- const matches = text.match(/```/g);
113631
- if (matches && matches.length % 2 !== 0) {
113632
- return text + "\n```";
113633
- }
113634
- return text;
113635
- }
113636
-
113637
113893
  // src/feishu/streaming-card.ts
113638
113894
  var StreamingCard = class {
113639
113895
  constructor(cardkit, feishuClient, chatId, log) {
@@ -113696,9 +113952,9 @@ var StreamingCard = class {
113696
113952
  /**
113697
113953
  * 更新工具状态到 tools 元素
113698
113954
  */
113699
- async setToolStatus(callID, tool, state) {
113955
+ async setToolStatus(callID, tool2, state) {
113700
113956
  if (this.closed || !this.cardId) return;
113701
- this.toolStates.set(callID, { tool, state });
113957
+ this.toolStates.set(callID, { tool: tool2, state });
113702
113958
  this.enqueue(() => this.doUpdateTools());
113703
113959
  }
113704
113960
  /**
@@ -113786,6 +114042,7 @@ async function handleChat(ctx, deps, signal) {
113786
114042
  const query = directory ? { directory } : void 0;
113787
114043
  const sessionKey = buildSessionKey(chatType, chatType === "p2p" ? senderId : chatId);
113788
114044
  const session = await getOrCreateSession(client, sessionKey, directory);
114045
+ registerSessionChat(session.id, chatId, chatType);
113789
114046
  const parts = await buildPromptParts(feishuClient, messageId, messageType, rawContent, content, chatType, senderId, log);
113790
114047
  if (!parts.length) return void 0;
113791
114048
  log("info", "\u6536\u5230\u7528\u6237\u6D88\u606F", {
@@ -113871,12 +114128,12 @@ async function handleChat(ctx, deps, signal) {
113871
114128
  break;
113872
114129
  case "permission-requested":
113873
114130
  if (deps.interactiveDeps) {
113874
- handlePermissionRequested(action.request, chatId, deps.interactiveDeps);
114131
+ handlePermissionRequested(action.request, chatId, deps.interactiveDeps, chatType);
113875
114132
  }
113876
114133
  break;
113877
114134
  case "question-requested":
113878
114135
  if (deps.interactiveDeps) {
113879
- handleQuestionRequested(action.request, chatId, deps.interactiveDeps);
114136
+ handleQuestionRequested(action.request, chatId, deps.interactiveDeps, chatType);
113880
114137
  }
113881
114138
  break;
113882
114139
  }
@@ -113907,108 +114164,50 @@ async function handleChat(ctx, deps, signal) {
113907
114164
  await abortCleanup(streamingCard, feishuClient, placeholderId);
113908
114165
  return void 0;
113909
114166
  }
113910
- let sessionError;
113911
- if (err instanceof SessionErrorDetected) {
113912
- sessionError = err.sessionError;
113913
- clearSessionError(session.id);
113914
- } else {
113915
- sessionError = getSessionError(session.id);
113916
- clearSessionError(session.id);
113917
- }
114167
+ const sessionError = extractSessionError(err, session.id);
114168
+ let displayError = sessionError;
113918
114169
  if (sessionError) {
113919
- log("info", "\u9519\u8BEF\u5B57\u6BB5\u68C0\u67E5", {
113920
- sessionKey,
113921
- fields: sessionError.fields,
113922
- isModel: isModelError(sessionError.fields)
113923
- });
113924
- }
113925
- if (sessionError && isModelError(sessionError.fields)) {
113926
- const attempts = getRetryAttempts(sessionKey);
113927
- if (attempts < MAX_RETRY_ATTEMPTS) {
113928
- try {
113929
- let modelOverride;
113930
- try {
113931
- modelOverride = await getGlobalDefaultModel(client, directory);
113932
- } catch (configErr) {
113933
- log("warn", "\u8BFB\u53D6\u5168\u5C40\u6A21\u578B\u914D\u7F6E\u5931\u8D25", {
113934
- sessionKey,
113935
- error: configErr instanceof Error ? configErr.message : String(configErr)
113936
- });
113937
- }
113938
- if (!modelOverride) {
113939
- log("warn", "\u5168\u5C40\u9ED8\u8BA4\u6A21\u578B\u672A\u914D\u7F6E\uFF0C\u653E\u5F03\u6062\u590D", { sessionKey });
113940
- } else {
113941
- setRetryAttempts(sessionKey, attempts + 1);
113942
- log("info", "\u4F7F\u7528\u5168\u5C40\u9ED8\u8BA4\u6A21\u578B\u6062\u590D", {
113943
- sessionKey,
113944
- providerID: modelOverride.providerID,
113945
- modelID: modelOverride.modelID
113946
- });
113947
- clearSessionError(session.id);
113948
- await client.session.promptAsync({
113949
- path: { id: session.id },
113950
- query,
113951
- body: { ...baseBody, model: modelOverride }
113952
- });
113953
- const finalText = await pollForResponse(client, session.id, { timeout, pollInterval, stablePolls, query, signal });
113954
- log("info", "\u6A21\u578B\u6062\u590D\u540E\u54CD\u5E94\u5B8C\u6210", {
113955
- sessionKey,
113956
- sessionId: session.id,
113957
- output: finalText || "(empty)"
113958
- });
113959
- clearRetryAttempts(sessionKey);
113960
- await finalizeReply(streamingCard, feishuClient, chatId, placeholderId, finalText || "\u26A0\uFE0F \u54CD\u5E94\u8D85\u65F6");
113961
- log("info", "\u6A21\u578B\u4E0D\u517C\u5BB9\u6062\u590D\u6210\u529F", {
113962
- sessionId: session.id,
113963
- sessionKey,
113964
- model: `${modelOverride.providerID}/${modelOverride.modelID}`,
113965
- attempt: attempts + 1
113966
- });
113967
- if (config2.autoPrompt.enabled && shouldReply) {
113968
- return { sessionId: session.id, sessionKey, chatId, deps };
113969
- }
113970
- return void 0;
113971
- }
113972
- } catch (recoveryErr) {
113973
- if (recoveryErr instanceof Error && recoveryErr.name === "AbortError") {
113974
- log("info", "\u6A21\u578B\u6062\u590D\u88AB\u4E2D\u65AD", { sessionKey });
113975
- await abortCleanup(streamingCard, feishuClient, placeholderId);
113976
- return void 0;
113977
- }
113978
- const errMsg = recoveryErr instanceof Error ? recoveryErr.message : String(recoveryErr);
113979
- if (recoveryErr instanceof SessionErrorDetected) {
113980
- sessionError = recoveryErr.sessionError;
113981
- clearSessionError(session.id);
113982
- } else {
113983
- const sseError = getSessionError(session.id);
113984
- if (sseError) {
113985
- sessionError = sseError;
113986
- clearSessionError(session.id);
113987
- } else {
113988
- sessionError = { message: errMsg, fields: [] };
113989
- }
113990
- }
113991
- log("error", "\u6A21\u578B\u6062\u590D\u5931\u8D25", {
113992
- sessionId: session.id,
113993
- sessionKey,
113994
- error: errMsg
113995
- });
113996
- }
113997
- } else {
113998
- log("warn", "\u5DF2\u8FBE\u91CD\u8BD5\u4E0A\u9650\uFF0C\u653E\u5F03\u6062\u590D", {
114170
+ try {
114171
+ const recovery = await tryModelRecovery({
114172
+ sessionError,
114173
+ sessionId: session.id,
113999
114174
  sessionKey,
114000
- attempts
114175
+ client,
114176
+ directory,
114177
+ parts,
114178
+ timeout,
114179
+ pollInterval,
114180
+ stablePolls,
114181
+ query,
114182
+ signal,
114183
+ log,
114184
+ poll: pollForResponse
114001
114185
  });
114186
+ if (recovery.recovered) {
114187
+ await finalizeReply(streamingCard, feishuClient, chatId, placeholderId, recovery.text || "\u26A0\uFE0F \u54CD\u5E94\u8D85\u65F6");
114188
+ if (config2.autoPrompt.enabled && shouldReply) {
114189
+ return { sessionId: session.id, sessionKey, chatId, deps };
114190
+ }
114191
+ return void 0;
114192
+ }
114193
+ displayError = recovery.sessionError;
114194
+ } catch (abortErr) {
114195
+ if (abortErr instanceof Error && abortErr.name === "AbortError") {
114196
+ log("info", "\u6A21\u578B\u6062\u590D\u88AB\u4E2D\u65AD", { sessionKey });
114197
+ await abortCleanup(streamingCard, feishuClient, placeholderId);
114198
+ return void 0;
114199
+ }
114200
+ throw abortErr;
114002
114201
  }
114003
114202
  }
114004
114203
  const thrownError = err instanceof Error ? err.message : String(err);
114005
- const errorMessage = sessionError?.message || thrownError;
114204
+ const errorMessage = displayError?.message || thrownError;
114006
114205
  log("error", "\u5BF9\u8BDD\u5904\u7406\u5931\u8D25", {
114007
114206
  sessionId: session.id,
114008
114207
  sessionKey,
114009
114208
  chatType,
114010
114209
  error: thrownError,
114011
- ...sessionError ? { sessionError: sessionError.message } : {}
114210
+ ...displayError ? { sessionError: displayError.message } : {}
114012
114211
  });
114013
114212
  await finalizeReply(streamingCard, feishuClient, chatId, placeholderId, "\u274C " + errorMessage);
114014
114213
  } finally {
@@ -114018,17 +114217,6 @@ async function handleChat(ctx, deps, signal) {
114018
114217
  unregisterPending(activeSessionId);
114019
114218
  }
114020
114219
  }
114021
- async function getGlobalDefaultModel(client, directory) {
114022
- const query = directory ? { directory } : void 0;
114023
- const { data: config2 } = await client.config.get({ query });
114024
- const model = config2?.model;
114025
- if (!model || !model.includes("/")) return void 0;
114026
- const slash = model.indexOf("/");
114027
- const providerID = model.slice(0, slash).trim();
114028
- const modelID = model.slice(slash + 1).trim();
114029
- if (!providerID || !modelID) return void 0;
114030
- return { providerID, modelID };
114031
- }
114032
114220
  async function buildPromptParts(feishuClient, messageId, messageType, rawContent, textContent, chatType, senderId, log) {
114033
114221
  if (messageType === "text") {
114034
114222
  let promptText = textContent;
@@ -114043,13 +114231,6 @@ async function buildPromptParts(feishuClient, messageId, messageType, rawContent
114043
114231
  }
114044
114232
  return parts;
114045
114233
  }
114046
- var SessionErrorDetected = class extends Error {
114047
- constructor(sessionError) {
114048
- super(sessionError.message);
114049
- this.sessionError = sessionError;
114050
- this.name = "SessionErrorDetected";
114051
- }
114052
- };
114053
114234
  async function pollForResponse(client, sessionId, opts) {
114054
114235
  const { timeout, pollInterval, stablePolls, query, signal } = opts;
114055
114236
  const start = Date.now();
@@ -114170,6 +114351,7 @@ function extractLastAssistantText(messages) {
114170
114351
  }
114171
114352
 
114172
114353
  // src/handler/session-queue.ts
114354
+ var QUEUE_MONITOR_INTERVAL_MS = 200;
114173
114355
  var states = /* @__PURE__ */ new Map();
114174
114356
  function getOrCreateState(sessionKey) {
114175
114357
  const existing = states.get(sessionKey);
@@ -114317,7 +114499,7 @@ async function drainLoop(sessionKey, state) {
114317
114499
  const autoPromptController = new AbortController();
114318
114500
  const monitor = setInterval(() => {
114319
114501
  if (state.queue.length > 0) autoPromptController.abort();
114320
- }, 200);
114502
+ }, QUEUE_MONITOR_INTERVAL_MS);
114321
114503
  try {
114322
114504
  const result = await runOneAutoPromptIteration(
114323
114505
  autoPromptCtx,
@@ -114456,11 +114638,17 @@ function formatHistoryAsContext(messages) {
114456
114638
  return `${header}
114457
114639
  ${body}`;
114458
114640
  }
114459
-
114460
- // src/index.ts
114461
114641
  var SERVICE_NAME = "opencode-feishu";
114462
114642
  var LOG_PREFIX = "[feishu]";
114463
114643
  var isDebug = !!process.env.FEISHU_DEBUG;
114644
+ function loadFeishuSkill() {
114645
+ const skillPath = join(fileURLToPath(import.meta.url), "../../skills/feishu-card-interaction.md");
114646
+ if (existsSync(skillPath)) {
114647
+ return readFileSync(skillPath, "utf-8");
114648
+ }
114649
+ return "\u5F53\u524D\u7528\u6237\u901A\u8FC7\u98DE\u4E66\uFF08Feishu/Lark\uFF09\u4E0E\u4F60\u5BF9\u8BDD\u3002\u4F60\u53EF\u4EE5\u4F7F\u7528 feishu_send_card \u5DE5\u5177\u53D1\u9001\u683C\u5F0F\u5316\u5361\u7247\u6D88\u606F\uFF08\u652F\u6301\u6309\u94AE\u4EA4\u4E92\uFF09\u3002";
114650
+ }
114651
+ var feishuSystemPrompt = loadFeishuSkill();
114464
114652
  var FeishuPlugin = async (ctx) => {
114465
114653
  const { client } = ctx;
114466
114654
  let gateway = null;
@@ -114480,21 +114668,9 @@ var FeishuPlugin = async (ctx) => {
114480
114668
  });
114481
114669
  };
114482
114670
  const configPath = join(homedir(), ".config", "opencode", "plugins", "feishu.json");
114483
- if (!existsSync(configPath)) {
114484
- throw new Error(
114485
- `\u7F3A\u5C11\u98DE\u4E66\u914D\u7F6E\u6587\u4EF6\uFF1A\u8BF7\u521B\u5EFA ${configPath}\uFF0C\u5185\u5BB9\u4E3A {"appId":"cli_xxx","appSecret":"xxx"}`
114486
- );
114487
- }
114488
114671
  let resolvedConfig;
114489
114672
  try {
114490
- const raw = resolveEnvPlaceholders(
114491
- JSON.parse(readFileSync(configPath, "utf-8"))
114492
- );
114493
- const parsed = FeishuConfigSchema.parse(raw);
114494
- resolvedConfig = {
114495
- ...parsed,
114496
- directory: expandDirectoryPath(parsed.directory ?? ctx.directory ?? "")
114497
- };
114673
+ resolvedConfig = loadAndValidateConfig(configPath, ctx.directory ?? "");
114498
114674
  } catch (e) {
114499
114675
  if (e instanceof external_exports.ZodError) {
114500
114676
  const details = e.issues.map((i) => ` - ${i.path.join(".")}: ${i.message}`).join("\n");
@@ -114515,7 +114691,7 @@ ${details}`);
114515
114691
  });
114516
114692
  const cardkit = new CardKitClient(larkClient, log);
114517
114693
  const botOpenId = await fetchBotOpenId(larkClient, log);
114518
- const v2Client = void 0;
114694
+ const v2Client = createOpencodeClient({ directory: resolvedConfig.directory || void 0 });
114519
114695
  gateway = startFeishuGateway({
114520
114696
  config: resolvedConfig,
114521
114697
  larkClient,
@@ -114569,10 +114745,25 @@ ${details}`);
114569
114745
  event: async ({ event }) => {
114570
114746
  if (!gateway) return;
114571
114747
  await handleEvent(event, { log, directory: resolvedConfig.directory });
114748
+ },
114749
+ tool: {
114750
+ feishu_send_card: createSendCardTool({ feishuClient: larkClient, log })
114751
+ },
114752
+ "experimental.chat.system.transform": async (input, output) => {
114753
+ if (!input.sessionID || !getChatIdBySession(input.sessionID)) return;
114754
+ output.system.push(feishuSystemPrompt);
114572
114755
  }
114573
114756
  };
114574
114757
  return hooks;
114575
114758
  };
114759
+ function loadAndValidateConfig(configPath, ctxDirectory) {
114760
+ if (!existsSync(configPath)) {
114761
+ throw new Error(`\u7F3A\u5C11\u98DE\u4E66\u914D\u7F6E\u6587\u4EF6\uFF1A\u8BF7\u521B\u5EFA ${configPath}\uFF0C\u5185\u5BB9\u4E3A {"appId":"cli_xxx","appSecret":"xxx"}`);
114762
+ }
114763
+ const raw = resolveEnvPlaceholders(JSON.parse(readFileSync(configPath, "utf-8")));
114764
+ const parsed = FeishuConfigSchema.parse(raw);
114765
+ return { ...parsed, directory: expandDirectoryPath(parsed.directory ?? ctxDirectory ?? "") };
114766
+ }
114576
114767
  function expandDirectoryPath(dir) {
114577
114768
  if (!dir) return dir;
114578
114769
  if (dir.startsWith("~")) {