From 35c080b65c0bd51890f69c2ce772318eb6e4baf4 Mon Sep 17 00:00:00 2001 From: Gal Eyal Date: Wed, 29 Jul 2026 12:42:57 +0300 Subject: [PATCH 01/15] fix(auth): keep the OAuth return target through the provider callback MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit OAuth2AuthorizationRequestRedirectFilter invokes the resolver on every request in the chain and the delegate answers null for anything that is not an authorization request. Recording the return target on those calls cleared it again on the next request without a returnTo parameter — the provider callback included, which this filter processes before login succeeds. The success handler therefore always found an empty session attribute and fell back to the default target, so returnTo never worked. Guard the write on a non-null authorization request. As a side effect, anonymous API requests no longer allocate a session via getSession(). Signed-off-by: Gal Eyal --- ...HubOAuth2AuthorizationRequestResolver.java | 21 ++++++++++++++----- ...Auth2AuthorizationRequestResolverTest.java | 19 +++++++++++++++++ 2 files changed, 35 insertions(+), 5 deletions(-) diff --git a/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/oauth/SkillHubOAuth2AuthorizationRequestResolver.java b/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/oauth/SkillHubOAuth2AuthorizationRequestResolver.java index c72b1d9d..5cd28902 100644 --- a/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/oauth/SkillHubOAuth2AuthorizationRequestResolver.java +++ b/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/oauth/SkillHubOAuth2AuthorizationRequestResolver.java @@ -28,15 +28,26 @@ public class SkillHubOAuth2AuthorizationRequestResolver @Override public OAuth2AuthorizationRequest resolve(HttpServletRequest request) { - OAuth2AuthorizationRequest authorizationRequest = delegate.resolve(request); - oauthLoginFlowService.rememberReturnTo(request); - return authorizationRequest; + return rememberIfAuthorizationRequest(request, delegate.resolve(request)); } @Override public OAuth2AuthorizationRequest resolve(HttpServletRequest request, String clientRegistrationId) { - OAuth2AuthorizationRequest authorizationRequest = delegate.resolve(request, clientRegistrationId); - oauthLoginFlowService.rememberReturnTo(request); + return rememberIfAuthorizationRequest(request, delegate.resolve(request, clientRegistrationId)); + } + + /** + * {@code OAuth2AuthorizationRequestRedirectFilter} calls the resolver on every request in the + * chain, not only on authorization requests; the delegate simply answers null for the rest. + * Recording the return target on those calls would clear it again on the very next request — + * including the provider callback, which carries no {@code returnTo} and is processed by this + * filter before authentication succeeds. Only an actual authorization request may touch it. + */ + private OAuth2AuthorizationRequest rememberIfAuthorizationRequest( + HttpServletRequest request, OAuth2AuthorizationRequest authorizationRequest) { + if (authorizationRequest != null) { + oauthLoginFlowService.rememberReturnTo(request); + } return authorizationRequest; } } diff --git a/server/skillhub-auth/src/test/java/com/iflytek/skillhub/auth/oauth/OAuth2AuthorizationRequestResolverTest.java b/server/skillhub-auth/src/test/java/com/iflytek/skillhub/auth/oauth/OAuth2AuthorizationRequestResolverTest.java index 357ada33..9cef26b2 100644 --- a/server/skillhub-auth/src/test/java/com/iflytek/skillhub/auth/oauth/OAuth2AuthorizationRequestResolverTest.java +++ b/server/skillhub-auth/src/test/java/com/iflytek/skillhub/auth/oauth/OAuth2AuthorizationRequestResolverTest.java @@ -54,6 +54,25 @@ class OAuth2AuthorizationRequestResolverTest { .isEqualTo("/dashboard/publish?draft=1"); } + @Test + void resolve_keepsReturnToOnNonAuthorizationRequests() { + // The redirect filter runs the resolver on every request in the chain, the provider + // callback included. That request carries no returnTo, so treating it as an + // authorization request would clear the target before the success handler reads it. + MockHttpServletRequest authorization = new MockHttpServletRequest("GET", "/oauth2/authorization/github"); + authorization.setParameter("returnTo", "/device"); + resolver.resolve(authorization, "github"); + HttpSession session = authorization.getSession(false); + + MockHttpServletRequest callback = new MockHttpServletRequest("GET", "/login/oauth2/code/github"); + callback.setParameter("code", "auth-code"); + callback.setSession(session); + + assertThat(resolver.resolve(callback)).isNull(); + assertThat(session.getAttribute(OAuthLoginRedirectSupport.SESSION_RETURN_TO_ATTRIBUTE)) + .isEqualTo("/device"); + } + @Test void resolve_ignoresUnsafeReturnTo() { MockHttpServletRequest request = new MockHttpServletRequest("GET", "/oauth2/authorization/github"); From 1c3e9be9e9ceee225973de55044d440100a3338e Mon Sep 17 00:00:00 2001 From: FenjuFu <92919259+FenjuFu@users.noreply.github.com> Date: Tue, 25 Aug 2026 13:49:08 +0800 Subject: [PATCH 02/15] fix(web): self-host Inter and JetBrains Mono fonts (#753) * fix(web): self-host Inter and JetBrains Mono fonts web/index.html loaded Inter + JetBrains Mono from fonts.googleapis.com at runtime. On networks where Google Fonts is slow or unreachable (e.g. CN), that stylesheet blocks first paint for tens of seconds (#716). Vendor the same woff2 (from the @fontsource distribution) under web/public/fonts and declare them in fonts.css with latin/latin-ext unicode-range splits and font-display: swap, then point index.html at the local stylesheet and drop the fonts.googleapis.com / fonts.gstatic.com hosts from the page CSP. Only Inter and JetBrains Mono were ever fetched from the CDN, so families and weights are unchanged; no external font requests remain. Closes #716 Signed-off-by: FenjuFu <92919259+FenjuFu@users.noreply.github.com> * docs(web): record vendored font licenses Signed-off-by: XiaoSeS <87064762+XiaoSeS@users.noreply.github.com> --------- Signed-off-by: FenjuFu <92919259+FenjuFu@users.noreply.github.com> Signed-off-by: XiaoSeS <87064762+XiaoSeS@users.noreply.github.com> Co-authored-by: XiaoSeS <87064762+XiaoSeS@users.noreply.github.com> --- web/index.html | 9 +- web/public/fonts/LICENSE.md | 101 ++++++++++++++++ web/public/fonts/fonts.css | 109 ++++++++++++++++++ web/public/fonts/inter-latin-400-normal.woff2 | Bin 0 -> 23664 bytes web/public/fonts/inter-latin-500-normal.woff2 | Bin 0 -> 24272 bytes web/public/fonts/inter-latin-600-normal.woff2 | Bin 0 -> 24452 bytes web/public/fonts/inter-latin-700-normal.woff2 | Bin 0 -> 24356 bytes .../fonts/inter-latin-ext-400-normal.woff2 | Bin 0 -> 35000 bytes .../fonts/inter-latin-ext-500-normal.woff2 | Bin 0 -> 36024 bytes .../fonts/inter-latin-ext-600-normal.woff2 | Bin 0 -> 36260 bytes .../fonts/inter-latin-ext-700-normal.woff2 | Bin 0 -> 36244 bytes .../jetbrains-mono-latin-400-normal.woff2 | Bin 0 -> 21168 bytes .../jetbrains-mono-latin-500-normal.woff2 | Bin 0 -> 21832 bytes .../jetbrains-mono-latin-ext-400-normal.woff2 | Bin 0 -> 7336 bytes .../jetbrains-mono-latin-ext-500-normal.woff2 | Bin 0 -> 7528 bytes 15 files changed, 215 insertions(+), 4 deletions(-) create mode 100644 web/public/fonts/LICENSE.md create mode 100644 web/public/fonts/fonts.css create mode 100644 web/public/fonts/inter-latin-400-normal.woff2 create mode 100644 web/public/fonts/inter-latin-500-normal.woff2 create mode 100644 web/public/fonts/inter-latin-600-normal.woff2 create mode 100644 web/public/fonts/inter-latin-700-normal.woff2 create mode 100644 web/public/fonts/inter-latin-ext-400-normal.woff2 create mode 100644 web/public/fonts/inter-latin-ext-500-normal.woff2 create mode 100644 web/public/fonts/inter-latin-ext-600-normal.woff2 create mode 100644 web/public/fonts/inter-latin-ext-700-normal.woff2 create mode 100644 web/public/fonts/jetbrains-mono-latin-400-normal.woff2 create mode 100644 web/public/fonts/jetbrains-mono-latin-500-normal.woff2 create mode 100644 web/public/fonts/jetbrains-mono-latin-ext-400-normal.woff2 create mode 100644 web/public/fonts/jetbrains-mono-latin-ext-500-normal.woff2 diff --git a/web/index.html b/web/index.html index 63a5423d..dc99437f 100644 --- a/web/index.html +++ b/web/index.html @@ -6,13 +6,14 @@ SkillHub - - - + + diff --git a/web/public/fonts/LICENSE.md b/web/public/fonts/LICENSE.md new file mode 100644 index 00000000..8dee9361 --- /dev/null +++ b/web/public/fonts/LICENSE.md @@ -0,0 +1,101 @@ +# Vendored web fonts + +The font files in this directory are vendored from Fontsource packages and +served by SkillHub to avoid a runtime dependency on Google Fonts. + +| Font | Source package | Version | License | Copyright | +| --- | --- | --- | --- | --- | +| Inter | `@fontsource/inter` | `5.3.0` | SIL Open Font License 1.1 | Copyright 2016 The Inter Project Authors | +| JetBrains Mono | `@fontsource/jetbrains-mono` | `5.3.0` | SIL Open Font License 1.1 | Copyright 2020 The JetBrains Mono Project Authors | + +The vendored `.woff2` files are unmodified copies from the Fontsource package +distributions. + +## SIL Open Font License 1.1 + +``` +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. +``` diff --git a/web/public/fonts/fonts.css b/web/public/fonts/fonts.css new file mode 100644 index 00000000..7d643dab --- /dev/null +++ b/web/public/fonts/fonts.css @@ -0,0 +1,109 @@ +/* + * Self-hosted web fonts (Inter, JetBrains Mono). + * + * Replaces the runtime dependency on fonts.googleapis.com / fonts.gstatic.com, + * which is slow or unreachable on some networks and blocks first paint + * (see #716). Files are the same woff2 Google serves, vendored from the + * @fontsource distribution. latin / latin-ext are split by unicode-range so + * the browser only fetches the subset a glyph actually needs. + */ + +/* ---------- Inter ---------- */ +@font-face { + font-family: 'Inter'; + font-style: normal; + font-weight: 400; + font-display: swap; + src: url('/fonts/inter-latin-400-normal.woff2') format('woff2'); + unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; +} +@font-face { + font-family: 'Inter'; + font-style: normal; + font-weight: 400; + font-display: swap; + src: url('/fonts/inter-latin-ext-400-normal.woff2') format('woff2'); + unicode-range: U+0100-02AF, U+0304, U+0308, U+0329, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF; +} +@font-face { + font-family: 'Inter'; + font-style: normal; + font-weight: 500; + font-display: swap; + src: url('/fonts/inter-latin-500-normal.woff2') format('woff2'); + unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; +} +@font-face { + font-family: 'Inter'; + font-style: normal; + font-weight: 500; + font-display: swap; + src: url('/fonts/inter-latin-ext-500-normal.woff2') format('woff2'); + unicode-range: U+0100-02AF, U+0304, U+0308, U+0329, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF; +} +@font-face { + font-family: 'Inter'; + font-style: normal; + font-weight: 600; + font-display: swap; + src: url('/fonts/inter-latin-600-normal.woff2') format('woff2'); + unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; +} +@font-face { + font-family: 'Inter'; + font-style: normal; + font-weight: 600; + font-display: swap; + src: url('/fonts/inter-latin-ext-600-normal.woff2') format('woff2'); + unicode-range: U+0100-02AF, U+0304, U+0308, U+0329, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF; +} +@font-face { + font-family: 'Inter'; + font-style: normal; + font-weight: 700; + font-display: swap; + src: url('/fonts/inter-latin-700-normal.woff2') format('woff2'); + unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; +} +@font-face { + font-family: 'Inter'; + font-style: normal; + font-weight: 700; + font-display: swap; + src: url('/fonts/inter-latin-ext-700-normal.woff2') format('woff2'); + unicode-range: U+0100-02AF, U+0304, U+0308, U+0329, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF; +} + +/* ---------- JetBrains Mono ---------- */ +@font-face { + font-family: 'JetBrains Mono'; + font-style: normal; + font-weight: 400; + font-display: swap; + src: url('/fonts/jetbrains-mono-latin-400-normal.woff2') format('woff2'); + unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; +} +@font-face { + font-family: 'JetBrains Mono'; + font-style: normal; + font-weight: 400; + font-display: swap; + src: url('/fonts/jetbrains-mono-latin-ext-400-normal.woff2') format('woff2'); + unicode-range: U+0100-02AF, U+0304, U+0308, U+0329, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF; +} +@font-face { + font-family: 'JetBrains Mono'; + font-style: normal; + font-weight: 500; + font-display: swap; + src: url('/fonts/jetbrains-mono-latin-500-normal.woff2') format('woff2'); + unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; +} +@font-face { + font-family: 'JetBrains Mono'; + font-style: normal; + font-weight: 500; + font-display: swap; + src: url('/fonts/jetbrains-mono-latin-ext-500-normal.woff2') format('woff2'); + unicode-range: U+0100-02AF, U+0304, U+0308, U+0329, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF; +} diff --git a/web/public/fonts/inter-latin-400-normal.woff2 b/web/public/fonts/inter-latin-400-normal.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..f15b025d666269fb07daf0263e54231481e4289c GIT binary patch literal 23664 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 literal 0 HcmV?d00001 diff --git a/web/public/fonts/inter-latin-500-normal.woff2 b/web/public/fonts/inter-latin-500-normal.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..54f0a595dd3c76344a980109e7517ed42416de93 GIT binary patch literal 24272 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 literal 0 HcmV?d00001 diff --git a/web/public/fonts/inter-latin-700-normal.woff2 b/web/public/fonts/inter-latin-700-normal.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..a68fb101cd9224aa7bb6ca80356b5132c40c6f3f GIT binary patch literal 24356 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 Ikp=(&0G%$$k^lez literal 0 HcmV?d00001 diff --git a/web/public/fonts/inter-latin-ext-600-normal.woff2 b/web/public/fonts/inter-latin-ext-600-normal.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..bad65d4b7a4e7ded6d25e998a8441fb256de9a79 GIT binary patch literal 36260 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 iiaG zfTf8p$b*RhaVxOWp_r&jp+*=$ydCwg&HFSwWzP%SFPKUuY)H9@rkzxALgDSZ2xE~x zEC-`gNJovu*fP3$hW_G&Je~zNQdL)>by_P=$qUc-22MQg=ex)&)PsEC71-KTtAQb& zez@tx@pl;-Zza=$2GFqb>!5Mcf>rkPy%*V;7OkC~rsJXPu|=!0##`5L3G$KPyE6}5 zyJXEiQ7793(-9dFN%@=ifpP}`y<_NL&?7QSS{O}g^|I2?6c50yzrf6PQw%B8hyV!3 zVp#M@kIydiDd(o|tw6vF9h2zEJ#=|eFHoyPM>D|$*4lmwM}ViPsN^P8ty8PVJ65Ao zgYA1bkReCz&BEg5)JI?-pl^zlh$I?Bev=dNq5l%y4d_ zzp52BtyaeXk>c^Tl?uJ;#g$6LO4WRIofz(=ba-@qub6oF5E@ik6ROq-jk#cR6~H44 z=8xs4WDBybD_2b}4eDrPdrIf+(=h~lwRPLM|3Y!=PYns%rYztZx;kd{pw0VkBQ;Q( z5$%cQs6v#|Tlq_Hh)rX-Tn zI+f~}w3;kJXB1XSuW&Isg>=-IV9Uu_0DO1~QNI?!!fMb@8tTmy8uBN&V>r*{6RF0k ziRy3*NM}5Cw%M-+^b;hIv35~c!E)M(0@0Y_3ao`AfE;T55O~aT)F*&DP2T#p$CwBW zy&Yar;`^?AK#jx4cHNx5!<5AJ|LyDLj$(jMG+s54#peCMDYJKsSKu8+6s0?d$-Rni$!P2HHa);WI-%hDR=47d(|yCVLLkt!S?Pjb4c{`@#Xa*^_#K7E*#y zsjAe;HN5hYx{{bmr49+JiKzgqQISE?9#$-tCCf>Ns0Ga(a@NX=)`$;puoRA@%~vIp zSkct0sbdL7m(ywe6jRWeRDFjd>FhamYA%w4Z_qGdY}b$8O$Jr-$C{^1wC1H9BTW^S zYl76mBcM4_dUyQ2oYMmVHa#+$5#Fqy2Mv6%)D;(9%IWAK5UeBtYs!7yDpb}i$(F+_Ts6cKr`^Om+-fXH-WEF;H!}Exc(uli;Vl**T(=@g z9k*KV*7~0BLCWg!bj?gG-(5m3Yr0iMj)h7F2u&S` z0Y#N-BI`D443y?uS51diI!6C|+WW8a%+5)ehp0V(a7sw^>~qFrJ5;>wrCFi2+)8zQ z2EXQ8vinbjNH4aJ7o&HCdF+vi69|44zQ1kgDJ!Sp!@}LkmgZmaM-yI*Xt>kl0@1uH zWIj~HNzdHNjR@rt_RnGVOg;`Nie zumgla<6SM!5?`dDC9~z*{}fMlc4>_c3~<+R_3Hc5UDdfJn+N1~Dtkp@tfMk9_C47d zuW)!%^q_y(K1|s#oe@l18{1B;p$6xppl}_nw_3FrV8>*tyCR*vJwDG4=LUoTpFaxc zLxWBm|HI(&0SLgyj}b|{w}yig_g~1t51ZUT0RK>j0sUVWI|VmxLU*UDfG__=Xoq1I zyFLKKzi2(LaHv0i9QFUKGzm=C5%Zq)Iop&;{acP0e0iLL;Xe!1f6aVX)KdcO)gEHw zfnq)1~oH#RSs zXFj{&T;#={d^vU#Io(srl)pITH$}p{b1^rU7qo`YQ;p7CkS(O~&k)K6Rg32tHwqdz zwa?uC>fFleL0MZu&=4LV!p8(cct1wQX ziIGsfz@OA_I0_G-ud8u|>I7!v6fCKWTCtndi3gagm*~^1<0rbqdc0k93ZyB=dgCq} zWuM_4=z%$^s688Pw0>Gr4kB(O`m|47YmT09&9>6(OQN#oR@zGuZ6|AqdniAl^~UA7 z)$#_*rwhNzUI~cmWuZr_=&+eBV+gTwG^|OqV5_x}9p3oFBnr3uJ;u ziaB4Vt%*5Tg%nlWev+qv`(aQcd92^%V$<-$#a}dm#qYc)eX+*AiF5RWlz4VP}Y~?5J${0 z-i5$ApyUw)zs`HV?LY+r{o=UqSU=I^`jDRwfY!FI9b$DMw4$NL^wT!%!Rm!3iT^)H|~d ziCqs!Uw?&$7lSi35<-_b;@)zpO9dIKr|TtXrOOhNjEdCIN{VG!8rG!tumr&HgjZ=^ z1TPHv3aZRsj{xe`4@`vTh`U|Z_-fC7a9)aw1FT6Vxk}IC8XrCNr~VU3#F&z^bHZK! zie_C?2=P=|5D~jhbN;Y1ITrU>N=@a}4L^80IUba}g0gruN$X{5iqI%2DjPSy(zpzC zwaL5wxkX6QZwg!Qzzp{#$inEcH&}7SldY5{rHWHalF_7DDKjW5O=8oDm|ti&!gxPo z$9ln8WLN}uz;aGSc>sRhp5k7rcC`deYnq%@?I?kB-AKO z2H&ZSUp?RIQdQGZcRbhybvBfN*eXPKImYCU8^uEu094;TLLtQka`#q}e*8!khsQ4e;{o z_c`va;u-$W@fecF0&WKA3etjFS(km^ltiMXXq$sOij)n|2?^JqkwRW{%d^bYB^ws# zzV*J(xzxaVwSC}Lz>A61tPUpQJ?RPVps*u$zBo4)x(;I<4Wm@y^(G^5h(WVQa z1%Nh)C(Jf*0BHwK_+l|20DRGqb{8WCT!1iVt~zlAZw9c;XEO%Fz-9tMEN&*DihG-B za6qY0U2ife^jO^F=JxCYV~bMkM%bJY7PC3i$GdP&dlhp(efAwj<9rnA`;?%6$+cne z=67kfO!PB0aL646PeXET|MvfIQlkj@>pGeTGrto*1U0qeqrAY3e|YA=vOPH!O=Utp z6nsQNR!LKbKU7?6X+>h^q2ihJC65wC-DZY2{;zHvK9sHAsma7vx}>nd%t*ihUfJ5iJ;WapTT6YIb0q;q6VQzERo9O3Z+V|(E@-V zFa!#NBao;_mSM2CD250`F9d)PjG$OlwRrzGkx7bXSdNdTMGz%YnOvb%m7!0g)#(jJ zli6am*&R-o+v5cx7(ppHo22F9K7+|(b8_{Vmz!BZpM4^+L@JZ#?zysdm0F|K>GRWU zG?^_{o894bxjjYteR>o0={I1|kl{olM*EE!huj5#{by3KWC{vVk`-0c4Kr~3E!%NH z1q%Ta3JxJmxCoJrDA8iXw&KJ~=w~2Nl4K|;QaflESZUJX;1Lj!kWo<4Wb`nut~?g`tP0o^vMb`?%uTTprOK46 z2&z(*YBg%rsn@`zQIlpZTD9e_U58Fxy7d5nATR_9gCmeAGzN>q6NserR#X0Q{S%GW zGg>V7PGMRC4b#wRd^z!!c_4Ain z>}%;VWXh5)N3J~i3KWtkQtSt*5~a$Nt5B&*b%{G_)s?!hL8B(kTC{2_^VrWip7`yL zzdCg4(yd3YKK;SGHE77N5uF9DaMKG`hKkrIodft)0Dtqm#3XtDC!rrnub)4I$zpT3 zJib6E5=*2qxk9N@YqS6$2n>P3;0PoNjltsZ1R{w{q0;CKCX3DC>ggL88X23InweWz zT3OrJ+SxleIyt+zy19FJdU^Z!`uT$pjGz=Mjm}`Q*c>j8FA$2v5~)nCP^#1#txj(+ zn#>lf&F*lz+#at_uRi?-3>q?Q#HcajWF}1dLIwEZ0K$Z!ESHCuZ+`eiW3V_p6*Ud5 zuY+y5qRk+_qvb6Pv3D(N5pR3TTPAa@t$56fIhe!M9;>3R#Tvvg4x<&Vpia!o6bY%G zCtAHS=?YhX-_x{u;0_z51)x8V&XNzrrM3LL>M(~8&+L&Gv6YiDo=WNdE-lrfNFvN4 zhx#1%F6s5^K1jTF8^ZR+d-BP8$5gNCx+kil8n9d<)?!N%F4k(0!9rv$Z^qs`Fh|u% zV#xVQYy$#5VVRDBFjfpg)(yn6RhtA^>JV!|$BrRk$iIsaV8mxUlWkb`D4jY%Q5Vhl z8CFK2s1wUa(yB)WwR)tKjTYROO?xeZ9`jST%LJrLg|HG;2&&a#zRJ|Fs<)R&f35nG zL4Xk;GQ%++S)N5|b+wFT&iGX8WiDS^skMl~I!pF>>Rk2E%d;w;C|wk9`Iax_lr6S{ zOVRpi2L9gEaL1ts#33XD5nALZm;%K5%OZ=sSHs*xPIk#-{twGz;cMt}8QeqJwgqu2 zYLRQ}^U|)XhsQ=AXKTSd&_`Lf2z4bass#|t+tM{F8%8JVjS1UIqpm<2LyJ4_wuysu zE@jJ=$yOe4iC`|5!333lwrcVcJ{a=!6^5p2Ys&K5B}*Q+8C(ltdR!(Cnn zb^a@9xajeP8Di?QNOTH4C~UWg;v^t)aL(aCf~?LQJF8um6K2{>8ST74H_zah0=@*h z%2hyT!gMB#Hok*UC+0D?^pm&mb4j&J4;NL_js2g$CytLHw-<2ueAJCwm%ZlldnF%1 zH&Eg9ufoRB^HeN0U!EqWs{viyFS+k2k9+!YuU5#OdM!_y<(BgB>ePi<7H5ZarfK34 zpJx$P#xIh8TJ1jm#Z(g_I+=t{GEtib?qz0C|E=4Jp`93-exBx$F^98%RkEsdo%mA} zsQk-FWJI9nImr%rR)B51kqGD!N8+>|a72^>*| zKN`FD{kGLlIbqT+i$c9-Q0Kq8U zHvNSQ^5TMj35 zJWCurQyNVtTwPl1CX~e~jlRq3xH{4pECi!7AvzOAIdJ66wOQE3?vl5U-k*c*Uhq?#HNgy+6*Q_Gcg^d5=r&*RWwXKkT(4zfq&-Ng5|x+W!FB zdTGMxy77C~Yyjwg_kAO>2E^{&6}4-ZWq_Lo7W?d<%{ZhefYnTpNq|R-w*C-m#SR({dRMX8lwLYhSBr& z+c9!v)R=+2j9Y5LGLxoEo3Xb!^A;>xVWm}8TQd;nBFoHK%!&|ogyYuje!E;}d(w={ z=PEtbgp+o2A|bUC%;2n_i3=!5^iH=JF8pai=~C|ilUlv@x|X{>abna&^bQnpEl2W} zeHe{*gUJ3CvhRg{{t$LbX%T#X+`;Du&TT6>bp|FQ9<`Nmqof>V za;zu&%a!|6b;%vdJ#~#*fSzypcrp%+$zv zUi1IO4*RjZzigrecJ8}s;^5%RbQozi#trK=hXuY zk#btb!-nE`5(Tcq#11U;JaRs`nrN7zb~9cm+H2Az!M1xh{o}kIgu{H<++RYo*bdj+ zdHz-s)e5>nelFZ)s&zxd47^opn@7KgE@5*~xttFcu2*GLzd~-&Is_qW^1Ns^$!BCE z<%)}dVIF3lX9YlrMrSZ#1jTS7@_M!1KZ^c1|Gr(pE5?d7m;?UwdR#a4YM3u^fBgMw z-Vta4$o;EYF)hdhF~&qb>!$~ivxNy~WNX%^!lD+EafF!WA=ISx)n8%ucJlPk(=dbK znDNp2mP@zHOGd)1GeM&(E7^=h;G}`Irx19LCXlLqeUH~^kVtnW$-)GK`qu8>L?@{! zhXsTtHgdu(`%t97o80Sst+-HqlxJ^a3NUUz|9%m@NrN6(5DL9C1!#2<2{2e!tFN)D z!m400*NC z3OjnDrQ41$ zku4jAdY(h=YH?Q9T~KMlzwZ!Tk6XX@GiB;(kgcy_S%vf9>QZB<#m3B{T@3lCUU1K#MJYCtJ$}? zluNjuAA8m}JT`ar&x}3FT@2*rhYB_4_2Kz#{<-&|0(J~hQBlzuJw-*#O=b@(5Uba^ zHkPf+^t&ttCx-;2-K@@9z=b1e`%nW1TPo1Xnd~xr4?(X9fTB zZqWrE$bKIz`BR7;xv^WhHUA)2yId_#9~xMfeT!)>eKnuNmkuIu8}0HwYZ3&Z>~b%H zAeesM^^=>A?+&pfh**#a5D1!Im38kjcb>bf9h{f*z{867@VuM;A5&8$iAk!KMiCqc zBB;n(cfw`M;R1?R0S6jUab_$#vN}wfZN@UA>C4z=?2iAE{TtJ%U1pBAmN{J8ecgyR z!>u)yNGP-*ZVex{ZC|!Mofo4MEB=3-ezi}w=YM^+XJ&!8XeNIFps2Ry`<|XG>B)*^ zJ0L?6Tgl(Z9~I|=JQ!Q{A1g3~D91z0)dFBUXHLIOxphHMl=Yr1x;^WEEo-*l{GGa< z%gaGlTPP&LHF=P(ceXF|_WlbwI6|6>eSugWp)e*y7qUbxC3hFAePbnyORU zmDH(?!$~jepmzX;SR_rQv@*7a54fDp8J1-8v?hM;OU)aU-CpY)$f3>(;}IDnj0mRK z?XCar(F4+D7$bqV@fE^s8O#yF7jI3pimHY+fag+O{C{D5*))w1LI`1m@u&2DY2O3U zDP2gqQ7n_&?|a&5O>5AYZ6)by6gvV2R0N`+TfSHRtbzd00RR9LrU=*|;KBugkPv~p zNCXnm2x2877-t*;nM?$7as=6m5h#@-s8olbK?8y&tq4B&fZ(gI2q_^YV2Fa0fgu&7 z8VqSAlfaN^q#XkSfD1?7s6?8W3cbpL#%kz+1pwco$g6t|umKRp^3a@rB`OZBjAM;=RdIr=lk&NW-Ci1F`)N9h;f&5u8 zg0w1q!SXYZq1;*R8dw+eUtR$qg}LLKBBygShq4FAHvc98ted>Vz#4><0Yk=LQb7mi zjwH^0uO~tKI0*qzfO7?T(db$+(1Bj5E9*|cd4Z1}lx#3M;me+&FhIZtnw;3=OfD2o z5hSG;lF@jQQ#KV+5h>P|(F2e>$V75k?wJIM04c->E|Vtv1BB>Bb@6Yohul-ZUQiGa z5SY*-C7p!Js3cVnR%9ZXWK1V*kI3idchoA7!(67pWa>`7$vjTR=2#iwQU2ljZhHlX(a`j}|*58Q2cj>tr$xfr^6UQgpj_n^Q2 z+umt6nlDY$&umnU3{2W4dee>yx`W&xEfY&izAV{5s9{NAycQ) zeNzvc0!gLzs+H0zYEj?99pB%yYLk+VR%&@YE>>=KKKK z=-WZcKtX384VpLgU_W{T<7N8H$@fQ+$r%3sxt7$CdeT4|NfT*~^1oQ9mq|zujBh83 z^ZZg!kUN86-x}$XhBd1du;isBE2#dblXe^2N;?*Yf(_KlOk zM8Bt}onURs6n^|8bPr@5s{0E!sY0~KiR~y4(bZn8WAYHqmGJMW(SFyd;ct8{Q$N{x1bPWPSXV}BiGu}XJ=T*`uEg%9{@vy`_4yl{<{ zZ)XJzmCrnzR`TUjN%-PlihA|8CVy=v9HU*jaZY9eqAm_4p@cn7b>Eh|q{h{5(diPK zEbVV2EXDH4N~g2Z6>PnyW5ZN{j1}$Y)IM#X&eFax`{d;HRB%921eDkVZxaCV^RoaC zMoeFOo z6ww_=#h>aQe_-}cRhPq&8eLH*+!<_MIUmPwsR75q@ZkHfn&17j5p<>RZ!h7$?a*B< zg#G;+H-OoDeO~pWfpi|-YyFD?7mr4Gh`r!?RQ(z&d%3>{^DFj*^jnF@dqk)A3fNyq zc!)jf_HFtZ-(-GQo1d{eWtX}3>F(J{OJ*e_KE%E--1=RVe#F;fY&F+m^rL(oIjqi3 z^CPzY!ItW_6_K_ZokO>iP7NoQ5k}Z{T*sjA@x}KY(R`0hE6*>kom!=x{%n|4jWEKt z>^2&Gi%-UHfcX|1UDlp!pO$E6tv6=JjyS@a8?LeEGJWBr#OilXkiO(2c39mE%@#sHb9AVTEFagFmZPu6oc~ld(#feBll8jv^K5Xvnatlia|l{ zHdAy8^zFoUiY;x|HQE_Fy{%QqZg21dZ57ZJR(dC(2i6qmtZM^wmo?U5bxxW)=m?KI z#F2^iQPDG=rXw9EH#kM-KHMUXR#A&}!P64c;$|Pxy!nsgR3kHHBJ*uSYcv^&^0ZBp z<@nz#&bcY)bJSpbxrfno@Z;VEi~VY55W_OkYs)@t}y+9n@ED z+$rLj#sPcJ#2DfS_0hONL>fZ+2U=qzglBjkfv(`?jxH?2b}CSSsg;K{1t^rMr^OQWz79Y1qyGbHskwO#zht0>(3C8Dz6r_ zKTwG@NleGJ)*{kL`9Gmia;(!?U`JzPLO{fdeG^1*&VJQH6du;&A^H z9GXM_N&YMtl~>mT9I?1&A4A0>nL=mwM0W6!uXu^F$z{tll z=|DpYULp@GM!^$%4hRmzYftC3I6d##6mN#`g8;q{@~H{(vth|~+CmbB7bs~i-y>0m zlQ2Fx0vof7p&$hqHFU7Gh;ZcE=9v2t?@`3b7V^2Q_-VsGdf$gLI9D5hOIUY z5rr;}cGabBBBcw4@u}I8N-OQ$W!2OQrI2=7Jvb{pf+w1)q;yv(km8mpPyhv<(O`Vi zH3-eoUQ$IwYA`Kml2f0BWF?PT2#to_B(1*(HRsQkM$RBX2T;i~kg(c2ya*KH2)g2rq*C+zO{9(jFKnN(q1r1c_oIw>B0!}gDZ$|qT$r}KD} zl^9;&A732azPbk_{`h6%byjm&cfJ5D;{(4S0INof_#}o)ElUfghIKS<9H*;CNI}z* znsvt*;2R$IP?HBuX@IZt*l<|4WdKVwGzqEdmrq?X9adqf3hKGLSUQSW870}G0)584 zT_#YWrcZJ&Ct#)alj2FjGOMO$D25~y_2A0(2%}LkrFK%G_@Iz^J*gt1ct3-i@z=h2 zk84e8*=0qh>w7I|z2K{PZfqh7M9h^biyVy^;4M7=BoP;#L}Vvltve5&*BcZE#EUt?5H#}@I31ooY zhzHw@CywSdhyi=d$OIKNJzkaNuEiC81@74loF6z#26zegWpMV6n+RaTbDVj0*}wvE zHb_LqG_5%!@83tM>B&Z!bnXyZI2X^A+2gSL3~>_ZV~Xo>!FzNcaR15;uHSfekOl|% z6U_xucicoE8c1qMM2=cID>bxEF*L#hH(kX zXL;2SDV!6eUr}Fs$8CN^1P&cbj*L@HpBYj+wnVKt>Z*NM*oDs@GrWPw9KJ_@enzeH z;VbhL;W@(rL@O-1o8Vs9$yb6kyGLOu}}N<_|kI^7j@ z>n?i){x}zVH0H7V^K)@^iPiH6+4!*w|UA~XmUBXLi?7^SkH z=Y9--E!YxRH5UV#q*%_`rYll>QCQs?%@t(dc-YyIFl7xYMjW1cED>=9a6lqUI?GCz zPFVy&t6Xq{TOWTs>DKb{|_tC6j;lSKhtK*2&LbmoB0OF{0ix(3a8WE39x zeZi4Plt_oHY^IUY78E_*Of-T0+5TW2VMqBKiZsq}CDTMYiyTFIz)I!@=#1ou!&`Vy zrrh-DTL`(@!oHIaEaM{=YG4f)OnM!%ky0_NHFLxSsR&--YLh?<^Q15UI*F_X&ErBR zoscZwVRZ=231~VV31LSjO-V)14CS|Cj_V&sprxsyQEsy{M&HmeWNWY!#-L-8NfOqC z<`~q%i*2_7m?br>^QdjnduJ|Pt70r4A5dM+7SR!;Id5~!5vaPMJ!86HoNCoRVCmgT zwQltX)&?Mg^M*=gaaE+BkiuCRF+hEi%FKABWe!6rvB!)|$|V(LU2BfI;rBO0Ob6dQ zdH)6?M7Vpfx_&DY?nZs`uG;nTEK;4Pj33A50*WLKvUAt4fTvt%FEnLM=f?Wk|MHGT0 zbFPTCNaRjf9h+tg+-Z>*mmQg$krq@Uy2pE*I*6PfLkqk$DkKxi#T_J^98c?HPn!_Y zv-1@90DxZiCjva)iJBfb7{m2hIS1Hi$F>78PU#60!o#WqHX16*>T?4LJgSo&&;qmp zq0KuqvmOq>>>HjU9N2xJa7Dj;yUt#99DNYW@~_!|HAy+Sc)`b*eVZKzA~iv45ez>o zl>nRg=`VPIN|%}XDw{};Z$#aJ;4!ODq@U3O%$Qd&^i;eIRA=ss}I9Wo@AD-~!t{6cA& z3KOjm{|y1VWtKxG?^zJ);xq#-s3p|Z?(B!w;A`@03lKgqvh+p)S_*&FOL4OVQiusB zdhL3-7f+n9Ivve?cnWcrFMHa6V=bt|^|hywql#aQ89}{d zEwj}+C#b7*)2igoA?gx09>$;DrmHl^i^RE_x?D|1?Dq(tvrEKYQ9a+nOo`q}1)2wU zIqMY#s6$-PVRc!Wc`zNKC-vFW8$gY0A7NU>u@+Rv)C*JsrZ;0r-a{uICYzm1(?N6K zoWY!S`bslHUc8=s^ewGeF1Oi(*)pEtv%9fDd+Z=yeR zBn$G6t5R*!0*h#?X4?(z&@*PW6aWnzFOF;fq65o@4EVfbLo^*$&9fde9sHTF)hZ zpTSjFToq|D%y#Co4A3Mo%Vs>6G!wx^*keW}&PPRAedmqM5Y!x*gDudsxND@7W4n1% zs+#R3MM7>QZVny3eL)5A>{IqquoLFWmxbyvTov!cu{Z3E9j)bB`Dje=Jjb3>BaAm- zgI8^`(kcEG)*b7}w`PGBbzC#X7-!y;-^BtpSK?2zQmR9pIwFkeAh}Zihy>?ir>+9k z!m4ay7x81Q;}*!hdZopfuUH{ zGnHU@qBN*mP=R2XlWS&X$y!&kEk&fPXva`TE(21-DSJh^zGHVxc`0SCGtq)9C3U)@ zyBT`F9!LtwIaGK?E~J1u$qF3-DigKSj8_1ofFu*S%V(*_V51kduTjToX0@PlUdyga z6muUDa5Dt4wkKn(Kt)jXkZnAyic})1g7DVpH8v{VQ-v1XDQJoyda^8JL=r8!g^}mD z>|W+@_-`#60T7q&p*h4Dr~t}S&{RkIsX&yd8BaP)0WMz-sFoXvNSC_Kqg2`9AU1i# z2`;hWsp7OEPk}gP9Wg2LW;13y(Xn~TcyNl8A!=w?!5@C|m5aPLSq3tl6^DnbBIQ6n z&lf5Klr8ds8Gjy`ESy{HF(cEtOhs8e4$p5$dn_$*D3d8xAOVvD+L+x904AW6xn1B} zyUb57P@w*-@D!xlG&enQS{TRL)+weXDQ{GWk#?oHW?S$4W14uIfHTnnMCp()4?>f^ zcBA+!Pk*b&;eX*&^{A^TLz18z9RBSz)uoZ6e^3KI+?j=Mc zK_k#;Jj58pHNoRdcNTaN<8BCUsiI0|4Mbb$iF-GGCj8bMEO8qD#_B3HPxS@nGPHB` z2dvht^!Hx?fPf0Nr~-|H2xP2NfRg0WHRG|ujDsNG!y?rF&5{-rbJTM$p=71!u_4^N z*+G4Cs7;BuI6bO#iJ$(#A{1Q_38}t(S}LpcNHZP=CJtOLc>bpU+aB8K3~+x6F7tV*EjS*<(f~+D1iLJ^nteg@wtl zRe^i9nO;_ae8Dw6%{4xt=}b7$Ylq0*`ukLc_sIr%#94!S%I2y_qrq7-6=i_Dgws2$ z4x{k|W#<`NWTK2zl+~|takk>)nE}*-qX6;%+w)}4cz_}cS-TT}918g{5gI_YJ(-?g zOjCUq(=N|rkJ@htEe~#g*y?bfX-BnO<3Q>ii4|^IWgW(?Bqh5@({=tG4bIiKL#pXB zdFn`7poy)3nfRtPl3mH)$4=-vTQ%qXc)+WYNNvFwCYO?lX;~p7S7H1KSiW&73CA`A zb&v)Tdq(t<%gx)x-yj6+ge!mqU|4g4gx-LhgdsQM(YbK~{>By^DEb4rfE%nV*Cyd2 z6g^AkVN$$Y>9-qY;Gjc7asci2h;1BzEzd$!92GDDHe7^B#%iuX51Zg+;I~y~%$ZIq zkUeN>gH?feLXDd7Xk>syFz5*w|!=DN4y^N{~T^bz}w>-=ou*fgHGk z*5caLYRwfC?$}Ry)`dj7QdeK_x8ILso`Z?RYKjv`Hum)L!hvudX(JObnVwV|lQq0( zh>RgEgS8%^2gJldLPntfcz7El;BBDgA>(&U^FQ1(slM0qn!PhkZ&OCR{w%fRBt#|5 z=G|%LpwU0H=?&A#=T%!v2G5`pD!Cvh{XHsrc{Pl_!SzHM1{bVoQzFJc9^Tl|8_g0w zVS`lijGro!F31j$bQY^3=~P4YRzk!!CTZ`HDJ$cN&~QOa*pZQmfQlKje?8dXnYiHx z+zM`Pf!H8A8OISIt@u~>UB)&TO<)PTdTL-z8#O(y?KhCz@ZB2{ORP7?tT&uFZdFrT z)9bc}Q_VF~l#mtiL`oRhz=$d#HCgKrc?Bssvo)Hj+XsIRSTvI?q5;s(9Y!qS!^4Q= z!aFfQ0S*8FZJ+>@1K0o%e+F=tu4*BC=fS`X$DIeV@pl8LfIb26Xn>Rz{qk<*h_)!o z8VP_!K1?I}Spl=wm;{vM#zBQJO$qBm2K3}8*@VI%=S4bBK(PM!n5Mp@%TIe&xaSxlSCIW$9{2qn9%s9s{aXe zfz3;>p5UqQivR*aAiub^PAfyn4mI3{=~%B>?dI zUAfjiup$fD3KtLLVkfnK5U>mYq>}_=Mbf5b@;2kG}wcg7sQsQ&`xZ zI)sQKAr)(^agxYnm_RN|wp^vkD3z;JrCx(ZOU_ za^Yl{Ghd^5^AW^Ng>a$9h!93BQnW;I;w4CzEJdmqISS>;SFFH@B6X_OsMSoNg{zWE zNEP?bOOMb0F(>ZeE`=ZpkW6v>i_@)s=nz!zaZYiMwmz*z?NPG zpLC(UX_POf9YX?07NDhRI#ydtd2EkuV-tC+qU0f@6+12!BFRaFBeGyeOi(=N=aFf4h+v!}*o$ko!>a})GB^@lz~0l=5z6F2 zE?q+OO;S<5E=x@Mo5XT;QjS$X5Zx#j9-wD$s&B>HH%Rfmo*CCZOIt*1ns?(K(bXXy zfi@=~5@#c$x#&*0ZLjsnd%VY-OGfsVwwa3)XmZbQz%3{vsG^UMpR&B=#aov|b(ydt z7D*Z1%k@*|m`;X~HFj&%gdmQTvFV$s(rM!^WbW}+6dFbIoZfDi=)Fy!VyUU zv=4x0Lm?j(gq6)BZb=)!@hIS~%q(2B%Uj%Tg2n!DR4nbA%ab8Fxw@ixC7`8q>*m+rzwIx;OW1=co;M*_@Q_a{0*DZ!!T4&3U8X+ZHi`=@w!{h<@ zl;VhXA~HEqx;8`^?>u!1W@q+j8}f)AvYu6d3`?(j6?W}kZVN(#a%8or z0+!wO>1A%b(?iBNv96?c1uD|SRv!sT5v1|q5SIo#lr7i_Vj*!WcuxgaHiH9(Pwklr zz8KeI2w`{bdqG$}acDofSR;n17q*(6#Sf0O`SUHr4v5_XRmkgq04Pk;x>4;}=!QiI zg~2kJTq*%ok~GXBlbw9f;&e_Q){0|7LWDqzJ(j5?(0AA^y9Trb*xr?=GXmi?@ zy(}|ghp57)iaTLPf7DnP_9BnLiW<^m8uXMrc-OnPhY0R60-b=HuJbLt!h!{F;A~6B z7Aw+Ta(v9lSR3#YbS5tNDfU?q)W)sJI~3SP8{uu1)67VoNV(lqbUiT}x{^Dq&EJF$K*Ivw4)hg#JmS62T{^hw%G5rk zNGWS8*>V}~j}BseU#Hzl^J>AGU7}R|wQSn&d-BJsjNhtP?2(&UjIhxtK)&O#%{l_; z6vGPCpzUjlK&!$vP^~5mOnL27*fG8CkGfFkK5FbViz(>hVIhwdJc3?{YrQ^_Zn2C^ z^D9NFq7h+rt5%evd~O++*_d?RULzjywli8B9$Keffh4(RWc{YAU^lMlLT%~tr8a|N zT{2*n8un<>T9nHJsmBc?+KHPWp(0VmJ}-trT{{{O#JaaZ;p*<34C{95huHThdJ3#F z57QIdzvS8)g9c1f?w#cqk2Ctgq0hNzQr?u&c4;ZD^i<+&qMI4eGe~%GichZ7^wCQ) zTu(G*e+KzVw6Xv1qkT#X?qgVam=9o+rux)#=Udv6;_tcR_6`zBs`w4~}APwksP zzO%FM3Nkg%%W$_}jOgd-JLRKQ*v#D-6Q;aUOV3KV$Vvtz*%U*rk{ODLN33A9Z7{rR z9kfyF*X%kAqZNgz&bPKLgFtYpLy&4UPf>WZP}<(kbBx( zElyXBNneTRs-lH*h$uaXvK4)L4ct+8>>^RXNKMx*%a3(|`!nT;dYCt?bYOTA4tOh4 zLdnD|Ib&RxjvBJBp;BYVuH~ll_oaUqo>w1U?MfThq|+u+k?@4`iD9wMEyFI;csZZQ z+5Dqc(i*=t8WIw^OC!g|p-S~#$TQ*Dri3UIF1V;GX(P;BGHQb#(!sTCTmcD1Byvgu zCowxwo~qDb3&eqEKLd%5^#`jYIqBW!xtge7OUI z`T})?o$X|TOt7w0O!FRB%P3XpG&d=k#&0c{yST_cIB@fmKp%5UPaFCMd)z!eYmYwM zQN3Vc;7F5>x(j5uyT#2PXVKQ)EZ}!*ItD{@c-VX~8T8iv>>vH&pLE?xM$#<-eHb0*(kfFDpJ1Hj`5)&z7&Q_~McN`cbshvb30(;oXmcQEdt^?{ zM%E)v36^T+^n5(6HL1&rz!Hy^w3pWO&SY$glWsDVThnPSBecfM#7nd(rI`gLAWUQZ z!>CDoMQCwI3mJcF?j~fPXQDaT2A`j?~^`hnq8; znJ?+07|w6!XF4d2d!fgar%4a5=MFzu|1jTZ!=1l7^Cn}fJabY$Ss6i!X&)bi4G0ogt$&5D9EaQwELRhSMzpz2v|760tO^32hb~l}zd$^L_~$;v#+85}FK%B+jrq#n?bPaFZa z>RiTvB=@<(oaq^j;+cS#Ti4(whN{MX(%Bf2p^BNWR?OKErYu*Fr)8MAY{-J1rodzy zI_2}^JeAxjpP}=7jIMp;TvdJJHLF#%b#7+kea znYz8;n8}Q|0zB5o5zBJLnhP_rMrhI}3Qkm?Nx4c9OWjz#zRgZOo$*mlckal~CpQ$H zGrh{sT;xkA+2Y27B$G*9W?;Epy2Qn`d=&Ts^>mHy*=FX0O!&Vw{%nm~*l)|UW&)j} z?LSFhAbwvjJ`+0V@QX;9J~yYF6{d|hWxGqP8y^eZvwTjq1f-E@2ZFPFMpy)4dYy)` zea>EoGHv=Ki56r#o%Nz(Ek!GYj#=r-XildktTQagir|zdq@K-HphioLP#X=ZdH~QU zOLr>*BYkrDnOECs@%Wiqz0>R4UtSmvk4}?9`+&qmtFL^G;up(uY^pF4$*-S6chHzB zrOOz1k-NmGeMnBBclx556)?o+x6}9Ye?Qtx!BcNvsuIh6TVKpY1|E4QM9H^yRWOL% zd`xx^-zy9dueXtLXk+eCHXipIqpnw513UO*BY3j5c3$}{vzs#f#}C+7KNYq6%p=J9 zmY4YvDZ5Vb&cq@70LarzU<~$)g2TeNbxM+4!o_L>Q79 z;dDCsJdWtp@m-UB6RvP$`vh{q7De5@r5ev9(38+{RwpBtKZb|_2w1P5dD>7fE4C4+$8LE*HL`1F#;S7Qi1_Oa3PJ|d?Wg!fv2&aF2 z1B2HBr0iWOhDv!}Sm#>V?aR!=OV!C|%llUkpR+cqw6!Fk)Yh3M);do;;jj~YT#oyC zz$#0a;{0P-`mf69@J`x!M>JmaMnW=({Ct2e|4V4H-h3S#iO7M~^oxCT*h9mN_DP_; zYXzS`5Tn_z6TFKS;BZOIxRd>S0FLx=mX*5I#R=>TYk0 z2HnHVM-CdehG#gR9^{o`ql;pm%oq-`iZp6C)40t6(_14mS}9heR+mQSXcpwdvwrKC zk{C;&7Fmr#vDLh4BhNcj6JC1BV8LKpco<40`kkFa#8E z?wY0Y7T3ou;`%z_$utAF7n3IFX= zulToc?T_r?r;>W5Izr&;8haa?R}}TEFi8*3}@YZA}0qY9O(8XO~35zLe3S|ywp0wZ;^rFczuGYB$j%xKVK8hNo4zfqrUV|(Hbqoy({1Ft(SCqJPte+=AM zr4=2usZ6R-Z!sAS7Go-tTG!kTUYr=pnfvl28m}E4A|wPrG+1|WYE#FL?+Y1+@cphI>o3+0 z&YU{$;rl|TQ}BMzBcL|?c_3%Gj!6o6A_B+nJNdTgGydxB`w9zbOT7UXGU=Ks_#8kg+f)kvFJhyK;I2F)#QHFt)M+pa z(uZIcAZ0}s%2Z*zk5Gl`WJIL9VN8WQAJb|L76Ms|SV)5w)A5-x6%7sGrZPdSl~ekP zk7p$p$HUtq^>LrsYe&-46Q=Yq!%QqS9rfA-ZY@d?NV}HC+Zl*i5`{+FK$6P3uo~qK zeyh;aee4r6+nsQx##+~OVU-!iO5Gh7luI#6*VL%9crcd-FUrI2O2S4NXMZ4JMdT}# zVYClZhJfLL2X+XnEmz^V*`)kyu{_z_2nz@Sj#&^BZo#=@@$>qAx<7(@9ky-sw(JSE zdAI4?n?cWE`k=T$L?zT4O$<%4kb&B^@U(DkJV1IZ#)*c?O1*;#c=PkGAQ(f49>av_ zp#K^T#M1I{H)ZkFHUztvCIf=0wU`A9g8=>Y{5^EycgJYpKkEAcF%za^50bH@+Lq1+ z5E*9HHgCP7I63yjsMyxOR(wKBsZ~&5kv>00r=fY}IR|M#2t7pVk0K4BNJb)`i3>^u zVx`uNZTIcS`=gEZJ^mKk4*oqGn}NKYAX~_gpu8ZCG$l!qd77A88Hn&QrM?P}V07Rf z1CC=WG07P81OQk&C_dg&b)w}!nkH-07g}rc7zwkfk95*#Zg&#tGZAKRloK_!-2U(Q zu>n$4xEin~&ngs=?tr83w_>3&JJX{JY&i9?|1Aj3&Z%}*_Q}`K zO-fFj164Eat1{356U1R)o;T^NkLorP7p7*|DOYbck=z!9@v*Ts*h%%s5N zyh#c2YP?9S(&zIZ4aJRzx@EDHBLu@jPsF~3sT_3`DDDgR@9naW59-1Mo2#b04pg%d zMRiz?VG*&CHp<2BGKryZe=(^-5hYI0TBC*1>&>{Ss7$OyEGqr$w+6@cY|vyab6Uua z^u~?M#$S?uAyH!=4aBUxx{lf~6Gl+8MvIw|`MlJ2cYv2i1l6-U`?E^1tmQL0tAf)8 zb1MW;y+}|udGdULaZ$Mu0`vcfXcxJv1ois4hPBz*Go7ZnK>n3g|G4tX-OX1t&R5Fg zyJN@4pR+8Vezf_zP5W%pjHBS-Vfg(urg*xq<~)j?hckV(6NeyY6tj!y6^w4Sg$C0u zYM5?wF)ve!5hpWnP(&|azB)U;xT&*7iK;{OO0c2Fu{392o#=w-)gk##;L4g%SH*J69isu^ z2#Ht`d6*Qe`%<2`s*Nrj^FHgC)S*IypsuanTHDzHidfPzy)H&+c(IY=!s!1yf}#v))1uZOEz;JxpvWv% zQz$rtpEm^8)M&Blnx^V4^v!ItZsLR-VjdS8mSGFTv&%g-R3z4ffzOszY?~EX!WM4e z^EU{oF?f<2GrL_lztHMyDN=@=W}>}fAw!)rnl&%#te5h&z;d-G%imEkxlX+ z1Ip=C4XKk#ExK9D%3NkQmf*UR^9_OEhO^T(Y}x2h&;LLTBeQwU4oOH%hQ}CE$b}EC)ILCfx|8e8{SB2Vs?f}|9m~_r+o^>sj{A^{I z=?ors#5hwk!QT&0O<#<`lT8mYl`G?LP;NRG;bh6U;MR>kIp&}Y1_y=r1bV1Or8Yw7 zq>NwH6*CT2aE89!)<(G|jOj`${EGQ8rqqjYgWf@G!UxqTosz%$Mv+PtN$~AJ5Go}> zf`J^XOy&McDgIe#|5q-Rk1(o<-XyO_{7aCMe<=;GfG z?0vbvJ!@H-xB15MfjZlk#qlb3U^FLH(c>9#%X`A~DC%ccViQBj@*ekqN6{1VkEUg_ z7sYK`>IT+qzJUi-2ES0^;IlS6hVeGohKKX5F0m)o@ zN#?;I1crL&h!g0=MpXj?l-?v6&T5kbdYhZ4deiyp{{#P*-*|rWUGhnXmSVoW)>B@@ zlbr;z+u)Edz4rW8F}d7%j{y!Rhsyoq@5*bLaf7A@9VBYLd_`^FxaDxqgVJtj-N%0Gh8&vBwNXQ(Jbx9rGA6wUvVZ3H(Gk+YH+C zLXqR}cwqUdL- zl%IQ5mNJ%Kl%3fs?p-35n)yPk$QUr@1adg7`rdiK(8?dZ8+L-J z;4C{BYR_%Ao~HLfTPjUPdBQ}9!|nLekMAIUuJa&*;%AD83e{TCgu3>uWWQC+{vV2c zoq5_~<7qpB8`OO1J{`|`D1-`JoqtA&gi1!wq@9#-fKI+H&A_<8BHKw^An7u?mj zHANQDaey(#bhsOh$qBtW%K?fwHI3lwW*@lYmB+n=O+I5$d4z`b;B`e zxPOjVO?fO7?f+M){KN-N96FzCN|z z9SZ1V&usR;PSt)=`EzEk-TpxakNdveK0kAqg`$=wJedqM68cDs(r#~2>FY>SNyw*J z6=e>}ajTuc%N!QUV?~W`I$Wv4dr+-?-uK+FERKwuJ_>C*ct}U%Ti-pY5}o|z20Hz% zA>d`xL3r-5^+5gEDr-S{yq;$@XV-zU1E86h1z7n-BvOzCZxK|hQwEa0geT#Nf5{Gp z-MrlD=IDBj#H~I5@WPSr3BlXq_uD_PKk6NWhBIhoZa1+JyEtYaeIv z=PQjqtlkMbpd8fq@gR;y%RBt~1jdXMI~OI%wH74+PQ6p>1n=}BMuh*tvdM$%MQwR` zsEwE17WeyqrG0x{BQ5PkMMXTsLq}6muG93xe~cQH{4&)KP{DSNP{`RX5FC*R zg_0w{sCPjqsktm6AeZegfAjON-7XZYo9ati7`lK?s#gFnBOQ17>y$6tCZU^JDv}or zYGrwSNq6!`4fd4d8g9NQbrOWzRYj2(_eMD;vaik;5-QP_w=w~ty@*B9W0uQPWR*3RWGdD@reEnTvFIsX8? ztXLf_C=xoCI|m1~!N${Oyp#d|dUycte<|At{McBK7=v|i92vI+NU77eah}K9$0>k~ zN<*xaUZA6X_?L!*o$lIRKeX%g;yO~`H zV8YR1dDN)I5LAa5qfE3W))J9oQ%Fx@!ldF!#s-3?KNRf-m{FASD(GKpDKC=)!{d3O(7R_ zH&qAVr_87DQ(&`R9TaNKJ(2f+658p`7vz^@RHd3ze3^Qk-5X8fta(@EXetl$JgpuU zme;zVfT!pc=9COgNTnuv(@c7s$Cz{ha1xL0_W%8gvN%&Wk$!M(9-luTv`_s0+Uz{x zE;f+FU0nvW#nxv-Te=J`cjqhWJS~~AvP2fp_332(N~zJvFc3!$C=Sf8H6zOTN-?xr zaxB9+Ad>?)^V!M-99+4r8Rc=8(iOhWM67$avg*^9RffCNkG#Ad`TH+9ug?6%%HL-G zpsQ0Il_z=^t3Palxee}5xZGxUPD5Ff=2v42<@l6)BHKnq&Q#H03MXHb_AA`JjG{d_ z&S}!@9RSYEEH6wGOVSEOZ@3BHz!qzvs@0Gjt(vY5Rh>hbd6iPhfzF(k z$thm7y0QA~Tlu&|rJvQ;d8fd&j){#O^i`7>`tCQ`W;=*!cb{NaVbvRI19rSM!biO8X7HK)F@(GQoDc zsIcOtBRQ$|nUf(7ptAs#Fe2S01Mc`>@nOdMU!NK{!*5<3%mJ`Eho^;X0|2)o%Z1cl zF>HS9Rm#LruKnrdurIP)R_<=+J3I4Yi|e5P@w^NHj4q-@@7ZVFx01wnZ-KWOx9VWr zIt_1w-$#tFW1}DVm5B=m1CZS0LuTC2Z9}hbOxTs}73a-s4fFW(vCs39YMzgMy(?Hd z60QA$=@?V;DBd`|aW0Q7?iVe2ThRN5A2xxddc!$cPm^n2wsq=^d!4-YHIkUA}CiXKDD2E0+q}jE{7K&)8Q19 zKZF$)`X;00(=WBZ+8t4{%e$I;=_cNtFq|&vlT^AjN_R43<@eNmwbl7yQheX|B^5^A_O_tLqgxcZi0_#~ zFiNiMn-=Ho{H4g>727ZMksoVUN0qXsE2BPV)$a<}n2=`?dVbb!o9r03yC)-!3+rmV zWa%3DWFr#$q6f7}$q(|n+S@@-`?`1wh{?x<#O@#NEA*rs0-NmHY5R-O_}KXxCT&oWLo>lG}bB+CqZ<;u1kbVitahc*!S{*36s= z-dH2qU`}*Cn692rT@bmgNNtx^lmxYQdyzu$IQ@s+>|KotP(t`?7dRz$Nx8h7w1*}5 zENzsqaXrZy{;_*a!c%L%F=A1PrBI+#|G)-*+navDR;ykV5OI3h=^_3qsjHYk%OHU; z(eCtjrd-W`1;V>yAbh`@o8biixs!3}kDF89sgd}u3HXw;|7w58b=fg*3<{5p98Y860EHg z+=T$3?qi;RJbgVHQ>x~wxz9`E^TSlW%2)X+U-eSGR4>)b4+MogQsywmlp1PD zO!3k$=UfFSHdbaAQ$j6qiu5%8ky3rc$~;O+Wk}P;(sISSQl<*h{OnxRTs2qC4Rh}S zRrpn5YL!~0R{2fRmL`9#7P@^MO*t3~dK6{>lMEZ8wYpl%H3g-K}Pz~bqdDqs9q zw{+IKs{5*9!IO?`G7sJ-Rt~L9o@@_OvGPazYaN*K)p&L0t5+_!%pf4~0Ikvo&!fYD zATKZOG@yB2Gqg0D@x@cbc;wkD)G8h5Llc{xtajhfzd%5-!C+23@_>3oJ!~;Z&`6DV z$g9qg|AxBzy!>%gU@{lQ<8UQoxdK#1N-C!cs-zlfrHM3&Ceu`!CZ_+@gl})(=5D>D zv#l$>>I(pt1-l#0{E=en0vUh>=qy?Xs}0l$-vD?66)^o~uY-2WpB#$8PSd>*$|sZm z(Q*)wTa}&5oCAt98A_9oOQKqb?xmaqP(O;;%Mg2U5K3xwkR~a!9y|`lfx6iVH6o=_ zl!Rtg28OaBb}&qlLE2`W6817wEgV`2n47VO=B|Ali%ciG7E46xGon?(?l9R=4`u)A#dip zlH=5C_-Y$|!|f6twUGkCY_Iljm2_vjc92@Nv*2CAlXV)L1e;*b>z`>s!$0N_pnlit3(XO zG;&Qbwrr^F$0rh}J_1umN1iqZ{WUxD+7N>V8_|M4-q%<8WJ^oXU7fknI!0{xse0|^ z^doCnnYxT7Py3+$7Ls}0Z_r>PTJXo}zEWjN{~03NQ%mxMSTn=O=Td*x?6qk!&=38@ zW|&t1Jbi3igX^Dn-hT^nN;AV=4QPS_00{7Vm^G(8@=+2bb>Bv2x>m04$6Lb=IbkNU z!M#v1@;FnvpAzg5iUuSyH5pte$b7>hIT(ftwm`Fyf}Ar#m=6Rl7H57;KLAIm4FF?H zP`X7R0v?EiR;vuZF%Patfp*8xCd*|FSUwtU2nTU0gs?GXBfw@ALTtoZ{z%F^jZ6&j zX9jy<p{Qi7+l}4ZVyHxo` zKYkY5q$;~@WMuTVsy3A&AxfN(--aXA0)&bC(eOf18>!lnE$&9cSFMC_*t(NT_8Emu z;*uQsDu47;sg|R_X1HQ)afIv$UCedyy8XxEFV1@?`qAJ%3`9_->r(0>rcEjozK3sR(0lsY)g)q8vxT>bQ zB`Gq}wr^2n|Fh92R>|R(==(Ihuv)a&X@B$WP{TFGvA*QT#O6=wGJTyYq1&l4Y`doV z>%^{5#h^NlVG6)hjpyjcAX?BDcJn=7@+E_*=*J*hyuL3XO%m3Nv2S;r1iH)nb%(wL zf&hG-NT82{%+SD!mmvDwD%=~&09*jcYE&suEl#5Xm6VEPD50cKrcvYWRckc} z=Be2>_-uV=Ydbw=MH;TJBvPZELYvhwpw7v4z?P79+LT-LX%4E%cUz( zPs%wSZz63+cN>Ja@cQ%c?hUXZ{nLsRl00o~KB0}$E43!7uS}Aw^P)bUR0kzn$hl9Z z-Yk?WQ4~55p{c_8+7(25yHtBXLFw&lQux6YHETc51PApjmtB9o8G*WFc8&D?A0(Xs zM^~e`qR?JN_F2SJG-&48Q&%s%%uxW7zvL}IM9X;a^(vnW)K&L92V)KOZxnz-w`|Qa@rXh z;Jyx0X=5ESg-W9{m@GDj%i{}#BC$j&lPi?2Dz!%I=I-IC(;K{uCU3LFYO_0heE9|d zAsC5qF}zt$(ej#YMOb>h^vaDVSGH9OY!Q(X+o$D#)ko&&(y80bC`Nf+g<-KdTpnK_ z6p1BLnOvb%sWn=i-e5GDS1Y^6(RVplJo^l<&%f^3e-DVm6Nn@-g-W9{m@GDj%i{}# zBC$j&lPi=ewMMIp1)|LN$ghSB`z^h}Xfi_cNTC1v8a(EH~s#6 zI$KYpR;N2OM_QNTu+!N!R`Sj60?rG^^j}?k|EJy#AoJ$9)fd-%xSxe6fhRpoMS-pD ze?^C#qKEaE({0OKb8LGz!9QZS$hU02WO3SbCB?Mzf9$0jMXMP)9 z$AA1|-#_nk+WoKRGqbD~2cB-%GYtLu>C@?2_S5Nt?*Gc$E1}gsR_)0wyksi{oak+` z%gBYziERf`yOnsS@I9tsPSuusOraPe`Pn^q19ph`!98WpM?DttH z1Z^{7MDZkELjBAPox^%bk^ zN{Z)^fhc~&T$uZZS6|Je@>|s}(Wxk*s^(ze6)ECET#k@*KqI~35=%hoa<2(BEz(ou zAXZW_&=XbIAk>zU(JTZ}R*Lu16{U454zpCWHcut!h=otQQM3}xY3!I8gSf}?31-2H z(PPv=2^$(pmBIc6NYkqHW~lg0oq*j-@g8pc{~&1w@f_#O<<oFXct>f0{>5#OdL4PYZ*@aP6s|!d-ZQ{nHwBig?}hv?M0g T=ND-DucNQ`tADQ-AuI&|D5jKI literal 0 HcmV?d00001 diff --git a/web/public/fonts/jetbrains-mono-latin-500-normal.woff2 b/web/public/fonts/jetbrains-mono-latin-500-normal.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..be878e68fa71bc1f08894c0882d07adbe485ef99 GIT binary patch literal 21832 zcmV)CK*GOwPew8T0RR91098l;5C8xG0O8O80954w0RR9100000000000000000000 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 ya*KwSMMnsRWQd}xG5}9b Date: Tue, 25 Aug 2026 13:49:24 +0800 Subject: [PATCH 03/15] feat(api): return skill labels from the skill listing endpoints on request (#730) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(api): return skill labels from the skill listing endpoints on request Skill labels were reachable only one skill at a time, through /api/{v1,web}/skills/{namespace}/{slug}/labels, so a client rendering a list had to issue a follow-up request per row. Add includeLabels=true to GET /api/v1/skills and GET /api/web/skills. The labels array is populated only when the parameter is set and left out of the payload otherwise, so existing responses are byte-identical. Labels for the whole page are resolved by SkillLabelProjectionService in three queries — assignments, definitions, translations — rather than three per skill. Closes #710 Signed-off-by: FenjuFu * fix(api): use include parameter for skill labels Signed-off-by: XiaoSeS <87064762+XiaoSeS@users.noreply.github.com> * test(api): reject unsupported include before search Signed-off-by: XiaoSeS <87064762+XiaoSeS@users.noreply.github.com> --------- Signed-off-by: FenjuFu Signed-off-by: XiaoSeS <87064762+XiaoSeS@users.noreply.github.com> Co-authored-by: XiaoSeS <87064762+XiaoSeS@users.noreply.github.com> --- .../compat/ClawHubCompatAppService.java | 31 +++++- .../compat/ClawHubCompatController.java | 8 +- .../compat/dto/ClawHubSkillListResponse.java | 23 ++++- .../portal/SkillSearchController.java | 25 ++++- .../controller/support/IncludeOptions.java | 42 +++++++++ .../skillhub/dto/SkillSummaryResponse.java | 47 +++++++++- .../service/SkillLabelProjectionService.java | 94 +++++++++++++++++++ .../src/main/resources/messages.properties | 1 + .../src/main/resources/messages_zh.properties | 1 + .../compat/ClawHubCompatAppServiceTest.java | 40 +++++++- .../compat/ClawHubCompatControllerTest.java | 63 +++++++++++++ .../controller/SkillSearchControllerTest.java | 62 ++++++++++++ .../support/IncludeOptionsTest.java | 28 ++++++ .../SkillLabelProjectionServiceTest.java | 81 ++++++++++++++++ .../domain/label/SkillLabelService.java | 7 ++ web/src/api/generated/schema.d.ts | 6 ++ 16 files changed, 548 insertions(+), 11 deletions(-) create mode 100644 server/skillhub-app/src/main/java/com/iflytek/skillhub/controller/support/IncludeOptions.java create mode 100644 server/skillhub-app/src/main/java/com/iflytek/skillhub/service/SkillLabelProjectionService.java create mode 100644 server/skillhub-app/src/test/java/com/iflytek/skillhub/controller/support/IncludeOptionsTest.java create mode 100644 server/skillhub-app/src/test/java/com/iflytek/skillhub/service/SkillLabelProjectionServiceTest.java diff --git a/server/skillhub-app/src/main/java/com/iflytek/skillhub/compat/ClawHubCompatAppService.java b/server/skillhub-app/src/main/java/com/iflytek/skillhub/compat/ClawHubCompatAppService.java index b9cfb13d..801b2ed2 100644 --- a/server/skillhub-app/src/main/java/com/iflytek/skillhub/compat/ClawHubCompatAppService.java +++ b/server/skillhub-app/src/main/java/com/iflytek/skillhub/compat/ClawHubCompatAppService.java @@ -20,8 +20,10 @@ import com.iflytek.skillhub.domain.skill.SkillVisibility; import com.iflytek.skillhub.domain.skill.service.SkillPublishService; import com.iflytek.skillhub.domain.skill.service.SkillQueryService; import com.iflytek.skillhub.domain.social.SkillStarService; +import com.iflytek.skillhub.dto.SkillLabelDto; import com.iflytek.skillhub.dto.SkillSummaryResponse; import com.iflytek.skillhub.observability.RequestIdAccessor; +import com.iflytek.skillhub.service.SkillLabelProjectionService; import com.iflytek.skillhub.service.SkillSearchAppService; import java.io.IOException; import java.util.HashMap; @@ -51,6 +53,7 @@ public class ClawHubCompatAppService { private final CompatSkillLookupService compatSkillLookupService; private final SkillStarService skillStarService; private final RequestIdAccessor requestIdAccessor; + private final SkillLabelProjectionService skillLabelProjectionService; public ClawHubCompatAppService(CanonicalSlugMapper mapper, SkillSearchAppService skillSearchAppService, @@ -61,7 +64,8 @@ public class ClawHubCompatAppService { AuditLogService auditLogService, CompatSkillLookupService compatSkillLookupService, SkillStarService skillStarService, - RequestIdAccessor requestIdAccessor) { + RequestIdAccessor requestIdAccessor, + SkillLabelProjectionService skillLabelProjectionService) { this.mapper = mapper; this.skillSearchAppService = skillSearchAppService; this.skillQueryService = skillQueryService; @@ -72,6 +76,7 @@ public class ClawHubCompatAppService { this.compatSkillLookupService = compatSkillLookupService; this.skillStarService = skillStarService; this.requestIdAccessor = requestIdAccessor; + this.skillLabelProjectionService = skillLabelProjectionService; } public ClawHubSearchResponse search(String q, @@ -195,6 +200,15 @@ public class ClawHubCompatAppService { String sort, String userId, Map userNsRoles) { + return listSkills(page, limit, sort, false, userId, userNsRoles); + } + + public ClawHubSkillListResponse listSkills(int page, + int limit, + String sort, + boolean includeLabels, + String userId, + Map userNsRoles) { String sortBy = sort != null ? sort : "newest"; SkillSearchAppService.SearchResponse response = skillSearchAppService.search( "", @@ -206,8 +220,15 @@ public class ClawHubCompatAppService { userNsRoles ); + Map> labelsBySkillId = includeLabels + ? skillLabelProjectionService.labelsBySkillIds( + response.items().stream().map(SkillSummaryResponse::id).toList()) + : Map.of(); + List items = response.items().stream() - .map(this::toSkillListItem) + .map(item -> toSkillListItem( + item, + includeLabels ? labelsBySkillId.getOrDefault(item.id(), List.of()) : null)) .toList(); String nextCursor = null; @@ -383,7 +404,8 @@ public class ClawHubCompatAppService { return new ClawHubResolveResponse(matchVersion, latestVersion); } - private ClawHubSkillListResponse.SkillListItem toSkillListItem(SkillSummaryResponse item) { + private ClawHubSkillListResponse.SkillListItem toSkillListItem(SkillSummaryResponse item, + List labels) { long createdAt = 0; long updatedAt = item.updatedAt() != null ? item.updatedAt().toEpochMilli() : 0; @@ -413,7 +435,8 @@ public class ClawHubCompatAppService { stats, createdAt, updatedAt, - latestVersion + latestVersion, + labels ); } diff --git a/server/skillhub-app/src/main/java/com/iflytek/skillhub/compat/ClawHubCompatController.java b/server/skillhub-app/src/main/java/com/iflytek/skillhub/compat/ClawHubCompatController.java index 2d499c31..50013251 100644 --- a/server/skillhub-app/src/main/java/com/iflytek/skillhub/compat/ClawHubCompatController.java +++ b/server/skillhub-app/src/main/java/com/iflytek/skillhub/compat/ClawHubCompatController.java @@ -10,10 +10,13 @@ import com.iflytek.skillhub.compat.dto.ClawHubSkillResponse; import com.iflytek.skillhub.compat.dto.ClawHubStarResponse; import com.iflytek.skillhub.compat.dto.ClawHubUnstarResponse; import com.iflytek.skillhub.compat.dto.ClawHubWhoamiResponse; +import com.iflytek.skillhub.controller.support.IncludeOptions; import com.iflytek.skillhub.domain.namespace.NamespaceRole; import com.iflytek.skillhub.ratelimit.RateLimit; +import io.swagger.v3.oas.annotations.Parameter; import jakarta.servlet.http.HttpServletRequest; import java.io.IOException; +import java.util.List; import java.util.Map; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; @@ -93,9 +96,12 @@ public class ClawHubCompatController { public ClawHubSkillListResponse listSkills(@RequestParam(defaultValue = "0") int page, @RequestParam(defaultValue = "25") int limit, @RequestParam(required = false) String sort, + @Parameter(description = "Optional response expansions. Supported value: labels") + @RequestParam(name = "include", required = false) List include, @RequestAttribute(value = "userId", required = false) String userId, @RequestAttribute(value = "userNsRoles", required = false) Map userNsRoles) { - return clawHubCompatAppService.listSkills(page, limit, sort, userId, userNsRoles); + return clawHubCompatAppService.listSkills( + page, limit, sort, IncludeOptions.includesLabels(include), userId, userNsRoles); } @RateLimit(category = "skills", authenticated = 60, anonymous = 20) diff --git a/server/skillhub-app/src/main/java/com/iflytek/skillhub/compat/dto/ClawHubSkillListResponse.java b/server/skillhub-app/src/main/java/com/iflytek/skillhub/compat/dto/ClawHubSkillListResponse.java index d1f7f376..f2bdea7e 100644 --- a/server/skillhub-app/src/main/java/com/iflytek/skillhub/compat/dto/ClawHubSkillListResponse.java +++ b/server/skillhub-app/src/main/java/com/iflytek/skillhub/compat/dto/ClawHubSkillListResponse.java @@ -1,5 +1,7 @@ package com.iflytek.skillhub.compat.dto; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.iflytek.skillhub.dto.SkillLabelDto; import java.util.List; public record ClawHubSkillListResponse( @@ -14,8 +16,27 @@ public record ClawHubSkillListResponse( Object stats, long createdAt, long updatedAt, - LatestVersion latestVersion + LatestVersion latestVersion, + /** + * Labels attached to the skill, present only when the caller passes + * {@code include=labels}. Omitted otherwise, so the legacy payload is unchanged. + */ + @JsonInclude(JsonInclude.Include.NON_NULL) + List labels ) { + + public SkillListItem( + String slug, + String displayName, + String summary, + Object tags, + Object stats, + long createdAt, + long updatedAt, + LatestVersion latestVersion) { + this(slug, displayName, summary, tags, stats, createdAt, updatedAt, latestVersion, null); + } + public record LatestVersion( String version, long createdAt, diff --git a/server/skillhub-app/src/main/java/com/iflytek/skillhub/controller/portal/SkillSearchController.java b/server/skillhub-app/src/main/java/com/iflytek/skillhub/controller/portal/SkillSearchController.java index 36227b31..403560aa 100644 --- a/server/skillhub-app/src/main/java/com/iflytek/skillhub/controller/portal/SkillSearchController.java +++ b/server/skillhub-app/src/main/java/com/iflytek/skillhub/controller/portal/SkillSearchController.java @@ -1,16 +1,20 @@ package com.iflytek.skillhub.controller.portal; import com.iflytek.skillhub.controller.BaseApiController; +import com.iflytek.skillhub.controller.support.IncludeOptions; import com.iflytek.skillhub.domain.namespace.NamespaceRole; import com.iflytek.skillhub.dto.ApiResponse; import com.iflytek.skillhub.dto.ApiResponseFactory; +import com.iflytek.skillhub.dto.SkillLabelDto; +import com.iflytek.skillhub.dto.SkillSummaryResponse; import com.iflytek.skillhub.ratelimit.RateLimit; +import com.iflytek.skillhub.service.SkillLabelProjectionService; import com.iflytek.skillhub.service.SkillSearchAppService; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.media.Schema; -import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.*; +import java.util.List; import java.util.Map; import java.util.regex.Pattern; @@ -27,11 +31,14 @@ public class SkillSearchController extends BaseApiController { private static final int DEFAULT_SIZE = 20; private final SkillSearchAppService skillSearchAppService; + private final SkillLabelProjectionService skillLabelProjectionService; public SkillSearchController(SkillSearchAppService skillSearchAppService, + SkillLabelProjectionService skillLabelProjectionService, ApiResponseFactory responseFactory) { super(responseFactory); this.skillSearchAppService = skillSearchAppService; + this.skillLabelProjectionService = skillLabelProjectionService; } @GetMapping @@ -40,6 +47,8 @@ public class SkillSearchController extends BaseApiController { @RequestParam(required = false) String q, @RequestParam(required = false) String namespace, @RequestParam(name = "label", required = false) java.util.List labels, + @Parameter(description = "Optional response expansions. Supported value: labels") + @RequestParam(name = "include", required = false) List include, @Parameter(schema = @Schema(defaultValue = DEFAULT_SORT)) @RequestParam(required = false) String sort, @Parameter(schema = @Schema(type = "integer", defaultValue = "0", minimum = "0")) @@ -49,6 +58,7 @@ public class SkillSearchController extends BaseApiController { @RequestAttribute(value = "userId", required = false) String userId, @RequestAttribute(value = "userNsRoles", required = false) Map userNsRoles) { + boolean includeLabels = IncludeOptions.includesLabels(include); SkillSearchAppService.SearchResponse response = skillSearchAppService.search( q, namespace, @@ -60,7 +70,18 @@ public class SkillSearchController extends BaseApiController { userNsRoles ); - return ok("response.success.read", response); + return ok("response.success.read", includeLabels ? withLabels(response) : response); + } + + private SkillSearchAppService.SearchResponse withLabels(SkillSearchAppService.SearchResponse response) { + Map> labelsBySkillId = skillLabelProjectionService.labelsBySkillIds( + response.items().stream().map(SkillSummaryResponse::id).toList()); + + List items = response.items().stream() + .map(item -> item.withLabels(labelsBySkillId.getOrDefault(item.id(), List.of()))) + .toList(); + + return new SkillSearchAppService.SearchResponse(items, response.total(), response.page(), response.size()); } private String normalizeSort(String sort) { diff --git a/server/skillhub-app/src/main/java/com/iflytek/skillhub/controller/support/IncludeOptions.java b/server/skillhub-app/src/main/java/com/iflytek/skillhub/controller/support/IncludeOptions.java new file mode 100644 index 00000000..72af5407 --- /dev/null +++ b/server/skillhub-app/src/main/java/com/iflytek/skillhub/controller/support/IncludeOptions.java @@ -0,0 +1,42 @@ +package com.iflytek.skillhub.controller.support; + +import com.iflytek.skillhub.domain.shared.exception.DomainBadRequestException; +import java.util.List; +import java.util.Locale; +import java.util.Set; + +/** + * Parses optional response expansions from {@code include=...} query parameters. + */ +public final class IncludeOptions { + + private static final String LABELS = "labels"; + private static final Set SUPPORTED = Set.of(LABELS); + + private IncludeOptions() { + } + + public static boolean includesLabels(List include) { + if (include == null || include.isEmpty()) { + return false; + } + + boolean requested = false; + for (String rawValue : include) { + if (rawValue == null || rawValue.isBlank()) { + continue; + } + for (String rawOption : rawValue.split(",")) { + String option = rawOption.trim().toLowerCase(Locale.ROOT); + if (option.isBlank()) { + continue; + } + if (!SUPPORTED.contains(option)) { + throw new DomainBadRequestException("error.request.include.unsupported", option); + } + requested = true; + } + } + return requested; + } +} diff --git a/server/skillhub-app/src/main/java/com/iflytek/skillhub/dto/SkillSummaryResponse.java b/server/skillhub-app/src/main/java/com/iflytek/skillhub/dto/SkillSummaryResponse.java index 8011a12c..8717ba62 100644 --- a/server/skillhub-app/src/main/java/com/iflytek/skillhub/dto/SkillSummaryResponse.java +++ b/server/skillhub-app/src/main/java/com/iflytek/skillhub/dto/SkillSummaryResponse.java @@ -1,7 +1,9 @@ package com.iflytek.skillhub.dto; +import com.fasterxml.jackson.annotation.JsonInclude; import java.math.BigDecimal; import java.time.Instant; +import java.util.List; public record SkillSummaryResponse( Long id, @@ -21,5 +23,46 @@ public record SkillSummaryResponse( SkillLifecycleVersionResponse publishedVersion, SkillLifecycleVersionResponse ownerPreviewVersion, String resolutionMode, - ComplianceSnapshotResponse complianceSnapshot -) {} + ComplianceSnapshotResponse complianceSnapshot, + /** + * Labels attached to the skill, present only when the caller asked for them. + * Left out of the payload otherwise, so responses are unchanged for callers + * that do not opt in. + */ + @JsonInclude(JsonInclude.Include.NON_NULL) + List labels +) { + + /** + * Summary without label projection. + */ + public SkillSummaryResponse( + Long id, + String slug, + String displayName, + String summary, + String visibility, + String status, + Long downloadCount, + Integer starCount, + BigDecimal ratingAvg, + Integer ratingCount, + String namespace, + Instant updatedAt, + boolean canSubmitPromotion, + SkillLifecycleVersionResponse headlineVersion, + SkillLifecycleVersionResponse publishedVersion, + SkillLifecycleVersionResponse ownerPreviewVersion, + String resolutionMode, + ComplianceSnapshotResponse complianceSnapshot) { + this(id, slug, displayName, summary, visibility, status, downloadCount, starCount, ratingAvg, + ratingCount, namespace, updatedAt, canSubmitPromotion, headlineVersion, publishedVersion, + ownerPreviewVersion, resolutionMode, complianceSnapshot, null); + } + + public SkillSummaryResponse withLabels(List labels) { + return new SkillSummaryResponse(id, slug, displayName, summary, visibility, status, downloadCount, + starCount, ratingAvg, ratingCount, namespace, updatedAt, canSubmitPromotion, headlineVersion, + publishedVersion, ownerPreviewVersion, resolutionMode, complianceSnapshot, labels); + } +} diff --git a/server/skillhub-app/src/main/java/com/iflytek/skillhub/service/SkillLabelProjectionService.java b/server/skillhub-app/src/main/java/com/iflytek/skillhub/service/SkillLabelProjectionService.java new file mode 100644 index 00000000..a0292f34 --- /dev/null +++ b/server/skillhub-app/src/main/java/com/iflytek/skillhub/service/SkillLabelProjectionService.java @@ -0,0 +1,94 @@ +package com.iflytek.skillhub.service; + +import com.iflytek.skillhub.domain.label.LabelDefinition; +import com.iflytek.skillhub.domain.label.LabelDefinitionService; +import com.iflytek.skillhub.domain.label.LabelTranslation; +import com.iflytek.skillhub.domain.label.SkillLabel; +import com.iflytek.skillhub.domain.label.SkillLabelService; +import com.iflytek.skillhub.dto.SkillLabelDto; +import java.util.Comparator; +import java.util.List; +import java.util.Map; +import java.util.function.Function; +import java.util.stream.Collectors; +import org.springframework.stereotype.Service; + +/** + * Projects skill labels for a whole page of skills in a fixed number of queries. + * + *

Listing endpoints need labels for every item they return, so resolving them one + * skill at a time would issue three queries per row. This service batches the + * assignment, definition, and translation lookups instead.

+ */ +@Service +public class SkillLabelProjectionService { + + private final SkillLabelService skillLabelService; + private final LabelDefinitionService labelDefinitionService; + private final LabelLocalizationService labelLocalizationService; + + public SkillLabelProjectionService(SkillLabelService skillLabelService, + LabelDefinitionService labelDefinitionService, + LabelLocalizationService labelLocalizationService) { + this.skillLabelService = skillLabelService; + this.labelDefinitionService = labelDefinitionService; + this.labelLocalizationService = labelLocalizationService; + } + + /** + * Labels for each requested skill, keyed by skill id. Skills without labels are absent + * from the map rather than mapped to an empty list. + */ + public Map> labelsBySkillIds(List skillIds) { + if (skillIds == null || skillIds.isEmpty()) { + return Map.of(); + } + + List distinctSkillIds = skillIds.stream() + .filter(java.util.Objects::nonNull) + .distinct() + .toList(); + if (distinctSkillIds.isEmpty()) { + return Map.of(); + } + + List assignments = skillLabelService.listSkillLabelsBySkillIds(distinctSkillIds); + if (assignments.isEmpty()) { + return Map.of(); + } + + List labelIds = assignments.stream() + .map(SkillLabel::getLabelId) + .distinct() + .toList(); + Map definitionsById = labelDefinitionService.listByIds(labelIds).stream() + .collect(Collectors.toMap(LabelDefinition::getId, Function.identity())); + Map> translationsByLabelId = + labelDefinitionService.listTranslationsByLabelIds(labelIds); + + return assignments.stream() + .filter(assignment -> definitionsById.containsKey(assignment.getLabelId())) + .collect(Collectors.groupingBy( + SkillLabel::getSkillId, + Collectors.collectingAndThen( + Collectors.toList(), + skillAssignments -> skillAssignments.stream() + .map(assignment -> toDto( + definitionsById.get(assignment.getLabelId()), + translationsByLabelId)) + .sorted(Comparator.comparing(SkillLabelDto::type) + .thenComparing(SkillLabelDto::slug)) + .toList()))); + } + + private SkillLabelDto toDto(LabelDefinition definition, + Map> translationsByLabelId) { + return new SkillLabelDto( + definition.getSlug(), + definition.getType().name(), + labelLocalizationService.resolveDisplayName( + definition.getSlug(), + translationsByLabelId.getOrDefault(definition.getId(), List.of())) + ); + } +} diff --git a/server/skillhub-app/src/main/resources/messages.properties b/server/skillhub-app/src/main/resources/messages.properties index 8791e6be..50493763 100644 --- a/server/skillhub-app/src/main/resources/messages.properties +++ b/server/skillhub-app/src/main/resources/messages.properties @@ -54,6 +54,7 @@ error.forbidden=Forbidden error.apiToken.scope.missing=API token is missing required scope: {0} error.apiToken.endpoint.unsupported=API token cannot access endpoint: {0} error.request.timeout=Request timed out +error.request.include.unsupported=Unsupported include option: {0} error.rateLimit.exceeded=Rate limit exceeded error.storage.unavailable=Object storage is temporarily unavailable. Please try again later. error.internal=An unexpected error occurred diff --git a/server/skillhub-app/src/main/resources/messages_zh.properties b/server/skillhub-app/src/main/resources/messages_zh.properties index 0e1b3fc3..febc9c4e 100644 --- a/server/skillhub-app/src/main/resources/messages_zh.properties +++ b/server/skillhub-app/src/main/resources/messages_zh.properties @@ -54,6 +54,7 @@ error.forbidden=没有权限执行该操作 error.apiToken.scope.missing=API 令牌缺少所需权限范围:{0} error.apiToken.endpoint.unsupported=API 令牌无法访问接口:{0} error.request.timeout=请求超时 +error.request.include.unsupported=不支持的 include 参数:{0} error.rateLimit.exceeded=请求过于频繁,请稍后再试 error.storage.unavailable=对象存储暂时不可用,请稍后再试 error.internal=服务器内部错误 diff --git a/server/skillhub-app/src/test/java/com/iflytek/skillhub/compat/ClawHubCompatAppServiceTest.java b/server/skillhub-app/src/test/java/com/iflytek/skillhub/compat/ClawHubCompatAppServiceTest.java index 7c2a694f..b3a7f019 100644 --- a/server/skillhub-app/src/test/java/com/iflytek/skillhub/compat/ClawHubCompatAppServiceTest.java +++ b/server/skillhub-app/src/test/java/com/iflytek/skillhub/compat/ClawHubCompatAppServiceTest.java @@ -15,8 +15,13 @@ import com.iflytek.skillhub.domain.skill.SkillVisibility; import com.iflytek.skillhub.domain.skill.service.SkillPublishService; import com.iflytek.skillhub.domain.skill.service.SkillQueryService; import com.iflytek.skillhub.domain.social.SkillStarService; +import com.iflytek.skillhub.compat.dto.ClawHubSkillListResponse; +import com.iflytek.skillhub.dto.SkillLabelDto; +import com.iflytek.skillhub.dto.SkillSummaryResponse; import com.iflytek.skillhub.observability.RequestIdAccessor; +import com.iflytek.skillhub.service.SkillLabelProjectionService; import com.iflytek.skillhub.service.SkillSearchAppService; +import java.util.List; import java.util.Map; import java.util.Optional; import org.junit.jupiter.api.Test; @@ -31,6 +36,7 @@ class ClawHubCompatAppServiceTest { private final AuditLogService auditLogService = mock(AuditLogService.class); private final CompatSkillLookupService compatSkillLookupService = mock(CompatSkillLookupService.class); private final SkillStarService skillStarService = mock(SkillStarService.class); + private final SkillLabelProjectionService skillLabelProjectionService = mock(SkillLabelProjectionService.class); private final ClawHubCompatAppService service = new ClawHubCompatAppService( new CanonicalSlugMapper(), @@ -42,7 +48,8 @@ class ClawHubCompatAppServiceTest { auditLogService, compatSkillLookupService, skillStarService, - new RequestIdAccessor() + new RequestIdAccessor(), + skillLabelProjectionService ); @Test @@ -120,4 +127,35 @@ class ClawHubCompatAppServiceTest { assertThat(location).isEqualTo("/api/v1/skills/team-a/my-skill/versions/20260707.025847/download"); } + + @Test + void listSkills_omitsLabelsByDefault() { + when(skillSearchAppService.search("", null, "newest", 0, 25, null, Map.of())) + .thenReturn(new SkillSearchAppService.SearchResponse(List.of(summary(7L)), 1, 0, 25)); + + ClawHubSkillListResponse response = service.listSkills(0, 25, null, null, Map.of()); + + assertThat(response.items()).hasSize(1); + assertThat(response.items().get(0).labels()).isNull(); + } + + @Test + void listSkills_returnsLabelsWhenRequested() { + when(skillSearchAppService.search("", null, "newest", 0, 25, null, Map.of())) + .thenReturn(new SkillSearchAppService.SearchResponse(List.of(summary(7L)), 1, 0, 25)); + when(skillLabelProjectionService.labelsBySkillIds(List.of(7L))) + .thenReturn(Map.of(7L, List.of(new SkillLabelDto("automation", "RECOMMENDED", "Automation")))); + + ClawHubSkillListResponse response = service.listSkills(0, 25, null, true, null, Map.of()); + + assertThat(response.items().get(0).labels()) + .extracting(SkillLabelDto::slug) + .containsExactly("automation"); + } + + private static SkillSummaryResponse summary(Long id) { + return new SkillSummaryResponse( + id, "demo-skill", "Demo Skill", "A demo", "PUBLIC", "PUBLISHED", + 0L, 0, null, 0, "global", null, false, null, null, null, null, null); + } } diff --git a/server/skillhub-app/src/test/java/com/iflytek/skillhub/compat/ClawHubCompatControllerTest.java b/server/skillhub-app/src/test/java/com/iflytek/skillhub/compat/ClawHubCompatControllerTest.java index b07914a4..ac1abaff 100644 --- a/server/skillhub-app/src/test/java/com/iflytek/skillhub/compat/ClawHubCompatControllerTest.java +++ b/server/skillhub-app/src/test/java/com/iflytek/skillhub/compat/ClawHubCompatControllerTest.java @@ -19,7 +19,9 @@ import com.iflytek.skillhub.domain.skill.SkillVisibility; import com.iflytek.skillhub.domain.user.UserAccount; import com.iflytek.skillhub.domain.user.UserAccountRepository; import com.iflytek.skillhub.dto.SkillLifecycleVersionResponse; +import com.iflytek.skillhub.dto.SkillLabelDto; import com.iflytek.skillhub.dto.SkillSummaryResponse; +import com.iflytek.skillhub.service.SkillLabelProjectionService; import com.iflytek.skillhub.service.SkillSearchAppService; import java.math.BigDecimal; import java.nio.charset.StandardCharsets; @@ -48,6 +50,7 @@ import static org.mockito.ArgumentMatchers.anyMap; import static org.mockito.ArgumentMatchers.isNull; import static org.mockito.ArgumentMatchers.same; import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; import static org.mockito.Mockito.when; import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.authentication; import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf; @@ -72,6 +75,9 @@ class ClawHubCompatControllerTest { @MockBean private SkillSearchAppService skillSearchAppService; + @MockBean + private SkillLabelProjectionService skillLabelProjectionService; + @MockBean private SkillQueryService skillQueryService; @@ -154,6 +160,41 @@ class ClawHubCompatControllerTest { verify(apiTokenService).touchLastUsed(same(token)); } + @Test + void listSkills_shouldOmitLabelsByDefault() throws Exception { + when(skillSearchAppService.search(eq(""), isNull(), eq("newest"), eq(0), eq(25), isNull(), isNull())) + .thenReturn(new SkillSearchAppService.SearchResponse(List.of(summary(7L)), 1, 0, 25)); + + mockMvc.perform(get("/api/v1/skills")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.items[0].slug").value("demo-skill")) + .andExpect(jsonPath("$.items[0].labels").doesNotExist()); + } + + @Test + void listSkills_shouldReturnLabelsWhenIncluded() throws Exception { + when(skillSearchAppService.search(eq(""), isNull(), eq("newest"), eq(0), eq(25), isNull(), isNull())) + .thenReturn(new SkillSearchAppService.SearchResponse(List.of(summary(7L)), 1, 0, 25)); + when(skillLabelProjectionService.labelsBySkillIds(List.of(7L))) + .thenReturn(java.util.Map.of( + 7L, + List.of(new SkillLabelDto("automation", "RECOMMENDED", "Automation")))); + + mockMvc.perform(get("/api/v1/skills").param("include", "labels")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.items[0].labels[0].slug").value("automation")) + .andExpect(jsonPath("$.items[0].labels[0].type").value("RECOMMENDED")) + .andExpect(jsonPath("$.items[0].labels[0].displayName").value("Automation")); + } + + @Test + void listSkills_shouldRejectUnsupportedIncludeOptions() throws Exception { + mockMvc.perform(get("/api/v1/skills").param("include", "labels,stats")) + .andExpect(status().isBadRequest()); + + verifyNoInteractions(skillSearchAppService); + } + @Test void downloadQuery_withBearerToken_shouldProjectNamespaceRolesIntoRequestContext() throws Exception { ApiToken token = new ApiToken("user-7", "cli", "sk_test", "hash", "[]"); @@ -401,6 +442,28 @@ class ClawHubCompatControllerTest { return version; } + private SkillSummaryResponse summary(Long id) { + return new SkillSummaryResponse( + id, + "demo-skill", + "Demo Skill", + "A demo", + "PUBLIC", + "PUBLISHED", + 0L, + 0, + null, + 0, + "global", + null, + false, + null, + null, + null, + null, + null); + } + private UsernamePasswordAuthenticationToken superAdminAuth() { PlatformPrincipal principal = new PlatformPrincipal( "user-42", diff --git a/server/skillhub-app/src/test/java/com/iflytek/skillhub/controller/SkillSearchControllerTest.java b/server/skillhub-app/src/test/java/com/iflytek/skillhub/controller/SkillSearchControllerTest.java index bed761ad..2674fc2e 100644 --- a/server/skillhub-app/src/test/java/com/iflytek/skillhub/controller/SkillSearchControllerTest.java +++ b/server/skillhub-app/src/test/java/com/iflytek/skillhub/controller/SkillSearchControllerTest.java @@ -1,6 +1,9 @@ package com.iflytek.skillhub.controller; import com.iflytek.skillhub.domain.namespace.NamespaceMemberRepository; +import com.iflytek.skillhub.dto.SkillLabelDto; +import com.iflytek.skillhub.dto.SkillSummaryResponse; +import com.iflytek.skillhub.service.SkillLabelProjectionService; import com.iflytek.skillhub.service.SkillSearchAppService; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; @@ -15,6 +18,7 @@ import java.util.Map; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.verifyNoInteractions; import static org.mockito.Mockito.when; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; @@ -34,6 +38,9 @@ class SkillSearchControllerTest { @MockBean private SkillSearchAppService skillSearchAppService; + @MockBean + private SkillLabelProjectionService skillLabelProjectionService; + @Test void searchShouldUseUnifiedEnvelopeAndItemsField() throws Exception { when(skillSearchAppService.search( @@ -143,4 +150,59 @@ class SkillSearchControllerTest { .andExpect(jsonPath("$.data.page").value(0)) .andExpect(jsonPath("$.data.size").value(20)); } + + @Test + void searchShouldOmitLabelsUnlessRequested() throws Exception { + when(skillSearchAppService.search( + eq(null), eq(null), eq("newest"), eq(0), eq(20), eq(null), any(), any())) + .thenReturn(new SkillSearchAppService.SearchResponse(List.of(summary(7L)), 1, 0, 20)); + + mockMvc.perform(get("/api/web/skills")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.data.items[0].slug").value("demo-skill")) + .andExpect(jsonPath("$.data.items[0].labels").doesNotExist()); + } + + @Test + void searchShouldReturnLabelsWhenRequested() throws Exception { + when(skillSearchAppService.search( + eq(null), eq(null), eq("newest"), eq(0), eq(20), eq(null), any(), any())) + .thenReturn(new SkillSearchAppService.SearchResponse(List.of(summary(7L)), 1, 0, 20)); + when(skillLabelProjectionService.labelsBySkillIds(List.of(7L))) + .thenReturn(Map.of(7L, List.of(new SkillLabelDto("automation", "TOPIC", "Automation")))); + + mockMvc.perform(get("/api/web/skills").param("include", "labels")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.data.items[0].labels[0].slug").value("automation")) + .andExpect(jsonPath("$.data.items[0].labels[0].type").value("TOPIC")) + .andExpect(jsonPath("$.data.items[0].labels[0].displayName").value("Automation")); + } + + @Test + void searchShouldReturnEmptyLabelArrayForSkillsWithoutLabels() throws Exception { + when(skillSearchAppService.search( + eq(null), eq(null), eq("newest"), eq(0), eq(20), eq(null), any(), any())) + .thenReturn(new SkillSearchAppService.SearchResponse(List.of(summary(7L)), 1, 0, 20)); + when(skillLabelProjectionService.labelsBySkillIds(List.of(7L))).thenReturn(Map.of()); + + mockMvc.perform(get("/api/web/skills").param("include", "labels")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.data.items[0].labels").isArray()) + .andExpect(jsonPath("$.data.items[0].labels").isEmpty()); + } + + @Test + void searchShouldRejectUnsupportedIncludeOptions() throws Exception { + mockMvc.perform(get("/api/web/skills").param("include", "labels,stats")) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.code").value(400)); + + verifyNoInteractions(skillSearchAppService); + } + + private static SkillSummaryResponse summary(Long id) { + return new SkillSummaryResponse( + id, "demo-skill", "Demo Skill", "A demo", "PUBLIC", "PUBLISHED", + 0L, 0, null, 0, "global", null, false, null, null, null, null, null); + } } diff --git a/server/skillhub-app/src/test/java/com/iflytek/skillhub/controller/support/IncludeOptionsTest.java b/server/skillhub-app/src/test/java/com/iflytek/skillhub/controller/support/IncludeOptionsTest.java new file mode 100644 index 00000000..7379f7c0 --- /dev/null +++ b/server/skillhub-app/src/test/java/com/iflytek/skillhub/controller/support/IncludeOptionsTest.java @@ -0,0 +1,28 @@ +package com.iflytek.skillhub.controller.support; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import com.iflytek.skillhub.domain.shared.exception.DomainBadRequestException; +import java.util.List; +import org.junit.jupiter.api.Test; + +class IncludeOptionsTest { + + @Test + void includesLabels_acceptsAbsentBlankRepeatedAndCommaSeparatedInputs() { + assertThat(IncludeOptions.includesLabels(null)).isFalse(); + assertThat(IncludeOptions.includesLabels(List.of("", " "))).isFalse(); + assertThat(IncludeOptions.includesLabels(List.of("labels"))).isTrue(); + assertThat(IncludeOptions.includesLabels(List.of(" LABELS "))).isTrue(); + assertThat(IncludeOptions.includesLabels(List.of("labels,"))).isTrue(); + assertThat(IncludeOptions.includesLabels(List.of("", "labels"))).isTrue(); + } + + @Test + void includesLabels_rejectsUnsupportedOptions() { + assertThatThrownBy(() -> IncludeOptions.includesLabels(List.of("labels,stats"))) + .isInstanceOf(DomainBadRequestException.class) + .hasMessage("error.request.include.unsupported"); + } +} diff --git a/server/skillhub-app/src/test/java/com/iflytek/skillhub/service/SkillLabelProjectionServiceTest.java b/server/skillhub-app/src/test/java/com/iflytek/skillhub/service/SkillLabelProjectionServiceTest.java new file mode 100644 index 00000000..55fa32dc --- /dev/null +++ b/server/skillhub-app/src/test/java/com/iflytek/skillhub/service/SkillLabelProjectionServiceTest.java @@ -0,0 +1,81 @@ +package com.iflytek.skillhub.service; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyList; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import com.iflytek.skillhub.domain.label.LabelDefinition; +import com.iflytek.skillhub.domain.label.LabelDefinitionService; +import com.iflytek.skillhub.domain.label.LabelType; +import com.iflytek.skillhub.domain.label.SkillLabel; +import com.iflytek.skillhub.domain.label.SkillLabelService; +import com.iflytek.skillhub.dto.SkillLabelDto; +import java.util.List; +import java.util.Map; +import org.junit.jupiter.api.Test; +import org.springframework.test.util.ReflectionTestUtils; + +class SkillLabelProjectionServiceTest { + + private final SkillLabelService skillLabelService = mock(SkillLabelService.class); + private final LabelDefinitionService labelDefinitionService = mock(LabelDefinitionService.class); + private final LabelLocalizationService labelLocalizationService = new LabelLocalizationService(); + + private final SkillLabelProjectionService service = new SkillLabelProjectionService( + skillLabelService, labelDefinitionService, labelLocalizationService); + + @Test + void labelsBySkillIds_groupsLabelsPerSkillInOneBatch() { + LabelDefinition automation = definition(10L, "automation", LabelType.RECOMMENDED); + LabelDefinition audited = definition(11L, "audited", LabelType.PRIVILEGED); + + when(skillLabelService.listSkillLabelsBySkillIds(List.of(1L, 2L))).thenReturn(List.of( + new SkillLabel(1L, 10L, "owner-1"), + new SkillLabel(1L, 11L, "owner-1"), + new SkillLabel(2L, 10L, "owner-2") + )); + when(labelDefinitionService.listByIds(anyList())).thenReturn(List.of(automation, audited)); + when(labelDefinitionService.listTranslationsByLabelIds(anyList())).thenReturn(Map.of()); + + Map> labels = service.labelsBySkillIds(List.of(1L, 2L)); + + // sorted by label type, then slug: PRIVILEGED before RECOMMENDED + assertEquals(List.of("audited", "automation"), labels.get(1L).stream().map(SkillLabelDto::slug).toList()); + assertEquals(List.of("automation"), labels.get(2L).stream().map(SkillLabelDto::slug).toList()); + + // One query per lookup for the whole page, not per skill. + verify(skillLabelService, times(1)).listSkillLabelsBySkillIds(anyList()); + verify(labelDefinitionService, times(1)).listByIds(anyList()); + verify(labelDefinitionService, times(1)).listTranslationsByLabelIds(anyList()); + } + + @Test + void labelsBySkillIds_skipsAssignmentsWithoutADefinition() { + when(skillLabelService.listSkillLabelsBySkillIds(anyList())) + .thenReturn(List.of(new SkillLabel(1L, 99L, "owner-1"))); + when(labelDefinitionService.listByIds(anyList())).thenReturn(List.of()); + when(labelDefinitionService.listTranslationsByLabelIds(anyList())).thenReturn(Map.of()); + + assertTrue(service.labelsBySkillIds(List.of(1L)).isEmpty()); + } + + @Test + void labelsBySkillIds_touchesNoRepositoryForAnEmptyPage() { + assertTrue(service.labelsBySkillIds(List.of()).isEmpty()); + assertTrue(service.labelsBySkillIds(null).isEmpty()); + + verify(skillLabelService, never()).listSkillLabelsBySkillIds(any()); + } + + private static LabelDefinition definition(Long id, String slug, LabelType type) { + LabelDefinition definition = new LabelDefinition(slug, type, true, 0, "admin"); + ReflectionTestUtils.setField(definition, "id", id); + return definition; + } +} diff --git a/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/label/SkillLabelService.java b/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/label/SkillLabelService.java index 9c699cb5..b0b66095 100644 --- a/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/label/SkillLabelService.java +++ b/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/label/SkillLabelService.java @@ -38,6 +38,13 @@ public class SkillLabelService { return skillLabelRepository.findBySkillId(skillId); } + public List listSkillLabelsBySkillIds(List skillIds) { + if (skillIds == null || skillIds.isEmpty()) { + return List.of(); + } + return skillLabelRepository.findBySkillIdIn(skillIds); + } + public List listByLabelId(Long labelId) { return skillLabelRepository.findByLabelId(labelId); } diff --git a/web/src/api/generated/schema.d.ts b/web/src/api/generated/schema.d.ts index 3c4f5b8e..423ee239 100644 --- a/web/src/api/generated/schema.d.ts +++ b/web/src/api/generated/schema.d.ts @@ -4353,6 +4353,7 @@ export interface components { ownerPreviewVersion?: components["schemas"]["SkillLifecycleVersionResponse"]; resolutionMode?: string; complianceSnapshot?: components["schemas"]["ComplianceSnapshotResponse"]; + labels?: components["schemas"]["SkillLabelDto"][]; }; ApiResponseBoolean: { /** Format: int32 */ @@ -4980,6 +4981,7 @@ export interface components { /** Format: int64 */ updatedAt?: number; latestVersion?: components["schemas"]["LatestVersion"]; + labels?: components["schemas"]["SkillLabelDto"][]; }; ApiResponseListSecurityAuditResponse: { /** Format: int32 */ @@ -8002,6 +8004,8 @@ export interface operations { page?: number; limit?: number; sort?: string; + /** @description Optional response expansions. Supported value: labels */ + include?: string[]; }; header?: never; path?: never; @@ -9039,6 +9043,8 @@ export interface operations { q?: string; namespace?: string; label?: string[]; + /** @description Optional response expansions. Supported value: labels */ + include?: string[]; sort?: string; page?: number; size?: number; From 907d8eff906fcf1c3e42b0ab6e50453e088065f2 Mon Sep 17 00:00:00 2001 From: FenjuFu <92919259+FenjuFu@users.noreply.github.com> Date: Tue, 25 Aug 2026 16:56:58 +0800 Subject: [PATCH 04/15] fix(web): keep select menus within viewport (#754) Cap shared Radix select content to the available viewport height and enable vertical scrolling so long option lists remain usable near page and dialog edges. Add a shared component regression test for the viewport and overflow classes.\n\nCloses #714 Signed-off-by: FenjuFu <92919259+FenjuFu@users.noreply.github.com> --- web/src/shared/ui/select.test.ts | 8 ++++++++ web/src/shared/ui/select.tsx | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/web/src/shared/ui/select.test.ts b/web/src/shared/ui/select.test.ts index 0768fd3a..9da2727f 100644 --- a/web/src/shared/ui/select.test.ts +++ b/web/src/shared/ui/select.test.ts @@ -40,6 +40,14 @@ describe('shared select contract', () => { expect(SELECT_ITEM_CLASS_NAME).toContain('rounded-md') }) + it('keeps long option lists inside the available viewport', () => { + expect(SELECT_CONTENT_CLASS_NAME).toContain( + 'max-h-[var(--radix-select-content-available-height)]' + ) + expect(SELECT_CONTENT_CLASS_NAME).toContain('overflow-y-auto') + expect(SELECT_CONTENT_CLASS_NAME).toContain('overflow-x-hidden') + }) + it('uses pointer cursors for expanded select interactions', () => { expect(SELECT_ITEM_CLASS_NAME).toContain('cursor-pointer') expect(SELECT_SCROLL_BUTTON_CLASS_NAME).toContain('cursor-pointer') diff --git a/web/src/shared/ui/select.tsx b/web/src/shared/ui/select.tsx index f45cc4f5..3f0c72ba 100644 --- a/web/src/shared/ui/select.tsx +++ b/web/src/shared/ui/select.tsx @@ -12,7 +12,7 @@ export const SELECT_TRIGGER_CLASS_NAME = cn( ) export const SELECT_CONTENT_CLASS_NAME = cn( - 'z-50 overflow-hidden rounded-lg border border-border bg-popover text-popover-foreground shadow-md', + 'z-50 max-h-[var(--radix-select-content-available-height)] overflow-x-hidden overflow-y-auto rounded-lg border border-border bg-popover text-popover-foreground shadow-md', // In-tree (no Portal): avoids React 19 removeChild races on route unmount. // No exit animations: delayed unmount still races commits when Content was portaled. 'data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95', From 5a95278528726c904a895f77c5432062c14cd860 Mon Sep 17 00:00:00 2001 From: wrj97 Date: Tue, 25 Aug 2026 16:58:17 +0800 Subject: [PATCH 05/15] fix(compat): prioritize PUBLIC/GLOBAL skills in legacy slug lookup (#750) * fix(compat): prioritize PUBLIC/GLOBAL skills in legacy slug lookup When multiple skills share the same slug across namespaces, findByLegacySlug now prefers PUBLIC visibility and GLOBAL namespace over NAMESPACE_ONLY/PRIVATE ones, so plain slug lookups resolve to the most accessible skill. Namespaces are batch-fetched via findByIdIn to avoid N+1 database queries. Signed-off-by: wurongjie * fix(compat): prefer published legacy slug candidates Signed-off-by: XiaoSeS <87064762+XiaoSeS@users.noreply.github.com> --------- Signed-off-by: wurongjie Signed-off-by: XiaoSeS <87064762+XiaoSeS@users.noreply.github.com> Co-authored-by: XiaoSeS <87064762+XiaoSeS@users.noreply.github.com> --- .../compat/CompatSkillLookupService.java | 45 +++++++++++++++-- .../compat/CompatSkillLookupServiceTest.java | 50 +++++++++++++++++++ 2 files changed, 91 insertions(+), 4 deletions(-) diff --git a/server/skillhub-app/src/main/java/com/iflytek/skillhub/compat/CompatSkillLookupService.java b/server/skillhub-app/src/main/java/com/iflytek/skillhub/compat/CompatSkillLookupService.java index 9da699a2..36d7d7b0 100644 --- a/server/skillhub-app/src/main/java/com/iflytek/skillhub/compat/CompatSkillLookupService.java +++ b/server/skillhub-app/src/main/java/com/iflytek/skillhub/compat/CompatSkillLookupService.java @@ -3,15 +3,21 @@ package com.iflytek.skillhub.compat; import com.iflytek.skillhub.domain.namespace.Namespace; import com.iflytek.skillhub.domain.namespace.NamespaceRole; import com.iflytek.skillhub.domain.namespace.NamespaceRepository; +import com.iflytek.skillhub.domain.namespace.NamespaceType; import com.iflytek.skillhub.domain.shared.exception.DomainNotFoundException; import com.iflytek.skillhub.domain.skill.Skill; import com.iflytek.skillhub.domain.skill.SkillRepository; import com.iflytek.skillhub.domain.skill.SkillVersion; import com.iflytek.skillhub.domain.skill.SkillVersionRepository; +import com.iflytek.skillhub.domain.skill.SkillVisibility; import com.iflytek.skillhub.domain.skill.VisibilityChecker; import com.iflytek.skillhub.domain.skill.service.SkillSlugResolutionService; +import java.util.Comparator; +import java.util.List; import java.util.Map; import java.util.Optional; +import java.util.function.Function; +import java.util.stream.Collectors; import org.springframework.stereotype.Service; /** @@ -21,6 +27,10 @@ import org.springframework.stereotype.Service; @Service public class CompatSkillLookupService { + private static final int LEGACY_SLUG_PUBLISHED_SCORE = 1_000; + private static final int LEGACY_SLUG_PUBLIC_SCORE = 100; + private static final int LEGACY_SLUG_GLOBAL_SCORE = 10; + private final SkillRepository skillRepository; private final NamespaceRepository namespaceRepository; private final SkillVersionRepository skillVersionRepository; @@ -40,10 +50,27 @@ public class CompatSkillLookupService { } public CompatSkillContext findByLegacySlug(String slug) { - Skill skill = skillRepository.findBySlug(slug).stream().findFirst() - .orElseThrow(() -> new DomainNotFoundException("error.skill.notFound", slug)); - Namespace namespace = namespaceRepository.findById(skill.getNamespaceId()) - .orElseThrow(() -> new DomainNotFoundException("error.namespace.notFound", skill.getNamespaceId())); + List skills = skillRepository.findBySlug(slug); + if (skills.isEmpty()) { + throw new DomainNotFoundException("error.skill.notFound", slug); + } + // Batch-fetch all candidate namespaces in a single query to avoid N+1 lookups. + List namespaceIds = skills.stream() + .map(Skill::getNamespaceId) + .distinct() + .toList(); + Map namespacesById = namespaceIds.isEmpty() + ? Map.of() + : namespaceRepository.findByIdIn(namespaceIds).stream() + .collect(Collectors.toMap(Namespace::getId, Function.identity())); + Skill skill = skills.stream() + .min(Comparator.comparingInt(s -> -legacySlugCandidateScore(s, namespacesById)) + .thenComparing(Skill::getId)) + .orElse(skills.get(0)); + Namespace namespace = namespacesById.get(skill.getNamespaceId()); + if (namespace == null) { + throw new DomainNotFoundException("error.namespace.notFound", skill.getNamespaceId()); + } return new CompatSkillContext(namespace, skill, findLatestVersion(skill)); } @@ -86,6 +113,16 @@ public class CompatSkillLookupService { return skillVersionRepository.findById(skill.getLatestVersionId()); } + private static int legacySlugCandidateScore(Skill skill, Map namespacesById) { + Namespace namespace = namespacesById.get(skill.getNamespaceId()); + int score = 0; + // Bare-slug CLI resolution should prefer installable candidates before namespace defaults. + if (skill.getLatestVersionId() != null) score += LEGACY_SLUG_PUBLISHED_SCORE; + if (skill.getVisibility() == SkillVisibility.PUBLIC) score += LEGACY_SLUG_PUBLIC_SCORE; + if (namespace != null && namespace.getType() == NamespaceType.GLOBAL) score += LEGACY_SLUG_GLOBAL_SCORE; + return score; + } + private Skill resolveVisibleSkill(Long namespaceId, String slug, String currentUserId) { try { return skillSlugResolutionService.resolve( diff --git a/server/skillhub-app/src/test/java/com/iflytek/skillhub/compat/CompatSkillLookupServiceTest.java b/server/skillhub-app/src/test/java/com/iflytek/skillhub/compat/CompatSkillLookupServiceTest.java index 3500ce04..b8722391 100644 --- a/server/skillhub-app/src/test/java/com/iflytek/skillhub/compat/CompatSkillLookupServiceTest.java +++ b/server/skillhub-app/src/test/java/com/iflytek/skillhub/compat/CompatSkillLookupServiceTest.java @@ -8,6 +8,7 @@ import static org.mockito.Mockito.when; import com.iflytek.skillhub.domain.namespace.Namespace; import com.iflytek.skillhub.domain.namespace.NamespaceRepository; import com.iflytek.skillhub.domain.namespace.NamespaceRole; +import com.iflytek.skillhub.domain.namespace.NamespaceType; import com.iflytek.skillhub.domain.shared.exception.DomainNotFoundException; import com.iflytek.skillhub.domain.skill.Skill; import com.iflytek.skillhub.domain.skill.SkillRepository; @@ -15,6 +16,7 @@ import com.iflytek.skillhub.domain.skill.SkillVersionRepository; import com.iflytek.skillhub.domain.skill.SkillVisibility; import com.iflytek.skillhub.domain.skill.VisibilityChecker; import com.iflytek.skillhub.domain.skill.service.SkillSlugResolutionService; +import java.util.List; import java.util.Map; import java.util.Optional; import org.junit.jupiter.api.Test; @@ -36,6 +38,40 @@ class CompatSkillLookupServiceTest { visibilityChecker ); + @Test + void findByLegacySlug_prefersPublicGlobalPublishedCandidate() { + Skill privateTeamSkill = skill(11L, 1L, "demo", SkillVisibility.PRIVATE, 110L); + Skill publicTeamSkill = skill(12L, 1L, "demo", SkillVisibility.PUBLIC, 120L); + Skill publicGlobalSkill = skill(13L, 2L, "demo", SkillVisibility.PUBLIC, 130L); + Namespace teamNamespace = namespace(1L, "team-a", NamespaceType.TEAM); + Namespace globalNamespace = namespace(2L, "global", NamespaceType.GLOBAL); + + when(skillRepository.findBySlug("demo")) + .thenReturn(List.of(privateTeamSkill, publicTeamSkill, publicGlobalSkill)); + when(namespaceRepository.findByIdIn(List.of(1L, 2L))).thenReturn(List.of(teamNamespace, globalNamespace)); + + CompatSkillLookupService.CompatSkillContext result = service.findByLegacySlug("demo"); + + assertThat(result.skill().getId()).isEqualTo(13L); + assertThat(result.namespace().getSlug()).isEqualTo("global"); + } + + @Test + void findByLegacySlug_prefersPublishedCandidateOverGlobalDraft() { + Skill publicGlobalDraft = skill(21L, 2L, "demo", SkillVisibility.PUBLIC, null); + Skill publicTeamPublished = skill(22L, 1L, "demo", SkillVisibility.PUBLIC, 220L); + Namespace teamNamespace = namespace(1L, "team-a", NamespaceType.TEAM); + Namespace globalNamespace = namespace(2L, "global", NamespaceType.GLOBAL); + + when(skillRepository.findBySlug("demo")).thenReturn(List.of(publicGlobalDraft, publicTeamPublished)); + when(namespaceRepository.findByIdIn(List.of(2L, 1L))).thenReturn(List.of(globalNamespace, teamNamespace)); + + CompatSkillLookupService.CompatSkillContext result = service.findByLegacySlug("demo"); + + assertThat(result.skill().getId()).isEqualTo(22L); + assertThat(result.namespace().getSlug()).isEqualTo("team-a"); + } + @Test void resolveVisible_throwsNotFoundWhenCallerCannotAccessSkill() { Namespace namespace = new Namespace("team-a", "Team A", "owner-1"); @@ -75,4 +111,18 @@ class CompatSkillLookupServiceTest { assertThat(result.skill().getId()).isEqualTo(7L); } + + private static Skill skill(Long id, Long namespaceId, String slug, SkillVisibility visibility, Long latestVersionId) { + Skill skill = new Skill(namespaceId, slug, "owner-1", visibility); + ReflectionTestUtils.setField(skill, "id", id); + skill.setLatestVersionId(latestVersionId); + return skill; + } + + private static Namespace namespace(Long id, String slug, NamespaceType type) { + Namespace namespace = new Namespace(slug, slug, "owner-1"); + ReflectionTestUtils.setField(namespace, "id", id); + namespace.setType(type); + return namespace; + } } From 7599dd0ca96cf98cafad0e324d887c553dd66c63 Mon Sep 17 00:00:00 2001 From: XiaoSeS <87064762+XiaoSeS@users.noreply.github.com> Date: Tue, 25 Aug 2026 20:07:58 +0800 Subject: [PATCH 06/15] fix(web): keep select poppers in collision bounds (#755) Signed-off-by: XiaoSeS <87064762+XiaoSeS@users.noreply.github.com> --- web/src/shared/ui/select.test.ts | 5 +++++ web/src/shared/ui/select.tsx | 5 ++--- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/web/src/shared/ui/select.test.ts b/web/src/shared/ui/select.test.ts index 9da2727f..3f304500 100644 --- a/web/src/shared/ui/select.test.ts +++ b/web/src/shared/ui/select.test.ts @@ -48,6 +48,11 @@ describe('shared select contract', () => { expect(SELECT_CONTENT_CLASS_NAME).toContain('overflow-x-hidden') }) + it('does not move popper content with static translate utilities', () => { + expect(SELECT_CONTENT_CLASS_NAME).not.toContain('translate-y-1') + expect(SELECT_CONTENT_CLASS_NAME).not.toContain('translate-x-1') + }) + it('uses pointer cursors for expanded select interactions', () => { expect(SELECT_ITEM_CLASS_NAME).toContain('cursor-pointer') expect(SELECT_SCROLL_BUTTON_CLASS_NAME).toContain('cursor-pointer') diff --git a/web/src/shared/ui/select.tsx b/web/src/shared/ui/select.tsx index 3f0c72ba..102e4279 100644 --- a/web/src/shared/ui/select.tsx +++ b/web/src/shared/ui/select.tsx @@ -89,16 +89,15 @@ SelectScrollDownButton.displayName = SelectPrimitive.ScrollDownButton.displayNam const SelectContent = React.forwardRef< React.ElementRef, React.ComponentPropsWithoutRef ->(({ className, children, position = 'popper', ...props }, ref) => ( +>(({ className, children, position = 'popper', sideOffset = 4, ...props }, ref) => ( // No Portal: Content stays in the React tree with its trigger so route/Dialog // unmount cannot orphan a body/#skillhub-portals node (removeChild). Date: Wed, 26 Aug 2026 10:23:31 +0800 Subject: [PATCH 07/15] fix(smoke): support separate actuator target (#689) Signed-off-by: ShinyHero666 <160204855+ShinyHero666@users.noreply.github.com> --- README.md | 12 ++++ scripts/smoke-test.sh | 63 +++++++++++++++++++-- scripts/tests/smoke-test-admin-mode-test.sh | 51 +++++++++++++++-- 3 files changed, 115 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index a5c0fb36..d787c1d9 100644 --- a/README.md +++ b/README.md @@ -410,6 +410,18 @@ Run it against a local backend: ./scripts/smoke-test.sh http://localhost:8080 ``` +Local Compose and staging runs can keep using one backend URL. For an ingress +deployment where the public URL exposes application APIs but keeps Actuator on +the backend service, set a separate Actuator target: + +```bash +ACTUATOR_BASE_URL=http://skillhub-server:8080 \ + ./scripts/smoke-test.sh https://skillhub.example.com +``` + +The health check requires an Actuator JSON response, so an HTML SPA fallback is +reported as a routing or target error instead of a successful health response. + Admin label-management smoke checks run only when current admin credentials are supplied explicitly: diff --git a/scripts/smoke-test.sh b/scripts/smoke-test.sh index a523aafe..78852f1b 100755 --- a/scripts/smoke-test.sh +++ b/scripts/smoke-test.sh @@ -2,16 +2,18 @@ set -euo pipefail BASE_URL="${1:-http://localhost:8080}" +ACTUATOR_BASE_URL="${ACTUATOR_BASE_URL:-$BASE_URL}" PASS=0 FAIL=0 -COOKIE_JAR="$(mktemp)" +TMP_DIR="$(mktemp -d)" +COOKIE_JAR="$TMP_DIR/cookies" USERNAME="smoketest_$(date +%s)" EMAIL="${USERNAME}@example.com" PASSWORD="Smoke@2026" NEW_PASSWORD="Smoke@2027" cleanup() { - rm -f "$COOKIE_JAR" + rm -rf "$TMP_DIR" } trap cleanup EXIT @@ -31,6 +33,56 @@ check() { fi } +check_health() { + local desc="$1" + local url="$2" + local body_file="$TMP_DIR/health-body" + local result + local status + local content_type + result="$(curl --retry 3 --retry-delay 1 --max-time 10 -sS -o "$body_file" \ + -w "%{http_code}|%{content_type}" "$url" || true)" + status="${result%%|*}" + content_type="${result#*|}" + + if [[ "$content_type" == text/html* ]]; then + echo "FAIL: $desc (routing/target error: received $content_type from $url)" + FAIL=$((FAIL + 1)) + elif [[ "$status" == "200" \ + && ( "$content_type" == application/json* || "$content_type" == application/*+json* ) \ + && -f "$body_file" \ + && "$(grep -Ec '"status"[[:space:]]*:' "$body_file" || true)" -gt 0 ]]; then + echo "PASS: $desc (HTTP $status, $content_type)" + PASS=$((PASS + 1)) + else + echo "FAIL: $desc (expected HTTP 200 actuator JSON, got HTTP $status, ${content_type:-no content type})" + FAIL=$((FAIL + 1)) + fi +} + +check_protected_actuator() { + local desc="$1" + local url="$2" + local result + local status + local content_type + result="$(curl --retry 3 --retry-delay 1 --max-time 10 -sS -o /dev/null \ + -w "%{http_code}|%{content_type}" "$url" || true)" + status="${result%%|*}" + content_type="${result#*|}" + + if [[ "$content_type" == text/html* ]]; then + echo "FAIL: $desc (routing/target error: received $content_type from $url)" + FAIL=$((FAIL + 1)) + elif [[ "$status" == "401" ]]; then + echo "PASS: $desc (HTTP $status)" + PASS=$((PASS + 1)) + else + echo "FAIL: $desc (expected 401, got $status)" + FAIL=$((FAIL + 1)) + fi +} + finish() { echo echo "Results: $PASS passed, $FAIL failed" @@ -38,11 +90,12 @@ finish() { } echo "=== SkillHub Smoke Test ===" -echo "Target: $BASE_URL" +echo "API target: $BASE_URL" +echo "Actuator target: $ACTUATOR_BASE_URL" echo -check "Health endpoint" "$BASE_URL/actuator/health" "200" -check "Prometheus metrics requires auth" "$BASE_URL/actuator/prometheus" "401" +check_health "Health endpoint" "$ACTUATOR_BASE_URL/actuator/health" +check_protected_actuator "Prometheus metrics requires auth" "$ACTUATOR_BASE_URL/actuator/prometheus" check "Namespaces API requires auth" "$BASE_URL/api/v1/namespaces" "401" check "Auth required" "$BASE_URL/api/v1/auth/me" "401" diff --git a/scripts/tests/smoke-test-admin-mode-test.sh b/scripts/tests/smoke-test-admin-mode-test.sh index aa0c6d9e..672b1a58 100755 --- a/scripts/tests/smoke-test-admin-mode-test.sh +++ b/scripts/tests/smoke-test-admin-mode-test.sh @@ -26,6 +26,7 @@ data="" cookie_in="" cookie_out="" write_code=false +write_format="" output_file="" while (($#)); do case "$1" in @@ -47,6 +48,7 @@ while (($#)); do ;; -w) write_code=true + write_format="$2" shift 2 ;; -o) @@ -76,8 +78,16 @@ fi printf '%s\n' "$method $url $data" >>"${SMOKE_CURL_LOG:?SMOKE_CURL_LOG is required}" status=200 +content_type="application/json" +body='{}' case "$url" in - */actuator/health) status=200 ;; + https://public.example/actuator/health|https://public.example/actuator/prometheus) + content_type="text/html" + body='SkillHub' + ;; + */actuator/health) + body='{"status":"UP"}' + ;; */actuator/prometheus) status=401 ;; */api/v1/namespaces) if [[ -n "$cookie_in" && -f "$cookie_in.session" ]]; then status=200; else status=401; fi @@ -108,24 +118,35 @@ case "$url" in esac if [[ "$output_file" != "/dev/null" && -n "$output_file" ]]; then - printf '{}\n' >"$output_file" + printf '%s\n' "$body" >"$output_file" fi if [[ "$write_code" == true ]]; then - printf '%s' "$status" + if [[ "$write_format" == *content_type* ]]; then + printf '%s|%s' "$status" "$content_type" + else + printf '%s' "$status" + fi fi EOF chmod +x "$TMP_DIR/bin/curl" -run_smoke() { +run_smoke_at() { local name="$1" - shift + local base_url="$2" + shift 2 local log="$TMP_DIR/$name.curl.log" local out="$TMP_DIR/$name.out" local status=0 - env PATH="$TMP_DIR/bin:$PATH" SMOKE_CURL_LOG="$log" "$@" "$SMOKE_SCRIPT" http://skillhub.test >"$out" 2>&1 || status=$? + env PATH="$TMP_DIR/bin:$PATH" SMOKE_CURL_LOG="$log" "$@" "$SMOKE_SCRIPT" "$base_url" >"$out" 2>&1 || status=$? printf '%s\n' "$status" } +run_smoke() { + local name="$1" + shift + run_smoke_at "$name" http://skillhub.test "$@" +} + status="$(run_smoke skip-admin env)" [[ "$status" == "0" ]] || fail "default smoke without admin credentials should pass" grep -Fq "SKIP: Admin label management" "$TMP_DIR/skip-admin.out" \ @@ -153,4 +174,22 @@ if grep -Fq 'ChangeMe!2026' "$TMP_DIR/explicit-admin.curl.log"; then fail "admin login must not fall back to the bootstrap default password" fi +status="$(run_smoke_at split-targets https://public.example env \ + ACTUATOR_BASE_URL=http://actuator.internal:8080 SMOKE_ADMIN_CHECKS=false)" +[[ "$status" == "0" ]] || fail "split public and actuator targets should pass" +grep -Fq "GET http://actuator.internal:8080/actuator/health" "$TMP_DIR/split-targets.curl.log" \ + || fail "health check should use ACTUATOR_BASE_URL" +grep -Fq "GET http://actuator.internal:8080/actuator/prometheus" "$TMP_DIR/split-targets.curl.log" \ + || fail "Prometheus check should use ACTUATOR_BASE_URL" +if grep -Fq "https://public.example/actuator/" "$TMP_DIR/split-targets.curl.log"; then + fail "actuator checks must not use the public API target when ACTUATOR_BASE_URL is set" +fi +grep -Fq "GET https://public.example/api/v1/auth/me" "$TMP_DIR/split-targets.curl.log" \ + || fail "application API checks should continue using BASE_URL" + +status="$(run_smoke_at html-fallback https://public.example env SMOKE_ADMIN_CHECKS=false)" +[[ "$status" != "0" ]] || fail "HTML SPA fallback must not pass as actuator health" +grep -Fq "routing/target error: received text/html" "$TMP_DIR/html-fallback.out" \ + || fail "HTML fallback should produce an actionable routing/target error" + echo "smoke-test-admin-mode-test passed" From d7e8c51775f6352fbba4aae77b7691b279573027 Mon Sep 17 00:00:00 2001 From: wrj97 Date: Wed, 26 Aug 2026 10:23:43 +0800 Subject: [PATCH 08/15] fix(dashboard): remove encoding from slug params (#747) The router handles parameter encoding automatically. Explicitly encoding the slug results in double-encoded characters in the URL. Signed-off-by: wurongjie --- web/src/pages/dashboard.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/src/pages/dashboard.tsx b/web/src/pages/dashboard.tsx index c18e72c9..209aba28 100644 --- a/web/src/pages/dashboard.tsx +++ b/web/src/pages/dashboard.tsx @@ -144,7 +144,7 @@ export function DashboardPage() {
{skill.displayName}
From 243e9b68f4a25df21e5a53890fe6362e7e3c6f35 Mon Sep 17 00:00:00 2001 From: XiaoSeS <87064762+XiaoSeS@users.noreply.github.com> Date: Wed, 26 Aug 2026 15:06:27 +0800 Subject: [PATCH 09/15] fix(auth): prevent login page redirect loop Signed-off-by: XiaoSeS <87064762+XiaoSeS@users.noreply.github.com> --- web/src/app/layout.tsx | 1 - web/src/app/router.tsx | 4 +-- .../features/auth/use-auth-methods.test.ts | 26 +++++++++++-------- web/src/features/auth/use-auth-methods.ts | 14 +++++++--- web/src/pages/reset-password.tsx | 2 +- web/src/pages/settings/security.tsx | 2 +- 6 files changed, 30 insertions(+), 19 deletions(-) diff --git a/web/src/app/layout.tsx b/web/src/app/layout.tsx index dc5a3b80..7a52720b 100644 --- a/web/src/app/layout.tsx +++ b/web/src/app/layout.tsx @@ -123,7 +123,6 @@ export function Layout() { ) : ( {t('nav.login')} diff --git a/web/src/app/router.tsx b/web/src/app/router.tsx index bb1aac30..71d91116 100644 --- a/web/src/app/router.tsx +++ b/web/src/app/router.tsx @@ -183,8 +183,8 @@ const skillsRoute = createRoute({ const loginRoute = createRoute({ getParentRoute: () => rootRoute, path: 'login', - validateSearch: (search: Record): { returnTo: string; reason?: string } => ({ - returnTo: typeof search.returnTo === 'string' ? search.returnTo : '', + validateSearch: (search: Record): { returnTo?: string; reason?: string } => ({ + returnTo: typeof search.returnTo === 'string' && search.returnTo ? search.returnTo : undefined, reason: typeof search.reason === 'string' ? search.reason : undefined, }), component: LoginPage, diff --git a/web/src/features/auth/use-auth-methods.test.ts b/web/src/features/auth/use-auth-methods.test.ts index 36fdad2e..f353b95c 100644 --- a/web/src/features/auth/use-auth-methods.test.ts +++ b/web/src/features/auth/use-auth-methods.test.ts @@ -1,15 +1,19 @@ import { describe, expect, it } from 'vitest' -import * as authMethods from './use-auth-methods' +import { getAuthMethodsQueryOptions, useAuthMethods } from './use-auth-methods' -/** - * use-auth-methods is a thin useQuery wrapper around authApi.getMethods. - * The query key includes the returnTo parameter for proper cache isolation. - * There are no exported pure functions or data transformations to unit-test. - * - * This file verifies the public API surface so that accidental export removals are caught. - */ -describe('use-auth-methods module exports', () => { - it('exports useAuthMethods hook', () => { - expect(authMethods.useAuthMethods).toBeTypeOf('function') +describe('getAuthMethodsQueryOptions', () => { + it('keeps login auth-method lookup local to the page and bypasses the global 401 redirect', () => { + const options = getAuthMethodsQueryOptions('/dashboard') + + expect(options.queryKey).toEqual(['auth', 'methods', '/dashboard']) + expect(options.retry).toBe(false) + expect(options.meta).toEqual({ skipGlobalErrorHandler: true }) + expect(options.queryFn).toBeTypeOf('function') + }) +}) + +describe('use-auth-methods module exports', () => { + it('exports useAuthMethods hook', () => { + expect(useAuthMethods).toBeTypeOf('function') }) }) diff --git a/web/src/features/auth/use-auth-methods.ts b/web/src/features/auth/use-auth-methods.ts index 864d054a..cfe1c907 100644 --- a/web/src/features/auth/use-auth-methods.ts +++ b/web/src/features/auth/use-auth-methods.ts @@ -5,9 +5,17 @@ import type { AuthMethod } from '@/api/types' /** * Loads the backend-advertised authentication methods for the current entry point. */ -export function useAuthMethods(returnTo?: string) { - return useQuery({ +export function getAuthMethodsQueryOptions(returnTo?: string) { + return { queryKey: ['auth', 'methods', returnTo ?? ''], queryFn: () => authApi.getMethods(returnTo), - }) + retry: false, + meta: { + skipGlobalErrorHandler: true, + }, + } +} + +export function useAuthMethods(returnTo?: string) { + return useQuery(getAuthMethodsQueryOptions(returnTo)) } diff --git a/web/src/pages/reset-password.tsx b/web/src/pages/reset-password.tsx index ef4777de..229bf7bd 100644 --- a/web/src/pages/reset-password.tsx +++ b/web/src/pages/reset-password.tsx @@ -100,7 +100,7 @@ export function ResetPasswordPage() {

{t('resetPassword.successMessage')}

- + {t('resetPassword.backToLogin')} diff --git a/web/src/pages/settings/security.tsx b/web/src/pages/settings/security.tsx index 3726ad7d..8e4dc005 100644 --- a/web/src/pages/settings/security.tsx +++ b/web/src/pages/settings/security.tsx @@ -72,7 +72,7 @@ export function SecuritySettingsPage() { clearSessionScopedQueries(queryClient) queryClient.setQueryData(['auth', 'me'], null) } - await navigate({ to: '/login', search: { returnTo: '' } }) + await navigate({ to: '/login' }) } catch (error) { if (error instanceof ApiError && error.status === 401) { setErrorMessage(t('security.invalidCurrentPassword')) From 3b5d4381a979f96e5a95e7503b73f9172e78013f Mon Sep 17 00:00:00 2001 From: FenjuFu <92919259+FenjuFu@users.noreply.github.com> Date: Wed, 26 Aug 2026 17:37:21 +0800 Subject: [PATCH 10/15] feat(publish): allow uploading a skill folder directly Fix E2E ZIP picker selector after adding folder upload input. --- web/e2e/publish-flow-ui.spec.ts | 4 +- web/src/features/publish/folder-zip.test.ts | 89 +++++++++ web/src/features/publish/folder-zip.ts | 189 ++++++++++++++++++++ web/src/features/publish/upload-zone.tsx | 119 ++++++++---- web/src/i18n/locales/en.json | 6 +- web/src/i18n/locales/zh.json | 6 +- web/src/pages/dashboard/publish.tsx | 17 +- 7 files changed, 387 insertions(+), 43 deletions(-) create mode 100644 web/src/features/publish/folder-zip.test.ts create mode 100644 web/src/features/publish/folder-zip.ts diff --git a/web/e2e/publish-flow-ui.spec.ts b/web/e2e/publish-flow-ui.spec.ts index 867c8248..6cb0cf40 100644 --- a/web/e2e/publish-flow-ui.spec.ts +++ b/web/e2e/publish-flow-ui.spec.ts @@ -45,7 +45,9 @@ test.describe('Publish Flow UI (Real API)', () => { }) await expect(namespaceTrigger).toContainText(`@${namespace.slug}`) - await page.locator('input[type="file"]').setInputFiles(packagePath) + // The publish form also exposes a second file input for folder selection. + // Scope this regression test to the existing ZIP picker. + await page.locator('input[type="file"][accept*=".zip"]').setInputFiles(packagePath) await expect(page.getByText(path.basename(packagePath))).toBeVisible() const confirmButton = page.getByRole('button', { name: 'Confirm Publish' }) await expect(confirmButton).toBeEnabled() diff --git a/web/src/features/publish/folder-zip.test.ts b/web/src/features/publish/folder-zip.test.ts new file mode 100644 index 00000000..3568c806 --- /dev/null +++ b/web/src/features/publish/folder-zip.test.ts @@ -0,0 +1,89 @@ +import { describe, expect, it } from 'vitest' +import { + collectFolderEntries, + createZipBlob, + crc32, + isIgnoredPath, + packageFolderAsZip, +} from './folder-zip' + +const utf8 = new TextEncoder() + +function fileAt(relativePath: string, content = 'x'): File { + const name = relativePath.split('/').pop() || relativePath + const file = new File([content], name) + Object.defineProperty(file, 'webkitRelativePath', { value: relativePath }) + return file +} + +async function bytesOf(blob: Blob): Promise { + return new Uint8Array(await blob.arrayBuffer()) +} + +describe('isIgnoredPath', () => { + it('keeps normal skill files', () => { + expect(isIgnoredPath('my-skill/SKILL.md')).toBe(false) + expect(isIgnoredPath('my-skill/scripts/run.sh')).toBe(false) + }) + + it('drops VCS, build and OS junk', () => { + expect(isIgnoredPath('my-skill/.git/config')).toBe(true) + expect(isIgnoredPath('my-skill/node_modules/x/index.js')).toBe(true) + expect(isIgnoredPath('my-skill/__pycache__/m.pyc')).toBe(true) + expect(isIgnoredPath('my-skill/.DS_Store')).toBe(true) + expect(isIgnoredPath('my-skill/._resource')).toBe(true) + expect(isIgnoredPath('my-skill/Thumbs.db')).toBe(true) + }) +}) + +describe('crc32', () => { + it('matches known CRC-32/ISO-HDLC vectors', () => { + expect(crc32(utf8.encode(''))).toBe(0x00000000) + expect(crc32(utf8.encode('a'))).toBe(0xe8b7be43) + expect(crc32(utf8.encode('abc'))).toBe(0x352441c2) + }) +}) + +describe('createZipBlob', () => { + it('writes a STORE archive with local, central and EOCD records', async () => { + const blob = createZipBlob([{ path: 'SKILL.md', data: utf8.encode('hello') }]) + const bytes = await bytesOf(blob) + const view = new DataView(bytes.buffer) + + // Local file header signature at offset 0. + expect(view.getUint32(0, true)).toBe(0x04034b50) + // Contains a central directory header and an end-of-central-directory record. + const eocd = bytes.length - 22 + expect(view.getUint32(eocd, true)).toBe(0x06054b50) + expect(view.getUint16(eocd + 10, true)).toBe(1) // total entries + // Central dir offset points at a central directory header signature. + const cdOffset = view.getUint32(eocd + 16, true) + expect(view.getUint32(cdOffset, true)).toBe(0x02014b50) + }) +}) + +describe('collectFolderEntries', () => { + it('filters junk and sorts remaining files by path', async () => { + const entries = await collectFolderEntries([ + fileAt('my-skill/scripts/run.sh', 'run'), + fileAt('my-skill/.git/config', 'gitcfg'), + fileAt('my-skill/SKILL.md', 'md'), + ]) + expect(entries.map((e) => e.path)).toEqual(['my-skill/SKILL.md', 'my-skill/scripts/run.sh']) + }) +}) + +describe('packageFolderAsZip', () => { + it('names the zip after the top-level folder', async () => { + const file = await packageFolderAsZip([fileAt('my-skill/SKILL.md', 'md')]) + expect(file.name).toBe('my-skill.zip') + expect(file.type).toBe('application/zip') + expect(file.size).toBeGreaterThan(0) + }) + + it('throws when everything was filtered out', async () => { + await expect(packageFolderAsZip([fileAt('my-skill/.git/config', 'x')])).rejects.toThrow( + 'empty-folder' + ) + }) +}) diff --git a/web/src/features/publish/folder-zip.ts b/web/src/features/publish/folder-zip.ts new file mode 100644 index 00000000..f10bcfa6 --- /dev/null +++ b/web/src/features/publish/folder-zip.ts @@ -0,0 +1,189 @@ +/** + * Dependency-free packaging of a selected folder into a skill ZIP. + * + * Browsers expose a picked folder as a flat FileList (each File carries a + * `webkitRelativePath` like `my-skill/SKILL.md`). We build a STORE-method ZIP + * (no compression — skill packages are small text files, and STORE keeps this + * dependency-free) from those files so the result flows through the exact same + * upload/publish path as a hand-made ZIP. + * + * We drop VCS/build/OS junk that a real on-disk folder almost always contains + * (`.git/`, `node_modules/`, `.DS_Store`, …) so it never bloats the package or + * the file-count limit. The server additionally strips a single root directory + * and OS-metadata entries, so paths are kept as-is (`my-skill/SKILL.md`). + */ + +/** Directory names whose entire subtree is excluded from the package. */ +const IGNORED_DIR_SEGMENTS = new Set([ + '.git', + '.svn', + '.hg', + 'node_modules', + '__pycache__', + '__MACOSX', +]) + +/** Exact file names that are always excluded. */ +const IGNORED_FILE_NAMES = new Set(['.DS_Store', 'Thumbs.db', 'desktop.ini']) + +/** Returns true if a relative path should be excluded from the package. */ +export function isIgnoredPath(relativePath: string): boolean { + const parts = relativePath.split('/') + const name = parts[parts.length - 1] + if (!name) return true // trailing slash / directory marker + if (parts.some((segment) => IGNORED_DIR_SEGMENTS.has(segment))) return true + if (IGNORED_FILE_NAMES.has(name)) return true + if (name.startsWith('._')) return true + if (name.endsWith('.pyc') || name.endsWith('.swp')) return true + return false +} + +// --- CRC-32 (IEEE 802.3, polynomial 0xEDB88320) ------------------------------- + +const CRC_TABLE = (() => { + const table = new Uint32Array(256) + for (let n = 0; n < 256; n++) { + let c = n + for (let k = 0; k < 8; k++) { + c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1 + } + table[n] = c >>> 0 + } + return table +})() + +export function crc32(bytes: Uint8Array): number { + let crc = 0xffffffff + for (let i = 0; i < bytes.length; i++) { + crc = CRC_TABLE[(crc ^ bytes[i]) & 0xff] ^ (crc >>> 8) + } + return (crc ^ 0xffffffff) >>> 0 +} + +// --- ZIP writer (STORE method, no data descriptors) --------------------------- + +export interface ZipEntry { + path: string + data: Uint8Array +} + +const utf8 = new TextEncoder() + +/** + * Builds a ZIP archive (STORE method) containing the given entries and returns + * it as a Blob. 32-bit size fields are used; skill packages are far below the + * 4 GB boundary where ZIP64 would be required. + */ +export function createZipBlob(entries: ZipEntry[]): Blob { + const localParts: Uint8Array[] = [] + const centralParts: Uint8Array[] = [] + let offset = 0 + + for (const entry of entries) { + const nameBytes = utf8.encode(entry.path) + const crc = crc32(entry.data) + const size = entry.data.length + + const local = new Uint8Array(30 + nameBytes.length) + const lv = new DataView(local.buffer) + lv.setUint32(0, 0x04034b50, true) // local file header signature + lv.setUint16(4, 20, true) // version needed + lv.setUint16(6, 0x0800, true) // flags: bit 11 = UTF-8 names + lv.setUint16(8, 0, true) // method: STORE + lv.setUint16(10, 0, true) // mod time + lv.setUint16(12, 0, true) // mod date + lv.setUint32(14, crc, true) + lv.setUint32(18, size, true) // compressed size (== uncompressed for STORE) + lv.setUint32(22, size, true) // uncompressed size + lv.setUint16(26, nameBytes.length, true) + lv.setUint16(28, 0, true) // extra length + local.set(nameBytes, 30) + + localParts.push(local, entry.data) + + const central = new Uint8Array(46 + nameBytes.length) + const cv = new DataView(central.buffer) + cv.setUint32(0, 0x02014b50, true) // central directory header signature + cv.setUint16(4, 20, true) // version made by + cv.setUint16(6, 20, true) // version needed + cv.setUint16(8, 0x0800, true) // flags + cv.setUint16(10, 0, true) // method: STORE + cv.setUint16(12, 0, true) // mod time + cv.setUint16(14, 0, true) // mod date + cv.setUint32(16, crc, true) + cv.setUint32(20, size, true) + cv.setUint32(24, size, true) + cv.setUint16(28, nameBytes.length, true) + cv.setUint16(30, 0, true) // extra length + cv.setUint16(32, 0, true) // comment length + cv.setUint16(34, 0, true) // disk number start + cv.setUint16(36, 0, true) // internal attrs + cv.setUint32(38, 0, true) // external attrs + cv.setUint32(42, offset, true) // relative offset of local header + central.set(nameBytes, 46) + centralParts.push(central) + + offset += local.length + entry.data.length + } + + const centralSize = centralParts.reduce((n, p) => n + p.length, 0) + const eocd = new Uint8Array(22) + const ev = new DataView(eocd.buffer) + ev.setUint32(0, 0x06054b50, true) // end of central directory signature + ev.setUint16(4, 0, true) // disk number + ev.setUint16(6, 0, true) // central dir start disk + ev.setUint16(8, entries.length, true) // entries on this disk + ev.setUint16(10, entries.length, true) // total entries + ev.setUint32(12, centralSize, true) // central dir size + ev.setUint32(16, offset, true) // central dir offset + ev.setUint16(20, 0, true) // comment length + + // Concatenate into a single buffer so the Blob part is a Uint8Array. + const parts = [...localParts, ...centralParts, eocd] + const total = parts.reduce((n, p) => n + p.length, 0) + const out = new Uint8Array(total) + let pos = 0 + for (const part of parts) { + out.set(part, pos) + pos += part.length + } + return new Blob([out], { type: 'application/zip' }) +} + +// --- Folder -> File ------------------------------------------------------------ + +/** Reads the picked folder's files into ZIP entries, skipping junk. */ +export async function collectFolderEntries(files: File[]): Promise { + const entries: ZipEntry[] = [] + for (const file of files) { + const path = (file as File & { webkitRelativePath?: string }).webkitRelativePath || file.name + if (isIgnoredPath(path)) continue + const data = new Uint8Array(await file.arrayBuffer()) + entries.push({ path, data }) + } + entries.sort((a, b) => (a.path < b.path ? -1 : a.path > b.path ? 1 : 0)) + return entries +} + +/** Top-level folder name of a webkitdirectory selection, for naming the zip. */ +function rootFolderName(files: File[]): string { + for (const file of files) { + const rel = (file as File & { webkitRelativePath?: string }).webkitRelativePath + if (rel && rel.includes('/')) return rel.slice(0, rel.indexOf('/')) + } + return 'skill' +} + +/** + * Packages a picked folder into a `.zip` File ready for the existing + * upload flow. Throws if every file was filtered out as junk. + */ +export async function packageFolderAsZip(fileList: FileList | File[]): Promise { + const files = Array.from(fileList) + const entries = await collectFolderEntries(files) + if (entries.length === 0) { + throw new Error('empty-folder') + } + const blob = createZipBlob(entries) + return new File([blob], `${rootFolderName(files)}.zip`, { type: 'application/zip' }) +} diff --git a/web/src/features/publish/upload-zone.tsx b/web/src/features/publish/upload-zone.tsx index b28fa92b..e5ab4c72 100644 --- a/web/src/features/publish/upload-zone.tsx +++ b/web/src/features/publish/upload-zone.tsx @@ -1,10 +1,12 @@ -import { useCallback } from 'react' +import { useCallback, useEffect, useRef, type ChangeEvent } from 'react' import { useTranslation } from 'react-i18next' import { useDropzone } from 'react-dropzone' import { cn } from '@/shared/lib/utils' interface UploadZoneProps { onFileSelect: (file: File) => void + /** Optional: called with the raw files of a picked folder (webkitdirectory). */ + onFolderSelect?: (files: File[]) => void disabled?: boolean } @@ -13,8 +15,20 @@ interface UploadZoneProps { * The component is intentionally stateless so packaging validation can remain in * the publish flow that knows the surrounding form and backend constraints. */ -export function UploadZone({ onFileSelect, disabled }: UploadZoneProps) { +export function UploadZone({ onFileSelect, onFolderSelect, disabled }: UploadZoneProps) { const { t } = useTranslation() + const folderInputRef = useRef(null) + + // `webkitdirectory` / `directory` are not in React's input attribute types; + // set them imperatively so the folder picker works without an untyped cast. + useEffect(() => { + const el = folderInputRef.current + if (el) { + el.setAttribute('webkitdirectory', '') + el.setAttribute('directory', '') + } + }, []) + const onDrop = useCallback( (acceptedFiles: File[]) => { if (acceptedFiles.length > 0) { @@ -33,44 +47,75 @@ export function UploadZone({ onFileSelect, disabled }: UploadZoneProps) { disabled, }) + const handleFolderChange = (event: ChangeEvent) => { + const files = event.target.files + if (files && files.length > 0) { + onFolderSelect?.(Array.from(files)) + } + // Reset so picking the same folder again re-triggers change. + event.target.value = '' + } + return ( -
- -
-
- - - -
- {isDragActive ? ( -

{t('upload.dropHint')}

- ) : ( - <> -

{t('upload.dragHint')}

-

{t('upload.formatHint')}

- +
+
+ +
+
+ + + +
+ {isDragActive ? ( +

{t('upload.dropHint')}

+ ) : ( + <> +

{t('upload.dragHint')}

+

{t('upload.formatHint')}

+ + )} +
+ {onFolderSelect && ( +
+ + +
+ )}
) } diff --git a/web/src/i18n/locales/en.json b/web/src/i18n/locales/en.json index dcfc28bf..f22f7cf9 100644 --- a/web/src/i18n/locales/en.json +++ b/web/src/i18n/locales/en.json @@ -1212,7 +1212,8 @@ "upload": { "dropHint": "Drop to upload...", "dragHint": "Drag a ZIP file here, or click to select", - "formatHint": "Only .zip format supported" + "formatHint": "Only .zip format supported", + "folderHint": "Or select a folder to package and upload" }, "layout": { "footerDescription": "Skill registry, providing efficient skill management and distribution for developers." @@ -1389,7 +1390,8 @@ "warningConfirmCancel": "Go back and fix", "frontmatterFailedTitle": "SKILL.md format is invalid", "frontmatterFailedDescription": "Please check the YAML frontmatter at the top of SKILL.md. If a field value contains a colon, wrap it in quotes.", - "selectRequired": "Please select namespace and file" + "selectRequired": "Please select namespace and file", + "folderPackagingFailed": "Could not package the selected folder. Make sure it contains files." }, "toast": { "success": "Success", diff --git a/web/src/i18n/locales/zh.json b/web/src/i18n/locales/zh.json index e2846cae..f858ce9a 100644 --- a/web/src/i18n/locales/zh.json +++ b/web/src/i18n/locales/zh.json @@ -1212,7 +1212,8 @@ "upload": { "dropHint": "放开以上传文件...", "dragHint": "拖拽 ZIP 文件到此处,或点击选择", - "formatHint": "仅支持 .zip 格式" + "formatHint": "仅支持 .zip 格式", + "folderHint": "或选择文件夹,自动打包上传" }, "layout": { "footerDescription": "技能注册中心,为开发者提供高效的技能管理和分发平台。" @@ -1389,7 +1390,8 @@ "warningConfirmCancel": "返回修改", "frontmatterFailedTitle": "SKILL.md 格式有误", "frontmatterFailedDescription": "请检查 SKILL.md 顶部 frontmatter 的 YAML 格式。若字段值中包含冒号,请用引号包裹。", - "selectRequired": "请选择命名空间和文件" + "selectRequired": "请选择命名空间和文件", + "folderPackagingFailed": "无法打包所选文件夹,请确认其中包含文件。" }, "toast": { "success": "成功", diff --git a/web/src/pages/dashboard/publish.tsx b/web/src/pages/dashboard/publish.tsx index bcaeb16a..23770c85 100644 --- a/web/src/pages/dashboard/publish.tsx +++ b/web/src/pages/dashboard/publish.tsx @@ -2,6 +2,7 @@ import { useEffect, useState } from 'react' import { useNavigate, useSearch } from '@tanstack/react-router' import { useTranslation } from 'react-i18next' import { UploadZone } from '@/features/publish/upload-zone' +import { packageFolderAsZip } from '@/features/publish/folder-zip' import { extractPrecheckWarnings, isFrontmatterFailureMessage, @@ -41,6 +42,7 @@ export function PublishPage() { const [visibility, setVisibility] = useState(prefill.visibility) const [warningDialogOpen, setWarningDialogOpen] = useState(false) const [precheckWarnings, setPrecheckWarnings] = useState([]) + const [isPackaging, setIsPackaging] = useState(false) const { data: namespaces, isLoading: isLoadingNamespaces } = useMyNamespaces() const publishMutation = usePublishSkill() @@ -66,6 +68,18 @@ export function PublishPage() { setWarningDialogOpen(false) } + const handleFolderSelect = async (files: File[]) => { + setIsPackaging(true) + try { + const zip = await packageFolderAsZip(files) + handleFileSelect(zip) + } catch { + toast.error(t('publish.folderPackagingFailed')) + } finally { + setIsPackaging(false) + } + } + const publishSkill = async (confirmWarnings = false) => { if (!selectedFile || !namespaceSlug) { toast.error(t('publish.selectRequired')) @@ -201,7 +215,8 @@ export function PublishPage() { {selectedFile && (
From 95e630c09674b759392ae6b409b3cb6a2ec17d58 Mon Sep 17 00:00:00 2001 From: bbdu3 Date: Wed, 26 Aug 2026 17:46:16 +0800 Subject: [PATCH 11/15] fix(scan): prevent scan tasks from being lost after transaction commit Preserve scan delivery on lock contention through the retry path and add reclaim regression coverage. --- .../skillhub/stream/ScanTaskConsumer.java | 54 ++++++- .../task/ScanTaskOutboxDispatcher.java | 109 ++++++++++++++ .../db/migration/V44__scan_task_outbox.sql | 28 ++++ .../V45__scan_task_outbox_metadata.sql | 2 + .../stream/ScanTaskConsumerLoggingTest.java | 13 +- .../skillhub/stream/ScanTaskConsumerTest.java | 123 ++++++++++++++- .../task/ScanTaskOutboxDispatcherTest.java | 140 ++++++++++++++++++ .../domain/security/ScanTaskOutbox.java | 131 ++++++++++++++++ .../security/ScanTaskOutboxRepository.java | 11 ++ .../domain/security/ScanTaskOutboxStatus.java | 8 + .../domain/security/SecurityAudit.java | 12 ++ .../security/SecurityAuditRepository.java | 2 + .../domain/security/SecurityScanService.java | 37 ++++- .../domain/security/package-info.java | 2 + .../domain/security/ScanTaskOutboxTest.java | 56 +++++++ .../security/SecurityScanOutboxTest.java | 59 ++++++++ .../jpa/ScanTaskOutboxJpaRepository.java | 34 +++++ 17 files changed, 809 insertions(+), 12 deletions(-) create mode 100644 server/skillhub-app/src/main/java/com/iflytek/skillhub/task/ScanTaskOutboxDispatcher.java create mode 100644 server/skillhub-app/src/main/resources/db/migration/V44__scan_task_outbox.sql create mode 100644 server/skillhub-app/src/main/resources/db/migration/V45__scan_task_outbox_metadata.sql create mode 100644 server/skillhub-app/src/test/java/com/iflytek/skillhub/task/ScanTaskOutboxDispatcherTest.java create mode 100644 server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/security/ScanTaskOutbox.java create mode 100644 server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/security/ScanTaskOutboxRepository.java create mode 100644 server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/security/ScanTaskOutboxStatus.java create mode 100644 server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/security/package-info.java create mode 100644 server/skillhub-domain/src/test/java/com/iflytek/skillhub/domain/security/ScanTaskOutboxTest.java create mode 100644 server/skillhub-domain/src/test/java/com/iflytek/skillhub/domain/security/SecurityScanOutboxTest.java create mode 100644 server/skillhub-infra/src/main/java/com/iflytek/skillhub/infra/jpa/ScanTaskOutboxJpaRepository.java diff --git a/server/skillhub-app/src/main/java/com/iflytek/skillhub/stream/ScanTaskConsumer.java b/server/skillhub-app/src/main/java/com/iflytek/skillhub/stream/ScanTaskConsumer.java index fce9bcc2..3b1b2c66 100644 --- a/server/skillhub-app/src/main/java/com/iflytek/skillhub/stream/ScanTaskConsumer.java +++ b/server/skillhub-app/src/main/java/com/iflytek/skillhub/stream/ScanTaskConsumer.java @@ -11,6 +11,7 @@ import com.iflytek.skillhub.domain.skill.SkillVersionRepository; import com.iflytek.skillhub.domain.skill.SkillVersionStatus; import com.iflytek.skillhub.observability.MessageObservationSupport; import com.iflytek.skillhub.storage.ObjectStorageService; +import org.redisson.api.RLock; import org.redisson.api.RedissonClient; import java.io.IOException; @@ -26,6 +27,7 @@ import java.util.Map; public class ScanTaskConsumer extends AbstractStreamConsumer { private static final Path SCAN_TEMP_DIR = Paths.get("/tmp/skillhub-scans").toAbsolutePath().normalize(); + private final RedissonClient redissonClient; private final SecurityScanner securityScanner; private final SecurityScanService securityScanService; private final SkillVersionRepository skillVersionRepository; @@ -42,6 +44,7 @@ public class ScanTaskConsumer extends AbstractStreamConsumer= maxAttempts) { + outbox.markFailed(now, error.toString()); + repository.save(outbox); + versionRepository.findById(outbox.getVersionId()) + .filter(version -> version.getStatus() == SkillVersionStatus.SCANNING) + .ifPresent(version -> { + version.setStatus(SkillVersionStatus.SCAN_FAILED); + versionRepository.save(version); + }); + log.error("Scan task publish failed permanently: taskId={}, versionId={}, attempts={}", + outbox.getTaskId(), outbox.getVersionId(), outbox.getRetryCount(), error); + return; + } + Duration delay = retryDelay(nextAttempt); + outbox.markRetry(now, delay, error.toString()); + repository.save(outbox); + log.warn("Failed to publish scan task; will retry taskId={}, retryCount={}, nextDelay={}", + outbox.getTaskId(), outbox.getRetryCount(), delay, error); + } + + @Scheduled(cron = "0 20 2 * * ?") + @Transactional + public void cleanupSent() { + int deleted = repository.deleteSentBefore(Instant.now(clock).minus(Duration.ofDays(7))); + if (deleted > 0) { + log.info("Cleaned up {} sent scan outbox records", deleted); + } + } + + private Duration retryDelay(int retryCount) { + long seconds = Math.min(maxBackoff.toSeconds(), 1L << Math.min(retryCount, 16)); + return Duration.ofSeconds(Math.max(seconds, 1)); + } +} diff --git a/server/skillhub-app/src/main/resources/db/migration/V44__scan_task_outbox.sql b/server/skillhub-app/src/main/resources/db/migration/V44__scan_task_outbox.sql new file mode 100644 index 00000000..2b56fb36 --- /dev/null +++ b/server/skillhub-app/src/main/resources/db/migration/V44__scan_task_outbox.sql @@ -0,0 +1,28 @@ +CREATE TABLE scan_task_outbox ( + id BIGSERIAL PRIMARY KEY, + task_id VARCHAR(100) NOT NULL, + version_id BIGINT NOT NULL, + skill_path VARCHAR(1000), + bundle_key VARCHAR(1000), + publisher_id VARCHAR(255), + status VARCHAR(20) NOT NULL, + retry_count INTEGER NOT NULL DEFAULT 0, + next_attempt_at TIMESTAMPTZ NOT NULL, + lease_until TIMESTAMPTZ, + last_error VARCHAR(2000), + created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, + entity_version BIGINT NOT NULL DEFAULT 0, + CONSTRAINT uk_scan_task_outbox_task_id UNIQUE (task_id), + CONSTRAINT ck_scan_task_outbox_status CHECK (status IN ('PENDING', 'SENDING', 'SENT', 'FAILED')) +); + +CREATE INDEX idx_scan_task_outbox_pending + ON scan_task_outbox (status, next_attempt_at, created_at); +CREATE INDEX idx_scan_task_outbox_lease + ON scan_task_outbox (status, lease_until); +CREATE INDEX idx_scan_task_outbox_version + ON scan_task_outbox (version_id); + +ALTER TABLE security_audit ADD COLUMN task_id VARCHAR(100); +CREATE INDEX idx_security_audit_task_id ON security_audit (task_id); \ No newline at end of file diff --git a/server/skillhub-app/src/main/resources/db/migration/V45__scan_task_outbox_metadata.sql b/server/skillhub-app/src/main/resources/db/migration/V45__scan_task_outbox_metadata.sql new file mode 100644 index 00000000..b07bb8e8 --- /dev/null +++ b/server/skillhub-app/src/main/resources/db/migration/V45__scan_task_outbox_metadata.sql @@ -0,0 +1,2 @@ +ALTER TABLE scan_task_outbox + ADD COLUMN metadata JSONB NOT NULL DEFAULT '{}'::jsonb; diff --git a/server/skillhub-app/src/test/java/com/iflytek/skillhub/stream/ScanTaskConsumerLoggingTest.java b/server/skillhub-app/src/test/java/com/iflytek/skillhub/stream/ScanTaskConsumerLoggingTest.java index 8f473b65..50e73da8 100644 --- a/server/skillhub-app/src/test/java/com/iflytek/skillhub/stream/ScanTaskConsumerLoggingTest.java +++ b/server/skillhub-app/src/test/java/com/iflytek/skillhub/stream/ScanTaskConsumerLoggingTest.java @@ -20,6 +20,7 @@ import com.iflytek.skillhub.storage.ObjectStorageService; import io.micrometer.observation.ObservationRegistry; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Test; +import org.redisson.api.RLock; import org.redisson.api.RStream; import org.redisson.api.RedissonClient; import org.redisson.api.StreamMessageId; @@ -35,6 +36,7 @@ import java.util.Optional; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; class ScanTaskConsumerLoggingTest { @@ -150,6 +152,15 @@ class ScanTaskConsumerLoggingTest { } } + private static RedissonClient redissonClientWithAvailableProcessingLock() { + RedissonClient redissonClient = mock(RedissonClient.class); + RLock processingLock = mock(RLock.class); + when(redissonClient.getLock(org.mockito.ArgumentMatchers.anyString())).thenReturn(processingLock); + when(processingLock.tryLock()).thenReturn(true); + when(processingLock.isHeldByCurrentThread()).thenReturn(true); + return redissonClient; + } + private static final class TestableLoggingConsumer extends ScanTaskConsumer { private final RStream stream = mock(RStream.class); @@ -159,7 +170,7 @@ class ScanTaskConsumerLoggingTest { ScanTaskProducer scanTaskProducer, ObjectStorageService objectStorageService) { super( - mock(RedissonClient.class), + redissonClientWithAvailableProcessingLock(), "skillhub:scan:requests", "skillhub-scanners", securityScanner, diff --git a/server/skillhub-app/src/test/java/com/iflytek/skillhub/stream/ScanTaskConsumerTest.java b/server/skillhub-app/src/test/java/com/iflytek/skillhub/stream/ScanTaskConsumerTest.java index 7f8cabaa..07d7d9d6 100644 --- a/server/skillhub-app/src/test/java/com/iflytek/skillhub/stream/ScanTaskConsumerTest.java +++ b/server/skillhub-app/src/test/java/com/iflytek/skillhub/stream/ScanTaskConsumerTest.java @@ -20,6 +20,7 @@ import com.iflytek.skillhub.storage.ObjectStorageService; import com.iflytek.skillhub.storage.ObjectMetadata; import io.micrometer.observation.ObservationRegistry; import org.junit.jupiter.api.Test; +import org.redisson.api.RLock; import org.redisson.api.RStream; import org.redisson.api.RedissonClient; import org.redisson.api.StreamMessageId; @@ -38,7 +39,11 @@ import java.util.Map; import java.util.Optional; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; class ScanTaskConsumerTest { private static final Path SCAN_TEMP_DIR = Path.of("/tmp/skillhub-scans"); @@ -268,6 +273,88 @@ class ScanTaskConsumerTest { assertThat(listScanTempFiles(versionId)).isEmpty(); } + @Test + void processBusiness_whenTaskIsAlreadyInFlight_skipsScanAndPreservesSharedTempPath() throws Exception { + Files.createDirectories(SCAN_TEMP_DIR); + Path tempDir = Files.createTempDirectory(SCAN_TEMP_DIR, "scan-task-consumer-inflight"); + Path skillFile = Files.writeString(tempDir.resolve("SKILL.md"), "# demo"); + StubSecurityScanner securityScanner = new StubSecurityScanner(); + RLock processingLock = mock(RLock.class); + when(processingLock.tryLock()).thenReturn(false); + TestableScanTaskConsumer consumer = new TestableScanTaskConsumer( + securityScanner, + new StubSecurityScanService(), + new InMemorySkillVersionRepository(), + new InMemoryScanTaskProducer(), + new InMemoryObjectStorageService(), + redissonClient(processingLock) + ); + ScanTaskConsumer.ScanTaskPayload payload = new ScanTaskConsumer.ScanTaskPayload( + "task-inflight", 42L, tempDir.toString(), null, ScannerType.SKILL_SCANNER); + + try { + assertThatThrownBy(() -> consumer.invokeProcessBusiness(payload)) + .isInstanceOf(RuntimeException.class) + .hasMessage("Security scan is already in progress: taskId=task-inflight"); + + assertThat(securityScanner.lastRequest).isNull(); + assertThat(skillFile).exists(); + verify(processingLock, never()).unlock(); + } finally { + Files.deleteIfExists(skillFile); + Files.deleteIfExists(tempDir); + } + } + + @Test + void handleMessage_whenTaskLockIsHeld_republishesInsteadOfDroppingDelivery() { + StubSecurityScanner securityScanner = new StubSecurityScanner(); + InMemoryScanTaskProducer producer = new InMemoryScanTaskProducer(); + RLock processingLock = mock(RLock.class); + when(processingLock.tryLock()).thenReturn(false); + TestableScanTaskConsumer consumer = new TestableScanTaskConsumer( + securityScanner, + new StubSecurityScanService(), + new InMemorySkillVersionRepository(), + producer, + new InMemoryObjectStorageService(), + redissonClient(processingLock) + ); + + consumer.handleMessage(new StreamMessageId(11, 0), Map.of( + "taskId", "task-reclaimed", + "versionId", "42", + "skillPath", "/tmp/skillhub-scans/42", + "scannerType", ScannerType.SKILL_SCANNER.getValue() + )); + + assertThat(producer.publishedTask.taskId()).isEqualTo("task-reclaimed"); + assertThat(producer.publishedTask.metadata()).containsEntry("retryCount", "1"); + verify(consumer.stream).ack("skillhub-scanners", new StreamMessageId(11, 0)); + } + + @Test + void processBusiness_whenScannerFails_releasesProcessingLock() { + StubSecurityScanner securityScanner = new StubSecurityScanner(); + securityScanner.failure = new IllegalStateException("scanner unavailable"); + RLock processingLock = availableProcessingLock(); + TestableScanTaskConsumer consumer = new TestableScanTaskConsumer( + securityScanner, + new StubSecurityScanService(), + new InMemorySkillVersionRepository(), + new InMemoryScanTaskProducer(), + new InMemoryObjectStorageService(), + redissonClient(processingLock) + ); + ScanTaskConsumer.ScanTaskPayload payload = new ScanTaskConsumer.ScanTaskPayload( + "task-failure", 42L, "/tmp/failure", null, ScannerType.SKILL_SCANNER); + + assertThatThrownBy(() -> consumer.invokeProcessBusiness(payload)) + .isInstanceOf(IllegalStateException.class) + .hasMessage("scanner unavailable"); + verify(processingLock).unlock(); + } + private void setField(Object target, String fieldName, Object value) throws Exception { Field field = target.getClass().getDeclaredField(fieldName); field.setAccessible(true); @@ -299,7 +386,28 @@ class ScanTaskConsumerTest { ScanTaskProducer scanTaskProducer, ObjectStorageService objectStorageService) { super( - mock(RedissonClient.class), + redissonClient(availableProcessingLock()), + "skillhub:scan:requests", + "skillhub-scanners", + securityScanner, + securityScanService, + skillVersionRepository, + scanTaskProducer, + objectStorageService, + new MessageObservationSupport(ObservationRegistry.NOOP, new RequestIdAccessor()) + ); + this.stream = mock(RStream.class); + } + + @SuppressWarnings("unchecked") + private TestableScanTaskConsumer(SecurityScanner securityScanner, + SecurityScanService securityScanService, + SkillVersionRepository skillVersionRepository, + ScanTaskProducer scanTaskProducer, + ObjectStorageService objectStorageService, + RedissonClient redissonClient) { + super( + redissonClient, "skillhub:scan:requests", "skillhub-scanners", securityScanner, @@ -334,6 +442,19 @@ class ScanTaskConsumerTest { } } + private static RLock availableProcessingLock() { + RLock processingLock = mock(RLock.class); + when(processingLock.tryLock()).thenReturn(true); + when(processingLock.isHeldByCurrentThread()).thenReturn(true); + return processingLock; + } + + private static RedissonClient redissonClient(RLock processingLock) { + RedissonClient redissonClient = mock(RedissonClient.class); + when(redissonClient.getLock(org.mockito.ArgumentMatchers.anyString())).thenReturn(processingLock); + return redissonClient; + } + private static final class StubSecurityScanner implements SecurityScanner { private SecurityScanRequest lastRequest; private SecurityScanResponse response; diff --git a/server/skillhub-app/src/test/java/com/iflytek/skillhub/task/ScanTaskOutboxDispatcherTest.java b/server/skillhub-app/src/test/java/com/iflytek/skillhub/task/ScanTaskOutboxDispatcherTest.java new file mode 100644 index 00000000..eafa3b6f --- /dev/null +++ b/server/skillhub-app/src/test/java/com/iflytek/skillhub/task/ScanTaskOutboxDispatcherTest.java @@ -0,0 +1,140 @@ +package com.iflytek.skillhub.task; + +import com.iflytek.skillhub.domain.security.ScanTask; +import com.iflytek.skillhub.domain.security.ScanTaskOutbox; +import com.iflytek.skillhub.domain.security.ScanTaskOutboxRepository; +import com.iflytek.skillhub.domain.security.ScanTaskOutboxStatus; +import com.iflytek.skillhub.domain.security.ScanTaskProducer; +import com.iflytek.skillhub.domain.security.ScannerType; +import com.iflytek.skillhub.domain.skill.SkillVersion; +import com.iflytek.skillhub.domain.skill.SkillVersionRepository; +import com.iflytek.skillhub.domain.skill.SkillVersionStatus; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.time.Clock; +import java.time.Duration; +import java.time.Instant; +import java.time.ZoneOffset; +import java.util.List; +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; + +@ExtendWith(MockitoExtension.class) +class ScanTaskOutboxDispatcherTest { + @Mock ScanTaskOutboxRepository repository; + @Mock ScanTaskProducer producer; + @Mock SkillVersionRepository versionRepository; + + @Test + void failedRedisPublishLeavesTaskPendingForRetry() { + ScanTaskOutbox outbox = outbox("task-1", 1L); + given(repository.findDispatchable(any(), any(Integer.class))).willReturn(List.of(outbox)); + doThrow(new IllegalStateException("redis unavailable")).when(producer).publishScanTask(any()); + + dispatcher(10).dispatch(); + + assertThat(outbox.getStatus()).isEqualTo(ScanTaskOutboxStatus.PENDING); + assertThat(outbox.getRetryCount()).isEqualTo(1); + verify(producer).publishScanTask(any()); + verify(repository).save(outbox); + } + + @Test + void successfulPublishMarksTaskSentWithoutChangingVersion() { + ScanTaskOutbox outbox = outbox("task-success", 3L); + given(repository.findDispatchable(any(), any(Integer.class))).willReturn(List.of(outbox)); + + dispatcher(10).dispatch(); + + assertThat(outbox.getStatus()).isEqualTo(ScanTaskOutboxStatus.SENT); + assertThat(outbox.getRetryCount()).isZero(); + verify(producer).publishScanTask(any()); + verify(repository).save(outbox); + verifyNoInteractions(versionRepository); + } + + @Test + void lastPublishAttemptMarksOutboxAndVersionFailed() { + ScanTaskOutbox outbox = outbox("task-2", 2L); + SkillVersion version = new SkillVersion(9L, "1.0.0", "user"); + version.setStatus(SkillVersionStatus.SCANNING); + given(repository.findDispatchable(any(), any(Integer.class))).willReturn(List.of(outbox)); + given(versionRepository.findById(2L)).willReturn(Optional.of(version)); + doThrow(new IllegalStateException("redis unavailable")).when(producer).publishScanTask(any()); + + dispatcher(1).dispatch(); + + assertThat(outbox.getStatus()).isEqualTo(ScanTaskOutboxStatus.FAILED); + assertThat(version.getStatus()).isEqualTo(SkillVersionStatus.SCAN_FAILED); + verify(versionRepository).save(version); + } + + @Test + void lastPublishAttemptDoesNotOverwriteTerminalVersionStatus() { + ScanTaskOutbox outbox = outbox("task-published", 4L); + SkillVersion version = new SkillVersion(9L, "1.0.0", "user"); + version.setStatus(SkillVersionStatus.PUBLISHED); + given(repository.findDispatchable(any(), any(Integer.class))).willReturn(List.of(outbox)); + given(versionRepository.findById(4L)).willReturn(Optional.of(version)); + doThrow(new IllegalStateException("redis unavailable")).when(producer).publishScanTask(any()); + + dispatcher(1).dispatch(); + + assertThat(outbox.getStatus()).isEqualTo(ScanTaskOutboxStatus.FAILED); + assertThat(version.getStatus()).isEqualTo(SkillVersionStatus.PUBLISHED); + verify(versionRepository, never()).save(version); + } + + @Test + void expiredLeaseCanBeReclaimedAndPublished() { + ScanTaskOutbox outbox = outbox("task-expired", 5L); + assertThat(outbox.claim(Instant.parse("2025-12-31T23:00:00Z"), Duration.ofMinutes(2))).isTrue(); + given(repository.findDispatchable(any(), any(Integer.class))).willReturn(List.of(outbox)); + + dispatcher(10).dispatch(); + + assertThat(outbox.getStatus()).isEqualTo(ScanTaskOutboxStatus.SENT); + verify(producer).publishScanTask(any()); + } + + @Test + void staleFinderResultInTerminalStateIsIgnored() { + ScanTaskOutbox outbox = outbox("task-sent", 6L); + outbox.markSent(Instant.parse("2025-12-31T23:00:00Z")); + given(repository.findDispatchable(any(), any(Integer.class))).willReturn(List.of(outbox)); + + dispatcher(10).dispatch(); + + verifyNoInteractions(producer); + verify(repository, never()).save(outbox); + } + + @Test + void maxAttemptsMustBePositive() { + assertThatThrownBy(() -> dispatcher(0)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("maxAttempts"); + } + + private ScanTaskOutboxDispatcher dispatcher(int maxAttempts) { + Clock clock = Clock.fixed(Instant.parse("2026-01-01T00:00:00Z"), ZoneOffset.UTC); + return new ScanTaskOutboxDispatcher(repository, producer, versionRepository, clock, + 50, maxAttempts, Duration.ofMinutes(2), Duration.ofMinutes(5)); + } + + private ScanTaskOutbox outbox(String taskId, Long versionId) { + return new ScanTaskOutbox(new ScanTask(taskId, versionId, "/tmp/" + versionId, null, "user", 1L, + java.util.Map.of("scannerType", ScannerType.SKILL_SCANNER.getValue()))); + } +} diff --git a/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/security/ScanTaskOutbox.java b/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/security/ScanTaskOutbox.java new file mode 100644 index 00000000..7da371fb --- /dev/null +++ b/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/security/ScanTaskOutbox.java @@ -0,0 +1,131 @@ +package com.iflytek.skillhub.domain.security; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.PrePersist; +import jakarta.persistence.Table; +import jakarta.persistence.Version; +import org.hibernate.annotations.JdbcTypeCode; +import org.hibernate.type.SqlTypes; + +import java.time.Clock; +import java.time.Duration; +import java.time.Instant; +import java.util.Map; + +@Entity +@Table(name = "scan_task_outbox") +public class ScanTaskOutbox { + @Id @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + @Column(name = "task_id", nullable = false, unique = true, length = 100) + private String taskId; + @Column(name = "version_id", nullable = false) + private Long versionId; + @Column(name = "skill_path", length = 1000) + private String skillPath; + @Column(name = "bundle_key", length = 1000) + private String bundleKey; + @Column(name = "publisher_id", length = 255) + private String publisherId; + @JdbcTypeCode(SqlTypes.JSON) + @Column(name = "metadata", nullable = false, columnDefinition = "jsonb") + private Map metadata; + @Enumerated(EnumType.STRING) @Column(nullable = false, length = 20) + private ScanTaskOutboxStatus status; + @Column(name = "retry_count", nullable = false) + private int retryCount; + @Column(name = "next_attempt_at", nullable = false) + private Instant nextAttemptAt; + @Column(name = "lease_until") + private Instant leaseUntil; + @Column(name = "last_error", length = 2000) + private String lastError; + @Column(name = "created_at", nullable = false, updatable = false) + private Instant createdAt; + @Column(name = "updated_at", nullable = false) + private Instant updatedAt; + @Version @Column(nullable = false) + private long entityVersion; + + protected ScanTaskOutbox() { } + + public ScanTaskOutbox(ScanTask task) { + this.taskId = task.taskId(); + this.versionId = task.versionId(); + this.skillPath = task.skillPath(); + this.bundleKey = task.bundleKey(); + this.publisherId = task.publisherId(); + this.metadata = task.metadata() == null ? Map.of() : Map.copyOf(task.metadata()); + this.status = ScanTaskOutboxStatus.PENDING; + Instant taskCreatedAt = Instant.ofEpochMilli(task.createdAtMillis()); + this.nextAttemptAt = taskCreatedAt; + this.createdAt = taskCreatedAt; + this.updatedAt = taskCreatedAt; + } + + @PrePersist + protected void onCreate() { + Instant now = Instant.now(Clock.systemUTC()); + if (createdAt == null) createdAt = now; + if (updatedAt == null) updatedAt = now; + if (nextAttemptAt == null) nextAttemptAt = now; + } + + public ScanTask toScanTask() { + return new ScanTask(taskId, versionId, skillPath, bundleKey, publisherId, + createdAt.toEpochMilli(), metadata == null ? Map.of() : Map.copyOf(metadata)); + } + + public boolean claim(Instant now, Duration lease) { + if (status != ScanTaskOutboxStatus.PENDING + && !(status == ScanTaskOutboxStatus.SENDING && leaseUntil != null && leaseUntil.isBefore(now))) return false; + status = ScanTaskOutboxStatus.SENDING; + leaseUntil = now.plus(lease); + updatedAt = now; + return true; + } + + public void markSent(Instant now) { + status = ScanTaskOutboxStatus.SENT; + leaseUntil = null; + lastError = null; + updatedAt = now; + } + + public void markFailed(Instant now, String error) { + retryCount++; + status = ScanTaskOutboxStatus.FAILED; + leaseUntil = null; + lastError = truncateError(error); + updatedAt = now; + } + + public void markRetry(Instant now, Duration delay, String error) { + retryCount++; + status = ScanTaskOutboxStatus.PENDING; + nextAttemptAt = now.plus(delay); + leaseUntil = null; + lastError = truncateError(error); + updatedAt = now; + } + + public Long getId() { return id; } + public String getTaskId() { return taskId; } + public Long getVersionId() { return versionId; } + public ScanTaskOutboxStatus getStatus() { return status; } + public int getRetryCount() { return retryCount; } + public Instant getNextAttemptAt() { return nextAttemptAt; } + public Instant getLeaseUntil() { return leaseUntil; } + + private String truncateError(String error) { + return error == null ? null : error.substring(0, Math.min(error.length(), 2000)); + } + + public Instant getCreatedAt() { return createdAt; } +} diff --git a/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/security/ScanTaskOutboxRepository.java b/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/security/ScanTaskOutboxRepository.java new file mode 100644 index 00000000..7ed29c5d --- /dev/null +++ b/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/security/ScanTaskOutboxRepository.java @@ -0,0 +1,11 @@ +package com.iflytek.skillhub.domain.security; + +import java.time.Instant; +import java.util.List; + +public interface ScanTaskOutboxRepository { + ScanTaskOutbox save(ScanTaskOutbox outbox); + List findDispatchable(Instant now, int limit); + int deleteSentBefore(Instant cutoff); + int deleteByVersionId(Long versionId); +} diff --git a/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/security/ScanTaskOutboxStatus.java b/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/security/ScanTaskOutboxStatus.java new file mode 100644 index 00000000..dc0fd83a --- /dev/null +++ b/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/security/ScanTaskOutboxStatus.java @@ -0,0 +1,8 @@ +package com.iflytek.skillhub.domain.security; + +public enum ScanTaskOutboxStatus { + PENDING, + SENDING, + SENT, + FAILED +} diff --git a/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/security/SecurityAudit.java b/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/security/SecurityAudit.java index 055287f0..0b7844de 100644 --- a/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/security/SecurityAudit.java +++ b/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/security/SecurityAudit.java @@ -26,6 +26,9 @@ public class SecurityAudit { @Column(name = "skill_version_id", nullable = false) private Long skillVersionId; + @Column(name = "task_id", length = 100) + private String taskId; + @Column(name = "scan_id", length = 100) private String scanId; @@ -66,8 +69,13 @@ public class SecurityAudit { } public SecurityAudit(Long skillVersionId, ScannerType scannerType) { + this(skillVersionId, scannerType, null); + } + + public SecurityAudit(Long skillVersionId, ScannerType scannerType, String taskId) { this.skillVersionId = skillVersionId; this.scannerType = scannerType; + this.taskId = taskId; this.verdict = SecurityVerdict.SUSPICIOUS; this.isSafe = false; this.findingsCount = 0; @@ -91,6 +99,10 @@ public class SecurityAudit { return scanId; } + public String getTaskId() { + return taskId; + } + public ScannerType getScannerType() { return scannerType; } diff --git a/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/security/SecurityAuditRepository.java b/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/security/SecurityAuditRepository.java index 2ae16856..9bde6ea6 100644 --- a/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/security/SecurityAuditRepository.java +++ b/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/security/SecurityAuditRepository.java @@ -12,6 +12,8 @@ public interface SecurityAuditRepository { Optional findByScanId(String scanId); + boolean existsByTaskIdAndScannedAtIsNotNull(String taskId); + boolean existsBySkillVersionId(Long skillVersionId); /** diff --git a/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/security/SecurityScanService.java b/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/security/SecurityScanService.java index 2d455064..df12ee8e 100644 --- a/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/security/SecurityScanService.java +++ b/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/security/SecurityScanService.java @@ -9,6 +9,7 @@ import com.iflytek.skillhub.domain.skill.SkillVersionStatus; import com.iflytek.skillhub.domain.skill.validation.PackageEntry; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -32,23 +33,36 @@ public class SecurityScanService { private final SecurityAuditRepository auditRepository; private final SkillVersionRepository skillVersionRepository; + private final ScanTaskOutboxRepository scanTaskOutboxRepository; private final ScanTaskProducer scanTaskProducer; private final ObjectMapper objectMapper; private final String scanMode; private final boolean enabled; + @Autowired public SecurityScanService(SecurityAuditRepository auditRepository, SkillVersionRepository skillVersionRepository, ScanTaskProducer scanTaskProducer, ObjectMapper objectMapper, @Value("${skillhub.security.scanner.mode:local}") String scanMode, - @Value("${skillhub.security.scanner.enabled:false}") boolean enabled) { + @Value("${skillhub.security.scanner.enabled:false}") boolean enabled, + ScanTaskOutboxRepository scanTaskOutboxRepository) { this.auditRepository = auditRepository; this.skillVersionRepository = skillVersionRepository; this.scanTaskProducer = scanTaskProducer; this.objectMapper = objectMapper; this.scanMode = scanMode; this.enabled = enabled; + this.scanTaskOutboxRepository = scanTaskOutboxRepository; + } + + public SecurityScanService(SecurityAuditRepository auditRepository, + SkillVersionRepository skillVersionRepository, + ScanTaskProducer scanTaskProducer, + ObjectMapper objectMapper, + String scanMode, + boolean enabled) { + this(auditRepository, skillVersionRepository, scanTaskProducer, objectMapper, scanMode, enabled, null); } public boolean isEnabled() { @@ -74,7 +88,6 @@ public class SecurityScanService { packagePath = saveTempDirectory(versionId, entries).toString(); } // Always create a new audit record — supports multiple rounds per version - auditRepository.save(new SecurityAudit(versionId, ScannerType.SKILL_SCANNER)); final ScanTask scanTask = new ScanTask( UUID.randomUUID().toString(), versionId, @@ -84,9 +97,12 @@ public class SecurityScanService { System.currentTimeMillis(), Map.of("scannerType", ScannerType.SKILL_SCANNER.getValue()) ); - // The stream consumer must not observe this task before skill_version / - // security_audit rows are committed and visible. - TransactionCommitCallbacks.afterCommitOrNow(() -> scanTaskProducer.publishScanTask(scanTask)); + auditRepository.save(new SecurityAudit(versionId, ScannerType.SKILL_SCANNER, scanTask.taskId())); + if (scanTaskOutboxRepository != null) { + scanTaskOutboxRepository.save(new ScanTaskOutbox(scanTask)); + } else { + TransactionCommitCallbacks.afterCommitOrNow(() -> scanTaskProducer.publishScanTask(scanTask)); + } // Only transition to SCANNING if the version is not already published (auto-publish flow) if (version.getStatus() != SkillVersionStatus.PUBLISHED) { version.setStatus(SkillVersionStatus.SCANNING); @@ -94,6 +110,11 @@ public class SecurityScanService { } } + public boolean isTaskAlreadyProcessed(String taskId) { + return taskId != null && auditRepository != null + && auditRepository.existsByTaskIdAndScannedAtIsNotNull(taskId); + } + @Transactional public void processScanResult(Long versionId, ScannerType scannerType, SecurityScanResponse response) { SecurityAudit audit = auditRepository.findLatestActiveByVersionIdAndScannerType(versionId, scannerType) @@ -186,6 +207,9 @@ public class SecurityScanService { */ @Transactional public void softDeleteByVersionId(Long versionId) { + if (scanTaskOutboxRepository != null) { + scanTaskOutboxRepository.deleteByVersionId(versionId); + } List audits = auditRepository.findAllActiveBySkillVersionId(versionId); if (audits.isEmpty()) { log.debug("No active security audits to soft-delete for versionId={}", versionId); @@ -203,5 +227,8 @@ public class SecurityScanService { @Transactional public void hardDeleteByVersionId(Long versionId) { auditRepository.deleteBySkillVersionId(versionId); + if (scanTaskOutboxRepository != null) { + scanTaskOutboxRepository.deleteByVersionId(versionId); + } } } diff --git a/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/security/package-info.java b/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/security/package-info.java new file mode 100644 index 00000000..8abf6ca7 --- /dev/null +++ b/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/security/package-info.java @@ -0,0 +1,2 @@ +/** Security scanning domain model and durable task dispatch ports. */ +package com.iflytek.skillhub.domain.security; diff --git a/server/skillhub-domain/src/test/java/com/iflytek/skillhub/domain/security/ScanTaskOutboxTest.java b/server/skillhub-domain/src/test/java/com/iflytek/skillhub/domain/security/ScanTaskOutboxTest.java new file mode 100644 index 00000000..373baaf6 --- /dev/null +++ b/server/skillhub-domain/src/test/java/com/iflytek/skillhub/domain/security/ScanTaskOutboxTest.java @@ -0,0 +1,56 @@ +package com.iflytek.skillhub.domain.security; + +import org.junit.jupiter.api.Test; + +import java.time.Duration; +import java.time.Instant; +import java.util.Map; + +import static org.assertj.core.api.Assertions.assertThat; + +class ScanTaskOutboxTest { + @Test + void claimAndMarkSentProducesStableTaskPayload() { + ScanTask task = new ScanTask("task-1", 7L, "/tmp/7", null, "u1", 123L, + Map.of( + "scannerType", ScannerType.SKILL_SCANNER.getValue(), + "futureAttribute", "preserved")); + ScanTaskOutbox outbox = new ScanTaskOutbox(task); + Instant now = Instant.parse("2026-01-01T00:00:00Z"); + + assertThat(outbox.claim(now, Duration.ofMinutes(2))).isTrue(); + assertThat(outbox.getStatus()).isEqualTo(ScanTaskOutboxStatus.SENDING); + outbox.markSent(now.plusSeconds(1)); + + assertThat(outbox.getStatus()).isEqualTo(ScanTaskOutboxStatus.SENT); + assertThat(outbox.toScanTask()).isEqualTo(task); + } + + @Test + void exhaustedPublishAttemptsMoveTaskToFailed() { + ScanTaskOutbox outbox = new ScanTaskOutbox( + new ScanTask("task-failed", 9L, null, "bundle.zip", null, 1L, Map.of())); + Instant now = Instant.parse("2026-01-01T00:00:00Z"); + outbox.claim(now, Duration.ofMinutes(2)); + + outbox.markFailed(now, "permanent failure"); + + assertThat(outbox.getStatus()).isEqualTo(ScanTaskOutboxStatus.FAILED); + assertThat(outbox.getRetryCount()).isEqualTo(1); + assertThat(outbox.getLeaseUntil()).isNull(); + assertThat(outbox.claim(now.plusSeconds(1), Duration.ofMinutes(2))).isFalse(); + } + + @Test + void failedPublishReturnsToPendingWithBackoffAndTruncatesError() { + ScanTaskOutbox outbox = new ScanTaskOutbox( + new ScanTask("task-2", 8L, null, "packages/1/8/bundle.zip", null, 1L, Map.of())); + Instant now = Instant.parse("2026-01-01T00:00:00Z"); + outbox.claim(now, Duration.ofMinutes(2)); + outbox.markRetry(now, Duration.ofSeconds(5), "x".repeat(5000)); + + assertThat(outbox.getStatus()).isEqualTo(ScanTaskOutboxStatus.PENDING); + assertThat(outbox.getRetryCount()).isEqualTo(1); + assertThat(outbox.getNextAttemptAt()).isEqualTo(now.plusSeconds(5)); + } +} diff --git a/server/skillhub-domain/src/test/java/com/iflytek/skillhub/domain/security/SecurityScanOutboxTest.java b/server/skillhub-domain/src/test/java/com/iflytek/skillhub/domain/security/SecurityScanOutboxTest.java new file mode 100644 index 00000000..f0e056bf --- /dev/null +++ b/server/skillhub-domain/src/test/java/com/iflytek/skillhub/domain/security/SecurityScanOutboxTest.java @@ -0,0 +1,59 @@ +package com.iflytek.skillhub.domain.security; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.iflytek.skillhub.domain.skill.SkillVersion; +import com.iflytek.skillhub.domain.skill.SkillVersionRepository; +import com.iflytek.skillhub.domain.skill.validation.PackageEntry; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.lang.reflect.Field; +import java.util.List; +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; + +@ExtendWith(MockitoExtension.class) +class SecurityScanOutboxTest { + @Mock SecurityAuditRepository auditRepository; + @Mock SkillVersionRepository versionRepository; + @Mock ScanTaskProducer producer; + @Mock ScanTaskOutboxRepository outboxRepository; + + @Test + void triggerPersistsAuditStateAndOutboxWithoutPublishingInsideTransaction() throws Exception { + SkillVersion version = new SkillVersion(9L, "1.0.0", "publisher"); + Field id = SkillVersion.class.getDeclaredField("id"); + id.setAccessible(true); + id.set(version, 42L); + given(versionRepository.findById(42L)).willReturn(Optional.of(version)); + SecurityScanService service = new SecurityScanService(auditRepository, versionRepository, producer, + new ObjectMapper(), "upload", true, outboxRepository); + + service.triggerScan(42L, List.of(new PackageEntry("SKILL.md", new byte[0], 0, "text/markdown")), "publisher"); + + ArgumentCaptor outbox = ArgumentCaptor.forClass(ScanTaskOutbox.class); + verify(outboxRepository).save(outbox.capture()); + verify(producer, never()).publishScanTask(org.mockito.ArgumentMatchers.any()); + assertThat(outbox.getValue().getVersionId()).isEqualTo(42L); + assertThat(outbox.getValue().getStatus()).isEqualTo(ScanTaskOutboxStatus.PENDING); + } + + @Test + void softDeleteRemovesPendingOutboxEvenWhenNoActiveAuditExists() { + given(auditRepository.findAllActiveBySkillVersionId(42L)).willReturn(List.of()); + SecurityScanService service = new SecurityScanService(auditRepository, versionRepository, producer, + new ObjectMapper(), "upload", true, outboxRepository); + + service.softDeleteByVersionId(42L); + + verify(outboxRepository).deleteByVersionId(42L); + verify(auditRepository, never()).saveAll(org.mockito.ArgumentMatchers.anyList()); + } +} diff --git a/server/skillhub-infra/src/main/java/com/iflytek/skillhub/infra/jpa/ScanTaskOutboxJpaRepository.java b/server/skillhub-infra/src/main/java/com/iflytek/skillhub/infra/jpa/ScanTaskOutboxJpaRepository.java new file mode 100644 index 00000000..84b08993 --- /dev/null +++ b/server/skillhub-infra/src/main/java/com/iflytek/skillhub/infra/jpa/ScanTaskOutboxJpaRepository.java @@ -0,0 +1,34 @@ +package com.iflytek.skillhub.infra.jpa; + +import com.iflytek.skillhub.domain.security.ScanTaskOutbox; +import com.iflytek.skillhub.domain.security.ScanTaskOutboxRepository; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import org.springframework.stereotype.Repository; + +import java.time.Instant; +import java.util.List; + +@Repository +public interface ScanTaskOutboxJpaRepository extends JpaRepository, ScanTaskOutboxRepository { + @Override + @Query(value = """ + SELECT * FROM scan_task_outbox + WHERE (status = 'PENDING' AND next_attempt_at <= :now) + OR (status = 'SENDING' AND lease_until < :now) + ORDER BY created_at + LIMIT :limit + FOR UPDATE SKIP LOCKED + """, nativeQuery = true) + List findDispatchable(@Param("now") Instant now, @Param("limit") int limit); + + @Override + @Modifying + @Query("DELETE FROM ScanTaskOutbox o WHERE o.status = com.iflytek.skillhub.domain.security.ScanTaskOutboxStatus.SENT AND o.updatedAt < :cutoff") + int deleteSentBefore(@Param("cutoff") Instant cutoff); + + @Override + int deleteByVersionId(Long versionId); +} \ No newline at end of file From 16306dd4f473d0e9163ce94317b8c0d42015f01d Mon Sep 17 00:00:00 2001 From: Michael Date: Wed, 26 Aug 2026 14:43:07 +0300 Subject: [PATCH 12/15] feat(i18n): add Russian locale for web UI and server messages (#700) Signed-off-by: Mikhail Neradkov --- .../src/main/resources/messages_ru.properties | 179 ++ .../notification/notification-dropdown.tsx | 16 +- web/src/i18n/config.test.ts | 8 +- web/src/i18n/config.ts | 2 + .../i18n/landing-quick-start-locale.test.ts | 5 +- web/src/i18n/locales/ru.json | 1567 +++++++++++++++++ web/src/i18n/ru-locale.test.ts | 51 + web/src/pages/notifications.tsx | 14 +- .../shared/components/language-switcher.tsx | 3 +- web/src/shared/lib/api-error.ts | 3 + web/src/shared/lib/format-relative-time.ts | 34 + 11 files changed, 1851 insertions(+), 31 deletions(-) create mode 100644 server/skillhub-app/src/main/resources/messages_ru.properties create mode 100644 web/src/i18n/locales/ru.json create mode 100644 web/src/i18n/ru-locale.test.ts create mode 100644 web/src/shared/lib/format-relative-time.ts diff --git a/server/skillhub-app/src/main/resources/messages_ru.properties b/server/skillhub-app/src/main/resources/messages_ru.properties new file mode 100644 index 00000000..8ea9d823 --- /dev/null +++ b/server/skillhub-app/src/main/resources/messages_ru.properties @@ -0,0 +1,179 @@ +response.success=Успешно +response.success.read=Успешно получено +response.success.created=Успешно создано +response.success.updated=Успешно обновлено +response.success.deleted=Успешно удалено +response.success.published=Успешно опубликовано +response.success.revoked=Успешно отозвано +response.success.health=Сервис работает +validation.namespace.slug.notBlank=Slug не может быть пустым +validation.namespace.slug.size=Slug должен содержать от 2 до 64 символов +validation.namespace.displayName.notBlank=Отображаемое имя не может быть пустым +validation.namespace.displayName.size=Отображаемое имя не должно превышать 128 символов +validation.namespace.description.size=Описание не должно превышать 512 символов +validation.member.userId.notNull=Требуется ID пользователя +validation.member.role.notNull=Требуется роль +validation.auth.local.username.notBlank=Имя пользователя не может быть пустым +validation.auth.local.password.notBlank=Пароль не может быть пустым +validation.auth.local.email.notBlank=Email не может быть пустым +validation.auth.local.currentPassword.notBlank=Текущий пароль не может быть пустым +validation.auth.local.newPassword.notBlank=Новый пароль не может быть пустым +validation.auth.local.email.invalid=Некорректный формат email +validation.token.name.notBlank=Имя токена не может быть пустым +validation.token.name.size=Имя токена должно содержать не более 64 символов +validation.token.expiresAt.invalid=Некорректный формат времени истечения +validation.token.expiresAt.future=Время истечения должно быть в будущем +error.token.name.duplicate=У вас уже есть токен с таким именем +error.token.notFound=Токен не найден: {0} +error.auth.required=Требуется аутентификация +error.auth.local.username.exists=Имя пользователя уже занято +error.auth.local.email.exists=Email уже занят +error.auth.local.password.tooShort=Пароль должен содержать не менее 8 символов +error.auth.local.password.tooLong=Пароль не должен превышать 128 символов +error.auth.local.password.tooWeak=Пароль должен включать не менее 3 типов символов +error.auth.local.username.invalid=Имя пользователя: 3–64 символа, только буквы, цифры или подчёркивания +error.auth.local.invalidCredentials=Неверное имя пользователя или пароль +error.auth.local.notEnabled=Вход по локальной учётной записи для этого пользователя не включён +error.auth.local.accountDisabled=Эта учётная запись отключена +error.auth.local.accountPending=Эта учётная запись ожидает активации +error.auth.local.accountMerged=Эта учётная запись объединена и больше не может использоваться для входа +error.auth.local.locked=Слишком много неудачных попыток. Повторите через {0} мин. +error.auth.login.throttled=Слишком много попыток входа. Повторите через {0} мин. +error.auth.direct.disabled=Совместимость прямой аутентификации отключена +error.auth.direct.providerUnsupported=Неподдерживаемый провайдер прямой аутентификации: {0} +error.auth.sessionBootstrap.disabled=Инициализация сессии отключена +error.auth.sessionBootstrap.providerUnsupported=Неподдерживаемый провайдер инициализации сессии: {0} +error.auth.sessionBootstrap.notAuthenticated=Внешняя аутентифицированная сессия не найдена +error.badRequest=Некорректный запрос +error.methodNotAllowed=HTTP-метод не поддерживается +error.unsupportedMediaType=Неподдерживаемый тип медиа +error.notAcceptable=Запрошенный тип ответа не поддерживается +error.forbidden=Доступ запрещён +error.apiToken.scope.missing=У API-токена отсутствует требуемая область доступа: {0} +error.apiToken.endpoint.unsupported=API-токен не может обращаться к эндпоинту: {0} +error.request.timeout=Время ожидания запроса истекло +error.rateLimit.exceeded=Превышен лимит запросов +error.storage.unavailable=Объектное хранилище временно недоступно. Повторите попытку позже. +error.internal=Произошла непредвиденная ошибка +error.slug.blank=Slug не может быть пустым +error.slug.length=Длина slug должна быть от {0} до {1} символов +error.slug.pattern=Slug может содержать только строчные буквы, цифры и дефисы и должен начинаться и заканчиваться буквой или цифрой +error.slug.doubleHyphen=Slug не может содержать два дефиса подряд +error.slug.reserved=Slug ''{0}'' зарезервирован и не может быть использован +label.definition.too_many=Достигнут лимит определений меток ({0}) +label.sort_order.empty=Полезная нагрузка обновления порядка сортировки не может быть пустой +label.translation.empty=Требуется хотя бы один перевод метки +label.translation.locale.blank=Локаль перевода метки не может быть пустой +label.translation.display_name.blank=Отображаемое имя перевода метки не может быть пустым +label.translation.locale.duplicate=Дублирующаяся локаль перевода метки: {0} +label.translation.locale.conflict=Дублирующаяся локаль перевода метки +error.namespace.slug.exists=Slug пространства имён ''{0}'' уже существует +error.namespace.id.notFound=Пространство имён не найдено: {0} +error.namespace.slug.notFound=Пространство имён не найдено: {0} +error.namespace.membership.required=Требуется членство в пространстве имён +error.namespace.global.members.platformAdmin.required=Только администраторы пользователей платформы могут просматривать участников глобального пространства имён +error.namespace.admin.required=Требуется роль владельца или администратора пространства имён +error.namespace.owner.required=Требуется роль владельца пространства имён +error.namespace.create.platformAdminRequired=Создавать пространства имён могут только SKILL_ADMIN или SUPER_ADMIN +error.namespace.delete.hasDependencies=Пространство имён нельзя удалить, пока в нём есть скиллы или записи управления +error.namespace.member.owner.assignDirect=Нельзя назначить роль OWNER напрямую +error.namespace.member.alreadyExists=Пользователь уже является участником пространства имён +error.namespace.member.notFound=Участник не найден +error.namespace.member.owner.remove=Нельзя удалить владельца пространства имён +error.namespace.member.owner.setDirect=Нельзя задать роль OWNER напрямую, используйте передачу владения +error.namespace.member.search.tooShort=Поисковый запрос должен содержать не менее 2 символов +error.namespace.owner.current.notFound=Текущий владелец не найден +error.namespace.owner.current.invalid=Текущий пользователь не является владельцем пространства имён +error.namespace.owner.new.notFound=Новый владелец не является участником пространства имён +error.skill.metadata.content.empty=Содержимое SKILL.md не может быть пустым +error.skill.metadata.frontmatter.missingStart=Отсутствует начальный маркер frontmatter ''---'' +error.skill.metadata.frontmatter.missingContent=Отсутствует содержимое frontmatter после начального маркера +error.skill.metadata.frontmatter.missingEnd=Отсутствует конечный маркер frontmatter ''---'' +error.skill.metadata.yaml.notMap=Frontmatter должен быть YAML-объектом +error.skill.metadata.yaml.invalid=Некорректный YAML во frontmatter: {0} +error.skill.metadata.requiredField.missing=Отсутствует обязательное поле: {0} +error.skill.metadata.compliance.invalid=Некорректные метаданные x-astron-compliance: {0} +error.skill.publish.publisher.notMember=Публикующий не является участником пространства имён: {0} +error.skill.publish.package.invalid=Проверка пакета не пройдена: {0} +error.skill.publish.skillMd.notFound=SKILL.md не найден +error.skill.publish.precheck.confirmRequired=Предупреждения перед публикацией требуют подтверждения:\n{0} +error.skill.publish.precheck.failed=Проверка перед публикацией не пройдена: {0} +error.security.scanner.required=Перед публикацией публичных или видимых в пространстве имён скиллов необходимо включить сканер безопасности +error.skill.publish.archived=Архивный скилл нужно восстановить перед публикацией: {0} +review.withdraw.not_pending=Отозвать можно только заявки на ревью со статусом pending: {0} +review.withdraw.not_submitter=Отозвать это ревью может только отправитель +review.approve.scan_in_progress=Сканирование безопасности ещё выполняется. Одобрение недоступно до завершения сканирования. +review_task.not_found_for_version=Не найдена ожидающая заявка на ревью для версии: {0} +error.skill.publish.summary.tooLong=Описание скилла не должно превышать {0} символов +error.skill.notFound=Скилл не найден: {0} +error.skill.access.denied=Нет доступа к скиллу: {0} +error.skill.status.notActive=Скилл не активен +error.skill.lifecycle.noPermission=Управлять этим скиллом может только владелец скилла или администратор пространства имён +error.skill.version.exists=Версия уже существует: {0} +error.skill.version.notFound=Версия не найдена: {0} +error.skill.version.notPublished=Версия не опубликована: {0} +error.skill.version.delete.unsupported=Удалять можно только версии в статусах DRAFT, UPLOADED, REJECTED или SCAN_FAILED: {0} +error.skill.version.delete.lastVersion=Нельзя удалить последнюю оставшуюся версию: {0} +error.skill.version.compare.same=Нельзя сравнить версию саму с собой +error.skill.report.reason.required=Укажите причину жалобы +error.skill.report.unavailable=Сейчас на этот скилл нельзя пожаловаться: {0} +error.skill.report.self=Нельзя пожаловаться на свой собственный скилл +error.skill.report.duplicate=У вас уже есть ожидающая жалоба на этот скилл +error.skill.report.notFound=Жалоба на скилл не найдена: {0} +error.skill.report.alreadyHandled=Эта жалоба на скилл уже обработана +error.skill.report.status.invalid=Неподдерживаемый статус жалобы на скилл: {0} +error.skill.version.latest.unavailable=Нет опубликованной версии для скилла: {0} +error.skill.version.latest.notFound=Последняя опубликованная версия не найдена +error.skill.file.notFound=Файл не найден: {0} +error.skill.tag.latest.reserved=Имя тега ''latest'' зарезервировано +error.skill.tag.latest.delete=Имя тега ''latest'' зарезервировано и не может быть удалено +error.skill.tag.notFound=Тег не найден: {0} +error.skill.tag.targetVersion.notPublished=Целевая версия должна быть опубликована +error.skill.tag.version.missing=Тег не указывает на версию: {0} +error.skill.tag.version.notFound=Версия, на которую указывает тег, не найдена: {0} +error.skill.bundle.notFound=Опубликованный пакет не найден в хранилище +error.skill.resolve.versionTag.conflict=Параметры version и tag нельзя использовать вместе +error.deviceAuth.userCode.invalid=Неверный или просроченный код пользователя +error.deviceAuth.deviceCode.expired=Код устройства истёк +error.deviceAuth.deviceCode.invalid=Код устройства истёк или недействителен +error.deviceAuth.deviceCode.used=Код устройства уже использован +error.admin.user.notFound=Пользователь не найден: {0} +error.admin.user.role.invalid=Неверная роль: {0} +error.admin.user.role.superAdmin.assignDenied=Изменять состояние роли SUPER_ADMIN может только SUPER_ADMIN +error.admin.user.systemAccount.immutable=Системные учётные записи нельзя изменять из управления пользователями +error.admin.user.status.invalid=Неверный статус пользователя: {0} +error.admin.user.status.unsupported=Здесь можно управлять только статусами ACTIVE или DISABLED +error.skill.publish.nameConflict=Опубликованный скилл с именем ''{0}'' уже существует в этом пространстве имён +error.skill.publish.nameConflict.private=Приватный скилл с именем ''{0}'' уже опубликован в этом пространстве имён +error.skill.approve.nameConflict=Нельзя одобрить: опубликованный скилл с именем ''{0}'' уже существует в этом пространстве имён +error.skill.version.submit.notUploaded=Версия ''{0}'' не в статусе UPLOADED и не может быть отправлена на ревью +error.skill.version.confirm.notUploaded=Версия ''{0}'' не в статусе UPLOADED и не может быть подтверждена +error.skill.confirm.notPrivate=Confirm-publish доступен только для PRIVATE скиллов +error.skill.version.notDownloadable=Версия ''{0}'' недоступна для скачивания +error.profile.displayName.length=Отображаемое имя должно содержать от 2 до 32 символов +error.profile.displayName.pattern=Отображаемое имя может содержать только китайские иероглифы, латинские буквы, цифры, пробелы, подчёркивания и дефисы +error.profile.noChanges=Необходимо указать хотя бы одно поле +response.profile.updated=Профиль успешно обновлён +response.profile.pendingReview=Изменения профиля отправлены на ревью +error.profileReview.notFound=Заявка на изменение профиля не найдена +error.profileReview.notPending=Эта заявка уже рассмотрена +error.profileReview.commentRequired=Требуется причина отклонения +error.profileReview.commentTooLong=Причина отклонения не должна превышать 500 символов +error.profileReview.status.invalid=Неверный статус ревью: {0} +error.profileReview.userDisabled=Нельзя применить изменения — учётная запись пользователя отключена +response.auth.password.reset.requested=Если учётная запись подходит, код подтверждения для сброса пароля отправлен. +response.auth.password.reset.confirmed=Пароль успешно сброшен. Войдите с новым паролем. +error.auth.password.reset.invalid.code=Код подтверждения недействителен или истёк. +error.auth.password.reset.not.eligible=Эта учётная запись не подходит для сброса пароля. +error.auth.password.reset.no.credential=У этой учётной записи нет локальных учётных данных. +error.auth.password.reset.email.failed=Не удалось отправить код подтверждения для сброса пароля. Повторите попытку позже. +validation.auth.password.reset.email.notBlank=Email не может быть пустым +validation.auth.password.reset.email.invalid=Некорректный формат email +validation.auth.password.reset.code.notBlank=Код подтверждения не может быть пустым +validation.auth.password.reset.code.invalid=Код подтверждения должен состоять из 6 цифр +validation.auth.password.reset.newPassword.notBlank=Новый пароль не может быть пустым +promotion.target_skill_conflict=Целевой глобальный скилл "{0}" уже существует +promotion.status.invalid=Неподдерживаемый статус продвижения: {0} +promotion.sort.field.invalid=Неподдерживаемое поле сортировки продвижения: {0} +promotion.sort.direction.invalid=Неподдерживаемое направление сортировки продвижения: {0} +promotion.sort.pending_unsupported=Ожидающие заявки на продвижение не поддерживают сортировку по времени ревью diff --git a/web/src/features/notification/notification-dropdown.tsx b/web/src/features/notification/notification-dropdown.tsx index a1c64532..90c0b568 100644 --- a/web/src/features/notification/notification-dropdown.tsx +++ b/web/src/features/notification/notification-dropdown.tsx @@ -6,26 +6,12 @@ import { resolveNotificationDisplay } from './notification-content' import { useAuth } from '@/features/auth/use-auth' import { useNotifications, useMarkAllRead, useMarkRead } from './use-notifications' import { resolveNotificationTarget } from './notification-target' +import { formatRelativeTime } from '@/shared/lib/format-relative-time' interface Props { onClose: () => void } -function formatRelativeTime(dateStr: string, lang: string): string { - const diff = Date.now() - new Date(dateStr).getTime() - const minutes = Math.floor(diff / 60_000) - const hours = Math.floor(diff / 3_600_000) - const days = Math.floor(diff / 86_400_000) - - const isChinese = lang.startsWith('zh') - - if (minutes < 1) return isChinese ? '刚刚' : 'just now' - if (minutes < 60) return isChinese ? `${minutes}分钟` : `${minutes}m` - if (hours < 24) return isChinese ? `${hours}小时` : `${hours}h` - if (days < 30) return isChinese ? `${days}天` : `${days}d` - return new Date(dateStr).toLocaleDateString() -} - /** * Dropdown panel showing the latest 5 notifications with mark-all-read and view-all actions. */ diff --git a/web/src/i18n/config.test.ts b/web/src/i18n/config.test.ts index 843605db..dd76b794 100644 --- a/web/src/i18n/config.test.ts +++ b/web/src/i18n/config.test.ts @@ -29,6 +29,10 @@ vi.mock('./locales/zh.json', () => ({ default: { greeting: '你好' }, })) +vi.mock('./locales/ru.json', () => ({ + default: { greeting: 'Привет' }, +})) + // Import triggers the side-effect initialization await import('./config') @@ -54,11 +58,13 @@ describe('i18n config', () => { expect(initOptions.detection.caches).toEqual(['localStorage']) }) - it('registers both english and chinese resource bundles', () => { + it('registers english, russian and chinese resource bundles', () => { const initOptions = initMock.mock.calls[0][0] expect(initOptions.resources).toHaveProperty('en') + expect(initOptions.resources).toHaveProperty('ru') expect(initOptions.resources).toHaveProperty('zh') expect(initOptions.resources.en).toHaveProperty('translation') + expect(initOptions.resources.ru).toHaveProperty('translation') expect(initOptions.resources.zh).toHaveProperty('translation') }) }) diff --git a/web/src/i18n/config.ts b/web/src/i18n/config.ts index 9973671f..e1cee718 100644 --- a/web/src/i18n/config.ts +++ b/web/src/i18n/config.ts @@ -2,6 +2,7 @@ import i18n from 'i18next' import { initReactI18next } from 'react-i18next' import LanguageDetector from 'i18next-browser-languagedetector' import en from './locales/en.json' +import ru from './locales/ru.json' import zh from './locales/zh.json' /** @@ -15,6 +16,7 @@ i18n .init({ resources: { en: { translation: en }, + ru: { translation: ru }, zh: { translation: zh }, }, fallbackLng: 'en', diff --git a/web/src/i18n/landing-quick-start-locale.test.ts b/web/src/i18n/landing-quick-start-locale.test.ts index 0fcac247..1aeb0e8c 100644 --- a/web/src/i18n/landing-quick-start-locale.test.ts +++ b/web/src/i18n/landing-quick-start-locale.test.ts @@ -1,16 +1,19 @@ import { describe, expect, it } from 'vitest' import en from './locales/en.json' +import ru from './locales/ru.json' import zh from './locales/zh.json' describe('landing quick start locales', () => { - it('uses localized agent setup prompts for chinese and english', () => { + it('uses localized agent setup prompts for chinese, english, and russian', () => { expect(zh.landing.quickStart.agent.command).toBe('阅读 https://www.example.com/registry/skill.md,并按照说明完成 SkillHub Skills Registry 的配置') expect(en.landing.quickStart.agent.command).toBe('Read https://www.example.com/registry/skill.md and follow the instructions to setup SkillHub Skills Registry') + expect(ru.landing.quickStart.agent.command).toBe('Прочитайте https://www.example.com/registry/skill.md и следуйте инструкциям для настройки SkillHub Skills Registry') }) it('provides command templates with url placeholder for dynamic rendering', () => { expect(zh.landing.quickStart.agent.commandTemplate).toBe('阅读 {{url}},并按照说明完成 SkillHub Skills Registry 的配置') expect(en.landing.quickStart.agent.commandTemplate).toBe('Read {{url}} and follow the instructions to setup SkillHub Skills Registry') + expect(ru.landing.quickStart.agent.commandTemplate).toBe('Прочитайте {{url}} и следуйте инструкциям для настройки SkillHub Skills Registry') }) it('exposes CLI install command in both locales', () => { diff --git a/web/src/i18n/locales/ru.json b/web/src/i18n/locales/ru.json new file mode 100644 index 00000000..4cf75626 --- /dev/null +++ b/web/src/i18n/locales/ru.json @@ -0,0 +1,1567 @@ +{ + "compliance": { + "title": "Заявления о соответствии", + "mappingCount": "{{count}} элементов", + "unknownStandard": "Неизвестный стандарт", + "evidence": "Доказательства" + }, + "common": { + "expand": "Развернуть подробности", + "collapse": "Свернуть подробности" + }, + "nav": { + "landing": "Главная", + "home": "Центр скиллов", + "search": "Поиск", + "skillDetail": "Карточка скилла", + "explore": "Обзор скиллов", + "dashboard": "Панель управления", + "mySkills": "Мои скиллы", + "publish": "Опубликовать", + "login": "Вход" + }, + "landing": { + "hero": { + "title": "Находите и делитесь AI-скиллами", + "subtitle": "Создавайте мощных AI-агентов на скиллах сообщества", + "searchPlaceholder": "Поиск скиллов...", + "exploreSkills": "Обзор скиллов", + "publishSkill": "Опубликовать скилл" + }, + "features": { + "secure": { + "title": "Безопасность и конфиденциальность", + "description": "Корпоративный уровень защиты для ваших AI-процессов" + }, + "community": { + "title": "Сообщество", + "description": "Делитесь скиллами и находите их у разработчиков по всему миру" + }, + "integration": { + "title": "Простая интеграция", + "description": "Легко встраивается в ваши существующие инструменты" + }, + "versionControl": { + "title": "Управление версиями", + "description": "Структурированное версионирование и релизы, чтобы пакеты скиллов оставались прослеживаемыми и надёжными." + }, + "cli": { + "title": "Инструменты CLI", + "description": "Мощные сценарии командной строки для публикации, установки и управления пакетами скиллов." + }, + "governance": { + "title": "Управление рецензированием", + "description": "Встроенные процессы рецензирования и контроль прав для высокого качества корпоративных скиллов." + } + }, + "stats": { + "skills": "Каталоги", + "downloads": "Загрузки", + "teams": "Команды" + }, + "whySkillHub": { + "title": "Почему SkillHub", + "subtitle": "Частная платформа Agent-скиллов для корпоративных команд" + }, + "badge": "Корпоративный реестр скиллов", + "tagline": "Публикуйте, находите, управляйте", + "taglineHighlight": " скиллами агентов", + "description": "Самостоятельно размещаемый частный реестр скиллов для безопасного и эффективного обмена и совместной работы в командах", + "publishSkill": "Опубликовать скилл", + "statsSkills": "Скиллы", + "statsDownloads": "Загрузки", + "statsTeams": "Команды", + "whyTitle": "Почему выбирают", + "whyDescription": "Корпоративная платформа управления скиллами с полным циклом публикации, поиска и контроля", + "featuresList": { + "privateDeploy": { + "title": "Частное развёртывание", + "description": "Полностью self-hosted с суверенитетом данных. Развёртывание в вашей инфраструктуре в один клик, безопасно за межсетевым экраном." + }, + "versionControl": { + "title": "Управление версиями", + "description": "Семантическое версионирование, пользовательские теги (beta, stable), автоматическое отслеживание последней версии." + }, + "smartSearch": { + "title": "Умный поиск", + "description": "Полнотекстовый поиск с многомерной фильтрацией по пространству имён, загрузкам, рейтингам и времени." + }, + "teamwork": { + "title": "Командная работа", + "description": "Управление пространствами имён, ролевой доступ (Owner/Admin/Member), настройка политики публикации." + }, + "governance": { + "title": "Рецензирование и контроль", + "description": "Командное рецензирование, одобрение на уровне платформы, полный аудит — соответствие требованиям комплаенса." + }, + "cliFirst": { + "title": "CLI в приоритете", + "description": "Нативный REST API, совместимость с существующими инструментами ClawHub CLI — без изменений на стороне клиента." + } + }, + "ctaTitle": "Готовы начать?", + "ctaDescription": "Запустите локальное окружение одной командой и пройдите полный цикл управления скиллами", + "ctaButton": "Попробовать", + "footerLicense": "© 2026 SkillHub. Apache License 2.0.", + "footerDocs": "Документация", + "footerGithub": "GitHub", + "footerCommunity": "Сообщество", + "quickStart": { + "title": "Быстрый старт", + "subtitle": "Быстрый старт", + "description": "Выберите способ работы, скопируйте инструкцию по настройке и продолжайте", + "tip": "💡 Совет: на странице скилла есть команды установки в один клик с переменными окружения", + "tabs": { + "agent": "Я Agent", + "human": "Я человек", + "cli": "CLI" + }, + "cli": { + "description": "Установите SkillHub CLI локально, чтобы выполнять skillhub install для скиллов.", + "command": "npm i -g @astron-team/skillhub" + }, + "agent": { + "description": "Отправьте промпт своему Agent, чтобы настроить SkillHub Registry", + "command": "Прочитайте https://www.example.com/registry/skill.md и следуйте инструкциям для настройки SkillHub Skills Registry", + "commandTemplate": "Прочитайте {{url}} и следуйте инструкциям для настройки SkillHub Skills Registry" + }, + "human": { + "description": "Используйте CLI для установки Skills", + "command": "npx clawhub search " + }, + "steps": { + "configureEnv": { + "title": "1. Настройте переменные окружения", + "description": "Настройте ClawHub CLI для подключения к SkillHub" + }, + "installSkills": { + "title": "2. Установите скиллы", + "description": "Найдите и установите нужные скиллы", + "code": "# Поиск скиллов\nclawhub search \n\n# Установить скилл\nclawhub install " + }, + "publishSkills": { + "title": "3. Опубликуйте скиллы", + "description": "Делитесь скиллами с командой", + "code": "# Опубликовать скилл\nclawhub publish\n\n# Или через веб-интерфейс\n# Нажмите «Опубликовать скилл»" + } + } + } + }, + "home": { + "subtitle": "Реестр скиллов", + "description": "Эффективная платформа управления, распространения и совместной работы со скиллами для разработчиков", + "browseSkills": "Обзор скиллов", + "publishSkill": "Опубликовать скилл", + "popularTitle": "Популярные загрузки", + "popularDescription": "Самые популярные скиллы в сообществе", + "latestTitle": "Последние релизы", + "latestDescription": "Недавно опубликованные скиллы", + "viewAll": "Смотреть все →", + "quickStart": { + "title": "Быстрый старт", + "subtitle": "Быстрый старт", + "description": "Начните работу с SkillHub за несколько простых шагов", + "tip": "💡 Совет: на странице скилла есть команды установки в один клик с переменными окружения", + "steps": { + "configureEnv": { + "title": "1. Настройте переменные окружения", + "description": "Настройте ClawHub CLI для подключения к SkillHub" + }, + "installSkills": { + "title": "2. Установите скиллы", + "description": "Найдите и установите нужные скиллы", + "code": "# Поиск скиллов\nclawhub search \n\n# Установить скилл\nclawhub install " + }, + "publishSkills": { + "title": "3. Опубликуйте скиллы", + "description": "Делитесь скиллами с командой", + "code": "# Опубликовать скилл\nclawhub publish\n\n# Или через веб-интерфейс\n# Нажмите «Опубликовать скилл»" + } + } + } + }, + "search": { + "title": "Поиск скиллов", + "placeholder": "Поиск скиллов...", + "filters": { + "label": "Фильтр:" + }, + "sort": { + "label": "Сортировка:", + "relevance": "Релевантность", + "downloads": "Загрузки", + "stars": "Звёзды", + "newest": "Новизна" + }, + "noResults": "Ничего не найдено", + "noResultsFor": "Скиллы по запросу «{{q}}» не найдены", + "filterStarred": "Только избранные", + "noStarredResults": "Избранные скиллы не найдены", + "noStarredResultsFor": "Нет избранных скиллов по запросу «{{q}}»", + "noStarredSkills": "У вас пока нет избранных скиллов", + "namespaceFilter": "@{{namespace}}", + "enterKeyword": "Введите ключевое слово для поиска", + "results": "Найдено скиллов: {{count}}", + "resultCount": "Найдено результатов: <1>{{count}}", + "loadingMore": "Обновление результатов поиска..." + }, + "searchBar": { + "placeholder": "Поиск скиллов...", + "button": "Найти", + "clear": "Очистить поиск" + }, + "login": { + "title": "Вход в SkillHub", + "subtitle": "Выберите способ продолжения", + "tabPassword": "Пароль", + "tabOAuth": "OAuth", + "username": "Имя пользователя", + "password": "Пароль", + "usernamePlaceholder": "Введите имя пользователя", + "passwordPlaceholder": "Введите пароль", + "usernameRequired": "Имя пользователя обязательно", + "passwordRequired": "Пароль обязателен", + "showPassword": "Показать пароль", + "hidePassword": "Скрыть пароль", + "submitting": "Вход...", + "submit": "Войти", + "forgotPassword": "Забыли пароль?", + "noAccount": "Нет аккаунта?", + "register": "Зарегистрироваться", + "oauthHint": "После аутентификации OAuth вы будете автоматически перенаправлены обратно на этот сайт.", + "passwordCompatHint": "В этом развёртывании включён слой совместимости паролей. Форма направит запрос в {{name}} вместо фиксированной локальной учётной записи.", + "enterpriseSsoTitle": "Корпоративный SSO", + "enterpriseSsoHint": "В этом развёртывании включён слой совместимости. Если в браузере уже есть сессия {{name}}, можно попробовать сразу установить сессию SkillHub.", + "enterpriseSsoAutoHint": "В этом развёртывании включено автоматическое зондирование {{name}}. Если оно не сработает, продолжайте стандартными способами входа.", + "enterpriseSsoAction": "Попробовать {{name}}", + "enterpriseSsoSubmitting": "Пробуем {{name}}...", + "agreementPrefix": "Входя в систему, вы соглашаетесь с", + "terms": "Условиями использования", + "and": "и", + "privacy": "Политикой конфиденциальности" + }, + "register": { + "title": "Создать аккаунт", + "subtitle": "Зарегистрируйтесь локально или войдите через OAuth.", + "tabLocal": "Локальный аккаунт", + "tabOAuth": "OAuth", + "username": "Имя пользователя", + "email": "Электронная почта", + "password": "Пароль", + "usernamePlaceholder": "3–64 символа: буквы, цифры или подчёркивания", + "emailPlaceholder": "Введите email", + "emailRequired": "Email обязателен", + "passwordPlaceholder": "Не менее 8 символов, минимум 3 типа символов", + "submitting": "Регистрация...", + "submit": "Зарегистрироваться и войти", + "hasAccount": "Уже есть аккаунт?", + "login": "Вернуться ко входу", + "oauthHint": "Войдите напрямую через существующий OAuth-аккаунт — локальный пароль не нужен.", + "usernameRequired": "Имя пользователя обязательно", + "usernameInvalid": "Допустимы только буквы, цифры или подчёркивания (3–64 символа)", + "usernameExists": "Имя пользователя уже занято", + "passwordRequired": "Пароль обязателен", + "passwordTooShort": "Пароль должен содержать не менее 8 символов", + "passwordTooWeak": "Пароль должен содержать минимум 3 типа символов (заглавные, строчные, цифры, спецсимволы)", + "emailInvalid": "Неверный формат email", + "emailExists": "Email уже используется" + }, + "resetPassword": { + "title": "Сброс пароля", + "subtitle": "Введите email, код подтверждения и новый пароль.", + "email": "Электронная почта", + "emailPlaceholder": "Введите email", + "emailRequired": "Введите email", + "emailInvalid": "Введите корректный адрес email", + "code": "Код подтверждения", + "codePlaceholder": "Введите 6-значный код", + "sendCode": "Отправить код", + "sendingCode": "Отправка...", + "codeSentMessage": "Если аккаунт подходит, код подтверждения отправлен.", + "codeRequired": "Введите код подтверждения", + "newPassword": "Новый пароль", + "newPasswordPlaceholder": "Введите новый пароль", + "newPasswordRequired": "Введите новый пароль", + "confirmPassword": "Подтвердите пароль", + "confirmPasswordPlaceholder": "Повторите новый пароль", + "passwordMismatch": "Пароли не совпадают", + "submit": "Сбросить пароль", + "submitting": "Сброс...", + "successMessage": "Пароль успешно сброшен. Войдите с новым паролем.", + "genericError": "Не удалось сбросить пароль", + "backToLogin": "Вернуться ко входу" + }, + "device": { + "title": "Авторизация устройства", + "subtitle": "Введите 8-значный код пользователя, показанный на устройстве", + "codeLabel": "Код пользователя", + "codeHint": "Формат: XXXX-XXXX (можно вставить из буфера)", + "incompleteCode": "Введите полный 8-значный код пользователя", + "success": "Устройство успешно авторизовано!", + "defaultError": "Авторизация не удалась, проверьте код пользователя", + "submitting": "Авторизация...", + "submit": "Авторизовать устройство", + "notice": "После авторизации устройство получит доступ к вашему аккаунту" + }, + "cliAuth": { + "validating": "Проверка...", + "pleaseWait": "Подождите", + "creatingToken": "Создание токена...", + "almostThere": "Почти готово", + "success": "Авторизация успешна", + "redirecting": "Перенаправление в CLI...", + "fallbackInstructions": "Если браузер не перенаправил автоматически, скопируйте токен ниже:", + "error": "Авторизация не удалась", + "notAuthenticated": "Вы не вошли в систему", + "invalidRedirectUri": "Некорректный redirect URI", + "missingState": "Отсутствует параметр безопасности", + "windowsUrlBug": "Известная проблема Windows: браузер открыл неполный URL. Скопируйте полный URL из вывода CLI (строка, начинающаяся с 'Opening browser:') и вставьте его в адресную строку браузера.", + "tokenCreationFailed": "Не удалось создать токен", + "loginRequired": "Сначала войдите в систему, чтобы авторизовать доступ CLI", + "goToLogin": "Перейти ко входу" + }, + "dashboard": { + "title": "Панель управления", + "subtitle": "Аккаунт, скиллы и учётные данные доступа — в одном месте", + "backToDashboard": "Назад к панели", + "userInfo": "Сведения об аккаунте", + "userInfoDesc": "Основные данные аккаунта и роли на платформе", + "loginVia": "Вход через {{provider}}", + "platformRoles": "Роли платформы", + "starsAndRatings": "Звёзды и рейтинги", + "viewStars": "Мои звёзды", + "subscriptions": "Подписки", + "viewSubscriptions": "Мои подписки", + "mySkillsTitle": "Мои скиллы", + "openMySkills": "Перейти к моим скиллам", + "mySkillsPreviewDescription": "Показаны 5 последних скиллов. Откройте скилл или перейдите в «Мои скиллы», чтобы увидеть все.", + "mySkillsPreviewEmpty": "Вы ещё не публиковали скиллы", + "credentials": "Учётные данные", + "openTokens": "Токены API", + "governanceTitle": "Рецензирование и контроль", + "viewGovernance": "Центр управления", + "viewPromotions": "Продвижения", + "reportsTitle": "Управление жалобами", + "viewReports": "Жалобы на скиллы", + "previewMore": "...", + "previewMoreLabel": "Смотреть все", + "userId": "ID пользователя" + }, + "mySkills": { + "title": "Мои скиллы", + "subtitle": "Управление опубликованными скиллами", + "searchPlaceholder": "Поиск по имени, slug или описанию", + "namespaceFilterLabel": "Фильтр по пространству имён", + "namespaceFilterAll": "Все пространства имён", + "clearSearch": "Сбросить фильтры", + "emptySearchTitle": "Нет подходящих скиллов", + "emptySearchDescription": "Измените ключевое слово или выберите другое пространство имён.", + "filters": { + "ALL": "Все", + "PENDING_REVIEW": "На рецензии", + "PUBLISHED": "Опубликованы", + "REJECTED": "Отклонены", + "ARCHIVED": "В архиве", + "HIDDEN": "Скрыты" + }, + "publishNew": "Опубликовать новый скилл", + "update": "Обновить", + "archive": "В архив", + "unarchive": "Восстановить", + "statusArchived": "В архиве", + "statusPendingReview": "На рецензии", + "statusPublished": "Опубликован", + "statusRejected": "Отклонён", + "statusHidden": "Скрыт", + "statusScanning": "Сканирование", + "statusScanFailed": "Ошибка сканирования", + "archiveConfirmTitle": "Архивировать скилл", + "archiveConfirmDescription": "После архивирования обычные пользователи больше не смогут просматривать или скачивать «{{skill}}». Продолжить?", + "unarchiveConfirmTitle": "Восстановить скилл", + "unarchiveConfirmDescription": "«{{skill}}» снова станет видимым и сможет публиковать новые версии после восстановления.", + "archiveSuccessTitle": "Скилл архивирован", + "archiveSuccessDescription": "«{{skill}}» архивирован.", + "archiveErrorTitle": "Не удалось архивировать скилл", + "unarchiveSuccessTitle": "Скилл восстановлен", + "unarchiveSuccessDescription": "«{{skill}}» восстановлен и снова может публиковать новые версии.", + "unarchiveErrorTitle": "Не удалось восстановить скилл", + "withdrawReview": "Отозвать рецензию", + "withdrawConfirmTitle": "Отозвать загрузку", + "withdrawConfirmDescription": "После отзыва «{{skill}}» больше не будет рецензироваться, а ожидающая версия будет удалена.", + "withdrawSuccessTitle": "Загрузка отозвана", + "withdrawSuccessDescription": "Ожидающая версия для «{{skill}}» отозвана.", + "withdrawErrorTitle": "Не удалось отозвать загрузку", + "promoteToGlobal": "Продвинуть в Global", + "promotionConfirmTitle": "Отправить запрос на продвижение", + "promotionConfirmDescription": "Отправить v{{version}} скилла «{{skill}}» на продвижение в глобальное пространство имён?", + "promotionSuccessTitle": "Запрос на продвижение отправлен", + "promotionSuccessDescription": "v{{version}} скилла «{{skill}}» добавлена в очередь рецензирования глобального продвижения.", + "promotionDuplicateTitle": "Продвижение уже ожидает", + "promotionDuplicateDescription": "У этой версии уже есть ожидающий запрос на продвижение.", + "promotionAlreadyPromotedTitle": "Скилл уже продвинут", + "promotionAlreadyPromotedDescription": "Этот скилл уже продвинут в глобальное пространство имён.", + "promotionErrorTitle": "Не удалось отправить запрос на продвижение", + "emptyTitle": "Скиллов пока нет", + "emptyDescription": "Опубликуйте свой первый скилл", + "emptyFilteredTitle": "Нет скиллов по этому фильтру", + "emptyFilteredDescription": { + "PENDING_REVIEW": "Сейчас нет скиллов, ожидающих рецензии.", + "PUBLISHED": "Сейчас нет опубликованных скиллов.", + "REJECTED": "Сейчас нет отклонённых скиллов.", + "ARCHIVED": "Сейчас нет архивных скиллов.", + "HIDDEN": "Сейчас нет скрытых скиллов." + }, + "publishSkill": "Опубликовать скилл" + }, + "myNamespaces": { + "title": "Мои пространства имён", + "subtitle": "Управление пространствами имён и командами", + "create": "Создать пространство имён", + "creating": "Создание...", + "createSubmit": "Создать", + "createDialogTitle": "Создать командное пространство имён", + "createDialogDescription": "После создания вы станете OWNER и сможете управлять участниками, рецензиями и публикацией скиллов.", + "createSlugLabel": "Slug пространства имён", + "createSlugPlaceholder": "напр. team-ml", + "createSlugHint": "2–64 символа: строчные буквы, цифры или дефисы, без дефиса в начале и конце.", + "createSlugRequired": "Slug пространства имён обязателен", + "createSlugLength": "Slug пространства имён должен быть от {{min}} до {{max}} символов", + "createSlugPattern": "Slug пространства имён может содержать только строчные буквы, цифры или дефисы", + "createSlugDoubleHyphen": "Slug пространства имён не может содержать подряд идущие дефисы", + "createSlugReserved": "«{{slug}}» зарезервирован. Выберите другой slug.", + "createDisplayNameLabel": "Отображаемое имя", + "createDisplayNamePlaceholder": "напр. Команда машинного обучения", + "createDisplayNameRequired": "Отображаемое имя обязательно", + "createDisplayNameLength": "Отображаемое имя — не более {{max}} символов", + "createDescriptionLabel": "Описание", + "createDescriptionPlaceholder": "Кратко опишите назначение пространства имён, кто им пользуется и как оно управляется", + "createDescriptionHint": "Необязательно, до 512 символов.", + "createDescriptionLength": "Описание — не более {{max}} символов", + "createSuccessTitle": "Пространство имён создано", + "createSuccessDescription": "«{{name}}» готово. Можно управлять участниками или публиковать скиллы.", + "createErrorTitle": "Не удалось создать пространство имён", + "typeGlobal": "Глобальное", + "typeTeam": "Команда", + "roleLabel": "Текущая роль", + "roleUnknown": "Неизвестно", + "manageMembers": "Участники", + "reviewTasks": "Задачи рецензирования", + "freeze": "Заморозить", + "unfreeze": "Разморозить", + "archive": "В архив", + "restore": "Восстановить", + "delete": "Удалить", + "activeHint": "Это пространство имён полностью активно: можно управлять участниками, рецензиями и публикацией скиллов.", + "frozenHint": "Пространство имён заморожено. Участники могут его просматривать, но оно только для чтения: публикация и изменение состава недоступны.", + "archivedHint": "Пространство имён в архиве. Публичные точки входа скрыты, но его можно просмотреть и восстановить из панели управления.", + "immutableHint": "Это встроенное системное пространство имён и всегда только для чтения.", + "freezeConfirmTitle": "Заморозить пространство имён", + "freezeConfirmDescription": "«{{name}}» станет доступно только для чтения. Продолжить?", + "unfreezeConfirmTitle": "Разморозить пространство имён", + "unfreezeConfirmDescription": "После разморозки «{{name}}» снова получит обычное управление и публикацию.", + "archiveConfirmTitle": "Архивировать пространство имён", + "archiveConfirmDescription": "«{{name}}» будет скрыто из публичных точек входа и останется доступным только для восстановления из панели управления.", + "restoreConfirmTitle": "Восстановить пространство имён", + "restoreConfirmDescription": "«{{name}}» вернётся в активное рабочее состояние.", + "deleteConfirmTitle": "Удалить пространство имён", + "deleteConfirmDescription": "«{{name}}» будет удалено безвозвратно. Это действие нельзя отменить.", + "freezeSuccessTitle": "Пространство имён заморожено", + "freezeSuccessDescription": "«{{name}}» теперь только для чтения.", + "freezeErrorTitle": "Не удалось заморозить пространство имён", + "unfreezeSuccessTitle": "Пространство имён разморожено", + "unfreezeSuccessDescription": "«{{name}}» снова активно.", + "unfreezeErrorTitle": "Не удалось разморозить пространство имён", + "archiveSuccessTitle": "Пространство имён архивировано", + "archiveSuccessDescription": "«{{name}}» скрыто из публичных точек входа.", + "archiveErrorTitle": "Не удалось архивировать пространство имён", + "restoreSuccessTitle": "Пространство имён восстановлено", + "restoreSuccessDescription": "«{{name}}» снова работает в обычном режиме.", + "restoreErrorTitle": "Не удалось восстановить пространство имён", + "deleteSuccessTitle": "Пространство имён удалено", + "deleteSuccessDescription": "«{{name}}» удалено безвозвратно.", + "deleteErrorTitle": "Не удалось удалить пространство имён", + "emptyTitle": "Пространств имён пока нет", + "emptyDescription": "Создайте пространство имён, чтобы организовать скиллы" + }, + "tokens": { + "pageTitle": "Управление токенами", + "pageSubtitle": "Учётные данные доступа для CLI и API" + }, + "reviews": { + "title": "Центр рецензирования", + "subtitle": "Задачи рецензирования на платформе", + "typeSkill": "Рецензии скиллов", + "typeProfile": "Рецензии профилей", + "tabPending": "Ожидают", + "tabApproved": "Одобрены", + "tabRejected": "Отклонены", + "empty": "Нет задач рецензирования", + "colSkill": "Скилл", + "colVersion": "Версия", + "colSubmitter": "Отправитель", + "colSubmitTime": "Отправлено", + "colReviewer": "Рецензент", + "colReviewTime": "Рецензировано", + "sortLabel": "Порядок по времени", + "sortNewest": "Сначала новые", + "sortOldest": "Сначала старые", + "pageSummary": "Всего записей: {{total}}, страница {{page}}", + "prevPage": "Назад", + "nextPage": "Вперёд" + }, + "profileReview": { + "queueTitle": "Очередь рецензий профилей", + "queueSubtitle": "Обработка запросов на смену отображаемого имени: фильтры статуса, пагинация и действия рецензирования.", + "empty": "Нет задач рецензирования профилей", + "colUser": "Пользователь", + "colCurrentName": "Текущее имя", + "colRequestedName": "Запрошенное имя", + "colSubmittedAt": "Отправлено", + "colMachineResult": "Машинная проверка", + "colActions": "Действия", + "colReviewInfo": "Сведения о рецензии", + "approve": "Одобрить", + "reject": "Отклонить", + "approveSuccess": "Смена профиля одобрена", + "approveFailed": "Не удалось одобрить", + "rejectSuccess": "Смена профиля отклонена", + "rejectFailed": "Не удалось отклонить", + "confirmApproveTitle": "Подтвердить одобрение", + "confirmApproveDesc": "Одобрить смену «{{from}}» на «{{to}}»?", + "confirmRejectTitle": "Отклонить изменение", + "confirmRejectDesc": "Укажите причину отклонения.", + "rejectPlaceholder": "Причина отклонения...", + "userId": "ID пользователя", + "reviewer": "Рецензент", + "comment": "Комментарий", + "totalItems": "Всего записей рецензий профилей: {{total}}", + "sortLabel": "Порядок по времени", + "sortNewest": "Сначала новые", + "sortOldest": "Сначала старые", + "pageSummary": "Всего записей: {{total}}, страница {{page}}", + "prevPage": "Назад", + "nextPage": "Вперёд", + "tabPending": "Ожидают", + "tabApproved": "Одобрены", + "tabRejected": "Отклонены" + }, + "stars": { + "title": "Мои звёзды", + "subtitle": "Избранные скиллы", + "empty": "Избранных скиллов пока нет" + }, + "subscriptions": { + "title": "Мои подписки", + "subtitle": "Скиллы, на обновления которых вы подписаны", + "empty": "Вы ещё не подписались ни на один скилл." + }, + "promotions": { + "title": "Рецензирование продвижений", + "subtitle": "Запросы на продвижение командных скиллов в глобальное пространство имён", + "tabPending": "Ожидают", + "tabApproved": "Одобрены", + "tabRejected": "Отклонены", + "commentPlaceholder": "Комментарий рецензии (необязательно)", + "approve": "Одобрить", + "reject": "Отклонить", + "empty": "Нет запросов на продвижение", + "historyTableLabel": "История продвижений", + "colSkill": "Скилл", + "colVersion": "Версия", + "colSubmitter": "Отправитель", + "colReviewer": "Рецензент", + "colReviewedAt": "Рецензировано", + "colReviewComment": "Комментарий рецензии", + "sortReviewedTimeAsc": "Сортировка по времени рецензии по возрастанию", + "sortReviewedTimeDesc": "Сортировка по времени рецензии по убыванию", + "emptyValue": "-", + "versionTag": "v{{version}}", + "submitterTag": "Отправитель {{user}}", + "fileCountTag": "Файлов: {{count}}", + "packageSizeTag": "{{size}}", + "downloadCountTag": "Загрузок: {{value}}", + "starCountTag": "Звёзд: {{value}}" + }, + "adminUsers": { + "title": "Управление пользователями", + "subtitle": "Управление пользователями платформы и правами доступа", + "searchLabel": "Поиск пользователей", + "searchPlaceholder": "Поиск по имени пользователя или email...", + "searchHint": "Введите имя пользователя или email, затем нажмите Enter или «Поиск».", + "searchAction": "Поиск", + "clearSearch": "Очистить", + "filterLabel": "Фильтр по статусу", + "filterAll": "Все", + "filterActive": "Активные", + "filterPending": "Ожидающие", + "filterDisabled": "Отключённые", + "empty": "Нет данных о пользователях", + "colUsername": "Имя пользователя", + "colUserId": "ID пользователя", + "copyUserId": "Скопировать ID пользователя для {{username}}", + "colEmail": "Электронная почта", + "colStatus": "Статус", + "colRole": "Роль", + "colCreatedAt": "Создан", + "colActions": "Действия", + "statusActive": "Активен", + "statusPending": "Ожидает", + "statusDisabled": "Отключён", + "changeRole": "Изменить роль", + "approveUser": "Одобрить", + "disable": "Отключить", + "enable": "Включить", + "resetPassword": "Сбросить пароль", + "totalRecords": "Всего {{total}} записей, страница {{page}}", + "prevPage": "Назад", + "nextPage": "Вперёд", + "changeRoleTitle": "Изменить роль пользователя", + "changeRoleDesc": "Назначить новую роль пользователю {{username}}", + "roleLabel": "Роль", + "selectRole": "Выберите роль", + "roleUser": "Пользователь", + "roleReviewer": "Ревьюер", + "roleUserAdmin": "Администратор пользователей", + "roleAuditor": "Аудитор", + "roleSuperAdmin": "Суперадминистратор", + "confirmAction": "Подтвердить действие", + "confirmDisable": "Вы уверены, что хотите отключить пользователя {{username}}?", + "confirmEnable": "Вы уверены, что хотите включить пользователя {{username}}?", + "confirmResetPassword": "Отправить код подтверждения сброса пароля пользователю {{username}}?" + }, + "adminLabels": { + "title": "Управление метками", + "subtitle": "Управление определениями меток, видимостью, переводами и порядком в фильтрах.", + "createAction": "Создать метку", + "editAction": "Редактировать", + "deleteAction": "Удалить", + "saveAction": "Сохранить изменения", + "cancelAction": "Отмена", + "moveUp": "Выше", + "moveDown": "Ниже", + "empty": "Метки ещё не определены.", + "summaryDefinitionsTitle": "Определения", + "summaryVisibleTitle": "Видимые фильтры", + "summaryPrivilegedTitle": "Привилегированные метки", + "colLabel": "Метка", + "colType": "Тип", + "colVisibility": "Видимость", + "colSortOrder": "Порядок сортировки", + "colTranslations": "Переводы", + "colCreatedAt": "Создана", + "colActions": "Действия", + "typeRecommended": "Рекомендуемая", + "typePrivileged": "Привилегированная", + "visibilityVisible": "Видна в фильтрах", + "visibilityHidden": "Скрыта из фильтров", + "createDialogTitle": "Создать метку", + "createDialogDescription": "Задайте новую метку и переводы, которые будут отображаться в поиске и на экранах деталей.", + "editDialogTitle": "Редактировать метку", + "editDialogDescription": "Обновите правила отображения и переводы для этого определения метки.", + "deleteDialogTitle": "Удалить метку", + "deleteDialogDescription": "Удалить метку «{{slug}}» и снять её со всех привязанных скиллов.", + "formSlug": "Slug", + "formType": "Тип", + "formVisibility": "Видимость", + "formSortOrder": "Порядок сортировки", + "formTranslations": "Переводы", + "formTranslationsHint": "Укажите хотя бы одну пару локаль и отображаемое имя.", + "translationLocalePlaceholder": "Локаль, например en или zh-CN", + "translationDisplayNamePlaceholder": "Отображаемое имя", + "addTranslation": "Добавить перевод", + "removeTranslation": "Удалить", + "validationSlugTitle": "Требуется slug", + "validationSlugDescription": "Введите стабильный slug перед сохранением метки.", + "validationSlugPatternDescription": "Используйте только строчные буквы, цифры и одиночные дефисы. Slug должен начинаться и заканчиваться буквой или цифрой.", + "validationTranslationsTitle": "Требуются переводы", + "validationTranslationsDescription": "Нужна хотя бы одна полная пара локаль и отображаемое имя.", + "validationDuplicateLocaleDescription": "Каждая локаль перевода может встречаться только один раз.", + "createSuccessTitle": "Метка создана", + "createSuccessDescription": "Определение метки теперь доступно.", + "createErrorTitle": "Не удалось создать метку", + "updateSuccessTitle": "Метка обновлена", + "updateSuccessDescription": "Определение метки обновлено.", + "updateErrorTitle": "Не удалось обновить метку", + "deleteSuccessTitle": "Метка удалена", + "deleteSuccessDescription": "Определение метки удалено.", + "deleteErrorTitle": "Не удалось удалить метку", + "sortSuccessTitle": "Порядок сортировки обновлён", + "sortSuccessDescription": "Порядок меток сохранён.", + "sortErrorTitle": "Не удалось обновить порядок сортировки", + "fallbackErrorDescription": "Действие с меткой не удалось выполнить." + }, + "auditLog": { + "title": "Журнал аудита", + "subtitle": "Просмотр записей о системных операциях", + "filterAll": "Все", + "filterCliPublish": "Публикация CLI", + "filterCompatPublish": "Совместимая публикация", + "filterReviewSubmit": "Ревью отправлено", + "filterReviewApprove": "Ревью одобрено", + "filterReviewReject": "Ревью отклонено", + "filterPromotionSubmit": "Продвижение отправлено", + "filterPromotionApprove": "Продвижение одобрено", + "filterPromotionReject": "Продвижение отклонено", + "filterReportSkill": "Скилл пожалован", + "filterResolveSkillReport": "Жалоба обработана", + "filterDismissSkillReport": "Жалоба отклонена", + "filterHideSkill": "Скилл скрыт", + "filterArchiveSkill": "Скилл архивирован", + "filterUnhideSkill": "Скилл показан", + "filterUnarchiveSkill": "Скилл восстановлен", + "filterYankVersion": "Версия отозвана", + "filterRebuildSearchIndex": "Индекс поиска перестроен", + "quickFilterSearchRebuild": "Только перестройки индекса поиска", + "clearFilters": "Сбросить фильтры", + "userIdPlaceholder": "ID пользователя...", + "requestIdPlaceholder": "ID запроса...", + "ipPlaceholder": "IP-адрес...", + "resourceTypePlaceholder": "Тип ресурса...", + "resourceIdPlaceholder": "ID ресурса...", + "empty": "Нет записей аудита", + "colTime": "Время", + "colAction": "Действие", + "colUserId": "ID пользователя", + "colUsername": "Имя пользователя", + "colIp": "IP-адрес", + "colDetail": "Подробности", + "totalRecords": "Всего {{total}} записей, страница {{page}}", + "prevPage": "Назад", + "nextPage": "Вперёд" + }, + "profile": { + "title": "Настройки профиля", + "subtitle": "Управление отображаемым именем и личной информацией.", + "displayName": "Отображаемое имя", + "email": "Электронная почта", + "resetPassword": "Сбросить пароль", + "edit": "Редактировать", + "save": "Сохранить", + "saving": "Сохранение...", + "cancel": "Отмена", + "successTitle": "Профиль обновлён", + "successDescription": "Ваше отображаемое имя обновлено.", + "pendingReviewTitle": "Отправлено на проверку", + "pendingReviewDescription": "Изменение отображаемого имени ожидает проверки и вступит в силу после одобрения.", + "defaultError": "Не удалось обновить профиль. Попробуйте снова.", + "validation": { + "length": "Отображаемое имя должно содержать 2–32 символа.", + "pattern": "Отображаемое имя может содержать только китайские и английские символы, цифры, пробелы, подчёркивания и дефисы." + }, + "pendingReview": "Изменение отображаемого имени на «{{name}}» ожидает проверки.", + "rejected": "Изменение отображаемого имени отклонено.", + "rejectedReason": "Причина: {{reason}}", + "reviewHint": "Некоторые изменения требуют проверки администратором перед вступлением в силу.", + "partiallyAppliedTitle": "Применено частично", + "partiallyAppliedDescription": "Часть изменений применена сразу. Остальные ожидают проверки администратором.", + "noChanges": "Нет изменений для сохранения.", + "userId": "ID пользователя" + }, + "security": { + "title": "Настройки безопасности", + "subtitle": "Обновите пароль, если включён вход через локальную учётную запись.", + "currentPassword": "Текущий пароль", + "newPassword": "Новый пароль", + "currentPasswordRequired": "Введите текущий пароль", + "newPasswordRequired": "Введите новый пароль", + "invalidCurrentPassword": "Текущий пароль неверен", + "success": "Пароль успешно изменён", + "successTitle": "Пароль успешно изменён", + "successDescription": "Войдите снова с новым паролем.", + "defaultError": "Не удалось изменить пароль", + "unavailableTitle": "Смена пароля недоступна для этой учётной записи.", + "unavailableDescription": "Эта учётная запись входит через внешнего поставщика идентификации или не имеет локальных учётных данных пароля.", + "submitting": "Отправка...", + "submit": "Обновить пароль" + }, + "accounts": { + "initiateTitle": "Начать объединение учётных записей", + "initiateDesc": "Введите идентификатор вторичной учётной записи. Поддерживается локальное имя пользователя или формат `provider:subject` для внешних идентичностей.", + "secondaryLabel": "Вторичный идентификатор", + "secondaryPlaceholder": "например: other_user или github:123456", + "initiating": "Инициализация...", + "initiate": "Начать объединение", + "initiateSuccess": "Запрос на объединение создан, secondary={{secondaryUserId}}", + "initiateError": "Не удалось начать объединение", + "verifyTitle": "Проверить и завершить объединение", + "verifyDesc": "Сначала завершите проверку токена, затем подтвердите выполнение миграции данных.", + "mergeRequestId": "ID запроса на объединение", + "verificationToken": "Токен проверки", + "verifying": "Проверка...", + "verify": "Завершить объединение", + "verifySuccess": "Проверка успешна, подтвердите выполнение объединения", + "verifyError": "Проверка объединения не удалась", + "confirming": "Подтверждение...", + "confirm": "Подтвердить и завершить объединение", + "confirmSuccess": "Объединение учётных записей завершено", + "confirmError": "Подтверждение объединения не удалось" + }, + "namespace": { + "notFound": "Пространство имён не найдено", + "skillList": "Скиллы", + "emptyTitle": "Нет скиллов", + "emptyDescription": "В этом пространстве имён ещё не опубликовано ни одного скилла" + }, + "skillDetail": { + "back": "Назад", + "notFound": "Скилл не найден", + "notFoundDesc": "Этот скилл мог быть удалён или никогда не существовал", + "loginRequired": "Требуется вход", + "loginRequiredDesc": "Этот скилл является приватным. Войдите, чтобы просмотреть подробности.", + "accessDenied": "Доступ запрещён", + "accessDeniedDesc": "У вас нет прав на просмотр этого скилла", + "tabReadme": "README", + "tabOverview": "Обзор", + "tabFiles": "Файлы", + "tabVersions": "Версии", + "noReadme": "Нет README", + "readmeUnavailable": "README временно недоступен. Попробуйте позже или загрузите эту версию скилла заново.", + "documentationSource": "Источник: {{path}}", + "documentationUnavailableTitle": "Документация недоступна", + "documentationUnavailable": "Не удалось загрузить файл документации. Вы всё ещё можете просмотреть содержимое пакета в списке файлов.", + "packageLinkMissingTitle": "Файл не найден", + "packageLinkMissingDescription": "Эта ссылка указывает на файл, которого нет в текущей версии скилла.", + "authorLabel": "Автор: {{name}}", + "expandOverview": "Развернуть полный обзор", + "collapseOverview": "Свернуть содержимое", + "noDocumentationTitle": "Нет документации пакета", + "noDocumentationDescription": "В этой версии скилла нет читаемого файла обзора.", + "noDocumentationHint": "Многие скиллы содержат только исполняемые файлы. Вы можете продолжить со списком файлов и сведениями о версиях ниже.", + "summaryLabel": "Краткое описание", + "noFiles": "Нет файлов", + "noVersions": "Нет версий", + "fileCount": "{{count}} файлов", + "version": "Версия", + "downloads": "Загрузки", + "rating": "Оценка", + "ratingNone": "Нет", + "namespaceLabel": "Пространство имён", + "loginToRate": "Войдите, чтобы отметить звёздочкой и оценить", + "install": "Установить", + "installMethodClawhub": "ClawHub CLI", + "installMethodSkillhub": "SkillHub CLI", + "download": "Скачать", + "labelsSectionTitle": "Метки", + "labelsSectionDescription": "Прикрепите или удалите рекомендуемые метки, которые помогают пользователям фильтровать и находить этот скилл.", + "labelsSectionDescriptionSuperAdmin": "Управляйте всеми метками этого скилла, включая привилегированные, скрытые из публичных фильтров.", + "currentLabelsTitle": "Текущие метки", + "availableLabelsTitle": "Доступные метки", + "noLabelsAssigned": "К этому скиллу ещё не прикреплено ни одной метки.", + "loadingAvailableLabels": "Загрузка доступных меток...", + "noAvailableLabels": "Сейчас нельзя прикрепить больше меток.", + "addLabel": "Добавить {{label}}", + "removeLabel": "Удалить", + "labelRestrictedHint": "Только суперадминистратор", + "labelAttachSuccessTitle": "Метка прикреплена", + "labelAttachSuccessDescription": "Метки скилла обновлены.", + "labelAttachErrorTitle": "Не удалось прикрепить метку", + "labelDetachSuccessTitle": "Метка удалена", + "labelDetachSuccessDescription": "Метки скилла обновлены.", + "labelDetachErrorTitle": "Не удалось удалить метку", + "labelActionFallbackError": "Действие с меткой не удалось выполнить.", + "lifecycle": "Жизненный цикл", + "lifecycleHint": "Вы можете архивировать этот скилл. Архивированные скиллы скрыты от обычных пользователей и недоступны для скачивания.", + "archivedPublishHint": "Этот скилл архивирован. Восстановите его перед публикацией новой версии.", + "archivedInstallHint": "Этот скилл архивирован и недоступен для публичного скачивания.", + "statusActive": "Активен", + "statusArchived": "Архивирован", + "statusHidden": "Скрыт", + "versionStatusDraft": "Черновик", + "versionStatusScanning": "Сканирование", + "versionStatusScanFailed": "Сканирование не удалось", + "versionStatusUploaded": "Загружена", + "versionStatusPendingReview": "Ожидает ревью", + "versionStatusPublished": "Опубликована", + "versionStatusRejected": "Отклонена", + "versionStatusYanked": "Отозвана", + "pendingPreviewBadge": "Предпросмотр ожидающей", + "rejectedBadge": "Ревью отклонено", + "pendingPreviewTitle": "Вы просматриваете ожидающую версию", + "pendingPreviewDescription": "Эта версия видна только вам. До одобрения ревью вы можете просмотреть README, файлы и сведения о версии, но не можете отмечать звёздочкой, оценивать, жаловаться или скачивать.", + "pendingPreviewInteractionHint": "Звёздочки, оценки и жалобы отключены, пока эта версия ожидает ревью.", + "rejectedPreviewDescription": "Эта версия отклонена и сейчас видна только вам. Обновите её по замечаниям ниже и опубликуйте снова.", + "rejectedPreviewInteractionHint": "Отклонённые версии для предпросмотра нельзя отмечать звёздочкой, оценивать или жаловаться на них.", + "rejectedFeedbackTitle": "Ревью отклонено", + "rejectedFeedbackLabel": "Комментарий ревьюера", + "rejectedFeedbackFallback": "Комментарий об отклонении не предоставлен.", + "governance": "Управление", + "processing": "Обработка...", + "archiveSkill": "Архивировать скилл", + "unarchiveSkill": "Восстановить скилл", + "deleteSkill": "Удалить скилл", + "deleteSkillContinue": "Продолжить", + "deleteSkillFinal": "Удалить безвозвратно", + "withdrawReview": "Отозвать ревью", + "hideSkill": "Скрыть скилл", + "unhideSkill": "Показать скилл", + "archiveConfirmTitle": "Архивировать скилл", + "archiveConfirmDescription": "После архивирования обычные пользователи больше не смогут просматривать или скачивать «{{skill}}». Продолжить?", + "unarchiveConfirmTitle": "Восстановить скилл", + "unarchiveConfirmDescription": "«{{skill}}» снова станет видимым и сможет публиковать новые версии после восстановления.", + "deleteSkillConfirmTitle": "Безвозвратно удалить скилл", + "deleteSkillConfirmDescription": "Будут удалены все версии, файлы и пакеты загрузки для «{{skill}}». Это действие нельзя отменить.", + "deleteSkillInputTitle": "Введите slug скилла для подтверждения", + "deleteSkillInputDescription": "Введите slug скилла «{{slug}}», чтобы продолжить.", + "deleteSkillInputPlaceholder": "Введите slug скилла", + "deleteSkillWarning": "Это физическое жёсткое удаление. Исторические версии и пакеты будут удалены безвозвратно.", + "archiveSuccessTitle": "Скилл архивирован", + "archiveSuccessDescription": "«{{skill}}» архивирован.", + "archiveErrorTitle": "Не удалось архивировать скилл", + "unarchiveSuccessTitle": "Скилл восстановлен", + "unarchiveSuccessDescription": "«{{skill}}» восстановлен.", + "unarchiveErrorTitle": "Не удалось восстановить скилл", + "deleteSkillSuccessTitle": "Скилл удалён", + "deleteSkillSuccessDescription": "«{{skill}}» безвозвратно удалён.", + "deleteSkillErrorTitle": "Не удалось удалить скилл", + "withdrawReviewConfirmTitle": "Отозвать ревью", + "withdrawReviewConfirmDescription": "После отзыва версия {{version}} покинет очередь ревью и вернётся в черновик, чтобы её можно было отправить снова позже.", + "withdrawReviewSuccessTitle": "Ревью отозвано", + "withdrawReviewSuccessDescription": "Версия {{version}} отозвана из ревью.", + "withdrawReviewErrorTitle": "Не удалось отозвать ревью", + "confirmPublish": "Подтвердить публикацию", + "confirmPublishDialogTitle": "Подтвердить публикацию", + "confirmPublishDialogDescription": "Опубликовать версию {{version}} как приватный скилл? Она будет доступна вам для скачивания и установки, но не видна на маркетплейсе.", + "confirmPublishSuccessTitle": "Версия опубликована", + "confirmPublishSuccessDescription": "Версия {{version}} опубликована как приватный скилл.", + "confirmPublishErrorTitle": "Не удалось подтвердить публикацию", + "submitReview": "Отправить на ревью", + "submitReviewDialogTitle": "Отправить на ревью", + "submitReviewDialogDescription": "Отправить версию {{version}} на публичное ревью? После одобрения она станет видимой на маркетплейсе.", + "submitReviewSuccessTitle": "Отправлено на ревью", + "submitReviewSuccessDescription": "Версия {{version}} отправлена на ревью.", + "submitReviewErrorTitle": "Не удалось отправить на ревью", + "deleteVersion": "Удалить версию", + "deleteVersionConfirmTitle": "Удалить версию", + "deleteVersionConfirmDescription": "Версию {{version}} нельзя будет восстановить после удаления. Продолжить?", + "deleteVersionSuccessTitle": "Версия удалена", + "deleteVersionSuccessDescription": "Версия {{version}} удалена.", + "deleteVersionErrorTitle": "Не удалось удалить версию", + "currentVersion": "Текущая", + "currentPublicVersion": "Текущая публичная версия", + "pendingReviewSectionTitle": "Версия на ревью", + "pendingReviewSectionDescription": "v{{pendingVersion}} сейчас на ревью. Публичная и устанавливаемая версия остаётся v{{publishedVersion}}.", + "pendingReviewVersionLabel": "Ожидающая версия", + "pendingReviewStatusLabel": "Статус ревью", + "pendingReviewStatusValue": "На ревью", + "lifecyclePublicVersionLabel": "Текущая публичная версия", + "lifecycleNoPublishedVersion": "Нет опубликованной версии", + "lifecyclePendingVersionLabel": "Версия, ожидающая ревью", + "lifecyclePendingVersionValue": "v{{version}} на ревью", + "lifecycleNoPendingVersion": "Нет версии, ожидающей ревью", + "lifecycleContainerStateLabel": "Состояние контейнера скилла", + "compareVersions": "Сравнить", + "compareDialogTitle": "Сравнение версий", + "compareDialogDescription": "Сравнить v{{source}} с v{{target}}.", + "compareSourceLabel": "Выбранная версия", + "compareTargetLabel": "Сравнение с", + "versionCompareUnavailableTitle": "Недостаточно версий для сравнения", + "versionCompareUnavailableDescription": "Опубликуйте хотя бы две версии, прежде чем использовать сравнение версий.", + "metadataChanges": "Изменения метаданных", + "noMetadataChanges": "Нет изменений метаданных", + "readmeChange": "Изменение README", + "readmeChanged": "Содержимое README изменено", + "readmeUnchanged": "Содержимое README не изменено", + "fileChanges": "Изменения файлов", + "filesAdded": "Добавлено", + "filesRemoved": "Удалено", + "filesChanged": "Изменено", + "expandDiff": "Показать diff", + "collapseDiff": "Скрыть diff", + "binaryFileNotice": "Бинарный файл — нельзя отобразить diff", + "largeFileWarning": "Этот файл большой. Отрисовка может быть медленной.", + "diffLoadError": "Не удалось загрузить содержимое файла", + "diffRetry": "Повторить", + "diffViewUnified": "Объединённый", + "diffViewSplit": "Разделённый", + "rereleaseVersion": "Переиздать", + "rereleaseDialogTitle": "Переиздание из версии", + "rereleaseDialogDescription": "Создать новую опубликованную версию на основе v{{version}}.", + "rereleaseSourceVersion": "Исходная версия", + "rereleaseTargetVersion": "Новая версия", + "rereleaseSuccessTitle": "Версия переиздана", + "rereleaseSuccessDescription": "Создана v{{target}} из v{{source}}.", + "rereleaseErrorTitle": "Не удалось переиздать версию", + "rereleaseWarningTitle": "Предупреждение перед публикацией", + "rereleaseWarningDescription": "Обнаружены следующие напоминания о рисках. Если вы их понимаете и всё равно хотите продолжить, можно продолжить переиздание.", + "rereleaseWarningConfirm": "Продолжить переиздание", + "yankVersion": "Отозвать текущую версию", + "promoteToGlobal": "Продвинуть в Global", + "promotionSectionTitle": "Продвинуть в Global", + "promotionSectionDescription": "Отправить текущую опубликованную версию v{{version}} на ревью в глобальное пространство имён.", + "promotionConfirmTitle": "Отправить запрос на продвижение", + "promotionConfirmDescription": "Отправить v{{version}} скилла «{{skill}}» на продвижение в глобальное пространство имён?", + "promotionSuccessTitle": "Запрос на продвижение отправлен", + "promotionSuccessDescription": "v{{version}} скилла «{{skill}}» теперь в очереди ревью на продвижение в Global.", + "promotionDuplicateTitle": "Продвижение уже ожидает", + "promotionDuplicateDescription": "Для этой версии уже есть ожидающий запрос на продвижение.", + "promotionAlreadyPromotedTitle": "Скилл уже продвинут", + "promotionAlreadyPromotedDescription": "Этот скилл уже продвинут в глобальное пространство имён.", + "promotionErrorTitle": "Не удалось отправить запрос на продвижение", + "reportSkill": "Пожаловаться на скилл", + "reportedSkill": "Жалоба отправлена", + "reportDialogTitle": "Пожаловаться на скилл", + "reportDialogDescription": "Укажите причину, чтобы администраторы могли быстро проверить и принять меры.", + "reportReasonPlaceholder": "Причина, например нарушение политики, вводящий в заблуждение контент или нарушение прав", + "reportDetailsPlaceholder": "Дополнительные сведения (необязательно)", + "submitReport": "Отправить жалобу", + "reportReasonRequired": "Укажите причину жалобы", + "reportSuccessTitle": "Жалоба отправлена", + "reportSuccessDescription": "Администраторы скоро рассмотрят эту жалобу.", + "downloadErrorTitle": "Скачивание не удалось", + "reportErrorTitle": "Жалоба не отправлена", + "share": { + "button": "Поделиться", + "copied": "Скопировано", + "defaultDescription": "Полезный скилл" + } + }, + "skillCompare": { + "pageTitle": "Сравнение версий", + "filesChanged": "Изменено файлов: {{count}}", + "searchFiles": "Поиск файлов", + "errorLoadingCompare": "Не удалось загрузить сравнение версий", + "loadDiff": "Загрузить diff", + "loading": "Загрузка...", + "totalFiles": "Файлы", + "addedLines": "Добавленные строки", + "removedLines": "Удалённые строки", + "baseVersion": "Базовая версия", + "headVersion": "Целевая версия", + "fileList": "Список файлов", + "binaryFile": "Бинарный файл — нельзя отобразить diff", + "truncatedFile": "Вывод diff обрезан", + "notEnoughPublishedVersions": "Опубликуйте хотя бы две версии перед сравнением.", + "noFilesFound": "Подходящие файлы не найдены.", + "changeTypeAdded": "Добавлен", + "changeTypeModified": "Изменён", + "changeTypeRemoved": "Удалён" + }, + "reports": { + "title": "Жалобы на скиллы", + "subtitle": "Обработка жалоб на скиллы, отправленных пользователями", + "tabPending": "Ожидающие", + "tabResolved": "Обработанные", + "tabDismissed": "Отклонённые", + "empty": "Нет жалоб", + "reporter": "Автор жалобы", + "handledBy": "Обработал", + "resolve": "Обработать", + "resolveAndHide": "Обработать и скрыть", + "resolveAndArchive": "Обработать и архивировать", + "dismiss": "Отклонить", + "resolveConfirmTitle": "Обработать жалобу", + "resolveConfirmDescription": "Отметить жалобу на «{{skill}}» как обработанную?", + "resolveAndHideConfirmDescription": "Обработать жалобу на «{{skill}}» и скрыть скилл от публичного доступа?", + "resolveAndArchiveConfirmDescription": "Обработать жалобу на «{{skill}}» и архивировать скилл?", + "dismissConfirmTitle": "Отклонить жалобу", + "dismissConfirmDescription": "Отклонить жалобу на «{{skill}}»?", + "resolveSuccessTitle": "Жалоба обработана", + "resolveSuccessDescription": "Жалоба на «{{skill}}» отмечена как обработанная.", + "resolveAndHideSuccessTitle": "Жалоба обработана, скилл скрыт", + "resolveAndHideSuccessDescription": "«{{skill}}» скрыт после обработки жалобы.", + "resolveAndArchiveSuccessTitle": "Жалоба обработана, скилл архивирован", + "resolveAndArchiveSuccessDescription": "«{{skill}}» архивирован после обработки жалобы.", + "dismissSuccessTitle": "Жалоба отклонена", + "dismissSuccessDescription": "Жалоба на «{{skill}}» отклонена.", + "resolveErrorTitle": "Не удалось обработать жалобу", + "resolveAndHideErrorTitle": "Не удалось обработать жалобу и скрыть скилл", + "resolveAndArchiveErrorTitle": "Не удалось обработать жалобу и архивировать скилл", + "dismissErrorTitle": "Не удалось отклонить жалобу" + }, + "governance": { + "title": "Центр управления", + "subtitle": "Отслеживайте ревью, продвижения, жалобы и активность аудита в одном месте.", + "pendingReviews": "Ожидающие ревью", + "pendingPromotions": "Ожидающие продвижения", + "pendingReports": "Ожидающие жалобы", + "unreadNotifications": "Непрочитанные уведомления", + "inboxTitle": "Входящие управления", + "inboxSubtitle": "Единая очередь операционных задач, требующих действия.", + "tabAll": "Все", + "tabReview": "Ревью", + "tabPromotion": "Продвижения", + "tabReport": "Жалобы", + "emptyInbox": "Сейчас нет задач управления.", + "openItem": "Открыть", + "notificationsTitle": "Уведомления", + "notificationsSubtitle": "Недавние обновления управления, требующие вашего внимания.", + "emptyNotifications": "Уведомлений пока нет.", + "markRead": "Отметить как прочитанное", + "activityTitle": "Активность управления", + "activitySubtitle": "Недавние события аудита по ревью, продвижению, жалобам и действиям жизненного цикла.", + "emptyActivity": "Нет недавней активности управления.", + "unknownActor": "Неизвестный участник", + "searchMaintenanceTitle": "Обслуживание индекса поиска", + "searchMaintenanceDescription": "Перестроить полный индекс поиска скиллов. Это действие доступно только суперадминистраторам и предназначено для полной перезагрузки после изменения правил поиска.", + "searchMaintenanceHint": "Это может занять время. Не запускайте повторно слишком часто.", + "searchRebuildAction": "Перестроить полный индекс поиска", + "searchRebuildRunning": "Перестроение...", + "searchRebuildConfirmTitle": "Перестроить полный индекс поиска?", + "searchRebuildConfirmDescription": "Система перестроит документы поиска для всех скиллов по текущим правилам индексации. Операция может занять некоторое время.", + "searchRebuildSuccessTitle": "Перестроение индекса поиска запущено", + "searchRebuildSuccessDescription": "Система перестраивает полный индекс поиска по текущим правилам.", + "searchRebuildErrorTitle": "Не удалось перестроить индекс поиска" + }, + "members": { + "title": "Управление участниками", + "addMember": "Добавить участника", + "addingMember": "Добавление...", + "addDialogTitle": "Добавить участника пространства имён", + "addDialogDescription": "Найдите пользователей платформы или введите ID пользователя напрямую. Добавленные пользователи сразу смогут работать в этом пространстве имён.", + "searchLabel": "Поиск пользователей", + "searchPlaceholder": "Поиск по имени пользователя, email или ID пользователя", + "searchHint": "Введите не менее 2 символов для поиска. В результатах будет ID пользователя, которого можно добавить напрямую.", + "searchAction": "Поиск", + "searchTooShort": "Ключевые слова поиска должны содержать не менее 2 символов", + "searchResultsTitle": "Кандидаты", + "searchEmpty": "Нет активных пользователей для добавления", + "selectCandidate": "Использовать этого пользователя", + "manualUserIdLabel": "Ввести ID пользователя вручную", + "manualUserIdPlaceholder": "Например: user-123 или usr_abcd", + "manualUserIdHint": "Если вы уже знаете ID пользователя, можете добавить его здесь напрямую.", + "userIdRequired": "Требуется ID пользователя", + "roleLabel": "Роль участника", + "roleOwner": "OWNER", + "roleAdmin": "ADMIN", + "roleMember": "MEMBER", + "saveRole": "Сохранить роль", + "savingRole": "Сохранение...", + "changeRole": "Изменить роль", + "colUserId": "ID пользователя", + "colUsername": "Имя пользователя", + "colEmail": "Электронная почта", + "colRole": "Роль", + "colJoinedAt": "Дата вступления", + "colActions": "Действия", + "remove": "Удалить", + "empty": "Нет участников", + "namespaceNotFound": "Пространство имён не найдено", + "globalReadOnly": "Это встроенное системное пространство имён. Состав участников можно только просматривать, но нельзя изменять.", + "frozenReadOnly": "Это пространство имён заморожено. Вы можете просматривать участников, но не можете добавлять, удалять или менять роли.", + "archivedReadOnly": "Это пространство имён архивировано. Вы можете просматривать участников, но не можете менять состав, пока оно не будет восстановлено.", + "memberReadOnly": "Вы обычный участник этого пространства имён. Список участников можно просматривать, но изменять его могут только OWNER или ADMIN.", + "addSuccessTitle": "Участник добавлен", + "addSuccessDescription": "Пользователь {{userId}} добавлен в это пространство имён.", + "addErrorTitle": "Не удалось добавить участника", + "updateRoleSuccessTitle": "Роль участника обновлена", + "updateRoleSuccessDescription": "Пользователь {{userId}} теперь {{role}}.", + "updateRoleErrorTitle": "Не удалось обновить роль участника", + "removeConfirmTitle": "Удалить участника", + "removeConfirmDescription": "После удаления пользователь {{userId}} потеряет доступ к этому пространству имён.", + "removeSuccessTitle": "Участник удалён", + "removeSuccessDescription": "Пользователь {{userId}} удалён из этого пространства имён.", + "removeErrorTitle": "Не удалось удалить участника", + "searchEmptyMemberHint": "Если пользователь уже в этом пространстве имён, он здесь не появится — проверьте список участников.", + "transferOwnership": "Передать владение", + "transferDialogTitle": "Передать владение пространством имён", + "transferDialogDescription": "Передайте владение другому участнику. Вы станете ADMIN.", + "transferWarning": "Это действие нельзя отменить. После передачи вы больше не будете владельцем.", + "transferNewOwnerLabel": "Новый владелец", + "transferSelectPlaceholder": "Выберите участника", + "transferNoCandidates": "Нет других участников, которым можно передать владение.", + "transferConfirmSlugPrompt": "Введите slug пространства имён для подтверждения", + "transferConfirmSlugHint": "Введите {{slug}} для подтверждения", + "transferConfirmAction": "Передать владение", + "transferring": "Передача...", + "transferSuccessTitle": "Владение передано", + "transferSuccessDescription": "{{userId}} теперь владелец этого пространства имён.", + "transferErrorTitle": "Не удалось передать владение", + "batchImport": "Пакетный импорт", + "batchDialogTitle": "Пакетный импорт участников", + "batchDialogDescription": "Загрузите CSV-файл, чтобы добавить нескольких участников сразу. В каждой строке должны быть ID пользователя и роль.", + "batchStepUpload": "Загрузить CSV", + "batchStepPreview": "Предпросмотр", + "batchStepResults": "Результаты", + "batchDownloadTemplate": "Скачать шаблон CSV", + "batchDropHint": "Перетащите CSV-файл сюда или нажмите, чтобы выбрать", + "batchFormatHint": "Поддерживается только формат .csv", + "batchParseError": "Не удалось разобрать CSV-файл", + "batchEmptyFile": "CSV-файл пуст или не содержит допустимых строк", + "batchPreviewTitle": "Предпросмотр ({{count}} строк)", + "batchColUserId": "ID пользователя", + "batchColRole": "Роль", + "batchColStatus": "Статус", + "batchValidationMissingUserId": "Отсутствует ID пользователя", + "batchValidationInvalidRole": "Недопустимая роль (должна быть MEMBER или ADMIN)", + "batchValidationDuplicate": "Дублирующийся ID пользователя", + "batchValidationValid": "Готово", + "batchValidRows": "{{valid}} допустимых, {{invalid}} недопустимых", + "batchSubmitting": "Импорт...", + "batchSubmit": "Импортировать {{count}} участников", + "batchResultTitle": "Результаты импорта", + "batchResultSummary": "{{success}} успешно, {{failure}} с ошибкой из {{total}} всего", + "batchResultSuccess": "Добавлен", + "batchResultAlreadyMember": "Уже участник", + "batchResultUserNotFound": "Пользователь не найден", + "batchResultInvalidRole": "Недопустимая роль", + "batchResultUnknownError": "Неизвестная ошибка", + "batchDone": "Готово", + "batchBack": "Назад" + }, + "namespaceEdit": { + "editButton": "Редактировать", + "dialogTitle": "Редактировать пространство имён", + "displayNameLabel": "Отображаемое имя", + "displayNameRequired": "Требуется отображаемое имя", + "descriptionLabel": "Описание", + "saveAction": "Сохранить", + "saving": "Сохранение...", + "saveSuccess": "Пространство имён успешно обновлено", + "saveErrorTitle": "Не удалось обновить пространство имён" + }, + "apiError": { + "unauthorized": "Сессия истекла, войдите снова", + "forbidden": "У вас нет прав для этого действия", + "notFound": "Запрошенный ресурс не найден", + "serverError": "Ошибка сервера, повторите попытку позже", + "networkError": "Сбой сетевого подключения, проверьте сеть", + "unknown": "Операция не выполнена" + }, + "copyButton": { + "copied": "Скопировано", + "copy": "Копировать" + }, + "createToken": { + "title": "Создать API-токен", + "description": "Создать новый API-токен для доступа через CLI или API", + "nameLabel": "Имя токена", + "namePlaceholder": "например: my-cli-token", + "nameRequired": "Укажите имя токена", + "nameTooLong": "Имя токена не более {{max}} символов", + "nameDuplicate": "У вас уже есть токен с таким именем", + "creating": "Создание...", + "create": "Создать", + "successTitle": "Токен создан", + "successDescription": "Скопируйте и сохраните токен сейчас. Он показывается и копируется только при создании. Если понадобится снова — создайте новый токен.", + "tokenLabel": "Токен", + "nameDisplay": "Имя", + "expirationLabel": "Срок действия", + "expirationHint": "По умолчанию токены не истекают. Можно задать автоматический срок действия.", + "expirationNever": "Без срока", + "expiration7d": "Истекает через 7 дней", + "expiration30d": "Истекает через 30 дней", + "expiration90d": "Истекает через 90 дней", + "expirationCustom": "Своя дата и время", + "expiresAtRequired": "Выберите свой срок действия", + "expiresAtDisplay": "Истекает", + "copyToken": "Копировать токен", + "copySuccess": "Токен скопирован в буфер обмена", + "copyFailed": "Не удалось скопировать токен. Повторите попытку." + }, + "dialog": { + "confirm": "Подтвердить", + "cancel": "Отмена", + "delete": "Удалить", + "close": "Закрыть" + }, + "error": { + "auth": { + "local": { + "invalidCredentials": "Неверное имя пользователя или пароль", + "accountDisabled": "Эта учётная запись отключена", + "accountPending": "Эта учётная запись ожидает активации", + "accountMerged": "Эта учётная запись объединена и больше не может использоваться для входа", + "locked": "Слишком много неудачных попыток. Повторите позже" + }, + "direct": { + "disabled": "Совместимость прямой аутентификации отключена", + "providerUnsupported": "Этот способ входа не поддерживается" + }, + "sessionBootstrap": { + "disabled": "Инициализация сессии отключена", + "providerUnsupported": "Этот способ SSO-инициализации не поддерживается", + "notAuthenticated": "Внешняя аутентифицированная сессия не найдена" + } + } + }, + "filePreview": { + "loadError": "Не удалось загрузить файл", + "tooLarge": "Файл слишком большой для предпросмотра", + "binaryFile": "Бинарные файлы нельзя просмотреть", + "unsupported": "Этот тип файла не поддерживается для предпросмотра", + "downloadFile": "Скачать файл", + "downloadHint": "Скачать {{name}}", + "copy": "Копировать содержимое", + "copySuccess": "Скопировано в буфер обмена", + "close": "Закрыть" + }, + "fileTree": { + "title": "Файлы" + }, + "footer": { + "resources": "Ресурсы", + "docs": "Документация", + "api": "API", + "community": "Сообщество", + "privacy": "Политика конфиденциальности", + "terms": "Условия использования", + "copyright": "© 2026 SkillHub. Все права защищены." + }, + "layout": { + "footerDescription": "Реестр скиллов: эффективное управление и распространение скиллов для разработчиков." + }, + "loginButton": { + "loading": "Загрузка...", + "loginWith": "Войти через {{name}}" + }, + "namespaceStatus": { + "active": "Активно", + "frozen": "Заморожено", + "archived": "В архиве", + "frozenHint": "Это пространство имён сейчас только для чтения: нельзя публиковать, рецензировать или менять участников.", + "archivedHint": "Это пространство имён скрыто из публичных точек входа и видно только участникам на панели.", + "immutableHint": "Это встроенное системное пространство имён: управление и состав участников менять нельзя." + }, + "notification": { + "title": "Уведомления", + "empty": "Нет уведомлений", + "markAllRead": "Отметить все прочитанными", + "deleteRead": "Удалить прочитанные", + "viewAll": "Все уведомления", + "unread": "Непрочитанные", + "all": "Все", + "publish": "Публикация", + "review": "Ревью", + "promotion": "Продвижение", + "report": "Жалоба", + "timeAgo": "{{time}} назад", + "preferences": { + "title": "Настройки уведомлений", + "description": "Управление предпочтениями уведомлений", + "publish": "Уведомления о публикации", + "publishDesc": "Уведомлять, когда скилл опубликован", + "review": "Уведомления о ревью", + "reviewDesc": "Уведомлять об отправке, одобрении или отклонении ревью", + "promotion": "Уведомления о продвижении", + "promotionDesc": "Уведомлять о заявке, одобрении или отклонении продвижения", + "report": "Уведомления о жалобах", + "reportDesc": "Уведомлять о подаче или разрешении жалобы" + } + }, + "nsReviews": { + "title": "Ревью пространства имён", + "loadingNamespace": "Загрузка сведений о пространстве имён", + "reviewsFor": "Задачи ревью для {{name}}", + "empty": "Нет записей ревью", + "version": "Версия {{version}}", + "tabPending": "Ожидают", + "tabApproved": "Одобрено", + "tabRejected": "Отклонено", + "sortLabel": "Порядок по времени", + "sortNewest": "Сначала новые", + "sortOldest": "Сначала старые", + "openReview": "Открыть ревью", + "pageSummary": "Всего записей: {{total}}, страница {{page}}", + "prevPage": "Назад", + "nextPage": "Вперёд", + "globalReadOnly": "Это встроенное системное пространство имён. История ревью видна, но новые действия ревью недоступны.", + "frozenReadOnly": "Это пространство имён заморожено. Историю ревью можно смотреть, но обрабатывать задачи нельзя.", + "archivedReadOnly": "Это пространство имён в архиве. Историю ревью можно смотреть, но обрабатывать задачи нельзя, пока оно не восстановлено." + }, + "pagination": { + "prev": "Назад", + "next": "Вперёд", + "pagePrefix": "Стр.", + "pageSuffix": "", + "goToPage": "Перейти на страницу {{page}}" + }, + "publish": { + "title": "Опубликовать скилл", + "subtitle": "Загрузить пакет скилла в SkillHub", + "reviewNotice": { + "title": "Уведомление о ревью", + "description": "Отправленные пакеты скиллов проходят ревью администратора перед публикацией." + }, + "namespace": "Пространство имён", + "selectNamespace": "Выберите пространство имён", + "visibility": "Видимость", + "visibilityOptions": { + "public": "Публичный", + "namespaceOnly": "Только пространство имён", + "loggedInUsersOnly": "Только вошедшие пользователи", + "private": "Приватный" + }, + "file": "Файл пакета скилла", + "removeSelectedFile": "Убрать выбранный файл", + "publishing": "Публикация...", + "confirm": "Подтвердить публикацию", + "success": "Успешно опубликовано", + "successDescription": "{{skill}} отправлен на ревью и станет доступен после одобрения администратором", + "publishedTitle": "Успешно опубликовано", + "publishedDescription": "{{skill}} уже доступен для скачивания", + "pendingReviewTitle": "Отправлено на ревью", + "pendingReviewDescription": "{{skill}} отправлен на ревью и станет доступен после одобрения администратором", + "error": "Публикация не удалась", + "timeoutTitle": "Истекло время публикации", + "timeoutDescription": "Запрос на публикацию занял слишком много времени. Позже проверьте список скиллов или повторите попытку.", + "versionExistsTitle": "Версия уже существует", + "versionExistsDescription": "Эта версия скилла уже опубликована. Обновите версию в SKILL.md, пересоберите пакет и загрузите снова.", + "precheckFailedTitle": "Проверка перед публикацией не пройдена", + "precheckFailedDescription": "В пакете, похоже, есть секрет, токен или пароль. Замените реальные учётные данные на плейсхолдеры и повторите попытку.", + "warningConfirmTitle": "Предупреждение перед публикацией", + "warningConfirmDescription": "Обнаружены следующие напоминания о рисках. Если вы их понимаете и всё равно хотите продолжить — можно опубликовать.", + "warningConfirmContinue": "Продолжить публикацию", + "warningConfirmCancel": "Вернуться и исправить", + "frontmatterFailedTitle": "Неверный формат SKILL.md", + "frontmatterFailedDescription": "Проверьте YAML frontmatter в начале SKILL.md. Если значение поля содержит двоеточие, заключите его в кавычки.", + "selectRequired": "Выберите пространство имён и файл" + }, + "ratingInput": { + "yourRating": "Ваша оценка: {{score}} зв." + }, + "review": { + "detail": "Детали ревью", + "id": "ID ревью", + "backToList": "К списку", + "namespace": "Пространство имён / Slug", + "version": "Версия", + "status": "Статус", + "statusPending": "Ожидает", + "statusApproved": "Одобрено", + "statusRejected": "Отклонено", + "submitter": "Отправитель", + "submitTime": "Отправлено", + "reviewer": "Рецензент", + "reviewTime": "Рассмотрено", + "reviewComment": "Комментарий ревью", + "actions": "Действия ревью", + "commentLabel": "Комментарий (необязательно)", + "commentPlaceholder": "Введите комментарий ревью...", + "approve": "Одобрить", + "reject": "Отклонить", + "confirmReject": "Подтвердить отклонение", + "cancelReject": "Отмена", + "rejectReasonRequired": "При отклонении нужна причина", + "approveTitle": "Одобрить ревью", + "approveDescription": "Одобрить это ревью?", + "approveConfirm": "Одобрить", + "rejectTitle": "Отклонить ревью", + "rejectDescription": "Отклонить это ревью?", + "rejectConfirm": "Отклонить", + "approveSuccess": "Ревью одобрено", + "approveFailed": "Не удалось одобрить", + "rejectSuccess": "Ревью отклонено", + "rejectFailed": "Не удалось отклонить", + "approveDisabledScanning": "Сканирование безопасности ещё выполняется. Одобрение станет доступно после завершения.", + "notFound": "Задача ревью не найдена", + "skillDetailTitle": "Карточка скилла", + "skillDetailDescription": "Разделы ниже зафиксированы на ожидающей версии, привязанной к этой задаче ревью: {{skill}}", + "skillDetailError": "Не удалось загрузить карточку скилла для этого ревью.", + "activeReviewVersion": "Версия на ревью", + "downloadSkillZip": "Скачать ZIP скилла", + "noDocumentation": "В этой версии на ревью нет читаемого файла документации." + }, + "routeGuard": { + "forbiddenTitle": "У вас нет прав для просмотра этой страницы", + "forbiddenDescription": "Права вашей учётной записи изменились. Возврат на предыдущую страницу." + }, + "securityAudit": { + "title": "Аудит безопасности", + "dialogDescription": "Подробные результаты сканирования безопасности для этой версии.", + "findings": "Находки", + "findingsCount": "Находок: {{count}}", + "totalFindings": "Всего находок: {{count}}", + "scanDuration": "{{seconds}} с", + "statusScanning": "Сканирование", + "statusScanFailed": "Сканирование не удалось", + "remediation": "Рекомендации", + "viewDetails": "Подробности", + "verdict": { + "SAFE": "Безопасно", + "SUSPICIOUS": "Подозрительно", + "DANGEROUS": "Опасно", + "BLOCKED": "Высокий риск" + }, + "severity": { + "CRITICAL": "Критический", + "HIGH": "Высокий", + "MEDIUM": "Средний", + "LOW": "Низкий", + "INFO": "Инфо" + } + }, + "skillCard": { + "starred": "В избранном", + "starredAction": "Нажмите, чтобы убрать из избранного", + "unstarTitle": "Убрать из избранного", + "unstarDescription": "Убрать «{{name}}» из избранных скиллов?", + "unstarConfirm": "Убрать" + }, + "starButton": { + "starred": "В избранном", + "star": "В избранное" + }, + "subscribeButton": { + "subscribed": "Подписка оформлена", + "subscribe": "Подписаться" + }, + "toast": { + "success": "Успех", + "error": "Ошибка", + "warning": "Предупреждение", + "info": "Инфо" + }, + "token": { + "title": "API-токены", + "createNew": "Создать токен", + "copyHint": "Из соображений безопасности открытый текст токена можно скопировать только один раз при создании. Позже его нельзя просмотреть или скопировать снова. Если токен нужен — создайте новый.", + "empty": "Токены ещё не созданы", + "emptyHint": "Нажмите кнопку выше, чтобы создать первый токен", + "name": "Имя", + "prefix": "Префикс токена", + "createdAt": "Создан", + "lastUsed": "Последнее использование", + "expiresAt": "Истекает", + "neverExpires": "Без срока", + "actions": "Действия", + "copy": "Копировать", + "copySuccess": "Токен скопирован в буфер обмена", + "copyFailed": "Не удалось скопировать токен. Повторите попытку.", + "copyUnavailableTitle": "Открытый текст старого токена нельзя скопировать снова", + "copyUnavailableDescription": "Открытый текст токена показывается только один раз после создания. Если его нет — создайте новый токен.", + "editExpiration": "Изменить срок", + "editExpirationTitle": "Изменить срок действия токена", + "editExpirationDescription": "Задайте новый срок действия для токена «{{name}}».", + "saveExpiration": "Сохранить срок", + "updatingExpiration": "Сохранение...", + "updateExpirationSuccess": "Срок действия токена обновлён", + "updateExpirationFailed": "Не удалось обновить срок действия токена", + "delete": "Удалить", + "deleteTitle": "Удалить токен", + "deleteDescription": "Удалить токен «{{name}}»? Это действие нельзя отменить.", + "deleteSuccess": "Токен удалён", + "deleteFailed": "Не удалось удалить", + "loading": "Загрузка..." + }, + "upload": { + "dropHint": "Отпустите для загрузки...", + "dragHint": "Перетащите ZIP-файл сюда или нажмите, чтобы выбрать", + "formatHint": "Поддерживается только формат .zip" + }, + "user": { + "menu": { + "dashboard": "Панель", + "mySkills": "Мои скиллы", + "myNamespaces": "Мои пространства имён", + "governance": "Центр управления", + "stars": "Избранное", + "subscriptions": "Мои подписки", + "reviews": "Управление ревью", + "promotions": "Управление продвижением", + "reports": "Управление жалобами", + "users": "Пользователи", + "labels": "Метки", + "auditLog": "Журнал аудита", + "security": "Безопасность", + "profile": "Профиль", + "notifications": "Уведомления", + "accounts": "Объединение учётных записей", + "logout": "Выйти" + } + }, + "routeError": { + "title": "Что-то пошло не так", + "description": "На этой странице произошла ошибка. Можно вернуться назад или перезагрузить раздел.", + "reset": "Попробовать снова" + } +} diff --git a/web/src/i18n/ru-locale.test.ts b/web/src/i18n/ru-locale.test.ts new file mode 100644 index 00000000..9f262ead --- /dev/null +++ b/web/src/i18n/ru-locale.test.ts @@ -0,0 +1,51 @@ +import { describe, expect, it } from 'vitest' +import en from './locales/en.json' +import ru from './locales/ru.json' + +function leafKeys(value: unknown, prefix = ''): string[] { + if (value !== null && typeof value === 'object' && !Array.isArray(value)) { + return Object.entries(value as Record).flatMap(([key, child]) => + leafKeys(child, prefix ? `${prefix}.${key}` : key), + ) + } + return [prefix] +} + +function placeholders(text: string): string[] { + return [...text.matchAll(/\{\{[^}]+\}\}/g)].map((match) => match[0]).sort() +} + +describe('russian locale', () => { + it('mirrors the english key tree', () => { + expect(leafKeys(ru).sort()).toEqual(leafKeys(en).sort()) + }) + + it('preserves interpolation placeholders', () => { + const enMap = Object.fromEntries(leafKeys(en).map((key) => { + const parts = key.split('.') + let cursor: unknown = en + for (const part of parts) { + cursor = (cursor as Record)[part] + } + return [key, String(cursor)] + })) + const mismatches: string[] = [] + for (const key of leafKeys(ru)) { + const parts = key.split('.') + let cursor: unknown = ru + for (const part of parts) { + cursor = (cursor as Record)[part] + } + if (placeholders(String(cursor)).join() !== placeholders(enMap[key] ?? '').join()) { + mismatches.push(key) + } + } + expect(mismatches).toEqual([]) + }) + + it('translates core navigation labels', () => { + expect(ru.nav.home).not.toBe(en.nav.home) + expect(ru.nav.home.length).toBeGreaterThan(0) + expect(ru.login.title.length).toBeGreaterThan(0) + }) +}) diff --git a/web/src/pages/notifications.tsx b/web/src/pages/notifications.tsx index 891c5732..858b0718 100644 --- a/web/src/pages/notifications.tsx +++ b/web/src/pages/notifications.tsx @@ -12,6 +12,7 @@ import { DashboardPageHeader } from '@/shared/components/dashboard-page-header' import { Pagination } from '@/shared/components/pagination' import { Button } from '@/shared/ui/button' import { Card } from '@/shared/ui/card' +import { formatRelativeTime } from '@/shared/lib/format-relative-time' const PAGE_SIZE = 20 @@ -29,19 +30,6 @@ function getCategoryKey(cat: Category): string { } } -function formatRelativeTime(dateStr: string, lang: string): string { - const diff = Date.now() - new Date(dateStr).getTime() - const minutes = Math.floor(diff / 60_000) - const hours = Math.floor(diff / 3_600_000) - const days = Math.floor(diff / 86_400_000) - const isChinese = lang.startsWith('zh') - if (minutes < 1) return isChinese ? '刚刚' : 'just now' - if (minutes < 60) return isChinese ? `${minutes}分钟` : `${minutes}m` - if (hours < 24) return isChinese ? `${hours}小时` : `${hours}h` - if (days < 30) return isChinese ? `${days}天` : `${days}d` - return new Date(dateStr).toLocaleDateString() -} - function CategoryBadge({ category }: { category: NotificationItem['category'] }) { const { t } = useTranslation() const colorMap: Record = { diff --git a/web/src/shared/components/language-switcher.tsx b/web/src/shared/components/language-switcher.tsx index d28f54e5..8feae28a 100644 --- a/web/src/shared/components/language-switcher.tsx +++ b/web/src/shared/components/language-switcher.tsx @@ -24,9 +24,10 @@ export function LanguageSwitcher({ className }: LanguageSwitcherProps) { const languages = [ { code: 'zh', name: '中文' }, { code: 'en', name: 'English' }, + { code: 'ru', name: 'Русский' }, ] - // 获取当前语言的主要代码(去掉地区代码) + // Primary language code only (strip region, e.g. ru-RU → ru). const currentLangCode = i18n.language?.split('-')[0] || 'zh' const currentLanguage = languages.find((lang) => lang.code === currentLangCode) || languages[0] diff --git a/web/src/shared/lib/api-error.ts b/web/src/shared/lib/api-error.ts index 218f38b1..2a75e719 100644 --- a/web/src/shared/lib/api-error.ts +++ b/web/src/shared/lib/api-error.ts @@ -30,6 +30,7 @@ function isAccountDisabledError(error: ApiError): boolean { const accountDisabledMessages = [ i18n.t('apiError.auth.accountDisabled'), i18n.getFixedT('en')('apiError.auth.accountDisabled'), + i18n.getFixedT('ru')('apiError.auth.accountDisabled'), i18n.getFixedT('zh')('apiError.auth.accountDisabled'), ] const normalizedServerMessage = (error.serverMessage ?? '').toLowerCase() @@ -43,6 +44,8 @@ function isAccountDisabledError(error: ApiError): boolean { || normalizedMessage.includes('disabled') || (error.serverMessage ?? '').includes('禁用') || error.message.includes('禁用') + || (error.serverMessage ?? '').toLowerCase().includes('отключ') + || error.message.toLowerCase().includes('отключ') } export function handleApiError(error: unknown): void { diff --git a/web/src/shared/lib/format-relative-time.ts b/web/src/shared/lib/format-relative-time.ts new file mode 100644 index 00000000..b0f02277 --- /dev/null +++ b/web/src/shared/lib/format-relative-time.ts @@ -0,0 +1,34 @@ +/** + * Formats a timestamp as a compact relative time string for notification UI. + * Mirrors the zh inline pattern; ru support added for the Russian locale. + */ +export function formatRelativeTime(dateStr: string, lang: string): string { + const diff = Date.now() - new Date(dateStr).getTime() + const minutes = Math.floor(diff / 60_000) + const hours = Math.floor(diff / 3_600_000) + const days = Math.floor(diff / 86_400_000) + const isChinese = lang.startsWith('zh') + const isRussian = lang.startsWith('ru') + + if (minutes < 1) { + if (isChinese) return '刚刚' + if (isRussian) return 'только что' + return 'just now' + } + if (minutes < 60) { + if (isChinese) return `${minutes}分钟` + if (isRussian) return `${minutes} мин` + return `${minutes}m` + } + if (hours < 24) { + if (isChinese) return `${hours}小时` + if (isRussian) return `${hours} ч` + return `${hours}h` + } + if (days < 30) { + if (isChinese) return `${days}天` + if (isRussian) return `${days} д` + return `${days}d` + } + return new Date(dateStr).toLocaleDateString(isRussian ? 'ru-RU' : undefined) +} From 0587c55f8b3a57400eb23b83547b074075276671 Mon Sep 17 00:00:00 2001 From: FenjuFu <92919259+FenjuFu@users.noreply.github.com> Date: Wed, 26 Aug 2026 19:43:13 +0800 Subject: [PATCH 13/15] docs(examples): add Python client and usage examples (#702) Add examples/python: a dependency-light (requests-only) SkillHubClient plus runnable examples covering search, resolve, download and publish against the REST API, and link them from the README Documentation sections. Serves the large Python-leaning audience and doubles as a reference SDK seed (see #701). Signed-off-by: FenjuFu --- README.md | 1 + README_zh.md | 1 + examples/python/README.md | 85 ++++++++++ examples/python/example_usage.py | 90 +++++++++++ examples/python/requirements.txt | 1 + examples/python/skillhub_client.py | 244 +++++++++++++++++++++++++++++ 6 files changed, 422 insertions(+) create mode 100644 examples/python/README.md create mode 100644 examples/python/example_usage.py create mode 100644 examples/python/requirements.txt create mode 100644 examples/python/skillhub_client.py diff --git a/README.md b/README.md index d787c1d9..3a01513a 100644 --- a/README.md +++ b/README.md @@ -64,6 +64,7 @@ with the Skill's source and the problem it solves, or submit a PR by following t - 📖 **[User Guide](https://iflytek.github.io/skillhub/)** — Skill publishing, search, CLI usage and other user guides - 🛠️ **[Developer Docs](https://zread.ai/iflytek/skillhub)** — Architecture, API reference, local development, deployment and operations +- 🐍 **[Python Examples](./examples/python)** — Search, download, and publish skills from Python via the REST API ## Highlights diff --git a/README_zh.md b/README_zh.md index 6f655763..90ee1ae9 100644 --- a/README_zh.md +++ b/README_zh.md @@ -50,6 +50,7 @@ Skill,欢迎分享给 SkillHub 社区,与大家一起丰富开放、实用 - 📖 **[用户指南](https://iflytek.github.io/skillhub/)** — 技能发布、搜索、CLI 使用等用户操作指南 - 🛠️ **[开发者文档](https://zread.ai/iflytek/skillhub)** — 架构设计、API 参考、本地开发、部署运维等技术文档 +- 🐍 **[Python 示例](./examples/python)** — 使用 REST API 在 Python 中搜索、下载和发布技能 ## 核心特性 diff --git a/examples/python/README.md b/examples/python/README.md new file mode 100644 index 00000000..39f0e7a2 --- /dev/null +++ b/examples/python/README.md @@ -0,0 +1,85 @@ +# SkillHub Python Examples + +A minimal, dependency-light (`requests`-only) Python client and runnable +examples for the SkillHub REST API. Use it to **search, inspect, download, +and publish** skills from Python — the same operations the ClawHub CLI +performs, without shelling out to the CLI. + +> These are reference examples, not (yet) an officially published pip +> package. See [iflytek/skillhub#701](https://github.com/iflytek/skillhub/issues/701) +> for the discussion on whether to ship a full published SDK. + +## Files + +| File | What it is | +|------|------------| +| [`skillhub_client.py`](./skillhub_client.py) | A small `SkillHubClient` class wrapping the REST API | +| [`example_usage.py`](./example_usage.py) | Runnable script: search → resolve → download, and publish | +| [`requirements.txt`](./requirements.txt) | The only dependency: `requests` | + +## Setup + +```bash +pip install -r requirements.txt + +# Point at your SkillHub instance +export SKILLHUB_URL=https://skill.example.com +# Only needed for write operations (publish / star / rate) +export SKILLHUB_TOKEN= +``` + +Generate an API token from the SkillHub web UI (**Settings → API Tokens**) or +via `POST /api/v1/tokens`. + +## Quick start + +```python +from skillhub_client import SkillHubClient + +client = SkillHubClient() # reads SKILLHUB_URL / SKILLHUB_TOKEN from env + +# Search public skills +results = client.search(keyword="email", size=5) + +# Inspect and resolve a version +detail = client.get_skill("my-namespace", "my-skill") +resolved = client.resolve("my-namespace", "my-skill", tag="stable") + +# Download the latest package (returns the written file path) +path = client.download("my-namespace", "my-skill") + +# Publish a skill package (requires a token) +client.publish("./my-skill.zip", namespace="my-namespace") +``` + +Or run the end-to-end script: + +```bash +python example_usage.py # search + inspect + download +python example_usage.py publish ./my-skill.zip my-namespace +``` + +## Supported operations + +| Method | Endpoint | Auth | +|--------|----------|------| +| `search(keyword, namespace, page, size)` | `GET /api/v1/skills` | — | +| `get_skill(namespace, slug)` | `GET /api/v1/skills/{ns}/{slug}` | — | +| `list_versions(namespace, slug)` | `GET /api/v1/skills/{ns}/{slug}/versions` | — | +| `resolve(namespace, slug, version, tag)` | `GET /api/v1/skills/{ns}/{slug}/resolve` | — | +| `download(namespace, slug, version, dest)` | `GET /api/v1/skills/{ns}/{slug}[/versions/{v}]/download` | — | +| `whoami()` | `GET /api/v1/whoami` | Bearer | +| `publish(zip_path, namespace, request_id)` | `POST /api/v1/publish` | Bearer | +| `star(namespace, slug)` | `POST /api/v1/skills/{ns}/{slug}/star` | Bearer | +| `rate(namespace, slug, score)` | `POST /api/v1/skills/{ns}/{slug}/rating` | Bearer | + +The client unwraps the unified `{code, msg, data}` response envelope +automatically and raises `SkillHubError` on a non-zero business code. + +## Notes + +- Write operations accept an optional `request_id` (a UUID) that is sent as + the `X-Request-Id` header for idempotency. +- For the full API surface (namespaces, reviews, promotion, tags), see the + [Developer Docs → API](https://iflytek.github.io/skillhub/) and + [`document/docs/04-developer/api`](../../document/docs/04-developer/api). diff --git a/examples/python/example_usage.py b/examples/python/example_usage.py new file mode 100644 index 00000000..71540caa --- /dev/null +++ b/examples/python/example_usage.py @@ -0,0 +1,90 @@ +"""Runnable examples for the SkillHub Python client. + +Configure the target registry via environment variables: + + export SKILLHUB_URL=https://skill.example.com + export SKILLHUB_TOKEN= # only needed for write operations + +Then run: + + python example_usage.py # search + inspect + download + python example_usage.py publish ./my-skill.zip my-namespace +""" + +from __future__ import annotations + +import os +import sys + +from skillhub_client import SkillHubClient, SkillHubError + + +def _pick(obj, *keys, default=None): + """Best-effort field access across slightly different response shapes.""" + for key in keys: + if isinstance(obj, dict) and obj.get(key) is not None: + return obj[key] + return default + + +def demo_read(client: SkillHubClient) -> None: + print(f"Searching {client.base_url} for skills matching 'email'...\n") + result = client.search(keyword="email", size=5) + + # The search payload may expose the list under 'items' or 'results'. + items = _pick(result, "items", "results", default=result if isinstance(result, list) else []) + if not items: + print("No skills found. Try a different keyword or registry.") + return + + for skill in items: + name = _pick(skill, "name", "slug", default="(unnamed)") + ns = _pick(skill, "namespace", default="") + version = _pick(skill, "version", "latestVersion", default="?") + downloads = _pick(skill, "downloadCount", "downloads", default=0) + coord = f"{ns}/{name}" if ns else name + print(f" - {coord} v{version} ({downloads} downloads)") + + # Download the first result's latest package. + first = items[0] + ns = _pick(first, "namespace", default="") + slug = _pick(first, "slug", "name") + if ns and slug: + print(f"\nResolving latest version of {ns}/{slug}...") + resolved = client.resolve(ns, slug) + version = _pick(resolved, "version", default=None) + print(f" resolved version: {version}") + + dest = client.download(ns, slug, version=version) + size = os.path.getsize(dest) + print(f" downloaded -> {dest} ({size} bytes)") + + +def demo_publish(client: SkillHubClient, zip_path: str, namespace: str) -> None: + if not client.token: + sys.exit("Publishing requires SKILLHUB_TOKEN to be set.") + print(f"Publishing {zip_path} to namespace '{namespace}'...") + result = client.publish(zip_path, namespace) + print(f" published: {result}") + + +def main() -> None: + try: + client = SkillHubClient() + except ValueError as exc: + sys.exit(str(exc)) + + args = sys.argv[1:] + try: + if args and args[0] == "publish": + if len(args) != 3: + sys.exit("usage: python example_usage.py publish ") + demo_publish(client, args[1], args[2]) + else: + demo_read(client) + except SkillHubError as exc: + sys.exit(f"API error: {exc}") + + +if __name__ == "__main__": + main() diff --git a/examples/python/requirements.txt b/examples/python/requirements.txt new file mode 100644 index 00000000..e8691f91 --- /dev/null +++ b/examples/python/requirements.txt @@ -0,0 +1 @@ +requests>=2.25 diff --git a/examples/python/skillhub_client.py b/examples/python/skillhub_client.py new file mode 100644 index 00000000..f684ed95 --- /dev/null +++ b/examples/python/skillhub_client.py @@ -0,0 +1,244 @@ +"""A minimal Python client for the SkillHub REST API. + +This is a dependency-light reference client (only ``requests``) that mirrors +the operations the ClawHub CLI performs: search, inspect, resolve, download +and publish skills. It is meant as a copy-pasteable starting point for Python +integrations, not (yet) an officially published package. + +API reference: https://iflytek.github.io/skillhub/ (Developer Docs -> API) + +Endpoints used (see docs/04-developer/api): + Public (no auth): + GET /api/v1/skills?keyword=&namespace=&page=&size= + GET /api/v1/skills/{namespace}/{slug} + GET /api/v1/skills/{namespace}/{slug}/versions + GET /api/v1/skills/{namespace}/{slug}/resolve?version=&tag= + GET /api/v1/skills/{namespace}/{slug}/download + GET /api/v1/skills/{namespace}/{slug}/versions/{version}/download + Authenticated (Bearer token): + GET /api/v1/whoami + POST /api/v1/publish (multipart: file, namespace) + POST /api/v1/skills/{namespace}/{slug}/star + POST /api/v1/skills/{namespace}/{slug}/rating (json: {"score": 1-5}) +""" + +from __future__ import annotations + +import os +from typing import Any, Dict, Optional + +import requests + + +class SkillHubError(RuntimeError): + """Raised when the API returns a non-zero business code.""" + + def __init__(self, code: Any, message: str, request_id: Optional[str] = None): + self.code = code + self.request_id = request_id + super().__init__(f"SkillHub API error {code}: {message}" + + (f" (requestId={request_id})" if request_id else "")) + + +class SkillHubClient: + """Thin wrapper over the SkillHub REST API. + + Args: + base_url: Registry base URL, e.g. ``https://skill.example.com``. + token: Optional API token for authenticated calls (Bearer). + timeout: Per-request timeout in seconds. + session: Optional pre-configured ``requests.Session``. + """ + + def __init__( + self, + base_url: Optional[str] = None, + token: Optional[str] = None, + timeout: int = 30, + session: Optional[requests.Session] = None, + ): + base_url = base_url or os.environ.get("SKILLHUB_URL") + if not base_url: + raise ValueError( + "base_url is required (pass it explicitly or set SKILLHUB_URL)" + ) + self.base_url = base_url.rstrip("/") + self.token = token or os.environ.get("SKILLHUB_TOKEN") + self.timeout = timeout + self.session = session or requests.Session() + + # -- internals ------------------------------------------------------- + + def _headers(self, extra: Optional[Dict[str, str]] = None) -> Dict[str, str]: + headers: Dict[str, str] = {} + if self.token: + headers["Authorization"] = f"Bearer {self.token}" + if extra: + headers.update(extra) + return headers + + def _url(self, path: str) -> str: + return f"{self.base_url}{path}" + + def _unwrap(self, resp: requests.Response) -> Any: + """Return the payload, unwrapping the ``{code,msg,data}`` envelope. + + Native ``/api/v1`` endpoints wrap responses in a unified envelope, + while the CLI-compat endpoints return the object directly. This + handles both. + """ + resp.raise_for_status() + payload = resp.json() + if isinstance(payload, dict) and "code" in payload and "data" in payload: + if payload.get("code") not in (0, None): + raise SkillHubError( + payload.get("code"), payload.get("msg", ""), payload.get("requestId") + ) + return payload["data"] + return payload + + # -- public API ------------------------------------------------------ + + def search( + self, + keyword: Optional[str] = None, + namespace: Optional[str] = None, + page: int = 1, + size: int = 20, + ) -> Any: + """Search public skills.""" + params = {"keyword": keyword, "namespace": namespace, "page": page, "size": size} + params = {k: v for k, v in params.items() if v is not None} + return self._unwrap( + self.session.get( + self._url("/api/v1/skills"), + params=params, + headers=self._headers(), + timeout=self.timeout, + ) + ) + + def get_skill(self, namespace: str, slug: str) -> Any: + """Fetch a single skill's detail.""" + return self._unwrap( + self.session.get( + self._url(f"/api/v1/skills/{namespace}/{slug}"), + headers=self._headers(), + timeout=self.timeout, + ) + ) + + def list_versions(self, namespace: str, slug: str) -> Any: + """List all versions of a skill.""" + return self._unwrap( + self.session.get( + self._url(f"/api/v1/skills/{namespace}/{slug}/versions"), + headers=self._headers(), + timeout=self.timeout, + ) + ) + + def resolve( + self, + namespace: str, + slug: str, + version: Optional[str] = None, + tag: Optional[str] = None, + ) -> Any: + """Resolve a version constraint / tag to a concrete version.""" + params = {"version": version, "tag": tag} + params = {k: v for k, v in params.items() if v is not None} + return self._unwrap( + self.session.get( + self._url(f"/api/v1/skills/{namespace}/{slug}/resolve"), + params=params, + headers=self._headers(), + timeout=self.timeout, + ) + ) + + def download( + self, + namespace: str, + slug: str, + version: Optional[str] = None, + dest: Optional[str] = None, + ) -> str: + """Download a skill package (zip). Returns the written file path. + + If ``version`` is omitted the ``latest`` package is downloaded. If + ``dest`` is omitted a file named ``{slug}-{version}.zip`` (or + ``{slug}.zip``) is written to the current directory. + """ + if version: + path = f"/api/v1/skills/{namespace}/{slug}/versions/{version}/download" + else: + path = f"/api/v1/skills/{namespace}/{slug}/download" + if dest is None: + dest = f"{slug}-{version}.zip" if version else f"{slug}.zip" + with self.session.get( + self._url(path), headers=self._headers(), timeout=self.timeout, stream=True + ) as resp: + resp.raise_for_status() + with open(dest, "wb") as fh: + for chunk in resp.iter_content(chunk_size=8192): + if chunk: + fh.write(chunk) + return dest + + # -- authenticated API ---------------------------------------------- + + def whoami(self) -> Any: + """Return the authenticated principal (requires a token).""" + return self._unwrap( + self.session.get( + self._url("/api/v1/whoami"), + headers=self._headers(), + timeout=self.timeout, + ) + ) + + def publish( + self, zip_path: str, namespace: str, request_id: Optional[str] = None + ) -> Any: + """Publish a skill package (zip) to a namespace. Requires a token. + + Pass ``request_id`` (a UUID) to make the publish idempotent via the + ``X-Request-Id`` header. + """ + extra = {"X-Request-Id": request_id} if request_id else None + with open(zip_path, "rb") as fh: + files = {"file": (os.path.basename(zip_path), fh, "application/zip")} + data = {"namespace": namespace} + return self._unwrap( + self.session.post( + self._url("/api/v1/publish"), + files=files, + data=data, + headers=self._headers(extra), + timeout=self.timeout, + ) + ) + + def star(self, namespace: str, slug: str) -> Any: + """Star a skill. Requires a token.""" + return self._unwrap( + self.session.post( + self._url(f"/api/v1/skills/{namespace}/{slug}/star"), + headers=self._headers(), + timeout=self.timeout, + ) + ) + + def rate(self, namespace: str, slug: str, score: int) -> Any: + """Rate a skill from 1 to 5. Requires a token.""" + if not 1 <= score <= 5: + raise ValueError("score must be between 1 and 5") + return self._unwrap( + self.session.post( + self._url(f"/api/v1/skills/{namespace}/{slug}/rating"), + json={"score": score}, + headers=self._headers(), + timeout=self.timeout, + ) + ) From 412514b299c3c0b3a2675d7ed1f23d229a782986 Mon Sep 17 00:00:00 2001 From: qwn3213 Date: Wed, 26 Aug 2026 19:45:08 +0800 Subject: [PATCH 14/15] =?UTF-8?q?fix(web)=20=E6=B7=BB=E5=8A=A0oidc?= =?UTF-8?q?=E7=9A=84logo=E6=96=87=E4=BB=B6=EF=BC=8C=E9=85=8D=E7=BD=AEoidc?= =?UTF-8?q?=E7=99=BB=E5=BD=95=E5=90=8E=E5=9B=BE=E8=A3=82=E6=97=A0logo?= =?UTF-8?q?=E5=A4=AA=E4=B8=91=E4=BA=86=20(#734)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(web) 添加oidc的logo文件,配置oidc登录后图裂无logo太丑了 Signed-off-by: qwn3213 * fix(web) rename loge.svg to logo.svg Signed-off-by: qwn3213 --------- Signed-off-by: qwn3213 --- web/public/oidc-logo.svg | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 web/public/oidc-logo.svg diff --git a/web/public/oidc-logo.svg b/web/public/oidc-logo.svg new file mode 100644 index 00000000..c1ab624c --- /dev/null +++ b/web/public/oidc-logo.svg @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file From 7e37935da8a430afc345fd2113c2a8a2e1c3ae9f Mon Sep 17 00:00:00 2001 From: XiaoSeS <87064762+XiaoSeS@users.noreply.github.com> Date: Thu, 27 Aug 2026 14:51:22 +0800 Subject: [PATCH 15/15] fix(i18n): keep Russian locale keys in sync Merge validated locale parity fix. --- web/src/i18n/locales/ru.json | 89 ++++++++++++++++++++++++++++++++++-- 1 file changed, 86 insertions(+), 3 deletions(-) diff --git a/web/src/i18n/locales/ru.json b/web/src/i18n/locales/ru.json index 4cf75626..216a3791 100644 --- a/web/src/i18n/locales/ru.json +++ b/web/src/i18n/locales/ru.json @@ -582,6 +582,71 @@ "downloadCountTag": "Загрузок: {{value}}", "starCountTag": "Звёзд: {{value}}" }, + "adminNamespaces": { + "title": "Управление пространствами имён", + "subtitle": "Просмотр всех пространств имён и управление участниками, жизненным циклом и действиями управления.", + "statTotal": "Всего пространств имён", + "statActive": "Активные", + "statFrozen": "Замороженные", + "statArchived": "В архиве", + "searchLabel": "Поиск пространств имён", + "searchPlaceholder": "Поиск по slug, отображаемому имени или описанию", + "searchAction": "Поиск", + "clearSearch": "Очистить", + "statusFilter": "Статус", + "typeFilter": "Тип", + "filterAll": "Все", + "typeGlobal": "Глобальное", + "typeTeam": "Командное", + "colNamespace": "Пространство имён", + "colType": "Тип", + "colStatus": "Статус", + "colMembers": "Участники", + "colSkills": "Скиллы", + "colUpdated": "Обновлено", + "empty": "Нет пространств имён, соответствующих текущим фильтрам", + "selectNamespace": "Выберите пространство имён для просмотра деталей", + "members": "Участники", + "skills": "Скиллы", + "currentRole": "Текущая роль", + "platformOverride": "Переопределение платформы", + "yes": "Да", + "no": "Нет", + "noMembership": "Не участник", + "createdAt": "Создано", + "updatedAt": "Обновлено", + "membersTitle": "Управление участниками", + "membersDescription": "Администраторы платформы могут управлять участниками и ролями командных пространств имён. Глобальное пространство имён доступно только для чтения.", + "governanceTitle": "Действия управления", + "governanceDescription": "Заморозка, архивация и восстановление влияют на публикацию и обычную видимость. Действия записываются в журнал аудита.", + "reasonLabel": "Причина", + "reasonPlaceholder": "Объясните необходимость действия для аудиторской проверки.", + "memberRoleUpdated": "Роль участника обновлена", + "freezeAction": "Заморозить пространство имён", + "unfreezeAction": "Разморозить пространство имён", + "archiveAction": "Архивировать пространство имён", + "restoreAction": "Восстановить пространство имён", + "freezeConfirmTitle": "Заморозить пространство имён?", + "freezeConfirmDescription": "«{{name}}» станет доступным только для чтения, и участники не смогут публиковать новые версии скиллов.", + "unfreezeConfirmTitle": "Разморозить пространство имён?", + "unfreezeConfirmDescription": "«{{name}}» снова получит возможности публикации и управления участниками.", + "archiveConfirmTitle": "Архивировать пространство имён?", + "archiveConfirmDescription": "«{{name}}» будет скрыто из обычных разделов продукта и останется доступным в панели администрирования.", + "restoreConfirmTitle": "Восстановить пространство имён?", + "restoreConfirmDescription": "«{{name}}» снова станет видимым в обычных разделах продукта.", + "freezeSuccessTitle": "Пространство имён заморожено", + "freezeSuccessDescription": "«{{name}}» теперь доступно только для чтения.", + "unfreezeSuccessTitle": "Пространство имён разморожено", + "unfreezeSuccessDescription": "«{{name}}» снова активно.", + "archiveSuccessTitle": "Пространство имён архивировано", + "archiveSuccessDescription": "«{{name}}» скрыто из обычных разделов продукта.", + "restoreSuccessTitle": "Пространство имён восстановлено", + "restoreSuccessDescription": "«{{name}}» снова отображается.", + "freezeErrorTitle": "Не удалось заморозить пространство имён", + "unfreezeErrorTitle": "Не удалось разморозить пространство имён", + "archiveErrorTitle": "Не удалось архивировать пространство имён", + "restoreErrorTitle": "Не удалось восстановить пространство имён" + }, "adminUsers": { "title": "Управление пользователями", "subtitle": "Управление пользователями платформы и правами доступа", @@ -1404,7 +1469,8 @@ "warningConfirmCancel": "Вернуться и исправить", "frontmatterFailedTitle": "Неверный формат SKILL.md", "frontmatterFailedDescription": "Проверьте YAML frontmatter в начале SKILL.md. Если значение поля содержит двоеточие, заключите его в кавычки.", - "selectRequired": "Выберите пространство имён и файл" + "selectRequired": "Выберите пространство имён и файл", + "folderPackagingFailed": "Не удалось упаковать выбранную папку. Убедитесь, что она содержит файлы." }, "ratingInput": { "yourRating": "Ваша оценка: {{score}} зв." @@ -1449,7 +1515,22 @@ "skillDetailError": "Не удалось загрузить карточку скилла для этого ревью.", "activeReviewVersion": "Версия на ревью", "downloadSkillZip": "Скачать ZIP скилла", - "noDocumentation": "В этой версии на ревью нет читаемого файла документации." + "noDocumentation": "В этой версии на ревью нет читаемого файла документации.", + "complianceDiffTitle": "Сравнение деклараций соответствия", + "complianceDiffDescription": "Сравните изменения соответствия между опубликованной версией {{baseVersion}} и ожидающей версией {{pendingVersion}}.", + "complianceDiffBaseVersion": "Базовая версия", + "complianceDiffPendingVersion": "Ожидающая версия", + "complianceDiffBaseDigest": "Базовый дайджест", + "complianceDiffPendingDigest": "Дайджест ожидающей версии", + "complianceDiffAdded": "Добавлено", + "complianceDiffRemoved": "Удалено", + "complianceDiffModified": "Изменено", + "complianceDiffAddedLabel": "Добавлено: {{count}}", + "complianceDiffRemovedLabel": "Удалено: {{count}}", + "complianceDiffModifiedLabel": "Изменено: {{count}}", + "complianceDiffViewDetails": "Подробнее", + "complianceDiffBaseRemoved": "Эта декларация есть в базовой версии, но удалена из ожидающей версии.", + "complianceDiffPendingAdded": "Эта декларация добавлена в ожидающую версию." }, "routeGuard": { "forbiddenTitle": "У вас нет прав для просмотра этой страницы", @@ -1536,13 +1617,15 @@ "upload": { "dropHint": "Отпустите для загрузки...", "dragHint": "Перетащите ZIP-файл сюда или нажмите, чтобы выбрать", - "formatHint": "Поддерживается только формат .zip" + "formatHint": "Поддерживается только формат .zip", + "folderHint": "Или выберите папку для упаковки и загрузки" }, "user": { "menu": { "dashboard": "Панель", "mySkills": "Мои скиллы", "myNamespaces": "Мои пространства имён", + "namespacesAdmin": "Управление пространствами имён", "governance": "Центр управления", "stars": "Избранное", "subscriptions": "Мои подписки",