evolcore 0.0.29 → 0.0.30

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.
Files changed (68) hide show
  1. package/CHANGELOG.md +7 -0
  2. package/dist/agents/codex-runner.js +61 -14
  3. package/dist/aun/aid/agentmd.js +224 -19
  4. package/dist/aun/aid/index.js +2 -1
  5. package/dist/aun/aid/managed-operation.js +70 -4
  6. package/dist/aun/group/index.js +1 -0
  7. package/dist/{core → aun}/group/invite-service.js +4 -4
  8. package/dist/channels/daemon.js +1 -1
  9. package/dist/cli/agent-command.js +7 -7
  10. package/dist/cli/aun-commands.js +97 -37
  11. package/dist/cli/data-command.js +2 -1
  12. package/dist/cli/managed-command-guard.js +2 -2
  13. package/dist/cli/model.js +1 -1
  14. package/dist/cli/response.js +1 -1
  15. package/dist/cli/trigger-command.js +52 -29
  16. package/dist/config/boot-log.js +95 -1
  17. package/dist/config/builtin-roles.js +1 -0
  18. package/dist/config/peer-role-resolver.js +21 -0
  19. package/dist/core/agent-ipc-registration.js +409 -0
  20. package/dist/core/agent-runtime.js +435 -0
  21. package/dist/core/auth/delegation-authorization.js +58 -0
  22. package/dist/core/auth/operation-catalog.js +8 -0
  23. package/dist/core/bootstrap-service.js +65 -1
  24. package/dist/core/channel-runtime.js +435 -0
  25. package/dist/core/command/command-handler.js +4 -1
  26. package/dist/core/daemon-bootstrap.js +151 -0
  27. package/dist/core/daemon-control.js +205 -0
  28. package/dist/core/daemon-lifecycle.js +225 -0
  29. package/dist/core/data-migration.js +4 -1
  30. package/dist/core/handoff/ipc-registration.js +156 -0
  31. package/dist/core/ipc-registration.js +457 -0
  32. package/dist/core/memory/consolidation-trigger.js +8 -0
  33. package/dist/core/memory/consolidation.js +838 -0
  34. package/dist/core/message/aun-ipc-sender.js +226 -0
  35. package/dist/core/message/outbound.js +165 -0
  36. package/dist/core/permission/approval-gateway.js +13 -0
  37. package/dist/core/permission/ec-command-parser.js +80 -35
  38. package/dist/core/permission/sandbox-runtime.js +23 -2
  39. package/dist/core/permission/tool-error-code.js +1 -0
  40. package/dist/core/permission/tool-policy.js +61 -24
  41. package/dist/core/session/session-idle-monitor.js +17 -0
  42. package/dist/index.js +352 -4841
  43. package/dist/ipc.js +2 -1
  44. package/dist/stats/db.js +24 -0
  45. package/dist/stats/index.js +1 -1
  46. package/dist/stats/writer.js +25 -1
  47. package/dist/trigger/application-service.js +157 -0
  48. package/dist/trigger/authorization.js +371 -0
  49. package/dist/trigger/ipc-service.js +406 -0
  50. package/dist/trigger/local-files.js +133 -0
  51. package/dist/trigger/manager.js +235 -22
  52. package/dist/trigger/scheduler.js +30 -5
  53. package/dist/trigger/script-executor.js +176 -39
  54. package/dist/trigger/script-sandbox.js +458 -0
  55. package/dist/trigger/system-definitions.js +188 -0
  56. package/dist/trigger/validation.js +0 -3
  57. package/dist/utils/aid-bind.js +1 -1
  58. package/dist/utils/evolcore-version.js +74 -0
  59. package/dist/utils/logger.js +16 -0
  60. package/kits/docs/evolcore/INDEX.md +1 -1
  61. package/kits/docs/evolcore/aid.md +9 -1
  62. package/kits/docs/evolcore/model.md +3 -2
  63. package/kits/docs/evolcore/rpc.md +4 -0
  64. package/kits/docs/evolcore/trigger.md +15 -1
  65. package/kits/schemas/relation-config.schema.9.json +1 -1
  66. package/kits/templates/system-fragments/commands.md +0 -1
  67. package/package.json +1 -1
  68. package/dist/core/memory/launch-consolidation.js +0 -395
@@ -0,0 +1,435 @@
1
+ import path from 'path';
2
+ import fs from 'fs';
3
+ import { tryParseChannelKey } from './channel-loader.js';
4
+ import { agentMdPath } from '../paths.js';
5
+ import { registerAdapterInteractions } from './interaction-router.js';
6
+ import { resolvePeerDisplayLabel } from '../config/contact-book.js';
7
+ import { sanitizeMenuLogValue } from './command/menu-protocol.js';
8
+ import { appendMessageLog, buildOutboundEntry } from './message/message-log.js';
9
+ import crypto from 'crypto';
10
+ import { buildEnvelope } from './message/message-utils.js';
11
+ import { normalizeMessageSource, outboundPayloadToLogText, resolvePersistedRestartDelivery, sendSystemPayload, shouldCountSentPayload, summarizeOutboundPayload, tagErrorPayloadWithEvolcoreVersion, } from './message/outbound.js';
12
+ import { getFirstStaticAgentOwner } from '../config/peer-role-resolver.js';
13
+ import { isManagementRole } from '../config/builtin-roles.js';
14
+ import { pendingInterruptHints } from './message/pending-hints.js';
15
+ import { logger } from '../utils/logger.js';
16
+ export function createAgentStorageClientResolver(channelInstances) {
17
+ return (channelKey, targetAid) => {
18
+ const instance = channelInstances.find(candidate => candidate.channelType === 'aun'
19
+ && (candidate.adapter.channelKey === channelKey || candidate.adapter.channelName === channelKey)
20
+ && candidate.channel.getAid?.() === targetAid);
21
+ if (!instance || instance.channel.getStatus?.().connected !== true)
22
+ return undefined;
23
+ const client = instance.channel.getClient?.();
24
+ return client?.aid === targetAid ? client : undefined;
25
+ };
26
+ }
27
+ export function bindStartupChannelsToAgents(channelInstances, agentRegistry, agentInstances) {
28
+ for (const instance of channelInstances) {
29
+ const agent = agentRegistry.resolveByChannel(instance.adapter.channelKey);
30
+ if (!agent || agent.status === 'error')
31
+ continue;
32
+ agent.channels.set(instance.adapter.channelKey, instance.adapter);
33
+ const hasRunner = agentInstances.some(runner => runner.evolagentName === agent.aid);
34
+ if (agent.status === 'stopped' && hasRunner)
35
+ agent.status = 'running';
36
+ }
37
+ }
38
+ export function createDefaultChannelPolicy() {
39
+ return {
40
+ canSwitchProject: (chatType, role) => chatType === 'private' ? isManagementRole(role) : role === 'owner',
41
+ canListProjects: (chatType, role) => chatType === 'private' ? isManagementRole(role) : role === 'owner',
42
+ canCreateSession: () => true,
43
+ canDeleteSession: (chatType, role) => chatType === 'private' ? isManagementRole(role) : role === 'owner',
44
+ canImportCliSession: (chatType, role) => chatType === 'private' ? isManagementRole(role) : role === 'owner',
45
+ messagePrefix: () => '',
46
+ showMiddleResult: () => true,
47
+ showIdleMonitor: () => true,
48
+ accumulateErrors: () => true,
49
+ };
50
+ }
51
+ export function preloadChannelThreads(channelInstances, sessionManager) {
52
+ for (const instance of channelInstances) {
53
+ const channelType = instance.channelType || instance.adapter.channelName;
54
+ if (channelType === 'feishu' && 'preloadThreads' in instance.channel) {
55
+ instance.channel.preloadThreads(sessionManager.getKnownThreadIds(instance.adapter.channelKey));
56
+ }
57
+ }
58
+ }
59
+ export function createChannelConnectionRuntime(dependencies) {
60
+ const { channelInstances, connectedChannels } = dependencies;
61
+ const queuePendingObserverHints = (aid, channelKey) => (pendingInterruptHints(dependencies.sessionsDir, aid)
62
+ .filter(pending => pending.channelKey === channelKey)
63
+ .map(pending => dependencies.responseEngine.scheduleObserverHint({ ...pending, selfAid: aid })
64
+ .then(result => {
65
+ if (result === 'SESSION_NOT_FOUND')
66
+ logger.warn(`[ObserverHint] recovery target missing hint=${pending.hintId}`);
67
+ })
68
+ .catch(error => logger.warn(`[ObserverHint] recovery failed hint=${pending.hintId}: ${error}`))));
69
+ const markChannelConnected = async (instance) => {
70
+ const name = instance.adapter.channelName;
71
+ const key = instance.adapter.channelKey;
72
+ if (connectedChannels.has(key))
73
+ return;
74
+ connectedChannels.add(name);
75
+ connectedChannels.add(key);
76
+ const type = instance.channelType || name;
77
+ dependencies.eventBus.publish({ type: 'channel:connected', channel: type.toLowerCase(), channelName: name, timestamp: Date.now() });
78
+ const agent = dependencies.resolveOwningAgent(key, name);
79
+ if (instance.channelType === 'aun' && agent) {
80
+ const recoveries = queuePendingObserverHints(agent.aid, key);
81
+ if (dependencies.isRestoringStartupQueue())
82
+ dependencies.startupObserverRecoveries.push(...recoveries);
83
+ }
84
+ await Promise.all([
85
+ dependencies.bootstrapService.tryStartBootstrap({
86
+ adapter: instance.adapter,
87
+ channelKey: key,
88
+ channelType: instance.channelType || type,
89
+ source: 'connected',
90
+ }),
91
+ agent ? dependencies.ensureTriggerSchedulerStarted(agent) : Promise.resolve(),
92
+ Promise.resolve().then(() => dependencies.sendOnlineNoticeForChannel(instance)),
93
+ dependencies.trySendPendingRestartNotice(),
94
+ ]);
95
+ };
96
+ const markChannelDisconnected = (channelName) => {
97
+ connectedChannels.delete(channelName);
98
+ const instance = channelInstances.find(candidate => candidate.adapter.channelName === channelName);
99
+ if (instance)
100
+ connectedChannels.delete(instance.adapter.channelKey);
101
+ };
102
+ const unregisterRuntimeChannel = (channelName) => {
103
+ markChannelDisconnected(channelName);
104
+ dependencies.processor.unregisterChannel(channelName);
105
+ dependencies.commandHandler.unregisterChannel(channelName);
106
+ dependencies.messageBridge.removeChannel(channelName);
107
+ };
108
+ return { markChannelConnected, markChannelDisconnected, unregisterRuntimeChannel, queuePendingObserverHints };
109
+ }
110
+ export function registerChannelInstance(inst, dependencies) {
111
+ const { defaultProjectPath, defaultPolicy, processor, commandHandler, interactionRouter, messageBridge, eventBus, sessionManager, responseEngine, resolveOwningAgent, } = dependencies;
112
+ wrapChannelSend(inst, {
113
+ eventBus,
114
+ sessionManager,
115
+ aidStatsCollector: dependencies.aidStatsCollector,
116
+ resolveOwningAgent: () => resolveOwningAgent(inst.adapter.channelKey, inst.adapter.channelName),
117
+ });
118
+ if (inst.onProjectPathRequest && inst.channel.onProjectPathRequest) {
119
+ inst.channel.onProjectPathRequest(async (channelId) => {
120
+ const owningAgent = resolveOwningAgent(inst.adapter.channelKey, inst.adapter.channelName);
121
+ const effectiveDefault = owningAgent?.projectPath ?? defaultProjectPath;
122
+ const parsedKey = tryParseChannelKey(inst.adapter.channelKey);
123
+ const session = await sessionManager.getOrCreateSession(inst.adapter.channelKey, channelId, effectiveDefault, undefined, undefined, undefined, undefined, undefined, owningAgent?.baseagent, parsedKey?.selfAID, parsedKey?.type);
124
+ return path.isAbsolute(session.projectPath)
125
+ ? session.projectPath
126
+ : path.resolve(process.cwd(), session.projectPath);
127
+ });
128
+ }
129
+ const options = inst.channelType
130
+ ? { ...inst.options, channelType: inst.channelType }
131
+ : inst.options;
132
+ processor.registerChannel(inst.adapter, inst.policy || defaultPolicy, options);
133
+ commandHandler.registerAdapter(inst.adapter);
134
+ commandHandler.registerChannel(inst.adapter.channelName, inst.channel, inst.channelType);
135
+ if (inst.policy)
136
+ commandHandler.registerPolicy(inst.adapter.channelName, inst.policy);
137
+ registerAdapterInteractions(inst.adapter, interactionRouter);
138
+ const channelType = inst.channelType || inst.adapter.channelName;
139
+ inst.registerBridge?.(messageBridge, channelType);
140
+ inst.registerHooks?.({ eventBus, sessionManager });
141
+ const channelForObserver = inst.channel;
142
+ if (typeof channelForObserver.setObserverConfigResolver === 'function') {
143
+ const channelKey = inst.adapter.channelKey;
144
+ channelForObserver.setObserverConfigResolver(() => {
145
+ const owningAgent = resolveOwningAgent(channelKey, inst.adapter.channelName);
146
+ return {
147
+ observable: owningAgent?.getObservable() ?? false,
148
+ owners: owningAgent ? Array.from(new Set(owningAgent.config.owners ?? [])) : [],
149
+ };
150
+ });
151
+ }
152
+ if (inst.channelType === 'aun') {
153
+ channelForObserver.setObserverHintRuntime({
154
+ hasSession: target => responseEngine.hasObserverTargetSession(target),
155
+ schedule: target => responseEngine.scheduleObserverHint(target),
156
+ });
157
+ }
158
+ inst.channel.onRecall?.((messageId, selfAID) => {
159
+ messageBridge.cancel(messageId, selfAID);
160
+ });
161
+ }
162
+ export function createChannelStartupNotifications({ channelInstances, agentRegistry, commandHandler, connectedChannels, pendingFile, upmsg, }) {
163
+ const onlineNoticeSent = new Set();
164
+ let pendingRestartNoticeInFlight = null;
165
+ let pendingRestartNoticeSent = false;
166
+ const sendOnlineNoticeForChannel = (inst) => {
167
+ const name = inst.adapter.channelName;
168
+ const agent = agentRegistry.resolveByChannel(inst.adapter.channelKey) ?? agentRegistry.resolveByChannel(name);
169
+ if (!agent || !upmsg)
170
+ return;
171
+ const ownerAid = getFirstStaticAgentOwner(agent.aid);
172
+ if (!ownerAid)
173
+ return;
174
+ const noticeKey = `${agent.aid}#${name}`;
175
+ if (onlineNoticeSent.has(noticeKey))
176
+ return;
177
+ onlineNoticeSent.add(noticeKey);
178
+ setTimeout(() => {
179
+ const adapter = agent.channels.get(inst.adapter.channelKey) ?? agent.channels.get(name);
180
+ if (!adapter)
181
+ return;
182
+ let agentName = agent.aid;
183
+ try {
184
+ const content = fs.readFileSync(agentMdPath(agent.aid), 'utf-8');
185
+ const nameMatch = content.match(/^name:\s*"?([^"\n]+)/m);
186
+ if (nameMatch)
187
+ agentName = nameMatch[1].trim().replace(/"$/, '');
188
+ }
189
+ catch { }
190
+ const projectDir = path.basename(agent.projectPath);
191
+ const text = `✓ ${agentName} 已上线 | 工作目录: ${projectDir}`;
192
+ const envelope = buildEnvelope({
193
+ taskId: `system-online-${crypto.randomBytes(5).toString('hex')}`,
194
+ channel: adapter.channelName,
195
+ channelId: ownerAid,
196
+ agentName,
197
+ delivery: { chatType: 'private' },
198
+ });
199
+ sendSystemPayload(adapter, envelope, {
200
+ kind: 'system.notice',
201
+ text,
202
+ subtype: 'restarted',
203
+ }).catch(() => { });
204
+ }, 1000 + Math.random() * 2000);
205
+ };
206
+ const trySendPendingRestartNotice = async () => {
207
+ if (pendingRestartNoticeSent)
208
+ return;
209
+ if (pendingRestartNoticeInFlight) {
210
+ try {
211
+ await pendingRestartNoticeInFlight;
212
+ }
213
+ catch { }
214
+ return;
215
+ }
216
+ if (!fs.existsSync(pendingFile))
217
+ return;
218
+ pendingRestartNoticeInFlight = (async () => {
219
+ if (pendingRestartNoticeSent || !fs.existsSync(pendingFile))
220
+ return;
221
+ const pending = JSON.parse(fs.readFileSync(pendingFile, 'utf-8'));
222
+ const adapter = commandHandler.getAdapter(pending.channel)
223
+ ?? channelInstances.find(inst => inst.adapter.channelKey === pending.channel)?.adapter;
224
+ if (!adapter)
225
+ return;
226
+ if (!connectedChannels.has(pending.channel)
227
+ && !connectedChannels.has(adapter.channelName)
228
+ && !connectedChannels.has(adapter.channelKey)) {
229
+ logger.info(`[Restart] Pending notification waits for channel connection: ${pending.channel}`);
230
+ return;
231
+ }
232
+ const restartDelivery = resolvePersistedRestartDelivery({
233
+ channel: pending.channel,
234
+ adapterChannel: adapter.channelName,
235
+ channelId: pending.channelId,
236
+ delivery: pending.delivery,
237
+ });
238
+ if (!restartDelivery.valid) {
239
+ logger.warn(`[Restart] Discarding pending notification without a valid persisted delivery route: channel=${pending.channel} channelId=${pending.channelId}`);
240
+ fs.rmSync(pendingFile, { force: true });
241
+ return;
242
+ }
243
+ const replyContext = {
244
+ ...(restartDelivery.delivery ? { delivery: restartDelivery.delivery } : {}),
245
+ ...(pending.threadId ? { threadId: pending.threadId } : {}),
246
+ ...(pending.rootId ? { replyToMessageId: pending.rootId, replyInThread: !!pending.threadId } : {}),
247
+ };
248
+ const owningAgent = agentRegistry.resolveByChannel(adapter.channelKey);
249
+ const envelope = buildEnvelope({
250
+ taskId: `system-restart-${process.pid}`,
251
+ channel: adapter.channelKey,
252
+ channelId: pending.channelId,
253
+ agentName: owningAgent?.aid || 'evolcore',
254
+ ...(restartDelivery.delivery ? { delivery: restartDelivery.delivery } : {}),
255
+ replyContext: Object.keys(replyContext).length > 0 ? replyContext : undefined,
256
+ });
257
+ const restartNotice = pending.outcome === 'rolled-back'
258
+ ? {
259
+ text: `⚠️ 服务已重启,但运行的是上一个版本。${pending.outcomeReason ? `原因:${pending.outcomeReason}。` : ''}本次改动未生效,请检查后重试。`,
260
+ subtype: 'restarted-rolled-back',
261
+ }
262
+ : pending.outcome === 'upgraded'
263
+ ? {
264
+ text: `✅ 服务重启成功!${pending.outcomeReason ? `${pending.outcomeReason}` : ''}`,
265
+ subtype: 'restarted-upgraded',
266
+ }
267
+ : { text: '✅ 服务重启成功!', subtype: 'restarted' };
268
+ await sendSystemPayload(adapter, envelope, {
269
+ kind: 'system.notice',
270
+ text: restartNotice.text,
271
+ subtype: restartNotice.subtype,
272
+ });
273
+ pendingRestartNoticeSent = true;
274
+ fs.rmSync(pendingFile, { force: true });
275
+ logger.info(`[Restart] Notification sent via ${pending.channel}`);
276
+ })();
277
+ try {
278
+ await pendingRestartNoticeInFlight;
279
+ }
280
+ catch (error) {
281
+ logger.error('[Restart] Failed to send restart notification:', error);
282
+ }
283
+ finally {
284
+ pendingRestartNoticeInFlight = null;
285
+ }
286
+ };
287
+ const summarizeConnectedChannels = (connected) => {
288
+ const connectedTypeCount = new Map();
289
+ const typeOrder = [];
290
+ for (const inst of channelInstances) {
291
+ const name = inst.adapter.channelName;
292
+ if (!connected.includes(name))
293
+ continue;
294
+ const type = inst.channelType || name;
295
+ if (!connectedTypeCount.has(type)) {
296
+ connectedTypeCount.set(type, 0);
297
+ typeOrder.push(type);
298
+ }
299
+ connectedTypeCount.set(type, connectedTypeCount.get(type) + 1);
300
+ }
301
+ return typeOrder
302
+ .map(type => {
303
+ const count = connectedTypeCount.get(type);
304
+ return count === 1 ? type : `${type}×${count}`;
305
+ })
306
+ .join(', ');
307
+ };
308
+ return { sendOnlineNoticeForChannel, trySendPendingRestartNotice, summarizeConnectedChannels };
309
+ }
310
+ export function registerChannelHealthNotifications({ eventBus, channelInstances, agentRegistry, }) {
311
+ eventBus.subscribe('channel:error', (event) => {
312
+ if (event.type !== 'channel:error' || event.status !== 'auth_error')
313
+ return;
314
+ const sourceChannelType = event.channel;
315
+ const sourceChannelName = event.channelName || sourceChannelType;
316
+ const message = event.message;
317
+ logger.error(`[ChannelHealth] ${sourceChannelName} auth_error: ${message}`);
318
+ const notified = new Set();
319
+ for (const other of channelInstances) {
320
+ const otherType = other.channelType || other.adapter.channelName;
321
+ if (otherType === sourceChannelType)
322
+ continue;
323
+ if (notified.has(otherType))
324
+ continue;
325
+ const owningAgent = agentRegistry.resolveByChannel(other.adapter.channelKey);
326
+ const ownerId = owningAgent ? getFirstStaticAgentOwner(owningAgent.aid) : undefined;
327
+ if (!ownerId)
328
+ continue;
329
+ notified.add(otherType);
330
+ const envelope = buildEnvelope({
331
+ taskId: `system-channel-down-${crypto.randomBytes(5).toString('hex')}`,
332
+ channel: other.adapter.channelKey,
333
+ channelId: ownerId,
334
+ agentName: owningAgent?.aid || 'evolcore',
335
+ delivery: { chatType: 'private' },
336
+ });
337
+ sendSystemPayload(other.adapter, envelope, {
338
+ kind: 'system.error',
339
+ text: message,
340
+ subtype: 'channel_down',
341
+ recoverable: false,
342
+ }).catch(error => {
343
+ logger.error(`[ChannelHealth] Failed to notify ${other.adapter.channelName} owner:`, error);
344
+ });
345
+ }
346
+ });
347
+ }
348
+ /** Wrap a channel send so delivery logging, message statistics, and sent events stay centralized. */
349
+ export function wrapChannelSend(inst, dependencies) {
350
+ const originalSend = inst.adapter.send.bind(inst.adapter);
351
+ const adapter = inst.adapter;
352
+ adapter.send = async (envelope, rawPayload) => {
353
+ const channelType = inst.channelType || adapter.channelName;
354
+ const payload = rawPayload ? tagErrorPayloadWithEvolcoreVersion(rawPayload) : rawPayload;
355
+ const owningAgent = dependencies.resolveOwningAgent();
356
+ logger.channelOut({
357
+ channel: adapter.channelName,
358
+ channelId: envelope.channelId,
359
+ taskId: envelope.taskId,
360
+ correlationId: envelope.operationId ?? envelope.taskId,
361
+ sessionId: envelope.sessionId,
362
+ agentAid: owningAgent?.aid,
363
+ agentName: envelope.agentName,
364
+ payload: sanitizeMenuLogValue(summarizeOutboundPayload(payload)),
365
+ });
366
+ const result = await originalSend(envelope, payload);
367
+ // AUN adapters return an explicit receipt. Only a receipt with a remote
368
+ // message ID proves delivery; queued/failed results must not be emitted
369
+ // as `message:sent`. Legacy adapters that return void remain compatible.
370
+ const deliveryConfirmed = result === undefined
371
+ || (result && typeof result === 'object' && result.status === 'sent'
372
+ && typeof result.messageId === 'string'
373
+ && result.messageId.length > 0);
374
+ if (shouldCountSentPayload(payload) && deliveryConfirmed) {
375
+ if (channelType !== 'aun') {
376
+ try {
377
+ const logPayload = outboundPayloadToLogText(payload);
378
+ const session = envelope.sessionId
379
+ ? await dependencies.sessionManager.getSessionById(envelope.sessionId)
380
+ : envelope.replyContext?.threadId
381
+ ? await dependencies.sessionManager.getThreadSession(envelope.channel ?? adapter.channelName, envelope.channelId, envelope.replyContext.threadId)
382
+ : dependencies.sessionManager.getActiveSessionSync(envelope.channel ?? adapter.channelName, envelope.channelId, channelType, owningAgent?.aid);
383
+ if (logPayload && session) {
384
+ const chatDir = dependencies.sessionManager.getChatDir(session);
385
+ const isGroup = session.chatType === 'group';
386
+ const target = isGroup
387
+ ? (session.metadata?.groupId || session.channelId)
388
+ : (envelope.replyContext?.peerId || session.metadata?.peerId || envelope.channelId);
389
+ appendMessageLog(chatDir, buildOutboundEntry({
390
+ from: owningAgent?.aid ?? envelope.agentName ?? session.selfAID ?? 'self',
391
+ to: String(target || envelope.channelId),
392
+ sessionId: session.id,
393
+ threadId: envelope.replyContext?.threadId || session.threadId || null,
394
+ chatType: isGroup ? 'group' : 'private',
395
+ groupId: isGroup ? String(session.metadata?.groupId || session.channelId) : null,
396
+ msgId: `${envelope.taskId || 'out'}_${Date.now()}`,
397
+ content: logPayload.text,
398
+ replyTo: envelope.replyContext?.replyToMessageId ?? null,
399
+ agent: session.baseagent ?? null,
400
+ model: null,
401
+ durationMs: null,
402
+ msgType: logPayload.msgType,
403
+ source: normalizeMessageSource(envelope.replyContext?.metadata?.source),
404
+ chatmode: envelope.chatmode,
405
+ }));
406
+ // Per-AID 统计(预览行/tooltip 数据源)。AUN 在 channel 层自行记账,此处只补非 AUN。
407
+ const statsAid = owningAgent?.aid ?? envelope.agentName ?? session.selfAID;
408
+ if (statsAid) {
409
+ dependencies.aidStatsCollector.recordOutbound(statsAid, String(target || envelope.channelId), Buffer.byteLength(logPayload.text, 'utf-8'), logPayload.text, false, undefined, envelope.chatmode, 'send', {
410
+ channelType,
411
+ // 与入向同一套展示名解析:联系人簿 displayName 优先,兜底原生 ID
412
+ peerLabel: resolvePeerDisplayLabel(statsAid, channelType, String(target || envelope.channelId), adapter.channelKey) || session.metadata?.peerName || String(target || envelope.channelId),
413
+ routeId: envelope.channelId,
414
+ });
415
+ }
416
+ }
417
+ }
418
+ catch (error) {
419
+ logger.debug(`[MessageLog] Failed to write outbound channel log: ${error}`);
420
+ }
421
+ }
422
+ dependencies.eventBus.publish({
423
+ type: 'message:sent',
424
+ sessionId: envelope.sessionId ?? envelope.taskId ?? `out-${Date.now()}`,
425
+ channel: envelope.channel ?? adapter.channelName,
426
+ channelName: adapter.channelName,
427
+ channelId: envelope.channelId,
428
+ agentName: owningAgent?.aid ?? envelope.agentName,
429
+ payloadKind: payload.kind,
430
+ timestamp: Date.now(),
431
+ });
432
+ }
433
+ return result;
434
+ };
435
+ }
@@ -2077,7 +2077,10 @@ export class CommandHandler {
2077
2077
  if (sessionSelfAid && sessionSelfAid !== grantSelfAid) {
2078
2078
  return { ok: false, code: 'INVALID_DELEGATION', error: 'Delegation self agent does not match the active session' };
2079
2079
  }
2080
- const requiresSelfAid = parsed.command.kind === 'show' || parsed.command.kind === 'agentmd.put' || parsed.command.kind === 'avatar';
2080
+ const requiresSelfAid = parsed.command.kind === 'show'
2081
+ || parsed.command.kind === 'agentmd.put'
2082
+ || parsed.command.kind === 'agentmd.set'
2083
+ || parsed.command.kind === 'avatar';
2081
2084
  if (requiresSelfAid
2082
2085
  && parsed.command.aid !== grantSelfAid
2083
2086
  && (parsed.command.kind === 'avatar' || !hasTrustedFullAccessDelegation(delegation.grant))) {
@@ -0,0 +1,151 @@
1
+ import crypto from 'crypto';
2
+ import fs from 'fs';
3
+ import path from 'path';
4
+ import { resolvePaths } from '../paths.js';
5
+ import { AUNChannel } from '../channels/aun.js';
6
+ import { BindService } from '../utils/aid-bind.js';
7
+ import { commandExists } from '../utils/cross-platform.js';
8
+ import { logger } from '../utils/logger.js';
9
+ import { remoteAccessService } from './auth/remote-access-store.js';
10
+ import { IpcServer } from '../ipc.js';
11
+ import { removeAll } from '../utils/instance-registry.js';
12
+ import { controlReplyContext } from './message/outbound.js';
13
+ import { TriggerDefinitionManager } from '../trigger/manager.js';
14
+ import { seedUpgradeCheckTrigger } from '../trigger/system-definitions.js';
15
+ export function detectAvailableBaseagents() {
16
+ const out = [];
17
+ for (const command of ['claude', 'gemini', 'codex']) {
18
+ if (commandExists(command))
19
+ out.push(command);
20
+ }
21
+ return out;
22
+ }
23
+ export async function runBindBootstrapDaemon(daemonCfg) {
24
+ logger.warn('[daemon-bootstrap] starting control AID + IPC only');
25
+ const bootstrapControlAid = daemonCfg.aid || '__daemon__';
26
+ try {
27
+ seedUpgradeCheckTrigger(new TriggerDefinitionManager(bootstrapControlAid), {
28
+ aid: bootstrapControlAid,
29
+ originPeerId: bootstrapControlAid,
30
+ baseagent: 'codex',
31
+ });
32
+ }
33
+ catch (error) {
34
+ logger.warn(`[daemon-bootstrap] failed to seed daemon upgrade trigger: ${error instanceof Error ? error.message : String(error)}`);
35
+ }
36
+ const bindService = new BindService({
37
+ receiverAid: daemonCfg.aid,
38
+ getAvailableBaseagents: detectAvailableBaseagents,
39
+ getUptimeSeconds: () => Math.floor(process.uptime()),
40
+ });
41
+ bindService.startCleanup();
42
+ let controlChannel;
43
+ let shutdown;
44
+ let pendingShutdownReason;
45
+ let shutdownInProgress = false;
46
+ controlChannel = new AUNChannel({
47
+ aid: daemonCfg.aid,
48
+ agentName: daemonCfg.aid,
49
+ channelName: 'control',
50
+ pureIdentity: true,
51
+ defaultEncrypt: daemonCfg.aun?.defaultEncrypt,
52
+ aunTrace: daemonCfg.debug?.aunTrace,
53
+ aunSdkLog: daemonCfg.debug?.aunSdkLog,
54
+ });
55
+ try {
56
+ await controlChannel.connect();
57
+ logger.info(`✓ 控制 AID 已连接: ${daemonCfg.aid}`);
58
+ }
59
+ catch (error) {
60
+ logger.warn(`控制 AID 首连失败(后台自动重连,不影响 bootstrap IPC): ${error?.message || error}`);
61
+ }
62
+ controlChannel.onMessage(async (opts) => {
63
+ const text = (opts.content || '').trim();
64
+ let parsed = null;
65
+ try {
66
+ parsed = JSON.parse(text);
67
+ }
68
+ catch {
69
+ parsed = null;
70
+ }
71
+ const response = parsed ? await bindService.handleRequest(parsed, opts.peerId) : null;
72
+ if (response) {
73
+ await controlChannel.sendMessage(opts.channelId, JSON.stringify(response), controlReplyContext(opts));
74
+ }
75
+ });
76
+ const controlToken = crypto.randomBytes(32).toString('base64url');
77
+ fs.mkdirSync(path.dirname(resolvePaths().controlToken), { recursive: true });
78
+ fs.writeFileSync(resolvePaths().controlToken, controlToken, { mode: 0o600 });
79
+ try {
80
+ fs.chmodSync(resolvePaths().controlToken, 0o600);
81
+ }
82
+ catch { }
83
+ const ipcServer = new IpcServer(resolvePaths().socket, () => ({
84
+ pid: process.pid,
85
+ uptime: Math.round(process.uptime() * 1000),
86
+ channels: {},
87
+ channelsByType: {},
88
+ queue: { pending: 0, processing: 0 },
89
+ controlAid: (() => {
90
+ const aidState = controlChannel?.getAidState();
91
+ return {
92
+ aid: daemonCfg.aid,
93
+ connected: aidState?.status === 'connected',
94
+ ...(aidState ? {
95
+ status: aidState.status,
96
+ gatewayUrl: aidState.gatewayUrl,
97
+ reconnectCount: aidState.reconnectCount,
98
+ lastConnectedAt: aidState.lastConnectedAt,
99
+ lastTransportEventAt: aidState.lastTransportEventAt,
100
+ lastInboundAt: aidState.lastInboundAt,
101
+ lastError: aidState.lastError,
102
+ } : {}),
103
+ };
104
+ })(),
105
+ }));
106
+ ipcServer.setControlToken(controlToken);
107
+ ipcServer.setRemoteAccessService(remoteAccessService);
108
+ ipcServer.setRemoteAccessOwnerProvider(() => daemonCfg.owners?.[0]);
109
+ ipcServer.setShutdownExecutor(async (reason) => {
110
+ if (!shutdown) {
111
+ pendingShutdownReason = reason;
112
+ return;
113
+ }
114
+ await shutdown(reason);
115
+ });
116
+ ipcServer.setBindExecutor({
117
+ begin: (cmd) => bindService.begin(cmd),
118
+ status: (taskId) => bindService.status(taskId),
119
+ cancel: (taskId) => bindService.cancel(taskId),
120
+ });
121
+ await ipcServer.start();
122
+ fs.writeFileSync(resolvePaths().readySignal, String(Date.now()));
123
+ logger.info(`✓ Bind bootstrap ready signal written: ${resolvePaths().readySignal}`);
124
+ shutdown = async (signal) => {
125
+ if (shutdownInProgress)
126
+ return;
127
+ shutdownInProgress = true;
128
+ logger.info(`[daemon-bootstrap] shutting down${signal ? ` (${signal})` : ''}`);
129
+ ipcServer.stop();
130
+ try {
131
+ fs.unlinkSync(resolvePaths().readySignal);
132
+ }
133
+ catch { }
134
+ bindService.stopCleanup();
135
+ if (controlChannel) {
136
+ try {
137
+ await controlChannel.disconnect();
138
+ }
139
+ catch { /* ignore */ }
140
+ }
141
+ removeAll();
142
+ process.exit(0);
143
+ };
144
+ if (pendingShutdownReason) {
145
+ const reason = pendingShutdownReason;
146
+ setImmediate(() => { void shutdown?.(reason); });
147
+ }
148
+ process.on('SIGINT', () => shutdown('SIGINT'));
149
+ process.on('SIGTERM', () => shutdown('SIGTERM'));
150
+ process.on('exit', () => removeAll());
151
+ }