opencode-feishu 0.7.14 → 0.7.15

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
@@ -99175,6 +99175,12 @@ async function updateMessage(client, messageId, text2) {
99175
99175
  return { ok: false, error: err instanceof Error ? err.message : String(err) };
99176
99176
  }
99177
99177
  }
99178
+ async function deleteMessage(client, messageId) {
99179
+ try {
99180
+ await client.im.message.delete({ path: { message_id: messageId } });
99181
+ } catch {
99182
+ }
99183
+ }
99178
99184
 
99179
99185
  // src/handler/event.ts
99180
99186
  var pendingBySession = /* @__PURE__ */ new Map();
@@ -99230,7 +99236,7 @@ function setSessionError(sessionId, message, fields) {
99230
99236
  sessionErrorTimeouts.set(sessionId, timeoutId);
99231
99237
  }
99232
99238
  function registerPending(sessionId, payload) {
99233
- pendingBySession.set(sessionId, { ...payload, textBuffer: "" });
99239
+ pendingBySession.set(sessionId, { ...payload, textBuffer: "", expectedMessageId: void 0 });
99234
99240
  }
99235
99241
  function unregisterPending(sessionId) {
99236
99242
  pendingBySession.delete(sessionId);
@@ -99284,6 +99290,16 @@ async function handleEvent(event, deps) {
99284
99290
  if (!sessionId) break;
99285
99291
  const payload = pendingBySession.get(sessionId);
99286
99292
  if (!payload) break;
99293
+ const messageId = part.messageID;
99294
+ if (messageId) {
99295
+ if (!payload.expectedMessageId) {
99296
+ payload.expectedMessageId = messageId;
99297
+ } else if (payload.expectedMessageId !== messageId) {
99298
+ break;
99299
+ }
99300
+ } else if (payload.expectedMessageId) {
99301
+ break;
99302
+ }
99287
99303
  const delta = event.properties.delta;
99288
99304
  if (delta) {
99289
99305
  payload.textBuffer += delta;
@@ -99343,6 +99359,9 @@ function buildSessionKey(chatType, id) {
99343
99359
  function generateSessionTitle(sessionKey) {
99344
99360
  return `${TITLE_PREFIX}-${sessionKey}-${Date.now()}`;
99345
99361
  }
99362
+ function getCachedSession(sessionKey) {
99363
+ return sessionCache.get(sessionKey);
99364
+ }
99346
99365
  async function getOrCreateSession(client, sessionKey, directory) {
99347
99366
  const cached = sessionCache.get(sessionKey);
99348
99367
  if (cached) return cached;
@@ -99381,9 +99400,8 @@ async function getOrCreateSession(client, sessionKey, directory) {
99381
99400
  }
99382
99401
 
99383
99402
  // src/handler/chat.ts
99384
- var SSE_RACE_WAIT_MS = 100;
99385
99403
  var activeAutoPrompts = /* @__PURE__ */ new Map();
99386
- async function handleChat(ctx, deps) {
99404
+ async function handleChat(ctx, deps, signal) {
99387
99405
  const { content, chatId, chatType, senderId, shouldReply, messageType, rawContent, messageId } = ctx;
99388
99406
  if (!content.trim() && messageType === "text") return;
99389
99407
  const { config, client, feishuClient, log, directory } = deps;
@@ -99411,7 +99429,7 @@ async function handleChat(ctx, deps) {
99411
99429
  const baseBody = { parts };
99412
99430
  if (!shouldReply) {
99413
99431
  try {
99414
- await client.session.prompt({
99432
+ await client.session.promptAsync({
99415
99433
  path: { id: session.id },
99416
99434
  query,
99417
99435
  body: { ...baseBody, noReply: true }
@@ -99488,12 +99506,12 @@ async function handleChat(ctx, deps) {
99488
99506
  }
99489
99507
  try {
99490
99508
  clearSessionError(session.id);
99491
- await client.session.prompt({
99509
+ await client.session.promptAsync({
99492
99510
  path: { id: session.id },
99493
99511
  query,
99494
99512
  body: baseBody
99495
99513
  });
99496
- const finalText = await pollForResponse(client, session.id, { timeout, pollInterval, stablePolls, query });
99514
+ const finalText = await pollForResponse(client, session.id, { timeout, pollInterval, stablePolls, query, signal });
99497
99515
  log("info", "\u6A21\u578B\u54CD\u5E94\u5B8C\u6210", {
99498
99516
  sessionKey,
99499
99517
  sessionId: session.id,
@@ -99503,21 +99521,21 @@ async function handleChat(ctx, deps) {
99503
99521
  await replyOrUpdate(feishuClient, chatId, placeholderId, finalText || "\u26A0\uFE0F \u54CD\u5E94\u8D85\u65F6");
99504
99522
  await runAutoPromptLoop(session.id);
99505
99523
  } catch (err) {
99524
+ if (err instanceof Error && err.name === "AbortError") {
99525
+ log("info", "\u5904\u7406\u88AB\u4E2D\u65AD", { sessionKey, sessionId: session.id });
99526
+ if (placeholderId) {
99527
+ await deleteMessage(feishuClient, placeholderId).catch(() => {
99528
+ });
99529
+ }
99530
+ return;
99531
+ }
99506
99532
  let sessionError;
99507
99533
  if (err instanceof SessionErrorDetected) {
99508
99534
  sessionError = err.sessionError;
99509
99535
  clearSessionError(session.id);
99510
99536
  } else {
99511
- await new Promise((r) => setTimeout(r, SSE_RACE_WAIT_MS));
99512
99537
  sessionError = getSessionError(session.id);
99513
99538
  clearSessionError(session.id);
99514
- if (!sessionError) {
99515
- const thrownFields = extractErrorFields(err);
99516
- if (isModelError(thrownFields)) {
99517
- const thrownMsg = err instanceof Error ? err.message : String(err);
99518
- sessionError = { message: thrownMsg, fields: thrownFields };
99519
- }
99520
- }
99521
99539
  }
99522
99540
  if (sessionError) {
99523
99541
  log("info", "\u9519\u8BEF\u5B57\u6BB5\u68C0\u67E5", {
@@ -99549,12 +99567,12 @@ async function handleChat(ctx, deps) {
99549
99567
  modelID: modelOverride.modelID
99550
99568
  });
99551
99569
  clearSessionError(session.id);
99552
- await client.session.prompt({
99570
+ await client.session.promptAsync({
99553
99571
  path: { id: session.id },
99554
99572
  query,
99555
99573
  body: { ...baseBody, model: modelOverride }
99556
99574
  });
99557
- const finalText = await pollForResponse(client, session.id, { timeout, pollInterval, stablePolls, query });
99575
+ const finalText = await pollForResponse(client, session.id, { timeout, pollInterval, stablePolls, query, signal });
99558
99576
  log("info", "\u6A21\u578B\u6062\u590D\u540E\u54CD\u5E94\u5B8C\u6210", {
99559
99577
  sessionKey,
99560
99578
  sessionId: session.id,
@@ -99572,19 +99590,25 @@ async function handleChat(ctx, deps) {
99572
99590
  return;
99573
99591
  }
99574
99592
  } catch (recoveryErr) {
99593
+ if (recoveryErr instanceof Error && recoveryErr.name === "AbortError") {
99594
+ log("info", "\u6A21\u578B\u6062\u590D\u88AB\u4E2D\u65AD", { sessionKey });
99595
+ if (placeholderId) {
99596
+ await deleteMessage(feishuClient, placeholderId).catch(() => {
99597
+ });
99598
+ }
99599
+ return;
99600
+ }
99575
99601
  const errMsg = recoveryErr instanceof Error ? recoveryErr.message : String(recoveryErr);
99576
99602
  if (recoveryErr instanceof SessionErrorDetected) {
99577
99603
  sessionError = recoveryErr.sessionError;
99578
99604
  clearSessionError(session.id);
99579
99605
  } else {
99580
- await new Promise((r) => setTimeout(r, SSE_RACE_WAIT_MS));
99581
99606
  const sseError = getSessionError(session.id);
99582
99607
  if (sseError) {
99583
99608
  sessionError = sseError;
99584
99609
  clearSessionError(session.id);
99585
99610
  } else {
99586
- const thrownFields = extractErrorFields(recoveryErr);
99587
- sessionError = { message: errMsg, fields: thrownFields };
99611
+ sessionError = { message: errMsg, fields: [] };
99588
99612
  }
99589
99613
  }
99590
99614
  log("error", "\u6A21\u578B\u6062\u590D\u5931\u8D25", {
@@ -99607,7 +99631,7 @@ async function handleChat(ctx, deps) {
99607
99631
  sessionKey,
99608
99632
  chatType,
99609
99633
  error: thrownError,
99610
- ...sessionError ? { sessionError: sessionError.message } : { sseRaceMiss: true }
99634
+ ...sessionError ? { sessionError: sessionError.message } : {}
99611
99635
  });
99612
99636
  const msg = "\u274C " + errorMessage;
99613
99637
  await replyOrUpdate(feishuClient, chatId, placeholderId, msg);
@@ -99718,6 +99742,113 @@ function extractLastAssistantText(messages) {
99718
99742
  return parts.filter((p) => p.type === "text").map((p) => p.text ?? "").join("\n").trim();
99719
99743
  }
99720
99744
 
99745
+ // src/handler/session-queue.ts
99746
+ var states = /* @__PURE__ */ new Map();
99747
+ function getOrCreateState(sessionKey) {
99748
+ const existing = states.get(sessionKey);
99749
+ if (existing) return existing;
99750
+ const state = {
99751
+ controller: null,
99752
+ currentTask: null,
99753
+ queue: [],
99754
+ processing: false
99755
+ };
99756
+ states.set(sessionKey, state);
99757
+ return state;
99758
+ }
99759
+ function cleanupStateIfIdle(sessionKey, state) {
99760
+ if (!state.processing && state.queue.length === 0) {
99761
+ states.delete(sessionKey);
99762
+ }
99763
+ }
99764
+ async function enqueueMessage(ctx, deps) {
99765
+ if (!ctx.shouldReply) {
99766
+ await handleChat(ctx, deps);
99767
+ return;
99768
+ }
99769
+ const sessionKey = buildSessionKey(
99770
+ ctx.chatType,
99771
+ ctx.chatType === "p2p" ? ctx.senderId : ctx.chatId
99772
+ );
99773
+ if (ctx.chatType === "p2p") {
99774
+ await handleP2PMessage(sessionKey, ctx, deps);
99775
+ } else {
99776
+ await handleGroupMessage(sessionKey, ctx, deps);
99777
+ }
99778
+ }
99779
+ async function handleP2PMessage(sessionKey, ctx, deps) {
99780
+ const state = getOrCreateState(sessionKey);
99781
+ while (state.processing) {
99782
+ if (state.controller) {
99783
+ state.controller.abort();
99784
+ await abortServerSession(sessionKey, deps);
99785
+ }
99786
+ if (state.currentTask) {
99787
+ await state.currentTask.catch(() => {
99788
+ });
99789
+ }
99790
+ }
99791
+ const controller = new AbortController();
99792
+ state.controller = controller;
99793
+ state.processing = true;
99794
+ const task = processMessage(ctx, deps, controller.signal).finally(() => {
99795
+ state.processing = false;
99796
+ state.controller = null;
99797
+ state.currentTask = null;
99798
+ cleanupStateIfIdle(sessionKey, state);
99799
+ });
99800
+ state.currentTask = task;
99801
+ await task;
99802
+ }
99803
+ async function handleGroupMessage(sessionKey, ctx, deps) {
99804
+ const state = getOrCreateState(sessionKey);
99805
+ state.queue.push({ ctx, deps });
99806
+ if (state.processing) return;
99807
+ await drainLoop(sessionKey, state);
99808
+ }
99809
+ async function drainLoop(sessionKey, state) {
99810
+ state.processing = true;
99811
+ try {
99812
+ while (state.queue.length > 0) {
99813
+ const item = state.queue.shift();
99814
+ if (!item) break;
99815
+ const controller = new AbortController();
99816
+ state.controller = controller;
99817
+ try {
99818
+ await processMessage(item.ctx, item.deps, controller.signal);
99819
+ } catch (err) {
99820
+ item.deps.log("error", "\u7FA4\u804A\u961F\u5217\u6D88\u606F\u5904\u7406\u5931\u8D25", {
99821
+ sessionKey,
99822
+ error: err instanceof Error ? err.message : String(err)
99823
+ });
99824
+ } finally {
99825
+ state.controller = null;
99826
+ }
99827
+ }
99828
+ } finally {
99829
+ state.processing = false;
99830
+ cleanupStateIfIdle(sessionKey, state);
99831
+ }
99832
+ }
99833
+ async function processMessage(ctx, deps, signal) {
99834
+ await handleChat(ctx, deps, signal);
99835
+ }
99836
+ async function abortServerSession(sessionKey, deps) {
99837
+ const { client, log, directory } = deps;
99838
+ const query = directory ? { directory } : void 0;
99839
+ const cached = getCachedSession(sessionKey);
99840
+ if (!cached) return;
99841
+ try {
99842
+ await client.session.abort({ path: { id: cached.id }, query });
99843
+ log("info", "\u5DF2\u4E2D\u65AD\u5F53\u524D\u4F1A\u8BDD\u5904\u7406", { sessionKey, sessionId: cached.id });
99844
+ } catch (err) {
99845
+ log("warn", "\u4E2D\u65AD\u4F1A\u8BDD\u5931\u8D25", {
99846
+ sessionKey,
99847
+ error: err instanceof Error ? err.message : String(err)
99848
+ });
99849
+ }
99850
+ }
99851
+
99721
99852
  // src/feishu/history.ts
99722
99853
  var DEFAULT_PAGE_SIZE = 50;
99723
99854
  async function ingestGroupHistory(feishuClient, opencodeClient, chatId, options) {
@@ -99802,6 +99933,7 @@ ${body}`;
99802
99933
 
99803
99934
  // src/index.ts
99804
99935
  var SERVICE_NAME = "opencode-feishu";
99936
+ var LOG_PREFIX = "[feishu]";
99805
99937
  var isDebug = !!process.env.FEISHU_DEBUG;
99806
99938
  var DEFAULT_CONFIG = {
99807
99939
  timeout: 12e4,
@@ -99823,14 +99955,15 @@ var FeishuPlugin = async (ctx) => {
99823
99955
  const { client } = ctx;
99824
99956
  let gateway = null;
99825
99957
  const log = (level, message, extra) => {
99958
+ const prefixed = `${LOG_PREFIX} ${message}`;
99826
99959
  if (isDebug) {
99827
- console.error(JSON.stringify({ ts: (/* @__PURE__ */ new Date()).toISOString(), service: SERVICE_NAME, level, message, ...extra }));
99960
+ console.error(JSON.stringify({ ts: (/* @__PURE__ */ new Date()).toISOString(), service: SERVICE_NAME, level, message: prefixed, ...extra }));
99828
99961
  }
99829
99962
  client.app.log({
99830
99963
  body: {
99831
99964
  service: SERVICE_NAME,
99832
99965
  level,
99833
- message,
99966
+ message: prefixed,
99834
99967
  extra
99835
99968
  }
99836
99969
  }).catch(() => {
@@ -99884,7 +100017,7 @@ var FeishuPlugin = async (ctx) => {
99884
100017
  botOpenId,
99885
100018
  onMessage: async (msgCtx) => {
99886
100019
  if (!msgCtx.content.trim() || !gateway) return;
99887
- await handleChat(msgCtx, {
100020
+ await enqueueMessage(msgCtx, {
99888
100021
  config: resolvedConfig,
99889
100022
  client,
99890
100023
  feishuClient: gateway.client,