@@ -21,6 +21,24 @@ export class DocumentationGenerator {
2121 this . i18n = new I18nService ( rootDir ) ;
2222 }
2323
24+ /** 将 YYYY 或 YYYY-MM 或 YYYY-MM-DD 字符串解析为时间戳(不可解析返回 null) */
25+ private parseDateToTimestamp ( dateStr ?: string ) : number | null {
26+ if ( ! dateStr || typeof dateStr !== 'string' ) return null ;
27+ // 规范化为完整日期,优先使用原字符串可被 Date 解析
28+ const direct = Date . parse ( dateStr ) ;
29+ if ( ! Number . isNaN ( direct ) ) return direct ;
30+ // 补全到月初/日:支持 "YYYY"、"YYYY-MM"
31+ if ( / ^ \d { 4 } $ / . test ( dateStr ) ) {
32+ const ts = Date . parse ( `${ dateStr } -01-01` ) ;
33+ return Number . isNaN ( ts ) ? null : ts ;
34+ }
35+ if ( / ^ \d { 4 } - \d { 2 } $ / . test ( dateStr ) ) {
36+ const ts = Date . parse ( `${ dateStr } -01` ) ;
37+ return Number . isNaN ( ts ) ? null : ts ;
38+ }
39+ return null ;
40+ }
41+
2442 /** 计算 NewAPI 比率(文档用) */
2543 private calculateNewApiRatios ( cost ?: {
2644 input ?: number ;
@@ -173,4 +191,136 @@ ${tr('intro.data')}
173191
174192 return markdown ;
175193 }
194+
195+ /** 生成“最新发布” Markdown(全站按 release_date 降序) */
196+ generateReleasesMarkdown (
197+ allModelsData : NormalizedData ,
198+ manifest : BuildManifest ,
199+ locale : string = 'en' ,
200+ ) : string {
201+ const { stats } = manifest ;
202+ const lastUpdated = new Date ( manifest . generatedAt ) . toLocaleString (
203+ this . i18n . getDateLocale ( locale ) ,
204+ { timeZone : this . i18n . getTimeZone ( locale ) } ,
205+ ) ;
206+
207+ const messages = this . i18n . getDocMessages ( locale ) ;
208+ const tr = ( key : string ) : string => messages [ key ] || key ;
209+
210+ let markdown = `---
211+ hide:
212+ - navigation
213+ ---
214+
215+ # ${ tr ( 'title.releases' ) }
216+
217+ ${ tr ( 'intro.releases' ) }
218+
219+ !!! info "${ tr ( 'stats.title' ) } "
220+ - **${ tr ( 'stats.providers' ) } **: ${ stats . providers }
221+ - **${ tr ( 'stats.models' ) } **: ${ stats . models }
222+ - **${ tr ( 'stats.updated' ) } **: ${ lastUpdated }
223+
224+ ` ;
225+
226+ type Row = {
227+ providerId : string ;
228+ providerName : string ;
229+ modelId : string ;
230+ modelName : string ;
231+ releaseRaw ?: string | undefined ;
232+ releaseTs : number | null ;
233+ context ?: number ;
234+ output ?: number ;
235+ pricing : string ;
236+ ratios : string ;
237+ capabilities : string ;
238+ knowledge ?: string | undefined ;
239+ modalities : string ;
240+ details : string ;
241+ } ;
242+
243+ const rows : Row [ ] = [ ] ;
244+ for ( const [ providerId , provider ] of Object . entries ( allModelsData . providers ) ) {
245+ const providerName = provider . name || providerId ;
246+ for ( const [ modelId , model ] of Object . entries ( provider . models || { } ) ) {
247+ const releaseRaw = model . release_date || undefined ;
248+ const releaseTs = this . parseDateToTimestamp ( releaseRaw ) ;
249+ const row : Row = {
250+ providerId,
251+ providerName,
252+ modelId,
253+ modelName : model . name || modelId ,
254+ releaseRaw,
255+ releaseTs,
256+ pricing : formatPricing ( model . cost ) ,
257+ ratios : this . formatNewApiRatios ( this . calculateNewApiRatios ( model . cost ) ) ,
258+ capabilities : formatCapabilities ( model ) ,
259+ knowledge : model . knowledge ,
260+ modalities : formatModalities ( model . modalities ) ,
261+ details : formatDetails ( model ) ,
262+ } ;
263+ if ( typeof model . limit ?. context === 'number' ) {
264+ row . context = model . limit . context ;
265+ }
266+ if ( typeof model . limit ?. output === 'number' ) {
267+ row . output = model . limit . output ;
268+ }
269+ rows . push ( row ) ;
270+ }
271+ }
272+
273+ rows . sort ( ( a , b ) => {
274+ const at = a . releaseTs ?? - Infinity ;
275+ const bt = b . releaseTs ?? - Infinity ;
276+ return bt - at ; // 降序(新 → 旧 → 未知)
277+ } ) ;
278+
279+ const headers = [
280+ tr ( 'table.model' ) ,
281+ tr ( 'table.provider' ) ,
282+ tr ( 'table.modelId' ) ,
283+ tr ( 'table.released' ) ,
284+ tr ( 'table.pricing' ) ,
285+ tr ( 'table.ratios' ) ,
286+ tr ( 'table.capabilities' ) ,
287+ tr ( 'table.knowledge' ) ,
288+ tr ( 'table.modalities' ) ,
289+ tr ( 'table.details' ) ,
290+ ] ;
291+ const separators = [
292+ '-------' ,
293+ '--------' ,
294+ '--------' ,
295+ '--------' ,
296+ '----------------' ,
297+ '---------------' ,
298+ '--------------' ,
299+ '-----------' ,
300+ '------------' ,
301+ '----------' ,
302+ ] ;
303+
304+ markdown += `| ${ headers . join ( ' | ' ) } |\n` ;
305+ markdown += `|${ separators . join ( '|' ) } |\n` ;
306+
307+ for ( const r of rows ) {
308+ const fields = [
309+ `**${ escapeMarkdownPipes ( r . modelName ) } **` ,
310+ escapeMarkdownPipes ( r . providerName ) ,
311+ escapeMarkdownPipes ( r . modelId ) ,
312+ escapeMarkdownPipes ( r . releaseRaw || '-' ) ,
313+ r . pricing ,
314+ r . ratios ,
315+ r . capabilities ,
316+ r . knowledge || '-' ,
317+ r . modalities ,
318+ r . details ,
319+ ] ;
320+ markdown += `| ${ fields . join ( ' | ' ) } |\n` ;
321+ }
322+
323+ markdown += '\n' ;
324+ return markdown ;
325+ }
176326}
0 commit comments