metame-cli 1.4.15 → 1.4.18

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.
@@ -53,6 +53,82 @@ function createCommandRouter(deps) {
53
53
  return true;
54
54
  }
55
55
 
56
+ const pendingMacConfirmations = new Map();
57
+ const MAC_CONFIRM_TTL_MS = 2 * 60 * 1000;
58
+
59
+ function setPendingMacConfirmation(chatId, payload) {
60
+ pendingMacConfirmations.set(String(chatId), { ...payload, createdAt: Date.now() });
61
+ }
62
+
63
+ function getPendingMacConfirmation(chatId) {
64
+ const key = String(chatId);
65
+ const pending = pendingMacConfirmations.get(key);
66
+ if (!pending) return null;
67
+ if ((Date.now() - Number(pending.createdAt || 0)) > MAC_CONFIRM_TTL_MS) {
68
+ pendingMacConfirmations.delete(key);
69
+ return null;
70
+ }
71
+ return pending;
72
+ }
73
+
74
+ function clearPendingMacConfirmation(chatId) {
75
+ pendingMacConfirmations.delete(String(chatId));
76
+ }
77
+
78
+ function isAffirmativeConfirmation(input) {
79
+ return /^(确认|确认执行|执行|继续|好|好的|可以|同意|yes|y|ok|okay)$/i.test(String(input || '').trim());
80
+ }
81
+
82
+ function isNegativeConfirmation(input) {
83
+ return /^(取消|不执行|不用了|算了|停止|否|no|n)$/i.test(String(input || '').trim());
84
+ }
85
+
86
+ function isReadOnlyMacNaturalLanguageCommand(command) {
87
+ const normalized = String(command || '').trim().toLowerCase();
88
+ return normalized === '/mac check' || normalized === '/mac perms';
89
+ }
90
+
91
+ async function requestMacSideEffectConfirmation(bot, chatId, originalText, syntheticCommand, sourceTag) {
92
+ const label = String(originalText || '').trim().slice(0, 160);
93
+ setPendingMacConfirmation(chatId, { originalText: label, syntheticCommand, sourceTag });
94
+ await bot.sendMessage(chatId, [
95
+ '⚠️ 检测到可能有副作用的 macOS 操作,已暂停自动执行。',
96
+ `来源: ${sourceTag}`,
97
+ `原始请求: ${label || '(empty)'}`,
98
+ `拟执行命令: ${syntheticCommand}`,
99
+ '回复“确认”执行,回复“取消”放弃(120 秒内有效)。',
100
+ ].join('\n'));
101
+ }
102
+
103
+ async function tryResolvePendingMacConfirmation(bot, chatId, text, config, executeTaskByName) {
104
+ const pending = getPendingMacConfirmation(chatId);
105
+ if (!pending) return false;
106
+
107
+ const trimmed = String(text || '').trim();
108
+ if (isNegativeConfirmation(trimmed)) {
109
+ clearPendingMacConfirmation(chatId);
110
+ await bot.sendMessage(chatId, '✅ 已取消该 macOS 操作。');
111
+ return true;
112
+ }
113
+
114
+ if (isAffirmativeConfirmation(trimmed)) {
115
+ clearPendingMacConfirmation(chatId);
116
+ log('WARN', `Mac side-effect confirmed [${String(chatId).slice(-8)}] (${pending.sourceTag})`);
117
+ return handleExecCommand({
118
+ bot,
119
+ chatId,
120
+ text: pending.syntheticCommand,
121
+ config,
122
+ executeTaskByName,
123
+ nlIntentText: pending.originalText,
124
+ });
125
+ }
126
+
127
+ // Any unrelated message cancels stale pending intent to avoid context stickiness.
128
+ clearPendingMacConfirmation(chatId);
129
+ return false;
130
+ }
131
+
56
132
  function extractQuotedContent(input) {
57
133
  const m = String(input || '').match(/[“"'「](.+?)[”"'」]/);
58
134
  return m ? m[1].trim() : '';
@@ -119,6 +195,16 @@ function createCommandRouter(deps) {
119
195
  return fallbackName || 'workspace-agent';
120
196
  }
121
197
 
198
+ function projectKeyFromVirtualChatId(chatId) {
199
+ const v = String(chatId || '');
200
+ if (v.startsWith('_agent_')) return v.slice(7) || null;
201
+ if (v.startsWith('_scope_')) {
202
+ const idx = v.lastIndexOf('__');
203
+ if (idx > 7 && idx + 2 < v.length) return v.slice(idx + 2);
204
+ }
205
+ return null;
206
+ }
207
+
122
208
  function getBoundProjectForChat(chatId, cfg) {
123
209
  const map = {
124
210
  ...(cfg.telegram ? cfg.telegram.chat_agent_map : {}),
@@ -129,6 +215,132 @@ function createCommandRouter(deps) {
129
215
  return { key: key || null, project: proj || null };
130
216
  }
131
217
 
218
+ function escapeAppleScriptString(input) {
219
+ return String(input || '').replace(/\\/g, '\\\\').replace(/"/g, '\\"');
220
+ }
221
+
222
+ function resolveAppNameFromNaturalLanguage(rawName) {
223
+ if (!rawName) return null;
224
+ const aliasMap = {
225
+ '微信': 'WeChat',
226
+ 'wechat': 'WeChat',
227
+ '飞书': 'Feishu',
228
+ 'feishu': 'Feishu',
229
+ 'finder': 'Finder',
230
+ '访达': 'Finder',
231
+ 'safari': 'Safari',
232
+ 'chrome': 'Google Chrome',
233
+ '谷歌浏览器': 'Google Chrome',
234
+ 'calendar': 'Calendar',
235
+ '日历': 'Calendar',
236
+ 'mail': 'Mail',
237
+ '邮件': 'Mail',
238
+ 'notes': 'Notes',
239
+ '备忘录': 'Notes',
240
+ 'terminal': 'Terminal',
241
+ 'iterm': 'iTerm',
242
+ 'iterm2': 'iTerm',
243
+ 'system settings': 'System Settings',
244
+ '系统设置': 'System Settings',
245
+ };
246
+ let name = String(rawName).trim();
247
+ name = name.replace(/[。!?!?,,;;::]+$/g, '').trim();
248
+ if (!name) return null;
249
+ const key = name.toLowerCase();
250
+ if (aliasMap[key]) return aliasMap[key];
251
+ if (aliasMap[name]) return aliasMap[name];
252
+ if (!/^[a-zA-Z0-9_\u4e00-\u9fa5 .()\-]{1,64}$/.test(name)) return null;
253
+ return name;
254
+ }
255
+
256
+ function deriveMacNaturalLanguageCommand(input) {
257
+ const text = String(input || '').trim();
258
+ if (!text) return null;
259
+
260
+ // Priority 1: explicit permission/setup intents
261
+ if (/(打开|进入).*(权限|隐私).*设置/.test(text) || /(权限|隐私).*(设置|页面).*(打开|进入)/.test(text)) {
262
+ return '/mac perms open';
263
+ }
264
+ if (/(检查|检测|体检).*(mac|权限|自动化|脚本)/i.test(text) || /(mac|权限|自动化).*(检查|检测|体检)/i.test(text)) {
265
+ return '/mac check';
266
+ }
267
+ if (/(权限|授权).*(怎么|如何|开启|打开|配置)/.test(text)) {
268
+ return '/mac perms';
269
+ }
270
+
271
+ // Priority 2: volume / mute
272
+ if (/(取消静音|解除静音|恢复声音|unmute)/i.test(text)) {
273
+ return '/mac osa set volume without output muted';
274
+ }
275
+ if (/(静音|mute)/i.test(text)) {
276
+ return '/mac osa set volume with output muted';
277
+ }
278
+ const volMatch = text.match(/(?:音量|volume)[^\d]{0,8}(\d{1,3})/i) || text.match(/调到\s*(\d{1,3})\s*(?:%|%)/);
279
+ if (volMatch) {
280
+ const v = Math.max(0, Math.min(100, Number(volMatch[1])));
281
+ if (Number.isFinite(v)) return `/mac osa set volume output volume ${v}`;
282
+ }
283
+
284
+ // Priority 3: common system actions
285
+ if (/(锁屏|锁定屏幕|lock\s*screen)/i.test(text)) {
286
+ return '/mac osa tell application "System Events" to keystroke "q" using {control down, command down}';
287
+ }
288
+ if (/(让|使|进入)?.*(电脑|系统|mac).*(睡眠|休眠)|(^睡眠$)|(^sleep$)/i.test(text)) {
289
+ return '/mac osa tell application "System Events" to sleep';
290
+ }
291
+
292
+ // Priority 4: app open/close
293
+ const openMatch = text.match(/(?:请|帮我|麻烦)?(?:把)?(?:打开|启动|唤起|切到)\s*([a-zA-Z0-9_\u4e00-\u9fa5 .()\-]{1,64})(?:\s*(?:应用|app))?$/i);
294
+ if (openMatch) {
295
+ const app = resolveAppNameFromNaturalLanguage(openMatch[1]);
296
+ if (app) return `/mac osa tell application "${escapeAppleScriptString(app)}" to activate`;
297
+ }
298
+ const closeMatch = text.match(/(?:请|帮我|麻烦)?(?:把)?(?:关闭|退出|停止)\s*([a-zA-Z0-9_\u4e00-\u9fa5 .()\-]{1,64})(?:\s*(?:应用|app))?$/i);
299
+ if (closeMatch) {
300
+ const app = resolveAppNameFromNaturalLanguage(closeMatch[1]);
301
+ if (app) return `/mac osa tell application "${escapeAppleScriptString(app)}" to quit`;
302
+ }
303
+
304
+ return null;
305
+ }
306
+
307
+ async function tryHandleMacNaturalLanguageIntent(bot, chatId, text, config, options = {}) {
308
+ if (!text || text.startsWith('/')) return false;
309
+ if (process.platform !== 'darwin') return false;
310
+ const daemonCfg = (config && config.daemon) || {};
311
+ if (daemonCfg.enable_nl_mac_control === false) return false;
312
+ const sourceTag = String(options.source || 'direct');
313
+ const safeOnly = !!options.safeOnly;
314
+ const confirmSideEffects = !!options.confirmSideEffects;
315
+
316
+ const syntheticCommand = deriveMacNaturalLanguageCommand(text);
317
+ if (!syntheticCommand) return false;
318
+ const isReadOnly = isReadOnlyMacNaturalLanguageCommand(syntheticCommand);
319
+
320
+ if (safeOnly && !isReadOnly) {
321
+ if (confirmSideEffects) {
322
+ await requestMacSideEffectConfirmation(bot, chatId, text, syntheticCommand, sourceTag);
323
+ return true;
324
+ }
325
+ return false;
326
+ }
327
+
328
+ if (confirmSideEffects && !isReadOnly) {
329
+ await requestMacSideEffectConfirmation(bot, chatId, text, syntheticCommand, sourceTag);
330
+ return true;
331
+ }
332
+
333
+ log('INFO', `NL mac intent [${String(chatId).slice(-8)}] (${sourceTag}): ${text.slice(0, 80)} -> ${syntheticCommand}`);
334
+ return handleExecCommand({
335
+ bot,
336
+ chatId,
337
+ text: syntheticCommand,
338
+ config,
339
+ executeTaskByName: () => ({ success: false, error: 'not available' }),
340
+ nlIntentText: text,
341
+ });
342
+ }
343
+
132
344
  async function tryHandleAgentIntent(bot, chatId, text, config) {
133
345
  if (!agentTools || !text || text.startsWith('/')) return false;
134
346
  const key = String(chatId);
@@ -267,13 +479,17 @@ function createCommandRouter(deps) {
267
479
  return;
268
480
  }
269
481
 
482
+ if (await tryResolvePendingMacConfirmation(bot, chatId, text, config, executeTaskByName)) {
483
+ return;
484
+ }
485
+
270
486
  // --- chat_agent_map: auto-switch agent based on dedicated chatId ---
271
487
  // Configure in daemon.yaml: feishu.chat_agent_map or telegram.chat_agent_map
272
488
  // e.g. chat_agent_map: { "oc_xxx": "personal", "oc_yyy": "metame" }
273
489
  const chatAgentMap = { ...(config.telegram ? config.telegram.chat_agent_map : {}), ...(config.feishu ? config.feishu.chat_agent_map : {}) };
274
490
  const _chatIdStr = String(chatId);
275
491
  const mappedKey = chatAgentMap[_chatIdStr] ||
276
- (_chatIdStr.startsWith('_agent_') ? _chatIdStr.slice(7) : null);
492
+ projectKeyFromVirtualChatId(_chatIdStr);
277
493
  if (mappedKey && config.projects && config.projects[mappedKey]) {
278
494
  const proj = config.projects[mappedKey];
279
495
  const projCwd = normalizeCwd(proj.cwd);
@@ -338,8 +554,10 @@ function createCommandRouter(deps) {
338
554
  '/quit — 结束会话,重新加载 MCP/配置',
339
555
  '',
340
556
  `⚙️ /model [${currentModel}] /provider [${currentProvider}] /status /tasks /run /budget /reload`,
557
+ '🧩 /TeamTask create <agent> <目标> [--scope <id>] · /TeamTask · /TeamTask <id>',
341
558
  '🧠 /memory — 记忆统计 · /memory <关键词> — 搜索事实',
342
- `🔧 /doctor /fix /reset /sh <cmd> /nosleep [${getNoSleepProcess() ? 'ON' : 'OFF'}]`,
559
+ '🧬 /skill-evo 查看/处理技能演化队列',
560
+ `🔧 /doctor /fix /reset /mac /sh <cmd> /nosleep [${getNoSleepProcess() ? 'ON' : 'OFF'}]`,
343
561
  '',
344
562
  '直接打字即可对话 💬',
345
563
  ].join('\n'));
@@ -401,13 +619,34 @@ function createCommandRouter(deps) {
401
619
  return;
402
620
  }
403
621
 
622
+ const daemonCfg = (config && config.daemon) || {};
623
+ const macControlMode = String(daemonCfg.mac_control_mode || 'claude-first').trim().toLowerCase();
624
+ const macLocalFirst = (macControlMode === 'local-first');
625
+ const macFallbackEnabled = (daemonCfg.enable_nl_mac_fallback !== false);
626
+ const allowLocalMacControl = !readOnly && (daemonCfg.enable_nl_mac_control !== false);
627
+ if (macLocalFirst && allowLocalMacControl && await tryHandleMacNaturalLanguageIntent(bot, chatId, text, config, { source: 'local-first' })) {
628
+ return;
629
+ }
630
+
404
631
  const cd = checkCooldown(chatId);
405
632
  if (!cd.ok) { await bot.sendMessage(chatId, `${cd.wait}s`); return; }
406
633
  if (!checkBudget(loadConfig(), loadState())) {
407
634
  await bot.sendMessage(chatId, 'Daily token budget exceeded.');
408
635
  return;
409
636
  }
410
- await askClaude(bot, chatId, text, config, readOnly);
637
+ const claudeResult = await askClaude(bot, chatId, text, config, readOnly);
638
+ const claudeFailed = !!(claudeResult && claudeResult.ok === false);
639
+ const claudeInterrupted = !!(claudeResult && claudeResult.interrupted);
640
+ if (claudeFailed && !claudeInterrupted && !macLocalFirst && macFallbackEnabled && allowLocalMacControl) {
641
+ const fallbackHandled = await tryHandleMacNaturalLanguageIntent(bot, chatId, text, config, {
642
+ source: 'claude-fallback',
643
+ safeOnly: true,
644
+ confirmSideEffects: true,
645
+ });
646
+ if (fallbackHandled) {
647
+ log('WARN', `Claude-first mac fallback handled for ${String(chatId).slice(-8)} (mode=${macControlMode})`);
648
+ }
649
+ }
411
650
  }
412
651
 
413
652
  return { handleCommand };
@@ -24,7 +24,8 @@ projects:
24
24
  # - name: "daily-task"
25
25
  # cwd: "~/AGI/MyProject"
26
26
  # model: "sonnet"
27
- # interval: "24h"
27
+ # at: "09:30" # fixed local time (HH:MM)
28
+ # days: "weekdays" # optional: weekdays | weekends | [mon, tue, ...]
28
29
  # notify: true
29
30
  # enabled: true
30
31
  # prompt: "..."
@@ -68,13 +69,15 @@ heartbeat:
68
69
  # Scheduled task (calls claude -p with your profile context):
69
70
  # - name: "morning-news"
70
71
  # prompt: "抓取今天的AI领域重要新闻,整理成3条摘要,中英双语。"
71
- # interval: "24h"
72
+ # at: "09:00" # 每天本地时间 09:00
73
+ # days: "weekdays" # 可选,不填表示每天
72
74
  # model: "haiku"
73
75
  # notify: true
74
76
  #
75
77
  # - name: "weekly-review"
76
78
  # prompt: "基于我的profile,生成本周个人成长回顾和下周建议。"
77
- # interval: "7d"
79
+ # at: "20:30"
80
+ # days: [sun]
78
81
  # model: "haiku"
79
82
  # notify: true
80
83
  #
@@ -94,6 +97,10 @@ heartbeat:
94
97
  # precondition: "test -s ~/.metame/skill_signals.jsonl"
95
98
  # notify: false
96
99
  #
100
+ # Scheduling:
101
+ # - interval: "6h" -> run every 6 hours (legacy + fully supported)
102
+ # - at: "HH:MM" -> run at fixed local time, optional days filter
103
+ #
97
104
  # precondition: shell command, empty output → skip (zero tokens)
98
105
  # type: "script" → runs command directly instead of claude -p
99
106
 
@@ -1,5 +1,7 @@
1
1
  'use strict';
2
2
 
3
+ const { classifyTaskUsage } = require('./usage-classifier');
4
+
3
5
  function createExecCommandHandler(deps) {
4
6
  const {
5
7
  fs,
@@ -23,8 +25,129 @@ function createExecCommandHandler(deps) {
23
25
  loadConfig,
24
26
  } = deps;
25
27
 
28
+ function truncateOutput(output, maxLen = 4000) {
29
+ const text = (output || '').trim() || '(no output)';
30
+ return text.length > maxLen ? text.slice(0, maxLen) + '\n... (truncated)' : text;
31
+ }
32
+
33
+ function maxReplyLengthForChat(chatId, defaultLen) {
34
+ // Feishu text messages have a lower practical limit than Telegram.
35
+ return typeof chatId === 'number' ? defaultLen : Math.min(defaultLen, 1200);
36
+ }
37
+
38
+ async function runCommand(bin, args, options = {}) {
39
+ return new Promise((resolve) => {
40
+ let settled = false;
41
+ const child = spawn(bin, args, options);
42
+ let stdout = '';
43
+ let stderr = '';
44
+
45
+ const finish = (code, errorText = '') => {
46
+ if (settled) return;
47
+ settled = true;
48
+ const merged = `${stdout}${stderr}${errorText}`;
49
+ resolve({
50
+ code: typeof code === 'number' ? code : 1,
51
+ stdout,
52
+ stderr: `${stderr}${errorText}`,
53
+ output: merged.trim(),
54
+ });
55
+ };
56
+
57
+ child.stdout.on('data', d => { stdout += d; });
58
+ child.stderr.on('data', d => { stderr += d; });
59
+ child.on('close', code => finish(code));
60
+ child.on('error', err => finish(1, `${stderr ? '\n' : ''}${err.message}`));
61
+ });
62
+ }
63
+
64
+ async function runMacCapabilityChecksInline() {
65
+ const checks = [];
66
+ const shotPath = `/tmp/metame_gui_test_${process.pid}_${Date.now()}.png`;
67
+ checks.push({ name: 'osascript binary available', mode: 'pass_on_zero', cmd: 'which osascript' });
68
+ checks.push({ name: 'AppleScript baseline', mode: 'pass_on_zero', cmd: 'osascript -e \'return "ok"\'' });
69
+ checks.push({ name: 'Finder automation', mode: 'pass_on_zero', cmd: 'osascript -e \'tell application "Finder" to get name of startup disk\'' });
70
+ checks.push({ name: 'System Events accessibility', mode: 'pass_on_zero', cmd: 'osascript -e \'tell application "System Events" to get UI elements enabled\'' });
71
+ checks.push({
72
+ name: 'GUI app launch/control (Calculator)',
73
+ mode: 'pass_on_zero',
74
+ cmd: 'open -a Calculator >/dev/null 2>&1; sleep 1; osascript -e \'tell application "System Events" to tell process "Calculator" to return {frontmost, (count of windows)}\'; osascript -e \'tell application "Calculator" to quit\' >/dev/null 2>&1',
75
+ });
76
+ checks.push({ name: 'Screenshot capability (screencapture)', mode: 'pass_on_zero', cmd: `screencapture -x '${shotPath}' && ls -lh '${shotPath}'` });
77
+ checks.push({ name: 'Full Disk probe: read ~/Library/Mail', mode: 'warn_on_nonzero', cmd: "ls '$HOME/Library/Mail' | head -n 3" });
78
+ checks.push({ name: 'Full Disk probe: query Safari History.db', mode: 'warn_on_nonzero', cmd: "sqlite3 '$HOME/Library/Safari/History.db' 'select count(*) from history_items;'" });
79
+
80
+ const lines = [];
81
+ let pass = 0;
82
+ let warn = 0;
83
+ let fail = 0;
84
+ lines.push('MetaMe macOS control capability check');
85
+ lines.push(`Timestamp: ${new Date().toISOString()}`);
86
+ lines.push('');
87
+
88
+ for (const c of checks) {
89
+ const r = await runCommand('bash', ['-o', 'pipefail', '-lc', c.cmd], { timeout: 30000 });
90
+ let level = 'FAIL';
91
+ if (c.mode === 'pass_on_zero') {
92
+ if (r.code === 0) {
93
+ pass++;
94
+ level = 'PASS';
95
+ } else {
96
+ fail++;
97
+ level = 'FAIL';
98
+ }
99
+ } else {
100
+ if (r.code === 0) {
101
+ pass++;
102
+ level = 'PASS';
103
+ } else {
104
+ warn++;
105
+ level = 'WARN';
106
+ }
107
+ }
108
+ lines.push(`[${level}] ${c.name}`);
109
+ if (r.output) lines.push(` ${r.output.split('\n').join('\n ')}`);
110
+ }
111
+
112
+ await runCommand('rm', ['-f', shotPath], { timeout: 3000 });
113
+ lines.push('');
114
+ lines.push(`Summary: pass=${pass} warn=${warn} fail=${fail}`);
115
+ return { code: fail > 0 ? 1 : 0, output: lines.join('\n') };
116
+ }
117
+
118
+ function macCommandHelp() {
119
+ return [
120
+ '🍎 macOS 控制命令',
121
+ '/mac check — 检查 AppleScript/UI 自动化/截图/磁盘权限',
122
+ '/mac perms — 查看建议开启的系统权限',
123
+ '/mac perms open — 打开系统设置权限页',
124
+ '/mac osa <AppleScript> — 直接执行 AppleScript',
125
+ '/mac jxa <JavaScript> — 通过 osascript 执行 JXA',
126
+ '',
127
+ '示例:',
128
+ '/mac osa tell application "Finder" to get name of startup disk',
129
+ ].join('\n');
130
+ }
131
+
132
+ function macPermissionGuide() {
133
+ return [
134
+ '🛡 建议给 MetaMe/终端开启这些权限(系统设置 → 隐私与安全性):',
135
+ '1) 辅助功能(Accessibility)',
136
+ '2) 自动化(Automation / Apple Events)',
137
+ '3) 完全磁盘访问(Full Disk Access)',
138
+ '4) 屏幕录制(Screen Recording)',
139
+ '',
140
+ '说明:',
141
+ '- 辅助功能/自动化:用于控制 Finder、System Events、GUI 应用',
142
+ '- 完全磁盘访问:用于读取 Mail/Safari 等受保护目录',
143
+ '- 屏幕录制:用于截图和视觉回传',
144
+ '',
145
+ '执行 `/mac perms open` 可尝试直接跳转到对应设置页。',
146
+ ].join('\n');
147
+ }
148
+
26
149
  async function handleExecCommand(ctx) {
27
- const { bot, chatId, text, config, executeTaskByName } = ctx;
150
+ const { bot, chatId, text, config, executeTaskByName, nlIntentText } = ctx;
28
151
 
29
152
  if (text.startsWith('/run ')) {
30
153
  const cd = checkCooldown(chatId);
@@ -65,7 +188,7 @@ function createExecCommandHandler(deps) {
65
188
  await bot.sendMessage(chatId, `❌ ${taskName}: ${error}`);
66
189
  } else {
67
190
  const est = Math.ceil((fullPrompt.length + (output || '').length) / 4);
68
- recordTokens(loadState(), est);
191
+ recordTokens(loadState(), est, { category: classifyTaskUsage({ name: taskName, type: 'manual_task' }) });
69
192
  const st = loadState();
70
193
  st.tasks[taskName] = { last_run: new Date().toISOString(), status: 'success', output_preview: (output || '').slice(0, 200) };
71
194
  saveState(st);
@@ -248,6 +371,127 @@ function createExecCommandHandler(deps) {
248
371
  return true;
249
372
  }
250
373
 
374
+ // /mac — macOS control helpers (AppleScript/JXA/permissions)
375
+ const macMatch = String(text || '').match(/^\/mac(?:\s+(.*))?$/i);
376
+ if (macMatch) {
377
+ if (process.platform !== 'darwin') {
378
+ await bot.sendMessage(chatId, '❌ /mac 仅支持 macOS');
379
+ return true;
380
+ }
381
+
382
+ const argRaw = (macMatch[1] || '').trim();
383
+ const arg = argRaw.toLowerCase();
384
+ if (!argRaw || arg === 'help') {
385
+ if (bot.sendButtons) {
386
+ await bot.sendButtons(chatId, macCommandHelp(), [
387
+ [{ text: '✅ /mac check', callback_data: '/mac check' }],
388
+ [{ text: '🛡 /mac perms', callback_data: '/mac perms' }],
389
+ [{ text: '⚙️ /mac perms open', callback_data: '/mac perms open' }],
390
+ ]);
391
+ } else {
392
+ await bot.sendMessage(chatId, macCommandHelp());
393
+ }
394
+ return true;
395
+ }
396
+
397
+ if (arg === 'check') {
398
+ const checkScript = path.join(__dirname, 'check-macos-control-capabilities.sh');
399
+ await bot.sendMessage(chatId, '🔍 正在检查 macOS 控制能力...');
400
+ let result;
401
+ if (fs.existsSync(checkScript)) {
402
+ result = await runCommand('bash', [checkScript], { timeout: 120000 });
403
+ } else {
404
+ await bot.sendMessage(chatId, '⚠️ 检测脚本缺失,已切换为内置检查模式。');
405
+ result = await runMacCapabilityChecksInline();
406
+ }
407
+ const out = truncateOutput(result.output, maxReplyLengthForChat(chatId, 3600));
408
+ if (result.code === 0) {
409
+ await bot.sendMessage(chatId, `✅ macOS 能力检查完成\n\n${out}`);
410
+ } else {
411
+ await bot.sendMessage(chatId, `⚠️ 检查未全部通过\n\n${out}`);
412
+ }
413
+ return true;
414
+ }
415
+
416
+ if (arg === 'perms' || arg === 'permissions') {
417
+ await bot.sendMessage(chatId, macPermissionGuide());
418
+ return true;
419
+ }
420
+
421
+ if (arg === 'perms open' || arg === 'permissions open') {
422
+ const panes = [
423
+ 'x-apple.systempreferences:com.apple.preference.security?Privacy_Accessibility',
424
+ 'x-apple.systempreferences:com.apple.preference.security?Privacy_Automation',
425
+ 'x-apple.systempreferences:com.apple.preference.security?Privacy_AllFiles',
426
+ 'x-apple.systempreferences:com.apple.preference.security?Privacy_ScreenCapture',
427
+ ];
428
+ let ok = 0;
429
+ for (const pane of panes) {
430
+ const r = await runCommand('open', [pane], { timeout: 5000 });
431
+ if (r.code === 0) ok++;
432
+ }
433
+ await bot.sendMessage(chatId, `⚙️ 已尝试打开 ${ok}/${panes.length} 个权限设置页。\n若未跳转,请手动进入“系统设置 → 隐私与安全性”。`);
434
+ return true;
435
+ }
436
+
437
+ if (/^osa(?:\s+|$)/i.test(argRaw)) {
438
+ const script = argRaw.replace(/^osa(?:\s+|$)/i, '').trim();
439
+ if (!script) {
440
+ await bot.sendMessage(chatId, '用法: /mac osa <AppleScript>');
441
+ return true;
442
+ }
443
+ const result = await runCommand('osascript', ['-e', script], { timeout: 45000 });
444
+ const out = (result.output || '').trim();
445
+ if (result.code !== 0) {
446
+ await bot.sendMessage(chatId, `❌ AppleScript 执行失败\n${truncateOutput(out, maxReplyLengthForChat(chatId, 3000))}`);
447
+ } else if (nlIntentText) {
448
+ const label = String(nlIntentText).trim().slice(0, 120);
449
+ if (out) {
450
+ await bot.sendMessage(chatId, `✅ 已执行:${label}\n${truncateOutput(out, maxReplyLengthForChat(chatId, 1200))}`);
451
+ } else {
452
+ await bot.sendMessage(chatId, `✅ 已执行:${label}`);
453
+ }
454
+ } else {
455
+ if (out) {
456
+ await bot.sendMessage(chatId, `🍎 AppleScript 结果\n${truncateOutput(out, maxReplyLengthForChat(chatId, 3000))}`);
457
+ } else {
458
+ await bot.sendMessage(chatId, '✅ AppleScript 已执行(无返回值)');
459
+ }
460
+ }
461
+ return true;
462
+ }
463
+
464
+ if (/^jxa(?:\s+|$)/i.test(argRaw)) {
465
+ const script = argRaw.replace(/^jxa(?:\s+|$)/i, '').trim();
466
+ if (!script) {
467
+ await bot.sendMessage(chatId, '用法: /mac jxa <JavaScript>');
468
+ return true;
469
+ }
470
+ const result = await runCommand('osascript', ['-l', 'JavaScript', '-e', script], { timeout: 45000 });
471
+ const out = (result.output || '').trim();
472
+ if (result.code !== 0) {
473
+ await bot.sendMessage(chatId, `❌ JXA 执行失败\n${truncateOutput(out, maxReplyLengthForChat(chatId, 3000))}`);
474
+ } else if (nlIntentText) {
475
+ const label = String(nlIntentText).trim().slice(0, 120);
476
+ if (out) {
477
+ await bot.sendMessage(chatId, `✅ 已执行:${label}\n${truncateOutput(out, maxReplyLengthForChat(chatId, 1200))}`);
478
+ } else {
479
+ await bot.sendMessage(chatId, `✅ 已执行:${label}`);
480
+ }
481
+ } else {
482
+ if (out) {
483
+ await bot.sendMessage(chatId, `🍎 JXA 结果\n${truncateOutput(out, maxReplyLengthForChat(chatId, 3000))}`);
484
+ } else {
485
+ await bot.sendMessage(chatId, '✅ JXA 已执行(无返回值)');
486
+ }
487
+ }
488
+ return true;
489
+ }
490
+
491
+ await bot.sendMessage(chatId, macCommandHelp());
492
+ return true;
493
+ }
494
+
251
495
  // /sh [command] — direct shell execution (emergency lifeline)
252
496
  if (text === '/sh' || text.startsWith('/sh ')) {
253
497
  const command = text.slice(3).trim();
@@ -263,17 +507,8 @@ function createExecCommandHandler(deps) {
263
507
  return true;
264
508
  }
265
509
  try {
266
- const child = spawn('sh', ['-c', command], { timeout: 30000 });
267
- let stdout = '';
268
- let stderr = '';
269
- child.stdout.on('data', d => { stdout += d; });
270
- child.stderr.on('data', d => { stderr += d; });
271
- await new Promise((resolve) => {
272
- child.on('close', resolve);
273
- child.on('error', resolve);
274
- });
275
- let output = (stdout + stderr).trim() || '(no output)';
276
- if (output.length > 4000) output = output.slice(0, 4000) + '\n... (truncated)';
510
+ const result = await runCommand('sh', ['-c', command], { timeout: 30000 });
511
+ const output = truncateOutput(result.output, maxReplyLengthForChat(chatId, 4000));
277
512
  await bot.sendMessage(chatId, `💻 $ ${command}\n${output}`);
278
513
  } catch (e) {
279
514
  await bot.sendMessage(chatId, `❌ ${e.message}`);