@templmf/temp-solf-lmf 0.0.43 → 0.0.45
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/ai-gateway/.env +42 -0
- package/ai-gateway/README.md +295 -0
- package/ai-gateway/package-lock.json +1370 -0
- package/ai-gateway/package.json +18 -0
- package/ai-gateway/src/index.js +132 -0
- package/ai-gateway/src/middleware/auth.js +45 -0
- package/ai-gateway/src/middleware/rateLimit.js +87 -0
- package/ai-gateway/src/routes/chat.js +657 -0
- package/ai-gateway/src/skills/detector.js +145 -0
- package/ai-gateway/src/skills/html.md +18 -0
- package/ai-gateway/src/skills/markdown.md +18 -0
- package/ai-gateway/src/skills/react.md +27 -0
- package/ai-gateway/src/skills/registry.js +441 -0
- package/ai-gateway/src/skills/skill-creator/LICENSE.txt +202 -0
- package/ai-gateway/src/skills/skill-creator/SKILL.md +485 -0
- package/ai-gateway/src/skills/skill-creator/agents/analyzer.md +274 -0
- package/ai-gateway/src/skills/skill-creator/agents/comparator.md +202 -0
- package/ai-gateway/src/skills/skill-creator/agents/grader.md +223 -0
- package/ai-gateway/src/skills/skill-creator/assets/eval_review.html +146 -0
- package/ai-gateway/src/skills/skill-creator/eval-viewer/generate_review.py +471 -0
- package/ai-gateway/src/skills/skill-creator/eval-viewer/viewer.html +1325 -0
- package/ai-gateway/src/skills/skill-creator/references/schemas.md +430 -0
- package/ai-gateway/src/skills/skill-creator/scripts/__init__.py +0 -0
- package/ai-gateway/src/skills/skill-creator/scripts/aggregate_benchmark.py +401 -0
- package/ai-gateway/src/skills/skill-creator/scripts/generate_report.py +326 -0
- package/ai-gateway/src/skills/skill-creator/scripts/improve_description.py +247 -0
- package/ai-gateway/src/skills/skill-creator/scripts/package_skill.py +136 -0
- package/ai-gateway/src/skills/skill-creator/scripts/quick_validate.py +103 -0
- package/ai-gateway/src/skills/skill-creator/scripts/run_eval.py +310 -0
- package/ai-gateway/src/skills/skill-creator/scripts/run_loop.py +328 -0
- package/ai-gateway/src/skills/skill-creator/scripts/utils.py +47 -0
- package/ai-gateway/src/skills/skill-creator/skill-creator.skill +0 -0
- package/ai-gateway/src/skills/ticket.md +36 -0
- package/ai-gateway/src/skills/vue.md +31 -0
- package/ai-gateway/src/utils/logger.js +21 -0
- package/ai-gateway/src/utils/retry.js +90 -0
- package/ai-gateway/src/utils/sessionManager.js +159 -0
- package/ai-gateway/src/utils/structuredResponse.js +144 -0
- package/ai-gateway/src/utils/toolAdapter.js +151 -0
- package/package.json +1 -1
- package//345/216/213/347/274/251/345/220/216/347/232/204/346/226/207/344/273/266.7z +0 -0
- package/skill-mcp/README.md +0 -74
- package/skill-mcp/index.ts +0 -336
- package/skill-mcp/package (1).json +0 -19
- package/skill-mcp/tsconfig.json +0 -16
- package//347/247/273/345/212/250/345/272/224/347/224/250/345/217/260/350/264/246/347/247/273/344/272/244/346/270/205/345/215/225.xlsx +0 -0
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AI 驱动的 Skill 检测器
|
|
3
|
+
*
|
|
4
|
+
* 替代原来的关键词匹配,用轻量模型做语义分类:
|
|
5
|
+
* 1. 把所有已加载的 Skill 列表(name + description)发给模型
|
|
6
|
+
* 2. 模型返回命中的 Skill 名称数组(JSON)
|
|
7
|
+
* 3. 结果缓存 30 秒,避免每次请求都多一次模型调用
|
|
8
|
+
*
|
|
9
|
+
* 降级策略:模型调用失败时自动 fallback 到关键词匹配
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import OpenAI from "openai";
|
|
13
|
+
import { logger } from "../utils/logger.js";
|
|
14
|
+
|
|
15
|
+
// ─────────────────────────────────────────────────────
|
|
16
|
+
// 轻量检测用的模型客户端(和主客户端共享配置)
|
|
17
|
+
// ─────────────────────────────────────────────────────
|
|
18
|
+
let detectorClient = null;
|
|
19
|
+
|
|
20
|
+
export function initDetector() {
|
|
21
|
+
detectorClient = new OpenAI({
|
|
22
|
+
apiKey: process.env.UPSTREAM_API_KEY || "none",
|
|
23
|
+
baseURL: process.env.UPSTREAM_BASE_URL || "http://localhost:8000/v1",
|
|
24
|
+
timeout: 10_000, // 检测超时短一点,快速失败
|
|
25
|
+
maxRetries: 0
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// ─────────────────────────────────────────────────────
|
|
30
|
+
// 简单 LRU 缓存(避免同一会话重复检测)
|
|
31
|
+
// key = 最近 2 条消息的文本摘要,TTL = 30s
|
|
32
|
+
// ─────────────────────────────────────────────────────
|
|
33
|
+
const cache = new Map();
|
|
34
|
+
const CACHE_TTL = 30_000;
|
|
35
|
+
|
|
36
|
+
function getCacheKey(messages) {
|
|
37
|
+
return messages
|
|
38
|
+
.slice(-2)
|
|
39
|
+
.map(m => (typeof m.content === "string" ? m.content : "").slice(0, 80))
|
|
40
|
+
.join("|");
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// ─────────────────────────────────────────────────────
|
|
44
|
+
// AI 驱动的 Skill 检测(主路径)
|
|
45
|
+
// ─────────────────────────────────────────────────────
|
|
46
|
+
export async function detectSkillsWithAI(messages, skillDefinitions, loadedSkills) {
|
|
47
|
+
if (!detectorClient) return null;
|
|
48
|
+
|
|
49
|
+
// 只有已加载的 Skill 才参与检测
|
|
50
|
+
const available = Object.entries(skillDefinitions)
|
|
51
|
+
.filter(([name]) => loadedSkills[name])
|
|
52
|
+
.map(([name, def]) => ({
|
|
53
|
+
name,
|
|
54
|
+
description: def.description,
|
|
55
|
+
// 把关键词也给模型做参考,但不作为唯一判断依据
|
|
56
|
+
hints: def.keywords?.slice(0, 5).join(", ")
|
|
57
|
+
}));
|
|
58
|
+
|
|
59
|
+
if (available.length === 0) return [];
|
|
60
|
+
|
|
61
|
+
// 缓存命中
|
|
62
|
+
const cacheKey = getCacheKey(messages);
|
|
63
|
+
const cached = cache.get(cacheKey);
|
|
64
|
+
if (cached && Date.now() - cached.ts < CACHE_TTL) {
|
|
65
|
+
logger.debug("Skill detection cache hit", { key: cacheKey, skills: cached.skills });
|
|
66
|
+
return cached.skills;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// 取最近 3 条消息做上下文
|
|
70
|
+
const recentMessages = messages.slice(-3).map(m => ({
|
|
71
|
+
role: m.role,
|
|
72
|
+
content: typeof m.content === "string"
|
|
73
|
+
? m.content.slice(0, 300) // 截断避免 token 浪费
|
|
74
|
+
: "[非文本内容]"
|
|
75
|
+
}));
|
|
76
|
+
|
|
77
|
+
// 构建检测 prompt
|
|
78
|
+
const skillList = available
|
|
79
|
+
.map(s => `- ${s.name}:${s.description}${s.hints ? `(关键词:${s.hints})` : ""}`)
|
|
80
|
+
.join("\n");
|
|
81
|
+
|
|
82
|
+
const detectionPrompt = `你是一个任务分类器。根据用户的对话内容,从下面的 Skill 列表中选出需要激活的 Skill。
|
|
83
|
+
|
|
84
|
+
## 可用 Skill 列表
|
|
85
|
+
${skillList}
|
|
86
|
+
|
|
87
|
+
## 用户最近的对话
|
|
88
|
+
${recentMessages.map(m => `[${m.role}]: ${m.content}`).join("\n")}
|
|
89
|
+
|
|
90
|
+
## 要求
|
|
91
|
+
- 只返回 JSON 数组,包含需要激活的 Skill name
|
|
92
|
+
- 如果不需要任何 Skill,返回空数组 []
|
|
93
|
+
- 不要返回任何解释文字
|
|
94
|
+
- 示例:["react", "ticket"] 或 []`;
|
|
95
|
+
|
|
96
|
+
try {
|
|
97
|
+
const resp = await detectorClient.chat.completions.create({
|
|
98
|
+
model: process.env.DETECTOR_MODEL || process.env.DEFAULT_MODEL || "qwen2.5-7b-instruct",
|
|
99
|
+
max_tokens: 64,
|
|
100
|
+
temperature: 0, // 分类任务用确定性输出
|
|
101
|
+
messages: [
|
|
102
|
+
{ role: "system", content: "你是一个 JSON 输出机器人,只输出 JSON,不输出任何其他内容。" },
|
|
103
|
+
{ role: "user", content: detectionPrompt }
|
|
104
|
+
]
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
const raw = resp.choices[0]?.message?.content?.trim() || "[]";
|
|
108
|
+
|
|
109
|
+
// 解析 JSON,容错处理
|
|
110
|
+
let skillNames = [];
|
|
111
|
+
try {
|
|
112
|
+
// 去掉可能的代码块包裹
|
|
113
|
+
const cleaned = raw.replace(/```json|```/g, "").trim();
|
|
114
|
+
skillNames = JSON.parse(cleaned);
|
|
115
|
+
if (!Array.isArray(skillNames)) skillNames = [];
|
|
116
|
+
} catch {
|
|
117
|
+
logger.warn("Skill detection parse failed, raw:", { raw });
|
|
118
|
+
return null; // 解析失败,触发 fallback
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// 过滤掉不存在的 Skill 名称(防止模型幻觉)
|
|
122
|
+
const valid = skillNames.filter(n => loadedSkills[n]);
|
|
123
|
+
|
|
124
|
+
logger.debug("AI skill detection", { detected: valid, raw });
|
|
125
|
+
|
|
126
|
+
// 写入缓存
|
|
127
|
+
cache.set(cacheKey, { skills: valid, ts: Date.now() });
|
|
128
|
+
|
|
129
|
+
return valid;
|
|
130
|
+
|
|
131
|
+
} catch (err) {
|
|
132
|
+
logger.warn("Skill detection failed, fallback to keyword match", { error: err.message });
|
|
133
|
+
return null; // 返回 null 触发 fallback
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// ─────────────────────────────────────────────────────
|
|
138
|
+
// 定期清理过期缓存
|
|
139
|
+
// ─────────────────────────────────────────────────────
|
|
140
|
+
setInterval(() => {
|
|
141
|
+
const now = Date.now();
|
|
142
|
+
for (const [k, v] of cache.entries()) {
|
|
143
|
+
if (now - v.ts > CACHE_TTL) cache.delete(k);
|
|
144
|
+
}
|
|
145
|
+
}, 60_000);
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# HTML 生成规范
|
|
2
|
+
|
|
3
|
+
生成语义化、可直接渲染的 HTML 片段。
|
|
4
|
+
|
|
5
|
+
## 要求
|
|
6
|
+
- 使用语义化标签(header/main/section/article/nav/footer)
|
|
7
|
+
- 内联 CSS 样式(不依赖外部 CSS 文件)
|
|
8
|
+
- 响应式:使用 flexbox 或 grid 布局
|
|
9
|
+
- 禁止 <script> 标签(安全限制)
|
|
10
|
+
- 禁止外部资源引用(img src 除外)
|
|
11
|
+
|
|
12
|
+
## 样式规范
|
|
13
|
+
- 字体:system-ui, sans-serif
|
|
14
|
+
- 颜色:使用 CSS 变量或 HSL 颜色
|
|
15
|
+
- 间距:使用 rem 单位
|
|
16
|
+
|
|
17
|
+
## 输出
|
|
18
|
+
直接输出 HTML 代码,不要用 Markdown 代码块包裹。
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# Markdown 生成规范
|
|
2
|
+
|
|
3
|
+
生成标准 GitHub Flavored Markdown(GFM)文档。
|
|
4
|
+
|
|
5
|
+
## 格式要求
|
|
6
|
+
- 使用 ATX 风格标题(# ## ###)
|
|
7
|
+
- 代码块必须标注语言(```javascript)
|
|
8
|
+
- 表格使用 GFM 表格语法
|
|
9
|
+
- 列表使用 - 而非 *
|
|
10
|
+
- 数学公式使用 $...$ 行内或 $$...$$ 块级
|
|
11
|
+
|
|
12
|
+
## 文档结构
|
|
13
|
+
技术文档:标题 → 概述 → 详细内容 → 示例 → 注意事项
|
|
14
|
+
报告类:执行摘要 → 背景 → 分析 → 结论 → 建议
|
|
15
|
+
|
|
16
|
+
## 禁止事项
|
|
17
|
+
- 不要输出 HTML 标签
|
|
18
|
+
- 不要在代码块外使用反引号
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# React 组件生成规范
|
|
2
|
+
|
|
3
|
+
生成 React 18 函数式组件。
|
|
4
|
+
|
|
5
|
+
## 规范
|
|
6
|
+
- 使用函数式组件 + Hooks(useState, useEffect, useMemo 等)
|
|
7
|
+
- 默认使用 TypeScript(.tsx)
|
|
8
|
+
- Props 必须定义 interface
|
|
9
|
+
- 样式优先使用 Tailwind className,无 Tailwind 时用 CSS-in-JS 对象
|
|
10
|
+
|
|
11
|
+
## 组件结构
|
|
12
|
+
```tsx
|
|
13
|
+
interface Props {
|
|
14
|
+
// prop 定义
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export default function ComponentName({ ...props }: Props) {
|
|
18
|
+
// hooks
|
|
19
|
+
// handlers
|
|
20
|
+
return (/* JSX */);
|
|
21
|
+
}
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## 禁止
|
|
25
|
+
- 不要使用 class 组件
|
|
26
|
+
- 不要直接操作 DOM(除非封装为 ref)
|
|
27
|
+
- 不要在渲染函数中做副作用
|
|
@@ -0,0 +1,441 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Skill 注册表与加载器 v2
|
|
3
|
+
*
|
|
4
|
+
* 支持两种 Skill 类型:
|
|
5
|
+
*
|
|
6
|
+
* 单文件 Skill(simple)
|
|
7
|
+
* src/skills/react.md
|
|
8
|
+
* 加载时直接读取文件内容注入 system prompt。
|
|
9
|
+
*
|
|
10
|
+
* 目录型 Skill(directory)
|
|
11
|
+
* src/skills/skill-creator/
|
|
12
|
+
* SKILL.md ← 入口,必须存在
|
|
13
|
+
* agents/ ← 子 agent 指令
|
|
14
|
+
* references/ ← 参考资料
|
|
15
|
+
* scripts/ ← 可执行脚本(不注入 prompt,按需引用路径)
|
|
16
|
+
* ...
|
|
17
|
+
*
|
|
18
|
+
* 目录型 Skill 的加载策略:
|
|
19
|
+
* - SKILL.md 内容始终注入 system prompt(主指令)
|
|
20
|
+
* - agents/*.md、references/*.md 等文本子文件作为"可引用资源"
|
|
21
|
+
* 注册到 system prompt 的资源索引里,模型按需通过工具读取
|
|
22
|
+
* - scripts/ 目录不注入内容,只告知模型路径,由模型决定是否调用
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
import fs from "fs";
|
|
26
|
+
import path from "path";
|
|
27
|
+
import { fileURLToPath } from "url";
|
|
28
|
+
|
|
29
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
30
|
+
|
|
31
|
+
// ─────────────────────────────────────────────────────
|
|
32
|
+
// Skill 定义表
|
|
33
|
+
// ─────────────────────────────────────────────────────
|
|
34
|
+
const SKILL_DEFINITIONS = {
|
|
35
|
+
|
|
36
|
+
// ── 单文件 Skill ────────────────────────────────────
|
|
37
|
+
ticket: {
|
|
38
|
+
type: "simple",
|
|
39
|
+
description: "AI 驱动式提单/工单助手",
|
|
40
|
+
keywords: ["提单", "工单", "故障单", "需求单", "issue", "ticket", "bug"],
|
|
41
|
+
file: "ticket.md"
|
|
42
|
+
},
|
|
43
|
+
markdown: {
|
|
44
|
+
type: "simple",
|
|
45
|
+
description: "生成或渲染 Markdown 文档",
|
|
46
|
+
keywords: ["markdown", "md", "渲染", "文档生成", "readme", "笔记"],
|
|
47
|
+
file: "markdown.md"
|
|
48
|
+
},
|
|
49
|
+
html: {
|
|
50
|
+
type: "simple",
|
|
51
|
+
description: "生成或渲染 HTML 页面/组件",
|
|
52
|
+
keywords: ["html", "网页", "页面", "渲染"],
|
|
53
|
+
file: "html.md"
|
|
54
|
+
},
|
|
55
|
+
react: {
|
|
56
|
+
type: "simple",
|
|
57
|
+
description: "生成 React 组件代码",
|
|
58
|
+
keywords: ["react", "jsx", "tsx", "hooks", "next.js"],
|
|
59
|
+
file: "react.md"
|
|
60
|
+
},
|
|
61
|
+
vue: {
|
|
62
|
+
type: "simple",
|
|
63
|
+
description: "生成 Vue 3 单文件组件",
|
|
64
|
+
keywords: ["vue", "sfc", "composition api", "options api", "nuxt"],
|
|
65
|
+
file: "vue.md"
|
|
66
|
+
},
|
|
67
|
+
docx: {
|
|
68
|
+
type: "simple",
|
|
69
|
+
description: "创建或编辑 Word (.docx) 文档",
|
|
70
|
+
keywords: ["word", "文档", "docx", ".doc", "报告", "合同", "简历"],
|
|
71
|
+
file: "docx.md"
|
|
72
|
+
},
|
|
73
|
+
pptx: {
|
|
74
|
+
type: "simple",
|
|
75
|
+
description: "创建演示文稿 (.pptx)",
|
|
76
|
+
keywords: ["ppt", "pptx", "幻灯片", "演示", "presentation"],
|
|
77
|
+
file: "pptx.md"
|
|
78
|
+
},
|
|
79
|
+
xlsx: {
|
|
80
|
+
type: "simple",
|
|
81
|
+
description: "创建或处理电子表格 (.xlsx)",
|
|
82
|
+
keywords: ["excel", "xlsx", "表格", "spreadsheet", "数据统计"],
|
|
83
|
+
file: "xlsx.md"
|
|
84
|
+
},
|
|
85
|
+
|
|
86
|
+
// ── 目录型 Skill ────────────────────────────────────
|
|
87
|
+
"skill-creator": {
|
|
88
|
+
type: "directory",
|
|
89
|
+
// description 是 AI 检测模式的主要判断依据,要足够明确
|
|
90
|
+
description: "创建、编写、改进或评测任何类型的 Skill 文档(SKILL.md)。只要用户提到要'写/做/建/创建一个skill'、'skill文档'、'技能文档',无论 skill 是关于什么领域的,都应使用此条目。",
|
|
91
|
+
keywords: [
|
|
92
|
+
// 精确单词命中
|
|
93
|
+
"skill creator", "skill.md", "SKILL.md",
|
|
94
|
+
"创建skill", "写skill", "做skill", "建skill",
|
|
95
|
+
"制作技能", "评测skill", "改进skill",
|
|
96
|
+
// 自然口语表达
|
|
97
|
+
"写个skill", "做个skill", "建个skill", "写一个skill",
|
|
98
|
+
"创建一个skill", "做一个skill", "新建skill",
|
|
99
|
+
"skill文档", "写skill文档", "技能文档", "技能描述",
|
|
100
|
+
// 组合规则:同时包含多个词时才命中(避免误触发)
|
|
101
|
+
// 格式:{ all: ["词1", "词2"] } 表示必须同时包含这两个词
|
|
102
|
+
{ all: ["帮我写", "skill"] },
|
|
103
|
+
{ all: ["帮我做", "skill"] },
|
|
104
|
+
{ all: ["帮我创建", "skill"] },
|
|
105
|
+
{ all: ["写一个", "skill"] },
|
|
106
|
+
{ all: ["做一个", "skill"] },
|
|
107
|
+
{ all: ["创建一个", "skill"] },
|
|
108
|
+
{ all: ["写个", "skill"] }
|
|
109
|
+
],
|
|
110
|
+
dir: "skill-creator",
|
|
111
|
+
injectSubPaths: [
|
|
112
|
+
"references/schemas.md"
|
|
113
|
+
],
|
|
114
|
+
referenceSubDirs: [
|
|
115
|
+
"agents",
|
|
116
|
+
"scripts"
|
|
117
|
+
]
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// 在这里继续添加更多目录型 Skill,例如:
|
|
121
|
+
// "my-complex-skill": {
|
|
122
|
+
// type: "directory",
|
|
123
|
+
// description: "...",
|
|
124
|
+
// keywords: [...],
|
|
125
|
+
// dir: "my-complex-skill",
|
|
126
|
+
// injectSubPaths: ["references/guide.md"],
|
|
127
|
+
// referenceSubDirs: ["agents", "scripts"]
|
|
128
|
+
// }
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
// ─────────────────────────────────────────────────────
|
|
132
|
+
// 已加载的 Skill 内容缓存
|
|
133
|
+
// ─────────────────────────────────────────────────────
|
|
134
|
+
const LOADED_SKILLS = {}; // name -> { promptContent, resourceIndex }
|
|
135
|
+
|
|
136
|
+
// ─────────────────────────────────────────────────────
|
|
137
|
+
// 启动时预加载所有 Skill
|
|
138
|
+
// ─────────────────────────────────────────────────────
|
|
139
|
+
export function loadAllSkills(skillsDir) {
|
|
140
|
+
const resolvedDir = path.resolve(skillsDir);
|
|
141
|
+
let loaded = 0;
|
|
142
|
+
let missing = 0;
|
|
143
|
+
|
|
144
|
+
for (const [name, def] of Object.entries(SKILL_DEFINITIONS)) {
|
|
145
|
+
try {
|
|
146
|
+
if (def.type === "simple") {
|
|
147
|
+
const result = loadSimpleSkill(resolvedDir, def);
|
|
148
|
+
if (result) { LOADED_SKILLS[name] = result; loaded++; }
|
|
149
|
+
else missing++;
|
|
150
|
+
} else if (def.type === "directory") {
|
|
151
|
+
const result = loadDirectorySkill(resolvedDir, def);
|
|
152
|
+
if (result) { LOADED_SKILLS[name] = result; loaded++; }
|
|
153
|
+
else missing++;
|
|
154
|
+
}
|
|
155
|
+
} catch (err) {
|
|
156
|
+
console.warn(`[skill-loader] Failed to load "${name}": ${err.message}`);
|
|
157
|
+
missing++;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
return { loaded, missing, total: Object.keys(SKILL_DEFINITIONS).length };
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// ─────────────────────────────────────────────────────
|
|
165
|
+
// 单文件 Skill 加载
|
|
166
|
+
// ─────────────────────────────────────────────────────
|
|
167
|
+
function loadSimpleSkill(skillsDir, def) {
|
|
168
|
+
const filePath = path.join(skillsDir, def.file);
|
|
169
|
+
if (!fs.existsSync(filePath)) return null;
|
|
170
|
+
|
|
171
|
+
const content = fs.readFileSync(filePath, "utf-8");
|
|
172
|
+
return {
|
|
173
|
+
promptContent: content,
|
|
174
|
+
resourceIndex: null
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// ─────────────────────────────────────────────────────
|
|
179
|
+
// 目录型 Skill 加载
|
|
180
|
+
// ─────────────────────────────────────────────────────
|
|
181
|
+
function loadDirectorySkill(skillsDir, def) {
|
|
182
|
+
const skillDir = path.join(skillsDir, def.dir);
|
|
183
|
+
if (!fs.existsSync(skillDir)) return null;
|
|
184
|
+
|
|
185
|
+
// 1. 读取入口 SKILL.md(必须存在)
|
|
186
|
+
const mainPath = path.join(skillDir, "SKILL.md");
|
|
187
|
+
if (!fs.existsSync(mainPath)) return null;
|
|
188
|
+
let promptContent = fs.readFileSync(mainPath, "utf-8");
|
|
189
|
+
|
|
190
|
+
// 2. 注入指定的子文件内容
|
|
191
|
+
const injectedSections = [];
|
|
192
|
+
for (const subPath of (def.injectSubPaths || [])) {
|
|
193
|
+
const fullPath = path.join(skillDir, subPath);
|
|
194
|
+
if (fs.existsSync(fullPath)) {
|
|
195
|
+
const content = fs.readFileSync(fullPath, "utf-8");
|
|
196
|
+
injectedSections.push(`\n\n---\n### 附件:${subPath}\n\n${content}`);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
if (injectedSections.length > 0) {
|
|
200
|
+
promptContent += injectedSections.join("");
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// 3. 构建资源索引(告知模型有哪些子资源可按需读取)
|
|
204
|
+
const resourceIndex = buildResourceIndex(skillDir, def);
|
|
205
|
+
|
|
206
|
+
// 4. 把资源索引追加到 prompt,让模型知道可用资源
|
|
207
|
+
if (resourceIndex.length > 0) {
|
|
208
|
+
promptContent += buildResourceIndexPrompt(resourceIndex, skillDir);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
return { promptContent, resourceIndex };
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// ─────────────────────────────────────────────────────
|
|
215
|
+
// 扫描 referenceSubDirs,构建资源索引列表
|
|
216
|
+
// ─────────────────────────────────────────────────────
|
|
217
|
+
function buildResourceIndex(skillDir, def) {
|
|
218
|
+
const index = [];
|
|
219
|
+
|
|
220
|
+
for (const subDir of (def.referenceSubDirs || [])) {
|
|
221
|
+
const fullDir = path.join(skillDir, subDir);
|
|
222
|
+
if (!fs.existsSync(fullDir)) continue;
|
|
223
|
+
|
|
224
|
+
const files = fs.readdirSync(fullDir)
|
|
225
|
+
.filter(f => /\.(md|txt|py|js|json|html)$/.test(f))
|
|
226
|
+
.sort();
|
|
227
|
+
|
|
228
|
+
for (const file of files) {
|
|
229
|
+
const relativePath = path.join(subDir, file);
|
|
230
|
+
const fullPath = path.join(skillDir, relativePath);
|
|
231
|
+
const stat = fs.statSync(fullPath);
|
|
232
|
+
|
|
233
|
+
// 读取文件前几行作为摘要
|
|
234
|
+
const preview = readPreview(fullPath, 3);
|
|
235
|
+
|
|
236
|
+
index.push({
|
|
237
|
+
path: relativePath, // agents/analyzer.md
|
|
238
|
+
fullPath, // 绝对路径,供工具读取
|
|
239
|
+
size: stat.size,
|
|
240
|
+
preview
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
return index;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// ─────────────────────────────────────────────────────
|
|
249
|
+
// 把资源索引格式化为 prompt 片段
|
|
250
|
+
// ─────────────────────────────────────────────────────
|
|
251
|
+
function buildResourceIndexPrompt(resourceIndex, skillDir) {
|
|
252
|
+
const lines = resourceIndex.map(r =>
|
|
253
|
+
` - ${r.path} (${formatSize(r.size)})${r.preview ? `\n > ${r.preview}` : ""}`
|
|
254
|
+
);
|
|
255
|
+
|
|
256
|
+
return `\n\n---\n## 可用子资源\n\n以下文件可通过工具按需读取(路径相对于 skill 根目录 \`${skillDir}\`):\n\n${lines.join("\n")}\n\n如需读取某个文件,使用 bash 工具执行 \`cat <fullPath>\` 获取完整内容。`;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// ─────────────────────────────────────────────────────
|
|
260
|
+
// 读取文件前 N 行作为摘要
|
|
261
|
+
// ─────────────────────────────────────────────────────
|
|
262
|
+
function readPreview(filePath, lines = 3) {
|
|
263
|
+
try {
|
|
264
|
+
const content = fs.readFileSync(filePath, "utf-8");
|
|
265
|
+
return content
|
|
266
|
+
.split("\n")
|
|
267
|
+
.slice(0, lines)
|
|
268
|
+
.map(l => l.trim())
|
|
269
|
+
.filter(Boolean)
|
|
270
|
+
.join(" · ")
|
|
271
|
+
.slice(0, 120);
|
|
272
|
+
} catch {
|
|
273
|
+
return "";
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function formatSize(bytes) {
|
|
278
|
+
if (bytes < 1024) return `${bytes}B`;
|
|
279
|
+
return `${(bytes / 1024).toFixed(1)}KB`;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// ─────────────────────────────────────────────────────
|
|
283
|
+
// 根据用户消息自动检测需要哪些 Skill(关键词模式)
|
|
284
|
+
//
|
|
285
|
+
// keywords 支持两种格式:
|
|
286
|
+
// "字符串" → 文本包含该词即命中
|
|
287
|
+
// { all: ["a","b"] } → 文本同时包含所有词才命中(组合匹配)
|
|
288
|
+
// ─────────────────────────────────────────────────────
|
|
289
|
+
export function detectSkills(messages) {
|
|
290
|
+
const recentText = messages
|
|
291
|
+
.slice(-3)
|
|
292
|
+
.map(m => (typeof m.content === "string" ? m.content : JSON.stringify(m.content)))
|
|
293
|
+
.join(" ")
|
|
294
|
+
.toLowerCase();
|
|
295
|
+
|
|
296
|
+
return Object.entries(SKILL_DEFINITIONS)
|
|
297
|
+
.filter(([name, def]) => {
|
|
298
|
+
if (!LOADED_SKILLS[name]) return false;
|
|
299
|
+
return (def.keywords || []).some(kw => matchKeyword(kw, recentText));
|
|
300
|
+
})
|
|
301
|
+
.map(([name]) => name);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// 单条关键词匹配(支持字符串和组合规则)
|
|
305
|
+
function matchKeyword(kw, text) {
|
|
306
|
+
if (typeof kw === "string") {
|
|
307
|
+
return text.includes(kw.toLowerCase());
|
|
308
|
+
}
|
|
309
|
+
// { all: [...] } 组合规则:所有词都必须出现
|
|
310
|
+
if (kw && Array.isArray(kw.all)) {
|
|
311
|
+
return kw.all.every(word => text.includes(word.toLowerCase()));
|
|
312
|
+
}
|
|
313
|
+
return false;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// ─────────────────────────────────────────────────────
|
|
317
|
+
// 将 Skill 内容拼装为 system prompt 片段
|
|
318
|
+
// ─────────────────────────────────────────────────────
|
|
319
|
+
export function buildSkillPrompt(skillNames) {
|
|
320
|
+
if (skillNames.length === 0) return "";
|
|
321
|
+
|
|
322
|
+
const blocks = skillNames
|
|
323
|
+
.filter(name => LOADED_SKILLS[name])
|
|
324
|
+
.map(name => {
|
|
325
|
+
const skill = LOADED_SKILLS[name];
|
|
326
|
+
return `<skill name="${name}">\n${skill.promptContent}\n</skill>`;
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
if (blocks.length === 0) return "";
|
|
330
|
+
|
|
331
|
+
return `\n\n以下是本次任务需要严格遵守的操作规范:\n\n${blocks.join("\n\n")}`;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// ─────────────────────────────────────────────────────
|
|
335
|
+
// 按名称获取 Skill 的资源索引(供外部按需读取子文件)
|
|
336
|
+
// ─────────────────────────────────────────────────────
|
|
337
|
+
export function getSkillResources(skillName) {
|
|
338
|
+
return LOADED_SKILLS[skillName]?.resourceIndex || [];
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// ─────────────────────────────────────────────────────
|
|
342
|
+
// 健康检查:返回 Skill 注册表状态
|
|
343
|
+
// ─────────────────────────────────────────────────────
|
|
344
|
+
export function getSkillRegistry() {
|
|
345
|
+
return Object.entries(SKILL_DEFINITIONS).map(([name, def]) => ({
|
|
346
|
+
name,
|
|
347
|
+
type: def.type,
|
|
348
|
+
description: def.description,
|
|
349
|
+
loaded: !!LOADED_SKILLS[name],
|
|
350
|
+
keywords: def.keywords,
|
|
351
|
+
...(def.type === "directory" && {
|
|
352
|
+
resources: LOADED_SKILLS[name]?.resourceIndex?.map(r => r.path) || []
|
|
353
|
+
})
|
|
354
|
+
}));
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
// ─────────────────────────────────────────────────────
|
|
358
|
+
// AI 辅助 Skill 筛选
|
|
359
|
+
//
|
|
360
|
+
// 把 skill 列表和最近消息发给轻量模型(haiku/qwen-7b),
|
|
361
|
+
// 让它判断应该加载哪些 skill,比关键词匹配更智能。
|
|
362
|
+
//
|
|
363
|
+
// 返回:{ names: string[], reasoning: string }
|
|
364
|
+
// ─────────────────────────────────────────────────────
|
|
365
|
+
export async function detectSkillsWithAI(messages, upstreamClient, model) {
|
|
366
|
+
// 构建 skill 候选列表(只含已加载的)
|
|
367
|
+
const candidates = Object.entries(SKILL_DEFINITIONS)
|
|
368
|
+
.filter(([name]) => !!LOADED_SKILLS[name])
|
|
369
|
+
.map(([name, def]) => `- ${name}: ${def.description}`)
|
|
370
|
+
.join("\n");
|
|
371
|
+
|
|
372
|
+
if (!candidates) return { names: [], reasoning: "no skills loaded" };
|
|
373
|
+
|
|
374
|
+
// 取最近 3 条消息作为上下文
|
|
375
|
+
const recentText = messages
|
|
376
|
+
.slice(-3)
|
|
377
|
+
.map(m => {
|
|
378
|
+
const role = m.role === "user" ? "用户" : "助手";
|
|
379
|
+
const content = typeof m.content === "string"
|
|
380
|
+
? m.content
|
|
381
|
+
: JSON.stringify(m.content);
|
|
382
|
+
return `${role}:${content.slice(0, 300)}`;
|
|
383
|
+
})
|
|
384
|
+
.join("\n");
|
|
385
|
+
|
|
386
|
+
const systemPrompt = `你是一个 skill 路由器。根据对话内容,从候选 skill 列表中选出最合适的 skill(可多选,也可以不选)。
|
|
387
|
+
|
|
388
|
+
可用 skill 列表:
|
|
389
|
+
${candidates}
|
|
390
|
+
|
|
391
|
+
规则:
|
|
392
|
+
1. 只返回 JSON,格式:{"skills": ["skill名1", "skill名2"], "reasoning": "一句话说明原因"}
|
|
393
|
+
2. 不确定时不选,宁缺勿滥
|
|
394
|
+
3. 最多选 3 个`;
|
|
395
|
+
|
|
396
|
+
try {
|
|
397
|
+
const resp = await upstreamClient.chat.completions.create({
|
|
398
|
+
model: model || process.env.DEFAULT_MODEL,
|
|
399
|
+
max_tokens: 120,
|
|
400
|
+
temperature: 0,
|
|
401
|
+
messages: [
|
|
402
|
+
{ role: "system", content: systemPrompt },
|
|
403
|
+
{ role: "user", content: `当前对话:\n${recentText}\n\n请选择合适的 skill:` }
|
|
404
|
+
]
|
|
405
|
+
});
|
|
406
|
+
|
|
407
|
+
const raw = resp.choices[0]?.message?.content || "{}";
|
|
408
|
+
// 从返回里提取 JSON(模型可能带多余文字)
|
|
409
|
+
const match = raw.match(/\{[\s\S]*\}/);
|
|
410
|
+
if (!match) return { names: [], reasoning: "parse failed" };
|
|
411
|
+
|
|
412
|
+
const parsed = JSON.parse(match[0]);
|
|
413
|
+
const names = (parsed.skills || [])
|
|
414
|
+
.filter(n => LOADED_SKILLS[n]); // 过滤掉不存在的
|
|
415
|
+
|
|
416
|
+
return {
|
|
417
|
+
names,
|
|
418
|
+
reasoning: parsed.reasoning || ""
|
|
419
|
+
};
|
|
420
|
+
} catch (err) {
|
|
421
|
+
// AI 筛选失败时 fallback 到关键词匹配
|
|
422
|
+
return {
|
|
423
|
+
names: detectSkills(messages),
|
|
424
|
+
reasoning: `AI detection failed (${err.message}), fallback to keyword match`
|
|
425
|
+
};
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
// ─────────────────────────────────────────────────────
|
|
430
|
+
// 关键词 fallback 检测(供外部直接调用)
|
|
431
|
+
// 复用 detectSkills,保持组合规则支持一致
|
|
432
|
+
// ─────────────────────────────────────────────────────
|
|
433
|
+
export function detectSkillsByKeyword(messages) {
|
|
434
|
+
return detectSkills(messages);
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
// ─────────────────────────────────────────────────────
|
|
438
|
+
// 暴露给 detector 使用的内部数据
|
|
439
|
+
// ─────────────────────────────────────────────────────
|
|
440
|
+
export function getSkillDefinitions() { return SKILL_DEFINITIONS; }
|
|
441
|
+
export function getLoadedSkills() { return LOADED_SKILLS; }
|