chore: 项目已迁移至 Gitee/GitHub,仅保留迁移说明 (tombstone)
This commit is contained in:
1 parent
9531f2ef83
commit
1bd23cf015
9 files changed
+1
-877
No files matched your search
@@ -1,5 +0,0 @@
|
||||
node_modules/
|
||||
*.bcmkn
|
||||
*.bcmknk
|
||||
*.png
|
||||
.DS_Store
|
||||
@@ -1,21 +0,0 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2026 shuiyu
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -2,8 +2,7 @@
|
||||
|
||||
你好呀~这个仓库已经搬到更好用的地方啦。
|
||||
|
||||
为了让代码管理更清爽、多平台同步更省心,我们把原先分散的镜像统一收拢到
|
||||
**Gitee** 与 **GitHub** 主仓维护,这里就只保留这张"迁移小纸条"咯。
|
||||
为了让代码管理更清爽、多平台同步更省心,我们把原先分散的镜像统一收拢到主仓维护,这里就只保留这张"迁移小纸条"咯。
|
||||
|
||||
📦 新家在这里:
|
||||
- Gitee:https://gitee.com/shuiyu1123/codemao-cli
|
||||
|
||||
@@ -1,41 +0,0 @@
|
||||
{
|
||||
"name": "codemao-cli",
|
||||
"version": "1.0.0",
|
||||
"description": "编程猫 (Codemao) 社区命令行工具 - 作品管理、BCM 解析、API 调用",
|
||||
"main": "src/index.js",
|
||||
"bin": {
|
||||
"codemao": "./src/index.js"
|
||||
},
|
||||
"scripts": {
|
||||
"test": "node src/index.js help"
|
||||
},
|
||||
"keywords": [
|
||||
"codemao",
|
||||
"programming-cat",
|
||||
"neko",
|
||||
"kitten",
|
||||
"bcm",
|
||||
"blockly",
|
||||
"scratch",
|
||||
"cli",
|
||||
"education"
|
||||
],
|
||||
"author": "",
|
||||
"license": "MIT",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://gitee.com/shuiyu1123/codemao-cli.git"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14"
|
||||
},
|
||||
"files": [
|
||||
"src/",
|
||||
"README.md",
|
||||
"LICENSE"
|
||||
],
|
||||
"homepage": "https://gitee.com/shuiyu1123/codemao-cli",
|
||||
"bugs": {
|
||||
"url": "https://gitee.com/shuiyu1123/codemao-cli/issues"
|
||||
}
|
||||
}
|
||||
-223
@@ -1,223 +0,0 @@
|
||||
/**
|
||||
* 编程猫 API 端点注册表
|
||||
*/
|
||||
const DOMAINS = require('./domains');
|
||||
|
||||
const ENDPOINTS = [
|
||||
// ═══════════════ 创作服务 ═══════════════
|
||||
{ method: 'GET', path: '/neko/sample/list', auth: false, desc: '示例作品列表', host: 'creation', params: 'subject_id' },
|
||||
{ method: 'GET', path: '/neko/sample/detail', auth: false, desc: '示例作品详情', host: 'creation', params: 'id' },
|
||||
{ method: 'GET', path: '/neko/works/', auth: true, desc: '用户作品列表', host: 'creation' },
|
||||
{ method: 'POST', path: '/neko/works/', auth: true, desc: '创建作品', host: 'creation' },
|
||||
{ method: 'GET', path: '/neko/works/{id}', auth: true, desc: '作品详情', host: 'creation' },
|
||||
{ method: 'PUT', path: '/neko/works/{id}', auth: true, desc: '更新作品', host: 'creation' },
|
||||
{ method: 'GET', path: '/neko/works/archive/{id}', auth: true, desc: '作品归档', host: 'creation' },
|
||||
{ method: 'POST', path: '/neko/works/{id}/publish', auth: true, desc: '发布作品', host: 'creation' },
|
||||
{ method: 'GET', path: '/neko/community/work/detail/', auth: true, desc: '社区作品详情', host: 'creation', params: 'work_id' },
|
||||
{ method: 'POST', path: '/neko/community/work/publish/', auth: true, desc: '发布到社区', host: 'creation' },
|
||||
{ method: 'GET', path: '/neko/community/player/published-work-detail/', auth: true, desc: '已发布作品', host: 'creation', params: 'work_id' },
|
||||
{ method: 'GET', path: '/neko/package/list', auth: true, desc: '积木包列表', host: 'creation' },
|
||||
{ method: 'GET', path: '/neko/package/{id}', auth: true, desc: '积木包详情', host: 'creation' },
|
||||
{ method: 'GET', path: '/neko/ranking-list/', auth: true, desc: '排行榜', host: 'creation' },
|
||||
{ method: 'POST', path: '/neko/ranking-list/record', auth: true, desc: '提交排行记录', host: 'creation' },
|
||||
{ method: 'GET', path: '/neko/ranking-list/{id}', auth: true, desc: '排行榜详情', host: 'creation' },
|
||||
{ method: 'DELETE', path: '/neko/ranking-list/clear', auth: true, desc: '清空排行', host: 'creation', params: 'id' },
|
||||
{ method: 'GET', path: '/neko/ranking-list/record/list', auth: true, desc: '排行记录列表', host: 'creation', params: 'id' },
|
||||
{ method: 'GET', path: '/neko/ai-painting/templates', auth: true, desc: 'AI 绘画模板', host: 'creation', params: 'type' },
|
||||
{ method: 'POST', path: '/neko/ai-painting/match', auth: true, desc: 'AI 绘画匹配', host: 'creation' },
|
||||
{ method: 'GET', path: '/neko/inspiration-pool', auth: true, desc: '灵感池', host: 'creation' },
|
||||
{ method: 'GET', path: '/neko/material/categories', auth: true, desc: '素材分类', host: 'creation', params: 'type' },
|
||||
{ method: 'GET', path: '/neko/material/list', auth: true, desc: '素材列表', host: 'creation' },
|
||||
{ method: 'GET', path: '/neko/user/status', auth: true, desc: '用户状态', host: 'creation' },
|
||||
{ method: 'GET', path: '/neko/cv/list/variables/{id}', auth: true, desc: '云变量列表', host: 'creation' },
|
||||
{ method: 'POST', path: '/neko/cv/add/variables', auth: true, desc: '添加云变量', host: 'creation' },
|
||||
{ method: 'POST', path: '/neko/cv/delete/variables', auth: true, desc: '删除云变量', host: 'creation' },
|
||||
{ method: 'POST', path: '/neko/cv/sync/', auth: true, desc: '同步云变量', host: 'creation' },
|
||||
{ method: 'GET', path: '/neko/web/pack/status', auth: true, desc: '打包状态', host: 'creation' },
|
||||
{ method: 'POST', path: '/neko/web/pack', auth: true, desc: 'Web 打包', host: 'creation' },
|
||||
{ method: 'GET', path: '/neko/web/pack/limit', auth: true, desc: '打包限制', host: 'creation' },
|
||||
{ method: 'POST', path: '/neko/text2img/prompt', auth: true, desc: '文生图提示词', host: 'creation' },
|
||||
{ method: 'GET', path: '/neko/text2img/prompt', auth: true, desc: '获取提示词', host: 'creation' },
|
||||
{ method: 'POST', path: '/neko/course/user/progress', auth: true, desc: '课程进度', host: 'creation' },
|
||||
{ method: 'GET', path: '/neko/course/user/course-work', auth: true, desc: '课程作品', host: 'creation' },
|
||||
{ method: 'POST', path: '/neko/teaching-plan/save/team/work', auth: true, desc: '教学计划保存', host: 'creation' },
|
||||
{ method: 'POST', path: '/neko/teaching-plan/add/opr/log', auth: true, desc: '教学计划日志', host: 'creation' },
|
||||
{ method: 'GET', path: '/neko/teaching-plan/list/opr/log', auth: true, desc: '教学计划日志列表', host: 'creation' },
|
||||
{ method: 'GET', path: '/neko/teaching-plan/work/editing-status/{id}', auth: true, desc: '编辑状态', host: 'creation' },
|
||||
{ method: 'GET', path: '/common/users/detail', auth: true, desc: '用户详情', host: 'creation' },
|
||||
{ method: 'GET', path: '/common/api/neko/text2img/usage-left', auth: true, desc: '文生图剩余次数', host: 'creation' },
|
||||
{ method: 'GET', path: '/coconut/clouddb/currentTime', auth: false, desc: '云数据库时间', host: 'creation' },
|
||||
{ method: 'GET', path: '/public-api/cone-ai/token', auth: true, desc: 'AI Token', host: 'creation' },
|
||||
|
||||
// ═══════════════ 主 API ═══════════════
|
||||
{ method: 'GET', path: '/tiger/v3/web/accounts/profile', auth: true, desc: '用户信息', host: 'api' },
|
||||
{ method: 'POST', path: '/tiger/v3/web/accounts/logout', auth: true, desc: '退出登录', host: 'api' },
|
||||
{ method: 'GET', path: '/tiger/kitten/public-course/courses/', auth: true, desc: '公开课列表', host: 'api' },
|
||||
{ method: 'GET', path: '/tiger/kitten/wechat/user', auth: true, desc: '微信用户信息', host: 'api' },
|
||||
{ method: 'GET', path: '/tiger/nemo/miao-codes', auth: false, desc: '喵码', host: 'api', params: 'token' },
|
||||
{ method: 'GET', path: '/tiger/kitten/public-course/progress', auth: true, desc: '公开课进度', host: 'api' },
|
||||
{ method: 'GET', path: '/tiger/kitten/public-course/status', auth: true, desc: '公开课状态', host: 'api' },
|
||||
{ method: 'GET', path: '/kitten/work/ide/load/{id}', auth: true, desc: '加载作品(IDE)', host: 'api' },
|
||||
{ method: 'GET', path: '/kitten/work/load', auth: true, desc: '加载作品', host: 'api', params: 'work_token' },
|
||||
{ method: 'GET', path: '/kitten/work/key', auth: true, desc: '作品密钥', host: 'api', params: 'work_id' },
|
||||
{ method: 'POST', path: '/kitten/work/bcm/decode', auth: true, desc: 'BCM 解码', host: 'api' },
|
||||
{ method: 'POST', path: '/kitten/work/translate', auth: true, desc: '作品翻译', host: 'api' },
|
||||
{ method: 'GET', path: '/kitten/work/checkname', auth: true, desc: '检查名称', host: 'api', params: 'name' },
|
||||
{ method: 'GET', path: '/kitten/common/work/{id}', auth: true, desc: '通用作品信息', host: 'api' },
|
||||
{ method: 'GET', path: '/kitten/sample/{id}', auth: true, desc: '示例详情', host: 'api' },
|
||||
{ method: 'GET', path: '/kitten/activity/latest', auth: true, desc: '最新活动', host: 'api' },
|
||||
{ method: 'GET', path: '/kitten/cv/dashboard/summary', auth: true, desc: '云变量概览', host: 'api' },
|
||||
{ method: 'GET', path: '/kitten/cv/dashboard/capacity/correction', auth: true, desc: '容量校正', host: 'api' },
|
||||
{ method: 'GET', path: '/kitten/cv/dashboard/capacity/expand', auth: true, desc: '容量扩展', host: 'api' },
|
||||
{ method: 'GET', path: '/kitten/cv/dashboard/works/', auth: true, desc: '云变量作品', host: 'api' },
|
||||
{ method: 'GET', path: '/kitten/cv/variables-limit/check', auth: true, desc: '变量限制检查', host: 'api' },
|
||||
{ method: 'GET', path: '/kitten/materials/lesson/student/all', auth: true, desc: '学生素材', host: 'api' },
|
||||
{ method: 'GET', path: '/kitten/lesson/attendance/current', auth: true, desc: '当前考勤', host: 'api' },
|
||||
{ method: 'GET', path: '/kitten/v4/work/ide/list', auth: true, desc: 'IDE 作品列表', host: 'api', params: 'published_status' },
|
||||
|
||||
// ═══════════════ 开放服务 ═══════════════
|
||||
{ method: 'GET', path: '/tiger/kitten/public-course/courses/', auth: false, desc: '公开课', host: 'open' },
|
||||
{ method: 'GET', path: '/tiger/online/check', auth: false, desc: '在线检查', host: 'open' },
|
||||
|
||||
// ═══════════════ AI 服务 ═══════════════
|
||||
{ method: 'POST', path: '/ai/tts/generate', auth: true, desc: 'TTS 语音生成', host: 'ai' },
|
||||
{ method: 'GET', path: '/ai/tts/status', auth: true, desc: 'TTS 状态', host: 'ai', params: 'taskId' },
|
||||
{ method: 'POST', path: '/neko/image-classify', auth: true, desc: '图像分类', host: 'socketcv' },
|
||||
{ method: 'GET', path: '/neko/image-classify/list', auth: true, desc: '分类列表', host: 'socketcv' },
|
||||
|
||||
// ═══════════════ CDN 资源 (无认证) ═══════════════
|
||||
{ method: 'GET', path: '/neko/bcm/kn-default-v-0.13.1.bcmkn', auth: false, desc: '默认竖屏模板', host: 'cdn' },
|
||||
{ method: 'GET', path: '/neko/bcm/kn-default-h-0.13.1.bcmkn', auth: false, desc: '默认横屏模板', host: 'cdn' },
|
||||
{ method: 'GET', path: '/neko/bcm/kn-kids-default-v-0.13.1.bcmknk', auth: false, desc: 'Kids竖屏模板', host: 'cdn' },
|
||||
{ method: 'GET', path: '/neko/bcm/kn-kids-default-h-0.13.1.bcmknk', auth: false, desc: 'Kids横屏模板', host: 'cdn' },
|
||||
{ method: 'GET', path: '/neko/bcm/kn-exam-default-v-0.18.0.bcmkn', auth: false, desc: '考试竖屏模板', host: 'cdn' },
|
||||
{ method: 'GET', path: '/neko/bcm/kn-exam-default-h-0.18.0.bcmkn', auth: false, desc: '考试横屏模板', host: 'cdn' },
|
||||
{ method: 'GET', path: '/neko/json/kitten_n_update_info.json', auth: false, desc: '更新信息', host: 'cdn' },
|
||||
{ method: 'GET', path: '/neko/json/gai_text_to_image_package_id.json', auth: false, desc: '文生图配置', host: 'cdn' },
|
||||
];
|
||||
|
||||
module.exports = { ENDPOINTS, DOMAINS };
|
||||
|
||||
// 已知积木类型 (从编辑器代码逆向)
|
||||
const BLOCK_TYPES = {
|
||||
// 事件
|
||||
'sprite_on_tap': '角色点击事件',
|
||||
'event_when_run': '当绿旗被点击',
|
||||
'event_when_key': '当按下按键',
|
||||
'event_when_receive': '当接收到广播',
|
||||
'event_broadcast': '广播消息',
|
||||
'event_broadcast_wait': '广播并等待',
|
||||
'scene_on_tap': '场景点击事件',
|
||||
'stage_on_tap': '舞台点击事件',
|
||||
'self_on_start_as_clone': '当作为克隆体启动',
|
||||
'self_listen_with_param': '监听事件',
|
||||
|
||||
// 动作
|
||||
'move': '移动',
|
||||
'turn_right': '右转',
|
||||
'turn_left': '左转',
|
||||
'goto_xy': '移到坐标',
|
||||
'glide_to_xy': '滑行到坐标',
|
||||
'set_x': '设置 x',
|
||||
'set_y': '设置 y',
|
||||
'change_x': 'x 增加',
|
||||
'change_y': 'y 增加',
|
||||
'point_dir': '面向方向',
|
||||
'point_towards': '面向角色',
|
||||
'if_on_edge_bounce': '碰边缘反弹',
|
||||
'set_rotation_style': '设置旋转方式',
|
||||
|
||||
// 外观
|
||||
'show': '显示',
|
||||
'hide': '隐藏',
|
||||
'say': '说话',
|
||||
'say_for': '说话(限时)',
|
||||
'self_prev_next_style': '切换造型',
|
||||
'self_set_style': '设置造型',
|
||||
'set_size': '设置大小',
|
||||
'change_size': '大小增加',
|
||||
'set_effect': '设置特效',
|
||||
'change_effect': '特效增加',
|
||||
'clear_effects': '清除特效',
|
||||
'go_to_layer': '移到图层',
|
||||
|
||||
// 控制
|
||||
'wait': '等待',
|
||||
'repeat': '重复',
|
||||
'forever': '重复执行',
|
||||
'if': '如果',
|
||||
'if_else': '如果...否则',
|
||||
'repeat_until': '重复直到',
|
||||
'wait_until': '等待直到',
|
||||
'stop': '停止',
|
||||
'clone': '克隆',
|
||||
'dispose_clone': '删除克隆体',
|
||||
|
||||
// 侦测
|
||||
'touching': '碰到角色',
|
||||
'touching_color': '碰到颜色',
|
||||
'color_touching_color': '颜色碰颜色',
|
||||
'ask': '询问',
|
||||
'get_mouse_x': '鼠标 x',
|
||||
'get_mouse_y': '鼠标 y',
|
||||
'is_mouse_down': '按下鼠标',
|
||||
'get_timer': '计时器',
|
||||
'reset_timer': '重置计时器',
|
||||
|
||||
// 运算
|
||||
'add': '加法',
|
||||
'subtract': '减法',
|
||||
'multiply': '乘法',
|
||||
'divide': '除法',
|
||||
'random': '随机数',
|
||||
'gt': '大于',
|
||||
'lt': '小于',
|
||||
'equals': '等于',
|
||||
'and': '且',
|
||||
'or': '或',
|
||||
'not': '不是',
|
||||
'join': '连接',
|
||||
'length': '长度',
|
||||
'mod': '取余',
|
||||
'round': '四舍五入',
|
||||
'math_op': '数学运算',
|
||||
|
||||
// 变量
|
||||
'set_variable': '设置变量',
|
||||
'change_variable': '变量增加',
|
||||
'get_variable': '获取变量',
|
||||
'add_to_list': '添加到列表',
|
||||
'delete_of_list': '删除列表项',
|
||||
'insert_at_list': '插入列表项',
|
||||
'replace_at_list': '替换列表项',
|
||||
'item_of_list': '列表项',
|
||||
'item_num_of_list': '列表项编号',
|
||||
'length_of_list': '列表长度',
|
||||
'list_contains': '列表包含',
|
||||
|
||||
// 声音
|
||||
'play_sound': '播放声音',
|
||||
'stop_all_sounds': '停止所有声音',
|
||||
|
||||
// 画笔
|
||||
'pen_down': '落笔',
|
||||
'pen_up': '抬笔',
|
||||
'pen_clear': '清除画笔',
|
||||
'pen_set_color': '设置画笔颜色',
|
||||
'pen_set_size': '设置画笔粗细',
|
||||
|
||||
// AI
|
||||
'ai_text_actor': 'AI 文本识别(角色)',
|
||||
'ai_text_photo': 'AI 拍照识别',
|
||||
'ai_text_result': 'AI 识别结果',
|
||||
'ai_chat_ask': 'AI 聊天提问',
|
||||
'ai_chat_answer': 'AI 聊天回答',
|
||||
|
||||
// 硬件
|
||||
'microbit_display': 'micro:bit 显示',
|
||||
'microbit_get_button': 'micro:bit 按钮',
|
||||
'tudao_motor': '途道电机',
|
||||
'tudao_servo': '途道舵机',
|
||||
};
|
||||
|
||||
module.exports = { ENDPOINTS, DOMAINS, BLOCK_TYPES };
|
||||
-94
@@ -1,94 +0,0 @@
|
||||
/**
|
||||
* BCM 文件格式解析器
|
||||
* 兼容编程猫 Neko / Kitten 作品格式 (.bcmkn / .bcmknk)
|
||||
*/
|
||||
const fs = require('fs');
|
||||
|
||||
function parse(filePath) {
|
||||
const raw = fs.readFileSync(filePath, 'utf8');
|
||||
return JSON.parse(raw);
|
||||
}
|
||||
|
||||
function summary(bcm) {
|
||||
const actors = Object.values(bcm.actors?.actorsDict || {});
|
||||
const scenes = Object.values(bcm.scenes?.scenesDict || {});
|
||||
const styles = Object.values(bcm.styles?.stylesDict || {});
|
||||
const audios = Object.values(bcm.audios?.audiosDict || {});
|
||||
|
||||
const blockTypes = {};
|
||||
let totalBlocks = 0;
|
||||
actors.forEach(a => {
|
||||
(a.nekoBlockJsonList || []).forEach(b => {
|
||||
const bj = typeof b === 'string' ? JSON.parse(b) : b;
|
||||
blockTypes[bj.type] = (blockTypes[bj.type] || 0) + 1;
|
||||
totalBlocks++;
|
||||
});
|
||||
});
|
||||
|
||||
return {
|
||||
name: bcm.projectName || '',
|
||||
version: bcm.version || '',
|
||||
toolType: bcm.toolType || 'KN',
|
||||
stageSize: bcm.stageSize || {},
|
||||
actors: actors.map(a => ({
|
||||
id: a.id,
|
||||
name: a.name,
|
||||
blocks: (a.nekoBlockJsonList || []).length,
|
||||
styles: (a.styles || []).length,
|
||||
x: a.position?.x ?? 0,
|
||||
y: a.position?.y ?? 0,
|
||||
scale: a.scale ?? 100,
|
||||
rotation: a.rotation ?? 0,
|
||||
visible: a.visible !== false,
|
||||
})),
|
||||
scenes: scenes.map(s => ({
|
||||
id: s.id,
|
||||
name: s.name,
|
||||
actors: (s.actorIds || []).length,
|
||||
styles: (s.styles || []).length,
|
||||
})),
|
||||
totalStyles: styles.length,
|
||||
totalAudios: audios.length,
|
||||
totalBlocks,
|
||||
blockTypes,
|
||||
broadcasts: [...new Set(Object.values(bcm.broadcasts?.broadcastsDict || {}).flat().filter(b => typeof b === 'string'))],
|
||||
variables: Object.keys(bcm.variables?.variablesDict || {}),
|
||||
hiddenBlocks: bcm.hidden_toolbox?.blocks?.length || 0,
|
||||
previewUrl: bcm.previewUrl || '',
|
||||
courseMaterials: Object.keys(bcm.courseMaterials?.courseMaterialsDict || {}),
|
||||
};
|
||||
}
|
||||
|
||||
function listActors(bcm) {
|
||||
return Object.values(bcm.actors?.actorsDict || {}).map(a => {
|
||||
const styleUrl = bcm.styles?.stylesDict?.[a.currentStyleId]?.url || '';
|
||||
return { id: a.id, name: a.name, styleUrl, blocks: (a.nekoBlockJsonList || []).length };
|
||||
});
|
||||
}
|
||||
|
||||
function listBlocks(bcm) {
|
||||
const { BLOCK_TYPES } = require('./api');
|
||||
const result = [];
|
||||
Object.values(bcm.actors?.actorsDict || {}).forEach(a => {
|
||||
(a.nekoBlockJsonList || []).forEach((b, i) => {
|
||||
const bj = typeof b === 'string' ? JSON.parse(b) : b;
|
||||
const desc = BLOCK_TYPES[bj.type] || '';
|
||||
result.push({ actor: a.name, index: i, type: bj.type, desc, id: bj.id, hasNext: !!bj.next });
|
||||
});
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
function extractUrls(bcm) {
|
||||
const urls = [];
|
||||
Object.values(bcm.styles?.stylesDict || {}).forEach(s => {
|
||||
if (s.url) urls.push({ type: 'style', name: s.name, url: s.url });
|
||||
});
|
||||
Object.values(bcm.audios?.audiosDict || {}).forEach(a => {
|
||||
if (a.url) urls.push({ type: 'audio', name: a.name, url: a.url });
|
||||
});
|
||||
if (bcm.previewUrl) urls.push({ type: 'preview', name: 'preview', url: bcm.previewUrl });
|
||||
return urls;
|
||||
}
|
||||
|
||||
module.exports = { parse, summary, listActors, listBlocks, extractUrls };
|
||||
@@ -1,24 +0,0 @@
|
||||
/**
|
||||
* 编程猫域名配置
|
||||
*/
|
||||
module.exports = {
|
||||
creation: 'https://api-creation.codemao.cn',
|
||||
api: 'https://api.codemao.cn',
|
||||
open: 'https://open-service.codemao.cn',
|
||||
ai: 'https://cr-aichat.codemao.cn',
|
||||
socketcv: 'https://socketcv.codemao.cn',
|
||||
coneAi: 'https://cone-ai-web.codemao.cn',
|
||||
autojudge: 'https://autojudge.codemao.cn',
|
||||
cdn: 'https://creation.codemao.cn',
|
||||
staticCdn: 'https://static.codemao.cn',
|
||||
knCdn: 'https://kn-cdn.codemao.cn',
|
||||
bcmcdn: 'https://creation.bcmcdn.com',
|
||||
staticBcm: 'https://static.bcmcdn.com',
|
||||
shequ: 'https://shequ.codemao.cn',
|
||||
devCreation:'https://dev-api-creation.codemao.cn',
|
||||
devApi: 'https://backend-dev.codemao.cn',
|
||||
devOpen: 'https://dev-open-service.codemao.cn',
|
||||
testApi: 'https://test-api.codemao.cn',
|
||||
testKn: 'https://test-kn.codemao.cn',
|
||||
stagingKn: 'https://staging-kn.codemao.cn',
|
||||
};
|
||||
-389
@@ -1,389 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Codemao CLI - 编程猫社区命令行工具
|
||||
*
|
||||
* 开源地址: https://github.com/user/codemao-cli
|
||||
* 问题反馈: https://github.com/user/codemao-cli/issues
|
||||
*/
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const { ENDPOINTS, DOMAINS } = require('./api');
|
||||
const BCM = require('./bcm');
|
||||
const { request, printJson, printTable, error, warn, success } = require('./utils');
|
||||
|
||||
const VERSION = '1.0.0';
|
||||
const COOKIE_ENV = 'CODEMAO_COOKIE';
|
||||
|
||||
function getCookie() { return process.env[COOKIE_ENV] || ''; }
|
||||
|
||||
// ═══════════════ 命令注册 ═══════════════
|
||||
|
||||
const commands = {};
|
||||
|
||||
// ─── sample: 示例作品 ───
|
||||
|
||||
commands.sample = {
|
||||
desc: '示例作品操作',
|
||||
async run(args) {
|
||||
const sub = args[0] || 'list';
|
||||
if (sub === 'list') {
|
||||
const sid = args[1] || 1;
|
||||
const res = await request(`${DOMAINS.creation}/neko/sample/list?subject_id=${sid}`);
|
||||
if (res.status !== 200) error(`HTTP ${res.status}: ${res.raw}`);
|
||||
const items = res.data.items || [];
|
||||
console.log(`\n示例作品 (subject_id=${sid}):\n`);
|
||||
printTable(
|
||||
['ID', '名称', '使用次数', 'BCM URL'],
|
||||
items.map(i => [i.id, i.name, i.n_uses, i.bcm_url])
|
||||
);
|
||||
console.log(`\n共 ${items.length} 个\n`);
|
||||
} else if (sub === 'get') {
|
||||
const id = args[1];
|
||||
if (!id) error('用法: codemao sample get <id>');
|
||||
const res = await request(`${DOMAINS.creation}/neko/sample/detail?id=${id}`);
|
||||
printJson(res.data);
|
||||
} else if (sub === 'download') {
|
||||
const id = args[1];
|
||||
if (!id) error('用法: codemao sample download <id> [output]');
|
||||
const listRes = await request(`${DOMAINS.creation}/neko/sample/list?subject_id=1`);
|
||||
const item = (listRes.data.items || []).find(i => String(i.id) === String(id));
|
||||
if (!item) error(`未找到示例 ID: ${id}`);
|
||||
console.log(`下载: ${item.name} ...`);
|
||||
const bcmRes = await request(item.bcm_url);
|
||||
const outFile = args[2] || `${item.name}.bcmkn`;
|
||||
fs.writeFileSync(outFile, bcmRes.raw);
|
||||
success(`已保存: ${outFile} (${bcmRes.raw.length} bytes)`);
|
||||
} else {
|
||||
error(`未知子命令: ${sub}\n用法: codemao sample <list|get|download>`);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// ─── work: 用户作品 ───
|
||||
|
||||
commands.work = {
|
||||
desc: '用户作品操作 (需登录)',
|
||||
async run(args) {
|
||||
const sub = args[0] || 'list';
|
||||
const cookie = getCookie();
|
||||
if (!cookie) warn(`未设置 ${COOKIE_ENV},需认证的接口将返回 401`);
|
||||
|
||||
if (sub === 'list') {
|
||||
const page = args[1] || 1;
|
||||
const res = await request(`${DOMAINS.creation}/neko/works/?page=${page}`, { cookie });
|
||||
if (res.status === 401) error('请先登录 (设置 CODEMAO_COOKIE)');
|
||||
printJson(res.data);
|
||||
} else if (sub === 'get') {
|
||||
const id = args[1];
|
||||
if (!id) error('用法: codemao work get <id>');
|
||||
const res = await request(`${DOMAINS.creation}/neko/works/${id}`, { cookie });
|
||||
printJson(res.data);
|
||||
} else if (sub === 'download') {
|
||||
const id = args[1];
|
||||
if (!id) error('用法: codemao work download <id> [output]');
|
||||
const res = await request(`${DOMAINS.creation}/neko/works/${id}`, { cookie });
|
||||
if (res.data?.bcm_url) {
|
||||
const bcmRes = await request(res.data.bcm_url);
|
||||
const outFile = args[2] || `work_${id}.bcmkn`;
|
||||
fs.writeFileSync(outFile, bcmRes.raw);
|
||||
success(`已保存: ${outFile}`);
|
||||
} else if (res.data?.config) {
|
||||
const outFile = args[2] || `work_${id}.bcmkn`;
|
||||
fs.writeFileSync(outFile, typeof res.data.config === 'string' ? res.data.config : JSON.stringify(res.data.config));
|
||||
success(`已保存: ${outFile} (from config)`);
|
||||
} else {
|
||||
printJson(res.data);
|
||||
}
|
||||
} else {
|
||||
error(`未知子命令: ${sub}\n用法: codemao work <list|get|download>`);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// ─── bcm: BCM 文件操作 ───
|
||||
|
||||
commands.bcm = {
|
||||
desc: 'BCM 文件解析与操作',
|
||||
run(args) {
|
||||
const sub = args[0];
|
||||
const file = args[1];
|
||||
if (!sub || sub === 'help') {
|
||||
console.log(`
|
||||
用法: codemao bcm <子命令> <file>
|
||||
|
||||
子命令:
|
||||
info <file> BCM 摘要信息
|
||||
parse <file> 详细解析
|
||||
actors <file> 列出角色
|
||||
blocks <file> 列出积木
|
||||
extract <file> 提取资源 URL
|
||||
convert <input> <output> 转换格式 (美化 JSON)
|
||||
`);
|
||||
return;
|
||||
}
|
||||
if (!file || !fs.existsSync(file)) error(`文件不存在: ${file || '(未指定)'}`);
|
||||
|
||||
if (sub === 'info' || sub === 'parse') {
|
||||
const bcm = BCM.parse(file);
|
||||
const s = BCM.summary(bcm);
|
||||
console.log(`\n作品: ${s.name}`);
|
||||
console.log(`版本: ${s.version}`);
|
||||
console.log(`类型: ${s.toolType || 'KN'}`);
|
||||
console.log(`舞台: ${s.stageSize?.width} × ${s.stageSize?.height}`);
|
||||
console.log(`角色: ${s.actors.length}`);
|
||||
console.log(`场景: ${s.scenes.length}`);
|
||||
console.log(`造型: ${s.totalStyles}`);
|
||||
console.log(`音频: ${s.totalAudios}`);
|
||||
console.log(`积木: ${s.totalBlocks} (${Object.keys(s.blockTypes).length} 种类型)`);
|
||||
console.log(`广播: ${s.broadcasts.join(', ') || '(无)'}`);
|
||||
console.log(`变量: ${s.variables.join(', ') || '(无)'}`);
|
||||
console.log(`隐藏积木: ${s.hiddenBlocks}`);
|
||||
if (s.previewUrl) console.log(`预览: ${s.previewUrl}`);
|
||||
|
||||
if (sub === 'parse') {
|
||||
console.log('\n角色:');
|
||||
s.actors.forEach(a => console.log(` ${a.name} (${a.id}) blocks:${a.blocks} styles:${a.styles} pos:(${a.x},${a.y})`));
|
||||
console.log('\n场景:');
|
||||
s.scenes.forEach(sc => console.log(` ${sc.name} (${sc.id}) actors:${sc.actors} styles:${sc.styles}`));
|
||||
if (Object.keys(s.blockTypes).length) {
|
||||
console.log('\n积木类型:');
|
||||
Object.entries(s.blockTypes).sort((a, b) => b[1] - a[1]).forEach(([t, c]) => console.log(` ${t}: ${c}`));
|
||||
}
|
||||
}
|
||||
} else if (sub === 'actors') {
|
||||
const bcm = BCM.parse(file);
|
||||
BCM.listActors(bcm).forEach(a => {
|
||||
console.log(`${a.name} | ${a.id} | blocks:${a.blocks} | ${a.styleUrl}`);
|
||||
});
|
||||
} else if (sub === 'blocks') {
|
||||
const bcm = BCM.parse(file);
|
||||
const blocks = BCM.listBlocks(bcm);
|
||||
let lastActor = '';
|
||||
blocks.forEach(b => {
|
||||
if (b.actor !== lastActor) { console.log(`\n[${b.actor}]`); lastActor = b.actor; }
|
||||
const desc = b.desc ? ` [${b.desc}]` : ''; const next = b.hasNext ? ' → ...' : '';
|
||||
console.log(` ${b.index}: ${b.type} (${b.id})${desc}${next}`);
|
||||
});
|
||||
} else if (sub === 'extract') {
|
||||
const bcm = BCM.parse(file);
|
||||
BCM.extractUrls(bcm).forEach(u => console.log(`${u.type}\t${u.name}\t${u.url}`));
|
||||
} else if (sub === 'convert') {
|
||||
const outFile = args[2];
|
||||
if (!outFile) error('用法: codemao bcm convert <input> <output>');
|
||||
const bcm = BCM.parse(file);
|
||||
fs.writeFileSync(outFile, JSON.stringify(bcm, null, 2));
|
||||
success(`已转换: ${file} → ${outFile}`);
|
||||
} else {
|
||||
error(`未知子命令: ${sub}`);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// ─── api: 直接调用 API ───
|
||||
|
||||
commands.api = {
|
||||
desc: '直接调用编程猫 API',
|
||||
async run(args) {
|
||||
const apiPath = args[0];
|
||||
if (!apiPath) error('用法: codemao api <path> [--method M] [--host H] [--data JSON] [--raw]');
|
||||
|
||||
let method = 'GET', hostKey = 'creation', data = null, raw = false;
|
||||
for (let i = 1; i < args.length; i++) {
|
||||
if (args[i] === '--method') method = args[++i];
|
||||
else if (args[i] === '--host') hostKey = args[++i];
|
||||
else if (args[i] === '--data') data = args[++i];
|
||||
else if (args[i] === '--raw') raw = true;
|
||||
}
|
||||
|
||||
const host = DOMAINS[hostKey] || hostKey;
|
||||
const url = `${host}${apiPath.startsWith('/') ? '' : '/'}${apiPath}`;
|
||||
const cookie = getCookie();
|
||||
|
||||
console.log(`${method} ${url}\n`);
|
||||
const res = await request(url, {
|
||||
method,
|
||||
body: data,
|
||||
cookie,
|
||||
headers: data ? { 'Content-Type': 'application/json' } : {},
|
||||
});
|
||||
|
||||
console.log(`HTTP ${res.status}\n`);
|
||||
if (raw) {
|
||||
console.log(res.raw.substring(0, 5000));
|
||||
} else if (res.data) {
|
||||
printJson(res.data);
|
||||
} else {
|
||||
console.log(res.raw.substring(0, 5000));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// ─── domains: 域名列表 ───
|
||||
|
||||
commands.domains = {
|
||||
desc: '显示所有编程猫域名',
|
||||
run() {
|
||||
console.log('\n编程猫域名:\n');
|
||||
Object.entries(DOMAINS).forEach(([k, v]) => console.log(` ${k.padEnd(14)} ${v}`));
|
||||
console.log();
|
||||
}
|
||||
};
|
||||
|
||||
// ─── endpoints: 端点列表 ───
|
||||
|
||||
commands.endpoints = {
|
||||
desc: '显示所有 API 端点',
|
||||
run(args) {
|
||||
const filter = args[0];
|
||||
let list = ENDPOINTS;
|
||||
if (filter) {
|
||||
const f = filter.toLowerCase();
|
||||
list = list.filter(e =>
|
||||
e.path.toLowerCase().includes(f) ||
|
||||
(e.desc || '').toLowerCase().includes(f) ||
|
||||
(e.host || '').toLowerCase().includes(f)
|
||||
);
|
||||
}
|
||||
console.log(`\n端点列表 (${list.length}):\n`);
|
||||
printTable(
|
||||
['方法', '路径', '域名', '认证', '说明'],
|
||||
list.map(e => [e.method, e.path, e.host || '-', e.auth ? '🔒' : '🌐', e.desc || ''])
|
||||
);
|
||||
console.log();
|
||||
}
|
||||
};
|
||||
|
||||
// ─── search: 搜索端点 ───
|
||||
|
||||
commands.search = {
|
||||
desc: '搜索 API 端点',
|
||||
run(args) {
|
||||
const q = args.join(' ').toLowerCase();
|
||||
if (!q) error('用法: codemao search <关键词>');
|
||||
const results = ENDPOINTS.filter(e =>
|
||||
e.path.toLowerCase().includes(q) || (e.desc || '').toLowerCase().includes(q)
|
||||
);
|
||||
if (!results.length) { console.log('无匹配结果'); return; }
|
||||
console.log(`\n搜索 "${q}" 结果 (${results.length}):\n`);
|
||||
printTable(['方法', '路径', '说明'], results.map(e => [e.method, e.path, e.desc || '']));
|
||||
}
|
||||
};
|
||||
|
||||
// ─── download: 下载 BCM 模板 ───
|
||||
|
||||
commands.download = {
|
||||
desc: '下载 BCM 模板或 CDN 资源',
|
||||
async run(args) {
|
||||
const what = args[0] || 'help';
|
||||
const templates = {
|
||||
'default-v': '/neko/bcm/kn-default-v-0.13.1.bcmkn',
|
||||
'default-h': '/neko/bcm/kn-default-h-0.13.1.bcmkn',
|
||||
'kids-v': '/neko/bcm/kn-kids-default-v-0.13.1.bcmknk',
|
||||
'kids-h': '/neko/bcm/kn-kids-default-h-0.13.1.bcmknk',
|
||||
'exam-v': '/neko/bcm/kn-exam-default-v-0.18.0.bcmkn',
|
||||
'exam-h': '/neko/bcm/kn-exam-default-h-0.18.0.bcmkn',
|
||||
};
|
||||
if (what === 'help' || what === 'list') {
|
||||
console.log('\n可用模板:\n');
|
||||
Object.entries(templates).forEach(([k, v]) => console.log(` ${k.padEnd(12)} ${v}`));
|
||||
console.log('\n用法: codemao download <模板名> [输出文件]\n');
|
||||
return;
|
||||
}
|
||||
const tplPath = templates[what];
|
||||
if (!tplPath) error(`未知模板: ${what}\n运行 codemao download list 查看可用模板`);
|
||||
const url = `${DOMAINS.cdn}${tplPath}`;
|
||||
console.log(`下载: ${what} ...`);
|
||||
const res = await request(url);
|
||||
const outFile = args[1] || `${what}.bcmkn`;
|
||||
fs.writeFileSync(outFile, res.raw);
|
||||
success(`已保存: ${outFile} (${res.raw.length} bytes)`);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
// ─── blocks: 积木类型一览 ───
|
||||
|
||||
commands.blocks = {
|
||||
desc: '显示所有已知积木类型',
|
||||
run(args) {
|
||||
const { BLOCK_TYPES } = require('./api');
|
||||
const filter = args[0];
|
||||
let entries = Object.entries(BLOCK_TYPES);
|
||||
if (filter) {
|
||||
const f = filter.toLowerCase();
|
||||
entries = entries.filter(([k, v]) => k.includes(f) || v.includes(f));
|
||||
}
|
||||
console.log(`\n已知积木类型 (${entries.length}):\n`);
|
||||
printTable(['类型', '说明'], entries.map(([k, v]) => [k, v]));
|
||||
console.log();
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
// ─── version / help ───
|
||||
|
||||
commands.version = {
|
||||
desc: '显示版本',
|
||||
run() { console.log(`codemao-cli v${VERSION}`); }
|
||||
};
|
||||
|
||||
commands.help = {
|
||||
desc: '显示帮助',
|
||||
run() {
|
||||
console.log(`
|
||||
\x1b[1mCodemao CLI\x1b[0m v${VERSION} - 编程猫社区命令行工具
|
||||
|
||||
\x1b[1m用法:\x1b[0m
|
||||
`);
|
||||
Object.entries(commands).forEach(([name, cmd]) => {
|
||||
console.log(` \x1b[36m${name.padEnd(14)}\x1b[0m ${cmd.desc}`);
|
||||
});
|
||||
console.log(`
|
||||
\x1b[1m示例:\x1b[0m
|
||||
codemao sample list 列出示例作品
|
||||
codemao sample download 2 下载示例 BCM
|
||||
codemao bcm info ./作品.bcmkn BCM 摘要
|
||||
codemao bcm extract ./作品.bcmkn 提取资源 URL
|
||||
codemao work list 列出用户作品
|
||||
codemao endpoints ranking 搜索端点
|
||||
codemao api /neko/sample/list?subject_id=1
|
||||
codemao download default-v 下载默认模板
|
||||
|
||||
\x1b[1m环境变量:\x1b[0m
|
||||
CODEMAO_COOKIE 登录 Cookie (用于需认证的接口)
|
||||
|
||||
\x1b[1m域名:\x1b[0m
|
||||
creation api-creation.codemao.cn 创作服务
|
||||
api api.codemao.cn 主 API
|
||||
cdn creation.codemao.cn CDN 资源
|
||||
|
||||
\x1b[1m开源:\x1b[0m MIT License
|
||||
`);
|
||||
}
|
||||
};
|
||||
|
||||
// ═══════════════ 入口 ═══════════════
|
||||
|
||||
async function main() {
|
||||
const args = process.argv.slice(2);
|
||||
const cmd = args[0];
|
||||
const cmdArgs = args.slice(1);
|
||||
|
||||
if (!cmd || cmd === '-h' || cmd === '--help') return commands.help.run();
|
||||
if (cmd === '-v' || cmd === '--version') return commands.version.run();
|
||||
|
||||
const handler = commands[cmd];
|
||||
if (!handler) {
|
||||
console.error(`\x1b[31m未知命令: ${cmd}\x1b[0m\n运行 codemao help 查看帮助`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
try {
|
||||
await handler.run(cmdArgs);
|
||||
} catch (e) {
|
||||
console.error(`\x1b[31m错误: ${e.message}\x1b[0m`);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
main();
|
||||
@@ -1,78 +0,0 @@
|
||||
/**
|
||||
* 共享工具函数
|
||||
*/
|
||||
const https = require('https');
|
||||
const http = require('http');
|
||||
|
||||
function request(url, options = {}) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const u = new URL(url);
|
||||
const mod = u.protocol === 'https:' ? https : http;
|
||||
const opts = {
|
||||
hostname: u.hostname,
|
||||
port: u.port || (u.protocol === 'https:' ? 443 : 80),
|
||||
path: u.pathname + u.search,
|
||||
method: options.method || 'GET',
|
||||
headers: {
|
||||
'User-Agent': 'CodemaoCLI/1.0.0',
|
||||
'Accept': 'application/json, */*',
|
||||
...options.headers,
|
||||
},
|
||||
};
|
||||
if (options.cookie) opts.headers['Cookie'] = options.cookie;
|
||||
|
||||
const req = mod.request(opts, res => {
|
||||
const chunks = [];
|
||||
res.on('data', chunk => chunks.push(chunk));
|
||||
res.on('end', () => {
|
||||
const raw = Buffer.concat(chunks).toString();
|
||||
let data;
|
||||
try { data = JSON.parse(raw); } catch { data = null; }
|
||||
resolve({ status: res.statusCode, data, headers: res.headers, raw });
|
||||
});
|
||||
});
|
||||
req.on('error', reject);
|
||||
req.setTimeout(options.timeout || 30000, () => { req.destroy(new Error('Request timeout')); });
|
||||
if (options.body) req.write(typeof options.body === 'string' ? options.body : JSON.stringify(options.body));
|
||||
req.end();
|
||||
});
|
||||
}
|
||||
|
||||
function printJson(data) {
|
||||
console.log(JSON.stringify(data, null, 2));
|
||||
}
|
||||
|
||||
function printTable(headers, rows) {
|
||||
if (!rows.length) { console.log('(空)'); return; }
|
||||
const widths = headers.map((h, i) => Math.max(h.length, ...rows.map(r => displayWidth(String(r[i] ?? '')))));
|
||||
const sep = widths.map(w => '─'.repeat(w + 2)).join('┼');
|
||||
console.log(headers.map((h, i) => padEnd(h, widths[i])).join(' │ '));
|
||||
console.log(sep);
|
||||
rows.forEach(r => console.log(r.map((c, i) => padEnd(String(c ?? ''), widths[i])).join(' │ ')));
|
||||
}
|
||||
|
||||
function displayWidth(str) {
|
||||
let w = 0;
|
||||
for (const ch of str) w += ch.charCodeAt(0) > 255 ? 2 : 1;
|
||||
return w;
|
||||
}
|
||||
|
||||
function padEnd(str, len) {
|
||||
const diff = len - displayWidth(str);
|
||||
return diff > 0 ? str + ' '.repeat(diff) : str;
|
||||
}
|
||||
|
||||
function error(msg) {
|
||||
console.error(`\x1b[31m错误: ${msg}\x1b[0m`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
function warn(msg) {
|
||||
console.error(`\x1b[33m提示: ${msg}\x1b[0m`);
|
||||
}
|
||||
|
||||
function success(msg) {
|
||||
console.log(`\x1b[32m${msg}\x1b[0m`);
|
||||
}
|
||||
|
||||
module.exports = { request, printJson, printTable, error, warn, success };
|
||||
Reference in new issue
Block a user