From 1b08bd64ddd0a9fd43e075683fdf9be0e9f3e293 Mon Sep 17 00:00:00 2001 From: Codex Date: Fri, 9 Oct 2026 16:42:58 +0800 Subject: [PATCH 1/2] =?UTF-8?q?feat(protocol):=20=E5=8F=AF=E7=BE=8E=20KM?= =?UTF-8?q?=20Seedream=20-=20=E6=8E=A5=E5=85=A5=E5=9B=BE=E7=89=87=E7=94=9F?= =?UTF-8?q?=E6=88=90=E4=B8=8E=E7=BC=96=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../km-seedream-size-presets.jpg | Bin 0 -> 69662 bytes .../internal/app/model_capability_defaults.go | 13 + .../internal/app/model_capability_kemei.go | 29 + .../app/model_capability_kemei_test.go | 23 + .../internal/app/model_capability_validate.go | 6 +- .../protocol/km_kemei_seedream_test.go | 105 ++ docs/content/docs/backend/code-map.mdx | 7 + docs/content/docs/backend/http-api.mdx | 4 + docs/content/docs/overview/features.mdx | 6 + docs/content/docs/progress/pending-test.mdx | 6 + .../km-kemei-seedream.yingce-plugin | Bin 0 -> 16469 bytes plugin-packages/km-kemei-seedream/README.md | 9 + .../km-kemei-seedream/docs/interface.md | 1525 +++++++++++++++++ .../km-kemei-seedream/manifest.json | 1459 ++++++++++++++++ web/src/components/image-size-picker.tsx | 3 +- .../components/image-size-presets-editor.tsx | 3 +- web/src/lib/image-resolution-tiers.ts | 4 +- web/src/lib/image-size-presets.ts | 16 +- web/src/lib/kemei-image-presets.ts | 17 + web/src/lib/model-capabilities.ts | 16 +- web/src/services/api/logical-models.ts | 9 +- web/test/channel-model-editor-form.test.ts | 7 + web/test/image-size-presets.test.tsx | 13 + web/test/model-capabilities.test.ts | 18 +- 24 files changed, 3277 insertions(+), 21 deletions(-) create mode 100644 .github/pr-screenshots/km-seedream-size-presets.jpg create mode 100644 backend/internal/app/model_capability_kemei.go create mode 100644 backend/internal/app/model_capability_kemei_test.go create mode 100644 backend/internal/protocol/km_kemei_seedream_test.go create mode 100644 plugin-packages/km-kemei-seedream.yingce-plugin create mode 100644 plugin-packages/km-kemei-seedream/README.md create mode 100644 plugin-packages/km-kemei-seedream/docs/interface.md create mode 100644 plugin-packages/km-kemei-seedream/manifest.json create mode 100644 web/src/lib/kemei-image-presets.ts diff --git a/.github/pr-screenshots/km-seedream-size-presets.jpg b/.github/pr-screenshots/km-seedream-size-presets.jpg new file mode 100644 index 0000000000000000000000000000000000000000..7005b378b98ff7d02a681345f75d478baf93ccef GIT binary patch literal 69662 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

f0C<=_T_ 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

UJAs#bR{=Dd^*m<E}ah-4M z$B%=0D|)5fPA@O!^--(I#=co4{S7H@>Yn!#GKGQu_b=CB{9BKmW8;rLv8~V~0FjOP ziS-*i<2PKM4&2()IG^1#JR?~V+1ibNlbyz<==gH)eIQcg$uV8MTo<8hMcHjqmmBG| z@rr|Hdg=mV``z!PAiy*C2WvgeM(A|b=I)ocKl9;Qi zBvgLuEgF+-yeVFw{!P1!N(h(m08-%S?h(oj3mZzYzc~W0T$8TlyJ`!pkY2(`7qJwv zxUS8{_uc;U%J=rPv{pv|VkYN&L%V*jf@DDjH^n^h%g9jFVK4XglAkX`;*4Wcc&*Eg7X^Y%{#Gb2rh6sxL9Rde|2eP@ltlTd{?)5DQ=*+ z*pE!MUOBfI5Gd*BO>OxHfyK@Ke%i;)v(c7#)z><2&fyuFS&4Io_J zSepK&h%Q{x#!sPC`j(B^?7q!vj{C8dC(rJD}G3#EF zMy!SvBkhkkN_$Wot~*K;X4N1gJzwVbNU$QZS%%C@7O089a?ggEtc|LC$|T;_I?{*P zjG$^=R4@o;jT@{HkS)(tHBWrDU|O(98BBZ~|5}7bE_=2XqhGrDEvDwvQ)ICv43TM0 z=uhyBUT&jKH)aG1{I++)7{X{>7iGxl7Lb~gOw2y#+`*@FM&T9PDPk8K*blm4~4T6`mcCqp1No zt3BhdD&Fk!(@m>$42~kd9oaw|LYfu1}G?HXav!PiEKe}#S*{eO>n5zD} zf`V_@>z{fRFi7i$i8{nGR|I$jk`OnNTat))ba>UJstnghy8dp|w4f%`p|(Jp*u~5f zzvNC%0)9a2^yU$=#z^ zGh9Bjj<5O?yFgPDxrQiRKPnqHzJHVrx#hW=Mn}Lkr};#3cjFOZqS{RC1yWFEr>nBI z(Ox&>J@zUMQWu@oM)%VUNyEN%H>@Sm4?1();XbnxJ)WTfE>3yv^SEJn_;T6_vV?0+ zdCvL=d^~W{O(V%Kb?yz(JoX#zw&fK0yaBu;f-~k!fw?CNZ)GHTwBfa9NBik#GaD0J zpXxvq9}T$+G+TYM3W74clE9Q-vzHX<3u|{{zKbCoKazRy5%o5QHKqV%{l1w)k+b*e zdVC9efHxvynDR9wcC;Sig-UBR(Y}*%aML|GRkS2qH*hD2~6$PXb zkPf6K3?d>q=cJNGk_z20ga)B$ng*KOWK=-1Eh`!R9Ipdsr=Dv6S_niB_ z_x*2uAM~!;ReSGRd)2O8Yp?bDVc=X$PC9K{ZX>H#oB&CIKy(E;J5eSB-WZ7U`N>+m zSW=4!d(^Uhal(TuS9%oBdRp~!8+<7zk=loJDF|z&Cb5G@2?KVb)>-RC8}+5z--`H4 zFs0AD4d>Pp@LL(5E(WcM-}+&fX5ojG0_BK5%$~0{%-QgzJ=(vsB+etv=GKBZtxMnU z?tBGJ^Z{NC?l%OS2 zFU{sy^RL`oI^9QOA5_!)$vf4&=#UpU!j0Wrh#C1)Qdl#eE7%7r2RF(rQ6l7 zaXYr?@VuAzQmu2MZ!=J;V&nEh%DaNCZ@JaK=XU&Yf2U7MDOExZyKbTFvwBivlCUx9@e} zU#DJplu2xPpA23o?<#$AlnB!+I)$(;Rbo;^VaDWyLK1~;7T(b`>$%}qO*bP9Ypb!R zj=@~>W@$*iYOG^<_EG}izw!I_7hd4Ym!iYeTy;1Gg#;WrC&3C2i6TZH84Wg~KEx!w z873NnjN&4B1?;M4% za=0VWQeAt0j_V+nS==daq-zyi==U0UE&MWADAX7}4CWBCbn)rlQQ7LtA7_Jm7+hdi zlk9*&joV?S{?LRl0)X<87ob+)HAUb{Gf>)dp=#SMnW`k9LWWh}-^Pt)}oyCt4` z7{)S;_V(i!j?Kn##Kr3q8PJmkYOo=5OvY3>H$Bhlq-vlNyXf`2km>oD8ufhpeqm ze3R%dXLaDRGq2$;@;(xH;QMkD2F%ZzrkEzLD9;_Jcub zpOp={0#aRKR}-DhSy>b1F2yK}>58dIQbzP6O)Y}ljz_`E-ohp}JiUcECG4HsM&=ei z$!`hqsnGQbn~0c^`Pt)=!z3@F)1*n3AQWq>d)bIUTdUq7rYfOa+8UHz zrdvwnD4T~NgPny)Nlp_22|PZ2urB$#vlH`)VEYCSp@x(&Vd)i0ZeK>RXxZ@Iu(itc8$HaO}ES zM7}}!0>_y`SjRTF?p_2c2CQ3_0clSAr0xJ|SJID~PJwjCr5pV!RHCU2WLH;T7jKIewm4?)R?@`@Ni z>*CtCv0ebcceMY6fhycxuP$swI4(vJP0)41LOq!%p@~+F_VIclE6O{nv1TTN)gI#B z10TH(at20X2Nnk0{dF`e0K~61n_kQ-cWy4HOEZaamhsFE%*mEP+Tt zgt4m?^poUt8TI{9+R66GEl$1+x?e##eWQyNthDOws9y-B;Lcj|qN#-|-Z2ZA5xjpr zkk)+Ar*iMg(c6>jcWO8qrbkCdqR5MKiHHl1_9v>rsF>H~yVk`YTKYcO6d!Srfu60a zC=a!(aMcxncgi8z8wsPvUy|yD>^%61rpEFEVNhm_v99ZDE^{BkXj?L+lfI*R z8(^vv{3#iWtayyDozaY(B+J{Pu_gc}lTd@WryV@fmGG@&)1O)IGoToF@R)-h^y{&KRK(yUs^RFnrqUVLA!EYdQq{96RgB(G?l3O?kW9D zUT@5{<40twnYwmB+*`OSMSm{MXgb<;)?jBz4z`a~nK7n7&m@PslOc5&wvDMx04Y!L zMU=N8Jf_)-Gt5M4Nk8o}!$b#)tubI@b5149qguh!)?^CdB)8pH;gJEqr=*)YmS*yV zknZnG)w3{M^ddv}X&BTkbX=i7Cl1Y5MTq6W-JhG^9n4W%#J5M=jTvGLPt@k*^CkFi zFn{acp1J{?jrk>_qsxNaiT2t%o!iQ2CSd-#vocT~Hq$@Yi1ft;* z0@blBFUE9UB)N4h?&F1ExMK^5Feq~TUO7jcW2M!TUgI*xh5)3xLM36S$D&#oJEX-4GOK4R(^U2q^a(!ev0a18WaoP7YBo)GO|NC4ar&tG9$;M;M9DBR4mW#9iqi`kCkB7(l;?UgSs>wc|IKm_itI zVDC3Mz&jc`oF(>ww~&Q)??jTPV)+hYu+gn*)QFPKO6GC{Ff}_W(%iA;VJm}!bhmM| zZ?ENIST-PtIA49+0!E{bwV4hD27JzF9ctUd6x)!sSSt|%Mi;#c=FzB?>426w3ExKA z<;o0}=trX3YF%PYjHbIEz3*iA2%0o+$E)bhN-s8)Z)#n4ggPmwb@>Q=1?iv>+nm*i zJXlOnv7_)dp(-v~me{~(qzkc}6P@GHzyLx5Ym1o%wsS{6( zhl=`h``IIHg^y!>@m%Z zmt!8a7ZbI#iCR^p+l}g<_A4K!g(J`2c3dW>X+NH0)m*jWj!GOB5{V=u!0J$K+dGoF z5ZAfJ{&}TxwD|n}G9gIck_(Q_vF)C)mX*9w_jvkMR(M@Sp1HIwG6ot)s6%lXbbJ^! zEgg9m4)-RC0DxuRG#q+6q#(1L7ox20-Z34kHO_^6<`i)QC?T@_A}chXu}MP!W1i* zt<|DpbaG(fZk3kG72)qP#Ow9cDhmNfKXWg{wEO^G4UL}4%IuG)=(sws>ixof09^;Z zkX?0T=!#-)5=E6|Fmcn`0N#al76oOBm_Xv8m(Z2&+KI<1IiG_-?zwj81v_by$n8Qs ze>q)@t-6CQR9r_~;X^u0abuF1m>pQuY_zO8j9o|(1!d1G+W<3XeVTD^kuu+H-<&os zt0Aai6i{8pIZNdPohCeYs*58zaU|BEKQ_vxK+v2rG||V1u;%nZ?8ykyuE?$zRl4Z0 zuYI7=n9E}Es@Bmyz!;!yJNvN^b!X;b`DyvU&pel3FPNG3kt;BMnzaK-@nkiMZ)qGwKb^ffKyr(c?rmdpiY_n3zaz#D10Aq!ZyzV>L$f(kjNIZ{?l|%v z`WKxg(-Z_b9+ zwac}MV_UQkvS=t*Tt=WZ$jpGHRJl!P+b8?%K?FtJy8IQA(PhE|gX}wYZg|}~V-Z^b zG9cMvYQ8c83GAZ>8J3@u6XDtGNp zSr!JMTTQU`)kE7^lk9ME_(#UbHD~rW$rcyCRAmk|Y=)se8WYm%K)f(oUUF)9{^)=>ef!Xw3WL^ihV94PQ#ma zAQIK8nG)HJckv0(S6FPv)3mLyn@i;XbkNc(y;;B=~ZEltYlb{XfY!XALkHt*6W+j*PdIZ87#p43ho* zFc*;K`gq;^u?bs;cAaTexq2USv>&%YAATo~96e1Z6U1KVa^2Jt1eVLsOoF@h&n5K%rlmh02+qP?Cb)TzUw zVn}04Rl1TD0Q(`s6ZRMK4ecK$+=}d)`}J}DzJa#(YS<IK>-J=(6Lb~MD1@=U3 zRKkT8hRd<$b#-irLN=NEk* zZ*=o;#Wx%HrVOqUZ<92z#t!Xyt|7LQFP1mEr+gPVxCiTGY`Di(0A%6zf*~$588Cme zYga={dskQCogLxJ6W2Vsiu2M%f->XabK(fk3 z0kOPhKTu1`8M#e@m3ixDEd_PLyQ4Z%W+xOTy^howA`zqoT@v<I#%|yU)W_wwV$vH4wHGu{mCt5h+Azbw94UNv<2w`L^W z6{9HDV8*OrX0L43;%~komil{zuN>(P69jB{E>U1<9nm8pC%@C9f{qD_9uWr(V@xk| zR0+%8a=B3PD%;bAx*1?b#WpM5{Khoh(@swv?F3&D3v<}Tpp33Y^P_K_2mKI%_mAm) z8K36bi63W>a?%zGiKK$L-oxLQA_`?D_1`*mJ%sM$e0!V-M*6{a5FLx+@3>38TZNAX z|Ayhf{EwVy>UDhEl`avcJ114wv|QDV9ZdC8W}2@IhkONH6)4-2pUvi%*dk?Cne;{{ zd)UqdW4OV5L-2MPyEZ7!KH(B|O+|L%tJsxYc^7N6AOxb}GaA*{;_S|cygq3_l9^h6 zvSZvmMg8iVYW-i>{*%S{T3)r=3o!?16WCj{JEfep=y1q1DI+l3=v_NgU0H_t?G3Y= z5NR5k`Y;9fd`!WZsTV;(beBYUy@U6_u?aOW>PH(eY13YOrEC+nHs=2;(iflJ#1A3n z=a2V_Mv6RkxSFaj{qyTMDhT^4jjMb%)QmuoIEb1WbmZX!&`&>s{%Qww%ouP{2TlNG zRtT7@xwZ={kU86ubv}$>xQgkF&UbUi!zN2JieF{pMIW}MJiC=z;%Qd#{0VaVNLR?S zifbK0bte1twmpp>z@Dcx>doXQS-_wp%zx_!z$`rUB-#lX3*|i>O5Mbx-dd2{AbO3- zG+N4s?}zcRSJFpD8klIdCglK7_5*zQA*ETqC2bh{nm70=6hkJA1XkvYVd zu_u{NQUaI}6aa7^pz(|R=6=ri5A}_`St<(P09^)h#|EC9Kmz#c$^e8ZvyHb!SO0)7$5U}ch^34w?Xo+?&5EK4x$E#w&U5=uUfKEIB!1ul`86DIA8=i+xGO* z-J?Q3lyFqi_+rNEq9dV2)}RL+AM6M2HGZ&1u24azvfC9f&X>jS?R?<8H8rN6(0{b; z-E>(e=Qz<_0h80+cW($iu=nsCvh=LR;*w2SIio*~*bsw8y-lhmV@jU&oL7>Ie!h@I#0zUdFSd+KQ!srB+bw3CG&mr(}jkJ zLD5xSGQD=7;)_N6;OhiPz2!YU6<~%|&lTh5H=Ca0BkN5ejXC9+stw<+AV?Dp(x%+v ztzS5#^RkLvQ8`XD|n>bMni7z`2{lQH`Ix-Y{08AFBU3^0Pcn4sBKyDp1hJO zt*7>i&2vS^St4Yh%88`%f#ODFL!fyDhN;Kyq}YeddKB)T>m64MYvLSdX{Zy4JhaQV zQI}DyYzB~rWaaNJQ$~Bs%JI(jQ0Duv$S_5hSY9@KXx#GEZvi_1Mv=jbA%|+gT)+~7 z7Zx6ZVY*E{a$yO*SGmna66bu;@hhTSfmt*8lPYe>r&m2~lSH2NaF-fMcxxN1mkx$d|i8v>@@v z4$w^08PUgl8hlTgGyvadMj%+>uiDA@%@YV15kVkumyt&8l4atcu?EISw*B$iA)i@G zRz^Lho$37dDB$TnTZfzN(Iwykyy5+ksCI>+HtY#EVZI+mwKlFPZz@wQ9%^J}loWCU z%MhtN0$H`VB&^;q+X314!zXIl53cD1L7(YDhPsSE9MFKtuno_2y1g(k49I3dC3d@>M2X(-if z?|tS_PKoHLo$Q*S;e}poaSj-pDI+RRYBx=le&K0o8|a(t<0z#REyjjfTX=>EZMu2Z z48dE(CCW~lC)-1^>!|gv^m3GYE|?qkkJkzza|v|J*5^iAtobPBA_<%=`yalqBmYTG z|N0%R-_gP(2U~v&2Y59O0woF<7>nP^KiQHb$vArp@LT#rQ>i|_{3Ck)<`EC=J4mLt z5ASMSyv+9?4&YS=eT$y|tA<8hY?Bl{`7O867?^=|aMnj>W+rNRGXj7_MI*F2=OJU_ z)10B-KsbFKJtaJvY0dKq-#KPV=beML6Hqrg$$S3CqK_UqlGr*-q&TRmgU@h~=_)_G zMc;IINRsX}A!6CDW_py+T+U|lm9y6Dxz@LRWHEs8E9kJkPt{Xz<<`FGr_y?5*GU?! zPlH#;QVW;aq@KVgyO5*0lOHJM`5$bnPYVo^ z1X&&|^!wguU&z1s>PIO7<4(6FttS7^KQVY873T@D#y=7}FO#Zr+>J0VOHp^b$)E+_ z219v5+_b?MY%)nyOZf^KmVX@X*unjC^9k|e@@z-n!yi8QE%5}6 zk=>lydk=(2{42KqOJf6LTpRC6x?TDBXcx;ic9{CLd`x?sb=4)xQH;7b6C%;|-fhZN z!~!r*V8&2o;eAI*?W_D8wr45VZIAF3-3_PkrQMVS(fuKTV@HqP^g4F#yl@`enj^ki zYx?3wtumKgUs^v^{%WFcY)QIp69r&5PTi!rV@QAeY?axZDw038-vF<5_jFGH*boj( z)SJA3u+iDTivbny-Q0qhJnnvS{Pw;h{$q5%KPvy8f)KxaIVWooDgG^y-kCTRda{ko z($}$UHg`0;PSL9(4y$NKX4kzO6}LQtp?Hh)3*9CPS=p+Sf2j)Wh*GYY?^K)WT3O~> zg2MFOQaLHBu@kI2(y=rCpWryVR4dMu$2koQbA9hxpXF&<;9x7)8R{F9&zGm({V@Gl zrrrxYkRejd5l}#yhUB)NJ&cdNP_m~y7cpknn@Lnljuy#%7JYtRV?x`u*-3kHn$~TK z20CXHc4CpyQz$QzeZ1don(sNjW5`(L&V;NSD;)}zYzMOq;Lff^9Bl0+dta&~>KWp6 zyRs?@P%=DB!KK>aJ}-n~Y+%#Ss`9YZNi}R?Jv?h_Tvz!iOV8kv$m9MhgLg-o#o#Fv zCzLYGK8|9nm;lDR+h~;a!BXA&KDg!>-9aw44TaUorXrrt2m-`4j(jEkuG&M6DBZ=d zf@JqAgE5_>7@cCC>f&7NO7+s#ZKL0aT9zVRaw!465;!slvBgXte@F z^Ybk+3n?R}<^v=(53fb_oHj!JHF5h0OS4CnCiod>IR*mH&nSgJPd6DYJ3e$2yDld~ zG{jdSo)Z+R2yAn44ke`7)%}U_zLc1|yLQ>-ZV9g40;D>+ffb1vSH~AbgR)to+_?xI zeY3GR*4)WOSt+~?43Sf%AS1-zEI*m}mYP!Y6|}`e6qb6jJbrV)@s5^sx|6@Z4lYr( zXs6l}i8p&=+a6COpLC7$ZFZ04Zn^!3>K%zxUJO{<*0`lV2cHYhWMbMF36TW;gu_fC zIh8Bh#P9a&#kc9$`ygzWuhnIZ-Y;j2X5nQ{bfW@Wms0mf2BzpeB#97(W91`A2Mm!T z<79A>D!oAYqLbSb3^sdjH{bPWd!ka6=B0RD1;>lGE8)qpjaudLpXlr%%ks-wWIcec z3Gi847izZ42OuEe@swpMm;dG{{eSb9XF*m`6rEkcagEx=$s_`tA9-=}9-8N-Z22BD zg7j!a;?e!#s5bR+t3JyyKON~Tn|n75`YX48F{I9mx|DG~rox50o>XJm??d-CrrEek zBfA$^qjKctykGW6jxnD8+0*BLbm!Icd=Wos_=AQ>+O{0-WjdRRZXXQRX)ccxOr7fW z=7wlpjKnZeX?S&T0YsdxA3XSx;Hu}q`w|uSE>z{BSsxMjHq}m7*b}kc{`hUq13-zq z`agMN(%H{ShHjus(v;4&oU5!ng1DPi%96aMaU-WM{tdDI#YbkVyxuM8FS%lP$&;Rb zs`t_dum5_01%AH85I1Xkh)}FMC#AQTVg3Ka3A_r;^pZZSX8D*CZP%*y`h4wcdKdZW z5bw86Xh`%F-MW=aTu&lR>WIl`sFh1h0o6n57n3yV-kb{8AL{4l@O5OIJX;Sa_Jcp} z`pgO1^NdeLa^M1;k3};pKr(iB`wK?;(F}fNZZrbA8fWi)CLJDkiUsZP&~AO1_zf=Y ze8AlM0wood-w>uQ{XN2z)cF&Su7hqcy?VuXtmx9QYoaHQoYAKG|K|Y^*l=S$@Zx87 zTVya#M4v?c!)nkG;0v$BG|7nliR4}0H^2M|bctpyi1nrG>WgpCB=4Udolkh3%U^u7 zHSWe8ts{JZN$v=MclSdF{?U~{>IjQ^++8EKZ#ZQ?C8;~9Gh)mItl$EeTPrO3tr|<| zN0Wx?6S48oX@#H6SD98W6&U(tf4`?@Y$VM3O4i^<9h_x=`r26{>-d z*7-o?e$SYD{W=vH?i>{c(uEnanbTkyfecZ($9GQmF38$#S5IKd<6(7ShjhsRhT{BQ zFG)wuV=+KnvZL|ItAINcUn8o%kY@WdCTRwG(*fymb9`r^eg@r+lZ~^S_M{CXuz0p^ zwB}4NAr$*?QUtd@6yXl6#>sn{pchP0T7m{qsRI`1cD(n8_z`?B8cT9E8S=E%$j^lnIO3mY9NZ*)v6w%+g*(hB_C7>9 z&xngSN!tj`ILuf}Y*~Jk-lQ?*vM+dM+(UFfq2W2_EYzHvHM#Ci+2RjPr#?7Y@D$~# zjfzAs!D2cMp{e&0EL24pvMb$9HyJnX?;QZ-uMVegp1MUo;P8Z~l@YXk5*yJRkDNGt+-6c419|($ z>D$qEx#Q+gPU+|rc)~c{mJWE2d>ZvnP;zqyp3&$kG2b-1w4f(2`vhfhxB4(d%xX5 zd$F9HYlCltq_R_>w%#fUJ=`ES+a+C(`N4#gK;1CIW4YSiVP$dE% zv73wM+a<19kuf9DTU0p=W|Ch%`W^yXi3G26&TV~NH8jLKTC;4nDqJ$itlh{W0 zX+7P&PgowiujHg0MTCtk%U*iS_u#bw6NJiZ@DL`Yg%i_6TU0UhXP3BhhrGZ+Qn74X%}B^>l-_5P|(Bo*^_z=0?$rvVZPaf zZhu0(+Wzoa=>FqPO5R~oI^yqvd_OryV5wjWEQxr^0zw#A9PY}W(H%#kVJ@bUea79Q z8(O;pL0O>BdKqS?usmpZ-JqYCnyj3`yG%Pc1~1>9O6=c5en>n{7c&@cC{z5lQ$&RZ zucy`L(ZNma7btdg_eH5QyRNXDh`${4QOk+8W_KOe5tPDQ*g-25fH%d>HSB4Q(KhK9 zEp%KV)6?1I`!!kX0|;j>Xv=dWW#uIaLx{VFG*`6x6yeu#!{x)tPB?%9C?WA>8CFmb z7U_<;-{KQT(Y6wE?UJ8y+2=a4)uO(wM3Ta@fS0S;i3>Y+9^uGoPxDY44q5k*s3?Ef zNKk4peBK4LUfRQ)hcY*%^xKjAb`}XXwP-6b0+_$^g-C>NPrt`vBC|)H-LvX)f}M@p6+lFUb-xi=OzxR7iLpOJmU}Kvzn4z>32}~Gu1WjORaDRE<_G$6YJbon3x*3t= zgpZ6b2x5t9h^4Mm!c4|)p(c)PL}g!y7th|2DKbfMe=(uZV0@S)P|SuvRj!9jvQ>Uo zmA6>DQyw2s)(Js|F66S(m1SsEk=J>6m<^bw>&5#=LCUfbwmBwd{n(kh*J2n*b4WxZ z+Nv~KK4(4gWd6tLbShmS4oNpmgyU&BmaV=!7wM)eL7U(7SyaKb#dcqaszZed-$l@< z&R#Ilv?eN+#^_)>!kJy`XSTY07>R0Aby;&z%*?$tVyBjRFHxGPA#Op>HkIzW_=*P> zo^CRfhIOX%W+WPT%$cWk4e*w#kSb*Awdqek;o)v#b`YPp8wQK#jvHpKBAbk>VfObJ zQNnb}TLP7Q6#W3byPyr^_GtCCgB87RJ7*Q55^o?M`zCuf^JQnMYz=N?R41mcJ;SxC zs4~VbK|Ib=45N&Qk8H62ETS;o#ykE%dEAjF0c+k@rzpoNJ21_Lz;ndMM@8w%FuS*$ zPBxF_V(4^)hKVGPjy`Uvqtzc%L+5^0R!Jy_yRPV!CW=qIqC(vYCEAvw7X{gy(Y$;I z95+Y)edE!eN&)d3fd16Q{c^0c0V406Nk8!%hg2T(@OCR_@9eGNvMQXX z4zir2OP1Xr(?TRnWI-EgKg?sU`2PS)bL;`@4S*_E>}Qr6alA}7X}{S+PXH*!e`>`K zJAa$_0(djIc@0!d#1O{3cqjmpuc1?d%OJlB&CR-2QF4Dcr=QAq*lbp@K|zvqkuJaF z+9)!9g}=^hP8_TGsB^krV=KR=tsz_YR<3f2Y`?>)I7tnz$Bb8hs0#GpsuoQvugU|v zF&O36tU(FP@%>6`uGW!yOvLjDSc~OY++)PpZ7y5meHui=2ag#cOx1ZMb6Ur8Wxo6U zzpccaPOW9h+V{s>PRPGZ%ShDprD5-Kl1QFh zK6<3~?09fmuy;{oW^L8s#H$O>;`}i)9jm(x?VB?n{XyXxTC0YM5;bq-L^=}3myfcu z3s#w%zF7NdtWey-yXeH-_9<<*+N!{b`YK?h8aU4AOf9MB{>As(k?$v-X>C*aUKaI? zuiHW0gX_F}AHSEy`MsVaC!^zkEz9Y9J;!dz{+BM7yqoa7o{;Zt_~-BEZrZPxf4`|$ z7t^l$@c(vaV_!jO-*5TPr~I=L`^6z`>GS46UC6IB{Ii$eZ4>)dnseU@`QJ)YQc~}? z?)kMd+R&T7_t*CWi~%R_rhPB!U(+7-TDE_#yZ=K2<50iqU-zeP`WIsLpKHtCU;g*X zCZP4JTFG6R`=-7Of2VB!yUYJxSCjaERchdw`4_0dU4cJ!4&&uG|MabY;@SJp6*1EH zSjG3dma6{s(L8hPqs2EVa29^6H@6Snoqqm3H7SQM)@pSb1w)@g8h$YAjO#&n@2t4fO`hYRULAy(DpzZ;H%<#i{;LHiJ@fVk*PL^Lg<3)zx#+P_7UF!-W03lfLo=}E$afTk)SqOolDgyCF7lSj+%efR ztG5>;WkTY5*ohtqlZ&@Qs2r11zPb}cJK^V_^?C6dF5W``7ca+Q`}c3p9%{~$u|Cfw zM`AL4%o^#dj+pmQL>pGG|9wh<@}iEgz&a#bvBNE#7@1G(uz8P%a=JV2-Vsdg2@u-W z1_;&4R<7v7Ybrg#^6d=P4gL%Pqz$vBpEq!`yD8W2Z)|@S^w99bXdfpcV>%TO(bGzt zKR1gfzu6MhQFq9f-J;a=_D9^3R+!%}$m4vyEd6FDdGs`6ll;JB<75DP)A}0g6uhQP z_6f^p;cafSo{GQ=#nFrX7q({J@bR z<3LYNZz;I-w~QlYvJtK@8(r~jVw4-0yp)em*iR+dcDzsKr!v@WAeHnp_o zk*!z7!+)-v|Ja~M@j8%t(e-v;^2||=KMg}`MHZ@MaR%+p?IvL+uo0q+KKzk= zvfgR=Pp8f;6$g*Zd<6k)T^*|7JRMV8&5|t)gt>DGHbN20fVO}3-+B9TdGjl%Ie6yD z@SU%q(vGhnfYvL)!eb-2bnP5^(TYdntyg&O-p3z`I#74uXdVLZ$CfXnI=OY{(`;JO5#RR93)N?Gx$$< zDV?X*kEcy)1xZSRPW}qu*xLHrU`c)T$g|)43x4+|pysTP1D^w(y5F9lqt%%WP2EH| z%BsO{M&n=g6(X~#S9koVcepBC4qBDMuf3hPP^-yrs=6gU`SD>tfRz4Dd@D*R!P2xy zDKa+@|13e5TO{F~6u}v8%j_6J|13`E_fh(!tmb{%P)qi!TA&9stx-3U!jEx?#EDtE z9ZkBCt1s~k@nAcRen`>xH^SBHsy;uERw*5oJfGfZ0fBJU1x?`#p7@$Xxf1DMuJ*;# zzktW=!AoNU<-H`ufrYwc3_&dUcHqcL@C97XV&KK1>_uc*F^0bjv743H@p|Px%l3+g zKh(p3wr7U?`4l4@5X){`r;g|eAhWT0D;8y)}dWj zUZIUzegZ!%_3m(c3C&~>hp3Ztt*D3XTU&%BY+{IPY%zp`RClVuZihFxxn<)Zq5`}4 zv#))4NVGoMy(H4AIInB=08w{g%~^N{TkWW`42?q!?GVJuD+?x0?t7cee@P+@R1k@j z+^r0bqU_xnZVXK6?1y3b_0J3@W~b(7*cNhQqpY;I9a2eh9%wU`Pt)9obyKn_f#gV+ z8)Iv4?GD454CJWy0|C!?>Rp#T>2M{(jxB0K32e2VZC_-aAP|GQuwO@is4+$E6oGom zjzYES#;vbSiCmsh7Ap{QReHgK%Ui?>4gz@ggSLj{^I2DJ!ukt^8l?FJVY*<``=0X$Z@)fkmr`WY@=#@m|OISpoF(6eg11wf45yyi9WMhPb_ z*keO~V?I12V-e}RO_GhNn=#SP5GJ`eb?rkhK5ftYSFo^J8epQkCmKMeoX zUp4w&%sD66!OPiO(aLt|N{5W+6}T@f$Ug3^yq!>&JF?JQK*?IQ%W))kv(I&MnyBVk zl5Drv%A(p?6Fem|OhN}TM3t3g7Ib)M8gz>Z$OXcUGL!w77(7aeX!lV@+=*8Qj^{u^ zG_KI*x5M)4-Cv_Vcm$lZXQ4z~6Lk<8%7D$OKk8crbL3{BGSR%!!}47att)!ZPkWpg za6^2U1Vb#R#HJ-`R3f@LNGo5``;sUP<)r&KdfVoS0+K<6VSFJ{7t-u66*ZzgDXaHkF^$ljPQxHKg|W7O5^xBC1~?BwmL9x zNowKcEstalQj-n0Y*(a#hoI&iSY9|*9nNSc!2JD;a(^>5Y!9)By?Wi4oRY^x1`zNCAGAI-C?QOX$ ztT_jnBUT^USA)b1XRo{}-n@&jT_+r$zYz_<33;D;#tu5oIT?vJQ9UJ-JT#cwQwTki zDh$h0bkp-aw{eYV_2G(O`M_G4GLmh!*tNEo}TU_!9 z-)P)6)1lu!Ni~utu@Xnrt6!<(9QBXUgL!#i1|X(+CS<`|1RX>c05=8THa zkW4N&Z(VG>N$rY9Zf^=^@blQ5;GN7dulu?phaaq)%4S&BOXFtN-?d$yNPkHc-^-Ev zQeE1lcVc;<1SeCMv#1w+b`KULwH8P)Dg{eFlVd5Eo7y?&fs!(wil)q!yfOW3Uic>X zVe(hds&K&Y-U-7Nw|F41ZcAWu zcot!X4*!*LVZ}flCp|CeFHRsOs;^aNxnzJwVDLejo*I^y`?*Rc}7+0i|SCb(G^LW z`Rdh>tbRYU<9fO!hNaPXa}{f7jJQ7xo}w)hXD?WY%Usyr;WogV7?4aG5OAyt9*DlJ zBbe6^`oh!Q3A)AzYy9h{^>elwFCcjXG zMy1M+k6yoz^RaQDFAwk4SQma^Q6?K%9^S*U!jMVIC_6rK9D1Yw=ud!mX`|28JFuI% z$f+6~F|{R6mQ7##6<2sX>Z-pgnP(F^B~kWyWBaj9vqbsuz=wpMbI+SIpGiH;w#aRB zRYK38kG%q&#IZ^KExI6dNMDKp%;9Tc85rk{Ek4~~mZR-3(Y6?7U2bLbk)tO=j-KGI5X>Q^-A8M#T;H*0zp zPWSkA+jlsu?}N134VdVwSNiN%FJ8QPjO})#WVF{27Q>4}YFFVGiL9-;R{>7SKNto} z=hCUK{Ect$lE^cXr?c*AjJ98g^kP$kcFzz45nTB3*{7>p6tBYlQ)l`;=Y*ABKGaxf zT3u`@ePUJbMC@CUr9@-!K4zve>sC6mkdGA|G#Poi;GZT97*o&5w^>W++bX)vSlaGh z-aYckA7!JQdQF08IDt|9vq2X+0=QuLc$G9b#I`5UPtZaUhEsj&j=e|BWC zMg6!imFc)Qu3W0LvhS3|hg+w%uj|#6)YJjekR8k&%%eljQwVIT=crB7SY_Ijy}WR( z-Z|TOx_Rbx^34a#$x(47`Taf}M*8jc5vp5uOC}lWCad#ihS)dOiY{TI+KkO8ShKSA zn~lM;rQBp)tP?gb&)x72j0i<7+;i;G$u3SC0(eO{6ip7<9Y-}*hhjXfoxutsHZXc$ z1IE3}09J0Z84$h9*l#hKa`U<1@$08UG?olOAY-N(8g?z5X!FA4?yrO3&Nt$8BL!2O|uoUe6d2^UZaQ&tPKJ&258!bnML<(YH15 zF3qf*3fqv-b_A-nYM!9!cXO=(YiH;^oKn@FnTexAoeJ&K*G>*VId0fo&rsnSSXQ+1 z%|rG|V~;l1MOFap1mn&)^L!gJU5pbkhYTpioUJ#{L7;Zm^`#3h@N*Y-o-!rJhoU*6 zliyRi*gp+Mga~_iisd$MQjqiTP-}$|qEb?IN_#0ONqI%9tx40y#&*MI-ha{D?VK^g zj)cPL{7@b5voE^OerX)8nty@M8hfW->YZ3tU?rv++n%ADoRx6`QqjU})wHg&`QDv; zg<1~mxGG}okq$E{E3ov0&d#X05@ht;W=g>{zZs!D zS|Yj@EGe6v9cH;Na-)BH2kt*8wlKOf&52^v?^2Qq@%gpiu7B6Di*LrpveeZtyKnft zy!?gQ{wt`GxMlHPBMWCBe#ReV%4r%Wn(CD~^-1QD$aDy)*9?{xQ!x6@fS_2mKqRoa z`$j$4PxL6onM7#HC=Pu@3kwVDc^a!PBc534eOQ*a*;Z<_(6g=l#aCc*CC6jU72mCg zBj|^PkIQhXdd%o(*x5O!pi?tS=y`=Uncx6d9(i0oU(YLs1@pZ>f#Kmp0Q>&*-{1ODP5;pR*03I zhA}j7vNyVRH~9L#N%>$Qp7{35jKU?5d-GParmmPF+V>C+p-enz2O|B_Xk?PSJJlWK=x2}5UmBr(`KV12tZYD&w7{Ur;a2}CsMuR&xiTHU{Vb?UbH=2> ze<9$x?M~9i(tFLTcDYlX%$2Mr5|rmxbmyDA`MFBRQOfn_(mjyMH$LvZxJYFqpnvrK z$p@g{gK*fKsiBM7Cr(zK&x%pRxU|9Z;&ASKrq5960JU(y87D^^=lxlwbgkIZ1);?i zoYMUn&vY$}`vMiAJbHyUX^5TQg+|ks>DFAs3kxYH^ycwb^?w*)K%j3dfg8!!H!TZLyC+-NpxmjyRN^;}PhR4zm-snqPUc3o# z!33Xv{!wdpfbi+r9L-TVOkrG{ zDb!w|?926{EDkYGMPAk4Yr{Sj9uZ`V(-(Mp>?Xi2q-OunGW@}o$x01T*GO}YqdZ9g z5qvzoXdIRMcwK&2@rIq;FAqPl&bKFR7?Z8>-99N%SG4@Nq^2?{ilP@kBWSMR%Hy(e zXnzr(sS^WbrHHr&#mlj0*z3QUxPKo|nFmeTUD-t+cIL6hOW)aaf+pOseV-Gecekf9 z#9_%{9fD=`?+pj5?#7#SSn6@DbF5E(C3W22*Q*6b~G%28e!OGi)3 zj(`P3WpDxhwm$~2p8;u6TU{uUwEU&jf$IF5644VSRVS zRK|+Z+pDC^)!nZ8X--n%>A!+DOAmyD7FZ|g@mD&nccl;e-;`Z@t|GM_)|Wopcfb*} z5HWcg$ijcml2(7Na%Jf>-s1ay?|$Qg;g)S+%oUH#y-PhjxSfc;!tWQ&AKvlE`&9O# zIkDQ;;pCCUU?c4Rn%2Ku{SC|C^+CSU-jppy=({4&DRLzI;7k}*p>*lI^`E;Ax z3AyEPKFqX3^LJI|PTa)45^6e-Zx1(kUUW)M<6y7|;X{y*_5;HM;v8lpi+U=m-QyxzgV(vtJk~P53U&U{0-^1nrP9O zq|+JWEg+_JVk+>e@FOQvnsUxL%ut>vRH{5_1RR$fLWGTrMDG4+~bs~_-tUEDT4Lo!OYzashF&q;B9#?Y=c-Oc=3IhFh@Mg@rv5NYLPg8CiwAR3=VgRPto;W$>8g eCT^6$vnZnrbY?}5q(h1_TL9?tc-(;T|4jf{zLD+# literal 0 HcmV?d00001 diff --git a/backend/internal/app/model_capability_defaults.go b/backend/internal/app/model_capability_defaults.go index b42acedf3..53f516960 100644 --- a/backend/internal/app/model_capability_defaults.go +++ b/backend/internal/app/model_capability_defaults.go @@ -27,6 +27,19 @@ func DefaultImageCapabilityConfig(protocol string, modelName string) *ImageCapab MaxOutputs: 15, } switch model.ChannelInterfaceType(protocol) { + case "km-kemei-seedream": + image.References.MaxImages = 10 + image.References.MaskSupported = false + presets := kemeiSeedreamImagePresets() + values := make([]string, 0, len(presets)) + for _, preset := range presets { + values = append(values, preset.Size) + } + image.Size = ImageSizeConfig{Parameter: "size", Values: values, Default: "2048x2048", AllowCustom: true, Presets: presets} + image.Quality = ImageQualityConfig{Supported: false, Values: []string{}, Default: "auto"} + // 透明背景需要带 alpha 的单张参考图,通过插件命名空间显式配置。 + image.TransparentBackground = VideoBooleanConfig{Supported: false, Default: false} + image.MaxOutputs = 1 case model.ChannelInterfaceGrokImage: image.References.MaxImages = 1 image.References.MaskSupported = false diff --git a/backend/internal/app/model_capability_kemei.go b/backend/internal/app/model_capability_kemei.go new file mode 100644 index 000000000..0b9cfdaba --- /dev/null +++ b/backend/internal/app/model_capability_kemei.go @@ -0,0 +1,29 @@ +package app + +import ( + "strconv" + "strings" +) + +// 供应商与 Seedream 5.0 官方文档的精确尺寸;档位不能由通用像素阈值推断。 +func kemeiSeedreamImagePresets() []ImageSizePreset { + ratios := []string{"1:1", "4:3", "3:4", "16:9", "9:16", "3:2", "2:3", "21:9"} + tiers := []struct { + name string + sizes []string + }{ + {"1k", []string{"1024x1024", "1152x864", "864x1152", "1424x800", "800x1424", "1248x832", "832x1248", "1568x672"}}, + {"1.5k", []string{"1536x1536", "1792x1344", "1344x1792", "2048x1152", "1152x2048", "1872x1248", "1248x1872", "2352x1008"}}, + {"2k", []string{"2048x2048", "2368x1776", "1776x2368", "2816x1584", "1584x2816", "2496x1664", "1664x2496", "3136x1344"}}, + } + result := make([]ImageSizePreset, 0, 24) + for _, tier := range tiers { + for index, size := range tier.sizes { + parts := strings.Split(size, "x") + width, _ := strconv.Atoi(parts[0]) + height, _ := strconv.Atoi(parts[1]) + result = append(result, ImageSizePreset{Tier: tier.name, Ratio: ratios[index], Width: width, Height: height, Size: size}) + } + } + return result +} diff --git a/backend/internal/app/model_capability_kemei_test.go b/backend/internal/app/model_capability_kemei_test.go new file mode 100644 index 000000000..87314eed6 --- /dev/null +++ b/backend/internal/app/model_capability_kemei_test.go @@ -0,0 +1,23 @@ +package app + +import "testing" + +func TestKemeiSeedreamDefaultsAreValidAndUseOfficialTiers(t *testing.T) { + for _, name := range []string{"doubao-seedream-5-0-pro-260628", "doubao-seedream-5-0-flash-260915"} { + profile := DefaultModelCapabilityConfigForModel("km-kemei-seedream", name).Image + if err := validateImageCapabilityConfig(profile); err != nil { + t.Fatal(err) + } + if profile.References.MaxImages != 10 || profile.References.MaskSupported || profile.MaxOutputs != 1 || profile.Size.Default != "2048x2048" || len(profile.Size.Presets) != 24 || profile.Quality.Supported || profile.TransparentBackground.Supported { + t.Fatalf("defaults: %#v", profile) + } + for _, preset := range profile.Size.Presets { + if preset.Tier == "4k" { + t.Fatal("unsupported 4K default") + } + if preset.Size == "2816x1584" && preset.Tier != "2k" { + t.Fatal("official 2K must not become 4K") + } + } + } +} diff --git a/backend/internal/app/model_capability_validate.go b/backend/internal/app/model_capability_validate.go index 36dc40361..d9b1b9faf 100644 --- a/backend/internal/app/model_capability_validate.go +++ b/backend/internal/app/model_capability_validate.go @@ -65,8 +65,8 @@ func validateImageCapabilityConfig(value *ImageCapabilityConfig) error { } seenPresets := make(map[string]bool) for _, preset := range value.Size.Presets { - if preset.Tier != "1k" && preset.Tier != "2k" && preset.Tier != "4k" { - return BadAuthRequest("图片分辨率档位仅支持 1K、2K、4K") + if preset.Tier != "1k" && preset.Tier != "1.5k" && preset.Tier != "2k" && preset.Tier != "4k" { + return BadAuthRequest("图片分辨率档位仅支持 1K、1.5K、2K、4K") } parts := strings.Split(preset.Ratio, ":") if len(parts) != 2 { @@ -416,6 +416,8 @@ func imageResolutionTier(quality string) string { switch strings.ToLower(strings.TrimSpace(quality)) { case "1k", "low": return "1k" + case "1.5k": + return "1.5k" case "2k", "medium": return "2k" case "4k", "high": diff --git a/backend/internal/protocol/km_kemei_seedream_test.go b/backend/internal/protocol/km_kemei_seedream_test.go new file mode 100644 index 000000000..c9d97213f --- /dev/null +++ b/backend/internal/protocol/km_kemei_seedream_test.go @@ -0,0 +1,105 @@ +package protocol + +import ( + "context" + "testing" +) + +func TestKemeiSeedreamRequestDefaultsAndReferences(t *testing.T) { + adapter := officialPackageAdapter(t, "km-kemei-seedream.yingce-plugin", "km-kemei-seedream") + for _, name := range []string{"doubao-seedream-5-0-pro-260628", "doubao-seedream-5-0-flash-260915"} { + request := GenerationRequest{Model: name, Prompt: "暖色书房中的橘猫", ImageCount: 1, Quality: "high"} + spec, err := adapter.BuildCreate(context.Background(), RequestContext{Request: request}) + if err != nil { + t.Fatal(err) + } + body := manifestTestBody(t, spec) + if spec.Path != "/v1/images/generations" || body["model"] != name || body["n"] != float64(1) || body["size"] != "2K" || body["response_format"] != "url" || body["output_format"] != "jpeg" || body["watermark"] != false { + t.Fatalf("defaults = %#v", body) + } + for _, key := range []string{"image", "quality", "resolution", "seed", "stream", "web_search", "sequential_image_generation"} { + if _, exists := body[key]; exists { + t.Fatalf("unexpected %s: %#v", key, body) + } + } + request.Images = []MediaReference{{URL: "https://example.com/b.png", Order: 2}, {DataURL: "data:image/png;base64,aA==", Order: 1}} + request.AspectRatio = "16:9" + spec, err = adapter.BuildCreate(context.Background(), RequestContext{Request: request}) + if err != nil { + t.Fatal(err) + } + body = manifestTestBody(t, spec) + images := body["image"].([]any) + if body["size"] != "2816x1584" || len(images) != 2 || images[0] != request.Images[1].DataURL { + t.Fatalf("references = %#v", body) + } + for _, size := range []string{"1K", "1.5K", "2K", "2352x1008", "2816x1584", "1536x1536"} { + request.AspectRatio = size + if _, err := adapter.BuildCreate(context.Background(), RequestContext{Request: request}); err != nil { + t.Fatalf("size %s: %v", size, err) + } + } + } +} + +func TestKemeiSeedreamRejectsUnsupportedAndOutOfRangeRequests(t *testing.T) { + adapter := officialPackageAdapter(t, "km-kemei-seedream.yingce-plugin", "km-kemei-seedream") + base := GenerationRequest{Model: "doubao-seedream-5-0-pro-260628", Prompt: "橘猫", ImageCount: 1, AspectRatio: "2048x2048"} + for _, tc := range []struct { + name string + change func(*GenerationRequest) + }{ + {"wrong model", func(r *GenerationRequest) { r.Model = "midjourney-1k" }}, + {"multiple outputs", func(r *GenerationRequest) { r.ImageCount = 2 }}, + {"empty prompt", func(r *GenerationRequest) { r.Prompt = " " }}, + {"too small", func(r *GenerationRequest) { r.AspectRatio = "512x512" }}, + {"4k", func(r *GenerationRequest) { r.AspectRatio = "4K" }}, + {"too large", func(r *GenerationRequest) { r.AspectRatio = "2880x2880" }}, + {"bad dimensions", func(r *GenerationRequest) { r.AspectRatio = "2048x2048x1" }}, + {"mask", func(r *GenerationRequest) { + r.Images = []MediaReference{{URL: "https://example.com/a.png", Role: "mask"}} + }}, + {"too many references", func(r *GenerationRequest) { + for i := 0; i < 11; i++ { + r.Images = append(r.Images, MediaReference{URL: "https://example.com/a.png"}) + } + }}, + {"oversize reference", func(r *GenerationRequest) { + r.Images = []MediaReference{{URL: "https://example.com/a.png", Metadata: map[string]any{"bytes": 30*1024*1024 + 1}}} + }}, + {"unsupported format", func(r *GenerationRequest) { + r.ProviderOptions = map[string]map[string]any{"km-kemei-seedream": {"output_format": "webp"}} + }}, + {"group generation", func(r *GenerationRequest) { + r.ProviderOptions = map[string]map[string]any{"km-kemei-seedream": {"sequential_image_generation": "auto"}} + }}, + {"transparent text", func(r *GenerationRequest) { + r.ProviderOptions = map[string]map[string]any{"km-kemei-seedream": {"background": "transparent", "output_format": "png"}} + }}, + } { + t.Run(tc.name, func(t *testing.T) { + request := base + tc.change(&request) + if _, err := adapter.BuildCreate(context.Background(), RequestContext{Request: request}); err == nil { + t.Fatal("invalid request accepted") + } + }) + } +} + +func TestKemeiSeedreamLayerResultsAndErrors(t *testing.T) { + adapter := officialPackageAdapter(t, "km-kemei-seedream.yingce-plugin", "km-kemei-seedream") + request := GenerationRequest{Model: "doubao-seedream-5-0-flash-260915", AspectRatio: "auto", Images: []MediaReference{{URL: "https://example.com/source.png"}}, ProviderOptions: map[string]map[string]any{"km-kemei-seedream": {"layer_decomposition": true, "output_format": "png"}}} + spec, err := adapter.BuildCreate(context.Background(), RequestContext{Request: request}) + if err != nil || manifestTestBody(t, spec)["size"] != "auto" { + t.Fatalf("layer request: %#v %v", spec, err) + } + result, err := adapter.ParseCreate(context.Background(), []byte(`{"data":[{"url":"https://example.com/base.png","z_index":0},{"b64_json":"aA==","z_index":1}],"usage":{"generated_images":2}}`)) + if err != nil || result.Status != StatusSucceeded || result.Result == nil || len(result.Result.Images) != 2 { + t.Fatalf("results: %#v %v", result, err) + } + result, err = adapter.ParseCreate(context.Background(), []byte(`{"error":{"code":"invalid_size","message":"尺寸不合法"}}`)) + if err != nil || result.Status != StatusFailed || result.Message != "尺寸不合法" { + t.Fatalf("error: %#v %v", result, err) + } +} diff --git a/docs/content/docs/backend/code-map.mdx b/docs/content/docs/backend/code-map.mdx index 22b333ec2..71b0a08da 100644 --- a/docs/content/docs/backend/code-map.mdx +++ b/docs/content/docs/backend/code-map.mdx @@ -5,6 +5,13 @@ description: 从产品功能快速定位主要代码入口 # 代码功能地图 +## 可美 KM Seedream + +- 声明式请求、响应与参数合同:`plugin-packages/km-kemei-seedream/manifest.json`、`docs/interface.md`;分发包:`plugin-packages/km-kemei-seedream.yingce-plugin`。 +- 后端默认能力与预设:`backend/internal/app/model_capability_defaults.go`、`model_capability_kemei.go`;档位校验:`model_capability_validate.go`。 +- 前端预设与选择:`web/src/lib/kemei-image-presets.ts`、`image-resolution-tiers.ts`、`image-size-presets.ts`、`model-capabilities.ts`;界面入口:`web/src/components/image-size-picker.tsx`、`image-size-presets-editor.tsx`。 +- 协议回归:`backend/internal/protocol/km_kemei_seedream_test.go`;前后端能力与尺寸测试位于对应测试目录。 + ## 自定义更新公告 - 后台编辑:`web/src/pages/admin/settings/components/update-announcement-editor.tsx`;版本、排序与草稿校验:`web/src/lib/update-announcement.ts`。 diff --git a/docs/content/docs/backend/http-api.mdx b/docs/content/docs/backend/http-api.mdx index 54a7821e4..2676dca02 100644 --- a/docs/content/docs/backend/http-api.mdx +++ b/docs/content/docs/backend/http-api.mdx @@ -5,6 +5,10 @@ description: 信封、错误码与 OpenAPI # HTTP API 合同 +## KM 图片尺寸能力 + +图片能力 `size.presets[].tier` 在已有档位之外接受 `1.5k`;前端档位选择、请求归一化与服务端预设校验保持一致。`km-kemei-seedream` 默认 `2048x2048`、单次一张、最多十张参考图,提供二十四个精确尺寸。透明输出使用插件扩展字段,不开启宿主通用透明背景开关。模型能力仍保存在现有 JSON 字段,无数据库迁移或新端点。 + ## 注册邮箱等价检查 公开注册与邮箱绑定的唯一性检查将 `gmail.com` 和 `googlemail.com` 视为同一域,移除本地部分的点及 `+` 后缀。例如 `a.b+x@googlemail.com` 与 `ab@gmail.com` 不允许创建两个账号。其他域保留点和 `+`;现有域名白名单规则保持不变。 diff --git a/docs/content/docs/overview/features.mdx b/docs/content/docs/overview/features.mdx index 4ab23ff5b..bbf44e11f 100644 --- a/docs/content/docs/overview/features.mdx +++ b/docs/content/docs/overview/features.mdx @@ -5,6 +5,12 @@ description: 当前项目已实现的核心工作台能力与使用入口 # 功能清单 +## 可美 KM Seedream 图片 + +`km-kemei-seedream` 声明式插件接入 Seedream 5.0 Pro / Flash 的文生图与参考图编辑,支持 URL / Base64 响应并由宿主统一保存图片。精确模型 ID 与扩展参数见插件包内 `docs/interface.md`。 + +协议默认 2K 方图、单张生成、最多十张参考图,提供 1K / 1.5K / 2K 共二十四个精确尺寸预设。后台能力编辑、画布选择与服务端校验同步支持 1.5K。蒙版与通用质量开关关闭;透明输出通过插件命名空间配置,并要求单张含 alpha 的参考图。供应商错误和空结果保留失败语义。 + ## 卡藏 Midjourney 画布接收一次任务返回的全部图片,四张结果自动组成收起的图片堆叠,可展开、选择主图和分别使用。每张图保留独立资源及素材绑定,保存、刷新和任务重复同步不会将后续图片覆盖为第一张。已有仅显示首图的成功 Midjourney 节点在重新打开画布时从原任务恢复遗漏结果,无需再次生成。 diff --git a/docs/content/docs/progress/pending-test.mdx b/docs/content/docs/progress/pending-test.mdx index d2e08f1b6..1a9a5d423 100644 --- a/docs/content/docs/progress/pending-test.mdx +++ b/docs/content/docs/progress/pending-test.mdx @@ -5,6 +5,12 @@ description: 当前项目已实现功能的手工验证清单与未确认边界 # 待测试 +## 可美 KM Seedream 图片 + +- 已实现:Pro / Flash 声明式协议、参考图与扩展参数、结果保存,以及前后端一致的 1K / 1.5K / 2K 尺寸预设。 +- [ ] 用户验收协议选择后的默认能力、1.5K 档位与宽高比切换、真实图片生成及刷新后的素材显示。 +- [ ] 在供应商环境验证多参考图、Base64 响应、透明输出、全部尺寸和扩展参数;协议测试不替代真实供应商验收。 + ## 复制生图节点后节点消失 - 已修复:副本不再继承原节点的 `generationEffectKeys`,避免普通保存把它误判为未确认的生成结果。图片内容、提示词和参考字段仍保留,原节点不受影响。 diff --git a/plugin-packages/km-kemei-seedream.yingce-plugin b/plugin-packages/km-kemei-seedream.yingce-plugin new file mode 100644 index 0000000000000000000000000000000000000000..3c793699c715cc5ec1a49071f16ccf171ec856d0 GIT binary patch literal 16469 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 literal 0 HcmV?d00001 diff --git a/plugin-packages/km-kemei-seedream/README.md b/plugin-packages/km-kemei-seedream/README.md new file mode 100644 index 000000000..0177ada05 --- /dev/null +++ b/plugin-packages/km-kemei-seedream/README.md @@ -0,0 +1,9 @@ +# KM 可美 Seedream 5.0 + +`yingce.plugin/v2` 声明式图片协议包。使用渠道凭证,宿主统一负责 HTTPS、SSRF 校验、超时与媒体持久化;不包含脚本运行时或密钥。 + +适配精确模型 ID:`doubao-seedream-5-0-pro-260628`、`doubao-seedream-5-0-flash-260915`。 + +选择协议时“能力与参数”默认设为:2K / 1:1(2048x2048)、单张生成、最多 10 张参考图、单图 30 MB、关闭蒙版、关闭通用 quality 和透明背景开关。1K / 1.5K / 2K 各有八种官方精确尺寸。响应默认 URL、输出 JPEG、关闭水印;所有返回图片及用量交给宿主消费。 + +详情见 [接口说明](docs/interface.md)。 diff --git a/plugin-packages/km-kemei-seedream/docs/interface.md b/plugin-packages/km-kemei-seedream/docs/interface.md new file mode 100644 index 000000000..8dfd7e6ae --- /dev/null +++ b/plugin-packages/km-kemei-seedream/docs/interface.md @@ -0,0 +1,1525 @@ +# KM 可美 Seedream 5.0 接口 + +## 来源与范围 + +- [供应商图像文档](https://token.xinhankr.com/docs/quickstart/doc0002),公开文档 `volc-image`:OpenAI 兼容端点、Pro 的限制与默认值。 +- [火山引擎 Image generation API](https://docs.volcengine.com/docs/ark/image-generation-api?lang=zh&redirect=1):Pro / Flash 的精确模型 ID、尺寸、参考图、格式和能力限制。 +- 供应商模型市场确认两个精确模型 ID 可用。市场计费档不是参考图或组图数量上限。 + +## 请求 + +`POST https://token.xinhankr.com/v1/images/generations`,JSON,渠道 API Key 作为 Bearer Token。 + +```json +{ + "model": "doubao-seedream-5-0-pro-260628", + "prompt": "暖色书房中的橘猫,写实照片,正方形构图", + "n": 1, + "size": "2048x2048", + "response_format": "url", + "output_format": "jpeg", + "watermark": false, + "background": "opaque" +} +``` + +Flash 使用 `doubao-seedream-5-0-flash-260915`。图生图仍使用同一端点;参考图映射为 `image`,单图是字符串,多图是数组,保持输入顺序,支持 URL / Base64。最多 10 张,每张不超过 30 MB。官方还要求参考图边长大于 14、总像素 196–36000000、比例 1:16–16:1;这些条件需素材本身满足。没有蒙版接口。 + +`size` 支持 `1K` / `1.5K` / `2K`,或像素字符串。空值 / auto 默认 2K;宿主比例值映射为官方 2K 像素。自定义总像素 921600–4624220,比例 1:16–16:1。宿主交互控件还保留全站的 16px 步长、最长边 3840 与 3:1 比例约束。提示词 32000 字符是宿主限制,未声称是供应商的上限。 + +| 比例 | 1K | 1.5K | 2K | +| --- | --- | --- | --- | +| 1:1 | 1024x1024 | 1536x1536 | 2048x2048 | +| 4:3 | 1152x864 | 1792x1344 | 2368x1776 | +| 3:4 | 864x1152 | 1344x1792 | 1776x2368 | +| 16:9 | 1424x800 | 2048x1152 | 2816x1584 | +| 9:16 | 800x1424 | 1152x2048 | 1584x2816 | +| 3:2 | 1248x832 | 1872x1248 | 2496x1664 | +| 2:3 | 832x1248 | 1248x1872 | 1664x2496 | +| 21:9 | 1568x672 | 2352x1008 | 3136x1344 | + +## 命名空间扩展 + +`providerOptions.km-kemei-seedream`: + +- `response_format`:url(默认)/ b64_json。 +- `output_format`:jpeg(默认)/ png。 +- `watermark`:false(默认)/ true。 +- `background`:opaque(默认)/ transparent。透明只用于一张带 alpha 的输入图,且输出 png;插件校验单图与 png 条件,alpha 条件由供应商检查。宿主通用透明开关默认关闭,避免把任意文生图伪装成此能力。 +- `layer_decomposition`:布尔值,仅一张输入图,尺寸使用档位或 auto;此时允许空提示词,保存全部图层,不按 n=1 截断。 + +Pro / Flash 不支持 sequential_image_generation、组图、联网搜索、流式输出。插件拒绝相关扩展字段,不发送通用 quality、resolution、seed。 + +## 响应与失败 + +同步响应中的 `data[]` 全部提取,兼容 url / b64_json,并透传 usage。供应商 URL 有有效期,宿主必须持久化结果。HTTP 错误由宿主处理,JSON 的 error.code / error.message 映射为失败;不以空结果冒充成功。 + +安装包不携带渠道密钥;管理员通过正常插件安装机制启用,再给渠道模型选择此协议。模型价格与启用状态由管理员独立配置。 + + +## Manifest 完整接口定义 + +以下 JSON 与插件包内实际 `manifest.json` 逐字段一致,覆盖插件身份、权限、配置、鉴权、参数、校验、创建、Agent、查询、取消、结果下载、响应和 Agent 响应映射。`documentation` 字段的值就是当前完整文档;为避免文档在自身内部无限递归,JSON 中仅用等义占位文本表示正文。 + +```json +{ + "apiVersion": "yingce.plugin/v2", + "id": "km-kemei-seedream", + "name": "KM 可美 Seedream 5.0", + "version": "1.0.0", + "author": "KM 可美 / 影绘", + "description": "适配可美 Seedream 5.0 Pro 与 Flash,单张生成、参考图与全部结果提取。", + "enabled": true, + "installable": true, + "permissions": [ + "generation.run", + "media.read" + ], + "configuration": { + "fields": [ + { + "name": "apiKey", + "type": "secret", + "label": "API Key", + "required": true + } + ] + }, + "contributes": { + "providers": [ + { + "id": "km-kemei-seedream", + "label": "KM 可美 Seedream 5.0", + "capabilities": [ + "image" + ], + "scopes": [ + "admin.system-channel", + "user.custom-channel", + "canvas", + "creation", + "agent" + ], + "baseUrl": "https://token.xinhankr.com", + "requiresPublicMediaUrls": false, + "auth": { + "type": "bearer", + "field": "apiKey" + }, + "parameters": [ + { + "name": "model", + "type": "string", + "required": true, + "mapping": "model", + "description": "精确 ID:doubao-seedream-5-0-pro-260628 / doubao-seedream-5-0-flash-260915。" + }, + { + "name": "prompt", + "type": "string", + "required": true, + "mapping": "prompt", + "description": "图片提示词;宿主限制 32000 字符。" + }, + { + "name": "imageCount", + "type": "integer", + "required": false, + "mapping": "n=1", + "description": "仅单张生成;图层分解返回的所有图层均保存。" + }, + { + "name": "aspectRatio", + "type": "string", + "required": false, + "mapping": "size", + "description": "默认 2048x2048(2K、1:1);支持 1K / 1.5K / 2K、八种比例或合法像素尺寸。" + }, + { + "name": "images", + "type": "media[]", + "required": false, + "mapping": "image", + "description": "最多 10 张;URL / Base64;不支持蒙版。" + }, + { + "name": "providerOptions", + "type": "object", + "required": false, + "mapping": "providerOptions.km-kemei-seedream", + "description": "可选 response_format / output_format / watermark / background / layer_decomposition。" + } + ], + "validations": [ + { + "assert": { + "$in": [ + { + "$ref": "request.model" + }, + [ + "doubao-seedream-5-0-pro-260628", + "doubao-seedream-5-0-flash-260915" + ] + ] + }, + "message": "此协议只适用于可美 Seedream 5.0 Pro / Flash 的已确认模型 ID。" + }, + { + "assert": { + "$lte": [ + { + "$ref": "request.imageCount" + }, + 1 + ] + }, + "message": "Seedream 5.0 Pro / Flash 仅支持单张生成,n 只能为 1。" + }, + { + "assert": { + "$or": [ + { + "$gt": [ + { + "$len": { + "$trim": { + "$ref": "request.prompt" + } + } + }, + 0 + ] + }, + { + "$eq": [ + { + "$coalesce": [ + { + "$ref": "request.providerOptions.km-kemei-seedream.layer_decomposition" + }, + false + ] + }, + true + ] + } + ] + }, + "message": "提示词不能为空;仅图层分解模式可省略提示词。" + }, + { + "assert": { + "$lte": [ + { + "$len": { + "$ref": "request.prompt" + } + }, + 32000 + ] + }, + "message": "提示词超过宿主配置的 32000 字符上限。" + }, + { + "assert": { + "$lte": [ + { + "$len": { + "$ref": "request.images" + } + }, + 10 + ] + }, + "message": "Seedream 5.0 参考图最多 10 张。" + }, + { + "assert": { + "$eq": [ + { + "$len": { + "$filter": { + "from": { + "$ref": "request.images" + }, + "as": "media", + "where": { + "$eq": [ + { + "$ref": "media.role" + }, + "mask" + ] + } + } + } + }, + 0 + ] + }, + "message": "此协议不支持蒙版编辑。" + }, + { + "assert": { + "$eq": [ + { + "$len": { + "$filter": { + "from": { + "$ref": "request.images" + }, + "as": "media", + "where": { + "$gt": [ + { + "$coalesce": [ + { + "$ref": "media.metadata.bytes" + }, + 0 + ] + }, + 31457280 + ] + } + } + } + }, + 0 + ] + }, + "message": "单张参考图不得超过 30 MB。" + }, + { + "assert": { + "$or": [ + { + "$in": [ + { + "$lower": { + "$trim": { + "$coalesce": [ + { + "$ref": "request.aspectRatio" + }, + "2k" + ] + } + } + }, + [ + "", + "auto", + "1k", + "1.5k", + "2k", + "1:1", + "4:3", + "3:4", + "16:9", + "9:16", + "3:2", + "2:3", + "21:9" + ] + ] + }, + { + "$and": [ + { + "$eq": [ + { + "$len": { + "$split": [ + { + "$lower": { + "$trim": { + "$coalesce": [ + { + "$ref": "request.aspectRatio" + }, + "2k" + ] + } + } + }, + "x" + ] + } + }, + 2 + ] + }, + { + "$gt": [ + { + "$toInt": { + "$at": [ + { + "$split": [ + { + "$lower": { + "$trim": { + "$coalesce": [ + { + "$ref": "request.aspectRatio" + }, + "2k" + ] + } + } + }, + "x" + ] + }, + 0 + ] + } + }, + 0 + ] + }, + { + "$gt": [ + { + "$toInt": { + "$at": [ + { + "$split": [ + { + "$lower": { + "$trim": { + "$coalesce": [ + { + "$ref": "request.aspectRatio" + }, + "2k" + ] + } + } + }, + "x" + ] + }, + 1 + ] + } + }, + 0 + ] + }, + { + "$gte": [ + { + "$multiply": [ + { + "$toInt": { + "$at": [ + { + "$split": [ + { + "$lower": { + "$trim": { + "$coalesce": [ + { + "$ref": "request.aspectRatio" + }, + "2k" + ] + } + } + }, + "x" + ] + }, + 0 + ] + } + }, + { + "$toInt": { + "$at": [ + { + "$split": [ + { + "$lower": { + "$trim": { + "$coalesce": [ + { + "$ref": "request.aspectRatio" + }, + "2k" + ] + } + } + }, + "x" + ] + }, + 1 + ] + } + } + ] + }, + 921600 + ] + }, + { + "$lte": [ + { + "$multiply": [ + { + "$toInt": { + "$at": [ + { + "$split": [ + { + "$lower": { + "$trim": { + "$coalesce": [ + { + "$ref": "request.aspectRatio" + }, + "2k" + ] + } + } + }, + "x" + ] + }, + 0 + ] + } + }, + { + "$toInt": { + "$at": [ + { + "$split": [ + { + "$lower": { + "$trim": { + "$coalesce": [ + { + "$ref": "request.aspectRatio" + }, + "2k" + ] + } + } + }, + "x" + ] + }, + 1 + ] + } + } + ] + }, + 4624220 + ] + }, + { + "$lte": [ + { + "$toInt": { + "$at": [ + { + "$split": [ + { + "$lower": { + "$trim": { + "$coalesce": [ + { + "$ref": "request.aspectRatio" + }, + "2k" + ] + } + } + }, + "x" + ] + }, + 0 + ] + } + }, + { + "$multiply": [ + { + "$toInt": { + "$at": [ + { + "$split": [ + { + "$lower": { + "$trim": { + "$coalesce": [ + { + "$ref": "request.aspectRatio" + }, + "2k" + ] + } + } + }, + "x" + ] + }, + 1 + ] + } + }, + 16 + ] + } + ] + }, + { + "$lte": [ + { + "$toInt": { + "$at": [ + { + "$split": [ + { + "$lower": { + "$trim": { + "$coalesce": [ + { + "$ref": "request.aspectRatio" + }, + "2k" + ] + } + } + }, + "x" + ] + }, + 1 + ] + } + }, + { + "$multiply": [ + { + "$toInt": { + "$at": [ + { + "$split": [ + { + "$lower": { + "$trim": { + "$coalesce": [ + { + "$ref": "request.aspectRatio" + }, + "2k" + ] + } + } + }, + "x" + ] + }, + 0 + ] + } + }, + 16 + ] + } + ] + } + ] + } + ] + }, + "message": "size 必须为 1K / 1.5K / 2K 或合法像素尺寸;像素总数 921600–4624220,比例 1:16–16:1。" + }, + { + "assert": { + "$in": [ + { + "$coalesce": [ + { + "$ref": "request.providerOptions.km-kemei-seedream.response_format" + }, + "url" + ] + }, + [ + "url", + "b64_json" + ] + ] + }, + "message": "response_format 仅支持 url / b64_json。" + }, + { + "assert": { + "$in": [ + { + "$coalesce": [ + { + "$ref": "request.providerOptions.km-kemei-seedream.output_format" + }, + "jpeg" + ] + }, + [ + "jpeg", + "png" + ] + ] + }, + "message": "output_format 仅支持 jpeg / png。" + }, + { + "assert": { + "$in": [ + { + "$coalesce": [ + { + "$ref": "request.providerOptions.km-kemei-seedream.background" + }, + "opaque" + ] + }, + [ + "opaque", + "transparent" + ] + ] + }, + "message": "background 仅支持 opaque / transparent。" + }, + { + "assert": { + "$or": [ + { + "$ne": [ + { + "$coalesce": [ + { + "$ref": "request.providerOptions.km-kemei-seedream.background" + }, + "opaque" + ] + }, + "transparent" + ] + }, + { + "$and": [ + { + "$eq": [ + { + "$len": { + "$ref": "request.images" + } + }, + 1 + ] + }, + { + "$eq": [ + { + "$coalesce": [ + { + "$ref": "request.providerOptions.km-kemei-seedream.output_format" + }, + "jpeg" + ] + }, + "png" + ] + } + ] + } + ] + }, + "message": "透明背景仅用于一张带透明通道的参考图,输出必须为 png。" + }, + { + "assert": { + "$in": [ + { + "$coalesce": [ + { + "$ref": "request.providerOptions.km-kemei-seedream.layer_decomposition" + }, + false + ] + }, + [ + true, + false + ] + ] + }, + "message": "layer_decomposition 必须为布尔值。" + }, + { + "assert": { + "$or": [ + { + "$ne": [ + { + "$coalesce": [ + { + "$ref": "request.providerOptions.km-kemei-seedream.layer_decomposition" + }, + false + ] + }, + true + ] + }, + { + "$and": [ + { + "$eq": [ + { + "$len": { + "$ref": "request.images" + } + }, + 1 + ] + }, + { + "$in": [ + { + "$lower": { + "$trim": { + "$coalesce": [ + { + "$ref": "request.aspectRatio" + }, + "2k" + ] + } + } + }, + [ + "", + "auto", + "1k", + "1.5k", + "2k" + ] + ] + } + ] + } + ] + }, + "message": "图层分解仅支持一张参考图,size 使用 1K / 1.5K / 2K / auto 档位。" + }, + { + "assert": { + "$eq": [ + { + "$ref": "request.providerOptions.km-kemei-seedream.sequential_image_generation" + }, + null + ] + }, + "message": "sequential_image_generation 不适用于 Seedream 5.0 Pro / Flash,已拒绝请求。" + }, + { + "assert": { + "$eq": [ + { + "$ref": "request.providerOptions.km-kemei-seedream.sequential_image_generation_options" + }, + null + ] + }, + "message": "sequential_image_generation_options 不适用于 Seedream 5.0 Pro / Flash,已拒绝请求。" + }, + { + "assert": { + "$eq": [ + { + "$ref": "request.providerOptions.km-kemei-seedream.web_search" + }, + null + ] + }, + "message": "web_search 不适用于 Seedream 5.0 Pro / Flash,已拒绝请求。" + }, + { + "assert": { + "$eq": [ + { + "$ref": "request.providerOptions.km-kemei-seedream.stream" + }, + null + ] + }, + "message": "stream 不适用于 Seedream 5.0 Pro / Flash,已拒绝请求。" + }, + { + "assert": { + "$eq": [ + { + "$ref": "request.providerOptions.km-kemei-seedream.tools" + }, + null + ] + }, + "message": "tools 不适用于 Seedream 5.0 Pro / Flash,已拒绝请求。" + } + ], + "create": { + "method": "POST", + "path": "/v1/images/generations", + "contentType": "application/json", + "body": { + "model": { + "$ref": "request.model" + }, + "prompt": { + "$ref": "request.prompt" + }, + "n": 1, + "size": { + "$if": { + "condition": { + "$eq": [ + { + "$coalesce": [ + { + "$ref": "request.providerOptions.km-kemei-seedream.layer_decomposition" + }, + false + ] + }, + true + ] + }, + "then": { + "$if": { + "condition": { + "$in": [ + { + "$lower": { + "$trim": { + "$coalesce": [ + { + "$ref": "request.aspectRatio" + }, + "2k" + ] + } + } + }, + [ + "", + "auto" + ] + ] + }, + "then": "auto", + "else": { + "$switch": { + "cases": [ + { + "when": { + "$in": [ + { + "$lower": { + "$trim": { + "$coalesce": [ + { + "$ref": "request.aspectRatio" + }, + "2k" + ] + } + } + }, + [ + "", + "auto", + "2k" + ] + ] + }, + "then": "2K" + }, + { + "when": { + "$eq": [ + { + "$lower": { + "$trim": { + "$coalesce": [ + { + "$ref": "request.aspectRatio" + }, + "2k" + ] + } + } + }, + "1k" + ] + }, + "then": "1K" + }, + { + "when": { + "$eq": [ + { + "$lower": { + "$trim": { + "$coalesce": [ + { + "$ref": "request.aspectRatio" + }, + "2k" + ] + } + } + }, + "1.5k" + ] + }, + "then": "1.5K" + }, + { + "when": { + "$eq": [ + { + "$lower": { + "$trim": { + "$coalesce": [ + { + "$ref": "request.aspectRatio" + }, + "2k" + ] + } + } + }, + "1:1" + ] + }, + "then": "2048x2048" + }, + { + "when": { + "$eq": [ + { + "$lower": { + "$trim": { + "$coalesce": [ + { + "$ref": "request.aspectRatio" + }, + "2k" + ] + } + } + }, + "4:3" + ] + }, + "then": "2368x1776" + }, + { + "when": { + "$eq": [ + { + "$lower": { + "$trim": { + "$coalesce": [ + { + "$ref": "request.aspectRatio" + }, + "2k" + ] + } + } + }, + "3:4" + ] + }, + "then": "1776x2368" + }, + { + "when": { + "$eq": [ + { + "$lower": { + "$trim": { + "$coalesce": [ + { + "$ref": "request.aspectRatio" + }, + "2k" + ] + } + } + }, + "16:9" + ] + }, + "then": "2816x1584" + }, + { + "when": { + "$eq": [ + { + "$lower": { + "$trim": { + "$coalesce": [ + { + "$ref": "request.aspectRatio" + }, + "2k" + ] + } + } + }, + "9:16" + ] + }, + "then": "1584x2816" + }, + { + "when": { + "$eq": [ + { + "$lower": { + "$trim": { + "$coalesce": [ + { + "$ref": "request.aspectRatio" + }, + "2k" + ] + } + } + }, + "3:2" + ] + }, + "then": "2496x1664" + }, + { + "when": { + "$eq": [ + { + "$lower": { + "$trim": { + "$coalesce": [ + { + "$ref": "request.aspectRatio" + }, + "2k" + ] + } + } + }, + "2:3" + ] + }, + "then": "1664x2496" + }, + { + "when": { + "$eq": [ + { + "$lower": { + "$trim": { + "$coalesce": [ + { + "$ref": "request.aspectRatio" + }, + "2k" + ] + } + } + }, + "21:9" + ] + }, + "then": "3136x1344" + } + ], + "default": { + "$lower": { + "$trim": { + "$coalesce": [ + { + "$ref": "request.aspectRatio" + }, + "2k" + ] + } + } + } + } + } + } + }, + "else": { + "$switch": { + "cases": [ + { + "when": { + "$in": [ + { + "$lower": { + "$trim": { + "$coalesce": [ + { + "$ref": "request.aspectRatio" + }, + "2k" + ] + } + } + }, + [ + "", + "auto", + "2k" + ] + ] + }, + "then": "2K" + }, + { + "when": { + "$eq": [ + { + "$lower": { + "$trim": { + "$coalesce": [ + { + "$ref": "request.aspectRatio" + }, + "2k" + ] + } + } + }, + "1k" + ] + }, + "then": "1K" + }, + { + "when": { + "$eq": [ + { + "$lower": { + "$trim": { + "$coalesce": [ + { + "$ref": "request.aspectRatio" + }, + "2k" + ] + } + } + }, + "1.5k" + ] + }, + "then": "1.5K" + }, + { + "when": { + "$eq": [ + { + "$lower": { + "$trim": { + "$coalesce": [ + { + "$ref": "request.aspectRatio" + }, + "2k" + ] + } + } + }, + "1:1" + ] + }, + "then": "2048x2048" + }, + { + "when": { + "$eq": [ + { + "$lower": { + "$trim": { + "$coalesce": [ + { + "$ref": "request.aspectRatio" + }, + "2k" + ] + } + } + }, + "4:3" + ] + }, + "then": "2368x1776" + }, + { + "when": { + "$eq": [ + { + "$lower": { + "$trim": { + "$coalesce": [ + { + "$ref": "request.aspectRatio" + }, + "2k" + ] + } + } + }, + "3:4" + ] + }, + "then": "1776x2368" + }, + { + "when": { + "$eq": [ + { + "$lower": { + "$trim": { + "$coalesce": [ + { + "$ref": "request.aspectRatio" + }, + "2k" + ] + } + } + }, + "16:9" + ] + }, + "then": "2816x1584" + }, + { + "when": { + "$eq": [ + { + "$lower": { + "$trim": { + "$coalesce": [ + { + "$ref": "request.aspectRatio" + }, + "2k" + ] + } + } + }, + "9:16" + ] + }, + "then": "1584x2816" + }, + { + "when": { + "$eq": [ + { + "$lower": { + "$trim": { + "$coalesce": [ + { + "$ref": "request.aspectRatio" + }, + "2k" + ] + } + } + }, + "3:2" + ] + }, + "then": "2496x1664" + }, + { + "when": { + "$eq": [ + { + "$lower": { + "$trim": { + "$coalesce": [ + { + "$ref": "request.aspectRatio" + }, + "2k" + ] + } + } + }, + "2:3" + ] + }, + "then": "1664x2496" + }, + { + "when": { + "$eq": [ + { + "$lower": { + "$trim": { + "$coalesce": [ + { + "$ref": "request.aspectRatio" + }, + "2k" + ] + } + } + }, + "21:9" + ] + }, + "then": "3136x1344" + } + ], + "default": { + "$lower": { + "$trim": { + "$coalesce": [ + { + "$ref": "request.aspectRatio" + }, + "2k" + ] + } + } + } + } + } + } + }, + "image": { + "$omitEmpty": { + "$if": { + "condition": { + "$eq": [ + { + "$len": { + "$ref": "request.images" + } + }, + 1 + ] + }, + "then": { + "$first": { + "$map": { + "from": { + "$sortByOrder": { + "$ref": "request.images" + } + }, + "as": "media", + "in": { + "$ref": "media.value" + } + } + } + }, + "else": { + "$map": { + "from": { + "$sortByOrder": { + "$ref": "request.images" + } + }, + "as": "media", + "in": { + "$ref": "media.value" + } + } + } + } + } + }, + "response_format": { + "$coalesce": [ + { + "$ref": "request.providerOptions.km-kemei-seedream.response_format" + }, + "url" + ] + }, + "output_format": { + "$coalesce": [ + { + "$ref": "request.providerOptions.km-kemei-seedream.output_format" + }, + "jpeg" + ] + }, + "watermark": { + "$coalesce": [ + { + "$ref": "request.providerOptions.km-kemei-seedream.watermark" + }, + false + ] + }, + "background": { + "$coalesce": [ + { + "$ref": "request.providerOptions.km-kemei-seedream.background" + }, + "opaque" + ] + }, + "layer_decomposition": { + "$omitEmpty": { + "$ref": "request.providerOptions.km-kemei-seedream.layer_decomposition" + } + } + } + }, + "response": { + "status": "succeeded", + "images": { + "$ref": "response.data" + }, + "usage": { + "$ref": "response.usage" + }, + "errorPaths": [ + "error.code", + "error.message" + ], + "messagePaths": [ + "error.message" + ] + } + } + ] + }, + "documentation": "<当前插件的完整 documentation,由 README.md 与 docs/interface.md 拼接而成;为避免 JSON 递归,此处不重复展开正文。>" +} +``` + diff --git a/plugin-packages/km-kemei-seedream/manifest.json b/plugin-packages/km-kemei-seedream/manifest.json new file mode 100644 index 000000000..0d033a8eb --- /dev/null +++ b/plugin-packages/km-kemei-seedream/manifest.json @@ -0,0 +1,1459 @@ +{ + "apiVersion": "yingce.plugin/v2", + "id": "km-kemei-seedream", + "name": "KM 可美 Seedream 5.0", + "version": "1.0.0", + "author": "KM 可美 / 影绘", + "description": "适配可美 Seedream 5.0 Pro 与 Flash,单张生成、参考图与全部结果提取。", + "enabled": true, + "installable": true, + "permissions": [ + "generation.run", + "media.read" + ], + "configuration": { + "fields": [ + { + "name": "apiKey", + "type": "secret", + "label": "API Key", + "required": true + } + ] + }, + "contributes": { + "providers": [ + { + "id": "km-kemei-seedream", + "label": "KM 可美 Seedream 5.0", + "capabilities": [ + "image" + ], + "scopes": [ + "admin.system-channel", + "user.custom-channel", + "canvas", + "creation", + "agent" + ], + "baseUrl": "https://token.xinhankr.com", + "requiresPublicMediaUrls": false, + "auth": { + "type": "bearer", + "field": "apiKey" + }, + "parameters": [ + { + "name": "model", + "type": "string", + "required": true, + "mapping": "model", + "description": "精确 ID:doubao-seedream-5-0-pro-260628 / doubao-seedream-5-0-flash-260915。" + }, + { + "name": "prompt", + "type": "string", + "required": true, + "mapping": "prompt", + "description": "图片提示词;宿主限制 32000 字符。" + }, + { + "name": "imageCount", + "type": "integer", + "required": false, + "mapping": "n=1", + "description": "仅单张生成;图层分解返回的所有图层均保存。" + }, + { + "name": "aspectRatio", + "type": "string", + "required": false, + "mapping": "size", + "description": "默认 2048x2048(2K、1:1);支持 1K / 1.5K / 2K、八种比例或合法像素尺寸。" + }, + { + "name": "images", + "type": "media[]", + "required": false, + "mapping": "image", + "description": "最多 10 张;URL / Base64;不支持蒙版。" + }, + { + "name": "providerOptions", + "type": "object", + "required": false, + "mapping": "providerOptions.km-kemei-seedream", + "description": "可选 response_format / output_format / watermark / background / layer_decomposition。" + } + ], + "validations": [ + { + "assert": { + "$in": [ + { + "$ref": "request.model" + }, + [ + "doubao-seedream-5-0-pro-260628", + "doubao-seedream-5-0-flash-260915" + ] + ] + }, + "message": "此协议只适用于可美 Seedream 5.0 Pro / Flash 的已确认模型 ID。" + }, + { + "assert": { + "$lte": [ + { + "$ref": "request.imageCount" + }, + 1 + ] + }, + "message": "Seedream 5.0 Pro / Flash 仅支持单张生成,n 只能为 1。" + }, + { + "assert": { + "$or": [ + { + "$gt": [ + { + "$len": { + "$trim": { + "$ref": "request.prompt" + } + } + }, + 0 + ] + }, + { + "$eq": [ + { + "$coalesce": [ + { + "$ref": "request.providerOptions.km-kemei-seedream.layer_decomposition" + }, + false + ] + }, + true + ] + } + ] + }, + "message": "提示词不能为空;仅图层分解模式可省略提示词。" + }, + { + "assert": { + "$lte": [ + { + "$len": { + "$ref": "request.prompt" + } + }, + 32000 + ] + }, + "message": "提示词超过宿主配置的 32000 字符上限。" + }, + { + "assert": { + "$lte": [ + { + "$len": { + "$ref": "request.images" + } + }, + 10 + ] + }, + "message": "Seedream 5.0 参考图最多 10 张。" + }, + { + "assert": { + "$eq": [ + { + "$len": { + "$filter": { + "from": { + "$ref": "request.images" + }, + "as": "media", + "where": { + "$eq": [ + { + "$ref": "media.role" + }, + "mask" + ] + } + } + } + }, + 0 + ] + }, + "message": "此协议不支持蒙版编辑。" + }, + { + "assert": { + "$eq": [ + { + "$len": { + "$filter": { + "from": { + "$ref": "request.images" + }, + "as": "media", + "where": { + "$gt": [ + { + "$coalesce": [ + { + "$ref": "media.metadata.bytes" + }, + 0 + ] + }, + 31457280 + ] + } + } + } + }, + 0 + ] + }, + "message": "单张参考图不得超过 30 MB。" + }, + { + "assert": { + "$or": [ + { + "$in": [ + { + "$lower": { + "$trim": { + "$coalesce": [ + { + "$ref": "request.aspectRatio" + }, + "2k" + ] + } + } + }, + [ + "", + "auto", + "1k", + "1.5k", + "2k", + "1:1", + "4:3", + "3:4", + "16:9", + "9:16", + "3:2", + "2:3", + "21:9" + ] + ] + }, + { + "$and": [ + { + "$eq": [ + { + "$len": { + "$split": [ + { + "$lower": { + "$trim": { + "$coalesce": [ + { + "$ref": "request.aspectRatio" + }, + "2k" + ] + } + } + }, + "x" + ] + } + }, + 2 + ] + }, + { + "$gt": [ + { + "$toInt": { + "$at": [ + { + "$split": [ + { + "$lower": { + "$trim": { + "$coalesce": [ + { + "$ref": "request.aspectRatio" + }, + "2k" + ] + } + } + }, + "x" + ] + }, + 0 + ] + } + }, + 0 + ] + }, + { + "$gt": [ + { + "$toInt": { + "$at": [ + { + "$split": [ + { + "$lower": { + "$trim": { + "$coalesce": [ + { + "$ref": "request.aspectRatio" + }, + "2k" + ] + } + } + }, + "x" + ] + }, + 1 + ] + } + }, + 0 + ] + }, + { + "$gte": [ + { + "$multiply": [ + { + "$toInt": { + "$at": [ + { + "$split": [ + { + "$lower": { + "$trim": { + "$coalesce": [ + { + "$ref": "request.aspectRatio" + }, + "2k" + ] + } + } + }, + "x" + ] + }, + 0 + ] + } + }, + { + "$toInt": { + "$at": [ + { + "$split": [ + { + "$lower": { + "$trim": { + "$coalesce": [ + { + "$ref": "request.aspectRatio" + }, + "2k" + ] + } + } + }, + "x" + ] + }, + 1 + ] + } + } + ] + }, + 921600 + ] + }, + { + "$lte": [ + { + "$multiply": [ + { + "$toInt": { + "$at": [ + { + "$split": [ + { + "$lower": { + "$trim": { + "$coalesce": [ + { + "$ref": "request.aspectRatio" + }, + "2k" + ] + } + } + }, + "x" + ] + }, + 0 + ] + } + }, + { + "$toInt": { + "$at": [ + { + "$split": [ + { + "$lower": { + "$trim": { + "$coalesce": [ + { + "$ref": "request.aspectRatio" + }, + "2k" + ] + } + } + }, + "x" + ] + }, + 1 + ] + } + } + ] + }, + 4624220 + ] + }, + { + "$lte": [ + { + "$toInt": { + "$at": [ + { + "$split": [ + { + "$lower": { + "$trim": { + "$coalesce": [ + { + "$ref": "request.aspectRatio" + }, + "2k" + ] + } + } + }, + "x" + ] + }, + 0 + ] + } + }, + { + "$multiply": [ + { + "$toInt": { + "$at": [ + { + "$split": [ + { + "$lower": { + "$trim": { + "$coalesce": [ + { + "$ref": "request.aspectRatio" + }, + "2k" + ] + } + } + }, + "x" + ] + }, + 1 + ] + } + }, + 16 + ] + } + ] + }, + { + "$lte": [ + { + "$toInt": { + "$at": [ + { + "$split": [ + { + "$lower": { + "$trim": { + "$coalesce": [ + { + "$ref": "request.aspectRatio" + }, + "2k" + ] + } + } + }, + "x" + ] + }, + 1 + ] + } + }, + { + "$multiply": [ + { + "$toInt": { + "$at": [ + { + "$split": [ + { + "$lower": { + "$trim": { + "$coalesce": [ + { + "$ref": "request.aspectRatio" + }, + "2k" + ] + } + } + }, + "x" + ] + }, + 0 + ] + } + }, + 16 + ] + } + ] + } + ] + } + ] + }, + "message": "size 必须为 1K / 1.5K / 2K 或合法像素尺寸;像素总数 921600–4624220,比例 1:16–16:1。" + }, + { + "assert": { + "$in": [ + { + "$coalesce": [ + { + "$ref": "request.providerOptions.km-kemei-seedream.response_format" + }, + "url" + ] + }, + [ + "url", + "b64_json" + ] + ] + }, + "message": "response_format 仅支持 url / b64_json。" + }, + { + "assert": { + "$in": [ + { + "$coalesce": [ + { + "$ref": "request.providerOptions.km-kemei-seedream.output_format" + }, + "jpeg" + ] + }, + [ + "jpeg", + "png" + ] + ] + }, + "message": "output_format 仅支持 jpeg / png。" + }, + { + "assert": { + "$in": [ + { + "$coalesce": [ + { + "$ref": "request.providerOptions.km-kemei-seedream.background" + }, + "opaque" + ] + }, + [ + "opaque", + "transparent" + ] + ] + }, + "message": "background 仅支持 opaque / transparent。" + }, + { + "assert": { + "$or": [ + { + "$ne": [ + { + "$coalesce": [ + { + "$ref": "request.providerOptions.km-kemei-seedream.background" + }, + "opaque" + ] + }, + "transparent" + ] + }, + { + "$and": [ + { + "$eq": [ + { + "$len": { + "$ref": "request.images" + } + }, + 1 + ] + }, + { + "$eq": [ + { + "$coalesce": [ + { + "$ref": "request.providerOptions.km-kemei-seedream.output_format" + }, + "jpeg" + ] + }, + "png" + ] + } + ] + } + ] + }, + "message": "透明背景仅用于一张带透明通道的参考图,输出必须为 png。" + }, + { + "assert": { + "$in": [ + { + "$coalesce": [ + { + "$ref": "request.providerOptions.km-kemei-seedream.layer_decomposition" + }, + false + ] + }, + [ + true, + false + ] + ] + }, + "message": "layer_decomposition 必须为布尔值。" + }, + { + "assert": { + "$or": [ + { + "$ne": [ + { + "$coalesce": [ + { + "$ref": "request.providerOptions.km-kemei-seedream.layer_decomposition" + }, + false + ] + }, + true + ] + }, + { + "$and": [ + { + "$eq": [ + { + "$len": { + "$ref": "request.images" + } + }, + 1 + ] + }, + { + "$in": [ + { + "$lower": { + "$trim": { + "$coalesce": [ + { + "$ref": "request.aspectRatio" + }, + "2k" + ] + } + } + }, + [ + "", + "auto", + "1k", + "1.5k", + "2k" + ] + ] + } + ] + } + ] + }, + "message": "图层分解仅支持一张参考图,size 使用 1K / 1.5K / 2K / auto 档位。" + }, + { + "assert": { + "$eq": [ + { + "$ref": "request.providerOptions.km-kemei-seedream.sequential_image_generation" + }, + null + ] + }, + "message": "sequential_image_generation 不适用于 Seedream 5.0 Pro / Flash,已拒绝请求。" + }, + { + "assert": { + "$eq": [ + { + "$ref": "request.providerOptions.km-kemei-seedream.sequential_image_generation_options" + }, + null + ] + }, + "message": "sequential_image_generation_options 不适用于 Seedream 5.0 Pro / Flash,已拒绝请求。" + }, + { + "assert": { + "$eq": [ + { + "$ref": "request.providerOptions.km-kemei-seedream.web_search" + }, + null + ] + }, + "message": "web_search 不适用于 Seedream 5.0 Pro / Flash,已拒绝请求。" + }, + { + "assert": { + "$eq": [ + { + "$ref": "request.providerOptions.km-kemei-seedream.stream" + }, + null + ] + }, + "message": "stream 不适用于 Seedream 5.0 Pro / Flash,已拒绝请求。" + }, + { + "assert": { + "$eq": [ + { + "$ref": "request.providerOptions.km-kemei-seedream.tools" + }, + null + ] + }, + "message": "tools 不适用于 Seedream 5.0 Pro / Flash,已拒绝请求。" + } + ], + "create": { + "method": "POST", + "path": "/v1/images/generations", + "contentType": "application/json", + "body": { + "model": { + "$ref": "request.model" + }, + "prompt": { + "$ref": "request.prompt" + }, + "n": 1, + "size": { + "$if": { + "condition": { + "$eq": [ + { + "$coalesce": [ + { + "$ref": "request.providerOptions.km-kemei-seedream.layer_decomposition" + }, + false + ] + }, + true + ] + }, + "then": { + "$if": { + "condition": { + "$in": [ + { + "$lower": { + "$trim": { + "$coalesce": [ + { + "$ref": "request.aspectRatio" + }, + "2k" + ] + } + } + }, + [ + "", + "auto" + ] + ] + }, + "then": "auto", + "else": { + "$switch": { + "cases": [ + { + "when": { + "$in": [ + { + "$lower": { + "$trim": { + "$coalesce": [ + { + "$ref": "request.aspectRatio" + }, + "2k" + ] + } + } + }, + [ + "", + "auto", + "2k" + ] + ] + }, + "then": "2K" + }, + { + "when": { + "$eq": [ + { + "$lower": { + "$trim": { + "$coalesce": [ + { + "$ref": "request.aspectRatio" + }, + "2k" + ] + } + } + }, + "1k" + ] + }, + "then": "1K" + }, + { + "when": { + "$eq": [ + { + "$lower": { + "$trim": { + "$coalesce": [ + { + "$ref": "request.aspectRatio" + }, + "2k" + ] + } + } + }, + "1.5k" + ] + }, + "then": "1.5K" + }, + { + "when": { + "$eq": [ + { + "$lower": { + "$trim": { + "$coalesce": [ + { + "$ref": "request.aspectRatio" + }, + "2k" + ] + } + } + }, + "1:1" + ] + }, + "then": "2048x2048" + }, + { + "when": { + "$eq": [ + { + "$lower": { + "$trim": { + "$coalesce": [ + { + "$ref": "request.aspectRatio" + }, + "2k" + ] + } + } + }, + "4:3" + ] + }, + "then": "2368x1776" + }, + { + "when": { + "$eq": [ + { + "$lower": { + "$trim": { + "$coalesce": [ + { + "$ref": "request.aspectRatio" + }, + "2k" + ] + } + } + }, + "3:4" + ] + }, + "then": "1776x2368" + }, + { + "when": { + "$eq": [ + { + "$lower": { + "$trim": { + "$coalesce": [ + { + "$ref": "request.aspectRatio" + }, + "2k" + ] + } + } + }, + "16:9" + ] + }, + "then": "2816x1584" + }, + { + "when": { + "$eq": [ + { + "$lower": { + "$trim": { + "$coalesce": [ + { + "$ref": "request.aspectRatio" + }, + "2k" + ] + } + } + }, + "9:16" + ] + }, + "then": "1584x2816" + }, + { + "when": { + "$eq": [ + { + "$lower": { + "$trim": { + "$coalesce": [ + { + "$ref": "request.aspectRatio" + }, + "2k" + ] + } + } + }, + "3:2" + ] + }, + "then": "2496x1664" + }, + { + "when": { + "$eq": [ + { + "$lower": { + "$trim": { + "$coalesce": [ + { + "$ref": "request.aspectRatio" + }, + "2k" + ] + } + } + }, + "2:3" + ] + }, + "then": "1664x2496" + }, + { + "when": { + "$eq": [ + { + "$lower": { + "$trim": { + "$coalesce": [ + { + "$ref": "request.aspectRatio" + }, + "2k" + ] + } + } + }, + "21:9" + ] + }, + "then": "3136x1344" + } + ], + "default": { + "$lower": { + "$trim": { + "$coalesce": [ + { + "$ref": "request.aspectRatio" + }, + "2k" + ] + } + } + } + } + } + } + }, + "else": { + "$switch": { + "cases": [ + { + "when": { + "$in": [ + { + "$lower": { + "$trim": { + "$coalesce": [ + { + "$ref": "request.aspectRatio" + }, + "2k" + ] + } + } + }, + [ + "", + "auto", + "2k" + ] + ] + }, + "then": "2K" + }, + { + "when": { + "$eq": [ + { + "$lower": { + "$trim": { + "$coalesce": [ + { + "$ref": "request.aspectRatio" + }, + "2k" + ] + } + } + }, + "1k" + ] + }, + "then": "1K" + }, + { + "when": { + "$eq": [ + { + "$lower": { + "$trim": { + "$coalesce": [ + { + "$ref": "request.aspectRatio" + }, + "2k" + ] + } + } + }, + "1.5k" + ] + }, + "then": "1.5K" + }, + { + "when": { + "$eq": [ + { + "$lower": { + "$trim": { + "$coalesce": [ + { + "$ref": "request.aspectRatio" + }, + "2k" + ] + } + } + }, + "1:1" + ] + }, + "then": "2048x2048" + }, + { + "when": { + "$eq": [ + { + "$lower": { + "$trim": { + "$coalesce": [ + { + "$ref": "request.aspectRatio" + }, + "2k" + ] + } + } + }, + "4:3" + ] + }, + "then": "2368x1776" + }, + { + "when": { + "$eq": [ + { + "$lower": { + "$trim": { + "$coalesce": [ + { + "$ref": "request.aspectRatio" + }, + "2k" + ] + } + } + }, + "3:4" + ] + }, + "then": "1776x2368" + }, + { + "when": { + "$eq": [ + { + "$lower": { + "$trim": { + "$coalesce": [ + { + "$ref": "request.aspectRatio" + }, + "2k" + ] + } + } + }, + "16:9" + ] + }, + "then": "2816x1584" + }, + { + "when": { + "$eq": [ + { + "$lower": { + "$trim": { + "$coalesce": [ + { + "$ref": "request.aspectRatio" + }, + "2k" + ] + } + } + }, + "9:16" + ] + }, + "then": "1584x2816" + }, + { + "when": { + "$eq": [ + { + "$lower": { + "$trim": { + "$coalesce": [ + { + "$ref": "request.aspectRatio" + }, + "2k" + ] + } + } + }, + "3:2" + ] + }, + "then": "2496x1664" + }, + { + "when": { + "$eq": [ + { + "$lower": { + "$trim": { + "$coalesce": [ + { + "$ref": "request.aspectRatio" + }, + "2k" + ] + } + } + }, + "2:3" + ] + }, + "then": "1664x2496" + }, + { + "when": { + "$eq": [ + { + "$lower": { + "$trim": { + "$coalesce": [ + { + "$ref": "request.aspectRatio" + }, + "2k" + ] + } + } + }, + "21:9" + ] + }, + "then": "3136x1344" + } + ], + "default": { + "$lower": { + "$trim": { + "$coalesce": [ + { + "$ref": "request.aspectRatio" + }, + "2k" + ] + } + } + } + } + } + } + }, + "image": { + "$omitEmpty": { + "$if": { + "condition": { + "$eq": [ + { + "$len": { + "$ref": "request.images" + } + }, + 1 + ] + }, + "then": { + "$first": { + "$map": { + "from": { + "$sortByOrder": { + "$ref": "request.images" + } + }, + "as": "media", + "in": { + "$ref": "media.value" + } + } + } + }, + "else": { + "$map": { + "from": { + "$sortByOrder": { + "$ref": "request.images" + } + }, + "as": "media", + "in": { + "$ref": "media.value" + } + } + } + } + } + }, + "response_format": { + "$coalesce": [ + { + "$ref": "request.providerOptions.km-kemei-seedream.response_format" + }, + "url" + ] + }, + "output_format": { + "$coalesce": [ + { + "$ref": "request.providerOptions.km-kemei-seedream.output_format" + }, + "jpeg" + ] + }, + "watermark": { + "$coalesce": [ + { + "$ref": "request.providerOptions.km-kemei-seedream.watermark" + }, + false + ] + }, + "background": { + "$coalesce": [ + { + "$ref": "request.providerOptions.km-kemei-seedream.background" + }, + "opaque" + ] + }, + "layer_decomposition": { + "$omitEmpty": { + "$ref": "request.providerOptions.km-kemei-seedream.layer_decomposition" + } + } + } + }, + "response": { + "status": "succeeded", + "images": { + "$ref": "response.data" + }, + "usage": { + "$ref": "response.usage" + }, + "errorPaths": [ + "error.code", + "error.message" + ], + "messagePaths": [ + "error.message" + ] + } + } + ] + }, + "documentation": "# KM 可美 Seedream 5.0\n\n`yingce.plugin/v2` 声明式图片协议包。使用渠道凭证,宿主统一负责 HTTPS、SSRF 校验、超时与媒体持久化;不包含脚本运行时或密钥。\n\n适配精确模型 ID:`doubao-seedream-5-0-pro-260628`、`doubao-seedream-5-0-flash-260915`。\n\n选择协议时“能力与参数”默认设为:2K / 1:1(2048x2048)、单张生成、最多 10 张参考图、单图 30 MB、关闭蒙版、关闭通用 quality 和透明背景开关。1K / 1.5K / 2K 各有八种官方精确尺寸。响应默认 URL、输出 JPEG、关闭水印;所有返回图片及用量交给宿主消费。\n\n详情见 [接口说明](docs/interface.md)。\n\n---\n\n# KM 可美 Seedream 5.0 接口\n\n## 来源与范围\n\n- [供应商图像文档](https://token.xinhankr.com/docs/quickstart/doc0002),公开文档 `volc-image`:OpenAI 兼容端点、Pro 的限制与默认值。\n- [火山引擎 Image generation API](https://docs.volcengine.com/docs/ark/image-generation-api?lang=zh&redirect=1):Pro / Flash 的精确模型 ID、尺寸、参考图、格式和能力限制。\n- 供应商模型市场确认两个精确模型 ID 可用。市场计费档不是参考图或组图数量上限。\n\n## 请求\n\n`POST https://token.xinhankr.com/v1/images/generations`,JSON,渠道 API Key 作为 Bearer Token。\n\n```json\n{\n \"model\": \"doubao-seedream-5-0-pro-260628\",\n \"prompt\": \"暖色书房中的橘猫,写实照片,正方形构图\",\n \"n\": 1,\n \"size\": \"2048x2048\",\n \"response_format\": \"url\",\n \"output_format\": \"jpeg\",\n \"watermark\": false,\n \"background\": \"opaque\"\n}\n```\n\nFlash 使用 `doubao-seedream-5-0-flash-260915`。图生图仍使用同一端点;参考图映射为 `image`,单图是字符串,多图是数组,保持输入顺序,支持 URL / Base64。最多 10 张,每张不超过 30 MB。官方还要求参考图边长大于 14、总像素 196–36000000、比例 1:16–16:1;这些条件需素材本身满足。没有蒙版接口。\n\n`size` 支持 `1K` / `1.5K` / `2K`,或像素字符串。空值 / auto 默认 2K;宿主比例值映射为官方 2K 像素。自定义总像素 921600–4624220,比例 1:16–16:1。宿主交互控件还保留全站的 16px 步长、最长边 3840 与 3:1 比例约束。提示词 32000 字符是宿主限制,未声称是供应商的上限。\n\n| 比例 | 1K | 1.5K | 2K |\n| --- | --- | --- | --- |\n| 1:1 | 1024x1024 | 1536x1536 | 2048x2048 |\n| 4:3 | 1152x864 | 1792x1344 | 2368x1776 |\n| 3:4 | 864x1152 | 1344x1792 | 1776x2368 |\n| 16:9 | 1424x800 | 2048x1152 | 2816x1584 |\n| 9:16 | 800x1424 | 1152x2048 | 1584x2816 |\n| 3:2 | 1248x832 | 1872x1248 | 2496x1664 |\n| 2:3 | 832x1248 | 1248x1872 | 1664x2496 |\n| 21:9 | 1568x672 | 2352x1008 | 3136x1344 |\n\n## 命名空间扩展\n\n`providerOptions.km-kemei-seedream`:\n\n- `response_format`:url(默认)/ b64_json。\n- `output_format`:jpeg(默认)/ png。\n- `watermark`:false(默认)/ true。\n- `background`:opaque(默认)/ transparent。透明只用于一张带 alpha 的输入图,且输出 png;插件校验单图与 png 条件,alpha 条件由供应商检查。宿主通用透明开关默认关闭,避免把任意文生图伪装成此能力。\n- `layer_decomposition`:布尔值,仅一张输入图,尺寸使用档位或 auto;此时允许空提示词,保存全部图层,不按 n=1 截断。\n\nPro / Flash 不支持 sequential_image_generation、组图、联网搜索、流式输出。插件拒绝相关扩展字段,不发送通用 quality、resolution、seed。\n\n## 响应与失败\n\n同步响应中的 `data[]` 全部提取,兼容 url / b64_json,并透传 usage。供应商 URL 有有效期,宿主必须持久化结果。HTTP 错误由宿主处理,JSON 的 error.code / error.message 映射为失败;不以空结果冒充成功。\n\n安装包不携带渠道密钥;管理员通过正常插件安装机制启用,再给渠道模型选择此协议。模型价格与启用状态由管理员独立配置。\n\n\n## Manifest 完整接口定义\n\n以下 JSON 与插件包内实际 `manifest.json` 逐字段一致,覆盖插件身份、权限、配置、鉴权、参数、校验、创建、Agent、查询、取消、结果下载、响应和 Agent 响应映射。`documentation` 字段的值就是当前完整文档;为避免文档在自身内部无限递归,JSON 中仅用等义占位文本表示正文。\n\n```json\n{\n \"apiVersion\": \"yingce.plugin/v2\",\n \"id\": \"km-kemei-seedream\",\n \"name\": \"KM 可美 Seedream 5.0\",\n \"version\": \"1.0.0\",\n \"author\": \"KM 可美 / 影绘\",\n \"description\": \"适配可美 Seedream 5.0 Pro 与 Flash,单张生成、参考图与全部结果提取。\",\n \"enabled\": true,\n \"installable\": true,\n \"permissions\": [\n \"generation.run\",\n \"media.read\"\n ],\n \"configuration\": {\n \"fields\": [\n {\n \"name\": \"apiKey\",\n \"type\": \"secret\",\n \"label\": \"API Key\",\n \"required\": true\n }\n ]\n },\n \"contributes\": {\n \"providers\": [\n {\n \"id\": \"km-kemei-seedream\",\n \"label\": \"KM 可美 Seedream 5.0\",\n \"capabilities\": [\n \"image\"\n ],\n \"scopes\": [\n \"admin.system-channel\",\n \"user.custom-channel\",\n \"canvas\",\n \"creation\",\n \"agent\"\n ],\n \"baseUrl\": \"https://token.xinhankr.com\",\n \"requiresPublicMediaUrls\": false,\n \"auth\": {\n \"type\": \"bearer\",\n \"field\": \"apiKey\"\n },\n \"parameters\": [\n {\n \"name\": \"model\",\n \"type\": \"string\",\n \"required\": true,\n \"mapping\": \"model\",\n \"description\": \"精确 ID:doubao-seedream-5-0-pro-260628 / doubao-seedream-5-0-flash-260915。\"\n },\n {\n \"name\": \"prompt\",\n \"type\": \"string\",\n \"required\": true,\n \"mapping\": \"prompt\",\n \"description\": \"图片提示词;宿主限制 32000 字符。\"\n },\n {\n \"name\": \"imageCount\",\n \"type\": \"integer\",\n \"required\": false,\n \"mapping\": \"n=1\",\n \"description\": \"仅单张生成;图层分解返回的所有图层均保存。\"\n },\n {\n \"name\": \"aspectRatio\",\n \"type\": \"string\",\n \"required\": false,\n \"mapping\": \"size\",\n \"description\": \"默认 2048x2048(2K、1:1);支持 1K / 1.5K / 2K、八种比例或合法像素尺寸。\"\n },\n {\n \"name\": \"images\",\n \"type\": \"media[]\",\n \"required\": false,\n \"mapping\": \"image\",\n \"description\": \"最多 10 张;URL / Base64;不支持蒙版。\"\n },\n {\n \"name\": \"providerOptions\",\n \"type\": \"object\",\n \"required\": false,\n \"mapping\": \"providerOptions.km-kemei-seedream\",\n \"description\": \"可选 response_format / output_format / watermark / background / layer_decomposition。\"\n }\n ],\n \"validations\": [\n {\n \"assert\": {\n \"$in\": [\n {\n \"$ref\": \"request.model\"\n },\n [\n \"doubao-seedream-5-0-pro-260628\",\n \"doubao-seedream-5-0-flash-260915\"\n ]\n ]\n },\n \"message\": \"此协议只适用于可美 Seedream 5.0 Pro / Flash 的已确认模型 ID。\"\n },\n {\n \"assert\": {\n \"$lte\": [\n {\n \"$ref\": \"request.imageCount\"\n },\n 1\n ]\n },\n \"message\": \"Seedream 5.0 Pro / Flash 仅支持单张生成,n 只能为 1。\"\n },\n {\n \"assert\": {\n \"$or\": [\n {\n \"$gt\": [\n {\n \"$len\": {\n \"$trim\": {\n \"$ref\": \"request.prompt\"\n }\n }\n },\n 0\n ]\n },\n {\n \"$eq\": [\n {\n \"$coalesce\": [\n {\n \"$ref\": \"request.providerOptions.km-kemei-seedream.layer_decomposition\"\n },\n false\n ]\n },\n true\n ]\n }\n ]\n },\n \"message\": \"提示词不能为空;仅图层分解模式可省略提示词。\"\n },\n {\n \"assert\": {\n \"$lte\": [\n {\n \"$len\": {\n \"$ref\": \"request.prompt\"\n }\n },\n 32000\n ]\n },\n \"message\": \"提示词超过宿主配置的 32000 字符上限。\"\n },\n {\n \"assert\": {\n \"$lte\": [\n {\n \"$len\": {\n \"$ref\": \"request.images\"\n }\n },\n 10\n ]\n },\n \"message\": \"Seedream 5.0 参考图最多 10 张。\"\n },\n {\n \"assert\": {\n \"$eq\": [\n {\n \"$len\": {\n \"$filter\": {\n \"from\": {\n \"$ref\": \"request.images\"\n },\n \"as\": \"media\",\n \"where\": {\n \"$eq\": [\n {\n \"$ref\": \"media.role\"\n },\n \"mask\"\n ]\n }\n }\n }\n },\n 0\n ]\n },\n \"message\": \"此协议不支持蒙版编辑。\"\n },\n {\n \"assert\": {\n \"$eq\": [\n {\n \"$len\": {\n \"$filter\": {\n \"from\": {\n \"$ref\": \"request.images\"\n },\n \"as\": \"media\",\n \"where\": {\n \"$gt\": [\n {\n \"$coalesce\": [\n {\n \"$ref\": \"media.metadata.bytes\"\n },\n 0\n ]\n },\n 31457280\n ]\n }\n }\n }\n },\n 0\n ]\n },\n \"message\": \"单张参考图不得超过 30 MB。\"\n },\n {\n \"assert\": {\n \"$or\": [\n {\n \"$in\": [\n {\n \"$lower\": {\n \"$trim\": {\n \"$coalesce\": [\n {\n \"$ref\": \"request.aspectRatio\"\n },\n \"2k\"\n ]\n }\n }\n },\n [\n \"\",\n \"auto\",\n \"1k\",\n \"1.5k\",\n \"2k\",\n \"1:1\",\n \"4:3\",\n \"3:4\",\n \"16:9\",\n \"9:16\",\n \"3:2\",\n \"2:3\",\n \"21:9\"\n ]\n ]\n },\n {\n \"$and\": [\n {\n \"$eq\": [\n {\n \"$len\": {\n \"$split\": [\n {\n \"$lower\": {\n \"$trim\": {\n \"$coalesce\": [\n {\n \"$ref\": \"request.aspectRatio\"\n },\n \"2k\"\n ]\n }\n }\n },\n \"x\"\n ]\n }\n },\n 2\n ]\n },\n {\n \"$gt\": [\n {\n \"$toInt\": {\n \"$at\": [\n {\n \"$split\": [\n {\n \"$lower\": {\n \"$trim\": {\n \"$coalesce\": [\n {\n \"$ref\": \"request.aspectRatio\"\n },\n \"2k\"\n ]\n }\n }\n },\n \"x\"\n ]\n },\n 0\n ]\n }\n },\n 0\n ]\n },\n {\n \"$gt\": [\n {\n \"$toInt\": {\n \"$at\": [\n {\n \"$split\": [\n {\n \"$lower\": {\n \"$trim\": {\n \"$coalesce\": [\n {\n \"$ref\": \"request.aspectRatio\"\n },\n \"2k\"\n ]\n }\n }\n },\n \"x\"\n ]\n },\n 1\n ]\n }\n },\n 0\n ]\n },\n {\n \"$gte\": [\n {\n \"$multiply\": [\n {\n \"$toInt\": {\n \"$at\": [\n {\n \"$split\": [\n {\n \"$lower\": {\n \"$trim\": {\n \"$coalesce\": [\n {\n \"$ref\": \"request.aspectRatio\"\n },\n \"2k\"\n ]\n }\n }\n },\n \"x\"\n ]\n },\n 0\n ]\n }\n },\n {\n \"$toInt\": {\n \"$at\": [\n {\n \"$split\": [\n {\n \"$lower\": {\n \"$trim\": {\n \"$coalesce\": [\n {\n \"$ref\": \"request.aspectRatio\"\n },\n \"2k\"\n ]\n }\n }\n },\n \"x\"\n ]\n },\n 1\n ]\n }\n }\n ]\n },\n 921600\n ]\n },\n {\n \"$lte\": [\n {\n \"$multiply\": [\n {\n \"$toInt\": {\n \"$at\": [\n {\n \"$split\": [\n {\n \"$lower\": {\n \"$trim\": {\n \"$coalesce\": [\n {\n \"$ref\": \"request.aspectRatio\"\n },\n \"2k\"\n ]\n }\n }\n },\n \"x\"\n ]\n },\n 0\n ]\n }\n },\n {\n \"$toInt\": {\n \"$at\": [\n {\n \"$split\": [\n {\n \"$lower\": {\n \"$trim\": {\n \"$coalesce\": [\n {\n \"$ref\": \"request.aspectRatio\"\n },\n \"2k\"\n ]\n }\n }\n },\n \"x\"\n ]\n },\n 1\n ]\n }\n }\n ]\n },\n 4624220\n ]\n },\n {\n \"$lte\": [\n {\n \"$toInt\": {\n \"$at\": [\n {\n \"$split\": [\n {\n \"$lower\": {\n \"$trim\": {\n \"$coalesce\": [\n {\n \"$ref\": \"request.aspectRatio\"\n },\n \"2k\"\n ]\n }\n }\n },\n \"x\"\n ]\n },\n 0\n ]\n }\n },\n {\n \"$multiply\": [\n {\n \"$toInt\": {\n \"$at\": [\n {\n \"$split\": [\n {\n \"$lower\": {\n \"$trim\": {\n \"$coalesce\": [\n {\n \"$ref\": \"request.aspectRatio\"\n },\n \"2k\"\n ]\n }\n }\n },\n \"x\"\n ]\n },\n 1\n ]\n }\n },\n 16\n ]\n }\n ]\n },\n {\n \"$lte\": [\n {\n \"$toInt\": {\n \"$at\": [\n {\n \"$split\": [\n {\n \"$lower\": {\n \"$trim\": {\n \"$coalesce\": [\n {\n \"$ref\": \"request.aspectRatio\"\n },\n \"2k\"\n ]\n }\n }\n },\n \"x\"\n ]\n },\n 1\n ]\n }\n },\n {\n \"$multiply\": [\n {\n \"$toInt\": {\n \"$at\": [\n {\n \"$split\": [\n {\n \"$lower\": {\n \"$trim\": {\n \"$coalesce\": [\n {\n \"$ref\": \"request.aspectRatio\"\n },\n \"2k\"\n ]\n }\n }\n },\n \"x\"\n ]\n },\n 0\n ]\n }\n },\n 16\n ]\n }\n ]\n }\n ]\n }\n ]\n },\n \"message\": \"size 必须为 1K / 1.5K / 2K 或合法像素尺寸;像素总数 921600–4624220,比例 1:16–16:1。\"\n },\n {\n \"assert\": {\n \"$in\": [\n {\n \"$coalesce\": [\n {\n \"$ref\": \"request.providerOptions.km-kemei-seedream.response_format\"\n },\n \"url\"\n ]\n },\n [\n \"url\",\n \"b64_json\"\n ]\n ]\n },\n \"message\": \"response_format 仅支持 url / b64_json。\"\n },\n {\n \"assert\": {\n \"$in\": [\n {\n \"$coalesce\": [\n {\n \"$ref\": \"request.providerOptions.km-kemei-seedream.output_format\"\n },\n \"jpeg\"\n ]\n },\n [\n \"jpeg\",\n \"png\"\n ]\n ]\n },\n \"message\": \"output_format 仅支持 jpeg / png。\"\n },\n {\n \"assert\": {\n \"$in\": [\n {\n \"$coalesce\": [\n {\n \"$ref\": \"request.providerOptions.km-kemei-seedream.background\"\n },\n \"opaque\"\n ]\n },\n [\n \"opaque\",\n \"transparent\"\n ]\n ]\n },\n \"message\": \"background 仅支持 opaque / transparent。\"\n },\n {\n \"assert\": {\n \"$or\": [\n {\n \"$ne\": [\n {\n \"$coalesce\": [\n {\n \"$ref\": \"request.providerOptions.km-kemei-seedream.background\"\n },\n \"opaque\"\n ]\n },\n \"transparent\"\n ]\n },\n {\n \"$and\": [\n {\n \"$eq\": [\n {\n \"$len\": {\n \"$ref\": \"request.images\"\n }\n },\n 1\n ]\n },\n {\n \"$eq\": [\n {\n \"$coalesce\": [\n {\n \"$ref\": \"request.providerOptions.km-kemei-seedream.output_format\"\n },\n \"jpeg\"\n ]\n },\n \"png\"\n ]\n }\n ]\n }\n ]\n },\n \"message\": \"透明背景仅用于一张带透明通道的参考图,输出必须为 png。\"\n },\n {\n \"assert\": {\n \"$in\": [\n {\n \"$coalesce\": [\n {\n \"$ref\": \"request.providerOptions.km-kemei-seedream.layer_decomposition\"\n },\n false\n ]\n },\n [\n true,\n false\n ]\n ]\n },\n \"message\": \"layer_decomposition 必须为布尔值。\"\n },\n {\n \"assert\": {\n \"$or\": [\n {\n \"$ne\": [\n {\n \"$coalesce\": [\n {\n \"$ref\": \"request.providerOptions.km-kemei-seedream.layer_decomposition\"\n },\n false\n ]\n },\n true\n ]\n },\n {\n \"$and\": [\n {\n \"$eq\": [\n {\n \"$len\": {\n \"$ref\": \"request.images\"\n }\n },\n 1\n ]\n },\n {\n \"$in\": [\n {\n \"$lower\": {\n \"$trim\": {\n \"$coalesce\": [\n {\n \"$ref\": \"request.aspectRatio\"\n },\n \"2k\"\n ]\n }\n }\n },\n [\n \"\",\n \"auto\",\n \"1k\",\n \"1.5k\",\n \"2k\"\n ]\n ]\n }\n ]\n }\n ]\n },\n \"message\": \"图层分解仅支持一张参考图,size 使用 1K / 1.5K / 2K / auto 档位。\"\n },\n {\n \"assert\": {\n \"$eq\": [\n {\n \"$ref\": \"request.providerOptions.km-kemei-seedream.sequential_image_generation\"\n },\n null\n ]\n },\n \"message\": \"sequential_image_generation 不适用于 Seedream 5.0 Pro / Flash,已拒绝请求。\"\n },\n {\n \"assert\": {\n \"$eq\": [\n {\n \"$ref\": \"request.providerOptions.km-kemei-seedream.sequential_image_generation_options\"\n },\n null\n ]\n },\n \"message\": \"sequential_image_generation_options 不适用于 Seedream 5.0 Pro / Flash,已拒绝请求。\"\n },\n {\n \"assert\": {\n \"$eq\": [\n {\n \"$ref\": \"request.providerOptions.km-kemei-seedream.web_search\"\n },\n null\n ]\n },\n \"message\": \"web_search 不适用于 Seedream 5.0 Pro / Flash,已拒绝请求。\"\n },\n {\n \"assert\": {\n \"$eq\": [\n {\n \"$ref\": \"request.providerOptions.km-kemei-seedream.stream\"\n },\n null\n ]\n },\n \"message\": \"stream 不适用于 Seedream 5.0 Pro / Flash,已拒绝请求。\"\n },\n {\n \"assert\": {\n \"$eq\": [\n {\n \"$ref\": \"request.providerOptions.km-kemei-seedream.tools\"\n },\n null\n ]\n },\n \"message\": \"tools 不适用于 Seedream 5.0 Pro / Flash,已拒绝请求。\"\n }\n ],\n \"create\": {\n \"method\": \"POST\",\n \"path\": \"/v1/images/generations\",\n \"contentType\": \"application/json\",\n \"body\": {\n \"model\": {\n \"$ref\": \"request.model\"\n },\n \"prompt\": {\n \"$ref\": \"request.prompt\"\n },\n \"n\": 1,\n \"size\": {\n \"$if\": {\n \"condition\": {\n \"$eq\": [\n {\n \"$coalesce\": [\n {\n \"$ref\": \"request.providerOptions.km-kemei-seedream.layer_decomposition\"\n },\n false\n ]\n },\n true\n ]\n },\n \"then\": {\n \"$if\": {\n \"condition\": {\n \"$in\": [\n {\n \"$lower\": {\n \"$trim\": {\n \"$coalesce\": [\n {\n \"$ref\": \"request.aspectRatio\"\n },\n \"2k\"\n ]\n }\n }\n },\n [\n \"\",\n \"auto\"\n ]\n ]\n },\n \"then\": \"auto\",\n \"else\": {\n \"$switch\": {\n \"cases\": [\n {\n \"when\": {\n \"$in\": [\n {\n \"$lower\": {\n \"$trim\": {\n \"$coalesce\": [\n {\n \"$ref\": \"request.aspectRatio\"\n },\n \"2k\"\n ]\n }\n }\n },\n [\n \"\",\n \"auto\",\n \"2k\"\n ]\n ]\n },\n \"then\": \"2K\"\n },\n {\n \"when\": {\n \"$eq\": [\n {\n \"$lower\": {\n \"$trim\": {\n \"$coalesce\": [\n {\n \"$ref\": \"request.aspectRatio\"\n },\n \"2k\"\n ]\n }\n }\n },\n \"1k\"\n ]\n },\n \"then\": \"1K\"\n },\n {\n \"when\": {\n \"$eq\": [\n {\n \"$lower\": {\n \"$trim\": {\n \"$coalesce\": [\n {\n \"$ref\": \"request.aspectRatio\"\n },\n \"2k\"\n ]\n }\n }\n },\n \"1.5k\"\n ]\n },\n \"then\": \"1.5K\"\n },\n {\n \"when\": {\n \"$eq\": [\n {\n \"$lower\": {\n \"$trim\": {\n \"$coalesce\": [\n {\n \"$ref\": \"request.aspectRatio\"\n },\n \"2k\"\n ]\n }\n }\n },\n \"1:1\"\n ]\n },\n \"then\": \"2048x2048\"\n },\n {\n \"when\": {\n \"$eq\": [\n {\n \"$lower\": {\n \"$trim\": {\n \"$coalesce\": [\n {\n \"$ref\": \"request.aspectRatio\"\n },\n \"2k\"\n ]\n }\n }\n },\n \"4:3\"\n ]\n },\n \"then\": \"2368x1776\"\n },\n {\n \"when\": {\n \"$eq\": [\n {\n \"$lower\": {\n \"$trim\": {\n \"$coalesce\": [\n {\n \"$ref\": \"request.aspectRatio\"\n },\n \"2k\"\n ]\n }\n }\n },\n \"3:4\"\n ]\n },\n \"then\": \"1776x2368\"\n },\n {\n \"when\": {\n \"$eq\": [\n {\n \"$lower\": {\n \"$trim\": {\n \"$coalesce\": [\n {\n \"$ref\": \"request.aspectRatio\"\n },\n \"2k\"\n ]\n }\n }\n },\n \"16:9\"\n ]\n },\n \"then\": \"2816x1584\"\n },\n {\n \"when\": {\n \"$eq\": [\n {\n \"$lower\": {\n \"$trim\": {\n \"$coalesce\": [\n {\n \"$ref\": \"request.aspectRatio\"\n },\n \"2k\"\n ]\n }\n }\n },\n \"9:16\"\n ]\n },\n \"then\": \"1584x2816\"\n },\n {\n \"when\": {\n \"$eq\": [\n {\n \"$lower\": {\n \"$trim\": {\n \"$coalesce\": [\n {\n \"$ref\": \"request.aspectRatio\"\n },\n \"2k\"\n ]\n }\n }\n },\n \"3:2\"\n ]\n },\n \"then\": \"2496x1664\"\n },\n {\n \"when\": {\n \"$eq\": [\n {\n \"$lower\": {\n \"$trim\": {\n \"$coalesce\": [\n {\n \"$ref\": \"request.aspectRatio\"\n },\n \"2k\"\n ]\n }\n }\n },\n \"2:3\"\n ]\n },\n \"then\": \"1664x2496\"\n },\n {\n \"when\": {\n \"$eq\": [\n {\n \"$lower\": {\n \"$trim\": {\n \"$coalesce\": [\n {\n \"$ref\": \"request.aspectRatio\"\n },\n \"2k\"\n ]\n }\n }\n },\n \"21:9\"\n ]\n },\n \"then\": \"3136x1344\"\n }\n ],\n \"default\": {\n \"$lower\": {\n \"$trim\": {\n \"$coalesce\": [\n {\n \"$ref\": \"request.aspectRatio\"\n },\n \"2k\"\n ]\n }\n }\n }\n }\n }\n }\n },\n \"else\": {\n \"$switch\": {\n \"cases\": [\n {\n \"when\": {\n \"$in\": [\n {\n \"$lower\": {\n \"$trim\": {\n \"$coalesce\": [\n {\n \"$ref\": \"request.aspectRatio\"\n },\n \"2k\"\n ]\n }\n }\n },\n [\n \"\",\n \"auto\",\n \"2k\"\n ]\n ]\n },\n \"then\": \"2K\"\n },\n {\n \"when\": {\n \"$eq\": [\n {\n \"$lower\": {\n \"$trim\": {\n \"$coalesce\": [\n {\n \"$ref\": \"request.aspectRatio\"\n },\n \"2k\"\n ]\n }\n }\n },\n \"1k\"\n ]\n },\n \"then\": \"1K\"\n },\n {\n \"when\": {\n \"$eq\": [\n {\n \"$lower\": {\n \"$trim\": {\n \"$coalesce\": [\n {\n \"$ref\": \"request.aspectRatio\"\n },\n \"2k\"\n ]\n }\n }\n },\n \"1.5k\"\n ]\n },\n \"then\": \"1.5K\"\n },\n {\n \"when\": {\n \"$eq\": [\n {\n \"$lower\": {\n \"$trim\": {\n \"$coalesce\": [\n {\n \"$ref\": \"request.aspectRatio\"\n },\n \"2k\"\n ]\n }\n }\n },\n \"1:1\"\n ]\n },\n \"then\": \"2048x2048\"\n },\n {\n \"when\": {\n \"$eq\": [\n {\n \"$lower\": {\n \"$trim\": {\n \"$coalesce\": [\n {\n \"$ref\": \"request.aspectRatio\"\n },\n \"2k\"\n ]\n }\n }\n },\n \"4:3\"\n ]\n },\n \"then\": \"2368x1776\"\n },\n {\n \"when\": {\n \"$eq\": [\n {\n \"$lower\": {\n \"$trim\": {\n \"$coalesce\": [\n {\n \"$ref\": \"request.aspectRatio\"\n },\n \"2k\"\n ]\n }\n }\n },\n \"3:4\"\n ]\n },\n \"then\": \"1776x2368\"\n },\n {\n \"when\": {\n \"$eq\": [\n {\n \"$lower\": {\n \"$trim\": {\n \"$coalesce\": [\n {\n \"$ref\": \"request.aspectRatio\"\n },\n \"2k\"\n ]\n }\n }\n },\n \"16:9\"\n ]\n },\n \"then\": \"2816x1584\"\n },\n {\n \"when\": {\n \"$eq\": [\n {\n \"$lower\": {\n \"$trim\": {\n \"$coalesce\": [\n {\n \"$ref\": \"request.aspectRatio\"\n },\n \"2k\"\n ]\n }\n }\n },\n \"9:16\"\n ]\n },\n \"then\": \"1584x2816\"\n },\n {\n \"when\": {\n \"$eq\": [\n {\n \"$lower\": {\n \"$trim\": {\n \"$coalesce\": [\n {\n \"$ref\": \"request.aspectRatio\"\n },\n \"2k\"\n ]\n }\n }\n },\n \"3:2\"\n ]\n },\n \"then\": \"2496x1664\"\n },\n {\n \"when\": {\n \"$eq\": [\n {\n \"$lower\": {\n \"$trim\": {\n \"$coalesce\": [\n {\n \"$ref\": \"request.aspectRatio\"\n },\n \"2k\"\n ]\n }\n }\n },\n \"2:3\"\n ]\n },\n \"then\": \"1664x2496\"\n },\n {\n \"when\": {\n \"$eq\": [\n {\n \"$lower\": {\n \"$trim\": {\n \"$coalesce\": [\n {\n \"$ref\": \"request.aspectRatio\"\n },\n \"2k\"\n ]\n }\n }\n },\n \"21:9\"\n ]\n },\n \"then\": \"3136x1344\"\n }\n ],\n \"default\": {\n \"$lower\": {\n \"$trim\": {\n \"$coalesce\": [\n {\n \"$ref\": \"request.aspectRatio\"\n },\n \"2k\"\n ]\n }\n }\n }\n }\n }\n }\n },\n \"image\": {\n \"$omitEmpty\": {\n \"$if\": {\n \"condition\": {\n \"$eq\": [\n {\n \"$len\": {\n \"$ref\": \"request.images\"\n }\n },\n 1\n ]\n },\n \"then\": {\n \"$first\": {\n \"$map\": {\n \"from\": {\n \"$sortByOrder\": {\n \"$ref\": \"request.images\"\n }\n },\n \"as\": \"media\",\n \"in\": {\n \"$ref\": \"media.value\"\n }\n }\n }\n },\n \"else\": {\n \"$map\": {\n \"from\": {\n \"$sortByOrder\": {\n \"$ref\": \"request.images\"\n }\n },\n \"as\": \"media\",\n \"in\": {\n \"$ref\": \"media.value\"\n }\n }\n }\n }\n }\n },\n \"response_format\": {\n \"$coalesce\": [\n {\n \"$ref\": \"request.providerOptions.km-kemei-seedream.response_format\"\n },\n \"url\"\n ]\n },\n \"output_format\": {\n \"$coalesce\": [\n {\n \"$ref\": \"request.providerOptions.km-kemei-seedream.output_format\"\n },\n \"jpeg\"\n ]\n },\n \"watermark\": {\n \"$coalesce\": [\n {\n \"$ref\": \"request.providerOptions.km-kemei-seedream.watermark\"\n },\n false\n ]\n },\n \"background\": {\n \"$coalesce\": [\n {\n \"$ref\": \"request.providerOptions.km-kemei-seedream.background\"\n },\n \"opaque\"\n ]\n },\n \"layer_decomposition\": {\n \"$omitEmpty\": {\n \"$ref\": \"request.providerOptions.km-kemei-seedream.layer_decomposition\"\n }\n }\n }\n },\n \"response\": {\n \"status\": \"succeeded\",\n \"images\": {\n \"$ref\": \"response.data\"\n },\n \"usage\": {\n \"$ref\": \"response.usage\"\n },\n \"errorPaths\": [\n \"error.code\",\n \"error.message\"\n ],\n \"messagePaths\": [\n \"error.message\"\n ]\n }\n }\n ]\n },\n \"documentation\": \"<当前插件的完整 documentation,由 README.md 与 docs/interface.md 拼接而成;为避免 JSON 递归,此处不重复展开正文。>\"\n}\n```\n\n" +} diff --git a/web/src/components/image-size-picker.tsx b/web/src/components/image-size-picker.tsx index 1b9f7c0e8..94b871c8f 100644 --- a/web/src/components/image-size-picker.tsx +++ b/web/src/components/image-size-picker.tsx @@ -2,12 +2,13 @@ import { useState } from "react"; import "./image-size-picker.css"; import { Input, Button } from "antd"; import type { ImageCapabilityConfig } from "@/lib/model-capabilities"; -import { IMAGE_RESOLUTIONS, imagePresetForRatio, imagePresetValue, imageQualityForSelection, imageQualityForTier, imageResolutionUsesQuality, imageSizePresets, imageTierAvailable } from "@/lib/image-size-presets"; +import { imageResolutionTiers, imagePresetForRatio, imagePresetValue, imageQualityForSelection, imageQualityForTier, imageResolutionUsesQuality, imageSizePresets, imageTierAvailable } from "@/lib/image-size-presets"; import { buildImageResolutionOptions, type ImageResolutionOption, type ImageResolutionTier } from "@/lib/image-resolution-tiers"; import { resolveImageRequestSize, validateImageSize } from "@/services/api/image-validation"; export function ImageSizePicker({ profile, size, quality, onChange }: { profile: ImageCapabilityConfig; size: string; quality?: string; onChange: (size: string, quality?: string) => void }) { + const IMAGE_RESOLUTIONS = imageResolutionTiers(profile); const presets = imageSizePresets(profile).filter((preset) => imageTierAvailable(profile, preset.tier)); const visibleTiers = IMAGE_RESOLUTIONS.filter((value) => presets.some((preset) => preset.tier === value)); const [chosenTier, setChosenTier] = useState("1k"); diff --git a/web/src/components/image-size-presets-editor.tsx b/web/src/components/image-size-presets-editor.tsx index 72d0929ae..04cd20e9b 100644 --- a/web/src/components/image-size-presets-editor.tsx +++ b/web/src/components/image-size-presets-editor.tsx @@ -4,10 +4,11 @@ import { Check, Plus } from "lucide-react"; import { Switch } from "@/components/ui/base/switch"; import "./image-size-picker.css"; import type { ImageCapabilityConfig } from "@/lib/model-capabilities"; -import { IMAGE_RATIOS, IMAGE_RESOLUTIONS, imagePresetForRatio, imageResolutionUsesQuality, imageSizeConfigWithPresets, imageSizePresets, imageTierAvailable } from "@/lib/image-size-presets"; +import { IMAGE_RATIOS, imageResolutionTiers, imagePresetForRatio, imageResolutionUsesQuality, imageSizeConfigWithPresets, imageSizePresets, imageTierAvailable } from "@/lib/image-size-presets"; import type { ImageResolutionTier } from "@/lib/image-resolution-tiers"; export function ImageSizePresetsEditor({ profile, disabled, onChange }: { profile: ImageCapabilityConfig; disabled?: boolean; onChange: (size: ImageCapabilityConfig["size"]) => void }) { + const IMAGE_RESOLUTIONS = imageResolutionTiers(profile); const presets = imageSizePresets(profile); const id = useId(); const [drafts, setDrafts] = useState>>({}); diff --git a/web/src/lib/image-resolution-tiers.ts b/web/src/lib/image-resolution-tiers.ts index 76a34b291..3bc5bd109 100644 --- a/web/src/lib/image-resolution-tiers.ts +++ b/web/src/lib/image-resolution-tiers.ts @@ -1,4 +1,4 @@ -export type ImageResolutionTier = "1k" | "2k" | "4k"; +export type ImageResolutionTier = "1k" | "1.5k" | "2k" | "4k"; export type ImageResolutionOption = { size: string; @@ -10,7 +10,7 @@ export type ImageResolutionOption = { export type ImageResolutionChoice = "auto" | ImageResolutionTier; -const tierOrder: ImageResolutionTier[] = ["1k", "2k", "4k"]; +const tierOrder: ImageResolutionTier[] = ["1k", "1.5k", "2k", "4k"]; const ratioOrder = ["1:1", "3:2", "2:3", "4:3", "3:4", "4:5", "5:4", "16:9", "9:16", "2:1", "1:2", "21:9"]; export function supportsImageResolutionPresets(size: { parameter: string; values: string[]; allowCustom?: boolean }) { diff --git a/web/src/lib/image-size-presets.ts b/web/src/lib/image-size-presets.ts index 645fb978f..6288e96d2 100644 --- a/web/src/lib/image-size-presets.ts +++ b/web/src/lib/image-size-presets.ts @@ -2,6 +2,10 @@ import { buildImageResolutionOptions, type ImageResolutionOption, type ImageReso import type { ImageCapabilityConfig } from "./model-capabilities"; export const IMAGE_RESOLUTIONS: ImageResolutionTier[] = ["1k", "2k", "4k"]; +// 1.5K 只在模型明确配置此档位时出现,既有模型的档位保持原有顺序。 +export function imageResolutionTiers(profile: ImageCapabilityConfig): ImageResolutionTier[] { + return profile.size.presets?.some((preset) => preset.tier === "1.5k") || profile.quality.values.includes("1.5k") ? ["1k", "1.5k", "2k", "4k"] : IMAGE_RESOLUTIONS; +} export const IMAGE_RATIOS = ["1:1", "16:9", "9:16", "4:3", "3:4", "3:2", "2:3", "4:5", "5:4", "21:9"]; const standardSizes: Record = { "1:1": ["1024x1024", "2048x2048", "2880x2880"], @@ -33,9 +37,9 @@ export function imagePresetForRatio(tier: ImageResolutionTier, input: string): I return width * h === height * w; }) || `${w}:${h}`; const index = IMAGE_RESOLUTIONS.indexOf(tier); - if (index < 0) throw new Error("分辨率仅支持 1K、2K、4K"); - const standard = standardSizes[ratio]?.[index]; - const pixels = [1_048_576, 4_194_304, 8_294_400][index]; + if (index < 0 && tier !== "1.5k") throw new Error("分辨率仅支持 1K、1.5K、2K、4K"); + const standard = tier === "1.5k" ? undefined : standardSizes[ratio]?.[index]; + const pixels = tier === "1.5k" ? 2_359_296 : [1_048_576, 4_194_304, 8_294_400][index]; let width = Math.round(Math.sqrt((pixels * w) / h) / 16) * 16; let height = Math.round((width * h) / w / 16) * 16; while (width * height > pixels || Math.max(width, height) > 3840) { @@ -52,12 +56,12 @@ export function imagePresetForRatio(tier: ImageResolutionTier, input: string): I } export function imageQualityForTier(profile: ImageCapabilityConfig, tier: ImageResolutionTier) { - const aliases = { "1k": ["1k", "low"], "2k": ["2k", "medium"], "4k": ["4k", "high"] }[tier]; + const aliases = { "1k": ["1k", "low"], "1.5k": ["1.5k"], "2k": ["2k", "medium"], "4k": ["4k", "high"] }[tier]; return profile.quality.supported ? profile.quality.values.find((value) => aliases.includes(value.toLowerCase())) : undefined; } export function imageResolutionUsesQuality(profile: ImageCapabilityConfig) { - return profile.size.parameter === "aspect_ratio" && IMAGE_RESOLUTIONS.some((tier) => imageQualityForTier(profile, tier)); + return profile.size.parameter === "aspect_ratio" && imageResolutionTiers(profile).some((tier) => imageQualityForTier(profile, tier)); } export function imageTierAvailable(profile: ImageCapabilityConfig, tier: ImageResolutionTier) { @@ -82,7 +86,7 @@ export function imageSizePresets(profile: ImageCapabilityConfig): ImageResolutio if (profile.size.parameter === "none") return []; if (profile.size.presets) return profile.size.presets; const pixels = buildImageResolutionOptions(profile.size.values); - const tiers = profile.size.parameter === "aspect_ratio" ? IMAGE_RESOLUTIONS.filter((tier) => imageQualityForTier(profile, tier)) : []; + const tiers = profile.size.parameter === "aspect_ratio" ? imageResolutionTiers(profile).filter((tier) => imageQualityForTier(profile, tier)) : []; const ratios = tiers.flatMap((tier) => profile.size.values.flatMap((ratio) => { try { diff --git a/web/src/lib/kemei-image-presets.ts b/web/src/lib/kemei-image-presets.ts new file mode 100644 index 000000000..58c759ba5 --- /dev/null +++ b/web/src/lib/kemei-image-presets.ts @@ -0,0 +1,17 @@ +import type { ImageResolutionOption, ImageResolutionTier } from "./image-resolution-tiers"; + +// Seedream 5.0 官方像素表;不能按通用像素阈值把 2K 横屏误分为 4K。 +export function kemeiSeedreamPresets(): ImageResolutionOption[] { + const ratios = ["1:1", "4:3", "3:4", "16:9", "9:16", "3:2", "2:3", "21:9"]; + const tiers: [ImageResolutionTier, string[]][] = [ + ["1k", ["1024x1024", "1152x864", "864x1152", "1424x800", "800x1424", "1248x832", "832x1248", "1568x672"]], + ["1.5k", ["1536x1536", "1792x1344", "1344x1792", "2048x1152", "1152x2048", "1872x1248", "1248x1872", "2352x1008"]], + ["2k", ["2048x2048", "2368x1776", "1776x2368", "2816x1584", "1584x2816", "2496x1664", "1664x2496", "3136x1344"]], + ]; + return tiers.flatMap(([tier, sizes]) => + sizes.map((size, index) => { + const [width, height] = size.split("x").map(Number); + return { tier, ratio: ratios[index], width, height, size }; + }), + ); +} diff --git a/web/src/lib/model-capabilities.ts b/web/src/lib/model-capabilities.ts index 97f5acdd8..e25ab68fe 100644 --- a/web/src/lib/model-capabilities.ts +++ b/web/src/lib/model-capabilities.ts @@ -1,6 +1,7 @@ import type { ModelProtocol, ModelProtocolWorkflow } from "@/lib/model-protocols"; import type { ImageResolutionOption, ImageResolutionTier } from "@/lib/image-resolution-tiers"; import { imagePresetForRatio } from "./image-size-presets"; +import { kemeiSeedreamPresets } from "./kemei-image-presets"; import { type WorkflowVideoFieldLike, workflowImageCapabilityConfig, workflowVideoCapabilityConfig } from "./model-capabilities-workflow"; import { resolveWorkflowVideoScreenSpec } from "./video-screen-specs"; @@ -220,6 +221,19 @@ export function defaultImageCapabilityConfig(protocol?: ModelProtocol, model = " outputFormat: { supported: true }, maxOutputs: 15, }; + if (protocol === "km-kemei-seedream") { + const presets = kemeiSeedreamPresets(); + image.references = { ...image.references, maxImages: 10, maskSupported: false }; + image.size = { parameter: "size", values: presets.map((preset) => preset.size), default: "2048x2048", allowCustom: true, presets }; + image.quality = { supported: false, values: [], default: "auto" }; + // 透明输出要求一张含 alpha 的参考图,宿主通用开关不能保证此条件。 + // 此能力通过插件命名空间的 background + output_format 显式配置。 + image.transparentBackground = { supported: false, default: false }; + image.responseFormat = { supported: true }; + image.outputFormat = { supported: true }; + image.maxOutputs = 1; + return image; + } if (protocol === "kacang-midjourney-special" || protocol === "kacang-midjourney-v7" || protocol === "kacang-midjourney") { const stable = protocol === "kacang-midjourney"; const extendedRatios = stable || protocol === "kacang-midjourney-v7"; @@ -493,7 +507,7 @@ export function normalizeImageValue(profile: ImageCapabilityConfig, value: { siz : value.quality && profile.quality.values.includes(value.quality) ? value.quality : profile.quality.default || "auto" - : requestedQuality === "1k" || requestedQuality === "2k" || requestedQuality === "4k" + : requestedQuality === "1k" || requestedQuality === "1.5k" || requestedQuality === "2k" || requestedQuality === "4k" ? requestedQuality : presetTier || profile.quality.default || "auto"; const count = String(Math.max(1, Math.min(profile.maxOutputs, Math.floor(Math.abs(Number(value.count)) || 1)))); diff --git a/web/src/services/api/logical-models.ts b/web/src/services/api/logical-models.ts index 0679b9a92..f49be1438 100644 --- a/web/src/services/api/logical-models.ts +++ b/web/src/services/api/logical-models.ts @@ -1,15 +1,10 @@ import { http } from "@/services/api/request"; import type { ModelTag } from "@/lib/model-tags"; +import type { ImageResolutionOption } from "@/lib/image-resolution-tiers"; export type InputConstraint = { min: number; max: number }; export type OptionConstraint = { values?: unknown[]; min?: number; max?: number; step?: number }; -export type CapabilityImageSizePreset = { - size: string; - tier: "1k" | "2k" | "4k"; - ratio: string; - width: number; - height: number; -}; +export type CapabilityImageSizePreset = ImageResolutionOption; export type CapabilityImageSize = { parameter?: "size" | "aspect_ratio"; allowCustom?: boolean; diff --git a/web/test/channel-model-editor-form.test.ts b/web/test/channel-model-editor-form.test.ts index 62340048b..06ae67b25 100644 --- a/web/test/channel-model-editor-form.test.ts +++ b/web/test/channel-model-editor-form.test.ts @@ -9,6 +9,13 @@ const protocols = [definition("disabled-text", "text", false), definition("text" const protocolsWithAudio = [...protocols, definition("audio", "audio")]; describe("channel model editor drafts", () => { + test("Kemei Seedream selection initializes the image profile and preserves pricing", () => { + const draft = initialChannelModelValues(null, protocols); + draft.priceTiers[0].unitPrice = 19; + const next = changeChannelModelCapability({ ...draft, capability: "image", protocol: "km-kemei-seedream", modelKey: "doubao-seedream-5-0-pro-260628" }, [definition("km-kemei-seedream", "image")]); + expect(next.capabilityConfig?.image).toMatchObject({ references: { maxImages: 10, maskSupported: false }, maxOutputs: 1, size: { default: "2048x2048" } }); + expect(next.priceTiers[0].unitPrice).toBe(19); + }); test("new drafts select an enabled protocol and never share price state", () => { const first = initialChannelModelValues(null, protocols); first.priceTiers[0].unitPrice = 42; diff --git a/web/test/image-size-presets.test.tsx b/web/test/image-size-presets.test.tsx index 003b16c29..6ac393167 100644 --- a/web/test/image-size-presets.test.tsx +++ b/web/test/image-size-presets.test.tsx @@ -11,6 +11,19 @@ import { resolveImageRequestSize, validateImageSize } from "../src/services/api/ import { buildGeminiImageGenerationConfig } from "../src/lib/gemini-image"; describe("统一图片分辨率与宽高比", () => { + test("Seedream 5.0 shows 1.5K and retains the exact official 2K wide size", () => { + const profile = defaultImageCapabilityConfig("km-kemei-seedream", "doubao-seedream-5-0-pro-260628"); + const intermediate = renderToStaticMarkup( {}} />); + expect(intermediate).toMatch(/aria-pressed="true"[^>]*>1\.5K/); + expect(intermediate).toContain("1536 × 1536 px"); + expect(intermediate).not.toMatch(/]*>4K/); + const wide = renderToStaticMarkup( {}} />); + expect(wide).toMatch(/aria-pressed="true"[^>]*>2K/); + expect(wide).toContain("2816 × 1584 px"); + const reopened = normalizeModelCapabilityConfig({ version: 1, image: profile }); + expect(reopened.image?.size.presets?.filter((p) => p.tier === "1.5k")).toHaveLength(8); + expect(resolveImageRequestSize(profile, "auto", "1536x1536")?.value).toBe("1536x1536"); + }); test("管理员档位开关按已有比例启用,部分比例不显示为关闭", () => { for (const model of ["midjourney-1k", "midjourney-2k"]) { const profile = defaultImageCapabilityConfig("cangyuan-midjourney-v82", model); diff --git a/web/test/model-capabilities.test.ts b/web/test/model-capabilities.test.ts index b4b9e07c1..59e36dc36 100644 --- a/web/test/model-capabilities.test.ts +++ b/web/test/model-capabilities.test.ts @@ -3,7 +3,23 @@ import test from "node:test"; // Bun 直接执行 TypeScript 测试时需要保留扩展名;生产 tsconfig 不包含 test/。 import { DEFAULT_VIDEO_PROMPT_MAX_CHARS, defaultModelCapabilityConfig, normalizeImageValue, normalizeVideoValue } from "../src/lib/model-capabilities.ts"; -import { imageTierAvailable } from "../src/lib/image-size-presets.ts"; +import { imageTierAvailable, imageResolutionTiers } from "../src/lib/image-size-presets.ts"; + +test("Kemei Seedream defaults retain official 1K/1.5K/2K sizes without a 4K preset", () => { + for (const model of ["doubao-seedream-5-0-pro-260628", "doubao-seedream-5-0-flash-260915"]) { + const image = defaultModelCapabilityConfig("km-kemei-seedream", model).image!; + assert.equal(image.references.maxImages, 10); + assert.equal(image.references.maskSupported, false); + assert.equal(image.maxOutputs, 1); + assert.equal(image.size.default, "2048x2048"); + assert.equal(image.quality.supported, false); + assert.equal(image.size.presets!.length, 24); + assert.deepEqual([...new Set(image.size.presets!.map((p) => p.tier))], ["1k", "1.5k", "2k"]); + assert.equal(image.size.presets!.find((p) => p.size === "2816x1584")?.tier, "2k"); + assert.ok(imageResolutionTiers(image).includes("1.5k")); + } + assert.deepEqual(imageResolutionTiers(defaultModelCapabilityConfig("openai-image").image!), ["1k", "2k", "4k"]); +}); test("text multimodal capability is not guessed from a model name", () => { for (const model of ["gpt-4o", "gemini-2.5-pro", "doubao-seed"]) { From 03506fb6fcf090463480e72458c45f1595c28f50 Mon Sep 17 00:00:00 2001 From: Codex Date: Fri, 9 Oct 2026 18:04:34 +0800 Subject: [PATCH 2/2] =?UTF-8?q?test(previs):=20=E6=B5=8F=E8=A7=88=E5=99=A8?= =?UTF-8?q?=E9=AA=8C=E6=94=B6=20-=20=E7=AD=89=E5=BE=85=E7=A5=96=E5=85=88?= =?UTF-8?q?=E5=8A=A8=E7=94=BB=E4=B8=8E=E6=82=AC=E5=81=9C=E5=9D=90=E6=A0=87?= =?UTF-8?q?=E7=A8=B3=E5=AE=9A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- web/scripts/previs-p0-chrome-e2e.mjs | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/web/scripts/previs-p0-chrome-e2e.mjs b/web/scripts/previs-p0-chrome-e2e.mjs index 1d90be026..bdb5aca59 100644 --- a/web/scripts/previs-p0-chrome-e2e.mjs +++ b/web/scripts/previs-p0-chrome-e2e.mjs @@ -277,6 +277,13 @@ async function connectCdp(cdpPort) { const rect = el.getBoundingClientRect(); const style = getComputedStyle(el); if (rect.width <= 0 || rect.height <= 0 || style.display === "none" || style.visibility === "hidden" || style.pointerEvents === "none" || Number(style.opacity) <= 0 || el.matches(":disabled") || el.getAttribute("aria-disabled") === "true") return null; + // 入场缩放作用在弹窗祖先上,按钮本身的 opacity 和动画可能都是正常值。 + // 软件渲染可延迟首帧,让初始缩放坐标看似稳定;必须检查整条祖先链。 + for (let ancestor = el; ancestor; ancestor = ancestor.parentElement) { + const ancestorStyle = getComputedStyle(ancestor); + if (ancestorStyle.display === "none" || ancestorStyle.visibility === "hidden" || Number(ancestorStyle.opacity) <= 0) return null; + if (ancestor.getAnimations().some((animation) => animation.pending || animation.playState === "running")) return null; + } const x = rect.left + rect.width / 2; const y = rect.top + rect.height / 2; if (x < 0 || y < 0 || x >= innerWidth || y >= innerHeight) return null; @@ -289,6 +296,7 @@ async function connectCdp(cdpPort) { // 点击落到遮罩上(mask.closable=false)就完全无效("留在预演台" 曾这样偶发失败)。 const deadline = Date.now() + 8000; let previous = null; + let hovered = null; let stableSince = 0; let box = null; while (Date.now() < deadline) { @@ -296,8 +304,13 @@ async function connectCdp(cdpPort) { if (next && previous && next.x === previous.x && next.y === previous.y) { if (!stableSince) stableSince = Date.now(); if (Date.now() - stableSince >= 300) { - box = next; - break; + if (hovered && hovered.x === next.x && hovered.y === next.y) { + box = next; + break; + } + await send("Input.dispatchMouseEvent", { type: "mouseMoved", x: next.x, y: next.y, button: "left", buttons: 0 }); + hovered = next; + stableSince = 0; } } else { stableSince = 0; @@ -310,7 +323,7 @@ async function connectCdp(cdpPort) { return false; } const point = { x: box.x, y: box.y, button: "left", pointerType: "mouse" }; - await send("Input.dispatchMouseEvent", { type: "mouseMoved", ...point, buttons: 0 }); + // 移入鼠标后已重新等待动画和坐标稳定,避免 hover 引起的位移造成误点。 await send("Input.dispatchMouseEvent", { type: "mousePressed", ...point, buttons: 1, clickCount: 1 }); await sleep(50); await send("Input.dispatchMouseEvent", { type: "mouseReleased", ...point, buttons: 0, clickCount: 1 });