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.
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +64 -102
- 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,99 +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
|
-
|
|
413
|
-
|
|
414
|
-
|
|
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
|
-
}
|
|
432
|
-
|
|
433
|
-
}
|
|
434
|
-
}
|
|
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:
|
|
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;
|
|
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