@sokeai/cli 1.0.34 → 1.0.39
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 +24 -1
- package/package.json +1 -1
- package/scripts/build-binaries.sh +30 -1
- package/scripts/ci/check-legacy-constants.sh +27 -0
- package/scripts/ci/check-yaml-env-whitelist.sh +33 -0
- package/scripts/install.js +67 -16
- package/scripts/local-test.js +85 -10
- package/scripts/local-test.sh +7 -27
- package/scripts/mcp-stdout-scan.sh +46 -0
- package/scripts/push.sh +16 -25
- package/scripts/regress-auth.sh +56 -0
- package/scripts/regress-baseline.md +84 -0
- package/scripts/release.sh +7 -0
- package/scripts/test-auto-detect.js +48 -71
- package/scripts/test-local-reconcile.js +55 -0
- package/skills/soke-business-training-report/SKILL.md +1 -0
- package/skills/soke-course/SKILL.md +64 -0
- package/skills/soke-course/templates/batch-create-from-excel.md +75 -0
- package/skills/soke-learning-profile/SKILL.md +1 -0
- package/skills/soke-exam/SKILL.md +0 -318
- package/skills/soke-exam/references/exam-get-exam-user.md +0 -212
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# auth 回归 baseline 流程(task 8.2)
|
|
2
|
+
|
|
3
|
+
> 配套脚本:`scripts/regress-auth.sh`
|
|
4
|
+
> 用途:在本 change 引入前后两次跑同一脚本,diff 验证零回归。
|
|
5
|
+
|
|
6
|
+
## 1. baseline 采样(本 change 引入**前**)
|
|
7
|
+
|
|
8
|
+
在 master 或上一个 release tag 下:
|
|
9
|
+
|
|
10
|
+
```bash
|
|
11
|
+
git checkout <last-release-tag>
|
|
12
|
+
make install # 或:go build -o bin/soke-cli main.go
|
|
13
|
+
SOKE_CLIENT_ID=... SOKE_CLIENT_SECRET=... \
|
|
14
|
+
bash scripts/regress-auth.sh 2>&1 | tee /tmp/regress-before.txt
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
`/tmp/regress-before.txt` 即 baseline,包含:
|
|
18
|
+
- 各步 stdout/stderr
|
|
19
|
+
- 各步 exit code(脚本内 `==== [label] exit=N ====` 行)
|
|
20
|
+
- timestamps 已 mask,仅保留结构性差异
|
|
21
|
+
|
|
22
|
+
## 2. 引入后采样
|
|
23
|
+
|
|
24
|
+
切到本 change 分支,重新 build 跑一次:
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
git checkout worktree-generalize-vendor-plugin
|
|
28
|
+
make install
|
|
29
|
+
SOKE_CLIENT_ID=... SOKE_CLIENT_SECRET=... \
|
|
30
|
+
bash scripts/regress-auth.sh 2>&1 | tee /tmp/regress-after.txt
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## 3. diff
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
diff /tmp/regress-before.txt /tmp/regress-after.txt
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
期望:**无差异**(允许时间戳 mask 后的剩余 token 字段差异,因每次实际签发的
|
|
40
|
+
token bytes 不同 —— 但 mask 应已覆盖)。
|
|
41
|
+
|
|
42
|
+
若 diff 显示行为变化,本 change 视为引入回归,**禁止 merge**,
|
|
43
|
+
回到代码层检查 ClientID() / Endpoints() / CallbackPath 取值路径。
|
|
44
|
+
|
|
45
|
+
## 4. keychain 字段验证(任选其一,平台相关)
|
|
46
|
+
|
|
47
|
+
macOS:
|
|
48
|
+
```bash
|
|
49
|
+
security find-generic-password -s "soke-cli" -g 2>&1 | grep -E "acct|cdat"
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Linux(libsecret):
|
|
53
|
+
```bash
|
|
54
|
+
secret-tool search service soke-cli
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
字段名(`access_token` / `refresh_token` 等)与 baseline 完全一致。
|
|
58
|
+
|
|
59
|
+
## 5. 三平台 matrix(task 8.3)
|
|
60
|
+
|
|
61
|
+
本 change 不接入 GH Actions yml(避免 PR 卡在未跑通的 CI 任务),由 ①
|
|
62
|
+
`make-soke-cli-mcp-first` 在 same release 时一并引入。matrix 草案:
|
|
63
|
+
|
|
64
|
+
```yaml
|
|
65
|
+
# .github/workflows/regress-auth.yml(草案,在 ① 同 release 一起合)
|
|
66
|
+
strategy:
|
|
67
|
+
matrix:
|
|
68
|
+
os: [ubuntu-latest, macos-latest, windows-latest]
|
|
69
|
+
jobs:
|
|
70
|
+
regress:
|
|
71
|
+
runs-on: ${{ matrix.os }}
|
|
72
|
+
steps:
|
|
73
|
+
- uses: actions/checkout@v4
|
|
74
|
+
- uses: actions/setup-go@v5
|
|
75
|
+
with: { go-version: '1.25' }
|
|
76
|
+
- run: |
|
|
77
|
+
go build -o bin/soke-cli main.go
|
|
78
|
+
# auth login 需要真账号,CI 内只跑 status / logout 路径
|
|
79
|
+
./bin/soke-cli auth status --json
|
|
80
|
+
./bin/soke-cli auth logout
|
|
81
|
+
./bin/soke-cli auth status --json
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
跑出 CI matrix 三个 job 全绿,本 change 才视为通过 task 8.3 AC。
|
package/scripts/release.sh
CHANGED
|
@@ -48,6 +48,13 @@ check_tools() {
|
|
|
48
48
|
check_git_status() {
|
|
49
49
|
echo -e "${YELLOW}检查 Git 状态...${NC}"
|
|
50
50
|
|
|
51
|
+
# 检查当前分支是否为 master
|
|
52
|
+
CURRENT_BRANCH=$(git branch --show-current)
|
|
53
|
+
if [ "$CURRENT_BRANCH" != "master" ]; then
|
|
54
|
+
echo -e "${RED}错误: 当前分支为 $CURRENT_BRANCH,发布必须在 master 分支上进行${NC}"
|
|
55
|
+
exit 1
|
|
56
|
+
fi
|
|
57
|
+
|
|
51
58
|
if [ -n "$(git status --porcelain)" ]; then
|
|
52
59
|
echo -e "${RED}错误: 工作目录有未提交的更改${NC}"
|
|
53
60
|
echo "请先提交或暂存所有更改"
|
|
@@ -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');
|
|
@@ -3,6 +3,7 @@ name: soke-course
|
|
|
3
3
|
summary: 授客课程管理(课程列表/分类/课程详情/学习记录),通过 soke-cli 查询
|
|
4
4
|
version: 1.0.0
|
|
5
5
|
description: "授客课程管理:查询课程、课程分类、课程详情、学习记录。查询课程列表、课程分类、课程用户学习记录、课件列表、人脸识别记录。当用户需要查询课程信息、查看课程列表、查询学习记录、查看课程分类时使用。"
|
|
6
|
+
requiredAuthorizations: [{"platformId":"soke-ai"}]
|
|
6
7
|
metadata:
|
|
7
8
|
requires:
|
|
8
9
|
bins: ["soke-cli"]
|
|
@@ -51,6 +52,7 @@ Shortcut 是对常用操作的高级封装(`soke-cli course +<verb> [flags]`
|
|
|
51
52
|
| [`+get-course-user`](#get-course-user) | 获取单个用户的课程学习详情 |
|
|
52
53
|
| [`+list-lesson-learns`](#list-lesson-learns) | 列出课件学习记录 |
|
|
53
54
|
| [`+list-lesson-faces`](#list-lesson-faces) | 列出课件人脸识别记录 |
|
|
55
|
+
| [`+batch-create-from-excel`](#batch-create-from-excel) | 通过 Excel 批量创建课程并上传课件(原 createcourse 工具能力,已合入) |
|
|
54
56
|
|
|
55
57
|
## 命令详解
|
|
56
58
|
|
|
@@ -483,6 +485,68 @@ soke-cli course +list-lesson-learns --lesson-id <lesson_id>
|
|
|
483
485
|
soke-cli course +list-lesson-faces --lesson-id <lesson_id>
|
|
484
486
|
```
|
|
485
487
|
|
|
488
|
+
### +batch-create-from-excel
|
|
489
|
+
|
|
490
|
+
通过 Excel 文件批量创建课程并上传课件素材。能力来自原独立工具 createcourse,已物理合入本 CLI(`internal/courseimport`)。
|
|
491
|
+
|
|
492
|
+
**命令格式**:
|
|
493
|
+
```bash
|
|
494
|
+
soke-cli course +batch-create-from-excel \
|
|
495
|
+
--file <课程信息.xlsx> \
|
|
496
|
+
[--material-dir <素材目录>] \
|
|
497
|
+
[--separator <分隔符>] \
|
|
498
|
+
[--mode default|hg] \
|
|
499
|
+
[--picture-time 6] \
|
|
500
|
+
[--material-category-id <分类ID>] \
|
|
501
|
+
[--json-events]
|
|
502
|
+
```
|
|
503
|
+
|
|
504
|
+
**Flags**:
|
|
505
|
+
|
|
506
|
+
| 名称 | 必填 | 默认 | 说明 |
|
|
507
|
+
| --- | --- | --- | --- |
|
|
508
|
+
| `--file` | 是 | - | Excel 文件路径,必须 `.xlsx` |
|
|
509
|
+
| `--material-dir` | 否 | Excel 同目录 | 素材文件相对路径解析的根目录 |
|
|
510
|
+
| `--separator` | 否 | `-` | Excel `分类` / `标签` 列的分隔符,同时影响两列 |
|
|
511
|
+
| `--mode` | 否 | `default` | `default` 普通课程 / `hg` 海关学时模式 |
|
|
512
|
+
| `--picture-time` | 否 | `6` | 视频抽帧秒数;仅当系统已装 ffmpeg 时生效 |
|
|
513
|
+
| `--material-category-id` | 否 | 空 | 素材库默认分类 ID,空则交由后端兜底 |
|
|
514
|
+
| `--json-events` | 否 | false | stdout 输出 JSON-lines 进度事件,供 sokeclaw 等 IPC 调用方解析 |
|
|
515
|
+
|
|
516
|
+
**Excel 模板**: 详见 [`templates/batch-create-from-excel.md`](./templates/batch-create-from-excel.md)。
|
|
517
|
+
|
|
518
|
+
**进度事件协议**(`--json-events` 模式):
|
|
519
|
+
|
|
520
|
+
| `type` | 含义 |
|
|
521
|
+
| --- | --- |
|
|
522
|
+
| `env` | 启动时报告 ffmpeg 是否可用 |
|
|
523
|
+
| `course_start` | 开始处理某门课程 |
|
|
524
|
+
| `course_done` | 某门课程全流程成功 |
|
|
525
|
+
| `course_failed` | 某门课程失败(单门失败不中断整批) |
|
|
526
|
+
| `episode_uploaded` | 单节课件上传完成 |
|
|
527
|
+
| `cover_fallback` | 封面降级为默认封面 |
|
|
528
|
+
| `run_done` | 整批结束,带 `ok` / `failed` 计数 |
|
|
529
|
+
|
|
530
|
+
**示例**:
|
|
531
|
+
|
|
532
|
+
```bash
|
|
533
|
+
# 默认模式,Excel 与素材同目录
|
|
534
|
+
soke-cli course +batch-create-from-excel --file ./courses.xlsx
|
|
535
|
+
|
|
536
|
+
# HG 学时模式,显式指定素材目录,机器可读输出
|
|
537
|
+
soke-cli course +batch-create-from-excel \
|
|
538
|
+
--file ./hg-courses.xlsx \
|
|
539
|
+
--material-dir ./materials \
|
|
540
|
+
--mode hg \
|
|
541
|
+
--json-events
|
|
542
|
+
```
|
|
543
|
+
|
|
544
|
+
**注意**:
|
|
545
|
+
|
|
546
|
+
- ffmpeg 是可选依赖:已装则启用视频抽帧封面;未装则封面策略自动降级到默认封面,不影响主流程。如需启用抽帧,把 ffmpeg 装到 `PATH`,或设置环境变量 `SOKE_CLI_FFMPEG_PATH` 指向可执行文件。
|
|
547
|
+
- HG 模式下"所属单位标签"的 tag id 通过环境变量 `SOKE_CLI_HG_ORG_TAG_ID` 注入(原 createcourse 写死在 token 配置表里,合入 soke-cli 后改为外部注入)。
|
|
548
|
+
- 单门课程失败不会中断整批运行,失败统计会在 `run_done` 事件汇总。
|
|
549
|
+
|
|
486
550
|
## 注意事项
|
|
487
551
|
|
|
488
552
|
1. **时间格式**: 所有时间参数使用Unix时间戳(毫秒),不是秒
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# `+batch-create-from-excel` Excel 模板说明
|
|
2
|
+
|
|
3
|
+
`soke-cli course +batch-create-from-excel` 接受的 Excel 模板与原 `createcourse` 工具兼容。下面给出列定义、续行规则与制作技巧。
|
|
4
|
+
|
|
5
|
+
## 列定义
|
|
6
|
+
|
|
7
|
+
| 列名 | 必填 | 类型/格式 | 说明 |
|
|
8
|
+
| --- | --- | --- | --- |
|
|
9
|
+
| 序号 | 是 | 数字/文本 | 课程序号,行级唯一;续行(同一课程的下一节分集)填空 |
|
|
10
|
+
| 课程名称 | 是 | 文本 | 课程标题。续行可留空或填与上一行相同的标题 |
|
|
11
|
+
| 分类 | 否 | 分隔字符串 | 多级分类用 `--separator` 分隔,例如 `公共课-合规-反洗钱` |
|
|
12
|
+
| 标签 | 否 | 分隔字符串 | 多标签同样按 `--separator` 分隔 |
|
|
13
|
+
| 简介 | 否 | 文本 | 课程介绍 |
|
|
14
|
+
| 学分 | 否 | 数字 | `default` 模式有效 |
|
|
15
|
+
| 积分 | 否 | 数字 | `default` 模式有效;HG 模式忽略 |
|
|
16
|
+
| 封面图 | 否 | 文本 | URL / `default` / 本地图片路径 / 数字(秒,需 ffmpeg) / 留空(视频自动抽帧,需 ffmpeg) |
|
|
17
|
+
| 素材文件 | 否(分集行必填) | 路径 | 课件文件,相对路径会按 `--material-dir` 解析 |
|
|
18
|
+
|
|
19
|
+
### HG 模式额外列
|
|
20
|
+
|
|
21
|
+
启用 `--mode hg` 时,额外读取四列学时:
|
|
22
|
+
|
|
23
|
+
| 列名 | 单位 | 说明 |
|
|
24
|
+
| --- | --- | --- |
|
|
25
|
+
| 习思想 | 学时 | 习近平新时代中国特色社会主义思想 |
|
|
26
|
+
| 政治能力 | 学时 | - |
|
|
27
|
+
| 业务能力 | 学时 | - |
|
|
28
|
+
| 执法能力 | 学时 | - |
|
|
29
|
+
|
|
30
|
+
## 表头规则
|
|
31
|
+
|
|
32
|
+
- 前 2 行为表头,允许两行合并表头(工具按 trim 后的名称合并到位置映射)。
|
|
33
|
+
- 第 3 行起为数据。
|
|
34
|
+
- 总行数低于 4 会直接报错。
|
|
35
|
+
|
|
36
|
+
## 续行(同一课程多分集)
|
|
37
|
+
|
|
38
|
+
当一门课程含多节课件时:
|
|
39
|
+
|
|
40
|
+
1. 第一行填完整课程信息(序号 + 课程名称 + 其他元数据 + 第 1 节素材文件)。
|
|
41
|
+
2. 后续行的 `序号` 和 `课程名称` 留空(或 `课程名称` 与上一行相同),只填 `素材文件` (以及非 HG 模式的 `积分`)。
|
|
42
|
+
3. 工具会自动把后续行 append 到上一门课程的 `Episodes` 数组。
|
|
43
|
+
|
|
44
|
+
## 封面图来源策略
|
|
45
|
+
|
|
46
|
+
按以下优先级决定课程封面:
|
|
47
|
+
|
|
48
|
+
1. `封面图` 是 `http(s)://...` → 直接使用 URL。
|
|
49
|
+
2. `封面图` 是 `default` → 使用平台默认封面。
|
|
50
|
+
3. `封面图` 是本地图片路径(.jpg/.jpeg/.png/.gif/.bmp) → 上传 OSS 并使用返回的 URL。
|
|
51
|
+
4. `封面图` 是数字(秒数) + `素材文件` 是视频 + 系统有 ffmpeg → 从视频抽帧并上传。
|
|
52
|
+
5. `封面图` 留空 + `素材文件` 是视频 + 系统有 ffmpeg → 用 `--picture-time`(默认 6 秒)抽帧。
|
|
53
|
+
6. 以上都不满足 → 使用平台默认封面,并 emit `cover_fallback` 事件。
|
|
54
|
+
|
|
55
|
+
## 快速根据本地课件目录制作模板(Windows 示例)
|
|
56
|
+
|
|
57
|
+
> 来自原 createcourse 工具的工作流提示。
|
|
58
|
+
|
|
59
|
+
1. 打开 `cmd`,切到课件所在的最上层目录,例如 `cd d:\course`。
|
|
60
|
+
2. 把所有文件路径导出到文本文件:
|
|
61
|
+
```cmd
|
|
62
|
+
d:
|
|
63
|
+
cd course
|
|
64
|
+
dir /s /b /a:-d > filelist.txt
|
|
65
|
+
```
|
|
66
|
+
3. 打开空白 Excel,在「数据」选项卡选「从文本/CSV」导入 `filelist.txt`。
|
|
67
|
+
4. 对导入的数据列进行分列。
|
|
68
|
+
5. 根据实际存储情况合并单元格(同一课程的多节分集),补全课程学分、简介、封面图等信息。
|
|
69
|
+
|
|
70
|
+
## 模板示例
|
|
71
|
+
|
|
72
|
+
可参照仓库内随 createcourse 一起合入的两张样例图:
|
|
73
|
+
|
|
74
|
+
- `internal/courseimport/template_example.jpg`
|
|
75
|
+
- `internal/courseimport/template_example2.jpg`
|
|
@@ -3,6 +3,7 @@ name: soke-learning-profile
|
|
|
3
3
|
summary: 学员学习档案查询
|
|
4
4
|
version: 1.0.0
|
|
5
5
|
description: "学员学习档案查询:查询学员的完整学习记录,包括课程学习、考试成绩、证书获取、学分积分等。当用户需要查询学员学习档案、查看学员学习情况、统计学员学习数据、查询学员综合学习信息时使用。"
|
|
6
|
+
requiredAuthorizations: [{"platformId":"soke-ai"}]
|
|
6
7
|
metadata:
|
|
7
8
|
requires:
|
|
8
9
|
bins: ["soke-cli"]
|