evolcore 0.0.3 → 0.0.4
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 +64 -0
- package/README.md +45 -10
- package/bin/ec-safe-output.js +89 -24
- package/dist/agents/baseagent.js +46 -0
- package/dist/agents/claude-runner.js +1 -31
- package/dist/agents/codex-app-server-client.js +68 -0
- package/dist/agents/codex-runner.js +157 -5
- package/dist/agents/runner-types.js +2 -2
- package/dist/aun/aid/agentmd.js +79 -0
- package/dist/aun/aid/encryption-seed-policy.js +29 -0
- package/dist/aun/aid/index.js +1 -1
- package/dist/aun/aid/store.js +2 -5
- package/dist/channels/aun.js +220 -112
- package/dist/channels/daemon.js +18 -4
- package/dist/channels/dingtalk.js +52 -137
- package/dist/channels/feishu.js +56 -4
- package/dist/channels/qqbot.js +23 -1
- package/dist/channels/wechat.js +303 -155
- package/dist/channels/wecom.js +383 -7
- package/dist/cli/aun-commands.js +11 -11
- package/dist/cli/handoff-command.js +2 -1
- package/dist/cli/init-channel.js +185 -67
- package/dist/cli/init.js +7 -5
- package/dist/cli/trigger-command.js +92 -11
- package/dist/config/access-policy-domain.js +38 -0
- package/dist/config/access-policy.js +134 -0
- package/dist/config/builtin-role-templates.js +27 -14
- package/dist/config/builtin-roles.js +25 -6
- package/dist/config/config-field-policy.js +17 -5
- package/dist/config/config-manager.js +198 -22
- package/dist/config/config-operation-service.js +35 -38
- package/dist/config/contact-bind-code.js +274 -0
- package/dist/config/contact-book-store.js +173 -5
- package/dist/config/contact-book.js +32 -0
- package/dist/config/contact-operation-service.js +9 -3
- package/dist/config/contact-request-service.js +331 -0
- package/dist/config/peer-role-resolver.js +3 -1
- package/dist/config/schema-registry.js +49 -24
- package/dist/config-store.js +14 -2
- package/dist/core/auth/agent-delegation.js +105 -11
- package/dist/core/auth/authorization-audit.js +6 -0
- package/dist/core/auth/operation-authorizer.js +8 -0
- package/dist/core/auth/operation-catalog.js +51 -3
- package/dist/core/auth/trigger-authorization.js +15 -0
- package/dist/core/bootstrap-service.js +2 -9
- package/dist/core/channel-loader.js +6 -2
- package/dist/core/command/command-handler.js +10 -13
- package/dist/core/command/connect-menu.js +249 -24
- package/dist/core/command/evol-menu-version-gate.js +38 -0
- package/dist/core/command/group-menu.js +421 -0
- package/dist/core/command/menu-handler.js +269 -85
- package/dist/core/command/menu-protocol.js +8 -2
- package/dist/core/command/menu-token-store.js +102 -0
- package/dist/core/command/slash-gate.js +1 -1
- package/dist/core/command/slash-handler.js +158 -31
- package/dist/core/daemon-file-cache.js +28 -0
- package/dist/core/event-catalog.js +29 -0
- package/dist/core/evolagent-registry.js +4 -39
- package/dist/core/handoff/runtime.js +162 -37
- package/dist/core/handoff/store.js +46 -2
- package/dist/core/handoff/types.js +1 -0
- package/dist/core/inference/text-inference.js +16 -74
- package/dist/core/message/file-markers.js +7 -0
- package/dist/core/message/im-renderer.js +18 -14
- package/dist/core/message/inbound-admission.js +124 -0
- package/dist/core/message/message-bridge.js +616 -74
- package/dist/core/message/message-log.js +1 -0
- package/dist/core/message/message-queue.js +185 -10
- package/dist/core/message/peer-mode.js +7 -8
- package/dist/core/message/response-engine.js +381 -79
- package/dist/core/permission/approval-gateway.js +17 -10
- package/dist/core/permission/ec-command-parser.js +101 -46
- package/dist/core/permission/tool-policy.js +69 -24
- package/dist/core/protected-paths.js +36 -11
- package/dist/core/session/session-manager.js +25 -3
- package/dist/core/session/session-mapper.js +2 -0
- package/dist/core/session/session-renew.js +125 -69
- package/dist/core/session/session-turn-coordinator.js +5 -1
- package/dist/eck/kit-renderer.js +12 -1
- package/dist/eck/message-renderer.js +79 -1
- package/dist/index.js +224 -89
- package/dist/ipc.js +81 -4
- package/dist/paths.js +3 -0
- package/dist/response-system/config-resolver.js +29 -0
- package/dist/response-system/coordinator.js +8 -32
- package/dist/response-system/engines/v1/proactive-flow.js +1 -1
- package/dist/response-system/index.js +1 -0
- package/dist/response-system/modes/single-session/index.js +7 -4
- package/dist/trigger/parser.js +55 -15
- package/dist/trigger/patch.js +4 -1
- package/dist/trigger/scheduler.js +441 -39
- package/dist/utils/error-dict.json +7 -0
- package/dist/utils/evolcore-version.js +21 -0
- package/dist/utils/logger.js +2 -2
- package/dist/utils/stable-semver.js +21 -0
- package/dist/utils/stats.js +33 -9
- package/kits/docs/channels/aun.md +4 -13
- package/kits/docs/evolcore/INDEX.md +1 -1
- package/kits/docs/evolcore/config.md +47 -2
- package/kits/docs/evolcore/contact.md +7 -3
- package/kits/docs/evolcore/group-rules.md +46 -4
- package/kits/docs/evolcore/group.md +4 -4
- package/kits/docs/evolcore/msg.md +5 -5
- package/kits/docs/evolcore/trigger.md +25 -8
- package/kits/docs/path-registry.md +36 -17
- package/kits/eck_message_manifest.json +12 -1
- package/kits/migrations/migrate-contact-book-v2.mjs +7 -0
- package/kits/rules/01-overview.md +17 -7
- package/kits/rules/02-navigation.md +38 -18
- package/kits/rules/03-identity.md +28 -24
- package/kits/rules/04-relation.md +44 -28
- package/kits/rules/05-venue.md +31 -15
- package/kits/rules/06-channel.md +11 -7
- package/kits/schemas/_meta.json +18 -7
- package/kits/schemas/agent-config.schema.7.json +303 -0
- package/kits/schemas/agent-config.schema.8.json +304 -0
- package/kits/schemas/agent-config.schema.9.json +374 -0
- package/kits/schemas/contact-book.schema.3.json +67 -0
- package/kits/schemas/daemon.schema.1.json +3 -2
- package/kits/schemas/daemon.schema.2.json +101 -0
- package/kits/schemas/daemon.schema.3.json +123 -0
- package/kits/schemas/defaults.schema.2.json +85 -0
- package/kits/schemas/defaults.schema.3.json +73 -0
- package/kits/schemas/relation-config.schema.6.json +46 -0
- package/kits/schemas/relation-config.schema.7.json +59 -0
- package/kits/schemas/single-session.schema.2.json +57 -0
- package/kits/templates/message-fragments/handoff-context-to-target.md +9 -0
- package/kits/templates/message-fragments/handoff-request-to-target.md +14 -8
- package/kits/templates/message-fragments/handoff-response-to-origin.md +5 -6
- package/kits/templates/roles/admin.json +46 -0
- package/kits/templates/roles/member.json +4 -0
- package/kits/templates/roles/visitor.json +4 -0
- package/kits/templates/system-fragments/channel.md +12 -2
- package/kits/templates/system-fragments/identity.md +3 -1
- package/kits/templates/system-fragments/relation.md +1 -1
- package/kits/templates/system-fragments/session.md +6 -2
- package/package.json +4 -2
- package/MIGRATION-0.5.0.md +0 -378
- package/ROLE_ACCESS_CONTROL.md +0 -174
- package/dist/channels/contact-bind-code.js +0 -134
- package/dist/channels/wecom-card.js +0 -101
- package/dist/channels/wecom-onboarding.js +0 -82
- package/dist/channels/wecom-state.js +0 -191
package/dist/channels/daemon.js
CHANGED
|
@@ -28,10 +28,13 @@ export class DaemonChannel {
|
|
|
28
28
|
const pendingId = ctx.attemptId ?? runId;
|
|
29
29
|
const messageId = pendingId;
|
|
30
30
|
const startedAt = Date.now();
|
|
31
|
+
const remainingExecutionMs = ctx.executionDeadlineAt === undefined
|
|
32
|
+
? this.totalExecutionMs
|
|
33
|
+
: Math.max(1, ctx.executionDeadlineAt - startedAt);
|
|
31
34
|
const promise = new Promise((resolve) => {
|
|
32
35
|
const watchdogTimer = setTimeout(() => this.watchdogTimeout(pendingId), this.watchdogMs);
|
|
33
36
|
watchdogTimer.unref?.();
|
|
34
|
-
const totalExecutionTimer = setTimeout(() => this.totalExecutionTimeout(pendingId),
|
|
37
|
+
const totalExecutionTimer = setTimeout(() => this.totalExecutionTimeout(pendingId, remainingExecutionMs), remainingExecutionMs);
|
|
35
38
|
totalExecutionTimer.unref?.();
|
|
36
39
|
this.pending.set(pendingId, {
|
|
37
40
|
resolve,
|
|
@@ -208,6 +211,17 @@ export class DaemonChannel {
|
|
|
208
211
|
: `trigger:${trigger.id}`;
|
|
209
212
|
return await this.sessionManager.getOrCreateSession(this.channelName, channelId, projectPath, threadId, { channelKey: this.channelKey, peerId: `trigger:${trigger.id}`, peerName: trigger.name }, trigger.name, undefined, 'private', baseagent, trigger.agentAid, this.channelName, 'system');
|
|
210
213
|
}
|
|
214
|
+
executionDeadlineAt(startedAt = Date.now()) {
|
|
215
|
+
return startedAt + this.totalExecutionMs;
|
|
216
|
+
}
|
|
217
|
+
async getResumableConversationSession(sessionId) {
|
|
218
|
+
const session = await this.sessionManager.getSessionById(sessionId);
|
|
219
|
+
if (!session?.agentSessionId)
|
|
220
|
+
return undefined;
|
|
221
|
+
if (!this.sessionManager.checkSessionFileExists(session.projectPath, session.agentSessionId, session.baseagent))
|
|
222
|
+
return undefined;
|
|
223
|
+
return session;
|
|
224
|
+
}
|
|
211
225
|
/** Internal Trigger audit hook. It does not emit user-visible output. */
|
|
212
226
|
recordExecutionAnomaly(runId, value) {
|
|
213
227
|
const slot = this.pending.get(runId);
|
|
@@ -330,17 +344,17 @@ export class DaemonChannel {
|
|
|
330
344
|
slot.watchdogTimer = setTimeout(() => this.watchdogTimeout(runId), this.watchdogMs);
|
|
331
345
|
slot.watchdogTimer.unref?.();
|
|
332
346
|
}
|
|
333
|
-
totalExecutionTimeout(runId) {
|
|
347
|
+
totalExecutionTimeout(runId, totalExecutionMs = this.totalExecutionMs) {
|
|
334
348
|
const slot = this.pending.get(runId);
|
|
335
349
|
if (!slot || slot.terminalClaim)
|
|
336
350
|
return;
|
|
337
351
|
slot.acc.error = {
|
|
338
352
|
reason: 'daemon_total_execution_timeout',
|
|
339
|
-
text: `daemon conversation exceeded total execution limit of ${
|
|
353
|
+
text: `daemon conversation exceeded total execution limit of ${totalExecutionMs}ms`,
|
|
340
354
|
};
|
|
341
355
|
if (!this.claimTerminal(slot, 'timeout', {
|
|
342
356
|
errorType: 'daemon_total_execution_timeout',
|
|
343
|
-
totalExecutionMs
|
|
357
|
+
totalExecutionMs,
|
|
344
358
|
}))
|
|
345
359
|
return;
|
|
346
360
|
this.cancelDeliveryInBackground(slot);
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
import fs from 'fs';
|
|
2
2
|
import path from 'path';
|
|
3
|
-
import { createHash
|
|
3
|
+
import { createHash } from 'crypto';
|
|
4
4
|
import { logger } from '../utils/logger.js';
|
|
5
5
|
import { requireOptional } from '../utils/npm-ops.js';
|
|
6
6
|
import { middleOutputModePolicy, resolveShowActivities, showActivitiesPolicy } from '../core/channel-loader.js';
|
|
7
7
|
import { formatItemsAsText } from '../core/message/items-formatter.js';
|
|
8
|
+
import { createSendFileMarkerPattern } from '../core/message/file-markers.js';
|
|
8
9
|
import { sentReceipt, suppressedReceipt } from '../core/message/send-receipt.js';
|
|
9
10
|
import { initWelcomeManager, sendWelcomeIfNeeded } from '../utils/welcome.js';
|
|
10
|
-
import {
|
|
11
|
-
import { bindContactAlias } from '../config/contact-book.js';
|
|
11
|
+
import { CONTACT_BIND_CODE_TTL_MS, CONTACT_BIND_MAX_FAILED_ATTEMPTS, ContactBindCodeRegistry, handleExplicitContactBindCommand, } from '../config/contact-bind-code.js';
|
|
12
12
|
import { resolvePaths } from '../paths.js';
|
|
13
13
|
const DINGTALK_QUEUED_EMOTION = 'Pin';
|
|
14
14
|
const DINGTALK_THINKING_EMOTION = 'BusinessTrip';
|
|
@@ -125,129 +125,24 @@ export function parseDingtalkCardCallback(data) {
|
|
|
125
125
|
values: params,
|
|
126
126
|
};
|
|
127
127
|
}
|
|
128
|
-
export const DINGTALK_BIND_CODE_TTL_MS =
|
|
129
|
-
export const DINGTALK_BIND_MAX_FAILED_ATTEMPTS =
|
|
130
|
-
const
|
|
128
|
+
export const DINGTALK_BIND_CODE_TTL_MS = CONTACT_BIND_CODE_TTL_MS;
|
|
129
|
+
export const DINGTALK_BIND_MAX_FAILED_ATTEMPTS = CONTACT_BIND_MAX_FAILED_ATTEMPTS;
|
|
130
|
+
const dingtalkContactBinds = new ContactBindCodeRegistry({
|
|
131
|
+
channelType: 'dingtalk',
|
|
132
|
+
displayName: '钉钉',
|
|
133
|
+
initCommand: 'ec init dingtalk',
|
|
134
|
+
});
|
|
131
135
|
export function registerPendingDingtalkContactBind(req) {
|
|
132
|
-
|
|
133
|
-
const channelName = String(req.channelName || '').trim();
|
|
134
|
-
const primaryId = String(req.primaryId || '').trim();
|
|
135
|
-
const now = req.now ?? Date.now();
|
|
136
|
-
const code = req.code ? String(req.code).trim() : generateDingtalkBindCode();
|
|
137
|
-
if (!isValidAid(selfAid))
|
|
138
|
-
return { ok: false, error: `invalid selfAid: ${req.selfAid}` };
|
|
139
|
-
if (!channelName)
|
|
140
|
-
return { ok: false, error: 'missing channelName' };
|
|
141
|
-
if (!isValidAid(primaryId))
|
|
142
|
-
return { ok: false, error: `invalid primaryId: ${req.primaryId}` };
|
|
143
|
-
if (!/^\d{6}$/.test(code))
|
|
144
|
-
return { ok: false, error: 'binding code must be 6 digits' };
|
|
145
|
-
const key = pendingBindKey(selfAid, channelName);
|
|
146
|
-
const replaced = pendingContactBinds.has(key);
|
|
147
|
-
const item = {
|
|
148
|
-
selfAid,
|
|
149
|
-
channelName,
|
|
150
|
-
primaryId,
|
|
151
|
-
code,
|
|
152
|
-
createdAt: now,
|
|
153
|
-
expiresAt: now + DINGTALK_BIND_CODE_TTL_MS,
|
|
154
|
-
failedAttempts: 0,
|
|
155
|
-
maxFailedAttempts: DINGTALK_BIND_MAX_FAILED_ATTEMPTS,
|
|
156
|
-
};
|
|
157
|
-
pendingContactBinds.set(key, item);
|
|
158
|
-
return { ok: true, code, expiresAt: item.expiresAt, replaced };
|
|
136
|
+
return dingtalkContactBinds.register(req);
|
|
159
137
|
}
|
|
160
138
|
export function handlePendingDingtalkContactBindMessage(ctx) {
|
|
161
|
-
|
|
162
|
-
return { handled: false };
|
|
163
|
-
const selfAid = String(ctx.selfAid || '').trim();
|
|
164
|
-
const channelName = String(ctx.channelName || '').trim();
|
|
165
|
-
if (!selfAid || !channelName)
|
|
166
|
-
return { handled: false };
|
|
167
|
-
const key = pendingBindKey(selfAid, channelName);
|
|
168
|
-
const item = pendingContactBinds.get(key);
|
|
169
|
-
if (!item)
|
|
170
|
-
return { handled: false };
|
|
171
|
-
const now = ctx.now ?? Date.now();
|
|
172
|
-
if (now >= item.expiresAt) {
|
|
173
|
-
pendingContactBinds.delete(key);
|
|
174
|
-
return {
|
|
175
|
-
handled: true,
|
|
176
|
-
status: 'expired',
|
|
177
|
-
remainingAttempts: 0,
|
|
178
|
-
reply: '钉钉身份绑定码已超时,本次绑定失败。请重新执行 ec init dingtalk 并再次扫码绑定。',
|
|
179
|
-
};
|
|
180
|
-
}
|
|
181
|
-
if (ctx.chatType !== 'private')
|
|
182
|
-
return { handled: false };
|
|
183
|
-
const input = String(ctx.content ?? '').trim();
|
|
184
|
-
if (!/^\d+$/.test(input) || !/^\d{6}$/.test(input)) {
|
|
185
|
-
return {
|
|
186
|
-
handled: true,
|
|
187
|
-
status: 'format',
|
|
188
|
-
reply: '请直接发送 6 位数字绑定码。',
|
|
189
|
-
remainingAttempts: item.maxFailedAttempts - item.failedAttempts,
|
|
190
|
-
};
|
|
191
|
-
}
|
|
192
|
-
if (input !== item.code) {
|
|
193
|
-
item.failedAttempts += 1;
|
|
194
|
-
const remaining = Math.max(0, item.maxFailedAttempts - item.failedAttempts);
|
|
195
|
-
if (remaining === 0) {
|
|
196
|
-
pendingContactBinds.delete(key);
|
|
197
|
-
return {
|
|
198
|
-
handled: true,
|
|
199
|
-
status: 'failed',
|
|
200
|
-
remainingAttempts: 0,
|
|
201
|
-
reply: '绑定码无效,本次钉钉身份绑定失败。请重新执行 ec init dingtalk 并再次扫码绑定。',
|
|
202
|
-
};
|
|
203
|
-
}
|
|
204
|
-
return {
|
|
205
|
-
handled: true,
|
|
206
|
-
status: 'wrong-code',
|
|
207
|
-
remainingAttempts: remaining,
|
|
208
|
-
reply: '绑定码错误,请重新发送 6 位数字绑定码。',
|
|
209
|
-
};
|
|
210
|
-
}
|
|
211
|
-
const actorId = String(ctx.actorId || '').trim();
|
|
212
|
-
if (!actorId) {
|
|
213
|
-
return {
|
|
214
|
-
handled: true,
|
|
215
|
-
status: 'missing-actor',
|
|
216
|
-
remainingAttempts: item.maxFailedAttempts - item.failedAttempts,
|
|
217
|
-
reply: '无法识别当前钉钉发送者身份,未建立绑定。请重新发送绑定码或重新执行绑定流程。',
|
|
218
|
-
};
|
|
219
|
-
}
|
|
220
|
-
try {
|
|
221
|
-
bindContactAlias(item.selfAid, item.primaryId, 'dingtalk', actorId, item.channelName);
|
|
222
|
-
pendingContactBinds.delete(key);
|
|
223
|
-
return {
|
|
224
|
-
handled: true,
|
|
225
|
-
status: 'bound',
|
|
226
|
-
reply: `钉钉身份绑定成功:${item.channelName}:${encodeURIComponent(actorId)} -> ${item.primaryId}`,
|
|
227
|
-
};
|
|
228
|
-
}
|
|
229
|
-
catch (error) {
|
|
230
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
231
|
-
return {
|
|
232
|
-
handled: true,
|
|
233
|
-
status: 'write-failed',
|
|
234
|
-
remainingAttempts: item.maxFailedAttempts - item.failedAttempts,
|
|
235
|
-
reply: `钉钉身份绑定写入失败:${message}`,
|
|
236
|
-
};
|
|
237
|
-
}
|
|
139
|
+
return handleExplicitContactBindCommand(dingtalkContactBinds, ctx);
|
|
238
140
|
}
|
|
239
|
-
export function getPendingDingtalkContactBind(selfAid, channelName) {
|
|
240
|
-
|
|
241
|
-
return item ? { ...item } : null;
|
|
141
|
+
export function getPendingDingtalkContactBind(selfAid, channelName, primaryId) {
|
|
142
|
+
return dingtalkContactBinds.get(selfAid, channelName, primaryId);
|
|
242
143
|
}
|
|
243
144
|
export function clearPendingDingtalkContactBinds() {
|
|
244
|
-
|
|
245
|
-
}
|
|
246
|
-
function generateDingtalkBindCode() {
|
|
247
|
-
return String(randomInt(0, 1_000_000)).padStart(6, '0');
|
|
248
|
-
}
|
|
249
|
-
function pendingBindKey(selfAid, channelName) {
|
|
250
|
-
return `${String(selfAid || '').trim()}\u0000${String(channelName || '').trim()}`;
|
|
145
|
+
dingtalkContactBinds.clear();
|
|
251
146
|
}
|
|
252
147
|
// ── Webhook SSRF validation ────────────────────────────────────────────────────
|
|
253
148
|
const WEBHOOK_RE = /^https:\/\/(api|oapi)\.dingtalk\.com\//;
|
|
@@ -268,6 +163,9 @@ export class DingtalkChannel {
|
|
|
268
163
|
queuedReactions = new Map();
|
|
269
164
|
thinkingReactions = new Map();
|
|
270
165
|
taskReactionMessages = new Map();
|
|
166
|
+
// A resumed task has no inbound event in this process. Retain the persisted
|
|
167
|
+
// chat route used to create its replacement and terminal reactions.
|
|
168
|
+
reactionConversationIds = new Map();
|
|
271
169
|
seenMessages = new Map();
|
|
272
170
|
interactionCallback;
|
|
273
171
|
interactionInvalidationCallback;
|
|
@@ -813,16 +711,26 @@ export class DingtalkChannel {
|
|
|
813
711
|
await this.ensureReaction(messageId, DINGTALK_QUEUED_EMOTION, this.queuedReactions);
|
|
814
712
|
}
|
|
815
713
|
/** Upgrade Queued to Thinking without leaving a visible state gap. */
|
|
816
|
-
async promoteAckReaction(messageId, taskId, sourceMessageIds) {
|
|
714
|
+
async promoteAckReaction(messageId, taskId, sourceMessageIds, restartResume = false, channelId) {
|
|
817
715
|
const messageIds = [...new Set([...(sourceMessageIds ?? []), messageId].filter(Boolean))];
|
|
818
716
|
if (taskId)
|
|
819
717
|
this.taskReactionMessages.set(taskId, messageIds);
|
|
718
|
+
const fallbackConversationId = this.conversationIdFor(channelId);
|
|
719
|
+
if (fallbackConversationId) {
|
|
720
|
+
for (const id of messageIds)
|
|
721
|
+
this.reactionConversationIds.set(id, fallbackConversationId);
|
|
722
|
+
}
|
|
820
723
|
// Register every in-flight reaction before yielding. A very short task may
|
|
821
724
|
// otherwise finalize while later messages in a merged batch are still
|
|
822
725
|
// untracked, leaving a late BusinessTrip behind.
|
|
823
|
-
const thinking = messageIds.map(id => (this.ensureReaction(id, DINGTALK_THINKING_EMOTION, this.thinkingReactions)));
|
|
726
|
+
const thinking = messageIds.map(id => (this.ensureReaction(id, DINGTALK_THINKING_EMOTION, this.thinkingReactions, fallbackConversationId)));
|
|
824
727
|
await Promise.all(thinking);
|
|
825
|
-
|
|
728
|
+
if (restartResume) {
|
|
729
|
+
// Daemon shutdown finalized the old task as Wrong. DingTalk can recall a
|
|
730
|
+
// built-in reaction by name, so no process-local reaction ID is needed.
|
|
731
|
+
await Promise.all(messageIds.map(id => (this.sendEmotion(id, DINGTALK_WRONG_EMOTION, true, fallbackConversationId))));
|
|
732
|
+
}
|
|
733
|
+
await Promise.all(messageIds.map(id => (this.clearReaction(id, DINGTALK_QUEUED_EMOTION, this.queuedReactions, fallbackConversationId))));
|
|
826
734
|
}
|
|
827
735
|
/** Clear processing reactions and replace them with Done or Wrong. */
|
|
828
736
|
async completeAckReaction(taskId, completed) {
|
|
@@ -831,24 +739,31 @@ export class DingtalkChannel {
|
|
|
831
739
|
return;
|
|
832
740
|
this.taskReactionMessages.delete(taskId);
|
|
833
741
|
for (const messageId of messageIds) {
|
|
742
|
+
const fallbackConversationId = this.reactionConversationIds.get(messageId);
|
|
834
743
|
// Add the terminal state first so users never see an empty reaction gap.
|
|
835
|
-
let terminalAdded = await this.sendEmotion(messageId, completed ? DINGTALK_DONE_EMOTION : DINGTALK_WRONG_EMOTION, false);
|
|
744
|
+
let terminalAdded = await this.sendEmotion(messageId, completed ? DINGTALK_DONE_EMOTION : DINGTALK_WRONG_EMOTION, false, fallbackConversationId);
|
|
836
745
|
if (!terminalAdded) {
|
|
837
|
-
terminalAdded = await this.sendEmotion(messageId, completed ? DINGTALK_DONE_EMOTION : DINGTALK_WRONG_EMOTION, false);
|
|
746
|
+
terminalAdded = await this.sendEmotion(messageId, completed ? DINGTALK_DONE_EMOTION : DINGTALK_WRONG_EMOTION, false, fallbackConversationId);
|
|
838
747
|
}
|
|
839
748
|
if (!terminalAdded) {
|
|
840
749
|
logger.warn(`[DingTalk] Failed to add terminal reaction for ${messageId}; preserving processing reaction`);
|
|
841
750
|
continue;
|
|
842
751
|
}
|
|
843
|
-
await this.clearReaction(messageId, DINGTALK_QUEUED_EMOTION, this.queuedReactions);
|
|
844
|
-
await this.clearReaction(messageId, DINGTALK_THINKING_EMOTION, this.thinkingReactions);
|
|
752
|
+
await this.clearReaction(messageId, DINGTALK_QUEUED_EMOTION, this.queuedReactions, fallbackConversationId);
|
|
753
|
+
await this.clearReaction(messageId, DINGTALK_THINKING_EMOTION, this.thinkingReactions, fallbackConversationId);
|
|
754
|
+
this.reactionConversationIds.delete(messageId);
|
|
845
755
|
}
|
|
846
756
|
}
|
|
847
|
-
|
|
757
|
+
conversationIdFor(channelId) {
|
|
758
|
+
if (!channelId)
|
|
759
|
+
return undefined;
|
|
760
|
+
return this.routes.get(channelId)?.conversationId ?? this.conversationIdCache.get(channelId);
|
|
761
|
+
}
|
|
762
|
+
async ensureReaction(messageId, emotionName, reactions, fallbackConversationId) {
|
|
848
763
|
const existing = reactions.get(messageId);
|
|
849
764
|
if (existing)
|
|
850
765
|
return existing;
|
|
851
|
-
const pending = this.sendEmotion(messageId, emotionName, false)
|
|
766
|
+
const pending = this.sendEmotion(messageId, emotionName, false, fallbackConversationId)
|
|
852
767
|
.then((ok) => {
|
|
853
768
|
if (!ok)
|
|
854
769
|
reactions.delete(messageId);
|
|
@@ -861,18 +776,18 @@ export class DingtalkChannel {
|
|
|
861
776
|
reactions.set(messageId, pending);
|
|
862
777
|
return pending;
|
|
863
778
|
}
|
|
864
|
-
async clearReaction(messageId, emotionName, reactions) {
|
|
779
|
+
async clearReaction(messageId, emotionName, reactions, fallbackConversationId) {
|
|
865
780
|
const pending = reactions.get(messageId);
|
|
866
781
|
if (!pending)
|
|
867
782
|
return;
|
|
868
783
|
const added = await pending.catch(() => false);
|
|
869
784
|
if (added)
|
|
870
|
-
await this.sendEmotion(messageId, emotionName, true);
|
|
785
|
+
await this.sendEmotion(messageId, emotionName, true, fallbackConversationId);
|
|
871
786
|
reactions.delete(messageId);
|
|
872
787
|
}
|
|
873
|
-
async sendEmotion(messageId, emotionName, recall) {
|
|
874
|
-
const
|
|
875
|
-
if (!
|
|
788
|
+
async sendEmotion(messageId, emotionName, recall, fallbackConversationId) {
|
|
789
|
+
const openConversationId = this.messageContextCache.get(messageId)?.openConversationId ?? fallbackConversationId;
|
|
790
|
+
if (!openConversationId) {
|
|
876
791
|
logger.debug(`[DingTalk] Cannot ${recall ? 'recall' : 'add'} emotion: no context for message ${messageId}`);
|
|
877
792
|
return false;
|
|
878
793
|
}
|
|
@@ -891,7 +806,7 @@ export class DingtalkChannel {
|
|
|
891
806
|
body: JSON.stringify({
|
|
892
807
|
robotCode: this.config.clientId,
|
|
893
808
|
openMsgId: messageId,
|
|
894
|
-
openConversationId
|
|
809
|
+
openConversationId,
|
|
895
810
|
// Pin / BusinessTrip / Done / Wrong are DingTalk built-in
|
|
896
811
|
// reactions. emotionType=2 is for custom text reactions and
|
|
897
812
|
// causes BusinessTrip to fail with system.error.
|
|
@@ -1438,7 +1353,7 @@ export class DingtalkChannelPlugin {
|
|
|
1438
1353
|
}
|
|
1439
1354
|
},
|
|
1440
1355
|
acknowledge: (messageId) => channel.acknowledgeMessage(messageId),
|
|
1441
|
-
promoteAck: (messageId, context) => (channel.promoteAckReaction(messageId, context?.taskId, context?.messageIds)),
|
|
1356
|
+
promoteAck: (messageId, context) => (channel.promoteAckReaction(messageId, context?.taskId, context?.messageIds, context?.restartResume, context?.channelId)),
|
|
1442
1357
|
onInteraction: (callback) => channel.onInteraction(callback),
|
|
1443
1358
|
onInteractionInvalidated: (callback) => channel.onInteractionInvalidated(callback),
|
|
1444
1359
|
invalidateInteraction: (interactionId, reason) => channel.invalidateInteraction(interactionId, reason),
|
|
@@ -1458,7 +1373,7 @@ export class DingtalkChannelPlugin {
|
|
|
1458
1373
|
return {
|
|
1459
1374
|
channelType: 'dingtalk', adapter, channel,
|
|
1460
1375
|
policy,
|
|
1461
|
-
options: { fileMarkerPattern:
|
|
1376
|
+
options: { fileMarkerPattern: createSendFileMarkerPattern(), supportsImages: true, flushDelay: inst.flushDelay },
|
|
1462
1377
|
connect: () => channel.connect(),
|
|
1463
1378
|
disconnect: () => channel.disconnect(),
|
|
1464
1379
|
onProjectPathRequest: () => Promise.resolve(ctx.defaultProjectPath),
|
package/dist/channels/feishu.js
CHANGED
|
@@ -4,9 +4,28 @@ import imageType from 'image-type';
|
|
|
4
4
|
import { sanitizeFileName, saveToUploads, bufferToInboundImage } from '../utils/media-cache.js';
|
|
5
5
|
import { logger } from '../utils/logger.js';
|
|
6
6
|
import { formatItemsAsText } from '../core/message/items-formatter.js';
|
|
7
|
+
import { createSendFileMarkerPattern } from '../core/message/file-markers.js';
|
|
7
8
|
import { initWelcomeManager, sendWelcomeIfNeeded } from '../utils/welcome.js';
|
|
8
9
|
import { chatDirPath, decodeDirSegment } from '../core/session/session-fs-store.js';
|
|
9
10
|
import { sentReceipt, suppressedReceipt } from '../core/message/send-receipt.js';
|
|
11
|
+
import { ContactBindCodeRegistry, handleExplicitContactBindCommand, } from '../config/contact-bind-code.js';
|
|
12
|
+
const feishuContactBinds = new ContactBindCodeRegistry({
|
|
13
|
+
channelType: 'feishu',
|
|
14
|
+
displayName: '飞书',
|
|
15
|
+
initCommand: 'ec init feishu',
|
|
16
|
+
});
|
|
17
|
+
export function registerPendingFeishuContactBind(req) {
|
|
18
|
+
return feishuContactBinds.register(req);
|
|
19
|
+
}
|
|
20
|
+
export function handlePendingFeishuContactBindMessage(ctx) {
|
|
21
|
+
return handleExplicitContactBindCommand(feishuContactBinds, ctx);
|
|
22
|
+
}
|
|
23
|
+
export function getPendingFeishuContactBind(selfAid, channelName, primaryId) {
|
|
24
|
+
return feishuContactBinds.get(selfAid, channelName, primaryId);
|
|
25
|
+
}
|
|
26
|
+
export function clearPendingFeishuContactBinds() {
|
|
27
|
+
feishuContactBinds.clear();
|
|
28
|
+
}
|
|
10
29
|
export class FeishuChannel {
|
|
11
30
|
config;
|
|
12
31
|
agentAid;
|
|
@@ -1307,7 +1326,7 @@ export class FeishuChannel {
|
|
|
1307
1326
|
this.pinReactions.set(messageId, p);
|
|
1308
1327
|
}
|
|
1309
1328
|
/** Runner 开始执行时:添加 StatusInFlight,然后移除 Pin(视觉无闪烁) */
|
|
1310
|
-
async promoteAckReaction(messageId, taskId, sourceMessageIds) {
|
|
1329
|
+
async promoteAckReaction(messageId, taskId, sourceMessageIds, restartResume = false) {
|
|
1311
1330
|
const messageIds = [...new Set([...(sourceMessageIds ?? []), messageId].filter(Boolean))];
|
|
1312
1331
|
if (taskId)
|
|
1313
1332
|
this.taskAckMessages.set(taskId, messageIds);
|
|
@@ -1328,6 +1347,8 @@ export class FeishuChannel {
|
|
|
1328
1347
|
// Every in-flight reaction is registered before this first await, so a
|
|
1329
1348
|
// fast terminal event cannot race ahead and strand a merged-batch status.
|
|
1330
1349
|
for (const { id, pending } of promotions) {
|
|
1350
|
+
if (restartResume)
|
|
1351
|
+
await this.clearRestartCrossMark(id);
|
|
1331
1352
|
const reactionId = await pending;
|
|
1332
1353
|
if (reactionId) {
|
|
1333
1354
|
await this.client.im.messageReaction.delete({
|
|
@@ -1336,6 +1357,31 @@ export class FeishuChannel {
|
|
|
1336
1357
|
}
|
|
1337
1358
|
}
|
|
1338
1359
|
}
|
|
1360
|
+
/**
|
|
1361
|
+
* A daemon restart terminates the old task with CrossMark. The reaction IDs
|
|
1362
|
+
* are process-local, so a resumed process must discover and remove its own
|
|
1363
|
+
* stale terminal reaction before it can show the new task state.
|
|
1364
|
+
*/
|
|
1365
|
+
async clearRestartCrossMark(messageId) {
|
|
1366
|
+
const reactions = this.client?.im?.messageReaction;
|
|
1367
|
+
if (!reactions?.list || !reactions?.delete)
|
|
1368
|
+
return;
|
|
1369
|
+
try {
|
|
1370
|
+
const response = await reactions.list({
|
|
1371
|
+
path: { message_id: messageId },
|
|
1372
|
+
params: { reaction_type: 'CrossMark', page_size: 50 },
|
|
1373
|
+
});
|
|
1374
|
+
const staleReactionIds = (response?.data?.items ?? [])
|
|
1375
|
+
.filter((reaction) => (reaction?.reaction_type?.emoji_type === 'CrossMark'
|
|
1376
|
+
&& reaction?.operator?.operator_type === 'app'
|
|
1377
|
+
&& typeof reaction?.reaction_id === 'string'))
|
|
1378
|
+
.map((reaction) => reaction.reaction_id);
|
|
1379
|
+
await Promise.all(staleReactionIds.map(reactionId => (reactions.delete({ path: { message_id: messageId, reaction_id: reactionId } }).catch(() => { }))));
|
|
1380
|
+
}
|
|
1381
|
+
catch {
|
|
1382
|
+
// A missing reaction-list permission must not block the resumed task.
|
|
1383
|
+
}
|
|
1384
|
+
}
|
|
1339
1385
|
/** 任务终态:用 CheckMark/CrossMark 替换 StatusInFlight。 */
|
|
1340
1386
|
async finalizeAckReaction(taskId, terminalEmoji) {
|
|
1341
1387
|
const messageIds = this.taskAckMessages.get(taskId);
|
|
@@ -1820,7 +1866,7 @@ export class FeishuChannelPlugin {
|
|
|
1820
1866
|
}
|
|
1821
1867
|
},
|
|
1822
1868
|
acknowledge: (messageId) => { channel.addPinReaction(messageId); return Promise.resolve(); },
|
|
1823
|
-
promoteAck: (messageId, context) => (channel.promoteAckReaction(messageId, context?.taskId, context?.messageIds)),
|
|
1869
|
+
promoteAck: (messageId, context) => (channel.promoteAckReaction(messageId, context?.taskId, context?.messageIds, context?.restartResume)),
|
|
1824
1870
|
onInteraction: (callback) => channel.onInteraction(callback),
|
|
1825
1871
|
onInteractionInvalidated: (callback) => channel.onInteractionInvalidated(callback),
|
|
1826
1872
|
invalidateInteraction: (interactionId, reason) => channel.invalidateInteraction(interactionId, reason),
|
|
@@ -1840,17 +1886,23 @@ export class FeishuChannelPlugin {
|
|
|
1840
1886
|
return {
|
|
1841
1887
|
channelType: 'feishu', adapter, channel,
|
|
1842
1888
|
policy,
|
|
1843
|
-
options: { fileMarkerPattern:
|
|
1889
|
+
options: { fileMarkerPattern: createSendFileMarkerPattern(), supportsImages: true, flushDelay: inst.flushDelay },
|
|
1844
1890
|
connect: () => channel.connect(),
|
|
1845
1891
|
disconnect: () => channel.disconnect(),
|
|
1846
1892
|
onProjectPathRequest: () => Promise.resolve(ctx.defaultProjectPath),
|
|
1847
1893
|
registerBridge(bridge, channelType) {
|
|
1848
1894
|
bridge.register(adapter.channelName, (handler) => channel.onMessage(async ({ channelId: chatId, content, images, peerId, peerName, messageId, mentions, mentionAids, threadId, rootId, chatType, source }) => {
|
|
1895
|
+
const replyContext = peerId || threadId
|
|
1896
|
+
? {
|
|
1897
|
+
...(peerId ? { peerId } : {}),
|
|
1898
|
+
...(threadId ? { threadId, replyToMessageId: rootId ?? threadId, replyInThread: true } : {}),
|
|
1899
|
+
}
|
|
1900
|
+
: undefined;
|
|
1849
1901
|
await handler({
|
|
1850
1902
|
channel: adapter.channelName, channelType, channelId: chatId, content, images,
|
|
1851
1903
|
selfAID: ctx.agentName, chatType: chatType || 'private',
|
|
1852
1904
|
peerId: peerId || '', peerName, messageId, mentions, mentionAids, threadId,
|
|
1853
|
-
replyContext
|
|
1905
|
+
replyContext,
|
|
1854
1906
|
source,
|
|
1855
1907
|
});
|
|
1856
1908
|
}), async (channelId, text, replyContext) => {
|
package/dist/channels/qqbot.js
CHANGED
|
@@ -3,7 +3,26 @@ import { markdownToPlainText } from '../utils/markdown-to-plain-text.js';
|
|
|
3
3
|
import { requireOptional } from '../utils/npm-ops.js';
|
|
4
4
|
import { middleOutputModePolicy, resolveShowActivities, showActivitiesPolicy } from '../core/channel-loader.js';
|
|
5
5
|
import { formatItemsAsText } from '../core/message/items-formatter.js';
|
|
6
|
+
import { createSendFileMarkerPattern } from '../core/message/file-markers.js';
|
|
6
7
|
import { initWelcomeManager, sendWelcomeIfNeeded } from '../utils/welcome.js';
|
|
8
|
+
import { ContactBindCodeRegistry, handleExplicitContactBindCommand, } from '../config/contact-bind-code.js';
|
|
9
|
+
const qqbotContactBinds = new ContactBindCodeRegistry({
|
|
10
|
+
channelType: 'qqbot',
|
|
11
|
+
displayName: 'QQ',
|
|
12
|
+
initCommand: 'ec init qqbot',
|
|
13
|
+
});
|
|
14
|
+
export function registerPendingQQBotContactBind(req) {
|
|
15
|
+
return qqbotContactBinds.register(req);
|
|
16
|
+
}
|
|
17
|
+
export function handlePendingQQBotContactBindMessage(ctx) {
|
|
18
|
+
return handleExplicitContactBindCommand(qqbotContactBinds, ctx);
|
|
19
|
+
}
|
|
20
|
+
export function getPendingQQBotContactBind(selfAid, channelName, primaryId) {
|
|
21
|
+
return qqbotContactBinds.get(selfAid, channelName, primaryId);
|
|
22
|
+
}
|
|
23
|
+
export function clearPendingQQBotContactBinds() {
|
|
24
|
+
qqbotContactBinds.clear();
|
|
25
|
+
}
|
|
7
26
|
// ── QQBotChannel ────────────────────────────────────────────────────────────
|
|
8
27
|
export class QQBotChannel {
|
|
9
28
|
agentAid;
|
|
@@ -93,6 +112,9 @@ export class QQBotChannel {
|
|
|
93
112
|
}
|
|
94
113
|
logger.info('[QQBot] Disconnected');
|
|
95
114
|
}
|
|
115
|
+
getStatus() {
|
|
116
|
+
return { connected: this.connected };
|
|
117
|
+
}
|
|
96
118
|
onMessage(handler) {
|
|
97
119
|
this.messageHandler = handler;
|
|
98
120
|
}
|
|
@@ -390,7 +412,7 @@ export class QQBotChannelPlugin {
|
|
|
390
412
|
return {
|
|
391
413
|
channelType: 'qqbot', adapter, channel,
|
|
392
414
|
policy,
|
|
393
|
-
options: { fileMarkerPattern:
|
|
415
|
+
options: { fileMarkerPattern: createSendFileMarkerPattern(), supportsImages: true, flushDelay: inst.flushDelay },
|
|
394
416
|
connect: () => channel.connect(),
|
|
395
417
|
disconnect: () => channel.disconnect(),
|
|
396
418
|
onProjectPathRequest: () => Promise.resolve(ctx.defaultProjectPath),
|