@sokeai/cli 1.3.0 → 1.3.2
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 +23 -0
- package/package.json +34 -34
- package/scripts/build-binaries.sh +4 -1
- package/scripts/install.js +67 -16
- package/scripts/local-test.js +85 -10
- package/scripts/local-test.sh +7 -27
- package/scripts/test-auto-detect.js +48 -71
- package/scripts/test-local-reconcile.js +55 -0
- package/skills/soke-exam/SKILL.md +0 -319
- package/skills/soke-exam/references/exam-get-exam-user.md +0 -212
package/README.md
CHANGED
|
@@ -39,6 +39,7 @@
|
|
|
39
39
|
4. **AI Agent Skills**
|
|
40
40
|
- 提供 AI Agent 技能(Skills),使 AI 助手能够自动发现和调用 CLI 功能。
|
|
41
41
|
- 支持自然语言交互,无需记忆复杂的命令参数。
|
|
42
|
+
- 支持维护者删除受管 Skill,并通过本地同步与升级安装自动清理废弃 Skill。
|
|
42
43
|
|
|
43
44
|
## 安装方法
|
|
44
45
|
|
|
@@ -164,6 +165,28 @@ soke-cli api GET /users/me
|
|
|
164
165
|
soke-cli api POST /some/endpoint --data '{"key": "value"}'
|
|
165
166
|
```
|
|
166
167
|
|
|
168
|
+
### 删除受管 Skill(维护者)
|
|
169
|
+
|
|
170
|
+
如果你需要从当前仓库中下线一个受管 Skill,可以执行:
|
|
171
|
+
|
|
172
|
+
```bash
|
|
173
|
+
soke-cli delete-skill soke-business-training-report
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
删除后再执行:
|
|
177
|
+
|
|
178
|
+
```bash
|
|
179
|
+
bash ./scripts/local-test.sh
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
本地 Agent 中由 `soke-cli` 管理的过期 `soke-*` Skills 会被自动清理。客户后续执行:
|
|
183
|
+
|
|
184
|
+
```bash
|
|
185
|
+
npm install -g @sokeai/cli@latest
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
也会按当前发布包中的 Skill 集合自动清理废弃的受管 Skill。
|
|
189
|
+
|
|
167
190
|
## 开发指南
|
|
168
191
|
|
|
169
192
|
### 从接口封装到发布的完整流程
|
package/package.json
CHANGED
|
@@ -1,36 +1,36 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
2
|
+
"name": "@sokeai/cli",
|
|
3
|
+
"version": "1.3.2",
|
|
4
|
+
"description": "授客AI官方CLI工具 - 支持AI Agent Skills",
|
|
5
|
+
"bin": {
|
|
6
|
+
"soke-cli": "scripts/run.js"
|
|
7
|
+
},
|
|
8
|
+
"scripts": {
|
|
9
|
+
"postinstall": "node scripts/install.js",
|
|
10
|
+
"build": "./scripts/build-binaries.sh",
|
|
11
|
+
"release": "./scripts/release.sh"
|
|
12
|
+
},
|
|
13
|
+
"files": [
|
|
14
|
+
"scripts/",
|
|
15
|
+
"skills/"
|
|
16
|
+
],
|
|
17
|
+
"repository": {
|
|
18
|
+
"type": "git",
|
|
19
|
+
"url": "https://github.com/liuchenlong1111/soke-cli.git"
|
|
20
|
+
},
|
|
21
|
+
"keywords": [
|
|
22
|
+
"soke",
|
|
23
|
+
"cli",
|
|
24
|
+
"授客AI",
|
|
25
|
+
"shouke",
|
|
26
|
+
"command-line",
|
|
27
|
+
"ai-agent",
|
|
28
|
+
"skills",
|
|
29
|
+
"claude-code"
|
|
30
|
+
],
|
|
31
|
+
"author": "授客AI",
|
|
32
|
+
"license": "MIT",
|
|
33
|
+
"engines": {
|
|
34
|
+
"node": ">=14.0.0"
|
|
35
|
+
}
|
|
36
36
|
}
|
|
@@ -42,7 +42,10 @@ fi
|
|
|
42
42
|
export SOKE_CLIENT_ID SOKE_CLIENT_SECRET
|
|
43
43
|
|
|
44
44
|
# 编译参数
|
|
45
|
-
|
|
45
|
+
# -X 注入路径必须匹配 internal/version.Version 所在包,
|
|
46
|
+
# 此前误写为 main.Version,导致 --version 始终落回 checker.go 里
|
|
47
|
+
# 的 hardcoded 默认值。
|
|
48
|
+
LDFLAGS="-s -w -X codeup.aliyun.com/5edbc121d1d1abe63b55f1c7/soke/soke-cli/internal/version.Version=${VERSION}"
|
|
46
49
|
|
|
47
50
|
# 创建输出目录
|
|
48
51
|
BIN_DIR="bin"
|
package/scripts/install.js
CHANGED
|
@@ -107,6 +107,46 @@ function detectSkillNames(packagedSkillsDir) {
|
|
|
107
107
|
}
|
|
108
108
|
}
|
|
109
109
|
|
|
110
|
+
function removeManagedSkillDir(targetDir, skillName) {
|
|
111
|
+
const dest = path.join(targetDir, skillName);
|
|
112
|
+
if (!fs.existsSync(dest)) return false;
|
|
113
|
+
try {
|
|
114
|
+
fs.rmSync(dest, { recursive: true, force: true });
|
|
115
|
+
return true;
|
|
116
|
+
} catch (_) {
|
|
117
|
+
return false;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function pruneManagedSkillDirs(targetDir, expectedSkillNames) {
|
|
122
|
+
if (!fs.existsSync(targetDir)) return [];
|
|
123
|
+
try {
|
|
124
|
+
const entries = fs.readdirSync(targetDir, { withFileTypes: true });
|
|
125
|
+
const removed = [];
|
|
126
|
+
for (const entry of entries) {
|
|
127
|
+
if (!entry.isDirectory() || !entry.name.startsWith('soke-')) continue;
|
|
128
|
+
if (expectedSkillNames.includes(entry.name)) continue;
|
|
129
|
+
if (removeManagedSkillDir(targetDir, entry.name)) {
|
|
130
|
+
removed.push(entry.name);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
return removed;
|
|
134
|
+
} catch (_) {
|
|
135
|
+
return [];
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function pruneRegistrySkills(registry, expectedSkillNames) {
|
|
140
|
+
if (!registry || !Array.isArray(registry.skills)) return 0;
|
|
141
|
+
const before = registry.skills.length;
|
|
142
|
+
registry.skills = registry.skills.filter((entry) => {
|
|
143
|
+
if (!entry || typeof entry.name !== 'string') return true;
|
|
144
|
+
if (!entry.name.startsWith('soke-')) return true;
|
|
145
|
+
return expectedSkillNames.includes(entry.name);
|
|
146
|
+
});
|
|
147
|
+
return before - registry.skills.length;
|
|
148
|
+
}
|
|
149
|
+
|
|
110
150
|
function syncSkillsToSokeclawWorkspace() {
|
|
111
151
|
const packageRoot = path.join(__dirname, '..');
|
|
112
152
|
const packagedSkillsDir = path.join(packageRoot, 'skills');
|
|
@@ -118,6 +158,7 @@ function syncSkillsToSokeclawWorkspace() {
|
|
|
118
158
|
for (const targetDir of targetDirs) {
|
|
119
159
|
try {
|
|
120
160
|
fs.mkdirSync(targetDir, { recursive: true });
|
|
161
|
+
pruneManagedSkillDirs(targetDir, skillNames);
|
|
121
162
|
for (const skillName of skillNames) {
|
|
122
163
|
const src = path.join(packagedSkillsDir, skillName);
|
|
123
164
|
const dest = path.join(targetDir, skillName);
|
|
@@ -263,6 +304,9 @@ function syncSkillsToWorkclawRegistry() {
|
|
|
263
304
|
if (fs.existsSync(src)) copyDirRecursive(src, dest);
|
|
264
305
|
}
|
|
265
306
|
|
|
307
|
+
pruneManagedSkillDirs(workclawSkillInstallDir, skillNames);
|
|
308
|
+
pruneRegistrySkills(registry, skillNames);
|
|
309
|
+
|
|
266
310
|
// 自动注册所有 skills
|
|
267
311
|
for (const skillName of skillNames) {
|
|
268
312
|
const skillDir = path.join(packagedSkillsDir, skillName);
|
|
@@ -418,29 +462,19 @@ const archMap = {
|
|
|
418
462
|
const mappedPlatform = platformMap[platform];
|
|
419
463
|
const mappedArch = archMap[arch];
|
|
420
464
|
|
|
421
|
-
if (!mappedPlatform || !mappedArch) {
|
|
422
|
-
console.error(`不支持的平台: ${platform}-${arch}`);
|
|
423
|
-
console.error('支持的平台: darwin-x64, darwin-arm64, linux-x64, windows-x64');
|
|
424
|
-
process.exit(1);
|
|
425
|
-
}
|
|
426
|
-
|
|
427
465
|
const binaryName = platform === 'win32' ? 'soke-cli.exe' : 'soke-cli';
|
|
428
|
-
const binaryFileName =
|
|
466
|
+
const binaryFileName = mappedPlatform && mappedArch
|
|
467
|
+
? `soke-cli-${mappedPlatform}-${mappedArch}${platform === 'win32' ? '.exe' : ''}`
|
|
468
|
+
: '';
|
|
429
469
|
|
|
430
470
|
// GitHub Releases 下载地址
|
|
431
|
-
const downloadURL =
|
|
471
|
+
const downloadURL = binaryFileName
|
|
472
|
+
? `https://github.com/liuchenlong1111/soke-cli/releases/download/v${version}/${binaryFileName}`
|
|
473
|
+
: '';
|
|
432
474
|
|
|
433
475
|
const binDir = path.join(__dirname, '..', 'bin');
|
|
434
476
|
const binaryPath = path.join(binDir, binaryName);
|
|
435
477
|
|
|
436
|
-
console.log(`正在为 ${mappedPlatform}-${mappedArch} 下载 soke-cli v${version}...`);
|
|
437
|
-
console.log(`下载地址: ${downloadURL}`);
|
|
438
|
-
|
|
439
|
-
// 创建 bin 目录
|
|
440
|
-
if (!fs.existsSync(binDir)) {
|
|
441
|
-
fs.mkdirSync(binDir, { recursive: true });
|
|
442
|
-
}
|
|
443
|
-
|
|
444
478
|
// 下载文件
|
|
445
479
|
function downloadFile(url, dest) {
|
|
446
480
|
return new Promise((resolve, reject) => {
|
|
@@ -492,6 +526,19 @@ function downloadFile(url, dest) {
|
|
|
492
526
|
|
|
493
527
|
// 执行下载
|
|
494
528
|
if (require.main === module) {
|
|
529
|
+
if (!mappedPlatform || !mappedArch) {
|
|
530
|
+
console.error(`不支持的平台: ${platform}-${arch}`);
|
|
531
|
+
console.error('支持的平台: darwin-x64, darwin-arm64, linux-x64, windows-x64');
|
|
532
|
+
process.exit(1);
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
console.log(`正在为 ${mappedPlatform}-${mappedArch} 下载 soke-cli v${version}...`);
|
|
536
|
+
console.log(`下载地址: ${downloadURL}`);
|
|
537
|
+
|
|
538
|
+
if (!fs.existsSync(binDir)) {
|
|
539
|
+
fs.mkdirSync(binDir, { recursive: true });
|
|
540
|
+
}
|
|
541
|
+
|
|
495
542
|
downloadFile(downloadURL, binaryPath)
|
|
496
543
|
.then(() => {
|
|
497
544
|
// 设置可执行权限(非 Windows 平台)
|
|
@@ -535,6 +582,10 @@ if (require.main === module) {
|
|
|
535
582
|
}
|
|
536
583
|
|
|
537
584
|
module.exports = {
|
|
585
|
+
detectSkillNames,
|
|
586
|
+
parseSkillMetadata,
|
|
587
|
+
pruneManagedSkillDirs,
|
|
588
|
+
pruneRegistrySkills,
|
|
538
589
|
syncSkillsToSokeclawWorkspace,
|
|
539
590
|
syncSkillsToWorkclawRegistry
|
|
540
591
|
};
|
package/scripts/local-test.js
CHANGED
|
@@ -104,6 +104,46 @@ function detectSkillNames(packagedSkillsDir) {
|
|
|
104
104
|
}
|
|
105
105
|
}
|
|
106
106
|
|
|
107
|
+
function removeManagedSkillDir(targetDir, skillName) {
|
|
108
|
+
const skillDir = path.join(targetDir, skillName);
|
|
109
|
+
if (!fs.existsSync(skillDir)) return false;
|
|
110
|
+
try {
|
|
111
|
+
fs.rmSync(skillDir, { recursive: true, force: true });
|
|
112
|
+
return true;
|
|
113
|
+
} catch (_) {
|
|
114
|
+
return false;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function pruneManagedSkillDirs(targetDir, expectedSkillNames) {
|
|
119
|
+
if (!fs.existsSync(targetDir)) return [];
|
|
120
|
+
try {
|
|
121
|
+
const entries = fs.readdirSync(targetDir, { withFileTypes: true });
|
|
122
|
+
const removed = [];
|
|
123
|
+
for (const entry of entries) {
|
|
124
|
+
if (!entry.isDirectory() || !entry.name.startsWith('soke-')) continue;
|
|
125
|
+
if (expectedSkillNames.includes(entry.name)) continue;
|
|
126
|
+
if (removeManagedSkillDir(targetDir, entry.name)) {
|
|
127
|
+
removed.push(entry.name);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
return removed;
|
|
131
|
+
} catch (_) {
|
|
132
|
+
return [];
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function pruneRegistrySkills(registry, expectedSkillNames) {
|
|
137
|
+
if (!registry || !Array.isArray(registry.skills)) return 0;
|
|
138
|
+
const before = registry.skills.length;
|
|
139
|
+
registry.skills = registry.skills.filter((entry) => {
|
|
140
|
+
if (!entry || typeof entry.name !== 'string') return true;
|
|
141
|
+
if (!entry.name.startsWith('soke-')) return true;
|
|
142
|
+
return expectedSkillNames.includes(entry.name);
|
|
143
|
+
});
|
|
144
|
+
return before - registry.skills.length;
|
|
145
|
+
}
|
|
146
|
+
|
|
107
147
|
/**
|
|
108
148
|
* 从 SKILL.md 解析元数据
|
|
109
149
|
*/
|
|
@@ -226,6 +266,7 @@ function detectLocalAgentDirs() {
|
|
|
226
266
|
* 更新 workclaw registry.json
|
|
227
267
|
*/
|
|
228
268
|
function updateWorkclawRegistry(registryPath, skillName, metadata, skillInstallPath) {
|
|
269
|
+
const safeMetadata = metadata && typeof metadata === 'object' ? metadata : {};
|
|
229
270
|
let registry;
|
|
230
271
|
|
|
231
272
|
try {
|
|
@@ -238,11 +279,11 @@ function updateWorkclawRegistry(registryPath, skillName, metadata, skillInstallP
|
|
|
238
279
|
if (!registry.migrations) registry.migrations = {};
|
|
239
280
|
if (!Array.isArray(registry.skills)) registry.skills = [];
|
|
240
281
|
|
|
241
|
-
const displayName =
|
|
242
|
-
const description =
|
|
243
|
-
const version =
|
|
282
|
+
const displayName = safeMetadata.summary || safeMetadata.name || skillName;
|
|
283
|
+
const description = safeMetadata.description || `${displayName} - 授客AI CLI工具`;
|
|
284
|
+
const version = safeMetadata.version || '1.0.0';
|
|
244
285
|
const emoji = inferSkillEmoji(skillName);
|
|
245
|
-
const requires =
|
|
286
|
+
const requires = safeMetadata.bins ? { bins: safeMetadata.bins } : {};
|
|
246
287
|
|
|
247
288
|
const skillEntry = {
|
|
248
289
|
id: `skill:${skillName}`,
|
|
@@ -323,6 +364,26 @@ function distributeSkill(skillName, packagedSkillsDir, targetDirs) {
|
|
|
323
364
|
return successCount > 0;
|
|
324
365
|
}
|
|
325
366
|
|
|
367
|
+
function reconcileTarget(target, expectedSkillNames) {
|
|
368
|
+
const removedDirs = pruneManagedSkillDirs(target.path, expectedSkillNames);
|
|
369
|
+
if (removedDirs.length > 0) {
|
|
370
|
+
logInfo(` 清理过期 skills: ${removedDirs.join(', ')}`);
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
if (target.type === 'workclaw' && target.registryPath && fs.existsSync(target.registryPath)) {
|
|
374
|
+
try {
|
|
375
|
+
const registry = JSON.parse(fs.readFileSync(target.registryPath, 'utf8'));
|
|
376
|
+
const removedEntries = pruneRegistrySkills(registry, expectedSkillNames);
|
|
377
|
+
if (removedEntries > 0) {
|
|
378
|
+
fs.writeFileSync(target.registryPath, JSON.stringify(registry, null, 2));
|
|
379
|
+
logInfo(` 清理 registry 过期条目: ${removedEntries}`);
|
|
380
|
+
}
|
|
381
|
+
} catch (err) {
|
|
382
|
+
logError(` ✗ 清理 registry 失败: ${err.message}`);
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
|
|
326
387
|
/**
|
|
327
388
|
* 清理所有本地环境中的 skills
|
|
328
389
|
*/
|
|
@@ -455,6 +516,10 @@ function main() {
|
|
|
455
516
|
}
|
|
456
517
|
}
|
|
457
518
|
|
|
519
|
+
for (const target of targetDirs) {
|
|
520
|
+
reconcileTarget(target, allSkills);
|
|
521
|
+
}
|
|
522
|
+
|
|
458
523
|
// 总结
|
|
459
524
|
logSection('分发完成');
|
|
460
525
|
|
|
@@ -493,11 +558,21 @@ function main() {
|
|
|
493
558
|
console.log('');
|
|
494
559
|
}
|
|
495
560
|
|
|
561
|
+
module.exports = {
|
|
562
|
+
detectSkillNames,
|
|
563
|
+
parseSkillMetadata,
|
|
564
|
+
pruneManagedSkillDirs,
|
|
565
|
+
pruneRegistrySkills,
|
|
566
|
+
updateWorkclawRegistry
|
|
567
|
+
};
|
|
568
|
+
|
|
496
569
|
// 运行
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
570
|
+
if (require.main === module) {
|
|
571
|
+
try {
|
|
572
|
+
main();
|
|
573
|
+
} catch (err) {
|
|
574
|
+
logError(`发生错误: ${err.message}`);
|
|
575
|
+
console.error(err);
|
|
576
|
+
process.exit(1);
|
|
577
|
+
}
|
|
503
578
|
}
|
package/scripts/local-test.sh
CHANGED
|
@@ -109,34 +109,15 @@ echo ""
|
|
|
109
109
|
|
|
110
110
|
# 步骤3.5: 分发 Skills 到本地 Agent
|
|
111
111
|
echo -e "${YELLOW}[3.5/4] 分发 Skills 到本地 Agent...${NC}"
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
# 使用 node 调用 install.js 中导出的同步函数
|
|
116
|
-
cat > ./scripts/sync-skills-local.js << 'EOF'
|
|
117
|
-
const install = require('./install.js');
|
|
118
|
-
|
|
119
|
-
try {
|
|
120
|
-
console.log(" 同步到 Sokeclaw Workspace...");
|
|
121
|
-
install.syncSkillsToSokeclawWorkspace();
|
|
122
|
-
console.log(" ✓ Sokeclaw Workspace 同步成功");
|
|
123
|
-
} catch (e) {
|
|
124
|
-
console.error(" ✗ Sokeclaw Workspace 同步失败:", e.message);
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
try {
|
|
128
|
-
console.log(" 同步到 Workclaw Registry...");
|
|
129
|
-
install.syncSkillsToWorkclawRegistry();
|
|
130
|
-
console.log(" ✓ Workclaw Registry 同步成功");
|
|
131
|
-
} catch (e) {
|
|
132
|
-
console.error(" ✗ Workclaw Registry 同步失败:", e.message);
|
|
133
|
-
}
|
|
134
|
-
EOF
|
|
135
|
-
node ./scripts/sync-skills-local.js
|
|
136
|
-
rm ./scripts/sync-skills-local.js
|
|
112
|
+
echo -e " 执行 scripts/local-test.js 同步并清理本地 Agent Skills..."
|
|
113
|
+
if node ./scripts/local-test.js; then
|
|
137
114
|
echo -e "${GREEN} ✓ Skills 同步完成${NC}"
|
|
115
|
+
if [ "$SKIP_GLOBAL" = true ]; then
|
|
116
|
+
echo -e "${YELLOW} 提示: 已跳过全局 CLI 覆盖,但本地 Agent Skills 已按当前仓库状态完成同步${NC}"
|
|
117
|
+
fi
|
|
138
118
|
else
|
|
139
|
-
echo -e "${
|
|
119
|
+
echo -e "${RED} ✗ Skills 同步失败${NC}"
|
|
120
|
+
exit 1
|
|
140
121
|
fi
|
|
141
122
|
echo ""
|
|
142
123
|
|
|
@@ -197,4 +178,3 @@ echo ""
|
|
|
197
178
|
echo -e "${GREEN}========================================${NC}"
|
|
198
179
|
echo -e "${GREEN} 本地测试完成! ✓${NC}"
|
|
199
180
|
echo -e "${GREEN}========================================${NC}"
|
|
200
|
-
|
|
@@ -5,66 +5,14 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
const fs = require('fs');
|
|
8
|
+
const os = require('os');
|
|
8
9
|
const path = require('path');
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
const entries = fs.readdirSync(packagedSkillsDir, { withFileTypes: true });
|
|
16
|
-
return entries
|
|
17
|
-
.filter(entry => entry.isDirectory() && entry.name.startsWith('soke-'))
|
|
18
|
-
.map(entry => entry.name)
|
|
19
|
-
.sort();
|
|
20
|
-
} catch (_) {
|
|
21
|
-
return [];
|
|
22
|
-
}
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
// 复制 parseSkillMetadata 函数
|
|
26
|
-
function parseSkillMetadata(skillDir) {
|
|
27
|
-
const skillMdPath = path.join(skillDir, 'SKILL.md');
|
|
28
|
-
if (!fs.existsSync(skillMdPath)) {
|
|
29
|
-
return null;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
try {
|
|
33
|
-
const content = fs.readFileSync(skillMdPath, 'utf8');
|
|
34
|
-
|
|
35
|
-
const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---/);
|
|
36
|
-
if (!frontmatterMatch) return null;
|
|
37
|
-
|
|
38
|
-
const frontmatter = frontmatterMatch[1];
|
|
39
|
-
const metadata = {};
|
|
40
|
-
|
|
41
|
-
const nameMatch = frontmatter.match(/^name:\s*(.+)$/m);
|
|
42
|
-
if (nameMatch) metadata.name = nameMatch[1].trim();
|
|
43
|
-
|
|
44
|
-
const summaryMatch = frontmatter.match(/^summary:\s*(.+)$/m);
|
|
45
|
-
if (summaryMatch) metadata.summary = summaryMatch[1].trim();
|
|
46
|
-
|
|
47
|
-
const descMatch = frontmatter.match(/^description:\s*["'](.+)["']$/m);
|
|
48
|
-
if (descMatch) {
|
|
49
|
-
metadata.description = descMatch[1].trim();
|
|
50
|
-
} else {
|
|
51
|
-
const descMatch2 = frontmatter.match(/^description:\s*(.+)$/m);
|
|
52
|
-
if (descMatch2) metadata.description = descMatch2[1].trim();
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
const versionMatch = frontmatter.match(/^version:\s*(.+)$/m);
|
|
56
|
-
if (versionMatch) metadata.version = versionMatch[1].trim();
|
|
57
|
-
|
|
58
|
-
const binsMatch = frontmatter.match(/bins:\s*\[(.+?)\]/);
|
|
59
|
-
if (binsMatch) {
|
|
60
|
-
metadata.bins = binsMatch[1].split(',').map(b => b.trim().replace(/['"]/g, ''));
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
return metadata;
|
|
64
|
-
} catch (_) {
|
|
65
|
-
return null;
|
|
66
|
-
}
|
|
67
|
-
}
|
|
10
|
+
const {
|
|
11
|
+
detectSkillNames,
|
|
12
|
+
parseSkillMetadata,
|
|
13
|
+
pruneManagedSkillDirs,
|
|
14
|
+
pruneRegistrySkills
|
|
15
|
+
} = require('./install.js');
|
|
68
16
|
|
|
69
17
|
// 测试
|
|
70
18
|
const packageRoot = path.join(__dirname, '..');
|
|
@@ -102,19 +50,13 @@ for (const skillName of skillNames) {
|
|
|
102
50
|
// 验证结果
|
|
103
51
|
console.log('📊 验证结果:\n');
|
|
104
52
|
|
|
105
|
-
const
|
|
106
|
-
const
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
console.log(`❌ 缺少的 skills: ${missingSkills.join(', ')}`);
|
|
111
|
-
} else {
|
|
112
|
-
console.log('✅ 所有预期的 skills 都已检测到');
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
if (extraSkills.length > 0) {
|
|
116
|
-
console.log(`ℹ️ 额外的 skills: ${extraSkills.join(', ')}`);
|
|
53
|
+
const requiredSkills = ['soke-shared'];
|
|
54
|
+
const missingRequiredSkills = requiredSkills.filter(s => !skillNames.includes(s));
|
|
55
|
+
if (missingRequiredSkills.length > 0) {
|
|
56
|
+
console.log(`❌ 缺少基础 skills: ${missingRequiredSkills.join(', ')}`);
|
|
57
|
+
process.exit(1);
|
|
117
58
|
}
|
|
59
|
+
console.log('✅ 基础 skills 检测通过');
|
|
118
60
|
|
|
119
61
|
console.log('');
|
|
120
62
|
console.log('🎉 测试完成!');
|
|
@@ -123,3 +65,38 @@ console.log('💡 提示:');
|
|
|
123
65
|
console.log(' - 新增 skill 时,只需在 skills/ 目录下创建 soke-* 目录');
|
|
124
66
|
console.log(' - 确保每个 skill 都有 SKILL.md 文件,包含完整的 frontmatter');
|
|
125
67
|
console.log(' - install.js 会自动检测并注册所有 skills');
|
|
68
|
+
|
|
69
|
+
// 验证清理逻辑
|
|
70
|
+
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'soke-skill-prune-'));
|
|
71
|
+
const targetDir = path.join(tempRoot, 'skills');
|
|
72
|
+
fs.mkdirSync(targetDir, { recursive: true });
|
|
73
|
+
fs.mkdirSync(path.join(targetDir, 'soke-stale'));
|
|
74
|
+
fs.mkdirSync(path.join(targetDir, 'soke-course'));
|
|
75
|
+
fs.mkdirSync(path.join(targetDir, 'custom-skill'));
|
|
76
|
+
|
|
77
|
+
const removedDirs = pruneManagedSkillDirs(targetDir, ['soke-course']);
|
|
78
|
+
if (!removedDirs.includes('soke-stale')) {
|
|
79
|
+
console.error('❌ pruneManagedSkillDirs 未删除过期 managed skill');
|
|
80
|
+
process.exit(1);
|
|
81
|
+
}
|
|
82
|
+
if (!fs.existsSync(path.join(targetDir, 'custom-skill'))) {
|
|
83
|
+
console.error('❌ pruneManagedSkillDirs 错误删除了非受管 skill');
|
|
84
|
+
process.exit(1);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const registry = {
|
|
88
|
+
skills: [
|
|
89
|
+
{ name: 'soke-stale' },
|
|
90
|
+
{ name: 'soke-course' },
|
|
91
|
+
{ name: 'custom-skill' }
|
|
92
|
+
]
|
|
93
|
+
};
|
|
94
|
+
const removedEntries = pruneRegistrySkills(registry, ['soke-course']);
|
|
95
|
+
if (removedEntries !== 1) {
|
|
96
|
+
console.error('❌ pruneRegistrySkills 未正确移除过期条目');
|
|
97
|
+
process.exit(1);
|
|
98
|
+
}
|
|
99
|
+
if (!registry.skills.find((entry) => entry.name === 'custom-skill')) {
|
|
100
|
+
console.error('❌ pruneRegistrySkills 错误移除了非受管条目');
|
|
101
|
+
process.exit(1);
|
|
102
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const os = require('os');
|
|
5
|
+
const path = require('path');
|
|
6
|
+
const {
|
|
7
|
+
pruneManagedSkillDirs,
|
|
8
|
+
pruneRegistrySkills,
|
|
9
|
+
updateWorkclawRegistry
|
|
10
|
+
} = require('./local-test.js');
|
|
11
|
+
|
|
12
|
+
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'soke-local-reconcile-'));
|
|
13
|
+
const targetDir = path.join(tempRoot, 'skills');
|
|
14
|
+
fs.mkdirSync(targetDir, { recursive: true });
|
|
15
|
+
|
|
16
|
+
fs.mkdirSync(path.join(targetDir, 'soke-stale'));
|
|
17
|
+
fs.mkdirSync(path.join(targetDir, 'soke-exam'));
|
|
18
|
+
fs.mkdirSync(path.join(targetDir, 'custom-skill'));
|
|
19
|
+
|
|
20
|
+
const removedDirs = pruneManagedSkillDirs(targetDir, ['soke-exam']);
|
|
21
|
+
if (!removedDirs.includes('soke-stale')) {
|
|
22
|
+
console.error('❌ stale managed skill was not removed');
|
|
23
|
+
process.exit(1);
|
|
24
|
+
}
|
|
25
|
+
if (!fs.existsSync(path.join(targetDir, 'custom-skill'))) {
|
|
26
|
+
console.error('❌ unmanaged local skill should remain untouched');
|
|
27
|
+
process.exit(1);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const registry = {
|
|
31
|
+
skills: [
|
|
32
|
+
{ name: 'soke-stale' },
|
|
33
|
+
{ name: 'soke-exam' },
|
|
34
|
+
{ name: 'custom-skill' }
|
|
35
|
+
]
|
|
36
|
+
};
|
|
37
|
+
const removedEntries = pruneRegistrySkills(registry, ['soke-exam']);
|
|
38
|
+
if (removedEntries !== 1) {
|
|
39
|
+
console.error('❌ registry stale entry removal failed');
|
|
40
|
+
process.exit(1);
|
|
41
|
+
}
|
|
42
|
+
if (!registry.skills.find((entry) => entry.name === 'custom-skill')) {
|
|
43
|
+
console.error('❌ unmanaged registry entry should remain untouched');
|
|
44
|
+
process.exit(1);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const registryPath = path.join(tempRoot, 'registry.json');
|
|
48
|
+
updateWorkclawRegistry(registryPath, 'soke-contact', null, '/tmp/soke-contact');
|
|
49
|
+
const updatedRegistry = JSON.parse(fs.readFileSync(registryPath, 'utf8'));
|
|
50
|
+
if (!updatedRegistry.skills.find((entry) => entry.name === 'soke-contact')) {
|
|
51
|
+
console.error('❌ registry update should tolerate missing metadata');
|
|
52
|
+
process.exit(1);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
console.log('✅ local reconciliation tests passed');
|
|
@@ -1,319 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: soke-exam
|
|
3
|
-
summary: 授客考试管理(考试列表/分类/考试用户成绩/详情),通过 soke-cli 查询
|
|
4
|
-
version: 1.0.0
|
|
5
|
-
description: "授客考试管理:查询考试、考试用户和成绩。查询考试列表、考试分类、考试用户成绩、考试详情。当用户需要查询考试成绩、查看考试列表、查询考试用户信息、查看考试分类时使用。"
|
|
6
|
-
requiredAuthorizations: [{"platformId":"soke-ai"}]
|
|
7
|
-
metadata:
|
|
8
|
-
requires:
|
|
9
|
-
bins: ["soke-cli"]
|
|
10
|
-
cliHelp: "soke-cli exam --help"
|
|
11
|
-
---
|
|
12
|
-
|
|
13
|
-
# 考试管理 (exam)
|
|
14
|
-
|
|
15
|
-
**CRITICAL — 开始前 MUST 先用 Read 工具读取 [`../soke-shared/SKILL.md`](../soke-shared/SKILL.md),其中包含认证、配置、权限处理**
|
|
16
|
-
|
|
17
|
-
## 核心概念
|
|
18
|
-
|
|
19
|
-
- **Exam(考试)**: 考试实体,包含标题、时间范围、状态等信息,通过 `uuid` 标识
|
|
20
|
-
- **ExamUser(考试用户)**: 用户的考试记录,包含成绩、状态、答题时间等,通过 `target_id` 标识
|
|
21
|
-
- **Category(考试分类)**: 考试分类,支持层级结构,通过 `uuid` 标识
|
|
22
|
-
- **DeptUser(部门用户)**: 企业内的用户,通过 `dept_user_id` 标识
|
|
23
|
-
|
|
24
|
-
## 资源关系
|
|
25
|
-
|
|
26
|
-
```
|
|
27
|
-
Exam (考试)
|
|
28
|
-
├── ExamUser (考试用户记录)
|
|
29
|
-
│ ├── dept_user_id (用户ID)
|
|
30
|
-
│ ├── score (成绩)
|
|
31
|
-
│ ├── exam_status (考试状态)
|
|
32
|
-
│ └── submit_time (提交时间)
|
|
33
|
-
└── Category (考试分类)
|
|
34
|
-
```
|
|
35
|
-
|
|
36
|
-
## Shortcuts(推荐优先使用)
|
|
37
|
-
|
|
38
|
-
Shortcut 是对常用操作的高级封装(`soke-cli exam +<verb> [flags]`)。有 Shortcut 的操作优先使用。
|
|
39
|
-
|
|
40
|
-
| Shortcut | 说明 |
|
|
41
|
-
|----------|------|
|
|
42
|
-
| [`+list-exams`](#list-exams) | 列出考试列表,支持时间范围和状态筛选 |
|
|
43
|
-
| [`+list-exam-users`](#list-exam-users) | 列出考试用户成绩列表,支持用户筛选和时间范围 |
|
|
44
|
-
| [`+get-exam-user`](#get-exam-user) | 获取单个考试用户的详细成绩信息 |
|
|
45
|
-
| [`+list-categories`](#list-categories) | 列出考试分类 |
|
|
46
|
-
|
|
47
|
-
## 命令详解
|
|
48
|
-
|
|
49
|
-
### +list-exams
|
|
50
|
-
|
|
51
|
-
列出考试列表,支持按时间范围和状态筛选。
|
|
52
|
-
|
|
53
|
-
**命令格式**:
|
|
54
|
-
```bash
|
|
55
|
-
soke-cli exam +list-exams \
|
|
56
|
-
--start-time <timestamp> \
|
|
57
|
-
--end-time <timestamp> \
|
|
58
|
-
[--status <status>] \
|
|
59
|
-
[--page <page>] \
|
|
60
|
-
[--page-size <size>]
|
|
61
|
-
```
|
|
62
|
-
|
|
63
|
-
**参数说明**:
|
|
64
|
-
- `--start-time`: 开始时间(Unix时间戳,毫秒)**必需**
|
|
65
|
-
- `--end-time`: 结束时间(Unix时间戳,毫秒)**必需**
|
|
66
|
-
- `--status`: 考试状态(可选)
|
|
67
|
-
- `--page`: 页码,从1开始(默认: 1)
|
|
68
|
-
- `--page-size`: 每页数量,最大100(默认: 100)
|
|
69
|
-
|
|
70
|
-
**返回字段**:
|
|
71
|
-
- `uuid`: 考试ID
|
|
72
|
-
- `title`: 考试标题
|
|
73
|
-
- `start_time`: 开始时间
|
|
74
|
-
- `end_time`: 结束时间
|
|
75
|
-
- `status`: 考试状态
|
|
76
|
-
|
|
77
|
-
**示例**:
|
|
78
|
-
```bash
|
|
79
|
-
# 查询2023年的所有考试
|
|
80
|
-
soke-cli exam +list-exams \
|
|
81
|
-
--start-time 1672502400000 \
|
|
82
|
-
--end-time 1704038400000
|
|
83
|
-
|
|
84
|
-
# 查询进行中的考试
|
|
85
|
-
soke-cli exam +list-exams \
|
|
86
|
-
--start-time 1672502400000 \
|
|
87
|
-
--end-time 1704038400000 \
|
|
88
|
-
--status "进行中"
|
|
89
|
-
```
|
|
90
|
-
|
|
91
|
-
**权限要求**: `exam:exam:readonly`
|
|
92
|
-
|
|
93
|
-
---
|
|
94
|
-
|
|
95
|
-
### +list-exam-users
|
|
96
|
-
|
|
97
|
-
列出考试用户成绩列表,支持按用户ID和完成时间筛选。
|
|
98
|
-
|
|
99
|
-
**命令格式**:
|
|
100
|
-
```bash
|
|
101
|
-
soke-cli exam +list-exam-users \
|
|
102
|
-
--exam-id <exam_id> \
|
|
103
|
-
[--userid-list <user_ids>] \
|
|
104
|
-
[--finish-start-time <timestamp>] \
|
|
105
|
-
[--finish-end-time <timestamp>] \
|
|
106
|
-
[--page <page>] \
|
|
107
|
-
[--page-size <size>]
|
|
108
|
-
```
|
|
109
|
-
|
|
110
|
-
**参数说明**:
|
|
111
|
-
- `--exam-id`: 考试ID **必需**
|
|
112
|
-
- `--userid-list`: 用户ID列表,逗号分隔,最多100个(可选)
|
|
113
|
-
- `--finish-start-time`: 完成开始时间(Unix时间戳,毫秒)(可选)
|
|
114
|
-
- `--finish-end-time`: 完成结束时间(Unix时间戳,毫秒)(可选)
|
|
115
|
-
- `--page`: 页码,从1开始(默认: 1)
|
|
116
|
-
- `--page-size`: 每页数量,最大100(默认: 100)
|
|
117
|
-
|
|
118
|
-
**返回字段**:
|
|
119
|
-
- `target_id`: 考试用户记录ID
|
|
120
|
-
- `dept_user_id`: 部门用户ID
|
|
121
|
-
- `score`: 成绩
|
|
122
|
-
- `exam_status`: 考试状态
|
|
123
|
-
- `create_time`: 创建时间
|
|
124
|
-
|
|
125
|
-
**示例**:
|
|
126
|
-
```bash
|
|
127
|
-
# 查询某个考试的所有用户成绩
|
|
128
|
-
soke-cli exam +list-exam-users --exam-id exam123
|
|
129
|
-
|
|
130
|
-
# 查询特定用户的成绩
|
|
131
|
-
soke-cli exam +list-exam-users \
|
|
132
|
-
--exam-id exam123 \
|
|
133
|
-
--userid-list "user1,user2,user3"
|
|
134
|
-
|
|
135
|
-
# 查询某个时间段内完成的考试
|
|
136
|
-
soke-cli exam +list-exam-users \
|
|
137
|
-
--exam-id exam123 \
|
|
138
|
-
--finish-start-time 1672502400000 \
|
|
139
|
-
--finish-end-time 1704038400000
|
|
140
|
-
```
|
|
141
|
-
|
|
142
|
-
**权限要求**: `exam:examUser:readonly`
|
|
143
|
-
|
|
144
|
-
---
|
|
145
|
-
|
|
146
|
-
### +get-exam-user
|
|
147
|
-
|
|
148
|
-
获取单个考试用户的详细成绩信息,包含答题详情。
|
|
149
|
-
|
|
150
|
-
**命令格式**:
|
|
151
|
-
```bash
|
|
152
|
-
soke-cli exam +get-exam-user \
|
|
153
|
-
--exam-id <exam_id> \
|
|
154
|
-
--dept-user-id <dept_user_id>
|
|
155
|
-
```
|
|
156
|
-
|
|
157
|
-
**参数说明**:
|
|
158
|
-
- `--exam-id`: 考试ID **必需**
|
|
159
|
-
- `--dept-user-id`: 部门用户ID **必需**
|
|
160
|
-
|
|
161
|
-
**返回字段**:
|
|
162
|
-
- `target_id`: 考试用户记录ID
|
|
163
|
-
- `target_title`: 考试标题
|
|
164
|
-
- `dept_user_id`: 部门用户ID
|
|
165
|
-
- `score`: 成绩
|
|
166
|
-
- `exam_status`: 考试状态
|
|
167
|
-
- `start_time`: 开始时间
|
|
168
|
-
- `submit_time`: 提交时间
|
|
169
|
-
- `question_count`: 题目数量
|
|
170
|
-
- `create_time`: 创建时间
|
|
171
|
-
|
|
172
|
-
**示例**:
|
|
173
|
-
```bash
|
|
174
|
-
# 查询张三的考试成绩
|
|
175
|
-
soke-cli exam +get-exam-user \
|
|
176
|
-
--exam-id exam123 \
|
|
177
|
-
--dept-user-id user456
|
|
178
|
-
```
|
|
179
|
-
|
|
180
|
-
**权限要求**: `exam:examUser:readonly`
|
|
181
|
-
|
|
182
|
-
**使用场景**:
|
|
183
|
-
- 当用户询问"查询某人的考试成绩"时使用
|
|
184
|
-
- 需要同时提供考试ID和用户ID
|
|
185
|
-
- 如果只知道用户名,需要先通过 `soke-cli contact +search-user` 查询用户ID
|
|
186
|
-
|
|
187
|
-
---
|
|
188
|
-
|
|
189
|
-
### +list-categories
|
|
190
|
-
|
|
191
|
-
列出考试分类,支持分页。
|
|
192
|
-
|
|
193
|
-
**命令格式**:
|
|
194
|
-
```bash
|
|
195
|
-
soke-cli exam +list-categories \
|
|
196
|
-
[--page <page>] \
|
|
197
|
-
[--page-size <size>]
|
|
198
|
-
```
|
|
199
|
-
|
|
200
|
-
**参数说明**:
|
|
201
|
-
- `--page`: 页码,从1开始(默认: 1)
|
|
202
|
-
- `--page-size`: 每页数量,最大100(默认: 100)
|
|
203
|
-
|
|
204
|
-
**返回字段**:
|
|
205
|
-
- `uuid`: 分类ID
|
|
206
|
-
- `title`: 分类名称
|
|
207
|
-
- `parent_id`: 父分类ID
|
|
208
|
-
- `create_time`: 创建时间
|
|
209
|
-
|
|
210
|
-
**示例**:
|
|
211
|
-
```bash
|
|
212
|
-
# 查询所有考试分类
|
|
213
|
-
soke-cli exam +list-categories
|
|
214
|
-
|
|
215
|
-
# 分页查询
|
|
216
|
-
soke-cli exam +list-categories --page 1 --page-size 20
|
|
217
|
-
```
|
|
218
|
-
|
|
219
|
-
**权限要求**: `exam:category:readonly`
|
|
220
|
-
|
|
221
|
-
## 通用API调用
|
|
222
|
-
|
|
223
|
-
如果Shortcuts不满足需求,可以使用通用API调用:
|
|
224
|
-
|
|
225
|
-
```bash
|
|
226
|
-
soke-cli api <METHOD> <path> [--params <json>]
|
|
227
|
-
```
|
|
228
|
-
|
|
229
|
-
示例:
|
|
230
|
-
```bash
|
|
231
|
-
soke-cli api GET /exam/exam/list --params '{"start_time":"1672502400000","end_time":"1704038400000"}'
|
|
232
|
-
```
|
|
233
|
-
|
|
234
|
-
## 权限表
|
|
235
|
-
|
|
236
|
-
| 操作 | 所需权限 |
|
|
237
|
-
|------|---------|
|
|
238
|
-
| `+list-exams` | `exam:exam:readonly` |
|
|
239
|
-
| `+list-exam-users` | `exam:examUser:readonly` |
|
|
240
|
-
| `+get-exam-user` | `exam:examUser:readonly` |
|
|
241
|
-
| `+list-categories` | `exam:category:readonly` |
|
|
242
|
-
|
|
243
|
-
## 常见工作流
|
|
244
|
-
|
|
245
|
-
### 工作流1: 查询用户考试成绩
|
|
246
|
-
|
|
247
|
-
当用户询问"查询张三的考试成绩"时:
|
|
248
|
-
|
|
249
|
-
**步骤1**: 如果只知道用户名,先查询用户ID
|
|
250
|
-
```bash
|
|
251
|
-
soke-cli contact +search-user --name "张三"
|
|
252
|
-
```
|
|
253
|
-
|
|
254
|
-
**步骤2**: 获取考试列表,找到目标考试ID
|
|
255
|
-
```bash
|
|
256
|
-
soke-cli exam +list-exams \
|
|
257
|
-
--start-time 1672502400000 \
|
|
258
|
-
--end-time 1704038400000
|
|
259
|
-
```
|
|
260
|
-
|
|
261
|
-
**步骤3**: 查询该用户的考试成绩
|
|
262
|
-
```bash
|
|
263
|
-
soke-cli exam +get-exam-user \
|
|
264
|
-
--exam-id <exam_id> \
|
|
265
|
-
--dept-user-id <dept_user_id>
|
|
266
|
-
```
|
|
267
|
-
|
|
268
|
-
### 工作流2: 统计考试完成情况
|
|
269
|
-
|
|
270
|
-
当用户询问"统计某个考试的完成情况"时:
|
|
271
|
-
|
|
272
|
-
**步骤1**: 获取考试用户列表
|
|
273
|
-
```bash
|
|
274
|
-
soke-cli exam +list-exam-users --exam-id <exam_id>
|
|
275
|
-
```
|
|
276
|
-
|
|
277
|
-
**步骤2**: 分析返回的数据
|
|
278
|
-
- 统计 `exam_status` 字段的分布
|
|
279
|
-
- 计算平均分(`score` 字段)
|
|
280
|
-
- 统计完成人数
|
|
281
|
-
|
|
282
|
-
### 工作流3: 查询某个时间段的考试
|
|
283
|
-
|
|
284
|
-
当用户询问"查询本月的考试"时:
|
|
285
|
-
|
|
286
|
-
**步骤1**: 计算时间范围(Unix时间戳,毫秒)
|
|
287
|
-
```bash
|
|
288
|
-
# 例如:2024年1月1日 00:00:00 = 1704038400000
|
|
289
|
-
# 2024年1月31日 23:59:59 = 1706716799000
|
|
290
|
-
```
|
|
291
|
-
|
|
292
|
-
**步骤2**: 查询考试列表
|
|
293
|
-
```bash
|
|
294
|
-
soke-cli exam +list-exams \
|
|
295
|
-
--start-time 1704038400000 \
|
|
296
|
-
--end-time 1706716799000
|
|
297
|
-
```
|
|
298
|
-
|
|
299
|
-
## 注意事项
|
|
300
|
-
|
|
301
|
-
1. **时间格式**: 所有时间参数使用Unix时间戳(毫秒),不是秒
|
|
302
|
-
2. **分页**: 默认每页100条,最大100条,超过需要分页查询
|
|
303
|
-
3. **用户ID**: `dept_user_id` 是企业内的用户ID,不是用户名
|
|
304
|
-
4. **考试ID**: `exam-id` 和 `uuid` 是同一个字段,都表示考试ID
|
|
305
|
-
5. **权限**: 所有操作都需要先完成认证(`soke-cli auth login`)
|
|
306
|
-
|
|
307
|
-
## 错误处理
|
|
308
|
-
|
|
309
|
-
### 权限不足
|
|
310
|
-
如果遇到权限错误,参考 [`../soke-shared/SKILL.md`](../soke-shared/SKILL.md) 中的权限处理章节。
|
|
311
|
-
|
|
312
|
-
### 参数错误
|
|
313
|
-
使用 `--help` 查看命令参数说明:
|
|
314
|
-
```bash
|
|
315
|
-
soke-cli exam +get-exam-user --help
|
|
316
|
-
```
|
|
317
|
-
|
|
318
|
-
### 数据不存在
|
|
319
|
-
如果查询的考试或用户不存在,API会返回空数据或错误提示。
|
|
@@ -1,212 +0,0 @@
|
|
|
1
|
-
# +get-exam-user - 获取考试用户详细成绩
|
|
2
|
-
|
|
3
|
-
## 概述
|
|
4
|
-
|
|
5
|
-
获取单个用户在特定考试中的详细成绩信息,包括分数、状态、答题时间等。
|
|
6
|
-
|
|
7
|
-
## 命令格式
|
|
8
|
-
|
|
9
|
-
```bash
|
|
10
|
-
soke-cli exam +get-exam-user \
|
|
11
|
-
--exam-id <exam_id> \
|
|
12
|
-
--dept-user-id <dept_user_id>
|
|
13
|
-
```
|
|
14
|
-
|
|
15
|
-
## 参数说明
|
|
16
|
-
|
|
17
|
-
### 必需参数
|
|
18
|
-
|
|
19
|
-
| 参数 | 类型 | 说明 |
|
|
20
|
-
|------|------|------|
|
|
21
|
-
| `--exam-id` | string | 考试ID(uuid) |
|
|
22
|
-
| `--dept-user-id` | string | 部门用户ID |
|
|
23
|
-
|
|
24
|
-
### 可选参数
|
|
25
|
-
|
|
26
|
-
| 参数 | 类型 | 默认值 | 说明 |
|
|
27
|
-
|------|------|--------|------|
|
|
28
|
-
| `--format` | string | json | 输出格式(json/table) |
|
|
29
|
-
|
|
30
|
-
## 返回数据
|
|
31
|
-
|
|
32
|
-
### JSON格式
|
|
33
|
-
|
|
34
|
-
```json
|
|
35
|
-
{
|
|
36
|
-
"code": 0,
|
|
37
|
-
"msg": "success",
|
|
38
|
-
"data": {
|
|
39
|
-
"target_id": "exam_user_123",
|
|
40
|
-
"target_title": "2024年度安全培训考试",
|
|
41
|
-
"dept_user_id": "user456",
|
|
42
|
-
"score": 85,
|
|
43
|
-
"exam_status": "已完成",
|
|
44
|
-
"start_time": 1704038400000,
|
|
45
|
-
"submit_time": 1704042000000,
|
|
46
|
-
"question_count": 20,
|
|
47
|
-
"create_time": 1704038400000
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
```
|
|
51
|
-
|
|
52
|
-
### 表格格式
|
|
53
|
-
|
|
54
|
-
```
|
|
55
|
-
target_id | target_title | dept_user_id | score | exam_status | start_time | submit_time | question_count | create_time
|
|
56
|
-
exam_user_123 | 2024年度安全培训考试 | user456 | 85 | 已完成 | 1704038400000 | 1704042000000 | 20 | 1704038400000
|
|
57
|
-
```
|
|
58
|
-
|
|
59
|
-
## 字段说明
|
|
60
|
-
|
|
61
|
-
| 字段 | 类型 | 说明 |
|
|
62
|
-
|------|------|------|
|
|
63
|
-
| `target_id` | string | 考试用户记录ID |
|
|
64
|
-
| `target_title` | string | 考试标题 |
|
|
65
|
-
| `dept_user_id` | string | 部门用户ID |
|
|
66
|
-
| `score` | number | 考试成绩(分数) |
|
|
67
|
-
| `exam_status` | string | 考试状态(如:已完成、进行中、未开始) |
|
|
68
|
-
| `start_time` | number | 开始答题时间(Unix时间戳,毫秒) |
|
|
69
|
-
| `submit_time` | number | 提交时间(Unix时间戳,毫秒) |
|
|
70
|
-
| `question_count` | number | 题目总数 |
|
|
71
|
-
| `create_time` | number | 记录创建时间(Unix时间戳,毫秒) |
|
|
72
|
-
|
|
73
|
-
## 使用示例
|
|
74
|
-
|
|
75
|
-
### 示例1: 查询单个用户成绩
|
|
76
|
-
|
|
77
|
-
```bash
|
|
78
|
-
soke-cli exam +get-exam-user \
|
|
79
|
-
--exam-id exam123 \
|
|
80
|
-
--dept-user-id user456
|
|
81
|
-
```
|
|
82
|
-
|
|
83
|
-
### 示例2: 以表格格式输出
|
|
84
|
-
|
|
85
|
-
```bash
|
|
86
|
-
soke-cli exam +get-exam-user \
|
|
87
|
-
--exam-id exam123 \
|
|
88
|
-
--dept-user-id user456 \
|
|
89
|
-
--format table
|
|
90
|
-
```
|
|
91
|
-
|
|
92
|
-
## 常见场景
|
|
93
|
-
|
|
94
|
-
### 场景1: 用户询问自己的成绩
|
|
95
|
-
|
|
96
|
-
**用户输入**: "我的考试成绩是多少?"
|
|
97
|
-
|
|
98
|
-
**处理步骤**:
|
|
99
|
-
1. 获取当前用户的 `dept_user_id`(通过 `soke-cli api GET /users/me`)
|
|
100
|
-
2. 确认考试ID(可能需要先列出考试)
|
|
101
|
-
3. 执行查询命令
|
|
102
|
-
|
|
103
|
-
```bash
|
|
104
|
-
# 步骤1: 获取当前用户信息
|
|
105
|
-
soke-cli api GET /users/me
|
|
106
|
-
|
|
107
|
-
# 步骤2: 查询成绩
|
|
108
|
-
soke-cli exam +get-exam-user \
|
|
109
|
-
--exam-id exam123 \
|
|
110
|
-
--dept-user-id <从步骤1获取的user_id>
|
|
111
|
-
```
|
|
112
|
-
|
|
113
|
-
### 场景2: 管理员查询员工成绩
|
|
114
|
-
|
|
115
|
-
**用户输入**: "查询张三的考试成绩"
|
|
116
|
-
|
|
117
|
-
**处理步骤**:
|
|
118
|
-
1. 通过姓名查询用户ID(使用 `soke-cli contact +search-user`)
|
|
119
|
-
2. 确认考试ID
|
|
120
|
-
3. 执行查询命令
|
|
121
|
-
|
|
122
|
-
```bash
|
|
123
|
-
# 步骤1: 查询用户ID
|
|
124
|
-
soke-cli contact +search-user --name "张三"
|
|
125
|
-
|
|
126
|
-
# 步骤2: 查询成绩
|
|
127
|
-
soke-cli exam +get-exam-user \
|
|
128
|
-
--exam-id exam123 \
|
|
129
|
-
--dept-user-id <从步骤1获取的dept_user_id>
|
|
130
|
-
```
|
|
131
|
-
|
|
132
|
-
### 场景3: 批量查询多个用户成绩
|
|
133
|
-
|
|
134
|
-
**用户输入**: "查询所有人的考试成绩"
|
|
135
|
-
|
|
136
|
-
**处理步骤**:
|
|
137
|
-
使用 `+list-exam-users` 更合适,可以一次获取所有用户的成绩列表。
|
|
138
|
-
|
|
139
|
-
```bash
|
|
140
|
-
soke-cli exam +list-exam-users --exam-id exam123
|
|
141
|
-
```
|
|
142
|
-
|
|
143
|
-
## 权限要求
|
|
144
|
-
|
|
145
|
-
- **所需权限**: `exam:examUser:readonly`
|
|
146
|
-
- **认证方式**: 需要先执行 `soke-cli auth login` 完成用户认证
|
|
147
|
-
|
|
148
|
-
## 错误处理
|
|
149
|
-
|
|
150
|
-
### 错误1: 考试不存在
|
|
151
|
-
|
|
152
|
-
```json
|
|
153
|
-
{
|
|
154
|
-
"code": 404,
|
|
155
|
-
"msg": "考试不存在"
|
|
156
|
-
}
|
|
157
|
-
```
|
|
158
|
-
|
|
159
|
-
**解决方案**: 检查 `exam-id` 是否正确
|
|
160
|
-
|
|
161
|
-
### 错误2: 用户未参加考试
|
|
162
|
-
|
|
163
|
-
```json
|
|
164
|
-
{
|
|
165
|
-
"code": 404,
|
|
166
|
-
"msg": "用户未参加该考试"
|
|
167
|
-
}
|
|
168
|
-
```
|
|
169
|
-
|
|
170
|
-
**解决方案**: 确认用户是否已参加该考试
|
|
171
|
-
|
|
172
|
-
### 错误3: 权限不足
|
|
173
|
-
|
|
174
|
-
```json
|
|
175
|
-
{
|
|
176
|
-
"code": 403,
|
|
177
|
-
"msg": "权限不足"
|
|
178
|
-
}
|
|
179
|
-
```
|
|
180
|
-
|
|
181
|
-
**解决方案**:
|
|
182
|
-
1. 确认已执行 `soke-cli auth login`
|
|
183
|
-
2. 联系管理员开通 `exam:examUser:readonly` 权限
|
|
184
|
-
|
|
185
|
-
### 错误4: 参数缺失
|
|
186
|
-
|
|
187
|
-
```bash
|
|
188
|
-
Error: required flag(s) "exam-id", "dept-user-id" not set
|
|
189
|
-
```
|
|
190
|
-
|
|
191
|
-
**解决方案**: 检查是否提供了所有必需参数
|
|
192
|
-
|
|
193
|
-
## API详情
|
|
194
|
-
|
|
195
|
-
- **HTTP方法**: GET
|
|
196
|
-
- **API路径**: `/exam/user/info`
|
|
197
|
-
- **请求参数**:
|
|
198
|
-
- `exam_id`: 考试ID
|
|
199
|
-
- `dept_user_id`: 部门用户ID
|
|
200
|
-
|
|
201
|
-
## 相关命令
|
|
202
|
-
|
|
203
|
-
- `+list-exam-users`: 列出考试用户成绩列表
|
|
204
|
-
- `+list-exams`: 列出考试列表
|
|
205
|
-
- `soke-cli contact +search-user`: 查询用户信息
|
|
206
|
-
|
|
207
|
-
## 注意事项
|
|
208
|
-
|
|
209
|
-
1. **时间戳格式**: 所有时间字段都是Unix时间戳(毫秒),不是秒
|
|
210
|
-
2. **用户ID**: 必须使用 `dept_user_id`,不能使用用户名或其他标识
|
|
211
|
-
3. **考试状态**: 状态值可能因系统配置而异,常见值包括:已完成、进行中、未开始、已过期
|
|
212
|
-
4. **成绩计算**: 成绩字段可能为null(如果考试未完成或未提交)
|