Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 52 additions & 0 deletions .github/workflows/generate-plugins-data.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
name: Generate Plugins Data

on:
push:
branches:
- master

permissions:
contents: write

jobs:
generate:
runs-on: ubuntu-latest

steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
submodules: recursive
token: ${{ secrets.GITHUB_TOKEN }}

- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '22'

- name: Setup pnpm
uses: pnpm/action-setup@v4
with:
version: 10

- name: Install dependencies
run: pnpm install

- name: Run generate script
run: pnpm generate

- name: Check for changes
id: changes
run: |
if [ -n "$(git status --porcelain)" ]; then
echo "has_changes=true" >> $GITHUB_OUTPUT
fi

- name: Commit and push changes
if: steps.changes.outputs.has_changes == 'true'
run: |
git config --local user.email "github-actions[bot]@users.noreply.github.com"
git config --local user.name "github-actions[bot]"
git add plugins-data.ts plugins-data.json
git commit -m "chore: auto-generate plugins data"
git push
9 changes: 9 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# 依赖
node_modules/

# IDE
.idea/
.vscode/
*.swp
*.swo
*~
228 changes: 228 additions & 0 deletions generate.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,228 @@
import fs from 'fs'
import path from 'path'
import { fileURLToPath } from 'url'
import toml from 'toml'

const __filename = fileURLToPath(import.meta.url)
const __dirname = path.dirname(__filename)

const VALID_TAGS = [
"ai",
"mcp",
"agent",
"rag",
"permission",
"sso",
"rbac",
"auth",
"ldap",
"storage",
"notification",
"task",
"other",
] as const

const VALID_DATABASES = ['mysql', 'postgresql'] as const

const DEFAULT_ICON = 'https://wu-clan.github.io/picx-images-hosting/logo/fba.svg'

interface PluginTomlPlugin {
icon?: string
summary: string
version: string
description: string
author: string
tags?: string[]
database?: string[]
}

interface PluginToml {
plugin: PluginTomlPlugin
}

interface GitModule {
path: string
url: string
branch: string
}

interface PluginData {
plugin: PluginTomlPlugin
git: GitModule
}


function resolveIconUrl(iconPath: string | undefined, gitUrl: string, branch: string): string {
if (!iconPath) return DEFAULT_ICON

if (iconPath.startsWith('http://') || iconPath.startsWith('https://')) {
return iconPath
}

const match = gitUrl.match(/github\.com[/:]([^/]+)\/([^/.]+)/)
if (!match) return DEFAULT_ICON

const [, owner, repo] = match
return `https://raw.githubusercontent.com/${ owner }/${ repo }/${ branch }/${ iconPath }`
}

function loadPluginToml(pluginPath: string): PluginToml | null {
const tomlPath = path.join(pluginPath, 'plugin.toml')
if (!fs.existsSync(tomlPath)) return null

try {
return toml.parse(fs.readFileSync(tomlPath, 'utf-8')) as PluginToml
} catch {
return null
}
}

function parseGitModules(gitmodulesPath: string): Map<string, GitModule> {
const modules = new Map<string, GitModule>()
if (!fs.existsSync(gitmodulesPath)) return modules

const content = fs.readFileSync(gitmodulesPath, 'utf-8')
const lines = content.split('\n')

let currentPath = ''
let currentUrl = ''
let currentBranch = ''

for (const line of lines) {
const pathMatch = line.match(/path\s*=\s*(.+)/)
const urlMatch = line.match(/url\s*=\s*(.+)/)
const branchMatch = line.match(/branch\s*=\s*(.+)/)

if (pathMatch) currentPath = pathMatch[1].trim()
if (urlMatch) currentUrl = urlMatch[1].trim()
if (branchMatch) currentBranch = branchMatch[1].trim()

if (currentPath && currentUrl) {
modules.set(currentPath, {
path: currentPath,
url: currentUrl,
branch: currentBranch || 'master',
})
currentPath = ''
currentUrl = ''
currentBranch = ''
}
}

return modules
}

function generatePluginData(pluginsDir: string, gitmodulesPath: string): PluginData[] {
const gitModules = parseGitModules(gitmodulesPath)
const pluginDataList: PluginData[] = []

const pluginDirs = fs.readdirSync(pluginsDir, { withFileTypes: true })
.filter(entry => entry.isDirectory() && !entry.name.startsWith('.'))
.map(entry => entry.name)
.sort()

for (const pluginName of pluginDirs) {
const pluginPath = path.join(pluginsDir, pluginName)
const pluginConfig = loadPluginToml(pluginPath)

if (!pluginConfig?.plugin) {
console.warn(`警告: ${ pluginName } 没有有效的 plugin.toml`)
continue
}

const modulePath = `plugins/${ pluginName }`
const gitModule = gitModules.get(modulePath)

if (!gitModule) {
console.warn(`警告: ${ pluginName } 没有对应的 git submodule`)
continue
}

const rawPlugin = pluginConfig.plugin
let database: string[] | undefined
if (rawPlugin.database && Array.isArray(rawPlugin.database)) {
const filtered = rawPlugin.database
.map(db => {
const lower = db.toLowerCase()
if (lower === 'pgsql') return 'postgresql'
return lower
})
.filter(db => VALID_DATABASES.includes(db as any))
if (filtered.length > 0) {
database = [...new Set(filtered)]
}
}

const plugin: PluginTomlPlugin = {
icon: resolveIconUrl(rawPlugin.icon, gitModule.url, gitModule.branch),
summary: rawPlugin.summary,
version: rawPlugin.version,
description: rawPlugin.description,
author: rawPlugin.author,
...(rawPlugin.tags && { tags: rawPlugin.tags }),
...(database && { database }),
}

pluginDataList.push({
plugin,
git: gitModule,
})
}

return pluginDataList
}

function generateTypeScriptCode(pluginDataList: PluginData[]): string {
return `export const validTags = ${ JSON.stringify(VALID_TAGS, null, 2) } as const

export interface PluginTomlPlugin {
icon: string
summary: string
version: string
description: string
author: string
tags?: string[]
database?: string[]
}

export interface GitModule {
path: string
url: string
branch: string
}

export interface PluginData {
plugin: PluginTomlPlugin
git: GitModule
}

export const pluginDataList: PluginData[] = ${ JSON.stringify(pluginDataList, null, 2) }
`
}

function main() {
const baseDir = __dirname
const pluginsDir = path.join(baseDir, 'plugins')
const gitmodulesPath = path.join(baseDir, '.gitmodules')

console.log('生成插件数据...')

const pluginDataList = generatePluginData(pluginsDir, gitmodulesPath)
console.log(`找到 ${ pluginDataList.length } 个插件`)

fs.writeFileSync(
path.join(baseDir, 'plugins-data.ts'),
generateTypeScriptCode(pluginDataList),
'utf-8'
)

fs.writeFileSync(
path.join(baseDir, 'plugins-data.json'),
JSON.stringify({ validTags: VALID_TAGS, pluginDataList }, null, 2),
'utf-8'
)

console.log('完成')
}

main()
14 changes: 14 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"type": "module",
"scripts": {
"generate": "tsx generate.ts",
"build": "tsx generate.ts"
},
"devDependencies": {
"@types/node": "^20.0.0",
"tsx": "^4.7.0"
},
"dependencies": {
"toml": "^3.0.0"
}
}
Loading