feat: codemao-cli v1.0.0

- 62 个 API 端点 (13 个域名)
- BCM 文件格式解析
- 示例作品下载
- 模板下载
- 端点搜索
- MIT 开源
This commit is contained in:
codemao-cli committed 2026-05-02 23:55:39 +08:00
commit 67ff2655da
9 files changed
+885

No files matched your search

+5
View File
@@ -0,0 +1,5 @@
node_modules/
*.bcmkn
*.bcmknk
*.png
.DS_Store
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2026
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.
+160
View File
@@ -0,0 +1,160 @@
# Codemao CLI
编程猫 (Codemao) 社区命令行工具。无需登录即可访问公开 API,解析 BCM 作品文件。
## 安装
```bash
npm install -g codemao-cli
```
或直接运行:
```bash
npx codemao-cli help
```
## 功能
| 命令 | 说明 |
|------|------|
| `codemao sample list` | 列出 14 个官方示例作品 |
| `codemao sample download <id>` | 下载示例 BCM 文件 |
| `codemao bcm info <file>` | 解析 BCM 文件摘要 |
| `codemao bcm actors <file>` | 列出所有角色 |
| `codemao bcm blocks <file>` | 列出所有积木 |
| `codemao bcm extract <file>` | 提取资源 URL |
| `codemao work list` | 列出用户作品 (需登录) |
| `codemao download <template>` | 下载 BCM 模板 |
| `codemao endpoints [filter]` | 搜索 62 个 API 端点 |
| `codemao domains` | 显示 13 个域名 |
| `codemao api <path>` | 直接调用 API |
## 快速开始
```bash
# 列出示例作品
codemao sample list
# 下载 "脸萌" 示例
codemao sample download 2
# 查看作品结构
codemao bcm info 脸萌.bcmkn
# 提取所有图片/音频 URL
codemao bcm extract 脸萌.bcmkn
# 下载默认模板
codemao download default-v
```
## BCM 文件格式
BCM (Block Code Metadata) 是编程猫的积木作品文件格式,纯 JSON 结构:
```json
{
"projectName": "我的作品",
"version": "0.18.0",
"toolType": "KN",
"stageSize": { "width": 562, "height": 900 },
"scenes": {
"scenesDict": { "scene-uuid": { "name": "背景", "actorIds": [...] } }
},
"actors": {
"actorsDict": { "actor-uuid": { "name": "角色", "nekoBlockJsonList": [...] } }
},
"styles": {
"stylesDict": { "style-uuid": { "name": "造型", "url": "https://..." } }
},
"audios": { "audiosDict": {} },
"variables": { "variablesDict": {} },
"broadcasts": { "broadcastsDict": {} },
"procedures": { "proceduresDict": {} }
}
```
### 积木结构
```json
{
"type": "sprite_on_tap",
"id": "block-uuid",
"fields": { "type": "mouse_click", "sprite": "actor-uuid" },
"parent_id": "",
"next": { "type": "self_prev_next_style", "id": "..." }
}
```
### 已知积木类型
| 类型 | 说明 |
|------|------|
| `sprite_on_tap` | 角色点击事件 |
| `event_when_run` | 绿旗点击 |
| `event_when_key` | 按键事件 |
| `move` | 移动 |
| `turn_right` / `turn_left` | 转向 |
| `goto_xy` | 移到坐标 |
| `show` / `hide` | 显示/隐藏 |
| `say` | 说话 |
| `wait` | 等待 |
| `repeat` | 重复 |
| `forever` | 重复执行 |
| `if` / `if_else` | 条件判断 |
| `self_prev_next_style` | 切换造型 |
## API 端点
完整端点列表 (62 个):
```bash
codemao endpoints # 全部
codemao endpoints ranking # 搜索
codemao search 作品 # 搜索
```
### 公开端点 (无需登录)
| 端点 | 说明 |
|------|------|
| `GET /neko/sample/list` | 示例作品列表 |
| `GET /neko/sample/detail` | 示例作品详情 |
| `GET /coconut/clouddb/currentTime` | 云数据库时间 |
| `GET /tiger/online/check` | 在线检查 |
### 需认证端点
设置环境变量:
```bash
export CODEMAO_COOKIE="your_cookie_value"
```
从浏览器开发者工具获取 Cookie。
## 域名
| 名称 | 域名 | 用途 |
|------|------|------|
| creation | api-creation.codemao.cn | 创作服务 |
| api | api.codemao.cn | 主 API |
| open | open-service.codemao.cn | 开放服务 |
| ai | cr-aichat.codemao.cn | AI 服务 |
| cdn | creation.codemao.cn | CDN 资源 |
| staticCdn | static.codemao.cn | 静态资源 |
| bcmcdn | creation.bcmcdn.com | BCM CDN |
## 开发
```bash
git clone https://github.com/user/codemao-cli
cd codemao-cli
npm install
npm test
```
## License
MIT
+37
View File
@@ -0,0 +1,37 @@
{
"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": ""
},
"engines": {
"node": ">=14"
},
"files": [
"src/",
"README.md",
"LICENSE"
]
}
+99
View File
@@ -0,0 +1,99 @@
/**
* 编程猫 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 };
+92
View File
@@ -0,0 +1,92 @@
/**
* 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 result = [];
Object.values(bcm.actors?.actorsDict || {}).forEach(a => {
(a.nekoBlockJsonList || []).forEach((b, i) => {
const bj = typeof b === 'string' ? JSON.parse(b) : b;
result.push({ actor: a.name, index: i, type: bj.type, 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
@@ -0,0 +1,24 @@
/**
* 编程猫域名配置
*/
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',
};
Executable
+369
View File
@@ -0,0 +1,369 @@
#!/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 next = b.hasNext ? ' → ...' : '';
console.log(` ${b.index}: ${b.type} (${b.id})${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)`);
}
};
// ─── 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
@@ -0,0 +1,78 @@
/**
* 共享工具函数
*/
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 };