@sokeai/cli 1.0.21 → 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/link-skills.sh +203 -0
- package/scripts/local-test.js +503 -0
- package/scripts/local-test.sh +178 -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
|
|
|
@@ -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 ""
|
|
@@ -0,0 +1,503 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* 本地测试脚本 - 将 skills 分发到本地 AI Agent 环境
|
|
5
|
+
* 用于开发完 skill 后在本地测试,无需发布到 npm
|
|
6
|
+
*
|
|
7
|
+
* 使用方法:
|
|
8
|
+
* node scripts/local-test.js
|
|
9
|
+
* node scripts/local-test.js --skill soke-course
|
|
10
|
+
* node scripts/local-test.js --clean
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
const fs = require('fs');
|
|
14
|
+
const path = require('path');
|
|
15
|
+
const os = require('os');
|
|
16
|
+
|
|
17
|
+
// 颜色输出
|
|
18
|
+
const colors = {
|
|
19
|
+
reset: '\x1b[0m',
|
|
20
|
+
bright: '\x1b[1m',
|
|
21
|
+
red: '\x1b[31m',
|
|
22
|
+
green: '\x1b[32m',
|
|
23
|
+
yellow: '\x1b[33m',
|
|
24
|
+
blue: '\x1b[34m',
|
|
25
|
+
cyan: '\x1b[36m'
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
function log(message, color = 'reset') {
|
|
29
|
+
console.log(`${colors[color]}${message}${colors.reset}`);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function logSection(title) {
|
|
33
|
+
console.log('');
|
|
34
|
+
log(`${'='.repeat(60)}`, 'blue');
|
|
35
|
+
log(` ${title}`, 'bright');
|
|
36
|
+
log(`${'='.repeat(60)}`, 'blue');
|
|
37
|
+
console.log('');
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function logSuccess(message) {
|
|
41
|
+
log(`✅ ${message}`, 'green');
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function logError(message) {
|
|
45
|
+
log(`❌ ${message}`, 'red');
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function logWarning(message) {
|
|
49
|
+
log(`⚠️ ${message}`, 'yellow');
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function logInfo(message) {
|
|
53
|
+
log(`ℹ️ ${message}`, 'cyan');
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* 递归复制目录
|
|
58
|
+
*/
|
|
59
|
+
function copyDirRecursive(srcDir, destDir) {
|
|
60
|
+
if (!fs.existsSync(srcDir)) return false;
|
|
61
|
+
|
|
62
|
+
if (!fs.existsSync(destDir)) {
|
|
63
|
+
fs.mkdirSync(destDir, { recursive: true });
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const entries = fs.readdirSync(srcDir, { withFileTypes: true });
|
|
67
|
+
|
|
68
|
+
for (const entry of entries) {
|
|
69
|
+
const srcPath = path.join(srcDir, entry.name);
|
|
70
|
+
const destPath = path.join(destDir, entry.name);
|
|
71
|
+
|
|
72
|
+
if (entry.isDirectory()) {
|
|
73
|
+
copyDirRecursive(srcPath, destPath);
|
|
74
|
+
} else if (entry.isSymbolicLink()) {
|
|
75
|
+
try {
|
|
76
|
+
const linkTarget = fs.readlinkSync(srcPath);
|
|
77
|
+
try {
|
|
78
|
+
fs.unlinkSync(destPath);
|
|
79
|
+
} catch (_) {}
|
|
80
|
+
fs.symlinkSync(linkTarget, destPath);
|
|
81
|
+
} catch (_) {}
|
|
82
|
+
} else {
|
|
83
|
+
fs.copyFileSync(srcPath, destPath);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return true;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* 检测所有 soke-* skills
|
|
92
|
+
*/
|
|
93
|
+
function detectSkillNames(packagedSkillsDir) {
|
|
94
|
+
if (!fs.existsSync(packagedSkillsDir)) return [];
|
|
95
|
+
|
|
96
|
+
try {
|
|
97
|
+
const entries = fs.readdirSync(packagedSkillsDir, { withFileTypes: true });
|
|
98
|
+
return entries
|
|
99
|
+
.filter(entry => entry.isDirectory() && entry.name.startsWith('soke-'))
|
|
100
|
+
.map(entry => entry.name)
|
|
101
|
+
.sort();
|
|
102
|
+
} catch (_) {
|
|
103
|
+
return [];
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* 从 SKILL.md 解析元数据
|
|
109
|
+
*/
|
|
110
|
+
function parseSkillMetadata(skillDir) {
|
|
111
|
+
const skillMdPath = path.join(skillDir, 'SKILL.md');
|
|
112
|
+
if (!fs.existsSync(skillMdPath)) {
|
|
113
|
+
return null;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
try {
|
|
117
|
+
const content = fs.readFileSync(skillMdPath, 'utf8');
|
|
118
|
+
const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---/);
|
|
119
|
+
if (!frontmatterMatch) return null;
|
|
120
|
+
|
|
121
|
+
const frontmatter = frontmatterMatch[1];
|
|
122
|
+
const metadata = {};
|
|
123
|
+
|
|
124
|
+
const nameMatch = frontmatter.match(/^name:\s*(.+)$/m);
|
|
125
|
+
if (nameMatch) metadata.name = nameMatch[1].trim();
|
|
126
|
+
|
|
127
|
+
const summaryMatch = frontmatter.match(/^summary:\s*(.+)$/m);
|
|
128
|
+
if (summaryMatch) metadata.summary = summaryMatch[1].trim();
|
|
129
|
+
|
|
130
|
+
const descMatch = frontmatter.match(/^description:\s*["'](.+)["']$/m);
|
|
131
|
+
if (descMatch) {
|
|
132
|
+
metadata.description = descMatch[1].trim();
|
|
133
|
+
} else {
|
|
134
|
+
const descMatch2 = frontmatter.match(/^description:\s*(.+)$/m);
|
|
135
|
+
if (descMatch2) metadata.description = descMatch2[1].trim();
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const versionMatch = frontmatter.match(/^version:\s*(.+)$/m);
|
|
139
|
+
if (versionMatch) metadata.version = versionMatch[1].trim();
|
|
140
|
+
|
|
141
|
+
const binsMatch = frontmatter.match(/bins:\s*\[(.+?)\]/);
|
|
142
|
+
if (binsMatch) {
|
|
143
|
+
metadata.bins = binsMatch[1].split(',').map(b => b.trim().replace(/['"]/g, ''));
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return metadata;
|
|
147
|
+
} catch (_) {
|
|
148
|
+
return null;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* 推断 skill emoji
|
|
154
|
+
*/
|
|
155
|
+
function inferSkillEmoji(skillName) {
|
|
156
|
+
const emojiMap = {
|
|
157
|
+
'soke-exam': '📝',
|
|
158
|
+
'soke-course': '📚',
|
|
159
|
+
'soke-shared': '🔧',
|
|
160
|
+
'soke-user': '👤',
|
|
161
|
+
'soke-contact': '📇',
|
|
162
|
+
'soke-department': '🏢',
|
|
163
|
+
'soke-approval': '✅',
|
|
164
|
+
'soke-attendance': '📅',
|
|
165
|
+
'soke-report': '📊'
|
|
166
|
+
};
|
|
167
|
+
return emojiMap[skillName] || '📦';
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* 检测本地 AI Agent 环境
|
|
172
|
+
* 只检测 agent 相关的目录,不包括全局安装目录
|
|
173
|
+
*/
|
|
174
|
+
function detectLocalAgentDirs() {
|
|
175
|
+
const homeDir = os.homedir();
|
|
176
|
+
const dirs = [];
|
|
177
|
+
|
|
178
|
+
// 1. workclaw (Claude Code) - Agent skills 目录
|
|
179
|
+
const workclawDir = path.join(homeDir, '.workclaw', 'skills');
|
|
180
|
+
if (fs.existsSync(path.join(homeDir, '.workclaw'))) {
|
|
181
|
+
dirs.push({
|
|
182
|
+
name: 'workclaw (Claude Code)',
|
|
183
|
+
path: workclawDir,
|
|
184
|
+
registryPath: path.join(workclawDir, 'registry.json'),
|
|
185
|
+
type: 'workclaw'
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// 2. claude (Claude Desktop) - Agent skills 目录
|
|
190
|
+
const claudeDir = path.join(homeDir, '.claude', 'skills');
|
|
191
|
+
if (fs.existsSync(path.join(homeDir, '.claude'))) {
|
|
192
|
+
dirs.push({
|
|
193
|
+
name: 'claude (Claude Desktop)',
|
|
194
|
+
path: claudeDir,
|
|
195
|
+
registryPath: null,
|
|
196
|
+
type: 'claude'
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// 3. sokeclaw - Agent skills 目录
|
|
201
|
+
const sokeclawDir = path.join(homeDir, '.sokeclaw', 'openai-agents', 'workspaces', 'main', 'skills');
|
|
202
|
+
if (fs.existsSync(path.join(homeDir, '.sokeclaw'))) {
|
|
203
|
+
dirs.push({
|
|
204
|
+
name: 'sokeclaw',
|
|
205
|
+
path: sokeclawDir,
|
|
206
|
+
registryPath: null,
|
|
207
|
+
type: 'sokeclaw'
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// 4. zev - Agent skills 目录
|
|
212
|
+
const zevDir = path.join(homeDir, '.zev', 'openai-agents', 'workspaces', 'main', 'skills');
|
|
213
|
+
if (fs.existsSync(path.join(homeDir, '.zev'))) {
|
|
214
|
+
dirs.push({
|
|
215
|
+
name: 'zev',
|
|
216
|
+
path: zevDir,
|
|
217
|
+
registryPath: null,
|
|
218
|
+
type: 'zev'
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
return dirs;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* 更新 workclaw registry.json
|
|
227
|
+
*/
|
|
228
|
+
function updateWorkclawRegistry(registryPath, skillName, metadata, skillInstallPath) {
|
|
229
|
+
let registry;
|
|
230
|
+
|
|
231
|
+
try {
|
|
232
|
+
registry = JSON.parse(fs.readFileSync(registryPath, 'utf8'));
|
|
233
|
+
} catch (_) {
|
|
234
|
+
registry = { version: 1, migrations: {}, skills: [] };
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
if (registry.version == null) registry.version = 1;
|
|
238
|
+
if (!registry.migrations) registry.migrations = {};
|
|
239
|
+
if (!Array.isArray(registry.skills)) registry.skills = [];
|
|
240
|
+
|
|
241
|
+
const displayName = metadata.summary || metadata.name || skillName;
|
|
242
|
+
const description = metadata.description || `${displayName} - 授客AI CLI工具`;
|
|
243
|
+
const version = metadata.version || '1.0.0';
|
|
244
|
+
const emoji = inferSkillEmoji(skillName);
|
|
245
|
+
const requires = metadata.bins ? { bins: metadata.bins } : {};
|
|
246
|
+
|
|
247
|
+
const skillEntry = {
|
|
248
|
+
id: `skill:${skillName}`,
|
|
249
|
+
name: skillName,
|
|
250
|
+
displayName: displayName,
|
|
251
|
+
description: description,
|
|
252
|
+
source: { type: 'local', slug: '', url: '' },
|
|
253
|
+
install: {
|
|
254
|
+
path: skillInstallPath,
|
|
255
|
+
installedAt: new Date().toISOString(),
|
|
256
|
+
updatedAt: new Date().toISOString(),
|
|
257
|
+
version: version
|
|
258
|
+
},
|
|
259
|
+
state: { enabled: true, health: 'ok', lastError: '' },
|
|
260
|
+
runtime: { supported: ['openclaw'], enabled: ['openclaw'], primary: 'openclaw' },
|
|
261
|
+
security: { riskLevel: 'normal', requiresApproval: false },
|
|
262
|
+
metadata: { emoji: emoji, homepage: '', requires: requires }
|
|
263
|
+
};
|
|
264
|
+
|
|
265
|
+
const idx = registry.skills.findIndex(s => s && s.id === skillEntry.id);
|
|
266
|
+
if (idx >= 0) {
|
|
267
|
+
registry.skills[idx] = { ...registry.skills[idx], ...skillEntry };
|
|
268
|
+
} else {
|
|
269
|
+
registry.skills.push(skillEntry);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
fs.writeFileSync(registryPath, JSON.stringify(registry, null, 2));
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* 分发单个 skill 到所有本地环境
|
|
277
|
+
*/
|
|
278
|
+
function distributeSkill(skillName, packagedSkillsDir, targetDirs) {
|
|
279
|
+
const srcDir = path.join(packagedSkillsDir, skillName);
|
|
280
|
+
|
|
281
|
+
if (!fs.existsSync(srcDir)) {
|
|
282
|
+
logError(`Skill 目录不存在: ${srcDir}`);
|
|
283
|
+
return false;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
const metadata = parseSkillMetadata(srcDir);
|
|
287
|
+
if (!metadata || !metadata.name) {
|
|
288
|
+
logWarning(`无法解析 ${skillName} 的元数据,将使用默认值`);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
logInfo(`分发 ${skillName}...`);
|
|
292
|
+
console.log('');
|
|
293
|
+
|
|
294
|
+
let successCount = 0;
|
|
295
|
+
|
|
296
|
+
for (const target of targetDirs) {
|
|
297
|
+
try {
|
|
298
|
+
fs.mkdirSync(target.path, { recursive: true });
|
|
299
|
+
|
|
300
|
+
const destDir = path.join(target.path, skillName);
|
|
301
|
+
const success = copyDirRecursive(srcDir, destDir);
|
|
302
|
+
|
|
303
|
+
if (success) {
|
|
304
|
+
logSuccess(` → ${target.name}`);
|
|
305
|
+
logInfo(` ${destDir}`);
|
|
306
|
+
|
|
307
|
+
// 更新 workclaw registry
|
|
308
|
+
if (target.type === 'workclaw' && target.registryPath) {
|
|
309
|
+
updateWorkclawRegistry(target.registryPath, skillName, metadata, destDir);
|
|
310
|
+
logInfo(` 已更新 registry.json`);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
successCount++;
|
|
314
|
+
} else {
|
|
315
|
+
logError(` → ${target.name} (复制失败)`);
|
|
316
|
+
}
|
|
317
|
+
} catch (err) {
|
|
318
|
+
logError(` → ${target.name} (错误: ${err.message})`);
|
|
319
|
+
}
|
|
320
|
+
console.log('');
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
return successCount > 0;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* 清理所有本地环境中的 skills
|
|
328
|
+
*/
|
|
329
|
+
function cleanSkills(targetDirs, skillNames) {
|
|
330
|
+
logSection('清理本地 Skills');
|
|
331
|
+
|
|
332
|
+
for (const target of targetDirs) {
|
|
333
|
+
logInfo(`清理 ${target.name}...`);
|
|
334
|
+
|
|
335
|
+
for (const skillName of skillNames) {
|
|
336
|
+
const skillDir = path.join(target.path, skillName);
|
|
337
|
+
|
|
338
|
+
if (fs.existsSync(skillDir)) {
|
|
339
|
+
try {
|
|
340
|
+
fs.rmSync(skillDir, { recursive: true, force: true });
|
|
341
|
+
logSuccess(` ✓ 删除 ${skillName}`);
|
|
342
|
+
} catch (err) {
|
|
343
|
+
logError(` ✗ 删除 ${skillName} 失败: ${err.message}`);
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// 清理 workclaw registry
|
|
349
|
+
if (target.type === 'workclaw' && target.registryPath && fs.existsSync(target.registryPath)) {
|
|
350
|
+
try {
|
|
351
|
+
const registry = JSON.parse(fs.readFileSync(target.registryPath, 'utf8'));
|
|
352
|
+
if (Array.isArray(registry.skills)) {
|
|
353
|
+
const before = registry.skills.length;
|
|
354
|
+
registry.skills = registry.skills.filter(s => {
|
|
355
|
+
return !s || !s.name || !skillNames.includes(s.name);
|
|
356
|
+
});
|
|
357
|
+
const after = registry.skills.length;
|
|
358
|
+
|
|
359
|
+
if (before !== after) {
|
|
360
|
+
fs.writeFileSync(target.registryPath, JSON.stringify(registry, null, 2));
|
|
361
|
+
logSuccess(` ✓ 清理 registry.json (删除 ${before - after} 个条目)`);
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
} catch (err) {
|
|
365
|
+
logError(` ✗ 清理 registry.json 失败: ${err.message}`);
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
console.log('');
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
/**
|
|
374
|
+
* 主函数
|
|
375
|
+
*/
|
|
376
|
+
function main() {
|
|
377
|
+
const args = process.argv.slice(2);
|
|
378
|
+
const isClean = args.includes('--clean');
|
|
379
|
+
const skillArg = args.find(arg => arg.startsWith('--skill='));
|
|
380
|
+
const specificSkill = skillArg ? skillArg.split('=')[1] : null;
|
|
381
|
+
|
|
382
|
+
logSection('本地 Skill 测试工具');
|
|
383
|
+
|
|
384
|
+
// 检测项目目录
|
|
385
|
+
const packageRoot = path.join(__dirname, '..');
|
|
386
|
+
const packagedSkillsDir = path.join(packageRoot, 'skills');
|
|
387
|
+
|
|
388
|
+
if (!fs.existsSync(packagedSkillsDir)) {
|
|
389
|
+
logError(`Skills 目录不存在: ${packagedSkillsDir}`);
|
|
390
|
+
process.exit(1);
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
// 检测所有 skills
|
|
394
|
+
const allSkills = detectSkillNames(packagedSkillsDir);
|
|
395
|
+
|
|
396
|
+
if (allSkills.length === 0) {
|
|
397
|
+
logError('未检测到任何 soke-* skills');
|
|
398
|
+
process.exit(1);
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
logInfo(`检测到 ${allSkills.length} 个 skills: ${allSkills.join(', ')}`);
|
|
402
|
+
console.log('');
|
|
403
|
+
|
|
404
|
+
// 检测本地 AI Agent 环境
|
|
405
|
+
const targetDirs = detectLocalAgentDirs();
|
|
406
|
+
|
|
407
|
+
if (targetDirs.length === 0) {
|
|
408
|
+
logError('未检测到任何本地 AI Agent 环境');
|
|
409
|
+
logInfo('支持的环境:');
|
|
410
|
+
logInfo(' • workclaw (~/.workclaw/skills/)');
|
|
411
|
+
logInfo(' • claude (~/.claude/skills/)');
|
|
412
|
+
logInfo(' • sokeclaw (~/.sokeclaw/openai-agents/workspaces/main/skills/)');
|
|
413
|
+
logInfo(' • zev (~/.zev/openai-agents/workspaces/main/skills/)');
|
|
414
|
+
console.log('');
|
|
415
|
+
logWarning('注意: 此脚本只分发到 Agent skills 目录,不包括全局安装目录');
|
|
416
|
+
process.exit(1);
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
logInfo(`检测到 ${targetDirs.length} 个本地环境:`);
|
|
420
|
+
for (const target of targetDirs) {
|
|
421
|
+
logInfo(` • ${target.name}`);
|
|
422
|
+
logInfo(` ${target.path}`);
|
|
423
|
+
}
|
|
424
|
+
console.log('');
|
|
425
|
+
|
|
426
|
+
// 清理模式
|
|
427
|
+
if (isClean) {
|
|
428
|
+
cleanSkills(targetDirs, allSkills);
|
|
429
|
+
logSuccess('清理完成!');
|
|
430
|
+
return;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
// 确定要分发的 skills
|
|
434
|
+
const skillsToDistribute = specificSkill
|
|
435
|
+
? (allSkills.includes(specificSkill) ? [specificSkill] : [])
|
|
436
|
+
: allSkills;
|
|
437
|
+
|
|
438
|
+
if (skillsToDistribute.length === 0) {
|
|
439
|
+
logError(`Skill 不存在: ${specificSkill}`);
|
|
440
|
+
process.exit(1);
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
// 分发 skills
|
|
444
|
+
logSection('分发 Skills 到本地环境');
|
|
445
|
+
|
|
446
|
+
let totalSuccess = 0;
|
|
447
|
+
let totalFailed = 0;
|
|
448
|
+
|
|
449
|
+
for (const skillName of skillsToDistribute) {
|
|
450
|
+
const success = distributeSkill(skillName, packagedSkillsDir, targetDirs);
|
|
451
|
+
if (success) {
|
|
452
|
+
totalSuccess++;
|
|
453
|
+
} else {
|
|
454
|
+
totalFailed++;
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
// 总结
|
|
459
|
+
logSection('分发完成');
|
|
460
|
+
|
|
461
|
+
logInfo(`总计: ${skillsToDistribute.length} 个 skills`);
|
|
462
|
+
logSuccess(`成功: ${totalSuccess} 个`);
|
|
463
|
+
if (totalFailed > 0) {
|
|
464
|
+
logError(`失败: ${totalFailed} 个`);
|
|
465
|
+
}
|
|
466
|
+
console.log('');
|
|
467
|
+
|
|
468
|
+
// 下一步提示
|
|
469
|
+
logSection('下一步');
|
|
470
|
+
console.log('');
|
|
471
|
+
log('1. 重启你的 AI Agent', 'cyan');
|
|
472
|
+
log(' • Claude Code: 重启 VS Code 或重新打开 Claude Code 窗口', 'cyan');
|
|
473
|
+
log(' • Claude Desktop: 重启 Claude Desktop 应用', 'cyan');
|
|
474
|
+
log(' • Sokeclaw: 重启 sokeclaw 进程', 'cyan');
|
|
475
|
+
log(' • Zev: 重启 zev 进程', 'cyan');
|
|
476
|
+
console.log('');
|
|
477
|
+
log('2. 在对话中测试 skill 功能', 'cyan');
|
|
478
|
+
log(' 例如: "查询课程列表" 或 "查询考试成绩"', 'cyan');
|
|
479
|
+
console.log('');
|
|
480
|
+
log('3. 验证命令是否可用:', 'cyan');
|
|
481
|
+
console.log('');
|
|
482
|
+
for (const skillName of skillsToDistribute) {
|
|
483
|
+
log(` soke-cli ${skillName.replace('soke-', '')} --help`, 'yellow');
|
|
484
|
+
}
|
|
485
|
+
console.log('');
|
|
486
|
+
log('4. 测试完成后,可以清理:', 'cyan');
|
|
487
|
+
log(' node scripts/local-test.js --clean', 'yellow');
|
|
488
|
+
console.log('');
|
|
489
|
+
log('💡 提示:', 'cyan');
|
|
490
|
+
log(' • 此脚本只分发到 Agent skills 目录', 'cyan');
|
|
491
|
+
log(' • 全局安装 (npm install -g) 需要单独处理', 'cyan');
|
|
492
|
+
log(' • 修改后重新运行此脚本即可更新', 'cyan');
|
|
493
|
+
console.log('');
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
// 运行
|
|
497
|
+
try {
|
|
498
|
+
main();
|
|
499
|
+
} catch (err) {
|
|
500
|
+
logError(`发生错误: ${err.message}`);
|
|
501
|
+
console.error(err);
|
|
502
|
+
process.exit(1);
|
|
503
|
+
}
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
|
|
3
|
+
# soke-cli 本地测试脚本
|
|
4
|
+
# 用于本地开发测试:编译 -> 安装到全局 -> 验证功能
|
|
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 本地测试${NC}"
|
|
15
|
+
echo -e "${BLUE}========================================${NC}"
|
|
16
|
+
echo ""
|
|
17
|
+
|
|
18
|
+
# 步骤1: 编译
|
|
19
|
+
echo -e "${YELLOW}[1/4] 编译 soke-cli...${NC}"
|
|
20
|
+
if go build -o soke-cli main.go; then
|
|
21
|
+
echo -e "${GREEN}✓ 编译成功${NC}"
|
|
22
|
+
else
|
|
23
|
+
echo -e "${RED}✗ 编译失败${NC}"
|
|
24
|
+
exit 1
|
|
25
|
+
fi
|
|
26
|
+
echo ""
|
|
27
|
+
|
|
28
|
+
# 步骤2: 检查本地版本功能
|
|
29
|
+
echo -e "${YELLOW}[2/4] 检查本地版本功能...${NC}"
|
|
30
|
+
echo -e " 检查 learning-profile 模块..."
|
|
31
|
+
if ./soke-cli learning-profile --help &>/dev/null; then
|
|
32
|
+
echo -e "${GREEN} ✓ learning-profile 模块存在${NC}"
|
|
33
|
+
else
|
|
34
|
+
echo -e "${RED} ✗ learning-profile 模块不存在${NC}"
|
|
35
|
+
exit 1
|
|
36
|
+
fi
|
|
37
|
+
|
|
38
|
+
echo -e " 检查 contact +search-dept 命令..."
|
|
39
|
+
if ./soke-cli contact +search-dept --help &>/dev/null; then
|
|
40
|
+
echo -e "${GREEN} ✓ contact +search-dept 命令存在${NC}"
|
|
41
|
+
else
|
|
42
|
+
echo -e "${RED} ✗ contact +search-dept 命令不存在${NC}"
|
|
43
|
+
exit 1
|
|
44
|
+
fi
|
|
45
|
+
echo ""
|
|
46
|
+
|
|
47
|
+
# 步骤3: 安装到全局
|
|
48
|
+
echo -e "${YELLOW}[3/4] 安装到全局...${NC}"
|
|
49
|
+
|
|
50
|
+
# 查找全局 CLI 路径
|
|
51
|
+
GLOBAL_CLI=$(which soke-cli 2>/dev/null)
|
|
52
|
+
if [ -z "$GLOBAL_CLI" ]; then
|
|
53
|
+
echo -e "${YELLOW} 未找到全局 soke-cli 安装${NC}"
|
|
54
|
+
echo -e "${YELLOW} 请先通过 npm 安装: npm install -g @sokeai/cli${NC}"
|
|
55
|
+
echo -e "${YELLOW} 跳过全局安装,仅测试本地版本${NC}"
|
|
56
|
+
SKIP_GLOBAL=true
|
|
57
|
+
else
|
|
58
|
+
echo -e " 全局 CLI 路径: ${BLUE}${GLOBAL_CLI}${NC}"
|
|
59
|
+
|
|
60
|
+
# 检查是否需要更新
|
|
61
|
+
NEED_UPDATE=false
|
|
62
|
+
if ! $GLOBAL_CLI learning-profile --help &>/dev/null; then
|
|
63
|
+
echo -e "${YELLOW} 全局版本不支持 learning-profile 模块${NC}"
|
|
64
|
+
NEED_UPDATE=true
|
|
65
|
+
elif ! $GLOBAL_CLI contact +search-dept --help &>/dev/null; then
|
|
66
|
+
echo -e "${YELLOW} 全局版本不支持 contact +search-dept 命令${NC}"
|
|
67
|
+
NEED_UPDATE=true
|
|
68
|
+
fi
|
|
69
|
+
|
|
70
|
+
if [ "$NEED_UPDATE" = true ]; then
|
|
71
|
+
echo -e "${YELLOW} 需要更新全局安装${NC}"
|
|
72
|
+
echo ""
|
|
73
|
+
echo -e "${YELLOW} 是否要用本地版本覆盖全局安装? (y/n)${NC}"
|
|
74
|
+
read -r CONFIRM
|
|
75
|
+
|
|
76
|
+
if [ "$CONFIRM" = "y" ] || [ "$CONFIRM" = "Y" ]; then
|
|
77
|
+
# 备份原文件
|
|
78
|
+
BACKUP_FILE="${GLOBAL_CLI}.backup.$(date +%Y%m%d_%H%M%S)"
|
|
79
|
+
echo -e " 备份原文件到: ${BACKUP_FILE}"
|
|
80
|
+
if sudo cp $GLOBAL_CLI $BACKUP_FILE; then
|
|
81
|
+
echo -e "${GREEN} ✓ 备份成功${NC}"
|
|
82
|
+
else
|
|
83
|
+
echo -e "${RED} ✗ 备份失败${NC}"
|
|
84
|
+
exit 1
|
|
85
|
+
fi
|
|
86
|
+
|
|
87
|
+
# 安装新版本
|
|
88
|
+
echo -e " 安装新版本..."
|
|
89
|
+
if sudo cp ./soke-cli $GLOBAL_CLI; then
|
|
90
|
+
echo -e "${GREEN} ✓ 安装成功${NC}"
|
|
91
|
+
echo -e "${BLUE} 备份文件: ${BACKUP_FILE}${NC}"
|
|
92
|
+
else
|
|
93
|
+
echo -e "${RED} ✗ 安装失败${NC}"
|
|
94
|
+
echo -e "${YELLOW} 恢复备份...${NC}"
|
|
95
|
+
sudo cp $BACKUP_FILE $GLOBAL_CLI
|
|
96
|
+
exit 1
|
|
97
|
+
fi
|
|
98
|
+
SKIP_GLOBAL=false
|
|
99
|
+
else
|
|
100
|
+
echo -e "${YELLOW} 跳过全局安装${NC}"
|
|
101
|
+
SKIP_GLOBAL=true
|
|
102
|
+
fi
|
|
103
|
+
else
|
|
104
|
+
echo -e "${GREEN} ✓ 全局版本已是最新,无需更新${NC}"
|
|
105
|
+
SKIP_GLOBAL=false
|
|
106
|
+
fi
|
|
107
|
+
fi
|
|
108
|
+
echo ""
|
|
109
|
+
|
|
110
|
+
# 步骤4: 运行测试
|
|
111
|
+
echo -e "${YELLOW}[4/4] 运行功能测试...${NC}"
|
|
112
|
+
|
|
113
|
+
# 测试本地版本
|
|
114
|
+
echo -e " ${BLUE}测试本地版本:${NC}"
|
|
115
|
+
if ./soke-cli learning-profile +list --help &>/dev/null; then
|
|
116
|
+
echo -e "${GREEN} ✓ learning-profile +list 命令可用${NC}"
|
|
117
|
+
else
|
|
118
|
+
echo -e "${RED} ✗ learning-profile +list 命令不可用${NC}"
|
|
119
|
+
exit 1
|
|
120
|
+
fi
|
|
121
|
+
|
|
122
|
+
if ./soke-cli contact +search-dept --help &>/dev/null; then
|
|
123
|
+
echo -e "${GREEN} ✓ contact +search-dept 命令可用${NC}"
|
|
124
|
+
else
|
|
125
|
+
echo -e "${RED} ✗ contact +search-dept 命令不可用${NC}"
|
|
126
|
+
exit 1
|
|
127
|
+
fi
|
|
128
|
+
|
|
129
|
+
if ./soke-cli contact +search-user --help &>/dev/null; then
|
|
130
|
+
echo -e "${GREEN} ✓ contact +search-user 命令可用${NC}"
|
|
131
|
+
else
|
|
132
|
+
echo -e "${RED} ✗ contact +search-user 命令不可用${NC}"
|
|
133
|
+
exit 1
|
|
134
|
+
fi
|
|
135
|
+
|
|
136
|
+
# 测试全局版本(如果已安装)
|
|
137
|
+
if [ "$SKIP_GLOBAL" = false ] && [ -n "$GLOBAL_CLI" ]; then
|
|
138
|
+
echo ""
|
|
139
|
+
echo -e " ${BLUE}测试全局版本:${NC}"
|
|
140
|
+
if $GLOBAL_CLI learning-profile +list --help &>/dev/null; then
|
|
141
|
+
echo -e "${GREEN} ✓ learning-profile +list 命令可用${NC}"
|
|
142
|
+
else
|
|
143
|
+
echo -e "${RED} ✗ learning-profile +list 命令不可用${NC}"
|
|
144
|
+
echo -e "${YELLOW} 提示: 全局安装可能未成功更新${NC}"
|
|
145
|
+
fi
|
|
146
|
+
|
|
147
|
+
if $GLOBAL_CLI contact +search-dept --help &>/dev/null; then
|
|
148
|
+
echo -e "${GREEN} ✓ contact +search-dept 命令可用${NC}"
|
|
149
|
+
else
|
|
150
|
+
echo -e "${RED} ✗ contact +search-dept 命令不可用${NC}"
|
|
151
|
+
echo -e "${YELLOW} 提示: 全局安装可能未成功更新${NC}"
|
|
152
|
+
fi
|
|
153
|
+
|
|
154
|
+
if $GLOBAL_CLI contact +search-user --help &>/dev/null; then
|
|
155
|
+
echo -e "${GREEN} ✓ contact +search-user 命令可用${NC}"
|
|
156
|
+
else
|
|
157
|
+
echo -e "${RED} ✗ contact +search-user 命令不可用${NC}"
|
|
158
|
+
echo -e "${YELLOW} 提示: 全局安装可能未成功更新${NC}"
|
|
159
|
+
fi
|
|
160
|
+
fi
|
|
161
|
+
echo ""
|
|
162
|
+
|
|
163
|
+
# 完成
|
|
164
|
+
echo -e "${GREEN}========================================${NC}"
|
|
165
|
+
echo -e "${GREEN} 本地测试完成! ✓${NC}"
|
|
166
|
+
echo -e "${GREEN}========================================${NC}"
|
|
167
|
+
echo ""
|
|
168
|
+
echo -e "${BLUE}下一步:${NC}"
|
|
169
|
+
if [ "$SKIP_GLOBAL" = true ]; then
|
|
170
|
+
echo -e " ${YELLOW}提示: 未更新全局安装,Skills 可能无法使用新功能${NC}"
|
|
171
|
+
echo -e " ${YELLOW}如需测试 Skills,请重新运行此脚本并选择更新全局安装${NC}"
|
|
172
|
+
echo ""
|
|
173
|
+
fi
|
|
174
|
+
echo -e " 1. 运行完整测试: ${BLUE}bash ./scripts/e2e-test.sh${NC}"
|
|
175
|
+
echo -e " 2. 测试 Skills: ${BLUE}npx skills add liuchenlong1111/soke-cli -y -g${NC}"
|
|
176
|
+
echo -e " 3. 在 AI Agent 中测试: ${BLUE}\"查询张三的学习档案\"${NC}"
|
|
177
|
+
echo ""
|
|
178
|
+
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: soke-learning-profile
|
|
3
|
+
summary: 学员学习档案查询
|
|
4
|
+
version: 1.0.0
|
|
5
|
+
description: "学员学习档案查询:查询学员的完整学习记录,包括课程学习、考试成绩、证书获取、学分积分等。当用户需要查询学员学习档案、查看学员学习情况、统计学员学习数据、查询学员综合学习信息时使用。"
|
|
6
|
+
metadata:
|
|
7
|
+
requires:
|
|
8
|
+
bins: ["soke-cli"]
|
|
9
|
+
cliHelp: "soke-cli learning-profile --help"
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
**CRITICAL — 开始前 MUST 先用 Read 工具读取 [`../soke-shared/SKILL.md`](../soke-shared/SKILL.md),其中包含认证、配置、权限处理**
|
|
13
|
+
|
|
14
|
+
## 核心概念
|
|
15
|
+
|
|
16
|
+
- **LearningProfile(学习档案)**: 学员的完整学习记录,包含课程、考试、证书、培训等多维度数据
|
|
17
|
+
- **DeptUser(部门用户)**: 企业内的学员,通过 `dept_user_id` 标识
|
|
18
|
+
- **Department(部门)**: 组织架构单元,通过 `dept_id` 标识
|
|
19
|
+
|
|
20
|
+
## 资源关系
|
|
21
|
+
|
|
22
|
+
```
|
|
23
|
+
LearningProfile (学习档案)
|
|
24
|
+
├── DeptUser (学员)
|
|
25
|
+
│ ├── dept_user_id (学员ID)
|
|
26
|
+
│ ├── dept_user_name (学员姓名)
|
|
27
|
+
│ └── dept_names (所属部门)
|
|
28
|
+
├── CourseData (课程数据)
|
|
29
|
+
│ ├── required_learning/finished (必修课程)
|
|
30
|
+
│ ├── optional_learning/finished (选修课程)
|
|
31
|
+
│ └── learn_time (学习时长)
|
|
32
|
+
├── ExamData (考试数据)
|
|
33
|
+
│ ├── not_attempt (未开始)
|
|
34
|
+
│ ├── passed (通过)
|
|
35
|
+
│ ├── notpassed (未通过)
|
|
36
|
+
│ └── reviewing (批阅中)
|
|
37
|
+
├── CertificateData (证书数据)
|
|
38
|
+
│ └── certificate_number (证书数量)
|
|
39
|
+
├── PointsAndCredits (积分学分)
|
|
40
|
+
│ ├── points (积分)
|
|
41
|
+
│ └── credits (学分)
|
|
42
|
+
└── TrainingData (培训数据)
|
|
43
|
+
├── training_finished (线下培训)
|
|
44
|
+
├── learning_map_finished (学习地图)
|
|
45
|
+
└── training_class_finished (培训班)
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Shortcuts(推荐优先使用)
|
|
49
|
+
|
|
50
|
+
| Shortcut | 说明 |
|
|
51
|
+
|----------|------|
|
|
52
|
+
| [`+list`](#list) | 查询学员学习档案列表 |
|
|
53
|
+
|
|
54
|
+
## 辅助命令(contact 模块)
|
|
55
|
+
|
|
56
|
+
| Shortcut | 说明 |
|
|
57
|
+
|----------|------|
|
|
58
|
+
| `contact +search-user` | 根据姓名搜索学员 |
|
|
59
|
+
| `contact +search-dept` | 根据名称搜索部门 |
|
|
60
|
+
|
|
61
|
+
---
|
|
62
|
+
|
|
63
|
+
## 命令详解
|
|
64
|
+
|
|
65
|
+
### +list
|
|
66
|
+
|
|
67
|
+
查询学员学习档案列表,支持按部门、学员ID筛选。
|
|
68
|
+
|
|
69
|
+
**命令格式**:
|
|
70
|
+
```bash
|
|
71
|
+
soke-cli learning-profile +list \
|
|
72
|
+
[--dept-user-ids <user_id1,user_id2,...>] \
|
|
73
|
+
[--dept-ids <dept_id1,dept_id2,...>] \
|
|
74
|
+
[--is-new <0|1>] \
|
|
75
|
+
[--offset <offset>] \
|
|
76
|
+
[--page-size <size>]
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
**参数说明**:
|
|
80
|
+
- `--dept-user-ids`: 学员ID列表,多个ID用逗号分隔(可选)
|
|
81
|
+
- `--dept-ids`: 部门ID列表,多个ID用逗号分隔(可选)
|
|
82
|
+
- `--is-new`: 是否新员工,0-否,1-是(可选)
|
|
83
|
+
- `--offset`: 偏移量,默认从0开始(可选)
|
|
84
|
+
- `--page-size`: 每页条数,最大100,默认10(可选)
|
|
85
|
+
|
|
86
|
+
**返回字段**(表格显示关键字段):
|
|
87
|
+
- `姓名`: 学员姓名
|
|
88
|
+
- `部门`: 所属部门
|
|
89
|
+
- `职位`: 职位
|
|
90
|
+
- `必修完成`: 必修课程完成情况(完成数/总数)
|
|
91
|
+
- `选修完成`: 选修课程完成情况(完成数/总数)
|
|
92
|
+
- `考试通过`: 考试通过数
|
|
93
|
+
- `学习时长`: 学习时长(格式化为"X小时Y分钟")
|
|
94
|
+
- `证书数`: 获得证书数量
|
|
95
|
+
- `学分`: 学分
|
|
96
|
+
- `积分`: 积分
|
|
97
|
+
|
|
98
|
+
**JSON 输出包含完整字段**(30+ 字段):
|
|
99
|
+
- 基础信息:`dept_user_id`, `dept_user_name`, `dept_names`, `position`, `job_number`, `avatar`, `is_leave`, `hired_date`
|
|
100
|
+
- 课程学习:`optional_learning`, `optional_finished`, `required_learning`, `required_finished`, `learn_time`
|
|
101
|
+
- 考试情况:`not_attempt`, `passed`, `notpassed`, `reviewing`
|
|
102
|
+
- 证书积分:`certificate_number`, `points`, `credits`
|
|
103
|
+
- 培训情况:`training_not_attempt`, `training_finished`, `class_length`
|
|
104
|
+
- 学习地图:`leaning_map_attempt`, `learning_map_finished`
|
|
105
|
+
- 培训班:`training_class_attempt`, `training_class_finished`
|
|
106
|
+
- 其他:`live_learn_time`, `external_training_time`, `knowledge_number`, `evaluation_score`
|
|
107
|
+
|
|
108
|
+
**示例**:
|
|
109
|
+
```bash
|
|
110
|
+
# 查询所有学员学习档案
|
|
111
|
+
soke-cli learning-profile +list --offset 0 --page-size 20
|
|
112
|
+
|
|
113
|
+
# 查询特定学员(单个)
|
|
114
|
+
soke-cli learning-profile +list --dept-user-ids user123
|
|
115
|
+
|
|
116
|
+
# 查询多个学员
|
|
117
|
+
soke-cli learning-profile +list --dept-user-ids user123,user456,user789
|
|
118
|
+
|
|
119
|
+
# 查询特定部门的学员
|
|
120
|
+
soke-cli learning-profile +list --dept-ids dept456
|
|
121
|
+
|
|
122
|
+
# 查询多个部门的学员
|
|
123
|
+
soke-cli learning-profile +list --dept-ids dept456,dept789
|
|
124
|
+
|
|
125
|
+
# 查询新员工的学习档案
|
|
126
|
+
soke-cli learning-profile +list --is-new 1 --page-size 50
|
|
127
|
+
|
|
128
|
+
# 分页查询(使用 offset)
|
|
129
|
+
soke-cli learning-profile +list --offset 0 --page-size 10 # 第1页
|
|
130
|
+
soke-cli learning-profile +list --offset 10 --page-size 10 # 第2页
|
|
131
|
+
soke-cli learning-profile +list --offset 20 --page-size 10 # 第3页
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
**权限要求**: `learningProfile:readonly`
|
|
135
|
+
|
|
136
|
+
**使用场景**:
|
|
137
|
+
- 当用户询问"查询学员学习档案"时使用
|
|
138
|
+
- 当用户需要查看学员学习情况、统计学习数据时使用
|
|
139
|
+
- 当用户需要导出学员学习报表时使用
|
|
140
|
+
|
|
141
|
+
---
|
|
142
|
+
|
|
143
|
+
## 权限表
|
|
144
|
+
|
|
145
|
+
| 命令 | 所需权限 | 说明 |
|
|
146
|
+
|------|----------|------|
|
|
147
|
+
| `+list` | `learningProfile:readonly` | 查询学员学习档案 |
|
|
148
|
+
| `contact +search-user` | `contact:user:readonly` | 搜索学员 |
|
|
149
|
+
| `contact +search-dept` | `contact:department:readonly` | 搜索部门 |
|
|
150
|
+
|
|
151
|
+
---
|
|
152
|
+
|
|
153
|
+
## 常见工作流
|
|
154
|
+
|
|
155
|
+
### 工作流1: 根据姓名查询学员学习档案
|
|
156
|
+
|
|
157
|
+
当用户询问"查询张三的学习档案"时:
|
|
158
|
+
|
|
159
|
+
**步骤1**: 搜索学员获取 dept_user_id
|
|
160
|
+
```bash
|
|
161
|
+
soke-cli contact +search-user --dept-user-name "张三"
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
**步骤2**: 使用 dept_user_id 查询学习档案
|
|
165
|
+
```bash
|
|
166
|
+
soke-cli learning-profile +list --dept-user-ids <从步骤1获取的dept_user_id>
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
**注意**: 学习档案接口不支持按姓名直接查询,必须先通过 contact 模块获取 dept_user_id。
|
|
170
|
+
|
|
171
|
+
### 工作流2: 查询部门所有学员的学习档案
|
|
172
|
+
|
|
173
|
+
当用户询问"查询技术部所有学员的学习情况"时:
|
|
174
|
+
|
|
175
|
+
**步骤1**: 搜索部门获取 dept_id
|
|
176
|
+
```bash
|
|
177
|
+
soke-cli contact +search-dept --dept-name "技术部"
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
**步骤2**: 使用 dept_id 查询该部门学员学习档案
|
|
181
|
+
```bash
|
|
182
|
+
soke-cli learning-profile +list --dept-ids <从步骤1获取的dept_id> --page-size 100
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
### 工作流3: 批量导出学习档案
|
|
186
|
+
|
|
187
|
+
当用户需要导出学习档案数据时:
|
|
188
|
+
|
|
189
|
+
```bash
|
|
190
|
+
# 使用 JSON 格式输出并保存到文件
|
|
191
|
+
soke-cli learning-profile +list --page-size 100 --format json > learning_profiles.json
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
---
|
|
195
|
+
|
|
196
|
+
## 注意事项
|
|
197
|
+
|
|
198
|
+
1. **时间格式**: `learn_time`, `class_length` 等时长字段单位为秒,表格输出时会自动格式化为"X小时Y分钟"
|
|
199
|
+
2. **分页**: 使用 `offset` 和 `page_size` 进行分页,默认每页10条,最大100条
|
|
200
|
+
3. **数组参数**: `dept_user_ids` 和 `dept_ids` 是数组类型,多个ID用逗号分隔
|
|
201
|
+
4. **姓名查询**: 学习档案接口不支持按姓名查询,需要先通过 `contact +search-user` 获取 dept_user_id
|
|
202
|
+
5. **权限**: 需要 `learningProfile:readonly` 权限,如遇权限错误参考 soke-shared
|
|
203
|
+
6. **数据完整性**: 表格输出仅显示关键字段,完整数据请使用 `--format json`
|
|
204
|
+
|
|
205
|
+
---
|
|
206
|
+
|
|
207
|
+
## 错误处理
|
|
208
|
+
|
|
209
|
+
### 权限不足
|
|
210
|
+
如果遇到权限错误,参考 [`../soke-shared/SKILL.md`](../soke-shared/SKILL.md)
|
|
211
|
+
|
|
212
|
+
### 参数错误
|
|
213
|
+
使用 `--help` 查看命令参数说明:
|
|
214
|
+
```bash
|
|
215
|
+
soke-cli learning-profile +list --help
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
### 数据为空
|
|
219
|
+
如果查询结果为空,检查:
|
|
220
|
+
1. 筛选条件是否正确(dept_ids, dept_user_ids, is_new)
|
|
221
|
+
2. 学员是否有学习记录
|
|
222
|
+
3. 是否有权限查看该部门/学员的数据
|
|
223
|
+
4. 注意:学习档案接口不支持按姓名查询,需要先获取 dept_user_id
|