opencode-chat-channel 1.2.11 → 1.2.13
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/channels/feishu/index.d.ts.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +247 -158
- package/dist/session-manager.d.ts +5 -0
- package/dist/session-manager.d.ts.map +1 -1
- package/dist/types.d.ts +19 -2
- package/dist/types.d.ts.map +1 -1
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/channels/feishu/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAIH,OAAO,KAAK,EAAe,cAAc,EAAiC,MAAM,gBAAgB,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/channels/feishu/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAIH,OAAO,KAAK,EAAe,cAAc,EAAiC,MAAM,gBAAgB,CAAC;AAoSjG;;;GAGG;AACH,eAAO,MAAM,oBAAoB,EAAE,cAuBlC,CAAC"}
|
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;AAiUlD,eAAO,MAAM,iBAAiB,EAAE,MA8K/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,20 @@
|
|
|
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 (err) {
|
|
15
|
+
process.stderr.write(`[chat-channel] fileLog 写入失败: ${String(err)}
|
|
16
|
+
`);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
6
19
|
var SESSION_TTL_MS = 2 * 60 * 60 * 1000;
|
|
7
20
|
|
|
8
21
|
class SessionManager {
|
|
@@ -49,11 +62,6 @@ class SessionManager {
|
|
|
49
62
|
return setInterval(() => this.cleanup(), intervalMs);
|
|
50
63
|
}
|
|
51
64
|
}
|
|
52
|
-
function extractResponseText(parts) {
|
|
53
|
-
if (!Array.isArray(parts))
|
|
54
|
-
return "";
|
|
55
|
-
return parts.filter((p) => p?.type === "text").map((p) => p?.text ?? "").join("").trim();
|
|
56
|
-
}
|
|
57
65
|
function loadDotEnv(envPath) {
|
|
58
66
|
try {
|
|
59
67
|
const content = readFileSync(envPath, "utf8");
|
|
@@ -150,7 +158,7 @@ class FeishuChannel {
|
|
|
150
158
|
}
|
|
151
159
|
}
|
|
152
160
|
async sendThinkingCard(chatId) {
|
|
153
|
-
const card =
|
|
161
|
+
const card = buildCard("⏳ 正在思考...", "", "thinking");
|
|
154
162
|
try {
|
|
155
163
|
const res = await this.larkClient.im.message.create({
|
|
156
164
|
params: { receive_id_type: "chat_id" },
|
|
@@ -160,26 +168,63 @@ class FeishuChannel {
|
|
|
160
168
|
msg_type: "interactive"
|
|
161
169
|
}
|
|
162
170
|
});
|
|
163
|
-
|
|
171
|
+
const msgId = res.data?.message_id ?? null;
|
|
172
|
+
fileLog("info", `[feishu] sendThinkingCard 成功: msgId=${msgId}`);
|
|
173
|
+
return msgId;
|
|
164
174
|
} catch (err) {
|
|
175
|
+
const errStr = err?.response?.data ? JSON.stringify(err.response.data) : String(err);
|
|
165
176
|
this.client.app.log({
|
|
166
177
|
body: {
|
|
167
178
|
service: "chat-channel",
|
|
168
179
|
level: "warn",
|
|
169
|
-
message: `[feishu]
|
|
180
|
+
message: `[feishu] sendThinkingCard 失败: ${errStr}`
|
|
170
181
|
}
|
|
171
182
|
});
|
|
183
|
+
fileLog("warn", `[feishu] sendThinkingCard 失败: ${errStr}`);
|
|
172
184
|
return null;
|
|
173
185
|
}
|
|
174
186
|
}
|
|
175
187
|
async updateThinkingCard(messageId, statusText) {
|
|
176
|
-
const card =
|
|
188
|
+
const card = buildCard(statusText, "", "thinking");
|
|
177
189
|
try {
|
|
178
190
|
await this.larkClient.im.message.patch({
|
|
179
191
|
data: { content: JSON.stringify(card) },
|
|
180
192
|
path: { message_id: messageId }
|
|
181
193
|
});
|
|
182
|
-
|
|
194
|
+
fileLog("info", `[feishu] updateThinkingCard 成功: msgId=${messageId}`);
|
|
195
|
+
} catch (err) {
|
|
196
|
+
const errStr = err?.response?.data ? JSON.stringify(err.response.data) : String(err);
|
|
197
|
+
this.client.app.log({ body: { service: "chat-channel", level: "warn", message: `[feishu] updateThinkingCard patch 失败: ${errStr}` } });
|
|
198
|
+
fileLog("warn", `[feishu] updateThinkingCard patch 失败: ${errStr}`);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
async patchProgress(messageId, summary, detail, stage) {
|
|
202
|
+
const card = buildCard(summary, detail, stage);
|
|
203
|
+
try {
|
|
204
|
+
await this.larkClient.im.message.patch({
|
|
205
|
+
data: { content: JSON.stringify(card) },
|
|
206
|
+
path: { message_id: messageId }
|
|
207
|
+
});
|
|
208
|
+
fileLog("info", `[feishu] patchProgress 成功: msgId=${messageId} stage=${stage}`);
|
|
209
|
+
} catch (err) {
|
|
210
|
+
const errStr = err?.response?.data ? JSON.stringify(err.response.data) : String(err);
|
|
211
|
+
this.client.app.log({ body: { service: "chat-channel", level: "warn", message: `[feishu] patchProgress 失败: ${errStr}` } });
|
|
212
|
+
fileLog("warn", `[feishu] patchProgress 失败: ${errStr}`);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
async patchDone(messageId, detail) {
|
|
216
|
+
const card = buildCard("✅ 已完成", detail, "done");
|
|
217
|
+
try {
|
|
218
|
+
await this.larkClient.im.message.patch({
|
|
219
|
+
data: { content: JSON.stringify(card) },
|
|
220
|
+
path: { message_id: messageId }
|
|
221
|
+
});
|
|
222
|
+
fileLog("info", `[feishu] patchDone 成功: msgId=${messageId}`);
|
|
223
|
+
} catch (err) {
|
|
224
|
+
const errStr = err?.response?.data ? JSON.stringify(err.response.data) : String(err);
|
|
225
|
+
this.client.app.log({ body: { service: "chat-channel", level: "warn", message: `[feishu] patchDone 失败: ${errStr}` } });
|
|
226
|
+
fileLog("warn", `[feishu] patchDone 失败: ${errStr}`);
|
|
227
|
+
}
|
|
183
228
|
}
|
|
184
229
|
parseEvent(data) {
|
|
185
230
|
const { message, sender } = data ?? {};
|
|
@@ -218,16 +263,25 @@ class FeishuChannel {
|
|
|
218
263
|
};
|
|
219
264
|
}
|
|
220
265
|
}
|
|
221
|
-
function
|
|
222
|
-
|
|
266
|
+
function buildCard(summary, detail, stage) {
|
|
267
|
+
const templates = {
|
|
268
|
+
thinking: "blue",
|
|
269
|
+
reasoning: "wathet",
|
|
270
|
+
replying: "turquoise",
|
|
271
|
+
done: "green"
|
|
272
|
+
};
|
|
273
|
+
const card = {
|
|
274
|
+
schema: "2.0",
|
|
223
275
|
config: { update_multi: true },
|
|
224
|
-
|
|
225
|
-
{
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
276
|
+
header: {
|
|
277
|
+
title: { tag: "plain_text", content: summary },
|
|
278
|
+
template: templates[stage]
|
|
279
|
+
}
|
|
280
|
+
};
|
|
281
|
+
card["body"] = {
|
|
282
|
+
elements: detail ? [{ tag: "markdown", content: detail }] : [{ tag: "markdown", content: " " }]
|
|
230
283
|
};
|
|
284
|
+
return card;
|
|
231
285
|
}
|
|
232
286
|
var feishuChannelFactory = async (client) => {
|
|
233
287
|
const appId = process.env["FEISHU_APP_ID"];
|
|
@@ -320,54 +374,90 @@ function resolveEnabledChannels(client) {
|
|
|
320
374
|
}
|
|
321
375
|
return enabled;
|
|
322
376
|
}
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
377
|
+
var PATCH_THROTTLE_MS = 2000;
|
|
378
|
+
function buildDetailContent(entry) {
|
|
379
|
+
const parts = [];
|
|
380
|
+
if (entry.reasoning) {
|
|
381
|
+
parts.push(`**\uD83D\uDCAD 推理过程**
|
|
382
|
+
${entry.reasoning}`);
|
|
383
|
+
}
|
|
384
|
+
for (const callId of entry.toolPartOrder) {
|
|
385
|
+
const t = entry.toolParts.get(callId);
|
|
386
|
+
if (!t)
|
|
387
|
+
continue;
|
|
388
|
+
const statusIcon = t.status === "completed" ? "✓" : t.status === "running" ? "..." : "⏳";
|
|
389
|
+
const inputStr = t.input !== undefined ? `
|
|
390
|
+
input: \`${JSON.stringify(t.input)}\`` : "";
|
|
391
|
+
const outputStr = t.output ? `
|
|
392
|
+
output: ${t.output.length > 200 ? t.output.slice(0, 200) + "..." : t.output}` : "";
|
|
393
|
+
parts.push(`\uD83D\uDD27 **${t.tool}** (${statusIcon})${inputStr}${outputStr}`);
|
|
394
|
+
}
|
|
395
|
+
const allText = entry.textPartOrder.map((id) => entry.textParts.get(id) ?? "").filter(Boolean).join(`
|
|
396
|
+
|
|
397
|
+
`);
|
|
398
|
+
if (allText) {
|
|
399
|
+
parts.push(`**✍️ 回复内容**
|
|
400
|
+
${allText}`);
|
|
401
|
+
}
|
|
402
|
+
return parts.join(`
|
|
403
|
+
|
|
404
|
+
---
|
|
405
|
+
|
|
406
|
+
`) || "(暂无内容)";
|
|
407
|
+
}
|
|
408
|
+
function doPatch(entry) {
|
|
409
|
+
if (!entry.thinkingMsgId)
|
|
410
|
+
return;
|
|
411
|
+
entry.lastPatchTime = Date.now();
|
|
412
|
+
const hasText = entry.textPartOrder.some((id) => (entry.textParts.get(id) ?? "").length > 0);
|
|
413
|
+
const stage = hasText ? "replying" : "reasoning";
|
|
414
|
+
const summaryText = hasText ? "✍️ 正在输出回复..." : "\uD83E\uDD14 正在推理...";
|
|
415
|
+
const detail = buildDetailContent(entry);
|
|
416
|
+
entry.channel.patchProgress?.(entry.thinkingMsgId, summaryText, detail, stage);
|
|
417
|
+
}
|
|
418
|
+
function scheduleThrottledPatch(entry) {
|
|
419
|
+
const now = Date.now();
|
|
420
|
+
if (now - entry.lastPatchTime >= PATCH_THROTTLE_MS) {
|
|
421
|
+
doPatch(entry);
|
|
422
|
+
} else if (!entry.pendingPatchTimer) {
|
|
423
|
+
const delay = PATCH_THROTTLE_MS - (now - entry.lastPatchTime);
|
|
424
|
+
entry.pendingPatchTimer = setTimeout(() => {
|
|
425
|
+
entry.pendingPatchTimer = null;
|
|
426
|
+
doPatch(entry);
|
|
427
|
+
}, delay);
|
|
428
|
+
}
|
|
334
429
|
}
|
|
335
|
-
|
|
336
|
-
var PATCH_THROTTLE_MS = 3000;
|
|
337
|
-
function createMessageHandler(channel, sessionManager, client) {
|
|
430
|
+
function createMessageHandler(channel, sessionManager, client, pendingMap) {
|
|
338
431
|
return async (msg) => {
|
|
339
432
|
const { userId, replyTarget, text } = msg;
|
|
433
|
+
const incomingMsg = `[${channel.name}] 收到消息: "${text.slice(0, 80)}${text.length > 80 ? "..." : ""}"`;
|
|
434
|
+
fileLog("info", incomingMsg);
|
|
340
435
|
await client.app.log({
|
|
341
436
|
body: {
|
|
342
437
|
service: "chat-channel",
|
|
343
438
|
level: "info",
|
|
344
|
-
message:
|
|
439
|
+
message: incomingMsg,
|
|
345
440
|
extra: { userId, replyTarget }
|
|
346
441
|
}
|
|
347
442
|
});
|
|
348
443
|
let thinkingMsgId = null;
|
|
349
444
|
if (channel.sendThinkingCard) {
|
|
350
445
|
thinkingMsgId = await channel.sendThinkingCard(replyTarget);
|
|
446
|
+
fileLog("info", `[${channel.name}] sendThinkingCard 完成: thinkingMsgId=${thinkingMsgId}`);
|
|
351
447
|
}
|
|
352
448
|
let sessionId;
|
|
353
449
|
try {
|
|
354
450
|
sessionId = await sessionManager.getOrCreate(userId);
|
|
355
451
|
} catch (err) {
|
|
356
452
|
const errorMsg = err?.message ?? String(err);
|
|
453
|
+
const errMsg1 = `[${channel.name}] 获取 session 失败: ${errorMsg}`;
|
|
454
|
+
fileLog("error", errMsg1);
|
|
357
455
|
await client.app.log({
|
|
358
|
-
body: { service: "chat-channel", level: "error", message:
|
|
456
|
+
body: { service: "chat-channel", level: "error", message: errMsg1, extra: { userId } }
|
|
359
457
|
});
|
|
360
458
|
await channel.send(replyTarget, `⚠️ 出错了:${errorMsg}`);
|
|
361
459
|
return;
|
|
362
460
|
}
|
|
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
461
|
try {
|
|
372
462
|
await client.session.promptAsync({
|
|
373
463
|
path: { id: sessionId },
|
|
@@ -375,8 +465,10 @@ function createMessageHandler(channel, sessionManager, client) {
|
|
|
375
465
|
parts: [{ type: "text", text }]
|
|
376
466
|
}
|
|
377
467
|
});
|
|
468
|
+
fileLog("info", `[${channel.name}] promptAsync 成功: sessionId=${sessionId}`);
|
|
378
469
|
} catch (err) {
|
|
379
470
|
const errorMsg = err?.data?.message ?? err?.message ?? String(err);
|
|
471
|
+
fileLog("error", `[${channel.name}] promptAsync 失败: ${errorMsg}`);
|
|
380
472
|
await client.app.log({
|
|
381
473
|
body: { service: "chat-channel", level: "error", message: `[${channel.name}] promptAsync 失败: ${errorMsg}`, extra: { userId } }
|
|
382
474
|
});
|
|
@@ -387,122 +479,47 @@ function createMessageHandler(channel, sessionManager, client) {
|
|
|
387
479
|
}
|
|
388
480
|
return;
|
|
389
481
|
}
|
|
390
|
-
await
|
|
482
|
+
await waitForSessionAndReply(client, channel, sessionId, replyTarget, thinkingMsgId, pendingMap);
|
|
391
483
|
};
|
|
392
484
|
}
|
|
393
|
-
async function
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
const
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
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 {
|
|
485
|
+
async function waitForSessionAndReply(client, channel, sessionId, replyTarget, thinkingMsgId, pendingMap) {
|
|
486
|
+
const log = (level, message) => {
|
|
487
|
+
fileLog(level, message);
|
|
488
|
+
client.app.log({ body: { service: "chat-channel", level, message } });
|
|
489
|
+
};
|
|
490
|
+
const SESSION_TIMEOUT_MS = 5 * 60 * 1000;
|
|
491
|
+
await new Promise((resolve, reject) => {
|
|
492
|
+
const timeoutId = setTimeout(() => {
|
|
493
|
+
pendingMap.delete(sessionId);
|
|
494
|
+
log("warn", `[${channel.name}] session 等待超时(5分钟),强制继续读取回复`);
|
|
495
|
+
resolve();
|
|
496
|
+
}, SESSION_TIMEOUT_MS);
|
|
497
|
+
pendingMap.set(sessionId, {
|
|
498
|
+
resolve: () => {
|
|
455
499
|
clearTimeout(timeoutId);
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
}
|
|
477
|
-
|
|
478
|
-
|
|
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;
|
|
500
|
+
pendingMap.delete(sessionId);
|
|
501
|
+
resolve();
|
|
502
|
+
},
|
|
503
|
+
reject: (err) => {
|
|
504
|
+
clearTimeout(timeoutId);
|
|
505
|
+
pendingMap.delete(sessionId);
|
|
506
|
+
reject(err);
|
|
507
|
+
},
|
|
508
|
+
thinkingMsgId,
|
|
509
|
+
channel,
|
|
510
|
+
replyTarget,
|
|
511
|
+
lastPatchTime: 0,
|
|
512
|
+
pendingPatchTimer: null,
|
|
513
|
+
reasoning: "",
|
|
514
|
+
textParts: new Map,
|
|
515
|
+
textPartOrder: [],
|
|
516
|
+
toolParts: new Map,
|
|
517
|
+
toolPartOrder: []
|
|
518
|
+
});
|
|
519
|
+
}).catch((err) => {
|
|
520
|
+
log("error", `[${channel.name}] session 出错: ${err.message}`);
|
|
521
|
+
});
|
|
522
|
+
log("info", `[${channel.name}] session 完成,卡片已更新,跳过文本回复`);
|
|
506
523
|
}
|
|
507
524
|
var ChatChannelPlugin = async ({ client }) => {
|
|
508
525
|
const configDir = join(process.env["HOME"] ?? `/Users/${process.env["USER"] ?? "unknown"}`, ".config", "opencode");
|
|
@@ -535,12 +552,13 @@ var ChatChannelPlugin = async ({ client }) => {
|
|
|
535
552
|
});
|
|
536
553
|
return {};
|
|
537
554
|
}
|
|
555
|
+
const pendingMap = new Map;
|
|
538
556
|
const cleanupTimers = [];
|
|
539
557
|
for (const channel of channels) {
|
|
540
558
|
const sessionManager = new SessionManager(client, channel.name, (userId) => `${channel.name} 对话 · ${userId}`);
|
|
541
559
|
const timer = sessionManager.startAutoCleanup();
|
|
542
560
|
cleanupTimers.push(timer);
|
|
543
|
-
const handleMessage = createMessageHandler(channel, sessionManager, client);
|
|
561
|
+
const handleMessage = createMessageHandler(channel, sessionManager, client, pendingMap);
|
|
544
562
|
await channel.start(handleMessage);
|
|
545
563
|
}
|
|
546
564
|
await client.app.log({
|
|
@@ -552,15 +570,86 @@ var ChatChannelPlugin = async ({ client }) => {
|
|
|
552
570
|
});
|
|
553
571
|
return {
|
|
554
572
|
event: async ({ event }) => {
|
|
573
|
+
const props = event.properties;
|
|
574
|
+
if (event.type === "message.part.updated") {
|
|
575
|
+
const part = props?.["part"];
|
|
576
|
+
fileLog("info", `[diag] part.updated raw: ${JSON.stringify(part).slice(0, 300)}`);
|
|
577
|
+
const sessionId = part?.["sessionID"] ?? part?.["session_id"];
|
|
578
|
+
if (sessionId) {
|
|
579
|
+
const entry = pendingMap.get(sessionId);
|
|
580
|
+
if (entry) {
|
|
581
|
+
const partType = part?.["type"];
|
|
582
|
+
const partId = part?.["id"];
|
|
583
|
+
if (partType === "reasoning") {
|
|
584
|
+
entry.reasoning = part?.["text"] ?? "";
|
|
585
|
+
scheduleThrottledPatch(entry);
|
|
586
|
+
} else if (partType === "text") {
|
|
587
|
+
if (partId) {
|
|
588
|
+
if (!entry.textParts.has(partId)) {
|
|
589
|
+
entry.textPartOrder.push(partId);
|
|
590
|
+
}
|
|
591
|
+
entry.textParts.set(partId, part?.["text"] ?? "");
|
|
592
|
+
}
|
|
593
|
+
scheduleThrottledPatch(entry);
|
|
594
|
+
} else if (partType === "tool") {
|
|
595
|
+
const callId = part?.["callID"];
|
|
596
|
+
const toolName = part?.["tool"];
|
|
597
|
+
const toolState = part?.["state"];
|
|
598
|
+
const status = toolState?.["status"];
|
|
599
|
+
const toolInput = toolState?.["input"];
|
|
600
|
+
const toolOutput = toolState?.["output"];
|
|
601
|
+
if (callId && toolName && status) {
|
|
602
|
+
if (!entry.toolParts.has(callId)) {
|
|
603
|
+
entry.toolPartOrder.push(callId);
|
|
604
|
+
}
|
|
605
|
+
entry.toolParts.set(callId, { tool: toolName, status, input: toolInput, output: toolOutput });
|
|
606
|
+
scheduleThrottledPatch(entry);
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
if (event.type === "session.idle") {
|
|
613
|
+
const sessionId = props?.["sessionID"] ?? props?.["id"];
|
|
614
|
+
if (sessionId) {
|
|
615
|
+
fileLog("info", `[diag] event hook: session.idle sessionId=${sessionId}`);
|
|
616
|
+
const entry = pendingMap.get(sessionId);
|
|
617
|
+
if (entry) {
|
|
618
|
+
if (entry.pendingPatchTimer) {
|
|
619
|
+
clearTimeout(entry.pendingPatchTimer);
|
|
620
|
+
entry.pendingPatchTimer = null;
|
|
621
|
+
}
|
|
622
|
+
fileLog("info", `[diag] session.idle: thinkingMsgId=${entry.thinkingMsgId}, hasPatchDone=${!!entry.channel.patchDone}, reasoning.len=${entry.reasoning.length}, textParts.count=${entry.textPartOrder.length}, toolParts.count=${entry.toolPartOrder.length}`);
|
|
623
|
+
if (entry.thinkingMsgId && entry.channel.patchDone) {
|
|
624
|
+
const detail = buildDetailContent(entry);
|
|
625
|
+
await entry.channel.patchDone(entry.thinkingMsgId, detail);
|
|
626
|
+
}
|
|
627
|
+
entry.resolve();
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
}
|
|
555
631
|
if (event.type === "session.error") {
|
|
632
|
+
const sessionId = props?.["sessionID"] ?? props?.["id"];
|
|
633
|
+
const errMsg = props?.["message"] ?? "unknown error";
|
|
556
634
|
await client.app.log({
|
|
557
635
|
body: {
|
|
558
636
|
service: "chat-channel",
|
|
559
637
|
level: "warn",
|
|
560
638
|
message: "opencode session 出现错误",
|
|
561
|
-
extra:
|
|
639
|
+
extra: props
|
|
562
640
|
}
|
|
563
641
|
});
|
|
642
|
+
if (sessionId) {
|
|
643
|
+
fileLog("info", `[diag] event hook: session.error sessionId=${sessionId} err=${errMsg}`);
|
|
644
|
+
const entry = pendingMap.get(sessionId);
|
|
645
|
+
if (entry) {
|
|
646
|
+
if (entry.pendingPatchTimer) {
|
|
647
|
+
clearTimeout(entry.pendingPatchTimer);
|
|
648
|
+
entry.pendingPatchTimer = null;
|
|
649
|
+
}
|
|
650
|
+
entry.reject(new Error(errMsg));
|
|
651
|
+
}
|
|
652
|
+
}
|
|
564
653
|
}
|
|
565
654
|
}
|
|
566
655
|
};
|
|
@@ -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/dist/types.d.ts
CHANGED
|
@@ -25,7 +25,9 @@ export interface IncomingMessage {
|
|
|
25
25
|
* - start(): 启动监听,收到消息时调用 onMessage 回调
|
|
26
26
|
* - send(): 向指定 target 发送文本回复
|
|
27
27
|
* - sendThinkingCard(): 发送占位卡片,返回可更新的 ID(可选)
|
|
28
|
-
* - updateThinkingCard():
|
|
28
|
+
* - updateThinkingCard(): 更新占位卡片内容(可选,旧接口向下兼容)
|
|
29
|
+
* - patchProgress(): 更新卡片为进度状态,外部摘要+内部详情折叠面板(可选)
|
|
30
|
+
* - patchDone(): 更新卡片为完成状态(可选)
|
|
29
31
|
* - stop(): 优雅关闭(可选)
|
|
30
32
|
*/
|
|
31
33
|
export interface ChatChannel {
|
|
@@ -48,11 +50,26 @@ export interface ChatChannel {
|
|
|
48
50
|
*/
|
|
49
51
|
sendThinkingCard?(replyTarget: string): Promise<string | null>;
|
|
50
52
|
/**
|
|
51
|
-
*
|
|
53
|
+
* 更新占位消息的内容(旧接口,向下兼容)。
|
|
52
54
|
* @param placeholderId sendThinkingCard 返回的 ID
|
|
53
55
|
* @param statusText 新状态文本
|
|
54
56
|
*/
|
|
55
57
|
updateThinkingCard?(placeholderId: string, statusText: string): Promise<void>;
|
|
58
|
+
/**
|
|
59
|
+
* 更新思考卡片为进度状态(节流调用)。
|
|
60
|
+
* 卡片外部显示 summary 摘要,折叠面板内部展示 detail 详细内容。
|
|
61
|
+
* @param placeholderId sendThinkingCard 返回的 ID
|
|
62
|
+
* @param summary 外部摘要文字(如"🤔 正在推理...")
|
|
63
|
+
* @param detail 折叠面板内部展开后的详细内容(reasoning / text 累积)
|
|
64
|
+
* @param stage 当前阶段,用于选择卡片图标和样式
|
|
65
|
+
*/
|
|
66
|
+
patchProgress?(placeholderId: string, summary: string, detail: string, stage: "reasoning" | "replying"): Promise<void>;
|
|
67
|
+
/**
|
|
68
|
+
* 更新思考卡片为完成状态。
|
|
69
|
+
* @param placeholderId sendThinkingCard 返回的 ID
|
|
70
|
+
* @param detail 展开后显示的完整内容(reasoning + text 摘要)
|
|
71
|
+
*/
|
|
72
|
+
patchDone?(placeholderId: string, detail: string): Promise<void>;
|
|
56
73
|
/** 优雅停止渠道(可选)。 */
|
|
57
74
|
stop?(): Promise<void>;
|
|
58
75
|
}
|
package/dist/types.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,qBAAqB,CAAC;AAElD,8CAA8C;AAC9C,MAAM,MAAM,YAAY,GAAG,UAAU,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC;AAI3D,kBAAkB;AAClB,MAAM,WAAW,eAAe;IAC9B,oBAAoB;IACpB,SAAS,EAAE,MAAM,CAAC;IAClB,qCAAqC;IACrC,MAAM,EAAE,MAAM,CAAC;IACf,sCAAsC;IACtC,WAAW,EAAE,MAAM,CAAC;IACpB,aAAa;IACb,IAAI,EAAE,MAAM,CAAC;CACd;AAID
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,qBAAqB,CAAC;AAElD,8CAA8C;AAC9C,MAAM,MAAM,YAAY,GAAG,UAAU,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC;AAI3D,kBAAkB;AAClB,MAAM,WAAW,eAAe;IAC9B,oBAAoB;IACpB,SAAS,EAAE,MAAM,CAAC;IAClB,qCAAqC;IACrC,MAAM,EAAE,MAAM,CAAC;IACf,sCAAsC;IACtC,WAAW,EAAE,MAAM,CAAC;IACpB,aAAa;IACb,IAAI,EAAE,MAAM,CAAC;CACd;AAID;;;;;;;;;;;;GAYG;AACH,MAAM,WAAW,WAAW;IAC1B,gCAAgC;IAChC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IAEtB;;;OAGG;IACH,KAAK,CAAC,SAAS,EAAE,CAAC,GAAG,EAAE,eAAe,KAAK,OAAO,CAAC,IAAI,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAEzE;;;OAGG;IACH,IAAI,CAAC,WAAW,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAEvD;;;;OAIG;IACH,gBAAgB,CAAC,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;IAE/D;;;;OAIG;IACH,kBAAkB,CAAC,CAAC,aAAa,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAE9E;;;;;;;OAOG;IACH,aAAa,CAAC,CACZ,aAAa,EAAE,MAAM,EACrB,OAAO,EAAE,MAAM,EACf,MAAM,EAAE,MAAM,EACd,KAAK,EAAE,WAAW,GAAG,UAAU,GAC9B,OAAO,CAAC,IAAI,CAAC,CAAC;IAEjB;;;;OAIG;IACH,SAAS,CAAC,CAAC,aAAa,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAEjE,kBAAkB;IAClB,IAAI,CAAC,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACxB;AAID;;;;;GAKG;AACH,MAAM,MAAM,cAAc,GAAG,CAC3B,MAAM,EAAE,YAAY,KACjB,OAAO,CAAC,WAAW,GAAG,IAAI,CAAC,CAAC;AAIjC;;;GAGG;AACH,MAAM,MAAM,WAAW,GAAG,QAAQ,GAAG,OAAO,CAAC"}
|
package/package.json
CHANGED