evolclaw 2.7.2 → 2.8.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/dist/agents/claude-runner.js +91 -48
- package/dist/channels/aun.js +90 -33
- package/dist/core/command-handler.js +95 -2
- package/dist/core/message/message-bridge.js +14 -2
- package/dist/core/message/message-processor.js +7 -3
- package/dist/core/message/thought-emitter.js +4 -2
- package/dist/core/session/session-manager.js +22 -3
- package/dist/index.js +8 -0
- package/dist/templates/prompts.md +1 -0
- package/package.json +2 -2
|
@@ -204,7 +204,7 @@ export class AgentRunner {
|
|
|
204
204
|
// 没有交互上下文(无渠道适配器),回退到纯文本
|
|
205
205
|
const permCtx = this.permissionContexts.get(sessionId);
|
|
206
206
|
if (!permCtx?.adapter?.sendInteraction || !permCtx?.channelId) {
|
|
207
|
-
return this.handleAskUserQuestionFallback(input, questions);
|
|
207
|
+
return this.handleAskUserQuestionFallback(sessionId, input, questions);
|
|
208
208
|
}
|
|
209
209
|
const answers = {};
|
|
210
210
|
// 从 permCtx 构造 per-session 的发送函数,避免全局 sendPromptFn 被其他 channel 实例覆盖
|
|
@@ -307,20 +307,43 @@ export class AgentRunner {
|
|
|
307
307
|
return { behavior: 'allow', updatedInput, decisionClassification: 'user_temporary' };
|
|
308
308
|
}
|
|
309
309
|
/**
|
|
310
|
-
* AskUserQuestion 纯文本 fallback
|
|
310
|
+
* AskUserQuestion 纯文本 fallback:发送选项列表,等待用户通过 /ask 命令选择
|
|
311
|
+
* 注册到 interactionRouter,用户回复 /ask 1 或 /ask 自定义内容
|
|
311
312
|
*/
|
|
312
|
-
async handleAskUserQuestionFallback(input, questions) {
|
|
313
|
-
|
|
313
|
+
async handleAskUserQuestionFallback(sessionId, input, questions) {
|
|
314
|
+
const permCtx = this.permissionContexts.get(sessionId);
|
|
315
|
+
const sendPrompt = permCtx?.adapter && permCtx?.channelId
|
|
316
|
+
? async (text) => permCtx.adapter.sendText(permCtx.channelId, text, permCtx.replyContext)
|
|
317
|
+
: this.sendPromptFn;
|
|
314
318
|
const answers = {};
|
|
315
319
|
if (questions?.length) {
|
|
316
|
-
const
|
|
317
|
-
const firstLabel = q.options[0]?.label || '';
|
|
318
|
-
answers[q.question] = firstLabel;
|
|
320
|
+
for (const q of questions) {
|
|
319
321
|
const optText = q.options.map((o, i) => ` ${i + 1}. ${o.label}${o.description ? ` — ${o.description}` : ''}`).join('\n');
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
322
|
+
const prompt = `💬 ${q.question}\n${optText}\n\n回复 /ask <数字> 选择,或 /ask <自定义内容>`;
|
|
323
|
+
if (sendPrompt && permCtx?.interactionRouter) {
|
|
324
|
+
await sendPrompt(prompt);
|
|
325
|
+
const requestId = `ask-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
326
|
+
const answer = await new Promise((resolve) => {
|
|
327
|
+
permCtx.interactionRouter.register(requestId, sessionId, (action) => {
|
|
328
|
+
const num = parseInt(action.trim(), 10);
|
|
329
|
+
if (num >= 1 && num <= q.options.length) {
|
|
330
|
+
resolve(q.options[num - 1].label);
|
|
331
|
+
}
|
|
332
|
+
else {
|
|
333
|
+
resolve(action.trim());
|
|
334
|
+
}
|
|
335
|
+
}, { timeoutMs: 120_000, onTimeout: () => resolve(q.options[0]?.label || '') });
|
|
336
|
+
});
|
|
337
|
+
answers[q.question] = answer;
|
|
338
|
+
}
|
|
339
|
+
else {
|
|
340
|
+
// 无交互能力,自动选第一项
|
|
341
|
+
const firstLabel = q.options[0]?.label || '';
|
|
342
|
+
answers[q.question] = firstLabel;
|
|
343
|
+
if (sendPrompt) {
|
|
344
|
+
await sendPrompt(`${prompt}\n → 自动选择:${firstLabel}`);
|
|
345
|
+
}
|
|
346
|
+
}
|
|
324
347
|
}
|
|
325
348
|
}
|
|
326
349
|
const updatedInput = { ...input, answers };
|
|
@@ -331,51 +354,71 @@ export class AgentRunner {
|
|
|
331
354
|
*/
|
|
332
355
|
async handleExitPlanMode(sessionId, input, options) {
|
|
333
356
|
const permCtx = this.permissionContexts.get(sessionId);
|
|
334
|
-
// 从 permCtx 构造 per-session 的发送函数,避免全局 sendPromptFn 被其他 channel 实例覆盖
|
|
335
357
|
const sendPrompt = permCtx?.adapter && permCtx?.channelId
|
|
336
358
|
? async (text) => permCtx.adapter.sendText(permCtx.channelId, text, permCtx.replyContext)
|
|
337
359
|
: this.sendPromptFn;
|
|
338
|
-
//
|
|
339
|
-
if (!permCtx?.
|
|
360
|
+
// 无任何交互能力,直接 allow
|
|
361
|
+
if (!permCtx?.channelId || !sendPrompt) {
|
|
340
362
|
return { behavior: 'allow', updatedInput: input, decisionClassification: 'user_temporary' };
|
|
341
363
|
}
|
|
342
|
-
|
|
343
|
-
const interaction = {
|
|
344
|
-
type: 'interaction',
|
|
345
|
-
id: requestId,
|
|
346
|
-
kind: {
|
|
347
|
-
kind: 'action',
|
|
348
|
-
title: '📋 计划审批',
|
|
349
|
-
body: 'AI 已完成规划,等待审批。\n请查看以上计划内容后决定。',
|
|
350
|
-
buttons: [
|
|
351
|
-
{ key: 'approve', label: '✅ 批准执行', style: 'primary' },
|
|
352
|
-
{ key: 'reject', label: '❌ 拒绝', style: 'danger' },
|
|
353
|
-
],
|
|
354
|
-
},
|
|
355
|
-
channelId: permCtx.channelId,
|
|
356
|
-
sessionId,
|
|
357
|
-
};
|
|
364
|
+
// 尝试发送交互卡片
|
|
358
365
|
let cardSent = false;
|
|
359
|
-
|
|
360
|
-
const
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
366
|
+
if (permCtx.adapter?.sendInteraction) {
|
|
367
|
+
const requestId = `plan-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
368
|
+
const interaction = {
|
|
369
|
+
type: 'interaction',
|
|
370
|
+
id: requestId,
|
|
371
|
+
kind: {
|
|
372
|
+
kind: 'action',
|
|
373
|
+
title: '📋 计划审批',
|
|
374
|
+
body: 'AI 已完成规划,等待审批。\n请查看以上计划内容后决定。',
|
|
375
|
+
buttons: [
|
|
376
|
+
{ key: 'approve', label: '✅ 批准执行', style: 'primary' },
|
|
377
|
+
{ key: 'reject', label: '❌ 拒绝', style: 'danger' },
|
|
378
|
+
],
|
|
379
|
+
},
|
|
380
|
+
channelId: permCtx.channelId,
|
|
381
|
+
sessionId,
|
|
382
|
+
};
|
|
383
|
+
try {
|
|
384
|
+
const result = await permCtx.adapter.sendInteraction(permCtx.channelId, interaction, permCtx.replyContext);
|
|
385
|
+
cardSent = !!result;
|
|
386
|
+
}
|
|
387
|
+
catch (err) {
|
|
388
|
+
logger.warn('[AgentRunner] ExitPlanMode card send failed:', err);
|
|
389
|
+
}
|
|
390
|
+
if (cardSent) {
|
|
391
|
+
return new Promise((resolve) => {
|
|
392
|
+
permCtx.interactionRouter?.register(requestId, sessionId, (action) => {
|
|
393
|
+
if (action === 'approve') {
|
|
394
|
+
resolve({ behavior: 'allow', updatedInput: input, decisionClassification: 'user_temporary' });
|
|
395
|
+
}
|
|
396
|
+
else {
|
|
397
|
+
resolve({ behavior: 'deny', message: '用户拒绝了计划', decisionClassification: 'user_reject' });
|
|
398
|
+
}
|
|
399
|
+
});
|
|
400
|
+
});
|
|
401
|
+
}
|
|
368
402
|
}
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
403
|
+
// 文本 fallback:注册到 interactionRouter,等待用户 /ask 回复
|
|
404
|
+
if (permCtx.interactionRouter) {
|
|
405
|
+
await sendPrompt('📋 计划审批\nAI 已完成规划,等待审批。\n\n 1. 批准执行\n 2. 拒绝\n\n回复 /ask 1 批准,/ask 2 拒绝:');
|
|
406
|
+
const requestId = `plan-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
407
|
+
return new Promise((resolve) => {
|
|
408
|
+
permCtx.interactionRouter.register(requestId, sessionId, (action) => {
|
|
409
|
+
const trimmed = action.trim();
|
|
410
|
+
if (trimmed === '2' || trimmed.toLowerCase() === 'reject' || trimmed === '拒绝') {
|
|
411
|
+
resolve({ behavior: 'deny', message: '用户拒绝了计划', decisionClassification: 'user_reject' });
|
|
412
|
+
}
|
|
413
|
+
else {
|
|
414
|
+
resolve({ behavior: 'allow', updatedInput: input, decisionClassification: 'user_temporary' });
|
|
415
|
+
}
|
|
416
|
+
}, { timeoutMs: 300_000, onTimeout: () => resolve({ behavior: 'allow', updatedInput: input, decisionClassification: 'user_temporary' }) });
|
|
377
417
|
});
|
|
378
|
-
}
|
|
418
|
+
}
|
|
419
|
+
// 无交互能力,发提示后直接 allow
|
|
420
|
+
await sendPrompt('📋 计划审批\nAI 已完成规划,自动批准执行。');
|
|
421
|
+
return { behavior: 'allow', updatedInput: input, decisionClassification: 'user_temporary' };
|
|
379
422
|
}
|
|
380
423
|
/**
|
|
381
424
|
* SDK 原始事件 → 标准 AgentEvent 转换
|
package/dist/channels/aun.js
CHANGED
|
@@ -131,8 +131,27 @@ export class AUNChannel {
|
|
|
131
131
|
.replace(/[ \t]+/g, ' ')
|
|
132
132
|
.trim();
|
|
133
133
|
}
|
|
134
|
-
|
|
135
|
-
const
|
|
134
|
+
extractMentionAids(mentions) {
|
|
135
|
+
const aids = [];
|
|
136
|
+
for (const m of mentions) {
|
|
137
|
+
if (typeof m === 'string')
|
|
138
|
+
aids.push(m);
|
|
139
|
+
else if (m && typeof m === 'object' && typeof m.aid === 'string')
|
|
140
|
+
aids.push(m.aid);
|
|
141
|
+
}
|
|
142
|
+
return aids;
|
|
143
|
+
}
|
|
144
|
+
hasMentionAll(mentions) {
|
|
145
|
+
for (const m of mentions) {
|
|
146
|
+
if (m === 'all')
|
|
147
|
+
return true;
|
|
148
|
+
if (m && typeof m === 'object' && m.scope === 'all')
|
|
149
|
+
return true;
|
|
150
|
+
}
|
|
151
|
+
return false;
|
|
152
|
+
}
|
|
153
|
+
buildGroupReplyContext(taskId, senderAid, encrypted) {
|
|
154
|
+
const replyContext = { metadata: { encrypted } };
|
|
136
155
|
if (taskId)
|
|
137
156
|
replyContext.threadId = taskId;
|
|
138
157
|
replyContext.peerId = senderAid;
|
|
@@ -167,6 +186,11 @@ export class AUNChannel {
|
|
|
167
186
|
peerE2ee = new Map();
|
|
168
187
|
static E2EE_PROBE_TTL = 10 * 60 * 1000; // 10min
|
|
169
188
|
plaintextRecv = 0;
|
|
189
|
+
sessionModeResolver;
|
|
190
|
+
static PROACTIVE_ALLOW_TYPES = new Set([
|
|
191
|
+
'text', 'quote', 'image', 'video', 'voice', 'file', 'json',
|
|
192
|
+
'merge', 'link', 'location', 'personal_card',
|
|
193
|
+
]);
|
|
170
194
|
// Reconnect state (TS-layer fallback, on top of SDK auto_reconnect)
|
|
171
195
|
intentionalDisconnect = false;
|
|
172
196
|
reconnectAttempt = 0;
|
|
@@ -555,16 +579,10 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
|
|
|
555
579
|
logger.debug(`[AUN] P2P dropped: echo from self (from=${fromAid} mid=${messageId})`);
|
|
556
580
|
return;
|
|
557
581
|
}
|
|
558
|
-
//
|
|
582
|
+
// 记录入站消息加密状态,透传到出站 ReplyContext
|
|
559
583
|
const msgEncrypted = !!(msg.e2ee);
|
|
560
|
-
if (
|
|
561
|
-
|
|
562
|
-
this.peerE2ee.set(fromAid, { ok: true, ts: Date.now() });
|
|
563
|
-
}
|
|
564
|
-
else {
|
|
565
|
-
this.plaintextRecv++;
|
|
566
|
-
}
|
|
567
|
-
}
|
|
584
|
+
if (!msgEncrypted)
|
|
585
|
+
this.plaintextRecv++;
|
|
568
586
|
// Detect @mentions
|
|
569
587
|
const mentions = [];
|
|
570
588
|
if (this._aid && text.includes(`@${this._aid}`)) {
|
|
@@ -600,7 +618,10 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
|
|
|
600
618
|
const peerInfo = await this.fetchPeerInfo(fromAid);
|
|
601
619
|
const shortAid = this.getShortAid(fromAid);
|
|
602
620
|
const displayName = peerInfo.name || shortAid;
|
|
603
|
-
logger.info(`[AUN] P2P dispatched: from=${shortAid}(${displayName}) mid=${messageId} text=${finalText.slice(0, 60)}`);
|
|
621
|
+
logger.info(`[AUN] P2P dispatched: from=${shortAid}(${displayName}) mid=${messageId} encrypt=${msgEncrypted} text=${finalText.slice(0, 60)}`);
|
|
622
|
+
const replyContext = { metadata: { encrypted: msgEncrypted } };
|
|
623
|
+
if (taskId)
|
|
624
|
+
replyContext.threadId = taskId;
|
|
604
625
|
this.dispatchMessage({
|
|
605
626
|
channelId: chatId,
|
|
606
627
|
userId: fromAid,
|
|
@@ -612,6 +633,7 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
|
|
|
612
633
|
mentions,
|
|
613
634
|
peerName: displayName || undefined,
|
|
614
635
|
peerType: peerInfo.type || 'unknown',
|
|
636
|
+
replyContext,
|
|
615
637
|
});
|
|
616
638
|
}
|
|
617
639
|
async handleIncomingGroupMessage(data) {
|
|
@@ -640,16 +662,33 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
|
|
|
640
662
|
logger.debug(`[AUN] Group dropped: own message (group=${groupId} mid=${messageId})`);
|
|
641
663
|
return;
|
|
642
664
|
}
|
|
643
|
-
//
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
if (
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
665
|
+
// ── proactive 模式入站白名单 ──
|
|
666
|
+
if (this.sessionModeResolver) {
|
|
667
|
+
const sessionMode = await this.sessionModeResolver(groupId).catch(() => undefined);
|
|
668
|
+
if (sessionMode === 'proactive') {
|
|
669
|
+
const payloadObj = (payload && typeof payload === 'object') ? payload : null;
|
|
670
|
+
const payloadType = payloadObj?.type ?? '';
|
|
671
|
+
if (!AUNChannel.PROACTIVE_ALLOW_TYPES.has(payloadType)) {
|
|
672
|
+
this.acknowledgeImmediately(messageId, seq);
|
|
673
|
+
logger.debug(`[AUN] Group dropped (proactive deny): type=${payloadType} group=${groupId} sender=${senderAid} mid=${messageId}`);
|
|
674
|
+
return;
|
|
675
|
+
}
|
|
676
|
+
const rawText = typeof payloadObj?.text === 'string' ? payloadObj.text : '';
|
|
677
|
+
const rawMentions = Array.isArray(payloadObj?.mentions) ? payloadObj.mentions : [];
|
|
678
|
+
const mentionAids = this.extractMentionAids(rawMentions);
|
|
679
|
+
const mentionsSelf = !!this._aid && (this.hasExplicitMention(rawText, this._aid) || mentionAids.includes(this._aid));
|
|
680
|
+
const mentionsAll = this.hasExplicitMention(rawText, 'all') || this.hasMentionAll(rawMentions);
|
|
681
|
+
if (!mentionsSelf && !mentionsAll) {
|
|
682
|
+
this.acknowledgeImmediately(messageId, seq);
|
|
683
|
+
logger.debug(`[AUN] Group dropped (proactive whitelist): type=${payloadType} group=${groupId} sender=${senderAid} mid=${messageId}`);
|
|
684
|
+
return;
|
|
685
|
+
}
|
|
651
686
|
}
|
|
652
687
|
}
|
|
688
|
+
// 记录入站消息加密状态,透传到出站 ReplyContext
|
|
689
|
+
const msgEncrypted = !!(msg.e2ee);
|
|
690
|
+
if (!msgEncrypted)
|
|
691
|
+
this.plaintextRecv++;
|
|
653
692
|
// dispatch_mode from server tells agent how to work in this group
|
|
654
693
|
const dispatchMode = msg.dispatch_mode ?? payload?.dispatch_mode ?? 'mention';
|
|
655
694
|
const mentionedSelf = this._aid
|
|
@@ -712,7 +751,7 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
|
|
|
712
751
|
seq,
|
|
713
752
|
taskId,
|
|
714
753
|
mentions,
|
|
715
|
-
replyContext: this.buildGroupReplyContext(taskId, senderAid),
|
|
754
|
+
replyContext: this.buildGroupReplyContext(taskId, senderAid, msgEncrypted),
|
|
716
755
|
});
|
|
717
756
|
}
|
|
718
757
|
dispatchMessage(event) {
|
|
@@ -812,6 +851,9 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
|
|
|
812
851
|
onMessage(handler) {
|
|
813
852
|
this.messageHandler = handler;
|
|
814
853
|
}
|
|
854
|
+
setSessionModeResolver(resolver) {
|
|
855
|
+
this.sessionModeResolver = resolver;
|
|
856
|
+
}
|
|
815
857
|
onRecall(handler) {
|
|
816
858
|
this.recallHandler = handler;
|
|
817
859
|
}
|
|
@@ -851,17 +893,25 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
|
|
|
851
893
|
payload.chat_id = channelId;
|
|
852
894
|
}
|
|
853
895
|
const encryptTarget = isGroup ? channelId : targetAid;
|
|
854
|
-
const encrypt =
|
|
896
|
+
const encrypt = context?.metadata?.encrypted != null
|
|
897
|
+
? !!(context.metadata.encrypted)
|
|
898
|
+
: this.shouldEncrypt(encryptTarget);
|
|
855
899
|
const params = { payload, encrypt };
|
|
856
900
|
try {
|
|
857
901
|
if (isGroup) {
|
|
858
902
|
params.group_id = channelId;
|
|
859
903
|
const result = await this.callAndTrace('group.send', params);
|
|
860
904
|
if (!result || !result.message_id) {
|
|
861
|
-
|
|
905
|
+
const dispatchStatus = result?.message_dispatch?.status;
|
|
906
|
+
if (dispatchStatus === 'debounced' || dispatchStatus === 'dispatched') {
|
|
907
|
+
logger.info(`[AUN] group.send ok (${dispatchStatus}): group=${channelId} encrypt=${encrypt} text=${finalText.slice(0, 60)}`);
|
|
908
|
+
}
|
|
909
|
+
else {
|
|
910
|
+
logger.warn(`[AUN] group.send returned no message_id: ${JSON.stringify(result)}`);
|
|
911
|
+
}
|
|
862
912
|
}
|
|
863
913
|
else {
|
|
864
|
-
logger.info(`[AUN] group.send ok: group=${channelId} mid=${result.message_id} text=${finalText.slice(0, 60)}`);
|
|
914
|
+
logger.info(`[AUN] group.send ok: group=${channelId} mid=${result.message_id} encrypt=${encrypt} text=${finalText.slice(0, 60)}`);
|
|
865
915
|
}
|
|
866
916
|
}
|
|
867
917
|
else {
|
|
@@ -871,7 +921,7 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
|
|
|
871
921
|
logger.warn(`[AUN] message.send returned no message_id: ${JSON.stringify(result)}`);
|
|
872
922
|
}
|
|
873
923
|
else {
|
|
874
|
-
logger.info(`[AUN] message.send ok: to=${targetAid} mid=${result.message_id} text=${finalText.slice(0, 60)}`);
|
|
924
|
+
logger.info(`[AUN] message.send ok: to=${targetAid} mid=${result.message_id} encrypt=${encrypt} text=${finalText.slice(0, 60)}`);
|
|
875
925
|
}
|
|
876
926
|
}
|
|
877
927
|
}
|
|
@@ -917,7 +967,7 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
|
|
|
917
967
|
* selector 使用 context: { type: 'task', id: taskId }
|
|
918
968
|
* 存储键:group_id/peer_aid + sender_aid + context.type + context.id
|
|
919
969
|
*/
|
|
920
|
-
async sendThought(channelId, taskId, payload) {
|
|
970
|
+
async sendThought(channelId, taskId, payload, context) {
|
|
921
971
|
if (!this.connected || !this.client)
|
|
922
972
|
return;
|
|
923
973
|
if (!taskId)
|
|
@@ -925,22 +975,25 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
|
|
|
925
975
|
// Multi-instance routing
|
|
926
976
|
const colonIdx = channelId.indexOf(':');
|
|
927
977
|
const targetId = colonIdx > 0 ? channelId.substring(0, colonIdx) : channelId;
|
|
978
|
+
const encrypt = context?.metadata?.encrypted != null
|
|
979
|
+
? !!(context.metadata.encrypted)
|
|
980
|
+
: this.shouldEncrypt(targetId);
|
|
928
981
|
const params = {
|
|
929
982
|
context: { type: 'task', id: taskId },
|
|
930
983
|
payload,
|
|
931
|
-
encrypt
|
|
984
|
+
encrypt,
|
|
932
985
|
};
|
|
933
986
|
try {
|
|
934
987
|
const stage = payload?.stage ?? 'unknown';
|
|
935
988
|
if (this.isGroupId(channelId)) {
|
|
936
989
|
params.group_id = targetId;
|
|
937
990
|
await this.callAndTrace('group.thought.put', params);
|
|
938
|
-
logger.info(`[AUN] thought.put ok group=${targetId} task=${taskId} stage=${stage}`);
|
|
991
|
+
logger.info(`[AUN] thought.put ok group=${targetId} task=${taskId} stage=${stage} encrypt=${encrypt}`);
|
|
939
992
|
}
|
|
940
993
|
else {
|
|
941
994
|
params.to = targetId;
|
|
942
995
|
await this.callAndTrace('message.thought.put', params);
|
|
943
|
-
logger.info(`[AUN] thought.put ok p2p=${targetId} task=${taskId} stage=${stage}`);
|
|
996
|
+
logger.info(`[AUN] thought.put ok p2p=${targetId} task=${taskId} stage=${stage} encrypt=${encrypt}`);
|
|
944
997
|
}
|
|
945
998
|
}
|
|
946
999
|
catch (e) {
|
|
@@ -1035,7 +1088,9 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
|
|
|
1035
1088
|
filePayload.chat_id = channelId;
|
|
1036
1089
|
}
|
|
1037
1090
|
const encryptTarget = isGroup ? channelId : fileTargetAid;
|
|
1038
|
-
const encrypt =
|
|
1091
|
+
const encrypt = context?.metadata?.encrypted != null
|
|
1092
|
+
? !!(context.metadata.encrypted)
|
|
1093
|
+
: this.shouldEncrypt(encryptTarget);
|
|
1039
1094
|
const params = { payload: filePayload, encrypt };
|
|
1040
1095
|
try {
|
|
1041
1096
|
if (isGroup) {
|
|
@@ -1127,7 +1182,9 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
|
|
|
1127
1182
|
payload.chat_id = channelId;
|
|
1128
1183
|
}
|
|
1129
1184
|
const encryptTarget = isGroup ? channelId : statusTargetAid;
|
|
1130
|
-
const encrypt =
|
|
1185
|
+
const encrypt = context?.metadata?.encrypted != null
|
|
1186
|
+
? !!(context.metadata.encrypted)
|
|
1187
|
+
: this.shouldEncrypt(encryptTarget);
|
|
1131
1188
|
const params = { payload, encrypt };
|
|
1132
1189
|
const sendWithFallback = (method) => {
|
|
1133
1190
|
this.client.call(method, params).catch(e => {
|
|
@@ -1154,7 +1211,7 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
|
|
|
1154
1211
|
this.trace('OUT', 'message.send.status', params);
|
|
1155
1212
|
sendWithFallback('message.send');
|
|
1156
1213
|
}
|
|
1157
|
-
logger.info(`[AUN] task.${status} task=${taskId} session=${sessionId} target=${channelId}`);
|
|
1214
|
+
logger.info(`[AUN] task.${status} task=${taskId} session=${sessionId} encrypt=${encrypt} target=${channelId}`);
|
|
1158
1215
|
}
|
|
1159
1216
|
sendCustomPayload(channelId, payload) {
|
|
1160
1217
|
if (!this.client || !this.connected)
|
|
@@ -1367,7 +1424,7 @@ export class AUNChannelPlugin {
|
|
|
1367
1424
|
sendCustomPayload: (id, payload) => channel.sendCustomPayload(id, payload),
|
|
1368
1425
|
uploadAgentMd: (content) => channel.uploadAgentMd(content),
|
|
1369
1426
|
downloadAgentMd: (aid) => channel.downloadAgentMd(aid),
|
|
1370
|
-
putThought: (id, taskId, payload) => channel.sendThought(id, taskId, payload),
|
|
1427
|
+
putThought: (id, taskId, payload, context) => channel.sendThought(id, taskId, payload, context),
|
|
1371
1428
|
_selfAid: () => channel.getStatus().aid,
|
|
1372
1429
|
_selfName: () => channel.getSelfName(),
|
|
1373
1430
|
};
|
|
@@ -104,7 +104,7 @@ function formatIdleTime(ms) {
|
|
|
104
104
|
return '刚刚';
|
|
105
105
|
}
|
|
106
106
|
// 支持的命令列表
|
|
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', '/agentmd', '/chatmode'];
|
|
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', '/agentmd', '/chatmode', '/ask'];
|
|
108
108
|
// 命令别名映射
|
|
109
109
|
const aliases = {
|
|
110
110
|
'/p': '/project',
|
|
@@ -113,7 +113,7 @@ const aliases = {
|
|
|
113
113
|
'/rw': '/rewind'
|
|
114
114
|
};
|
|
115
115
|
// 命令快速路径前缀(所有命令都不进入消息队列)
|
|
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', '/agentmd'];
|
|
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', '/agentmd', '/ask'];
|
|
117
117
|
export class CommandHandler {
|
|
118
118
|
sessionManager;
|
|
119
119
|
config;
|
|
@@ -401,6 +401,10 @@ export class CommandHandler {
|
|
|
401
401
|
{ value: 'high', label: 'High' },
|
|
402
402
|
{ value: 'max', label: 'Max' },
|
|
403
403
|
] } },
|
|
404
|
+
{ cmd: '/chatmode', label: '切换会话模式', desc: '控制 Agent 主动性(被动响应或主动推进)', next: { type: 'select', items: [
|
|
405
|
+
{ value: 'interactive', label: '交互模式', desc: '仅在收到消息时响应' },
|
|
406
|
+
{ value: 'proactive', label: '主动模式', desc: 'Agent 可主动推进任务' },
|
|
407
|
+
] } },
|
|
404
408
|
]
|
|
405
409
|
});
|
|
406
410
|
items.push({
|
|
@@ -515,6 +519,57 @@ export class CommandHandler {
|
|
|
515
519
|
}
|
|
516
520
|
return null;
|
|
517
521
|
}
|
|
522
|
+
/** 菜单 exec 模式:查询状态或执行命令,返回结构化数据 */
|
|
523
|
+
async execMenu(cmd, mode, channel, channelId, userId) {
|
|
524
|
+
const session = await this.sessionManager.getActiveSession(channel, channelId);
|
|
525
|
+
if (!session)
|
|
526
|
+
return { error: '当前无活跃会话' };
|
|
527
|
+
const trimmed = cmd.trim();
|
|
528
|
+
const cmdBase = trimmed.split(' ')[0];
|
|
529
|
+
if (!cmdBase)
|
|
530
|
+
return { error: '缺少命令' };
|
|
531
|
+
const arg = trimmed.slice(cmdBase.length).trim();
|
|
532
|
+
if (cmdBase === '/perm') {
|
|
533
|
+
const currentMode = session.metadata?.permissionMode ?? DEFAULT_PERMISSION_MODE;
|
|
534
|
+
if (mode === 'query') {
|
|
535
|
+
return { data: { mode: currentMode } };
|
|
536
|
+
}
|
|
537
|
+
// update
|
|
538
|
+
if (!arg)
|
|
539
|
+
return { error: '缺少目标模式' };
|
|
540
|
+
const identity = this.sessionManager.resolveIdentity(channel, userId);
|
|
541
|
+
if (identity.role !== 'owner')
|
|
542
|
+
return { error: '无权限' };
|
|
543
|
+
const permAgent = this.getAgent(session.agentId);
|
|
544
|
+
const validModes = hasPermissionController(permAgent)
|
|
545
|
+
? permAgent.listModes().filter(m => m.available).map(m => m.key)
|
|
546
|
+
: ['auto', 'bypass', 'plan', 'edit', 'request', 'noask'];
|
|
547
|
+
if (!validModes.includes(arg))
|
|
548
|
+
return { error: `无效模式: ${arg}` };
|
|
549
|
+
const metadata = { ...(session.metadata || {}), permissionMode: arg };
|
|
550
|
+
await this.sessionManager.updateSession(session.id, { metadata });
|
|
551
|
+
return { data: { mode: arg } };
|
|
552
|
+
}
|
|
553
|
+
if (cmdBase === '/chatmode') {
|
|
554
|
+
const currentMode = session.sessionMode || 'interactive';
|
|
555
|
+
if (mode === 'query') {
|
|
556
|
+
return { data: { mode: currentMode } };
|
|
557
|
+
}
|
|
558
|
+
// update
|
|
559
|
+
if (!arg)
|
|
560
|
+
return { error: '缺少目标模式' };
|
|
561
|
+
if (arg !== 'interactive' && arg !== 'proactive')
|
|
562
|
+
return { error: `无效模式: ${arg}` };
|
|
563
|
+
const identity = this.sessionManager.resolveIdentity(channel, userId);
|
|
564
|
+
const chatType = session.chatType || 'private';
|
|
565
|
+
if (chatType === 'group' && identity.role !== 'owner' && identity.role !== 'admin') {
|
|
566
|
+
return { error: '无权限:群聊中仅管理员可切换' };
|
|
567
|
+
}
|
|
568
|
+
await this.sessionManager.updateSession(session.id, { sessionMode: arg });
|
|
569
|
+
return { data: { mode: arg } };
|
|
570
|
+
}
|
|
571
|
+
return { error: `不支持 exec 模式: ${cmdBase}` };
|
|
572
|
+
}
|
|
518
573
|
isCommand(content) {
|
|
519
574
|
return content === '/p' || content === '/s' || quickCommandPrefixes.some(cmd => content.startsWith(cmd));
|
|
520
575
|
}
|
|
@@ -817,6 +872,31 @@ export class CommandHandler {
|
|
|
817
872
|
const allModeKeys = hasPermissionController(permAgent) ? permAgent.listModes().map(m => m.key).join('|') : 'auto|bypass|request|edit|plan|noask';
|
|
818
873
|
return `❌ 未知参数: ${args}\n用法: /perm <${allModeKeys}> 或 /perm allow|always|deny`;
|
|
819
874
|
}
|
|
875
|
+
// /ask 命令:回答 AskUserQuestion / ExitPlanMode 的交互式问题
|
|
876
|
+
if (normalizedContent.startsWith('/ask')) {
|
|
877
|
+
const args = normalizedContent.slice(4).trim();
|
|
878
|
+
if (!args) {
|
|
879
|
+
// 无参数:列出当前 pending 的交互请求
|
|
880
|
+
const askResult = await this.ensureSession(channel, channelId, threadId);
|
|
881
|
+
if ('error' in askResult)
|
|
882
|
+
return askResult.error;
|
|
883
|
+
const pendingIds = this.interactionRouter?.getPending(askResult.session.id) || [];
|
|
884
|
+
if (pendingIds.length === 0)
|
|
885
|
+
return '当前没有待回答的问题';
|
|
886
|
+
return `当前有 ${pendingIds.length} 个待回答问题,请回复 /ask <选项>`;
|
|
887
|
+
}
|
|
888
|
+
const askResult = await this.ensureSession(channel, channelId, threadId);
|
|
889
|
+
if ('error' in askResult)
|
|
890
|
+
return askResult.error;
|
|
891
|
+
const { session: askSession } = askResult;
|
|
892
|
+
const pendingIds = this.interactionRouter?.getPending(askSession.id) || [];
|
|
893
|
+
if (pendingIds.length === 0)
|
|
894
|
+
return '❌ 当前没有待回答的问题';
|
|
895
|
+
// 路由到最早的 pending interaction
|
|
896
|
+
const targetId = pendingIds[0];
|
|
897
|
+
this.interactionRouter.handle({ type: 'interaction.response', id: targetId, action: args, operatorId: userId });
|
|
898
|
+
return `✓ 已回答`;
|
|
899
|
+
}
|
|
820
900
|
// /agent 命令:查看或切换 Agent 后端
|
|
821
901
|
if (normalizedContent === '/agent' || normalizedContent.startsWith('/agent ')) {
|
|
822
902
|
const args = normalizedContent.slice(6).trim();
|
|
@@ -2809,6 +2889,7 @@ export class CommandHandler {
|
|
|
2809
2889
|
* 从 session 恢复 ReplyContext,用于 ctl send 主动发送文本时的路由
|
|
2810
2890
|
* - 群聊话题:metadata.replyContext.{threadId,peerId}
|
|
2811
2891
|
* - 私聊:metadata.peerId
|
|
2892
|
+
* - taskId/chatmode:从 processing_state 和 sessionMode 注入
|
|
2812
2893
|
*/
|
|
2813
2894
|
buildCtlReplyContext(session) {
|
|
2814
2895
|
const ctx = {};
|
|
@@ -2819,6 +2900,18 @@ export class CommandHandler {
|
|
|
2819
2900
|
ctx.peerId = meta.replyContext.peerId;
|
|
2820
2901
|
if (!ctx.peerId && meta?.peerId)
|
|
2821
2902
|
ctx.peerId = meta.peerId;
|
|
2903
|
+
const taskId = this.sessionManager.getActiveTaskId(session.id);
|
|
2904
|
+
const chatmode = session.sessionMode || 'interactive';
|
|
2905
|
+
const encrypted = this.sessionManager.getSessionEncrypt(session.id);
|
|
2906
|
+
if (taskId || chatmode !== 'interactive' || encrypted != null) {
|
|
2907
|
+
ctx.metadata = {};
|
|
2908
|
+
if (taskId)
|
|
2909
|
+
ctx.metadata.taskId = taskId;
|
|
2910
|
+
if (chatmode !== 'interactive')
|
|
2911
|
+
ctx.metadata.chatmode = chatmode;
|
|
2912
|
+
if (encrypted != null)
|
|
2913
|
+
ctx.metadata.encrypted = encrypted;
|
|
2914
|
+
}
|
|
2822
2915
|
return Object.keys(ctx).length > 0 ? ctx : undefined;
|
|
2823
2916
|
}
|
|
2824
2917
|
/**
|
|
@@ -151,8 +151,19 @@ export class MessageBridge {
|
|
|
151
151
|
if (!parsed || typeof parsed !== 'object' || !parsed.type)
|
|
152
152
|
return false;
|
|
153
153
|
if (parsed.type === 'menu.query') {
|
|
154
|
-
|
|
155
|
-
|
|
154
|
+
if (parsed.cmd && (parsed.mode === 'query' || parsed.mode === 'update')) {
|
|
155
|
+
// exec 模式:查询状态或执行命令
|
|
156
|
+
const result = await this.cmdHandler.execMenu(parsed.cmd, parsed.mode, channel, msg.channelId, msg.peerId);
|
|
157
|
+
const base = { type: 'menu.response', cmd: parsed.cmd };
|
|
158
|
+
const response = JSON.stringify('error' in result ? { ...base, error: result.error } : { ...base, data: result.data });
|
|
159
|
+
if (adapter?.sendCustomPayload) {
|
|
160
|
+
adapter.sendCustomPayload(msg.channelId, response);
|
|
161
|
+
}
|
|
162
|
+
else {
|
|
163
|
+
await sendReply(msg.channelId, response);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
else if (parsed.cmd) {
|
|
156
167
|
// 动态子菜单查询
|
|
157
168
|
const items = await this.cmdHandler.getSubMenuItems(parsed.cmd, channel, msg.channelId, msg.peerId);
|
|
158
169
|
const response = JSON.stringify({ type: 'menu.response', cmd: parsed.cmd, items: items ?? [] });
|
|
@@ -165,6 +176,7 @@ export class MessageBridge {
|
|
|
165
176
|
}
|
|
166
177
|
else {
|
|
167
178
|
// 全量菜单
|
|
179
|
+
const identity = this.sessionManager.resolveIdentity(channel, msg.peerId);
|
|
168
180
|
const items = this.cmdHandler.getMenuItems(identity.role, msg.chatType || 'private');
|
|
169
181
|
const response = JSON.stringify({ type: 'menu.response', items });
|
|
170
182
|
if (adapter?.sendCustomPayload) {
|
|
@@ -316,7 +316,8 @@ export class MessageProcessor {
|
|
|
316
316
|
});
|
|
317
317
|
const imageInfo = message.images && message.images.length > 0 ? ` [${message.images.length} image(s)]` : '';
|
|
318
318
|
const modeInfo = isBackground ? ' [\u540e\u53f0]' : '';
|
|
319
|
-
|
|
319
|
+
const e2eeInfo = message.replyContext?.metadata?.encrypted != null ? ` encrypt=${message.replyContext.metadata.encrypted}` : '';
|
|
320
|
+
logger.info(`[${message.channel}] ${message.channelId}: ${message.content}${imageInfo}${modeInfo}${e2eeInfo}`);
|
|
320
321
|
logger.info(`[MessageProcessor] session=${session.id} task=${taskId} chatType=${session.chatType} sessionMode=${session.sessionMode} agentId=${session.agentId} msgChatType=${message.chatType ?? 'n/a'}`);
|
|
321
322
|
// 记录开始处理
|
|
322
323
|
this.eventBus.publish({ type: 'message:processing', sessionId: session.id });
|
|
@@ -363,7 +364,7 @@ export class MessageProcessor {
|
|
|
363
364
|
// Proactive 模式可观测:创建 ThoughtEmitter,将静默的流式事件转发为 thought
|
|
364
365
|
// selector: context = { type: 'task', id: taskId }
|
|
365
366
|
if (isProactive && adapter.putThought) {
|
|
366
|
-
thoughtEmitter = new ThoughtEmitter(adapter, message.channelId, taskId, chatmode);
|
|
367
|
+
thoughtEmitter = new ThoughtEmitter(adapter, message.channelId, taskId, chatmode, this.getReplyContext(message));
|
|
367
368
|
}
|
|
368
369
|
// 调用 AgentRunner(含上下文过长自动 compact 重试)
|
|
369
370
|
// 捕获当前消息的上下文(闭包),避免后续消息处理时串台
|
|
@@ -389,7 +390,10 @@ export class MessageProcessor {
|
|
|
389
390
|
// 设置 per-session 权限模式(默认 bypass,所有角色统一)
|
|
390
391
|
agent.setMode(session.metadata?.permissionMode ?? DEFAULT_PERMISSION_MODE);
|
|
391
392
|
// 标记会话为处理中(实时持久化,重启后可恢复)
|
|
392
|
-
this.sessionManager.markProcessing(session.id);
|
|
393
|
+
this.sessionManager.markProcessing(session.id, taskId);
|
|
394
|
+
if (message.replyContext?.metadata?.encrypted != null) {
|
|
395
|
+
this.sessionManager.setSessionEncrypt(session.id, !!(message.replyContext.metadata.encrypted));
|
|
396
|
+
}
|
|
393
397
|
logger.info(`[MessageProcessor] session ${session.id} marked as processing task=${taskId}`);
|
|
394
398
|
// 检查是否因新消息自动中断 — 包装 prompt 让 Agent 知道上下文
|
|
395
399
|
const prevInterruptReason = this.interruptedSessions.get(session.id);
|
|
@@ -14,8 +14,9 @@ export class ThoughtEmitter {
|
|
|
14
14
|
channelId;
|
|
15
15
|
taskId;
|
|
16
16
|
chatmode;
|
|
17
|
+
replyContext;
|
|
17
18
|
hasEmittedText = false;
|
|
18
|
-
constructor(adapter, channelId, taskId, chatmode = 'proactive') {
|
|
19
|
+
constructor(adapter, channelId, taskId, chatmode = 'proactive', replyContext) {
|
|
19
20
|
if (!taskId) {
|
|
20
21
|
throw new Error('[ThoughtEmitter] taskId is required at construction');
|
|
21
22
|
}
|
|
@@ -23,6 +24,7 @@ export class ThoughtEmitter {
|
|
|
23
24
|
this.channelId = channelId;
|
|
24
25
|
this.taskId = taskId;
|
|
25
26
|
this.chatmode = chatmode;
|
|
27
|
+
this.replyContext = replyContext;
|
|
26
28
|
logger.info(`[ThoughtEmitter] created channel=${channelId} task=${taskId} chatmode=${chatmode}`);
|
|
27
29
|
}
|
|
28
30
|
async emit(event) {
|
|
@@ -45,7 +47,7 @@ export class ThoughtEmitter {
|
|
|
45
47
|
payload.task_id = this.taskId;
|
|
46
48
|
payload.chatmode = this.chatmode;
|
|
47
49
|
try {
|
|
48
|
-
await this.adapter.putThought(this.channelId, this.taskId, payload);
|
|
50
|
+
await this.adapter.putThought(this.channelId, this.taskId, payload, this.replyContext);
|
|
49
51
|
}
|
|
50
52
|
catch (err) {
|
|
51
53
|
logger.debug(`[ThoughtEmitter] putThought failed: ${err.message}`);
|
|
@@ -13,6 +13,7 @@ export class SessionManager {
|
|
|
13
13
|
adminResolver;
|
|
14
14
|
sessionModeResolver;
|
|
15
15
|
fileAdapters = new Map();
|
|
16
|
+
sessionEncryptState = new Map();
|
|
16
17
|
constructor(dbPath = resolvePaths().db, eventBus, ownerResolver, adminResolver) {
|
|
17
18
|
ensureDir(path.dirname(dbPath));
|
|
18
19
|
this.db = new DatabaseSync(dbPath);
|
|
@@ -428,11 +429,21 @@ export class SessionManager {
|
|
|
428
429
|
}
|
|
429
430
|
/**
|
|
430
431
|
* 标记会话为处理中(实时写 DB,crash 也能恢复)
|
|
432
|
+
* processing_state 格式: "timestamp:taskId"
|
|
431
433
|
*/
|
|
432
|
-
markProcessing(sessionId) {
|
|
434
|
+
markProcessing(sessionId, taskId) {
|
|
433
435
|
const now = Date.now();
|
|
436
|
+
const state = taskId ? `${now}:${taskId}` : String(now);
|
|
434
437
|
this.db.prepare(`UPDATE sessions SET processing_state = ?, updated_at = ? WHERE id = ?`)
|
|
435
|
-
.run(
|
|
438
|
+
.run(state, now, sessionId);
|
|
439
|
+
}
|
|
440
|
+
/** 从 processing_state 解析当前活跃 taskId */
|
|
441
|
+
getActiveTaskId(sessionId) {
|
|
442
|
+
const row = this.db.prepare(`SELECT processing_state FROM sessions WHERE id = ?`).get(sessionId);
|
|
443
|
+
if (!row?.processing_state)
|
|
444
|
+
return undefined;
|
|
445
|
+
const colonIdx = row.processing_state.indexOf(':');
|
|
446
|
+
return colonIdx > 0 ? row.processing_state.slice(colonIdx + 1) : undefined;
|
|
436
447
|
}
|
|
437
448
|
/**
|
|
438
449
|
* 清除会话处理中状态
|
|
@@ -440,6 +451,13 @@ export class SessionManager {
|
|
|
440
451
|
clearProcessing(sessionId) {
|
|
441
452
|
this.db.prepare(`UPDATE sessions SET processing_state = NULL, updated_at = ? WHERE id = ?`)
|
|
442
453
|
.run(Date.now(), sessionId);
|
|
454
|
+
this.sessionEncryptState.delete(sessionId);
|
|
455
|
+
}
|
|
456
|
+
setSessionEncrypt(sessionId, encrypted) {
|
|
457
|
+
this.sessionEncryptState.set(sessionId, encrypted);
|
|
458
|
+
}
|
|
459
|
+
getSessionEncrypt(sessionId) {
|
|
460
|
+
return this.sessionEncryptState.get(sessionId);
|
|
443
461
|
}
|
|
444
462
|
/**
|
|
445
463
|
* 获取所有处于 processing 状态的会话(用于重启后恢复)
|
|
@@ -453,7 +471,8 @@ export class SessionManager {
|
|
|
453
471
|
const now = Date.now();
|
|
454
472
|
const result = [];
|
|
455
473
|
for (const row of rows) {
|
|
456
|
-
const
|
|
474
|
+
const colonIdx = row.processing_state.indexOf(':');
|
|
475
|
+
const ts = parseInt(colonIdx > 0 ? row.processing_state.slice(0, colonIdx) : row.processing_state, 10);
|
|
457
476
|
if (!isNaN(ts) && (now - ts) < maxAgeMs) {
|
|
458
477
|
result.push(this.rowToSession(row));
|
|
459
478
|
}
|
package/dist/index.js
CHANGED
|
@@ -289,6 +289,14 @@ async function main() {
|
|
|
289
289
|
});
|
|
290
290
|
});
|
|
291
291
|
}
|
|
292
|
+
// proactive 模式入站白名单:注入 sessionMode 查询器
|
|
293
|
+
if (typeof inst.channel.setSessionModeResolver === 'function') {
|
|
294
|
+
const chName = inst.adapter.channelName;
|
|
295
|
+
inst.channel.setSessionModeResolver(async (channelId) => {
|
|
296
|
+
const session = await sessionManager.getActiveSession(chName, channelId);
|
|
297
|
+
return session?.sessionMode;
|
|
298
|
+
});
|
|
299
|
+
}
|
|
292
300
|
}
|
|
293
301
|
if (channelType === 'dingtalk') {
|
|
294
302
|
msgBridge.register(inst.adapter.channelName, (handler) => inst.channel.onMessage(async (event) => {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "evolclaw",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.8.0",
|
|
4
4
|
"description": "Lightweight AI Agent gateway connecting Claude Agent SDK to messaging channels (Feishu, ACP) with multi-project session management",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
"prepublishOnly": "npm run build && npm test"
|
|
24
24
|
},
|
|
25
25
|
"dependencies": {
|
|
26
|
-
"@agentunion/fastaun": "^0.2.
|
|
26
|
+
"@agentunion/fastaun": "^0.2.17",
|
|
27
27
|
"@anthropic-ai/claude-agent-sdk": "^0.2.100",
|
|
28
28
|
"image-type": "^6.0.0",
|
|
29
29
|
"qrcode-terminal": "^0.12.0"
|