neo-skill 0.1.20 → 0.1.22
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/package.json +1 -1
- package/skills/skill-finder/README.md +52 -0
- package/skills/skill-finder/references/install-modes.md +13 -0
- package/skills/skill-finder/references/matching-algorithm.md +172 -0
- package/skills/skill-finder/skillspec.json +77 -0
- package/src/skill_finder/__init__.py +21 -0
- package/src/skill_finder/cli.py +106 -0
- package/src/skill_finder/doctor.py +105 -0
- package/src/skill_finder/install_record.py +58 -0
- package/src/skill_finder/installer.py +117 -0
- package/src/skill_finder/interview.py +101 -0
- package/src/skill_finder/matcher.py +159 -0
- package/src/skill_finder/models.py +117 -0
- package/src/skill_finder/recommender.py +51 -0
- package/src/skill_finder/registry.py +108 -0
package/package.json
CHANGED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# skill-finder
|
|
2
|
+
|
|
3
|
+
发现/匹配/安装第三方 skill 库。
|
|
4
|
+
|
|
5
|
+
## 功能
|
|
6
|
+
|
|
7
|
+
- **主动搜索**:通过两级提问收集需求,匹配第三方能力单元
|
|
8
|
+
- **被动推荐**:供 skill-creator 调用,推荐高置信度第三方 unit
|
|
9
|
+
- **两种安装模式**:自动安装(默认)/ 手动安装(仅输出命令)
|
|
10
|
+
- **可追溯性**:所有安装行为记录到 `~/.omni-skill/install_records.json`
|
|
11
|
+
- **诊断工具**:提供匹配 trace 和安装记录分析
|
|
12
|
+
|
|
13
|
+
## 触发方式
|
|
14
|
+
|
|
15
|
+
- "找第三方 skill"
|
|
16
|
+
- "有没有现成的工具"
|
|
17
|
+
- "推荐一个能力库"
|
|
18
|
+
- "安装第三方能力"
|
|
19
|
+
- "search for third-party skill"
|
|
20
|
+
|
|
21
|
+
## 使用方式
|
|
22
|
+
|
|
23
|
+
### CLI 模式
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
python -m skill_finder.cli
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
### 编程接口
|
|
30
|
+
|
|
31
|
+
```python
|
|
32
|
+
from skill_finder.recommender import Recommender
|
|
33
|
+
from skill_finder.models import SearchQuery
|
|
34
|
+
|
|
35
|
+
recommender = Recommender()
|
|
36
|
+
query = SearchQuery(goal="AI 代码编辑", tags=["code-generation"], ide="windsurf")
|
|
37
|
+
result = recommender.recommend(query)
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## 设计原则
|
|
41
|
+
|
|
42
|
+
- **宁缺毋滥**:置信度 < 60% 必须拒绝
|
|
43
|
+
- **诚实反馈**:明确告知拒绝原因(4 种分类)
|
|
44
|
+
- **可追溯**:install_record 记录所有安装行为
|
|
45
|
+
- **轻量集成**:skill-creator 集成不打断主流程
|
|
46
|
+
|
|
47
|
+
## 详细文档
|
|
48
|
+
|
|
49
|
+
参见:
|
|
50
|
+
- `DESIGN_SKILL_FINDER.md` - 完整设计文档
|
|
51
|
+
- `SKILL_FINDER_USAGE.md` - 使用指南
|
|
52
|
+
- `references/matching-algorithm.md` - 匹配算法详解
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# skill-finder 安装模式
|
|
2
|
+
|
|
3
|
+
## 两种安装模式
|
|
4
|
+
|
|
5
|
+
### 模式 1: 自动安装(默认)
|
|
6
|
+
- 执行 `package.install.auto_install_cmd`
|
|
7
|
+
- 捕获输出并记录结果
|
|
8
|
+
- 写入 install_record (executed=true)
|
|
9
|
+
|
|
10
|
+
### 模式 2: 手动安装
|
|
11
|
+
- 输出 `package.install.manual_install_cmd`
|
|
12
|
+
- 记录指令 (executed=false, result=skipped)
|
|
13
|
+
- 任务结束,不执行安装
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
# skill-finder 匹配算法
|
|
2
|
+
|
|
3
|
+
## 一、两阶段匹配流程
|
|
4
|
+
|
|
5
|
+
### 阶段 1: 粗筛(Coarse Filtering)
|
|
6
|
+
**目标**: 快速缩小候选范围
|
|
7
|
+
|
|
8
|
+
**输入**: SearchQuery (goal, tags, keywords, ide, env, constraints)
|
|
9
|
+
|
|
10
|
+
**方法**: 倒排索引命中
|
|
11
|
+
1. **Tag 倒排**: 从 `indexes/units.by_tag.json` 查找匹配 `query.tags` 的 unit_id
|
|
12
|
+
2. **Keyword 倒排**: 从 `indexes/units.by_keyword.json` 查找匹配 `query.keywords` 的 unit_id
|
|
13
|
+
3. **IDE 倒排**: 从 `indexes/units.by_ide.json` 查找匹配 `query.ide` 的 unit_id
|
|
14
|
+
4. **合并**: 取并集(tag OR keyword)与 IDE 交集
|
|
15
|
+
|
|
16
|
+
**输出**: 候选 unit_id 列表
|
|
17
|
+
|
|
18
|
+
**拒绝条件**: 若候选为空,返回 `no_candidates_by_tag`
|
|
19
|
+
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
### 阶段 2: 精排(Fine Ranking)
|
|
23
|
+
**目标**: 计算置信度分数,过滤低质量匹配
|
|
24
|
+
|
|
25
|
+
**评分公式**:
|
|
26
|
+
```
|
|
27
|
+
score = tag_coverage * 0.6 + keyword_match * 0.3 + ide_bonus * 0.1
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
**评分细节**:
|
|
31
|
+
|
|
32
|
+
1. **Tag 覆盖率** (权重 0.6)
|
|
33
|
+
```
|
|
34
|
+
tag_coverage = len(matched_tags) / len(query.tags)
|
|
35
|
+
matched_tags = set(query.tags) ∩ set(unit.capability_tags)
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
2. **Keyword 匹配** (权重 0.3)
|
|
39
|
+
```
|
|
40
|
+
keyword_match = len(matched_keywords) / len(query.keywords)
|
|
41
|
+
matched_keywords = set(query.keywords) ∩ set(unit.keywords)
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
3. **IDE 支持** (权重 0.1)
|
|
45
|
+
- 支持: +0.1
|
|
46
|
+
- 不支持: score *= 0.2 (显著降权)
|
|
47
|
+
|
|
48
|
+
4. **约束过滤** (降权因子)
|
|
49
|
+
- **env 冲突**: 若 `unit.conflicts` 提及 `query.env` → score *= 0.3
|
|
50
|
+
- **constraints 违反**: 若 unit 描述与约束冲突 → score *= 0.5
|
|
51
|
+
- 示例: `no-network` 约束 + unit 描述含 "network" → 降权
|
|
52
|
+
|
|
53
|
+
**输出**: 按 score 降序排列的 MatchResult 列表
|
|
54
|
+
|
|
55
|
+
---
|
|
56
|
+
|
|
57
|
+
## 二、置信门槛与拒绝策略
|
|
58
|
+
|
|
59
|
+
### 置信门槛
|
|
60
|
+
```python
|
|
61
|
+
MIN_SCORE = 0.6 # 60%
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
**规则**:
|
|
65
|
+
- `score >= 0.6`: 返回 Top 1-3
|
|
66
|
+
- `score < 0.6`: **必须拒绝**,不得返回低质量结果
|
|
67
|
+
|
|
68
|
+
---
|
|
69
|
+
|
|
70
|
+
### 拒绝原因分类
|
|
71
|
+
|
|
72
|
+
#### 1. `no_candidates_by_tag`
|
|
73
|
+
**条件**: 粗筛阶段无候选
|
|
74
|
+
**原因**: 未找到匹配的能力标签或关键词
|
|
75
|
+
**建议**: 补充更多标签/关键词,或使用 skill-creator 创建自研 skill
|
|
76
|
+
|
|
77
|
+
#### 2. `candidates_but_no_ide_support`
|
|
78
|
+
**条件**: 有候选但所有 unit 均不支持目标 IDE
|
|
79
|
+
**原因**: 找到候选能力,但均不支持 {query.ide}
|
|
80
|
+
**建议**: 更换 IDE 或使用 generic 模式
|
|
81
|
+
|
|
82
|
+
#### 3. `candidates_but_incompatible_env`
|
|
83
|
+
**条件**: 有候选但所有 unit 均因 env/constraints 被降权至 < 0.6
|
|
84
|
+
**原因**: 找到候选能力,但均不满足环境或约束条件
|
|
85
|
+
**建议**: 放宽约束或调整环境
|
|
86
|
+
|
|
87
|
+
#### 4. `insufficient_info`
|
|
88
|
+
**条件**: 有候选但最高分 < 0.6,且无明确冲突
|
|
89
|
+
**原因**: 候选能力置信度不足(< 60%),建议补充更多需求信息
|
|
90
|
+
**建议**: 回答 Level 2 问题,提供更多细节
|
|
91
|
+
|
|
92
|
+
---
|
|
93
|
+
|
|
94
|
+
## 三、匹配示例
|
|
95
|
+
|
|
96
|
+
### 示例 1: 高置信匹配
|
|
97
|
+
```
|
|
98
|
+
Query:
|
|
99
|
+
goal: "AI 辅助多文件代码重构"
|
|
100
|
+
tags: ["code-refactoring", "multi-file-editing"]
|
|
101
|
+
ide: "windsurf"
|
|
102
|
+
|
|
103
|
+
候选: gh:aider-chat/aider#code-edit
|
|
104
|
+
capability_tags: ["code-generation", "code-refactoring", "multi-file-editing"]
|
|
105
|
+
ide_support: ["windsurf", "cursor"]
|
|
106
|
+
|
|
107
|
+
评分:
|
|
108
|
+
tag_coverage = 2/2 = 1.0 → 0.6
|
|
109
|
+
keyword_match = 0 (未提供) → 0
|
|
110
|
+
ide_bonus = 0.1
|
|
111
|
+
score = 0.7 ✓ (通过)
|
|
112
|
+
|
|
113
|
+
结果: 返回 Top 1
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
### 示例 2: IDE 不支持拒绝
|
|
117
|
+
```
|
|
118
|
+
Query:
|
|
119
|
+
goal: "生成 commit message"
|
|
120
|
+
tags: ["git-commit"]
|
|
121
|
+
ide: "vscode"
|
|
122
|
+
|
|
123
|
+
候选: gh:aider-chat/aider#git-commit
|
|
124
|
+
capability_tags: ["git-commit"]
|
|
125
|
+
ide_support: ["windsurf", "cursor", "generic"]
|
|
126
|
+
|
|
127
|
+
评分:
|
|
128
|
+
tag_coverage = 1/1 = 1.0 → 0.6
|
|
129
|
+
ide_bonus = 0 (不支持 vscode)
|
|
130
|
+
降权: score *= 0.2 → 0.12 ✗
|
|
131
|
+
|
|
132
|
+
拒绝原因: candidates_but_no_ide_support
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
### 示例 3: 约束冲突拒绝
|
|
136
|
+
```
|
|
137
|
+
Query:
|
|
138
|
+
goal: "自动化代码编辑"
|
|
139
|
+
tags: ["code-generation"]
|
|
140
|
+
constraints: ["no-network"]
|
|
141
|
+
ide: "windsurf"
|
|
142
|
+
|
|
143
|
+
候选: gh:aider-chat/aider#code-edit
|
|
144
|
+
description: "AI-powered ... (需要 LLM API)"
|
|
145
|
+
conflicts: "需要网络访问 LLM provider"
|
|
146
|
+
|
|
147
|
+
评分:
|
|
148
|
+
tag_coverage = 1/1 = 1.0 → 0.6
|
|
149
|
+
ide_bonus = 0.1
|
|
150
|
+
约束冲突: score *= 0.5 → 0.35 ✗
|
|
151
|
+
|
|
152
|
+
拒绝原因: candidates_but_incompatible_env
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
---
|
|
156
|
+
|
|
157
|
+
## 四、实现要点
|
|
158
|
+
|
|
159
|
+
### 宁缺毋滥原则
|
|
160
|
+
- **禁止强行匹配**: 低于阈值必须拒绝
|
|
161
|
+
- **禁止虚构结果**: 不得返回不存在的 unit
|
|
162
|
+
- **诚实反馈**: 明确告知拒绝原因与缺口信息
|
|
163
|
+
|
|
164
|
+
### 性能优化
|
|
165
|
+
- **按需加载**: 粗筛仅加载索引,精排才加载完整 unit/package 数据
|
|
166
|
+
- **Top-K 限制**: 最多返回 Top 3,避免信息过载
|
|
167
|
+
- **索引预构建**: 使用 `build_third_party_indexes.py` 预生成倒排索引
|
|
168
|
+
|
|
169
|
+
### 可扩展性
|
|
170
|
+
- **插件化评分**: 可新增评分因子(如 popularity, last_updated)
|
|
171
|
+
- **动态阈值**: 可根据 query 复杂度调整 MIN_SCORE
|
|
172
|
+
- **多语言支持**: 索引构建时归一化大小写,支持中英文关键词
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 1,
|
|
3
|
+
"name": "skill-finder",
|
|
4
|
+
"description": "发现/匹配/安装第三方 skill 库。支持两种模式:(1) 主动搜索(最小两级提问);(2) 被动推荐(skill-creator 集成)。宁缺毋滥,未达阈值必须拒绝并说明原因。",
|
|
5
|
+
"primary_target": "windsurf",
|
|
6
|
+
"backward_compat": ["claude", "cursor", "github"],
|
|
7
|
+
"questions": [
|
|
8
|
+
"你想实现什么目标?(一句话描述任务)",
|
|
9
|
+
"输入是什么?(文件/代码/数据/命令等)",
|
|
10
|
+
"运行环境?(OS/Shell/Runtime)",
|
|
11
|
+
"使用哪个 IDE/编辑器?(windsurf/cursor/vscode/generic)"
|
|
12
|
+
],
|
|
13
|
+
"triggers": [
|
|
14
|
+
"找第三方 skill",
|
|
15
|
+
"有没有现成的工具",
|
|
16
|
+
"推荐一个能力库",
|
|
17
|
+
"安装第三方能力",
|
|
18
|
+
"search for third-party skill",
|
|
19
|
+
"find existing tools"
|
|
20
|
+
],
|
|
21
|
+
"freedom_level": "low",
|
|
22
|
+
"workflow": {
|
|
23
|
+
"type": "sequential",
|
|
24
|
+
"steps": [
|
|
25
|
+
{
|
|
26
|
+
"id": "interview",
|
|
27
|
+
"title": "两级提问收集需求",
|
|
28
|
+
"kind": "action",
|
|
29
|
+
"commands": [],
|
|
30
|
+
"notes": "Level 1 固定 3-4 问(goal/input/env/ide);Level 2 仅不确定时追问 1-2 个。详见 references/interview-protocol.md"
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
"id": "match",
|
|
34
|
+
"title": "匹配第三方 unit",
|
|
35
|
+
"kind": "action",
|
|
36
|
+
"commands": [],
|
|
37
|
+
"notes": "两阶段:粗筛(倒排索引)+ 精排(tag 覆盖率 + 约束过滤)。置信度 < 60% 必须拒绝并说明原因。详见 references/matching-algorithm.md"
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
"id": "present",
|
|
41
|
+
"title": "展示结果(Top 1-3 或拒绝)",
|
|
42
|
+
"kind": "gate",
|
|
43
|
+
"commands": [],
|
|
44
|
+
"notes": "命中:列出 unit + package + 匹配原因 + README 链接 + 使用方式。未命中:明确拒绝原因分类(no_candidates_by_tag/no_ide_support/incompatible_env/insufficient_info)"
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
"id": "install",
|
|
48
|
+
"title": "安装(默认自动/可选手动)",
|
|
49
|
+
"kind": "action",
|
|
50
|
+
"commands": [],
|
|
51
|
+
"notes": "默认:执行 auto_install_cmd 并记录。手动:输出 manual_install_cmd 并记录(executed=false)。详见 references/install-modes.md"
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
"id": "record",
|
|
55
|
+
"title": "记录安装行为",
|
|
56
|
+
"kind": "action",
|
|
57
|
+
"commands": [],
|
|
58
|
+
"notes": "写入 ~/.omni-skill/install_records.json(或配置路径)。字段:timestamp/unit_id/package_id/mode/executed/result/error_summary/docs_links"
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
"id": "output",
|
|
62
|
+
"title": "输出使用方式",
|
|
63
|
+
"kind": "action",
|
|
64
|
+
"commands": [],
|
|
65
|
+
"notes": "展示 unit.entrypoints(原生命令/参数/示例)+ README 链接 + usage_notes"
|
|
66
|
+
}
|
|
67
|
+
]
|
|
68
|
+
},
|
|
69
|
+
"references": [
|
|
70
|
+
"references/matching-algorithm.md",
|
|
71
|
+
"references/install-modes.md",
|
|
72
|
+
"references/interview-protocol.md",
|
|
73
|
+
"references/registry-schema.md"
|
|
74
|
+
],
|
|
75
|
+
"scripts": [],
|
|
76
|
+
"assets": []
|
|
77
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"""skill-finder - 发现/匹配/安装第三方 skill 库"""
|
|
2
|
+
|
|
3
|
+
__version__ = "1.0.0"
|
|
4
|
+
|
|
5
|
+
from .models import (
|
|
6
|
+
SkillPackage,
|
|
7
|
+
SkillUnit,
|
|
8
|
+
SearchQuery,
|
|
9
|
+
SearchResult,
|
|
10
|
+
MatchResult,
|
|
11
|
+
InstallRecord,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
__all__ = [
|
|
15
|
+
"SkillPackage",
|
|
16
|
+
"SkillUnit",
|
|
17
|
+
"SearchQuery",
|
|
18
|
+
"SearchResult",
|
|
19
|
+
"MatchResult",
|
|
20
|
+
"InstallRecord",
|
|
21
|
+
]
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
"""主动搜索入口 - CLI 接口"""
|
|
2
|
+
|
|
3
|
+
import sys
|
|
4
|
+
from typing import Optional
|
|
5
|
+
from .interview import Interview
|
|
6
|
+
from .registry import Registry
|
|
7
|
+
from .matcher import Matcher
|
|
8
|
+
from .installer import Installer
|
|
9
|
+
from .doctor import Doctor
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class SkillFinderCLI:
|
|
13
|
+
"""skill-finder CLI 主入口"""
|
|
14
|
+
|
|
15
|
+
def __init__(self):
|
|
16
|
+
self.registry = Registry()
|
|
17
|
+
self.matcher = Matcher(self.registry)
|
|
18
|
+
self.installer = Installer()
|
|
19
|
+
self.doctor = Doctor(self.registry)
|
|
20
|
+
|
|
21
|
+
def run(self):
|
|
22
|
+
"""主流程"""
|
|
23
|
+
print("=== skill-finder ===")
|
|
24
|
+
print("发现/匹配/安装第三方 skill 库\n")
|
|
25
|
+
|
|
26
|
+
interview = Interview()
|
|
27
|
+
query = interview.collect()
|
|
28
|
+
|
|
29
|
+
print("\n=== 匹配结果 ===")
|
|
30
|
+
result = self.matcher.match(query)
|
|
31
|
+
|
|
32
|
+
if not result.matches:
|
|
33
|
+
print(f"✗ 未找到匹配的第三方能力\n")
|
|
34
|
+
print(f"拒绝原因: {result.rejection_reason}")
|
|
35
|
+
print(f"建议: 尝试使用 skill-creator 创建自研 skill")
|
|
36
|
+
return
|
|
37
|
+
|
|
38
|
+
print(f"找到 {len(result.matches)} 个匹配的能力:\n")
|
|
39
|
+
|
|
40
|
+
for i, match in enumerate(result.matches, 1):
|
|
41
|
+
print(f"【{i}】{match.unit.name} ({match.package.name})")
|
|
42
|
+
print(f"- 描述: {match.unit.description}")
|
|
43
|
+
print(f"- 匹配原因: {'; '.join(match.reasons)}")
|
|
44
|
+
print(f"- 置信度: {match.score:.2f}")
|
|
45
|
+
print(f"- README: {match.package.docs.readme}")
|
|
46
|
+
|
|
47
|
+
if match.warnings:
|
|
48
|
+
print(f"- ⚠ 警告: {'; '.join(match.warnings)}")
|
|
49
|
+
print()
|
|
50
|
+
|
|
51
|
+
choice = input("选择要安装的能力 [1]: ").strip() or "1"
|
|
52
|
+
|
|
53
|
+
try:
|
|
54
|
+
idx = int(choice) - 1
|
|
55
|
+
if idx < 0 or idx >= len(result.matches):
|
|
56
|
+
print("无效选择")
|
|
57
|
+
return
|
|
58
|
+
except ValueError:
|
|
59
|
+
print("无效选择")
|
|
60
|
+
return
|
|
61
|
+
|
|
62
|
+
selected = result.matches[idx]
|
|
63
|
+
|
|
64
|
+
print("\n=== 安装方式 ===")
|
|
65
|
+
print("[1] 自动安装(默认)")
|
|
66
|
+
print("[2] 手动安装(仅输出命令)")
|
|
67
|
+
|
|
68
|
+
mode_choice = input("选择 [1]: ").strip() or "1"
|
|
69
|
+
mode = "manual" if mode_choice == "2" else "auto"
|
|
70
|
+
|
|
71
|
+
record = self.installer.install(selected, mode=mode)
|
|
72
|
+
|
|
73
|
+
if record.result == "success" or record.result == "skipped":
|
|
74
|
+
print("\n=== 使用方式 ===")
|
|
75
|
+
for ep in selected.unit.entrypoints:
|
|
76
|
+
print(f"命令: {ep.command}")
|
|
77
|
+
if ep.args:
|
|
78
|
+
print(f"参数: {ep.args}")
|
|
79
|
+
if ep.cwd:
|
|
80
|
+
print(f"工作目录: {ep.cwd}")
|
|
81
|
+
if ep.examples:
|
|
82
|
+
print("示例:")
|
|
83
|
+
for example in ep.examples:
|
|
84
|
+
print(f" {example}")
|
|
85
|
+
print()
|
|
86
|
+
|
|
87
|
+
print(f"详细文档: {selected.package.docs.readme}")
|
|
88
|
+
|
|
89
|
+
if selected.unit.usage_notes:
|
|
90
|
+
print(f"\n使用说明:\n{selected.unit.usage_notes}")
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def main():
|
|
94
|
+
"""CLI 入口"""
|
|
95
|
+
if len(sys.argv) > 1 and sys.argv[1] == "doctor":
|
|
96
|
+
from .models import SearchQuery
|
|
97
|
+
doctor = Doctor()
|
|
98
|
+
query = SearchQuery(goal="测试", ide="windsurf")
|
|
99
|
+
print(doctor.show_install_records())
|
|
100
|
+
else:
|
|
101
|
+
cli = SkillFinderCLI()
|
|
102
|
+
cli.run()
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
if __name__ == "__main__":
|
|
106
|
+
main()
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
"""诊断与 trace"""
|
|
2
|
+
|
|
3
|
+
from typing import Optional
|
|
4
|
+
from .models import SearchQuery, SearchResult
|
|
5
|
+
from .registry import Registry
|
|
6
|
+
from .matcher import Matcher
|
|
7
|
+
from .install_record import InstallRecordManager
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class Doctor:
|
|
11
|
+
"""诊断工具 - 提供匹配 trace 和安装记录分析"""
|
|
12
|
+
|
|
13
|
+
def __init__(self, registry: Optional[Registry] = None,
|
|
14
|
+
record_manager: Optional[InstallRecordManager] = None):
|
|
15
|
+
if registry is None:
|
|
16
|
+
registry = Registry()
|
|
17
|
+
if record_manager is None:
|
|
18
|
+
record_manager = InstallRecordManager()
|
|
19
|
+
|
|
20
|
+
self.registry = registry
|
|
21
|
+
self.matcher = Matcher(registry)
|
|
22
|
+
self.record_manager = record_manager
|
|
23
|
+
|
|
24
|
+
def trace_match(self, query: SearchQuery) -> str:
|
|
25
|
+
"""输出匹配 trace"""
|
|
26
|
+
output = ["=== skill-finder Doctor Trace ===\n"]
|
|
27
|
+
|
|
28
|
+
output.append("【匹配 Trace】")
|
|
29
|
+
output.append(f"- Query: goal=\"{query.goal}\", tags={query.tags}, ide=\"{query.ide}\"")
|
|
30
|
+
|
|
31
|
+
candidates = self.matcher._coarse_filter(query)
|
|
32
|
+
output.append(f"- 粗筛命中: {len(candidates)} 个候选")
|
|
33
|
+
|
|
34
|
+
if candidates:
|
|
35
|
+
for tag in (query.tags or []):
|
|
36
|
+
units = self.registry.search_by_tag(tag)
|
|
37
|
+
if units:
|
|
38
|
+
output.append(f" - Tag '{tag}': {len(units)} 个 unit")
|
|
39
|
+
|
|
40
|
+
result = self.matcher.match(query)
|
|
41
|
+
|
|
42
|
+
if result.matches:
|
|
43
|
+
output.append("- 精排结果:")
|
|
44
|
+
for i, match in enumerate(result.matches[:3], 1):
|
|
45
|
+
output.append(f" {i}. {match.unit.unit_id} (score={match.score:.2f})")
|
|
46
|
+
output.append(f" - {', '.join(match.reasons)}")
|
|
47
|
+
output.append(f"- 最终返回: Top {len(result.matches)}")
|
|
48
|
+
else:
|
|
49
|
+
output.append(f"- 拒绝原因: {result.rejection_reason}")
|
|
50
|
+
output.append(f"- 拒绝分类: {result.rejection_category}")
|
|
51
|
+
|
|
52
|
+
return "\n".join(output)
|
|
53
|
+
|
|
54
|
+
def show_install_records(self, n: int = 5) -> str:
|
|
55
|
+
"""显示最近的安装记录"""
|
|
56
|
+
output = ["\n【安装记录(最近 {} 条)】".format(n)]
|
|
57
|
+
|
|
58
|
+
records = self.record_manager.get_recent(n)
|
|
59
|
+
|
|
60
|
+
if not records:
|
|
61
|
+
output.append("- 暂无安装记录")
|
|
62
|
+
return "\n".join(output)
|
|
63
|
+
|
|
64
|
+
for i, record in enumerate(records, 1):
|
|
65
|
+
status_icon = "✓" if record.result == "success" else "✗" if record.result == "failed" else "○"
|
|
66
|
+
output.append(f"{i}. {record.timestamp[:10]} {record.timestamp[11:19]} | {record.unit_id} | {record.install_mode} | {status_icon} {record.result}")
|
|
67
|
+
|
|
68
|
+
if record.result == "failed" and record.error_summary:
|
|
69
|
+
error_preview = record.error_summary[:100].replace("\n", " ")
|
|
70
|
+
output.append(f" 错误: {error_preview}...")
|
|
71
|
+
|
|
72
|
+
return "\n".join(output)
|
|
73
|
+
|
|
74
|
+
def suggest_fix(self, package_id: str) -> str:
|
|
75
|
+
"""针对安装失败提供修复建议"""
|
|
76
|
+
output = ["\n【冲突/异常建议】"]
|
|
77
|
+
|
|
78
|
+
records = self.record_manager.filter_by_package(package_id)
|
|
79
|
+
failed = [r for r in records if r.result == "failed"]
|
|
80
|
+
|
|
81
|
+
if not failed:
|
|
82
|
+
output.append(f"- Package {package_id} 无失败记录")
|
|
83
|
+
return "\n".join(output)
|
|
84
|
+
|
|
85
|
+
last_failed = failed[-1]
|
|
86
|
+
|
|
87
|
+
package = self.registry.get_package(package_id)
|
|
88
|
+
if package and package.install.uninstall_cmd:
|
|
89
|
+
output.append(f"- 若安装失败,尝试重装:")
|
|
90
|
+
output.append(f" {package.install.uninstall_cmd} && {package.install.auto_install_cmd}")
|
|
91
|
+
else:
|
|
92
|
+
output.append(f"- 建议手动检查错误信息并重试安装")
|
|
93
|
+
|
|
94
|
+
if last_failed.error_summary:
|
|
95
|
+
output.append(f"\n最近错误摘要:")
|
|
96
|
+
output.append(f" {last_failed.error_summary[:200]}")
|
|
97
|
+
|
|
98
|
+
return "\n".join(output)
|
|
99
|
+
|
|
100
|
+
def full_report(self, query: SearchQuery) -> str:
|
|
101
|
+
"""完整诊断报告"""
|
|
102
|
+
output = []
|
|
103
|
+
output.append(self.trace_match(query))
|
|
104
|
+
output.append(self.show_install_records())
|
|
105
|
+
return "\n".join(output)
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"""安装记录管理"""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import List, Optional
|
|
6
|
+
from datetime import datetime
|
|
7
|
+
from .models import InstallRecord
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class InstallRecordManager:
|
|
11
|
+
"""安装记录管理器"""
|
|
12
|
+
|
|
13
|
+
def __init__(self, record_path: Optional[Path] = None):
|
|
14
|
+
if record_path is None:
|
|
15
|
+
record_path = Path.home() / ".omni-skill" / "install_records.json"
|
|
16
|
+
|
|
17
|
+
self.record_path = Path(record_path)
|
|
18
|
+
self.record_path.parent.mkdir(parents=True, exist_ok=True)
|
|
19
|
+
|
|
20
|
+
if not self.record_path.exists():
|
|
21
|
+
self._save_records([])
|
|
22
|
+
|
|
23
|
+
def add(self, record: InstallRecord):
|
|
24
|
+
"""添加安装记录"""
|
|
25
|
+
records = self.load_all()
|
|
26
|
+
records.append(record.dict())
|
|
27
|
+
self._save_records(records)
|
|
28
|
+
|
|
29
|
+
def load_all(self) -> List[dict]:
|
|
30
|
+
"""加载所有记录"""
|
|
31
|
+
try:
|
|
32
|
+
with open(self.record_path, 'r', encoding='utf-8') as f:
|
|
33
|
+
return json.load(f)
|
|
34
|
+
except Exception:
|
|
35
|
+
return []
|
|
36
|
+
|
|
37
|
+
def get_recent(self, n: int = 10) -> List[InstallRecord]:
|
|
38
|
+
"""获取最近 N 条记录"""
|
|
39
|
+
records = self.load_all()
|
|
40
|
+
recent = records[-n:] if len(records) > n else records
|
|
41
|
+
return [InstallRecord(**r) for r in reversed(recent)]
|
|
42
|
+
|
|
43
|
+
def filter_by_package(self, package_id: str) -> List[InstallRecord]:
|
|
44
|
+
"""按 package_id 过滤"""
|
|
45
|
+
records = self.load_all()
|
|
46
|
+
filtered = [r for r in records if r.get("package_id") == package_id]
|
|
47
|
+
return [InstallRecord(**r) for r in filtered]
|
|
48
|
+
|
|
49
|
+
def filter_by_unit(self, unit_id: str) -> List[InstallRecord]:
|
|
50
|
+
"""按 unit_id 过滤"""
|
|
51
|
+
records = self.load_all()
|
|
52
|
+
filtered = [r for r in records if r.get("unit_id") == unit_id]
|
|
53
|
+
return [InstallRecord(**r) for r in filtered]
|
|
54
|
+
|
|
55
|
+
def _save_records(self, records: List[dict]):
|
|
56
|
+
"""保存记录"""
|
|
57
|
+
with open(self.record_path, 'w', encoding='utf-8') as f:
|
|
58
|
+
json.dump(records, f, indent=2, ensure_ascii=False)
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
"""安装执行器"""
|
|
2
|
+
|
|
3
|
+
import subprocess
|
|
4
|
+
from datetime import datetime
|
|
5
|
+
from typing import Literal, Optional
|
|
6
|
+
from .models import MatchResult, InstallRecord
|
|
7
|
+
from .install_record import InstallRecordManager
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class Installer:
|
|
11
|
+
"""安装执行器 - 支持自动/手动两种模式"""
|
|
12
|
+
|
|
13
|
+
def __init__(self, record_manager: Optional[InstallRecordManager] = None):
|
|
14
|
+
if record_manager is None:
|
|
15
|
+
record_manager = InstallRecordManager()
|
|
16
|
+
self.record_manager = record_manager
|
|
17
|
+
|
|
18
|
+
def install(self, match: MatchResult, mode: Literal["auto", "manual"] = "auto") -> InstallRecord:
|
|
19
|
+
"""执行安装"""
|
|
20
|
+
package = match.package
|
|
21
|
+
unit = match.unit
|
|
22
|
+
|
|
23
|
+
if mode == "manual":
|
|
24
|
+
return self._manual_install(package, unit)
|
|
25
|
+
else:
|
|
26
|
+
return self._auto_install(package, unit)
|
|
27
|
+
|
|
28
|
+
def _manual_install(self, package, unit) -> InstallRecord:
|
|
29
|
+
"""手动安装模式 - 仅输出命令"""
|
|
30
|
+
print(f"\n=== 手动安装 {package.name} ===")
|
|
31
|
+
print(package.install.manual_install_cmd)
|
|
32
|
+
|
|
33
|
+
if package.install.notes:
|
|
34
|
+
print(f"\n注意事项:\n{package.install.notes}")
|
|
35
|
+
|
|
36
|
+
print("\n已记录安装指令。请手动执行上述命令。")
|
|
37
|
+
|
|
38
|
+
record = InstallRecord(
|
|
39
|
+
timestamp=datetime.now().isoformat(),
|
|
40
|
+
unit_id=unit.unit_id,
|
|
41
|
+
package_id=package.package_id,
|
|
42
|
+
install_mode="manual",
|
|
43
|
+
executed=False,
|
|
44
|
+
method=package.install.method,
|
|
45
|
+
commands=package.install.manual_install_cmd,
|
|
46
|
+
result="skipped",
|
|
47
|
+
error_summary=None,
|
|
48
|
+
docs_links=package.docs
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
self.record_manager.add(record)
|
|
52
|
+
return record
|
|
53
|
+
|
|
54
|
+
def _auto_install(self, package, unit) -> InstallRecord:
|
|
55
|
+
"""自动安装模式 - 执行命令"""
|
|
56
|
+
print(f"\n=== 自动安装 {package.name} ===")
|
|
57
|
+
|
|
58
|
+
if package.install.notes:
|
|
59
|
+
print(f"注意事项: {package.install.notes}")
|
|
60
|
+
|
|
61
|
+
print(f"\n执行命令:\n{package.install.auto_install_cmd}\n")
|
|
62
|
+
|
|
63
|
+
try:
|
|
64
|
+
result = subprocess.run(
|
|
65
|
+
package.install.auto_install_cmd,
|
|
66
|
+
shell=True,
|
|
67
|
+
capture_output=True,
|
|
68
|
+
text=True,
|
|
69
|
+
timeout=300
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
if result.returncode == 0:
|
|
73
|
+
print("✓ 安装成功")
|
|
74
|
+
record = InstallRecord(
|
|
75
|
+
timestamp=datetime.now().isoformat(),
|
|
76
|
+
unit_id=unit.unit_id,
|
|
77
|
+
package_id=package.package_id,
|
|
78
|
+
install_mode="auto",
|
|
79
|
+
executed=True,
|
|
80
|
+
method=package.install.method,
|
|
81
|
+
commands=package.install.auto_install_cmd,
|
|
82
|
+
result="success",
|
|
83
|
+
error_summary=None,
|
|
84
|
+
docs_links=package.docs
|
|
85
|
+
)
|
|
86
|
+
else:
|
|
87
|
+
print(f"✗ 安装失败\n{result.stderr}")
|
|
88
|
+
record = InstallRecord(
|
|
89
|
+
timestamp=datetime.now().isoformat(),
|
|
90
|
+
unit_id=unit.unit_id,
|
|
91
|
+
package_id=package.package_id,
|
|
92
|
+
install_mode="auto",
|
|
93
|
+
executed=True,
|
|
94
|
+
method=package.install.method,
|
|
95
|
+
commands=package.install.auto_install_cmd,
|
|
96
|
+
result="failed",
|
|
97
|
+
error_summary=result.stderr[:500] if result.stderr else None,
|
|
98
|
+
docs_links=package.docs
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
except Exception as e:
|
|
102
|
+
print(f"✗ 安装异常:{e}")
|
|
103
|
+
record = InstallRecord(
|
|
104
|
+
timestamp=datetime.now().isoformat(),
|
|
105
|
+
unit_id=unit.unit_id,
|
|
106
|
+
package_id=package.package_id,
|
|
107
|
+
install_mode="auto",
|
|
108
|
+
executed=True,
|
|
109
|
+
method=package.install.method,
|
|
110
|
+
commands=package.install.auto_install_cmd,
|
|
111
|
+
result="failed",
|
|
112
|
+
error_summary=str(e)[:500],
|
|
113
|
+
docs_links=package.docs
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
self.record_manager.add(record)
|
|
117
|
+
return record
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
"""两级提问逻辑"""
|
|
2
|
+
|
|
3
|
+
from typing import Dict, List, Optional
|
|
4
|
+
from .models import SearchQuery
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class Interview:
|
|
8
|
+
"""最小两级提问收集需求"""
|
|
9
|
+
|
|
10
|
+
LEVEL1_QUESTIONS = [
|
|
11
|
+
{
|
|
12
|
+
"id": "goal",
|
|
13
|
+
"text": "你想实现什么目标?(一句话描述任务)",
|
|
14
|
+
"required": True
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
"id": "input",
|
|
18
|
+
"text": "输入是什么?(文件/代码/数据/命令等)",
|
|
19
|
+
"required": False,
|
|
20
|
+
"default": "任意"
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
"id": "env",
|
|
24
|
+
"text": "运行环境?(OS/Shell/Runtime,如 Windows/Linux/Python/Node)",
|
|
25
|
+
"required": False,
|
|
26
|
+
"default": "通用"
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
"id": "ide",
|
|
30
|
+
"text": "使用哪个 IDE/编辑器?(windsurf/cursor/vscode/generic)",
|
|
31
|
+
"required": True
|
|
32
|
+
}
|
|
33
|
+
]
|
|
34
|
+
|
|
35
|
+
def collect(self) -> SearchQuery:
|
|
36
|
+
"""收集需求"""
|
|
37
|
+
answers = {}
|
|
38
|
+
|
|
39
|
+
print("=== 需求收集(Level 1)===\n")
|
|
40
|
+
for q in self.LEVEL1_QUESTIONS:
|
|
41
|
+
if q["required"]:
|
|
42
|
+
answer = input(f"{q['text']}: ").strip()
|
|
43
|
+
while not answer:
|
|
44
|
+
answer = input(f"{q['text']}(必填): ").strip()
|
|
45
|
+
answers[q["id"]] = answer
|
|
46
|
+
else:
|
|
47
|
+
default_hint = f" [默认: {q.get('default', '跳过')}]" if q.get('default') else ""
|
|
48
|
+
answer = input(f"{q['text']}{default_hint}: ").strip()
|
|
49
|
+
answers[q["id"]] = answer if answer else q.get("default")
|
|
50
|
+
|
|
51
|
+
if self._needs_clarification(answers):
|
|
52
|
+
print("\n=== 补充信息(Level 2)===\n")
|
|
53
|
+
|
|
54
|
+
if len(answers["goal"].split()) < 3:
|
|
55
|
+
detail = input("能否详细描述一下具体要做什么?: ").strip()
|
|
56
|
+
if detail:
|
|
57
|
+
answers["goal"] += f" - {detail}"
|
|
58
|
+
|
|
59
|
+
constraints = input("有什么硬约束吗?(如不可联网/必须离线/不可修改文件等,无则回车): ").strip()
|
|
60
|
+
if constraints:
|
|
61
|
+
answers["constraints"] = [c.strip() for c in constraints.split(",")]
|
|
62
|
+
|
|
63
|
+
return SearchQuery(
|
|
64
|
+
goal=answers["goal"],
|
|
65
|
+
input_type=answers.get("input"),
|
|
66
|
+
env=answers.get("env"),
|
|
67
|
+
ide=answers["ide"],
|
|
68
|
+
constraints=answers.get("constraints"),
|
|
69
|
+
tags=self._extract_tags(answers["goal"]),
|
|
70
|
+
keywords=self._extract_keywords(answers["goal"])
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
def _needs_clarification(self, answers: Dict) -> bool:
|
|
74
|
+
"""判断是否需要 Level 2"""
|
|
75
|
+
return len(answers["goal"].split()) < 5
|
|
76
|
+
|
|
77
|
+
def _extract_tags(self, goal: str) -> List[str]:
|
|
78
|
+
"""从 goal 提取 tags(简单关键词映射)"""
|
|
79
|
+
tag_map = {
|
|
80
|
+
"代码": "code-generation",
|
|
81
|
+
"生成": "code-generation",
|
|
82
|
+
"重构": "code-refactoring",
|
|
83
|
+
"git": "git-integration",
|
|
84
|
+
"commit": "git-commit",
|
|
85
|
+
"多文件": "multi-file-editing",
|
|
86
|
+
"测试": "testing",
|
|
87
|
+
"文档": "documentation",
|
|
88
|
+
"编辑": "code-generation",
|
|
89
|
+
}
|
|
90
|
+
tags = []
|
|
91
|
+
goal_lower = goal.lower()
|
|
92
|
+
for keyword, tag in tag_map.items():
|
|
93
|
+
if keyword in goal_lower:
|
|
94
|
+
if tag not in tags:
|
|
95
|
+
tags.append(tag)
|
|
96
|
+
return tags
|
|
97
|
+
|
|
98
|
+
def _extract_keywords(self, goal: str) -> List[str]:
|
|
99
|
+
"""提取关键词"""
|
|
100
|
+
words = goal.split()
|
|
101
|
+
return [w.strip() for w in words if len(w.strip()) > 2]
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
"""匹配算法实现 - 两阶段匹配"""
|
|
2
|
+
|
|
3
|
+
from typing import List, Dict, Set
|
|
4
|
+
from .models import SearchQuery, SearchResult, MatchResult
|
|
5
|
+
from .registry import Registry
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class Matcher:
|
|
9
|
+
"""匹配器 - 两阶段匹配算法"""
|
|
10
|
+
|
|
11
|
+
def __init__(self, registry: Registry, min_score: float = 0.6):
|
|
12
|
+
self.registry = registry
|
|
13
|
+
self.min_score = min_score
|
|
14
|
+
|
|
15
|
+
def match(self, query: SearchQuery) -> SearchResult:
|
|
16
|
+
"""执行两阶段匹配"""
|
|
17
|
+
candidates = self._coarse_filter(query)
|
|
18
|
+
|
|
19
|
+
if not candidates:
|
|
20
|
+
return SearchResult(
|
|
21
|
+
query=query,
|
|
22
|
+
matches=[],
|
|
23
|
+
rejection_reason="未找到匹配的能力标签或关键词",
|
|
24
|
+
rejection_category="no_candidates_by_tag"
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
scored = self._fine_rank(candidates, query)
|
|
28
|
+
filtered = [m for m in scored if m.score >= self.min_score]
|
|
29
|
+
|
|
30
|
+
if not filtered:
|
|
31
|
+
rejection = self._analyze_rejection(candidates, query)
|
|
32
|
+
return SearchResult(
|
|
33
|
+
query=query,
|
|
34
|
+
matches=[],
|
|
35
|
+
rejection_reason=rejection["reason"],
|
|
36
|
+
rejection_category=rejection["category"]
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
return SearchResult(
|
|
40
|
+
query=query,
|
|
41
|
+
matches=filtered[:3],
|
|
42
|
+
rejection_reason=None,
|
|
43
|
+
rejection_category=None
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
def _coarse_filter(self, query: SearchQuery) -> List[str]:
|
|
47
|
+
"""阶段 1: 粗筛 - 倒排索引命中"""
|
|
48
|
+
unit_ids = set()
|
|
49
|
+
|
|
50
|
+
if query.tags:
|
|
51
|
+
for tag in query.tags:
|
|
52
|
+
unit_ids.update(self.registry.search_by_tag(tag))
|
|
53
|
+
|
|
54
|
+
if query.keywords:
|
|
55
|
+
for kw in query.keywords:
|
|
56
|
+
unit_ids.update(self.registry.search_by_keyword(kw))
|
|
57
|
+
|
|
58
|
+
if query.ide:
|
|
59
|
+
ide_units = set(self.registry.search_by_ide(query.ide))
|
|
60
|
+
if unit_ids:
|
|
61
|
+
unit_ids &= ide_units
|
|
62
|
+
else:
|
|
63
|
+
unit_ids = ide_units
|
|
64
|
+
|
|
65
|
+
return list(unit_ids)
|
|
66
|
+
|
|
67
|
+
def _fine_rank(self, unit_ids: List[str], query: SearchQuery) -> List[MatchResult]:
|
|
68
|
+
"""阶段 2: 精排 - 计算置信度分数"""
|
|
69
|
+
results = []
|
|
70
|
+
|
|
71
|
+
for uid in unit_ids:
|
|
72
|
+
unit = self.registry.get_unit(uid)
|
|
73
|
+
if not unit:
|
|
74
|
+
continue
|
|
75
|
+
|
|
76
|
+
package = self.registry.get_package(unit.package_id)
|
|
77
|
+
if not package:
|
|
78
|
+
continue
|
|
79
|
+
|
|
80
|
+
score = 0.0
|
|
81
|
+
reasons = []
|
|
82
|
+
warnings = []
|
|
83
|
+
|
|
84
|
+
if query.tags:
|
|
85
|
+
matched_tags = set(t.lower() for t in query.tags) & set(t.lower() for t in unit.capability_tags)
|
|
86
|
+
if matched_tags:
|
|
87
|
+
tag_coverage = len(matched_tags) / len(query.tags)
|
|
88
|
+
score += tag_coverage * 0.6
|
|
89
|
+
reasons.append(f"匹配标签: {', '.join(matched_tags)}")
|
|
90
|
+
|
|
91
|
+
if query.keywords:
|
|
92
|
+
matched_kw = set(kw.lower() for kw in query.keywords) & set(kw.lower() for kw in unit.keywords)
|
|
93
|
+
if matched_kw:
|
|
94
|
+
kw_score = len(matched_kw) / len(query.keywords)
|
|
95
|
+
score += kw_score * 0.3
|
|
96
|
+
reasons.append(f"匹配关键词: {', '.join(matched_kw)}")
|
|
97
|
+
|
|
98
|
+
if query.ide:
|
|
99
|
+
if query.ide.lower() in [ide.lower() for ide in unit.ide_support]:
|
|
100
|
+
score += 0.1
|
|
101
|
+
reasons.append(f"支持 {query.ide}")
|
|
102
|
+
else:
|
|
103
|
+
score *= 0.2
|
|
104
|
+
warnings.append(f"不支持 {query.ide}(仅支持 {', '.join(unit.ide_support)})")
|
|
105
|
+
|
|
106
|
+
if query.env and unit.conflicts:
|
|
107
|
+
if query.env.lower() in unit.conflicts.lower():
|
|
108
|
+
score *= 0.3
|
|
109
|
+
warnings.append(f"可能与 {query.env} 环境冲突")
|
|
110
|
+
|
|
111
|
+
if query.constraints:
|
|
112
|
+
for constraint in query.constraints:
|
|
113
|
+
if "no-network" in constraint.lower() and "network" in unit.description.lower():
|
|
114
|
+
score *= 0.5
|
|
115
|
+
warnings.append("可能需要网络访问,与约束冲突")
|
|
116
|
+
|
|
117
|
+
results.append(MatchResult(
|
|
118
|
+
unit=unit,
|
|
119
|
+
package=package,
|
|
120
|
+
score=score,
|
|
121
|
+
reasons=reasons,
|
|
122
|
+
warnings=warnings if warnings else None
|
|
123
|
+
))
|
|
124
|
+
|
|
125
|
+
results.sort(key=lambda x: x.score, reverse=True)
|
|
126
|
+
return results
|
|
127
|
+
|
|
128
|
+
def _analyze_rejection(self, candidates: List[str], query: SearchQuery) -> Dict:
|
|
129
|
+
"""分析拒绝原因"""
|
|
130
|
+
if not candidates:
|
|
131
|
+
return {
|
|
132
|
+
"reason": "未找到匹配的能力标签或关键词",
|
|
133
|
+
"category": "no_candidates_by_tag"
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if query.ide:
|
|
137
|
+
ide_supported = False
|
|
138
|
+
for uid in candidates:
|
|
139
|
+
unit = self.registry.get_unit(uid)
|
|
140
|
+
if unit and query.ide.lower() in [ide.lower() for ide in unit.ide_support]:
|
|
141
|
+
ide_supported = True
|
|
142
|
+
break
|
|
143
|
+
|
|
144
|
+
if not ide_supported:
|
|
145
|
+
return {
|
|
146
|
+
"reason": f"找到候选能力,但均不支持 {query.ide}",
|
|
147
|
+
"category": "candidates_but_no_ide_support"
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if query.env or query.constraints:
|
|
151
|
+
return {
|
|
152
|
+
"reason": "找到候选能力,但均不满足环境或约束条件",
|
|
153
|
+
"category": "candidates_but_incompatible_env"
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return {
|
|
157
|
+
"reason": "候选能力置信度不足(< 60%),建议补充更多需求信息",
|
|
158
|
+
"category": "insufficient_info"
|
|
159
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
"""skill-finder 核心数据结构定义"""
|
|
2
|
+
|
|
3
|
+
from typing import Literal, Optional, List, Dict
|
|
4
|
+
from pydantic import BaseModel, Field
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class InstallerSpec(BaseModel):
|
|
8
|
+
"""安装器规范"""
|
|
9
|
+
method: Literal["git", "pip", "npm", "release_asset"]
|
|
10
|
+
auto_install_cmd: str = Field(..., description="自动安装执行的命令(可多行)")
|
|
11
|
+
manual_install_cmd: str = Field(..., description="手动安装给用户的命令(可多行)")
|
|
12
|
+
uninstall_cmd: Optional[str] = Field(None, description="卸载命令(可选)")
|
|
13
|
+
notes: Optional[str] = Field(None, description="安装注意事项")
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class SourceSpec(BaseModel):
|
|
17
|
+
"""源信息"""
|
|
18
|
+
type: Literal["git", "pip", "npm", "release_asset"]
|
|
19
|
+
repo: Optional[str] = None
|
|
20
|
+
url: Optional[str] = None
|
|
21
|
+
package: Optional[str] = None
|
|
22
|
+
ref: Optional[str] = None
|
|
23
|
+
tag: Optional[str] = None
|
|
24
|
+
commit: Optional[str] = None
|
|
25
|
+
version: Optional[str] = None
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class DocsSpec(BaseModel):
|
|
29
|
+
"""文档链接"""
|
|
30
|
+
readme: str = Field(..., description="README 链接(必须)")
|
|
31
|
+
homepage: Optional[str] = None
|
|
32
|
+
issues: Optional[str] = None
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class SkillPackage(BaseModel):
|
|
36
|
+
"""Package 定义(安装载体)"""
|
|
37
|
+
package_id: str = Field(..., description="唯一标识:gh:owner/repo | pip:pkg | npm:pkg")
|
|
38
|
+
name: str
|
|
39
|
+
description: str
|
|
40
|
+
source: SourceSpec
|
|
41
|
+
docs: DocsSpec
|
|
42
|
+
supported_ides: List[str] = Field(..., description="包级 IDE 支持")
|
|
43
|
+
install: InstallerSpec
|
|
44
|
+
units: List[str] = Field(..., description="包含的 unit_id 列表")
|
|
45
|
+
trust_level: Optional[Literal["trusted", "experimental", "deprecated"]] = "trusted"
|
|
46
|
+
notes: Optional[str] = None
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class EntrypointSpec(BaseModel):
|
|
50
|
+
"""使用入口"""
|
|
51
|
+
command: str = Field(..., description="原生命令")
|
|
52
|
+
args: Optional[str] = Field(None, description="参数说明")
|
|
53
|
+
cwd: Optional[str] = Field(None, description="工作目录要求")
|
|
54
|
+
examples: Optional[List[str]] = Field(None, description="使用示例")
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class SkillUnit(BaseModel):
|
|
58
|
+
"""Unit 定义(能力单元,匹配维度)"""
|
|
59
|
+
unit_id: str = Field(..., description="唯一标识:gh:owner/repo#skill-a")
|
|
60
|
+
package_id: str = Field(..., description="所属 package")
|
|
61
|
+
name: str
|
|
62
|
+
description: str
|
|
63
|
+
capability_tags: List[str] = Field(..., description="能力标签")
|
|
64
|
+
keywords: List[str] = Field(..., description="关键词")
|
|
65
|
+
ide_support: List[str] = Field(..., description="IDE 支持列表")
|
|
66
|
+
entrypoints: List[EntrypointSpec] = Field(..., description="使用入口")
|
|
67
|
+
docs_override: Optional[DocsSpec] = None
|
|
68
|
+
usage_notes: Optional[str] = Field(None, description="10-20 行微摘要")
|
|
69
|
+
conflicts: Optional[str] = Field(None, description="已知冲突/注意事项")
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
class SearchQuery(BaseModel):
|
|
73
|
+
"""搜索查询"""
|
|
74
|
+
goal: str
|
|
75
|
+
input_type: Optional[str] = None
|
|
76
|
+
output_type: Optional[str] = None
|
|
77
|
+
env: Optional[str] = None
|
|
78
|
+
ide: Optional[str] = None
|
|
79
|
+
constraints: Optional[List[str]] = None
|
|
80
|
+
tags: Optional[List[str]] = None
|
|
81
|
+
keywords: Optional[List[str]] = None
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
class MatchResult(BaseModel):
|
|
85
|
+
"""匹配结果"""
|
|
86
|
+
unit: SkillUnit
|
|
87
|
+
package: SkillPackage
|
|
88
|
+
score: float
|
|
89
|
+
reasons: List[str] = Field(..., description="匹配原因")
|
|
90
|
+
warnings: Optional[List[str]] = None
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
class SearchResult(BaseModel):
|
|
94
|
+
"""搜索结果"""
|
|
95
|
+
query: SearchQuery
|
|
96
|
+
matches: List[MatchResult]
|
|
97
|
+
rejection_reason: Optional[str] = None
|
|
98
|
+
rejection_category: Optional[Literal[
|
|
99
|
+
"no_candidates_by_tag",
|
|
100
|
+
"candidates_but_no_ide_support",
|
|
101
|
+
"candidates_but_incompatible_env",
|
|
102
|
+
"insufficient_info"
|
|
103
|
+
]] = None
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
class InstallRecord(BaseModel):
|
|
107
|
+
"""安装记录"""
|
|
108
|
+
timestamp: str
|
|
109
|
+
unit_id: str
|
|
110
|
+
package_id: str
|
|
111
|
+
install_mode: Literal["auto", "manual"]
|
|
112
|
+
executed: bool
|
|
113
|
+
method: str
|
|
114
|
+
commands: str
|
|
115
|
+
result: Literal["success", "failed", "skipped"]
|
|
116
|
+
error_summary: Optional[str] = None
|
|
117
|
+
docs_links: DocsSpec
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"""被动推荐接口 - 供 skill-creator 调用"""
|
|
2
|
+
|
|
3
|
+
from typing import Optional
|
|
4
|
+
from .models import SearchQuery, SearchResult
|
|
5
|
+
from .registry import Registry
|
|
6
|
+
from .matcher import Matcher
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class Recommender:
|
|
10
|
+
"""推荐器 - 被动推荐接口"""
|
|
11
|
+
|
|
12
|
+
def __init__(self, registry: Optional[Registry] = None, min_score: float = 0.7):
|
|
13
|
+
"""
|
|
14
|
+
初始化推荐器
|
|
15
|
+
|
|
16
|
+
Args:
|
|
17
|
+
registry: Registry 实例(可选)
|
|
18
|
+
min_score: 最小置信度(推荐模式使用更高阈值 0.7)
|
|
19
|
+
"""
|
|
20
|
+
if registry is None:
|
|
21
|
+
registry = Registry()
|
|
22
|
+
|
|
23
|
+
self.registry = registry
|
|
24
|
+
self.matcher = Matcher(registry, min_score=min_score)
|
|
25
|
+
|
|
26
|
+
def recommend(self, query: SearchQuery) -> SearchResult:
|
|
27
|
+
"""
|
|
28
|
+
推荐第三方 unit(高置信度)
|
|
29
|
+
|
|
30
|
+
Args:
|
|
31
|
+
query: 搜索查询
|
|
32
|
+
|
|
33
|
+
Returns:
|
|
34
|
+
SearchResult: 仅返回高置信度结果(>= 0.7),否则返回空
|
|
35
|
+
"""
|
|
36
|
+
result = self.matcher.match(query)
|
|
37
|
+
|
|
38
|
+
if result.matches and result.matches[0].score >= 0.7:
|
|
39
|
+
return SearchResult(
|
|
40
|
+
query=query,
|
|
41
|
+
matches=result.matches[:1],
|
|
42
|
+
rejection_reason=None,
|
|
43
|
+
rejection_category=None
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
return SearchResult(
|
|
47
|
+
query=query,
|
|
48
|
+
matches=[],
|
|
49
|
+
rejection_reason="未找到高置信度推荐(< 70%)",
|
|
50
|
+
rejection_category="insufficient_info"
|
|
51
|
+
)
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
"""Registry 加载与索引查询"""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Dict, List, Optional
|
|
6
|
+
from .models import SkillPackage, SkillUnit
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class Registry:
|
|
10
|
+
"""Registry 管理器 - 加载 packages/units 和索引"""
|
|
11
|
+
|
|
12
|
+
def __init__(self, registry_root: Optional[Path] = None):
|
|
13
|
+
if registry_root is None:
|
|
14
|
+
registry_root = Path(__file__).parent.parent.parent / "data" / "third_party"
|
|
15
|
+
|
|
16
|
+
self.registry_root = Path(registry_root)
|
|
17
|
+
self.packages_dir = self.registry_root / "packages"
|
|
18
|
+
self.units_dir = self.registry_root / "units"
|
|
19
|
+
self.indexes_dir = self.registry_root / "indexes"
|
|
20
|
+
|
|
21
|
+
self._packages_cache: Dict[str, SkillPackage] = {}
|
|
22
|
+
self._units_cache: Dict[str, SkillUnit] = {}
|
|
23
|
+
self.indexes: Dict[str, dict] = {}
|
|
24
|
+
|
|
25
|
+
self._load_indexes()
|
|
26
|
+
|
|
27
|
+
def _load_indexes(self):
|
|
28
|
+
"""加载所有索引文件"""
|
|
29
|
+
if not self.indexes_dir.exists():
|
|
30
|
+
raise FileNotFoundError(f"索引目录不存在: {self.indexes_dir}")
|
|
31
|
+
|
|
32
|
+
index_files = {
|
|
33
|
+
"by_tag": "units.by_tag.json",
|
|
34
|
+
"by_keyword": "units.by_keyword.json",
|
|
35
|
+
"by_ide": "units.by_ide.json",
|
|
36
|
+
"packages_all": "packages.all.json",
|
|
37
|
+
"units_all": "units.all.json",
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
for key, filename in index_files.items():
|
|
41
|
+
index_path = self.indexes_dir / filename
|
|
42
|
+
if index_path.exists():
|
|
43
|
+
with open(index_path, 'r', encoding='utf-8') as f:
|
|
44
|
+
self.indexes[key] = json.load(f)
|
|
45
|
+
else:
|
|
46
|
+
self.indexes[key] = {} if key != "packages_all" and key != "units_all" else []
|
|
47
|
+
|
|
48
|
+
def get_package(self, package_id: str) -> Optional[SkillPackage]:
|
|
49
|
+
"""获取 package(带缓存)"""
|
|
50
|
+
if package_id in self._packages_cache:
|
|
51
|
+
return self._packages_cache[package_id]
|
|
52
|
+
|
|
53
|
+
sanitized_id = package_id.replace(":", "_").replace("/", "_")
|
|
54
|
+
package_path = self.packages_dir / f"{sanitized_id}.json"
|
|
55
|
+
|
|
56
|
+
if not package_path.exists():
|
|
57
|
+
return None
|
|
58
|
+
|
|
59
|
+
try:
|
|
60
|
+
with open(package_path, 'r', encoding='utf-8') as f:
|
|
61
|
+
data = json.load(f)
|
|
62
|
+
package = SkillPackage(**data)
|
|
63
|
+
self._packages_cache[package_id] = package
|
|
64
|
+
return package
|
|
65
|
+
except Exception as e:
|
|
66
|
+
print(f"加载 package {package_id} 失败: {e}")
|
|
67
|
+
return None
|
|
68
|
+
|
|
69
|
+
def get_unit(self, unit_id: str) -> Optional[SkillUnit]:
|
|
70
|
+
"""获取 unit(带缓存)"""
|
|
71
|
+
if unit_id in self._units_cache:
|
|
72
|
+
return self._units_cache[unit_id]
|
|
73
|
+
|
|
74
|
+
sanitized_id = unit_id.replace(":", "_").replace("/", "_")
|
|
75
|
+
unit_path = self.units_dir / f"{sanitized_id}.json"
|
|
76
|
+
|
|
77
|
+
if not unit_path.exists():
|
|
78
|
+
return None
|
|
79
|
+
|
|
80
|
+
try:
|
|
81
|
+
with open(unit_path, 'r', encoding='utf-8') as f:
|
|
82
|
+
data = json.load(f)
|
|
83
|
+
unit = SkillUnit(**data)
|
|
84
|
+
self._units_cache[unit_id] = unit
|
|
85
|
+
return unit
|
|
86
|
+
except Exception as e:
|
|
87
|
+
print(f"加载 unit {unit_id} 失败: {e}")
|
|
88
|
+
return None
|
|
89
|
+
|
|
90
|
+
def search_by_tag(self, tag: str) -> List[str]:
|
|
91
|
+
"""通过 tag 搜索 unit_id 列表"""
|
|
92
|
+
return self.indexes.get("by_tag", {}).get(tag.lower(), [])
|
|
93
|
+
|
|
94
|
+
def search_by_keyword(self, keyword: str) -> List[str]:
|
|
95
|
+
"""通过 keyword 搜索 unit_id 列表"""
|
|
96
|
+
return self.indexes.get("by_keyword", {}).get(keyword.lower(), [])
|
|
97
|
+
|
|
98
|
+
def search_by_ide(self, ide: str) -> List[str]:
|
|
99
|
+
"""通过 IDE 搜索 unit_id 列表"""
|
|
100
|
+
return self.indexes.get("by_ide", {}).get(ide.lower(), [])
|
|
101
|
+
|
|
102
|
+
def get_all_packages(self) -> List[str]:
|
|
103
|
+
"""获取所有 package_id"""
|
|
104
|
+
return self.indexes.get("packages_all", [])
|
|
105
|
+
|
|
106
|
+
def get_all_units(self) -> List[str]:
|
|
107
|
+
"""获取所有 unit_id"""
|
|
108
|
+
return self.indexes.get("units_all", [])
|