@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 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.21",
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
 
@@ -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