diff --git a/cookies/0d86b848uu2183uu4a08/xhs.json b/cookies/0d86b848uu2183uu4a08/xhs.json index 0637a08..2c36014 100644 --- a/cookies/0d86b848uu2183uu4a08/xhs.json +++ b/cookies/0d86b848uu2183uu4a08/xhs.json @@ -1 +1 @@ -[] \ No newline at end of file +[{"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}] \ No newline at end of file diff --git a/docs/qqqq.docx b/docs/qqqq.docx new file mode 100644 index 0000000..f5b5837 Binary files /dev/null and b/docs/qqqq.docx differ diff --git a/go.mod b/go.mod index 4834fb1..1ff2bdc 100644 --- a/go.mod +++ b/go.mod @@ -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 ) diff --git a/go.sum b/go.sum index 8a5547e..d660db2 100644 --- a/go.sum +++ b/go.sum @@ -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= diff --git a/internal/manager/publish_manager.go b/internal/manager/publish_manager.go index 782ea4b..fec5959 100644 --- a/internal/manager/publish_manager.go +++ b/internal/manager/publish_manager.go @@ -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] } diff --git a/internal/publisher/baijiahao.go b/internal/publisher/baijiahao.go index 73402b2..79f8c95 100644 --- a/internal/publisher/baijiahao.go +++ b/internal/publisher/baijiahao.go @@ -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 +} diff --git a/internal/publisher/base.go b/internal/publisher/base.go index 18ae520..5d55dbc 100644 --- a/internal/publisher/base.go +++ b/internal/publisher/base.go @@ -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 { diff --git a/internal/publisher/interface.go b/internal/publisher/interface.go new file mode 100644 index 0000000..f40f14c --- /dev/null +++ b/internal/publisher/interface.go @@ -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, + }, +} diff --git a/internal/publisher/xiaohongshu.go b/internal/publisher/xiaohongshu.go index 42f8236..9ad7e32 100644 --- a/internal/publisher/xiaohongshu.go +++ b/internal/publisher/xiaohongshu.go @@ -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 "" -} diff --git a/internal/service/login.go b/internal/service/login.go index 3f59466..60a1c4d 100644 --- a/internal/service/login.go +++ b/internal/service/login.go @@ -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) diff --git a/pkg/doc.go b/pkg/doc.go index 637b72d..b095e64 100644 --- a/pkg/doc.go +++ b/pkg/doc.go @@ -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 "

" + content + "

", nil - case "markdown": - converter := md.NewConverter("", true, nil) - return converter.ConvertString("

" + content + "

") - 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 } diff --git a/pkg/func.go b/pkg/func.go index 03f68c7..fd5b26a 100644 --- a/pkg/func.go +++ b/pkg/func.go @@ -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 +} diff --git a/plugins/docx_extractor.exe b/plugins/docx_extractor.exe new file mode 100644 index 0000000..bc0ef69 Binary files /dev/null and b/plugins/docx_extractor.exe differ