chore: 项目已迁移至 Gitee/GitHub,仅保留迁移说明 (tombstone)

This commit is contained in:
shuiyuPyLab committed 2026-08-09 10:15:46 +08:00
1 parent 9531f2ef83
commit 1bd23cf015
9 files changed
+1 -877

No files matched your search

-5
View File
@@ -1,5 +0,0 @@
node_modules/
*.bcmkn
*.bcmknk
*.png
.DS_Store
-21
View File
@@ -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.
+1 -2
View File
@@ -2,8 +2,7 @@
你好呀~这个仓库已经搬到更好用的地方啦。 你好呀~这个仓库已经搬到更好用的地方啦。
为了让代码管理更清爽、多平台同步更省心,我们把原先分散的镜像统一收拢到 为了让代码管理更清爽、多平台同步更省心,我们把原先分散的镜像统一收拢到主仓维护,这里就只保留这张"迁移小纸条"咯。
**Gitee** 与 **GitHub** 主仓维护,这里就只保留这张"迁移小纸条"咯。
📦 新家在这里: 📦 新家在这里:
- Gitee:https://gitee.com/shuiyu1123/codemao-cli - Gitee:https://gitee.com/shuiyu1123/codemao-cli
-41
View File
@@ -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
View File
@@ -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
View File
@@ -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 };
-24
View File
@@ -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
View File
@@ -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();
-78
View File
@@ -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 };