opencode-feishu 0.4.1 → 0.5.0

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
@@ -71,6 +71,21 @@ opencode
71
71
  | `pollInterval` | number | 否 | `1000` | 轮询 AI 响应的间隔(毫秒) |
72
72
  | `stablePolls` | number | 否 | `3` | 连续几次轮询内容不变视为回复完成 |
73
73
  | `dedupTtl` | number | 否 | `600000` | 消息去重缓存过期时间(毫秒) |
74
+ | `directory` | string | 否 | `""` | 默认工作目录,支持 `~` 和 `${ENV_VAR}` 展开 |
75
+ | `autoPrompt.enabled` | boolean | 否 | `false` | 启用自动提示(响应完成后自动发送"继续") |
76
+ | `autoPrompt.intervalSeconds` | number | 否 | `30` | 响应完成后等待秒数 |
77
+ | `autoPrompt.maxIterations` | number | 否 | `10` | 单轮对话最大自动提示次数 |
78
+ | `autoPrompt.message` | string | 否 | `"请同步当前进度,如需帮助请说明"` | 自动发送的提示内容 |
79
+
80
+ ## 特性
81
+
82
+ - **多媒体消息支持** — 图片、文件、音频、富文本、卡片等,自动下载为 data URL
83
+ - **实时流式更新** — 通过 `message.part.updated` 事件更新占位消息
84
+ - **群聊静默监听** — 所有群消息作为上下文积累,仅 @提及时回复
85
+ - **入群自动摄入历史消息**
86
+ - **代理支持** — `HTTPS_PROXY` / `HTTP_PROXY` / `ALL_PROXY`
87
+ - **消息去重** — 可配置 TTL
88
+ - **自动提示** — 响应完成后自动发送"继续",推动 OpenCode 持续工作;用户发新消息自动中断
74
89
 
75
90
  ## 群聊行为
76
91
 
@@ -84,11 +99,26 @@ opencode
84
99
  ## 开发
85
100
 
86
101
  ```bash
87
- npm install # 安装依赖
88
- npm run build # 构建
89
- npm run dev # 开发模式(监听变更)
90
- npm run typecheck # 类型检查
91
- npm publish # 发布到 npm
102
+ npm install # 安装依赖
103
+ npm run build # 构建
104
+ npm run dev # 开发模式(监听变更)
105
+ npm run typecheck # 类型检查
106
+ npm run release # 交互式版本发布(bumpp:选版本 → commit → tag → push)
107
+ npm publish # 发布到 npm(自动先构建+类型检查)
108
+ npm publish --dry-run # 预览将要发布的内容
109
+ ```
110
+
111
+ ## 调试
112
+
113
+ ```bash
114
+ # 启用调试日志(结构化 JSON 输出到 stderr)
115
+ FEISHU_DEBUG=1 opencode
116
+
117
+ # 过滤错误日志
118
+ FEISHU_DEBUG=1 opencode 2>&1 | grep '"level":"error"'
119
+
120
+ # 重定向到文件
121
+ FEISHU_DEBUG=1 opencode 2>feishu-debug.log
92
122
  ```
93
123
 
94
124
  ## 许可证
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);