evolcore 0.0.26 → 0.0.28
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 +36 -0
- package/bin/codex-managed-acl.ps1 +43 -0
- package/dist/agents/baseagent-availability.js +68 -0
- package/dist/agents/baseagent.js +10 -5
- package/dist/agents/claude-runner.js +123 -22
- package/dist/agents/codex/app-server-client.js +70 -26
- package/dist/agents/codex/catalog.js +7 -1
- package/dist/agents/codex-runner.js +363 -421
- package/dist/agents/ecagent-runner.js +170 -25
- package/dist/agents/gemini-runner.js +6 -0
- package/dist/agents/runner-types.js +50 -2
- package/dist/aun/aid/agentmd.js +73 -31
- package/dist/aun/aid/client.js +1 -1
- package/dist/aun/aid/control-aid.js +180 -1
- package/dist/aun/aid/identity.js +3 -3
- package/dist/aun/group/history.js +303 -0
- package/dist/aun/group/identity.js +43 -0
- package/dist/aun/group/index.js +5 -0
- package/dist/aun/{msg/group.js → group/operations.js} +161 -38
- package/dist/aun/{menu-client.js → menu/client.js} +13 -7
- package/dist/aun/menu/index.js +2 -0
- package/dist/aun/menu/transport.js +49 -0
- package/dist/aun/msg/index.js +2 -1
- package/dist/aun/msg/managed-operation.js +84 -18
- package/dist/aun/msg/mention-schema.js +51 -0
- package/dist/aun/msg/p2p.js +13 -8
- package/dist/aun/msg/upload.js +43 -88
- package/dist/aun/outbox.js +29 -7
- package/dist/aun/rpc/client.js +1 -0
- package/dist/aun/storage/client.js +39 -50
- package/dist/channels/aun.js +544 -239
- package/dist/channels/daemon.js +10 -6
- package/dist/cli/agent-command.js +108 -6
- package/dist/cli/agent-create.js +515 -2
- package/dist/cli/agent.js +109 -8
- package/dist/cli/aun-commands.js +191 -42
- package/dist/cli/config.js +8 -5
- package/dist/cli/contact.js +2 -2
- package/dist/cli/ctl-command.js +2 -2
- package/dist/cli/daemon-commands.js +111 -162
- package/dist/cli/diag.js +3 -3
- package/dist/cli/fs-command.js +3 -3
- package/dist/cli/handoff-command.js +6 -6
- package/dist/cli/index.js +8 -2
- package/dist/cli/init.js +34 -1
- package/dist/cli/managed-command-guard.js +2 -1
- package/dist/cli/menu-client.js +13 -5
- package/dist/cli/model.js +3 -3
- package/dist/cli/queue-command.js +2 -2
- package/dist/cli/response.js +2 -2
- package/dist/cli/restart-monitor.js +81 -19
- package/dist/cli/task-command.js +59 -0
- package/dist/cli/task-context.js +97 -49
- package/dist/cli/trigger-command.js +20 -10
- package/dist/config/builtin-roles.js +10 -0
- package/dist/config/config-field-policy.js +11 -0
- package/dist/config/config-manager.js +32 -4
- package/dist/config/config-operation-service.js +20 -5
- package/dist/config/contact-book-store.js +1 -1
- package/dist/config/contact-book.js +1 -1
- package/dist/config/contact-operation-service.js +1 -1
- package/dist/config/peer-role-resolver.js +1 -1
- package/dist/config/resolved-config-op.js +3 -0
- package/dist/config-store.js +1 -1
- package/dist/core/agent-application-service.js +5 -0
- package/dist/core/audit/event-key.js +17 -2
- package/dist/core/audit/log-integrity.js +34 -8
- package/dist/core/auth/agent-delegation.js +48 -14
- package/dist/core/auth/auth-gateway.js +2 -0
- package/dist/core/auth/authorization-audit.js +34 -38
- package/dist/core/auth/operation-authorizer.js +67 -5
- package/dist/core/auth/operation-catalog.js +110 -0
- package/dist/core/auth/remote-access-store.js +115 -9
- package/dist/core/bootstrap-messages.js +66 -7
- package/dist/core/bootstrap-service.js +110 -19
- package/dist/core/command/agent-control.js +88 -7
- package/dist/core/command/agent-storage-menu.js +443 -0
- package/dist/core/command/cli-intent-parser.js +19 -0
- package/dist/core/command/command-handler.js +105 -40
- package/dist/core/command/data-root-resolver.js +273 -0
- package/dist/core/command/diag-service.js +292 -27
- package/dist/core/command/group-menu.js +7 -2
- package/dist/core/command/menu-catalog.js +34 -4
- package/dist/core/command/menu-handler.js +364 -96
- package/dist/core/command/menu-protocol.js +43 -6
- package/dist/core/command/role-menu.js +2 -2
- package/dist/core/command/slash-handler.js +61 -37
- package/dist/core/event-catalog.js +4 -0
- package/dist/core/evolagent-registry.js +28 -2
- package/dist/core/group/invite-service.js +924 -0
- package/dist/core/interaction-router.js +22 -8
- package/dist/core/message/im-renderer.js +94 -16
- package/dist/core/message/message-bridge.js +140 -13
- package/dist/core/message/message-log.js +5 -3
- package/dist/core/message/message-queue.js +73 -41
- package/dist/core/message/response-engine.js +729 -118
- package/dist/core/permission/approval-gateway.js +36 -3
- package/dist/core/permission/ec-command-parser.js +76 -36
- package/dist/core/permission/protected-paths.js +134 -16
- package/dist/core/permission/sandbox-runtime.js +72 -3
- package/dist/core/permission/tool-error-code.js +15 -0
- package/dist/core/permission/tool-policy.js +8 -8
- package/dist/core/relation/peer-identity.js +57 -6
- package/dist/core/relation/shared-data.js +106 -0
- package/dist/core/role/runtime-policy.js +64 -0
- package/dist/core/runtime-lock.js +10 -26
- package/dist/core/session/runner-task-history.js +170 -0
- package/dist/core/session/session-manager.js +38 -6
- package/dist/core/session/session-mapper.js +41 -0
- package/dist/core/session/session-turn-coordinator.js +37 -7
- package/dist/core/session/session-turns.js +48 -7
- package/dist/core/task/task-recovery-service.js +176 -0
- package/dist/core/task/task-resume-operation.js +32 -0
- package/dist/eck/group-rules-sync.js +5 -3
- package/dist/eck/kit-renderer.js +1 -0
- package/dist/index.js +322 -207
- package/dist/ipc.js +141 -15
- package/dist/paths.js +17 -4
- package/dist/product.js +3 -0
- package/dist/trigger/event-source.js +19 -0
- package/dist/trigger/feedback.js +233 -40
- package/dist/trigger/parser.js +55 -5
- package/dist/trigger/patch.js +9 -0
- package/dist/trigger/scheduler.js +30 -4
- package/dist/trigger/validation.js +20 -3
- package/dist/utils/atomic-write.js +98 -0
- package/dist/utils/cross-platform.js +87 -16
- package/dist/utils/ecweb-utils.js +1 -0
- package/dist/utils/error-utils.js +40 -2
- package/dist/utils/logger.js +23 -13
- package/dist/utils/npm-ops.js +119 -4
- package/dist/utils/package-version-info.js +244 -0
- package/dist/utils/tool-summary.js +59 -3
- package/dist/utils/welcome.js +174 -1
- package/dist/utils/windows-shell-trust.js +37 -0
- package/ecagent/dist/harness/agent-harness.js +3 -0
- package/ecagent/dist/harness/session/jsonl-storage.js +21 -2
- package/kits/docs/INDEX.md +0 -6
- package/kits/docs/channels/aun.md +23 -2
- package/kits/docs/channels/feishu.md +1 -1
- package/kits/docs/context-assembly.md +29 -17
- package/kits/docs/evolcore/INDEX.md +8 -3
- package/kits/docs/evolcore/agent-create.md +39 -7
- package/kits/docs/evolcore/agent.md +33 -1
- package/kits/docs/evolcore/aid.md +9 -1
- package/kits/docs/evolcore/config.md +40 -1
- package/kits/docs/evolcore/group-collaboration.md +1 -1
- package/kits/docs/evolcore/group.md +117 -2
- package/kits/docs/evolcore/menu-client.md +16 -0
- package/kits/docs/evolcore/msg.md +3 -0
- package/kits/docs/evolcore/storage.md +3 -0
- package/kits/docs/evolcore/task.md +14 -0
- package/kits/docs/evolcore/trigger.md +51 -3
- package/kits/docs/venues/group.md +5 -0
- package/kits/eck_manifest.json +27 -0
- package/kits/rules/01-overview.md +11 -7
- package/kits/rules/04-relation.md +2 -1
- package/kits/rules/05-venue.md +1 -1
- package/kits/rules/06-channel.md +6 -4
- package/kits/schemas/agent-config.schema.12.json +25 -1
- package/kits/schemas/daemon.schema.6.json +2 -2
- package/kits/schemas/defaults.schema.5.json +1 -1
- package/kits/schemas/relation-config.schema.1.json +1 -1
- package/kits/schemas/relation-config.schema.5.json +1 -1
- package/kits/schemas/role-config.schema.1.json +14 -0
- package/kits/templates/roles/member.json +5 -0
- package/kits/templates/roles/visitor.json +5 -0
- package/kits/templates/system-fragments/baseagent.md +1 -1
- package/kits/templates/system-fragments/bootstrap.md +26 -9
- package/kits/templates/system-fragments/channel.md +8 -44
- package/kits/templates/system-fragments/commands.md +10 -2
- package/kits/templates/system-fragments/group-unread-history.md +7 -0
- package/kits/templates/system-fragments/reply.md +56 -0
- package/package.json +7 -3
- package/dist/aun/group-identity.js +0 -9
- /package/dist/aun/{group-fs-download.js → group/fs-download.js} +0 -0
- /package/dist/aun/{msg/group-index.js → group/index-rpc.js} +0 -0
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { createHash } from 'crypto';
|
|
4
|
+
import { TextDecoder } from 'util';
|
|
5
|
+
import { isValidAid } from '../aid/validation.js';
|
|
6
|
+
import { normalizeGroupAid } from './identity.js';
|
|
7
|
+
import { resolvePaths } from '../../paths.js';
|
|
8
|
+
import { appendJsonlDurable, chatDirPath } from '../../core/session/session-fs-store.js';
|
|
9
|
+
import { isTransientProtocolMessage, messageLogPath } from '../../core/message/message-log.js';
|
|
10
|
+
export class GroupHistoryError extends Error {
|
|
11
|
+
code;
|
|
12
|
+
constructor(code, message) {
|
|
13
|
+
super(message);
|
|
14
|
+
this.code = code;
|
|
15
|
+
this.name = 'GroupHistoryError';
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
const MAX_PAGE_BYTES = 24 * 1024;
|
|
19
|
+
/** Shared strict grammar for standalone and daemon-managed history. */
|
|
20
|
+
export function parseGroupHistoryArgs(argv) {
|
|
21
|
+
const args = argv[0] === 'history' ? argv.slice(1) : argv;
|
|
22
|
+
const from = args[0];
|
|
23
|
+
if (!from || !isValidAid(from))
|
|
24
|
+
fail('INVALID_ARGUMENT', 'history 需要有效的 self AID');
|
|
25
|
+
if (!args[1])
|
|
26
|
+
fail('INVALID_ARGUMENT', 'history 需要群 AID');
|
|
27
|
+
const groupId = normalizeGroupAid(args[1], from.slice(from.indexOf('.') + 1));
|
|
28
|
+
const values = new Map();
|
|
29
|
+
const flags = new Set(['--unread', '--all']);
|
|
30
|
+
const named = new Set(['--limit', '--thread', '--before', '--after', '--direction', '--format', '--message', '--offset']);
|
|
31
|
+
for (let i = 2; i < args.length; i++) {
|
|
32
|
+
const flag = args[i];
|
|
33
|
+
if (values.has(flag))
|
|
34
|
+
fail('INVALID_ARGUMENT', flag + ' 不能重复');
|
|
35
|
+
if (flags.has(flag))
|
|
36
|
+
values.set(flag, 'true');
|
|
37
|
+
else if (named.has(flag)) {
|
|
38
|
+
const value = args[++i];
|
|
39
|
+
if (!value || value.startsWith('--'))
|
|
40
|
+
fail('INVALID_ARGUMENT', flag + ' 缺少值');
|
|
41
|
+
values.set(flag, value);
|
|
42
|
+
}
|
|
43
|
+
else
|
|
44
|
+
fail('INVALID_ARGUMENT', '不支持的 history 参数: ' + flag);
|
|
45
|
+
}
|
|
46
|
+
if (values.has('--unread') && values.has('--all'))
|
|
47
|
+
fail('INVALID_ARGUMENT', '--unread 与 --all 互斥');
|
|
48
|
+
if (!values.has('--unread') && !values.has('--all')) {
|
|
49
|
+
fail('INVALID_ARGUMENT', 'history 必须明确指定 --unread 或 --all');
|
|
50
|
+
}
|
|
51
|
+
if (values.has('--format') && !['text', 'json'].includes(values.get('--format')))
|
|
52
|
+
fail('INVALID_ARGUMENT', '--format 仅支持 text|json');
|
|
53
|
+
if ((values.has('--message') || values.has('--offset')) && !values.has('--all')) {
|
|
54
|
+
fail('INVALID_ARGUMENT', '--message/--offset 仅支持 --all');
|
|
55
|
+
}
|
|
56
|
+
if (values.has('--offset') && !values.has('--message'))
|
|
57
|
+
fail('INVALID_ARGUMENT', '--offset 需要 --message');
|
|
58
|
+
const offset = values.has('--offset') ? Number(values.get('--offset')) : undefined;
|
|
59
|
+
if (offset !== undefined && (!Number.isSafeInteger(offset) || offset < 0))
|
|
60
|
+
fail('INVALID_ARGUMENT', '无效 offset');
|
|
61
|
+
const limit = values.has('--limit') ? Number(values.get('--limit')) : undefined;
|
|
62
|
+
if (limit !== undefined && (!Number.isInteger(limit) || limit < 1 || limit > 500))
|
|
63
|
+
fail('INVALID_ARGUMENT', '--limit 必须是 1–500');
|
|
64
|
+
const direction = values.get('--direction');
|
|
65
|
+
if (direction && !['in', 'out', 'all'].includes(direction))
|
|
66
|
+
fail('INVALID_ARGUMENT', '无效 direction');
|
|
67
|
+
const time = (flag) => {
|
|
68
|
+
const raw = values.get(flag);
|
|
69
|
+
if (raw === undefined)
|
|
70
|
+
return undefined;
|
|
71
|
+
const result = /^\d+$/.test(raw) ? Number(raw) : Date.parse(raw);
|
|
72
|
+
if (!Number.isFinite(result) || result < 0)
|
|
73
|
+
fail('INVALID_ARGUMENT', flag + ' 需要有效时间');
|
|
74
|
+
return result;
|
|
75
|
+
};
|
|
76
|
+
const before = time('--before');
|
|
77
|
+
const after = time('--after');
|
|
78
|
+
if (before !== undefined && after !== undefined && after >= before)
|
|
79
|
+
fail('INVALID_ARGUMENT', '--after 必须早于 --before');
|
|
80
|
+
return {
|
|
81
|
+
from, groupId, unread: values.has('--unread'), thread: values.get('--thread'),
|
|
82
|
+
limit, direction, before, after, message: values.get('--message'), offset,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
const DETAIL_BYTES = 8 * 1024;
|
|
86
|
+
const MAX_LINE_BYTES = 16 * 1024 * 1024;
|
|
87
|
+
const decoder = new TextDecoder('utf-8', { fatal: true });
|
|
88
|
+
const digest = (value) => createHash('sha256').update(value).digest('hex');
|
|
89
|
+
const fail = (code, message) => { throw new GroupHistoryError(code, message); };
|
|
90
|
+
/** Bounded line reader. Partial tails are retriable; malformed complete lines fail visibly. */
|
|
91
|
+
function* lines(file, snapshot) {
|
|
92
|
+
let fd;
|
|
93
|
+
try {
|
|
94
|
+
fd = fs.openSync(file, 'r');
|
|
95
|
+
}
|
|
96
|
+
catch (error) {
|
|
97
|
+
if (error.code === 'ENOENT')
|
|
98
|
+
return;
|
|
99
|
+
throw error;
|
|
100
|
+
}
|
|
101
|
+
try {
|
|
102
|
+
const end = Math.min(snapshot ?? Number.MAX_SAFE_INTEGER, fs.fstatSync(fd).size);
|
|
103
|
+
const buffer = Buffer.alloc(64 * 1024);
|
|
104
|
+
let pending = Buffer.alloc(0);
|
|
105
|
+
let position = 0;
|
|
106
|
+
while (position < end) {
|
|
107
|
+
const count = fs.readSync(fd, buffer, 0, Math.min(buffer.length, end - position), position);
|
|
108
|
+
if (!count)
|
|
109
|
+
break;
|
|
110
|
+
position += count;
|
|
111
|
+
pending = Buffer.concat([pending, buffer.subarray(0, count)]);
|
|
112
|
+
let start = 0;
|
|
113
|
+
let newline;
|
|
114
|
+
while ((newline = pending.indexOf(10, start)) >= 0) {
|
|
115
|
+
if (newline - start > MAX_LINE_BYTES)
|
|
116
|
+
fail('HISTORY_RECORD_TOO_LARGE', '历史单条记录超过 16 MiB');
|
|
117
|
+
const line = decoder.decode(pending.subarray(start, newline));
|
|
118
|
+
if (line.trim())
|
|
119
|
+
yield line;
|
|
120
|
+
start = newline + 1;
|
|
121
|
+
}
|
|
122
|
+
pending = pending.subarray(start);
|
|
123
|
+
if (pending.length > MAX_LINE_BYTES)
|
|
124
|
+
fail('HISTORY_RECORD_TOO_LARGE', '历史单条记录超过 16 MiB');
|
|
125
|
+
}
|
|
126
|
+
if (pending.length)
|
|
127
|
+
fail('HISTORY_INCOMPLETE', '历史文件末行尚未完整写入,请稍后重试');
|
|
128
|
+
}
|
|
129
|
+
finally {
|
|
130
|
+
fs.closeSync(fd);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
function json(line) {
|
|
134
|
+
try {
|
|
135
|
+
return JSON.parse(line);
|
|
136
|
+
}
|
|
137
|
+
catch {
|
|
138
|
+
return fail('HISTORY_CORRUPT', '历史文件含损坏的完整 JSONL 行,未推进已读状态');
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
function generation(file) {
|
|
142
|
+
try {
|
|
143
|
+
const stat = fs.statSync(file);
|
|
144
|
+
// First complete record plus file identity stays stable across append, but changes on replacement.
|
|
145
|
+
let first = '';
|
|
146
|
+
for (const line of lines(file)) {
|
|
147
|
+
first = line;
|
|
148
|
+
break;
|
|
149
|
+
}
|
|
150
|
+
return digest(JSON.stringify([stat.dev, stat.ino, stat.birthtimeMs, digest(first)]));
|
|
151
|
+
}
|
|
152
|
+
catch (error) {
|
|
153
|
+
if (error.code === 'ENOENT')
|
|
154
|
+
return 'missing';
|
|
155
|
+
throw error;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
function* records(file, args) {
|
|
159
|
+
let size;
|
|
160
|
+
try {
|
|
161
|
+
size = fs.statSync(file).size;
|
|
162
|
+
}
|
|
163
|
+
catch (error) {
|
|
164
|
+
if (error.code === 'ENOENT')
|
|
165
|
+
return;
|
|
166
|
+
throw error;
|
|
167
|
+
}
|
|
168
|
+
const seen = new Set();
|
|
169
|
+
for (const line of lines(file, size)) {
|
|
170
|
+
const entry = json(line);
|
|
171
|
+
if (!entry || entry.chatType !== 'group' || entry.groupId !== args.groupId
|
|
172
|
+
|| (entry.dir !== 'in' && entry.dir !== 'out') || typeof entry.content !== 'string'
|
|
173
|
+
|| typeof entry.from !== 'string' || typeof entry.to !== 'string'
|
|
174
|
+
|| !Number.isFinite(entry.ts) || typeof entry.msgType !== 'string'
|
|
175
|
+
|| (entry.dir === 'in' ? entry.to !== args.from : entry.from !== args.from)
|
|
176
|
+
|| isTransientProtocolMessage(entry))
|
|
177
|
+
continue;
|
|
178
|
+
const identity = entry.msgId ? JSON.stringify([entry.dir, entry.msgId]) : digest(line);
|
|
179
|
+
if (seen.has(identity))
|
|
180
|
+
continue;
|
|
181
|
+
seen.add(identity);
|
|
182
|
+
// Keep the existing read-state fingerprints compatible; redelivery seq/time do not change identity.
|
|
183
|
+
const hash = digest(JSON.stringify([identity, entry.threadId ?? null, entry.from, entry.to,
|
|
184
|
+
entry.msgType, entry.encrypt, entry.content, entry.payloadSummary, entry.groupAttention, entry.groupDelivery]));
|
|
185
|
+
yield { entry, hash };
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
function readState(chatDir, logGeneration) {
|
|
189
|
+
const read = new Set();
|
|
190
|
+
for (const line of lines(path.join(chatDir, 'group-history-read.jsonl'))) {
|
|
191
|
+
const event = json(line);
|
|
192
|
+
if (!event || event.version !== 1 || typeof event.generation !== 'string' || !Array.isArray(event.records)
|
|
193
|
+
|| !event.records.every(value => typeof value === 'string' && /^[a-f0-9]{64}$/.test(value))) {
|
|
194
|
+
fail('HISTORY_STATE_CORRUPT', '群历史已读状态损坏');
|
|
195
|
+
}
|
|
196
|
+
if (event.generation === logGeneration)
|
|
197
|
+
for (const value of event.records)
|
|
198
|
+
read.add(value);
|
|
199
|
+
}
|
|
200
|
+
return read;
|
|
201
|
+
}
|
|
202
|
+
function matchThread(entry, thread) {
|
|
203
|
+
return thread === 'all' || (thread === 'main' ? !entry.threadId : entry.threadId === thread);
|
|
204
|
+
}
|
|
205
|
+
function utf8Prefix(content, bytes) {
|
|
206
|
+
const buffer = Buffer.from(content);
|
|
207
|
+
let end = Math.min(bytes, buffer.length);
|
|
208
|
+
while (end > 0 && end < buffer.length && (buffer[end] & 0xc0) === 0x80)
|
|
209
|
+
end--;
|
|
210
|
+
return buffer.subarray(0, end).toString('utf8');
|
|
211
|
+
}
|
|
212
|
+
/** Local-only history. Unread queries record only the messages selected for this result. */
|
|
213
|
+
export function groupHistory(args, context = {}) {
|
|
214
|
+
if (!isValidAid(args.from))
|
|
215
|
+
fail('INVALID_SELF_AID', '无效 self AID');
|
|
216
|
+
args = { ...args, groupId: normalizeGroupAid(args.groupId, args.from.slice(args.from.indexOf('.') + 1)) };
|
|
217
|
+
const limit = args.limit ?? 50;
|
|
218
|
+
if (!Number.isInteger(limit) || limit < 1 || limit > 500)
|
|
219
|
+
fail('INVALID_LIMIT', 'limit 必须是 1–500');
|
|
220
|
+
const thread = args.thread ?? context.defaultThread ?? 'all';
|
|
221
|
+
if (!thread || thread.length > 512 || /[\x00-\x1f]/.test(thread))
|
|
222
|
+
fail('INVALID_THREAD', '无效 thread');
|
|
223
|
+
const chatDir = chatDirPath(context.sessionsDir ?? resolvePaths().sessionsDir, 'aun', args.groupId, args.from);
|
|
224
|
+
const file = messageLogPath(chatDir);
|
|
225
|
+
const base = { ok: true, source: 'local', self_aid: args.from, group_id: args.groupId };
|
|
226
|
+
const logGeneration = args.unread ? generation(file) : '';
|
|
227
|
+
const read = args.unread ? readState(chatDir, logGeneration) : new Set();
|
|
228
|
+
if (args.message) {
|
|
229
|
+
if (args.unread)
|
|
230
|
+
fail('INVALID_ARGUMENT', '--message 仅支持 --all');
|
|
231
|
+
const offset = args.offset ?? 0;
|
|
232
|
+
if (!Number.isSafeInteger(offset) || offset < 0)
|
|
233
|
+
fail('INVALID_ARGUMENT', '无效 offset');
|
|
234
|
+
for (const { entry } of records(file, args)) {
|
|
235
|
+
if (entry.msgId !== args.message || !matchThread(entry, thread))
|
|
236
|
+
continue;
|
|
237
|
+
const content = Buffer.from(entry.content);
|
|
238
|
+
if (offset > content.length || (offset < content.length && (content[offset] & 0xc0) === 0x80)) {
|
|
239
|
+
fail('INVALID_OFFSET', 'offset 必须是正文内的 UTF-8 字节边界');
|
|
240
|
+
}
|
|
241
|
+
const chunk = utf8Prefix(content.subarray(offset).toString('utf8'), DETAIL_BYTES);
|
|
242
|
+
const next = offset + Buffer.byteLength(chunk);
|
|
243
|
+
return { ...base, mode: 'message', msgId: entry.msgId, thread_id: entry.threadId ?? null,
|
|
244
|
+
offset, content: chunk, content_truncated: next < content.length,
|
|
245
|
+
next_offset: next < content.length ? next : null };
|
|
246
|
+
}
|
|
247
|
+
return fail('MESSAGE_NOT_FOUND', '本地当前范围内找不到该消息');
|
|
248
|
+
}
|
|
249
|
+
const selected = [];
|
|
250
|
+
let hasMore = false;
|
|
251
|
+
let bytes = 0;
|
|
252
|
+
for (const record of records(file, args)) {
|
|
253
|
+
const { entry } = record;
|
|
254
|
+
if (!matchThread(entry, thread)
|
|
255
|
+
|| (args.direction && args.direction !== 'all' && entry.dir !== args.direction)
|
|
256
|
+
|| (args.after !== undefined && entry.ts <= args.after)
|
|
257
|
+
|| (args.before !== undefined && entry.ts >= args.before))
|
|
258
|
+
continue;
|
|
259
|
+
if (args.unread && (entry.dir !== 'in' || entry.groupDelivery !== 'filtered'
|
|
260
|
+
|| entry.groupAttention !== 'unmentioned' || read.has(record.hash)))
|
|
261
|
+
continue;
|
|
262
|
+
const size = Buffer.byteLength(JSON.stringify(entry));
|
|
263
|
+
if (args.unread) {
|
|
264
|
+
if (selected.length >= limit || (selected.length && bytes + size > MAX_PAGE_BYTES)) {
|
|
265
|
+
hasMore = true;
|
|
266
|
+
break;
|
|
267
|
+
}
|
|
268
|
+
selected.push(record);
|
|
269
|
+
bytes += size;
|
|
270
|
+
}
|
|
271
|
+
else {
|
|
272
|
+
selected.push(record);
|
|
273
|
+
bytes += size;
|
|
274
|
+
while (selected.length > limit || (selected.length > 1 && bytes > MAX_PAGE_BYTES)) {
|
|
275
|
+
bytes -= Buffer.byteLength(JSON.stringify(selected.shift().entry));
|
|
276
|
+
hasMore = true;
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
const messages = selected.map(({ entry }) => {
|
|
281
|
+
if (Buffer.byteLength(JSON.stringify(entry)) <= MAX_PAGE_BYTES)
|
|
282
|
+
return entry;
|
|
283
|
+
const preview = utf8Prefix(entry.content, DETAIL_BYTES);
|
|
284
|
+
return {
|
|
285
|
+
ts: entry.ts, time: entry.time, dir: entry.dir, from: entry.from, to: entry.to,
|
|
286
|
+
chatType: entry.chatType, groupId: entry.groupId, threadId: entry.threadId, msgId: entry.msgId,
|
|
287
|
+
msgType: entry.msgType, encrypt: entry.encrypt, content: preview, content_truncated: true,
|
|
288
|
+
next_offset: Buffer.byteLength(preview),
|
|
289
|
+
};
|
|
290
|
+
});
|
|
291
|
+
const result = {
|
|
292
|
+
...base, mode: args.unread ? 'unread' : 'all', thread_id: thread,
|
|
293
|
+
count: messages.length, has_more: hasMore, messages,
|
|
294
|
+
};
|
|
295
|
+
// Persist the exact returned selection, never an entire group/time/seq watermark.
|
|
296
|
+
// Query errors above leave state untouched; --all never writes read state.
|
|
297
|
+
if (args.unread && selected.length) {
|
|
298
|
+
appendJsonlDurable(path.join(chatDir, 'group-history-read.jsonl'), {
|
|
299
|
+
version: 1, generation: logGeneration, records: selected.map(record => record.hash), confirmedAt: Date.now(),
|
|
300
|
+
});
|
|
301
|
+
}
|
|
302
|
+
return result;
|
|
303
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { createRequire } from 'node:module';
|
|
2
|
+
import { fileURLToPath } from 'node:url';
|
|
3
|
+
const requireSdk = createRequire(import.meta.url);
|
|
4
|
+
let groupValidator;
|
|
5
|
+
/** Validate a group command argument, expanding a local base to a full AID. */
|
|
6
|
+
export function normalizeGroupAid(value, localIssuer) {
|
|
7
|
+
// The SDK still converts group.issuer/base. EvolCore no longer accepts it.
|
|
8
|
+
if (value.includes('/')) {
|
|
9
|
+
throw new Error('Path-style group IDs are not supported; use base.issuer');
|
|
10
|
+
}
|
|
11
|
+
// Keep contact/config type hints free of SDK startup side effects (SQLite,
|
|
12
|
+
// etc.). Node >=22.16 supports synchronous ESM loading; resolve the SDK's
|
|
13
|
+
// public import entry because the package has no CommonJS export condition.
|
|
14
|
+
groupValidator ??= requireSdk(fileURLToPath(import.meta.resolve('@agentunion/fastaun'))).validateGroupAIDFormat;
|
|
15
|
+
const aid = groupValidator(value, { localIssuer });
|
|
16
|
+
const dot = aid.indexOf('.');
|
|
17
|
+
if (dot <= 0 || dot === aid.length - 1) {
|
|
18
|
+
throw new Error(`Group AID must include an issuer: ${value}`);
|
|
19
|
+
}
|
|
20
|
+
return aid;
|
|
21
|
+
}
|
|
22
|
+
/** Compare identities only in a context already known to be an AUN group. */
|
|
23
|
+
export function sameGroupAid(left, right) {
|
|
24
|
+
try {
|
|
25
|
+
return normalizeGroupAid(left) === normalizeGroupAid(right);
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Legacy group identity hint, NOT a format validator. Canonical group AIDs and
|
|
33
|
+
* private AIDs share a syntax; prefer trusted chat/targetKind information.
|
|
34
|
+
* Do not broaden this predicate to every SDK-valid group argument.
|
|
35
|
+
*/
|
|
36
|
+
export function isExplicitGroupId(value) {
|
|
37
|
+
const id = value.trim();
|
|
38
|
+
if (!id)
|
|
39
|
+
return false;
|
|
40
|
+
return /^\d+\.[a-z0-9.-]+$/i.test(id)
|
|
41
|
+
|| /^grp_[a-z0-9_-]+$/i.test(id)
|
|
42
|
+
|| /^g-[a-z0-9_-]+(?:[.@][a-z0-9.-]+)?$/i.test(id);
|
|
43
|
+
}
|
|
@@ -1,20 +1,21 @@
|
|
|
1
1
|
import fs from 'fs';
|
|
2
2
|
import path from 'path';
|
|
3
|
-
import { createHash } from 'crypto';
|
|
3
|
+
import { createHash, randomInt } from 'crypto';
|
|
4
4
|
import { TextDecoder } from 'util';
|
|
5
5
|
import { createShortConnection } from '../rpc/index.js';
|
|
6
6
|
import { getAidStore, loadClient, SLOT } from '../aid/store.js';
|
|
7
|
-
import { uploadFileAndBuildPayload } from '
|
|
8
|
-
import { daemonTaskSendIpcTimeoutMs, resolveAunMessageEncrypt } from '
|
|
9
|
-
import { checkGroupIndex, getGroupIndex } from './
|
|
7
|
+
import { uploadFileAndBuildPayload } from '../msg/upload.js';
|
|
8
|
+
import { daemonTaskSendIpcTimeoutMs, resolveAunMessageEncrypt } from '../msg/p2p.js';
|
|
9
|
+
import { checkGroupIndex, getGroupIndex } from './index-rpc.js';
|
|
10
10
|
import { appendMessageLog, buildOutboundEntry, classifyAunPayloadForLog } from '../../core/message/message-log.js';
|
|
11
11
|
import { chatDirPath } from '../../core/session/session-fs-store.js';
|
|
12
12
|
import { normalizeCodexTextPayload, readBestTaskRuntimeContext } from '../../cli/task-context.js';
|
|
13
|
-
import { ipcQuery } from '../../ipc.js';
|
|
13
|
+
import { ipcQuery, IpcTransportError } from '../../ipc.js';
|
|
14
14
|
import { resolvePaths } from '../../paths.js';
|
|
15
|
-
import {
|
|
16
|
-
import { normalizeAunMentionEntries } from '
|
|
17
|
-
import { downloadGroupFsBytes } from '
|
|
15
|
+
import { readDelegationCredential } from '../../core/auth/agent-delegation.js';
|
|
16
|
+
import { normalizeAunMentionEntries, withAunAttention } from '../msg/mention-schema.js';
|
|
17
|
+
import { downloadGroupFsBytes } from './fs-download.js';
|
|
18
|
+
import { normalizeGroupAid, sameGroupAid } from './identity.js';
|
|
18
19
|
/** Map a group directory probe failure to a stable Menu protocol code. */
|
|
19
20
|
export function groupDirectoryErrorCode(rawCode, error) {
|
|
20
21
|
if (rawCode === -33001 || rawCode === -33005 || rawCode === -33006 || rawCode === '-33001' || rawCode === '-33005' || rawCode === '-33006')
|
|
@@ -31,7 +32,8 @@ export function groupDirectoryErrorCode(rawCode, error) {
|
|
|
31
32
|
return 'TEMPORARILY_UNAVAILABLE';
|
|
32
33
|
}
|
|
33
34
|
function buildGroupPayload(body) {
|
|
34
|
-
|
|
35
|
+
const payload = body.mode === 'text' ? { type: 'text', text: body.text } : { ...body.payload };
|
|
36
|
+
return withAunAttention(payload);
|
|
35
37
|
}
|
|
36
38
|
function groupMessageLogContent(body) {
|
|
37
39
|
return body.mode === 'text' ? body.text
|
|
@@ -42,7 +44,8 @@ function isCurrentGroupRuntime(args, runtimeContext) {
|
|
|
42
44
|
return runtimeContext?.channel === 'aun'
|
|
43
45
|
&& runtimeContext.chatType === 'group'
|
|
44
46
|
&& runtimeContext.selfAid === args.from
|
|
45
|
-
&& runtimeContext.channelId
|
|
47
|
+
&& !!runtimeContext.channelId
|
|
48
|
+
&& (runtimeContext.channelId === args.groupId || sameGroupAid(runtimeContext.channelId, args.groupId));
|
|
46
49
|
}
|
|
47
50
|
function groupThreadId(args, payload, runtimeContext) {
|
|
48
51
|
const payloadThread = typeof payload?.thread_id === 'string' && payload.thread_id.trim()
|
|
@@ -89,7 +92,7 @@ function appendGroupSendOutboundLog(args, messageId, payload, runtimeContext, en
|
|
|
89
92
|
}
|
|
90
93
|
async function tryDaemonGroupSend(args, payload, runtimeContext, file) {
|
|
91
94
|
const delegatedTask = !!process.env.EVOLCORE_SESSION_ID
|
|
92
|
-
|| !!
|
|
95
|
+
|| !!readDelegationCredential();
|
|
93
96
|
if (!runtimeContext) {
|
|
94
97
|
return delegatedTask
|
|
95
98
|
? { ok: false, code: 'DELEGATION_REQUIRED', error: 'Task runtime context is unavailable' }
|
|
@@ -117,9 +120,13 @@ async function tryDaemonGroupSend(args, payload, runtimeContext, file) {
|
|
|
117
120
|
returnPolicy: args.returnPolicy,
|
|
118
121
|
originSessionId: runtimeContext.sessionId,
|
|
119
122
|
originMessageId: runtimeContext.messageId,
|
|
120
|
-
delegationToken:
|
|
123
|
+
delegationToken: readDelegationCredential(),
|
|
121
124
|
log: groupSendLogMetadata(args, payload, runtimeContext),
|
|
122
|
-
}, daemonTaskSendIpcTimeoutMs(!!file))
|
|
125
|
+
}, daemonTaskSendIpcTimeoutMs(!!file), { throwOnError: true }).catch(error => {
|
|
126
|
+
if (!(error instanceof IpcTransportError))
|
|
127
|
+
throw error;
|
|
128
|
+
return { ok: false, code: error.code, error: error.message };
|
|
129
|
+
});
|
|
123
130
|
return response ?? {
|
|
124
131
|
ok: false,
|
|
125
132
|
code: 'AUN_DAEMON_UNAVAILABLE',
|
|
@@ -224,6 +231,7 @@ export async function groupSend(args) {
|
|
|
224
231
|
}
|
|
225
232
|
applyGroupRoutingPayloadFields(payload, args, runtimeContext);
|
|
226
233
|
const encrypt = resolveAunMessageEncrypt(args.encrypt);
|
|
234
|
+
payload = withAunAttention(payload);
|
|
227
235
|
const sendParams = { group_id: args.groupId, payload, encrypt };
|
|
228
236
|
const result = await conn.call('group.send', sendParams);
|
|
229
237
|
const messageId = typeof result?.message?.message_id === 'string' && result.message.message_id.trim()
|
|
@@ -296,6 +304,8 @@ export async function groupAck(args) {
|
|
|
296
304
|
await conn.close();
|
|
297
305
|
}
|
|
298
306
|
}
|
|
307
|
+
// ==================== Lifecycle ====================
|
|
308
|
+
const MAX_GROUP_NAME_ATTEMPTS = 5;
|
|
299
309
|
export async function groupCreate(args) {
|
|
300
310
|
return groupCreateWithDependencies(args, { getAidStore, loadClient });
|
|
301
311
|
}
|
|
@@ -305,30 +315,68 @@ export async function groupCreate(args) {
|
|
|
305
315
|
* without opening a real gateway connection.
|
|
306
316
|
*/
|
|
307
317
|
export async function groupCreateWithDependencies(args, dependencies) {
|
|
318
|
+
let requestedGroupAid;
|
|
319
|
+
try {
|
|
320
|
+
if (args.groupId !== undefined) {
|
|
321
|
+
requestedGroupAid = normalizeGroupAid(args.groupId, args.from.slice(args.from.indexOf('.') + 1));
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
catch (error) {
|
|
325
|
+
return { ok: false, code: 'INVALID_GROUP', error: errorMessage(error) };
|
|
326
|
+
}
|
|
308
327
|
const store = await dependencies.getAidStore({ slotId: args.slotId ?? SLOT.cli, aunPath: args.aunPath });
|
|
309
|
-
|
|
328
|
+
let conn;
|
|
310
329
|
try {
|
|
330
|
+
conn = await dependencies.loadClient(store, args.from);
|
|
311
331
|
await conn.connect({ connection_kind: 'short', short_ttl_ms: 30000, auto_reconnect: false });
|
|
312
|
-
const
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
332
|
+
const attempts = requestedGroupAid ? 1 : MAX_GROUP_NAME_ATTEMPTS;
|
|
333
|
+
const randomGroupNumber = dependencies.randomGroupNumber ?? (() => randomInt(10000, 100000));
|
|
334
|
+
for (let attempt = 0; attempt < attempts; attempt++) {
|
|
335
|
+
// Use the service's issuer for default names, just as SDK named-group creation does.
|
|
336
|
+
const groupName = requestedGroupAid?.split('.')[0]
|
|
337
|
+
?? `g-${randomGroupNumber()}`;
|
|
338
|
+
const params = { name: args.name, group_name: groupName };
|
|
339
|
+
if (requestedGroupAid)
|
|
340
|
+
params.group_id = requestedGroupAid;
|
|
341
|
+
if (args.visibility)
|
|
342
|
+
params.visibility = args.visibility;
|
|
343
|
+
if (args.description)
|
|
344
|
+
params.description = args.description;
|
|
345
|
+
if (args.joinMode)
|
|
346
|
+
params.join_mode = args.joinMode;
|
|
347
|
+
let result;
|
|
348
|
+
try {
|
|
349
|
+
result = await conn.createGroup(params, { aidStore: store });
|
|
350
|
+
}
|
|
351
|
+
catch (error) {
|
|
352
|
+
// Only a definite identity collision permits a NEW name. An interrupted
|
|
353
|
+
// create/import may already have created a group and must not be repeated.
|
|
354
|
+
if (!requestedGroupAid && attempt + 1 < attempts && isGroupNameConflict(error))
|
|
355
|
+
continue;
|
|
356
|
+
throw error;
|
|
357
|
+
}
|
|
358
|
+
const group = normalizeGroupInfo(result?.group ?? result);
|
|
359
|
+
const actualAid = normalizeGroupAid(group.group_id);
|
|
360
|
+
const differs = requestedGroupAid ? actualAid !== requestedGroupAid : actualAid.split('.')[0] !== groupName;
|
|
361
|
+
return {
|
|
362
|
+
ok: true,
|
|
363
|
+
group,
|
|
364
|
+
...(differs ? {
|
|
365
|
+
requested_group_aid: requestedGroupAid ?? groupName,
|
|
366
|
+
warning: `请求的群标识 ${requestedGroupAid ?? groupName} 与实际创建结果不同;后续请使用 ${actualAid}`,
|
|
367
|
+
} : {}),
|
|
368
|
+
};
|
|
369
|
+
}
|
|
370
|
+
throw new Error(`Unable to allocate a group name after ${attempts} attempts`);
|
|
326
371
|
}
|
|
327
372
|
catch (e) {
|
|
328
373
|
return formatRpcError(e);
|
|
329
374
|
}
|
|
330
375
|
finally {
|
|
331
|
-
|
|
376
|
+
try {
|
|
377
|
+
await conn?.close();
|
|
378
|
+
}
|
|
379
|
+
catch { }
|
|
332
380
|
try {
|
|
333
381
|
store.close();
|
|
334
382
|
}
|
|
@@ -765,6 +813,86 @@ export async function groupBanlist(args) {
|
|
|
765
813
|
await conn.close();
|
|
766
814
|
}
|
|
767
815
|
}
|
|
816
|
+
export async function groupBlock(args) {
|
|
817
|
+
let conn;
|
|
818
|
+
try {
|
|
819
|
+
conn = await createShortConnection(args.from, args);
|
|
820
|
+
const params = { group_id: args.groupId, subject: args.memberAid };
|
|
821
|
+
if (args.reason !== undefined)
|
|
822
|
+
params.reason = args.reason;
|
|
823
|
+
const data = await conn.call('group.block', params);
|
|
824
|
+
return { ok: true, group_id: args.groupId, data };
|
|
825
|
+
}
|
|
826
|
+
catch (error) {
|
|
827
|
+
return formatRpcError(error);
|
|
828
|
+
}
|
|
829
|
+
finally {
|
|
830
|
+
await conn?.close();
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
export async function groupUnblock(args) {
|
|
834
|
+
let conn;
|
|
835
|
+
try {
|
|
836
|
+
conn = await createShortConnection(args.from, args);
|
|
837
|
+
const data = await conn.call('group.unblock', { group_id: args.groupId, subject: args.memberAid });
|
|
838
|
+
return { ok: true, group_id: args.groupId, data };
|
|
839
|
+
}
|
|
840
|
+
catch (error) {
|
|
841
|
+
return formatRpcError(error);
|
|
842
|
+
}
|
|
843
|
+
finally {
|
|
844
|
+
await conn?.close();
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
export async function groupBlocklist(args) {
|
|
848
|
+
let conn;
|
|
849
|
+
try {
|
|
850
|
+
conn = await createShortConnection(args.from, args);
|
|
851
|
+
const result = await conn.call('group.get_blocklist', { group_id: args.groupId });
|
|
852
|
+
return {
|
|
853
|
+
ok: true, group_id: result?.group_aid ?? result?.group_id ?? args.groupId,
|
|
854
|
+
items: result?.items ?? [], total: result?.total, page: result?.page, size: result?.size,
|
|
855
|
+
};
|
|
856
|
+
}
|
|
857
|
+
catch (error) {
|
|
858
|
+
return formatRpcError(error);
|
|
859
|
+
}
|
|
860
|
+
finally {
|
|
861
|
+
await conn?.close();
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
export async function groupReportStatus(args) {
|
|
865
|
+
let conn;
|
|
866
|
+
try {
|
|
867
|
+
conn = await createShortConnection(args.from, args);
|
|
868
|
+
const data = await conn.call('group.report_status', {
|
|
869
|
+
group_id: args.groupId, online_status: args.onlineStatus, work_status: args.workStatus,
|
|
870
|
+
});
|
|
871
|
+
return { ok: true, group_id: args.groupId, data };
|
|
872
|
+
}
|
|
873
|
+
catch (error) {
|
|
874
|
+
return formatRpcError(error);
|
|
875
|
+
}
|
|
876
|
+
finally {
|
|
877
|
+
await conn?.close();
|
|
878
|
+
}
|
|
879
|
+
}
|
|
880
|
+
export async function groupMemberStatuses(args) {
|
|
881
|
+
let conn;
|
|
882
|
+
try {
|
|
883
|
+
conn = await createShortConnection(args.from, args);
|
|
884
|
+
const result = await conn.call('group.get_member_statuses', { group_id: args.groupId });
|
|
885
|
+
return {
|
|
886
|
+
ok: true, group_id: result?.group_aid ?? result?.group_id ?? args.groupId, items: result?.items ?? [],
|
|
887
|
+
};
|
|
888
|
+
}
|
|
889
|
+
catch (error) {
|
|
890
|
+
return formatRpcError(error);
|
|
891
|
+
}
|
|
892
|
+
finally {
|
|
893
|
+
await conn?.close();
|
|
894
|
+
}
|
|
895
|
+
}
|
|
768
896
|
export async function groupRules(args) {
|
|
769
897
|
const conn = await createShortConnection(args.from, { aunPath: args.aunPath, slotId: args.slotId });
|
|
770
898
|
try {
|
|
@@ -1406,13 +1534,8 @@ function errorMessage(e) {
|
|
|
1406
1534
|
return String(e.message);
|
|
1407
1535
|
return String(e);
|
|
1408
1536
|
}
|
|
1409
|
-
function
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
const first = raw.split(/[.@/]/, 1)[0] ?? '';
|
|
1414
|
-
const slug = first.startsWith('g-') ? first.slice(2) : first;
|
|
1415
|
-
if (/^[a-z0-9][a-z0-9_-]{3,63}$/.test(slug))
|
|
1416
|
-
return slug;
|
|
1417
|
-
return undefined;
|
|
1537
|
+
function isGroupNameConflict(error) {
|
|
1538
|
+
const value = error;
|
|
1539
|
+
return value?.code === -32052
|
|
1540
|
+
|| value?.stringCode === 'IDENTITY_CONFLICT' || value?.code === 'IDENTITY_CONFLICT';
|
|
1418
1541
|
}
|