xinyu-pro 0.21.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 (96) hide show
  1. package/.env.example +21 -0
  2. package/README.md +36 -0
  3. package/app/api/chat/route.ts +84 -0
  4. package/app/api/generate-svg/route.ts +171 -0
  5. package/app/api/generate-theme/route.ts +137 -0
  6. package/app/api/plugins/bindings/route.ts +173 -0
  7. package/app/api/plugins/export/route.ts +122 -0
  8. package/app/api/plugins/export-xye/route.ts +156 -0
  9. package/app/api/plugins/files/route.ts +146 -0
  10. package/app/api/plugins/files-list/route.ts +168 -0
  11. package/app/api/plugins/files-upload/route.ts +101 -0
  12. package/app/api/plugins/files-write/route.ts +272 -0
  13. package/app/api/plugins/import/route.ts +140 -0
  14. package/app/api/plugins/import-package/route.ts +231 -0
  15. package/app/api/plugins/resources/route.ts +109 -0
  16. package/app/api/plugins/route.ts +308 -0
  17. package/app/api/plugins/scan/route.ts +280 -0
  18. package/app/api/plugins/storage/route.ts +146 -0
  19. package/app/api/sessions/route.ts +165 -0
  20. package/app/api/settings/route.ts +40 -0
  21. package/app/api/suggest-fields/route.ts +129 -0
  22. package/app/api/templates/route.ts +159 -0
  23. package/app/api/test-api/route.ts +63 -0
  24. package/app/editor/page.tsx +1466 -0
  25. package/app/extensions/create/page.tsx +1422 -0
  26. package/app/extensions/edit/[id]/page.tsx +2342 -0
  27. package/app/extensions/page.tsx +1572 -0
  28. package/app/extensions/tutorial/page.tsx +4258 -0
  29. package/app/favicon.ico +0 -0
  30. package/app/fonts/GeistMonoVF.woff +0 -0
  31. package/app/fonts/GeistVF.woff +0 -0
  32. package/app/game/[id]/page.tsx +996 -0
  33. package/app/globals.css +3 -0
  34. package/app/layout.tsx +26 -0
  35. package/app/loading.tsx +26 -0
  36. package/app/page.tsx +345 -0
  37. package/app/settings/page.tsx +1490 -0
  38. package/bin/cli.js +262 -0
  39. package/components/ChatInput.tsx +106 -0
  40. package/components/ChatWindow.tsx +52 -0
  41. package/components/FullPageLoader.tsx +107 -0
  42. package/components/LoadingDots.tsx +20 -0
  43. package/components/MathCurveLoader.tsx +173 -0
  44. package/components/MessageBubble.tsx +147 -0
  45. package/components/WorldCardPreview.tsx +98 -0
  46. package/components/WorldCardUploader.tsx +58 -0
  47. package/components/ui/ConfirmDialog.tsx +135 -0
  48. package/components/ui/PageHeader.tsx +99 -0
  49. package/components/ui/PermissionConflictDialog.tsx +206 -0
  50. package/components/ui/PluginConfigForm.tsx +192 -0
  51. package/components/ui/PluginFloatingLayer.tsx +52 -0
  52. package/components/ui/PluginIcon.tsx +53 -0
  53. package/components/ui/PluginModalRenderer.tsx +185 -0
  54. package/components/ui/PluginProvider.tsx +1038 -0
  55. package/components/ui/PluginSlotRenderer.tsx +76 -0
  56. package/components/ui/ThemeCustomizer.tsx +174 -0
  57. package/components/ui/ThemeProvider.tsx +125 -0
  58. package/components/ui/ThemeSwitcher.tsx +140 -0
  59. package/components/ui/ToastProvider.tsx +141 -0
  60. package/lib/builtin-plugins.ts +11 -0
  61. package/lib/db-init.ts +35 -0
  62. package/lib/db.ts +244 -0
  63. package/lib/manifest-parser.ts +185 -0
  64. package/lib/parseWorldCard.ts +110 -0
  65. package/lib/plugin-dom-sandbox.ts +327 -0
  66. package/lib/plugin-events.ts +88 -0
  67. package/lib/plugin-files.ts +186 -0
  68. package/lib/plugin-html-sanitizer.ts +79 -0
  69. package/lib/plugin-resource-tracker.ts +175 -0
  70. package/lib/plugin-runtime.ts +2287 -0
  71. package/lib/plugin-security.ts +151 -0
  72. package/lib/plugin-types.ts +416 -0
  73. package/lib/prompt-builder.ts +55 -0
  74. package/lib/router-history.ts +119 -0
  75. package/lib/storage.ts +381 -0
  76. package/lib/themes.ts +129 -0
  77. package/lib/types.ts +117 -0
  78. package/lib/version.ts +55 -0
  79. package/next.config.mjs +43 -0
  80. package/package.json +56 -0
  81. package/plugins/xinyu.bag-system.xye +0 -0
  82. package/plugins/xinyu.cache-optimizer.xye +0 -0
  83. package/plugins/xinyu.dice-arbiter.xye +0 -0
  84. package/plugins/xinyu.game-auto-start-choices.xye +0 -0
  85. package/plugins/xinyu.markdown-render.xye +0 -0
  86. package/plugins/xinyu.slot-ui-beautify.xye +0 -0
  87. package/plugins/xinyu.world-info.xye +0 -0
  88. package/postcss.config.mjs +8 -0
  89. package/public/templates/atlantis.svg +63 -0
  90. package/public/templates/cyber-city.svg +68 -0
  91. package/public/templates/jianghu.svg +69 -0
  92. package/public/templates//351/255/224/346/263/225/344/270/226/347/225/214.svg +137 -0
  93. package/styles/themes.css +111 -0
  94. package/tailwind.config.ts +18 -0
  95. package/tsconfig.json +26 -0
  96. package/version.json +6 -0
@@ -0,0 +1,146 @@
1
+ // app/api/plugins/storage/route.ts - 插件持久化存储 API
2
+
3
+ import { NextRequest, NextResponse } from 'next/server';
4
+ import { getDbPool } from '@/lib/db';
5
+ import { ensureDb } from '@/lib/db-init';
6
+
7
+ /** 验证 pluginId 格式(防止 SQL 注入) */
8
+ function isValidPluginId(id: string): boolean {
9
+ return /^[a-z0-9][a-z0-9._-]{0,98}[a-z0-9]$/.test(id);
10
+ }
11
+
12
+ /** 验证 key 格式 */
13
+ function isValidKey(key: string): boolean {
14
+ return typeof key === 'string' && key.length > 0 && key.length <= 200;
15
+ }
16
+
17
+ /**
18
+ * 安全解析 storage_value:
19
+ * MySQL JSON 列会被 mysql2 驱动自动解析为 JS 对象,
20
+ * 此时不需要再 JSON.parse;如果是字符串则需要解析。
21
+ */
22
+ function parseStorageValue(raw: unknown): unknown {
23
+ if (raw === null || raw === undefined) return null;
24
+ if (typeof raw === 'object') return raw; // 已被驱动自动解析
25
+ if (typeof raw === 'string') {
26
+ try { return JSON.parse(raw); } catch { return raw; }
27
+ }
28
+ return raw;
29
+ }
30
+
31
+ // GET /api/plugins/storage?pluginId=xxx&key=yyy&worldId=zzz
32
+ export async function GET(request: NextRequest) {
33
+ try {
34
+ await ensureDb();
35
+ const db = getDbPool();
36
+ const { searchParams } = new URL(request.url);
37
+ const pluginId = searchParams.get('pluginId');
38
+ const key = searchParams.get('key');
39
+ const worldId = searchParams.get('worldId') || '';
40
+
41
+ if (!pluginId || !isValidPluginId(pluginId)) {
42
+ return NextResponse.json({ error: '无效的插件 ID' }, { status: 400 });
43
+ }
44
+
45
+ if (key) {
46
+ // 读取单个 key
47
+ if (!isValidKey(key)) {
48
+ return NextResponse.json({ error: '无效的 key' }, { status: 400 });
49
+ }
50
+ const [rows] = await db.execute(
51
+ 'SELECT storage_value FROM plugin_storage WHERE plugin_id = ? AND storage_key = ? AND world_id = ?',
52
+ [pluginId, key, worldId]
53
+ );
54
+ if (rows.length > 0) {
55
+ const row = rows[0] as { storage_value: unknown };
56
+ const value = parseStorageValue(row.storage_value);
57
+ return NextResponse.json({ success: true, value });
58
+ }
59
+ return NextResponse.json({ success: true, value: null });
60
+ } else {
61
+ // 读取所有 keys
62
+ const [rows] = await db.execute(
63
+ 'SELECT storage_key, storage_value FROM plugin_storage WHERE plugin_id = ? AND world_id = ?',
64
+ [pluginId, worldId]
65
+ );
66
+ const result: Record<string, unknown> = {};
67
+ for (const row of rows as Array<{ storage_key: string; storage_value: unknown }>) {
68
+ result[row.storage_key] = parseStorageValue(row.storage_value);
69
+ }
70
+ return NextResponse.json({ success: true, values: result });
71
+ }
72
+ } catch (e) {
73
+ console.error('[PluginStorage] GET error:', e);
74
+ return NextResponse.json({ error: '读取存储失败' }, { status: 500 });
75
+ }
76
+ }
77
+
78
+ // PUT /api/plugins/storage — 写入/更新
79
+ export async function PUT(request: NextRequest) {
80
+ try {
81
+ await ensureDb();
82
+ const db = getDbPool();
83
+ const body = await request.json();
84
+ const { pluginId, key, value, worldId } = body;
85
+
86
+ if (!pluginId || !isValidPluginId(pluginId)) {
87
+ return NextResponse.json({ error: '无效的插件 ID' }, { status: 400 });
88
+ }
89
+ if (!isValidKey(key)) {
90
+ return NextResponse.json({ error: '无效的 key' }, { status: 400 });
91
+ }
92
+
93
+ const wid = worldId || '';
94
+ const jsonValue = JSON.stringify(value);
95
+
96
+ await db.execute(
97
+ `INSERT INTO plugin_storage (plugin_id, storage_key, storage_value, world_id)
98
+ VALUES (?, ?, ?, ?)
99
+ ON DUPLICATE KEY UPDATE storage_value = VALUES(storage_value)`,
100
+ [pluginId, key, jsonValue, wid]
101
+ );
102
+
103
+ return NextResponse.json({ success: true });
104
+ } catch (e) {
105
+ console.error('[PluginStorage] PUT error:', e);
106
+ return NextResponse.json({ error: '写入存储失败' }, { status: 500 });
107
+ }
108
+ }
109
+
110
+ // DELETE /api/plugins/storage?pluginId=xxx&key=yyy&worldId=zzz
111
+ export async function DELETE(request: NextRequest) {
112
+ try {
113
+ await ensureDb();
114
+ const db = getDbPool();
115
+ const { searchParams } = new URL(request.url);
116
+ const pluginId = searchParams.get('pluginId');
117
+ const key = searchParams.get('key');
118
+ const worldId = searchParams.get('worldId') || '';
119
+
120
+ if (!pluginId || !isValidPluginId(pluginId)) {
121
+ return NextResponse.json({ error: '无效的插件 ID' }, { status: 400 });
122
+ }
123
+
124
+ if (key) {
125
+ // 删除单个 key
126
+ if (!isValidKey(key)) {
127
+ return NextResponse.json({ error: '无效的 key' }, { status: 400 });
128
+ }
129
+ await db.execute(
130
+ 'DELETE FROM plugin_storage WHERE plugin_id = ? AND storage_key = ? AND world_id = ?',
131
+ [pluginId, key, worldId]
132
+ );
133
+ } else {
134
+ // 删除该插件在该世界的所有数据
135
+ await db.execute(
136
+ 'DELETE FROM plugin_storage WHERE plugin_id = ? AND world_id = ?',
137
+ [pluginId, worldId]
138
+ );
139
+ }
140
+
141
+ return NextResponse.json({ success: true });
142
+ } catch (e) {
143
+ console.error('[PluginStorage] DELETE error:', e);
144
+ return NextResponse.json({ error: '删除存储失败' }, { status: 500 });
145
+ }
146
+ }
@@ -0,0 +1,165 @@
1
+ // app/api/sessions/route.ts - 游戏会话 REST API
2
+
3
+ import { NextRequest, NextResponse } from 'next/server';
4
+ import { getDbPool } from '@/lib/db';
5
+ import { ensureDb } from '@/lib/db-init';
6
+ import { RowDataPacket } from 'mysql2';
7
+
8
+ // 会话数据类型定义
9
+ interface SessionRow extends RowDataPacket {
10
+ id: string;
11
+ world_setting: unknown;
12
+ messages: unknown;
13
+ created_at: string;
14
+ updated_at: string;
15
+ }
16
+
17
+ interface SessionPayload {
18
+ id: string;
19
+ worldSetting: Record<string, unknown>;
20
+ messages: unknown[];
21
+ createdAt?: string;
22
+ updatedAt?: string;
23
+ }
24
+
25
+ /**
26
+ * GET /api/sessions - 获取游戏会话列表
27
+ * GET /api/sessions?id=xxx - 获取单个游戏会话
28
+ */
29
+ export async function GET(request: NextRequest) {
30
+ try {
31
+ const db = getDbPool();
32
+ await ensureDb();
33
+
34
+ const { searchParams } = new URL(request.url);
35
+ const id = searchParams.get('id');
36
+
37
+ if (id) {
38
+ // 获取单个会话
39
+ const [rows] = await db.execute<SessionRow[]>(
40
+ 'SELECT id, world_setting, messages, created_at, updated_at FROM game_sessions WHERE id = ?',
41
+ [id]
42
+ );
43
+ if (Array.isArray(rows) && rows.length > 0) {
44
+ const row = rows[0];
45
+ const session = {
46
+ id: row.id,
47
+ worldSetting: typeof row.world_setting === 'string'
48
+ ? JSON.parse(row.world_setting)
49
+ : row.world_setting,
50
+ messages: typeof row.messages === 'string'
51
+ ? JSON.parse(row.messages)
52
+ : row.messages,
53
+ createdAt: row.created_at,
54
+ updatedAt: row.updated_at,
55
+ };
56
+ return NextResponse.json(session);
57
+ }
58
+ return NextResponse.json({ error: '会话不存在' }, { status: 404 });
59
+ }
60
+
61
+ // 获取所有会话,按更新时间降序
62
+ const [rows] = await db.execute<SessionRow[]>(
63
+ 'SELECT id, world_setting, messages, created_at, updated_at FROM game_sessions ORDER BY updated_at DESC'
64
+ );
65
+ const sessions = rows.map((row) => ({
66
+ id: row.id,
67
+ worldSetting: typeof row.world_setting === 'string'
68
+ ? JSON.parse(row.world_setting)
69
+ : row.world_setting,
70
+ messages: typeof row.messages === 'string'
71
+ ? JSON.parse(row.messages)
72
+ : row.messages,
73
+ createdAt: row.created_at,
74
+ updatedAt: row.updated_at,
75
+ }));
76
+ return NextResponse.json(sessions);
77
+ } catch (error) {
78
+ console.error('Sessions GET error:', error);
79
+ return NextResponse.json({ error: '获取会话失败' }, { status: 500 });
80
+ }
81
+ }
82
+
83
+ /**
84
+ * POST /api/sessions - 创建或更新游戏会话
85
+ * 使用 INSERT ... ON DUPLICATE KEY UPDATE 实现 upsert
86
+ */
87
+ export async function POST(request: NextRequest) {
88
+ try {
89
+ const body: SessionPayload = await request.json();
90
+
91
+ // 参数校验
92
+ if (!body.id || !body.worldSetting || !body.messages) {
93
+ return NextResponse.json(
94
+ { error: '缺少必要参数:id, worldSetting, messages' },
95
+ { status: 400 }
96
+ );
97
+ }
98
+
99
+ const db = getDbPool();
100
+ await ensureDb();
101
+
102
+ const worldSettingJson = JSON.stringify(body.worldSetting);
103
+ const messagesJson = JSON.stringify(body.messages);
104
+
105
+ await db.execute(
106
+ `INSERT INTO game_sessions (id, world_setting, messages)
107
+ VALUES (?, ?, ?)
108
+ ON DUPLICATE KEY UPDATE world_setting = VALUES(world_setting), messages = VALUES(messages)`,
109
+ [body.id, worldSettingJson, messagesJson]
110
+ );
111
+
112
+ return NextResponse.json({ success: true, id: body.id });
113
+ } catch (error) {
114
+ console.error('Sessions POST error:', error);
115
+ return NextResponse.json({ error: '创建或更新会话失败' }, { status: 500 });
116
+ }
117
+ }
118
+
119
+ /**
120
+ * DELETE /api/sessions?id=xxx - 删除单个会话
121
+ * DELETE /api/sessions?action=clearAll - 清除所有会话
122
+ */
123
+ export async function DELETE(request: NextRequest) {
124
+ try {
125
+ const db = getDbPool();
126
+ await ensureDb();
127
+
128
+ const { searchParams } = new URL(request.url);
129
+ const action = searchParams.get('action');
130
+ const id = searchParams.get('id');
131
+
132
+ if (action === 'clearAll') {
133
+ // 清除所有会话,同时清理所有世界级插件绑定
134
+ await db.execute('DELETE FROM game_sessions');
135
+ await db.execute("DELETE FROM extension_bindings WHERE scope = 'world'");
136
+ return NextResponse.json({ success: true, message: '已清除所有会话' });
137
+ }
138
+
139
+ if (id) {
140
+ // 删除单个会话,同时清理该世界的插件绑定
141
+ const [result] = await db.execute(
142
+ 'DELETE FROM game_sessions WHERE id = ?',
143
+ [id]
144
+ );
145
+ const affectedRows = (result as { affectedRows: number }).affectedRows;
146
+ if (affectedRows === 0) {
147
+ return NextResponse.json({ error: '会话不存在' }, { status: 404 });
148
+ }
149
+ // 清理该世界对应的插件绑定记录
150
+ await db.execute(
151
+ "DELETE FROM extension_bindings WHERE scope = 'world' AND world_id = ?",
152
+ [id]
153
+ );
154
+ return NextResponse.json({ success: true, id });
155
+ }
156
+
157
+ return NextResponse.json(
158
+ { error: '请提供 id 或 action 参数' },
159
+ { status: 400 }
160
+ );
161
+ } catch (error) {
162
+ console.error('Sessions DELETE error:', error);
163
+ return NextResponse.json({ error: '删除会话失败' }, { status: 500 });
164
+ }
165
+ }
@@ -0,0 +1,40 @@
1
+ // app/api/settings/route.ts - 应用设置 API
2
+
3
+ import { NextRequest, NextResponse } from 'next/server';
4
+ import { getDbPool } from '@/lib/db';
5
+ import { ensureDb } from '@/lib/db-init';
6
+
7
+ const SETTINGS_KEY = 'app_settings';
8
+
9
+ export async function GET() {
10
+ try {
11
+ const db = getDbPool();
12
+ await ensureDb();
13
+ const [rows] = await db.execute('SELECT `value` FROM app_settings WHERE `key` = ?', [SETTINGS_KEY]);
14
+ if (Array.isArray(rows) && rows.length > 0) {
15
+ const raw = (rows[0] as { value: unknown }).value;
16
+ const parsed = typeof raw === 'string' ? JSON.parse(raw) : raw;
17
+ return NextResponse.json(parsed);
18
+ }
19
+ return NextResponse.json({});
20
+ } catch (error) {
21
+ console.error('Settings GET error:', error);
22
+ return NextResponse.json({ error: '读取设置失败' }, { status: 500 });
23
+ }
24
+ }
25
+
26
+ export async function PUT(request: NextRequest) {
27
+ try {
28
+ const settings = await request.json();
29
+ const db = getDbPool();
30
+ await ensureDb();
31
+ await db.execute(
32
+ 'INSERT INTO app_settings (`key`, `value`) VALUES (?, ?) ON DUPLICATE KEY UPDATE `value` = ?',
33
+ [SETTINGS_KEY, JSON.stringify(settings), JSON.stringify(settings)]
34
+ );
35
+ return NextResponse.json({ success: true });
36
+ } catch (error) {
37
+ console.error('Settings PUT error:', error);
38
+ return NextResponse.json({ error: '保存设置失败' }, { status: 500 });
39
+ }
40
+ }
@@ -0,0 +1,129 @@
1
+ // app/api/suggest-fields/route.ts - AI 智能补全字段 API
2
+
3
+ import { NextRequest, NextResponse } from 'next/server';
4
+
5
+ const SERVER_API_KEY = process.env.AI_API_KEY || '';
6
+ const SERVER_API_BASE = process.env.AI_API_BASE || 'https://api.openai.com/v1';
7
+ const SERVER_MODEL = process.env.AI_MODEL || 'gpt-4o';
8
+
9
+ const SYSTEM_PROMPT = `你是一个 RPG 世界卡片设计专家。根据模板名称和已有字段的键名、类型,为每个字段智能补全默认值、占位文本、选项等内容。
10
+
11
+ 规则:
12
+ 1. 分析模板名称推断世界观主题(如奇幻、科幻、武侠、末日等)
13
+ 2. 根据主题为每个字段生成合理的内容
14
+ 3. text 类型:补全 value(默认值)和 placeholder(占位提示)
15
+ 4. textarea 类型:补全 value 和 placeholder
16
+ 5. select 类型:补全 options(3-6 个选项,每个有 label 和 value)和 allowCustomOption(是否允许自定义)
17
+ 6. checkbox 类型:补全 value(true 或 false)
18
+ 7. slider 类型:补全 value、min、max、step
19
+ 8. 如果字段已有非空值,可以保留或优化,不要清空已有内容
20
+ 9. 用中文回复
21
+
22
+ 请严格按以下 JSON 格式返回,不要包含其他内容。返回一个对象,key 为字段的 key,value 为该字段的补全内容:
23
+ {
24
+ "字段key1": {
25
+ "value": "默认值",
26
+ "placeholder": "占位提示文本",
27
+ "options": [{"label": "选项1", "value": "选项1"}],
28
+ "allowCustomOption": true,
29
+ "min": 0,
30
+ "max": 100,
31
+ "step": 1
32
+ },
33
+ "字段key2": {
34
+ ...
35
+ }
36
+ }`;
37
+
38
+ export async function POST(request: NextRequest) {
39
+ try {
40
+ const body = await request.json();
41
+ const { templateName, existingFields, apiConfig } = body;
42
+
43
+ const apiKey = apiConfig?.apiKey || SERVER_API_KEY;
44
+ const apiBase = apiConfig?.apiBase || SERVER_API_BASE;
45
+ const model = apiConfig?.model || SERVER_MODEL;
46
+
47
+ if (!apiKey) {
48
+ return NextResponse.json(
49
+ { error: 'AI API Key 未配置,请在设置中填写 API Key' },
50
+ { status: 500 }
51
+ );
52
+ }
53
+
54
+ // 构建已有字段摘要(包含当前值)
55
+ const existingSummary = (existingFields || [])
56
+ .map((f: { key: string; label: string; type: string; value: unknown; placeholder?: string }) => {
57
+ const val = Array.isArray(f.value) ? f.value.join('、') : String(f.value || '');
58
+ return `- ${f.label} (${f.key}, ${f.type})${val ? ` 当前值: ${val}` : ' 当前值: 空'}${f.placeholder ? ` 占位: ${f.placeholder}` : ''}`;
59
+ })
60
+ .join('\n');
61
+
62
+ const userMessage = `模板名称:${templateName || '未命名'}
63
+
64
+ 已有字段:
65
+ ${existingSummary || '(暂无字段)'}
66
+
67
+ 请根据模板主题,为以上每个字段补全合理的内容。`;
68
+
69
+ const response = await fetch(`${apiBase}/chat/completions`, {
70
+ method: 'POST',
71
+ headers: {
72
+ 'Content-Type': 'application/json',
73
+ Authorization: `Bearer ${apiKey}`,
74
+ },
75
+ body: JSON.stringify({
76
+ model,
77
+ messages: [
78
+ { role: 'system', content: SYSTEM_PROMPT },
79
+ { role: 'user', content: userMessage },
80
+ ],
81
+ temperature: 0.7,
82
+ max_tokens: 4096,
83
+ }),
84
+ });
85
+
86
+ if (!response.ok) {
87
+ const errorText = await response.text();
88
+ console.error('AI suggest fields error:', errorText);
89
+ return NextResponse.json(
90
+ { error: `AI API 调用失败 (${response.status})` },
91
+ { status: 502 }
92
+ );
93
+ }
94
+
95
+ const data = await response.json();
96
+ const content: string = data.choices?.[0]?.message?.content || '';
97
+
98
+ // 提取 JSON(可能被 markdown 代码块包裹)
99
+ let jsonStr = content.trim();
100
+ const jsonMatch = jsonStr.match(/```(?:json)?\s*([\s\S]*?)```/);
101
+ if (jsonMatch) {
102
+ jsonStr = jsonMatch[1].trim();
103
+ }
104
+
105
+ let updates;
106
+ try {
107
+ updates = JSON.parse(jsonStr);
108
+ } catch {
109
+ // 尝试找到第一个 { 和最后一个 }
110
+ const start = jsonStr.indexOf('{');
111
+ const end = jsonStr.lastIndexOf('}');
112
+ if (start !== -1 && end !== -1) {
113
+ updates = JSON.parse(jsonStr.slice(start, end + 1));
114
+ } else {
115
+ throw new Error('无法解析 AI 返回的数据');
116
+ }
117
+ }
118
+
119
+ if (typeof updates !== 'object' || Array.isArray(updates)) {
120
+ throw new Error('AI 返回的数据格式不正确');
121
+ }
122
+
123
+ return NextResponse.json({ updates });
124
+ } catch (error) {
125
+ console.error('Suggest fields API error:', error);
126
+ const message = error instanceof Error ? error.message : '服务器内部错误';
127
+ return NextResponse.json({ error: message }, { status: 500 });
128
+ }
129
+ }
@@ -0,0 +1,159 @@
1
+ // app/api/templates/route.ts - 世界卡片模板 REST API
2
+
3
+ import { NextRequest, NextResponse } from 'next/server';
4
+ import { getDbPool } from '@/lib/db';
5
+ import { ensureDb } from '@/lib/db-init';
6
+ import { RowDataPacket } from 'mysql2';
7
+
8
+ // 模板数据类型定义
9
+ interface TemplateRow extends RowDataPacket {
10
+ id: string;
11
+ name: string;
12
+ svg_content: string | null;
13
+ fields: unknown;
14
+ plugins: unknown;
15
+ created_at: string;
16
+ updated_at: string;
17
+ }
18
+
19
+ interface TemplatePayload {
20
+ id: string;
21
+ name: string;
22
+ svgContent?: string;
23
+ fields: Record<string, unknown>;
24
+ plugins?: string[];
25
+ createdAt?: string;
26
+ updatedAt?: string;
27
+ }
28
+
29
+ /**
30
+ * GET /api/templates - 获取所有模板(按更新时间降序)
31
+ * GET /api/templates?id=xxx - 获取单个模板
32
+ */
33
+ export async function GET(request: NextRequest) {
34
+ try {
35
+ const db = getDbPool();
36
+ await ensureDb();
37
+
38
+ const { searchParams } = new URL(request.url);
39
+ const id = searchParams.get('id');
40
+
41
+ if (id) {
42
+ // 获取单个模板
43
+ const [rows] = await db.execute<TemplateRow[]>(
44
+ 'SELECT id, name, svg_content, fields, plugins, created_at, updated_at FROM world_templates WHERE id = ?',
45
+ [id]
46
+ );
47
+ if (Array.isArray(rows) && rows.length > 0) {
48
+ const row = rows[0];
49
+ const template = {
50
+ id: row.id,
51
+ name: row.name,
52
+ svgContent: row.svg_content,
53
+ fields: typeof row.fields === 'string'
54
+ ? JSON.parse(row.fields)
55
+ : row.fields,
56
+ plugins: typeof row.plugins === 'string'
57
+ ? JSON.parse(row.plugins)
58
+ : (row.plugins || []),
59
+ createdAt: row.created_at,
60
+ updatedAt: row.updated_at,
61
+ };
62
+ return NextResponse.json(template);
63
+ }
64
+ return NextResponse.json({ error: '模板不存在' }, { status: 404 });
65
+ }
66
+
67
+ // 获取所有模板,按更新时间降序
68
+ const [rows] = await db.execute<TemplateRow[]>(
69
+ 'SELECT id, name, svg_content, fields, plugins, created_at, updated_at FROM world_templates ORDER BY updated_at DESC'
70
+ );
71
+ const templates = rows.map((row) => ({
72
+ id: row.id,
73
+ name: row.name,
74
+ svgContent: row.svg_content,
75
+ fields: typeof row.fields === 'string'
76
+ ? JSON.parse(row.fields)
77
+ : row.fields,
78
+ plugins: typeof row.plugins === 'string'
79
+ ? JSON.parse(row.plugins)
80
+ : (row.plugins || []),
81
+ createdAt: row.created_at,
82
+ updatedAt: row.updated_at,
83
+ }));
84
+ return NextResponse.json(templates);
85
+ } catch (error) {
86
+ console.error('Templates GET error:', error);
87
+ return NextResponse.json({ error: '获取模板失败' }, { status: 500 });
88
+ }
89
+ }
90
+
91
+ /**
92
+ * POST /api/templates - 创建或更新模板
93
+ * 使用 INSERT ... ON DUPLICATE KEY UPDATE 实现 upsert
94
+ */
95
+ export async function POST(request: NextRequest) {
96
+ try {
97
+ const body: TemplatePayload = await request.json();
98
+
99
+ // 参数校验
100
+ if (!body.id || !body.name || !body.fields) {
101
+ return NextResponse.json(
102
+ { error: '缺少必要参数:id, name, fields' },
103
+ { status: 400 }
104
+ );
105
+ }
106
+
107
+ const db = getDbPool();
108
+ await ensureDb();
109
+
110
+ const fieldsJson = JSON.stringify(body.fields);
111
+ const svgContent = body.svgContent ?? null;
112
+ const pluginsJson = JSON.stringify(body.plugins || []);
113
+
114
+ await db.execute(
115
+ `INSERT INTO world_templates (id, name, svg_content, fields, plugins)
116
+ VALUES (?, ?, ?, ?, ?)
117
+ ON DUPLICATE KEY UPDATE name = VALUES(name), svg_content = VALUES(svg_content), fields = VALUES(fields), plugins = VALUES(plugins)`,
118
+ [body.id, body.name, svgContent, fieldsJson, pluginsJson]
119
+ );
120
+
121
+ return NextResponse.json({ success: true, id: body.id });
122
+ } catch (error) {
123
+ console.error('Templates POST error:', error);
124
+ return NextResponse.json({ error: '创建或更新模板失败' }, { status: 500 });
125
+ }
126
+ }
127
+
128
+ /**
129
+ * DELETE /api/templates?id=xxx - 删除单个模板
130
+ */
131
+ export async function DELETE(request: NextRequest) {
132
+ try {
133
+ const db = getDbPool();
134
+ await ensureDb();
135
+
136
+ const { searchParams } = new URL(request.url);
137
+ const id = searchParams.get('id');
138
+
139
+ if (!id) {
140
+ return NextResponse.json(
141
+ { error: '请提供 id 参数' },
142
+ { status: 400 }
143
+ );
144
+ }
145
+
146
+ const [result] = await db.execute(
147
+ 'DELETE FROM world_templates WHERE id = ?',
148
+ [id]
149
+ );
150
+ const affectedRows = (result as { affectedRows: number }).affectedRows;
151
+ if (affectedRows === 0) {
152
+ return NextResponse.json({ error: '模板不存在' }, { status: 404 });
153
+ }
154
+ return NextResponse.json({ success: true, id });
155
+ } catch (error) {
156
+ console.error('Templates DELETE error:', error);
157
+ return NextResponse.json({ error: '删除模板失败' }, { status: 500 });
158
+ }
159
+ }
@@ -0,0 +1,63 @@
1
+ // app/api/test-api/route.ts - API Key 测试连接
2
+
3
+ import { NextRequest, NextResponse } from 'next/server';
4
+
5
+ export async function POST(request: NextRequest) {
6
+ try {
7
+ const { apiKey, apiBase, model } = await request.json();
8
+
9
+ if (!apiKey) {
10
+ return NextResponse.json({ error: '请输入 API Key' }, { status: 400 });
11
+ }
12
+
13
+ const base = (apiBase || 'https://api.openai.com/v1').replace(/\/+$/, '');
14
+ const modelName = model || 'gpt-4o';
15
+
16
+ const controller = new AbortController();
17
+ const timeout = setTimeout(() => controller.abort(), 10000);
18
+
19
+ try {
20
+ const response = await fetch(`${base}/chat/completions`, {
21
+ method: 'POST',
22
+ headers: {
23
+ 'Content-Type': 'application/json',
24
+ Authorization: `Bearer ${apiKey}`,
25
+ },
26
+ body: JSON.stringify({
27
+ model: modelName,
28
+ messages: [{ role: 'user', content: 'Hi' }],
29
+ max_tokens: 5,
30
+ }),
31
+ signal: controller.signal,
32
+ });
33
+
34
+ clearTimeout(timeout);
35
+
36
+ if (!response.ok) {
37
+ const errorText = await response.text();
38
+ return NextResponse.json({
39
+ success: false,
40
+ error: `HTTP ${response.status}`,
41
+ detail: errorText.slice(0, 200),
42
+ });
43
+ }
44
+
45
+ const data = await response.json();
46
+ const modelUsed = data.model || modelName;
47
+
48
+ return NextResponse.json({
49
+ success: true,
50
+ model: modelUsed,
51
+ message: `连接成功,模型: ${modelUsed}`,
52
+ });
53
+ } catch (fetchError: unknown) {
54
+ clearTimeout(timeout);
55
+ if (fetchError instanceof Error && fetchError.name === 'AbortError') {
56
+ return NextResponse.json({ success: false, error: '连接超时(10秒)' });
57
+ }
58
+ return NextResponse.json({ success: false, error: '网络连接失败,请检查 API 地址' });
59
+ }
60
+ } catch {
61
+ return NextResponse.json({ error: '请求格式错误' }, { status: 400 });
62
+ }
63
+ }