@sokeai/cli 1.0.41 → 1.0.54
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 +1 -24
- package/package.json +3 -2
- package/scripts/build-binaries.sh +1 -30
- package/scripts/e2e-test.sh +63 -3
- package/scripts/install.js +52 -110
- package/scripts/local-test.js +10 -85
- package/scripts/local-test.sh +52 -7
- package/scripts/push.sh +12 -4
- package/scripts/release.sh +50 -40
- package/scripts/test-auto-detect.js +71 -48
- package/skills/README.md +0 -1
- package/skills/oss-upload/skill.md +50 -0
- package/skills/oss-upload/upload.py +355 -0
- package/skills/soke-assign/README.md +310 -0
- package/skills/soke-assign/SKILL.md +383 -0
- package/skills/soke-course/README.md +168 -92
- package/skills/soke-course/SKILL.md +372 -460
- package/skills/soke-exam/SKILL.md +565 -0
- package/skills/soke-exam/references/exam-get-exam-user.md +212 -0
- package/skills/soke-learning-map/SKILL.md +544 -0
- package/skills/soke-learning-profile/SKILL.md +0 -1
- package/skills/soke-lesson/README.md +53 -0
- package/skills/soke-lesson/SKILL.md +510 -0
- package/skills/soke-material/README.md +49 -0
- package/skills/soke-material/SKILL.md +390 -0
- package/scripts/ci/check-legacy-constants.sh +0 -27
- package/scripts/ci/check-yaml-env-whitelist.sh +0 -33
- package/scripts/mcp-stdout-scan.sh +0 -46
- package/scripts/regress-auth.sh +0 -56
- package/scripts/regress-baseline.md +0 -84
- package/scripts/test-local-reconcile.js +0 -55
- package/skills/soke-business-training-report/SKILL.md +0 -58
- package/skills/soke-course/SUMMARY.md +0 -236
- package/skills/soke-course/templates/batch-create-from-excel.md +0 -75
|
@@ -0,0 +1,355 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""
|
|
4
|
+
阿里云OSS文件上传工具
|
|
5
|
+
通过授客AI接口获取签名后上传文件
|
|
6
|
+
仅使用Python标准库,无需安装第三方依赖
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import os
|
|
10
|
+
import sys
|
|
11
|
+
import json
|
|
12
|
+
import urllib.request
|
|
13
|
+
import urllib.parse
|
|
14
|
+
import urllib.error
|
|
15
|
+
import mimetypes
|
|
16
|
+
import uuid
|
|
17
|
+
import time
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
MAX_FILE_SIZE = 2 * 1024 * 1024 * 1024 # 2GB
|
|
21
|
+
|
|
22
|
+
def load_config():
|
|
23
|
+
"""加载配置文件获取access_token"""
|
|
24
|
+
config_path = os.path.expanduser("~/.soke-cli/config.json")
|
|
25
|
+
if not os.path.exists(config_path):
|
|
26
|
+
print("错误: 配置文件不存在,请先运行 'soke-cli config init'")
|
|
27
|
+
sys.exit(1)
|
|
28
|
+
|
|
29
|
+
with open(config_path, 'r', encoding='utf-8') as f:
|
|
30
|
+
config = json.load(f)
|
|
31
|
+
|
|
32
|
+
if 'UserToken' not in config or not config['UserToken']:
|
|
33
|
+
print("错误: 未找到用户令牌,请先运行 'soke-cli auth login' 登录")
|
|
34
|
+
sys.exit(1)
|
|
35
|
+
|
|
36
|
+
return config
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def get_upload_signature(access_token, api_base_url, max_retries=3):
|
|
40
|
+
"""从接口获取上传签名和配置"""
|
|
41
|
+
params = urllib.parse.urlencode({
|
|
42
|
+
'lang': 'zh',
|
|
43
|
+
'utcoffset': '-28800'
|
|
44
|
+
})
|
|
45
|
+
signature_url = f"{api_base_url}/skills/uploadFile/signature?{params}"
|
|
46
|
+
|
|
47
|
+
headers = {
|
|
48
|
+
'Authorization': f'{access_token}',
|
|
49
|
+
'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8'
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
for attempt in range(max_retries):
|
|
53
|
+
try:
|
|
54
|
+
if attempt > 0:
|
|
55
|
+
print(f"正在重试... ({attempt + 1}/{max_retries})")
|
|
56
|
+
time.sleep(2 ** attempt) # 指数退避: 2s, 4s, 8s
|
|
57
|
+
else:
|
|
58
|
+
print("正在获取上传签名...")
|
|
59
|
+
|
|
60
|
+
req = urllib.request.Request(signature_url, method='POST', headers=headers)
|
|
61
|
+
|
|
62
|
+
with urllib.request.urlopen(req, timeout=30) as response:
|
|
63
|
+
response_data = response.read().decode('utf-8')
|
|
64
|
+
result = json.loads(response_data)
|
|
65
|
+
|
|
66
|
+
# 检查响应格式
|
|
67
|
+
# 支持多种格式:
|
|
68
|
+
# 1. {"code": 0, "message": "success", "data": {...}}
|
|
69
|
+
# 2. {"code": "get_success", "status": "ok", "message": "获取成功", "data": {...}}
|
|
70
|
+
# 3. {"success": true, "err_code": 0, "err_message": "success", "data": {...}}
|
|
71
|
+
code = result.get('code')
|
|
72
|
+
status = result.get('status', '')
|
|
73
|
+
success = result.get('success')
|
|
74
|
+
err_code = result.get('err_code')
|
|
75
|
+
|
|
76
|
+
# 判断是否成功
|
|
77
|
+
is_success = (
|
|
78
|
+
(isinstance(code, int) and code == 0) or # 数字格式
|
|
79
|
+
(isinstance(code, str) and code in ['get_success', 'success']) or # 字符串格式
|
|
80
|
+
(status == 'ok') or # 通过status判断
|
|
81
|
+
(success is True and err_code == 0) # success格式
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
if not is_success:
|
|
85
|
+
print(f"✗ 获取签名失败: {result.get('message', result.get('err_message', '未知错误'))}")
|
|
86
|
+
return None
|
|
87
|
+
|
|
88
|
+
# 提取签名数据
|
|
89
|
+
data = result.get('data', {})
|
|
90
|
+
if not data:
|
|
91
|
+
print("✗ 响应数据为空")
|
|
92
|
+
return None
|
|
93
|
+
|
|
94
|
+
print("✓ 签名获取成功")
|
|
95
|
+
return data
|
|
96
|
+
|
|
97
|
+
except urllib.error.HTTPError as e:
|
|
98
|
+
error_msg = f"HTTP {e.code} {e.reason}"
|
|
99
|
+
try:
|
|
100
|
+
error_body = e.read().decode('utf-8')
|
|
101
|
+
except:
|
|
102
|
+
error_body = ""
|
|
103
|
+
|
|
104
|
+
# 503/502/504 等服务端错误可以重试
|
|
105
|
+
if e.code in [502, 503, 504] and attempt < max_retries - 1:
|
|
106
|
+
print(f"✗ {error_msg} - 服务暂时不可用,将自动重试...")
|
|
107
|
+
continue
|
|
108
|
+
else:
|
|
109
|
+
print(f"✗ HTTP错误: {error_msg}")
|
|
110
|
+
if error_body:
|
|
111
|
+
print(f"响应内容: {error_body}")
|
|
112
|
+
if e.code == 503:
|
|
113
|
+
print("\n提示: 503错误通常表示服务端暂时不可用,请稍后重试")
|
|
114
|
+
print("可能的原因:")
|
|
115
|
+
print(" 1. 服务正在维护或重启")
|
|
116
|
+
print(" 2. 服务负载过高")
|
|
117
|
+
print(" 3. 网络连接问题")
|
|
118
|
+
return None
|
|
119
|
+
|
|
120
|
+
except urllib.error.URLError as e:
|
|
121
|
+
if attempt < max_retries - 1:
|
|
122
|
+
print(f"✗ 网络错误: {str(e.reason)} - 将自动重试...")
|
|
123
|
+
continue
|
|
124
|
+
else:
|
|
125
|
+
print(f"✗ 请求失败: {str(e.reason)}")
|
|
126
|
+
return None
|
|
127
|
+
|
|
128
|
+
except json.JSONDecodeError as e:
|
|
129
|
+
print(f"✗ 响应解析失败: {str(e)}")
|
|
130
|
+
return None
|
|
131
|
+
|
|
132
|
+
except Exception as e:
|
|
133
|
+
print(f"✗ 发生错误: {str(e)}")
|
|
134
|
+
return None
|
|
135
|
+
|
|
136
|
+
print(f"✗ 已重试 {max_retries} 次,仍然失败")
|
|
137
|
+
return None
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def create_multipart_form_data(fields, file_path):
|
|
141
|
+
"""创建multipart/form-data格式的请求体"""
|
|
142
|
+
boundary = f'----WebKitFormBoundary{uuid.uuid4().hex[:16]}'
|
|
143
|
+
|
|
144
|
+
body = []
|
|
145
|
+
|
|
146
|
+
# 添加表单字段
|
|
147
|
+
for key, value in fields.items():
|
|
148
|
+
body.append(f'--{boundary}'.encode())
|
|
149
|
+
body.append(f'Content-Disposition: form-data; name="{key}"'.encode())
|
|
150
|
+
body.append(b'')
|
|
151
|
+
body.append(str(value).encode())
|
|
152
|
+
|
|
153
|
+
# 添加文件
|
|
154
|
+
filename = os.path.basename(file_path)
|
|
155
|
+
mimetype = mimetypes.guess_type(file_path)[0] or 'application/octet-stream'
|
|
156
|
+
|
|
157
|
+
body.append(f'--{boundary}'.encode())
|
|
158
|
+
body.append(f'Content-Disposition: form-data; name="file"; filename="{filename}"'.encode())
|
|
159
|
+
body.append(f'Content-Type: {mimetype}'.encode())
|
|
160
|
+
body.append(b'')
|
|
161
|
+
|
|
162
|
+
with open(file_path, 'rb') as f:
|
|
163
|
+
body.append(f.read())
|
|
164
|
+
|
|
165
|
+
body.append(f'--{boundary}--'.encode())
|
|
166
|
+
body.append(b'')
|
|
167
|
+
|
|
168
|
+
content_type = f'multipart/form-data; boundary={boundary}'
|
|
169
|
+
body_bytes = b'\r\n'.join(body)
|
|
170
|
+
|
|
171
|
+
return content_type, body_bytes
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def upload_file(file_path, object_name, signature_data, corp_id):
|
|
175
|
+
"""上传文件到OSS"""
|
|
176
|
+
if not os.path.exists(file_path):
|
|
177
|
+
print(f"错误: 文件不存在: {file_path}")
|
|
178
|
+
sys.exit(1)
|
|
179
|
+
|
|
180
|
+
# 从签名数据中提取上传配置
|
|
181
|
+
upload_url = signature_data.get('host')
|
|
182
|
+
access_key_id = signature_data.get('accessid') or signature_data.get('OSSAccessKeyId')
|
|
183
|
+
policy = signature_data.get('policy')
|
|
184
|
+
signature = signature_data.get('signature')
|
|
185
|
+
callback = signature_data.get('callback', '')
|
|
186
|
+
dir_prefix = signature_data.get('dir', '')
|
|
187
|
+
|
|
188
|
+
if not all([upload_url, access_key_id, policy, signature]):
|
|
189
|
+
print("✗ 签名数据不完整")
|
|
190
|
+
print(f"签名数据: {json.dumps(signature_data, indent=2, ensure_ascii=False)}")
|
|
191
|
+
return False
|
|
192
|
+
|
|
193
|
+
# 获取文件信息
|
|
194
|
+
file_size = os.path.getsize(file_path)
|
|
195
|
+
file_name = os.path.basename(file_path)
|
|
196
|
+
file_ext = os.path.splitext(file_name)[1].lstrip('.') # 移除点号,如 pdf
|
|
197
|
+
|
|
198
|
+
# 构建完整的对象路径:CorpID/dir_prefix/object_name
|
|
199
|
+
if corp_id:
|
|
200
|
+
# 如果有CorpID,添加为顶层目录
|
|
201
|
+
if dir_prefix:
|
|
202
|
+
# 移除dir_prefix开头的斜杠(如果有)
|
|
203
|
+
dir_prefix = dir_prefix.lstrip('/')
|
|
204
|
+
full_path = f"{corp_id}/{dir_prefix}{object_name}"
|
|
205
|
+
else:
|
|
206
|
+
full_path = f"{corp_id}/{object_name}"
|
|
207
|
+
else:
|
|
208
|
+
# 如果没有CorpID,使用原逻辑
|
|
209
|
+
if dir_prefix and not object_name.startswith(dir_prefix):
|
|
210
|
+
full_path = dir_prefix + object_name
|
|
211
|
+
else:
|
|
212
|
+
full_path = object_name
|
|
213
|
+
|
|
214
|
+
# 准备表单数据
|
|
215
|
+
form_fields = {
|
|
216
|
+
'key': full_path,
|
|
217
|
+
'policy': policy,
|
|
218
|
+
'OSSAccessKeyId': access_key_id,
|
|
219
|
+
'signature': signature,
|
|
220
|
+
'success_action_status': '200',
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
# 如果有callback,添加到表单
|
|
224
|
+
if callback:
|
|
225
|
+
form_fields['callback'] = callback
|
|
226
|
+
|
|
227
|
+
# 显示上传信息
|
|
228
|
+
print(f"\n正在上传文件: {file_path}")
|
|
229
|
+
print(f"文件名称: {file_name}")
|
|
230
|
+
print(f"文件大小: {file_size} 字节")
|
|
231
|
+
print(f"文件后缀: {file_ext if file_ext else '(无后缀)'}")
|
|
232
|
+
if corp_id:
|
|
233
|
+
print(f"企业ID: {corp_id}")
|
|
234
|
+
print(f"目标路径: {full_path}")
|
|
235
|
+
print(f"上传地址: {upload_url}")
|
|
236
|
+
|
|
237
|
+
try:
|
|
238
|
+
# 创建multipart/form-data请求体
|
|
239
|
+
content_type, body = create_multipart_form_data(form_fields, file_path)
|
|
240
|
+
|
|
241
|
+
headers = {
|
|
242
|
+
'Content-Type': content_type,
|
|
243
|
+
'Content-Length': str(len(body))
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
req = urllib.request.Request(upload_url, data=body, headers=headers, method='POST')
|
|
247
|
+
|
|
248
|
+
with urllib.request.urlopen(req, timeout=300) as response:
|
|
249
|
+
response_code = response.getcode()
|
|
250
|
+
response_body = response.read().decode('utf-8')
|
|
251
|
+
|
|
252
|
+
if response_code == 200:
|
|
253
|
+
file_url = f"{upload_url.rstrip('/')}/{full_path}"
|
|
254
|
+
|
|
255
|
+
print(f"\n✓ 上传成功!")
|
|
256
|
+
print(f"=" * 60)
|
|
257
|
+
print(f"文件信息:")
|
|
258
|
+
print(f" - 文件名: {file_name}")
|
|
259
|
+
print(f" - 大小: {file_size} 字节")
|
|
260
|
+
print(f" - 后缀: {file_ext if file_ext else '(无后缀)'}")
|
|
261
|
+
print(f" - OSS路径: {full_path}")
|
|
262
|
+
print(f" - 访问URL: {file_url}")
|
|
263
|
+
print(f"=" * 60)
|
|
264
|
+
|
|
265
|
+
# 如果有callback响应,打印出来
|
|
266
|
+
if response_body:
|
|
267
|
+
try:
|
|
268
|
+
callback_result = json.loads(response_body)
|
|
269
|
+
if callback_result:
|
|
270
|
+
print(f"\n回调响应: {json.dumps(callback_result, indent=2, ensure_ascii=False)}")
|
|
271
|
+
except:
|
|
272
|
+
pass
|
|
273
|
+
|
|
274
|
+
return True
|
|
275
|
+
else:
|
|
276
|
+
print(f"\n✗ 上传失败!")
|
|
277
|
+
print(f"状态码: {response_code}")
|
|
278
|
+
print(f"响应内容: {response_body}")
|
|
279
|
+
return False
|
|
280
|
+
|
|
281
|
+
except urllib.error.HTTPError as e:
|
|
282
|
+
print(f"\n✗ 上传失败: HTTP {e.code} {e.reason}")
|
|
283
|
+
try:
|
|
284
|
+
error_body = e.read().decode('utf-8')
|
|
285
|
+
print(f"响应内容: {error_body}")
|
|
286
|
+
except:
|
|
287
|
+
pass
|
|
288
|
+
return False
|
|
289
|
+
except urllib.error.URLError as e:
|
|
290
|
+
print(f"\n✗ 上传失败: {str(e.reason)}")
|
|
291
|
+
return False
|
|
292
|
+
except Exception as e:
|
|
293
|
+
print(f"\n✗ 发生错误: {str(e)}")
|
|
294
|
+
import traceback
|
|
295
|
+
traceback.print_exc()
|
|
296
|
+
return False
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
def main():
|
|
300
|
+
"""主函数"""
|
|
301
|
+
if len(sys.argv) < 2:
|
|
302
|
+
print("用法: python upload.py <本地文件路径> [OSS对象名]")
|
|
303
|
+
print("示例: python upload.py ./document.pdf uploads/2024/document.pdf")
|
|
304
|
+
sys.exit(1)
|
|
305
|
+
|
|
306
|
+
file_path = sys.argv[1]
|
|
307
|
+
|
|
308
|
+
# 检查文件是否存在
|
|
309
|
+
if not os.path.exists(file_path):
|
|
310
|
+
print(f"错误: 文件不存在: {file_path}")
|
|
311
|
+
sys.exit(1)
|
|
312
|
+
|
|
313
|
+
# 检查是否是文件(不是目录)
|
|
314
|
+
if not os.path.isfile(file_path):
|
|
315
|
+
print(f"错误: 路径不是文件: {file_path}")
|
|
316
|
+
sys.exit(1)
|
|
317
|
+
|
|
318
|
+
# 检查文件大小(不能超过2GB)
|
|
319
|
+
file_size = os.path.getsize(file_path)
|
|
320
|
+
if file_size > MAX_FILE_SIZE:
|
|
321
|
+
print(f"错误: 文件大小超过限制")
|
|
322
|
+
print(f" 当前大小: {file_size / (1024 * 1024 * 1024):.2f} GB ({file_size} 字节)")
|
|
323
|
+
print(f" 最大限制: 2 GB ({MAX_FILE_SIZE} 字节)")
|
|
324
|
+
sys.exit(1)
|
|
325
|
+
|
|
326
|
+
# 如果未指定对象名,使用文件名
|
|
327
|
+
if len(sys.argv) >= 3:
|
|
328
|
+
object_name = sys.argv[2]
|
|
329
|
+
else:
|
|
330
|
+
object_name = os.path.basename(file_path)
|
|
331
|
+
|
|
332
|
+
# 加载配置
|
|
333
|
+
config = load_config()
|
|
334
|
+
access_token = config.get('UserToken')
|
|
335
|
+
api_base_url = config.get('APIBaseURL', 'https://app2651.eapps.dingtalkcloud.com/NewSoke')
|
|
336
|
+
corp_id = config.get('CorpID', '')
|
|
337
|
+
|
|
338
|
+
if not corp_id:
|
|
339
|
+
print("警告: 未找到CorpID配置,文件将上传到根目录")
|
|
340
|
+
print("建议在配置文件中添加 CorpID 字段")
|
|
341
|
+
|
|
342
|
+
# 获取上传签名
|
|
343
|
+
signature_data = get_upload_signature(access_token, api_base_url)
|
|
344
|
+
if not signature_data:
|
|
345
|
+
print("✗ 无法获取上传签名")
|
|
346
|
+
sys.exit(1)
|
|
347
|
+
|
|
348
|
+
# 上传文件
|
|
349
|
+
success = upload_file(file_path, object_name, signature_data, corp_id)
|
|
350
|
+
|
|
351
|
+
sys.exit(0 if success else 1)
|
|
352
|
+
|
|
353
|
+
|
|
354
|
+
if __name__ == '__main__':
|
|
355
|
+
main()
|
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
# 授客指派管理 Skill
|
|
2
|
+
|
|
3
|
+
通用的学习资源指派系统,支持将课程、考试、学习地图等学习资源指派给部门和用户。
|
|
4
|
+
|
|
5
|
+
## 功能概述
|
|
6
|
+
|
|
7
|
+
这个 skill 提供了完整的指派管理功能,包括:
|
|
8
|
+
|
|
9
|
+
- 📋 **部门查询**: 查询可用的部门列表,支持搜索和分页
|
|
10
|
+
- 👥 **用户查询**: 查询可用的用户列表,支持多种搜索方式
|
|
11
|
+
- ✅ **创建指派**: 将学习资源指派给部门或用户
|
|
12
|
+
- 📊 **查询指派**: 查询已创建的指派记录,支持多维度筛选
|
|
13
|
+
|
|
14
|
+
## 支持的模块
|
|
15
|
+
|
|
16
|
+
- **course** - 课程指派
|
|
17
|
+
- **exam** - 考试指派
|
|
18
|
+
- **learning_map** - 学习地图指派
|
|
19
|
+
- 其他模块(根据系统配置)
|
|
20
|
+
|
|
21
|
+
## 支持的指派类型
|
|
22
|
+
|
|
23
|
+
- **dept(部门)**: 部门下所有成员都可以学习
|
|
24
|
+
- **user(用户)**: 只有指定的用户可以学习
|
|
25
|
+
|
|
26
|
+
## 快速开始
|
|
27
|
+
|
|
28
|
+
### 创建课程指派
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
# 1. 查询部门列表,获取部门ID
|
|
32
|
+
soke-cli assign +list-departments --keyword "研发"
|
|
33
|
+
|
|
34
|
+
# 2. 查询用户列表(可选)
|
|
35
|
+
soke-cli assign +list-users --dept-id "130110584"
|
|
36
|
+
|
|
37
|
+
# 3. 创建指派
|
|
38
|
+
soke-cli assign +create \
|
|
39
|
+
--module "course" \
|
|
40
|
+
--target-id "E08464D0-D567-4A01-8FAB-BD41AFFDB271" \
|
|
41
|
+
--dept-ids "130110584" \
|
|
42
|
+
--dept-names "研发部" \
|
|
43
|
+
--learn-type "required" \
|
|
44
|
+
--message-content "新课程已为您安排,请及时学习!"
|
|
45
|
+
|
|
46
|
+
# 4. 验证指派
|
|
47
|
+
soke-cli assign +list \
|
|
48
|
+
--module "course" \
|
|
49
|
+
--target-id "E08464D0-D567-4A01-8FAB-BD41AFFDB271"
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
### 创建考试指派
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
# 指派考试给部门
|
|
56
|
+
soke-cli assign +create \
|
|
57
|
+
--module "exam" \
|
|
58
|
+
--target-id "EXAM-UUID-12345" \
|
|
59
|
+
--dept-ids "130110584" \
|
|
60
|
+
--dept-names "研发部" \
|
|
61
|
+
--learn-type "required" \
|
|
62
|
+
--message-content "年度考核考试已开放,请按时参加!"
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## 主要命令
|
|
66
|
+
|
|
67
|
+
### 查询命令
|
|
68
|
+
| 命令 | 说明 |
|
|
69
|
+
|------|------|
|
|
70
|
+
| `+list-departments` | 查询部门列表(获取部门ID和名称) |
|
|
71
|
+
| `+list-users` | 查询用户列表(获取用户ID和姓名) |
|
|
72
|
+
| `+list` | 查询指派列表 |
|
|
73
|
+
|
|
74
|
+
### 操作命令
|
|
75
|
+
| 命令 | 说明 |
|
|
76
|
+
|------|------|
|
|
77
|
+
| `+create` | 创建指派 |
|
|
78
|
+
|
|
79
|
+
## 使用场景
|
|
80
|
+
|
|
81
|
+
### 场景1: 为新员工培训课程创建指派
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
# 1. 查询部门列表
|
|
85
|
+
soke-cli assign +list-departments --keyword "研发"
|
|
86
|
+
# 输出:部门ID: 130110584, 部门名称: 研发部
|
|
87
|
+
|
|
88
|
+
# 2. 查询该部门的用户
|
|
89
|
+
soke-cli assign +list-users --dept-id "130110584"
|
|
90
|
+
# 输出:用户列表
|
|
91
|
+
|
|
92
|
+
# 3. 创建课程指派(指派给整个部门)
|
|
93
|
+
soke-cli assign +create \
|
|
94
|
+
--module "course" \
|
|
95
|
+
--target-id "E08464D0-D567-4A01-8FAB-BD41AFFDB271" \
|
|
96
|
+
--dept-ids "130110584" \
|
|
97
|
+
--dept-names "研发部" \
|
|
98
|
+
--learn-type "required" \
|
|
99
|
+
--message-content "新员工培训课程已为您安排,请在一周内完成学习!"
|
|
100
|
+
|
|
101
|
+
# 4. 验证指派是否成功
|
|
102
|
+
soke-cli assign +list \
|
|
103
|
+
--module "course" \
|
|
104
|
+
--target-id "E08464D0-D567-4A01-8FAB-BD41AFFDB271"
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
### 场景2: 为特定用户创建指派
|
|
108
|
+
|
|
109
|
+
```bash
|
|
110
|
+
# 1. 搜索用户
|
|
111
|
+
soke-cli assign +list-users --keyword "张三" --search-target "name"
|
|
112
|
+
# 输出:用户ID: 1241453468881932, 姓名: 张三
|
|
113
|
+
|
|
114
|
+
# 2. 创建指派
|
|
115
|
+
soke-cli assign +create \
|
|
116
|
+
--module "course" \
|
|
117
|
+
--target-id "E08464D0-D567-4A01-8FAB-BD41AFFDB271" \
|
|
118
|
+
--user-ids "1241453468881932" \
|
|
119
|
+
--user-names "张三" \
|
|
120
|
+
--learn-type "required"
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
### 场景3: 批量指派给多个部门
|
|
124
|
+
|
|
125
|
+
```bash
|
|
126
|
+
# 1. 查询多个部门
|
|
127
|
+
soke-cli assign +list-departments --keyword "战区"
|
|
128
|
+
|
|
129
|
+
# 2. 批量创建指派
|
|
130
|
+
soke-cli assign +create \
|
|
131
|
+
--module "course" \
|
|
132
|
+
--target-id "E08464D0-D567-4A01-8FAB-BD41AFFDB271" \
|
|
133
|
+
--dept-ids "130110584,130110585,130110586" \
|
|
134
|
+
--dept-names "安徽战区,江苏战区,浙江战区" \
|
|
135
|
+
--learn-type "required"
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
### 场景4: 创建多维度指派
|
|
139
|
+
|
|
140
|
+
```bash
|
|
141
|
+
# 同时指派给部门和用户
|
|
142
|
+
soke-cli assign +create \
|
|
143
|
+
--module "exam" \
|
|
144
|
+
--target-id "EXAM-UUID-12345" \
|
|
145
|
+
--dept-ids "130110584,130110585" \
|
|
146
|
+
--dept-names "研发部,测试部" \
|
|
147
|
+
--user-ids "1241453468881932" \
|
|
148
|
+
--user-names "张三" \
|
|
149
|
+
--learn-type "required" \
|
|
150
|
+
--message-content "年度考核考试已开放,请按时参加!"
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
### 场景5: 查询和统计指派情况
|
|
154
|
+
|
|
155
|
+
```bash
|
|
156
|
+
# 查询特定课程的所有指派
|
|
157
|
+
soke-cli assign +list \
|
|
158
|
+
--module "course" \
|
|
159
|
+
--target-id "E08464D0-D567-4A01-8FAB-BD41AFFDB271"
|
|
160
|
+
|
|
161
|
+
# 查询部门指派
|
|
162
|
+
soke-cli assign +list \
|
|
163
|
+
--module "course" \
|
|
164
|
+
--target-id "E08464D0-D567-4A01-8FAB-BD41AFFDB271" \
|
|
165
|
+
--assign-type "dept"
|
|
166
|
+
|
|
167
|
+
# 查询用户指派
|
|
168
|
+
soke-cli assign +list \
|
|
169
|
+
--module "course" \
|
|
170
|
+
--target-id "E08464D0-D567-4A01-8FAB-BD41AFFDB271" \
|
|
171
|
+
--assign-type "user"
|
|
172
|
+
|
|
173
|
+
# 按关键词搜索
|
|
174
|
+
soke-cli assign +list \
|
|
175
|
+
--module "course" \
|
|
176
|
+
--keyword "研发"
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
## 参数说明
|
|
180
|
+
|
|
181
|
+
### 查询部门列表参数
|
|
182
|
+
|
|
183
|
+
| 参数 | 类型 | 必填 | 默认值 | 说明 |
|
|
184
|
+
|------|------|------|--------|------|
|
|
185
|
+
| --page | int | 否 | 1 | 页码 |
|
|
186
|
+
| --limit | int | 否 | 10 | 每页数量 |
|
|
187
|
+
| --parent-id | string | 否 | 1 | 父部门ID |
|
|
188
|
+
| --keyword | string | 否 | - | 搜索关键词 |
|
|
189
|
+
|
|
190
|
+
### 查询用户列表参数
|
|
191
|
+
|
|
192
|
+
| 参数 | 类型 | 必填 | 默认值 | 说明 |
|
|
193
|
+
|------|------|------|--------|------|
|
|
194
|
+
| --dept-id | string | 否 | - | 部门ID |
|
|
195
|
+
| --keyword | string | 否 | - | 搜索关键词 |
|
|
196
|
+
| --search-target | string | 否 | name | 搜索目标:name/phone/job_number |
|
|
197
|
+
| --page | int | 否 | 1 | 页码 |
|
|
198
|
+
| --limit | int | 否 | 20 | 每页数量 |
|
|
199
|
+
|
|
200
|
+
### 创建指派参数
|
|
201
|
+
|
|
202
|
+
| 参数 | 类型 | 必填 | 默认值 | 说明 |
|
|
203
|
+
|------|------|------|--------|------|
|
|
204
|
+
| --module | string | 是 | - | 模块类型:course/exam/learning_map |
|
|
205
|
+
| --target-id | string | 是 | - | 目标ID |
|
|
206
|
+
| --learn-type | string | 否 | required | 学习类型:required/optional |
|
|
207
|
+
| --dept-ids | string | 否 | - | 部门ID列表,逗号分隔 |
|
|
208
|
+
| --dept-names | string | 否 | - | 部门名称列表,逗号分隔 |
|
|
209
|
+
| --user-ids | string | 否 | - | 用户ID列表,逗号分隔 |
|
|
210
|
+
| --user-names | string | 否 | - | 用户名称列表,逗号分隔 |
|
|
211
|
+
| --message-content | string | 否 | 默认消息 | 通知消息内容 |
|
|
212
|
+
| --message-switch | int | 否 | 1 | 消息开关:0关闭/1开启 |
|
|
213
|
+
|
|
214
|
+
### 查询指派列表参数
|
|
215
|
+
|
|
216
|
+
| 参数 | 类型 | 必填 | 默认值 | 说明 |
|
|
217
|
+
|------|------|------|--------|------|
|
|
218
|
+
| --target-id | string | 否 | - | 目标ID |
|
|
219
|
+
| --module | string | 否 | - | 模块类型 |
|
|
220
|
+
| --learn-type | string | 否 | - | 学习类型 |
|
|
221
|
+
| --assign-type | string | 否 | - | 指派类型:dept/user/group/position/role |
|
|
222
|
+
| --keyword | string | 否 | - | 搜索关键词 |
|
|
223
|
+
| --page | int | 否 | 1 | 页码 |
|
|
224
|
+
| --limit | int | 否 | 10 | 每页数量 |
|
|
225
|
+
|
|
226
|
+
## 注意事项
|
|
227
|
+
|
|
228
|
+
1. **指派前准备**
|
|
229
|
+
- 确保目标资源已创建(如课程已发布)
|
|
230
|
+
- 使用 `+list-departments` 和 `+list-users` 查询可用的部门和用户
|
|
231
|
+
- 记录部门ID和用户ID用于创建指派
|
|
232
|
+
|
|
233
|
+
2. **批量指派**
|
|
234
|
+
- 支持同时指派给多个对象
|
|
235
|
+
- 使用逗号分隔多个ID和名称
|
|
236
|
+
- ID和名称的顺序要一一对应
|
|
237
|
+
|
|
238
|
+
3. **必修 vs 选修**
|
|
239
|
+
- **必修**(required):用户必须完成学习
|
|
240
|
+
- **选修**(optional):用户可选择性学习
|
|
241
|
+
|
|
242
|
+
4. **消息通知**
|
|
243
|
+
- 默认开启消息通知(message-switch=1)
|
|
244
|
+
- 可自定义消息内容(message-content)
|
|
245
|
+
- 可通过 message-switch=0 关闭通知
|
|
246
|
+
|
|
247
|
+
5. **权限要求**
|
|
248
|
+
- 需要具有指派的读写权限
|
|
249
|
+
- 只能查询和操作权限范围内的部门和用户
|
|
250
|
+
|
|
251
|
+
6. **搜索方式**
|
|
252
|
+
- 部门搜索:按部门名称搜索
|
|
253
|
+
- 用户搜索:支持按姓名、手机号、工号搜索
|
|
254
|
+
|
|
255
|
+
## 错误处理
|
|
256
|
+
|
|
257
|
+
### 权限不足
|
|
258
|
+
|
|
259
|
+
```bash
|
|
260
|
+
# 错误信息: Permission denied
|
|
261
|
+
# 解决方案: 检查是否已登录,并确认账号有相应权限
|
|
262
|
+
soke-cli auth login
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
### 目标资源不存在
|
|
266
|
+
|
|
267
|
+
```bash
|
|
268
|
+
# 错误信息: Target not found
|
|
269
|
+
# 解决方案: 检查目标ID是否正确,确认资源已创建
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
### 部门或用户不存在
|
|
273
|
+
|
|
274
|
+
```bash
|
|
275
|
+
# 错误信息: Department/User not found
|
|
276
|
+
# 解决方案: 使用 +list-departments 或 +list-users 查询正确的ID
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
### 参数格式错误
|
|
280
|
+
|
|
281
|
+
```bash
|
|
282
|
+
# 错误信息: Invalid parameter format
|
|
283
|
+
# 解决方案: 检查ID和名称列表是否一一对应,逗号分隔是否正确
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
## 相关文档
|
|
287
|
+
|
|
288
|
+
- [SKILL.md](./SKILL.md) - 完整的 Skill 文档和 API 参考
|
|
289
|
+
- [使用文档](../../docs/ASSIGN_USAGE.md) - 详细的使用指南和示例
|
|
290
|
+
- [课程管理 Skill](../soke-course/SKILL.md) - 课程创建和管理
|
|
291
|
+
- [考试管理 Skill](../soke-exam/SKILL.md) - 考试创建和管理
|
|
292
|
+
- [共享配置 Skill](../soke-shared/SKILL.md) - 认证和配置管理
|
|
293
|
+
- [API 接口文档](../../docs/API_PROXY_INTERFACES_2.0.md) - 完整的 API 接口说明
|
|
294
|
+
|
|
295
|
+
## 工作流程图
|
|
296
|
+
|
|
297
|
+
```
|
|
298
|
+
指派工作流
|
|
299
|
+
├── 1. 查询可用对象
|
|
300
|
+
│ ├── +list-departments (查询部门)
|
|
301
|
+
│ └── +list-users (查询用户)
|
|
302
|
+
├── 2. 创建指派
|
|
303
|
+
│ └── +create (指派给部门/用户/群组/职位/角色)
|
|
304
|
+
└── 3. 验证和查询
|
|
305
|
+
└── +list (查询指派记录)
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
## 版本历史
|
|
309
|
+
|
|
310
|
+
- v1.0.0 (2026-05-28): 初始版本,支持部门查询、用户查询、创建指派、查询指派列表
|