opencode-feishu 0.7.13 → 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);
@@ -99238,30 +99244,41 @@ function unregisterPending(sessionId) {
99238
99244
  function extractErrorFields(error) {
99239
99245
  if (typeof error === "string") return [error];
99240
99246
  if (error && typeof error === "object") {
99241
- const e = error;
99242
- const explicit = [e.message, e.type, e.name];
99243
- const enumerable = Object.values(e);
99244
- const fields = [...explicit, ...enumerable].filter((v) => typeof v === "string" && v.length > 0);
99245
- if (e.data && typeof e.data === "object" && "message" in e.data) {
99246
- const dataMsg = e.data.message;
99247
- if (typeof dataMsg === "string" && dataMsg.length > 0) fields.push(dataMsg);
99248
- }
99249
- if (e.data && typeof e.data === "object" && "error" in e.data) {
99250
- const dataErr = e.data.error;
99251
- if (dataErr && typeof dataErr === "object") {
99252
- const errStrings = Object.values(dataErr).filter((v) => typeof v === "string" && v.length > 0);
99253
- fields.push(...errStrings);
99254
- }
99255
- }
99247
+ const fields = [];
99248
+ collectStrings(error, fields, 3);
99256
99249
  return [...new Set(fields)];
99257
99250
  }
99258
99251
  return [String(error)];
99259
99252
  }
99253
+ function collectStrings(obj, out, maxDepth) {
99254
+ if (maxDepth <= 0 || !obj || typeof obj !== "object") return;
99255
+ const e = obj;
99256
+ for (const key of ["message", "type", "name"]) {
99257
+ const v = e[key];
99258
+ if (typeof v === "string" && v.length > 0) out.push(v);
99259
+ }
99260
+ for (const v of Object.values(e)) {
99261
+ if (typeof v === "string" && v.length > 0) out.push(v);
99262
+ else if (Array.isArray(v)) {
99263
+ for (const item of v) collectStrings(item, out, maxDepth - 1);
99264
+ } else if (v && typeof v === "object") collectStrings(v, out, maxDepth - 1);
99265
+ }
99266
+ }
99260
99267
  function isModelError(fields) {
99261
- const patterns = ["model not found", "modelnotfound", "model not supported", "model_not_supported", "model is not supported"];
99268
+ const exactPatterns = [
99269
+ "model not found",
99270
+ "modelnotfound",
99271
+ "model_not_found",
99272
+ "model not supported",
99273
+ "model_not_supported",
99274
+ "model is not supported"
99275
+ ];
99276
+ const negativeWords = ["not", "unsupported", "invalid", "unavailable", "unknown", "does not", "doesn't", "cannot", "\u4E0D\u652F\u6301", "\u4E0D\u5B58\u5728", "\u65E0\u6548"];
99262
99277
  return fields.some((f) => {
99263
99278
  const l = f.toLowerCase();
99264
- return patterns.some((p) => l.includes(p));
99279
+ if (exactPatterns.some((p) => l.includes(p))) return true;
99280
+ if (l.includes("model") && negativeWords.some((w) => l.includes(w))) return true;
99281
+ return false;
99265
99282
  });
99266
99283
  }
99267
99284
  async function handleEvent(event, deps) {
@@ -99273,6 +99290,16 @@ async function handleEvent(event, deps) {
99273
99290
  if (!sessionId) break;
99274
99291
  const payload = pendingBySession.get(sessionId);
99275
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
+ }
99276
99303
  const delta = event.properties.delta;
99277
99304
  if (delta) {
99278
99305
  payload.textBuffer += delta;
@@ -99332,6 +99359,9 @@ function buildSessionKey(chatType, id) {
99332
99359
  function generateSessionTitle(sessionKey) {
99333
99360
  return `${TITLE_PREFIX}-${sessionKey}-${Date.now()}`;
99334
99361
  }
99362
+ function getCachedSession(sessionKey) {
99363
+ return sessionCache.get(sessionKey);
99364
+ }
99335
99365
  async function getOrCreateSession(client, sessionKey, directory) {
99336
99366
  const cached = sessionCache.get(sessionKey);
99337
99367
  if (cached) return cached;
@@ -99370,9 +99400,8 @@ async function getOrCreateSession(client, sessionKey, directory) {
99370
99400
  }
99371
99401
 
99372
99402
  // src/handler/chat.ts
99373
- var SSE_RACE_WAIT_MS = 100;
99374
99403
  var activeAutoPrompts = /* @__PURE__ */ new Map();
99375
- async function handleChat(ctx, deps) {
99404
+ async function handleChat(ctx, deps, signal) {
99376
99405
  const { content, chatId, chatType, senderId, shouldReply, messageType, rawContent, messageId } = ctx;
99377
99406
  if (!content.trim() && messageType === "text") return;
99378
99407
  const { config, client, feishuClient, log, directory } = deps;
@@ -99400,7 +99429,7 @@ async function handleChat(ctx, deps) {
99400
99429
  const baseBody = { parts };
99401
99430
  if (!shouldReply) {
99402
99431
  try {
99403
- await client.session.prompt({
99432
+ await client.session.promptAsync({
99404
99433
  path: { id: session.id },
99405
99434
  query,
99406
99435
  body: { ...baseBody, noReply: true }
@@ -99477,12 +99506,12 @@ async function handleChat(ctx, deps) {
99477
99506
  }
99478
99507
  try {
99479
99508
  clearSessionError(session.id);
99480
- await client.session.prompt({
99509
+ await client.session.promptAsync({
99481
99510
  path: { id: session.id },
99482
99511
  query,
99483
99512
  body: baseBody
99484
99513
  });
99485
- const finalText = await pollForResponse(client, session.id, { timeout, pollInterval, stablePolls, query });
99514
+ const finalText = await pollForResponse(client, session.id, { timeout, pollInterval, stablePolls, query, signal });
99486
99515
  log("info", "\u6A21\u578B\u54CD\u5E94\u5B8C\u6210", {
99487
99516
  sessionKey,
99488
99517
  sessionId: session.id,
@@ -99492,21 +99521,21 @@ async function handleChat(ctx, deps) {
99492
99521
  await replyOrUpdate(feishuClient, chatId, placeholderId, finalText || "\u26A0\uFE0F \u54CD\u5E94\u8D85\u65F6");
99493
99522
  await runAutoPromptLoop(session.id);
99494
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
+ }
99495
99532
  let sessionError;
99496
99533
  if (err instanceof SessionErrorDetected) {
99497
99534
  sessionError = err.sessionError;
99498
99535
  clearSessionError(session.id);
99499
99536
  } else {
99500
- await new Promise((r) => setTimeout(r, SSE_RACE_WAIT_MS));
99501
99537
  sessionError = getSessionError(session.id);
99502
99538
  clearSessionError(session.id);
99503
- if (!sessionError) {
99504
- const thrownFields = extractErrorFields(err);
99505
- if (isModelError(thrownFields)) {
99506
- const thrownMsg = err instanceof Error ? err.message : String(err);
99507
- sessionError = { message: thrownMsg, fields: thrownFields };
99508
- }
99509
- }
99510
99539
  }
99511
99540
  if (sessionError) {
99512
99541
  log("info", "\u9519\u8BEF\u5B57\u6BB5\u68C0\u67E5", {
@@ -99538,12 +99567,12 @@ async function handleChat(ctx, deps) {
99538
99567
  modelID: modelOverride.modelID
99539
99568
  });
99540
99569
  clearSessionError(session.id);
99541
- await client.session.prompt({
99570
+ await client.session.promptAsync({
99542
99571
  path: { id: session.id },
99543
99572
  query,
99544
99573
  body: { ...baseBody, model: modelOverride }
99545
99574
  });
99546
- const finalText = await pollForResponse(client, session.id, { timeout, pollInterval, stablePolls, query });
99575
+ const finalText = await pollForResponse(client, session.id, { timeout, pollInterval, stablePolls, query, signal });
99547
99576
  log("info", "\u6A21\u578B\u6062\u590D\u540E\u54CD\u5E94\u5B8C\u6210", {
99548
99577
  sessionKey,
99549
99578
  sessionId: session.id,
@@ -99561,19 +99590,25 @@ async function handleChat(ctx, deps) {
99561
99590
  return;
99562
99591
  }
99563
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
+ }
99564
99601
  const errMsg = recoveryErr instanceof Error ? recoveryErr.message : String(recoveryErr);
99565
99602
  if (recoveryErr instanceof SessionErrorDetected) {
99566
99603
  sessionError = recoveryErr.sessionError;
99567
99604
  clearSessionError(session.id);
99568
99605
  } else {
99569
- await new Promise((r) => setTimeout(r, SSE_RACE_WAIT_MS));
99570
99606
  const sseError = getSessionError(session.id);
99571
99607
  if (sseError) {
99572
99608
  sessionError = sseError;
99573
99609
  clearSessionError(session.id);
99574
99610
  } else {
99575
- const thrownFields = extractErrorFields(recoveryErr);
99576
- sessionError = { message: errMsg, fields: thrownFields };
99611
+ sessionError = { message: errMsg, fields: [] };
99577
99612
  }
99578
99613
  }
99579
99614
  log("error", "\u6A21\u578B\u6062\u590D\u5931\u8D25", {
@@ -99596,7 +99631,7 @@ async function handleChat(ctx, deps) {
99596
99631
  sessionKey,
99597
99632
  chatType,
99598
99633
  error: thrownError,
99599
- ...sessionError ? { sessionError: sessionError.message } : { sseRaceMiss: true }
99634
+ ...sessionError ? { sessionError: sessionError.message } : {}
99600
99635
  });
99601
99636
  const msg = "\u274C " + errorMessage;
99602
99637
  await replyOrUpdate(feishuClient, chatId, placeholderId, msg);
@@ -99707,6 +99742,113 @@ function extractLastAssistantText(messages) {
99707
99742
  return parts.filter((p) => p.type === "text").map((p) => p.text ?? "").join("\n").trim();
99708
99743
  }
99709
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
+
99710
99852
  // src/feishu/history.ts
99711
99853
  var DEFAULT_PAGE_SIZE = 50;
99712
99854
  async function ingestGroupHistory(feishuClient, opencodeClient, chatId, options) {
@@ -99791,6 +99933,7 @@ ${body}`;
99791
99933
 
99792
99934
  // src/index.ts
99793
99935
  var SERVICE_NAME = "opencode-feishu";
99936
+ var LOG_PREFIX = "[feishu]";
99794
99937
  var isDebug = !!process.env.FEISHU_DEBUG;
99795
99938
  var DEFAULT_CONFIG = {
99796
99939
  timeout: 12e4,
@@ -99812,14 +99955,15 @@ var FeishuPlugin = async (ctx) => {
99812
99955
  const { client } = ctx;
99813
99956
  let gateway = null;
99814
99957
  const log = (level, message, extra) => {
99958
+ const prefixed = `${LOG_PREFIX} ${message}`;
99815
99959
  if (isDebug) {
99816
- 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 }));
99817
99961
  }
99818
99962
  client.app.log({
99819
99963
  body: {
99820
99964
  service: SERVICE_NAME,
99821
99965
  level,
99822
- message,
99966
+ message: prefixed,
99823
99967
  extra
99824
99968
  }
99825
99969
  }).catch(() => {
@@ -99873,7 +100017,7 @@ var FeishuPlugin = async (ctx) => {
99873
100017
  botOpenId,
99874
100018
  onMessage: async (msgCtx) => {
99875
100019
  if (!msgCtx.content.trim() || !gateway) return;
99876
- await handleChat(msgCtx, {
100020
+ await enqueueMessage(msgCtx, {
99877
100021
  config: resolvedConfig,
99878
100022
  client,
99879
100023
  feishuClient: gateway.client,