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.
- package/lib/comments.js +48 -14
- 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
|
-
|
|
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.
|
|
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
|
-
|
|
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.
|
|
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,
|
|
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
|
|
195
|
-
this.
|
|
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,
|
|
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,
|
|
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
|
-
|
|
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.
|
|
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
|
|
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
|
|
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