wechat-opencode-bot 0.2.4 → 0.2.5

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
@@ -87,6 +87,12 @@ opencode serve 为无人值守环境。建议在 opencode 配置(`~/.config/op
87
87
  - `bash`/`read` 等放行:微信里可直接让 AI 执行 `tea`、`git` 等命令并读取文件
88
88
  - `skill` 设为 `deny`:skill 工具在 serve 环境可能不触发事件结束(卡住会话),建议禁止
89
89
 
90
+ ### PATH 注意事项
91
+
92
+ serve 由 launchd / 计划任务启动时,PATH 常缺少 `/opt/homebrew/bin`(macOS)或 WinGet 目录(Windows),会导致 AI 执行 `which ffmpeg`、`tea` 等找不到工具而误判"未安装"。
93
+
94
+ 本工具的 `wechat-bot install` 生成的 plist 已自动补全这些路径(`buildPath()`);`wechat-bot serve` 也会把补全后的 PATH 传给 opencode serve。若你手动用 launchd 跑 `opencode serve`,请自行在 plist 的 `EnvironmentVariables.PATH` 加入 `/opt/homebrew/bin` 等目录。
95
+
90
96
  ## 数据文件
91
97
 
92
98
  - `sessions.json`:微信用户 → opencode session 映射
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wechat-opencode-bot",
3
- "version": "0.2.4",
3
+ "version": "0.2.5",
4
4
  "description": "微信 ⇄ opencode 桥接机器人:微信消息通过 opencode serve 驱动 AI 回复,支持定时任务主动推送、媒体收发,跨平台(macOS/Windows/Linux)。",
5
5
  "type": "module",
6
6
  "bin": {
package/src/bridge.js CHANGED
@@ -408,7 +408,10 @@ async function askOpenCode(sessionId, text) {
408
408
  [schedule_del: 任务ID] 删除指定定时任务
409
409
  到点后系统会调用你生成内容并主动推送给用户。
410
410
  回复要求: 请给出完整、详细、原样的回答,把你在命令行中会输出的全部内容直接发过来,不要缩略、不要只给总结或要点提示,确保用户看到完整答案。如果执行了代码或命令,把关键输出也一并附上。
411
- 微信格式: 不要使用 ASCII 艺术表格(┌─┬─┐ box-drawing 字符),微信会显示错乱。表格/列表改用纯文本(如 "编号 - 标题 (状态)"、"- 项目"、用缩进和符号组织),代码块用围栏标记即可。`;
411
+ 微信格式: 不要使用 ASCII 艺术表格(┌─┬─┐ box-drawing 字符),微信会显示错乱。表格/列表改用纯文本(如 "编号 - 标题 (状态)"、"- 项目"、用缩进和符号组织),代码块用围栏标记即可。
412
+ 截图能力: 用户要求"截图/截屏"网页时,用 headless Chrome 截取 URL 并保存 PNG,再用 [media:] 标记发出:
413
+ /Applications/Google Chrome.app/Contents/MacOS/Google Chrome --headless --disable-gpu --screenshot=/tmp/wechat-shot.png --window-size=1280,800 "目标URL"
414
+ 截图后回复 [media: /tmp/wechat-shot.png]。URL 若需登录可提示用户;若截图失败把错误告诉用户。`;
412
415
 
413
416
  // 确认 session 存在
414
417
  await api(`/session/${sessionId}`, {}, 5000);
@@ -426,7 +429,15 @@ async function askOpenCode(sessionId, text) {
426
429
  const textParts = parts
427
430
  .filter((p) => p.type === 'text' && typeof p.text === 'string')
428
431
  .map((p) => p.text);
429
- return textParts.join('\n').trim();
432
+ const answer = textParts.join('\n').trim();
433
+ if (answer) return answer;
434
+ // 回退:模型有时把结论只写在 reasoning 里,text 为空,此时用 reasoning 兜底
435
+ const reasoning = parts
436
+ .filter((p) => p.type === 'reasoning' && typeof p.text === 'string')
437
+ .map((p) => p.text)
438
+ .join('\n')
439
+ .trim();
440
+ return reasoning;
430
441
  }
431
442
 
432
443
  // ─── 微信消息处理 ─────────────────────────────────────
@@ -570,16 +581,18 @@ async function handleMessage(msg) {
570
581
  // 处理 schedule 标记,再发送媒体
571
582
  if (fullReply) {
572
583
  let textToSend = processScheduleMarks(fullReply, fromUser);
584
+ let mediaSent = 0;
573
585
  try {
574
- const { text, mediaSent } = await sendMediaMarks(fromUser, contextToken, textToSend);
575
- textToSend = text;
586
+ const r = await sendMediaMarks(fromUser, contextToken, textToSend);
587
+ textToSend = r.text;
588
+ mediaSent = r.mediaSent;
576
589
  if (mediaSent > 0) log(`已发送 ${mediaSent} 个媒体文件 from=${fromUser}`);
577
590
  } catch (err) {
578
591
  log(`媒体处理失败: ${String(err)}`);
579
592
  }
580
593
  // 一次性分段发送
581
- if (textToSend.trim()) {
582
- const plainText = stripMarkdown(textToSend);
594
+ const plainText = textToSend.trim() ? stripMarkdown(textToSend) : '';
595
+ if (plainText) {
583
596
  const chunks = chunkText(plainText, 3900);
584
597
  for (const c of chunks) {
585
598
  const ok = await sendWechat(fromUser, c, contextToken, messageId);
@@ -587,9 +600,17 @@ async function handleMessage(msg) {
587
600
  await sleep(1000); // 发送间隔,避免高频触发微信限流
588
601
  }
589
602
  log(`已回复 from=${fromUser} chunks=${chunks.length}`);
590
- } else {
603
+ } else if (mediaSent > 0) {
591
604
  log(`已回复(仅媒体) from=${fromUser}`);
605
+ } else {
606
+ // AI 既无文本也无媒体,发提示避免静默无响应
607
+ await sendWechat(fromUser, '(我这边没有生成有效回复,请再试一次或换个说法)', contextToken, messageId);
608
+ log(`已回复(空回复兜底) from=${fromUser}`);
592
609
  }
610
+ } else {
611
+ // 完全空回复(模型只思考未产出文本)
612
+ await sendWechat(fromUser, '(我这边没有生成有效回复,请再试一次或换个说法)', contextToken, messageId);
613
+ log(`已回复(空回复兜底) from=${fromUser}`);
593
614
  }
594
615
  }
595
616
 
package/src/cli.js CHANGED
@@ -13,7 +13,7 @@
13
13
  import { spawn, spawnSync } from 'node:child_process';
14
14
  import { existsSync, readFileSync } from 'node:fs';
15
15
  import { fileURLToPath } from 'node:url';
16
- import { join, dirname } from 'node:path';
16
+ import { join } from 'node:path';
17
17
  import { homedir } from 'node:os';
18
18
  import {
19
19
  SERVER_BASE,
@@ -57,6 +57,20 @@ function resolveOpenCodeBin() {
57
57
  return null;
58
58
  }
59
59
 
60
+ /**
61
+ * 补全 PATH:launchd/计划任务环境下 PATH 常缺 homebrew 等目录,
62
+ * 会导致 AI 在 serve 里执行 `which ffmpeg` 等找不到工具。
63
+ * 把常见工具目录加入 PATH 最前面。
64
+ */
65
+ function buildPath() {
66
+ const sep = isWin ? ';' : ':';
67
+ const extra = isWin
68
+ ? [join(homedir(), 'AppData', 'Local', 'Microsoft', 'WinGet', 'Links')]
69
+ : ['/opt/homebrew/bin', '/usr/local/bin', join(homedir(), '.local', 'bin')];
70
+ const cur = (process.env.PATH || '').split(sep);
71
+ return [...new Set([...extra, ...cur])].filter(Boolean).join(sep);
72
+ }
73
+
60
74
  function usage() {
61
75
  console.log(`opencode-wechat-bot
62
76
  用法:
@@ -90,6 +104,7 @@ async function startServe() {
90
104
  // 禁用 wechat MCP,避免与 bridge 抢轮询;可选鉴权
91
105
  const env = {
92
106
  ...process.env,
107
+ PATH: buildPath(),
93
108
  OPENCODE_CONFIG_CONTENT: '{"mcp":{"wechat":{"enabled":false}}}',
94
109
  };
95
110
  console.log(`[serve] 启动 ${opencodeBin} ${args.join(' ')}`);
@@ -184,13 +199,7 @@ async function installService() {
184
199
  ['serve'],
185
200
  {
186
201
  OPENCODE_CONFIG_CONTENT: '{"mcp":{"wechat":{"enabled":false}}}',
187
- PATH: [
188
- dirname(resolveOpenCodeBin() || ''),
189
- process.env.PATH || '',
190
- '/usr/local/bin',
191
- '/opt/homebrew/bin',
192
- join(homedir(), '.local', 'bin'),
193
- ].filter(Boolean).join(':'),
202
+ PATH: buildPath(),
194
203
  },
195
204
  ),
196
205
  bridge: makePlist(
@@ -198,13 +207,7 @@ async function installService() {
198
207
  ['bridge'],
199
208
  {
200
209
  WECHAT_BOT_HOME: BOT_HOME,
201
- PATH: [
202
- dirname(resolveOpenCodeBin() || ''),
203
- process.env.PATH || '',
204
- '/usr/local/bin',
205
- '/opt/homebrew/bin',
206
- join(homedir(), '.local', 'bin'),
207
- ].filter(Boolean).join(':'),
210
+ PATH: buildPath(),
208
211
  },
209
212
  ),
210
213
  };