evolcore 0.0.3 → 0.0.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 +83 -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/daemon-commands.js +46 -15
- package/dist/cli/handoff-command.js +2 -1
- package/dist/cli/init-channel.js +185 -67
- package/dist/cli/init.js +49 -27
- package/dist/cli/trigger-command.js +92 -11
- package/dist/config/access-policy-domain.js +18 -0
- package/dist/config/access-policy.js +110 -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 +229 -26
- 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/role-menu.js +16 -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 +134 -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-fs-store.js +19 -4
- package/dist/core/session/session-manager.js +232 -4
- 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/cross-platform.js +8 -2
- 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/process-introspect.js +19 -3
- 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 +8 -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 +364 -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/wecom.js
CHANGED
|
@@ -1,13 +1,17 @@
|
|
|
1
1
|
import crypto from 'node:crypto';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import os from 'node:os';
|
|
4
|
+
import path from 'node:path';
|
|
2
5
|
import { logger } from '../utils/logger.js';
|
|
3
6
|
import { requireOptional } from '../utils/npm-ops.js';
|
|
4
7
|
import { middleOutputModePolicy, resolveShowActivities, showActivitiesPolicy } from '../core/channel-loader.js';
|
|
5
8
|
import { formatItemsAsText } from '../core/message/items-formatter.js';
|
|
9
|
+
import { createSendFileMarkerPattern } from '../core/message/file-markers.js';
|
|
6
10
|
import { initWelcomeManager, sendWelcomeIfNeeded } from '../utils/welcome.js';
|
|
7
11
|
import { bufferToInboundImage, sanitizeFileName, saveToUploads, validateUrl } from '../utils/media-cache.js';
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import { ContactBindCodeRegistry, } from '
|
|
12
|
+
import { agentDataDir } from '../paths.js';
|
|
13
|
+
import { atomicReadJson, atomicWriteJson } from '../utils/atomic-write.js';
|
|
14
|
+
import { ContactBindCodeRegistry, handleExplicitContactBindCommand, } from '../config/contact-bind-code.js';
|
|
11
15
|
function wecomErrorCode(error) {
|
|
12
16
|
if (!error || typeof error !== 'object')
|
|
13
17
|
return undefined;
|
|
@@ -93,14 +97,384 @@ export function registerPendingWecomContactBind(req) {
|
|
|
93
97
|
return wecomContactBinds.register(req);
|
|
94
98
|
}
|
|
95
99
|
export function handlePendingWecomContactBindMessage(ctx) {
|
|
96
|
-
return wecomContactBinds
|
|
100
|
+
return handleExplicitContactBindCommand(wecomContactBinds, ctx);
|
|
97
101
|
}
|
|
98
|
-
export function getPendingWecomContactBind(selfAid, channelName) {
|
|
99
|
-
return wecomContactBinds.get(selfAid, channelName);
|
|
102
|
+
export function getPendingWecomContactBind(selfAid, channelName, primaryId) {
|
|
103
|
+
return wecomContactBinds.get(selfAid, channelName, primaryId);
|
|
100
104
|
}
|
|
101
105
|
export function clearPendingWecomContactBinds() {
|
|
102
106
|
wecomContactBinds.clear();
|
|
103
107
|
}
|
|
108
|
+
// ── Persistent channel state ──────────────────────────────────────────────────
|
|
109
|
+
const STORE_VERSION = 1;
|
|
110
|
+
const DEFAULT_DEDUP_TTL_MS = 24 * 60 * 60 * 1000;
|
|
111
|
+
const DEFAULT_CARD_RETENTION_MS = 24 * 60 * 60 * 1000;
|
|
112
|
+
function writePrivateJson(filePath, value) {
|
|
113
|
+
atomicWriteJson(filePath, value);
|
|
114
|
+
try {
|
|
115
|
+
fs.chmodSync(filePath, 0o600);
|
|
116
|
+
}
|
|
117
|
+
catch { /* best effort on non-POSIX filesystems */ }
|
|
118
|
+
}
|
|
119
|
+
export class PersistentWecomDeduper {
|
|
120
|
+
filePath;
|
|
121
|
+
channelKey;
|
|
122
|
+
entries = new Map();
|
|
123
|
+
ttlMs;
|
|
124
|
+
maxEntries;
|
|
125
|
+
constructor(filePath, channelKey, opts) {
|
|
126
|
+
this.filePath = filePath;
|
|
127
|
+
this.channelKey = channelKey;
|
|
128
|
+
this.ttlMs = opts?.ttlMs ?? DEFAULT_DEDUP_TTL_MS;
|
|
129
|
+
this.maxEntries = opts?.maxEntries ?? 20_000;
|
|
130
|
+
this.load();
|
|
131
|
+
}
|
|
132
|
+
checkAndMark(messageId, now = Date.now()) {
|
|
133
|
+
this.evict(now);
|
|
134
|
+
const seenAt = this.entries.get(messageId);
|
|
135
|
+
if (seenAt !== undefined && now - seenAt <= this.ttlMs)
|
|
136
|
+
return true;
|
|
137
|
+
this.entries.set(messageId, now);
|
|
138
|
+
this.trimToLimit();
|
|
139
|
+
this.flush();
|
|
140
|
+
return false;
|
|
141
|
+
}
|
|
142
|
+
get size() {
|
|
143
|
+
return this.entries.size;
|
|
144
|
+
}
|
|
145
|
+
load() {
|
|
146
|
+
if (!this.filePath)
|
|
147
|
+
return;
|
|
148
|
+
try {
|
|
149
|
+
const stored = atomicReadJson(this.filePath);
|
|
150
|
+
if (!stored || stored.version !== STORE_VERSION || stored.channelKey !== this.channelKey)
|
|
151
|
+
return;
|
|
152
|
+
for (const [id, timestamp] of Object.entries(stored.entries ?? {})) {
|
|
153
|
+
if (id && Number.isFinite(timestamp))
|
|
154
|
+
this.entries.set(id, timestamp);
|
|
155
|
+
}
|
|
156
|
+
const before = this.entries.size;
|
|
157
|
+
this.evict(Date.now());
|
|
158
|
+
this.trimToLimit();
|
|
159
|
+
if (this.entries.size !== before)
|
|
160
|
+
this.flush();
|
|
161
|
+
}
|
|
162
|
+
catch (error) {
|
|
163
|
+
logger.warn(`[WeCom] Ignoring unreadable dedup store for ${this.channelKey}: ${String(error)}`);
|
|
164
|
+
this.entries.clear();
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
evict(now) {
|
|
168
|
+
for (const [id, timestamp] of this.entries) {
|
|
169
|
+
if (now - timestamp > this.ttlMs)
|
|
170
|
+
this.entries.delete(id);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
trimToLimit() {
|
|
174
|
+
if (this.entries.size <= this.maxEntries)
|
|
175
|
+
return;
|
|
176
|
+
const oldest = [...this.entries.entries()].sort((a, b) => a[1] - b[1]);
|
|
177
|
+
for (let i = 0; i < oldest.length - this.maxEntries; i++) {
|
|
178
|
+
this.entries.delete(oldest[i][0]);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
flush() {
|
|
182
|
+
writePrivateJson(this.filePath, {
|
|
183
|
+
version: STORE_VERSION,
|
|
184
|
+
channelKey: this.channelKey,
|
|
185
|
+
entries: Object.fromEntries(this.entries),
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
export class PersistentWecomCardStore {
|
|
190
|
+
filePath;
|
|
191
|
+
channelKey;
|
|
192
|
+
records = new Map();
|
|
193
|
+
constructor(filePath, channelKey) {
|
|
194
|
+
this.filePath = filePath;
|
|
195
|
+
this.channelKey = channelKey;
|
|
196
|
+
if (filePath)
|
|
197
|
+
this.load();
|
|
198
|
+
}
|
|
199
|
+
set(record) {
|
|
200
|
+
this.records.set(record.taskId, record);
|
|
201
|
+
this.cleanup();
|
|
202
|
+
this.flush();
|
|
203
|
+
}
|
|
204
|
+
get(taskId) {
|
|
205
|
+
this.cleanup();
|
|
206
|
+
return this.records.get(taskId);
|
|
207
|
+
}
|
|
208
|
+
findByInteractionId(interactionId) {
|
|
209
|
+
this.cleanup();
|
|
210
|
+
return [...this.records.values()].find(record => record.interaction.id === interactionId);
|
|
211
|
+
}
|
|
212
|
+
listPendingByChat(chatId) {
|
|
213
|
+
this.cleanup();
|
|
214
|
+
return [...this.records.values()].filter(record => (record.chatId === chatId
|
|
215
|
+
&& record.status === 'pending'
|
|
216
|
+
&& (!record.expiresAt || record.expiresAt >= Date.now())));
|
|
217
|
+
}
|
|
218
|
+
resolve(taskId, action) {
|
|
219
|
+
const record = this.records.get(taskId);
|
|
220
|
+
if (!record)
|
|
221
|
+
return undefined;
|
|
222
|
+
record.status = 'resolved';
|
|
223
|
+
record.resolution = action;
|
|
224
|
+
record.updatedAt = Date.now();
|
|
225
|
+
this.flush();
|
|
226
|
+
return record;
|
|
227
|
+
}
|
|
228
|
+
invalidateByInteractionId(interactionId, reason) {
|
|
229
|
+
const record = this.findByInteractionId(interactionId);
|
|
230
|
+
if (!record || record.status === 'resolved')
|
|
231
|
+
return record;
|
|
232
|
+
record.status = 'invalidated';
|
|
233
|
+
record.invalidationReason = reason;
|
|
234
|
+
record.updatedAt = Date.now();
|
|
235
|
+
this.flush();
|
|
236
|
+
return record;
|
|
237
|
+
}
|
|
238
|
+
load() {
|
|
239
|
+
if (!this.filePath)
|
|
240
|
+
return;
|
|
241
|
+
try {
|
|
242
|
+
const stored = atomicReadJson(this.filePath);
|
|
243
|
+
if (!stored || stored.version !== STORE_VERSION || stored.channelKey !== this.channelKey)
|
|
244
|
+
return;
|
|
245
|
+
for (const record of stored.records ?? []) {
|
|
246
|
+
if (record?.taskId && record?.interaction?.id)
|
|
247
|
+
this.records.set(record.taskId, record);
|
|
248
|
+
}
|
|
249
|
+
this.cleanup(true);
|
|
250
|
+
}
|
|
251
|
+
catch (error) {
|
|
252
|
+
logger.warn(`[WeCom] Ignoring unreadable card store for ${this.channelKey}: ${String(error)}`);
|
|
253
|
+
this.records.clear();
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
cleanup(flushWhenChanged = false) {
|
|
257
|
+
const now = Date.now();
|
|
258
|
+
let changed = false;
|
|
259
|
+
for (const [taskId, record] of this.records) {
|
|
260
|
+
const terminalExpiry = record.updatedAt + DEFAULT_CARD_RETENTION_MS;
|
|
261
|
+
const pendingExpiry = (record.expiresAt ?? record.createdAt + 7 * DEFAULT_CARD_RETENTION_MS)
|
|
262
|
+
+ DEFAULT_CARD_RETENTION_MS;
|
|
263
|
+
const expiresAt = record.status === 'pending' ? pendingExpiry : terminalExpiry;
|
|
264
|
+
if (now > expiresAt) {
|
|
265
|
+
this.records.delete(taskId);
|
|
266
|
+
changed = true;
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
if (changed && flushWhenChanged)
|
|
270
|
+
this.flush();
|
|
271
|
+
}
|
|
272
|
+
flush() {
|
|
273
|
+
if (!this.filePath)
|
|
274
|
+
return;
|
|
275
|
+
writePrivateJson(this.filePath, {
|
|
276
|
+
version: STORE_VERSION,
|
|
277
|
+
channelKey: this.channelKey,
|
|
278
|
+
records: [...this.records.values()],
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
export function wecomChannelDataPaths(agentAid, channelKey) {
|
|
283
|
+
const safeKey = encodeURIComponent(channelKey);
|
|
284
|
+
const dir = path.join(agentDataDir(agentAid), 'channels', safeKey);
|
|
285
|
+
return {
|
|
286
|
+
dedupFile: path.join(dir, 'wecom-message-dedup.json'),
|
|
287
|
+
cardFile: path.join(dir, 'wecom-cards.json'),
|
|
288
|
+
};
|
|
289
|
+
}
|
|
290
|
+
export function createWecomCardTaskId(channelKey, interactionId) {
|
|
291
|
+
const hash = crypto.createHash('sha256').update(`${channelKey}\0${interactionId}`).digest('hex').slice(0, 32);
|
|
292
|
+
return `ec_${hash}`;
|
|
293
|
+
}
|
|
294
|
+
// ── Template cards ────────────────────────────────────────────────────────────
|
|
295
|
+
function truncate(value, max) {
|
|
296
|
+
if (!value)
|
|
297
|
+
return undefined;
|
|
298
|
+
const chars = [...value.trim()];
|
|
299
|
+
return chars.length <= max ? value.trim() : `${chars.slice(0, Math.max(1, max - 1)).join('')}...`;
|
|
300
|
+
}
|
|
301
|
+
function buttonStyle(style) {
|
|
302
|
+
if (style === 'primary')
|
|
303
|
+
return 2;
|
|
304
|
+
if (style === 'danger')
|
|
305
|
+
return 3;
|
|
306
|
+
return 1;
|
|
307
|
+
}
|
|
308
|
+
export function buildWecomInteractionCard(interaction, taskId) {
|
|
309
|
+
const bodyParts = [];
|
|
310
|
+
if (interaction.kind.body)
|
|
311
|
+
bodyParts.push(interaction.kind.body);
|
|
312
|
+
if (interaction.kind.kind === 'action' && interaction.kind.checkers?.length) {
|
|
313
|
+
bodyParts.push(interaction.kind.checkers.map((item, index) => `${index + 1}. ${item.label}`).join('\n'));
|
|
314
|
+
}
|
|
315
|
+
return {
|
|
316
|
+
card_type: 'button_interaction',
|
|
317
|
+
task_id: taskId,
|
|
318
|
+
main_title: {
|
|
319
|
+
title: truncate(interaction.kind.title, 26),
|
|
320
|
+
desc: truncate(bodyParts.join('\n\n'), 120),
|
|
321
|
+
},
|
|
322
|
+
button_list: interaction.kind.buttons
|
|
323
|
+
.map((button, index) => ({ button, index }))
|
|
324
|
+
.filter(({ button }) => !('disabled' in button) || !button.disabled)
|
|
325
|
+
.slice(0, 6)
|
|
326
|
+
.map(({ button, index }) => ({
|
|
327
|
+
text: truncate(button.label, 10) || `选项 ${index + 1}`,
|
|
328
|
+
style: buttonStyle(button.style),
|
|
329
|
+
key: interaction.kind.kind === 'command-card' ? `cmd_${index}` : `act_${index}`,
|
|
330
|
+
})),
|
|
331
|
+
};
|
|
332
|
+
}
|
|
333
|
+
export function parseWecomCardResponse(record, event, operatorId) {
|
|
334
|
+
const eventKey = String(event.event_key ?? '').trim();
|
|
335
|
+
const match = /^(cmd|act)_(\d+)$/.exec(eventKey);
|
|
336
|
+
if (!match)
|
|
337
|
+
return null;
|
|
338
|
+
const index = Number(match[2]);
|
|
339
|
+
if (record.interaction.kind.kind === 'command-card') {
|
|
340
|
+
const button = record.interaction.kind.buttons[index];
|
|
341
|
+
if (!button || button.disabled)
|
|
342
|
+
return null;
|
|
343
|
+
return { command: button.command };
|
|
344
|
+
}
|
|
345
|
+
const button = record.interaction.kind.buttons[index];
|
|
346
|
+
if (!button)
|
|
347
|
+
return null;
|
|
348
|
+
const values = {};
|
|
349
|
+
const selectedItems = event.selected_items?.selected_item;
|
|
350
|
+
if (Array.isArray(selectedItems)) {
|
|
351
|
+
for (const selected of selectedItems) {
|
|
352
|
+
const questionKey = String(selected?.question_key ?? '').trim();
|
|
353
|
+
const optionIds = selected?.option_ids?.option_id;
|
|
354
|
+
if (questionKey && Array.isArray(optionIds))
|
|
355
|
+
values[questionKey] = optionIds.filter(Boolean);
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
return {
|
|
359
|
+
response: {
|
|
360
|
+
type: 'interaction.response',
|
|
361
|
+
id: record.interaction.id,
|
|
362
|
+
action: button.key,
|
|
363
|
+
values: Object.keys(values).length > 0 ? values : undefined,
|
|
364
|
+
operatorId,
|
|
365
|
+
},
|
|
366
|
+
};
|
|
367
|
+
}
|
|
368
|
+
export function buildWecomTerminalCard(record, status, detail) {
|
|
369
|
+
const statusText = status === 'resolved'
|
|
370
|
+
? `已处理${detail ? `:${detail}` : ''}`
|
|
371
|
+
: status === 'rejected'
|
|
372
|
+
? '仅卡片发起者可操作'
|
|
373
|
+
: status === 'unavailable'
|
|
374
|
+
? '处理服务暂不可用,请重新发起'
|
|
375
|
+
: `卡片已失效${detail ? `:${detail}` : ''}`;
|
|
376
|
+
return {
|
|
377
|
+
card_type: 'text_notice',
|
|
378
|
+
task_id: record.taskId,
|
|
379
|
+
main_title: {
|
|
380
|
+
title: truncate(record.interaction.kind.title, 26),
|
|
381
|
+
desc: truncate(statusText, 120),
|
|
382
|
+
},
|
|
383
|
+
sub_title_text: truncate(record.interaction.kind.body, 120),
|
|
384
|
+
};
|
|
385
|
+
}
|
|
386
|
+
export function buildWecomUnknownCard(taskId) {
|
|
387
|
+
return {
|
|
388
|
+
card_type: 'text_notice',
|
|
389
|
+
task_id: taskId,
|
|
390
|
+
main_title: {
|
|
391
|
+
title: '卡片已失效',
|
|
392
|
+
desc: '本地状态不存在,请重新发起操作',
|
|
393
|
+
},
|
|
394
|
+
};
|
|
395
|
+
}
|
|
396
|
+
// ── QR onboarding ─────────────────────────────────────────────────────────────
|
|
397
|
+
const WECOM_QR_GENERATE_URL = 'https://work.weixin.qq.com/ai/qc/generate';
|
|
398
|
+
const WECOM_QR_QUERY_URL = 'https://work.weixin.qq.com/ai/qc/query_result';
|
|
399
|
+
const WECOM_QR_PAGE_URL = 'https://work.weixin.qq.com/ai/qc/gen';
|
|
400
|
+
function platformCode() {
|
|
401
|
+
if (os.platform() === 'darwin')
|
|
402
|
+
return 1;
|
|
403
|
+
if (os.platform() === 'win32')
|
|
404
|
+
return 2;
|
|
405
|
+
if (os.platform() === 'linux')
|
|
406
|
+
return 3;
|
|
407
|
+
return 0;
|
|
408
|
+
}
|
|
409
|
+
async function fetchJson(url, fetchImpl, signal) {
|
|
410
|
+
const timeoutSignal = AbortSignal.timeout(15_000);
|
|
411
|
+
const response = await fetchImpl(url, {
|
|
412
|
+
headers: { Accept: 'application/json' },
|
|
413
|
+
signal: signal ? AbortSignal.any([signal, timeoutSignal]) : timeoutSignal,
|
|
414
|
+
});
|
|
415
|
+
if (!response.ok)
|
|
416
|
+
throw new Error(`企微扫码服务请求失败: HTTP ${response.status}`);
|
|
417
|
+
return response.json();
|
|
418
|
+
}
|
|
419
|
+
export async function createWecomQrSession(fetchImpl = fetch) {
|
|
420
|
+
const url = new URL(WECOM_QR_GENERATE_URL);
|
|
421
|
+
url.searchParams.set('source', 'wecom-cli');
|
|
422
|
+
url.searchParams.set('plat', String(platformCode()));
|
|
423
|
+
const payload = await fetchJson(url, fetchImpl);
|
|
424
|
+
const scode = String(payload?.data?.scode ?? '').trim();
|
|
425
|
+
const authUrl = String(payload?.data?.auth_url ?? '').trim();
|
|
426
|
+
if (!scode || !authUrl)
|
|
427
|
+
throw new Error('企微扫码服务未返回 scode 或授权地址');
|
|
428
|
+
const browserUrl = new URL(WECOM_QR_PAGE_URL);
|
|
429
|
+
browserUrl.searchParams.set('source', 'wecom-cli');
|
|
430
|
+
browserUrl.searchParams.set('scode', scode);
|
|
431
|
+
return { scode, authUrl, browserUrl: browserUrl.toString() };
|
|
432
|
+
}
|
|
433
|
+
export async function pollWecomQrCredentials(scode, opts) {
|
|
434
|
+
const fetchImpl = opts?.fetchImpl ?? fetch;
|
|
435
|
+
const timeoutMs = opts?.timeoutMs ?? 5 * 60 * 1000;
|
|
436
|
+
const intervalMs = opts?.intervalMs ?? 3_000;
|
|
437
|
+
const deadline = Date.now() + timeoutMs;
|
|
438
|
+
const url = new URL(WECOM_QR_QUERY_URL);
|
|
439
|
+
url.searchParams.set('scode', scode);
|
|
440
|
+
let previousStatus = '';
|
|
441
|
+
while (Date.now() < deadline) {
|
|
442
|
+
if (opts?.signal?.aborted)
|
|
443
|
+
throw new Error('企微扫码已取消');
|
|
444
|
+
const payload = await fetchJson(url, fetchImpl, opts?.signal);
|
|
445
|
+
const status = String(payload?.data?.status ?? 'waiting');
|
|
446
|
+
if (status !== previousStatus) {
|
|
447
|
+
opts?.onStatus?.(status);
|
|
448
|
+
previousStatus = status;
|
|
449
|
+
}
|
|
450
|
+
if (status === 'success') {
|
|
451
|
+
const botId = String(payload?.data?.bot_info?.botid ?? '').trim();
|
|
452
|
+
const secret = String(payload?.data?.bot_info?.secret ?? '').trim();
|
|
453
|
+
if (!botId || !secret)
|
|
454
|
+
throw new Error('扫码成功,但企微未返回 Bot ID 或 Secret');
|
|
455
|
+
return { botId, secret };
|
|
456
|
+
}
|
|
457
|
+
if (status === 'expired' || status === 'cancelled' || status === 'rejected') {
|
|
458
|
+
throw new Error(`企微扫码未完成: ${status}`);
|
|
459
|
+
}
|
|
460
|
+
await new Promise((resolve, reject) => {
|
|
461
|
+
const finish = () => {
|
|
462
|
+
opts?.signal?.removeEventListener('abort', onAbort);
|
|
463
|
+
resolve();
|
|
464
|
+
};
|
|
465
|
+
const timer = setTimeout(finish, intervalMs);
|
|
466
|
+
const onAbort = () => {
|
|
467
|
+
clearTimeout(timer);
|
|
468
|
+
opts?.signal?.removeEventListener('abort', onAbort);
|
|
469
|
+
reject(new Error('企微扫码已取消'));
|
|
470
|
+
};
|
|
471
|
+
opts?.signal?.addEventListener('abort', onAbort, { once: true });
|
|
472
|
+
if (opts?.signal?.aborted)
|
|
473
|
+
onAbort();
|
|
474
|
+
});
|
|
475
|
+
}
|
|
476
|
+
throw new Error('企微扫码超时,请重新生成二维码');
|
|
477
|
+
}
|
|
104
478
|
// ── WecomChannel ───────────────────────────────────────────────────────────────
|
|
105
479
|
export class WecomChannel {
|
|
106
480
|
agentAid;
|
|
@@ -1082,6 +1456,8 @@ export class WecomChannelPlugin {
|
|
|
1082
1456
|
await channel.finishStream(envelope.replyContext);
|
|
1083
1457
|
return;
|
|
1084
1458
|
case 'status.interrupted':
|
|
1459
|
+
await channel.finishStream(envelope.replyContext, payload.metadata?.reason === 'daemon_restart' ? '服务正在重启,将继续处理…' : '任务已中止');
|
|
1460
|
+
return;
|
|
1085
1461
|
case 'status.timeout':
|
|
1086
1462
|
await channel.finishStream(envelope.replyContext, '任务已中止');
|
|
1087
1463
|
return;
|
|
@@ -1136,7 +1512,7 @@ export class WecomChannelPlugin {
|
|
|
1136
1512
|
return {
|
|
1137
1513
|
channelType: 'wecom', adapter, channel,
|
|
1138
1514
|
policy,
|
|
1139
|
-
options: { fileMarkerPattern:
|
|
1515
|
+
options: { fileMarkerPattern: createSendFileMarkerPattern(), supportsImages: true, flushDelay: inst.flushDelay },
|
|
1140
1516
|
connect: () => channel.connect(),
|
|
1141
1517
|
disconnect: () => channel.disconnect(),
|
|
1142
1518
|
onProjectPathRequest: () => Promise.resolve(ctx.defaultProjectPath),
|
package/dist/cli/aun-commands.js
CHANGED
|
@@ -791,7 +791,7 @@ export async function cmdMsg(args) {
|
|
|
791
791
|
console.log(`用法: ec msg <command> <from-aid> [args...] [options]
|
|
792
792
|
|
|
793
793
|
Commands:
|
|
794
|
-
send <from> <to> <text>
|
|
794
|
+
send <from> <to> <text> --return <none|required> 发送文本(跨会话时 --return 必填)
|
|
795
795
|
send <from> <to> --text-from-file <path> 从文件读取文本内容
|
|
796
796
|
send <from> <to> --file <path> [--as <type>] 发送文件(image|video|voice|file)
|
|
797
797
|
send <from> <to> --link <url> [--title T] 发送链接卡片
|
|
@@ -814,7 +814,7 @@ Options:
|
|
|
814
814
|
--before <time> 只返回该时间之前的历史
|
|
815
815
|
--after <time> 只返回该时间之后的历史
|
|
816
816
|
--direction <value> 历史方向:in|out|all
|
|
817
|
-
--return <required
|
|
817
|
+
--return <none|required> 跨会话发送必填:none=仅补充目标上下文,required=回答回流来源
|
|
818
818
|
--content-type <mime> 显式覆盖 MIME(仅 --file 模式)
|
|
819
819
|
--as <type> 附件类型:image|video|voice|file(仅 --file 模式)
|
|
820
820
|
--text <说明> 附件说明文字(仅 --file 模式)
|
|
@@ -825,12 +825,12 @@ Options:
|
|
|
825
825
|
(用于发送恰好等于某 flag 的文本,如 send a b -- --encrypt)
|
|
826
826
|
|
|
827
827
|
示例:
|
|
828
|
-
ec msg send alice.agentid.pub bob.agentid.pub "hello"
|
|
829
|
-
ec msg send alice.agentid.pub bob.agentid.pub --text-from-file long-message.txt
|
|
830
|
-
ec msg send alice.agentid.pub bob.agentid.pub "讨论项目A" --thread "project-A"
|
|
831
|
-
ec msg send alice.agentid.pub bob.agentid.pub --file ./pic.png
|
|
832
|
-
ec msg send alice.agentid.pub bob.agentid.pub --file ./demo.mp4 --as video
|
|
833
|
-
ec msg send alice.agentid.pub bob.agentid.pub --link https://example.com --title "AUN"
|
|
828
|
+
ec msg send alice.agentid.pub bob.agentid.pub "hello" --return none
|
|
829
|
+
ec msg send alice.agentid.pub bob.agentid.pub --text-from-file long-message.txt --return none
|
|
830
|
+
ec msg send alice.agentid.pub bob.agentid.pub "讨论项目A" --thread "project-A" --return required
|
|
831
|
+
ec msg send alice.agentid.pub bob.agentid.pub --file ./pic.png --return none
|
|
832
|
+
ec msg send alice.agentid.pub bob.agentid.pub --file ./demo.mp4 --as video --return none
|
|
833
|
+
ec msg send alice.agentid.pub bob.agentid.pub --link https://example.com --title "AUN" --return none
|
|
834
834
|
ec msg pull alice.agentid.pub --app my-bot
|
|
835
835
|
ec msg ack alice.agentid.pub 42 --app my-bot
|
|
836
836
|
ec msg recall alice.agentid.pub msg-uuid-1 msg-uuid-2
|
|
@@ -1234,7 +1234,7 @@ export async function cmdGroup(args) {
|
|
|
1234
1234
|
console.log(`用法: ec group <command> <from-aid> [args...] [options]
|
|
1235
1235
|
|
|
1236
1236
|
消息:
|
|
1237
|
-
send <from> <group-id> <text>
|
|
1237
|
+
send <from> <group-id> <text> --return <none|required> 发送群文本(跨会话时 --return 必填)
|
|
1238
1238
|
send <from> <group-id> --file <path> [--as <type>] 发送群文件
|
|
1239
1239
|
send <from> <group-id> --payload <json> 发送自定义 payload
|
|
1240
1240
|
pull <from> <group-id> [--after-seq N] [--limit N] 拉取群消息
|
|
@@ -1273,7 +1273,7 @@ Options:
|
|
|
1273
1273
|
--encrypt 启用端到端加密(仅 send)
|
|
1274
1274
|
--no-encrypt 强制明文发送(优先于 --encrypt;仅 send)
|
|
1275
1275
|
--thread <id> 指定话题 ID(仅 send)
|
|
1276
|
-
--return <required
|
|
1276
|
+
--return <none|required> 跨会话发送必填(仅 send;Trigger 只能使用 none)
|
|
1277
1277
|
--mention <aid> 发送时 @ 某个成员(可多次,或用逗号分隔多个 aid)
|
|
1278
1278
|
--mention-all 发送时 @ 所有人
|
|
1279
1279
|
--content-type <mime> 显式覆盖 MIME(仅 --file 模式)
|
|
@@ -1286,7 +1286,7 @@ Options:
|
|
|
1286
1286
|
|
|
1287
1287
|
格式示例(.example.invalid 是保留的虚构域,以下 AID 均不是真实地址):
|
|
1288
1288
|
ec group create sender.example.invalid "Dev Team" --visibility private
|
|
1289
|
-
ec group send sender.example.invalid 12345.example.invalid "hello team"
|
|
1289
|
+
ec group send sender.example.invalid 12345.example.invalid "hello team" --return none
|
|
1290
1290
|
ec group send sender.example.invalid 12345.example.invalid "@member 看下 PR" --mention member.example.invalid
|
|
1291
1291
|
ec group send sender.example.invalid 12345.example.invalid --file ./arch.png
|
|
1292
1292
|
ec group rules sender.example.invalid 12345.example.invalid set ./rules.md
|
|
@@ -29,6 +29,22 @@ import { AGENT_DELEGATION_TOKEN_ENV } from '../core/auth/agent-delegation.js';
|
|
|
29
29
|
import { WEB_CLI_BIN, WEB_PACKAGE_LATEST, WEB_PACKAGE_NAME } from '../product.js';
|
|
30
30
|
import { rotateStdoutLog } from '../utils/log-writer.js';
|
|
31
31
|
const execFileAsync = promisify(execFile);
|
|
32
|
+
function printNoSelfAgentHints(options) {
|
|
33
|
+
const { daemonConfig, ecwebStarted, skipped } = options;
|
|
34
|
+
console.log('\nℹ 未配置任何 self-agent,Control Plane 已启动。');
|
|
35
|
+
console.log(' 命令行:ec agent new <aid>.agentid.pub');
|
|
36
|
+
if (daemonConfig.aid && (daemonConfig.owners?.length ?? 0) > 0) {
|
|
37
|
+
console.log(' Evol App:可通过进程级菜单创建 agent');
|
|
38
|
+
}
|
|
39
|
+
if (ecwebStarted) {
|
|
40
|
+
console.log(' ECWeb:可通过控制台创建 agent');
|
|
41
|
+
}
|
|
42
|
+
if (skipped.length > 0) {
|
|
43
|
+
console.log('跳过的目录:');
|
|
44
|
+
for (const skippedAgent of skipped)
|
|
45
|
+
console.log(` - ${skippedAgent.dirName}: ${skippedAgent.reason}`);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
32
48
|
async function probeDaemon(socketPath, timeoutMs = 1000) {
|
|
33
49
|
const response = await ipcQuery(socketPath, { type: 'ping' }, timeoutMs);
|
|
34
50
|
if (response?.pong !== true || !Number.isInteger(response.pid) || response.pid <= 0)
|
|
@@ -196,8 +212,16 @@ export async function cmdStart(opts = {}) {
|
|
|
196
212
|
}
|
|
197
213
|
else {
|
|
198
214
|
console.log('⚡ 未检测到初始化配置,自动启动初始化向导...\n');
|
|
199
|
-
|
|
200
|
-
|
|
215
|
+
// cmdStart 直接调用 cmdInit,不会经过 CLI 的 `init` 分发分支;
|
|
216
|
+
// 这里需显式抑制 AUN SDK 的常规 keystore 日志。
|
|
217
|
+
const { suppressSdkLogs } = await import('../aun/aid/index.js');
|
|
218
|
+
suppressSdkLogs();
|
|
219
|
+
await cmdInit({ invokedByStart: true });
|
|
220
|
+
if (!loadDefaults()) {
|
|
221
|
+
console.log('⚠ 初始化未完成,未启动 EvolCore。');
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
console.log('\n初始化完成,开始启动 EvolCore ....');
|
|
201
225
|
}
|
|
202
226
|
}
|
|
203
227
|
// 控制 AID 门禁:缺 aid 且交互式 → 只补全控制 AID + owners(不重走 baseagent 向导)。
|
|
@@ -236,19 +260,12 @@ export async function cmdStart(opts = {}) {
|
|
|
236
260
|
}
|
|
237
261
|
// 检查至少有一个 self-agent
|
|
238
262
|
const { agents, skipped } = loadAllAgents();
|
|
263
|
+
const isControlAidBootstrap = opts.bindBootstrap && !!daemonCfgStart.aid;
|
|
264
|
+
const shouldPrintNoSelfAgentHints = agents.length === 0 && !isControlAidBootstrap;
|
|
239
265
|
if (agents.length === 0) {
|
|
240
|
-
if (
|
|
266
|
+
if (isControlAidBootstrap) {
|
|
241
267
|
console.log('ℹ 未配置任何 self-agent,绑定 bootstrap 将仅启动控制 AID');
|
|
242
268
|
}
|
|
243
|
-
else {
|
|
244
|
-
console.log('ℹ 未配置任何 self-agent,将仅启动 Control Plane。');
|
|
245
|
-
console.log(' 可通过控制 AID 远程创建 agent,或稍后运行 ec agent new <aid>.agentid.pub');
|
|
246
|
-
if (skipped.length > 0) {
|
|
247
|
-
console.log(`跳过的目录:`);
|
|
248
|
-
for (const s of skipped)
|
|
249
|
-
console.log(` - ${s.dirName}: ${s.reason}`);
|
|
250
|
-
}
|
|
251
|
-
}
|
|
252
269
|
}
|
|
253
270
|
// 检查 instance 目录中的进程状态
|
|
254
271
|
const status = scanInstances();
|
|
@@ -323,10 +340,22 @@ export async function cmdStart(opts = {}) {
|
|
|
323
340
|
console.log(` Logs: ${p.logs}/`);
|
|
324
341
|
console.log(`⏱ ready in ${((Date.now() - cmdStartedAt) / 1000).toFixed(1)}s`);
|
|
325
342
|
if (!ecwebStartedBeforeDaemon) {
|
|
326
|
-
startEcwebIfEnabled(p)
|
|
343
|
+
startEcwebIfEnabled(p)
|
|
344
|
+
.then((ecwebStarted) => {
|
|
345
|
+
if (shouldPrintNoSelfAgentHints) {
|
|
346
|
+
printNoSelfAgentHints({ daemonConfig: loadDaemonConfig(), ecwebStarted, skipped });
|
|
347
|
+
}
|
|
348
|
+
})
|
|
349
|
+
.catch((err) => {
|
|
327
350
|
console.error(`⚠ ECWeb 启动检查失败: ${err instanceof Error ? err.message : String(err)}`);
|
|
351
|
+
if (shouldPrintNoSelfAgentHints) {
|
|
352
|
+
printNoSelfAgentHints({ daemonConfig: loadDaemonConfig(), ecwebStarted: false, skipped });
|
|
353
|
+
}
|
|
328
354
|
});
|
|
329
355
|
}
|
|
356
|
+
else if (shouldPrintNoSelfAgentHints) {
|
|
357
|
+
printNoSelfAgentHints({ daemonConfig: loadDaemonConfig(), ecwebStarted: true, skipped });
|
|
358
|
+
}
|
|
330
359
|
}, 500);
|
|
331
360
|
let forwardingShutdown = false;
|
|
332
361
|
const forwardSignal = (signal) => {
|
|
@@ -426,8 +455,10 @@ export async function cmdStart(opts = {}) {
|
|
|
426
455
|
}
|
|
427
456
|
console.log(`⏱ done in ${((Date.now() - cmdStartedAt) / 1000).toFixed(1)}s`);
|
|
428
457
|
// ECWeb 自动后台启动
|
|
429
|
-
|
|
430
|
-
|
|
458
|
+
const ecwebStarted = ecwebStartedBeforeDaemon || await startEcwebIfEnabled(p);
|
|
459
|
+
if (shouldPrintNoSelfAgentHints) {
|
|
460
|
+
printNoSelfAgentHints({ daemonConfig: loadDaemonConfig(), ecwebStarted, skipped });
|
|
461
|
+
}
|
|
431
462
|
return;
|
|
432
463
|
}
|
|
433
464
|
// 超时
|
|
@@ -246,11 +246,12 @@ Commands:
|
|
|
246
246
|
console.log('没有匹配的 handoff');
|
|
247
247
|
return;
|
|
248
248
|
}
|
|
249
|
-
console.log('ID\tSTATE\tORIGIN\tTARGET\tUPDATED\tATTENTION');
|
|
249
|
+
console.log('ID\tSTATE\tRETURN_POLICY\tORIGIN\tTARGET\tUPDATED\tATTENTION');
|
|
250
250
|
for (const item of result.handoffs) {
|
|
251
251
|
console.log([
|
|
252
252
|
item.handoff_id,
|
|
253
253
|
item.state,
|
|
254
|
+
item.return_policy,
|
|
254
255
|
item.origin_session_id,
|
|
255
256
|
item.target_session_id,
|
|
256
257
|
formatTimestamp(item.updated_at),
|