dsh-foxy-weibo 0.2.4 → 0.3.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/README.md CHANGED
@@ -6,13 +6,13 @@ Foxy 的微博子插件:按 foxy3 预设关注微博账号、阅读博文、
6
6
 
7
7
  ## 功能
8
8
 
9
- - **博文评论回复**:检查指定的监控账号 UID,读取其新博文下的评论,只回复未处理的他人评论;登录账号是机器人自己,监控账号通常是主人账号;
10
- - 可设置每日检查时间(`HH:MM`,最多 6 个),例如 `08:30, 20:00`;
11
- - 回复前按 foxy3 预设判断 `skip / comment`,不设最少字数,最多 80 字;
12
- - 旧模式保留:关注一个或多个微博 UID,定时读取时间线并评论;
13
- - 可选自动回复/评论,支持 `replyOthers`、每日上限;
14
- - 浏览、评论、回复事件写入 Foxy 记忆,字段包含 `platform: weibo`、`channelType: weibo_post|weibo_comment`、`eventType: weibo_browse|weibo_comment|weibo_reply`;
15
- - 设置页提供扫码登录、Cookie 备用、自己的 UID、关注 UID、模型、每日时间、回复模式、调试日志与立即运行。
9
+ - **阅读目标账号博文**:按设定时间阅读监控账号的博文,每篇写入一条“目标账号博文”记忆,按博文 ID 去重;
10
+ - 可选对博文发表一级评论;可设置评论起始时间(`replyAfter`),只评论该时间之后的博文;
11
+ - 评论前按 foxy3 预设判断 `skip / comment`,不设最少字数,最多 80 字;
12
+ - 自检去重:已评论/已跳过的博文写入 `postSeen`,不会重复评论同一篇;
13
+ - 旧模式保留:关注一个或多个微博 UID,定时读取时间线并评论(`replyOwnPosts: false`);
14
+ - 阅读、评论事件写入 Foxy 记忆,字段包含 `platform: weibo`、`channelType: weibo_post`、`eventType: weibo_owner_post|weibo_comment`;
15
+ - 设置页提供扫码登录、Cookie 备用、监控账号 UID、关注 UID、模型、每日时间、评论模式、评论起始时间、记忆开关、调试日志与立即检查/立即回复。
16
16
 
17
17
  ## 安装
18
18
 
@@ -47,7 +47,7 @@ Cookie 只保存在本机 `~/.dsh/foxy-weibo/settings.json`,不会回传给前
47
47
 
48
48
  ### 自己的 UID / 关注 UID
49
49
 
50
- `监控账号 UID` 是要检查博文评论的目标账号(通常是主人账号);`当前账号` 是扫码登录的机器人账号,两者可以不同。
50
+ `监控账号 UID` 是被阅读、被评论的目标账号(通常是主人账号);`当前账号` 是扫码登录的机器人账号,两者可以不同。插件不会把登录的机器人账号当成监控账号。
51
51
 
52
52
  `关注 UID` 是旧模式使用,多个 UID 用逗号分隔,最多 20 个。UID 是微博用户主页 URL 里的数字 ID,例如 `https://weibo.com/u/1234567890`。
53
53
 
@@ -64,18 +64,17 @@ Cookie 只保存在本机 `~/.dsh/foxy-weibo/settings.json`,不会回传给前
64
64
  ## 建议顺序
65
65
 
66
66
  1. 扫码登录,填监控账号 UID,`enabled: true`、`replyMode: off`;
67
- 2. 设置每日检查时间,或先点「立即运行」,看日志和「调试」里的模型决策;
67
+ 2. 设置每日检查时间,或先点「立即检查」,看日志和「调试」里的模型决策;
68
68
  3. 确认判断正常后,再开启 `replyMode: auto`,每日上限先设为 1;
69
- 4. 「立即检查」只判断不发布,用来预览回复;「立即回复」会真实发布一条,适合测试;
70
- 5. 微博评论有账号风控,不要高频、不要对陌生人全自动回复。
69
+ 4. 「立即检查」只预览、不评论;「立即回复」会真实评论一篇,适合测试;
70
+ 5. 评论有账号风控,不要高频;阅读往期博文会写入记忆,但按博文 ID 去重。
71
71
 
72
72
  ## 记忆事件
73
73
 
74
74
  微博插件写入的示例:
75
75
 
76
- - 浏览:`我在微博看到 <博主> 的博文「...」,觉得...`
77
- - 评论:`我在微博评论了 <博主> 的博文「...」:...`
78
- - 回复:`我在微博回复了 <网友> 在自己博文「...」下的评论:...`
76
+ - 阅读目标博文:`我看到 <博主> 发了一条微博:「...」`
77
+ - 评论博文:`我在微博评论了 <博主> 的博文「...」:...`
79
78
 
80
79
  事件字段:
81
80
 
package/cordis.patch.yml CHANGED
@@ -14,10 +14,12 @@
14
14
  targetUids: []
15
15
  replyOwnPosts: true
16
16
  replyMode: 'off'
17
+ replyAfter: ''
18
+ learnPosts: true
17
19
  scheduleTimes:
18
20
  - '20:00'
19
21
  intervalMinutes: 60
20
- maxPostsPerRun: 5
22
+ maxPostsPerRun: 20
21
23
  commentMode: 'off'
22
24
  maxCommentsPerDay: 1
23
25
  replyOthers: true
package/lib/client.js CHANGED
@@ -224,10 +224,11 @@ window.__ModuleLoader__.load({
224
224
  ? '正在运行,请稍候'
225
225
  : result.skipped === 'no-target'
226
226
  ? '未开始:请先填写监控账号 UID'
227
- : `运行完成:博文 ${result.posts || 0} 篇、评论 ${result.comments || 0} 条,` +
228
- `处理 ${result.checked || 0} 条,回复 ${result.replied || 0} 条` +
227
+ : `运行完成:博文 ${result.posts || 0} 篇,读取记忆 ${result.memories || 0} 条,` +
228
+ `评论 ${result.commented || 0} 条` +
229
229
  (result.previewed ? `,预览 ${result.previewed} 条` : '') +
230
- `,跳过 ${result.skipped || 0} 条`,
230
+ `,跳过 ${result.skipped || 0} 条` +
231
+ (result.errors ? `,失败 ${result.errors} 条` : ''),
231
232
  )
232
233
  } catch (e) { setError(e.message) } finally { setBusy('') }
233
234
  }
@@ -247,8 +248,8 @@ window.__ModuleLoader__.load({
247
248
  ? '未开始:请先填写监控账号 UID'
248
249
  : result.skipped === 'busy'
249
250
  ? '正在运行,请稍候'
250
- : `检查完成:博文 ${result.posts || 0} 篇、评论 ${result.comments || 0} 条,` +
251
- `待处理 ${result.checked || 0} 条,预览 ${result.previewed || 0} 条,跳过 ${result.skipped || 0} 条` +
251
+ : `检查完成:博文 ${result.posts || 0} 篇,可评论 ${result.previewed || 0} 条,` +
252
+ `读取记忆 ${result.memories || 0} 条,跳过 ${result.skipped || 0} 条` +
252
253
  (result.errors ? `,失败 ${result.errors} 条` : ''),
253
254
  )
254
255
  } catch (e) { setError(e.message) } finally { setBusy('') }
@@ -266,11 +267,11 @@ window.__ModuleLoader__.load({
266
267
  return
267
268
  }
268
269
  setNotice(
269
- result.replied > 0
270
- ? `已立即回复 ${result.replied} 条,可在下方日志查看`
270
+ result.commented > 0
271
+ ? `已评论 ${result.commented} 篇博文,可在下方日志查看`
271
272
  : result.skipped === 'no-target'
272
273
  ? '未开始:请先填写监控账号 UID'
273
- : `已浏览 ${result.posts || 0} 篇博文、${result.comments || 0} 条评论,但没有可立即回复的新评论(可能已处理或模型选择跳过)`,
274
+ : `已浏览 ${result.posts || 0} 篇博文,但没有可立即评论的新博文(可能已处理或模型选择跳过)`,
274
275
  )
275
276
  } catch (e) { setError(e.message) } finally { setBusy('') }
276
277
  }
@@ -328,21 +329,28 @@ window.__ModuleLoader__.load({
328
329
  ),
329
330
 
330
331
  h('section', { className: 'fw-card' },
331
- h(SectionHead, { title: '博文评论回复', description: '每日按设定时间检查目标账号的新博文,并回复其评论区中尚未处理的他人评论。' }),
332
+ h(SectionHead, { title: '博文阅读与评论', description: '阅读目标账号的博文并写入记忆;仅对设定时间之后的博文发表评论。' }),
332
333
  h(Toggle, { checked: settings.enabled, onChange: (v) => setField('enabled', v), label: '启用自动检查', hint: '关闭后定时任务不运行,仍可使用「立即运行」。' }),
333
334
  h(Toggle, { checked: settings.replyOwnPosts, onChange: (v) => setField('replyOwnPosts', v), label: '仅监控目标账号的博文', hint: '关闭后回到旧模式,浏览关注账号的时间线并评论博文。' }),
334
335
  h('div', { className: 'fw-grid' },
335
336
  h(Field, { label: '每日检查时间', hint: 'HH:MM,多个用逗号分隔,最多 6 个,例如 08:30, 20:00' },
336
337
  h('input', { value: scheduleText, placeholder: '20:00', onChange: (e) => setScheduleText(e.target.value) })),
337
- h(Field, { label: '回复模式', hint: '自动才会发布回复;建议先仅记录' },
338
+ h(Field, { label: '评论模式', hint: '自动才会发布博文评论;建议先仅检查' },
338
339
  h('select', { value: settings.replyMode, onChange: (e) => setField('replyMode', e.target.value) },
339
340
  h('option', { value: 'off' }, '仅检查,不回复'),
340
341
  h('option', { value: 'auto' }, '自动回复'),
341
342
  )),
342
343
  h(Field, { label: '浏览范围(博文条数)', hint: '按时间倒序检查;1–100,填 100 即尽量浏览全部' },
343
344
  h('input', { type: 'number', min: 1, max: 100, value: settings.maxPostsPerRun, onChange: (e) => setField('maxPostsPerRun', e.target.value) })),
344
- h(Field, { label: '每日回复上限', hint: '0–10,同时限制评论' },
345
+ h(Field, { label: '每日评论上限', hint: '0–10,仅统计自动评论' },
345
346
  h('input', { type: 'number', min: 0, max: 10, value: settings.maxCommentsPerDay, onChange: (e) => setField('maxCommentsPerDay', e.target.value) })),
347
+ h(Field, { label: '评论起始时间', hint: '只评论此时间之后的博文;留空表示不限制' },
348
+ h('input', { type: 'datetime-local', value: settings.replyAfter || '', onChange: (e) => setField('replyAfter', e.target.value) })),
349
+ h(Field, { label: '阅读后写入记忆', hint: '每篇未读博文写一条“目标账号博文”记忆,按博文去重' },
350
+ h('select', { value: settings.learnPosts === false ? 'off' : 'on', onChange: (e) => setField('learnPosts', e.target.value === 'on') },
351
+ h('option', { value: 'on' }, '写入'),
352
+ h('option', { value: 'off' }, '不写入'),
353
+ )),
346
354
  ),
347
355
  h('div', { className: 'fw-grid' },
348
356
  h(Field, { label: '时间线轮询(旧模式备用)', hint: '关闭“仅监控目标账号的博文”时使用,分钟' },
@@ -353,8 +361,7 @@ window.__ModuleLoader__.load({
353
361
  h('option', { value: 'auto' }, '自动评论'),
354
362
  )),
355
363
  ),
356
- h(Toggle, { checked: settings.replyOthers, onChange: (v) => setField('replyOthers', v), label: '回复他人评论', hint: '旧模式可从评论区选择一条评论回复。' }),
357
- h(Toggle, { checked: settings.enableMemory, onChange: (v) => setField('enableMemory', v), label: '写入 Foxy 记忆', hint: '通过 foxyMemory 写入微博事件,按 preset 召回。' }),
364
+ h(Toggle, { checked: settings.enableMemory, onChange: (v) => setField('enableMemory', v), label: '启用 Foxy 记忆', hint: '通过 foxyMemory 写入微博事件,按 preset 召回。' }),
358
365
  h(Toggle, { checked: settings.runOnStart, onChange: (v) => setField('runOnStart', v), label: '启动 DSH 后自动运行', hint: '启动约 20 秒后执行一轮。' }),
359
366
  h(Toggle, { checked: settings.debug, onChange: (v) => setField('debug', v), label: '记录调试日志', hint: '保存模型输出与记忆写入结果,便于排查。' }),
360
367
  h('div', { className: 'fw-actions' },
package/lib/index.js CHANGED
@@ -30,6 +30,9 @@ function loadState() {
30
30
  seen: parsed.seen && typeof parsed.seen === 'object' ? parsed.seen : {},
31
31
  commentSeen:
32
32
  parsed.commentSeen && typeof parsed.commentSeen === 'object' ? parsed.commentSeen : {},
33
+ postSeen: parsed.postSeen && typeof parsed.postSeen === 'object' ? parsed.postSeen : {},
34
+ postMemory:
35
+ parsed.postMemory && typeof parsed.postMemory === 'object' ? parsed.postMemory : {},
33
36
  days: parsed.days && typeof parsed.days === 'object' ? parsed.days : {},
34
37
  history: Array.isArray(parsed.history) ? parsed.history.slice(0, 50) : [],
35
38
  debug: Array.isArray(parsed.debug) ? parsed.debug.slice(0, 80) : [],
@@ -37,7 +40,17 @@ function loadState() {
37
40
  lastResult: parsed.lastResult || null,
38
41
  }
39
42
  } catch {
40
- return { seen: {}, commentSeen: {}, days: {}, history: [], debug: [], lastRun: 0, lastResult: null }
43
+ return {
44
+ seen: {},
45
+ commentSeen: {},
46
+ postSeen: {},
47
+ postMemory: {},
48
+ days: {},
49
+ history: [],
50
+ debug: [],
51
+ lastRun: 0,
52
+ lastResult: null,
53
+ }
41
54
  }
42
55
  }
43
56
 
@@ -65,6 +78,13 @@ function firstLine(text, limit = 120) {
65
78
  return String(text || '').replace(/\s+/g, ' ').trim().slice(0, limit)
66
79
  }
67
80
 
81
+ function entryReason(decision) {
82
+ return (
83
+ String(decision?.mood || decision?.memory || decision?.reason || '').trim() ||
84
+ '模型判断不需要评论'
85
+ )
86
+ }
87
+
68
88
  function pushHistory(state, entry) {
69
89
  state.history = [entry, ...(Array.isArray(state.history) ? state.history : [])].slice(0, 50)
70
90
  }
@@ -158,6 +178,200 @@ async function rememberReply(ctx, config, state, post, comment, replyText) {
158
178
  }
159
179
  }
160
180
 
181
+ async function rememberPost(ctx, config, state, post) {
182
+ if (!config.enableMemory || config.learnPosts === false) {
183
+ return { ok: false, reason: 'disabled' }
184
+ }
185
+ if (state.postMemory?.[post.id]) return { ok: false, reason: 'already' }
186
+ const provider = memoryProvider(ctx)
187
+ if (!provider?.record) return { ok: false, reason: 'no-provider' }
188
+ const excerpt = firstLine(post.text, 180)
189
+ const text = `我看到 ${post.author.name || '目标账号'} 发了一条微博:「${excerpt}」`
190
+ try {
191
+ await provider.record({
192
+ role: 'assistant',
193
+ text,
194
+ sessionId: `weibo-post:${post.id}`,
195
+ presetId: config.memoryPresetId || 'foxy3(Character)',
196
+ platform: 'weibo',
197
+ sourceLabel: '目标账号博文',
198
+ eventType: 'weibo_owner_post',
199
+ channelType: 'weibo_post',
200
+ channelId: post.id,
201
+ title: excerpt,
202
+ url: post.url,
203
+ scene: 'weibo',
204
+ })
205
+ state.postMemory = state.postMemory || {}
206
+ state.postMemory[post.id] = Date.now()
207
+ debugLog(state, config, {
208
+ kind: 'memory',
209
+ post: post.id,
210
+ memory: 'written',
211
+ eventType: 'weibo_owner_post',
212
+ text: firstLine(text, 300),
213
+ })
214
+ return { ok: true, reason: 'written', text }
215
+ } catch (error) {
216
+ const reason = error?.message || String(error)
217
+ debugLog(state, config, { kind: 'memory', post: post.id, memory: 'error', reason })
218
+ return { ok: false, reason }
219
+ }
220
+ }
221
+
222
+ async function scanTargetPosts(ctx, config, state, log, force = false, options = {}) {
223
+ if (!config.enabled && !force) return { skipped: 'disabled' }
224
+ const previewOnly = options.previewOnly === true
225
+ const forceReply = options.forceReply === true
226
+ const canComment = !previewOnly && (forceReply || config.replyMode === 'auto')
227
+ const targetUid = String(config.targetUid || '').trim()
228
+ const result = {
229
+ mode: options.mode || 'target-posts',
230
+ posts: 0,
231
+ checked: 0,
232
+ commented: 0,
233
+ previewed: 0,
234
+ skipped: 0,
235
+ errors: 0,
236
+ memories: 0,
237
+ items: [],
238
+ }
239
+ if (!targetUid) return { ...result, skipped: 'no-target' }
240
+ if (!config.cookie) return { ...result, errors: 1, error: '请先扫码登录微博账号' }
241
+ const account = await fetchAccount(config.cookie)
242
+ if (!account.uid) return { ...result, errors: 1, error: '微博登录态无效,请重新扫码登录后再检查' }
243
+ const loginUid = String(config.accountUid || account.uid || '')
244
+ const maxPosts = Math.max(1, Math.min(100, Number(options.maxPosts ?? config.maxPostsPerRun) || 5))
245
+ const cutoff = config.replyAfter ? Date.parse(config.replyAfter) : 0
246
+ const maxComments = Math.max(1, Math.min(10, Number(config.maxCommentsPerDay) || 1))
247
+ let posts = []
248
+ try {
249
+ posts = await fetchTimeline(targetUid, config, {
250
+ maxPosts,
251
+ maxPages: Math.ceil(maxPosts / 10) + 1,
252
+ })
253
+ result.posts = posts.length
254
+ } catch (error) {
255
+ return { ...result, errors: 1, error: error?.message || String(error) }
256
+ }
257
+ debugLog(state, config, {
258
+ kind: 'timeline',
259
+ targetUid,
260
+ posts: posts.length,
261
+ replyAfter: config.replyAfter || '',
262
+ })
263
+
264
+ for (const post of posts) {
265
+ if (!post.id) continue
266
+ const memory = await rememberPost(ctx, config, state, post)
267
+ if (memory.ok) result.memories += 1
268
+ if (state.postSeen?.[post.id]) continue
269
+ if (cutoff && post.createdAtMs && post.createdAtMs < cutoff) continue
270
+ if (!canComment && config.replyMode === 'off' && !previewOnly) {
271
+ // Still preview in check mode; normal runs with reply off only learn.
272
+ }
273
+ if (canComment && commentCount(state) >= maxComments) {
274
+ result.skipped += 1
275
+ debugLog(state, config, {
276
+ kind: 'quota',
277
+ post: post.id,
278
+ message: '每日评论额度已用完',
279
+ })
280
+ continue
281
+ }
282
+ result.checked += 1
283
+ let entry
284
+ try {
285
+ const decision = await judgePost(config, {
286
+ summary: postSummary(post),
287
+ comments: [],
288
+ commentsText: '',
289
+ targetPost: post,
290
+ mayComment: true,
291
+ mayReply: false,
292
+ forceComment: forceReply,
293
+ })
294
+ const comment = String(decision.comment || '').trim()
295
+ if (decision.action === 'comment' && comment && canComment) {
296
+ const published = await postComment({ mid: post.mid || post.id, text: comment, config })
297
+ result.commented += 1
298
+ bumpComments(state)
299
+ state.postSeen[post.id] = { status: 'commented', at: Date.now(), commentId: published.cid }
300
+ entry = {
301
+ at: Date.now(),
302
+ kind: 'weibo-post',
303
+ postId: post.id,
304
+ title: firstLine(post.text, 80),
305
+ url: post.url,
306
+ author: post.author.name,
307
+ action: 'comment',
308
+ comment,
309
+ commentId: published.cid,
310
+ memory: memory.ok ? 'written' : memory.reason,
311
+ }
312
+ ctx.logger?.info?.(`[foxy-weibo] 评论博文 ${post.id}:${firstLine(comment, 80)}`)
313
+ } else if (decision.action === 'comment' && comment) {
314
+ result.previewed += 1
315
+ entry = {
316
+ at: Date.now(),
317
+ kind: 'weibo-post',
318
+ postId: post.id,
319
+ title: firstLine(post.text, 80),
320
+ url: post.url,
321
+ author: post.author.name,
322
+ action: 'preview',
323
+ comment,
324
+ memory: memory.ok ? 'written' : memory.reason,
325
+ }
326
+ } else {
327
+ result.skipped += 1
328
+ state.postSeen[post.id] = { status: 'skipped', at: Date.now() }
329
+ entry = {
330
+ at: Date.now(),
331
+ kind: 'weibo-post',
332
+ postId: post.id,
333
+ title: firstLine(post.text, 80),
334
+ url: post.url,
335
+ author: post.author.name,
336
+ action: 'skip',
337
+ reason: entryReason(decision),
338
+ memory: memory.ok ? 'written' : memory.reason,
339
+ }
340
+ }
341
+ debugLog(state, config, {
342
+ kind: 'target-judge',
343
+ post: post.id,
344
+ action: decision.action,
345
+ memory: memory.ok ? 'written' : memory.reason,
346
+ raw: String(decision.raw || '').slice(0, 500),
347
+ })
348
+ } catch (error) {
349
+ result.errors += 1
350
+ entry = {
351
+ at: Date.now(),
352
+ kind: 'weibo-post',
353
+ postId: post.id,
354
+ title: firstLine(post.text, 80),
355
+ action: 'error',
356
+ error: error?.message || String(error),
357
+ }
358
+ }
359
+ if (entry) {
360
+ pushHistory(state, entry)
361
+ result.items.push(entry)
362
+ log(
363
+ `[foxy-weibo] 博文 ${entry.postId} 《${entry.title}》 -> ${ACTION_LABELS[entry.action] || entry.action}` +
364
+ (entry.error ? `(${firstLine(entry.error, 100)})` : ''),
365
+ )
366
+ }
367
+ if (result.commented >= maxComments) break
368
+ await new Promise((resolve) => setTimeout(resolve, 600))
369
+ }
370
+ state.lastRun = Date.now()
371
+ state.lastResult = result
372
+ return result
373
+ }
374
+
161
375
  async function scanOwnComments(ctx, config, state, log, force = false, options = {}) {
162
376
  if (!config.enabled && !force) return { skipped: 'disabled' }
163
377
  const previewOnly = options.previewOnly === true
@@ -489,7 +703,7 @@ export function apply(ctx, rawConfig = {}) {
489
703
  const result =
490
704
  config.replyOwnPosts === false
491
705
  ? await scanOnce(ctx, config, state, log, force)
492
- : await scanOwnComments(ctx, config, state, log, force)
706
+ : await scanTargetPosts(ctx, config, state, log, force)
493
707
  saveState(state)
494
708
  log(`[foxy-weibo] 运行结束 ${JSON.stringify(result)}`)
495
709
  return result
@@ -507,9 +721,9 @@ export function apply(ctx, rawConfig = {}) {
507
721
  running = true
508
722
  try {
509
723
  log('[foxy-weibo] 开始立即检查')
510
- const result = await scanOwnComments(ctx, config, state, log, true, {
724
+ const result = await scanTargetPosts(ctx, config, state, log, true, {
511
725
  previewOnly: true,
512
- mode: 'own-comments-check',
726
+ mode: 'target-posts-check',
513
727
  })
514
728
  saveState(state)
515
729
  log(`[foxy-weibo] 立即检查结束 ${JSON.stringify(result)}`)
@@ -528,11 +742,10 @@ export function apply(ctx, rawConfig = {}) {
528
742
  running = true
529
743
  try {
530
744
  log(`[foxy-weibo] 开始立即回复${cid ? ` cid=${cid}` : ''}`)
531
- const result = await scanOwnComments(ctx, config, state, log, true, {
745
+ const result = await scanTargetPosts(ctx, config, state, log, true, {
532
746
  forceReply: true,
533
- maxReplies: 1,
534
- cid,
535
- mode: 'own-comments-reply-now',
747
+ maxPosts: 100,
748
+ mode: 'target-posts-reply-now',
536
749
  })
537
750
  saveState(state)
538
751
  log(`[foxy-weibo] 立即回复结束 ${JSON.stringify(result)}`)
package/lib/judge.js CHANGED
@@ -125,6 +125,7 @@ function finalize(parsed, input, fallbackText) {
125
125
  if (action === 'comment' && !comment) action = 'remember'
126
126
  let replyTo = input.mayReply ? String(parsed.replyTo || '0') : '0'
127
127
  if (input.targetComment?.cid) replyTo = String(input.targetComment.cid)
128
+ if (input.targetPost) replyTo = '0'
128
129
  return {
129
130
  action,
130
131
  comment: comment.slice(0, 300),
@@ -139,14 +140,20 @@ export async function judgePost(config, input) {
139
140
  const persona = readFoxyPersona(config)
140
141
  const prompt = [
141
142
  '你正在浏览微博,不是回复 QQ/微信,也不是客服。',
142
- '下面是一个微博账号的博文与评论。请用 Foxy 的判断,决定:',
143
+ input.targetPost
144
+ ? '下面是一条目标账号的博文。请阅读后判断是否值得在博文下发表一条评论:'
145
+ : '下面是一个微博账号的博文与评论。请用 Foxy 的判断,决定:',
143
146
  '- skip:广告、无意义、纯转发抽奖、引战、没信息量,跳过;',
144
147
  '- remember:有点意思,但不必公开评论,自己记一笔;',
145
148
  '- comment:值得在评论区说一句,才评论。',
146
149
  input.mayComment ? '本次允许评论。' : '本次不允许评论,只能 skip 或 remember。',
147
150
  '',
148
- '评论要求:像 Foxy 自己刷到后随口说的,中文第一人称;不设最少字数,能一句说清就一句,最多 80 字;不要客服腔,不要复述原文,不要写 @。',
149
- '记忆要求:如果值得记,用真实事件写第一人称,例如“我在微博看到 xx 说……,觉得……”。不要把所有事件都写成“我刷到”。不超过 180 字。',
151
+ input.targetPost
152
+ ? '评论是发在这条博文下的一级评论,不是回复某个人;不要写 @,不要假设对方在跟你对话。'
153
+ : '评论要求:像 Foxy 自己刷到后随口说的,中文第一人称;不设最少字数,能一句说清就一句,最多 80 字;不要客服腔,不要复述原文,不要写 @。',
154
+ input.targetPost
155
+ ? '评论要求:像一个了解对方的人看到这条博文后的自然回应;不设最少字数,能一句说清就一句,最多 80 字。'
156
+ : '记忆要求:如果值得记,用真实事件写第一人称,例如“我在微博看到 xx 说……,觉得……”。不要把所有事件都写成“我刷到”。不超过 180 字。',
150
157
  '',
151
158
  input.targetComment
152
159
  ? [
@@ -156,7 +163,7 @@ export async function judgePost(config, input) {
156
163
  '如果是广告、引战、辱骂、诈骗或无意义内容,输出 action=skip。',
157
164
  'replyTo 填这条评论的 cid,不要回复其他人。',
158
165
  ].join('\n')
159
- : input.mayReply && input.comments.length
166
+ : input.mayReply && Array.isArray(input.comments) && input.comments.length
160
167
  ? [
161
168
  '回复规则:可以回复下面某条评论。replyTo 填要回复的 cid;只发一级评论时填 0。',
162
169
  '不要回复自己,不要回复广告/引战/辱骂/诈骗。',
package/lib/settings.js CHANGED
@@ -8,7 +8,9 @@ export const DEFAULTS = {
8
8
  targetUid: '',
9
9
  targetUids: [],
10
10
  replyOwnPosts: true,
11
- replyMode: 'off', // off | auto
11
+ replyMode: 'off',
12
+ replyAfter: '',
13
+ learnPosts: true,
12
14
  scheduleTimes: ['20:00'],
13
15
  intervalMinutes: 60,
14
16
  maxPostsPerRun: 5,
@@ -30,6 +32,8 @@ export const SETTING_KEYS = [
30
32
  'targetUids',
31
33
  'replyOwnPosts',
32
34
  'replyMode',
35
+ 'replyAfter',
36
+ 'learnPosts',
33
37
  'scheduleTimes',
34
38
  'intervalMinutes',
35
39
  'maxPostsPerRun',
@@ -46,6 +50,7 @@ export const SETTING_KEYS = [
46
50
 
47
51
  const TEXT_KEYS = new Set([
48
52
  'targetUid',
53
+ 'replyAfter',
49
54
  'commentMode',
50
55
  'replyMode',
51
56
  'memoryPresetId',
@@ -56,6 +61,7 @@ const BOOLEAN_KEYS = new Set([
56
61
  'enabled',
57
62
  'replyOwnPosts',
58
63
  'replyOthers',
64
+ 'learnPosts',
59
65
  'enableMemory',
60
66
  'runOnStart',
61
67
  'debug',
@@ -124,6 +130,7 @@ export function normalizePatch(current = {}, patch = {}) {
124
130
  next.enabled = next.enabled === true
125
131
  next.replyOwnPosts = next.replyOwnPosts !== false
126
132
  next.replyOthers = next.replyOthers !== false
133
+ next.learnPosts = next.learnPosts !== false
127
134
  next.enableMemory = next.enableMemory !== false
128
135
  next.replyMode = next.replyMode === 'auto' ? 'auto' : 'off'
129
136
  next.targetUids = normalizeUids(next.targetUids)
package/lib/weibo.js CHANGED
@@ -79,10 +79,41 @@ async function readJson(response) {
79
79
  }
80
80
  }
81
81
 
82
+ export function parseWeiboTime(value, now = Date.now()) {
83
+ const text = String(value || '').trim()
84
+ if (!text) return 0
85
+ const current = new Date(now)
86
+ const at = (year, month, day, hour = 0, minute = 0) =>
87
+ new Date(year, month - 1, day, hour, minute).getTime()
88
+ let match = text.match(/^(\d+)\s*秒前/)
89
+ if (match) return now - Number(match[1]) * 1000
90
+ match = text.match(/^(\d+)\s*分钟前/)
91
+ if (match) return now - Number(match[1]) * 60_000
92
+ match = text.match(/^(\d+)\s*小时前/)
93
+ if (match) return now - Number(match[1]) * 3_600_000
94
+ if (/^刚刚/.test(text)) return now
95
+ match = text.match(/^今天\s*(\d{1,2}):(\d{2})/)
96
+ if (match) return at(current.getFullYear(), current.getMonth() + 1, current.getDate(), Number(match[1]), Number(match[2]))
97
+ match = text.match(/^昨天\s*(\d{1,2}):(\d{2})/)
98
+ if (match) {
99
+ const day = new Date(current.getFullYear(), current.getMonth(), current.getDate() - 1)
100
+ return at(day.getFullYear(), day.getMonth() + 1, day.getDate(), Number(match[1]), Number(match[2]))
101
+ }
102
+ match = text.match(/^(\d{4})-(\d{1,2})-(\d{1,2})(?:\s+(\d{1,2}):(\d{2}))?/)
103
+ if (match) return at(Number(match[1]), Number(match[2]), Number(match[3]), Number(match[4] || 0), Number(match[5] || 0))
104
+ match = text.match(/^(\d{1,2})-(\d{1,2})(?:\s+(\d{1,2}):(\d{2}))?/)
105
+ if (match) return at(current.getFullYear(), Number(match[1]), Number(match[2]), Number(match[3] || 0), Number(match[4] || 0))
106
+ match = text.match(/^(\d{1,2})月(\d{1,2})日(?:\s+(\d{1,2}):(\d{2}))?/)
107
+ if (match) return at(current.getFullYear(), Number(match[1]), Number(match[2]), Number(match[3] || 0), Number(match[4] || 0))
108
+ const parsed = Date.parse(text)
109
+ return Number.isFinite(parsed) ? parsed : 0
110
+ }
111
+
82
112
  export function normalizePost(raw) {
83
113
  const mblog = raw?.mblog || raw || {}
84
114
  const mid = String(mblog.mid || mblog.id || mblog.mblogid || '')
85
115
  const id = String(mblog.mblogid || mid)
116
+ const createdAt = String(mblog.created_at || '')
86
117
  return {
87
118
  id,
88
119
  mid,
@@ -92,7 +123,8 @@ export function normalizePost(raw) {
92
123
  uid: String(mblog.user?.id || mblog.user?.idstr || ''),
93
124
  name: String(mblog.user?.screen_name || ''),
94
125
  },
95
- createdAt: String(mblog.created_at || ''),
126
+ createdAt,
127
+ createdAtMs: parseWeiboTime(createdAt),
96
128
  commentsCount: Number(mblog.comments_count || 0),
97
129
  attitudesCount: Number(mblog.attitudes_count || 0),
98
130
  repostsCount: Number(mblog.reposts_count || 0),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-foxy-weibo",
3
- "version": "0.2.4",
3
+ "version": "0.3.0",
4
4
  "description": "Foxy's Weibo transport for DeepSeek Harness: follow users, read posts, comment, reply, and remember events.",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
@@ -48,5 +48,5 @@
48
48
  "test": "node test/test-weibo.cjs && node test/test-client.cjs"
49
49
  }
50
50
  }
51
-
52
-
51
+
52
+