evolclaw 2.8.3 → 3.1.0
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 +21 -12
- package/bin/ec.js +29 -0
- package/dist/agents/baseagent-normalize.js +19 -0
- package/dist/agents/claude-runner.js +108 -46
- package/dist/agents/codex-runner.js +13 -14
- package/dist/agents/gemini-runner.js +15 -17
- package/dist/agents/kit-renderer.js +281 -0
- package/dist/agents/resolve.js +134 -0
- package/dist/aun/aid/agentmd.js +186 -0
- package/dist/aun/aid/client.js +134 -0
- package/dist/aun/aid/identity.js +159 -0
- package/dist/aun/aid/index.js +3 -0
- package/dist/aun/aid/lifecycle-log.js +33 -0
- package/dist/aun/aid/types.js +1 -0
- package/dist/aun/aid/validation.js +21 -0
- package/dist/aun/msg/group.js +293 -0
- package/dist/aun/msg/index.js +4 -0
- package/dist/aun/msg/p2p.js +147 -0
- package/dist/aun/msg/payload-type.js +27 -0
- package/dist/aun/msg/upload.js +98 -0
- package/dist/aun/outbox.js +138 -0
- package/dist/aun/rpc/caller.js +42 -0
- package/dist/aun/rpc/connection.js +34 -0
- package/dist/aun/rpc/index.js +2 -0
- package/dist/aun/storage/download.js +29 -0
- package/dist/aun/storage/index.js +3 -0
- package/dist/aun/storage/manage.js +10 -0
- package/dist/aun/storage/upload.js +35 -0
- package/dist/channels/aun.js +1340 -349
- package/dist/channels/dingtalk.js +59 -5
- package/dist/channels/feishu.js +381 -32
- package/dist/channels/qqbot.js +68 -12
- package/dist/channels/wechat.js +63 -4
- package/dist/channels/wecom.js +59 -5
- package/dist/cli/agent.js +800 -0
- package/dist/cli/bench.js +1219 -0
- package/dist/cli/index.js +4513 -0
- package/dist/{utils → cli}/init-channel.js +211 -621
- package/dist/cli/init.js +178 -0
- package/dist/cli/link-rules.js +245 -0
- package/dist/cli/net-check.js +640 -0
- package/dist/cli/watch-msg.js +589 -0
- package/dist/config-store.js +645 -0
- package/dist/core/{agent-loader.js → baseagent-loader.js} +6 -12
- package/dist/core/channel-loader.js +176 -12
- package/dist/core/command-handler.js +883 -848
- package/dist/core/evolagent-registry.js +191 -371
- package/dist/core/evolagent.js +202 -238
- package/dist/core/interaction-router.js +52 -5
- package/dist/core/message/im-renderer.js +486 -0
- package/dist/core/message/items-formatter.js +68 -0
- package/dist/core/message/message-bridge.js +109 -56
- package/dist/core/message/message-log.js +93 -0
- package/dist/core/message/message-processor.js +430 -212
- package/dist/core/message/message-queue.js +13 -6
- package/dist/core/permission.js +116 -11
- package/dist/core/session/adapters/codex-session-file-adapter.js +24 -2
- package/dist/core/session/session-fs-store.js +230 -0
- package/dist/core/session/session-manager.js +740 -777
- package/dist/core/session/session-mapper.js +87 -0
- package/dist/core/trigger/manager.js +122 -0
- package/dist/core/trigger/parser.js +128 -0
- package/dist/core/trigger/scheduler.js +224 -0
- package/dist/data/error-dict.json +118 -0
- package/dist/eck/baseagent-caps.js +18 -0
- package/dist/eck/detect.js +47 -0
- package/dist/eck/init.js +77 -0
- package/dist/eck/rules-loader.js +28 -0
- package/dist/index.js +560 -283
- package/dist/ipc.js +49 -0
- package/dist/net-check.js +640 -0
- package/dist/paths.js +73 -9
- package/dist/types.js +8 -2
- package/dist/utils/aid-lifecycle-log.js +33 -0
- package/dist/utils/atomic-write.js +89 -0
- package/dist/utils/channel-helpers.js +46 -0
- package/dist/utils/cross-platform.js +17 -26
- package/dist/utils/error-utils.js +10 -2
- package/dist/utils/instance-registry.js +434 -0
- package/dist/utils/log-writer.js +217 -0
- package/dist/utils/logger.js +34 -77
- package/dist/utils/media-cache.js +23 -0
- package/dist/utils/npm-ops.js +163 -0
- package/dist/utils/process-introspect.js +122 -0
- package/dist/utils/stats.js +192 -0
- package/dist/watch-msg.js +544 -0
- package/evolclaw-install-aun.md +127 -47
- package/kits/docs/GUIDE.md +20 -0
- package/kits/docs/INDEX.md +52 -0
- package/kits/docs/aun/CHEATSHEET.md +17 -0
- package/kits/docs/aun/SYNC_PROTOCOL.md +15 -0
- package/kits/docs/channels/aun.md +25 -0
- package/kits/docs/channels/feishu.md +27 -0
- package/kits/docs/eck_templates/GUIDE.template.md +22 -0
- package/kits/docs/eck_templates/INDEX.template.md +28 -0
- package/kits/docs/eck_templates/path-registry.template.md +33 -0
- package/kits/docs/eck_templates/runtime.template.md +19 -0
- package/kits/docs/evolclaw/AGENT_CMD.md +31 -0
- package/kits/docs/evolclaw/MSG_GROUP.md +30 -0
- package/kits/docs/evolclaw/MSG_PRIVATE.md +25 -0
- package/kits/docs/evolclaw/self-summary.md +29 -0
- package/kits/docs/evolclaw/tools.md +25 -0
- package/kits/docs/identity/AID_PROFILE_SPEC.md +27 -0
- package/kits/docs/identity/PATH_OPS.md +16 -0
- package/kits/docs/identity/ROLE_DETAIL.md +20 -0
- package/kits/docs/identity/identity-tools.md +26 -0
- package/kits/docs/path-registry.md +43 -0
- package/kits/eck_manifest.json +95 -0
- package/kits/rules/01-overview.md +120 -0
- package/kits/rules/02-navigation.md +75 -0
- package/kits/rules/03-identity.md +34 -0
- package/kits/rules/04-relation.md +49 -0
- package/kits/rules/05-venue.md +45 -0
- package/kits/rules/06-channel.md +43 -0
- package/kits/templates/system-fragments/baseagent.md +2 -0
- package/kits/templates/system-fragments/channel.md +10 -0
- package/kits/templates/system-fragments/identity.md +12 -0
- package/kits/templates/system-fragments/relation.md +9 -0
- package/kits/templates/system-fragments/runtime.md +19 -0
- package/kits/templates/system-fragments/venue.md +5 -0
- package/package.json +10 -6
- package/data/evolclaw.sample.json +0 -60
- package/dist/agents/templates.js +0 -122
- package/dist/channels/aun-ops.js +0 -275
- package/dist/cli.js +0 -2178
- package/dist/config.js +0 -591
- package/dist/core/agent-registry.js +0 -450
- package/dist/core/evolagent-schema.js +0 -72
- package/dist/core/message/stream-flusher.js +0 -238
- package/dist/core/message/thought-emitter.js +0 -162
- package/dist/core/reload-hooks.js +0 -87
- package/dist/prompts/templates.js +0 -122
- package/dist/templates/prompts.md +0 -104
- package/dist/templates/skills.md +0 -66
- package/dist/utils/channel-fingerprint.js +0 -59
- package/dist/utils/error-dict.js +0 -63
- package/dist/utils/format.js +0 -32
- package/dist/utils/init.js +0 -645
- package/dist/utils/migrate-project.js +0 -122
- package/dist/utils/reload-hooks.js +0 -87
- package/dist/utils/stats-collector.js +0 -99
- package/dist/utils/upgrade.js +0 -100
|
@@ -1,11 +1,16 @@
|
|
|
1
1
|
import { DEFAULT_PERMISSION_MODE } from '../types.js';
|
|
2
2
|
import { hasModelSwitcher, hasPermissionController } from '../agents/claude-runner.js';
|
|
3
|
-
import {
|
|
3
|
+
import { renderCommandCardAsText } from './interaction-router.js';
|
|
4
|
+
import { buildEnvelope, sendInteractionPayload } from './message/message-processor.js';
|
|
5
|
+
import { resolvePaths, getPackageRoot } from '../paths.js';
|
|
4
6
|
import { logger } from '../utils/logger.js';
|
|
5
7
|
import crypto from 'crypto';
|
|
6
8
|
import path from 'path';
|
|
7
9
|
import fs from 'fs';
|
|
8
10
|
import os from 'os';
|
|
11
|
+
import { parseTriggerSet } from './trigger/parser.js';
|
|
12
|
+
import { calcNextFireAt } from './trigger/scheduler.js';
|
|
13
|
+
import { checkLatestVersion, getLocalVersion, isLinkedInstall, compareVersions } from '../utils/npm-ops.js';
|
|
9
14
|
const allEfforts = ['low', 'medium', 'high', 'max'];
|
|
10
15
|
const nonMaxEfforts = allEfforts.filter(e => e !== 'max');
|
|
11
16
|
function getAvailableEfforts(agent, model) {
|
|
@@ -104,7 +109,7 @@ function formatIdleTime(ms) {
|
|
|
104
109
|
return '刚刚';
|
|
105
110
|
}
|
|
106
111
|
// 支持的命令列表
|
|
107
|
-
const commands = ['/new', '/pwd', '/plist', '/project', '/bind', '/help', '/evolhelp', '/status', '/restart', '/model', '/setmodel', '/effort', '/agent', '/slist', '/session', '/rename', '/stop', '/clear', '/compact', '/repair', '/safe', '/fork', '/del', '/perm', '/file', '/check', '/rewind', '/activity', '/aid', '/
|
|
112
|
+
const commands = ['/new', '/pwd', '/plist', '/project', '/bind', '/help', '/evolhelp', '/status', '/restart', '/model', '/setmodel', '/effort', '/agent', '/slist', '/session', '/rename', '/stop', '/clear', '/compact', '/repair', '/safe', '/fork', '/del', '/perm', '/file', '/check', '/rewind', '/activity', '/chatmode', '/dispatch', '/ask', '/resume', '/aid', '/rpc', '/storage', '/trigger', '/upgrade'];
|
|
108
113
|
// 命令别名映射
|
|
109
114
|
const aliases = {
|
|
110
115
|
'/p': '/project',
|
|
@@ -113,10 +118,9 @@ const aliases = {
|
|
|
113
118
|
'/rw': '/rewind'
|
|
114
119
|
};
|
|
115
120
|
// 命令快速路径前缀(所有命令都不进入消息队列)
|
|
116
|
-
const quickCommandPrefixes = ['/new', '/pwd', '/plist', '/project', '/bind', '/help', '/evolhelp', '/status', '/restart', '/model', '/setmodel', '/effort', '/agent', '/slist', '/session', '/rename', '/repair', '/fork', '/stop', '/clear', '/compact', '/safe', '/del', '/perm', '/file', '/check', '/p ', '/s ', '/name', '/rewind', '/rw', '/rw ', '/activity', '/chatmode', '/aid', '/
|
|
121
|
+
const quickCommandPrefixes = ['/new', '/pwd', '/plist', '/project', '/bind', '/help', '/evolhelp', '/status', '/restart', '/model', '/setmodel', '/effort', '/agent', '/slist', '/session', '/rename', '/repair', '/fork', '/stop', '/clear', '/compact', '/safe', '/del', '/perm', '/file', '/check', '/p ', '/s ', '/name', '/rewind', '/rw', '/rw ', '/activity', '/chatmode', '/dispatch', '/ask', '/resume', '/aid', '/rpc', '/storage', '/trigger', '/upgrade'];
|
|
117
122
|
export class CommandHandler {
|
|
118
123
|
sessionManager;
|
|
119
|
-
config;
|
|
120
124
|
messageCache;
|
|
121
125
|
eventBus;
|
|
122
126
|
adapters = new Map();
|
|
@@ -129,8 +133,10 @@ export class CommandHandler {
|
|
|
129
133
|
interactionRouter;
|
|
130
134
|
statsCollector;
|
|
131
135
|
agentMap;
|
|
132
|
-
|
|
136
|
+
primaryRunnerKey;
|
|
133
137
|
agentRegistry;
|
|
138
|
+
triggerScheduler;
|
|
139
|
+
triggerManager;
|
|
134
140
|
/**
|
|
135
141
|
* Get the runner for a (channel, baseagent) pair.
|
|
136
142
|
*
|
|
@@ -139,18 +145,18 @@ export class CommandHandler {
|
|
|
139
145
|
*/
|
|
140
146
|
getAgent(channel, baseagent) {
|
|
141
147
|
if (channel && baseagent) {
|
|
142
|
-
const evolName = this.agentRegistry?.resolveByChannel(channel)?.name || '
|
|
148
|
+
const evolName = this.agentRegistry?.resolveByChannel(channel)?.name || '<unknown>';
|
|
143
149
|
const key = `${evolName}::${baseagent}`;
|
|
144
150
|
if (this.agentMap.has(key))
|
|
145
151
|
return this.agentMap.get(key);
|
|
146
152
|
}
|
|
147
|
-
if (this.agentMap.has(this.
|
|
148
|
-
return this.agentMap.get(this.
|
|
153
|
+
if (this.agentMap.has(this.primaryRunnerKey))
|
|
154
|
+
return this.agentMap.get(this.primaryRunnerKey);
|
|
149
155
|
return this.agentMap.values().next().value;
|
|
150
156
|
}
|
|
151
157
|
/** Return the list of baseagents available to a given channel (per-EvolAgent isolation). */
|
|
152
158
|
getAvailableBaseagents(channel) {
|
|
153
|
-
const evolName = this.agentRegistry?.resolveByChannel(channel)?.name || '
|
|
159
|
+
const evolName = this.agentRegistry?.resolveByChannel(channel)?.name || '<unknown>';
|
|
154
160
|
const prefix = `${evolName}::`;
|
|
155
161
|
const result = [];
|
|
156
162
|
for (const key of this.agentMap.keys()) {
|
|
@@ -159,48 +165,49 @@ export class CommandHandler {
|
|
|
159
165
|
}
|
|
160
166
|
return result;
|
|
161
167
|
}
|
|
162
|
-
/** Extract the baseagent component from `
|
|
168
|
+
/** Extract the baseagent component from `primaryRunnerKey` (e.g. `aid::claude` → `claude`). */
|
|
163
169
|
parseDefaultBaseagent() {
|
|
164
|
-
const idx = this.
|
|
165
|
-
return idx >= 0 ? this.
|
|
170
|
+
const idx = this.primaryRunnerKey.indexOf('::');
|
|
171
|
+
return idx >= 0 ? this.primaryRunnerKey.slice(idx + 2) : this.primaryRunnerKey;
|
|
166
172
|
}
|
|
167
|
-
constructor(sessionManager, agentRunnerOrMap,
|
|
173
|
+
constructor(sessionManager, agentRunnerOrMap, messageCache, eventBus, primaryRunnerKey) {
|
|
168
174
|
this.sessionManager = sessionManager;
|
|
169
|
-
this.config = config;
|
|
170
175
|
this.messageCache = messageCache;
|
|
171
176
|
this.eventBus = eventBus;
|
|
172
177
|
if (agentRunnerOrMap instanceof Map) {
|
|
173
178
|
this.agentMap = agentRunnerOrMap;
|
|
174
|
-
this.
|
|
179
|
+
this.primaryRunnerKey = primaryRunnerKey || '<unknown>::claude';
|
|
175
180
|
}
|
|
176
181
|
else {
|
|
177
|
-
//
|
|
178
|
-
this.agentMap = new Map([[
|
|
179
|
-
this.
|
|
182
|
+
// 测试 / 单 runner 路径:占位 agent name 用 '<unknown>'
|
|
183
|
+
this.agentMap = new Map([[`<unknown>::${agentRunnerOrMap.name}`, agentRunnerOrMap]]);
|
|
184
|
+
this.primaryRunnerKey = `<unknown>::${agentRunnerOrMap.name}`;
|
|
180
185
|
}
|
|
181
186
|
}
|
|
182
187
|
/** 注入 EvolAgentRegistry,用于判断通道是否被 EvolAgent 管理 */
|
|
183
188
|
setAgentRegistry(registry) {
|
|
184
189
|
this.agentRegistry = registry;
|
|
185
190
|
}
|
|
186
|
-
/**
|
|
191
|
+
/** 注入触发器调度器(由 index.ts 在初始化后调用) */
|
|
192
|
+
setTriggerScheduler(scheduler, manager) {
|
|
193
|
+
this.triggerScheduler = scheduler;
|
|
194
|
+
this.triggerManager = manager;
|
|
195
|
+
}
|
|
196
|
+
/** 返回管理当前通道的 EvolAgent,无则返回 null */
|
|
187
197
|
getOwningAgent(channel) {
|
|
188
198
|
if (!this.agentRegistry)
|
|
189
199
|
return null;
|
|
190
|
-
|
|
191
|
-
if (!agent || agent.isDefault)
|
|
192
|
-
return null;
|
|
193
|
-
return agent;
|
|
200
|
+
return this.agentRegistry.resolveByChannel(channel);
|
|
194
201
|
}
|
|
195
|
-
/**
|
|
202
|
+
/** 返回当前通道的有效项目路径:从 owning agent 取。*/
|
|
196
203
|
getEffectiveDefaultPath(channel) {
|
|
197
204
|
const owning = this.getOwningAgent(channel);
|
|
198
205
|
if (owning)
|
|
199
206
|
return owning.projectPath;
|
|
200
|
-
return
|
|
207
|
+
return process.cwd();
|
|
201
208
|
}
|
|
202
209
|
/**
|
|
203
|
-
* 返回当前通道有效的 projects.list
|
|
210
|
+
* 返回当前通道有效的 projects.list(从 owning agent 的 config 取)。
|
|
204
211
|
* 都没配 list 时回退到 defaultPath 单项目。
|
|
205
212
|
*/
|
|
206
213
|
getEffectiveProjects(channel) {
|
|
@@ -211,39 +218,24 @@ export class CommandHandler {
|
|
|
211
218
|
return this.projects;
|
|
212
219
|
}
|
|
213
220
|
/**
|
|
214
|
-
*
|
|
221
|
+
* 添加项目到当前通道范围(写到 owning agent 的 config.json)。
|
|
215
222
|
*/
|
|
216
223
|
async addProjectInScope(channel, name, projectPath) {
|
|
217
224
|
const owning = this.getOwningAgent(channel);
|
|
218
|
-
if (owning) {
|
|
219
|
-
|
|
220
|
-
owning.addProject(name, projectPath);
|
|
221
|
-
}
|
|
222
|
-
catch (e) {
|
|
223
|
-
return `⚠️ 写入 agent.json 失败: ${e?.message || e}`;
|
|
224
|
-
}
|
|
225
|
-
return undefined;
|
|
225
|
+
if (!owning) {
|
|
226
|
+
return `⚠️ 找不到通道 "${channel}" 所属的 self-agent`;
|
|
226
227
|
}
|
|
227
|
-
if (!this.config.projects) {
|
|
228
|
-
this.config.projects = { defaultPath: process.cwd(), autoCreate: false, list: {} };
|
|
229
|
-
}
|
|
230
|
-
if (!this.config.projects.list) {
|
|
231
|
-
this.config.projects.list = {};
|
|
232
|
-
}
|
|
233
|
-
this.config.projects.list[name] = projectPath;
|
|
234
228
|
try {
|
|
235
|
-
|
|
236
|
-
saveConfig(this.config);
|
|
229
|
+
owning.addProject(name, projectPath);
|
|
237
230
|
}
|
|
238
231
|
catch (e) {
|
|
239
|
-
return `⚠️ 写入
|
|
232
|
+
return `⚠️ 写入 agent config 失败: ${e?.message || e}`;
|
|
240
233
|
}
|
|
241
|
-
// Refresh in-memory list cache (this.projects getter reads from this.config)
|
|
242
234
|
return undefined;
|
|
243
235
|
}
|
|
244
236
|
/**
|
|
245
|
-
* 持久化 baseagent.model
|
|
246
|
-
*
|
|
237
|
+
* 持久化 baseagent.model:写到 agent config.json;找不到 owning agent 时
|
|
238
|
+
* 退到用户级 ~/.claude/settings.json(Claude 专用)。
|
|
247
239
|
*/
|
|
248
240
|
persistBaseagentModel(channel, baseagentName, newModel) {
|
|
249
241
|
const owning = this.getOwningAgent(channel);
|
|
@@ -252,42 +244,14 @@ export class CommandHandler {
|
|
|
252
244
|
owning.setBaseagentModel(newModel);
|
|
253
245
|
}
|
|
254
246
|
catch (e) {
|
|
255
|
-
return `⚠️ 写入 agent
|
|
247
|
+
return `⚠️ 写入 agent config 失败: ${e?.message || e}`;
|
|
256
248
|
}
|
|
257
249
|
return undefined;
|
|
258
250
|
}
|
|
259
|
-
//
|
|
260
|
-
if (
|
|
261
|
-
|
|
262
|
-
const isCodex = baseagentName === 'codex';
|
|
263
|
-
if (isCodex) {
|
|
264
|
-
if (!this.config.agents.codex)
|
|
265
|
-
this.config.agents.codex = {};
|
|
266
|
-
if (newModel)
|
|
267
|
-
this.config.agents.codex.model = newModel;
|
|
268
|
-
try {
|
|
269
|
-
saveConfig(this.config);
|
|
270
|
-
}
|
|
271
|
-
catch (e) {
|
|
272
|
-
return `⚠️ 写入 evolclaw.json 失败: ${e.message}`;
|
|
273
|
-
}
|
|
274
|
-
return undefined;
|
|
251
|
+
// 无 owning agent(罕见,新结构下应当不会发生)→ 仅 Claude 走用户级 fallback
|
|
252
|
+
if (baseagentName !== 'claude') {
|
|
253
|
+
return `⚠️ 找不到通道 "${channel}" 所属的 self-agent`;
|
|
275
254
|
}
|
|
276
|
-
const configuredInEvolclaw = !!(this.config.agents?.claude?.model || this.config.agents?.claude?.effort);
|
|
277
|
-
if (configuredInEvolclaw) {
|
|
278
|
-
if (!this.config.agents.claude)
|
|
279
|
-
this.config.agents.claude = {};
|
|
280
|
-
if (newModel)
|
|
281
|
-
this.config.agents.claude.model = newModel;
|
|
282
|
-
try {
|
|
283
|
-
saveConfig(this.config);
|
|
284
|
-
}
|
|
285
|
-
catch (e) {
|
|
286
|
-
return `⚠️ 写入 evolclaw.json 失败: ${e.message}`;
|
|
287
|
-
}
|
|
288
|
-
return undefined;
|
|
289
|
-
}
|
|
290
|
-
// Fallback: ~/.claude/settings.json
|
|
291
255
|
const updates = {};
|
|
292
256
|
if (newModel)
|
|
293
257
|
updates.model = newModel;
|
|
@@ -298,7 +262,7 @@ export class CommandHandler {
|
|
|
298
262
|
return undefined;
|
|
299
263
|
}
|
|
300
264
|
/**
|
|
301
|
-
* 持久化 baseagent.effort
|
|
265
|
+
* 持久化 baseagent.effort:写到 agent config.json;找不到时退到用户级 settings。
|
|
302
266
|
*/
|
|
303
267
|
persistBaseagentEffort(channel, baseagentName, newEffort) {
|
|
304
268
|
const owning = this.getOwningAgent(channel);
|
|
@@ -307,57 +271,12 @@ export class CommandHandler {
|
|
|
307
271
|
owning.setBaseagentEffort(newEffort);
|
|
308
272
|
}
|
|
309
273
|
catch (e) {
|
|
310
|
-
return `⚠️ 写入 agent
|
|
274
|
+
return `⚠️ 写入 agent config 失败: ${e?.message || e}`;
|
|
311
275
|
}
|
|
312
276
|
return undefined;
|
|
313
277
|
}
|
|
314
|
-
if (
|
|
315
|
-
|
|
316
|
-
const isCodex = baseagentName === 'codex';
|
|
317
|
-
if (isCodex) {
|
|
318
|
-
if (newEffort === undefined) {
|
|
319
|
-
if (this.config.agents.codex?.reasoning) {
|
|
320
|
-
delete this.config.agents.codex.reasoning;
|
|
321
|
-
try {
|
|
322
|
-
saveConfig(this.config);
|
|
323
|
-
}
|
|
324
|
-
catch { }
|
|
325
|
-
}
|
|
326
|
-
}
|
|
327
|
-
else {
|
|
328
|
-
if (!this.config.agents.codex)
|
|
329
|
-
this.config.agents.codex = {};
|
|
330
|
-
this.config.agents.codex.reasoning = newEffort;
|
|
331
|
-
try {
|
|
332
|
-
saveConfig(this.config);
|
|
333
|
-
}
|
|
334
|
-
catch (e) {
|
|
335
|
-
return `⚠️ 写入 evolclaw.json 失败: ${e.message}`;
|
|
336
|
-
}
|
|
337
|
-
}
|
|
338
|
-
return undefined;
|
|
339
|
-
}
|
|
340
|
-
const configuredInEvolclaw = !!(this.config.agents?.claude?.model || this.config.agents?.claude?.effort);
|
|
341
|
-
if (configuredInEvolclaw) {
|
|
342
|
-
if (newEffort === undefined) {
|
|
343
|
-
delete this.config.agents.claude.effort;
|
|
344
|
-
try {
|
|
345
|
-
saveConfig(this.config);
|
|
346
|
-
}
|
|
347
|
-
catch { }
|
|
348
|
-
}
|
|
349
|
-
else {
|
|
350
|
-
if (!this.config.agents.claude)
|
|
351
|
-
this.config.agents.claude = {};
|
|
352
|
-
this.config.agents.claude.effort = newEffort;
|
|
353
|
-
try {
|
|
354
|
-
saveConfig(this.config);
|
|
355
|
-
}
|
|
356
|
-
catch (e) {
|
|
357
|
-
return `⚠️ 写入 evolclaw.json 失败: ${e.message}`;
|
|
358
|
-
}
|
|
359
|
-
}
|
|
360
|
-
return undefined;
|
|
278
|
+
if (baseagentName !== 'claude') {
|
|
279
|
+
return `⚠️ 找不到通道 "${channel}" 所属的 self-agent`;
|
|
361
280
|
}
|
|
362
281
|
const updates = { effortLevel: newEffort ?? null };
|
|
363
282
|
const writeResult = writeUserSettings(updates);
|
|
@@ -366,14 +285,8 @@ export class CommandHandler {
|
|
|
366
285
|
}
|
|
367
286
|
return undefined;
|
|
368
287
|
}
|
|
369
|
-
/**
|
|
288
|
+
/** 项目列表快捷访问(无 channel 上下文时的 fallback,尽量不用) */
|
|
370
289
|
get projects() {
|
|
371
|
-
const list = this.config.projects?.list;
|
|
372
|
-
if (list && Object.keys(list).length > 0)
|
|
373
|
-
return list;
|
|
374
|
-
const dp = this.config.projects?.defaultPath;
|
|
375
|
-
if (dp)
|
|
376
|
-
return { [path.basename(dp)]: dp };
|
|
377
290
|
return {};
|
|
378
291
|
}
|
|
379
292
|
/** 根据项目路径查找配置中的项目名称 */
|
|
@@ -412,72 +325,62 @@ export class CommandHandler {
|
|
|
412
325
|
return session.metadata?.replyContext;
|
|
413
326
|
}
|
|
414
327
|
/**
|
|
415
|
-
*
|
|
416
|
-
*
|
|
328
|
+
* 发送 CommandCard 卡片。卡片成功返回 null(调用方直接 return),失败返回降级文本。
|
|
329
|
+
* CommandCard 不进 InteractionRouter,按钮点击由 channel 直接构造伪命令入站消息。
|
|
330
|
+
*
|
|
331
|
+
* 走统一 adapter.send(envelope, { kind: 'interaction', ... }) 入口。
|
|
417
332
|
*/
|
|
418
|
-
async
|
|
419
|
-
const adapter = this.adapters.get(channel);
|
|
420
|
-
if (
|
|
421
|
-
|
|
333
|
+
async sendCommandCard(opts) {
|
|
334
|
+
const adapter = this.adapters.get(opts.channel);
|
|
335
|
+
if (opts.interaction.kind.kind !== 'command-card') {
|
|
336
|
+
logger.warn(`[CommandHandler] sendCommandCard called with non-CommandCard kind`);
|
|
337
|
+
return null;
|
|
338
|
+
}
|
|
339
|
+
const card = opts.interaction.kind;
|
|
340
|
+
if (opts.canWrite === false)
|
|
341
|
+
return renderCommandCardAsText(card);
|
|
342
|
+
if (!adapter?.send)
|
|
343
|
+
return renderCommandCardAsText(card);
|
|
344
|
+
if (this.isSessionBusy(opts.interaction.sessionId))
|
|
345
|
+
return renderCommandCardAsText(card);
|
|
422
346
|
try {
|
|
423
|
-
|
|
347
|
+
const envelope = buildEnvelope({
|
|
348
|
+
channel: opts.channel,
|
|
349
|
+
channelId: opts.channelId,
|
|
350
|
+
agentName: this.agentRegistry?.resolveByChannel(opts.channel)?.name,
|
|
351
|
+
replyContext: opts.replyCtx,
|
|
352
|
+
});
|
|
353
|
+
const fallbackText = renderCommandCardAsText(card);
|
|
354
|
+
const messageId = await sendInteractionPayload(adapter, envelope, opts.interaction, fallbackText, opts.replyCtx);
|
|
355
|
+
if (messageId)
|
|
356
|
+
return null;
|
|
424
357
|
}
|
|
425
358
|
catch (e) {
|
|
426
|
-
logger.warn(`[CommandHandler]
|
|
427
|
-
return false;
|
|
428
|
-
}
|
|
429
|
-
}
|
|
430
|
-
/** 作废某 session 下所有 pending 交互卡片(PATCH 禁用 + cancel) */
|
|
431
|
-
async invalidateOldCards(channel, sessionId) {
|
|
432
|
-
if (!this.interactionRouter)
|
|
433
|
-
return;
|
|
434
|
-
const adapter = this.adapters.get(channel);
|
|
435
|
-
const pending = this.interactionRouter.getPending(sessionId);
|
|
436
|
-
if (pending.length === 0)
|
|
437
|
-
return;
|
|
438
|
-
const disabledCard = {
|
|
439
|
-
config: { wide_screen_mode: true },
|
|
440
|
-
header: { template: 'grey', title: { tag: 'plain_text', content: '已过期' } },
|
|
441
|
-
elements: [{ tag: 'markdown', content: '此卡片已过期,请查看最新卡片。' }],
|
|
442
|
-
};
|
|
443
|
-
for (const id of pending) {
|
|
444
|
-
const msgId = this.interactionRouter.getMessageId(id);
|
|
445
|
-
if (msgId && adapter?.patchInteractionCard) {
|
|
446
|
-
adapter.patchInteractionCard(msgId, disabledCard).catch(() => { });
|
|
447
|
-
}
|
|
448
|
-
this.interactionRouter.cancel(id);
|
|
359
|
+
logger.warn(`[CommandHandler] sendCommandCard failed: ${e}`);
|
|
449
360
|
}
|
|
361
|
+
return renderCommandCardAsText(card);
|
|
450
362
|
}
|
|
451
363
|
/**
|
|
452
|
-
*
|
|
453
|
-
* 返回 true
|
|
364
|
+
* 通用降级应答入口:按 (sessionId, fallbackCommand) 查找 pending interaction 并路由。
|
|
365
|
+
* 返回 { matched: true } 表示已处理,调用方直接返回 result。
|
|
454
366
|
*/
|
|
455
|
-
async
|
|
367
|
+
async handleInteractionFallback(command, args, sessionId, userId) {
|
|
456
368
|
if (!this.interactionRouter)
|
|
457
|
-
return false;
|
|
458
|
-
|
|
459
|
-
if (
|
|
460
|
-
return false;
|
|
461
|
-
|
|
462
|
-
if (
|
|
463
|
-
return
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
adapter?.sendText(opts.channelId, '⚠️ 当前正在处理消息,请稍后再试\n使用 /stop 中断当前任务后重试', opts.replyCtx);
|
|
473
|
-
return;
|
|
474
|
-
}
|
|
475
|
-
await opts.callback(action, values, operatorId);
|
|
476
|
-
// 已完成交互的卡片:保留原始内容,仅禁用按钮(不标记为"已过期")
|
|
477
|
-
// "已过期"仅用于被新卡片替代的旧卡片(invalidateOldCards)
|
|
478
|
-
};
|
|
479
|
-
this.interactionRouter.register(opts.requestId, opts.sessionId, wrappedCallback, { timeoutMs: 120_000, messageId });
|
|
480
|
-
return true;
|
|
369
|
+
return { matched: false };
|
|
370
|
+
const pendingId = this.interactionRouter.findPendingByCommand(sessionId, command);
|
|
371
|
+
if (!pendingId)
|
|
372
|
+
return { matched: false };
|
|
373
|
+
const initiatorId = this.interactionRouter.getInitiator(pendingId);
|
|
374
|
+
if (initiatorId && userId && initiatorId !== userId) {
|
|
375
|
+
return { matched: true, result: '⚠️ 仅卡片发起者可应答' };
|
|
376
|
+
}
|
|
377
|
+
this.interactionRouter.handle({
|
|
378
|
+
type: 'interaction.response',
|
|
379
|
+
id: pendingId,
|
|
380
|
+
action: args,
|
|
381
|
+
operatorId: userId,
|
|
382
|
+
});
|
|
383
|
+
return { matched: true, result: '✓ 已回答' };
|
|
481
384
|
}
|
|
482
385
|
/** 判断指定 session 是否有活跃流(用于 idle 守卫和卡片降级) */
|
|
483
386
|
isSessionBusy(sessionId) {
|
|
@@ -487,8 +390,8 @@ export class CommandHandler {
|
|
|
487
390
|
}
|
|
488
391
|
return false;
|
|
489
392
|
}
|
|
490
|
-
/**
|
|
491
|
-
async ensureSession(channel, channelId, threadId) {
|
|
393
|
+
/** 获取活跃会话,无会话时自动创建(话题除外) */
|
|
394
|
+
async ensureSession(channel, channelId, threadId, chatType) {
|
|
492
395
|
if (threadId) {
|
|
493
396
|
// 话题会话:仅查询,不创建
|
|
494
397
|
const session = await this.sessionManager.getThreadSession(channel, channelId, threadId);
|
|
@@ -497,9 +400,13 @@ export class CommandHandler {
|
|
|
497
400
|
}
|
|
498
401
|
return { session };
|
|
499
402
|
}
|
|
500
|
-
const
|
|
501
|
-
|
|
502
|
-
|
|
403
|
+
const ct = chatType === 'group' ? 'group' : chatType === 'private' ? 'private' : undefined;
|
|
404
|
+
const session = await this.sessionManager.getActiveSession(channel, channelId)
|
|
405
|
+
?? await this.sessionManager.getOrCreateSession(channel, channelId, this.getEffectiveDefaultPath(channel), undefined, undefined, undefined, undefined, ct);
|
|
406
|
+
// 如果 session 已存在但 chatType 跟传入的不一致,更新
|
|
407
|
+
if (ct && session.chatType !== ct) {
|
|
408
|
+
await this.sessionManager.updateSession(session.id, { chatType: ct });
|
|
409
|
+
session.chatType = ct;
|
|
503
410
|
}
|
|
504
411
|
return { session };
|
|
505
412
|
}
|
|
@@ -617,6 +524,10 @@ export class CommandHandler {
|
|
|
617
524
|
{ value: 'interactive', label: '交互模式', desc: '仅在收到消息时响应' },
|
|
618
525
|
{ value: 'proactive', label: '主动模式', desc: 'Agent 可主动推进任务' },
|
|
619
526
|
] } },
|
|
527
|
+
{ cmd: '/dispatch', label: '切换分发模式', desc: '控制群聊消息过滤(仅@提及或广播响应)', next: { type: 'select', items: [
|
|
528
|
+
{ value: 'mention', label: '@ 提及', desc: '仅在被 @ 提及时响应' },
|
|
529
|
+
{ value: 'broadcast', label: '广播', desc: '响应群内所有消息' },
|
|
530
|
+
] } },
|
|
620
531
|
]
|
|
621
532
|
});
|
|
622
533
|
items.push({
|
|
@@ -654,14 +565,6 @@ export class CommandHandler {
|
|
|
654
565
|
] : []),
|
|
655
566
|
...(isOwner ? [
|
|
656
567
|
{ cmd: '/file', label: '发送项目内文件', desc: '将项目目录内的文件发送给用户' },
|
|
657
|
-
{ cmd: '/aid', label: 'AID 身份管理', desc: '管理本地 AID 身份(创建/列表)', next: { type: 'select', items: [
|
|
658
|
-
{ value: 'list', label: '列表', desc: '列出本地所有 AID' },
|
|
659
|
-
{ value: 'new', label: '创建', desc: '创建新 AID 身份', next: { type: 'text' } },
|
|
660
|
-
] } },
|
|
661
|
-
{ cmd: '/agentmd', label: '管理 agent.md', desc: '查看或更新 AUN 网络上的 agent.md 身份文件', next: { type: 'select', items: [
|
|
662
|
-
{ value: 'put', label: '上传当前', desc: '将本地 agent.md 上传到 AUN 网络' },
|
|
663
|
-
{ value: 'set', label: '直接设置', desc: '输入内容直接更新 agent.md', next: { type: 'text' } },
|
|
664
|
-
] } },
|
|
665
568
|
] : []),
|
|
666
569
|
]
|
|
667
570
|
});
|
|
@@ -708,7 +611,7 @@ export class CommandHandler {
|
|
|
708
611
|
}
|
|
709
612
|
if (cmd === '/p') {
|
|
710
613
|
// Use agent-scoped project list: agent-owned channels see their agent.json's
|
|
711
|
-
// projects.list; default channel sees
|
|
614
|
+
// projects.list; default channel sees agent config's projects.list
|
|
712
615
|
const list = this.getEffectiveProjects(channel);
|
|
713
616
|
return Object.entries(list).map(([name, path]) => ({ value: name, label: name, desc: path }));
|
|
714
617
|
}
|
|
@@ -725,10 +628,6 @@ export class CommandHandler {
|
|
|
725
628
|
return null;
|
|
726
629
|
}
|
|
727
630
|
if (cmd === '/restart') {
|
|
728
|
-
// /restart 是服务级操作(重连/重启进程),仅限 default 通道。
|
|
729
|
-
// EvolAgent 通道返回空菜单(用户在 agent-owned 通道上无可选项)
|
|
730
|
-
if (this.getOwningAgent(channel))
|
|
731
|
-
return [];
|
|
732
631
|
const isOwner = userId ? this.sessionManager.resolveIdentity(channel, userId).role === 'owner' : false;
|
|
733
632
|
// 列出所有 channel type
|
|
734
633
|
const visibleTypes = new Set();
|
|
@@ -791,6 +690,27 @@ export class CommandHandler {
|
|
|
791
690
|
return { error: '无权限:群聊中仅管理员可切换' };
|
|
792
691
|
}
|
|
793
692
|
await this.sessionManager.updateSession(session.id, { sessionMode: arg });
|
|
693
|
+
this.eventBus.publish({ type: 'session:chat-mode-changed', sessionId: session.id, mode: arg, timestamp: Date.now() });
|
|
694
|
+
return { data: { mode: arg } };
|
|
695
|
+
}
|
|
696
|
+
if (cmdBase === '/dispatch') {
|
|
697
|
+
const currentMode = session.metadata?.dispatchMode;
|
|
698
|
+
if (mode === 'query') {
|
|
699
|
+
return { data: { mode: currentMode ?? null } };
|
|
700
|
+
}
|
|
701
|
+
// update
|
|
702
|
+
if (!arg)
|
|
703
|
+
return { error: '缺少目标模式' };
|
|
704
|
+
if (arg !== 'mention' && arg !== 'broadcast')
|
|
705
|
+
return { error: `无效模式: ${arg}` };
|
|
706
|
+
const identity = this.sessionManager.resolveIdentity(channel, userId);
|
|
707
|
+
const chatType = session.chatType || 'private';
|
|
708
|
+
if (chatType === 'group' && identity.role !== 'owner' && identity.role !== 'admin') {
|
|
709
|
+
return { error: '无权限:群聊中仅管理员可切换' };
|
|
710
|
+
}
|
|
711
|
+
const metadata = { ...(session.metadata || {}), dispatchMode: arg };
|
|
712
|
+
await this.sessionManager.updateSession(session.id, { metadata });
|
|
713
|
+
this.eventBus.publish({ type: 'session:dispatch-mode-changed', sessionId: session.id, mode: arg, timestamp: Date.now() });
|
|
794
714
|
return { data: { mode: arg } };
|
|
795
715
|
}
|
|
796
716
|
return { error: `不支持 exec 模式: ${cmdBase}` };
|
|
@@ -801,7 +721,11 @@ export class CommandHandler {
|
|
|
801
721
|
/**
|
|
802
722
|
* 主命令处理入口
|
|
803
723
|
*/
|
|
804
|
-
async handle(content, channel, channelId, sendMessage, userId, threadId) {
|
|
724
|
+
async handle(content, channel, channelId, sendMessage, userId, threadId, chatType, source) {
|
|
725
|
+
const result = await this._handleInternal(content, channel, channelId, sendMessage, userId, threadId, chatType, source);
|
|
726
|
+
return result;
|
|
727
|
+
}
|
|
728
|
+
async _handleInternal(content, channel, channelId, sendMessage, userId, threadId, chatType, source) {
|
|
805
729
|
// 解析身份(按实例名)
|
|
806
730
|
const identity = this.sessionManager.resolveIdentity(channel, userId);
|
|
807
731
|
const policy = this.getPolicy(channel);
|
|
@@ -825,7 +749,7 @@ export class CommandHandler {
|
|
|
825
749
|
const threadBlocked = ['/new', '/slist', '/plist', '/bind', '/s', '/session', '/project', '/p', '/fork', '/del', '/agent'];
|
|
826
750
|
const isBlocked = threadBlocked.some(c => normalizedContent === c || normalizedContent.startsWith(c + ' '));
|
|
827
751
|
if (isBlocked) {
|
|
828
|
-
return '⚠️ 话题中不支持此命令';
|
|
752
|
+
return { kind: 'command.error', text: '⚠️ 话题中不支持此命令' };
|
|
829
753
|
}
|
|
830
754
|
}
|
|
831
755
|
// Agent-owned 通道:禁止项目切换和 agent 切换
|
|
@@ -836,10 +760,10 @@ export class CommandHandler {
|
|
|
836
760
|
normalizedContent === '/plist' ||
|
|
837
761
|
normalizedContent === '/p' || normalizedContent.startsWith('/p ');
|
|
838
762
|
if (isProjectCmd) {
|
|
839
|
-
return `❌ 当前通道由 agent [${owningAgent.name}] 管理,项目已锁定为 ${owningAgent.projectPath}
|
|
763
|
+
return { kind: 'command.error', text: `❌ 当前通道由 agent [${owningAgent.name}] 管理,项目已锁定为 ${owningAgent.projectPath}` };
|
|
840
764
|
}
|
|
841
765
|
if (normalizedContent.startsWith('/agent ')) {
|
|
842
|
-
return `❌ 当前通道由 agent [${owningAgent.name}] 管理,baseagent 已锁定为 ${owningAgent.baseagent}
|
|
766
|
+
return { kind: 'command.error', text: `❌ 当前通道由 agent [${owningAgent.name}] 管理,baseagent 已锁定为 ${owningAgent.baseagent}` };
|
|
843
767
|
}
|
|
844
768
|
}
|
|
845
769
|
// 权限检查:区分用户级命令和管理级命令
|
|
@@ -849,9 +773,9 @@ export class CommandHandler {
|
|
|
849
773
|
if (normalizedContent.startsWith('/')) {
|
|
850
774
|
// guest 在群聊和私聊中均可访问的只读命令:纯查询形态(带参写操作由各 handler 内部守卫拦截)
|
|
851
775
|
const guestGroupCommands = [
|
|
852
|
-
'/status', '/help', '/evolhelp', '/check', '/chatmode',
|
|
776
|
+
'/status', '/help', '/evolhelp', '/check', '/chatmode', '/dispatch',
|
|
853
777
|
'/model', '/setmodel', '/effort', '/agent', '/perm', '/activity', '/safe', '/stop',
|
|
854
|
-
'/resume',
|
|
778
|
+
'/resume', '/trigger',
|
|
855
779
|
];
|
|
856
780
|
const userCommands = activeChatType === 'group' && !isAdmin
|
|
857
781
|
? guestGroupCommands
|
|
@@ -862,9 +786,9 @@ export class CommandHandler {
|
|
|
862
786
|
];
|
|
863
787
|
const isUserCommand = userCommands.some(cmd => normalizedContent === cmd.trimEnd() || normalizedContent.startsWith(cmd));
|
|
864
788
|
if (!isUserCommand && !isAdmin) {
|
|
865
|
-
return activeChatType === 'group'
|
|
866
|
-
|
|
867
|
-
|
|
789
|
+
return { kind: 'command.error', text: activeChatType === 'group'
|
|
790
|
+
? '❌ 无权限:当前群聊仅支持 /status 和 /help'
|
|
791
|
+
: '❌ 无权限:此命令仅限管理员使用' };
|
|
868
792
|
}
|
|
869
793
|
}
|
|
870
794
|
// 空闲检查:某些命令需要等待当前会话空闲
|
|
@@ -872,6 +796,7 @@ export class CommandHandler {
|
|
|
872
796
|
// - 始终需要 idle(无参即写):/new /clear /compact /repair /fork
|
|
873
797
|
// - 仅带参时需要 idle(无参是列表/用法):/session /bind /project /agent /rewind
|
|
874
798
|
// - /chatmode:在 handler 内部自行做写操作的 idle 检查
|
|
799
|
+
// - /dispatch:在 handler 内部自行做写操作的 idle 检查
|
|
875
800
|
// - /safe:已禁用 no-op,不再要求 idle
|
|
876
801
|
const idleAlways = ['/new', '/clear', '/compact', '/repair', '/fork'];
|
|
877
802
|
const idleWhenArg = ['/session', '/bind', '/project', '/agent', '/rewind'];
|
|
@@ -884,12 +809,12 @@ export class CommandHandler {
|
|
|
884
809
|
if (threadSession) {
|
|
885
810
|
const threadAgent = this.getAgent(channel, threadSession.agentId);
|
|
886
811
|
if (threadAgent.hasActiveStream(threadSession.id)) {
|
|
887
|
-
return '⚠️ 当前正在处理消息,请稍后再试\n使用 /stop 中断当前任务后重试';
|
|
812
|
+
return { kind: 'command.error', text: '⚠️ 当前正在处理消息,请稍后再试\n使用 /stop 中断当前任务后重试' };
|
|
888
813
|
}
|
|
889
814
|
}
|
|
890
815
|
}
|
|
891
816
|
else if (activeSession && agent.hasActiveStream(activeSession.id)) {
|
|
892
|
-
return '⚠️ 当前正在处理消息,请稍后再试\n使用 /stop 中断当前任务后重试';
|
|
817
|
+
return { kind: 'command.error', text: '⚠️ 当前正在处理消息,请稍后再试\n使用 /stop 中断当前任务后重试' };
|
|
893
818
|
}
|
|
894
819
|
}
|
|
895
820
|
// 检查是否以 / 开头(可能是命令)
|
|
@@ -902,10 +827,10 @@ export class CommandHandler {
|
|
|
902
827
|
return distance <= 2;
|
|
903
828
|
});
|
|
904
829
|
if (similar) {
|
|
905
|
-
return `❌ 未知命令: ${inputCmd}\n💡 你是不是想输入: ${similar}\n\n输入 /help
|
|
830
|
+
return { kind: 'command.error', text: `❌ 未知命令: ${inputCmd}\n💡 你是不是想输入: ${similar}\n\n输入 /help 查看所有可用命令` };
|
|
906
831
|
}
|
|
907
832
|
else {
|
|
908
|
-
return `❌ 未知命令: ${inputCmd}\n\n输入 /help
|
|
833
|
+
return { kind: 'command.error', text: `❌ 未知命令: ${inputCmd}\n\n输入 /help 查看所有可用命令` };
|
|
909
834
|
}
|
|
910
835
|
}
|
|
911
836
|
}
|
|
@@ -923,7 +848,7 @@ export class CommandHandler {
|
|
|
923
848
|
' /check - 检查渠道健康',
|
|
924
849
|
' /help - 显示此帮助信息',
|
|
925
850
|
];
|
|
926
|
-
return lines.join('\n');
|
|
851
|
+
return { kind: 'command.result', text: lines.join('\n') };
|
|
927
852
|
}
|
|
928
853
|
if (!isAdmin) {
|
|
929
854
|
const lines = [
|
|
@@ -940,7 +865,7 @@ export class CommandHandler {
|
|
|
940
865
|
'❓ 帮助:',
|
|
941
866
|
' /help - 显示此帮助信息',
|
|
942
867
|
];
|
|
943
|
-
return lines.join('\n');
|
|
868
|
+
return { kind: 'command.result', text: lines.join('\n') };
|
|
944
869
|
}
|
|
945
870
|
// admin+ 基础命令
|
|
946
871
|
const lines = [
|
|
@@ -965,6 +890,11 @@ export class CommandHandler {
|
|
|
965
890
|
' /model [model] - 查看或切换模型',
|
|
966
891
|
' /effort [level] - 查看或切换推理强度',
|
|
967
892
|
'',
|
|
893
|
+
'💬 聊天设置:',
|
|
894
|
+
' /activity [all|dm|owner|none] - 查看/控制中间输出显示模式',
|
|
895
|
+
' /chatmode [interactive|proactive] - 查看/切换会话模式(被动响应或主动推进)',
|
|
896
|
+
' /dispatch [mention|broadcast] - 查看/切换群聊分发模式(仅@响应或广播响应,仅群聊)',
|
|
897
|
+
'',
|
|
968
898
|
'🔐 权限管理:',
|
|
969
899
|
' /perm - 查看当前权限模式',
|
|
970
900
|
...(isOwner ? [' /perm <auto|bypass|request|edit|plan|noask> - 切换权限模式'] : []),
|
|
@@ -974,21 +904,24 @@ export class CommandHandler {
|
|
|
974
904
|
' /status - 显示会话状态',
|
|
975
905
|
' /stop - 中断当前任务',
|
|
976
906
|
' /check - 检查渠道状态',
|
|
977
|
-
' /activity [all|dm|owner|none] - 查看/控制中间输出显示模式',
|
|
978
907
|
...(isAdmin ? [
|
|
979
908
|
' /restart <type> - 重连该类型所有渠道实例(服务级,admin+)',
|
|
980
909
|
] : []),
|
|
981
910
|
...(isOwner ? [
|
|
982
911
|
' /restart - 重启服务',
|
|
912
|
+
] : []),
|
|
913
|
+
...(isOwner ? [
|
|
914
|
+
'',
|
|
915
|
+
'🧰 工具:',
|
|
983
916
|
' /file [channel] <path> - 发送项目内文件',
|
|
984
|
-
' /aid [list|new
|
|
985
|
-
' /
|
|
917
|
+
' /aid [list|show|new|delete|lookup|agentmd] - AID 身份管理',
|
|
918
|
+
' /storage [upload|download|ls|rm|quota] <aid> - 文件存储',
|
|
986
919
|
] : []),
|
|
987
920
|
'',
|
|
988
921
|
'❓ 帮助:',
|
|
989
922
|
' /help - 显示此帮助信息',
|
|
990
923
|
];
|
|
991
|
-
return lines.join('\n');
|
|
924
|
+
return { kind: 'command.result', text: lines.join('\n') };
|
|
992
925
|
}
|
|
993
926
|
// /evolhelp 命令:返回 JSON 格式的命令列表(供程序解析)
|
|
994
927
|
if (normalizedContent === '/evolhelp') {
|
|
@@ -1013,7 +946,6 @@ export class CommandHandler {
|
|
|
1013
946
|
if (isAdmin) {
|
|
1014
947
|
cmds.push({ command: '/agent', args: '[name]', description: '查看或切换 Agent 后端', category: 'Agent 与模型', roles: ['admin', 'owner'] });
|
|
1015
948
|
cmds.push({ command: '/model', args: '[model]', description: '查看或切换模型', category: 'Agent 与模型', roles: ['admin', 'owner'] });
|
|
1016
|
-
cmds.push({ command: '/setmodel', description: '返回 JSON 格式的模型列表(供程序解析)', category: 'Agent 与模型', roles: ['admin', 'owner'] });
|
|
1017
949
|
cmds.push({ command: '/effort', args: '[level]', description: '查看或切换推理强度', category: 'Agent 与模型', roles: ['admin', 'owner'] });
|
|
1018
950
|
}
|
|
1019
951
|
// 权限管理
|
|
@@ -1026,86 +958,69 @@ export class CommandHandler {
|
|
|
1026
958
|
cmds.push({ command: '/stop', description: '中断当前任务', category: '运维', roles: ['admin', 'owner'] });
|
|
1027
959
|
cmds.push({ command: '/check', description: '检查渠道状态', category: '运维', roles: ['guest', 'admin', 'owner'] });
|
|
1028
960
|
if (isAdmin) {
|
|
1029
|
-
cmds.push({ command: '/activity', args: '[all|dm|owner|none]', description: '查看/控制中间输出显示模式', category: '
|
|
961
|
+
cmds.push({ command: '/activity', args: '[all|dm|owner|none]', description: '查看/控制中间输出显示模式', category: '聊天设置', roles: ['admin', 'owner'] });
|
|
1030
962
|
cmds.push({ command: '/restart', args: '<channel>', description: '重连指定渠道', category: '运维', roles: ['admin', 'owner'] });
|
|
1031
963
|
}
|
|
1032
964
|
if (isOwner) {
|
|
1033
965
|
cmds.push({ command: '/restart', description: '重启服务', category: '运维', roles: ['owner'] });
|
|
1034
|
-
cmds.push({ command: '/file', args: '[channel] <path>', description: '发送项目内文件', category: '
|
|
1035
|
-
cmds.push({ command: '/aid', args: '[list|new
|
|
1036
|
-
cmds.push({ command: '/
|
|
966
|
+
cmds.push({ command: '/file', args: '[channel] <path>', description: '发送项目内文件', category: '工具', roles: ['owner'] });
|
|
967
|
+
cmds.push({ command: '/aid', args: '[list|show|new|delete|lookup|agentmd]', description: 'AID 身份管理', category: '工具', roles: ['owner'] });
|
|
968
|
+
cmds.push({ command: '/storage', args: '[upload|download|ls|rm|quota] <aid>', description: '文件存储', category: '工具', roles: ['owner'] });
|
|
1037
969
|
}
|
|
1038
|
-
//
|
|
970
|
+
// 聊天设置
|
|
1039
971
|
if (isAdmin) {
|
|
1040
|
-
cmds.push({ command: '/chatmode', args: '[interactive|proactive]', description: '查看/切换会话模式(被动响应或主动推进)', category: '
|
|
972
|
+
cmds.push({ command: '/chatmode', args: '[interactive|proactive]', description: '查看/切换会话模式(被动响应或主动推进)', category: '聊天设置', roles: ['admin', 'owner'] });
|
|
973
|
+
cmds.push({ command: '/dispatch', args: '[mention|broadcast]', description: '查看/切换群聊分发模式(仅@响应或广播响应)', category: '聊天设置', roles: ['admin', 'owner'] });
|
|
1041
974
|
}
|
|
1042
975
|
// 交互
|
|
1043
976
|
cmds.push({ command: '/ask', args: '<选项>', description: '回答 Agent 的交互式问题', category: '运维', roles: ['guest', 'admin', 'owner'] });
|
|
1044
|
-
cmds.push({ command: '/resume', description: '查看当前项目的 Claude 会话记录', category: '会话管理', roles: ['guest', 'admin', 'owner'] });
|
|
1045
977
|
// 帮助
|
|
1046
978
|
cmds.push({ command: '/help', description: '显示帮助信息', category: '帮助', roles: ['guest', 'admin', 'owner'] });
|
|
1047
|
-
cmds.push({ command: '/evolhelp', description: '返回 JSON 格式命令列表', category: '帮助', roles: ['guest', 'admin', 'owner'] });
|
|
1048
979
|
const categories = [...new Set(cmds.map(c => c.category))];
|
|
1049
|
-
return JSON.stringify({ commands: cmds, categories });
|
|
980
|
+
return { kind: 'command.result', text: JSON.stringify({ commands: cmds, categories }) };
|
|
1050
981
|
}
|
|
1051
982
|
// /perm 命令:权限模式切换 + 权限审批(快速路径,不进入消息队列)
|
|
1052
983
|
if (normalizedContent.startsWith('/perm')) {
|
|
1053
984
|
const args = normalizedContent.slice(5).trim();
|
|
1054
985
|
// 先获取正确的 session 和 agent(话题可能用不同 agent)
|
|
1055
|
-
const permResult = await this.ensureSession(channel, channelId, threadId);
|
|
986
|
+
const permResult = await this.ensureSession(channel, channelId, threadId, chatType);
|
|
1056
987
|
if ('error' in permResult)
|
|
1057
|
-
return permResult.error;
|
|
988
|
+
return { kind: 'command.result', text: permResult.error };
|
|
1058
989
|
const { session: permSession } = permResult;
|
|
1059
990
|
const permAgent = this.getAgent(channel, permSession.agentId);
|
|
1060
991
|
// /perm(无参数):显示当前模式和可选模式
|
|
1061
992
|
if (!args) {
|
|
1062
993
|
if (!hasPermissionController(permAgent)) {
|
|
1063
|
-
return '❌ 权限控制不可用';
|
|
994
|
+
return { kind: 'command.error', text: '❌ 权限控制不可用' };
|
|
1064
995
|
}
|
|
1065
996
|
const currentMode = permSession.metadata?.permissionMode ?? DEFAULT_PERMISSION_MODE;
|
|
1066
997
|
const modes = permAgent.listModes();
|
|
1067
|
-
//
|
|
1068
|
-
|
|
1069
|
-
const requestId = `perm-${Date.now()}-${crypto.randomBytes(4).toString('hex')}`;
|
|
998
|
+
// 尝试发送 CommandCard 卡片
|
|
999
|
+
{
|
|
1070
1000
|
const availableModes = modes.filter(m => m.available);
|
|
1071
1001
|
const interaction = {
|
|
1072
1002
|
type: 'interaction',
|
|
1073
|
-
id:
|
|
1003
|
+
id: `perm-${Date.now()}-${crypto.randomBytes(4).toString('hex')}`,
|
|
1074
1004
|
channelId,
|
|
1075
1005
|
sessionId: permSession.id,
|
|
1006
|
+
initiatorId: userId,
|
|
1076
1007
|
kind: {
|
|
1077
|
-
kind: '
|
|
1008
|
+
kind: 'command-card',
|
|
1078
1009
|
title: '🔐 权限模式',
|
|
1079
1010
|
body: availableModes.map(m => `${m.key === currentMode ? '✓' : '•'} **${m.key}** (${m.nameZh}) - ${m.description}`).join('\n'),
|
|
1080
1011
|
buttons: availableModes.map(m => ({
|
|
1081
|
-
key: m.key,
|
|
1082
1012
|
label: m.key === currentMode ? `✓ ${m.key}` : m.key,
|
|
1083
|
-
|
|
1013
|
+
command: `/perm ${m.key}`,
|
|
1014
|
+
style: (m.key === currentMode ? 'primary' : 'default'),
|
|
1015
|
+
disabled: m.key === currentMode,
|
|
1084
1016
|
})),
|
|
1085
1017
|
},
|
|
1086
1018
|
};
|
|
1087
1019
|
const replyCtx = this.getReplyContext(permSession);
|
|
1088
|
-
const
|
|
1089
|
-
|
|
1090
|
-
canWrite: isOwner,
|
|
1091
|
-
callback: async (action, _values, operatorId) => {
|
|
1092
|
-
if (action !== currentMode) {
|
|
1093
|
-
if (userId && operatorId && operatorId !== userId)
|
|
1094
|
-
return;
|
|
1095
|
-
const result = await this.handle(`/perm ${action}`, channel, channelId, undefined, userId, threadId);
|
|
1096
|
-
if (result) {
|
|
1097
|
-
const adapter = this.adapters.get(channel);
|
|
1098
|
-
adapter?.sendText(channelId, result, replyCtx);
|
|
1099
|
-
}
|
|
1100
|
-
else {
|
|
1101
|
-
// 切换成功后重新发新卡片(会自动 invalidate 旧卡片)
|
|
1102
|
-
await this.handle('/perm', channel, channelId, undefined, userId, threadId);
|
|
1103
|
-
}
|
|
1104
|
-
}
|
|
1105
|
-
},
|
|
1106
|
-
});
|
|
1107
|
-
if (cardSent)
|
|
1020
|
+
const cardResult = await this.sendCommandCard({ channel, channelId, interaction, replyCtx, canWrite: isOwner });
|
|
1021
|
+
if (cardResult === null)
|
|
1108
1022
|
return null;
|
|
1023
|
+
return { kind: 'command.result', text: cardResult };
|
|
1109
1024
|
}
|
|
1110
1025
|
// 降级:文本
|
|
1111
1026
|
const modeList = modes.map(m => {
|
|
@@ -1114,25 +1029,30 @@ export class CommandHandler {
|
|
|
1114
1029
|
return ` ${prefix} ${m.key} (${m.nameZh}) - ${m.description}${suffix}`;
|
|
1115
1030
|
}).join('\n');
|
|
1116
1031
|
if (isOwner) {
|
|
1117
|
-
return `🔐 当前权限模式: ${currentMode}\n\n${modeList}\n\n用法:\n /perm <模式> 切换权限模式\n /perm allow|always|deny
|
|
1032
|
+
return { kind: 'command.result', text: `🔐 当前权限模式: ${currentMode}\n\n${modeList}\n\n用法:\n /perm <模式> 切换权限模式\n /perm allow|always|deny 审批权限请求` };
|
|
1118
1033
|
}
|
|
1119
|
-
return `🔐 当前权限模式: ${currentMode}
|
|
1034
|
+
return { kind: 'command.result', text: `🔐 当前权限模式: ${currentMode}` };
|
|
1120
1035
|
}
|
|
1121
1036
|
const parts = args.split(/\s+/);
|
|
1122
1037
|
// /perm <mode> 或 /perm allow|always|deny:切换模式 / 快捷审批
|
|
1123
1038
|
if (parts.length === 1) {
|
|
1124
1039
|
const arg = parts[0];
|
|
1125
|
-
// /perm allow|always|deny
|
|
1040
|
+
// /perm allow|always|deny:快捷审批
|
|
1041
|
+
// 优先走 InteractionRouter fallback(统一降级路径)
|
|
1126
1042
|
if (arg === 'allow' || arg === 'always' || arg === 'deny') {
|
|
1043
|
+
const fb = await this.handleInteractionFallback('perm', arg, permSession.id, userId);
|
|
1044
|
+
if (fb.matched)
|
|
1045
|
+
return { kind: 'command.result', text: fb.result ?? '✓ 已回答' };
|
|
1046
|
+
// fallback 不命中:走 permissionGateway 直接审批(兼容旧路径)
|
|
1127
1047
|
if (!this.permissionGateway) {
|
|
1128
|
-
return '❌ 权限审批未启用';
|
|
1048
|
+
return { kind: 'command.error', text: '❌ 权限审批未启用' };
|
|
1129
1049
|
}
|
|
1130
1050
|
const pendingIds = this.permissionGateway.getPendingRequests(permSession.id);
|
|
1131
1051
|
if (pendingIds.length === 0) {
|
|
1132
|
-
return '❌ 当前没有待审批的权限请求';
|
|
1052
|
+
return { kind: 'command.error', text: '❌ 当前没有待审批的权限请求' };
|
|
1133
1053
|
}
|
|
1134
1054
|
if (pendingIds.length > 1) {
|
|
1135
|
-
return `❌ 当前有 ${pendingIds.length} 个待审批请求,请指定 requestId:\n${pendingIds.map(id => ` /perm ${id} ${arg}`).join('\n')}
|
|
1055
|
+
return { kind: 'command.error', text: `❌ 当前有 ${pendingIds.length} 个待审批请求,请指定 requestId:\n${pendingIds.map(id => ` /perm ${id} ${arg}`).join('\n')}` };
|
|
1136
1056
|
}
|
|
1137
1057
|
const requestId = pendingIds[0];
|
|
1138
1058
|
const decision = arg;
|
|
@@ -1142,7 +1062,7 @@ export class CommandHandler {
|
|
|
1142
1062
|
always: '✓ 已授权(始终允许该工具),继续执行……',
|
|
1143
1063
|
deny: '✓ 已拒绝'
|
|
1144
1064
|
};
|
|
1145
|
-
return labels[decision];
|
|
1065
|
+
return { kind: 'command.result', text: labels[decision] };
|
|
1146
1066
|
}
|
|
1147
1067
|
// /perm <mode>:切换权限模式
|
|
1148
1068
|
if (hasPermissionController(permAgent)) {
|
|
@@ -1150,56 +1070,51 @@ export class CommandHandler {
|
|
|
1150
1070
|
const matched = modes.find(m => m.key === arg);
|
|
1151
1071
|
if (matched) {
|
|
1152
1072
|
if (!matched.available) {
|
|
1153
|
-
return `❌ ${matched.key} 模式当前不可用:${matched.unavailableReason}
|
|
1073
|
+
return { kind: 'command.error', text: `❌ ${matched.key} 模式当前不可用:${matched.unavailableReason}` };
|
|
1154
1074
|
}
|
|
1155
1075
|
// guest 和 admin 用户不能切换权限模式(仅 owner)
|
|
1156
1076
|
if (!isOwner) {
|
|
1157
|
-
return '❌ 权限模式切换仅限 owner';
|
|
1077
|
+
return { kind: 'command.error', text: '❌ 权限模式切换仅限 owner' };
|
|
1158
1078
|
}
|
|
1159
1079
|
const metadata = permSession.metadata || {};
|
|
1160
1080
|
metadata.permissionMode = arg;
|
|
1161
1081
|
await this.sessionManager.updateSession(permSession.id, { metadata });
|
|
1162
|
-
return `✓ 权限模式已切换为: ${matched.key} (${matched.nameZh})\n${matched.description}
|
|
1082
|
+
return { kind: 'command.result', text: `✓ 权限模式已切换为: ${matched.key} (${matched.nameZh})\n${matched.description}` };
|
|
1163
1083
|
}
|
|
1164
1084
|
}
|
|
1165
1085
|
// 不是已知模式名也不是 allow/deny
|
|
1166
1086
|
const modeKeys = hasPermissionController(permAgent) ? permAgent.listModes().map(m => m.key).join('|') : 'auto|bypass|request|edit|plan|noask';
|
|
1167
|
-
return `❌ 未知参数: ${arg}\n用法: /perm <${modeKeys}> 或 /perm allow|always|deny
|
|
1087
|
+
return { kind: 'command.error', text: `❌ 未知参数: ${arg}\n用法: /perm <${modeKeys}> 或 /perm allow|always|deny` };
|
|
1168
1088
|
}
|
|
1169
1089
|
// 双参数不再支持,提示正确用法
|
|
1170
1090
|
const allModeKeys = hasPermissionController(permAgent) ? permAgent.listModes().map(m => m.key).join('|') : 'auto|bypass|request|edit|plan|noask';
|
|
1171
|
-
return `❌ 未知参数: ${args}\n用法: /perm <${allModeKeys}> 或 /perm allow|always|deny
|
|
1091
|
+
return { kind: 'command.error', text: `❌ 未知参数: ${args}\n用法: /perm <${allModeKeys}> 或 /perm allow|always|deny` };
|
|
1172
1092
|
}
|
|
1173
1093
|
// /ask 命令:回答 AskUserQuestion / ExitPlanMode 的交互式问题
|
|
1174
1094
|
if (normalizedContent.startsWith('/ask')) {
|
|
1175
1095
|
const args = normalizedContent.slice(4).trim();
|
|
1176
1096
|
if (!args) {
|
|
1177
|
-
|
|
1178
|
-
const askResult = await this.ensureSession(channel, channelId, threadId);
|
|
1097
|
+
const askResult = await this.ensureSession(channel, channelId, threadId, chatType);
|
|
1179
1098
|
if ('error' in askResult)
|
|
1180
|
-
return askResult.error;
|
|
1099
|
+
return { kind: 'command.result', text: askResult.error };
|
|
1181
1100
|
const pendingIds = this.interactionRouter?.getPending(askResult.session.id) || [];
|
|
1182
1101
|
if (pendingIds.length === 0)
|
|
1183
|
-
return '当前没有待回答的问题';
|
|
1184
|
-
return `当前有 ${pendingIds.length} 个待回答问题,请回复 /ask
|
|
1102
|
+
return { kind: 'command.result', text: '当前没有待回答的问题' };
|
|
1103
|
+
return { kind: 'command.result', text: `当前有 ${pendingIds.length} 个待回答问题,请回复 /ask <选项>` };
|
|
1185
1104
|
}
|
|
1186
|
-
const askResult = await this.ensureSession(channel, channelId, threadId);
|
|
1105
|
+
const askResult = await this.ensureSession(channel, channelId, threadId, chatType);
|
|
1187
1106
|
if ('error' in askResult)
|
|
1188
|
-
return askResult.error;
|
|
1189
|
-
const
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
// 路由到最早的 pending interaction
|
|
1194
|
-
const targetId = pendingIds[0];
|
|
1195
|
-
this.interactionRouter.handle({ type: 'interaction.response', id: targetId, action: args, operatorId: userId });
|
|
1196
|
-
return `✓ 已回答`;
|
|
1107
|
+
return { kind: 'command.result', text: askResult.error };
|
|
1108
|
+
const fb = await this.handleInteractionFallback('ask', args, askResult.session.id, userId);
|
|
1109
|
+
if (fb.matched)
|
|
1110
|
+
return { kind: 'command.result', text: fb.result ?? '✓ 已回答' };
|
|
1111
|
+
return { kind: 'command.error', text: '❌ 当前没有待回答的问题' };
|
|
1197
1112
|
}
|
|
1198
1113
|
// /resume 命令:返回当前项目的 Claude 会话记录(JSON)
|
|
1199
1114
|
if (normalizedContent === '/resume' || normalizedContent.startsWith('/resume ')) {
|
|
1200
|
-
const resumeResult = await this.ensureSession(channel, channelId, threadId);
|
|
1115
|
+
const resumeResult = await this.ensureSession(channel, channelId, threadId, chatType);
|
|
1201
1116
|
if ('error' in resumeResult)
|
|
1202
|
-
return resumeResult.error;
|
|
1117
|
+
return { kind: 'command.result', text: resumeResult.error };
|
|
1203
1118
|
const { session: resumeSession } = resumeResult;
|
|
1204
1119
|
try {
|
|
1205
1120
|
const { encodePath } = await import('../utils/cross-platform.js');
|
|
@@ -1207,11 +1122,11 @@ export class CommandHandler {
|
|
|
1207
1122
|
const encodedPath = encodePath(resumeSession.projectPath);
|
|
1208
1123
|
const projectDir = path.join(homeDir, '.claude', 'projects', encodedPath);
|
|
1209
1124
|
if (!fs.existsSync(projectDir)) {
|
|
1210
|
-
return '❌ 未找到 Claude 会话记录目录';
|
|
1125
|
+
return { kind: 'command.error', text: '❌ 未找到 Claude 会话记录目录' };
|
|
1211
1126
|
}
|
|
1212
1127
|
const jsonlFiles = fs.readdirSync(projectDir).filter(f => f.endsWith('.jsonl'));
|
|
1213
1128
|
if (jsonlFiles.length === 0) {
|
|
1214
|
-
return '❌ 当前项目没有 Claude 会话记录';
|
|
1129
|
+
return { kind: 'command.error', text: '❌ 当前项目没有 Claude 会话记录' };
|
|
1215
1130
|
}
|
|
1216
1131
|
const sessions = [];
|
|
1217
1132
|
for (const file of jsonlFiles) {
|
|
@@ -1271,11 +1186,11 @@ export class CommandHandler {
|
|
|
1271
1186
|
});
|
|
1272
1187
|
}
|
|
1273
1188
|
sessions.sort((a, b) => b.lastMessageTime.localeCompare(a.lastMessageTime));
|
|
1274
|
-
return JSON.stringify(sessions, null, 2);
|
|
1189
|
+
return { kind: 'command.result', text: JSON.stringify(sessions, null, 2) };
|
|
1275
1190
|
}
|
|
1276
1191
|
catch (error) {
|
|
1277
1192
|
logger.error('[CommandHandler] /resume failed:', error);
|
|
1278
|
-
return `❌ 读取会话记录失败: ${error instanceof Error ? error.message : '未知错误'}
|
|
1193
|
+
return { kind: 'command.error', text: `❌ 读取会话记录失败: ${error instanceof Error ? error.message : '未知错误'}` };
|
|
1279
1194
|
}
|
|
1280
1195
|
}
|
|
1281
1196
|
// /agent 命令:查看或切换 Agent 后端
|
|
@@ -1283,7 +1198,7 @@ export class CommandHandler {
|
|
|
1283
1198
|
const args = normalizedContent.slice(6).trim();
|
|
1284
1199
|
// 切换(带参)需权限:群聊 owner only,私聊 admin+;无参查询对所有人放开
|
|
1285
1200
|
if (args && (activeChatType === 'group' ? !isOwner : !isAdmin)) {
|
|
1286
|
-
return '❌ 无权限:此命令仅限管理员使用';
|
|
1201
|
+
return { kind: 'command.error', text: '❌ 无权限:此命令仅限管理员使用' };
|
|
1287
1202
|
}
|
|
1288
1203
|
const available = this.getAvailableBaseagents(channel);
|
|
1289
1204
|
if (!args) {
|
|
@@ -1291,57 +1206,45 @@ export class CommandHandler {
|
|
|
1291
1206
|
const currentAgent = activeSession?.agentId
|
|
1292
1207
|
|| this.agentRegistry?.resolveByChannel(channel)?.baseagent
|
|
1293
1208
|
|| this.parseDefaultBaseagent();
|
|
1294
|
-
//
|
|
1209
|
+
// 尝试发送 CommandCard 卡片
|
|
1295
1210
|
if (this.interactionRouter && available.length > 1) {
|
|
1296
|
-
const requestId = `agent-${Date.now()}-${crypto.randomBytes(4).toString('hex')}`;
|
|
1297
1211
|
const interaction = {
|
|
1298
1212
|
type: 'interaction',
|
|
1299
|
-
id:
|
|
1213
|
+
id: `agent-${Date.now()}-${crypto.randomBytes(4).toString('hex')}`,
|
|
1300
1214
|
channelId,
|
|
1301
|
-
sessionId: activeSession?.id ||
|
|
1215
|
+
sessionId: activeSession?.id || `agent-${Date.now()}`,
|
|
1216
|
+
initiatorId: userId,
|
|
1302
1217
|
kind: {
|
|
1303
|
-
kind: '
|
|
1218
|
+
kind: 'command-card',
|
|
1304
1219
|
title: '🔌 切换 Agent',
|
|
1305
1220
|
buttons: available.map(a => ({
|
|
1306
|
-
key: a,
|
|
1307
1221
|
label: a === currentAgent ? `✓ ${a}` : a,
|
|
1308
|
-
|
|
1222
|
+
command: `/agent ${a}`,
|
|
1223
|
+
style: (a === currentAgent ? 'primary' : 'default'),
|
|
1224
|
+
disabled: a === currentAgent,
|
|
1309
1225
|
})),
|
|
1310
1226
|
},
|
|
1311
1227
|
};
|
|
1312
1228
|
const replyCtx = activeSession ? this.getReplyContext(activeSession) : undefined;
|
|
1313
|
-
const
|
|
1314
|
-
|
|
1315
|
-
canWrite: activeChatType === 'group' ? isOwner : isAdmin,
|
|
1316
|
-
callback: async (action, _values, operatorId) => {
|
|
1317
|
-
if (action !== currentAgent) {
|
|
1318
|
-
if (userId && operatorId && operatorId !== userId)
|
|
1319
|
-
return;
|
|
1320
|
-
const result = await this.handle(`/agent ${action}`, channel, channelId, undefined, userId, threadId);
|
|
1321
|
-
if (result) {
|
|
1322
|
-
const adapter = this.adapters.get(channel);
|
|
1323
|
-
adapter?.sendText(channelId, result, replyCtx);
|
|
1324
|
-
}
|
|
1325
|
-
}
|
|
1326
|
-
},
|
|
1327
|
-
});
|
|
1328
|
-
if (cardSent)
|
|
1229
|
+
const cardResult = await this.sendCommandCard({ channel, channelId, interaction, replyCtx, canWrite: activeChatType === 'group' ? isOwner : isAdmin });
|
|
1230
|
+
if (cardResult === null)
|
|
1329
1231
|
return null;
|
|
1232
|
+
return { kind: 'command.result', text: cardResult };
|
|
1330
1233
|
}
|
|
1331
1234
|
// 降级:文本
|
|
1332
1235
|
const list = available.map(a => `${a === currentAgent ? ' ✓' : ' '} ${a}`).join('\n');
|
|
1333
1236
|
const canSwitchAgent = activeChatType === 'group' ? isOwner : isAdmin;
|
|
1334
1237
|
if (canSwitchAgent) {
|
|
1335
|
-
return `当前 Agent: ${currentAgent}\n\n可用:\n${list}\n\n用法: /agent <name
|
|
1238
|
+
return { kind: 'command.result', text: `当前 Agent: ${currentAgent}\n\n可用:\n${list}\n\n用法: /agent <name>` };
|
|
1336
1239
|
}
|
|
1337
|
-
return `当前 Agent: ${currentAgent}
|
|
1240
|
+
return { kind: 'command.result', text: `当前 Agent: ${currentAgent}` };
|
|
1338
1241
|
}
|
|
1339
1242
|
if (!available.includes(args)) {
|
|
1340
|
-
return `❌ 未知 Agent: ${args}\n可用: ${available.join(', ')}
|
|
1243
|
+
return { kind: 'command.error', text: `❌ 未知 Agent: ${args}\n可用: ${available.join(', ')}` };
|
|
1341
1244
|
}
|
|
1342
|
-
const result = await this.ensureSession(channel, channelId, threadId);
|
|
1245
|
+
const result = await this.ensureSession(channel, channelId, threadId, chatType);
|
|
1343
1246
|
if ('error' in result)
|
|
1344
|
-
return result.error;
|
|
1247
|
+
return { kind: 'command.error', text: result.error };
|
|
1345
1248
|
const { session } = result;
|
|
1346
1249
|
// 取消原会话的 pending 权限请求和交互卡片
|
|
1347
1250
|
if (this.permissionGateway) {
|
|
@@ -1355,13 +1258,13 @@ export class CommandHandler {
|
|
|
1355
1258
|
const hasExistingSession = newSession.agentSessionId ? '(恢复已有会话)' : '(新建会话)';
|
|
1356
1259
|
const projectName = this.getProjectName(session.projectPath);
|
|
1357
1260
|
let agentSwitchResponse = `✓ 已切换 Agent: ${args}\n 项目: ${projectName}\n 会话: ${newSession.name || '(未命名)'}\n ${hasExistingSession}`;
|
|
1358
|
-
return agentSwitchResponse;
|
|
1261
|
+
return { kind: 'command.result', text: agentSwitchResponse };
|
|
1359
1262
|
}
|
|
1360
1263
|
// /setmodel 命令:返回 JSON 格式的模型列表(供程序解析)
|
|
1361
1264
|
if (normalizedContent === '/setmodel' || normalizedContent.startsWith('/setmodel ')) {
|
|
1362
|
-
const setmodelResult = await this.ensureSession(channel, channelId, threadId);
|
|
1265
|
+
const setmodelResult = await this.ensureSession(channel, channelId, threadId, chatType);
|
|
1363
1266
|
if ('error' in setmodelResult)
|
|
1364
|
-
return setmodelResult.error;
|
|
1267
|
+
return { kind: 'command.result', text: setmodelResult.error };
|
|
1365
1268
|
const { session: setmodelSession } = setmodelResult;
|
|
1366
1269
|
const setmodelAgent = this.getAgent(channel, setmodelSession.agentId);
|
|
1367
1270
|
const currentModel = hasModelSwitcher(setmodelAgent) ? setmodelAgent.getModel() : setmodelAgent.name;
|
|
@@ -1370,7 +1273,7 @@ export class CommandHandler {
|
|
|
1370
1273
|
// 获取 API URL 用于请求 /models
|
|
1371
1274
|
let apiBaseUrl;
|
|
1372
1275
|
try {
|
|
1373
|
-
const configBaseUrl = this.config
|
|
1276
|
+
const configBaseUrl = this.getOwningAgent(channel)?.config?.baseagents?.claude?.baseUrl;
|
|
1374
1277
|
const isPlaceholderUrl = configBaseUrl?.includes('api.anthropic.com');
|
|
1375
1278
|
if (configBaseUrl && !isPlaceholderUrl) {
|
|
1376
1279
|
apiBaseUrl = configBaseUrl;
|
|
@@ -1397,7 +1300,7 @@ export class CommandHandler {
|
|
|
1397
1300
|
const timeout = setTimeout(() => controller.abort(), 5000);
|
|
1398
1301
|
const resp = await fetch(modelsUrl, {
|
|
1399
1302
|
signal: controller.signal,
|
|
1400
|
-
headers: { 'Authorization': `Bearer ${this.config
|
|
1303
|
+
headers: { 'Authorization': `Bearer ${this.getOwningAgent(channel)?.config?.baseagents?.claude?.apiKey || process.env.ANTHROPIC_AUTH_TOKEN || ''}` },
|
|
1401
1304
|
});
|
|
1402
1305
|
clearTimeout(timeout);
|
|
1403
1306
|
if (resp.ok) {
|
|
@@ -1418,20 +1321,20 @@ export class CommandHandler {
|
|
|
1418
1321
|
],
|
|
1419
1322
|
};
|
|
1420
1323
|
}
|
|
1421
|
-
return JSON.stringify({
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1324
|
+
return { kind: 'command.result', text: JSON.stringify({
|
|
1325
|
+
current_model: currentModel,
|
|
1326
|
+
current_effort: currentEffort,
|
|
1327
|
+
available_efforts: efforts,
|
|
1328
|
+
models: modelListData,
|
|
1329
|
+
}, null, 2) };
|
|
1427
1330
|
}
|
|
1428
1331
|
// /model 命令:查看或切换模型/推理强度
|
|
1429
1332
|
if (normalizedContent.startsWith('/model')) {
|
|
1430
1333
|
const args = normalizedContent.slice(6).trim();
|
|
1431
1334
|
// 获取当前会话(话题会话可能绑定不同 agent)
|
|
1432
|
-
const modelResult = await this.ensureSession(channel, channelId, threadId);
|
|
1335
|
+
const modelResult = await this.ensureSession(channel, channelId, threadId, chatType);
|
|
1433
1336
|
if ('error' in modelResult)
|
|
1434
|
-
return modelResult.error;
|
|
1337
|
+
return { kind: 'command.result', text: modelResult.error };
|
|
1435
1338
|
const { session: modelSession } = modelResult;
|
|
1436
1339
|
const modelAgent = this.getAgent(channel, modelSession.agentId);
|
|
1437
1340
|
const models = hasModelSwitcher(modelAgent) ? modelAgent.listModels() : [];
|
|
@@ -1439,42 +1342,30 @@ export class CommandHandler {
|
|
|
1439
1342
|
const currentModel = hasModelSwitcher(modelAgent) ? modelAgent.getModel() : modelAgent.name;
|
|
1440
1343
|
const efforts = getAvailableEfforts(modelAgent, currentModel);
|
|
1441
1344
|
const currentEffort = modelAgent.getEffort?.() || 'auto';
|
|
1442
|
-
//
|
|
1345
|
+
// 尝试发送 CommandCard 卡片
|
|
1443
1346
|
if (this.interactionRouter && models.length > 0) {
|
|
1444
|
-
const requestId = `model-${Date.now()}-${crypto.randomBytes(4).toString('hex')}`;
|
|
1445
1347
|
const interaction = {
|
|
1446
1348
|
type: 'interaction',
|
|
1447
|
-
id:
|
|
1349
|
+
id: `model-${Date.now()}-${crypto.randomBytes(4).toString('hex')}`,
|
|
1448
1350
|
channelId,
|
|
1449
1351
|
sessionId: modelSession.id,
|
|
1352
|
+
initiatorId: userId,
|
|
1450
1353
|
kind: {
|
|
1451
|
-
kind: '
|
|
1354
|
+
kind: 'command-card',
|
|
1452
1355
|
title: '🤖 切换模型',
|
|
1453
1356
|
buttons: models.map((m) => ({
|
|
1454
|
-
key: m,
|
|
1455
1357
|
label: m === currentModel ? `✓ ${m}` : m,
|
|
1456
|
-
|
|
1358
|
+
command: `/model ${m}`,
|
|
1359
|
+
style: (m === currentModel ? 'primary' : 'default'),
|
|
1360
|
+
disabled: m === currentModel,
|
|
1457
1361
|
})),
|
|
1458
1362
|
},
|
|
1459
1363
|
};
|
|
1460
1364
|
const replyCtx = this.getReplyContext(modelSession);
|
|
1461
|
-
const
|
|
1462
|
-
|
|
1463
|
-
canWrite: isAdmin,
|
|
1464
|
-
callback: async (action, _values, operatorId) => {
|
|
1465
|
-
if (action !== currentModel) {
|
|
1466
|
-
if (userId && operatorId && operatorId !== userId)
|
|
1467
|
-
return;
|
|
1468
|
-
const result = await this.handle(`/model ${action}`, channel, channelId, undefined, userId, threadId);
|
|
1469
|
-
if (result) {
|
|
1470
|
-
const adapter = this.adapters.get(channel);
|
|
1471
|
-
adapter?.sendText(channelId, result, replyCtx);
|
|
1472
|
-
}
|
|
1473
|
-
}
|
|
1474
|
-
},
|
|
1475
|
-
});
|
|
1476
|
-
if (cardSent)
|
|
1365
|
+
const cardResult = await this.sendCommandCard({ channel, channelId, interaction, replyCtx, canWrite: isAdmin });
|
|
1366
|
+
if (cardResult === null)
|
|
1477
1367
|
return null;
|
|
1368
|
+
return { kind: 'command.result', text: cardResult };
|
|
1478
1369
|
}
|
|
1479
1370
|
// 降级:文本
|
|
1480
1371
|
const modelList = models.map((m) => ` ${m === currentModel ? '✓' : ' '} ${m}`).join('\n');
|
|
@@ -1482,13 +1373,13 @@ export class CommandHandler {
|
|
|
1482
1373
|
? `\n推理强度: ${currentEffort === 'auto' ? 'auto (SDK默认)' : currentEffort} (使用 /effort 调整)`
|
|
1483
1374
|
: '';
|
|
1484
1375
|
if (isAdmin) {
|
|
1485
|
-
return `当前模型: ${currentModel}${effortHint}\n\n可用模型:\n${modelList}\n\n${formatModelUsage(modelAgent, currentModel)}
|
|
1376
|
+
return { kind: 'command.result', text: `当前模型: ${currentModel}${effortHint}\n\n可用模型:\n${modelList}\n\n${formatModelUsage(modelAgent, currentModel)}` };
|
|
1486
1377
|
}
|
|
1487
|
-
return `当前模型: ${currentModel}${effortHint}
|
|
1378
|
+
return { kind: 'command.result', text: `当前模型: ${currentModel}${effortHint}` };
|
|
1488
1379
|
}
|
|
1489
1380
|
// 带参(切换/调整)需 admin+;无参查询已在上方返回
|
|
1490
1381
|
if (!isAdmin)
|
|
1491
|
-
return '❌ 无权限:切换模型仅限管理员使用';
|
|
1382
|
+
return { kind: 'command.error', text: '❌ 无权限:切换模型仅限管理员使用' };
|
|
1492
1383
|
const parts = args.split(/\s+/);
|
|
1493
1384
|
let newModel;
|
|
1494
1385
|
let newEffort;
|
|
@@ -1498,10 +1389,11 @@ export class CommandHandler {
|
|
|
1498
1389
|
const efforts = getAvailableEfforts(modelAgent, currentModel);
|
|
1499
1390
|
// effort 相关参数统一转发到 /effort
|
|
1500
1391
|
if (efforts.includes(arg) || arg === 'auto') {
|
|
1501
|
-
|
|
1392
|
+
const delegated = await this.handle(`/effort ${arg}`, channel, channelId, undefined, userId, threadId);
|
|
1393
|
+
return typeof delegated === 'string' ? { kind: 'command.result', text: delegated } : delegated;
|
|
1502
1394
|
}
|
|
1503
1395
|
else if (allEfforts.includes(arg)) {
|
|
1504
|
-
return `⚠️ 请使用 /effort ${arg}
|
|
1396
|
+
return { kind: 'command.error', text: `⚠️ 请使用 /effort ${arg} 调整推理强度` };
|
|
1505
1397
|
}
|
|
1506
1398
|
else if (models.includes(arg)) {
|
|
1507
1399
|
newModel = arg;
|
|
@@ -1509,34 +1401,33 @@ export class CommandHandler {
|
|
|
1509
1401
|
else {
|
|
1510
1402
|
const modelList = models.map((m) => ` ${m === currentModel ? '✓' : ' '} ${m}`).join('\n');
|
|
1511
1403
|
const effortHint = efforts.length > 0 ? `\n\n推理强度请使用 /effort 命令` : '';
|
|
1512
|
-
return `❌ 无效参数: ${arg}\n\n可用模型:\n${modelList}${effortHint}
|
|
1404
|
+
return { kind: 'command.error', text: `❌ 无效参数: ${arg}\n\n可用模型:\n${modelList}${effortHint}` };
|
|
1513
1405
|
}
|
|
1514
1406
|
}
|
|
1515
1407
|
else {
|
|
1516
1408
|
// 双参数:model effort
|
|
1517
1409
|
const [modelArg, effortArg] = parts;
|
|
1518
1410
|
if (!models.includes(modelArg)) {
|
|
1519
|
-
return `❌ 无效的模型ID: ${modelArg}
|
|
1411
|
+
return { kind: 'command.error', text: `❌ 无效的模型ID: ${modelArg}` };
|
|
1520
1412
|
}
|
|
1521
1413
|
const targetEfforts = getAvailableEfforts(modelAgent, modelArg);
|
|
1522
1414
|
if (targetEfforts.length === 0) {
|
|
1523
|
-
return `⚠️ ${modelArg}
|
|
1415
|
+
return { kind: 'command.error', text: `⚠️ ${modelArg} 不支持推理强度设置` };
|
|
1524
1416
|
}
|
|
1525
1417
|
if (!targetEfforts.includes(effortArg)) {
|
|
1526
1418
|
const errorLabel = allEfforts.includes(effortArg) ? '⚠️' : '❌';
|
|
1527
|
-
return `${errorLabel} ${modelArg} 不支持 ${effortArg} 推理强度\n可选: ${targetEfforts.join(' / ')}
|
|
1419
|
+
return { kind: 'command.result', text: `${errorLabel} ${modelArg} 不支持 ${effortArg} 推理强度\n可选: ${targetEfforts.join(' / ')}` };
|
|
1528
1420
|
}
|
|
1529
1421
|
newModel = modelArg;
|
|
1530
1422
|
newEffort = effortArg;
|
|
1531
1423
|
}
|
|
1532
|
-
|
|
1533
|
-
this.config.agents = {};
|
|
1424
|
+
// 运行时 model/effort 切换已通过 EvolAgent.setBaseagentModel/setBaseagentEffort 持久化
|
|
1534
1425
|
const isCodexAgent = modelAgent.name === 'codex';
|
|
1535
1426
|
const changes = [];
|
|
1536
1427
|
if (newModel) {
|
|
1537
1428
|
modelAgent.setModel?.(newModel);
|
|
1538
1429
|
this.eventBus.publish({
|
|
1539
|
-
type: '
|
|
1430
|
+
type: 'runner:model-changed',
|
|
1540
1431
|
sessionId: modelSession.id,
|
|
1541
1432
|
model: newModel,
|
|
1542
1433
|
timestamp: Date.now()
|
|
@@ -1551,231 +1442,160 @@ export class CommandHandler {
|
|
|
1551
1442
|
if (newModel) {
|
|
1552
1443
|
const err = this.persistBaseagentModel(channel, modelAgent.name, newModel);
|
|
1553
1444
|
if (err)
|
|
1554
|
-
return `${err}\n
|
|
1445
|
+
return { kind: 'command.result', text: `${err}\n已更新运行时配置,但未持久化` };
|
|
1555
1446
|
}
|
|
1556
1447
|
if (newEffort) {
|
|
1557
1448
|
const err = this.persistBaseagentEffort(channel, modelAgent.name, newEffort);
|
|
1558
1449
|
if (err)
|
|
1559
|
-
return `${err}\n
|
|
1450
|
+
return { kind: 'command.result', text: `${err}\n已更新运行时配置,但未持久化` };
|
|
1560
1451
|
}
|
|
1561
|
-
return `✓ 已切换\n ${changes.join('\n ')}
|
|
1452
|
+
return { kind: 'command.result', text: `✓ 已切换\n ${changes.join('\n ')}` };
|
|
1562
1453
|
}
|
|
1563
1454
|
// /effort 命令:查看或切换推理强度
|
|
1564
1455
|
if (normalizedContent.startsWith('/effort')) {
|
|
1565
1456
|
const args = normalizedContent.slice(7).trim();
|
|
1566
|
-
const effortResult = await this.ensureSession(channel, channelId, threadId);
|
|
1457
|
+
const effortResult = await this.ensureSession(channel, channelId, threadId, chatType);
|
|
1567
1458
|
if ('error' in effortResult)
|
|
1568
|
-
return effortResult.error;
|
|
1459
|
+
return { kind: 'command.result', text: effortResult.error };
|
|
1569
1460
|
const { session: effortSession } = effortResult;
|
|
1570
1461
|
const effortAgent = this.getAgent(channel, effortSession.agentId);
|
|
1571
1462
|
const currentModel = hasModelSwitcher(effortAgent) ? effortAgent.getModel() : effortAgent.name;
|
|
1572
1463
|
const efforts = getAvailableEfforts(effortAgent, currentModel);
|
|
1573
1464
|
const currentEffort = effortAgent.getEffort?.() || 'auto';
|
|
1574
1465
|
if (efforts.length === 0) {
|
|
1575
|
-
return '⚠️ 当前模型不支持推理强度设置';
|
|
1466
|
+
return { kind: 'command.error', text: '⚠️ 当前模型不支持推理强度设置' };
|
|
1576
1467
|
}
|
|
1577
1468
|
if (!args) {
|
|
1578
|
-
// /effort(无参数):显示当前推理强度 + 发送
|
|
1469
|
+
// /effort(无参数):显示当前推理强度 + 发送 CommandCard 卡片
|
|
1579
1470
|
if (this.interactionRouter) {
|
|
1580
|
-
const
|
|
1581
|
-
const buttons = [
|
|
1582
|
-
...efforts.map(e => ({
|
|
1583
|
-
key: e,
|
|
1584
|
-
label: e === currentEffort ? `✓ ${e}` : e,
|
|
1585
|
-
style: e === currentEffort ? 'primary' : 'default',
|
|
1586
|
-
})),
|
|
1587
|
-
{
|
|
1588
|
-
key: 'auto',
|
|
1589
|
-
label: currentEffort === 'auto' ? '✓ auto' : 'auto',
|
|
1590
|
-
style: currentEffort === 'auto' ? 'primary' : 'default',
|
|
1591
|
-
},
|
|
1592
|
-
];
|
|
1471
|
+
const allItems = [...efforts, 'auto'];
|
|
1593
1472
|
const interaction = {
|
|
1594
1473
|
type: 'interaction',
|
|
1595
|
-
id:
|
|
1474
|
+
id: `effort-${Date.now()}-${crypto.randomBytes(4).toString('hex')}`,
|
|
1596
1475
|
channelId,
|
|
1597
1476
|
sessionId: effortSession.id,
|
|
1477
|
+
initiatorId: userId,
|
|
1598
1478
|
kind: {
|
|
1599
|
-
kind: '
|
|
1479
|
+
kind: 'command-card',
|
|
1600
1480
|
title: '⚡ 推理强度',
|
|
1601
|
-
buttons
|
|
1481
|
+
buttons: allItems.map(e => ({
|
|
1482
|
+
label: e === currentEffort ? `✓ ${e}` : e,
|
|
1483
|
+
command: `/effort ${e}`,
|
|
1484
|
+
style: (e === currentEffort ? 'primary' : 'default'),
|
|
1485
|
+
disabled: e === currentEffort,
|
|
1486
|
+
})),
|
|
1602
1487
|
},
|
|
1603
1488
|
};
|
|
1604
1489
|
const replyCtx = this.getReplyContext(effortSession);
|
|
1605
|
-
const
|
|
1606
|
-
|
|
1607
|
-
canWrite: isAdmin,
|
|
1608
|
-
callback: async (action, _values, operatorId) => {
|
|
1609
|
-
if (action !== currentEffort) {
|
|
1610
|
-
if (userId && operatorId && operatorId !== userId)
|
|
1611
|
-
return;
|
|
1612
|
-
const result = await this.handle(`/effort ${action}`, channel, channelId, undefined, userId, threadId);
|
|
1613
|
-
if (result) {
|
|
1614
|
-
const adapter = this.adapters.get(channel);
|
|
1615
|
-
adapter?.sendText(channelId, result, replyCtx);
|
|
1616
|
-
}
|
|
1617
|
-
}
|
|
1618
|
-
},
|
|
1619
|
-
});
|
|
1620
|
-
if (cardSent)
|
|
1490
|
+
const cardResult = await this.sendCommandCard({ channel, channelId, interaction, replyCtx, canWrite: isAdmin });
|
|
1491
|
+
if (cardResult === null)
|
|
1621
1492
|
return null;
|
|
1493
|
+
return { kind: 'command.result', text: cardResult };
|
|
1622
1494
|
}
|
|
1623
1495
|
// 降级:文本
|
|
1624
1496
|
const effortDisplay = currentEffort === 'auto' ? 'auto (SDK默认)' : currentEffort;
|
|
1625
1497
|
const allItems = [...efforts, 'auto'];
|
|
1626
1498
|
const effortList = allItems.map(e => ` ${e === currentEffort ? '✓' : ' '} ${e}${e === 'auto' ? ' (SDK默认)' : ''}`).join('\n');
|
|
1627
1499
|
if (isAdmin) {
|
|
1628
|
-
return `⚡ 推理强度: ${effortDisplay}\n\n可选:\n${effortList}\n\n用法: /effort <level
|
|
1500
|
+
return { kind: 'command.result', text: `⚡ 推理强度: ${effortDisplay}\n\n可选:\n${effortList}\n\n用法: /effort <level>` };
|
|
1629
1501
|
}
|
|
1630
|
-
return `⚡ 推理强度: ${effortDisplay}
|
|
1502
|
+
return { kind: 'command.result', text: `⚡ 推理强度: ${effortDisplay}` };
|
|
1631
1503
|
}
|
|
1632
1504
|
// 带参(切换)需 admin+;无参查询已在上方返回
|
|
1633
1505
|
if (!isAdmin)
|
|
1634
|
-
return '❌ 无权限:切换推理强度仅限管理员使用';
|
|
1506
|
+
return { kind: 'command.error', text: '❌ 无权限:切换推理强度仅限管理员使用' };
|
|
1635
1507
|
// /effort auto:恢复 SDK 默认
|
|
1636
1508
|
if (args === 'auto') {
|
|
1637
1509
|
effortAgent.setEffort?.(undefined);
|
|
1638
1510
|
const err = this.persistBaseagentEffort(channel, effortAgent.name, undefined);
|
|
1639
1511
|
if (err)
|
|
1640
|
-
return `${err}\n
|
|
1641
|
-
return '✓ 推理强度已恢复为 auto (SDK默认)';
|
|
1512
|
+
return { kind: 'command.result', text: `${err}\n已更新运行时配置,但未持久化` };
|
|
1513
|
+
return { kind: 'command.result', text: '✓ 推理强度已恢复为 auto (SDK默认)' };
|
|
1642
1514
|
}
|
|
1643
1515
|
// /effort <level>:切换推理强度
|
|
1644
1516
|
if (!efforts.includes(args)) {
|
|
1645
1517
|
if (allEfforts.includes(args)) {
|
|
1646
|
-
return `⚠️ ${currentModel} 不支持 ${args} 推理强度\n可选: ${efforts.join(' / ')}
|
|
1518
|
+
return { kind: 'command.error', text: `⚠️ ${currentModel} 不支持 ${args} 推理强度\n可选: ${efforts.join(' / ')}` };
|
|
1647
1519
|
}
|
|
1648
|
-
return `❌ 无效参数: ${args}\n可选: ${efforts.join(' / ')} / auto
|
|
1520
|
+
return { kind: 'command.error', text: `❌ 无效参数: ${args}\n可选: ${efforts.join(' / ')} / auto` };
|
|
1649
1521
|
}
|
|
1650
1522
|
const newEffort = args;
|
|
1651
1523
|
effortAgent.setEffort?.(newEffort);
|
|
1652
1524
|
const err = this.persistBaseagentEffort(channel, effortAgent.name, newEffort);
|
|
1653
1525
|
if (err)
|
|
1654
|
-
return `${err}\n
|
|
1655
|
-
return `✓ 推理强度: ${newEffort}
|
|
1656
|
-
}
|
|
1657
|
-
// /aid 命令:AID 身份管理(list / new)
|
|
1658
|
-
if (normalizedContent === '/aid' || normalizedContent === '/aid list' || normalizedContent.startsWith('/aid ')) {
|
|
1659
|
-
if (!isOwner)
|
|
1660
|
-
return '❌ 无权限:此命令仅限 owner 使用';
|
|
1661
|
-
const arg = normalizedContent.slice(4).trim();
|
|
1662
|
-
const { aidList, aidCreate, agentmdPut, buildInitialAgentMd, isValidAid } = await import('../channels/aun-ops.js');
|
|
1663
|
-
// /aid 或 /aid list — 列出本地所有 AID
|
|
1664
|
-
if (!arg || arg === 'list') {
|
|
1665
|
-
const aids = aidList();
|
|
1666
|
-
if (aids.length === 0)
|
|
1667
|
-
return '本地无 AID';
|
|
1668
|
-
const lines = ['本地 AID:'];
|
|
1669
|
-
for (const a of aids) {
|
|
1670
|
-
const icons = [
|
|
1671
|
-
a.hasPrivateKey ? '🔑' : ' ',
|
|
1672
|
-
a.hasAgentMd ? '📄' : ' ',
|
|
1673
|
-
].join('');
|
|
1674
|
-
lines.push(` ${icons} ${a.aid}`);
|
|
1675
|
-
}
|
|
1676
|
-
lines.push('\n🔑=私钥 📄=agent.md');
|
|
1677
|
-
return lines.join('\n');
|
|
1678
|
-
}
|
|
1679
|
-
// /aid new <aid> — 创建 AID(纯身份,不动 config)
|
|
1680
|
-
if (arg.startsWith('new ')) {
|
|
1681
|
-
const rawAid = arg.slice(4).trim();
|
|
1682
|
-
if (!rawAid)
|
|
1683
|
-
return '用法: /aid new <完整AID>\n例: /aid new reviewer.agentid.pub';
|
|
1684
|
-
if (!isValidAid(rawAid))
|
|
1685
|
-
return `❌ 无效 AID 格式: ${rawAid}`;
|
|
1686
|
-
try {
|
|
1687
|
-
const result = await aidCreate(rawAid);
|
|
1688
|
-
if (!result.alreadyExisted) {
|
|
1689
|
-
const content = buildInitialAgentMd({ aid: rawAid });
|
|
1690
|
-
try {
|
|
1691
|
-
await agentmdPut(content, { aid: rawAid, client: result.client });
|
|
1692
|
-
}
|
|
1693
|
-
catch { /* non-fatal */ }
|
|
1694
|
-
}
|
|
1695
|
-
try {
|
|
1696
|
-
await result.client.close();
|
|
1697
|
-
}
|
|
1698
|
-
catch { /* ignore */ }
|
|
1699
|
-
const verb = result.alreadyExisted ? '已存在' : '已创建';
|
|
1700
|
-
return `✓ ${rawAid} ${verb}
|
|
1701
|
-
如需上线 AUN 通道,运行 evolclaw init aun`;
|
|
1702
|
-
}
|
|
1703
|
-
catch (e) {
|
|
1704
|
-
return `❌ 创建失败: ${String(e.message || e).slice(0, 200)}`;
|
|
1705
|
-
}
|
|
1706
|
-
}
|
|
1707
|
-
return '用法: /aid [list|new <aid>]';
|
|
1526
|
+
return { kind: 'command.result', text: `${err}\n已更新运行时配置,但未持久化` };
|
|
1527
|
+
return { kind: 'command.result', text: `✓ 推理强度: ${newEffort}` };
|
|
1708
1528
|
}
|
|
1709
|
-
// /
|
|
1710
|
-
if (normalizedContent === '/
|
|
1529
|
+
// /aid, /rpc, /storage — 转发到 CLI 执行
|
|
1530
|
+
if (normalizedContent === '/aid' || normalizedContent.startsWith('/aid ') ||
|
|
1531
|
+
normalizedContent === '/rpc' || normalizedContent.startsWith('/rpc ') ||
|
|
1532
|
+
normalizedContent === '/storage' || normalizedContent.startsWith('/storage ')) {
|
|
1711
1533
|
if (!isOwner)
|
|
1712
|
-
return '❌ 无权限:此命令仅限 owner 使用';
|
|
1713
|
-
|
|
1714
|
-
if (
|
|
1715
|
-
return
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
// set <content> — upload inline content
|
|
1739
|
-
if (arg.startsWith('set ')) {
|
|
1740
|
-
const content = arg.slice(4).trim();
|
|
1741
|
-
if (!content)
|
|
1742
|
-
return '用法:/agentmd set <内容>';
|
|
1743
|
-
if (!selfAid)
|
|
1744
|
-
return '❌ 未连接,无法确定本地 AID';
|
|
1745
|
-
try {
|
|
1746
|
-
await agentmdPut(content, { aid: selfAid });
|
|
1747
|
-
return '✅ agent.md 已更新并发布到AUN网络';
|
|
1748
|
-
}
|
|
1749
|
-
catch (e) {
|
|
1750
|
-
return `❌ 发布失败: ${String(e.message || e).slice(0, 100)}`;
|
|
1751
|
-
}
|
|
1534
|
+
return { kind: 'command.error', text: '❌ 无权限:此命令仅限 owner 使用' };
|
|
1535
|
+
// 无参数时返回用法说明
|
|
1536
|
+
if (normalizedContent === '/aid') {
|
|
1537
|
+
return { kind: 'command.result', text: `🆔 AID 身份管理
|
|
1538
|
+
|
|
1539
|
+
用法:
|
|
1540
|
+
/aid list 列出本地所有 AID
|
|
1541
|
+
/aid show <aid> 查看 AID 详情
|
|
1542
|
+
/aid new <aid> 创建新 AID
|
|
1543
|
+
/aid delete <aid> 删除本地 AID
|
|
1544
|
+
/aid lookup <aid> 远程探测 AID
|
|
1545
|
+
/aid agentmd put <aid> 签名并上传 agent.md
|
|
1546
|
+
/aid agentmd get <aid> 下载并验签 agent.md` };
|
|
1547
|
+
}
|
|
1548
|
+
if (normalizedContent === '/rpc') {
|
|
1549
|
+
return { kind: 'command.result', text: `📡 AUN RPC 调用
|
|
1550
|
+
|
|
1551
|
+
用法:
|
|
1552
|
+
/rpc --as <aid> --params <json>
|
|
1553
|
+
|
|
1554
|
+
参数格式:
|
|
1555
|
+
单行 JSON 单次调用
|
|
1556
|
+
多行 JSONL 逐行执行,失败即停
|
|
1557
|
+
|
|
1558
|
+
示例:
|
|
1559
|
+
/rpc --as myaid.agentid.pub --params {"method":"meta.ping","params":{}}` };
|
|
1752
1560
|
}
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1561
|
+
if (normalizedContent === '/storage') {
|
|
1562
|
+
return { kind: 'command.result', text: `📦 文件存储
|
|
1563
|
+
|
|
1564
|
+
用法:
|
|
1565
|
+
/storage upload <aid> <file> <path> [--public] 上传文件
|
|
1566
|
+
/storage download <aid> <url> [local-path] 下载文件
|
|
1567
|
+
/storage ls <aid> [prefix] 列文件
|
|
1568
|
+
/storage rm <aid> <path> 删文件
|
|
1569
|
+
/storage quota <aid> 查配额` };
|
|
1570
|
+
}
|
|
1571
|
+
const cliArgs = normalizedContent.slice(1); // strip leading /
|
|
1757
1572
|
try {
|
|
1758
|
-
const
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1573
|
+
const { execFile } = await import('node:child_process');
|
|
1574
|
+
const { promisify } = await import('node:util');
|
|
1575
|
+
const execFileAsync = promisify(execFile);
|
|
1576
|
+
const { stdout, stderr } = await execFileAsync('evolclaw', cliArgs.split(/\s+/), {
|
|
1577
|
+
timeout: 30000,
|
|
1578
|
+
encoding: 'utf-8',
|
|
1579
|
+
env: { ...process.env, AUN_LOG_INI_DISABLE: '1' },
|
|
1580
|
+
});
|
|
1581
|
+
const output = (stdout || '').trim();
|
|
1582
|
+
if (!output && stderr)
|
|
1583
|
+
return { kind: 'command.result', text: `⚠ ${stderr.trim().slice(0, 500)}` };
|
|
1584
|
+
return { kind: 'command.result', text: output || '(无输出)' };
|
|
1762
1585
|
}
|
|
1763
1586
|
catch (e) {
|
|
1764
|
-
const msg = String(e.message || e);
|
|
1765
|
-
|
|
1766
|
-
return `ℹ️ ${aidToView} 尚未设置 agent.md`;
|
|
1767
|
-
}
|
|
1768
|
-
return `❌ 获取失败: ${msg.slice(0, 100)}`;
|
|
1587
|
+
const msg = e.stderr?.trim() || e.stdout?.trim() || String(e.message || e);
|
|
1588
|
+
return { kind: 'command.error', text: `❌ ${msg.slice(0, 500)}` };
|
|
1769
1589
|
}
|
|
1770
1590
|
}
|
|
1771
1591
|
if (normalizedContent === '/activity' || normalizedContent.startsWith('/activity ')) {
|
|
1772
1592
|
const activityArg = normalizedContent.slice(9).trim();
|
|
1773
1593
|
// 带参(写操作)需 admin+;无参查询对所有人开放(owner 门在具体切换点还有一道)
|
|
1774
1594
|
if (activityArg && !isAdmin)
|
|
1775
|
-
return '❌ 无权限:此命令仅限管理员使用';
|
|
1595
|
+
return { kind: 'command.error', text: '❌ 无权限:此命令仅限管理员使用' };
|
|
1776
1596
|
// proactive 模式下流式输出全部静默,activity 配置无意义
|
|
1777
1597
|
if (activeSession?.sessionMode === 'proactive') {
|
|
1778
|
-
return '❌ 当前会话为 proactive 模式,不支持 activity 配置(流式输出已全部静默)';
|
|
1598
|
+
return { kind: 'command.error', text: '❌ 当前会话为 proactive 模式,不支持 activity 配置(流式输出已全部静默)' };
|
|
1779
1599
|
}
|
|
1780
1600
|
const modeMap = {
|
|
1781
1601
|
all: 'all',
|
|
@@ -1783,7 +1603,7 @@ export class CommandHandler {
|
|
|
1783
1603
|
owner: 'owner-dm-only',
|
|
1784
1604
|
none: 'none',
|
|
1785
1605
|
};
|
|
1786
|
-
const currentMode = this.agentRegistry?.getShowActivities?.(channel) ??
|
|
1606
|
+
const currentMode = this.agentRegistry?.getShowActivities?.(channel) ?? 'all';
|
|
1787
1607
|
// 模式描述列表(用于 body 和文本降级)
|
|
1788
1608
|
const modeDescriptions = [
|
|
1789
1609
|
{ key: 'all', configVal: 'all', label: '全部显示' },
|
|
@@ -1792,99 +1612,126 @@ export class CommandHandler {
|
|
|
1792
1612
|
{ key: 'none', configVal: 'none', label: '全部静默' },
|
|
1793
1613
|
];
|
|
1794
1614
|
if (!activityArg) {
|
|
1795
|
-
//
|
|
1796
|
-
|
|
1797
|
-
const requestId = `activity-${Date.now()}-${crypto.randomBytes(4).toString('hex')}`;
|
|
1798
|
-
const body = modeDescriptions.map(m => `${m.configVal === currentMode ? '✓' : '•'} **${m.key}** (${m.label})`).join('\n');
|
|
1799
|
-
const buttons = modeDescriptions.map(m => ({
|
|
1800
|
-
key: m.key,
|
|
1801
|
-
label: m.configVal === currentMode ? `✓ ${m.key}` : m.key,
|
|
1802
|
-
style: m.configVal === currentMode ? 'primary' : 'default',
|
|
1803
|
-
}));
|
|
1615
|
+
// 尝试发送 CommandCard 卡片
|
|
1616
|
+
{
|
|
1804
1617
|
const interaction = {
|
|
1805
1618
|
type: 'interaction',
|
|
1806
|
-
id:
|
|
1619
|
+
id: `activity-${Date.now()}-${crypto.randomBytes(4).toString('hex')}`,
|
|
1807
1620
|
channelId,
|
|
1808
|
-
sessionId: activeSession?.id ||
|
|
1621
|
+
sessionId: activeSession?.id || '',
|
|
1622
|
+
initiatorId: userId,
|
|
1809
1623
|
kind: {
|
|
1810
|
-
kind: '
|
|
1624
|
+
kind: 'command-card',
|
|
1811
1625
|
title: '📋 中间输出模式',
|
|
1812
|
-
body,
|
|
1813
|
-
buttons
|
|
1626
|
+
body: modeDescriptions.map(m => `${m.configVal === currentMode ? '✓' : '•'} **${m.key}** (${m.label})`).join('\n'),
|
|
1627
|
+
buttons: modeDescriptions.map(m => ({
|
|
1628
|
+
label: m.configVal === currentMode ? `✓ ${m.key}` : m.key,
|
|
1629
|
+
command: `/activity ${m.key}`,
|
|
1630
|
+
style: (m.configVal === currentMode ? 'primary' : 'default'),
|
|
1631
|
+
disabled: m.configVal === currentMode,
|
|
1632
|
+
})),
|
|
1814
1633
|
},
|
|
1815
1634
|
};
|
|
1816
1635
|
const replyCtx = activeSession ? this.getReplyContext(activeSession) : undefined;
|
|
1817
|
-
const
|
|
1818
|
-
|
|
1819
|
-
canWrite: isOwner,
|
|
1820
|
-
callback: async (action, _values, operatorId) => {
|
|
1821
|
-
const newMode = modeMap[action];
|
|
1822
|
-
if (newMode && newMode !== currentMode) {
|
|
1823
|
-
if (userId && operatorId && operatorId !== userId)
|
|
1824
|
-
return;
|
|
1825
|
-
const result = await this.handle(`/activity ${action}`, channel, channelId, undefined, userId, threadId);
|
|
1826
|
-
if (result) {
|
|
1827
|
-
const adapter = this.adapters.get(channel);
|
|
1828
|
-
adapter?.sendText(channelId, result, replyCtx);
|
|
1829
|
-
}
|
|
1830
|
-
}
|
|
1831
|
-
},
|
|
1832
|
-
});
|
|
1833
|
-
if (cardSent)
|
|
1636
|
+
const cardResult = await this.sendCommandCard({ channel, channelId, interaction, replyCtx, canWrite: isOwner });
|
|
1637
|
+
if (cardResult === null)
|
|
1834
1638
|
return null;
|
|
1639
|
+
// 卡片降级:fall through 到下方文本输出
|
|
1835
1640
|
}
|
|
1836
1641
|
// 降级:文本
|
|
1837
1642
|
const modeList = modeDescriptions.map(m => {
|
|
1838
|
-
const prefix = m.configVal === currentMode ? '✓' : '
|
|
1839
|
-
return ` ${prefix} ${m.key}
|
|
1643
|
+
const prefix = m.configVal === currentMode ? '✓' : '•';
|
|
1644
|
+
return ` ${prefix} ${m.key} — ${m.label}`;
|
|
1840
1645
|
}).join('\n');
|
|
1841
1646
|
if (isOwner) {
|
|
1842
|
-
return `📋 中间输出模式: ${currentMode}
|
|
1647
|
+
return { kind: 'command.result', text: [`📋 中间输出模式: ${currentMode}`, '', modeList, '', '用法: /activity <all|dm|owner|none>'].join('\n') };
|
|
1843
1648
|
}
|
|
1844
|
-
return `📋 中间输出模式: ${currentMode}
|
|
1649
|
+
return { kind: 'command.result', text: `📋 中间输出模式: ${currentMode}` };
|
|
1845
1650
|
}
|
|
1846
1651
|
const newMode = modeMap[activityArg];
|
|
1847
1652
|
if (!newMode) {
|
|
1848
|
-
return `❌ 无效参数: ${activityArg}\n可选: all / dm / owner / none
|
|
1653
|
+
return { kind: 'command.error', text: `❌ 无效参数: ${activityArg}\n可选: all / dm / owner / none` };
|
|
1849
1654
|
}
|
|
1850
1655
|
const label = modeDescriptions.find(m => m.configVal === newMode)?.label || newMode;
|
|
1851
1656
|
if (newMode === currentMode) {
|
|
1852
|
-
return `📋 中间输出模式已是 ${activityArg}(${label}
|
|
1657
|
+
return { kind: 'command.result', text: `📋 中间输出模式已是 ${activityArg}(${label})` };
|
|
1853
1658
|
}
|
|
1854
1659
|
// 切换操作仅 owner
|
|
1855
1660
|
if (!isOwner)
|
|
1856
|
-
return '❌ 中间输出模式切换仅限 owner';
|
|
1661
|
+
return { kind: 'command.error', text: '❌ 中间输出模式切换仅限 owner' };
|
|
1857
1662
|
if (this.agentRegistry?.setShowActivities) {
|
|
1858
1663
|
this.agentRegistry.setShowActivities(channel, newMode);
|
|
1859
1664
|
}
|
|
1860
1665
|
else {
|
|
1861
|
-
|
|
1666
|
+
return { kind: 'command.error', text: `⚠️ 找不到通道 "${channel}" 所属的 self-agent,无法持久化` };
|
|
1862
1667
|
}
|
|
1863
|
-
return `✅ 中间输出模式: ${activityArg}(${label}
|
|
1668
|
+
return { kind: 'command.result', text: `✅ 中间输出模式: ${activityArg}(${label})` };
|
|
1864
1669
|
}
|
|
1865
1670
|
// /chatmode 命令:查看/切换 session 会话模式(interactive | proactive)
|
|
1866
1671
|
// - 查看:所有人可用
|
|
1867
1672
|
// - 设置:单聊任何角色可设置;群聊仅管理员可设置
|
|
1868
1673
|
if (normalizedContent === '/chatmode' || normalizedContent.startsWith('/chatmode ')) {
|
|
1869
|
-
|
|
1870
|
-
|
|
1674
|
+
const chatmodeResult = await this.ensureSession(channel, channelId, threadId, chatType);
|
|
1675
|
+
if ('error' in chatmodeResult)
|
|
1676
|
+
return { kind: 'command.result', text: chatmodeResult.error };
|
|
1677
|
+
const chatmodeSession = chatmodeResult.session;
|
|
1871
1678
|
const arg = normalizedContent.slice(9).trim();
|
|
1872
|
-
const currentMode =
|
|
1679
|
+
const currentMode = chatmodeSession.sessionMode || 'interactive';
|
|
1680
|
+
const chatmodeChatType = chatmodeSession.chatType || activeChatType;
|
|
1681
|
+
const canSwitch = chatmodeChatType !== 'group' || isAdmin;
|
|
1873
1682
|
if (!arg) {
|
|
1874
|
-
|
|
1683
|
+
// 尝试发送 CommandCard 卡片
|
|
1875
1684
|
if (canSwitch) {
|
|
1876
|
-
|
|
1685
|
+
const modes = [
|
|
1686
|
+
{ key: 'interactive', name: '交互模式', desc: '被动响应:收到消息时才回复,回复直接显示' },
|
|
1687
|
+
{ key: 'proactive', name: '主动模式', desc: '主动推进:流式输出静默,由 Agent 自调 ctl send 发声' },
|
|
1688
|
+
];
|
|
1689
|
+
const interaction = {
|
|
1690
|
+
type: 'interaction',
|
|
1691
|
+
id: `chatmode-${Date.now()}-${crypto.randomBytes(4).toString('hex')}`,
|
|
1692
|
+
channelId,
|
|
1693
|
+
sessionId: chatmodeSession.id,
|
|
1694
|
+
initiatorId: userId,
|
|
1695
|
+
kind: {
|
|
1696
|
+
kind: 'command-card',
|
|
1697
|
+
title: '🔄 会话模式',
|
|
1698
|
+
body: modes.map(m => `${m.key === currentMode ? '✓' : '•'} **${m.key}** (${m.name}) - ${m.desc}`).join('\n'),
|
|
1699
|
+
buttons: modes.map(m => ({
|
|
1700
|
+
label: m.key === currentMode ? `✓ ${m.key}` : m.key,
|
|
1701
|
+
command: `/chatmode ${m.key}`,
|
|
1702
|
+
style: (m.key === currentMode ? 'primary' : 'default'),
|
|
1703
|
+
disabled: m.key === currentMode,
|
|
1704
|
+
})),
|
|
1705
|
+
},
|
|
1706
|
+
};
|
|
1707
|
+
const replyCtx = this.getReplyContext(chatmodeSession);
|
|
1708
|
+
const cardResult = await this.sendCommandCard({ channel, channelId, interaction, replyCtx, canWrite: isAdmin });
|
|
1709
|
+
if (cardResult === null)
|
|
1710
|
+
return null;
|
|
1711
|
+
// 卡片降级:fall through 到下方文本输出
|
|
1877
1712
|
}
|
|
1878
|
-
|
|
1713
|
+
// 降级:文本
|
|
1714
|
+
if (canSwitch) {
|
|
1715
|
+
return { kind: 'command.result', text: [
|
|
1716
|
+
`📋 会话模式: ${currentMode}`,
|
|
1717
|
+
'',
|
|
1718
|
+
'模式说明:',
|
|
1719
|
+
' • interactive — 交互模式:收到消息时才回复,回复直接显示',
|
|
1720
|
+
' • proactive — 主动模式:流式输出静默,由 Agent 自调 ctl send 发声',
|
|
1721
|
+
'',
|
|
1722
|
+
'用法: /chatmode <interactive|proactive>',
|
|
1723
|
+
].join('\n') };
|
|
1724
|
+
}
|
|
1725
|
+
return { kind: 'command.result', text: `📋 会话模式: ${currentMode}` };
|
|
1879
1726
|
}
|
|
1880
1727
|
if (arg !== 'interactive' && arg !== 'proactive') {
|
|
1881
|
-
return `❌ 无效模式: ${arg}\n可选: interactive / proactive
|
|
1728
|
+
return { kind: 'command.error', text: `❌ 无效模式: ${arg}\n可选: interactive / proactive` };
|
|
1882
1729
|
}
|
|
1883
|
-
if (activeChatType === 'group' && !isAdmin) {
|
|
1884
|
-
return '❌ 无权限:群聊中切换会话模式仅限管理员使用';
|
|
1730
|
+
if ((chatmodeSession.chatType || activeChatType) === 'group' && !isAdmin) {
|
|
1731
|
+
return { kind: 'command.error', text: '❌ 无权限:群聊中切换会话模式仅限管理员使用' };
|
|
1885
1732
|
}
|
|
1886
1733
|
if (arg === currentMode) {
|
|
1887
|
-
return `📋 当前会话模式已是 ${arg}
|
|
1734
|
+
return { kind: 'command.result', text: `📋 当前会话模式已是 ${arg}` };
|
|
1888
1735
|
}
|
|
1889
1736
|
// 仅在真正需要切换时才要求会话空闲
|
|
1890
1737
|
if (threadId) {
|
|
@@ -1892,53 +1739,126 @@ export class CommandHandler {
|
|
|
1892
1739
|
if (threadSession) {
|
|
1893
1740
|
const threadAgent = this.getAgent(channel, threadSession.agentId);
|
|
1894
1741
|
if (threadAgent.hasActiveStream(threadSession.id)) {
|
|
1895
|
-
return '⚠️ 当前正在处理消息,请稍后再试\n使用 /stop 中断当前任务后重试';
|
|
1742
|
+
return { kind: 'command.error', text: '⚠️ 当前正在处理消息,请稍后再试\n使用 /stop 中断当前任务后重试' };
|
|
1896
1743
|
}
|
|
1897
1744
|
}
|
|
1898
1745
|
}
|
|
1899
|
-
else if (agent.hasActiveStream(
|
|
1900
|
-
return '⚠️ 当前正在处理消息,请稍后再试\n使用 /stop 中断当前任务后重试';
|
|
1746
|
+
else if (agent.hasActiveStream(chatmodeSession.id)) {
|
|
1747
|
+
return { kind: 'command.error', text: '⚠️ 当前正在处理消息,请稍后再试\n使用 /stop 中断当前任务后重试' };
|
|
1901
1748
|
}
|
|
1902
|
-
await this.sessionManager.updateSession(
|
|
1903
|
-
|
|
1749
|
+
await this.sessionManager.updateSession(chatmodeSession.id, { sessionMode: arg });
|
|
1750
|
+
this.eventBus.publish({ type: 'session:chat-mode-changed', sessionId: chatmodeSession.id, mode: arg, timestamp: Date.now() });
|
|
1751
|
+
return { kind: 'command.result', text: `✅ 会话模式已切换: ${arg}` };
|
|
1752
|
+
}
|
|
1753
|
+
// /dispatch 命令:查看/切换群聊分发模式(mention | broadcast)
|
|
1754
|
+
// 仅群聊可用;群聊中设置需管理员权限
|
|
1755
|
+
if (normalizedContent === '/dispatch' || normalizedContent.startsWith('/dispatch ')) {
|
|
1756
|
+
const dispatchResult = await this.ensureSession(channel, channelId, threadId, chatType);
|
|
1757
|
+
if ('error' in dispatchResult)
|
|
1758
|
+
return { kind: 'command.result', text: dispatchResult.error };
|
|
1759
|
+
const dispatchSession = dispatchResult.session;
|
|
1760
|
+
const dispatchChatType = dispatchSession.chatType || activeChatType;
|
|
1761
|
+
if (dispatchChatType !== 'group') {
|
|
1762
|
+
return { kind: 'command.error', text: '❌ /dispatch 仅在群聊中可用' };
|
|
1763
|
+
}
|
|
1764
|
+
const arg = normalizedContent.slice(9).trim();
|
|
1765
|
+
const currentMode = dispatchSession.metadata?.dispatchMode;
|
|
1766
|
+
if (!arg) {
|
|
1767
|
+
const displayMode = currentMode ?? '未设置(跟随群设置)';
|
|
1768
|
+
// 尝试发送 CommandCard 卡片
|
|
1769
|
+
if (isAdmin) {
|
|
1770
|
+
const modes = [
|
|
1771
|
+
{ key: 'mention', name: '提及模式', desc: '仅当被 @ 提及(含 @all)时响应群消息' },
|
|
1772
|
+
{ key: 'broadcast', name: '广播模式', desc: '群内所有消息都触发响应' },
|
|
1773
|
+
];
|
|
1774
|
+
const interaction = {
|
|
1775
|
+
type: 'interaction',
|
|
1776
|
+
id: `dispatch-${Date.now()}-${crypto.randomBytes(4).toString('hex')}`,
|
|
1777
|
+
channelId,
|
|
1778
|
+
sessionId: dispatchSession.id,
|
|
1779
|
+
initiatorId: userId,
|
|
1780
|
+
kind: {
|
|
1781
|
+
kind: 'command-card',
|
|
1782
|
+
title: '📡 分发模式',
|
|
1783
|
+
body: modes.map(m => `${m.key === currentMode ? '✓' : '•'} **${m.key}** (${m.name}) - ${m.desc}`).join('\n'),
|
|
1784
|
+
buttons: modes.map(m => ({
|
|
1785
|
+
label: m.key === currentMode ? `✓ ${m.key}` : m.key,
|
|
1786
|
+
command: `/dispatch ${m.key}`,
|
|
1787
|
+
style: (m.key === currentMode ? 'primary' : 'default'),
|
|
1788
|
+
disabled: m.key === currentMode,
|
|
1789
|
+
})),
|
|
1790
|
+
},
|
|
1791
|
+
};
|
|
1792
|
+
const replyCtx = this.getReplyContext(dispatchSession);
|
|
1793
|
+
const cardResult = await this.sendCommandCard({ channel, channelId, interaction, replyCtx, canWrite: isAdmin });
|
|
1794
|
+
if (cardResult === null)
|
|
1795
|
+
return null;
|
|
1796
|
+
// 卡片降级:fall through 到下方文本输出
|
|
1797
|
+
}
|
|
1798
|
+
// 降级:文本
|
|
1799
|
+
const lines = [];
|
|
1800
|
+
lines.push(`📋 分发模式: ${displayMode}`);
|
|
1801
|
+
lines.push('');
|
|
1802
|
+
lines.push('模式说明:');
|
|
1803
|
+
lines.push(' • mention — 提及模式:仅当被@提及时响应群消息(含@all)');
|
|
1804
|
+
lines.push(' • broadcast — 广播模式:群内所有消息都触发响应');
|
|
1805
|
+
if (isAdmin) {
|
|
1806
|
+
lines.push('');
|
|
1807
|
+
lines.push('用法: /dispatch <mention|broadcast>');
|
|
1808
|
+
}
|
|
1809
|
+
return { kind: 'command.result', text: lines.join('\n') };
|
|
1810
|
+
}
|
|
1811
|
+
if (arg !== 'mention' && arg !== 'broadcast') {
|
|
1812
|
+
return { kind: 'command.error', text: `❌ 无效模式: ${arg}\n可选: mention / broadcast\n用法: /dispatch <模式>` };
|
|
1813
|
+
}
|
|
1814
|
+
if (!isAdmin) {
|
|
1815
|
+
return { kind: 'command.error', text: '❌ 无权限:群聊中切换分发模式仅限管理员使用' };
|
|
1816
|
+
}
|
|
1817
|
+
if (arg === currentMode) {
|
|
1818
|
+
return { kind: 'command.result', text: `📋 当前已是 ${arg}` };
|
|
1819
|
+
}
|
|
1820
|
+
const metadata = { ...(dispatchSession.metadata || {}), dispatchMode: arg };
|
|
1821
|
+
await this.sessionManager.updateSession(dispatchSession.id, { metadata });
|
|
1822
|
+
this.eventBus.publish({ type: 'session:dispatch-mode-changed', sessionId: dispatchSession.id, mode: arg, timestamp: Date.now() });
|
|
1823
|
+
return { kind: 'command.result', text: `✅ 分发模式已切换: ${currentMode ?? '未设置'} → ${arg}` };
|
|
1904
1824
|
}
|
|
1905
1825
|
// /stop 命令:中断当前任务
|
|
1906
1826
|
if (normalizedContent === '/stop') {
|
|
1907
|
-
const stopResult = await this.ensureSession(channel, channelId, threadId);
|
|
1827
|
+
const stopResult = await this.ensureSession(channel, channelId, threadId, chatType);
|
|
1908
1828
|
if ('error' in stopResult)
|
|
1909
|
-
return '当前没有正在处理的任务';
|
|
1829
|
+
return { kind: 'command.result', text: '当前没有正在处理的任务' };
|
|
1910
1830
|
const { session: stopSession } = stopResult;
|
|
1911
1831
|
const stopAgent = this.getAgent(channel, stopSession.agentId);
|
|
1912
1832
|
const sessionKey = stopSession.id;
|
|
1913
1833
|
const queueLength = this.messageQueue.getQueueLength(sessionKey);
|
|
1914
1834
|
const hasActive = stopAgent.hasActiveStream(sessionKey);
|
|
1915
1835
|
if (queueLength === 0 && !hasActive) {
|
|
1916
|
-
return '当前没有正在处理的任务';
|
|
1836
|
+
return { kind: 'command.result', text: '当前没有正在处理的任务' };
|
|
1917
1837
|
}
|
|
1918
1838
|
await stopAgent.interrupt(sessionKey);
|
|
1919
1839
|
// 发布中断事件,让 MessageProcessor 标记为 interrupted(而非 done)
|
|
1920
1840
|
this.eventBus.publish({
|
|
1921
|
-
type: '
|
|
1841
|
+
type: 'task:interrupted',
|
|
1922
1842
|
sessionId: sessionKey,
|
|
1923
1843
|
reason: 'stop',
|
|
1924
|
-
agentName: this.agentRegistry?.resolveByChannel(channel)?.name ?? '
|
|
1844
|
+
agentName: this.agentRegistry?.resolveByChannel(channel)?.name ?? '<unknown>',
|
|
1925
1845
|
});
|
|
1926
1846
|
// 强制清除 processing_state
|
|
1927
1847
|
this.sessionManager.clearProcessing(sessionKey);
|
|
1928
|
-
return '✓ 已发送中断信号,任务将尽快停止';
|
|
1848
|
+
return { kind: 'command.result', text: '✓ 已发送中断信号,任务将尽快停止' };
|
|
1929
1849
|
}
|
|
1930
1850
|
// /clear 命令:通过 SDK /clear 清空会话历史
|
|
1931
1851
|
if (normalizedContent === '/clear') {
|
|
1932
|
-
const result = await this.ensureSession(channel, channelId, threadId);
|
|
1852
|
+
const result = await this.ensureSession(channel, channelId, threadId, chatType);
|
|
1933
1853
|
if ('error' in result)
|
|
1934
|
-
return result.error;
|
|
1854
|
+
return { kind: 'command.error', text: result.error };
|
|
1935
1855
|
const { session } = result;
|
|
1936
1856
|
const sessionAgent = this.getAgent(channel, session.agentId);
|
|
1937
1857
|
if (!sessionAgent.capabilities?.clear) {
|
|
1938
|
-
return `❌ 当前 Agent (${sessionAgent.name}) 不支持 /clear\n\n可使用 /new
|
|
1858
|
+
return { kind: 'command.error', text: `❌ 当前 Agent (${sessionAgent.name}) 不支持 /clear\n\n可使用 /new 创建新会话替代` };
|
|
1939
1859
|
}
|
|
1940
1860
|
if (!session.agentSessionId) {
|
|
1941
|
-
return '❌ 当前会话没有历史记录,无需清空';
|
|
1861
|
+
return { kind: 'command.error', text: '❌ 当前会话没有历史记录,无需清空' };
|
|
1942
1862
|
}
|
|
1943
1863
|
const projectPath = path.isAbsolute(session.projectPath)
|
|
1944
1864
|
? session.projectPath
|
|
@@ -1949,10 +1869,10 @@ export class CommandHandler {
|
|
|
1949
1869
|
if (cleared) {
|
|
1950
1870
|
await this.sessionManager.updateAgentSessionIdBySessionId(session.id, '');
|
|
1951
1871
|
sessionAgent.updateSessionId(session.id, '');
|
|
1952
|
-
return '✅ 已清空当前会话的对话历史';
|
|
1872
|
+
return { kind: 'command.result', text: '✅ 已清空当前会话的对话历史' };
|
|
1953
1873
|
}
|
|
1954
1874
|
else {
|
|
1955
|
-
return '❌ 清空会话失败,请稍后重试';
|
|
1875
|
+
return { kind: 'command.error', text: '❌ 清空会话失败,请稍后重试' };
|
|
1956
1876
|
}
|
|
1957
1877
|
}
|
|
1958
1878
|
finally {
|
|
@@ -1961,16 +1881,16 @@ export class CommandHandler {
|
|
|
1961
1881
|
}
|
|
1962
1882
|
// /compact 命令:手动压缩会话上下文
|
|
1963
1883
|
if (normalizedContent === '/compact') {
|
|
1964
|
-
const result = await this.ensureSession(channel, channelId, threadId);
|
|
1884
|
+
const result = await this.ensureSession(channel, channelId, threadId, chatType);
|
|
1965
1885
|
if ('error' in result)
|
|
1966
|
-
return result.error;
|
|
1886
|
+
return { kind: 'command.error', text: result.error };
|
|
1967
1887
|
const { session } = result;
|
|
1968
1888
|
const sessionAgent = this.getAgent(channel, session.agentId);
|
|
1969
1889
|
if (!sessionAgent.capabilities?.compact) {
|
|
1970
|
-
return `❌ 当前 Agent (${sessionAgent.name}) 不支持 /compact
|
|
1890
|
+
return { kind: 'command.error', text: `❌ 当前 Agent (${sessionAgent.name}) 不支持 /compact` };
|
|
1971
1891
|
}
|
|
1972
1892
|
if (!session.agentSessionId) {
|
|
1973
|
-
return '❌ 当前会话没有历史记录,无需压缩';
|
|
1893
|
+
return { kind: 'command.error', text: '❌ 当前会话没有历史记录,无需压缩' };
|
|
1974
1894
|
}
|
|
1975
1895
|
const projectPath = path.isAbsolute(session.projectPath)
|
|
1976
1896
|
? session.projectPath
|
|
@@ -1982,10 +1902,10 @@ export class CommandHandler {
|
|
|
1982
1902
|
}
|
|
1983
1903
|
const compacted = await sessionAgent.compactSession(session.id, session.agentSessionId, projectPath);
|
|
1984
1904
|
if (compacted) {
|
|
1985
|
-
return '✅ 会话上下文已压缩';
|
|
1905
|
+
return { kind: 'command.result', text: '✅ 会话上下文已压缩' };
|
|
1986
1906
|
}
|
|
1987
1907
|
else {
|
|
1988
|
-
return '❌ 会话压缩失败,请稍后重试';
|
|
1908
|
+
return { kind: 'command.error', text: '❌ 会话压缩失败,请稍后重试' };
|
|
1989
1909
|
}
|
|
1990
1910
|
}
|
|
1991
1911
|
finally {
|
|
@@ -2013,7 +1933,7 @@ export class CommandHandler {
|
|
|
2013
1933
|
if (normalizedContent === '/status') {
|
|
2014
1934
|
// session 现在总是存在(上面已自动创建)
|
|
2015
1935
|
if (!session) {
|
|
2016
|
-
return `❌
|
|
1936
|
+
return { kind: 'command.error', text: `❌ 无法创建会话,请检查配置` };
|
|
2017
1937
|
}
|
|
2018
1938
|
const sessionKey = this.getQueueKey(session, channel, channelId);
|
|
2019
1939
|
const sessionAgent = this.getAgent(channel, session.agentId);
|
|
@@ -2032,6 +1952,8 @@ export class CommandHandler {
|
|
|
2032
1952
|
}
|
|
2033
1953
|
}
|
|
2034
1954
|
const projectName = this.getProjectName(session.projectPath);
|
|
1955
|
+
const owningAgent = this.getOwningAgent(channel);
|
|
1956
|
+
const agentName = owningAgent?.name ?? 'DefaultAgent';
|
|
2035
1957
|
const health = await this.sessionManager.getHealthStatus(session.id);
|
|
2036
1958
|
const timeSinceSuccess = Date.now() - health.lastSuccessTime;
|
|
2037
1959
|
const timeStr = timeSinceSuccess < 60000 ? '刚刚' :
|
|
@@ -2049,23 +1971,25 @@ export class CommandHandler {
|
|
|
2049
1971
|
}
|
|
2050
1972
|
const lines = [];
|
|
2051
1973
|
const sessionMode = session.sessionMode || 'interactive';
|
|
1974
|
+
const dispatchMode = session.metadata?.dispatchMode ?? '未设置(跟随群设置)';
|
|
2052
1975
|
const chatModeLine = `会话模式: ${sessionMode}`;
|
|
1976
|
+
const dispatchModeLine = session.chatType === 'group' ? `分发模式: ${dispatchMode}` : null;
|
|
2053
1977
|
if (isAdmin) {
|
|
2054
|
-
lines.push(`📊 ${isThread ? '话题' : '会话'}
|
|
1978
|
+
lines.push(`📊 ${isThread ? '话题' : '会话'}状态 (Agent: ${agentName}):`, `渠道: ${this.resolveChannelType(channel)} / 项目: ${projectName} / 会话: ${session.name || '(未命名)'}`, `会话ID: ${session.id}`, `项目路径: ${session.projectPath}`, `会话状态: ${sessionStatus}`, chatModeLine, ...(dispatchModeLine ? [dispatchModeLine] : []), `会话轮数: ${sessionTurns}`);
|
|
2055
1979
|
if (health.consecutiveErrors > 0) {
|
|
2056
1980
|
lines.push(`异常计数: ${health.consecutiveErrors}`);
|
|
2057
1981
|
}
|
|
2058
1982
|
lines.push(`最后成功: ${timeStr}`, `${session.agentId}会话: ${session.agentSessionId || '(未初始化)'}`, `创建时间: ${new Date(session.createdAt).toLocaleString('zh-CN')}`, `更新时间: ${new Date(session.updatedAt).toLocaleString('zh-CN')}`);
|
|
2059
1983
|
}
|
|
2060
1984
|
else {
|
|
2061
|
-
lines.push(`📊 ${isThread ? '话题' : '会话'}
|
|
1985
|
+
lines.push(`📊 ${isThread ? '话题' : '会话'}状态 (Agent: ${agentName}):`, `渠道: ${channel} / 项目: ${projectName} / ${session.agentId}会话`, `状态: ${sessionStatus}`, chatModeLine, ...(dispatchModeLine ? [dispatchModeLine] : []), `会话轮数: ${sessionTurns}`, `最后活跃: ${timeStr}`);
|
|
2062
1986
|
}
|
|
2063
1987
|
if (health.lastError) {
|
|
2064
1988
|
lines.push('');
|
|
2065
1989
|
lines.push(`最后错误: ${health.lastErrorType || 'unknown'}`);
|
|
2066
1990
|
lines.push(`错误信息: ${health.lastError.substring(0, 100)}`);
|
|
2067
1991
|
}
|
|
2068
|
-
return lines.join('\n');
|
|
1992
|
+
return { kind: 'command.result', text: lines.join('\n') };
|
|
2069
1993
|
}
|
|
2070
1994
|
// /new 命令:创建新会话(支持命名)
|
|
2071
1995
|
if (normalizedContent.startsWith('/new')) {
|
|
@@ -2073,11 +1997,11 @@ export class CommandHandler {
|
|
|
2073
1997
|
if (sessionName) {
|
|
2074
1998
|
const existing = await this.sessionManager.getSessionByName(channel, channelId, sessionName);
|
|
2075
1999
|
if (existing) {
|
|
2076
|
-
return `❌ 会话名称 "${sessionName}"
|
|
2000
|
+
return { kind: 'command.error', text: `❌ 会话名称 "${sessionName}" 已存在,请使用其他名称` };
|
|
2077
2001
|
}
|
|
2078
2002
|
}
|
|
2079
|
-
const projectPath =
|
|
2080
|
-
const newSession = await this.sessionManager.createNewSession(channel, channelId, projectPath, sessionName, session?.agentId || this.
|
|
2003
|
+
const projectPath = this.getEffectiveDefaultPath(channel);
|
|
2004
|
+
const newSession = await this.sessionManager.createNewSession(channel, channelId, projectPath, sessionName, session?.agentId || this.primaryRunnerKey);
|
|
2081
2005
|
this.eventBus.publish({
|
|
2082
2006
|
type: 'session:created',
|
|
2083
2007
|
sessionId: newSession.id,
|
|
@@ -2093,7 +2017,7 @@ export class CommandHandler {
|
|
|
2093
2017
|
await agent.clearSession(session.id, session.agentSessionId || '', session.projectPath);
|
|
2094
2018
|
await agent.closeSession(session.id);
|
|
2095
2019
|
}
|
|
2096
|
-
return `✓ 已创建新会话${sessionName ? `: ${sessionName}` : ''}\n 之前的对话历史已保留,可通过 /s
|
|
2020
|
+
return { kind: 'command.result', text: `✓ 已创建新会话${sessionName ? `: ${sessionName}` : ''}\n 项目: ${this.getProjectName(projectPath)}\n 之前的对话历史已保留,可通过 /s 查看` };
|
|
2097
2021
|
}
|
|
2098
2022
|
// /check 命令:检查渠道状态(guest 可用,详情仅 admin)/ 重连指定渠道(admin only)
|
|
2099
2023
|
if (normalizedContent === '/check' || normalizedContent.startsWith('/check ')) {
|
|
@@ -2106,17 +2030,18 @@ export class CommandHandler {
|
|
|
2106
2030
|
allowedChannels = new Set(checkOwningAgent.channelInstanceNames());
|
|
2107
2031
|
}
|
|
2108
2032
|
else {
|
|
2109
|
-
// default
|
|
2033
|
+
// default 范围:不再有 default channel 概念,等价于"所有 channel"
|
|
2110
2034
|
const defaultNames = [];
|
|
2111
2035
|
for (const [name] of this.adapters) {
|
|
2112
2036
|
const owner = this.agentRegistry?.resolveByChannel(name);
|
|
2113
|
-
if (!owner
|
|
2037
|
+
if (!owner)
|
|
2114
2038
|
defaultNames.push(name);
|
|
2115
2039
|
}
|
|
2116
2040
|
allowedChannels = new Set(defaultNames);
|
|
2117
2041
|
}
|
|
2118
2042
|
// Default: show system health check (non-admin 仅看摘要)
|
|
2119
|
-
const
|
|
2043
|
+
const checkAgentName = checkOwningAgent?.name ?? 'DefaultAgent';
|
|
2044
|
+
const lines = [`📡 渠道状态 (Agent: ${checkAgentName}):`];
|
|
2120
2045
|
// Group by channelType
|
|
2121
2046
|
const groups = new Map();
|
|
2122
2047
|
for (const [name] of this.adapters) {
|
|
@@ -2127,7 +2052,7 @@ export class CommandHandler {
|
|
|
2127
2052
|
let status;
|
|
2128
2053
|
if (ch?.getStatus) {
|
|
2129
2054
|
const s = ch.getStatus();
|
|
2130
|
-
status = s.connected ? '✓ 已连接' :
|
|
2055
|
+
status = s.connected ? '✓ 已连接' : '⏳ 重连中';
|
|
2131
2056
|
}
|
|
2132
2057
|
else {
|
|
2133
2058
|
status = '✓ 已注册';
|
|
@@ -2141,19 +2066,23 @@ export class CommandHandler {
|
|
|
2141
2066
|
const total = [...groups.values()].flat().length;
|
|
2142
2067
|
const healthy = [...groups.values()].flat().filter(i => i.status.includes('✓')).length;
|
|
2143
2068
|
lines.push(` ${healthy}/${total} 渠道正常`);
|
|
2144
|
-
return lines.join('\n');
|
|
2069
|
+
return { kind: 'command.result', text: lines.join('\n') };
|
|
2145
2070
|
}
|
|
2146
2071
|
for (const [type, instances] of groups) {
|
|
2147
2072
|
if (instances.length === 1) {
|
|
2148
|
-
lines.push(` ${
|
|
2073
|
+
lines.push(` ${type}: ${instances[0].status}`);
|
|
2149
2074
|
}
|
|
2150
2075
|
else {
|
|
2151
|
-
const parts = instances.map(i =>
|
|
2152
|
-
|
|
2076
|
+
const parts = instances.map(i => {
|
|
2077
|
+
const seg = i.name.split('#');
|
|
2078
|
+
const instName = seg.length >= 3 ? seg.slice(2).join('#') : i.name;
|
|
2079
|
+
return `${i.status.includes('✓') ? '✓' : '⏳'} ${instName}`;
|
|
2080
|
+
});
|
|
2081
|
+
lines.push(` ${type}: ${parts.join(', ')}`);
|
|
2153
2082
|
}
|
|
2154
2083
|
}
|
|
2155
2084
|
// 当前 agent 名(用于 agent 维度 stats / queue 查询)
|
|
2156
|
-
const currentAgentName = checkOwningAgent?.name ?? '
|
|
2085
|
+
const currentAgentName = checkOwningAgent?.name ?? '<unknown>';
|
|
2157
2086
|
// 队列状态(按当前 agent 维度)
|
|
2158
2087
|
lines.push('', '📬 队列状态:');
|
|
2159
2088
|
lines.push(` 待处理消息: ${this.messageQueue.getQueueLengthByAgent(currentAgentName)}`);
|
|
@@ -2187,19 +2116,15 @@ export class CommandHandler {
|
|
|
2187
2116
|
lines.push(` 平均响应耗时: ${(h.avgResponseMs / 1000).toFixed(1)}s`);
|
|
2188
2117
|
}
|
|
2189
2118
|
}
|
|
2190
|
-
return lines.join('\n');
|
|
2119
|
+
return { kind: 'command.result', text: lines.join('\n') };
|
|
2191
2120
|
}
|
|
2192
2121
|
// /restart 命令:重启服务(owner only) / 重连指定渠道(admin+)
|
|
2193
2122
|
if (normalizedContent === '/restart' || normalizedContent.startsWith('/restart ')) {
|
|
2194
2123
|
const restartArg = normalizedContent.slice('/restart'.length).trim();
|
|
2195
|
-
// /restart <type> — 重连指定类型的所有渠道(admin only
|
|
2196
|
-
// 服务级操作仅可从 default 通道发起,避免 evolagent owner/admin 越权
|
|
2124
|
+
// /restart <type> — 重连指定类型的所有渠道(admin only)
|
|
2197
2125
|
if (restartArg) {
|
|
2198
|
-
if (this.getOwningAgent(channel)) {
|
|
2199
|
-
return '❌ 渠道重连只能从 DefaultAgent 通道发起(服务级操作)';
|
|
2200
|
-
}
|
|
2201
2126
|
if (!isAdmin)
|
|
2202
|
-
return '❌ 无权限:渠道重连仅限管理员使用';
|
|
2127
|
+
return { kind: 'command.error', text: '❌ 无权限:渠道重连仅限管理员使用' };
|
|
2203
2128
|
const type = restartArg;
|
|
2204
2129
|
// /restart 是服务级操作:重连该 type 下的所有实例(不分 agent)
|
|
2205
2130
|
const scopedNames = [];
|
|
@@ -2208,7 +2133,7 @@ export class CommandHandler {
|
|
|
2208
2133
|
scopedNames.push(name);
|
|
2209
2134
|
}
|
|
2210
2135
|
if (scopedNames.length === 0) {
|
|
2211
|
-
return `❌ 没有类型为 "${type}"
|
|
2136
|
+
return { kind: 'command.error', text: `❌ 没有类型为 "${type}" 的渠道` };
|
|
2212
2137
|
}
|
|
2213
2138
|
const results = [];
|
|
2214
2139
|
for (const name of scopedNames) {
|
|
@@ -2229,15 +2154,11 @@ export class CommandHandler {
|
|
|
2229
2154
|
results.push(`${name}: 重连失败 - ${e?.message || e}`);
|
|
2230
2155
|
}
|
|
2231
2156
|
}
|
|
2232
|
-
return `🔄 重连 ${type}:\n ${results.join('\n ')}
|
|
2233
|
-
}
|
|
2234
|
-
// /restart(无参数)— 重启整个服务(owner only,且仅可从 default 通道触发)
|
|
2235
|
-
// 防止 evolagent 通道的 owner 越权杀整个 evolclaw 进程(影响所有租户)
|
|
2236
|
-
if (this.getOwningAgent(channel)) {
|
|
2237
|
-
return '❌ 服务重启只能从 DefaultAgent 通道发起。EvolAgent 通道仅可执行 /restart <type> 重连特定类型渠道';
|
|
2157
|
+
return { kind: 'command.result', text: `🔄 重连 ${type}:\n ${results.join('\n ')}` };
|
|
2238
2158
|
}
|
|
2159
|
+
// /restart(无参数)— 重启整个服务(owner only)
|
|
2239
2160
|
if (!isOwner)
|
|
2240
|
-
return '❌ 无权限:服务重启仅限 owner 使用';
|
|
2161
|
+
return { kind: 'command.error', text: '❌ 无权限:服务重启仅限 owner 使用' };
|
|
2241
2162
|
const allSessions = await this.sessionManager.listSessions(channel, channelId);
|
|
2242
2163
|
const sessionsWithMessages = allSessions
|
|
2243
2164
|
.filter(s => this.messageCache.hasMessages(s.id))
|
|
@@ -2260,16 +2181,20 @@ export class CommandHandler {
|
|
|
2260
2181
|
};
|
|
2261
2182
|
fs.writeFileSync(path.join(resolvePaths().dataDir, 'restart-pending.json'), JSON.stringify(restartInfo));
|
|
2262
2183
|
const { spawn } = await import('child_process');
|
|
2263
|
-
spawn('node', [path.join(getPackageRoot(), 'dist', 'cli.js'), 'restart-monitor'], {
|
|
2184
|
+
spawn('node', [path.join(getPackageRoot(), 'dist', 'cli', 'index.js'), 'restart-monitor'], {
|
|
2264
2185
|
detached: true,
|
|
2265
2186
|
stdio: 'ignore',
|
|
2266
2187
|
env: { ...process.env, EVOLCLAW_HOME: resolvePaths().root }
|
|
2267
2188
|
}).unref();
|
|
2268
2189
|
this.eventBus.publish({ type: 'system:restart', channel, channelId });
|
|
2190
|
+
// 发 SIGTERM 而非直接 process.exit(0),让 index.ts 的 shutdown() 先
|
|
2191
|
+
// 正常关闭所有 channel(包括 Feishu WebSocket close frame),
|
|
2192
|
+
// 避免 Feishu 服务端因连接异常断开而重推未 ack 的消息给新进程。
|
|
2269
2193
|
setTimeout(() => {
|
|
2270
2194
|
logger.info('[System] Restarting by user command...');
|
|
2271
|
-
process.
|
|
2195
|
+
process.kill(process.pid, 'SIGTERM');
|
|
2272
2196
|
}, 1000);
|
|
2197
|
+
return true;
|
|
2273
2198
|
};
|
|
2274
2199
|
// 文本确认流程
|
|
2275
2200
|
if (sessionsWithMessages.length > 0) {
|
|
@@ -2283,38 +2208,55 @@ export class CommandHandler {
|
|
|
2283
2208
|
}
|
|
2284
2209
|
else {
|
|
2285
2210
|
fs.writeFileSync(restartConfirmFile, JSON.stringify({ timestamp: now }));
|
|
2286
|
-
return sessionsWithMessages.join('\n') + '\n再次输入 /restart 将强制重启。';
|
|
2211
|
+
return { kind: 'command.result', text: sessionsWithMessages.join('\n') + '\n再次输入 /restart 将强制重启。' };
|
|
2287
2212
|
}
|
|
2288
2213
|
}
|
|
2289
2214
|
else {
|
|
2290
2215
|
fs.writeFileSync(restartConfirmFile, JSON.stringify({ timestamp: Date.now() }));
|
|
2291
|
-
return sessionsWithMessages.join('\n') + '\n再次输入 /restart 将强制重启。';
|
|
2216
|
+
return { kind: 'command.result', text: sessionsWithMessages.join('\n') + '\n再次输入 /restart 将强制重启。' };
|
|
2292
2217
|
}
|
|
2293
2218
|
}
|
|
2294
2219
|
await executeRestart();
|
|
2295
|
-
return '🔄 服务正在重启,请稍候...(约 5 秒后恢复)';
|
|
2220
|
+
return { kind: 'command.result', text: '🔄 服务正在重启,请稍候...(约 5 秒后恢复)' };
|
|
2221
|
+
}
|
|
2222
|
+
// /upgrade 命令:检查版本更新,提示用户手动重启
|
|
2223
|
+
if (normalizedContent === '/upgrade') {
|
|
2224
|
+
if (!isAdmin)
|
|
2225
|
+
return { kind: 'command.error', text: '❌ 无权限:升级检查仅限管理员使用' };
|
|
2226
|
+
if (isLinkedInstall()) {
|
|
2227
|
+
return { kind: 'command.result', text: '⏭ 开发模式,跳过升级检查' };
|
|
2228
|
+
}
|
|
2229
|
+
const localVer = getLocalVersion();
|
|
2230
|
+
const remoteVer = await checkLatestVersion();
|
|
2231
|
+
if (!remoteVer) {
|
|
2232
|
+
return { kind: 'command.result', text: `⚠️ 无法连接 npm registry(当前版本 ${localVer})` };
|
|
2233
|
+
}
|
|
2234
|
+
if (compareVersions(localVer, remoteVer) >= 0) {
|
|
2235
|
+
return { kind: 'command.result', text: `✓ 已是最新版本 (${localVer})` };
|
|
2236
|
+
}
|
|
2237
|
+
return { kind: 'command.result', text: `📦 发现新版本 ${localVer} → ${remoteVer}\n执行 /restart 升级` };
|
|
2296
2238
|
}
|
|
2297
2239
|
// /pwd 命令:显示当前项目路径
|
|
2298
2240
|
if (normalizedContent === '/pwd') {
|
|
2299
2241
|
// session 现在总是存在(上面已自动创建)
|
|
2300
2242
|
if (!session) {
|
|
2301
|
-
return `❌
|
|
2243
|
+
return { kind: 'command.error', text: `❌ 无法创建会话,请检查配置` };
|
|
2302
2244
|
}
|
|
2303
2245
|
const configName = this.getConfiguredProjectName(session.projectPath);
|
|
2304
2246
|
if (configName) {
|
|
2305
|
-
return `当前项目: ${configName}\n路径: ${session.projectPath}
|
|
2247
|
+
return { kind: 'command.result', text: `当前项目: ${configName}\n路径: ${session.projectPath}` };
|
|
2306
2248
|
}
|
|
2307
|
-
return `当前项目: ${session.projectPath}
|
|
2249
|
+
return { kind: 'command.result', text: `当前项目: ${session.projectPath}` };
|
|
2308
2250
|
}
|
|
2309
2251
|
// /file 命令:发送项目内文件,支持 /file path 和 /file channel path(owner only)
|
|
2310
2252
|
if (normalizedContent.startsWith('/file')) {
|
|
2311
2253
|
if (!isOwner)
|
|
2312
|
-
return '❌ 无权限:此命令仅限 owner 使用';
|
|
2254
|
+
return { kind: 'command.error', text: '❌ 无权限:此命令仅限 owner 使用' };
|
|
2313
2255
|
// 飞书会将 .md 等后缀自动转为 Markdown 链接: foo.md → [foo.md](http://foo.md/)
|
|
2314
2256
|
// 还原: 将 [text](url) 替换为 text
|
|
2315
2257
|
const rawArg = normalizedContent.slice(5).trim().replace(/\[([^\]]+)\]\([^)]+\)/g, '$1');
|
|
2316
2258
|
if (!rawArg) {
|
|
2317
|
-
return '用法: /file <相对路径> 或 /file <渠道> <相对路径>\n示例: /file src/index.ts\n示例: /file feishu report.md';
|
|
2259
|
+
return { kind: 'command.result', text: '用法: /file <相对路径> 或 /file <渠道> <相对路径>\n示例: /file src/index.ts\n示例: /file feishu report.md' };
|
|
2318
2260
|
}
|
|
2319
2261
|
// 解析目标通道:第一个 token 按实例名匹配,再按 channelType 匹配
|
|
2320
2262
|
const tokens = rawArg.split(/\s+/);
|
|
@@ -2344,89 +2286,89 @@ export class CommandHandler {
|
|
|
2344
2286
|
const isCrossChannel = targetChannel !== channel;
|
|
2345
2287
|
// 跨通道仅限 owner
|
|
2346
2288
|
if (isCrossChannel && identity.role !== 'owner') {
|
|
2347
|
-
return '❌ 跨通道发送仅限管理员';
|
|
2289
|
+
return { kind: 'command.error', text: '❌ 跨通道发送仅限管理员' };
|
|
2348
2290
|
}
|
|
2349
2291
|
// 找目标 adapter
|
|
2350
2292
|
const targetAdapter = this.adapters.get(targetChannel);
|
|
2351
2293
|
if (!targetAdapter) {
|
|
2352
|
-
return `❌ 通道 ${targetLabel}
|
|
2294
|
+
return { kind: 'command.error', text: `❌ 通道 ${targetLabel} 未启用或不存在` };
|
|
2353
2295
|
}
|
|
2354
|
-
if (!targetAdapter.
|
|
2355
|
-
return `❌ 通道 ${targetLabel}
|
|
2296
|
+
if (!targetAdapter.capabilities?.file) {
|
|
2297
|
+
return { kind: 'command.error', text: `❌ 通道 ${targetLabel} 不支持文件发送` };
|
|
2356
2298
|
}
|
|
2357
2299
|
// 获取 session(需要 projectPath)
|
|
2358
|
-
const sendResult = await this.ensureSession(channel, channelId, threadId);
|
|
2300
|
+
const sendResult = await this.ensureSession(channel, channelId, threadId, chatType);
|
|
2359
2301
|
if ('error' in sendResult)
|
|
2360
|
-
return sendResult.error;
|
|
2302
|
+
return { kind: 'command.result', text: sendResult.error };
|
|
2361
2303
|
const sendSession = sendResult.session;
|
|
2362
2304
|
// 路径安全校验
|
|
2363
2305
|
if (path.isAbsolute(filePath)) {
|
|
2364
|
-
return '❌ 不支持绝对路径\n请使用项目内的相对路径';
|
|
2306
|
+
return { kind: 'command.error', text: '❌ 不支持绝对路径\n请使用项目内的相对路径' };
|
|
2365
2307
|
}
|
|
2366
2308
|
if (filePath.split(path.sep).includes('..') || filePath.split('/').includes('..')) {
|
|
2367
|
-
return '❌ 不支持 .. 路径穿越';
|
|
2309
|
+
return { kind: 'command.error', text: '❌ 不支持 .. 路径穿越' };
|
|
2368
2310
|
}
|
|
2369
2311
|
const resolvedPath = path.resolve(sendSession.projectPath, filePath);
|
|
2370
2312
|
// 存在性检查
|
|
2371
2313
|
if (!fs.existsSync(resolvedPath)) {
|
|
2372
|
-
return `❌ 文件不存在: ${filePath}
|
|
2314
|
+
return { kind: 'command.error', text: `❌ 文件不存在: ${filePath}` };
|
|
2373
2315
|
}
|
|
2374
2316
|
// 符号链接安全:realpath 后验证仍在项目目录内
|
|
2375
2317
|
const realPath = fs.realpathSync(resolvedPath);
|
|
2376
2318
|
const realProjectPath = fs.realpathSync(sendSession.projectPath);
|
|
2377
2319
|
if (!realPath.startsWith(realProjectPath + path.sep) && realPath !== realProjectPath) {
|
|
2378
|
-
return '❌ 路径不允许: 文件不在项目目录内';
|
|
2320
|
+
return { kind: 'command.error', text: '❌ 路径不允许: 文件不在项目目录内' };
|
|
2379
2321
|
}
|
|
2380
2322
|
const stat = fs.statSync(resolvedPath);
|
|
2381
2323
|
if (stat.isDirectory()) {
|
|
2382
|
-
return '❌ 暂不支持发送目录\n目录打包发送将在后续版本支持';
|
|
2324
|
+
return { kind: 'command.error', text: '❌ 暂不支持发送目录\n目录打包发送将在后续版本支持' };
|
|
2383
2325
|
}
|
|
2384
2326
|
const MAX_SIZE = 10 * 1024 * 1024;
|
|
2385
2327
|
if (stat.size > MAX_SIZE) {
|
|
2386
|
-
return `❌ 文件过大: ${(stat.size / 1024 / 1024).toFixed(1)} MB (限制 10 MB)
|
|
2328
|
+
return { kind: 'command.error', text: `❌ 文件过大: ${(stat.size / 1024 / 1024).toFixed(1)} MB (限制 10 MB)` };
|
|
2387
2329
|
}
|
|
2388
2330
|
// 找目标 channelId
|
|
2389
2331
|
let targetChannelId = channelId;
|
|
2390
2332
|
if (isCrossChannel) {
|
|
2391
|
-
const ownerPeerId = this.agentRegistry?.getOwner?.(targetChannel)
|
|
2333
|
+
const ownerPeerId = this.agentRegistry?.getOwner?.(targetChannel);
|
|
2392
2334
|
targetChannelId = ownerPeerId ? (this.sessionManager.getOwnerChatId(targetChannel, ownerPeerId) ?? '') : '';
|
|
2393
2335
|
if (!targetChannelId) {
|
|
2394
|
-
return `❌ 未找到 ${targetLabel}
|
|
2336
|
+
return { kind: 'command.error', text: `❌ 未找到 ${targetLabel} 的私聊会话,请先在该通道发送一条消息` };
|
|
2395
2337
|
}
|
|
2396
2338
|
}
|
|
2397
2339
|
// 发送文件
|
|
2398
2340
|
try {
|
|
2399
2341
|
const replyCtx = isCrossChannel ? undefined : this.getReplyContext(sendSession);
|
|
2400
|
-
await targetAdapter.
|
|
2342
|
+
await targetAdapter.send(buildEnvelope({ channel: targetAdapter.channelName, channelId: targetChannelId, replyContext: replyCtx }), { kind: 'result.file', filePath: realPath });
|
|
2401
2343
|
const sizeStr = stat.size < 1024 ? `${stat.size} B`
|
|
2402
2344
|
: stat.size < 1024 * 1024 ? `${(stat.size / 1024).toFixed(1)} KB`
|
|
2403
2345
|
: `${(stat.size / 1024 / 1024).toFixed(1)} MB`;
|
|
2404
|
-
return isCrossChannel
|
|
2405
|
-
|
|
2406
|
-
|
|
2346
|
+
return { kind: 'command.result', text: isCrossChannel
|
|
2347
|
+
? `📎 文件已通过 ${targetLabel} 发送: ${filePath} (${sizeStr})`
|
|
2348
|
+
: `✅ 已发送: ${filePath} (${sizeStr})` };
|
|
2407
2349
|
}
|
|
2408
2350
|
catch (error) {
|
|
2409
2351
|
logger.error('[CommandHandler] /file failed:', error);
|
|
2410
|
-
return `❌ 文件发送失败: ${error.message || error}
|
|
2352
|
+
return { kind: 'command.error', text: `❌ 文件发送失败: ${error.message || error}` };
|
|
2411
2353
|
}
|
|
2412
2354
|
}
|
|
2413
2355
|
// /plist 命令:列出所有项目
|
|
2414
2356
|
if (normalizedContent === '/plist') {
|
|
2415
2357
|
if (!policy.canListProjects(session?.chatType || 'private', identity.role)) {
|
|
2416
2358
|
if (!session) {
|
|
2417
|
-
return `❌ 当前群聊未绑定项目
|
|
2359
|
+
return { kind: 'command.error', text: `❌ 当前群聊未绑定项目
|
|
2418
2360
|
|
|
2419
|
-
请使用 /bind <项目路径>
|
|
2361
|
+
请使用 /bind <项目路径> 绑定项目` };
|
|
2420
2362
|
}
|
|
2421
2363
|
const projectName = this.getProjectName(session.projectPath);
|
|
2422
2364
|
const isProcessing = !!session.processingState;
|
|
2423
2365
|
const status = isProcessing ? '[处理中]' : '[空闲]';
|
|
2424
|
-
return `当前群聊绑定的项目:
|
|
2366
|
+
return { kind: 'command.result', text: `当前群聊绑定的项目:
|
|
2425
2367
|
${projectName} (${session.projectPath}) - ${status}
|
|
2426
2368
|
|
|
2427
|
-
|
|
2369
|
+
提示:群聊不支持切换项目` };
|
|
2428
2370
|
}
|
|
2429
|
-
// 收集项目信息并按最近活跃排序(唯一来源:
|
|
2371
|
+
// 收集项目信息并按最近活跃排序(唯一来源:agent config projects.list)
|
|
2430
2372
|
const entries = [];
|
|
2431
2373
|
for (const [name, projectPath] of Object.entries(this.projects)) {
|
|
2432
2374
|
// 跳过不存在的路径
|
|
@@ -2471,14 +2413,8 @@ export class CommandHandler {
|
|
|
2471
2413
|
}
|
|
2472
2414
|
return parts.join(' ');
|
|
2473
2415
|
};
|
|
2474
|
-
// 尝试发送
|
|
2475
|
-
if (
|
|
2476
|
-
const requestId = `plist-${Date.now()}-${crypto.randomBytes(4).toString('hex')}`;
|
|
2477
|
-
const buttons = entries.map(e => ({
|
|
2478
|
-
key: e.name,
|
|
2479
|
-
label: e.isCurrent ? `✓ ${e.name}` : e.name,
|
|
2480
|
-
style: e.isCurrent ? 'primary' : 'default',
|
|
2481
|
-
}));
|
|
2416
|
+
// 尝试发送 CommandCard 卡片(每个项目一个按钮,一键切换)
|
|
2417
|
+
if (entries.length > 0) {
|
|
2482
2418
|
const bodyLines = entries.map(e => {
|
|
2483
2419
|
const status = buildStatusText(e);
|
|
2484
2420
|
const prefix = e.isCurrent ? '✓' : '•';
|
|
@@ -2486,35 +2422,27 @@ export class CommandHandler {
|
|
|
2486
2422
|
});
|
|
2487
2423
|
const interaction = {
|
|
2488
2424
|
type: 'interaction',
|
|
2489
|
-
id:
|
|
2425
|
+
id: `plist-${Date.now()}-${crypto.randomBytes(4).toString('hex')}`,
|
|
2490
2426
|
channelId,
|
|
2491
|
-
sessionId: activeSession?.id ||
|
|
2427
|
+
sessionId: activeSession?.id || '',
|
|
2428
|
+
initiatorId: userId,
|
|
2492
2429
|
kind: {
|
|
2493
|
-
kind: '
|
|
2430
|
+
kind: 'command-card',
|
|
2494
2431
|
title: '📂 项目列表',
|
|
2495
2432
|
body: bodyLines.join('\n'),
|
|
2496
|
-
buttons
|
|
2433
|
+
buttons: entries.map(e => ({
|
|
2434
|
+
label: e.isCurrent ? `✓ ${e.name}` : e.name,
|
|
2435
|
+
command: `/project ${e.name}`,
|
|
2436
|
+
style: (e.isCurrent ? 'primary' : 'default'),
|
|
2437
|
+
disabled: e.isCurrent,
|
|
2438
|
+
})),
|
|
2497
2439
|
},
|
|
2498
2440
|
};
|
|
2499
2441
|
const replyCtx = activeSession ? this.getReplyContext(activeSession) : undefined;
|
|
2500
|
-
const
|
|
2501
|
-
|
|
2502
|
-
canWrite: isAdmin,
|
|
2503
|
-
callback: async (action, _values, operatorId) => {
|
|
2504
|
-
if (userId && operatorId && operatorId !== userId)
|
|
2505
|
-
return;
|
|
2506
|
-
const selectedEntry = entries.find(e => e.name === action);
|
|
2507
|
-
if (selectedEntry && !selectedEntry.isCurrent) {
|
|
2508
|
-
const result = await this.handle(`/project ${action}`, channel, channelId, undefined, userId, threadId);
|
|
2509
|
-
if (result) {
|
|
2510
|
-
const adapter = this.adapters.get(channel);
|
|
2511
|
-
adapter?.sendText(channelId, result, replyCtx);
|
|
2512
|
-
}
|
|
2513
|
-
}
|
|
2514
|
-
},
|
|
2515
|
-
});
|
|
2516
|
-
if (cardSent)
|
|
2442
|
+
const cardResult = await this.sendCommandCard({ channel, channelId, interaction, replyCtx, canWrite: isAdmin });
|
|
2443
|
+
if (cardResult === null)
|
|
2517
2444
|
return null;
|
|
2445
|
+
return { kind: 'command.result', text: cardResult };
|
|
2518
2446
|
}
|
|
2519
2447
|
// 降级:文本列表
|
|
2520
2448
|
const lines = ['可用项目:'];
|
|
@@ -2523,25 +2451,26 @@ export class CommandHandler {
|
|
|
2523
2451
|
lines.push(`${prefix} ${entry.name} (${entry.projectPath}) - ${buildStatusText(entry)}`);
|
|
2524
2452
|
}
|
|
2525
2453
|
lines.push('', '提示: 使用 /p <名称> 切换项目');
|
|
2526
|
-
return lines.join('\n');
|
|
2454
|
+
return { kind: 'command.result', text: lines.join('\n') };
|
|
2527
2455
|
}
|
|
2528
2456
|
// /project(无参数):直接复用 /plist 逻辑(含卡片交互)
|
|
2529
2457
|
if (normalizedContent === '/project') {
|
|
2530
2458
|
if (!policy.canSwitchProject(session?.chatType || 'private', identity.role)) {
|
|
2531
2459
|
// 群聊不能切换项目,交由 /plist 逻辑处理
|
|
2532
2460
|
}
|
|
2533
|
-
|
|
2461
|
+
const delegated = await this.handle('/plist', channel, channelId, undefined, userId, threadId);
|
|
2462
|
+
return typeof delegated === 'string' ? { kind: 'command.result', text: delegated } : delegated;
|
|
2534
2463
|
}
|
|
2535
2464
|
// /project 命令:切换项目(支持名称或路径)
|
|
2536
2465
|
if (normalizedContent.startsWith('/project ')) {
|
|
2537
2466
|
if (!policy.canSwitchProject(session?.chatType || 'private', identity.role)) {
|
|
2538
|
-
return `❌ 群聊不支持切换项目
|
|
2467
|
+
return { kind: 'command.error', text: `❌ 群聊不支持切换项目
|
|
2539
2468
|
|
|
2540
|
-
|
|
2469
|
+
群聊只能绑定一个项目。如需更换项目,请联系管理员重新配置。` };
|
|
2541
2470
|
}
|
|
2542
2471
|
let arg = normalizedContent.slice(9).trim();
|
|
2543
2472
|
if (!arg)
|
|
2544
|
-
return '用法: /p <name|path> 或 /project <name|path>';
|
|
2473
|
+
return { kind: 'command.result', text: '用法: /p <name|path> 或 /project <name|path>' };
|
|
2545
2474
|
// 检查确认标志
|
|
2546
2475
|
const hasConfirm = arg.endsWith(' --confirm');
|
|
2547
2476
|
if (hasConfirm) {
|
|
@@ -2551,10 +2480,10 @@ export class CommandHandler {
|
|
|
2551
2480
|
let projectName;
|
|
2552
2481
|
if (arg.includes('/')) {
|
|
2553
2482
|
if (!path.isAbsolute(arg)) {
|
|
2554
|
-
return '❌ 项目路径必须是绝对路径';
|
|
2483
|
+
return { kind: 'command.error', text: '❌ 项目路径必须是绝对路径' };
|
|
2555
2484
|
}
|
|
2556
2485
|
if (!fs.existsSync(arg)) {
|
|
2557
|
-
return `❌ 路径不存在: ${arg}
|
|
2486
|
+
return { kind: 'command.error', text: `❌ 路径不存在: ${arg}` };
|
|
2558
2487
|
}
|
|
2559
2488
|
projectPath = arg;
|
|
2560
2489
|
projectName = path.basename(arg);
|
|
@@ -2562,7 +2491,7 @@ export class CommandHandler {
|
|
|
2562
2491
|
else {
|
|
2563
2492
|
projectPath = this.projects[arg];
|
|
2564
2493
|
if (!projectPath) {
|
|
2565
|
-
return `❌ 项目 "${arg}" 不存在\n提示: 使用 /p
|
|
2494
|
+
return { kind: 'command.error', text: `❌ 项目 "${arg}" 不存在\n提示: 使用 /p 查看可用项目` };
|
|
2566
2495
|
}
|
|
2567
2496
|
projectName = arg;
|
|
2568
2497
|
}
|
|
@@ -2570,13 +2499,13 @@ export class CommandHandler {
|
|
|
2570
2499
|
const normalizedSessionPath = path.resolve(session.projectPath);
|
|
2571
2500
|
const normalizedProjectPath = path.resolve(projectPath);
|
|
2572
2501
|
if (normalizedSessionPath === normalizedProjectPath) {
|
|
2573
|
-
return `当前已在项目: ${projectName}\n 路径: ${projectPath}
|
|
2502
|
+
return { kind: 'command.result', text: `当前已在项目: ${projectName}\n 路径: ${projectPath}` };
|
|
2574
2503
|
}
|
|
2575
2504
|
}
|
|
2576
2505
|
// 群聊切换项目需要确认
|
|
2577
2506
|
const isGroupChat = session?.chatType === 'group';
|
|
2578
2507
|
if (isGroupChat && !hasConfirm) {
|
|
2579
|
-
return `⚠️ 群聊切换项目风险提示:
|
|
2508
|
+
return { kind: 'command.error', text: `⚠️ 群聊切换项目风险提示:
|
|
2580
2509
|
|
|
2581
2510
|
切换项目将影响所有群成员的对话上下文,可能导致:
|
|
2582
2511
|
• 当前项目的会话历史被切换
|
|
@@ -2584,9 +2513,9 @@ export class CommandHandler {
|
|
|
2584
2513
|
• 其他成员的工作受到影响
|
|
2585
2514
|
|
|
2586
2515
|
确认切换请执行:
|
|
2587
|
-
/p ${projectName} --confirm
|
|
2516
|
+
/p ${projectName} --confirm` };
|
|
2588
2517
|
}
|
|
2589
|
-
const currentAgentId = activeSession?.agentId || this.
|
|
2518
|
+
const currentAgentId = activeSession?.agentId || this.primaryRunnerKey;
|
|
2590
2519
|
const newSession = await this.sessionManager.switchProject(channel, channelId, projectPath, currentAgentId);
|
|
2591
2520
|
this.eventBus.publish({
|
|
2592
2521
|
type: 'project:switched',
|
|
@@ -2598,7 +2527,7 @@ export class CommandHandler {
|
|
|
2598
2527
|
});
|
|
2599
2528
|
const cachedEvents = this.messageCache.getEvents(newSession.id);
|
|
2600
2529
|
const hasExistingSession = newSession.agentSessionId ? '(恢复已有会话)' : '(新建会话)';
|
|
2601
|
-
const currentAgent = newSession.agentId || this.
|
|
2530
|
+
const currentAgent = newSession.agentId || this.primaryRunnerKey;
|
|
2602
2531
|
let response = `✓ 已切换到项目: ${projectName}\n 路径: ${projectPath}\n Agent: ${currentAgent}\n ${hasExistingSession}`;
|
|
2603
2532
|
if (cachedEvents.length > 0 && sendMessage) {
|
|
2604
2533
|
for (const event of cachedEvents) {
|
|
@@ -2617,28 +2546,28 @@ export class CommandHandler {
|
|
|
2617
2546
|
await sendMessage(channelId, event.message);
|
|
2618
2547
|
}
|
|
2619
2548
|
this.messageCache.clearEvents(newSession.id);
|
|
2620
|
-
return '';
|
|
2549
|
+
return { kind: 'command.result', text: '' };
|
|
2621
2550
|
}
|
|
2622
|
-
return response;
|
|
2551
|
+
return { kind: 'command.result', text: response };
|
|
2623
2552
|
}
|
|
2624
2553
|
// /bind 命令:持久化项目到配置(不切换)(owner only)
|
|
2625
2554
|
if (normalizedContent === '/bind')
|
|
2626
|
-
return '用法: /bind <路径>';
|
|
2555
|
+
return { kind: 'command.result', text: '用法: /bind <路径>' };
|
|
2627
2556
|
if (normalizedContent.startsWith('/bind ')) {
|
|
2628
2557
|
if (!isOwner)
|
|
2629
|
-
return '❌ 无权限:此命令仅限 owner 使用';
|
|
2558
|
+
return { kind: 'command.error', text: '❌ 无权限:此命令仅限 owner 使用' };
|
|
2630
2559
|
const projectPath = normalizedContent.slice(6).trim();
|
|
2631
2560
|
if (!projectPath)
|
|
2632
|
-
return '用法: /bind <路径>';
|
|
2561
|
+
return { kind: 'command.result', text: '用法: /bind <路径>' };
|
|
2633
2562
|
if (!path.isAbsolute(projectPath)) {
|
|
2634
|
-
return '❌ 项目路径必须是绝对路径';
|
|
2563
|
+
return { kind: 'command.error', text: '❌ 项目路径必须是绝对路径' };
|
|
2635
2564
|
}
|
|
2636
2565
|
if (!fs.existsSync(projectPath)) {
|
|
2637
|
-
if (this.config
|
|
2566
|
+
if (this.getOwningAgent(channel)?.config?.projects?.autoCreate) {
|
|
2638
2567
|
fs.mkdirSync(projectPath, { recursive: true });
|
|
2639
2568
|
}
|
|
2640
2569
|
else {
|
|
2641
|
-
return `❌ 路径不存在: ${projectPath}
|
|
2570
|
+
return { kind: 'command.error', text: `❌ 路径不存在: ${projectPath}` };
|
|
2642
2571
|
}
|
|
2643
2572
|
}
|
|
2644
2573
|
// 生成项目名称(使用目录名)
|
|
@@ -2648,34 +2577,34 @@ export class CommandHandler {
|
|
|
2648
2577
|
const existing = scopeProjects[projectName];
|
|
2649
2578
|
if (existing) {
|
|
2650
2579
|
if (existing === projectPath) {
|
|
2651
|
-
return `项目 "${projectName}" 已存在\n 路径: ${projectPath}\n\n使用 /p ${projectName}
|
|
2580
|
+
return { kind: 'command.result', text: `项目 "${projectName}" 已存在\n 路径: ${projectPath}\n\n使用 /p ${projectName} 切换到该项目` };
|
|
2652
2581
|
}
|
|
2653
|
-
return `❌ 项目名称 "${projectName}" 已被占用\n 现有路径: ${existing}\n 新路径: ${projectPath}\n\n
|
|
2582
|
+
return { kind: 'command.error', text: `❌ 项目名称 "${projectName}" 已被占用\n 现有路径: ${existing}\n 新路径: ${projectPath}\n\n请重命名目录或手动编辑配置文件` };
|
|
2654
2583
|
}
|
|
2655
|
-
// 写入:agent-owned channel → agent.json;default →
|
|
2584
|
+
// 写入:agent-owned channel → agent.json;default → agent config
|
|
2656
2585
|
const err = await this.addProjectInScope(channel, projectName, projectPath);
|
|
2657
2586
|
if (err)
|
|
2658
|
-
return err;
|
|
2659
|
-
return `✓ 已添加项目: ${projectName}\n 路径: ${projectPath}\n\n使用 /p ${projectName}
|
|
2587
|
+
return { kind: 'command.result', text: err };
|
|
2588
|
+
return { kind: 'command.result', text: `✓ 已添加项目: ${projectName}\n 路径: ${projectPath}\n\n使用 /p ${projectName} 切换到该项目` };
|
|
2660
2589
|
}
|
|
2661
2590
|
// /slist 命令:列出当前项目的会话
|
|
2662
2591
|
// /slist — 仅 EvolClaw 会话
|
|
2663
2592
|
// /slist cli — 仅 CLI 会话(未导入的)
|
|
2664
2593
|
if (normalizedContent === '/slist' || normalizedContent === '/slist cli') {
|
|
2665
2594
|
if (!session) {
|
|
2666
|
-
return `❌ 当前没有活跃会话
|
|
2595
|
+
return { kind: 'command.error', text: `❌ 当前没有活跃会话
|
|
2667
2596
|
|
|
2668
2597
|
请先执行以下操作之一:
|
|
2669
2598
|
1. 发送任意消息 - 自动创建新会话
|
|
2670
2599
|
2. /new [名称] - 创建命名会话
|
|
2671
|
-
3. /p <项目> -
|
|
2600
|
+
3. /p <项目> - 切换到指定项目` };
|
|
2672
2601
|
}
|
|
2673
2602
|
const showCliOnly = normalizedContent === '/slist cli';
|
|
2674
2603
|
// /slist cli — 仅显示 CLI 会话
|
|
2675
2604
|
if (showCliOnly) {
|
|
2676
2605
|
const canImportCli = policy.canImportCliSession(session.chatType || 'private', identity.role);
|
|
2677
2606
|
if (!canImportCli) {
|
|
2678
|
-
return '❌ 当前无权查看 CLI 会话';
|
|
2607
|
+
return { kind: 'command.error', text: '❌ 当前无权查看 CLI 会话' };
|
|
2679
2608
|
}
|
|
2680
2609
|
const cliSessions = await this.sessionManager.scanCliSessions(session.projectPath, session.agentId);
|
|
2681
2610
|
const sessions = await this.sessionManager.listSessions(channel, channelId);
|
|
@@ -2683,7 +2612,7 @@ export class CommandHandler {
|
|
|
2683
2612
|
const dbSessionIds = new Set(currentProjectSessions.map(s => s.agentSessionId).filter(Boolean));
|
|
2684
2613
|
const orphanCliSessions = cliSessions.filter(c => !dbSessionIds.has(c.uuid));
|
|
2685
2614
|
if (orphanCliSessions.length === 0) {
|
|
2686
|
-
return `当前项目 ${path.basename(session.projectPath)} 没有未导入的 CLI
|
|
2615
|
+
return { kind: 'command.result', text: `当前项目 ${path.basename(session.projectPath)} 没有未导入的 CLI 会话` };
|
|
2687
2616
|
}
|
|
2688
2617
|
// 构建显示数据(复用于卡片和文本)
|
|
2689
2618
|
const cliDisplayItems = orphanCliSessions.map(c => {
|
|
@@ -2692,42 +2621,31 @@ export class CommandHandler {
|
|
|
2692
2621
|
const uuid = c.uuid.substring(0, 8);
|
|
2693
2622
|
return { uuid, fullUuid: c.uuid, time, message };
|
|
2694
2623
|
});
|
|
2695
|
-
// 尝试发送
|
|
2624
|
+
// 尝试发送 CommandCard 卡片
|
|
2696
2625
|
if (this.interactionRouter && cliDisplayItems.length > 0) {
|
|
2697
|
-
const requestId = `slist-cli-${Date.now()}-${crypto.randomBytes(4).toString('hex')}`;
|
|
2698
|
-
const buttons = cliDisplayItems.map(item => ({
|
|
2699
|
-
key: item.uuid,
|
|
2700
|
-
label: item.uuid,
|
|
2701
|
-
style: 'default',
|
|
2702
|
-
}));
|
|
2703
2626
|
const bodyLines = cliDisplayItems.map(item => `• ${item.time} (${item.uuid}) "${item.message}"`);
|
|
2704
2627
|
const interaction = {
|
|
2705
2628
|
type: 'interaction',
|
|
2706
|
-
id:
|
|
2629
|
+
id: `slist-cli-${Date.now()}-${crypto.randomBytes(4).toString('hex')}`,
|
|
2707
2630
|
channelId,
|
|
2708
2631
|
sessionId: session.id,
|
|
2632
|
+
initiatorId: userId,
|
|
2709
2633
|
kind: {
|
|
2710
|
-
kind: '
|
|
2634
|
+
kind: 'command-card',
|
|
2711
2635
|
title: `📋 ${path.basename(session.projectPath)} CLI 会话 (${cliDisplayItems.length})`,
|
|
2712
2636
|
body: bodyLines.join('\n'),
|
|
2713
|
-
buttons
|
|
2637
|
+
buttons: cliDisplayItems.map(item => ({
|
|
2638
|
+
label: item.uuid,
|
|
2639
|
+
command: `/session ${item.uuid}`,
|
|
2640
|
+
style: 'default',
|
|
2641
|
+
})),
|
|
2714
2642
|
},
|
|
2715
2643
|
};
|
|
2716
2644
|
const replyCtx = this.getReplyContext(session);
|
|
2717
|
-
const
|
|
2718
|
-
|
|
2719
|
-
callback: async (action, _values, operatorId) => {
|
|
2720
|
-
if (userId && operatorId && operatorId !== userId)
|
|
2721
|
-
return;
|
|
2722
|
-
const result = await this.handle(`/session ${action}`, channel, channelId, undefined, userId, threadId);
|
|
2723
|
-
if (result) {
|
|
2724
|
-
const adapter = this.adapters.get(channel);
|
|
2725
|
-
adapter?.sendText(channelId, result, replyCtx);
|
|
2726
|
-
}
|
|
2727
|
-
},
|
|
2728
|
-
});
|
|
2729
|
-
if (cardSent)
|
|
2645
|
+
const cardResult = await this.sendCommandCard({ channel, channelId, interaction, replyCtx });
|
|
2646
|
+
if (cardResult === null)
|
|
2730
2647
|
return null;
|
|
2648
|
+
return { kind: 'command.result', text: cardResult };
|
|
2731
2649
|
}
|
|
2732
2650
|
// 降级:文本列表
|
|
2733
2651
|
const lines = [`当前项目 ${path.basename(session.projectPath)} 的 CLI 会话 (共 ${orphanCliSessions.length} 个):`, ''];
|
|
@@ -2736,7 +2654,7 @@ export class CommandHandler {
|
|
|
2736
2654
|
}
|
|
2737
2655
|
lines.push('');
|
|
2738
2656
|
lines.push('使用 /s <8位uuid> 导入并切换到 CLI 会话');
|
|
2739
|
-
return lines.join('\n');
|
|
2657
|
+
return { kind: 'command.result', text: lines.join('\n') };
|
|
2740
2658
|
}
|
|
2741
2659
|
// /slist — 仅显示 EvolClaw 会话
|
|
2742
2660
|
const sessions = await this.sessionManager.listSessions(channel, channelId);
|
|
@@ -2786,17 +2704,8 @@ export class CommandHandler {
|
|
|
2786
2704
|
}
|
|
2787
2705
|
displaySessions.push({ session: s, index: displayIndex, isActive, name, status, idleTime, fileMissing });
|
|
2788
2706
|
}
|
|
2789
|
-
// 尝试发送
|
|
2707
|
+
// 尝试发送 CommandCard 卡片(每个会话一个按钮,一键切换)
|
|
2790
2708
|
if (this.interactionRouter && displaySessions.length >= 1) {
|
|
2791
|
-
const requestId = `slist-${Date.now()}-${crypto.randomBytes(4).toString('hex')}`;
|
|
2792
|
-
const buttons = displaySessions.map(ds => {
|
|
2793
|
-
const shortId = ds.session.agentSessionId ? ds.session.agentSessionId.substring(0, 8) : ds.name;
|
|
2794
|
-
return {
|
|
2795
|
-
key: String(ds.index),
|
|
2796
|
-
label: ds.isActive ? `✓ ${ds.index}. ${shortId}` : `${ds.index}. ${shortId}`,
|
|
2797
|
-
style: ds.isActive ? 'primary' : 'default',
|
|
2798
|
-
};
|
|
2799
|
-
});
|
|
2800
2709
|
const bodyLines = displaySessions.map(ds => {
|
|
2801
2710
|
const prefix = ds.isActive ? '✓' : '•';
|
|
2802
2711
|
const threadTag = ds.session.threadId ? '[话题] ' : '';
|
|
@@ -2806,34 +2715,30 @@ export class CommandHandler {
|
|
|
2806
2715
|
});
|
|
2807
2716
|
const interaction = {
|
|
2808
2717
|
type: 'interaction',
|
|
2809
|
-
id:
|
|
2718
|
+
id: `slist-${Date.now()}-${crypto.randomBytes(4).toString('hex')}`,
|
|
2810
2719
|
channelId,
|
|
2811
2720
|
sessionId: session.id,
|
|
2721
|
+
initiatorId: userId,
|
|
2812
2722
|
kind: {
|
|
2813
|
-
kind: '
|
|
2723
|
+
kind: 'command-card',
|
|
2814
2724
|
title: `📋 ${path.basename(session.projectPath)} 会话列表`,
|
|
2815
2725
|
body: bodyLines.join('\n'),
|
|
2816
|
-
buttons
|
|
2726
|
+
buttons: displaySessions.map(ds => {
|
|
2727
|
+
const shortId = ds.session.agentSessionId ? ds.session.agentSessionId.substring(0, 8) : ds.name;
|
|
2728
|
+
return {
|
|
2729
|
+
label: ds.isActive ? `✓ ${ds.index}. ${shortId}` : `${ds.index}. ${shortId}`,
|
|
2730
|
+
command: `/session ${ds.index}`,
|
|
2731
|
+
style: (ds.isActive ? 'primary' : 'default'),
|
|
2732
|
+
disabled: ds.isActive,
|
|
2733
|
+
};
|
|
2734
|
+
}),
|
|
2817
2735
|
},
|
|
2818
2736
|
};
|
|
2819
2737
|
const replyCtx = this.getReplyContext(session);
|
|
2820
|
-
const
|
|
2821
|
-
|
|
2822
|
-
callback: async (action, _values, operatorId) => {
|
|
2823
|
-
if (userId && operatorId && operatorId !== userId)
|
|
2824
|
-
return;
|
|
2825
|
-
const target = displaySessions.find(ds => String(ds.index) === action);
|
|
2826
|
-
if (target && !target.isActive) {
|
|
2827
|
-
const result = await this.handle(`/session ${action}`, channel, channelId, undefined, userId, threadId);
|
|
2828
|
-
if (result) {
|
|
2829
|
-
const adapter = this.adapters.get(channel);
|
|
2830
|
-
adapter?.sendText(channelId, result, replyCtx);
|
|
2831
|
-
}
|
|
2832
|
-
}
|
|
2833
|
-
},
|
|
2834
|
-
});
|
|
2835
|
-
if (cardSent)
|
|
2738
|
+
const cardResult = await this.sendCommandCard({ channel, channelId, interaction, replyCtx });
|
|
2739
|
+
if (cardResult === null)
|
|
2836
2740
|
return null;
|
|
2741
|
+
return { kind: 'command.result', text: cardResult };
|
|
2837
2742
|
}
|
|
2838
2743
|
// 降级:文本列表
|
|
2839
2744
|
const lines = [`当前项目 ${path.basename(session.projectPath)} 的 [${session.agentId}] 会话列表:`, ''];
|
|
@@ -2863,21 +2768,23 @@ export class CommandHandler {
|
|
|
2863
2768
|
}
|
|
2864
2769
|
lines.push('使用 /s <序号、name或8位uuid> 切换会话');
|
|
2865
2770
|
lines.push('使用 /s cli 查看 CLI 会话');
|
|
2866
|
-
return lines.join('\n');
|
|
2771
|
+
return { kind: 'command.result', text: lines.join('\n') };
|
|
2867
2772
|
}
|
|
2868
2773
|
// /session(无参数):直接复用 /slist 逻辑(含卡片交互)
|
|
2869
2774
|
if (normalizedContent === '/session') {
|
|
2870
|
-
|
|
2775
|
+
const delegated = await this.handle('/slist', channel, channelId, undefined, userId, threadId);
|
|
2776
|
+
return typeof delegated === 'string' ? { kind: 'command.result', text: delegated } : delegated;
|
|
2871
2777
|
}
|
|
2872
2778
|
// /session cli(= /s cli):列出未导入的 CLI 会话
|
|
2873
2779
|
if (normalizedContent === '/session cli') {
|
|
2874
|
-
|
|
2780
|
+
const delegated = await this.handle('/slist cli', channel, channelId, undefined, userId, threadId);
|
|
2781
|
+
return typeof delegated === 'string' ? { kind: 'command.result', text: delegated } : delegated;
|
|
2875
2782
|
}
|
|
2876
2783
|
// /session 或 /s 命令:切换会话
|
|
2877
2784
|
if (normalizedContent.startsWith('/session ')) {
|
|
2878
2785
|
const sessionName = normalizedContent.slice(9).trim();
|
|
2879
2786
|
if (!sessionName)
|
|
2880
|
-
return '用法: /s <序号、会话名称或前8位UUID>';
|
|
2787
|
+
return { kind: 'command.result', text: '用法: /s <序号、会话名称或前8位UUID>' };
|
|
2881
2788
|
let targetSession = await this.sessionManager.getSessionByName(channel, channelId, sessionName);
|
|
2882
2789
|
// 序号切换:纯数字时按 /slist 显示的序号匹配(超过10个时隐藏非活跃话题会话)
|
|
2883
2790
|
if (!targetSession && /^\d+$/.test(sessionName) && session) {
|
|
@@ -2893,7 +2800,7 @@ export class CommandHandler {
|
|
|
2893
2800
|
targetSession = visibleSessions[idx - 1];
|
|
2894
2801
|
}
|
|
2895
2802
|
else {
|
|
2896
|
-
return `❌ 序号超出范围 (1-${visibleSessions.length})\n使用 /s
|
|
2803
|
+
return { kind: 'command.error', text: `❌ 序号超出范围 (1-${visibleSessions.length})\n使用 /s 查看可用会话` };
|
|
2897
2804
|
}
|
|
2898
2805
|
}
|
|
2899
2806
|
if (!targetSession && sessionName.length >= 8) {
|
|
@@ -2906,19 +2813,19 @@ export class CommandHandler {
|
|
|
2906
2813
|
projectPaths.unshift(session.projectPath);
|
|
2907
2814
|
}
|
|
2908
2815
|
for (const projectPath of projectPaths) {
|
|
2909
|
-
const currentAgentId = session?.agentId || this.
|
|
2816
|
+
const currentAgentId = session?.agentId || this.primaryRunnerKey;
|
|
2910
2817
|
const cliSessions = await this.sessionManager.scanCliSessions(projectPath, currentAgentId);
|
|
2911
2818
|
const cliSession = cliSessions.find(c => c.uuid.startsWith(sessionName));
|
|
2912
2819
|
if (cliSession) {
|
|
2913
2820
|
const imported = await this.sessionManager.importCliSession(channel, channelId, projectPath, cliSession.uuid, currentAgentId);
|
|
2914
2821
|
this.eventBus.publish({ type: 'session:imported', sessionId: imported.id, agentSessionId: cliSession.uuid, projectPath });
|
|
2915
2822
|
const projectName = this.getProjectName(projectPath);
|
|
2916
|
-
return `✓ 已导入 CLI 会话: ${imported.name}\n 项目: ${projectName}\n
|
|
2823
|
+
return { kind: 'command.result', text: `✓ 已导入 CLI 会话: ${imported.name}\n 项目: ${projectName}\n 将继续之前的对话历史` };
|
|
2917
2824
|
}
|
|
2918
2825
|
}
|
|
2919
2826
|
}
|
|
2920
2827
|
if (!targetSession) {
|
|
2921
|
-
return `❌ 会话不存在: ${sessionName}\n使用 /s
|
|
2828
|
+
return { kind: 'command.error', text: `❌ 会话不存在: ${sessionName}\n使用 /s 查看可用会话` };
|
|
2922
2829
|
}
|
|
2923
2830
|
const lastInput = targetSession.agentSessionId
|
|
2924
2831
|
? this.sessionManager.readSessionLastUserMessage(targetSession.projectPath, targetSession.agentSessionId, targetSession.agentId)
|
|
@@ -2927,64 +2834,64 @@ export class CommandHandler {
|
|
|
2927
2834
|
if (!session) {
|
|
2928
2835
|
const switched = await this.sessionManager.switchToSession(channel, channelId, targetSession.id);
|
|
2929
2836
|
if (!switched) {
|
|
2930
|
-
return `❌
|
|
2837
|
+
return { kind: 'command.error', text: `❌ 切换会话失败` };
|
|
2931
2838
|
}
|
|
2932
|
-
return `✓ 已切换到会话: ${targetSession.name || sessionName}\n 项目: ${path.basename(targetSession.projectPath)}${lastInputLine}
|
|
2839
|
+
return { kind: 'command.result', text: `✓ 已切换到会话: ${targetSession.name || sessionName}\n 项目: ${path.basename(targetSession.projectPath)}${lastInputLine}` };
|
|
2933
2840
|
}
|
|
2934
2841
|
if (targetSession.id === session.id) {
|
|
2935
|
-
return `当前已在会话: ${targetSession.name || sessionName}
|
|
2842
|
+
return { kind: 'command.result', text: `当前已在会话: ${targetSession.name || sessionName}` };
|
|
2936
2843
|
}
|
|
2937
2844
|
// 阻止从主会话切换到话题会话
|
|
2938
2845
|
if (!session.threadId && targetSession.threadId) {
|
|
2939
|
-
return `❌ 无法从主会话切换到话题会话\n
|
|
2846
|
+
return { kind: 'command.error', text: `❌ 无法从主会话切换到话题会话\n话题会话仅在对应话题内可用` };
|
|
2940
2847
|
}
|
|
2941
2848
|
const switched = await this.sessionManager.switchToSession(channel, channelId, targetSession.id);
|
|
2942
2849
|
if (!switched) {
|
|
2943
|
-
return `❌
|
|
2850
|
+
return { kind: 'command.error', text: `❌ 切换会话失败` };
|
|
2944
2851
|
}
|
|
2945
2852
|
this.eventBus.publish({ type: 'session:switched', sessionId: targetSession.id, fromSessionId: session.id, toSessionId: targetSession.id });
|
|
2946
2853
|
const continueHint = lastInput ? '\n 将继续之前的对话历史' : '\n 当前会话未有发言';
|
|
2947
|
-
return `✓ 已切换到会话: ${targetSession.name || sessionName}${continueHint}${lastInputLine}
|
|
2854
|
+
return { kind: 'command.result', text: `✓ 已切换到会话: ${targetSession.name || sessionName}${continueHint}${lastInputLine}` };
|
|
2948
2855
|
}
|
|
2949
2856
|
// /rename 或 /name 命令:重命名当前会话
|
|
2950
2857
|
if (normalizedContent === '/rename' || normalizedContent === '/name') {
|
|
2951
|
-
return '用法: /name <新名称> 或 /rename <新名称>';
|
|
2858
|
+
return { kind: 'command.result', text: '用法: /name <新名称> 或 /rename <新名称>' };
|
|
2952
2859
|
}
|
|
2953
2860
|
if (normalizedContent.startsWith('/rename ')) {
|
|
2954
2861
|
const newName = normalizedContent.slice(8).trim();
|
|
2955
2862
|
if (!newName)
|
|
2956
|
-
return '用法: /name <新名称> 或 /rename <新名称>';
|
|
2863
|
+
return { kind: 'command.result', text: '用法: /name <新名称> 或 /rename <新名称>' };
|
|
2957
2864
|
if (!session) {
|
|
2958
|
-
return `❌ 当前没有活跃会话
|
|
2865
|
+
return { kind: 'command.error', text: `❌ 当前没有活跃会话
|
|
2959
2866
|
|
|
2960
2867
|
请先执行以下操作之一:
|
|
2961
2868
|
1. 发送任意消息 - 自动创建新会话
|
|
2962
2869
|
2. /new [名称] - 创建命名会话
|
|
2963
|
-
3. /session <名称> -
|
|
2870
|
+
3. /session <名称> - 切换到已有会话` };
|
|
2964
2871
|
}
|
|
2965
2872
|
const existing = await this.sessionManager.getSessionByName(channel, channelId, newName);
|
|
2966
2873
|
if (existing && existing.id !== session.id) {
|
|
2967
|
-
return `❌ 会话名称 "${newName}"
|
|
2874
|
+
return { kind: 'command.error', text: `❌ 会话名称 "${newName}" 已存在,请使用其他名称` };
|
|
2968
2875
|
}
|
|
2969
2876
|
const oldName = session.name || '(未命名)';
|
|
2970
2877
|
const success = await this.sessionManager.renameSession(session.id, newName);
|
|
2971
2878
|
if (!success) {
|
|
2972
|
-
return `❌
|
|
2879
|
+
return { kind: 'command.error', text: `❌ 重命名失败` };
|
|
2973
2880
|
}
|
|
2974
2881
|
this.eventBus.publish({ type: 'session:renamed', sessionId: session.id, oldName, newName });
|
|
2975
|
-
return `✓ 已将当前会话重命名为: ${newName}
|
|
2882
|
+
return { kind: 'command.result', text: `✓ 已将当前会话重命名为: ${newName}` };
|
|
2976
2883
|
}
|
|
2977
2884
|
// /del 命令:删除指定会话(仅解绑,不删除文件)
|
|
2978
2885
|
if (normalizedContent.startsWith('/del ')) {
|
|
2979
2886
|
const sessionName = normalizedContent.slice(5).trim();
|
|
2980
2887
|
if (!sessionName)
|
|
2981
|
-
return '用法: /del <序号、会话名称或前8位UUID>';
|
|
2888
|
+
return { kind: 'command.result', text: '用法: /del <序号、会话名称或前8位UUID>' };
|
|
2982
2889
|
if (!session) {
|
|
2983
|
-
return `❌
|
|
2890
|
+
return { kind: 'command.error', text: `❌ 当前没有活跃会话` };
|
|
2984
2891
|
}
|
|
2985
2892
|
// 权限检查:policy 控制谁可以删除会话
|
|
2986
2893
|
if (!policy.canDeleteSession(session.chatType || 'private', identity.role)) {
|
|
2987
|
-
return `❌
|
|
2894
|
+
return { kind: 'command.error', text: `❌ 无权限:群聊中仅管理员可删除会话` };
|
|
2988
2895
|
}
|
|
2989
2896
|
let targetSession = await this.sessionManager.getSessionByName(channel, channelId, sessionName);
|
|
2990
2897
|
// 序号删除(与 /slist 显示序号一致)
|
|
@@ -3000,107 +2907,107 @@ export class CommandHandler {
|
|
|
3000
2907
|
targetSession = visibleSessions[idx - 1];
|
|
3001
2908
|
}
|
|
3002
2909
|
else {
|
|
3003
|
-
return `❌ 序号超出范围 (1-${visibleSessions.length})\n使用 /s
|
|
2910
|
+
return { kind: 'command.error', text: `❌ 序号超出范围 (1-${visibleSessions.length})\n使用 /s 查看可用会话` };
|
|
3004
2911
|
}
|
|
3005
2912
|
}
|
|
3006
2913
|
if (!targetSession && sessionName.length >= 8) {
|
|
3007
2914
|
targetSession = await this.sessionManager.getSessionByUuidPrefix(channel, channelId, sessionName);
|
|
3008
2915
|
}
|
|
3009
2916
|
if (!targetSession) {
|
|
3010
|
-
return `❌ 会话不存在: ${sessionName}\n使用 /s
|
|
2917
|
+
return { kind: 'command.error', text: `❌ 会话不存在: ${sessionName}\n使用 /s 查看可用会话` };
|
|
3011
2918
|
}
|
|
3012
2919
|
if (targetSession.id === session.id) {
|
|
3013
|
-
return `❌ 无法删除当前活跃会话\n
|
|
2920
|
+
return { kind: 'command.error', text: `❌ 无法删除当前活跃会话\n请先切换到其他会话` };
|
|
3014
2921
|
}
|
|
3015
2922
|
const success = await this.sessionManager.unbindSession(targetSession.id);
|
|
3016
2923
|
if (!success) {
|
|
3017
|
-
return `❌
|
|
2924
|
+
return { kind: 'command.error', text: `❌ 删除失败` };
|
|
3018
2925
|
}
|
|
3019
2926
|
this.eventBus.publish({ type: 'session:deleted', sessionId: targetSession.id });
|
|
3020
2927
|
const targetAgent = this.getAgent(channel, targetSession.agentId);
|
|
3021
2928
|
await targetAgent.closeSession(targetSession.id);
|
|
3022
|
-
return `✓ 已删除会话: ${targetSession.name || sessionName}\n会话文件已保留,可通过 CLI
|
|
2929
|
+
return { kind: 'command.result', text: `✓ 已删除会话: ${targetSession.name || sessionName}\n会话文件已保留,可通过 CLI 访问` };
|
|
3023
2930
|
}
|
|
3024
2931
|
// /fork 命令:分支当前会话
|
|
3025
2932
|
if (normalizedContent === '/fork' || normalizedContent.startsWith('/fork ')) {
|
|
3026
2933
|
const forkName = normalizedContent.slice(5).trim() || undefined;
|
|
3027
2934
|
if (!session) {
|
|
3028
|
-
return `❌
|
|
2935
|
+
return { kind: 'command.error', text: `❌ 当前没有活跃会话,无法分支` };
|
|
3029
2936
|
}
|
|
3030
2937
|
if (!session.agentSessionId) {
|
|
3031
|
-
return `❌ 当前会话尚未初始化对话,无法分支\n\n请先发送一条消息,然后再使用 /fork
|
|
2938
|
+
return { kind: 'command.error', text: `❌ 当前会话尚未初始化对话,无法分支\n\n请先发送一条消息,然后再使用 /fork` };
|
|
3032
2939
|
}
|
|
3033
2940
|
const forkAgent = this.getAgent(channel, session.agentId);
|
|
3034
2941
|
if (!forkAgent.capabilities?.fork) {
|
|
3035
|
-
return `❌ 当前 Agent (${forkAgent.name}) 不支持 /fork\n\n可使用 /new
|
|
2942
|
+
return { kind: 'command.error', text: `❌ 当前 Agent (${forkAgent.name}) 不支持 /fork\n\n可使用 /new 创建新会话替代` };
|
|
3036
2943
|
}
|
|
3037
2944
|
try {
|
|
3038
2945
|
const forkedSessionId = await forkAgent.forkSession(session.agentSessionId, session.projectPath, forkName);
|
|
3039
2946
|
const newSession = await this.sessionManager.createForkedSession(session, forkedSessionId, forkName);
|
|
3040
2947
|
this.eventBus.publish({ type: 'session:forked', sessionId: newSession.id, sourceSessionId: session.id, name: forkName });
|
|
3041
|
-
return `✅ 会话已分支: ${newSession.name}\n新会话已激活,可以继续对话\n\n使用 /s 查看所有会话,/s <名称>
|
|
2948
|
+
return { kind: 'command.result', text: `✅ 会话已分支: ${newSession.name}\n新会话已激活,可以继续对话\n\n使用 /s 查看所有会话,/s <名称> 切换回原会话` };
|
|
3042
2949
|
}
|
|
3043
2950
|
catch (error) {
|
|
3044
2951
|
logger.error('[CommandHandler] Fork session failed:', error);
|
|
3045
|
-
return `❌ 会话分支失败: ${error instanceof Error ? error.message : '未知错误'}
|
|
2952
|
+
return { kind: 'command.error', text: `❌ 会话分支失败: ${error instanceof Error ? error.message : '未知错误'}` };
|
|
3046
2953
|
}
|
|
3047
2954
|
}
|
|
3048
2955
|
// /rewind 命令:查看历史 / 回退会话
|
|
3049
2956
|
if (normalizedContent === '/rewind' || normalizedContent.startsWith('/rewind ')) {
|
|
3050
|
-
const result = await this.ensureSession(channel, channelId, threadId);
|
|
2957
|
+
const result = await this.ensureSession(channel, channelId, threadId, chatType);
|
|
3051
2958
|
if ('error' in result)
|
|
3052
|
-
return result.error;
|
|
2959
|
+
return { kind: 'command.error', text: result.error };
|
|
3053
2960
|
const { session } = result;
|
|
3054
2961
|
const rewindAgent = this.getAgent(channel, session.agentId);
|
|
3055
2962
|
if (rewindAgent.name !== 'claude') {
|
|
3056
|
-
return '❌ /rewind 仅支持 Claude 后端';
|
|
2963
|
+
return { kind: 'command.error', text: '❌ /rewind 仅支持 Claude 后端' };
|
|
3057
2964
|
}
|
|
3058
2965
|
if (!session.agentSessionId) {
|
|
3059
|
-
return '❌ 当前会话无历史记录\n\n请先发送一条消息,然后再使用 /rewind';
|
|
2966
|
+
return { kind: 'command.error', text: '❌ 当前会话无历史记录\n\n请先发送一条消息,然后再使用 /rewind' };
|
|
3060
2967
|
}
|
|
3061
2968
|
if (!rewindAgent.getSessionMessages) {
|
|
3062
|
-
return '❌ 当前 Agent 不支持 /rewind';
|
|
2969
|
+
return { kind: 'command.error', text: '❌ 当前 Agent 不支持 /rewind' };
|
|
3063
2970
|
}
|
|
3064
2971
|
const args = normalizedContent.slice('/rewind'.length).trim();
|
|
3065
2972
|
if (!args) {
|
|
3066
|
-
return await this.handleRewindList(session, rewindAgent);
|
|
2973
|
+
return { kind: 'command.result', text: await this.handleRewindList(session, rewindAgent) };
|
|
3067
2974
|
}
|
|
3068
2975
|
// 带参(执行回退,会删除文件/改对话)需 admin+
|
|
3069
2976
|
if (!isAdmin)
|
|
3070
|
-
return '❌ 无权限:回退操作仅限管理员使用';
|
|
2977
|
+
return { kind: 'command.error', text: '❌ 无权限:回退操作仅限管理员使用' };
|
|
3071
2978
|
const parts = args.split(/\s+/);
|
|
3072
2979
|
const turnNum = parseInt(parts[0], 10);
|
|
3073
2980
|
if (isNaN(turnNum) || turnNum < 1) {
|
|
3074
|
-
return '❌ 无效轮次,用法:/rewind <N> chat|file|all(撤销第N轮)';
|
|
2981
|
+
return { kind: 'command.error', text: '❌ 无效轮次,用法:/rewind <N> chat|file|all(撤销第N轮)' };
|
|
3075
2982
|
}
|
|
3076
2983
|
const mode = parts[1]?.toLowerCase();
|
|
3077
2984
|
if (!mode) {
|
|
3078
|
-
return `❌ 请指定回退模式:/rewind ${turnNum} chat | file | all(撤销第${turnNum}
|
|
2985
|
+
return { kind: 'command.error', text: `❌ 请指定回退模式:/rewind ${turnNum} chat | file | all(撤销第${turnNum}轮)` };
|
|
3079
2986
|
}
|
|
3080
2987
|
if (!['chat', 'file', 'all'].includes(mode)) {
|
|
3081
|
-
return `❌ 无效模式 "${mode}",可选:chat | file | all
|
|
2988
|
+
return { kind: 'command.error', text: `❌ 无效模式 "${mode}",可选:chat | file | all` };
|
|
3082
2989
|
}
|
|
3083
|
-
return await this.handleRewind(session, rewindAgent, turnNum, mode);
|
|
2990
|
+
return { kind: 'command.result', text: await this.handleRewind(session, rewindAgent, turnNum, mode) };
|
|
3084
2991
|
}
|
|
3085
2992
|
// /repair 命令:检查并修复会话文件
|
|
3086
2993
|
if (normalizedContent === '/repair') {
|
|
3087
|
-
const repairResult = await this.ensureSession(channel, channelId, threadId);
|
|
2994
|
+
const repairResult = await this.ensureSession(channel, channelId, threadId, chatType);
|
|
3088
2995
|
if ('error' in repairResult)
|
|
3089
|
-
return repairResult.error;
|
|
2996
|
+
return { kind: 'command.result', text: repairResult.error };
|
|
3090
2997
|
const { session: repairSession } = repairResult;
|
|
3091
2998
|
const repairAgent = this.getAgent(channel, repairSession.agentId);
|
|
3092
2999
|
const { checkSessionFile, backupSessionFile } = await import('./session/session-file-health.js');
|
|
3093
3000
|
try {
|
|
3094
3001
|
if (!repairSession.agentSessionId) {
|
|
3095
3002
|
await this.sessionManager.resetHealthStatus(repairSession.id);
|
|
3096
|
-
return `✓ 修复完成\n\n修复内容:\n- 未发现问题(新会话)\n-
|
|
3003
|
+
return { kind: 'command.result', text: `✓ 修复完成\n\n修复内容:\n- 未发现问题(新会话)\n- 已重置异常计数器` };
|
|
3097
3004
|
}
|
|
3098
3005
|
// 通过 agent 定位 session 文件
|
|
3099
3006
|
const sessionFile = repairAgent.resolveSessionFile?.(repairSession.agentSessionId, repairSession.projectPath) ?? null;
|
|
3100
3007
|
if (!sessionFile) {
|
|
3101
3008
|
// 文件不存在(已被删除或从未创建),直接重置
|
|
3102
3009
|
await this.sessionManager.resetHealthStatus(repairSession.id);
|
|
3103
|
-
return `✓ 修复完成\n\n修复内容:\n- 会话文件不存在(可能已被清理)\n-
|
|
3010
|
+
return { kind: 'command.result', text: `✓ 修复完成\n\n修复内容:\n- 会话文件不存在(可能已被清理)\n- 已重置异常计数器` };
|
|
3104
3011
|
}
|
|
3105
3012
|
const healthCheck = await checkSessionFile(sessionFile);
|
|
3106
3013
|
if (healthCheck.corrupt) {
|
|
@@ -3110,26 +3017,146 @@ export class CommandHandler {
|
|
|
3110
3017
|
await this.sessionManager.updateAgentSessionIdBySessionId(repairSession.id, '');
|
|
3111
3018
|
repairAgent.updateSessionId(repairSession.id, '');
|
|
3112
3019
|
await this.sessionManager.resetHealthStatus(repairSession.id);
|
|
3113
|
-
return `✓ 修复完成\n\n检测到问题:\n${healthCheck.issues.map((i) => `- ${i}`).join('\n')}\n\n修复操作:\n- 已备份损坏文件\n- 已删除损坏文件\n- 已重置异常计数器\n\n备份位置:${backupPath}
|
|
3020
|
+
return { kind: 'command.result', text: `✓ 修复完成\n\n检测到问题:\n${healthCheck.issues.map((i) => `- ${i}`).join('\n')}\n\n修复操作:\n- 已备份损坏文件\n- 已删除损坏文件\n- 已重置异常计数器\n\n备份位置:${backupPath}` };
|
|
3114
3021
|
}
|
|
3115
3022
|
if (healthCheck.issues.length > 0) {
|
|
3116
3023
|
await this.sessionManager.resetHealthStatus(repairSession.id);
|
|
3117
|
-
return `⚠️ 检测到问题:\n${healthCheck.issues.map((i) => `- ${i}`).join('\n')}\n\n建议使用 /new 创建新会话\n\n
|
|
3024
|
+
return { kind: 'command.error', text: `⚠️ 检测到问题:\n${healthCheck.issues.map((i) => `- ${i}`).join('\n')}\n\n建议使用 /new 创建新会话\n\n已重置异常计数器,可继续使用当前会话。` };
|
|
3118
3025
|
}
|
|
3119
3026
|
await this.sessionManager.resetHealthStatus(repairSession.id);
|
|
3120
|
-
return `✓ 修复完成\n\n修复内容:\n- 未发现问题\n-
|
|
3027
|
+
return { kind: 'command.result', text: `✓ 修复完成\n\n修复内容:\n- 未发现问题\n- 已重置异常计数器` };
|
|
3121
3028
|
}
|
|
3122
3029
|
catch (error) {
|
|
3123
3030
|
logger.error('[Repair] Failed:', error);
|
|
3124
|
-
return `❌ 修复失败: ${error.message}
|
|
3031
|
+
return { kind: 'command.error', text: `❌ 修复失败: ${error.message}` };
|
|
3125
3032
|
}
|
|
3126
3033
|
}
|
|
3127
3034
|
// /safe 命令:安全模式已禁用
|
|
3128
3035
|
if (normalizedContent === '/safe') {
|
|
3129
|
-
return `ℹ️ 安全模式已禁用\n\n如需重置会话,请使用 /new
|
|
3036
|
+
return { kind: 'command.result', text: `ℹ️ 安全模式已禁用\n\n如需重置会话,请使用 /new 创建新会话。` };
|
|
3037
|
+
}
|
|
3038
|
+
// /trigger 命令
|
|
3039
|
+
if (normalizedContent === '/trigger' || normalizedContent.startsWith('/trigger ')) {
|
|
3040
|
+
const text = this.handleTrigger(normalizedContent, channel, channelId, userId ?? '', isAdmin);
|
|
3041
|
+
return { kind: 'command.result', text };
|
|
3130
3042
|
}
|
|
3131
3043
|
return null;
|
|
3132
3044
|
}
|
|
3045
|
+
handleTrigger(content, channel, channelId, peerId, isAdmin) {
|
|
3046
|
+
const scheduler = this.triggerScheduler;
|
|
3047
|
+
const manager = this.triggerManager;
|
|
3048
|
+
// Bare /trigger → list active
|
|
3049
|
+
if (content === '/trigger') {
|
|
3050
|
+
if (!manager)
|
|
3051
|
+
return '⚠️ 触发器功能未启用';
|
|
3052
|
+
const active = manager.listActive();
|
|
3053
|
+
if (active.length === 0)
|
|
3054
|
+
return '📭 当前没有活跃的触发器';
|
|
3055
|
+
const lines = active.map(t => {
|
|
3056
|
+
const next = new Date(t.nextFireAt).toLocaleString();
|
|
3057
|
+
const fired = t.fireCount > 0 ? ` | 已触发 ${t.fireCount} 次` : '';
|
|
3058
|
+
return `• **${t.name}** [${t.scheduleType}] 下次: ${next}${fired}`;
|
|
3059
|
+
});
|
|
3060
|
+
return `📋 活跃触发器(${active.length} 个):\n\n${lines.join('\n')}`;
|
|
3061
|
+
}
|
|
3062
|
+
const sub = content.slice('/trigger '.length).trim();
|
|
3063
|
+
// /trigger list → list all (active + history)
|
|
3064
|
+
if (sub === 'list' || sub.startsWith('list ')) {
|
|
3065
|
+
if (!manager)
|
|
3066
|
+
return '⚠️ 触发器功能未启用';
|
|
3067
|
+
const { active, history } = manager.listAll();
|
|
3068
|
+
const lines = [];
|
|
3069
|
+
if (active.length > 0) {
|
|
3070
|
+
lines.push(`**活跃 (${active.length})**`);
|
|
3071
|
+
for (const t of active) {
|
|
3072
|
+
const next = new Date(t.nextFireAt).toLocaleString();
|
|
3073
|
+
lines.push(`• ${t.name} [${t.scheduleType}] 下次: ${next} | 触发 ${t.fireCount} 次`);
|
|
3074
|
+
}
|
|
3075
|
+
}
|
|
3076
|
+
if (history.length > 0) {
|
|
3077
|
+
lines.push(`\n**历史 (${history.length})**`);
|
|
3078
|
+
for (const h of history.slice(-10)) {
|
|
3079
|
+
const done = new Date(h.doneAt).toLocaleString();
|
|
3080
|
+
lines.push(`• ${h.name} [${h.doneReason}] ${done}`);
|
|
3081
|
+
}
|
|
3082
|
+
}
|
|
3083
|
+
if (lines.length === 0)
|
|
3084
|
+
return '📭 没有触发器记录';
|
|
3085
|
+
return lines.join('\n');
|
|
3086
|
+
}
|
|
3087
|
+
// /trigger cancel <name|id>
|
|
3088
|
+
if (sub.startsWith('cancel ')) {
|
|
3089
|
+
if (!manager || !scheduler)
|
|
3090
|
+
return '⚠️ 触发器功能未启用';
|
|
3091
|
+
const nameOrId = sub.slice('cancel '.length).trim();
|
|
3092
|
+
if (!nameOrId)
|
|
3093
|
+
return '❌ 用法:/trigger cancel <名称>';
|
|
3094
|
+
// Find trigger: non-admin lookup is scoped to (peerId, channel) to avoid info disclosure
|
|
3095
|
+
// Non-admins can cancel by name or by their own trigger's UUID
|
|
3096
|
+
let trigger;
|
|
3097
|
+
if (isAdmin) {
|
|
3098
|
+
trigger = manager.getByName(nameOrId) ?? manager.getById(nameOrId);
|
|
3099
|
+
}
|
|
3100
|
+
else {
|
|
3101
|
+
trigger = manager.getByNameScoped(nameOrId, peerId, channel)
|
|
3102
|
+
?? manager.getByIdScoped(nameOrId, peerId, channel);
|
|
3103
|
+
}
|
|
3104
|
+
if (!trigger) {
|
|
3105
|
+
return isAdmin
|
|
3106
|
+
? `❌ 未找到触发器:${nameOrId}`
|
|
3107
|
+
: `❌ 未找到触发器 "${nameOrId}",或无权限取消`;
|
|
3108
|
+
}
|
|
3109
|
+
manager.moveToDone(trigger.id, 'cancelled');
|
|
3110
|
+
scheduler.cancel(trigger.id);
|
|
3111
|
+
this.eventBus.publish({ type: 'trigger:cancelled', triggerId: trigger.id, by: peerId });
|
|
3112
|
+
return `✅ 触发器已取消:**${trigger.name}**`;
|
|
3113
|
+
}
|
|
3114
|
+
// /trigger set ...
|
|
3115
|
+
if (sub.startsWith('set ')) {
|
|
3116
|
+
if (!manager || !scheduler)
|
|
3117
|
+
return '⚠️ 触发器功能未启用';
|
|
3118
|
+
const args = sub.slice('set '.length);
|
|
3119
|
+
const result = parseTriggerSet(args);
|
|
3120
|
+
if (!result.ok)
|
|
3121
|
+
return `❌ ${result.error}`;
|
|
3122
|
+
const parsed = result.value;
|
|
3123
|
+
const now = Date.now();
|
|
3124
|
+
const nextFireAt = calcNextFireAt(parsed.scheduleType, parsed.scheduleValue, now);
|
|
3125
|
+
// Auto-generate name if not provided
|
|
3126
|
+
const name = parsed.name ?? `trigger-${Date.now().toString(36)}`;
|
|
3127
|
+
const trigger = {
|
|
3128
|
+
id: crypto.randomUUID(),
|
|
3129
|
+
name,
|
|
3130
|
+
scheduleType: parsed.scheduleType,
|
|
3131
|
+
scheduleValue: parsed.scheduleValue,
|
|
3132
|
+
nextFireAt,
|
|
3133
|
+
targetChannel: parsed.targetChannel ?? channel,
|
|
3134
|
+
targetChannelId: parsed.targetChannelId ?? channelId,
|
|
3135
|
+
targetThreadId: parsed.targetThreadId,
|
|
3136
|
+
targetSessionStrategy: parsed.targetSessionStrategy,
|
|
3137
|
+
agentId: parsed.agentId,
|
|
3138
|
+
prompt: parsed.prompt,
|
|
3139
|
+
createdByPeerId: peerId,
|
|
3140
|
+
createdByChannel: channel,
|
|
3141
|
+
fireCount: 0,
|
|
3142
|
+
createdAt: now,
|
|
3143
|
+
updatedAt: now,
|
|
3144
|
+
};
|
|
3145
|
+
try {
|
|
3146
|
+
// Validate name uniqueness before persisting (manager.register writes to disk)
|
|
3147
|
+
// scheduler.register is in-memory only and cannot fail, so order is safe here.
|
|
3148
|
+
// If manager.register throws (duplicate name/ID), nothing is persisted.
|
|
3149
|
+
manager.register(trigger);
|
|
3150
|
+
scheduler.register(trigger);
|
|
3151
|
+
}
|
|
3152
|
+
catch (err) {
|
|
3153
|
+
return `❌ 注册失败:${err.message}`;
|
|
3154
|
+
}
|
|
3155
|
+
const nextStr = new Date(nextFireAt).toLocaleString();
|
|
3156
|
+
return `✅ 触发器已注册:**${name}**\n下次触发:${nextStr}`;
|
|
3157
|
+
}
|
|
3158
|
+
return `❌ 未知子命令。用法:\n/trigger — 查看活跃触发器\n/trigger list — 查看所有触发器\n/trigger set <参数> — 注册触发器\n/trigger cancel <名称> — 取消触发器`;
|
|
3159
|
+
}
|
|
3133
3160
|
// ── /rewind helpers ──
|
|
3134
3161
|
async handleRewindList(session, agent) {
|
|
3135
3162
|
try {
|
|
@@ -3246,8 +3273,9 @@ export class CommandHandler {
|
|
|
3246
3273
|
static CTL_COMMANDS = [
|
|
3247
3274
|
'/help', '/status', '/check', '/pwd',
|
|
3248
3275
|
'/model', '/effort', '/perm', '/agent',
|
|
3249
|
-
'/compact', '/
|
|
3250
|
-
'/rename', '/name', '/evolagent',
|
|
3276
|
+
'/compact', '/file', '/send', '/restart', '/bind', '/aid', '/rpc', '/storage',
|
|
3277
|
+
'/rename', '/name', '/evolagent', '/trigger',
|
|
3278
|
+
'/chatmode', '/dispatch', '/activity',
|
|
3251
3279
|
];
|
|
3252
3280
|
/** ctl 中仅允许查询形态的指令;写形态(带参)一律拒绝 */
|
|
3253
3281
|
static CTL_READONLY = new Set(['/agent']);
|
|
@@ -3269,6 +3297,8 @@ export class CommandHandler {
|
|
|
3269
3297
|
const taskId = this.sessionManager.getActiveTaskId(session.id);
|
|
3270
3298
|
const chatmode = session.sessionMode || 'interactive';
|
|
3271
3299
|
const encrypted = this.sessionManager.getSessionEncrypt(session.id);
|
|
3300
|
+
// 诊断日志:记录 task_id 解析结果
|
|
3301
|
+
logger.info(`[CommandHandler] buildCtlReplyContext: sessionId=${session.id} taskId=${taskId ?? 'none'} chatmode=${chatmode} threadId=${ctx.threadId ?? 'none'}`);
|
|
3272
3302
|
if (taskId || chatmode !== 'interactive' || encrypted != null) {
|
|
3273
3303
|
ctx.metadata = {};
|
|
3274
3304
|
if (taskId)
|
|
@@ -3285,6 +3315,7 @@ export class CommandHandler {
|
|
|
3285
3315
|
* 复用现有 slash cmd 逻辑,权限继承 session 用户角色
|
|
3286
3316
|
*/
|
|
3287
3317
|
async handleCtl(cmd, sessionId) {
|
|
3318
|
+
logger.info(`[ctl] cmd="${cmd}" sessionId=${sessionId}`);
|
|
3288
3319
|
// 1. 白名单检查
|
|
3289
3320
|
const inputCmd = cmd.split(' ')[0];
|
|
3290
3321
|
if (!CommandHandler.CTL_COMMANDS.includes(inputCmd)) {
|
|
@@ -3353,7 +3384,9 @@ export class CommandHandler {
|
|
|
3353
3384
|
return { ok: false, error: `adapter 未找到: ${session.channel}` };
|
|
3354
3385
|
try {
|
|
3355
3386
|
const replyContext = this.buildCtlReplyContext(session);
|
|
3356
|
-
|
|
3387
|
+
const taskId = replyContext?.metadata?.taskId;
|
|
3388
|
+
const chatmode = replyContext?.metadata?.chatmode ?? 'interactive';
|
|
3389
|
+
await adapter.send(buildEnvelope({ taskId, channel: adapter.channelName, channelId: session.channelId, chatmode, replyContext }), { kind: 'result.text', text, isFinal: true });
|
|
3357
3390
|
return { ok: true, result: '已发送' };
|
|
3358
3391
|
}
|
|
3359
3392
|
catch (err) {
|
|
@@ -3366,8 +3399,9 @@ export class CommandHandler {
|
|
|
3366
3399
|
const parts = sendArgs.split(/\s+/);
|
|
3367
3400
|
const filePath = parts[parts.length - 1];
|
|
3368
3401
|
if (filePath) {
|
|
3369
|
-
const resolved = path.resolve(session.projectPath, filePath);
|
|
3370
|
-
|
|
3402
|
+
const resolved = path.resolve(session.projectPath, filePath).replace(/\\/g, '/');
|
|
3403
|
+
const projectPath = session.projectPath.replace(/\\/g, '/');
|
|
3404
|
+
if (!resolved.startsWith(projectPath)) {
|
|
3371
3405
|
return { ok: false, error: '路径越界:只能发送项目目录下的文件' };
|
|
3372
3406
|
}
|
|
3373
3407
|
}
|
|
@@ -3376,7 +3410,8 @@ export class CommandHandler {
|
|
|
3376
3410
|
try {
|
|
3377
3411
|
const result = await this.handle(cmd, session.channel, session.channelId, undefined, // 不发送消息
|
|
3378
3412
|
userId);
|
|
3379
|
-
|
|
3413
|
+
const text = typeof result === 'string' ? result : (result && 'text' in result ? result.text : '(无输出)');
|
|
3414
|
+
return { ok: true, result: text || '(无输出)' };
|
|
3380
3415
|
}
|
|
3381
3416
|
catch (err) {
|
|
3382
3417
|
return { ok: false, error: err.message };
|