koishi-plugin-foxy-daily 0.2.3 → 0.2.5

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 +48 -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,22 @@ 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
+ // Qzone can reuse a raw comment id for a root comment and a nested reply.
149
+ // Legacy state has only the raw id; prefer the reply sibling because old
150
+ // flows usually consumed the nested reply first.
151
+ const replySibling = siblings.find((item) => item.kind === 'reply')
152
+ if (replySibling) return replySibling === comment ? legacy : undefined
153
+ return siblings[0] === comment ? legacy : undefined
154
+ }
155
+
126
156
  async listComments(postId) {
127
157
  const posts = await this._targets(postId)
128
158
  const lines = []
@@ -133,7 +163,7 @@ class CommentRunner {
133
163
  const comments = flattenComments(post)
134
164
  if (comments.length === 0) lines.push(' (无评论)')
135
165
  for (const comment of comments) {
136
- const state = this.state.commentState(post.id, comment.id)
166
+ const state = this._commentState(post, comment, comments)
137
167
  const mark = state?.status === 'replied' ? '已回复' : state?.status === 'unknown' ? '待确认' : '未回复'
138
168
  const nested = comment.kind === 'reply'
139
169
  ? `,回复 ${comment.replyToUser?.nickname || comment.replyTo?.id || '?'}`
@@ -151,10 +181,11 @@ class CommentRunner {
151
181
  const post = await this.qzone.getPost(summary)
152
182
  if (!this._isOwnPost(post) && !this._otherPostAllowed(post)) continue
153
183
  const pending = []
154
- for (const comment of flattenComments(post)) {
184
+ const comments = flattenComments(post)
185
+ for (const comment of comments) {
155
186
  if (!this._commentAllowed(post, comment)) continue
156
187
  if (!this._commenterAllowed(post, comment)) continue
157
- const existing = this.state.commentState(post.id, comment.id)
188
+ const existing = this._commentState(post, comment, comments)
158
189
  if (existing && ['replied', 'processing', 'unknown', 'skipped'].includes(existing.status)) continue
159
190
  if ((existing?.attempts || 0) >= (this.config.commentMaxAttempts || 3)) continue
160
191
  const ageMs = comment.createdAt ? Date.now() - new Date(comment.createdAt).getTime() : 0
@@ -179,8 +210,9 @@ class CommentRunner {
179
210
  const comment = findComment(post, commentId)
180
211
  if (!comment) throw new Error(`动态 ${postId} 下找不到评论 ${commentId}`)
181
212
  if (!this._commentAllowed(post, comment)) throw new Error('这条评论不在允许回复的范围内')
213
+ const key = commentKey(comment)
182
214
  const result = await this.qzone.reply(post, comment, content)
183
- this.state.setCommentState(post.id, comment.id, {
215
+ this.state.setCommentState(post.id, key, {
184
216
  status: result.outcome === 'unknown' ? 'unknown' : 'replied',
185
217
  outcome: result.outcome,
186
218
  reply: content,
@@ -191,8 +223,9 @@ class CommentRunner {
191
223
  }
192
224
 
193
225
  async _reply(post, comment) {
194
- const existing = this.state.commentState(post.id, comment.id)
195
- this.state.setCommentState(post.id, comment.id, {
226
+ const key = commentKey(comment)
227
+ const existing = this._commentState(post, comment)
228
+ this.state.setCommentState(post.id, key, {
196
229
  status: 'processing',
197
230
  attempts: (existing?.attempts || 0) + 1,
198
231
  })
@@ -201,7 +234,7 @@ class CommentRunner {
201
234
  const persona = await this.getPersona()
202
235
  const content = await generateReply({ model, post, comment, persona })
203
236
  if (!content) {
204
- this.state.setCommentState(post.id, comment.id, {
237
+ this.state.setCommentState(post.id, key, {
205
238
  status: 'failed',
206
239
  attempts: (existing?.attempts || 0) + 1,
207
240
  error: 'empty reply',
@@ -210,7 +243,7 @@ class CommentRunner {
210
243
  return { replied: false, reason: 'empty' }
211
244
  }
212
245
  const result = await this.qzone.reply(post, comment, content)
213
- this.state.setCommentState(post.id, comment.id, {
246
+ this.state.setCommentState(post.id, key, {
214
247
  status: result.outcome === 'unknown' ? 'unknown' : 'replied',
215
248
  outcome: result.outcome,
216
249
  reply: content,
@@ -232,18 +265,19 @@ class CommentRunner {
232
265
  for (const summary of posts) {
233
266
  const post = await this.qzone.getPost(summary)
234
267
  if (!this._isOwnPost(post) && !this._otherPostAllowed(post)) continue
235
- for (const comment of flattenComments(post)) {
268
+ const comments = flattenComments(post)
269
+ for (const comment of comments) {
236
270
  if (!this._commentAllowed(post, comment)) continue
237
271
  if (!this._commenterAllowed(post, comment)) {
238
272
  report.skipped++
239
273
  continue
240
274
  }
241
- const existing = this.state.commentState(post.id, comment.id)
275
+ const existing = this._commentState(post, comment, comments)
242
276
  if (existing && ['replied', 'processing', 'unknown', 'skipped'].includes(existing.status)) continue
243
277
  if ((existing?.attempts || 0) >= (this.config.commentMaxAttempts || 3)) continue
244
278
  const ageMs = comment.createdAt ? Date.now() - new Date(comment.createdAt).getTime() : 0
245
279
  if (ageMs > (this.config.commentMaxAgeHours || 72) * 3600_000) {
246
- this.state.setCommentState(post.id, comment.id, { status: 'skipped', reason: 'too-old' })
280
+ this.state.setCommentState(post.id, commentKey(comment), { status: 'skipped', reason: 'too-old' })
247
281
  await this.state.save()
248
282
  report.skipped++
249
283
  continue
@@ -259,7 +293,7 @@ class CommentRunner {
259
293
  report.skipped++
260
294
  }
261
295
  } catch (error) {
262
- this.state.setCommentState(post.id, comment.id, {
296
+ this.state.setCommentState(post.id, commentKey(comment), {
263
297
  status: 'failed',
264
298
  attempts: (existing?.attempts || 0) + 1,
265
299
  error: error.message,
@@ -276,4 +310,4 @@ class CommentRunner {
276
310
  }
277
311
  }
278
312
 
279
- module.exports = { CommentRunner, flattenComments, findComment }
313
+ 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.5",
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": [