Skip to content

Commit c72250b

Browse files
committed
feat: add plugin config validate
1 parent 499aa7d commit c72250b

5 files changed

Lines changed: 213 additions & 32 deletions

File tree

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
name: Validate Plugin TOML
2+
3+
on:
4+
pull_request:
5+
branches:
6+
- master
7+
paths:
8+
- 'plugins/**/plugin.toml'
9+
10+
jobs:
11+
validate:
12+
runs-on: ubuntu-latest
13+
14+
steps:
15+
- name: Checkout repository
16+
uses: actions/checkout@v4
17+
with:
18+
submodules: recursive
19+
20+
- name: Setup Node.js
21+
uses: actions/setup-node@v4
22+
with:
23+
node-version: '22'
24+
25+
- name: Setup pnpm
26+
uses: pnpm/action-setup@v4
27+
with:
28+
version: 10
29+
30+
- name: Install dependencies
31+
run: pnpm install
32+
33+
- name: Validate plugin.toml files
34+
run: pnpm validate

constants.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
export const VALID_TAGS = [
2+
"ai",
3+
"mcp",
4+
"agent",
5+
"auth",
6+
"storage",
7+
"notification",
8+
"task",
9+
"payment",
10+
"other",
11+
] as const
12+
13+
export const VALID_DATABASES = ['mysql', 'pgsql', 'postgresql'] as const
14+
15+
export type ValidTag = typeof VALID_TAGS[number]
16+
export type ValidDatabase = typeof VALID_DATABASES[number]

generate.ts

Lines changed: 27 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -2,28 +2,11 @@ import fs from 'fs'
22
import path from 'path'
33
import { fileURLToPath } from 'url'
44
import toml from 'toml'
5+
import { VALID_TAGS, VALID_DATABASES, ValidTag, ValidDatabase } from './constants.js'
56

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

9-
const VALID_TAGS = [
10-
"ai",
11-
"mcp",
12-
"agent",
13-
"rag",
14-
"permission",
15-
"sso",
16-
"rbac",
17-
"auth",
18-
"ldap",
19-
"storage",
20-
"notification",
21-
"task",
22-
"other",
23-
] as const
24-
25-
const VALID_DATABASES = ['mysql', 'postgresql'] as const
26-
2710
const DEFAULT_ICON = 'https://wu-clan.github.io/picx-images-hosting/logo/fba.svg'
2811

2912
interface PluginTomlPlugin {
@@ -32,8 +15,8 @@ interface PluginTomlPlugin {
3215
version: string
3316
description: string
3417
author: string
35-
tags?: string[]
36-
database?: string[]
18+
tags?: ValidTag[]
19+
database?: ValidDatabase[]
3720
}
3821

3922
interface PluginToml {
@@ -139,15 +122,24 @@ function generatePluginData(pluginsDir: string, gitmodulesPath: string): PluginD
139122
}
140123

141124
const rawPlugin = pluginConfig.plugin
142-
let database: string[] | undefined
125+
126+
// 验证并过滤 tags
127+
let tags: ValidTag[] | undefined
128+
if (rawPlugin.tags && Array.isArray(rawPlugin.tags)) {
129+
const filtered = rawPlugin.tags
130+
.map(tag => tag.toLowerCase())
131+
.filter((tag): tag is ValidTag => VALID_TAGS.includes(tag as ValidTag))
132+
if (filtered.length > 0) {
133+
tags = [...new Set(filtered)]
134+
}
135+
}
136+
137+
// 验证并过滤 database
138+
let database: ValidDatabase[] | undefined
143139
if (rawPlugin.database && Array.isArray(rawPlugin.database)) {
144140
const filtered = rawPlugin.database
145-
.map(db => {
146-
const lower = db.toLowerCase()
147-
if (lower === 'pgsql') return 'postgresql'
148-
return lower
149-
})
150-
.filter(db => VALID_DATABASES.includes(db as any))
141+
.map(db => db.toLowerCase() as ValidDatabase)
142+
.filter((db): db is ValidDatabase => VALID_DATABASES.includes(db))
151143
if (filtered.length > 0) {
152144
database = [...new Set(filtered)]
153145
}
@@ -159,7 +151,7 @@ function generatePluginData(pluginsDir: string, gitmodulesPath: string): PluginD
159151
version: rawPlugin.version,
160152
description: rawPlugin.description,
161153
author: rawPlugin.author,
162-
...(rawPlugin.tags && { tags: rawPlugin.tags }),
154+
...(tags && { tags }),
163155
...(database && { database }),
164156
}
165157

@@ -174,15 +166,19 @@ function generatePluginData(pluginsDir: string, gitmodulesPath: string): PluginD
174166

175167
function generateTypeScriptCode(pluginDataList: PluginData[]): string {
176168
return `export const validTags = ${ JSON.stringify(VALID_TAGS, null, 2) } as const
169+
export const validDatabases = ${ JSON.stringify(VALID_DATABASES, null, 2) } as const
170+
171+
export type ValidTag = typeof validTags[number]
172+
export type ValidDatabase = typeof validDatabases[number]
177173
178174
export interface PluginTomlPlugin {
179175
icon: string
180176
summary: string
181177
version: string
182178
description: string
183179
author: string
184-
tags?: string[]
185-
database?: string[]
180+
tags?: ValidTag[]
181+
database?: ValidDatabase[]
186182
}
187183
188184
export interface GitModule {
@@ -218,7 +214,7 @@ function main() {
218214

219215
fs.writeFileSync(
220216
path.join(baseDir, 'plugins-data.json'),
221-
JSON.stringify({ validTags: VALID_TAGS, pluginDataList }, null, 2),
217+
JSON.stringify({ validTags: VALID_TAGS, validDatabases: VALID_DATABASES, pluginDataList }, null, 2),
222218
'utf-8'
223219
)
224220

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@
22
"type": "module",
33
"scripts": {
44
"generate": "tsx generate.ts",
5-
"build": "tsx generate.ts"
5+
"build": "tsx generate.ts",
6+
"validate": "tsx validate.ts"
67
},
78
"devDependencies": {
89
"@types/node": "^20.0.0",

validate.ts

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
import fs from 'fs'
2+
import path from 'path'
3+
import { fileURLToPath } from 'url'
4+
import toml from 'toml'
5+
import { VALID_TAGS, VALID_DATABASES, ValidTag, ValidDatabase } from './constants.js'
6+
7+
const __filename = fileURLToPath(import.meta.url)
8+
const __dirname = path.dirname(__filename)
9+
10+
interface PluginTomlPlugin {
11+
icon?: string
12+
summary: string
13+
version: string
14+
description: string
15+
author: string
16+
tags?: string[]
17+
database?: string[]
18+
}
19+
20+
interface PluginToml {
21+
plugin: PluginTomlPlugin
22+
}
23+
24+
interface ValidationError {
25+
plugin: string
26+
field: string
27+
message: string
28+
invalidValues?: string[]
29+
}
30+
31+
function validatePluginToml(pluginName: string, pluginPath: string): ValidationError[] {
32+
const errors: ValidationError[] = []
33+
const tomlPath = path.join(pluginPath, 'plugin.toml')
34+
35+
if (!fs.existsSync(tomlPath)) {
36+
return errors
37+
}
38+
39+
let config: PluginToml
40+
try {
41+
config = toml.parse(fs.readFileSync(tomlPath, 'utf-8')) as PluginToml
42+
} catch (e) {
43+
errors.push({
44+
plugin: pluginName,
45+
field: 'plugin.toml',
46+
message: `解析失败: ${e instanceof Error ? e.message : String(e)}`,
47+
})
48+
return errors
49+
}
50+
51+
if (!config.plugin) {
52+
errors.push({
53+
plugin: pluginName,
54+
field: 'plugin',
55+
message: '缺少 [plugin] 配置块',
56+
})
57+
return errors
58+
}
59+
60+
// 验证 tags
61+
if (config.plugin.tags && Array.isArray(config.plugin.tags)) {
62+
const invalidTags = config.plugin.tags.filter(
63+
tag => !VALID_TAGS.includes(tag.toLowerCase() as ValidTag)
64+
)
65+
if (invalidTags.length > 0) {
66+
errors.push({
67+
plugin: pluginName,
68+
field: 'tags',
69+
message: `包含无效的 tags 值`,
70+
invalidValues: invalidTags,
71+
})
72+
}
73+
}
74+
75+
// 验证 database
76+
if (config.plugin.database && Array.isArray(config.plugin.database)) {
77+
const invalidDatabases = config.plugin.database.filter(
78+
db => !VALID_DATABASES.includes(db.toLowerCase() as ValidDatabase)
79+
)
80+
if (invalidDatabases.length > 0) {
81+
errors.push({
82+
plugin: pluginName,
83+
field: 'database',
84+
message: `包含无效的 database 值`,
85+
invalidValues: invalidDatabases,
86+
})
87+
}
88+
}
89+
90+
return errors
91+
}
92+
93+
function main() {
94+
const pluginsDir = path.join(__dirname, 'plugins')
95+
const allErrors: ValidationError[] = []
96+
97+
if (!fs.existsSync(pluginsDir)) {
98+
console.log('plugins 目录不存在')
99+
process.exit(0)
100+
}
101+
102+
const pluginDirs = fs.readdirSync(pluginsDir, { withFileTypes: true })
103+
.filter(entry => entry.isDirectory() && !entry.name.startsWith('.'))
104+
.map(entry => entry.name)
105+
106+
console.log(`验证 ${pluginDirs.length} 个插件...\n`)
107+
108+
for (const pluginName of pluginDirs) {
109+
const pluginPath = path.join(pluginsDir, pluginName)
110+
const errors = validatePluginToml(pluginName, pluginPath)
111+
allErrors.push(...errors)
112+
}
113+
114+
if (allErrors.length === 0) {
115+
console.log('所有插件验证通过')
116+
console.log(`\n允许的 tags: ${VALID_TAGS.join(', ')}`)
117+
console.log(`允许的 database: ${VALID_DATABASES.join(', ')}`)
118+
process.exit(0)
119+
}
120+
121+
console.error('验证失败:\n')
122+
for (const error of allErrors) {
123+
console.error(`[${error.plugin}] ${error.field}: ${error.message}`)
124+
if (error.invalidValues) {
125+
console.error(` 无效值: ${error.invalidValues.join(', ')}`)
126+
}
127+
}
128+
129+
console.error(`\n允许的 tags: ${VALID_TAGS.join(', ')}`)
130+
console.error(`允许的 database: ${VALID_DATABASES.join(', ')}`)
131+
process.exit(1)
132+
}
133+
134+
main()

0 commit comments

Comments
 (0)