dsh-foxy-weibo 0.2.0 → 0.2.2
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/README.md +4 -4
- package/cordis.patch.yml +1 -1
- package/lib/client.js +44 -8
- package/lib/index.js +74 -11
- package/lib/settings.js +5 -3
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -6,7 +6,7 @@ Foxy 的微博子插件:按 foxy3 预设关注微博账号、阅读博文、
|
|
|
6
6
|
|
|
7
7
|
## 功能
|
|
8
8
|
|
|
9
|
-
-
|
|
9
|
+
- **博文评论回复**:检查指定的监控账号 UID,读取其新博文下的评论,只回复未处理的他人评论;登录账号是机器人自己,监控账号通常是主人账号;
|
|
10
10
|
- 可设置每日检查时间(`HH:MM`,最多 6 个),例如 `08:30, 20:00`;
|
|
11
11
|
- 回复前按 foxy3 预设判断 `skip / comment`,不设最少字数,最多 80 字;
|
|
12
12
|
- 旧模式保留:关注一个或多个微博 UID,定时读取时间线并评论;
|
|
@@ -47,7 +47,7 @@ Cookie 只保存在本机 `~/.dsh/foxy-weibo/settings.json`,不会回传给前
|
|
|
47
47
|
|
|
48
48
|
### 自己的 UID / 关注 UID
|
|
49
49
|
|
|
50
|
-
|
|
50
|
+
`监控账号 UID` 是要检查博文评论的目标账号(通常是主人账号);`当前账号` 是扫码登录的机器人账号,两者可以不同。
|
|
51
51
|
|
|
52
52
|
`关注 UID` 是旧模式使用,多个 UID 用逗号分隔,最多 20 个。UID 是微博用户主页 URL 里的数字 ID,例如 `https://weibo.com/u/1234567890`。
|
|
53
53
|
|
|
@@ -63,10 +63,10 @@ Cookie 只保存在本机 `~/.dsh/foxy-weibo/settings.json`,不会回传给前
|
|
|
63
63
|
|
|
64
64
|
## 建议顺序
|
|
65
65
|
|
|
66
|
-
1.
|
|
66
|
+
1. 扫码登录,填监控账号 UID,`enabled: true`、`replyMode: off`;
|
|
67
67
|
2. 设置每日检查时间,或先点「立即运行」,看日志和「调试」里的模型决策;
|
|
68
68
|
3. 确认判断正常后,再开启 `replyMode: auto`,每日上限先设为 1;
|
|
69
|
-
4.
|
|
69
|
+
4. 「立即检查」只判断不发布,用来预览回复;「立即回复」会真实发布一条,适合测试;
|
|
70
70
|
5. 微博评论有账号风控,不要高频、不要对陌生人全自动回复。
|
|
71
71
|
|
|
72
72
|
## 记忆事件
|
package/cordis.patch.yml
CHANGED
package/lib/client.js
CHANGED
|
@@ -218,8 +218,8 @@ window.__ModuleLoader__.load({
|
|
|
218
218
|
? '未开始:自动浏览已关闭'
|
|
219
219
|
: result.skipped === 'busy'
|
|
220
220
|
? '正在运行,请稍候'
|
|
221
|
-
: result.skipped === 'no-
|
|
222
|
-
? '
|
|
221
|
+
: result.skipped === 'no-target'
|
|
222
|
+
? '未开始:请先填写监控账号 UID'
|
|
223
223
|
: `运行完成:检查 ${result.checked || 0} 条,回复 ${result.replied || 0} 条` +
|
|
224
224
|
(result.previewed ? `,预览 ${result.previewed} 条` : '') +
|
|
225
225
|
`,跳过 ${result.skipped || 0} 条`,
|
|
@@ -227,6 +227,40 @@ window.__ModuleLoader__.load({
|
|
|
227
227
|
} catch (e) { setError(e.message) } finally { setBusy('') }
|
|
228
228
|
}
|
|
229
229
|
|
|
230
|
+
const checkNow = async () => {
|
|
231
|
+
setBusy('check'); setError(''); setNotice('')
|
|
232
|
+
try {
|
|
233
|
+
const data = await request('/check-now', { method: 'POST', body: '{}' })
|
|
234
|
+
setStatus(data.status)
|
|
235
|
+
const result = data.result || {}
|
|
236
|
+
setNotice(
|
|
237
|
+
result.skipped === 'no-target'
|
|
238
|
+
? '未开始:请先填写监控账号 UID'
|
|
239
|
+
: result.skipped === 'busy'
|
|
240
|
+
? '正在运行,请稍候'
|
|
241
|
+
: `检查完成:检查 ${result.checked || 0} 条评论,预览 ${result.previewed || 0} 条,跳过 ${result.skipped || 0} 条` +
|
|
242
|
+
(result.errors ? `,失败 ${result.errors} 条` : ''),
|
|
243
|
+
)
|
|
244
|
+
} catch (e) { setError(e.message) } finally { setBusy('') }
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
const replyNow = async () => {
|
|
248
|
+
if (!window.confirm('立即回复会真实发布到微博,确定继续?')) return
|
|
249
|
+
setBusy('reply'); setError(''); setNotice('')
|
|
250
|
+
try {
|
|
251
|
+
const data = await request('/reply-now', { method: 'POST', body: '{}' })
|
|
252
|
+
setStatus(data.status)
|
|
253
|
+
const result = data.result || {}
|
|
254
|
+
setNotice(
|
|
255
|
+
result.replied > 0
|
|
256
|
+
? `已立即回复 ${result.replied} 条,可在下方日志查看`
|
|
257
|
+
: result.skipped === 'no-target'
|
|
258
|
+
? '未开始:请先填写监控账号 UID'
|
|
259
|
+
: '没有可立即回复的评论(可能已处理或模型选择跳过)',
|
|
260
|
+
)
|
|
261
|
+
} catch (e) { setError(e.message) } finally { setBusy('') }
|
|
262
|
+
}
|
|
263
|
+
|
|
230
264
|
if (!settings) return h('div', { className: 'fw-page' }, h('p', { className: 'fw-muted' }, error || '正在读取微博设置…'))
|
|
231
265
|
|
|
232
266
|
const recent = Array.isArray(status?.recent) ? status.recent : []
|
|
@@ -238,7 +272,7 @@ window.__ModuleLoader__.load({
|
|
|
238
272
|
h('div', { className: 'fw-head' },
|
|
239
273
|
h('div', null,
|
|
240
274
|
h('p', { className: 'fw-eyebrow' }, 'FOXY EXTENSION'),
|
|
241
|
-
h('h1', null, '
|
|
275
|
+
h('h1', null, '微博评论'),
|
|
242
276
|
h('p', null, 'Foxy 按 foxy3 预设关注微博账号,阅读、判断、评论并写入记忆。'),
|
|
243
277
|
),
|
|
244
278
|
h('span', { className: 'fw-badge' }, settings.enabled ? '自动浏览已开启' : '自动浏览已关闭'),
|
|
@@ -250,8 +284,8 @@ window.__ModuleLoader__.load({
|
|
|
250
284
|
h(Field, { label: '当前账号', hint: '扫码成功后自动显示' },
|
|
251
285
|
h('input', { value: settings.accountName ? `${settings.accountName}(${settings.accountUid || '?'})` : settings.cookieSet ? '已登录' : '未登录', readOnly: true }),
|
|
252
286
|
),
|
|
253
|
-
h(Field, { label: '
|
|
254
|
-
h('input', { value: settings.
|
|
287
|
+
h(Field, { label: '监控账号 UID', hint: '要检查博文评论的目标账号;不是登录的机器人账号' },
|
|
288
|
+
h('input', { value: settings.targetUid || '', placeholder: '例如 1234567890', onChange: (e) => setField('targetUid', e.target.value) })),
|
|
255
289
|
),
|
|
256
290
|
h('div', { className: 'fw-actions' },
|
|
257
291
|
h('button', { className: 'fw-btn primary', disabled: !!busy, onClick: startLogin }, busy === 'login' ? '获取二维码…' : qr ? '刷新二维码' : '扫码登录'),
|
|
@@ -280,9 +314,9 @@ window.__ModuleLoader__.load({
|
|
|
280
314
|
),
|
|
281
315
|
|
|
282
316
|
h('section', { className: 'fw-card' },
|
|
283
|
-
h(SectionHead, { title: '
|
|
317
|
+
h(SectionHead, { title: '博文评论回复', description: '每日按设定时间检查目标账号的新博文,并回复其评论区中尚未处理的他人评论。' }),
|
|
284
318
|
h(Toggle, { checked: settings.enabled, onChange: (v) => setField('enabled', v), label: '启用自动检查', hint: '关闭后定时任务不运行,仍可使用「立即运行」。' }),
|
|
285
|
-
h(Toggle, { checked: settings.replyOwnPosts, onChange: (v) => setField('replyOwnPosts', v), label: '
|
|
319
|
+
h(Toggle, { checked: settings.replyOwnPosts, onChange: (v) => setField('replyOwnPosts', v), label: '仅监控目标账号的博文', hint: '关闭后回到旧模式,浏览关注账号的时间线并评论博文。' }),
|
|
286
320
|
h('div', { className: 'fw-grid' },
|
|
287
321
|
h(Field, { label: '每日检查时间', hint: 'HH:MM,多个用逗号分隔,最多 6 个,例如 08:30, 20:00' },
|
|
288
322
|
h('input', { value: scheduleText, placeholder: '20:00', onChange: (e) => setScheduleText(e.target.value) })),
|
|
@@ -297,7 +331,7 @@ window.__ModuleLoader__.load({
|
|
|
297
331
|
h('input', { type: 'number', min: 0, max: 10, value: settings.maxCommentsPerDay, onChange: (e) => setField('maxCommentsPerDay', e.target.value) })),
|
|
298
332
|
),
|
|
299
333
|
h('div', { className: 'fw-grid' },
|
|
300
|
-
h(Field, { label: '时间线轮询(旧模式备用)', hint: '
|
|
334
|
+
h(Field, { label: '时间线轮询(旧模式备用)', hint: '关闭“仅监控目标账号的博文”时使用,分钟' },
|
|
301
335
|
h('input', { type: 'number', min: 5, max: 1440, value: settings.intervalMinutes, onChange: (e) => setField('intervalMinutes', e.target.value) })),
|
|
302
336
|
h(Field, { label: '旧模式评论模式', hint: '浏览他人博文时是否发一级评论' },
|
|
303
337
|
h('select', { value: settings.commentMode, onChange: (e) => setField('commentMode', e.target.value) },
|
|
@@ -311,6 +345,8 @@ window.__ModuleLoader__.load({
|
|
|
311
345
|
h(Toggle, { checked: settings.debug, onChange: (v) => setField('debug', v), label: '记录调试日志', hint: '保存模型输出与记忆写入结果,便于排查。' }),
|
|
312
346
|
h('div', { className: 'fw-actions' },
|
|
313
347
|
h('button', { className: 'fw-btn primary', disabled: !!busy, onClick: runNow }, busy === 'run' ? '正在运行…' : '立即运行'),
|
|
348
|
+
h('button', { className: 'fw-btn', disabled: !!busy, onClick: checkNow }, busy === 'check' ? '检查中…' : '立即检查'),
|
|
349
|
+
h('button', { className: 'fw-btn', disabled: !!busy, onClick: replyNow }, busy === 'reply' ? '回复中…' : '立即回复'),
|
|
314
350
|
h('button', { className: 'fw-btn', disabled: !!busy, onClick: save }, busy === 'save' ? '保存中…' : '保存设置'),
|
|
315
351
|
),
|
|
316
352
|
h('div', { className: 'fw-stats' },
|
package/lib/index.js
CHANGED
|
@@ -11,6 +11,8 @@ export const inject = ['connection']
|
|
|
11
11
|
export const STATUS_ROUTE = '/api/dsh-foxy-weibo/status'
|
|
12
12
|
export const SETTINGS_ROUTE = '/api/dsh-foxy-weibo/settings'
|
|
13
13
|
export const RUN_ROUTE = '/api/dsh-foxy-weibo/run'
|
|
14
|
+
export const CHECK_NOW_ROUTE = '/api/dsh-foxy-weibo/check-now'
|
|
15
|
+
export const REPLY_NOW_ROUTE = '/api/dsh-foxy-weibo/reply-now'
|
|
14
16
|
export const LOGIN_START_ROUTE = '/api/dsh-foxy-weibo/login/start'
|
|
15
17
|
export const LOGIN_POLL_ROUTE = '/api/dsh-foxy-weibo/login/poll'
|
|
16
18
|
export const LOGOUT_ROUTE = '/api/dsh-foxy-weibo/logout'
|
|
@@ -156,15 +158,20 @@ async function rememberReply(ctx, config, state, post, comment, replyText) {
|
|
|
156
158
|
}
|
|
157
159
|
}
|
|
158
160
|
|
|
159
|
-
async function scanOwnComments(ctx, config, state, log, force = false) {
|
|
161
|
+
async function scanOwnComments(ctx, config, state, log, force = false, options = {}) {
|
|
160
162
|
if (!config.enabled && !force) return { skipped: 'disabled' }
|
|
161
|
-
const
|
|
162
|
-
const
|
|
163
|
-
|
|
163
|
+
const previewOnly = options.previewOnly === true
|
|
164
|
+
const forceReply = options.forceReply === true
|
|
165
|
+
const canReply = !previewOnly && (forceReply || config.replyMode === 'auto')
|
|
166
|
+
const targetCid = String(options.cid || '').trim()
|
|
167
|
+
const targetUid = String(config.targetUid || '').trim()
|
|
168
|
+
if (!targetUid) return { skipped: 'no-target' }
|
|
169
|
+
const loginUid = String(config.accountUid || '').trim()
|
|
164
170
|
const maxPosts = Math.max(1, Math.min(20, Number(config.maxPostsPerRun) || 5))
|
|
165
|
-
const maxReplies = Math.max(1, Math.min(8, Number(config.maxCommentsPerDay) || 1))
|
|
171
|
+
const maxReplies = Math.max(1, Math.min(8, Number(options.maxReplies ?? config.maxCommentsPerDay) || 1))
|
|
172
|
+
const ignoreQuota = forceReply
|
|
166
173
|
const result = {
|
|
167
|
-
mode: 'own-comments',
|
|
174
|
+
mode: options.mode || 'own-comments',
|
|
168
175
|
checked: 0,
|
|
169
176
|
replied: 0,
|
|
170
177
|
previewed: 0,
|
|
@@ -174,7 +181,7 @@ async function scanOwnComments(ctx, config, state, log, force = false) {
|
|
|
174
181
|
}
|
|
175
182
|
let posts = []
|
|
176
183
|
try {
|
|
177
|
-
posts = await fetchTimeline(
|
|
184
|
+
posts = await fetchTimeline(targetUid, config)
|
|
178
185
|
} catch (error) {
|
|
179
186
|
return { ...result, errors: 1, error: error?.message || String(error) }
|
|
180
187
|
}
|
|
@@ -190,14 +197,15 @@ async function scanOwnComments(ctx, config, state, log, force = false) {
|
|
|
190
197
|
continue
|
|
191
198
|
}
|
|
192
199
|
for (const comment of comments) {
|
|
200
|
+
if (targetCid && String(comment.cid) !== targetCid) continue
|
|
193
201
|
if (canReply && result.replied >= maxReplies) break
|
|
194
202
|
if (!comment.cid || state.commentSeen[comment.cid]) continue
|
|
195
|
-
if (String(comment.author?.uid || '') ===
|
|
203
|
+
if (loginUid && String(comment.author?.uid || '') === loginUid) {
|
|
196
204
|
state.commentSeen[comment.cid] = { at: Date.now(), status: 'self' }
|
|
197
205
|
continue
|
|
198
206
|
}
|
|
199
207
|
const limit = Math.max(0, Number(config.maxCommentsPerDay) || 0)
|
|
200
|
-
if (canReply && commentCount(state) >= limit) break
|
|
208
|
+
if (canReply && !ignoreQuota && commentCount(state) >= limit) break
|
|
201
209
|
result.checked += 1
|
|
202
210
|
let entry
|
|
203
211
|
try {
|
|
@@ -474,11 +482,56 @@ export function apply(ctx, rawConfig = {}) {
|
|
|
474
482
|
}
|
|
475
483
|
}
|
|
476
484
|
|
|
485
|
+
const checkNow = async () => {
|
|
486
|
+
if (running) return { skipped: 'busy' }
|
|
487
|
+
running = true
|
|
488
|
+
try {
|
|
489
|
+
log('[foxy-weibo] 开始立即检查')
|
|
490
|
+
const result = await scanOwnComments(ctx, config, state, log, true, {
|
|
491
|
+
previewOnly: true,
|
|
492
|
+
mode: 'own-comments-check',
|
|
493
|
+
})
|
|
494
|
+
saveState(state)
|
|
495
|
+
log(`[foxy-weibo] 立即检查结束 ${JSON.stringify(result)}`)
|
|
496
|
+
return result
|
|
497
|
+
} catch (error) {
|
|
498
|
+
saveState(state)
|
|
499
|
+
ctx.logger?.warn?.(`[foxy-weibo] check failed: ${error?.message || error}`)
|
|
500
|
+
return { error: String(error?.message || error) }
|
|
501
|
+
} finally {
|
|
502
|
+
running = false
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
const replyNow = async (cid = '') => {
|
|
507
|
+
if (running) return { skipped: 'busy' }
|
|
508
|
+
running = true
|
|
509
|
+
try {
|
|
510
|
+
log(`[foxy-weibo] 开始立即回复${cid ? ` cid=${cid}` : ''}`)
|
|
511
|
+
const result = await scanOwnComments(ctx, config, state, log, true, {
|
|
512
|
+
forceReply: true,
|
|
513
|
+
maxReplies: 1,
|
|
514
|
+
cid,
|
|
515
|
+
mode: 'own-comments-reply-now',
|
|
516
|
+
})
|
|
517
|
+
saveState(state)
|
|
518
|
+
log(`[foxy-weibo] 立即回复结束 ${JSON.stringify(result)}`)
|
|
519
|
+
return result
|
|
520
|
+
} catch (error) {
|
|
521
|
+
saveState(state)
|
|
522
|
+
ctx.logger?.warn?.(`[foxy-weibo] reply-now failed: ${error?.message || error}`)
|
|
523
|
+
return { error: String(error?.message || error) }
|
|
524
|
+
} finally {
|
|
525
|
+
running = false
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
|
|
477
529
|
const status = () => ({
|
|
478
530
|
enabled: config.enabled === true,
|
|
479
531
|
running,
|
|
480
532
|
mode: config.replyOwnPosts === false ? 'timeline' : 'own-comments',
|
|
481
|
-
|
|
533
|
+
targetUid: config.targetUid || '',
|
|
534
|
+
loginUid: config.accountUid || '',
|
|
482
535
|
scheduleTimes: config.scheduleTimes,
|
|
483
536
|
nextRunAt,
|
|
484
537
|
replyMode: config.replyMode,
|
|
@@ -580,7 +633,6 @@ export function apply(ctx, rawConfig = {}) {
|
|
|
580
633
|
config.cookie = result.cookie
|
|
581
634
|
config.accountName = result.account?.screenName || ''
|
|
582
635
|
config.accountUid = result.account?.uid || ''
|
|
583
|
-
if (!config.selfUid && result.account?.uid) config.selfUid = result.account.uid
|
|
584
636
|
saveSettings(config)
|
|
585
637
|
}
|
|
586
638
|
return json({ ok: true, ...result, settings: publicSettings(config), status: status() })
|
|
@@ -598,6 +650,17 @@ export function apply(ctx, rawConfig = {}) {
|
|
|
598
650
|
const result = await run('manual', true)
|
|
599
651
|
return json({ ok: true, result, status: status() })
|
|
600
652
|
})
|
|
653
|
+
|
|
654
|
+
route(CHECK_NOW_ROUTE, ['POST'], async () => {
|
|
655
|
+
const result = await checkNow()
|
|
656
|
+
return json({ ok: true, result, status: status() })
|
|
657
|
+
})
|
|
658
|
+
|
|
659
|
+
route(REPLY_NOW_ROUTE, ['POST'], async (request) => {
|
|
660
|
+
const body = await readJsonBody(request)
|
|
661
|
+
const result = await replyNow(body?.cid)
|
|
662
|
+
return json({ ok: true, result, status: status() })
|
|
663
|
+
})
|
|
601
664
|
})
|
|
602
665
|
|
|
603
666
|
log(
|
package/lib/settings.js
CHANGED
|
@@ -5,7 +5,7 @@ import { dirname, join } from 'node:path'
|
|
|
5
5
|
export const DEFAULTS = {
|
|
6
6
|
enabled: false,
|
|
7
7
|
cookie: '',
|
|
8
|
-
|
|
8
|
+
targetUid: '',
|
|
9
9
|
targetUids: [],
|
|
10
10
|
replyOwnPosts: true,
|
|
11
11
|
replyMode: 'off', // off | auto
|
|
@@ -26,7 +26,7 @@ export const DEFAULTS = {
|
|
|
26
26
|
|
|
27
27
|
export const SETTING_KEYS = [
|
|
28
28
|
'enabled',
|
|
29
|
-
'
|
|
29
|
+
'targetUid',
|
|
30
30
|
'targetUids',
|
|
31
31
|
'replyOwnPosts',
|
|
32
32
|
'replyMode',
|
|
@@ -45,7 +45,7 @@ export const SETTING_KEYS = [
|
|
|
45
45
|
]
|
|
46
46
|
|
|
47
47
|
const TEXT_KEYS = new Set([
|
|
48
|
-
'
|
|
48
|
+
'targetUid',
|
|
49
49
|
'commentMode',
|
|
50
50
|
'replyMode',
|
|
51
51
|
'memoryPresetId',
|
|
@@ -137,6 +137,8 @@ export function loadSettings(base = {}) {
|
|
|
137
137
|
if (existsSync(settingsPath())) {
|
|
138
138
|
const parsed = JSON.parse(readFileSync(settingsPath(), 'utf8'))
|
|
139
139
|
next = normalizePatch(next, parsed)
|
|
140
|
+
// 0.2.x used selfUid for the monitored account; migrate it once.
|
|
141
|
+
if (!next.targetUid && typeof parsed.selfUid === 'string') next.targetUid = parsed.selfUid.trim()
|
|
140
142
|
if (typeof parsed.cookie === 'string') next.cookie = parsed.cookie
|
|
141
143
|
if (typeof parsed.llmApiKey === 'string') next.llmApiKey = parsed.llmApiKey
|
|
142
144
|
}
|
package/package.json
CHANGED