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
|
@@ -1,5 +1,9 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { createHash } from 'node:crypto';
|
|
1
3
|
import { isValidAid } from '../../aun/aid/validation.js';
|
|
4
|
+
import { atomicReadJson, atomicWriteJson } from '../../utils/atomic-write.js';
|
|
2
5
|
import { remoteAccessService } from '../auth/remote-access-store.js';
|
|
6
|
+
import { KeyedFairMutex } from '../handoff/mutex.js';
|
|
3
7
|
export const DIAGNOSTIC_AGENT_AID = 'diag.agentid.cn';
|
|
4
8
|
/** IPC verifies local control authority; managed calls must independently prove their actor. */
|
|
5
9
|
export async function executeDiagIpc(service, registry, owners, cmd) {
|
|
@@ -55,11 +59,17 @@ export class DiagService {
|
|
|
55
59
|
context;
|
|
56
60
|
send;
|
|
57
61
|
tokens;
|
|
62
|
+
sessionControlTail = Promise.resolve();
|
|
58
63
|
constructor(context, send, tokens = remoteAccessService) {
|
|
59
64
|
this.context = context;
|
|
60
65
|
this.send = send;
|
|
61
66
|
this.tokens = tokens;
|
|
62
67
|
}
|
|
68
|
+
withSessionControl(operation) {
|
|
69
|
+
const pending = this.sessionControlTail.then(operation, operation);
|
|
70
|
+
this.sessionControlTail = pending.then(() => undefined, () => undefined);
|
|
71
|
+
return pending;
|
|
72
|
+
}
|
|
63
73
|
/** Local CLI authority is checked by IPC; slash identity is authenticated AUN peerId. */
|
|
64
74
|
async execute(command, actorAid) {
|
|
65
75
|
const { controlAid, owners } = this.context();
|
|
@@ -72,47 +82,302 @@ export class DiagService {
|
|
|
72
82
|
if (!previous)
|
|
73
83
|
return { ok: true, message: '当前没有诊断授权' };
|
|
74
84
|
const diag = previous.diagnostic;
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
85
|
+
return this.withSessionControl(async () => {
|
|
86
|
+
try {
|
|
87
|
+
await this.send(diag.agentAid, [
|
|
88
|
+
'诊断事件:cancel',
|
|
89
|
+
'诊断授权已取消。',
|
|
90
|
+
`目标 daemon:${controlAid}`,
|
|
91
|
+
`case:${diag.caseId}`,
|
|
92
|
+
`沟通 Owner:${diag.ownerAid}`,
|
|
93
|
+
'请停止该 case 的后续 Menu 访问,并告知 Owner 该 case 已取消。',
|
|
94
|
+
'不要把本通知当作新的诊断请求;请向 Owner 发送取消确认。',
|
|
95
|
+
].join('\n'));
|
|
96
|
+
}
|
|
97
|
+
catch {
|
|
98
|
+
return { ok: true, code: 'DIAG_STOP_NOTICE_FAILED', message: '诊断授权已撤销;取消通知发送失败,诊断 Agent 后续访问将被拒绝' };
|
|
99
|
+
}
|
|
100
|
+
return { ok: true, message: '诊断授权已撤销,已通知诊断 Agent 停止该 case;远端执行状态未确认' };
|
|
101
|
+
});
|
|
82
102
|
}
|
|
83
103
|
const ownerAid = actorAid ?? command.ownerAid ?? owners[0];
|
|
84
104
|
if (!ownerAid || !isValidAid(ownerAid) || !owners.includes(ownerAid))
|
|
85
105
|
return { ok: false, code: 'DAEMON_OWNER_REQUIRED', message: '请配置本机 owners,或指定其中一个有效 Owner AID' };
|
|
86
106
|
if (!command.problem?.trim() || command.problem.length > 16_000)
|
|
87
107
|
return { ok: false, code: 'INVALID_ARGUMENT', message: '请提供 1 到 16000 字符的问题描述' };
|
|
108
|
+
const appendExisting = async (existing) => {
|
|
109
|
+
if (!existing.diagnostic)
|
|
110
|
+
return { ok: false, code: 'DIAG_AUTH_FAILED', message: '当前诊断授权记录无效' };
|
|
111
|
+
if (existing.diagnostic.ownerAid !== ownerAid) {
|
|
112
|
+
return { ok: false, code: 'DIAG_OWNER_MISMATCH', message: `当前诊断已绑定沟通 Owner ${existing.diagnostic.ownerAid},不能由 ${ownerAid} 追加信息` };
|
|
113
|
+
}
|
|
114
|
+
const { caseId } = existing.diagnostic;
|
|
115
|
+
const append = [
|
|
116
|
+
'诊断事件:append',
|
|
117
|
+
'当前诊断 case 的追加信息(不要新建诊断授权或 case):',
|
|
118
|
+
`目标 daemon:${controlAid}`,
|
|
119
|
+
`沟通 Owner:${ownerAid}`,
|
|
120
|
+
`case:${caseId}`,
|
|
121
|
+
`问题补充:\n${command.problem.trim()}`,
|
|
122
|
+
'请在现有诊断上下文中处理;继续使用原有授权,必要时向 Owner 汇报新增影响。',
|
|
123
|
+
].join('\n');
|
|
124
|
+
return this.withSessionControl(async () => {
|
|
125
|
+
if (this.tokens.getDiagnosticToken(controlAid)?.token !== existing.token) {
|
|
126
|
+
return { ok: false, code: 'DIAG_CANCELLED', caseId, ownerAid, agentAid: existing.diagnostic.agentAid, message: '追加信息发送前诊断授权已取消' };
|
|
127
|
+
}
|
|
128
|
+
try {
|
|
129
|
+
await this.send(existing.diagnostic.agentAid, append);
|
|
130
|
+
}
|
|
131
|
+
catch (error) {
|
|
132
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
133
|
+
return { ok: false, code: 'DIAG_APPEND_FAILED', caseId, ownerAid, agentAid: existing.diagnostic.agentAid,
|
|
134
|
+
message: `诊断追加信息发送失败:${reason};原诊断授权仍保持有效` };
|
|
135
|
+
}
|
|
136
|
+
if (this.tokens.getDiagnosticToken(controlAid)?.token !== existing.token) {
|
|
137
|
+
return { ok: false, code: 'DIAG_CANCELLED', caseId, ownerAid, agentAid: existing.diagnostic.agentAid, message: '追加信息发送期间诊断授权已取消' };
|
|
138
|
+
}
|
|
139
|
+
return { ok: true, code: 'DIAG_APPENDED', caseId, ownerAid, agentAid: existing.diagnostic.agentAid,
|
|
140
|
+
message: `已将补充信息追加到现有诊断 case ${caseId},未创建新的诊断授权` };
|
|
141
|
+
});
|
|
142
|
+
};
|
|
143
|
+
const existing = this.tokens.getDiagnosticToken(controlAid);
|
|
144
|
+
if (existing?.diagnostic)
|
|
145
|
+
return appendExisting(existing);
|
|
88
146
|
let record;
|
|
89
147
|
try {
|
|
90
148
|
record = this.tokens.issueDiagnosticToken(controlAid, DIAGNOSTIC_AGENT_AID, ownerAid);
|
|
91
149
|
}
|
|
92
150
|
catch (error) {
|
|
151
|
+
// Another request may have reserved the grant after the inspection above.
|
|
152
|
+
// Treat that reservation as the same case and append instead of surfacing a duplicate error.
|
|
153
|
+
const current = error?.code === 'DIAG_ALREADY_ACTIVE' ? this.tokens.getDiagnosticToken(controlAid) : undefined;
|
|
154
|
+
if (current?.diagnostic)
|
|
155
|
+
return appendExisting(current);
|
|
93
156
|
return { ok: false, code: error.code ?? 'DIAG_AUTH_FAILED', message: error.message };
|
|
94
157
|
}
|
|
95
158
|
const caseId = record.diagnostic.caseId;
|
|
96
|
-
const
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
159
|
+
const sessionName = `diag-${caseId.slice(0, 8)}`;
|
|
160
|
+
return this.withSessionControl(async () => {
|
|
161
|
+
if (this.tokens.getDiagnosticToken(controlAid)?.token !== record.token) {
|
|
162
|
+
return { ok: false, code: 'DIAG_CANCELLED', message: '邀请发送前授权已撤销,本次诊断已取消' };
|
|
163
|
+
}
|
|
164
|
+
const invitation = [
|
|
165
|
+
'诊断事件:new',
|
|
166
|
+
'请协助诊断以下主机,并主动联系指定 Owner 展示进展、追问和报告。',
|
|
167
|
+
`目标 daemon Control AID:${controlAid}`, `沟通 Owner AID:${ownerAid}`, `case:${caseId}`,
|
|
168
|
+
`诊断创建时间(Unix毫秒):${record.createdAt}`,
|
|
169
|
+
`远程访问 Token:${record.token}`,
|
|
170
|
+
'请通过标准 Menu 调用诊断;Token 放入 E2EE 受保护头。权限复用该主机既有 Control Menu,包含 restart。有效访问滑动续期,空闲 60 分钟失效。',
|
|
171
|
+
`诊断端应在处理前按 case 建立或选择独立 EvolCore 会话「${sessionName}」,不得恢复或复用其他 case 的 Codex 执行会话。`,
|
|
172
|
+
'先给 Owner 发开始消息,使用通用 handoff 保留目标、Token、问题和发现;无需等待 Owner 回复即可开始。授权失效后停止访问。若会话隔离失败,先告知 Owner 并暂停 Menu 访问。',
|
|
173
|
+
`问题描述:\n${command.problem.trim()}`,
|
|
174
|
+
].join('\n');
|
|
175
|
+
try {
|
|
176
|
+
await this.send(DIAGNOSTIC_AGENT_AID, invitation);
|
|
177
|
+
}
|
|
178
|
+
catch (error) {
|
|
179
|
+
this.tokens.revokeDiagnosticToken(controlAid, record.token);
|
|
180
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
181
|
+
return { ok: false, code: 'DIAG_INVITATION_FAILED', message: `未能确认诊断邀请发送成功:${reason}。本次授权已撤销,请重试` };
|
|
182
|
+
}
|
|
183
|
+
// A stop may race with delivery; never report an already revoked grant as active.
|
|
184
|
+
if (this.tokens.getDiagnosticToken(controlAid)?.token !== record.token)
|
|
185
|
+
return { ok: false, code: 'DIAG_CANCELLED', message: '邀请发送期间授权已撤销,本次诊断已取消' };
|
|
186
|
+
return { ok: true, caseId, ownerAid, agentAid: DIAGNOSTIC_AGENT_AID,
|
|
187
|
+
message: `诊断邀请已发出;诊断 Agent 收到后会按 case 建立独立执行会话,并联系 ${ownerAid}。` };
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
192
|
+
const INVITATION = '请协助诊断以下主机,并主动联系指定 Owner 展示进展、追问和报告。';
|
|
193
|
+
const APPEND = '当前诊断 case 的追加信息(不要新建诊断授权或 case):';
|
|
194
|
+
const diagnosticLocks = new KeyedFairMutex();
|
|
195
|
+
const diagnosticLedgerPath = (chatDir) => path.join(chatDir, 'diagnostic-cases.json');
|
|
196
|
+
export const diagnosticTokenHash = (token) => createHash('sha256').update(token).digest('hex');
|
|
197
|
+
/** Parse only the generated header, never quoted text or the user's problem. */
|
|
198
|
+
export function parseDiagnosticEvent(content) {
|
|
199
|
+
const text = content.trim().replace(/\r\n/g, '\n');
|
|
200
|
+
const first = text.split('\n', 1)[0];
|
|
201
|
+
const explicit = /^诊断事件[::]\s*(new|append|cancel)$/.exec(first)?.[1];
|
|
202
|
+
if (first.startsWith('诊断事件') && !explicit)
|
|
203
|
+
throw new Error('未知诊断事件,未选择执行会话');
|
|
204
|
+
// Existing released senders need no new slash command or protocol support.
|
|
205
|
+
const kind = explicit ?? (first === INVITATION ? 'new' : first === APPEND ? 'append'
|
|
206
|
+
: text.startsWith('诊断授权已取消。') ? 'cancel' : undefined);
|
|
207
|
+
if (!kind)
|
|
208
|
+
return undefined;
|
|
209
|
+
const header = text.split(/^问题(?:描述|补充)[::]/m, 1)[0];
|
|
210
|
+
const legacyCancel = !explicit && kind === 'cancel';
|
|
211
|
+
const fields = legacyCancel ? header.replace(/^诊断授权已取消。/, '').replace(/;/g, '\n') : header;
|
|
212
|
+
const field = (label) => {
|
|
213
|
+
const matches = [...fields.matchAll(new RegExp(`^${label}[::]([^\\n]*)$`, 'gm'))];
|
|
214
|
+
return matches.length === 1 ? matches[0][1].trim() : undefined;
|
|
215
|
+
};
|
|
216
|
+
const caseId = field('case')?.toLowerCase();
|
|
217
|
+
const controlAid = field('目标 daemon(?: Control AID)?');
|
|
218
|
+
const ownerAid = legacyCancel
|
|
219
|
+
? field('沟通 Owner')?.split('。', 1)[0]
|
|
220
|
+
: field('沟通 Owner(?: AID)?');
|
|
221
|
+
if (!caseId || !UUID.test(caseId) || !controlAid || !isValidAid(controlAid) || !ownerAid || !isValidAid(ownerAid)) {
|
|
222
|
+
throw new Error('诊断通知字段无效,未选择执行会话');
|
|
223
|
+
}
|
|
224
|
+
if (kind !== 'new')
|
|
225
|
+
return { kind, caseId, controlAid, ownerAid };
|
|
226
|
+
const token = field('远程访问 Token');
|
|
227
|
+
if (!token || !/^[0-9a-f]{64}$/i.test(token))
|
|
228
|
+
throw new Error('诊断邀请缺少有效 Token,未选择执行会话');
|
|
229
|
+
const created = field('诊断创建时间(Unix毫秒)');
|
|
230
|
+
const createdAt = created === undefined ? undefined : Number(created);
|
|
231
|
+
if (createdAt !== undefined && (!Number.isSafeInteger(createdAt) || createdAt <= 0))
|
|
232
|
+
throw new Error('诊断创建时间无效');
|
|
233
|
+
return { kind, caseId, controlAid, ownerAid, tokenHash: diagnosticTokenHash(token), createdAt };
|
|
234
|
+
}
|
|
235
|
+
function readDiagnosticLedger(chatDir) {
|
|
236
|
+
const value = atomicReadJson(diagnosticLedgerPath(chatDir));
|
|
237
|
+
if (!value)
|
|
238
|
+
return { version: 1, cases: {} };
|
|
239
|
+
if (value.version !== 1 || !value.cases || typeof value.cases !== 'object' || Array.isArray(value.cases)
|
|
240
|
+
|| Object.entries(value.cases).some(([key, record]) => !UUID.test(key) || !record
|
|
241
|
+
|| !['active', 'closed'].includes(record.state) || !isValidAid(record.ownerAid))) {
|
|
242
|
+
throw new Error('诊断会话记录损坏,已阻止恢复旧会话');
|
|
243
|
+
}
|
|
244
|
+
return value;
|
|
245
|
+
}
|
|
246
|
+
/** Called again at execution and Menu boundaries, including delayed/recovered work. */
|
|
247
|
+
export function isDiagnosticSessionActive(session, chatDir) {
|
|
248
|
+
if (session.metadata?.diagnosticStatus === 'cancelled')
|
|
249
|
+
return false;
|
|
250
|
+
const binding = session.metadata?.diagnostic;
|
|
251
|
+
if (!binding)
|
|
252
|
+
return true;
|
|
253
|
+
const ledger = readDiagnosticLedger(chatDir);
|
|
254
|
+
const record = ledger.cases[binding.caseId];
|
|
255
|
+
return ledger.activeCaseId === binding.caseId && record?.state === 'active'
|
|
256
|
+
&& record.sessionId === session.id && record.tokenHash === binding.tokenHash
|
|
257
|
+
&& record.ownerAid === binding.ownerAid && session.channelId === binding.controlAid;
|
|
258
|
+
}
|
|
259
|
+
/** Invalid authorization is terminal; it must not be revived by a retry/append. */
|
|
260
|
+
export function closeDiagnosticSession(session, chatDir) {
|
|
261
|
+
const binding = session.metadata?.diagnostic;
|
|
262
|
+
if (!binding)
|
|
263
|
+
return;
|
|
264
|
+
const ledger = readDiagnosticLedger(chatDir);
|
|
265
|
+
const record = ledger.cases[binding.caseId];
|
|
266
|
+
if (record?.sessionId !== session.id)
|
|
267
|
+
return;
|
|
268
|
+
record.state = 'closed';
|
|
269
|
+
atomicWriteJson(diagnosticLedgerPath(chatDir), ledger);
|
|
270
|
+
}
|
|
271
|
+
/** The bridge already applies normal authentication/admission before calling this. */
|
|
272
|
+
export class DiagnosticSessionRouter {
|
|
273
|
+
sessions;
|
|
274
|
+
stopSession;
|
|
275
|
+
constructor(sessions, stopSession) {
|
|
276
|
+
this.sessions = sessions;
|
|
277
|
+
this.stopSession = stopSession;
|
|
278
|
+
}
|
|
279
|
+
async route(input) {
|
|
280
|
+
const { event, message, channel, channelType, selfAid, chatDir } = input;
|
|
281
|
+
if (channelType !== 'aun' || selfAid !== DIAGNOSTIC_AGENT_AID || message.chatType !== 'private'
|
|
282
|
+
|| message.threadId || (message.source !== undefined && message.source !== 'user')
|
|
283
|
+
|| !(message.encrypted ?? message.replyContext?.metadata?.encrypted)
|
|
284
|
+
|| message.peerId !== event.controlAid || message.channelId !== event.controlAid || !message.messageId) {
|
|
285
|
+
throw new Error('诊断通知来源或会话不匹配,未执行会话切换');
|
|
111
286
|
}
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
287
|
+
return diagnosticLocks.forKey(diagnosticLedgerPath(chatDir)).runExclusive(async () => {
|
|
288
|
+
const all = await this.sessions.listSessions(channel, message.channelId, { archiveFilter: 'all' });
|
|
289
|
+
const ledger = readDiagnosticLedger(chatDir);
|
|
290
|
+
const find = (caseId) => all.find(s => !s.threadId && s.selfAID === selfAid
|
|
291
|
+
&& s.metadata?.diagnostic?.caseId === caseId && s.metadata.diagnostic.controlAid === event.controlAid);
|
|
292
|
+
let record = ledger.cases[event.caseId];
|
|
293
|
+
if (record && record.ownerAid !== event.ownerAid)
|
|
294
|
+
throw new Error('诊断通知 Owner 与原授权不一致');
|
|
295
|
+
if (event.kind === 'cancel') {
|
|
296
|
+
let session = find(event.caseId);
|
|
297
|
+
if (!session) {
|
|
298
|
+
const legacy = this.sessions.getActiveSessionSync(channel, message.channelId, channelType, selfAid);
|
|
299
|
+
if (legacy && !legacy.threadId && !legacy.metadata?.diagnostic) {
|
|
300
|
+
await this.sessions.patchSessionMetadata(legacy.id, { diagnosticStatus: 'cancelled' });
|
|
301
|
+
session = legacy;
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
// Keep a tombstone even if cancellation overtook the first invitation.
|
|
305
|
+
ledger.cases[event.caseId] = { ...record, ownerAid: event.ownerAid, state: 'closed', sessionId: record?.sessionId ?? session?.id };
|
|
306
|
+
atomicWriteJson(diagnosticLedgerPath(chatDir), ledger);
|
|
307
|
+
if (session)
|
|
308
|
+
await this.stopSession(session.id);
|
|
309
|
+
return { notice: '已关闭该诊断并停止其待处理任务。' };
|
|
310
|
+
}
|
|
311
|
+
if (record?.state === 'closed')
|
|
312
|
+
return { notice: '该诊断已结束,旧邀请和追加消息不会重新执行。' };
|
|
313
|
+
if (event.kind === 'append' && !record)
|
|
314
|
+
throw new Error('未找到该诊断的独立会话,请停止诊断后重新发起');
|
|
315
|
+
if (record && event.kind === 'new' && record.tokenHash !== event.tokenHash)
|
|
316
|
+
throw new Error('相同诊断的授权不一致,未恢复会话');
|
|
317
|
+
if (!record) {
|
|
318
|
+
if (find(event.caseId))
|
|
319
|
+
throw new Error('诊断生命周期记录缺失,拒绝恢复旧会话');
|
|
320
|
+
const sequence = Number.isSafeInteger(message.gatewaySeq) ? message.gatewaySeq : undefined;
|
|
321
|
+
const last = ledger.activeCaseId ? ledger.cases[ledger.activeCaseId] : undefined;
|
|
322
|
+
const stale = last && (sequence !== undefined && last.sequence !== undefined
|
|
323
|
+
? sequence <= last.sequence
|
|
324
|
+
: event.createdAt !== undefined && last.createdAt !== undefined
|
|
325
|
+
? event.createdAt <= last.createdAt : true);
|
|
326
|
+
if (stale)
|
|
327
|
+
return { notice: '邀请顺序已过期或无法确认,未恢复旧诊断。' };
|
|
328
|
+
// Persist all revocations before interruption/creation. A crash here
|
|
329
|
+
// cannot let a restored old task use its previous authorization.
|
|
330
|
+
for (const previous of Object.values(ledger.cases))
|
|
331
|
+
previous.state = 'closed';
|
|
332
|
+
record = { state: 'active', ownerAid: event.ownerAid, tokenHash: event.tokenHash, sequence, createdAt: event.createdAt };
|
|
333
|
+
ledger.cases[event.caseId] = record;
|
|
334
|
+
ledger.activeCaseId = event.caseId;
|
|
335
|
+
atomicWriteJson(diagnosticLedgerPath(chatDir), ledger);
|
|
336
|
+
}
|
|
337
|
+
if (ledger.activeCaseId !== event.caseId)
|
|
338
|
+
return { notice: '该诊断已被新诊断替代。' };
|
|
339
|
+
const oldActive = this.sessions.getActiveSessionSync(channel, message.channelId, channelType, selfAid);
|
|
340
|
+
if (!record.cleanupComplete && oldActive && !oldActive.threadId && !oldActive.metadata?.diagnostic) {
|
|
341
|
+
// Migration from the historical shared Control conversation. Owner
|
|
342
|
+
// conversations and sessions belonging to other hosts are untouched.
|
|
343
|
+
await this.sessions.patchSessionMetadata(oldActive.id, { diagnosticStatus: 'cancelled' });
|
|
344
|
+
await this.stopSession(oldActive.id);
|
|
345
|
+
}
|
|
346
|
+
// Retry interruption too after an interrupted creation attempt.
|
|
347
|
+
for (const previous of record.cleanupComplete ? [] : all) {
|
|
348
|
+
if (!previous.threadId && previous.metadata?.diagnostic?.controlAid === event.controlAid
|
|
349
|
+
&& previous.metadata.diagnostic.caseId !== event.caseId)
|
|
350
|
+
await this.stopSession(previous.id);
|
|
351
|
+
}
|
|
352
|
+
let session = find(event.caseId);
|
|
353
|
+
if (record.sessionId && (!session || record.sessionId !== session.id))
|
|
354
|
+
throw new Error('诊断会话绑定缺失,拒绝回退到历史会话');
|
|
355
|
+
if (!session) {
|
|
356
|
+
if (event.kind !== 'new' || !input.baseagent)
|
|
357
|
+
throw new Error('无法创建诊断执行会话');
|
|
358
|
+
const metadata = {
|
|
359
|
+
channelKey: channel, peerId: event.controlAid, peerName: message.peerName,
|
|
360
|
+
diagnostic: { caseId: event.caseId, controlAid: event.controlAid, ownerAid: event.ownerAid, tokenHash: record.tokenHash },
|
|
361
|
+
};
|
|
362
|
+
// The case binding is committed in the first session snapshot. Replay
|
|
363
|
+
// can find it even if the process died before saving the ledger pointer.
|
|
364
|
+
session = await this.sessions.createNewSession(channel, message.channelId, input.projectPath, `diag-${event.caseId.slice(0, 8)}`, input.baseagent, metadata);
|
|
365
|
+
if (session.agentSessionId || session.metadata?.agentSessions || session.metadata?.resumeAt) {
|
|
366
|
+
throw new Error('诊断会话错误继承了旧执行上下文');
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
if (session.metadata?.diagnostic?.tokenHash !== record.tokenHash)
|
|
370
|
+
throw new Error('诊断会话授权绑定不一致');
|
|
371
|
+
// A concurrent Menu response can close this case while we await runner
|
|
372
|
+
// interruption/session creation. Never overwrite that terminal state.
|
|
373
|
+
const latest = readDiagnosticLedger(chatDir);
|
|
374
|
+
const current = latest.cases[event.caseId];
|
|
375
|
+
if (latest.activeCaseId !== event.caseId || current?.state !== 'active')
|
|
376
|
+
return { notice: '该诊断已结束。' };
|
|
377
|
+
current.sessionId = session.id;
|
|
378
|
+
current.cleanupComplete = true;
|
|
379
|
+
atomicWriteJson(diagnosticLedgerPath(chatDir), latest);
|
|
380
|
+
return { session };
|
|
381
|
+
});
|
|
117
382
|
}
|
|
118
383
|
}
|
|
@@ -2,7 +2,7 @@ import crypto from 'crypto';
|
|
|
2
2
|
import { ConfigTarget, mutateConfig, read as readConfig, resolveEffectiveFieldWithSource, } from '../../config/config-manager.js';
|
|
3
3
|
import { normalizeMentionMode } from '../../config/mention-mode.js';
|
|
4
4
|
import { readRolesConfig } from '../../config/roles.js';
|
|
5
|
-
import { groupInfo, groupList, groupDirectoryErrorCode } from '../../aun/
|
|
5
|
+
import { groupInfo, groupList, groupDirectoryErrorCode, normalizeGroupAid } from '../../aun/group/index.js';
|
|
6
6
|
import { formatPeerKey, parsePeerKey } from '../relation/peer-identity.js';
|
|
7
7
|
const DEFAULT_DIRECTORY = {
|
|
8
8
|
getGroup: groupInfo,
|
|
@@ -185,7 +185,12 @@ function groupIdFromArgs(args, currentGroupId) {
|
|
|
185
185
|
if (!groupId) {
|
|
186
186
|
throw new GroupMenuError('INVALID_ARGUMENT', 'relation settings require args.groupId or args.peerKey');
|
|
187
187
|
}
|
|
188
|
-
|
|
188
|
+
try {
|
|
189
|
+
return normalizeGroupAid(groupId);
|
|
190
|
+
}
|
|
191
|
+
catch (error) {
|
|
192
|
+
throw new GroupMenuError('INVALID_ARGUMENT', error.message);
|
|
193
|
+
}
|
|
189
194
|
}
|
|
190
195
|
function settingOptions(key, target) {
|
|
191
196
|
const state = readSettingState(key, target);
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { createHash } from 'node:crypto';
|
|
2
|
-
import { groupInfo, groupMembers } from '../../aun/
|
|
2
|
+
import { groupInfo, groupMembers } from '../../aun/group/index.js';
|
|
3
3
|
import { formatPeerKey } from '../relation/peer-identity.js';
|
|
4
4
|
import { authorizeAccess, authorizeOperation, buildAuthSubject } from '../auth/auth-gateway.js';
|
|
5
5
|
import { authorizationDenialData, formatAuthorizationDenial } from '../auth/authorization-denial.js';
|
|
@@ -8,6 +8,7 @@ import { readRolesConfig } from '../../config/roles.js';
|
|
|
8
8
|
import { normalizeMenuError } from './menu-protocol.js';
|
|
9
9
|
import { isValidAid } from '../../aun/aid/validation.js';
|
|
10
10
|
const DEFAULT_DIRECTORY = { getGroup: groupInfo, getMembers: groupMembers };
|
|
11
|
+
const HOST_BASEAGENTS = ['claude', 'codex', 'ecagent', 'gemini'];
|
|
11
12
|
export function validateMenuValueBatchArgs(args) {
|
|
12
13
|
if (!args || typeof args !== 'object' || Array.isArray(args))
|
|
13
14
|
throw { code: 'INVALID_ARGUMENT', message: 'batch args must be an object' };
|
|
@@ -80,11 +81,11 @@ const ENTRIES = [
|
|
|
80
81
|
{ id: 'activity.agent', type: 'setting', contexts: ['agent'], name: 'activity', label: 'Agent 中间输出', section: 'agent', order: 50, query: { cmd: '/activity', args: { scope: 'agent' }, operation: 'activity.current', scope: 'agent' }, options: { cmd: '/activity', args: { scope: 'agent' }, operation: 'activity.current', scope: 'agent' }, update: { cmd: '/activity', args: { scope: 'agent' }, operation: 'activity.update', scope: 'agent' }, control: 'select' },
|
|
81
82
|
{ id: 'observable.agent', type: 'setting', contexts: ['agent'], name: 'observable', label: '观察模式', section: 'agent', order: 60, query: { cmd: '/observable', operation: 'observable.current', scope: 'agent' }, update: { cmd: '/observable', operation: 'observable.update', scope: 'agent' }, valueSchema: { type: 'boolean' }, control: 'toggle' },
|
|
82
83
|
{ id: 'aid.avatar', type: 'action', contexts: ['agent'], name: 'aid', label: '更换头像', section: 'agent', order: 70, action: { cmd: '/aid', action: 'avatar', operation: 'ec.aid.avatar', scope: 'agent' }, valueSchema: { type: 'object', properties: { data: { type: 'string' }, url: { type: 'string', format: 'uri' } }, oneOf: [{ required: ['data'] }, { required: ['url'] }] }, control: 'action' },
|
|
84
|
+
{ id: 'baseagents.inventory', type: 'list', contexts: ['process'], name: 'baseagents', label: 'Base Agent 清单', section: 'process', order: 9, query: { cmd: '/baseagents', operation: 'system.status', scope: 'process' }, control: 'list' },
|
|
83
85
|
{ id: 'system.status', type: 'list', contexts: ['process'], name: 'system', label: '系统状态', section: 'process', order: 10, query: { cmd: '/system', operation: 'system.status', scope: 'process' }, control: 'list' },
|
|
84
86
|
{ id: 'system.monitor', type: 'list', contexts: ['process'], name: 'system', label: '运行监控快照', section: 'process', order: 11, query: { cmd: '/system', args: { view: 'monitor' }, operation: 'system.status', scope: 'process' }, control: 'list' },
|
|
85
87
|
{ id: 'system.logs', type: 'list', contexts: ['process'], name: 'system', label: 'Daemon 日志', section: 'process', order: 12, query: { cmd: '/system', args: { view: 'logs' }, operation: 'system.status', scope: 'process' }, options: { cmd: '/system', args: { option: 'logs' }, operation: 'system.status', scope: 'process', mode: 'dynamic' }, control: 'list' },
|
|
86
88
|
{ id: 'config.rawFile', type: 'list', contexts: ['process'], name: 'config', label: '原始配置文件', section: 'process', order: 13, query: { cmd: '/config', args: { view: 'rawFile' }, operation: 'config.read', scope: 'process' }, options: { cmd: '/config', args: { option: 'rawFile' }, operation: 'config.read', scope: 'process', mode: 'dynamic' }, control: 'list' },
|
|
87
|
-
{ id: 'baseagent.models', type: 'list', contexts: ['process'], name: 'baseagent-models', label: 'Baseagent 模型目录', section: 'process', order: 41, query: { cmd: '/baseagent-models', args: { baseagent: 'claude' }, operation: 'baseagent.models.list', scope: 'process' }, control: 'list' },
|
|
88
89
|
{ id: 'system.restart', type: 'action', contexts: ['process'], name: 'system', label: '重启服务', section: 'process', order: 20, action: { cmd: '/system', action: 'restart', operation: 'system.restart', scope: 'process' } },
|
|
89
90
|
{ id: 'system.upgrade', type: 'action', contexts: ['process'], name: 'system', label: '升级服务', section: 'process', order: 30, action: { cmd: '/system', action: 'upgrade', operation: 'system.upgrade', scope: 'process' } },
|
|
90
91
|
{ id: 'system.paircode', type: 'action', contexts: ['process'], name: 'system', label: '获取配对码', section: 'process', order: 35, action: { cmd: '/system', action: 'paircode', operation: 'system.paircode', scope: 'process' } },
|
|
@@ -304,6 +305,34 @@ export async function resolveExplicitMenuExecutionContext(input) {
|
|
|
304
305
|
};
|
|
305
306
|
}
|
|
306
307
|
function entryVisible(entry, context) { return entry.contexts.includes(context.kind); }
|
|
308
|
+
function configuredHostBaseagentEntries() {
|
|
309
|
+
const defaults = readConfig(ConfigTarget.Defaults);
|
|
310
|
+
const configured = defaults?.baseagents;
|
|
311
|
+
if (!configured || typeof configured !== 'object' || Array.isArray(configured))
|
|
312
|
+
return [];
|
|
313
|
+
return HOST_BASEAGENTS
|
|
314
|
+
.filter(baseagent => {
|
|
315
|
+
const block = configured[baseagent];
|
|
316
|
+
return Object.hasOwn(configured, baseagent) && block !== null
|
|
317
|
+
&& typeof block === 'object' && !Array.isArray(block);
|
|
318
|
+
})
|
|
319
|
+
.map((baseagent, index) => ({
|
|
320
|
+
// Value batches only carry entry IDs, so each configured backend gets a
|
|
321
|
+
// binding with an explicit backend instead of an implicit Claude default.
|
|
322
|
+
id: `baseagent.models.${baseagent}`,
|
|
323
|
+
type: 'list',
|
|
324
|
+
contexts: ['process'],
|
|
325
|
+
name: 'baseagent-models',
|
|
326
|
+
label: `${baseagent} 模型目录`,
|
|
327
|
+
section: 'process',
|
|
328
|
+
order: 41 + index,
|
|
329
|
+
query: { cmd: '/baseagent-models', args: { baseagent }, operation: 'baseagent.models.list', scope: 'process' },
|
|
330
|
+
control: 'list',
|
|
331
|
+
}));
|
|
332
|
+
}
|
|
333
|
+
function menuEntryDefinitions(context) {
|
|
334
|
+
return context.kind === 'process' ? [...ENTRIES, ...configuredHostBaseagentEntries()] : ENTRIES;
|
|
335
|
+
}
|
|
307
336
|
function intentFor(binding, context) { return { operation: binding.operation, scope: binding.scope, source: 'menu', args: { ...bindingArgs(binding, context) } }; }
|
|
308
337
|
async function operationView(binding, context, options = {}) {
|
|
309
338
|
if (!binding)
|
|
@@ -340,7 +369,8 @@ export async function getMenuCatalog(input) {
|
|
|
340
369
|
const context = await resolveMenuCatalogContext(input);
|
|
341
370
|
const entries = [];
|
|
342
371
|
const paircodeAllowed = input.paircodeAllowed ?? input.channelScope !== 'control';
|
|
343
|
-
|
|
372
|
+
const definitions = menuEntryDefinitions(context);
|
|
373
|
+
for (const entry of definitions.filter(candidate => entryVisible(candidate, context)).sort((a, b) => a.order - b.order)) {
|
|
344
374
|
const operations = {};
|
|
345
375
|
for (const [key, binding] of [['query', entry.query], ['options', entry.options], ['update', entry.update], ['action', entry.action]]) {
|
|
346
376
|
const view = await operationView(binding, context, { paircodeAllowed });
|
|
@@ -378,7 +408,7 @@ export async function execMenuValueBatch(input) {
|
|
|
378
408
|
normalizedItems.push({ itemId, entryId });
|
|
379
409
|
}
|
|
380
410
|
const results = new Array(normalizedItems.length);
|
|
381
|
-
const byId = new Map(
|
|
411
|
+
const byId = new Map(menuEntryDefinitions(context).map(entry => [entry.id, entry]));
|
|
382
412
|
let cursor = 0;
|
|
383
413
|
const resolveItem = async (item) => {
|
|
384
414
|
const { itemId, entryId } = item;
|