@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 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
- 1. 依赖管理:项目使用 Go Modules,可以运行 `go mod tidy` 整理依赖。
160
- 2. 添加新命令:
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
- 3. 测试:运行 `make test` 进行单元测试。
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sokeai/cli",
3
- "version": "1.0.20",
3
+ "version": "1.0.23",
4
4
  "description": "授客AI官方CLI工具 - 支持AI Agent Skills",
5
5
  "bin": {
6
6
  "soke-cli": "scripts/run.js"
@@ -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
 
@@ -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 = ['soke-shared', 'soke-exam'];
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
- const skillNames = ['soke-shared', 'soke-exam'];
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
- upsertSkillRegistryEntry(registry, {
176
- id: 'skill:soke-exam',
177
- name: 'soke-exam',
178
- displayName: '授客考试管理',
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
- upsertSkillRegistryEntry(registry, {
194
- id: 'skill:soke-shared',
195
- name: 'soke-shared',
196
- displayName: 'soke-shared 共享规则',
197
- description: '授客CLI共享基础:配置、登录、权限管理、错误处理、安全规则。',
198
- source: { type: defaultSourceType, slug: '', url: '' },
199
- install: {
200
- path: path.join(workclawSkillInstallDir, 'soke-shared'),
201
- installedAt: '',
202
- updatedAt: '',
203
- version: '1.0.0'
204
- },
205
- state: { enabled: true, health: 'ok', lastError: '' },
206
- runtime: { supported: ['openclaw'], enabled: ['openclaw'], primary: 'openclaw' },
207
- security: { riskLevel: 'normal', requiresApproval: false },
208
- metadata: { emoji: '🔧', homepage: '', requires: {} }
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 ""