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.
- package/.env.example +21 -0
- package/README.md +36 -0
- package/app/api/chat/route.ts +84 -0
- package/app/api/generate-svg/route.ts +171 -0
- package/app/api/generate-theme/route.ts +137 -0
- package/app/api/plugins/bindings/route.ts +173 -0
- package/app/api/plugins/export/route.ts +122 -0
- package/app/api/plugins/export-xye/route.ts +156 -0
- package/app/api/plugins/files/route.ts +146 -0
- package/app/api/plugins/files-list/route.ts +168 -0
- package/app/api/plugins/files-upload/route.ts +101 -0
- package/app/api/plugins/files-write/route.ts +272 -0
- package/app/api/plugins/import/route.ts +140 -0
- package/app/api/plugins/import-package/route.ts +231 -0
- package/app/api/plugins/resources/route.ts +109 -0
- package/app/api/plugins/route.ts +308 -0
- package/app/api/plugins/scan/route.ts +280 -0
- package/app/api/plugins/storage/route.ts +146 -0
- package/app/api/sessions/route.ts +165 -0
- package/app/api/settings/route.ts +40 -0
- package/app/api/suggest-fields/route.ts +129 -0
- package/app/api/templates/route.ts +159 -0
- package/app/api/test-api/route.ts +63 -0
- package/app/editor/page.tsx +1466 -0
- package/app/extensions/create/page.tsx +1422 -0
- package/app/extensions/edit/[id]/page.tsx +2342 -0
- package/app/extensions/page.tsx +1572 -0
- package/app/extensions/tutorial/page.tsx +4258 -0
- package/app/favicon.ico +0 -0
- package/app/fonts/GeistMonoVF.woff +0 -0
- package/app/fonts/GeistVF.woff +0 -0
- package/app/game/[id]/page.tsx +996 -0
- package/app/globals.css +3 -0
- package/app/layout.tsx +26 -0
- package/app/loading.tsx +26 -0
- package/app/page.tsx +345 -0
- package/app/settings/page.tsx +1490 -0
- package/bin/cli.js +262 -0
- package/components/ChatInput.tsx +106 -0
- package/components/ChatWindow.tsx +52 -0
- package/components/FullPageLoader.tsx +107 -0
- package/components/LoadingDots.tsx +20 -0
- package/components/MathCurveLoader.tsx +173 -0
- package/components/MessageBubble.tsx +147 -0
- package/components/WorldCardPreview.tsx +98 -0
- package/components/WorldCardUploader.tsx +58 -0
- package/components/ui/ConfirmDialog.tsx +135 -0
- package/components/ui/PageHeader.tsx +99 -0
- package/components/ui/PermissionConflictDialog.tsx +206 -0
- package/components/ui/PluginConfigForm.tsx +192 -0
- package/components/ui/PluginFloatingLayer.tsx +52 -0
- package/components/ui/PluginIcon.tsx +53 -0
- package/components/ui/PluginModalRenderer.tsx +185 -0
- package/components/ui/PluginProvider.tsx +1038 -0
- package/components/ui/PluginSlotRenderer.tsx +76 -0
- package/components/ui/ThemeCustomizer.tsx +174 -0
- package/components/ui/ThemeProvider.tsx +125 -0
- package/components/ui/ThemeSwitcher.tsx +140 -0
- package/components/ui/ToastProvider.tsx +141 -0
- package/lib/builtin-plugins.ts +11 -0
- package/lib/db-init.ts +35 -0
- package/lib/db.ts +244 -0
- package/lib/manifest-parser.ts +185 -0
- package/lib/parseWorldCard.ts +110 -0
- package/lib/plugin-dom-sandbox.ts +327 -0
- package/lib/plugin-events.ts +88 -0
- package/lib/plugin-files.ts +186 -0
- package/lib/plugin-html-sanitizer.ts +79 -0
- package/lib/plugin-resource-tracker.ts +175 -0
- package/lib/plugin-runtime.ts +2287 -0
- package/lib/plugin-security.ts +151 -0
- package/lib/plugin-types.ts +416 -0
- package/lib/prompt-builder.ts +55 -0
- package/lib/router-history.ts +119 -0
- package/lib/storage.ts +381 -0
- package/lib/themes.ts +129 -0
- package/lib/types.ts +117 -0
- package/lib/version.ts +55 -0
- package/next.config.mjs +43 -0
- package/package.json +56 -0
- package/plugins/xinyu.bag-system.xye +0 -0
- package/plugins/xinyu.cache-optimizer.xye +0 -0
- package/plugins/xinyu.dice-arbiter.xye +0 -0
- package/plugins/xinyu.game-auto-start-choices.xye +0 -0
- package/plugins/xinyu.markdown-render.xye +0 -0
- package/plugins/xinyu.slot-ui-beautify.xye +0 -0
- package/plugins/xinyu.world-info.xye +0 -0
- package/postcss.config.mjs +8 -0
- package/public/templates/atlantis.svg +63 -0
- package/public/templates/cyber-city.svg +68 -0
- package/public/templates/jianghu.svg +69 -0
- package/public/templates//351/255/224/346/263/225/344/270/226/347/225/214.svg +137 -0
- package/styles/themes.css +111 -0
- package/tailwind.config.ts +18 -0
- package/tsconfig.json +26 -0
- package/version.json +6 -0
package/.env.example
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# AI 大模型 API 密钥
|
|
2
|
+
AI_API_KEY=sk-your-api-key-here
|
|
3
|
+
|
|
4
|
+
# AI 模型名称(可选,默认 gpt-4o)
|
|
5
|
+
AI_MODEL=gpt-4o
|
|
6
|
+
|
|
7
|
+
# AI API 地址(可选,默认 OpenAI,可替换为兼容接口)
|
|
8
|
+
AI_API_BASE=https://api.openai.com/v1
|
|
9
|
+
|
|
10
|
+
# GitHub Personal Access Token(需具备 repo 权限)
|
|
11
|
+
GITHUB_TOKEN=ghp_your-token-here
|
|
12
|
+
|
|
13
|
+
# GitHub 用户名
|
|
14
|
+
GITHUB_USERNAME=your-username
|
|
15
|
+
|
|
16
|
+
# 数据库配置
|
|
17
|
+
DB_HOST=localhost
|
|
18
|
+
DB_PORT=3306
|
|
19
|
+
DB_USER=root
|
|
20
|
+
DB_PASSWORD=你的密码
|
|
21
|
+
DB_NAME=xinyu
|
package/README.md
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app).
|
|
2
|
+
|
|
3
|
+
## Getting Started
|
|
4
|
+
|
|
5
|
+
First, run the development server:
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm run dev
|
|
9
|
+
# or
|
|
10
|
+
yarn dev
|
|
11
|
+
# or
|
|
12
|
+
pnpm dev
|
|
13
|
+
# or
|
|
14
|
+
bun dev
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
|
|
18
|
+
|
|
19
|
+
You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
|
|
20
|
+
|
|
21
|
+
This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel.
|
|
22
|
+
|
|
23
|
+
## Learn More
|
|
24
|
+
|
|
25
|
+
To learn more about Next.js, take a look at the following resources:
|
|
26
|
+
|
|
27
|
+
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
|
|
28
|
+
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
|
|
29
|
+
|
|
30
|
+
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome!
|
|
31
|
+
|
|
32
|
+
## Deploy on Vercel
|
|
33
|
+
|
|
34
|
+
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
|
|
35
|
+
|
|
36
|
+
Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details.
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
// app/api/chat/route.ts - AI 对话 API 接口
|
|
2
|
+
|
|
3
|
+
import { NextRequest, NextResponse } from 'next/server';
|
|
4
|
+
import { buildSystemPrompt } from '@/lib/prompt-builder';
|
|
5
|
+
|
|
6
|
+
const SERVER_API_KEY = process.env.AI_API_KEY || '';
|
|
7
|
+
const SERVER_API_BASE = process.env.AI_API_BASE || 'https://api.openai.com/v1';
|
|
8
|
+
const SERVER_MODEL = process.env.AI_MODEL || 'gpt-4o';
|
|
9
|
+
|
|
10
|
+
export async function POST(request: NextRequest) {
|
|
11
|
+
try {
|
|
12
|
+
const body = await request.json();
|
|
13
|
+
const { worldSetting, messages, apiConfig, prebuiltMessages } = body;
|
|
14
|
+
|
|
15
|
+
if (!worldSetting || !messages || messages.length === 0) {
|
|
16
|
+
return NextResponse.json(
|
|
17
|
+
{ error: '请求参数无效,需要 worldSetting 和 messages' },
|
|
18
|
+
{ status: 400 }
|
|
19
|
+
);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const apiKey = apiConfig?.apiKey || SERVER_API_KEY;
|
|
23
|
+
const apiBase = apiConfig?.apiBase || SERVER_API_BASE;
|
|
24
|
+
const model = apiConfig?.model || SERVER_MODEL;
|
|
25
|
+
const temperature = apiConfig?.temperature ?? 0.8;
|
|
26
|
+
const maxTokens = apiConfig?.maxTokens ?? 1024;
|
|
27
|
+
|
|
28
|
+
if (!apiKey) {
|
|
29
|
+
return NextResponse.json(
|
|
30
|
+
{ error: 'AI API Key 未配置,请在设置中填写 API Key,或在 .env.local 中设置 AI_API_KEY' },
|
|
31
|
+
{ status: 500 }
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// 如果客户端已构建完整消息数组(含 system prompt),直接使用
|
|
36
|
+
// 否则走传统流程:服务端构建 system prompt + 用户消息
|
|
37
|
+
const apiMessages = prebuiltMessages?.length
|
|
38
|
+
? prebuiltMessages.map((m: { role: string; content: string }) => ({
|
|
39
|
+
role: m.role as 'user' | 'assistant' | 'system',
|
|
40
|
+
content: m.content,
|
|
41
|
+
}))
|
|
42
|
+
: [
|
|
43
|
+
{ role: 'system' as const, content: buildSystemPrompt(worldSetting, apiConfig?.systemPromptExtra) },
|
|
44
|
+
...messages.map((m: { role: string; content: string }) => ({
|
|
45
|
+
role: m.role as 'user' | 'assistant',
|
|
46
|
+
content: m.content,
|
|
47
|
+
})),
|
|
48
|
+
];
|
|
49
|
+
|
|
50
|
+
const response = await fetch(`${apiBase}/chat/completions`, {
|
|
51
|
+
method: 'POST',
|
|
52
|
+
headers: {
|
|
53
|
+
'Content-Type': 'application/json',
|
|
54
|
+
Authorization: `Bearer ${apiKey}`,
|
|
55
|
+
},
|
|
56
|
+
body: JSON.stringify({
|
|
57
|
+
model,
|
|
58
|
+
messages: apiMessages,
|
|
59
|
+
temperature,
|
|
60
|
+
max_tokens: maxTokens,
|
|
61
|
+
}),
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
if (!response.ok) {
|
|
65
|
+
const errorText = await response.text();
|
|
66
|
+
console.error('AI API error:', errorText);
|
|
67
|
+
return NextResponse.json(
|
|
68
|
+
{ error: `AI API 调用失败 (${response.status}): ${errorText}` },
|
|
69
|
+
{ status: 502 }
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const data = await response.json();
|
|
74
|
+
const content: string = data.choices?.[0]?.message?.content || '(无回复)';
|
|
75
|
+
|
|
76
|
+
return NextResponse.json({ content });
|
|
77
|
+
} catch (error) {
|
|
78
|
+
console.error('Chat API error:', error);
|
|
79
|
+
return NextResponse.json(
|
|
80
|
+
{ error: '服务器内部错误' },
|
|
81
|
+
{ status: 500 }
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
// app/api/generate-svg/route.ts - AI 生成 SVG 世界卡片
|
|
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
|
+
interface GenerateSvgRequest {
|
|
10
|
+
worldData: Record<string, unknown>;
|
|
11
|
+
customRules?: string;
|
|
12
|
+
apiConfig?: {
|
|
13
|
+
apiKey: string;
|
|
14
|
+
apiBase: string;
|
|
15
|
+
model: string;
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const SVG_SYSTEM_PROMPT = `你是一位专业的 SVG 视觉设计师。你的唯一任务是生成纯视觉的 SVG 世界卡片代码。
|
|
20
|
+
|
|
21
|
+
## 绝对规则(必须遵守)
|
|
22
|
+
1. 你必须输出一个完整的、可渲染的 SVG 代码
|
|
23
|
+
2. SVG 必须以 <svg 开头,以 </svg> 结尾
|
|
24
|
+
3. SVG 必须包含实际的视觉元素(矩形、文字、渐变、装饰等)
|
|
25
|
+
4. 不要输出任何解释文字,只输出 SVG 代码
|
|
26
|
+
5. 不要包含任何 <script> 标签,数据会由系统自动注入
|
|
27
|
+
|
|
28
|
+
## SVG 世界卡片规范
|
|
29
|
+
|
|
30
|
+
### 尺寸
|
|
31
|
+
viewBox="0 0 400 560"
|
|
32
|
+
|
|
33
|
+
### 结构
|
|
34
|
+
1. 背景层:渐变背景 + 装饰图案
|
|
35
|
+
2. 标题区:游戏标题(大字)
|
|
36
|
+
3. 信息区:展示世界设定的各项信息(时代、地点、基调、冲突等)
|
|
37
|
+
4. 装饰层:边框、分隔线、角落装饰
|
|
38
|
+
|
|
39
|
+
### 视觉设计
|
|
40
|
+
- 根据世界设定的时代、氛围、基调来设计配色和装饰风格
|
|
41
|
+
- 使用渐变(linearGradient/radialGradient)增加质感
|
|
42
|
+
- 文字清晰可读,层次分明
|
|
43
|
+
- 使用中文显示世界信息
|
|
44
|
+
- 不要使用外部资源(字体、图片等)
|
|
45
|
+
- 确保所有标签正确闭合
|
|
46
|
+
|
|
47
|
+
### 风格参考
|
|
48
|
+
- 奇幻/史诗:深色背景 + 金色装饰
|
|
49
|
+
- 科幻/赛博朋克:纯黑背景 + 荧光色
|
|
50
|
+
- 古典/历史:暖色纸张质感 + 棕色装饰
|
|
51
|
+
- 轻松/日常:明亮配色 + 圆润元素
|
|
52
|
+
- 恐怖/悬疑:暗红/深紫 + 锐利线条`;
|
|
53
|
+
|
|
54
|
+
export async function POST(request: NextRequest) {
|
|
55
|
+
try {
|
|
56
|
+
const body: GenerateSvgRequest = await request.json();
|
|
57
|
+
const { worldData, customRules, apiConfig } = body;
|
|
58
|
+
|
|
59
|
+
if (!worldData?.title) {
|
|
60
|
+
return NextResponse.json({ error: '缺少世界设定信息' }, { status: 400 });
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const apiKey = apiConfig?.apiKey || SERVER_API_KEY;
|
|
64
|
+
const apiBase = apiConfig?.apiBase || SERVER_API_BASE;
|
|
65
|
+
const model = apiConfig?.model || SERVER_MODEL;
|
|
66
|
+
|
|
67
|
+
if (!apiKey) {
|
|
68
|
+
return NextResponse.json(
|
|
69
|
+
{ error: 'AI API Key 未配置,请在设置中填写' },
|
|
70
|
+
{ status: 500 }
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// 构建世界信息文本(包含 label)
|
|
75
|
+
const worldInfoLines = Object.entries(worldData)
|
|
76
|
+
.filter(([k]) => !k.startsWith('_'))
|
|
77
|
+
.filter(([, v]) => v !== undefined && v !== null && v !== '')
|
|
78
|
+
.map(([k, v]) => {
|
|
79
|
+
const label = (worldData._labels as Record<string, string>)?.[k] || k;
|
|
80
|
+
return `- ${label}: ${Array.isArray(v) ? v.join('、') : v}`;
|
|
81
|
+
});
|
|
82
|
+
const worldInfo = `## 世界设定\n${worldInfoLines.join('\n')}`;
|
|
83
|
+
|
|
84
|
+
const userPrompt = `请为以下游戏世界生成一张 SVG 世界卡片。
|
|
85
|
+
|
|
86
|
+
${worldInfo}
|
|
87
|
+
|
|
88
|
+
${customRules ? `## 自定义生成规则\n${customRules}\n` : ''}
|
|
89
|
+
|
|
90
|
+
请直接输出完整的 SVG 代码。要求:
|
|
91
|
+
1. 必须以 <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 400 560"> 开头
|
|
92
|
+
2. 必须包含渐变背景、标题、世界信息文字、装饰边框等视觉元素
|
|
93
|
+
3. 不要包含 <script> 标签
|
|
94
|
+
4. 不要输出任何 SVG 之外的文字`;
|
|
95
|
+
|
|
96
|
+
const response = await fetch(`${apiBase}/chat/completions`, {
|
|
97
|
+
method: 'POST',
|
|
98
|
+
headers: {
|
|
99
|
+
'Content-Type': 'application/json',
|
|
100
|
+
Authorization: `Bearer ${apiKey}`,
|
|
101
|
+
},
|
|
102
|
+
body: JSON.stringify({
|
|
103
|
+
model,
|
|
104
|
+
messages: [
|
|
105
|
+
{ role: 'system', content: SVG_SYSTEM_PROMPT },
|
|
106
|
+
{ role: 'user', content: userPrompt },
|
|
107
|
+
],
|
|
108
|
+
temperature: 0.9,
|
|
109
|
+
max_tokens: 16384,
|
|
110
|
+
}),
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
if (!response.ok) {
|
|
114
|
+
const errorText = await response.text();
|
|
115
|
+
console.error('SVG generation API error:', errorText);
|
|
116
|
+
return NextResponse.json(
|
|
117
|
+
{ error: `AI 调用失败 (${response.status})` },
|
|
118
|
+
{ status: 502 }
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const data = await response.json();
|
|
123
|
+
let svgContent: string = data.choices?.[0]?.message?.content || '';
|
|
124
|
+
|
|
125
|
+
// 清理 markdown 代码块包裹
|
|
126
|
+
svgContent = svgContent.trim();
|
|
127
|
+
if (svgContent.startsWith('```svg')) {
|
|
128
|
+
svgContent = svgContent.slice(6);
|
|
129
|
+
} else if (svgContent.startsWith('```')) {
|
|
130
|
+
svgContent = svgContent.slice(3);
|
|
131
|
+
}
|
|
132
|
+
if (svgContent.endsWith('```')) {
|
|
133
|
+
svgContent = svgContent.slice(0, -3);
|
|
134
|
+
}
|
|
135
|
+
svgContent = svgContent.trim();
|
|
136
|
+
|
|
137
|
+
// 提取 SVG 内容(可能被其他文字包裹)
|
|
138
|
+
const svgStart = svgContent.indexOf('<svg');
|
|
139
|
+
const svgEnd = svgContent.lastIndexOf('</svg>');
|
|
140
|
+
if (svgStart !== -1 && svgEnd !== -1) {
|
|
141
|
+
svgContent = svgContent.slice(svgStart, svgEnd + 6);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// 确保 SVG 有 xmlns
|
|
145
|
+
if (!svgContent.includes('xmlns')) {
|
|
146
|
+
svgContent = svgContent.replace('<svg', '<svg xmlns="http://www.w3.org/2000/svg"');
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// 验证 SVG 包含实际视觉元素
|
|
150
|
+
const hasVisualElements = /<(rect|circle|ellipse|line|polyline|polygon|path|text|image|g|linearGradient|radialGradient|defs)\b/i.test(svgContent);
|
|
151
|
+
if (!hasVisualElements) {
|
|
152
|
+
return NextResponse.json(
|
|
153
|
+
{ error: 'AI 生成的 SVG 不包含视觉元素,请重试' },
|
|
154
|
+
{ status: 500 }
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// 确保 SVG 正确闭合
|
|
159
|
+
if (!svgContent.endsWith('</svg>')) {
|
|
160
|
+
svgContent += '\n</svg>';
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// 移除 AI 可能误生成的 script 标签
|
|
164
|
+
svgContent = svgContent.replace(/<script[^>]*>[\s\S]*?<\/script>/gi, '').trim();
|
|
165
|
+
|
|
166
|
+
return NextResponse.json({ svg: svgContent });
|
|
167
|
+
} catch (error) {
|
|
168
|
+
console.error('Generate SVG error:', error);
|
|
169
|
+
return NextResponse.json({ error: '服务器内部错误' }, { status: 500 });
|
|
170
|
+
}
|
|
171
|
+
}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
// app/api/generate-theme/route.ts - AI 生成配色方案
|
|
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
|
+
interface GenerateThemeRequest {
|
|
10
|
+
prompt: string;
|
|
11
|
+
format: 'svg' | 'json';
|
|
12
|
+
apiConfig?: {
|
|
13
|
+
apiKey: string;
|
|
14
|
+
apiBase: string;
|
|
15
|
+
model: string;
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const THEME_SYSTEM_PROMPT = `你是一位专业的 UI 配色方案设计师。你的任务是生成视觉精美的配色方案。
|
|
20
|
+
|
|
21
|
+
## 输出格式
|
|
22
|
+
|
|
23
|
+
### SVG 格式
|
|
24
|
+
生成一个 400x560 的 SVG 色卡展示图,要求:
|
|
25
|
+
1. 包含渐变背景、色块展示、文字标注
|
|
26
|
+
2. 展示主背景色、次背景色、文字色、强调色、边框色等
|
|
27
|
+
3. 每个颜色用色块+十六进制值标注
|
|
28
|
+
4. 风格要美观,体现配色方案的氛围
|
|
29
|
+
5. 只输出 SVG 代码,不要包含 <script> 标签
|
|
30
|
+
6. 必须以 <svg 开头,以 </svg> 结尾
|
|
31
|
+
|
|
32
|
+
### JSON 格式
|
|
33
|
+
输出一个 JSON 对象,包含以下结构:
|
|
34
|
+
{
|
|
35
|
+
"name": "主题名称",
|
|
36
|
+
"description": "主题描述",
|
|
37
|
+
"isDark": true/false,
|
|
38
|
+
"variables": {
|
|
39
|
+
"--color-bg-primary": "#背景色",
|
|
40
|
+
"--color-bg-secondary": "#次背景色",
|
|
41
|
+
"--color-bg-tertiary": "#三级背景色",
|
|
42
|
+
"--color-text-primary": "#主文字色",
|
|
43
|
+
"--color-text-secondary": "#次文字色",
|
|
44
|
+
"--color-text-muted": "#弱化文字色",
|
|
45
|
+
"--color-accent": "#强调色",
|
|
46
|
+
"--color-accent-hover": "#强调色悬停",
|
|
47
|
+
"--color-border": "#边框色",
|
|
48
|
+
"--color-shadow": "rgba阴影色",
|
|
49
|
+
"--color-user-bubble": "#用户气泡色",
|
|
50
|
+
"--color-ai-bubble": "#AI气泡色",
|
|
51
|
+
"--font-body": "字体",
|
|
52
|
+
"--font-heading": "标题字体",
|
|
53
|
+
"--border-radius": "圆角大小"
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
## 配色原则
|
|
58
|
+
- 确保文字在背景上有足够对比度(WCAG AA 标准)
|
|
59
|
+
- 强调色要醒目但不刺眼
|
|
60
|
+
- 背景色层次分明
|
|
61
|
+
- 整体配色和谐统一`;
|
|
62
|
+
|
|
63
|
+
export async function POST(request: NextRequest) {
|
|
64
|
+
try {
|
|
65
|
+
const body: GenerateThemeRequest = await request.json();
|
|
66
|
+
const { prompt, format, apiConfig } = body;
|
|
67
|
+
|
|
68
|
+
if (!prompt) {
|
|
69
|
+
return NextResponse.json({ error: '请描述你想要的配色风格' }, { status: 400 });
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const apiKey = apiConfig?.apiKey || SERVER_API_KEY;
|
|
73
|
+
const apiBase = apiConfig?.apiBase || SERVER_API_BASE;
|
|
74
|
+
const model = apiConfig?.model || SERVER_MODEL;
|
|
75
|
+
|
|
76
|
+
if (!apiKey) {
|
|
77
|
+
return NextResponse.json({ error: 'AI API Key 未配置' }, { status: 500 });
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const formatInstruction = format === 'svg'
|
|
81
|
+
? '请以 SVG 格式输出一个精美的色卡展示图。只输出 SVG 代码。'
|
|
82
|
+
: '请以 JSON 格式输出配色方案。只输出 JSON 代码。';
|
|
83
|
+
|
|
84
|
+
const response = await fetch(`${apiBase}/chat/completions`, {
|
|
85
|
+
method: 'POST',
|
|
86
|
+
headers: {
|
|
87
|
+
'Content-Type': 'application/json',
|
|
88
|
+
Authorization: `Bearer ${apiKey}`,
|
|
89
|
+
},
|
|
90
|
+
body: JSON.stringify({
|
|
91
|
+
model,
|
|
92
|
+
messages: [
|
|
93
|
+
{ role: 'system', content: THEME_SYSTEM_PROMPT },
|
|
94
|
+
{ role: 'user', content: `请设计一个配色方案:${prompt}\n\n${formatInstruction}` },
|
|
95
|
+
],
|
|
96
|
+
temperature: 0.9,
|
|
97
|
+
max_tokens: 8192,
|
|
98
|
+
}),
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
if (!response.ok) {
|
|
102
|
+
return NextResponse.json({ error: `AI 调用失败 (${response.status})` }, { status: 502 });
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const data = await response.json();
|
|
106
|
+
let content: string = data.choices?.[0]?.message?.content || '';
|
|
107
|
+
|
|
108
|
+
content = content.trim();
|
|
109
|
+
// 清理 markdown 代码块
|
|
110
|
+
if (content.startsWith('```svg')) content = content.slice(6);
|
|
111
|
+
else if (content.startsWith('```json')) content = content.slice(7);
|
|
112
|
+
else if (content.startsWith('```')) content = content.slice(3);
|
|
113
|
+
if (content.endsWith('```')) content = content.slice(0, -3);
|
|
114
|
+
content = content.trim();
|
|
115
|
+
|
|
116
|
+
if (format === 'svg') {
|
|
117
|
+
// 提取 SVG
|
|
118
|
+
const start = content.indexOf('<svg');
|
|
119
|
+
const end = content.lastIndexOf('</svg>');
|
|
120
|
+
if (start !== -1 && end !== -1) content = content.slice(start, end + 6);
|
|
121
|
+
if (!content.includes('xmlns')) content = content.replace('<svg', '<svg xmlns="http://www.w3.org/2000/svg"');
|
|
122
|
+
// 移除 script 标签
|
|
123
|
+
content = content.replace(/<script[^>]*>[\s\S]*?<\/script>/gi, '').trim();
|
|
124
|
+
return NextResponse.json({ type: 'svg', content });
|
|
125
|
+
} else {
|
|
126
|
+
// 提取 JSON
|
|
127
|
+
const start = content.indexOf('{');
|
|
128
|
+
const end = content.lastIndexOf('}');
|
|
129
|
+
if (start !== -1 && end !== -1) content = content.slice(start, end + 1);
|
|
130
|
+
const parsed = JSON.parse(content);
|
|
131
|
+
return NextResponse.json({ type: 'json', content: parsed });
|
|
132
|
+
}
|
|
133
|
+
} catch (error) {
|
|
134
|
+
console.error('Generate theme error:', error);
|
|
135
|
+
return NextResponse.json({ error: '生成失败' }, { status: 500 });
|
|
136
|
+
}
|
|
137
|
+
}
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
// app/api/plugins/bindings/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
|
+
/** 解析 JSON 字段 */
|
|
8
|
+
function parseJsonField<T>(value: unknown): T {
|
|
9
|
+
if (typeof value === 'string') {
|
|
10
|
+
try { return JSON.parse(value) as T; } catch { return value as unknown as T; }
|
|
11
|
+
}
|
|
12
|
+
return value as T;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** 行数据转换为绑定对象 */
|
|
16
|
+
function rowToBinding(row: Record<string, unknown>) {
|
|
17
|
+
return {
|
|
18
|
+
id: row.id,
|
|
19
|
+
extensionId: row.extension_id,
|
|
20
|
+
scope: row.scope,
|
|
21
|
+
worldId: row.world_id,
|
|
22
|
+
enabled: Boolean(row.enabled),
|
|
23
|
+
config: parseJsonField<Record<string, unknown>>(row.config),
|
|
24
|
+
sortOrder: row.sort_order,
|
|
25
|
+
createdAt: row.created_at,
|
|
26
|
+
updatedAt: row.updated_at,
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// GET: 获取绑定列表
|
|
31
|
+
export async function GET(request: NextRequest) {
|
|
32
|
+
try {
|
|
33
|
+
await ensureDb();
|
|
34
|
+
const db = getDbPool();
|
|
35
|
+
const { searchParams } = new URL(request.url);
|
|
36
|
+
const id = searchParams.get('id');
|
|
37
|
+
const scope = searchParams.get('scope');
|
|
38
|
+
const worldId = searchParams.get('worldId');
|
|
39
|
+
|
|
40
|
+
if (id) {
|
|
41
|
+
const [rows] = await db.execute(
|
|
42
|
+
'SELECT * FROM extension_bindings WHERE id = ?',
|
|
43
|
+
[id]
|
|
44
|
+
) as [Record<string, unknown>[], unknown];
|
|
45
|
+
if (rows.length === 0) {
|
|
46
|
+
return NextResponse.json({ error: '绑定不存在' }, { status: 404 });
|
|
47
|
+
}
|
|
48
|
+
return NextResponse.json(rowToBinding(rows[0]));
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
let query = 'SELECT * FROM extension_bindings WHERE 1=1';
|
|
52
|
+
const params: (string | null)[] = [];
|
|
53
|
+
|
|
54
|
+
if (scope) {
|
|
55
|
+
query += ' AND scope = ?';
|
|
56
|
+
params.push(scope);
|
|
57
|
+
}
|
|
58
|
+
if (worldId) {
|
|
59
|
+
query += ' AND world_id = ?';
|
|
60
|
+
params.push(worldId);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
query += ' ORDER BY sort_order ASC, id ASC';
|
|
64
|
+
const [rows] = await db.execute(query, params) as [Record<string, unknown>[], unknown];
|
|
65
|
+
return NextResponse.json(rows.map(rowToBinding));
|
|
66
|
+
} catch (e) {
|
|
67
|
+
console.error('获取插件绑定失败:', e);
|
|
68
|
+
return NextResponse.json({ error: '获取插件绑定失败' }, { status: 500 });
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// POST: 创建或更新绑定(upsert)
|
|
73
|
+
export async function POST(request: NextRequest) {
|
|
74
|
+
try {
|
|
75
|
+
await ensureDb();
|
|
76
|
+
const db = getDbPool();
|
|
77
|
+
const body = await request.json();
|
|
78
|
+
const { extensionId, scope, worldId, enabled, config, sortOrder } = body;
|
|
79
|
+
|
|
80
|
+
if (!extensionId) {
|
|
81
|
+
return NextResponse.json({ error: '缺少插件 ID' }, { status: 400 });
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const bindingScope = scope ?? 'global';
|
|
85
|
+
const bindingWorldId = worldId ?? '';
|
|
86
|
+
const bindingEnabled = enabled !== undefined ? enabled : true;
|
|
87
|
+
const bindingConfig = config ?? {};
|
|
88
|
+
const bindingSortOrder = sortOrder ?? 0;
|
|
89
|
+
|
|
90
|
+
await db.execute(
|
|
91
|
+
`INSERT INTO extension_bindings (extension_id, scope, world_id, enabled, config, sort_order)
|
|
92
|
+
VALUES (?, ?, ?, ?, ?, ?)
|
|
93
|
+
ON DUPLICATE KEY UPDATE
|
|
94
|
+
enabled = VALUES(enabled), config = VALUES(config), sort_order = VALUES(sort_order)`,
|
|
95
|
+
[extensionId, bindingScope, bindingWorldId, bindingEnabled, JSON.stringify(bindingConfig), bindingSortOrder]
|
|
96
|
+
);
|
|
97
|
+
|
|
98
|
+
return NextResponse.json({ success: true, extensionId });
|
|
99
|
+
} catch (e) {
|
|
100
|
+
console.error('创建插件绑定失败:', e);
|
|
101
|
+
return NextResponse.json({ error: '创建插件绑定失败' }, { status: 500 });
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// PUT: 更新绑定
|
|
106
|
+
export async function PUT(request: NextRequest) {
|
|
107
|
+
try {
|
|
108
|
+
await ensureDb();
|
|
109
|
+
const db = getDbPool();
|
|
110
|
+
const body = await request.json();
|
|
111
|
+
const { id, ...fields } = body;
|
|
112
|
+
|
|
113
|
+
if (!id) {
|
|
114
|
+
return NextResponse.json({ error: '缺少绑定 ID' }, { status: 400 });
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const updates: string[] = [];
|
|
118
|
+
const params: (string | number | boolean | null)[] = [];
|
|
119
|
+
|
|
120
|
+
const allowedFields = ['enabled', 'config', 'sortOrder', 'scope', 'worldId'];
|
|
121
|
+
const columnMap: Record<string, string> = {
|
|
122
|
+
sortOrder: 'sort_order',
|
|
123
|
+
worldId: 'world_id',
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
for (const field of allowedFields) {
|
|
127
|
+
if (fields[field] !== undefined) {
|
|
128
|
+
const col = columnMap[field] || field;
|
|
129
|
+
updates.push(`${col} = ?`);
|
|
130
|
+
if (field === 'config') {
|
|
131
|
+
params.push(JSON.stringify(fields[field]));
|
|
132
|
+
} else {
|
|
133
|
+
params.push(fields[field]);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (updates.length === 0) {
|
|
139
|
+
return NextResponse.json({ error: '没有需要更新的字段' }, { status: 400 });
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
params.push(id);
|
|
143
|
+
await db.execute(
|
|
144
|
+
`UPDATE extension_bindings SET ${updates.join(', ')} WHERE id = ?`,
|
|
145
|
+
params
|
|
146
|
+
);
|
|
147
|
+
|
|
148
|
+
return NextResponse.json({ success: true, id });
|
|
149
|
+
} catch (e) {
|
|
150
|
+
console.error('更新插件绑定失败:', e);
|
|
151
|
+
return NextResponse.json({ error: '更新插件绑定失败' }, { status: 500 });
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// DELETE: 删除绑定
|
|
156
|
+
export async function DELETE(request: NextRequest) {
|
|
157
|
+
try {
|
|
158
|
+
await ensureDb();
|
|
159
|
+
const db = getDbPool();
|
|
160
|
+
const { searchParams } = new URL(request.url);
|
|
161
|
+
const id = searchParams.get('id');
|
|
162
|
+
|
|
163
|
+
if (!id) {
|
|
164
|
+
return NextResponse.json({ error: '缺少绑定 ID' }, { status: 400 });
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
await db.execute('DELETE FROM extension_bindings WHERE id = ?', [id]);
|
|
168
|
+
return NextResponse.json({ success: true, id });
|
|
169
|
+
} catch (e) {
|
|
170
|
+
console.error('删除插件绑定失败:', e);
|
|
171
|
+
return NextResponse.json({ error: '删除插件绑定失败' }, { status: 500 });
|
|
172
|
+
}
|
|
173
|
+
}
|