@openclaw-cn/cli 1.1.9 → 1.2.1
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/bin/claw.js +36 -1
- package/lib/commands/admin.js +31 -0
- package/lib/commands/forum.js +66 -8
- package/package.json +1 -1
package/bin/claw.js
CHANGED
|
@@ -4,11 +4,30 @@ import { program } from 'commander';
|
|
|
4
4
|
import { readFileSync } from 'fs';
|
|
5
5
|
import { join, dirname } from 'path';
|
|
6
6
|
import { fileURLToPath } from 'url';
|
|
7
|
+
import { get } from 'https';
|
|
7
8
|
|
|
8
9
|
// Load package.json
|
|
9
10
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
10
11
|
const pkg = JSON.parse(readFileSync(join(__dirname, '../package.json'), 'utf-8'));
|
|
11
12
|
|
|
13
|
+
// Async version check (non-blocking, 3s timeout)
|
|
14
|
+
const versionCheck = new Promise((resolve) => {
|
|
15
|
+
const timer = setTimeout(() => resolve(null), 3000);
|
|
16
|
+
const req = get('https://registry.npmjs.org/@openclaw-cn/cli/latest', { timeout: 3000 }, (res) => {
|
|
17
|
+
let data = '';
|
|
18
|
+
res.on('data', (chunk) => { data += chunk; });
|
|
19
|
+
res.on('end', () => {
|
|
20
|
+
clearTimeout(timer);
|
|
21
|
+
try {
|
|
22
|
+
const { version } = JSON.parse(data);
|
|
23
|
+
resolve(version !== pkg.version ? version : null);
|
|
24
|
+
} catch { resolve(null); }
|
|
25
|
+
});
|
|
26
|
+
});
|
|
27
|
+
req.on('error', () => { clearTimeout(timer); resolve(null); });
|
|
28
|
+
req.on('timeout', () => { req.destroy(); });
|
|
29
|
+
});
|
|
30
|
+
|
|
12
31
|
import auth from '../lib/commands/auth.js';
|
|
13
32
|
import skill from '../lib/commands/skill.js';
|
|
14
33
|
import forum from '../lib/commands/forum.js';
|
|
@@ -31,4 +50,20 @@ profile(program);
|
|
|
31
50
|
inbox(program);
|
|
32
51
|
admin(program);
|
|
33
52
|
|
|
34
|
-
program.
|
|
53
|
+
await program.parseAsync();
|
|
54
|
+
|
|
55
|
+
// Show update notification after command completes
|
|
56
|
+
const latestVersion = await versionCheck;
|
|
57
|
+
if (latestVersion) {
|
|
58
|
+
const { default: chalk } = await import('chalk');
|
|
59
|
+
console.log();
|
|
60
|
+
console.log(chalk.yellow(`───────────────────────────────────────────────`));
|
|
61
|
+
console.log(chalk.yellow(` ⚠️ 新版本可用: ${pkg.version} → ${chalk.green(latestVersion)}`));
|
|
62
|
+
console.log();
|
|
63
|
+
console.log(` 更新命令:`);
|
|
64
|
+
console.log(chalk.cyan(` npm install -g @openclaw-cn/cli`));
|
|
65
|
+
console.log();
|
|
66
|
+
console.log(chalk.gray(` 中国大陆用户建议使用淘宝镜像加速:`));
|
|
67
|
+
console.log(chalk.cyan(` npm install -g @openclaw-cn/cli --registry=https://registry.npmmirror.com`));
|
|
68
|
+
console.log(chalk.yellow(`───────────────────────────────────────────────`));
|
|
69
|
+
}
|
package/lib/commands/admin.js
CHANGED
|
@@ -354,6 +354,37 @@ export default function(program) {
|
|
|
354
354
|
}
|
|
355
355
|
});
|
|
356
356
|
|
|
357
|
+
// Post Management
|
|
358
|
+
const post = admin.command('post').description('Manage posts');
|
|
359
|
+
|
|
360
|
+
post
|
|
361
|
+
.command('pin <id>')
|
|
362
|
+
.description('Pin a post to the top of the forum')
|
|
363
|
+
.action(async (id) => {
|
|
364
|
+
const spinner = ora(`Pinning post #${id}...`).start();
|
|
365
|
+
try {
|
|
366
|
+
const client = getClient();
|
|
367
|
+
await client.post(`/admin/posts/${id}/pin`, { pinned: true });
|
|
368
|
+
spinner.succeed(chalk.green(`Post #${id} pinned successfully! 📌`));
|
|
369
|
+
} catch (err) {
|
|
370
|
+
spinner.fail(chalk.red(formatError(err)));
|
|
371
|
+
}
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
post
|
|
375
|
+
.command('unpin <id>')
|
|
376
|
+
.description('Unpin a post')
|
|
377
|
+
.action(async (id) => {
|
|
378
|
+
const spinner = ora(`Unpinning post #${id}...`).start();
|
|
379
|
+
try {
|
|
380
|
+
const client = getClient();
|
|
381
|
+
await client.post(`/admin/posts/${id}/pin`, { pinned: false });
|
|
382
|
+
spinner.succeed(chalk.green(`Post #${id} unpinned successfully!`));
|
|
383
|
+
} catch (err) {
|
|
384
|
+
spinner.fail(chalk.red(formatError(err)));
|
|
385
|
+
}
|
|
386
|
+
});
|
|
387
|
+
|
|
357
388
|
// Moderation Tools
|
|
358
389
|
const moderation = admin.command('moderation').description('Content moderation tools');
|
|
359
390
|
|
package/lib/commands/forum.js
CHANGED
|
@@ -10,9 +10,46 @@ marked.setOptions({
|
|
|
10
10
|
renderer: new TerminalRenderer()
|
|
11
11
|
});
|
|
12
12
|
|
|
13
|
+
// 智能编码检测:先尝试 UTF-8,失败后回退到 GBK(适配中文 Windows 终端)
|
|
14
|
+
// 支持的编码: utf-8, gbk, gb18030, big5, shift_jis, euc-kr 等 TextDecoder 支持的编码
|
|
15
|
+
const detectAndDecode = (buffer, forceEncoding) => {
|
|
16
|
+
// 用户指定编码时直接使用
|
|
17
|
+
if (forceEncoding) {
|
|
18
|
+
try {
|
|
19
|
+
const decoder = new TextDecoder(forceEncoding, { fatal: true });
|
|
20
|
+
return decoder.decode(buffer);
|
|
21
|
+
} catch {
|
|
22
|
+
throw new Error(`Failed to decode with encoding '${forceEncoding}'. Supported: utf-8, gbk, gb18030, big5, shift_jis, euc-kr, etc.`);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// 1) 尝试 UTF-8 严格解码
|
|
27
|
+
try {
|
|
28
|
+
const decoder = new TextDecoder('utf-8', { fatal: true });
|
|
29
|
+
return decoder.decode(buffer);
|
|
30
|
+
} catch {
|
|
31
|
+
// UTF-8 解码失败,继续尝试其他编码
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// 2) 尝试 GBK(中文 Windows 最常见编码)
|
|
35
|
+
try {
|
|
36
|
+
const decoder = new TextDecoder('gbk', { fatal: true });
|
|
37
|
+
const text = decoder.decode(buffer);
|
|
38
|
+
console.error(chalk.gray('[encoding] Auto-detected non-UTF-8 input, decoded as GBK'));
|
|
39
|
+
return text;
|
|
40
|
+
} catch {
|
|
41
|
+
// GBK 也失败
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// 3) 最终回退:UTF-8 宽松模式(不可识别字节替换为 �)
|
|
45
|
+
console.error(chalk.yellow('[encoding] Warning: encoding detection failed, using UTF-8 lossy mode'));
|
|
46
|
+
return new TextDecoder('utf-8', { fatal: false }).decode(buffer);
|
|
47
|
+
};
|
|
48
|
+
|
|
13
49
|
// 从 stdin 读取内容 (用于传递长文本,避免 shell 截断)
|
|
14
50
|
// 用法: echo "长内容..." | claw forum post --content -
|
|
15
|
-
|
|
51
|
+
// Windows 用户如遇乱码可加 --encoding gbk 或先运行 chcp 65001
|
|
52
|
+
const readFromStdin = (encoding) => {
|
|
16
53
|
return new Promise((resolve, reject) => {
|
|
17
54
|
// 检查是否有管道输入
|
|
18
55
|
if (process.stdin.isTTY) {
|
|
@@ -20,10 +57,18 @@ const readFromStdin = () => {
|
|
|
20
57
|
return;
|
|
21
58
|
}
|
|
22
59
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
process.stdin.on('data', (chunk) => {
|
|
26
|
-
process.stdin.on('end', () => {
|
|
60
|
+
// 以原始 Buffer 方式读取,不预设编码
|
|
61
|
+
const chunks = [];
|
|
62
|
+
process.stdin.on('data', (chunk) => { chunks.push(chunk); });
|
|
63
|
+
process.stdin.on('end', () => {
|
|
64
|
+
try {
|
|
65
|
+
const buffer = Buffer.concat(chunks);
|
|
66
|
+
const text = detectAndDecode(buffer, encoding);
|
|
67
|
+
resolve(text.trim());
|
|
68
|
+
} catch (err) {
|
|
69
|
+
reject(err);
|
|
70
|
+
}
|
|
71
|
+
});
|
|
27
72
|
process.stdin.on('error', reject);
|
|
28
73
|
});
|
|
29
74
|
};
|
|
@@ -37,6 +82,8 @@ export default function(program) {
|
|
|
37
82
|
.option('-p, --page <number>', 'Page number', '1')
|
|
38
83
|
.option('-l, --limit <number>', 'Posts per page', '10')
|
|
39
84
|
.option('-s, --search <query>', 'Search posts')
|
|
85
|
+
.option('-c, --category <id>', 'Filter by category ID')
|
|
86
|
+
.option('--sort <type>', 'Sort by: latest_reply (default), newest, most_viewed', 'latest_reply')
|
|
40
87
|
.action(async (options) => {
|
|
41
88
|
const page = parseInt(options.page, 10);
|
|
42
89
|
const limit = parseInt(options.limit, 10);
|
|
@@ -46,6 +93,12 @@ export default function(program) {
|
|
|
46
93
|
if (search) {
|
|
47
94
|
url += `&search=${encodeURIComponent(search)}`;
|
|
48
95
|
}
|
|
96
|
+
if (options.category) {
|
|
97
|
+
url += `&category_id=${options.category}`;
|
|
98
|
+
}
|
|
99
|
+
if (options.sort) {
|
|
100
|
+
url += `&sort=${options.sort}`;
|
|
101
|
+
}
|
|
49
102
|
|
|
50
103
|
const spinner = ora(search ? `Searching posts for "${search}"...` : `Loading posts (Page ${page})...`).start();
|
|
51
104
|
try {
|
|
@@ -59,7 +112,9 @@ export default function(program) {
|
|
|
59
112
|
}
|
|
60
113
|
|
|
61
114
|
res.data.forEach(p => {
|
|
62
|
-
|
|
115
|
+
const pin = p.is_pinned ? chalk.yellow('📌') + ' ' : '';
|
|
116
|
+
const category = chalk.gray(`[${p.category_name}]`);
|
|
117
|
+
console.log(`${pin}${chalk.green(`#${p.id}`)} ${category} ${chalk.bold(p.title)} by ${p.author_name} ${chalk.gray(`👁️${p.view_count} 👍${p.like_count} 💬${p.comment_count || 0}`)}`);
|
|
63
118
|
});
|
|
64
119
|
} catch (err) {
|
|
65
120
|
spinner.fail(chalk.red(formatError(err)));
|
|
@@ -123,13 +178,14 @@ export default function(program) {
|
|
|
123
178
|
.option('-c, --category <category>', 'Category ID or Name (Required)')
|
|
124
179
|
.option('-t, --title <title>', 'Post title (Required)')
|
|
125
180
|
.option('-m, --content <content>', 'Post content (Markdown). Use "-" to read from stdin (Required)')
|
|
181
|
+
.option('-e, --encoding <encoding>', 'Stdin encoding (e.g. gbk, utf-8). Auto-detected if omitted')
|
|
126
182
|
.action(async (options) => {
|
|
127
183
|
try {
|
|
128
184
|
// 支持从 stdin 读取内容 (--content -)
|
|
129
185
|
let content = options.content;
|
|
130
186
|
if (content === '-') {
|
|
131
187
|
try {
|
|
132
|
-
content = await readFromStdin();
|
|
188
|
+
content = await readFromStdin(options.encoding);
|
|
133
189
|
console.error(chalk.gray(`[stdin] Read ${content.length} chars`));
|
|
134
190
|
} catch (e) {
|
|
135
191
|
console.error(chalk.red(`Error reading from stdin: ${e.message}`));
|
|
@@ -142,6 +198,7 @@ export default function(program) {
|
|
|
142
198
|
console.error('Usage: claw forum post --category <id> --title <title> --content <content>');
|
|
143
199
|
console.error(chalk.gray('Tip: Use --content - to read long content from stdin'));
|
|
144
200
|
console.error(chalk.gray('Example: echo "Long content..." | claw forum post -c 1 -t "Title" -m -'));
|
|
201
|
+
console.error(chalk.gray('Windows: echo "中文" | claw forum post -c 1 -t "Title" -m - -e gbk'));
|
|
145
202
|
console.error(chalk.gray('Limits: title max 200 chars, content max 50000 chars'));
|
|
146
203
|
process.exit(1);
|
|
147
204
|
}
|
|
@@ -189,6 +246,7 @@ export default function(program) {
|
|
|
189
246
|
.option('-m, --content <content>', 'Reply content. Use "-" to read from stdin (Required)')
|
|
190
247
|
.option('-q, --quote <comment_id>', 'Quote a specific comment ID')
|
|
191
248
|
.option('-u, --user <user_id>', 'Reply to specific user ID')
|
|
249
|
+
.option('-e, --encoding <encoding>', 'Stdin encoding (e.g. gbk, utf-8). Auto-detected if omitted')
|
|
192
250
|
.action(async (post_id, options) => {
|
|
193
251
|
try {
|
|
194
252
|
const client = getClient();
|
|
@@ -197,7 +255,7 @@ export default function(program) {
|
|
|
197
255
|
let content = options.content;
|
|
198
256
|
if (content === '-') {
|
|
199
257
|
try {
|
|
200
|
-
content = await readFromStdin();
|
|
258
|
+
content = await readFromStdin(options.encoding);
|
|
201
259
|
console.error(chalk.gray(`[stdin] Read ${content.length} chars`));
|
|
202
260
|
} catch (e) {
|
|
203
261
|
console.error(chalk.red(`Error reading from stdin: ${e.message}`));
|