opencode-chat-channel 1.2.10 → 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;AAyTlD,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,99 +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
- try {
408
- for await (const event of eventStream.stream) {
409
- if (!isSessionEvent(event, sessionId))
410
- continue;
411
- if (event.type === "message.part.updated") {
412
- const part = event.properties?.part;
413
- if (!part)
414
- continue;
415
- if (part.type === "reasoning" && part.text) {
416
- reasoningAccum = part.text;
417
- const preview = stripMarkdownTables(reasoningAccum.slice(0, REASONING_PREVIEW_LEN));
418
- const suffix = reasoningAccum.length > REASONING_PREVIEW_LEN ? "..." : "";
419
- await throttledPatch(`\uD83D\uDCAD **正在思考...**
420
-
421
- ${preview}${suffix}`);
422
- } else if (part.type === "tool" && part.state?.status === "running") {
423
- const toolLabel = (part.tool ?? "") || "工具";
424
- await throttledPatch(`\uD83D\uDD27 **正在使用工具:${toolLabel}**`);
425
- }
426
- }
427
- if (event.type === "session.idle" || event.type === "session.error") {
428
- break;
429
- }
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) => {
404
+ clearTimeout(timeoutId);
405
+ pendingMap.delete(sessionId);
406
+ reject(err);
430
407
  }
431
- } catch (err) {
432
- log("warn", `[${channel.name}] SSE 事件流中断: ${String(err)}`);
433
- }
434
- } else {
435
- await pollForSessionCompletion(client, channel.name, sessionId);
436
- }
408
+ });
409
+ }).catch((err) => {
410
+ log("error", `[${channel.name}] session 出错: ${err.message}`);
411
+ });
437
412
  let responseText = null;
438
413
  try {
439
414
  const messagesRes = await client.session.messages({ path: { id: sessionId } });
440
415
  const messages = messagesRes.data ?? [];
441
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"}`);
442
418
  if (lastAssistant) {
443
419
  responseText = extractResponseText(lastAssistant.parts ?? []);
444
420
  }
445
421
  } catch (err) {
446
422
  log("error", `[${channel.name}] 获取最终回复失败: ${String(err)}`);
447
423
  }
424
+ log("info", `[${channel.name}] [diag] responseText length=${responseText?.length ?? 0} preview="${responseText?.slice(0, 80)}"`);
448
425
  if (!responseText) {
449
426
  responseText = "(AI 没有返回文字回复)";
450
427
  }
451
428
  await channel.send(replyTarget, responseText);
452
429
  }
453
- async function pollForSessionCompletion(client, channelName, sessionId) {
454
- const POLL_INTERVAL_MS = 1000;
455
- const MAX_WAIT_MS = 5 * 60 * 1000;
456
- const started = Date.now();
457
- while (Date.now() - started < MAX_WAIT_MS) {
458
- await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS));
459
- try {
460
- const res = await client.session.status();
461
- const allStatuses = res.data ?? {};
462
- const sessionStatus = allStatuses[sessionId];
463
- if (!sessionStatus || sessionStatus.type === "idle")
464
- break;
465
- } catch {
466
- client.app.log({
467
- body: { service: "chat-channel", level: "warn", message: `[${channelName}] 轮询 session 状态失败,继续等待...` }
468
- });
469
- }
470
- }
471
- }
472
- function isSessionEvent(event, sessionId) {
473
- if (!event || !event.type)
474
- return false;
475
- const props = event.properties;
476
- if (!props)
477
- return false;
478
- if (event.type === "message.part.updated") {
479
- return props.part?.sessionID === sessionId;
480
- }
481
- return props.sessionID === sessionId || props.id === sessionId;
482
- }
483
430
  var ChatChannelPlugin = async ({ client }) => {
484
431
  const configDir = join(process.env["HOME"] ?? `/Users/${process.env["USER"] ?? "unknown"}`, ".config", "opencode");
485
432
  loadDotEnv(join(configDir, ".env"));
@@ -511,12 +458,13 @@ var ChatChannelPlugin = async ({ client }) => {
511
458
  });
512
459
  return {};
513
460
  }
461
+ const pendingMap = new Map;
514
462
  const cleanupTimers = [];
515
463
  for (const channel of channels) {
516
464
  const sessionManager = new SessionManager(client, channel.name, (userId) => `${channel.name} 对话 · ${userId}`);
517
465
  const timer = sessionManager.startAutoCleanup();
518
466
  cleanupTimers.push(timer);
519
- const handleMessage = createMessageHandler(channel, sessionManager, client);
467
+ const handleMessage = createMessageHandler(channel, sessionManager, client, pendingMap);
520
468
  await channel.start(handleMessage);
521
469
  }
522
470
  await client.app.log({
@@ -528,15 +476,29 @@ var ChatChannelPlugin = async ({ client }) => {
528
476
  });
529
477
  return {
530
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
+ }
531
487
  if (event.type === "session.error") {
488
+ const sessionId = props?.["sessionID"] ?? props?.["id"];
489
+ const errMsg = props?.["message"] ?? "unknown error";
532
490
  await client.app.log({
533
491
  body: {
534
492
  service: "chat-channel",
535
493
  level: "warn",
536
494
  message: "opencode session 出现错误",
537
- extra: event.properties
495
+ extra: props
538
496
  }
539
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
+ }
540
502
  }
541
503
  }
542
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.10",
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",