xinyu-pro 0.22.6 → 0.23.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/app/api/chat/route.ts +2 -2
- package/app/api/generate-svg/route.ts +1 -1
- package/app/api/generate-theme/route.ts +112 -135
- package/app/api/plugins/bindings/route.ts +4 -4
- package/app/api/plugins/export/route.ts +9 -3
- package/app/api/plugins/export-xye/route.ts +12 -7
- package/app/api/plugins/files/route.ts +9 -9
- package/app/api/plugins/files-list/route.ts +7 -3
- package/app/api/plugins/files-upload/route.ts +3 -3
- package/app/api/plugins/files-write/all/route.ts +250 -0
- package/app/api/plugins/files-write/route.ts +13 -15
- package/app/api/plugins/import/route.ts +5 -5
- package/app/api/plugins/import-package/route.ts +9 -9
- package/app/api/plugins/resources/route.ts +6 -2
- package/app/api/plugins/route.ts +215 -119
- package/app/api/plugins/scan/route.ts +1 -1
- package/app/api/plugins/storage/route.ts +3 -3
- package/app/api/plugins/toggle/route.ts +21 -0
- package/app/api/sessions/route.ts +8 -3
- package/app/api/settings/route.ts +2 -2
- package/app/api/suggest-fields/route.ts +1 -1
- package/app/api/templates/route.ts +2 -2
- package/app/api/themes/route.ts +116 -0
- package/app/editor/page.tsx +21 -4
- package/app/extensions/edit/[id]/page.tsx +2148 -984
- package/app/extensions/page.tsx +477 -86
- package/app/extensions/tutorial/code-examples.ts +833 -0
- package/app/extensions/tutorial/nav-link.tsx +27 -0
- package/app/extensions/tutorial/page.tsx +47 -3616
- package/app/extensions/tutorial/sections/AiInterceptionSection.tsx +87 -0
- package/app/extensions/tutorial/sections/ArchitectureSection.tsx +39 -0
- package/app/extensions/tutorial/sections/BestPracticesSection.tsx +78 -0
- package/app/extensions/tutorial/sections/ChatApiSection.tsx +212 -0
- package/app/extensions/tutorial/sections/CrossPluginSection.tsx +90 -0
- package/app/extensions/tutorial/sections/CssVariablesSection.tsx +189 -0
- package/app/extensions/tutorial/sections/DebugSection.tsx +48 -0
- package/app/extensions/tutorial/sections/DependencySection.tsx +134 -0
- package/app/extensions/tutorial/sections/DomSandboxSection.tsx +229 -0
- package/app/extensions/tutorial/sections/ExamplesSection.tsx +29 -0
- package/app/extensions/tutorial/sections/FileApiSection.tsx +44 -0
- package/app/extensions/tutorial/sections/GlobalApiSection.tsx +50 -0
- package/app/extensions/tutorial/sections/InputApiSection.tsx +213 -0
- package/app/extensions/tutorial/sections/LifecycleSection.tsx +105 -0
- package/app/extensions/tutorial/sections/OverviewSection.tsx +54 -0
- package/app/extensions/tutorial/sections/PermissionsSection.tsx +239 -0
- package/app/extensions/tutorial/sections/PersistentStorageSection.tsx +49 -0
- package/app/extensions/tutorial/sections/PluginConfigSection.tsx +81 -0
- package/app/extensions/tutorial/sections/PluginImportExportSection.tsx +68 -0
- package/app/extensions/tutorial/sections/PluginStorageSection.tsx +94 -0
- package/app/extensions/tutorial/sections/ResourceApiSection.tsx +88 -0
- package/app/extensions/tutorial/sections/SecuritySection.tsx +106 -0
- package/app/extensions/tutorial/sections/SlotsSection.tsx +130 -0
- package/app/extensions/tutorial/sections/UiApiSection.tsx +147 -0
- package/app/extensions/tutorial/sections/UtilsApiSection.tsx +96 -0
- package/app/extensions/tutorial/sections-data.tsx +325 -0
- package/app/extensions/tutorial/styles.ts +59 -0
- package/app/extensions/tutorial/types.ts +14 -0
- package/app/game/[id]/page.tsx +336 -148
- package/app/globals.css +41 -0
- package/app/page.tsx +17 -10
- package/app/settings/page.tsx +218 -236
- package/bin/cli.js +3 -10
- package/components/ChatInput.tsx +20 -6
- package/components/ChatWindow.tsx +1 -1
- package/components/MessageBubble.tsx +5 -6
- package/components/ui/ConfirmDialog.tsx +28 -6
- package/components/ui/DeprecatedBadge.tsx +163 -0
- package/components/ui/PageHeader.tsx +1 -1
- package/components/ui/PermissionConflictDialog.tsx +1 -1
- package/components/ui/PluginConfigForm.tsx +2 -3
- package/components/ui/PluginFloatingLayer.tsx +2 -2
- package/components/ui/PluginModalRenderer.tsx +84 -38
- package/components/ui/PluginProvider.tsx +32 -13
- package/components/ui/PluginSlotRenderer.tsx +2 -2
- package/components/ui/ThemeCustomizer.tsx +4 -7
- package/components/ui/ThemeProvider.tsx +214 -125
- package/components/ui/ThemeSwitcher.tsx +119 -157
- package/components/ui/ToastProvider.tsx +47 -17
- package/lib/{parseWorldCard.ts → client/parseWorldCard.ts} +1 -1
- package/lib/{plugin-dom-sandbox.ts → client/plugin-dom-sandbox.ts} +2 -2
- package/lib/{plugin-runtime.ts → client/plugin-runtime.ts} +166 -67
- package/lib/client/plugin-utils.ts +105 -0
- package/lib/{router-history.ts → client/router-history.ts} +1 -1
- package/lib/{storage.ts → client/storage.ts} +66 -3
- package/lib/{themes.ts → client/themes.ts} +21 -1
- package/lib/{builtin-plugins.ts → server/builtin-plugins.ts} +1 -1
- package/lib/{db-init.ts → server/db-init.ts} +2 -2
- package/lib/{db.ts → server/db.ts} +48 -8
- package/lib/{get-ai-config.ts → server/get-ai-config.ts} +2 -2
- package/lib/{plugin-files.ts → server/plugin-files.ts} +7 -2
- package/lib/server/plugin-utils.ts +88 -0
- package/lib/{manifest-parser.ts → shared/manifest-parser.ts} +4 -4
- package/lib/{plugin-types.ts → shared/plugin-types.ts} +11 -6
- package/lib/{version.ts → shared/version.ts} +1 -1
- package/next.config.mjs +3 -2
- package/package.json +4 -3
- package/styles/themes.css +7 -2
- package/version.json +2 -2
- package/app/extensions/create/page.tsx +0 -1422
- /package/lib/{plugin-events.ts → client/plugin-events.ts} +0 -0
- /package/lib/{plugin-resource-tracker.ts → client/plugin-resource-tracker.ts} +0 -0
- /package/lib/{plugin-html-sanitizer.ts → shared/plugin-html-sanitizer.ts} +0 -0
- /package/lib/{plugin-security.ts → shared/plugin-security.ts} +0 -0
- /package/lib/{prompt-builder.ts → shared/prompt-builder.ts} +0 -0
- /package/lib/{types.ts → shared/types.ts} +0 -0
package/app/api/plugins/route.ts
CHANGED
|
@@ -1,11 +1,18 @@
|
|
|
1
1
|
// app/api/plugins/route.ts - 插件 CRUD API
|
|
2
2
|
|
|
3
3
|
import { NextRequest, NextResponse } from 'next/server';
|
|
4
|
-
import { execute } from '@/lib/db';
|
|
5
|
-
import { ensureDb } from '@/lib/db-init';
|
|
6
|
-
import {
|
|
4
|
+
import { execute } from '@/lib/server/db';
|
|
5
|
+
import { ensureDb } from '@/lib/server/db-init';
|
|
6
|
+
import {
|
|
7
|
+
writePluginCode,
|
|
8
|
+
deletePluginCode,
|
|
9
|
+
getPluginDir,
|
|
10
|
+
getPluginCodeRelativePath,
|
|
11
|
+
PLUGIN_ROOT
|
|
12
|
+
} from '@/lib/server/plugin-files';
|
|
7
13
|
import path from 'path';
|
|
8
14
|
import { promises as fs } from 'fs';
|
|
15
|
+
import { PROJECT_ROOT } from '@/lib/server/plugin-files';
|
|
9
16
|
|
|
10
17
|
/** 解析 JSON 字段 */
|
|
11
18
|
function parseJsonField<T>(value: unknown): T {
|
|
@@ -40,6 +47,7 @@ function parsePermissions(value: unknown): {
|
|
|
40
47
|
function rowToPlugin(row: Record<string, unknown>) {
|
|
41
48
|
const perms = parsePermissions(row.ui_permissions);
|
|
42
49
|
return {
|
|
50
|
+
fileEntry: (row.file_entry as string) || 'plugin.js',
|
|
43
51
|
id: row.id,
|
|
44
52
|
name: row.name,
|
|
45
53
|
version: row.version,
|
|
@@ -49,7 +57,6 @@ function rowToPlugin(row: Record<string, unknown>) {
|
|
|
49
57
|
icon: row.icon,
|
|
50
58
|
minAppVersion: row.min_app_version,
|
|
51
59
|
code: '',
|
|
52
|
-
codePath: (row.code_path as string) || '',
|
|
53
60
|
configSchema: parseJsonField(row.config_schema),
|
|
54
61
|
uiSlots: parseJsonField(row.ui_slots),
|
|
55
62
|
...perms,
|
|
@@ -58,6 +65,10 @@ function rowToPlugin(row: Record<string, unknown>) {
|
|
|
58
65
|
const parsed = parseJsonField(row.dependencies);
|
|
59
66
|
return Array.isArray(parsed) ? parsed : [];
|
|
60
67
|
})(),
|
|
68
|
+
commands: (() => {
|
|
69
|
+
const parsed = parseJsonField(row.commands);
|
|
70
|
+
return Array.isArray(parsed) ? parsed : [];
|
|
71
|
+
})(),
|
|
61
72
|
createdAt: row.created_at,
|
|
62
73
|
updatedAt: row.updated_at,
|
|
63
74
|
};
|
|
@@ -66,16 +77,66 @@ function rowToPlugin(row: Record<string, unknown>) {
|
|
|
66
77
|
/** 异步填充插件代码(从文件读取) */
|
|
67
78
|
async function fillPluginCode(plugin: ReturnType<typeof rowToPlugin>): Promise<typeof plugin> {
|
|
68
79
|
try {
|
|
69
|
-
const
|
|
70
|
-
|
|
71
|
-
const filePath = path.join(pluginDir, codeFileName);
|
|
72
|
-
plugin.code = await fs.readFile(filePath, 'utf-8');
|
|
80
|
+
const codeFilePath = path.join(getPluginDir(plugin.id as string), plugin.fileEntry);
|
|
81
|
+
plugin.code = await fs.readFile(codeFilePath, 'utf-8');
|
|
73
82
|
} catch {
|
|
74
83
|
plugin.code = '';
|
|
75
84
|
}
|
|
76
85
|
return plugin;
|
|
77
86
|
}
|
|
78
87
|
|
|
88
|
+
// 同步绑定配置的辅助函数
|
|
89
|
+
async function syncBindingsForPlugin(pluginId: string, configSchema: any[]) {
|
|
90
|
+
if (!configSchema || configSchema.length === 0) return;
|
|
91
|
+
|
|
92
|
+
try {
|
|
93
|
+
const [bindings] = await execute(
|
|
94
|
+
'SELECT * FROM extension_bindings WHERE extension_id = ?',
|
|
95
|
+
[pluginId]
|
|
96
|
+
);
|
|
97
|
+
|
|
98
|
+
const pluginBindings = bindings as Record<string, unknown>[];
|
|
99
|
+
if (pluginBindings.length === 0) return;
|
|
100
|
+
|
|
101
|
+
const schemaKeys = new Set(configSchema.map((f: any) => f.key));
|
|
102
|
+
|
|
103
|
+
for (const binding of pluginBindings) {
|
|
104
|
+
const oldConfig: Record<string, any> = JSON.parse(binding.config as string) || {};
|
|
105
|
+
const newConfig: Record<string, unknown> = {};
|
|
106
|
+
let changed = false;
|
|
107
|
+
|
|
108
|
+
// 处理新增和保留的配置项
|
|
109
|
+
for (const field of configSchema) {
|
|
110
|
+
if (field.key in oldConfig) {
|
|
111
|
+
newConfig[field.key] = oldConfig[field.key];
|
|
112
|
+
} else {
|
|
113
|
+
newConfig[field.key] = field.defaultValue ?? '';
|
|
114
|
+
changed = true;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// 检查被删除的配置项
|
|
119
|
+
for (const key of Object.keys(oldConfig)) {
|
|
120
|
+
if (!schemaKeys.has(key)) {
|
|
121
|
+
changed = true;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (changed) {
|
|
126
|
+
await execute(
|
|
127
|
+
`UPDATE extension_bindings
|
|
128
|
+
SET config = ?, updated_at = CURRENT_TIMESTAMP
|
|
129
|
+
WHERE id = ?`,
|
|
130
|
+
[JSON.stringify(newConfig), binding.id]
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
} catch (e) {
|
|
135
|
+
console.error(`同步插件 ${pluginId} 绑定配置失败:`, e);
|
|
136
|
+
// 不抛出错误,避免影响主流程
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
79
140
|
// GET: 获取所有插件或单个插件
|
|
80
141
|
export async function GET(request: NextRequest) {
|
|
81
142
|
try {
|
|
@@ -106,7 +167,10 @@ export async function GET(request: NextRequest) {
|
|
|
106
167
|
|
|
107
168
|
query += ' ORDER BY updated_at DESC';
|
|
108
169
|
const [rows] = await execute(query, params);
|
|
109
|
-
const plugins = await Promise.all(
|
|
170
|
+
const plugins = await Promise.all(
|
|
171
|
+
(rows as Record<string, unknown>[])
|
|
172
|
+
.map(r => fillPluginCode(rowToPlugin(r)))
|
|
173
|
+
);
|
|
110
174
|
return NextResponse.json(plugins);
|
|
111
175
|
} catch (e) {
|
|
112
176
|
console.error('获取插件失败:', e);
|
|
@@ -119,14 +183,57 @@ export async function POST(request: NextRequest) {
|
|
|
119
183
|
try {
|
|
120
184
|
await ensureDb();
|
|
121
185
|
const body = await request.json();
|
|
122
|
-
const { id, name, version, description, author, type, icon, minAppVersion, code, configSchema, uiSlots, commonPermissions, exclusivePermissions, requiredPermissions, publicExports, dependencies } = body;
|
|
186
|
+
const { id, name, version, description, author, type, icon, minAppVersion, code, configSchema, uiSlots, commonPermissions, exclusivePermissions, requiredPermissions, publicExports, dependencies, commands } = body;
|
|
123
187
|
|
|
124
|
-
if (!id || !name || !version || !type
|
|
125
|
-
return NextResponse.json({ error: '缺少必填字段 (id, name, version, type
|
|
188
|
+
if (!id || !name || !version || !type) {
|
|
189
|
+
return NextResponse.json({ error: '缺少必填字段 (id, name, version, type)' }, { status: 400 });
|
|
126
190
|
}
|
|
127
191
|
|
|
128
|
-
|
|
129
|
-
|
|
192
|
+
let fileEntry: string;
|
|
193
|
+
|
|
194
|
+
if (code) {
|
|
195
|
+
// 有 code 时,写入单个代码文件(兼容旧流程)
|
|
196
|
+
fileEntry = await writePluginCode(id, code);
|
|
197
|
+
} else {
|
|
198
|
+
// 无 code 时,生成 .xye 结构(manifest.json + plugin.js)
|
|
199
|
+
const pluginDir = getPluginDir(id);
|
|
200
|
+
await fs.mkdir(pluginDir, { recursive: true });
|
|
201
|
+
|
|
202
|
+
// 生成 manifest.json
|
|
203
|
+
const manifest = {
|
|
204
|
+
pluginEntry: 'plugin.js',
|
|
205
|
+
id,
|
|
206
|
+
name,
|
|
207
|
+
version,
|
|
208
|
+
description: description || '',
|
|
209
|
+
author: author || '',
|
|
210
|
+
type,
|
|
211
|
+
icon: icon || undefined,
|
|
212
|
+
minAppVersion: minAppVersion || undefined,
|
|
213
|
+
configSchema: configSchema || [],
|
|
214
|
+
uiSlots: uiSlots || [],
|
|
215
|
+
commonPermissions: commonPermissions || [],
|
|
216
|
+
exclusivePermissions: exclusivePermissions || [],
|
|
217
|
+
requiredPermissions: requiredPermissions || [],
|
|
218
|
+
publicExports: publicExports || null,
|
|
219
|
+
dependencies: dependencies || [],
|
|
220
|
+
commands: commands || [],
|
|
221
|
+
};
|
|
222
|
+
await fs.writeFile(
|
|
223
|
+
path.join(pluginDir, 'manifest.json'),
|
|
224
|
+
JSON.stringify(manifest, null, 2),
|
|
225
|
+
'utf-8'
|
|
226
|
+
);
|
|
227
|
+
|
|
228
|
+
// 生成默认 plugin.js
|
|
229
|
+
const defaultCode = `function setup(xinyu) {\n // 在此编写插件逻辑\n \n}\n`;
|
|
230
|
+
await fs.writeFile(path.join(pluginDir, 'plugin.js'), defaultCode, 'utf-8');
|
|
231
|
+
|
|
232
|
+
fileEntry = getPluginCodeRelativePath(id, 'plugin.js');
|
|
233
|
+
|
|
234
|
+
// 写入 manifest.json → 同步数据库字段
|
|
235
|
+
// 由于这是新插件,database fields will use the body params below
|
|
236
|
+
}
|
|
130
237
|
|
|
131
238
|
const permissionsJson = JSON.stringify({
|
|
132
239
|
commonPermissions: commonPermissions || [],
|
|
@@ -135,19 +242,28 @@ export async function POST(request: NextRequest) {
|
|
|
135
242
|
});
|
|
136
243
|
|
|
137
244
|
await execute(
|
|
138
|
-
`INSERT INTO extensions (id, name, version, description, author, type, icon, min_app_version,
|
|
139
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
245
|
+
`INSERT INTO extensions (id, name, version, description, author, type, icon, min_app_version, file_entry, config_schema, ui_slots, ui_permissions, public_exports, dependencies, commands)
|
|
246
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
140
247
|
ON CONFLICT(id) DO UPDATE SET
|
|
141
248
|
name = excluded.name, version = excluded.version, description = excluded.description,
|
|
142
249
|
author = excluded.author, type = excluded.type, icon = excluded.icon,
|
|
143
|
-
min_app_version = excluded.min_app_version,
|
|
250
|
+
min_app_version = excluded.min_app_version, file_entry = excluded.file_entry,
|
|
144
251
|
config_schema = excluded.config_schema, ui_slots = excluded.ui_slots,
|
|
145
252
|
ui_permissions = excluded.ui_permissions,
|
|
146
|
-
public_exports = excluded.public_exports, dependencies = excluded.dependencies
|
|
147
|
-
|
|
253
|
+
public_exports = excluded.public_exports, dependencies = excluded.dependencies,
|
|
254
|
+
commands = excluded.commands`,
|
|
255
|
+
[id, name, version, description || '', author || '', type, icon || '', minAppVersion || '', fileEntry,
|
|
148
256
|
JSON.stringify(configSchema || []), JSON.stringify(uiSlots || []),
|
|
149
257
|
permissionsJson,
|
|
150
|
-
JSON.stringify(publicExports || null), JSON.stringify(dependencies || null)]
|
|
258
|
+
JSON.stringify(publicExports || null), JSON.stringify(dependencies || null), JSON.stringify(commands || null)]
|
|
259
|
+
);
|
|
260
|
+
|
|
261
|
+
// 为新插件创建默认全局绑定
|
|
262
|
+
await execute(
|
|
263
|
+
`INSERT INTO extension_bindings (extension_id, scope, world_id, enabled, config, sort_order)
|
|
264
|
+
VALUES (?, 'global', '', 0, '{}', 0)
|
|
265
|
+
ON CONFLICT(extension_id, scope, world_id) DO NOTHING`,
|
|
266
|
+
[id]
|
|
151
267
|
);
|
|
152
268
|
|
|
153
269
|
return NextResponse.json({ success: true, id });
|
|
@@ -157,137 +273,115 @@ export async function POST(request: NextRequest) {
|
|
|
157
273
|
}
|
|
158
274
|
}
|
|
159
275
|
|
|
160
|
-
// PUT
|
|
276
|
+
// app/api/plugins/route.ts - PUT 方法
|
|
277
|
+
|
|
161
278
|
export async function PUT(request: NextRequest) {
|
|
162
279
|
try {
|
|
163
|
-
await ensureDb();
|
|
164
280
|
const body = await request.json();
|
|
165
|
-
const { id, newId, ...
|
|
281
|
+
const { id, newId, ...updateFields } = body;
|
|
166
282
|
|
|
167
283
|
if (!id) {
|
|
168
284
|
return NextResponse.json({ error: '缺少插件 ID' }, { status: 400 });
|
|
169
285
|
}
|
|
170
286
|
|
|
171
|
-
|
|
287
|
+
await ensureDb();
|
|
288
|
+
|
|
289
|
+
let finalId: string = id;
|
|
172
290
|
if (newId && newId !== id) {
|
|
173
|
-
// 检查新 ID 是否已存在
|
|
174
291
|
const [existing] = await execute('SELECT id FROM extensions WHERE id = ?', [newId]);
|
|
175
292
|
if ((existing as Record<string, unknown>[]).length > 0) {
|
|
176
293
|
return NextResponse.json({ error: `插件 ID「${newId}」已存在` }, { status: 409 });
|
|
177
294
|
}
|
|
178
|
-
|
|
179
|
-
const rawDb = (await import('@/lib/db')).getRawDb();
|
|
180
|
-
rawDb
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
await execute('UPDATE extension_bindings SET extension_id = ? WHERE extension_id = ?', [newId, id]);
|
|
184
|
-
} finally {
|
|
185
|
-
rawDb?.run('PRAGMA foreign_keys = ON');
|
|
186
|
-
}
|
|
187
|
-
// 后续字段更新使用新 ID
|
|
188
|
-
fields.id = newId;
|
|
189
|
-
// 重命名插件目录(data/plugins/{oldId}/ → data/plugins/{newId}/)
|
|
295
|
+
|
|
296
|
+
const rawDb = (await import('@/lib/server/db')).getRawDb();
|
|
297
|
+
if (!rawDb) return NextResponse.json({ error: '数据库连接失败' }, { status: 500 });
|
|
298
|
+
|
|
299
|
+
// 先重命名目录(失败则提前返回,不碰数据库)
|
|
190
300
|
const oldDir = getPluginDir(id);
|
|
191
301
|
const newDir = getPluginDir(newId);
|
|
192
302
|
try {
|
|
193
303
|
await fs.rename(oldDir, newDir);
|
|
194
|
-
} catch (
|
|
195
|
-
console.error(`重命名插件目录失败: ${oldDir} → ${newDir}`,
|
|
196
|
-
// 回滚数据库
|
|
197
|
-
rawDb?.run('PRAGMA foreign_keys = OFF');
|
|
198
|
-
try {
|
|
199
|
-
await execute('UPDATE extensions SET id = ? WHERE id = ?', [id, newId]);
|
|
200
|
-
await execute('UPDATE extension_bindings SET extension_id = ? WHERE extension_id = ?', [id, newId]);
|
|
201
|
-
} finally {
|
|
202
|
-
rawDb?.run('PRAGMA foreign_keys = ON');
|
|
203
|
-
}
|
|
304
|
+
} catch (err) {
|
|
305
|
+
console.error(`重命名插件目录失败: ${oldDir} → ${newDir}`, err);
|
|
204
306
|
return NextResponse.json({ error: '重命名插件目录失败' }, { status: 500 });
|
|
205
307
|
}
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
308
|
+
|
|
309
|
+
// 目录成功 → 更新数据库(临时禁用外键)
|
|
310
|
+
rawDb.run('PRAGMA foreign_keys = OFF');
|
|
311
|
+
try {
|
|
312
|
+
await execute('UPDATE extensions SET id = ? WHERE id = ?', [newId, id]);
|
|
313
|
+
await execute('UPDATE extension_bindings SET extension_id = ? WHERE extension_id = ?', [newId, id]);
|
|
314
|
+
await execute('UPDATE plugin_storage SET plugin_id = ? WHERE plugin_id = ?', [newId, id]);
|
|
315
|
+
finalId = newId;
|
|
316
|
+
} finally {
|
|
317
|
+
rawDb.run('PRAGMA foreign_keys = ON');
|
|
215
318
|
}
|
|
216
319
|
}
|
|
217
320
|
|
|
218
|
-
|
|
219
|
-
const
|
|
220
|
-
|
|
221
|
-
const allowedFields = ['name', 'version', 'description', 'author', 'type', 'icon', 'minAppVersion', 'code', 'configSchema', 'uiSlots', 'commonPermissions', 'exclusivePermissions', 'requiredPermissions', 'publicExports', 'dependencies', 'id'];
|
|
222
|
-
const columnMap: Record<string, string> = {
|
|
223
|
-
minAppVersion: 'min_app_version',
|
|
224
|
-
configSchema: 'config_schema',
|
|
225
|
-
uiSlots: 'ui_slots',
|
|
226
|
-
commonPermissions: 'ui_permissions',
|
|
227
|
-
exclusivePermissions: 'ui_permissions',
|
|
228
|
-
requiredPermissions: 'ui_permissions',
|
|
229
|
-
publicExports: 'public_exports',
|
|
230
|
-
dependencies: 'dependencies',
|
|
231
|
-
};
|
|
321
|
+
// 更新其他字段
|
|
322
|
+
const updated: Map<string, any> = new Map<string, any>();
|
|
232
323
|
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
if (fields.code !== undefined) {
|
|
236
|
-
const [existingRows] = await execute('SELECT code_path FROM extensions WHERE id = ?', [targetId]);
|
|
237
|
-
const erRows = existingRows as Record<string, string>[];
|
|
238
|
-
const currentCodePath = erRows.length > 0 ? erRows[0].code_path : null;
|
|
239
|
-
const entryFile = currentCodePath ? currentCodePath.split('/').pop() : undefined;
|
|
240
|
-
const codePath = await writePluginCode(targetId, String(fields.code), entryFile);
|
|
241
|
-
updates.push('code_path = ?');
|
|
242
|
-
params.push(codePath);
|
|
243
|
-
delete fields.code;
|
|
324
|
+
if (updateFields.name !== undefined) {
|
|
325
|
+
updated.set('name', updateFields.name);
|
|
244
326
|
}
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
const permFields = ['commonPermissions', 'exclusivePermissions', 'requiredPermissions'];
|
|
248
|
-
const hasPermUpdate = permFields.some(f => fields[f] !== undefined);
|
|
249
|
-
|
|
250
|
-
if (hasPermUpdate) {
|
|
251
|
-
const [rows] = await execute('SELECT ui_permissions FROM extensions WHERE id = ?', [targetId]);
|
|
252
|
-
const permRows = rows as Record<string, unknown>[];
|
|
253
|
-
const currentPerms = permRows.length > 0 ? parsePermissions(permRows[0].ui_permissions) : {};
|
|
254
|
-
const newPerms = {
|
|
255
|
-
commonPermissions: fields.commonPermissions !== undefined ? fields.commonPermissions : (currentPerms.commonPermissions || []),
|
|
256
|
-
exclusivePermissions: fields.exclusivePermissions !== undefined ? fields.exclusivePermissions : (currentPerms.exclusivePermissions || []),
|
|
257
|
-
requiredPermissions: fields.requiredPermissions !== undefined ? fields.requiredPermissions : (currentPerms.requiredPermissions || []),
|
|
258
|
-
};
|
|
259
|
-
updates.push('ui_permissions = ?');
|
|
260
|
-
params.push(JSON.stringify(newPerms));
|
|
327
|
+
if (updateFields.version !== undefined) {
|
|
328
|
+
updated.set('version', updateFields.version);
|
|
261
329
|
}
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
330
|
+
if (updateFields.description !== undefined) {
|
|
331
|
+
updated.set('description', updateFields.description);
|
|
332
|
+
}
|
|
333
|
+
if (updateFields.author !== undefined) {
|
|
334
|
+
updated.set('author', updateFields.author);
|
|
335
|
+
}
|
|
336
|
+
if (updateFields.type !== undefined) {
|
|
337
|
+
updated.set('type', updateFields.type);
|
|
338
|
+
}
|
|
339
|
+
if (updateFields.icon !== undefined) {
|
|
340
|
+
updated.set('icon', updateFields.icon);
|
|
341
|
+
}
|
|
342
|
+
if (updateFields.configSchema !== undefined) {
|
|
343
|
+
updated.set('config_schema', JSON.stringify(updateFields.configSchema));
|
|
344
|
+
}
|
|
345
|
+
if (updateFields.publicExports !== undefined) {
|
|
346
|
+
updated.set('public_exports', JSON.stringify(updateFields.publicExports));
|
|
347
|
+
}
|
|
348
|
+
if (updateFields.dependencies !== undefined) {
|
|
349
|
+
updated.set('dependencies', JSON.stringify(updateFields.dependencies));
|
|
350
|
+
}
|
|
351
|
+
if (updateFields.commands !== undefined) {
|
|
352
|
+
updated.set('commands', JSON.stringify(updateFields.commands));
|
|
353
|
+
}
|
|
354
|
+
if (updateFields.pluginEntry !== undefined) {
|
|
355
|
+
updated.set('file_entry', updateFields.pluginEntry);
|
|
356
|
+
}
|
|
357
|
+
if (updateFields.minAppVersion !== undefined) {
|
|
358
|
+
updated.set('min_app_version', updateFields.minAppVersion);
|
|
359
|
+
}
|
|
360
|
+
// 处理权限字段
|
|
361
|
+
if (updateFields.commonPermissions !== undefined ||
|
|
362
|
+
updateFields.exclusivePermissions !== undefined ||
|
|
363
|
+
updateFields.requiredPermissions !== undefined) {
|
|
364
|
+
updated.set('ui_permissions', JSON.stringify({
|
|
365
|
+
commonPermissions: updateFields.commonPermissions || [],
|
|
366
|
+
exclusivePermissions: updateFields.exclusivePermissions || [],
|
|
367
|
+
requiredPermissions: updateFields.requiredPermissions || [],
|
|
368
|
+
}));
|
|
275
369
|
}
|
|
276
370
|
|
|
277
|
-
if (
|
|
278
|
-
|
|
371
|
+
if (updated.size > 0) {
|
|
372
|
+
await execute(`UPDATE extensions SET ${Array.from(updated.keys()).map(k => `${k} = ?`).join(', ')} WHERE id = ?`,
|
|
373
|
+
[...Array.from(updated.values()), finalId]);
|
|
279
374
|
}
|
|
280
375
|
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
);
|
|
376
|
+
// 同步绑定配置
|
|
377
|
+
if (updateFields.configSchema !== undefined) {
|
|
378
|
+
await syncBindingsForPlugin(finalId, updateFields.configSchema as any[]);
|
|
379
|
+
}
|
|
286
380
|
|
|
287
|
-
return NextResponse.json({ success: true, id:
|
|
381
|
+
return NextResponse.json({ success: true, id: finalId });
|
|
288
382
|
} catch (e) {
|
|
289
383
|
console.error('更新插件失败:', e);
|
|
290
|
-
return NextResponse.json({ error: '
|
|
384
|
+
return NextResponse.json({ error: '更新失败' }, { status: 500 });
|
|
291
385
|
}
|
|
292
386
|
}
|
|
293
387
|
|
|
@@ -303,6 +397,8 @@ export async function DELETE(request: NextRequest) {
|
|
|
303
397
|
}
|
|
304
398
|
|
|
305
399
|
await execute('DELETE FROM extensions WHERE id = ?', [id]);
|
|
400
|
+
await execute("DELETE FROM extension_bindings WHERE extension_id = ?", [id]);
|
|
401
|
+
await execute('DELETE FROM plugin_storage WHERE plugin_id = ?', [id]);
|
|
306
402
|
await deletePluginCode(id);
|
|
307
403
|
return NextResponse.json({ success: true, id });
|
|
308
404
|
} catch (e) {
|
|
@@ -213,7 +213,7 @@ export async function POST(request: NextRequest) {
|
|
|
213
213
|
/** 尝试使用 AI API 进行深度分析 */
|
|
214
214
|
async function aiAnalysis(code: string, manifest?: ScanRequest['manifest']): Promise<ScanResult | null> {
|
|
215
215
|
try {
|
|
216
|
-
const { getAiConfig } = await import('@/lib/get-ai-config');
|
|
216
|
+
const { getAiConfig } = await import('@/lib/server/get-ai-config');
|
|
217
217
|
const config = await getAiConfig();
|
|
218
218
|
if (!config.apiKey) return null;
|
|
219
219
|
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
// app/api/plugins/storage/route.ts - 插件持久化存储 API
|
|
2
2
|
|
|
3
3
|
import { NextRequest, NextResponse } from 'next/server';
|
|
4
|
-
import { execute } from '@/lib/db';
|
|
5
|
-
import { ensureDb } from '@/lib/db-init';
|
|
4
|
+
import { execute } from '@/lib/server/db';
|
|
5
|
+
import { ensureDb } from '@/lib/server/db-init';
|
|
6
6
|
|
|
7
7
|
/** 验证 pluginId 格式(防止 SQL 注入) */
|
|
8
8
|
function isValidPluginId(id: string): boolean {
|
|
@@ -11,7 +11,7 @@ function isValidPluginId(id: string): boolean {
|
|
|
11
11
|
|
|
12
12
|
/** 验证 key 格式 */
|
|
13
13
|
function isValidKey(key: string): boolean {
|
|
14
|
-
return
|
|
14
|
+
return key.length > 0 && key.length <= 200;
|
|
15
15
|
}
|
|
16
16
|
|
|
17
17
|
/**
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
// app/api/plugins/toggle/route.ts - 插件切换 API
|
|
2
|
+
|
|
3
|
+
import { NextRequest, NextResponse } from 'next/server';
|
|
4
|
+
import { ensureDb } from '@/lib/server/db-init';
|
|
5
|
+
import { enablePlugin, disablePlugin } from "@/lib/server/plugin-utils";
|
|
6
|
+
|
|
7
|
+
export async function POST(request: NextRequest) {
|
|
8
|
+
try {
|
|
9
|
+
await ensureDb();
|
|
10
|
+
const { pluginId, enabled, scope, worldId } = await request.json();
|
|
11
|
+
if (enabled) {
|
|
12
|
+
await enablePlugin(pluginId, scope, worldId);
|
|
13
|
+
} else {
|
|
14
|
+
await disablePlugin(pluginId, scope, worldId);
|
|
15
|
+
}
|
|
16
|
+
return NextResponse.json({ success: true });
|
|
17
|
+
} catch (e) {
|
|
18
|
+
console.error('插件状态切换失败:', e);
|
|
19
|
+
return NextResponse.json({ error: '插件状态切换失败' }, { status: 500 });
|
|
20
|
+
}
|
|
21
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { NextRequest, NextResponse } from 'next/server';
|
|
2
|
-
import { execute } from '@/lib/db';
|
|
3
|
-
import { ensureDb } from '@/lib/db-init';
|
|
2
|
+
import { execute } from '@/lib/server/db';
|
|
3
|
+
import { ensureDb } from '@/lib/server/db-init';
|
|
4
4
|
|
|
5
5
|
interface SessionRow {
|
|
6
6
|
id: string;
|
|
@@ -108,8 +108,9 @@ export async function DELETE(request: NextRequest) {
|
|
|
108
108
|
const id = searchParams.get('id');
|
|
109
109
|
|
|
110
110
|
if (action === 'clearAll') {
|
|
111
|
-
await execute('DELETE FROM game_sessions');
|
|
111
|
+
await execute('DELETE FROM game_sessions WHERE TRUE');
|
|
112
112
|
await execute("DELETE FROM extension_bindings WHERE scope = 'world'");
|
|
113
|
+
await execute("DELETE FROM plugin_storage WHERE TRUE");
|
|
113
114
|
return NextResponse.json({ success: true, message: '已清除所有会话' });
|
|
114
115
|
}
|
|
115
116
|
|
|
@@ -123,6 +124,10 @@ export async function DELETE(request: NextRequest) {
|
|
|
123
124
|
"DELETE FROM extension_bindings WHERE scope = 'world' AND world_id = ?",
|
|
124
125
|
[id]
|
|
125
126
|
);
|
|
127
|
+
await execute(
|
|
128
|
+
"DELETE FROM plugin_storage WHERE world_id = ?",
|
|
129
|
+
[id]
|
|
130
|
+
);
|
|
126
131
|
return NextResponse.json({ success: true, id });
|
|
127
132
|
}
|
|
128
133
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { NextRequest, NextResponse } from 'next/server';
|
|
2
|
-
import { execute } from '@/lib/db';
|
|
3
|
-
import { ensureDb } from '@/lib/db-init';
|
|
2
|
+
import { execute } from '@/lib/server/db';
|
|
3
|
+
import { ensureDb } from '@/lib/server/db-init';
|
|
4
4
|
|
|
5
5
|
export const dynamic = 'force-dynamic';
|
|
6
6
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// app/api/suggest-fields/route.ts - AI 智能补全字段 API
|
|
2
2
|
|
|
3
3
|
import { NextRequest, NextResponse } from 'next/server';
|
|
4
|
-
import { getAiConfig } from '@/lib/get-ai-config';
|
|
4
|
+
import { getAiConfig } from '@/lib/server/get-ai-config';
|
|
5
5
|
|
|
6
6
|
const SYSTEM_PROMPT = `你是一个 RPG 世界卡片设计专家。根据模板名称和已有字段的键名、类型,为每个字段智能补全默认值、占位文本、选项等内容。
|
|
7
7
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { NextRequest, NextResponse } from 'next/server';
|
|
2
|
-
import { execute } from '@/lib/db';
|
|
3
|
-
import { ensureDb } from '@/lib/db-init';
|
|
2
|
+
import { execute } from '@/lib/server/db';
|
|
3
|
+
import { ensureDb } from '@/lib/server/db-init';
|
|
4
4
|
|
|
5
5
|
interface TemplateRow {
|
|
6
6
|
id: string;
|