@2api-store/activate 0.5.1 → 0.6.0
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/dist/activate.mjs +2 -0
- package/dist/lib/api.mjs +1 -0
- package/dist/lib/backup.mjs +1 -0
- package/dist/lib/cmds.mjs +1 -0
- package/dist/lib/detect.mjs +1 -0
- package/dist/lib/device.mjs +1 -0
- package/dist/lib/install.mjs +1 -0
- package/dist/lib/platforms.mjs +1 -0
- package/dist/lib/pow.mjs +1 -0
- package/dist/lib/prompt.mjs +1 -0
- package/dist/lib/render.mjs +1 -0
- package/dist/lib/ui.mjs +1 -0
- package/dist/lib/write.mjs +1 -0
- package/dist/package.json +5 -0
- package/package.json +10 -5
- package/activate.mjs +0 -540
- package/lib/api.mjs +0 -184
- package/lib/backup.mjs +0 -109
- package/lib/cmds.mjs +0 -546
- package/lib/detect.mjs +0 -122
- package/lib/install.mjs +0 -110
- package/lib/platforms.mjs +0 -123
- package/lib/prompt.mjs +0 -160
- package/lib/render.mjs +0 -94
- package/lib/ui.mjs +0 -88
- package/lib/write.mjs +0 -57
package/lib/api.mjs
DELETED
|
@@ -1,184 +0,0 @@
|
|
|
1
|
-
import https from 'node:https'
|
|
2
|
-
import http from 'node:http'
|
|
3
|
-
|
|
4
|
-
const USER_AGENT = '@2api-store/activate/0.1.0'
|
|
5
|
-
const DEFAULT_TIMEOUT_MS = 30000
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* 发送 POST /api/v1/cli/activate 请求
|
|
9
|
-
* @param {{credential: string, email?: string, platform?: string, group_id?: number, apiBase: string}} opts
|
|
10
|
-
* @returns {Promise<object>} 解析后的 JSON 响应
|
|
11
|
-
*/
|
|
12
|
-
export function cliActivate({ credential, email, platform, groupId, apiBase }) {
|
|
13
|
-
return new Promise((resolve, reject) => {
|
|
14
|
-
let url
|
|
15
|
-
try {
|
|
16
|
-
url = new URL('/api/v1/cli/activate', apiBase)
|
|
17
|
-
} catch (err) {
|
|
18
|
-
reject(new Error(`无效的 apiBase:${apiBase}(${err.message})`))
|
|
19
|
-
return
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
const lib = url.protocol === 'https:' ? https : http
|
|
23
|
-
const payload = {
|
|
24
|
-
credential,
|
|
25
|
-
...(email ? { email } : {}),
|
|
26
|
-
...(platform ? { platform } : {}),
|
|
27
|
-
...(groupId ? { group_id: groupId } : {}),
|
|
28
|
-
}
|
|
29
|
-
const body = JSON.stringify(payload)
|
|
30
|
-
|
|
31
|
-
const req = lib.request(
|
|
32
|
-
url,
|
|
33
|
-
{
|
|
34
|
-
method: 'POST',
|
|
35
|
-
headers: {
|
|
36
|
-
'Content-Type': 'application/json',
|
|
37
|
-
'Content-Length': Buffer.byteLength(body),
|
|
38
|
-
'User-Agent': USER_AGENT,
|
|
39
|
-
},
|
|
40
|
-
timeout: DEFAULT_TIMEOUT_MS,
|
|
41
|
-
},
|
|
42
|
-
(res) => {
|
|
43
|
-
const chunks = []
|
|
44
|
-
res.on('data', (c) => chunks.push(c))
|
|
45
|
-
res.on('end', () => {
|
|
46
|
-
const text = Buffer.concat(chunks).toString('utf8')
|
|
47
|
-
let json
|
|
48
|
-
try {
|
|
49
|
-
json = text ? JSON.parse(text) : {}
|
|
50
|
-
} catch {
|
|
51
|
-
const err = new Error(`响应非 JSON(HTTP ${res.statusCode}):${text.slice(0, 200)}`)
|
|
52
|
-
err.statusCode = res.statusCode
|
|
53
|
-
err.raw = text
|
|
54
|
-
reject(err)
|
|
55
|
-
return
|
|
56
|
-
}
|
|
57
|
-
if (res.statusCode >= 200 && res.statusCode < 300) {
|
|
58
|
-
resolve(json)
|
|
59
|
-
} else {
|
|
60
|
-
const errObj = json?.error
|
|
61
|
-
const msg = (errObj && typeof errObj === 'object' ? errObj.message : errObj) || json?.message || `HTTP ${res.statusCode}`
|
|
62
|
-
const err = new Error(msg)
|
|
63
|
-
err.statusCode = res.statusCode
|
|
64
|
-
err.body = json
|
|
65
|
-
reject(err)
|
|
66
|
-
}
|
|
67
|
-
})
|
|
68
|
-
},
|
|
69
|
-
)
|
|
70
|
-
|
|
71
|
-
req.on('timeout', () => {
|
|
72
|
-
req.destroy(new Error(`请求超时(${DEFAULT_TIMEOUT_MS / 1000}s)— ${url.href}`))
|
|
73
|
-
})
|
|
74
|
-
req.on('error', reject)
|
|
75
|
-
req.write(body)
|
|
76
|
-
req.end()
|
|
77
|
-
})
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
/**
|
|
81
|
-
* 获取可用分组列表:GET /api/v1/cli/groups
|
|
82
|
-
* 兑换码模式(balance 类型码无 group_id)CLI 选组用。
|
|
83
|
-
* @param {{apiBase: string}} opts
|
|
84
|
-
* @returns {Promise<Array<{id: number, name: string}>>} 分组列表(精简,仅 id+name)
|
|
85
|
-
*/
|
|
86
|
-
export function listGroups({ apiBase }) {
|
|
87
|
-
return new Promise((resolve, reject) => {
|
|
88
|
-
let url
|
|
89
|
-
try {
|
|
90
|
-
url = new URL('/api/v1/cli/groups', apiBase)
|
|
91
|
-
} catch (err) {
|
|
92
|
-
reject(new Error(`无效的 apiBase:${apiBase}(${err.message})`))
|
|
93
|
-
return
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
const lib = url.protocol === 'https:' ? https : http
|
|
97
|
-
const req = lib.request(
|
|
98
|
-
url,
|
|
99
|
-
{
|
|
100
|
-
method: 'GET',
|
|
101
|
-
headers: {
|
|
102
|
-
'User-Agent': USER_AGENT,
|
|
103
|
-
Accept: 'application/json',
|
|
104
|
-
},
|
|
105
|
-
timeout: DEFAULT_TIMEOUT_MS,
|
|
106
|
-
},
|
|
107
|
-
(res) => {
|
|
108
|
-
const chunks = []
|
|
109
|
-
res.on('data', (c) => chunks.push(c))
|
|
110
|
-
res.on('end', () => {
|
|
111
|
-
const text = Buffer.concat(chunks).toString('utf8')
|
|
112
|
-
let json
|
|
113
|
-
try {
|
|
114
|
-
json = text ? JSON.parse(text) : {}
|
|
115
|
-
} catch {
|
|
116
|
-
// 响应非 JSON,退化为空列表(CLI 兜底)
|
|
117
|
-
resolve([])
|
|
118
|
-
return
|
|
119
|
-
}
|
|
120
|
-
if (res.statusCode >= 200 && res.statusCode < 300) {
|
|
121
|
-
// 后端返回 {groups: [{id,name}]};兼容直接返回数组的形式
|
|
122
|
-
const arr = Array.isArray(json) ? json : json?.groups
|
|
123
|
-
resolve(Array.isArray(arr) ? arr : [])
|
|
124
|
-
} else {
|
|
125
|
-
// 失败退化:返回空列表(不阻塞激活主流程,让用户看到「无组可选」提示)
|
|
126
|
-
resolve([])
|
|
127
|
-
}
|
|
128
|
-
})
|
|
129
|
-
},
|
|
130
|
-
)
|
|
131
|
-
|
|
132
|
-
req.on('timeout', () => {
|
|
133
|
-
req.destroy(new Error(`请求超时(${DEFAULT_TIMEOUT_MS / 1000}s)— ${url.href}`))
|
|
134
|
-
})
|
|
135
|
-
req.on('error', (err) => {
|
|
136
|
-
// 网络错误同样退化为空列表(CLI 兜底)
|
|
137
|
-
resolve([])
|
|
138
|
-
})
|
|
139
|
-
req.end()
|
|
140
|
-
})
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
/**
|
|
144
|
-
* 验证 API Key:GET {baseUrl}/v1/models(Bearer api_key),期望 200
|
|
145
|
-
* @param {{apiKey: string, baseUrl: string}} opts
|
|
146
|
-
* @returns {Promise<{ok: boolean, statusCode?: number, error?: string}>}
|
|
147
|
-
*/
|
|
148
|
-
export function verifyApiKey({ apiKey, baseUrl }) {
|
|
149
|
-
return new Promise((resolve) => {
|
|
150
|
-
let url
|
|
151
|
-
try {
|
|
152
|
-
url = new URL('/v1/models', baseUrl)
|
|
153
|
-
} catch {
|
|
154
|
-
resolve({ ok: false, error: `无效的 baseUrl:${baseUrl}` })
|
|
155
|
-
return
|
|
156
|
-
}
|
|
157
|
-
const lib = url.protocol === 'https:' ? https : http
|
|
158
|
-
const req = lib.request(
|
|
159
|
-
url,
|
|
160
|
-
{
|
|
161
|
-
method: 'GET',
|
|
162
|
-
headers: {
|
|
163
|
-
Authorization: `Bearer ${apiKey}`,
|
|
164
|
-
'User-Agent': USER_AGENT,
|
|
165
|
-
},
|
|
166
|
-
timeout: 15000,
|
|
167
|
-
},
|
|
168
|
-
(res) => {
|
|
169
|
-
const chunks = []
|
|
170
|
-
res.on('data', (c) => chunks.push(c))
|
|
171
|
-
res.on('end', () => {
|
|
172
|
-
const ok = res.statusCode >= 200 && res.statusCode < 300
|
|
173
|
-
resolve({ ok, statusCode: res.statusCode })
|
|
174
|
-
})
|
|
175
|
-
},
|
|
176
|
-
)
|
|
177
|
-
req.on('timeout', () => {
|
|
178
|
-
req.destroy()
|
|
179
|
-
resolve({ ok: false, error: '验证请求超时(15s)' })
|
|
180
|
-
})
|
|
181
|
-
req.on('error', (err) => resolve({ ok: false, error: err.message }))
|
|
182
|
-
req.end()
|
|
183
|
-
})
|
|
184
|
-
}
|
package/lib/backup.mjs
DELETED
|
@@ -1,109 +0,0 @@
|
|
|
1
|
-
import fs from 'node:fs'
|
|
2
|
-
import path from 'node:path'
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* 备份目标文件到同目录 .bak.<ISO时间戳>(非覆盖,保留多次)。
|
|
6
|
-
*
|
|
7
|
-
* 行为(spec §5.4):
|
|
8
|
-
* - 文件不存在 → 返回 null(跳过备份)
|
|
9
|
-
* - 文件存在且备份成功 → 返回备份路径
|
|
10
|
-
* - 文件存在但备份失败 → 抛错(调用方应中止写入,避免无备份覆盖)
|
|
11
|
-
*
|
|
12
|
-
* @param {string} targetPath 目标文件绝对路径
|
|
13
|
-
* @returns {string|null} 备份路径,或 null(文件不存在)
|
|
14
|
-
* @throws {Error} 文件存在但 copyFileSync 失败
|
|
15
|
-
*/
|
|
16
|
-
export function backupFile(targetPath) {
|
|
17
|
-
let exists = false
|
|
18
|
-
try {
|
|
19
|
-
exists = fs.existsSync(targetPath)
|
|
20
|
-
} catch {
|
|
21
|
-
// 无法检查存在性(权限等)— 视为不存在,best effort
|
|
22
|
-
return null
|
|
23
|
-
}
|
|
24
|
-
if (!exists) return null
|
|
25
|
-
|
|
26
|
-
// ISO 时间戳,文件名安全化(: 和 . 替换为 -)
|
|
27
|
-
const ts = new Date().toISOString().replace(/[:.]/g, '-')
|
|
28
|
-
const backupPath = `${targetPath}.bak.${ts}`
|
|
29
|
-
|
|
30
|
-
// copyFileSync 失败会抛出 — 调用方知道文件存在且备份失败
|
|
31
|
-
fs.copyFileSync(targetPath, backupPath)
|
|
32
|
-
return backupPath
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
/**
|
|
36
|
-
* 扫描多个目录下所有 .bak.<timestamp> 备份文件,按修改时间倒序排列。
|
|
37
|
-
*
|
|
38
|
-
* 文件名格式约定:<original>.bak.<ISO时间戳>(backupFile 写入)。
|
|
39
|
-
*
|
|
40
|
-
* @param {string[]} dirs 要扫描的目录绝对路径列表
|
|
41
|
-
* @returns {Array<{bakPath: string, origPath: string, mtime: number, dir: string, file: string, origName: string, size: number}>}
|
|
42
|
-
* 按修改时间倒序(最新在前)
|
|
43
|
-
*/
|
|
44
|
-
export function listBackups(dirs) {
|
|
45
|
-
const backups = []
|
|
46
|
-
for (const dir of dirs) {
|
|
47
|
-
if (!dir) continue
|
|
48
|
-
let entries
|
|
49
|
-
try {
|
|
50
|
-
entries = fs.readdirSync(dir, { withFileTypes: true })
|
|
51
|
-
} catch {
|
|
52
|
-
// 目录不存在或权限不足 — 跳过
|
|
53
|
-
continue
|
|
54
|
-
}
|
|
55
|
-
for (const entry of entries) {
|
|
56
|
-
if (!entry.isFile()) continue
|
|
57
|
-
// 匹配 <filename>.bak.<timestamp>(timestamp 含 T Z - 数字)
|
|
58
|
-
const match = entry.name.match(/^(.+)\.bak\.[\dTZ:-]+$/)
|
|
59
|
-
if (!match) continue
|
|
60
|
-
const origName = match[1]
|
|
61
|
-
const bakPath = path.join(dir, entry.name)
|
|
62
|
-
const origPath = path.join(dir, origName)
|
|
63
|
-
try {
|
|
64
|
-
const stat = fs.statSync(bakPath)
|
|
65
|
-
backups.push({
|
|
66
|
-
bakPath,
|
|
67
|
-
origPath,
|
|
68
|
-
mtime: stat.mtimeMs,
|
|
69
|
-
dir,
|
|
70
|
-
file: entry.name,
|
|
71
|
-
origName,
|
|
72
|
-
size: stat.size,
|
|
73
|
-
})
|
|
74
|
-
} catch {
|
|
75
|
-
// stat 失败 — 跳过
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
// 修改时间倒序(最新在前)
|
|
80
|
-
backups.sort((a, b) => b.mtime - a.mtime)
|
|
81
|
-
return backups
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
/**
|
|
85
|
-
* 从备份恢复(复制回原文件,覆盖当前)。
|
|
86
|
-
*
|
|
87
|
-
* 行为:
|
|
88
|
-
* - 备份文件不存在 → 抛错(调用方应预先校验)
|
|
89
|
-
* - 目标父目录不存在 → 自动 mkdir
|
|
90
|
-
* - 恢复后保留备份文件(不删,多次恢复可用)
|
|
91
|
-
*
|
|
92
|
-
* @param {string} bakPath 备份文件绝对路径
|
|
93
|
-
* @param {string} origPath 原文件目标绝对路径
|
|
94
|
-
* @returns {string} origPath(恢复后的路径)
|
|
95
|
-
* @throws {Error} 备份文件不存在或复制失败
|
|
96
|
-
*/
|
|
97
|
-
export function restoreBackup(bakPath, origPath) {
|
|
98
|
-
if (!fs.existsSync(bakPath)) {
|
|
99
|
-
throw new Error(`备份文件不存在:${bakPath}`)
|
|
100
|
-
}
|
|
101
|
-
const dir = path.dirname(origPath)
|
|
102
|
-
try {
|
|
103
|
-
fs.mkdirSync(dir, { recursive: true })
|
|
104
|
-
} catch {
|
|
105
|
-
// 父目录已存在或创建失败忽略(copyFileSync 会再报)
|
|
106
|
-
}
|
|
107
|
-
fs.copyFileSync(bakPath, origPath)
|
|
108
|
-
return origPath
|
|
109
|
-
}
|