opencode-feishu 0.4.2 → 0.5.1

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/README.md CHANGED
@@ -72,6 +72,10 @@ opencode
72
72
  | `stablePolls` | number | 否 | `3` | 连续几次轮询内容不变视为回复完成 |
73
73
  | `dedupTtl` | number | 否 | `600000` | 消息去重缓存过期时间(毫秒) |
74
74
  | `directory` | string | 否 | `""` | 默认工作目录,支持 `~` 和 `${ENV_VAR}` 展开 |
75
+ | `autoPrompt.enabled` | boolean | 否 | `false` | 启用自动提示(响应完成后自动发送"继续") |
76
+ | `autoPrompt.intervalSeconds` | number | 否 | `30` | 响应完成后等待秒数 |
77
+ | `autoPrompt.maxIterations` | number | 否 | `10` | 单轮对话最大自动提示次数 |
78
+ | `autoPrompt.message` | string | 否 | `"请同步当前进度,如需帮助请说明"` | 自动发送的提示内容 |
75
79
 
76
80
  ## 特性
77
81
 
@@ -81,6 +85,7 @@ opencode
81
85
  - **入群自动摄入历史消息**
82
86
  - **代理支持** — `HTTPS_PROXY` / `HTTP_PROXY` / `ALL_PROXY`
83
87
  - **消息去重** — 可配置 TTL
88
+ - **自动提示** — 响应完成后自动发送"继续",推动 OpenCode 持续工作;用户发新消息自动中断
84
89
 
85
90
  ## 群聊行为
86
91
 
package/dist/index.js CHANGED
@@ -99267,12 +99267,19 @@ async function getOrCreateSession(client, sessionKey, directory) {
99267
99267
  }
99268
99268
 
99269
99269
  // src/handler/chat.ts
99270
+ var activeAutoPrompts = /* @__PURE__ */ new Map();
99270
99271
  async function handleChat(ctx, deps) {
99271
99272
  const { content, chatId, chatType, senderId, shouldReply, messageType, rawContent, messageId } = ctx;
99272
99273
  if (!content.trim() && messageType === "text") return;
99273
99274
  const { config, client, feishuClient, log, directory } = deps;
99274
99275
  const query = directory ? { directory } : void 0;
99275
99276
  const sessionKey = buildSessionKey(chatType, chatType === "p2p" ? senderId : chatId);
99277
+ const existing = activeAutoPrompts.get(sessionKey);
99278
+ if (existing) {
99279
+ existing.abort();
99280
+ activeAutoPrompts.delete(sessionKey);
99281
+ log("info", "\u7528\u6237\u4ECB\u5165\uFF0C\u81EA\u52A8\u63D0\u793A\u5DF2\u4E2D\u65AD", { sessionKey });
99282
+ }
99276
99283
  const session = await getOrCreateSession(client, sessionKey, directory);
99277
99284
  const parts = await buildPromptParts(feishuClient, messageId, messageType, rawContent, content, chatType, senderId, log);
99278
99285
  if (!parts.length) return;
@@ -99323,24 +99330,41 @@ async function handleChat(ctx, deps) {
99323
99330
  parts
99324
99331
  }
99325
99332
  });
99326
- const start = Date.now();
99327
- let lastText = "";
99328
- let sameCount = 0;
99329
- while (Date.now() - start < timeout) {
99330
- await new Promise((r) => setTimeout(r, pollInterval));
99331
- const { data: messages } = await client.session.messages({ path: { id: session.id }, query });
99332
- const text2 = extractLastAssistantText(messages ?? []);
99333
- if (text2 && text2 !== lastText) {
99334
- lastText = text2;
99335
- sameCount = 0;
99336
- } else if (text2 && text2.length > 0) {
99337
- sameCount++;
99338
- if (sameCount >= stablePolls) break;
99333
+ const finalText = await pollForResponse(client, session.id, { timeout, pollInterval, stablePolls, query });
99334
+ await replyOrUpdate(feishuClient, chatId, placeholderId, finalText || "\u26A0\uFE0F \u54CD\u5E94\u8D85\u65F6");
99335
+ const { autoPrompt } = config;
99336
+ if (autoPrompt.enabled && shouldReply) {
99337
+ const ac = new AbortController();
99338
+ activeAutoPrompts.set(sessionKey, ac);
99339
+ log("info", "\u542F\u52A8\u81EA\u52A8\u63D0\u793A\u5FAA\u73AF", { sessionKey, maxIterations: autoPrompt.maxIterations });
99340
+ try {
99341
+ for (let i = 0; i < autoPrompt.maxIterations; i++) {
99342
+ await abortableSleep(autoPrompt.intervalSeconds * 1e3, ac.signal);
99343
+ log("info", "\u53D1\u9001\u81EA\u52A8\u63D0\u793A", { sessionKey, iteration: i + 1 });
99344
+ await client.session.prompt({
99345
+ path: { id: session.id },
99346
+ query,
99347
+ body: { parts: [{ type: "text", text: autoPrompt.message }] }
99348
+ });
99349
+ const text2 = await pollForResponse(client, session.id, { timeout, pollInterval, stablePolls, query, signal: ac.signal });
99350
+ if (text2) {
99351
+ await sendTextMessage(feishuClient, chatId, text2);
99352
+ }
99353
+ }
99354
+ log("info", "\u81EA\u52A8\u63D0\u793A\u5FAA\u73AF\u7ED3\u675F\uFF08\u8FBE\u5230\u6700\u5927\u6B21\u6570\uFF09", { sessionKey });
99355
+ } catch (err) {
99356
+ if (err.name === "AbortError") {
99357
+ log("info", "\u81EA\u52A8\u63D0\u793A\u5FAA\u73AF\u88AB\u4E2D\u65AD", { sessionKey });
99358
+ } else {
99359
+ log("error", "\u81EA\u52A8\u63D0\u793A\u5FAA\u73AF\u5F02\u5E38", {
99360
+ sessionKey,
99361
+ error: err instanceof Error ? err.message : String(err)
99362
+ });
99363
+ }
99364
+ } finally {
99365
+ activeAutoPrompts.delete(sessionKey);
99339
99366
  }
99340
99367
  }
99341
- const { data: finalMessages } = await client.session.messages({ path: { id: session.id }, query });
99342
- const finalText = extractLastAssistantText(finalMessages ?? []) || lastText || (Date.now() - start >= timeout ? "\u26A0\uFE0F \u54CD\u5E94\u8D85\u65F6" : "[\u65E0\u56DE\u590D]");
99343
- await replyOrUpdate(feishuClient, chatId, placeholderId, finalText);
99344
99368
  } catch (err) {
99345
99369
  log("error", "\u5BF9\u8BDD\u5904\u7406\u5931\u8D25", {
99346
99370
  error: err instanceof Error ? err.message : String(err)
@@ -99367,6 +99391,30 @@ async function buildPromptParts(feishuClient, messageId, messageType, rawContent
99367
99391
  }
99368
99392
  return parts;
99369
99393
  }
99394
+ async function pollForResponse(client, sessionId, opts) {
99395
+ const { timeout, pollInterval, stablePolls, query, signal } = opts;
99396
+ const start = Date.now();
99397
+ let lastText = "";
99398
+ let sameCount = 0;
99399
+ while (Date.now() - start < timeout) {
99400
+ if (signal) {
99401
+ await abortableSleep(pollInterval, signal);
99402
+ } else {
99403
+ await new Promise((r) => setTimeout(r, pollInterval));
99404
+ }
99405
+ const { data: messages } = await client.session.messages({ path: { id: sessionId }, query });
99406
+ const text2 = extractLastAssistantText(messages ?? []);
99407
+ if (text2 && text2 !== lastText) {
99408
+ lastText = text2;
99409
+ sameCount = 0;
99410
+ } else if (text2 && text2.length > 0) {
99411
+ sameCount++;
99412
+ if (sameCount >= stablePolls) break;
99413
+ }
99414
+ }
99415
+ const { data: finalMessages } = await client.session.messages({ path: { id: sessionId }, query });
99416
+ return extractLastAssistantText(finalMessages ?? []) || lastText;
99417
+ }
99370
99418
  async function replyOrUpdate(feishuClient, chatId, placeholderId, text2) {
99371
99419
  if (placeholderId) {
99372
99420
  const res = await updateMessage(feishuClient, placeholderId, text2);
@@ -99377,6 +99425,27 @@ async function replyOrUpdate(feishuClient, chatId, placeholderId, text2) {
99377
99425
  await sendTextMessage(feishuClient, chatId, text2);
99378
99426
  }
99379
99427
  }
99428
+ function abortableSleep(ms, signal) {
99429
+ return new Promise((resolve, reject) => {
99430
+ if (signal.aborted) {
99431
+ reject(new DOMException("Aborted", "AbortError"));
99432
+ return;
99433
+ }
99434
+ const onDone = () => {
99435
+ clearTimeout(timer);
99436
+ signal.removeEventListener("abort", onAbort);
99437
+ };
99438
+ const onAbort = () => {
99439
+ onDone();
99440
+ reject(new DOMException("Aborted", "AbortError"));
99441
+ };
99442
+ const timer = setTimeout(() => {
99443
+ onDone();
99444
+ resolve();
99445
+ }, ms);
99446
+ signal.addEventListener("abort", onAbort, { once: true });
99447
+ });
99448
+ }
99380
99449
  function extractLastAssistantText(messages) {
99381
99450
  const assistant = messages.filter((m) => m.info?.role === "assistant").pop();
99382
99451
  const parts = assistant?.parts ?? [];
@@ -99476,7 +99545,13 @@ var DEFAULT_CONFIG = {
99476
99545
  pollInterval: 1e3,
99477
99546
  stablePolls: 3,
99478
99547
  dedupTtl: 10 * 60 * 1e3,
99479
- directory: ""
99548
+ directory: "",
99549
+ autoPrompt: {
99550
+ enabled: false,
99551
+ intervalSeconds: 30,
99552
+ maxIterations: 10,
99553
+ message: "\u8BF7\u540C\u6B65\u5F53\u524D\u8FDB\u5EA6\uFF0C\u5982\u9700\u5E2E\u52A9\u8BF7\u8BF4\u660E"
99554
+ }
99480
99555
  };
99481
99556
  var FeishuPlugin = async (ctx) => {
99482
99557
  const { client } = ctx;
@@ -99530,7 +99605,11 @@ var FeishuPlugin = async (ctx) => {
99530
99605
  pollInterval: feishuRaw.pollInterval ?? DEFAULT_CONFIG.pollInterval,
99531
99606
  stablePolls: feishuRaw.stablePolls ?? DEFAULT_CONFIG.stablePolls,
99532
99607
  dedupTtl: feishuRaw.dedupTtl ?? DEFAULT_CONFIG.dedupTtl,
99533
- directory: expandDirectoryPath(feishuRaw.directory ?? ctx.directory ?? DEFAULT_CONFIG.directory)
99608
+ directory: expandDirectoryPath(feishuRaw.directory ?? ctx.directory ?? DEFAULT_CONFIG.directory),
99609
+ autoPrompt: {
99610
+ ...DEFAULT_CONFIG.autoPrompt,
99611
+ ...feishuRaw.autoPrompt ?? {}
99612
+ }
99534
99613
  };
99535
99614
  initDedup(resolvedConfig.dedupTtl);
99536
99615
  const botOpenId = await fetchBotOpenId(resolvedConfig.appId, resolvedConfig.appSecret, log);