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.
Files changed (105) hide show
  1. package/app/api/chat/route.ts +2 -2
  2. package/app/api/generate-svg/route.ts +1 -1
  3. package/app/api/generate-theme/route.ts +112 -135
  4. package/app/api/plugins/bindings/route.ts +4 -4
  5. package/app/api/plugins/export/route.ts +9 -3
  6. package/app/api/plugins/export-xye/route.ts +12 -7
  7. package/app/api/plugins/files/route.ts +9 -9
  8. package/app/api/plugins/files-list/route.ts +7 -3
  9. package/app/api/plugins/files-upload/route.ts +3 -3
  10. package/app/api/plugins/files-write/all/route.ts +250 -0
  11. package/app/api/plugins/files-write/route.ts +13 -15
  12. package/app/api/plugins/import/route.ts +5 -5
  13. package/app/api/plugins/import-package/route.ts +9 -9
  14. package/app/api/plugins/resources/route.ts +6 -2
  15. package/app/api/plugins/route.ts +215 -119
  16. package/app/api/plugins/scan/route.ts +1 -1
  17. package/app/api/plugins/storage/route.ts +3 -3
  18. package/app/api/plugins/toggle/route.ts +21 -0
  19. package/app/api/sessions/route.ts +8 -3
  20. package/app/api/settings/route.ts +2 -2
  21. package/app/api/suggest-fields/route.ts +1 -1
  22. package/app/api/templates/route.ts +2 -2
  23. package/app/api/themes/route.ts +116 -0
  24. package/app/editor/page.tsx +21 -4
  25. package/app/extensions/edit/[id]/page.tsx +2148 -984
  26. package/app/extensions/page.tsx +477 -86
  27. package/app/extensions/tutorial/code-examples.ts +833 -0
  28. package/app/extensions/tutorial/nav-link.tsx +27 -0
  29. package/app/extensions/tutorial/page.tsx +47 -3616
  30. package/app/extensions/tutorial/sections/AiInterceptionSection.tsx +87 -0
  31. package/app/extensions/tutorial/sections/ArchitectureSection.tsx +39 -0
  32. package/app/extensions/tutorial/sections/BestPracticesSection.tsx +78 -0
  33. package/app/extensions/tutorial/sections/ChatApiSection.tsx +212 -0
  34. package/app/extensions/tutorial/sections/CrossPluginSection.tsx +90 -0
  35. package/app/extensions/tutorial/sections/CssVariablesSection.tsx +189 -0
  36. package/app/extensions/tutorial/sections/DebugSection.tsx +48 -0
  37. package/app/extensions/tutorial/sections/DependencySection.tsx +134 -0
  38. package/app/extensions/tutorial/sections/DomSandboxSection.tsx +229 -0
  39. package/app/extensions/tutorial/sections/ExamplesSection.tsx +29 -0
  40. package/app/extensions/tutorial/sections/FileApiSection.tsx +44 -0
  41. package/app/extensions/tutorial/sections/GlobalApiSection.tsx +50 -0
  42. package/app/extensions/tutorial/sections/InputApiSection.tsx +213 -0
  43. package/app/extensions/tutorial/sections/LifecycleSection.tsx +105 -0
  44. package/app/extensions/tutorial/sections/OverviewSection.tsx +54 -0
  45. package/app/extensions/tutorial/sections/PermissionsSection.tsx +239 -0
  46. package/app/extensions/tutorial/sections/PersistentStorageSection.tsx +49 -0
  47. package/app/extensions/tutorial/sections/PluginConfigSection.tsx +81 -0
  48. package/app/extensions/tutorial/sections/PluginImportExportSection.tsx +68 -0
  49. package/app/extensions/tutorial/sections/PluginStorageSection.tsx +94 -0
  50. package/app/extensions/tutorial/sections/ResourceApiSection.tsx +88 -0
  51. package/app/extensions/tutorial/sections/SecuritySection.tsx +106 -0
  52. package/app/extensions/tutorial/sections/SlotsSection.tsx +130 -0
  53. package/app/extensions/tutorial/sections/UiApiSection.tsx +147 -0
  54. package/app/extensions/tutorial/sections/UtilsApiSection.tsx +96 -0
  55. package/app/extensions/tutorial/sections-data.tsx +325 -0
  56. package/app/extensions/tutorial/styles.ts +59 -0
  57. package/app/extensions/tutorial/types.ts +14 -0
  58. package/app/game/[id]/page.tsx +336 -148
  59. package/app/globals.css +41 -0
  60. package/app/page.tsx +17 -10
  61. package/app/settings/page.tsx +218 -236
  62. package/bin/cli.js +3 -10
  63. package/components/ChatInput.tsx +20 -6
  64. package/components/ChatWindow.tsx +1 -1
  65. package/components/MessageBubble.tsx +5 -6
  66. package/components/ui/ConfirmDialog.tsx +28 -6
  67. package/components/ui/DeprecatedBadge.tsx +163 -0
  68. package/components/ui/PageHeader.tsx +1 -1
  69. package/components/ui/PermissionConflictDialog.tsx +1 -1
  70. package/components/ui/PluginConfigForm.tsx +2 -3
  71. package/components/ui/PluginFloatingLayer.tsx +2 -2
  72. package/components/ui/PluginModalRenderer.tsx +84 -38
  73. package/components/ui/PluginProvider.tsx +32 -13
  74. package/components/ui/PluginSlotRenderer.tsx +2 -2
  75. package/components/ui/ThemeCustomizer.tsx +4 -7
  76. package/components/ui/ThemeProvider.tsx +214 -125
  77. package/components/ui/ThemeSwitcher.tsx +119 -157
  78. package/components/ui/ToastProvider.tsx +47 -17
  79. package/lib/{parseWorldCard.ts → client/parseWorldCard.ts} +1 -1
  80. package/lib/{plugin-dom-sandbox.ts → client/plugin-dom-sandbox.ts} +2 -2
  81. package/lib/{plugin-runtime.ts → client/plugin-runtime.ts} +166 -67
  82. package/lib/client/plugin-utils.ts +105 -0
  83. package/lib/{router-history.ts → client/router-history.ts} +1 -1
  84. package/lib/{storage.ts → client/storage.ts} +66 -3
  85. package/lib/{themes.ts → client/themes.ts} +21 -1
  86. package/lib/{builtin-plugins.ts → server/builtin-plugins.ts} +1 -1
  87. package/lib/{db-init.ts → server/db-init.ts} +2 -2
  88. package/lib/{db.ts → server/db.ts} +48 -8
  89. package/lib/{get-ai-config.ts → server/get-ai-config.ts} +2 -2
  90. package/lib/{plugin-files.ts → server/plugin-files.ts} +7 -2
  91. package/lib/server/plugin-utils.ts +88 -0
  92. package/lib/{manifest-parser.ts → shared/manifest-parser.ts} +4 -4
  93. package/lib/{plugin-types.ts → shared/plugin-types.ts} +11 -6
  94. package/lib/{version.ts → shared/version.ts} +1 -1
  95. package/next.config.mjs +3 -2
  96. package/package.json +4 -3
  97. package/styles/themes.css +7 -2
  98. package/version.json +2 -2
  99. package/app/extensions/create/page.tsx +0 -1422
  100. /package/lib/{plugin-events.ts → client/plugin-events.ts} +0 -0
  101. /package/lib/{plugin-resource-tracker.ts → client/plugin-resource-tracker.ts} +0 -0
  102. /package/lib/{plugin-html-sanitizer.ts → shared/plugin-html-sanitizer.ts} +0 -0
  103. /package/lib/{plugin-security.ts → shared/plugin-security.ts} +0 -0
  104. /package/lib/{prompt-builder.ts → shared/prompt-builder.ts} +0 -0
  105. /package/lib/{types.ts → shared/types.ts} +0 -0
@@ -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 { writePluginCode, deletePluginCode, getPluginDir } from '@/lib/plugin-files';
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 codeFileName = plugin.codePath ? (plugin.codePath.split('/').pop() || 'plugin.js') : 'plugin.js';
70
- const pluginDir = getPluginDir(plugin.id as string);
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((rows as Record<string, unknown>[]).map(r => fillPluginCode(rowToPlugin(r))));
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 || !code) {
125
- return NextResponse.json({ error: '缺少必填字段 (id, name, version, type, code)' }, { status: 400 });
188
+ if (!id || !name || !version || !type) {
189
+ return NextResponse.json({ error: '缺少必填字段 (id, name, version, type)' }, { status: 400 });
126
190
  }
127
191
 
128
- // 将代码写入文件
129
- const codePath = await writePluginCode(id, code);
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, code_path, config_schema, ui_slots, ui_permissions, public_exports, dependencies)
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, code_path = excluded.code_path,
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
- [id, name, version, description || '', author || '', type, icon || '', minAppVersion || '', codePath,
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, ...fields } = body;
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
- // 处理 ID 变更
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
- // 更新子表引用和父表 ID
179
- const rawDb = (await import('@/lib/db')).getRawDb();
180
- rawDb?.run('PRAGMA foreign_keys = OFF');
181
- try {
182
- await execute('UPDATE extensions SET id = ? WHERE id = ?', [newId, id]);
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 (e) {
195
- console.error(`重命名插件目录失败: ${oldDir} → ${newDir}`, e);
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
- // 更新 code_path 中的目录名
207
- const [codePathRows] = await execute('SELECT code_path FROM extensions WHERE id = ?', [newId]);
208
- const cpRows = codePathRows as Record<string, string>[];
209
- if (cpRows.length > 0 && cpRows[0].code_path) {
210
- const oldCodePath = cpRows[0].code_path;
211
- const newCodePath = oldCodePath.replace(`data/plugins/${id}/`, `data/plugins/${newId}/`);
212
- if (newCodePath !== oldCodePath) {
213
- await execute('UPDATE extensions SET code_path = ? WHERE id = ?', [newCodePath, newId]);
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
- const updates: string[] = [];
219
- const params: (string | number | null)[] = [];
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
- // code 字段特殊处理:写入文件,数据库存路径
234
- const targetId = newId || id;
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
- for (const field of allowedFields) {
264
- if (permFields.includes(field)) continue;
265
- if (field === 'id') continue;
266
- if (fields[field] !== undefined) {
267
- const col = columnMap[field] || field;
268
- updates.push(`${col} = ?`);
269
- if (field === 'configSchema' || field === 'uiSlots' || field === 'publicExports' || field === 'dependencies') {
270
- params.push(JSON.stringify(fields[field]));
271
- } else {
272
- params.push(fields[field]);
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 (updates.length === 0) {
278
- return NextResponse.json({ success: true, id: targetId });
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
- params.push(targetId);
282
- await execute(
283
- `UPDATE extensions SET ${updates.join(', ')} WHERE id = ?`,
284
- params
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: targetId });
381
+ return NextResponse.json({ success: true, id: finalId });
288
382
  } catch (e) {
289
383
  console.error('更新插件失败:', e);
290
- return NextResponse.json({ error: '更新插件失败' }, { status: 500 });
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 typeof key === 'string' && key.length > 0 && key.length <= 200;
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;