feat: codemao-cli v1.0.0
- 62 个 API 端点 (13 个域名) - BCM 文件格式解析 - 示例作品下载 - 模板下载 - 端点搜索 - MIT 开源
This commit is contained in:
commit
67ff2655da
9 files changed
+885
No files matched your search
@@ -0,0 +1,5 @@
|
|||||||
|
node_modules/
|
||||||
|
*.bcmkn
|
||||||
|
*.bcmknk
|
||||||
|
*.png
|
||||||
|
.DS_Store
|
||||||
@@ -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.
|
||||||
@@ -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
|
||||||
@@ -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
@@ -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
@@ -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 };
|
||||||
@@ -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
@@ -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();
|
||||||
@@ -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 };
|
||||||
Reference in new issue
Block a user