skill-auto-loader-hook-paperfly777 0.1.8 → 0.1.10

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.
Files changed (2) hide show
  1. package/index.ts +106 -38
  2. package/package.json +1 -1
package/index.ts CHANGED
@@ -65,6 +65,13 @@ function safeString(value: unknown): string {
65
65
  return typeof value === "string" ? value : "";
66
66
  }
67
67
 
68
+ function shortText(text: string, max = 120): string {
69
+ if (!text) {
70
+ return "";
71
+ }
72
+ return text.length > max ? `${text.slice(0, max)}...` : text;
73
+ }
74
+
68
75
  // 工具函数:把 "~/" 展开为用户 home 目录绝对路径
69
76
  function expandHome(inputPath: string): string {
70
77
  if (!inputPath) {
@@ -374,49 +381,71 @@ async function callDecisionModel(
374
381
 
375
382
  let text = "";
376
383
 
377
- if (modelCfg.apiType === "openai-completions") {
378
- const response = await fetch(`${baseUrl}/chat/completions`, {
379
- method: "POST",
380
- headers,
381
- body: JSON.stringify({
382
- model: modelCfg.modelId,
383
- temperature: 0,
384
- max_tokens: 300,
385
- response_format: { type: "json_object" },
386
- messages: [
387
- { role: "system", content: system },
388
- { role: "user", content: user },
389
- ],
390
- }),
391
- });
392
- const data = (await response.json()) as any;
393
- text = safeString(data?.choices?.[0]?.message?.content);
394
- } else if (modelCfg.apiType === "openai-responses") {
395
- const response = await fetch(`${baseUrl}/responses`, {
396
- method: "POST",
397
- headers,
398
- body: JSON.stringify({
399
- model: modelCfg.modelId,
400
- temperature: 0,
401
- max_output_tokens: 300,
402
- input: [
403
- { role: "system", content: [{ type: "input_text", text: system }] },
404
- { role: "user", content: [{ type: "input_text", text: user }] },
405
- ],
406
- }),
407
- });
408
- const data = (await response.json()) as any;
409
- text =
410
- safeString(data?.output_text) ||
411
- safeString(data?.output?.[0]?.content?.[0]?.text) ||
412
- safeString(data?.choices?.[0]?.message?.content);
413
- } else {
384
+ try {
385
+ if (modelCfg.apiType === "openai-completions") {
386
+ const response = await fetch(`${baseUrl}/chat/completions`, {
387
+ method: "POST",
388
+ headers,
389
+ body: JSON.stringify({
390
+ model: modelCfg.modelId,
391
+ temperature: 0,
392
+ max_tokens: 300,
393
+ response_format: { type: "json_object" },
394
+ messages: [
395
+ { role: "system", content: system },
396
+ { role: "user", content: user },
397
+ ],
398
+ }),
399
+ });
400
+ const data = (await response.json()) as any;
401
+ text = safeString(data?.choices?.[0]?.message?.content);
402
+ } else if (modelCfg.apiType === "openai-responses") {
403
+ const response = await fetch(`${baseUrl}/responses`, {
404
+ method: "POST",
405
+ headers,
406
+ body: JSON.stringify({
407
+ model: modelCfg.modelId,
408
+ temperature: 0,
409
+ max_output_tokens: 300,
410
+ input: [
411
+ { role: "system", content: [{ type: "input_text", text: system }] },
412
+ { role: "user", content: [{ type: "input_text", text: user }] },
413
+ ],
414
+ }),
415
+ });
416
+ const data = (await response.json()) as any;
417
+ text =
418
+ safeString(data?.output_text) ||
419
+ safeString(data?.output?.[0]?.content?.[0]?.text) ||
420
+ safeString(data?.choices?.[0]?.message?.content);
421
+ } else {
422
+ return null;
423
+ }
424
+ } catch {
414
425
  return null;
415
426
  }
416
427
 
417
428
  return parseDecisionJson(text, candidateSkills);
418
429
  }
419
430
 
431
+ function fallbackDecisionFromRules(candidateSkills: SkillMeta[], matchedRules: RoutingRule[]): ModelDecision | null {
432
+ const preferred = new Set<string>();
433
+ for (const rule of matchedRules) {
434
+ (rule.preferSkills || []).forEach((name) => preferred.add(name));
435
+ }
436
+
437
+ const selected = candidateSkills.filter((skill) => preferred.has(skill.name)).slice(0, 3);
438
+ if (selected.length === 0) {
439
+ return null;
440
+ }
441
+
442
+ return {
443
+ use_skill: true,
444
+ selected_skills: selected.map((skill) => skill.name),
445
+ reason: "模型路由未返回有效结果,已回退到规则 preferSkills。",
446
+ };
447
+ }
448
+
420
449
  // 解析 SKILL.md 头部 frontmatter(name/description)
421
450
  function parseYamlLikeFrontmatter(raw: string): { name: string; description: string } {
422
451
  const result = { name: "", description: "" };
@@ -661,19 +690,25 @@ export default definePluginEntry({
661
690
  // 步骤 1:读取插件配置与独立路由配置
662
691
  const cfg = normalizePluginConfig(api);
663
692
  if (!cfg.enabled) {
693
+ api.logger?.debug?.("skill-auto-loader-hook: plugin disabled, skip");
664
694
  return undefined;
665
695
  }
666
696
 
667
697
  const routerCfg = readRouterConfig(cfg.routerConfigPath!, __dirname);
668
698
  if (routerCfg.enabled === false) {
699
+ api.logger?.debug?.("skill-auto-loader-hook: router config disabled, skip");
669
700
  return undefined;
670
701
  }
671
702
 
672
703
  // 步骤 2:提取本轮用户输入
673
704
  const userText = extractLatestUserText(event);
674
705
  if (!userText) {
706
+ api.logger?.debug?.("skill-auto-loader-hook: no user text extracted, skip");
675
707
  return undefined;
676
708
  }
709
+ if (routerCfg.logRouting) {
710
+ api.logger?.info?.(`skill-auto-loader-hook: extracted userText="${shortText(userText)}"`);
711
+ }
677
712
 
678
713
  // 步骤 3:扫描并收集可用 skill
679
714
  const scanDirs =
@@ -684,19 +719,52 @@ export default definePluginEntry({
684
719
  const allSkills = scanInstalledSkills(scanDirs);
685
720
  const matchedRules = (routerCfg.rules || []).filter((rule) => rule.enabled !== false && matchesRule(userText, rule));
686
721
  const defaultBehavior = routerCfg.defaultBehavior || "no-op";
722
+ if (routerCfg.logRouting) {
723
+ const ruleIds = matchedRules.map((rule) => rule.id).join(",") || "none";
724
+ api.logger?.info?.(`skill-auto-loader-hook: matchedRules=${ruleIds} defaultBehavior=${defaultBehavior}`);
725
+ }
687
726
  // 步骤 4:默认 no-op 时,未命中规则就不注入(避免每轮追加)
688
727
  if (matchedRules.length === 0 && defaultBehavior === "no-op") {
728
+ api.logger?.debug?.("skill-auto-loader-hook: no rules matched and defaultBehavior=no-op, skip");
689
729
  return undefined;
690
730
  }
691
731
  // 步骤 5:先根据规则收缩候选 skill 范围
692
732
  const candidateSkills = applyRuleFiltering(allSkills, routerCfg, matchedRules);
733
+ if (routerCfg.logRouting) {
734
+ api.logger?.info?.(
735
+ `skill-auto-loader-hook: candidateSkills=${candidateSkills.map((skill) => skill.name).join(",") || "none"}`,
736
+ );
737
+ }
693
738
  if (candidateSkills.length === 0) {
739
+ api.logger?.debug?.("skill-auto-loader-hook: no candidate skills after filtering, skip");
694
740
  return undefined;
695
741
  }
696
742
 
697
743
  // 步骤 6:主动调用一次默认大模型,让模型在候选 skill 中做最终路由判断
698
- const decision = await callDecisionModel(cfg.openclawConfigPath!, userText, candidateSkills, matchedRules, routerCfg);
744
+ if (routerCfg.logRouting) {
745
+ api.logger?.info?.("skill-auto-loader-hook: calling decision model");
746
+ }
747
+ let decision = await callDecisionModel(cfg.openclawConfigPath!, userText, candidateSkills, matchedRules, routerCfg);
748
+ if (routerCfg.logRouting) {
749
+ api.logger?.info?.(
750
+ `skill-auto-loader-hook: decisionModelResult=${decision ? JSON.stringify(decision) : "null"}`,
751
+ );
752
+ }
753
+ if ((!decision || !decision.use_skill || decision.selected_skills.length === 0) && matchedRules.length > 0) {
754
+ if (routerCfg.logRouting) {
755
+ api.logger?.info?.("skill-auto-loader-hook: model decision empty, fallback to preferSkills");
756
+ }
757
+ decision = fallbackDecisionFromRules(candidateSkills, matchedRules);
758
+ if (routerCfg.logRouting) {
759
+ api.logger?.info?.(
760
+ `skill-auto-loader-hook: fallbackDecision=${decision ? JSON.stringify(decision) : "null"}`,
761
+ );
762
+ }
763
+ }
699
764
  if (!decision || !decision.use_skill || decision.selected_skills.length === 0) {
765
+ if (routerCfg.logRouting) {
766
+ api.logger?.info?.("skill-auto-loader-hook: no valid decision returned by model and no fallback applied");
767
+ }
700
768
  return undefined;
701
769
  }
702
770
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "skill-auto-loader-hook-paperfly777",
3
- "version": "0.1.8",
3
+ "version": "0.1.10",
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",