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
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 路由历史栈 - 基于 localStorage 实现
|
|
3
|
+
*
|
|
4
|
+
* 用法:
|
|
5
|
+
* import { useRouterHistory } from '@/lib/router-history';
|
|
6
|
+
* const { push, back, canGoBack } = useRouterHistory();
|
|
7
|
+
*
|
|
8
|
+
* // 跳转到新页面(自动将当前路径压入栈)
|
|
9
|
+
* push('/extensions');
|
|
10
|
+
*
|
|
11
|
+
* // 返回上一页(从栈中弹出)
|
|
12
|
+
* back();
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
const STORAGE_KEY = 'xinyu_route_history';
|
|
16
|
+
|
|
17
|
+
function getStack(): string[] {
|
|
18
|
+
if (typeof window === 'undefined') return [];
|
|
19
|
+
try {
|
|
20
|
+
const raw = localStorage.getItem(STORAGE_KEY);
|
|
21
|
+
return raw ? JSON.parse(raw) : [];
|
|
22
|
+
} catch {
|
|
23
|
+
return [];
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function setStack(stack: string[]) {
|
|
28
|
+
if (typeof window === 'undefined') return;
|
|
29
|
+
try {
|
|
30
|
+
localStorage.setItem(STORAGE_KEY, JSON.stringify(stack));
|
|
31
|
+
} catch {
|
|
32
|
+
// localStorage 满了或不可用时静默失败
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** 将当前路径压入路由栈(去重:如果栈顶已经是当前路径则不重复压入) */
|
|
37
|
+
export function routeHistoryPush(currentPath: string) {
|
|
38
|
+
const stack = getStack();
|
|
39
|
+
// 去重:栈顶与当前路径相同则跳过
|
|
40
|
+
if (stack.length > 0 && stack[stack.length - 1] === currentPath) return;
|
|
41
|
+
// 限制栈深度,避免无限增长
|
|
42
|
+
if (stack.length >= 50) stack.shift();
|
|
43
|
+
stack.push(currentPath);
|
|
44
|
+
setStack(stack);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** 从路由栈弹出并返回上一条路径,栈为空时返回 null */
|
|
48
|
+
export function routeHistoryPop(): string | null {
|
|
49
|
+
const stack = getStack();
|
|
50
|
+
if (stack.length === 0) return null;
|
|
51
|
+
const prev = stack.pop()!;
|
|
52
|
+
setStack(stack);
|
|
53
|
+
return prev;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** 查看栈顶路径但不弹出 */
|
|
57
|
+
export function routeHistoryPeek(): string | null {
|
|
58
|
+
const stack = getStack();
|
|
59
|
+
return stack.length > 0 ? stack[stack.length - 1] : null;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** 路由栈是否非空 */
|
|
63
|
+
export function routeHistoryCanGoBack(): boolean {
|
|
64
|
+
return getStack().length > 0;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** 清空路由栈 */
|
|
68
|
+
export function routeHistoryClear() {
|
|
69
|
+
setStack([]);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** 获取完整路由栈(调试用) */
|
|
73
|
+
export function routeHistoryGetAll(): string[] {
|
|
74
|
+
return getStack();
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
import { useCallback } from 'react';
|
|
78
|
+
import { useRouter as useNextRouter } from 'next/navigation';
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* 自定义 Hook:封装路由跳转 + 历史栈管理
|
|
82
|
+
*
|
|
83
|
+
* - navigate(path): 跳转到目标页面,自动将当前路径压入栈
|
|
84
|
+
* - back(): 返回上一页,从栈中弹出路径并跳转
|
|
85
|
+
* - canGoBack: 是否可以返回
|
|
86
|
+
*/
|
|
87
|
+
export function useRouterHistory() {
|
|
88
|
+
const nextRouter = useNextRouter();
|
|
89
|
+
|
|
90
|
+
const navigate = useCallback(
|
|
91
|
+
(targetPath: string, {record = true, pop = false} = {}) => {
|
|
92
|
+
// 将当前路径压入栈
|
|
93
|
+
if (pop) {
|
|
94
|
+
routeHistoryPop();
|
|
95
|
+
}
|
|
96
|
+
if (typeof window !== 'undefined' && record) {
|
|
97
|
+
routeHistoryPush(window.location.pathname);
|
|
98
|
+
}
|
|
99
|
+
nextRouter.push(targetPath);
|
|
100
|
+
},
|
|
101
|
+
[nextRouter],
|
|
102
|
+
);
|
|
103
|
+
|
|
104
|
+
const back = useCallback(() => {
|
|
105
|
+
const prevPath = routeHistoryPop();
|
|
106
|
+
if (prevPath) {
|
|
107
|
+
nextRouter.push(prevPath);
|
|
108
|
+
} else {
|
|
109
|
+
// 栈为空时回退到首页
|
|
110
|
+
nextRouter.push('/');
|
|
111
|
+
}
|
|
112
|
+
}, [nextRouter]);
|
|
113
|
+
|
|
114
|
+
const canGoBack = useCallback(() => {
|
|
115
|
+
return routeHistoryCanGoBack();
|
|
116
|
+
}, []);
|
|
117
|
+
|
|
118
|
+
return { navigate, back, canGoBack };
|
|
119
|
+
}
|
package/lib/storage.ts
ADDED
|
@@ -0,0 +1,381 @@
|
|
|
1
|
+
// lib/storage.ts - 数据存取封装
|
|
2
|
+
// 游戏会话、世界卡片模板、应用设置 → 后端 MySQL API
|
|
3
|
+
// 主题配置 → localStorage(纯客户端 UI 偏好)
|
|
4
|
+
|
|
5
|
+
import { ThemeConfig, AppSettings, WorldCardTemplate } from './types';
|
|
6
|
+
import { PluginManifest, PluginBinding } from './plugin-types';
|
|
7
|
+
import { getDefaultThemeConfig } from './themes';
|
|
8
|
+
|
|
9
|
+
// ==================== 主题相关(localStorage) ====================
|
|
10
|
+
|
|
11
|
+
const THEME_CONFIG_KEY = 'xinyu-theme-config';
|
|
12
|
+
|
|
13
|
+
/** 通用 localStorage 读取 */
|
|
14
|
+
function getLocalItem<T>(key: string, fallback: T): T {
|
|
15
|
+
if (typeof window === 'undefined') return fallback;
|
|
16
|
+
try {
|
|
17
|
+
const raw = localStorage.getItem(key);
|
|
18
|
+
if (raw === null) return fallback;
|
|
19
|
+
return JSON.parse(raw) as T;
|
|
20
|
+
} catch {
|
|
21
|
+
return fallback;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** 通用 localStorage 写入 */
|
|
26
|
+
function setLocalItem<T>(key: string, value: T): void {
|
|
27
|
+
if (typeof window === 'undefined') return;
|
|
28
|
+
try {
|
|
29
|
+
localStorage.setItem(key, JSON.stringify(value));
|
|
30
|
+
} catch (e) {
|
|
31
|
+
console.error('Failed to write to localStorage:', e);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** 读取主题配置 */
|
|
36
|
+
export function getThemeConfig(): ThemeConfig {
|
|
37
|
+
return getLocalItem<ThemeConfig>(THEME_CONFIG_KEY, getDefaultThemeConfig());
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** 保存主题配置 */
|
|
41
|
+
export function saveThemeConfig(config: ThemeConfig): void {
|
|
42
|
+
setLocalItem(THEME_CONFIG_KEY, config);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// ==================== 游戏会话相关(后端 API) ====================
|
|
46
|
+
|
|
47
|
+
export interface StoredSession {
|
|
48
|
+
id: string;
|
|
49
|
+
worldSetting: Record<string, unknown>;
|
|
50
|
+
messages: Array<{ role: string; content: string }>;
|
|
51
|
+
createdAt: string;
|
|
52
|
+
updatedAt: string;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** 读取所有游戏会话 */
|
|
56
|
+
export async function getGameSessions(): Promise<StoredSession[]> {
|
|
57
|
+
try {
|
|
58
|
+
const res = await fetch('/api/sessions');
|
|
59
|
+
if (res.ok) {
|
|
60
|
+
return await res.json();
|
|
61
|
+
}
|
|
62
|
+
return [];
|
|
63
|
+
} catch {
|
|
64
|
+
return [];
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** 保存单个游戏会话(新增或更新) */
|
|
69
|
+
export async function saveGameSession(session: StoredSession): Promise<void> {
|
|
70
|
+
try {
|
|
71
|
+
await fetch('/api/sessions', {
|
|
72
|
+
method: 'POST',
|
|
73
|
+
headers: { 'Content-Type': 'application/json' },
|
|
74
|
+
body: JSON.stringify(session),
|
|
75
|
+
});
|
|
76
|
+
} catch (e) {
|
|
77
|
+
console.error('Failed to save game session:', e);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** 读取单个游戏会话 */
|
|
82
|
+
export async function getGameSession(id: string): Promise<StoredSession | undefined> {
|
|
83
|
+
try {
|
|
84
|
+
const res = await fetch(`/api/sessions?id=${encodeURIComponent(id)}`);
|
|
85
|
+
if (res.ok) {
|
|
86
|
+
return await res.json();
|
|
87
|
+
}
|
|
88
|
+
return undefined;
|
|
89
|
+
} catch {
|
|
90
|
+
return undefined;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** 删除游戏会话 */
|
|
95
|
+
export async function deleteGameSession(id: string): Promise<void> {
|
|
96
|
+
try {
|
|
97
|
+
await fetch(`/api/sessions?id=${encodeURIComponent(id)}`, { method: 'DELETE' });
|
|
98
|
+
} catch (e) {
|
|
99
|
+
console.error('Failed to delete game session:', e);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** 清除所有游戏会话 */
|
|
104
|
+
export async function clearAllGameSessions(): Promise<void> {
|
|
105
|
+
try {
|
|
106
|
+
await fetch('/api/sessions?action=clearAll', { method: 'DELETE' });
|
|
107
|
+
} catch (e) {
|
|
108
|
+
console.error('Failed to clear all game sessions:', e);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// ==================== 应用设置相关(后端 API) ====================
|
|
113
|
+
|
|
114
|
+
/** 默认应用设置 */
|
|
115
|
+
export function getDefaultAppSettings(): AppSettings {
|
|
116
|
+
return {
|
|
117
|
+
aiApiKey: '',
|
|
118
|
+
aiApiBase: 'https://api.openai.com/v1',
|
|
119
|
+
aiModel: 'gpt-4o',
|
|
120
|
+
aiTemperature: 0.8,
|
|
121
|
+
aiMaxTokens: 1024,
|
|
122
|
+
systemPromptExtra: '',
|
|
123
|
+
maxHistorySessions: 50,
|
|
124
|
+
pluginToastEnabled: true,
|
|
125
|
+
pluginToastLevels: ['info', 'success', 'warning', 'error'],
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** 读取应用设置(从后端 API) */
|
|
130
|
+
export async function getAppSettings(): Promise<AppSettings> {
|
|
131
|
+
try {
|
|
132
|
+
const res = await fetch('/api/settings');
|
|
133
|
+
if (res.ok) {
|
|
134
|
+
const data = await res.json();
|
|
135
|
+
if (data && !data.error) {
|
|
136
|
+
return { ...getDefaultAppSettings(), ...data };
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
} catch {
|
|
140
|
+
// ignore
|
|
141
|
+
}
|
|
142
|
+
return getDefaultAppSettings();
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/** 保存应用设置(到后端 API) */
|
|
146
|
+
export async function saveAppSettings(settings: AppSettings): Promise<boolean> {
|
|
147
|
+
try {
|
|
148
|
+
const res = await fetch('/api/settings', {
|
|
149
|
+
method: 'PUT',
|
|
150
|
+
headers: { 'Content-Type': 'application/json' },
|
|
151
|
+
body: JSON.stringify(settings),
|
|
152
|
+
});
|
|
153
|
+
return res.ok;
|
|
154
|
+
} catch {
|
|
155
|
+
return false;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// ==================== 世界卡片模板相关(后端 API) ====================
|
|
160
|
+
|
|
161
|
+
/** 读取所有世界卡片模板 */
|
|
162
|
+
export async function getWorldTemplates(): Promise<WorldCardTemplate[]> {
|
|
163
|
+
try {
|
|
164
|
+
const res = await fetch('/api/templates');
|
|
165
|
+
if (res.ok) {
|
|
166
|
+
return await res.json();
|
|
167
|
+
}
|
|
168
|
+
return [];
|
|
169
|
+
} catch {
|
|
170
|
+
return [];
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/** 保存世界卡片模板(新增或更新) */
|
|
175
|
+
export async function saveWorldTemplate(template: WorldCardTemplate): Promise<void> {
|
|
176
|
+
try {
|
|
177
|
+
await fetch('/api/templates', {
|
|
178
|
+
method: 'POST',
|
|
179
|
+
headers: { 'Content-Type': 'application/json' },
|
|
180
|
+
body: JSON.stringify(template),
|
|
181
|
+
});
|
|
182
|
+
} catch (e) {
|
|
183
|
+
console.error('Failed to save world template:', e);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/** 读取单个世界卡片模板 */
|
|
188
|
+
export async function getWorldTemplate(id: string): Promise<WorldCardTemplate | undefined> {
|
|
189
|
+
try {
|
|
190
|
+
const res = await fetch(`/api/templates?id=${encodeURIComponent(id)}`);
|
|
191
|
+
if (res.ok) {
|
|
192
|
+
return await res.json();
|
|
193
|
+
}
|
|
194
|
+
return undefined;
|
|
195
|
+
} catch {
|
|
196
|
+
return undefined;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/** 删除世界卡片模板 */
|
|
201
|
+
export async function deleteWorldTemplate(id: string): Promise<void> {
|
|
202
|
+
try {
|
|
203
|
+
await fetch(`/api/templates?id=${encodeURIComponent(id)}`, { method: 'DELETE' });
|
|
204
|
+
} catch (e) {
|
|
205
|
+
console.error('Failed to delete world template:', e);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// ==================== 插件相关(后端 API) ====================
|
|
210
|
+
|
|
211
|
+
/** 读取所有插件 */
|
|
212
|
+
export async function getPlugins(type?: string): Promise<PluginManifest[]> {
|
|
213
|
+
try {
|
|
214
|
+
const params = type ? `?type=${encodeURIComponent(type)}` : '';
|
|
215
|
+
const res = await fetch(`/api/plugins${params}`);
|
|
216
|
+
if (res.ok) {
|
|
217
|
+
return await res.json();
|
|
218
|
+
}
|
|
219
|
+
return [];
|
|
220
|
+
} catch {
|
|
221
|
+
return [];
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/** 读取单个插件 */
|
|
226
|
+
export async function getPlugin(id: string): Promise<PluginManifest | undefined> {
|
|
227
|
+
try {
|
|
228
|
+
const res = await fetch(`/api/plugins?id=${encodeURIComponent(id)}`);
|
|
229
|
+
if (res.ok) {
|
|
230
|
+
return await res.json();
|
|
231
|
+
}
|
|
232
|
+
return undefined;
|
|
233
|
+
} catch {
|
|
234
|
+
return undefined;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/** 创建插件 */
|
|
239
|
+
export async function createPlugin(plugin: PluginManifest): Promise<boolean> {
|
|
240
|
+
try {
|
|
241
|
+
const res = await fetch('/api/plugins', {
|
|
242
|
+
method: 'POST',
|
|
243
|
+
headers: { 'Content-Type': 'application/json' },
|
|
244
|
+
body: JSON.stringify(plugin),
|
|
245
|
+
});
|
|
246
|
+
return res.ok;
|
|
247
|
+
} catch (e) {
|
|
248
|
+
console.error('Failed to create plugin:', e);
|
|
249
|
+
return false;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/** 更新插件 */
|
|
254
|
+
export async function updatePlugin(id: string, fields: Partial<PluginManifest> & { newId?: string }): Promise<boolean> {
|
|
255
|
+
try {
|
|
256
|
+
const { newId, ...restFields } = fields;
|
|
257
|
+
const body: Record<string, unknown> = { id, ...restFields };
|
|
258
|
+
if (newId) body.newId = newId;
|
|
259
|
+
const res = await fetch('/api/plugins', {
|
|
260
|
+
method: 'PUT',
|
|
261
|
+
headers: { 'Content-Type': 'application/json' },
|
|
262
|
+
body: JSON.stringify(body),
|
|
263
|
+
});
|
|
264
|
+
return res.ok;
|
|
265
|
+
} catch (e) {
|
|
266
|
+
console.error('Failed to update plugin:', e);
|
|
267
|
+
return false;
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/** 删除插件 */
|
|
272
|
+
export async function deletePlugin(id: string): Promise<void> {
|
|
273
|
+
try {
|
|
274
|
+
await fetch(`/api/plugins?id=${encodeURIComponent(id)}`, { method: 'DELETE' });
|
|
275
|
+
} catch (e) {
|
|
276
|
+
console.error('Failed to delete plugin:', e);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/** 获取插件绑定列表 */
|
|
281
|
+
export async function getPluginBindings(scope?: string, worldId?: string): Promise<PluginBinding[]> {
|
|
282
|
+
try {
|
|
283
|
+
const params = new URLSearchParams();
|
|
284
|
+
if (scope) params.set('scope', scope);
|
|
285
|
+
if (worldId) params.set('worldId', worldId);
|
|
286
|
+
const query = params.toString();
|
|
287
|
+
const res = await fetch(`/api/plugins/bindings${query ? `?${query}` : ''}`);
|
|
288
|
+
if (res.ok) {
|
|
289
|
+
return await res.json();
|
|
290
|
+
}
|
|
291
|
+
return [];
|
|
292
|
+
} catch {
|
|
293
|
+
return [];
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/** 创建或更新插件绑定 */
|
|
298
|
+
export async function upsertPluginBinding(binding: Partial<PluginBinding>): Promise<boolean> {
|
|
299
|
+
try {
|
|
300
|
+
const res = await fetch('/api/plugins/bindings', {
|
|
301
|
+
method: 'POST',
|
|
302
|
+
headers: { 'Content-Type': 'application/json' },
|
|
303
|
+
body: JSON.stringify(binding),
|
|
304
|
+
});
|
|
305
|
+
return res.ok;
|
|
306
|
+
} catch (e) {
|
|
307
|
+
console.error('Failed to upsert plugin binding:', e);
|
|
308
|
+
return false;
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/** 更新插件绑定 */
|
|
313
|
+
export async function updatePluginBinding(id: number, fields: Partial<PluginBinding>): Promise<boolean> {
|
|
314
|
+
try {
|
|
315
|
+
const res = await fetch('/api/plugins/bindings', {
|
|
316
|
+
method: 'PUT',
|
|
317
|
+
headers: { 'Content-Type': 'application/json' },
|
|
318
|
+
body: JSON.stringify({ id, ...fields }),
|
|
319
|
+
});
|
|
320
|
+
return res.ok;
|
|
321
|
+
} catch (e) {
|
|
322
|
+
console.error('Failed to update plugin binding:', e);
|
|
323
|
+
return false;
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/** 删除插件绑定 */
|
|
328
|
+
export async function deletePluginBinding(id: number): Promise<void> {
|
|
329
|
+
try {
|
|
330
|
+
await fetch(`/api/plugins/bindings?id=${id}`, { method: 'DELETE' });
|
|
331
|
+
} catch (e) {
|
|
332
|
+
console.error('Failed to delete plugin binding:', e);
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/** 导出所有插件 */
|
|
337
|
+
export async function exportPlugins(): Promise<string> {
|
|
338
|
+
try {
|
|
339
|
+
const res = await fetch('/api/plugins/export');
|
|
340
|
+
if (res.ok) {
|
|
341
|
+
return await res.text();
|
|
342
|
+
}
|
|
343
|
+
return '[]';
|
|
344
|
+
} catch {
|
|
345
|
+
return '[]';
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
/** 导入插件 */
|
|
350
|
+
export async function importPlugins(data: unknown): Promise<{ imported: number; skipped: number; total: number }> {
|
|
351
|
+
try {
|
|
352
|
+
const res = await fetch('/api/plugins/import', {
|
|
353
|
+
method: 'POST',
|
|
354
|
+
headers: { 'Content-Type': 'application/json' },
|
|
355
|
+
body: JSON.stringify(data),
|
|
356
|
+
});
|
|
357
|
+
if (res.ok) {
|
|
358
|
+
return await res.json();
|
|
359
|
+
}
|
|
360
|
+
return { imported: 0, skipped: 0, total: 0 };
|
|
361
|
+
} catch {
|
|
362
|
+
return { imported: 0, skipped: 0, total: 0 };
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
/** AI 安全检测 */
|
|
367
|
+
export async function scanPluginSecurity(code: string, manifest?: Partial<PluginManifest>): Promise<import('./plugin-types').SecurityScanResult | null> {
|
|
368
|
+
try {
|
|
369
|
+
const res = await fetch('/api/plugins/scan', {
|
|
370
|
+
method: 'POST',
|
|
371
|
+
headers: { 'Content-Type': 'application/json' },
|
|
372
|
+
body: JSON.stringify({ code, manifest }),
|
|
373
|
+
});
|
|
374
|
+
if (res.ok) {
|
|
375
|
+
return await res.json();
|
|
376
|
+
}
|
|
377
|
+
return null;
|
|
378
|
+
} catch {
|
|
379
|
+
return null;
|
|
380
|
+
}
|
|
381
|
+
}
|
package/lib/themes.ts
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
// lib/themes.ts - 主题定义(预设主题 & 类型)
|
|
2
|
+
|
|
3
|
+
import { Theme, ThemeConfig } from './types';
|
|
4
|
+
|
|
5
|
+
/** 暗夜紫(Deep Night)- 默认主题 */
|
|
6
|
+
const deepNight: Theme = {
|
|
7
|
+
id: 'deep-night',
|
|
8
|
+
name: '暗夜紫',
|
|
9
|
+
description: '深紫黑背景配金色强调,适合奇幻/史诗氛围',
|
|
10
|
+
isDark: true,
|
|
11
|
+
variables: {
|
|
12
|
+
'--color-bg-primary': '#0f0a1a',
|
|
13
|
+
'--color-bg-secondary': '#1a1128',
|
|
14
|
+
'--color-bg-tertiary': '#2a1f3d',
|
|
15
|
+
'--color-text-primary': '#e8e0f0',
|
|
16
|
+
'--color-text-secondary': '#a89bc2',
|
|
17
|
+
'--color-text-muted': '#6b5f85',
|
|
18
|
+
'--color-accent': '#d4a843',
|
|
19
|
+
'--color-accent-hover': '#e6bc5a',
|
|
20
|
+
'--color-border': '#2a1f3d',
|
|
21
|
+
'--color-shadow': 'rgba(0, 0, 0, 0.5)',
|
|
22
|
+
'--color-user-bubble': '#2a1f3d',
|
|
23
|
+
'--color-ai-bubble': '#1a1128',
|
|
24
|
+
'--font-body': "'Noto Sans SC', 'PingFang SC', 'Microsoft YaHei', sans-serif",
|
|
25
|
+
'--font-heading': "'Noto Sans SC', 'PingFang SC', 'Microsoft YaHei', serif",
|
|
26
|
+
'--border-radius': '12px',
|
|
27
|
+
},
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
/** 月光白(Moonlight)*/
|
|
31
|
+
const moonlight: Theme = {
|
|
32
|
+
id: 'moonlight',
|
|
33
|
+
name: '月光白',
|
|
34
|
+
description: '米白背景配深蓝强调,适合轻松/日常氛围',
|
|
35
|
+
isDark: false,
|
|
36
|
+
variables: {
|
|
37
|
+
'--color-bg-primary': '#f5f3f0',
|
|
38
|
+
'--color-bg-secondary': '#ffffff',
|
|
39
|
+
'--color-bg-tertiary': '#e8e4df',
|
|
40
|
+
'--color-text-primary': '#1a1a2e',
|
|
41
|
+
'--color-text-secondary': '#4a4a6a',
|
|
42
|
+
'--color-text-muted': '#8888a0',
|
|
43
|
+
'--color-accent': '#2a4a7f',
|
|
44
|
+
'--color-accent-hover': '#3a5a9f',
|
|
45
|
+
'--color-border': '#d8d4cf',
|
|
46
|
+
'--color-shadow': 'rgba(0, 0, 0, 0.08)',
|
|
47
|
+
'--color-user-bubble': '#2a4a7f',
|
|
48
|
+
'--color-ai-bubble': '#e8e4df',
|
|
49
|
+
'--font-body': "'Noto Sans SC', 'PingFang SC', 'Microsoft YaHei', sans-serif",
|
|
50
|
+
'--font-heading': "'Noto Sans SC', 'PingFang SC', 'Microsoft YaHei', serif",
|
|
51
|
+
'--border-radius': '12px',
|
|
52
|
+
},
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
/** 赛博绿(Cyber Glow)*/
|
|
56
|
+
const cyberGlow: Theme = {
|
|
57
|
+
id: 'cyber-glow',
|
|
58
|
+
name: '赛博绿',
|
|
59
|
+
description: '纯黑背景配荧光绿强调,适合科幻/赛博朋克氛围',
|
|
60
|
+
isDark: true,
|
|
61
|
+
variables: {
|
|
62
|
+
'--color-bg-primary': '#0a0a0a',
|
|
63
|
+
'--color-bg-secondary': '#111111',
|
|
64
|
+
'--color-bg-tertiary': '#1a1a1a',
|
|
65
|
+
'--color-text-primary': '#e0ffe0',
|
|
66
|
+
'--color-text-secondary': '#80c080',
|
|
67
|
+
'--color-text-muted': '#406040',
|
|
68
|
+
'--color-accent': '#00ff88',
|
|
69
|
+
'--color-accent-hover': '#33ffaa',
|
|
70
|
+
'--color-border': '#1a3a1a',
|
|
71
|
+
'--color-shadow': 'rgba(0, 255, 136, 0.1)',
|
|
72
|
+
'--color-user-bubble': '#0a2a15',
|
|
73
|
+
'--color-ai-bubble': '#111111',
|
|
74
|
+
'--font-body': "'Noto Sans SC', 'PingFang SC', 'Microsoft YaHei', monospace",
|
|
75
|
+
'--font-heading': "'Noto Sans SC', 'PingFang SC', 'Microsoft YaHei', monospace",
|
|
76
|
+
'--border-radius': '4px',
|
|
77
|
+
},
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
/** 羊皮纸(Parchment)*/
|
|
81
|
+
const parchment: Theme = {
|
|
82
|
+
id: 'parchment',
|
|
83
|
+
name: '羊皮纸',
|
|
84
|
+
description: '暖黄仿旧纸张背景配棕色强调,适合古典/历史氛围',
|
|
85
|
+
isDark: false,
|
|
86
|
+
variables: {
|
|
87
|
+
'--color-bg-primary': '#f0e6d0',
|
|
88
|
+
'--color-bg-secondary': '#f8f0e0',
|
|
89
|
+
'--color-bg-tertiary': '#e0d4be',
|
|
90
|
+
'--color-text-primary': '#3a2a1a',
|
|
91
|
+
'--color-text-secondary': '#6a5a4a',
|
|
92
|
+
'--color-text-muted': '#9a8a7a',
|
|
93
|
+
'--color-accent': '#8b4513',
|
|
94
|
+
'--color-accent-hover': '#a0522d',
|
|
95
|
+
'--color-border': '#c8b898',
|
|
96
|
+
'--color-shadow': 'rgba(60, 40, 20, 0.15)',
|
|
97
|
+
'--color-user-bubble': '#8b4513',
|
|
98
|
+
'--color-ai-bubble': '#e8dcc8',
|
|
99
|
+
'--font-body': "'Noto Serif SC', 'STSong', 'SimSun', serif",
|
|
100
|
+
'--font-heading': "'Noto Serif SC', 'STSong', 'SimSun', serif",
|
|
101
|
+
'--border-radius': '8px',
|
|
102
|
+
},
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
/** 所有预设主题 */
|
|
106
|
+
export const presetThemes: Theme[] = [deepNight, moonlight, cyberGlow, parchment];
|
|
107
|
+
|
|
108
|
+
/** 默认主题 ID */
|
|
109
|
+
export const DEFAULT_THEME_ID = 'deep-night';
|
|
110
|
+
|
|
111
|
+
/** 获取默认主题配置 */
|
|
112
|
+
export function getDefaultThemeConfig(): ThemeConfig {
|
|
113
|
+
return {
|
|
114
|
+
presets: presetThemes,
|
|
115
|
+
activeThemeId: DEFAULT_THEME_ID,
|
|
116
|
+
customTheme: null,
|
|
117
|
+
importedThemes: [],
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** 根据 ID 查找主题 */
|
|
122
|
+
export function findThemeById(config: ThemeConfig, id: string): Theme | undefined {
|
|
123
|
+
if (id === 'custom' && config.customTheme) {
|
|
124
|
+
return config.customTheme;
|
|
125
|
+
}
|
|
126
|
+
const preset = config.presets.find((t) => t.id === id);
|
|
127
|
+
if (preset) return preset;
|
|
128
|
+
return (config.importedThemes || []).find((t) => t.id === id);
|
|
129
|
+
}
|