opencode-chat-channel 1.2.8 → 1.2.10
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 +177 -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;AAmOjG;;;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,17 @@ class FeishuChannel {
|
|
|
196
218
|
};
|
|
197
219
|
}
|
|
198
220
|
}
|
|
221
|
+
function buildThinkingCard(text) {
|
|
222
|
+
return {
|
|
223
|
+
config: { update_multi: true },
|
|
224
|
+
elements: [
|
|
225
|
+
{
|
|
226
|
+
tag: "markdown",
|
|
227
|
+
content: text
|
|
228
|
+
}
|
|
229
|
+
]
|
|
230
|
+
};
|
|
231
|
+
}
|
|
199
232
|
var feishuChannelFactory = async (client) => {
|
|
200
233
|
const appId = process.env["FEISHU_APP_ID"];
|
|
201
234
|
const appSecret = readAppSecret();
|
|
@@ -287,6 +320,20 @@ function resolveEnabledChannels(client) {
|
|
|
287
320
|
}
|
|
288
321
|
return enabled;
|
|
289
322
|
}
|
|
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;
|
|
290
337
|
function createMessageHandler(channel, sessionManager, client) {
|
|
291
338
|
return async (msg) => {
|
|
292
339
|
const { userId, replyTarget, text } = msg;
|
|
@@ -298,32 +345,141 @@ function createMessageHandler(channel, sessionManager, client) {
|
|
|
298
345
|
extra: { userId, replyTarget }
|
|
299
346
|
}
|
|
300
347
|
});
|
|
301
|
-
let
|
|
348
|
+
let thinkingMsgId = null;
|
|
349
|
+
if (channel.sendThinkingCard) {
|
|
350
|
+
thinkingMsgId = await channel.sendThinkingCard(replyTarget);
|
|
351
|
+
}
|
|
352
|
+
let sessionId;
|
|
302
353
|
try {
|
|
303
|
-
|
|
304
|
-
|
|
354
|
+
sessionId = await sessionManager.getOrCreate(userId);
|
|
355
|
+
} catch (err) {
|
|
356
|
+
const errorMsg = err?.message ?? String(err);
|
|
357
|
+
await client.app.log({
|
|
358
|
+
body: { service: "chat-channel", level: "error", message: `[${channel.name}] 获取 session 失败: ${errorMsg}`, extra: { userId } }
|
|
359
|
+
});
|
|
360
|
+
await channel.send(replyTarget, `⚠️ 出错了:${errorMsg}`);
|
|
361
|
+
return;
|
|
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
|
+
try {
|
|
372
|
+
await client.session.promptAsync({
|
|
305
373
|
path: { id: sessionId },
|
|
306
374
|
body: {
|
|
307
375
|
parts: [{ type: "text", text }]
|
|
308
376
|
}
|
|
309
377
|
});
|
|
310
|
-
responseText = extractResponseText(result.data?.parts ?? []);
|
|
311
378
|
} catch (err) {
|
|
312
379
|
const errorMsg = err?.data?.message ?? err?.message ?? String(err);
|
|
313
380
|
await client.app.log({
|
|
314
|
-
body: {
|
|
315
|
-
service: "chat-channel",
|
|
316
|
-
level: "error",
|
|
317
|
-
message: `[${channel.name}] 处理消息失败: ${errorMsg}`,
|
|
318
|
-
extra: { userId }
|
|
319
|
-
}
|
|
381
|
+
body: { service: "chat-channel", level: "error", message: `[${channel.name}] promptAsync 失败: ${errorMsg}`, extra: { userId } }
|
|
320
382
|
});
|
|
321
|
-
|
|
383
|
+
if (thinkingMsgId && channel.updateThinkingCard) {
|
|
384
|
+
await channel.updateThinkingCard(thinkingMsgId, `⚠️ 出错了:${errorMsg}`);
|
|
385
|
+
} else {
|
|
386
|
+
await channel.send(replyTarget, `⚠️ 出错了:${errorMsg}`);
|
|
387
|
+
}
|
|
322
388
|
return;
|
|
323
389
|
}
|
|
324
|
-
await
|
|
390
|
+
await consumeSessionEvents(client, channel, sessionId, replyTarget, thinkingMsgId, eventStream);
|
|
325
391
|
};
|
|
326
392
|
}
|
|
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
|
+
}
|
|
430
|
+
}
|
|
431
|
+
} catch (err) {
|
|
432
|
+
log("warn", `[${channel.name}] SSE 事件流中断: ${String(err)}`);
|
|
433
|
+
}
|
|
434
|
+
} else {
|
|
435
|
+
await pollForSessionCompletion(client, channel.name, sessionId);
|
|
436
|
+
}
|
|
437
|
+
let responseText = null;
|
|
438
|
+
try {
|
|
439
|
+
const messagesRes = await client.session.messages({ path: { id: sessionId } });
|
|
440
|
+
const messages = messagesRes.data ?? [];
|
|
441
|
+
const lastAssistant = [...messages].reverse().find((m) => m.info?.role === "assistant");
|
|
442
|
+
if (lastAssistant) {
|
|
443
|
+
responseText = extractResponseText(lastAssistant.parts ?? []);
|
|
444
|
+
}
|
|
445
|
+
} catch (err) {
|
|
446
|
+
log("error", `[${channel.name}] 获取最终回复失败: ${String(err)}`);
|
|
447
|
+
}
|
|
448
|
+
if (!responseText) {
|
|
449
|
+
responseText = "(AI 没有返回文字回复)";
|
|
450
|
+
}
|
|
451
|
+
await channel.send(replyTarget, responseText);
|
|
452
|
+
}
|
|
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
|
+
}
|
|
327
483
|
var ChatChannelPlugin = async ({ client }) => {
|
|
328
484
|
const configDir = join(process.env["HOME"] ?? `/Users/${process.env["USER"] ?? "unknown"}`, ".config", "opencode");
|
|
329
485
|
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