evolcore 0.0.2 → 0.0.3
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 +44 -793
- package/dist/agents/claude-runner.js +197 -17
- package/dist/agents/codex-runner.js +46 -3
- package/dist/aun/outbox.js +8 -0
- package/dist/channels/aun.js +21 -4
- package/dist/channels/contact-bind-code.js +134 -0
- package/dist/channels/dingtalk.js +979 -149
- package/dist/channels/feishu.js +130 -54
- package/dist/channels/wecom-card.js +101 -0
- package/dist/channels/wecom-onboarding.js +82 -0
- package/dist/channels/wecom-state.js +191 -0
- package/dist/channels/wecom.js +755 -163
- package/dist/cli/agent-command.js +2 -1
- package/dist/cli/aun-commands.js +88 -33
- package/dist/cli/bench.js +2 -2
- package/dist/cli/contact.js +71 -0
- package/dist/cli/ctl-command.js +2 -2
- package/dist/cli/daemon-commands.js +77 -214
- package/dist/cli/handoff-command.js +2 -2
- package/dist/cli/help.js +9 -5
- package/dist/cli/index.js +132 -116
- package/dist/cli/init-channel.js +92 -97
- package/dist/cli/init.js +63 -26
- package/dist/cli/model.js +2 -1
- package/dist/cli/net-check.js +2 -2
- package/dist/cli/queue-command.js +30 -6
- package/dist/cli/raw-key-input.js +25 -0
- package/dist/cli/response.js +5 -6
- package/dist/cli/restart-monitor.js +25 -1
- package/dist/cli/stats.js +6 -4
- package/dist/cli/trigger-command.js +55 -15
- package/dist/cli/version.js +6 -1
- package/dist/config/builtin-role-templates.js +22 -10
- package/dist/config/builtin-roles.js +7 -1
- package/dist/config/config-manager.js +221 -17
- package/dist/config/contact-alias.js +68 -0
- package/dist/config/contact-book-store.js +454 -0
- package/dist/config/contact-book-v2-startup.js +35 -0
- package/dist/config/contact-book.js +156 -303
- package/dist/config/contact-operation-service.js +110 -0
- package/dist/config/peer-role-resolver.js +133 -54
- package/dist/config/role-ranks.js +18 -0
- package/dist/config/role-service.js +16 -19
- package/dist/config/role-store.js +16 -5
- package/dist/config/roles.js +10 -1
- package/dist/config-store.js +0 -2
- package/dist/core/auth/authorization-audit.js +10 -1
- package/dist/core/auth/operation-authorizer.js +2 -0
- package/dist/core/auth/operation-catalog.js +56 -0
- package/dist/core/command/command-handler.js +105 -10
- package/dist/core/command/connect-menu.js +374 -0
- package/dist/core/command/menu-handler.js +114 -16
- package/dist/core/command/role-menu.js +128 -29
- package/dist/core/command/slash-handler.js +28 -18
- package/dist/core/daemon-file-cache.js +12 -6
- package/dist/core/event-catalog.js +70 -0
- package/dist/core/evolagent-registry.js +0 -1
- package/dist/core/evolagent.js +20 -9
- package/dist/core/message/im-renderer.js +2 -0
- package/dist/core/message/message-bridge.js +79 -8
- package/dist/core/message/message-queue.js +10 -0
- package/dist/core/message/message-utils.js +8 -2
- package/dist/core/message/response-engine.js +269 -25
- package/dist/core/message/send-receipt.js +24 -0
- package/dist/core/message/stream-debouncer.js +11 -2
- package/dist/core/permission/tool-policy.js +47 -15
- package/dist/core/protected-paths.js +2 -0
- package/dist/core/session/session-fs-store.js +44 -3
- package/dist/core/session/session-manager.js +61 -5
- package/dist/index.js +62 -6
- package/dist/ipc.js +34 -5
- package/dist/stats/billing.js +20 -8
- package/dist/trigger/manager.js +3 -0
- package/dist/trigger/parser.js +77 -2
- package/dist/trigger/patch.js +8 -1
- package/dist/trigger/scheduler.js +3 -1
- package/dist/trigger/validation.js +13 -0
- package/dist/utils/aid-bind.js +43 -29
- package/dist/utils/instance-registry.js +14 -7
- package/dist/utils/log-writer.js +46 -0
- package/dist/utils/media-cache.js +4 -1
- package/dist/utils/model-prices.jsonl +6 -3
- package/dist/utils/restart-safety.js +31 -0
- package/dist/utils/system-memory.js +62 -0
- package/kits/docs/INDEX.md +2 -1
- package/kits/docs/evolcore/INDEX.md +5 -3
- package/kits/docs/evolcore/agent.md +9 -1
- package/kits/docs/evolcore/aid.md +5 -2
- package/kits/docs/evolcore/contact.md +57 -0
- package/kits/docs/evolcore/fs.md +9 -0
- package/kits/docs/evolcore/group.md +12 -3
- package/kits/docs/evolcore/model.md +4 -1
- package/kits/docs/evolcore/msg.md +9 -3
- package/kits/docs/evolcore/response.md +16 -21
- package/kits/docs/evolcore/rpc.md +2 -0
- package/kits/docs/evolcore/stats.md +15 -2
- package/kits/docs/evolcore/storage.md +1 -0
- package/kits/docs/evolcore/trigger.md +17 -2
- package/kits/eck_manifest.json +12 -0
- package/kits/migrations/migrate-contact-book-v2.mjs +747 -0
- package/kits/schemas/_meta.json +7 -4
- package/kits/schemas/agent-config.schema.6.json +322 -0
- package/kits/schemas/contact-book.schema.2.json +43 -0
- package/kits/schemas/relation-config.schema.5.json +47 -0
- package/kits/schemas/role-config.schema.1.json +1 -0
- package/kits/schemas/role-registry.schema.1.json +2 -2
- package/kits/templates/roles/admin.json +1 -0
- package/kits/templates/roles/member.json +1 -0
- package/kits/templates/roles/owner.json +1 -0
- package/kits/templates/roles/visitor.json +1 -0
- package/kits/templates/system-fragments/commands.md +3 -1
- package/package.json +4 -4
- package/assets/brand/evolcore/README.md +0 -19
- package/assets/brand/evolcore/evolcore-app-icon.png +0 -0
- package/assets/brand/evolcore/evolcore-app-icon.svg +0 -13
- package/assets/brand/evolcore/evolcore-brand-board.png +0 -0
- package/assets/brand/evolcore/evolcore-brand-board.svg +0 -126
- package/assets/brand/evolcore/evolcore-logo-kit.zip +0 -0
- package/assets/brand/evolcore/evolcore-logo-reverse.png +0 -0
- package/assets/brand/evolcore/evolcore-logo-reverse.svg +0 -14
- package/assets/brand/evolcore/evolcore-logo.png +0 -0
- package/assets/brand/evolcore/evolcore-logo.svg +0 -14
- package/assets/brand/evolcore/evolcore-mark.png +0 -0
- package/assets/brand/evolcore/evolcore-mark.svg +0 -10
|
@@ -32,9 +32,11 @@ import { resolveRuntimePermissionMode, resolveRuntimeStringField, validateRuntim
|
|
|
32
32
|
import { isManagementRole } from '../../config/builtin-roles.js';
|
|
33
33
|
import { SYSTEM_CONTROL_CHANNEL } from '../system-channels.js';
|
|
34
34
|
import { logger } from '../../utils/logger.js';
|
|
35
|
+
import { shouldSuppressRealRestart } from '../../utils/restart-safety.js';
|
|
35
36
|
import { dispatchToMentionMode } from '../../config/mention-mode.js';
|
|
36
37
|
import { menuFailure, menuSuccess, normalizeMenuError, validateConfigWriteScope, validateMenuRequest } from './menu-protocol.js';
|
|
37
38
|
import { roleMenuAction, roleMenuOperation, roleMenuOptions, roleMenuQuery, roleMenuUpdate, } from './role-menu.js';
|
|
39
|
+
import { handleConnectMenu, connectMenuOperation } from './connect-menu.js';
|
|
38
40
|
/**
|
|
39
41
|
* 获取 baseagent CLI 的版本号(claude/gemini/codex)。
|
|
40
42
|
* 失败返回 null(命令不存在或执行失败)。
|
|
@@ -885,7 +887,7 @@ function buildMenuIntent(verb, cmdBase, args, action, value, fromControlChannel
|
|
|
885
887
|
return intent('agent.delete', 'control', { action });
|
|
886
888
|
}
|
|
887
889
|
if (cmdBase === '/trigger') {
|
|
888
|
-
if (action === 'set')
|
|
890
|
+
if (action === 'create' || action === 'set')
|
|
889
891
|
return intent('trigger.create', 'relation', { action });
|
|
890
892
|
if (action === 'show')
|
|
891
893
|
return intent('trigger.show', 'relation', { action });
|
|
@@ -1024,7 +1026,7 @@ async function authorizeRoleMenu(owner, params) {
|
|
|
1024
1026
|
fromControlChannel: params.fromControlChannel,
|
|
1025
1027
|
subject: params.subject,
|
|
1026
1028
|
});
|
|
1027
|
-
const operation = roleMenuOperation(params.kind, params.args, params.value);
|
|
1029
|
+
const operation = params.operation ?? roleMenuOperation(params.kind, params.args, params.value);
|
|
1028
1030
|
const denied = await authorizeMenuIntent.call(owner, {
|
|
1029
1031
|
intent: { operation, scope: 'agent', source: params.source, args: { ...(params.args ?? {}) } },
|
|
1030
1032
|
identity: params.identity,
|
|
@@ -1248,6 +1250,25 @@ export async function getSubMenuItems(cmd, channel, channelId, userId, args, ove
|
|
|
1248
1250
|
throw authorized;
|
|
1249
1251
|
return await roleMenuOptions(authorized.context, args);
|
|
1250
1252
|
}
|
|
1253
|
+
if (cmdBase0 === '/connect') {
|
|
1254
|
+
const identity = overrideIdentity ?? this.sessionManager.resolveIdentity(channel, userId, ...menuIdentityArgs(session));
|
|
1255
|
+
const authorized = await authorizeRoleMenu(this, {
|
|
1256
|
+
kind: 'options', args, operation: connectMenuOperation('options', args), identity, subject: authSubject, session, channel, channelId,
|
|
1257
|
+
userId, fromControlChannel, source,
|
|
1258
|
+
});
|
|
1259
|
+
if ('error' in authorized)
|
|
1260
|
+
throw authorized;
|
|
1261
|
+
try {
|
|
1262
|
+
const result = await handleConnectMenu({ type: 'menu.options', subtype: 'options', args }, authorized.context);
|
|
1263
|
+
if ('error' in result)
|
|
1264
|
+
throw result.error;
|
|
1265
|
+
const opts = result.data?.options ?? [];
|
|
1266
|
+
return opts.map((opt) => ({ value: opt.option, label: opt.label, desc: opt.description }));
|
|
1267
|
+
}
|
|
1268
|
+
catch (error) {
|
|
1269
|
+
throw menuResultFailure(error);
|
|
1270
|
+
}
|
|
1271
|
+
}
|
|
1251
1272
|
// ── /agent list(只读) ──
|
|
1252
1273
|
// 控制 channel:验 evolcore.owners,返回全量;agent channel:放行但仅返回自身单条。
|
|
1253
1274
|
if (cmd === '/agent') {
|
|
@@ -1590,6 +1611,28 @@ export async function execMenuQuery(cmd, channel, channelId, userId, args, expli
|
|
|
1590
1611
|
return menuResultFailure(error);
|
|
1591
1612
|
}
|
|
1592
1613
|
}
|
|
1614
|
+
if (cmdBase === '/connect') {
|
|
1615
|
+
const authorized = await authorizeRoleMenu(this, {
|
|
1616
|
+
kind: 'query', args, operation: connectMenuOperation('query', args), identity, subject, session, channel, channelId,
|
|
1617
|
+
userId, fromControlChannel, source,
|
|
1618
|
+
});
|
|
1619
|
+
if ('error' in authorized)
|
|
1620
|
+
return authorized;
|
|
1621
|
+
try {
|
|
1622
|
+
const result = await handleConnectMenu({ type: 'menu.query', subtype: 'query', args }, authorized.context);
|
|
1623
|
+
if (result.error) {
|
|
1624
|
+
return {
|
|
1625
|
+
error: result.error.message,
|
|
1626
|
+
code: result.error.code,
|
|
1627
|
+
...(result.error.data === undefined ? {} : { data: result.error.data }),
|
|
1628
|
+
};
|
|
1629
|
+
}
|
|
1630
|
+
return { data: result.data };
|
|
1631
|
+
}
|
|
1632
|
+
catch (error) {
|
|
1633
|
+
return menuResultFailure(error);
|
|
1634
|
+
}
|
|
1635
|
+
}
|
|
1593
1636
|
const authDenied = await authorizeMenuIntent.call(this, {
|
|
1594
1637
|
intent: buildMenuIntent('query', cmdBase, args, undefined, undefined, fromControlChannel),
|
|
1595
1638
|
identity,
|
|
@@ -2088,6 +2131,28 @@ export async function execMenuUpdate(cmd, value, channel, channelId, userId, ove
|
|
|
2088
2131
|
return menuResultFailure(error);
|
|
2089
2132
|
}
|
|
2090
2133
|
}
|
|
2134
|
+
if (cmdBase === '/connect') {
|
|
2135
|
+
const authorized = await authorizeRoleMenu(this, {
|
|
2136
|
+
kind: 'update', args, value: arg, operation: connectMenuOperation('update', args, arg), identity, subject: authSubject, session, channel, channelId,
|
|
2137
|
+
userId, fromControlChannel, source,
|
|
2138
|
+
});
|
|
2139
|
+
if ('error' in authorized)
|
|
2140
|
+
return authorized;
|
|
2141
|
+
try {
|
|
2142
|
+
const result = await handleConnectMenu({ type: 'menu.update', subtype: 'update', args, value: arg }, authorized.context);
|
|
2143
|
+
if (result.error) {
|
|
2144
|
+
return {
|
|
2145
|
+
error: result.error.message,
|
|
2146
|
+
code: result.error.code,
|
|
2147
|
+
...(result.error.data === undefined ? {} : { data: result.error.data }),
|
|
2148
|
+
};
|
|
2149
|
+
}
|
|
2150
|
+
return { data: result.data };
|
|
2151
|
+
}
|
|
2152
|
+
catch (error) {
|
|
2153
|
+
return menuResultFailure(error);
|
|
2154
|
+
}
|
|
2155
|
+
}
|
|
2091
2156
|
const authDenied = await authorizeMenuIntent.call(this, {
|
|
2092
2157
|
intent: buildMenuIntent('update', cmdBase, args, undefined, arg, fromControlChannel),
|
|
2093
2158
|
identity,
|
|
@@ -2465,6 +2530,28 @@ export async function execMenuAction(cmd, action, args, channel, channelId, user
|
|
|
2465
2530
|
return menuResultFailure(error);
|
|
2466
2531
|
}
|
|
2467
2532
|
}
|
|
2533
|
+
if (cmdBase === '/connect') {
|
|
2534
|
+
const authorized = await authorizeRoleMenu(this, {
|
|
2535
|
+
kind: 'action', args, operation: connectMenuOperation('action', args), identity: authIdentity, subject, session: authSession,
|
|
2536
|
+
channel, channelId, userId, fromControlChannel, source,
|
|
2537
|
+
});
|
|
2538
|
+
if ('error' in authorized)
|
|
2539
|
+
return authorized;
|
|
2540
|
+
try {
|
|
2541
|
+
const result = await handleConnectMenu({ type: 'menu.action', subtype: 'action', action, args }, authorized.context);
|
|
2542
|
+
if (result.error) {
|
|
2543
|
+
return {
|
|
2544
|
+
error: result.error.message,
|
|
2545
|
+
code: result.error.code,
|
|
2546
|
+
...(result.error.data === undefined ? {} : { data: result.error.data }),
|
|
2547
|
+
};
|
|
2548
|
+
}
|
|
2549
|
+
return { data: result.data };
|
|
2550
|
+
}
|
|
2551
|
+
catch (error) {
|
|
2552
|
+
return menuResultFailure(error);
|
|
2553
|
+
}
|
|
2554
|
+
}
|
|
2468
2555
|
if (cmdBase === '/agent' && !fromControlChannel && !args?.aid) {
|
|
2469
2556
|
const selfAid = this.getOwningAgent?.(channel)?.aid;
|
|
2470
2557
|
if (selfAid)
|
|
@@ -2618,7 +2705,7 @@ export async function execMenuAction(cmd, action, args, channel, channelId, user
|
|
|
2618
2705
|
const triggerScheduler = this.getTriggerSchedulerForChannel?.(channel);
|
|
2619
2706
|
if (!triggerScheduler)
|
|
2620
2707
|
return { error: '触发器功能未启用', code: 'NOT_SUPPORTED' };
|
|
2621
|
-
if (action === 'set') {
|
|
2708
|
+
if (action === 'create' || action === 'set') {
|
|
2622
2709
|
// args 结构化 → 直接组装 ParsedTriggerSet(绕过 parseTriggerSet 文本解析,无注入风险)
|
|
2623
2710
|
if (!args?.scheduleType || (args.scheduleType !== 'once' && !args?.scheduleValue) || !args?.prompt) {
|
|
2624
2711
|
return { error: '缺少必填参数:scheduleType / scheduleValue / prompt', code: 'INVALID_ARGS' };
|
|
@@ -2652,6 +2739,7 @@ export async function execMenuAction(cmd, action, args, channel, channelId, user
|
|
|
2652
2739
|
effort: args.effort,
|
|
2653
2740
|
permissionMode: args.permissionMode,
|
|
2654
2741
|
triggerThread,
|
|
2742
|
+
baseagent: args.baseagent,
|
|
2655
2743
|
};
|
|
2656
2744
|
const r = await this.registerTriggerFromParsed(parsed, channel, channelId, userId ?? '', undefined, this.resolveMenuChatType(channel, channelId, explicitChatType), undefined, isAdmin);
|
|
2657
2745
|
if (!r.ok)
|
|
@@ -3020,18 +3108,26 @@ export async function execMenuAction(cmd, action, args, channel, channelId, user
|
|
|
3020
3108
|
return { error: '操作需要 owner 权限', code: 'FORBIDDEN' };
|
|
3021
3109
|
}
|
|
3022
3110
|
if (action === 'restart') {
|
|
3023
|
-
const
|
|
3024
|
-
|
|
3025
|
-
|
|
3026
|
-
|
|
3027
|
-
|
|
3028
|
-
|
|
3029
|
-
|
|
3030
|
-
|
|
3031
|
-
|
|
3032
|
-
|
|
3111
|
+
const suppressRealRestart = shouldSuppressRealRestart();
|
|
3112
|
+
if (!suppressRealRestart) {
|
|
3113
|
+
const restartInfo = { channel, channelId, timestamp: Date.now() };
|
|
3114
|
+
const dataDir = resolvePaths().dataDir;
|
|
3115
|
+
fs.mkdirSync(dataDir, { recursive: true });
|
|
3116
|
+
fs.writeFileSync(path.join(dataDir, 'restart-pending.json'), JSON.stringify(restartInfo));
|
|
3117
|
+
const { spawn } = await import('child_process');
|
|
3118
|
+
spawn('node', [path.join(getPackageRoot(), 'dist', 'cli', 'index.js'), 'restart-monitor'], {
|
|
3119
|
+
detached: true,
|
|
3120
|
+
stdio: 'ignore',
|
|
3121
|
+
env: { ...process.env, EVOLCORE_HOME: resolvePaths().root }
|
|
3122
|
+
}).unref();
|
|
3123
|
+
}
|
|
3124
|
+
else {
|
|
3125
|
+
logger.info('[System] Suppressed real menu restart in test runtime');
|
|
3126
|
+
}
|
|
3033
3127
|
this.eventBus.publish({ type: 'system:restart', channel, channelId });
|
|
3034
|
-
|
|
3128
|
+
if (!suppressRealRestart) {
|
|
3129
|
+
setTimeout(() => { process.kill(process.pid, 'SIGTERM'); }, 1000);
|
|
3130
|
+
}
|
|
3035
3131
|
return { data: { action: 'restart', success: true } };
|
|
3036
3132
|
}
|
|
3037
3133
|
if (action === 'check') {
|
|
@@ -3239,7 +3335,8 @@ async function execMenuForSystemControl(payload, context) {
|
|
|
3239
3335
|
}
|
|
3240
3336
|
const isProcessLevel = isProcessLevelMenu(name, payload?.cmd);
|
|
3241
3337
|
const isRoleRequest = name === 'role' || payload?.cmd === '/role';
|
|
3242
|
-
const
|
|
3338
|
+
const isConnectRequest = name === 'connect' || payload?.cmd === '/connect';
|
|
3339
|
+
const requiresAgentTarget = isRoleRequest || isConnectRequest || [
|
|
3243
3340
|
'observable', 'baseagent', 'model', 'effort', 'trigger', 'capability',
|
|
3244
3341
|
].includes(name)
|
|
3245
3342
|
|| ['/observable', '/baseagent', '/model', '/effort', '/trigger', '/capability'].includes(payload?.cmd);
|
|
@@ -3342,7 +3439,8 @@ export async function execMenuForControl(payload, peerId) {
|
|
|
3342
3439
|
return menuFailure({ id, ...(name ? { name } : {}) }, validationError);
|
|
3343
3440
|
const owners = loadDaemonConfig().owners ?? [];
|
|
3344
3441
|
const isRoleRequest = name === 'role' || payload?.cmd === '/role';
|
|
3345
|
-
|
|
3442
|
+
const isConnectRequest = name === 'connect' || payload?.cmd === '/connect';
|
|
3443
|
+
if (!isRoleRequest && !isConnectRequest && !isProcessLevelOwner(peerId, owners)) {
|
|
3346
3444
|
return menuFailure({ id, ...(name ? { name } : {}) }, { code: 'ROLE_ACCESS_DENIED', message: '控制 channel 操作需要 owner 权限' });
|
|
3347
3445
|
}
|
|
3348
3446
|
return execMenuForSystemControl.call(this, payload, {
|
|
@@ -3,8 +3,8 @@ import path from 'path';
|
|
|
3
3
|
import crypto from 'crypto';
|
|
4
4
|
import { ConfigTarget, read, resolveEffectiveWithSources, write } from '../../config/config-manager.js';
|
|
5
5
|
import { getBuiltinRolesConfig, getRoleDefinition, isValidUserRoleName, readRolesConfig, } from '../../config/roles.js';
|
|
6
|
-
import { BUILTIN_USER_ROLES, MANAGEMENT_ROLES, isManagementRole } from '../../config/builtin-roles.js';
|
|
7
|
-
import { checkRoleAccess,
|
|
6
|
+
import { BUILTIN_USER_ROLES, MANAGEMENT_ROLES, expectedRoleRank, isManagementRole } from '../../config/builtin-roles.js';
|
|
7
|
+
import { checkRoleAccess, isStaticAgentOwner, resolvePeerRoleDetail } from '../../config/peer-role-resolver.js';
|
|
8
8
|
import { relationFieldWriteOperation, resolveRoleFieldPermission, } from '../../config/config-field-policy.js';
|
|
9
9
|
import { evaluateRoleOperationCapability } from '../auth/operation-authorizer.js';
|
|
10
10
|
import { listOperations } from '../auth/operation-catalog.js';
|
|
@@ -55,8 +55,10 @@ export function roleMenuOperation(kind, args, value) {
|
|
|
55
55
|
}
|
|
56
56
|
if (kind === 'action')
|
|
57
57
|
return 'role.policy.write';
|
|
58
|
-
if (args?.resource !== 'assignment')
|
|
58
|
+
if (args?.resource !== 'assignment' && args?.resource !== 'admin')
|
|
59
59
|
return 'role.policy.write';
|
|
60
|
+
if (args?.resource === 'admin')
|
|
61
|
+
return 'role.assign';
|
|
60
62
|
try {
|
|
61
63
|
return JSON.parse(value ?? '') === null ? 'role.revoke' : 'role.assign';
|
|
62
64
|
}
|
|
@@ -168,9 +170,11 @@ export async function roleMenuUpdate(context, args, rawValue) {
|
|
|
168
170
|
result = updateDefinition(context.self, args, value);
|
|
169
171
|
}
|
|
170
172
|
else if (resource === 'defaultRoles')
|
|
171
|
-
result = updateDefaultRoles(context
|
|
173
|
+
result = updateDefaultRoles(context, args, value);
|
|
172
174
|
else if (resource === 'assignment')
|
|
173
175
|
result = await updateAssignment(context, args, value);
|
|
176
|
+
else if (resource === 'admin')
|
|
177
|
+
result = await updateAdminAssignment(context, args, value);
|
|
174
178
|
else
|
|
175
179
|
throw roleError('NOT_SUPPORTED', 'The requested Role resource is not supported', context.self, { resource: resource || null });
|
|
176
180
|
auditMutation(context, args, before, result, resource === 'defaultRoles' ? 'default-update' : resource === 'assignment' ? 'assignment' : 'update');
|
|
@@ -260,6 +264,8 @@ function auditMutation(context, args, before, result, operation) {
|
|
|
260
264
|
const afterRelation = relationFile ? afterFiles[relationFile] : undefined;
|
|
261
265
|
const roleId = (stringArg(args?.roleId) || undefined)
|
|
262
266
|
?? result?.roleId
|
|
267
|
+
?? result?.assignment?.explicitRole
|
|
268
|
+
?? result?.assignment?.explicitDefaultRole
|
|
263
269
|
?? (isObject(afterRelation) && typeof afterRelation.role === 'string' ? afterRelation.role : undefined)
|
|
264
270
|
?? (isObject(beforeRelation) && typeof beforeRelation.role === 'string' ? beforeRelation.role : undefined);
|
|
265
271
|
const primaryFile = before.relation
|
|
@@ -300,7 +306,7 @@ export function recoverAllRoleMutationsSync() {
|
|
|
300
306
|
if (!fs.existsSync(agentsDir))
|
|
301
307
|
return;
|
|
302
308
|
for (const entry of fs.readdirSync(agentsDir, { withFileTypes: true })) {
|
|
303
|
-
if (!entry.isDirectory())
|
|
309
|
+
if (!entry.isDirectory() || !isValidAid(entry.name))
|
|
304
310
|
continue;
|
|
305
311
|
if (!fs.existsSync(roleJournalPath(entry.name)))
|
|
306
312
|
continue;
|
|
@@ -447,7 +453,8 @@ function updateDefinition(self, args, value) {
|
|
|
447
453
|
...(args?.remove ? { removed: normalizeRemove(args.remove) } : {}),
|
|
448
454
|
};
|
|
449
455
|
}
|
|
450
|
-
function updateDefaultRoles(
|
|
456
|
+
function updateDefaultRoles(context, args, value) {
|
|
457
|
+
const self = context.self;
|
|
451
458
|
if (!isObject(value))
|
|
452
459
|
throw invalidJsonType(self, 'defaultRoles', 'object');
|
|
453
460
|
assertOnlyKeys(value, ['private', 'group'], 'defaultRoles', self);
|
|
@@ -460,6 +467,13 @@ function updateDefaultRoles(self, args, value) {
|
|
|
460
467
|
};
|
|
461
468
|
assertPolicyRevision(self, args);
|
|
462
469
|
const registry = readRoleRegistry(self);
|
|
470
|
+
const changesAdminDefault = ['private', 'group'].some(scene => registry.defaultRoles[scene] !== nextDefaults[scene]
|
|
471
|
+
&& (registry.defaultRoles[scene] === 'admin' || nextDefaults[scene] === 'admin'));
|
|
472
|
+
if (context.actorRole !== 'owner' && changesAdminDefault) {
|
|
473
|
+
throw roleError('NOT_ALLOWED', 'Only an owner can set or replace an admin default role', self, {
|
|
474
|
+
kind: 'admin_owner_only',
|
|
475
|
+
});
|
|
476
|
+
}
|
|
463
477
|
const nextRegistry = { ...registry, defaultRoles: nextDefaults };
|
|
464
478
|
const changed = stableStringify(registry) !== stableStringify(nextRegistry);
|
|
465
479
|
if (changed)
|
|
@@ -484,30 +498,79 @@ async function updateAssignment(context, args, value) {
|
|
|
484
498
|
if (roleId !== null)
|
|
485
499
|
target = await validateTargetExists(context, target);
|
|
486
500
|
const targetAid = target.kind === 'private' ? target.peerAid : null;
|
|
487
|
-
if (targetAid &&
|
|
488
|
-
throw roleError('NOT_ALLOWED', '
|
|
489
|
-
kind: '
|
|
501
|
+
if (targetAid && isStaticAgentOwner(self, targetAid)) {
|
|
502
|
+
throw roleError('NOT_ALLOWED', 'Owner assignments cannot be changed through relations', self, {
|
|
503
|
+
kind: 'owner_protected',
|
|
490
504
|
targetAid,
|
|
491
505
|
});
|
|
492
506
|
}
|
|
493
507
|
const current = readRelation(target.peerKey, self);
|
|
508
|
+
if (context.actorRole !== 'owner' && (roleId === 'admin' || current?.config.role === 'admin')) {
|
|
509
|
+
throw roleError('NOT_ALLOWED', 'Only an owner can grant or revoke admin', self, {
|
|
510
|
+
kind: 'admin_owner_only',
|
|
511
|
+
target: target.peerKey,
|
|
512
|
+
});
|
|
513
|
+
}
|
|
494
514
|
assertRelationRevision(self, args, current?.revision ?? null, target.peerKey);
|
|
495
|
-
if (!current && roleId === null)
|
|
515
|
+
if (!current && roleId === null)
|
|
496
516
|
return assignmentResponse(self, target, null, false, null);
|
|
497
|
-
}
|
|
498
517
|
const relation = clone(current?.config ?? { $schema_version: currentVersion('relation-config') });
|
|
499
|
-
relation.targetKind = target.kind
|
|
518
|
+
relation.targetKind = target.kind;
|
|
500
519
|
if (roleId === null)
|
|
501
520
|
delete relation.role;
|
|
502
521
|
else
|
|
503
522
|
relation.role = roleId;
|
|
504
|
-
const normalized = normalizeRelationForScene(relation, target.kind
|
|
523
|
+
const normalized = normalizeRelationForScene(relation, target.kind);
|
|
505
524
|
const changed = stableStringify(current?.config ?? null) !== stableStringify(normalized);
|
|
506
525
|
if (changed)
|
|
507
526
|
write(ConfigTarget.Relation, normalized, { self, peerKey: target.peerKey });
|
|
508
527
|
const nextRevision = changed ? revision(normalized) : current?.revision ?? null;
|
|
509
528
|
return assignmentResponse(self, target, normalized, changed, nextRevision);
|
|
510
529
|
}
|
|
530
|
+
async function updateAdminAssignment(context, args, value) {
|
|
531
|
+
const self = context.self;
|
|
532
|
+
if (context.actorRole !== 'owner') {
|
|
533
|
+
throw roleError('NOT_ALLOWED', 'Only an owner can grant or revoke admin', self, { kind: 'admin_owner_only' });
|
|
534
|
+
}
|
|
535
|
+
let target = normalizeTarget(args?.target, self);
|
|
536
|
+
if (target.kind !== 'private')
|
|
537
|
+
throw invalidTargetValue(self, 'target.kind');
|
|
538
|
+
if (isStaticAgentOwner(self, target.peerAid)) {
|
|
539
|
+
throw roleError('NOT_ALLOWED', 'Owner assignments cannot be changed through contact admin', self, { kind: 'owner_protected' });
|
|
540
|
+
}
|
|
541
|
+
const enabled = value === true || value === 'grant'
|
|
542
|
+
? true
|
|
543
|
+
: value === false || value === null || value === 'revoke'
|
|
544
|
+
? false
|
|
545
|
+
: undefined;
|
|
546
|
+
if (enabled === undefined)
|
|
547
|
+
throw invalidJsonType(self, 'admin', 'boolean, grant, revoke, or null');
|
|
548
|
+
if (enabled)
|
|
549
|
+
target = await validateTargetExists(context, target);
|
|
550
|
+
const current = readRelation(target.peerKey, self);
|
|
551
|
+
assertRelationRevision(self, args, current?.revision ?? null, target.peerKey);
|
|
552
|
+
if (!current && !enabled) {
|
|
553
|
+
return {
|
|
554
|
+
$schema_version: ROLE_MENU_SCHEMA_VERSION, self, resource: 'admin', target,
|
|
555
|
+
changed: false, relationRevision: null, admin: false,
|
|
556
|
+
};
|
|
557
|
+
}
|
|
558
|
+
const relation = clone(current?.config ?? { $schema_version: currentVersion('relation-config') });
|
|
559
|
+
relation.targetKind = 'private';
|
|
560
|
+
if (enabled)
|
|
561
|
+
relation.role = 'admin';
|
|
562
|
+
else if (relation.role === 'admin')
|
|
563
|
+
delete relation.role;
|
|
564
|
+
const normalized = normalizeRelationForScene(relation, 'private');
|
|
565
|
+
const changed = stableStringify(current?.config ?? null) !== stableStringify(normalized);
|
|
566
|
+
if (changed)
|
|
567
|
+
write(ConfigTarget.Relation, normalized, { self, peerKey: target.peerKey });
|
|
568
|
+
const nextRevision = changed ? revision(normalized) : current?.revision ?? null;
|
|
569
|
+
return {
|
|
570
|
+
$schema_version: ROLE_MENU_SCHEMA_VERSION, self, resource: 'admin', target,
|
|
571
|
+
changed, relationRevision: nextRevision, admin: normalized.role === 'admin',
|
|
572
|
+
};
|
|
573
|
+
}
|
|
511
574
|
function createRole(self, args) {
|
|
512
575
|
const roleId = requiredRoleId(args, self);
|
|
513
576
|
if (!isValidUserRoleName(roleId)) {
|
|
@@ -520,6 +583,10 @@ function createRole(self, args) {
|
|
|
520
583
|
if (!isObject(args?.definition))
|
|
521
584
|
throw validationFailed(self, roleId, [{ field: 'definition', code: 'INVALID_TYPE', message: 'definition must be an object' }]);
|
|
522
585
|
const definition = clone(args.definition);
|
|
586
|
+
if (definition.rank !== undefined && definition.rank !== expectedRoleRank(roleId)) {
|
|
587
|
+
throw validationFailed(self, roleId, [{ field: 'definition.rank', code: 'INVALID_VALUE', message: 'custom role rank is fixed at 500' }]);
|
|
588
|
+
}
|
|
589
|
+
definition.rank = expectedRoleRank(roleId);
|
|
523
590
|
const errors = validateRoleConfig(definition);
|
|
524
591
|
if (errors.length)
|
|
525
592
|
throw validationFailed(self, roleId, errors);
|
|
@@ -596,6 +663,11 @@ async function deleteRole(context, args) {
|
|
|
596
663
|
const replacement = hasReplacement
|
|
597
664
|
? validateAssignableRole(args?.replacementRoleId, self, 'replacementRoleId')
|
|
598
665
|
: undefined;
|
|
666
|
+
if (replacement === 'admin' && context.actorRole !== 'owner') {
|
|
667
|
+
throw roleError('NOT_ALLOWED', 'Only an owner can replace role assignments with admin', self, {
|
|
668
|
+
kind: 'admin_owner_only',
|
|
669
|
+
});
|
|
670
|
+
}
|
|
599
671
|
if (replacement === roleId) {
|
|
600
672
|
throw validationFailed(self, roleId, [{ field: 'replacementRoleId', code: 'INVALID_VALUE', message: 'replacementRoleId must differ from roleId' }]);
|
|
601
673
|
}
|
|
@@ -646,11 +718,13 @@ async function assignmentInventory(context, args, policyRevision) {
|
|
|
646
718
|
if (args?.roleId !== undefined && !roleFilter)
|
|
647
719
|
throw validationFailed(self, undefined, [{ field: 'roleId', code: 'INVALID_VALUE', message: 'roleId must be a non-empty string' }]);
|
|
648
720
|
const items = [];
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
const
|
|
652
|
-
|
|
653
|
-
|
|
721
|
+
if (args?.includeDefaults !== false) {
|
|
722
|
+
const defaults = effectiveDefaultRoles(self);
|
|
723
|
+
for (const scene of ['private', 'group']) {
|
|
724
|
+
const roleId = defaults[scene];
|
|
725
|
+
if (roleId && (!roleFilter || roleFilter === roleId)) {
|
|
726
|
+
items.push({ target: { kind: 'default', scene }, roleId, relationRevision: null });
|
|
727
|
+
}
|
|
654
728
|
}
|
|
655
729
|
}
|
|
656
730
|
let complete = true;
|
|
@@ -930,11 +1004,13 @@ function normalizeTarget(value, self) {
|
|
|
930
1004
|
const groupId = stringArg(value.groupId);
|
|
931
1005
|
if (!groupId)
|
|
932
1006
|
throw invalidTarget(self, 'target.groupId');
|
|
1007
|
+
if (!isValidAid(groupId) && !isExplicitGroupId(groupId))
|
|
1008
|
+
throw invalidTargetValue(self, 'target.groupId');
|
|
933
1009
|
return { kind, groupId, peerKey: formatPeerKey('aun', groupId) };
|
|
934
1010
|
}
|
|
935
1011
|
throw validationFailed(self, undefined, [{ field: 'target.kind', code: 'INVALID_VALUE', message: 'target.kind must be private or group' }]);
|
|
936
1012
|
}
|
|
937
|
-
function targetRoleView(self, target, relation) {
|
|
1013
|
+
function targetRoleView(self, target, relation = null) {
|
|
938
1014
|
if (target.kind === 'private') {
|
|
939
1015
|
const explicitRole = relation?.role ?? null;
|
|
940
1016
|
const detail = resolvePeerRoleDetail({
|
|
@@ -1086,7 +1162,15 @@ function policyDocument(agent) {
|
|
|
1086
1162
|
function roleMeta(self, roleId) {
|
|
1087
1163
|
if (isManagementRole(roleId)) {
|
|
1088
1164
|
const overridden = fs.existsSync(agentRoleConfig(self, roleId));
|
|
1089
|
-
return {
|
|
1165
|
+
return {
|
|
1166
|
+
origin: 'builtin',
|
|
1167
|
+
overridden,
|
|
1168
|
+
kind: 'management',
|
|
1169
|
+
assignable: roleId === 'admin',
|
|
1170
|
+
editable: true,
|
|
1171
|
+
deletable: false,
|
|
1172
|
+
resettable: true,
|
|
1173
|
+
};
|
|
1090
1174
|
}
|
|
1091
1175
|
const builtin = Object.prototype.hasOwnProperty.call(getBuiltinRolesConfig().roles, roleId);
|
|
1092
1176
|
const overridden = fs.existsSync(agentRoleConfig(self, roleId));
|
|
@@ -1131,7 +1215,7 @@ function parseUpdateValue(rawValue, self) {
|
|
|
1131
1215
|
function validateAssignableRole(value, self, field) {
|
|
1132
1216
|
if (value === null)
|
|
1133
1217
|
return null;
|
|
1134
|
-
if (typeof value !== 'string' || !value.trim() ||
|
|
1218
|
+
if (typeof value !== 'string' || !value.trim() || value === 'owner' || !getRoleDefinition(value, self)) {
|
|
1135
1219
|
throw validationFailed(self, undefined, [{ field, code: 'INVALID_VALUE', message: 'value must be null or an existing assignable user role' }]);
|
|
1136
1220
|
}
|
|
1137
1221
|
return value;
|
|
@@ -1288,11 +1372,15 @@ function validateRoleDomainTransactionChanges(self, registry, changes) {
|
|
|
1288
1372
|
for (const change of changes) {
|
|
1289
1373
|
if (!change.after || !change.file.endsWith('.json') || change.file.endsWith('index.json'))
|
|
1290
1374
|
continue;
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1375
|
+
const isRelation = change.file.includes(`${path.sep}relations${path.sep}`);
|
|
1376
|
+
const schema = loadSchema(isRelation ? 'relation-config' : 'role-config');
|
|
1377
|
+
const value = JSON.parse(change.after);
|
|
1378
|
+
if (!schema.validate(value)) {
|
|
1379
|
+
throw validationFailed(self, undefined, [{
|
|
1380
|
+
field: isRelation ? 'relation' : 'role',
|
|
1381
|
+
code: 'SCHEMA_INVALID',
|
|
1382
|
+
message: isRelation ? 'invalid relation config' : 'invalid role definition',
|
|
1383
|
+
}]);
|
|
1296
1384
|
}
|
|
1297
1385
|
}
|
|
1298
1386
|
}
|
|
@@ -1337,12 +1425,23 @@ function removeJournalFiles(journalFile) {
|
|
|
1337
1425
|
}
|
|
1338
1426
|
function validateJournal(self, journal) {
|
|
1339
1427
|
const root = path.resolve(agentDir(self));
|
|
1340
|
-
if (journal.$schema_version !== 1 || journal.self !== self
|
|
1428
|
+
if (journal.$schema_version !== 1 || journal.self !== self
|
|
1429
|
+
|| (journal.state !== 'prepared' && journal.state !== 'committed')
|
|
1430
|
+
|| !Array.isArray(journal.changes))
|
|
1341
1431
|
throw new Error('Invalid Role mutation journal');
|
|
1432
|
+
const seen = new Set();
|
|
1342
1433
|
for (const change of journal.changes) {
|
|
1434
|
+
if (!change || typeof change !== 'object' || typeof change.file !== 'string'
|
|
1435
|
+
|| (change.before !== null && typeof change.before !== 'string')
|
|
1436
|
+
|| (change.after !== null && typeof change.after !== 'string')) {
|
|
1437
|
+
throw new Error('Role mutation journal contains an invalid change');
|
|
1438
|
+
}
|
|
1343
1439
|
const file = path.resolve(change.file);
|
|
1344
|
-
if (file
|
|
1440
|
+
if (file === root || !file.startsWith(root + path.sep))
|
|
1345
1441
|
throw new Error('Role mutation journal contains an unsafe path');
|
|
1442
|
+
if (seen.has(file))
|
|
1443
|
+
throw new Error('Role mutation journal contains duplicate file changes');
|
|
1444
|
+
seen.add(file);
|
|
1346
1445
|
}
|
|
1347
1446
|
}
|
|
1348
1447
|
function applyFileContent(file, content) {
|
|
@@ -1498,7 +1597,7 @@ function operationLabel(id) {
|
|
|
1498
1597
|
return labels[id] ?? title(id);
|
|
1499
1598
|
}
|
|
1500
1599
|
function validRoleOrNull(value, self) {
|
|
1501
|
-
return typeof value === 'string' &&
|
|
1600
|
+
return typeof value === 'string' && value !== 'owner' && getRoleDefinition(value, self) ? value : null;
|
|
1502
1601
|
}
|
|
1503
1602
|
function getNested(value, field) {
|
|
1504
1603
|
let cursor = value;
|
|
@@ -3,6 +3,7 @@ import { getCodexEfforts } from '../../agents/codex-runner.js';
|
|
|
3
3
|
import { buildEnvelope } from '../message/message-utils.js';
|
|
4
4
|
import { resolvePaths, getPackageRoot } from '../../paths.js';
|
|
5
5
|
import { logger } from '../../utils/logger.js';
|
|
6
|
+
import { shouldSuppressRealRestart } from '../../utils/restart-safety.js';
|
|
6
7
|
import crypto from 'crypto';
|
|
7
8
|
import path from 'path';
|
|
8
9
|
import fs from 'fs';
|
|
@@ -186,7 +187,7 @@ function triggerOperationForSlash(content) {
|
|
|
186
187
|
return 'trigger.show';
|
|
187
188
|
if (subcommand === 'history')
|
|
188
189
|
return 'trigger.history';
|
|
189
|
-
if (subcommand === 'set')
|
|
190
|
+
if (subcommand === 'create' || subcommand === 'set')
|
|
190
191
|
return 'trigger.create';
|
|
191
192
|
if (subcommand === 'update')
|
|
192
193
|
return 'trigger.update';
|
|
@@ -351,7 +352,7 @@ async function getGitWorkingDirInfo(projectPath) {
|
|
|
351
352
|
return null; // git 命令失败时静默返回 null
|
|
352
353
|
}
|
|
353
354
|
}
|
|
354
|
-
export async function handleSlashCommand(content, channel, channelId, sendMessage, userId, threadId, chatType, source, messageId, selfAID, overrideIdentity, overrideSubject) {
|
|
355
|
+
export async function handleSlashCommand(content, channel, channelId, sendMessage, userId, threadId, chatType, source, messageId, selfAID, overrideIdentity, overrideSubject, replyContext) {
|
|
355
356
|
// 卡片回调的 chatType 不可靠(飞书 bot 单聊 chatId 也是 oc_ 前缀),
|
|
356
357
|
// 不应覆盖 session 中已有的正确值
|
|
357
358
|
if (source === 'card-trigger')
|
|
@@ -1628,11 +1629,12 @@ export async function handleSlashCommand(content, channel, channelId, sendMessag
|
|
|
1628
1629
|
}
|
|
1629
1630
|
if (normalizedContent === '/activity' || normalizedContent.startsWith('/activity ')) {
|
|
1630
1631
|
const activityArg = normalizedContent.slice(9).trim();
|
|
1632
|
+
const activitySession = await getExistingSessionForCommand();
|
|
1631
1633
|
// 带参(写操作)需 admin+;无参查询对所有人开放(owner 门在具体切换点还有一道)
|
|
1632
1634
|
if (activityArg && !isAdmin)
|
|
1633
1635
|
return { kind: 'command.error', text: '❌ 无权限:此命令仅限管理员使用' };
|
|
1634
1636
|
// proactive 模式下流式输出全部静默,activity 配置无意义
|
|
1635
|
-
if (getEffectiveChatmode(
|
|
1637
|
+
if (getEffectiveChatmode(activitySession) === 'proactive') {
|
|
1636
1638
|
return { kind: 'command.error', text: '❌ 当前会话为 proactive 模式,不支持 activity 配置(流式输出已全部静默)' };
|
|
1637
1639
|
}
|
|
1638
1640
|
const modeMap = {
|
|
@@ -1655,7 +1657,7 @@ export async function handleSlashCommand(content, channel, channelId, sendMessag
|
|
|
1655
1657
|
type: 'interaction',
|
|
1656
1658
|
id: `activity-${Date.now()}-${crypto.randomBytes(4).toString('hex')}`,
|
|
1657
1659
|
channelId,
|
|
1658
|
-
sessionId:
|
|
1660
|
+
sessionId: activitySession?.id || '',
|
|
1659
1661
|
initiatorId: userId,
|
|
1660
1662
|
kind: {
|
|
1661
1663
|
kind: 'command-card',
|
|
@@ -1669,7 +1671,7 @@ export async function handleSlashCommand(content, channel, channelId, sendMessag
|
|
|
1669
1671
|
})),
|
|
1670
1672
|
},
|
|
1671
1673
|
};
|
|
1672
|
-
const replyCtx =
|
|
1674
|
+
const replyCtx = replyContext ?? (activitySession ? this.getReplyContext(activitySession) : undefined);
|
|
1673
1675
|
const cardResult = await this.sendCommandCard({ channel, channelId, interaction, replyCtx, canWrite: isOwner });
|
|
1674
1676
|
if (cardResult === null)
|
|
1675
1677
|
return null;
|
|
@@ -2370,6 +2372,7 @@ export async function handleSlashCommand(content, channel, channelId, sendMessag
|
|
|
2370
2372
|
});
|
|
2371
2373
|
// 执行重启逻辑(共用于卡片回调和文本确认)
|
|
2372
2374
|
const executeRestart = async () => {
|
|
2375
|
+
const suppressRealRestart = shouldSuppressRealRestart();
|
|
2373
2376
|
let replyContext;
|
|
2374
2377
|
if (threadId) {
|
|
2375
2378
|
const threadSession = await this.sessionManager.getThreadSession(channel, channelId, threadId);
|
|
@@ -2382,15 +2385,20 @@ export async function handleSlashCommand(content, channel, channelId, sendMessag
|
|
|
2382
2385
|
timestamp: Date.now(),
|
|
2383
2386
|
...(replyContext?.replyToMessageId ? { rootId: replyContext.replyToMessageId } : {}),
|
|
2384
2387
|
};
|
|
2385
|
-
|
|
2386
|
-
|
|
2387
|
-
|
|
2388
|
-
|
|
2389
|
-
|
|
2390
|
-
|
|
2391
|
-
|
|
2392
|
-
|
|
2393
|
-
|
|
2388
|
+
if (!suppressRealRestart) {
|
|
2389
|
+
const dataDir = resolvePaths().dataDir;
|
|
2390
|
+
fs.mkdirSync(dataDir, { recursive: true });
|
|
2391
|
+
fs.writeFileSync(path.join(dataDir, 'restart-pending.json'), JSON.stringify(restartInfo));
|
|
2392
|
+
const { spawn } = await import('child_process');
|
|
2393
|
+
spawn('node', [path.join(getPackageRoot(), 'dist', 'cli', 'index.js'), 'restart-monitor'], {
|
|
2394
|
+
detached: true,
|
|
2395
|
+
stdio: 'ignore',
|
|
2396
|
+
env: { ...process.env, EVOLCORE_HOME: resolvePaths().root }
|
|
2397
|
+
}).unref();
|
|
2398
|
+
}
|
|
2399
|
+
else {
|
|
2400
|
+
logger.info('[System] Suppressed real restart in test runtime');
|
|
2401
|
+
}
|
|
2394
2402
|
this.eventBus.publish({ type: 'system:restart', channel, channelId });
|
|
2395
2403
|
// 先发送重启反馈消息,等待发送完成后再 kill 进程
|
|
2396
2404
|
// 避免消息还没发出去进程就退出了
|
|
@@ -2416,10 +2424,12 @@ export async function handleSlashCommand(content, channel, channelId, sendMessag
|
|
|
2416
2424
|
// 发 SIGTERM 而非直接 process.exit(0),让 index.ts 的 shutdown() 先
|
|
2417
2425
|
// 正常关闭所有 channel(包括 Feishu WebSocket close frame),
|
|
2418
2426
|
// 避免 Feishu 服务端因连接异常断开而重推未 ack 的消息给新进程。
|
|
2419
|
-
|
|
2420
|
-
|
|
2421
|
-
|
|
2422
|
-
|
|
2427
|
+
if (!suppressRealRestart) {
|
|
2428
|
+
setTimeout(() => {
|
|
2429
|
+
logger.info('[System] Restarting by user command...');
|
|
2430
|
+
process.kill(process.pid, 'SIGTERM');
|
|
2431
|
+
}, 1000);
|
|
2432
|
+
}
|
|
2423
2433
|
return true;
|
|
2424
2434
|
};
|
|
2425
2435
|
// 文本确认流程
|