evolclaw 3.1.10 → 3.2.0
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 +38 -0
- package/README.md +26 -4
- package/dist/agents/kit-renderer.js +5 -1
- package/dist/agents/manifest-engine.js +108 -35
- package/dist/agents/message-renderer.js +2 -0
- package/dist/aun/aid/control-aid.js +67 -0
- package/dist/aun/aid/identity.js +20 -7
- package/dist/aun/aid/store.js +2 -2
- package/dist/channels/aun.js +212 -158
- package/dist/channels/feishu.js +10 -14
- package/dist/channels/wechat.js +8 -2
- package/dist/cli/agent.js +38 -10
- package/dist/cli/index.js +50 -8
- package/dist/cli/init-channel.js +38 -148
- package/dist/cli/init.js +162 -82
- package/dist/config-store.js +38 -7
- package/dist/core/cache/file-cache.js +216 -0
- package/dist/core/command-handler.js +291 -68
- package/dist/core/evolagent-registry.js +3 -0
- package/dist/core/evolagent.js +28 -23
- package/dist/core/message/command-handler-agent-control.js +153 -0
- package/dist/core/message/create-status.js +67 -0
- package/dist/core/message/message-bridge.js +5 -3
- package/dist/core/message/message-processor.js +44 -36
- package/dist/core/message/message-queue.js +13 -6
- package/dist/core/model/model-scope.js +39 -6
- package/dist/core/session/adapters/claude-session-file-adapter.js +48 -5
- package/dist/evolclaw-config.js +11 -0
- package/dist/index.js +57 -2
- package/dist/ipc.js +6 -0
- package/dist/paths.js +7 -3
- package/dist/utils/media-cache.js +40 -1
- package/dist/utils/npm-ops.js +13 -3
- package/kits/templates/message-fragments/item.md +1 -1
- package/package.json +1 -1
|
@@ -13,6 +13,9 @@ import { parseTriggerSet, parseTriggerUpdate } from './trigger/parser.js';
|
|
|
13
13
|
import { calcNextFireAt } from './trigger/scheduler.js';
|
|
14
14
|
import { checkLatestVersion, getLocalVersion, isLinkedInstall, compareVersions } from '../utils/npm-ops.js';
|
|
15
15
|
import { tryParseChannelKey } from './channel-loader.js';
|
|
16
|
+
import { loadDefaults } from '../config-store.js';
|
|
17
|
+
import { loadEvolclawConfig } from '../evolclaw-config.js';
|
|
18
|
+
import { execAgentAction, execAgentQuery, execAgentOptions, resolveProjectPath } from './message/command-handler-agent-control.js';
|
|
16
19
|
const allEfforts = ['low', 'medium', 'high', 'xhigh', 'max'];
|
|
17
20
|
const nonMaxEfforts = allEfforts.filter(e => e !== 'max' && e !== 'xhigh');
|
|
18
21
|
// ── CLI 透传(menu.action name=cli action=exec)─────────────────────────
|
|
@@ -573,8 +576,37 @@ export class CommandHandler {
|
|
|
573
576
|
return items;
|
|
574
577
|
}
|
|
575
578
|
/** 动态子菜单:根据 cmd 路径返回选项列表(供 menu.query + cmd 使用) */
|
|
576
|
-
async getSubMenuItems(cmd, channel, channelId, userId) {
|
|
579
|
+
async getSubMenuItems(cmd, channel, channelId, userId, args) {
|
|
577
580
|
const session = await this.sessionManager.getActiveSession(channel, channelId);
|
|
581
|
+
// ── 进程级 /agent list(owners 鉴权) ──
|
|
582
|
+
if (cmd === '/agent') {
|
|
583
|
+
if (!isProcessLevelOwner(userId, loadEvolclawConfig().owners)) {
|
|
584
|
+
throw { code: 'FORBIDDEN', message: '操作需要 owner 权限' };
|
|
585
|
+
}
|
|
586
|
+
const res = await execAgentOptions(args);
|
|
587
|
+
if ('error' in res)
|
|
588
|
+
throw { code: res.code, message: res.error };
|
|
589
|
+
return res.data.agents.map(ag => ({ value: ag.aid, label: ag.name || ag.aid, desc: ag.status }));
|
|
590
|
+
}
|
|
591
|
+
// ── 关系级 /trigger list(每个 trigger 一个 MenuItem) ──
|
|
592
|
+
if (cmd === '/trigger') {
|
|
593
|
+
const owningAgent = this.getOwningAgent(channel);
|
|
594
|
+
const manager = (owningAgent?.triggerManager ?? this.triggerManager);
|
|
595
|
+
if (!manager)
|
|
596
|
+
return [];
|
|
597
|
+
const scope = args?.options === 'all' ? 'all' : 'enabled';
|
|
598
|
+
const role = this.sessionManager.resolveIdentity(channel, userId).role;
|
|
599
|
+
const isAdmin = role === 'owner' || role === 'admin';
|
|
600
|
+
const all = manager.listAll();
|
|
601
|
+
const list = scope === 'all' ? all.active.concat(all.history) : manager.listActive();
|
|
602
|
+
const visible = isAdmin ? list
|
|
603
|
+
: list.filter((t) => t.createdByPeerId === (userId ?? '') && t.createdByChannel === channel);
|
|
604
|
+
return visible.map((t) => ({
|
|
605
|
+
value: t.id,
|
|
606
|
+
label: t.name,
|
|
607
|
+
desc: `${t.scheduleType}${t.nextFireAt ? ` | 下次 ${new Date(t.nextFireAt).toLocaleString()}` : ''}`,
|
|
608
|
+
}));
|
|
609
|
+
}
|
|
578
610
|
if (cmd === '/s' || cmd === '/session' || cmd === '/del') {
|
|
579
611
|
const sessions = await this.sessionManager.listSessions(channel, channelId);
|
|
580
612
|
const active = cmd === '/del' ? await this.sessionManager.getActiveSession(channel, channelId) : null;
|
|
@@ -698,12 +730,18 @@ export class CommandHandler {
|
|
|
698
730
|
return s ? null : { error: '当前无活跃会话', code: 'NO_ACTIVE_SESSION' };
|
|
699
731
|
}
|
|
700
732
|
/** menu.query — 查询当前值。 */
|
|
701
|
-
async execMenuQuery(cmd, channel, channelId, userId) {
|
|
702
|
-
void userId;
|
|
733
|
+
async execMenuQuery(cmd, channel, channelId, userId, args) {
|
|
703
734
|
const cmdBase = cmd.trim().split(' ')[0];
|
|
704
735
|
if (!cmdBase)
|
|
705
736
|
return { error: '缺少命令', code: 'MISSING_CMD' };
|
|
706
737
|
const { session, evolagent } = await this.loadMenuContext(channel, channelId);
|
|
738
|
+
// ── 进程级 /agent(owners 鉴权) ──
|
|
739
|
+
if (cmdBase === '/agent') {
|
|
740
|
+
if (!isProcessLevelOwner(userId, loadEvolclawConfig().owners)) {
|
|
741
|
+
return { error: '操作需要 owner 权限', code: 'FORBIDDEN' };
|
|
742
|
+
}
|
|
743
|
+
return await execAgentQuery(args);
|
|
744
|
+
}
|
|
707
745
|
if (cmdBase === '/pwd') {
|
|
708
746
|
const sessPath = session?.projectPath;
|
|
709
747
|
const fallbackPath = evolagent?.config?.projects?.defaultPath;
|
|
@@ -792,6 +830,9 @@ export class CommandHandler {
|
|
|
792
830
|
const fallback = evolagent?.config?.dispatch;
|
|
793
831
|
return { data: { mode: sessionMode ?? fallback ?? null } };
|
|
794
832
|
}
|
|
833
|
+
if (cmdBase === '/observable') {
|
|
834
|
+
return { data: { observable: evolagent?.getObservable() ?? false } };
|
|
835
|
+
}
|
|
795
836
|
if (cmdBase === '/perm') {
|
|
796
837
|
const need = this.requireSession(session);
|
|
797
838
|
if (need)
|
|
@@ -804,6 +845,9 @@ export class CommandHandler {
|
|
|
804
845
|
return { data: { mode: currentMode } };
|
|
805
846
|
}
|
|
806
847
|
if (cmdBase === '/system') {
|
|
848
|
+
if (!isProcessLevelOwner(userId, loadEvolclawConfig().owners)) {
|
|
849
|
+
return { error: '操作需要 owner 权限', code: 'FORBIDDEN' };
|
|
850
|
+
}
|
|
807
851
|
const owningAgent = this.getOwningAgent(channel);
|
|
808
852
|
const data = {
|
|
809
853
|
agent: owningAgent?.name ?? 'DefaultAgent',
|
|
@@ -836,6 +880,56 @@ export class CommandHandler {
|
|
|
836
880
|
return { error: '缺少 value 参数', code: 'MISSING_VALUE' };
|
|
837
881
|
const { session, evolagent } = await this.loadMenuContext(channel, channelId);
|
|
838
882
|
const identity = this.sessionManager.resolveIdentity(channel, userId);
|
|
883
|
+
// ── 关系级 /trigger update(调度参数,value 为 JSON 字符串) ──
|
|
884
|
+
if (cmdBase === '/trigger') {
|
|
885
|
+
const owningAgent = this.getOwningAgent(channel);
|
|
886
|
+
const manager = (owningAgent?.triggerManager ?? this.triggerManager);
|
|
887
|
+
const scheduler = (owningAgent?.triggerScheduler ?? this.triggerScheduler);
|
|
888
|
+
if (!manager || !scheduler)
|
|
889
|
+
return { error: '触发器功能未启用', code: 'NOT_SUPPORTED' };
|
|
890
|
+
let patch;
|
|
891
|
+
try {
|
|
892
|
+
patch = JSON.parse(arg);
|
|
893
|
+
}
|
|
894
|
+
catch {
|
|
895
|
+
return { error: 'value 需为 JSON', code: 'INVALID_ARGS' };
|
|
896
|
+
}
|
|
897
|
+
if (!patch?.nameOrId)
|
|
898
|
+
return { error: '缺少 nameOrId', code: 'INVALID_ARGS' };
|
|
899
|
+
const isAdmin = identity.role === 'owner' || identity.role === 'admin';
|
|
900
|
+
if (!isAdmin && !userId)
|
|
901
|
+
return { error: '无法确认身份,请确保渠道提供发送者 ID', code: 'FORBIDDEN' };
|
|
902
|
+
const trigger = isAdmin
|
|
903
|
+
? (manager.getByName(patch.nameOrId) ?? manager.getById(patch.nameOrId))
|
|
904
|
+
: (manager.getByNameScoped(patch.nameOrId, userId ?? '', channel) ?? manager.getByIdScoped(patch.nameOrId, userId ?? '', channel));
|
|
905
|
+
if (!trigger)
|
|
906
|
+
return { error: '触发器不存在或无权限', code: 'NOT_FOUND' };
|
|
907
|
+
const fields = {};
|
|
908
|
+
if (patch.scheduleType !== undefined)
|
|
909
|
+
fields.scheduleType = patch.scheduleType;
|
|
910
|
+
if (patch.scheduleValue !== undefined)
|
|
911
|
+
fields.scheduleValue = String(patch.scheduleValue);
|
|
912
|
+
if (patch.prompt !== undefined)
|
|
913
|
+
fields.prompt = String(patch.prompt);
|
|
914
|
+
// 调度参数变化时重算 nextFireAt——先校验避免 NaN 污染 scheduler heap
|
|
915
|
+
if (fields.scheduleType !== undefined || fields.scheduleValue !== undefined) {
|
|
916
|
+
const effType = fields.scheduleType ?? trigger.scheduleType;
|
|
917
|
+
const effValue = fields.scheduleValue ?? trigger.scheduleValue;
|
|
918
|
+
const schedErr = validateScheduleParams(effType, effValue);
|
|
919
|
+
if (schedErr)
|
|
920
|
+
return { error: schedErr, code: 'INVALID_ARGS' };
|
|
921
|
+
fields.nextFireAt = calcNextFireAt(effType, effValue, Date.now());
|
|
922
|
+
}
|
|
923
|
+
let updated;
|
|
924
|
+
try {
|
|
925
|
+
updated = manager.update(trigger.id, fields);
|
|
926
|
+
}
|
|
927
|
+
catch (err) {
|
|
928
|
+
return { error: `更新失败:${err?.message || err}`, code: 'INVALID_ARGS' };
|
|
929
|
+
}
|
|
930
|
+
scheduler.update(updated);
|
|
931
|
+
return { data: { id: updated.id, nextFireAt: updated.nextFireAt } };
|
|
932
|
+
}
|
|
839
933
|
if (cmdBase === '/baseagent') {
|
|
840
934
|
const valid = this.getAvailableBaseagents(channel);
|
|
841
935
|
if (valid.length && !valid.includes(arg)) {
|
|
@@ -945,6 +1039,16 @@ export class CommandHandler {
|
|
|
945
1039
|
this.agentRegistry.setShowActivities(channel, newMode);
|
|
946
1040
|
return { data: { mode: newMode } };
|
|
947
1041
|
}
|
|
1042
|
+
if (cmdBase === '/observable') {
|
|
1043
|
+
if (identity.role !== 'owner')
|
|
1044
|
+
return { error: '观察者模式仅限 owner 开关', code: 'NO_PERMISSION' };
|
|
1045
|
+
if (arg !== 'true' && arg !== 'false')
|
|
1046
|
+
return { error: `无效值: ${arg},可选: true / false`, code: 'INVALID_VALUE' };
|
|
1047
|
+
if (!evolagent)
|
|
1048
|
+
return { error: '找不到通道所属 agent,无法持久化', code: 'EXEC_FAILED' };
|
|
1049
|
+
evolagent.setObservable(arg === 'true');
|
|
1050
|
+
return { data: { observable: arg === 'true' } };
|
|
1051
|
+
}
|
|
948
1052
|
return { error: `不支持 update: ${cmdBase}`, code: 'NOT_SUPPORTED' };
|
|
949
1053
|
}
|
|
950
1054
|
/** menu.action — 触发动词。 */
|
|
@@ -956,6 +1060,77 @@ export class CommandHandler {
|
|
|
956
1060
|
return { error: '缺少 action', code: 'MISSING_VALUE' };
|
|
957
1061
|
const { session } = await this.loadMenuContext(channel, channelId);
|
|
958
1062
|
const identity = this.sessionManager.resolveIdentity(channel, userId);
|
|
1063
|
+
// ── 进程级 /agent(owners 鉴权,不依赖 session/channel) ──
|
|
1064
|
+
// NOTE(D5): 本次进程级 /agent 仅按 evolclaw.json owners 鉴权,任意 evolagent 的 AUN
|
|
1065
|
+
// channel 均可作为入口。part1(daemon 控制 AID)落地后,应叠加 isControlChannel(channelId)
|
|
1066
|
+
// 闸:仅控制 AID channel 上的 /agent /system 生效。见 part1 计划。
|
|
1067
|
+
if (cmdBase === '/agent') {
|
|
1068
|
+
if (!isProcessLevelOwner(userId, loadEvolclawConfig().owners)) {
|
|
1069
|
+
return { error: '操作需要 owner 权限', code: 'FORBIDDEN' };
|
|
1070
|
+
}
|
|
1071
|
+
const a = { ...(args ?? {}) };
|
|
1072
|
+
if (action === 'create') {
|
|
1073
|
+
a.project = resolveProjectPath(a.project, a.aid ?? '', loadDefaults());
|
|
1074
|
+
}
|
|
1075
|
+
return await execAgentAction(action, a, userId ?? '');
|
|
1076
|
+
}
|
|
1077
|
+
// ── 关系级 /trigger(不走 owners;复用 isAdmin + scoped 逻辑,D4 直调底层) ──
|
|
1078
|
+
if (cmdBase === '/trigger') {
|
|
1079
|
+
const role = identity.role;
|
|
1080
|
+
const isAdmin = role === 'owner' || role === 'admin';
|
|
1081
|
+
const owningAgent = this.getOwningAgent(channel);
|
|
1082
|
+
const manager = (owningAgent?.triggerManager ?? this.triggerManager);
|
|
1083
|
+
const scheduler = (owningAgent?.triggerScheduler ?? this.triggerScheduler);
|
|
1084
|
+
if (!manager || !scheduler)
|
|
1085
|
+
return { error: '触发器功能未启用', code: 'NOT_SUPPORTED' };
|
|
1086
|
+
if (action === 'set') {
|
|
1087
|
+
// args 结构化 → 直接组装 ParsedTriggerSet(绕过 parseTriggerSet 文本解析,无注入风险)
|
|
1088
|
+
if (!args?.scheduleType || !args?.scheduleValue || !args?.prompt) {
|
|
1089
|
+
return { error: '缺少必填参数:scheduleType / scheduleValue / prompt', code: 'INVALID_ARGS' };
|
|
1090
|
+
}
|
|
1091
|
+
// menu 路径绕过了 parseTriggerSet 的校验,必须自行校验枚举/数值,
|
|
1092
|
+
// 否则非法值会传到 calcNextFireAt 产出 NaN nextFireAt,污染 scheduler heap。
|
|
1093
|
+
const schedErr = validateScheduleParams(args.scheduleType, String(args.scheduleValue));
|
|
1094
|
+
if (schedErr)
|
|
1095
|
+
return { error: schedErr, code: 'INVALID_ARGS' };
|
|
1096
|
+
const strategy = args.targetSessionStrategy ?? 'latest';
|
|
1097
|
+
if (!['latest', 'current', 'thread'].includes(strategy)) {
|
|
1098
|
+
return { error: `无效 targetSessionStrategy: ${strategy}`, code: 'INVALID_ARGS' };
|
|
1099
|
+
}
|
|
1100
|
+
const parsed = {
|
|
1101
|
+
scheduleType: args.scheduleType,
|
|
1102
|
+
scheduleValue: String(args.scheduleValue),
|
|
1103
|
+
prompt: String(args.prompt),
|
|
1104
|
+
name: args.name,
|
|
1105
|
+
targetChannel: args.targetChannel,
|
|
1106
|
+
targetChannelId: args.targetChannelId,
|
|
1107
|
+
targetThreadId: args.targetThreadId,
|
|
1108
|
+
targetSessionStrategy: strategy,
|
|
1109
|
+
agentId: args.agentId,
|
|
1110
|
+
};
|
|
1111
|
+
const r = await this.registerTriggerFromParsed(parsed, channel, channelId, userId ?? '', undefined);
|
|
1112
|
+
if (!r.ok)
|
|
1113
|
+
return { error: r.error, code: /已存在|exists|重复/.test(r.error) ? 'CONFLICT' : 'INVALID_ARGS' };
|
|
1114
|
+
return { data: { id: r.trigger.id, name: r.trigger.name, nextFireAt: r.trigger.nextFireAt } };
|
|
1115
|
+
}
|
|
1116
|
+
if (action === 'cancel') {
|
|
1117
|
+
const nameOrId = args?.nameOrId;
|
|
1118
|
+
if (!nameOrId)
|
|
1119
|
+
return { error: '缺少 nameOrId', code: 'INVALID_ARGS' };
|
|
1120
|
+
if (!isAdmin && !userId)
|
|
1121
|
+
return { error: '无法确认身份,请确保渠道提供发送者 ID', code: 'FORBIDDEN' };
|
|
1122
|
+
const trigger = isAdmin
|
|
1123
|
+
? (manager.getByName(nameOrId) ?? manager.getById(nameOrId))
|
|
1124
|
+
: (manager.getByNameScoped(nameOrId, userId ?? '', channel) ?? manager.getByIdScoped(nameOrId, userId ?? '', channel));
|
|
1125
|
+
if (!trigger)
|
|
1126
|
+
return { error: '触发器不存在或无权限', code: 'NOT_FOUND' };
|
|
1127
|
+
manager.moveToDone(trigger.id, 'cancelled');
|
|
1128
|
+
scheduler.cancel(trigger.id);
|
|
1129
|
+
this.eventBus.publish({ type: 'trigger:cancelled', triggerId: trigger.id, by: userId ?? '' });
|
|
1130
|
+
return { data: { id: trigger.id, cancelled: true } };
|
|
1131
|
+
}
|
|
1132
|
+
return { error: `不支持的 trigger action: ${action}`, code: 'INVALID_ARGS' };
|
|
1133
|
+
}
|
|
959
1134
|
// ── /session 系列 ──
|
|
960
1135
|
if (cmdBase === '/session' || cmdBase === '/s') {
|
|
961
1136
|
if (action === 'stop') {
|
|
@@ -1011,9 +1186,11 @@ export class CommandHandler {
|
|
|
1011
1186
|
}
|
|
1012
1187
|
// ── /system 系列 ──
|
|
1013
1188
|
if (cmdBase === '/system') {
|
|
1189
|
+
// D1 迁移:进程级鉴权统一查 evolclaw.json owners,替代各 action 内联的 identity.role 判断
|
|
1190
|
+
if (!isProcessLevelOwner(userId, loadEvolclawConfig().owners)) {
|
|
1191
|
+
return { error: '操作需要 owner 权限', code: 'FORBIDDEN' };
|
|
1192
|
+
}
|
|
1014
1193
|
if (action === 'restart') {
|
|
1015
|
-
if (identity.role !== 'owner')
|
|
1016
|
-
return { error: '无权限:服务重启仅限 owner 使用', code: 'NO_PERMISSION' };
|
|
1017
1194
|
const restartInfo = { channel, channelId, timestamp: Date.now() };
|
|
1018
1195
|
fs.writeFileSync(path.join(resolvePaths().dataDir, 'restart-pending.json'), JSON.stringify(restartInfo));
|
|
1019
1196
|
const { spawn } = await import('child_process');
|
|
@@ -1030,8 +1207,6 @@ export class CommandHandler {
|
|
|
1030
1207
|
return await this.delegateAsAction(action, '/check', channel, channelId, userId);
|
|
1031
1208
|
}
|
|
1032
1209
|
if (action === 'upgrade') {
|
|
1033
|
-
if (identity.role !== 'owner')
|
|
1034
|
-
return { error: '无权限:升级仅限 owner 使用', code: 'NO_PERMISSION' };
|
|
1035
1210
|
return await this.delegateAsAction(action, '/upgrade', channel, channelId, userId);
|
|
1036
1211
|
}
|
|
1037
1212
|
return { error: `不支持的 system action: ${action}`, code: 'NOT_SUPPORTED' };
|
|
@@ -3254,70 +3429,83 @@ export class CommandHandler {
|
|
|
3254
3429
|
const result = parseTriggerSet(args);
|
|
3255
3430
|
if (!result.ok)
|
|
3256
3431
|
return `❌ ${result.error}`;
|
|
3257
|
-
const
|
|
3258
|
-
|
|
3259
|
-
|
|
3260
|
-
|
|
3261
|
-
|
|
3262
|
-
|
|
3263
|
-
|
|
3264
|
-
|
|
3265
|
-
|
|
3266
|
-
|
|
3267
|
-
|
|
3268
|
-
|
|
3269
|
-
|
|
3270
|
-
|
|
3271
|
-
|
|
3272
|
-
|
|
3273
|
-
|
|
3274
|
-
|
|
3275
|
-
|
|
3276
|
-
|
|
3277
|
-
|
|
3278
|
-
|
|
3279
|
-
|
|
3280
|
-
|
|
3281
|
-
|
|
3282
|
-
|
|
3283
|
-
|
|
3284
|
-
|
|
3285
|
-
|
|
3286
|
-
|
|
3287
|
-
|
|
3288
|
-
|
|
3289
|
-
|
|
3290
|
-
|
|
3291
|
-
|
|
3292
|
-
|
|
3293
|
-
|
|
3294
|
-
|
|
3295
|
-
|
|
3296
|
-
|
|
3297
|
-
|
|
3298
|
-
|
|
3299
|
-
|
|
3300
|
-
|
|
3301
|
-
|
|
3302
|
-
|
|
3303
|
-
|
|
3304
|
-
|
|
3305
|
-
|
|
3306
|
-
|
|
3432
|
+
const reg = await this.registerTriggerFromParsed(result.value, channel, channelId, peerId, messageId);
|
|
3433
|
+
if (!reg.ok)
|
|
3434
|
+
return `❌ ${reg.error}`;
|
|
3435
|
+
const nextStr = new Date(reg.trigger.nextFireAt).toLocaleString();
|
|
3436
|
+
return `✅ 触发器已注册:**${reg.trigger.name}**\n下次触发:${nextStr}`;
|
|
3437
|
+
}
|
|
3438
|
+
return `❌ 未知子命令。用法:\n/trigger — 查看活跃触发器\n/trigger list — 查看所有触发器\n/trigger set <参数> — 注册触发器\n/trigger update <名称|ID> <参数> — 修改触发器\n/trigger cancel <名称> — 取消触发器`;
|
|
3439
|
+
}
|
|
3440
|
+
/** 从已解析的 trigger 参数组装 Trigger 并注册。文本路径(handleTrigger)与 menu 路径共用。
|
|
3441
|
+
* parsed 形状 = parseTriggerSet 的 result.value(ParsedTriggerSet)。
|
|
3442
|
+
* 失败 return { ok:false, error };成功 return { ok:true, trigger }。本方法不改变原文本路径行为。 */
|
|
3443
|
+
async registerTriggerFromParsed(parsed, channel, channelId, peerId, messageId) {
|
|
3444
|
+
const owningAgent = this.getOwningAgent(channel);
|
|
3445
|
+
const scheduler = (owningAgent?.triggerScheduler ?? this.triggerScheduler);
|
|
3446
|
+
const manager = (owningAgent?.triggerManager ?? this.triggerManager);
|
|
3447
|
+
if (!manager || !scheduler)
|
|
3448
|
+
return { ok: false, error: '触发器功能未启用' };
|
|
3449
|
+
const now = Date.now();
|
|
3450
|
+
const nextFireAt = calcNextFireAt(parsed.scheduleType, parsed.scheduleValue, now);
|
|
3451
|
+
// Auto-generate name if not provided
|
|
3452
|
+
const name = parsed.name ?? `trigger-${Date.now().toString(36)}`;
|
|
3453
|
+
const trigger = {
|
|
3454
|
+
id: crypto.randomUUID(),
|
|
3455
|
+
name,
|
|
3456
|
+
scheduleType: parsed.scheduleType,
|
|
3457
|
+
scheduleValue: parsed.scheduleValue,
|
|
3458
|
+
nextFireAt,
|
|
3459
|
+
targetChannel: parsed.targetChannel ?? channel,
|
|
3460
|
+
targetChannelId: parsed.targetChannelId ?? channelId,
|
|
3461
|
+
targetChannelType: this.resolveChannelType(parsed.targetChannel ?? channel),
|
|
3462
|
+
targetThreadId: parsed.targetThreadId,
|
|
3463
|
+
targetSessionStrategy: parsed.targetSessionStrategy,
|
|
3464
|
+
agentId: parsed.agentId,
|
|
3465
|
+
prompt: parsed.prompt,
|
|
3466
|
+
createdByPeerId: peerId,
|
|
3467
|
+
createdByChannel: channel,
|
|
3468
|
+
fireCount: 0,
|
|
3469
|
+
createdAt: now,
|
|
3470
|
+
updatedAt: now,
|
|
3471
|
+
};
|
|
3472
|
+
try {
|
|
3473
|
+
// Strategy-based session binding
|
|
3474
|
+
if (parsed.targetSessionStrategy === 'current') {
|
|
3475
|
+
const active = await this.sessionManager.getActiveSession(channel, channelId);
|
|
3476
|
+
if (!active)
|
|
3477
|
+
return { ok: false, error: '当前没有活跃会话,改用 --session latest 或 thread' };
|
|
3478
|
+
trigger.boundSessionId = active.id;
|
|
3479
|
+
}
|
|
3480
|
+
else if (parsed.targetSessionStrategy === 'thread') {
|
|
3481
|
+
const targetAdapterName = parsed.targetChannel ?? channel;
|
|
3482
|
+
const adapter = this.adapters.get(targetAdapterName);
|
|
3483
|
+
if (!adapter?.capabilities.thread)
|
|
3484
|
+
return { ok: false, error: '目标渠道不支持 thread 会话' };
|
|
3485
|
+
const channelType = adapter.channelKey.split('#')[0];
|
|
3486
|
+
trigger.targetChannelType = channelType;
|
|
3487
|
+
if (channelType === 'aun') {
|
|
3488
|
+
trigger.threadKind = 'aun';
|
|
3489
|
+
trigger.targetThreadId = `trigger-${trigger.id}`;
|
|
3490
|
+
}
|
|
3491
|
+
else {
|
|
3492
|
+
if (!messageId)
|
|
3493
|
+
return { ok: false, error: '飞书 thread 模式需要消息 ID,请重新发送命令' };
|
|
3494
|
+
trigger.threadKind = 'feishu';
|
|
3495
|
+
trigger.rootMessageId = messageId;
|
|
3496
|
+
trigger.pendingThread = true;
|
|
3307
3497
|
}
|
|
3308
|
-
// Validate name uniqueness before persisting (manager.register writes to disk)
|
|
3309
|
-
// scheduler.register is in-memory only and cannot fail, so order is safe here.
|
|
3310
|
-
// If manager.register throws (duplicate name/ID), nothing is persisted.
|
|
3311
|
-
manager.register(trigger);
|
|
3312
|
-
scheduler.register(trigger);
|
|
3313
|
-
}
|
|
3314
|
-
catch (err) {
|
|
3315
|
-
return `❌ 注册失败:${err.message}`;
|
|
3316
3498
|
}
|
|
3317
|
-
|
|
3318
|
-
|
|
3499
|
+
// Validate name uniqueness before persisting (manager.register writes to disk)
|
|
3500
|
+
// scheduler.register is in-memory only and cannot fail, so order is safe here.
|
|
3501
|
+
// If manager.register throws (duplicate name/ID), nothing is persisted.
|
|
3502
|
+
manager.register(trigger);
|
|
3503
|
+
scheduler.register(trigger);
|
|
3319
3504
|
}
|
|
3320
|
-
|
|
3505
|
+
catch (err) {
|
|
3506
|
+
return { ok: false, error: `注册失败:${err.message}` };
|
|
3507
|
+
}
|
|
3508
|
+
return { ok: true, trigger };
|
|
3321
3509
|
}
|
|
3322
3510
|
// ── /rewind helpers ──
|
|
3323
3511
|
async handleRewindList(session, agent) {
|
|
@@ -3651,3 +3839,38 @@ export class CommandHandler {
|
|
|
3651
3839
|
return text.length > 50 ? text.substring(0, 50) + '…' : text;
|
|
3652
3840
|
}
|
|
3653
3841
|
}
|
|
3842
|
+
/** 进程级 menu 操作(/agent、/system)鉴权:发送方 AID 必须在 owners 名单中。
|
|
3843
|
+
* owners 来自 evolclaw.json 顶层(进程级控制面配置)。纯静态名单比对。 */
|
|
3844
|
+
export function isProcessLevelOwner(peerId, owners) {
|
|
3845
|
+
if (!peerId)
|
|
3846
|
+
return false;
|
|
3847
|
+
return (owners ?? []).includes(peerId);
|
|
3848
|
+
}
|
|
3849
|
+
/** 校验 menu 路径直传的 trigger 调度参数(绕过 parseTriggerSet 文本解析后必须自校验)。
|
|
3850
|
+
* 返回错误字符串表示非法;返回 null 表示通过。
|
|
3851
|
+
* 防止非法 scheduleType/scheduleValue 传到 calcNextFireAt 产出 NaN/throw,污染 scheduler heap。 */
|
|
3852
|
+
export function validateScheduleParams(scheduleType, scheduleValue) {
|
|
3853
|
+
if (!['delay', 'at', 'cron'].includes(scheduleType)) {
|
|
3854
|
+
return `无效 scheduleType: ${scheduleType}(可选: delay / at / cron)`;
|
|
3855
|
+
}
|
|
3856
|
+
if (scheduleType === 'delay') {
|
|
3857
|
+
const ms = Number(scheduleValue);
|
|
3858
|
+
if (!Number.isFinite(ms) || ms <= 0)
|
|
3859
|
+
return `delay 的 scheduleValue 需为正整数毫秒: ${scheduleValue}`;
|
|
3860
|
+
}
|
|
3861
|
+
else if (scheduleType === 'at') {
|
|
3862
|
+
const ts = new Date(scheduleValue).getTime();
|
|
3863
|
+
if (!Number.isFinite(ts))
|
|
3864
|
+
return `at 的 scheduleValue 需为合法时间: ${scheduleValue}`;
|
|
3865
|
+
}
|
|
3866
|
+
else {
|
|
3867
|
+
// cron:交给 calcNextFireAt 内部的 CronExpressionParser 校验(会 throw,被上层 catch)
|
|
3868
|
+
try {
|
|
3869
|
+
calcNextFireAt('cron', scheduleValue, Date.now());
|
|
3870
|
+
}
|
|
3871
|
+
catch {
|
|
3872
|
+
return `无效 cron 表达式: ${scheduleValue}`;
|
|
3873
|
+
}
|
|
3874
|
+
}
|
|
3875
|
+
return null;
|
|
3876
|
+
}
|
|
@@ -300,6 +300,9 @@ export class EvolAgentRegistry {
|
|
|
300
300
|
}
|
|
301
301
|
// swap config 后再起新 channel —— startChannel hook 需要看到新 config
|
|
302
302
|
oldAgent.swapConfig(raw, merged);
|
|
303
|
+
// 热重载也刷新身份层缓存(persona / working 等 fileCache 'agent-files:<aid>' 组),
|
|
304
|
+
// 使 personal 文件改动经 reload 即时生效,不必重启。
|
|
305
|
+
oldAgent.invalidatePersonaCache();
|
|
303
306
|
for (const ch of toAdd) {
|
|
304
307
|
await hooks.startChannel(oldAgent, ch);
|
|
305
308
|
addedSuccessfully.push(ch);
|
package/dist/core/evolagent.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import path from 'path';
|
|
2
|
-
import fs from 'fs';
|
|
3
2
|
import { logger } from '../utils/logger.js';
|
|
4
3
|
import { saveAgent } from '../config-store.js';
|
|
5
4
|
import { formatChannelKey, tryParseChannelKey } from './channel-loader.js';
|
|
6
5
|
import { agentPersonalDir } from '../paths.js';
|
|
6
|
+
import { fileCache } from './cache/file-cache.js';
|
|
7
7
|
/**
|
|
8
8
|
* EvolAgent —— 一个 self-agent 的运行时表示。
|
|
9
9
|
*
|
|
@@ -213,40 +213,45 @@ export class EvolAgent {
|
|
|
213
213
|
this.merged.dispatch = value;
|
|
214
214
|
this.persist();
|
|
215
215
|
}
|
|
216
|
+
/** 读取观察者模式开关(默认 false)。 */
|
|
217
|
+
getObservable() {
|
|
218
|
+
return this.merged.observable === true;
|
|
219
|
+
}
|
|
220
|
+
/** 设置观察者模式开关:开启后入站/出站消息各转发一份给 owners[]。 */
|
|
221
|
+
setObservable(value) {
|
|
222
|
+
if (value)
|
|
223
|
+
this.rawAgent.observable = true;
|
|
224
|
+
else
|
|
225
|
+
delete this.rawAgent.observable;
|
|
226
|
+
this.merged.observable = value;
|
|
227
|
+
this.persist();
|
|
228
|
+
}
|
|
216
229
|
// ── Personal layer ────────────────────────────────────────────────────
|
|
217
|
-
|
|
230
|
+
/** 本 agent 身份层文件在 fileCache 的组名(带 aid,避免 reload 单个 agent 误失效他人)。 */
|
|
231
|
+
agentFilesGroup() {
|
|
232
|
+
return `agent-files:${this.aid}`;
|
|
233
|
+
}
|
|
218
234
|
/**
|
|
219
|
-
* 读取 personal/persona.md
|
|
220
|
-
*
|
|
235
|
+
* 读取 personal/persona.md 内容。走 fileCache(mtime 门控):persona 没有任何
|
|
236
|
+
* 写入命令、由 agent 自己带外改写,与 working memory 同样改了即应生效,故每次读
|
|
237
|
+
* stat 比对、变了自动重读。文件不存在返回 null。
|
|
221
238
|
*/
|
|
222
239
|
getPersona() {
|
|
223
|
-
if (this._personaCache !== undefined)
|
|
224
|
-
return this._personaCache;
|
|
225
240
|
const personaPath = path.join(agentPersonalDir(this.aid), 'persona.md');
|
|
226
|
-
|
|
227
|
-
this._personaCache = fs.readFileSync(personaPath, 'utf-8').trim() || null;
|
|
228
|
-
}
|
|
229
|
-
catch {
|
|
230
|
-
this._personaCache = null;
|
|
231
|
-
}
|
|
232
|
-
return this._personaCache;
|
|
241
|
+
return fileCache.get(personaPath, (raw) => (raw === null ? null : (raw.trim() || null)), { policy: 'mtime', group: this.agentFilesGroup() });
|
|
233
242
|
}
|
|
234
243
|
/**
|
|
235
|
-
* 读取 personal/memory/working.md
|
|
244
|
+
* 读取 personal/memory/working.md 内容。走 fileCache(mtime 门控):
|
|
245
|
+
* agent 在对话中改写 working memory、不触发 reload,故每次读 stat 比对、
|
|
246
|
+
* 变了自动重读,既即时反映又避免无谓重读。
|
|
236
247
|
*/
|
|
237
248
|
getWorkingMemory() {
|
|
238
249
|
const workingPath = path.join(agentPersonalDir(this.aid), 'memory', 'working.md');
|
|
239
|
-
|
|
240
|
-
const content = fs.readFileSync(workingPath, 'utf-8').trim();
|
|
241
|
-
return content || null;
|
|
242
|
-
}
|
|
243
|
-
catch {
|
|
244
|
-
return null;
|
|
245
|
-
}
|
|
250
|
+
return fileCache.get(workingPath, (raw) => (raw === null ? null : (raw.trim() || null)), { policy: 'mtime', group: this.agentFilesGroup() });
|
|
246
251
|
}
|
|
247
|
-
/**
|
|
252
|
+
/** 清除本 agent 身份层缓存(reload 后重新读取)。只失效自己的文件组,不波及他人。 */
|
|
248
253
|
invalidatePersonaCache() {
|
|
249
|
-
this.
|
|
254
|
+
fileCache.invalidateGroup(this.agentFilesGroup());
|
|
250
255
|
}
|
|
251
256
|
// ── Context(喂给 message-processor / command-handler) ──────────────
|
|
252
257
|
getContext(_channelKey, chatType, globalChatmode) {
|