@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/client.js CHANGED
@@ -539,16 +539,40 @@ window.__ModuleLoader__.load({
539
539
 
540
540
  const normalizeScrollConfig = (raw) => {
541
541
  const c = (raw && typeof raw === 'object' && !Array.isArray(raw)) ? raw : {}
542
+ const baseTags = cleanArr(c.interestTags, ['art', 'deepseek', 'coding', 'kemono', 'tech'])
542
543
  return {
543
544
  masterUsername: cleanStr(c.masterUsername, ''),
544
- interestTags: cleanArr(c.interestTags, ['ai', 'deepseek', 'coding', 'tech', 'gadgets']),
545
- personaPrompt: cleanStr(c.personaPrompt, '你是一位洞察力敏銳、品味高雅的社群特助。請評估這篇 Instagram 貼文/影片是否具有前沿乾貨、創意見解、實用價值或視覺驚艷。若是主人會感興趣的好內容,請給出大於80分的評價,並撰寫簡短自然、熱情真誠的推坑一句話。'),
546
- minScore: cleanNum(c.minScore, 75),
545
+ fuzzySearchEnabled: cleanBool(c.fuzzySearchEnabled, true),
546
+ fuzzyKeywords: cleanArr(c.fuzzyKeywords, baseTags),
547
+ tagSearchEnabled: cleanBool(c.tagSearchEnabled, false),
548
+ interestTags: baseTags,
549
+ userInterests: cleanStr(
550
+ c.userInterests,
551
+ '專注於前沿 AI 技術 (LLM, Agent, 開源模型)、全端與前端架構、優質開源專案、高效率開發工具、優質 UI/UX 設計或深度乾貨技術解析。偏好乾貨分享、實踐案例與原創見解,不喜歡空洞無物、純蹭熱度、打廣告或純娛樂搞笑的內容。'
552
+ ),
553
+ positiveKeywords: cleanArr(c.positiveKeywords, [
554
+ 'ai', 'deepseek', 'agent', 'coding', '技術', '前端', '後端', '架構', '開源', '教學', '程式', '開發', '模型', 'github', 'prompt', '演算法', '工具', '實戰', '乾貨',
555
+ ]),
556
+ negativeKeywords: cleanArr(c.negativeKeywords, [
557
+ '互粉', '代購', '博彩', '運彩', '兼職', '抽獎', '免費領', '私訊', '點擊連結', '主頁連結', '減肥', '微商', '外匯', '虛擬幣', '心靈雞湯', '穿搭', '搞笑', '自拍', '八卦', '暴富', '躺平', '加盟', 'AI生成', 'AI影片', 'AI動畫', 'AI繪圖', 'AI文章', 'AIGC', 'Midjourney', 'Stable Diffusion', 'Sora', 'Runway', 'Kling', '可靈', '即夢', 'AI配音', 'AI美女',
558
+ ]),
559
+ minCaptionLength: cleanNum(c.minCaptionLength, 30),
560
+ requireBodyMatch: cleanBool(c.requireBodyMatch, true),
561
+ strictContentCheck: cleanBool(c.strictContentCheck, true),
562
+ personaPrompt: cleanStr(
563
+ c.personaPrompt,
564
+ '你是一位洞察力敏銳、品味高雅的社群特助。請以極高標準評估這篇 Instagram 貼文是否深度符合主人的興趣偏好。若是具備前沿乾貨、創意見解或高實用價值的精選內容,請給出高分評價並撰寫簡短自然、熱情切題的推坑一句話。'
565
+ ),
566
+ minScore: cleanNum(c.minScore, 80),
547
567
  dailyRecommendCap: cleanNum(c.dailyRecommendCap, 5),
548
568
  scrollIntervalMinutes: cleanNum(c.scrollIntervalMinutes, 60),
549
569
  autoScrollEnabled: cleanBool(c.autoScrollEnabled, true),
550
570
  likeOnRecommend: cleanBool(c.likeOnRecommend, true),
551
571
  batchSize: cleanNum(c.batchSize, 15),
572
+ maxRecommendsPerRun: cleanNum(c.maxRecommendsPerRun, 1),
573
+ minPostReadDelaySeconds: cleanNum(c.minPostReadDelaySeconds, 3),
574
+ maxPostReadDelaySeconds: cleanNum(c.maxPostReadDelaySeconds, 7),
575
+ cooldownOnBlockMinutes: cleanNum(c.cooldownOnBlockMinutes, 30),
552
576
  }
553
577
  }
554
578
 
@@ -563,6 +587,21 @@ window.__ModuleLoader__.load({
563
587
  const [runningRound, setRunningRound] = useState(false)
564
588
  const [msg, setMsg] = useState(null)
565
589
 
590
+ // 獨立字串狀態:解決 input 鍵入逗號與空格被 React 重新格式化吃掉的問題
591
+ const [fuzzyWordsText, setFuzzyWordsText] = useState('')
592
+ const [tagsInputText, setTagsInputText] = useState('')
593
+ const [positiveWordsText, setPositiveWordsText] = useState('')
594
+ const [negativeWordsText, setNegativeWordsText] = useState('')
595
+
596
+ const parseListFromText = (text, isTag = false) => {
597
+ if (typeof text !== 'string') return []
598
+ return text
599
+ .split(/[,,\n]+/)
600
+ .map((s) => s.trim())
601
+ .map((s) => (isTag ? s.replace(/^#/, '') : s))
602
+ .filter(Boolean)
603
+ }
604
+
566
605
  const refresh = async () => {
567
606
  try {
568
607
  const [cfgRes, statusRes, reportRes, mcpRes] = await Promise.all([
@@ -573,7 +612,14 @@ window.__ModuleLoader__.load({
573
612
  .then((r) => r.json())
574
613
  .catch(() => null),
575
614
  ])
576
- if (cfgRes?.ok) setCfg(normalizeScrollConfig(cfgRes.config))
615
+ if (cfgRes?.ok) {
616
+ const loaded = normalizeScrollConfig(cfgRes.config)
617
+ setCfg(loaded)
618
+ setFuzzyWordsText(loaded.fuzzyKeywords.join(', '))
619
+ setTagsInputText(loaded.interestTags.join(', '))
620
+ setPositiveWordsText(loaded.positiveKeywords.join(', '))
621
+ setNegativeWordsText(loaded.negativeKeywords.join(', '))
622
+ }
577
623
  if (statusRes?.ok) setStatus(statusRes)
578
624
  if (reportRes?.ok) setReport(reportRes.report || '')
579
625
  if (mcpRes?.ok) setMcpStatus(mcpRes)
@@ -594,10 +640,21 @@ window.__ModuleLoader__.load({
594
640
  setSaving(true)
595
641
  setMsg(null)
596
642
  try {
597
- const cleanPayload = normalizeScrollConfig(cfg)
643
+ const cleanPayload = {
644
+ ...normalizeScrollConfig(cfg),
645
+ fuzzyKeywords: parseListFromText(fuzzyWordsText, true),
646
+ interestTags: parseListFromText(tagsInputText, true),
647
+ positiveKeywords: parseListFromText(positiveWordsText, false),
648
+ negativeKeywords: parseListFromText(negativeWordsText, false),
649
+ }
598
650
  const res = await jpost('/config', cleanPayload)
599
651
  if (res?.ok) {
600
- setCfg(normalizeScrollConfig(res.config))
652
+ const saved = normalizeScrollConfig(res.config)
653
+ setCfg(saved)
654
+ setFuzzyWordsText(saved.fuzzyKeywords.join(', '))
655
+ setTagsInputText(saved.interestTags.join(', '))
656
+ setPositiveWordsText(saved.positiveKeywords.join(', '))
657
+ setNegativeWordsText(saved.negativeKeywords.join(', '))
601
658
  setMsg('✅ 滾動推坑設定已成功保存!')
602
659
  }
603
660
  } catch (e) {
@@ -640,11 +697,23 @@ window.__ModuleLoader__.load({
640
697
  const rawAccount =
641
698
  status?.mcpAccount ||
642
699
  status?.loginStatus?.username ||
700
+ mcpStatus?.botUsername ||
643
701
  mcpStatus?.loginStatus?.username ||
644
702
  (mcpStatus?.loginStatus?.message?.match(/@([^\s)]+)/)?.[1]) ||
645
703
  (isMcpConnected ? '已連線' : '')
646
704
 
647
- const displayAccount = rawAccount ? rawAccount.replace(/^@/, '') : ''
705
+ let cachedMcpAccounts = {}
706
+ try {
707
+ cachedMcpAccounts = JSON.parse(localStorage.getItem('dsh_ig_mcp_accounts') || '{}') || {}
708
+ } catch {}
709
+ const displayAccount = (cachedMcpAccounts.username || rawAccount || '').replace(/^@/, '')
710
+ const displayMaster = (
711
+ cachedMcpAccounts.masterUsername ||
712
+ mcpStatus?.masterUsername ||
713
+ status?.masterUsername ||
714
+ cfg.masterUsername ||
715
+ ''
716
+ ).replace(/^@/, '')
648
717
 
649
718
  return h(
650
719
  'div',
@@ -665,7 +734,7 @@ window.__ModuleLoader__.load({
665
734
  h(
666
735
  'div',
667
736
  { className: 'ig-scr-brand-sub', style: { display: 'flex', alignItems: 'center', gap: '8px' } },
668
- cfg.masterUsername ? `目標主人: @${cfg.masterUsername.replace(/^@/, '')}` : '尚未綁定主人帳號',
737
+ displayMaster ? `目標主人 (繼承自 MCP): @${displayMaster}` : '尚未於 MCP 設定主人帳號',
669
738
  h('span', { style: { color: 'rgba(255, 255, 255, 0.2)' } }, '|'),
670
739
  isMcpConnected
671
740
  ? h(
@@ -856,7 +925,7 @@ window.__ModuleLoader__.load({
856
925
  h('div', { className: 'ig-scr-section-desc' }, '設定主人的 Instagram 帳號名與感興趣的標籤領域,AI 將依此進行嚴格內容篩選。')
857
926
  ),
858
927
 
859
- // MCP Session 來源提示
928
+ // MCP Session 與主人帳號來源提示 (直接繼承自 MCP,無需重複輸入)
860
929
  h(
861
930
  'div',
862
931
  {
@@ -864,52 +933,211 @@ window.__ModuleLoader__.load({
864
933
  background: 'rgba(59, 130, 246, 0.06)',
865
934
  border: '1px solid rgba(59, 130, 246, 0.2)',
866
935
  borderRadius: '8px',
867
- padding: '10px 14px',
936
+ padding: '12px 14px',
868
937
  fontSize: '12px',
869
938
  color: '#93c5fd',
870
939
  display: 'flex',
871
- alignItems: 'center',
872
- gap: '8px',
940
+ flexDirection: 'column',
941
+ gap: '6px',
873
942
  },
874
943
  },
875
- h(Icon.Sparkles, { size: 14 }),
876
944
  h(
877
- 'span',
878
- null,
879
- isMcpConnected
880
- ? `已沿用 @luoyi-dsh/instagram-mcp 的登入會話 (@${displayAccount || '已授權'}),本外掛無需填寫帳密。`
881
- : '本外掛無需輸入帳密,直接沿用依賴的 dsh-instagram-mcp 獲取之 Session。目前 MCP 尚未登入,請先至 MCP 面板完成登入。'
945
+ 'div',
946
+ { style: { display: 'flex', alignItems: 'center', gap: '8px', fontWeight: 600 } },
947
+ h(Icon.Sparkles, { size: 14 }),
948
+ h(
949
+ 'span',
950
+ null,
951
+ '機器人與主人帳號已統一由 dsh-instagram-mcp 管理,本外掛無需重複輸入'
952
+ )
953
+ ),
954
+ h(
955
+ 'div',
956
+ { style: { display: 'flex', flexWrap: 'wrap', gap: '16px', color: '#e2e8f0', paddingLeft: '22px' } },
957
+ h('span', null, `🤖 機器人帳號: ${displayAccount ? `@${displayAccount}` : '尚未於 MCP 設定'}`),
958
+ h('span', null, `🎯 主人帳號 (推坑 @ 對象): ${displayMaster ? `@${displayMaster}` : '尚未於 MCP 設定'}`)
959
+ )
960
+ ),
961
+
962
+ // 正文實質內容審查開關
963
+ h(
964
+ 'div',
965
+ { className: 'ig-scr-switch' },
966
+ h(
967
+ 'div',
968
+ { className: 'ig-scr-switch-text' },
969
+ h('div', { className: 'ig-scr-switch-name' }, '🛡️ 啟用 @ 前正文內容深度審查'),
970
+ h('div', { className: 'ig-scr-hint' }, '拒絕隨便推薦!留言 @ 主人前深度檢查正文實質乾貨與品質,剔除純圖廢文與空洞標籤')
971
+ ),
972
+ h(
973
+ 'button',
974
+ {
975
+ className: `ig-scr-toggle ${cfg.strictContentCheck ? 'on' : ''}`,
976
+ onClick: () => updateField('strictContentCheck', !cfg.strictContentCheck),
977
+ },
978
+ h('span', { className: 'ig-scr-toggle-thumb' })
979
+ )
980
+ ),
981
+
982
+ // 正文實質命中開關
983
+ h(
984
+ 'div',
985
+ { className: 'ig-scr-switch' },
986
+ h(
987
+ 'div',
988
+ { className: 'ig-scr-switch-text' },
989
+ h('div', { className: 'ig-scr-switch-name' }, '🔍 正文必須實質討論偏好主題 (拒絕僅在 tag 蹭熱度)'),
990
+ h('div', { className: 'ig-scr-hint' }, '關鍵詞必須在正文中真正出現;若僅在文末堆砌 #標籤 則視為標籤農場直接過濾')
991
+ ),
992
+ h(
993
+ 'button',
994
+ {
995
+ className: `ig-scr-toggle ${cfg.requireBodyMatch ? 'on' : ''}`,
996
+ onClick: () => updateField('requireBodyMatch', !cfg.requireBodyMatch),
997
+ },
998
+ h('span', { className: 'ig-scr-toggle-thumb' })
882
999
  )
883
1000
  ),
884
1001
 
1002
+ // 主人喜歡的內容風格與偏好詳述
1003
+ h(
1004
+ 'div',
1005
+ { className: 'ig-scr-field' },
1006
+ h('label', { className: 'ig-scr-label' }, '💎 主人喜歡的內容風格與領域詳述 (User Interests)'),
1007
+ h('textarea', {
1008
+ className: 'ig-scr-textarea',
1009
+ style: { minHeight: '75px' },
1010
+ value: cleanStr(cfg.userInterests),
1011
+ placeholder: '詳細描述您喜歡的貼文類型、技術領域、內容風格與乾貨要求...',
1012
+ onChange: (e) => updateField('userInterests', e.target.value),
1013
+ }),
1014
+ h('div', { className: 'ig-scr-hint' }, 'AI 將依據此描述深度比對正文語義,確保推薦完全符合您的個人品味與需求')
1015
+ ),
1016
+
1017
+ // 正向關鍵字
885
1018
  h(
886
1019
  'div',
887
1020
  { className: 'ig-scr-field' },
888
- h('label', { className: 'ig-scr-label' }, '🎯 主人 Instagram 帳號 (Master Username)'),
1021
+ h('label', { className: 'ig-scr-label' }, '✨ 正文正向領域關鍵字 (Positive Keywords)'),
889
1022
  h('input', {
890
1023
  className: 'ig-scr-input',
891
- value: cleanStr(cfg.masterUsername),
892
- placeholder: '例如 your_main_account (可不帶 @)',
893
- onChange: (e) => updateField('masterUsername', e.target.value.replace(/^@/, '').trim()),
1024
+ value: positiveWordsText,
1025
+ placeholder: 'ai, deepseek, coding, 前端, 架構, 開源, 教學, 工具',
1026
+ onChange: (e) => {
1027
+ const raw = e.target.value
1028
+ setPositiveWordsText(raw)
1029
+ updateField('positiveKeywords', parseListFromText(raw, false))
1030
+ },
894
1031
  }),
895
- h('div', { className: 'ig-scr-hint' }, '發現值得推薦的貼文/影片時,會在留言區 @ 此帳號進行推薦')
1032
+ h('div', { className: 'ig-scr-hint' }, '正文實質提及這些關鍵詞或相關領域時給予高權重加分')
896
1033
  ),
897
1034
 
1035
+ // 負向排除詞
898
1036
  h(
899
1037
  'div',
900
1038
  { className: 'ig-scr-field' },
901
- h('label', { className: 'ig-scr-label' }, '🏷️ 主人感興趣的主題標籤 (Interest Tags)'),
1039
+ h('label', { className: 'ig-scr-label' }, '🚫 負向排除關鍵字 (Negative Keywords / Anti-Spam)'),
902
1040
  h('input', {
903
1041
  className: 'ig-scr-input',
904
- value: Array.isArray(cfg.interestTags) ? cfg.interestTags.join(', ') : '',
905
- placeholder: 'ai, deepseek, coding, photography, gadgets, travel',
906
- onChange: (e) =>
907
- updateField(
908
- 'interestTags',
909
- e.target.value.split(',').map((s) => s.trim().replace(/^#/, '')).filter(Boolean)
910
- ),
1042
+ value: negativeWordsText,
1043
+ placeholder: '互粉, 代購, 博彩, 抽獎, 微商, 減肥, 穿搭, 搞笑, 自拍',
1044
+ onChange: (e) => {
1045
+ const raw = e.target.value
1046
+ setNegativeWordsText(raw)
1047
+ updateField('negativeKeywords', parseListFromText(raw, false))
1048
+ },
911
1049
  }),
912
- h('div', { className: 'ig-scr-hint' }, '多個標籤請用逗號分隔,除時間軸外亦會輪流針對這些主題進行深度探索')
1050
+ h('div', { className: 'ig-scr-hint' }, '命中任一排除詞直接判定不合格,杜絕廣告宣傳、抽獎促銷與無意義廢文')
1051
+ ),
1052
+
1053
+ // 模糊搜尋探索開關 (預設開啟)
1054
+ h(
1055
+ 'div',
1056
+ { className: 'ig-scr-switch' },
1057
+ h(
1058
+ 'div',
1059
+ { className: 'ig-scr-switch-text' },
1060
+ h('div', { className: 'ig-scr-switch-name' }, '🔎 啟用模糊搜尋抓取貼文 (Fuzzy Search — 推薦)'),
1061
+ h('div', { className: 'ig-scr-hint' }, '結合 Instagram 綜合關鍵字 SERP、Reels 搜尋與相關創作者貼文,解決單純 Tags 搜尋一直抓到 0 則貼文的問題')
1062
+ ),
1063
+ h(
1064
+ 'button',
1065
+ {
1066
+ className: `ig-scr-toggle ${cfg.fuzzySearchEnabled !== false ? 'on' : ''}`,
1067
+ onClick: () => updateField('fuzzySearchEnabled', !(cfg.fuzzySearchEnabled !== false)),
1068
+ },
1069
+ h('span', { className: 'ig-scr-toggle-thumb' })
1070
+ )
1071
+ ),
1072
+
1073
+ h(
1074
+ 'div',
1075
+ { className: 'ig-scr-field', style: { opacity: cfg.fuzzySearchEnabled !== false ? 1 : 0.6 } },
1076
+ h('label', { className: 'ig-scr-label' }, '🔎 模糊搜尋關鍵字 (Fuzzy Search Keywords)'),
1077
+ h('input', {
1078
+ className: 'ig-scr-input',
1079
+ value: fuzzyWordsText,
1080
+ placeholder: 'ai, deepseek, coding, tech, gadgets',
1081
+ onChange: (e) => {
1082
+ const raw = e.target.value
1083
+ setFuzzyWordsText(raw)
1084
+ updateField('fuzzyKeywords', parseListFromText(raw, true))
1085
+ },
1086
+ }),
1087
+ h('div', { className: 'ig-scr-hint' }, '每輪滾動時隨機挑選關鍵字進行綜合模糊搜尋抓取貼文 (支援逗號分隔)')
1088
+ ),
1089
+
1090
+ // 主題標籤搜尋開關 (保留功能,用按鈕開關,預設關閉)
1091
+ h(
1092
+ 'div',
1093
+ { className: 'ig-scr-switch' },
1094
+ h(
1095
+ 'div',
1096
+ { className: 'ig-scr-switch-text' },
1097
+ h('div', { className: 'ig-scr-switch-name' }, '🏷️ 啟用主題標籤搜尋 (Tags Search — 保留備用)'),
1098
+ h('div', { className: 'ig-scr-hint' }, '預設關閉(因 IG 官方常限制公開 Hashtag API 返回 0 則貼文);保留此設定,可隨時點擊按鈕開關')
1099
+ ),
1100
+ h(
1101
+ 'button',
1102
+ {
1103
+ className: `ig-scr-toggle ${cfg.tagSearchEnabled ? 'on' : ''}`,
1104
+ onClick: () => updateField('tagSearchEnabled', !cfg.tagSearchEnabled),
1105
+ },
1106
+ h('span', { className: 'ig-scr-toggle-thumb' })
1107
+ )
1108
+ ),
1109
+
1110
+ h(
1111
+ 'div',
1112
+ { className: 'ig-scr-grid-2' },
1113
+ h(
1114
+ 'div',
1115
+ { className: 'ig-scr-field', style: { opacity: cfg.tagSearchEnabled ? 1 : 0.6 } },
1116
+ h('label', { className: 'ig-scr-label' }, '🏷️ 探索主題標籤 (Interest Tags — 開啟上方按鈕時生效)'),
1117
+ h('input', {
1118
+ className: 'ig-scr-input',
1119
+ value: tagsInputText,
1120
+ placeholder: 'ai, deepseek, coding, tech, gadgets',
1121
+ onChange: (e) => {
1122
+ const raw = e.target.value
1123
+ setTagsInputText(raw)
1124
+ updateField('interestTags', parseListFromText(raw, true))
1125
+ },
1126
+ }),
1127
+ h('div', { className: 'ig-scr-hint' }, '保留之主題標籤清單 (支援空格與中英文逗號)')
1128
+ ),
1129
+ h(
1130
+ 'div',
1131
+ { className: 'ig-scr-field' },
1132
+ h('label', { className: 'ig-scr-label' }, '📏 正文有效文字最小長度 (字數)'),
1133
+ h('input', {
1134
+ type: 'number',
1135
+ className: 'ig-scr-input',
1136
+ value: cfg.minCaptionLength ?? 30,
1137
+ onChange: (e) => updateField('minCaptionLength', Number(e.target.value)),
1138
+ }),
1139
+ h('div', { className: 'ig-scr-hint' }, '扣除 #標籤 後的正文字數門檻')
1140
+ )
913
1141
  ),
914
1142
 
915
1143
  h(
@@ -1017,7 +1245,7 @@ window.__ModuleLoader__.load({
1017
1245
  h('input', {
1018
1246
  type: 'number',
1019
1247
  className: 'ig-scr-input',
1020
- value: cfg.minScore ?? 75,
1248
+ value: cfg.minScore ?? 80,
1021
1249
  onChange: (e) => updateField('minScore', Number(e.target.value)),
1022
1250
  }),
1023
1251
  h('div', { className: 'ig-scr-hint' }, '高於此分數的貼文才會在留言區 @主人')
package/dist/config.d.ts CHANGED
@@ -3,7 +3,16 @@
3
3
  */
4
4
  export interface InstagramScrollConfig {
5
5
  masterUsername?: string;
6
+ fuzzySearchEnabled?: boolean;
7
+ fuzzyKeywords?: string[];
8
+ tagSearchEnabled?: boolean;
6
9
  interestTags?: string[];
10
+ userInterests?: string;
11
+ positiveKeywords?: string[];
12
+ negativeKeywords?: string[];
13
+ minCaptionLength?: number;
14
+ requireBodyMatch?: boolean;
15
+ strictContentCheck?: boolean;
7
16
  personaPrompt?: string;
8
17
  minScore?: number;
9
18
  dailyRecommendCap?: number;
@@ -11,6 +20,10 @@ export interface InstagramScrollConfig {
11
20
  autoScrollEnabled?: boolean;
12
21
  likeOnRecommend?: boolean;
13
22
  batchSize?: number;
23
+ maxRecommendsPerRun?: number;
24
+ minPostReadDelaySeconds?: number;
25
+ maxPostReadDelaySeconds?: number;
26
+ cooldownOnBlockMinutes?: number;
14
27
  }
15
28
  export declare const defaultScrollConfig: InstagramScrollConfig;
16
29
  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,kBAAkB,CAAC,EAAE,OAAO,CAAA;IAC5B,aAAa,CAAC,EAAE,MAAM,EAAE,CAAA;IACxB,gBAAgB,CAAC,EAAE,OAAO,CAAA;IAC1B,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,qBAoFjC,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,CA2BpE;AAED,wBAAgB,uBAAuB,CAAC,CAAC,EAAE,GAAG,OA2F7C"}
package/dist/config.js CHANGED
@@ -3,14 +3,86 @@
3
3
  */
4
4
  export const defaultScrollConfig = {
5
5
  masterUsername: '',
6
- interestTags: ['ai', 'deepseek', 'coding', 'tech', 'gadgets'],
7
- personaPrompt: '你是一位洞察力敏銳、品味高雅的社群特助。請評估這篇 Instagram 貼文/影片是否具有前沿乾貨、創意見解、實用價值或視覺驚艷。若是主人會感興趣的好內容,請給出大於80分的評價,並撰寫簡短自然、熱情真誠的推坑一句話。',
8
- minScore: 75,
6
+ fuzzySearchEnabled: true,
7
+ fuzzyKeywords: ['art', 'deepseek', 'coding', 'kemono', 'tech'],
8
+ tagSearchEnabled: false,
9
+ interestTags: ['art', 'deepseek', 'coding', 'kemono', 'tech'],
10
+ userInterests: '專注於真實人類原創手繪藝術 (插畫、Kemono、角色設計)、前沿軟體技術 (LLM 架構、Agent、開源專案)、全端與前端開發、高效率工具或深度技術解析。偏好真實人類原創作品與實踐乾貨,嚴格排除任何 AI 生成的影片、AI 動畫、AI 繪圖/算圖或 AI 生成的農場文章!',
11
+ positiveKeywords: [
12
+ 'art',
13
+ 'kemono',
14
+ 'deepseek',
15
+ 'agent',
16
+ 'coding',
17
+ '技術',
18
+ '前端',
19
+ '後端',
20
+ '架構',
21
+ '開源',
22
+ '教學',
23
+ '程式',
24
+ '開發',
25
+ '模型',
26
+ 'github',
27
+ 'prompt',
28
+ '演算法',
29
+ '工具',
30
+ '實戰',
31
+ '乾貨',
32
+ ],
33
+ negativeKeywords: [
34
+ '互粉',
35
+ '代購',
36
+ '博彩',
37
+ '運彩',
38
+ '兼職',
39
+ '抽獎',
40
+ '免費領',
41
+ '私訊',
42
+ '點擊連結',
43
+ '主頁連結',
44
+ '減肥',
45
+ '微商',
46
+ '外匯',
47
+ '虛擬幣',
48
+ '心靈雞湯',
49
+ '穿搭',
50
+ '搞笑',
51
+ '自拍',
52
+ '八卦',
53
+ '暴富',
54
+ '躺平',
55
+ '加盟',
56
+ 'AI生成',
57
+ 'AI影片',
58
+ 'AI動畫',
59
+ 'AI繪圖',
60
+ 'AI文章',
61
+ 'AIGC',
62
+ 'Midjourney',
63
+ 'Stable Diffusion',
64
+ 'Sora',
65
+ 'Runway',
66
+ 'Kling',
67
+ '可靈',
68
+ '即夢',
69
+ 'AI配音',
70
+ 'AI美女',
71
+ ],
72
+ minCaptionLength: 30,
73
+ requireBodyMatch: true,
74
+ strictContentCheck: true,
75
+ personaPrompt: '你是一位洞察力敏銳、品味高雅的社群特助。請以極高標準評估這篇 Instagram 貼文是否深度符合主人的興趣偏好。若是具備前沿乾貨、創意見解或高實用價值的精選內容,請給出高分評價並撰寫簡短自然、熱情切題的推坑一句話。',
76
+ minScore: 80,
9
77
  dailyRecommendCap: 5,
10
78
  scrollIntervalMinutes: 60,
11
79
  autoScrollEnabled: true,
12
80
  likeOnRecommend: true,
13
81
  batchSize: 15,
82
+ maxRecommendsPerRun: 1,
83
+ minPostReadDelaySeconds: 3,
84
+ maxPostReadDelaySeconds: 7,
85
+ cooldownOnBlockMinutes: 30,
14
86
  };
15
87
  export function unwrapScrollValue(val, fallback) {
16
88
  if (val === null || val === undefined)
@@ -26,18 +98,41 @@ export function unwrapScrollValue(val, fallback) {
26
98
  }
27
99
  return val;
28
100
  }
101
+ function parseListValue(val, fallback = []) {
102
+ if (Array.isArray(val)) {
103
+ return val.map((s) => String(s || '').trim()).filter(Boolean);
104
+ }
105
+ if (typeof val === 'string') {
106
+ return val.split(/[,,\n]+/).map((s) => s.trim().replace(/^#/, '')).filter(Boolean);
107
+ }
108
+ return fallback;
109
+ }
29
110
  export function sanitizeScrollConfig(raw) {
30
111
  const c = raw || {};
112
+ const parsedTags = parseListValue(c.interestTags, defaultScrollConfig.interestTags || []);
31
113
  return {
32
114
  masterUsername: unwrapScrollValue(c.masterUsername, defaultScrollConfig.masterUsername || ''),
33
- interestTags: Array.isArray(c.interestTags) ? c.interestTags : (defaultScrollConfig.interestTags || []),
115
+ fuzzySearchEnabled: unwrapScrollValue(c.fuzzySearchEnabled, defaultScrollConfig.fuzzySearchEnabled ?? true),
116
+ fuzzyKeywords: parseListValue(c.fuzzyKeywords, parsedTags.length > 0 ? parsedTags : (defaultScrollConfig.fuzzyKeywords || [])),
117
+ tagSearchEnabled: unwrapScrollValue(c.tagSearchEnabled, defaultScrollConfig.tagSearchEnabled ?? false),
118
+ interestTags: parsedTags,
119
+ userInterests: unwrapScrollValue(c.userInterests, defaultScrollConfig.userInterests || ''),
120
+ positiveKeywords: parseListValue(c.positiveKeywords, defaultScrollConfig.positiveKeywords || []),
121
+ negativeKeywords: parseListValue(c.negativeKeywords, defaultScrollConfig.negativeKeywords || []),
122
+ minCaptionLength: unwrapScrollValue(c.minCaptionLength, defaultScrollConfig.minCaptionLength || 30),
123
+ requireBodyMatch: unwrapScrollValue(c.requireBodyMatch, defaultScrollConfig.requireBodyMatch ?? true),
124
+ strictContentCheck: unwrapScrollValue(c.strictContentCheck, defaultScrollConfig.strictContentCheck ?? true),
34
125
  personaPrompt: unwrapScrollValue(c.personaPrompt, defaultScrollConfig.personaPrompt || ''),
35
- minScore: unwrapScrollValue(c.minScore, defaultScrollConfig.minScore || 75),
126
+ minScore: unwrapScrollValue(c.minScore, defaultScrollConfig.minScore || 80),
36
127
  dailyRecommendCap: unwrapScrollValue(c.dailyRecommendCap, defaultScrollConfig.dailyRecommendCap || 5),
37
128
  scrollIntervalMinutes: unwrapScrollValue(c.scrollIntervalMinutes, defaultScrollConfig.scrollIntervalMinutes || 60),
38
129
  autoScrollEnabled: unwrapScrollValue(c.autoScrollEnabled, defaultScrollConfig.autoScrollEnabled ?? true),
39
130
  likeOnRecommend: unwrapScrollValue(c.likeOnRecommend, defaultScrollConfig.likeOnRecommend ?? true),
40
131
  batchSize: unwrapScrollValue(c.batchSize, defaultScrollConfig.batchSize || 15),
132
+ maxRecommendsPerRun: unwrapScrollValue(c.maxRecommendsPerRun, defaultScrollConfig.maxRecommendsPerRun || 1),
133
+ minPostReadDelaySeconds: unwrapScrollValue(c.minPostReadDelaySeconds, defaultScrollConfig.minPostReadDelaySeconds || 3),
134
+ maxPostReadDelaySeconds: unwrapScrollValue(c.maxPostReadDelaySeconds, defaultScrollConfig.maxPostReadDelaySeconds || 7),
135
+ cooldownOnBlockMinutes: unwrapScrollValue(c.cooldownOnBlockMinutes, defaultScrollConfig.cooldownOnBlockMinutes || 30),
41
136
  };
42
137
  }
43
138
  export function buildScrollConfigSchema(z) {
@@ -46,18 +141,54 @@ export function buildScrollConfigSchema(z) {
46
141
  .string()
47
142
  .default('')
48
143
  .description('主人的 Instagram 使用者名稱 (發現好文時將在留言區 @主人推薦)'),
144
+ fuzzySearchEnabled: z
145
+ .boolean()
146
+ .default(true)
147
+ .description('是否啟用模糊搜尋探索貼文 (預設開啟,解決單純 Tags 搜尋抓到 0 則貼文的問題)'),
148
+ fuzzyKeywords: z
149
+ .array(z.string())
150
+ .default(['ai', 'deepseek', 'coding', 'tech', 'gadgets'])
151
+ .description('模糊搜尋關鍵字清單 (結合綜合 SERP、Reels 搜尋與相關創作者貼文)'),
152
+ tagSearchEnabled: z
153
+ .boolean()
154
+ .default(false)
155
+ .description('是否啟用主題標籤 #Tag 搜尋 (預設關閉,保留並可用按鈕隨時開關)'),
49
156
  interestTags: z
50
157
  .array(z.string())
51
- .default(['ai', 'deepseek', 'coding', 'tech'])
52
- .description('主人感興趣的主題標籤或領域關鍵字清單'),
158
+ .default(['ai', 'deepseek', 'coding', 'tech', 'gadgets'])
159
+ .description('探索的主題標籤清單 (保留設定,當開啟 Tags 搜尋按鈕時使用)'),
160
+ userInterests: z
161
+ .string()
162
+ .default(defaultScrollConfig.userInterests)
163
+ .description('主人喜歡的內容風格與領域詳細描述 (AI 將依此深度審查貼文實質內容)'),
164
+ positiveKeywords: z
165
+ .array(z.string())
166
+ .default(defaultScrollConfig.positiveKeywords)
167
+ .description('正文必須討論或偏好的正面核心關鍵字清單'),
168
+ negativeKeywords: z
169
+ .array(z.string())
170
+ .default(defaultScrollConfig.negativeKeywords)
171
+ .description('負向排除關鍵字 (命中時直接拒絕推薦,避免廣告、抽獎或廢文)'),
172
+ minCaptionLength: z
173
+ .number()
174
+ .default(30)
175
+ .description('正文有效文字最小字數要求 (扣除 #標籤 後,過短或無實質內容者直接過濾)'),
176
+ requireBodyMatch: z
177
+ .boolean()
178
+ .default(true)
179
+ .description('正文實質內容必須命中主人偏好主題 (拒絕僅在文末蹭 tag 的貼文)'),
180
+ strictContentCheck: z
181
+ .boolean()
182
+ .default(true)
183
+ .description('啟用嚴格內容審查機制 (結合 LLM / 正文語義深度質檢)'),
53
184
  personaPrompt: z
54
185
  .string()
55
186
  .default(defaultScrollConfig.personaPrompt)
56
187
  .description('評估貼文趣味度與撰寫 @主人推薦語的人設提示詞'),
57
188
  minScore: z
58
189
  .number()
59
- .default(75)
60
- .description('推坑門檻分數 (0-100,達標才在貼文下留言 @主人)'),
190
+ .default(80)
191
+ .description('推坑門檻分數 (0-100,達標才在貼文下留言 @主人,避免隨便推薦)'),
61
192
  dailyRecommendCap: z
62
193
  .number()
63
194
  .default(5)
@@ -78,6 +209,22 @@ export function buildScrollConfigSchema(z) {
78
209
  .number()
79
210
  .default(15)
80
211
  .description('每輪滾動瀏覽抓取的貼文筆數 (10-30)'),
212
+ maxRecommendsPerRun: z
213
+ .number()
214
+ .default(1)
215
+ .description('單次滾動瀏覽最多推坑留言則數 (避免短時間連發觸發官方風控)'),
216
+ minPostReadDelaySeconds: z
217
+ .number()
218
+ .default(3)
219
+ .description('每掃描一則貼文前人造擬真閱讀微延遲最小秒數'),
220
+ maxPostReadDelaySeconds: z
221
+ .number()
222
+ .default(7)
223
+ .description('每掃描一則貼文前人造擬真閱讀微延遲最大秒數'),
224
+ cooldownOnBlockMinutes: z
225
+ .number()
226
+ .default(30)
227
+ .description('遇到 Action Block / 頻率限制時自動熔斷暫停分鐘數'),
81
228
  });
82
229
  }
83
230
  //# sourceMappingURL=config.js.map