This commit is contained in:
parent
83f7d34f3e
commit
4538a300d7
|
|
@ -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}]
|
||||||
Binary file not shown.
37
go.mod
37
go.mod
|
|
@ -3,32 +3,34 @@ module geo
|
||||||
go 1.26.1
|
go 1.26.1
|
||||||
|
|
||||||
require (
|
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-kratos/kratos/v2 v2.9.2
|
||||||
github.com/go-playground/validator/v10 v10.30.2
|
github.com/go-playground/validator/v10 v10.30.2
|
||||||
github.com/go-rod/rod v0.116.2
|
github.com/go-rod/rod v0.116.2
|
||||||
github.com/go-viper/mapstructure/v2 v2.5.0
|
github.com/go-viper/mapstructure/v2 v2.5.0
|
||||||
github.com/gofiber/fiber/v2 v2.52.12
|
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/uuid v1.6.0
|
||||||
github.com/google/wire v0.7.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/redis/go-redis/v9 v9.18.0
|
||||||
github.com/spf13/viper v1.21.0
|
|
||||||
gorm.io/driver/mysql v1.6.0
|
gorm.io/driver/mysql v1.6.0
|
||||||
gorm.io/gorm v1.31.1
|
gorm.io/gorm v1.31.1
|
||||||
xorm.io/builder v0.3.13
|
xorm.io/builder v0.3.13
|
||||||
)
|
)
|
||||||
|
|
||||||
|
require github.com/nguyenthenguyen/docx v0.0.0-20230621112118-9c8e795a11db // indirect
|
||||||
|
|
||||||
require (
|
require (
|
||||||
filippo.io/edwards25519 v1.1.0 // indirect
|
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/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/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/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/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/locales v0.14.1 // indirect
|
||||||
github.com/go-playground/universal-translator v0.18.1 // indirect
|
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||||
github.com/go-sql-driver/mysql v1.8.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/jinzhu/now v1.1.5 // indirect
|
||||||
github.com/klauspost/compress v1.17.9 // indirect
|
github.com/klauspost/compress v1.17.9 // indirect
|
||||||
github.com/leodido/go-urn v1.4.0 // 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-colorable v0.1.13 // indirect
|
||||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
github.com/mattn/go-runewidth v0.0.16 // indirect
|
github.com/mattn/go-runewidth v0.0.16 // indirect
|
||||||
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
|
github.com/moipa-cn/pptx v0.0.0-20220526133451-f2b53ab6e594 // indirect
|
||||||
github.com/rivo/uniseg v0.2.0 // indirect
|
github.com/muesli/reflow v0.3.0 // indirect
|
||||||
github.com/sagikazarmark/locafero v0.11.0 // indirect
|
github.com/muesli/termenv v0.15.2 // indirect
|
||||||
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 // indirect
|
github.com/richardlehane/mscfb v1.0.4 // indirect
|
||||||
github.com/spf13/afero v1.15.0 // indirect
|
github.com/richardlehane/msoleps v1.0.3 // indirect
|
||||||
github.com/spf13/cast v1.10.0 // indirect
|
github.com/rivo/uniseg v0.4.7 // indirect
|
||||||
github.com/spf13/pflag v1.0.10 // indirect
|
github.com/stretchr/testify v1.11.1 // indirect
|
||||||
github.com/subosito/gotenv v1.6.0 // indirect
|
github.com/thedatashed/xlsxreader v1.2.8 // indirect
|
||||||
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
||||||
github.com/valyala/fasthttp v1.51.0 // indirect
|
github.com/valyala/fasthttp v1.51.0 // indirect
|
||||||
github.com/valyala/tcplisten v1.0.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/gson v0.7.3 // indirect
|
||||||
github.com/ysmood/leakless v0.9.0 // indirect
|
github.com/ysmood/leakless v0.9.0 // indirect
|
||||||
go.uber.org/atomic v1.11.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/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/sys v0.42.0 // indirect
|
||||||
golang.org/x/text v0.35.0 // indirect
|
golang.org/x/text v0.35.0 // indirect
|
||||||
)
|
)
|
||||||
|
|
|
||||||
152
go.sum
152
go.sum
|
|
@ -2,33 +2,37 @@ filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
|
||||||
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
|
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 h1:lSA0F4e9A2NcQSqGqTOXqu2aRi/XEQxDCBwM8yJtE6s=
|
||||||
gitea.com/xorm/sqlfiddle v0.0.0-20180821085327-62ce714f951a/go.mod h1:EXuID2Zs0pAQhH8yz+DNjUbjppKQzKFAn28TMYPB6IU=
|
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/WhityGhost/gh0ffice v1.0.0 h1:caSkJ90733riUlaqJffTjutoUUGdRi1M0A+x/qqQdKc=
|
||||||
github.com/JohannesKaufmann/html-to-markdown v1.6.0/go.mod h1:NUI78lGg/a7vpEJTz/0uOcYMaibytE4BUOQS8k78yPQ=
|
github.com/WhityGhost/gh0ffice v1.0.0/go.mod h1:+bSISwCkGiY+vUMvdHHyaYvp4vKtvIt9iovKrrMGvLY=
|
||||||
github.com/PuerkitoBio/goquery v1.9.2 h1:4/wZksC3KgkQw7SQgkKotmKljk0M6V8TUvA8Wb4yPeE=
|
|
||||||
github.com/PuerkitoBio/goquery v1.9.2/go.mod h1:GHPCaP0ODyyxqcNoFGYlAprUFH81NuRPd0GX3Zu2Mvk=
|
|
||||||
github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M=
|
github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M=
|
||||||
github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY=
|
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/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
|
||||||
github.com/andybalholm/cascadia v1.3.2/go.mod h1:7gtRlve5FxPPgIgX36uWBX58OdBsSS6lUvCFb+h7KvU=
|
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 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
|
||||||
github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c=
|
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 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
|
||||||
github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0=
|
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 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
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.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 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
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 h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
|
||||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
|
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/extrame/goyymmdd v0.0.0-20210114090516-7cc815f00d1a h1:c5k29baTzznteWs+9dxrtqpNxgtQ3V5NbU8d6laLK9Q=
|
||||||
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
|
github.com/extrame/goyymmdd v0.0.0-20210114090516-7cc815f00d1a/go.mod h1:xbpgo9r3xURoPa/l3sLKLGcnWlkz9UkfFsQ7lW0S6h8=
|
||||||
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
|
github.com/extrame/ole2 v0.0.0-20160812065207-d69429661ad7 h1:n+nk0bNe2+gVbRI8WRbLFVwwcBQ0rr5p+gzkKb6ol8c=
|
||||||
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
|
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 h1:46nXokslUBsAJE/wMsp5gtO500a4F3Nkz9Ufpk2AcUM=
|
||||||
github.com/gabriel-vasile/mimetype v1.4.13/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
|
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 h1:px8GJQBeLpquDKQWQ9zohEWiLA8n4D/pv7aH3asvUvo=
|
||||||
github.com/go-kratos/kratos/v2 v2.9.2/go.mod h1:Jc7jaeYd4RAPjetun2C+oFAOO7HNMHTT/Z4LxpuEDJM=
|
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 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
|
||||||
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
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=
|
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/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 h1:0LdToKclcPOj8PktUdIKo9BUohjjwfnQl42Dhw8/WUw=
|
||||||
github.com/gofiber/fiber/v2 v2.52.12/go.mod h1:YEcBbO/FB+5M1IZNBP9FO3J9281zgPAreiI1oqg8nDw=
|
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 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
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 h1:JxUKI6+CVBgCO2WToKy/nQk0sS+amI9z9EjVmdaocj4=
|
||||||
github.com/google/wire v0.7.0/go.mod h1:n6YbUQD9cPKTnHXEBN2DXlOp/mVADhVErcMFb0v3J18=
|
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 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
|
||||||
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
|
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
|
||||||
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
|
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/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 h1:lgaqFMSdTdQYdZ04uHyN2d/eKdOMyi2YLSvlQIBFYa4=
|
||||||
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
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 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
|
||||||
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
|
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 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
||||||
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
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.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 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
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 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
|
||||||
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
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/moipa-cn/pptx v0.0.0-20220526133451-f2b53ab6e594 h1:oRA3NxvX2usoIybfgLP37H1G7VVBDbnwseE7uKnK7lo=
|
||||||
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
|
github.com/moipa-cn/pptx v0.0.0-20220526133451-f2b53ab6e594/go.mod h1:eH2lLq9oH3EoCtPcCpsfPDpFKzknE2Hj5mUdlTivH8Q=
|
||||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
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 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
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 h1:pMkxYPkEbMPwRdenAzUNyFNrDgHx9U+DrBabWNfSRQs=
|
||||||
github.com/redis/go-redis/v9 v9.18.0/go.mod h1:k3ufPphLU5YXwNTUcCRXGxUoF1fqxnhFQmscfkCoDA0=
|
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/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||||
github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M=
|
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
|
||||||
github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA=
|
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||||
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/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
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.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 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||||
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
|
github.com/thedatashed/xlsxreader v1.2.8 h1:8aGbkXIPEThQbA8KzUZqIa4v4oqFrJFKLQ36vWePI5U=
|
||||||
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
|
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 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
|
||||||
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
|
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
|
||||||
github.com/valyala/fasthttp v1.51.0 h1:8b30A5JlZ6C7AS81RsWjYMQmrZG6feChmgAolCl1SqA=
|
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/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 h1:qxCG5VirSBvmi3uynXFkcnLMzkphdh3xx5FtrORwDCU=
|
||||||
github.com/ysmood/leakless v0.9.0/go.mod h1:R8iAXPRaG97QJwqxs74RdwzcRHT1SWCGTNqY8q0JvMQ=
|
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 h1:xZmwmqxHZA8AI603jOQ0tMqmBr9lPeFwGg6d+xy9DC0=
|
||||||
github.com/zeebo/xxh3 v1.0.2/go.mod h1:5NWz9Sef7zIDm2JHfFlcQvNekmcEl9ekUZQQKCYaDcA=
|
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 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
|
||||||
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
|
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 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4=
|
||||||
golang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA=
|
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/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI=
|
||||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo=
|
||||||
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/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
|
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/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.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.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 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
|
||||||
golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
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 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8=
|
||||||
golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA=
|
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 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
gorm.io/driver/mysql v1.6.0 h1:eNbLmNTpPpTOVZi8MMxCi2aaIm0ZpInbORNXDwyLGvg=
|
gorm.io/driver/mysql v1.6.0 h1:eNbLmNTpPpTOVZi8MMxCi2aaIm0ZpInbORNXDwyLGvg=
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,11 @@ import (
|
||||||
"geo/internal/config"
|
"geo/internal/config"
|
||||||
"geo/internal/publisher"
|
"geo/internal/publisher"
|
||||||
"geo/pkg"
|
"geo/pkg"
|
||||||
|
"io"
|
||||||
"log"
|
"log"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
|
@ -38,6 +42,43 @@ func GetPublishManager(config *config.Config, db *utils.Db) *PublishManager {
|
||||||
return 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 {
|
func (pm *PublishManager) Start(tokenID int) bool {
|
||||||
pm.mu.Lock()
|
pm.mu.Lock()
|
||||||
defer pm.mu.Unlock()
|
defer pm.mu.Unlock()
|
||||||
|
|
@ -124,7 +165,7 @@ func (pm *PublishManager) getPendingPublish() map[string]interface{} {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
requestID := getString(result, "request_id")
|
requestID := pkg.GetString(result, "request_id")
|
||||||
log.Printf("获取到待发布任务: token_id=%d, request_id=%s", pm.TokenID, requestID)
|
log.Printf("获取到待发布任务: token_id=%d, request_id=%s", pm.TokenID, requestID)
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
@ -139,98 +180,133 @@ func (pm *PublishManager) GetTaskByRequestID(requestID string) (map[string]inter
|
||||||
return pm.db.GetOne(sql, requestID)
|
return pm.db.GetOne(sql, requestID)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (pm *PublishManager) processSingleTask(publishData map[string]interface{}) map[string]interface{} {
|
func (pm *PublishManager) processSingleTask(publishData map[string]interface{}) (result map[string]interface{}) {
|
||||||
requestID := getString(publishData, "request_id")
|
requestID := pkg.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")
|
|
||||||
|
|
||||||
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, "")
|
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 {
|
if err != nil {
|
||||||
errMsg := fmt.Sprintf("下载文档失败: %v", err)
|
errMsg := fmt.Sprintf("下载文档失败: %v", err)
|
||||||
log.Printf("[任务 %s] %s", requestID, errMsg)
|
taskLogger.Printf("[任务 %s] ❌ %s", requestID, errMsg)
|
||||||
pm.updatePublishStatus(requestID, 3, errMsg)
|
pm.updatePublishStatus(requestID, 3, errMsg)
|
||||||
return map[string]interface{}{"success": false, "message": errMsg, "request_id": requestID}
|
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 {
|
if err != nil {
|
||||||
errMsg := fmt.Sprintf("下载图片失败: %v", err)
|
errMsg := fmt.Sprintf("下载图片失败: %v", err)
|
||||||
log.Printf("[任务 %s] %s", requestID, errMsg)
|
taskLogger.Printf("[任务 %s] ❌ %s", requestID, errMsg)
|
||||||
pm.updatePublishStatus(requestID, 3, errMsg)
|
pm.updatePublishStatus(requestID, 3, errMsg)
|
||||||
// 图片下载失败,清理已下载的文档
|
|
||||||
pkg.DeleteFile(docPath)
|
|
||||||
return map[string]interface{}{"success": false, "message": errMsg, "request_id": requestID}
|
return map[string]interface{}{"success": false, "message": errMsg, "request_id": requestID}
|
||||||
}
|
}
|
||||||
log.Printf("[任务 %s] 图片下载成功: %s", requestID, imgPath)
|
taskLogger.Printf("[任务 %s] ✅ 图片下载成功: %s", requestID, imgPath)
|
||||||
|
|
||||||
// 确保清理临时文件
|
|
||||||
defer func() {
|
|
||||||
pkg.DeleteFile(docPath)
|
|
||||||
pkg.DeleteFile(imgPath)
|
|
||||||
}()
|
|
||||||
|
|
||||||
// 解析标签
|
// 解析标签
|
||||||
tags := pkg.ParseTags(tagRaw)
|
tags := pkg.ParseTags(tagRaw)
|
||||||
log.Printf("[任务 %s] 标签解析完成: %v", requestID, tags)
|
taskLogger.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))
|
|
||||||
|
|
||||||
// 获取发布器
|
// 获取发布器
|
||||||
publisherClass := getPublisherClass(platIndex)
|
publisherClass := GetPublisherClass(platIndex)
|
||||||
if publisherClass == nil {
|
if publisherClass == nil {
|
||||||
errMsg := fmt.Sprintf("不支持的平台: %s", platIndex)
|
errMsg := fmt.Sprintf("不支持的平台: %s", platIndex)
|
||||||
log.Printf("[任务 %s] %s", requestID, errMsg)
|
taskLogger.Printf("[任务 %s] ❌ %s", requestID, errMsg)
|
||||||
pm.updatePublishStatus(requestID, 3, errMsg)
|
pm.updatePublishStatus(requestID, 3, errMsg)
|
||||||
return map[string]interface{}{"success": false, "message": errMsg, "request_id": requestID}
|
return map[string]interface{}{"success": false, "message": errMsg, "request_id": requestID}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 创建并执行发布器
|
// 提取内容
|
||||||
var pub interface{ PublishNote() (bool, string) }
|
taskLogger.Printf("[任务 %s] 开始提取文档内容...", requestID)
|
||||||
|
var content string
|
||||||
switch platIndex {
|
if publisherClass.Type == 1 {
|
||||||
case "xhs":
|
content, err = pkg.ExtractWordContent(docPath, publisherClass.ContentFormat)
|
||||||
pub = publisher.NewXiaohongshuPublisher(false, title, content, tags, userIndex, platIndex, requestID, imgPath, docPath, publishData, pm.Conf)
|
if err != nil {
|
||||||
log.Printf("[任务 %s] 创建小红书发布器", requestID)
|
errMsg := fmt.Sprintf("提取文档内容失败: %v", err)
|
||||||
case "bjh":
|
taskLogger.Printf("[任务 %s] ❌ %s", requestID, errMsg)
|
||||||
pub = publisher.NewBaijiahaoPublisher(false, title, content, tags, userIndex, platIndex, requestID, imgPath, docPath, publishData, pm.Conf)
|
pm.updatePublishStatus(requestID, 3, errMsg)
|
||||||
log.Printf("[任务 %s] 创建百家号发布器", requestID)
|
return map[string]interface{}{"success": false, "message": errMsg, "request_id": requestID}
|
||||||
default:
|
}
|
||||||
log.Printf("[任务 %s] 未知平台 %s,使用默认小红书发布器", requestID, platIndex)
|
|
||||||
pub = publisher.NewXiaohongshuPublisher(false, title, content, tags, userIndex, platIndex, requestID, imgPath, docPath, publishData, pm.Conf)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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()
|
success, message := pub.PublishNote()
|
||||||
|
|
||||||
if success {
|
if success {
|
||||||
log.Printf("[任务 %s] 发布成功: %s", requestID, message)
|
taskLogger.Printf("[任务 %s] ✅ 发布成功: %s", requestID, message)
|
||||||
pm.updatePublishStatus(requestID, 4, message)
|
pm.updatePublishStatus(requestID, 4, message)
|
||||||
} else {
|
} else {
|
||||||
log.Printf("[任务 %s] 发布失败: %s", requestID, message)
|
taskLogger.Printf("[任务 %s] ❌ 发布失败: %s", requestID, message)
|
||||||
pm.updatePublishStatus(requestID, 3, 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{}{
|
return map[string]interface{}{
|
||||||
"success": success,
|
"success": success,
|
||||||
"message": message,
|
"message": message,
|
||||||
|
|
@ -270,28 +346,7 @@ func (pm *PublishManager) GetStatus() map[string]interface{} {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func getPublisherClass(platIndex string) interface{} {
|
func GetPublisherClass(platIndex string) *publisher.PublisherValue {
|
||||||
platformMap := map[string]interface{}{
|
|
||||||
"xhs": struct{}{},
|
|
||||||
"bjh": struct{}{},
|
|
||||||
"csdn": struct{}{},
|
|
||||||
}
|
|
||||||
return platformMap[platIndex]
|
|
||||||
}
|
|
||||||
|
|
||||||
func getString(m map[string]interface{}, key string) string {
|
return publisher.PublisherMap[platIndex]
|
||||||
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 ""
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,10 +2,10 @@ package publisher
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"geo/internal/config"
|
"geo/internal/config"
|
||||||
|
"geo/pkg"
|
||||||
|
"log"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/go-rod/rod"
|
"github.com/go-rod/rod"
|
||||||
"github.com/go-rod/rod/lib/proto"
|
"github.com/go-rod/rod/lib/proto"
|
||||||
|
|
@ -16,40 +16,46 @@ type BaijiahaoPublisher struct {
|
||||||
Category string
|
Category string
|
||||||
ArticleType string
|
ArticleType string
|
||||||
IsTop bool
|
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 {
|
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)
|
base := NewBasePublisher(headless, title, content, tags, tenantID, platIndex, requestID, imagePath, wordPath, platInfo, cfg, logger)
|
||||||
if platInfo != nil {
|
if platInfo != nil {
|
||||||
base.LoginURL = getString(platInfo, "login_url")
|
base.LoginURL = pkg.GetString(platInfo, "login_url")
|
||||||
base.EditorURL = getString(platInfo, "edit_url")
|
base.EditorURL = pkg.GetString(platInfo, "edit_url")
|
||||||
base.LoginedURL = getString(platInfo, "logined_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 {
|
func (p *BaijiahaoPublisher) CheckLoginStatus() bool {
|
||||||
url := p.GetCurrentURL()
|
currentURL := p.GetCurrentURL()
|
||||||
// 如果URL包含登录相关关键词,表示未登录
|
if strings.Contains(currentURL, p.LoginURL) {
|
||||||
if strings.Contains(url, "login") || strings.Contains(url, "passport") {
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
// 如果URL是编辑页面或主页,表示已登录
|
|
||||||
if strings.Contains(url, "baijiahao") || strings.Contains(url, "edit") {
|
|
||||||
return true
|
return true
|
||||||
}
|
|
||||||
return url != p.LoginURL
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *BaijiahaoPublisher) CheckLogin() (bool, string) {
|
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 {
|
if err := p.SetupDriver(); err != nil {
|
||||||
return false, fmt.Sprintf("浏览器启动失败: %v", err)
|
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.Sleep(3)
|
||||||
p.WaitForPageReady(5)
|
p.WaitForPageReady(5)
|
||||||
|
|
||||||
|
|
@ -61,33 +67,33 @@ func (p *BaijiahaoPublisher) CheckLogin() (bool, string) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *BaijiahaoPublisher) WaitLogin() (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 {
|
if err := p.SetupDriver(); err != nil {
|
||||||
return false, fmt.Sprintf("浏览器启动失败: %v", err)
|
return false, fmt.Sprintf("浏览器启动失败: %v", err)
|
||||||
}
|
}
|
||||||
defer p.Close()
|
driverCreated = true
|
||||||
|
|
||||||
// 先尝试访问已登录页面
|
|
||||||
p.Page.MustNavigate(p.LoginedURL)
|
p.Page.MustNavigate(p.LoginedURL)
|
||||||
p.Sleep(3)
|
p.Sleep(3)
|
||||||
|
|
||||||
if p.CheckLoginStatus() {
|
if p.CheckLoginStatus() {
|
||||||
p.SaveCookies()
|
p.SaveCookies()
|
||||||
p.LogInfo("已有登录状态")
|
|
||||||
return true, "already_logged_in"
|
return true, "already_logged_in"
|
||||||
}
|
}
|
||||||
|
|
||||||
// 未登录,跳转到登录页
|
|
||||||
p.Page.MustNavigate(p.LoginURL)
|
p.Page.MustNavigate(p.LoginURL)
|
||||||
p.LogInfo("请扫描二维码登录...")
|
p.LogInfo("请扫描二维码登录...")
|
||||||
|
|
||||||
// 等待登录完成,最多120秒
|
|
||||||
for i := 0; i < 120; i++ {
|
for i := 0; i < 120; i++ {
|
||||||
time.Sleep(1 * time.Second)
|
p.Sleep(1)
|
||||||
if p.CheckLoginStatus() {
|
if p.CheckLoginStatus() {
|
||||||
p.SaveCookies()
|
p.SaveCookies()
|
||||||
p.LogInfo("登录成功")
|
|
||||||
return true, "login_success"
|
return true, "login_success"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -95,273 +101,58 @@ func (p *BaijiahaoPublisher) WaitLogin() (bool, string) {
|
||||||
return false, "登录超时"
|
return false, "登录超时"
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *BaijiahaoPublisher) inputTitle() error {
|
func (p *BaijiahaoPublisher) checkElementExists(selector string, timeout int) bool {
|
||||||
p.LogInfo("输入标题...")
|
_, err := p.WaitForElement(selector, timeout)
|
||||||
|
return err == nil
|
||||||
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) PublishNote() (bool, string) {
|
func (p *BaijiahaoPublisher) PublishNote() (bool, string) {
|
||||||
p.LogInfo(strings.Repeat("=", 50))
|
driverCreated := false
|
||||||
p.LogInfo("开始发布百家号文章...")
|
defer func() {
|
||||||
p.LogInfo(fmt.Sprintf("标题: %s", p.Title))
|
if driverCreated && p.Browser != nil {
|
||||||
p.LogInfo(fmt.Sprintf("内容长度: %d", len(p.Content)))
|
p.Close()
|
||||||
p.LogInfo(strings.Repeat("=", 50))
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
// 初始化浏览器
|
|
||||||
if err := p.SetupDriver(); err != nil {
|
if err := p.SetupDriver(); err != nil {
|
||||||
return false, fmt.Sprintf("浏览器启动失败: %v", err)
|
return false, fmt.Sprintf("浏览器启动失败: %v", err)
|
||||||
}
|
}
|
||||||
defer p.Close()
|
driverCreated = true
|
||||||
|
|
||||||
// 访问编辑器页面
|
|
||||||
p.Page.MustNavigate(p.EditorURL)
|
p.Page.MustNavigate(p.EditorURL)
|
||||||
p.Sleep(3)
|
p.Sleep(3)
|
||||||
p.WaitForPageReady(5)
|
p.WaitForPageReady(5)
|
||||||
|
|
||||||
// 尝试加载cookies
|
if p.LoadCookies() == nil {
|
||||||
if err := p.LoadCookies(); err == nil {
|
|
||||||
p.RefreshPage()
|
p.RefreshPage()
|
||||||
p.Sleep(2)
|
p.Sleep(3)
|
||||||
if p.CheckLoginStatus() {
|
if p.CheckLoginStatus() {
|
||||||
p.LogInfo("使用cookies登录成功")
|
return p.doPublish()
|
||||||
} else {
|
|
||||||
p.LogInfo("cookies已过期,需要重新登录")
|
|
||||||
return false, "需要登录"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 检查登录状态
|
if p.CheckLoginStatus() {
|
||||||
if !p.CheckLoginStatus() {
|
|
||||||
return false, "需要登录"
|
|
||||||
}
|
|
||||||
|
|
||||||
// 保存cookies
|
|
||||||
p.SaveCookies()
|
p.SaveCookies()
|
||||||
|
return p.doPublish()
|
||||||
|
}
|
||||||
|
|
||||||
|
return false, "需要登录"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *BaijiahaoPublisher) doPublish() (bool, string) {
|
||||||
|
p.LogInfo("开始发布百家号文章...")
|
||||||
|
p.Sleep(3)
|
||||||
|
|
||||||
// 执行发布流程
|
|
||||||
steps := []struct {
|
steps := []struct {
|
||||||
name string
|
name string
|
||||||
fn func() error
|
fn func() error
|
||||||
}{
|
}{
|
||||||
{"输入标题", p.inputTitle},
|
//{"切换到图文编辑模式", p.switchToGraphicMode},
|
||||||
{"输入内容", p.inputContent},
|
{"输入内容", p.inputContent},
|
||||||
{"上传封面", p.uploadImage},
|
{"输入标题", p.inputTitle},
|
||||||
|
{"设置封面", p.uploadImage},
|
||||||
|
{"点击发布按钮", p.clickPublish},
|
||||||
|
{"处理确认弹窗", p.handleConfirmModal},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, step := range steps {
|
for _, step := range steps {
|
||||||
|
|
@ -373,11 +164,385 @@ func (p *BaijiahaoPublisher) PublishNote() (bool, string) {
|
||||||
p.SleepMs(500)
|
p.SleepMs(500)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 点击发布
|
|
||||||
if err := p.clickPublish(); err != nil {
|
|
||||||
return false, err.Error()
|
|
||||||
}
|
|
||||||
|
|
||||||
// 等待发布结果
|
|
||||||
return p.waitForPublishResult()
|
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
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,7 @@ type BasePublisher struct {
|
||||||
|
|
||||||
Browser *rod.Browser
|
Browser *rod.Browser
|
||||||
Page *rod.Page
|
Page *rod.Page
|
||||||
|
|
||||||
Logger *log.Logger
|
Logger *log.Logger
|
||||||
LogFile *os.File
|
LogFile *os.File
|
||||||
|
|
||||||
|
|
@ -40,13 +41,29 @@ type BasePublisher struct {
|
||||||
config *config.Config
|
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)
|
cookiesDir := filepath.Join(config.Sys.CookiesDir, tenantID)
|
||||||
os.MkdirAll(cookiesDir, 0755)
|
os.MkdirAll(cookiesDir, 0755)
|
||||||
cookiesFile := filepath.Join(cookiesDir, platIndex+".json")
|
cookiesFile := filepath.Join(cookiesDir, platIndex+".json")
|
||||||
|
|
||||||
logFile, _ := os.Create(filepath.Join(config.Sys.LogsDir, requestID+".log"))
|
var baseLogger *log.Logger
|
||||||
logger := log.New(logFile, "", log.LstdFlags)
|
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{
|
return &BasePublisher{
|
||||||
Headless: headless,
|
Headless: headless,
|
||||||
|
|
@ -58,7 +75,7 @@ func NewBasePublisher(headless bool, title, content string, tags []string, tenan
|
||||||
RequestID: requestID,
|
RequestID: requestID,
|
||||||
ImagePath: imagePath,
|
ImagePath: imagePath,
|
||||||
WordPath: wordPath,
|
WordPath: wordPath,
|
||||||
Logger: logger,
|
Logger: baseLogger,
|
||||||
LogFile: logFile,
|
LogFile: logFile,
|
||||||
CookiesFile: cookiesFile,
|
CookiesFile: cookiesFile,
|
||||||
PlatInfo: platInfo,
|
PlatInfo: platInfo,
|
||||||
|
|
@ -69,6 +86,7 @@ func NewBasePublisher(headless bool, title, content string, tags []string, tenan
|
||||||
func (b *BasePublisher) SetupDriver() error {
|
func (b *BasePublisher) SetupDriver() error {
|
||||||
l := launcher.New()
|
l := launcher.New()
|
||||||
l.Headless(b.Headless)
|
l.Headless(b.Headless)
|
||||||
|
|
||||||
// 设置用户数据目录
|
// 设置用户数据目录
|
||||||
userDataDir := filepath.Join(b.config.Sys.ChromeDataDir, b.TenantID)
|
userDataDir := filepath.Join(b.config.Sys.ChromeDataDir, b.TenantID)
|
||||||
os.MkdirAll(userDataDir, 0755)
|
os.MkdirAll(userDataDir, 0755)
|
||||||
|
|
@ -89,7 +107,8 @@ func (b *BasePublisher) SetupDriver() error {
|
||||||
// 窗口大小
|
// 窗口大小
|
||||||
l.Set("window-size", "1920,1080")
|
l.Set("window-size", "1920,1080")
|
||||||
l.Set("lang", "zh-CN")
|
l.Set("lang", "zh-CN")
|
||||||
|
l.Set("start-maximized")
|
||||||
|
l.Set("force-device-scale-factor", "1")
|
||||||
url, err := l.Launch()
|
url, err := l.Launch()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("启动浏览器失败: %v", err)
|
return fmt.Errorf("启动浏览器失败: %v", err)
|
||||||
|
|
@ -97,7 +116,9 @@ func (b *BasePublisher) SetupDriver() error {
|
||||||
|
|
||||||
b.Browser = rod.New().ControlURL(url).MustConnect()
|
b.Browser = rod.New().ControlURL(url).MustConnect()
|
||||||
b.Page = b.Browser.MustPage()
|
b.Page = b.Browser.MustPage()
|
||||||
b.Page.MustSetViewport(1920, 1080, 1, false)
|
|
||||||
|
// 删除这行!!!!
|
||||||
|
// b.Page.MustSetViewport(1920, 1080, 1, false)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
@ -178,11 +199,24 @@ func (b *BasePublisher) WaitForElementClickable(selector string, timeout int) (*
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *BasePublisher) JSClick(element *rod.Element) error {
|
func (b *BasePublisher) JSClick(element *rod.Element) error {
|
||||||
// 使用 element.Evaluate 并传入 EvalOptions
|
if element == nil {
|
||||||
_, err := element.Evaluate(&rod.EvalOptions{
|
return fmt.Errorf("element is nil")
|
||||||
JS: `el => el.click()`,
|
}
|
||||||
})
|
|
||||||
return err
|
// 方法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 {
|
func (b *BasePublisher) ScrollToElement(element *rod.Element) error {
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
@ -2,6 +2,8 @@ package publisher
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"geo/pkg"
|
||||||
|
"log"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
|
@ -13,29 +15,20 @@ import (
|
||||||
|
|
||||||
type XiaohongshuPublisher struct {
|
type XiaohongshuPublisher struct {
|
||||||
*BasePublisher
|
*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 {
|
// NewXiaohongshuPublisher 构造函数,增加 logger 参数
|
||||||
base := NewBasePublisher(headless, title, content, tags, tenantID, platIndex, requestID, imagePath, wordPath, platInfo, cfg)
|
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 {
|
if platInfo != nil {
|
||||||
base.LoginURL = getString(platInfo, "login_url")
|
base.LoginURL = pkg.GetString(platInfo, "login_url")
|
||||||
base.EditorURL = getString(platInfo, "edit_url")
|
base.EditorURL = pkg.GetString(platInfo, "edit_url")
|
||||||
base.LoginedURL = getString(platInfo, "logined_url")
|
base.LoginedURL = pkg.GetString(platInfo, "logined_url")
|
||||||
}
|
}
|
||||||
return &XiaohongshuPublisher{BasePublisher: base}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *XiaohongshuPublisher) CheckLoginStatus() bool {
|
return &XiaohongshuPublisher{BasePublisher: base, maxRetries: 5, retryDelay: 200}
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *XiaohongshuPublisher) CheckLogin() (bool, string) {
|
func (p *XiaohongshuPublisher) CheckLogin() (bool, string) {
|
||||||
|
|
@ -48,7 +41,7 @@ func (p *XiaohongshuPublisher) CheckLogin() (bool, string) {
|
||||||
|
|
||||||
p.Page.MustNavigate(p.LoginedURL)
|
p.Page.MustNavigate(p.LoginedURL)
|
||||||
p.Sleep(3)
|
p.Sleep(3)
|
||||||
p.WaitForPageReady(5)
|
//p.WaitForPageReady(5)
|
||||||
|
|
||||||
if p.CheckLoginStatus() {
|
if p.CheckLoginStatus() {
|
||||||
p.SaveCookies()
|
p.SaveCookies()
|
||||||
|
|
@ -260,86 +253,143 @@ func (p *XiaohongshuPublisher) clickPublish() error {
|
||||||
}
|
}
|
||||||
p.SleepMs(1000)
|
p.SleepMs(1000)
|
||||||
|
|
||||||
// 查找发布按钮
|
// 查找并点击 next-btn(对应Python中的第一步)
|
||||||
publishSelectors := []string{
|
for attempt := 0; attempt < p.maxRetries; attempt++ {
|
||||||
".publish-page-publish-btn button",
|
nextBtns, err := p.Page.Elements("button[class*='next-btn']")
|
||||||
".publish-btn",
|
if err != nil || len(nextBtns) == 0 {
|
||||||
".submit-btn",
|
nextBtns, err = p.Page.Elements(".next-btn")
|
||||||
"button[type='submit']",
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var publishBtn *rod.Element
|
if err == nil && len(nextBtns) > 0 {
|
||||||
var err error
|
if err := p.JSClick(nextBtns[0]); err != nil {
|
||||||
|
p.LogInfo(fmt.Sprintf("点击next-btn失败: %v", err))
|
||||||
|
} else {
|
||||||
|
p.LogInfo("已点击next-btn")
|
||||||
|
p.SleepMs(1000)
|
||||||
|
|
||||||
for _, selector := range publishSelectors {
|
}
|
||||||
publishBtn, err = p.WaitForElementClickable(selector, 5)
|
}
|
||||||
if err == nil && publishBtn != nil {
|
p.SleepMs(p.retryDelay)
|
||||||
p.LogInfo(fmt.Sprintf("找到发布按钮: %s", selector))
|
}
|
||||||
|
|
||||||
|
// 进入发布设置页面,点击submit按钮
|
||||||
|
p.LogInfo("进入发布设置页面...")
|
||||||
|
for attempt := 0; attempt < p.maxRetries; attempt++ {
|
||||||
|
submitBtn, err := p.WaitForElement("button[class*='submit']", 3)
|
||||||
|
if err != nil {
|
||||||
|
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
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
p.LogInfo(fmt.Sprintf("未找到submit按钮,第%d次重试...", attempt+1))
|
||||||
// 如果还是没找到,通过文本查找
|
p.SleepMs(p.retryDelay)
|
||||||
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("输入话题标签...")
|
||||||
p.LogInfo(fmt.Sprintf("滚动到按钮失败: %v", err))
|
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)
|
||||||
}
|
}
|
||||||
p.SleepMs(500)
|
}
|
||||||
|
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)
|
||||||
|
|
||||||
// 点击发布 - 使用 Click 方法
|
// 最终发布
|
||||||
if err := publishBtn.Click(proto.InputMouseButtonLeft, 1); err != nil {
|
p.LogInfo("最终发布...")
|
||||||
return fmt.Errorf("点击发布按钮失败: %v", err)
|
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
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *XiaohongshuPublisher) waitForPublishResult() (bool, string) {
|
func (p *XiaohongshuPublisher) waitForPublishResult() (bool, string) {
|
||||||
p.LogInfo("等待发布结果...")
|
p.LogInfo("等待发布结果...")
|
||||||
|
|
||||||
// 等待最多60秒
|
for attempt := 0; attempt < 30; attempt++ {
|
||||||
for i := 0; i < 60; i++ {
|
// 检查是否出现失败提示
|
||||||
p.SleepMs(1000)
|
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是否跳转到成功页面
|
// 检查URL是否包含success
|
||||||
currentURL := p.GetCurrentURL()
|
info, err := p.Page.Info()
|
||||||
if strings.Contains(currentURL, "success") ||
|
if err == nil && strings.Contains(info.URL, "success") {
|
||||||
strings.Contains(currentURL, "content/manage") ||
|
p.LogInfo(fmt.Sprintf("发布成功,URL包含success: %s", info.URL))
|
||||||
strings.Contains(currentURL, "work-management") {
|
|
||||||
p.LogInfo("发布成功!")
|
|
||||||
return true, "发布成功"
|
return true, "发布成功"
|
||||||
}
|
}
|
||||||
|
|
||||||
// 检查是否有成功提示
|
p.SleepMs(1000)
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 检查是否有失败提示
|
return false, "发布结果未知"
|
||||||
elements, _ = p.Page.Elements(".semi-toast-error, .toast-error, [class*='error']")
|
}
|
||||||
for _, el := range elements {
|
|
||||||
text, _ := el.Text()
|
// JSInputContentEditable 向contenteditable元素输入内容
|
||||||
if strings.Contains(text, "失败") || strings.Contains(text, "错误") {
|
func (p *XiaohongshuPublisher) JSInputContentEditable(element *rod.Element, text string) error {
|
||||||
p.LogError(fmt.Sprintf("发布失败: %s", text))
|
_, err := element.Eval(fmt.Sprintf(`() => { this.innerText = %s; }`, jsQuote(text)))
|
||||||
return false, 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 false, "发布结果未知(超时)"
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *XiaohongshuPublisher) PublishNote() (bool, string) {
|
func (p *XiaohongshuPublisher) PublishNote() (bool, string) {
|
||||||
|
|
@ -356,9 +406,9 @@ func (p *XiaohongshuPublisher) PublishNote() (bool, string) {
|
||||||
}
|
}
|
||||||
defer p.Close()
|
defer p.Close()
|
||||||
|
|
||||||
// 访问已登录页面
|
// 访问发布页面
|
||||||
p.Page.MustNavigate(p.LoginedURL)
|
p.Page.MustNavigate(p.LoginedURL)
|
||||||
p.Sleep(3)
|
|
||||||
p.WaitForPageReady(5)
|
p.WaitForPageReady(5)
|
||||||
|
|
||||||
// 尝试加载cookies
|
// 尝试加载cookies
|
||||||
|
|
@ -383,9 +433,28 @@ func (p *XiaohongshuPublisher) PublishNote() (bool, string) {
|
||||||
|
|
||||||
// 访问发布页面
|
// 访问发布页面
|
||||||
p.Page.MustNavigate(p.EditorURL)
|
p.Page.MustNavigate(p.EditorURL)
|
||||||
p.Sleep(3)
|
|
||||||
p.WaitForPageReady(5)
|
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 {
|
steps := []struct {
|
||||||
name string
|
name string
|
||||||
|
|
@ -393,8 +462,6 @@ func (p *XiaohongshuPublisher) PublishNote() (bool, string) {
|
||||||
}{
|
}{
|
||||||
{"输入内容", p.inputContent},
|
{"输入内容", p.inputContent},
|
||||||
{"输入标题", p.inputTitle},
|
{"输入标题", p.inputTitle},
|
||||||
{"设置标签", p.inputTags},
|
|
||||||
{"上传封面", p.uploadImage},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, step := range steps {
|
for _, step := range steps {
|
||||||
|
|
@ -414,12 +481,3 @@ func (p *XiaohongshuPublisher) PublishNote() (bool, string) {
|
||||||
// 等待发布结果
|
// 等待发布结果
|
||||||
return p.waitForPublishResult()
|
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 ""
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
package service
|
package service
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"geo/internal/manager"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
|
||||||
|
|
@ -9,7 +10,6 @@ import (
|
||||||
"geo/internal/biz"
|
"geo/internal/biz"
|
||||||
"geo/internal/config"
|
"geo/internal/config"
|
||||||
"geo/internal/entitys"
|
"geo/internal/entitys"
|
||||||
"geo/internal/publisher"
|
|
||||||
"geo/pkg"
|
"geo/pkg"
|
||||||
"geo/tmpl/errcode"
|
"geo/tmpl/errcode"
|
||||||
)
|
)
|
||||||
|
|
@ -49,14 +49,8 @@ func (s *LoginService) LoginPlatform(c *fiber.Ctx, req *entitys.LoginPlatformReq
|
||||||
"logined_url": platInfo.LoginedURL,
|
"logined_url": platInfo.LoginedURL,
|
||||||
}
|
}
|
||||||
|
|
||||||
var pub interface{ WaitLogin() (bool, string) }
|
publisherClass := manager.GetPublisherClass(req.PlatIndex)
|
||||||
switch req.PlatIndex {
|
pub := publisherClass.InitMethod(false, "", "", nil, req.UserIndex, req.PlatIndex, "", "", "", platMap, s.cfg, nil)
|
||||||
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)
|
|
||||||
}
|
|
||||||
|
|
||||||
success, msg := pub.WaitLogin()
|
success, msg := pub.WaitLogin()
|
||||||
if !success {
|
if !success {
|
||||||
return errcode.SysErr(msg)
|
return errcode.SysErr(msg)
|
||||||
|
|
|
||||||
90
pkg/doc.go
90
pkg/doc.go
|
|
@ -1,45 +1,59 @@
|
||||||
package pkg
|
package pkg
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"strings"
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
md "github.com/JohannesKaufmann/html-to-markdown"
|
"os"
|
||||||
strip "github.com/grokify/html-strip-tags-go"
|
"os/exec"
|
||||||
|
"path/filepath"
|
||||||
)
|
)
|
||||||
|
|
||||||
func ExtractWordContent(filePath string, format string) (string, error) {
|
// ExtractWordContent 从docx文件提取内容
|
||||||
// 简化版:读取文件内容并转换格式
|
func ExtractWordContent(filePath, format string) (string, error) {
|
||||||
// 完整实现需要使用专门的docx库
|
// 获取exe路径
|
||||||
|
baseDir, err := os.Getwd()
|
||||||
content := "从Word文档提取的内容"
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("获取工作目录失败: %w", err)
|
||||||
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
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
exePath := filepath.Join(baseDir, "plugins", "docx_extractor.exe")
|
||||||
func ParseTags(tagStr string) []string {
|
|
||||||
if tagStr == "" {
|
// 检查exe是否存在
|
||||||
return []string{}
|
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 {
|
// 检查docx文件是否存在
|
||||||
trimmed := strings.TrimSpace(t)
|
if _, err := os.Stat(filePath); os.IsNotExist(err) {
|
||||||
if trimmed != "" {
|
return "", fmt.Errorf("文件不存在: %s", filePath)
|
||||||
result = append(result, trimmed)
|
}
|
||||||
}
|
|
||||||
}
|
// 正确调用方式:使用 --format 参数
|
||||||
return result
|
cmd := exec.Command(exePath, filePath, "--format", format, "--json")
|
||||||
}
|
|
||||||
|
output, err := cmd.Output()
|
||||||
func IsTimeExceeded(targetTime string) bool {
|
if err != nil {
|
||||||
// 实现时间比较
|
// 获取错误输出
|
||||||
return false
|
if exitErr, ok := err.(*exec.ExitError); ok {
|
||||||
|
return "", fmt.Errorf("执行失败: %w, stderr: %s", err, string(exitErr.Stderr))
|
||||||
|
}
|
||||||
|
return "", fmt.Errorf("执行失败: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 解析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
|
||||||
}
|
}
|
||||||
|
|
|
||||||
33
pkg/func.go
33
pkg/func.go
|
|
@ -9,6 +9,7 @@ import (
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"reflect"
|
"reflect"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/go-viper/mapstructure/v2"
|
"github.com/go-viper/mapstructure/v2"
|
||||||
|
|
@ -245,3 +246,35 @@ func GenerateUUID() string {
|
||||||
func GenerateUserIndex() string {
|
func GenerateUserIndex() string {
|
||||||
return uuid.New().String()[:20]
|
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
|
||||||
|
}
|
||||||
|
|
|
||||||
Binary file not shown.
Loading…
Reference in New Issue