evolcore 0.0.26 → 0.0.27
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 +25 -0
- package/bin/codex-managed-acl.ps1 +43 -0
- package/dist/agents/baseagent-availability.js +68 -0
- package/dist/agents/claude-runner.js +123 -22
- package/dist/agents/codex/app-server-client.js +63 -26
- package/dist/agents/codex-runner.js +316 -411
- 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} +12 -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 +522 -233
- 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 +35 -23
- package/dist/cli/diag.js +2 -2
- 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 +9 -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 +16 -2
- 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 +76 -39
- package/dist/core/command/data-root-resolver.js +273 -0
- 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 +348 -77
- package/dist/core/command/menu-protocol.js +40 -6
- package/dist/core/command/role-menu.js +2 -2
- package/dist/core/command/slash-handler.js +37 -22
- 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 +76 -15
- package/dist/core/message/message-bridge.js +94 -11
- 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 +709 -117
- package/dist/core/permission/approval-gateway.js +36 -3
- package/dist/core/permission/ec-command-parser.js +61 -29
- 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 +7 -7
- 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 +34 -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 +308 -54
- package/dist/ipc.js +141 -15
- package/dist/paths.js +17 -4
- package/dist/product.js +2 -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/ecweb-utils.js +1 -0
- package/dist/utils/error-utils.js +40 -2
- package/dist/utils/logger.js +23 -13
- package/dist/utils/tool-summary.js +59 -3
- package/dist/utils/welcome.js +174 -1
- 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 +15 -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
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { ensureDir } from '../../utils/atomic-write.js';
|
|
2
|
+
import { resolvePaths } from '../../paths.js';
|
|
2
3
|
import { logger } from '../../utils/logger.js';
|
|
3
4
|
import { encodePath } from '../../utils/cross-platform.js';
|
|
5
|
+
import { readRunnerTaskHistory } from './runner-task-history.js';
|
|
4
6
|
import { chatDirPath, generateSessionId, formatTimestamp, atomicWriteJson, atomicWriteText, appendJsonl, appendJsonlDurable, fsyncJsonlFile, readJsonFile, readLastJsonlLine, readLastJsonlLineStrict, readAllJsonlLines, scanChatDirs, scanMetaFiles, ensureChatDir, readThreadIndex, writeThreadIndex, } from './session-fs-store.js';
|
|
5
|
-
import { sessionToFile, fileToSession, formatSessionKey, DEFAULT_THREAD_ID } from './session-mapper.js';
|
|
7
|
+
import { sessionToFile, fileToSession, formatSessionKey, DEFAULT_THREAD_ID, isArchivedTopicLable, normalizeTopicLable, } from './session-mapper.js';
|
|
6
8
|
import { tryParseChannelKey } from '../channel-loader.js';
|
|
7
9
|
import { isSystemControlChannel } from '../system-channels.js';
|
|
8
10
|
import path from 'path';
|
|
@@ -330,6 +332,9 @@ export class SessionManager {
|
|
|
330
332
|
// backend boundary itself. Always take the latest binding and owner.
|
|
331
333
|
session.agentSessionId = latestId;
|
|
332
334
|
session.baseagent = latest.baseagent;
|
|
335
|
+
// Classification is owned by setTopicLable, not stale turn snapshots.
|
|
336
|
+
if (!opts?.lableMutation)
|
|
337
|
+
session.lable = latest.lable;
|
|
333
338
|
// Merge metadata at field level as well. A caller can hold a stale
|
|
334
339
|
// Session object across an await; preserving keys from the latest
|
|
335
340
|
// snapshot prevents unrelated turn/route updates from being erased.
|
|
@@ -725,7 +730,7 @@ export class SessionManager {
|
|
|
725
730
|
return result;
|
|
726
731
|
}
|
|
727
732
|
// ─── Session lifecycle ───
|
|
728
|
-
async getOrCreateSession(channel, channelId, defaultProjectPath, threadId, metadata, name, userId, chatType, baseagent, selfAID, channelType, peerType, identity) {
|
|
733
|
+
async getOrCreateSession(channel, channelId, defaultProjectPath, threadId, metadata, name, userId, chatType, baseagent, selfAID, channelType, peerType, identity, lable) {
|
|
729
734
|
if (isSystemControlChannel(channel) || isSystemControlChannel(channelType)) {
|
|
730
735
|
throw new Error(`[SessionManager] system control channel is sessionless. channel="${channel}"`);
|
|
731
736
|
}
|
|
@@ -739,7 +744,7 @@ export class SessionManager {
|
|
|
739
744
|
// 用于 resolveIdentity fallback 正确命中群成员角色表(见 resolvePeerRoleDetail)。
|
|
740
745
|
const identityConversationId = chatType === 'group' ? (metadata?.groupId || channelId) : userId;
|
|
741
746
|
if (threadId) {
|
|
742
|
-
const session = this.getOrCreateThreadSession(channel, channelId, threadId, defaultProjectPath, metadata, name, baseagent, selfAID, channelType, peerType, chatType);
|
|
747
|
+
const session = this.getOrCreateThreadSession(channel, channelId, threadId, defaultProjectPath, metadata, name, baseagent, selfAID, channelType, peerType, chatType, lable);
|
|
743
748
|
session.identity = identity ?? this.resolveIdentity(channel, userId, chatType, identityConversationId);
|
|
744
749
|
return session;
|
|
745
750
|
}
|
|
@@ -960,7 +965,7 @@ export class SessionManager {
|
|
|
960
965
|
current.metadata = metadata;
|
|
961
966
|
this.persistSession(current, 'sync', { turnStateMutation: true });
|
|
962
967
|
}
|
|
963
|
-
getOrCreateThreadSession(channel, channelId, threadId, defaultProjectPath, metadata, name, baseagent, selfAID, channelType, peerType, chatType) {
|
|
968
|
+
getOrCreateThreadSession(channel, channelId, threadId, defaultProjectPath, metadata, name, baseagent, selfAID, channelType, peerType, chatType, lable) {
|
|
964
969
|
// 使用精确路径(channelType + selfAID)
|
|
965
970
|
const chatDir = (channelType && selfAID)
|
|
966
971
|
? (() => { const d = chatDirPath(this.sessionsDir, channelType, channelId, selfAID, channel); fs.mkdirSync(d, { recursive: true }); fs.mkdirSync(path.join(d, '_threads'), { recursive: true }); return d; })()
|
|
@@ -1023,6 +1028,7 @@ export class SessionManager {
|
|
|
1023
1028
|
chatType: effectiveChatType,
|
|
1024
1029
|
metadata,
|
|
1025
1030
|
name: name || '话题会话',
|
|
1031
|
+
lable: normalizeTopicLable(lable),
|
|
1026
1032
|
createdAt: Date.now(),
|
|
1027
1033
|
updatedAt: Date.now(),
|
|
1028
1034
|
};
|
|
@@ -1286,7 +1292,7 @@ export class SessionManager {
|
|
|
1286
1292
|
const validSessionId = normalizeBackendSessionId(session.agentSessionId);
|
|
1287
1293
|
return { ...session, agentSessionId: validSessionId };
|
|
1288
1294
|
}
|
|
1289
|
-
async listSessions(channel, channelId) {
|
|
1295
|
+
async listSessions(channel, channelId, options = {}) {
|
|
1290
1296
|
let chatDir;
|
|
1291
1297
|
try {
|
|
1292
1298
|
chatDir = this.resolveChatDirSafe(channel, channelId);
|
|
@@ -1294,7 +1300,16 @@ export class SessionManager {
|
|
|
1294
1300
|
catch {
|
|
1295
1301
|
return [];
|
|
1296
1302
|
}
|
|
1297
|
-
const
|
|
1303
|
+
const archiveFilter = options.archiveFilter ?? 'active';
|
|
1304
|
+
const sessions = this.findAllSessionsInChat(chatDir, true)
|
|
1305
|
+
.filter(session => {
|
|
1306
|
+
if (archiveFilter === 'all')
|
|
1307
|
+
return true;
|
|
1308
|
+
if (!session.threadId)
|
|
1309
|
+
return archiveFilter !== 'archived';
|
|
1310
|
+
const archived = isArchivedTopicLable(session.lable);
|
|
1311
|
+
return archiveFilter === 'archived' ? archived : !archived;
|
|
1312
|
+
});
|
|
1298
1313
|
return sessions.sort((a, b) => b.updatedAt - a.updatedAt);
|
|
1299
1314
|
}
|
|
1300
1315
|
async getSessionByProjectPath(channel, channelId, projectPath) {
|
|
@@ -1361,6 +1376,15 @@ export class SessionManager {
|
|
|
1361
1376
|
this.persistSession(current, 'sync');
|
|
1362
1377
|
return true;
|
|
1363
1378
|
}
|
|
1379
|
+
async setTopicLable(sessionId, lable) {
|
|
1380
|
+
const normalized = normalizeTopicLable(lable);
|
|
1381
|
+
const loaded = this.loadSessionForUpdate(sessionId);
|
|
1382
|
+
if (!loaded?.current.threadId)
|
|
1383
|
+
return false;
|
|
1384
|
+
loaded.current.lable = normalized;
|
|
1385
|
+
this.persistSession(loaded.current, 'sync', { lableMutation: true });
|
|
1386
|
+
return true;
|
|
1387
|
+
}
|
|
1364
1388
|
async unbindSession(sessionId) {
|
|
1365
1389
|
const found = this.findSessionFileById(sessionId);
|
|
1366
1390
|
if (!found)
|
|
@@ -1598,6 +1622,7 @@ export class SessionManager {
|
|
|
1598
1622
|
agentSessionId: forkedAgentSessionId,
|
|
1599
1623
|
metadata,
|
|
1600
1624
|
name: options.name || `${sourceSession.name || '会话'} · 第 ${options.sourceTurn} 轮`,
|
|
1625
|
+
lable: normalizeTopicLable(options.lable === undefined ? sourceSession.lable : options.lable),
|
|
1601
1626
|
createdAt: now,
|
|
1602
1627
|
updatedAt: now,
|
|
1603
1628
|
};
|
|
@@ -1740,6 +1765,9 @@ export class SessionManager {
|
|
|
1740
1765
|
}
|
|
1741
1766
|
return adapter.getFileInfo(projectPath, agentSessionId, agentAid);
|
|
1742
1767
|
}
|
|
1768
|
+
getRunnerTaskHistory(sessionId) {
|
|
1769
|
+
return readRunnerTaskHistory(resolvePaths().logs, sessionId);
|
|
1770
|
+
}
|
|
1743
1771
|
async listSdkSessions(projectPath, baseagent, agentAid) {
|
|
1744
1772
|
const adapter = this.getFileAdapter(baseagent);
|
|
1745
1773
|
if (!adapter?.listSdkSessions)
|
|
@@ -1,6 +1,45 @@
|
|
|
1
1
|
import { formatTimestamp } from './session-fs-store.js';
|
|
2
2
|
/** Default topic component used by the stable session routing key. */
|
|
3
3
|
export const DEFAULT_THREAD_ID = 'main';
|
|
4
|
+
/** Normalize the optional topic category. The public field is spelled `lable`. */
|
|
5
|
+
export function normalizeTopicLable(value) {
|
|
6
|
+
if (value === undefined || value === null)
|
|
7
|
+
return undefined;
|
|
8
|
+
if (typeof value !== 'string')
|
|
9
|
+
throw new Error('lable 必须是字符串或 null');
|
|
10
|
+
const lable = value.trim();
|
|
11
|
+
if (lable.length > 100 || /[\x00-\x1F\x7F]/.test(lable)) {
|
|
12
|
+
throw new Error('lable 最多 100 个字符,且不能包含控制字符');
|
|
13
|
+
}
|
|
14
|
+
return lable || undefined;
|
|
15
|
+
}
|
|
16
|
+
/** The reserved category used to hide a topic from the default topic list. */
|
|
17
|
+
export const ARCHIVED_TOPIC_LABLE = '归档';
|
|
18
|
+
export function isArchivedTopicLable(lable) {
|
|
19
|
+
return lable === ARCHIVED_TOPIC_LABLE;
|
|
20
|
+
}
|
|
21
|
+
/** Resolve the public archived query flag while keeping omitted values backward compatible. */
|
|
22
|
+
export function resolveTopicArchiveFilter(value) {
|
|
23
|
+
if (value === undefined || value === false)
|
|
24
|
+
return 'active';
|
|
25
|
+
if (value === true)
|
|
26
|
+
return 'archived';
|
|
27
|
+
if (value === 'all')
|
|
28
|
+
return 'all';
|
|
29
|
+
throw new Error('archived 必须是布尔值或 "all"');
|
|
30
|
+
}
|
|
31
|
+
/** Ordinary messages may initialize a category; malformed metadata is ignored. */
|
|
32
|
+
export function readTopicLable(payload) {
|
|
33
|
+
if (!payload || typeof payload !== 'object')
|
|
34
|
+
return undefined;
|
|
35
|
+
const fields = payload;
|
|
36
|
+
try {
|
|
37
|
+
return normalizeTopicLable(Object.hasOwn(fields, 'lable') ? fields.lable : fields.label);
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
return undefined;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
4
43
|
export function formatSessionKey(channelType, channelId, threadId) {
|
|
5
44
|
const tid = threadId || DEFAULT_THREAD_ID;
|
|
6
45
|
return `${channelType}#${encodeURIComponent(channelId)}#${encodeURIComponent(tid)}`;
|
|
@@ -64,6 +103,7 @@ export function sessionToFile(session) {
|
|
|
64
103
|
projectPath: session.projectPath,
|
|
65
104
|
agentSessionId: session.agentSessionId ?? null,
|
|
66
105
|
name: session.name ?? null,
|
|
106
|
+
...(session.lable ? { lable: session.lable } : {}),
|
|
67
107
|
activeTask: session.processingState ?? null,
|
|
68
108
|
metadata,
|
|
69
109
|
createdAt: session.createdAt,
|
|
@@ -113,6 +153,7 @@ export function fileToSession(file) {
|
|
|
113
153
|
projectPath: file.projectPath,
|
|
114
154
|
agentSessionId: file.agentSessionId ?? undefined,
|
|
115
155
|
name: file.name ?? undefined,
|
|
156
|
+
...(file.lable ? { lable: file.lable } : {}),
|
|
116
157
|
processingState: file.activeTask ?? undefined,
|
|
117
158
|
metadata,
|
|
118
159
|
createdAt: file.createdAt,
|
|
@@ -13,6 +13,19 @@ function cloneTurnMetadata(value) {
|
|
|
13
13
|
export function readTurnMetadata(session) {
|
|
14
14
|
return cloneTurnMetadata(session.metadata?.turnState);
|
|
15
15
|
}
|
|
16
|
+
function terminalRecoveryContext(session, context) {
|
|
17
|
+
if (!context)
|
|
18
|
+
return undefined;
|
|
19
|
+
return {
|
|
20
|
+
...context,
|
|
21
|
+
baseagent: context.baseagent ?? session.baseagent,
|
|
22
|
+
// A newly allocated backend becomes known only after begin(). Preserve
|
|
23
|
+
// that binding at the terminal boundary, without adopting a backend switch.
|
|
24
|
+
agentSessionId: (!context.baseagent || context.baseagent === session.baseagent)
|
|
25
|
+
? session.agentSessionId ?? context.agentSessionId
|
|
26
|
+
: context.agentSessionId,
|
|
27
|
+
};
|
|
28
|
+
}
|
|
16
29
|
export class SessionTurnCoordinator {
|
|
17
30
|
sessionManager;
|
|
18
31
|
states = new Map();
|
|
@@ -69,7 +82,7 @@ export class SessionTurnCoordinator {
|
|
|
69
82
|
this.states.set(sessionId, cloneTurnMetadata(next));
|
|
70
83
|
return cloneTurnMetadata(next);
|
|
71
84
|
}
|
|
72
|
-
async begin(session, taskId, inputId = crypto.randomUUID()) {
|
|
85
|
+
async begin(session, taskId, inputId = crypto.randomUUID(), recoveryContext) {
|
|
73
86
|
const state = this.hydrate(session);
|
|
74
87
|
const generation = state.generation + 1;
|
|
75
88
|
const turnId = `turn-${crypto.randomUUID()}`;
|
|
@@ -82,6 +95,7 @@ export class SessionTurnCoordinator {
|
|
|
82
95
|
status: 'starting',
|
|
83
96
|
startedAt,
|
|
84
97
|
interactions: [],
|
|
98
|
+
recoveryContext,
|
|
85
99
|
};
|
|
86
100
|
const next = { ...state, schemaVersion: 1, generation, active };
|
|
87
101
|
await this.persist(session, next);
|
|
@@ -96,20 +110,34 @@ export class SessionTurnCoordinator {
|
|
|
96
110
|
async completeInterrupt(session, reason) {
|
|
97
111
|
const state = this.hydrate(session);
|
|
98
112
|
const active = state.active;
|
|
99
|
-
if (!active
|
|
113
|
+
if (!active) {
|
|
114
|
+
// A delayed task:error must not resurrect work after an idle /stop or a
|
|
115
|
+
// replacement message. Keep completed tasks completed.
|
|
116
|
+
const terminal = state.lastTerminal;
|
|
117
|
+
const cancelled = reason === 'stop' || reason === 'explicit_cancel';
|
|
118
|
+
const superseded = reason === 'new_message' || reason === 'recalled' || reason === 'restart';
|
|
119
|
+
await this.persist(session, terminal && terminal.status !== 'committed' && (cancelled || superseded)
|
|
120
|
+
? { ...state, lastTerminal: { ...terminal, status: cancelled ? 'cancelled' : 'superseded', reason } }
|
|
121
|
+
: state);
|
|
122
|
+
return undefined;
|
|
123
|
+
}
|
|
124
|
+
if (active.status !== 'interrupting')
|
|
100
125
|
return undefined;
|
|
101
126
|
const next = {
|
|
102
127
|
...state,
|
|
103
128
|
active: undefined,
|
|
104
129
|
lastTerminal: {
|
|
105
130
|
turnId: active.turnId,
|
|
131
|
+
recoveryContext: terminalRecoveryContext(session, active.recoveryContext),
|
|
106
132
|
taskId: active.taskId,
|
|
107
133
|
generation: active.generation,
|
|
108
|
-
status: reason === '
|
|
109
|
-
? '
|
|
110
|
-
: reason === '
|
|
111
|
-
? '
|
|
112
|
-
: '
|
|
134
|
+
status: reason === 'timeout' || reason === 'execution_error'
|
|
135
|
+
? 'failed'
|
|
136
|
+
: reason === 'daemon_restart'
|
|
137
|
+
? 'interrupted'
|
|
138
|
+
: reason === 'stop' || reason === 'explicit_cancel'
|
|
139
|
+
? 'cancelled'
|
|
140
|
+
: 'superseded',
|
|
113
141
|
reason,
|
|
114
142
|
finishedAt: Date.now(),
|
|
115
143
|
},
|
|
@@ -178,6 +206,7 @@ export class SessionTurnCoordinator {
|
|
|
178
206
|
taskId: lease.taskId,
|
|
179
207
|
generation: lease.generation,
|
|
180
208
|
status: 'committed',
|
|
209
|
+
recoveryContext: terminalRecoveryContext(session, state.active?.recoveryContext),
|
|
181
210
|
finishedAt: Date.now(),
|
|
182
211
|
},
|
|
183
212
|
};
|
|
@@ -196,6 +225,7 @@ export class SessionTurnCoordinator {
|
|
|
196
225
|
taskId: lease.taskId,
|
|
197
226
|
generation: lease.generation,
|
|
198
227
|
status,
|
|
228
|
+
recoveryContext: terminalRecoveryContext(session, state.active?.recoveryContext),
|
|
199
229
|
reason,
|
|
200
230
|
finishedAt: Date.now(),
|
|
201
231
|
},
|
|
@@ -1,4 +1,23 @@
|
|
|
1
|
-
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
// These prompts are steered into the active base-agent turn by EvolCore. The
|
|
3
|
+
// Codex history adapter exposes steered input as a regular `user` item, so it
|
|
4
|
+
// must not split the user's actual turn when reconstructing fork anchors.
|
|
5
|
+
const TASK_RECOVERY_PROMPT = '继续当前会话尚未完成的任务。先核对最新用户指令、已有结果和任务状态;若已完成、用户已停止或正在等待必要输入/审批,则不要重复执行。对可能已产生副作用的步骤先核实结果,再继续未完成的工作。';
|
|
6
|
+
function isInternalInjectedUserMessage(message) {
|
|
7
|
+
const text = extractUserText(message);
|
|
8
|
+
if (!text)
|
|
9
|
+
return false;
|
|
10
|
+
if (text === TASK_RECOVERY_PROMPT)
|
|
11
|
+
return true;
|
|
12
|
+
if (text.startsWith('⚠️ 策略拒绝')
|
|
13
|
+
&& text.endsWith('命令未执行,请按提示重新执行正确的命令。'))
|
|
14
|
+
return true;
|
|
15
|
+
if (text.startsWith('⚠️ proactive 模式违规:')
|
|
16
|
+
&& text.endsWith('请重新执行正确的命令。'))
|
|
17
|
+
return true;
|
|
18
|
+
return false;
|
|
19
|
+
}
|
|
20
|
+
function extractText(message) {
|
|
2
21
|
const value = message;
|
|
3
22
|
let text = '';
|
|
4
23
|
if (typeof value?.content === 'string') {
|
|
@@ -10,8 +29,12 @@ export function extractSessionUserContent(message) {
|
|
|
10
29
|
.map((block) => block.text)
|
|
11
30
|
.join(' ');
|
|
12
31
|
}
|
|
13
|
-
|
|
14
|
-
|
|
32
|
+
return text.trim();
|
|
33
|
+
}
|
|
34
|
+
function extractUserText(message) {
|
|
35
|
+
let text = extractText(message);
|
|
36
|
+
text = text.replace(/^【新消息插入】\s*/, '')
|
|
37
|
+
.replace(/\s*【(?:请无视之前中断继续处理|请根据前后消息酌情处理)】$/, '').trim();
|
|
15
38
|
if (text.startsWith('‹')) {
|
|
16
39
|
const newline = text.indexOf('\n');
|
|
17
40
|
if (newline !== -1)
|
|
@@ -27,10 +50,15 @@ export function extractSessionUserContent(message) {
|
|
|
27
50
|
text = parts.join(' ');
|
|
28
51
|
}
|
|
29
52
|
text = text.replace(/\s+/g, ' ').trim();
|
|
30
|
-
|
|
31
|
-
|
|
53
|
+
return text;
|
|
54
|
+
}
|
|
55
|
+
export function extractSessionUserContent(message) {
|
|
56
|
+
const text = extractUserText(message);
|
|
32
57
|
return text.length > 50 ? `${text.slice(0, 50)}…` : text;
|
|
33
58
|
}
|
|
59
|
+
function textHash(text) {
|
|
60
|
+
return createHash('sha256').update(text.replace(/\s+/g, ' ').trim()).digest('hex');
|
|
61
|
+
}
|
|
34
62
|
function isToolResultMessage(message) {
|
|
35
63
|
const content = message?.content;
|
|
36
64
|
return Array.isArray(content)
|
|
@@ -48,19 +76,32 @@ export function buildSessionTurnList(messages) {
|
|
|
48
76
|
userContent: pending.userContent,
|
|
49
77
|
userUuid: pending.userUuid,
|
|
50
78
|
assistantUuid: pending.assistantUuid,
|
|
79
|
+
assistantUuids: [...(pending.assistantUuids ?? [])],
|
|
80
|
+
userTextHash: pending.userTextHash,
|
|
81
|
+
assistantTextHashes: [...pending.assistantTextHashes],
|
|
51
82
|
});
|
|
52
83
|
};
|
|
53
84
|
for (const message of messages) {
|
|
54
85
|
if (message.type === 'user') {
|
|
55
86
|
if (isToolResultMessage(message.message))
|
|
56
87
|
continue;
|
|
88
|
+
if (isInternalInjectedUserMessage(message.message))
|
|
89
|
+
continue;
|
|
57
90
|
finishPending();
|
|
58
91
|
const userContent = extractSessionUserContent(message.message);
|
|
59
|
-
pending = userContent ? {
|
|
92
|
+
pending = userContent ? {
|
|
93
|
+
userContent, userUuid: message.uuid, assistantUuids: [],
|
|
94
|
+
userTextHash: textHash(extractUserText(message.message)), assistantTextHashes: [],
|
|
95
|
+
} : undefined;
|
|
60
96
|
continue;
|
|
61
97
|
}
|
|
62
|
-
if (message.type === 'assistant' && pending)
|
|
98
|
+
if (message.type === 'assistant' && pending) {
|
|
63
99
|
pending.assistantUuid = message.uuid;
|
|
100
|
+
pending.assistantUuids.push(message.uuid);
|
|
101
|
+
const text = extractText(message.message);
|
|
102
|
+
if (text)
|
|
103
|
+
pending.assistantTextHashes.push(textHash(text));
|
|
104
|
+
}
|
|
64
105
|
}
|
|
65
106
|
finishPending();
|
|
66
107
|
return turns;
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import { replyContextFromSession } from '../message/message-utils.js';
|
|
2
|
+
export const TASK_RECOVERY_PROMPT = '继续当前会话尚未完成的任务。先核对最新用户指令、已有结果和任务状态;若已完成、用户已停止或正在等待必要输入/审批,则不要重复执行。对可能已产生副作用的步骤先核实结果,再继续未完成的工作。';
|
|
3
|
+
function lastTask(state) {
|
|
4
|
+
return state?.active ?? state?.lastTerminal;
|
|
5
|
+
}
|
|
6
|
+
function sameRecoveryRun(previous, requested) {
|
|
7
|
+
return !!previous && !!requested
|
|
8
|
+
&& previous.triggerId === requested.triggerId && previous.runId === requested.runId
|
|
9
|
+
&& previous.taskId === requested.taskId && previous.generation === requested.generation;
|
|
10
|
+
}
|
|
11
|
+
function canRecover(session, automatic, origin) {
|
|
12
|
+
const state = session.metadata?.turnState;
|
|
13
|
+
const task = lastTask(state);
|
|
14
|
+
if (!task)
|
|
15
|
+
return !automatic && !!session.processingState;
|
|
16
|
+
if (task.status === 'committed' || task.status === 'waiting_interaction')
|
|
17
|
+
return false;
|
|
18
|
+
if (state?.active?.interactions.some(item => !item.settlement))
|
|
19
|
+
return false;
|
|
20
|
+
const context = task.recoveryContext;
|
|
21
|
+
if ((context?.baseagent && context.baseagent !== session.baseagent)
|
|
22
|
+
|| (context?.agentSessionId && context.agentSessionId !== session.agentSessionId))
|
|
23
|
+
return false;
|
|
24
|
+
const reason = 'cancelReason' in task ? task.cancelReason : 'reason' in task ? task.reason : undefined;
|
|
25
|
+
if (reason && /interaction|approval|permission|denied|recalled|new_message|empty_prompt/i.test(reason))
|
|
26
|
+
return false;
|
|
27
|
+
if (task.status === 'cancelled' || task.status === 'superseded' || reason === 'stop' || reason === 'explicit_cancel') {
|
|
28
|
+
return !automatic && (reason === 'stop' || reason === 'explicit_cancel');
|
|
29
|
+
}
|
|
30
|
+
if (!automatic)
|
|
31
|
+
return true;
|
|
32
|
+
if (state?.active)
|
|
33
|
+
return false;
|
|
34
|
+
return !context?.originTriggerId || sameRecoveryRun(context.recovery, origin);
|
|
35
|
+
}
|
|
36
|
+
/** Check again at dequeue: a newer task, /stop, /renew or backend switch wins. */
|
|
37
|
+
export function isTaskRecoveryCurrent(session, recovery) {
|
|
38
|
+
const state = session.metadata?.turnState;
|
|
39
|
+
return !session.deletedAt
|
|
40
|
+
&& session.agentSessionId === recovery.agentSessionId
|
|
41
|
+
&& session.baseagent === recovery.baseagent
|
|
42
|
+
&& (state?.generation ?? 0) === recovery.generation
|
|
43
|
+
&& lastTask(state)?.taskId === recovery.taskId
|
|
44
|
+
&& canRecover(session, recovery.automatic, recovery.origin);
|
|
45
|
+
}
|
|
46
|
+
export class TaskRecoveryService {
|
|
47
|
+
deps;
|
|
48
|
+
constructor(deps) {
|
|
49
|
+
this.deps = deps;
|
|
50
|
+
}
|
|
51
|
+
async resume(request) {
|
|
52
|
+
const result = (status, message, ok = true) => ({
|
|
53
|
+
ok, status, sessionId: request.sessionId, message,
|
|
54
|
+
});
|
|
55
|
+
return this.deps.queue.withSessionBarrier(request.sessionId, async () => {
|
|
56
|
+
if (request.signal?.aborted)
|
|
57
|
+
return result('not_recoverable', '恢复请求已取消', false);
|
|
58
|
+
if (request.automatic && (!request.expectedTaskId || !Number.isSafeInteger(request.expectedGeneration))) {
|
|
59
|
+
return result('not_recoverable', '自动恢复必须指定原任务及其版本', false);
|
|
60
|
+
}
|
|
61
|
+
const session = await this.deps.sessions.getSessionById(request.sessionId);
|
|
62
|
+
if (!session || session.deletedAt || (request.agentAid && session.selfAID !== request.agentAid)) {
|
|
63
|
+
return result('not_found', '目标会话不存在或不属于当前 Agent', false);
|
|
64
|
+
}
|
|
65
|
+
const state = session.metadata?.turnState;
|
|
66
|
+
const task = lastTask(state);
|
|
67
|
+
const trigger = request.message?.triggerMeta;
|
|
68
|
+
const origin = request.automatic && trigger
|
|
69
|
+
&& typeof trigger.runId === 'string' && typeof trigger.attemptId === 'string'
|
|
70
|
+
&& request.expectedTaskId && request.expectedGeneration !== undefined
|
|
71
|
+
? { taskId: request.expectedTaskId, generation: request.expectedGeneration,
|
|
72
|
+
triggerId: trigger.triggerId, runId: trigger.runId, attemptId: trigger.attemptId }
|
|
73
|
+
: undefined;
|
|
74
|
+
const retry = sameRecoveryRun(task?.recoveryContext?.recovery, origin);
|
|
75
|
+
if (!retry && ((request.expectedTaskId !== undefined && task?.taskId !== request.expectedTaskId)
|
|
76
|
+
|| (request.expectedGeneration !== undefined && task?.generation !== request.expectedGeneration))) {
|
|
77
|
+
return result('stale', '原任务状态已变化,已忽略过期恢复请求');
|
|
78
|
+
}
|
|
79
|
+
if (retry && task?.recoveryContext?.recovery?.attemptId === origin?.attemptId) {
|
|
80
|
+
return result('stale', '该恢复 attempt 已执行,已忽略重复请求');
|
|
81
|
+
}
|
|
82
|
+
if (state?.active?.status === 'waiting_interaction'
|
|
83
|
+
|| state?.active?.interactions.some(item => !item.settlement)) {
|
|
84
|
+
return result('waiting_interaction', '任务正在等待必要输入或审批,请先完成交互');
|
|
85
|
+
}
|
|
86
|
+
let runner;
|
|
87
|
+
try {
|
|
88
|
+
runner = this.deps.getRunner(session);
|
|
89
|
+
}
|
|
90
|
+
catch {
|
|
91
|
+
return result('not_recoverable', '原 baseagent 当前不可用', false);
|
|
92
|
+
}
|
|
93
|
+
if (!request.automatic && this.deps.resumePause(session.id)) {
|
|
94
|
+
return result('resumed_pause', '✓ 已继续暂停的任务');
|
|
95
|
+
}
|
|
96
|
+
// Other gate holders may have captured an older cutoff. Recovery must
|
|
97
|
+
// see messages admitted during the session-store read as well.
|
|
98
|
+
const snapshot = this.deps.queue.getSessionSnapshot(session.id);
|
|
99
|
+
// task:error is published before queue cleanup. Only the exact durable
|
|
100
|
+
// failed task may queue its successor while that batch is winding down.
|
|
101
|
+
const finishingFailedTask = request.automatic && !!request.expectedTaskId
|
|
102
|
+
&& !state?.active && state?.lastTerminal?.status === 'failed'
|
|
103
|
+
&& !snapshot.dispatching
|
|
104
|
+
&& snapshot.activeTaskIds.length > 0 && snapshot.activeTaskIds.every(id => id === task?.taskId);
|
|
105
|
+
if (snapshot.prequeue || snapshot.queued || ((snapshot.active || snapshot.dispatching
|
|
106
|
+
|| runner.hasActiveStream(session.id)) && !finishingFailedTask)) {
|
|
107
|
+
return result('already_running', '任务正在执行或已有待处理消息,无需重复恢复');
|
|
108
|
+
}
|
|
109
|
+
if (task?.status === 'committed')
|
|
110
|
+
return result('completed', '上一任务已正常完成,无需恢复');
|
|
111
|
+
if (!canRecover(session, request.automatic === true, origin)) {
|
|
112
|
+
return result('not_recoverable', '当前没有可恢复的异常任务', false);
|
|
113
|
+
}
|
|
114
|
+
if (!runner.capabilities?.taskRecovery || !session.agentSessionId) {
|
|
115
|
+
return result('not_recoverable', '原 baseagent 会话不可恢复;不会创建替代会话', false);
|
|
116
|
+
}
|
|
117
|
+
if (runner.resolveSessionFile && !runner.resolveSessionFile(session.agentSessionId, session.projectPath)) {
|
|
118
|
+
return result('not_recoverable', '原 baseagent 会话文件不存在;不会创建替代会话', false);
|
|
119
|
+
}
|
|
120
|
+
if (!this.deps.hasChannel(session))
|
|
121
|
+
return result('not_recoverable', '原会话渠道当前不可用', false);
|
|
122
|
+
const context = task?.recoveryContext;
|
|
123
|
+
const peerId = context?.peerId || request.peerId || session.metadata?.peerId;
|
|
124
|
+
const peerName = session.metadata?.peerName;
|
|
125
|
+
const peerType = session.metadata?.peerType;
|
|
126
|
+
const channelType = session.channelType || session.channel.split('#')[0];
|
|
127
|
+
const encrypted = context?.encrypted ?? request.encrypted;
|
|
128
|
+
if (!peerId || (channelType === 'aun' && typeof encrypted !== 'boolean')) {
|
|
129
|
+
return result('not_recoverable', '缺少原任务身份或加密状态,无法安全恢复', false);
|
|
130
|
+
}
|
|
131
|
+
const recovery = {
|
|
132
|
+
generation: state?.generation ?? 0, taskId: task?.taskId,
|
|
133
|
+
agentSessionId: session.agentSessionId, baseagent: session.baseagent,
|
|
134
|
+
automatic: request.automatic === true,
|
|
135
|
+
origin,
|
|
136
|
+
};
|
|
137
|
+
const replyContext = replyContextFromSession(session);
|
|
138
|
+
const message = {
|
|
139
|
+
...request.message,
|
|
140
|
+
...(trigger ? { triggerMeta: {
|
|
141
|
+
...trigger,
|
|
142
|
+
originPeerId: peerId,
|
|
143
|
+
originChannelKey: session.metadata?.channelKey || session.channel,
|
|
144
|
+
originChannelType: channelType,
|
|
145
|
+
originChatType: session.chatType === 'group' ? 'group' : 'private',
|
|
146
|
+
originConversationId: session.channelId,
|
|
147
|
+
originControl: false,
|
|
148
|
+
} } : {}),
|
|
149
|
+
channel: session.metadata?.channelKey || session.channel,
|
|
150
|
+
channelType, channelId: session.channelId, selfAID: session.selfAID,
|
|
151
|
+
baseagent: session.baseagent, threadId: session.threadId,
|
|
152
|
+
chatType: session.chatType === 'group' ? 'group' : 'private',
|
|
153
|
+
groupId: session.chatType === 'group' ? session.metadata?.groupId || session.channelId : undefined,
|
|
154
|
+
peerId, peerName, peerType,
|
|
155
|
+
encrypted, content: TASK_RECOVERY_PROMPT,
|
|
156
|
+
source: request.message?.source ?? 'recovery',
|
|
157
|
+
messageId: request.message?.messageId ?? `recovery:${session.id}:${recovery.generation}:${task?.taskId ?? session.processingState}`,
|
|
158
|
+
timestamp: Date.now(), boundSessionId: session.id, taskRecovery: recovery,
|
|
159
|
+
replyContext: { ...replyContext, metadata: { ...replyContext?.metadata, encrypted } },
|
|
160
|
+
};
|
|
161
|
+
// Keep the completion promise alive for Trigger cancellation, but release
|
|
162
|
+
// the dequeue gate as soon as admission is durable (never await the run).
|
|
163
|
+
let admitted;
|
|
164
|
+
const persisted = new Promise(resolve => { admitted = resolve; });
|
|
165
|
+
const completion = this.deps.queue.enqueue(session.id, message, session.projectPath, {
|
|
166
|
+
interruptible: false, onPersisted: admitted, selfAID: session.selfAID,
|
|
167
|
+
agentName: session.selfAID, sessionKeyField: session.sessionKey, signal: request.signal,
|
|
168
|
+
});
|
|
169
|
+
await Promise.race([persisted, completion]);
|
|
170
|
+
// Manual callers need only the acknowledgement. Trigger callers retain
|
|
171
|
+
// completion to observe failure and wait for the interrupt barrier.
|
|
172
|
+
void completion.catch(() => { });
|
|
173
|
+
return { ...result('queued_recovery', '✓ 已排入恢复队列,将继续原 baseagent 会话'), completion };
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/** IPC has authenticated the transport; this enforces the Agent/task boundary. */
|
|
2
|
+
export async function executeTaskResume(request, deps) {
|
|
3
|
+
const authorization = request.sessionId
|
|
4
|
+
? await deps.authorizeManaged({
|
|
5
|
+
sessionId: request.sessionId,
|
|
6
|
+
delegationToken: request.delegationToken,
|
|
7
|
+
delegationCommandHash: request.delegationCommandHash,
|
|
8
|
+
}, 'ec.task.resume')
|
|
9
|
+
: undefined;
|
|
10
|
+
if (authorization && !authorization.ok)
|
|
11
|
+
return authorization;
|
|
12
|
+
if (!authorization && !request.localControl) {
|
|
13
|
+
return { ok: false, code: 'INVALID_CONTROL_TOKEN', error: 'valid local control token is required' };
|
|
14
|
+
}
|
|
15
|
+
if (authorization && !authorization.management && authorization.dataScope !== 'daemon') {
|
|
16
|
+
return { ok: false, code: 'NOT_ALLOWED', error: 'task resume requires Agent management authority' };
|
|
17
|
+
}
|
|
18
|
+
const target = await deps.sessions.getSessionById(request.targetSessionId);
|
|
19
|
+
if (!target || target.deletedAt || !target.selfAID) {
|
|
20
|
+
return { ok: false, code: 'SESSION_NOT_FOUND', error: 'target Agent session not found' };
|
|
21
|
+
}
|
|
22
|
+
if (authorization && authorization.dataScope !== 'daemon' && target.selfAID !== authorization.aid) {
|
|
23
|
+
return { ok: false, code: 'SCOPE_MISMATCH', error: 'only sessions of the current Agent can be resumed' };
|
|
24
|
+
}
|
|
25
|
+
// Identity, encryption, task version and backend binding are read from the
|
|
26
|
+
// target's durable state. None may be overridden by the CLI payload.
|
|
27
|
+
const { completion: _completion, ...result } = await deps.recover({
|
|
28
|
+
sessionId: target.id,
|
|
29
|
+
agentAid: target.selfAID,
|
|
30
|
+
});
|
|
31
|
+
return result;
|
|
32
|
+
}
|
|
@@ -2,10 +2,10 @@ import fs from 'fs';
|
|
|
2
2
|
import path from 'path';
|
|
3
3
|
import { TextDecoder } from 'util';
|
|
4
4
|
import { getAidStore, isValidAid, loadClient, SLOT } from '../aun/aid/index.js';
|
|
5
|
-
import { downloadGroupFsBytes, GroupFsDownloadError } from '../aun/group
|
|
6
|
-
import { checkGroupIndex as checkGroupIndexRpc, getGroupIndex as getGroupIndexRpc } from '../aun/msg/group-index.js';
|
|
5
|
+
import { downloadGroupFsBytes, GroupFsDownloadError, checkGroupIndex as checkGroupIndexRpc, getGroupIndex as getGroupIndexRpc, } from '../aun/group/index.js';
|
|
7
6
|
import { agentRelationsDir } from '../paths.js';
|
|
8
7
|
import { formatPeerKey } from '../core/relation/peer-identity.js';
|
|
8
|
+
import { syncRelationSharedFile } from '../core/relation/shared-data.js';
|
|
9
9
|
import { atomicWriteBytes } from '../utils/atomic-write.js';
|
|
10
10
|
import { logger } from '../utils/logger.js';
|
|
11
11
|
import { invalidateKitCache } from './kit-renderer.js';
|
|
@@ -23,8 +23,10 @@ export async function syncGroupRulesContext(opts) {
|
|
|
23
23
|
const group = lazyGroupClient(selfAid);
|
|
24
24
|
try {
|
|
25
25
|
const resolved = await resolveGroupRules(group.client, groupId, paths.localRulesPath);
|
|
26
|
-
if (resolved.changed)
|
|
26
|
+
if (resolved.changed) {
|
|
27
|
+
syncRelationSharedFile(selfAid, paths.peerKey, RULES_LOCAL_PATH);
|
|
27
28
|
invalidateKitCache();
|
|
29
|
+
}
|
|
28
30
|
return {
|
|
29
31
|
groupRulesPath: resolved.usable ? paths.localRulesPath : undefined,
|
|
30
32
|
groupRulesStatus: resolved.status,
|
package/dist/eck/kit-renderer.js
CHANGED
|
@@ -228,6 +228,7 @@ const PARAM_DESCRIPTIONS = {
|
|
|
228
228
|
channel: '渠道类型(aun/feishu/wechat/dingtalk/qqbot/wecom)',
|
|
229
229
|
venueUid: '场所唯一标识(预留)',
|
|
230
230
|
mentionMode: '群 @ 处理模式(mention-only=被@才响应 / disabled=所有消息都响应)',
|
|
231
|
+
groupHistoryAvailable: '本地群历史读取能力是否可用',
|
|
231
232
|
clientType: '客户端类型(desktop/web/mobile)',
|
|
232
233
|
permissionMode: '权限模式(readonly/auto/request/bypass;旧值会安全迁移)',
|
|
233
234
|
capabilities: '当前渠道支持的能力列表',
|