opencode-chat-channel 1.2.8 → 1.2.9
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 +179 -21
- package/dist/types.d.ts +14 -0
- 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;AAoOjG;;;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;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"}
|
package/dist/index.js
CHANGED
|
@@ -149,15 +149,37 @@ class FeishuChannel {
|
|
|
149
149
|
});
|
|
150
150
|
}
|
|
151
151
|
}
|
|
152
|
-
async
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
152
|
+
async sendThinkingCard(chatId) {
|
|
153
|
+
const card = buildThinkingCard("⏳ 正在思考...");
|
|
154
|
+
try {
|
|
155
|
+
const res = await this.larkClient.im.message.create({
|
|
156
|
+
params: { receive_id_type: "chat_id" },
|
|
157
|
+
data: {
|
|
158
|
+
receive_id: chatId,
|
|
159
|
+
content: JSON.stringify(card),
|
|
160
|
+
msg_type: "interactive"
|
|
161
|
+
}
|
|
162
|
+
});
|
|
163
|
+
return res.data?.message_id ?? null;
|
|
164
|
+
} catch (err) {
|
|
165
|
+
this.client.app.log({
|
|
166
|
+
body: {
|
|
167
|
+
service: "chat-channel",
|
|
168
|
+
level: "warn",
|
|
169
|
+
message: `[feishu] 发送思考卡片失败: ${String(err)}`
|
|
170
|
+
}
|
|
171
|
+
});
|
|
172
|
+
return null;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
async updateThinkingCard(messageId, statusText) {
|
|
176
|
+
const card = buildThinkingCard(statusText);
|
|
177
|
+
try {
|
|
178
|
+
await this.larkClient.im.message.patch({
|
|
179
|
+
data: { content: JSON.stringify(card) },
|
|
180
|
+
path: { message_id: messageId }
|
|
181
|
+
});
|
|
182
|
+
} catch {}
|
|
161
183
|
}
|
|
162
184
|
parseEvent(data) {
|
|
163
185
|
const { message, sender } = data ?? {};
|
|
@@ -196,6 +218,19 @@ class FeishuChannel {
|
|
|
196
218
|
};
|
|
197
219
|
}
|
|
198
220
|
}
|
|
221
|
+
function buildThinkingCard(text) {
|
|
222
|
+
return {
|
|
223
|
+
config: { update_multi: true },
|
|
224
|
+
body: {
|
|
225
|
+
elements: [
|
|
226
|
+
{
|
|
227
|
+
tag: "markdown",
|
|
228
|
+
content: text
|
|
229
|
+
}
|
|
230
|
+
]
|
|
231
|
+
}
|
|
232
|
+
};
|
|
233
|
+
}
|
|
199
234
|
var feishuChannelFactory = async (client) => {
|
|
200
235
|
const appId = process.env["FEISHU_APP_ID"];
|
|
201
236
|
const appSecret = readAppSecret();
|
|
@@ -287,6 +322,20 @@ function resolveEnabledChannels(client) {
|
|
|
287
322
|
}
|
|
288
323
|
return enabled;
|
|
289
324
|
}
|
|
325
|
+
function stripMarkdownTables(text) {
|
|
326
|
+
return text.split(`
|
|
327
|
+
`).map((line) => {
|
|
328
|
+
const trimmed = line.trim();
|
|
329
|
+
if (/^\|[-:\s|]+\|$/.test(trimmed))
|
|
330
|
+
return "";
|
|
331
|
+
if (trimmed.startsWith("|"))
|
|
332
|
+
return "[表格内容]";
|
|
333
|
+
return line;
|
|
334
|
+
}).filter((line, i, arr) => !(line === "[表格内容]" && arr[i - 1] === "[表格内容]")).join(`
|
|
335
|
+
`).trim();
|
|
336
|
+
}
|
|
337
|
+
var REASONING_PREVIEW_LEN = 200;
|
|
338
|
+
var PATCH_THROTTLE_MS = 3000;
|
|
290
339
|
function createMessageHandler(channel, sessionManager, client) {
|
|
291
340
|
return async (msg) => {
|
|
292
341
|
const { userId, replyTarget, text } = msg;
|
|
@@ -298,32 +347,141 @@ function createMessageHandler(channel, sessionManager, client) {
|
|
|
298
347
|
extra: { userId, replyTarget }
|
|
299
348
|
}
|
|
300
349
|
});
|
|
301
|
-
let
|
|
350
|
+
let thinkingMsgId = null;
|
|
351
|
+
if (channel.sendThinkingCard) {
|
|
352
|
+
thinkingMsgId = await channel.sendThinkingCard(replyTarget);
|
|
353
|
+
}
|
|
354
|
+
let sessionId;
|
|
302
355
|
try {
|
|
303
|
-
|
|
304
|
-
|
|
356
|
+
sessionId = await sessionManager.getOrCreate(userId);
|
|
357
|
+
} catch (err) {
|
|
358
|
+
const errorMsg = err?.message ?? String(err);
|
|
359
|
+
await client.app.log({
|
|
360
|
+
body: { service: "chat-channel", level: "error", message: `[${channel.name}] 获取 session 失败: ${errorMsg}`, extra: { userId } }
|
|
361
|
+
});
|
|
362
|
+
await channel.send(replyTarget, `⚠️ 出错了:${errorMsg}`);
|
|
363
|
+
return;
|
|
364
|
+
}
|
|
365
|
+
let eventStream = null;
|
|
366
|
+
try {
|
|
367
|
+
eventStream = await client.event.subscribe();
|
|
368
|
+
} catch (err) {
|
|
369
|
+
await client.app.log({
|
|
370
|
+
body: { service: "chat-channel", level: "warn", message: `[${channel.name}] SSE 订阅失败,降级为轮询: ${String(err)}` }
|
|
371
|
+
});
|
|
372
|
+
}
|
|
373
|
+
try {
|
|
374
|
+
await client.session.promptAsync({
|
|
305
375
|
path: { id: sessionId },
|
|
306
376
|
body: {
|
|
307
377
|
parts: [{ type: "text", text }]
|
|
308
378
|
}
|
|
309
379
|
});
|
|
310
|
-
responseText = extractResponseText(result.data?.parts ?? []);
|
|
311
380
|
} catch (err) {
|
|
312
381
|
const errorMsg = err?.data?.message ?? err?.message ?? String(err);
|
|
313
382
|
await client.app.log({
|
|
314
|
-
body: {
|
|
315
|
-
service: "chat-channel",
|
|
316
|
-
level: "error",
|
|
317
|
-
message: `[${channel.name}] 处理消息失败: ${errorMsg}`,
|
|
318
|
-
extra: { userId }
|
|
319
|
-
}
|
|
383
|
+
body: { service: "chat-channel", level: "error", message: `[${channel.name}] promptAsync 失败: ${errorMsg}`, extra: { userId } }
|
|
320
384
|
});
|
|
321
|
-
|
|
385
|
+
if (thinkingMsgId && channel.updateThinkingCard) {
|
|
386
|
+
await channel.updateThinkingCard(thinkingMsgId, `⚠️ 出错了:${errorMsg}`);
|
|
387
|
+
} else {
|
|
388
|
+
await channel.send(replyTarget, `⚠️ 出错了:${errorMsg}`);
|
|
389
|
+
}
|
|
322
390
|
return;
|
|
323
391
|
}
|
|
324
|
-
await
|
|
392
|
+
await consumeSessionEvents(client, channel, sessionId, replyTarget, thinkingMsgId, eventStream);
|
|
325
393
|
};
|
|
326
394
|
}
|
|
395
|
+
async function consumeSessionEvents(client, channel, sessionId, replyTarget, thinkingMsgId, eventStream) {
|
|
396
|
+
let lastPatchAt = 0;
|
|
397
|
+
let reasoningAccum = "";
|
|
398
|
+
const log = (level, message) => void client.app.log({ body: { service: "chat-channel", level, message } });
|
|
399
|
+
async function throttledPatch(text, force = false) {
|
|
400
|
+
if (!thinkingMsgId || !channel.updateThinkingCard)
|
|
401
|
+
return;
|
|
402
|
+
const now = Date.now();
|
|
403
|
+
if (!force && now - lastPatchAt < PATCH_THROTTLE_MS)
|
|
404
|
+
return;
|
|
405
|
+
lastPatchAt = now;
|
|
406
|
+
await channel.updateThinkingCard(thinkingMsgId, text);
|
|
407
|
+
}
|
|
408
|
+
if (eventStream) {
|
|
409
|
+
try {
|
|
410
|
+
for await (const event of eventStream.stream) {
|
|
411
|
+
if (!isSessionEvent(event, sessionId))
|
|
412
|
+
continue;
|
|
413
|
+
if (event.type === "message.part.updated") {
|
|
414
|
+
const part = event.properties?.part;
|
|
415
|
+
if (!part)
|
|
416
|
+
continue;
|
|
417
|
+
if (part.type === "reasoning" && part.text) {
|
|
418
|
+
reasoningAccum = part.text;
|
|
419
|
+
const preview = stripMarkdownTables(reasoningAccum.slice(0, REASONING_PREVIEW_LEN));
|
|
420
|
+
const suffix = reasoningAccum.length > REASONING_PREVIEW_LEN ? "..." : "";
|
|
421
|
+
await throttledPatch(`\uD83D\uDCAD **正在思考...**
|
|
422
|
+
|
|
423
|
+
${preview}${suffix}`);
|
|
424
|
+
} else if (part.type === "tool" && part.state?.status === "running") {
|
|
425
|
+
const toolLabel = (part.tool ?? "") || "工具";
|
|
426
|
+
await throttledPatch(`\uD83D\uDD27 **正在使用工具:${toolLabel}**`);
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
if (event.type === "session.idle" || event.type === "session.error") {
|
|
430
|
+
break;
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
} catch (err) {
|
|
434
|
+
log("warn", `[${channel.name}] SSE 事件流中断: ${String(err)}`);
|
|
435
|
+
}
|
|
436
|
+
} else {
|
|
437
|
+
await pollForSessionCompletion(client, channel.name, sessionId);
|
|
438
|
+
}
|
|
439
|
+
let responseText = null;
|
|
440
|
+
try {
|
|
441
|
+
const messagesRes = await client.session.messages({ path: { id: sessionId } });
|
|
442
|
+
const messages = messagesRes.data ?? [];
|
|
443
|
+
const lastAssistant = [...messages].reverse().find((m) => m.info?.role === "assistant");
|
|
444
|
+
if (lastAssistant) {
|
|
445
|
+
responseText = extractResponseText(lastAssistant.parts ?? []);
|
|
446
|
+
}
|
|
447
|
+
} catch (err) {
|
|
448
|
+
log("error", `[${channel.name}] 获取最终回复失败: ${String(err)}`);
|
|
449
|
+
}
|
|
450
|
+
if (!responseText) {
|
|
451
|
+
responseText = "(AI 没有返回文字回复)";
|
|
452
|
+
}
|
|
453
|
+
await channel.send(replyTarget, responseText);
|
|
454
|
+
}
|
|
455
|
+
async function pollForSessionCompletion(client, channelName, sessionId) {
|
|
456
|
+
const POLL_INTERVAL_MS = 1000;
|
|
457
|
+
const MAX_WAIT_MS = 5 * 60 * 1000;
|
|
458
|
+
const started = Date.now();
|
|
459
|
+
while (Date.now() - started < MAX_WAIT_MS) {
|
|
460
|
+
await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS));
|
|
461
|
+
try {
|
|
462
|
+
const res = await client.session.status();
|
|
463
|
+
const allStatuses = res.data ?? {};
|
|
464
|
+
const sessionStatus = allStatuses[sessionId];
|
|
465
|
+
if (!sessionStatus || sessionStatus.type === "idle")
|
|
466
|
+
break;
|
|
467
|
+
} catch {
|
|
468
|
+
client.app.log({
|
|
469
|
+
body: { service: "chat-channel", level: "warn", message: `[${channelName}] 轮询 session 状态失败,继续等待...` }
|
|
470
|
+
});
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
function isSessionEvent(event, sessionId) {
|
|
475
|
+
if (!event || !event.type)
|
|
476
|
+
return false;
|
|
477
|
+
const props = event.properties;
|
|
478
|
+
if (!props)
|
|
479
|
+
return false;
|
|
480
|
+
if (event.type === "message.part.updated") {
|
|
481
|
+
return props.part?.sessionID === sessionId;
|
|
482
|
+
}
|
|
483
|
+
return props.sessionID === sessionId || props.id === sessionId;
|
|
484
|
+
}
|
|
327
485
|
var ChatChannelPlugin = async ({ client }) => {
|
|
328
486
|
const configDir = join(process.env["HOME"] ?? `/Users/${process.env["USER"] ?? "unknown"}`, ".config", "opencode");
|
|
329
487
|
loadDotEnv(join(configDir, ".env"));
|
package/dist/types.d.ts
CHANGED
|
@@ -24,6 +24,8 @@ export interface IncomingMessage {
|
|
|
24
24
|
* - name: 渠道标识符(用于日志、配置 key)
|
|
25
25
|
* - start(): 启动监听,收到消息时调用 onMessage 回调
|
|
26
26
|
* - send(): 向指定 target 发送文本回复
|
|
27
|
+
* - sendThinkingCard(): 发送占位卡片,返回可更新的 ID(可选)
|
|
28
|
+
* - updateThinkingCard(): 更新占位卡片内容(可选)
|
|
27
29
|
* - stop(): 优雅关闭(可选)
|
|
28
30
|
*/
|
|
29
31
|
export interface ChatChannel {
|
|
@@ -39,6 +41,18 @@ export interface ChatChannel {
|
|
|
39
41
|
* replyTarget 为 IncomingMessage.replyTarget。
|
|
40
42
|
*/
|
|
41
43
|
send(replyTarget: string, text: string): Promise<void>;
|
|
44
|
+
/**
|
|
45
|
+
* 发送"正在思考"占位卡片,返回可用于后续更新的占位消息 ID。
|
|
46
|
+
* 返回 null 表示该渠道不支持更新式占位(降级为无占位)。
|
|
47
|
+
* 可选——未实现的渠道会跳过思考展示。
|
|
48
|
+
*/
|
|
49
|
+
sendThinkingCard?(replyTarget: string): Promise<string | null>;
|
|
50
|
+
/**
|
|
51
|
+
* 更新占位消息的内容。
|
|
52
|
+
* @param placeholderId sendThinkingCard 返回的 ID
|
|
53
|
+
* @param statusText 新状态文本
|
|
54
|
+
*/
|
|
55
|
+
updateThinkingCard?(placeholderId: string, statusText: string): Promise<void>;
|
|
42
56
|
/** 优雅停止渠道(可选)。 */
|
|
43
57
|
stop?(): Promise<void>;
|
|
44
58
|
}
|
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;;;;;;;;;;GAUG;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,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