This commit is contained in:
2026-07-10 14:36:04 +08:00
parent 2d99bda915
commit dbeea873a7
19 changed files with 369 additions and 662 deletions
-30
View File
@@ -3,30 +3,19 @@ module video-factory
go 1.26.1
require (
github.com/cloudwego/eino v0.9.9
github.com/cloudwego/eino-ext/components/model/qwen v0.1.9
github.com/cloudwego/eino-ext/libs/acl/openai v0.1.17
github.com/gogf/gf/contrib/drivers/sqlite/v2 v2.10.2
github.com/gogf/gf/v2 v2.10.2
)
require (
github.com/BurntSushi/toml v1.6.0 // indirect
github.com/bahlo/generic-list-go v0.2.0 // indirect
github.com/buger/jsonparser v1.2.0 // indirect
github.com/bytedance/gopkg v0.1.4 // indirect
github.com/bytedance/sonic v1.15.2 // indirect
github.com/bytedance/sonic/loader v0.5.1 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/clbanning/mxj/v2 v2.7.0 // indirect
github.com/clipperhouse/displaywidth v0.11.0 // indirect
github.com/clipperhouse/uax29/v2 v2.7.0 // indirect
github.com/cloudwego/base64x v0.1.7 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/eino-contrib/jsonschema v1.0.3 // indirect
github.com/emirpasic/gods/v2 v2.0.0-alpha // indirect
github.com/evanphx/json-patch v0.5.2 // indirect
github.com/fatih/color v1.19.0 // indirect
github.com/fsnotify/fsnotify v1.10.1 // indirect
github.com/glebarez/go-sqlite v1.21.2 // indirect
@@ -35,42 +24,23 @@ require (
github.com/goccy/go-json v0.10.6 // indirect
github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/goph/emperror v0.17.2 // indirect
github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 // indirect
github.com/grokify/html-strip-tags-go v0.1.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
github.com/magiconair/properties v1.8.10 // indirect
github.com/mailru/easyjson v0.9.2 // indirect
github.com/mattn/go-colorable v0.1.15 // indirect
github.com/mattn/go-isatty v0.0.22 // indirect
github.com/mattn/go-runewidth v0.0.24 // indirect
github.com/meguminnnnnnnnn/go-openai v0.1.5 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect
github.com/nikolalohinski/gonja v1.5.3 // indirect
github.com/olekukonko/cat v0.0.0-20250911104152-50322a0618f6 // indirect
github.com/olekukonko/errors v1.3.0 // indirect
github.com/olekukonko/ll v0.1.8 // indirect
github.com/olekukonko/tablewriter v1.1.4 // indirect
github.com/pelletier/go-toml/v2 v2.4.1 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
github.com/sirupsen/logrus v1.9.4 // indirect
github.com/slongfield/pyfmt v0.0.0-20220222012616-ea85ff4c361f // indirect
github.com/smarty/assertions v1.16.0 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/wk8/go-ordered-map/v2 v2.1.8 // indirect
github.com/yargevad/filepathx v1.0.0 // indirect
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
go.opentelemetry.io/otel v1.44.0 // indirect
go.opentelemetry.io/otel/metric v1.44.0 // indirect
go.opentelemetry.io/otel/sdk v1.44.0 // indirect
go.opentelemetry.io/otel/trace v1.44.0 // indirect
go.uber.org/mock v0.5.0 // indirect
golang.org/x/arch v0.28.0 // indirect
golang.org/x/exp v0.0.0-20260611194520-c48552f49976 // indirect
golang.org/x/net v0.56.0 // indirect
golang.org/x/sys v0.46.0 // indirect
golang.org/x/text v0.38.0 // indirect
-128
View File
@@ -1,23 +1,5 @@
github.com/BurntSushi/toml v1.6.0 h1:dRaEfpa2VI55EwlIW72hMRHdWouJeRF7TPYhI+AUQjk=
github.com/BurntSushi/toml v1.6.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
github.com/airbrake/gobrake v3.6.1+incompatible/go.mod h1:wM4gu3Cn0W0K7GUuVWnlXZU11AGBXMILnrdOU8Kn00o=
github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk=
github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg=
github.com/bitly/go-simplejson v0.5.0/go.mod h1:cXHtHw4XUPsvGaxgjIAn8PhEWG9NfngEKAMDJEczWVA=
github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4=
github.com/buger/jsonparser v1.2.0 h1:4EFcvK1kD4jyj6YqNK6skK6w+y7FHHBR+XBCtxwu/6g=
github.com/buger/jsonparser v1.2.0/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0=
github.com/bugsnag/bugsnag-go v1.4.0/go.mod h1:2oa8nejYd4cQ/b0hMIopN0lCRxU0bueqREvZLWFrtK8=
github.com/bugsnag/panicwrap v1.2.0/go.mod h1:D/8v3kj0zr8ZAKg1AQ6crr+5VwKN5eIywRkfhyM/+dE=
github.com/bytedance/gopkg v0.1.4 h1:oZnQwnX82KAIWb7033bEwtxvTqXcYMxDBaQxo5JJHWM=
github.com/bytedance/gopkg v0.1.4/go.mod h1:v1zWfPm21Fb+OsyXN2VAHdL6TBb2L88anLQgdyje6R4=
github.com/bytedance/mockey v1.3.0 h1:ONLRdvhqmCfr9rTasUB8ZKCfvbdD2tohOg4u+4Q/ed0=
github.com/bytedance/mockey v1.3.0/go.mod h1:1BPHF9sol5R1ud/+0VEHGQq/+i2lN+GTsr3O2Q9IENY=
github.com/bytedance/sonic v1.15.2 h1:90H+rcF/FwLXwfB1cudOLq/je83n683Utf4Cbp0xHCo=
github.com/bytedance/sonic v1.15.2/go.mod h1:mT2NbXunuaEbnZ+mRIX/vYqKISmgEuHFDI4UzmKx2SA=
github.com/bytedance/sonic/loader v0.5.1 h1:Ygpfa9zwRCCKSlrp5bBP/b/Xzc3VxsAW+5NIYXrOOpI=
github.com/bytedance/sonic/loader v0.5.1/go.mod h1:AR4NYCk5DdzZizZ5djGqQ92eEhCCcdf5x77udYiSJRo=
github.com/certifi/gocertifi v0.0.0-20190105021004-abcd57078448/go.mod h1:GJKEexRPVJrBSOjoqN5VNOIKJ5Q3RViH6eu3puDRwx4=
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/clbanning/mxj/v2 v2.7.0 h1:WA/La7UGCanFe5NpHF0Q3DNtnCsVoxbPKuyBNHWRyME=
@@ -26,36 +8,18 @@ github.com/clipperhouse/displaywidth v0.11.0 h1:lBc6kY44VFw+TDx4I8opi/EtL9m20WSE
github.com/clipperhouse/displaywidth v0.11.0/go.mod h1:bkrFNkf81G8HyVqmKGxsPufD3JhNl3dSqnGhOoSD/o0=
github.com/clipperhouse/uax29/v2 v2.7.0 h1:+gs4oBZ2gPfVrKPthwbMzWZDaAFPGYK72F0NJv2v7Vk=
github.com/clipperhouse/uax29/v2 v2.7.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM=
github.com/cloudwego/base64x v0.1.7 h1:NppS+Fgzg5ovhn4NkUXaDT3x9jldgH5ToMCqzBSi2zI=
github.com/cloudwego/base64x v0.1.7/go.mod h1:Cu1PV9zfrSf7ET2tIbWbbEy7jO7HHJ13q4X2SQ8aWYg=
github.com/cloudwego/eino v0.9.9 h1:x63hvRif6ANPh9YEPoTIrp1potEeoLQFAjOclKaX/Kg=
github.com/cloudwego/eino v0.9.9/go.mod h1:OBD1mrkfkt/pJa4rkg1P0VnaMeOVl7l8IAdEqY//3IQ=
github.com/cloudwego/eino-ext/components/model/qwen v0.1.9 h1:xCz/mp43JeWqupjPR3zLRArmwC6P29/6lTwbwh1yzYM=
github.com/cloudwego/eino-ext/components/model/qwen v0.1.9/go.mod h1:slTGTuhzkzhNavf+1UtUg1FvUSA31iNAF+rq1mT4SnI=
github.com/cloudwego/eino-ext/libs/acl/openai v0.1.17 h1:EeVcR1TslRA2IdNW1h/2LaGbPlffwGhQm99jM3zWZiI=
github.com/cloudwego/eino-ext/libs/acl/openai v0.1.17/go.mod h1:Zkcx6DPTR2NfWmtSXbhItswGw6hqUezNPhNcke0pOG8=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/eino-contrib/jsonschema v1.0.3 h1:2Kfsm1xlMV0ssY2nuxshS4AwbLFuqmPmzIjLVJ1Fsp0=
github.com/eino-contrib/jsonschema v1.0.3/go.mod h1:cpnX4SyKjWjGC7iN2EbhxaTdLqGjCi0e9DxpLYxddD4=
github.com/emirpasic/gods/v2 v2.0.0-alpha h1:dwFlh8pBg1VMOXWGipNMRt8v96dKAIvBehtCt6OtunU=
github.com/emirpasic/gods/v2 v2.0.0-alpha/go.mod h1:W0y4M2dtBB9U5z3YlghmpuUhiaZT2h6yoeE+C1sCp6A=
github.com/evanphx/json-patch v0.5.2 h1:xVCHIVMUu1wtM/VkR9jVZ45N3FhZfYMMYGorLCR8P3k=
github.com/evanphx/json-patch v0.5.2/go.mod h1:ZWS5hhDbVDyob71nXKNL0+PWn6ToqBHMikGIFbs31qQ=
github.com/fatih/color v1.19.0 h1:Zp3PiM21/9Ld6FzSKyL5c/BULoe/ONr9KlbYVOfG8+w=
github.com/fatih/color v1.19.0/go.mod h1:zNk67I0ZUT1bEGsSGyCZYZNrHuTkJJB+r6Q9VuMi0LE=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/fsnotify/fsnotify v1.10.1 h1:b0/UzAf9yR5rhf3RPm9gf3ehBPpf0oZKIjtpKrx59Ho=
github.com/fsnotify/fsnotify v1.10.1/go.mod h1:TLheqan6HD6GBK6PrDWyDPBaEV8LspOxvPSjC+bVfgo=
github.com/getsentry/raven-go v0.2.0/go.mod h1:KungGk8q33+aIAZUIVWZDr2OfAEBsO49PX4NzFV5kcQ=
github.com/glebarez/go-sqlite v1.21.2 h1:3a6LFC4sKahUunAmynQKLZceZCOzUthkRkEAl9gAXWo=
github.com/glebarez/go-sqlite v1.21.2/go.mod h1:sfxdZyhQjTM2Wry3gVYWaW072Ri1WMdWJi0k6+3382k=
github.com/go-check/check v0.0.0-20180628173108-788fd7840127 h1:0gkP6mzaMqkmpcJYCFOLkIBwI7xFExG03bbkOkCvUPI=
github.com/go-check/check v0.0.0-20180628173108-788fd7840127/go.mod h1:9ES+weclKsC9YodN5RgxqK/VD9HM9JsCSh7rNhMZE98=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
@@ -63,66 +27,32 @@ github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/goccy/go-json v0.10.6 h1:p8HrPJzOakx/mn/bQtjgNjdTcN+/S6FcG2CTtQOrHVU=
github.com/goccy/go-json v0.10.6/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
github.com/gofrs/uuid v3.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
github.com/gogf/gf/contrib/drivers/sqlite/v2 v2.10.2 h1:KLS68SWS2W749x7e+eCCOO3UD2Sbw+bIbLEPR8o1FXw=
github.com/gogf/gf/contrib/drivers/sqlite/v2 v2.10.2/go.mod h1:uLcsu73PfpyhRc0Jq0gGAWQjN1tyGU9iBRrYgt/lu7g=
github.com/gogf/gf/v2 v2.10.2 h1:46IO0Uc8e85/FqdftJFskfDejJLBL0JBnGS5qOftUu8=
github.com/gogf/gf/v2 v2.10.2/go.mod h1:Svl1N+E8G/QshU2DUbh/3J/AJauqCgUnxHurXWR4Qx0=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 h1:BHT72Gu3keYf3ZEu2J0b1vyeLSOYI8bm5wbJM/8yDe8=
github.com/google/pprof v0.0.0-20250403155104-27863c87afa6/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA=
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/goph/emperror v0.17.2 h1:yLapQcmEsO0ipe9p5TaN22djm3OFV/TfM/fcYP0/J18=
github.com/goph/emperror v0.17.2/go.mod h1:+ZbQ+fUNO/6FNiUo0ujtMjhgad9Xa6fQL9KhH4LNHic=
github.com/gopherjs/gopherjs v1.17.2 h1:fQnZVsXk8uxXIStYb0N4bGk7jeyTalG/wsZjQ25dO0g=
github.com/gopherjs/gopherjs v1.17.2/go.mod h1:pRRIvn/QzFLrKfvEz3qUuEhtE/zLCWfreZ6J5gM2i+k=
github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 h1:JeSE6pjso5THxAzdVpqr6/geYxZytqFMBCOtn/ujyeo=
github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674/go.mod h1:r4w70xmWCQKmi1ONH4KIaBptdivuRPyosB9RmPlGEwA=
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/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo=
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0/go.mod h1:1NbS8ALrpOvjt0rHPNLyCIeMtbizbir8U//inJ+zuB8=
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
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/magiconair/properties v1.8.10 h1:s31yESBquKXCV9a/ScB3ESkOjUYYv+X0rg8SYxI99mE=
github.com/magiconair/properties v1.8.10/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
github.com/mailru/easyjson v0.9.2 h1:dX8U45hQsZpxd80nLvDGihsQ/OxlvTkVUXH2r/8cb2M=
github.com/mailru/easyjson v0.9.2/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU=
github.com/mattn/go-colorable v0.1.15 h1:+u9SLTRGnXv73cEsnsmoZBom+dMU88B2M0aDcWy0/jY=
github.com/mattn/go-colorable v0.1.15/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
github.com/mattn/go-isatty v0.0.22 h1:j8l17JJ9i6VGPUFUYoTUKPSgKe/83EYU2zBC7YNKMw4=
github.com/mattn/go-isatty v0.0.22/go.mod h1:ZXfXG4SQHsB/w3ZeOYbR0PrPwLy+n6xiMrJlRFqopa4=
github.com/mattn/go-runewidth v0.0.24 h1:cpokDiIn0MGnhdHwuWnJBITySJ20QyNGnY2kR/ay2DU=
github.com/mattn/go-runewidth v0.0.24/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=
github.com/meguminnnnnnnnn/go-openai v0.1.5 h1:K9XFfnEUj9E+9djustmfa4eIdg8Q2vWD4mGv+AHbQ2k=
github.com/meguminnnnnnnnn/go-openai v0.1.5/go.mod h1:qs96ysDmxhE4BZoU45I43zcyfnaYxU3X+aRzLko/htY=
github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b h1:j7+1HpAFS1zy5+Q4qx1fWh90gTKwiN4QCGoY9TWyyO4=
github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee h1:W5t00kpgFdJifH4BDsTlE89Zl93FEloxaWZfGcifgq8=
github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/nikolalohinski/gonja v1.5.3 h1:GsA+EEaZDZPGJ8JtpeGN78jidhOlxeJROpqMT9fTj9c=
github.com/nikolalohinski/gonja v1.5.3/go.mod h1:RmjwxNiXAEqcq1HeK5SSMmqFJvKOfTfXhkJv6YBtPa4=
github.com/olekukonko/cat v0.0.0-20250911104152-50322a0618f6 h1:zrbMGy9YXpIeTnGj4EljqMiZsIcE09mmF8XsD5AYOJc=
github.com/olekukonko/cat v0.0.0-20250911104152-50322a0618f6/go.mod h1:rEKTHC9roVVicUIfZK7DYrdIoM0EOr8mK1Hj5s3JjH0=
github.com/olekukonko/errors v1.3.0 h1:teJvgLGUEqMzBUms+Dj3/3szNqCG/Jdw9iDbum8fR6U=
@@ -131,15 +61,6 @@ github.com/olekukonko/ll v0.1.8 h1:ysHCJRGHYKzmBSdz9w5AySztx7lG8SQY+naTGYUbsz8=
github.com/olekukonko/ll v0.1.8/go.mod h1:RPRC6UcscfFZgjo1nulkfMH5IM0QAYim0LfnMvUuozw=
github.com/olekukonko/tablewriter v1.1.4 h1:ORUMI3dXbMnRlRggJX3+q7OzQFDdvgbN9nVWj1drm6I=
github.com/olekukonko/tablewriter v1.1.4/go.mod h1:+kedxuyTtgoZLwif3P1Em4hARJs+mVnzKxmsCL/C5RY=
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.8.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/gomega v1.5.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
github.com/pelletier/go-toml/v2 v2.4.1 h1:j5OMOImsH+j2k7GJ5YO+RxfWwohNiH6t5zB/+h3bagc=
github.com/pelletier/go-toml/v2 v2.4.1/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
@@ -147,37 +68,8 @@ github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
github.com/rollbar/rollbar-go v1.0.2/go.mod h1:AcFs5f0I+c71bpHlXNNDbOWJiKwjFDtISeXco0L5PKQ=
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
github.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w=
github.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g=
github.com/slongfield/pyfmt v0.0.0-20220222012616-ea85ff4c361f h1:Z2cODYsUxQPofhpYRMQVwWz4yUVpHF+vPi+eUdruUYI=
github.com/slongfield/pyfmt v0.0.0-20220222012616-ea85ff4c361f/go.mod h1:JqzWyvTuI2X4+9wOHmKSQCYxybB/8j6Ko43qVmXDuZg=
github.com/smarty/assertions v1.16.0 h1:EvHNkdRA4QHMrn75NZSoUQ/mAUXAYWfatfB01yTCzfY=
github.com/smarty/assertions v1.16.0/go.mod h1:duaaFdCS0K9dnoM50iyek/eYINOZ64gbh1Xlf6LG7AI=
github.com/smartystreets/goconvey v1.8.1 h1:qGjIddxOk4grTu9JPOU31tVfq3cNdBlNa5sSznIX1xY=
github.com/smartystreets/goconvey v1.8.1/go.mod h1:+/u4qLyY6x1jReYOp7GOM2FSt8aP9CzCZL03bI28W60=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
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/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
github.com/wk8/go-ordered-map/v2 v2.1.8 h1:5h/BUHu93oj4gIdvHHHGsScSTMijfx5PeYkE/fJgbpc=
github.com/wk8/go-ordered-map/v2 v2.1.8/go.mod h1:5nJHM5DyteebpVlHnWMV0rPz6Zp7+xBAnxjb1X5vnTw=
github.com/x-cray/logrus-prefixed-formatter v0.5.2 h1:00txxvfBM9muc0jiLIEAkAcIMJzfthRT6usrui8uGmg=
github.com/x-cray/logrus-prefixed-formatter v0.5.2/go.mod h1:2duySbKsL6M18s5GU7VPsoEPHyzalCE06qoARUCeBBE=
github.com/yargevad/filepathx v1.0.0 h1:SYcT+N3tYGi+NvazubCNlvgIPbzAk7i7y2dwg3I5FYc=
github.com/yargevad/filepathx v1.0.0/go.mod h1:BprfX/gpYNJHJfc35GjRRpVcwWXS89gGulUIU5tK3tA=
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
go.opentelemetry.io/otel v1.44.0 h1:JjwHmHpA4iZ3wBxluu2fbbE7j4kqlE8jXyAyPXH7HqU=
@@ -192,35 +84,15 @@ go.opentelemetry.io/otel/trace v1.44.0 h1:jxF5CsGYCe74MCRx2X4g7WsY/VBKRqqpNvXlX/
go.opentelemetry.io/otel/trace v1.44.0/go.mod h1:oLl1jrMQAVo6v3GAggN+1VH9VIz9iUSvW53sW1Q8PIE=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
go.uber.org/mock v0.5.0 h1:KAMbZvZPyBPWgD14IrIQ38QCyjwpvVVV6K/bHl1IwQU=
go.uber.org/mock v0.5.0/go.mod h1:ge71pBPLYDk7QIi1LupWxdAykm7KIEFchiOqd6z7qMM=
golang.org/x/arch v0.28.0 h1:wVwVdqsTuUbJvhYVCspQYwZXHNYeLSoZnmHD+ggddpQ=
golang.org/x/arch v0.28.0/go.mod h1:0X+GdSIP+kL5wPmpK7sdkEVTt2XoYP0cSjQSbZBwOi8=
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.53.0 h1:QZ4Muo8THX6CizN2vPPd5fBGHyogrdK9fG4wLPFUsto=
golang.org/x/crypto v0.53.0/go.mod h1:DNLU434OwVakk9PzuwV8w62mAJpRJL3vsgcfp4Qnsio=
golang.org/x/exp v0.0.0-20260611194520-c48552f49976 h1:X8Hz2ImujgbmetVuW+w2YkyZChE3cBpZi2P158rTG9M=
golang.org/x/exp v0.0.0-20260611194520-c48552f49976/go.mod h1:vnf4pv9iKZXY58sQE1L86zmNWJ4159e1RkcWiLCkeEY=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.56.0 h1:Rw8j/hFzGvJUZwNBXnAtf5sVDVt+65SK2C7IxCxZt5o=
golang.org/x/net v0.56.0/go.mod h1:D3Ku6r+V6JROoZK144D2XfMHFcMq/0zSfLelVTCFKec=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.46.0 h1:noSf2Fq6F8DBgS+LysIkx7rIExoNHJsxOAtPp4rthXw=
golang.org/x/sys v0.46.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/term v0.44.0 h1:0rLvDRCtNj0gZkyIXhCyOb2OAzEhLVqc4B+hrsBhrmc=
golang.org/x/term v0.44.0/go.mod h1:7ze4MdzUzLXpSAoFP1H0bOI9aXDqveSvatT5vKcFh2Y=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.38.0 h1:sXmwo9DwP3OK9EZ7PqAdaooSGozfl/3a6/xJcbzPRhE=
golang.org/x/text v0.38.0/go.mod h1:YXZt3QhHUKYT53r2lLKFIVi6Ao1jdzrTR/KQ09qyxF4=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/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/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
modernc.org/libc v1.22.5 h1:91BNch/e5B0uPbJFgqbxXuOnxBQjlS//icfQEGmvyjE=
BIN
View File
Binary file not shown.
+146 -61
View File
@@ -1,17 +1,17 @@
package agent
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"time"
"github.com/cloudwego/eino/components/model"
"video-factory/shortdrama/model/adapter"
)
// ModelConfig 模型配置 — 所有字段必须显式提供,无硬编码默认值
// ModelConfig 模型配置
type ModelConfig struct {
Provider string // 模型供应商(qwen/openai_compatible),空则自动检测
ModelName string // 对话模型名
APIKey string // API密钥
BaseURL string // API地址
@@ -20,53 +20,8 @@ type ModelConfig struct {
Timeout time.Duration // HTTP请求超时(0表示默认)
}
// context keys
type ctxKey string
const (
ctxKeyAPIKey ctxKey = "api_key"
ctxKeyBaseURL ctxKey = "base_url"
ctxKeyDramaId ctxKey = "drama_id"
)
// WithDramaId 将短剧ID注入 context,供工具函数读取场景图片
func WithDramaId(ctx context.Context, dramaId int64) context.Context {
return context.WithValue(ctx, ctxKeyDramaId, dramaId)
}
// GetDramaId 从 context 获取短剧ID
func GetDramaId(ctx context.Context) int64 {
if v, ok := ctx.Value(ctxKeyDramaId).(int64); ok {
return v
}
return 0
}
// WithModelConfig 将模型配置注入 context,供工具函数读取
func WithModelConfig(ctx context.Context, cfg *ModelConfig) context.Context {
ctx = context.WithValue(ctx, ctxKeyAPIKey, cfg.APIKey)
ctx = context.WithValue(ctx, ctxKeyBaseURL, cfg.BaseURL)
return ctx
}
// GetAPIKey 从 context 获取 API key
func GetAPIKey(ctx context.Context) string {
if v, ok := ctx.Value(ctxKeyAPIKey).(string); ok && v != "" {
return v
}
return ""
}
// GetBaseURL 从 context 获取 API 地址
func GetBaseURL(ctx context.Context) string {
if v, ok := ctx.Value(ctxKeyBaseURL).(string); ok && v != "" {
return v
}
return ""
}
// NewChatModel 根据配置初始化聊天模型(使用适配器模式,自动选择供应商)
func NewChatModel(ctx context.Context, cfg *ModelConfig) (cm model.ChatModel, err error) {
// CallChatModel 调用大模型聊天接口(OpenAI 兼容格式)
func CallChatModel(ctx context.Context, cfg *ModelConfig, req *ChatRequest) (*ChatResponse, error) {
if cfg == nil {
return nil, fmt.Errorf("模型配置不能为空")
}
@@ -80,14 +35,144 @@ func NewChatModel(ctx context.Context, cfg *ModelConfig) (cm model.ChatModel, er
return nil, fmt.Errorf("API 地址未配置")
}
adapterCfg := &adapter.ChatModelConfig{
Provider: adapter.ChatProvider(cfg.Provider),
ModelName: cfg.ModelName,
APIKey: cfg.APIKey,
BaseURL: cfg.BaseURL,
MaxTokens: cfg.MaxTokens,
Temperature: cfg.Temperature,
Timeout: cfg.Timeout,
timeout := cfg.Timeout
if timeout <= 0 {
timeout = 60 * time.Second
}
return adapter.NewChatModel(ctx, adapterCfg)
body, err := buildReqBody(cfg.ModelName, req)
if err != nil {
return nil, err
}
url := trimSlashes(cfg.BaseURL) + "/v1/chat/completions"
httpReq, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(body))
if err != nil {
return nil, fmt.Errorf("创建请求失败: %w", err)
}
httpReq.Header.Set("Authorization", "Bearer "+cfg.APIKey)
httpReq.Header.Set("Content-Type", "application/json")
client := &http.Client{Timeout: timeout}
resp, err := client.Do(httpReq)
if err != nil {
return nil, fmt.Errorf("请求失败: %w", err)
}
defer resp.Body.Close()
respBody, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("读取响应失败: %w", err)
}
return parseRespBody(respBody)
}
// ==================== 内部实现 ====================
type openAIReqBody struct {
Model string `json:"model"`
Messages []*ChatMessage `json:"messages"`
MaxTokens int `json:"max_tokens,omitempty"`
Temperature float32 `json:"temperature,omitempty"`
Stream bool `json:"stream,omitempty"`
Tools []openAIToolDef `json:"tools,omitempty"`
}
type openAIToolDef struct {
Type string `json:"type"`
Function openAIToolFunction `json:"function"`
}
type openAIToolFunction struct {
Name string `json:"name"`
Description string `json:"description"`
Parameters map[string]any `json:"parameters"`
}
type openAIRespBody struct {
Choices []openAIChoice `json:"choices"`
Error *struct {
Message string `json:"message"`
Code string `json:"code"`
} `json:"error,omitempty"`
}
type openAIChoice struct {
Index int `json:"index"`
Message openAIMsg `json:"message"`
}
type openAIMsg struct {
Content string `json:"content"`
ToolCalls []openAIToolCall `json:"tool_calls,omitempty"`
}
type openAIToolCall struct {
ID string `json:"id"`
Type string `json:"type"`
Function openAIFuncCall `json:"function"`
}
type openAIFuncCall struct {
Name string `json:"name"`
Arguments string `json:"arguments"`
}
func buildReqBody(model string, req *ChatRequest) ([]byte, error) {
body := openAIReqBody{
Model: model,
Messages: req.Messages,
MaxTokens: req.MaxTokens,
Temperature: req.Temperature,
Stream: req.Stream,
}
if len(req.Tools) > 0 {
body.Tools = make([]openAIToolDef, 0, len(req.Tools))
for _, t := range req.Tools {
body.Tools = append(body.Tools, openAIToolDef{
Type: "function",
Function: openAIToolFunction{
Name: t.Name,
Description: t.Description,
Parameters: t.Parameters,
},
})
}
}
return json.Marshal(body)
}
func parseRespBody(data []byte) (*ChatResponse, error) {
var resp openAIRespBody
if err := json.Unmarshal(data, &resp); err != nil {
return nil, fmt.Errorf("解析响应失败: %s", string(data))
}
if resp.Error != nil {
return nil, fmt.Errorf("API错误(code=%s): %s", resp.Error.Code, resp.Error.Message)
}
if len(resp.Choices) == 0 {
return nil, fmt.Errorf("响应为空")
}
msg := resp.Choices[0].Message
cr := &ChatResponse{Content: msg.Content}
if len(msg.ToolCalls) > 0 {
cr.ToolCalls = make([]*ToolCall, 0, len(msg.ToolCalls))
for _, tc := range msg.ToolCalls {
cr.ToolCalls = append(cr.ToolCalls, &ToolCall{
ID: tc.ID,
Name: tc.Function.Name,
Arguments: tc.Function.Arguments,
})
}
}
return cr, nil
}
func trimSlashes(s string) string {
for len(s) > 0 && s[len(s)-1] == '/' {
s = s[:len(s)-1]
}
return s
}
+20
View File
@@ -0,0 +1,20 @@
package agent
import "context"
type contextKey string
const ctxKeyDramaID contextKey = "drama_id"
// WithDramaID 将短剧ID存入context
func WithDramaID(ctx context.Context, dramaID int64) context.Context {
return context.WithValue(ctx, ctxKeyDramaID, dramaID)
}
// GetDramaID 从context中获取短剧ID
func GetDramaID(ctx context.Context) int64 {
if id, ok := ctx.Value(ctxKeyDramaID).(int64); ok {
return id
}
return 0
}
+39 -46
View File
@@ -6,23 +6,21 @@ import (
"fmt"
"time"
"github.com/cloudwego/eino/components/model"
"github.com/cloudwego/eino/schema"
"github.com/gogf/gf/v2/frame/g"
)
// ReActAgent 实现 ReAct 模式的智能体
type ReActAgent struct {
model model.ChatModel
cfg *ModelConfig
tools []*ToolInfo
systemPrompt string
maxStep int
}
// NewReActAgent 创建 ReAct 智能体
func NewReActAgent(ctx context.Context, chatModel model.ChatModel, tools []*ToolInfo, systemPrompt string, maxStep int) *ReActAgent {
func NewReActAgent(cfg *ModelConfig, tools []*ToolInfo, systemPrompt string, maxStep int) *ReActAgent {
return &ReActAgent{
model: chatModel,
cfg: cfg,
tools: tools,
systemPrompt: systemPrompt,
maxStep: maxStep,
@@ -32,15 +30,9 @@ func NewReActAgent(ctx context.Context, chatModel model.ChatModel, tools []*Tool
// Run 执行 ReAct 循环
// 标准流程: 思考 → 行动(调用工具) → 观察(工具结果) → 重复 → 最终回答
func (a *ReActAgent) Run(ctx context.Context, userInput string) (string, error) {
messages := []*schema.Message{
schema.SystemMessage(a.systemPrompt),
schema.UserMessage(userInput),
}
// 构建 toolInfos 传给模型
toolInfos := make([]*schema.ToolInfo, 0, len(a.tools))
for _, t := range a.tools {
toolInfos = append(toolInfos, t.ToEinoToolInfo())
messages := []*ChatMessage{
{Role: RoleSystem, Content: a.systemPrompt},
{Role: RoleUser, Content: userInput},
}
g.Log().Infof(ctx, "ReAct 开始执行,maxStep=%d, tools=%d", a.maxStep, len(a.tools))
@@ -48,9 +40,13 @@ func (a *ReActAgent) Run(ctx context.Context, userInput string) (string, error)
for step := 0; step < a.maxStep; step++ {
g.Log().Infof(ctx, "ReAct step %d/%d: 调用模型...", step+1, a.maxStep)
// 1. LLM 思考并决定行动
startTime := time.Now()
result, err := a.model.Generate(ctx, messages, model.WithTools(toolInfos))
result, err := CallChatModel(ctx, a.cfg, &ChatRequest{
Messages: messages,
MaxTokens: a.cfg.MaxTokens,
Temperature: a.cfg.Temperature,
Tools: a.tools,
})
elapsed := time.Since(startTime)
if err != nil {
return "", fmt.Errorf("step %d: 模型调用失败: %w", step, err)
@@ -59,69 +55,66 @@ func (a *ReActAgent) Run(ctx context.Context, userInput string) (string, error)
g.Log().Infof(ctx, "ReAct step %d/%d: 模型返回 (耗时 %v), content_len=%d, ToolCalls=%d",
step+1, a.maxStep, elapsed, len(result.Content), len(result.ToolCalls))
messages = append(messages, result)
messages = append(messages, &ChatMessage{
Role: RoleAssistant,
Content: result.Content,
ToolCalls: result.ToolCalls,
})
// 2. 检查是否有工具调用
// 无工具调用 → 最终回答
if len(result.ToolCalls) == 0 {
// 没有工具调用 → 最终回答
g.Log().Infof(ctx, "ReAct step %d/%d: 无工具调用,返回最终结果 (content长度=%d)", step+1, a.maxStep, len(result.Content))
g.Log().Infof(ctx, "ReAct step %d/%d: 无工具调用,返回最终结果", step+1, a.maxStep)
return result.Content, nil
}
// 3. 执行每个工具调用
// 执行每个工具调用
for _, tc := range result.ToolCalls {
tool := a.findTool(tc.Function.Name)
tool := a.findTool(tc.Name)
if tool == nil {
g.Log().Warningf(ctx, "ReAct step %d: 未知工具: %s", step+1, tc.Function.Name)
toolResultMsg := &schema.Message{
Role: schema.Tool,
Content: fmt.Sprintf("未知工具: %s", tc.Function.Name),
ToolName: tc.Function.Name,
g.Log().Warningf(ctx, "ReAct step %d: 未知工具: %s", step+1, tc.Name)
messages = append(messages, &ChatMessage{
Role: RoleTool,
Content: fmt.Sprintf("未知工具: %s", tc.Name),
Name: tc.Name,
ToolCallID: tc.ID,
}
messages = append(messages, toolResultMsg)
})
continue
}
// 解析参数
var args map[string]any
if err := json.Unmarshal([]byte(tc.Function.Arguments), &args); err != nil {
if err := json.Unmarshal([]byte(tc.Arguments), &args); err != nil {
g.Log().Warningf(ctx, "ReAct step %d: 参数解析失败: %v", step+1, err)
toolResultMsg := &schema.Message{
Role: schema.Tool,
messages = append(messages, &ChatMessage{
Role: RoleTool,
Content: fmt.Sprintf("参数解析失败: %v", err),
ToolName: tc.Function.Name,
Name: tc.Name,
ToolCallID: tc.ID,
}
messages = append(messages, toolResultMsg)
})
continue
}
g.Log().Infof(ctx, "ReAct step %d: 调用工具 %s, 参数: %s", step+1, tc.Function.Name, tc.Function.Arguments)
g.Log().Infof(ctx, "ReAct step %d: 调用工具 %s, 参数: %s", step+1, tc.Name, tc.Arguments)
// 执行工具
toolStart := time.Now()
output, err := tool.Func(ctx, args)
toolElapsed := time.Since(toolStart)
if err != nil {
output = fmt.Sprintf("工具执行失败: %v", err)
g.Log().Warningf(ctx, "ReAct step %d: 工具 %s 执行失败 (耗时 %v): %v", step+1, tc.Function.Name, toolElapsed, err)
g.Log().Warningf(ctx, "ReAct step %d: 工具 %s 执行失败 (耗时 %v): %v", step+1, tc.Name, toolElapsed, err)
} else {
truncated := output
if len(truncated) > 200 {
truncated = truncated[:200] + "..."
}
g.Log().Infof(ctx, "ReAct step %d: 工具 %s 执行成功 (耗时 %v), 结果长度=%d, 预览: %s", step+1, tc.Function.Name, toolElapsed, len(output), truncated)
g.Log().Infof(ctx, "ReAct step %d: 工具 %s 执行成功 (耗时 %v), 结果长度=%d", step+1, tc.Name, toolElapsed, len(output))
}
// 4. 观察工具结果
toolResultMsg := &schema.Message{
Role: schema.Tool,
messages = append(messages, &ChatMessage{
Role: RoleTool,
Content: output,
Name: tc.Function.Name,
Name: tc.Name,
ToolCallID: tc.ID,
}
messages = append(messages, toolResultMsg)
})
}
}
+4 -57
View File
@@ -11,51 +11,9 @@ import (
"video-factory/shortdrama/dao"
"github.com/cloudwego/eino/schema"
"github.com/gogf/gf/v2/frame/g"
)
// ToolInfo 工具定义
type ToolInfo struct {
Name string
Description string
Parameters map[string]any
Func func(ctx context.Context, args map[string]any) (string, error)
}
// ToEinoToolInfo 转换为 Eino 的 ToolInfo 格式
func (t *ToolInfo) ToEinoToolInfo() *schema.ToolInfo {
params := make(map[string]*schema.ParameterInfo)
if paramsMap, ok := t.Parameters["properties"].(map[string]any); ok {
for key, val := range paramsMap {
if prop, ok := val.(map[string]any); ok {
desc, _ := prop["description"].(string)
pType, _ := prop["type"].(string)
params[key] = &schema.ParameterInfo{
Type: schema.DataType(pType),
Desc: desc,
Required: false,
}
}
}
}
if required, ok := t.Parameters["required"].([]any); ok {
for _, r := range required {
if rStr, ok := r.(string); ok {
if p, exists := params[rStr]; exists {
p.Required = true
}
}
}
}
return &schema.ToolInfo{
Name: t.Name,
Desc: t.Description,
ParamsOneOf: schema.NewParamsOneOfByParams(params),
}
}
// GetTools 获取 ReAct Agent 可用的所有工具
func GetTools() []*ToolInfo {
return []*ToolInfo{
@@ -87,7 +45,6 @@ func parseScriptTool() *ToolInfo {
return "", fmt.Errorf("剧本内容不能为空")
}
// 按 --- 分割多集
episodeTexts := strings.Split(rawScript, "---")
type episodeInfo struct {
Index int `json:"index"`
@@ -103,7 +60,6 @@ func parseScriptTool() *ToolInfo {
}
lines := strings.SplitN(text, "\n", 2)
title := strings.TrimSpace(lines[0])
// 去掉可能的序号前缀如 "第1集"、"第一集"、"Episode 1" 等
title = cleanEpisodeTitle(title)
content := ""
if len(lines) > 1 {
@@ -168,13 +124,12 @@ func analyzeScriptForEpisodeTool() *ToolInfo {
return "", fmt.Errorf("剧本内容不能为空")
}
if duration <= 0 {
duration = 60 // 默认60秒
duration = 60
}
episodeIndex, _ := args["episode_index"].(float64)
title, _ := args["episode_title"].(string)
// 按空行或场景标记分割场景
sceneTexts := strings.Split(scriptContent, "\n\n")
type sceneInfo struct {
Index int `json:"index"`
@@ -192,7 +147,6 @@ func analyzeScriptForEpisodeTool() *ToolInfo {
sceneTexts = []string{scriptContent}
}
// 推测出场演员
var characters []string
if charsRaw, ok := args["characters"].(string); ok && charsRaw != "" {
var chars []struct {
@@ -218,7 +172,6 @@ func analyzeScriptForEpisodeTool() *ToolInfo {
sceneDur++
}
// 提取第一行作为场景描述
lines := strings.SplitN(text, "\n", 2)
desc := strings.TrimSpace(lines[0])
content := ""
@@ -228,7 +181,6 @@ func analyzeScriptForEpisodeTool() *ToolInfo {
content = desc
}
// 匹配出场演员
var sceneChars []string
for _, c := range characters {
if strings.Contains(text, c) {
@@ -260,7 +212,7 @@ func analyzeScriptForEpisodeTool() *ToolInfo {
}
}
// ==================== Tool 4: 生成场景图 ====================
// ==================== Tool 3: 生成场景图 ====================
func generateSceneImageTool() *ToolInfo {
return &ToolInfo{
@@ -293,12 +245,9 @@ func generateSceneImageTool() *ToolInfo {
style, _ := args["style"].(string)
episodeIdx, _ := args["episode_index"].(float64)
sceneIdx, _ := args["scene_index"].(float64)
_ = style
prompt := fmt.Sprintf("画面描述:%s,风格:%s", visualDesc, style)
_ = prompt // 保留供日志使用
// 从 DB 场景表中查找已有场景图片
dramaId := GetDramaId(ctx)
dramaId := GetDramaID(ctx)
var imgBase64 string
if dramaId > 0 && visualDesc != "" {
scenes, err := dao.Scene.ListByDrama(ctx, dramaId)
@@ -307,7 +256,6 @@ func generateSceneImageTool() *ToolInfo {
if sc.ImagePath == "" {
continue
}
// 匹配场景描述(双向包含匹配)
if strings.Contains(visualDesc, sc.Description) || strings.Contains(sc.Description, visualDesc) {
b64, err := readImageFileAsBase64(sc.ImagePath)
if err == nil {
@@ -341,7 +289,6 @@ func cleanEpisodeTitle(title string) string {
prefixes := []string{"第", "Episode", "episode", "EP"}
for _, p := range prefixes {
if strings.HasPrefix(title, p) {
// 去掉序号前缀后取标题部分
parts := strings.SplitN(title, " ", 2)
if len(parts) > 1 {
return parts[1]
+55
View File
@@ -0,0 +1,55 @@
package agent
import "context"
// ==================== 工具 ====================
// ToolInfo 工具定义(包含执行函数)
type ToolInfo struct {
Name string
Description string
Parameters map[string]any
Func func(ctx context.Context, args map[string]any) (string, error)
}
// ToolCall 模型请求的工具调用
type ToolCall struct {
ID string `json:"id"`
Name string `json:"name"`
Arguments string `json:"arguments"`
}
// ==================== 聊天消息 ====================
// ChatMessage 对话消息
type ChatMessage struct {
Role string `json:"role"`
Content string `json:"content"`
ToolCalls []*ToolCall `json:"tool_calls,omitempty"`
ToolCallID string `json:"tool_call_id,omitempty"`
Name string `json:"name,omitempty"`
}
// ChatRequest 聊天请求
type ChatRequest struct {
Messages []*ChatMessage
MaxTokens int
Temperature float32
Stream bool
Tools []*ToolInfo
}
// ChatResponse 聊天响应
type ChatResponse struct {
Content string
ToolCalls []*ToolCall
}
// ==================== 角色常量 ====================
const (
RoleSystem = "system"
RoleUser = "user"
RoleAssistant = "assistant"
RoleTool = "tool"
)
+2 -20
View File
@@ -5,7 +5,6 @@ import (
"fmt"
"video-factory/shortdrama/model/dto"
"video-factory/shortdrama/model/entity"
"video-factory/shortdrama/service"
)
@@ -14,8 +13,7 @@ type config struct{}
var Config = new(config)
func (c *config) Get(ctx context.Context, req *dto.GetModelConfigReq) (res *dto.GetModelConfigRes, err error) {
cfg := service.ConfigService.Get(ctx)
return &dto.GetModelConfigRes{ModelConfig: cfg}, nil
return service.ConfigService.GetResponse(ctx), nil
}
func (c *config) Save(ctx context.Context, req *dto.SaveModelConfigReq) (res *struct{}, err error) {
@@ -28,21 +26,5 @@ func (c *config) Save(ctx context.Context, req *dto.SaveModelConfigReq) (res *st
if req.MinSingleDuration > req.MaxSingleDuration {
return nil, fmt.Errorf("单次生成最小时长不能大于最大时长")
}
cfg := &entity.ModelConfig{
ChatProvider: req.ChatProvider,
ChatApiKey: req.ChatApiKey,
ChatBaseUrl: req.ChatBaseUrl,
ChatModelName: req.ChatModelName,
MaxTokens: req.MaxTokens,
Temperature: req.Temperature,
ChatSchema: req.ChatSchema,
VideoProvider: req.VideoProvider,
VideoApiKey: req.VideoApiKey,
VideoBaseUrl: req.VideoBaseUrl,
VideoModelName: req.VideoModelName,
MinSingleDuration: req.MinSingleDuration,
MaxSingleDuration: req.MaxSingleDuration,
VideoSchema: req.VideoSchema,
}
return nil, service.ConfigService.Save(ctx, cfg)
return nil, service.ConfigService.Save(ctx, req)
}
+7 -36
View File
@@ -2,8 +2,6 @@ package dao
import (
"context"
"encoding/json"
"video-factory/shortdrama/consts/public"
"video-factory/shortdrama/model/entity"
@@ -19,27 +17,28 @@ func init() {
ctx := context.Background()
if _, err := g.DB().Exec(ctx, `CREATE TABLE IF NOT EXISTS `+public.TableNameModelConfig+` (
id INTEGER PRIMARY KEY AUTOINCREMENT,
chat_provider TEXT NOT NULL DEFAULT '',
chat_api_key TEXT NOT NULL DEFAULT '',
chat_base_url TEXT NOT NULL DEFAULT '',
chat_model_name TEXT NOT NULL DEFAULT '',
max_tokens INTEGER NOT NULL DEFAULT 4096,
temperature REAL NOT NULL DEFAULT 0.8,
chat_schema TEXT NOT NULL DEFAULT '',
video_provider TEXT NOT NULL DEFAULT '',
video_api_key TEXT NOT NULL DEFAULT '',
video_base_url TEXT NOT NULL DEFAULT '',
video_model_name TEXT NOT NULL DEFAULT '',
max_single_duration INTEGER NOT NULL DEFAULT 15,
min_single_duration INTEGER NOT NULL DEFAULT 5,
video_schema TEXT NOT NULL DEFAULT '',
video_task_callback_url TEXT NOT NULL DEFAULT '',
created_at DATETIME,
updated_at DATETIME
)`); err != nil {
g.Log().Warningf(ctx, "创建模型配置表失败: %v", err)
}
// 迁移:清理孤儿字段
// 清理旧字段
for _, col := range []string{
"chat_provider", "video_provider",
"video_no_duration_support",
"video_model_category", "image_model_name",
"chat_params", "video_params",
@@ -53,12 +52,11 @@ func init() {
g.Log().Debugf(ctx, "删除孤儿列 %s 失败(可能已删除): %v", col, err)
}
}
// 迁移:补充新字段
// 补充新字段
for _, col := range []string{
"chat_provider",
"video_provider",
"chat_schema",
"video_schema",
"video_task_callback_url",
} {
if _, err := g.DB().Exec(ctx, `ALTER TABLE `+public.TableNameModelConfig+` ADD COLUMN `+col+` TEXT NOT NULL DEFAULT ''`); err != nil {
g.Log().Debugf(ctx, "添加列 %s 失败(可能已存在): %v", col, err)
@@ -76,12 +74,7 @@ func (d *modelConfigDao) GetFirst(ctx context.Context) (res *entity.ModelConfig,
return nil, nil
}
res = new(entity.ModelConfig)
if err := r.Struct(&res); err != nil {
return nil, err
}
// SQLite TEXT → *json.RawMessage 反序列化
res.ChatSchema = textToJSONPtr(r["chat_schema"].String())
res.VideoSchema = textToJSONPtr(r["video_schema"].String())
err = r.Struct(&res)
return
}
@@ -100,9 +93,6 @@ func (d *modelConfigDao) Save(ctx context.Context, data *entity.ModelConfig) err
delete(m, "id")
delete(m, "created_at")
delete(m, "updated_at")
// *json.RawMessage → SQLite TEXT 序列化
m["chat_schema"] = jsonPtrToText(data.ChatSchema)
m["video_schema"] = jsonPtrToText(data.VideoSchema)
_, err = g.DB().Model(public.TableNameModelConfig).Ctx(ctx).Data(m).Insert()
return err
}
@@ -119,22 +109,3 @@ func (d *modelConfigDao) UpdateField(ctx context.Context, field string, value in
_, err = g.DB().Model(public.TableNameModelConfig).Ctx(ctx).Data(g.Map{field: value}).Where("id", existing.Id).Update()
return err
}
// ==================== JSON 序列化/反序列化辅助 ====================
// textToJSONPtr 将 SQLite TEXT 转换为 *json.RawMessage
func textToJSONPtr(s string) *json.RawMessage {
if s == "" {
return nil
}
raw := json.RawMessage(s)
return &raw
}
// jsonPtrToText 将 *json.RawMessage 转换为 SQLite TEXT
func jsonPtrToText(j *json.RawMessage) string {
if j == nil {
return ""
}
return string(*j)
}
-81
View File
@@ -1,81 +0,0 @@
package adapter
import (
"context"
"fmt"
"time"
"github.com/cloudwego/eino/components/model"
)
// ==================== Provider Types ====================
// ChatProvider 对话模型供应商
type ChatProvider string
const (
ChatProviderQwen ChatProvider = "qwen" // 通义千问(使用Eino专用适配器)
ChatProviderDeepSeek ChatProvider = "deepseek" // DeepSeekOpenAI兼容协议)
ChatProviderDoubao ChatProvider = "doubao" // 豆包(OpenAI兼容协议)
ChatProviderGLM ChatProvider = "glm" // 智谱GLMOpenAI兼容协议)
ChatProviderErnie ChatProvider = "ernie" // 文心ERNIEOpenAI兼容协议)
ChatProviderOpenAI ChatProvider = "openai" // OpenAIGPT/o1等)
)
// ==================== Config ====================
// ChatModelConfig 对话模型适配器统一配置
type ChatModelConfig struct {
Provider ChatProvider // 模型供应商
ModelName string // 模型名称
APIKey string // API密钥
BaseURL string // API地址
MaxTokens int // 最大Token数
Temperature float32 // 温度参数
Timeout time.Duration // HTTP请求超时(0表示默认)
}
// ==================== Factory ====================
// NewChatModel 根据配置创建对应的对话模型适配器,返回 Eino ChatModel 接口
func NewChatModel(ctx context.Context, cfg *ChatModelConfig) (model.ChatModel, error) {
if cfg == nil {
return nil, fmt.Errorf("模型配置不能为空")
}
if cfg.APIKey == "" {
return nil, fmt.Errorf("APIKey 未配置")
}
if cfg.ModelName == "" {
return nil, fmt.Errorf("模型名称未配置")
}
if cfg.BaseURL == "" {
return nil, fmt.Errorf("API 地址未配置")
}
// 设置默认值
maxTokens := cfg.MaxTokens
if maxTokens <= 0 {
maxTokens = 4096
}
temperature := cfg.Temperature
if temperature <= 0 {
temperature = 0.8
}
adapterCfg := &ChatModelConfig{
Provider: cfg.Provider,
ModelName: cfg.ModelName,
APIKey: cfg.APIKey,
BaseURL: cfg.BaseURL,
MaxTokens: maxTokens,
Temperature: temperature,
Timeout: cfg.Timeout,
}
switch cfg.Provider {
case ChatProviderQwen:
return newQwenChatModel(ctx, adapterCfg)
default:
return newOpenAIChatModel(ctx, adapterCfg)
}
}
-37
View File
@@ -1,37 +0,0 @@
package adapter
import (
"context"
"fmt"
"net/http"
"github.com/cloudwego/eino-ext/libs/acl/openai"
"github.com/cloudwego/eino/components/model"
"github.com/gogf/gf/v2/util/gconv"
)
// newOpenAIChatModel 创建 OpenAI 兼容协议对话模型
// 适用于:DeepSeek、GLM(智谱)、ERNIE(文心)、Moonshot(Kimi)、Minimax、Stepfun(阶跃星辰)、
//
// Baichuan(百川)、GPT系列、Claude系列等所有兼容 OpenAI API 格式的模型
func newOpenAIChatModel(ctx context.Context, cfg *ChatModelConfig) (model.ChatModel, error) {
httpClient := http.DefaultClient
if cfg.Timeout > 0 {
httpClient = &http.Client{Timeout: cfg.Timeout}
}
config := &openai.Config{
APIKey: cfg.APIKey,
Model: cfg.ModelName,
BaseURL: cfg.BaseURL,
HTTPClient: httpClient,
MaxTokens: gconv.PtrInt(cfg.MaxTokens),
Temperature: gconv.PtrFloat32(cfg.Temperature),
}
client, err := openai.NewClient(ctx, config)
if err != nil {
return nil, fmt.Errorf("创建OpenAI兼容模型失败: %w", err)
}
return client, nil
}
-22
View File
@@ -1,22 +0,0 @@
package adapter
import (
"context"
"github.com/cloudwego/eino-ext/components/model/qwen"
"github.com/cloudwego/eino/components/model"
"github.com/gogf/gf/v2/util/gconv"
)
// newQwenChatModel 创建通义千问对话模型(使用 Eino 专用适配器)
func newQwenChatModel(ctx context.Context, cfg *ChatModelConfig) (model.ChatModel, error) {
config := &qwen.ChatModelConfig{
APIKey: cfg.APIKey,
Model: cfg.ModelName,
BaseURL: cfg.BaseURL,
MaxTokens: gconv.PtrInt(cfg.MaxTokens),
Temperature: gconv.PtrFloat32(cfg.Temperature),
Timeout: cfg.Timeout,
}
return qwen.NewChatModel(ctx, config)
}
+10 -15
View File
@@ -94,21 +94,16 @@ func parseInt(s string) (int, error) {
// ==================== Factory ====================
// NewVideoAdapter 根据供应商类型创建视频模型适配器
func NewVideoAdapter(provider VideoProvider, apiKey, baseURL, modelName string) VideoAdapter {
switch provider {
case VideoProviderDashScope:
return NewDashScopeAdapter(apiKey, baseURL, modelName)
case VideoProviderVolcano:
// NewVideoAdapter 根据模型名称自动识别供应商创建视频模型适配器
func NewVideoAdapter(apiKey, baseURL, modelName string) VideoAdapter {
modelLower := strings.ToLower(modelName)
baseURLLower := strings.ToLower(baseURL)
// 火山引擎(豆包视频生成)
if strings.Contains(modelLower, "doubao") || strings.Contains(modelLower, "seedo") ||
strings.Contains(baseURLLower, "volc") || strings.Contains(baseURLLower, "volcano") {
return NewVolcanoAdapter(apiKey, baseURL, modelName)
// 后续可在此添加:
// case VideoProviderKling:
// return NewKlingAdapter(apiKey, baseURL, modelName)
// case VideoProviderRunway:
// return NewRunwayAdapter(apiKey, baseURL, modelName)
// case VideoProviderPika:
// return NewPikaAdapter(apiKey, baseURL, modelName)
default:
return NewDashScopeAdapter(apiKey, baseURL, modelName)
}
// 默认使用通义万相(阿里云百炼)
return NewDashScopeAdapter(apiKey, baseURL, modelName)
}
+20 -17
View File
@@ -1,35 +1,38 @@
package dto
import (
"encoding/json"
"video-factory/shortdrama/model/entity"
"github.com/gogf/gf/v2/encoding/gjson"
"github.com/gogf/gf/v2/frame/g"
)
type GetModelConfigReq struct {
g.Meta `path:"/model" method:"get" tags:"模型配置" summary:"获取模型配置"`
*entity.ModelConfig
ChatSchema *gjson.Json `json:"chatSchema"`
VideoSchema *gjson.Json `json:"videoSchema"`
}
type GetModelConfigRes struct {
*entity.ModelConfig
ChatSchema *gjson.Json `json:"chatSchema"`
VideoSchema *gjson.Json `json:"videoSchema"`
}
type SaveModelConfigReq struct {
g.Meta `path:"/model" method:"post" tags:"模型配置" summary:"保存模型配置"`
ChatProvider string `json:"chatProvider" dc:"对话模型供应商(qwen/deepseek/doubao/glm/ernie/openai)"`
ChatApiKey string `v:"required" json:"chatApiKey" dc:"对话模型API密钥"`
ChatBaseUrl string `v:"required|url" json:"chatBaseUrl" dc:"对话模型API接口地址"`
ChatModelName string `v:"required" json:"chatModelName" dc:"对话模型名称"`
MaxTokens int `v:"required" json:"maxTokens" dc:"最大Token数"`
Temperature float64 `v:"required" json:"temperature" dc:"温度参数"`
ChatSchema *json.RawMessage `json:"chatSchema" dc:"对话模型schema"`
VideoProvider string `json:"videoProvider" dc:"视频模型供应商(dashscope/kling/runway),空则自动检测"`
VideoApiKey string `v:"required" json:"videoApiKey" dc:"视频模型API密钥"`
VideoBaseUrl string `v:"required|url" json:"videoBaseUrl" dc:"视频模型API接口地址"`
VideoModelName string `v:"required" json:"videoModelName" dc:"视频模型名称"`
MaxSingleDuration int `v:"required" json:"maxSingleDuration" dc:"单段最大时长"`
MinSingleDuration int `v:"required" json:"minSingleDuration" dc:"单段最小时长"`
VideoSchema *json.RawMessage `json:"videoSchema" dc:"视频生成模型schema"`
g.Meta `path:"/model" method:"post" tags:"模型配置" summary:"保存模型配置"`
ChatApiKey string `v:"required" json:"chatApiKey" dc:"对话模型API密钥"`
ChatBaseUrl string `v:"required|url" json:"chatBaseUrl" dc:"对话模型API接口地址"`
ChatModelName string `v:"required" json:"chatModelName" dc:"对话模型名称"`
MaxTokens int `v:"required" json:"maxTokens" dc:"最大Token数"`
Temperature float64 `v:"required" json:"temperature" dc:"温度参数"`
ChatSchema *gjson.Json `json:"chatSchema" dc:"对话模型schema"`
VideoApiKey string `v:"required" json:"videoApiKey" dc:"视频模型API密钥"`
VideoBaseUrl string `v:"required|url" json:"videoBaseUrl" dc:"视频模型API接口地址"`
VideoModelName string `v:"required" json:"videoModelName" dc:"视频模型名称"`
MaxSingleDuration int `v:"required" json:"maxSingleDuration" dc:"单段最大时长"`
MinSingleDuration int `v:"required" json:"minSingleDuration" dc:"单段最小时长"`
VideoSchema *gjson.Json `json:"videoSchema" dc:"视频生成模型schema"`
VideoTaskCallbackUrl string `json:"videoTaskCallbackUrl" dc:"视频生成任务回调地址"`
}
+16 -19
View File
@@ -1,27 +1,24 @@
package entity
import (
"encoding/json"
"github.com/gogf/gf/v2/os/gtime"
)
type ModelConfig struct {
Id int64 `orm:"id" json:"id" dc:"配置ID"`
ChatProvider string `orm:"chat_provider" json:"chatProvider" dc:"对话模型供应商(qwen/deepseek/doubao/glm/ernie/openai)"`
ChatApiKey string `orm:"chat_api_key" json:"chatApiKey" dc:"对话模型API密钥"`
ChatBaseUrl string `orm:"chat_base_url" json:"chatBaseUrl" dc:"对话模型接口地址"`
ChatModelName string `orm:"chat_model_name" json:"chatModelName" dc:"对话模型名称"`
MaxTokens int `orm:"max_tokens" json:"maxTokens" dc:"最大Token数"`
Temperature float64 `orm:"temperature" json:"temperature" dc:"温度参数"`
ChatSchema *json.RawMessage `orm:"chat_schema" json:"chatSchema" dc:"对话模型schema"`
VideoProvider string `orm:"video_provider" json:"videoProvider" dc:"视频模型供应商(dashscope/kling/runway)"`
VideoApiKey string `orm:"video_api_key" json:"videoApiKey" dc:"视频模型API密钥"`
VideoBaseUrl string `orm:"video_base_url" json:"videoBaseUrl" dc:"视频模型接口地址"`
VideoModelName string `orm:"video_model_name" json:"videoModelName" dc:"视频模型名称"`
MaxSingleDuration int `orm:"max_single_duration" json:"maxSingleDuration" dc:"单段最大时长"`
MinSingleDuration int `orm:"min_single_duration" json:"minSingleDuration" dc:"单段最小时长"`
VideoSchema *json.RawMessage `orm:"video_schema" json:"videoSchema" dc:"视频生成模型schema"`
CreatedAt *gtime.Time `orm:"created_at" json:"createdAt" dc:"创建时间"`
UpdatedAt *gtime.Time `orm:"updated_at" json:"updatedAt" dc:"更新时间"`
Id int64 `orm:"id" json:"id" dc:"配置ID"`
ChatApiKey string `orm:"chat_api_key" json:"chatApiKey" dc:"对话模型API密钥"`
ChatBaseUrl string `orm:"chat_base_url" json:"chatBaseUrl" dc:"对话模型接口地址"`
ChatModelName string `orm:"chat_model_name" json:"chatModelName" dc:"对话模型名称"`
MaxTokens int `orm:"max_tokens" json:"maxTokens" dc:"最大Token数"`
Temperature float64 `orm:"temperature" json:"temperature" dc:"温度参数"`
ChatSchema string `orm:"chat_schema" json:"chatSchema" dc:"对话模型schema"`
VideoApiKey string `orm:"video_api_key" json:"videoApiKey" dc:"视频模型API密钥"`
VideoBaseUrl string `orm:"video_base_url" json:"videoBaseUrl" dc:"视频模型接口地址"`
VideoModelName string `orm:"video_model_name" json:"videoModelName" dc:"视频模型名称"`
MaxSingleDuration int `orm:"max_single_duration" json:"maxSingleDuration" dc:"单段最大时长"`
MinSingleDuration int `orm:"min_single_duration" json:"minSingleDuration" dc:"单段最小时长"`
VideoSchema string `orm:"video_schema" json:"videoSchema" dc:"视频生成模型schema"`
VideoTaskCallbackUrl string `orm:"video_task_callback_url" json:"videoTaskCallbackUrl" dc:"视频生成任务回调地址"`
CreatedAt *gtime.Time `orm:"created_at" json:"createdAt" dc:"创建时间"`
UpdatedAt *gtime.Time `orm:"updated_at" json:"updatedAt" dc:"更新时间"`
}
+30 -54
View File
@@ -8,10 +8,12 @@ import (
"strings"
"video-factory/shortdrama/dao"
"video-factory/shortdrama/model/dto"
"video-factory/shortdrama/model/entity"
"github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/v2/os/gcache"
"github.com/gogf/gf/v2/util/gconv"
)
const cacheKeyConfig = "model_config"
@@ -21,7 +23,7 @@ type configService struct{}
var ConfigService = new(configService)
// Get 获取模型配置(优先走内存缓存
// Get 获取模型配置实体(供内部调用,schema 为 string 类型
func (s *configService) Get(ctx context.Context) *entity.ModelConfig {
if v, err := gcache.Get(ctx, cacheKeyConfig); err == nil && !v.IsNil() {
return v.Val().(*entity.ModelConfig)
@@ -38,67 +40,50 @@ func (s *configService) Get(ctx context.Context) *entity.ModelConfig {
return m
}
// Save 保存模型配置:先合并用户输入,再统一写入数据库
func (s *configService) Save(ctx context.Context, cfg *entity.ModelConfig) error {
if existing, _ := dao.ModelConfig.GetFirst(ctx); existing != nil {
// 已有配置:把现有值作为默认值填充到 cfg,当前请求携带的值优先
if cfg.ChatProvider == "" {
cfg.ChatProvider = existing.ChatProvider
}
if cfg.ChatApiKey == "" {
cfg.ChatApiKey = existing.ChatApiKey
}
if cfg.ChatBaseUrl == "" {
cfg.ChatBaseUrl = existing.ChatBaseUrl
}
if cfg.ChatModelName == "" {
cfg.ChatModelName = existing.ChatModelName
}
if cfg.VideoProvider == "" {
cfg.VideoProvider = existing.VideoProvider
}
if cfg.VideoApiKey == "" {
cfg.VideoApiKey = existing.VideoApiKey
}
if cfg.VideoBaseUrl == "" {
cfg.VideoBaseUrl = existing.VideoBaseUrl
}
if cfg.VideoModelName == "" {
cfg.VideoModelName = existing.VideoModelName
}
if cfg.MaxTokens == 0 && existing.MaxTokens > 0 {
cfg.MaxTokens = existing.MaxTokens
}
if cfg.Temperature == 0 && existing.Temperature > 0 {
cfg.Temperature = existing.Temperature
}
if cfg.MinSingleDuration == 0 && existing.MinSingleDuration > 0 {
cfg.MinSingleDuration = existing.MinSingleDuration
}
if cfg.MaxSingleDuration == 0 && existing.MaxSingleDuration > 0 {
cfg.MaxSingleDuration = existing.MaxSingleDuration
}
// GetResponse 获取模型配置 DTO(给 Controller 返回前端,schema 为 *json.RawMessage
func (s *configService) GetResponse(ctx context.Context) *dto.GetModelConfigRes {
cfg := s.Get(ctx)
res := &dto.GetModelConfigRes{ModelConfig: cfg}
gconv.Struct(cfg.ChatSchema, &res.ChatSchema)
gconv.Struct(cfg.VideoSchema, &res.VideoSchema)
return res
}
// Save 保存模型配置(接收 DTO,内部用 gconv.Struct 转换)
func (s *configService) Save(ctx context.Context, req *dto.SaveModelConfigReq) error {
existing, _ := dao.ModelConfig.GetFirst(ctx)
cfg := new(entity.ModelConfig)
if existing != nil {
gconv.Struct(existing, cfg)
}
// DTO → Entity(同名按字段名映射,schema 除外)
gconv.Struct(req, cfg)
// 特殊处理:*json.RawMessage → string
if req.ChatSchema != nil {
cfg.ChatSchema = req.ChatSchema.String()
}
if req.VideoSchema != nil {
cfg.VideoSchema = req.VideoSchema.String()
}
// 查询官方API获取推荐参数(直接在 cfg 上修改)
syncModelDurationFromAPI(ctx, cfg)
// 统一写入数据库
if err := dao.ModelConfig.Save(ctx, cfg); err != nil {
return err
}
_ = gcache.Set(ctx, cacheKeyConfig, cfg, 0)
return nil
}
// syncModelDurationFromAPI 查询模型API,获取模型支持的参数和时长范围
// 并自动更新模型配置中的各项参数
func syncModelDurationFromAPI(ctx context.Context, cfg *entity.ModelConfig) {
if cfg.VideoApiKey == "" || cfg.VideoBaseUrl == "" || cfg.VideoModelName == "" {
return
}
// ----- 视频模型参数默认值 -----
if cfg.MinSingleDuration <= 0 {
cfg.MinSingleDuration = 2
g.Log().Infof(ctx, "使用默认单段最小时长: 2 秒")
@@ -107,19 +92,15 @@ func syncModelDurationFromAPI(ctx context.Context, cfg *entity.ModelConfig) {
cfg.MaxSingleDuration = 15
g.Log().Infof(ctx, "使用默认单段最大时长: 15 秒")
}
// Temperature 默认值
if cfg.Temperature <= 0 {
cfg.Temperature = 0.85
g.Log().Infof(ctx, "使用默认 Temperature: 0.85")
}
// ----- 查询对话模型参数 -----
if cfg.ChatApiKey == "" || cfg.ChatBaseUrl == "" || cfg.ChatModelName == "" {
return
}
// 从 ChatBaseUrl 确定模型列表 API 地址
modelsURL := strings.TrimRight(cfg.ChatBaseUrl, "/") + "/models"
chatReq, err := http.NewRequestWithContext(ctx, "GET", modelsURL, nil)
if err != nil {
@@ -137,7 +118,6 @@ func syncModelDurationFromAPI(ctx context.Context, cfg *entity.ModelConfig) {
defer chatResp.Body.Close()
chatBody, _ := io.ReadAll(chatResp.Body)
var chatModelList struct {
Object string `json:"object"`
Data []struct {
@@ -155,21 +135,18 @@ func syncModelDurationFromAPI(ctx context.Context, cfg *entity.ModelConfig) {
return
}
// 根据模型名称模式推断 max_tokens 和 temperature(仅当用户未设置时)
chatModelName := strings.ToLower(cfg.ChatModelName)
if cfg.MaxTokens <= 0 {
cfg.MaxTokens = inferChatMaxTokens(chatModelName)
g.Log().Infof(ctx, "根据模型名称推断 max_tokens: %d(用户在表单中可手动修改)", cfg.MaxTokens)
}
if cfg.Temperature <= 0 {
cfg.Temperature = inferChatTemperature(chatModelName)
g.Log().Infof(ctx, "根据模型名称推断 Temperature: %.2f(用户在表单中可手动修改)", cfg.Temperature)
}
}
// inferChatMaxTokens 根据模型名推断合适的 max_tokens
func inferChatMaxTokens(modelName string) int {
switch {
case strings.Contains(modelName, "qwen3"):
@@ -199,7 +176,6 @@ func inferChatMaxTokens(modelName string) int {
}
}
// inferChatTemperature 根据模型名推断合适的 temperature
func inferChatTemperature(modelName string) float64 {
switch {
case strings.Contains(modelName, "qwen3"):
+7 -24
View File
@@ -232,11 +232,7 @@ func (s *dramaService) GenerateEpisode(ctx context.Context, dramaId, epId int64,
g.Log().Infof(ctx, "预加载引用数据: %d个演员, %d个场景, %d个道具, %d个引用",
len(genCtx2.Characters), len(genCtx2.Scenes), len(genCtx2.Props), len(genCtx2.OrderedRefs))
genCtx := agent.WithModelConfig(context.Background(), &agent.ModelConfig{
APIKey: modelCfg.ChatApiKey,
BaseURL: modelCfg.ChatBaseUrl,
})
genCtx = agent.WithDramaId(genCtx, d.Id)
genCtx := agent.WithDramaID(context.Background(), d.Id)
segDurs := calcSegDurs(d.EpisodeDuration, modelCfg)
numSegments := len(segDurs)
@@ -451,15 +447,12 @@ func (s *dramaService) generateSegment(ctx context.Context, d *entity.Drama, ep
segIdx, segDur, segStartTime, totalSegs int, modelCfg *entity.ModelConfig, feedback string,
genCtx *GenerationContext) (string, error) {
chatModel, err := agent.NewChatModel(ctx, &agent.ModelConfig{
chatCfg := &agent.ModelConfig{
ModelName: modelCfg.ChatModelName,
APIKey: modelCfg.ChatApiKey,
BaseURL: modelCfg.ChatBaseUrl,
MaxTokens: modelCfg.MaxTokens,
Temperature: float32(modelCfg.Temperature),
})
if err != nil {
return "", err
}
systemPrompt := s.buildSegPrompt(ctx, d, ep, segIdx, segDur, totalSegs, modelCfg)
@@ -501,15 +494,9 @@ func (s *dramaService) generateSegment(ctx context.Context, d *entity.Drama, ep
}
g.Log().Infof(ctx, "Agent动态调整 max_tokens=%d (输入约%d tokens)", effectiveMaxTokens, estimatedInputTokens)
}
chatModel, _ = agent.NewChatModel(ctx, &agent.ModelConfig{
ModelName: modelCfg.ChatModelName,
APIKey: modelCfg.ChatApiKey,
BaseURL: modelCfg.ChatBaseUrl,
MaxTokens: effectiveMaxTokens,
Temperature: float32(modelCfg.Temperature),
})
chatCfg.MaxTokens = effectiveMaxTokens
reactAgent := agent.NewReActAgent(ctx, chatModel, agent.GetTools(), systemPrompt, 15)
reactAgent := agent.NewReActAgent(chatCfg, agent.GetTools(), systemPrompt, 15)
result, err := reactAgent.Run(ctx, userInput)
if err != nil {
return "", err
@@ -774,11 +761,7 @@ func (s *dramaService) FeedbackSegment(ctx context.Context, taskId int64, feedba
go func() {
genCtx := context.Background()
genCtx = agent.WithModelConfig(genCtx, &agent.ModelConfig{
APIKey: modelCfg.ChatApiKey,
BaseURL: modelCfg.ChatBaseUrl,
})
genCtx = agent.WithDramaId(genCtx, d.Id)
genCtx = agent.WithDramaID(genCtx, d.Id)
g.Log().Infof(genCtx, "第%d集第%d段根据反馈重新生成", ep.Index, task.SegmentIdx+1)
if err := s.generateOneSegment(genCtx, d, ep, taskId, task.SegmentIdx, segDur, feedback, feedbackGenCtx); err != nil {
g.Log().Errorf(genCtx, "第%d集第%d段重新生成失败: %v", ep.Index, task.SegmentIdx+1, err)
@@ -982,7 +965,7 @@ func (s *dramaService) pollPendingVideos(ctx context.Context) {
// pollVideoTaskOnce 单次查询视频任务状态(使用视频适配器)
func (s *dramaService) pollVideoTaskOnce(ctx context.Context, modelCfg *entity.ModelConfig, taskId string) (string, error) {
videoAdapter := adapter.NewVideoAdapter(adapter.VideoProvider(modelCfg.VideoProvider), modelCfg.VideoApiKey, modelCfg.VideoBaseUrl, modelCfg.VideoModelName)
videoAdapter := adapter.NewVideoAdapter(modelCfg.VideoApiKey, modelCfg.VideoBaseUrl, modelCfg.VideoModelName)
res, err := videoAdapter.Query(ctx, taskId)
if err != nil {
@@ -1017,7 +1000,7 @@ func getVideoAdapter(ctx context.Context) (adapter.VideoAdapter, error) {
if modelCfg.VideoApiKey == "" || modelCfg.VideoBaseUrl == "" || modelCfg.VideoModelName == "" {
return nil, fmt.Errorf("视频模型未配置")
}
return adapter.NewVideoAdapter(adapter.VideoProvider(modelCfg.VideoProvider), modelCfg.VideoApiKey, modelCfg.VideoBaseUrl, modelCfg.VideoModelName), nil
return adapter.NewVideoAdapter(modelCfg.VideoApiKey, modelCfg.VideoBaseUrl, modelCfg.VideoModelName), nil
}
// submitVideoTask 提交视频合成任务(使用视频适配器)
+13 -15
View File
@@ -14,7 +14,6 @@ import (
"video-factory/shortdrama/dao"
"video-factory/shortdrama/model/entity"
"github.com/cloudwego/eino/schema"
"github.com/gogf/gf/v2/database/gdb"
"github.com/gogf/gf/v2/frame/g"
)
@@ -166,27 +165,26 @@ func (s *dramaService) GenerateScript(ctx context.Context, dramaId int64, episod
return
}
// 创建聊天模型
chatModel, err := agent.NewChatModel(ctx, &agent.ModelConfig{
systemPrompt := PromptService.GetScriptGenerationPrompt(ctx)
userInput := s.buildScriptGenUserInput(d, episodeTitle, description, genCtx)
chatCfg := &agent.ModelConfig{
ModelName: modelCfg.ChatModelName,
APIKey: modelCfg.ChatApiKey,
BaseURL: modelCfg.ChatBaseUrl,
MaxTokens: modelCfg.MaxTokens,
Temperature: float32(modelCfg.Temperature),
Timeout: 3 * time.Minute,
}
messages := []*agent.ChatMessage{
{Role: agent.RoleSystem, Content: systemPrompt},
{Role: agent.RoleUser, Content: userInput},
}
result, err := agent.CallChatModel(ctx, chatCfg, &agent.ChatRequest{
Messages: messages,
MaxTokens: chatCfg.MaxTokens,
})
if err != nil {
return
}
systemPrompt := PromptService.GetScriptGenerationPrompt(ctx)
userInput := s.buildScriptGenUserInput(d, episodeTitle, description, genCtx)
messages := []*schema.Message{
schema.SystemMessage(systemPrompt),
schema.UserMessage(userInput),
}
result, err := chatModel.Generate(ctx, messages)
if err != nil {
err = fmt.Errorf("生成脚本失败: %w", err)
return