This commit is contained in:
renzhiyuan 2026-04-08 03:15:16 +08:00
parent 83f7d34f3e
commit 4538a300d7
13 changed files with 961 additions and 629 deletions

View File

@ -1 +1 @@
[]
[{"name":"loadts","value":"1775587764105","domain":".xiaohongshu.com","path":"/","expires":1807123764,"size":19,"httpOnly":false,"secure":false,"session":false,"priority":"Medium","sameParty":false,"sourceScheme":"Secure","sourcePort":443},{"name":"sec_poison_id","value":"e2a04087-a39c-4880-8ae0-b5e883c79cf9","domain":".xiaohongshu.com","path":"/","expires":1775587813,"size":49,"httpOnly":false,"secure":false,"session":false,"priority":"Medium","sameParty":false,"sourceScheme":"Secure","sourcePort":443},{"name":"websectiga","value":"7750c37de43b7be9de8ed9ff8ea0e576519e8cd2157322eb972ecb429a7735d4","domain":".xiaohongshu.com","path":"/","expires":1775846408,"size":74,"httpOnly":false,"secure":false,"session":false,"priority":"Medium","sameParty":false,"sourceScheme":"Secure","sourcePort":443},{"name":"a1","value":"19d683c737fwjbhd74tlf1z8pn3k0yzvdu3kqz8qk30000423644","domain":".xiaohongshu.com","path":"/","expires":1807106285,"size":54,"httpOnly":false,"secure":false,"session":false,"priority":"Medium","sameParty":false,"sourceScheme":"Secure","sourcePort":443},{"name":"access-token-creator.xiaohongshu.com","value":"customer.creator.AT-68c517626043192484708355gq1bgv9gaekvksnp","domain":".xiaohongshu.com","path":"/","expires":1778168281.361146,"size":96,"httpOnly":true,"secure":false,"session":false,"priority":"Medium","sameParty":false,"sourceScheme":"Secure","sourcePort":443},{"name":"acw_tc","value":"0a0d096b17755871571385002e93f7de908303182e2b596681bf8035f47188","domain":"creator.xiaohongshu.com","path":"/","expires":1775588693.515565,"size":68,"httpOnly":true,"secure":false,"session":false,"priority":"Medium","sameParty":false,"sourceScheme":"Secure","sourcePort":443},{"name":"customerClientId","value":"104269762757230","domain":".xiaohongshu.com","path":"/","expires":1810130303.328316,"size":31,"httpOnly":true,"secure":false,"session":false,"priority":"Medium","sameParty":false,"sourceScheme":"Secure","sourcePort":443},{"name":"galaxy_creator_session_id","value":"p2OOUhXT3z4D6OTUiVUO4G9xfaqTgRUYLlRG","domain":".xiaohongshu.com","path":"/","expires":1778168282.361207,"size":61,"httpOnly":true,"secure":false,"session":false,"priority":"Medium","sameParty":false,"sourceScheme":"Secure","sourcePort":443},{"name":"galaxy.creator.beaker.session.id","value":"1775576545935015586770","domain":".xiaohongshu.com","path":"/","expires":1778168282.361281,"size":54,"httpOnly":true,"secure":false,"session":false,"priority":"Medium","sameParty":false,"sourceScheme":"Secure","sourcePort":443},{"name":"x-user-id-creator.xiaohongshu.com","value":"65d74a4c0000000005032a98","domain":".xiaohongshu.com","path":"/","expires":1810136282.361083,"size":57,"httpOnly":true,"secure":false,"session":false,"priority":"Medium","sameParty":false,"sourceScheme":"Secure","sourcePort":443},{"name":"customer-sso-sid","value":"68c517626017512875294727zy1y0kednadz2ql9","domain":".xiaohongshu.com","path":"/","expires":1776175103.360961,"size":56,"httpOnly":true,"secure":false,"session":false,"priority":"Medium","sameParty":false,"sourceScheme":"Secure","sourcePort":443},{"name":"gid","value":"yjfKY48YfqfyyjfKYqSWq7uYWiEVDxfW491iqATyuYUIqTq8T8vuhA8884JqK44824iWfq0q","domain":".xiaohongshu.com","path":"/","expires":1810137369.281301,"size":75,"httpOnly":false,"secure":false,"session":false,"priority":"Medium","sameParty":false,"sourceScheme":"Secure","sourcePort":443},{"name":"webId","value":"4afc571d437c6b9cde5f42ac3bd5ab88","domain":".xiaohongshu.com","path":"/","expires":1807106285,"size":37,"httpOnly":false,"secure":false,"session":false,"priority":"Medium","sameParty":false,"sourceScheme":"Secure","sourcePort":443},{"name":"ets","value":"1775570285237","domain":".xiaohongshu.com","path":"/","expires":1778162285.237612,"size":16,"httpOnly":false,"secure":false,"session":false,"priority":"Medium","sameParty":false,"sourceScheme":"Secure","sourcePort":443},{"name":"xsecappid","value":"ugc","domain":".xiaohongshu.com","path":"/","expires":1807123764,"size":12,"httpOnly":false,"secure":false,"session":false,"priority":"Medium","sameParty":false,"sourceScheme":"Secure","sourcePort":443}]

BIN
docs/qqqq.docx Normal file

Binary file not shown.

37
go.mod
View File

@ -3,32 +3,34 @@ module geo
go 1.26.1
require (
github.com/JohannesKaufmann/html-to-markdown v1.6.0
github.com/WhityGhost/gh0ffice v1.0.0
github.com/go-kratos/kratos/v2 v2.9.2
github.com/go-playground/validator/v10 v10.30.2
github.com/go-rod/rod v0.116.2
github.com/go-viper/mapstructure/v2 v2.5.0
github.com/gofiber/fiber/v2 v2.52.12
github.com/golang-jwt/jwt/v5 v5.3.1
github.com/google/uuid v1.6.0
github.com/google/wire v0.7.0
github.com/grokify/html-strip-tags-go v0.1.0
github.com/redis/go-redis/v9 v9.18.0
github.com/spf13/viper v1.21.0
gorm.io/driver/mysql v1.6.0
gorm.io/gorm v1.31.1
xorm.io/builder v0.3.13
)
require github.com/nguyenthenguyen/docx v0.0.0-20230621112118-9c8e795a11db // indirect
require (
filippo.io/edwards25519 v1.1.0 // indirect
github.com/PuerkitoBio/goquery v1.9.2 // indirect
github.com/andybalholm/brotli v1.1.0 // indirect
github.com/andybalholm/cascadia v1.3.2 // indirect
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/charmbracelet/lipgloss v0.10.0 // indirect
github.com/charmbracelet/log v0.4.0 // indirect
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
github.com/fsnotify/fsnotify v1.9.0 // indirect
github.com/extrame/goyymmdd v0.0.0-20210114090516-7cc815f00d1a // indirect
github.com/extrame/ole2 v0.0.0-20160812065207-d69429661ad7 // indirect
github.com/gabriel-vasile/mimetype v1.4.13 // indirect
github.com/go-logfmt/logfmt v0.6.0 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-sql-driver/mysql v1.8.1 // indirect
@ -36,17 +38,19 @@ require (
github.com/jinzhu/now v1.1.5 // indirect
github.com/klauspost/compress v1.17.9 // indirect
github.com/leodido/go-urn v1.4.0 // indirect
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
github.com/mattetti/filebuffer v1.0.1 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-runewidth v0.0.16 // indirect
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
github.com/rivo/uniseg v0.2.0 // indirect
github.com/sagikazarmark/locafero v0.11.0 // indirect
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 // indirect
github.com/spf13/afero v1.15.0 // indirect
github.com/spf13/cast v1.10.0 // indirect
github.com/spf13/pflag v1.0.10 // indirect
github.com/subosito/gotenv v1.6.0 // indirect
github.com/moipa-cn/pptx v0.0.0-20220526133451-f2b53ab6e594 // indirect
github.com/muesli/reflow v0.3.0 // indirect
github.com/muesli/termenv v0.15.2 // indirect
github.com/richardlehane/mscfb v1.0.4 // indirect
github.com/richardlehane/msoleps v1.0.3 // indirect
github.com/rivo/uniseg v0.4.7 // indirect
github.com/stretchr/testify v1.11.1 // indirect
github.com/thedatashed/xlsxreader v1.2.8 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/valyala/fasthttp v1.51.0 // indirect
github.com/valyala/tcplisten v1.0.0 // indirect
@ -56,9 +60,8 @@ require (
github.com/ysmood/gson v0.7.3 // indirect
github.com/ysmood/leakless v0.9.0 // indirect
go.uber.org/atomic v1.11.0 // indirect
go.yaml.in/yaml/v3 v3.0.4 // indirect
golang.org/x/crypto v0.49.0 // indirect
golang.org/x/net v0.51.0 // indirect
golang.org/x/exp v0.0.0-20231006140011-7918f672742d // indirect
golang.org/x/sys v0.42.0 // indirect
golang.org/x/text v0.35.0 // indirect
)

152
go.sum
View File

@ -2,33 +2,37 @@ filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
gitea.com/xorm/sqlfiddle v0.0.0-20180821085327-62ce714f951a h1:lSA0F4e9A2NcQSqGqTOXqu2aRi/XEQxDCBwM8yJtE6s=
gitea.com/xorm/sqlfiddle v0.0.0-20180821085327-62ce714f951a/go.mod h1:EXuID2Zs0pAQhH8yz+DNjUbjppKQzKFAn28TMYPB6IU=
github.com/JohannesKaufmann/html-to-markdown v1.6.0 h1:04VXMiE50YYfCfLboJCLcgqF5x+rHJnb1ssNmqpLH/k=
github.com/JohannesKaufmann/html-to-markdown v1.6.0/go.mod h1:NUI78lGg/a7vpEJTz/0uOcYMaibytE4BUOQS8k78yPQ=
github.com/PuerkitoBio/goquery v1.9.2 h1:4/wZksC3KgkQw7SQgkKotmKljk0M6V8TUvA8Wb4yPeE=
github.com/PuerkitoBio/goquery v1.9.2/go.mod h1:GHPCaP0ODyyxqcNoFGYlAprUFH81NuRPd0GX3Zu2Mvk=
github.com/WhityGhost/gh0ffice v1.0.0 h1:caSkJ90733riUlaqJffTjutoUUGdRi1M0A+x/qqQdKc=
github.com/WhityGhost/gh0ffice v1.0.0/go.mod h1:+bSISwCkGiY+vUMvdHHyaYvp4vKtvIt9iovKrrMGvLY=
github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M=
github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY=
github.com/andybalholm/cascadia v1.3.2 h1:3Xi6Dw5lHF15JtdcmAHD3i1+T8plmv7BQ/nsViSLyss=
github.com/andybalholm/cascadia v1.3.2/go.mod h1:7gtRlve5FxPPgIgX36uWBX58OdBsSS6lUvCFb+h7KvU=
github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c=
github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/charmbracelet/lipgloss v0.10.0 h1:KWeXFSexGcfahHX+54URiZGkBFazf70JNMtwg/AFW3s=
github.com/charmbracelet/lipgloss v0.10.0/go.mod h1:Wig9DSfvANsxqkRsqj6x87irdy123SR4dOXlKa91ciE=
github.com/charmbracelet/log v0.4.0 h1:G9bQAcx8rWA2T3pWvx7YtPTPwgqpk7D68BX21IRW8ZM=
github.com/charmbracelet/log v0.4.0/go.mod h1:63bXt/djrizTec0l11H20t8FDSvA4CRZJ1KH22MdptM=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
github.com/extrame/goyymmdd v0.0.0-20210114090516-7cc815f00d1a h1:c5k29baTzznteWs+9dxrtqpNxgtQ3V5NbU8d6laLK9Q=
github.com/extrame/goyymmdd v0.0.0-20210114090516-7cc815f00d1a/go.mod h1:xbpgo9r3xURoPa/l3sLKLGcnWlkz9UkfFsQ7lW0S6h8=
github.com/extrame/ole2 v0.0.0-20160812065207-d69429661ad7 h1:n+nk0bNe2+gVbRI8WRbLFVwwcBQ0rr5p+gzkKb6ol8c=
github.com/extrame/ole2 v0.0.0-20160812065207-d69429661ad7/go.mod h1:GPpMrAfHdb8IdQ1/R2uIRBsNfnPnwsYE9YYI5WyY1zw=
github.com/gabriel-vasile/mimetype v1.4.13 h1:46nXokslUBsAJE/wMsp5gtO500a4F3Nkz9Ufpk2AcUM=
github.com/gabriel-vasile/mimetype v1.4.13/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
github.com/go-kratos/kratos/v2 v2.9.2 h1:px8GJQBeLpquDKQWQ9zohEWiLA8n4D/pv7aH3asvUvo=
github.com/go-kratos/kratos/v2 v2.9.2/go.mod h1:Jc7jaeYd4RAPjetun2C+oFAOO7HNMHTT/Z4LxpuEDJM=
github.com/go-logfmt/logfmt v0.6.0 h1:wGYYu3uicYdqXVgoYbvnkrPVXkuLM1p1ifugDMEdRi4=
github.com/go-logfmt/logfmt v0.6.0/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs=
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
@ -45,16 +49,10 @@ github.com/go-viper/mapstructure/v2 v2.5.0 h1:vM5IJoUAy3d7zRSVtIwQgBj7BiWtMPfmPE
github.com/go-viper/mapstructure/v2 v2.5.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
github.com/gofiber/fiber/v2 v2.52.12 h1:0LdToKclcPOj8PktUdIKo9BUohjjwfnQl42Dhw8/WUw=
github.com/gofiber/fiber/v2 v2.52.12/go.mod h1:YEcBbO/FB+5M1IZNBP9FO3J9281zgPAreiI1oqg8nDw=
github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY=
github.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/wire v0.7.0 h1:JxUKI6+CVBgCO2WToKy/nQk0sS+amI9z9EjVmdaocj4=
github.com/google/wire v0.7.0/go.mod h1:n6YbUQD9cPKTnHXEBN2DXlOp/mVADhVErcMFb0v3J18=
github.com/grokify/html-strip-tags-go v0.1.0 h1:03UrQLjAny8xci+R+qjCce/MYnpNXCtgzltlQbOBae4=
github.com/grokify/html-strip-tags-go v0.1.0/go.mod h1:ZdzgfHEzAfz9X6Xe5eBLVblWIxXfYSQ40S/VKrAOGpc=
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
@ -63,57 +61,47 @@ github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2
github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=
github.com/klauspost/cpuid/v2 v2.0.9 h1:lgaqFMSdTdQYdZ04uHyN2d/eKdOMyi2YLSvlQIBFYa4=
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
github.com/mattetti/filebuffer v1.0.1 h1:gG7pyfnSIZCxdoKq+cPa8T0hhYtD9NxCdI4D7PTjRLM=
github.com/mattetti/filebuffer v1.0.1/go.mod h1:YdMURNDOttIiruleeVr6f56OrMc+MydEnTcXwtkxNVs=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/moipa-cn/pptx v0.0.0-20220526133451-f2b53ab6e594 h1:oRA3NxvX2usoIybfgLP37H1G7VVBDbnwseE7uKnK7lo=
github.com/moipa-cn/pptx v0.0.0-20220526133451-f2b53ab6e594/go.mod h1:eH2lLq9oH3EoCtPcCpsfPDpFKzknE2Hj5mUdlTivH8Q=
github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s=
github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8=
github.com/muesli/termenv v0.15.2 h1:GohcuySI0QmI3wN8Ok9PtKGkgkFIk7y6Vpb5PvrY+Wo=
github.com/muesli/termenv v0.15.2/go.mod h1:Epx+iuz8sNs7mNKhxzH4fWXGNpZwUaJKRS1noLXviQ8=
github.com/nguyenthenguyen/docx v0.0.0-20230621112118-9c8e795a11db h1:v0cW/tTMrJQyZr7r6t+t9+NhH2OBAjydHisVYxuyObc=
github.com/nguyenthenguyen/docx v0.0.0-20230621112118-9c8e795a11db/go.mod h1:BZyH8oba3hE/BTt2FfBDGPOHhXiKs9RFmUvvXRdzrhM=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/redis/go-redis/v9 v9.18.0 h1:pMkxYPkEbMPwRdenAzUNyFNrDgHx9U+DrBabWNfSRQs=
github.com/redis/go-redis/v9 v9.18.0/go.mod h1:k3ufPphLU5YXwNTUcCRXGxUoF1fqxnhFQmscfkCoDA0=
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
github.com/richardlehane/mscfb v1.0.4 h1:WULscsljNPConisD5hR0+OyZjwK46Pfyr6mPu5ZawpM=
github.com/richardlehane/mscfb v1.0.4/go.mod h1:YzVpcZg9czvAuhk9T+a3avCpcFPMUWm7gK3DypaEsUk=
github.com/richardlehane/msoleps v1.0.1/go.mod h1:BWev5JBpU9Ko2WAgmZEuiz4/u3ZYTKbjLycmwiWUfWg=
github.com/richardlehane/msoleps v1.0.3 h1:aznSZzrwYRl3rLKRT3gUk9am7T/mLNSnJINvN0AQoVM=
github.com/richardlehane/msoleps v1.0.3/go.mod h1:BWev5JBpU9Ko2WAgmZEuiz4/u3ZYTKbjLycmwiWUfWg=
github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M=
github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA=
github.com/sagikazarmark/locafero v0.11.0 h1:1iurJgmM9G3PA/I+wWYIOw/5SyBtxapeHDcg+AAIFXc=
github.com/sagikazarmark/locafero v0.11.0/go.mod h1:nVIGvgyzw595SUSUE6tvCp3YYTeHs15MvlmU87WwIik=
github.com/sebdah/goldie/v2 v2.5.3 h1:9ES/mNN+HNUbNWpVAlrzuZ7jE+Nrczbj8uFRjM7624Y=
github.com/sebdah/goldie/v2 v2.5.3/go.mod h1:oZ9fp0+se1eapSRjfYbsV/0Hqhbuu3bJVvKI/NNtssI=
github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8=
github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I=
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 h1:+jumHNA0Wrelhe64i8F6HNlS8pkoyMv5sreGx2Ry5Rw=
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8/go.mod h1:3n1Cwaq1E1/1lhQhtRK2ts/ZwZEhjcQeJQ1RuC6Q/8U=
github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I=
github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg=
github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY=
github.com/spf13/cast v1.10.0/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo=
github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/viper v1.21.0 h1:x5S+0EU27Lbphp4UKm1C+1oQO+rKx36vfCoaVebLFSU=
github.com/spf13/viper v1.21.0/go.mod h1:P0lhsswPGWD/1lZJ9ny3fYnVqxiegrlNrEmgLjbTCAY=
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
github.com/thedatashed/xlsxreader v1.2.8 h1:8aGbkXIPEThQbA8KzUZqIa4v4oqFrJFKLQ36vWePI5U=
github.com/thedatashed/xlsxreader v1.2.8/go.mod h1:wZyb/2xF1+rkZ2ujhC72tuuOWBY574QvcXHFls+5AXc=
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/valyala/fasthttp v1.51.0 h1:8b30A5JlZ6C7AS81RsWjYMQmrZG6feChmgAolCl1SqA=
@ -134,84 +122,22 @@ github.com/ysmood/gson v0.7.3 h1:QFkWbTH8MxyUTKPkVWAENJhxqdBa4lYTQWqZCiLG6kE=
github.com/ysmood/gson v0.7.3/go.mod h1:3Kzs5zDl21g5F/BlLTNcuAGAYLKt2lV5G8D1zF3RNmg=
github.com/ysmood/leakless v0.9.0 h1:qxCG5VirSBvmi3uynXFkcnLMzkphdh3xx5FtrORwDCU=
github.com/ysmood/leakless v0.9.0/go.mod h1:R8iAXPRaG97QJwqxs74RdwzcRHT1SWCGTNqY8q0JvMQ=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
github.com/yuin/goldmark v1.7.1 h1:3bajkSilaCbjdKVsKdZjZCLBNPL9pYzrCakKaf4U49U=
github.com/yuin/goldmark v1.7.1/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E=
github.com/zeebo/xxh3 v1.0.2 h1:xZmwmqxHZA8AI603jOQ0tMqmBr9lPeFwGg6d+xy9DC0=
github.com/zeebo/xxh3 v1.0.2/go.mod h1:5NWz9Sef7zIDm2JHfFlcQvNekmcEl9ekUZQQKCYaDcA=
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M=
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
golang.org/x/crypto v0.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4=
golang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8=
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
golang.org/x/net v0.51.0 h1:94R/GTO7mt3/4wIKpcR5gkGmRLOuE/2hNGeWq/GBIFo=
golang.org/x/net v0.51.0/go.mod h1:aamm+2QF5ogm02fjy5Bb7CQ0WMt1/WVM7FtyaTLlA9Y=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI=
golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo=
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY=
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
golang.org/x/term v0.19.0/go.mod h1:2CuTdWZ7KHSQwUzKva0cbMg6q2DMI3Mmxp+gKJbskEk=
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8=
golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gorm.io/driver/mysql v1.6.0 h1:eNbLmNTpPpTOVZi8MMxCi2aaIm0ZpInbORNXDwyLGvg=

View File

@ -5,7 +5,11 @@ import (
"geo/internal/config"
"geo/internal/publisher"
"geo/pkg"
"io"
"log"
"os"
"path/filepath"
"strings"
"sync"
"time"
@ -38,6 +42,43 @@ func GetPublishManager(config *config.Config, db *utils.Db) *PublishManager {
return publishManager
}
// getTaskLogger 获取任务专属日志记录器(同一个文件)
func (pm *PublishManager) getTaskLogger(requestID string) (*log.Logger, *os.File, error) {
// 确定日志目录
logsDir := pm.Conf.Sys.LogsDir
if logsDir == "" {
logsDir = "./logs"
}
// 按日期创建子目录
dateDir := time.Now().Format("2006-01-02")
taskLogDir := filepath.Join(logsDir, "tasks", dateDir)
if err := os.MkdirAll(taskLogDir, 0755); err != nil {
return nil, nil, fmt.Errorf("创建日志目录失败: %v", err)
}
// 创建以requestId命名的日志文件
logPath := filepath.Join(taskLogDir, fmt.Sprintf("%s.log", requestID))
logFile, err := os.OpenFile(logPath, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644)
if err != nil {
return nil, nil, fmt.Errorf("创建日志文件失败: %v", err)
}
// 创建写入器:同时写入文件和标准输出
multiWriter := io.MultiWriter(logFile, os.Stdout)
// 创建专用的logger
taskLogger := log.New(multiWriter, "", log.LstdFlags|log.Lmicroseconds)
// 写入任务开始分隔线
taskLogger.Printf(strings.Repeat("=", 80))
taskLogger.Printf("任务开始 | RequestID: %s | 时间: %s", requestID, time.Now().Format("2006-01-02 15:04:05.000"))
taskLogger.Printf(strings.Repeat("=", 80))
return taskLogger, logFile, nil
}
func (pm *PublishManager) Start(tokenID int) bool {
pm.mu.Lock()
defer pm.mu.Unlock()
@ -124,7 +165,7 @@ func (pm *PublishManager) getPendingPublish() map[string]interface{} {
return nil
}
requestID := getString(result, "request_id")
requestID := pkg.GetString(result, "request_id")
log.Printf("获取到待发布任务: token_id=%d, request_id=%s", pm.TokenID, requestID)
return result
}
@ -139,98 +180,133 @@ func (pm *PublishManager) GetTaskByRequestID(requestID string) (map[string]inter
return pm.db.GetOne(sql, requestID)
}
func (pm *PublishManager) processSingleTask(publishData map[string]interface{}) map[string]interface{} {
requestID := getString(publishData, "request_id")
platIndex := getString(publishData, "plat_index")
title := getString(publishData, "title")
tagRaw := getString(publishData, "tag")
userIndex := getString(publishData, "user_index")
url := getString(publishData, "url")
imgURL := getString(publishData, "img")
func (pm *PublishManager) processSingleTask(publishData map[string]interface{}) (result map[string]interface{}) {
requestID := pkg.GetString(publishData, "request_id")
log.Printf("[任务 %s] 开始处理,平台:%s标题%s", requestID, platIndex, title)
// 获取任务专属日志(同一个文件)
taskLogger, logFile, err := pm.getTaskLogger(requestID)
if err != nil {
log.Printf("[任务 %s] 创建日志文件失败: %v使用全局日志", requestID, err)
taskLogger = log.Default()
}
if logFile != nil {
defer logFile.Close()
}
// 全局defer用于捕获panic并记录到同一个日志文件
defer func() {
if r := recover(); r != nil {
errMsg := fmt.Sprintf("任务执行发生panic: %v", r)
taskLogger.Printf("❌ CRITICAL: %s", errMsg)
taskLogger.Printf(strings.Repeat("=", 80))
taskLogger.Printf("任务异常结束 | RequestID: %s | 时间: %s", requestID, time.Now().Format("2006-01-02 15:04:05.000"))
taskLogger.Printf(strings.Repeat("=", 80))
result = map[string]interface{}{
"success": false,
"message": errMsg,
"request_id": requestID,
}
}
}()
taskLogger.Printf("[任务 %s] 开始处理", requestID)
platIndex := pkg.GetString(publishData, "plat_index")
title := pkg.GetString(publishData, "title")
tagRaw := pkg.GetString(publishData, "tag")
userIndex := pkg.GetString(publishData, "user_index")
url := pkg.GetString(publishData, "url")
imgURL := pkg.GetString(publishData, "img")
taskLogger.Printf("[任务 %s] 任务详情 - 平台:%s标题%s用户%s", requestID, platIndex, title, userIndex)
taskLogger.Printf("[任务 %s] 文档URL: %s", requestID, url)
taskLogger.Printf("[任务 %s] 图片URL: %s", requestID, imgURL)
// 更新状态为发布中
pm.updatePublishStatus(requestID, 2, "")
log.Printf("[任务 %s] 状态已更新为发布中", requestID)
taskLogger.Printf("[任务 %s] 状态已更新为发布中", requestID)
// 下载文件
docPath, err := pkg.DownloadFile(url, "", requestID+".docx")
taskLogger.Printf("[任务 %s] 开始下载文档...", requestID)
docPath, err := pkg.DownloadFile(url, pm.Conf.Sys.DocsDir, requestID+".docx")
if err != nil {
errMsg := fmt.Sprintf("下载文档失败: %v", err)
log.Printf("[任务 %s] %s", requestID, errMsg)
taskLogger.Printf("[任务 %s] %s", requestID, errMsg)
pm.updatePublishStatus(requestID, 3, errMsg)
return map[string]interface{}{"success": false, "message": errMsg, "request_id": requestID}
}
log.Printf("[任务 %s] 文档下载成功: %s", requestID, docPath)
defer func() {
if docPath != "" {
pkg.DeleteFile(docPath)
taskLogger.Printf("[任务 %s] 已删除文档文件: %s", requestID, docPath)
}
}()
taskLogger.Printf("[任务 %s] ✅ 文档下载成功: %s", requestID, docPath)
// 下载图片
imgPath, err := pkg.DownloadImage(imgURL, requestID, "img")
taskLogger.Printf("[任务 %s] 开始下载图片...", requestID)
imgPath, err := pkg.DownloadImage(imgURL, requestID, pm.Conf.Sys.UploadDir)
defer func() {
if imgPath != "" {
pkg.DeleteFile(imgPath)
taskLogger.Printf("[任务 %s] 已删除图片文件: %s", requestID, imgPath)
}
}()
if err != nil {
errMsg := fmt.Sprintf("下载图片失败: %v", err)
log.Printf("[任务 %s] %s", requestID, errMsg)
taskLogger.Printf("[任务 %s] %s", requestID, errMsg)
pm.updatePublishStatus(requestID, 3, errMsg)
// 图片下载失败,清理已下载的文档
pkg.DeleteFile(docPath)
return map[string]interface{}{"success": false, "message": errMsg, "request_id": requestID}
}
log.Printf("[任务 %s] 图片下载成功: %s", requestID, imgPath)
// 确保清理临时文件
defer func() {
pkg.DeleteFile(docPath)
pkg.DeleteFile(imgPath)
}()
taskLogger.Printf("[任务 %s] ✅ 图片下载成功: %s", requestID, imgPath)
// 解析标签
tags := pkg.ParseTags(tagRaw)
log.Printf("[任务 %s] 标签解析完成: %v", requestID, tags)
// 提取内容
content, err := pkg.ExtractWordContent(docPath, "html")
if err != nil {
errMsg := fmt.Sprintf("提取文档内容失败: %v", err)
log.Printf("[任务 %s] %s", requestID, errMsg)
pm.updatePublishStatus(requestID, 3, errMsg)
return map[string]interface{}{"success": false, "message": errMsg, "request_id": requestID}
}
log.Printf("[任务 %s] 内容提取成功,长度: %d", requestID, len(content))
taskLogger.Printf("[任务 %s] 标签解析完成: %v", requestID, tags)
// 获取发布器
publisherClass := getPublisherClass(platIndex)
publisherClass := GetPublisherClass(platIndex)
if publisherClass == nil {
errMsg := fmt.Sprintf("不支持的平台: %s", platIndex)
log.Printf("[任务 %s] %s", requestID, errMsg)
taskLogger.Printf("[任务 %s] ❌ %s", requestID, errMsg)
pm.updatePublishStatus(requestID, 3, errMsg)
return map[string]interface{}{"success": false, "message": errMsg, "request_id": requestID}
}
// 创建并执行发布器
var pub interface{ PublishNote() (bool, string) }
switch platIndex {
case "xhs":
pub = publisher.NewXiaohongshuPublisher(false, title, content, tags, userIndex, platIndex, requestID, imgPath, docPath, publishData, pm.Conf)
log.Printf("[任务 %s] 创建小红书发布器", requestID)
case "bjh":
pub = publisher.NewBaijiahaoPublisher(false, title, content, tags, userIndex, platIndex, requestID, imgPath, docPath, publishData, pm.Conf)
log.Printf("[任务 %s] 创建百家号发布器", requestID)
default:
log.Printf("[任务 %s] 未知平台 %s使用默认小红书发布器", requestID, platIndex)
pub = publisher.NewXiaohongshuPublisher(false, title, content, tags, userIndex, platIndex, requestID, imgPath, docPath, publishData, pm.Conf)
// 提取内容
taskLogger.Printf("[任务 %s] 开始提取文档内容...", requestID)
var content string
if publisherClass.Type == 1 {
content, err = pkg.ExtractWordContent(docPath, publisherClass.ContentFormat)
if err != nil {
errMsg := fmt.Sprintf("提取文档内容失败: %v", err)
taskLogger.Printf("[任务 %s] ❌ %s", requestID, errMsg)
pm.updatePublishStatus(requestID, 3, errMsg)
return map[string]interface{}{"success": false, "message": errMsg, "request_id": requestID}
}
}
log.Printf("[任务 %s] 开始执行发布...", requestID)
taskLogger.Printf("[任务 %s] ✅ 内容提取成功,长度: %d", requestID, len(content))
taskLogger.Printf("[任务 %s] 创建发布器...", requestID)
pub := publisherClass.InitMethod(false, title, content, tags, userIndex, platIndex, requestID, imgPath, docPath, publishData, pm.Conf, taskLogger)
taskLogger.Printf("[任务 %s] 创建%s发布器", publisherClass.Name, requestID)
taskLogger.Printf("[任务 %s] 开始执行发布...", requestID)
success, message := pub.PublishNote()
if success {
log.Printf("[任务 %s] 发布成功: %s", requestID, message)
taskLogger.Printf("[任务 %s] 发布成功: %s", requestID, message)
pm.updatePublishStatus(requestID, 4, message)
} else {
log.Printf("[任务 %s] 发布失败: %s", requestID, message)
taskLogger.Printf("[任务 %s] 发布失败: %s", requestID, message)
pm.updatePublishStatus(requestID, 3, message)
}
taskLogger.Printf(strings.Repeat("=", 80))
taskLogger.Printf("任务结束 | RequestID: %s | 结果: %v | 时间: %s", requestID, success, time.Now().Format("2006-01-02 15:04:05.000"))
taskLogger.Printf(strings.Repeat("=", 80))
return map[string]interface{}{
"success": success,
"message": message,
@ -270,28 +346,7 @@ func (pm *PublishManager) GetStatus() map[string]interface{} {
}
}
func getPublisherClass(platIndex string) interface{} {
platformMap := map[string]interface{}{
"xhs": struct{}{},
"bjh": struct{}{},
"csdn": struct{}{},
}
return platformMap[platIndex]
}
func GetPublisherClass(platIndex string) *publisher.PublisherValue {
func getString(m map[string]interface{}, key string) string {
if v, ok := m[key]; ok {
switch v.(type) {
case []uint8:
return string(v.([]uint8))
case string:
return v.(string)
case int64:
return fmt.Sprintf("%d", v)
default:
return fmt.Sprintf("%v", v)
}
}
return ""
return publisher.PublisherMap[platIndex]
}

View File

@ -2,10 +2,10 @@ package publisher
import (
"fmt"
"strings"
"time"
"geo/internal/config"
"geo/pkg"
"log"
"strings"
"github.com/go-rod/rod"
"github.com/go-rod/rod/lib/proto"
@ -16,40 +16,46 @@ type BaijiahaoPublisher struct {
Category string
ArticleType string
IsTop bool
maxRetries int
retryDelay int
}
func NewBaijiahaoPublisher(headless bool, title, content string, tags []string, tenantID, platIndex, requestID, imagePath, wordPath string, platInfo map[string]interface{}, cfg *config.Config) *BaijiahaoPublisher {
base := NewBasePublisher(headless, title, content, tags, tenantID, platIndex, requestID, imagePath, wordPath, platInfo, cfg)
func NewBaijiahaoPublisher(headless bool, title, content string, tags []string, tenantID, platIndex, requestID, imagePath, wordPath string, platInfo map[string]interface{}, cfg *config.Config, logger *log.Logger) PublisherInerface {
base := NewBasePublisher(headless, title, content, tags, tenantID, platIndex, requestID, imagePath, wordPath, platInfo, cfg, logger)
if platInfo != nil {
base.LoginURL = getString(platInfo, "login_url")
base.EditorURL = getString(platInfo, "edit_url")
base.LoginedURL = getString(platInfo, "logined_url")
base.LoginURL = pkg.GetString(platInfo, "login_url")
base.EditorURL = pkg.GetString(platInfo, "edit_url")
base.LoginedURL = pkg.GetString(platInfo, "logined_url")
}
return &BaijiahaoPublisher{
BasePublisher: base,
maxRetries: 5,
retryDelay: 2,
}
return &BaijiahaoPublisher{BasePublisher: base}
}
func (p *BaijiahaoPublisher) CheckLoginStatus() bool {
url := p.GetCurrentURL()
// 如果URL包含登录相关关键词表示未登录
if strings.Contains(url, "login") || strings.Contains(url, "passport") {
currentURL := p.GetCurrentURL()
if strings.Contains(currentURL, p.LoginURL) {
return false
}
// 如果URL是编辑页面或主页表示已登录
if strings.Contains(url, "baijiahao") || strings.Contains(url, "edit") {
return true
}
return url != p.LoginURL
return true
}
func (p *BaijiahaoPublisher) CheckLogin() (bool, string) {
p.LogInfo("检查登录状态...")
driverCreated := false
defer func() {
if driverCreated && p.Browser != nil {
p.Close()
}
}()
if err := p.SetupDriver(); err != nil {
return false, fmt.Sprintf("浏览器启动失败: %v", err)
}
defer p.Close()
driverCreated = true
p.Page.MustNavigate(p.LoginedURL)
p.Page.MustNavigate(p.EditorURL)
p.Sleep(3)
p.WaitForPageReady(5)
@ -61,33 +67,33 @@ func (p *BaijiahaoPublisher) CheckLogin() (bool, string) {
}
func (p *BaijiahaoPublisher) WaitLogin() (bool, string) {
p.LogInfo("开始等待登录...")
driverCreated := false
defer func() {
if driverCreated && p.Browser != nil {
p.Close()
}
}()
if err := p.SetupDriver(); err != nil {
return false, fmt.Sprintf("浏览器启动失败: %v", err)
}
defer p.Close()
driverCreated = true
// 先尝试访问已登录页面
p.Page.MustNavigate(p.LoginedURL)
p.Sleep(3)
if p.CheckLoginStatus() {
p.SaveCookies()
p.LogInfo("已有登录状态")
return true, "already_logged_in"
}
// 未登录,跳转到登录页
p.Page.MustNavigate(p.LoginURL)
p.LogInfo("请扫描二维码登录...")
// 等待登录完成最多120秒
for i := 0; i < 120; i++ {
time.Sleep(1 * time.Second)
p.Sleep(1)
if p.CheckLoginStatus() {
p.SaveCookies()
p.LogInfo("登录成功")
return true, "login_success"
}
}
@ -95,273 +101,58 @@ func (p *BaijiahaoPublisher) WaitLogin() (bool, string) {
return false, "登录超时"
}
func (p *BaijiahaoPublisher) inputTitle() error {
p.LogInfo("输入标题...")
titleSelectors := []string{
".client_pages_edit_components_titleInput [contenteditable='true']",
".input-box [contenteditable='true']",
"[contenteditable='true']",
}
var titleInput *rod.Element
var err error
for _, selector := range titleSelectors {
titleInput, err = p.WaitForElementVisible(selector, 5)
if err == nil && titleInput != nil {
p.LogInfo(fmt.Sprintf("找到标题输入框: %s", selector))
break
}
}
if titleInput == nil {
return fmt.Errorf("未找到标题输入框")
}
// 点击获取焦点
if err := titleInput.Click(proto.InputMouseButtonLeft, 1); err != nil {
return fmt.Errorf("点击标题框失败: %v", err)
}
p.SleepMs(500)
// 清空输入框
if err := p.ClearContentEditable(titleInput); err != nil {
p.LogInfo(fmt.Sprintf("清空标题框失败: %v", err))
}
p.SleepMs(300)
// 输入标题
if err := p.SetContentEditable(titleInput, p.Title); err != nil {
// 备用输入方式
titleInput.Input(p.Title)
}
p.LogInfo(fmt.Sprintf("标题已输入: %s", p.Title))
return nil
}
func (p *BaijiahaoPublisher) inputContent() error {
p.LogInfo("输入内容...")
// 查找内容编辑器
contentEditor, err := p.WaitForElementVisible(".ProseMirror", 10)
if err != nil {
contentEditor, err = p.WaitForElementVisible("[contenteditable='true']", 10)
if err != nil {
return fmt.Errorf("未找到内容编辑器: %v", err)
}
}
// 点击获取焦点
if err := contentEditor.Click(proto.InputMouseButtonLeft, 1); err != nil {
return fmt.Errorf("点击编辑器失败: %v", err)
}
p.SleepMs(500)
// 清空编辑器
if err := p.ClearContentEditable(contentEditor); err != nil {
p.LogInfo(fmt.Sprintf("清空编辑器失败: %v", err))
}
p.SleepMs(300)
// 输入内容
if err := p.SetContentEditable(contentEditor, p.Content); err != nil {
// 备用输入方式
contentEditor.Input(p.Content)
}
p.LogInfo(fmt.Sprintf("内容已输入,长度: %d", len(p.Content)))
return nil
}
func (p *BaijiahaoPublisher) uploadImage() error {
if p.ImagePath == "" {
p.LogInfo("无封面图片,跳过")
return nil
}
p.LogInfo(fmt.Sprintf("上传封面: %s", p.ImagePath))
// 查找封面区域
coverArea, err := p.WaitForElementClickable(".cheetah-spin-container", 5)
if err != nil {
p.LogInfo("未找到封面区域,跳过")
return nil
}
if err := coverArea.Click(proto.InputMouseButtonLeft, 1); err != nil {
p.LogInfo(fmt.Sprintf("点击封面区域失败: %v", err))
}
p.SleepMs(1000)
// 查找文件输入框
fileInput, err := p.Page.Element("input[type='file'][accept*='image']")
if err != nil {
fileInput, err = p.Page.Element("input[type='file']")
if err != nil {
return fmt.Errorf("未找到文件输入框: %v", err)
}
}
// 上传图片
if err := fileInput.SetFiles([]string{p.ImagePath}); err != nil {
return fmt.Errorf("上传图片失败: %v", err)
}
p.LogInfo("图片上传成功")
p.Sleep(3)
// 查找确认按钮
confirmBtn, err := p.WaitForElementClickable(".cheetah-btn-primary", 5)
if err == nil && confirmBtn != nil {
if err := confirmBtn.Click(proto.InputMouseButtonLeft, 1); err != nil {
p.LogInfo(fmt.Sprintf("点击确认按钮失败: %v", err))
}
p.LogInfo("已确认封面")
p.SleepMs(1000)
}
return nil
}
func (p *BaijiahaoPublisher) clickPublish() error {
p.LogInfo("点击发布按钮...")
// 滚动到底部
if _, err := p.Page.Eval(`() => window.scrollTo(0, document.body.scrollHeight)`); err != nil {
p.LogInfo(fmt.Sprintf("滚动到底部失败: %v", err))
}
p.SleepMs(1000)
// 查找发布按钮
publishSelectors := []string{
"[data-testid='publish-btn']",
".op-list-right .cheetah-btn-primary",
".cheetah-btn-primary",
"button:contains('发布')",
}
var publishBtn *rod.Element
var err error
for _, selector := range publishSelectors {
publishBtn, err = p.WaitForElementClickable(selector, 5)
if err == nil && publishBtn != nil {
p.LogInfo(fmt.Sprintf("找到发布按钮: %s", selector))
break
}
}
// 如果还是没找到,通过 XPath 查找
if publishBtn == nil {
publishBtn, err = p.Page.ElementX("//button[contains(text(), '发布')]")
if err != nil {
return fmt.Errorf("未找到发布按钮: %v", err)
}
}
// 滚动到按钮位置
if err := p.ScrollToElement(publishBtn); err != nil {
p.LogInfo(fmt.Sprintf("滚动到按钮失败: %v", err))
}
p.SleepMs(500)
// 点击发布
if err := publishBtn.Click(proto.InputMouseButtonLeft, 1); err != nil {
return fmt.Errorf("点击发布按钮失败: %v", err)
}
p.LogInfo("已点击发布按钮")
return nil
}
func (p *BaijiahaoPublisher) waitForPublishResult() (bool, string) {
p.LogInfo("等待发布结果...")
// 等待最多60秒
for i := 0; i < 60; i++ {
p.SleepMs(1000)
// 检查URL是否跳转到成功页面
currentURL := p.GetCurrentURL()
if strings.Contains(currentURL, "clue") ||
strings.Contains(currentURL, "success") ||
strings.Contains(currentURL, "article/list") {
p.LogInfo("发布成功!")
return true, "发布成功"
}
// 检查是否有成功提示
elements, _ := p.Page.Elements(".cheetah-message-success, .cheetah-message-info, [class*='success']")
for _, el := range elements {
text, _ := el.Text()
if strings.Contains(text, "成功") || strings.Contains(text, "已发布") {
p.LogInfo(fmt.Sprintf("发布成功: %s", text))
return true, text
}
}
// 检查是否有失败提示
elements, _ = p.Page.Elements(".cheetah-message-error, .cheetah-message-warning, [class*='error']")
for _, el := range elements {
text, _ := el.Text()
if strings.Contains(text, "失败") || strings.Contains(text, "错误") {
p.LogError(fmt.Sprintf("发布失败: %s", text))
return false, text
}
}
}
return false, "发布结果未知(超时)"
func (p *BaijiahaoPublisher) checkElementExists(selector string, timeout int) bool {
_, err := p.WaitForElement(selector, timeout)
return err == nil
}
func (p *BaijiahaoPublisher) PublishNote() (bool, string) {
p.LogInfo(strings.Repeat("=", 50))
p.LogInfo("开始发布百家号文章...")
p.LogInfo(fmt.Sprintf("标题: %s", p.Title))
p.LogInfo(fmt.Sprintf("内容长度: %d", len(p.Content)))
p.LogInfo(strings.Repeat("=", 50))
driverCreated := false
defer func() {
if driverCreated && p.Browser != nil {
p.Close()
}
}()
// 初始化浏览器
if err := p.SetupDriver(); err != nil {
return false, fmt.Sprintf("浏览器启动失败: %v", err)
}
defer p.Close()
driverCreated = true
// 访问编辑器页面
p.Page.MustNavigate(p.EditorURL)
p.Sleep(3)
p.WaitForPageReady(5)
// 尝试加载cookies
if err := p.LoadCookies(); err == nil {
if p.LoadCookies() == nil {
p.RefreshPage()
p.Sleep(2)
p.Sleep(3)
if p.CheckLoginStatus() {
p.LogInfo("使用cookies登录成功")
} else {
p.LogInfo("cookies已过期需要重新登录")
return false, "需要登录"
return p.doPublish()
}
}
// 检查登录状态
if !p.CheckLoginStatus() {
return false, "需要登录"
if p.CheckLoginStatus() {
p.SaveCookies()
return p.doPublish()
}
// 保存cookies
p.SaveCookies()
return false, "需要登录"
}
func (p *BaijiahaoPublisher) doPublish() (bool, string) {
p.LogInfo("开始发布百家号文章...")
p.Sleep(3)
// 执行发布流程
steps := []struct {
name string
fn func() error
}{
{"输入标题", p.inputTitle},
//{"切换到图文编辑模式", p.switchToGraphicMode},
{"输入内容", p.inputContent},
{"上传封面", p.uploadImage},
{"输入标题", p.inputTitle},
{"设置封面", p.uploadImage},
{"点击发布按钮", p.clickPublish},
{"处理确认弹窗", p.handleConfirmModal},
}
for _, step := range steps {
@ -373,11 +164,385 @@ func (p *BaijiahaoPublisher) PublishNote() (bool, string) {
p.SleepMs(500)
}
// 点击发布
if err := p.clickPublish(); err != nil {
return false, err.Error()
}
// 等待发布结果
return p.waitForPublishResult()
}
func (p *BaijiahaoPublisher) switchToGraphicMode() error {
p.LogInfo("切换到图文编辑模式...")
tabSelectors := []string{
".list-item.item-active",
".header-list-content .list-item",
"div[role='tab']:first-child",
}
for _, selector := range tabSelectors {
tab, err := p.Page.Element(selector)
if err == nil && tab != nil {
visible, _ := tab.Visible()
if visible {
p.JSClick(tab)
p.LogInfo(fmt.Sprintf("已点击图文标签: %s", selector))
p.Sleep(1)
return nil
}
}
}
return nil
}
func (p *BaijiahaoPublisher) inputTitle() error {
p.LogInfo("输入文章标题...")
titleSelectors := []string{
".client_pages_edit_components_titleInput ._9ddb7e475b559749-editor",
".input-box ._9ddb7e475b559749-editor",
"[contenteditable='true']",
".bjh-news-drag-tip + div [contenteditable='true']",
}
var titleInput *rod.Element
for _, selector := range titleSelectors {
titleInput, _ = p.WaitForElementVisible(selector, 5)
if titleInput != nil {
p.LogInfo(fmt.Sprintf("找到标题输入框: %s", selector))
break
}
}
if titleInput == nil {
return fmt.Errorf("未找到标题输入框")
}
titleInput.Click(proto.InputMouseButtonLeft, 1)
p.SleepMs(500)
currentTitle, _ := titleInput.Text()
if currentTitle != "" {
p.LogInfo(fmt.Sprintf("清空当前标题: %s", currentTitle[:min(50, len(currentTitle))]))
p.ClearContentEditable(titleInput)
p.SleepMs(300)
titleInput.Input("\u0001")
p.SleepMs(200)
titleInput.Input("\u007F")
p.SleepMs(200)
}
titleInput.Input(p.Title)
p.LogInfo(fmt.Sprintf("新标题已输入: %s", p.Title))
p.triggerInputEvents(titleInput)
p.SleepMs(500)
finalTitle, _ := titleInput.Text()
if finalTitle != p.Title {
p.Page.Eval(fmt.Sprintf(`() => { arguments[0].innerHTML = '%s'; }`, p.Title))
p.triggerInputEvents(titleInput)
p.LogInfo("已通过 JavaScript 重新设置标题")
}
return nil
}
func (p *BaijiahaoPublisher) inputContent() error {
p.LogInfo("输入文章内容...")
titleInput, err := p.Page.Element("[contenteditable='true']")
if err != nil || titleInput == nil {
return fmt.Errorf("未找到标题输入框")
}
p.LogInfo("从标题框按 Tab 键切换到内容编辑器")
titleInput.Click(proto.InputMouseButtonLeft, 1)
p.SleepMs(500)
titleInput.Input("\t")
p.LogInfo("已按 Tab 键")
p.SleepMs(1500)
contentEditor, err := p.Page.Element(".ProseMirror")
if err != nil {
contentEditor, err = p.Page.Element("[contenteditable='true']")
}
if contentEditor == nil {
return fmt.Errorf("未找到内容编辑器")
}
contentEditor.Click(proto.InputMouseButtonLeft, 1)
p.SleepMs(500)
p.ClearContentEditable(contentEditor)
p.SleepMs(300)
p.SetContentEditable(contentEditor, p.Content)
p.SleepMs(2000)
inputContent, _ := contentEditor.Text()
if len(inputContent) == 0 {
contentEditor.Input(p.Content)
p.SleepMs(2000)
}
return nil
}
func (p *BaijiahaoPublisher) uploadImage() error {
if p.ImagePath == "" {
p.LogInfo("未提供封面图片路径,跳过封面设置")
return nil
}
p.LogInfo("设置文章封面...")
p.SleepMs(2000)
// 查找并点击封面选择区域
coverSelectors := []string{
".cheetah-spin-container",
"._73a3a52aab7e3a36-default",
".cover-selector",
"[class*='spin-container']",
}
var coverArea *rod.Element
for _, selector := range coverSelectors {
coverArea, _ = p.WaitForElement(selector, 3)
if coverArea != nil {
visible, _ := coverArea.Visible()
if visible {
p.LogInfo(fmt.Sprintf("找到封面区域: %s", selector))
break
}
}
}
if coverArea != nil {
p.ScrollToElement(coverArea)
p.SleepMs(500)
p.JSClick(coverArea)
p.LogInfo("已点击封面选择区域")
p.SleepMs(2000)
}
// 查找并点击上传区域
uploadSelectors := []string{
"div[class*='cheetah-upload']",
".cheetah-upload",
"div[class*='upload']",
".upload-area",
"._73a3a52aab7e3a36-content",
"._93c3fe2a3121c388-item",
}
var uploadArea *rod.Element
for _, selector := range uploadSelectors {
elements, _ := p.Page.Elements(selector)
for _, elem := range elements {
visible, _ := elem.Visible()
if visible {
uploadArea = elem
p.LogInfo(fmt.Sprintf("找到上传区域: %s", selector))
break
}
}
if uploadArea != nil {
break
}
}
if uploadArea != nil {
p.ScrollToElement(uploadArea)
p.SleepMs(500)
p.JSClick(uploadArea)
p.LogInfo("已点击图片上传区域")
p.SleepMs(1000)
}
// 查找cheetah-upload组件
componentSelectors := []string{
"div[class*='cheetah-upload']",
".cheetah-upload",
"div[class*='upload']",
}
var uploadComponent *rod.Element
for _, selector := range componentSelectors {
elements, _ := p.Page.Elements(selector)
for _, elem := range elements {
visible, _ := elem.Visible()
if visible {
uploadComponent = elem
p.LogInfo(fmt.Sprintf("找到cheetah-upload组件: %s", selector))
break
}
}
if uploadComponent != nil {
break
}
}
if uploadComponent != nil {
p.ScrollToElement(uploadComponent)
p.SleepMs(500)
p.JSClick(uploadComponent)
p.LogInfo("已点击cheetah-upload上传组件")
p.SleepMs(2000)
}
// 查找文件上传输入框
var fileInput *rod.Element
for i := 0; i < 10; i++ {
fileInput, _ = p.Page.Element("input[name='media'][type='file'][accept='image/*']")
if fileInput != nil {
p.LogInfo("找到文件上传输入框")
break
}
fileInput, _ = p.Page.Element("input[type='file'][accept*='image']")
if fileInput != nil {
p.LogInfo("通过备用选择器找到文件上传输入框")
break
}
p.SleepMs(500)
}
if fileInput != nil {
fileInput.SetFiles([]string{p.ImagePath})
p.LogInfo(fmt.Sprintf("图片上传成功: %s", p.ImagePath))
p.Sleep(3)
}
// 查找并点击确认按钮
var confirmBtn *rod.Element
for i := 0; i < 10; i++ {
confirmBtn, _ = p.Page.ElementX("//button[contains(text(), '确定')]")
if confirmBtn != nil {
visible, _ := confirmBtn.Visible()
if visible {
p.LogInfo("通过文本找到确认按钮")
break
}
}
confirmBtn, _ = p.Page.Element(".cheetah-btn-primary")
if confirmBtn != nil {
text, _ := confirmBtn.Text()
if strings.Contains(text, "确定") {
p.LogInfo(fmt.Sprintf("通过CSS选择器找到确认按钮: %s", text))
break
}
}
buttons, _ := p.Page.Elements("button[class*='cheetah-btn']")
for _, btn := range buttons {
visible, _ := btn.Visible()
if visible {
text, _ := btn.Text()
if strings.Contains(text, "确定") || strings.Contains(text, "确认") {
confirmBtn = btn
p.LogInfo(fmt.Sprintf("通过遍历按钮找到确认按钮: %s", text))
break
}
}
}
if confirmBtn != nil {
break
}
p.Sleep(1)
}
if confirmBtn != nil {
p.ScrollToElement(confirmBtn)
p.SleepMs(500)
p.JSClick(confirmBtn)
p.LogInfo("已点击确认按钮")
p.SleepMs(2000)
}
return nil
}
func (p *BaijiahaoPublisher) clickPublish() error {
p.LogInfo("点击发布按钮...")
publishSelectors := []string{
"[data-testid='publish-btn']",
".op-list-right .cheetah-btn-primary",
}
var publishBtn *rod.Element
for i := 0; i < 10; i++ {
for _, selector := range publishSelectors {
publishBtn, _ = p.Page.Element(selector)
if publishBtn != nil {
visible, _ := publishBtn.Visible()
if visible {
p.LogInfo(fmt.Sprintf("找到发布按钮: %s", selector))
break
}
}
}
if publishBtn != nil {
break
}
publishBtn, _ = p.Page.ElementX("//button[contains(text(), '发布')]")
if publishBtn != nil {
visible, _ := publishBtn.Visible()
if visible {
p.LogInfo("通过XPath找到发布按钮")
break
}
}
p.Sleep(1)
}
if publishBtn == nil {
return fmt.Errorf("未找到发布按钮")
}
p.ScrollToElement(publishBtn)
p.Sleep(1)
for attempt := 0; attempt < 3; attempt++ {
err := p.JSClick(publishBtn)
if err == nil {
p.LogInfo(fmt.Sprintf("已通过JavaScript点击发布按钮 (尝试 %d)", attempt+1))
p.Sleep(3)
return nil
}
err = publishBtn.Click(proto.InputMouseButtonLeft, 1)
if err == nil {
p.LogInfo(fmt.Sprintf("已通过普通点击发布按钮 (尝试 %d)", attempt+1))
p.Sleep(3)
return nil
}
p.Sleep(1)
}
return fmt.Errorf("点击发布按钮失败")
}
func (p *BaijiahaoPublisher) handleConfirmModal() error {
confirmBtn, _ := p.WaitForElement(".cheetah-modal .cheetah-btn-primary", 3)
if confirmBtn != nil {
p.JSClick(confirmBtn)
p.LogInfo("已点击确认弹窗")
p.Sleep(2)
}
return nil
}
func (p *BaijiahaoPublisher) waitForPublishResult() (bool, string) {
p.LogInfo("等待发布结果...")
for attempt := 0; attempt < 60; attempt++ {
currentURL := p.GetCurrentURL()
p.LogInfo(fmt.Sprintf("第 %d 次检查 - URL: %s", attempt+1, currentURL))
if strings.Contains(currentURL, "clue") {
p.LogInfo(fmt.Sprintf("发布成功URL: %s", currentURL))
return true, "发布成功"
}
elements, _ := p.Page.Elements(".cheetah-message-success, .cheetah-message-info")
for _, elem := range elements {
visible, _ := elem.Visible()
if visible {
text, _ := elem.Text()
if strings.Contains(text, "成功") || strings.Contains(text, "发布") {
p.LogInfo(fmt.Sprintf("发布成功: %s", text))
return true, text
}
}
}
elements, _ = p.Page.Elements(".cheetah-message-error, .cheetah-message-warning")
for _, elem := range elements {
visible, _ := elem.Visible()
if visible {
text, _ := elem.Text()
if strings.Contains(text, "失败") || strings.Contains(text, "错误") {
p.LogError(fmt.Sprintf("发布失败: %s", text))
return false, fmt.Sprintf("发布失败: %s", text)
}
}
}
p.Sleep(1)
}
return false, "发布结果未知"
}
func (p *BaijiahaoPublisher) triggerInputEvents(el *rod.Element) {
el.Eval(`() => {
arguments[0].dispatchEvent(new Event('input', {bubbles: true}));
arguments[0].dispatchEvent(new Event('change', {bubbles: true}));
arguments[0].dispatchEvent(new Event('blur', {bubbles: true}));
}`)
}
func min(a, b int) int {
if a < b {
return a
}
return b
}

View File

@ -28,6 +28,7 @@ type BasePublisher struct {
Browser *rod.Browser
Page *rod.Page
Logger *log.Logger
LogFile *os.File
@ -40,13 +41,29 @@ type BasePublisher struct {
config *config.Config
}
func NewBasePublisher(headless bool, title, content string, tags []string, tenantID, platIndex, requestID, imagePath, wordPath string, platInfo map[string]interface{}, config *config.Config) *BasePublisher {
// NewBasePublisher 构造函数,增加 logger 参数
func NewBasePublisher(headless bool, title, content string, tags []string, tenantID, platIndex, requestID, imagePath, wordPath string, platInfo map[string]interface{}, config *config.Config, logger *log.Logger) *BasePublisher {
cookiesDir := filepath.Join(config.Sys.CookiesDir, tenantID)
os.MkdirAll(cookiesDir, 0755)
cookiesFile := filepath.Join(cookiesDir, platIndex+".json")
logFile, _ := os.Create(filepath.Join(config.Sys.LogsDir, requestID+".log"))
logger := log.New(logFile, "", log.LstdFlags)
var baseLogger *log.Logger
var logFile *os.File
if logger != nil {
// 使用传入的logger
baseLogger = logger
logFile = nil
} else {
// 兼容旧逻辑
logsDir := config.Sys.LogsDir
if logsDir == "" {
logsDir = "./logs"
}
os.MkdirAll(logsDir, 0755)
logFile, _ = os.Create(filepath.Join(logsDir, requestID+".log"))
baseLogger = log.New(logFile, "", log.LstdFlags)
}
return &BasePublisher{
Headless: headless,
@ -58,7 +75,7 @@ func NewBasePublisher(headless bool, title, content string, tags []string, tenan
RequestID: requestID,
ImagePath: imagePath,
WordPath: wordPath,
Logger: logger,
Logger: baseLogger,
LogFile: logFile,
CookiesFile: cookiesFile,
PlatInfo: platInfo,
@ -69,6 +86,7 @@ func NewBasePublisher(headless bool, title, content string, tags []string, tenan
func (b *BasePublisher) SetupDriver() error {
l := launcher.New()
l.Headless(b.Headless)
// 设置用户数据目录
userDataDir := filepath.Join(b.config.Sys.ChromeDataDir, b.TenantID)
os.MkdirAll(userDataDir, 0755)
@ -89,7 +107,8 @@ func (b *BasePublisher) SetupDriver() error {
// 窗口大小
l.Set("window-size", "1920,1080")
l.Set("lang", "zh-CN")
l.Set("start-maximized")
l.Set("force-device-scale-factor", "1")
url, err := l.Launch()
if err != nil {
return fmt.Errorf("启动浏览器失败: %v", err)
@ -97,7 +116,9 @@ func (b *BasePublisher) SetupDriver() error {
b.Browser = rod.New().ControlURL(url).MustConnect()
b.Page = b.Browser.MustPage()
b.Page.MustSetViewport(1920, 1080, 1, false)
// 删除这行!!!!
// b.Page.MustSetViewport(1920, 1080, 1, false)
return nil
}
@ -178,11 +199,24 @@ func (b *BasePublisher) WaitForElementClickable(selector string, timeout int) (*
}
func (b *BasePublisher) JSClick(element *rod.Element) error {
// 使用 element.Evaluate 并传入 EvalOptions
_, err := element.Evaluate(&rod.EvalOptions{
JS: `el => el.click()`,
})
return err
if element == nil {
return fmt.Errorf("element is nil")
}
// 方法1使用 rod 自带的 Click
return element.Click(proto.InputMouseButtonLeft, 1)
//// 方法2使用 JavaScript 点击(修复版)
//_, err := element.Evaluate(&rod.EvalOptions{
// JS: `function(el) {
// if(el && el.click) {
// el.click();
// return true;
// }
// return false;
// }(this)`,
//})
//return err
}
func (b *BasePublisher) ScrollToElement(element *rod.Element) error {

View File

@ -0,0 +1,50 @@
package publisher
import (
"geo/internal/config"
"log"
)
type PublisherInerface interface {
PublishNote() (bool, string)
WaitLogin() (bool, string)
}
type NewPublisher func(
headless bool,
title string,
content string,
tags []string,
tenantID string,
platIndex string,
requestID string,
imagePath string,
wordPath string,
platInfo map[string]interface{},
cfg *config.Config,
logger *log.Logger) PublisherInerface
type PublisherValue struct {
Name string
InitMethod NewPublisher
ContentFormat string //需要的文章格式html,text,markdown
ImgNeed int8 //是否需要图片1需要2非必须3不要
Type int8 //类型1文章2视频
}
var PublisherMap = map[string]*PublisherValue{
"xhs": {
Name: "小红书",
InitMethod: NewXiaohongshuPublisher,
ContentFormat: "text",
ImgNeed: 3,
Type: 1,
},
"bjh": {
Name: "百家号",
InitMethod: NewBaijiahaoPublisher,
ContentFormat: "text",
ImgNeed: 1,
Type: 1,
},
}

View File

@ -2,6 +2,8 @@ package publisher
import (
"fmt"
"geo/pkg"
"log"
"strings"
"time"
@ -13,29 +15,20 @@ import (
type XiaohongshuPublisher struct {
*BasePublisher
maxRetries int
retryDelay int
}
func NewXiaohongshuPublisher(headless bool, title, content string, tags []string, tenantID, platIndex, requestID, imagePath, wordPath string, platInfo map[string]interface{}, cfg *config.Config) *XiaohongshuPublisher {
base := NewBasePublisher(headless, title, content, tags, tenantID, platIndex, requestID, imagePath, wordPath, platInfo, cfg)
// NewXiaohongshuPublisher 构造函数,增加 logger 参数
func NewXiaohongshuPublisher(headless bool, title, content string, tags []string, tenantID, platIndex, requestID, imagePath, wordPath string, platInfo map[string]interface{}, cfg *config.Config, logger *log.Logger) PublisherInerface {
base := NewBasePublisher(headless, title, content, tags, tenantID, platIndex, requestID, imagePath, wordPath, platInfo, cfg, logger)
if platInfo != nil {
base.LoginURL = getString(platInfo, "login_url")
base.EditorURL = getString(platInfo, "edit_url")
base.LoginedURL = getString(platInfo, "logined_url")
base.LoginURL = pkg.GetString(platInfo, "login_url")
base.EditorURL = pkg.GetString(platInfo, "edit_url")
base.LoginedURL = pkg.GetString(platInfo, "logined_url")
}
return &XiaohongshuPublisher{BasePublisher: base}
}
func (p *XiaohongshuPublisher) CheckLoginStatus() bool {
url := p.GetCurrentURL()
// 如果URL包含登录相关关键词表示未登录
if strings.Contains(url, "login") || strings.Contains(url, "signin") || strings.Contains(url, "passport") {
return false
}
// 如果URL是编辑页面或主页表示已登录
if strings.Contains(url, "creator") || strings.Contains(url, "editor") || strings.Contains(url, "publish") {
return true
}
return true
return &XiaohongshuPublisher{BasePublisher: base, maxRetries: 5, retryDelay: 200}
}
func (p *XiaohongshuPublisher) CheckLogin() (bool, string) {
@ -48,7 +41,7 @@ func (p *XiaohongshuPublisher) CheckLogin() (bool, string) {
p.Page.MustNavigate(p.LoginedURL)
p.Sleep(3)
p.WaitForPageReady(5)
//p.WaitForPageReady(5)
if p.CheckLoginStatus() {
p.SaveCookies()
@ -260,86 +253,143 @@ func (p *XiaohongshuPublisher) clickPublish() error {
}
p.SleepMs(1000)
// 查找发布按钮
publishSelectors := []string{
".publish-page-publish-btn button",
".publish-btn",
".submit-btn",
"button[type='submit']",
}
var publishBtn *rod.Element
var err error
for _, selector := range publishSelectors {
publishBtn, err = p.WaitForElementClickable(selector, 5)
if err == nil && publishBtn != nil {
p.LogInfo(fmt.Sprintf("找到发布按钮: %s", selector))
break
// 查找并点击 next-btn对应Python中的第一步
for attempt := 0; attempt < p.maxRetries; attempt++ {
nextBtns, err := p.Page.Elements("button[class*='next-btn']")
if err != nil || len(nextBtns) == 0 {
nextBtns, err = p.Page.Elements(".next-btn")
}
if err == nil && len(nextBtns) > 0 {
if err := p.JSClick(nextBtns[0]); err != nil {
p.LogInfo(fmt.Sprintf("点击next-btn失败: %v", err))
} else {
p.LogInfo("已点击next-btn")
p.SleepMs(1000)
}
}
p.SleepMs(p.retryDelay)
}
// 如果还是没找到,通过文本查找
if publishBtn == nil {
publishBtn, err = p.Page.ElementX("//button[contains(text(), '发布')]")
// 进入发布设置页面点击submit按钮
p.LogInfo("进入发布设置页面...")
for attempt := 0; attempt < p.maxRetries; attempt++ {
submitBtn, err := p.WaitForElement("button[class*='submit']", 3)
if err != nil {
return fmt.Errorf("未找到发布按钮: %v", err)
submitBtn, err = p.WaitForElement("button.submit", 3)
}
if err == nil && submitBtn != nil {
if err := p.JSClick(submitBtn); err != nil {
p.LogInfo(fmt.Sprintf("点击submit按钮失败: %v", err))
} else {
p.LogInfo("已点击submit按钮")
p.SleepMs(2000)
break
}
}
p.LogInfo(fmt.Sprintf("未找到submit按钮第%d次重试...", attempt+1))
p.SleepMs(p.retryDelay)
}
// 输入话题标签
p.LogInfo("输入话题标签...")
tiptap, err := p.WaitForElement(".tiptap-container", 10)
if err == nil && tiptap != nil {
editors, err := tiptap.Elements("[contenteditable='true']")
if err == nil && len(editors) > 0 {
// 将tags转换为 #tag1 #tag2 格式
var tagStrings []string
for _, tag := range p.Tags {
if tag != "" && strings.TrimSpace(tag) != "" {
tagStrings = append(tagStrings, "#"+tag)
}
}
tagString := strings.Join(tagStrings, " ")
p.LogInfo(fmt.Sprintf("输入标签: %s", tagString))
if err := p.JSInputContentEditable(editors[0], tagString); err != nil {
p.LogInfo(fmt.Sprintf("输入标签失败: %v", err))
}
p.SleepMs(1000)
}
}
p.SleepMs(2000)
// 滚动到按钮位置
if err := p.ScrollToElement(publishBtn); err != nil {
p.LogInfo(fmt.Sprintf("滚动到按钮失败: %v", err))
}
p.SleepMs(500)
// 点击发布 - 使用 Click 方法
if err := publishBtn.Click(proto.InputMouseButtonLeft, 1); err != nil {
return fmt.Errorf("点击发布按钮失败: %v", err)
// 最终发布
p.LogInfo("最终发布...")
for attempt := 0; attempt < p.maxRetries; attempt++ {
if p.CheckElementExists(".publish-page-publish-btn", 2) {
publishDiv, err := p.Page.Element(".publish-page-publish-btn")
if err == nil && publishDiv != nil {
buttons, err := publishDiv.Elements("button")
if err == nil && len(buttons) >= 2 {
if err := p.JSClick(buttons[1]); err != nil {
p.LogInfo(fmt.Sprintf("点击最终发布按钮失败: %v", err))
} else {
p.LogInfo("已点击最终发布按钮")
break
}
}
}
}
p.SleepMs(p.retryDelay)
}
p.LogInfo("已点击发布按钮")
return nil
}
func (p *XiaohongshuPublisher) waitForPublishResult() (bool, string) {
p.LogInfo("等待发布结果...")
// 等待最多60秒
for i := 0; i < 60; i++ {
p.SleepMs(1000)
for attempt := 0; attempt < 30; attempt++ {
// 检查是否出现失败提示
toastDiv, err := p.WaitForElement(".creator-publish-toast", 2)
if err == nil && toastDiv != nil {
toastText, err := toastDiv.Text()
if err == nil && toastText != "" {
p.LogInfo(fmt.Sprintf("发布失败提示: %s", toastText))
return false, fmt.Sprintf("发布失败: %s", toastText)
}
}
// 检查URL是否跳转到成功页面
currentURL := p.GetCurrentURL()
if strings.Contains(currentURL, "success") ||
strings.Contains(currentURL, "content/manage") ||
strings.Contains(currentURL, "work-management") {
p.LogInfo("发布成功!")
// 检查URL是否包含success
info, err := p.Page.Info()
if err == nil && strings.Contains(info.URL, "success") {
p.LogInfo(fmt.Sprintf("发布成功URL包含success: %s", info.URL))
return true, "发布成功"
}
// 检查是否有成功提示
elements, _ := p.Page.Elements(".semi-toast-content, .toast-success, [class*='success']")
for _, el := range elements {
text, _ := el.Text()
if strings.Contains(text, "成功") || strings.Contains(text, "已发布") {
p.LogInfo(fmt.Sprintf("发布成功: %s", text))
return true, text
}
}
// 检查是否有失败提示
elements, _ = p.Page.Elements(".semi-toast-error, .toast-error, [class*='error']")
for _, el := range elements {
text, _ := el.Text()
if strings.Contains(text, "失败") || strings.Contains(text, "错误") {
p.LogError(fmt.Sprintf("发布失败: %s", text))
return false, text
}
}
p.SleepMs(1000)
}
return false, "发布结果未知(超时)"
return false, "发布结果未知"
}
// JSInputContentEditable 向contenteditable元素输入内容
func (p *XiaohongshuPublisher) JSInputContentEditable(element *rod.Element, text string) error {
_, err := element.Eval(fmt.Sprintf(`() => { this.innerText = %s; }`, jsQuote(text)))
return err
}
// CheckElementExists 检查元素是否存在
func (p *XiaohongshuPublisher) CheckElementExists(selector string, timeout int) bool {
_, err := p.WaitForElement(selector, timeout)
return err == nil
}
func jsQuote(s string) string {
return "`" + strings.ReplaceAll(s, "`", "\\`") + "`"
}
func (p *XiaohongshuPublisher) CheckLoginStatus() bool {
url := p.GetCurrentURL()
// 如果URL包含登录相关关键词表示未登录
if strings.Contains(url, p.LoginURL) {
return false
}
return true
}
func (p *XiaohongshuPublisher) PublishNote() (bool, string) {
@ -356,9 +406,9 @@ func (p *XiaohongshuPublisher) PublishNote() (bool, string) {
}
defer p.Close()
// 访问已登录页面
// 访问发布页面
p.Page.MustNavigate(p.LoginedURL)
p.Sleep(3)
p.WaitForPageReady(5)
// 尝试加载cookies
@ -383,9 +433,28 @@ func (p *XiaohongshuPublisher) PublishNote() (bool, string) {
// 访问发布页面
p.Page.MustNavigate(p.EditorURL)
p.Sleep(3)
p.WaitForPageReady(5)
// 执行发布流程之前,先点击上传区域的第一个按钮
p.LogInfo("点击上传按钮...")
uploadDiv, err := p.WaitForElement(".upload-content", 10)
if err != nil {
p.LogInfo(fmt.Sprintf("未找到上传区域: %v", err))
} else {
buttons, err := uploadDiv.Elements("button")
if err != nil {
p.LogInfo(fmt.Sprintf("查找按钮失败: %v", err))
} else if len(buttons) > 0 {
if err := p.JSClick(buttons[0]); err != nil {
p.LogInfo(fmt.Sprintf("JS点击按钮失败: %v", err))
} else {
p.LogInfo("已点击上传按钮")
p.Sleep(1)
}
}
}
// 执行发布流程
steps := []struct {
name string
@ -393,8 +462,6 @@ func (p *XiaohongshuPublisher) PublishNote() (bool, string) {
}{
{"输入内容", p.inputContent},
{"输入标题", p.inputTitle},
{"设置标签", p.inputTags},
{"上传封面", p.uploadImage},
}
for _, step := range steps {
@ -414,12 +481,3 @@ func (p *XiaohongshuPublisher) PublishNote() (bool, string) {
// 等待发布结果
return p.waitForPublishResult()
}
func getString(m map[string]interface{}, key string) string {
if v, ok := m[key]; ok {
if s, ok := v.(string); ok {
return s
}
}
return ""
}

View File

@ -1,6 +1,7 @@
package service
import (
"geo/internal/manager"
"os"
"path/filepath"
@ -9,7 +10,6 @@ import (
"geo/internal/biz"
"geo/internal/config"
"geo/internal/entitys"
"geo/internal/publisher"
"geo/pkg"
"geo/tmpl/errcode"
)
@ -49,14 +49,8 @@ func (s *LoginService) LoginPlatform(c *fiber.Ctx, req *entitys.LoginPlatformReq
"logined_url": platInfo.LoginedURL,
}
var pub interface{ WaitLogin() (bool, string) }
switch req.PlatIndex {
case "xhs":
pub = publisher.NewXiaohongshuPublisher(false, "", "", nil, req.UserIndex, req.PlatIndex, "", "", "", platMap, s.cfg)
default:
pub = publisher.NewXiaohongshuPublisher(false, "", "", nil, req.UserIndex, req.PlatIndex, "", "", "", platMap, s.cfg)
}
publisherClass := manager.GetPublisherClass(req.PlatIndex)
pub := publisherClass.InitMethod(false, "", "", nil, req.UserIndex, req.PlatIndex, "", "", "", platMap, s.cfg, nil)
success, msg := pub.WaitLogin()
if !success {
return errcode.SysErr(msg)

View File

@ -1,45 +1,59 @@
package pkg
import (
"strings"
md "github.com/JohannesKaufmann/html-to-markdown"
strip "github.com/grokify/html-strip-tags-go"
"encoding/json"
"fmt"
"os"
"os/exec"
"path/filepath"
)
func ExtractWordContent(filePath string, format string) (string, error) {
// 简化版:读取文件内容并转换格式
// 完整实现需要使用专门的docx库
content := "从Word文档提取的内容"
switch format {
case "html":
return "<p>" + content + "</p>", nil
case "markdown":
converter := md.NewConverter("", true, nil)
return converter.ConvertString("<p>" + content + "</p>")
default:
return strip.StripTags(content), nil
// ExtractWordContent 从docx文件提取内容
func ExtractWordContent(filePath, format string) (string, error) {
// 获取exe路径
baseDir, err := os.Getwd()
if err != nil {
return "", fmt.Errorf("获取工作目录失败: %w", err)
}
}
func ParseTags(tagStr string) []string {
if tagStr == "" {
return []string{}
exePath := filepath.Join(baseDir, "plugins", "docx_extractor.exe")
// 检查exe是否存在
if _, err := os.Stat(exePath); os.IsNotExist(err) {
return "", fmt.Errorf("exe不存在: %s", exePath)
}
tags := strings.Split(tagStr, ",")
result := make([]string, 0)
for _, t := range tags {
trimmed := strings.TrimSpace(t)
if trimmed != "" {
result = append(result, trimmed)
// 检查docx文件是否存在
if _, err := os.Stat(filePath); os.IsNotExist(err) {
return "", fmt.Errorf("文件不存在: %s", filePath)
}
// 正确调用方式:使用 --format 参数
cmd := exec.Command(exePath, filePath, "--format", format, "--json")
output, err := cmd.Output()
if err != nil {
// 获取错误输出
if exitErr, ok := err.(*exec.ExitError); ok {
return "", fmt.Errorf("执行失败: %w, stderr: %s", err, string(exitErr.Stderr))
}
return "", fmt.Errorf("执行失败: %w", err)
}
return result
}
func IsTimeExceeded(targetTime string) bool {
// 实现时间比较
return false
// 解析JSON
var result struct {
Success bool `json:"success"`
Content string `json:"content"`
Error string `json:"error"`
}
if err := json.Unmarshal(output, &result); err != nil {
return "", fmt.Errorf("解析结果失败: %w, output: %s", err, string(output))
}
if !result.Success {
return "", fmt.Errorf("提取失败: %s", result.Error)
}
return result.Content, nil
}

View File

@ -9,6 +9,7 @@ import (
"os"
"path/filepath"
"reflect"
"strings"
"time"
"github.com/go-viper/mapstructure/v2"
@ -245,3 +246,35 @@ func GenerateUUID() string {
func GenerateUserIndex() string {
return uuid.New().String()[:20]
}
func GetString(m map[string]interface{}, key string) string {
if v, ok := m[key]; ok {
switch v.(type) {
case []uint8:
return string(v.([]uint8))
case string:
return v.(string)
case int64:
return fmt.Sprintf("%d", v)
default:
return fmt.Sprintf("%v", v)
}
}
return ""
}
func ParseTags(tagStr string) []string {
if tagStr == "" {
return []string{}
}
tags := strings.Split(tagStr, ",")
result := make([]string, 0)
for _, t := range tags {
trimmed := strings.TrimSpace(t)
if trimmed != "" {
result = append(result, trimmed)
}
}
return result
}

BIN
plugins/docx_extractor.exe Normal file

Binary file not shown.