@sokeai/cli 1.0.20 → 1.0.23
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +67 -4
- package/package.json +1 -1
- package/scripts/e2e-test.sh +38 -0
- package/scripts/install.js +126 -36
- package/scripts/link-skills.sh +203 -0
- package/scripts/local-test.js +503 -0
- package/scripts/local-test.sh +178 -0
- package/scripts/test-auto-detect.js +125 -0
- package/skills/soke-learning-profile/SKILL.md +223 -0
package/README.md
CHANGED
|
@@ -15,9 +15,10 @@
|
|
|
15
15
|
|
|
16
16
|
3. **业务模块快捷命令**
|
|
17
17
|
- 针对授客AI核心业务场景提供了丰富的快捷命令集:
|
|
18
|
-
- **通讯录 (contact)**:
|
|
18
|
+
- **通讯录 (contact)**: 部门与用户查询、管理、搜索等。
|
|
19
19
|
- **课程 (course)**: 课程列表、分类、学习记录、人脸识别记录等。
|
|
20
20
|
- **考试 (exam)**: 考试列表、分类、考试成绩与记录查询。
|
|
21
|
+
- **学习档案 (learning-profile)**: 学员学习档案查询、学习情况统计。
|
|
21
22
|
- **学习地图 (learning-map)**: 学习地图、阶段、任务查询与分配。
|
|
22
23
|
- **证书 (certificate)**: 证书发放记录、分类等。
|
|
23
24
|
- **学分 (credit) & 积分 (point)**: 学分/积分日志及用户情况查询。
|
|
@@ -71,6 +72,8 @@ AI Agent 会自动:
|
|
|
71
72
|
|
|
72
73
|
- **soke-shared**: 配置初始化、用户认证、权限处理等基础功能
|
|
73
74
|
- **soke-exam**: 考试管理(查询考试、考试成绩、考试分类)
|
|
75
|
+
- **soke-course**: 课程管理(查询课程、课程分类、学习记录)
|
|
76
|
+
- **soke-learning-profile**: 学习档案查询(查询学员学习档案、学习情况统计)
|
|
74
77
|
- 更多业务模块的 Skills 正在开发中...
|
|
75
78
|
|
|
76
79
|
详细文档:[skills/README.md](skills/README.md)
|
|
@@ -156,8 +159,68 @@ soke-cli api POST /some/endpoint --data '{"key": "value"}'
|
|
|
156
159
|
|
|
157
160
|
## 开发与贡献
|
|
158
161
|
|
|
159
|
-
|
|
160
|
-
|
|
162
|
+
### 快速开始本地测试
|
|
163
|
+
|
|
164
|
+
```bash
|
|
165
|
+
# 1. 编译、安装到全局、运行测试(一条命令)
|
|
166
|
+
bash ./scripts/local-test.sh
|
|
167
|
+
# 提示时输入 'y' 更新全局安装
|
|
168
|
+
|
|
169
|
+
# 2. 链接 Skills 到 Claude(首次需要)
|
|
170
|
+
bash ./scripts/link-skills.sh
|
|
171
|
+
# 选择 'all' 链接到所有目录
|
|
172
|
+
|
|
173
|
+
# 3. 在 AI Agent 中测试
|
|
174
|
+
# 打开 Claude Code,输入:"查询张三的学习档案"
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
**📖 详细指南:** [docs/LOCAL_TESTING.md](docs/LOCAL_TESTING.md)
|
|
178
|
+
|
|
179
|
+
### 本地开发测试
|
|
180
|
+
|
|
181
|
+
1. **一键测试和安装**
|
|
182
|
+
```bash
|
|
183
|
+
# 编译 -> 安装到全局 -> 运行测试(一条命令完成所有步骤)
|
|
184
|
+
bash ./scripts/local-test.sh
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
这个脚本会:
|
|
188
|
+
- 编译项目
|
|
189
|
+
- 检查本地版本功能
|
|
190
|
+
- 检测全局安装状态
|
|
191
|
+
- 提示是否需要更新全局安装(会自动备份)
|
|
192
|
+
- 运行功能测试验证
|
|
193
|
+
|
|
194
|
+
2. **运行完整 E2E 测试**
|
|
195
|
+
```bash
|
|
196
|
+
# 测试所有模块
|
|
197
|
+
bash ./scripts/e2e-test.sh
|
|
198
|
+
|
|
199
|
+
# 测试特定模块
|
|
200
|
+
bash ./scripts/e2e-test.sh learning-profile
|
|
201
|
+
bash ./scripts/e2e-test.sh contact
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
3. **依赖管理**
|
|
205
|
+
|
|
206
|
+
项目使用 Go Modules,可以运行 `go mod tidy` 整理依赖。
|
|
207
|
+
|
|
208
|
+
4. **添加新命令**
|
|
161
209
|
- 业务接口建议在 `shortcuts/` 目录下添加对应的结构定义。
|
|
162
210
|
- 基础功能可在 `cmd/` 下新建对应包并在 `cmd/root.go` 中注册。
|
|
163
|
-
|
|
211
|
+
- 为新模块创建 Skill 文档:`skills/<module-name>/SKILL.md`
|
|
212
|
+
|
|
213
|
+
5. **完整开发流程**
|
|
214
|
+
```bash
|
|
215
|
+
# 1. 修改代码后,运行本地测试(会提示是否更新全局安装)
|
|
216
|
+
bash ./scripts/local-test.sh
|
|
217
|
+
|
|
218
|
+
# 2. 运行完整 E2E 测试
|
|
219
|
+
bash ./scripts/e2e-test.sh
|
|
220
|
+
|
|
221
|
+
# 3. 测试 Skills(需要先更新全局安装)
|
|
222
|
+
npx skills add liuchenlong1111/soke-cli -y -g
|
|
223
|
+
|
|
224
|
+
# 4. 在 AI Agent 中测试自然语言交互
|
|
225
|
+
# 例如:"查询张三的学习档案"
|
|
226
|
+
```
|
package/package.json
CHANGED
package/scripts/e2e-test.sh
CHANGED
|
@@ -42,6 +42,35 @@ if [ ! -f "./soke-cli" ]; then
|
|
|
42
42
|
echo ""
|
|
43
43
|
fi
|
|
44
44
|
|
|
45
|
+
# 检查全局安装的 CLI 版本
|
|
46
|
+
echo -e "${YELLOW}检查全局 CLI 安装...${NC}"
|
|
47
|
+
GLOBAL_CLI=$(which soke-cli 2>/dev/null)
|
|
48
|
+
if [ -n "$GLOBAL_CLI" ]; then
|
|
49
|
+
echo -e " 全局 CLI 路径: ${BLUE}${GLOBAL_CLI}${NC}"
|
|
50
|
+
|
|
51
|
+
# 检查全局版本是否支持 learning-profile
|
|
52
|
+
if ! $GLOBAL_CLI learning-profile --help &>/dev/null; then
|
|
53
|
+
echo -e "${YELLOW} 警告: 全局 CLI 不支持 learning-profile 模块${NC}"
|
|
54
|
+
echo -e "${YELLOW} 需要更新全局安装以支持 skill 测试${NC}"
|
|
55
|
+
echo ""
|
|
56
|
+
echo -e "${YELLOW}是否要将本地编译版本安装到全局? (y/n)${NC}"
|
|
57
|
+
read -r INSTALL_GLOBAL
|
|
58
|
+
if [ "$INSTALL_GLOBAL" = "y" ] || [ "$INSTALL_GLOBAL" = "Y" ]; then
|
|
59
|
+
echo -e "${YELLOW}安装到全局...${NC}"
|
|
60
|
+
sudo cp ./soke-cli $GLOBAL_CLI
|
|
61
|
+
echo -e "${GREEN}✓ 全局 CLI 已更新${NC}"
|
|
62
|
+
else
|
|
63
|
+
echo -e "${YELLOW}跳过全局安装,将仅测试本地版本${NC}"
|
|
64
|
+
fi
|
|
65
|
+
else
|
|
66
|
+
echo -e "${GREEN}✓ 全局 CLI 支持 learning-profile 模块${NC}"
|
|
67
|
+
fi
|
|
68
|
+
else
|
|
69
|
+
echo -e "${YELLOW} 未找到全局 CLI 安装${NC}"
|
|
70
|
+
echo -e "${YELLOW} Skill 测试需要全局安装 soke-cli${NC}"
|
|
71
|
+
fi
|
|
72
|
+
echo ""
|
|
73
|
+
|
|
45
74
|
# 检查是否已登录
|
|
46
75
|
echo -e "${YELLOW}检查登录状态...${NC}"
|
|
47
76
|
if ! ./soke-cli config show &>/dev/null; then
|
|
@@ -99,6 +128,15 @@ if should_test_module "contact"; then
|
|
|
99
128
|
test_command "contact" "+list-lectors" "" "获取讲师列表"
|
|
100
129
|
test_command "contact" "+list-groups" "--start-time 1672502400000 --end-time 1704038400000" "获取用户组列表"
|
|
101
130
|
test_command "contact" "+search-user" "--dept-user-name 测试" "搜索用户"
|
|
131
|
+
test_command "contact" "+search-dept" "--dept-name 测试" "搜索部门"
|
|
132
|
+
echo ""
|
|
133
|
+
fi
|
|
134
|
+
|
|
135
|
+
# ==================== Learning Profile 模块 ====================
|
|
136
|
+
if should_test_module "learning-profile"; then
|
|
137
|
+
echo -e "${YELLOW}[Learning Profile 模块]${NC}"
|
|
138
|
+
test_command "learning-profile" "+list" "--offset 0 --page-size 10" "获取学习档案列表"
|
|
139
|
+
test_command "learning-profile" "+list" "--is-new 1 --page-size 5" "获取新员工学习档案"
|
|
102
140
|
echo ""
|
|
103
141
|
fi
|
|
104
142
|
|
package/scripts/install.js
CHANGED
|
@@ -89,13 +89,31 @@ function detectSokeclawWorkspaceSkillsDirs() {
|
|
|
89
89
|
return dirs;
|
|
90
90
|
}
|
|
91
91
|
|
|
92
|
+
/**
|
|
93
|
+
* 自动检测 skills 目录中的所有 skill
|
|
94
|
+
* 只包含以 'soke-' 开头的目录
|
|
95
|
+
*/
|
|
96
|
+
function detectSkillNames(packagedSkillsDir) {
|
|
97
|
+
if (!fs.existsSync(packagedSkillsDir)) return [];
|
|
98
|
+
|
|
99
|
+
try {
|
|
100
|
+
const entries = fs.readdirSync(packagedSkillsDir, { withFileTypes: true });
|
|
101
|
+
return entries
|
|
102
|
+
.filter(entry => entry.isDirectory() && entry.name.startsWith('soke-'))
|
|
103
|
+
.map(entry => entry.name)
|
|
104
|
+
.sort(); // 排序确保一致性
|
|
105
|
+
} catch (_) {
|
|
106
|
+
return [];
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
92
110
|
function syncSkillsToSokeclawWorkspace() {
|
|
93
111
|
const packageRoot = path.join(__dirname, '..');
|
|
94
112
|
const packagedSkillsDir = path.join(packageRoot, 'skills');
|
|
95
113
|
if (!fs.existsSync(packagedSkillsDir)) return;
|
|
96
114
|
|
|
97
115
|
const targetDirs = detectSokeclawWorkspaceSkillsDirs();
|
|
98
|
-
const skillNames =
|
|
116
|
+
const skillNames = detectSkillNames(packagedSkillsDir);
|
|
99
117
|
|
|
100
118
|
for (const targetDir of targetDirs) {
|
|
101
119
|
try {
|
|
@@ -126,6 +144,76 @@ function upsertSkillRegistryEntry(registry, entry) {
|
|
|
126
144
|
registry.skills.push(entry);
|
|
127
145
|
}
|
|
128
146
|
|
|
147
|
+
/**
|
|
148
|
+
* 从 SKILL.md 文件中解析元数据
|
|
149
|
+
*/
|
|
150
|
+
function parseSkillMetadata(skillDir) {
|
|
151
|
+
const skillMdPath = path.join(skillDir, 'SKILL.md');
|
|
152
|
+
if (!fs.existsSync(skillMdPath)) {
|
|
153
|
+
return null;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
try {
|
|
157
|
+
const content = fs.readFileSync(skillMdPath, 'utf8');
|
|
158
|
+
|
|
159
|
+
// 解析 frontmatter (YAML)
|
|
160
|
+
const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---/);
|
|
161
|
+
if (!frontmatterMatch) return null;
|
|
162
|
+
|
|
163
|
+
const frontmatter = frontmatterMatch[1];
|
|
164
|
+
const metadata = {};
|
|
165
|
+
|
|
166
|
+
// 解析 name
|
|
167
|
+
const nameMatch = frontmatter.match(/^name:\s*(.+)$/m);
|
|
168
|
+
if (nameMatch) metadata.name = nameMatch[1].trim();
|
|
169
|
+
|
|
170
|
+
// 解析 summary (用作 displayName)
|
|
171
|
+
const summaryMatch = frontmatter.match(/^summary:\s*(.+)$/m);
|
|
172
|
+
if (summaryMatch) metadata.summary = summaryMatch[1].trim();
|
|
173
|
+
|
|
174
|
+
// 解析 description
|
|
175
|
+
const descMatch = frontmatter.match(/^description:\s*["'](.+)["']$/m);
|
|
176
|
+
if (descMatch) {
|
|
177
|
+
metadata.description = descMatch[1].trim();
|
|
178
|
+
} else {
|
|
179
|
+
const descMatch2 = frontmatter.match(/^description:\s*(.+)$/m);
|
|
180
|
+
if (descMatch2) metadata.description = descMatch2[1].trim();
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// 解析 version
|
|
184
|
+
const versionMatch = frontmatter.match(/^version:\s*(.+)$/m);
|
|
185
|
+
if (versionMatch) metadata.version = versionMatch[1].trim();
|
|
186
|
+
|
|
187
|
+
// 解析 metadata.requires.bins
|
|
188
|
+
const binsMatch = frontmatter.match(/bins:\s*\[(.+?)\]/);
|
|
189
|
+
if (binsMatch) {
|
|
190
|
+
metadata.bins = binsMatch[1].split(',').map(b => b.trim().replace(/['"]/g, ''));
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
return metadata;
|
|
194
|
+
} catch (_) {
|
|
195
|
+
return null;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* 根据 skill 名称推断 emoji
|
|
201
|
+
*/
|
|
202
|
+
function inferSkillEmoji(skillName) {
|
|
203
|
+
const emojiMap = {
|
|
204
|
+
'soke-exam': '📝',
|
|
205
|
+
'soke-course': '📚',
|
|
206
|
+
'soke-shared': '🔧',
|
|
207
|
+
'soke-user': '👤',
|
|
208
|
+
'soke-contact': '📇',
|
|
209
|
+
'soke-department': '🏢',
|
|
210
|
+
'soke-approval': '✅',
|
|
211
|
+
'soke-attendance': '📅',
|
|
212
|
+
'soke-report': '📊'
|
|
213
|
+
};
|
|
214
|
+
return emojiMap[skillName] || '📦';
|
|
215
|
+
}
|
|
216
|
+
|
|
129
217
|
function syncSkillsToWorkclawRegistry() {
|
|
130
218
|
const homeDir = os.homedir();
|
|
131
219
|
const workclawRootDir = path.join(homeDir, '.workclaw');
|
|
@@ -165,48 +253,50 @@ function syncSkillsToWorkclawRegistry() {
|
|
|
165
253
|
? existingSkills[0].source.type
|
|
166
254
|
: 'local';
|
|
167
255
|
|
|
168
|
-
|
|
256
|
+
// 自动检测所有 skills
|
|
257
|
+
const skillNames = detectSkillNames(packagedSkillsDir);
|
|
258
|
+
|
|
259
|
+
// 复制所有 skills
|
|
169
260
|
for (const skillName of skillNames) {
|
|
170
261
|
const src = path.join(packagedSkillsDir, skillName);
|
|
171
262
|
const dest = path.join(workclawSkillInstallDir, skillName);
|
|
172
263
|
if (fs.existsSync(src)) copyDirRecursive(src, dest);
|
|
173
264
|
}
|
|
174
265
|
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
description: '授客考试管理:查询考试、考试分类、考试用户成绩、考试详情。',
|
|
180
|
-
source: { type: defaultSourceType, slug: '', url: '' },
|
|
181
|
-
install: {
|
|
182
|
-
path: path.join(workclawSkillInstallDir, 'soke-exam'),
|
|
183
|
-
installedAt: '',
|
|
184
|
-
updatedAt: '',
|
|
185
|
-
version: '1.0.0'
|
|
186
|
-
},
|
|
187
|
-
state: { enabled: true, health: 'ok', lastError: '' },
|
|
188
|
-
runtime: { supported: ['openclaw'], enabled: ['openclaw'], primary: 'openclaw' },
|
|
189
|
-
security: { riskLevel: 'normal', requiresApproval: false },
|
|
190
|
-
metadata: { emoji: '📝', homepage: '', requires: { bins: ['soke-cli'] } }
|
|
191
|
-
});
|
|
266
|
+
// 自动注册所有 skills
|
|
267
|
+
for (const skillName of skillNames) {
|
|
268
|
+
const skillDir = path.join(packagedSkillsDir, skillName);
|
|
269
|
+
const metadata = parseSkillMetadata(skillDir);
|
|
192
270
|
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
271
|
+
if (!metadata || !metadata.name) {
|
|
272
|
+
console.warn(`警告: 无法解析 ${skillName} 的元数据,跳过注册`);
|
|
273
|
+
continue;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
const displayName = metadata.summary || metadata.name;
|
|
277
|
+
const description = metadata.description || `${displayName} - 授客AI CLI工具`;
|
|
278
|
+
const version = metadata.version || '1.0.0';
|
|
279
|
+
const emoji = inferSkillEmoji(skillName);
|
|
280
|
+
const requires = metadata.bins ? { bins: metadata.bins } : {};
|
|
281
|
+
|
|
282
|
+
upsertSkillRegistryEntry(registry, {
|
|
283
|
+
id: `skill:${skillName}`,
|
|
284
|
+
name: skillName,
|
|
285
|
+
displayName: displayName,
|
|
286
|
+
description: description,
|
|
287
|
+
source: { type: defaultSourceType, slug: '', url: '' },
|
|
288
|
+
install: {
|
|
289
|
+
path: path.join(workclawSkillInstallDir, skillName),
|
|
290
|
+
installedAt: '',
|
|
291
|
+
updatedAt: '',
|
|
292
|
+
version: version
|
|
293
|
+
},
|
|
294
|
+
state: { enabled: true, health: 'ok', lastError: '' },
|
|
295
|
+
runtime: { supported: ['openclaw'], enabled: ['openclaw'], primary: 'openclaw' },
|
|
296
|
+
security: { riskLevel: 'normal', requiresApproval: false },
|
|
297
|
+
metadata: { emoji: emoji, homepage: '', requires: requires }
|
|
298
|
+
});
|
|
299
|
+
}
|
|
210
300
|
|
|
211
301
|
try {
|
|
212
302
|
fs.writeFileSync(registryPath, JSON.stringify(registry, null, 2));
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
|
|
3
|
+
# soke-cli Skills 本地测试脚本
|
|
4
|
+
# 将本地 skills 链接到 Claude 的 skills 目录进行测试
|
|
5
|
+
|
|
6
|
+
# 颜色输出
|
|
7
|
+
RED='\033[0;31m'
|
|
8
|
+
GREEN='\033[0;32m'
|
|
9
|
+
YELLOW='\033[1;33m'
|
|
10
|
+
BLUE='\033[0;34m'
|
|
11
|
+
NC='\033[0m'
|
|
12
|
+
|
|
13
|
+
echo -e "${BLUE}========================================${NC}"
|
|
14
|
+
echo -e "${BLUE} soke-cli Skills 本地测试${NC}"
|
|
15
|
+
echo -e "${BLUE}========================================${NC}"
|
|
16
|
+
echo ""
|
|
17
|
+
|
|
18
|
+
# 检查 skills 目录是否存在
|
|
19
|
+
if [ ! -d "./skills" ]; then
|
|
20
|
+
echo -e "${RED}错误: 未找到 ./skills 目录${NC}"
|
|
21
|
+
exit 1
|
|
22
|
+
fi
|
|
23
|
+
|
|
24
|
+
# 查找所有可能的 skills 目录
|
|
25
|
+
echo -e "${YELLOW}查找 Claude skills 目录...${NC}"
|
|
26
|
+
SKILLS_DIRS=(
|
|
27
|
+
"$HOME/.codex/skills"
|
|
28
|
+
"$HOME/.openclaw/skills"
|
|
29
|
+
"$HOME/.agents/skills"
|
|
30
|
+
"$HOME/.workclaw/skills"
|
|
31
|
+
"$HOME/.skills"
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
FOUND_DIRS=()
|
|
35
|
+
for dir in "${SKILLS_DIRS[@]}"; do
|
|
36
|
+
if [ -d "$dir" ]; then
|
|
37
|
+
FOUND_DIRS+=("$dir")
|
|
38
|
+
echo -e " ${GREEN}✓${NC} 找到: $dir"
|
|
39
|
+
fi
|
|
40
|
+
done
|
|
41
|
+
|
|
42
|
+
if [ ${#FOUND_DIRS[@]} -eq 0 ]; then
|
|
43
|
+
echo -e "${RED}错误: 未找到任何 Claude skills 目录${NC}"
|
|
44
|
+
echo -e "${YELLOW}提示: 请先运行 'npx skills add' 安装任意一个 skill 来初始化目录${NC}"
|
|
45
|
+
exit 1
|
|
46
|
+
fi
|
|
47
|
+
echo ""
|
|
48
|
+
|
|
49
|
+
# 选择要链接的目录
|
|
50
|
+
if [ ${#FOUND_DIRS[@]} -eq 1 ]; then
|
|
51
|
+
TARGET_DIR="${FOUND_DIRS[0]}"
|
|
52
|
+
echo -e "${BLUE}将链接到: ${TARGET_DIR}${NC}"
|
|
53
|
+
else
|
|
54
|
+
echo -e "${YELLOW}找到多个 skills 目录,请选择:${NC}"
|
|
55
|
+
for i in "${!FOUND_DIRS[@]}"; do
|
|
56
|
+
echo -e " $((i+1)). ${FOUND_DIRS[$i]}"
|
|
57
|
+
done
|
|
58
|
+
echo ""
|
|
59
|
+
|
|
60
|
+
# 如果有参数,使用参数作为选择
|
|
61
|
+
if [ -n "$1" ]; then
|
|
62
|
+
choice=$1
|
|
63
|
+
else
|
|
64
|
+
echo -n "请输入序号 (1-${#FOUND_DIRS[@]}) 或 'all' 链接到所有目录: "
|
|
65
|
+
read -r choice
|
|
66
|
+
fi
|
|
67
|
+
|
|
68
|
+
if [ "$choice" = "all" ]; then
|
|
69
|
+
echo -e "${BLUE}将链接到所有目录${NC}"
|
|
70
|
+
LINK_ALL=true
|
|
71
|
+
elif [[ "$choice" =~ ^[0-9]+$ ]] && [ "$choice" -ge 1 ] && [ "$choice" -le ${#FOUND_DIRS[@]} ]; then
|
|
72
|
+
TARGET_DIR="${FOUND_DIRS[$((choice-1))]}"
|
|
73
|
+
echo -e "${BLUE}已选择: ${TARGET_DIR}${NC}"
|
|
74
|
+
LINK_ALL=false
|
|
75
|
+
else
|
|
76
|
+
echo -e "${RED}无效的选择${NC}"
|
|
77
|
+
exit 1
|
|
78
|
+
fi
|
|
79
|
+
fi
|
|
80
|
+
echo ""
|
|
81
|
+
|
|
82
|
+
# 获取当前项目的绝对路径
|
|
83
|
+
PROJECT_DIR=$(pwd)
|
|
84
|
+
SOURCE_SKILLS_DIR="${PROJECT_DIR}/skills"
|
|
85
|
+
|
|
86
|
+
# 列出要链接的 skills
|
|
87
|
+
echo -e "${YELLOW}准备链接以下 skills:${NC}"
|
|
88
|
+
for skill_dir in "$SOURCE_SKILLS_DIR"/*; do
|
|
89
|
+
if [ -d "$skill_dir" ]; then
|
|
90
|
+
skill_name=$(basename "$skill_dir")
|
|
91
|
+
echo -e " - ${BLUE}${skill_name}${NC}"
|
|
92
|
+
fi
|
|
93
|
+
done
|
|
94
|
+
echo ""
|
|
95
|
+
|
|
96
|
+
# 确认操作
|
|
97
|
+
echo -e "${YELLOW}是否继续? (y/n)${NC}"
|
|
98
|
+
read -r CONFIRM
|
|
99
|
+
if [ "$CONFIRM" != "y" ] && [ "$CONFIRM" != "Y" ]; then
|
|
100
|
+
echo -e "${YELLOW}取消操作${NC}"
|
|
101
|
+
exit 0
|
|
102
|
+
fi
|
|
103
|
+
echo ""
|
|
104
|
+
|
|
105
|
+
# 创建符号链接
|
|
106
|
+
echo -e "${YELLOW}创建符号链接...${NC}"
|
|
107
|
+
SUCCESS_COUNT=0
|
|
108
|
+
SKIP_COUNT=0
|
|
109
|
+
|
|
110
|
+
# 如果选择链接到所有目录
|
|
111
|
+
if [ "$LINK_ALL" = true ]; then
|
|
112
|
+
for target_dir in "${FOUND_DIRS[@]}"; do
|
|
113
|
+
echo -e "${BLUE}链接到: ${target_dir}${NC}"
|
|
114
|
+
for skill_dir in "$SOURCE_SKILLS_DIR"/*; do
|
|
115
|
+
if [ -d "$skill_dir" ]; then
|
|
116
|
+
skill_name=$(basename "$skill_dir")
|
|
117
|
+
target_link="${target_dir}/${skill_name}"
|
|
118
|
+
|
|
119
|
+
if [ -L "$target_link" ]; then
|
|
120
|
+
current_target=$(readlink "$target_link")
|
|
121
|
+
if [ "$current_target" = "$skill_dir" ]; then
|
|
122
|
+
echo -e " ${GREEN}✓${NC} ${skill_name} (已存在)"
|
|
123
|
+
SKIP_COUNT=$((SKIP_COUNT + 1))
|
|
124
|
+
else
|
|
125
|
+
rm "$target_link"
|
|
126
|
+
ln -s "$skill_dir" "$target_link"
|
|
127
|
+
echo -e " ${GREEN}✓${NC} ${skill_name} (已更新)"
|
|
128
|
+
SUCCESS_COUNT=$((SUCCESS_COUNT + 1))
|
|
129
|
+
fi
|
|
130
|
+
elif [ -e "$target_link" ]; then
|
|
131
|
+
echo -e " ${RED}✗${NC} ${skill_name} (存在同名文件)"
|
|
132
|
+
else
|
|
133
|
+
ln -s "$skill_dir" "$target_link"
|
|
134
|
+
echo -e " ${GREEN}✓${NC} ${skill_name} (已链接)"
|
|
135
|
+
SUCCESS_COUNT=$((SUCCESS_COUNT + 1))
|
|
136
|
+
fi
|
|
137
|
+
fi
|
|
138
|
+
done
|
|
139
|
+
echo ""
|
|
140
|
+
done
|
|
141
|
+
else
|
|
142
|
+
# 链接到单个目录
|
|
143
|
+
for skill_dir in "$SOURCE_SKILLS_DIR"/*; do
|
|
144
|
+
if [ -d "$skill_dir" ]; then
|
|
145
|
+
skill_name=$(basename "$skill_dir")
|
|
146
|
+
target_link="${TARGET_DIR}/${skill_name}"
|
|
147
|
+
|
|
148
|
+
# 检查是否已存在
|
|
149
|
+
if [ -L "$target_link" ]; then
|
|
150
|
+
# 已存在符号链接,检查是否指向正确位置
|
|
151
|
+
current_target=$(readlink "$target_link")
|
|
152
|
+
if [ "$current_target" = "$skill_dir" ]; then
|
|
153
|
+
echo -e " ${GREEN}✓${NC} ${skill_name} (已存在,指向正确)"
|
|
154
|
+
SKIP_COUNT=$((SKIP_COUNT + 1))
|
|
155
|
+
else
|
|
156
|
+
echo -e " ${YELLOW}!${NC} ${skill_name} (已存在,但指向: ${current_target})"
|
|
157
|
+
echo -n " 是否覆盖? (y/n): "
|
|
158
|
+
read -r overwrite
|
|
159
|
+
if [ "$overwrite" = "y" ] || [ "$overwrite" = "Y" ]; then
|
|
160
|
+
rm "$target_link"
|
|
161
|
+
ln -s "$skill_dir" "$target_link"
|
|
162
|
+
echo -e " ${GREEN}✓${NC} 已覆盖"
|
|
163
|
+
SUCCESS_COUNT=$((SUCCESS_COUNT + 1))
|
|
164
|
+
else
|
|
165
|
+
echo -e " ${YELLOW}跳过${NC}"
|
|
166
|
+
SKIP_COUNT=$((SKIP_COUNT + 1))
|
|
167
|
+
fi
|
|
168
|
+
fi
|
|
169
|
+
elif [ -e "$target_link" ]; then
|
|
170
|
+
# 存在同名文件/目录
|
|
171
|
+
echo -e " ${RED}✗${NC} ${skill_name} (存在同名文件/目录)"
|
|
172
|
+
echo -e " 请手动删除: rm -rf ${target_link}"
|
|
173
|
+
else
|
|
174
|
+
# 创建新链接
|
|
175
|
+
ln -s "$skill_dir" "$target_link"
|
|
176
|
+
echo -e " ${GREEN}✓${NC} ${skill_name} (已链接)"
|
|
177
|
+
SUCCESS_COUNT=$((SUCCESS_COUNT + 1))
|
|
178
|
+
fi
|
|
179
|
+
fi
|
|
180
|
+
done
|
|
181
|
+
fi
|
|
182
|
+
echo ""
|
|
183
|
+
|
|
184
|
+
# 完成
|
|
185
|
+
echo -e "${GREEN}========================================${NC}"
|
|
186
|
+
echo -e "${GREEN} Skills 链接完成! ✓${NC}"
|
|
187
|
+
echo -e "${GREEN}========================================${NC}"
|
|
188
|
+
echo ""
|
|
189
|
+
echo -e "${BLUE}统计:${NC}"
|
|
190
|
+
echo -e " 新建链接: ${GREEN}${SUCCESS_COUNT}${NC}"
|
|
191
|
+
echo -e " 已存在: ${YELLOW}${SKIP_COUNT}${NC}"
|
|
192
|
+
echo ""
|
|
193
|
+
echo -e "${BLUE}下一步:${NC}"
|
|
194
|
+
echo -e " 1. 确保全局 CLI 已更新: ${BLUE}bash ./scripts/local-test.sh${NC}"
|
|
195
|
+
echo -e " 2. 在 Claude Code 中测试:"
|
|
196
|
+
echo -e " ${BLUE}\"查询张三的学习档案\"${NC}"
|
|
197
|
+
echo -e " ${BLUE}\"查询技术部的学员学习情况\"${NC}"
|
|
198
|
+
echo ""
|
|
199
|
+
echo -e "${YELLOW}提示:${NC}"
|
|
200
|
+
echo -e " - Skills 已链接到: ${TARGET_DIR}"
|
|
201
|
+
echo -e " - 修改本地 skills 文件会立即生效"
|
|
202
|
+
echo -e " - 删除链接: ${BLUE}rm ${TARGET_DIR}/soke-*${NC}"
|
|
203
|
+
echo ""
|