diff --git a/cookies/0d86b848uu2183uu4a08/xhs.json b/cookies/0d86b848uu2183uu4a08/xhs.json index 2c36014..1a16ae2 100644 --- a/cookies/0d86b848uu2183uu4a08/xhs.json +++ b/cookies/0d86b848uu2183uu4a08/xhs.json @@ -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}] \ No newline at end of file +[{"name":"loadts","value":"1775643436062","domain":".xiaohongshu.com","path":"/","expires":1807179436,"size":19,"httpOnly":false,"secure":false,"session":false,"priority":"Medium","sameParty":false,"sourceScheme":"Secure","sourcePort":443},{"name":"sec_poison_id","value":"c61fcdd3-c715-4d43-b5b0-ca6c29a621ba","domain":".xiaohongshu.com","path":"/","expires":1775643877,"size":49,"httpOnly":false,"secure":false,"session":false,"priority":"Medium","sameParty":false,"sourceScheme":"Secure","sourcePort":443},{"name":"websectiga","value":"f47eda31ec99545da40c2f731f0630efd2b0959e1dd10d5fedac3dce0bd1e04d","domain":".xiaohongshu.com","path":"/","expires":1775902472,"size":74,"httpOnly":false,"secure":false,"session":false,"priority":"Medium","sameParty":false,"sourceScheme":"Secure","sourcePort":443},{"name":"a1","value":"19d6b2f0b04zdps8dison8k8oibcyzaju3ji7d03d30000118748","domain":".xiaohongshu.com","path":"/","expires":1807155738,"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-68c517626228786611519495vggizcwmifuvnaaw","domain":".xiaohongshu.com","path":"/","expires":1778211754.091569,"size":96,"httpOnly":true,"secure":false,"session":false,"priority":"Medium","sameParty":false,"sourceScheme":"Secure","sourcePort":443},{"name":"acw_tc","value":"0a0d09d017756432743251523e05604bffad685b0b4bb5706dbccf32be49f5","domain":"creator.xiaohongshu.com","path":"/","expires":1775645071.571978,"size":68,"httpOnly":true,"secure":false,"session":false,"priority":"Medium","sameParty":false,"sourceScheme":"Secure","sourcePort":443},{"name":"customerClientId","value":"231145420384063","domain":".xiaohongshu.com","path":"/","expires":1810179755.091552,"size":31,"httpOnly":true,"secure":false,"session":false,"priority":"Medium","sameParty":false,"sourceScheme":"Secure","sourcePort":443},{"name":"galaxy_creator_session_id","value":"nWC5PzTFCCSJsLYiRFLFORIWUoUf5c4Egr3v","domain":".xiaohongshu.com","path":"/","expires":1778211755.091586,"size":61,"httpOnly":true,"secure":false,"session":false,"priority":"Medium","sameParty":false,"sourceScheme":"Secure","sourcePort":443},{"name":"galaxy.creator.beaker.session.id","value":"1775619757404082990054","domain":".xiaohongshu.com","path":"/","expires":1778211755.091605,"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":1810179755.091531,"size":57,"httpOnly":true,"secure":false,"session":false,"priority":"Medium","sameParty":false,"sourceScheme":"Secure","sourcePort":443},{"name":"customer-sso-sid","value":"68c5176262287866115194944uxxdffhcemiwvwt","domain":".xiaohongshu.com","path":"/","expires":1776224554.091467,"size":56,"httpOnly":true,"secure":false,"session":false,"priority":"Medium","sameParty":false,"sourceScheme":"Secure","sourcePort":443},{"name":"gid","value":"yjfKDJiJ0J7SyjfKDJi8Dqf884ufUMYf3MlIVqfYTYl3D3q8Svu0VW888yyYW4Y8JD0j8Yd2","domain":".xiaohongshu.com","path":"/","expires":1810179744.008933,"size":75,"httpOnly":false,"secure":false,"session":false,"priority":"Medium","sameParty":false,"sourceScheme":"Secure","sourcePort":443},{"name":"webId","value":"f840cc8b10e0a01b0bdb839482af19d1","domain":".xiaohongshu.com","path":"/","expires":1807155738,"size":37,"httpOnly":false,"secure":false,"session":false,"priority":"Medium","sameParty":false,"sourceScheme":"Secure","sourcePort":443},{"name":"ets","value":"1775619738206","domain":".xiaohongshu.com","path":"/","expires":1778211738.206388,"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":1807179436,"size":12,"httpOnly":false,"secure":false,"session":false,"priority":"Medium","sameParty":false,"sourceScheme":"Secure","sourcePort":443}] \ No newline at end of file diff --git a/go.mod b/go.mod index 1ff2bdc..b3a9bb5 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,6 @@ module geo go 1.26.1 require ( - github.com/WhityGhost/gh0ffice v1.0.0 github.com/go-kratos/kratos/v2 v2.9.2 github.com/go-playground/validator/v10 v10.30.2 github.com/go-rod/rod v0.116.2 @@ -17,20 +16,12 @@ require ( xorm.io/builder v0.3.13 ) -require github.com/nguyenthenguyen/docx v0.0.0-20230621112118-9c8e795a11db // indirect - require ( filippo.io/edwards25519 v1.1.0 // indirect github.com/andybalholm/brotli v1.1.0 // indirect - github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect - github.com/charmbracelet/lipgloss v0.10.0 // indirect - github.com/charmbracelet/log v0.4.0 // indirect github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect - github.com/extrame/goyymmdd v0.0.0-20210114090516-7cc815f00d1a // indirect - github.com/extrame/ole2 v0.0.0-20160812065207-d69429661ad7 // indirect github.com/gabriel-vasile/mimetype v1.4.13 // indirect - github.com/go-logfmt/logfmt v0.6.0 // indirect github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect github.com/go-sql-driver/mysql v1.8.1 // indirect @@ -38,19 +29,11 @@ require ( github.com/jinzhu/now v1.1.5 // indirect github.com/klauspost/compress v1.17.9 // indirect github.com/leodido/go-urn v1.4.0 // indirect - github.com/lucasb-eyer/go-colorful v1.2.0 // indirect - github.com/mattetti/filebuffer v1.0.1 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-runewidth v0.0.16 // indirect - github.com/moipa-cn/pptx v0.0.0-20220526133451-f2b53ab6e594 // indirect - github.com/muesli/reflow v0.3.0 // indirect - github.com/muesli/termenv v0.15.2 // indirect - github.com/richardlehane/mscfb v1.0.4 // indirect - github.com/richardlehane/msoleps v1.0.3 // indirect github.com/rivo/uniseg v0.4.7 // indirect github.com/stretchr/testify v1.11.1 // indirect - github.com/thedatashed/xlsxreader v1.2.8 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/valyala/fasthttp v1.51.0 // indirect github.com/valyala/tcplisten v1.0.0 // indirect @@ -61,7 +44,6 @@ require ( github.com/ysmood/leakless v0.9.0 // indirect go.uber.org/atomic v1.11.0 // indirect golang.org/x/crypto v0.49.0 // indirect - golang.org/x/exp v0.0.0-20231006140011-7918f672742d // indirect golang.org/x/sys v0.42.0 // indirect golang.org/x/text v0.35.0 // indirect ) diff --git a/go.sum b/go.sum index d660db2..b56aa68 100644 --- a/go.sum +++ b/go.sum @@ -2,37 +2,23 @@ filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= gitea.com/xorm/sqlfiddle v0.0.0-20180821085327-62ce714f951a h1:lSA0F4e9A2NcQSqGqTOXqu2aRi/XEQxDCBwM8yJtE6s= gitea.com/xorm/sqlfiddle v0.0.0-20180821085327-62ce714f951a/go.mod h1:EXuID2Zs0pAQhH8yz+DNjUbjppKQzKFAn28TMYPB6IU= -github.com/WhityGhost/gh0ffice v1.0.0 h1:caSkJ90733riUlaqJffTjutoUUGdRi1M0A+x/qqQdKc= -github.com/WhityGhost/gh0ffice v1.0.0/go.mod h1:+bSISwCkGiY+vUMvdHHyaYvp4vKtvIt9iovKrrMGvLY= github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M= github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY= -github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= -github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs= github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c= github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA= github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= -github.com/charmbracelet/lipgloss v0.10.0 h1:KWeXFSexGcfahHX+54URiZGkBFazf70JNMtwg/AFW3s= -github.com/charmbracelet/lipgloss v0.10.0/go.mod h1:Wig9DSfvANsxqkRsqj6x87irdy123SR4dOXlKa91ciE= -github.com/charmbracelet/log v0.4.0 h1:G9bQAcx8rWA2T3pWvx7YtPTPwgqpk7D68BX21IRW8ZM= -github.com/charmbracelet/log v0.4.0/go.mod h1:63bXt/djrizTec0l11H20t8FDSvA4CRZJ1KH22MdptM= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= -github.com/extrame/goyymmdd v0.0.0-20210114090516-7cc815f00d1a h1:c5k29baTzznteWs+9dxrtqpNxgtQ3V5NbU8d6laLK9Q= -github.com/extrame/goyymmdd v0.0.0-20210114090516-7cc815f00d1a/go.mod h1:xbpgo9r3xURoPa/l3sLKLGcnWlkz9UkfFsQ7lW0S6h8= -github.com/extrame/ole2 v0.0.0-20160812065207-d69429661ad7 h1:n+nk0bNe2+gVbRI8WRbLFVwwcBQ0rr5p+gzkKb6ol8c= -github.com/extrame/ole2 v0.0.0-20160812065207-d69429661ad7/go.mod h1:GPpMrAfHdb8IdQ1/R2uIRBsNfnPnwsYE9YYI5WyY1zw= github.com/gabriel-vasile/mimetype v1.4.13 h1:46nXokslUBsAJE/wMsp5gtO500a4F3Nkz9Ufpk2AcUM= github.com/gabriel-vasile/mimetype v1.4.13/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s= github.com/go-kratos/kratos/v2 v2.9.2 h1:px8GJQBeLpquDKQWQ9zohEWiLA8n4D/pv7aH3asvUvo= github.com/go-kratos/kratos/v2 v2.9.2/go.mod h1:Jc7jaeYd4RAPjetun2C+oFAOO7HNMHTT/Z4LxpuEDJM= -github.com/go-logfmt/logfmt v0.6.0 h1:wGYYu3uicYdqXVgoYbvnkrPVXkuLM1p1ifugDMEdRi4= -github.com/go-logfmt/logfmt v0.6.0/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs= github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= @@ -63,36 +49,17 @@ github.com/klauspost/cpuid/v2 v2.0.9 h1:lgaqFMSdTdQYdZ04uHyN2d/eKdOMyi2YLSvlQIBF github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= -github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= -github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= -github.com/mattetti/filebuffer v1.0.1 h1:gG7pyfnSIZCxdoKq+cPa8T0hhYtD9NxCdI4D7PTjRLM= -github.com/mattetti/filebuffer v1.0.1/go.mod h1:YdMURNDOttIiruleeVr6f56OrMc+MydEnTcXwtkxNVs= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= -github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= -github.com/moipa-cn/pptx v0.0.0-20220526133451-f2b53ab6e594 h1:oRA3NxvX2usoIybfgLP37H1G7VVBDbnwseE7uKnK7lo= -github.com/moipa-cn/pptx v0.0.0-20220526133451-f2b53ab6e594/go.mod h1:eH2lLq9oH3EoCtPcCpsfPDpFKzknE2Hj5mUdlTivH8Q= -github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s= -github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8= -github.com/muesli/termenv v0.15.2 h1:GohcuySI0QmI3wN8Ok9PtKGkgkFIk7y6Vpb5PvrY+Wo= -github.com/muesli/termenv v0.15.2/go.mod h1:Epx+iuz8sNs7mNKhxzH4fWXGNpZwUaJKRS1noLXviQ8= -github.com/nguyenthenguyen/docx v0.0.0-20230621112118-9c8e795a11db h1:v0cW/tTMrJQyZr7r6t+t9+NhH2OBAjydHisVYxuyObc= -github.com/nguyenthenguyen/docx v0.0.0-20230621112118-9c8e795a11db/go.mod h1:BZyH8oba3hE/BTt2FfBDGPOHhXiKs9RFmUvvXRdzrhM= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/redis/go-redis/v9 v9.18.0 h1:pMkxYPkEbMPwRdenAzUNyFNrDgHx9U+DrBabWNfSRQs= github.com/redis/go-redis/v9 v9.18.0/go.mod h1:k3ufPphLU5YXwNTUcCRXGxUoF1fqxnhFQmscfkCoDA0= -github.com/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.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= @@ -100,8 +67,6 @@ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+ github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 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/thedatashed/xlsxreader v1.2.8 h1:8aGbkXIPEThQbA8KzUZqIa4v4oqFrJFKLQ36vWePI5U= -github.com/thedatashed/xlsxreader v1.2.8/go.mod h1:wZyb/2xF1+rkZ2ujhC72tuuOWBY574QvcXHFls+5AXc= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= github.com/valyala/fasthttp v1.51.0 h1:8b30A5JlZ6C7AS81RsWjYMQmrZG6feChmgAolCl1SqA= @@ -128,8 +93,6 @@ go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= 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/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI= -golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo= golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= diff --git a/internal/config/config.go b/internal/config/config.go index cebafd6..b309061 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -56,7 +56,7 @@ func LoadConfig() (*Config, error) { }, Sys: Sys{ MaxConcurrent: 1, - TaskTimeout: 60, + TaskTimeout: 200, SessionTimeout: 300, MaxImageSize: 5 * 1024 * 1024, LogsDir: filepath.Join(BaseDir, "logs"), diff --git a/internal/entitys/publish.go b/internal/entitys/publish.go new file mode 100644 index 0000000..2391b6e --- /dev/null +++ b/internal/entitys/publish.go @@ -0,0 +1,26 @@ +package entitys + +type PublishTaskDetail struct { + // publish 表字段 + RequestID string `db:"request_id"` + PlatIndex string `db:"plat_index"` + Title string `db:"title"` + Tag string `db:"tag"` + UserIndex string `db:"user_index"` + URL string `db:"url"` + Img string `db:"img"` + PublishTime string `db:"publish_time"` + Status int `db:"status"` + Msg string `db:"msg"` + TokenID int `db:"token_id"` + + // plat 表字段 + PlatName string `db:"plat_name"` + PlatType int `db:"plat_type"` + PlatStatus int `db:"plat_status"` + PlatContentFormat string `db:"content_format"` + LoginUrl string `db:"login_url"` + EditUrl string `db:"edit_url"` + LoginedUrl string `db:"logined_url"` + Desc string `db:"desc"` +} diff --git a/internal/manager/publish_manager.go b/internal/manager/publish_manager.go index fec5959..8f9cbf1 100644 --- a/internal/manager/publish_manager.go +++ b/internal/manager/publish_manager.go @@ -1,10 +1,13 @@ package manager import ( + "context" "fmt" "geo/internal/config" + "geo/internal/entitys" "geo/internal/publisher" "geo/pkg" + "geo/utils" "io" "log" "os" @@ -12,25 +15,45 @@ import ( "strings" "sync" "time" - - "geo/utils" ) +const ( + // 任务状态常量 + StatusPending = 1 + StatusProcessing = 2 + StatusFailed = 3 + StatusSuccess = 4 + + // 批处理间隔 + BatchInterval = 30 * time.Second + + // 日志格式 + LogSeparator = "================================================================" +) + +// PublishManager 发布管理器 type PublishManager struct { - AutoStatus bool - Conf *config.Config - TokenID int - running bool - mu sync.Mutex - stopCh chan struct{} - currentPublisher interface{} - db *utils.Db + AutoStatus bool + Conf *config.Config + TokenID int + running bool + mu sync.RWMutex // 使用读写锁优化并发读取 + stopCh chan struct{} + db *utils.Db + stopOnce sync.Once // 确保stopCh只关闭一次 } -var publishManager *PublishManager -var once sync.Once +var ( + publishManager *PublishManager + once sync.Once +) + +// GetPublishManager 获取单例实例(优化:添加nil检查) +func GetPublishManager(config *config.Config, db *utils.Db) (*PublishManager, error) { + if config == nil || db == nil { + return nil, fmt.Errorf("config和db参数不能为空") + } -func GetPublishManager(config *config.Config, db *utils.Db) *PublishManager { once.Do(func() { publishManager = &PublishManager{ AutoStatus: false, @@ -39,12 +62,16 @@ func GetPublishManager(config *config.Config, db *utils.Db) *PublishManager { db: db, } }) - return publishManager + return publishManager, nil } -// getTaskLogger 获取任务专属日志记录器(同一个文件) +// getTaskLogger 获取任务专属日志记录器(优化:添加参数验证和错误恢复) func (pm *PublishManager) getTaskLogger(requestID string) (*log.Logger, *os.File, error) { - // 确定日志目录 + if requestID == "" { + return nil, nil, fmt.Errorf("requestID不能为空") + } + + // 确定日志目录,提供默认值 logsDir := pm.Conf.Sys.LogsDir if logsDir == "" { logsDir = "./logs" @@ -57,7 +84,7 @@ func (pm *PublishManager) getTaskLogger(requestID string) (*log.Logger, *os.File return nil, nil, fmt.Errorf("创建日志目录失败: %v", err) } - // 创建以requestId命名的日志文件 + // 使用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) @@ -67,8 +94,6 @@ func (pm *PublishManager) getTaskLogger(requestID string) (*log.Logger, *os.File // 创建写入器:同时写入文件和标准输出 multiWriter := io.MultiWriter(logFile, os.Stdout) - - // 创建专用的logger taskLogger := log.New(multiWriter, "", log.LstdFlags|log.Lmicroseconds) // 写入任务开始分隔线 @@ -79,22 +104,26 @@ func (pm *PublishManager) getTaskLogger(requestID string) (*log.Logger, *os.File return taskLogger, logFile, nil } +// Start 启动自动发布(优化:使用读锁检查状态,写锁修改状态) func (pm *PublishManager) Start(tokenID int) bool { pm.mu.Lock() defer pm.mu.Unlock() if pm.AutoStatus { + log.Printf("自动发布服务已在运行中,tokenID=%d", tokenID) return false } pm.TokenID = tokenID pm.AutoStatus = true - pm.stopCh = make(chan struct{}) + pm.stopCh = make(chan struct{}) // 重新创建stopCh go pm.autoPublishLoop() + log.Printf("自动发布服务已启动,tokenID=%d", tokenID) return true } +// Stop 停止自动发布(优化:使用sync.Once确保stopCh只关闭一次) func (pm *PublishManager) Stop() bool { pm.mu.Lock() defer pm.mu.Unlock() @@ -104,12 +133,15 @@ func (pm *PublishManager) Stop() bool { } pm.AutoStatus = false - close(pm.stopCh) + pm.stopOnce.Do(func() { + close(pm.stopCh) + }) return true } +// autoPublishLoop 自动发布循环(优化:添加退出日志) func (pm *PublishManager) autoPublishLoop() { - log.Println("自动发布服务已启动") + log.Println("自动发布服务已启动,开始循环执行") for { select { @@ -118,235 +150,433 @@ func (pm *PublishManager) autoPublishLoop() { return default: pm.batchPublish() - time.Sleep(30 * time.Second) + time.Sleep(BatchInterval) } } } +// batchPublish 批量发布 func (pm *PublishManager) batchPublish() { - if !pm.AutoStatus { + if !pm.isAutoStatus() { return } - publishData := pm.getPendingPublish() - if publishData == nil { + publishData, err := pm.getPendingPublish() + if err != nil { return } - // 使用 defer recover 防止 panic 导致整个循环崩溃 - defer func() { - if r := recover(); r != nil { - log.Printf("批处理发布发生 panic: %v", r) - } + // 使用context实现超时控制 + ctx, cancel := context.WithTimeout(context.Background(), time.Duration(pm.Conf.Sys.TaskTimeout)*time.Second) + defer cancel() + + done := make(chan struct{}) + go func() { + defer func() { + if r := recover(); r != nil { + log.Printf("批处理发布发生 panic: %v", r) + } + }() + pm.processSingleTask(publishData) + close(done) }() - pm.processSingleTask(publishData) + select { + case <-done: + // 正常完成 + case <-ctx.Done(): + log.Printf("任务执行超时: %v", ctx.Err()) + } } -func (pm *PublishManager) getPendingPublish() map[string]interface{} { +// getPendingPublish 获取待发布任务(返回结构体) +func (pm *PublishManager) getPendingPublish() (*entitys.PublishTaskDetail, error) { currentTime := time.Now().Format("2006-01-02 15:04:05") + // SQL查询,明确指定字段 sql := ` - SELECT p.*, pl.* + SELECT + p.request_id, + p.plat_index, + p.title, + p.tag, + p.user_index, + p.url, + p.img, + p.publish_time, + p.status, + pl.index as plat_index_value, + pl.status as plat_status + pl.login_url, + pl.edit_url, + pl.logined_url, + pl.desc FROM publish p INNER JOIN plat pl ON p.plat_index = pl.index AND pl.status = 1 - WHERE p.token_id = ? AND p.status = 1 AND p.publish_time <= ? - ORDER BY p.publish_time DESC + WHERE p.token_id = ? AND p.status = ? AND p.publish_time <= ? + ORDER BY p.publish_time ASC LIMIT 1 ` - result, err := pm.db.GetOne(sql, pm.TokenID, currentTime) + var task entitys.PublishTaskDetail + err := pm.db.GetOneToStruct(sql, &task, pm.TokenID, StatusPending, currentTime) if err != nil { log.Printf("查询待发布任务失败: token_id=%d, error=%v", pm.TokenID, err) - return nil - } - if result == nil { - log.Printf("没有待发布任务: token_id=%d, current_time=%s", pm.TokenID, currentTime) - return nil + return nil, err } - requestID := pkg.GetString(result, "request_id") - log.Printf("获取到待发布任务: token_id=%d, request_id=%s", pm.TokenID, requestID) - return result + // 检查是否为空记录(根据你的db实现,可能需要判断task.RequestID是否为空) + if task.RequestID == "" { + log.Printf("没有待发布任务: token_id=%d, current_time=%s", pm.TokenID, currentTime) + return nil, nil + } + + log.Printf("获取到待发布任务: token_id=%d, request_id=%s", pm.TokenID, task.RequestID) + return &task, nil } -func (pm *PublishManager) GetTaskByRequestID(requestID string) (map[string]interface{}, error) { +// isAutoStatus 获取自动状态(优化:使用读锁) +func (pm *PublishManager) isAutoStatus() bool { + pm.mu.RLock() + defer pm.mu.RUnlock() + return pm.AutoStatus +} + +// GetTaskByRequestID 根据RequestID获取任务(优化:添加缓存机制可选项) +func (pm *PublishManager) GetTaskByRequestID(requestID string) (*entitys.PublishTaskDetail, error) { + if requestID == "" { + return nil, fmt.Errorf("requestID不能为空") + } + sql := ` - SELECT p.*, pl.* + SELECT + p.request_id, + p.plat_index, + p.title, + p.tag, + p.user_index, + p.url, + p.img, + p.publish_time, + p.status, + pl.index as plat_index_value, + pl.status as plat_status, + pl.login_url, + pl.edit_url, + pl.logined_url, + pl.desc FROM publish p INNER JOIN plat pl ON p.plat_index COLLATE utf8mb4_unicode_ci = pl.index AND pl.status = 1 WHERE p.request_id = ? ` - return pm.db.GetOne(sql, requestID) + var task entitys.PublishTaskDetail + err := pm.db.GetOneToStruct(sql, &task, requestID) + if err != nil { + return nil, err + } + // 检查是否为空记录(根据你的db实现,可能需要判断task.RequestID是否为空) + if task.RequestID == "" { + return nil, nil + } + + return &task, nil } -func (pm *PublishManager) processSingleTask(publishData map[string]interface{}) (result map[string]interface{}) { - requestID := pkg.GetString(publishData, "request_id") +type SingleResult struct { + Success bool `json:"success"` + Message string `json:"message"` + RequestId string `json:"request_id"` +} - // 获取任务专属日志(同一个文件) - taskLogger, logFile, err := pm.getTaskLogger(requestID) +func (pm *PublishManager) processSingleTask(publishData *entitys.PublishTaskDetail) (result *SingleResult) { + if publishData == nil { + return &SingleResult{ + Success: false, + Message: "publishData不能为空", + } + } + if publishData.RequestID == "" { + + return &SingleResult{ + Success: false, + Message: "requestID不能为空", + } + } + + // 获取任务专属日志 + taskLogger, logFile, err := pm.getTaskLogger(publishData.RequestID) if err != nil { - log.Printf("[任务 %s] 创建日志文件失败: %v,使用全局日志", requestID, err) + log.Printf("[任务 %s] 创建日志文件失败: %v,使用全局日志", publishData.RequestID, err) taskLogger = log.Default() } if logFile != nil { defer logFile.Close() } - // 全局defer用于捕获panic并记录到同一个日志文件 + // 全局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("任务异常结束 | RequestID: %s | 时间: %s", publishData.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, + + result = &SingleResult{ + Success: false, + Message: errMsg, + RequestId: publishData.RequestID, } + return } }() - taskLogger.Printf("[任务 %s] 开始处理", requestID) + taskLogger.Printf("[任务 %s] 开始处理", publishData.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, "") - taskLogger.Printf("[任务 %s] 状态已更新为发布中", requestID) - - // 下载文件 - taskLogger.Printf("[任务 %s] 开始下载文档...", requestID) - docPath, err := pkg.DownloadFile(url, pm.Conf.Sys.DocsDir, requestID+".docx") - if err != nil { - errMsg := fmt.Sprintf("下载文档失败: %v", err) - taskLogger.Printf("[任务 %s] ❌ %s", requestID, errMsg) - pm.updatePublishStatus(requestID, 3, errMsg) - return map[string]interface{}{"success": false, "message": errMsg, "request_id": requestID} + // 提取任务参数 + params, sourceUrl := pm.extractTaskParams(publishData, taskLogger) + if params == nil { + return &SingleResult{ + Success: false, + Message: "提取任务参数失败", + RequestId: publishData.RequestID, + } } - defer func() { - if docPath != "" { - pkg.DeleteFile(docPath) - taskLogger.Printf("[任务 %s] 已删除文档文件: %s", requestID, docPath) - } - }() - taskLogger.Printf("[任务 %s] ✅ 文档下载成功: %s", requestID, docPath) - - // 下载图片 - taskLogger.Printf("[任务 %s] 开始下载图片...", requestID) - imgPath, err := pkg.DownloadImage(imgURL, requestID, pm.Conf.Sys.UploadDir) - - defer func() { - if imgPath != "" { - pkg.DeleteFile(imgPath) - taskLogger.Printf("[任务 %s] 已删除图片文件: %s", requestID, imgPath) - } - }() - if err != nil { - errMsg := fmt.Sprintf("下载图片失败: %v", err) - taskLogger.Printf("[任务 %s] ❌ %s", requestID, errMsg) - pm.updatePublishStatus(requestID, 3, errMsg) - return map[string]interface{}{"success": false, "message": errMsg, "request_id": requestID} - } - taskLogger.Printf("[任务 %s] ✅ 图片下载成功: %s", requestID, imgPath) - - // 解析标签 - tags := pkg.ParseTags(tagRaw) - taskLogger.Printf("[任务 %s] 标签解析完成: %v", requestID, tags) - // 获取发布器 - publisherClass := GetPublisherClass(platIndex) + publisherClass := GetPublisherClass(params.PlatIndex) if publisherClass == nil { - errMsg := fmt.Sprintf("不支持的平台: %s", platIndex) - taskLogger.Printf("[任务 %s] ❌ %s", requestID, errMsg) - pm.updatePublishStatus(requestID, 3, errMsg) - return map[string]interface{}{"success": false, "message": errMsg, "request_id": requestID} - } - - // 提取内容 - taskLogger.Printf("[任务 %s] 开始提取文档内容...", requestID) - var content string - if publisherClass.Type == 1 { - content, err = pkg.ExtractWordContent(docPath, publisherClass.ContentFormat) - if err != nil { - errMsg := fmt.Sprintf("提取文档内容失败: %v", err) - taskLogger.Printf("[任务 %s] ❌ %s", requestID, errMsg) - pm.updatePublishStatus(requestID, 3, errMsg) - return map[string]interface{}{"success": false, "message": errMsg, "request_id": requestID} + errMsg := fmt.Sprintf("不支持的平台: %s", params.PlatIndex) + taskLogger.Printf("[任务 %s] ❌ %s", publishData.RequestID, errMsg) + pm.updatePublishStatus(publishData.RequestID, StatusFailed, errMsg) + return &SingleResult{ + Success: false, + Message: errMsg, + RequestId: publishData.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) + // 更新状态为发布中 + if err := pm.updatePublishStatus(publishData.RequestID, StatusProcessing, ""); err != nil { + taskLogger.Printf("[任务 %s] ❌ 更新状态失败: %v", publishData.RequestID, err) + } + + // 下载并处理文档 + params.SourcePath, params.ImagePath, err = pm.downloadAndPrepareFiles(params.RequestID, sourceUrl, taskLogger, publisherClass) + + if err != nil { + errMsg := fmt.Sprintf("准备文件失败: %v", err) + taskLogger.Printf("[任务 %s] ❌ %s", publishData.RequestID, errMsg) + pm.updatePublishStatus(publishData.RequestID, StatusFailed, errMsg) + return &SingleResult{ + Success: false, + Message: errMsg, + RequestId: publishData.RequestID, + } + } + + // 确保文件清理 + defer pm.cleanupFiles(params.SourcePath, params.ImagePath, taskLogger, publishData.RequestID) + + // 提取内容 + params.Content, err = pm.extractContent(params.SourcePath, publisherClass, taskLogger, publishData.RequestID) + if err != nil { + errMsg := fmt.Sprintf("提取文档内容失败: %v", err) + taskLogger.Printf("[任务 %s] ❌ %s", publishData.RequestID, errMsg) + pm.updatePublishStatus(publishData.RequestID, StatusFailed, errMsg) + return &SingleResult{ + Success: false, + Message: errMsg, + RequestId: publishData.RequestID, + } + } + + // 执行发布 + taskLogger.Printf("[任务 %s] ✅ 内容提取成功,长度: %d", publishData.RequestID, len(params.Content)) + taskLogger.Printf("[任务 %s] 创建发布器...", publishData.RequestID) + pub := publisherClass.InitMethod(params, pm.Conf, taskLogger) + taskLogger.Printf("[任务 %s] 创建%s发布器", publisherClass.Name, publishData.RequestID) + taskLogger.Printf("[任务 %s] 开始执行发布...", publishData.RequestID) success, message := pub.PublishNote() + // 更新最终状态 if success { - taskLogger.Printf("[任务 %s] ✅ 发布成功: %s", requestID, message) - pm.updatePublishStatus(requestID, 4, message) + taskLogger.Printf("[任务 %s] ✅ 发布成功: %s", publishData.RequestID, message) + pm.updatePublishStatus(publishData.RequestID, StatusSuccess, message) } else { - taskLogger.Printf("[任务 %s] ❌ 发布失败: %s", requestID, message) - pm.updatePublishStatus(requestID, 3, message) + taskLogger.Printf("[任务 %s] ❌ 发布失败: %s", publishData.RequestID, message) + pm.updatePublishStatus(publishData.RequestID, StatusFailed, 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("任务结束 | RequestID: %s | 结果: %v | 时间: %s", publishData.RequestID, success, time.Now().Format("2006-01-02 15:04:05.000")) taskLogger.Printf(strings.Repeat("=", 80)) - return map[string]interface{}{ - "success": success, - "message": message, - "request_id": requestID, + return &SingleResult{ + Success: success, + Message: message, + RequestId: publishData.RequestID, } } -func (pm *PublishManager) updatePublishStatus(requestID string, status int, message string) { +type fileUrl struct { + url string + imgURL string +} + +func (pm *PublishManager) extractTaskParams(publishData *entitys.PublishTaskDetail, taskLogger *log.Logger) (*publisher.TaskParams, *fileUrl) { + + taskLogger.Printf("[任务 %s] 任务详情 - 平台:%s,标题:%s,用户:%s", publishData.RequestID, publishData.PlatIndex, publishData.Title, publishData.UserIndex) + + // 解析标签 + tags := pkg.ParseTags(publishData.Tag) + taskLogger.Printf("[任务 %s] 标签解析完成: %v", publishData.RequestID, tags) + + return &publisher.TaskParams{ + RequestID: publishData.RequestID, + PlatIndex: publishData.PlatIndex, + Title: publishData.Title, + TagRaw: publishData.Tag, + UserIndex: publishData.UserIndex, + Tags: tags, + PublishData: publishData, + }, &fileUrl{ + url: publishData.URL, + imgURL: publishData.Img, + } +} + +func (pm *PublishManager) downloadAndPrepareFiles(requestId string, params *fileUrl, taskLogger *log.Logger, publishClass *publisher.PublisherValue) (docPath, imgPath string, err error) { + // 下载文档 + taskLogger.Printf("[任务 %s] 开始下载文档...", requestId) + docPath, err = pkg.DownloadFile(params.url, pm.Conf.Sys.DocsDir, requestId+".docx") + if err != nil { + return "", "", fmt.Errorf("下载文档失败: %v", err) + } + taskLogger.Printf("[任务 %s] ✅ 文档下载成功: %s", requestId, docPath) + + // 下载图片 + taskLogger.Printf("[任务 %s] 开始下载图片...", requestId) + imgPath, err = pkg.DownloadImage(params.imgURL, requestId, pm.Conf.Sys.UploadDir) + if err != nil { + // 如果图片下载失败,清理已下载的文档 + if docPath != "" { + pkg.DeleteFile(docPath) + } + return "", "", fmt.Errorf("下载图片失败: %v", err) + } + taskLogger.Printf("[任务 %s] ✅ 图片下载成功: %s", requestId, imgPath) + + if publishClass.Type == 1 && publishClass.WordContainImg { + // 如果文档中包含图片,则将图片复制到文档目录 + if err = pkg.CopyImageToDoc(docPath, imgPath); err != nil { + return docPath, imgPath, fmt.Errorf("复制图片到文档失败: %v", err) + } + } + + return docPath, imgPath, nil +} + +// cleanupFiles 清理临时文件 +func (pm *PublishManager) cleanupFiles(docPath, imgPath string, taskLogger *log.Logger, requestID string) { + if docPath != "" { + pkg.DeleteFile(docPath) + taskLogger.Printf("[任务 %s] 已删除文档文件: %s", requestID, docPath) + } + if imgPath != "" { + pkg.DeleteFile(imgPath) + taskLogger.Printf("[任务 %s] 已删除图片文件: %s", requestID, imgPath) + } +} + +// extractContent 提取文档内容 +func (pm *PublishManager) extractContent(docPath string, publisherClass *publisher.PublisherValue, taskLogger *log.Logger, requestID string) (string, error) { + if publisherClass.Type != 1 { + return "", nil // 不需要提取内容 + } + + taskLogger.Printf("[任务 %s] 开始提取文档内容...", requestID) + content, err := pkg.ExtractWordContent(docPath, publisherClass.ContentFormat) + if err != nil { + return "", err + } + taskLogger.Printf("[任务 %s] ✅ 内容提取成功,长度: %d", requestID, len(content)) + return content, nil +} + +// updatePublishStatus 更新发布状态(优化:添加错误处理) +func (pm *PublishManager) updatePublishStatus(requestID string, status int, message string) error { + if requestID == "" { + return fmt.Errorf("requestID不能为空") + } + + var err error if message != "" { - pm.db.Execute("UPDATE publish SET status = ?, msg = ? WHERE request_id = ?", status, message, requestID) + _, err = pm.db.Execute("UPDATE publish SET status = ?, msg = ? WHERE request_id = ?", status, message, requestID) } else { - pm.db.Execute("UPDATE publish SET status = ? WHERE request_id = ?", status, requestID) + _, err = pm.db.Execute("UPDATE publish SET status = ? WHERE request_id = ?", status, requestID) } + + if err != nil { + log.Printf("更新发布状态失败: requestID=%s, status=%d, error=%v", requestID, status, err) + } + return err } -func (pm *PublishManager) ExecuteOnce(tokenId int32) map[string]interface{} { - publishData := pm.getPendingPublish() - if publishData == nil { - return map[string]interface{}{"success": false, "message": "没有待发布任务"} +// RetryTask 重试任务(优化:添加任务状态检查) +func (pm *PublishManager) RetryTask(requestID string) *SingleResult { + if requestID == "" { + return &SingleResult{ + Success: false, + Message: "requestID不能为空", + } } - return pm.processSingleTask(publishData) -} -func (pm *PublishManager) RetryTask(requestID string) map[string]interface{} { publishData, err := pm.GetTaskByRequestID(requestID) if err != nil || publishData == nil { - return map[string]interface{}{"success": false, "message": "任务不存在"} + + return &SingleResult{ + Success: false, + Message: "任务不存在", + } } + + // 只允许重试失败的任务 + // + //if publishData.Status != StatusFailed { + // return &SingleResult{ + // Success: false, + // Message: fmt.Sprintf("只能重试失败的任务,当前状态: %d", publishData.Status), + // } + //} + return pm.processSingleTask(publishData) } +// GetStatus 获取状态(优化:使用读锁) func (pm *PublishManager) GetStatus() map[string]interface{} { + pm.mu.RLock() + defer pm.mu.RUnlock() + return map[string]interface{}{ "auto_status": pm.AutoStatus, "max_concurrent": pm.Conf.Sys.MaxConcurrent, "task_timeout": pm.Conf.Sys.TaskTimeout, + "token_id": pm.TokenID, } } +// GetPublisherClass 获取发布器类(优化:添加默认值处理) func GetPublisherClass(platIndex string) *publisher.PublisherValue { + if platIndex == "" { + return nil + } - return publisher.PublisherMap[platIndex] + if publisherClass, exists := publisher.PublisherMap[platIndex]; exists { + return publisherClass + } + + log.Printf("未找到平台 %s 对应的发布器", platIndex) + return nil } diff --git a/internal/publisher/baijiahao.go b/internal/publisher/baijiahao.go index 79f8c95..901473c 100644 --- a/internal/publisher/baijiahao.go +++ b/internal/publisher/baijiahao.go @@ -3,8 +3,8 @@ package publisher import ( "fmt" "geo/internal/config" - "geo/pkg" "log" + "path/filepath" "strings" "github.com/go-rod/rod" @@ -13,25 +13,10 @@ import ( type BaijiahaoPublisher struct { *BasePublisher - Category string - ArticleType string - IsTop bool - maxRetries int - retryDelay int } -func NewBaijiahaoPublisher(headless bool, title, content string, tags []string, tenantID, platIndex, requestID, imagePath, wordPath string, platInfo map[string]interface{}, cfg *config.Config, logger *log.Logger) PublisherInerface { - base := NewBasePublisher(headless, title, content, tags, tenantID, platIndex, requestID, imagePath, wordPath, platInfo, cfg, logger) - if platInfo != nil { - base.LoginURL = pkg.GetString(platInfo, "login_url") - base.EditorURL = pkg.GetString(platInfo, "edit_url") - base.LoginedURL = pkg.GetString(platInfo, "logined_url") - } - return &BaijiahaoPublisher{ - BasePublisher: base, - maxRetries: 5, - retryDelay: 2, - } +func NewBaijiahaoPublisher(task *TaskParams, cfg *config.Config, logger *log.Logger) PublisherInerface { + return &BaijiahaoPublisher{NewBasePublisher(task, cfg, logger)} } func (p *BaijiahaoPublisher) CheckLoginStatus() bool { @@ -141,8 +126,6 @@ func (p *BaijiahaoPublisher) PublishNote() (bool, string) { func (p *BaijiahaoPublisher) doPublish() (bool, string) { p.LogInfo("开始发布百家号文章...") - p.Sleep(3) - steps := []struct { name string fn func() error @@ -214,10 +197,6 @@ func (p *BaijiahaoPublisher) inputTitle() error { 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) @@ -234,35 +213,142 @@ func (p *BaijiahaoPublisher) inputTitle() error { } 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") + p.LogInfo("开始导入文档内容...") + + // 1. 找到 id="edui41" 的 div 并 hover + edui41, err := p.WaitForElement("#edui41", 10) if err != nil { - contentEditor, err = p.Page.Element("[contenteditable='true']") + return fmt.Errorf("未找到编辑器工具栏: %v", err) } - if contentEditor == nil { - return fmt.Errorf("未找到内容编辑器") + + // 鼠标 hover + if err := edui41.Hover(); err != nil { + return fmt.Errorf("hover 失败: %v", err) } - contentEditor.Click(proto.InputMouseButtonLeft, 1) + p.LogInfo("已 hover 到编辑器工具栏") 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) + + // 2. 查找并点击"导入文档" + var importDocBtn *rod.Element + // 等待 popover 出现 + for i := 0; i < 10; i++ { + // 查找 class 包含 cheetah-popover 的元素 + popover, err := p.Page.Element("[class*='cheetah-popover']") + if err != nil || popover == nil { + p.SleepMs(500) + continue + } + + // 在 popover 内查找 class 包含 "-label" 且文本为"导入文档"的 div + // 使用正则匹配 class 包含随机字符-label 的模式 + importDocBtn, err = popover.ElementX("//div[contains(@class, '-label') and contains(text(), '导入文档')]") + if err == nil && importDocBtn != nil { + p.LogInfo("找到导入文档按钮") + break + } + + // 备用查找方式:直接在整个页面中查找 + importDocBtn, err = p.Page.ElementX("//div[contains(@class, '-label') and contains(text(), '导入文档')]") + if err == nil && importDocBtn != nil { + p.LogInfo("通过 XPath 找到导入文档按钮") + break + } + + p.SleepMs(500) } + + if importDocBtn == nil { + return fmt.Errorf("未找到导入文档按钮") + } + + // 点击导入文档按钮 + if err := p.JSClick(importDocBtn); err != nil { + return fmt.Errorf("点击导入文档按钮失败: %v", err) + } + p.LogInfo("已点击导入文档按钮") + p.SleepMs(1000) + + // 3. 查找 dialog 中的文件上传 input + var fileInput *rod.Element + for i := 0; i < 10; i++ { + // 查找 role="dialog" 的元素 + dialog, err := p.Page.Element("[role='dialog']") + if err != nil || dialog == nil { + p.SleepMs(500) + continue + } + + // 在 dialog 内查找 name="file" 的 input + fileInput, err = dialog.Element("input[name='file']") + if err == nil && fileInput != nil { + p.LogInfo("找到文件上传输入框") + break + } + + // 备用:直接在整个页面中查找 + fileInput, err = p.Page.Element("input[name='file']") + if err == nil && fileInput != nil { + p.LogInfo("通过全局选择器找到文件上传输入框") + break + } + + p.SleepMs(500) + } + + if fileInput == nil { + return fmt.Errorf("未找到文件上传输入框") + } + + // 4. 上传文档 + if p.SourcePath == "" { + return fmt.Errorf("未提供文档路径") + } + + if err := fileInput.SetFiles([]string{p.SourcePath}); err != nil { + return fmt.Errorf("上传文档失败: %v", err) + } + p.LogInfo(fmt.Sprintf("已上传文档: %s", p.SourcePath)) + + // 5. 等待导入成功 + // 提取文件名(不含路径) + fileName := filepath.Base(p.SourcePath) + + // 等待导入成功的提示 + for i := 0; i < 30; i++ { + // 查找包含文件名的成功提示 + successMsg, err := p.Page.ElementX(fmt.Sprintf("//*[contains(text(), '%s') and (contains(text(), '成功') or contains(text(), '导入'))]", fileName)) + if err == nil && successMsg != nil { + text, _ := successMsg.Text() + p.LogInfo(fmt.Sprintf("文档导入成功: %s", text)) + p.SleepMs(2000) // 等待内容加载完成 + return nil + } + + // 通用成功提示查找 + successMsg, err = p.Page.ElementX("//*[contains(text(), '导入成功')]") + if err == nil && successMsg != nil { + text, _ := successMsg.Text() + p.LogInfo(fmt.Sprintf("文档导入成功: %s", text)) + p.SleepMs(2000) + return nil + } + + // 查找是否有错误提示 + errorMsg, err := p.Page.ElementX("//*[contains(text(), '失败') or contains(text(), '错误')]") + if err == nil && errorMsg != nil { + text, _ := errorMsg.Text() + if strings.Contains(text, fileName) || strings.Contains(text, "导入") { + return fmt.Errorf("文档导入失败: %s", text) + } + } + + p.SleepMs(500) + } + + // 虽然没有明确的成功提示,但等待几秒让内容加载 + p.LogInfo("等待内容加载完成...") + p.SleepMs(3000) + return nil } @@ -272,7 +358,6 @@ func (p *BaijiahaoPublisher) uploadImage() error { return nil } p.LogInfo("设置文章封面...") - p.SleepMs(2000) // 查找并点击封面选择区域 coverSelectors := []string{ @@ -300,130 +385,135 @@ func (p *BaijiahaoPublisher) uploadImage() error { 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) - } + //// 查找并点击上传区域 + //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(), '确定')]") + for i := 0; i < p.MaxRetries; i++ { + p.LogInfo("正在查找确认按钮...") + + // 精确匹配:button 包含 cheetah-btn-primary 类,且 span 文本为"确定 (1)" + confirmBtn, _ = p.Page.ElementX("//button[contains(@class, 'cheetah-btn-primary')]//span[text()='确定 (1)']/..") + if confirmBtn != nil { visible, _ := confirmBtn.Visible() if visible { - p.LogInfo("通过文本找到确认按钮") + p.LogInfo("找到确认按钮") break } } - confirmBtn, _ = p.Page.Element(".cheetah-btn-primary") + + // 备选:只匹配 span 文本 + confirmBtn, _ = p.Page.ElementX("//span[text()='确定 (1)']/..") 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() + visible, _ := confirmBtn.Visible() if visible { - text, _ := btn.Text() - if strings.Contains(text, "确定") || strings.Contains(text, "确认") { - confirmBtn = btn - p.LogInfo(fmt.Sprintf("通过遍历按钮找到确认按钮: %s", text)) - break - } + p.LogInfo("通过 span 文本找到确认按钮") + break } } + + // 备选:文本包含"确定"和数字 + confirmBtn, _ = p.Page.ElementX("//button[contains(@class, 'cheetah-btn-primary') and contains(., '确定')]") if confirmBtn != nil { - break + visible, _ := confirmBtn.Visible() + if visible { + p.LogInfo("通过文本内容找到确认按钮") + break + } } - p.Sleep(1) + + p.SleepMs(p.RetryDelay) } + if confirmBtn != nil { - p.ScrollToElement(confirmBtn) - p.SleepMs(500) p.JSClick(confirmBtn) p.LogInfo("已点击确认按钮") p.SleepMs(2000) + } else { + return fmt.Errorf("未找到确认按钮") } return nil } diff --git a/internal/publisher/base.go b/internal/publisher/base.go index 5d55dbc..9c3bbd9 100644 --- a/internal/publisher/base.go +++ b/internal/publisher/base.go @@ -3,9 +3,11 @@ package publisher import ( "encoding/json" "fmt" + "geo/internal/entitys" "log" "os" "path/filepath" + "strings" "time" "geo/internal/config" @@ -16,15 +18,15 @@ import ( ) type BasePublisher struct { - Headless bool - Title string - Content string - Tags []string - TenantID string - PlatIndex string - RequestID string - ImagePath string - WordPath string + Headless bool + Title string + Content string + Tags []string + UserIndex string + PlatIndex string + RequestID string + ImagePath string + SourcePath string Browser *rod.Browser Page *rod.Page @@ -37,15 +39,33 @@ type BasePublisher struct { LoginedURL string CookiesFile string - PlatInfo map[string]interface{} + PlatInfo *entitys.PublishTaskDetail config *config.Config + + MaxRetries int + RetryDelay int +} + +// taskParams 任务参数结构体 +type TaskParams struct { + Headless bool + Title string + TagRaw string + UserIndex string + PlatIndex string + RequestID string + ImagePath string + SourcePath string + Content string + Tags []string + PublishData *entitys.PublishTaskDetail } // 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) +func NewBasePublisher(task *TaskParams, config *config.Config, logger *log.Logger) *BasePublisher { + cookiesDir := filepath.Join(config.Sys.CookiesDir, task.UserIndex) os.MkdirAll(cookiesDir, 0755) - cookiesFile := filepath.Join(cookiesDir, platIndex+".json") + cookiesFile := filepath.Join(cookiesDir, task.PlatIndex+".json") var baseLogger *log.Logger var logFile *os.File @@ -61,34 +81,49 @@ func NewBasePublisher(headless bool, title, content string, tags []string, tenan logsDir = "./logs" } os.MkdirAll(logsDir, 0755) - logFile, _ = os.Create(filepath.Join(logsDir, requestID+".log")) + logFile, _ = os.Create(filepath.Join(logsDir, task.RequestID+".log")) baseLogger = log.New(logFile, "", log.LstdFlags) } return &BasePublisher{ - Headless: headless, - Title: title, - Content: content, - Tags: tags, - TenantID: tenantID, - PlatIndex: platIndex, - RequestID: requestID, - ImagePath: imagePath, - WordPath: wordPath, + Headless: task.Headless, + Title: task.Title, + Content: task.Content, + Tags: task.Tags, + UserIndex: task.UserIndex, + PlatIndex: task.PlatIndex, + RequestID: task.RequestID, + ImagePath: task.ImagePath, + SourcePath: task.SourcePath, Logger: baseLogger, LogFile: logFile, CookiesFile: cookiesFile, - PlatInfo: platInfo, + PlatInfo: task.PublishData, config: config, + LoginURL: task.PublishData.LoginUrl, + EditorURL: task.PublishData.EditUrl, + LoginedURL: task.PublishData.LoginedUrl, + MaxRetries: 3, + RetryDelay: 200, } } func (b *BasePublisher) SetupDriver() error { + headless := false l := launcher.New() - l.Headless(b.Headless) - + l.Headless(headless) + if headless { + // 无头模式专用参数 + l.Set("headless", "new") // 使用新版无头模式 + l.Set("disable-gpu") + l.Set("disable-software-rasterizer") + l.Set("disable-dev-shm-usage") + l.Set("no-sandbox") + // 禁用虚拟滚动,避免等待 + l.Set("disable-scroll-to-text-fragment") + } // 设置用户数据目录 - userDataDir := filepath.Join(b.config.Sys.ChromeDataDir, b.TenantID) + userDataDir := filepath.Join(b.config.Sys.ChromeDataDir, b.UserIndex) os.MkdirAll(userDataDir, 0755) l.UserDataDir(userDataDir) @@ -104,6 +139,12 @@ func (b *BasePublisher) SetupDriver() error { l.Set("disable-setuid-sandbox") l.Set("remote-debugging-port", "9222") + // 关键:禁用后台限制,让页面在后台也能正常执行 + l.Set("disable-background-timer-throttling") + l.Set("disable-backgrounding-occluded-windows") + l.Set("disable-renderer-backgrounding") + l.Set("disable-ipc-flooding-protection") + // 窗口大小 l.Set("window-size", "1920,1080") l.Set("lang", "zh-CN") @@ -117,9 +158,6 @@ func (b *BasePublisher) SetupDriver() error { b.Browser = rod.New().ControlURL(url).MustConnect() b.Page = b.Browser.MustPage() - // 删除这行!!!! - // b.Page.MustSetViewport(1920, 1080, 1, false) - return nil } @@ -200,23 +238,15 @@ func (b *BasePublisher) WaitForElementClickable(selector string, timeout int) (* func (b *BasePublisher) JSClick(element *rod.Element) error { if element == nil { + b.Logger.Printf("element is nil") return fmt.Errorf("element is nil") } - + err := element.Click(proto.InputMouseButtonLeft, 1) // 方法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 + if err != nil { + b.Logger.Printf("click fail:" + err.Error()) + } + return err } func (b *BasePublisher) ScrollToElement(element *rod.Element) error { @@ -314,3 +344,13 @@ func (b *BasePublisher) ClearInput(element *rod.Element) error { func (b *BasePublisher) SleepMs(milliseconds int) { time.Sleep(time.Duration(milliseconds) * time.Millisecond) } + +// StartNote 开始任务日志 + +func (b *BasePublisher) StartNote() { + b.LogInfo(strings.Repeat("=", 50)) + b.LogInfo(fmt.Sprintf("开始执行:%s", b.PlatIndex)) + b.LogInfo(fmt.Sprintf("标题: %s", b.Title)) + b.LogInfo(fmt.Sprintf("标签: %v", b.Tags)) + b.LogInfo(strings.Repeat("=", 50)) +} diff --git a/internal/publisher/interface.go b/internal/publisher/interface.go index f40f14c..d33fcaf 100644 --- a/internal/publisher/interface.go +++ b/internal/publisher/interface.go @@ -11,40 +11,42 @@ type PublisherInerface interface { } 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{}, + param *TaskParams, 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视频 + Name string + InitMethod NewPublisher + ContentFormat string //需要的文章格式,html,text,markdown + ImgNeed int8 //是否需要图片,1需要,2非必须,3不要 + Type int8 //类型:1文章2视频 + WordContainImg bool //文章带上头图 } var PublisherMap = map[string]*PublisherValue{ "xhs": { - Name: "小红书", - InitMethod: NewXiaohongshuPublisher, - ContentFormat: "text", - ImgNeed: 3, - Type: 1, + Name: "小红书", + InitMethod: NewXiaohongshuPublisher, + ContentFormat: "text", + ImgNeed: 3, + Type: 1, + WordContainImg: false, }, "bjh": { - Name: "百家号", - InitMethod: NewBaijiahaoPublisher, - ContentFormat: "text", - ImgNeed: 1, - Type: 1, + Name: "百家号", + InitMethod: NewBaijiahaoPublisher, + ContentFormat: "text", + ImgNeed: 1, + Type: 1, + WordContainImg: true, + }, + "toutiao": { + Name: "今日头条", + InitMethod: NewToutiaoPublisher, + ContentFormat: "text", + ImgNeed: 1, + Type: 1, + WordContainImg: true, }, } diff --git a/internal/publisher/toutiao.go b/internal/publisher/toutiao.go new file mode 100644 index 0000000..1c1f79d --- /dev/null +++ b/internal/publisher/toutiao.go @@ -0,0 +1,580 @@ +package publisher + +import ( + "fmt" + "geo/internal/config" + "log" + "strings" + "time" + + "github.com/go-rod/rod" + "github.com/go-rod/rod/lib/proto" +) + +type ToutiaoPublisher struct { + *BasePublisher +} + +// NewToutiaoPublisher 构造函数 +func NewToutiaoPublisher(task *TaskParams, cfg *config.Config, logger *log.Logger) PublisherInerface { + return &ToutiaoPublisher{NewBasePublisher(task, cfg, logger)} +} + +func (p *ToutiaoPublisher) CheckLogin() (bool, string) { + p.LogInfo("检查登录状态...") + + if err := p.SetupDriver(); err != nil { + return false, fmt.Sprintf("浏览器启动失败: %v", err) + } + defer p.Close() + + p.Page.MustNavigate(p.EditorURL) + p.Sleep(2) + p.WaitForPageReady(5) + + if p.CheckLoginStatus() { + p.SaveCookies() + return true, "已登录" + } + return false, "未登录" +} + +func (p *ToutiaoPublisher) CheckLoginStatus() bool { + currentURL := p.GetCurrentURL() + // 如果在登录页面,未登录 + if strings.Contains(currentURL, p.LoginURL) { + return false + } + return true +} + +func (p *ToutiaoPublisher) WaitLogin() (bool, string) { + p.LogInfo("开始等待登录...") + + if err := p.SetupDriver(); err != nil { + return false, fmt.Sprintf("浏览器启动失败: %v", err) + } + defer p.Close() + + // 先尝试访问已登录页面 + p.Page.MustNavigate(p.LoginedURL) + p.Sleep(3) + + if p.CheckLoginStatus() { + p.SaveCookies() + p.LogInfo("已有登录状态") + return true, "already_logged_in" + } + + // 未登录,跳转到登录页 + p.Page.MustNavigate(p.LoginURL) + p.WaitForPageReady(5) + p.LogInfo("请扫码登录...") + + // 等待登录完成,最多120秒 + for i := 0; i < 120; i++ { + currentURL := p.GetCurrentURL() + if strings.Contains(currentURL, p.LoginedURL) { + p.SaveCookies() + p.LogInfo("登录成功") + return true, "login_success" + } + time.Sleep(1 * time.Second) + } + + return false, "登录超时,请检查网络或账号状态" +} + +// closeCloseBtn 关闭页面上的关闭按钮(class="close-btn"的svg) +func (p *ToutiaoPublisher) closeCloseBtn() { + p.LogInfo("检查并关闭页面上的关闭按钮...") + + // 查找所有 class="close-btn" 的元素 + closeBtns, err := p.Page.Elements(".close-btn") + if err != nil { + p.LogInfo("查找关闭按钮失败或无关闭按钮") + return + } + + if len(closeBtns) == 0 { + p.LogInfo("未找到关闭按钮") + return + } + + p.LogInfo(fmt.Sprintf("找到 %d 个关闭按钮,尝试点击...", len(closeBtns))) + + for _, btn := range closeBtns { + if btn == nil { + continue + } + + // 检查元素是否可见 + visible, err := btn.Visible() + if err != nil { + continue + } + + if visible { + p.LogInfo("点击关闭按钮...") + // 使用 JavaScript 强制点击 + if err := p.JSClick(btn); err != nil { + p.LogInfo(fmt.Sprintf("点击关闭按钮失败: %v", err)) + } else { + p.LogInfo("成功点击关闭按钮") + p.SleepMs(500) // 等待弹窗关闭动画 + } + } + } +} + +// inputTitle 输入标题 +func (p *ToutiaoPublisher) inputTitle() error { + p.LogInfo("输入文章标题...") + + // 尝试多种选择器查找标题输入框 + titleSelectors := []string{ + ".publish-editor-title textarea", + "#txtTitle", + ".title-input textarea", + "textarea[placeholder*='标题']", + } + + 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(300) + + // 清空输入框 + if err := p.ClearInput(titleInput); err != nil { + titleInput.Input("") + } + p.SleepMs(300) + + // 输入标题 + if err := p.SetInputValue(titleInput, p.Title); err != nil { + titleInput.Input(p.Title) + } + + p.LogInfo(fmt.Sprintf("标题已输入: %s", p.Title)) + p.SleepMs(500) + + return nil +} + +// inputContent 通过导入文件输入内容 +func (p *ToutiaoPublisher) inputContent() error { + p.LogInfo("开始导入文章内容...") + // 查找所有 class="close-btn" 的元素 + p.closeCloseBtn() + p.SleepMs(500) + // 1. 找到并点击导入按钮(class="syl-toolbar-button") + p.LogInfo("查找导入按钮...") + p.LogInfo("查找导入按钮...") + + // 先找 class 包含 doc-import 的 div + docImportDiv, err := p.WaitForElementVisible("[class*='doc-import']", 10) + if err != nil { + return fmt.Errorf("未找到包含doc-import的元素: %v", err) + } + + p.LogInfo("找到包含doc-import的元素,开始查找其中的button...") + + // 在该 div 下查找 button + importBtn, err := docImportDiv.Element("button") + if err != nil { + // 尝试查找 button 的多种可能选择器 + importBtn, err = docImportDiv.Element("button:first-child") + if err != nil { + importBtn, err = docImportDiv.Element(".syl-toolbar-button") + if err != nil { + return fmt.Errorf("在doc-import元素下未找到按钮: %v", err) + } + } + } + + if importBtn == nil { + return fmt.Errorf("未找到导入按钮") + } + + p.LogInfo("找到导入按钮,准备点击...") + + // 点击导入按钮 + if err := p.JSClick(importBtn); err != nil { + return fmt.Errorf("点击导入按钮失败: %v", err) + } + p.LogInfo("已点击导入按钮") + p.SleepMs(1000) + + // 2. 找到文件上传输入框并上传文件 + p.LogInfo("查找文件上传输入框...") + + // 文件上传输入框的选择器 + fileInputSelectors := []string{ + "input[type='file'][accept*='.doc']", + "input[type='file'][accept*='application']", + "input[type='file']", + } + + var fileInput *rod.Element + for _, selector := range fileInputSelectors { + fileInput, err = p.Page.Element(selector) + if err == nil && fileInput != nil { + p.LogInfo(fmt.Sprintf("找到文件上传输入框: %s", selector)) + break + } + } + + if fileInput == nil { + return fmt.Errorf("未找到文件上传输入框") + } + + // 检查是否有可用的文件路径 + if p.SourcePath == "" && p.ImagePath == "" { + return fmt.Errorf("未提供要导入的文件路径") + } + + // 优先使用 SourcePath(Word文档路径),其次使用 ImagePath + filePath := p.SourcePath + if filePath == "" { + filePath = p.ImagePath + } + + p.LogInfo(fmt.Sprintf("开始上传文件: %s", filePath)) + + // 上传文件 + if err := fileInput.SetFiles([]string{filePath}); err != nil { + return fmt.Errorf("上传文件失败: %v", err) + } + p.LogInfo("文件已上传,等待导入处理...") + + // 3. 等待导入成功的弹窗出现 + p.LogInfo("等待导入成功弹窗...") + + // 等待导入成功的提示出现 + successSelectors := []string{ + ".byte-message-success", + ".toast-success", + "[class*='success']", + "[class*='导入成功']", + ".syl-toast-success", + } + + var successMsg *rod.Element + for attempt := 0; attempt < 30; attempt++ { + for _, selector := range successSelectors { + successMsg, err = p.Page.Element(selector) + if err == nil && successMsg != nil { + text, _ := successMsg.Text() + if strings.Contains(text, "成功") || strings.Contains(text, "导入") { + p.LogInfo(fmt.Sprintf("检测到导入成功提示: %s", text)) + break + } + } + } + if successMsg != nil { + break + } + p.SleepMs(500) + } + + // 等待弹窗消失(导入完成的标志) + p.LogInfo("等待导入完成,弹窗消失...") + time.Sleep(2 * time.Second) + // 等待内容加载完成 + p.LogInfo("等待内容加载到编辑器...") + time.Sleep(3 * time.Second) + + // 验证内容是否已导入 + contentEditor, err := p.WaitForElementVisible(".ProseMirror", 10) + if err == nil { + text, _ := contentEditor.Text() + if len(text) > 0 { + p.LogInfo(fmt.Sprintf("内容导入成功,内容长度: %d", len(text))) + } else { + p.LogWarning("内容可能未正确导入,编辑器为空") + } + } else { + p.LogWarning("未找到内容编辑器,无法验证导入结果") + } + + p.LogInfo("文章内容导入完成") + return nil +} + +// SetContentEditableHTML 设置 contenteditable 元素的 HTML 内容 +func (p *ToutiaoPublisher) SetContentEditableHTML(element *rod.Element, html string) error { + _, err := element.Evaluate(&rod.EvalOptions{ + JS: `(el, val) => { el.innerHTML = val; el.dispatchEvent(new Event('input', {bubbles: true})); }`, + JSArgs: []interface{}{html}, + }) + return err +} + +// findFileInput 查找文件上传输入框 +func (p *ToutiaoPublisher) findFileInput() (*rod.Element, error) { + selectors := []string{ + "input[type='file']", + ".byte-drawer-inner input[type='file']", + "input[accept*='image']", + ".upload-input input[type='file']", + "button input[type='file']", + } + + for _, selector := range selectors { + el, err := p.Page.Element(selector) + if err == nil && el != nil { + p.LogInfo(fmt.Sprintf("找到文件上传输入框: %s", selector)) + return el, nil + } + } + + return nil, fmt.Errorf("未找到文件上传输入框") +} + +// clickConfirmButton 点击确认按钮(带重试机制) +func (p *ToutiaoPublisher) clickConfirmButton() error { + for attempt := 1; attempt <= 10; attempt++ { + p.LogInfo(fmt.Sprintf("第 %d 次尝试点击确认按钮", attempt)) + + // 查找确认按钮 + confirmBtn, err := p.Page.Element("button[data-e2e='imageUploadConfirm-btn']") + if err == nil && confirmBtn != nil { + // 检查按钮是否可用 + if err := p.JSClick(confirmBtn); err != nil { + p.LogInfo(fmt.Sprintf("点击确认按钮失败: %v", err)) + } else { + p.LogInfo("已点击确认按钮") + p.SleepMs(1000) + + // 检查弹窗是否已关闭 + if p.isDrawerClosed() { + p.LogInfo("弹窗已成功关闭,封面设置完成") + return nil + } + } + } else { + p.LogInfo(fmt.Sprintf("第 %d 次尝试:未找到确认按钮", attempt)) + } + + if attempt < 10 { + time.Sleep(2 * time.Second) + } + } + + // 最终检查弹窗状态 + if p.isDrawerClosed() { + p.LogInfo("弹窗已关闭,封面设置成功") + return nil + } + + return fmt.Errorf("确认按钮点击后弹窗未关闭") +} + +// isDrawerClosed 检查弹窗是否已关闭 +func (p *ToutiaoPublisher) isDrawerClosed() bool { + drawerWrappers, err := p.Page.Elements(".byte-drawer-wrapper") + if err != nil { + // 没找到弹窗元素,可能已关闭 + return true + } + + for _, wrapper := range drawerWrappers { + className, err := wrapper.Attribute("class") + if err == nil && className != nil && strings.Contains(*className, "byte-drawer-wrapper-hide") { + return true + } + } + + return false +} + +// clickPublish 点击发布按钮 +func (p *ToutiaoPublisher) clickPublish() error { + p.LogInfo("点击发布按钮...") + + // 查找发布按钮 + publishBtn, err := p.WaitForElementClickable(".publish-btn-last, .publish-footer .byte-btn-primary", 5) + if err != nil { + // 尝试其他选择器 + publishBtn, err = p.Page.Element("button:contains('预览并发布')") + 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 := p.JSClick(publishBtn); err != nil { + return fmt.Errorf("点击发布按钮失败: %v", err) + } + p.LogInfo("已点击第一次发布按钮") + p.SleepMs(2000) + + // 第二次点击确认发布 + p.LogInfo("查找第二次确认发布按钮...") + secondPublishBtn, err := p.WaitForElementClickable(".publish-btn.publish-btn-last", 5) + if err != nil { + secondPublishBtn, err = p.Page.Element("button:contains('预览并发布')") + if err != nil { + p.LogInfo("未找到第二次发布确认按钮,可能已经发布") + return nil + } + } + + if secondPublishBtn != nil { + if err := p.ScrollToElement(secondPublishBtn); err != nil { + p.LogInfo(fmt.Sprintf("滚动到确认按钮失败: %v", err)) + } + p.SleepMs(500) + if err := p.JSClick(secondPublishBtn); err != nil { + p.LogInfo(fmt.Sprintf("点击第二次发布确认按钮失败: %v", err)) + } else { + p.LogInfo("已点击第二次发布确认按钮") + p.SleepMs(3000) + } + } + + return nil +} + +// waitForPublishResult 等待发布结果 +func (p *ToutiaoPublisher) 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, "success") || !strings.Contains(currentURL, "publish") { + p.LogInfo(fmt.Sprintf("发布成功!URL: %s", currentURL)) + return true, "发布成功" + } + + // 检查错误提示 + errorSelectors := []string{ + "[class*='error']", + "[class*='toast-error']", + ".byte-message-error", + } + for _, selector := range errorSelectors { + errorMsgs, err := p.Page.Elements(selector) + if err == nil { + for _, elem := range errorMsgs { + text, _ := elem.Text() + if text != "" && (strings.Contains(text, "失败") || strings.Contains(strings.ToLower(text), "error")) { + p.LogError(fmt.Sprintf("发布失败: %s", text)) + return false, fmt.Sprintf("发布失败: %s", text) + } + } + } + } + + // 检查成功提示 + successSelectors := []string{ + "[class*='success']", + ".byte-message-success", + } + for _, selector := range successSelectors { + successMsgs, err := p.Page.Elements(selector) + if err == nil { + for _, elem := range successMsgs { + text, _ := elem.Text() + if text != "" && strings.Contains(text, "成功") { + p.LogInfo(fmt.Sprintf("发布成功: %s", text)) + return true, text + } + } + } + } + + p.SleepMs(1000) + } + + p.LogWarning("发布结果未知") + return false, "发布结果未知" +} + +// InitPage 初始化页面 +func (p *ToutiaoPublisher) InitPage() error { + // 访问发布页面 + p.Page.MustNavigate(p.EditorURL) + p.Sleep(2) + p.WaitForPageReady(5) + + // 尝试加载cookies并检查登录状态 + if err := p.LoadCookies(); err == nil { + p.RefreshPage() + p.Sleep(2) + if p.CheckLoginStatus() { + p.SaveCookies() + return nil + } + } + + return fmt.Errorf("需要登录") +} + +// PublishNote 发布文章 +func (p *ToutiaoPublisher) PublishNote() (bool, string) { + p.StartNote() + + // 初始化浏览器 + if err := p.SetupDriver(); err != nil { + return false, fmt.Sprintf("浏览器启动失败: %v", err) + } + defer p.Close() + + // 执行发布流程 + steps := []struct { + name string + fn func() error + }{ + {"初始化页面", p.InitPage}, + {"输入内容", p.inputContent}, + //{"上传封面", p.uploadCover}, + {"输入标题", p.inputTitle}, + {"点击发布", p.clickPublish}, + } + + for _, step := range steps { + if err := step.fn(); err != nil { + // 失败时截图 + screenshotFile := fmt.Sprintf("screenshot_%s_%d.png", step.name, time.Now().Unix()) + p.Screenshot(screenshotFile) + p.LogStep(step.name, false, err.Error()) + return false, fmt.Sprintf("%s失败: %v", step.name, err) + } + p.LogStep(step.name, true, "") + p.SleepMs(500) + } + + // 等待发布结果 + return p.waitForPublishResult() +} + +// LogWarning 记录警告日志 +func (p *ToutiaoPublisher) LogWarning(message string) { + p.Logger.Printf("⚠️ %s", message) +} diff --git a/internal/publisher/xiaohongshu.go b/internal/publisher/xhs.go similarity index 85% rename from internal/publisher/xiaohongshu.go rename to internal/publisher/xhs.go index 9ad7e32..34d9544 100644 --- a/internal/publisher/xiaohongshu.go +++ b/internal/publisher/xhs.go @@ -2,33 +2,22 @@ package publisher import ( "fmt" - "geo/pkg" + "geo/internal/config" "log" "strings" "time" - "geo/internal/config" - "github.com/go-rod/rod" "github.com/go-rod/rod/lib/proto" ) type XiaohongshuPublisher struct { *BasePublisher - maxRetries int - retryDelay int } // NewXiaohongshuPublisher 构造函数,增加 logger 参数 -func NewXiaohongshuPublisher(headless bool, title, content string, tags []string, tenantID, platIndex, requestID, imagePath, wordPath string, platInfo map[string]interface{}, cfg *config.Config, logger *log.Logger) PublisherInerface { - base := NewBasePublisher(headless, title, content, tags, tenantID, platIndex, requestID, imagePath, wordPath, platInfo, cfg, logger) - if platInfo != nil { - base.LoginURL = pkg.GetString(platInfo, "login_url") - base.EditorURL = pkg.GetString(platInfo, "edit_url") - base.LoginedURL = pkg.GetString(platInfo, "logined_url") - } - - return &XiaohongshuPublisher{BasePublisher: base, maxRetries: 5, retryDelay: 200} +func NewXiaohongshuPublisher(task *TaskParams, cfg *config.Config, logger *log.Logger) PublisherInerface { + return &XiaohongshuPublisher{NewBasePublisher(task, cfg, logger)} } func (p *XiaohongshuPublisher) CheckLogin() (bool, string) { @@ -254,7 +243,7 @@ func (p *XiaohongshuPublisher) clickPublish() error { p.SleepMs(1000) // 查找并点击 next-btn(对应Python中的第一步) - for attempt := 0; attempt < p.maxRetries; attempt++ { + for attempt := 0; attempt < p.MaxRetries; attempt++ { nextBtns, err := p.Page.Elements("button[class*='next-btn']") if err != nil || len(nextBtns) == 0 { nextBtns, err = p.Page.Elements(".next-btn") @@ -269,12 +258,12 @@ func (p *XiaohongshuPublisher) clickPublish() error { } } - p.SleepMs(p.retryDelay) + p.SleepMs(p.RetryDelay) } // 进入发布设置页面,点击submit按钮 p.LogInfo("进入发布设置页面...") - for attempt := 0; attempt < p.maxRetries; attempt++ { + 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) @@ -290,7 +279,7 @@ func (p *XiaohongshuPublisher) clickPublish() error { } } p.LogInfo(fmt.Sprintf("未找到submit按钮,第%d次重试...", attempt+1)) - p.SleepMs(p.retryDelay) + p.SleepMs(p.RetryDelay) } // 输入话题标签 @@ -318,7 +307,7 @@ func (p *XiaohongshuPublisher) clickPublish() error { // 最终发布 p.LogInfo("最终发布...") - for attempt := 0; attempt < p.maxRetries; attempt++ { + 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 { @@ -333,7 +322,7 @@ func (p *XiaohongshuPublisher) clickPublish() error { } } } - p.SleepMs(p.retryDelay) + p.SleepMs(p.RetryDelay) } return nil @@ -392,13 +381,47 @@ func (p *XiaohongshuPublisher) CheckLoginStatus() bool { return true } +func (p *XiaohongshuPublisher) ClickUploadBotton() error { + // 执行发布流程之前,先点击上传区域的第一个按钮 + uploadDiv, err := p.WaitForElement(".upload-content", 10) + if err != nil { + return fmt.Errorf("未找到上传区域: %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 { + return fmt.Errorf("JS点击按钮失败: %v", err) + + } else { + p.LogInfo("已点击上传按钮") + p.Sleep(1) + } + } + } + return nil +} + +func (p *XiaohongshuPublisher) InitPage() error { + // 访问发布页面 + p.Page.MustNavigate(p.EditorURL) + p.WaitForPageReady(5) + // 尝试加载cookies并检查登录状态 + if err := p.LoadCookies(); err == nil { + p.RefreshPage() + p.Sleep(2) + } + // 统一检查登录状态 + if !p.CheckLoginStatus() { + p.LogInfo("未登录或登录已过期,需要重新登录") + return fmt.Errorf("需要登录") + } + return nil +} + func (p *XiaohongshuPublisher) PublishNote() (bool, string) { - p.LogInfo(strings.Repeat("=", 50)) - p.LogInfo("开始发布小红书笔记...") - p.LogInfo(fmt.Sprintf("标题: %s", p.Title)) - p.LogInfo(fmt.Sprintf("内容长度: %d", len(p.Content))) - p.LogInfo(fmt.Sprintf("标签: %v", p.Tags)) - p.LogInfo(strings.Repeat("=", 50)) + p.StartNote() // 初始化浏览器 if err := p.SetupDriver(); err != nil { @@ -406,62 +429,17 @@ func (p *XiaohongshuPublisher) PublishNote() (bool, string) { } defer p.Close() - // 访问发布页面 - p.Page.MustNavigate(p.LoginedURL) - - p.WaitForPageReady(5) - - // 尝试加载cookies - if err := p.LoadCookies(); err == nil { - p.RefreshPage() - p.Sleep(2) - if p.CheckLoginStatus() { - p.LogInfo("使用cookies登录成功") - } else { - p.LogInfo("cookies已过期,需要重新登录") - return false, "需要登录" - } - } - - // 检查登录状态 - if !p.CheckLoginStatus() { - return false, "需要登录" - } - - // 保存cookies - p.SaveCookies() - - // 访问发布页面 - p.Page.MustNavigate(p.EditorURL) - - p.WaitForPageReady(5) - - // 执行发布流程之前,先点击上传区域的第一个按钮 - p.LogInfo("点击上传按钮...") - uploadDiv, err := p.WaitForElement(".upload-content", 10) - if err != nil { - p.LogInfo(fmt.Sprintf("未找到上传区域: %v", err)) - } else { - buttons, err := uploadDiv.Elements("button") - if err != nil { - p.LogInfo(fmt.Sprintf("查找按钮失败: %v", err)) - } else if len(buttons) > 0 { - if err := p.JSClick(buttons[0]); err != nil { - p.LogInfo(fmt.Sprintf("JS点击按钮失败: %v", err)) - } else { - p.LogInfo("已点击上传按钮") - p.Sleep(1) - } - } - } - // 执行发布流程 steps := []struct { name string fn func() error }{ + {"初始化", p.InitPage}, + {"保存cookie", p.SaveCookies}, + {"点击上传按钮", p.ClickUploadBotton}, {"输入内容", p.inputContent}, {"输入标题", p.inputTitle}, + {"点击发布", p.clickPublish}, } for _, step := range steps { @@ -472,12 +450,6 @@ func (p *XiaohongshuPublisher) PublishNote() (bool, string) { p.LogStep(step.name, true, "") p.SleepMs(500) } - - // 点击发布 - if err := p.clickPublish(); err != nil { - return false, err.Error() - } - // 等待发布结果 return p.waitForPublishResult() } diff --git a/internal/server/router/app.go b/internal/server/router/app.go index 304b66b..03ab349 100644 --- a/internal/server/router/app.go +++ b/internal/server/router/app.go @@ -39,7 +39,7 @@ func (m *AppModule) Register(router fiber.Router) { router.Post("/publish_on", vali(m.publishService.PublishOn, &entitys.PublishOnRequest{})) router.Post("/publish_off", vali(m.publishService.PublishOff, &entitys.PublishOffRequest{})) router.Post("/publish_status", vali(m.publishService.PublishStatus, &entitys.PublishStatusRequest{})) - router.Post("/publish_execute_once", vali(m.publishService.PublishExecuteOnce, &entitys.PublishExecuteOnceRequest{})) + //router.Post("/publish_execute_once", vali(m.publishService.PublishExecuteOnce, &entitys.PublishExecuteOnceRequest{})) router.Post("/publish_execute_retry", vali(m.publishService.PublishExecuteRetry, &entitys.PublishExecuteRetryRequest{})) router.Post("/get_publish_list", vali(m.publishService.GetPublishList, &entitys.GetPublishListRequest{})) router.Post("/login_platform", vali(m.loginService.LoginPlatform, &entitys.LoginPlatformRequest{})) diff --git a/internal/service/app.go b/internal/service/app.go index 860d60c..bf0998a 100644 --- a/internal/service/app.go +++ b/internal/service/app.go @@ -82,7 +82,10 @@ func (a *AppService) GetUserAndAutoStatus(c *fiber.Ctx, req *entitys.GetUserAndA return err } - pm := manager.GetPublishManager(a.cfg, a.tokenImpl.GetDb()) + pm, err := manager.GetPublishManager(a.cfg, a.tokenImpl.GetDb()) + if err != nil { + return err + } return pkg.HandleResponse(c, fiber.Map{ "user": users, "auto_status": pm.AutoStatus, diff --git a/internal/service/login.go b/internal/service/login.go index 60a1c4d..786234a 100644 --- a/internal/service/login.go +++ b/internal/service/login.go @@ -2,6 +2,7 @@ package service import ( "geo/internal/manager" + "geo/internal/publisher" "os" "path/filepath" @@ -43,14 +44,21 @@ func (s *LoginService) LoginPlatform(c *fiber.Ctx, req *entitys.LoginPlatformReq } // 创建发布器 - platMap := map[string]interface{}{ - "login_url": platInfo.LoginURL, - "edit_url": platInfo.EditURL, - "logined_url": platInfo.LoginedURL, - } publisherClass := manager.GetPublisherClass(req.PlatIndex) - pub := publisherClass.InitMethod(false, "", "", nil, req.UserIndex, req.PlatIndex, "", "", "", platMap, s.cfg, nil) + if publisherClass == nil { + return errcode.NotFound("平台不存在") + } + task := &publisher.TaskParams{ + PlatIndex: req.PlatIndex, + UserIndex: req.UserIndex, + PublishData: &entitys.PublishTaskDetail{ + LoginedUrl: platInfo.LoginedURL, + EditUrl: platInfo.EditURL, + LoginUrl: platInfo.LoginURL, + }, + } + pub := publisherClass.InitMethod(task, s.cfg, nil) success, msg := pub.WaitLogin() if !success { return errcode.SysErr(msg) diff --git a/internal/service/publish.go b/internal/service/publish.go index d9508ed..a70f8a3 100644 --- a/internal/service/publish.go +++ b/internal/service/publish.go @@ -84,7 +84,10 @@ func (s *PublishService) PublishOn(c *fiber.Ctx, req *entitys.PublishOnRequest) return err } - pm := manager.GetPublishManager(s.cfg, s.db) + pm, err := manager.GetPublishManager(s.cfg, s.db) + if err != nil { + return err + } if pm.Start(int(tokenInfo.ID)) { return pkg.HandleResponse(c, fiber.Map{ "auto_status": pm.AutoStatus, @@ -99,7 +102,10 @@ func (s *PublishService) PublishOff(c *fiber.Ctx, req *entitys.PublishOffRequest return err } - pm := manager.GetPublishManager(s.cfg, s.db) + pm, err := manager.GetPublishManager(s.cfg, s.db) + if err != nil { + return err + } if pm.Stop() { return pkg.HandleResponse(c, fiber.Map{}) } @@ -112,8 +118,10 @@ func (s *PublishService) PublishStatus(c *fiber.Ctx, req *entitys.PublishStatusR return err } - pm := manager.GetPublishManager(s.cfg, s.db) - + pm, err := manager.GetPublishManager(s.cfg, s.db) + if err != nil { + return err + } if req.RequestID != "" { // 查询单个任务 task, err := s.publishBiz.GetTaskByRequestID(c.UserContext(), req.RequestID) @@ -127,16 +135,16 @@ func (s *PublishService) PublishStatus(c *fiber.Ctx, req *entitys.PublishStatusR return pkg.HandleResponse(c, pm.GetStatus()) } -func (s *PublishService) PublishExecuteOnce(c *fiber.Ctx, req *entitys.PublishExecuteOnceRequest) error { - tokenInfo, err := s.publishBiz.ValidateAccessToken(c.UserContext(), req.AccessToken) - if err != nil { - return err - } - - pm := manager.GetPublishManager(s.cfg, s.db) - result := pm.ExecuteOnce(tokenInfo.ID) - return pkg.HandleResponse(c, result) -} +//func (s *PublishService) PublishExecuteOnce(c *fiber.Ctx, req *entitys.PublishExecuteOnceRequest) error { +// tokenInfo, err := s.publishBiz.ValidateAccessToken(c.UserContext(), req.AccessToken) +// if err != nil { +// return err +// } +// +// pm := manager.GetPublishManager(s.cfg, s.db) +// result := pm.ExecuteOnce(tokenInfo.ID) +// return pkg.HandleResponse(c, result) +//} func (s *PublishService) PublishExecuteRetry(c *fiber.Ctx, req *entitys.PublishExecuteRetryRequest) error { _, err := s.publishBiz.ValidateAccessToken(c.UserContext(), req.AccessToken) @@ -144,7 +152,10 @@ func (s *PublishService) PublishExecuteRetry(c *fiber.Ctx, req *entitys.PublishE return err } - pm := manager.GetPublishManager(s.cfg, s.db) + pm, err := manager.GetPublishManager(s.cfg, s.db) + if err != nil { + return err + } result := pm.RetryTask(req.RequestID) return pkg.HandleResponse(c, result) } diff --git a/pkg/doc.go b/pkg/doc.go index b095e64..83226dc 100644 --- a/pkg/doc.go +++ b/pkg/doc.go @@ -57,3 +57,50 @@ func ExtractWordContent(filePath, format string) (string, error) { return result.Content, nil } + +// CopyImageToDoc 将图片插入到word头部 +func CopyImageToDoc(docPath, imgPath string) error { + baseDir, err := os.Getwd() + if err != nil { + return fmt.Errorf("获取工作目录失败: %w", err) + } + + exePath := filepath.Join(baseDir, "plugins", "insert_img_to_word.exe") + + // 检查exe是否存在 + if _, err = os.Stat(exePath); os.IsNotExist(err) { + return fmt.Errorf("exe不存在: %s", exePath) + } + + // 检查docx文件是否存在 + if _, err = os.Stat(docPath); os.IsNotExist(err) { + return fmt.Errorf("文件不存在: %s", docPath) + } + + // 检查图片文件是否存在 + if _, err = os.Stat(imgPath); os.IsNotExist(err) { + return fmt.Errorf("图片不存在: %s", imgPath) + } + + cmd := exec.Command(exePath, docPath, imgPath) + output, err := cmd.CombinedOutput() + if err != nil { + return fmt.Errorf("执行失败: %v", err) + } + + 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("解析结果失败: %v, 输出: %s", err, string(output)) + } + + if !result.Success { + return fmt.Errorf("插入图片失败: %s", result.Error) + } + + return nil +} diff --git a/plugins/insert_img_to_word.exe b/plugins/insert_img_to_word.exe new file mode 100644 index 0000000..6b05716 Binary files /dev/null and b/plugins/insert_img_to_word.exe differ diff --git a/utils/gorm.go b/utils/gorm.go index 9923987..7ede22e 100644 --- a/utils/gorm.go +++ b/utils/gorm.go @@ -131,3 +131,8 @@ func (d *Db) ExecuteMany(sql string, argsList [][]interface{}) (int64, error) { } return total, nil } + +// GetOneToStruct +func (d *Db) GetOneToStruct(sql string, data interface{}, args ...interface{}) error { + return d.Client.Raw(sql, args...).Scan(data).Error +}