evolclaw 3.1.4 → 3.1.5
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/CHANGELOG.md +10 -0
- package/dist/agents/claude-runner.js +348 -156
- package/dist/agents/kit-renderer.js +176 -21
- package/dist/aun/aid/agentmd.js +68 -103
- package/dist/aun/aid/client.js +1 -29
- package/dist/aun/aid/identity.js +105 -64
- package/dist/aun/aid/index.js +2 -1
- package/dist/aun/aid/store.js +74 -0
- package/dist/aun/msg/p2p.js +26 -2
- package/dist/aun/rpc/connection.js +23 -30
- package/dist/channels/aun.js +77 -88
- package/dist/channels/dingtalk.js +1 -0
- package/dist/channels/feishu.js +270 -190
- package/dist/channels/qqbot.js +1 -0
- package/dist/channels/wechat.js +1 -0
- package/dist/channels/wecom.js +1 -0
- package/dist/cli/agent.js +11 -5
- package/dist/cli/bench.js +40 -23
- package/dist/cli/index.js +170 -44
- package/dist/cli/init-channel.js +5 -1
- package/dist/cli/model.js +324 -0
- package/dist/cli/net-check.js +133 -50
- package/dist/cli/watch-msg.js +7 -7
- package/dist/cli/watch-web/debug-log.js +18 -0
- package/dist/cli/watch-web/server.js +306 -0
- package/dist/cli/watch-web/sources/aid.js +63 -0
- package/dist/cli/watch-web/sources/msg.js +70 -0
- package/dist/cli/watch-web/sources/session.js +638 -0
- package/dist/cli/watch-web/sources/types.js +10 -0
- package/dist/cli/watch-web/static/app.js +546 -0
- package/dist/cli/watch-web/static/index.html +54 -0
- package/dist/cli/watch-web/static/style.css +247 -0
- package/dist/core/channel-loader.js +7 -4
- package/dist/core/command-handler.js +81 -86
- package/dist/core/evolagent-registry.js +1 -1
- package/dist/core/evolagent.js +4 -4
- package/dist/core/interaction-router.js +59 -0
- package/dist/core/message/message-bridge.js +6 -6
- package/dist/core/message/message-log.js +2 -2
- package/dist/core/message/message-processor.js +86 -101
- package/dist/core/message/stream-idle-monitor.js +21 -0
- package/dist/core/model/model-catalog.js +215 -0
- package/dist/core/model/model-scope.js +250 -0
- package/dist/core/relation/peer-identity.js +40 -49
- package/dist/core/relation/peer-key.js +16 -0
- package/dist/core/session/session-fs-store.js +34 -55
- package/dist/core/session/session-key.js +24 -0
- package/dist/core/session/session-manager.js +308 -251
- package/dist/core/session/session-mapper.js +9 -4
- package/dist/core/trigger/manager.js +3 -3
- package/dist/core/trigger/scheduler.js +2 -1
- package/dist/index.js +6 -2
- package/dist/ipc.js +22 -0
- package/kits/docs/GUIDE.md +2 -2
- package/kits/docs/INDEX.md +11 -7
- package/kits/docs/channels/aun.md +56 -17
- package/kits/docs/channels/feishu.md +41 -12
- package/kits/docs/context-assembly.md +181 -0
- package/kits/docs/evolclaw/agent.md +49 -0
- package/kits/docs/evolclaw/aid.md +49 -0
- package/kits/docs/evolclaw/ctl.md +46 -0
- package/kits/docs/evolclaw/group.md +82 -0
- package/kits/docs/evolclaw/msg.md +86 -0
- package/kits/docs/evolclaw/rpc.md +35 -0
- package/kits/docs/evolclaw/storage.md +49 -0
- package/kits/docs/venues/aun-group.md +10 -0
- package/kits/docs/venues/aun-private.md +10 -0
- package/kits/docs/venues/client-desktop.md +10 -0
- package/kits/docs/venues/client-mobile.md +10 -0
- package/kits/docs/venues/feishu-group.md +13 -0
- package/kits/docs/venues/feishu-private.md +9 -0
- package/kits/docs/venues/group.md +11 -0
- package/kits/docs/venues/private.md +10 -0
- package/kits/eck_manifest.json +72 -36
- package/kits/rules/01-overview.md +20 -10
- package/kits/rules/06-channel.md +30 -27
- package/kits/templates/system-fragments/session.md +10 -3
- package/kits/templates/system-fragments/venue.md +9 -0
- package/package.json +11 -6
- package/dist/aun/aid/lifecycle-log.js +0 -33
- package/dist/utils/aid-lifecycle-log.js +0 -33
- package/kits/docs/evolclaw/AGENT_CMD.md +0 -31
- package/kits/docs/evolclaw/MSG_GROUP.md +0 -30
- package/kits/docs/evolclaw/MSG_PRIVATE.md +0 -72
- package/kits/docs/evolclaw/tools.md +0 -25
|
@@ -1,14 +1,55 @@
|
|
|
1
1
|
import { logger } from '../utils/logger.js';
|
|
2
2
|
export class InteractionRouter {
|
|
3
3
|
handlers = new Map();
|
|
4
|
+
/** sessionId → 该会话当前待应答的交互数量,用于触发 wait 生命周期钩子 */
|
|
5
|
+
pendingBySession = new Map();
|
|
6
|
+
waitHooks;
|
|
7
|
+
setWaitHooks(hooks) {
|
|
8
|
+
this.waitHooks = hooks;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* 在 register() 之前提前标记 session 为等待状态(适用于发卡片有异步延迟的场景)。
|
|
12
|
+
* 必须与 unmarkWaiting() 配对使用,或后续 register() 会接管计数。
|
|
13
|
+
*/
|
|
14
|
+
markWaiting(sessionId) {
|
|
15
|
+
this.incPending(sessionId);
|
|
16
|
+
}
|
|
17
|
+
/** 取消 markWaiting() 的占位(后续若有 register() 接管则不需调用此方法) */
|
|
18
|
+
unmarkWaiting(sessionId) {
|
|
19
|
+
this.decPending(sessionId);
|
|
20
|
+
}
|
|
21
|
+
/** 登记一个待应答交互;session 计数 0→1 时触发 onWaitStart */
|
|
22
|
+
incPending(sessionId) {
|
|
23
|
+
const next = (this.pendingBySession.get(sessionId) ?? 0) + 1;
|
|
24
|
+
this.pendingBySession.set(sessionId, next);
|
|
25
|
+
if (next === 1)
|
|
26
|
+
this.waitHooks?.onWaitStart(sessionId);
|
|
27
|
+
}
|
|
28
|
+
/** 注销一个待应答交互;session 计数 1→0 时触发 onWaitEnd */
|
|
29
|
+
decPending(sessionId) {
|
|
30
|
+
const cur = this.pendingBySession.get(sessionId) ?? 0;
|
|
31
|
+
if (cur <= 0)
|
|
32
|
+
return;
|
|
33
|
+
const next = cur - 1;
|
|
34
|
+
if (next === 0) {
|
|
35
|
+
this.pendingBySession.delete(sessionId);
|
|
36
|
+
this.waitHooks?.onWaitEnd(sessionId);
|
|
37
|
+
}
|
|
38
|
+
else {
|
|
39
|
+
this.pendingBySession.set(sessionId, next);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
4
42
|
register(id, sessionId, callback, opts) {
|
|
43
|
+
// 同 id 替换:槽位本就占用,计数不变,不触发 wait 钩子
|
|
5
44
|
const existing = this.handlers.get(id);
|
|
6
45
|
if (existing?.timer)
|
|
7
46
|
clearTimeout(existing.timer);
|
|
47
|
+
const isReplacement = !!existing;
|
|
8
48
|
let timer;
|
|
9
49
|
if (opts?.timeoutMs && opts.timeoutMs > 0) {
|
|
10
50
|
timer = setTimeout(() => {
|
|
11
51
|
this.handlers.delete(id);
|
|
52
|
+
this.decPending(sessionId);
|
|
12
53
|
logger.debug(`[InteractionRouter] Timeout for interaction: ${id}`);
|
|
13
54
|
opts.onTimeout?.();
|
|
14
55
|
}, opts.timeoutMs);
|
|
@@ -20,6 +61,8 @@ export class InteractionRouter {
|
|
|
20
61
|
initiatorId: opts?.initiatorId,
|
|
21
62
|
fallbackCommand: opts?.fallbackCommand,
|
|
22
63
|
});
|
|
64
|
+
if (!isReplacement)
|
|
65
|
+
this.incPending(sessionId);
|
|
23
66
|
}
|
|
24
67
|
handle(response) {
|
|
25
68
|
const handler = this.handlers.get(response.id);
|
|
@@ -28,6 +71,7 @@ export class InteractionRouter {
|
|
|
28
71
|
if (handler.timer)
|
|
29
72
|
clearTimeout(handler.timer);
|
|
30
73
|
this.handlers.delete(response.id);
|
|
74
|
+
this.decPending(handler.sessionId);
|
|
31
75
|
try {
|
|
32
76
|
const result = handler.callback(response.action, response.values, response.operatorId);
|
|
33
77
|
if (result && typeof result.catch === 'function') {
|
|
@@ -47,6 +91,7 @@ export class InteractionRouter {
|
|
|
47
91
|
if (handler.timer)
|
|
48
92
|
clearTimeout(handler.timer);
|
|
49
93
|
this.handlers.delete(id);
|
|
94
|
+
this.decPending(handler.sessionId);
|
|
50
95
|
}
|
|
51
96
|
}
|
|
52
97
|
}
|
|
@@ -56,6 +101,7 @@ export class InteractionRouter {
|
|
|
56
101
|
if (handler.timer)
|
|
57
102
|
clearTimeout(handler.timer);
|
|
58
103
|
this.handlers.delete(id);
|
|
104
|
+
this.decPending(handler.sessionId);
|
|
59
105
|
}
|
|
60
106
|
}
|
|
61
107
|
getPending(sessionId) {
|
|
@@ -100,6 +146,16 @@ export function renderActionAsText(req) {
|
|
|
100
146
|
const lines = [action.title];
|
|
101
147
|
if (action.body)
|
|
102
148
|
lines.push(action.body);
|
|
149
|
+
// checkers 多选:渲染选项列表
|
|
150
|
+
if (action.checkers?.length) {
|
|
151
|
+
lines.push('');
|
|
152
|
+
action.checkers.forEach((chk, idx) => {
|
|
153
|
+
const desc = chk.description ? ` — ${chk.description}` : '';
|
|
154
|
+
lines.push(` ${idx + 1}. ${chk.label}${desc}`);
|
|
155
|
+
});
|
|
156
|
+
lines.push('', '回复选项编号(多选用逗号分隔),或输入自定义内容');
|
|
157
|
+
return lines.join('\n');
|
|
158
|
+
}
|
|
103
159
|
if (!fb) {
|
|
104
160
|
return lines.join('\n');
|
|
105
161
|
}
|
|
@@ -111,5 +167,8 @@ export function renderActionAsText(req) {
|
|
|
111
167
|
if (fb.acceptFreeText && fb.freeTextHint) {
|
|
112
168
|
lines.push(` ${fb.freeTextHint}`);
|
|
113
169
|
}
|
|
170
|
+
if (action.allowCustomInput) {
|
|
171
|
+
lines.push(` 或直接输入自定义内容`);
|
|
172
|
+
}
|
|
114
173
|
return lines.join('\n');
|
|
115
174
|
}
|
|
@@ -81,12 +81,12 @@ export class MessageBridge {
|
|
|
81
81
|
logger.debug(`[MessageBridge] Command detected: "${cmdContent}", routing to handler`);
|
|
82
82
|
// 命令也要记录入方向 jsonl(不创建 session,直接用 chatDirPath 计算路径)
|
|
83
83
|
try {
|
|
84
|
-
const chatDir = chatDirPath(resolvePaths().sessionsDir, msg.channelType || effectiveChannelType, msg.channelId, msg.
|
|
84
|
+
const chatDir = chatDirPath(resolvePaths().sessionsDir, msg.channelType || effectiveChannelType, msg.channelId, msg.selfAID || '');
|
|
85
85
|
const inboundEncrypt = msg.replyContext?.metadata?.encrypted != null ? !!(msg.replyContext.metadata.encrypted) : undefined;
|
|
86
86
|
const inboundChatmode = msg.replyContext?.metadata?.chatmode;
|
|
87
87
|
appendMessageLog(chatDir, buildInboundEntry({
|
|
88
88
|
from: msg.peerId || 'unknown',
|
|
89
|
-
to: msg.
|
|
89
|
+
to: msg.selfAID || 'self',
|
|
90
90
|
chatType: msg.chatType || 'private',
|
|
91
91
|
groupId: msg.groupId ?? null,
|
|
92
92
|
msgId: msg.messageId ?? null,
|
|
@@ -114,7 +114,7 @@ export class MessageBridge {
|
|
|
114
114
|
if (msg.threadId && msg.replyContext)
|
|
115
115
|
metadata.replyContext = msg.replyContext;
|
|
116
116
|
// 写入实例名(审计 + 精确出站路由)
|
|
117
|
-
metadata.
|
|
117
|
+
metadata.channelKey = channelName;
|
|
118
118
|
if (chatType === 'private' && msg.peerId) {
|
|
119
119
|
metadata.peerId = msg.peerId;
|
|
120
120
|
if (msg.peerName)
|
|
@@ -134,7 +134,7 @@ export class MessageBridge {
|
|
|
134
134
|
const owningAgent = this.agentRegistry?.resolveByChannel(channelName);
|
|
135
135
|
const effectiveProjectPath = owningAgent?.projectPath
|
|
136
136
|
?? this.defaultProjectPath;
|
|
137
|
-
const session = await this.sessionManager.getOrCreateSession(channelName, msg.channelId, effectiveProjectPath, msg.threadId, Object.keys(metadata).length ? metadata : undefined, undefined, msg.peerId, chatType, undefined, msg.
|
|
137
|
+
const session = await this.sessionManager.getOrCreateSession(channelName, msg.channelId, effectiveProjectPath, msg.threadId, Object.keys(metadata).length ? metadata : undefined, undefined, msg.peerId, chatType, undefined, msg.selfAID, msg.channelType || effectiveChannelType, msg.peerType);
|
|
138
138
|
// 4. 消息前缀(由 policy 决定)
|
|
139
139
|
const channelInfo = this.processor.getChannelInfo?.(channelName);
|
|
140
140
|
if (channelInfo?.policy) {
|
|
@@ -147,7 +147,7 @@ export class MessageBridge {
|
|
|
147
147
|
channel: channelName,
|
|
148
148
|
channelType: msg.channelType || effectiveChannelType,
|
|
149
149
|
channelId: msg.channelId, content,
|
|
150
|
-
|
|
150
|
+
selfAID: msg.selfAID,
|
|
151
151
|
chatType,
|
|
152
152
|
images: msg.images, timestamp: Date.now(),
|
|
153
153
|
peerId: msg.peerId, peerName: msg.peerName,
|
|
@@ -162,7 +162,7 @@ export class MessageBridge {
|
|
|
162
162
|
const inboundChatmode = msg.replyContext?.metadata?.chatmode;
|
|
163
163
|
appendMessageLog(chatDir, buildInboundEntry({
|
|
164
164
|
from: msg.peerId || 'unknown',
|
|
165
|
-
to: msg.
|
|
165
|
+
to: msg.selfAID || 'self',
|
|
166
166
|
chatType,
|
|
167
167
|
groupId: msg.groupId ?? null,
|
|
168
168
|
msgId: msg.messageId ?? null,
|
|
@@ -31,8 +31,8 @@ function formatTimestampMs(epochMs) {
|
|
|
31
31
|
export function messageLogPath(chatDir) {
|
|
32
32
|
return path.join(chatDir, MESSAGE_LOG_FILE);
|
|
33
33
|
}
|
|
34
|
-
export function resolveChatDir(sessionsDir, channelType, channelId,
|
|
35
|
-
return chatDirPath(sessionsDir, channelType, channelId,
|
|
34
|
+
export function resolveChatDir(sessionsDir, channelType, channelId, selfAID) {
|
|
35
|
+
return chatDirPath(sessionsDir, channelType, channelId, selfAID);
|
|
36
36
|
}
|
|
37
37
|
export function appendMessageLog(chatDir, entry) {
|
|
38
38
|
if (entry.dir === 'in' && isDuplicate(entry.msgId)) {
|
|
@@ -12,6 +12,8 @@ import { getPackageRoot, resolveRoot } from '../../paths.js';
|
|
|
12
12
|
import { renderKitSections } from '../../agents/kit-renderer.js';
|
|
13
13
|
import { normalizeBaseagent } from '../../agents/baseagent-normalize.js';
|
|
14
14
|
import { renderActionAsText, renderCommandCardAsText } from '../interaction-router.js';
|
|
15
|
+
import { formatPeerKey } from '../relation/peer-key.js';
|
|
16
|
+
import { resolveEffectiveModel } from '../model/model-scope.js';
|
|
15
17
|
function getContextTooLongHint(agent) {
|
|
16
18
|
if (canCompactAgent(agent)) {
|
|
17
19
|
return '上下文过长,请精简提问或使用 /compact 压缩上下文';
|
|
@@ -69,7 +71,8 @@ export class MessageProcessor {
|
|
|
69
71
|
interruptedSessions = new Map(); // sessionId → reason ('new_message' | 'stop' | ...)
|
|
70
72
|
interactionRouter;
|
|
71
73
|
messageQueue;
|
|
72
|
-
|
|
74
|
+
/** sessionId → 活跃的空闲监控器,用于等待用户交互期间暂停/恢复计时 */
|
|
75
|
+
activeMonitors = new Map();
|
|
73
76
|
/**
|
|
74
77
|
* Get the runner for a given (channel, baseagent) pair.
|
|
75
78
|
*
|
|
@@ -99,7 +102,7 @@ export class MessageProcessor {
|
|
|
99
102
|
if (session.threadId)
|
|
100
103
|
return false;
|
|
101
104
|
// 使用 session 自身的 channelType 精确定位 active.json,避免扫描误匹配
|
|
102
|
-
const active = this.sessionManager.getActiveSessionSync(session.channel, session.channelId, session.channelType, session.
|
|
105
|
+
const active = this.sessionManager.getActiveSessionSync(session.channel, session.channelId, session.channelType, session.selfAID);
|
|
103
106
|
return active ? session.id !== active.id : false;
|
|
104
107
|
}
|
|
105
108
|
constructor(agentRunnerOrMap, sessionManager, globalSettings, messageCache, eventBus, commandHandler, primaryRunnerKey) {
|
|
@@ -126,6 +129,16 @@ export class MessageProcessor {
|
|
|
126
129
|
}
|
|
127
130
|
setInteractionRouter(router) {
|
|
128
131
|
this.interactionRouter = router;
|
|
132
|
+
// 等待用户交互期间暂停 idle 监控,应答/取消/超时后恢复——
|
|
133
|
+
// 避免把「正在等用户点按钮」误判为「任务卡死」而中断任务。
|
|
134
|
+
router.setWaitHooks({
|
|
135
|
+
onWaitStart: (sessionId) => {
|
|
136
|
+
this.activeMonitors.get(sessionId)?.pause();
|
|
137
|
+
},
|
|
138
|
+
onWaitEnd: (sessionId) => {
|
|
139
|
+
this.activeMonitors.get(sessionId)?.resume();
|
|
140
|
+
},
|
|
141
|
+
});
|
|
129
142
|
}
|
|
130
143
|
setMessageQueue(queue) {
|
|
131
144
|
this.messageQueue = queue;
|
|
@@ -211,10 +224,10 @@ export class MessageProcessor {
|
|
|
211
224
|
*/
|
|
212
225
|
async processMessage(message) {
|
|
213
226
|
const idleMs = (this.globalSettings.idleMonitor?.timeout ?? 120) * 1000;
|
|
214
|
-
// 先解析会话,再优先用 session.metadata.
|
|
227
|
+
// 先解析会话,再优先用 session.metadata.channelKey 精确定位实例级 adapter
|
|
215
228
|
// message.channel 现在存实例名(channelName),可直接用于精确路由
|
|
216
229
|
const { session, absoluteProjectPath } = await this.resolveSession(message);
|
|
217
|
-
const channelKey = session.metadata?.
|
|
230
|
+
const channelKey = session.metadata?.channelKey || message.channel;
|
|
218
231
|
const channelInfo = this.resolveChannelInfo(channelKey);
|
|
219
232
|
if (!channelInfo) {
|
|
220
233
|
logger.error(`[MessageProcessor] Unknown channel: ${channelKey}`);
|
|
@@ -252,6 +265,7 @@ export class MessageProcessor {
|
|
|
252
265
|
if (!monitorEnabled)
|
|
253
266
|
return;
|
|
254
267
|
monitor = new StreamIdleMonitor(idleMs);
|
|
268
|
+
this.activeMonitors.set(streamKey, monitor);
|
|
255
269
|
monitorInterval = setInterval(() => {
|
|
256
270
|
// Drain all pending levels in one tick
|
|
257
271
|
let result = monitor.check();
|
|
@@ -332,6 +346,7 @@ export class MessageProcessor {
|
|
|
332
346
|
finally {
|
|
333
347
|
if (monitorInterval)
|
|
334
348
|
clearInterval(monitorInterval);
|
|
349
|
+
this.activeMonitors.delete(streamKey);
|
|
335
350
|
}
|
|
336
351
|
}
|
|
337
352
|
/** 获取回复上下文(跟着任务走) */
|
|
@@ -341,7 +356,7 @@ export class MessageProcessor {
|
|
|
341
356
|
/** 自动安全模式已禁用:仅保留错误计数,不再自动切换状态 */
|
|
342
357
|
async _processMessageInternal(message, session, absoluteProjectPath, resetTimer, shouldSuppress) {
|
|
343
358
|
const messageId = `${message.channel}_${message.channelId}_${message.timestamp || Date.now()}`;
|
|
344
|
-
const channelKey = session.metadata?.
|
|
359
|
+
const channelKey = session.metadata?.channelKey || message.channel;
|
|
345
360
|
const channelInfo = this.resolveChannelInfo(channelKey);
|
|
346
361
|
// Per-method agent name for stats bucketing (agent.name or '<unknown>')
|
|
347
362
|
const agentNameForStats = this.agentRegistry?.resolveByChannel(channelKey)?.name ?? '<unknown>';
|
|
@@ -448,7 +463,7 @@ export class MessageProcessor {
|
|
|
448
463
|
if (baseReplyCtx) {
|
|
449
464
|
Object.assign(opts, baseReplyCtx);
|
|
450
465
|
}
|
|
451
|
-
else if (firstReply && message.messageId) {
|
|
466
|
+
else if (firstReply && message.messageId && message.source !== 'trigger') {
|
|
452
467
|
if (payload.kind === 'result.text' && payload.text) {
|
|
453
468
|
opts.replyToMessageId = message.messageId;
|
|
454
469
|
firstReply = false;
|
|
@@ -508,6 +523,7 @@ export class MessageProcessor {
|
|
|
508
523
|
: message.content;
|
|
509
524
|
let streamResult = { isError: false, lastReplyText: '', fullText: '', hasReceivedText: false };
|
|
510
525
|
let effectiveSystemPrompt;
|
|
526
|
+
let modelOverride;
|
|
511
527
|
try {
|
|
512
528
|
// 动态构建运行时上下文提示
|
|
513
529
|
const contextParts = [];
|
|
@@ -533,19 +549,41 @@ export class MessageProcessor {
|
|
|
533
549
|
contextParts.push(persona);
|
|
534
550
|
if (working)
|
|
535
551
|
contextParts.push(`[当前关注]\n${working}`);
|
|
536
|
-
// 计算 peerKey: <
|
|
552
|
+
// 计算 peerKey: <channelType>#<urlEncode(peerId)>
|
|
537
553
|
const peerIdRaw = message.peerId;
|
|
538
554
|
const peerKey = (currentChannelType && peerIdRaw)
|
|
539
|
-
?
|
|
555
|
+
? formatPeerKey(currentChannelType, peerIdRaw)
|
|
540
556
|
: undefined;
|
|
557
|
+
// 按 关系级 > agent级 > 全局 解析本次调用的模型/强度,作为 per-call 入参传入 runQuery。
|
|
558
|
+
// 不缓存、不绑会话——改关系级/agent级后该范围所有会话的下条消息即时生效;
|
|
559
|
+
// 多对端并发各自独立解析、各自传参,无共享状态可被污染。
|
|
560
|
+
try {
|
|
561
|
+
const resolved = resolveEffectiveModel({ self: selfAid || undefined, peerKey });
|
|
562
|
+
if (resolved.model)
|
|
563
|
+
modelOverride = { model: resolved.model, effort: resolved.effort };
|
|
564
|
+
}
|
|
565
|
+
catch (e) {
|
|
566
|
+
logger.warn(`[MessageProcessor] resolveEffectiveModel failed: ${e instanceof Error ? e.message : String(e)}`);
|
|
567
|
+
}
|
|
541
568
|
const normalizedBaseagent = normalizeBaseagent(agent.name);
|
|
542
569
|
const agentModel = (typeof agent.getModel === 'function') ? agent.getModel() : undefined;
|
|
543
570
|
// Kit renderer: 组装上下文
|
|
571
|
+
const pkgRoot = getPackageRoot();
|
|
544
572
|
const kitCtx = {
|
|
545
573
|
vars: {
|
|
546
574
|
EVOLCLAW_HOME: resolveRoot(),
|
|
547
|
-
PACKAGE_ROOT:
|
|
575
|
+
PACKAGE_ROOT: pkgRoot,
|
|
548
576
|
CURRENT_PROJECT: absoluteProjectPath,
|
|
577
|
+
// ECK 派生路径(manifest 引用时需要展开)
|
|
578
|
+
KITS: path.join(pkgRoot, 'kits'),
|
|
579
|
+
KITS_RULES: path.join(pkgRoot, 'kits', 'rules'),
|
|
580
|
+
KITS_DOCS: path.join(pkgRoot, 'kits', 'docs'),
|
|
581
|
+
KITS_TEMPLATES: path.join(pkgRoot, 'kits', 'templates'),
|
|
582
|
+
KITS_FRAGMENTS: path.join(pkgRoot, 'kits', 'templates', 'system-fragments'),
|
|
583
|
+
// 路径变量(用于 manifest 路径展开,resolvePath 用 ctx.vars 取真值)
|
|
584
|
+
PERSONAL_DIR: selfAid ? path.join(resolveRoot(), 'agents', selfAid, 'personal') : undefined,
|
|
585
|
+
RELATIONS_DIR: selfAid ? path.join(resolveRoot(), 'agents', selfAid, 'relations') : undefined,
|
|
586
|
+
VENUES_DIR: selfAid ? path.join(resolveRoot(), 'agents', selfAid, 'venues') : undefined,
|
|
549
587
|
selfAid: selfAid || undefined,
|
|
550
588
|
selfName: selfName || undefined,
|
|
551
589
|
hasPersona: !!persona,
|
|
@@ -553,18 +591,24 @@ export class MessageProcessor {
|
|
|
553
591
|
peerId: peerIdRaw || undefined,
|
|
554
592
|
peerKey,
|
|
555
593
|
peerName: peerName || undefined,
|
|
556
|
-
peerRole: session.identity?.role || '
|
|
594
|
+
peerRole: session.identity?.role || 'anonymous',
|
|
557
595
|
peerType: message.peerType || undefined,
|
|
558
596
|
groupId: session.metadata?.groupId || undefined,
|
|
559
597
|
chatType: session.chatType || null,
|
|
560
598
|
channel: currentChannelType || null,
|
|
561
599
|
venueUid: undefined,
|
|
600
|
+
// 群分发模式 / 客户端类型 / 权限模式
|
|
601
|
+
dispatch: session.metadata?.dispatchMode || undefined,
|
|
602
|
+
clientType: message.clientType || undefined,
|
|
603
|
+
permissionMode: session.metadata?.permissionMode || 'auto',
|
|
562
604
|
capabilities: capParts.length > 0 ? capParts.join('、') : undefined,
|
|
563
605
|
project: path.basename(absoluteProjectPath),
|
|
564
606
|
sessionId: session.id,
|
|
565
607
|
sessionName: session.name || undefined,
|
|
566
608
|
sessionCreatedAt: session.createdAt ? new Date(session.createdAt).toISOString() : undefined,
|
|
567
609
|
threadId: session.threadId || undefined,
|
|
610
|
+
// Stage 3: sessionKey 持久化字段
|
|
611
|
+
sessionKey: session.sessionKey,
|
|
568
612
|
chatMode: isProactive ? 'proactive' : 'interactive',
|
|
569
613
|
readonly: session.metadata?.permissionMode === 'readonly',
|
|
570
614
|
baseAgent: normalizedBaseagent.canonical,
|
|
@@ -584,7 +628,7 @@ export class MessageProcessor {
|
|
|
584
628
|
let streamRegistered = false;
|
|
585
629
|
try {
|
|
586
630
|
logger.info(`[MessageProcessor] agent.runQuery start: agent=${agent.name} session=${session.id} task=${taskId} attempt=${attempt}/${MAX_RETRIES} agentSessionId=${session.agentSessionId ?? 'none'}`);
|
|
587
|
-
const stream = await agent.runQuery(session.id, effectivePrompt, absoluteProjectPath, session.agentSessionId, message.images, effectiveSystemPrompt, this.sessionManager);
|
|
631
|
+
const stream = await agent.runQuery(session.id, effectivePrompt, absoluteProjectPath, session.agentSessionId, message.images, effectiveSystemPrompt, this.sessionManager, modelOverride);
|
|
588
632
|
agent.registerStream(streamKey, stream);
|
|
589
633
|
streamRegistered = true;
|
|
590
634
|
streamResult = await this.processEventStream(stream, session, agent, renderer, resetTimer, shouldSuppress);
|
|
@@ -613,9 +657,11 @@ export class MessageProcessor {
|
|
|
613
657
|
await renderer.flush();
|
|
614
658
|
const compacted = await agent.compact(session.id, session.agentSessionId, absoluteProjectPath);
|
|
615
659
|
if (compacted) {
|
|
616
|
-
// compact
|
|
660
|
+
// compact 成功,清除第一次流中混入的错误文本,再重试
|
|
661
|
+
const ctxErrPattern = /prompt is too long|input is too long|上下文过长/i;
|
|
662
|
+
renderer.stripContextError(ctxErrPattern);
|
|
617
663
|
renderer.addNotice('✅ 压缩完成,继续处理...', 'info', 'compact-retry', true);
|
|
618
|
-
const retryStream = await agent.runQuery(session.id, '上下文已自动压缩,请继续之前未完成的任务。', absoluteProjectPath, session.agentSessionId, undefined, effectiveSystemPrompt, this.sessionManager);
|
|
664
|
+
const retryStream = await agent.runQuery(session.id, '上下文已自动压缩,请继续之前未完成的任务。', absoluteProjectPath, session.agentSessionId, undefined, effectiveSystemPrompt, this.sessionManager, modelOverride);
|
|
619
665
|
agent.registerStream(streamKey, retryStream);
|
|
620
666
|
streamResult = await this.processEventStream(retryStream, session, agent, renderer, resetTimer, shouldSuppress);
|
|
621
667
|
}
|
|
@@ -636,12 +682,13 @@ export class MessageProcessor {
|
|
|
636
682
|
contextTooLongPattern.test(errorsText) ||
|
|
637
683
|
contextTooLongPattern.test(streamResult.fullText));
|
|
638
684
|
if (isPromptTooLong) {
|
|
685
|
+
renderer.stripContextError(contextTooLongPattern);
|
|
639
686
|
renderer.addNotice('上下文过长,正在压缩会话...', 'warn', 'compact-trigger', true);
|
|
640
687
|
await renderer.flush();
|
|
641
688
|
const compacted = await agent.compact(session.id, session.agentSessionId, absoluteProjectPath);
|
|
642
689
|
if (compacted) {
|
|
643
690
|
renderer.addNotice('✅ 压缩完成,继续处理...', 'info', 'compact-retry', true);
|
|
644
|
-
const retryStream = await agent.runQuery(session.id, '上下文已自动压缩,请继续之前未完成的任务。', absoluteProjectPath, session.agentSessionId, undefined, effectiveSystemPrompt, this.sessionManager);
|
|
691
|
+
const retryStream = await agent.runQuery(session.id, '上下文已自动压缩,请继续之前未完成的任务。', absoluteProjectPath, session.agentSessionId, undefined, effectiveSystemPrompt, this.sessionManager, modelOverride);
|
|
645
692
|
agent.registerStream(streamKey, retryStream);
|
|
646
693
|
streamResult = await this.processEventStream(retryStream, session, agent, renderer, resetTimer, shouldSuppress);
|
|
647
694
|
// 重试后仍然 prompt_too_long:清理 renderer 中可能混入的错误文本,显示友好提示
|
|
@@ -830,7 +877,7 @@ export class MessageProcessor {
|
|
|
830
877
|
adapter.send(envelope, { kind: 'status.interrupted', metadata: { reason: interruptReason } }).catch(() => { });
|
|
831
878
|
}
|
|
832
879
|
else {
|
|
833
|
-
adapter.send(envelope, { kind: 'status.completed', metadata: { durationMs, numTurns: streamResult.numTurns, usage: streamResult.usage } }).catch(() => { });
|
|
880
|
+
adapter.send(envelope, { kind: 'status.completed', metadata: { durationMs, ttftMs: streamResult.ttftMs, numTurns: streamResult.numTurns, usage: streamResult.usage } }).catch(() => { });
|
|
834
881
|
}
|
|
835
882
|
}
|
|
836
883
|
if (message.triggerMeta) {
|
|
@@ -953,7 +1000,7 @@ export class MessageProcessor {
|
|
|
953
1000
|
// 获取 session 用于话题回复(如果 resolveSession 已执行)
|
|
954
1001
|
let sendOpts;
|
|
955
1002
|
try {
|
|
956
|
-
await this.sessionManager.getOrCreateSession(message.channel, message.channelId, this.agentRegistry?.resolveByChannel(message.channel)?.projectPath || process.cwd(), message.threadId, undefined, undefined, message.peerId, message.chatType, undefined, message.
|
|
1003
|
+
await this.sessionManager.getOrCreateSession(message.channel, message.channelId, this.agentRegistry?.resolveByChannel(message.channel)?.projectPath || process.cwd(), message.threadId, undefined, undefined, message.peerId, message.chatType, undefined, message.selfAID, message.channelType, message.peerType);
|
|
957
1004
|
sendOpts = this.getReplyContext(message);
|
|
958
1005
|
}
|
|
959
1006
|
catch { }
|
|
@@ -990,7 +1037,7 @@ export class MessageProcessor {
|
|
|
990
1037
|
: path.resolve(process.cwd(), session.projectPath);
|
|
991
1038
|
return { session, absoluteProjectPath };
|
|
992
1039
|
}
|
|
993
|
-
const session = await this.sessionManager.getOrCreateSession(message.channel, message.channelId, projectPath, message.threadId, metadata, undefined, message.peerId, message.chatType, undefined, message.
|
|
1040
|
+
const session = await this.sessionManager.getOrCreateSession(message.channel, message.channelId, projectPath, message.threadId, metadata, undefined, message.peerId, message.chatType, undefined, message.selfAID, message.channelType, message.peerType);
|
|
994
1041
|
// 兜底纠正1:群聊强制 proactive
|
|
995
1042
|
if (message.chatType === 'group' && session.sessionMode !== 'proactive') {
|
|
996
1043
|
logger.info(`[MessageProcessor] group proactive upgrade: sessionId=${session.id} ${session.sessionMode} -> proactive`);
|
|
@@ -1004,6 +1051,13 @@ export class MessageProcessor {
|
|
|
1004
1051
|
session.sessionMode = 'proactive';
|
|
1005
1052
|
await this.sessionManager.updateSession(session.id, { sessionMode: 'proactive' });
|
|
1006
1053
|
}
|
|
1054
|
+
// Proactive→Interactive 模式切换提示:上一轮 proactive 使用了标志位,本轮已切换为 interactive
|
|
1055
|
+
if (session.sessionMode === 'interactive' && session.metadata?.lastProactiveFlag) {
|
|
1056
|
+
message.content = '本轮会话已切换为 interactive 模式,无需调用工具发送消息。\n\n' + message.content;
|
|
1057
|
+
delete session.metadata.lastProactiveFlag;
|
|
1058
|
+
await this.sessionManager.updateSession(session.id, { metadata: session.metadata });
|
|
1059
|
+
logger.info(`[MessageProcessor] Injected interactive mode hint for session ${session.id}`);
|
|
1060
|
+
}
|
|
1007
1061
|
// replyContext 不再写入 session.metadata(跟着 message 走,避免群聊多人覆盖)
|
|
1008
1062
|
const absoluteProjectPath = path.isAbsolute(session.projectPath)
|
|
1009
1063
|
? session.projectPath
|
|
@@ -1018,7 +1072,7 @@ export class MessageProcessor {
|
|
|
1018
1072
|
*/
|
|
1019
1073
|
async processEventStream(stream, session, agent, renderer, resetTimer, shouldSuppress) {
|
|
1020
1074
|
// Per-session agent name for stats bucketing
|
|
1021
|
-
const agentNameForStats = this.agentRegistry?.resolveByChannel(session.metadata?.
|
|
1075
|
+
const agentNameForStats = this.agentRegistry?.resolveByChannel(session.metadata?.channelKey || session.channel)?.name ?? '<unknown>';
|
|
1022
1076
|
let hasReceivedText = false;
|
|
1023
1077
|
let hasErrorResult = false; // 是否已有 tool_result/error 事件输出过错误
|
|
1024
1078
|
let completeResult = { isError: false, lastReplyText: '', fullText: '', hasReceivedText: false };
|
|
@@ -1100,7 +1154,7 @@ export class MessageProcessor {
|
|
|
1100
1154
|
if (event.type === 'compact') {
|
|
1101
1155
|
this.eventBus.publish({ type: 'runner:compact-complete', sessionId: session.id, preTokens: event.preTokens });
|
|
1102
1156
|
if (!shouldSuppress()) {
|
|
1103
|
-
renderer.addNotice(`\ud83d\udca1
|
|
1157
|
+
renderer.addNotice(`\ud83d\udca1 会话压缩完成,继续执行...)`, 'info', 'compact');
|
|
1104
1158
|
}
|
|
1105
1159
|
}
|
|
1106
1160
|
// 子任务进度
|
|
@@ -1178,14 +1232,15 @@ export class MessageProcessor {
|
|
|
1178
1232
|
// SDK 可能产生多个 complete 事件(如 subagent 或 auto-compact 二次查询),
|
|
1179
1233
|
// 仅记录状态,最终 flush(true) 在流结束后统一执行
|
|
1180
1234
|
if (event.type === 'complete') {
|
|
1181
|
-
|
|
1235
|
+
const isAbort = event.terminalReason === 'aborted_streaming' || event.terminalReason === 'aborted_tools';
|
|
1236
|
+
logger.info(`[MessageProcessor] ${isAbort ? 'task interrupted' : 'complete event'}: isError=${event.isError} terminalReason=${event.terminalReason ?? 'none'} subtype=${event.subtype ?? 'none'} hasReceivedText=${hasReceivedText}`);
|
|
1182
1237
|
// 自动回填会话名称
|
|
1183
1238
|
if (event.sessionTitle && session.name === '默认会话') {
|
|
1184
1239
|
await this.sessionManager.renameSession(session.id, event.sessionTitle);
|
|
1185
1240
|
logger.info(`[MessageProcessor] Auto-filled session name: ${event.sessionTitle}`);
|
|
1186
1241
|
}
|
|
1187
1242
|
// 记录完成状态 + 最后一轮回复文本(后续 complete 覆盖前序)
|
|
1188
|
-
completeResult = { isError: !!event.isError, subtype: event.subtype, errors: event.errors, terminalReason: event.terminalReason, lastReplyText, fullText: event.result || '', hasReceivedText, numTurns: event.numTurns, usage: event.usage };
|
|
1243
|
+
completeResult = { isError: !!event.isError, subtype: event.subtype, errors: event.errors, terminalReason: event.terminalReason, lastReplyText, fullText: event.result || '', hasReceivedText, numTurns: event.numTurns, ttftMs: event.ttftMs, usage: event.usage };
|
|
1189
1244
|
// thought jsonl 写入已下沉到 aun.ts:sendThought 成功后,
|
|
1190
1245
|
// 由那里按 LLM 输出的每个 text item 单独写一条,此处不再写。
|
|
1191
1246
|
// 失败且无前置错误输出:显示 errors 摘要
|
|
@@ -1213,6 +1268,15 @@ export class MessageProcessor {
|
|
|
1213
1268
|
if (renderer.hasContent()) {
|
|
1214
1269
|
await renderer.flushActivitiesOnly();
|
|
1215
1270
|
}
|
|
1271
|
+
// 检测 proactive 标志位,设置 lastProactiveFlag 供模式切换提示使用
|
|
1272
|
+
if (session.sessionMode === 'proactive' && lastReplyText) {
|
|
1273
|
+
if (/\[PROACTIVE:REPLY_CONFIRMED_(SENT|NONE)\]/.test(lastReplyText)) {
|
|
1274
|
+
session.metadata = session.metadata || {};
|
|
1275
|
+
session.metadata.lastProactiveFlag = true;
|
|
1276
|
+
await this.sessionManager.updateSession(session.id, { metadata: session.metadata });
|
|
1277
|
+
logger.debug(`[MessageProcessor] Set lastProactiveFlag for session ${session.id}`);
|
|
1278
|
+
}
|
|
1279
|
+
}
|
|
1216
1280
|
}
|
|
1217
1281
|
continue;
|
|
1218
1282
|
}
|
|
@@ -1232,7 +1296,7 @@ export class MessageProcessor {
|
|
|
1232
1296
|
logger.info(`[MessageProcessor] Auto-filled session name: ${event.sessionTitle}`);
|
|
1233
1297
|
}
|
|
1234
1298
|
// 记录完成状态
|
|
1235
|
-
completeResult = { isError: !!event.isError, subtype: event.subtype, errors: event.errors, terminalReason: event.terminalReason, lastReplyText, fullText: event.result || '', hasReceivedText, numTurns: event.numTurns, usage: event.usage };
|
|
1299
|
+
completeResult = { isError: !!event.isError, subtype: event.subtype, errors: event.errors, terminalReason: event.terminalReason, lastReplyText, fullText: event.result || '', hasReceivedText, numTurns: event.numTurns, ttftMs: event.ttftMs, usage: event.usage };
|
|
1236
1300
|
if (event.subtype === 'success') {
|
|
1237
1301
|
this.messageCache.addEvent(session.id, {
|
|
1238
1302
|
type: 'completed',
|
|
@@ -1345,85 +1409,6 @@ export class MessageProcessor {
|
|
|
1345
1409
|
// 都找不到,返回项目根目录路径
|
|
1346
1410
|
return rootPath;
|
|
1347
1411
|
}
|
|
1348
|
-
/**
|
|
1349
|
-
* 确保全局数据目录下有最新版本的 SKILLS.md
|
|
1350
|
-
* 目标:{EVOLCLAW_HOME}/data/SKILLS.md
|
|
1351
|
-
*/
|
|
1352
|
-
ensureSkillsFile() {
|
|
1353
|
-
try {
|
|
1354
|
-
const targetDir = path.join(resolveRoot(), 'data');
|
|
1355
|
-
const targetPath = path.join(targetDir, 'SKILLS.md');
|
|
1356
|
-
const templatePath = path.join(getPackageRoot(), 'src', 'templates', 'skills.md');
|
|
1357
|
-
// 模板不存在则跳过(构建环境可能没有 src/)
|
|
1358
|
-
if (!fs.existsSync(templatePath)) {
|
|
1359
|
-
// 尝试 dist/templates/skills.md
|
|
1360
|
-
const distTemplatePath = path.join(getPackageRoot(), 'dist', 'templates', 'skills.md');
|
|
1361
|
-
if (!fs.existsSync(distTemplatePath))
|
|
1362
|
-
return;
|
|
1363
|
-
this.copySkillsIfNeeded(distTemplatePath, targetDir, targetPath);
|
|
1364
|
-
return;
|
|
1365
|
-
}
|
|
1366
|
-
this.copySkillsIfNeeded(templatePath, targetDir, targetPath);
|
|
1367
|
-
}
|
|
1368
|
-
catch {
|
|
1369
|
-
// 静默失败,不影响正常消息处理
|
|
1370
|
-
}
|
|
1371
|
-
}
|
|
1372
|
-
copySkillsIfNeeded(templatePath, targetDir, targetPath) {
|
|
1373
|
-
const templateContent = fs.readFileSync(templatePath, 'utf-8');
|
|
1374
|
-
const templateVersion = templateContent.match(/^version:\s*(.+)$/m)?.[1]?.trim() || '0';
|
|
1375
|
-
if (fs.existsSync(targetPath)) {
|
|
1376
|
-
const existing = fs.readFileSync(targetPath, 'utf-8');
|
|
1377
|
-
const existingVersion = existing.match(/^version:\s*(.+)$/m)?.[1]?.trim() || '0';
|
|
1378
|
-
if (this.compareSemver(existingVersion, templateVersion) >= 0)
|
|
1379
|
-
return; // 已是最新
|
|
1380
|
-
}
|
|
1381
|
-
if (!fs.existsSync(targetDir)) {
|
|
1382
|
-
fs.mkdirSync(targetDir, { recursive: true });
|
|
1383
|
-
}
|
|
1384
|
-
fs.writeFileSync(targetPath, templateContent, 'utf-8');
|
|
1385
|
-
}
|
|
1386
|
-
/** 简易 semver 比较:支持 "1", "1.0", "1.0.0" 等格式,返回 -1/0/1 */
|
|
1387
|
-
compareSemver(a, b) {
|
|
1388
|
-
const pa = a.split('.').map(Number);
|
|
1389
|
-
const pb = b.split('.').map(Number);
|
|
1390
|
-
const len = Math.max(pa.length, pb.length);
|
|
1391
|
-
for (let i = 0; i < len; i++) {
|
|
1392
|
-
const na = pa[i] || 0;
|
|
1393
|
-
const nb = pb[i] || 0;
|
|
1394
|
-
if (na !== nb)
|
|
1395
|
-
return na > nb ? 1 : -1;
|
|
1396
|
-
}
|
|
1397
|
-
return 0;
|
|
1398
|
-
}
|
|
1399
|
-
/**
|
|
1400
|
-
* 从 data/SKILLS.md 读取 frontmatter 并生成提示。
|
|
1401
|
-
* 不缓存:每次读取保证用户编辑立即生效。
|
|
1402
|
-
* 调用前应确保 ensureSkillsFile() 已执行过(首次落盘)。
|
|
1403
|
-
*/
|
|
1404
|
-
getSkillsHint() {
|
|
1405
|
-
try {
|
|
1406
|
-
const skillsPath = path.join(resolveRoot(), 'data', 'SKILLS.md');
|
|
1407
|
-
if (!fs.existsSync(skillsPath))
|
|
1408
|
-
return null;
|
|
1409
|
-
const content = fs.readFileSync(skillsPath, 'utf-8');
|
|
1410
|
-
const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---/);
|
|
1411
|
-
if (!frontmatterMatch)
|
|
1412
|
-
return null;
|
|
1413
|
-
const fm = frontmatterMatch[1];
|
|
1414
|
-
const desc = fm.match(/^description:\s*(.+)$/m)?.[1]?.trim() || 'EvolClaw 运行时管理指令';
|
|
1415
|
-
const trigger = fm.match(/^trigger:\s*(.+)$/m)?.[1]?.trim() || '';
|
|
1416
|
-
const parts = [
|
|
1417
|
-
`可通过 Bash 指令管理运行时,${desc}。`,
|
|
1418
|
-
trigger ? `触发时机:${trigger}。` : '',
|
|
1419
|
-
`完整文档见 ${skillsPath}`,
|
|
1420
|
-
];
|
|
1421
|
-
return parts.filter(Boolean).join('');
|
|
1422
|
-
}
|
|
1423
|
-
catch {
|
|
1424
|
-
return null;
|
|
1425
|
-
}
|
|
1426
|
-
}
|
|
1427
1412
|
/**
|
|
1428
1413
|
* 判断文件路径是否为占位符/示例文本
|
|
1429
1414
|
* 用于过滤大模型在说明文字中误写的 [SEND_FILE:...] 标记
|
|
@@ -10,6 +10,7 @@ export class StreamIdleMonitor {
|
|
|
10
10
|
state;
|
|
11
11
|
triggeredLevels = new Set();
|
|
12
12
|
idleMs;
|
|
13
|
+
paused = false;
|
|
13
14
|
constructor(idleMs) {
|
|
14
15
|
this.idleMs = idleMs;
|
|
15
16
|
this.state = {
|
|
@@ -22,6 +23,24 @@ export class StreamIdleMonitor {
|
|
|
22
23
|
hasReceivedText: false,
|
|
23
24
|
};
|
|
24
25
|
}
|
|
26
|
+
/**
|
|
27
|
+
* 暂停空闲计时(等待用户交互期间调用,如权限确认 / AskUserQuestion / PlanMode)。
|
|
28
|
+
* 暂停期间 check() 始终返回 null,等待时长不计入 idle。
|
|
29
|
+
*/
|
|
30
|
+
pause() {
|
|
31
|
+
this.paused = true;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* 恢复空闲计时。从恢复时刻重新起算 idle,并清空已触发级别——
|
|
35
|
+
* 用户应答后是一次全新的执行周期,不应继承等待前的 idle 状态。
|
|
36
|
+
*/
|
|
37
|
+
resume() {
|
|
38
|
+
if (!this.paused)
|
|
39
|
+
return;
|
|
40
|
+
this.paused = false;
|
|
41
|
+
this.state.lastEventTime = Date.now();
|
|
42
|
+
this.triggeredLevels.clear();
|
|
43
|
+
}
|
|
25
44
|
/**
|
|
26
45
|
* 记录 SDK 事件,更新状态并重置空闲计时
|
|
27
46
|
*/
|
|
@@ -44,6 +63,8 @@ export class StreamIdleMonitor {
|
|
|
44
63
|
* 检查空闲状态,返回 null(未空闲)或分级结果
|
|
45
64
|
*/
|
|
46
65
|
check() {
|
|
66
|
+
if (this.paused)
|
|
67
|
+
return null;
|
|
47
68
|
const now = Date.now();
|
|
48
69
|
const idleDuration = now - this.state.lastEventTime;
|
|
49
70
|
const notifyThreshold = this.idleMs * NOTIFY_MULTIPLIER;
|