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,2287 @@
|
|
|
1
|
+
// lib/plugin-runtime.ts - 插件运行时引擎
|
|
2
|
+
// 负责插件加载、执行、Hook 调度、错误隔离
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
PluginManifest,
|
|
6
|
+
PluginInstance,
|
|
7
|
+
PluginBinding,
|
|
8
|
+
PluginHook,
|
|
9
|
+
HookHandler,
|
|
10
|
+
GameAttribute,
|
|
11
|
+
ChatCommand,
|
|
12
|
+
SidebarPanelRegistration,
|
|
13
|
+
ToolbarButtonRegistration,
|
|
14
|
+
MessageRenderer,
|
|
15
|
+
UISlotId,
|
|
16
|
+
SlotRegistration,
|
|
17
|
+
ModalRegistration,
|
|
18
|
+
ModalActionInput,
|
|
19
|
+
ConfirmOptions,
|
|
20
|
+
DependencyStatus,
|
|
21
|
+
} from './plugin-types';
|
|
22
|
+
import { PluginResourceTracker } from './plugin-resource-tracker';
|
|
23
|
+
import { sanitizeHTML } from './plugin-html-sanitizer';
|
|
24
|
+
import { createDOMSandbox } from './plugin-dom-sandbox';
|
|
25
|
+
import { checkSlotPermission, checkDOMPermission, checkStylePermission, checkModalPermission, checkHostEventPermission, checkStoragePermission, checkFileReadPermission, checkFileWritePermission, checkInputControlPermission, checkGameInfoPermission, checkGameInfoWritePermission } from './plugin-security';
|
|
26
|
+
|
|
27
|
+
/** Hook 注册项 */
|
|
28
|
+
interface HookRegistration {
|
|
29
|
+
pluginId: string;
|
|
30
|
+
handler: HookHandler;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** 宿主事件注册项 */
|
|
34
|
+
interface HostEventRegistration {
|
|
35
|
+
pluginId: string;
|
|
36
|
+
eventName: string;
|
|
37
|
+
handler: HookHandler;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** 插件运行时状态 */
|
|
41
|
+
interface RuntimeState {
|
|
42
|
+
instances: Map<string, PluginInstance>;
|
|
43
|
+
hooks: Map<PluginHook, HookRegistration[]>;
|
|
44
|
+
gameAttributes: Map<string, GameAttribute>;
|
|
45
|
+
chatCommands: Map<string, ChatCommand>;
|
|
46
|
+
sidebarPanels: SidebarPanelRegistration[];
|
|
47
|
+
toolbarButtons: Map<string, ToolbarButtonRegistration[]>; // pluginId -> buttons
|
|
48
|
+
messageRenderers: MessageRenderer[];
|
|
49
|
+
gameState: Record<string, unknown>;
|
|
50
|
+
promptModifiers: Array<{ pluginId: string; handler: (prompt: string, worldSetting: Record<string, unknown>) => string }>;
|
|
51
|
+
beforeSendHandlers: Array<{ pluginId: string; handler: (messages: { role: string; content: string }[]) => { role: string; content: string }[] }>;
|
|
52
|
+
beforeCompleteHandlers: Array<{ pluginId: string; handler: (messages: { role: string; content: string }[]) => { role: string; content: string }[] }>;
|
|
53
|
+
afterReceiveHandlers: Array<{ pluginId: string; handler: (content: string) => string | Promise<string> }>;
|
|
54
|
+
requestConfigModifiers: Array<{ pluginId: string; handler: (config: { temperature: number; maxTokens: number; model: string }) => { temperature: number; maxTokens: number; model: string } }>;
|
|
55
|
+
// 新增:全局 UI API 状态
|
|
56
|
+
slotRegistrations: SlotRegistration[];
|
|
57
|
+
actionHandlers: Array<{ pluginId: string; actionName: string; handler: (e: { target: HTMLElement; actionName: string; payload?: string }) => void }>;
|
|
58
|
+
modals: Map<string, ModalRegistration>;
|
|
59
|
+
hostEventHandlers: HostEventRegistration[];
|
|
60
|
+
// 新增:依赖状态追踪
|
|
61
|
+
dependencyStatus: Map<string, DependencyStatus[]>;
|
|
62
|
+
// 新增:游戏初始化标记(确保只触发一次)
|
|
63
|
+
gameInitTriggered: boolean;
|
|
64
|
+
// 新增:权限注册表(记录 permission → pluginId 的持有关系)
|
|
65
|
+
permissionRegistry: Map<string, { pluginId: string; type: 'common' | 'exclusive' }>;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** UI 回调(由 PluginProvider 注入) */
|
|
69
|
+
interface UICallbacks {
|
|
70
|
+
sendMessage?: (content: string) => Promise<void>;
|
|
71
|
+
insertSystemMessage?: (content: string) => void;
|
|
72
|
+
insertNotice?: (html: string) => void;
|
|
73
|
+
toast?: (message: string, type?: 'info' | 'success' | 'warning' | 'error') => void;
|
|
74
|
+
showModal?: (modal: ModalRegistration) => void;
|
|
75
|
+
closeModal?: (modalId: string) => void;
|
|
76
|
+
updateModal?: (modalId: string, options: { title?: string; content?: string; width?: string; style?: Record<string, unknown> }) => void;
|
|
77
|
+
confirm?: (title: string, message: string, options?: ConfirmOptions) => Promise<boolean>;
|
|
78
|
+
emitHostEvent?: (eventName: string, data?: unknown) => void;
|
|
79
|
+
/** 配置变更广播回调(由 PluginProvider 注入,用于跨标签页同步) */
|
|
80
|
+
onConfigChange?: (pluginId: string, config: Record<string, unknown>) => void;
|
|
81
|
+
/** 插槽内容变更回调(由 PluginProvider 注入,用于触发 UI 刷新) */
|
|
82
|
+
onSlotChange?: () => void;
|
|
83
|
+
/** 输入框控制回调(由 PluginProvider 注入) */
|
|
84
|
+
inputControl?: {
|
|
85
|
+
setInputElement: (el: HTMLTextAreaElement | null) => void;
|
|
86
|
+
getInputListeners: () => Array<(value: string) => void>;
|
|
87
|
+
getSendListeners: () => Array<(content: string) => void>;
|
|
88
|
+
getKeyDownListeners: () => Array<(event: { key: string; ctrlKey: boolean; shiftKey: boolean; altKey: boolean; preventDefault: () => void }) => void>;
|
|
89
|
+
};
|
|
90
|
+
/** 获取世界设定回调(由 PluginProvider 注入) */
|
|
91
|
+
getWorldSetting?: () => Record<string, unknown>;
|
|
92
|
+
/** 获取当前会话 ID 回调(由 PluginProvider 注入) */
|
|
93
|
+
getSessionId?: () => string;
|
|
94
|
+
/** 获取当前消息列表回调(由 PluginProvider 注入) */
|
|
95
|
+
getMessages?: () => { role: string; content: string }[];
|
|
96
|
+
/** 设置世界设定回调(由 PluginProvider 注入) */
|
|
97
|
+
setWorldSetting?: (data: Record<string, unknown>) => void;
|
|
98
|
+
/** 获取当前世界 ID 回调(由 PluginProvider 注入) */
|
|
99
|
+
getWorldId?: () => string | undefined;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** 获取插槽容器 DOM 元素的回调类型 */
|
|
103
|
+
type GetSlotContainerCallback = (slotId: UISlotId) => HTMLElement | null;
|
|
104
|
+
|
|
105
|
+
/** 创建初始运行时状态 */
|
|
106
|
+
function createRuntimeState(): RuntimeState {
|
|
107
|
+
return {
|
|
108
|
+
instances: new Map(),
|
|
109
|
+
hooks: new Map(),
|
|
110
|
+
gameAttributes: new Map(),
|
|
111
|
+
chatCommands: new Map(),
|
|
112
|
+
sidebarPanels: [],
|
|
113
|
+
toolbarButtons: new Map(),
|
|
114
|
+
messageRenderers: [],
|
|
115
|
+
gameState: {},
|
|
116
|
+
promptModifiers: [],
|
|
117
|
+
beforeSendHandlers: [],
|
|
118
|
+
beforeCompleteHandlers: [],
|
|
119
|
+
afterReceiveHandlers: [],
|
|
120
|
+
requestConfigModifiers: [],
|
|
121
|
+
slotRegistrations: [],
|
|
122
|
+
actionHandlers: [],
|
|
123
|
+
modals: new Map(),
|
|
124
|
+
hostEventHandlers: [],
|
|
125
|
+
dependencyStatus: new Map(),
|
|
126
|
+
gameInitTriggered: false,
|
|
127
|
+
permissionRegistry: new Map(),
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** 插件运行时引擎 */
|
|
132
|
+
class PluginRuntimeEngine {
|
|
133
|
+
private state: RuntimeState;
|
|
134
|
+
private pluginStorage: Map<string, Map<string, unknown>>;
|
|
135
|
+
private uiCallbacks: UICallbacks;
|
|
136
|
+
private resourceTracker: PluginResourceTracker;
|
|
137
|
+
private getSlotContainerCallback: GetSlotContainerCallback | null;
|
|
138
|
+
|
|
139
|
+
constructor() {
|
|
140
|
+
this.state = createRuntimeState();
|
|
141
|
+
this.pluginStorage = new Map();
|
|
142
|
+
this.uiCallbacks = {};
|
|
143
|
+
this.resourceTracker = new PluginResourceTracker();
|
|
144
|
+
this.getSlotContainerCallback = null;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// ==================== UI 回调注入 ====================
|
|
148
|
+
|
|
149
|
+
/** 由 PluginProvider 调用,注入 UI 交互回调 */
|
|
150
|
+
setUICallbacks(callbacks: UICallbacks): void {
|
|
151
|
+
this.uiCallbacks = { ...this.uiCallbacks, ...callbacks };
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/** 设置获取插槽容器 DOM 元素的回调 */
|
|
155
|
+
setGetSlotContainerCallback(callback: GetSlotContainerCallback): void {
|
|
156
|
+
this.getSlotContainerCallback = callback;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// ==================== 输入框控制 API ====================
|
|
160
|
+
|
|
161
|
+
/** 输入框控制对象,供插件沙箱 API 调用 */
|
|
162
|
+
get inputControl() {
|
|
163
|
+
const callbacks = this.uiCallbacks.inputControl;
|
|
164
|
+
return {
|
|
165
|
+
getContent: (): string | null => {
|
|
166
|
+
const el = document.getElementById('xinyu-chat-input') as HTMLTextAreaElement | null;
|
|
167
|
+
return el?.value ?? null;
|
|
168
|
+
},
|
|
169
|
+
setContent: (text: string): void => {
|
|
170
|
+
const el = document.getElementById('xinyu-chat-input') as HTMLTextAreaElement | null;
|
|
171
|
+
if (!el) return;
|
|
172
|
+
const nativeInputValueSetter = Object.getOwnPropertyDescriptor(window.HTMLTextAreaElement.prototype, 'value')?.set;
|
|
173
|
+
if (nativeInputValueSetter) {
|
|
174
|
+
nativeInputValueSetter.call(el, text);
|
|
175
|
+
} else {
|
|
176
|
+
el.value = text;
|
|
177
|
+
}
|
|
178
|
+
el.dispatchEvent(new Event('input', { bubbles: true }));
|
|
179
|
+
},
|
|
180
|
+
appendContent: (text: string): void => {
|
|
181
|
+
const el = document.getElementById('xinyu-chat-input') as HTMLTextAreaElement | null;
|
|
182
|
+
if (!el) return;
|
|
183
|
+
const start = el.selectionStart ?? el.value.length;
|
|
184
|
+
const end = el.selectionEnd ?? el.value.length;
|
|
185
|
+
const before = el.value.substring(0, start);
|
|
186
|
+
const after = el.value.substring(end);
|
|
187
|
+
const newValue = before + text + after;
|
|
188
|
+
const nativeInputValueSetter = Object.getOwnPropertyDescriptor(window.HTMLTextAreaElement.prototype, 'value')?.set;
|
|
189
|
+
if (nativeInputValueSetter) {
|
|
190
|
+
nativeInputValueSetter.call(el, newValue);
|
|
191
|
+
} else {
|
|
192
|
+
el.value = newValue;
|
|
193
|
+
}
|
|
194
|
+
const newCursorPos = start + text.length;
|
|
195
|
+
el.setSelectionRange(newCursorPos, newCursorPos);
|
|
196
|
+
el.dispatchEvent(new Event('input', { bubbles: true }));
|
|
197
|
+
},
|
|
198
|
+
clearContent: (): void => {
|
|
199
|
+
const el = document.getElementById('xinyu-chat-input') as HTMLTextAreaElement | null;
|
|
200
|
+
if (!el) return;
|
|
201
|
+
const nativeInputValueSetter = Object.getOwnPropertyDescriptor(window.HTMLTextAreaElement.prototype, 'value')?.set;
|
|
202
|
+
if (nativeInputValueSetter) {
|
|
203
|
+
nativeInputValueSetter.call(el, '');
|
|
204
|
+
} else {
|
|
205
|
+
el.value = '';
|
|
206
|
+
}
|
|
207
|
+
el.dispatchEvent(new Event('input', { bubbles: true }));
|
|
208
|
+
},
|
|
209
|
+
setPlaceholder: (text: string): void => {
|
|
210
|
+
const el = document.getElementById('xinyu-chat-input') as HTMLTextAreaElement | null;
|
|
211
|
+
if (!el) return;
|
|
212
|
+
el.placeholder = text;
|
|
213
|
+
},
|
|
214
|
+
setDisabled: (disabled: boolean): void => {
|
|
215
|
+
const el = document.getElementById('xinyu-chat-input') as HTMLTextAreaElement | null;
|
|
216
|
+
if (!el) return;
|
|
217
|
+
el.disabled = disabled;
|
|
218
|
+
},
|
|
219
|
+
focus: (): void => {
|
|
220
|
+
(document.getElementById('xinyu-chat-input') as HTMLTextAreaElement | null)?.focus();
|
|
221
|
+
},
|
|
222
|
+
blur: (): void => {
|
|
223
|
+
(document.getElementById('xinyu-chat-input') as HTMLTextAreaElement | null)?.blur();
|
|
224
|
+
},
|
|
225
|
+
setStyle: (style: Record<string, string>): void => {
|
|
226
|
+
const el = document.getElementById('xinyu-chat-input') as HTMLTextAreaElement | null;
|
|
227
|
+
if (!el) return;
|
|
228
|
+
Object.assign(el.style, style);
|
|
229
|
+
},
|
|
230
|
+
injectStyle: (css: string): string => {
|
|
231
|
+
const styleId = `xinyu-plugin-input-style-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
232
|
+
const styleEl = document.createElement('style');
|
|
233
|
+
styleEl.id = styleId;
|
|
234
|
+
styleEl.setAttribute('data-xinyu-input-style', 'true');
|
|
235
|
+
styleEl.textContent = css;
|
|
236
|
+
document.head.appendChild(styleEl);
|
|
237
|
+
return styleId;
|
|
238
|
+
},
|
|
239
|
+
removeStyle: (styleId: string): void => {
|
|
240
|
+
const el = document.getElementById(styleId);
|
|
241
|
+
if (el) el.remove();
|
|
242
|
+
},
|
|
243
|
+
onInput: (handler: (value: string) => void): (() => void) => {
|
|
244
|
+
if (!callbacks) return () => {};
|
|
245
|
+
callbacks.getInputListeners().push(handler);
|
|
246
|
+
return () => {
|
|
247
|
+
const listeners = callbacks.getInputListeners();
|
|
248
|
+
const idx = listeners.indexOf(handler);
|
|
249
|
+
if (idx !== -1) listeners.splice(idx, 1);
|
|
250
|
+
};
|
|
251
|
+
},
|
|
252
|
+
onSend: (handler: (content: string) => void): (() => void) => {
|
|
253
|
+
if (!callbacks) return () => {};
|
|
254
|
+
callbacks.getSendListeners().push(handler);
|
|
255
|
+
return () => {
|
|
256
|
+
const listeners = callbacks.getSendListeners();
|
|
257
|
+
const idx = listeners.indexOf(handler);
|
|
258
|
+
if (idx !== -1) listeners.splice(idx, 1);
|
|
259
|
+
};
|
|
260
|
+
},
|
|
261
|
+
onKeyDown: (handler: (event: { key: string; ctrlKey: boolean; shiftKey: boolean; altKey: boolean; preventDefault: () => void }) => void): (() => void) => {
|
|
262
|
+
if (!callbacks) return () => {};
|
|
263
|
+
callbacks.getKeyDownListeners().push(handler);
|
|
264
|
+
return () => {
|
|
265
|
+
const listeners = callbacks.getKeyDownListeners();
|
|
266
|
+
const idx = listeners.indexOf(handler);
|
|
267
|
+
if (idx !== -1) listeners.splice(idx, 1);
|
|
268
|
+
};
|
|
269
|
+
},
|
|
270
|
+
getSelection: (): { start: number; end: number; text: string } | null => {
|
|
271
|
+
const el = document.getElementById('xinyu-chat-input') as HTMLTextAreaElement | null;
|
|
272
|
+
if (!el) return null;
|
|
273
|
+
const start = el.selectionStart ?? 0;
|
|
274
|
+
const end = el.selectionEnd ?? 0;
|
|
275
|
+
return { start, end, text: el.value.substring(start, end) };
|
|
276
|
+
},
|
|
277
|
+
setSelection: (start: number, end: number): void => {
|
|
278
|
+
const el = document.getElementById('xinyu-chat-input') as HTMLTextAreaElement | null;
|
|
279
|
+
if (!el) return;
|
|
280
|
+
el.setSelectionRange(start, end);
|
|
281
|
+
},
|
|
282
|
+
insertAtCursor: (text: string): void => {
|
|
283
|
+
const el = document.getElementById('xinyu-chat-input') as HTMLTextAreaElement | null;
|
|
284
|
+
if (!el) return;
|
|
285
|
+
const start = el.selectionStart ?? el.value.length;
|
|
286
|
+
const end = el.selectionEnd ?? el.value.length;
|
|
287
|
+
const before = el.value.substring(0, start);
|
|
288
|
+
const after = el.value.substring(end);
|
|
289
|
+
const newValue = before + text + after;
|
|
290
|
+
const nativeInputValueSetter = Object.getOwnPropertyDescriptor(window.HTMLTextAreaElement.prototype, 'value')?.set;
|
|
291
|
+
if (nativeInputValueSetter) {
|
|
292
|
+
nativeInputValueSetter.call(el, newValue);
|
|
293
|
+
} else {
|
|
294
|
+
el.value = newValue;
|
|
295
|
+
}
|
|
296
|
+
const newCursorPos = start + text.length;
|
|
297
|
+
el.setSelectionRange(newCursorPos, newCursorPos);
|
|
298
|
+
el.dispatchEvent(new Event('input', { bubbles: true }));
|
|
299
|
+
},
|
|
300
|
+
setMaxLength: (length: number): void => {
|
|
301
|
+
const el = document.getElementById('xinyu-chat-input') as HTMLTextAreaElement | null;
|
|
302
|
+
if (!el) return;
|
|
303
|
+
el.maxLength = length;
|
|
304
|
+
},
|
|
305
|
+
getLength: (): number => {
|
|
306
|
+
const el = document.getElementById('xinyu-chat-input') as HTMLTextAreaElement | null;
|
|
307
|
+
return el?.value.length ?? 0;
|
|
308
|
+
},
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// ==================== 插件生命周期管理 ====================
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* 检查插件权限冲突
|
|
316
|
+
* 返回冲突列表,每项包含冲突的权限、冲突的插件 ID、冲突类型
|
|
317
|
+
*/
|
|
318
|
+
checkPermissionConflicts(manifest: PluginManifest): Array<{
|
|
319
|
+
permission: string;
|
|
320
|
+
conflictingPluginId: string;
|
|
321
|
+
conflictingPluginName: string;
|
|
322
|
+
conflictType: 'exclusive-vs-any' | 'common-vs-exclusive';
|
|
323
|
+
}> {
|
|
324
|
+
const conflicts: Array<{
|
|
325
|
+
permission: string;
|
|
326
|
+
conflictingPluginId: string;
|
|
327
|
+
conflictingPluginName: string;
|
|
328
|
+
conflictType: 'exclusive-vs-any' | 'common-vs-exclusive';
|
|
329
|
+
}> = [];
|
|
330
|
+
|
|
331
|
+
const commonPerms = manifest.commonPermissions || [];
|
|
332
|
+
const exclusivePerms = manifest.exclusivePermissions || [];
|
|
333
|
+
|
|
334
|
+
// 检查排他权限:与任何已存在的权限(common 或 exclusive)都冲突
|
|
335
|
+
for (const perm of exclusivePerms) {
|
|
336
|
+
const existing = this.state.permissionRegistry.get(perm);
|
|
337
|
+
if (existing && existing.pluginId !== manifest.id) {
|
|
338
|
+
const existingInstance = this.state.instances.get(existing.pluginId);
|
|
339
|
+
conflicts.push({
|
|
340
|
+
permission: perm,
|
|
341
|
+
conflictingPluginId: existing.pluginId,
|
|
342
|
+
conflictingPluginName: existingInstance?.manifest.name || existing.pluginId,
|
|
343
|
+
conflictType: 'exclusive-vs-any',
|
|
344
|
+
});
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// 检查共享权限:与已存在的排他权限冲突
|
|
349
|
+
for (const perm of commonPerms) {
|
|
350
|
+
const existing = this.state.permissionRegistry.get(perm);
|
|
351
|
+
if (existing && existing.pluginId !== manifest.id && existing.type === 'exclusive') {
|
|
352
|
+
const existingInstance = this.state.instances.get(existing.pluginId);
|
|
353
|
+
conflicts.push({
|
|
354
|
+
permission: perm,
|
|
355
|
+
conflictingPluginId: existing.pluginId,
|
|
356
|
+
conflictingPluginName: existingInstance?.manifest.name || existing.pluginId,
|
|
357
|
+
conflictType: 'common-vs-exclusive',
|
|
358
|
+
});
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
return conflicts;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
/**
|
|
366
|
+
* 检查必要权限是否满足
|
|
367
|
+
* 返回未满足的必要权限列表
|
|
368
|
+
*/
|
|
369
|
+
checkRequiredPermissions(manifest: PluginManifest): string[] {
|
|
370
|
+
const required = manifest.requiredPermissions || [];
|
|
371
|
+
const common = manifest.commonPermissions || [];
|
|
372
|
+
const exclusive = manifest.exclusivePermissions || [];
|
|
373
|
+
const allDeclared = [...common, ...exclusive];
|
|
374
|
+
|
|
375
|
+
return required.filter(perm => !allDeclared.includes(perm));
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
/**
|
|
379
|
+
* 注册插件的权限到注册表
|
|
380
|
+
*/
|
|
381
|
+
registerPermissions(manifest: PluginManifest): void {
|
|
382
|
+
const commonPerms = manifest.commonPermissions || [];
|
|
383
|
+
const exclusivePerms = manifest.exclusivePermissions || [];
|
|
384
|
+
|
|
385
|
+
for (const perm of commonPerms) {
|
|
386
|
+
this.state.permissionRegistry.set(perm, { pluginId: manifest.id, type: 'common' });
|
|
387
|
+
}
|
|
388
|
+
for (const perm of exclusivePerms) {
|
|
389
|
+
this.state.permissionRegistry.set(perm, { pluginId: manifest.id, type: 'exclusive' });
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
/**
|
|
394
|
+
* 释放插件的权限(仅释放该插件注册的)
|
|
395
|
+
*/
|
|
396
|
+
unregisterPermissions(pluginId: string): void {
|
|
397
|
+
this.state.permissionRegistry.forEach((entry, perm) => {
|
|
398
|
+
if (entry.pluginId === pluginId) {
|
|
399
|
+
this.state.permissionRegistry.delete(perm);
|
|
400
|
+
}
|
|
401
|
+
});
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
/**
|
|
405
|
+
* 移除插件的单个权限声明
|
|
406
|
+
* @returns true 如果成功移除
|
|
407
|
+
*/
|
|
408
|
+
removePluginPermission(pluginId: string, permission: string): boolean {
|
|
409
|
+
const entry = this.state.permissionRegistry.get(permission);
|
|
410
|
+
if (entry && entry.pluginId === pluginId) {
|
|
411
|
+
this.state.permissionRegistry.delete(permission);
|
|
412
|
+
return true;
|
|
413
|
+
}
|
|
414
|
+
return false;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
/** loadPlugin 返回结果 */
|
|
418
|
+
loadPluginResult = {
|
|
419
|
+
success: true,
|
|
420
|
+
conflicts: [] as Array<{
|
|
421
|
+
permission: string;
|
|
422
|
+
conflictingPluginId: string;
|
|
423
|
+
conflictingPluginName: string;
|
|
424
|
+
conflictType: 'exclusive-vs-any' | 'common-vs-exclusive';
|
|
425
|
+
}>,
|
|
426
|
+
missingRequired: [] as string[],
|
|
427
|
+
} as const;
|
|
428
|
+
|
|
429
|
+
/** 加载并激活插件 */
|
|
430
|
+
// @ts-ignore
|
|
431
|
+
loadPlugin(manifest: PluginManifest, binding: PluginBinding): ReturnType<typeof PluginRuntimeEngine.prototype.loadPlugin> {
|
|
432
|
+
// 防止重复加载:如果插件已加载且配置未变,跳过
|
|
433
|
+
const existing = this.state.instances.get(manifest.id);
|
|
434
|
+
if (existing && existing.enabled === binding.enabled) {
|
|
435
|
+
return { ...this.loadPluginResult };
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
// 如果插件已启用,先检查权限冲突
|
|
439
|
+
if (binding.enabled) {
|
|
440
|
+
const conflicts = this.checkPermissionConflicts(manifest);
|
|
441
|
+
if (conflicts.length > 0) {
|
|
442
|
+
console.warn(`[PluginRuntime] 插件 ${manifest.id} 启动失败:权限冲突`, conflicts);
|
|
443
|
+
// 仍然创建实例(标记为 disabled),但不执行 setup
|
|
444
|
+
const instance: PluginInstance = {
|
|
445
|
+
manifest,
|
|
446
|
+
enabled: false,
|
|
447
|
+
config: binding.config || {},
|
|
448
|
+
scope: binding.scope,
|
|
449
|
+
worldId: binding.worldId,
|
|
450
|
+
};
|
|
451
|
+
if (!this.pluginStorage.has(manifest.id)) {
|
|
452
|
+
this.pluginStorage.set(manifest.id, new Map());
|
|
453
|
+
}
|
|
454
|
+
const exports = this.executePluginCode(manifest.code, manifest.id);
|
|
455
|
+
instance.exports = exports;
|
|
456
|
+
this.state.instances.set(manifest.id, instance);
|
|
457
|
+
return { success: false, conflicts, missingRequired: [] };
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
// 检查必要权限
|
|
461
|
+
const missingRequired = this.checkRequiredPermissions(manifest);
|
|
462
|
+
if (missingRequired.length > 0) {
|
|
463
|
+
console.warn(`[PluginRuntime] 插件 ${manifest.id} 启动失败:必要权限未声明`, missingRequired);
|
|
464
|
+
const instance: PluginInstance = {
|
|
465
|
+
manifest,
|
|
466
|
+
enabled: false,
|
|
467
|
+
config: binding.config || {},
|
|
468
|
+
scope: binding.scope,
|
|
469
|
+
worldId: binding.worldId,
|
|
470
|
+
};
|
|
471
|
+
if (!this.pluginStorage.has(manifest.id)) {
|
|
472
|
+
this.pluginStorage.set(manifest.id, new Map());
|
|
473
|
+
}
|
|
474
|
+
const exports = this.executePluginCode(manifest.code, manifest.id);
|
|
475
|
+
instance.exports = exports;
|
|
476
|
+
this.state.instances.set(manifest.id, instance);
|
|
477
|
+
return { success: false, conflicts: [], missingRequired };
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
try {
|
|
482
|
+
// 创建运行时实例
|
|
483
|
+
const instance: PluginInstance = {
|
|
484
|
+
manifest,
|
|
485
|
+
enabled: binding.enabled,
|
|
486
|
+
config: binding.config || {},
|
|
487
|
+
scope: binding.scope,
|
|
488
|
+
worldId: binding.worldId,
|
|
489
|
+
};
|
|
490
|
+
|
|
491
|
+
// 确保有独立的持久化存储空间
|
|
492
|
+
if (!this.pluginStorage.has(manifest.id)) {
|
|
493
|
+
this.pluginStorage.set(manifest.id, new Map());
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
// 执行插件代码(始终执行,以获取 exports)
|
|
497
|
+
const exports = this.executePluginCode(manifest.code, manifest.id);
|
|
498
|
+
instance.exports = exports;
|
|
499
|
+
|
|
500
|
+
this.state.instances.set(manifest.id, instance);
|
|
501
|
+
|
|
502
|
+
// 注册权限
|
|
503
|
+
if (binding.enabled) {
|
|
504
|
+
this.registerPermissions(manifest);
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
// 检查依赖状态
|
|
508
|
+
const depStatus = this.checkDependencies(manifest);
|
|
509
|
+
this.state.dependencyStatus.set(manifest.id, depStatus);
|
|
510
|
+
|
|
511
|
+
// 判断是否有未满足的必需依赖
|
|
512
|
+
const hasUnmetRequired = depStatus.some(d => d.required && d.status !== 'satisfied');
|
|
513
|
+
|
|
514
|
+
// 如果插件已启用且依赖满足,调用 setup 函数并触发 onLoad 钩子
|
|
515
|
+
if (binding.enabled && !hasUnmetRequired) {
|
|
516
|
+
const xinyu = this.createXinyuBridge(manifest.id);
|
|
517
|
+
if (typeof exports.setup === 'function') {
|
|
518
|
+
exports.setup(xinyu);
|
|
519
|
+
}
|
|
520
|
+
// 只触发当前插件自己的 onLoad(避免在批量加载时重复触发已加载插件的 handler)
|
|
521
|
+
this.triggerHookForPlugin('onLoad', manifest.id, manifest.id);
|
|
522
|
+
// 通知所有插件:有插件加载完成
|
|
523
|
+
this.triggerHook('onPluginLoad', manifest.id);
|
|
524
|
+
} else if (hasUnmetRequired) {
|
|
525
|
+
console.warn(`[PluginRuntime] 插件 ${manifest.id} 有未满足的必需依赖,跳过 setup`);
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
return { ...this.loadPluginResult };
|
|
529
|
+
} catch (e) {
|
|
530
|
+
console.error(`[PluginRuntime] 加载插件 ${manifest.id} 失败:`, e);
|
|
531
|
+
return { success: false, conflicts: [], missingRequired: [] };
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
/** 卸载插件 */
|
|
536
|
+
unloadPlugin(pluginId: string): void {
|
|
537
|
+
const instance = this.state.instances.get(pluginId);
|
|
538
|
+
if (!instance) return;
|
|
539
|
+
|
|
540
|
+
// 触发卸载钩子
|
|
541
|
+
if (instance.enabled) {
|
|
542
|
+
this.triggerHook('onUnload', pluginId);
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
// 清理所有注册
|
|
546
|
+
this.unregisterAllForPlugin(pluginId);
|
|
547
|
+
|
|
548
|
+
// 释放权限
|
|
549
|
+
this.unregisterPermissions(pluginId);
|
|
550
|
+
|
|
551
|
+
// 清理资源追踪器中的所有资源(DOM、样式、事件等)
|
|
552
|
+
this.resourceTracker.cleanup(pluginId);
|
|
553
|
+
|
|
554
|
+
this.state.instances.delete(pluginId);
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
/** 启用插件 */
|
|
558
|
+
enablePlugin(pluginId: string): void {
|
|
559
|
+
const instance = this.state.instances.get(pluginId);
|
|
560
|
+
if (!instance) return;
|
|
561
|
+
instance.enabled = true;
|
|
562
|
+
// 调用 setup 函数注册属性、指令等
|
|
563
|
+
const xinyu = this.createXinyuBridge(pluginId);
|
|
564
|
+
if (instance.exports && typeof instance.exports.setup === 'function') {
|
|
565
|
+
instance.exports.setup(xinyu);
|
|
566
|
+
}
|
|
567
|
+
this.triggerHookForPlugin('onLoad', pluginId, pluginId);
|
|
568
|
+
this.triggerHook('onPluginLoad', pluginId);
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
/** 禁用插件 */
|
|
572
|
+
disablePlugin(pluginId: string): void {
|
|
573
|
+
const instance = this.state.instances.get(pluginId);
|
|
574
|
+
if (!instance) return;
|
|
575
|
+
this.triggerHook('onUnload', pluginId);
|
|
576
|
+
instance.enabled = false;
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
/**
|
|
580
|
+
* 统一配置更新入口(唯一修改配置的方法)
|
|
581
|
+
* 职责:
|
|
582
|
+
* 1. 更新内存中的 instance.config
|
|
583
|
+
* 2. 触发 onConfigChange hook(通知插件代码)
|
|
584
|
+
* 3. 异步持久化到 DB
|
|
585
|
+
* 4. 通过 UI 回调广播配置变更(跨标签页同步)
|
|
586
|
+
*
|
|
587
|
+
* @param options.persist 是否持久化到 DB(默认 true)
|
|
588
|
+
* @param options.broadcast 是否广播变更(默认 true)
|
|
589
|
+
*/
|
|
590
|
+
async updatePluginConfig(
|
|
591
|
+
pluginId: string,
|
|
592
|
+
config: Record<string, unknown>,
|
|
593
|
+
options?: { persist?: boolean; broadcast?: boolean; triggerHook?: boolean },
|
|
594
|
+
): Promise<void> {
|
|
595
|
+
const instance = this.state.instances.get(pluginId);
|
|
596
|
+
if (!instance) return;
|
|
597
|
+
|
|
598
|
+
// 1. 更新内存
|
|
599
|
+
instance.config = config;
|
|
600
|
+
|
|
601
|
+
// 2. 触发 onConfigChange hook(仅通知目标插件自身,handler 签名为 (config))
|
|
602
|
+
const shouldTriggerHook = options?.triggerHook !== false;
|
|
603
|
+
if (shouldTriggerHook) {
|
|
604
|
+
// onConfigChange:只通知 pluginId 自身注册的 handler
|
|
605
|
+
const configRegistrations = this.state.hooks.get('onConfigChange');
|
|
606
|
+
if (configRegistrations) {
|
|
607
|
+
for (const reg of configRegistrations) {
|
|
608
|
+
if (reg.pluginId !== pluginId) continue;
|
|
609
|
+
const inst = this.state.instances.get(reg.pluginId);
|
|
610
|
+
if (!inst || !inst.enabled) continue;
|
|
611
|
+
await this.callSafe(reg.handler, [config], reg.pluginId, 'onConfigChange');
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
// onGlobalConfigChange:通知所有注册了该 hook 的插件
|
|
615
|
+
this.triggerHook('onGlobalConfigChange', pluginId, config);
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
// 3. 异步持久化到 DB
|
|
619
|
+
const shouldPersist = options?.persist !== false;
|
|
620
|
+
if (shouldPersist) {
|
|
621
|
+
this.persistConfig(pluginId, config);
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
// 4. 广播配置变更(跨标签页同步)
|
|
625
|
+
const shouldBroadcast = options?.broadcast !== false;
|
|
626
|
+
if (shouldBroadcast && this.uiCallbacks.onConfigChange) {
|
|
627
|
+
this.uiCallbacks.onConfigChange(pluginId, config);
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
/** 异步持久化配置到 DB(仅负责 DB 写入,不修改内存) */
|
|
632
|
+
private persistConfig(pluginId: string, config: Record<string, unknown>): void {
|
|
633
|
+
const instance = this.state.instances.get(pluginId);
|
|
634
|
+
if (!instance) return;
|
|
635
|
+
const scope = instance.scope || 'global';
|
|
636
|
+
const worldId = instance.worldId || '';
|
|
637
|
+
fetch('/api/plugins/bindings', {
|
|
638
|
+
method: 'POST',
|
|
639
|
+
headers: { 'Content-Type': 'application/json' },
|
|
640
|
+
body: JSON.stringify({
|
|
641
|
+
extensionId: pluginId,
|
|
642
|
+
scope,
|
|
643
|
+
worldId,
|
|
644
|
+
enabled: instance.enabled,
|
|
645
|
+
config,
|
|
646
|
+
}),
|
|
647
|
+
}).catch(e => console.error(`[PluginRuntime] 持久化配置失败:`, e));
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
/** 获取所有已加载的插件实例 */
|
|
651
|
+
getInstances(): PluginInstance[] {
|
|
652
|
+
return Array.from(this.state.instances.values());
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
/** 获取已启用的插件实例 */
|
|
656
|
+
getEnabledInstances(): PluginInstance[] {
|
|
657
|
+
return Array.from(this.state.instances.values()).filter(i => i.enabled);
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
/** 获取插件实例 */
|
|
661
|
+
getInstance(pluginId: string): PluginInstance | undefined {
|
|
662
|
+
return this.state.instances.get(pluginId);
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
// ==================== Hook 系统 ====================
|
|
666
|
+
|
|
667
|
+
/** 注册 Hook */
|
|
668
|
+
registerHook(pluginId: string, hook: PluginHook, handler: HookHandler): void {
|
|
669
|
+
if (!this.state.hooks.has(hook)) {
|
|
670
|
+
this.state.hooks.set(hook, []);
|
|
671
|
+
}
|
|
672
|
+
const arr = this.state.hooks.get(hook)!;
|
|
673
|
+
arr.push({ pluginId, handler });
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
/** 注销 Hook */
|
|
677
|
+
unregisterHook(pluginId: string, hook: PluginHook, handler: HookHandler): void {
|
|
678
|
+
const registrations = this.state.hooks.get(hook);
|
|
679
|
+
if (!registrations) return;
|
|
680
|
+
const idx = registrations.findIndex(r => r.pluginId === pluginId && r.handler === handler);
|
|
681
|
+
if (idx !== -1) {
|
|
682
|
+
registrations.splice(idx, 1);
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
// ==================== 通用 Hook 调用工具 ====================
|
|
687
|
+
|
|
688
|
+
/**
|
|
689
|
+
* 通用安全调用函数:自动判断回调函数类型(async/普通),统一调用
|
|
690
|
+
* - 普通函数:直接调用,返回结果
|
|
691
|
+
* - async 函数:await 后返回结果
|
|
692
|
+
* - 出错时打印日志并返回 undefined(不中断管道)
|
|
693
|
+
*/
|
|
694
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
695
|
+
private async callSafe(handler: (...args: any[]) => any, args: unknown[], pluginId: string, hookName: string): Promise<any> {
|
|
696
|
+
try {
|
|
697
|
+
const result = handler(...args);
|
|
698
|
+
// 判断返回值是否为 Promise(兼容 async 函数和普通函数)
|
|
699
|
+
if (result instanceof Promise) {
|
|
700
|
+
return await result;
|
|
701
|
+
}
|
|
702
|
+
return result;
|
|
703
|
+
} catch (e) {
|
|
704
|
+
console.error(`[PluginRuntime] ${hookName} 执行出错 (插件: ${pluginId}):`, e);
|
|
705
|
+
return undefined;
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
/** 触发 Hook(通知型,不管道返回值,支持 async handler) */
|
|
710
|
+
async triggerHook(hook: PluginHook, ...args: unknown[]): Promise<void> {
|
|
711
|
+
const registrations = this.state.hooks.get(hook);
|
|
712
|
+
if (!registrations) return;
|
|
713
|
+
|
|
714
|
+
for (const reg of registrations) {
|
|
715
|
+
const instance = this.state.instances.get(reg.pluginId);
|
|
716
|
+
if (!instance || !instance.enabled) continue;
|
|
717
|
+
await this.callSafe(reg.handler, args, reg.pluginId, String(hook));
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
/** 触发 Hook(仅限指定插件) */
|
|
722
|
+
private async triggerHookForPlugin(hook: PluginHook, targetPluginId: string, ...args: unknown[]): Promise<void> {
|
|
723
|
+
const registrations = this.state.hooks.get(hook);
|
|
724
|
+
if (!registrations) return;
|
|
725
|
+
|
|
726
|
+
for (const reg of registrations) {
|
|
727
|
+
if (reg.pluginId !== targetPluginId) continue;
|
|
728
|
+
const instance = this.state.instances.get(reg.pluginId);
|
|
729
|
+
if (!instance || !instance.enabled) continue;
|
|
730
|
+
await this.callSafe(reg.handler, args, reg.pluginId, String(hook));
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
/** 管道式 Hook(前一个输出作为后一个输入,支持 async handler) */
|
|
735
|
+
async triggerPipelineHook<T>(hook: PluginHook, initialValue: T): Promise<T> {
|
|
736
|
+
const registrations = this.state.hooks.get(hook);
|
|
737
|
+
if (!registrations) return initialValue;
|
|
738
|
+
|
|
739
|
+
let value = initialValue;
|
|
740
|
+
for (const reg of registrations) {
|
|
741
|
+
const instance = this.state.instances.get(reg.pluginId);
|
|
742
|
+
if (!instance || !instance.enabled) continue;
|
|
743
|
+
const result = await this.callSafe(reg.handler, [value], reg.pluginId, `Pipeline ${hook}`);
|
|
744
|
+
if (result !== undefined) {
|
|
745
|
+
value = result as T;
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
return value;
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
/** 触发游戏初始化钩子(进入游戏且无聊天记录时调用,只触发一次) */
|
|
752
|
+
async triggerGameInit(): Promise<void> {
|
|
753
|
+
if (this.state.gameInitTriggered) return;
|
|
754
|
+
this.state.gameInitTriggered = true;
|
|
755
|
+
await this.triggerHook('onGameInit');
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
/** 重置游戏初始化标记(切换游戏时调用) */
|
|
759
|
+
resetGameInit(): void {
|
|
760
|
+
this.state.gameInitTriggered = false;
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
/** 触发插件绑定世界钩子 */
|
|
764
|
+
async triggerPluginBindingWorld(worldId: string): Promise<void> {
|
|
765
|
+
await this.triggerHook('onPluginBindingWorld', worldId);
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
// ==================== 跨插件通信 ====================
|
|
769
|
+
|
|
770
|
+
/** 跨插件调用(显式导出 + 按需调用,双方声明) */
|
|
771
|
+
async callPlugin(callerPluginId: string, targetPluginId: string, method: string, ...args: unknown[]): Promise<unknown> {
|
|
772
|
+
// 1. 检查调用方是否声明了依赖
|
|
773
|
+
const callerInstance = this.state.instances.get(callerPluginId);
|
|
774
|
+
if (!callerInstance) {
|
|
775
|
+
throw new Error(`[PluginRuntime] 调用方插件 ${callerPluginId} 未加载`);
|
|
776
|
+
}
|
|
777
|
+
const callerDeps = callerInstance.manifest.dependencies || [];
|
|
778
|
+
const hasDeclaredDep = callerDeps.some(d => d.pluginId === targetPluginId);
|
|
779
|
+
if (!hasDeclaredDep) {
|
|
780
|
+
throw new Error(`[PluginRuntime] 插件 ${callerPluginId} 未声明对 ${targetPluginId} 的依赖,请在 manifest.dependencies 中添加`);
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
// 2. 检查目标插件是否存在且已启用
|
|
784
|
+
const targetInstance = this.state.instances.get(targetPluginId);
|
|
785
|
+
if (!targetInstance || !targetInstance.enabled) {
|
|
786
|
+
throw new Error(`[PluginRuntime] 目标插件 ${targetPluginId} 未加载或未启用`);
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
// 3. 检查目标插件是否声明了该 publicExport
|
|
790
|
+
const publicExports = targetInstance.manifest.publicExports || {};
|
|
791
|
+
if (!(method in publicExports)) {
|
|
792
|
+
throw new Error(`[PluginRuntime] 插件 ${targetPluginId} 未公开方法 ${method},请在 manifest.publicExports 中声明`);
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
// 4. 调用目标插件的导出方法
|
|
796
|
+
const targetExports = targetInstance.exports || {};
|
|
797
|
+
const fn = targetExports[method];
|
|
798
|
+
if (typeof fn !== 'function') {
|
|
799
|
+
throw new Error(`[PluginRuntime] 插件 ${targetPluginId} 的导出方法 ${method} 不是函数`);
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
try {
|
|
803
|
+
return await fn(...args);
|
|
804
|
+
} catch (e) {
|
|
805
|
+
console.error(`[PluginRuntime] 跨插件调用出错 (${callerPluginId} -> ${targetPluginId}.${method}):`, e);
|
|
806
|
+
throw e;
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
/** 检查插件是否可用 */
|
|
811
|
+
isPluginAvailable(pluginId: string): boolean {
|
|
812
|
+
const instance = this.state.instances.get(pluginId);
|
|
813
|
+
return !!instance && instance.enabled;
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
/** 获取插件的公开导出 API 列表 */
|
|
817
|
+
getPluginExports(pluginId: string): Record<string, { description: string; params?: string; returns?: string }> | null {
|
|
818
|
+
const instance = this.state.instances.get(pluginId);
|
|
819
|
+
if (!instance || !instance.enabled) return null;
|
|
820
|
+
return instance.manifest.publicExports || null;
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
/** 运行时加载依赖(供插件代码中调用,支持降级) */
|
|
824
|
+
async loadDependency(callerPluginId: string, targetPluginId: string, options?: { version?: string; timeout?: number }): Promise<Record<string, (...args: unknown[]) => unknown> | null> {
|
|
825
|
+
const targetInstance = this.state.instances.get(targetPluginId);
|
|
826
|
+
if (!targetInstance || !targetInstance.enabled) {
|
|
827
|
+
console.warn(`[PluginRuntime] 插件 ${callerPluginId} 加载依赖 ${targetPluginId} 失败:目标插件未加载或未启用`);
|
|
828
|
+
return null;
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
// 版本检查(简单实现:精确匹配或前缀匹配)
|
|
832
|
+
if (options?.version) {
|
|
833
|
+
const installedVersion = targetInstance.manifest.version;
|
|
834
|
+
if (!this.versionSatisfies(installedVersion, options.version)) {
|
|
835
|
+
console.warn(`[PluginRuntime] 插件 ${callerPluginId} 加载依赖 ${targetPluginId} 失败:版本不满足 (需要 ${options.version},已安装 ${installedVersion})`);
|
|
836
|
+
return null;
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
return targetInstance.exports || {};
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
// ==================== 依赖管理 ====================
|
|
844
|
+
|
|
845
|
+
/** 检查插件的依赖状态 */
|
|
846
|
+
checkDependencies(manifest: PluginManifest): DependencyStatus[] {
|
|
847
|
+
const deps = manifest.dependencies || [];
|
|
848
|
+
return deps.map(dep => {
|
|
849
|
+
const targetInstance = this.state.instances.get(dep.pluginId);
|
|
850
|
+
if (!targetInstance) {
|
|
851
|
+
return {
|
|
852
|
+
pluginId: dep.pluginId,
|
|
853
|
+
status: 'missing',
|
|
854
|
+
required: !dep.optional,
|
|
855
|
+
};
|
|
856
|
+
}
|
|
857
|
+
if (dep.versionRange && !this.versionSatisfies(targetInstance.manifest.version, dep.versionRange)) {
|
|
858
|
+
return {
|
|
859
|
+
pluginId: dep.pluginId,
|
|
860
|
+
status: 'version_mismatch',
|
|
861
|
+
required: !dep.optional,
|
|
862
|
+
installedVersion: targetInstance.manifest.version,
|
|
863
|
+
};
|
|
864
|
+
}
|
|
865
|
+
return {
|
|
866
|
+
pluginId: dep.pluginId,
|
|
867
|
+
status: 'satisfied',
|
|
868
|
+
required: !dep.optional,
|
|
869
|
+
installedVersion: targetInstance.manifest.version,
|
|
870
|
+
};
|
|
871
|
+
});
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
/** 获取所有插件的依赖状态 */
|
|
875
|
+
getDependencyStatus(): Map<string, DependencyStatus[]> {
|
|
876
|
+
return new Map(this.state.dependencyStatus);
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
/** 获取指定插件的依赖状态 */
|
|
880
|
+
getPluginDependencyStatus(pluginId: string): DependencyStatus[] {
|
|
881
|
+
return this.state.dependencyStatus.get(pluginId) || [];
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
/** 根据依赖关系计算拓扑排序的加载顺序 */
|
|
885
|
+
getLoadOrder(manifests: PluginManifest[]): PluginManifest[] {
|
|
886
|
+
const manifestMap = new Map(manifests.map(m => [m.id, m]));
|
|
887
|
+
const visited = new Set<string>();
|
|
888
|
+
const visiting = new Set<string>();
|
|
889
|
+
const result: PluginManifest[] = [];
|
|
890
|
+
|
|
891
|
+
const visit = (id: string) => {
|
|
892
|
+
if (visited.has(id)) return;
|
|
893
|
+
if (visiting.has(id)) {
|
|
894
|
+
console.warn(`[PluginRuntime] 检测到循环依赖,涉及插件: ${id}`);
|
|
895
|
+
return;
|
|
896
|
+
}
|
|
897
|
+
visiting.add(id);
|
|
898
|
+
|
|
899
|
+
const manifest = manifestMap.get(id);
|
|
900
|
+
if (manifest?.dependencies) {
|
|
901
|
+
for (const dep of manifest.dependencies) {
|
|
902
|
+
if (manifestMap.has(dep.pluginId)) {
|
|
903
|
+
visit(dep.pluginId);
|
|
904
|
+
}
|
|
905
|
+
}
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
visiting.delete(id);
|
|
909
|
+
visited.add(id);
|
|
910
|
+
if (manifest) {
|
|
911
|
+
result.push(manifest);
|
|
912
|
+
}
|
|
913
|
+
};
|
|
914
|
+
|
|
915
|
+
for (const m of manifests) {
|
|
916
|
+
visit(m.id);
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
return result;
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
/** 简单的版本范围匹配(支持 ^, ~, >=, 精确匹配) */
|
|
923
|
+
private versionSatisfies(installed: string, range: string): boolean {
|
|
924
|
+
const installedParts = installed.split('.').map(Number);
|
|
925
|
+
const rangeStr = range.trim();
|
|
926
|
+
|
|
927
|
+
// 精确匹配
|
|
928
|
+
if (!rangeStr.startsWith('^') && !rangeStr.startsWith('~') && !rangeStr.startsWith('>=')) {
|
|
929
|
+
return installed === rangeStr;
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
const rangeParts = rangeStr.replace(/^[~^>=]+/, '').split('.').map(Number);
|
|
933
|
+
|
|
934
|
+
if (rangeStr.startsWith('>=')) {
|
|
935
|
+
for (let i = 0; i < Math.max(installedParts.length, rangeParts.length); i++) {
|
|
936
|
+
const a = installedParts[i] || 0;
|
|
937
|
+
const b = rangeParts[i] || 0;
|
|
938
|
+
if (a > b) return true;
|
|
939
|
+
if (a < b) return false;
|
|
940
|
+
}
|
|
941
|
+
return true;
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
// ^: 主版本号必须相同,次版本号和补丁号 >= range
|
|
945
|
+
if (rangeStr.startsWith('^')) {
|
|
946
|
+
if (installedParts[0] !== rangeParts[0]) return false;
|
|
947
|
+
for (let i = 1; i < Math.max(installedParts.length, rangeParts.length); i++) {
|
|
948
|
+
const a = installedParts[i] || 0;
|
|
949
|
+
const b = rangeParts[i] || 0;
|
|
950
|
+
if (a > b) return true;
|
|
951
|
+
if (a < b) return false;
|
|
952
|
+
}
|
|
953
|
+
return true;
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
// ~: 主版本号和次版本号必须相同,补丁号 >= range
|
|
957
|
+
if (rangeStr.startsWith('~')) {
|
|
958
|
+
if (installedParts[0] !== rangeParts[0] || installedParts[1] !== rangeParts[1]) return false;
|
|
959
|
+
return (installedParts[2] || 0) >= (rangeParts[2] || 0);
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
return false;
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
// ==================== 游戏属性 ====================
|
|
966
|
+
|
|
967
|
+
/** 注册游戏属性 */
|
|
968
|
+
registerAttribute(pluginId: string, attr: GameAttribute): void {
|
|
969
|
+
const instance = this.state.instances.get(pluginId);
|
|
970
|
+
if (!instance || !instance.enabled) return;
|
|
971
|
+
this.state.gameAttributes.set(attr.key, { ...attr, pluginId });
|
|
972
|
+
}
|
|
973
|
+
|
|
974
|
+
/** 更新属性值 */
|
|
975
|
+
setAttribute(key: string, value: number | string | boolean): void {
|
|
976
|
+
const attr = this.state.gameAttributes.get(key);
|
|
977
|
+
if (attr) {
|
|
978
|
+
attr.value = value;
|
|
979
|
+
}
|
|
980
|
+
}
|
|
981
|
+
|
|
982
|
+
/** 获取所有游戏属性 */
|
|
983
|
+
getAttributes(): GameAttribute[] {
|
|
984
|
+
return Array.from(this.state.gameAttributes.values());
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
// ==================== 游戏状态 ====================
|
|
988
|
+
|
|
989
|
+
/** 获取游戏状态 */
|
|
990
|
+
getGameState(): Record<string, unknown> {
|
|
991
|
+
return { ...this.state.gameState };
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
/** 设置游戏状态(合并更新) */
|
|
995
|
+
setGameState(partial: Record<string, unknown>): void {
|
|
996
|
+
this.state.gameState = { ...this.state.gameState, ...partial };
|
|
997
|
+
const newKeys = Object.keys(partial);
|
|
998
|
+
// 触发宿主事件
|
|
999
|
+
this.emitHostEvent('game:stateChange', { changedKeys: newKeys });
|
|
1000
|
+
}
|
|
1001
|
+
|
|
1002
|
+
/** 重置游戏状态 */
|
|
1003
|
+
resetGameState(): void {
|
|
1004
|
+
this.state.gameState = {};
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
// ==================== 快捷指令 ====================
|
|
1008
|
+
|
|
1009
|
+
/** 注册快捷指令 */
|
|
1010
|
+
registerCommand(pluginId: string, cmd: ChatCommand): void {
|
|
1011
|
+
const instance = this.state.instances.get(pluginId);
|
|
1012
|
+
if (!instance || !instance.enabled) return;
|
|
1013
|
+
this.state.chatCommands.set(cmd.name, { ...cmd, pluginId });
|
|
1014
|
+
}
|
|
1015
|
+
|
|
1016
|
+
/** 获取所有快捷指令 */
|
|
1017
|
+
getCommands(): ChatCommand[] {
|
|
1018
|
+
return Array.from(this.state.chatCommands.values());
|
|
1019
|
+
}
|
|
1020
|
+
|
|
1021
|
+
/**
|
|
1022
|
+
* 执行快捷指令
|
|
1023
|
+
*
|
|
1024
|
+
* 支持三种格式:
|
|
1025
|
+
* 1. `/command args` → 精确匹配命令名,执行对应插件的处理函数
|
|
1026
|
+
* 2. `pluginId/command args` → 指定目标插件,仅执行该插件注册的命令
|
|
1027
|
+
* 3. `/command args`(无精确匹配)→ 广播给所有注册了同名命令的插件
|
|
1028
|
+
*
|
|
1029
|
+
* 返回值:
|
|
1030
|
+
* - string → 作为用户消息显示
|
|
1031
|
+
* - 'handled' → 命令已处理(静默),不发送到 AI
|
|
1032
|
+
* - undefined → 未匹配到任何命令,继续作为普通消息发送
|
|
1033
|
+
*/
|
|
1034
|
+
async executeCommand(rawInput: string): Promise<string | void> {
|
|
1035
|
+
const trimmed = rawInput.trim();
|
|
1036
|
+
if (!trimmed.startsWith('/')) return undefined;
|
|
1037
|
+
|
|
1038
|
+
// 解析:去掉开头的 /,提取命令名和参数
|
|
1039
|
+
const withoutSlash = trimmed.slice(1);
|
|
1040
|
+
const spaceIdx = withoutSlash.indexOf(' ');
|
|
1041
|
+
const cmdPart = spaceIdx === -1 ? withoutSlash : withoutSlash.slice(0, spaceIdx);
|
|
1042
|
+
const args = spaceIdx === -1 ? '' : withoutSlash.slice(spaceIdx + 1).trim();
|
|
1043
|
+
|
|
1044
|
+
// 格式 2:pluginId/command — 指定目标插件
|
|
1045
|
+
const slashIdx = cmdPart.indexOf('/');
|
|
1046
|
+
if (slashIdx !== -1) {
|
|
1047
|
+
const targetPluginId = cmdPart.slice(0, slashIdx);
|
|
1048
|
+
const targetCmdName = '/' + cmdPart.slice(slashIdx + 1);
|
|
1049
|
+
// 尝试带 / 和不带 / 两种 key(兼容插件注册时 name 是否带 /)
|
|
1050
|
+
const cmd = this.state.chatCommands.get(targetCmdName) || this.state.chatCommands.get(cmdPart.slice(slashIdx + 1));
|
|
1051
|
+
if (cmd && cmd.pluginId === targetPluginId) {
|
|
1052
|
+
try {
|
|
1053
|
+
return await cmd.handler(args);
|
|
1054
|
+
} catch (e) {
|
|
1055
|
+
console.error(`[PluginRuntime] 指令 ${targetCmdName} 执行出错:`, e);
|
|
1056
|
+
return `指令执行出错: ${targetCmdName}`;
|
|
1057
|
+
}
|
|
1058
|
+
}
|
|
1059
|
+
// 指定了插件但未找到命令
|
|
1060
|
+
return `未找到命令: ${targetCmdName}(插件 ${targetPluginId})`;
|
|
1061
|
+
}
|
|
1062
|
+
|
|
1063
|
+
// 格式 1 & 3:/command — 精确匹配或广播
|
|
1064
|
+
const cmdName = '/' + cmdPart;
|
|
1065
|
+
const cmd = this.state.chatCommands.get(cmdName) || this.state.chatCommands.get(cmdPart);
|
|
1066
|
+
|
|
1067
|
+
if (cmd) {
|
|
1068
|
+
// 精确匹配(只有一个插件注册了该命令)
|
|
1069
|
+
try {
|
|
1070
|
+
return await cmd.handler(args);
|
|
1071
|
+
} catch (e) {
|
|
1072
|
+
console.error(`[PluginRuntime] 指令 ${cmdName} 执行出错:`, e);
|
|
1073
|
+
return `指令执行出错: ${cmdName}`;
|
|
1074
|
+
}
|
|
1075
|
+
}
|
|
1076
|
+
|
|
1077
|
+
// 检查是否有任何插件注册了同名命令(广播模式)
|
|
1078
|
+
// 注意:chatCommands Map 中同名命令只保留最后一个注册的,
|
|
1079
|
+
// 所以需要遍历所有实例的注册命令
|
|
1080
|
+
const allCommands = this.getCommands();
|
|
1081
|
+
const matched = allCommands.filter(c => c.name === cmdName);
|
|
1082
|
+
if (matched.length === 0) {
|
|
1083
|
+
return undefined; // 无匹配,继续作为普通消息
|
|
1084
|
+
}
|
|
1085
|
+
|
|
1086
|
+
// 广播给所有注册了该命令的插件
|
|
1087
|
+
let lastResult: string | void = undefined;
|
|
1088
|
+
for (const mcmd of matched) {
|
|
1089
|
+
try {
|
|
1090
|
+
const result = await mcmd.handler(args);
|
|
1091
|
+
if (result !== undefined && result !== '') lastResult = result;
|
|
1092
|
+
} catch (e) {
|
|
1093
|
+
console.error(`[PluginRuntime] 指令 ${cmdName} (插件 ${mcmd.pluginId}) 执行出错:`, e);
|
|
1094
|
+
}
|
|
1095
|
+
}
|
|
1096
|
+
return lastResult !== undefined ? lastResult : 'handled';
|
|
1097
|
+
}
|
|
1098
|
+
|
|
1099
|
+
// ==================== UI 注册 ====================
|
|
1100
|
+
|
|
1101
|
+
/** 注册侧边栏面板 */
|
|
1102
|
+
registerSidebarPanel(pluginId: string, panel: SidebarPanelRegistration): void {
|
|
1103
|
+
const instance = this.state.instances.get(pluginId);
|
|
1104
|
+
if (!instance || !instance.enabled) return;
|
|
1105
|
+
const panelWithId = { ...panel, pluginId };
|
|
1106
|
+
const existingIdx = this.state.sidebarPanels.findIndex(p => p.id === panel.id);
|
|
1107
|
+
if (existingIdx >= 0) {
|
|
1108
|
+
this.state.sidebarPanels[existingIdx] = panelWithId;
|
|
1109
|
+
} else {
|
|
1110
|
+
this.state.sidebarPanels.push(panelWithId);
|
|
1111
|
+
}
|
|
1112
|
+
this.state.sidebarPanels.sort((a, b) => (a.priority ?? 100) - (b.priority ?? 100));
|
|
1113
|
+
}
|
|
1114
|
+
|
|
1115
|
+
/** 获取所有侧边栏面板 */
|
|
1116
|
+
getSidebarPanels(): SidebarPanelRegistration[] {
|
|
1117
|
+
return [...this.state.sidebarPanels];
|
|
1118
|
+
}
|
|
1119
|
+
|
|
1120
|
+
/** 注册工具栏按钮 */
|
|
1121
|
+
registerToolbarButton(pluginId: string, button: ToolbarButtonRegistration): void {
|
|
1122
|
+
const instance = this.state.instances.get(pluginId);
|
|
1123
|
+
if (!instance || !instance.enabled) return;
|
|
1124
|
+
|
|
1125
|
+
// 追加按钮到列表(同 id 覆盖)
|
|
1126
|
+
let buttons = this.state.toolbarButtons.get(pluginId) || [];
|
|
1127
|
+
buttons = buttons.filter(b => b.id !== button.id);
|
|
1128
|
+
buttons.push(button);
|
|
1129
|
+
buttons.sort((a, b) => (a.order ?? 100) - (b.order ?? 100));
|
|
1130
|
+
this.state.toolbarButtons.set(pluginId, buttons);
|
|
1131
|
+
|
|
1132
|
+
// 注册 action handler
|
|
1133
|
+
const actionName = `toolbar-btn-${button.id}`;
|
|
1134
|
+
this.registerActionHandler(pluginId, actionName, () => {
|
|
1135
|
+
try {
|
|
1136
|
+
button.onClick();
|
|
1137
|
+
} catch (e) {
|
|
1138
|
+
console.error(`[PluginRuntime] toolbar button onClick error (${pluginId}:${button.id}):`, e);
|
|
1139
|
+
}
|
|
1140
|
+
});
|
|
1141
|
+
|
|
1142
|
+
// 重新渲染所有按钮到 input-toolbar 插槽
|
|
1143
|
+
const allHtml = buttons.map(b => {
|
|
1144
|
+
const aName = `toolbar-btn-${b.id}`;
|
|
1145
|
+
return `<button data-action="${aName}" title="${b.label}" style="display:inline-flex;align-items:center;gap:4px;padding:4px 10px;border-radius:6px;font-size:12px;background:var(--color-bg-tertiary);color:var(--color-text-secondary);border:none;cursor:pointer;white-space:nowrap;">${b.icon ? `<span>${b.icon}</span>` : ''}${b.label}</button>`;
|
|
1146
|
+
}).join('');
|
|
1147
|
+
|
|
1148
|
+
if (allHtml) {
|
|
1149
|
+
this.registerSlot(pluginId, 'input-toolbar', allHtml, { key: 'toolbar-buttons' });
|
|
1150
|
+
} else {
|
|
1151
|
+
// 没有按钮时注销插槽
|
|
1152
|
+
const existing = this.state.slotRegistrations.find(r => r.pluginId === pluginId && r.slotId === 'input-toolbar' && r.id === `${pluginId}_key_toolbar-buttons`);
|
|
1153
|
+
if (existing) {
|
|
1154
|
+
this.unregisterSlot(existing.id);
|
|
1155
|
+
}
|
|
1156
|
+
}
|
|
1157
|
+
}
|
|
1158
|
+
|
|
1159
|
+
/** 注册消息渲染器 */
|
|
1160
|
+
registerMessageRenderer(pluginId: string, renderer: MessageRenderer): void {
|
|
1161
|
+
const instance = this.state.instances.get(pluginId);
|
|
1162
|
+
if (!instance || !instance.enabled) return;
|
|
1163
|
+
this.state.messageRenderers.push({ ...renderer, pluginId });
|
|
1164
|
+
}
|
|
1165
|
+
|
|
1166
|
+
/** 获取所有消息渲染器 */
|
|
1167
|
+
getMessageRenderers(): MessageRenderer[] {
|
|
1168
|
+
return [...this.state.messageRenderers];
|
|
1169
|
+
}
|
|
1170
|
+
|
|
1171
|
+
/**
|
|
1172
|
+
* 执行 onRenderMessage 管道:多个插件依次修改消息内容
|
|
1173
|
+
* 每个 handler 接收当前 content,返回修改后的 content
|
|
1174
|
+
* 返回 null 表示跳过该消息的渲染(不显示)
|
|
1175
|
+
* 返回经过所有 handler 链式处理后的最终 content
|
|
1176
|
+
*/
|
|
1177
|
+
async applyRenderMessagePipeline(message: { role: string; content: string }): Promise<string | null> {
|
|
1178
|
+
const registrations = this.state.hooks.get('onRenderMessage');
|
|
1179
|
+
if (!registrations) return message.content;
|
|
1180
|
+
|
|
1181
|
+
let content: string | null = message.content;
|
|
1182
|
+
for (const reg of registrations) {
|
|
1183
|
+
const instance = this.state.instances.get(reg.pluginId);
|
|
1184
|
+
if (!instance || !instance.enabled) continue;
|
|
1185
|
+
if (content === null) break; // 已被上游标记为跳过,终止管道
|
|
1186
|
+
const result = await this.callSafe(reg.handler, [{ ...message, content }], reg.pluginId, 'onRenderMessage');
|
|
1187
|
+
if (result === null) {
|
|
1188
|
+
content = null; // 标记为跳过渲染
|
|
1189
|
+
break; // 终止管道
|
|
1190
|
+
}
|
|
1191
|
+
if (typeof result === 'string') {
|
|
1192
|
+
content = result;
|
|
1193
|
+
}
|
|
1194
|
+
}
|
|
1195
|
+
return content;
|
|
1196
|
+
}
|
|
1197
|
+
|
|
1198
|
+
// ==================== 全局 UI API:插槽注册 ====================
|
|
1199
|
+
|
|
1200
|
+
/** 注册插槽内容 */
|
|
1201
|
+
registerSlot(pluginId: string, slotId: UISlotId, content: string, options?: { priority?: number; persistent?: boolean; key?: string }): string | null {
|
|
1202
|
+
const instance = this.state.instances.get(pluginId);
|
|
1203
|
+
if (!instance || !instance.enabled) return null;
|
|
1204
|
+
|
|
1205
|
+
// 权限检查
|
|
1206
|
+
if (!checkSlotPermission(instance.manifest, slotId)) {
|
|
1207
|
+
console.warn(`[PluginRuntime] 插件 ${pluginId} 没有 slot:${slotId} 权限`);
|
|
1208
|
+
return null;
|
|
1209
|
+
}
|
|
1210
|
+
|
|
1211
|
+
const sanitizedContent = sanitizeHTML(content);
|
|
1212
|
+
|
|
1213
|
+
// 按 pluginId + slotId + key 查找(key 用于区分同一插件在同一插槽中的多个注册)
|
|
1214
|
+
const matchKey = options?.key;
|
|
1215
|
+
const existing = this.state.slotRegistrations.find(r =>
|
|
1216
|
+
r.pluginId === pluginId && r.slotId === slotId && (matchKey ? r.id === `${pluginId}_key_${matchKey}` : !r.id.startsWith(`${pluginId}_key_`))
|
|
1217
|
+
);
|
|
1218
|
+
let id: string;
|
|
1219
|
+
if (existing) {
|
|
1220
|
+
id = existing.id;
|
|
1221
|
+
existing.content = sanitizedContent;
|
|
1222
|
+
existing.priority = options?.priority;
|
|
1223
|
+
existing.persistent = options?.persistent;
|
|
1224
|
+
} else {
|
|
1225
|
+
id = matchKey ? `${pluginId}_key_${matchKey}` : `${pluginId}_slot_${Date.now()}`;
|
|
1226
|
+
const registration: SlotRegistration = {
|
|
1227
|
+
id,
|
|
1228
|
+
pluginId,
|
|
1229
|
+
slotId,
|
|
1230
|
+
content: sanitizedContent,
|
|
1231
|
+
priority: options?.priority,
|
|
1232
|
+
persistent: options?.persistent,
|
|
1233
|
+
};
|
|
1234
|
+
this.state.slotRegistrations.push(registration);
|
|
1235
|
+
this.resourceTracker.trackSlotRegistration(pluginId, id);
|
|
1236
|
+
}
|
|
1237
|
+
|
|
1238
|
+
this.state.slotRegistrations.sort((a, b) => (a.priority ?? 100) - (b.priority ?? 100));
|
|
1239
|
+
this.uiCallbacks.onSlotChange?.();
|
|
1240
|
+
return id;
|
|
1241
|
+
}
|
|
1242
|
+
|
|
1243
|
+
/** 更新已注册的插槽内容(语义化别名,等同于 registerSlot 更新) */
|
|
1244
|
+
updateSlot(registrationId: string, content: string, options?: { priority?: number }): boolean {
|
|
1245
|
+
const existing = this.state.slotRegistrations.find(r => r.id === registrationId);
|
|
1246
|
+
if (!existing) return false;
|
|
1247
|
+
existing.content = sanitizeHTML(content);
|
|
1248
|
+
if (options?.priority !== undefined) existing.priority = options.priority;
|
|
1249
|
+
this.state.slotRegistrations.sort((a, b) => (a.priority ?? 100) - (b.priority ?? 100));
|
|
1250
|
+
this.uiCallbacks.onSlotChange?.();
|
|
1251
|
+
return true;
|
|
1252
|
+
}
|
|
1253
|
+
|
|
1254
|
+
/** 移除插槽内容 */
|
|
1255
|
+
unregisterSlot(registrationId: string): void {
|
|
1256
|
+
this.state.slotRegistrations = this.state.slotRegistrations.filter(r => r.id !== registrationId);
|
|
1257
|
+
this.uiCallbacks.onSlotChange?.();
|
|
1258
|
+
}
|
|
1259
|
+
|
|
1260
|
+
/** 获取指定插槽的所有注册 */
|
|
1261
|
+
getSlotRegistrations(slotId: UISlotId): SlotRegistration[] {
|
|
1262
|
+
return this.state.slotRegistrations.filter(r => r.slotId === slotId);
|
|
1263
|
+
}
|
|
1264
|
+
|
|
1265
|
+
/** 注册 action handler(用于 data-action 事件委托) */
|
|
1266
|
+
registerActionHandler(pluginId: string, actionName: string, handler: (e: { target: HTMLElement; actionName: string; payload?: string }) => void): void {
|
|
1267
|
+
const instance = this.state.instances.get(pluginId);
|
|
1268
|
+
if (!instance || !instance.enabled) return;
|
|
1269
|
+
// 同 pluginId + actionName 时覆盖
|
|
1270
|
+
const idx = this.state.actionHandlers.findIndex(h => h.pluginId === pluginId && h.actionName === actionName);
|
|
1271
|
+
if (idx >= 0) {
|
|
1272
|
+
this.state.actionHandlers[idx].handler = handler;
|
|
1273
|
+
} else {
|
|
1274
|
+
this.state.actionHandlers.push({ pluginId, actionName, handler });
|
|
1275
|
+
}
|
|
1276
|
+
}
|
|
1277
|
+
|
|
1278
|
+
/** 触发 action handler(由 PluginSlotRenderer 事件委托调用) */
|
|
1279
|
+
dispatchAction(pluginId: string, actionName: string, target: HTMLElement, payload?: string): void {
|
|
1280
|
+
const entry = this.state.actionHandlers.find(h => h.pluginId === pluginId && h.actionName === actionName);
|
|
1281
|
+
if (entry) {
|
|
1282
|
+
try {
|
|
1283
|
+
entry.handler({ target, actionName, payload });
|
|
1284
|
+
} catch (e) {
|
|
1285
|
+
console.error(`[PluginRuntime] action handler 执行出错 (${pluginId}:${actionName}):`, e);
|
|
1286
|
+
}
|
|
1287
|
+
}
|
|
1288
|
+
}
|
|
1289
|
+
|
|
1290
|
+
/** 获取所有插槽注册 */
|
|
1291
|
+
getAllSlotRegistrations(): SlotRegistration[] {
|
|
1292
|
+
return [...this.state.slotRegistrations];
|
|
1293
|
+
}
|
|
1294
|
+
|
|
1295
|
+
// ==================== 全局 UI API:模态框 ====================
|
|
1296
|
+
|
|
1297
|
+
/** 显示模态框 */
|
|
1298
|
+
showModal(pluginId: string, options: { title: string; content: string; width?: string; style?: Record<string, unknown>; closable?: boolean; backdrop?: boolean; onClose?: () => void; actions?: ModalActionInput[] }): string | null {
|
|
1299
|
+
const instance = this.state.instances.get(pluginId);
|
|
1300
|
+
if (!instance || !instance.enabled) return null;
|
|
1301
|
+
|
|
1302
|
+
if (!checkModalPermission(instance.manifest)) {
|
|
1303
|
+
console.warn(`[PluginRuntime] 插件 ${pluginId} 没有模态框权限`);
|
|
1304
|
+
return null;
|
|
1305
|
+
}
|
|
1306
|
+
|
|
1307
|
+
const id = `${pluginId}_modal_${Date.now()}`;
|
|
1308
|
+
const wrappedActions = options.actions?.map(action => ({
|
|
1309
|
+
...action,
|
|
1310
|
+
onClick: () => action.onClick({ modalId: id, close: () => this.closeModal(id) }),
|
|
1311
|
+
}));
|
|
1312
|
+
const modal: ModalRegistration = { id, pluginId, ...options, actions: wrappedActions };
|
|
1313
|
+
this.state.modals.set(id, modal);
|
|
1314
|
+
this.resourceTracker.trackModal(pluginId, id);
|
|
1315
|
+
|
|
1316
|
+
if (this.uiCallbacks.showModal) {
|
|
1317
|
+
this.uiCallbacks.showModal(modal);
|
|
1318
|
+
}
|
|
1319
|
+
|
|
1320
|
+
return id;
|
|
1321
|
+
}
|
|
1322
|
+
|
|
1323
|
+
/** 更新模态框内容/样式(不关闭重开,避免闪烁) */
|
|
1324
|
+
updateModal(modalId: string, options: { title?: string; content?: string; width?: string; style?: Record<string, unknown> }): void {
|
|
1325
|
+
const modal = this.state.modals.get(modalId);
|
|
1326
|
+
if (!modal) return;
|
|
1327
|
+
|
|
1328
|
+
if (options.title !== undefined) modal.title = options.title;
|
|
1329
|
+
if (options.content !== undefined) modal.content = options.content;
|
|
1330
|
+
if (options.width !== undefined) modal.width = options.width;
|
|
1331
|
+
if (options.style !== undefined) modal.style = options.style as React.CSSProperties;
|
|
1332
|
+
|
|
1333
|
+
if (this.uiCallbacks.updateModal) {
|
|
1334
|
+
this.uiCallbacks.updateModal(modalId, modal as any);
|
|
1335
|
+
}
|
|
1336
|
+
}
|
|
1337
|
+
|
|
1338
|
+
/** 关闭模态框 */
|
|
1339
|
+
closeModal(modalId: string): void {
|
|
1340
|
+
const modal = this.state.modals.get(modalId);
|
|
1341
|
+
if (modal) {
|
|
1342
|
+
if (modal.onClose) {
|
|
1343
|
+
try {
|
|
1344
|
+
modal.onClose();
|
|
1345
|
+
} catch (e) {
|
|
1346
|
+
console.error(`[PluginRuntime] 模态框关闭回调出错:`, e);
|
|
1347
|
+
}
|
|
1348
|
+
}
|
|
1349
|
+
this.state.modals.delete(modalId);
|
|
1350
|
+
if (this.uiCallbacks.closeModal) {
|
|
1351
|
+
this.uiCallbacks.closeModal(modalId);
|
|
1352
|
+
}
|
|
1353
|
+
}
|
|
1354
|
+
}
|
|
1355
|
+
|
|
1356
|
+
/** 获取所有模态框 */
|
|
1357
|
+
getModals(): ModalRegistration[] {
|
|
1358
|
+
return Array.from(this.state.modals.values());
|
|
1359
|
+
}
|
|
1360
|
+
|
|
1361
|
+
/** 确认对话框 */
|
|
1362
|
+
async confirm(pluginId: string, title: string, message: string, options?: ConfirmOptions): Promise<boolean> {
|
|
1363
|
+
const instance = this.state.instances.get(pluginId);
|
|
1364
|
+
if (!instance) return false;
|
|
1365
|
+
|
|
1366
|
+
if (!checkModalPermission(instance.manifest)) {
|
|
1367
|
+
console.warn(`[PluginRuntime] 插件 ${pluginId} 没有确认对话框权限`);
|
|
1368
|
+
return false;
|
|
1369
|
+
}
|
|
1370
|
+
|
|
1371
|
+
if (this.uiCallbacks.confirm) {
|
|
1372
|
+
return this.uiCallbacks.confirm(title, message, options);
|
|
1373
|
+
}
|
|
1374
|
+
return false;
|
|
1375
|
+
}
|
|
1376
|
+
|
|
1377
|
+
// ==================== 全局 UI API:样式注入 ====================
|
|
1378
|
+
|
|
1379
|
+
/** 注入 CSS 样式 */
|
|
1380
|
+
injectStyle(pluginId: string, css: string): string | null {
|
|
1381
|
+
const instance = this.state.instances.get(pluginId);
|
|
1382
|
+
if (!instance) return null;
|
|
1383
|
+
|
|
1384
|
+
if (!checkStylePermission(instance.manifest)) {
|
|
1385
|
+
console.warn(`[PluginRuntime] 插件 ${pluginId} 没有样式注入权限`);
|
|
1386
|
+
return null;
|
|
1387
|
+
}
|
|
1388
|
+
|
|
1389
|
+
try {
|
|
1390
|
+
const styleId = `${pluginId}_style_${Date.now()}`;
|
|
1391
|
+
const styleElement = document.createElement('style');
|
|
1392
|
+
styleElement.setAttribute('data-plugin-style-id', styleId);
|
|
1393
|
+
styleElement.setAttribute('data-plugin-id', pluginId);
|
|
1394
|
+
styleElement.textContent = css;
|
|
1395
|
+
document.head.appendChild(styleElement);
|
|
1396
|
+
|
|
1397
|
+
this.resourceTracker.trackStyle(pluginId, styleId, styleElement);
|
|
1398
|
+
return styleId;
|
|
1399
|
+
} catch (e) {
|
|
1400
|
+
console.error(`[PluginRuntime] 插件 ${pluginId} 注入样式失败:`, e);
|
|
1401
|
+
return null;
|
|
1402
|
+
}
|
|
1403
|
+
}
|
|
1404
|
+
|
|
1405
|
+
/** 移除 CSS 样式 */
|
|
1406
|
+
removeStyle(pluginId: string, styleId: string): void {
|
|
1407
|
+
const res = this.resourceTracker.get(pluginId);
|
|
1408
|
+
if (!res) return;
|
|
1409
|
+
|
|
1410
|
+
const styleElement = res.styleTags.get(styleId);
|
|
1411
|
+
if (styleElement) {
|
|
1412
|
+
try {
|
|
1413
|
+
if (styleElement.parentNode) {
|
|
1414
|
+
styleElement.parentNode.removeChild(styleElement);
|
|
1415
|
+
}
|
|
1416
|
+
} catch (e) {
|
|
1417
|
+
console.error(`[PluginRuntime] 移除样式失败:`, e);
|
|
1418
|
+
}
|
|
1419
|
+
this.resourceTracker.untrackStyle(pluginId, styleId);
|
|
1420
|
+
}
|
|
1421
|
+
}
|
|
1422
|
+
|
|
1423
|
+
// ==================== 全局 UI API:宿主事件 ====================
|
|
1424
|
+
|
|
1425
|
+
/** 监听宿主事件 */
|
|
1426
|
+
onHostEvent(pluginId: string, eventName: string, handler: HookHandler): void {
|
|
1427
|
+
const instance = this.state.instances.get(pluginId);
|
|
1428
|
+
if (!instance) return;
|
|
1429
|
+
|
|
1430
|
+
if (!checkHostEventPermission(instance.manifest)) {
|
|
1431
|
+
console.warn(`[PluginRuntime] 插件 ${pluginId} 没有宿主事件监听权限`);
|
|
1432
|
+
return;
|
|
1433
|
+
}
|
|
1434
|
+
|
|
1435
|
+
this.state.hostEventHandlers.push({ pluginId, eventName, handler });
|
|
1436
|
+
this.resourceTracker.trackHostEventHandler(pluginId, eventName, handler);
|
|
1437
|
+
}
|
|
1438
|
+
|
|
1439
|
+
/** 取消监听宿主事件 */
|
|
1440
|
+
offHostEvent(pluginId: string, eventName: string, handler: HookHandler): void {
|
|
1441
|
+
this.state.hostEventHandlers = this.state.hostEventHandlers.filter(
|
|
1442
|
+
h => !(h.pluginId === pluginId && h.eventName === eventName && h.handler === handler)
|
|
1443
|
+
);
|
|
1444
|
+
}
|
|
1445
|
+
|
|
1446
|
+
/** 触发宿主事件(供 React 端调用) */
|
|
1447
|
+
emitHostEvent(eventName: string, data?: unknown): void {
|
|
1448
|
+
for (const reg of this.state.hostEventHandlers) {
|
|
1449
|
+
const instance = this.state.instances.get(reg.pluginId);
|
|
1450
|
+
if (!instance || !instance.enabled) continue;
|
|
1451
|
+
if (reg.eventName !== eventName) continue;
|
|
1452
|
+
try {
|
|
1453
|
+
reg.handler(data);
|
|
1454
|
+
} catch (e) {
|
|
1455
|
+
console.error(`[PluginRuntime] 宿主事件 ${eventName} 处理出错 (插件: ${reg.pluginId}):`, e);
|
|
1456
|
+
}
|
|
1457
|
+
}
|
|
1458
|
+
}
|
|
1459
|
+
|
|
1460
|
+
// ==================== 全局 UI API:DOM 沙箱 ====================
|
|
1461
|
+
|
|
1462
|
+
/** 创建 DOM 沙箱 API */
|
|
1463
|
+
createDOMSandboxAPI(pluginId: string) {
|
|
1464
|
+
const instance = this.state.instances.get(pluginId);
|
|
1465
|
+
if (!instance) {
|
|
1466
|
+
return {
|
|
1467
|
+
create: () => { throw new Error('插件未加载'); },
|
|
1468
|
+
append: () => null,
|
|
1469
|
+
remove: () => false,
|
|
1470
|
+
update: () => false,
|
|
1471
|
+
getContainer: () => null,
|
|
1472
|
+
query: () => [],
|
|
1473
|
+
on: () => false,
|
|
1474
|
+
off: () => false,
|
|
1475
|
+
};
|
|
1476
|
+
}
|
|
1477
|
+
|
|
1478
|
+
if (!checkDOMPermission(instance.manifest)) {
|
|
1479
|
+
console.warn(`[PluginRuntime] 插件 ${pluginId} 没有自由 DOM 操作权限`);
|
|
1480
|
+
return {
|
|
1481
|
+
create: () => { throw new Error('插件没有自由 DOM 操作权限 (dom:free)'); },
|
|
1482
|
+
append: () => null,
|
|
1483
|
+
remove: () => false,
|
|
1484
|
+
update: () => false,
|
|
1485
|
+
getContainer: () => null,
|
|
1486
|
+
query: () => [],
|
|
1487
|
+
on: () => false,
|
|
1488
|
+
off: () => false,
|
|
1489
|
+
};
|
|
1490
|
+
}
|
|
1491
|
+
|
|
1492
|
+
return createDOMSandbox({
|
|
1493
|
+
pluginId,
|
|
1494
|
+
resourceTracker: this.resourceTracker,
|
|
1495
|
+
getContainerElement: (slotId: UISlotId) => {
|
|
1496
|
+
if (this.getSlotContainerCallback) {
|
|
1497
|
+
return this.getSlotContainerCallback(slotId);
|
|
1498
|
+
}
|
|
1499
|
+
return document.querySelector(`[data-plugin-slot="${slotId}"]`);
|
|
1500
|
+
},
|
|
1501
|
+
});
|
|
1502
|
+
}
|
|
1503
|
+
|
|
1504
|
+
// ==================== AI 拦截 ====================
|
|
1505
|
+
|
|
1506
|
+
/** 注册 prompt 修改器 */
|
|
1507
|
+
registerPromptModifier(pluginId: string, handler: (prompt: string, worldSetting: Record<string, unknown>) => string): void {
|
|
1508
|
+
const instance = this.state.instances.get(pluginId);
|
|
1509
|
+
if (!instance || !instance.enabled) return;
|
|
1510
|
+
this.state.promptModifiers.push({ pluginId, handler });
|
|
1511
|
+
}
|
|
1512
|
+
|
|
1513
|
+
/** 应用所有 prompt 修改器 */
|
|
1514
|
+
async applyPromptModifiers(prompt: string, worldSetting: Record<string, unknown>): Promise<string> {
|
|
1515
|
+
let result = prompt;
|
|
1516
|
+
for (const { handler, pluginId } of this.state.promptModifiers) {
|
|
1517
|
+
const value = await this.callSafe(handler, [result, worldSetting], pluginId, 'Prompt 修改器');
|
|
1518
|
+
if (typeof value === 'string') result = value;
|
|
1519
|
+
}
|
|
1520
|
+
return result;
|
|
1521
|
+
}
|
|
1522
|
+
|
|
1523
|
+
/** 注册消息预处理 */
|
|
1524
|
+
registerBeforeSendHandler(pluginId: string, handler: (messages: { role: string; content: string }[]) => { role: string; content: string }[]): void {
|
|
1525
|
+
const instance = this.state.instances.get(pluginId);
|
|
1526
|
+
if (!instance || !instance.enabled) return;
|
|
1527
|
+
this.state.beforeSendHandlers.push({ pluginId, handler });
|
|
1528
|
+
}
|
|
1529
|
+
|
|
1530
|
+
/** 应用所有消息预处理 */
|
|
1531
|
+
async applyBeforeSendHandlers(messages: { role: string; content: string }[]): Promise<{ role: string; content: string }[]> {
|
|
1532
|
+
let result = messages;
|
|
1533
|
+
for (const { handler, pluginId } of this.state.beforeSendHandlers) {
|
|
1534
|
+
const value = await this.callSafe(handler, [result], pluginId, '消息预处理');
|
|
1535
|
+
if (Array.isArray(value)) result = value;
|
|
1536
|
+
}
|
|
1537
|
+
return result;
|
|
1538
|
+
}
|
|
1539
|
+
|
|
1540
|
+
/** 注册完整消息数组预处理(system + user/assistant 合并后) */
|
|
1541
|
+
registerBeforeCompleteHandler(pluginId: string, handler: (messages: { role: string; content: string }[]) => { role: string; content: string }[]): void {
|
|
1542
|
+
const instance = this.state.instances.get(pluginId);
|
|
1543
|
+
if (!instance || !instance.enabled) return;
|
|
1544
|
+
this.state.beforeCompleteHandlers.push({ pluginId, handler });
|
|
1545
|
+
}
|
|
1546
|
+
|
|
1547
|
+
/** 应用所有完整消息数组预处理 */
|
|
1548
|
+
async applyBeforeCompleteHandlers(messages: { role: string; content: string }[]): Promise<{ role: string; content: string }[]> {
|
|
1549
|
+
let result = messages;
|
|
1550
|
+
for (const { handler, pluginId } of this.state.beforeCompleteHandlers) {
|
|
1551
|
+
const value = await this.callSafe(handler, [result], pluginId, '完整消息预处理');
|
|
1552
|
+
if (Array.isArray(value)) result = value;
|
|
1553
|
+
}
|
|
1554
|
+
return result;
|
|
1555
|
+
}
|
|
1556
|
+
|
|
1557
|
+
/** 注册响应后处理 */
|
|
1558
|
+
registerAfterReceiveHandler(pluginId: string, handler: (content: string) => string | Promise<string>): void {
|
|
1559
|
+
const instance = this.state.instances.get(pluginId);
|
|
1560
|
+
if (!instance || !instance.enabled) return;
|
|
1561
|
+
this.state.afterReceiveHandlers.push({ pluginId, handler });
|
|
1562
|
+
}
|
|
1563
|
+
|
|
1564
|
+
/** 应用所有响应后处理(支持 async handler) */
|
|
1565
|
+
async applyAfterReceiveHandlers(content: string): Promise<string> {
|
|
1566
|
+
let result = content;
|
|
1567
|
+
for (const { handler, pluginId } of this.state.afterReceiveHandlers) {
|
|
1568
|
+
const value = await this.callSafe(handler, [result], pluginId, '响应后处理');
|
|
1569
|
+
if (typeof value === 'string') result = value;
|
|
1570
|
+
}
|
|
1571
|
+
return result;
|
|
1572
|
+
}
|
|
1573
|
+
|
|
1574
|
+
/** 注册请求配置修改器 */
|
|
1575
|
+
registerRequestConfigModifier(pluginId: string, handler: (config: { temperature: number; maxTokens: number; model: string }) => { temperature: number; maxTokens: number; model: string }): void {
|
|
1576
|
+
const instance = this.state.instances.get(pluginId);
|
|
1577
|
+
if (!instance || !instance.enabled) return;
|
|
1578
|
+
this.state.requestConfigModifiers.push({ pluginId, handler });
|
|
1579
|
+
}
|
|
1580
|
+
|
|
1581
|
+
/** 应用所有请求配置修改器 */
|
|
1582
|
+
async applyRequestConfigModifiers(config: { temperature: number; maxTokens: number; model: string }): Promise<{ temperature: number; maxTokens: number; model: string }> {
|
|
1583
|
+
let result = config;
|
|
1584
|
+
for (const { handler, pluginId } of this.state.requestConfigModifiers) {
|
|
1585
|
+
const value = await this.callSafe(handler, [result], pluginId, '请求配置修改器');
|
|
1586
|
+
if (value && typeof value === 'object' && 'temperature' in value) result = value;
|
|
1587
|
+
}
|
|
1588
|
+
return result;
|
|
1589
|
+
}
|
|
1590
|
+
|
|
1591
|
+
// ==================== 插件持久化存储 ====================
|
|
1592
|
+
|
|
1593
|
+
/** 获取插件存储值 */
|
|
1594
|
+
getPluginStorage(pluginId: string, key: string): unknown {
|
|
1595
|
+
return this.pluginStorage.get(pluginId)?.get(key);
|
|
1596
|
+
}
|
|
1597
|
+
|
|
1598
|
+
/** 设置插件存储值 */
|
|
1599
|
+
setPluginStorage(pluginId: string, key: string, value: unknown): void {
|
|
1600
|
+
if (!this.pluginStorage.has(pluginId)) {
|
|
1601
|
+
this.pluginStorage.set(pluginId, new Map());
|
|
1602
|
+
}
|
|
1603
|
+
this.pluginStorage.get(pluginId)!.set(key, value);
|
|
1604
|
+
}
|
|
1605
|
+
|
|
1606
|
+
/** 删除插件存储值 */
|
|
1607
|
+
removePluginStorage(pluginId: string, key: string): void {
|
|
1608
|
+
this.pluginStorage.get(pluginId)?.delete(key);
|
|
1609
|
+
}
|
|
1610
|
+
|
|
1611
|
+
/** 获取插件存储所有键 */
|
|
1612
|
+
getPluginStorageKeys(pluginId: string): string[] {
|
|
1613
|
+
return Array.from(this.pluginStorage.get(pluginId)?.keys() || []);
|
|
1614
|
+
}
|
|
1615
|
+
|
|
1616
|
+
// ==================== 内部方法 ====================
|
|
1617
|
+
|
|
1618
|
+
/** 执行插件代码,返回导出对象 */
|
|
1619
|
+
private executePluginCode(code: string, pluginId: string): Record<string, (...args: unknown[]) => unknown> {
|
|
1620
|
+
try {
|
|
1621
|
+
// 使用 Function 构造器创建隔离的执行环境
|
|
1622
|
+
// 插件通过 return { setup, exports } 导出 setup 函数和公开 API
|
|
1623
|
+
const wrappedCode = `
|
|
1624
|
+
'use strict';
|
|
1625
|
+
${code};
|
|
1626
|
+
if (typeof setup === 'function') {
|
|
1627
|
+
return { setup };
|
|
1628
|
+
}
|
|
1629
|
+
return {};
|
|
1630
|
+
`;
|
|
1631
|
+
const factory = new Function('xinyu', wrappedCode);
|
|
1632
|
+
const exports: Record<string, (...args: unknown[]) => unknown> = {};
|
|
1633
|
+
|
|
1634
|
+
// 创建 xinyu API 桥接对象
|
|
1635
|
+
const xinyu = this.createXinyuBridge(pluginId);
|
|
1636
|
+
|
|
1637
|
+
const result = factory(xinyu);
|
|
1638
|
+
if (result && typeof result === 'object') {
|
|
1639
|
+
Object.assign(exports, result);
|
|
1640
|
+
}
|
|
1641
|
+
|
|
1642
|
+
return exports;
|
|
1643
|
+
} catch (e) {
|
|
1644
|
+
console.error(`[PluginRuntime] 执行插件代码出错 (${pluginId}):`, e);
|
|
1645
|
+
return {};
|
|
1646
|
+
}
|
|
1647
|
+
}
|
|
1648
|
+
|
|
1649
|
+
/** 创建 xinyu API 桥接对象 */
|
|
1650
|
+
private createXinyuBridge(pluginId: string): Record<string, unknown> {
|
|
1651
|
+
// Blob 转 data URL 辅助函数
|
|
1652
|
+
const blobToDataUrl = (blob: Blob): Promise<string> =>
|
|
1653
|
+
new Promise((resolve, reject) => {
|
|
1654
|
+
const reader = new FileReader();
|
|
1655
|
+
reader.onload = () => resolve(reader.result as string);
|
|
1656
|
+
reader.onerror = reject;
|
|
1657
|
+
reader.readAsDataURL(blob);
|
|
1658
|
+
});
|
|
1659
|
+
|
|
1660
|
+
// 使用箭头函数绑定 this,避免 eslint no-this-alias
|
|
1661
|
+
const getGameState = () => this.getGameState();
|
|
1662
|
+
const setGameState = (partial: Record<string, unknown>) => this.setGameState(partial);
|
|
1663
|
+
const getAttributes = () => this.getAttributes();
|
|
1664
|
+
const registerAttribute = (attr: GameAttribute) => this.registerAttribute(pluginId, attr);
|
|
1665
|
+
const setAttribute = (key: string, value: number | string | boolean) => this.setAttribute(key, value);
|
|
1666
|
+
const registerCommand = (cmd: ChatCommand) => this.registerCommand(pluginId, cmd);
|
|
1667
|
+
const registerMessageRenderer = (matcher: (msg: { role: string; content: string }) => boolean, renderer: (msg: { role: string; content: string }) => string) => {
|
|
1668
|
+
this.registerMessageRenderer(pluginId, { matcher, renderer });
|
|
1669
|
+
};
|
|
1670
|
+
const registerSidebarPanel = (panel: SidebarPanelRegistration) => this.registerSidebarPanel(pluginId, panel);
|
|
1671
|
+
const registerToolbarButton = (button: ToolbarButtonRegistration) => this.registerToolbarButton(pluginId, button);
|
|
1672
|
+
const registerMessageCard = (matcher: (msg: { role: string; content: string }) => boolean, render: (data: Record<string, unknown>) => string) => {
|
|
1673
|
+
this.registerMessageRenderer(pluginId, { matcher, renderer: render as (msg: { role: string; content: string }) => string });
|
|
1674
|
+
};
|
|
1675
|
+
const getManifest = () => this.state.instances.get(pluginId)?.manifest;
|
|
1676
|
+
// plugin.config:插件配置读写(持久化到数据库,与 storage 独立)
|
|
1677
|
+
const configApi = {
|
|
1678
|
+
get: (key?: string) => {
|
|
1679
|
+
const config = this.state.instances.get(pluginId)?.config || {};
|
|
1680
|
+
return key !== undefined ? config[key] : config;
|
|
1681
|
+
},
|
|
1682
|
+
set: (keyOrConfig: string | Record<string, unknown>, value?: unknown) => {
|
|
1683
|
+
const instance = this.state.instances.get(pluginId);
|
|
1684
|
+
if (!instance) return;
|
|
1685
|
+
if (typeof keyOrConfig === 'string') {
|
|
1686
|
+
instance.config = { ...instance.config, [keyOrConfig]: value };
|
|
1687
|
+
} else {
|
|
1688
|
+
instance.config = keyOrConfig;
|
|
1689
|
+
}
|
|
1690
|
+
this.updatePluginConfig(pluginId, instance.config);
|
|
1691
|
+
},
|
|
1692
|
+
};
|
|
1693
|
+
// plugin.storage:纯粹的运行时临时存储,不涉及配置数据
|
|
1694
|
+
const getPluginStorageGet = (key: string) => {
|
|
1695
|
+
return this.getPluginStorage(pluginId, key);
|
|
1696
|
+
};
|
|
1697
|
+
const getPluginStorageSet = (key: string, value: unknown) => {
|
|
1698
|
+
this.setPluginStorage(pluginId, key, value);
|
|
1699
|
+
};
|
|
1700
|
+
const getPluginStorageRemove = (key: string) => this.removePluginStorage(pluginId, key);
|
|
1701
|
+
const getPluginStorageKeys = () => this.getPluginStorageKeys(pluginId);
|
|
1702
|
+
const registerHook = (hook: PluginHook, handler: HookHandler) => this.registerHook(pluginId, hook, handler);
|
|
1703
|
+
const unregisterHook = (hook: PluginHook, handler: HookHandler) => this.unregisterHook(pluginId, hook, handler);
|
|
1704
|
+
const parseDiceNotation = (notation: string) => this.parseDiceNotation(notation);
|
|
1705
|
+
const formatDate = (date: Date | string, format?: string) => this.formatDate(date, format);
|
|
1706
|
+
const registerPromptModifier = (handler: (prompt: string, worldSetting: Record<string, unknown>) => string) => {
|
|
1707
|
+
this.registerPromptModifier(pluginId, handler);
|
|
1708
|
+
};
|
|
1709
|
+
const registerBeforeSendHandler = (handler: (messages: { role: string; content: string }[]) => { role: string; content: string }[]) => {
|
|
1710
|
+
this.registerBeforeSendHandler(pluginId, handler);
|
|
1711
|
+
};
|
|
1712
|
+
const registerBeforeCompleteHandler = (handler: (messages: { role: string; content: string }[]) => { role: string; content: string }[]) => {
|
|
1713
|
+
this.registerBeforeCompleteHandler(pluginId, handler);
|
|
1714
|
+
};
|
|
1715
|
+
const registerAfterReceiveHandler = (handler: (content: string) => string | Promise<string>) => {
|
|
1716
|
+
this.registerAfterReceiveHandler(pluginId, handler);
|
|
1717
|
+
};
|
|
1718
|
+
const registerRequestConfigModifier = (handler: (config: { temperature: number; maxTokens: number; model: string }) => { temperature: number; maxTokens: number; model: string }) => {
|
|
1719
|
+
this.registerRequestConfigModifier(pluginId, handler);
|
|
1720
|
+
};
|
|
1721
|
+
|
|
1722
|
+
// 全局 UI API 方法
|
|
1723
|
+
const uiRegisterSlot = (slotId: UISlotId, content: string, options?: { priority?: number; persistent?: boolean }) => {
|
|
1724
|
+
return this.registerSlot(pluginId, slotId, content, options);
|
|
1725
|
+
};
|
|
1726
|
+
const uiUnregisterSlot = (registrationId: string) => {
|
|
1727
|
+
this.unregisterSlot(registrationId);
|
|
1728
|
+
};
|
|
1729
|
+
const uiUpdateSlot = (registrationId: string, content: string, options?: { priority?: number }) => {
|
|
1730
|
+
return this.updateSlot(registrationId, content, options);
|
|
1731
|
+
};
|
|
1732
|
+
const uiRegisterActionHandler = (actionName: string, handler: (e: { target: HTMLElement; actionName: string; payload?: string }) => void) => {
|
|
1733
|
+
this.registerActionHandler(pluginId, actionName, handler);
|
|
1734
|
+
};
|
|
1735
|
+
const uiShowModal = (options: { title: string; content: string; width?: string; style?: Record<string, unknown>; closable?: boolean; backdrop?: boolean; onClose?: () => void; actions?: ModalActionInput[] }) => {
|
|
1736
|
+
return this.showModal(pluginId, options);
|
|
1737
|
+
};
|
|
1738
|
+
const uiCloseModal = (modalId: string) => {
|
|
1739
|
+
this.closeModal(modalId);
|
|
1740
|
+
};
|
|
1741
|
+
const uiUpdateModal = (modalId: string, options: { title?: string; content?: string; width?: string; style?: Record<string, unknown> }) => {
|
|
1742
|
+
this.updateModal(modalId, options);
|
|
1743
|
+
};
|
|
1744
|
+
const uiConfirm = async (title: string, message: string, options?: ConfirmOptions) => {
|
|
1745
|
+
return this.confirm(pluginId, title, message, options);
|
|
1746
|
+
};
|
|
1747
|
+
const uiInjectStyle = (css: string) => {
|
|
1748
|
+
return this.injectStyle(pluginId, css);
|
|
1749
|
+
};
|
|
1750
|
+
const uiRemoveStyle = (styleId: string) => {
|
|
1751
|
+
this.removeStyle(pluginId, styleId);
|
|
1752
|
+
};
|
|
1753
|
+
const uiOnHostEvent = (eventName: string, handler: HookHandler) => {
|
|
1754
|
+
this.onHostEvent(pluginId, eventName, handler);
|
|
1755
|
+
};
|
|
1756
|
+
const uiOffHostEvent = (eventName: string, handler: HookHandler) => {
|
|
1757
|
+
this.offHostEvent(pluginId, eventName, handler);
|
|
1758
|
+
};
|
|
1759
|
+
|
|
1760
|
+
return {
|
|
1761
|
+
game: {
|
|
1762
|
+
getWorldSetting: () => {
|
|
1763
|
+
const manifest = getManifest();
|
|
1764
|
+
if (!manifest || !checkGameInfoPermission(manifest)) return {};
|
|
1765
|
+
return this.uiCallbacks.getWorldSetting?.() || {};
|
|
1766
|
+
},
|
|
1767
|
+
setWorldSetting: (data: Record<string, unknown>) => {
|
|
1768
|
+
const manifest = getManifest();
|
|
1769
|
+
if (!manifest || !checkGameInfoWritePermission(manifest)) return;
|
|
1770
|
+
this.uiCallbacks.setWorldSetting?.(data);
|
|
1771
|
+
},
|
|
1772
|
+
getState: getGameState,
|
|
1773
|
+
setState: setGameState,
|
|
1774
|
+
getSessionId: () => {
|
|
1775
|
+
const manifest = getManifest();
|
|
1776
|
+
if (!manifest || !checkGameInfoPermission(manifest)) return '';
|
|
1777
|
+
return this.uiCallbacks.getSessionId?.() || '';
|
|
1778
|
+
},
|
|
1779
|
+
getMessages: () => {
|
|
1780
|
+
const manifest = getManifest();
|
|
1781
|
+
if (!manifest || !checkGameInfoPermission(manifest)) return [];
|
|
1782
|
+
return this.uiCallbacks.getMessages?.() || [];
|
|
1783
|
+
},
|
|
1784
|
+
getAttributes: () => {
|
|
1785
|
+
const manifest = getManifest();
|
|
1786
|
+
if (!manifest || !checkGameInfoPermission(manifest)) return [];
|
|
1787
|
+
return getAttributes();
|
|
1788
|
+
},
|
|
1789
|
+
registerAttribute,
|
|
1790
|
+
setAttribute,
|
|
1791
|
+
},
|
|
1792
|
+
chat: {
|
|
1793
|
+
send: async (content: string) => {
|
|
1794
|
+
if (this.uiCallbacks.sendMessage) {
|
|
1795
|
+
await this.uiCallbacks.sendMessage(content);
|
|
1796
|
+
}
|
|
1797
|
+
},
|
|
1798
|
+
insertSystemMessage: (content: string) => {
|
|
1799
|
+
if (this.uiCallbacks.insertSystemMessage) {
|
|
1800
|
+
this.uiCallbacks.insertSystemMessage(content);
|
|
1801
|
+
}
|
|
1802
|
+
},
|
|
1803
|
+
insertNotice: (html: string) => {
|
|
1804
|
+
if (this.uiCallbacks.insertNotice) {
|
|
1805
|
+
this.uiCallbacks.insertNotice(html);
|
|
1806
|
+
}
|
|
1807
|
+
},
|
|
1808
|
+
injectChatStyle: (css: string) => {
|
|
1809
|
+
return this.injectStyle(pluginId, css);
|
|
1810
|
+
},
|
|
1811
|
+
removeChatStyle: (styleId: string) => {
|
|
1812
|
+
this.removeStyle(pluginId, styleId);
|
|
1813
|
+
},
|
|
1814
|
+
getHistory: () => [],
|
|
1815
|
+
clearHistory: () => {},
|
|
1816
|
+
registerCommand,
|
|
1817
|
+
registerMessageRenderer,
|
|
1818
|
+
// 输入框控制 API(需 input:control 权限)
|
|
1819
|
+
input: {
|
|
1820
|
+
getContent: (): string | null => {
|
|
1821
|
+
const manifest = getManifest();
|
|
1822
|
+
if (!manifest || !checkInputControlPermission(manifest)) return null;
|
|
1823
|
+
return this.inputControl.getContent();
|
|
1824
|
+
},
|
|
1825
|
+
setContent: (text: string): void => {
|
|
1826
|
+
const manifest = getManifest();
|
|
1827
|
+
if (!manifest || !checkInputControlPermission(manifest)) return;
|
|
1828
|
+
this.inputControl.setContent(text);
|
|
1829
|
+
},
|
|
1830
|
+
appendContent: (text: string): void => {
|
|
1831
|
+
const manifest = getManifest();
|
|
1832
|
+
if (!manifest || !checkInputControlPermission(manifest)) return;
|
|
1833
|
+
this.inputControl.appendContent(text);
|
|
1834
|
+
},
|
|
1835
|
+
clearContent: (): void => {
|
|
1836
|
+
const manifest = getManifest();
|
|
1837
|
+
if (!manifest || !checkInputControlPermission(manifest)) return;
|
|
1838
|
+
this.inputControl.clearContent();
|
|
1839
|
+
},
|
|
1840
|
+
setPlaceholder: (text: string): void => {
|
|
1841
|
+
const manifest = getManifest();
|
|
1842
|
+
if (!manifest || !checkInputControlPermission(manifest)) return;
|
|
1843
|
+
this.inputControl.setPlaceholder(text);
|
|
1844
|
+
},
|
|
1845
|
+
setDisabled: (disabled: boolean): void => {
|
|
1846
|
+
const manifest = getManifest();
|
|
1847
|
+
if (!manifest || !checkInputControlPermission(manifest)) return;
|
|
1848
|
+
this.inputControl.setDisabled(disabled);
|
|
1849
|
+
},
|
|
1850
|
+
focus: (): void => {
|
|
1851
|
+
const manifest = getManifest();
|
|
1852
|
+
if (!manifest || !checkInputControlPermission(manifest)) return;
|
|
1853
|
+
this.inputControl.focus();
|
|
1854
|
+
},
|
|
1855
|
+
blur: (): void => {
|
|
1856
|
+
const manifest = getManifest();
|
|
1857
|
+
if (!manifest || !checkInputControlPermission(manifest)) return;
|
|
1858
|
+
this.inputControl.blur();
|
|
1859
|
+
},
|
|
1860
|
+
setStyle: (style: Record<string, string>): void => {
|
|
1861
|
+
const manifest = getManifest();
|
|
1862
|
+
if (!manifest || !checkInputControlPermission(manifest)) return;
|
|
1863
|
+
this.inputControl.setStyle(style);
|
|
1864
|
+
},
|
|
1865
|
+
injectStyle: (css: string): string => {
|
|
1866
|
+
const manifest = getManifest();
|
|
1867
|
+
if (!manifest || !checkInputControlPermission(manifest)) return '';
|
|
1868
|
+
return this.inputControl.injectStyle(css);
|
|
1869
|
+
},
|
|
1870
|
+
removeStyle: (styleId: string): void => {
|
|
1871
|
+
const manifest = getManifest();
|
|
1872
|
+
if (!manifest || !checkInputControlPermission(manifest)) return;
|
|
1873
|
+
this.inputControl.removeStyle(styleId);
|
|
1874
|
+
},
|
|
1875
|
+
onInput: (handler: (value: string) => void): (() => void) => {
|
|
1876
|
+
const manifest = getManifest();
|
|
1877
|
+
if (!manifest || !checkInputControlPermission(manifest)) return () => {};
|
|
1878
|
+
return this.inputControl.onInput(handler);
|
|
1879
|
+
},
|
|
1880
|
+
onSend: (handler: (content: string) => void): (() => void) => {
|
|
1881
|
+
const manifest = getManifest();
|
|
1882
|
+
if (!manifest || !checkInputControlPermission(manifest)) return () => {};
|
|
1883
|
+
return this.inputControl.onSend(handler);
|
|
1884
|
+
},
|
|
1885
|
+
onKeyDown: (handler: (event: { key: string; ctrlKey: boolean; shiftKey: boolean; altKey: boolean; preventDefault: () => void }) => void): (() => void) => {
|
|
1886
|
+
const manifest = getManifest();
|
|
1887
|
+
if (!manifest || !checkInputControlPermission(manifest)) return () => {};
|
|
1888
|
+
return this.inputControl.onKeyDown(handler);
|
|
1889
|
+
},
|
|
1890
|
+
getSelection: (): { start: number; end: number; text: string } | null => {
|
|
1891
|
+
const manifest = getManifest();
|
|
1892
|
+
if (!manifest || !checkInputControlPermission(manifest)) return null;
|
|
1893
|
+
return this.inputControl.getSelection();
|
|
1894
|
+
},
|
|
1895
|
+
setSelection: (start: number, end: number): void => {
|
|
1896
|
+
const manifest = getManifest();
|
|
1897
|
+
if (!manifest || !checkInputControlPermission(manifest)) return;
|
|
1898
|
+
this.inputControl.setSelection(start, end);
|
|
1899
|
+
},
|
|
1900
|
+
insertAtCursor: (text: string): void => {
|
|
1901
|
+
const manifest = getManifest();
|
|
1902
|
+
if (!manifest || !checkInputControlPermission(manifest)) return;
|
|
1903
|
+
this.inputControl.insertAtCursor(text);
|
|
1904
|
+
},
|
|
1905
|
+
setMaxLength: (length: number): void => {
|
|
1906
|
+
const manifest = getManifest();
|
|
1907
|
+
if (!manifest || !checkInputControlPermission(manifest)) return;
|
|
1908
|
+
this.inputControl.setMaxLength(length);
|
|
1909
|
+
},
|
|
1910
|
+
getLength: (): number => {
|
|
1911
|
+
const manifest = getManifest();
|
|
1912
|
+
if (!manifest || !checkInputControlPermission(manifest)) return 0;
|
|
1913
|
+
return this.inputControl.getLength();
|
|
1914
|
+
},
|
|
1915
|
+
},
|
|
1916
|
+
},
|
|
1917
|
+
ui: {
|
|
1918
|
+
// 现有 API
|
|
1919
|
+
registerSidebarPanel,
|
|
1920
|
+
registerInputToolbarButton: registerToolbarButton,
|
|
1921
|
+
registerMessageCard,
|
|
1922
|
+
toast: (message: string, type?: 'info' | 'success' | 'warning' | 'error') => {
|
|
1923
|
+
if (this.uiCallbacks.toast) {
|
|
1924
|
+
this.uiCallbacks.toast(message, type);
|
|
1925
|
+
}
|
|
1926
|
+
},
|
|
1927
|
+
|
|
1928
|
+
// 新增:确认对话框
|
|
1929
|
+
confirm: uiConfirm,
|
|
1930
|
+
|
|
1931
|
+
// 新增:自定义模态框
|
|
1932
|
+
showModal: uiShowModal,
|
|
1933
|
+
closeModal: uiCloseModal,
|
|
1934
|
+
updateModal: uiUpdateModal,
|
|
1935
|
+
|
|
1936
|
+
// 新增:容器注册式 API
|
|
1937
|
+
registerSlot: uiRegisterSlot,
|
|
1938
|
+
unregisterSlot: uiUnregisterSlot,
|
|
1939
|
+
updateSlot: uiUpdateSlot,
|
|
1940
|
+
|
|
1941
|
+
// 新增:事件委托 action handler
|
|
1942
|
+
onAction: uiRegisterActionHandler,
|
|
1943
|
+
|
|
1944
|
+
// 新增:自由 DOM API
|
|
1945
|
+
dom: this.createDOMSandboxAPI(pluginId),
|
|
1946
|
+
|
|
1947
|
+
// 新增:样式注入 API
|
|
1948
|
+
injectStyle: uiInjectStyle,
|
|
1949
|
+
removeStyle: uiRemoveStyle,
|
|
1950
|
+
|
|
1951
|
+
// 新增:宿主事件系统
|
|
1952
|
+
onHostEvent: uiOnHostEvent,
|
|
1953
|
+
offHostEvent: uiOffHostEvent,
|
|
1954
|
+
},
|
|
1955
|
+
ai: {
|
|
1956
|
+
onPromptBuild: registerPromptModifier,
|
|
1957
|
+
onBeforeSend: registerBeforeSendHandler,
|
|
1958
|
+
onBeforeComplete: registerBeforeCompleteHandler,
|
|
1959
|
+
onAfterReceive: registerAfterReceiveHandler,
|
|
1960
|
+
onRequestConfig: registerRequestConfigModifier,
|
|
1961
|
+
},
|
|
1962
|
+
plugin: {
|
|
1963
|
+
getManifest,
|
|
1964
|
+
config: configApi,
|
|
1965
|
+
storage: {
|
|
1966
|
+
get: getPluginStorageGet,
|
|
1967
|
+
set: getPluginStorageSet,
|
|
1968
|
+
remove: getPluginStorageRemove,
|
|
1969
|
+
keys: getPluginStorageKeys,
|
|
1970
|
+
},
|
|
1971
|
+
on: registerHook,
|
|
1972
|
+
off: unregisterHook,
|
|
1973
|
+
// 跨插件通信 API
|
|
1974
|
+
callPlugin: (targetPluginId: string, method: string, ...args: unknown[]) => {
|
|
1975
|
+
return this.callPlugin(pluginId, targetPluginId, method, ...args);
|
|
1976
|
+
},
|
|
1977
|
+
// 运行时加载依赖(支持降级)
|
|
1978
|
+
loadDependency: (targetPluginId: string, options?: { version?: string; timeout?: number }) => {
|
|
1979
|
+
return this.loadDependency(pluginId, targetPluginId, options);
|
|
1980
|
+
},
|
|
1981
|
+
// 检查插件是否可用
|
|
1982
|
+
isPluginAvailable: (targetPluginId: string) => {
|
|
1983
|
+
return this.isPluginAvailable(targetPluginId);
|
|
1984
|
+
},
|
|
1985
|
+
// 获取插件的公开导出 API 列表
|
|
1986
|
+
getPluginExports: (targetPluginId: string) => {
|
|
1987
|
+
return this.getPluginExports(targetPluginId);
|
|
1988
|
+
},
|
|
1989
|
+
// 获取插件资源文件内容(需 file:read 权限)
|
|
1990
|
+
getResource: async (resourcePath: string): Promise<string | null> => {
|
|
1991
|
+
const manifest = getManifest();
|
|
1992
|
+
if (!manifest) return null;
|
|
1993
|
+
try {
|
|
1994
|
+
const params = new URLSearchParams({ pluginId, path: resourcePath });
|
|
1995
|
+
const res = await fetch(`/api/plugins/resources?${params}`);
|
|
1996
|
+
if (!res.ok) return null;
|
|
1997
|
+
const contentType = res.headers.get('content-type') || '';
|
|
1998
|
+
if (contentType.startsWith('text/') || contentType.includes('json') || contentType.includes('javascript')) {
|
|
1999
|
+
return await res.text();
|
|
2000
|
+
}
|
|
2001
|
+
// 二进制文件返回 data URL
|
|
2002
|
+
const blob = await res.blob();
|
|
2003
|
+
return await blobToDataUrl(blob);
|
|
2004
|
+
} catch { return null; }
|
|
2005
|
+
},
|
|
2006
|
+
// 获取插件资源文件的 HTTP URL(需 file:read 权限)
|
|
2007
|
+
getResourceUrl: (resourcePath: string): string => {
|
|
2008
|
+
return `/api/plugins/resources?pluginId=${encodeURIComponent(pluginId)}&path=${encodeURIComponent(resourcePath)}`;
|
|
2009
|
+
},
|
|
2010
|
+
},
|
|
2011
|
+
// storage:持久化 key-value 存储(需 storage:plugin 权限)
|
|
2012
|
+
storage: {
|
|
2013
|
+
get: async (key: string) => {
|
|
2014
|
+
const manifest = getManifest();
|
|
2015
|
+
if (!manifest || !checkStoragePermission(manifest)) {
|
|
2016
|
+
console.warn(`[PluginRuntime] storage.get 被拒绝: 插件 ${pluginId} 缺少 storage:plugin 权限`);
|
|
2017
|
+
return null;
|
|
2018
|
+
}
|
|
2019
|
+
try {
|
|
2020
|
+
const worldId = this.uiCallbacks.getWorldId?.() || '';
|
|
2021
|
+
const params = new URLSearchParams({ pluginId, key, worldId });
|
|
2022
|
+
const res = await fetch(`/api/plugins/storage?${params}`);
|
|
2023
|
+
if (!res.ok) {
|
|
2024
|
+
console.warn(`[PluginRuntime] storage.get 失败: ${res.status} ${res.statusText} (pluginId=${pluginId}, key=${key})`);
|
|
2025
|
+
return null;
|
|
2026
|
+
}
|
|
2027
|
+
const data = await res.json();
|
|
2028
|
+
return data.value;
|
|
2029
|
+
} catch (e) {
|
|
2030
|
+
console.error(`[PluginRuntime] storage.get 异常 (pluginId=${pluginId}, key=${key}):`, e);
|
|
2031
|
+
return null;
|
|
2032
|
+
}
|
|
2033
|
+
},
|
|
2034
|
+
set: async (key: string, value: unknown) => {
|
|
2035
|
+
const manifest = getManifest();
|
|
2036
|
+
if (!manifest || !checkStoragePermission(manifest)) {
|
|
2037
|
+
console.warn(`[PluginRuntime] storage.set 被拒绝: 插件 ${pluginId} 缺少 storage:plugin 权限`);
|
|
2038
|
+
return;
|
|
2039
|
+
}
|
|
2040
|
+
try {
|
|
2041
|
+
const worldId = this.uiCallbacks.getWorldId?.() || '';
|
|
2042
|
+
const res = await fetch('/api/plugins/storage', {
|
|
2043
|
+
method: 'PUT',
|
|
2044
|
+
headers: { 'Content-Type': 'application/json' },
|
|
2045
|
+
body: JSON.stringify({ pluginId, key, value, worldId }),
|
|
2046
|
+
});
|
|
2047
|
+
if (!res.ok) {
|
|
2048
|
+
console.warn(`[PluginRuntime] storage.set 失败: ${res.status} ${res.statusText} (pluginId=${pluginId}, key=${key})`);
|
|
2049
|
+
}
|
|
2050
|
+
} catch (e) {
|
|
2051
|
+
console.error(`[PluginRuntime] storage.set 异常 (pluginId=${pluginId}, key=${key}):`, e);
|
|
2052
|
+
}
|
|
2053
|
+
},
|
|
2054
|
+
remove: async (key: string) => {
|
|
2055
|
+
const manifest = getManifest();
|
|
2056
|
+
if (!manifest || !checkStoragePermission(manifest)) return;
|
|
2057
|
+
try {
|
|
2058
|
+
const worldId = this.uiCallbacks.getWorldId?.() || '';
|
|
2059
|
+
const params = new URLSearchParams({ pluginId, key, worldId });
|
|
2060
|
+
const res = await fetch(`/api/plugins/storage?${params}`, { method: 'DELETE' });
|
|
2061
|
+
if (!res.ok) {
|
|
2062
|
+
console.warn(`[PluginRuntime] storage.remove 失败: ${res.status} (pluginId=${pluginId}, key=${key})`);
|
|
2063
|
+
}
|
|
2064
|
+
} catch (e) {
|
|
2065
|
+
console.error(`[PluginRuntime] storage.remove 异常 (pluginId=${pluginId}, key=${key}):`, e);
|
|
2066
|
+
}
|
|
2067
|
+
},
|
|
2068
|
+
keys: async () => {
|
|
2069
|
+
const manifest = getManifest();
|
|
2070
|
+
if (!manifest || !checkStoragePermission(manifest)) return [];
|
|
2071
|
+
try {
|
|
2072
|
+
const worldId = this.uiCallbacks.getWorldId?.() || '';
|
|
2073
|
+
const params = new URLSearchParams({ pluginId, worldId });
|
|
2074
|
+
const res = await fetch(`/api/plugins/storage?${params}`);
|
|
2075
|
+
if (!res.ok) return [];
|
|
2076
|
+
const data = await res.json();
|
|
2077
|
+
return Object.keys(data.values || {});
|
|
2078
|
+
} catch { return []; }
|
|
2079
|
+
},
|
|
2080
|
+
},
|
|
2081
|
+
// file:文件操作(需 file:read / file:write 权限)
|
|
2082
|
+
file: {
|
|
2083
|
+
read: async (fileName: string) => {
|
|
2084
|
+
const manifest = getManifest();
|
|
2085
|
+
if (!manifest || !checkFileReadPermission(manifest)) return null;
|
|
2086
|
+
try {
|
|
2087
|
+
const params = new URLSearchParams({ pluginId, fileName });
|
|
2088
|
+
const res = await fetch(`/api/plugins/files?${params}`);
|
|
2089
|
+
if (!res.ok) return null;
|
|
2090
|
+
const data = await res.json();
|
|
2091
|
+
return data.content;
|
|
2092
|
+
} catch { return null; }
|
|
2093
|
+
},
|
|
2094
|
+
write: async (fileName: string, content: string) => {
|
|
2095
|
+
const manifest = getManifest();
|
|
2096
|
+
if (!manifest || !checkFileWritePermission(manifest)) return;
|
|
2097
|
+
try {
|
|
2098
|
+
await fetch('/api/plugins/files', {
|
|
2099
|
+
method: 'PUT',
|
|
2100
|
+
headers: { 'Content-Type': 'application/json' },
|
|
2101
|
+
body: JSON.stringify({ pluginId, fileName, content }),
|
|
2102
|
+
});
|
|
2103
|
+
} catch { /* ignore */ }
|
|
2104
|
+
},
|
|
2105
|
+
remove: async (fileName: string) => {
|
|
2106
|
+
const manifest = getManifest();
|
|
2107
|
+
if (!manifest || !checkFileWritePermission(manifest)) return;
|
|
2108
|
+
try {
|
|
2109
|
+
const params = new URLSearchParams({ pluginId, fileName });
|
|
2110
|
+
await fetch(`/api/plugins/files?${params}`, { method: 'DELETE' });
|
|
2111
|
+
} catch { /* ignore */ }
|
|
2112
|
+
},
|
|
2113
|
+
list: async () => {
|
|
2114
|
+
const manifest = getManifest();
|
|
2115
|
+
if (!manifest || !checkFileReadPermission(manifest)) return [];
|
|
2116
|
+
try {
|
|
2117
|
+
const params = new URLSearchParams({ pluginId });
|
|
2118
|
+
const res = await fetch(`/api/plugins/files?${params}`);
|
|
2119
|
+
if (!res.ok) return [];
|
|
2120
|
+
const data = await res.json();
|
|
2121
|
+
return data.files || [];
|
|
2122
|
+
} catch { return []; }
|
|
2123
|
+
},
|
|
2124
|
+
},
|
|
2125
|
+
utils: {
|
|
2126
|
+
randomInt: (min: number, max: number) => Math.floor(Math.random() * (max - min + 1)) + min,
|
|
2127
|
+
rollDice: parseDiceNotation,
|
|
2128
|
+
formatDate,
|
|
2129
|
+
debounce: (fn: (...args: unknown[]) => unknown, ms: number) => {
|
|
2130
|
+
let timer: ReturnType<typeof setTimeout> | null = null;
|
|
2131
|
+
return (...args: unknown[]) => {
|
|
2132
|
+
if (timer) clearTimeout(timer);
|
|
2133
|
+
timer = setTimeout(() => fn(...args), ms);
|
|
2134
|
+
};
|
|
2135
|
+
},
|
|
2136
|
+
throttle: (fn: (...args: unknown[]) => unknown, ms: number) => {
|
|
2137
|
+
let lastCall = 0;
|
|
2138
|
+
return (...args: unknown[]) => {
|
|
2139
|
+
const now = Date.now();
|
|
2140
|
+
if (now - lastCall >= ms) {
|
|
2141
|
+
lastCall = now;
|
|
2142
|
+
fn(...args);
|
|
2143
|
+
}
|
|
2144
|
+
};
|
|
2145
|
+
},
|
|
2146
|
+
eventBus: (() => {
|
|
2147
|
+
const handlers = new Map<string, Array<(...args: unknown[]) => void>>();
|
|
2148
|
+
return {
|
|
2149
|
+
_handlers: handlers,
|
|
2150
|
+
on(event: string, handler: (...args: unknown[]) => void) {
|
|
2151
|
+
if (!handlers.has(event)) {
|
|
2152
|
+
handlers.set(event, []);
|
|
2153
|
+
}
|
|
2154
|
+
handlers.get(event)!.push(handler);
|
|
2155
|
+
},
|
|
2156
|
+
off(event: string, handler: (...args: unknown[]) => void) {
|
|
2157
|
+
const hs = handlers.get(event);
|
|
2158
|
+
if (hs) {
|
|
2159
|
+
const idx = hs.indexOf(handler);
|
|
2160
|
+
if (idx !== -1) hs.splice(idx, 1);
|
|
2161
|
+
}
|
|
2162
|
+
},
|
|
2163
|
+
emit(event: string, ...args: unknown[]) {
|
|
2164
|
+
const hs = handlers.get(event);
|
|
2165
|
+
if (hs) {
|
|
2166
|
+
hs.forEach(h => { try { h(...args); } catch (e) { console.error(e); } });
|
|
2167
|
+
}
|
|
2168
|
+
},
|
|
2169
|
+
};
|
|
2170
|
+
})(),
|
|
2171
|
+
},
|
|
2172
|
+
};
|
|
2173
|
+
}
|
|
2174
|
+
|
|
2175
|
+
/** 解析骰子表达式 */
|
|
2176
|
+
private parseDiceNotation(notation: string): { notation: string; rolls: number[]; modifier: number; total: number } {
|
|
2177
|
+
const trimmed = notation.trim().toLowerCase();
|
|
2178
|
+
const match = trimmed.match(/^(\d+)d(\d+)([+-]\d+)?$/);
|
|
2179
|
+
if (!match) {
|
|
2180
|
+
return { notation: trimmed, rolls: [0], modifier: 0, total: 0 };
|
|
2181
|
+
}
|
|
2182
|
+
const count = parseInt(match[1]);
|
|
2183
|
+
const sides = parseInt(match[2]);
|
|
2184
|
+
const modifier = match[3] ? parseInt(match[3]) : 0;
|
|
2185
|
+
const rolls: number[] = [];
|
|
2186
|
+
for (let i = 0; i < count; i++) {
|
|
2187
|
+
rolls.push(Math.floor(Math.random() * sides) + 1);
|
|
2188
|
+
}
|
|
2189
|
+
const sum = rolls.reduce((a, b) => a + b, 0);
|
|
2190
|
+
return { notation: trimmed, rolls, modifier, total: sum + modifier };
|
|
2191
|
+
}
|
|
2192
|
+
|
|
2193
|
+
/** 格式化日期 */
|
|
2194
|
+
private formatDate(date: Date | string, format?: string): string {
|
|
2195
|
+
const d = typeof date === 'string' ? new Date(date) : date;
|
|
2196
|
+
const fmt = format || 'YYYY-MM-DD HH:mm:ss';
|
|
2197
|
+
const pad = (n: number) => n.toString().padStart(2, '0');
|
|
2198
|
+
return fmt
|
|
2199
|
+
.replace('YYYY', d.getFullYear().toString())
|
|
2200
|
+
.replace('MM', pad(d.getMonth() + 1))
|
|
2201
|
+
.replace('DD', pad(d.getDate()))
|
|
2202
|
+
.replace('HH', pad(d.getHours()))
|
|
2203
|
+
.replace('mm', pad(d.getMinutes()))
|
|
2204
|
+
.replace('ss', pad(d.getSeconds()));
|
|
2205
|
+
}
|
|
2206
|
+
|
|
2207
|
+
/** 清理插件的所有注册(按 pluginId 精确过滤) */
|
|
2208
|
+
private unregisterAllForPlugin(pluginId: string): void {
|
|
2209
|
+
// 清理 hooks
|
|
2210
|
+
this.state.hooks.forEach((registrations, hook) => {
|
|
2211
|
+
this.state.hooks.set(hook, registrations.filter(r => r.pluginId !== pluginId));
|
|
2212
|
+
});
|
|
2213
|
+
|
|
2214
|
+
// 清理 UI 注册(按 pluginId 过滤,修复原有 Bug)
|
|
2215
|
+
this.state.sidebarPanels = this.state.sidebarPanels.filter(p => p.pluginId !== pluginId);
|
|
2216
|
+
this.state.toolbarButtons.delete(pluginId);
|
|
2217
|
+
this.state.messageRenderers = this.state.messageRenderers.filter(r => r.pluginId !== pluginId);
|
|
2218
|
+
|
|
2219
|
+
// 清理 AI 拦截(按 pluginId 过滤)
|
|
2220
|
+
this.state.promptModifiers = this.state.promptModifiers.filter(m => m.pluginId !== pluginId);
|
|
2221
|
+
this.state.beforeSendHandlers = this.state.beforeSendHandlers.filter(h => h.pluginId !== pluginId);
|
|
2222
|
+
this.state.beforeCompleteHandlers = this.state.beforeCompleteHandlers.filter(h => h.pluginId !== pluginId);
|
|
2223
|
+
this.state.afterReceiveHandlers = this.state.afterReceiveHandlers.filter(h => h.pluginId !== pluginId);
|
|
2224
|
+
this.state.requestConfigModifiers = this.state.requestConfigModifiers.filter(m => m.pluginId !== pluginId);
|
|
2225
|
+
|
|
2226
|
+
// 清理插槽注册
|
|
2227
|
+
this.state.slotRegistrations = this.state.slotRegistrations.filter(r => r.pluginId !== pluginId);
|
|
2228
|
+
this.state.actionHandlers = this.state.actionHandlers.filter(h => h.pluginId !== pluginId);
|
|
2229
|
+
|
|
2230
|
+
// 清理模态框
|
|
2231
|
+
this.state.modals.forEach((modal, modalId) => {
|
|
2232
|
+
if (modal.pluginId === pluginId) {
|
|
2233
|
+
this.state.modals.delete(modalId);
|
|
2234
|
+
if (this.uiCallbacks.closeModal) {
|
|
2235
|
+
this.uiCallbacks.closeModal(modalId);
|
|
2236
|
+
}
|
|
2237
|
+
}
|
|
2238
|
+
});
|
|
2239
|
+
|
|
2240
|
+
// 清理宿主事件
|
|
2241
|
+
this.state.hostEventHandlers = this.state.hostEventHandlers.filter(h => h.pluginId !== pluginId);
|
|
2242
|
+
|
|
2243
|
+
// 清理游戏属性
|
|
2244
|
+
this.state.gameAttributes.forEach((attr, key) => {
|
|
2245
|
+
if (attr.pluginId === pluginId) {
|
|
2246
|
+
this.state.gameAttributes.delete(key);
|
|
2247
|
+
}
|
|
2248
|
+
});
|
|
2249
|
+
|
|
2250
|
+
// 清理快捷指令
|
|
2251
|
+
this.state.chatCommands.forEach((cmd, name) => {
|
|
2252
|
+
if (cmd.pluginId === pluginId) {
|
|
2253
|
+
this.state.chatCommands.delete(name);
|
|
2254
|
+
}
|
|
2255
|
+
});
|
|
2256
|
+
}
|
|
2257
|
+
|
|
2258
|
+
/** 重置整个运行时 */
|
|
2259
|
+
reset(): void {
|
|
2260
|
+
// 先对所有已加载的插件触发 onUnload 钩子
|
|
2261
|
+
this.state.instances.forEach((instance, pluginId) => {
|
|
2262
|
+
if (instance.enabled) {
|
|
2263
|
+
try {
|
|
2264
|
+
this.triggerHook('onUnload', pluginId);
|
|
2265
|
+
} catch (e) {
|
|
2266
|
+
console.error(`[PluginRuntime] 卸载插件 ${pluginId} 时出错:`, e);
|
|
2267
|
+
}
|
|
2268
|
+
}
|
|
2269
|
+
});
|
|
2270
|
+
// 清理所有插件资源
|
|
2271
|
+
this.resourceTracker.reset();
|
|
2272
|
+
this.state = createRuntimeState();
|
|
2273
|
+
this.pluginStorage = new Map();
|
|
2274
|
+
}
|
|
2275
|
+
}
|
|
2276
|
+
|
|
2277
|
+
// 单例导出
|
|
2278
|
+
let runtimeInstance: PluginRuntimeEngine | null = null;
|
|
2279
|
+
|
|
2280
|
+
export function getPluginRuntime(): PluginRuntimeEngine {
|
|
2281
|
+
if (!runtimeInstance) {
|
|
2282
|
+
runtimeInstance = new PluginRuntimeEngine();
|
|
2283
|
+
}
|
|
2284
|
+
return runtimeInstance;
|
|
2285
|
+
}
|
|
2286
|
+
|
|
2287
|
+
export { PluginRuntimeEngine };
|