xhscover 2.0.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.
Files changed (3) hide show
  1. package/README.md +106 -0
  2. package/bin/xhscover.js +367 -0
  3. package/package.json +29 -0
package/README.md ADDED
@@ -0,0 +1,106 @@
1
+ # xhscover-cli
2
+
3
+ 小红书封面生成命令行工具。支持 npm 安装或零依赖 bash 脚本。
4
+
5
+ ## 安装
6
+
7
+ ### npm(推荐,跨平台)
8
+
9
+ ```bash
10
+ # 直接使用,无需安装
11
+ npx xhscover setup
12
+
13
+ # 或全局安装
14
+ npm install -g xhscover
15
+ xhscover setup
16
+ ```
17
+
18
+ 需要 Node.js >= 18。
19
+
20
+ ### bash 脚本(macOS/Linux)
21
+
22
+ ```bash
23
+ curl -sL https://raw.githubusercontent.com/xwchris/xhscover-cli/main/xhscover -o /usr/local/bin/xhscover
24
+ chmod +x /usr/local/bin/xhscover
25
+ xhscover setup
26
+ ```
27
+
28
+ ## 快速开始
29
+
30
+ ```bash
31
+ # 首次使用:交互式注册/登录(注册即获 10 个免费积分)
32
+ xhscover setup
33
+
34
+ # 生成封面
35
+ xhscover generate "5个习惯让你越来越自律"
36
+ ```
37
+
38
+ `setup` 会引导你注册或登录,API Key 自动保存到 `~/.xhscover`,后续无需再配置。
39
+
40
+ ## 命令
41
+
42
+ ```bash
43
+ xhscover setup # 首次配置(注册或登录)
44
+ xhscover register # 注册新账号
45
+ xhscover login # 登录已有账号
46
+ xhscover generate <文案> [宽高比] # 生成封面
47
+ xhscover balance # 查询积分余额
48
+ xhscover history [数量] # 获取生成历史
49
+ xhscover help # 显示帮助
50
+ ```
51
+
52
+ ## 配置
53
+
54
+ API Key 保存在 `~/.xhscover`,`setup`/`register`/`login` 命令会自动写入。
55
+
56
+ 也可以通过环境变量配置(优先级高于配置文件):
57
+
58
+ ```bash
59
+ export XHS_COVER_API_KEY="xhs_your_api_key"
60
+ export XHS_COVER_API_URL="https://api.xhscover.cn" # 可选
61
+ ```
62
+
63
+ ## 宽高比
64
+
65
+ | 比例 | 说明 |
66
+ |------|------|
67
+ | `3:4` | 小红书标准竖版(默认) |
68
+ | `9:16` | 超长竖版 |
69
+ | `1:1` | 正方形 |
70
+ | `16:9` | 横版 |
71
+
72
+ ## 示例
73
+
74
+ ```bash
75
+ xhscover generate "5个习惯让你越来越自律"
76
+ xhscover generate "今日份好心情" 1:1
77
+ xhscover "这是封面文案" # 快捷方式
78
+ xhscover balance
79
+ xhscover history 20
80
+ ```
81
+
82
+ ## 在 AI Agent 中使用
83
+
84
+ 本工具可作为 Claude Code Skill 的后端,用户在 Skill 中首次使用时会自动引导配置:
85
+
86
+ ```bash
87
+ # Skill 检测到未配置时自动运行
88
+ xhscover setup
89
+
90
+ # Skill 生成封面
91
+ xhscover generate "$文案" "$宽高比"
92
+ xhscover balance
93
+ ```
94
+
95
+ 配置一次即可持续使用,支持 `npx` 零安装调用。
96
+
97
+ ## 相关链接
98
+
99
+ - 官网:https://xhscover.cn
100
+ - API 文档:https://xhscover.cn/docs
101
+ - MCP Server:https://github.com/xwchris/xhs-cover-mcp
102
+ - Skill:https://github.com/xwchris/xhs-cover-skill
103
+
104
+ ## License
105
+
106
+ MIT
@@ -0,0 +1,367 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { readFileSync, writeFileSync, existsSync, chmodSync } from 'node:fs';
4
+ import { join } from 'node:path';
5
+ import { createInterface } from 'node:readline';
6
+ import { homedir } from 'node:os';
7
+
8
+ // ==================== 配置 ====================
9
+
10
+ const API_URL = process.env.XHS_COVER_API_URL || 'https://api.xhscover.cn';
11
+ const CONFIG_FILE = join(homedir(), '.xhscover');
12
+
13
+ const SUPABASE_URL = 'https://oeacweghghyvmygiknlo.supabase.co';
14
+ const SUPABASE_ANON_KEY = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Im9lYWN3ZWdoZ2h5dm15Z2lrbmxvIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NzI0ODkzNDMsImV4cCI6MjA4ODA2NTM0M30.vM-YVckmdBKNLYlrceKAajLpoMdZWKX3onkWHJzGAyg';
15
+
16
+ // ==================== 工具函数 ====================
17
+
18
+ const colors = {
19
+ red: (s) => `\x1b[0;31m${s}\x1b[0m`,
20
+ green: (s) => `\x1b[0;32m${s}\x1b[0m`,
21
+ yellow: (s) => `\x1b[1;33m${s}\x1b[0m`,
22
+ cyan: (s) => `\x1b[0;36m${s}\x1b[0m`,
23
+ bold: (s) => `\x1b[1m${s}\x1b[0m`,
24
+ };
25
+
26
+ function error(msg) {
27
+ console.error(colors.red(`错误: ${msg}`));
28
+ process.exit(1);
29
+ }
30
+
31
+ function success(msg) {
32
+ console.log(colors.green(msg));
33
+ }
34
+
35
+ function info(msg) {
36
+ console.log(msg);
37
+ }
38
+
39
+ function prompt(msg) {
40
+ process.stderr.write(colors.cyan(msg));
41
+ }
42
+
43
+ // 交互式输入
44
+ function ask(question, { hidden = false } = {}) {
45
+ return new Promise((resolve) => {
46
+ const rl = createInterface({
47
+ input: process.stdin,
48
+ output: hidden ? undefined : process.stdout,
49
+ terminal: hidden,
50
+ });
51
+ rl.question(question, (answer) => {
52
+ rl.close();
53
+ resolve(answer);
54
+ });
55
+ });
56
+ }
57
+
58
+ // ==================== 配置文件 ====================
59
+
60
+ function loadConfig() {
61
+ if (process.env.XHS_COVER_API_KEY) return process.env.XHS_COVER_API_KEY;
62
+ if (!existsSync(CONFIG_FILE)) return null;
63
+ try {
64
+ const content = readFileSync(CONFIG_FILE, 'utf-8');
65
+ const match = content.match(/^XHS_COVER_API_KEY=(.+)$/m);
66
+ return match ? match[1].trim() : null;
67
+ } catch {
68
+ return null;
69
+ }
70
+ }
71
+
72
+ function saveConfig(apiKey) {
73
+ writeFileSync(CONFIG_FILE, `XHS_COVER_API_KEY=${apiKey}\n`, 'utf-8');
74
+ try { chmodSync(CONFIG_FILE, 0o600); } catch { /* Windows */ }
75
+ }
76
+
77
+ function getApiKey() {
78
+ const key = loadConfig();
79
+ if (!key) {
80
+ error(`未配置 API Key\n\n 运行 ${colors.bold('xhscover setup')} 注册账号并自动配置\n 或设置环境变量: export XHS_COVER_API_KEY="xhs_xxxxx"`);
81
+ }
82
+ return key;
83
+ }
84
+
85
+ // ==================== API 请求 ====================
86
+
87
+ async function apiCall(method, path, body) {
88
+ const apiKey = getApiKey();
89
+ const opts = {
90
+ method,
91
+ headers: {
92
+ 'Content-Type': 'application/json',
93
+ 'X-API-Key': apiKey,
94
+ },
95
+ };
96
+ if (body) opts.body = JSON.stringify(body);
97
+
98
+ const resp = await fetch(`${API_URL}${path}`, opts).catch(() => {
99
+ error('请求失败,请检查网络连接');
100
+ });
101
+
102
+ const data = await resp.json();
103
+ if (data.error) error(data.error);
104
+ return data;
105
+ }
106
+
107
+ // ==================== 认证 ====================
108
+
109
+ async function supabaseRequest(endpoint, body) {
110
+ const resp = await fetch(`${SUPABASE_URL}${endpoint}`, {
111
+ method: 'POST',
112
+ headers: {
113
+ 'apikey': SUPABASE_ANON_KEY,
114
+ 'Content-Type': 'application/json',
115
+ },
116
+ body: JSON.stringify(body),
117
+ });
118
+ return resp.json();
119
+ }
120
+
121
+ async function fetchApiKey(jwt) {
122
+ const resp = await fetch(`${SUPABASE_URL}/rest/v1/profiles?select=api_key`, {
123
+ headers: {
124
+ 'apikey': SUPABASE_ANON_KEY,
125
+ 'Authorization': `Bearer ${jwt}`,
126
+ },
127
+ });
128
+ const data = await resp.json();
129
+ return data?.[0]?.api_key || null;
130
+ }
131
+
132
+ async function doRegister(email, password) {
133
+ info('📧 注册中...');
134
+ const resp = await supabaseRequest('/auth/v1/signup', { email, password });
135
+
136
+ if (resp.msg) error(`注册失败: ${resp.msg}`);
137
+
138
+ if (!resp.access_token) {
139
+ // 需要邮箱确认
140
+ success('✅ 注册成功!请查收邮箱确认邮件');
141
+ info('');
142
+ info(` 确认邮箱后运行: ${colors.bold('xhscover login')}`);
143
+ process.exit(0);
144
+ }
145
+
146
+ const apiKey = await fetchApiKey(resp.access_token);
147
+ if (!apiKey) error(`注册成功但未获取到 API Key,请稍后使用 ${colors.bold('xhscover login')} 登录`);
148
+
149
+ saveConfig(apiKey);
150
+ success('✅ 注册成功!已获得 10 个免费积分');
151
+ success(`✅ API Key 已保存到 ${CONFIG_FILE}`);
152
+
153
+ return apiKey;
154
+ }
155
+
156
+ async function doLogin(email, password) {
157
+ info('🔑 登录中...');
158
+ const resp = await supabaseRequest('/auth/v1/token?grant_type=password', { email, password });
159
+
160
+ if (resp.msg) error(`登录失败: ${resp.msg}`);
161
+ if (!resp.access_token) error('登录失败,未获取到访问令牌');
162
+
163
+ const apiKey = await fetchApiKey(resp.access_token);
164
+ if (!apiKey) error('登录成功但未获取到 API Key');
165
+
166
+ saveConfig(apiKey);
167
+ success('✅ 登录成功!');
168
+ success(`✅ API Key 已保存到 ${CONFIG_FILE}`);
169
+
170
+ return apiKey;
171
+ }
172
+
173
+ // ==================== 命令 ====================
174
+
175
+ async function cmdSetup() {
176
+ info(colors.bold('小红书封面生成器 - 账号配置'));
177
+ console.log('');
178
+ console.log(' 1) 注册新账号(获得 10 个免费积分)');
179
+ console.log(' 2) 已有账号,直接登录');
180
+ console.log('');
181
+ const choice = await ask(colors.cyan('请选择 [1/2]: '));
182
+
183
+ if (choice === '1') return cmdRegister();
184
+ if (choice === '2') return cmdLogin();
185
+ error('无效选择');
186
+ }
187
+
188
+ async function cmdRegister() {
189
+ const email = await ask('📧 请输入邮箱: ');
190
+ if (!email) error('邮箱不能为空');
191
+
192
+ const password = await ask('🔒 请输入密码 (至少6位): ', { hidden: true });
193
+ console.log('');
194
+ if (!password || password.length < 6) error('密码不能少于6位');
195
+
196
+ const password2 = await ask('🔒 请确认密码: ', { hidden: true });
197
+ console.log('');
198
+ if (password !== password2) error('两次密码不一致');
199
+
200
+ const apiKey = await doRegister(email, password);
201
+ if (apiKey) {
202
+ console.log('');
203
+ await cmdBalance();
204
+ }
205
+ }
206
+
207
+ async function cmdLogin() {
208
+ const email = await ask('📧 请输入邮箱: ');
209
+ if (!email) error('邮箱不能为空');
210
+
211
+ const password = await ask('🔒 请输入密码: ', { hidden: true });
212
+ console.log('');
213
+ if (!password) error('密码不能为空');
214
+
215
+ const apiKey = await doLogin(email, password);
216
+ if (apiKey) {
217
+ console.log('');
218
+ await cmdBalance();
219
+ }
220
+ }
221
+
222
+ async function cmdGenerate(text, ratio = '3:4') {
223
+ if (!text) error('请提供封面文案\n 用法: xhscover generate <文案> [宽高比]');
224
+ getApiKey(); // 确保 API Key 存在
225
+
226
+ info(`🎨 生成封面: ${colors.yellow(text)}`);
227
+ info(` 宽高比: ${ratio}`);
228
+ info(' 提交中...');
229
+
230
+ const resp = await apiCall('POST', '/api/v1/generate', { text, aspectRatio: ratio });
231
+ const taskId = resp.taskId;
232
+ if (!taskId) error('创建任务失败,未返回 taskId');
233
+
234
+ // 轮询等待结果
235
+ const timeout = 60;
236
+ const interval = 2000;
237
+ const start = Date.now();
238
+
239
+ while (Date.now() - start < timeout * 1000) {
240
+ await new Promise((r) => setTimeout(r, interval));
241
+ const elapsed = Math.round((Date.now() - start) / 1000);
242
+
243
+ const poll = await apiCall('GET', `/api/v1/generate/${taskId}`);
244
+
245
+ if (poll.status === 'completed') {
246
+ console.log('');
247
+ success('✅ 封面生成完成!');
248
+ info(` 图片地址: ${colors.cyan(poll.resultUrl)}`);
249
+ if (poll.creditCost) info(` 消耗积分: ${poll.creditCost}`);
250
+ return;
251
+ }
252
+ if (poll.status === 'failed') {
253
+ error(`生成失败: ${poll.errorMessage || '未知错误'}`);
254
+ }
255
+ info(` 等待中... (${elapsed}s)`);
256
+ }
257
+
258
+ error(`生成超时(${timeout}s),任务 ID: ${taskId}`);
259
+ }
260
+
261
+ async function cmdBalance() {
262
+ const resp = await apiCall('GET', '/api/v1/credits');
263
+ success('💰 积分余额');
264
+ console.log(` 当前余额: ${resp.balance}`);
265
+ console.log(` 已购买: ${resp.totalPurchased}`);
266
+ console.log(` 已使用: ${resp.totalUsed}`);
267
+ }
268
+
269
+ async function cmdHistory(limit = 10) {
270
+ info(`📋 获取最近 ${limit} 条记录...`);
271
+ const resp = await apiCall('GET', `/api/v1/history?page=1&limit=${limit}`);
272
+ const tasks = resp.tasks || [];
273
+ console.log('');
274
+ info(`共 ${resp.total || tasks.length} 条记录,显示最近 ${limit} 条:`);
275
+ console.log('');
276
+
277
+ if (tasks.length === 0) {
278
+ info(' 暂无记录');
279
+ return;
280
+ }
281
+
282
+ for (const task of tasks) {
283
+ const status = task.status === 'completed' ? colors.green('✓') : task.status === 'failed' ? colors.red('✗') : '…';
284
+ const date = task.created_at ? new Date(task.created_at).toLocaleString('zh-CN') : '';
285
+ const prompt = (task.prompt || '').substring(0, 30);
286
+ console.log(` ${status} ${prompt.padEnd(32)} ${date}`);
287
+ }
288
+ }
289
+
290
+ function showHelp() {
291
+ console.log(`小红书封面生成器 v2.0.0
292
+
293
+ 用法:
294
+ xhscover setup 首次配置(注册或登录)
295
+ xhscover register 注册新账号
296
+ xhscover login 登录已有账号
297
+ xhscover generate <文案> [宽高比] 生成封面(含轮询等待)
298
+ xhscover balance 查询积分余额
299
+ xhscover history [数量] 获取生成历史
300
+ xhscover help 显示帮助
301
+
302
+ 首次使用:
303
+ xhscover setup 交互式引导,注册即获 10 个免费积分
304
+
305
+ 配置:
306
+ API Key 保存在 ~/.xhscover,也可通过环境变量设置:
307
+ export XHS_COVER_API_KEY="xhs_xxxxx"
308
+
309
+ 环境变量:
310
+ XHS_COVER_API_URL API 地址 (默认: https://api.xhscover.cn)
311
+ XHS_COVER_API_KEY API 密钥(优先级高于配置文件)
312
+
313
+ 宽高比:
314
+ 3:4 小红书标准竖版(默认)
315
+ 9:16 超长竖版
316
+ 1:1 正方形
317
+ 16:9 横版
318
+
319
+ 示例:
320
+ xhscover setup
321
+ xhscover generate "5个习惯让你越来越自律"
322
+ xhscover generate "今日份好心情" 1:1
323
+ xhscover balance
324
+ xhscover history 20
325
+
326
+ 主页: https://xhscover.cn
327
+ 使用: npx xhscover setup`);
328
+ }
329
+
330
+ // ==================== 主入口 ====================
331
+
332
+ const args = process.argv.slice(2);
333
+ const command = args[0];
334
+
335
+ switch (command) {
336
+ case 'setup':
337
+ await cmdSetup();
338
+ break;
339
+ case 'register':
340
+ await cmdRegister();
341
+ break;
342
+ case 'login':
343
+ await cmdLogin();
344
+ break;
345
+ case 'generate':
346
+ await cmdGenerate(args[1], args[2]);
347
+ break;
348
+ case 'balance':
349
+ case 'credits':
350
+ await cmdBalance();
351
+ break;
352
+ case 'history':
353
+ await cmdHistory(parseInt(args[1], 10) || 10);
354
+ break;
355
+ case 'help':
356
+ case '--help':
357
+ case '-h':
358
+ showHelp();
359
+ break;
360
+ default:
361
+ if (command) {
362
+ await cmdGenerate(command, args[1]);
363
+ } else {
364
+ showHelp();
365
+ }
366
+ break;
367
+ }
package/package.json ADDED
@@ -0,0 +1,29 @@
1
+ {
2
+ "name": "xhscover",
3
+ "version": "2.0.0",
4
+ "description": "小红书封面生成 CLI - 注册/登录/生成一条龙",
5
+ "type": "module",
6
+ "bin": {
7
+ "xhscover": "./bin/xhscover.js"
8
+ },
9
+ "files": [
10
+ "bin"
11
+ ],
12
+ "engines": {
13
+ "node": ">=18.0.0"
14
+ },
15
+ "keywords": [
16
+ "xiaohongshu",
17
+ "xhs",
18
+ "cover",
19
+ "image-generator",
20
+ "cli"
21
+ ],
22
+ "author": "xwchris",
23
+ "license": "MIT",
24
+ "repository": {
25
+ "type": "git",
26
+ "url": "git+https://github.com/xwchris/xhscover-cli.git"
27
+ },
28
+ "homepage": "https://xhscover.cn"
29
+ }