@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/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
- }