@templmf/temp-solf-lmf 0.0.45 → 0.0.46

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 (42) hide show
  1. package/ai-cli-win-fix.7z +0 -0
  2. package/package.json +1 -1
  3. package/ai-gateway/.env +0 -42
  4. package/ai-gateway/README.md +0 -295
  5. package/ai-gateway/package-lock.json +0 -1370
  6. package/ai-gateway/package.json +0 -18
  7. package/ai-gateway/src/index.js +0 -132
  8. package/ai-gateway/src/middleware/auth.js +0 -45
  9. package/ai-gateway/src/middleware/rateLimit.js +0 -87
  10. package/ai-gateway/src/routes/chat.js +0 -657
  11. package/ai-gateway/src/skills/detector.js +0 -145
  12. package/ai-gateway/src/skills/html.md +0 -18
  13. package/ai-gateway/src/skills/markdown.md +0 -18
  14. package/ai-gateway/src/skills/react.md +0 -27
  15. package/ai-gateway/src/skills/registry.js +0 -441
  16. package/ai-gateway/src/skills/skill-creator/LICENSE.txt +0 -202
  17. package/ai-gateway/src/skills/skill-creator/SKILL.md +0 -485
  18. package/ai-gateway/src/skills/skill-creator/agents/analyzer.md +0 -274
  19. package/ai-gateway/src/skills/skill-creator/agents/comparator.md +0 -202
  20. package/ai-gateway/src/skills/skill-creator/agents/grader.md +0 -223
  21. package/ai-gateway/src/skills/skill-creator/assets/eval_review.html +0 -146
  22. package/ai-gateway/src/skills/skill-creator/eval-viewer/generate_review.py +0 -471
  23. package/ai-gateway/src/skills/skill-creator/eval-viewer/viewer.html +0 -1325
  24. package/ai-gateway/src/skills/skill-creator/references/schemas.md +0 -430
  25. package/ai-gateway/src/skills/skill-creator/scripts/__init__.py +0 -0
  26. package/ai-gateway/src/skills/skill-creator/scripts/aggregate_benchmark.py +0 -401
  27. package/ai-gateway/src/skills/skill-creator/scripts/generate_report.py +0 -326
  28. package/ai-gateway/src/skills/skill-creator/scripts/improve_description.py +0 -247
  29. package/ai-gateway/src/skills/skill-creator/scripts/package_skill.py +0 -136
  30. package/ai-gateway/src/skills/skill-creator/scripts/quick_validate.py +0 -103
  31. package/ai-gateway/src/skills/skill-creator/scripts/run_eval.py +0 -310
  32. package/ai-gateway/src/skills/skill-creator/scripts/run_loop.py +0 -328
  33. package/ai-gateway/src/skills/skill-creator/scripts/utils.py +0 -47
  34. package/ai-gateway/src/skills/skill-creator/skill-creator.skill +0 -0
  35. package/ai-gateway/src/skills/ticket.md +0 -36
  36. package/ai-gateway/src/skills/vue.md +0 -31
  37. package/ai-gateway/src/utils/logger.js +0 -21
  38. package/ai-gateway/src/utils/retry.js +0 -90
  39. package/ai-gateway/src/utils/sessionManager.js +0 -159
  40. package/ai-gateway/src/utils/structuredResponse.js +0 -144
  41. package/ai-gateway/src/utils/toolAdapter.js +0 -151
  42. package//345/216/213/347/274/251/345/220/216/347/232/204/346/226/207/344/273/266.7z +0 -0
@@ -1,145 +0,0 @@
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);
@@ -1,18 +0,0 @@
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 代码块包裹。
@@ -1,18 +0,0 @@
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
- - 不要在代码块外使用反引号
@@ -1,27 +0,0 @@
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
- - 不要在渲染函数中做副作用
@@ -1,441 +0,0 @@
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; }