@luoyi-dsh/instagram-scroll 1.0.0 → 1.0.3

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/client.js CHANGED
@@ -13,7 +13,7 @@
13
13
  */
14
14
 
15
15
  window.__ModuleLoader__.load({
16
- id: 'dsh-instagram-scroll',
16
+ id: '@luoyi-dsh/instagram-scroll',
17
17
  factory: (require) => {
18
18
  const React = require('react')
19
19
  const h = React.createElement
@@ -542,13 +542,33 @@ window.__ModuleLoader__.load({
542
542
  return {
543
543
  masterUsername: cleanStr(c.masterUsername, ''),
544
544
  interestTags: cleanArr(c.interestTags, ['ai', 'deepseek', 'coding', 'tech', 'gadgets']),
545
- personaPrompt: cleanStr(c.personaPrompt, '你是一位洞察力敏銳、品味高雅的社群特助。請評估這篇 Instagram 貼文/影片是否具有前沿乾貨、創意見解、實用價值或視覺驚艷。若是主人會感興趣的好內容,請給出大於80分的評價,並撰寫簡短自然、熱情真誠的推坑一句話。'),
546
- minScore: cleanNum(c.minScore, 75),
545
+ userInterests: cleanStr(
546
+ c.userInterests,
547
+ '專注於前沿 AI 技術 (LLM, Agent, 開源模型)、全端與前端架構、優質開源專案、高效率開發工具、優質 UI/UX 設計或深度乾貨技術解析。偏好乾貨分享、實踐案例與原創見解,不喜歡空洞無物、純蹭熱度、打廣告或純娛樂搞笑的內容。'
548
+ ),
549
+ positiveKeywords: cleanArr(c.positiveKeywords, [
550
+ 'ai', 'deepseek', 'agent', 'coding', '技術', '前端', '後端', '架構', '開源', '教學', '程式', '開發', '模型', 'github', 'prompt', '演算法', '工具', '實戰', '乾貨',
551
+ ]),
552
+ negativeKeywords: cleanArr(c.negativeKeywords, [
553
+ '互粉', '代購', '博彩', '運彩', '兼職', '抽獎', '免費領', '私訊', '點擊連結', '主頁連結', '減肥', '微商', '外匯', '虛擬幣', '心靈雞湯', '穿搭', '搞笑', '自拍', '八卦', '暴富', '躺平', '加盟',
554
+ ]),
555
+ minCaptionLength: cleanNum(c.minCaptionLength, 30),
556
+ requireBodyMatch: cleanBool(c.requireBodyMatch, true),
557
+ strictContentCheck: cleanBool(c.strictContentCheck, true),
558
+ personaPrompt: cleanStr(
559
+ c.personaPrompt,
560
+ '你是一位洞察力敏銳、品味高雅的社群特助。請以極高標準評估這篇 Instagram 貼文是否深度符合主人的興趣偏好。若是具備前沿乾貨、創意見解或高實用價值的精選內容,請給出高分評價並撰寫簡短自然、熱情切題的推坑一句話。'
561
+ ),
562
+ minScore: cleanNum(c.minScore, 80),
547
563
  dailyRecommendCap: cleanNum(c.dailyRecommendCap, 5),
548
564
  scrollIntervalMinutes: cleanNum(c.scrollIntervalMinutes, 60),
549
565
  autoScrollEnabled: cleanBool(c.autoScrollEnabled, true),
550
566
  likeOnRecommend: cleanBool(c.likeOnRecommend, true),
551
567
  batchSize: cleanNum(c.batchSize, 15),
568
+ maxRecommendsPerRun: cleanNum(c.maxRecommendsPerRun, 1),
569
+ minPostReadDelaySeconds: cleanNum(c.minPostReadDelaySeconds, 3),
570
+ maxPostReadDelaySeconds: cleanNum(c.maxPostReadDelaySeconds, 7),
571
+ cooldownOnBlockMinutes: cleanNum(c.cooldownOnBlockMinutes, 30),
552
572
  }
553
573
  }
554
574
 
@@ -556,22 +576,47 @@ window.__ModuleLoader__.load({
556
576
  const [tab, setTab] = useState('feed')
557
577
  const [cfg, setCfg] = useState(normalizeScrollConfig({}))
558
578
  const [status, setStatus] = useState(null)
579
+ const [mcpStatus, setMcpStatus] = useState(null)
559
580
  const [report, setReport] = useState('')
560
581
  const [loading, setLoading] = useState(true)
561
582
  const [saving, setSaving] = useState(false)
562
583
  const [runningRound, setRunningRound] = useState(false)
563
584
  const [msg, setMsg] = useState(null)
564
585
 
586
+ // 獨立字串狀態:解決 input 鍵入逗號與空格被 React 重新格式化吃掉的問題
587
+ const [tagsInputText, setTagsInputText] = useState('')
588
+ const [positiveWordsText, setPositiveWordsText] = useState('')
589
+ const [negativeWordsText, setNegativeWordsText] = useState('')
590
+
591
+ const parseListFromText = (text, isTag = false) => {
592
+ if (typeof text !== 'string') return []
593
+ return text
594
+ .split(/[,,\n]+/)
595
+ .map((s) => s.trim())
596
+ .map((s) => (isTag ? s.replace(/^#/, '') : s))
597
+ .filter(Boolean)
598
+ }
599
+
565
600
  const refresh = async () => {
566
601
  try {
567
- const [cfgRes, statusRes, reportRes] = await Promise.all([
568
- jget('/config'),
569
- jget('/status'),
570
- jget('/report'),
602
+ const [cfgRes, statusRes, reportRes, mcpRes] = await Promise.all([
603
+ jget('/config').catch(() => null),
604
+ jget('/status').catch(() => null),
605
+ jget('/report').catch(() => null),
606
+ fetch('/api/instagram/mcp/status', { credentials: 'same-origin' })
607
+ .then((r) => r.json())
608
+ .catch(() => null),
571
609
  ])
572
- if (cfgRes?.ok) setCfg(normalizeScrollConfig(cfgRes.config))
610
+ if (cfgRes?.ok) {
611
+ const loaded = normalizeScrollConfig(cfgRes.config)
612
+ setCfg(loaded)
613
+ setTagsInputText(loaded.interestTags.join(', '))
614
+ setPositiveWordsText(loaded.positiveKeywords.join(', '))
615
+ setNegativeWordsText(loaded.negativeKeywords.join(', '))
616
+ }
573
617
  if (statusRes?.ok) setStatus(statusRes)
574
618
  if (reportRes?.ok) setReport(reportRes.report || '')
619
+ if (mcpRes?.ok) setMcpStatus(mcpRes)
575
620
  } catch (e) {
576
621
  setMsg(`讀取狀態失敗: ${e.message}`)
577
622
  } finally {
@@ -589,10 +634,19 @@ window.__ModuleLoader__.load({
589
634
  setSaving(true)
590
635
  setMsg(null)
591
636
  try {
592
- const cleanPayload = normalizeScrollConfig(cfg)
637
+ const cleanPayload = {
638
+ ...normalizeScrollConfig(cfg),
639
+ interestTags: parseListFromText(tagsInputText, true),
640
+ positiveKeywords: parseListFromText(positiveWordsText, false),
641
+ negativeKeywords: parseListFromText(negativeWordsText, false),
642
+ }
593
643
  const res = await jpost('/config', cleanPayload)
594
644
  if (res?.ok) {
595
- setCfg(normalizeScrollConfig(res.config))
645
+ const saved = normalizeScrollConfig(res.config)
646
+ setCfg(saved)
647
+ setTagsInputText(saved.interestTags.join(', '))
648
+ setPositiveWordsText(saved.positiveKeywords.join(', '))
649
+ setNegativeWordsText(saved.negativeKeywords.join(', '))
596
650
  setMsg('✅ 滾動推坑設定已成功保存!')
597
651
  }
598
652
  } catch (e) {
@@ -625,6 +679,22 @@ window.__ModuleLoader__.load({
625
679
  const daily = status?.dailyStats || {}
626
680
  const recommendedItems = daily.recommendedItems || []
627
681
 
682
+ const isMcpConnected = Boolean(
683
+ status?.mcpReady ||
684
+ status?.loginStatus?.ok ||
685
+ mcpStatus?.mcpReady ||
686
+ mcpStatus?.loginStatus?.ok
687
+ )
688
+
689
+ const rawAccount =
690
+ status?.mcpAccount ||
691
+ status?.loginStatus?.username ||
692
+ mcpStatus?.loginStatus?.username ||
693
+ (mcpStatus?.loginStatus?.message?.match(/@([^\s)]+)/)?.[1]) ||
694
+ (isMcpConnected ? '已連線' : '')
695
+
696
+ const displayAccount = rawAccount ? rawAccount.replace(/^@/, '') : ''
697
+
628
698
  return h(
629
699
  'div',
630
700
  { className: 'ig-scr-root' },
@@ -643,8 +713,22 @@ window.__ModuleLoader__.load({
643
713
  h('div', { className: 'ig-scr-brand-title' }, 'Instagram 滾動探索與主人推坑'),
644
714
  h(
645
715
  'div',
646
- { className: 'ig-scr-brand-sub' },
647
- cfg.masterUsername ? `當前主人目標: @${cfg.masterUsername.replace(/^@/, '')}` : '尚未綁定主人帳號'
716
+ { className: 'ig-scr-brand-sub', style: { display: 'flex', alignItems: 'center', gap: '8px' } },
717
+ cfg.masterUsername ? `目標主人: @${cfg.masterUsername.replace(/^@/, '')}` : '尚未綁定主人帳號',
718
+ h('span', { style: { color: 'rgba(255, 255, 255, 0.2)' } }, '|'),
719
+ isMcpConnected
720
+ ? h(
721
+ 'span',
722
+ { style: { color: '#34d399', fontSize: '11px', display: 'inline-flex', alignItems: 'center', gap: '4px' } },
723
+ h('span', { style: { width: '6px', height: '6px', borderRadius: '50%', background: '#10b981' } }),
724
+ `MCP 會話: @${displayAccount || '已連線'}`
725
+ )
726
+ : h(
727
+ 'span',
728
+ { style: { color: '#fbbf24', fontSize: '11px', display: 'inline-flex', alignItems: 'center', gap: '4px' } },
729
+ h('span', { style: { width: '6px', height: '6px', borderRadius: '50%', background: '#f59e0b' } }),
730
+ '等待 MCP 登入'
731
+ )
648
732
  )
649
733
  )
650
734
  ),
@@ -652,7 +736,8 @@ window.__ModuleLoader__.load({
652
736
  'button',
653
737
  {
654
738
  className: 'ig-scr-btn primary',
655
- disabled: runningRound,
739
+ disabled: runningRound || !isMcpConnected,
740
+ title: !isMcpConnected ? '等待 FastMCP 連線就緒後即可刷動態' : '立即替主人刷一輪動態',
656
741
  onClick: runNow,
657
742
  },
658
743
  h(Icon.Play, { size: 12 }),
@@ -820,6 +905,32 @@ window.__ModuleLoader__.load({
820
905
  h('div', { className: 'ig-scr-section-desc' }, '設定主人的 Instagram 帳號名與感興趣的標籤領域,AI 將依此進行嚴格內容篩選。')
821
906
  ),
822
907
 
908
+ // MCP Session 來源提示
909
+ h(
910
+ 'div',
911
+ {
912
+ style: {
913
+ background: 'rgba(59, 130, 246, 0.06)',
914
+ border: '1px solid rgba(59, 130, 246, 0.2)',
915
+ borderRadius: '8px',
916
+ padding: '10px 14px',
917
+ fontSize: '12px',
918
+ color: '#93c5fd',
919
+ display: 'flex',
920
+ alignItems: 'center',
921
+ gap: '8px',
922
+ },
923
+ },
924
+ h(Icon.Sparkles, { size: 14 }),
925
+ h(
926
+ 'span',
927
+ null,
928
+ isMcpConnected
929
+ ? `已沿用 @luoyi-dsh/instagram-mcp 的登入會話 (@${displayAccount || '已授權'}),本外掛無需填寫帳密。`
930
+ : '本外掛無需輸入帳密,直接沿用依賴的 dsh-instagram-mcp 獲取之 Session。目前 MCP 尚未登入,請先至 MCP 面板完成登入。'
931
+ )
932
+ ),
933
+
823
934
  h(
824
935
  'div',
825
936
  { className: 'ig-scr-field' },
@@ -833,21 +944,128 @@ window.__ModuleLoader__.load({
833
944
  h('div', { className: 'ig-scr-hint' }, '發現值得推薦的貼文/影片時,會在留言區 @ 此帳號進行推薦')
834
945
  ),
835
946
 
947
+ // 正文實質內容審查開關
948
+ h(
949
+ 'div',
950
+ { className: 'ig-scr-switch' },
951
+ h(
952
+ 'div',
953
+ { className: 'ig-scr-switch-text' },
954
+ h('div', { className: 'ig-scr-switch-name' }, '🛡️ 啟用 @ 前正文內容深度審查'),
955
+ h('div', { className: 'ig-scr-hint' }, '拒絕隨便推薦!留言 @ 主人前深度檢查正文實質乾貨與品質,剔除純圖廢文與空洞標籤')
956
+ ),
957
+ h(
958
+ 'button',
959
+ {
960
+ className: `ig-scr-toggle ${cfg.strictContentCheck ? 'on' : ''}`,
961
+ onClick: () => updateField('strictContentCheck', !cfg.strictContentCheck),
962
+ },
963
+ h('span', { className: 'ig-scr-toggle-thumb' })
964
+ )
965
+ ),
966
+
967
+ // 正文實質命中開關
968
+ h(
969
+ 'div',
970
+ { className: 'ig-scr-switch' },
971
+ h(
972
+ 'div',
973
+ { className: 'ig-scr-switch-text' },
974
+ h('div', { className: 'ig-scr-switch-name' }, '🔍 正文必須實質討論偏好主題 (拒絕僅在 tag 蹭熱度)'),
975
+ h('div', { className: 'ig-scr-hint' }, '關鍵詞必須在正文中真正出現;若僅在文末堆砌 #標籤 則視為標籤農場直接過濾')
976
+ ),
977
+ h(
978
+ 'button',
979
+ {
980
+ className: `ig-scr-toggle ${cfg.requireBodyMatch ? 'on' : ''}`,
981
+ onClick: () => updateField('requireBodyMatch', !cfg.requireBodyMatch),
982
+ },
983
+ h('span', { className: 'ig-scr-toggle-thumb' })
984
+ )
985
+ ),
986
+
987
+ // 主人喜歡的內容風格與偏好詳述
988
+ h(
989
+ 'div',
990
+ { className: 'ig-scr-field' },
991
+ h('label', { className: 'ig-scr-label' }, '💎 主人喜歡的內容風格與領域詳述 (User Interests)'),
992
+ h('textarea', {
993
+ className: 'ig-scr-textarea',
994
+ style: { minHeight: '75px' },
995
+ value: cleanStr(cfg.userInterests),
996
+ placeholder: '詳細描述您喜歡的貼文類型、技術領域、內容風格與乾貨要求...',
997
+ onChange: (e) => updateField('userInterests', e.target.value),
998
+ }),
999
+ h('div', { className: 'ig-scr-hint' }, 'AI 將依據此描述深度比對正文語義,確保推薦完全符合您的個人品味與需求')
1000
+ ),
1001
+
1002
+ // 正向關鍵字
836
1003
  h(
837
1004
  'div',
838
1005
  { className: 'ig-scr-field' },
839
- h('label', { className: 'ig-scr-label' }, '🏷️ 主人感興趣的主題標籤 (Interest Tags)'),
1006
+ h('label', { className: 'ig-scr-label' }, '✨ 正文正向領域關鍵字 (Positive Keywords)'),
840
1007
  h('input', {
841
1008
  className: 'ig-scr-input',
842
- value: Array.isArray(cfg.interestTags) ? cfg.interestTags.join(', ') : '',
843
- placeholder: 'ai, deepseek, coding, photography, gadgets, travel',
844
- onChange: (e) =>
845
- updateField(
846
- 'interestTags',
847
- e.target.value.split(',').map((s) => s.trim().replace(/^#/, '')).filter(Boolean)
848
- ),
1009
+ value: positiveWordsText,
1010
+ placeholder: 'ai, deepseek, coding, 前端, 架構, 開源, 教學, 工具',
1011
+ onChange: (e) => {
1012
+ const raw = e.target.value
1013
+ setPositiveWordsText(raw)
1014
+ updateField('positiveKeywords', parseListFromText(raw, false))
1015
+ },
849
1016
  }),
850
- h('div', { className: 'ig-scr-hint' }, '多個標籤請用逗號分隔,除時間軸外亦會輪流針對這些主題進行深度探索')
1017
+ h('div', { className: 'ig-scr-hint' }, '正文實質提及這些關鍵詞或相關領域時給予高權重加分')
1018
+ ),
1019
+
1020
+ // 負向排除詞
1021
+ h(
1022
+ 'div',
1023
+ { className: 'ig-scr-field' },
1024
+ h('label', { className: 'ig-scr-label' }, '🚫 負向排除關鍵字 (Negative Keywords / Anti-Spam)'),
1025
+ h('input', {
1026
+ className: 'ig-scr-input',
1027
+ value: negativeWordsText,
1028
+ placeholder: '互粉, 代購, 博彩, 抽獎, 微商, 減肥, 穿搭, 搞笑, 自拍',
1029
+ onChange: (e) => {
1030
+ const raw = e.target.value
1031
+ setNegativeWordsText(raw)
1032
+ updateField('negativeKeywords', parseListFromText(raw, false))
1033
+ },
1034
+ }),
1035
+ h('div', { className: 'ig-scr-hint' }, '命中任一排除詞直接判定不合格,杜絕廣告宣傳、抽獎促銷與無意義廢文')
1036
+ ),
1037
+
1038
+ h(
1039
+ 'div',
1040
+ { className: 'ig-scr-grid-2' },
1041
+ h(
1042
+ 'div',
1043
+ { className: 'ig-scr-field' },
1044
+ h('label', { className: 'ig-scr-label' }, '🏷️ 探索主題標籤 (Interest Tags)'),
1045
+ h('input', {
1046
+ className: 'ig-scr-input',
1047
+ value: tagsInputText,
1048
+ placeholder: 'ai, deepseek, coding, tech, gadgets',
1049
+ onChange: (e) => {
1050
+ const raw = e.target.value
1051
+ setTagsInputText(raw)
1052
+ updateField('interestTags', parseListFromText(raw, true))
1053
+ },
1054
+ }),
1055
+ h('div', { className: 'ig-scr-hint' }, '探索抓取起點標籤 (非推薦唯一標準,支援空格與中英文逗號)')
1056
+ ),
1057
+ h(
1058
+ 'div',
1059
+ { className: 'ig-scr-field' },
1060
+ h('label', { className: 'ig-scr-label' }, '📏 正文有效文字最小長度 (字數)'),
1061
+ h('input', {
1062
+ type: 'number',
1063
+ className: 'ig-scr-input',
1064
+ value: cfg.minCaptionLength ?? 30,
1065
+ onChange: (e) => updateField('minCaptionLength', Number(e.target.value)),
1066
+ }),
1067
+ h('div', { className: 'ig-scr-hint' }, '扣除 #標籤 後的正文字數門檻')
1068
+ )
851
1069
  ),
852
1070
 
853
1071
  h(
@@ -955,7 +1173,7 @@ window.__ModuleLoader__.load({
955
1173
  h('input', {
956
1174
  type: 'number',
957
1175
  className: 'ig-scr-input',
958
- value: cfg.minScore ?? 75,
1176
+ value: cfg.minScore ?? 80,
959
1177
  onChange: (e) => updateField('minScore', Number(e.target.value)),
960
1178
  }),
961
1179
  h('div', { className: 'ig-scr-hint' }, '高於此分數的貼文才會在留言區 @主人')
@@ -1013,47 +1231,71 @@ window.__ModuleLoader__.load({
1013
1231
  const slots = (typeof ctx.get === 'function' ? ctx.get('slots') : undefined) || ctx.slots
1014
1232
  if (!slots) return
1015
1233
 
1016
- try {
1017
- slots.inject('settings.section', () =>
1234
+ // 1. 左側一級專屬導航槽位 (settings.section)
1235
+ const registerSection = () => {
1236
+ try {
1018
1237
  slots.register(
1019
1238
  {
1020
1239
  name: 'settings.section',
1021
1240
  id: 'instagram-scroll',
1022
- order: 33,
1023
- label: 'Instagram 滾動推坑',
1241
+ order: 36,
1242
+ label: 'Instagram 滾動',
1024
1243
  },
1025
1244
  () => h(ScrollPanel)
1026
1245
  )
1027
- )
1028
- } catch (e) {}
1246
+ } catch (e) {
1247
+ console.error('[@luoyi-dsh/instagram-scroll] 註冊 settings.section 異常:', e)
1248
+ }
1249
+ }
1250
+ if (typeof slots.inject === 'function') {
1251
+ slots.inject('settings.section', registerSection)
1252
+ } else if (typeof slots.register === 'function') {
1253
+ registerSection()
1254
+ }
1029
1255
 
1030
- try {
1031
- slots.inject('settings.plugins.tab', () =>
1256
+ // 2. 外掛程式頁面頂部頁籤 (settings.plugins.tab)
1257
+ const registerTab = () => {
1258
+ try {
1032
1259
  slots.register(
1033
1260
  {
1034
1261
  name: 'settings.plugins.tab',
1035
1262
  id: 'instagram-scroll',
1036
- order: 33,
1037
- label: 'Instagram 滾動推坑',
1263
+ order: 36,
1264
+ label: 'Instagram 滾動',
1038
1265
  },
1039
1266
  () => h(ScrollPanel)
1040
1267
  )
1041
- )
1042
- } catch (e) {}
1268
+ } catch (e) {
1269
+ console.error('[@luoyi-dsh/instagram-scroll] 註冊 settings.plugins.tab 異常:', e)
1270
+ }
1271
+ }
1272
+ if (typeof slots.inject === 'function') {
1273
+ slots.inject('settings.plugins.tab', registerTab)
1274
+ } else if (typeof slots.register === 'function') {
1275
+ registerTab()
1276
+ }
1043
1277
 
1044
- try {
1045
- slots.inject('plugins.item', () =>
1278
+ // 3. 外掛程式卡片列表槽位 (plugins.item)
1279
+ const registerItem = () => {
1280
+ try {
1046
1281
  slots.register(
1047
1282
  {
1048
1283
  name: 'plugins.item',
1049
1284
  id: 'instagram-scroll',
1050
- order: 33,
1285
+ order: 36,
1051
1286
  label: 'Instagram 滾動探索與推坑',
1052
1287
  },
1053
1288
  () => h(ScrollPanel)
1054
1289
  )
1055
- )
1056
- } catch (e) {}
1290
+ } catch (e) {
1291
+ console.error('[@luoyi-dsh/instagram-scroll] 註冊 plugins.item 異常:', e)
1292
+ }
1293
+ }
1294
+ if (typeof slots.inject === 'function') {
1295
+ slots.inject('plugins.item', registerItem)
1296
+ } else if (typeof slots.register === 'function') {
1297
+ registerItem()
1298
+ }
1057
1299
  },
1058
1300
  }
1059
1301
  },
package/cordis.patch.yml CHANGED
@@ -4,6 +4,6 @@
4
4
 
5
5
  - insert:
6
6
  - id: instagram-scroll
7
- name: dsh-instagram-scroll
7
+ name: '@luoyi-dsh/instagram-scroll'
8
8
  config:
9
9
  enabled: true
package/dist/config.d.ts CHANGED
@@ -4,6 +4,12 @@
4
4
  export interface InstagramScrollConfig {
5
5
  masterUsername?: string;
6
6
  interestTags?: string[];
7
+ userInterests?: string;
8
+ positiveKeywords?: string[];
9
+ negativeKeywords?: string[];
10
+ minCaptionLength?: number;
11
+ requireBodyMatch?: boolean;
12
+ strictContentCheck?: boolean;
7
13
  personaPrompt?: string;
8
14
  minScore?: number;
9
15
  dailyRecommendCap?: number;
@@ -11,6 +17,10 @@ export interface InstagramScrollConfig {
11
17
  autoScrollEnabled?: boolean;
12
18
  likeOnRecommend?: boolean;
13
19
  batchSize?: number;
20
+ maxRecommendsPerRun?: number;
21
+ minPostReadDelaySeconds?: number;
22
+ maxPostReadDelaySeconds?: number;
23
+ cooldownOnBlockMinutes?: number;
14
24
  }
15
25
  export declare const defaultScrollConfig: InstagramScrollConfig;
16
26
  export declare function unwrapScrollValue<T>(val: any, fallback: T): T;
@@ -1 +1 @@
1
- {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,MAAM,WAAW,qBAAqB;IACpC,cAAc,CAAC,EAAE,MAAM,CAAA;IACvB,YAAY,CAAC,EAAE,MAAM,EAAE,CAAA;IACvB,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,iBAAiB,CAAC,EAAE,MAAM,CAAA;IAC1B,qBAAqB,CAAC,EAAE,MAAM,CAAA;IAC9B,iBAAiB,CAAC,EAAE,OAAO,CAAA;IAC3B,eAAe,CAAC,EAAE,OAAO,CAAA;IACzB,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB;AAED,eAAO,MAAM,mBAAmB,EAAE,qBAWjC,CAAA;AAED,wBAAgB,iBAAiB,CAAC,CAAC,EAAE,GAAG,EAAE,GAAG,EAAE,QAAQ,EAAE,CAAC,GAAG,CAAC,CAW7D;AAED,wBAAgB,oBAAoB,CAAC,GAAG,EAAE,GAAG,GAAG,qBAAqB,CAapE;AAED,wBAAgB,uBAAuB,CAAC,CAAC,EAAE,GAAG,OAuC7C"}
1
+ {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,MAAM,WAAW,qBAAqB;IACpC,cAAc,CAAC,EAAE,MAAM,CAAA;IACvB,YAAY,CAAC,EAAE,MAAM,EAAE,CAAA;IAEvB,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB,gBAAgB,CAAC,EAAE,MAAM,EAAE,CAAA;IAC3B,gBAAgB,CAAC,EAAE,MAAM,EAAE,CAAA;IAC3B,gBAAgB,CAAC,EAAE,MAAM,CAAA;IACzB,gBAAgB,CAAC,EAAE,OAAO,CAAA;IAC1B,kBAAkB,CAAC,EAAE,OAAO,CAAA;IAE5B,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,iBAAiB,CAAC,EAAE,MAAM,CAAA;IAC1B,qBAAqB,CAAC,EAAE,MAAM,CAAA;IAC9B,iBAAiB,CAAC,EAAE,OAAO,CAAA;IAC3B,eAAe,CAAC,EAAE,OAAO,CAAA;IACzB,SAAS,CAAC,EAAE,MAAM,CAAA;IAElB,mBAAmB,CAAC,EAAE,MAAM,CAAA;IAC5B,uBAAuB,CAAC,EAAE,MAAM,CAAA;IAChC,uBAAuB,CAAC,EAAE,MAAM,CAAA;IAChC,sBAAsB,CAAC,EAAE,MAAM,CAAA;CAChC;AAED,eAAO,MAAM,mBAAmB,EAAE,qBAiEjC,CAAA;AAED,wBAAgB,iBAAiB,CAAC,CAAC,EAAE,GAAG,EAAE,GAAG,EAAE,QAAQ,EAAE,CAAC,GAAG,CAAC,CAW7D;AAYD,wBAAgB,oBAAoB,CAAC,GAAG,EAAE,GAAG,GAAG,qBAAqB,CAuBpE;AAED,wBAAgB,uBAAuB,CAAC,CAAC,EAAE,GAAG,OA+E7C"}
package/dist/config.js CHANGED
@@ -4,13 +4,66 @@
4
4
  export const defaultScrollConfig = {
5
5
  masterUsername: '',
6
6
  interestTags: ['ai', 'deepseek', 'coding', 'tech', 'gadgets'],
7
- personaPrompt: '你是一位洞察力敏銳、品味高雅的社群特助。請評估這篇 Instagram 貼文/影片是否具有前沿乾貨、創意見解、實用價值或視覺驚艷。若是主人會感興趣的好內容,請給出大於80分的評價,並撰寫簡短自然、熱情真誠的推坑一句話。',
8
- minScore: 75,
7
+ userInterests: '專注於前沿 AI 技術 (LLM, Agent, 開源模型)、全端與前端架構、優質開源專案、高效率開發工具、優質 UI/UX 設計或深度乾貨技術解析。偏好乾貨分享、實踐案例與原創見解,不喜歡空洞無物、純蹭熱度、打廣告或純娛樂搞笑的內容。',
8
+ positiveKeywords: [
9
+ 'ai',
10
+ 'deepseek',
11
+ 'agent',
12
+ 'coding',
13
+ '技術',
14
+ '前端',
15
+ '後端',
16
+ '架構',
17
+ '開源',
18
+ '教學',
19
+ '程式',
20
+ '開發',
21
+ '模型',
22
+ 'github',
23
+ 'prompt',
24
+ '演算法',
25
+ '工具',
26
+ '實戰',
27
+ '乾貨',
28
+ ],
29
+ negativeKeywords: [
30
+ '互粉',
31
+ '代購',
32
+ '博彩',
33
+ '運彩',
34
+ '兼職',
35
+ '抽獎',
36
+ '免費領',
37
+ '私訊',
38
+ '點擊連結',
39
+ '主頁連結',
40
+ '減肥',
41
+ '微商',
42
+ '外匯',
43
+ '虛擬幣',
44
+ '心靈雞湯',
45
+ '穿搭',
46
+ '搞笑',
47
+ '自拍',
48
+ '八卦',
49
+ '暴富',
50
+ '躺平',
51
+ '加盟',
52
+ ],
53
+ minCaptionLength: 30,
54
+ requireBodyMatch: true,
55
+ strictContentCheck: true,
56
+ personaPrompt: '你是一位洞察力敏銳、品味高雅的社群特助。請以極高標準評估這篇 Instagram 貼文是否深度符合主人的興趣偏好。若是具備前沿乾貨、創意見解或高實用價值的精選內容,請給出高分評價並撰寫簡短自然、熱情切題的推坑一句話。',
57
+ minScore: 80,
9
58
  dailyRecommendCap: 5,
10
59
  scrollIntervalMinutes: 60,
11
60
  autoScrollEnabled: true,
12
61
  likeOnRecommend: true,
13
62
  batchSize: 15,
63
+ maxRecommendsPerRun: 1,
64
+ minPostReadDelaySeconds: 3,
65
+ maxPostReadDelaySeconds: 7,
66
+ cooldownOnBlockMinutes: 30,
14
67
  };
15
68
  export function unwrapScrollValue(val, fallback) {
16
69
  if (val === null || val === undefined)
@@ -26,18 +79,37 @@ export function unwrapScrollValue(val, fallback) {
26
79
  }
27
80
  return val;
28
81
  }
82
+ function parseListValue(val, fallback = []) {
83
+ if (Array.isArray(val)) {
84
+ return val.map((s) => String(s || '').trim()).filter(Boolean);
85
+ }
86
+ if (typeof val === 'string') {
87
+ return val.split(/[,,\n]+/).map((s) => s.trim().replace(/^#/, '')).filter(Boolean);
88
+ }
89
+ return fallback;
90
+ }
29
91
  export function sanitizeScrollConfig(raw) {
30
92
  const c = raw || {};
31
93
  return {
32
94
  masterUsername: unwrapScrollValue(c.masterUsername, defaultScrollConfig.masterUsername || ''),
33
- interestTags: Array.isArray(c.interestTags) ? c.interestTags : (defaultScrollConfig.interestTags || []),
95
+ interestTags: parseListValue(c.interestTags, defaultScrollConfig.interestTags || []),
96
+ userInterests: unwrapScrollValue(c.userInterests, defaultScrollConfig.userInterests || ''),
97
+ positiveKeywords: parseListValue(c.positiveKeywords, defaultScrollConfig.positiveKeywords || []),
98
+ negativeKeywords: parseListValue(c.negativeKeywords, defaultScrollConfig.negativeKeywords || []),
99
+ minCaptionLength: unwrapScrollValue(c.minCaptionLength, defaultScrollConfig.minCaptionLength || 30),
100
+ requireBodyMatch: unwrapScrollValue(c.requireBodyMatch, defaultScrollConfig.requireBodyMatch ?? true),
101
+ strictContentCheck: unwrapScrollValue(c.strictContentCheck, defaultScrollConfig.strictContentCheck ?? true),
34
102
  personaPrompt: unwrapScrollValue(c.personaPrompt, defaultScrollConfig.personaPrompt || ''),
35
- minScore: unwrapScrollValue(c.minScore, defaultScrollConfig.minScore || 75),
103
+ minScore: unwrapScrollValue(c.minScore, defaultScrollConfig.minScore || 80),
36
104
  dailyRecommendCap: unwrapScrollValue(c.dailyRecommendCap, defaultScrollConfig.dailyRecommendCap || 5),
37
105
  scrollIntervalMinutes: unwrapScrollValue(c.scrollIntervalMinutes, defaultScrollConfig.scrollIntervalMinutes || 60),
38
106
  autoScrollEnabled: unwrapScrollValue(c.autoScrollEnabled, defaultScrollConfig.autoScrollEnabled ?? true),
39
107
  likeOnRecommend: unwrapScrollValue(c.likeOnRecommend, defaultScrollConfig.likeOnRecommend ?? true),
40
108
  batchSize: unwrapScrollValue(c.batchSize, defaultScrollConfig.batchSize || 15),
109
+ maxRecommendsPerRun: unwrapScrollValue(c.maxRecommendsPerRun, defaultScrollConfig.maxRecommendsPerRun || 1),
110
+ minPostReadDelaySeconds: unwrapScrollValue(c.minPostReadDelaySeconds, defaultScrollConfig.minPostReadDelaySeconds || 3),
111
+ maxPostReadDelaySeconds: unwrapScrollValue(c.maxPostReadDelaySeconds, defaultScrollConfig.maxPostReadDelaySeconds || 7),
112
+ cooldownOnBlockMinutes: unwrapScrollValue(c.cooldownOnBlockMinutes, defaultScrollConfig.cooldownOnBlockMinutes || 30),
41
113
  };
42
114
  }
43
115
  export function buildScrollConfigSchema(z) {
@@ -48,16 +120,40 @@ export function buildScrollConfigSchema(z) {
48
120
  .description('主人的 Instagram 使用者名稱 (發現好文時將在留言區 @主人推薦)'),
49
121
  interestTags: z
50
122
  .array(z.string())
51
- .default(['ai', 'deepseek', 'coding', 'tech'])
52
- .description('主人感興趣的主題標籤或領域關鍵字清單'),
123
+ .default(['ai', 'deepseek', 'coding', 'tech', 'gadgets'])
124
+ .description('探索的主題標籤清單 (僅用於初步探索抓取,不作為推薦唯一判斷依據)'),
125
+ userInterests: z
126
+ .string()
127
+ .default(defaultScrollConfig.userInterests)
128
+ .description('主人喜歡的內容風格與領域詳細描述 (AI 將依此深度審查貼文實質內容)'),
129
+ positiveKeywords: z
130
+ .array(z.string())
131
+ .default(defaultScrollConfig.positiveKeywords)
132
+ .description('正文必須討論或偏好的正面核心關鍵字清單'),
133
+ negativeKeywords: z
134
+ .array(z.string())
135
+ .default(defaultScrollConfig.negativeKeywords)
136
+ .description('負向排除關鍵字 (命中時直接拒絕推薦,避免廣告、抽獎或廢文)'),
137
+ minCaptionLength: z
138
+ .number()
139
+ .default(30)
140
+ .description('正文有效文字最小字數要求 (扣除 #標籤 後,過短或無實質內容者直接過濾)'),
141
+ requireBodyMatch: z
142
+ .boolean()
143
+ .default(true)
144
+ .description('正文實質內容必須命中主人偏好主題 (拒絕僅在文末蹭 tag 的貼文)'),
145
+ strictContentCheck: z
146
+ .boolean()
147
+ .default(true)
148
+ .description('啟用嚴格內容審查機制 (結合 LLM / 正文語義深度質檢)'),
53
149
  personaPrompt: z
54
150
  .string()
55
151
  .default(defaultScrollConfig.personaPrompt)
56
152
  .description('評估貼文趣味度與撰寫 @主人推薦語的人設提示詞'),
57
153
  minScore: z
58
154
  .number()
59
- .default(75)
60
- .description('推坑門檻分數 (0-100,達標才在貼文下留言 @主人)'),
155
+ .default(80)
156
+ .description('推坑門檻分數 (0-100,達標才在貼文下留言 @主人,避免隨便推薦)'),
61
157
  dailyRecommendCap: z
62
158
  .number()
63
159
  .default(5)
@@ -78,6 +174,22 @@ export function buildScrollConfigSchema(z) {
78
174
  .number()
79
175
  .default(15)
80
176
  .description('每輪滾動瀏覽抓取的貼文筆數 (10-30)'),
177
+ maxRecommendsPerRun: z
178
+ .number()
179
+ .default(1)
180
+ .description('單次滾動瀏覽最多推坑留言則數 (避免短時間連發觸發官方風控)'),
181
+ minPostReadDelaySeconds: z
182
+ .number()
183
+ .default(3)
184
+ .description('每掃描一則貼文前人造擬真閱讀微延遲最小秒數'),
185
+ maxPostReadDelaySeconds: z
186
+ .number()
187
+ .default(7)
188
+ .description('每掃描一則貼文前人造擬真閱讀微延遲最大秒數'),
189
+ cooldownOnBlockMinutes: z
190
+ .number()
191
+ .default(30)
192
+ .description('遇到 Action Block / 頻率限制時自動熔斷暫停分鐘數'),
81
193
  });
82
194
  }
83
195
  //# sourceMappingURL=config.js.map