skillfree 0.1.2 → 0.1.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/SKILL.md +58 -9
- package/package.json +1 -1
- package/scripts/commands/pilot.js +246 -75
- package/skillfree-0.1.4.tgz +0 -0
package/SKILL.md
CHANGED
|
@@ -2,36 +2,85 @@
|
|
|
2
2
|
|
|
3
3
|
## 功能
|
|
4
4
|
|
|
5
|
-
SkillFree 是一个统一的 AI 能力调用层。通过一个 API Key
|
|
5
|
+
SkillFree 是一个统一的 AI 能力调用层。通过一个 API Key,调用对话、图像生成、语音合成、视频生成、音乐生成、文档 OCR、向量 Embedding 等 56 个模型。
|
|
6
6
|
|
|
7
7
|
## 使用方式
|
|
8
8
|
|
|
9
|
+
### 对话(chat)
|
|
9
10
|
```bash
|
|
10
|
-
# 对话(1 积分)
|
|
11
11
|
skillfree pilot --type chat --prompt "帮我写一首诗"
|
|
12
|
+
skillfree pilot --type chat --model claude-sonnet-4-6 --prompt "分析一下最近的 AI 趋势"
|
|
13
|
+
```
|
|
12
14
|
|
|
13
|
-
|
|
15
|
+
### 图像生成(image)
|
|
16
|
+
```bash
|
|
14
17
|
skillfree pilot --type image --prompt "赛博朋克的上海" --output ./out.png
|
|
18
|
+
skillfree pilot --type image --model doubao-seedream-5.0-lite --prompt "一只龙虾" --output ./lobster.png
|
|
19
|
+
```
|
|
15
20
|
|
|
16
|
-
|
|
21
|
+
### 语音合成(tts)
|
|
22
|
+
```bash
|
|
17
23
|
skillfree pilot --type tts --text "你好,龙虾" --output hello.mp3
|
|
24
|
+
```
|
|
18
25
|
|
|
19
|
-
|
|
26
|
+
### 视频生成(video)
|
|
27
|
+
```bash
|
|
20
28
|
skillfree pilot --type video --prompt "海浪拍打礁石" --output ./video.mp4
|
|
21
29
|
```
|
|
22
30
|
|
|
23
|
-
|
|
31
|
+
### 音乐生成(music)
|
|
32
|
+
```bash
|
|
33
|
+
skillfree pilot --type music --prompt "轻松欢快的背景音乐" --output ./bgm.mp3
|
|
34
|
+
```
|
|
24
35
|
|
|
36
|
+
### 查看所有模型
|
|
25
37
|
```bash
|
|
26
|
-
|
|
38
|
+
skillfree models
|
|
27
39
|
```
|
|
28
40
|
|
|
29
|
-
|
|
41
|
+
### 查看余额
|
|
42
|
+
```bash
|
|
43
|
+
skillfree balance
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## 积分参考(2026-03 定价)
|
|
47
|
+
|
|
48
|
+
| 类型 | 模型 | 积分/次 |
|
|
49
|
+
|------|------|---------|
|
|
50
|
+
| chat | DeepSeek V3.2 Fast | 1 |
|
|
51
|
+
| chat | Kimi K2.5 | 1 |
|
|
52
|
+
| chat | Gemini 2.5 Pro | 7 |
|
|
53
|
+
| chat | GPT-5.4 | 9 |
|
|
54
|
+
| chat | Claude Sonnet 4.6 | 12 |
|
|
55
|
+
| chat | Claude Opus 4.6 | 59 |
|
|
56
|
+
| image | 即梦 Seedream 5.0 Lite | 11 |
|
|
57
|
+
| image | Gemini 3 Pro Image | 54 |
|
|
58
|
+
| tts | MiniMax TTS 2.8 HD | 18 |
|
|
59
|
+
| video | 拍我 5.6 文生视频 | 160 |
|
|
60
|
+
| video | 可灵 2.6 文生视频 | 427 |
|
|
61
|
+
| music | MiniMax Music 2.5 | 80 |
|
|
62
|
+
| music | Suno V5 | 107 |
|
|
63
|
+
| ocr | 合合图文混排解析 | 3 |
|
|
64
|
+
| embedding | 豆包 Embedding Vision | 1 |
|
|
65
|
+
|
|
66
|
+
最新定价以 https://skillfree.tech/app/models 为准。
|
|
67
|
+
|
|
68
|
+
## 配置
|
|
30
69
|
|
|
31
70
|
```bash
|
|
71
|
+
# 方式一:登录(推荐)
|
|
32
72
|
skillfree auth login
|
|
73
|
+
|
|
74
|
+
# 方式二:环境变量
|
|
75
|
+
export SKILLFREE_API_KEY=sk-sf-xxxxxxxx
|
|
76
|
+
|
|
77
|
+
# 方式三:直接调用 API
|
|
78
|
+
curl https://skillfree.tech/v1/chat/completions \
|
|
79
|
+
-H "Authorization: Bearer sk-sf-xxxxxxxx" \
|
|
80
|
+
-H "Content-Type: application/json" \
|
|
81
|
+
-d '{"model":"DeepSeek-V3.2-Fast","messages":[{"role":"user","content":"你好"}]}'
|
|
33
82
|
```
|
|
34
83
|
|
|
35
84
|
## 充值
|
|
36
85
|
|
|
37
|
-
充多少用多少,积分永不过期:https://skillfree.tech/billing
|
|
86
|
+
充多少用多少,积分永不过期:https://skillfree.tech/app/billing
|
package/package.json
CHANGED
|
@@ -1,100 +1,271 @@
|
|
|
1
|
-
const { post, postStream, BASE_URL } = require('../lib/client')
|
|
1
|
+
const { post, postStream, get, request, getApiKey, BASE_URL } = require('../lib/client')
|
|
2
2
|
const fs = require('fs')
|
|
3
3
|
const path = require('path')
|
|
4
4
|
|
|
5
|
+
// ─── 工具:下载文件并保存 ──────────────────────────────────────────────────────
|
|
6
|
+
async function downloadAndSave(url, output) {
|
|
7
|
+
const resp = await fetch(url)
|
|
8
|
+
if (!resp.ok) throw new Error(`下载失败: ${resp.status}`)
|
|
9
|
+
fs.writeFileSync(output, Buffer.from(await resp.arrayBuffer()))
|
|
10
|
+
console.log(`✅ 已保存到 ${output}`)
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
// ─── 工具:封装 PCM 为 WAV ─────────────────────────────────────────────────────
|
|
14
|
+
function pcmToWav(pcmBytes, sampleRate = 24000, channels = 1, bitsPerSample = 16) {
|
|
15
|
+
const dataSize = pcmBytes.length
|
|
16
|
+
const wav = Buffer.alloc(44 + dataSize)
|
|
17
|
+
wav.write('RIFF', 0); wav.writeUInt32LE(36 + dataSize, 4); wav.write('WAVE', 8)
|
|
18
|
+
wav.write('fmt ', 12); wav.writeUInt32LE(16, 16); wav.writeUInt16LE(1, 20)
|
|
19
|
+
wav.writeUInt16LE(channels, 22); wav.writeUInt32LE(sampleRate, 24)
|
|
20
|
+
wav.writeUInt32LE(sampleRate * channels * bitsPerSample / 8, 28)
|
|
21
|
+
wav.writeUInt16LE(channels * bitsPerSample / 8, 32); wav.writeUInt16LE(bitsPerSample, 34)
|
|
22
|
+
wav.write('data', 36); wav.writeUInt32LE(dataSize, 40)
|
|
23
|
+
pcmBytes.copy(wav, 44)
|
|
24
|
+
return wav
|
|
25
|
+
}
|
|
26
|
+
|
|
5
27
|
async function pilot(flags) {
|
|
6
28
|
const type = flags.type || 'chat'
|
|
7
29
|
const prompt = flags.prompt || flags.text || ''
|
|
8
30
|
const output = flags.output || null
|
|
31
|
+
const model = flags.model || null
|
|
9
32
|
|
|
10
|
-
//
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
+
// ── CHAT ─────────────────────────────────────────────────────────────────────
|
|
34
|
+
if (type === 'chat') {
|
|
35
|
+
if (!output) {
|
|
36
|
+
// 流式输出
|
|
37
|
+
const res = await postStream('/chat/completions', {
|
|
38
|
+
model: model || 'DeepSeek-V3.2-Fast',
|
|
39
|
+
messages: [{ role: 'user', content: prompt }],
|
|
40
|
+
stream: true,
|
|
41
|
+
})
|
|
42
|
+
const reader = res.body.getReader()
|
|
43
|
+
const decoder = new TextDecoder()
|
|
44
|
+
let buf = ''
|
|
45
|
+
process.stdout.write('\n')
|
|
46
|
+
while (true) {
|
|
47
|
+
const { done, value } = await reader.read()
|
|
48
|
+
if (done) break
|
|
49
|
+
buf += decoder.decode(value, { stream: true })
|
|
50
|
+
const lines = buf.split('\n')
|
|
51
|
+
buf = lines.pop()
|
|
52
|
+
for (const line of lines) {
|
|
53
|
+
if (line.startsWith('data: ')) {
|
|
54
|
+
const chunk = line.slice(6)
|
|
55
|
+
if (chunk === '[DONE]') { process.stdout.write('\n'); return }
|
|
56
|
+
try {
|
|
57
|
+
const d = JSON.parse(chunk)
|
|
58
|
+
const text = d.choices?.[0]?.delta?.content
|
|
59
|
+
if (text) process.stdout.write(text)
|
|
60
|
+
} catch {}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
33
63
|
}
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
default:
|
|
45
|
-
body.inputs.prompt = prompt
|
|
64
|
+
return
|
|
65
|
+
}
|
|
66
|
+
const result = await post('/chat/completions', {
|
|
67
|
+
model: model || 'DeepSeek-V3.2-Fast',
|
|
68
|
+
messages: [{ role: 'user', content: prompt }],
|
|
69
|
+
})
|
|
70
|
+
const text = result.choices?.[0]?.message?.content || JSON.stringify(result, null, 2)
|
|
71
|
+
if (output) { fs.writeFileSync(output, text); console.log(`✅ 已保存到 ${output}`) }
|
|
72
|
+
else console.log(text)
|
|
73
|
+
return
|
|
46
74
|
}
|
|
47
75
|
|
|
48
|
-
//
|
|
49
|
-
if (type === '
|
|
50
|
-
const res = await
|
|
51
|
-
model:
|
|
52
|
-
|
|
53
|
-
|
|
76
|
+
// ── IMAGE ─────────────────────────────────────────────────────────────────────
|
|
77
|
+
if (type === 'image') {
|
|
78
|
+
const res = await post('/images/generations', {
|
|
79
|
+
model: model || 'gemini-3.1-flash-image-preview',
|
|
80
|
+
prompt,
|
|
81
|
+
n: 1,
|
|
82
|
+
size: flags.size || '1024x1024',
|
|
54
83
|
})
|
|
84
|
+
if (res.error) throw new Error(res.error.message || JSON.stringify(res.error))
|
|
85
|
+
const url = res.data?.[0]?.url || res.data?.[0]?.b64_json
|
|
86
|
+
if (!url) throw new Error('未返回图像数据: ' + JSON.stringify(res).slice(0, 200))
|
|
87
|
+
if (output) {
|
|
88
|
+
if (url.startsWith('http')) {
|
|
89
|
+
await downloadAndSave(url, output)
|
|
90
|
+
} else {
|
|
91
|
+
fs.writeFileSync(output, Buffer.from(url, 'base64'))
|
|
92
|
+
console.log(`✅ 已保存到 ${output}`)
|
|
93
|
+
}
|
|
94
|
+
} else {
|
|
95
|
+
console.log('图像 URL:', url)
|
|
96
|
+
}
|
|
97
|
+
return
|
|
98
|
+
}
|
|
55
99
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
}
|
|
100
|
+
// ── TTS ───────────────────────────────────────────────────────────────────────
|
|
101
|
+
if (type === 'tts') {
|
|
102
|
+
const ttsModel = model || 'speech-2.6-hd'
|
|
103
|
+
const text = prompt
|
|
104
|
+
|
|
105
|
+
if (ttsModel === 'speech-2.8-hd' || ttsModel === 'minimax-clone-lastversion') {
|
|
106
|
+
// 走 /v1/responses,返回 hex 音频
|
|
107
|
+
const res = await request('/responses', {
|
|
108
|
+
method: 'POST',
|
|
109
|
+
body: JSON.stringify({
|
|
110
|
+
model: ttsModel,
|
|
111
|
+
input: text,
|
|
112
|
+
stream: false,
|
|
113
|
+
voice_setting: {
|
|
114
|
+
voice_id: flags.voice || 'female-shaonv',
|
|
115
|
+
speed: 1, vol: 1, pitch: 0, emotion: 'fluent',
|
|
116
|
+
},
|
|
117
|
+
audio_setting: { sample_rate: 32000, bitrate: 128000, format: 'mp3', channel: 1 },
|
|
118
|
+
output_format: 'hex',
|
|
119
|
+
}),
|
|
120
|
+
})
|
|
121
|
+
const data = await res.json()
|
|
122
|
+
if (!data.data?.audio) throw new Error(JSON.stringify(data).slice(0, 200))
|
|
123
|
+
const audioBuf = Buffer.from(data.data.audio, 'hex')
|
|
124
|
+
if (output) { fs.writeFileSync(output, audioBuf); console.log(`✅ 已保存到 ${output}(${audioBuf.length} bytes)`) }
|
|
125
|
+
else console.log(`✅ TTS 成功,时长约 ${(data.extra_info?.audio_length / 1000).toFixed(1)} 秒`)
|
|
126
|
+
|
|
127
|
+
} else if (ttsModel === 'gemini-2.5-pro-preview-tts' || ttsModel === 'gemini-2.5-flash-preview-tts') {
|
|
128
|
+
// Gemini TTS:走 /v1beta/models/:id:generateContent,认证无 Bearer
|
|
129
|
+
const apiKey = getApiKey()
|
|
130
|
+
const res = await fetch(BASE_URL.replace('skillfree.tech', 'dmxapi.cn') + '/v1beta/models/' + ttsModel + ':generateContent', {
|
|
131
|
+
method: 'POST',
|
|
132
|
+
headers: { 'Authorization': apiKey, 'Content-Type': 'application/json' },
|
|
133
|
+
body: JSON.stringify({
|
|
134
|
+
contents: [{ parts: [{ text }] }],
|
|
135
|
+
generationConfig: {
|
|
136
|
+
responseModalities: ['AUDIO'],
|
|
137
|
+
speechConfig: {
|
|
138
|
+
voiceConfig: { prebuiltVoiceConfig: { voiceName: flags.voice || 'Kore' } }
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}),
|
|
142
|
+
})
|
|
143
|
+
const data = await res.json()
|
|
144
|
+
if (data.error) throw new Error(data.error.message || JSON.stringify(data.error))
|
|
145
|
+
const pcm = Buffer.from(data.candidates[0].content.parts[0].inlineData.data, 'base64')
|
|
146
|
+
const ext = (output || '').endsWith('.mp3') ? 'mp3' : 'wav'
|
|
147
|
+
const finalBuf = ext === 'wav' ? pcmToWav(pcm) : pcm
|
|
148
|
+
if (output) { fs.writeFileSync(output, finalBuf); console.log(`✅ 已保存到 ${output}(${finalBuf.length} bytes)`) }
|
|
149
|
+
else console.log(`✅ Gemini TTS 成功,时长约 ${(pcm.length / (24000 * 2)).toFixed(1)} 秒`)
|
|
150
|
+
|
|
151
|
+
} else {
|
|
152
|
+
// 标准 OpenAI 兼容(speech-2.6-hd 等)
|
|
153
|
+
const res = await request('/audio/speech', {
|
|
154
|
+
method: 'POST',
|
|
155
|
+
body: JSON.stringify({
|
|
156
|
+
model: ttsModel,
|
|
157
|
+
input: text,
|
|
158
|
+
voice: flags.voice || 'female-shaonv',
|
|
159
|
+
}),
|
|
160
|
+
})
|
|
161
|
+
if (res.status !== 200) {
|
|
162
|
+
const err = await res.json()
|
|
163
|
+
throw new Error(err.error?.message || JSON.stringify(err))
|
|
76
164
|
}
|
|
165
|
+
const buf = Buffer.from(await res.arrayBuffer())
|
|
166
|
+
if (output) { fs.writeFileSync(output, buf); console.log(`✅ 已保存到 ${output}(${buf.length} bytes)`) }
|
|
167
|
+
else console.log(`✅ TTS 成功,${buf.length} bytes`)
|
|
77
168
|
}
|
|
78
169
|
return
|
|
79
170
|
}
|
|
80
171
|
|
|
81
|
-
//
|
|
82
|
-
|
|
172
|
+
// ── MUSIC ─────────────────────────────────────────────────────────────────────
|
|
173
|
+
if (type === 'music') {
|
|
174
|
+
const musicModel = model || 'chirp-v5'
|
|
175
|
+
|
|
176
|
+
if (musicModel === 'chirp-v5') {
|
|
177
|
+
// Suno 异步接口
|
|
178
|
+
const res = await request('/suno/submit/music', {
|
|
179
|
+
method: 'POST',
|
|
180
|
+
headers: { 'Accept': 'application/json' },
|
|
181
|
+
body: JSON.stringify({
|
|
182
|
+
gpt_description_prompt: prompt,
|
|
183
|
+
make_instrumental: true,
|
|
184
|
+
mv: 'chirp-v5',
|
|
185
|
+
notify_hook: '',
|
|
186
|
+
}),
|
|
187
|
+
})
|
|
188
|
+
const submitData = await res.json()
|
|
189
|
+
if (submitData.code !== 'success') throw new Error(submitData.message || JSON.stringify(submitData))
|
|
190
|
+
const taskId = submitData.data
|
|
191
|
+
console.log(`🎵 Suno 任务已提交,task_id: ${taskId},等待生成(约 60-90 秒)...`)
|
|
192
|
+
|
|
193
|
+
// 轮询结果
|
|
194
|
+
for (let i = 0; i < 15; i++) {
|
|
195
|
+
await new Promise(r => setTimeout(r, 10000))
|
|
196
|
+
const poll = await request('/suno/fetch/' + taskId, { method: 'GET' })
|
|
197
|
+
const result = await poll.json()
|
|
198
|
+
if (result.code !== 'success') throw new Error(result.message || JSON.stringify(result))
|
|
199
|
+
const status = result.data?.status
|
|
200
|
+
const progress = result.data?.progress || '0%'
|
|
201
|
+
process.stdout.write(`\r状态: ${status} 进度: ${progress} `)
|
|
202
|
+
if (status === 'SUCCESS') {
|
|
203
|
+
const songs = result.data?.data || []
|
|
204
|
+
process.stdout.write('\n')
|
|
205
|
+
console.log(`✅ 生成成功!共 ${songs.length} 首`)
|
|
206
|
+
songs.forEach((s, i) => {
|
|
207
|
+
console.log(` 歌曲${i+1}: ${s.title} (${Math.floor(s.duration/60)}m${Math.floor(s.duration%60)}s)`)
|
|
208
|
+
console.log(` 音频: ${s.audio_url}`)
|
|
209
|
+
})
|
|
210
|
+
// 如果指定 output,下载第一首
|
|
211
|
+
if (output && songs[0]?.audio_url) await downloadAndSave(songs[0].audio_url, output)
|
|
212
|
+
return
|
|
213
|
+
}
|
|
214
|
+
if (status === 'FAILED') throw new Error('Suno 任务失败: ' + JSON.stringify(result.data))
|
|
215
|
+
}
|
|
216
|
+
throw new Error('Suno 生成超时(150s),请手动查询 task_id: ' + taskId)
|
|
83
217
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
218
|
+
} else {
|
|
219
|
+
// music-2.5(MiniMax)暂不支持(需要 lyrics 参数,建议直接调 /v1/responses)
|
|
220
|
+
throw new Error(`music-2.5 需要通过 /v1/responses 直接调用,请参考文档`)
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// ── OCR ───────────────────────────────────────────────────────────────────────
|
|
225
|
+
if (type === 'ocr') {
|
|
226
|
+
let input = prompt
|
|
227
|
+
// 如果是本地文件路径,读取为 base64
|
|
228
|
+
if (flags.file && fs.existsSync(path.resolve(flags.file))) {
|
|
229
|
+
input = fs.readFileSync(path.resolve(flags.file)).toString('base64')
|
|
230
|
+
}
|
|
231
|
+
const res = await request('/responses', {
|
|
232
|
+
method: 'POST',
|
|
233
|
+
body: JSON.stringify({
|
|
234
|
+
model: model || 'hehe-tywd',
|
|
235
|
+
input,
|
|
236
|
+
parse_mode: 'auto',
|
|
237
|
+
dpi: 144,
|
|
238
|
+
page_start: 0,
|
|
239
|
+
page_count: 1000,
|
|
240
|
+
markdown_details: 1,
|
|
241
|
+
page_details: 0,
|
|
242
|
+
table_flavor: 'html',
|
|
243
|
+
}),
|
|
244
|
+
})
|
|
245
|
+
const data = await res.json()
|
|
246
|
+
if (data.code !== 200) throw new Error(data.message || JSON.stringify(data).slice(0, 200))
|
|
247
|
+
const markdown = data.result?.markdown || ''
|
|
248
|
+
if (output) { fs.writeFileSync(output, markdown); console.log(`✅ OCR 结果已保存到 ${output}`) }
|
|
249
|
+
else console.log(markdown)
|
|
250
|
+
return
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// ── STT ───────────────────────────────────────────────────────────────────────
|
|
254
|
+
if (type === 'stt') {
|
|
255
|
+
if (!flags.file) throw new Error('--file 是必需的(音频文件路径)')
|
|
256
|
+
const audioBase64 = fs.readFileSync(path.resolve(flags.file)).toString('base64')
|
|
257
|
+
const result = await post('/v1/audio/transcriptions', {
|
|
258
|
+
model: 'whisper-1',
|
|
259
|
+
file: audioBase64,
|
|
260
|
+
filename: path.basename(flags.file),
|
|
261
|
+
})
|
|
262
|
+
const text = result.text || JSON.stringify(result, null, 2)
|
|
263
|
+
if (output) { fs.writeFileSync(output, text); console.log(`✅ 已保存到 ${output}`) }
|
|
264
|
+
else console.log(text)
|
|
91
265
|
return
|
|
92
266
|
}
|
|
93
267
|
|
|
94
|
-
|
|
95
|
-
const text = result.choices?.[0]?.message?.content
|
|
96
|
-
|| result.text || result.transcript || JSON.stringify(result, null, 2)
|
|
97
|
-
console.log(text)
|
|
268
|
+
throw new Error(`不支持的类型: ${type},可选: chat | image | tts | stt | music | ocr | video`)
|
|
98
269
|
}
|
|
99
270
|
|
|
100
271
|
module.exports = { pilot }
|
|
Binary file
|