metame-cli 1.5.13 → 1.5.15

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
@@ -358,7 +358,7 @@ systemctl --user start metame
358
358
  | **Mobile Bridge** | Full Claude/Codex via Telegram/Feishu. Stateful sessions, file transfer both ways, real-time streaming status. |
359
359
  | **Skill Evolution** | Queue-driven skill evolution: captures task signals, generates workflow proposals, and supports explicit approval/resolve via `/skill-evo` commands. |
360
360
  | **Token Budget** | Daily token usage tracking with per-category breakdown. Configurable daily limit, automatic 80% warning threshold, usage history with rollover. |
361
- | **Auto-Provisioning** | First run deploys default CLAUDE.md, documentation, and `dispatch_to` to `~/.metame/`. Subsequent runs sync scripts without overwriting user config. |
361
+ | **Auto-Provisioning** | First run deploys default CLAUDE.md, documentation, and runtime copies under `~/.metame/`. Subsequent runs redeploy generated runtime files without overwriting user config in `~/.metame/daemon.yaml`. |
362
362
  | **Heartbeat System** | Three-layer programmable nervous system. Layer 0 kernel always-on (zero config). Layer 1 system evolution built-in (5 tasks: distill + memory + skills + nightly reflection + memory index). Layer 2 your custom scheduled tasks with `require_idle`, `precondition`, `notify`, workflows. |
363
363
  | **Multi-Agent** | Multiple projects with dedicated chat groups. `/agent bind` for one-tap setup. True parallel execution. |
364
364
  | **Team Routing** | Project-level team clones: multiple AI agents work in parallel within a single chat group. Nickname routing, sticky follow, `/stop` per member, broadcast visibility. |
@@ -567,6 +567,40 @@ Use from mobile: `/dispatch to windows:hunter research competitors` or just ment
567
567
  | `/teamtask <task_id>` | View task detail |
568
568
  | `/teamtask resume <task_id>` | Resume a task |
569
569
 
570
+ ## Weixin Direct Bridge
571
+
572
+ MetaMe also supports a direct Weixin bridge. It is separate from WeCom and currently optimized for text-only request/response flows.
573
+
574
+ ### Enable and bind
575
+
576
+ 1. Edit `~/.metame/daemon.yaml` and enable the bridge:
577
+
578
+ ```yaml
579
+ weixin:
580
+ enabled: true
581
+ base_url: "https://ilinkai.weixin.qq.com"
582
+ bot_type: "3"
583
+ account_id: ""
584
+ route_tag: null
585
+ allowed_chat_ids: []
586
+ chat_agent_map: {}
587
+ poll_timeout_ms: 35000
588
+ ```
589
+
590
+ 2. Redeploy or restart MetaMe so the runtime picks up the new config.
591
+ 3. In your admin chat, run `/weixin login start` to generate the login QR/link.
592
+ 4. Scan and confirm with Weixin, then run `/weixin login wait --session <key>`.
593
+ 5. Run `/weixin` or `/weixin status` to verify the account is linked.
594
+
595
+ Natural-language setup is available through the intent hook. If you want the model to expose the setup flow on demand, enable this in `~/.metame/daemon.yaml`:
596
+
597
+ ```yaml
598
+ hooks:
599
+ weixin_bridge: true
600
+ ```
601
+
602
+ Then prompts like “帮我配置微信桥接” or “开始微信扫码登录” will inject the enable-and-bind workflow for the model.
603
+
570
604
  ## Mentor Mode (Why + How)
571
605
 
572
606
  Mentor Mode is designed for users who want MetaMe to actively improve decision quality, not just execute commands.
@@ -679,11 +713,13 @@ MetaMe is early-stage and evolving fast. Every issue and PR directly shapes the
679
713
 
680
714
  **Submit a PR:**
681
715
  1. Fork the repo and create a branch from `main`
682
- 2. All source edits go in `scripts/` run `npm run sync:plugin` to sync to `plugin/scripts/`
716
+ 2. All source edits go in `scripts/`. `~/.metame/` is a generated runtime copy, not a source directory. Run `node index.js` to redeploy local runtime files after edits, and use `npm run sync:plugin` only when you need to refresh `plugin/scripts/`
683
717
  3. Run `npx eslint scripts/daemon*.js` — zero errors required
684
718
  4. Run `npm test` — all tests must pass
685
719
  5. Open a PR against `main` with a clear description
686
720
 
721
+ Source checkouts and `npm link` installs default `metame-cli` auto-update to off. Published npm installs keep auto-update on. Override with `METAME_AUTO_UPDATE=on|off`.
722
+
687
723
  **Good first contributions:** Windows edge cases, new `/commands`, documentation improvements, test coverage.
688
724
 
689
725
  ## License
package/index.js CHANGED
@@ -162,6 +162,25 @@ const DAEMON_CONFIG_FILE = path.join(METAME_DIR, 'daemon.yaml');
162
162
  const METAME_START = '<!-- METAME:START -->';
163
163
  const METAME_END = '<!-- METAME:END -->';
164
164
 
165
+ function resolveAutoUpdateBehavior() {
166
+ const mode = String(process.env.METAME_AUTO_UPDATE || '').trim().toLowerCase();
167
+ if (['0', 'false', 'off', 'disable', 'disabled'].includes(mode)) {
168
+ return { enabled: false, reason: 'env-disabled' };
169
+ }
170
+ if (['1', 'true', 'on', 'enable', 'enabled', 'force'].includes(mode)) {
171
+ return { enabled: true, reason: 'env-forced' };
172
+ }
173
+
174
+ // Linked/source checkouts contain a .git entry (directory in main repo,
175
+ // file in worktrees). Published npm packages do not.
176
+ const dotGit = path.join(__dirname, '.git');
177
+ if (fs.existsSync(dotGit)) {
178
+ return { enabled: false, reason: 'source-checkout' };
179
+ }
180
+
181
+ return { enabled: true, reason: 'installed-package' };
182
+ }
183
+
165
184
  // ---------------------------------------------------------
166
185
  // 1.5 ENSURE METAME DIRECTORY + DEPLOY SCRIPTS
167
186
  // ---------------------------------------------------------
@@ -173,23 +192,10 @@ if (!fs.existsSync(METAME_DIR)) {
173
192
  // DEPLOY PHASE: sync scripts, docs, bin to ~/.metame/
174
193
  // ---------------------------------------------------------
175
194
 
176
- // Dev mode: when running from the real git repo, symlink instead of copy.
177
- // This ensures source files and runtime files are always the same,
178
- // preventing agents from accidentally editing copies instead of source.
179
- // IMPORTANT: git worktrees have a `.git` FILE (not directory) pointing to the main repo.
180
- // They must NOT be treated as dev mode — deploying from a worktree would overwrite
181
- // production symlinks with stale code. Only a real .git directory qualifies.
182
- const IS_DEV_MODE = (() => {
183
- const dotGit = path.join(__dirname, '.git');
184
- try {
185
- return fs.statSync(dotGit).isDirectory();
186
- } catch { return false; }
187
- })();
188
-
189
195
  /**
190
196
  * Sync files from srcDir to destDir.
191
- * - Dev mode (git repo): creates symlinks so source === runtime.
192
- * - Production (npm install): copies files, only writes when content differs.
197
+ * Always copies from source to runtime. Runtime files under ~/.metame are
198
+ * deploy artifacts, never editable sources.
193
199
  * @param {string} srcDir - source directory
194
200
  * @param {string} destDir - destination directory
195
201
  * @param {object} [opts]
@@ -207,35 +213,18 @@ function syncDirFiles(srcDir, destDir, { fileList, chmod } = {}) {
207
213
  const dest = path.join(destDir, f);
208
214
  try {
209
215
  if (!fs.existsSync(src)) continue;
210
-
211
- if (IS_DEV_MODE) {
212
- // Dev mode: symlink dest → src (replace copy/stale symlink if needed)
213
- const srcReal = fs.realpathSync(src);
214
- let needLink = true;
215
- try {
216
- const existing = fs.lstatSync(dest);
217
- if (existing.isSymbolicLink()) {
218
- if (fs.realpathSync(dest) === srcReal) needLink = false;
219
- else fs.unlinkSync(dest);
220
- } else {
221
- // Replace regular file with symlink
222
- fs.unlinkSync(dest);
223
- }
224
- } catch { /* dest doesn't exist */ }
225
- if (needLink) {
226
- fs.symlinkSync(srcReal, dest);
227
- if (chmod) try { fs.chmodSync(dest, chmod); } catch { /* Windows */ }
228
- updated = true;
229
- }
230
- } else {
231
- // Production: copy when content differs
232
- const srcContent = fs.readFileSync(src, 'utf8');
233
- const destContent = fs.existsSync(dest) ? fs.readFileSync(dest, 'utf8') : '';
234
- if (srcContent !== destContent) {
235
- fs.writeFileSync(dest, srcContent, 'utf8');
236
- if (chmod) try { fs.chmodSync(dest, chmod); } catch { /* Windows */ }
237
- updated = true;
238
- }
216
+ // Runtime deploy is always copy-based. Replace any legacy symlink with
217
+ // a regular file so ~/.metame never masquerades as the source of truth.
218
+ try {
219
+ const existing = fs.lstatSync(dest);
220
+ if (existing.isSymbolicLink()) fs.unlinkSync(dest);
221
+ } catch { /* dest doesn't exist */ }
222
+ const srcContent = fs.readFileSync(src, 'utf8');
223
+ const destContent = fs.existsSync(dest) ? fs.readFileSync(dest, 'utf8') : '';
224
+ if (srcContent !== destContent) {
225
+ fs.writeFileSync(dest, srcContent, 'utf8');
226
+ if (chmod) try { fs.chmodSync(dest, chmod); } catch { /* Windows */ }
227
+ updated = true;
239
228
  }
240
229
  } catch { /* non-fatal per file */ }
241
230
  }
@@ -454,7 +443,7 @@ if (syntaxErrors.length > 0) {
454
443
  } else {
455
444
  scriptsUpdated = syncDirFiles(scriptsDir, METAME_DIR, { fileList: BUNDLED_SCRIPTS });
456
445
  if (scriptsUpdated) {
457
- console.log(`${icon("pkg")} Scripts ${IS_DEV_MODE ? 'symlinked' : 'synced'} to ~/.metame/.`);
446
+ console.log(`${icon("pkg")} Scripts synced to ~/.metame/.`);
458
447
  }
459
448
  }
460
449
 
@@ -1400,38 +1389,43 @@ try {
1400
1389
  // 4.9 AUTO-UPDATE CHECK (non-blocking)
1401
1390
  // ---------------------------------------------------------
1402
1391
  const CURRENT_VERSION = pkgVersion;
1392
+ const AUTO_UPDATE = resolveAutoUpdateBehavior();
1403
1393
 
1404
- // Fire-and-forget: check npm for newer version and auto-update
1405
- (async () => {
1406
- try {
1407
- const https = require('https');
1408
- const latest = await new Promise((resolve, reject) => {
1409
- https.get('https://registry.npmjs.org/metame-cli/latest', { timeout: 5000 }, res => {
1410
- let data = '';
1411
- res.on('data', c => data += c);
1412
- res.on('end', () => {
1413
- try { resolve(JSON.parse(data).version); } catch { reject(); }
1414
- });
1415
- }).on('error', reject).on('timeout', function () { this.destroy(); reject(); });
1416
- });
1394
+ // Fire-and-forget: check npm for newer version and auto-update.
1395
+ // Only enabled for published npm installs; source checkouts and npm-link
1396
+ // development setups are opt-out by default to avoid overwriting local work.
1397
+ if (AUTO_UPDATE.enabled) {
1398
+ (async () => {
1399
+ try {
1400
+ const https = require('https');
1401
+ const latest = await new Promise((resolve, reject) => {
1402
+ https.get('https://registry.npmjs.org/metame-cli/latest', { timeout: 5000 }, res => {
1403
+ let data = '';
1404
+ res.on('data', c => data += c);
1405
+ res.on('end', () => {
1406
+ try { resolve(JSON.parse(data).version); } catch { reject(); }
1407
+ });
1408
+ }).on('error', reject).on('timeout', function () { this.destroy(); reject(); });
1409
+ });
1417
1410
 
1418
- if (latest && latest !== CURRENT_VERSION) {
1419
- console.log(`${icon("pkg")} MetaMe ${latest} available (current ${CURRENT_VERSION}), updating...`);
1420
- const { execSync } = require('child_process');
1421
- try {
1422
- execSync('npm install -g metame-cli@latest', {
1423
- stdio: 'pipe',
1424
- timeout: 60000,
1425
- ...(process.platform === 'win32' ? { shell: process.env.COMSPEC || true } : {}),
1426
- });
1427
- console.log(`${icon("ok")} Updated to ${latest}. Restart metame to use the new version.`);
1428
- } catch (e) {
1429
- const msg = e.stderr ? e.stderr.toString().trim().split('\n').pop() : '';
1430
- console.log(`${icon("warn")} Auto-update failed${msg ? ': ' + msg : ''}. Run manually: npm install -g metame-cli`);
1411
+ if (latest && latest !== CURRENT_VERSION) {
1412
+ console.log(`${icon("pkg")} MetaMe ${latest} available (current ${CURRENT_VERSION}), updating...`);
1413
+ const { execSync } = require('child_process');
1414
+ try {
1415
+ execSync('npm install -g metame-cli@latest', {
1416
+ stdio: 'pipe',
1417
+ timeout: 60000,
1418
+ ...(process.platform === 'win32' ? { shell: process.env.COMSPEC || true } : {}),
1419
+ });
1420
+ console.log(`${icon("ok")} Updated to ${latest}. Restart metame to use the new version.`);
1421
+ } catch (e) {
1422
+ const msg = e.stderr ? e.stderr.toString().trim().split('\n').pop() : '';
1423
+ console.log(`${icon("warn")} Auto-update failed${msg ? ': ' + msg : ''}. Run manually: npm install -g metame-cli`);
1424
+ }
1431
1425
  }
1432
- }
1433
- } catch { /* network unavailable, skip silently */ }
1434
- })();
1426
+ } catch { /* network unavailable, skip silently */ }
1427
+ })();
1428
+ }
1435
1429
 
1436
1430
  // ---------------------------------------------------------
1437
1431
  // 4.95 QMD OPTIONAL INSTALL PROMPT (one-time)
@@ -2574,8 +2568,8 @@ if (isSync) {
2574
2568
  if (process.env.METAME_ACTIVE_SESSION === 'true') {
2575
2569
  console.error(`\n${icon("stop")} ACTION BLOCKED: Nested Session Detected`);
2576
2570
  console.error(" You are actively running inside a MetaMe session.");
2577
- console.error(" To hot-reload daemon code from this session, run: \x1b[36mtouch ~/.metame/daemon.js\x1b[0m");
2578
- console.error(" In this dev workspace, \x1b[36mtouch scripts/daemon.js\x1b[0m works too because ~/.metame/daemon.js is symlinked.\n");
2571
+ console.error(" Edit source files under \x1b[36mscripts/\x1b[0m only, then redeploy with \x1b[36mnode index.js\x1b[0m.");
2572
+ console.error(" Do not edit \x1b[36m~/.metame/\x1b[0m directly; it is a generated runtime copy.\n");
2579
2573
  process.exit(1);
2580
2574
  }
2581
2575
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "metame-cli",
3
- "version": "1.5.13",
3
+ "version": "1.5.15",
4
4
  "description": "The Cognitive Profile Layer for Claude Code. Knows how you think, not just what you said.",
5
5
  "main": "index.js",
6
6
  "bin": {
@@ -42,5 +42,8 @@
42
42
  },
43
43
  "engines": {
44
44
  "node": ">=22.5"
45
+ },
46
+ "devDependencies": {
47
+ "qrcode": "^1.5.4"
45
48
  }
46
49
  }
@@ -17,6 +17,10 @@ const {
17
17
  } = require('./daemon-remote-dispatch');
18
18
  let mentorEngine = null;
19
19
  try { mentorEngine = require('./mentor-engine'); } catch { /* optional */ }
20
+ let weixinApiMod = null;
21
+ let weixinAuthMod = null;
22
+ try { weixinApiMod = require('./daemon-weixin-api'); } catch { /* optional */ }
23
+ try { weixinAuthMod = require('./daemon-weixin-auth'); } catch { /* optional */ }
20
24
 
21
25
  function createAdminCommandHandler(deps) {
22
26
  const {
@@ -45,6 +49,7 @@ function createAdminCommandHandler(deps) {
45
49
  getDefaultEngine = () => 'claude',
46
50
  setDefaultEngine = () => {},
47
51
  getDistillModel = () => 'haiku',
52
+ weixinAuthStore = null,
48
53
  } = deps;
49
54
 
50
55
  // resolveProjectKey: imported from daemon-team-dispatch.js (shared with dispatch_to and daemon.js)
@@ -262,6 +267,48 @@ function createAdminCommandHandler(deps) {
262
267
  }
263
268
  }
264
269
 
270
+ function getWeixinStore() {
271
+ if (weixinAuthStore) return weixinAuthStore;
272
+ if (!weixinApiMod || !weixinAuthMod) return null;
273
+ try {
274
+ const apiClient = weixinApiMod.createWeixinApiClient({ log });
275
+ return weixinAuthMod.createWeixinAuthStore({ apiClient, log });
276
+ } catch {
277
+ return null;
278
+ }
279
+ }
280
+
281
+ function parseWeixinCommand(raw) {
282
+ const src = String(raw || '').trim();
283
+ const tail = src.replace(/^\/weixin\b/i, '').trim();
284
+ if (!tail) return { action: 'status' };
285
+ const parts = tail.split(/\s+/).filter(Boolean);
286
+ const main = String(parts[0] || '').toLowerCase();
287
+ const sub = String(parts[1] || '').toLowerCase();
288
+ const rest = parts.slice(2);
289
+ const flags = {};
290
+ for (let i = 0; i < rest.length; i += 1) {
291
+ const token = rest[i];
292
+ if (!token.startsWith('--')) continue;
293
+ const key = token.slice(2);
294
+ const next = rest[i + 1];
295
+ if (!next || next.startsWith('--')) {
296
+ flags[key] = true;
297
+ } else {
298
+ flags[key] = next;
299
+ i += 1;
300
+ }
301
+ }
302
+ if (main === 'status') return { action: 'status' };
303
+ if (main === 'login' && (sub === 'start' || sub === 'wait')) {
304
+ return { action: `login:${sub}`, flags };
305
+ }
306
+ if (main === 'login' && !sub) {
307
+ return { action: 'usage' };
308
+ }
309
+ return { action: 'usage' };
310
+ }
311
+
265
312
  async function sendLocalDispatchReceipt(bot, chatId, targetKey, projInfo, result, preview) {
266
313
  if (!result || !result.success) return;
267
314
  const icon = projInfo && projInfo.icon ? projInfo.icon : '🤖';
@@ -345,6 +392,108 @@ function createAdminCommandHandler(deps) {
345
392
  return { handled: true, config };
346
393
  }
347
394
 
395
+ if (text === '/weixin' || text.startsWith('/weixin ')) {
396
+ const store = getWeixinStore();
397
+ if (!store) {
398
+ await bot.sendMessage(chatId, '❌ weixin 模块不可用');
399
+ return { handled: true, config };
400
+ }
401
+
402
+ const parsed = parseWeixinCommand(text);
403
+ const weixinCfg = (config && config.weixin) || {};
404
+
405
+ if (parsed.action === 'usage') {
406
+ await bot.sendMessage(chatId, [
407
+ '用法:',
408
+ '/weixin',
409
+ '/weixin status',
410
+ '/weixin login start [--bot-type 3] [--session <key>]',
411
+ '/weixin login wait --session <key>',
412
+ ].join('\n'));
413
+ return { handled: true, config };
414
+ }
415
+
416
+ if (parsed.action === 'status') {
417
+ const accountIds = store.listAccounts();
418
+ const activeAccountId = String(weixinCfg.account_id || accountIds[0] || '').trim();
419
+ const lines = [
420
+ '💬 Weixin',
421
+ `enabled: ${weixinCfg.enabled ? 'yes' : 'no'}`,
422
+ `base_url: ${weixinCfg.base_url || (weixinApiMod && weixinApiMod.DEFAULT_BASE_URL) || 'https://ilinkai.weixin.qq.com'}`,
423
+ `bot_type: ${weixinCfg.bot_type || '3'}`,
424
+ `linked_accounts: ${accountIds.length}`,
425
+ `active_account: ${activeAccountId || '(none)'}`,
426
+ ];
427
+ if (accountIds.length > 0) {
428
+ for (const id of accountIds.slice(0, 5)) {
429
+ const account = store.loadAccount(id) || {};
430
+ const label = id === activeAccountId ? ' (active)' : '';
431
+ lines.push(`- ${id}${label} user=${account.userId || '-'} linked=${account.linkedAt || account.savedAt || '-'}`);
432
+ }
433
+ } else {
434
+ lines.push('- no linked account');
435
+ }
436
+ await bot.sendMessage(chatId, lines.join('\n'));
437
+ return { handled: true, config };
438
+ }
439
+
440
+ if (parsed.action === 'login:start') {
441
+ const flags = parsed.flags || {};
442
+ const botType = String(flags['bot-type'] || weixinCfg.bot_type || '3').trim();
443
+ const sessionKey = String(flags.session || `${Date.now()}-${botType}`).trim();
444
+ try {
445
+ const session = await store.startQrLogin({
446
+ sessionKey,
447
+ botType,
448
+ baseUrl: weixinCfg.base_url || undefined,
449
+ routeTag: weixinCfg.route_tag || undefined,
450
+ });
451
+ const lines = [
452
+ '✅ 微信登录二维码已生成',
453
+ `session: ${session.sessionKey}`,
454
+ `bot_type: ${session.botType}`,
455
+ '',
456
+ `${session.qrcodeUrl || '(no qrcode url returned)'}`,
457
+ '',
458
+ `下一步: /weixin login wait --session ${session.sessionKey}`,
459
+ ];
460
+ await bot.sendMessage(chatId, lines.join('\n'));
461
+ } catch (e) {
462
+ await bot.sendMessage(chatId, `❌ 微信登录启动失败: ${e.message}`);
463
+ }
464
+ return { handled: true, config };
465
+ }
466
+
467
+ if (parsed.action === 'login:wait') {
468
+ const flags = parsed.flags || {};
469
+ const sessionKey = String(flags.session || '').trim();
470
+ if (!sessionKey) {
471
+ await bot.sendMessage(chatId, '❌ 缺少 session\n用法: /weixin login wait --session <key>');
472
+ return { handled: true, config };
473
+ }
474
+ try {
475
+ const result = await store.waitForQrLogin({ sessionKey });
476
+ if (result.connected) {
477
+ await bot.sendMessage(chatId, [
478
+ '✅ 微信账号已绑定',
479
+ `account: ${result.account.accountId}`,
480
+ `user: ${result.account.userId || '-'}`,
481
+ `base_url: ${result.account.baseUrl || '-'}`,
482
+ ].join('\n'));
483
+ } else if (result.expired) {
484
+ await bot.sendMessage(chatId, '⚠️ 二维码已过期,请重新执行 /weixin login start');
485
+ } else if (result.timeout) {
486
+ await bot.sendMessage(chatId, '⏳ 仍在等待扫码确认,可稍后再次执行 /weixin login wait --session <key>');
487
+ } else {
488
+ await bot.sendMessage(chatId, '⚠️ 登录未完成');
489
+ }
490
+ } catch (e) {
491
+ await bot.sendMessage(chatId, `❌ 微信登录等待失败: ${e.message}`);
492
+ }
493
+ return { handled: true, config };
494
+ }
495
+ }
496
+
348
497
  // /skill-evo — inspect and resolve skill evolution queue
349
498
  if (text === '/skill-evo' || text.startsWith('/skill-evo ')) {
350
499
  if (!skillEvolution) {
@@ -6,6 +6,7 @@ const { findTeamMember: _findTeamMember } = require('./daemon-team-dispatch');
6
6
  const { isRemoteMember } = require('./daemon-remote-dispatch');
7
7
  const imessageIO = (() => { try { return require('./daemon-siri-imessage'); } catch { return null; } })();
8
8
  const siriBridgeMod = (() => { try { return require('./daemon-siri-bridge'); } catch { return null; } })();
9
+ const weixinBridgeMod = (() => { try { return require('./daemon-weixin-bridge'); } catch { return null; } })();
9
10
 
10
11
  function createBridgeStarter(deps) {
11
12
  const {
@@ -159,6 +160,7 @@ function createBridgeStarter(deps) {
159
160
  const map = {
160
161
  ...(cfg.telegram ? cfg.telegram.chat_agent_map || {} : {}),
161
162
  ...(cfg.feishu ? cfg.feishu.chat_agent_map || {} : {}),
163
+ ...(cfg.weixin ? cfg.weixin.chat_agent_map || {} : {}),
162
164
  ...(cfg.imessage ? cfg.imessage.chat_agent_map || {} : {}),
163
165
  };
164
166
  const key = map[String(chatId)];
@@ -1177,7 +1179,19 @@ function createBridgeStarter(deps) {
1177
1179
  return bridge.startSiriBridge(config, executeTaskByName);
1178
1180
  }
1179
1181
 
1180
- return { startTelegramBridge, startFeishuBridge, startImessageBridge, startSiriBridge };
1182
+ function startWeixinBridge(config, executeTaskByName) {
1183
+ if (!weixinBridgeMod) { log('WARN', '[WEIXIN] daemon-weixin-bridge module not found'); return null; }
1184
+ const bridge = weixinBridgeMod.createWeixinBridge({
1185
+ HOME,
1186
+ log,
1187
+ sleep,
1188
+ loadConfig,
1189
+ pipeline,
1190
+ });
1191
+ return bridge.startWeixinBridge(config, executeTaskByName);
1192
+ }
1193
+
1194
+ return { startTelegramBridge, startFeishuBridge, startWeixinBridge, startImessageBridge, startSiriBridge };
1181
1195
  }
1182
1196
 
1183
1197
  module.exports = { createBridgeStarter };
@@ -19,6 +19,16 @@ feishu:
19
19
  chat_id: ""
20
20
  secret: ""
21
21
 
22
+ weixin:
23
+ enabled: false
24
+ base_url: "https://ilinkai.weixin.qq.com"
25
+ bot_type: "3"
26
+ account_id: ""
27
+ route_tag: null
28
+ allowed_chat_ids: []
29
+ chat_agent_map: {}
30
+ poll_timeout_ms: 35000
31
+
22
32
  projects:
23
33
  # Per-project heartbeat tasks. Each project's tasks are isolated and
24
34
  # notifications arrive as colored Feishu cards (visually distinct).
@@ -44,6 +44,7 @@ function mergeAgentMaps(cfg) {
44
44
  return {
45
45
  ...(cfg.telegram ? cfg.telegram.chat_agent_map : {}),
46
46
  ...(cfg.feishu ? cfg.feishu.chat_agent_map : {}),
47
+ ...(cfg.weixin ? cfg.weixin.chat_agent_map : {}),
47
48
  ...(cfg.imessage ? cfg.imessage.chat_agent_map : {}),
48
49
  ...(cfg.siri_bridge ? cfg.siri_bridge.chat_agent_map : {}),
49
50
  };