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
|
@@ -0,0 +1,629 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ec-skill 唯一实现。
|
|
3
|
+
*
|
|
4
|
+
* 被两个入口共用:
|
|
5
|
+
* - CLI: ec skill <sub> (薄转发到本文件的 export)
|
|
6
|
+
* - 模型侧: node <绝对路径>/skill.mjs <sub>
|
|
7
|
+
*
|
|
8
|
+
* 脚本必须自定位(import.meta.dirname),绝不依赖 cwd。
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import fs from 'node:fs';
|
|
12
|
+
import path from 'node:path';
|
|
13
|
+
import os from 'node:os';
|
|
14
|
+
import { evolcoreHome, loadDep } from './deps.mjs';
|
|
15
|
+
import { readSkillFrontmatter } from './frontmatter.mjs';
|
|
16
|
+
|
|
17
|
+
// ── 路径 ──────────────────────────────────────────────────────────────
|
|
18
|
+
|
|
19
|
+
/** 技能三级的根目录 */
|
|
20
|
+
function skillRoots() {
|
|
21
|
+
return {
|
|
22
|
+
builtin: path.join(packageRoot(), 'kits', 'skills'),
|
|
23
|
+
local: path.join(evolcoreHome(), 'skills'),
|
|
24
|
+
agent: agentSkillsDir(),
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** 包根:本文件在 <pkg>/kits/skills/ec-skill/scripts/ → 上溯 5 层 */
|
|
29
|
+
function packageRoot() {
|
|
30
|
+
return path.resolve(import.meta.dirname, '..', '..', '..', '..');
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** 当前 agent 的 skills 目录。由环境变量注入,缺失时退回本机级之外的空值。 */
|
|
34
|
+
function agentSkillsDir() {
|
|
35
|
+
const agentDir = process.env.EVOLCORE_AGENT_DIR;
|
|
36
|
+
if (agentDir) return path.join(agentDir, 'skills');
|
|
37
|
+
const aid = process.env.EVOLCORE_SELF_AID;
|
|
38
|
+
if (aid) return path.join(evolcoreHome(), 'agents', aid, 'skills');
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const SWITCHES_FILE = '.switches.json';
|
|
43
|
+
|
|
44
|
+
// ── 权限门禁 ──────────────────────────────────────────────────────────
|
|
45
|
+
//
|
|
46
|
+
// 三级技能目录的写权限不对等,判据也不同:
|
|
47
|
+
//
|
|
48
|
+
// agent 级 $AGENT_DIR/skills/ 只影响本 Agent → 本 Agent 的 owner
|
|
49
|
+
// 本机级 $EVOLCORE_HOME/skills/ 整机所有 agent 共享 → daemonowner
|
|
50
|
+
//
|
|
51
|
+
// 群聊一律拒绝——包括 owner 亲自下指令。技能开关是 agent 的自身配置,
|
|
52
|
+
// 不该被群里的对话推动(owner 要改就私聊说)。
|
|
53
|
+
//
|
|
54
|
+
// 判据取自 daemon 注入的运行时上下文(EVOLCORE_TASK_RUNTIME_CONTEXT)。
|
|
55
|
+
// peerRole 是关系层角色,peerId 是 daemon 自己填的对端原生 ID,会话内伪造不了。
|
|
56
|
+
// 无上下文(宿主终端直连、daemon 自己的调度脚本、CI)视为本机管理员在操作,放行。
|
|
57
|
+
//
|
|
58
|
+
// 门禁只在本文件实现:CLI(ec skill)和模型侧(node skill.mjs)共用同一份实现,
|
|
59
|
+
// 放 CLI 层挡不住模型直接跑脚本那条路径。
|
|
60
|
+
|
|
61
|
+
const RUNTIME_CONTEXT_ENV = 'EVOLCORE_TASK_RUNTIME_CONTEXT';
|
|
62
|
+
|
|
63
|
+
/** 读取 daemonowner 名单(daemon.json 的 owners[]);读不到返回空数组 */
|
|
64
|
+
function daemonOwners() {
|
|
65
|
+
const file = path.join(evolcoreHome(), 'daemon.json');
|
|
66
|
+
try {
|
|
67
|
+
const data = JSON.parse(fs.readFileSync(file, 'utf-8'));
|
|
68
|
+
if (!Array.isArray(data?.owners)) return [];
|
|
69
|
+
return data.owners.map(v => String(v || '').trim()).filter(Boolean);
|
|
70
|
+
} catch {
|
|
71
|
+
return [];
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* 校验当前调用方对某一级的写权限。
|
|
77
|
+
* @param {'agent'|'local'} scope
|
|
78
|
+
*/
|
|
79
|
+
function assertPermission(scope) {
|
|
80
|
+
const raw = process.env[RUNTIME_CONTEXT_ENV];
|
|
81
|
+
if (!raw) return; // 无上下文:终端直连,放行
|
|
82
|
+
|
|
83
|
+
let ctx;
|
|
84
|
+
try {
|
|
85
|
+
ctx = JSON.parse(raw);
|
|
86
|
+
} catch {
|
|
87
|
+
return; // 上下文损坏按无上下文处理,与本目录既有的「读不到就退回默认」一致
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const chatType = ctx?.chatType;
|
|
91
|
+
const target = scope === 'local' ? '本机级' : 'Agent 级';
|
|
92
|
+
|
|
93
|
+
if (chatType === 'group') {
|
|
94
|
+
throw new Error(
|
|
95
|
+
`拒绝:${target}技能开关不支持在群聊中操作。请在私聊中提出,或由管理员在宿主终端执行。`
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (scope === 'agent') {
|
|
100
|
+
if (ctx?.peerRole !== 'owner' && ctx?.peerRole !== 'admin') {
|
|
101
|
+
throw new Error(
|
|
102
|
+
`拒绝:Agent 级技能开关仅限本 Agent 的 owner 或 admin(当前角色:${ctx?.peerRole || '未知'})。`
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// 本机级:所有 agent 共享,仅 daemonowner 可改
|
|
109
|
+
const peerId = String(ctx?.peerId || '').trim();
|
|
110
|
+
if (!peerId || !daemonOwners().includes(peerId)) {
|
|
111
|
+
throw new Error(
|
|
112
|
+
`拒绝:本机级技能为整机所有 agent 共享,仅限 daemonowner 安装、删除、创建与开关。`
|
|
113
|
+
+ `\n 当前身份:${peerId || '未知'}(可用 ec skill where 查看本机级目录)`
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// ── 开关读写 ──────────────────────────────────────────────────────────
|
|
119
|
+
|
|
120
|
+
function readSwitches(dir) {
|
|
121
|
+
if (!dir) return {};
|
|
122
|
+
const file = path.join(dir, SWITCHES_FILE);
|
|
123
|
+
try {
|
|
124
|
+
if (!fs.existsSync(file)) return {};
|
|
125
|
+
const data = JSON.parse(fs.readFileSync(file, 'utf-8'));
|
|
126
|
+
if (!data || typeof data !== 'object') return {};
|
|
127
|
+
return data;
|
|
128
|
+
} catch {
|
|
129
|
+
return {};
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* 写开关:读-改-写。原子替换(先写临时文件再 rename),避免写入中途被读到半截。
|
|
135
|
+
* locked 只在内置级有意义,本机/Agent 级写入时忽略该字段。
|
|
136
|
+
*/
|
|
137
|
+
function writeSwitch(dir, name, { enabled, locked }) {
|
|
138
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
139
|
+
const file = path.join(dir, SWITCHES_FILE);
|
|
140
|
+
const current = readSwitches(dir);
|
|
141
|
+
|
|
142
|
+
const entry = { ...(current[name] || {}) };
|
|
143
|
+
if (enabled !== undefined) entry.enabled = enabled;
|
|
144
|
+
if (locked !== undefined) entry.locked = locked;
|
|
145
|
+
current[name] = entry;
|
|
146
|
+
|
|
147
|
+
const tmp = `${file}.tmp-${process.pid}`;
|
|
148
|
+
fs.writeFileSync(tmp, JSON.stringify(current, null, 2) + '\n', 'utf-8');
|
|
149
|
+
fs.renameSync(tmp, file);
|
|
150
|
+
return file;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// ── 技能扫描与校验 ────────────────────────────────────────────────────
|
|
154
|
+
|
|
155
|
+
const NAME_RE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
156
|
+
|
|
157
|
+
/** 从 SKILL.md 读 frontmatter(自带解析器,零外部依赖) */
|
|
158
|
+
function readFrontmatter(skillDir) {
|
|
159
|
+
return readSkillFrontmatter(skillDir);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* 校验技能是否合规。返回错误数组(空数组 = 合规)。
|
|
164
|
+
* 依据 skill-md-spec-and-loading.md §8.1。
|
|
165
|
+
*/
|
|
166
|
+
export function validateSkill(skillDir) {
|
|
167
|
+
const errors = [];
|
|
168
|
+
const dirName = path.basename(skillDir);
|
|
169
|
+
|
|
170
|
+
if (!fs.existsSync(path.join(skillDir, 'SKILL.md'))) {
|
|
171
|
+
return [`缺少 SKILL.md`];
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const fm = readFrontmatter(skillDir);
|
|
175
|
+
if (fm === null) return [`SKILL.md 缺少 frontmatter(需以 --- 包裹的 YAML 块)`];
|
|
176
|
+
if (fm.__error) return [`frontmatter 解析失败: ${fm.__error}`];
|
|
177
|
+
|
|
178
|
+
const name = typeof fm.name === 'string' ? fm.name.trim() : '';
|
|
179
|
+
const description = typeof fm.description === 'string' ? fm.description.trim() : '';
|
|
180
|
+
|
|
181
|
+
if (!name) errors.push('frontmatter 缺少必填字段 name');
|
|
182
|
+
if (!description) errors.push('frontmatter 缺少必填字段 description');
|
|
183
|
+
|
|
184
|
+
if (name) {
|
|
185
|
+
if (!NAME_RE.test(name)) errors.push(`name「${name}」不符合 kebab-case(小写字母/数字/连字符)`);
|
|
186
|
+
if (name.length > 64) errors.push(`name 超过 64 字符(当前 ${name.length})`);
|
|
187
|
+
if (name !== dirName) errors.push(`name「${name}」与目录名「${dirName}」不一致`);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
return errors;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/** 扫描一级目录下所有技能(含 SKILL.md 的目录) */
|
|
194
|
+
function scanSkills(root) {
|
|
195
|
+
if (!root) return [];
|
|
196
|
+
try {
|
|
197
|
+
if (!fs.existsSync(root)) return [];
|
|
198
|
+
return fs.readdirSync(root, { withFileTypes: true })
|
|
199
|
+
.filter(e => !e.name.startsWith('.'))
|
|
200
|
+
.filter(e => e.isDirectory() || e.isSymbolicLink())
|
|
201
|
+
.map(e => {
|
|
202
|
+
const dir = path.join(root, e.name);
|
|
203
|
+
try { if (!fs.statSync(dir).isDirectory()) return null; } catch { return null; }
|
|
204
|
+
if (!fs.existsSync(path.join(dir, 'SKILL.md'))) return null;
|
|
205
|
+
return { name: e.name, dir };
|
|
206
|
+
})
|
|
207
|
+
.filter(Boolean);
|
|
208
|
+
} catch {
|
|
209
|
+
return [];
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* 三级解析 + 覆盖链求值,与 src/core/skill/switch-resolver.ts 同规则。
|
|
215
|
+
* 来源由 SKILL.md 物理位置决定(本机有同名则算本机)。
|
|
216
|
+
*/
|
|
217
|
+
export function resolveAll() {
|
|
218
|
+
const roots = skillRoots();
|
|
219
|
+
const builtinSkills = scanSkills(roots.builtin);
|
|
220
|
+
const localSkills = scanSkills(roots.local);
|
|
221
|
+
const builtinSw = readSwitches(roots.builtin);
|
|
222
|
+
const localSw = readSwitches(roots.local);
|
|
223
|
+
const agentSw = readSwitches(roots.agent);
|
|
224
|
+
|
|
225
|
+
const byName = new Map();
|
|
226
|
+
for (const s of builtinSkills) byName.set(s.name, { ...s, source: 'builtin' });
|
|
227
|
+
for (const s of localSkills) byName.set(s.name, { ...s, source: 'local' }); // 本机覆盖内置
|
|
228
|
+
|
|
229
|
+
const out = [];
|
|
230
|
+
for (const [name, info] of byName) {
|
|
231
|
+
const b = builtinSw[name] || {};
|
|
232
|
+
const l = localSw[name] || {};
|
|
233
|
+
const a = agentSw[name] || {};
|
|
234
|
+
const hasBuiltinLock = b.locked === true;
|
|
235
|
+
|
|
236
|
+
let enabled;
|
|
237
|
+
let decidedBy;
|
|
238
|
+
if (hasBuiltinLock) {
|
|
239
|
+
enabled = b.enabled === true;
|
|
240
|
+
decidedBy = 'builtin(locked)';
|
|
241
|
+
} else if (name in agentSw) {
|
|
242
|
+
enabled = a.enabled === true;
|
|
243
|
+
decidedBy = 'agent';
|
|
244
|
+
} else if (name in localSw) {
|
|
245
|
+
enabled = l.enabled === true;
|
|
246
|
+
decidedBy = 'local';
|
|
247
|
+
} else if (name in builtinSw) {
|
|
248
|
+
enabled = b.enabled === true;
|
|
249
|
+
decidedBy = 'builtin';
|
|
250
|
+
} else {
|
|
251
|
+
enabled = false;
|
|
252
|
+
decidedBy = 'default(缺省禁用)';
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
out.push({
|
|
256
|
+
name,
|
|
257
|
+
source: info.source,
|
|
258
|
+
dir: info.dir,
|
|
259
|
+
enabled,
|
|
260
|
+
decidedBy,
|
|
261
|
+
locked: hasBuiltinLock,
|
|
262
|
+
overridable: !hasBuiltinLock,
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
return out.sort((x, y) => x.name.localeCompare(y.name));
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// ── 子命令 ────────────────────────────────────────────────────────────
|
|
269
|
+
|
|
270
|
+
export function cmdList({ json = false } = {}) {
|
|
271
|
+
const skills = resolveAll();
|
|
272
|
+
if (json) return skills;
|
|
273
|
+
|
|
274
|
+
if (skills.length === 0) return '(没有任何技能)';
|
|
275
|
+
|
|
276
|
+
const lines = [];
|
|
277
|
+
lines.push(`${skills.length} 个技能:`);
|
|
278
|
+
lines.push('');
|
|
279
|
+
const nameW = Math.max(20, ...skills.map(s => s.name.length + 2));
|
|
280
|
+
for (const s of skills) {
|
|
281
|
+
const state = s.enabled ? '开' : '关';
|
|
282
|
+
const lock = s.locked ? ' [locked]' : '';
|
|
283
|
+
lines.push(` ${s.name.padEnd(nameW)}${s.source.padEnd(9)}${state}${lock}`);
|
|
284
|
+
}
|
|
285
|
+
lines.push('');
|
|
286
|
+
lines.push('列:名称 / 来源 / 最终状态');
|
|
287
|
+
return lines.join('\n');
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
export function cmdInfo(name, { json = false } = {}) {
|
|
291
|
+
const skill = resolveAll().find(s => s.name === name);
|
|
292
|
+
if (!skill) throw new Error(`技能不存在: ${name}`);
|
|
293
|
+
|
|
294
|
+
const roots = skillRoots();
|
|
295
|
+
const b = readSwitches(roots.builtin)[name] || {};
|
|
296
|
+
const l = readSwitches(roots.local)[name] || {};
|
|
297
|
+
const a = readSwitches(roots.agent)[name] || {};
|
|
298
|
+
const fm = readFrontmatter(skill.dir) || {};
|
|
299
|
+
const errors = validateSkill(skill.dir);
|
|
300
|
+
|
|
301
|
+
const info = {
|
|
302
|
+
name: skill.name,
|
|
303
|
+
source: skill.source,
|
|
304
|
+
dir: skill.dir,
|
|
305
|
+
description: typeof fm.description === 'string' ? fm.description : '',
|
|
306
|
+
enabled: skill.enabled,
|
|
307
|
+
decidedBy: skill.decidedBy,
|
|
308
|
+
locked: skill.locked,
|
|
309
|
+
switches: {
|
|
310
|
+
builtin: name in readSwitches(roots.builtin) ? b : null,
|
|
311
|
+
local: name in readSwitches(roots.local) ? l : null,
|
|
312
|
+
agent: name in readSwitches(roots.agent) ? a : null,
|
|
313
|
+
},
|
|
314
|
+
valid: errors.length === 0,
|
|
315
|
+
errors,
|
|
316
|
+
};
|
|
317
|
+
|
|
318
|
+
if (json) return info;
|
|
319
|
+
|
|
320
|
+
const lines = [];
|
|
321
|
+
lines.push(`技能: ${info.name}`);
|
|
322
|
+
lines.push(`来源: ${info.source}`);
|
|
323
|
+
lines.push(`路径: ${info.dir}`);
|
|
324
|
+
lines.push(`状态: ${info.enabled ? '开' : '关'}(由 ${info.decidedBy} 决定)`);
|
|
325
|
+
if (info.locked) lines.push(`锁定: 内置级 locked,任何级别都关不掉`);
|
|
326
|
+
lines.push('');
|
|
327
|
+
lines.push(`描述: ${info.description || '(无)'}`);
|
|
328
|
+
lines.push('');
|
|
329
|
+
lines.push('三级开关:');
|
|
330
|
+
lines.push(` 内置级: ${info.switches.builtin ? JSON.stringify(info.switches.builtin) : '(无声明)'}`);
|
|
331
|
+
lines.push(` 本机级: ${info.switches.local ? JSON.stringify(info.switches.local) : '(无声明)'}`);
|
|
332
|
+
lines.push(` Agent级: ${info.switches.agent ? JSON.stringify(info.switches.agent) : '(无声明)'}`);
|
|
333
|
+
if (!info.valid) {
|
|
334
|
+
lines.push('');
|
|
335
|
+
lines.push('⚠️ 校验问题:');
|
|
336
|
+
errors.forEach(e => lines.push(` - ${e}`));
|
|
337
|
+
}
|
|
338
|
+
return lines.join('\n');
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
export function cmdValidate(target, { json = false } = {}) {
|
|
342
|
+
const dir = resolveSkillDirArg(target);
|
|
343
|
+
const errors = validateSkill(dir);
|
|
344
|
+
const valid = errors.length === 0;
|
|
345
|
+
if (json) return { dir, valid, errors };
|
|
346
|
+
|
|
347
|
+
const output = valid
|
|
348
|
+
? `✅ ${path.basename(dir)} 校验通过`
|
|
349
|
+
: [`❌ ${path.basename(dir)} 校验失败:`, ...errors.map(e => ` - ${e}`)].join('\n');
|
|
350
|
+
// 非 JSON 模式也带上 valid,供 CLI 决定退出码
|
|
351
|
+
return { valid, output, __isText: true };
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
/** 把「技能名」或「目录路径」统一解析成技能目录 */
|
|
355
|
+
export function resolveSkillDirArg(target) {
|
|
356
|
+
if (!target) throw new Error('缺少技能名或路径');
|
|
357
|
+
if (fs.existsSync(target) && fs.statSync(target).isDirectory()) return path.resolve(target);
|
|
358
|
+
// 当作技能名,在三级里找
|
|
359
|
+
const roots = skillRoots();
|
|
360
|
+
for (const root of [roots.local, roots.builtin]) {
|
|
361
|
+
if (!root) continue;
|
|
362
|
+
const dir = path.join(root, target);
|
|
363
|
+
if (fs.existsSync(dir)) return dir;
|
|
364
|
+
}
|
|
365
|
+
throw new Error(`找不到技能: ${target}`);
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
export function cmdCreate(name, { desc = '', force = false, enable = false, json = false } = {}) {
|
|
369
|
+
assertPermission('local'); // 骨架固定建在本机级,与 install 同档
|
|
370
|
+
if (!name) throw new Error('缺少技能名。用法:ec skill create <name> --desc "..."');
|
|
371
|
+
if (!NAME_RE.test(name)) {
|
|
372
|
+
throw new Error(`技能名「${name}」不符合 kebab-case(小写字母/数字/连字符,如 my-skill)`);
|
|
373
|
+
}
|
|
374
|
+
if (name.length > 64) throw new Error(`技能名超过 64 字符`);
|
|
375
|
+
|
|
376
|
+
const roots = skillRoots();
|
|
377
|
+
const target = path.join(roots.local, name);
|
|
378
|
+
|
|
379
|
+
if (fs.existsSync(target) && !force) {
|
|
380
|
+
throw new Error(`技能已存在: ${target}\n如需覆盖请加 --force`);
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
fs.mkdirSync(path.join(target, 'scripts'), { recursive: true });
|
|
384
|
+
fs.mkdirSync(path.join(target, 'references'), { recursive: true });
|
|
385
|
+
|
|
386
|
+
const tmpl = fs.readFileSync(path.join(import.meta.dirname, 'template', 'SKILL.md.tmpl'), 'utf-8');
|
|
387
|
+
const content = tmpl
|
|
388
|
+
.replaceAll('{{name}}', name)
|
|
389
|
+
.replaceAll('{{description}}', desc || '(一句话说明这个技能做什么。务必补全触发场景与不适用场景,description 是模型选择技能的唯一依据。)');
|
|
390
|
+
|
|
391
|
+
fs.writeFileSync(path.join(target, 'SKILL.md'), content, 'utf-8');
|
|
392
|
+
|
|
393
|
+
// 与 install 一致:缺省即禁用,不写开关文件;只有 --enable 才显式开启
|
|
394
|
+
if (enable) writeSwitch(roots.local, name, { enabled: true });
|
|
395
|
+
|
|
396
|
+
const result = { name, dir: target, created: true, enabled: enable };
|
|
397
|
+
if (json) return result;
|
|
398
|
+
|
|
399
|
+
return [
|
|
400
|
+
`✅ 已创建技能 ${name}`,
|
|
401
|
+
` 路径: ${target}`,
|
|
402
|
+
` 状态: ${enable ? '已启用' : '已禁用(开关缺省即禁用)'}`,
|
|
403
|
+
'',
|
|
404
|
+
'下一步:',
|
|
405
|
+
' 1. 补全 SKILL.md 的 description(决定技能会不会被选中,务必写清触发场景)',
|
|
406
|
+
' 2. 正文写清步骤;需要时把大块资料放 references/',
|
|
407
|
+
' 3. 需要脚本就放 scripts/,脚本内用 import.meta.dirname 自定位,不要依赖 cwd',
|
|
408
|
+
enable ? ` 4. 如需停用: ec skill disable ${name}` : ` 4. 完成后启用: ec skill enable ${name}`,
|
|
409
|
+
].join('\n');
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
export function cmdInstall(src, { force = false, enable = false, json = false } = {}) {
|
|
413
|
+
assertPermission('local'); // 目标恒为本机级
|
|
414
|
+
if (!src) throw new Error('缺少源。用法:ec skill install <目录|zip文件|SKILL.md>');
|
|
415
|
+
if (!fs.existsSync(src)) throw new Error(`源不存在: ${src}`);
|
|
416
|
+
|
|
417
|
+
const roots = skillRoots();
|
|
418
|
+
const stat = fs.statSync(src);
|
|
419
|
+
let staging = null;
|
|
420
|
+
let skillDir;
|
|
421
|
+
|
|
422
|
+
if (stat.isDirectory()) {
|
|
423
|
+
skillDir = path.resolve(src);
|
|
424
|
+
} else if (/\.zip$/i.test(src)) {
|
|
425
|
+
staging = extractZip(src);
|
|
426
|
+
skillDir = findSkillDirIn(staging);
|
|
427
|
+
if (!skillDir) throw new Error(`压缩包里没找到 SKILL.md(已解压到 ${staging})`);
|
|
428
|
+
} else if (/\.md$/i.test(src)) {
|
|
429
|
+
staging = fs.mkdtempSync(path.join(os.tmpdir(), 'ec-skill-'));
|
|
430
|
+
const name = path.basename(src).replace(/\.md$/i, '');
|
|
431
|
+
skillDir = path.join(staging, name);
|
|
432
|
+
fs.mkdirSync(skillDir, { recursive: true });
|
|
433
|
+
fs.copyFileSync(src, path.join(skillDir, 'SKILL.md'));
|
|
434
|
+
} else {
|
|
435
|
+
throw new Error(`不支持的源类型: ${src}(支持目录、.zip、.md)`);
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
try {
|
|
439
|
+
const name = path.basename(skillDir);
|
|
440
|
+
|
|
441
|
+
// 安全:不得装进内置级,不得装进 Agent 级
|
|
442
|
+
if (samePath(skillDir, roots.builtin) || isInside(skillDir, roots.builtin)) {
|
|
443
|
+
throw new Error('拒绝安装:源位于内置级目录内(内置技能随包发布,不可通过 install 修改)');
|
|
444
|
+
}
|
|
445
|
+
if (roots.agent && (samePath(skillDir, roots.agent) || isInside(skillDir, roots.agent))) {
|
|
446
|
+
throw new Error('拒绝安装:源位于 Agent 级目录内(Agent 级只放开关,不放 SKILL.md)');
|
|
447
|
+
}
|
|
448
|
+
if (!NAME_RE.test(name)) {
|
|
449
|
+
throw new Error(`技能名「${name}」不符合 kebab-case,拒绝安装`);
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
const errors = validateSkill(skillDir);
|
|
453
|
+
if (errors.length > 0) {
|
|
454
|
+
throw new Error(['拒绝安装:SKILL.md 校验未通过', ...errors.map(e => ` - ${e}`)].join('\n'));
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
const target = path.join(roots.local, name);
|
|
458
|
+
if (fs.existsSync(target)) {
|
|
459
|
+
if (!force) throw new Error(`技能已存在: ${target}\n如需覆盖请加 --force`);
|
|
460
|
+
fs.rmSync(target, { recursive: true, force: true });
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
copyDir(skillDir, target);
|
|
464
|
+
|
|
465
|
+
// 装完**不写开关文件**:开关字典里没有该技能 = 禁用(缺省即禁用)。
|
|
466
|
+
// 只有 --enable 才显式写入 {"enabled": true}。
|
|
467
|
+
if (enable) writeSwitch(roots.local, name, { enabled: true });
|
|
468
|
+
|
|
469
|
+
const result = { name, dir: target, installed: true, enabled: enable };
|
|
470
|
+
if (json) return result;
|
|
471
|
+
return [
|
|
472
|
+
`✅ 已安装技能 ${name} → ${target}`,
|
|
473
|
+
` 状态: ${enable ? '已启用' : '已禁用(开关缺省即禁用)'}`,
|
|
474
|
+
'',
|
|
475
|
+
'约 2 秒后标记待更新,下一条消息处理时重建索引。可用 `ec skill list` 确认。',
|
|
476
|
+
enable
|
|
477
|
+
? `如需停用: ec skill disable ${name}`
|
|
478
|
+
: `确认要启用吗?启用: ec skill enable ${name}(本机级全机启用加 --level local)`,
|
|
479
|
+
].join('\n');
|
|
480
|
+
} finally {
|
|
481
|
+
if (staging) fs.rmSync(staging, { recursive: true, force: true });
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
export function cmdRemove(name, { json = false } = {}) {
|
|
486
|
+
assertPermission('local'); // 只删本机级
|
|
487
|
+
if (!name) throw new Error('缺少技能名。用法:ec skill remove <name>');
|
|
488
|
+
const roots = skillRoots();
|
|
489
|
+
const target = path.join(roots.local, name);
|
|
490
|
+
|
|
491
|
+
if (!fs.existsSync(target)) {
|
|
492
|
+
if (fs.existsSync(path.join(roots.builtin, name))) {
|
|
493
|
+
throw new Error(`「${name}」是内置技能,随包发布,不能用 remove 删除。\n如要禁用请用: ec skill disable ${name}`);
|
|
494
|
+
}
|
|
495
|
+
throw new Error(`本机级没有这个技能: ${name}`);
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
fs.rmSync(target, { recursive: true, force: true });
|
|
499
|
+
|
|
500
|
+
// 顺带清掉该技能在本机级/Agent 级的开关残留
|
|
501
|
+
for (const dir of [roots.local, roots.agent]) {
|
|
502
|
+
if (!dir) continue;
|
|
503
|
+
const sw = readSwitches(dir);
|
|
504
|
+
if (name in sw) {
|
|
505
|
+
delete sw[name];
|
|
506
|
+
const file = path.join(dir, SWITCHES_FILE);
|
|
507
|
+
const tmp = `${file}.tmp-${process.pid}`;
|
|
508
|
+
fs.writeFileSync(tmp, JSON.stringify(sw, null, 2) + '\n', 'utf-8');
|
|
509
|
+
fs.renameSync(tmp, file);
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
const result = { name, removed: true };
|
|
514
|
+
if (json) return result;
|
|
515
|
+
return `✅ 已删除本机技能 ${name}`;
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
export function cmdEnableDisable(name, enabled, { level = 'agent', json = false } = {}) {
|
|
519
|
+
if (!name) throw new Error(`缺少技能名。用法:ec skill ${enabled ? 'enable' : 'disable'} <name>`);
|
|
520
|
+
if (!['agent', 'local'].includes(level)) {
|
|
521
|
+
throw new Error(`--level 只能是 agent 或 local(内置级不可写)`);
|
|
522
|
+
}
|
|
523
|
+
// 级别校验通过后再判权限,避免非法 --level 先撞权限门禁而掩盖真实错因
|
|
524
|
+
assertPermission(level === 'local' ? 'local' : 'agent');
|
|
525
|
+
|
|
526
|
+
const roots = skillRoots();
|
|
527
|
+
const dir = roots[level];
|
|
528
|
+
if (!dir) {
|
|
529
|
+
throw new Error(`无法定位 ${level} 级目录(Agent 级需要 EVOLCORE_AGENT_DIR 或 EVOLCORE_SELF_AID 环境变量)`);
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
// 校验技能存在(否则开关写了也没用)
|
|
533
|
+
const known = resolveAll().find(s => s.name === name);
|
|
534
|
+
if (!known) throw new Error(`技能不存在: ${name}`);
|
|
535
|
+
|
|
536
|
+
if (known.locked && level !== 'builtin') {
|
|
537
|
+
throw new Error(`「${name}」是内置 locked 技能,任何级别都关不掉`);
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
const file = writeSwitch(dir, name, { enabled });
|
|
541
|
+
|
|
542
|
+
const result = { name, level, enabled, file };
|
|
543
|
+
if (json) return result;
|
|
544
|
+
return [
|
|
545
|
+
`✅ ${enabled ? '已启用' : '已禁用'} ${name}(${level} 级)`,
|
|
546
|
+
` 写入: ${file}`,
|
|
547
|
+
'',
|
|
548
|
+
'约 2 秒后标记待更新,下一条消息处理时重建索引。',
|
|
549
|
+
].join('\n');
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
export function cmdWhere({ json = false } = {}) {
|
|
553
|
+
const roots = skillRoots();
|
|
554
|
+
const info = {
|
|
555
|
+
builtin: roots.builtin,
|
|
556
|
+
local: roots.local,
|
|
557
|
+
agent: roots.agent,
|
|
558
|
+
deps: path.join(evolcoreHome(), 'skills', '.deps'),
|
|
559
|
+
};
|
|
560
|
+
if (json) return info;
|
|
561
|
+
return [
|
|
562
|
+
'技能三级根目录:',
|
|
563
|
+
` 内置级: ${info.builtin}`,
|
|
564
|
+
` 本机级: ${info.local}`,
|
|
565
|
+
` Agent级: ${info.agent || '(未定位:缺少 EVOLCORE_AGENT_DIR / EVOLCORE_SELF_AID)'}`,
|
|
566
|
+
'',
|
|
567
|
+
`依赖目录: ${info.deps}`,
|
|
568
|
+
].join('\n');
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
// ── 工具 ──────────────────────────────────────────────────────────────
|
|
572
|
+
|
|
573
|
+
function extractZip(zipPath) {
|
|
574
|
+
const AdmZip = loadDep('adm-zip');
|
|
575
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'ec-skill-zip-'));
|
|
576
|
+
const zip = new AdmZip(zipPath);
|
|
577
|
+
|
|
578
|
+
// 防 zip-slip:条目路径不得逃出目标目录
|
|
579
|
+
for (const entry of zip.getEntries()) {
|
|
580
|
+
const dest = path.resolve(dir, entry.entryName);
|
|
581
|
+
if (!isInside(dest, dir) && dest !== path.resolve(dir)) {
|
|
582
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
583
|
+
throw new Error(`压缩包含越界路径,拒绝解压: ${entry.entryName}`);
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
zip.extractAllTo(dir, true);
|
|
587
|
+
return dir;
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
/** 压缩包里技能目录可能在任意深度(常见是顶层套一层同名目录) */
|
|
591
|
+
function findSkillDirIn(root) {
|
|
592
|
+
const direct = path.join(root, 'SKILL.md');
|
|
593
|
+
if (fs.existsSync(direct)) return root;
|
|
594
|
+
const entries = fs.readdirSync(root, { withFileTypes: true }).filter(e => e.isDirectory());
|
|
595
|
+
if (entries.length === 1) {
|
|
596
|
+
const nested = path.join(root, entries[0].name);
|
|
597
|
+
if (fs.existsSync(path.join(nested, 'SKILL.md'))) return nested;
|
|
598
|
+
}
|
|
599
|
+
// 再宽一点:任意一层找到 SKILL.md
|
|
600
|
+
for (const e of entries) {
|
|
601
|
+
const nested = path.join(root, e.name);
|
|
602
|
+
if (fs.existsSync(path.join(nested, 'SKILL.md'))) return nested;
|
|
603
|
+
}
|
|
604
|
+
return null;
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
function copyDir(src, dest) {
|
|
608
|
+
fs.mkdirSync(dest, { recursive: true });
|
|
609
|
+
for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
|
|
610
|
+
const s = path.join(src, entry.name);
|
|
611
|
+
const d = path.join(dest, entry.name);
|
|
612
|
+
if (entry.isDirectory()) copyDir(s, d);
|
|
613
|
+
else if (entry.isSymbolicLink()) {
|
|
614
|
+
// 不跟随软链接,避免把外部内容带进来
|
|
615
|
+
continue;
|
|
616
|
+
} else fs.copyFileSync(s, d);
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
function isInside(child, parent) {
|
|
621
|
+
if (!parent) return false;
|
|
622
|
+
const rel = path.relative(path.resolve(parent), path.resolve(child));
|
|
623
|
+
return rel !== '' && !rel.startsWith('..') && !path.isAbsolute(rel);
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
function samePath(a, b) {
|
|
627
|
+
if (!b) return false;
|
|
628
|
+
return path.resolve(a) === path.resolve(b);
|
|
629
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: {{name}}
|
|
3
|
+
description: >-
|
|
4
|
+
{{description}}
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# {{name}}
|
|
8
|
+
|
|
9
|
+
## 目标
|
|
10
|
+
|
|
11
|
+
(一句话说清这个技能解决什么问题。)
|
|
12
|
+
|
|
13
|
+
## 步骤
|
|
14
|
+
|
|
15
|
+
1. 确认用户的需求落在本技能的适用范围内
|
|
16
|
+
2. ……
|
|
17
|
+
3. ……
|
|
18
|
+
|
|
19
|
+
## 输出契约
|
|
20
|
+
|
|
21
|
+
(这个技能产出什么、什么格式、写到哪。有确定格式就写死。)
|
|
22
|
+
|
|
23
|
+
## 边界情况
|
|
24
|
+
|
|
25
|
+
- 输入缺失时如何处理
|
|
26
|
+
- 遇到不适用场景时如何退出
|
|
27
|
+
|
|
28
|
+
## 参考资料
|
|
29
|
+
|
|
30
|
+
(大块资料放 `references/`,在这里指出文件名。不要把它塞进本文件。)
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# Auto Mention Decision Prompt
|
|
2
|
+
|
|
3
|
+
每次模型判定前通过 mtime + size 检查本模板的变化,修改后下一次判定自动加载。
|
|
4
|
+
以下四个二级章节必须保留且不能为空。加载失败时沿用当前 gate 上次成功加载的版本并告警;
|
|
5
|
+
若尚无有效版本,则走现有的 mention-only 降级。失败结果不缓存,修复后会自动重试。
|
|
6
|
+
|
|
7
|
+
## Instructions
|
|
8
|
+
|
|
9
|
+
请结合 state 中提供的全部会话信息,判断 new_messages 整体应采用哪一种执行策略。state 中包含当前群会话历史对话、尚未完成处理的消息、本次新消息、当前处理状态以及适用的规则和资料。请综合这些信息判断本次新消息是否需要当前 Agent 介入,以及介入时机。只对本次 new_messages 统一选择一个 execution_policy;recent_messages 和 pending_messages 仅作为背景参考,不重新分类。判定时以消息内容、发送者 role 和是否明确要求当前 Agent 回应或行动为依据。role=owner/admin 是当前群的所有者/管理员:其在群内的发言默认应视为需要当前 Agent 关注,判 interrupt——不论语气是提问、追问、补充指示还是评述,也不要求它 @ 或点名当前 Agent。唯一的例外是:该发言内容明确是针对其他某个成员的一对一指示或对话、且与当前 Agent 无关,此时才可判 ignore。正文中出现 @ 到当前 Agent、或提到当前 Agent 的 AID 或显示名时,属于点名,应视为要求当前 Agent 回应,判 interrupt;若 @ 或提到的是其他成员,或当前 Agent 仅被他人在引述中转述提及,则不据此判 interrupt。
|
|
10
|
+
|
|
11
|
+
确实不需要当前 Agent 回应或行动的消息不构成介入理由。典型情形是:纯粹发生在其他成员之间、且没有指向当前 Agent 的进度汇报、状态确认、讨论、澄清、评审与争辩。但判断时必须区分两种情形,不可一概而论:只要消息的指向是当前 Agent 或全体成员,就应视为需要当前 Agent 回应,即使它没有 @、没有点名,也不得因为前文是成员之间的往来就一并忽略。尤其要记住:owner/admin 在群内的发言默认判 interrupt,不要求它指向当前 Agent 或全体、也不要求它用提问或追问的语气——把 owner/admin 的发言判成 ignore 只有一个出口,就是该发言明确是针对其他某个成员的一对一指示或对话、且与当前 Agent 无关。此外,不要因为消息出现在当前 Agent 曾参与过的话题串内,就把纯粹发生在其他成员之间的往来当成与自己相关。
|
|
12
|
+
|
|
13
|
+
若新消息承接当前 Agent 上一轮的发言或汇报,并据此给出后续追问、指令或分派,即使未直接 @ 或提及当前 Agent,也应视为要求当前 Agent 回应;其中明确排除当前 Agent 的部分不构成其处理理由,但其余仍需处理的后续任务不因此排除。
|
|
14
|
+
|
|
15
|
+
## Interrupt Criteria
|
|
16
|
+
|
|
17
|
+
需要当前 Agent 立即回应或行动的请求,包括:被 @、被点名,或正文提到当前 Agent 的 AID 或显示名;owner/admin 在群内的发言(默认视为需要当前 Agent 关注,唯一例外是它明确针对其他成员的一对一指示或对话、且与当前 Agent 无关);承接当前 Agent 上一轮发言或汇报所给出的后续追问、指令或分派。
|
|
18
|
+
|
|
19
|
+
## Defer Criteria
|
|
20
|
+
|
|
21
|
+
可能需要当前 Agent 处理,但当前 Agent 正在处理其他任务且不紧急,可等待其完成后执行。
|
|
22
|
+
|
|
23
|
+
## Ignore Criteria
|
|
24
|
+
|
|
25
|
+
不需要当前 Agent 回应或行动。
|