diff --git a/.gitignore b/.gitignore deleted file mode 100644 index 1f09ade..0000000 --- a/.gitignore +++ /dev/null @@ -1,5 +0,0 @@ -node_modules/ -*.bcmkn -*.bcmknk -*.png -.DS_Store diff --git a/LICENSE b/LICENSE deleted file mode 100644 index 7203af6..0000000 --- a/LICENSE +++ /dev/null @@ -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. diff --git a/README.md b/README.md index 549da7f..4d9a071 100644 --- a/README.md +++ b/README.md @@ -2,8 +2,7 @@ 你好呀~这个仓库已经搬到更好用的地方啦。 -为了让代码管理更清爽、多平台同步更省心,我们把原先分散的镜像统一收拢到 -**Gitee** 与 **GitHub** 主仓维护,这里就只保留这张"迁移小纸条"咯。 +为了让代码管理更清爽、多平台同步更省心,我们把原先分散的镜像统一收拢到主仓维护,这里就只保留这张"迁移小纸条"咯。 📦 新家在这里: - Gitee:https://gitee.com/shuiyu1123/codemao-cli diff --git a/package.json b/package.json deleted file mode 100644 index 8e04ba7..0000000 --- a/package.json +++ /dev/null @@ -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" - } -} diff --git a/src/api.js b/src/api.js deleted file mode 100644 index ecf074a..0000000 --- a/src/api.js +++ /dev/null @@ -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 }; diff --git a/src/bcm.js b/src/bcm.js deleted file mode 100644 index 6abd06a..0000000 --- a/src/bcm.js +++ /dev/null @@ -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 }; diff --git a/src/domains.js b/src/domains.js deleted file mode 100644 index 48cc73f..0000000 --- a/src/domains.js +++ /dev/null @@ -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', -}; diff --git a/src/index.js b/src/index.js deleted file mode 100755 index 650dcf3..0000000 --- a/src/index.js +++ /dev/null @@ -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 '); - 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 [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 `); - } - } -}; - -// ─── 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 '); - 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 [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 `); - } - } -}; - -// ─── bcm: BCM 文件操作 ─── - -commands.bcm = { - desc: 'BCM 文件解析与操作', - run(args) { - const sub = args[0]; - const file = args[1]; - if (!sub || sub === 'help') { - console.log(` -用法: codemao bcm <子命令> - -子命令: - info BCM 摘要信息 - parse 详细解析 - actors 列出角色 - blocks 列出积木 - extract 提取资源 URL - convert 转换格式 (美化 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 '); - 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 [--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(); diff --git a/src/utils.js b/src/utils.js deleted file mode 100644 index dcbae05..0000000 --- a/src/utils.js +++ /dev/null @@ -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 };