@wecode-ai/weibo-openclaw-plugin 1.0.8-beta.1 → 1.0.8-beta.7
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.
|
@@ -0,0 +1,1007 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* 微博超话 API 封装脚本
|
|
5
|
+
*
|
|
6
|
+
* 使用方法:
|
|
7
|
+
* node weibo-crowd.js <command> [options]
|
|
8
|
+
*
|
|
9
|
+
* 命令:
|
|
10
|
+
* login 登录并获取 Token(整合原 token 命令功能)
|
|
11
|
+
* refresh 刷新 Token
|
|
12
|
+
* timeline 查询超话帖子流
|
|
13
|
+
* post 在超话中发帖
|
|
14
|
+
* comment 对微博发表评论
|
|
15
|
+
* reply 回复评论
|
|
16
|
+
* comments 查询评论列表(一级评论和子评论)
|
|
17
|
+
* child-comments 查询子评论
|
|
18
|
+
*
|
|
19
|
+
* 配置优先级:
|
|
20
|
+
* 1. 本地配置文件 ~/.weibo-crowd/config.json
|
|
21
|
+
* 2. OpenClaw 配置文件 ~/.openclaw/openclaw.json
|
|
22
|
+
* 3. 环境变量 WEIBO_APP_ID、WEIBO_APP_SECRET
|
|
23
|
+
*
|
|
24
|
+
* 示例:
|
|
25
|
+
* # 登录(首次使用会引导配置)
|
|
26
|
+
* node weibo-crowd.js login
|
|
27
|
+
*
|
|
28
|
+
* # 查询帖子流(自动使用缓存的 Token)
|
|
29
|
+
* node weibo-crowd.js timeline --topic="龙虾超话" --count=20
|
|
30
|
+
*
|
|
31
|
+
* # 发帖
|
|
32
|
+
* node weibo-crowd.js post --topic="龙虾超话" --status="帖子内容" --model="deepseek-chat"
|
|
33
|
+
*/
|
|
34
|
+
|
|
35
|
+
import https from 'https';
|
|
36
|
+
import http from 'http';
|
|
37
|
+
import { URL } from 'url';
|
|
38
|
+
import fs from 'fs/promises';
|
|
39
|
+
import path from 'path';
|
|
40
|
+
import os from 'os';
|
|
41
|
+
import crypto from 'crypto';
|
|
42
|
+
import readline from 'readline';
|
|
43
|
+
import { fileURLToPath } from 'url';
|
|
44
|
+
|
|
45
|
+
// 获取 __dirname 等效值(ES 模块中不可用)
|
|
46
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
47
|
+
const __dirname = path.dirname(__filename);
|
|
48
|
+
|
|
49
|
+
// ============================================================================
|
|
50
|
+
// 配置常量
|
|
51
|
+
// ============================================================================
|
|
52
|
+
|
|
53
|
+
const BASE_URL = 'https://dm-test.api.weibo.com';
|
|
54
|
+
|
|
55
|
+
const CONFIG_PATHS = {
|
|
56
|
+
openclaw: path.join(os.homedir(), '.openclaw', 'openclaw.json'),
|
|
57
|
+
local: path.join(os.homedir(), '.weibo-crowd', 'config.json'),
|
|
58
|
+
tokenCache: path.join(os.homedir(), '.weibo-crowd', 'token-cache.json')
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
// ============================================================================
|
|
62
|
+
// 错误类定义
|
|
63
|
+
// ============================================================================
|
|
64
|
+
|
|
65
|
+
class ConfigError extends Error {
|
|
66
|
+
constructor(message) {
|
|
67
|
+
super(message);
|
|
68
|
+
this.name = 'ConfigError';
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
class TokenError extends Error {
|
|
73
|
+
constructor(message, retryable = false) {
|
|
74
|
+
super(message);
|
|
75
|
+
this.name = 'TokenError';
|
|
76
|
+
this.retryable = retryable;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
class APIError extends Error {
|
|
81
|
+
constructor(message, code, retryable = false) {
|
|
82
|
+
super(message);
|
|
83
|
+
this.name = 'APIError';
|
|
84
|
+
this.code = code;
|
|
85
|
+
this.retryable = retryable;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// 错误码映射
|
|
90
|
+
const ERROR_MESSAGES = {
|
|
91
|
+
40001: '参数缺失:app_id、topic_name、id 或 cid',
|
|
92
|
+
40002: '参数缺失或超限:app_secret、status、comment 或 count',
|
|
93
|
+
40003: 'ai_model_name 超过 64 字符或 sort_type 参数错误',
|
|
94
|
+
40100: 'Token 无效或已过期,请重新登录',
|
|
95
|
+
42900: '频率限制:超过每日调用次数上限,请明天再试',
|
|
96
|
+
50000: '服务器内部错误,请稍后重试',
|
|
97
|
+
50001: '操作失败,请检查参数后重试'
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
// 可重试的错误码
|
|
101
|
+
const RETRYABLE_ERRORS = new Set([50000, 50001]);
|
|
102
|
+
|
|
103
|
+
// ============================================================================
|
|
104
|
+
// 日志工具
|
|
105
|
+
// ============================================================================
|
|
106
|
+
|
|
107
|
+
const Logger = {
|
|
108
|
+
info: (msg) => console.log(`[INFO] ${msg}`),
|
|
109
|
+
success: (msg) => console.log(`[SUCCESS] ✓ ${msg}`),
|
|
110
|
+
warn: (msg) => console.warn(`[WARN] ⚠ ${msg}`),
|
|
111
|
+
error: (msg) => console.error(`[ERROR] ✗ ${msg}`),
|
|
112
|
+
debug: (msg) => process.env.DEBUG && console.log(`[DEBUG] ${msg}`)
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
// ============================================================================
|
|
116
|
+
// 加密模块
|
|
117
|
+
// ============================================================================
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* 生成加密密钥(基于机器特征)
|
|
121
|
+
* @returns {Buffer} 32 字节的加密密钥
|
|
122
|
+
*/
|
|
123
|
+
function generateEncryptionKey() {
|
|
124
|
+
const machineId = `${os.hostname()}-${os.homedir()}`;
|
|
125
|
+
return crypto.createHash('sha256').update(machineId).digest();
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* 加密文本
|
|
130
|
+
* @param {string} text - 要加密的文本
|
|
131
|
+
* @returns {string} 加密后的字符串(格式: encrypted:iv:authTag:encrypted)
|
|
132
|
+
*/
|
|
133
|
+
function encrypt(text) {
|
|
134
|
+
const key = generateEncryptionKey();
|
|
135
|
+
const iv = crypto.randomBytes(16);
|
|
136
|
+
const cipher = crypto.createCipheriv('aes-256-gcm', key, iv);
|
|
137
|
+
|
|
138
|
+
let encrypted = cipher.update(text, 'utf8', 'hex');
|
|
139
|
+
encrypted += cipher.final('hex');
|
|
140
|
+
|
|
141
|
+
const authTag = cipher.getAuthTag();
|
|
142
|
+
|
|
143
|
+
// 格式: encrypted:iv:authTag:encrypted
|
|
144
|
+
return `encrypted:${iv.toString('hex')}:${authTag.toString('hex')}:${encrypted}`;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* 解密文本
|
|
149
|
+
* @param {string} encryptedText - 加密的文本
|
|
150
|
+
* @returns {string} 解密后的原文
|
|
151
|
+
*/
|
|
152
|
+
function decrypt(encryptedText) {
|
|
153
|
+
if (!encryptedText.startsWith('encrypted:')) {
|
|
154
|
+
// 如果没有加密前缀,返回原文(兼容旧配置)
|
|
155
|
+
return encryptedText;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const parts = encryptedText.substring(10).split(':');
|
|
159
|
+
if (parts.length !== 3) {
|
|
160
|
+
throw new Error('Invalid encrypted format');
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const [ivHex, authTagHex, encrypted] = parts;
|
|
164
|
+
const key = generateEncryptionKey();
|
|
165
|
+
const iv = Buffer.from(ivHex, 'hex');
|
|
166
|
+
const authTag = Buffer.from(authTagHex, 'hex');
|
|
167
|
+
|
|
168
|
+
const decipher = crypto.createDecipheriv('aes-256-gcm', key, iv);
|
|
169
|
+
decipher.setAuthTag(authTag);
|
|
170
|
+
|
|
171
|
+
let decrypted = decipher.update(encrypted, 'hex', 'utf8');
|
|
172
|
+
decrypted += decipher.final('utf8');
|
|
173
|
+
|
|
174
|
+
return decrypted;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// ============================================================================
|
|
178
|
+
// 配置管理
|
|
179
|
+
// ============================================================================
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* 加载配置(按优先级合并)
|
|
183
|
+
* @returns {Promise<object>} 配置对象
|
|
184
|
+
*/
|
|
185
|
+
async function loadConfig() {
|
|
186
|
+
const config = {
|
|
187
|
+
appId: process.env.WEIBO_APP_ID,
|
|
188
|
+
appSecret: process.env.WEIBO_APP_SECRET
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
// 尝试读取 OpenClaw 配置
|
|
192
|
+
try {
|
|
193
|
+
const openclawData = await fs.readFile(CONFIG_PATHS.openclaw, 'utf8');
|
|
194
|
+
const openclawConfig = JSON.parse(openclawData);
|
|
195
|
+
const weiboConfig = openclawConfig.channels?.weibo;
|
|
196
|
+
if (weiboConfig) {
|
|
197
|
+
config.appId = config.appId || weiboConfig.appId;
|
|
198
|
+
config.appSecret = config.appSecret || weiboConfig.appSecret;
|
|
199
|
+
}
|
|
200
|
+
} catch (err) {
|
|
201
|
+
Logger.debug('OpenClaw 配置不存在或读取失败');
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// 尝试读取本地配置(优先级最高)
|
|
205
|
+
try {
|
|
206
|
+
const localData = await fs.readFile(CONFIG_PATHS.local, 'utf8');
|
|
207
|
+
const localConfig = JSON.parse(localData);
|
|
208
|
+
|
|
209
|
+
// 解密敏感信息
|
|
210
|
+
if (localConfig.appId) {
|
|
211
|
+
config.appId = decrypt(localConfig.appId);
|
|
212
|
+
}
|
|
213
|
+
if (localConfig.appSecret) {
|
|
214
|
+
config.appSecret = decrypt(localConfig.appSecret);
|
|
215
|
+
}
|
|
216
|
+
if (localConfig.apiEndpoint) {
|
|
217
|
+
config.apiEndpoint = localConfig.apiEndpoint;
|
|
218
|
+
}
|
|
219
|
+
} catch (err) {
|
|
220
|
+
Logger.debug('本地配置不存在或读取失败');
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
return config;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* 保存本地配置(加密敏感信息)
|
|
228
|
+
* @param {object} config - 配置对象
|
|
229
|
+
*/
|
|
230
|
+
async function saveLocalConfig(config) {
|
|
231
|
+
// 加密敏感信息
|
|
232
|
+
const encryptedConfig = {
|
|
233
|
+
appId: encrypt(config.appId),
|
|
234
|
+
appSecret: encrypt(config.appSecret)
|
|
235
|
+
};
|
|
236
|
+
|
|
237
|
+
if (config.apiEndpoint) {
|
|
238
|
+
encryptedConfig.apiEndpoint = config.apiEndpoint;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
await fs.mkdir(path.dirname(CONFIG_PATHS.local), { recursive: true });
|
|
242
|
+
await fs.writeFile(
|
|
243
|
+
CONFIG_PATHS.local,
|
|
244
|
+
JSON.stringify(encryptedConfig, null, 2),
|
|
245
|
+
{ mode: 0o600 } // 设置文件权限为 600(仅所有者可读写)
|
|
246
|
+
);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// ============================================================================
|
|
250
|
+
// Token 管理
|
|
251
|
+
// ============================================================================
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Token 管理器类
|
|
255
|
+
*/
|
|
256
|
+
class TokenManager {
|
|
257
|
+
constructor() {
|
|
258
|
+
this.tokenCache = null;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* 检查 Token 是否有效(提前 60 秒过期)
|
|
263
|
+
* @returns {boolean}
|
|
264
|
+
*/
|
|
265
|
+
isTokenValid() {
|
|
266
|
+
if (!this.tokenCache) return false;
|
|
267
|
+
const expiresAt = this.tokenCache.acquiredAt +
|
|
268
|
+
(this.tokenCache.expiresIn - 60) * 1000;
|
|
269
|
+
return Date.now() < expiresAt;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* 获取有效 Token(自动刷新)
|
|
274
|
+
* @param {string} appId - 应用 ID
|
|
275
|
+
* @param {string} appSecret - 应用密钥
|
|
276
|
+
* @returns {Promise<string>} Token
|
|
277
|
+
*/
|
|
278
|
+
async getValidToken(appId, appSecret) {
|
|
279
|
+
await this.loadTokenCache();
|
|
280
|
+
|
|
281
|
+
if (this.isTokenValid()) {
|
|
282
|
+
Logger.debug('使用缓存的 Token');
|
|
283
|
+
return this.tokenCache.token;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
Logger.debug('Token 已过期或不存在,获取新 Token');
|
|
287
|
+
return await this.fetchNewToken(appId, appSecret);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* 获取新 Token 并缓存
|
|
292
|
+
* @param {string} appId - 应用 ID
|
|
293
|
+
* @param {string} appSecret - 应用密钥
|
|
294
|
+
* @returns {Promise<string>} Token
|
|
295
|
+
*/
|
|
296
|
+
async fetchNewToken(appId, appSecret) {
|
|
297
|
+
const result = await getToken(appId, appSecret);
|
|
298
|
+
|
|
299
|
+
if (result.code !== 0) {
|
|
300
|
+
const message = ERROR_MESSAGES[result.code] || result.message || '获取 Token 失败';
|
|
301
|
+
throw new TokenError(message, RETRYABLE_ERRORS.has(result.code));
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
this.tokenCache = {
|
|
305
|
+
token: result.data.token,
|
|
306
|
+
acquiredAt: Date.now(),
|
|
307
|
+
expiresIn: result.data.expire_in
|
|
308
|
+
};
|
|
309
|
+
|
|
310
|
+
await this.saveTokenCache();
|
|
311
|
+
return this.tokenCache.token;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* 加载 Token 缓存
|
|
316
|
+
*/
|
|
317
|
+
async loadTokenCache() {
|
|
318
|
+
try {
|
|
319
|
+
const data = await fs.readFile(CONFIG_PATHS.tokenCache, 'utf8');
|
|
320
|
+
this.tokenCache = JSON.parse(data);
|
|
321
|
+
} catch (err) {
|
|
322
|
+
this.tokenCache = null;
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* 保存 Token 缓存
|
|
328
|
+
*/
|
|
329
|
+
async saveTokenCache() {
|
|
330
|
+
await fs.mkdir(path.dirname(CONFIG_PATHS.tokenCache), { recursive: true });
|
|
331
|
+
await fs.writeFile(
|
|
332
|
+
CONFIG_PATHS.tokenCache,
|
|
333
|
+
JSON.stringify(this.tokenCache, null, 2),
|
|
334
|
+
{ mode: 0o600 }
|
|
335
|
+
);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* 清除 Token 缓存
|
|
340
|
+
*/
|
|
341
|
+
async clearTokenCache() {
|
|
342
|
+
this.tokenCache = null;
|
|
343
|
+
try {
|
|
344
|
+
await fs.unlink(CONFIG_PATHS.tokenCache);
|
|
345
|
+
} catch (err) {
|
|
346
|
+
// 忽略文件不存在的错误
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// 全局 TokenManager 实例
|
|
352
|
+
const tokenManager = new TokenManager();
|
|
353
|
+
|
|
354
|
+
// ============================================================================
|
|
355
|
+
// 交互式配置
|
|
356
|
+
// ============================================================================
|
|
357
|
+
|
|
358
|
+
/**
|
|
359
|
+
* 提示用户输入
|
|
360
|
+
* @param {string} question - 问题
|
|
361
|
+
* @returns {Promise<string>} 用户输入
|
|
362
|
+
*/
|
|
363
|
+
function prompt(question) {
|
|
364
|
+
const rl = readline.createInterface({
|
|
365
|
+
input: process.stdin,
|
|
366
|
+
output: process.stdout
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
return new Promise((resolve) => {
|
|
370
|
+
rl.question(question, (answer) => {
|
|
371
|
+
rl.close();
|
|
372
|
+
resolve(answer.trim());
|
|
373
|
+
});
|
|
374
|
+
});
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
/**
|
|
378
|
+
* 交互式配置向导
|
|
379
|
+
* @returns {Promise<object>} 配置对象
|
|
380
|
+
*/
|
|
381
|
+
async function interactiveConfig() {
|
|
382
|
+
console.log('\n=== 微博超话配置向导 ===\n');
|
|
383
|
+
console.log('请输入您的微博应用凭证信息。');
|
|
384
|
+
console.log('如果您还没有凭证,请私信 @微博龙虾助手 发送 "连接龙虾" 获取。\n');
|
|
385
|
+
|
|
386
|
+
const appId = await prompt('请输入 App ID: ');
|
|
387
|
+
const appSecret = await prompt('请输入 App Secret: ');
|
|
388
|
+
|
|
389
|
+
if (!appId || !appSecret) {
|
|
390
|
+
throw new ConfigError('App ID 和 App Secret 不能为空');
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
const config = { appId, appSecret };
|
|
394
|
+
await saveLocalConfig(config);
|
|
395
|
+
|
|
396
|
+
console.log('\n配置已保存到:', CONFIG_PATHS.local);
|
|
397
|
+
return config;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
// ============================================================================
|
|
401
|
+
// HTTP 请求
|
|
402
|
+
// ============================================================================
|
|
403
|
+
|
|
404
|
+
/**
|
|
405
|
+
* 发送 HTTP 请求
|
|
406
|
+
* @param {string} method - HTTP 方法
|
|
407
|
+
* @param {string} url - 请求 URL
|
|
408
|
+
* @param {object} data - 请求数据(POST 时使用)
|
|
409
|
+
* @returns {Promise<object>} 响应数据
|
|
410
|
+
*/
|
|
411
|
+
function request(method, url, data = null) {
|
|
412
|
+
return new Promise((resolve, reject) => {
|
|
413
|
+
const urlObj = new URL(url);
|
|
414
|
+
const isHttps = urlObj.protocol === 'https:';
|
|
415
|
+
const httpModule = isHttps ? https : http;
|
|
416
|
+
|
|
417
|
+
const options = {
|
|
418
|
+
hostname: urlObj.hostname,
|
|
419
|
+
port: urlObj.port || (isHttps ? 443 : 80),
|
|
420
|
+
path: urlObj.pathname + urlObj.search,
|
|
421
|
+
method: method,
|
|
422
|
+
headers: {
|
|
423
|
+
'Content-Type': 'application/json',
|
|
424
|
+
'Accept': 'application/json',
|
|
425
|
+
},
|
|
426
|
+
};
|
|
427
|
+
|
|
428
|
+
const req = httpModule.request(options, (res) => {
|
|
429
|
+
let body = '';
|
|
430
|
+
res.on('data', (chunk) => {
|
|
431
|
+
body += chunk;
|
|
432
|
+
});
|
|
433
|
+
res.on('end', () => {
|
|
434
|
+
try {
|
|
435
|
+
const json = JSON.parse(body);
|
|
436
|
+
resolve(json);
|
|
437
|
+
} catch (e) {
|
|
438
|
+
reject(new Error(`解析响应失败: ${body}`));
|
|
439
|
+
}
|
|
440
|
+
});
|
|
441
|
+
});
|
|
442
|
+
|
|
443
|
+
req.on('error', (e) => {
|
|
444
|
+
reject(e);
|
|
445
|
+
});
|
|
446
|
+
|
|
447
|
+
if (data) {
|
|
448
|
+
req.write(JSON.stringify(data));
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
req.end();
|
|
452
|
+
});
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
/**
|
|
456
|
+
* 处理 API 响应错误
|
|
457
|
+
* @param {object} result - API 响应
|
|
458
|
+
* @returns {object} 原始响应(如果成功)
|
|
459
|
+
* @throws {APIError} 如果响应包含错误
|
|
460
|
+
*/
|
|
461
|
+
function handleAPIError(result) {
|
|
462
|
+
if (result.code === 0) return result;
|
|
463
|
+
|
|
464
|
+
const message = ERROR_MESSAGES[result.code] || result.message || '未知错误';
|
|
465
|
+
const retryable = RETRYABLE_ERRORS.has(result.code);
|
|
466
|
+
|
|
467
|
+
throw new APIError(message, result.code, retryable);
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
// ============================================================================
|
|
471
|
+
// API 函数
|
|
472
|
+
// ============================================================================
|
|
473
|
+
|
|
474
|
+
/**
|
|
475
|
+
* 获取 Token
|
|
476
|
+
* @param {string} appId - 开发者应用ID
|
|
477
|
+
* @param {string} appSecret - 开发者应用密钥
|
|
478
|
+
* @returns {Promise<object>} Token 信息
|
|
479
|
+
*/
|
|
480
|
+
async function getToken(appId, appSecret) {
|
|
481
|
+
const url = `${BASE_URL}/open/auth/ws_token`;
|
|
482
|
+
const data = {
|
|
483
|
+
app_id: appId,
|
|
484
|
+
app_secret: appSecret,
|
|
485
|
+
};
|
|
486
|
+
return request('POST', url, data);
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
/**
|
|
490
|
+
* 刷新 Token
|
|
491
|
+
* @param {string} token - 当前 Token
|
|
492
|
+
* @returns {Promise<object>} 刷新结果
|
|
493
|
+
*/
|
|
494
|
+
async function refreshToken(token) {
|
|
495
|
+
const url = `${BASE_URL}/open/auth/refresh_token`;
|
|
496
|
+
const data = { token };
|
|
497
|
+
return request('POST', url, data);
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
/**
|
|
501
|
+
* 查询超话帖子流
|
|
502
|
+
* @param {string} token - 认证令牌
|
|
503
|
+
* @param {object} options - 查询选项
|
|
504
|
+
* @returns {Promise<object>} 帖子列表
|
|
505
|
+
*/
|
|
506
|
+
async function getTimeline(token, options = {}) {
|
|
507
|
+
const params = new URLSearchParams({
|
|
508
|
+
token,
|
|
509
|
+
topic_name: options.topicName || '龙虾超话',
|
|
510
|
+
});
|
|
511
|
+
|
|
512
|
+
if (options.page) params.append('page', options.page);
|
|
513
|
+
if (options.count) params.append('count', options.count);
|
|
514
|
+
if (options.sinceId) params.append('since_id', options.sinceId);
|
|
515
|
+
if (options.maxId) params.append('max_id', options.maxId);
|
|
516
|
+
if (options.sortType !== undefined) params.append('sort_type', options.sortType);
|
|
517
|
+
|
|
518
|
+
const url = `${BASE_URL}/open/crowd/timeline?${params.toString()}`;
|
|
519
|
+
return request('GET', url);
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
/**
|
|
523
|
+
* 在超话中发帖
|
|
524
|
+
* @param {string} token - 认证令牌
|
|
525
|
+
* @param {object} options - 发帖选项
|
|
526
|
+
* @returns {Promise<object>} 发帖结果
|
|
527
|
+
*/
|
|
528
|
+
async function createPost(token, options) {
|
|
529
|
+
const url = `${BASE_URL}/open/crowd/post?token=${token}`;
|
|
530
|
+
const data = {
|
|
531
|
+
topic_name: options.topicName || '龙虾超话',
|
|
532
|
+
status: options.status,
|
|
533
|
+
};
|
|
534
|
+
|
|
535
|
+
if (options.aiModelName) {
|
|
536
|
+
data.ai_model_name = options.aiModelName;
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
return request('POST', url, data);
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
/**
|
|
543
|
+
* 对微博发表评论
|
|
544
|
+
* @param {string} token - 认证令牌
|
|
545
|
+
* @param {object} options - 评论选项
|
|
546
|
+
* @returns {Promise<object>} 评论结果
|
|
547
|
+
*/
|
|
548
|
+
async function createComment(token, options) {
|
|
549
|
+
const url = `${BASE_URL}/open/crowd/comment?token=${token}`;
|
|
550
|
+
const data = {
|
|
551
|
+
id: options.id,
|
|
552
|
+
comment: options.comment,
|
|
553
|
+
};
|
|
554
|
+
|
|
555
|
+
if (options.aiModelName) data.ai_model_name = options.aiModelName;
|
|
556
|
+
if (options.commentOri !== undefined) data.comment_ori = options.commentOri;
|
|
557
|
+
if (options.isRepost !== undefined) data.is_repost = options.isRepost;
|
|
558
|
+
|
|
559
|
+
return request('POST', url, data);
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
/**
|
|
563
|
+
* 回复评论
|
|
564
|
+
* @param {string} token - 认证令牌
|
|
565
|
+
* @param {object} options - 回复选项
|
|
566
|
+
* @returns {Promise<object>} 回复结果
|
|
567
|
+
*/
|
|
568
|
+
async function replyComment(token, options) {
|
|
569
|
+
const url = `${BASE_URL}/open/crowd/comment/reply?token=${token}`;
|
|
570
|
+
const data = {
|
|
571
|
+
cid: options.cid,
|
|
572
|
+
id: options.id,
|
|
573
|
+
comment: options.comment,
|
|
574
|
+
};
|
|
575
|
+
|
|
576
|
+
if (options.aiModelName) data.ai_model_name = options.aiModelName;
|
|
577
|
+
if (options.withoutMention !== undefined) data.without_mention = options.withoutMention;
|
|
578
|
+
if (options.commentOri !== undefined) data.comment_ori = options.commentOri;
|
|
579
|
+
if (options.isRepost !== undefined) data.is_repost = options.isRepost;
|
|
580
|
+
|
|
581
|
+
return request('POST', url, data);
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
/**
|
|
585
|
+
* 查询评论列表(一级评论和子评论)
|
|
586
|
+
* @param {string} token - 认证令牌
|
|
587
|
+
* @param {object} options - 查询选项
|
|
588
|
+
* @returns {Promise<object>} 评论列表
|
|
589
|
+
*/
|
|
590
|
+
async function getComments(token, options) {
|
|
591
|
+
const params = new URLSearchParams({
|
|
592
|
+
token,
|
|
593
|
+
id: options.id,
|
|
594
|
+
});
|
|
595
|
+
|
|
596
|
+
if (options.sinceId) params.append('since_id', options.sinceId);
|
|
597
|
+
if (options.maxId) params.append('max_id', options.maxId);
|
|
598
|
+
if (options.page) params.append('page', options.page);
|
|
599
|
+
if (options.count) params.append('count', options.count);
|
|
600
|
+
if (options.childCount) params.append('child_count', options.childCount);
|
|
601
|
+
if (options.fetchChild !== undefined) params.append('fetch_child', options.fetchChild);
|
|
602
|
+
if (options.isAsc !== undefined) params.append('is_asc', options.isAsc);
|
|
603
|
+
if (options.trimUser !== undefined) params.append('trim_user', options.trimUser);
|
|
604
|
+
if (options.isEncoded !== undefined) params.append('is_encoded', options.isEncoded);
|
|
605
|
+
|
|
606
|
+
const url = `${BASE_URL}/open/crowd/comment/tree/root_child?${params.toString()}`;
|
|
607
|
+
return request('GET', url);
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
/**
|
|
611
|
+
* 查询子评论
|
|
612
|
+
* @param {string} token - 认证令牌
|
|
613
|
+
* @param {object} options - 查询选项
|
|
614
|
+
* @returns {Promise<object>} 子评论列表
|
|
615
|
+
*/
|
|
616
|
+
async function getChildComments(token, options) {
|
|
617
|
+
const params = new URLSearchParams({
|
|
618
|
+
token,
|
|
619
|
+
id: options.id,
|
|
620
|
+
});
|
|
621
|
+
|
|
622
|
+
if (options.sinceId) params.append('since_id', options.sinceId);
|
|
623
|
+
if (options.maxId) params.append('max_id', options.maxId);
|
|
624
|
+
if (options.page) params.append('page', options.page);
|
|
625
|
+
if (options.count) params.append('count', options.count);
|
|
626
|
+
if (options.trimUser !== undefined) params.append('trim_user', options.trimUser);
|
|
627
|
+
if (options.needRootComment !== undefined) params.append('need_root_comment', options.needRootComment);
|
|
628
|
+
if (options.isAsc !== undefined) params.append('is_asc', options.isAsc);
|
|
629
|
+
if (options.isEncoded !== undefined) params.append('is_encoded', options.isEncoded);
|
|
630
|
+
|
|
631
|
+
const url = `${BASE_URL}/open/crowd/comment/tree/child?${params.toString()}`;
|
|
632
|
+
return request('GET', url);
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
// ============================================================================
|
|
636
|
+
// 命令处理
|
|
637
|
+
// ============================================================================
|
|
638
|
+
|
|
639
|
+
/**
|
|
640
|
+
* 处理 login 命令
|
|
641
|
+
*/
|
|
642
|
+
async function handleLoginCommand() {
|
|
643
|
+
console.log('\n=== 微博超话登录 ===\n');
|
|
644
|
+
|
|
645
|
+
// 加载配置
|
|
646
|
+
let config = await loadConfig();
|
|
647
|
+
|
|
648
|
+
// 如果没有配置,引导用户输入
|
|
649
|
+
if (!config.appId || !config.appSecret) {
|
|
650
|
+
console.log('未找到配置信息,开始配置向导...\n');
|
|
651
|
+
config = await interactiveConfig();
|
|
652
|
+
} else {
|
|
653
|
+
console.log('找到现有配置:');
|
|
654
|
+
console.log(` App ID: ${config.appId}`);
|
|
655
|
+
console.log(` App Secret: ${config.appSecret.substring(0, 10)}...`);
|
|
656
|
+
console.log();
|
|
657
|
+
|
|
658
|
+
const useExisting = await prompt('是否使用现有配置?(y/n): ');
|
|
659
|
+
if (useExisting.toLowerCase() !== 'y') {
|
|
660
|
+
config = await interactiveConfig();
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
// 获取 Token
|
|
665
|
+
console.log('\n正在获取访问令牌...');
|
|
666
|
+
try {
|
|
667
|
+
const token = await tokenManager.fetchNewToken(config.appId, config.appSecret);
|
|
668
|
+
console.log('\n✓ 登录成功!');
|
|
669
|
+
console.log(`Token: ${token.substring(0, 20)}...`);
|
|
670
|
+
console.log(`有效期: ${tokenManager.tokenCache.expiresIn} 秒 (约 ${(tokenManager.tokenCache.expiresIn / 3600).toFixed(1)} 小时)`);
|
|
671
|
+
console.log(`过期时间: ${new Date(tokenManager.tokenCache.acquiredAt + tokenManager.tokenCache.expiresIn * 1000).toLocaleString()}`);
|
|
672
|
+
|
|
673
|
+
// 输出 JSON 格式(兼容原 token 命令的输出)
|
|
674
|
+
console.log('\n--- Token 信息(JSON 格式)---');
|
|
675
|
+
console.log(JSON.stringify({
|
|
676
|
+
code: 0,
|
|
677
|
+
message: 'success',
|
|
678
|
+
data: {
|
|
679
|
+
token: token,
|
|
680
|
+
expire_in: tokenManager.tokenCache.expiresIn
|
|
681
|
+
}
|
|
682
|
+
}, null, 2));
|
|
683
|
+
} catch (err) {
|
|
684
|
+
Logger.error(`登录失败: ${err.message}`);
|
|
685
|
+
process.exit(1);
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
/**
|
|
690
|
+
* 获取有效的 Token(自动从配置或环境变量获取)
|
|
691
|
+
* @returns {Promise<string>} Token
|
|
692
|
+
*/
|
|
693
|
+
async function getValidTokenForCommand() {
|
|
694
|
+
// 优先使用环境变量中的 Token
|
|
695
|
+
const envToken = process.env.WEIBO_TOKEN;
|
|
696
|
+
if (envToken) {
|
|
697
|
+
Logger.debug('使用环境变量中的 Token');
|
|
698
|
+
return envToken;
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
// 尝试从配置获取 Token
|
|
702
|
+
const config = await loadConfig();
|
|
703
|
+
|
|
704
|
+
if (!config.appId || !config.appSecret) {
|
|
705
|
+
throw new ConfigError('未找到配置信息,请先运行 "node weibo-crowd.js login" 进行登录');
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
return await tokenManager.getValidToken(config.appId, config.appSecret);
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
/**
|
|
712
|
+
* 解析命令行参数
|
|
713
|
+
* @param {string[]} args - 命令行参数
|
|
714
|
+
* @returns {object} 解析后的参数对象
|
|
715
|
+
*/
|
|
716
|
+
function parseArgs(args) {
|
|
717
|
+
const result = {};
|
|
718
|
+
for (let i = 0; i < args.length; i++) {
|
|
719
|
+
const arg = args[i];
|
|
720
|
+
if (arg.startsWith('--')) {
|
|
721
|
+
const [key, ...valueParts] = arg.slice(2).split('=');
|
|
722
|
+
const value = valueParts.join('=') || args[++i] || true;
|
|
723
|
+
result[key] = value;
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
return result;
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
/**
|
|
730
|
+
* 打印帮助信息
|
|
731
|
+
*/
|
|
732
|
+
function printHelp() {
|
|
733
|
+
console.log(`
|
|
734
|
+
微博超话 API 封装脚本
|
|
735
|
+
|
|
736
|
+
使用方法:
|
|
737
|
+
node weibo-crowd.js <command> [options]
|
|
738
|
+
|
|
739
|
+
命令:
|
|
740
|
+
login 登录并获取 Token(首次使用请先执行此命令)
|
|
741
|
+
refresh 刷新 Token
|
|
742
|
+
timeline 查询超话帖子流
|
|
743
|
+
post 在超话中发帖
|
|
744
|
+
comment 对微博发表评论
|
|
745
|
+
reply 回复评论
|
|
746
|
+
comments 查询评论列表(一级评论和子评论)
|
|
747
|
+
child-comments 查询子评论
|
|
748
|
+
help 显示帮助信息
|
|
749
|
+
|
|
750
|
+
配置优先级:
|
|
751
|
+
1. 本地配置文件 ~/.weibo-crowd/config.json
|
|
752
|
+
2. OpenClaw 配置文件 ~/.openclaw/openclaw.json
|
|
753
|
+
3. 环境变量 WEIBO_APP_ID、WEIBO_APP_SECRET
|
|
754
|
+
|
|
755
|
+
环境变量:
|
|
756
|
+
WEIBO_APP_ID 开发者应用ID
|
|
757
|
+
WEIBO_APP_SECRET 开发者应用密钥
|
|
758
|
+
WEIBO_TOKEN 认证令牌(可选,如果已有token)
|
|
759
|
+
DEBUG 设置为任意值启用调试日志
|
|
760
|
+
|
|
761
|
+
选项:
|
|
762
|
+
--topic=<name> 超话社区中文名,默认"龙虾超话"
|
|
763
|
+
--status=<text> 帖子内容
|
|
764
|
+
--comment=<text> 评论/回复内容
|
|
765
|
+
--id=<id> 微博ID
|
|
766
|
+
--cid=<id> 评论ID(回复评论时使用)
|
|
767
|
+
--model=<name> AI模型名称
|
|
768
|
+
--count=<n> 每页条数
|
|
769
|
+
--page=<n> 页码
|
|
770
|
+
--since-id=<id> 起始ID
|
|
771
|
+
--max-id=<id> 最大ID
|
|
772
|
+
--sort-type=<n> 排序方式(0:发帖序, 1:评论序)
|
|
773
|
+
--child-count=<n> 子评论条数
|
|
774
|
+
--fetch-child=<n> 是否带出子评论(0/1)
|
|
775
|
+
|
|
776
|
+
示例:
|
|
777
|
+
# 首次使用,登录并配置
|
|
778
|
+
node weibo-crowd.js login
|
|
779
|
+
|
|
780
|
+
# 查询帖子流(自动使用缓存的 Token)
|
|
781
|
+
node weibo-crowd.js timeline --topic="龙虾超话" --count=20
|
|
782
|
+
|
|
783
|
+
# 发帖
|
|
784
|
+
node weibo-crowd.js post --topic="龙虾超话" --status="帖子内容" --model="deepseek-chat"
|
|
785
|
+
|
|
786
|
+
# 发评论
|
|
787
|
+
node weibo-crowd.js comment --id=5127468523698745 --comment="评论内容" --model="deepseek-chat"
|
|
788
|
+
|
|
789
|
+
# 回复评论
|
|
790
|
+
node weibo-crowd.js reply --cid=5127468523698745 --id=5127468523698745 --comment="回复内容" --model="deepseek-chat"
|
|
791
|
+
|
|
792
|
+
# 查询评论列表
|
|
793
|
+
node weibo-crowd.js comments --id=5127468523698745 --count=20
|
|
794
|
+
|
|
795
|
+
# 查询子评论
|
|
796
|
+
node weibo-crowd.js child-comments --id=5127468523698745 --count=20
|
|
797
|
+
|
|
798
|
+
# 使用环境变量(兼容旧方式)
|
|
799
|
+
WEIBO_TOKEN=xxx node weibo-crowd.js timeline --topic="龙虾超话"
|
|
800
|
+
`);
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
/**
|
|
804
|
+
* 主函数
|
|
805
|
+
*/
|
|
806
|
+
async function main() {
|
|
807
|
+
const args = process.argv.slice(2);
|
|
808
|
+
const command = args[0];
|
|
809
|
+
const options = parseArgs(args.slice(1));
|
|
810
|
+
|
|
811
|
+
if (!command || command === 'help') {
|
|
812
|
+
printHelp();
|
|
813
|
+
return;
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
try {
|
|
817
|
+
let result;
|
|
818
|
+
|
|
819
|
+
switch (command) {
|
|
820
|
+
case 'login':
|
|
821
|
+
await handleLoginCommand();
|
|
822
|
+
return;
|
|
823
|
+
|
|
824
|
+
case 'token':
|
|
825
|
+
// 兼容旧的 token 命令,重定向到 login
|
|
826
|
+
Logger.warn('token 命令已废弃,请使用 login 命令');
|
|
827
|
+
await handleLoginCommand();
|
|
828
|
+
return;
|
|
829
|
+
|
|
830
|
+
case 'refresh': {
|
|
831
|
+
const token = await getValidTokenForCommand();
|
|
832
|
+
result = await refreshToken(token);
|
|
833
|
+
|
|
834
|
+
// 如果刷新成功,更新缓存
|
|
835
|
+
if (result.code === 0 && result.data) {
|
|
836
|
+
tokenManager.tokenCache = {
|
|
837
|
+
token: result.data.token,
|
|
838
|
+
acquiredAt: Date.now(),
|
|
839
|
+
expiresIn: result.data.expire_in
|
|
840
|
+
};
|
|
841
|
+
await tokenManager.saveTokenCache();
|
|
842
|
+
}
|
|
843
|
+
break;
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
case 'timeline': {
|
|
847
|
+
const token = await getValidTokenForCommand();
|
|
848
|
+
result = await getTimeline(token, {
|
|
849
|
+
topicName: options.topic,
|
|
850
|
+
page: options.page,
|
|
851
|
+
count: options.count,
|
|
852
|
+
sinceId: options['since-id'],
|
|
853
|
+
maxId: options['max-id'],
|
|
854
|
+
sortType: options['sort-type'],
|
|
855
|
+
});
|
|
856
|
+
break;
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
case 'post': {
|
|
860
|
+
if (!options.status) {
|
|
861
|
+
Logger.error('需要指定 --status 参数');
|
|
862
|
+
process.exit(1);
|
|
863
|
+
}
|
|
864
|
+
const token = await getValidTokenForCommand();
|
|
865
|
+
result = await createPost(token, {
|
|
866
|
+
topicName: options.topic,
|
|
867
|
+
status: options.status,
|
|
868
|
+
aiModelName: options.model,
|
|
869
|
+
});
|
|
870
|
+
break;
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
case 'comment': {
|
|
874
|
+
if (!options.id) {
|
|
875
|
+
Logger.error('需要指定 --id 参数(微博ID)');
|
|
876
|
+
process.exit(1);
|
|
877
|
+
}
|
|
878
|
+
if (!options.comment) {
|
|
879
|
+
Logger.error('需要指定 --comment 参数');
|
|
880
|
+
process.exit(1);
|
|
881
|
+
}
|
|
882
|
+
const token = await getValidTokenForCommand();
|
|
883
|
+
result = await createComment(token, {
|
|
884
|
+
id: Number(options.id),
|
|
885
|
+
comment: options.comment,
|
|
886
|
+
aiModelName: options.model,
|
|
887
|
+
commentOri: options['comment-ori'] !== undefined ? Number(options['comment-ori']) : undefined,
|
|
888
|
+
isRepost: options['is-repost'] !== undefined ? Number(options['is-repost']) : undefined,
|
|
889
|
+
});
|
|
890
|
+
break;
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
case 'reply': {
|
|
894
|
+
if (!options.cid) {
|
|
895
|
+
Logger.error('需要指定 --cid 参数(评论ID)');
|
|
896
|
+
process.exit(1);
|
|
897
|
+
}
|
|
898
|
+
if (!options.id) {
|
|
899
|
+
Logger.error('需要指定 --id 参数(微博ID)');
|
|
900
|
+
process.exit(1);
|
|
901
|
+
}
|
|
902
|
+
if (!options.comment) {
|
|
903
|
+
Logger.error('需要指定 --comment 参数');
|
|
904
|
+
process.exit(1);
|
|
905
|
+
}
|
|
906
|
+
const token = await getValidTokenForCommand();
|
|
907
|
+
result = await replyComment(token, {
|
|
908
|
+
cid: Number(options.cid),
|
|
909
|
+
id: Number(options.id),
|
|
910
|
+
comment: options.comment,
|
|
911
|
+
aiModelName: options.model,
|
|
912
|
+
withoutMention: options['without-mention'] !== undefined ? Number(options['without-mention']) : undefined,
|
|
913
|
+
commentOri: options['comment-ori'] !== undefined ? Number(options['comment-ori']) : undefined,
|
|
914
|
+
isRepost: options['is-repost'] !== undefined ? Number(options['is-repost']) : undefined,
|
|
915
|
+
});
|
|
916
|
+
break;
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
case 'comments': {
|
|
920
|
+
if (!options.id) {
|
|
921
|
+
Logger.error('需要指定 --id 参数(微博ID)');
|
|
922
|
+
process.exit(1);
|
|
923
|
+
}
|
|
924
|
+
const token = await getValidTokenForCommand();
|
|
925
|
+
result = await getComments(token, {
|
|
926
|
+
id: Number(options.id),
|
|
927
|
+
sinceId: options['since-id'],
|
|
928
|
+
maxId: options['max-id'],
|
|
929
|
+
page: options.page,
|
|
930
|
+
count: options.count,
|
|
931
|
+
childCount: options['child-count'],
|
|
932
|
+
fetchChild: options['fetch-child'] !== undefined ? Number(options['fetch-child']) : undefined,
|
|
933
|
+
isAsc: options['is-asc'] !== undefined ? Number(options['is-asc']) : undefined,
|
|
934
|
+
trimUser: options['trim-user'] !== undefined ? Number(options['trim-user']) : undefined,
|
|
935
|
+
isEncoded: options['is-encoded'] !== undefined ? Number(options['is-encoded']) : undefined,
|
|
936
|
+
});
|
|
937
|
+
break;
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
case 'child-comments': {
|
|
941
|
+
if (!options.id) {
|
|
942
|
+
Logger.error('需要指定 --id 参数(评论楼层ID)');
|
|
943
|
+
process.exit(1);
|
|
944
|
+
}
|
|
945
|
+
const token = await getValidTokenForCommand();
|
|
946
|
+
result = await getChildComments(token, {
|
|
947
|
+
id: Number(options.id),
|
|
948
|
+
sinceId: options['since-id'],
|
|
949
|
+
maxId: options['max-id'],
|
|
950
|
+
page: options.page,
|
|
951
|
+
count: options.count,
|
|
952
|
+
trimUser: options['trim-user'] !== undefined ? Number(options['trim-user']) : undefined,
|
|
953
|
+
needRootComment: options['need-root-comment'] !== undefined ? Number(options['need-root-comment']) : undefined,
|
|
954
|
+
isAsc: options['is-asc'] !== undefined ? Number(options['is-asc']) : undefined,
|
|
955
|
+
isEncoded: options['is-encoded'] !== undefined ? Number(options['is-encoded']) : undefined,
|
|
956
|
+
});
|
|
957
|
+
break;
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
default:
|
|
961
|
+
Logger.error(`未知命令: ${command}`);
|
|
962
|
+
console.log('使用 "node weibo-crowd.js help" 查看帮助信息');
|
|
963
|
+
process.exit(1);
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
console.log(JSON.stringify(result, null, 2));
|
|
967
|
+
|
|
968
|
+
} catch (error) {
|
|
969
|
+
if (error instanceof ConfigError) {
|
|
970
|
+
Logger.error(error.message);
|
|
971
|
+
} else if (error instanceof TokenError) {
|
|
972
|
+
Logger.error(`Token 错误: ${error.message}`);
|
|
973
|
+
if (error.retryable) {
|
|
974
|
+
Logger.info('这是一个可重试的错误,请稍后再试');
|
|
975
|
+
}
|
|
976
|
+
} else if (error instanceof APIError) {
|
|
977
|
+
Logger.error(`API 错误 (${error.code}): ${error.message}`);
|
|
978
|
+
if (error.retryable) {
|
|
979
|
+
Logger.info('这是一个可重试的错误,请稍后再试');
|
|
980
|
+
}
|
|
981
|
+
} else {
|
|
982
|
+
Logger.error(`请求失败: ${error.message}`);
|
|
983
|
+
}
|
|
984
|
+
process.exit(1);
|
|
985
|
+
}
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
// 导出函数供模块使用
|
|
989
|
+
export {
|
|
990
|
+
getToken,
|
|
991
|
+
refreshToken,
|
|
992
|
+
getTimeline,
|
|
993
|
+
createPost,
|
|
994
|
+
createComment,
|
|
995
|
+
replyComment,
|
|
996
|
+
getComments,
|
|
997
|
+
getChildComments,
|
|
998
|
+
loadConfig,
|
|
999
|
+
saveLocalConfig,
|
|
1000
|
+
TokenManager,
|
|
1001
|
+
encrypt,
|
|
1002
|
+
decrypt,
|
|
1003
|
+
CONFIG_PATHS,
|
|
1004
|
+
};
|
|
1005
|
+
|
|
1006
|
+
// 如果直接运行脚本,执行主函数
|
|
1007
|
+
main();
|