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.
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +63 -125
- package/dist/session-manager.d.ts +5 -0
- package/dist/session-manager.d.ts.map +1 -1
- package/package.json +1 -1
package/dist/index.d.ts.map
CHANGED
|
@@ -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;
|
|
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
|
|
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:
|
|
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:
|
|
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
|
|
382
|
+
await waitForSessionAndReply(client, channel, sessionId, replyTarget, thinkingMsgId, pendingMap);
|
|
391
383
|
};
|
|
392
384
|
}
|
|
393
|
-
async function
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
const
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
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
|
-
}
|
|
459
|
-
|
|
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:
|
|
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;
|
|
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