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.
@@ -0,0 +1,308 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const os = require('os');
5
+ const path = require('path');
6
+
7
+ const { createWeixinApiClient, DEFAULT_BASE_URL, DEFAULT_LONG_POLL_TIMEOUT_MS } = require('./daemon-weixin-api');
8
+ const { createWeixinAuthStore } = require('./daemon-weixin-auth');
9
+
10
+ function extractInboundText(itemList) {
11
+ if (!Array.isArray(itemList)) return '';
12
+ for (const item of itemList) {
13
+ if (item && item.type === 1 && item.text_item && item.text_item.text != null) {
14
+ return String(item.text_item.text).trim();
15
+ }
16
+ if (item && item.type === 3 && item.voice_item && item.voice_item.text) {
17
+ return String(item.voice_item.text).trim();
18
+ }
19
+ }
20
+ return '';
21
+ }
22
+
23
+ function createContextTokenStore() {
24
+ const store = new Map();
25
+ return {
26
+ set(accountId, userId, token) {
27
+ const a = String(accountId || '').trim();
28
+ const u = String(userId || '').trim();
29
+ const t = String(token || '').trim();
30
+ if (!a || !u || !t) return;
31
+ store.set(`${a}:${u}`, t);
32
+ },
33
+ get(accountId, userId) {
34
+ return store.get(`${String(accountId || '').trim()}:${String(userId || '').trim()}`) || null;
35
+ },
36
+ clear(accountId, userId) {
37
+ store.delete(`${String(accountId || '').trim()}:${String(userId || '').trim()}`);
38
+ },
39
+ };
40
+ }
41
+
42
+ function createPersistentContextTokenStore(deps = {}) {
43
+ const mem = deps.tokenStore || createContextTokenStore();
44
+ const HOME = deps.HOME || os.homedir();
45
+ const fsMod = deps.fs || fs;
46
+ const pathMod = deps.path || path;
47
+ const filePath = deps.filePath || pathMod.join(HOME, '.metame', 'weixin', 'context-tokens.json');
48
+
49
+ function loadAll() {
50
+ try {
51
+ if (!fsMod.existsSync(filePath)) return {};
52
+ const raw = JSON.parse(fsMod.readFileSync(filePath, 'utf8'));
53
+ return raw && typeof raw === 'object' ? raw : {};
54
+ } catch {
55
+ return {};
56
+ }
57
+ }
58
+
59
+ function saveAll(data) {
60
+ try {
61
+ fsMod.mkdirSync(pathMod.dirname(filePath), { recursive: true });
62
+ fsMod.writeFileSync(filePath, JSON.stringify(data, null, 2), 'utf8');
63
+ try {
64
+ fsMod.chmodSync(filePath, 0o600);
65
+ } catch {}
66
+ } catch {}
67
+ }
68
+
69
+ return {
70
+ set(accountId, userId, token) {
71
+ mem.set(accountId, userId, token);
72
+ const key = `${String(accountId || '').trim()}:${String(userId || '').trim()}`;
73
+ const all = loadAll();
74
+ all[key] = {
75
+ token: String(token || '').trim(),
76
+ savedAt: new Date().toISOString(),
77
+ };
78
+ saveAll(all);
79
+ },
80
+ get(accountId, userId) {
81
+ const hit = mem.get(accountId, userId);
82
+ if (hit) return hit;
83
+ const key = `${String(accountId || '').trim()}:${String(userId || '').trim()}`;
84
+ const all = loadAll();
85
+ const token = all[key] && all[key].token ? String(all[key].token).trim() : '';
86
+ if (token) mem.set(accountId, userId, token);
87
+ return token || null;
88
+ },
89
+ clear(accountId, userId) {
90
+ mem.clear(accountId, userId);
91
+ const key = `${String(accountId || '').trim()}:${String(userId || '').trim()}`;
92
+ const all = loadAll();
93
+ if (Object.prototype.hasOwnProperty.call(all, key)) {
94
+ delete all[key];
95
+ saveAll(all);
96
+ }
97
+ },
98
+ };
99
+ }
100
+
101
+ function createWeixinBridge(deps = {}) {
102
+ const log = typeof deps.log === 'function' ? deps.log : () => {};
103
+ const loadConfig = deps.loadConfig;
104
+ const pipeline = deps.pipeline;
105
+ const sleep = typeof deps.sleep === 'function' ? deps.sleep : (ms) => new Promise(resolve => setTimeout(resolve, ms));
106
+ const apiClient = deps.apiClient || createWeixinApiClient({ log });
107
+ const authStore = deps.authStore || createWeixinAuthStore({
108
+ apiClient,
109
+ HOME: deps.HOME,
110
+ log,
111
+ sleep,
112
+ });
113
+ const tokenStore = deps.tokenStore || createPersistentContextTokenStore({ HOME: deps.HOME });
114
+
115
+ if (typeof loadConfig !== 'function') throw new Error('loadConfig is required');
116
+ if (!pipeline || typeof pipeline.processMessage !== 'function') throw new Error('pipeline.processMessage is required');
117
+
118
+ function resolveBridgeConfig(config) {
119
+ const cfg = (config && config.weixin) || {};
120
+ return {
121
+ enabled: !!cfg.enabled,
122
+ baseUrl: String(cfg.base_url || DEFAULT_BASE_URL).trim(),
123
+ botType: String(cfg.bot_type || '3').trim(),
124
+ routeTag: cfg.route_tag === undefined || cfg.route_tag === null ? null : String(cfg.route_tag).trim(),
125
+ pollTimeoutMs: Number(cfg.poll_timeout_ms || DEFAULT_LONG_POLL_TIMEOUT_MS),
126
+ allowedChatIds: Array.isArray(cfg.allowed_chat_ids) ? cfg.allowed_chat_ids.map(String) : [],
127
+ accountId: String(cfg.account_id || '').trim(),
128
+ };
129
+ }
130
+
131
+ function resolveActiveAccount(config) {
132
+ const cfg = resolveBridgeConfig(config);
133
+ const accountId = cfg.accountId || authStore.listAccounts()[0] || '';
134
+ if (!accountId) return null;
135
+ const account = authStore.loadAccount(accountId);
136
+ if (!account || !account.token) return null;
137
+ return {
138
+ accountId,
139
+ token: account.token,
140
+ baseUrl: String(account.baseUrl || cfg.baseUrl || DEFAULT_BASE_URL).trim(),
141
+ routeTag: account.routeTag || cfg.routeTag || null,
142
+ };
143
+ }
144
+
145
+ function createWeixinBot(params) {
146
+ const accountId = params.accountId;
147
+ const baseUrl = params.baseUrl;
148
+ const token = params.token;
149
+ const routeTag = params.routeTag;
150
+
151
+ async function sendPlain(chatId, text) {
152
+ const contextToken = tokenStore.get(accountId, chatId);
153
+ if (!contextToken) throw new Error(`weixin context token missing for ${chatId}`);
154
+ log('DEBUG', `[WEIXIN] send chatId=${chatId} text_len=${String(text || '').length} ctx=${contextToken.slice(0, 8)}`);
155
+ const result = await apiClient.sendTextMessage({
156
+ baseUrl,
157
+ token,
158
+ routeTag,
159
+ toUserId: chatId,
160
+ contextToken,
161
+ text,
162
+ });
163
+ log('DEBUG', `[WEIXIN] send ok chatId=${chatId} ret=${result && result.ret} payload=${JSON.stringify(result || {})}`);
164
+ return { message_id: Date.now() };
165
+ }
166
+
167
+ return {
168
+ suppressAck: true,
169
+ sendMessage: async (_chatId, text) => sendPlain(_chatId, String(text || '').trim()),
170
+ deleteMessage: async () => false,
171
+ sendTyping: async () => {},
172
+ };
173
+ }
174
+
175
+ async function startWeixinBridge(config, executeTaskByName) {
176
+ const cfg = resolveBridgeConfig(config);
177
+ if (!cfg.enabled) return null;
178
+ let running = true;
179
+ let processing = false;
180
+ let timer = null;
181
+ let getUpdatesBuf = '';
182
+ let currentAccount = null;
183
+ let currentBot = null;
184
+ let missingAccountLogged = false;
185
+
186
+ function sameAccount(a, b) {
187
+ if (!a || !b) return false;
188
+ return a.accountId === b.accountId
189
+ && a.token === b.token
190
+ && a.baseUrl === b.baseUrl
191
+ && a.routeTag === b.routeTag;
192
+ }
193
+
194
+ function setActiveAccount(account) {
195
+ if (sameAccount(currentAccount, account) && currentBot) return;
196
+ currentAccount = account;
197
+ currentBot = account ? createWeixinBot(account) : null;
198
+ getUpdatesBuf = '';
199
+ if (account) {
200
+ log('INFO', `[WEIXIN] bridge active account=${account.accountId}`);
201
+ }
202
+ }
203
+
204
+ function scheduleNext(delayMs) {
205
+ if (!running) return;
206
+ if (timer) clearTimeout(timer);
207
+ timer = setTimeout(() => { pump().catch(() => {}); }, delayMs);
208
+ }
209
+
210
+ async function pump() {
211
+ if (!running || processing) return;
212
+ processing = true;
213
+ let nextDelayMs = 250;
214
+ try {
215
+ const liveCfg = loadConfig();
216
+ const liveBridgeCfg = resolveBridgeConfig(liveCfg);
217
+ if (!liveBridgeCfg.enabled) {
218
+ setActiveAccount(null);
219
+ nextDelayMs = 1000;
220
+ return;
221
+ }
222
+ const liveAccount = resolveActiveAccount(liveCfg);
223
+ if (!liveAccount) {
224
+ if (!missingAccountLogged) {
225
+ missingAccountLogged = true;
226
+ log('WARN', '[WEIXIN] bridge enabled but no linked account found; waiting for account link');
227
+ }
228
+ setActiveAccount(null);
229
+ nextDelayMs = 500;
230
+ return;
231
+ }
232
+ missingAccountLogged = false;
233
+ setActiveAccount(liveAccount);
234
+ const resp = await apiClient.getUpdates({
235
+ baseUrl: currentAccount.baseUrl,
236
+ token: currentAccount.token,
237
+ routeTag: currentAccount.routeTag,
238
+ getUpdatesBuf,
239
+ timeoutMs: liveBridgeCfg.pollTimeoutMs,
240
+ });
241
+ if (resp && typeof resp.get_updates_buf === 'string' && resp.get_updates_buf) {
242
+ getUpdatesBuf = resp.get_updates_buf;
243
+ }
244
+ const messages = Array.isArray(resp && resp.msgs) ? resp.msgs : [];
245
+ for (const full of messages) {
246
+ const chatId = String(full && full.from_user_id || '').trim();
247
+ if (!chatId) continue;
248
+ if (liveBridgeCfg.allowedChatIds.length && !liveBridgeCfg.allowedChatIds.includes(chatId)) {
249
+ continue;
250
+ }
251
+ const text = extractInboundText(full.item_list);
252
+ if (!text) continue;
253
+ if (full && full.context_token) {
254
+ tokenStore.set(currentAccount.accountId, chatId, full.context_token);
255
+ }
256
+ log('DEBUG', `[WEIXIN] inbound chatId=${chatId} text_len=${text.length} has_ctx=${!!(full && full.context_token)} buf=${String(getUpdatesBuf || '').slice(0, 16)}`);
257
+ await pipeline.processMessage(chatId, text, {
258
+ bot: currentBot,
259
+ config: liveCfg,
260
+ executeTaskByName,
261
+ senderId: chatId,
262
+ readOnly: false,
263
+ });
264
+ }
265
+ } catch (err) {
266
+ log('WARN', `[WEIXIN] poll error: ${err.message}`);
267
+ nextDelayMs = 1000;
268
+ } finally {
269
+ processing = false;
270
+ scheduleNext(nextDelayMs);
271
+ }
272
+ }
273
+
274
+ scheduleNext(0);
275
+ return {
276
+ stop() {
277
+ running = false;
278
+ if (timer) clearTimeout(timer);
279
+ },
280
+ reconnect() {
281
+ getUpdatesBuf = '';
282
+ scheduleNext(0);
283
+ },
284
+ get bot() {
285
+ return currentBot;
286
+ },
287
+ get accountId() {
288
+ return currentAccount ? currentAccount.accountId : null;
289
+ },
290
+ };
291
+ }
292
+
293
+ return {
294
+ startWeixinBridge,
295
+ createWeixinBot,
296
+ createContextTokenStore,
297
+ createPersistentContextTokenStore,
298
+ resolveBridgeConfig,
299
+ resolveActiveAccount,
300
+ };
301
+ }
302
+
303
+ module.exports = {
304
+ createWeixinBridge,
305
+ createContextTokenStore,
306
+ createPersistentContextTokenStore,
307
+ extractInboundText,
308
+ };
package/scripts/daemon.js CHANGED
@@ -293,7 +293,7 @@ function restoreConfig() {
293
293
  try { curCfg = yaml.load(fs.readFileSync(CONFIG_FILE, 'utf8')) || {}; } catch { }
294
294
  // Secret fields that must NEVER be reverted by a restore
295
295
  const SECRET_FIELDS = ['app_id', 'app_secret', 'bot_token', 'operator_ids'];
296
- for (const adapter of ['feishu', 'telegram']) {
296
+ for (const adapter of ['feishu', 'telegram', 'weixin']) {
297
297
  if (curCfg[adapter] && bakCfg[adapter]) {
298
298
  // Preserve secrets: current config always wins
299
299
  for (const field of SECRET_FIELDS) {
@@ -2456,7 +2456,7 @@ _pipelineRef.current = pipeline;
2456
2456
  // ---------------------------------------------------------
2457
2457
  // BOT BRIDGES
2458
2458
  // ---------------------------------------------------------
2459
- const { startTelegramBridge, startFeishuBridge, startImessageBridge, startSiriBridge } = createBridgeStarter({
2459
+ const { startTelegramBridge, startFeishuBridge, startWeixinBridge, startImessageBridge, startSiriBridge } = createBridgeStarter({
2460
2460
  fs,
2461
2461
  path,
2462
2462
  HOME,
@@ -2593,7 +2593,7 @@ async function main() {
2593
2593
  }
2594
2594
 
2595
2595
  // Config validation: warn on unknown/suspect fields
2596
- const KNOWN_SECTIONS = ['daemon', 'telegram', 'feishu', 'heartbeat', 'budget', 'projects', 'imessage', 'siri_bridge'];
2596
+ const KNOWN_SECTIONS = ['daemon', 'telegram', 'feishu', 'weixin', 'heartbeat', 'budget', 'projects', 'imessage', 'siri_bridge'];
2597
2597
  const KNOWN_DAEMON = [
2598
2598
  'model', // legacy (still valid as fallback)
2599
2599
  'models', // per-engine model map: { claude, codex }
@@ -2684,12 +2684,13 @@ async function main() {
2684
2684
  // Bridges
2685
2685
  let telegramBridge = null;
2686
2686
  let feishuBridge = null;
2687
+ let weixinBridge = null;
2687
2688
  let lastWakeBridgeRecoveryAt = 0;
2688
2689
 
2689
2690
  const notifier = createNotifier({
2690
2691
  log,
2691
2692
  getConfig: () => config,
2692
- getBridges: () => ({ telegramBridge, feishuBridge }),
2693
+ getBridges: () => ({ telegramBridge, feishuBridge, weixinBridge }),
2693
2694
  });
2694
2695
  const notifyFn = notifier.notify;
2695
2696
  const adminNotifyFn = notifier.notifyAdmin;
@@ -2714,6 +2715,10 @@ async function main() {
2714
2715
  log('INFO', `[WAKE-DETECT] reconnecting Feishu bridge after ${sleepSeconds}s sleep`);
2715
2716
  tasks.push(Promise.resolve().then(() => feishuBridge.reconnect()));
2716
2717
  }
2718
+ if (weixinBridge && typeof weixinBridge.reconnect === 'function') {
2719
+ log('INFO', `[WAKE-DETECT] reconnecting Weixin bridge after ${sleepSeconds}s sleep`);
2720
+ tasks.push(Promise.resolve().then(() => weixinBridge.reconnect()));
2721
+ }
2717
2722
  await Promise.allSettled(tasks);
2718
2723
  };
2719
2724
 
@@ -2781,6 +2786,7 @@ async function main() {
2781
2786
  // Start bridges (both can run simultaneously)
2782
2787
  telegramBridge = await startTelegramBridge(config, executeTaskByName);
2783
2788
  feishuBridge = await startFeishuBridge(config, executeTaskByName);
2789
+ weixinBridge = await startWeixinBridge(config, executeTaskByName);
2784
2790
  await startImessageBridge(config, executeTaskByName);
2785
2791
  await startSiriBridge(config, executeTaskByName);
2786
2792
  if (feishuBridge) _dispatchBridgeRef = feishuBridge; // store bridge, not bot, so .bot stays live after reconnects
@@ -2795,6 +2801,7 @@ async function main() {
2795
2801
  const bots = [];
2796
2802
  if (feishuBridge && feishuBridge.bot) bots.push(feishuBridge.bot);
2797
2803
  if (telegramBridge && telegramBridge.bot) bots.push(telegramBridge.bot);
2804
+ if (weixinBridge && weixinBridge.bot) bots.push(weixinBridge.bot);
2798
2805
  if (bots.length === 0) return;
2799
2806
  const notifs = [];
2800
2807
  for (const [cid] of activeProcesses) {
@@ -2825,6 +2832,7 @@ async function main() {
2825
2832
  try { fs.unlinkSync(SOCK_PATH); } catch { }
2826
2833
  if (telegramBridge) telegramBridge.stop();
2827
2834
  if (feishuBridge) feishuBridge.stop();
2835
+ if (weixinBridge) weixinBridge.stop();
2828
2836
  // Stop QMD semantic search daemon if it was started
2829
2837
  try { require('./qmd-client').stopDaemon(); } catch { /* ignore */ }
2830
2838
  // Release warm pool processes before killing active ones
@@ -1,6 +1,6 @@
1
1
  # MetaMe Hook / Intent Engine 配置手册
2
2
 
3
- > 自动部署到 `~/.metame/docs/hook-config.md`。源文件:`scripts/docs/hook-config.md`。
3
+ > 自动部署到 `~/.metame/docs/hook-config.md`。源文件:`scripts/docs/hook-config.md`。只编辑 `scripts/`,不要直接改 `~/.metame/`。
4
4
 
5
5
  ---
6
6
 
@@ -29,6 +29,7 @@ Stop (每轮结束)
29
29
  | `ops_assist` | `intent-ops-assist.js` | 回退/日志/重启/gc/状态 相关语境 | `/undo` `/restart` `/logs` `/gc` `/status` 命令提示 |
30
30
  | `task_create` | `intent-task-create.js` | 定时/提醒/每天X点 等调度语境 | `/task-add` 命令用法提示 |
31
31
  | `file_transfer` | `intent-file-transfer.js` | "发给我/发过来/导出" 等文件传输语境 | `[[FILE:...]]` 协议 + 收发规则 |
32
+ | `weixin_bridge` | `intent-weixin-bridge.js` | "帮我绑定微信/配置微信桥接/开启微信接入/开始微信扫码登录" 等明确桥接语境 | 开启 `weixin.enabled` + `/weixin` 绑定流程提示 |
32
33
  | `memory_recall` | `intent-memory-recall.js` | "上次/之前/还记得" 等跨会话回忆语境 | `memory-search.js` 命令用法 |
33
34
  | `doc_router` | `intent-doc-router.js` | "创建/绑定 Agent"、"代码结构/脚本入口"、"hook/intent 配置" 等文档导向语境 | 统一 doc-router 文档指引 |
34
35
  ---
@@ -42,6 +43,7 @@ hooks:
42
43
  team_dispatch: true # 改为 false 可禁用
43
44
  ops_assist: true
44
45
  task_create: false # 禁用任务调度提示
46
+ weixin_bridge: false # 默认关闭;命中明确微信配置语境时再按需开启
45
47
  ```
46
48
 
47
49
  改完立即生效(intent-engine 每次运行时读取)。
@@ -128,7 +130,7 @@ for k, v in s.get('hooks', {}).items():
128
130
  |------|------|
129
131
  | `scripts/intent-registry.js` | 共享意图注册表(Claude hook / Codex runtime 共用) |
130
132
  | `scripts/hooks/intent-engine.js` | Claude hook adapter(源文件) |
131
- | `~/.metame/hooks/intent-engine.js` | 部署副本(symlink) |
133
+ | `~/.metame/hooks/intent-engine.js` | 部署副本(copy) |
132
134
  | `scripts/hooks/intent-*.js` | 各意图模块(源文件) |
133
135
  | `~/.metame/daemon.yaml` | 用户配置(包含 `hooks:` 开关) |
134
136
  | `~/.claude/settings.json` | Claude Code hook 注册表 |
@@ -83,6 +83,8 @@ feishu:
83
83
 
84
84
  ## 6. 运行时文件与状态
85
85
 
86
+ - 源码目录:`scripts/`
87
+ - 运行副本目录:`~/.metame/`(由 `node index.js` 部署生成,只读看状态,不直接改)
86
88
  - 配置:`~/.metame/daemon.yaml`
87
89
  - daemon 状态:`~/.metame/daemon_state.json`
88
90
  - 活跃子进程:`~/.metame/active_agent_pids.json`
@@ -91,10 +93,11 @@ feishu:
91
93
  - Dispatch 队列:`~/.metame/dispatch/pending.jsonl`(本地 socket 降级)
92
94
  - 远端 Dispatch 队列:`~/.metame/dispatch/remote-pending.jsonl`(跨设备中继)
93
95
  - Dispatch 签名密钥:`~/.metame/.dispatch_secret`(自动创建)
96
+ - 自动更新策略:发布版 npm 安装默认开启;源码 checkout / `npm link` 默认关闭,可用 `METAME_AUTO_UPDATE=on|off` 覆盖
94
97
 
95
98
  ## 7. 热重载安全机制(三层防护)
96
99
 
97
- 1. **部署前预检**(`index.js`):`node -c` 语法检查所有 `.js`,不通过则拒绝部署到 `~/.metame/`
100
+ 1. **部署前预检**(`index.js`):`node -c` 语法检查所有 `.js`,不通过则拒绝以 copy 模式部署到 `~/.metame/`
98
101
  2. **重启前预检**(`daemon-runtime-lifecycle.js`):daemon.js 变更触发重启前再次语法校验,不通过则阻止重启并通知 admin
99
102
  3. **崩溃循环自愈**:连续 2 次在 30s 内崩溃 → 自动从 `.last-good/` 恢复 → 通知 admin
100
103
 
@@ -195,5 +195,8 @@
195
195
 
196
196
  ## 同步提示
197
197
 
198
- - 每次改 `scripts/` 后执行:`npm run sync:plugin`
198
+ - `scripts/` 是唯一源码目录;`~/.metame/` 是运行副本,不直接编辑
199
+ - 每次改 `scripts/` 后先执行:`node index.js`,把最新运行文件 copy 到 `~/.metame/`
200
+ - 只有在需要刷新分发镜像时才执行:`npm run sync:plugin`
199
201
  - plugin 镜像路径:`plugin/scripts/*`
202
+ - 本地源码 checkout / `npm link` 默认关闭 auto-update;发布版 npm 安装默认开启,可用 `METAME_AUTO_UPDATE=on|off` 覆盖
@@ -0,0 +1,40 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Weixin Bridge Intent Module
5
+ *
6
+ * 按需暴露 MetaMe 的微信桥接配置能力,只在用户明确谈到微信接入、
7
+ * 开启桥接、扫码登录、绑定配置或微信 direct bridge 时注入。
8
+ *
9
+ * @param {string} prompt
10
+ * @returns {string|null}
11
+ */
12
+
13
+ const POSITIVE_PATTERNS = [
14
+ /(?:帮我|请|想|要|需要|如何|怎么).{0,10}(?:配置|绑定|接入|接上|开通|启用).{0,10}(?:微信|wechat)(?:.{0,10}(?:bridge|桥接|bot|通道))?/i,
15
+ /(?:微信|wechat).{0,12}(?:配置|绑定|接入|接上|开通|启用|扫码|二维码|登录|授权|bridge|桥接|bot|通道)/i,
16
+ /(?:开始|发起).{0,8}(?:微信|wechat).{0,10}(?:扫码|二维码|登录|绑定|授权)/i,
17
+ /(?:用|通过).{0,6}(?:微信|wechat).{0,10}(?:聊|对话|发消息|收消息|回复)/i,
18
+ ];
19
+ const NEGATIVE_PATTERNS = [
20
+ /企业微信|wecom/i,
21
+ /微信群|群聊|group/i,
22
+ ];
23
+
24
+ module.exports = function detectWeixinBridge(prompt) {
25
+ const text = String(prompt || '').trim();
26
+ if (!text || text.length < 4) return null;
27
+ if (NEGATIVE_PATTERNS.some(re => re.test(text))) return null;
28
+ if (!POSITIVE_PATTERNS.some(re => re.test(text))) return null;
29
+
30
+ return [
31
+ '[微信桥接提示]',
32
+ '- 当前已接入 MetaMe 的是 **微信 direct bridge**,不是企业微信。',
33
+ '- 如果用户是在让你代为配置,先确保 `~/.metame/daemon.yaml` 里的 `weixin.enabled=true`,`weixin.bot_type=\"3\"`,其余可先保持默认。',
34
+ '- 开启配置后,执行 `/weixin login start [--bot-type 3] [--session <key>]` 生成二维码/登录链接。',
35
+ '- 用户扫码确认后,再执行 `/weixin login wait --session <key>` 等待绑定完成。',
36
+ '- 查看状态:`/weixin` 或 `/weixin status`。',
37
+ '- 当前最稳的是 **text-only**:收到微信消息后回复文本;媒体、文件、富交互还不是第一优先级。',
38
+ '- 微信出站依赖最近一条入站消息携带的 `context_token`,所以更适合“用户来一句,系统回一句”的对话模式。',
39
+ ].join('\n');
40
+ };
@@ -12,6 +12,7 @@ const DEFAULTS = Object.freeze({
12
12
  ops_assist: true,
13
13
  task_create: true,
14
14
  file_transfer: true,
15
+ weixin_bridge: false,
15
16
  memory_recall: true,
16
17
  doc_router: true,
17
18
  perpetual: true,
@@ -23,6 +24,7 @@ const INTENT_MODULES = Object.freeze({
23
24
  ops_assist: require('./hooks/intent-ops-assist'),
24
25
  task_create: require('./hooks/intent-task-create'),
25
26
  file_transfer: require('./hooks/intent-file-transfer'),
27
+ weixin_bridge: require('./hooks/intent-weixin-bridge'),
26
28
  memory_recall: require('./hooks/intent-memory-recall'),
27
29
  doc_router: require('./hooks/intent-doc-router'),
28
30
  perpetual: require('./hooks/intent-perpetual'),