evolclaw 2.5.4 → 2.5.6

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
@@ -260,7 +260,7 @@ evolclaw/
260
260
 
261
261
  ### Owner 专属命令
262
262
 
263
- - `/send <文件路径>` - 发送文件给用户
263
+ - `/file <文件路径>` - 发送文件给用户
264
264
  - `/restart` - 重启服务(自愈机制)
265
265
  - `/repair` - 检查并修复会话
266
266
  - `/agentmd [put|set]` - 管理 AUN agent.md(仅 AUN 渠道)
@@ -1,7 +1,12 @@
1
1
  {
2
2
  "agents": {
3
3
  "anthropic": {
4
- "model": "sonnet"
4
+ "apiKey": "",
5
+ "baseUrl": "",
6
+ "model": "sonnet",
7
+ "effort": "high",
8
+ "useSettingSources": true,
9
+ "agentProgressSummaries": true
5
10
  },
6
11
  "openai": {
7
12
  "apiKey": "your-openai-api-key",
@@ -10,7 +15,10 @@
10
15
  "effort": "medium"
11
16
  },
12
17
  "google": {
13
- "model": "gemini-2.5-flash"
18
+ "apiKey": "",
19
+ "model": "gemini-2.5-flash",
20
+ "cliPath": "",
21
+ "mode": "cli"
14
22
  },
15
23
  "defaultAgent": "claude"
16
24
  },
@@ -19,17 +27,26 @@
19
27
  "feishu": {
20
28
  "enabled": true,
21
29
  "appId": "",
22
- "appSecret": ""
30
+ "appSecret": "",
31
+ "owner": "",
32
+ "flushDelay": 10,
33
+ "debounce": 2,
34
+ "showActivities": "dm-only"
23
35
  },
24
36
  "wechat": {
25
37
  "enabled": false,
26
38
  "baseUrl": "https://ilinkai.weixin.qq.com",
27
- "token": ""
39
+ "token": "",
40
+ "owner": "",
41
+ "flushDelay": 3,
42
+ "debounce": 2,
43
+ "showActivities": "all"
28
44
  },
29
45
  "aun": {
30
46
  "enabled": false,
31
47
  "aid": "your-agent.agentid.pub",
32
- "gatewayPort": 443
48
+ "owner": "",
49
+ "showActivities": "owner-dm-only"
33
50
  }
34
51
  },
35
52
  "projects": {
@@ -37,11 +54,16 @@
37
54
  "autoCreate": true,
38
55
  "list": {}
39
56
  },
57
+ "flushDelay": 4,
58
+ "debounce": 2,
59
+ "showActivities": "all",
40
60
  "idleMonitor": {
41
61
  "enabled": true,
42
62
  "timeout": 120,
43
- "safeModeThreshold": 3
63
+ "safeModeThreshold": 0
44
64
  },
45
- "flushDelay": 4,
46
- "showActivities": "all"
65
+ "debug": {
66
+ "flusherDiag": false,
67
+ "aunTrace": false
68
+ }
47
69
  }
@@ -84,6 +84,7 @@ export class AgentRunner {
84
84
  sendPromptFn;
85
85
  permissionContexts = new Map();
86
86
  currentEvolclawSessionId;
87
+ claudeExecutablePath;
87
88
  constructor(apiKey, model, onSessionIdUpdate, baseUrl, config) {
88
89
  this.apiKey = apiKey;
89
90
  this.model = model || 'sonnet';
@@ -91,6 +92,10 @@ export class AgentRunner {
91
92
  this.baseUrl = baseUrl;
92
93
  this.config = config;
93
94
  this.onSessionIdUpdate = onSessionIdUpdate;
95
+ if (config) {
96
+ const anthropic = resolveAnthropicConfig(config);
97
+ this.claudeExecutablePath = anthropic.pathToClaudeCodeExecutable;
98
+ }
94
99
  }
95
100
  getAgentEnv() {
96
101
  return {
@@ -626,11 +631,18 @@ export class AgentRunner {
626
631
  const excludeDynamic = this.config?.agents?.anthropic?.excludeDynamicSections === true;
627
632
  // 公共 options(新旧模式共用)
628
633
  const sdkPermissionMode = this.toSdkPermissionMode();
629
- logger.info(`[AgentRunner] runQuery model=${this.model} effort=${this.effort ?? 'auto'} permMode=${this.permissionMode} sdkMode=${sdkPermissionMode} systemPromptAppend=${systemPromptAppend ? systemPromptAppend.substring(0, 100) + '...' : 'none'}`);
634
+ logger.info(`[AgentRunner] runQuery model=${this.model} effort=${this.effort ?? 'auto'} permMode=${this.permissionMode} sdkMode=${sdkPermissionMode}`);
635
+ if (systemPromptAppend) {
636
+ logger.info(`[AgentRunner] systemPromptAppend (full):\n${systemPromptAppend}`);
637
+ }
638
+ else {
639
+ logger.info(`[AgentRunner] systemPromptAppend: none`);
640
+ }
630
641
  const commonOptions = {
631
642
  cwd: projectPath,
632
643
  model: this.model,
633
644
  ...(this.effort ? { effort: this.effort } : {}),
645
+ ...(this.claudeExecutablePath ? { pathToClaudeCodeExecutable: this.claudeExecutablePath } : {}),
634
646
  autoCompactWindow: 200000,
635
647
  advisorModel: 'haiku',
636
648
  canUseTool: canUseToolCallback,
@@ -5,7 +5,6 @@
5
5
  * Implements the same interface surface as AgentRunner (claude-runner.ts)
6
6
  * so MessageProcessor and CommandHandler can work with it transparently.
7
7
  */
8
- import { Codex } from '@openai/codex-sdk';
9
8
  import { resolveOpenaiConfig } from '../config.js';
10
9
  import { logger } from '../utils/logger.js';
11
10
  import fs from 'fs';
@@ -24,24 +23,33 @@ const CODEX_MODELS = ['gpt-5.3-codex', 'gpt-5.2-codex', 'gpt-5-codex', 'gpt-5.2'
24
23
  export class CodexRunner {
25
24
  name = 'codex';
26
25
  capabilities = { clear: false, compact: false, fork: false };
27
- codex;
26
+ codex = null;
27
+ codexModule = null;
28
28
  model;
29
29
  effort;
30
30
  activeAbortControllers = new Map();
31
31
  activeStreams = new Map();
32
32
  activeSessions = new Map(); // sessionId → threadId
33
33
  onSessionIdUpdate;
34
+ resolvedConfig;
34
35
  constructor(config, callbacks) {
35
- const resolved = resolveOpenaiConfig(config);
36
- this.codex = new Codex({
37
- apiKey: resolved.apiKey,
38
- baseUrl: resolved.baseUrl,
39
- });
40
- this.model = resolved.model;
41
- if (resolved.effort)
42
- this.effort = resolved.effort;
36
+ this.resolvedConfig = resolveOpenaiConfig(config);
37
+ this.model = this.resolvedConfig.model;
38
+ if (this.resolvedConfig.effort)
39
+ this.effort = this.resolvedConfig.effort;
43
40
  this.onSessionIdUpdate = callbacks.onSessionIdUpdate;
44
41
  }
42
+ async ensureCodex() {
43
+ if (!this.codex || !this.codexModule) {
44
+ const { requireOptional } = await import('../utils/init-channel.js');
45
+ this.codexModule = await requireOptional('@openai/codex-sdk');
46
+ this.codex = new this.codexModule.Codex({
47
+ apiKey: this.resolvedConfig.apiKey,
48
+ baseUrl: this.resolvedConfig.baseUrl,
49
+ });
50
+ }
51
+ return { codex: this.codex, mod: this.codexModule };
52
+ }
45
53
  // ── ModelSwitcher ──
46
54
  setModel(model) { this.model = model; }
47
55
  getModel() { return this.model; }
@@ -89,6 +97,7 @@ export class CodexRunner {
89
97
  async runQuery(sessionId, prompt, projectPath, initialAgentSessionId, images, systemPromptAppend, sessionManager) {
90
98
  // Agent ctl: 注入 EVOLCLAW_SESSION_ID 供子进程使用
91
99
  process.env.EVOLCLAW_SESSION_ID = sessionId;
100
+ const { codex } = await this.ensureCodex();
92
101
  let agentSessionId = initialAgentSessionId || this.activeSessions.get(sessionId);
93
102
  const threadOptions = {
94
103
  workingDirectory: projectPath,
@@ -99,8 +108,8 @@ export class CodexRunner {
99
108
  ...(this.effort ? { modelReasoningEffort: this.effort } : {}),
100
109
  };
101
110
  const thread = agentSessionId
102
- ? this.codex.resumeThread(agentSessionId, threadOptions)
103
- : this.codex.startThread(threadOptions);
111
+ ? codex.resumeThread(agentSessionId, threadOptions)
112
+ : codex.startThread(threadOptions);
104
113
  const controller = new AbortController();
105
114
  this.activeAbortControllers.set(sessionId, controller);
106
115
  // 构建输入:将 base64 图片写入临时文件,转换为 Codex SDK 的 local_image 格式
@@ -226,7 +235,7 @@ export class CodexRunner {
226
235
  yield { type: 'tool_use', name: `MCP:${item.server}/${item.tool}`, input: item.arguments };
227
236
  }
228
237
  else if (item.type === 'file_change') {
229
- const desc = item.changes.map(c => `${c.kind} ${c.path}`).join(', ');
238
+ const desc = item.changes.map((c) => `${c.kind} ${c.path}`).join(', ');
230
239
  yield { type: 'tool_use', name: 'FileChange', input: { description: desc } };
231
240
  }
232
241
  else if (item.type === 'web_search') {
@@ -1,4 +1,4 @@
1
- import { AUNClient, GatewayDiscovery } from '@eleans/aun-core-sdk';
1
+ import { AUNClient, GatewayDiscovery } from '@agentunion/aun-node';
2
2
  import crypto from 'crypto';
3
3
  import fs from 'fs';
4
4
  import path from 'path';
@@ -1072,6 +1072,7 @@ export class AUNChannelPlugin {
1072
1072
  const options = {
1073
1073
  flushDelay: inst.flushDelay ?? 3,
1074
1074
  fileMarkerPattern: /\[SEND_FILE:(?:(\w+):)?([^\]]+)\]/g,
1075
+ sessionMode: inst.sessionMode,
1075
1076
  };
1076
1077
  result.push({
1077
1078
  channelType: 'aun',
@@ -1,4 +1,5 @@
1
1
  import { logger } from '../utils/logger.js';
2
+ import { requireOptional } from '../utils/init-channel.js';
2
3
  import { normalizeChannelInstances, getChannelShowActivities } from '../config.js';
3
4
  // ── Webhook SSRF validation ────────────────────────────────────────────────────
4
5
  const WEBHOOK_RE = /^https:\/\/(api|oapi)\.dingtalk\.com\//;
@@ -56,7 +57,7 @@ export class DingtalkChannel {
56
57
  if (!clientId || !clientSecret || clientId.includes('your-') || clientSecret.includes('your-')) {
57
58
  throw new Error('DingTalk clientId/clientSecret not configured');
58
59
  }
59
- const { DWClient, TOPIC_ROBOT } = await import('dingtalk-stream');
60
+ const { DWClient, TOPIC_ROBOT } = await requireOptional('dingtalk-stream');
60
61
  this.client = new DWClient({ clientId, clientSecret });
61
62
  this.client.registerCallbackListener(TOPIC_ROBOT, async (msg) => {
62
63
  await this.handleIncoming(msg);
@@ -320,7 +321,7 @@ export class DingtalkChannel {
320
321
  return;
321
322
  }
322
323
  // Step 1: Upload media
323
- const FormData = (await import('form-data')).default;
324
+ const FormData = (await requireOptional('form-data')).default;
324
325
  const form = new FormData();
325
326
  form.append('type', 'image');
326
327
  form.append('media', png, { filename: 'image.png', contentType: 'image/png' });
@@ -360,7 +361,7 @@ export class DingtalkChannel {
360
361
  return;
361
362
  }
362
363
  // Step 1: Upload media
363
- const FormData = (await import('form-data')).default;
364
+ const FormData = (await requireOptional('form-data')).default;
364
365
  const form = new FormData();
365
366
  form.append('type', 'file');
366
367
  form.append('media', fs.createReadStream(filePath), { filename: path.basename(filePath) });
@@ -1,4 +1,3 @@
1
- import * as lark from '@larksuiteoapi/node-sdk';
2
1
  import fs from 'fs';
3
2
  import path from 'path';
4
3
  import imageType from 'image-type';
@@ -41,6 +40,8 @@ export class FeishuChannel {
41
40
  if (this.config.appId.startsWith('YOUR_') || this.config.appSecret.startsWith('YOUR_')) {
42
41
  throw new Error('Feishu credentials not configured (placeholder values detected)');
43
42
  }
43
+ const { requireOptional } = await import('../utils/init-channel.js');
44
+ const lark = await requireOptional('@larksuiteoapi/node-sdk');
44
45
  try {
45
46
  this.client = new lark.Client({
46
47
  appId: this.config.appId,
@@ -1,5 +1,6 @@
1
1
  import { logger } from '../utils/logger.js';
2
2
  import { markdownToPlainText } from '../utils/format.js';
3
+ import { requireOptional } from '../utils/init-channel.js';
3
4
  import { normalizeChannelInstances, getChannelShowActivities } from '../config.js';
4
5
  // ── QQBotChannel ────────────────────────────────────────────────────────────
5
6
  export class QQBotChannel {
@@ -37,7 +38,7 @@ export class QQBotChannel {
37
38
  if (!appId || !clientSecret || appId.includes('your-') || clientSecret.includes('your-')) {
38
39
  throw new Error('QQBot appId/clientSecret not configured');
39
40
  }
40
- const { QQBotClient } = await import('pure-qqbot');
41
+ const { QQBotClient } = await requireOptional('pure-qqbot');
41
42
  this.client = new QQBotClient({
42
43
  appId,
43
44
  clientSecret,
@@ -1,5 +1,6 @@
1
1
  import crypto from 'node:crypto';
2
2
  import { logger } from '../utils/logger.js';
3
+ import { requireOptional } from '../utils/init-channel.js';
3
4
  import { normalizeChannelInstances, getChannelShowActivities } from '../config.js';
4
5
  // ── WecomChannel ───────────────────────────────────────────────────────────────
5
6
  export class WecomChannel {
@@ -31,7 +32,7 @@ export class WecomChannel {
31
32
  if (!botId || !secret) {
32
33
  throw new Error('WeCom botId/secret not configured');
33
34
  }
34
- const { WSClient } = await import('@wecom/aibot-node-sdk');
35
+ const { WSClient } = await requireOptional('@wecom/aibot-node-sdk');
35
36
  this.client = new WSClient({ botId, secret });
36
37
  // Message events
37
38
  this.client.on('message', (frame) => {
package/dist/cli.js CHANGED
@@ -1241,33 +1241,6 @@ async function cmdDiagnose() {
1241
1241
  console.log('\n[diagnose] ✓ 所有检查通过');
1242
1242
  }
1243
1243
  }
1244
- async function cmdTui() {
1245
- const config = loadConfig();
1246
- // Find the first AUN instance (TUI connects to one AUN instance)
1247
- const aunResolved = resolveInstanceConfig(config, 'aun');
1248
- const aun = aunResolved?.type === 'aun' ? aunResolved.config : null;
1249
- if (!aun?.owner || !aun?.aid) {
1250
- console.error('[tui] AUN 未配置,请先运行: evolclaw init aun');
1251
- process.exit(1);
1252
- }
1253
- // TUI requires Python + aun_core (independent of init aun which is now pure TS)
1254
- const pythonCheck = aun.pythonBin || process.env.AUN_PYTHON || 'python3';
1255
- if (!platform.commandExists(pythonCheck)) {
1256
- console.error(`[tui] Python 未找到 (${pythonCheck})`);
1257
- console.error(' → TUI 依赖 Python 和 aun-core (>=0.2.9): pip3 install -U aun-core');
1258
- process.exit(1);
1259
- }
1260
- const pythonBin = aun.pythonBin || process.env.AUN_PYTHON || 'python3';
1261
- const cliScript = path.join(getPackageRoot(), 'aun', 'aun_cli.py');
1262
- if (!fs.existsSync(cliScript)) {
1263
- console.error(`[tui] aun_cli.py 不存在: ${cliScript}`);
1264
- console.error(' → TUI 需要 AUN CLI 工具,请确认源码目录包含 aun/aun_cli.py');
1265
- console.error(' → 安装: pip3 install -U aun-core && 从源码仓库获取 aun_cli.py');
1266
- process.exit(1);
1267
- }
1268
- const child = spawn(pythonBin, [cliScript, '-a', aun.owner, '-t', aun.aid], { stdio: 'inherit' });
1269
- child.on('exit', (code) => process.exit(code ?? 0));
1270
- }
1271
1244
  // ==================== Ctl ====================
1272
1245
  async function cmdCtl(args) {
1273
1246
  if (args.length === 0) {
@@ -1275,6 +1248,8 @@ async function cmdCtl(args) {
1275
1248
  console.error('示例: evolclaw ctl model sonnet');
1276
1249
  console.error(' evolclaw ctl status');
1277
1250
  console.error(' evolclaw ctl effort high');
1251
+ console.error(' evolclaw ctl send "<消息内容>" # proactive 模式主动发消息');
1252
+ console.error(' evolclaw ctl chatmode proactive # 切换会话模式');
1278
1253
  process.exit(1);
1279
1254
  }
1280
1255
  const sessionId = process.env.EVOLCLAW_SESSION_ID;
@@ -1332,6 +1307,12 @@ export async function main(args) {
1332
1307
  else if (args[1] === 'wecom') {
1333
1308
  await cmdInitWecom();
1334
1309
  }
1310
+ else if (args[1]) {
1311
+ const supported = ['feishu', 'wechat', 'aun', 'dingtalk', 'qqbot', 'wecom'];
1312
+ console.error(`❌ 不支持的渠道: ${args[1]}`);
1313
+ console.error(` 支持的渠道: ${supported.join(', ')}`);
1314
+ process.exit(1);
1315
+ }
1335
1316
  else {
1336
1317
  const nonInteractive = args.includes('--non-interactive');
1337
1318
  if (nonInteractive) {
@@ -1372,14 +1353,11 @@ export async function main(args) {
1372
1353
  case 'diagnose':
1373
1354
  await cmdDiagnose();
1374
1355
  break;
1375
- case 'tui':
1376
- await cmdTui();
1377
- break;
1378
1356
  case 'ctl':
1379
1357
  await cmdCtl(args.slice(1));
1380
1358
  break;
1381
1359
  default:
1382
- console.log(`Usage: evolclaw {init|start|stop|restart|status|logs|tui|ctl|diagnose|mv}
1360
+ console.log(`Usage: evolclaw {init|start|stop|restart|status|logs|ctl|diagnose|mv}
1383
1361
 
1384
1362
  Commands:
1385
1363
  init 创建配置文件 (${resolvePaths().config})
@@ -1397,7 +1375,6 @@ Commands:
1397
1375
  --level error|warn 只显示指定级别及以上
1398
1376
  --module <name> 只显示指定模块(如 feishu、AgentRunner)
1399
1377
  --raw 原始输出,不着色
1400
- tui 启动 AUN TUI 客户端
1401
1378
  diagnose 诊断启动环境(配置、数据库、进程)
1402
1379
  mv <old> <new> 迁移项目目录(保留 Claude/Codex/EvolClaw 会话)
1403
1380
 
package/dist/config.js CHANGED
@@ -73,7 +73,10 @@ export function resolveAnthropicConfig(config) {
73
73
  const effort = config.agents?.anthropic?.effort
74
74
  || settings.effortLevel
75
75
  || undefined;
76
- return { apiKey, baseUrl, model, effort };
76
+ const configExecPath = config.agents?.anthropic?.pathToClaudeCodeExecutable;
77
+ const isPlaceholderExec = !configExecPath || configExecPath.includes('your-') || configExecPath.includes('placeholder');
78
+ const pathToClaudeCodeExecutable = isPlaceholderExec ? undefined : configExecPath;
79
+ return { apiKey, baseUrl, model, effort, pathToClaudeCodeExecutable };
77
80
  }
78
81
  export function resolveOpenaiConfig(config) {
79
82
  const codexSettings = loadCodexSettings();
@@ -305,6 +308,28 @@ export function setChannelShowActivities(config, instanceName, mode) {
305
308
  }
306
309
  }
307
310
  }
311
+ /**
312
+ * 读取通道实例的 sessionMode 锁定配置
313
+ * 返回 undefined 表示未配置(由 session-manager 按 chatType 默认决定)
314
+ */
315
+ export function getChannelSessionMode(config, instanceName) {
316
+ for (const type of channelTypes) {
317
+ const raw = config.channels?.[type];
318
+ if (raw === undefined)
319
+ continue;
320
+ if (Array.isArray(raw)) {
321
+ const inst = raw.find((item) => item.name === instanceName);
322
+ if (inst)
323
+ return inst.sessionMode;
324
+ }
325
+ else {
326
+ const effectiveName = raw.name ?? type;
327
+ if (effectiveName === instanceName)
328
+ return raw.sessionMode;
329
+ }
330
+ }
331
+ return undefined;
332
+ }
308
333
  export function isOwner(config, channelOrType, userId) {
309
334
  // 按实例名精确匹配
310
335
  if (getOwner(config, channelOrType) === userId)
@@ -1,5 +1,5 @@
1
1
  import { hasModelSwitcher, hasPermissionController } from '../agents/claude-runner.js';
2
- import { saveConfig, resolvePaths, getPackageRoot, getOwner, getChannelShowActivities, setChannelShowActivities } from '../config.js';
2
+ import { saveConfig, resolvePaths, getPackageRoot, getOwner, getChannelShowActivities, setChannelShowActivities, getChannelSessionMode } from '../config.js';
3
3
  import { logger } from '../utils/logger.js';
4
4
  import crypto from 'crypto';
5
5
  import path from 'path';
@@ -103,7 +103,7 @@ function formatIdleTime(ms) {
103
103
  return '刚刚';
104
104
  }
105
105
  // 支持的命令列表
106
- const commands = ['/new', '/pwd', '/plist', '/project', '/bind', '/help', '/status', '/restart', '/model', '/effort', '/agent', '/slist', '/session', '/rename', '/stop', '/clear', '/compact', '/repair', '/safe', '/fork', '/del', '/perm', '/send', '/check', '/rewind', '/activity', '/agentmd'];
106
+ const commands = ['/new', '/pwd', '/plist', '/project', '/bind', '/help', '/status', '/restart', '/model', '/effort', '/agent', '/slist', '/session', '/rename', '/stop', '/clear', '/compact', '/repair', '/safe', '/fork', '/del', '/perm', '/file', '/check', '/rewind', '/activity', '/agentmd', '/chatmode'];
107
107
  // 命令别名映射
108
108
  const aliases = {
109
109
  '/p': '/project',
@@ -112,7 +112,7 @@ const aliases = {
112
112
  '/rw': '/rewind'
113
113
  };
114
114
  // 命令快速路径前缀(所有命令都不进入消息队列)
115
- const quickCommandPrefixes = ['/new', '/pwd', '/plist', '/project', '/bind', '/help', '/status', '/restart', '/model', '/effort', '/agent', '/slist', '/session', '/rename', '/repair', '/fork', '/stop', '/clear', '/compact', '/safe', '/del', '/perm', '/send', '/check', '/p ', '/s ', '/name', '/rewind', '/rw', '/rw ', '/activity'];
115
+ const quickCommandPrefixes = ['/new', '/pwd', '/plist', '/project', '/bind', '/help', '/status', '/restart', '/model', '/effort', '/agent', '/slist', '/session', '/rename', '/repair', '/fork', '/stop', '/clear', '/compact', '/safe', '/del', '/perm', '/file', '/check', '/p ', '/s ', '/name', '/rewind', '/rw', '/rw ', '/activity', '/chatmode'];
116
116
  export class CommandHandler {
117
117
  sessionManager;
118
118
  config;
@@ -406,7 +406,7 @@ export class CommandHandler {
406
406
  { cmd: '/restart', label: '重启/重连', desc: '重启服务或重连指定渠道', next: { type: 'select', dynamic: true } },
407
407
  ] : []),
408
408
  ...(isOwner ? [
409
- { cmd: '/send', label: '发送项目内文件', desc: '将项目目录内的文件发送给用户' },
409
+ { cmd: '/file', label: '发送项目内文件', desc: '将项目目录内的文件发送给用户' },
410
410
  { cmd: '/agentmd', label: '管理 agent.md', desc: '查看或更新 AUN 网络上的 agent.md 身份文件', next: { type: 'select', items: [
411
411
  { value: 'put', label: '上传当前', desc: '将本地 agent.md 上传到 AUN 网络' },
412
412
  { value: 'set', label: '直接设置', desc: '输入内容直接更新 agent.md', next: { type: 'text' } },
@@ -529,7 +529,7 @@ export class CommandHandler {
529
529
  }
530
530
  }
531
531
  // 空闲检查:某些命令需要等待当前会话空闲
532
- const requiresIdle = ['/new', '/session', '/clear', '/compact', '/safe', '/repair', '/fork', '/bind', '/project', '/agent', '/rewind'];
532
+ const requiresIdle = ['/new', '/session', '/clear', '/compact', '/safe', '/repair', '/fork', '/bind', '/project', '/agent', '/rewind', '/chatmode'];
533
533
  if (requiresIdle.some(cmd => normalizedContent === cmd || normalizedContent.startsWith(cmd + ' '))) {
534
534
  if (threadId) {
535
535
  // 话题中:检查话题 session 是否在处理(不创建)
@@ -633,7 +633,7 @@ export class CommandHandler {
633
633
  ] : []),
634
634
  ...(isOwner ? [
635
635
  ' /restart - 重启服务',
636
- ' /send [channel] <path> - 发送项目内文件',
636
+ ' /file [channel] <path> - 发送项目内文件',
637
637
  ' /agentmd [put|set <内容>] - 管理 agent.md',
638
638
  ] : []),
639
639
  '',
@@ -1207,6 +1207,10 @@ export class CommandHandler {
1207
1207
  if (normalizedContent === '/activity' || normalizedContent.startsWith('/activity ')) {
1208
1208
  if (!isAdmin)
1209
1209
  return '❌ 无权限:此命令仅限管理员使用';
1210
+ // proactive 模式下流式输出全部静默,activity 配置无意义
1211
+ if (activeSession?.sessionMode === 'proactive') {
1212
+ return '❌ 当前会话为 proactive 模式,不支持 activity 配置(流式输出已全部静默)';
1213
+ }
1210
1214
  const activityArg = normalizedContent.slice(9).trim();
1211
1215
  const modeMap = {
1212
1216
  all: 'all',
@@ -1284,6 +1288,31 @@ export class CommandHandler {
1284
1288
  setChannelShowActivities(this.config, channel, newMode);
1285
1289
  return `✅ 中间输出模式: ${activityArg}(${label})`;
1286
1290
  }
1291
+ // /chatmode 命令:查看/切换 session 会话模式(interactive | proactive)
1292
+ if (normalizedContent === '/chatmode' || normalizedContent.startsWith('/chatmode ')) {
1293
+ if (!isAdmin)
1294
+ return '❌ 无权限:此命令仅限管理员使用';
1295
+ if (!activeSession)
1296
+ return '❌ 当前无活跃会话';
1297
+ const lockedMode = getChannelSessionMode(this.config, channel);
1298
+ const arg = normalizedContent.slice(9).trim();
1299
+ const currentMode = activeSession.sessionMode || 'interactive';
1300
+ if (!arg) {
1301
+ const lockHint = lockedMode ? `(由通道配置锁定为 ${lockedMode})` : '';
1302
+ return `📋 当前会话模式: ${currentMode}${lockHint}\n可选: interactive / proactive\n用法: /chatmode <模式>`;
1303
+ }
1304
+ if (arg !== 'interactive' && arg !== 'proactive') {
1305
+ return `❌ 无效模式: ${arg}\n可选: interactive / proactive`;
1306
+ }
1307
+ if (lockedMode) {
1308
+ return `❌ 会话模式由通道配置锁定为 ${lockedMode},无法切换`;
1309
+ }
1310
+ if (arg === currentMode) {
1311
+ return `📋 当前会话模式已是 ${arg}`;
1312
+ }
1313
+ await this.sessionManager.updateSession(activeSession.id, { sessionMode: arg });
1314
+ return `✅ 会话模式已切换: ${arg}`;
1315
+ }
1287
1316
  // /stop 命令:中断当前任务
1288
1317
  if (normalizedContent === '/stop') {
1289
1318
  const stopResult = await this.ensureSession(channel, channelId, threadId);
@@ -1633,15 +1662,15 @@ export class CommandHandler {
1633
1662
  }
1634
1663
  return `当前项目: ${session.projectPath}`;
1635
1664
  }
1636
- // /send 命令:发送项目内文件,支持 /send path 和 /send channel path(owner only)
1637
- if (normalizedContent.startsWith('/send')) {
1665
+ // /file 命令:发送项目内文件,支持 /file path 和 /file channel path(owner only)
1666
+ if (normalizedContent.startsWith('/file')) {
1638
1667
  if (!isOwner)
1639
1668
  return '❌ 无权限:此命令仅限 owner 使用';
1640
1669
  // 飞书会将 .md 等后缀自动转为 Markdown 链接: foo.md → [foo.md](http://foo.md/)
1641
1670
  // 还原: 将 [text](url) 替换为 text
1642
1671
  const rawArg = normalizedContent.slice(5).trim().replace(/\[([^\]]+)\]\([^)]+\)/g, '$1');
1643
1672
  if (!rawArg) {
1644
- return '用法: /send <相对路径> 或 /send <渠道> <相对路径>\n示例: /send src/index.ts\n示例: /send feishu report.md';
1673
+ return '用法: /file <相对路径> 或 /file <渠道> <相对路径>\n示例: /file src/index.ts\n示例: /file feishu report.md';
1645
1674
  }
1646
1675
  // 解析目标通道:第一个 token 按实例名匹配,再按 channelType 匹配
1647
1676
  const tokens = rawArg.split(/\s+/);
@@ -1733,7 +1762,7 @@ export class CommandHandler {
1733
1762
  : `✅ 已发送: ${filePath} (${sizeStr})`;
1734
1763
  }
1735
1764
  catch (error) {
1736
- logger.error('[CommandHandler] /send failed:', error);
1765
+ logger.error('[CommandHandler] /file failed:', error);
1737
1766
  return `❌ 文件发送失败: ${error.message || error}`;
1738
1767
  }
1739
1768
  }
@@ -2572,8 +2601,24 @@ export class CommandHandler {
2572
2601
  static CTL_COMMANDS = [
2573
2602
  '/help', '/status', '/check',
2574
2603
  '/model', '/effort', '/perm',
2575
- '/compact', '/activity', '/send', '/restart', '/agentmd',
2604
+ '/compact', '/activity', '/file', '/send', '/chatmode', '/restart', '/agentmd',
2576
2605
  ];
2606
+ /**
2607
+ * 从 session 恢复 ReplyContext,用于 ctl send 主动发送文本时的路由
2608
+ * - 群聊话题:metadata.replyContext.{threadId,peerId}
2609
+ * - 私聊:metadata.peerId
2610
+ */
2611
+ buildCtlReplyContext(session) {
2612
+ const ctx = {};
2613
+ const meta = session.metadata;
2614
+ if (meta?.replyContext?.threadId)
2615
+ ctx.threadId = meta.replyContext.threadId;
2616
+ if (meta?.replyContext?.peerId)
2617
+ ctx.peerId = meta.replyContext.peerId;
2618
+ if (!ctx.peerId && meta?.peerId)
2619
+ ctx.peerId = meta.peerId;
2620
+ return Object.keys(ctx).length > 0 ? ctx : undefined;
2621
+ }
2577
2622
  /**
2578
2623
  * Agent ctl 入口:通过 IPC 接收 Agent 自主管理指令
2579
2624
  * 复用现有 slash cmd 逻辑,权限继承 session 用户角色
@@ -2591,8 +2636,25 @@ export class CommandHandler {
2591
2636
  }
2592
2637
  // 3. 从 session.metadata.peerId 获取 userId(用于权限判断)
2593
2638
  const userId = session.metadata?.peerId;
2594
- // 4. send 路径限制:只允许 projectPath 下的文件
2595
- if (cmd.startsWith('/send')) {
2639
+ // 4. /send 文本消息:直接通过 adapter 主动发送,不走 handle()
2640
+ if (cmd.startsWith('/send ') || cmd === '/send') {
2641
+ const text = cmd.startsWith('/send ') ? cmd.slice(6).trim() : '';
2642
+ if (!text)
2643
+ return { ok: false, error: '消息内容不能为空' };
2644
+ const adapter = this.adapters.get(session.channel);
2645
+ if (!adapter)
2646
+ return { ok: false, error: `adapter 未找到: ${session.channel}` };
2647
+ try {
2648
+ const replyContext = this.buildCtlReplyContext(session);
2649
+ await adapter.sendText(session.channelId, text, replyContext);
2650
+ return { ok: true, result: '已发送' };
2651
+ }
2652
+ catch (err) {
2653
+ return { ok: false, error: err.message || String(err) };
2654
+ }
2655
+ }
2656
+ // 5. file 路径限制:只允许 projectPath 下的文件
2657
+ if (cmd.startsWith('/file')) {
2596
2658
  const sendArgs = cmd.slice(5).trim();
2597
2659
  const parts = sendArgs.split(/\s+/);
2598
2660
  const filePath = parts[parts.length - 1];
@@ -2603,7 +2665,7 @@ export class CommandHandler {
2603
2665
  }
2604
2666
  }
2605
2667
  }
2606
- // 5. 调用现有 handle(),不传 sendMessage 回调(结果直接返回)
2668
+ // 6. 调用现有 handle(),不传 sendMessage 回调(结果直接返回)
2607
2669
  try {
2608
2670
  const result = await this.handle(cmd, session.channel, session.channelId, undefined, // 不发送消息
2609
2671
  userId);