@@ -76,6 +76,11 @@ const messages = defineMessages({
7676 description : 'Label for the SharkPool source filter in the extension library' ,
7777 id : 'tw.extensionLibrary.source.sharkpool'
7878 } ,
79+ sourceCCW : {
80+ defaultMessage : 'CCW' ,
81+ description : 'Label for the CCW source filter in the extension library' ,
82+ id : 'tw.extensionLibrary.source.ccw'
83+ } ,
7984 sourceOther : {
8085 defaultMessage : 'Other' ,
8186 description : 'Label for the Other source filter in the extension library' ,
@@ -175,16 +180,6 @@ const messages = defineMessages({
175180 defaultMessage : 'Click to import' ,
176181 description : 'Action hint for importing an extension' ,
177182 id : 'tw.extensionLibrary.action.import'
178- } ,
179- ccwName : {
180- defaultMessage : '加载CCW扩展' ,
181- description : 'Name of the CCW extension loader item' ,
182- id : 'tw.extensionLibrary.ccw.name'
183- } ,
184- ccwDescription : {
185- defaultMessage : '从共创世界加载扩展,可从 https://assets.ccw.site/extensions 获取。' ,
186- description : 'Description of the CCW extension loader item' ,
187- id : 'tw.extensionLibrary.ccw.description'
188183 }
189184} ) ;
190185
@@ -197,6 +192,7 @@ const SOURCE_KEYS = {
197192 PM : 'pm' ,
198193 MIST : 'mist' ,
199194 SHARKPOOL : 'sharkpool' ,
195+ CCW : 'ccw' ,
200196 OTHER : 'other' ,
201197 CUSTOM : 'custom' ,
202198 SPECIAL : 'special'
@@ -211,11 +207,78 @@ const SOURCE_NAV_ORDER = [
211207 SOURCE_KEYS . MIST ,
212208 SOURCE_KEYS . SHARKPOOL ,
213209 SOURCE_KEYS . ASTRA ,
210+ SOURCE_KEYS . CCW ,
214211 SOURCE_KEYS . OTHER
215212] ;
216213
217214const FAVORITES_STORAGE_KEY = 'tw:library-favorites:extensionLibrary' ;
218- const CCW_EXTENSION_ID = 'ccw_extension_loader' ;
215+ const CCW_EXTENSION_API_BASE = 'https://bfs-web.ccw.site/extensions' ;
216+
217+ const CCW_METADATA_CACHE = { } ;
218+
219+ const fetchCCWItemMetadata = async ( eid ) => {
220+ if ( CCW_METADATA_CACHE [ eid ] ) {
221+ return CCW_METADATA_CACHE [ eid ] ;
222+ }
223+ const response = await fetch ( `${ CCW_EXTENSION_API_BASE } /${ encodeURIComponent ( eid ) } ` ) ;
224+ if ( ! response . ok ) {
225+ throw new Error ( `CCW metadata HTTP error! status: ${ response . status } ` ) ;
226+ }
227+ const data = await response . json ( ) ;
228+ const metadata = data ?. body || data ;
229+ if ( ! metadata || ! Array . isArray ( metadata . versions ) || ! metadata . versions . length ) {
230+ throw new Error ( 'No versions found for this CCW extension.' ) ;
231+ }
232+ if ( ! metadata . versions [ 0 ] ?. assetUri ) {
233+ throw new Error ( 'The latest version does not include an asset URL.' ) ;
234+ }
235+ CCW_METADATA_CACHE [ eid ] = metadata ;
236+ return metadata ;
237+ } ;
238+
239+ // 实时调用CCW API进行搜索/排序,完全参考ccw-ext.html的请求构建逻辑
240+ const fetchCCWExtensions = async ( name , sortField , page , perPage ) => {
241+ let requestUrl = `${ CCW_EXTENSION_API_BASE } ?page=${ page || 1 } &perPage=${ perPage || 30 } &sortField=${ sortField || 'updatedAt' } &sortType=DESC` ;
242+ if ( name ) {
243+ requestUrl += `&name=${ encodeURIComponent ( name ) } ` ;
244+ }
245+ const response = await fetch ( requestUrl ) ;
246+ if ( ! response . ok ) {
247+ throw new Error ( `CCW API HTTP error! status: ${ response . status } ` ) ;
248+ }
249+ const json = await response . json ( ) ;
250+ if ( json ?. body ?. data ) {
251+ return json . body . data ;
252+ }
253+ throw new Error ( 'CCW API response format unexpected' ) ;
254+ } ;
255+
256+ // 映射CCW API数据为内部扩展格式,不带标签
257+ const toCCWGalleryItem = ( item ) => ( {
258+ name : item . name || item . eid || '未知扩展' ,
259+ nameTranslations : { } ,
260+ description : item . description || '暂无描述' ,
261+ descriptionTranslations : { } ,
262+ extensionId : `ccw_${ item . eid || item . id } ` ,
263+ extensionURL : null , // 点击时才通过fetchCCWItemMetadata获取assetUri
264+ iconURL : item . cover || 'https://placehold.co/600x310/f5f5f5/111111?text=No+Cover' ,
265+ tags : [ 'ccw' ] ,
266+ credits : [ ] ,
267+ docsURI : null ,
268+ samples : null ,
269+ incompatibleWithScratch : false ,
270+ featured : true ,
271+ _ccwMeta : {
272+ eid : item . eid ,
273+ id : item . id ,
274+ publisher : item . publisher ,
275+ stats : item . stats ,
276+ createdAt : item . createdAt ,
277+ updatedAt : item . updatedAt ,
278+ versions : item . versions ,
279+ activeVersionId : item . activeVersionId
280+ }
281+ } ) ;
219282
220283const getItemSelectionKey = item => item . extensionURL || item . extensionId ;
221284const isBatchSelectableItem = item => {
@@ -558,6 +621,7 @@ class ExtensionLibrary extends React.PureComponent {
558621 super ( props ) ;
559622 bindAll ( this , [
560623 'executeItemAction' ,
624+ 'fetchAndSetCCWItems' ,
561625 'getActionLabel' ,
562626 'getBatchSelectableItems' ,
563627 'getCardProps' ,
@@ -594,6 +658,8 @@ class ExtensionLibrary extends React.PureComponent {
594658 gallery : cachedGallery ,
595659 galleryError : null ,
596660 galleryTimedOut : false ,
661+ ccwItems : [ ] ,
662+ ccwLoading : false ,
597663 query : '' ,
598664 quickFilters : {
599665 favorites : false ,
@@ -605,6 +671,7 @@ class ExtensionLibrary extends React.PureComponent {
605671 selectedItemKeys : [ ] ,
606672 selectedSource : SOURCE_KEYS . ALL
607673 } ;
674+ this . _queryTimeout = null ;
608675 }
609676 componentDidMount ( ) {
610677 if ( ! this . state . gallery ) {
@@ -630,6 +697,19 @@ class ExtensionLibrary extends React.PureComponent {
630697 clearTimeout ( timeout ) ;
631698 } ) ;
632699 }
700+ // 初始加载CCW扩展
701+ this . fetchAndSetCCWItems ( '' , 'updatedAt' ) ;
702+ }
703+ async fetchAndSetCCWItems ( name , sortField ) {
704+ this . setState ( { ccwLoading : true } ) ;
705+ try {
706+ const rawItems = await fetchCCWExtensions ( name , sortField ) ;
707+ const ccwItems = rawItems . map ( item => toCCWGalleryItem ( item ) ) ;
708+ this . setState ( { ccwItems, ccwLoading : false } ) ;
709+ } catch ( err ) {
710+ log . error ( err ) ;
711+ this . setState ( { ccwItems : [ ] , ccwLoading : false } ) ;
712+ }
633713 }
634714 readFavoritesFromStorage ( ) {
635715 let data ;
@@ -650,35 +730,27 @@ class ExtensionLibrary extends React.PureComponent {
650730 [ SOURCE_KEYS . PM ] : messages . sourcePenguinMod ,
651731 [ SOURCE_KEYS . MIST ] : messages . sourceMist ,
652732 [ SOURCE_KEYS . SHARKPOOL ] : messages . sourceSharkPool ,
733+ [ SOURCE_KEYS . CCW ] : messages . sourceCCW ,
653734 [ SOURCE_KEYS . OTHER ] : messages . sourceOther ,
654735 [ SOURCE_KEYS . CUSTOM ] : messages . sourceCustom ,
655736 [ SOURCE_KEYS . SPECIAL ] : messages . sourceBuiltIn
656737 } ;
657738 return this . props . intl . formatMessage ( sourceMessages [ sourceKey ] || messages . sourceOther ) ;
658739 }
659- getCCWLoaderItem ( ) {
660- return {
661- name : < FormattedMessage { ...messages . ccwName } /> ,
662- extensionId : CCW_EXTENSION_ID ,
663- iconURL : ccwIcon ,
664- description : < FormattedMessage { ...messages . ccwDescription } /> ,
665- featured : true ,
666- tags : [ ]
667- } ;
668- }
669740 getLibraryItems ( ) {
670741 const locale = this . props . intl ?. locale ;
671742 const baseLibrary = extensionLibraryContent
672743 . map ( toLibraryItem )
673744 . map ( item => translateGalleryItem ( item , locale ) ) ;
674- baseLibrary . push ( toLibraryItem ( this . getCCWLoaderItem ( ) ) ) ;
675745 if ( this . state . gallery ) {
746+ const ccwItems = this . state . ccwItems . map ( toLibraryItem ) ;
676747 return [
677748 ...baseLibrary ,
678749 toLibraryItem ( galleryMore ) ,
679750 ...this . state . gallery
680751 . map ( toLibraryItem )
681- . map ( item => translateGalleryItem ( item , locale ) )
752+ . map ( item => translateGalleryItem ( item , locale ) ) ,
753+ ...ccwItems
682754 ] ;
683755 }
684756 if ( this . state . galleryError ) {
@@ -717,8 +789,8 @@ class ExtensionLibrary extends React.PureComponent {
717789 let source = SOURCE_KEYS . OTHER ;
718790 if ( extensionId === 'custom_extension' ) {
719791 source = SOURCE_KEYS . CUSTOM ;
720- } else if ( extensionId === CCW_EXTENSION_ID ) {
721- source = SOURCE_KEYS . OTHER ;
792+ } else if ( extensionId . startsWith ( 'ccw_' ) ) {
793+ source = SOURCE_KEYS . CCW ;
722794 } else if ( extensionId === 'procedures_enable_return' ) {
723795 source = SOURCE_KEYS . SPECIAL ;
724796 } else if ( item . tags && item . tags . includes ( 'scratch' ) ) {
@@ -735,10 +807,12 @@ class ExtensionLibrary extends React.PureComponent {
735807 source = SOURCE_KEYS . SHARKPOOL ;
736808 } else if ( item . tags && item . tags . includes ( 'tw' ) ) {
737809 source = SOURCE_KEYS . TW ;
810+ } else if ( item . tags && item . tags . includes ( 'ccw' ) ) {
811+ source = SOURCE_KEYS . CCW ;
738812 }
739813
740814 const isCustomLoad = extensionId === 'custom_extension' ;
741- const isCCWLoad = extensionId === CCW_EXTENSION_ID ;
815+ const isCCWLoad = source === SOURCE_KEYS . CCW ;
742816 const isSpecialAction = extensionId === 'procedures_enable_return' ;
743817 const isNative = Boolean ( extensionId && ! item . extensionURL && ! item . href && ! isCustomLoad && ! isCCWLoad && ! isSpecialAction ) ;
744818 const candidateValues = new Set ( [
@@ -840,11 +914,6 @@ class ExtensionLibrary extends React.PureComponent {
840914 return ;
841915 }
842916
843- if ( extensionId === CCW_EXTENSION_ID ) {
844- this . props . onOpenCCWExtensionModal ( ) ;
845- return ;
846- }
847-
848917 if ( extensionId === 'procedures_enable_return' ) {
849918 this . handleEnableProcedureReturns ( ) ;
850919 return ;
@@ -854,6 +923,49 @@ class ExtensionLibrary extends React.PureComponent {
854923 return ;
855924 }
856925
926+ // Handle CCW gallery items: fetch metadata and load extension
927+ if ( item . _ccwMeta && item . _ccwMeta . eid ) {
928+ const ccwEid = item . _ccwMeta . eid ;
929+ fetchCCWItemMetadata ( ccwEid )
930+ . then ( metadata => {
931+ const versions = Array . isArray ( metadata . versions ) ? metadata . versions : [ ] ;
932+ const selectedVersion = versions [ 0 ] ;
933+ if ( ! selectedVersion ?. assetUri ) {
934+ throw new Error ( 'No valid asset URL found for this CCW extension.' ) ;
935+ }
936+ const assetUri = selectedVersion . assetUri ;
937+ if ( this . props . onSetSelectedExtension && this . props . onOpenExtensionImportMethodModal ) {
938+ if ( this . props . onSetSelectedExtensions ) {
939+ this . props . onSetSelectedExtensions ( [ ] ) ;
940+ }
941+ this . props . onSetSelectedExtension ( {
942+ extensionId : ccwEid ,
943+ extensionURL : assetUri ,
944+ _ccwMeta : metadata
945+ } ) ;
946+ this . props . onOpenExtensionImportMethodModal ( ) ;
947+ } else {
948+ this . props . vm . extensionManager . loadExtensionURL ( assetUri )
949+ . then ( ( ) => {
950+ if ( this . props . onCategorySelected ) {
951+ this . props . onCategorySelected ( ccwEid ) ;
952+ }
953+ } )
954+ . catch ( err => {
955+ log . error ( err ) ;
956+ // eslint-disable-next-line no-alert
957+ alert ( err ) ;
958+ } ) ;
959+ }
960+ } )
961+ . catch ( err => {
962+ log . error ( err ) ;
963+ // eslint-disable-next-line no-alert
964+ alert ( err || String ( err ) ) ;
965+ } ) ;
966+ return ;
967+ }
968+
857969 if ( extensionId === 'extfind' ) {
858970 loadExtensionAsText ( this . props . vm , extensionURL )
859971 . then ( ( ) => {
@@ -933,11 +1045,22 @@ class ExtensionLibrary extends React.PureComponent {
9331045 this . setState ( {
9341046 selectedSource
9351047 } ) ;
1048+ // 切换到CCW时触发实时搜索
1049+ if ( selectedSource === SOURCE_KEYS . CCW || selectedSource === SOURCE_KEYS . ALL ) {
1050+ this . fetchAndSetCCWItems ( this . state . query , 'updatedAt' ) ;
1051+ }
9361052 }
9371053 handleQueryChange ( query ) {
9381054 this . setState ( {
9391055 query
9401056 } ) ;
1057+ // 防抖:用户停止输入500ms后触发CCW搜索
1058+ if ( this . _queryTimeout ) {
1059+ clearTimeout ( this . _queryTimeout ) ;
1060+ }
1061+ this . _queryTimeout = setTimeout ( ( ) => {
1062+ this . fetchAndSetCCWItems ( query , 'updatedAt' ) ;
1063+ } , 500 ) ;
9411064 }
9421065 handleToggleQuickFilter ( filterKey ) {
9431066 this . setState ( prevState => ( {
@@ -951,6 +1074,7 @@ class ExtensionLibrary extends React.PureComponent {
9511074 this . setState ( {
9521075 query : ''
9531076 } ) ;
1077+ this . fetchAndSetCCWItems ( '' , 'updatedAt' ) ;
9541078 }
9551079 handleClearFilters ( ) {
9561080 this . setState ( {
@@ -964,6 +1088,7 @@ class ExtensionLibrary extends React.PureComponent {
9641088 } ,
9651089 selectedSource : SOURCE_KEYS . ALL
9661090 } ) ;
1091+ this . fetchAndSetCCWItems ( '' , 'updatedAt' ) ;
9671092 }
9681093 handleCustomExtensionOpen ( ) {
9691094 this . props . onOpenCustomExtensionModal ( ) ;
@@ -975,8 +1100,12 @@ class ExtensionLibrary extends React.PureComponent {
9751100 return this . state . selectedItemKeys . includes ( getItemSelectionKey ( item ) ) ;
9761101 }
9771102 matchesSource ( item , sourceKey ) {
1103+ // CCW 扩展只在选中 "CCW" 分类时显示,不出现在 "All" 或 "Other" 中
1104+ if ( sourceKey === SOURCE_KEYS . CCW ) {
1105+ return item . source === SOURCE_KEYS . CCW ;
1106+ }
9781107 if ( sourceKey === SOURCE_KEYS . ALL ) {
979- return true ;
1108+ return item . source !== SOURCE_KEYS . CCW ;
9801109 }
9811110 if ( sourceKey === SOURCE_KEYS . OTHER ) {
9821111 return ! [
@@ -986,7 +1115,8 @@ class ExtensionLibrary extends React.PureComponent {
9861115 SOURCE_KEYS . ASTRA ,
9871116 SOURCE_KEYS . PM ,
9881117 SOURCE_KEYS . MIST ,
989- SOURCE_KEYS . SHARKPOOL
1118+ SOURCE_KEYS . SHARKPOOL ,
1119+ SOURCE_KEYS . CCW
9901120 ] . includes ( item . source ) ;
9911121 }
9921122 return item . source === sourceKey ;
@@ -1030,6 +1160,7 @@ class ExtensionLibrary extends React.PureComponent {
10301160 [ SOURCE_KEYS . PM ] : 0 ,
10311161 [ SOURCE_KEYS . MIST ] : 0 ,
10321162 [ SOURCE_KEYS . SHARKPOOL ] : 0 ,
1163+ [ SOURCE_KEYS . CCW ] : this . state . ccwItems . length ,
10331164 [ SOURCE_KEYS . OTHER ] : 0
10341165 } ;
10351166 for ( const item of items ) {
@@ -1286,7 +1417,6 @@ ExtensionLibrary.propTypes = {
12861417 intl : intlShape . isRequired ,
12871418 onCategorySelected : PropTypes . func ,
12881419 onEnableProcedureReturns : PropTypes . func ,
1289- onOpenCCWExtensionModal : PropTypes . func ,
12901420 onOpenCustomExtensionModal : PropTypes . func ,
12911421 onOpenExtensionImportMethodModal : PropTypes . func ,
12921422 onRequestClose : PropTypes . func ,
@@ -1296,4 +1426,4 @@ ExtensionLibrary.propTypes = {
12961426 vm : PropTypes . instanceOf ( VM ) . isRequired
12971427} ;
12981428
1299- export default injectIntl ( ExtensionLibrary ) ;
1429+ export default injectIntl ( ExtensionLibrary ) ;
0 commit comments