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