wechat-opencode-bot 0.1.0 → 0.2.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
@@ -41,11 +41,11 @@ wechat-bot install # macOS 生成 launchd plist 并加载
41
41
  | 变量 | 默认值 | 说明 |
42
42
  |---|---|---|
43
43
  | `WECHAT_BOT_HOME` | `~/.wechat-bot` | 数据目录(sessions/tasks/media/logs/pids) |
44
- | `WECHAT_STATE_DIR` | `~/.claude/channels/wechat/<profile>` | cc-wechat 状态目录 |
45
- | `WECHAT_PROFILE` | `default` | cc-wechat 账号 profile |
44
+ | `WECHAT_PROFILE` | `default` | cc-wechat 账号 profile,状态目录为 `~/.claude/channels/wechat/<profile>` |
46
45
  | `OPENCODE_SERVER_URL` | `http://127.0.0.1:4100` | opencode serve 地址 |
47
46
  | `OPENCODE_PORT` | `4100` | opencode serve 端口 |
48
- | `OPENCODE_BIN` | `opencode` | opencode 可执行文件 |
47
+ | `OPENCODE_BIN` | 自动探测 | opencode 可执行文件路径 |
48
+ | `OPENCODE_SERVER_PASSWORD` | 空 | 可选: opencode serve 鉴权密码(设置后 bridge 自动携带) |
49
49
  | `LLM_TIMEOUT_MS` | `300000` | LLM 响应超时 |
50
50
  | `BOT_PREFIX` | 空 | 仅响应带此前缀的消息 |
51
51
 
@@ -58,6 +58,27 @@ wechat-bot install # macOS 生成 launchd plist 并加载
58
58
  - **图片/文件**:接收时下载到本地供 AI 读取(`[图片: 路径]`);发送时用 `[media: /绝对/路径]` 标记上传。
59
59
  - **自动重连**:网络错误指数退避,session 过期自动提示重新登录。
60
60
 
61
+ ## 权限配置
62
+
63
+ opencode serve 为无人值守环境。建议在 opencode 配置(`~/.config/opencode/opencode.json`)中设置权限,让工具自动执行而不触发权限确认(权限确认在无头环境会卡住会话):
64
+
65
+ ```json
66
+ {
67
+ "permission": {
68
+ "bash": { "*": "allow" },
69
+ "read": { "*": "allow" },
70
+ "write": { "*": "allow" },
71
+ "edit": { "*": "allow" },
72
+ "skill": { "*": "deny" },
73
+ "question": "allow",
74
+ "webfetch": "allow"
75
+ }
76
+ }
77
+ ```
78
+
79
+ - `bash`/`read` 等放行:微信里可直接让 AI 执行 `tea`、`git` 等命令并读取文件
80
+ - `skill` 设为 `deny`:skill 工具在 serve 环境可能不触发事件结束(卡住会话),建议禁止
81
+
61
82
  ## 数据文件
62
83
 
63
84
  - `sessions.json`:微信用户 → opencode session 映射
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wechat-opencode-bot",
3
- "version": "0.1.0",
3
+ "version": "0.2.1",
4
4
  "description": "微信 ⇄ opencode 桥接机器人:微信消息通过 opencode serve 驱动 AI 回复,支持定时任务主动推送、媒体收发,跨平台(macOS/Windows/Linux)。",
5
5
  "type": "module",
6
6
  "bin": {
package/src/bridge.js CHANGED
@@ -12,7 +12,6 @@ import { join } from 'node:path';
12
12
  import { readFileSync, writeFileSync, existsSync } from 'node:fs';
13
13
  import {
14
14
  findCcWechatDir,
15
- STATE_DIR,
16
15
  SESSION_MAP_FILE,
17
16
  MEDIA_DIR,
18
17
  TASKS_FILE,
@@ -54,8 +53,23 @@ let pollingAbort = null;
54
53
  const sessionMap = loadSessionMap();
55
54
  const typingTicketCache = new Map();
56
55
  const processedIds = new Set();
56
+ const userQueues = new Map(); // userId -> Promise 链,同用户串行、不同用户并发
57
57
  let account = null;
58
58
 
59
+ /** 将消息处理任务按用户排队:同一用户串行,不同用户并发 */
60
+ function enqueueHandle(msg) {
61
+ const fromUser = msg.from_user_id ?? '';
62
+ const prev = userQueues.get(fromUser) || Promise.resolve();
63
+ const next = prev
64
+ .then(() => handleMessage(msg))
65
+ .catch((err) => log(`处理消息失败 from=${fromUser}: ${String(err)}`))
66
+ .finally(() => {
67
+ if (userQueues.get(fromUser) === next) userQueues.delete(fromUser);
68
+ });
69
+ userQueues.set(fromUser, next);
70
+ return next;
71
+ }
72
+
59
73
  function loadSessionMap() {
60
74
  try {
61
75
  if (existsSync(SESSION_MAP_FILE)) {
@@ -204,17 +218,18 @@ async function fireTask(task) {
204
218
  const prompt = `【定时任务触发】${task.prompt}\n请据此生成一条发给用户的微信消息。`;
205
219
  let reply = '';
206
220
  try {
207
- reply = await askOpenCode(sessionId, prompt);
221
+ reply = await askOpenCodeStream(sessionId, prompt, async (chunk) => {
222
+ const plainText = stripMarkdown(chunk);
223
+ if (!plainText.trim()) return;
224
+ for (const c of chunkText(plainText, 3900)) {
225
+ await sendMessage(account.token, task.userId, c, '', account.baseUrl, undefined);
226
+ }
227
+ });
208
228
  } catch (err) {
209
229
  reply = `[定时任务执行失败: ${String(err.message ?? err)}]`;
210
230
  }
211
231
  if (reply) {
212
- const plainText = stripMarkdown(reply);
213
- const chunks = chunkText(plainText, 3900);
214
- for (const chunk of chunks) {
215
- await sendMessage(account.token, task.userId, chunk, '', account.baseUrl, undefined);
216
- }
217
- log(`定时任务已推送 ${task.id} chunks=${chunks.length}`);
232
+ log(`定时任务已推送 ${task.id}`);
218
233
  }
219
234
  // 一次性任务执行后移除
220
235
  if (task.parsed.type === 'once') {
@@ -280,6 +295,16 @@ function sleep(ms, signal) {
280
295
  }
281
296
 
282
297
  // ─── opencode serve 调用 ─────────────────────────────
298
+ // 可选鉴权:与 cli.js startServe 的 OPENCODE_SERVER_PASSWORD 配合
299
+ const SERVER_PASSWORD = process.env.OPENCODE_SERVER_PASSWORD || '';
300
+ const authHeader = SERVER_PASSWORD
301
+ ? { Authorization: `Basic ${Buffer.from(`opencode:${SERVER_PASSWORD}`).toString('base64')}` }
302
+ : {};
303
+
304
+ function buildHeaders(extra = {}) {
305
+ return { 'Content-Type': 'application/json', ...authHeader, ...extra };
306
+ }
307
+
283
308
  async function api(path, options = {}, timeoutMs = 30_000) {
284
309
  const ctrl = new AbortController();
285
310
  const t = setTimeout(() => ctrl.abort(), timeoutMs);
@@ -287,7 +312,7 @@ async function api(path, options = {}, timeoutMs = 30_000) {
287
312
  const resp = await fetch(`${SERVER_BASE}${path}`, {
288
313
  ...options,
289
314
  signal: ctrl.signal,
290
- headers: { 'Content-Type': 'application/json', ...(options.headers || {}) },
315
+ headers: buildHeaders(options.headers || {}),
291
316
  });
292
317
  const text = await resp.text();
293
318
  if (!resp.ok) {
@@ -319,28 +344,160 @@ async function getSessionForUser(userId) {
319
344
  return session.id;
320
345
  }
321
346
 
322
- /** 向 opencode session 发送消息并等待完整回复文本 */
323
- async function askOpenCode(sessionId, text) {
324
- const SYSTEM_HINT = `你是微信自动回复助手。收到 [图片: 路径] 或 [文件: 路径] 标记时,可用 read 工具读取内容。
347
+ /**
348
+ * opencode session 异步发送消息,通过 SSE 事件流实时获取回复。
349
+ * 流式累积文本,按边界分块回调 onChunk(用于实时推送到微信),
350
+ * 返回完整回复文本(供媒体标记/schedule 处理)。
351
+ */
352
+ async function askOpenCodeStream(sessionId, text, onChunk) {
353
+ const SYSTEM_HINT = `你是微信自动回复助手,收到 [图片: 路径] 或 [文件: 路径] 标记时,可用 read 工具读取内容。
325
354
  如需在回复中发送媒体文件(本地绝对路径),在回复文本中用 [media: /绝对/路径] 标记,一行一个,其余内容按普通文本发送。
326
355
  定时任务指令(用户要求定时提醒/推送时使用):
327
356
  [schedule: 时间表达式 | 任务提示] 添加定时任务。时间表达式支持: HH:MM(每天) / 每N小时 / 每N分钟 / YYYY-MM-DD HH:MM(一次性)
328
357
  [schedule_list] 查看当前定时任务
329
358
  [schedule_del: 任务ID] 删除指定定时任务
330
- 到点后系统会调用你生成内容并主动推送给用户。`;
331
- const resp = await api(
332
- `/session/${sessionId}/message`,
359
+ 到点后系统会调用你生成内容并主动推送给用户。
360
+ 回复要求: 请给出完整、详细、原样的回答,把你在命令行中会输出的全部内容直接发过来,不要缩略、不要只给总结或要点提示,确保用户看到完整答案。如果执行了代码或命令,把关键输出也一并附上。`;
361
+
362
+ // 确认 session 存在
363
+ await api(`/session/${sessionId}`, {}, 5000);
364
+
365
+ // 1. 建立 SSE 事件流
366
+ const ctrl = new AbortController();
367
+ const eventFetch = fetch(`${SERVER_BASE}/event`, { signal: ctrl.signal, headers: buildHeaders() });
368
+ // 2. 异步发送消息(204 立即返回)
369
+ await api(
370
+ `/session/${sessionId}/prompt_async`,
333
371
  {
334
372
  method: 'POST',
335
373
  body: JSON.stringify({ parts: [{ type: 'text', text: `${SYSTEM_HINT}\n\n微信消息:\n${text}` }] }),
336
374
  },
337
- MAX_LLM_RESPONSE_TIME_MS,
375
+ 10_000,
338
376
  );
339
- const parts = resp?.parts || [];
340
- const textParts = parts
341
- .filter((p) => p.type === 'text' && typeof p.text === 'string')
342
- .map((p) => p.text);
343
- return textParts.join('\n').trim();
377
+
378
+ let accumulated = '';
379
+ let streamingBuffer = '';
380
+ let done = false;
381
+ const reasoningPartIDs = new Set();
382
+
383
+ const resp = await eventFetch;
384
+ if (!resp.ok || !resp.body) {
385
+ throw new Error(`SSE 连接失败: HTTP ${resp.status}`);
386
+ }
387
+ const reader = resp.body.getReader();
388
+ const decoder = new TextDecoder();
389
+ let buf = '';
390
+
391
+ let sendChain = Promise.resolve();
392
+ const flushBuffer = () => {
393
+ if (!streamingBuffer.trim()) return sendChain;
394
+ const chunk = streamingBuffer.trim();
395
+ streamingBuffer = '';
396
+ accumulated += chunk;
397
+ if (onChunk) {
398
+ // 串行发送队列,保证 onChunk 顺序执行(避免并发乱序)
399
+ sendChain = sendChain.then(() => onChunk(chunk));
400
+ }
401
+ return sendChain;
402
+ };
403
+
404
+ const timeout = setTimeout(async () => {
405
+ if (!done) {
406
+ log(`流式超时(${MAX_LLM_RESPONSE_TIME_MS / 1000}s),abort session ${sessionId}`);
407
+ try {
408
+ await api(`/session/${sessionId}/abort`, { method: 'POST', body: '{}' }, 5000);
409
+ } catch {}
410
+ ctrl.abort();
411
+ done = true;
412
+ }
413
+ }, MAX_LLM_RESPONSE_TIME_MS);
414
+
415
+ // 兜底检测:SSE 可能漏发 idle 事件(工具执行后),定期轮询 session 状态判断是否真正结束;
416
+ // 长时间无任何新 part 则视为卡死,abort session 避免永久 busy
417
+ let lastProgress = Date.now();
418
+ const stallTimer = setInterval(async () => {
419
+ if (done) return;
420
+ // 兜底:SSE 漏发 idle 时,主动查 session 状态结束循环
421
+ try {
422
+ const status = await api(`/session/status`, {}, 5000);
423
+ const entry = status?.[sessionId];
424
+ const st = typeof entry?.type === 'string' ? entry.type : entry?.status?.type;
425
+ if (st === 'idle') {
426
+ log(`SSE 未收到 idle,经轮询确认 session 已空闲`);
427
+ done = true;
428
+ return;
429
+ }
430
+ } catch {}
431
+ if (Date.now() - lastProgress > 90_000) {
432
+ log(`流式无进度超时(90s),abort session ${sessionId}`);
433
+ try {
434
+ await api(`/session/${sessionId}/abort`, { method: 'POST', body: '{}' }, 5000);
435
+ } catch {}
436
+ ctrl.abort();
437
+ done = true;
438
+ }
439
+ }, 10_000);
440
+
441
+ try {
442
+ while (!done) {
443
+ const { done: streamDone, value } = await reader.read();
444
+ if (streamDone) break;
445
+ buf += decoder.decode(value, { stream: true });
446
+ const lines = buf.split('\n');
447
+ buf = lines.pop();
448
+ for (const line of lines) {
449
+ if (!line.startsWith('data:')) continue;
450
+ const raw = line.slice(5).trim();
451
+ if (!raw) continue;
452
+ let evt;
453
+ try {
454
+ evt = JSON.parse(raw);
455
+ } catch {
456
+ continue;
457
+ }
458
+ const p = evt.properties ?? {};
459
+ if (p.sessionID && p.sessionID !== sessionId) continue;
460
+ switch (evt.type) {
461
+ case 'message.part.delta': {
462
+ if (p.field === 'text' && typeof p.delta === 'string') {
463
+ if (p.partID && reasoningPartIDs.has(p.partID)) break;
464
+ lastProgress = Date.now();
465
+ streamingBuffer += p.delta;
466
+ // 达到块大小或句子边界即推送
467
+ if (
468
+ streamingBuffer.length >= 60 ||
469
+ /[。!?\n]/.test(streamingBuffer.slice(-2))
470
+ ) {
471
+ await flushBuffer();
472
+ }
473
+ }
474
+ break;
475
+ }
476
+ case 'message.part.updated': {
477
+ const part = p.part ?? {};
478
+ if (part.type === 'reasoning' && part.id) {
479
+ reasoningPartIDs.add(part.id);
480
+ }
481
+ lastProgress = Date.now(); // 任何 part 更新都算进度
482
+ break;
483
+ }
484
+ case 'session.status': {
485
+ const st = p.status ?? {};
486
+ if (st.type === 'idle') done = true;
487
+ break;
488
+ }
489
+ default:
490
+ break;
491
+ }
492
+ }
493
+ }
494
+ } finally {
495
+ clearTimeout(timeout);
496
+ clearInterval(stallTimer);
497
+ ctrl.abort();
498
+ }
499
+ await flushBuffer();
500
+ return accumulated.trim();
344
501
  }
345
502
 
346
503
  // ─── 微信消息处理 ─────────────────────────────────────
@@ -471,20 +628,31 @@ async function handleMessage(msg) {
471
628
  return;
472
629
  }
473
630
 
474
- let reply = '';
631
+ let fullReply = '';
475
632
  try {
476
- reply = await askOpenCode(sessionId, text);
633
+ fullReply = await askOpenCodeStream(sessionId, text, async (chunk) => {
634
+ // 实时推送:边生成边发
635
+ const plainText = stripMarkdown(chunk);
636
+ if (!plainText.trim()) return;
637
+ const chunks = chunkText(plainText, 3900);
638
+ for (const c of chunks) {
639
+ try {
640
+ await sendMessage(account.token, fromUser, c, contextToken, account.baseUrl, messageId);
641
+ } catch (err) {
642
+ log(`发送失败: ${String(err)}`);
643
+ }
644
+ }
645
+ });
477
646
  } catch (err) {
478
647
  log(`调用 opencode 失败: ${String(err)}`);
479
- reply = `[处理失败: ${String(err.message ?? err)}]`;
648
+ fullReply = `[处理失败: ${String(err.message ?? err)}]`;
480
649
  }
481
650
 
482
651
  await sendTypingIndicator(fromUser, contextToken, 2);
483
652
 
484
- if (reply) {
485
- // 处理定时任务标记(schedule_*)
486
- let textToSend = processScheduleMarks(reply, fromUser);
487
- // 再发送媒体标记
653
+ // 流式已把文本发出,最后处理媒体与 schedule 标记
654
+ if (fullReply) {
655
+ let textToSend = processScheduleMarks(fullReply, fromUser);
488
656
  try {
489
657
  const { text, mediaSent } = await sendMediaMarks(fromUser, contextToken, textToSend);
490
658
  textToSend = text;
@@ -492,19 +660,8 @@ async function handleMessage(msg) {
492
660
  } catch (err) {
493
661
  log(`媒体处理失败: ${String(err)}`);
494
662
  }
495
- if (textToSend) {
496
- const plainText = stripMarkdown(textToSend);
497
- const chunks = chunkText(plainText, 3900);
498
- for (const chunk of chunks) {
499
- try {
500
- await sendMessage(account.token, fromUser, chunk, contextToken, account.baseUrl, messageId);
501
- } catch (err) {
502
- log(`发送失败: ${String(err)}`);
503
- }
504
- }
505
- log(`已回复 from=${fromUser} chunks=${chunks.length}`);
506
- } else {
507
- log(`已回复(仅媒体) from=${fromUser}`);
663
+ if (textToSend.trim()) {
664
+ log(`已回复(流式) from=${fromUser}`);
508
665
  }
509
666
  }
510
667
  }
@@ -553,11 +710,8 @@ async function pollLoop() {
553
710
  saveSyncBuf(buf);
554
711
  }
555
712
  for (const msg of resp.msgs ?? []) {
556
- try {
557
- await handleMessage(msg);
558
- } catch (err) {
559
- log(`处理消息失败: ${String(err)}`);
560
- }
713
+ // 按用户排队并发处理:不同用户并行,同用户串行,避免单条阻塞全部轮询
714
+ enqueueHandle(msg);
561
715
  }
562
716
  } catch (err) {
563
717
  if (pollingAbort?.signal.aborted) return;
@@ -593,7 +747,7 @@ function stopPolling() {
593
747
  }
594
748
 
595
749
  // ─── 主入口 ───────────────────────────────────────────
596
- export async function startBridge() {
750
+ async function startBridge() {
597
751
  account = getActiveAccount();
598
752
  if (!account) {
599
753
  log('未找到账号。请先登录: npx cc-wechat login');
@@ -640,6 +794,7 @@ export async function startBridge() {
640
794
  }
641
795
 
642
796
  export {
797
+ startBridge,
643
798
  parseScheduleExpr,
644
799
  addTask,
645
800
  deleteTask,
package/src/cli.js CHANGED
@@ -10,10 +10,11 @@
10
10
  * wechat-bot install 安装自启服务(macOS: launchd;Windows: 计划任务)
11
11
  * wechat-bot uninstall 卸载自启服务
12
12
  */
13
- import { spawn } from 'node:child_process';
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 } from 'node:path';
16
+ import { join, dirname } from 'node:path';
17
+ import { homedir } from 'node:os';
17
18
  import {
18
19
  SERVER_BASE,
19
20
  SERVE_PORT,
@@ -25,6 +26,37 @@ import {
25
26
 
26
27
  const isWin = process.platform === 'win32';
27
28
 
29
+ /** 探测 opencode 可执行文件真实路径(launchd/计划任务环境 PATH 可能不含它) */
30
+ function resolveOpenCodeBin() {
31
+ const fromEnv = process.env.OPENCODE_BIN;
32
+ if (fromEnv) return fromEnv;
33
+ if (!isWin) {
34
+ const which = spawnSync('which', ['opencode'], { encoding: 'utf-8' });
35
+ if (which.status === 0 && which.stdout.trim()) return which.stdout.trim();
36
+ // 常见安装位置
37
+ const candidates = [
38
+ join(homedir(), '.local', 'bin', 'opencode'),
39
+ join(homedir(), '.opencode', 'bin', 'opencode'),
40
+ '/usr/local/bin/opencode',
41
+ '/opt/homebrew/bin/opencode',
42
+ ];
43
+ for (const c of candidates) {
44
+ if (existsSync(c)) return c;
45
+ }
46
+ } else {
47
+ const where = spawnSync('where', ['opencode'], { encoding: 'utf-8' });
48
+ if (where.status === 0 && where.stdout.trim()) return where.stdout.trim().split('\n')[0];
49
+ const winCandidates = [
50
+ join(process.env.USERPROFILE || homedir(), '.local', 'bin', 'opencode.exe'),
51
+ join(process.env.USERPROFILE || homedir(), 'AppData', 'Roaming', 'npm', 'opencode.cmd'),
52
+ ];
53
+ for (const c of winCandidates) {
54
+ if (existsSync(c)) return c;
55
+ }
56
+ }
57
+ return null;
58
+ }
59
+
28
60
  function usage() {
29
61
  console.log(`opencode-wechat-bot
30
62
  用法:
@@ -37,10 +69,11 @@ function usage() {
37
69
 
38
70
  环境变量:
39
71
  WECHAT_BOT_HOME 数据目录(默认 ~/.wechat-bot)
40
- WECHAT_STATE_DIR cc-wechat 状态目录(默认 ~/.claude/channels/wechat/<profile>)
41
- WECHAT_PROFILE cc-wechat 账号 profile(默认 default)
72
+ WECHAT_PROFILE cc-wechat 账号 profile(默认 default),状态目录为 ~/.claude/channels/wechat/<profile>
42
73
  OPENCODE_SERVER_URL opencode serve 地址(默认 http://127.0.0.1:4100)
43
74
  OPENCODE_PORT opencode serve 端口(默认 4100)
75
+ OPENCODE_BIN opencode 可执行文件路径(默认自动探测)
76
+ OPENCODE_SERVER_PASSWORD 可选: opencode serve 鉴权密码(设置后 bridge 自动携带)
44
77
  LLM_TIMEOUT_MS LLM 响应超时(默认 300000)
45
78
  BOT_PREFIX 可选: 仅响应带此前缀的消息
46
79
  `);
@@ -48,9 +81,13 @@ function usage() {
48
81
 
49
82
  async function startServe() {
50
83
  ensureDirs();
51
- const opencodeBin = process.env.OPENCODE_BIN || 'opencode';
84
+ const opencodeBin = resolveOpenCodeBin();
85
+ if (!opencodeBin) {
86
+ console.error('[serve] 找不到 opencode。请安装或在 OPENCODE_BIN 指定可执行文件路径');
87
+ process.exit(1);
88
+ }
52
89
  const args = ['serve', '--port', String(SERVE_PORT)];
53
- // 禁用 wechat MCP,避免与 bridge 抢轮询
90
+ // 禁用 wechat MCP,避免与 bridge 抢轮询;可选鉴权
54
91
  const env = {
55
92
  ...process.env,
56
93
  OPENCODE_CONFIG_CONTENT: '{"mcp":{"wechat":{"enabled":false}}}',
@@ -147,7 +184,13 @@ async function installService() {
147
184
  ['serve'],
148
185
  {
149
186
  OPENCODE_CONFIG_CONTENT: '{"mcp":{"wechat":{"enabled":false}}}',
150
- PATH: process.env.PATH || '',
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(':'),
151
194
  },
152
195
  ),
153
196
  bridge: makePlist(
@@ -155,7 +198,13 @@ async function installService() {
155
198
  ['bridge'],
156
199
  {
157
200
  WECHAT_BOT_HOME: BOT_HOME,
158
- PATH: process.env.PATH || '',
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(':'),
159
208
  },
160
209
  ),
161
210
  };
package/src/paths.js CHANGED
@@ -3,8 +3,8 @@
3
3
  *
4
4
  * 约定:
5
5
  * - WECHAT_BOT_HOME 桥接自身数据目录(sessions/tasks/media/logs/pids),默认 ~/.wechat-bot
6
- * - WECHAT_STATE_DIR cc-wechat 状态目录(account.json/sync-buf),默认 ~/.claude/channels/wechat/<profile>
7
- * - WECHAT_PROFILE cc-wechat 账号 profile(默认 'default'),与 cc-wechat store.js 保持一致
6
+ * - WECHAT_PROFILE cc-wechat 账号 profile(默认 'default'),状态目录固定为
7
+ * ~/.claude/channels/wechat/<profile>(由 cc-wechat 内部决定)
8
8
  * - OPENCODE_SERVER_URL / OPENCODE_PORT opencode serve 地址(默认 http://127.0.0.1:4100)
9
9
  */
10
10
  import { homedir } from 'node:os';
@@ -16,9 +16,6 @@ const require = createRequire(import.meta.url);
16
16
 
17
17
  const home = homedir();
18
18
  const BOT_HOME = process.env.WECHAT_BOT_HOME || join(home, '.wechat-bot');
19
- const STATE_DIR =
20
- process.env.WECHAT_STATE_DIR ||
21
- join(home, '.claude', 'channels', 'wechat', process.env.WECHAT_PROFILE || 'default');
22
19
  const MEDIA_DIR = join(BOT_HOME, 'media');
23
20
  const SESSION_MAP_FILE = join(BOT_HOME, 'sessions.json');
24
21
  const TASKS_FILE = join(BOT_HOME, 'tasks.json');
@@ -75,7 +72,6 @@ function ensureDirs() {
75
72
  export {
76
73
  home,
77
74
  BOT_HOME,
78
- STATE_DIR,
79
75
  MEDIA_DIR,
80
76
  SESSION_MAP_FILE,
81
77
  TASKS_FILE,