@luoyi-dsh/instagram-scroll 1.0.1 → 1.0.6
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/AGENTS.md +92 -0
- package/README.md +77 -172
- package/README.zh-Hant.md +73 -174
- package/client.js +261 -33
- package/dist/config.d.ts +13 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +156 -9
- package/dist/config.js.map +1 -1
- package/dist/core/scrollDuty.d.ts +30 -1
- package/dist/core/scrollDuty.d.ts.map +1 -1
- package/dist/core/scrollDuty.js +563 -74
- package/dist/core/scrollDuty.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +21 -2
- package/dist/index.js.map +1 -1
- package/dist/lib/ledger.d.ts +73 -10
- package/dist/lib/ledger.d.ts.map +1 -1
- package/dist/lib/ledger.js +402 -22
- package/dist/lib/ledger.js.map +1 -1
- package/package.json +4 -3
package/dist/core/scrollDuty.js
CHANGED
|
@@ -8,76 +8,431 @@
|
|
|
8
8
|
* ============================================================================
|
|
9
9
|
*/
|
|
10
10
|
/**
|
|
11
|
-
*
|
|
12
|
-
*
|
|
11
|
+
* 正文解析與標籤分離器
|
|
12
|
+
* 徹底區分貼文真正的正文內容與文末堆砌的標籤 (防止標籤農場誤導)
|
|
13
13
|
*/
|
|
14
|
-
function
|
|
15
|
-
const
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
14
|
+
export function parsePostContent(caption = '') {
|
|
15
|
+
const rawCaption = String(caption || '').trim();
|
|
16
|
+
// 提取所有 hashtag 與 mention
|
|
17
|
+
const hashtags = (rawCaption.match(/#[\p{L}\p{N}_]+/gu) || []).map((t) => t.slice(1).toLowerCase());
|
|
18
|
+
const mentions = (rawCaption.match(/@[\p{L}\p{N}._]+/gu) || []).map((m) => m.slice(1));
|
|
19
|
+
// 移除所有 #tag 與 @mention,整理出真正的正文文字
|
|
20
|
+
let cleanBody = rawCaption
|
|
21
|
+
.replace(/#[\p{L}\p{N}_]+/gu, ' ')
|
|
22
|
+
.replace(/@[\p{L}\p{N}._]+/gu, ' ')
|
|
23
|
+
.replace(/[ \t]+/g, ' ')
|
|
24
|
+
.replace(/\n\s*\n+/g, '\n')
|
|
25
|
+
.trim();
|
|
26
|
+
const cleanLength = cleanBody.length;
|
|
27
|
+
const tagCount = hashtags.length;
|
|
28
|
+
const tagCharLength = hashtags.reduce((acc, tag) => acc + tag.length, 0);
|
|
29
|
+
// 判定是否為標籤農場 (Hashtag Spam):
|
|
30
|
+
// 1. 標籤數量超過 12 個
|
|
31
|
+
// 2. 標籤總長度超越正文長度且正文字數小於 40 字
|
|
32
|
+
const isTagSpam = tagCount > 12 || (tagCount >= 6 && tagCharLength > cleanLength && cleanLength < 40);
|
|
33
|
+
return {
|
|
34
|
+
rawCaption,
|
|
35
|
+
cleanBody,
|
|
36
|
+
hashtags,
|
|
37
|
+
mentions,
|
|
38
|
+
tagCount,
|
|
39
|
+
tagCharLength,
|
|
40
|
+
cleanLength,
|
|
41
|
+
isTagSpam,
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
const AIGC_REJECT_KEYWORDS = [
|
|
45
|
+
'ai生成',
|
|
46
|
+
'ai 生成',
|
|
47
|
+
'ai影片',
|
|
48
|
+
'ai 影片',
|
|
49
|
+
'ai視頻',
|
|
50
|
+
'ai 視頻',
|
|
51
|
+
'ai動畫',
|
|
52
|
+
'ai 動畫',
|
|
53
|
+
'ai動漫',
|
|
54
|
+
'ai 動漫',
|
|
55
|
+
'ai繪圖',
|
|
56
|
+
'ai 繪圖',
|
|
57
|
+
'ai畫',
|
|
58
|
+
'ai繪畫',
|
|
59
|
+
'ai算圖',
|
|
60
|
+
'ai美女',
|
|
61
|
+
'ai模特',
|
|
62
|
+
'ai寫作',
|
|
63
|
+
'ai文章',
|
|
64
|
+
'ai生成文章',
|
|
65
|
+
'ai配音',
|
|
66
|
+
'ai翻唱',
|
|
67
|
+
'ai漫改',
|
|
68
|
+
'ai短劇',
|
|
69
|
+
'aigc',
|
|
70
|
+
'midjourney',
|
|
71
|
+
'stable diffusion',
|
|
72
|
+
'stablediffusion',
|
|
73
|
+
'comfyui',
|
|
74
|
+
'sora',
|
|
75
|
+
'runwayml',
|
|
76
|
+
'runway gen',
|
|
77
|
+
'kling ai',
|
|
78
|
+
'klingai',
|
|
79
|
+
'可靈',
|
|
80
|
+
'即夢',
|
|
81
|
+
'luma dream',
|
|
82
|
+
'lumalabs',
|
|
83
|
+
'pika art',
|
|
84
|
+
'pika labs',
|
|
85
|
+
'hailuo ai',
|
|
86
|
+
'海螺ai',
|
|
87
|
+
'suno ai',
|
|
88
|
+
'udio',
|
|
89
|
+
'ai art',
|
|
90
|
+
'aiart',
|
|
91
|
+
'aiartist',
|
|
92
|
+
'ai video',
|
|
93
|
+
'aivideo',
|
|
94
|
+
'ai animation',
|
|
95
|
+
'aianimation',
|
|
96
|
+
'ai generated',
|
|
97
|
+
'ai-generated',
|
|
98
|
+
'aigenerated',
|
|
99
|
+
'ai girl',
|
|
100
|
+
'aigirl',
|
|
101
|
+
'ai artwork',
|
|
102
|
+
'generated by ai',
|
|
103
|
+
'made with ai',
|
|
104
|
+
'created with ai',
|
|
105
|
+
];
|
|
106
|
+
function detectAigcPost(caption = '', username = '') {
|
|
107
|
+
const textLower = `${caption || ''} ${username || ''}`.toLowerCase();
|
|
108
|
+
for (const kw of AIGC_REJECT_KEYWORDS) {
|
|
109
|
+
if (kw && textLower.includes(kw.toLowerCase())) {
|
|
110
|
+
return kw;
|
|
25
111
|
}
|
|
26
112
|
}
|
|
27
|
-
|
|
28
|
-
const
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
if (
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
score -= 35;
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
score = Math.min(98, Math.max(20, score));
|
|
49
|
-
const isWorthy = score >= (config.minScore || 75);
|
|
50
|
-
let reason = '';
|
|
51
|
-
let commentText = '';
|
|
52
|
-
const master = (config.masterUsername || '').replace(/^@/, '').trim();
|
|
53
|
-
if (isWorthy) {
|
|
54
|
-
if (hitTags.length > 0) {
|
|
55
|
-
reason = `精準命中主人關注領域 [#${hitTags.join(', #')}],內容高質且社群互動熱絡 (${likes} 讚, ${comments} 則留言)。`;
|
|
56
|
-
const hookPhrases = [
|
|
57
|
-
'這篇思路與案例很不錯,特別推薦你看一下!',
|
|
58
|
-
'發現這個很對你的胃口,覺得對你手邊專案很有幫助!',
|
|
59
|
-
'這篇乾貨滿滿,看到覺得不能錯過,推推!',
|
|
60
|
-
'這篇分析非常到位,特地 @ 你存起來參考!',
|
|
61
|
-
];
|
|
62
|
-
const chosenHook = hookPhrases[Math.floor(Math.random() * hookPhrases.length)];
|
|
63
|
-
commentText = master ? `@${master} ${chosenHook}` : `這篇乾貨非常精彩,推薦關注!`;
|
|
113
|
+
const aigcTagRegex = /#(aiart|aivideo|aianimation|aigenerated|midjourney|stablediffusion|sora|runwayml|klingai|pikaart|aigirl|aigc|ai繪圖|ai動畫|ai影片|ai生成)/i;
|
|
114
|
+
const m = textLower.match(aigcTagRegex);
|
|
115
|
+
if (m)
|
|
116
|
+
return m[0];
|
|
117
|
+
return null;
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* 調用共享外部插件訂閱額度 (dsh-plugin-subscriptions / __DSH_INSTAGRAM_LLM__)、宿主 LLM 或自訂 API 進行深度語義內容審查
|
|
121
|
+
*/
|
|
122
|
+
async function callLlmInference(ctx, systemPrompt, userPrompt) {
|
|
123
|
+
// 0. 優先使用 dsh-instagram-plugin 共享的統一 LLM 引擎 (支援讀取 dsh-plugin-subscriptions 外部額度)
|
|
124
|
+
const sharedIgLlm = globalThis.__DSH_INSTAGRAM_LLM__;
|
|
125
|
+
if (sharedIgLlm && typeof sharedIgLlm.callLlmForInstagram === 'function') {
|
|
126
|
+
try {
|
|
127
|
+
const sharedCfg = globalThis.__DSH_INSTAGRAM_PLUGIN_CONFIG__ || {};
|
|
128
|
+
const res = await sharedIgLlm.callLlmForInstagram(ctx, sharedCfg, systemPrompt, userPrompt);
|
|
129
|
+
if (res && res.trim())
|
|
130
|
+
return res.trim();
|
|
64
131
|
}
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
132
|
+
catch (e) {
|
|
133
|
+
console.warn(`[ScrollDuty LLM] 共享訂閱 LLM 調用異常: ${e.message}`);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
// 1. 嘗試 DSH 宿主注入的 LLM 服務
|
|
137
|
+
const llm = (typeof ctx?.get === 'function' ? ctx.get('llm') : null) || ctx?.llm;
|
|
138
|
+
if (llm) {
|
|
139
|
+
try {
|
|
140
|
+
if (typeof llm.chat === 'function') {
|
|
141
|
+
const res = await llm.chat({
|
|
142
|
+
messages: [
|
|
143
|
+
{ role: 'system', content: systemPrompt },
|
|
144
|
+
{ role: 'user', content: userPrompt },
|
|
145
|
+
],
|
|
146
|
+
temperature: 0.3,
|
|
147
|
+
});
|
|
148
|
+
const reply = res?.content || res?.text || (typeof res === 'string' ? res : '');
|
|
149
|
+
if (reply)
|
|
150
|
+
return reply;
|
|
151
|
+
}
|
|
152
|
+
if (typeof llm.complete === 'function') {
|
|
153
|
+
const reply = await llm.complete(`${systemPrompt}\n\n${userPrompt}`);
|
|
154
|
+
if (reply)
|
|
155
|
+
return reply;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
catch (e) {
|
|
159
|
+
console.warn(`[ScrollDuty LLM] 宿主 ctx.llm 調用異常: ${e.message}`);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
// 2. 檢查備援環境變數 (DeepSeek / OpenAI)
|
|
163
|
+
const apiKey = process.env.DEEPSEEK_API_KEY || process.env.OPENAI_API_KEY;
|
|
164
|
+
const baseUrl = process.env.DEEPSEEK_API_BASE || process.env.OPENAI_BASE_URL || 'https://api.deepseek.com';
|
|
165
|
+
if (apiKey) {
|
|
166
|
+
try {
|
|
167
|
+
const resp = await fetch(`${baseUrl}/chat/completions`, {
|
|
168
|
+
method: 'POST',
|
|
169
|
+
headers: {
|
|
170
|
+
'Content-Type': 'application/json',
|
|
171
|
+
Authorization: `Bearer ${apiKey}`,
|
|
172
|
+
},
|
|
173
|
+
body: JSON.stringify({
|
|
174
|
+
model: process.env.DEEPSEEK_MODEL || 'deepseek-chat',
|
|
175
|
+
messages: [
|
|
176
|
+
{ role: 'system', content: systemPrompt },
|
|
177
|
+
{ role: 'user', content: userPrompt },
|
|
178
|
+
],
|
|
179
|
+
temperature: 0.3,
|
|
180
|
+
response_format: { type: 'json_object' },
|
|
181
|
+
}),
|
|
182
|
+
});
|
|
183
|
+
if (resp.ok) {
|
|
184
|
+
const data = (await resp.json());
|
|
185
|
+
return data.choices?.[0]?.message?.content || null;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
catch (err) {
|
|
189
|
+
console.warn(`[ScrollDuty LLM] 外部 API 調用異常: ${err.message}`);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
return null;
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* 嚴密的高精度內容檢驗引擎 (Content Inspection Pipeline)
|
|
196
|
+
* 解決痛點:「不要隨便推薦、要依據用戶喜歡內容、@ 前要檢查貼文內容、不要只看 tag」
|
|
197
|
+
*/
|
|
198
|
+
export async function inspectAndEvaluatePost(ctx, post, config) {
|
|
199
|
+
const mcpSvc = globalThis.__DSH_INSTAGRAM_MCP__;
|
|
200
|
+
const master = (mcpSvc?.config?.masterUsername || config.masterUsername || '').replace(/^@/, '').trim();
|
|
201
|
+
const parsed = parsePostContent(post.caption || '');
|
|
202
|
+
const minScore = config.minScore || 80;
|
|
203
|
+
const minLength = config.minCaptionLength || 30;
|
|
204
|
+
const userInterests = config.userInterests ||
|
|
205
|
+
'專注於前沿 AI 技術、開源軟體、全端與前端架構、實用開發工具、優質 UI/UX 設計技巧或實用技術深度解析;喜愛實踐乾貨與原創見解。';
|
|
206
|
+
// === 第 1 關:正文實質內容字數檢驗 (拒絕無內文或純圖廢文) ===
|
|
207
|
+
if (parsed.cleanLength < minLength) {
|
|
208
|
+
return {
|
|
209
|
+
isWorthy: false,
|
|
210
|
+
score: 15,
|
|
211
|
+
reason: '',
|
|
212
|
+
rejectionReason: `正文有效字數不足 (${parsed.cleanLength} 字 < 門檻 ${minLength} 字),資訊量過少無實質乾貨內容。`,
|
|
213
|
+
commentText: '',
|
|
214
|
+
reviewMethod: 'heuristic',
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
// === 第 2 關:標籤農場檢查 (拒絕標籤堆砌蹭熱度) ===
|
|
218
|
+
if (parsed.isTagSpam) {
|
|
219
|
+
return {
|
|
220
|
+
isWorthy: false,
|
|
221
|
+
score: 20,
|
|
222
|
+
reason: '',
|
|
223
|
+
rejectionReason: `判定為標籤農場 (包含 ${parsed.tagCount} 個標籤,標籤佔比過高且正文空洞)。`,
|
|
224
|
+
commentText: '',
|
|
225
|
+
reviewMethod: 'heuristic',
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
// === 第 2.5 關:嚴格攔截 AI 生成的影片、動畫、繪圖或農場文章 (AIGC Filter) ===
|
|
229
|
+
const aigcHit = detectAigcPost(parsed.rawCaption, post.user?.username || '');
|
|
230
|
+
if (aigcHit) {
|
|
231
|
+
return {
|
|
232
|
+
isWorthy: false,
|
|
233
|
+
score: 5,
|
|
234
|
+
reason: '',
|
|
235
|
+
rejectionReason: `判定為 AI 生成的影片/動畫/繪圖或文章 (命中 AIGC 特徵詞 [${aigcHit}]),不予推薦。`,
|
|
236
|
+
commentText: '',
|
|
237
|
+
reviewMethod: 'heuristic',
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
// === 第 3 關:負面詞與廣告過濾 (Negative Keywords / Spam Filter) ===
|
|
241
|
+
const negativeWords = config.negativeKeywords || [];
|
|
242
|
+
const rawLower = parsed.rawCaption.toLowerCase();
|
|
243
|
+
for (const badKw of negativeWords) {
|
|
244
|
+
const cleanKw = badKw.toLowerCase().trim();
|
|
245
|
+
if (cleanKw && rawLower.includes(cleanKw)) {
|
|
246
|
+
return {
|
|
247
|
+
isWorthy: false,
|
|
248
|
+
score: 10,
|
|
249
|
+
reason: '',
|
|
250
|
+
rejectionReason: `命中排除關鍵字/廣告宣傳詞彙 [${cleanKw}]。`,
|
|
251
|
+
commentText: '',
|
|
252
|
+
reviewMethod: 'heuristic',
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
// === 第 4 關:正文實質主題核驗 (不能只看文末 tag) ===
|
|
257
|
+
const positiveWords = Array.from(new Set([
|
|
258
|
+
...(config.positiveKeywords || []),
|
|
259
|
+
...(config.fuzzyKeywords || []),
|
|
260
|
+
...(config.interestTags || []),
|
|
261
|
+
]));
|
|
262
|
+
const cleanLower = parsed.cleanBody.toLowerCase();
|
|
263
|
+
const hitBodyKeywords = [];
|
|
264
|
+
for (const kw of positiveWords) {
|
|
265
|
+
const cleanKw = kw.toLowerCase().trim();
|
|
266
|
+
if (cleanKw && cleanLower.includes(cleanKw)) {
|
|
267
|
+
hitBodyKeywords.push(cleanKw);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
// 若設定要求「正文必須命中用戶偏好」,且正文未命中任何關鍵字,直接退回
|
|
271
|
+
if (config.requireBodyMatch && hitBodyKeywords.length === 0) {
|
|
272
|
+
return {
|
|
273
|
+
isWorthy: false,
|
|
274
|
+
score: 30,
|
|
275
|
+
reason: '',
|
|
276
|
+
rejectionReason: `正文實質內容未提及主人偏好領域關鍵字 (僅文末標籤包含相關字詞,判定為蹭熱度)。`,
|
|
277
|
+
commentText: '',
|
|
278
|
+
reviewMethod: 'heuristic',
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
// === 第 5 關:LLM 深度語義質檢 (如果環境具備 LLM 能力) ===
|
|
282
|
+
const sharedCompanion = globalThis.__DSH_INSTAGRAM_LLM__;
|
|
283
|
+
const moodBlock = typeof sharedCompanion?.buildFeiyuMoodPromptBlock === 'function'
|
|
284
|
+
? sharedCompanion.buildFeiyuMoodPromptBlock(true)
|
|
285
|
+
: '';
|
|
286
|
+
const antiAiRule = sharedCompanion?.ANTI_AI_FLAVOR_PRIMARY_RULE ||
|
|
287
|
+
'【第一條件(最高優先級鐵則):推坑留言絕對不要 AI 味,嚴禁客服腔、總結腔、排比句或機械式吹捧,要像真人朋友隨手分享一樣自然口語】';
|
|
288
|
+
const systemPrompt = `${antiAiRule}\n${moodBlock ? `${moodBlock}\n` : ''}\n你是一位高品味、眼光極為嚴格的私人內容審查夥伴。
|
|
289
|
+
主人的核心原則是:「絕對不要隨便推薦、必須切合主人喜歡的內容,在留言 @ 主人之前必須深入檢查正文實質內容,嚴禁只看文末的 tag 就隨意推薦」。
|
|
290
|
+
主人的內容偏好定義如下:
|
|
291
|
+
"""
|
|
292
|
+
${userInterests}
|
|
293
|
+
"""
|
|
294
|
+
|
|
295
|
+
任務要求:
|
|
296
|
+
1. 【最高鐵律】嚴格排除任何「AI 生成的影片、AI 動畫、AI 繪圖/算圖、AI 配音或 AI 生成的農場文章」(例如 Sora、Runway、Midjourney、Stable Diffusion、Kling、可靈、即夢、AIGC 等)。只要貼文是 AI 算圖、AI 生成影片/動畫或 AI 農場文,必須無條件給低分並設 isWorthy: false!
|
|
297
|
+
2. 請審查貼文正文是否深度探討主人喜歡的主題(真實人類原創手繪創作、真實技術開發實踐、開源專案或優質乾貨),還是只是空泛的標籤農場或蹭流量。
|
|
298
|
+
3. 若內容確實極為優秀且深度符合主人喜好,請給出大於等於 ${minScore} 的高分,並撰寫簡短真誠、針對此篇貼文具體乾貨的一句話推坑留言 (不要千篇一律套話)。
|
|
299
|
+
4. 若品質普通、偏題或僅表面蹭熱度,給予低於 ${minScore} 的分數並給出 rejectionReason。
|
|
300
|
+
|
|
301
|
+
請嚴格輸出 JSON 格式:
|
|
302
|
+
{
|
|
303
|
+
"isWorthy": boolean,
|
|
304
|
+
"score": number, // 0-100
|
|
305
|
+
"summary": "貼文真正主旨簡介 (30字內)",
|
|
306
|
+
"rejectionReason": "未通過的原因 (若不合格)",
|
|
307
|
+
"reason": "推薦給主人的實質價值說明 (若合格)",
|
|
308
|
+
"hookPhrase": "針對本篇內容亮點量身打造的推坑一句話 (若合格,25字內,真誠具體,嚴禁空洞廢話)"
|
|
309
|
+
}`;
|
|
310
|
+
const userPrompt = `請審查以下 Instagram 貼文:
|
|
311
|
+
- 作者:@${post.user?.username || 'unknown'}
|
|
312
|
+
- 互動數據:${post.like_count || 0} 讚, ${post.comment_count || 0} 則留言
|
|
313
|
+
- 貼文真正正文內容 (已剔除標籤):
|
|
314
|
+
"""
|
|
315
|
+
${parsed.cleanBody.slice(0, 800)}
|
|
316
|
+
"""
|
|
317
|
+
- 文末標籤清單:#${parsed.hashtags.join(', #')}
|
|
318
|
+
- 正文命中之領域詞:${hitBodyKeywords.join(', ') || '無'}
|
|
319
|
+
|
|
320
|
+
請依據審查原則進行嚴格評估並以 JSON 返回。`;
|
|
321
|
+
const llmResponse = await callLlmInference(ctx, systemPrompt, userPrompt);
|
|
322
|
+
if (llmResponse) {
|
|
323
|
+
try {
|
|
324
|
+
const cleanedJson = llmResponse.replace(/^```json\s*/i, '').replace(/\s*```$/i, '').trim();
|
|
325
|
+
const parsedLlm = JSON.parse(cleanedJson);
|
|
326
|
+
const score = Number(parsedLlm.score || 0);
|
|
327
|
+
const isWorthy = Boolean(parsedLlm.isWorthy) && score >= minScore;
|
|
328
|
+
if (isWorthy) {
|
|
329
|
+
const rawHook = String(parsedLlm.hookPhrase || parsedLlm.reason || '這篇講得蠻透的,留給你看看').trim();
|
|
330
|
+
const hook = typeof sharedCompanion?.stripAiFlavorArtifacts === 'function'
|
|
331
|
+
? sharedCompanion.stripAiFlavorArtifacts(rawHook)
|
|
332
|
+
: rawHook;
|
|
333
|
+
const commentText = master ? `@${master} ${hook}` : hook;
|
|
334
|
+
return {
|
|
335
|
+
isWorthy: true,
|
|
336
|
+
score,
|
|
337
|
+
reason: String(parsedLlm.reason || parsedLlm.summary || '內容深度契合主人關注領域且具備乾貨價值。'),
|
|
338
|
+
commentText,
|
|
339
|
+
reviewMethod: 'llm',
|
|
340
|
+
};
|
|
341
|
+
}
|
|
342
|
+
else {
|
|
343
|
+
return {
|
|
344
|
+
isWorthy: false,
|
|
345
|
+
score,
|
|
346
|
+
reason: '',
|
|
347
|
+
rejectionReason: String(parsedLlm.rejectionReason || '內容深度未達主人品味標準或主題不符。'),
|
|
348
|
+
commentText: '',
|
|
349
|
+
reviewMethod: 'llm',
|
|
350
|
+
};
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
catch (e) {
|
|
354
|
+
console.warn(`[ScrollDuty] LLM 結果 JSON 解析異常: ${e.message}`);
|
|
68
355
|
}
|
|
69
356
|
}
|
|
70
|
-
|
|
357
|
+
// === 備援:啟發式嚴密內容質檢 (Heuristic Rule Engine) ===
|
|
358
|
+
// 當宿主無 LLM 或 LLM 調用異常時,依然嚴守品質底線,絕不隨便推薦!
|
|
359
|
+
let heuristicScore = 40; // 底分
|
|
360
|
+
// 正文字數深度加分 (有較長詳細說明通常具備更高乾貨密度)
|
|
361
|
+
if (parsed.cleanLength > 300)
|
|
362
|
+
heuristicScore += 18;
|
|
363
|
+
else if (parsed.cleanLength > 150)
|
|
364
|
+
heuristicScore += 12;
|
|
365
|
+
else if (parsed.cleanLength > 60)
|
|
366
|
+
heuristicScore += 6;
|
|
367
|
+
// 正文實質命中關鍵詞加分
|
|
368
|
+
heuristicScore += Math.min(25, hitBodyKeywords.length * 8);
|
|
369
|
+
// 乾貨排版特徵檢查 (具備條列、步驟或結構化排版)
|
|
370
|
+
if (/[0-9][.、]|👉|✅|🔹|💡|📌|\n- |\n\* /.test(parsed.cleanBody)) {
|
|
371
|
+
heuristicScore += 10;
|
|
372
|
+
}
|
|
373
|
+
// 社群互動數據適度輔助加分 (最多 12 分,避免高讚垃圾貼文)
|
|
374
|
+
const likes = post.like_count || 0;
|
|
375
|
+
const comments = post.comment_count || 0;
|
|
376
|
+
if (likes > 300)
|
|
377
|
+
heuristicScore += 6;
|
|
378
|
+
if (comments > 15)
|
|
379
|
+
heuristicScore += 6;
|
|
380
|
+
heuristicScore = Math.min(96, Math.max(10, heuristicScore));
|
|
381
|
+
const isHeuristicWorthy = heuristicScore >= minScore;
|
|
382
|
+
if (isHeuristicWorthy) {
|
|
383
|
+
const reason = `正文實質深度探討 [${hitBodyKeywords.slice(0, 3).join(', ')}],文字詳實完整 (${parsed.cleanLength} 字),結構條理分明。`;
|
|
384
|
+
const hookPhrases = [
|
|
385
|
+
`這篇關於 ${hitBodyKeywords[0] || '該主題'} 的內容分享非常扎實,特別推薦你看!`,
|
|
386
|
+
`發現這篇乾貨對你的方向很有啟發,特地 @ 你存起來!`,
|
|
387
|
+
`這篇思路與案例分析得很透徹,內容很硬核,推推!`,
|
|
388
|
+
];
|
|
389
|
+
const chosenHook = hookPhrases[Math.floor(Math.random() * hookPhrases.length)];
|
|
390
|
+
const commentText = master ? `@${master} ${chosenHook}` : `這篇乾貨非常精彩,推薦關注!`;
|
|
391
|
+
return {
|
|
392
|
+
isWorthy: true,
|
|
393
|
+
score: heuristicScore,
|
|
394
|
+
reason,
|
|
395
|
+
commentText,
|
|
396
|
+
reviewMethod: 'heuristic',
|
|
397
|
+
};
|
|
398
|
+
}
|
|
399
|
+
else {
|
|
400
|
+
return {
|
|
401
|
+
isWorthy: false,
|
|
402
|
+
score: heuristicScore,
|
|
403
|
+
reason: '',
|
|
404
|
+
rejectionReason: `內容乾貨密度評分 (${heuristicScore}) 未達嚴選門檻 (${minScore})。`,
|
|
405
|
+
commentText: '',
|
|
406
|
+
reviewMethod: 'heuristic',
|
|
407
|
+
};
|
|
408
|
+
}
|
|
71
409
|
}
|
|
72
410
|
export async function executeScrollRound(ctx, config, mcp, ledger, log = console.log) {
|
|
73
|
-
|
|
74
|
-
|
|
411
|
+
// 防風控冷卻保護檢查
|
|
412
|
+
if (ledger.isInCooldown()) {
|
|
413
|
+
const rem = ledger.getCooldownRemainingSeconds();
|
|
414
|
+
const reason = ledger.getCooldownReason();
|
|
415
|
+
const msg = `防風控冷卻保護中(剩餘 ${rem} 秒,原因: ${reason}),暫停本輪滾動瀏覽`;
|
|
416
|
+
log(`[ScrollDuty] 🚨 ${msg}`);
|
|
417
|
+
return {
|
|
418
|
+
ok: true,
|
|
419
|
+
totalScanned: 0,
|
|
420
|
+
worthyCount: 0,
|
|
421
|
+
recommendedCount: 0,
|
|
422
|
+
recommendations: [],
|
|
423
|
+
message: msg,
|
|
424
|
+
};
|
|
425
|
+
}
|
|
426
|
+
log('[ScrollDuty] 🚀 開始替主人刷一輪 Instagram (啟動正文內容嚴格審查模式)...');
|
|
427
|
+
const mcpSvc = globalThis.__DSH_INSTAGRAM_MCP__;
|
|
428
|
+
const master = (mcpSvc?.config?.masterUsername || config.masterUsername || '').replace(/^@/, '').trim();
|
|
75
429
|
if (!master) {
|
|
76
|
-
log('[ScrollDuty] ⚠️
|
|
430
|
+
log('[ScrollDuty] ⚠️ 尚未在 MCP 配置主人帳號 (masterUsername),將僅進行瀏覽審核而不留言 @主人。');
|
|
77
431
|
}
|
|
78
432
|
const batchSize = Math.max(5, config.batchSize || 15);
|
|
433
|
+
const maxPerRun = config.maxRecommendsPerRun || 1;
|
|
79
434
|
const candidatePosts = [];
|
|
80
|
-
// 1.
|
|
435
|
+
// 1. 抓取首頁時間軸動態牆 (Feed)
|
|
81
436
|
try {
|
|
82
437
|
log(`[ScrollDuty] 正在滾動時間軸動態牆 (抓取上限: ${batchSize})...`);
|
|
83
438
|
const feedRes = await mcp.igGetTimelineFeed(batchSize);
|
|
@@ -88,56 +443,176 @@ export async function executeScrollRound(ctx, config, mcp, ledger, log = console
|
|
|
88
443
|
}
|
|
89
444
|
catch (e) {
|
|
90
445
|
log(`[ScrollDuty] 獲取時間軸動態異常: ${e.message}`);
|
|
446
|
+
if (/feedback|action block|checkpoint|rate limit/i.test(e.message)) {
|
|
447
|
+
ledger.triggerCooldown(config.cooldownOnBlockMinutes || 30, e.message);
|
|
448
|
+
return {
|
|
449
|
+
ok: false,
|
|
450
|
+
totalScanned: 0,
|
|
451
|
+
worthyCount: 0,
|
|
452
|
+
recommendedCount: 0,
|
|
453
|
+
recommendations: [],
|
|
454
|
+
message: `時間軸獲取異常並觸發風控冷卻: ${e.message}`,
|
|
455
|
+
};
|
|
456
|
+
}
|
|
91
457
|
}
|
|
92
|
-
//
|
|
458
|
+
// 2A. 模糊搜尋探索 (Fuzzy Search) —— 預設啟用,解決單純 Tags 搜尋返回 0 則貼文問題
|
|
459
|
+
const useFuzzy = config.fuzzySearchEnabled !== false;
|
|
460
|
+
const fuzzyWords = (config.fuzzyKeywords && config.fuzzyKeywords.length > 0)
|
|
461
|
+
? config.fuzzyKeywords
|
|
462
|
+
: (config.interestTags || []);
|
|
463
|
+
if (useFuzzy && fuzzyWords.length > 0) {
|
|
464
|
+
const randomKeyword = fuzzyWords[Math.floor(Math.random() * fuzzyWords.length)].replace(/^#/, '').trim();
|
|
465
|
+
if (randomKeyword) {
|
|
466
|
+
try {
|
|
467
|
+
log(`[ScrollDuty] 🔎 正在使用模糊搜尋探索關鍵字 "${randomKeyword}"...`);
|
|
468
|
+
const fuzzyRes = typeof mcp.igFuzzySearchPosts === 'function'
|
|
469
|
+
? await mcp.igFuzzySearchPosts(randomKeyword, 10)
|
|
470
|
+
: await mcp.callTool('ig_fuzzy_search_posts', { query: randomKeyword, limit: 10 }, 75000);
|
|
471
|
+
if (fuzzyRes?.ok && Array.isArray(fuzzyRes.posts)) {
|
|
472
|
+
for (const p of fuzzyRes.posts) {
|
|
473
|
+
const pk = String(p.pk || p.id || '');
|
|
474
|
+
if (pk && !candidatePosts.some((cp) => String(cp.pk || cp.id || '') === pk)) {
|
|
475
|
+
candidatePosts.push(p);
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
log(`[ScrollDuty] 從模糊搜尋 "${randomKeyword}" 獲取到 ${fuzzyRes.posts.length} 則貼文。`);
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
catch (e) {
|
|
482
|
+
log(`[ScrollDuty] 模糊搜尋探索異常: ${e.message}`);
|
|
483
|
+
if (/feedback|action block|checkpoint|rate limit/i.test(e.message)) {
|
|
484
|
+
ledger.triggerCooldown(config.cooldownOnBlockMinutes || 30, e.message);
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
// 2B. 主題標籤探索 (Explore Tag) —— 保留功能,由 tagSearchEnabled 按鈕控制開關 (預設關閉)
|
|
490
|
+
const useTags = config.tagSearchEnabled === true;
|
|
93
491
|
const tags = config.interestTags || [];
|
|
94
|
-
if (tags.length > 0) {
|
|
492
|
+
if (useTags && tags.length > 0) {
|
|
95
493
|
const randomTag = tags[Math.floor(Math.random() * tags.length)].replace(/^#/, '').trim();
|
|
96
494
|
try {
|
|
97
|
-
log(`[ScrollDuty] 正在探索主題標籤 #${randomTag}...`);
|
|
495
|
+
log(`[ScrollDuty] 🏷️ 正在探索主題標籤 #${randomTag}...`);
|
|
98
496
|
const tagRes = await mcp.igSearchHashtag(randomTag, 10);
|
|
99
497
|
if (tagRes?.ok && Array.isArray(tagRes.posts)) {
|
|
100
|
-
|
|
498
|
+
for (const p of tagRes.posts) {
|
|
499
|
+
const pk = String(p.pk || p.id || '');
|
|
500
|
+
if (pk && !candidatePosts.some((cp) => String(cp.pk || cp.id || '') === pk)) {
|
|
501
|
+
candidatePosts.push(p);
|
|
502
|
+
}
|
|
503
|
+
}
|
|
101
504
|
log(`[ScrollDuty] 從 #${randomTag} 獲取到 ${tagRes.posts.length} 則主題貼文。`);
|
|
102
505
|
}
|
|
103
506
|
}
|
|
104
507
|
catch (e) {
|
|
105
508
|
log(`[ScrollDuty] 探索標籤異常: ${e.message}`);
|
|
509
|
+
if (/feedback|action block|checkpoint|rate limit/i.test(e.message)) {
|
|
510
|
+
ledger.triggerCooldown(config.cooldownOnBlockMinutes || 30, e.message);
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
// 3. 抓取連續短片探索流 (Explore Reels Feed) —— 豐富動態滾動來源,避免時間軸枯竭
|
|
515
|
+
try {
|
|
516
|
+
log(`[ScrollDuty] 正在探索熱門短影音 (Reels Feed, 抓取上限: 10)...`);
|
|
517
|
+
const reelsRes = await mcp.igGetReelsFeed(10);
|
|
518
|
+
if (reelsRes?.ok && Array.isArray(reelsRes.reels)) {
|
|
519
|
+
for (const r of reelsRes.reels) {
|
|
520
|
+
const pk = String(r.pk || r.id || '');
|
|
521
|
+
if (pk && !candidatePosts.some((cp) => String(cp.pk || cp.id || '') === pk)) {
|
|
522
|
+
candidatePosts.push(r);
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
log(`[ScrollDuty] 從 Reels 探索流獲取到 ${reelsRes.reels.length} 則連續短片。`);
|
|
106
526
|
}
|
|
107
527
|
}
|
|
528
|
+
catch (e) {
|
|
529
|
+
log(`[ScrollDuty] 獲取 Reels 探索流異常: ${e.message}`);
|
|
530
|
+
}
|
|
108
531
|
let totalScanned = 0;
|
|
109
532
|
let worthyCount = 0;
|
|
110
533
|
let recommendedCount = 0;
|
|
111
534
|
const recommendations = [];
|
|
112
|
-
// 3.
|
|
535
|
+
// 3. 逐條讀內文進行深度內容審查與推薦
|
|
113
536
|
for (const post of candidatePosts) {
|
|
114
|
-
|
|
537
|
+
if (recommendedCount >= maxPerRun) {
|
|
538
|
+
log(`[ScrollDuty] 達到本輪推坑上限 (${maxPerRun} 則),結束本輪推薦。`);
|
|
539
|
+
break;
|
|
540
|
+
}
|
|
541
|
+
const postPk = String(post.pk || post.id || '').split('_')[0].trim();
|
|
115
542
|
if (!postPk)
|
|
116
543
|
continue;
|
|
117
544
|
// 跳過已看過的貼文
|
|
118
|
-
if (ledger.hasSeen(
|
|
545
|
+
if (ledger.hasSeen(post) || ledger.hasEverSent(post)) {
|
|
119
546
|
continue;
|
|
120
547
|
}
|
|
548
|
+
ledger.markSeen(post, false);
|
|
549
|
+
// 模擬人類瀏覽閱讀貼文微延遲
|
|
550
|
+
const minDelay = config.minPostReadDelaySeconds || 3;
|
|
551
|
+
const maxDelay = Math.max(minDelay, config.maxPostReadDelaySeconds || 7);
|
|
552
|
+
const readWaitMs = Math.floor((minDelay + Math.random() * (maxDelay - minDelay)) * 1000);
|
|
553
|
+
await new Promise((r) => setTimeout(r, readWaitMs));
|
|
121
554
|
totalScanned++;
|
|
122
|
-
const evalRes =
|
|
555
|
+
const evalRes = await inspectAndEvaluatePost(ctx, post, config);
|
|
123
556
|
if (evalRes.isWorthy) {
|
|
124
557
|
worthyCount++;
|
|
125
|
-
log(`[ScrollDuty] ⭐
|
|
558
|
+
log(`[ScrollDuty] ⭐ 發現極高價值內容!審查機制: [${evalRes.reviewMethod.toUpperCase()}], 評分: ${evalRes.score}/100, 作者: @${post.user?.username || 'unknown'}`);
|
|
559
|
+
log(`[ScrollDuty] 推薦原因: ${evalRes.reason}`);
|
|
126
560
|
// 檢查是否還能推坑 (每日上限控制)
|
|
127
561
|
const cap = config.dailyRecommendCap || 5;
|
|
128
562
|
if (ledger.canRecommend(cap) && master) {
|
|
129
|
-
|
|
563
|
+
if (ledger.hasEverSent(post)) {
|
|
564
|
+
log(`[ScrollDuty] ⏭️ 貼文/Reel (${postPk}) 已經在其他任務發送過推坑留言,攔截重複發送!`);
|
|
565
|
+
continue;
|
|
566
|
+
}
|
|
567
|
+
const botUserLower = String(mcpSvc?.loginStatus?.username || mcpSvc?.config?.username || '').replace(/^@/, '').trim().toLowerCase();
|
|
568
|
+
try {
|
|
569
|
+
const ctxCheck = await mcp.igGetPostContext(postPk, 15);
|
|
570
|
+
const existingComments = ctxCheck?.comments || [];
|
|
571
|
+
const alreadyCommented = existingComments.some((c) => {
|
|
572
|
+
const cu = String(c.user?.username || '').toLowerCase();
|
|
573
|
+
const ct = String(c.text || '').toLowerCase();
|
|
574
|
+
return (botUserLower && cu === botUserLower) || (master && ct.includes(`@${master.toLowerCase()}`));
|
|
575
|
+
});
|
|
576
|
+
if (alreadyCommented) {
|
|
577
|
+
log(`[ScrollDuty] ⏭️ 偵測到貼文/Reel (${postPk}) 留言區已有機器人或 @${master} 的推坑紀錄,永久標記並跳過重複發送!`);
|
|
578
|
+
ledger.markSeen(post, true);
|
|
579
|
+
continue;
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
catch { }
|
|
583
|
+
// @ 前進行最後確認檢查 (Pre-mention Verification)
|
|
584
|
+
if (!evalRes.commentText || !evalRes.commentText.includes(master)) {
|
|
585
|
+
log(`[ScrollDuty] ⚠️ 推坑語句缺少 @${master},自動補上主人名稱`);
|
|
586
|
+
evalRes.commentText = `@${master} ${evalRes.commentText || '這篇分析非常到位,特地推薦你看看!'}`;
|
|
587
|
+
}
|
|
588
|
+
log(`[ScrollDuty] 正在貼文下留言推坑: "${evalRes.commentText}"...`);
|
|
130
589
|
try {
|
|
131
590
|
const commentRes = await mcp.igPostComment({
|
|
132
591
|
media_pk: postPk,
|
|
133
592
|
text: evalRes.commentText,
|
|
593
|
+
jitter_min: 6.0,
|
|
594
|
+
jitter_max: 15.0,
|
|
134
595
|
});
|
|
596
|
+
if (commentRes?.in_cooldown ||
|
|
597
|
+
(commentRes?.error &&
|
|
598
|
+
/action block|feedback|checkpoint|please wait|rate limit/i.test(commentRes.error))) {
|
|
599
|
+
ledger.triggerCooldown(config.cooldownOnBlockMinutes || 30, commentRes.error || 'Instagram 頻率風控阻擋');
|
|
600
|
+
log(`[ScrollDuty] 🚨 偵測到風控或熔斷保護,停止後續推坑!`);
|
|
601
|
+
ledger.markSeen(post, true);
|
|
602
|
+
break;
|
|
603
|
+
}
|
|
135
604
|
if (commentRes?.ok) {
|
|
136
605
|
log(`[ScrollDuty] ✅ 成功留言 @${master}!`);
|
|
137
606
|
recommendedCount++;
|
|
138
607
|
// 若開啟,順便點讚
|
|
139
608
|
if (config.likeOnRecommend) {
|
|
140
|
-
await mcp.igLikeMedia(postPk).catch(() => {
|
|
609
|
+
const likeRes = await mcp.igLikeMedia(postPk).catch(() => ({}));
|
|
610
|
+
if (likeRes?.in_cooldown ||
|
|
611
|
+
(likeRes?.error &&
|
|
612
|
+
/action block|feedback|checkpoint|please wait|rate limit/i.test(likeRes.error))) {
|
|
613
|
+
ledger.triggerCooldown(config.cooldownOnBlockMinutes || 30, likeRes.error || 'Instagram 點讚風控阻擋');
|
|
614
|
+
break;
|
|
615
|
+
}
|
|
141
616
|
}
|
|
142
617
|
const recItem = {
|
|
143
618
|
postPk,
|
|
@@ -154,24 +629,38 @@ export async function executeScrollRound(ctx, config, mcp, ledger, log = console
|
|
|
154
629
|
}
|
|
155
630
|
else {
|
|
156
631
|
log(`[ScrollDuty] 留言發布失敗: ${commentRes?.error || '未知原因'}`);
|
|
157
|
-
ledger.markSeen(
|
|
632
|
+
ledger.markSeen(post, true);
|
|
158
633
|
}
|
|
159
634
|
}
|
|
160
635
|
catch (e) {
|
|
161
636
|
log(`[ScrollDuty] 留言異常: ${e.message}`);
|
|
162
|
-
ledger.markSeen(
|
|
637
|
+
ledger.markSeen(post, true);
|
|
638
|
+
if (/feedback|action block|checkpoint|rate limit/i.test(e.message)) {
|
|
639
|
+
ledger.triggerCooldown(config.cooldownOnBlockMinutes || 30, e.message);
|
|
640
|
+
break;
|
|
641
|
+
}
|
|
163
642
|
}
|
|
164
643
|
}
|
|
165
644
|
else {
|
|
166
645
|
log(`[ScrollDuty] 今日推薦已達上限 (${cap}則) 或未配置主人帳號,僅標記值得並記錄日報。`);
|
|
167
|
-
ledger.markSeen(
|
|
646
|
+
ledger.markSeen(post, true);
|
|
168
647
|
}
|
|
169
648
|
}
|
|
170
649
|
else {
|
|
171
|
-
|
|
650
|
+
log(`[ScrollDuty] ⏭️ 未通過內容審核: ${evalRes.rejectionReason || '評分未達標準'}`);
|
|
651
|
+
ledger.markSeen(post, false);
|
|
172
652
|
}
|
|
173
653
|
}
|
|
174
|
-
|
|
654
|
+
let msg = '';
|
|
655
|
+
if (totalScanned > 0) {
|
|
656
|
+
msg = `本輪滾動完畢!共掃描 ${totalScanned} 則新內容,經正文與語義嚴格審查,精選 ${worthyCount} 則符合主人偏好,成功留評推薦 ${recommendedCount} 則。`;
|
|
657
|
+
}
|
|
658
|
+
else if (candidatePosts.length > 0) {
|
|
659
|
+
msg = `本輪滾動完畢!共獲取到 ${candidatePosts.length} 則動態,但均為今日已審閱過之內容(帳本已自動去重,避免重複審查與打擾),暫無未看過之新貼文。`;
|
|
660
|
+
}
|
|
661
|
+
else {
|
|
662
|
+
msg = `本輪滾動完畢!未檢索到動態內容(可能是 Instagram 官方暫時無新動態推薦或標籤搜尋受限)。`;
|
|
663
|
+
}
|
|
175
664
|
log(`[ScrollDuty] 🎉 ${msg}`);
|
|
176
665
|
return {
|
|
177
666
|
ok: true,
|