evolcore 0.0.23 → 0.0.24
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 +50 -0
- package/README.md +3 -1
- package/bin/codex-managed-acl.ps1 +7 -2
- package/bin/codex-managed-hook.mjs +127 -10
- package/bin/ec-safe-output.js +85 -3
- package/bin/install-codex-managed-hooks.mjs +17 -6
- package/dist/agents/baseagent-availability.js +4 -4
- package/dist/agents/baseagent.js +29 -1
- package/dist/agents/claude-runner.js +148 -13
- package/dist/agents/codex/app-server-client.js +18 -8
- package/dist/agents/codex/catalog.js +1 -1
- package/dist/agents/codex/diagnostics.js +215 -0
- package/dist/agents/codex-runner.js +438 -59
- package/dist/agents/ecagent-runner.js +17 -3
- package/dist/agents/gemini-runner.js +60 -2
- package/dist/agents/runner-types.js +1 -1
- package/dist/aun/aid/managed-operation.js +28 -6
- package/dist/aun/aid/store.js +41 -0
- package/dist/aun/msg/managed-operation.js +6 -1
- package/dist/aun/outbox.js +56 -18
- package/dist/aun/rpc/client.js +4 -5
- package/dist/channels/aun.js +147 -76
- package/dist/channels/daemon.js +7 -1
- package/dist/channels/dingtalk.js +1 -0
- package/dist/channels/feishu.js +10 -4
- package/dist/channels/wecom.js +2 -1
- package/dist/cli/agent-command.js +29 -3
- package/dist/cli/agent-create.js +538 -0
- package/dist/cli/agent.js +26 -675
- package/dist/cli/aun-commands.js +60 -20
- package/dist/cli/ctl-command.js +2 -0
- package/dist/cli/daemon-commands.js +148 -17
- package/dist/cli/index.js +2 -2
- package/dist/cli/managed-command-guard.js +3 -0
- package/dist/cli/restart-monitor.js +432 -8
- package/dist/cli/stats.js +110 -0
- package/dist/config/builtin-roles.js +2 -0
- package/dist/core/auth/auth-gateway.js +3 -2
- package/dist/core/auth/authorization-audit.js +72 -19
- package/dist/core/auth/operation-authorizer.js +63 -3
- package/dist/core/auth/operation-catalog.js +10 -2
- package/dist/core/auth/remote-access-store.js +3 -2
- package/dist/core/baseagent-loader.js +4 -4
- package/dist/core/bootstrap-messages.js +39 -18
- package/dist/core/bootstrap-service.js +32 -26
- package/dist/core/capability/capability-manager.js +8 -4
- package/dist/core/capability/providers/codex-capability-provider.js +1 -1
- package/dist/core/command/agent-control.js +7 -2
- package/dist/core/command/command-handler.js +87 -25
- package/dist/core/command/menu-catalog.js +3 -1
- package/dist/core/command/menu-handler.js +274 -70
- package/dist/core/command/menu-protocol.js +6 -2
- package/dist/core/command/slash-gate.js +1 -3
- package/dist/core/command/slash-handler.js +157 -48
- package/dist/core/data-migration.js +5 -15
- package/dist/core/event-catalog.js +3 -0
- package/dist/core/handoff/runtime.js +3 -1
- package/dist/core/handoff/store.js +7 -1
- package/dist/core/message/im-renderer.js +13 -6
- package/dist/core/message/message-bridge.js +5 -36
- package/dist/core/message/message-utils.js +6 -3
- package/dist/core/message/response-engine.js +73 -21
- package/dist/core/model/catalog-service.js +3 -1
- package/dist/core/model/model-catalog.js +10 -0
- package/dist/core/model/model-permission.js +4 -1
- package/dist/core/permission/approval-gateway.js +169 -33
- package/dist/core/permission/tool-policy.js +13 -6
- package/dist/core/session/adapters/codex-session-file-adapter.js +71 -3
- package/dist/core/session/session-manager.js +49 -13
- package/dist/core/session/session-renew.js +1 -0
- package/dist/index.js +174 -28
- package/dist/ipc.js +68 -21
- package/dist/paths.js +23 -1
- package/dist/stats/billing.js +18 -4
- package/dist/utils/atomic-write.js +26 -2
- package/dist/utils/cross-platform.js +436 -1
- package/dist/utils/error-utils.js +11 -4
- package/dist/utils/model-prices.jsonl +23 -20
- package/dist/utils/npm-ops.js +24 -1
- package/dist/utils/windows-autostart.js +1 -1
- package/kits/templates/roles/admin.json +2 -0
- package/package.json +7 -3
- package/AGENTS.md +0 -62
- package/dist/utils/codex-cli.js +0 -43
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
import { normalizePeer, writeScope } from '../model/config-scope.js';
|
|
2
2
|
import { formatPeerKey } from '../relation/peer-identity.js';
|
|
3
|
-
import { modelMatches, getCatalog, resolveCatalogModel, assertCatalogSelectable } from '../model/model-catalog.js';
|
|
3
|
+
import { modelMatches, getCatalog, getBaseagentCatalog, resolveCatalogModel, assertCatalogSelectable, TRANSPORT_EFFORTS } from '../model/model-catalog.js';
|
|
4
4
|
import { constrainResolvedModelForRole, filterModelsForRole, validateModelSelectionForRole } from '../model/model-permission.js';
|
|
5
5
|
import { hasModelSwitcher } from '../../agents/runner-types.js';
|
|
6
|
-
import { execCodexCliSync, resolveCodexCliPath } from '../../utils/codex-cli.js';
|
|
7
6
|
import { resolvePaths, getPackageRoot, daemonControlDir } from '../../paths.js';
|
|
8
7
|
import { inspectDataMigrationRequirement } from '../data-migration.js';
|
|
9
8
|
import { buildEnvelope, isDeliveryTarget, isDeliveryTargetForChannel, replyContextFromSession } from '../message/message-utils.js';
|
|
@@ -11,11 +10,12 @@ import path from 'path';
|
|
|
11
10
|
import fs from 'fs';
|
|
12
11
|
import os from 'os';
|
|
13
12
|
import crypto from 'crypto';
|
|
14
|
-
import { execFileSync } from 'child_process';
|
|
15
13
|
import { CronExpressionParser } from 'cron-parser';
|
|
16
14
|
import { parseDuration } from '../../trigger/parser.js';
|
|
17
15
|
import { checkLatestVersion, getLocalVersion, isLinkedInstall, compareVersions, resolveGlobalPkg } from '../../utils/npm-ops.js';
|
|
18
|
-
import { commandExists,
|
|
16
|
+
import { commandExists, resolveCliEntry, launchMonitorAndConfirm } from '../../utils/cross-platform.js';
|
|
17
|
+
import { cachedCliVersion } from '../../utils/cross-platform.js';
|
|
18
|
+
import { scanInstances } from '../../utils/instance-registry.js';
|
|
19
19
|
import { isFullAccessEnabled, loadDefaults, loadDaemonConfig } from '../../config-store.js';
|
|
20
20
|
import { WEB_PACKAGE_NAME } from '../../product.js';
|
|
21
21
|
import { read as cfgRead, resolveEffective, resolveEffectiveFieldWithSource, routeFieldPath, write as cfgWrite, ConfigTarget, } from '../../config/config-manager.js';
|
|
@@ -52,32 +52,7 @@ import { probeSessionActivity } from './slash-gate.js';
|
|
|
52
52
|
* 失败返回 null(命令不存在或执行失败)。
|
|
53
53
|
*/
|
|
54
54
|
function getBaseagentVersion(cmd) {
|
|
55
|
-
|
|
56
|
-
const executable = cmd === 'codex' ? resolveCodexCliPath() : cmd;
|
|
57
|
-
if (!executable)
|
|
58
|
-
return null;
|
|
59
|
-
const output = cmd === 'codex'
|
|
60
|
-
? execCodexCliSync(['--version'], {
|
|
61
|
-
encoding: 'utf-8',
|
|
62
|
-
timeout: 3000,
|
|
63
|
-
stdio: ['ignore', 'pipe', 'pipe'],
|
|
64
|
-
})
|
|
65
|
-
: execFileSync(executable, ['--version'], {
|
|
66
|
-
encoding: 'utf-8',
|
|
67
|
-
timeout: 3000,
|
|
68
|
-
stdio: ['ignore', 'pipe', 'pipe'],
|
|
69
|
-
windowsHide: process.platform === 'win32',
|
|
70
|
-
});
|
|
71
|
-
const versionOutput = String(output).trim();
|
|
72
|
-
// claude: "2.1.187 (Claude Code)" → 提取 "2.1.187"
|
|
73
|
-
// gemini: "0.38.0" → 直接返回
|
|
74
|
-
// codex: "codex-cli 0.142.0" → 提取 "0.142.0"
|
|
75
|
-
const match = versionOutput.match(/(\d+\.\d+\.\d+)/);
|
|
76
|
-
return match ? match[1] : null;
|
|
77
|
-
}
|
|
78
|
-
catch {
|
|
79
|
-
return null;
|
|
80
|
-
}
|
|
55
|
+
return cachedCliVersion(cmd);
|
|
81
56
|
}
|
|
82
57
|
const allEfforts = ['none', 'minimal', 'low', 'medium', 'high', 'xhigh', 'max'];
|
|
83
58
|
const PERMISSION_MODE_KEYS = PUBLIC_PERMISSION_MODES;
|
|
@@ -397,7 +372,14 @@ function tokenizeArgv(line) {
|
|
|
397
372
|
return out;
|
|
398
373
|
}
|
|
399
374
|
async function getAvailableEfforts(agent, model) {
|
|
400
|
-
|
|
375
|
+
const dynamic = await agent.getAvailableEfforts?.(model);
|
|
376
|
+
if (Array.isArray(dynamic))
|
|
377
|
+
return dynamic;
|
|
378
|
+
// Keep Menu usable with legacy/embedded runner implementations that do not
|
|
379
|
+
// expose the optional catalog method. Full runners still take the dynamic
|
|
380
|
+
// path above, so this fallback cannot broaden their advertised capabilities.
|
|
381
|
+
const baseagent = agent?.baseagent || agent?.name;
|
|
382
|
+
return (TRANSPORT_EFFORTS[baseagent] ?? []);
|
|
401
383
|
}
|
|
402
384
|
function modelDisplayLabel(agent, model) {
|
|
403
385
|
const full = agent.resolveModelId?.(model);
|
|
@@ -410,6 +392,29 @@ function menuStringArg(args, key) {
|
|
|
410
392
|
const trimmed = value.trim();
|
|
411
393
|
return trimmed || undefined;
|
|
412
394
|
}
|
|
395
|
+
function hostCatalogArgs(args = {}) {
|
|
396
|
+
const invalid = (message) => { throw { code: 'INVALID_ARGUMENT', message }; };
|
|
397
|
+
if (!args || typeof args !== 'object' || Array.isArray(args))
|
|
398
|
+
invalid('Baseagent catalog args must be an object');
|
|
399
|
+
if (Object.keys(args).some(key => !['baseagent', 'ba', 'refresh', 'scope', 'context'].includes(key))) {
|
|
400
|
+
invalid('Baseagent catalog only accepts baseagent, ba, refresh and process context');
|
|
401
|
+
}
|
|
402
|
+
if (args.scope !== undefined && args.scope !== 'process')
|
|
403
|
+
invalid('Baseagent catalog requires process scope');
|
|
404
|
+
if (args.context !== undefined && (!args.context || typeof args.context !== 'object'
|
|
405
|
+
|| Array.isArray(args.context) || args.context.kind !== 'process'
|
|
406
|
+
|| Object.keys(args.context).some(key => key !== 'kind'))) {
|
|
407
|
+
invalid('Baseagent catalog requires process context');
|
|
408
|
+
}
|
|
409
|
+
for (const key of ['baseagent', 'ba']) {
|
|
410
|
+
if (args[key] !== undefined && (typeof args[key] !== 'string' || !args[key].trim())) {
|
|
411
|
+
invalid(`${key} must be a non-empty string`);
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
if (args.refresh !== undefined && typeof args.refresh !== 'boolean')
|
|
415
|
+
invalid('refresh must be a boolean');
|
|
416
|
+
return { baseagent: menuStringArg(args, 'baseagent') ?? menuStringArg(args, 'ba') ?? 'claude', refresh: args.refresh === true };
|
|
417
|
+
}
|
|
413
418
|
const LEGACY_SESSION_CONFIG_ARG_KEYS = [
|
|
414
419
|
'session',
|
|
415
420
|
'sessionId',
|
|
@@ -852,6 +857,8 @@ export const SELF_MANAGE_AGENT_ACTIONS = new Set(['update', 'reload']);
|
|
|
852
857
|
/** 判断 (cmdBase, action) 是否为进程级操作(仅控制 channel 可执行)。
|
|
853
858
|
* /system 全部进程级;/agent 仅 create/delete/enable/disable 进程级;其余关系级。 */
|
|
854
859
|
export function isProcessLevelAction(cmdBase, action) {
|
|
860
|
+
if (cmdBase === '/baseagent-models')
|
|
861
|
+
return true;
|
|
855
862
|
if (cmdBase === '/system')
|
|
856
863
|
return true;
|
|
857
864
|
if (cmdBase === '/gateway')
|
|
@@ -900,6 +907,8 @@ function buildMenuIntent(verb, cmdBase, args, action, value, fromControlChannel
|
|
|
900
907
|
});
|
|
901
908
|
const modelScope = args?.scope === 'agent' ? 'agent' : 'relation';
|
|
902
909
|
if (verb === 'query') {
|
|
910
|
+
if (cmdBase === '/baseagent-models')
|
|
911
|
+
return intent('baseagent.models.list', 'process');
|
|
903
912
|
if (cmdBase === '/pwd')
|
|
904
913
|
return intent('project.current', 'relation');
|
|
905
914
|
if (cmdBase === '/session' || cmdBase === '/s')
|
|
@@ -1107,6 +1116,7 @@ async function authorizeMenuIntent(params) {
|
|
|
1107
1116
|
intent.source = source;
|
|
1108
1117
|
const decision = await authorizeOperation({ source, intent, subject });
|
|
1109
1118
|
if (!decision.allow) {
|
|
1119
|
+
logger.warn(`[MenuAuth] denied operation=${intent.operation} scope=${intent.scope} source=${source} selfAid=${subject.selfAid ?? '<none>'} actor=${subject.actorId ?? '<none>'} role=${subject.role} processRole=${subject.processRole} dataScope=${subject.dataScope} daemonOwner=${subject.isDaemonOwner} control=${subject.fromControlChannel} reason=${decision.reason}`);
|
|
1110
1120
|
if (intent.operation.startsWith('role.') || intent.operation.startsWith('connect.')) {
|
|
1111
1121
|
const namespace = intent.operation.split('.', 1)[0];
|
|
1112
1122
|
return {
|
|
@@ -1510,6 +1520,8 @@ export async function getSubMenuItems(cmd, channel, channelId, userId, args, ove
|
|
|
1510
1520
|
const gated0 = gateControlScope.call(this, { cmdBase: cmdBase0, args, channel, fromControlChannel });
|
|
1511
1521
|
if (gated0)
|
|
1512
1522
|
throw { code: gated0.code, message: gated0.error };
|
|
1523
|
+
if (cmdBase0 === '/baseagent-models')
|
|
1524
|
+
throw { code: 'NOT_SUPPORTED', message: 'Baseagent catalog only supports menu.query' };
|
|
1513
1525
|
if (cmdBase0 === '/role') {
|
|
1514
1526
|
const identity = overrideIdentity ?? this.sessionManager.resolveIdentity(channel, userId, ...menuIdentityArgs(session));
|
|
1515
1527
|
const authorized = await authorizeRoleMenu(this, {
|
|
@@ -1628,7 +1640,7 @@ export async function getSubMenuItems(cmd, channel, channelId, userId, args, ove
|
|
|
1628
1640
|
const triggerScheduler = this.getTriggerSchedulerForChannel?.(channel);
|
|
1629
1641
|
const scope = args?.options === 'all' ? 'all' : 'enabled';
|
|
1630
1642
|
const role = optionsIdentity.role;
|
|
1631
|
-
const isAdmin = isManagementRole(role) || subject.isDaemonOwner;
|
|
1643
|
+
const isAdmin = isManagementRole(role) || subject.isDaemonOwner || subject.processRole === 'daemon-service';
|
|
1632
1644
|
if (!triggerScheduler)
|
|
1633
1645
|
return [];
|
|
1634
1646
|
const list = triggerScheduler.list({ all: scope === 'all' });
|
|
@@ -1731,7 +1743,16 @@ export async function getSubMenuItems(cmd, channel, channelId, userId, args, ove
|
|
|
1731
1743
|
});
|
|
1732
1744
|
if ('error' in target)
|
|
1733
1745
|
throw { code: target.code, message: target.error };
|
|
1734
|
-
|
|
1746
|
+
let agent;
|
|
1747
|
+
try {
|
|
1748
|
+
agent = this.getAgent(channel, target.baseagent);
|
|
1749
|
+
}
|
|
1750
|
+
catch (error) {
|
|
1751
|
+
if (subject.processRole === 'daemon-service') {
|
|
1752
|
+
return [{ value: target.baseagent, label: target.baseagent, selected: true }];
|
|
1753
|
+
}
|
|
1754
|
+
throw error;
|
|
1755
|
+
}
|
|
1735
1756
|
if (hasModelSwitcher(agent)) {
|
|
1736
1757
|
const catalog = await getCatalog(target.sel.self, target.baseagent);
|
|
1737
1758
|
assertCatalogSelectable(catalog);
|
|
@@ -1793,7 +1814,16 @@ export async function getSubMenuItems(cmd, channel, channelId, userId, args, ove
|
|
|
1793
1814
|
});
|
|
1794
1815
|
if ('error' in target)
|
|
1795
1816
|
throw { code: target.code, message: target.error };
|
|
1796
|
-
|
|
1817
|
+
let agent;
|
|
1818
|
+
try {
|
|
1819
|
+
agent = this.getAgent(channel, target.baseagent);
|
|
1820
|
+
}
|
|
1821
|
+
catch (error) {
|
|
1822
|
+
if (subject.processRole === 'daemon-service') {
|
|
1823
|
+
return ['low', 'medium', 'high', 'auto'].map(e => ({ value: e, label: e === 'auto' ? 'auto (SDK默认)' : e }));
|
|
1824
|
+
}
|
|
1825
|
+
throw error;
|
|
1826
|
+
}
|
|
1797
1827
|
const requestedModel = menuStringArg(args, 'model') ?? menuStringArg(args, 'currentModel');
|
|
1798
1828
|
const modelTarget = { ...target, field: 'model', fieldPath: `baseagents.${target.baseagent}.model` };
|
|
1799
1829
|
const currentModel = requestedModel || (hasModelSwitcher(agent) ? (readMenuModel(modelTarget, agent).value || agent.getModel()) : agent.name);
|
|
@@ -1947,6 +1977,18 @@ export async function execMenuQuery(cmd, channel, channelId, userId, args, expli
|
|
|
1947
1977
|
});
|
|
1948
1978
|
if (authDenied)
|
|
1949
1979
|
return authDenied;
|
|
1980
|
+
if (cmdBase === '/baseagent-models') {
|
|
1981
|
+
try {
|
|
1982
|
+
const query = hostCatalogArgs(args);
|
|
1983
|
+
return { data: await getBaseagentCatalog(query.baseagent, { refresh: query.refresh }) };
|
|
1984
|
+
}
|
|
1985
|
+
catch (error) {
|
|
1986
|
+
if (error?.code === 'INVALID_ARGUMENT')
|
|
1987
|
+
return menuResultFailure(error);
|
|
1988
|
+
logger.error('Failed to query host baseagent catalog');
|
|
1989
|
+
return { error: 'Unable to query host baseagent catalog', code: 'INTERNAL_ERROR' };
|
|
1990
|
+
}
|
|
1991
|
+
}
|
|
1950
1992
|
// ── /agent 查询(只读) ──
|
|
1951
1993
|
// 授权由 agent.list/agent.show operation 决定;channel 闸门只负责目标范围。
|
|
1952
1994
|
if (cmdBase === '/agent') {
|
|
@@ -2408,7 +2450,7 @@ export async function execMenuQuery(cmd, channel, channelId, userId, args, expli
|
|
|
2408
2450
|
return { error: `不支持 query: ${cmdBase}`, code: 'NOT_SUPPORTED' };
|
|
2409
2451
|
}
|
|
2410
2452
|
/** menu.update — 写入新值。 */
|
|
2411
|
-
export async function execMenuUpdate(cmd, value, channel, channelId, userId, overrideIdentity, fromControlChannel = false, args, authSubject, source = 'menu') {
|
|
2453
|
+
export async function execMenuUpdate(cmd, value, channel, channelId, userId, overrideIdentity, fromControlChannel = false, args, authSubject, source = 'menu', transportReplyContext) {
|
|
2412
2454
|
const cmdBase = cmd.trim().split(' ')[0];
|
|
2413
2455
|
if (!cmdBase)
|
|
2414
2456
|
return { error: '缺少命令', code: 'MISSING_CMD' };
|
|
@@ -2424,10 +2466,27 @@ export async function execMenuUpdate(cmd, value, channel, channelId, userId, ove
|
|
|
2424
2466
|
const gated = gateControlScope.call(this, { cmdBase, args, channel, fromControlChannel });
|
|
2425
2467
|
if (gated)
|
|
2426
2468
|
return gated;
|
|
2469
|
+
const loadedContext = await this.loadMenuContext(channel, channelId);
|
|
2470
|
+
let session = loadedContext.session;
|
|
2471
|
+
const evolagent = loadedContext.evolagent;
|
|
2472
|
+
if (transportReplyContext?.threadId && typeof this.sessionManager.getThreadSession === 'function') {
|
|
2473
|
+
try {
|
|
2474
|
+
const topicSession = await this.sessionManager.getThreadSession(channel, channelId, transportReplyContext.threadId);
|
|
2475
|
+
if (!topicSession) {
|
|
2476
|
+
return { error: '话题会话不存在,拒绝回退到主会话', code: 'TOPIC_SESSION_NOT_FOUND' };
|
|
2477
|
+
}
|
|
2478
|
+
session = topicSession;
|
|
2479
|
+
}
|
|
2480
|
+
catch (error) {
|
|
2481
|
+
logger.debug('[MenuHandler] topic session lookup failed for menu update: thread=' + transportReplyContext.threadId + ': ' + (error instanceof Error ? error.message : String(error)));
|
|
2482
|
+
return { error: '无法读取话题会话,拒绝回退到主会话', code: 'TOPIC_SESSION_LOOKUP_FAILED' };
|
|
2483
|
+
}
|
|
2484
|
+
}
|
|
2485
|
+
if (cmdBase === '/baseagent-models')
|
|
2486
|
+
return { error: 'Baseagent catalog only supports menu.query', code: 'NOT_SUPPORTED' };
|
|
2427
2487
|
const arg = value.trim();
|
|
2428
2488
|
if (!arg)
|
|
2429
2489
|
return { error: '缺少 value 参数', code: 'MISSING_VALUE' };
|
|
2430
|
-
const { session, evolagent } = await this.loadMenuContext(channel, channelId);
|
|
2431
2490
|
const identity = overrideIdentity ?? this.sessionManager.resolveIdentity(channel, userId, ...menuIdentityArgs(session));
|
|
2432
2491
|
const isAdmin = isManagementRole(identity.role);
|
|
2433
2492
|
const subject = buildMenuAuthSubject(this, {
|
|
@@ -2516,7 +2575,7 @@ export async function execMenuUpdate(cmd, value, channel, channelId, userId, ove
|
|
|
2516
2575
|
}
|
|
2517
2576
|
if (!patch?.nameOrId)
|
|
2518
2577
|
return { error: '缺少 nameOrId', code: 'INVALID_ARGS' };
|
|
2519
|
-
const isAdmin = isManagementRole(identity.role) || subject.isDaemonOwner;
|
|
2578
|
+
const isAdmin = isManagementRole(identity.role) || subject.isDaemonOwner || subject.processRole === 'daemon-service';
|
|
2520
2579
|
if (!isAdmin && !userId)
|
|
2521
2580
|
return { error: '无法确认身份,请确保渠道提供发送者 ID', code: 'FORBIDDEN' };
|
|
2522
2581
|
const triggerScheduler = this.getTriggerSchedulerForChannel?.(channel);
|
|
@@ -2578,21 +2637,112 @@ export async function execMenuUpdate(cmd, value, channel, channelId, userId, ove
|
|
|
2578
2637
|
return { error: `无效 baseagent: ${arg},可选: ${valid.join(' / ')}`, code: 'INVALID_VALUE' };
|
|
2579
2638
|
}
|
|
2580
2639
|
const previousBaseagent = evolagent.baseagent;
|
|
2640
|
+
if (session?.threadId) {
|
|
2641
|
+
if (transportReplyContext?.threadId !== session.threadId) {
|
|
2642
|
+
return { error: '话题路由上下文不匹配,拒绝切换', code: 'THREAD_CONTEXT_MISMATCH' };
|
|
2643
|
+
}
|
|
2644
|
+
if (typeof this.messageQueue.withSessionBarrier !== 'function') {
|
|
2645
|
+
return { error: '当前运行环境不支持安全话题切换', code: 'NOT_SUPPORTED' };
|
|
2646
|
+
}
|
|
2647
|
+
const sessionKey = this.getQueueKey(session, channel, channelId);
|
|
2648
|
+
const currentAgent = this.getAgent(channel, session.baseagent);
|
|
2649
|
+
const outcome = await this.messageQueue.withSessionBarrier(sessionKey, async (snapshot) => {
|
|
2650
|
+
const latest = await this.sessionManager.getSessionById?.(session.id);
|
|
2651
|
+
if (!latest || latest.threadId !== session.threadId) {
|
|
2652
|
+
return { error: '话题会话不存在或归属已变化', code: 'NOT_FOUND' };
|
|
2653
|
+
}
|
|
2654
|
+
const busy = !!latest.processingState
|
|
2655
|
+
|| !!latest.metadata?.turnState?.active
|
|
2656
|
+
|| snapshot.active
|
|
2657
|
+
|| snapshot.queued > 0
|
|
2658
|
+
|| snapshot.dispatching
|
|
2659
|
+
|| this.messageQueue.isProcessing?.(sessionKey)
|
|
2660
|
+
|| (this.messageQueue.getQueueLength?.(sessionKey) ?? 0) > 0
|
|
2661
|
+
|| currentAgent.hasActiveStream(sessionKey);
|
|
2662
|
+
if (busy) {
|
|
2663
|
+
return { error: '当前话题会话忙碌,无法切换 baseagent', code: 'BUSY' };
|
|
2664
|
+
}
|
|
2665
|
+
try {
|
|
2666
|
+
evolagent.setActiveBaseagent(arg);
|
|
2667
|
+
await this.sessionManager.rotateTopicBackend({
|
|
2668
|
+
sessionId: latest.id,
|
|
2669
|
+
expectedThreadId: latest.threadId,
|
|
2670
|
+
baseagent: arg,
|
|
2671
|
+
});
|
|
2672
|
+
return { ok: true };
|
|
2673
|
+
}
|
|
2674
|
+
catch (error) {
|
|
2675
|
+
try {
|
|
2676
|
+
evolagent.setActiveBaseagent(previousBaseagent);
|
|
2677
|
+
}
|
|
2678
|
+
catch { }
|
|
2679
|
+
return { error: error instanceof Error ? error.message : String(error), code: 'EXEC_FAILED' };
|
|
2680
|
+
}
|
|
2681
|
+
});
|
|
2682
|
+
if ('error' in outcome)
|
|
2683
|
+
return outcome;
|
|
2684
|
+
this.eventBus.publish({
|
|
2685
|
+
type: 'agent:baseagent-changed',
|
|
2686
|
+
aid: evolagent.aid,
|
|
2687
|
+
baseagent: arg,
|
|
2688
|
+
previousBaseagent,
|
|
2689
|
+
scope: 'agent',
|
|
2690
|
+
timestamp: Date.now(),
|
|
2691
|
+
});
|
|
2692
|
+
return { data: {
|
|
2693
|
+
baseagent: arg,
|
|
2694
|
+
previousBaseagent,
|
|
2695
|
+
baseagentChanged: previousBaseagent !== arg,
|
|
2696
|
+
scope: 'agent',
|
|
2697
|
+
sessionId: session.id,
|
|
2698
|
+
threadId: session.threadId,
|
|
2699
|
+
sessionIdChanged: false,
|
|
2700
|
+
notice: '已切换 Baseagent: ' + previousBaseagent + ' → ' + arg + ',话题会话 ID 保持不变',
|
|
2701
|
+
} };
|
|
2702
|
+
}
|
|
2581
2703
|
try {
|
|
2582
2704
|
evolagent.setActiveBaseagent(arg);
|
|
2583
2705
|
}
|
|
2584
2706
|
catch (error) {
|
|
2585
2707
|
return { error: error instanceof Error ? error.message : String(error), code: 'INVALID_VALUE' };
|
|
2586
2708
|
}
|
|
2587
|
-
|
|
2588
|
-
|
|
2589
|
-
|
|
2590
|
-
|
|
2591
|
-
|
|
2592
|
-
|
|
2593
|
-
|
|
2594
|
-
|
|
2595
|
-
|
|
2709
|
+
try {
|
|
2710
|
+
const newSession = await this.sessionManager.createNewSession(channel, channelId, evolagent.projectPath, undefined, arg, session?.metadata, { previousBaseagent, baseagentChanged: previousBaseagent !== arg });
|
|
2711
|
+
if (session) {
|
|
2712
|
+
try {
|
|
2713
|
+
const previousAgent = this.getAgent(channel, session.baseagent);
|
|
2714
|
+
await previousAgent.clearSession(session.id, session.agentSessionId || '', session.projectPath);
|
|
2715
|
+
await previousAgent.closeSession(session.id);
|
|
2716
|
+
}
|
|
2717
|
+
catch (error) {
|
|
2718
|
+
// The new Evolcore session is already active; backend cleanup is best effort.
|
|
2719
|
+
logger.warn(`[Menu] baseagent switch cleanup failed: session=${session.id}: ${error instanceof Error ? error.message : String(error)}`);
|
|
2720
|
+
}
|
|
2721
|
+
}
|
|
2722
|
+
this.eventBus.publish({
|
|
2723
|
+
type: 'agent:baseagent-changed',
|
|
2724
|
+
aid: evolagent.aid,
|
|
2725
|
+
baseagent: arg,
|
|
2726
|
+
previousBaseagent,
|
|
2727
|
+
scope: 'agent',
|
|
2728
|
+
timestamp: Date.now(),
|
|
2729
|
+
});
|
|
2730
|
+
return { data: {
|
|
2731
|
+
baseagent: arg,
|
|
2732
|
+
previousBaseagent,
|
|
2733
|
+
baseagentChanged: previousBaseagent !== arg,
|
|
2734
|
+
scope: 'agent',
|
|
2735
|
+
sessionId: newSession.id,
|
|
2736
|
+
notice: `已切换 Baseagent: ${previousBaseagent} → ${arg},已创建新的主会话`,
|
|
2737
|
+
} };
|
|
2738
|
+
}
|
|
2739
|
+
catch (error) {
|
|
2740
|
+
try {
|
|
2741
|
+
evolagent.setActiveBaseagent(previousBaseagent);
|
|
2742
|
+
}
|
|
2743
|
+
catch { }
|
|
2744
|
+
return { error: `创建新会话失败:${error instanceof Error ? error.message : String(error)}`, code: 'EXEC_FAILED' };
|
|
2745
|
+
}
|
|
2596
2746
|
}
|
|
2597
2747
|
if (cmdBase === '/model') {
|
|
2598
2748
|
const target = resolveMenuModelTarget.call(this, {
|
|
@@ -2816,8 +2966,9 @@ export async function execMenuUpdate(cmd, value, channel, channelId, userId, ove
|
|
|
2816
2966
|
};
|
|
2817
2967
|
}
|
|
2818
2968
|
if (cmdBase === '/observable') {
|
|
2819
|
-
if (identity.role !== 'owner')
|
|
2969
|
+
if (identity.role !== 'owner' && subject.processRole !== 'daemon-service' && !subject.isDaemonOwner) {
|
|
2820
2970
|
return { error: '观察者模式仅限 owner 开关', code: 'NO_PERMISSION' };
|
|
2971
|
+
}
|
|
2821
2972
|
if (arg !== 'true' && arg !== 'false')
|
|
2822
2973
|
return { error: `无效值: ${arg},可选: true / false`, code: 'INVALID_VALUE' };
|
|
2823
2974
|
if (!evolagent)
|
|
@@ -2837,8 +2988,25 @@ export async function execMenuAction(cmd, action, args, channel, channelId, user
|
|
|
2837
2988
|
const gated = gateControlScope.call(this, { cmdBase, action, args, channel, fromControlChannel });
|
|
2838
2989
|
if (gated)
|
|
2839
2990
|
return gated;
|
|
2840
|
-
const
|
|
2991
|
+
const loadedContext = await this.loadMenuContext(channel, channelId);
|
|
2992
|
+
// Menu callbacks can arrive from a topic while the channel's active
|
|
2993
|
+
// session still points at the main conversation. Prefer the explicitly
|
|
2994
|
+
// transported topic route for session-scoped actions and authorization.
|
|
2995
|
+
let authSession = loadedContext.session;
|
|
2996
|
+
if (transportReplyContext?.threadId && typeof this.sessionManager.getThreadSession === 'function') {
|
|
2997
|
+
try {
|
|
2998
|
+
authSession = await this.sessionManager.getThreadSession(channel, channelId, transportReplyContext.threadId) ?? authSession;
|
|
2999
|
+
}
|
|
3000
|
+
catch (error) {
|
|
3001
|
+
logger.debug(`[MenuHandler] topic session lookup failed for menu action: thread=${transportReplyContext.threadId}: ${error instanceof Error ? error.message : String(error)}`);
|
|
3002
|
+
}
|
|
3003
|
+
}
|
|
3004
|
+
const session = authSession;
|
|
3005
|
+
const evolagent = loadedContext.evolagent;
|
|
3006
|
+
if (cmdBase === '/baseagent-models')
|
|
3007
|
+
return { error: 'Baseagent catalog only supports menu.query', code: 'NOT_SUPPORTED' };
|
|
2841
3008
|
const authIdentity = overrideIdentity ?? this.sessionManager.resolveIdentity(channel, userId, ...menuIdentityArgs(authSession));
|
|
3009
|
+
const identity = authIdentity;
|
|
2842
3010
|
const subject = buildMenuAuthSubject(this, {
|
|
2843
3011
|
identity: authIdentity,
|
|
2844
3012
|
session: authSession,
|
|
@@ -2850,7 +3018,7 @@ export async function execMenuAction(cmd, action, args, channel, channelId, user
|
|
|
2850
3018
|
fromControlChannel,
|
|
2851
3019
|
subject: authSubject,
|
|
2852
3020
|
});
|
|
2853
|
-
if (cmdBase === '/system' && fromControlChannel && !subject.isDaemonOwner) {
|
|
3021
|
+
if (cmdBase === '/system' && fromControlChannel && !subject.isDaemonOwner && subject.processRole !== 'daemon-service') {
|
|
2854
3022
|
return {
|
|
2855
3023
|
error: formatAuthorizationDenial('This daemon-level operation requires DaemonOwner.', 'DAEMON_OWNER_REQUIRED'),
|
|
2856
3024
|
code: 'NOT_ALLOWED',
|
|
@@ -2927,15 +3095,13 @@ export async function execMenuAction(cmd, action, args, channel, channelId, user
|
|
|
2927
3095
|
return await gatewaySyncEnv(args);
|
|
2928
3096
|
return { error: `不支持 gateway action: ${action}`, code: 'NOT_SUPPORTED' };
|
|
2929
3097
|
}
|
|
2930
|
-
const
|
|
2931
|
-
const identity = overrideIdentity ?? this.sessionManager.resolveIdentity(channel, userId, ...menuIdentityArgs(session));
|
|
2932
|
-
const isMenuAdmin = isManagementRole(identity.role);
|
|
3098
|
+
const isMenuAdmin = isManagementRole(authIdentity.role);
|
|
2933
3099
|
// ── /agent action ──
|
|
2934
3100
|
// 控制 channel:验 evolcore.owners,可执行进程级(create/delete/enable/disable)+ 自管理(update/reload)。
|
|
2935
3101
|
// agent channel:闸门已挡掉进程级 + 跨 agent,仅 update/reload 能到此;reload 允许 owner/admin,update 仅 owner。
|
|
2936
3102
|
if (cmdBase === '/agent') {
|
|
2937
3103
|
if (fromControlChannel) {
|
|
2938
|
-
if (!subject.isDaemonOwner) {
|
|
3104
|
+
if (!subject.isDaemonOwner && subject.processRole !== 'daemon-service') {
|
|
2939
3105
|
return {
|
|
2940
3106
|
error: formatAuthorizationDenial('This Agent lifecycle operation requires DaemonOwner.', 'DAEMON_OWNER_REQUIRED'),
|
|
2941
3107
|
code: 'NOT_ALLOWED',
|
|
@@ -3054,7 +3220,7 @@ export async function execMenuAction(cmd, action, args, channel, channelId, user
|
|
|
3054
3220
|
// ── 关系级 /trigger(不走 owners;复用 isAdmin + scoped 逻辑,D4 直调底层) ──
|
|
3055
3221
|
if (cmdBase === '/trigger') {
|
|
3056
3222
|
const role = identity.role;
|
|
3057
|
-
const isAdmin = isManagementRole(role) || subject.isDaemonOwner;
|
|
3223
|
+
const isAdmin = isManagementRole(role) || subject.isDaemonOwner || subject.processRole === 'daemon-service';
|
|
3058
3224
|
const triggerScheduler = this.getTriggerSchedulerForChannel?.(channel);
|
|
3059
3225
|
if (!triggerScheduler)
|
|
3060
3226
|
return { error: '触发器功能未启用', code: 'NOT_SUPPORTED' };
|
|
@@ -3135,7 +3301,7 @@ export async function execMenuAction(cmd, action, args, channel, channelId, user
|
|
|
3135
3301
|
if (action === 'enable' && trigger.execution.permissionMode === 'fullaccess') {
|
|
3136
3302
|
if (!isFullAccessEnabled())
|
|
3137
3303
|
return { error: 'fullaccess 功能未启用', code: 'NOT_SUPPORTED' };
|
|
3138
|
-
if (!subject.isDaemonOwner)
|
|
3304
|
+
if (!subject.isDaemonOwner && subject.processRole !== 'daemon-service')
|
|
3139
3305
|
return { error: '只有 daemon-owner 可以启用 fullaccess Trigger', code: 'FORBIDDEN' };
|
|
3140
3306
|
if (subject.canApprovePersistentFullAccess === false) {
|
|
3141
3307
|
return { error: '当前认证通道不能批准持久 fullaccess Trigger', code: 'FORBIDDEN' };
|
|
@@ -3513,7 +3679,7 @@ export async function execMenuAction(cmd, action, args, channel, channelId, user
|
|
|
3513
3679
|
}
|
|
3514
3680
|
if (action === 'new') {
|
|
3515
3681
|
const name = (args?.name ?? '').toString().trim();
|
|
3516
|
-
return await this.delegateAsAction(action, name ? `/new ${name}` : '/new', channel, channelId, userId, { enrichSession: true, authSubject: subject });
|
|
3682
|
+
return await this.delegateAsAction(action, name ? `/new ${name}` : '/new', channel, channelId, userId, { enrichSession: true, authSubject: subject, threadId: transportReplyContext?.threadId, chatType: transportReplyContext?.delivery?.chatType, replyContext: transportReplyContext });
|
|
3517
3683
|
}
|
|
3518
3684
|
if (action === 'rename') {
|
|
3519
3685
|
const newName = getRenameName(args);
|
|
@@ -3556,33 +3722,33 @@ export async function execMenuAction(cmd, action, args, channel, channelId, user
|
|
|
3556
3722
|
const target = (args?.target ?? '').toString().trim();
|
|
3557
3723
|
if (!target)
|
|
3558
3724
|
return { error: '缺少 args.target', code: 'MISSING_VALUE' };
|
|
3559
|
-
return await this.delegateAsAction(action, `/del ${target}`, channel, channelId, userId, { authSubject: subject });
|
|
3725
|
+
return await this.delegateAsAction(action, `/del ${target}`, channel, channelId, userId, { authSubject: subject, threadId: transportReplyContext?.threadId, chatType: transportReplyContext?.delivery?.chatType, replyContext: transportReplyContext });
|
|
3560
3726
|
}
|
|
3561
3727
|
if (action === 'switch') {
|
|
3562
3728
|
const target = (args?.target ?? '').toString().trim();
|
|
3563
3729
|
if (!target)
|
|
3564
3730
|
return { error: '缺少 args.target', code: 'MISSING_VALUE' };
|
|
3565
|
-
return await this.delegateAsAction(action, `/s ${target}`, channel, channelId, userId, { enrichSession: true, authSubject: subject });
|
|
3731
|
+
return await this.delegateAsAction(action, `/s ${target}`, channel, channelId, userId, { enrichSession: true, authSubject: subject, threadId: transportReplyContext?.threadId, chatType: transportReplyContext?.delivery?.chatType, replyContext: transportReplyContext });
|
|
3566
3732
|
}
|
|
3567
3733
|
if (action === 'compact') {
|
|
3568
3734
|
const need = this.requireSession(session);
|
|
3569
3735
|
if (need)
|
|
3570
3736
|
return need;
|
|
3571
|
-
return await this.delegateAsAction(action, '/compact', channel, channelId, userId, { authSubject: subject });
|
|
3737
|
+
return await this.delegateAsAction(action, '/compact', channel, channelId, userId, { authSubject: subject, threadId: transportReplyContext?.threadId, chatType: transportReplyContext?.delivery?.chatType, replyContext: transportReplyContext });
|
|
3572
3738
|
}
|
|
3573
3739
|
if (action === 'fork') {
|
|
3574
3740
|
const need = this.requireSession(session);
|
|
3575
3741
|
if (need)
|
|
3576
3742
|
return need;
|
|
3577
3743
|
const name = (args?.name ?? '').toString().trim();
|
|
3578
|
-
return await this.delegateAsAction(action, name ? `/fork ${name}` : '/fork', channel, channelId, userId, { enrichSession: true, authSubject: subject });
|
|
3744
|
+
return await this.delegateAsAction(action, name ? `/fork ${name}` : '/fork', channel, channelId, userId, { enrichSession: true, authSubject: subject, threadId: transportReplyContext?.threadId, chatType: transportReplyContext?.delivery?.chatType, replyContext: transportReplyContext });
|
|
3579
3745
|
}
|
|
3580
3746
|
return { error: `不支持的 session action: ${action}`, code: 'NOT_SUPPORTED' };
|
|
3581
3747
|
}
|
|
3582
3748
|
// ── /system 系列 ──
|
|
3583
3749
|
if (cmdBase === '/system') {
|
|
3584
3750
|
// D1 迁移:进程级鉴权统一查 daemon.json owners,替代各 action 内联的 identity.role 判断
|
|
3585
|
-
if (!subject.isDaemonOwner) {
|
|
3751
|
+
if (!subject.isDaemonOwner && subject.processRole !== 'daemon-service') {
|
|
3586
3752
|
return {
|
|
3587
3753
|
error: formatAuthorizationDenial('This daemon-level operation requires DaemonOwner.', 'DAEMON_OWNER_REQUIRED'),
|
|
3588
3754
|
code: 'NOT_ALLOWED',
|
|
@@ -3626,17 +3792,28 @@ export async function execMenuAction(cmd, action, args, channel, channelId, user
|
|
|
3626
3792
|
const migrationRequirement = inspectDataMigrationRequirement(resolvePaths().root);
|
|
3627
3793
|
if (migrationRequirement.required) {
|
|
3628
3794
|
return {
|
|
3629
|
-
error: `待处理的用户数据迁移(${migrationRequirement.operationCount}
|
|
3795
|
+
error: `待处理的用户数据迁移(${migrationRequirement.operationCount} 项)阻止重启;本次仅检查,未停止当前服务。请先运行 ec data migrate --dry-run,然后运行 ec data migrate --apply。`,
|
|
3630
3796
|
code: 'DATA_MIGRATION_REQUIRED',
|
|
3631
3797
|
data: { reasonCode: 'DATA_MIGRATION_REQUIRED', operationCount: migrationRequirement.operationCount },
|
|
3632
3798
|
};
|
|
3633
3799
|
}
|
|
3634
3800
|
if (!suppressRealRestart) {
|
|
3801
|
+
// 这条路径同样以 daemon 自杀收尾,入口缺失必须在写 pending 和自杀之前拦下。
|
|
3802
|
+
const restartEntry = resolveCliEntry(getPackageRoot());
|
|
3803
|
+
if (!fs.existsSync(restartEntry.entry)) {
|
|
3804
|
+
return {
|
|
3805
|
+
error: `找不到重启入口:${restartEntry.entry}。请先执行 npm run build 重建产物。`,
|
|
3806
|
+
code: 'RESTART_ENTRY_MISSING',
|
|
3807
|
+
data: { reasonCode: 'RESTART_ENTRY_MISSING', entry: restartEntry.entry },
|
|
3808
|
+
};
|
|
3809
|
+
}
|
|
3635
3810
|
const restartInfo = {
|
|
3636
3811
|
channel,
|
|
3637
3812
|
channelId,
|
|
3638
3813
|
timestamp: Date.now(),
|
|
3814
|
+
...(restartReplyContext?.threadId ? { threadId: restartReplyContext.threadId } : {}),
|
|
3639
3815
|
...(restartDelivery ? { delivery: restartDelivery } : {}),
|
|
3816
|
+
...(restartReplyContext?.replyToMessageId ? { rootId: restartReplyContext.replyToMessageId } : {}),
|
|
3640
3817
|
};
|
|
3641
3818
|
const controlDir = daemonControlDir();
|
|
3642
3819
|
fs.mkdirSync(controlDir, { recursive: true });
|
|
@@ -3645,23 +3822,41 @@ export async function execMenuAction(cmd, action, args, channel, channelId, user
|
|
|
3645
3822
|
// the version check and automatic upgrades before replacing the daemon.
|
|
3646
3823
|
// Do not inherit managed-session credentials: otherwise cmdRestart can
|
|
3647
3824
|
// delegate back to this daemon and recursively enqueue another restart.
|
|
3648
|
-
|
|
3825
|
+
//
|
|
3826
|
+
// `ec restart` owns the monitor liveness check, but it can itself die on
|
|
3827
|
+
// load (tsx + a broken source tree), and this path kills the daemon
|
|
3828
|
+
// afterwards — so confirm the CLI process survived before proceeding.
|
|
3829
|
+
const restartLaunch = await launchMonitorAndConfirm([...restartEntry.args, 'restart'], {
|
|
3649
3830
|
EVOLCORE_HOME: resolvePaths().root,
|
|
3650
3831
|
EVOLCORE_SESSION_ID: '',
|
|
3651
3832
|
[AGENT_DELEGATION_TOKEN_ENV]: '',
|
|
3652
|
-
}
|
|
3833
|
+
},
|
|
3834
|
+
// `ec restart` does not register itself; staying alive is the signal.
|
|
3835
|
+
() => false, pid => scanInstances().restartMonitors.some(m => m.alive && m.record.pid !== pid), 8_000);
|
|
3836
|
+
if (!restartLaunch.ok) {
|
|
3837
|
+
try {
|
|
3838
|
+
fs.rmSync(path.join(controlDir, 'restart-pending.json'), { force: true });
|
|
3839
|
+
}
|
|
3840
|
+
catch { }
|
|
3841
|
+
logger.error(`[System] Refusing menu restart: launcher did not survive (${restartLaunch.reason})`);
|
|
3842
|
+
for (const line of (restartLaunch.stderrTail ?? '').split(/\r?\n/).filter(Boolean).slice(-8)) {
|
|
3843
|
+
logger.error(`[System] | ${line}`);
|
|
3844
|
+
}
|
|
3845
|
+
return {
|
|
3846
|
+
error: `重启已中止:启动器未能运行(${restartLaunch.reason})。服务未停止,仍在运行当前版本。`,
|
|
3847
|
+
code: 'RESTART_LAUNCH_FAILED',
|
|
3848
|
+
data: { reasonCode: 'RESTART_LAUNCH_FAILED', reason: restartLaunch.reason },
|
|
3849
|
+
};
|
|
3850
|
+
}
|
|
3653
3851
|
}
|
|
3654
3852
|
else {
|
|
3655
3853
|
logger.info('[System] Suppressed real menu restart in test runtime');
|
|
3656
3854
|
}
|
|
3657
3855
|
this.eventBus.publish({ type: 'system:restart', channel, channelId });
|
|
3658
|
-
if (!suppressRealRestart) {
|
|
3659
|
-
setTimeout(() => { process.kill(process.pid, 'SIGTERM'); }, 1000);
|
|
3660
|
-
}
|
|
3661
3856
|
return { data: { action: 'restart', success: true } };
|
|
3662
3857
|
}
|
|
3663
3858
|
if (action === 'check') {
|
|
3664
|
-
const r = await this.delegateAsAction(action, '/check', channel, channelId, userId, { overrideIdentity, authSubject: subject });
|
|
3859
|
+
const r = await this.delegateAsAction(action, '/check', channel, channelId, userId, { overrideIdentity, authSubject: subject, threadId: transportReplyContext?.threadId, chatType: transportReplyContext?.delivery?.chatType, replyContext: transportReplyContext });
|
|
3665
3860
|
// delegateAsAction 会把 structured 展开到 data 顶层,并保留 data.structured 兼容旧客户端。
|
|
3666
3861
|
return r;
|
|
3667
3862
|
}
|
|
@@ -3927,8 +4122,8 @@ function menuCommandToken(cmd) {
|
|
|
3927
4122
|
}
|
|
3928
4123
|
function isProcessLevelMenu(name, cmd) {
|
|
3929
4124
|
const cmdToken = menuCommandToken(cmd);
|
|
3930
|
-
return name === 'system' || name === 'agent' || name === 'gateway' || name === 'config'
|
|
3931
|
-
|| cmdToken === '/system' || cmdToken === '/agent' || cmdToken === '/gateway' || cmdToken === '/config';
|
|
4125
|
+
return name === 'system' || name === 'agent' || name === 'gateway' || name === 'config' || name === 'baseagent-models'
|
|
4126
|
+
|| cmdToken === '/system' || cmdToken === '/agent' || cmdToken === '/gateway' || cmdToken === '/config' || cmdToken === '/baseagent-models';
|
|
3932
4127
|
}
|
|
3933
4128
|
function isProcessContextMenu(payload) {
|
|
3934
4129
|
const isCatalog = payload?.type === 'menu.list' && payload?.args?.view === 'catalog';
|
|
@@ -3986,10 +4181,18 @@ async function execMenuForSystemControl(payload, context) {
|
|
|
3986
4181
|
processOwners: context.owners,
|
|
3987
4182
|
fromControlChannel: true,
|
|
3988
4183
|
canApprovePersistentFullAccess: context.canApprovePersistentFullAccess,
|
|
4184
|
+
trustedProcessRole: context.source === 'ecweb' ? 'daemon-owner' : undefined,
|
|
3989
4185
|
});
|
|
3990
4186
|
const trustedIdentity = trustedSubject.identity;
|
|
3991
4187
|
const cmd = name ? (menuCommandForName(name) ?? payload.cmd) : payload.cmd;
|
|
3992
4188
|
try {
|
|
4189
|
+
if (menuCommandToken(cmd) === '/baseagent-models') {
|
|
4190
|
+
if (payload.type !== 'menu.query')
|
|
4191
|
+
return ecwebErr(id, name, 'NOT_SUPPORTED', 'Baseagent catalog only supports menu.query');
|
|
4192
|
+
if (payload.agent !== undefined)
|
|
4193
|
+
return ecwebErr(id, name, 'INVALID_ARGUMENT', 'Baseagent catalog does not accept an Agent target');
|
|
4194
|
+
hostCatalogArgs(payload.args);
|
|
4195
|
+
}
|
|
3993
4196
|
switch (payload?.type) {
|
|
3994
4197
|
case 'menu.list':
|
|
3995
4198
|
if (payload?.args?.view === 'catalog') {
|
|
@@ -4093,6 +4296,7 @@ async function execMenuForSystemControl(payload, context) {
|
|
|
4093
4296
|
}
|
|
4094
4297
|
}
|
|
4095
4298
|
catch (e) {
|
|
4299
|
+
logger.error(`[ECWebMenu] failed type=${payload?.type ?? '<none>'} name=${name ?? '<none>'} cmd=${cmd ?? '<none>'} agent=${payload?.agent ?? '<none>'} actor=${context.actorAid ?? '<none>'}: ${e?.stack || e?.message || String(e)}`);
|
|
4096
4300
|
return menuFailure({ id, ...(name ? { name } : {}) }, normalizeMenuError(e));
|
|
4097
4301
|
}
|
|
4098
4302
|
}
|
|
@@ -25,8 +25,11 @@ export function buildMenuClientProtectedHeaders(input) {
|
|
|
25
25
|
...(input.remoteAccessToken || input.ecwebToken ? { auth_method: 'remote-access-token', remote_access_token: input.remoteAccessToken ?? input.ecwebToken } : {}),
|
|
26
26
|
};
|
|
27
27
|
}
|
|
28
|
+
const appClientVersion = input.clientVersion?.trim();
|
|
28
29
|
return {
|
|
30
|
+
...(clientName === 'Evol App' ? { client_name: clientName } : {}),
|
|
29
31
|
...(input.evolVersion ? { evol_version: input.evolVersion } : {}),
|
|
32
|
+
...(clientName === 'Evol App' && appClientVersion ? { client_version: appClientVersion } : {}),
|
|
30
33
|
...(input.remoteAccessToken || input.ecwebToken ? { auth_method: 'remote-access-token', remote_access_token: input.remoteAccessToken ?? input.ecwebToken } : {}),
|
|
31
34
|
};
|
|
32
35
|
}
|
|
@@ -40,7 +43,7 @@ export function parseMenuClientProtectedHeaders(value) {
|
|
|
40
43
|
normalized[key.trim().toLowerCase()] = raw;
|
|
41
44
|
}
|
|
42
45
|
const result = {};
|
|
43
|
-
if (normalized.client_name === 'evol' || normalized.client_name === 'ec-menu-client')
|
|
46
|
+
if (normalized.client_name === 'evol' || normalized.client_name === 'Evol App' || normalized.client_name === 'ec-menu-client')
|
|
44
47
|
result.client_name = normalized.client_name;
|
|
45
48
|
else if (normalized.client_name !== undefined)
|
|
46
49
|
result.client_name = normalized.client_name;
|
|
@@ -71,7 +74,7 @@ export function evaluateMenuClientCompatibility(input) {
|
|
|
71
74
|
}
|
|
72
75
|
return null;
|
|
73
76
|
}
|
|
74
|
-
if (clientName !== 'evol') {
|
|
77
|
+
if (clientName !== 'evol' && clientName !== 'Evol App') {
|
|
75
78
|
return { code: 'INVALID_CLIENT', message: `Unsupported Menu client: ${String(clientName)}` };
|
|
76
79
|
}
|
|
77
80
|
return evaluateEvolMenuVersionGate(input);
|
|
@@ -104,6 +107,7 @@ export const MENU_NAME_COMMANDS = Object.freeze({
|
|
|
104
107
|
session: '/session',
|
|
105
108
|
topic: '/topic',
|
|
106
109
|
baseagent: '/baseagent',
|
|
110
|
+
'baseagent-models': '/baseagent-models',
|
|
107
111
|
model: '/model',
|
|
108
112
|
effort: '/effort',
|
|
109
113
|
chatmode: '/chatmode',
|