evolcore 0.0.28 → 0.0.29
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 +12 -0
- package/bin/ec-safe-output.js +270 -54
- package/dist/agents/baseagent-availability.js +37 -1
- package/dist/agents/baseagent.js +19 -9
- package/dist/agents/claude-runner.js +171 -13
- package/dist/agents/codex/app-server-client.js +6 -2
- package/dist/agents/codex/diagnostics.js +4 -3
- package/dist/agents/codex-runner.js +396 -95
- package/dist/agents/gemini-runner.js +35 -17
- package/dist/aun/group/history.js +130 -90
- package/dist/aun/group/operations.js +1 -3
- package/dist/aun/menu/transport.js +5 -3
- package/dist/aun/msg/managed-operation.js +70 -0
- package/dist/aun/msg/p2p.js +1 -3
- package/dist/aun/msg/upload.js +2 -2
- package/dist/aun/outbox.js +1 -37
- package/dist/channels/aun.js +249 -89
- package/dist/channels/daemon.js +18 -1
- package/dist/channels/feishu.js +1 -2
- package/dist/cli/agent-command.js +23 -3
- package/dist/cli/agent-create.js +68 -2
- package/dist/cli/agent.js +12 -2
- package/dist/cli/aun-commands.js +87 -13
- package/dist/cli/cli-argv.js +26 -0
- package/dist/cli/daemon-commands.js +40 -10
- package/dist/cli/data-command.js +4 -4
- package/dist/cli/handoff-command.js +2 -0
- package/dist/cli/index.js +18 -1
- package/dist/cli/managed-command-guard.js +10 -0
- package/dist/cli/memory-command.js +441 -0
- package/dist/cli/restart-monitor.js +62 -6
- package/dist/cli/skill-command.js +41 -0
- package/dist/cli/task-context.js +2 -23
- package/dist/cli/trigger-command.js +9 -1
- package/dist/cli/watch-msg-diag.js +209 -0
- package/dist/config/config-field-policy.js +52 -3
- package/dist/config/config-manager.js +17 -4
- package/dist/config/contact-book-store.js +2 -2
- package/dist/config/mention-mode.js +7 -2
- package/dist/config/merge.js +28 -9
- package/dist/config/resolved-config-op.js +1 -1
- package/dist/config/runtime-env.js +137 -0
- package/dist/config/schema-registry.js +11 -1
- package/dist/config-store.js +10 -10
- package/dist/core/audit/event-key.js +3 -1
- package/dist/core/audit/log-integrity.js +4 -0
- package/dist/core/auth/agent-delegation.js +2 -1
- package/dist/core/auth/auth-gateway.js +13 -6
- package/dist/core/auth/authorization-audit.js +54 -19
- package/dist/core/auth/operation-authorizer.js +17 -0
- package/dist/core/auth/operation-catalog.js +9 -0
- package/dist/core/causation/context.js +5 -1
- package/dist/core/command/agent-control.js +1 -2
- package/dist/core/command/command-handler.js +51 -23
- package/dist/core/command/memory-slash.js +270 -0
- package/dist/core/command/menu-catalog.js +1 -0
- package/dist/core/command/menu-handler.js +315 -60
- package/dist/core/command/menu-protocol.js +42 -4
- package/dist/core/command/slash-gate.js +10 -10
- package/dist/core/command/slash-handler.js +206 -64
- package/dist/core/data-migration.js +282 -505
- package/dist/core/event-catalog.js +26 -0
- package/dist/core/evolagent-registry.js +6 -0
- package/dist/core/evolagent.js +0 -10
- package/dist/core/handoff/store.js +1 -20
- package/dist/core/inference/text-inference.js +57 -1
- package/dist/core/memory/consolidation-context.js +505 -0
- package/dist/core/memory/consolidation-ops-records.js +124 -0
- package/dist/core/memory/consolidation-trigger.js +394 -0
- package/dist/core/memory/index-manager.js +308 -0
- package/dist/core/memory/launch-consolidation.js +395 -0
- package/dist/core/memory/memory-admin.js +234 -0
- package/dist/core/memory/memory-document.js +100 -0
- package/dist/core/memory/memory-session-policy.js +41 -0
- package/dist/core/memory/memory-signature.js +257 -0
- package/dist/core/memory/memory-writer.js +432 -0
- package/dist/core/memory/staleness.js +117 -0
- package/dist/core/message/auto-mention-gate.js +454 -0
- package/dist/core/message/im-renderer.js +69 -16
- package/dist/core/message/message-bridge.js +287 -21
- package/dist/core/message/message-log.js +1 -0
- package/dist/core/message/message-queue.js +114 -27
- package/dist/core/message/pending-hints.js +329 -161
- package/dist/core/message/response-engine.js +567 -80
- package/dist/core/permission/ec-command-parser.js +1029 -80
- package/dist/core/permission/protected-paths.js +376 -126
- package/dist/core/permission/sandbox-runtime.js +40 -12
- package/dist/core/permission/tool-error-code.js +96 -13
- package/dist/core/permission/tool-policy.js +276 -23
- package/dist/core/role/runtime-policy.js +3 -7
- package/dist/core/session/{runner-task-history.js → runner-event-summary.js} +1 -1
- package/dist/core/session/session-fs-store.js +0 -76
- package/dist/core/session/session-idle-monitor.js +355 -0
- package/dist/core/session/session-manager.js +478 -17
- package/dist/core/session/session-mapper.js +33 -16
- package/dist/core/session/session-renew.js +217 -155
- package/dist/core/session/{session-turns.js → session-transcript.js} +35 -6
- package/dist/core/skill/global-watcher.js +282 -0
- package/dist/core/skill/index-generator.js +84 -0
- package/dist/core/skill/startup.js +73 -0
- package/dist/core/skill/switch-resolver.js +115 -0
- package/dist/core/task/task-activity-store.js +219 -0
- package/dist/eck/kit-renderer.js +30 -3
- package/dist/eck/manifest-engine.js +26 -4
- package/dist/index.js +708 -18
- package/dist/ipc.js +564 -2
- package/dist/paths.js +30 -9
- package/dist/response-system/engines/v1/proactive-flow.js +116 -15
- package/dist/response-system/modes/single-session/index.js +3 -0
- package/dist/trigger/feedback.js +1 -0
- package/dist/trigger/manager.js +0 -10
- package/dist/trigger/scheduler.js +11 -0
- package/dist/trigger/validation.js +22 -0
- package/dist/utils/avatar-upload.js +59 -18
- package/dist/utils/error-dict.json +6 -3
- package/dist/utils/error-utils.js +27 -0
- package/dist/utils/logger.js +32 -10
- package/dist/utils/media-cache.js +84 -0
- package/dist/utils/msg-diag.js +97 -0
- package/dist/utils/welcome.js +37 -8
- package/kits/docs/AUTHORING.md +314 -0
- package/kits/docs/GUIDE.md +2 -0
- package/kits/docs/INDEX.md +2 -0
- package/kits/docs/aun/CHEATSHEET.md +4 -0
- package/kits/docs/aun/SDK.md +60 -0
- package/kits/docs/context-assembly.md +10 -6
- package/kits/docs/evolcore/INDEX.md +17 -3
- package/kits/docs/evolcore/agent-create.md +24 -0
- package/kits/docs/evolcore/agent.md +5 -2
- package/kits/docs/evolcore/config.md +1 -1
- package/kits/docs/evolcore/ec-capabilities.md +364 -0
- package/kits/docs/evolcore/group.md +34 -18
- package/kits/docs/evolcore/memory-architecture.md +78 -0
- package/kits/docs/evolcore/memory-consolidation-batches.md +133 -0
- package/kits/docs/evolcore/memory-consolidation.md +263 -0
- package/kits/docs/evolcore/memory-tags.md +63 -0
- package/kits/docs/evolcore/memory.md +110 -0
- package/kits/docs/evolcore/menu-client.md +27 -11
- package/kits/{schemas → docs/evolcore}/menu-exec-schema-commands.md +12 -6
- package/kits/docs/evolcore/menu-protocol.md +213 -0
- package/kits/docs/evolcore/msg.md +2 -0
- package/kits/docs/evolcore/trigger.md +7 -5
- package/kits/docs/path-registry.md +6 -8
- package/kits/docs/prompt-loading-architecture.md +5 -3
- package/kits/docs/venues/group.md +5 -4
- package/kits/eck_manifest.json +294 -88
- package/kits/eck_manifest.memory.json +257 -0
- package/kits/rules/03-identity.md +8 -8
- package/kits/rules/05-venue.md +4 -2
- package/kits/rules/06-channel.md +1 -1
- package/kits/schemas/_meta.json +276 -47
- package/kits/schemas/agent-config.schema.12.json +38 -2
- package/kits/schemas/agent-config.schema.13.json +591 -0
- package/kits/schemas/daemon.schema.6.json +9 -9
- package/kits/schemas/defaults.schema.5.json +29 -0
- package/kits/schemas/relation-config.schema.8.json +14 -1
- package/kits/schemas/relation-config.schema.9.json +129 -0
- package/kits/schemas/role-config.schema.1.json +3 -3
- package/kits/skills/.switches.json +3 -0
- package/kits/skills/ec-skill/SKILL.md +217 -0
- package/kits/skills/ec-skill/scripts/cli.mjs +128 -0
- package/kits/skills/ec-skill/scripts/deps.mjs +107 -0
- package/kits/skills/ec-skill/scripts/frontmatter.mjs +104 -0
- package/kits/skills/ec-skill/scripts/skill.mjs +629 -0
- package/kits/skills/ec-skill/scripts/template/SKILL.md.tmpl +30 -0
- package/kits/templates/decision/auto-mention.md +25 -0
- package/kits/templates/decision/session-renew.md +22 -0
- package/kits/templates/memory-consolidation/forget.md +104 -0
- package/kits/templates/memory-consolidation/generalize.md +76 -0
- package/kits/templates/memory-consolidation/notes.md +133 -0
- package/kits/templates/memory-consolidation/profile.md +58 -0
- package/kits/templates/memory-consolidation/promote.md +72 -0
- package/kits/templates/memory-consolidation/workflow.md +75 -0
- package/kits/templates/system-fragments/bootstrap.md +14 -5
- package/kits/templates/system-fragments/commands.md +1 -0
- package/kits/templates/system-fragments/group-history.md +6 -0
- package/kits/templates/system-fragments/identity.md +0 -3
- package/kits/templates/system-fragments/memory.md +79 -0
- package/kits/templates/system-fragments/reply.md +1 -0
- package/kits/templates/system-fragments/venue.md +1 -1
- package/package.json +4 -6
- package/dist/trigger/legacy-session-history.js +0 -19
- package/kits/templates/system-fragments/group-unread-history.md +0 -7
|
@@ -1,9 +1,18 @@
|
|
|
1
1
|
import { createHash } from 'node:crypto';
|
|
2
|
+
export function aunMessageTranscriptUuid(messageId) {
|
|
3
|
+
const bytes = createHash('sha256')
|
|
4
|
+
.update(`evolcore:aun-input:v1\0${messageId}`)
|
|
5
|
+
.digest();
|
|
6
|
+
bytes[6] = (bytes[6] & 0x0f) | 0x50;
|
|
7
|
+
bytes[8] = (bytes[8] & 0x3f) | 0x80;
|
|
8
|
+
const hex = bytes.subarray(0, 16).toString('hex');
|
|
9
|
+
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20, 32)}`;
|
|
10
|
+
}
|
|
2
11
|
// These prompts are steered into the active base-agent turn by EvolCore. The
|
|
3
12
|
// Codex history adapter exposes steered input as a regular `user` item, so it
|
|
4
13
|
// must not split the user's actual turn when reconstructing fork anchors.
|
|
5
14
|
const TASK_RECOVERY_PROMPT = '继续当前会话尚未完成的任务。先核对最新用户指令、已有结果和任务状态;若已完成、用户已停止或正在等待必要输入/审批,则不要重复执行。对可能已产生副作用的步骤先核实结果,再继续未完成的工作。';
|
|
6
|
-
function isInternalInjectedUserMessage(message) {
|
|
15
|
+
export function isInternalInjectedUserMessage(message) {
|
|
7
16
|
const text = extractUserText(message);
|
|
8
17
|
if (!text)
|
|
9
18
|
return false;
|
|
@@ -17,6 +26,14 @@ function isInternalInjectedUserMessage(message) {
|
|
|
17
26
|
return true;
|
|
18
27
|
return false;
|
|
19
28
|
}
|
|
29
|
+
function toolResultCallIds(message) {
|
|
30
|
+
const content = message?.content;
|
|
31
|
+
if (!Array.isArray(content))
|
|
32
|
+
return [];
|
|
33
|
+
return content.filter((block) => block?.type === 'tool_result')
|
|
34
|
+
.map((block) => block.tool_use_id)
|
|
35
|
+
.filter((id) => typeof id === 'string' && id.length > 0);
|
|
36
|
+
}
|
|
20
37
|
function extractText(message) {
|
|
21
38
|
const value = message;
|
|
22
39
|
let text = '';
|
|
@@ -59,13 +76,13 @@ export function extractSessionUserContent(message) {
|
|
|
59
76
|
function textHash(text) {
|
|
60
77
|
return createHash('sha256').update(text.replace(/\s+/g, ' ').trim()).digest('hex');
|
|
61
78
|
}
|
|
62
|
-
function isToolResultMessage(message) {
|
|
79
|
+
export function isToolResultMessage(message) {
|
|
63
80
|
const content = message?.content;
|
|
64
81
|
return Array.isArray(content)
|
|
65
82
|
&& content.length > 0
|
|
66
83
|
&& content.every((block) => block?.type === 'tool_result');
|
|
67
84
|
}
|
|
68
|
-
export function
|
|
85
|
+
export function buildTranscriptTurns(messages) {
|
|
69
86
|
const turns = [];
|
|
70
87
|
let pending;
|
|
71
88
|
const finishPending = () => {
|
|
@@ -79,12 +96,19 @@ export function buildSessionTurnList(messages) {
|
|
|
79
96
|
assistantUuids: [...(pending.assistantUuids ?? [])],
|
|
80
97
|
userTextHash: pending.userTextHash,
|
|
81
98
|
assistantTextHashes: [...pending.assistantTextHashes],
|
|
99
|
+
assistantTextAnchors: [...pending.assistantTextAnchors],
|
|
100
|
+
toolResultAnchors: [...pending.toolResultAnchors],
|
|
82
101
|
});
|
|
83
102
|
};
|
|
84
103
|
for (const message of messages) {
|
|
85
104
|
if (message.type === 'user') {
|
|
86
|
-
if (isToolResultMessage(message.message))
|
|
105
|
+
if (isToolResultMessage(message.message)) {
|
|
106
|
+
if (pending)
|
|
107
|
+
for (const callId of toolResultCallIds(message.message)) {
|
|
108
|
+
pending.toolResultAnchors.push({ callId, messageId: message.uuid });
|
|
109
|
+
}
|
|
87
110
|
continue;
|
|
111
|
+
}
|
|
88
112
|
if (isInternalInjectedUserMessage(message.message))
|
|
89
113
|
continue;
|
|
90
114
|
finishPending();
|
|
@@ -92,6 +116,8 @@ export function buildSessionTurnList(messages) {
|
|
|
92
116
|
pending = userContent ? {
|
|
93
117
|
userContent, userUuid: message.uuid, assistantUuids: [],
|
|
94
118
|
userTextHash: textHash(extractUserText(message.message)), assistantTextHashes: [],
|
|
119
|
+
assistantTextAnchors: [],
|
|
120
|
+
toolResultAnchors: [],
|
|
95
121
|
} : undefined;
|
|
96
122
|
continue;
|
|
97
123
|
}
|
|
@@ -99,8 +125,11 @@ export function buildSessionTurnList(messages) {
|
|
|
99
125
|
pending.assistantUuid = message.uuid;
|
|
100
126
|
pending.assistantUuids.push(message.uuid);
|
|
101
127
|
const text = extractText(message.message);
|
|
102
|
-
if (text)
|
|
103
|
-
|
|
128
|
+
if (text) {
|
|
129
|
+
const hash = textHash(text);
|
|
130
|
+
pending.assistantTextHashes.push(hash);
|
|
131
|
+
pending.assistantTextAnchors.push({ messageId: message.uuid, textHash: hash });
|
|
132
|
+
}
|
|
104
133
|
}
|
|
105
134
|
}
|
|
106
135
|
finishPending();
|
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
import chokidar from 'chokidar';
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import { getPackageRoot, resolveRoot, resolvePaths, agentDir } from '../../paths.js';
|
|
5
|
+
import { generateSkillIndexes } from './index-generator.js';
|
|
6
|
+
import { logger } from '../../utils/logger.js';
|
|
7
|
+
/**
|
|
8
|
+
* 全局技能监控器(单例)
|
|
9
|
+
*
|
|
10
|
+
* 职责:
|
|
11
|
+
* 1. 递归监控三级技能目录(内置/本机/每个Agent)
|
|
12
|
+
* 2. 标记哪些作用域有变化(dirty 标记)
|
|
13
|
+
* 3. 2秒防抖:连续变更时重置计时器,静默2秒后触发回调
|
|
14
|
+
*
|
|
15
|
+
* Agent 监控采用注册机制:
|
|
16
|
+
* - Agent 启动时调用 `watchAgent(aid)` 注册监控
|
|
17
|
+
* - Agent 停止时调用 `unwatchAgent(aid)` 注销监控
|
|
18
|
+
*/
|
|
19
|
+
class GlobalSkillWatcher {
|
|
20
|
+
// 内置和本机的 watcher(固定)
|
|
21
|
+
builtinWatcher = null;
|
|
22
|
+
localWatcher = null;
|
|
23
|
+
// Agent 级 watcher(动态注册/注销)
|
|
24
|
+
agentWatchers = new Map();
|
|
25
|
+
/** 路径覆盖(测试注入;生产为 undefined) */
|
|
26
|
+
pathsOverride;
|
|
27
|
+
/** 防抖时长(毫秒)。测试可缩短。 */
|
|
28
|
+
debounceMs = 2000;
|
|
29
|
+
dirtyScopes = new Set();
|
|
30
|
+
dirtyAgents = new Set();
|
|
31
|
+
/** 首次检查标志:记录哪些 agent 已完成首次检查 */
|
|
32
|
+
checkedAgents = new Set();
|
|
33
|
+
// 防抖计时器:key = "builtin" | "local" | "agent:<aid>"
|
|
34
|
+
timers = new Map();
|
|
35
|
+
// 回调函数:在防抖完成后被调用
|
|
36
|
+
callbacks = new Map();
|
|
37
|
+
started = false;
|
|
38
|
+
/**
|
|
39
|
+
* 启动全局监控器(仅监控内置和本机目录)
|
|
40
|
+
* @param override 路径与防抖覆盖(仅测试用)
|
|
41
|
+
*/
|
|
42
|
+
start(override) {
|
|
43
|
+
if (this.started)
|
|
44
|
+
return;
|
|
45
|
+
this.started = true;
|
|
46
|
+
this.pathsOverride = override;
|
|
47
|
+
if (override?.debounceMs !== undefined)
|
|
48
|
+
this.debounceMs = override.debounceMs;
|
|
49
|
+
const builtinDir = override?.builtin ?? path.join(getPackageRoot(), 'kits', 'skills');
|
|
50
|
+
const localDir = override?.local ?? path.join(resolveRoot(), 'skills');
|
|
51
|
+
// 1. 内置技能目录(递归监控)
|
|
52
|
+
this.builtinWatcher = chokidar.watch(builtinDir, {
|
|
53
|
+
persistent: true,
|
|
54
|
+
ignoreInitial: true,
|
|
55
|
+
awaitWriteFinish: { stabilityThreshold: 100, pollInterval: 50 },
|
|
56
|
+
});
|
|
57
|
+
this.builtinWatcher.on('all', () => {
|
|
58
|
+
this.markDirty('builtin');
|
|
59
|
+
});
|
|
60
|
+
// 2. 本机技能目录(递归监控)
|
|
61
|
+
this.localWatcher = chokidar.watch(localDir, {
|
|
62
|
+
persistent: true,
|
|
63
|
+
ignoreInitial: true,
|
|
64
|
+
awaitWriteFinish: { stabilityThreshold: 100, pollInterval: 50 },
|
|
65
|
+
});
|
|
66
|
+
this.localWatcher.on('all', () => {
|
|
67
|
+
this.markDirty('local');
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* 注册某个 Agent 的技能目录监控(Agent 启动时调用)
|
|
72
|
+
*/
|
|
73
|
+
watchAgent(aid) {
|
|
74
|
+
if (this.agentWatchers.has(aid))
|
|
75
|
+
return; // 已经监控,跳过
|
|
76
|
+
const resolveAgentDir = this.pathsOverride?.agentDir ?? agentDir;
|
|
77
|
+
const agentSkillsDir = path.join(resolveAgentDir(aid), 'skills');
|
|
78
|
+
const watcher = chokidar.watch(agentSkillsDir, {
|
|
79
|
+
persistent: true,
|
|
80
|
+
ignoreInitial: true,
|
|
81
|
+
awaitWriteFinish: { stabilityThreshold: 100, pollInterval: 50 },
|
|
82
|
+
});
|
|
83
|
+
watcher.on('all', () => {
|
|
84
|
+
this.markDirtyAgent(aid);
|
|
85
|
+
});
|
|
86
|
+
this.agentWatchers.set(aid, watcher);
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* 注销某个 Agent 的技能目录监控(Agent 停止时调用)
|
|
90
|
+
*/
|
|
91
|
+
async unwatchAgent(aid) {
|
|
92
|
+
const watcher = this.agentWatchers.get(aid);
|
|
93
|
+
if (!watcher)
|
|
94
|
+
return; // 没有监控记录,跳过
|
|
95
|
+
await watcher.close();
|
|
96
|
+
this.agentWatchers.delete(aid);
|
|
97
|
+
// 清除该 Agent 的 dirty 标记和计时器
|
|
98
|
+
this.dirtyAgents.delete(aid);
|
|
99
|
+
const timerKey = `agent:${aid}`;
|
|
100
|
+
if (this.timers.has(timerKey)) {
|
|
101
|
+
clearTimeout(this.timers.get(timerKey));
|
|
102
|
+
this.timers.delete(timerKey);
|
|
103
|
+
}
|
|
104
|
+
this.callbacks.delete(timerKey);
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* 停止全局监控器(关闭内置和本机 watcher,Agent watcher 已由注销时关闭)
|
|
108
|
+
*/
|
|
109
|
+
async stop() {
|
|
110
|
+
if (!this.started)
|
|
111
|
+
return;
|
|
112
|
+
// 关闭内置和本机 watcher
|
|
113
|
+
if (this.builtinWatcher) {
|
|
114
|
+
await this.builtinWatcher.close();
|
|
115
|
+
this.builtinWatcher = null;
|
|
116
|
+
}
|
|
117
|
+
if (this.localWatcher) {
|
|
118
|
+
await this.localWatcher.close();
|
|
119
|
+
this.localWatcher = null;
|
|
120
|
+
}
|
|
121
|
+
// 关闭所有 Agent watcher(兜底,正常应该已在注销时关闭)
|
|
122
|
+
for (const watcher of this.agentWatchers.values()) {
|
|
123
|
+
await watcher.close();
|
|
124
|
+
}
|
|
125
|
+
this.agentWatchers.clear();
|
|
126
|
+
// 清除所有防抖计时器
|
|
127
|
+
for (const timer of this.timers.values()) {
|
|
128
|
+
clearTimeout(timer);
|
|
129
|
+
}
|
|
130
|
+
this.timers.clear();
|
|
131
|
+
this.callbacks.clear();
|
|
132
|
+
this.dirtyScopes.clear();
|
|
133
|
+
this.dirtyAgents.clear();
|
|
134
|
+
this.checkedAgents.clear();
|
|
135
|
+
this.pathsOverride = undefined;
|
|
136
|
+
this.debounceMs = 2000;
|
|
137
|
+
this.started = false;
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* 标记某个作用域有变化(触发防抖)
|
|
141
|
+
*/
|
|
142
|
+
markDirty(scope) {
|
|
143
|
+
this.dirtyScopes.add(scope);
|
|
144
|
+
this.debounce(scope);
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* 标记某个 Agent 有变化(触发防抖)
|
|
148
|
+
*/
|
|
149
|
+
markDirtyAgent(aid) {
|
|
150
|
+
this.dirtyAgents.add(aid);
|
|
151
|
+
this.debounce(`agent:${aid}`);
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* 防抖机制:2秒内有新变化就重置计时器,静默2秒后触发回调
|
|
155
|
+
*/
|
|
156
|
+
debounce(key) {
|
|
157
|
+
// 清除旧计时器
|
|
158
|
+
if (this.timers.has(key)) {
|
|
159
|
+
clearTimeout(this.timers.get(key));
|
|
160
|
+
}
|
|
161
|
+
// 设置新计时器
|
|
162
|
+
const timer = setTimeout(() => {
|
|
163
|
+
this.timers.delete(key);
|
|
164
|
+
// 触发回调(如果注册了)
|
|
165
|
+
const callback = this.callbacks.get(key);
|
|
166
|
+
if (callback) {
|
|
167
|
+
callback();
|
|
168
|
+
}
|
|
169
|
+
}, this.debounceMs); // 默认 2 秒防抖
|
|
170
|
+
this.timers.set(key, timer);
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* 检查某个作用域是否 dirty
|
|
174
|
+
*/
|
|
175
|
+
isDirty(scope) {
|
|
176
|
+
return this.dirtyScopes.has(scope);
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* 检查某个 Agent 是否 dirty
|
|
180
|
+
*/
|
|
181
|
+
isAgentDirty(aid) {
|
|
182
|
+
return this.dirtyAgents.has(aid);
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* 清除某个作用域的 dirty 标记
|
|
186
|
+
*/
|
|
187
|
+
markClean(scope) {
|
|
188
|
+
this.dirtyScopes.delete(scope);
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* 清除某个 Agent 的 dirty 标记
|
|
192
|
+
*/
|
|
193
|
+
markAgentClean(aid) {
|
|
194
|
+
this.dirtyAgents.delete(aid);
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* 重置所有内存状态(仅测试用)。
|
|
198
|
+
* 不关闭 watcher,只清标志位,确保跨测试状态隔离。
|
|
199
|
+
*/
|
|
200
|
+
_resetForTest() {
|
|
201
|
+
this.dirtyScopes.clear();
|
|
202
|
+
this.dirtyAgents.clear();
|
|
203
|
+
this.checkedAgents.clear();
|
|
204
|
+
for (const timer of this.timers.values())
|
|
205
|
+
clearTimeout(timer);
|
|
206
|
+
this.timers.clear();
|
|
207
|
+
this.callbacks.clear();
|
|
208
|
+
}
|
|
209
|
+
/**
|
|
210
|
+
* 注册回调:防抖完成后执行
|
|
211
|
+
* @param scope 作用域或 Agent 标识(格式:"builtin" | "local" | "agent:<aid>")
|
|
212
|
+
* @param callback 回调函数
|
|
213
|
+
*/
|
|
214
|
+
onDebouncedChange(scope, callback) {
|
|
215
|
+
this.callbacks.set(scope, callback);
|
|
216
|
+
}
|
|
217
|
+
/**
|
|
218
|
+
* 判断某个 agent 是否需要生成索引(懒加载逻辑)。
|
|
219
|
+
*
|
|
220
|
+
* 返回 true 的情况:
|
|
221
|
+
* 1. 该 agent 未进行过首次检查
|
|
222
|
+
* 2. 已检查但被标记为"脏"
|
|
223
|
+
*/
|
|
224
|
+
needsIndexGeneration(aid) {
|
|
225
|
+
// 未进行过首次检查
|
|
226
|
+
if (!this.checkedAgents.has(aid)) {
|
|
227
|
+
return true;
|
|
228
|
+
}
|
|
229
|
+
// 已检查但标记为脏
|
|
230
|
+
if (this.dirtyAgents.has(aid)) {
|
|
231
|
+
return true;
|
|
232
|
+
}
|
|
233
|
+
return false;
|
|
234
|
+
}
|
|
235
|
+
/**
|
|
236
|
+
* 标记某个 agent 已完成首次检查
|
|
237
|
+
*/
|
|
238
|
+
markAgentChecked(aid) {
|
|
239
|
+
this.checkedAgents.add(aid);
|
|
240
|
+
}
|
|
241
|
+
/**
|
|
242
|
+
* 标记所有 agent 为脏(内置或本机技能变化时调用)
|
|
243
|
+
*/
|
|
244
|
+
markAllAgentsDirty() {
|
|
245
|
+
// 不实际遍历所有 agent,而是在 needsIndexGeneration 时判断
|
|
246
|
+
// 这里设置一个全局标志
|
|
247
|
+
this.dirtyScopes.add('builtin');
|
|
248
|
+
this.dirtyScopes.add('local');
|
|
249
|
+
// 清除所有 agent 的首次检查标志,强制下次检查时重新生成
|
|
250
|
+
this.checkedAgents.clear();
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
// 导出单例
|
|
254
|
+
export const globalSkillWatcher = new GlobalSkillWatcher();
|
|
255
|
+
/** Generate indexes for existing agents and start the shared watcher. */
|
|
256
|
+
export function generateSkillIndexesForAllAgents() {
|
|
257
|
+
const agentsDir = resolvePaths().agentsDir;
|
|
258
|
+
if (!fs.existsSync(agentsDir))
|
|
259
|
+
return;
|
|
260
|
+
for (const entry of fs.readdirSync(agentsDir, { withFileTypes: true })) {
|
|
261
|
+
if (!entry.isDirectory())
|
|
262
|
+
continue;
|
|
263
|
+
try {
|
|
264
|
+
generateSkillIndexes(path.join(agentsDir, entry.name));
|
|
265
|
+
}
|
|
266
|
+
catch (error) {
|
|
267
|
+
logger.warn(`[skill] 生成索引失败 (${entry.name}): ${error instanceof Error ? error.message : String(error)}`);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
globalSkillWatcher.start();
|
|
271
|
+
globalSkillWatcher.onDebouncedChange('builtin', () => {
|
|
272
|
+
logger.info('[skill] 内置技能有变化,重新生成所有 agent 索引');
|
|
273
|
+
generateSkillIndexesForAllAgents();
|
|
274
|
+
});
|
|
275
|
+
globalSkillWatcher.onDebouncedChange('local', () => {
|
|
276
|
+
logger.info('[skill] 本机技能有变化,重新生成所有 agent 索引');
|
|
277
|
+
generateSkillIndexesForAllAgents();
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
export async function stopGlobalSkillWatcher() {
|
|
281
|
+
await globalSkillWatcher.stop();
|
|
282
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { parseDocument } from 'yaml';
|
|
4
|
+
import { resolveSkills } from './switch-resolver.js';
|
|
5
|
+
import { logger } from '../../utils/logger.js';
|
|
6
|
+
/**
|
|
7
|
+
* 生成索引文件(汇总 → 分组 → 落两文件)。
|
|
8
|
+
*
|
|
9
|
+
* 按 injection §3 流程:
|
|
10
|
+
* 1. 汇总解析(调用 switch-resolver)
|
|
11
|
+
* 2. 按来源分组(builtin / local)
|
|
12
|
+
* 3. 分别生成 .index-builtin.md / .index-local.md
|
|
13
|
+
* 4. 空集时删除已存在的旧文件
|
|
14
|
+
*/
|
|
15
|
+
export function generateSkillIndexes(agentDir, override) {
|
|
16
|
+
const skills = resolveSkills(agentDir, override);
|
|
17
|
+
// ② 分组
|
|
18
|
+
const builtinSkills = skills.filter(s => s.source === 'builtin');
|
|
19
|
+
const localSkills = skills.filter(s => s.source === 'local');
|
|
20
|
+
const indexDir = path.join(agentDir, 'skills');
|
|
21
|
+
fs.mkdirSync(indexDir, { recursive: true });
|
|
22
|
+
// ③ 分别生成(或清理)
|
|
23
|
+
generateOneIndex(indexDir, '.index-builtin.md', builtinSkills, 'builtin');
|
|
24
|
+
generateOneIndex(indexDir, '.index-local.md', localSkills, 'local');
|
|
25
|
+
}
|
|
26
|
+
function generateOneIndex(indexDir, filename, skills, label) {
|
|
27
|
+
const filePath = path.join(indexDir, filename);
|
|
28
|
+
// 空集:删除旧文件
|
|
29
|
+
if (skills.length === 0) {
|
|
30
|
+
if (fs.existsSync(filePath)) {
|
|
31
|
+
fs.unlinkSync(filePath);
|
|
32
|
+
logger.debug(`[skill-index] ${filename} 已删除(无启用技能)`);
|
|
33
|
+
}
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
// 根目录从任一条目反推(同组内同源,父目录一致)
|
|
37
|
+
const rootDir = path.dirname(skills[0].skillDir);
|
|
38
|
+
// 示例用第一个技能的真实路径——模型照抄真路径比按规则推导可靠
|
|
39
|
+
const exampleName = skills[0].name;
|
|
40
|
+
const examplePath = path.join(skills[0].skillDir, 'SKILL.md');
|
|
41
|
+
// 路径规则块(用 path.sep,示例路径用真实技能名)
|
|
42
|
+
const sep = path.sep;
|
|
43
|
+
const header = `[skills-${label}] 本 Agent 已启用 ${skills.length} 个${label === 'builtin' ? '内置' : '本机'}技能。
|
|
44
|
+
|
|
45
|
+
路径规则:
|
|
46
|
+
根目录: ${rootDir}
|
|
47
|
+
技能目录 = 根目录 + "${sep}" + 技能名
|
|
48
|
+
SKILL.md = 技能目录 + "${sep}SKILL.md"
|
|
49
|
+
例: 技能名为 ${exampleName},其 SKILL.md 完整路径为
|
|
50
|
+
${examplePath}
|
|
51
|
+
执行脚本时必须写完整绝对路径。
|
|
52
|
+
`;
|
|
53
|
+
// 技能条目(name + description)
|
|
54
|
+
const items = [];
|
|
55
|
+
for (const skill of skills) {
|
|
56
|
+
const desc = readSkillDescription(skill.skillDir);
|
|
57
|
+
items.push(`- ${skill.name}\n ${desc || '(无描述)'}`);
|
|
58
|
+
}
|
|
59
|
+
const content = header + '\n' + items.join('\n\n') + '\n';
|
|
60
|
+
fs.writeFileSync(filePath, content, 'utf-8');
|
|
61
|
+
logger.debug(`[skill-index] ${filename} 已生成(${skills.length} 个技能)`);
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* 读取指定技能目录下 SKILL.md 的 description。
|
|
65
|
+
*
|
|
66
|
+
* 直接按目录读,不走 discoverSkillDirectories——后者按父目录扫描再按 name 匹配,
|
|
67
|
+
* 同名技能(本机覆盖内置)会读到另一份的 description。
|
|
68
|
+
*/
|
|
69
|
+
function readSkillDescription(skillDir) {
|
|
70
|
+
try {
|
|
71
|
+
const text = fs.readFileSync(path.join(skillDir, 'SKILL.md'), 'utf-8');
|
|
72
|
+
const match = text.match(/^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/);
|
|
73
|
+
if (!match)
|
|
74
|
+
return '';
|
|
75
|
+
const doc = parseDocument(match[1]);
|
|
76
|
+
if (doc.errors.length > 0)
|
|
77
|
+
return '';
|
|
78
|
+
const meta = doc.toJS();
|
|
79
|
+
return typeof meta?.description === 'string' ? meta.description.trim() : '';
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
return '';
|
|
83
|
+
}
|
|
84
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { generateSkillIndexes } from './index-generator.js';
|
|
2
|
+
import { globalSkillWatcher } from './global-watcher.js';
|
|
3
|
+
import { logger } from '../../utils/logger.js';
|
|
4
|
+
import { invalidateKitCache } from '../../eck/kit-renderer.js';
|
|
5
|
+
/**
|
|
6
|
+
* daemon 启动时,启动全局监控器。
|
|
7
|
+
*
|
|
8
|
+
* 采用懒加载机制:
|
|
9
|
+
* - 启动时不生成索引
|
|
10
|
+
* - 监控器检测到变化时标记所有 agent 为"脏"
|
|
11
|
+
* - agent 收到消息需要组装系统上下文时,检查并按需生成索引
|
|
12
|
+
*/
|
|
13
|
+
export function startSkillWatcher() {
|
|
14
|
+
// 启动全局监控器(监控内置和本机技能目录)
|
|
15
|
+
globalSkillWatcher.start();
|
|
16
|
+
// 注册防抖回调:内置和本机技能变化时标记所有 agent 为脏并失效缓存
|
|
17
|
+
globalSkillWatcher.onDebouncedChange('builtin', () => {
|
|
18
|
+
logger.info('[skill] 内置技能有变化,标记所有 agent 为脏状态');
|
|
19
|
+
globalSkillWatcher.markAllAgentsDirty();
|
|
20
|
+
invalidateKitCache();
|
|
21
|
+
});
|
|
22
|
+
globalSkillWatcher.onDebouncedChange('local', () => {
|
|
23
|
+
logger.info('[skill] 本机技能有变化,标记所有 agent 为脏状态');
|
|
24
|
+
globalSkillWatcher.markAllAgentsDirty();
|
|
25
|
+
invalidateKitCache();
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* 为单个 agent 注册技能目录监控及防抖回调。
|
|
30
|
+
*
|
|
31
|
+
* 在以下两处调用:
|
|
32
|
+
* 1. daemon 启动后 loadAll() 加载已有 agent(补注册)
|
|
33
|
+
* 2. startAgent 动态启动新 agent
|
|
34
|
+
*
|
|
35
|
+
* 注意:回调内不得再调 markDirtyAgent——脏标记已由文件事件触发时设置,
|
|
36
|
+
* 重复调用会 re-arm debounce timer 造成 2 秒自激循环。
|
|
37
|
+
*/
|
|
38
|
+
export function watchAgentSkills(aid) {
|
|
39
|
+
globalSkillWatcher.watchAgent(aid);
|
|
40
|
+
globalSkillWatcher.onDebouncedChange(`agent:${aid}`, () => {
|
|
41
|
+
logger.info(`[skill] Agent ${aid} 技能目录有变化,标记为脏状态并失效缓存`);
|
|
42
|
+
// 脏标记已在 watcher 的 'all' 事件里由 markDirtyAgent 设置,此处只失效缓存
|
|
43
|
+
invalidateKitCache();
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* 确保指定 agent 的技能索引是最新的(懒加载入口)。
|
|
48
|
+
*
|
|
49
|
+
* 在需要组装系统上下文时调用:
|
|
50
|
+
* 1. 如果该 agent 未进行过首次检查 → 生成索引并标记已检查
|
|
51
|
+
* 2. 如果已检查但标记为脏 → 重新生成索引并清除脏标记
|
|
52
|
+
* 3. 如果已检查且干净 → 直接返回
|
|
53
|
+
*/
|
|
54
|
+
export function ensureSkillIndexUpToDate(agentDir, aid) {
|
|
55
|
+
const needsGenerate = globalSkillWatcher.needsIndexGeneration(aid);
|
|
56
|
+
if (needsGenerate) {
|
|
57
|
+
try {
|
|
58
|
+
logger.debug(`[skill] 为 agent ${aid} 生成技能索引`);
|
|
59
|
+
generateSkillIndexes(agentDir);
|
|
60
|
+
globalSkillWatcher.markAgentChecked(aid);
|
|
61
|
+
globalSkillWatcher.markAgentClean(aid);
|
|
62
|
+
}
|
|
63
|
+
catch (e) {
|
|
64
|
+
logger.warn(`[skill] 生成索引失败 (${aid}): ${e instanceof Error ? e.message : String(e)}`);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* 停止全局监控器(daemon 关闭时调用)
|
|
70
|
+
*/
|
|
71
|
+
export async function stopGlobalSkillWatcher() {
|
|
72
|
+
await globalSkillWatcher.stop();
|
|
73
|
+
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { getPackageRoot } from '../../paths.js';
|
|
4
|
+
import { resolveRoot } from '../../paths.js';
|
|
5
|
+
function getSkillPaths(agentDir, override) {
|
|
6
|
+
return {
|
|
7
|
+
builtin: override?.builtin ?? path.join(getPackageRoot(), 'kits', 'skills'),
|
|
8
|
+
local: override?.local ?? path.join(resolveRoot(), 'skills'),
|
|
9
|
+
agent: path.join(agentDir, 'skills'),
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
/** 读取汇总开关文件(.switches.json),返回技能名 → 开关的字典 */
|
|
13
|
+
function readSwitchesFile(file) {
|
|
14
|
+
try {
|
|
15
|
+
if (!fs.existsSync(file))
|
|
16
|
+
return {};
|
|
17
|
+
const raw = fs.readFileSync(file, 'utf-8');
|
|
18
|
+
const data = JSON.parse(raw);
|
|
19
|
+
if (typeof data !== 'object' || data === null)
|
|
20
|
+
return {};
|
|
21
|
+
const result = {};
|
|
22
|
+
for (const [name, value] of Object.entries(data)) {
|
|
23
|
+
if (typeof value !== 'object' || value === null)
|
|
24
|
+
continue;
|
|
25
|
+
const sw = value;
|
|
26
|
+
result[name] = {
|
|
27
|
+
enabled: sw.enabled !== undefined ? Boolean(sw.enabled) : false,
|
|
28
|
+
locked: sw.locked !== undefined ? Boolean(sw.locked) : false,
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
return result;
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
return {};
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
/** 从字典中获取某个技能的开关(缺失时返回默认 enabled:false) */
|
|
38
|
+
function getSwitch(switches, name) {
|
|
39
|
+
return switches[name] || { enabled: false, locked: false };
|
|
40
|
+
}
|
|
41
|
+
/** 扫描一级目录下所有技能名(有 SKILL.md 才算) */
|
|
42
|
+
function scanSkillNames(root) {
|
|
43
|
+
const names = new Set();
|
|
44
|
+
try {
|
|
45
|
+
if (!fs.existsSync(root))
|
|
46
|
+
return names;
|
|
47
|
+
const entries = fs.readdirSync(root, { withFileTypes: true });
|
|
48
|
+
for (const entry of entries) {
|
|
49
|
+
if (entry.name.startsWith('.'))
|
|
50
|
+
continue;
|
|
51
|
+
if (!entry.isDirectory() && !entry.isSymbolicLink())
|
|
52
|
+
continue;
|
|
53
|
+
const skillFile = path.join(root, entry.name, 'SKILL.md');
|
|
54
|
+
if (fs.existsSync(skillFile))
|
|
55
|
+
names.add(entry.name);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
catch { }
|
|
59
|
+
return names;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* 三级开关解析 + 覆盖链求值。
|
|
63
|
+
*
|
|
64
|
+
* 按 design §4.1 算法:
|
|
65
|
+
* 1. 扫描三级目录,收集所有技能名
|
|
66
|
+
* 2. 对每个技能按覆盖链求值:内置 locked → Agent → 本机 → 内置
|
|
67
|
+
* 3. 返回最终启用的技能集合(带来源标记)
|
|
68
|
+
*/
|
|
69
|
+
export function resolveSkills(agentDir, override) {
|
|
70
|
+
const paths = getSkillPaths(agentDir, override);
|
|
71
|
+
// ① 收集所有技能名(合并三级)
|
|
72
|
+
const builtinNames = scanSkillNames(paths.builtin);
|
|
73
|
+
const localNames = scanSkillNames(paths.local);
|
|
74
|
+
const allNames = new Set([...builtinNames, ...localNames]);
|
|
75
|
+
// ② 读取三级汇总开关文件
|
|
76
|
+
const builtinSwitches = readSwitchesFile(path.join(paths.builtin, '.switches.json'));
|
|
77
|
+
const localSwitches = readSwitchesFile(path.join(paths.local, '.switches.json'));
|
|
78
|
+
const agentSwitches = readSwitchesFile(path.join(paths.agent, '.switches.json'));
|
|
79
|
+
const results = [];
|
|
80
|
+
for (const name of allNames) {
|
|
81
|
+
// ③ 判断来源:本机有 SKILL.md → local,否则 builtin
|
|
82
|
+
const source = localNames.has(name) ? 'local' : 'builtin';
|
|
83
|
+
const skillRoot = source === 'local' ? paths.local : paths.builtin;
|
|
84
|
+
const skillDir = path.join(skillRoot, name);
|
|
85
|
+
// ④ 覆盖链求值(design §4.1)
|
|
86
|
+
const builtinSwitch = getSwitch(builtinSwitches, name);
|
|
87
|
+
// 内置 locked 时,直接由内置 enabled 决定
|
|
88
|
+
if (builtinSwitch.locked) {
|
|
89
|
+
if (builtinSwitch.enabled) {
|
|
90
|
+
results.push({ name, source, enabled: true, skillDir });
|
|
91
|
+
}
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
// 否则从 Agent 级往下找第一个存在的开关
|
|
95
|
+
const agentSwitch = getSwitch(agentSwitches, name);
|
|
96
|
+
if (name in agentSwitches) {
|
|
97
|
+
if (agentSwitch.enabled) {
|
|
98
|
+
results.push({ name, source, enabled: true, skillDir });
|
|
99
|
+
}
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
const localSwitch = getSwitch(localSwitches, name);
|
|
103
|
+
if (name in localSwitches) {
|
|
104
|
+
if (localSwitch.enabled) {
|
|
105
|
+
results.push({ name, source, enabled: true, skillDir });
|
|
106
|
+
}
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
// 三级都没有显式开关 → 用缺省(enabled:false,装完必须显式启用)
|
|
110
|
+
if (builtinSwitch.enabled) {
|
|
111
|
+
results.push({ name, source, enabled: true, skillDir });
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
return results;
|
|
115
|
+
}
|