From dbeea873a7a5b707400278bc11edb9bdfb0f17ae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BC=A0=E6=96=8C?= <259278618@qq.com> Date: Fri, 10 Jul 2026 14:36:04 +0800 Subject: [PATCH] 1 --- go.mod | 30 --- go.sum | 128 ------------- short_drama.db | Bin 86016 -> 86016 bytes shortdrama/agent/chat_model.go | 207 +++++++++++++++------ shortdrama/agent/context.go | 20 ++ shortdrama/agent/react_agent.go | 85 ++++----- shortdrama/agent/tools.go | 61 +----- shortdrama/agent/types.go | 55 ++++++ shortdrama/controller/config_controller.go | 22 +-- shortdrama/dao/model_config_dao.go | 43 +---- shortdrama/model/adapter/chat.go | 81 -------- shortdrama/model/adapter/chat_openai.go | 37 ---- shortdrama/model/adapter/chat_qwen.go | 22 --- shortdrama/model/adapter/video.go | 25 +-- shortdrama/model/dto/config_dto.go | 37 ++-- shortdrama/model/entity/model_config.go | 35 ++-- shortdrama/service/config_service.go | 84 +++------ shortdrama/service/drama_service.go | 31 +-- shortdrama/service/episode_service.go | 28 ++- 19 files changed, 369 insertions(+), 662 deletions(-) create mode 100644 shortdrama/agent/context.go create mode 100644 shortdrama/agent/types.go delete mode 100644 shortdrama/model/adapter/chat.go delete mode 100644 shortdrama/model/adapter/chat_openai.go delete mode 100644 shortdrama/model/adapter/chat_qwen.go diff --git a/go.mod b/go.mod index 3d46eed..50695dd 100644 --- a/go.mod +++ b/go.mod @@ -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 diff --git a/go.sum b/go.sum index ee8efec..57341e6 100644 --- a/go.sum +++ b/go.sum @@ -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= diff --git a/short_drama.db b/short_drama.db index 6547712ff9ac1b3f8865da2398653e809a9d0849..2e742e4623299a348f85900aa02e3faf2878a393 100644 GIT binary patch literal 86016 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 delta 6359 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 SSWTs_rb=Vd>-9d{zx)>|P%Iz- diff --git a/shortdrama/agent/chat_model.go b/shortdrama/agent/chat_model.go index 3699ff4..2241e29 100644 --- a/shortdrama/agent/chat_model.go +++ b/shortdrama/agent/chat_model.go @@ -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 } diff --git a/shortdrama/agent/context.go b/shortdrama/agent/context.go new file mode 100644 index 0000000..a5e241b --- /dev/null +++ b/shortdrama/agent/context.go @@ -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 +} diff --git a/shortdrama/agent/react_agent.go b/shortdrama/agent/react_agent.go index bf84e05..881c230 100644 --- a/shortdrama/agent/react_agent.go +++ b/shortdrama/agent/react_agent.go @@ -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) + }) } } diff --git a/shortdrama/agent/tools.go b/shortdrama/agent/tools.go index 44c1710..ff83354 100644 --- a/shortdrama/agent/tools.go +++ b/shortdrama/agent/tools.go @@ -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] diff --git a/shortdrama/agent/types.go b/shortdrama/agent/types.go new file mode 100644 index 0000000..3fe6a67 --- /dev/null +++ b/shortdrama/agent/types.go @@ -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" +) diff --git a/shortdrama/controller/config_controller.go b/shortdrama/controller/config_controller.go index a997141..cbaac5a 100644 --- a/shortdrama/controller/config_controller.go +++ b/shortdrama/controller/config_controller.go @@ -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) } diff --git a/shortdrama/dao/model_config_dao.go b/shortdrama/dao/model_config_dao.go index b28460c..0b7183b 100644 --- a/shortdrama/dao/model_config_dao.go +++ b/shortdrama/dao/model_config_dao.go @@ -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) -} diff --git a/shortdrama/model/adapter/chat.go b/shortdrama/model/adapter/chat.go deleted file mode 100644 index dba5aa0..0000000 --- a/shortdrama/model/adapter/chat.go +++ /dev/null @@ -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" // DeepSeek(OpenAI兼容协议) - ChatProviderDoubao ChatProvider = "doubao" // 豆包(OpenAI兼容协议) - ChatProviderGLM ChatProvider = "glm" // 智谱GLM(OpenAI兼容协议) - ChatProviderErnie ChatProvider = "ernie" // 文心ERNIE(OpenAI兼容协议) - ChatProviderOpenAI ChatProvider = "openai" // OpenAI(GPT/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) - } -} diff --git a/shortdrama/model/adapter/chat_openai.go b/shortdrama/model/adapter/chat_openai.go deleted file mode 100644 index f61b323..0000000 --- a/shortdrama/model/adapter/chat_openai.go +++ /dev/null @@ -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 -} diff --git a/shortdrama/model/adapter/chat_qwen.go b/shortdrama/model/adapter/chat_qwen.go deleted file mode 100644 index 2b4b730..0000000 --- a/shortdrama/model/adapter/chat_qwen.go +++ /dev/null @@ -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) -} diff --git a/shortdrama/model/adapter/video.go b/shortdrama/model/adapter/video.go index 351cc51..743c8d6 100644 --- a/shortdrama/model/adapter/video.go +++ b/shortdrama/model/adapter/video.go @@ -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) } diff --git a/shortdrama/model/dto/config_dto.go b/shortdrama/model/dto/config_dto.go index 0cadc30..ddc8551 100644 --- a/shortdrama/model/dto/config_dto.go +++ b/shortdrama/model/dto/config_dto.go @@ -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:"视频生成任务回调地址"` } diff --git a/shortdrama/model/entity/model_config.go b/shortdrama/model/entity/model_config.go index b2929fd..7c871ec 100644 --- a/shortdrama/model/entity/model_config.go +++ b/shortdrama/model/entity/model_config.go @@ -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:"更新时间"` } diff --git a/shortdrama/service/config_service.go b/shortdrama/service/config_service.go index 4d65e85..bc0a3b8 100644 --- a/shortdrama/service/config_service.go +++ b/shortdrama/service/config_service.go @@ -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"): diff --git a/shortdrama/service/drama_service.go b/shortdrama/service/drama_service.go index 6f5310b..364023d 100644 --- a/shortdrama/service/drama_service.go +++ b/shortdrama/service/drama_service.go @@ -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 提交视频合成任务(使用视频适配器) diff --git a/shortdrama/service/episode_service.go b/shortdrama/service/episode_service.go index 292462e..596d063 100644 --- a/shortdrama/service/episode_service.go +++ b/shortdrama/service/episode_service.go @@ -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