opencode-chat-channel 1.2.11 → 1.2.12

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.
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,qBAAqB,CAAC;AAsVlD,eAAO,MAAM,iBAAiB,EAAE,MAyF/B,CAAC;AAEF,eAAe,iBAAiB,CAAC;AAMjC,YAAY,EAAE,WAAW,EAAE,cAAc,EAAE,WAAW,EAAE,eAAe,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,qBAAqB,CAAC;AAuPlD,eAAO,MAAM,iBAAiB,EAAE,MA6G/B,CAAC;AAEF,eAAe,iBAAiB,CAAC;AAMjC,YAAY,EAAE,WAAW,EAAE,cAAc,EAAE,WAAW,EAAE,eAAe,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC"}
package/dist/index.js CHANGED
@@ -2,7 +2,17 @@
2
2
  import { join } from "path";
3
3
 
4
4
  // src/session-manager.ts
5
- import { readFileSync } from "fs";
5
+ import { readFileSync, appendFileSync, mkdirSync } from "fs";
6
+ var LOG_DIR = `${process.env["HOME"] ?? "/tmp"}/.local/share/opencode/log`;
7
+ var LOG_FILE = `${LOG_DIR}/chat-channel.log`;
8
+ function fileLog(level, message) {
9
+ try {
10
+ mkdirSync(LOG_DIR, { recursive: true });
11
+ const ts = new Date().toISOString();
12
+ appendFileSync(LOG_FILE, `${ts} [${level.toUpperCase()}] ${message}
13
+ `, "utf8");
14
+ } catch {}
15
+ }
6
16
  var SESSION_TTL_MS = 2 * 60 * 60 * 1000;
7
17
 
8
18
  class SessionManager {
@@ -320,28 +330,16 @@ function resolveEnabledChannels(client) {
320
330
  }
321
331
  return enabled;
322
332
  }
323
- function stripMarkdownTables(text) {
324
- return text.split(`
325
- `).map((line) => {
326
- const trimmed = line.trim();
327
- if (/^\|[-:\s|]+\|$/.test(trimmed))
328
- return "";
329
- if (trimmed.startsWith("|"))
330
- return "[表格内容]";
331
- return line;
332
- }).filter((line, i, arr) => !(line === "[表格内容]" && arr[i - 1] === "[表格内容]")).join(`
333
- `).trim();
334
- }
335
- var REASONING_PREVIEW_LEN = 200;
336
- var PATCH_THROTTLE_MS = 3000;
337
- function createMessageHandler(channel, sessionManager, client) {
333
+ function createMessageHandler(channel, sessionManager, client, pendingMap) {
338
334
  return async (msg) => {
339
335
  const { userId, replyTarget, text } = msg;
336
+ const incomingMsg = `[${channel.name}] 收到消息: "${text.slice(0, 80)}${text.length > 80 ? "..." : ""}"`;
337
+ fileLog("info", incomingMsg);
340
338
  await client.app.log({
341
339
  body: {
342
340
  service: "chat-channel",
343
341
  level: "info",
344
- message: `[${channel.name}] 收到消息: "${text.slice(0, 80)}${text.length > 80 ? "..." : ""}"`,
342
+ message: incomingMsg,
345
343
  extra: { userId, replyTarget }
346
344
  }
347
345
  });
@@ -354,20 +352,14 @@ function createMessageHandler(channel, sessionManager, client) {
354
352
  sessionId = await sessionManager.getOrCreate(userId);
355
353
  } catch (err) {
356
354
  const errorMsg = err?.message ?? String(err);
355
+ const errMsg1 = `[${channel.name}] 获取 session 失败: ${errorMsg}`;
356
+ fileLog("error", errMsg1);
357
357
  await client.app.log({
358
- body: { service: "chat-channel", level: "error", message: `[${channel.name}] 获取 session 失败: ${errorMsg}`, extra: { userId } }
358
+ body: { service: "chat-channel", level: "error", message: errMsg1, extra: { userId } }
359
359
  });
360
360
  await channel.send(replyTarget, `⚠️ 出错了:${errorMsg}`);
361
361
  return;
362
362
  }
363
- let eventStream = null;
364
- try {
365
- eventStream = await client.event.subscribe();
366
- } catch (err) {
367
- await client.app.log({
368
- body: { service: "chat-channel", level: "warn", message: `[${channel.name}] SSE 订阅失败,降级为轮询: ${String(err)}` }
369
- });
370
- }
371
363
  try {
372
364
  await client.session.promptAsync({
373
365
  path: { id: sessionId },
@@ -387,123 +379,54 @@ function createMessageHandler(channel, sessionManager, client) {
387
379
  }
388
380
  return;
389
381
  }
390
- await consumeSessionEvents(client, channel, sessionId, replyTarget, thinkingMsgId, eventStream);
382
+ await waitForSessionAndReply(client, channel, sessionId, replyTarget, thinkingMsgId, pendingMap);
391
383
  };
392
384
  }
393
- async function consumeSessionEvents(client, channel, sessionId, replyTarget, thinkingMsgId, eventStream) {
394
- let lastPatchAt = 0;
395
- let reasoningAccum = "";
396
- const log = (level, message) => void client.app.log({ body: { service: "chat-channel", level, message } });
397
- async function throttledPatch(text, force = false) {
398
- if (!thinkingMsgId || !channel.updateThinkingCard)
399
- return;
400
- const now = Date.now();
401
- if (!force && now - lastPatchAt < PATCH_THROTTLE_MS)
402
- return;
403
- lastPatchAt = now;
404
- await channel.updateThinkingCard(thinkingMsgId, text);
405
- }
406
- if (eventStream) {
407
- let alreadyDone = false;
408
- try {
409
- const statusRes = await client.session.status();
410
- const allStatuses = statusRes.data ?? {};
411
- const sessionStatus = allStatuses[sessionId];
412
- if (!sessionStatus || sessionStatus.type === "idle") {
413
- alreadyDone = true;
414
- log("info", `[${channel.name}] session 已完成,跳过 SSE 等待`);
415
- }
416
- } catch {}
417
- if (!alreadyDone) {
418
- const SSE_TIMEOUT_MS = 3 * 60 * 1000;
419
- const abortController = new AbortController;
420
- const timeoutId = setTimeout(() => {
421
- log("warn", `[${channel.name}] SSE 等待超时,强制结束`);
422
- abortController.abort();
423
- }, SSE_TIMEOUT_MS);
424
- try {
425
- for await (const event of eventStream.stream) {
426
- if (abortController.signal.aborted)
427
- break;
428
- if (!isSessionEvent(event, sessionId))
429
- continue;
430
- if (event.type === "message.part.updated") {
431
- const part = event.properties?.part;
432
- if (!part)
433
- continue;
434
- if (part.type === "reasoning" && part.text) {
435
- reasoningAccum = part.text;
436
- const preview = stripMarkdownTables(reasoningAccum.slice(0, REASONING_PREVIEW_LEN));
437
- const suffix = reasoningAccum.length > REASONING_PREVIEW_LEN ? "..." : "";
438
- await throttledPatch(`\uD83D\uDCAD **正在思考...**
439
-
440
- ${preview}${suffix}`);
441
- } else if (part.type === "tool" && part.state?.status === "running") {
442
- const toolLabel = (part.tool ?? "") || "工具";
443
- await throttledPatch(`\uD83D\uDD27 **正在使用工具:${toolLabel}**`);
444
- }
445
- }
446
- if (event.type === "session.idle" || event.type === "session.error") {
447
- break;
448
- }
449
- }
450
- } catch (err) {
451
- if (!abortController.signal.aborted) {
452
- log("warn", `[${channel.name}] SSE 事件流中断: ${String(err)}`);
453
- }
454
- } finally {
385
+ async function waitForSessionAndReply(client, channel, sessionId, replyTarget, thinkingMsgId, pendingMap) {
386
+ const log = (level, message) => {
387
+ fileLog(level, message);
388
+ client.app.log({ body: { service: "chat-channel", level, message } });
389
+ };
390
+ const SESSION_TIMEOUT_MS = 5 * 60 * 1000;
391
+ await new Promise((resolve, reject) => {
392
+ const timeoutId = setTimeout(() => {
393
+ pendingMap.delete(sessionId);
394
+ log("warn", `[${channel.name}] session 等待超时(5分钟),强制继续读取回复`);
395
+ resolve();
396
+ }, SESSION_TIMEOUT_MS);
397
+ pendingMap.set(sessionId, {
398
+ resolve: () => {
399
+ clearTimeout(timeoutId);
400
+ pendingMap.delete(sessionId);
401
+ resolve();
402
+ },
403
+ reject: (err) => {
455
404
  clearTimeout(timeoutId);
405
+ pendingMap.delete(sessionId);
406
+ reject(err);
456
407
  }
457
- }
458
- } else {
459
- await pollForSessionCompletion(client, channel.name, sessionId);
460
- }
408
+ });
409
+ }).catch((err) => {
410
+ log("error", `[${channel.name}] session 出错: ${err.message}`);
411
+ });
461
412
  let responseText = null;
462
413
  try {
463
414
  const messagesRes = await client.session.messages({ path: { id: sessionId } });
464
415
  const messages = messagesRes.data ?? [];
465
416
  const lastAssistant = [...messages].reverse().find((m) => m.info?.role === "assistant");
417
+ log("info", `[${channel.name}] [diag] messages count=${messages.length}, lastAssistant=${lastAssistant ? `id=${lastAssistant.info?.id} parts=${JSON.stringify((lastAssistant.parts ?? []).map((p) => ({ type: p.type, textLen: p.text?.length ?? 0 })))}` : "null"}`);
466
418
  if (lastAssistant) {
467
419
  responseText = extractResponseText(lastAssistant.parts ?? []);
468
420
  }
469
421
  } catch (err) {
470
422
  log("error", `[${channel.name}] 获取最终回复失败: ${String(err)}`);
471
423
  }
424
+ log("info", `[${channel.name}] [diag] responseText length=${responseText?.length ?? 0} preview="${responseText?.slice(0, 80)}"`);
472
425
  if (!responseText) {
473
426
  responseText = "(AI 没有返回文字回复)";
474
427
  }
475
428
  await channel.send(replyTarget, responseText);
476
429
  }
477
- async function pollForSessionCompletion(client, channelName, sessionId) {
478
- const POLL_INTERVAL_MS = 1000;
479
- const MAX_WAIT_MS = 5 * 60 * 1000;
480
- const started = Date.now();
481
- while (Date.now() - started < MAX_WAIT_MS) {
482
- await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS));
483
- try {
484
- const res = await client.session.status();
485
- const allStatuses = res.data ?? {};
486
- const sessionStatus = allStatuses[sessionId];
487
- if (!sessionStatus || sessionStatus.type === "idle")
488
- break;
489
- } catch {
490
- client.app.log({
491
- body: { service: "chat-channel", level: "warn", message: `[${channelName}] 轮询 session 状态失败,继续等待...` }
492
- });
493
- }
494
- }
495
- }
496
- function isSessionEvent(event, sessionId) {
497
- if (!event || !event.type)
498
- return false;
499
- const props = event.properties;
500
- if (!props)
501
- return false;
502
- if (event.type === "message.part.updated") {
503
- return props.part?.sessionID === sessionId;
504
- }
505
- return props.sessionID === sessionId || props.id === sessionId;
506
- }
507
430
  var ChatChannelPlugin = async ({ client }) => {
508
431
  const configDir = join(process.env["HOME"] ?? `/Users/${process.env["USER"] ?? "unknown"}`, ".config", "opencode");
509
432
  loadDotEnv(join(configDir, ".env"));
@@ -535,12 +458,13 @@ var ChatChannelPlugin = async ({ client }) => {
535
458
  });
536
459
  return {};
537
460
  }
461
+ const pendingMap = new Map;
538
462
  const cleanupTimers = [];
539
463
  for (const channel of channels) {
540
464
  const sessionManager = new SessionManager(client, channel.name, (userId) => `${channel.name} 对话 · ${userId}`);
541
465
  const timer = sessionManager.startAutoCleanup();
542
466
  cleanupTimers.push(timer);
543
- const handleMessage = createMessageHandler(channel, sessionManager, client);
467
+ const handleMessage = createMessageHandler(channel, sessionManager, client, pendingMap);
544
468
  await channel.start(handleMessage);
545
469
  }
546
470
  await client.app.log({
@@ -552,15 +476,29 @@ var ChatChannelPlugin = async ({ client }) => {
552
476
  });
553
477
  return {
554
478
  event: async ({ event }) => {
479
+ const props = event.properties;
480
+ if (event.type === "session.idle") {
481
+ const sessionId = props?.["sessionID"] ?? props?.["id"];
482
+ if (sessionId) {
483
+ fileLog("info", `[diag] event hook: session.idle sessionId=${sessionId}`);
484
+ pendingMap.get(sessionId)?.resolve();
485
+ }
486
+ }
555
487
  if (event.type === "session.error") {
488
+ const sessionId = props?.["sessionID"] ?? props?.["id"];
489
+ const errMsg = props?.["message"] ?? "unknown error";
556
490
  await client.app.log({
557
491
  body: {
558
492
  service: "chat-channel",
559
493
  level: "warn",
560
494
  message: "opencode session 出现错误",
561
- extra: event.properties
495
+ extra: props
562
496
  }
563
497
  });
498
+ if (sessionId) {
499
+ fileLog("info", `[diag] event hook: session.error sessionId=${sessionId} err=${errMsg}`);
500
+ pendingMap.get(sessionId)?.reject(new Error(errMsg));
501
+ }
564
502
  }
565
503
  }
566
504
  };
@@ -5,6 +5,11 @@
5
5
  * 与具体渠道无关,所有 channel 实现共享同一套逻辑。
6
6
  */
7
7
  import type { PluginClient } from "./types.js";
8
+ /**
9
+ * 写日志到文件(同步追加),同时调用 client.app.log()。
10
+ * 用于替代纯依赖 client.app.log() 的场景,确保日志落盘可查。
11
+ */
12
+ export declare function fileLog(level: "info" | "warn" | "error", message: string): void;
8
13
  export declare class SessionManager {
9
14
  private readonly client;
10
15
  private readonly channel;
@@ -1 +1 @@
1
- {"version":3,"file":"session-manager.d.ts","sourceRoot":"","sources":["../src/session-manager.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAGH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAgB/C,qBAAa,cAAc;IAOvB,OAAO,CAAC,QAAQ,CAAC,MAAM;IACvB,OAAO,CAAC,QAAQ,CAAC,OAAO;IACxB,OAAO,CAAC,QAAQ,CAAC,OAAO;IAR1B;;;;OAIG;gBAEgB,MAAM,EAAE,YAAY,EACpB,OAAO,EAAE,MAAM,EACf,OAAO,GAAE,CAAC,MAAM,EAAE,MAAM,KAAK,MACxB;IAGxB,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAkC;IAE3D,wCAAwC;IAClC,WAAW,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IA6BlD,2BAA2B;IAC3B,OAAO,IAAI,IAAI;IASf,oDAAoD;IACpD,gBAAgB,CAAC,UAAU,SAAiB,GAAG,UAAU,CAAC,OAAO,WAAW,CAAC;CAG9E;AAID;;;GAGG;AACH,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,OAAO,EAAE,GAAG,MAAM,CAO5D;AAED;;;GAGG;AACH,wBAAgB,UAAU,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAiBhD"}
1
+ {"version":3,"file":"session-manager.d.ts","sourceRoot":"","sources":["../src/session-manager.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAGH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAO/C;;;GAGG;AACH,wBAAgB,OAAO,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,OAAO,EAAE,OAAO,EAAE,MAAM,GAAG,IAAI,CAQ/E;AAgBD,qBAAa,cAAc;IAOvB,OAAO,CAAC,QAAQ,CAAC,MAAM;IACvB,OAAO,CAAC,QAAQ,CAAC,OAAO;IACxB,OAAO,CAAC,QAAQ,CAAC,OAAO;IAR1B;;;;OAIG;gBAEgB,MAAM,EAAE,YAAY,EACpB,OAAO,EAAE,MAAM,EACf,OAAO,GAAE,CAAC,MAAM,EAAE,MAAM,KAAK,MACxB;IAGxB,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAkC;IAE3D,wCAAwC;IAClC,WAAW,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IA6BlD,2BAA2B;IAC3B,OAAO,IAAI,IAAI;IASf,oDAAoD;IACpD,gBAAgB,CAAC,UAAU,SAAiB,GAAG,UAAU,CAAC,OAAO,WAAW,CAAC;CAG9E;AAID;;;GAGG;AACH,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,OAAO,EAAE,GAAG,MAAM,CAO5D;AAED;;;GAGG;AACH,wBAAgB,UAAU,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAiBhD"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-chat-channel",
3
- "version": "1.2.11",
3
+ "version": "1.2.12",
4
4
  "description": "opencode plugin — multi-channel bot (Feishu/Lark, WeCom) with extensible ChatChannel interface",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",