skill-auto-loader-hook-paperfly777 0.1.2 → 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -86,6 +86,148 @@ openclaw plugins install skill-auto-loader-hook-paperfly777
86
86
  2. `skillsScanDirs` 如果写相对路径,也相对于插件目录解析。
87
87
  3. `openclawConfigPath` 建议使用 `~/.openclaw/openclaw.json`。
88
88
 
89
+ ## 使用说明(重点)
90
+
91
+ ### 1) 怎么使用
92
+
93
+ 1. 安装插件后,确保 `plugins.entries.skill-auto-loader-hook.enabled=true`。
94
+ 2. 配置好 `routerConfigPath` 指向 `skill-router.config.json`。
95
+ 3. 重启 gateway:`openclaw gateway restart`。
96
+ 4. 之后每次用户发消息,插件会在 `before_prompt_build` 自动执行 skill 路由判断。
97
+
98
+ ### 2) 配置文件在哪里
99
+
100
+ - OpenClaw 主配置:`~/.openclaw/openclaw.json`
101
+ - 插件路由配置:`skill-router.config.json`(默认按插件目录相对路径读取)
102
+
103
+ ### 3) 自定义 Prompt 怎么配
104
+
105
+ 在 `skill-router.config.json` 的每条 `rules[*].prompt` 里写即可,例如:
106
+
107
+ ```json
108
+ {
109
+ "id": "company-kb",
110
+ "keywords": ["知识库", "查文档", "公司规定"],
111
+ "preferSkills": ["company-knowledge-base"],
112
+ "excludeSkills": [],
113
+ "scopeNote": "仅限内部文档检索与制度问题",
114
+ "prompt": "先检索知识库再回答;若涉及删除/覆盖写入,必须二次确认。"
115
+ }
116
+ ```
117
+
118
+ 推荐写法:
119
+
120
+ 1. 先写使用范围(`scopeNote`)
121
+ 2. 再写执行约束(`prompt`)
122
+ 3. 避免只写关键词,不写动作规则
123
+
124
+ ### 4) 白名单和黑名单怎么配
125
+
126
+ - 全局白名单:`includeSkills`
127
+ - 全局黑名单:`excludeSkills`
128
+ - 规则级白名单倾向:`rules[*].preferSkills`
129
+ - 规则级黑名单:`rules[*].excludeSkills`
130
+
131
+ 示例:
132
+
133
+ ```json
134
+ {
135
+ "includeSkills": ["company-knowledge-base", "daily-report", "openclaw-feishu-plugin"],
136
+ "excludeSkills": ["remotion"],
137
+ "rules": [
138
+ {
139
+ "id": "daily-report",
140
+ "keywords": ["日报", "工时", "工作记录"],
141
+ "preferSkills": ["daily-report"],
142
+ "excludeSkills": ["company-knowledge-base"],
143
+ "prompt": "优先处理日报,不要误路由到知识库。"
144
+ }
145
+ ]
146
+ }
147
+ ```
148
+
149
+ 说明:
150
+
151
+ 1. `includeSkills` 不为空时,候选技能会先被限制在白名单内。
152
+ 2. `excludeSkills` 会在全局层面直接排除。
153
+ 3. 规则命中后会叠加 `preferSkills/excludeSkills` 做本轮精细路由。
154
+
155
+ ## 参数说明(完整)
156
+
157
+ ### A. `openclaw.json -> plugins.entries.skill-auto-loader-hook`
158
+
159
+ | 参数 | 类型 | 可选值/示例 | 默认值 | 作用 |
160
+ |---|---|---|---|---|
161
+ | `enabled` | boolean | `true/false` | `true` | 是否启用这个插件入口。 |
162
+ | `config.enabled` | boolean | `true/false` | `true` | 插件运行时开关。关闭后插件不做任何注入。 |
163
+ | `config.injectMode` | string | `prependContext` / `prependSystemContext` / `appendSystemContext` | `prependContext` | 选择把路由提示注入到哪里。 |
164
+ | `config.routerConfigPath` | string | `./skill-router.config.json` | `./skill-router.config.json` | 独立路由配置文件路径。相对路径按插件目录解析。 |
165
+ | `config.openclawConfigPath` | string | `~/.openclaw/openclaw.json` | `~/.openclaw/openclaw.json` | 读取默认模型信息的配置文件路径。 |
166
+
167
+ `injectMode` 选择建议:
168
+
169
+ 1. `prependContext`:推荐。影响最小,兼容性最好。
170
+ 2. `prependSystemContext`:约束最强,适合必须遵守路由的场景。
171
+ 3. `appendSystemContext`:权重相对弱,适合补充说明。
172
+
173
+ `injectMode` 详细解释(这三个参数到底什么意思):
174
+
175
+ 1. `prependContext`
176
+ - 含义:把插件生成的路由提示,放到“普通上下文”的前面。
177
+ - 特点:不会强压系统提示,兼容性最好,不容易和其他系统规则冲突。
178
+ - 适合:大多数场景,尤其是你只希望“引导模型优先用某些 skill”。
179
+
180
+ 2. `prependSystemContext`
181
+ - 含义:把路由提示放到“系统上下文”最前面。
182
+ - 特点:约束力最强,模型更容易优先执行这段规则。
183
+ - 风险:如果规则写得太死,可能压过原有系统策略,导致回答风格变硬。
184
+ - 适合:强流程场景(例如必须先知识库检索、必须二次确认写操作)。
185
+
186
+ 3. `appendSystemContext`
187
+ - 含义:把路由提示放到“系统上下文”末尾。
188
+ - 特点:仍在系统层,但通常权重低于前置系统上下文。
189
+ - 适合:你想补充一层路由提醒,但不想强覆盖原系统策略时使用。
190
+
191
+ ### B. `skill-router.config.json` 顶层参数
192
+
193
+ | 参数 | 类型 | 可选值/示例 | 默认值 | 作用 |
194
+ |---|---|---|---|---|
195
+ | `enabled` | boolean | `true/false` | `true` | 路由配置总开关。 |
196
+ | `defaultBehavior` | string | `no-op` / `advisory` | `no-op` | 未命中规则时是否注入提示。 |
197
+ | `skillsScanDirs` | string[] | `["~/.openclaw/workspace/skills"]` | 自动推断目录 | 扫描已安装 skill 的目录列表。 |
198
+ | `includeSkills` | string[] | `["company-knowledge-base"]` | `[]` | 全局白名单。非空时仅这些 skill 参与候选。 |
199
+ | `excludeSkills` | string[] | `["remotion"]` | `[]` | 全局黑名单。始终排除这些 skill。 |
200
+ | `maxSkillsInPrompt` | number | `10` / `20` | `20` | 注入提示中最多携带多少个候选 skill。 |
201
+ | `defaultScopeNote` | string | `"仅限公司知识库场景"` | 空 | 规则缺少 `scopeNote` 时的默认范围说明。 |
202
+ | `rules` | array | 见下方规则参数 | `[]` | 场景路由规则集合。 |
203
+
204
+ `defaultBehavior` 选择建议:
205
+
206
+ 1. `no-op`:推荐。未命中规则就不注入,避免“每轮追加”。
207
+ 2. `advisory`:未命中也注入轻量提示,适合想保留弱引导的场景。
208
+
209
+ ### C. `rules[*]` 规则参数
210
+
211
+ | 参数 | 类型 | 可选值/示例 | 是否必填 | 作用 |
212
+ |---|---|---|---|---|
213
+ | `id` | string | `"daily-report"` | 是 | 规则唯一标识,便于日志排障。 |
214
+ | `enabled` | boolean | `true/false` | 否 | 单条规则开关。 |
215
+ | `description` | string | `"日报相关路由"` | 否 | 规则说明文字。 |
216
+ | `keywords` | string[] | `["日报","工时"]` | 否 | 任意关键词命中即可。 |
217
+ | `allKeywords` | string[] | `["报销","流程"]` | 否 | 要求全部命中。 |
218
+ | `regexes` | string[] | `["(?i)日报.*提交"]` | 否 | 正则命中条件。 |
219
+ | `preferSkills` | string[] | `["daily-report"]` | 否 | 命中后优先的 skill。 |
220
+ | `excludeSkills` | string[] | `["company-knowledge-base"]` | 否 | 命中后排除的 skill。 |
221
+ | `scopeNote` | string | `"仅日报场景"` | 否 | 这条规则的适用范围说明。 |
222
+ | `prompt` | string | `"先确认草稿再上传"` | 否 | 规则级自定义提示词。 |
223
+
224
+ 规则命中逻辑(重要):
225
+
226
+ 1. `keywords`:满足“任意一个”即可。
227
+ 2. `allKeywords`:要求“全部”出现。
228
+ 3. `regexes`:任意一个正则匹配即可。
229
+ 4. 三类条件会组合判断,最终命中后才进入 `preferSkills/excludeSkills` 处理。
230
+
89
231
  ### 1. `openclaw.json` 中的插件入口配置
90
232
 
91
233
  ```json
package/index.ts CHANGED
@@ -3,6 +3,7 @@ import os from "node:os";
3
3
  import path from "node:path";
4
4
  import { definePluginEntry } from "openclaw/plugin-sdk/core";
5
5
 
6
+ // 插件固定 ID:对应 openclaw.json 里的 plugins.entries.<id>
6
7
  const PLUGIN_ID = "skill-auto-loader-hook";
7
8
 
8
9
  type RoutingRule = {
@@ -20,6 +21,7 @@ type RoutingRule = {
20
21
 
21
22
  type RouterFileConfig = {
22
23
  enabled?: boolean;
24
+ defaultBehavior?: "no-op" | "advisory";
23
25
  skillsScanDirs?: string[];
24
26
  includeSkills?: string[];
25
27
  excludeSkills?: string[];
@@ -41,10 +43,12 @@ type SkillMeta = {
41
43
  skillPath: string;
42
44
  };
43
45
 
46
+ // 工具函数:把任意输入安全转换为字符串
44
47
  function safeString(value: unknown): string {
45
48
  return typeof value === "string" ? value : "";
46
49
  }
47
50
 
51
+ // 工具函数:把 "~/" 展开为用户 home 目录绝对路径
48
52
  function expandHome(inputPath: string): string {
49
53
  if (!inputPath) {
50
54
  return inputPath;
@@ -55,6 +59,7 @@ function expandHome(inputPath: string): string {
55
59
  return inputPath;
56
60
  }
57
61
 
62
+ // 工具函数:把相对路径解析为基于 baseDir 的绝对路径
58
63
  function resolvePath(inputPath: string, baseDir: string): string {
59
64
  const expanded = expandHome(inputPath);
60
65
  if (!expanded) {
@@ -66,6 +71,7 @@ function resolvePath(inputPath: string, baseDir: string): string {
66
71
  return path.resolve(baseDir, expanded);
67
72
  }
68
73
 
74
+ // 安全读取 JSON:文件不存在或格式错误时返回 null,不抛异常
69
75
  function readJsonFile(filePath: string): any {
70
76
  try {
71
77
  const raw = fs.readFileSync(filePath, "utf8");
@@ -75,6 +81,7 @@ function readJsonFile(filePath: string): any {
75
81
  }
76
82
  }
77
83
 
84
+ // 从 OpenClaw 消息 content 结构中抽取纯文本
78
85
  function extractTextFromContent(content: unknown): string {
79
86
  if (typeof content === "string") {
80
87
  return content;
@@ -102,6 +109,7 @@ function extractTextFromContent(content: unknown): string {
102
109
  return "";
103
110
  }
104
111
 
112
+ // 提取“最后一条用户消息”作为本轮路由判断输入
105
113
  function extractLatestUserText(event: unknown): string {
106
114
  if (!event || typeof event !== "object") {
107
115
  return "";
@@ -128,6 +136,7 @@ function extractLatestUserText(event: unknown): string {
128
136
  return fallbackPrompt.trim();
129
137
  }
130
138
 
139
+ // 从 openclaw.json 读取插件级配置(plugins.entries.<id>.config)
131
140
  function normalizePluginConfig(api: any): PluginConfig {
132
141
  const pluginCfg =
133
142
  api?.config?.plugins?.entries?.[PLUGIN_ID]?.config ??
@@ -146,6 +155,7 @@ function normalizePluginConfig(api: any): PluginConfig {
146
155
  };
147
156
  }
148
157
 
158
+ // 从独立配置文件读取路由规则(skill-router.config.json)
149
159
  function readRouterConfig(configPath: string, baseDir: string): RouterFileConfig {
150
160
  const config = readJsonFile(resolvePath(configPath, baseDir));
151
161
  if (!config || typeof config !== "object") {
@@ -154,6 +164,7 @@ function readRouterConfig(configPath: string, baseDir: string): RouterFileConfig
154
164
  return config as RouterFileConfig;
155
165
  }
156
166
 
167
+ // 从 openclaw.json 读取默认模型信息,用于注入上下文
157
168
  function getDefaultModelInfo(openclawConfigPath: string): string {
158
169
  const cfg = readJsonFile(expandHome(openclawConfigPath));
159
170
  if (!cfg || typeof cfg !== "object") {
@@ -173,6 +184,7 @@ function getDefaultModelInfo(openclawConfigPath: string): string {
173
184
  return "openclaw.json 中未显式配置默认模型";
174
185
  }
175
186
 
187
+ // 解析 SKILL.md 头部 frontmatter(name/description)
176
188
  function parseYamlLikeFrontmatter(raw: string): { name: string; description: string } {
177
189
  const result = { name: "", description: "" };
178
190
  if (!raw.startsWith("---")) {
@@ -216,6 +228,7 @@ function parseYamlLikeFrontmatter(raw: string): { name: string; description: str
216
228
  return result;
217
229
  }
218
230
 
231
+ // frontmatter 缺失时,退化为读取 markdown 一级标题作为名称
219
232
  function extractHeadingFallback(raw: string): string {
220
233
  const lines = raw.split(/\r?\n/);
221
234
  for (const line of lines) {
@@ -226,6 +239,7 @@ function extractHeadingFallback(raw: string): string {
226
239
  return "";
227
240
  }
228
241
 
242
+ // 从一个 skill 目录提取技能元信息
229
243
  function extractSkillMeta(skillDir: string): SkillMeta | null {
230
244
  const skillMdPath = path.join(skillDir, "SKILL.md");
231
245
  if (!fs.existsSync(skillMdPath)) {
@@ -244,6 +258,7 @@ function extractSkillMeta(skillDir: string): SkillMeta | null {
244
258
  };
245
259
  }
246
260
 
261
+ // 扫描配置的技能目录,收集“已安装 skill”列表
247
262
  function scanInstalledSkills(scanDirs: string[]): SkillMeta[] {
248
263
  const results: SkillMeta[] = [];
249
264
  const visited = new Set<string>();
@@ -274,6 +289,7 @@ function scanInstalledSkills(scanDirs: string[]): SkillMeta[] {
274
289
  return results.sort((a, b) => a.name.localeCompare(b.name));
275
290
  }
276
291
 
292
+ // 规则匹配器:支持 keywords / allKeywords / regexes
277
293
  function matchesRule(text: string, rule: RoutingRule): boolean {
278
294
  const normalized = text.toLowerCase();
279
295
  const keywords = Array.isArray(rule.keywords) ? rule.keywords : [];
@@ -302,6 +318,7 @@ function matchesRule(text: string, rule: RoutingRule): boolean {
302
318
  return hasMatcher && anyKeywordMatched && allKeywordMatched && regexMatched;
303
319
  }
304
320
 
321
+ // 先应用全局白名单/黑名单,再应用规则级偏好与排除
305
322
  function applyRuleFiltering(skills: SkillMeta[], routerCfg: RouterFileConfig, matchedRules: RoutingRule[]): SkillMeta[] {
306
323
  let filtered = [...skills];
307
324
 
@@ -334,15 +351,15 @@ function applyRuleFiltering(skills: SkillMeta[], routerCfg: RouterFileConfig, ma
334
351
  return filtered.slice(0, maxSkills);
335
352
  }
336
353
 
354
+ // 生成精简版技能列表,避免注入内容过长
337
355
  function renderSkillList(skills: SkillMeta[]): string {
338
356
  if (skills.length === 0) {
339
357
  return "当前未扫描到可用 skill。";
340
358
  }
341
- return skills
342
- .map((skill, index) => `${index + 1}. ${skill.name}\n说明:${skill.description}\n路径:${skill.skillPath}`)
343
- .join("\n\n");
359
+ return skills.map((skill, index) => `${index + 1}. ${skill.name}(${skill.description.slice(0, 80)})`).join("\n");
344
360
  }
345
361
 
362
+ // 渲染命中规则说明,供模型做本轮技能路由判断
346
363
  function renderRuleHints(matchedRules: RoutingRule[], routerCfg: RouterFileConfig): string {
347
364
  if (matchedRules.length === 0) {
348
365
  return routerCfg.defaultScopeNote ? `默认范围提示:${routerCfg.defaultScopeNote}` : "未命中任何自定义规则。";
@@ -372,6 +389,7 @@ function renderRuleHints(matchedRules: RoutingRule[], routerCfg: RouterFileConfi
372
389
  .join("\n\n");
373
390
  }
374
391
 
392
+ // 构造最终注入 payload(用于 before_prompt_build)
375
393
  function buildRoutingPrompt(
376
394
  userText: string,
377
395
  defaultModel: string,
@@ -386,7 +404,7 @@ function buildRoutingPrompt(
386
404
  lines.push("");
387
405
  lines.push("请结合以下信息判断本轮是否需要优先使用某个 skill。");
388
406
  lines.push("");
389
- lines.push("一、当前可用 skill 列表");
407
+ lines.push("一、候选 skill(已过滤)");
390
408
  lines.push(renderSkillList(scannedSkills));
391
409
  lines.push("");
392
410
  lines.push("二、本轮命中的自定义规则");
@@ -413,6 +431,7 @@ export default definePluginEntry({
413
431
  api.on(
414
432
  "before_prompt_build",
415
433
  (event: unknown) => {
434
+ // 步骤 1:读取插件配置与独立路由配置
416
435
  const cfg = normalizePluginConfig(api);
417
436
  if (!cfg.enabled) {
418
437
  return undefined;
@@ -423,11 +442,13 @@ export default definePluginEntry({
423
442
  return undefined;
424
443
  }
425
444
 
445
+ // 步骤 2:提取本轮用户输入
426
446
  const userText = extractLatestUserText(event);
427
447
  if (!userText) {
428
448
  return undefined;
429
449
  }
430
450
 
451
+ // 步骤 3:扫描并收集可用 skill
431
452
  const scanDirs =
432
453
  Array.isArray(routerCfg.skillsScanDirs) && routerCfg.skillsScanDirs.length > 0
433
454
  ? routerCfg.skillsScanDirs.map((dir) => resolvePath(dir, __dirname))
@@ -435,6 +456,12 @@ export default definePluginEntry({
435
456
 
436
457
  const allSkills = scanInstalledSkills(scanDirs);
437
458
  const matchedRules = (routerCfg.rules || []).filter((rule) => rule.enabled !== false && matchesRule(userText, rule));
459
+ const defaultBehavior = routerCfg.defaultBehavior || "no-op";
460
+ // 步骤 4:默认 no-op 时,未命中规则就不注入(避免每轮追加)
461
+ if (matchedRules.length === 0 && defaultBehavior === "no-op") {
462
+ return undefined;
463
+ }
464
+ // 步骤 5:过滤候选 skill,并构造注入内容
438
465
  const candidateSkills = applyRuleFiltering(allSkills, routerCfg, matchedRules);
439
466
  const defaultModel = getDefaultModelInfo(cfg.openclawConfigPath!);
440
467
  const payload = buildRoutingPrompt(userText, defaultModel, candidateSkills, matchedRules, routerCfg);
@@ -443,12 +470,18 @@ export default definePluginEntry({
443
470
  `skill-auto-loader-hook: before_prompt_build matchedRules=${matchedRules.length} candidateSkills=${candidateSkills.length}`,
444
471
  );
445
472
 
473
+ // injectMode = prependSystemContext:
474
+ // 注入到系统上下文最前面,约束最强,优先级通常最高。
446
475
  if (cfg.injectMode === "prependSystemContext") {
447
476
  return { prependSystemContext: payload };
448
477
  }
478
+ // injectMode = appendSystemContext:
479
+ // 注入到系统上下文末尾,仍属于系统层,但权重通常弱于前置系统上下文。
449
480
  if (cfg.injectMode === "appendSystemContext") {
450
481
  return { appendSystemContext: payload };
451
482
  }
483
+ // injectMode = prependContext(默认):
484
+ // 注入到普通上下文前部,影响适中,兼容性最好,推荐默认使用。
452
485
  return { prependContext: payload };
453
486
  },
454
487
  { priority: 50 },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "skill-auto-loader-hook-paperfly777",
3
- "version": "0.1.2",
3
+ "version": "0.1.3",
4
4
  "description": "OpenClaw hook plugin that routes each user message to the most relevant installed skill.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -1,5 +1,6 @@
1
1
  {
2
2
  "enabled": true,
3
+ "defaultBehavior": "no-op",
3
4
  "skillsScanDirs": [
4
5
  "./skills",
5
6
  "./openclaw-skills/skills",