koishi-plugin-foxy-daily 0.2.3 → 0.2.4

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 (2) hide show
  1. package/lib/comments.js +43 -14
  2. package/package.json +1 -1
package/lib/comments.js CHANGED
@@ -1,5 +1,6 @@
1
1
  'use strict'
2
2
 
3
+ const { createHash } = require('node:crypto')
3
4
  const { generateReply } = require('./digest')
4
5
 
5
6
  function formatAuthor(comment) {
@@ -10,8 +11,21 @@ function flattenComments(post) {
10
11
  return Array.isArray(post.comments) ? post.comments : []
11
12
  }
12
13
 
14
+ function commentKey(comment) {
15
+ const id = String(comment?.id || '')
16
+ const kind = String(comment?.kind || 'comment')
17
+ const target = String(comment?.replyTo?.id ?? comment?.replyToUser?.id ?? comment?.parentId ?? '')
18
+ const author = String(comment?.author?.id || '')
19
+ const content = String(comment?.content || '')
20
+ const hash = createHash('sha1').update(content).digest('hex').slice(0, 10)
21
+ return `${id}:${kind}:${target}:${author}:${hash}`
22
+ }
23
+
13
24
  function findComment(post, commentId) {
14
- return flattenComments(post).find((comment) => String(comment.id) === String(commentId))
25
+ const value = String(commentId)
26
+ return flattenComments(post).find(
27
+ (comment) => commentKey(comment) === value || String(comment.id) === value,
28
+ )
15
29
  }
16
30
 
17
31
  function normalizeIds(values) {
@@ -123,6 +137,17 @@ class CommentRunner {
123
137
  return posts
124
138
  }
125
139
 
140
+ _commentState(post, comment, comments = flattenComments(post)) {
141
+ const stableKey = commentKey(comment)
142
+ const direct = this.state.commentState(post.id, stableKey)
143
+ if (direct) return direct
144
+ const legacy = this.state.commentState(post.id, String(comment.id))
145
+ if (!legacy) return undefined
146
+ const siblings = comments.filter((item) => String(item.id) === String(comment.id))
147
+ if (siblings.length <= 1) return legacy
148
+ return siblings[0] === comment ? legacy : undefined
149
+ }
150
+
126
151
  async listComments(postId) {
127
152
  const posts = await this._targets(postId)
128
153
  const lines = []
@@ -133,7 +158,7 @@ class CommentRunner {
133
158
  const comments = flattenComments(post)
134
159
  if (comments.length === 0) lines.push(' (无评论)')
135
160
  for (const comment of comments) {
136
- const state = this.state.commentState(post.id, comment.id)
161
+ const state = this._commentState(post, comment, comments)
137
162
  const mark = state?.status === 'replied' ? '已回复' : state?.status === 'unknown' ? '待确认' : '未回复'
138
163
  const nested = comment.kind === 'reply'
139
164
  ? `,回复 ${comment.replyToUser?.nickname || comment.replyTo?.id || '?'}`
@@ -151,10 +176,11 @@ class CommentRunner {
151
176
  const post = await this.qzone.getPost(summary)
152
177
  if (!this._isOwnPost(post) && !this._otherPostAllowed(post)) continue
153
178
  const pending = []
154
- for (const comment of flattenComments(post)) {
179
+ const comments = flattenComments(post)
180
+ for (const comment of comments) {
155
181
  if (!this._commentAllowed(post, comment)) continue
156
182
  if (!this._commenterAllowed(post, comment)) continue
157
- const existing = this.state.commentState(post.id, comment.id)
183
+ const existing = this._commentState(post, comment, comments)
158
184
  if (existing && ['replied', 'processing', 'unknown', 'skipped'].includes(existing.status)) continue
159
185
  if ((existing?.attempts || 0) >= (this.config.commentMaxAttempts || 3)) continue
160
186
  const ageMs = comment.createdAt ? Date.now() - new Date(comment.createdAt).getTime() : 0
@@ -179,8 +205,9 @@ class CommentRunner {
179
205
  const comment = findComment(post, commentId)
180
206
  if (!comment) throw new Error(`动态 ${postId} 下找不到评论 ${commentId}`)
181
207
  if (!this._commentAllowed(post, comment)) throw new Error('这条评论不在允许回复的范围内')
208
+ const key = commentKey(comment)
182
209
  const result = await this.qzone.reply(post, comment, content)
183
- this.state.setCommentState(post.id, comment.id, {
210
+ this.state.setCommentState(post.id, key, {
184
211
  status: result.outcome === 'unknown' ? 'unknown' : 'replied',
185
212
  outcome: result.outcome,
186
213
  reply: content,
@@ -191,8 +218,9 @@ class CommentRunner {
191
218
  }
192
219
 
193
220
  async _reply(post, comment) {
194
- const existing = this.state.commentState(post.id, comment.id)
195
- this.state.setCommentState(post.id, comment.id, {
221
+ const key = commentKey(comment)
222
+ const existing = this._commentState(post, comment)
223
+ this.state.setCommentState(post.id, key, {
196
224
  status: 'processing',
197
225
  attempts: (existing?.attempts || 0) + 1,
198
226
  })
@@ -201,7 +229,7 @@ class CommentRunner {
201
229
  const persona = await this.getPersona()
202
230
  const content = await generateReply({ model, post, comment, persona })
203
231
  if (!content) {
204
- this.state.setCommentState(post.id, comment.id, {
232
+ this.state.setCommentState(post.id, key, {
205
233
  status: 'failed',
206
234
  attempts: (existing?.attempts || 0) + 1,
207
235
  error: 'empty reply',
@@ -210,7 +238,7 @@ class CommentRunner {
210
238
  return { replied: false, reason: 'empty' }
211
239
  }
212
240
  const result = await this.qzone.reply(post, comment, content)
213
- this.state.setCommentState(post.id, comment.id, {
241
+ this.state.setCommentState(post.id, key, {
214
242
  status: result.outcome === 'unknown' ? 'unknown' : 'replied',
215
243
  outcome: result.outcome,
216
244
  reply: content,
@@ -232,18 +260,19 @@ class CommentRunner {
232
260
  for (const summary of posts) {
233
261
  const post = await this.qzone.getPost(summary)
234
262
  if (!this._isOwnPost(post) && !this._otherPostAllowed(post)) continue
235
- for (const comment of flattenComments(post)) {
263
+ const comments = flattenComments(post)
264
+ for (const comment of comments) {
236
265
  if (!this._commentAllowed(post, comment)) continue
237
266
  if (!this._commenterAllowed(post, comment)) {
238
267
  report.skipped++
239
268
  continue
240
269
  }
241
- const existing = this.state.commentState(post.id, comment.id)
270
+ const existing = this._commentState(post, comment, comments)
242
271
  if (existing && ['replied', 'processing', 'unknown', 'skipped'].includes(existing.status)) continue
243
272
  if ((existing?.attempts || 0) >= (this.config.commentMaxAttempts || 3)) continue
244
273
  const ageMs = comment.createdAt ? Date.now() - new Date(comment.createdAt).getTime() : 0
245
274
  if (ageMs > (this.config.commentMaxAgeHours || 72) * 3600_000) {
246
- this.state.setCommentState(post.id, comment.id, { status: 'skipped', reason: 'too-old' })
275
+ this.state.setCommentState(post.id, commentKey(comment), { status: 'skipped', reason: 'too-old' })
247
276
  await this.state.save()
248
277
  report.skipped++
249
278
  continue
@@ -259,7 +288,7 @@ class CommentRunner {
259
288
  report.skipped++
260
289
  }
261
290
  } catch (error) {
262
- this.state.setCommentState(post.id, comment.id, {
291
+ this.state.setCommentState(post.id, commentKey(comment), {
263
292
  status: 'failed',
264
293
  attempts: (existing?.attempts || 0) + 1,
265
294
  error: error.message,
@@ -276,4 +305,4 @@ class CommentRunner {
276
305
  }
277
306
  }
278
307
 
279
- module.exports = { CommentRunner, flattenComments, findComment }
308
+ module.exports = { CommentRunner, commentKey, flattenComments, findComment }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "koishi-plugin-foxy-daily",
3
- "version": "0.2.3",
3
+ "version": "0.2.4",
4
4
  "description": "LivingMemory-backed Qzone daily posts, nested comments, auto-like and public-feed tools for ChatLuna / Character.",
5
5
  "main": "lib/index.js",
6
6
  "files": [