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,4258 @@
|
|
|
1
|
+
// app/extensions/tutorial/page.tsx - 插件开发教程页面
|
|
2
|
+
'use client';
|
|
3
|
+
|
|
4
|
+
import React, { useState, useEffect, useRef, useCallback } from 'react';
|
|
5
|
+
import PageHeader from '@/components/ui/PageHeader';
|
|
6
|
+
import { useRouterHistory } from '@/lib/router-history';
|
|
7
|
+
import { ThemeSwitcher } from '@/components/ui/ThemeSwitcher';
|
|
8
|
+
import { presetThemes } from '@/lib/themes';
|
|
9
|
+
|
|
10
|
+
const s = {
|
|
11
|
+
bgPrimary: 'var(--color-bg-primary)',
|
|
12
|
+
bgSecondary: 'var(--color-bg-secondary)',
|
|
13
|
+
bgTertiary: 'var(--color-bg-tertiary)',
|
|
14
|
+
textPrimary: 'var(--color-text-primary)',
|
|
15
|
+
textSecondary: 'var(--color-text-secondary)',
|
|
16
|
+
textMuted: 'var(--color-text-muted)',
|
|
17
|
+
accent: 'var(--color-accent)',
|
|
18
|
+
accentHover: 'var(--color-accent-hover)',
|
|
19
|
+
border: 'var(--color-border)',
|
|
20
|
+
shadow: 'var(--color-shadow)',
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
// ==================== 代码块常量 ====================
|
|
24
|
+
|
|
25
|
+
const CODE_OVERVIEW_MINIMAL = [
|
|
26
|
+
'function setup(xinyu) {',
|
|
27
|
+
" // 插件加载时执行",
|
|
28
|
+
" xinyu.ui.toast('插件已启用!', 'success');",
|
|
29
|
+
'}',
|
|
30
|
+
].join('\n');
|
|
31
|
+
|
|
32
|
+
const CODE_ARCHITECTURE_EXEC = [
|
|
33
|
+
'// 运行时内部执行方式(简化)',
|
|
34
|
+
'const wrappedCode = `',
|
|
35
|
+
" 'use strict';",
|
|
36
|
+
' ${pluginCode};',
|
|
37
|
+
" if (typeof setup === 'function') {",
|
|
38
|
+
' return { setup };',
|
|
39
|
+
' }',
|
|
40
|
+
' return {};',
|
|
41
|
+
'`;',
|
|
42
|
+
"const factory = new Function('xinyu', wrappedCode);",
|
|
43
|
+
'const exports = factory(xinyuBridge);',
|
|
44
|
+
'exports.setup(xinyuBridge); // 调用插件入口',
|
|
45
|
+
].join('\n');
|
|
46
|
+
|
|
47
|
+
const CODE_LIFECYCLE_EXAMPLE = [
|
|
48
|
+
'function setup(xinyu) {',
|
|
49
|
+
" xinyu.plugin.on('onLoad', function() {",
|
|
50
|
+
" xinyu.ui.toast('插件已加载', 'success');",
|
|
51
|
+
' });',
|
|
52
|
+
'',
|
|
53
|
+
" xinyu.plugin.on('onGameInit', function() {",
|
|
54
|
+
' xinyu.game.registerAttribute({',
|
|
55
|
+
" key: 'hp', label: '生命值', type: 'number',",
|
|
56
|
+
" value: 100, icon: '❤️'",
|
|
57
|
+
' });',
|
|
58
|
+
' });',
|
|
59
|
+
'',
|
|
60
|
+
" xinyu.plugin.on('onUnload', function() {",
|
|
61
|
+
' // 资源会自动清理,通常不需要手动处理',
|
|
62
|
+
' });',
|
|
63
|
+
'}',
|
|
64
|
+
].join('\n');
|
|
65
|
+
|
|
66
|
+
const CODE_UI_SLOT = [
|
|
67
|
+
'// 在状态栏显示自定义信息',
|
|
68
|
+
"xinyu.ui.registerSlot('status-bar',",
|
|
69
|
+
' \'<div style="font-size:11px;color:var(--color-text-muted)">自定义状态</div>\',',
|
|
70
|
+
' { priority: 1 }',
|
|
71
|
+
');',
|
|
72
|
+
'',
|
|
73
|
+
'// 在顶部导航栏右侧注入元素',
|
|
74
|
+
"xinyu.ui.registerSlot('header-right',",
|
|
75
|
+
' \'<span style="font-size:12px">🔔 3</span>\',',
|
|
76
|
+
' { priority: 10 }',
|
|
77
|
+
');',
|
|
78
|
+
'',
|
|
79
|
+
'// 移除注册的内容',
|
|
80
|
+
'xinyu.ui.unregisterSlot(registrationId);',
|
|
81
|
+
].join('\n');
|
|
82
|
+
|
|
83
|
+
const CODE_UI_MODAL = [
|
|
84
|
+
'// 确认对话框(返回 Promise<boolean>)',
|
|
85
|
+
'var ok = await xinyu.ui.confirm(',
|
|
86
|
+
" '确认操作',",
|
|
87
|
+
" '确定要重置属性吗?',",
|
|
88
|
+
" { confirmText: '重置', cancelText: '取消', danger: true }",
|
|
89
|
+
');',
|
|
90
|
+
'',
|
|
91
|
+
'// 自定义模态框',
|
|
92
|
+
'var modalId = xinyu.ui.showModal({',
|
|
93
|
+
" title: '角色属性编辑',",
|
|
94
|
+
" content: '<div style=\"padding:12px\">自定义内容</div>',",
|
|
95
|
+
" width: '400px',",
|
|
96
|
+
' closable: true,',
|
|
97
|
+
' backdrop: true,',
|
|
98
|
+
" style: 'border-radius: 12px; box-shadow: 0 8px 32px rgba(0,0,0,0.2);', // 自定义模态框容器样式",
|
|
99
|
+
' actions: [',
|
|
100
|
+
" { text: '保存', primary: true, onClick: function(ctx) { /* 保存逻辑 */ ctx.close(); } },",
|
|
101
|
+
" { text: '取消', onClick: function(ctx) { ctx.close(); } }",
|
|
102
|
+
' ],',
|
|
103
|
+
' onClose: function() { /* 关闭回调 */ }',
|
|
104
|
+
'});',
|
|
105
|
+
'',
|
|
106
|
+
'// 更新模态框(无需关闭重开,可动态修改 title、content、width、style)',
|
|
107
|
+
'xinyu.ui.updateModal(modalId, {',
|
|
108
|
+
" title: '角色属性编辑(已修改)',",
|
|
109
|
+
" content: '<div style=\"padding:12px\">更新后的内容</div>',",
|
|
110
|
+
" width: '500px',",
|
|
111
|
+
" style: 'border-radius: 16px; background: #1a1a2e;'",
|
|
112
|
+
'});',
|
|
113
|
+
'',
|
|
114
|
+
'// 关闭模态框',
|
|
115
|
+
'xinyu.ui.closeModal(modalId);',
|
|
116
|
+
].join('\n');
|
|
117
|
+
|
|
118
|
+
const CODE_UI_STYLE = [
|
|
119
|
+
'// 注入自定义 CSS(使用 CSS 变量保持主题一致)',
|
|
120
|
+
'var styleId = xinyu.ui.injectStyle(`',
|
|
121
|
+
' .my-plugin-panel {',
|
|
122
|
+
' background: var(--color-bg-tertiary);',
|
|
123
|
+
' border: 1px solid var(--color-border);',
|
|
124
|
+
' border-radius: 8px;',
|
|
125
|
+
' padding: 12px;',
|
|
126
|
+
' }',
|
|
127
|
+
' .my-plugin-highlight {',
|
|
128
|
+
' color: var(--color-accent);',
|
|
129
|
+
' font-weight: bold;',
|
|
130
|
+
' }',
|
|
131
|
+
'`);',
|
|
132
|
+
'',
|
|
133
|
+
'// 需要时移除',
|
|
134
|
+
'xinyu.ui.removeStyle(styleId);',
|
|
135
|
+
].join('\n');
|
|
136
|
+
|
|
137
|
+
const CODE_UI_HOST_EVENT = [
|
|
138
|
+
'// 监听消息接收事件',
|
|
139
|
+
"xinyu.ui.onHostEvent('message:received', function(data) {",
|
|
140
|
+
" console.log('新消息:', data.message);",
|
|
141
|
+
'});',
|
|
142
|
+
'',
|
|
143
|
+
'// 监听游戏状态变更',
|
|
144
|
+
"xinyu.ui.onHostEvent('game:stateChange', function(data) {",
|
|
145
|
+
" console.log('变更的键:', data.changedKeys);",
|
|
146
|
+
'});',
|
|
147
|
+
'',
|
|
148
|
+
'// 可用事件:',
|
|
149
|
+
'// message:received / message:sent / game:stateChange',
|
|
150
|
+
'// sidebar:open / sidebar:close / input:focus / input:blur',
|
|
151
|
+
'// theme:change / plugin:enabled / plugin:disabled',
|
|
152
|
+
].join('\n');
|
|
153
|
+
|
|
154
|
+
const CODE_DOM_API = [
|
|
155
|
+
'// 创建元素',
|
|
156
|
+
"var el = xinyu.ui.dom.create('div', {",
|
|
157
|
+
" className: 'my-panel',",
|
|
158
|
+
" style: 'position:absolute;top:10px;right:10px;'",
|
|
159
|
+
"}, 'Hello World');",
|
|
160
|
+
'',
|
|
161
|
+
'// 追加到容器(支持插槽 ID)',
|
|
162
|
+
"xinyu.ui.dom.append('floating', el);",
|
|
163
|
+
'',
|
|
164
|
+
'// 也可以直接追加 HTML 字符串',
|
|
165
|
+
"xinyu.ui.dom.append('floating', '<div class=\"my-panel\">Hello</div>');",
|
|
166
|
+
'',
|
|
167
|
+
'// 更新元素',
|
|
168
|
+
'xinyu.ui.dom.update(el.id, {',
|
|
169
|
+
" style: 'position:absolute;top:20px;',",
|
|
170
|
+
" textContent: 'Updated!'",
|
|
171
|
+
'});',
|
|
172
|
+
'',
|
|
173
|
+
'// 查询元素(仅限插件自己的元素)',
|
|
174
|
+
"var results = xinyu.ui.dom.query('floating', '.my-panel');",
|
|
175
|
+
'',
|
|
176
|
+
'// 绑定事件',
|
|
177
|
+
"xinyu.ui.dom.on(el.id, 'click', function() {",
|
|
178
|
+
" xinyu.ui.toast('被点击了!', 'info');",
|
|
179
|
+
'});',
|
|
180
|
+
'',
|
|
181
|
+
'// 移除元素',
|
|
182
|
+
'xinyu.ui.dom.remove(el.id);',
|
|
183
|
+
].join('\n');
|
|
184
|
+
|
|
185
|
+
const CODE_PERMS_DEFAULT = "['slot:input-toolbar', 'style:inject', 'modal:confirm']";
|
|
186
|
+
|
|
187
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
188
|
+
const CODE_PERMS_MANIFEST = [
|
|
189
|
+
'// 在插件代码中查看自身权限',
|
|
190
|
+
'var manifest = xinyu.plugin.getManifest();',
|
|
191
|
+
'console.log(manifest.commonPermissions);',
|
|
192
|
+
'console.log(manifest.exclusivePermissions);',
|
|
193
|
+
'console.log(manifest.requiredPermissions);',
|
|
194
|
+
].join('\n');
|
|
195
|
+
|
|
196
|
+
const CODE_SLOTS_DIAGRAM = [
|
|
197
|
+
'┌──────────────────────────────────────────────────┐',
|
|
198
|
+
'│ [header-left] [header-center] [header-right] │ ← 顶部导航栏(header)',
|
|
199
|
+
'├──────────────┬───────────────────┬───────────────┤',
|
|
200
|
+
'│ │ [message-top] │ │ ← 消息上方(message-top)',
|
|
201
|
+
'│ │ │ │',
|
|
202
|
+
'│ │ Message List Area │ │',
|
|
203
|
+
'│[sidebar-left]│ │[sidebar-right]│',
|
|
204
|
+
'│ │ [message-bottom] │ │ ← 消息下方(message-bottom)',
|
|
205
|
+
'│ Left Side │ [input-above] │ Right Side │ ← 输入框正上方(input-above)',
|
|
206
|
+
'│ Bar Area │ [input-toolbar] │ Bar Area │ ← 工具栏(input-toolbar)',
|
|
207
|
+
'│ ├───────────────────┤ │',
|
|
208
|
+
'│ │ Input Area │ │',
|
|
209
|
+
'│ ├───────────────────┤ │',
|
|
210
|
+
'│ │ [status-bar] │ │ ← 底部状态栏(status-bar)',
|
|
211
|
+
'└──────────────┴───────────────────┴───────────────┘',
|
|
212
|
+
'[model] ← 模态框(由平台系统控制)',
|
|
213
|
+
'[floating] ← 浮动层(fixed 定位,自由放置)',
|
|
214
|
+
'[overlay] ← 全屏遮罩层(最高 z-index)',
|
|
215
|
+
].join('\n');
|
|
216
|
+
|
|
217
|
+
const CODE_SLOTS_PRIORITY = [
|
|
218
|
+
"xinyu.ui.registerSlot('status-bar', '插件A', { priority: 1 }); // 最前",
|
|
219
|
+
"xinyu.ui.registerSlot('status-bar', '插件B', { priority: 10 }); // 中间",
|
|
220
|
+
"xinyu.ui.registerSlot('status-bar', '插件C'); // 最后(默认100)",
|
|
221
|
+
].join('\n');
|
|
222
|
+
|
|
223
|
+
const CODE_EXAMPLE_1 = [
|
|
224
|
+
'function setup(xinyu) {',
|
|
225
|
+
' // 在状态栏显示游戏时间',
|
|
226
|
+
' var startTime = Date.now();',
|
|
227
|
+
'',
|
|
228
|
+
" xinyu.ui.registerSlot('status-bar',",
|
|
229
|
+
' \'<div id="game-timer" style="font-size:11px;color:var(--color-text-muted)">\' +',
|
|
230
|
+
" '⏱️ 游戏时间: 0分0秒</div>',",
|
|
231
|
+
' { priority: 1 }',
|
|
232
|
+
' );',
|
|
233
|
+
'',
|
|
234
|
+
' // 每秒更新',
|
|
235
|
+
' setInterval(function() {',
|
|
236
|
+
' var elapsed = Math.floor((Date.now() - startTime) / 1000);',
|
|
237
|
+
' var min = Math.floor(elapsed / 60);',
|
|
238
|
+
' var sec = elapsed % 60;',
|
|
239
|
+
" var el = document.querySelector('#game-timer');",
|
|
240
|
+
" if (el) el.textContent = '⏱️ 游戏时间: ' + min + '分' + sec + '秒';",
|
|
241
|
+
' }, 1000);',
|
|
242
|
+
'}',
|
|
243
|
+
].join('\n');
|
|
244
|
+
|
|
245
|
+
const CODE_EXAMPLE_2 = [
|
|
246
|
+
'function setup(xinyu) {',
|
|
247
|
+
' var msgCount = 0;',
|
|
248
|
+
'',
|
|
249
|
+
" xinyu.ui.onHostEvent('message:received', function() {",
|
|
250
|
+
' msgCount++;',
|
|
251
|
+
' // 更新状态栏',
|
|
252
|
+
" xinyu.ui.registerSlot('status-bar',",
|
|
253
|
+
' \'<div style="font-size:11px;color:var(--color-text-muted)">\' +',
|
|
254
|
+
" '💬 已收到 ' + msgCount + ' 条消息</div>',",
|
|
255
|
+
' { priority: 2 }',
|
|
256
|
+
' );',
|
|
257
|
+
' });',
|
|
258
|
+
'',
|
|
259
|
+
' // 注册一个查看统计的按钮',
|
|
260
|
+
' xinyu.ui.registerInputToolbarButton({',
|
|
261
|
+
" id: 'msg-stats',",
|
|
262
|
+
" label: '统计',",
|
|
263
|
+
" icon: '📊',",
|
|
264
|
+
' order: 20,',
|
|
265
|
+
' onClick: function() {',
|
|
266
|
+
' xinyu.ui.showModal({',
|
|
267
|
+
" title: '📊 消息统计',",
|
|
268
|
+
' content: \'<div style="padding:16px;text-align:center">\' +',
|
|
269
|
+
' \'<div style="font-size:32px;font-weight:bold;color:var(--color-accent)">\' + msgCount + \'</div>\' +',
|
|
270
|
+
" '<div style=\"color:var(--color-text-muted);margin-top:4px\">已接收消息总数</div></div>',",
|
|
271
|
+
" width: '300px'",
|
|
272
|
+
' });',
|
|
273
|
+
' }',
|
|
274
|
+
' });',
|
|
275
|
+
'}',
|
|
276
|
+
].join('\n');
|
|
277
|
+
|
|
278
|
+
const CODE_EXAMPLE_3 = [
|
|
279
|
+
'function setup(xinyu) {',
|
|
280
|
+
' // 注入样式',
|
|
281
|
+
' xinyu.ui.injectStyle(`',
|
|
282
|
+
' .quick-menu {',
|
|
283
|
+
' position: fixed; bottom: 80px; right: 20px;',
|
|
284
|
+
' display: flex; flex-direction: column; gap: 8px;',
|
|
285
|
+
' z-index: 50;',
|
|
286
|
+
' }',
|
|
287
|
+
' .quick-menu-btn {',
|
|
288
|
+
' width: 44px; height: 44px; border-radius: 50%;',
|
|
289
|
+
' background: var(--color-bg-secondary);',
|
|
290
|
+
' border: 1px solid var(--color-border);',
|
|
291
|
+
' color: var(--color-text-secondary);',
|
|
292
|
+
' font-size: 18px; cursor: pointer;',
|
|
293
|
+
' display: flex; align-items: center; justify-content: center;',
|
|
294
|
+
' transition: all 0.2s;',
|
|
295
|
+
' }',
|
|
296
|
+
' .quick-menu-btn:hover {',
|
|
297
|
+
' background: var(--color-accent); color: #fff;',
|
|
298
|
+
' transform: scale(1.1);',
|
|
299
|
+
' }',
|
|
300
|
+
' `);',
|
|
301
|
+
'',
|
|
302
|
+
' // 在浮动层注入按钮',
|
|
303
|
+
" xinyu.ui.registerSlot('floating',",
|
|
304
|
+
" '<div class=\"quick-menu\">' +",
|
|
305
|
+
" '<button class=\"quick-menu-btn\" onclick=\"document.dispatchEvent(new CustomEvent(\\'plugin-action\\', {detail:\\'look\\'}))\">👁️</button>' +",
|
|
306
|
+
" '<button class=\"quick-menu-btn\" onclick=\"document.dispatchEvent(new CustomEvent(\\'plugin-action\\', {detail:\\'talk\\'}))\">💬</button>' +",
|
|
307
|
+
" '<button class=\"quick-menu-btn\" onclick=\"document.dispatchEvent(new CustomEvent(\\'plugin-action\\', {detail:\\'rest\\'}))\">🏕️</button>' +",
|
|
308
|
+
" '</div>',",
|
|
309
|
+
' { priority: 1 }',
|
|
310
|
+
' );',
|
|
311
|
+
'}',
|
|
312
|
+
].join('\n');
|
|
313
|
+
|
|
314
|
+
const CODE_DEBUG_EXAMPLE = [
|
|
315
|
+
'function setup(xinyu) {',
|
|
316
|
+
" console.log('插件 manifest:', xinyu.plugin.getManifest());",
|
|
317
|
+
" console.log('插件配置:', xinyu.plugin.config.get());",
|
|
318
|
+
'',
|
|
319
|
+
' // 查看游戏状态',
|
|
320
|
+
" console.log('游戏状态:', xinyu.game.getState());",
|
|
321
|
+
" console.log('游戏属性:', xinyu.game.getAttributes());",
|
|
322
|
+
'}',
|
|
323
|
+
].join('\n');
|
|
324
|
+
|
|
325
|
+
// ==================== 新增代码块常量 ====================
|
|
326
|
+
|
|
327
|
+
// --- CSS 变量章节 ---
|
|
328
|
+
const CODE_CSS_VAR_INLINE = [
|
|
329
|
+
'// 在内联 style 中使用 CSS 变量',
|
|
330
|
+
"xinyu.ui.registerSlot('status-bar',",
|
|
331
|
+
" '<div style=\"color:var(--color-text-primary);background:var(--color-bg-secondary);",
|
|
332
|
+
" padding:4px 8px;border-radius:var(--border-radius);",
|
|
333
|
+
" border:1px solid var(--color-border)\">自定义状态栏</div>',",
|
|
334
|
+
' { priority: 1 }',
|
|
335
|
+
');',
|
|
336
|
+
'',
|
|
337
|
+
'// DOM 沙箱创建元素时使用',
|
|
338
|
+
"var el = xinyu.ui.dom.create('div', {",
|
|
339
|
+
" style: 'background:var(--color-bg-tertiary);color:var(--color-accent);",
|
|
340
|
+
" padding:8px;border-radius:var(--border-radius)'",
|
|
341
|
+
"}, '使用 CSS 变量的元素');",
|
|
342
|
+
].join('\n');
|
|
343
|
+
|
|
344
|
+
const CODE_CSS_VAR_INJECT = [
|
|
345
|
+
'// 在 injectStyle 注入的 CSS 中使用 CSS 变量',
|
|
346
|
+
'xinyu.ui.injectStyle(`',
|
|
347
|
+
' .my-plugin-card {',
|
|
348
|
+
' background: var(--color-bg-secondary);',
|
|
349
|
+
' border: 1px solid var(--color-border);',
|
|
350
|
+
' border-radius: var(--border-radius);',
|
|
351
|
+
' padding: 16px;',
|
|
352
|
+
' box-shadow: 0 2px 8px var(--color-shadow);',
|
|
353
|
+
' }',
|
|
354
|
+
' .my-plugin-title {',
|
|
355
|
+
' color: var(--color-text-primary);',
|
|
356
|
+
' font-family: var(--font-heading);',
|
|
357
|
+
' font-size: 16px;',
|
|
358
|
+
' margin-bottom: 8px;',
|
|
359
|
+
' }',
|
|
360
|
+
' .my-plugin-desc {',
|
|
361
|
+
' color: var(--color-text-secondary);',
|
|
362
|
+
' font-family: var(--font-body);',
|
|
363
|
+
' font-size: 13px;',
|
|
364
|
+
' line-height: 1.6;',
|
|
365
|
+
' }',
|
|
366
|
+
' .my-plugin-btn {',
|
|
367
|
+
' background: var(--color-accent);',
|
|
368
|
+
' color: var(--color-text-primary);',
|
|
369
|
+
' border: none;',
|
|
370
|
+
' border-radius: var(--border-radius);',
|
|
371
|
+
' padding: 8px 16px;',
|
|
372
|
+
' cursor: pointer;',
|
|
373
|
+
' transition: background 0.2s;',
|
|
374
|
+
' }',
|
|
375
|
+
' .my-plugin-btn:hover {',
|
|
376
|
+
' background: var(--color-accent-hover);',
|
|
377
|
+
' }',
|
|
378
|
+
'`);',
|
|
379
|
+
].join('\n');
|
|
380
|
+
|
|
381
|
+
const CODE_CSS_VAR_CUSTOM = [
|
|
382
|
+
'// 创建自定义 CSS 变量(建议使用插件前缀避免冲突)',
|
|
383
|
+
'xinyu.ui.injectStyle(`',
|
|
384
|
+
' :root {',
|
|
385
|
+
' --my-dice-primary: #e74c3c;',
|
|
386
|
+
' --my-dice-secondary: #c0392b;',
|
|
387
|
+
' --my-dice-bg: rgba(231, 76, 60, 0.1);',
|
|
388
|
+
' --my-dice-radius: 8px;',
|
|
389
|
+
' }',
|
|
390
|
+
' .dice-result {',
|
|
391
|
+
' background: var(--my-dice-bg);',
|
|
392
|
+
' color: var(--my-dice-primary);',
|
|
393
|
+
' border: 2px solid var(--my-dice-secondary);',
|
|
394
|
+
' border-radius: var(--my-dice-radius);',
|
|
395
|
+
' padding: 12px;',
|
|
396
|
+
' font-size: 24px;',
|
|
397
|
+
' font-weight: bold;',
|
|
398
|
+
' text-align: center;',
|
|
399
|
+
' }',
|
|
400
|
+
'`);',
|
|
401
|
+
'',
|
|
402
|
+
'// 使用自定义变量',
|
|
403
|
+
"xinyu.ui.registerSlot('status-bar',",
|
|
404
|
+
" '<div class=\"dice-result\">🎲 6</div>',",
|
|
405
|
+
' { priority: 5 }',
|
|
406
|
+
');',
|
|
407
|
+
].join('\n');
|
|
408
|
+
|
|
409
|
+
// --- 插件存储章节 ---
|
|
410
|
+
const CODE_STORAGE_PLUGIN = [
|
|
411
|
+
'// 使用 xinyu.plugin.storage(推荐方式)',
|
|
412
|
+
'function setup(xinyu) {',
|
|
413
|
+
" // 读取用户偏好",
|
|
414
|
+
' var theme = xinyu.plugin.storage.get("theme") || "auto";',
|
|
415
|
+
' var fontSize = xinyu.plugin.storage.get("fontSize") || 14;',
|
|
416
|
+
'',
|
|
417
|
+
' // 保存用户偏好',
|
|
418
|
+
' xinyu.plugin.storage.set("theme", "dark");',
|
|
419
|
+
' xinyu.plugin.storage.set("fontSize", 16);',
|
|
420
|
+
'',
|
|
421
|
+
' // 也可以存储 JSON 对象(自动序列化)',
|
|
422
|
+
' xinyu.plugin.storage.set("userSettings", {',
|
|
423
|
+
' theme: "dark",',
|
|
424
|
+
' fontSize: 16,',
|
|
425
|
+
' language: "zh-CN",',
|
|
426
|
+
' notifications: true',
|
|
427
|
+
' });',
|
|
428
|
+
'',
|
|
429
|
+
' // 读取 JSON 对象',
|
|
430
|
+
' var settings = xinyu.plugin.storage.get("userSettings");',
|
|
431
|
+
' console.log(settings.theme); // "dark"',
|
|
432
|
+
'',
|
|
433
|
+
' // 删除某个键',
|
|
434
|
+
' xinyu.plugin.storage.remove("theme");',
|
|
435
|
+
'',
|
|
436
|
+
' // 获取所有键名',
|
|
437
|
+
' var allKeys = xinyu.plugin.storage.keys();',
|
|
438
|
+
" console.log('存储的键:', allKeys);",
|
|
439
|
+
'}',
|
|
440
|
+
].join('\n');
|
|
441
|
+
|
|
442
|
+
const CODE_STORAGE_GAME = [
|
|
443
|
+
'// 使用 xinyu.game.setState / getState 管理游戏状态',
|
|
444
|
+
'function setup(xinyu) {',
|
|
445
|
+
" xinyu.plugin.on('onGameInit', function() {",
|
|
446
|
+
' // 初始化游戏状态',
|
|
447
|
+
' xinyu.game.setState({',
|
|
448
|
+
' gold: 100,',
|
|
449
|
+
' level: 1,',
|
|
450
|
+
' exp: 0,',
|
|
451
|
+
' currentScene: "village"',
|
|
452
|
+
' });',
|
|
453
|
+
' });',
|
|
454
|
+
'',
|
|
455
|
+
' // 读取游戏状态',
|
|
456
|
+
' var state = xinyu.game.getState();',
|
|
457
|
+
" console.log('金币:', state.gold);",
|
|
458
|
+
'',
|
|
459
|
+
' // 更新游戏状态(会触发 game:stateChange 事件)',
|
|
460
|
+
' xinyu.game.setState({ gold: state.gold + 50 });',
|
|
461
|
+
'',
|
|
462
|
+
' // 监听游戏状态变更(所有插件都能收到)',
|
|
463
|
+
" xinyu.ui.onHostEvent('game:stateChange', function(data) {",
|
|
464
|
+
" console.log('变更的键:', data.changedKeys);",
|
|
465
|
+
' // data.changedKeys = ["gold"]',
|
|
466
|
+
' });',
|
|
467
|
+
'}',
|
|
468
|
+
].join('\n');
|
|
469
|
+
|
|
470
|
+
const CODE_STORAGE_ATTR = [
|
|
471
|
+
'// 使用 registerAttribute / setAttribute 管理游戏属性',
|
|
472
|
+
'function setup(xinyu) {',
|
|
473
|
+
" xinyu.plugin.on('onGameInit', function() {",
|
|
474
|
+
' // 注册属性(会显示在游戏属性面板中)',
|
|
475
|
+
' xinyu.game.registerAttribute({',
|
|
476
|
+
" key: 'hp',",
|
|
477
|
+
" label: '生命值',",
|
|
478
|
+
" type: 'number',",
|
|
479
|
+
' value: 100,',
|
|
480
|
+
" icon: '❤️',",
|
|
481
|
+
" color: '#e74c3c'",
|
|
482
|
+
' });',
|
|
483
|
+
'',
|
|
484
|
+
' xinyu.game.registerAttribute({',
|
|
485
|
+
" key: 'mp',",
|
|
486
|
+
" label: '魔法值',",
|
|
487
|
+
" type: 'number',",
|
|
488
|
+
' value: 50,',
|
|
489
|
+
" icon: '💎',",
|
|
490
|
+
" color: '#3498db'",
|
|
491
|
+
' });',
|
|
492
|
+
'',
|
|
493
|
+
' xinyu.game.registerAttribute({',
|
|
494
|
+
" key: 'status',",
|
|
495
|
+
" label: '状态',",
|
|
496
|
+
" type: 'string',",
|
|
497
|
+
" value: '正常',",
|
|
498
|
+
" icon: '⭐'",
|
|
499
|
+
' });',
|
|
500
|
+
' });',
|
|
501
|
+
'',
|
|
502
|
+
' // 更新属性值',
|
|
503
|
+
' xinyu.game.setAttribute("hp", 80);',
|
|
504
|
+
'',
|
|
505
|
+
' // 读取所有属性',
|
|
506
|
+
' var attrs = xinyu.game.getAttributes();',
|
|
507
|
+
" console.log('HP:', attrs.hp.value); // 80",
|
|
508
|
+
'}',
|
|
509
|
+
].join('\n');
|
|
510
|
+
|
|
511
|
+
// --- AI 拦截章节 ---
|
|
512
|
+
const CODE_AI_PROMPT = [
|
|
513
|
+
'// 四种 AI 拦截器示例',
|
|
514
|
+
'function setup(xinyu) {',
|
|
515
|
+
'',
|
|
516
|
+
' // 1. Prompt 构建拦截 - 注入 NPC 人格',
|
|
517
|
+
' xinyu.ai.onPromptBuild(function(prompt, worldSetting) {',
|
|
518
|
+
' return prompt + "\\n\\n[系统指令] 你现在扮演酒馆老板老王,说话粗犷豪爽。";',
|
|
519
|
+
' });',
|
|
520
|
+
'',
|
|
521
|
+
' // 2. 发送前拦截 - 修改消息内容',
|
|
522
|
+
' xinyu.ai.onBeforeSend(function(messages) {',
|
|
523
|
+
' // messages: [{ role: "user", content: "..." }, ...]',
|
|
524
|
+
' return messages; // 返回修改后的消息数组',
|
|
525
|
+
' });',
|
|
526
|
+
'',
|
|
527
|
+
' // 3. 接收后拦截 - 内容格式化',
|
|
528
|
+
' xinyu.ai.onAfterReceive(function(content) {',
|
|
529
|
+
' // 移除多余的空行',
|
|
530
|
+
' return content.replace(/\\n{3,}/g, "\\n\\n");',
|
|
531
|
+
' });',
|
|
532
|
+
'',
|
|
533
|
+
' // 4. 请求配置拦截 - 动态调整模型参数',
|
|
534
|
+
' xinyu.ai.onRequestConfig(function(config) {',
|
|
535
|
+
' config.model = "gpt-4";',
|
|
536
|
+
' return config;',
|
|
537
|
+
' });',
|
|
538
|
+
'}',
|
|
539
|
+
].join('\n');
|
|
540
|
+
|
|
541
|
+
const CODE_AI_PIPELINE = [
|
|
542
|
+
'// 管道模式:多个插件的拦截器按注册顺序依次执行',
|
|
543
|
+
'//',
|
|
544
|
+
'// 假设有三个插件都注册了 onPromptBuild:',
|
|
545
|
+
'//',
|
|
546
|
+
'// 原始 Prompt:',
|
|
547
|
+
'// "你是一个奇幻世界的 AI 导游..."',
|
|
548
|
+
'//',
|
|
549
|
+
'// Plugin A (NPC 人格插件) onPromptBuild:',
|
|
550
|
+
'// 输入: "你是一个奇幻世界的 AI 导游..."',
|
|
551
|
+
'// 输出: "你是一个奇幻世界的 AI 导游...\\n[你现在是酒馆老板老王]"',
|
|
552
|
+
'//',
|
|
553
|
+
'// Plugin B (规则注入插件) onPromptBuild:',
|
|
554
|
+
'// 输入: "你是一个奇幻世界的 AI 导游...\\n[你现在是酒馆老板老王]"',
|
|
555
|
+
'// 输出: "你是一个奇幻世界的 AI 导游...\\n[你现在是酒馆老板老王]\\n[规则: 战斗判定使用 d20]"',
|
|
556
|
+
'//',
|
|
557
|
+
'// Plugin C (状态注入插件) onPromptBuild:',
|
|
558
|
+
'// 输入: "...\\n[规则: 战斗判定使用 d20]"',
|
|
559
|
+
'// 输出: "...\\n[规则: 战斗判定使用 d20]\\n[当前玩家HP: 80/100]"',
|
|
560
|
+
'//',
|
|
561
|
+
'// 最终发送给 AI 的 Prompt = Plugin C 的输出',
|
|
562
|
+
'//',
|
|
563
|
+
'// 注意:每个 handler 应该返回修改后的值,不要返回 undefined',
|
|
564
|
+
].join('\n');
|
|
565
|
+
|
|
566
|
+
const CODE_AI_NPC = [
|
|
567
|
+
'// 完整示例:NPC 人格注入插件',
|
|
568
|
+
'function setup(xinyu) {',
|
|
569
|
+
' var npcData = {',
|
|
570
|
+
' name: "酒馆老板·老王",',
|
|
571
|
+
' personality: "粗犷豪爽,喜欢讲冒险故事,偶尔会推销自己的特酿麦酒",',
|
|
572
|
+
' speech: "说话带点江湖气,用词朴实,偶尔夹杂方言"',
|
|
573
|
+
' };',
|
|
574
|
+
'',
|
|
575
|
+
' // 在 Prompt 中注入 NPC 人格',
|
|
576
|
+
' xinyu.ai.onPromptBuild(function(prompt) {',
|
|
577
|
+
' var npcPrompt = "\\n\\n[角色扮演指令]\\n"',
|
|
578
|
+
' + "你现在是 " + npcData.name + "。\\n"',
|
|
579
|
+
' + "性格: " + npcData.personality + "\\n"',
|
|
580
|
+
' + "说话风格: " + npcData.speech + "\\n"',
|
|
581
|
+
' + "请始终保持角色,不要脱离人设。";',
|
|
582
|
+
' return prompt + npcPrompt;',
|
|
583
|
+
' });',
|
|
584
|
+
'',
|
|
585
|
+
' // 在状态栏显示当前 NPC',
|
|
586
|
+
" xinyu.ui.registerSlot('status-bar',",
|
|
587
|
+
' \'<div style="font-size:11px;color:var(--color-text-muted)">\' +',
|
|
588
|
+
" '🎭 当前 NPC: ' + npcData.name + '</div>',",
|
|
589
|
+
' { priority: 1 }',
|
|
590
|
+
' );',
|
|
591
|
+
'',
|
|
592
|
+
' // 注册切换 NPC 的按钮',
|
|
593
|
+
' xinyu.ui.registerInputToolbarButton({',
|
|
594
|
+
" id: 'switch-npc',",
|
|
595
|
+
" label: '切换NPC',",
|
|
596
|
+
" icon: '🎭',",
|
|
597
|
+
' order: 15,',
|
|
598
|
+
' onClick: function() {',
|
|
599
|
+
' xinyu.ui.showModal({',
|
|
600
|
+
" title: '🎭 切换 NPC',",
|
|
601
|
+
" content: '<div style=\"padding:12px\">选择要扮演的 NPC</div>',",
|
|
602
|
+
" width: '350px'",
|
|
603
|
+
' });',
|
|
604
|
+
' }',
|
|
605
|
+
' });',
|
|
606
|
+
'}',
|
|
607
|
+
].join('\n');
|
|
608
|
+
|
|
609
|
+
// --- 插件配置章节 ---
|
|
610
|
+
const CODE_CONFIG_SCHEMA = [
|
|
611
|
+
'// configSchema 定义示例(骰子插件)',
|
|
612
|
+
'var configSchema = [',
|
|
613
|
+
' {',
|
|
614
|
+
" key: 'defaultDice',",
|
|
615
|
+
" label: '默认骰子面数',",
|
|
616
|
+
" type: 'select',",
|
|
617
|
+
" defaultValue: '20',",
|
|
618
|
+
" description: '掷骰时使用的默认面数',",
|
|
619
|
+
" options: [",
|
|
620
|
+
" { value: '4', label: 'D4 (四面骰)' },",
|
|
621
|
+
" { value: '6', label: 'D6 (六面骰)' },",
|
|
622
|
+
" { value: '8', label: 'D8 (八面骰)' },",
|
|
623
|
+
" { value: '10', label: 'D10 (十面骰)' },",
|
|
624
|
+
" { value: '12', label: 'D12 (十二面骰)' },",
|
|
625
|
+
" { value: '20', label: 'D20 (二十面骰)' },",
|
|
626
|
+
" { value: '100', label: 'D100 (百面骰)' }",
|
|
627
|
+
' ]',
|
|
628
|
+
' },',
|
|
629
|
+
' {',
|
|
630
|
+
" key: 'showAnimation',",
|
|
631
|
+
" label: '显示掷骰动画',",
|
|
632
|
+
" type: 'boolean',",
|
|
633
|
+
" defaultValue: true,",
|
|
634
|
+
" description: '掷骰时是否显示动画效果'",
|
|
635
|
+
' },',
|
|
636
|
+
' {',
|
|
637
|
+
" key: 'customModifier',",
|
|
638
|
+
" label: '固定修正值',",
|
|
639
|
+
" type: 'number',",
|
|
640
|
+
" defaultValue: 0,",
|
|
641
|
+
" description: '每次掷骰后自动加减的固定值'",
|
|
642
|
+
' },',
|
|
643
|
+
' {',
|
|
644
|
+
" key: 'rollCommand',",
|
|
645
|
+
" label: '掷骰指令前缀',",
|
|
646
|
+
" type: 'text',",
|
|
647
|
+
" defaultValue: '/roll',",
|
|
648
|
+
" description: '触发掷骰的聊天指令'",
|
|
649
|
+
' },',
|
|
650
|
+
' {',
|
|
651
|
+
" key: 'diceNote',",
|
|
652
|
+
" label: '骰子备注',",
|
|
653
|
+
" type: 'textarea',",
|
|
654
|
+
" defaultValue: '',",
|
|
655
|
+
" description: '自定义的骰子规则说明'",
|
|
656
|
+
' }',
|
|
657
|
+
'];',
|
|
658
|
+
].join('\n');
|
|
659
|
+
|
|
660
|
+
const CODE_CONFIG_READ = [
|
|
661
|
+
'// 在插件代码中读取配置',
|
|
662
|
+
'function setup(xinyu) {',
|
|
663
|
+
'',
|
|
664
|
+
' // 获取全部配置',
|
|
665
|
+
' var config = xinyu.plugin.config.get();',
|
|
666
|
+
" console.log('默认骰子:', config.defaultDice);",
|
|
667
|
+
" console.log('显示动画:', config.showAnimation);",
|
|
668
|
+
'',
|
|
669
|
+
' // 获取单个配置值',
|
|
670
|
+
' var dice = xinyu.plugin.config.get("defaultDice");',
|
|
671
|
+
' var modifier = xinyu.plugin.config.get("customModifier");',
|
|
672
|
+
'',
|
|
673
|
+
' // 配置值来自用户在插件管理页面中的设置',
|
|
674
|
+
' // 如果用户未修改,则使用 defaultValue',
|
|
675
|
+
' var total = xinyu.utils.rollDice("1d" + dice).total + modifier;',
|
|
676
|
+
" xinyu.ui.toast('掷骰结果: ' + total, 'info');",
|
|
677
|
+
'}',
|
|
678
|
+
].join('\n');
|
|
679
|
+
|
|
680
|
+
const CODE_CONFIG_FULL = [
|
|
681
|
+
'// 完整示例:带配置的骰子插件',
|
|
682
|
+
'function setup(xinyu) {',
|
|
683
|
+
' // 读取配置',
|
|
684
|
+
' var config = xinyu.plugin.config.get();',
|
|
685
|
+
' var dice = config.defaultDice || "20";',
|
|
686
|
+
' var modifier = config.customModifier || 0;',
|
|
687
|
+
' var showAnim = config.showAnimation !== false;',
|
|
688
|
+
'',
|
|
689
|
+
' // 注册掷骰指令',
|
|
690
|
+
' xinyu.chat.registerCommand({',
|
|
691
|
+
" name: config.rollCommand || '/roll',",
|
|
692
|
+
" description: '掷骰子',",
|
|
693
|
+
' handler: function(args) {',
|
|
694
|
+
' var notation = args[0] || ("1d" + dice);',
|
|
695
|
+
' var result = xinyu.utils.rollDice(notation);',
|
|
696
|
+
' var total = result.total + modifier;',
|
|
697
|
+
'',
|
|
698
|
+
' var msg = "🎲 " + notation;',
|
|
699
|
+
' if (modifier !== 0) msg += (modifier > 0 ? "+" : "") + modifier;',
|
|
700
|
+
' msg += " = " + total;',
|
|
701
|
+
'',
|
|
702
|
+
' xinyu.chat.insertSystemMessage(msg);',
|
|
703
|
+
' return msg;',
|
|
704
|
+
' }',
|
|
705
|
+
' });',
|
|
706
|
+
'',
|
|
707
|
+
' // 在状态栏显示当前骰子设置',
|
|
708
|
+
" xinyu.ui.registerSlot('status-bar',",
|
|
709
|
+
' \'<div style="font-size:11px;color:var(--color-text-muted)">\' +',
|
|
710
|
+
" '🎲 D' + dice + (modifier ? (modifier > 0 ? '+' : '') + modifier : '') + '</div>',",
|
|
711
|
+
' { priority: 5 }',
|
|
712
|
+
' );',
|
|
713
|
+
'}',
|
|
714
|
+
].join('\n');
|
|
715
|
+
|
|
716
|
+
// --- 导入导出章节 ---
|
|
717
|
+
const CODE_EXPORT_FORMAT = [
|
|
718
|
+
'// 导出格式示例',
|
|
719
|
+
'//',
|
|
720
|
+
'// 单个插件导出(直接返回插件 JSON):',
|
|
721
|
+
'{',
|
|
722
|
+
' "id": "my-dice-plugin",',
|
|
723
|
+
' "name": "我的骰子插件",',
|
|
724
|
+
' "version": "1.0.0",',
|
|
725
|
+
' "type": "game-mechanics",',
|
|
726
|
+
' "description": "一个简单的骰子插件",',
|
|
727
|
+
' "author": "作者名",',
|
|
728
|
+
' "code": "function setup(xinyu) { ... }",',
|
|
729
|
+
' "configSchema": [...],',
|
|
730
|
+
' "commonPermissions": ["slot:status-bar", "style:inject"],',
|
|
731
|
+
' "exclusivePermissions": [],',
|
|
732
|
+
' "requiredPermissions": []',
|
|
733
|
+
'}',
|
|
734
|
+
'',
|
|
735
|
+
'// 批量导出(xinyu-extension-v1 格式):',
|
|
736
|
+
'{',
|
|
737
|
+
' "format": "xinyu-extension-v1",',
|
|
738
|
+
' "exportedAt": "2026-04-26T12:00:00.000Z",',
|
|
739
|
+
' "plugins": [',
|
|
740
|
+
' { "id": "plugin-a", ... },',
|
|
741
|
+
' { "id": "plugin-b", ... }',
|
|
742
|
+
' ]',
|
|
743
|
+
'}',
|
|
744
|
+
].join('\n');
|
|
745
|
+
|
|
746
|
+
const CODE_IMPORT_FORMAT = [
|
|
747
|
+
'// 导入格式(支持 3 种)',
|
|
748
|
+
'//',
|
|
749
|
+
'// 格式 1:完整导出格式',
|
|
750
|
+
'{',
|
|
751
|
+
' "format": "xinyu-extension-v1",',
|
|
752
|
+
' "exportedAt": "2026-04-26T12:00:00.000Z",',
|
|
753
|
+
' "plugins": [',
|
|
754
|
+
' {',
|
|
755
|
+
' "id": "my-plugin",',
|
|
756
|
+
' "name": "我的插件",',
|
|
757
|
+
' "version": "1.0.0",',
|
|
758
|
+
' "type": "game-mechanics",',
|
|
759
|
+
' "description": "插件描述",',
|
|
760
|
+
' "author": "作者名",',
|
|
761
|
+
' "code": "function setup(xinyu) { ... }",',
|
|
762
|
+
' "configSchema": [...],',
|
|
763
|
+
' "commonPermissions": [...],',
|
|
764
|
+
' "exclusivePermissions": [...],',
|
|
765
|
+
' "requiredPermissions": [...]',
|
|
766
|
+
' }',
|
|
767
|
+
' ]',
|
|
768
|
+
'}',
|
|
769
|
+
'',
|
|
770
|
+
'// 格式 2:直接数组',
|
|
771
|
+
'[',
|
|
772
|
+
' { "id": "plugin-a", "name": "插件A", "code": "..." },',
|
|
773
|
+
' { "id": "plugin-b", "name": "插件B", "code": "..." }',
|
|
774
|
+
']',
|
|
775
|
+
'',
|
|
776
|
+
'// 格式 3:单个插件',
|
|
777
|
+
'{',
|
|
778
|
+
' "id": "my-plugin",',
|
|
779
|
+
' "name": "我的插件",',
|
|
780
|
+
' "code": "function setup(xinyu) { ... }"',
|
|
781
|
+
'}',
|
|
782
|
+
].join('\n');
|
|
783
|
+
|
|
784
|
+
// --- 安全章节 ---
|
|
785
|
+
const CODE_SECURITY_TAGS = [
|
|
786
|
+
'// 允许的 HTML 标签完整列表(46 个)',
|
|
787
|
+
'//',
|
|
788
|
+
'// 文本结构:div, span, p, h1, h2, h3, h4, h5, h6,',
|
|
789
|
+
'// br, hr, pre, code, blockquote',
|
|
790
|
+
'//',
|
|
791
|
+
'// 列表:ul, ol, li, dl, dt, dd',
|
|
792
|
+
'//',
|
|
793
|
+
'// 表格:table, thead, tbody, tfoot, tr, th, td, caption, colgroup, col',
|
|
794
|
+
'//',
|
|
795
|
+
'// 表单(仅展示,无功能):label, fieldset, legend',
|
|
796
|
+
'//',
|
|
797
|
+
'// 媒体:img, figure, figcaption, video, audio, source, canvas',
|
|
798
|
+
'//',
|
|
799
|
+
'// 语义化:article, section, aside, header, footer, nav, main,',
|
|
800
|
+
'// details, summary, time, mark, abbr, small, strong, em, b, i,',
|
|
801
|
+
'// u, s, sub, sup, del, ins',
|
|
802
|
+
'//',
|
|
803
|
+
'// 禁止的标签:script, style, link, meta, iframe,',
|
|
804
|
+
'// object, embed, applet, form, input, select, textarea, button',
|
|
805
|
+
'//',
|
|
806
|
+
'// 禁止的属性前缀:on*, srcdoc, formaction, xlink:href',
|
|
807
|
+
'// 禁止的协议:javascript:',
|
|
808
|
+
].join('\n');
|
|
809
|
+
|
|
810
|
+
// ==================== 样式常量 ====================
|
|
811
|
+
|
|
812
|
+
const codeStyle: React.CSSProperties = {
|
|
813
|
+
backgroundColor: 'var(--color-bg-tertiary)',
|
|
814
|
+
padding: '1px 5px',
|
|
815
|
+
borderRadius: '4px',
|
|
816
|
+
fontSize: '12px',
|
|
817
|
+
fontFamily: 'Consolas',
|
|
818
|
+
color: 'var(--color-accent)',
|
|
819
|
+
};
|
|
820
|
+
|
|
821
|
+
const preStyle: React.CSSProperties = {
|
|
822
|
+
backgroundColor: 'var(--color-bg-secondary)',
|
|
823
|
+
border: '1px solid var(--color-border)',
|
|
824
|
+
borderRadius: '8px',
|
|
825
|
+
padding: '12px 16px',
|
|
826
|
+
fontSize: '12px',
|
|
827
|
+
fontFamily: 'Consolas',
|
|
828
|
+
color: 'var(--color-text-secondary)',
|
|
829
|
+
whiteSpace: 'pre-wrap',
|
|
830
|
+
wordBreak: 'break-all',
|
|
831
|
+
overflowX: 'auto',
|
|
832
|
+
lineHeight: '1.6',
|
|
833
|
+
};
|
|
834
|
+
|
|
835
|
+
const linkStyle: React.CSSProperties = {
|
|
836
|
+
color: 'var(--color-accent)',
|
|
837
|
+
cursor: 'pointer',
|
|
838
|
+
borderBottom: '1px dashed var(--color-accent)',
|
|
839
|
+
paddingBottom: '1px',
|
|
840
|
+
borderBottomColor: 'var(--color-accent)',
|
|
841
|
+
transition: 'color 0.15s, border-color 0.15s, background-color 0.15s',
|
|
842
|
+
};
|
|
843
|
+
|
|
844
|
+
const linkHoverStyle: React.CSSProperties = {
|
|
845
|
+
color: 'var(--color-accent-hover)',
|
|
846
|
+
borderBottomColor: 'var(--color-accent-hover)',
|
|
847
|
+
backgroundColor: 'var(--color-bg-tertiary)',
|
|
848
|
+
};
|
|
849
|
+
|
|
850
|
+
// ==================== 类型定义 ====================
|
|
851
|
+
|
|
852
|
+
interface SubTitle {
|
|
853
|
+
id: string;
|
|
854
|
+
title: string;
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
interface Section {
|
|
858
|
+
id: string;
|
|
859
|
+
title: string;
|
|
860
|
+
icon: string;
|
|
861
|
+
subtitles?: SubTitle[];
|
|
862
|
+
content: React.ReactNode;
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
// ==================== 内部导航链接组件 ====================
|
|
866
|
+
|
|
867
|
+
function NavLink({ sectionId, children, style }: { sectionId: string; children: React.ReactNode; style?: React.CSSProperties }) {
|
|
868
|
+
const [hovered, setHovered] = useState(false);
|
|
869
|
+
return (
|
|
870
|
+
<span
|
|
871
|
+
style={{
|
|
872
|
+
...linkStyle,
|
|
873
|
+
...(hovered ? linkHoverStyle : {}),
|
|
874
|
+
...style,
|
|
875
|
+
}}
|
|
876
|
+
onClick={() => document.getElementById('section-' + sectionId)?.scrollIntoView({ behavior: 'smooth' })}
|
|
877
|
+
onMouseEnter={() => setHovered(true)}
|
|
878
|
+
onMouseLeave={() => setHovered(false)}
|
|
879
|
+
>
|
|
880
|
+
{children}
|
|
881
|
+
</span>
|
|
882
|
+
);
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
// ==================== 主页面组件 ====================
|
|
886
|
+
|
|
887
|
+
export default function TutorialPage() {
|
|
888
|
+
const { navigate } = useRouterHistory();
|
|
889
|
+
const [activeSection, setActiveSection] = useState('overview');
|
|
890
|
+
const [expandedSection, setExpandedSection] = useState<string | null>(null);
|
|
891
|
+
const [searchOpen, setSearchOpen] = useState(false);
|
|
892
|
+
const [searchQuery, setSearchQuery] = useState('');
|
|
893
|
+
const [searchHistory, setSearchHistory] = useState<string[]>([]);
|
|
894
|
+
const [searchResults, setSearchResults] = useState<Array<{ text: string; sectionTitle: string; sectionId: string }>>([]);
|
|
895
|
+
const mainRef = useRef<HTMLDivElement>(null);
|
|
896
|
+
const isScrollingRef = useRef(false);
|
|
897
|
+
const searchInputRef = useRef<HTMLInputElement>(null);
|
|
898
|
+
const searchMenuRef = useRef<HTMLDivElement>(null);
|
|
899
|
+
const searchTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
900
|
+
const [exportModalOpen, setExportModalOpen] = useState(false);
|
|
901
|
+
|
|
902
|
+
// 导出教程为自包含的 HTML 静态文件
|
|
903
|
+
const exportTutorialHTML = useCallback((themeId?: string) => {
|
|
904
|
+
const main = mainRef.current;
|
|
905
|
+
if (!main) return;
|
|
906
|
+
|
|
907
|
+
// 选择主题,默认使用当前激活的主题
|
|
908
|
+
const theme = themeId
|
|
909
|
+
? presetThemes.find(t => t.id === themeId)
|
|
910
|
+
: presetThemes.find(t => {
|
|
911
|
+
const style = getComputedStyle(document.documentElement);
|
|
912
|
+
return t.variables['--color-bg-primary'] === style.getPropertyValue('--color-bg-primary').trim();
|
|
913
|
+
});
|
|
914
|
+
const vars = theme?.variables || presetThemes[0].variables;
|
|
915
|
+
|
|
916
|
+
// 将 CSS 变量替换为具体色值
|
|
917
|
+
let html = main.innerHTML;
|
|
918
|
+
for (const [varName, value] of Object.entries(vars)) {
|
|
919
|
+
html = html.replace(new RegExp(`var\\(${varName}\\)`, 'g'), value);
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
const fullHTML = `<!DOCTYPE html>
|
|
923
|
+
<html lang="zh-CN">
|
|
924
|
+
<head>
|
|
925
|
+
<meta charset="UTF-8">
|
|
926
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
927
|
+
<title>星语插件开发教程</title>
|
|
928
|
+
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
929
|
+
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans+SC:wght@400;500;600;700&display=swap" rel="stylesheet">
|
|
930
|
+
<style>
|
|
931
|
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
932
|
+
body {
|
|
933
|
+
font-family: 'Noto Sans SC', 'PingFang SC', 'Microsoft YaHei', sans-serif;
|
|
934
|
+
background-color: ${vars['--color-bg-primary']};
|
|
935
|
+
color: ${vars['--color-text-secondary']};
|
|
936
|
+
line-height: 1.8;
|
|
937
|
+
font-size: 13px;
|
|
938
|
+
padding: 2rem;
|
|
939
|
+
max-width: 900px;
|
|
940
|
+
margin: 0 auto;
|
|
941
|
+
}
|
|
942
|
+
h2 { color: ${vars['--color-text-primary']}; font-size: 1.2rem; font-weight: 700; margin-bottom: 1rem; margin-top: 2.5rem; }
|
|
943
|
+
h3 { color: ${vars['--color-text-primary']}; font-size: 0.9rem; font-weight: 700; margin-bottom: 0.5rem; margin-top: 1.5rem; }
|
|
944
|
+
h4 { color: ${vars['--color-accent']}; font-size: 0.8rem; font-weight: 700; margin-bottom: 0.5rem; margin-top: 1rem; }
|
|
945
|
+
p { margin-bottom: 0.5rem; }
|
|
946
|
+
ul, ol { padding-left: 1.25rem; margin-bottom: 0.5rem; }
|
|
947
|
+
li { margin-bottom: 0.25rem; }
|
|
948
|
+
code {
|
|
949
|
+
background-color: ${vars['--color-bg-tertiary']};
|
|
950
|
+
padding: 0.1rem 0.4rem;
|
|
951
|
+
border-radius: 4px;
|
|
952
|
+
font-size: 0.85em;
|
|
953
|
+
font-family: 'Fira Code', 'Consolas', monospace;
|
|
954
|
+
color: ${vars['--color-accent']};
|
|
955
|
+
}
|
|
956
|
+
pre {
|
|
957
|
+
background-color: ${vars['--color-bg-secondary']};
|
|
958
|
+
border: 1px solid ${vars['--color-border']};
|
|
959
|
+
border-radius: 8px;
|
|
960
|
+
padding: 1rem;
|
|
961
|
+
overflow-x: auto;
|
|
962
|
+
margin: 0.5rem 0;
|
|
963
|
+
font-size: 0.8rem;
|
|
964
|
+
line-height: 1.6;
|
|
965
|
+
}
|
|
966
|
+
pre code {
|
|
967
|
+
background: none;
|
|
968
|
+
padding: 0;
|
|
969
|
+
border-radius: 0;
|
|
970
|
+
color: ${vars['--color-text-secondary']};
|
|
971
|
+
}
|
|
972
|
+
table {
|
|
973
|
+
width: 100%;
|
|
974
|
+
border-collapse: collapse;
|
|
975
|
+
margin: 0.5rem 0;
|
|
976
|
+
font-size: 0.8rem;
|
|
977
|
+
}
|
|
978
|
+
th, td {
|
|
979
|
+
padding: 0.4rem 0.75rem;
|
|
980
|
+
border-color: ${vars['--color-border']};
|
|
981
|
+
text-align: left;
|
|
982
|
+
}
|
|
983
|
+
th {
|
|
984
|
+
background-color: ${vars['--color-bg-secondary']};
|
|
985
|
+
color: ${vars['--color-text-primary']};
|
|
986
|
+
font-weight: 600;
|
|
987
|
+
}
|
|
988
|
+
td { color: ${vars['--color-text-secondary']}; }
|
|
989
|
+
a { color: ${vars['--color-accent']}; text-decoration: none; }
|
|
990
|
+
a:hover { color: ${vars['--color-accent-hover']}; }
|
|
991
|
+
blockquote {
|
|
992
|
+
border-left: 3px solid ${vars['--color-accent']};
|
|
993
|
+
padding-left: 1rem;
|
|
994
|
+
margin: 0.5rem 0;
|
|
995
|
+
color: ${vars['--color-text-muted']};
|
|
996
|
+
}
|
|
997
|
+
strong { color: ${vars['--color-text-primary']}; }
|
|
998
|
+
hr { border: none; border-top: 1px solid transparent; border-top-color: ${vars['--color-border']}; margin: 1.5rem 0; }
|
|
999
|
+
</style>
|
|
1000
|
+
</head>
|
|
1001
|
+
<body>
|
|
1002
|
+
${html}
|
|
1003
|
+
</body>
|
|
1004
|
+
</html>`;
|
|
1005
|
+
|
|
1006
|
+
// 触发下载
|
|
1007
|
+
const blob = new Blob([fullHTML], { type: 'text/html;charset=utf-8' });
|
|
1008
|
+
const url = URL.createObjectURL(blob);
|
|
1009
|
+
const a = document.createElement('a');
|
|
1010
|
+
a.href = url;
|
|
1011
|
+
a.download = 'xinyu-plugin-tutorial.html';
|
|
1012
|
+
document.body.appendChild(a);
|
|
1013
|
+
a.click();
|
|
1014
|
+
document.body.removeChild(a);
|
|
1015
|
+
URL.revokeObjectURL(url);
|
|
1016
|
+
}, []);
|
|
1017
|
+
|
|
1018
|
+
// 搜索历史持久化
|
|
1019
|
+
useEffect(() => {
|
|
1020
|
+
try {
|
|
1021
|
+
const saved = localStorage.getItem('tutorial_search_history');
|
|
1022
|
+
if (saved) setSearchHistory(JSON.parse(saved));
|
|
1023
|
+
} catch {}
|
|
1024
|
+
}, []);
|
|
1025
|
+
|
|
1026
|
+
const saveSearchHistory = useCallback((history: string[]) => {
|
|
1027
|
+
setSearchHistory(history);
|
|
1028
|
+
try { localStorage.setItem('tutorial_search_history', JSON.stringify(history)); } catch {}
|
|
1029
|
+
}, []);
|
|
1030
|
+
|
|
1031
|
+
const addSearchHistory = useCallback((query: string) => {
|
|
1032
|
+
if (!query.trim()) return;
|
|
1033
|
+
const trimmed = query.trim();
|
|
1034
|
+
const filtered = searchHistory.filter(h => h !== trimmed);
|
|
1035
|
+
const updated = [trimmed, ...filtered].slice(0, 20);
|
|
1036
|
+
saveSearchHistory(updated);
|
|
1037
|
+
}, [searchHistory, saveSearchHistory]);
|
|
1038
|
+
|
|
1039
|
+
const removeSearchHistory = useCallback((item: string) => {
|
|
1040
|
+
saveSearchHistory(searchHistory.filter(h => h !== item));
|
|
1041
|
+
}, [searchHistory, saveSearchHistory]);
|
|
1042
|
+
|
|
1043
|
+
const clearSearchHistory = useCallback(() => {
|
|
1044
|
+
saveSearchHistory([]);
|
|
1045
|
+
}, [saveSearchHistory]);
|
|
1046
|
+
|
|
1047
|
+
// 搜索框打开时聚焦
|
|
1048
|
+
useEffect(() => {
|
|
1049
|
+
if (searchOpen && searchInputRef.current) {
|
|
1050
|
+
searchInputRef.current.focus();
|
|
1051
|
+
}
|
|
1052
|
+
}, [searchOpen]);
|
|
1053
|
+
|
|
1054
|
+
// 点击外部关闭搜索菜单
|
|
1055
|
+
useEffect(() => {
|
|
1056
|
+
if (!searchOpen) return;
|
|
1057
|
+
const handler = (e: MouseEvent) => {
|
|
1058
|
+
if (searchMenuRef.current && !searchMenuRef.current.contains(e.target as Node)) {
|
|
1059
|
+
setSearchOpen(false);
|
|
1060
|
+
setSearchQuery('');
|
|
1061
|
+
setSearchResults([]);
|
|
1062
|
+
searchInputRef.current?.blur();
|
|
1063
|
+
}
|
|
1064
|
+
};
|
|
1065
|
+
document.addEventListener('mousedown', handler);
|
|
1066
|
+
return () => document.removeEventListener('mousedown', handler);
|
|
1067
|
+
}, [searchOpen]);
|
|
1068
|
+
|
|
1069
|
+
/** 在教程内容中搜索,返回匹配结果(纯计算,不操作 DOM) */
|
|
1070
|
+
const doSearch = useCallback((query: string): Array<{ text: string; sectionTitle: string; sectionId: string }> => {
|
|
1071
|
+
const main = mainRef.current;
|
|
1072
|
+
if (!main || !query.trim()) return [];
|
|
1073
|
+
const lowerQuery = query.trim().toLowerCase();
|
|
1074
|
+
const results: Array<{ text: string; sectionTitle: string; sectionId: string }> = [];
|
|
1075
|
+
main.querySelectorAll('section[id^="section-"]').forEach(secEl => {
|
|
1076
|
+
const secId = secEl.id.replace('section-', '');
|
|
1077
|
+
const titleEl = secEl.querySelector('h2');
|
|
1078
|
+
const title = titleEl?.textContent || secId;
|
|
1079
|
+
const text = (secEl.textContent || '').toLowerCase();
|
|
1080
|
+
if (text.includes(lowerQuery)) {
|
|
1081
|
+
const idx = text.indexOf(lowerQuery);
|
|
1082
|
+
const start = Math.max(0, idx - 20);
|
|
1083
|
+
const end = Math.min(text.length, idx + query.trim().length + 40);
|
|
1084
|
+
let snippet = (secEl.textContent || '').substring(start, end).replace(/\s+/g, ' ').trim();
|
|
1085
|
+
if (start > 0) snippet = '...' + snippet;
|
|
1086
|
+
if (end < text.length) snippet += '...';
|
|
1087
|
+
results.push({ text: snippet, sectionTitle: title, sectionId: secId });
|
|
1088
|
+
}
|
|
1089
|
+
});
|
|
1090
|
+
return results;
|
|
1091
|
+
}, []);
|
|
1092
|
+
|
|
1093
|
+
/** 防抖搜索(输入时调用,300ms 延迟) */
|
|
1094
|
+
const debouncedSearch = useCallback((query: string) => {
|
|
1095
|
+
if (searchTimerRef.current) clearTimeout(searchTimerRef.current);
|
|
1096
|
+
if (!query.trim()) { setSearchResults([]); return; }
|
|
1097
|
+
searchTimerRef.current = setTimeout(() => {
|
|
1098
|
+
setSearchResults(doSearch(query));
|
|
1099
|
+
}, 300);
|
|
1100
|
+
}, [doSearch]);
|
|
1101
|
+
|
|
1102
|
+
const highlightInSection = useCallback((sectionId: string, query: string) => {
|
|
1103
|
+
const main = mainRef.current;
|
|
1104
|
+
if (!main) return;
|
|
1105
|
+
// 先清除旧高亮
|
|
1106
|
+
main.querySelectorAll('mark[data-tutorial-search]').forEach(el => {
|
|
1107
|
+
const mark = el as HTMLElement;
|
|
1108
|
+
const parent = mark.parentNode;
|
|
1109
|
+
if (parent) {
|
|
1110
|
+
parent.replaceChild(document.createTextNode(mark.textContent || ''), mark);
|
|
1111
|
+
parent.normalize();
|
|
1112
|
+
}
|
|
1113
|
+
});
|
|
1114
|
+
// 在目标 section 中找第一个文本匹配
|
|
1115
|
+
const secEl = main.querySelector('#section-' + sectionId);
|
|
1116
|
+
if (!secEl) return;
|
|
1117
|
+
const lowerQuery = query.toLowerCase();
|
|
1118
|
+
const walker = document.createTreeWalker(secEl, NodeFilter.SHOW_TEXT, null);
|
|
1119
|
+
let node: Node | null;
|
|
1120
|
+
while ((node = walker.nextNode())) {
|
|
1121
|
+
const text = node.textContent || '';
|
|
1122
|
+
const idx = text.toLowerCase().indexOf(lowerQuery);
|
|
1123
|
+
if (idx >= 0) {
|
|
1124
|
+
try {
|
|
1125
|
+
const range = document.createRange();
|
|
1126
|
+
range.setStart(node, idx);
|
|
1127
|
+
range.setEnd(node, idx + query.length);
|
|
1128
|
+
const mark = document.createElement('mark');
|
|
1129
|
+
mark.setAttribute('data-tutorial-search', 'true');
|
|
1130
|
+
mark.style.backgroundColor = 'var(--color-accent)';
|
|
1131
|
+
mark.style.color = '#000';
|
|
1132
|
+
mark.style.borderRadius = '2px';
|
|
1133
|
+
mark.style.padding = '0 2px';
|
|
1134
|
+
range.surroundContents(mark);
|
|
1135
|
+
// 直接让高亮元素滚动到视口,一步到位
|
|
1136
|
+
mark.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
|
1137
|
+
} catch {}
|
|
1138
|
+
break; // 只高亮第一个匹配
|
|
1139
|
+
}
|
|
1140
|
+
}
|
|
1141
|
+
}, []);
|
|
1142
|
+
|
|
1143
|
+
const handleSearch = useCallback((query: string) => {
|
|
1144
|
+
if (!query.trim()) return;
|
|
1145
|
+
addSearchHistory(query);
|
|
1146
|
+
|
|
1147
|
+
const results = doSearch(query);
|
|
1148
|
+
setSearchResults(results);
|
|
1149
|
+
|
|
1150
|
+
// 如果有结果,跳转到第一个并高亮
|
|
1151
|
+
if (results.length > 0) {
|
|
1152
|
+
highlightInSection(results[0].sectionId, query.trim());
|
|
1153
|
+
}
|
|
1154
|
+
}, [addSearchHistory, doSearch, highlightInSection]);
|
|
1155
|
+
|
|
1156
|
+
const clearHighlights = useCallback(() => {
|
|
1157
|
+
const sel = window.getSelection();
|
|
1158
|
+
if (sel) sel.removeAllRanges();
|
|
1159
|
+
// 清除 DOM 中的搜索高亮 <mark> 元素
|
|
1160
|
+
const main = mainRef.current;
|
|
1161
|
+
if (main) {
|
|
1162
|
+
main.querySelectorAll('mark[data-tutorial-search]').forEach(el => {
|
|
1163
|
+
const mark = el as HTMLElement;
|
|
1164
|
+
const parent = mark.parentNode;
|
|
1165
|
+
if (parent) {
|
|
1166
|
+
parent.replaceChild(document.createTextNode(mark.textContent || ''), mark);
|
|
1167
|
+
parent.normalize();
|
|
1168
|
+
}
|
|
1169
|
+
});
|
|
1170
|
+
}
|
|
1171
|
+
setSearchResults([]);
|
|
1172
|
+
}, []);
|
|
1173
|
+
|
|
1174
|
+
// 关闭搜索面板时清除高亮
|
|
1175
|
+
useEffect(() => {
|
|
1176
|
+
if (!searchOpen) {
|
|
1177
|
+
const timer = setTimeout(clearHighlights, 300);
|
|
1178
|
+
return () => clearTimeout(timer);
|
|
1179
|
+
}
|
|
1180
|
+
}, [searchOpen, clearHighlights]);
|
|
1181
|
+
|
|
1182
|
+
const sections: Section[] = [
|
|
1183
|
+
{
|
|
1184
|
+
id: 'overview',
|
|
1185
|
+
title: '概述',
|
|
1186
|
+
icon: '📖',
|
|
1187
|
+
subtitles: [
|
|
1188
|
+
{ id: 'sub-what-is-plugin', title: '什么是插件?' },
|
|
1189
|
+
{ id: 'sub-plugin-types', title: '插件类型' },
|
|
1190
|
+
{ id: 'sub-minimal-plugin', title: '最简插件' },
|
|
1191
|
+
],
|
|
1192
|
+
content: <OverviewSection />,
|
|
1193
|
+
},
|
|
1194
|
+
{
|
|
1195
|
+
id: 'architecture',
|
|
1196
|
+
title: '架构设计',
|
|
1197
|
+
icon: '🏗️',
|
|
1198
|
+
subtitles: [
|
|
1199
|
+
{ id: 'sub-code-exec', title: '代码执行方式' },
|
|
1200
|
+
{ id: 'sub-dual-scope', title: '双层作用域' },
|
|
1201
|
+
],
|
|
1202
|
+
content: <ArchitectureSection />,
|
|
1203
|
+
},
|
|
1204
|
+
{
|
|
1205
|
+
id: 'lifecycle',
|
|
1206
|
+
title: '生命周期',
|
|
1207
|
+
icon: '🔄',
|
|
1208
|
+
subtitles: [
|
|
1209
|
+
{ id: 'sub-lifecycle-example', title: '使用示例' },
|
|
1210
|
+
],
|
|
1211
|
+
content: <LifecycleSection />,
|
|
1212
|
+
},
|
|
1213
|
+
{
|
|
1214
|
+
id: 'api-global',
|
|
1215
|
+
title: '全局 API',
|
|
1216
|
+
icon: '🌐',
|
|
1217
|
+
subtitles: [
|
|
1218
|
+
{ id: 'sub-common-api', title: '所有插件通用 API' },
|
|
1219
|
+
],
|
|
1220
|
+
content: <GlobalApiSection />,
|
|
1221
|
+
},
|
|
1222
|
+
{
|
|
1223
|
+
id: 'api-ui',
|
|
1224
|
+
title: 'UI API 详解',
|
|
1225
|
+
icon: '🎨',
|
|
1226
|
+
subtitles: [
|
|
1227
|
+
{ id: 'sub-slot-inject', title: '插槽注入' },
|
|
1228
|
+
{ id: 'sub-toolbar-button', title: '工具栏按钮' },
|
|
1229
|
+
{ id: 'sub-modal-confirm', title: '模态框与确认对话框' },
|
|
1230
|
+
{ id: 'sub-style-inject', title: '样式注入' },
|
|
1231
|
+
{ id: 'sub-host-event', title: '宿主事件监听' },
|
|
1232
|
+
{ id: 'sub-message-card', title: '消息卡片注册' },
|
|
1233
|
+
],
|
|
1234
|
+
content: <UiApiSection />,
|
|
1235
|
+
},
|
|
1236
|
+
{
|
|
1237
|
+
id: 'api-chat',
|
|
1238
|
+
title: 'Chat API 详解',
|
|
1239
|
+
icon: '💬',
|
|
1240
|
+
subtitles: [
|
|
1241
|
+
{ id: 'sub-chat-send', title: 'send / insertSystemMessage / insertNotice' },
|
|
1242
|
+
{ id: 'sub-chat-notice', title: 'insertNotice' },
|
|
1243
|
+
{ id: 'sub-chat-style', title: 'injectChatStyle' },
|
|
1244
|
+
{ id: 'sub-chat-command', title: 'registerCommand' },
|
|
1245
|
+
{ id: 'sub-chat-renderer', title: 'registerMessageRenderer' },
|
|
1246
|
+
{ id: 'sub-chat-on-render-message', title: 'onRenderMessage(消息内容管道)' },
|
|
1247
|
+
],
|
|
1248
|
+
content: <ChatApiSection />,
|
|
1249
|
+
},
|
|
1250
|
+
{
|
|
1251
|
+
id: 'api-input',
|
|
1252
|
+
title: '输入框 API',
|
|
1253
|
+
icon: '⌨️',
|
|
1254
|
+
subtitles: [
|
|
1255
|
+
{ id: 'sub-input-overview', title: '概述' },
|
|
1256
|
+
{ id: 'sub-input-content', title: '内容操作' },
|
|
1257
|
+
{ id: 'sub-input-state', title: '状态控制' },
|
|
1258
|
+
{ id: 'sub-input-style', title: '样式修改' },
|
|
1259
|
+
{ id: 'sub-input-event', title: '事件监听' },
|
|
1260
|
+
{ id: 'sub-input-cursor', title: '光标与选区' },
|
|
1261
|
+
],
|
|
1262
|
+
content: <InputApiSection />,
|
|
1263
|
+
},
|
|
1264
|
+
{
|
|
1265
|
+
id: 'api-utils',
|
|
1266
|
+
title: 'Utils API 详解',
|
|
1267
|
+
icon: '🔧',
|
|
1268
|
+
subtitles: [
|
|
1269
|
+
{ id: 'sub-utils-random', title: 'randomInt / rollDice' },
|
|
1270
|
+
{ id: 'sub-utils-format', title: 'formatDate' },
|
|
1271
|
+
{ id: 'sub-utils-debounce', title: 'debounce / throttle' },
|
|
1272
|
+
{ id: 'sub-utils-eventbus', title: 'eventBus' },
|
|
1273
|
+
],
|
|
1274
|
+
content: <UtilsApiSection />,
|
|
1275
|
+
},
|
|
1276
|
+
{
|
|
1277
|
+
id: 'api-dom',
|
|
1278
|
+
title: 'DOM 沙箱',
|
|
1279
|
+
icon: '🛡️',
|
|
1280
|
+
subtitles: [
|
|
1281
|
+
{ id: 'sub-dom-security', title: '安全边界' },
|
|
1282
|
+
{ id: 'sub-dom-slots', title: '可用插槽 ID' },
|
|
1283
|
+
{ id: 'sub-dom-api', title: 'API 详解' },
|
|
1284
|
+
{ id: 'sub-dom-permission', title: '权限说明' },
|
|
1285
|
+
{ id: 'sub-dom-vs-slot', title: 'DOM vs Slot' },
|
|
1286
|
+
],
|
|
1287
|
+
content: <DomSandboxSection />,
|
|
1288
|
+
},
|
|
1289
|
+
{
|
|
1290
|
+
id: 'permissions',
|
|
1291
|
+
title: '权限系统',
|
|
1292
|
+
icon: '🔐',
|
|
1293
|
+
subtitles: [
|
|
1294
|
+
{ id: 'sub-perm-overview', title: '三层权限体系' },
|
|
1295
|
+
{ id: 'sub-perm-list', title: '权限列表' },
|
|
1296
|
+
{ id: 'sub-perm-default', title: '默认权限' },
|
|
1297
|
+
{ id: 'sub-perm-editor', title: '编辑器中声明权限' },
|
|
1298
|
+
{ id: 'sub-perm-annotation', title: '代码注释声明' },
|
|
1299
|
+
{ id: 'sub-perm-conflict', title: '权限冲突' },
|
|
1300
|
+
],
|
|
1301
|
+
content: <PermissionsSection />,
|
|
1302
|
+
},
|
|
1303
|
+
{
|
|
1304
|
+
id: 'slots',
|
|
1305
|
+
title: 'UI 插槽',
|
|
1306
|
+
icon: '📍',
|
|
1307
|
+
subtitles: [
|
|
1308
|
+
{ id: 'sub-slot-diagram', title: '插槽位置示意图' },
|
|
1309
|
+
{ id: 'sub-slot-priority', title: '优先级' },
|
|
1310
|
+
{ id: 'sub-slot-update', title: '更新与覆盖' },
|
|
1311
|
+
{ id: 'sub-slot-action', title: '交互事件' },
|
|
1312
|
+
{ id: 'sub-slot-key', title: 'key 参数与精确注销'},
|
|
1313
|
+
],
|
|
1314
|
+
content: <SlotsSection />,
|
|
1315
|
+
},
|
|
1316
|
+
{
|
|
1317
|
+
id: 'css-variables',
|
|
1318
|
+
title: 'CSS 变量系统',
|
|
1319
|
+
icon: '🎨',
|
|
1320
|
+
subtitles: [
|
|
1321
|
+
{ id: 'sub-css-var-list', title: '完整的 CSS 变量列表' },
|
|
1322
|
+
{ id: 'sub-css-var-naming', title: 'CSS 变量命名规范' },
|
|
1323
|
+
{ id: 'sub-css-var-usage', title: '插件中使用 CSS 变量的方式' },
|
|
1324
|
+
{ id: 'sub-css-var-guide', title: '使用指南' },
|
|
1325
|
+
{ id: 'sub-css-var-custom', title: '创建自定义 CSS 变量' },
|
|
1326
|
+
{ id: 'sub-css-var-themes', title: '四个预设主题对比' },
|
|
1327
|
+
],
|
|
1328
|
+
content: <CssVariablesSection />,
|
|
1329
|
+
},
|
|
1330
|
+
{
|
|
1331
|
+
id: 'plugin-storage',
|
|
1332
|
+
title: '插件数据存储',
|
|
1333
|
+
icon: '💾',
|
|
1334
|
+
subtitles: [
|
|
1335
|
+
{ id: 'sub-storage-plugin', title: '方式 1:plugin.storage(推荐)' },
|
|
1336
|
+
{ id: 'sub-game-info', title: '方式 2:game.getWorldSetting / setWorldSetting / getSessionId / getMessages' },
|
|
1337
|
+
{ id: 'sub-storage-game', title: '方式 3:game.setState / getState' },
|
|
1338
|
+
{ id: 'sub-storage-attr', title: '方式 4:game.registerAttribute' },
|
|
1339
|
+
{ id: 'sub-storage-best', title: '存储最佳实践' },
|
|
1340
|
+
],
|
|
1341
|
+
content: <PluginStorageSection />,
|
|
1342
|
+
},
|
|
1343
|
+
{
|
|
1344
|
+
id: 'ai-interception',
|
|
1345
|
+
title: 'AI 拦截系统详解',
|
|
1346
|
+
icon: '🤖',
|
|
1347
|
+
subtitles: [
|
|
1348
|
+
{ id: 'sub-ai-interceptors', title: '五种拦截器' },
|
|
1349
|
+
{ id: 'sub-ai-pipeline', title: '管道模式详解' },
|
|
1350
|
+
{ id: 'sub-ai-npc-example', title: '完整示例:NPC 人格注入' },
|
|
1351
|
+
{ id: 'sub-ai-notes', title: '注意事项' },
|
|
1352
|
+
],
|
|
1353
|
+
content: <AiInterceptionSection />,
|
|
1354
|
+
},
|
|
1355
|
+
{
|
|
1356
|
+
id: 'plugin-config',
|
|
1357
|
+
title: '插件配置系统',
|
|
1358
|
+
icon: '⚙️',
|
|
1359
|
+
subtitles: [
|
|
1360
|
+
{ id: 'sub-config-schema', title: 'configSchema 定义' },
|
|
1361
|
+
{ id: 'sub-config-schema-example', title: 'configSchema 示例' },
|
|
1362
|
+
{ id: 'sub-config-read', title: '在插件代码中读取配置' },
|
|
1363
|
+
{ id: 'sub-config-full-example', title: '完整示例:带配置的骰子插件' },
|
|
1364
|
+
{ id: 'sub-config-ingame', title: '配置作用域' },
|
|
1365
|
+
],
|
|
1366
|
+
content: <PluginConfigSection />,
|
|
1367
|
+
},
|
|
1368
|
+
{
|
|
1369
|
+
id: 'plugin-import-export',
|
|
1370
|
+
title: '插件导入导出',
|
|
1371
|
+
icon: '📦',
|
|
1372
|
+
subtitles: [
|
|
1373
|
+
{ id: 'sub-export-format', title: '导出格式' },
|
|
1374
|
+
{ id: 'sub-import-format', title: '导入格式' },
|
|
1375
|
+
{ id: 'sub-import-behavior', title: '导入行为' },
|
|
1376
|
+
{ id: 'sub-json-spec', title: 'JSON 文件格式规范' },
|
|
1377
|
+
],
|
|
1378
|
+
content: <PluginImportExportSection />,
|
|
1379
|
+
},
|
|
1380
|
+
{
|
|
1381
|
+
id: 'persistent-storage',
|
|
1382
|
+
title: '持久化存储与文件操作',
|
|
1383
|
+
icon: '💾',
|
|
1384
|
+
subtitles: [
|
|
1385
|
+
{ id: 'sub-persistent-storage', title: '持久化存储 xinyu.storage' },
|
|
1386
|
+
{ id: 'sub-file-api', title: '文件操作 xinyu.file' },
|
|
1387
|
+
{ id: 'sub-resource-api', title: '插件资源访问 getResource / getResourceUrl' },
|
|
1388
|
+
],
|
|
1389
|
+
content: (
|
|
1390
|
+
<>
|
|
1391
|
+
<PersistentStorageSection />
|
|
1392
|
+
<FileApiSection />
|
|
1393
|
+
<ResourceApiSection />
|
|
1394
|
+
</>
|
|
1395
|
+
),
|
|
1396
|
+
},
|
|
1397
|
+
{
|
|
1398
|
+
id: 'cross-plugin',
|
|
1399
|
+
title: '跨插件通信',
|
|
1400
|
+
icon: '🔗',
|
|
1401
|
+
subtitles: [
|
|
1402
|
+
{ id: 'sub-cp-overview', title: '概述' },
|
|
1403
|
+
{ id: 'sub-cp-export', title: '被调用方:导出 API' },
|
|
1404
|
+
{ id: 'sub-cp-call', title: '调用方:使用 API' },
|
|
1405
|
+
{ id: 'sub-cp-security', title: '安全机制' },
|
|
1406
|
+
],
|
|
1407
|
+
content: <CrossPluginSection />,
|
|
1408
|
+
},
|
|
1409
|
+
{
|
|
1410
|
+
id: 'dependency',
|
|
1411
|
+
title: '依赖管理',
|
|
1412
|
+
icon: '📦',
|
|
1413
|
+
subtitles: [
|
|
1414
|
+
{ id: 'sub-dep-overview', title: '概述' },
|
|
1415
|
+
{ id: 'sub-dep-manifest', title: 'manifest 声明' },
|
|
1416
|
+
{ id: 'sub-dep-version', title: '版本范围语法' },
|
|
1417
|
+
{ id: 'sub-dep-runtime', title: '运行时加载(推荐)' },
|
|
1418
|
+
{ id: 'sub-dep-editor', title: '编辑器中的依赖管理' },
|
|
1419
|
+
],
|
|
1420
|
+
content: <DependencySection />,
|
|
1421
|
+
},
|
|
1422
|
+
{
|
|
1423
|
+
id: 'security',
|
|
1424
|
+
title: '安全机制详解',
|
|
1425
|
+
icon: '🔒',
|
|
1426
|
+
subtitles: [
|
|
1427
|
+
{ id: 'sub-security-layers', title: '六层安全架构' },
|
|
1428
|
+
{ id: 'sub-html-sanitize', title: 'HTML 净化规则' },
|
|
1429
|
+
{ id: 'sub-allowed-tags', title: '允许的 HTML 标签' },
|
|
1430
|
+
{ id: 'sub-ai-scan', title: 'AI 安全检测' },
|
|
1431
|
+
],
|
|
1432
|
+
content: <SecuritySection />,
|
|
1433
|
+
},
|
|
1434
|
+
{
|
|
1435
|
+
id: 'best-practices',
|
|
1436
|
+
title: '最佳实践',
|
|
1437
|
+
icon: '✨',
|
|
1438
|
+
subtitles: [
|
|
1439
|
+
{ id: 'sub-bp-general', title: '通用规范' },
|
|
1440
|
+
{ id: 'sub-bp-ui', title: 'UI 开发规范' },
|
|
1441
|
+
{ id: 'sub-bp-ai', title: 'AI 拦截规范' },
|
|
1442
|
+
{ id: 'sub-bp-storage', title: '数据存储规范' },
|
|
1443
|
+
{ id: 'sub-bp-config', title: '插件配置规范' },
|
|
1444
|
+
{ id: 'sub-bp-perf', title: '性能建议' },
|
|
1445
|
+
{ id: 'sub-bp-cross-plugin', title: '跨插件通信规范' },
|
|
1446
|
+
{ id: 'sub-bp-dependency', title: '依赖管理规范' },
|
|
1447
|
+
],
|
|
1448
|
+
content: <BestPracticesSection />,
|
|
1449
|
+
},
|
|
1450
|
+
{
|
|
1451
|
+
id: 'examples',
|
|
1452
|
+
title: '完整示例',
|
|
1453
|
+
icon: '💡',
|
|
1454
|
+
subtitles: [
|
|
1455
|
+
{ id: 'sub-ex-1', title: '示例 1:自定义状态栏插件' },
|
|
1456
|
+
{ id: 'sub-ex-2', title: '示例 2:消息统计面板' },
|
|
1457
|
+
{ id: 'sub-ex-3', title: '示例 3:浮动操作面板' },
|
|
1458
|
+
{ id: 'sub-ex-4', title: '示例 4:带配置的骰子插件' },
|
|
1459
|
+
{ id: 'sub-ex-5', title: '示例 5:NPC 人格注入插件' },
|
|
1460
|
+
],
|
|
1461
|
+
content: <ExamplesSection />,
|
|
1462
|
+
},
|
|
1463
|
+
{
|
|
1464
|
+
id: 'debug',
|
|
1465
|
+
title: '调试技巧',
|
|
1466
|
+
icon: '🔧',
|
|
1467
|
+
subtitles: [
|
|
1468
|
+
{ id: 'sub-debug-tools', title: '使用浏览器开发者工具' },
|
|
1469
|
+
{ id: 'sub-debug-faq', title: '常见问题排查' },
|
|
1470
|
+
{ id: 'sub-debug-scan', title: '安全检测' },
|
|
1471
|
+
],
|
|
1472
|
+
content: <DebugSection />,
|
|
1473
|
+
},
|
|
1474
|
+
];
|
|
1475
|
+
|
|
1476
|
+
// Scroll spy: 监听滚动,实时更新 activeSection 和 activeSub
|
|
1477
|
+
useEffect(() => {
|
|
1478
|
+
const main = mainRef.current;
|
|
1479
|
+
if (!main) return;
|
|
1480
|
+
|
|
1481
|
+
const observer = new IntersectionObserver(
|
|
1482
|
+
(entries) => {
|
|
1483
|
+
if (isScrollingRef.current) return;
|
|
1484
|
+
for (const entry of entries) {
|
|
1485
|
+
if (entry.isIntersecting) {
|
|
1486
|
+
const id = entry.target.id;
|
|
1487
|
+
if (id.startsWith('section-')) {
|
|
1488
|
+
setActiveSection(id.replace('section-', ''));
|
|
1489
|
+
} else if (id.startsWith('sub-')) {
|
|
1490
|
+
for (const sec of sections) {
|
|
1491
|
+
if (sec.subtitles?.some((st) => st.id === id)) {
|
|
1492
|
+
setActiveSection(sec.id);
|
|
1493
|
+
break;
|
|
1494
|
+
}
|
|
1495
|
+
}
|
|
1496
|
+
}
|
|
1497
|
+
}
|
|
1498
|
+
}
|
|
1499
|
+
},
|
|
1500
|
+
{ root: main, rootMargin: '-10% 0px -70% 0px', threshold: 0 }
|
|
1501
|
+
);
|
|
1502
|
+
|
|
1503
|
+
// 观察所有 section 和 subtitle 元素
|
|
1504
|
+
main.querySelectorAll('[id^="section-"], [id^="sub-"]').forEach((el) => {
|
|
1505
|
+
observer.observe(el);
|
|
1506
|
+
});
|
|
1507
|
+
|
|
1508
|
+
return () => observer.disconnect();
|
|
1509
|
+
}, [sections]);
|
|
1510
|
+
|
|
1511
|
+
const scrollTo = useCallback((id: string) => {
|
|
1512
|
+
isScrollingRef.current = true;
|
|
1513
|
+
const target = document.getElementById(id);
|
|
1514
|
+
if (!target) { isScrollingRef.current = false; return; }
|
|
1515
|
+
|
|
1516
|
+
target.scrollIntoView({ behavior: 'smooth' });
|
|
1517
|
+
|
|
1518
|
+
// 监听滚动结束再恢复 scroll spy
|
|
1519
|
+
const main = mainRef.current;
|
|
1520
|
+
if (main && 'onscrollend' in window) {
|
|
1521
|
+
main.addEventListener('scrollend', function onEnd() {
|
|
1522
|
+
isScrollingRef.current = false;
|
|
1523
|
+
main.removeEventListener('scrollend', onEnd);
|
|
1524
|
+
});
|
|
1525
|
+
} else {
|
|
1526
|
+
// 降级:用 scroll 事件 + 防抖
|
|
1527
|
+
let timer: ReturnType<typeof setTimeout>;
|
|
1528
|
+
const onScroll = () => {
|
|
1529
|
+
clearTimeout(timer);
|
|
1530
|
+
timer = setTimeout(() => {
|
|
1531
|
+
isScrollingRef.current = false;
|
|
1532
|
+
main!.removeEventListener('scroll', onScroll);
|
|
1533
|
+
}, 150);
|
|
1534
|
+
};
|
|
1535
|
+
main!.addEventListener('scroll', onScroll);
|
|
1536
|
+
}
|
|
1537
|
+
}, []);
|
|
1538
|
+
|
|
1539
|
+
const handleSectionClick = useCallback((secId: string) => {
|
|
1540
|
+
setActiveSection(secId);
|
|
1541
|
+
setExpandedSection((prev) => prev === secId ? null : secId);
|
|
1542
|
+
scrollTo('section-' + secId);
|
|
1543
|
+
}, [scrollTo]);
|
|
1544
|
+
|
|
1545
|
+
const handleSubClick = useCallback((secId: string, subId: string) => {
|
|
1546
|
+
setActiveSection(secId);
|
|
1547
|
+
scrollTo(subId);
|
|
1548
|
+
}, [scrollTo]);
|
|
1549
|
+
|
|
1550
|
+
// ⌘K / Ctrl+K 快捷键打开搜索
|
|
1551
|
+
useEffect(() => {
|
|
1552
|
+
const handler = (e: KeyboardEvent) => {
|
|
1553
|
+
if ((e.metaKey || e.ctrlKey) && e.key === 'k') {
|
|
1554
|
+
e.preventDefault();
|
|
1555
|
+
setSearchOpen(prev => !prev);
|
|
1556
|
+
}
|
|
1557
|
+
};
|
|
1558
|
+
document.addEventListener('keydown', handler);
|
|
1559
|
+
return () => document.removeEventListener('keydown', handler);
|
|
1560
|
+
}, []);
|
|
1561
|
+
|
|
1562
|
+
return (
|
|
1563
|
+
<div className="h-screen flex flex-col" style={{ backgroundColor: s.bgPrimary }}>
|
|
1564
|
+
{/* 顶部导航 */}
|
|
1565
|
+
<PageHeader
|
|
1566
|
+
title="星语 · 插件开发教程"
|
|
1567
|
+
showBack={true}
|
|
1568
|
+
actions={
|
|
1569
|
+
<div className="flex items-center gap-2">
|
|
1570
|
+
{/* 搜索输入框 */}
|
|
1571
|
+
<div className="relative" ref={searchMenuRef}>
|
|
1572
|
+
<div
|
|
1573
|
+
className="flex items-center gap-2 px-3 py-1.5 rounded-lg border"
|
|
1574
|
+
style={{
|
|
1575
|
+
borderColor: s.border,
|
|
1576
|
+
backgroundColor: s.bgSecondary,
|
|
1577
|
+
width: '260px',
|
|
1578
|
+
boxShadow: searchOpen ? '0 4px 20px rgba(0,0,0,0.3)' : 'none',
|
|
1579
|
+
}}
|
|
1580
|
+
>
|
|
1581
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" style={{ color: s.textMuted, flexShrink: 0 }}>
|
|
1582
|
+
<circle cx="11" cy="11" r="8" />
|
|
1583
|
+
<path d="m21 21-4.3-4.3" />
|
|
1584
|
+
</svg>
|
|
1585
|
+
<input
|
|
1586
|
+
ref={searchInputRef}
|
|
1587
|
+
type="text"
|
|
1588
|
+
value={searchQuery}
|
|
1589
|
+
onChange={(e) => {
|
|
1590
|
+
setSearchQuery(e.target.value);
|
|
1591
|
+
if (!searchOpen) setSearchOpen(true);
|
|
1592
|
+
debouncedSearch(e.target.value);
|
|
1593
|
+
}}
|
|
1594
|
+
onFocus={() => setSearchOpen(true)}
|
|
1595
|
+
onKeyDown={(e) => {
|
|
1596
|
+
if (e.key === 'Enter') handleSearch(searchQuery);
|
|
1597
|
+
if (e.key === 'Escape') { setSearchOpen(false); setSearchQuery(''); setSearchResults([]); searchInputRef.current?.blur(); }
|
|
1598
|
+
}}
|
|
1599
|
+
placeholder="搜索教程..."
|
|
1600
|
+
className="flex-1 bg-transparent outline-none text-xs"
|
|
1601
|
+
style={{ color: s.textPrimary }}
|
|
1602
|
+
/>
|
|
1603
|
+
{searchQuery && (
|
|
1604
|
+
<button
|
|
1605
|
+
onClick={() => { setSearchQuery(''); setSearchResults([]); searchInputRef.current?.focus(); }}
|
|
1606
|
+
className="shrink-0 text-[10px]"
|
|
1607
|
+
style={{ color: s.textMuted }}
|
|
1608
|
+
>
|
|
1609
|
+
✕
|
|
1610
|
+
</button>
|
|
1611
|
+
)}
|
|
1612
|
+
<kbd className="shrink-0 px-1 py-0.5 rounded text-[9px]" style={{ backgroundColor: s.bgTertiary, color: s.textMuted }}>⌘K</kbd>
|
|
1613
|
+
</div>
|
|
1614
|
+
{/* 展开面板 */}
|
|
1615
|
+
{searchOpen && (
|
|
1616
|
+
<div
|
|
1617
|
+
className="absolute right-0 top-full mt-1 rounded-lg shadow-xl z-50 overflow-hidden"
|
|
1618
|
+
style={{ backgroundColor: s.bgSecondary, border: `1px solid ${s.border}`, width: '380px' }}
|
|
1619
|
+
>
|
|
1620
|
+
{searchQuery.trim() && searchResults.length > 0 && (
|
|
1621
|
+
<div className="max-h-64 overflow-y-auto py-1">
|
|
1622
|
+
<div className="px-3 py-1.5">
|
|
1623
|
+
<span className="text-[10px] font-bold" style={{ color: s.textMuted }}>找到 {searchResults.length} 个结果</span>
|
|
1624
|
+
</div>
|
|
1625
|
+
{searchResults.map((r, idx) => (
|
|
1626
|
+
<div
|
|
1627
|
+
key={r.sectionId + '-' + idx}
|
|
1628
|
+
className="flex items-start gap-2 px-3 py-2 cursor-pointer transition-colors"
|
|
1629
|
+
style={{ color: s.textSecondary }}
|
|
1630
|
+
onMouseEnter={(e) => (e.currentTarget.style.backgroundColor = s.bgTertiary)}
|
|
1631
|
+
onMouseLeave={(e) => (e.currentTarget.style.backgroundColor = 'transparent')}
|
|
1632
|
+
onClick={() => {
|
|
1633
|
+
addSearchHistory(searchQuery);
|
|
1634
|
+
const main = mainRef.current;
|
|
1635
|
+
if (main) {
|
|
1636
|
+
const sec = main.querySelector('#section-' + r.sectionId);
|
|
1637
|
+
if (sec) {
|
|
1638
|
+
sec.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
|
1639
|
+
setTimeout(() => highlightInSection(r.sectionId, searchQuery.trim()), 400);
|
|
1640
|
+
}
|
|
1641
|
+
}
|
|
1642
|
+
}}
|
|
1643
|
+
>
|
|
1644
|
+
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="shrink-0 mt-0.5" style={{ color: s.textMuted }}>
|
|
1645
|
+
<path d="M4 19.5v-15A2.5 2.5 0 0 1 6.5 2H20v20H6.5a2.5 2.5 0 0 1 0-5H20" />
|
|
1646
|
+
</svg>
|
|
1647
|
+
<div className="flex-1 min-w-0">
|
|
1648
|
+
<div className="text-xs font-medium truncate" style={{ color: s.textPrimary }}>{r.sectionTitle}</div>
|
|
1649
|
+
<div className="text-[10px] mt-0.5 truncate" style={{ color: s.textMuted }}>{r.text}</div>
|
|
1650
|
+
</div>
|
|
1651
|
+
</div>
|
|
1652
|
+
))}
|
|
1653
|
+
</div>
|
|
1654
|
+
)}
|
|
1655
|
+
{searchQuery.trim() && searchResults.length === 0 && (
|
|
1656
|
+
<div className="px-3 py-4 text-center text-xs" style={{ color: s.textMuted }}>未找到匹配内容</div>
|
|
1657
|
+
)}
|
|
1658
|
+
{!searchQuery.trim() && searchHistory.length > 0 && (
|
|
1659
|
+
<div className="max-h-64 overflow-y-auto py-1">
|
|
1660
|
+
<div className="flex items-center justify-between px-3 py-1.5">
|
|
1661
|
+
<span className="text-[10px] font-bold" style={{ color: s.textMuted }}>搜索历史</span>
|
|
1662
|
+
<button
|
|
1663
|
+
onClick={clearSearchHistory}
|
|
1664
|
+
className="text-[10px] transition-colors"
|
|
1665
|
+
style={{ color: s.textMuted }}
|
|
1666
|
+
onMouseEnter={(e) => (e.currentTarget.style.color = s.accent)}
|
|
1667
|
+
onMouseLeave={(e) => (e.currentTarget.style.color = s.textMuted)}
|
|
1668
|
+
>清空全部</button>
|
|
1669
|
+
</div>
|
|
1670
|
+
{searchHistory.map((item) => (
|
|
1671
|
+
<div
|
|
1672
|
+
key={item}
|
|
1673
|
+
className="flex items-center gap-2 px-3 py-1.5 cursor-pointer transition-colors group"
|
|
1674
|
+
style={{ color: s.textSecondary }}
|
|
1675
|
+
onMouseEnter={(e) => (e.currentTarget.style.backgroundColor = s.bgTertiary)}
|
|
1676
|
+
onMouseLeave={(e) => (e.currentTarget.style.backgroundColor = 'transparent')}
|
|
1677
|
+
onClick={() => {
|
|
1678
|
+
setSearchQuery(item);
|
|
1679
|
+
setSearchResults(doSearch(item));
|
|
1680
|
+
addSearchHistory(item);
|
|
1681
|
+
searchInputRef.current?.focus();
|
|
1682
|
+
}}
|
|
1683
|
+
>
|
|
1684
|
+
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" style={{ color: s.textMuted, flexShrink: 0 }}>
|
|
1685
|
+
<circle cx="12" cy="12" r="10" />
|
|
1686
|
+
<polyline points="12 6 12 12 16 14" />
|
|
1687
|
+
</svg>
|
|
1688
|
+
<span className="flex-1 text-xs truncate">{item}</span>
|
|
1689
|
+
<button
|
|
1690
|
+
onClick={(e) => { e.stopPropagation(); removeSearchHistory(item); }}
|
|
1691
|
+
className="opacity-0 group-hover:opacity-100 shrink-0 text-[10px] transition-opacity"
|
|
1692
|
+
style={{ color: s.textMuted }}
|
|
1693
|
+
>✕</button>
|
|
1694
|
+
</div>
|
|
1695
|
+
))}
|
|
1696
|
+
</div>
|
|
1697
|
+
)}
|
|
1698
|
+
{!searchQuery.trim() && searchHistory.length === 0 && (
|
|
1699
|
+
<div className="px-3 py-4 text-center text-xs" style={{ color: s.textMuted }}>暂无搜索历史</div>
|
|
1700
|
+
)}
|
|
1701
|
+
<div className="px-3 py-1.5 flex items-center gap-3" style={{ borderTop: `1px solid ${s.border}`, color: s.textMuted }}>
|
|
1702
|
+
<span className="text-[9px]"><kbd className="px-1 py-0.5 rounded" style={{ backgroundColor: s.bgTertiary }}>↵</kbd> 搜索</span>
|
|
1703
|
+
<span className="text-[9px]"><kbd className="px-1 py-0.5 rounded" style={{ backgroundColor: s.bgTertiary }}>Esc</kbd> 关闭</span>
|
|
1704
|
+
</div>
|
|
1705
|
+
</div>
|
|
1706
|
+
)}
|
|
1707
|
+
</div>
|
|
1708
|
+
<button
|
|
1709
|
+
onClick={() => navigate('/extensions/create')}
|
|
1710
|
+
className="px-3 py-1.5 rounded-lg text-xs font-medium transition-colors"
|
|
1711
|
+
style={{ backgroundColor: 'var(--color-accent)', color: '#fff' }}
|
|
1712
|
+
>
|
|
1713
|
+
创建插件
|
|
1714
|
+
</button>
|
|
1715
|
+
<button
|
|
1716
|
+
onClick={() => setExportModalOpen(true)}
|
|
1717
|
+
className="px-3 py-1.5 rounded-lg text-xs font-medium transition-colors flex items-center gap-1.5"
|
|
1718
|
+
style={{ border: `1px solid ${s.border}`, color: s.textSecondary, backgroundColor: 'transparent' }}
|
|
1719
|
+
title="导出为 HTML 静态文件"
|
|
1720
|
+
>
|
|
1721
|
+
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
|
1722
|
+
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" />
|
|
1723
|
+
<polyline points="7 10 12 15 17 10" />
|
|
1724
|
+
<line x1="12" y1="15" x2="12" y2="3" />
|
|
1725
|
+
</svg>
|
|
1726
|
+
导出 HTML
|
|
1727
|
+
</button>
|
|
1728
|
+
<ThemeSwitcher buttonStyle={{ paddingTop: '0.375rem', paddingBottom: '0.375rem' }}/>
|
|
1729
|
+
</div>
|
|
1730
|
+
}
|
|
1731
|
+
/>
|
|
1732
|
+
|
|
1733
|
+
<div className="flex flex-1 overflow-hidden">
|
|
1734
|
+
{/* 左侧目录 */}
|
|
1735
|
+
<nav className="w-64 shrink-0 overflow-y-auto p-3 space-y-0.5" style={{ borderRight: `1px solid ${s.border}`, backgroundColor: s.bgSecondary }}>
|
|
1736
|
+
{sections.map((sec) => {
|
|
1737
|
+
const isActive = activeSection === sec.id;
|
|
1738
|
+
const hasSubs = sec.subtitles && sec.subtitles.length > 0;
|
|
1739
|
+
const isExpanded = expandedSection === sec.id;
|
|
1740
|
+
|
|
1741
|
+
return (
|
|
1742
|
+
<div key={sec.id}>
|
|
1743
|
+
<button
|
|
1744
|
+
onClick={() => handleSectionClick(sec.id)}
|
|
1745
|
+
className="w-full text-left px-3 py-2 rounded-lg text-xs flex items-center gap-2 transition-colors min-w-0"
|
|
1746
|
+
style={{
|
|
1747
|
+
backgroundColor: isActive ? s.bgTertiary : 'transparent',
|
|
1748
|
+
color: isActive ? s.accent : s.textSecondary,
|
|
1749
|
+
fontWeight: isActive ? 600 : 400,
|
|
1750
|
+
}}
|
|
1751
|
+
>
|
|
1752
|
+
<span className="shrink-0">{sec.icon}</span>
|
|
1753
|
+
<span className="truncate">{sec.title}</span>
|
|
1754
|
+
</button>
|
|
1755
|
+
{hasSubs && isExpanded && (
|
|
1756
|
+
<div className="ml-5 pl-3 space-y-0.5 mt-0.5" style={{ borderLeft: `1px solid ${s.border}` }}>
|
|
1757
|
+
{sec.subtitles!.map((sub) => (
|
|
1758
|
+
<button
|
|
1759
|
+
key={sub.id}
|
|
1760
|
+
onClick={() => handleSubClick(sec.id, sub.id)}
|
|
1761
|
+
className="w-full text-left px-2 py-1.5 rounded text-xs transition-colors truncate"
|
|
1762
|
+
style={{
|
|
1763
|
+
color: s.textMuted,
|
|
1764
|
+
}}
|
|
1765
|
+
>
|
|
1766
|
+
{sub.title}
|
|
1767
|
+
</button>
|
|
1768
|
+
))}
|
|
1769
|
+
</div>
|
|
1770
|
+
)}
|
|
1771
|
+
</div>
|
|
1772
|
+
);
|
|
1773
|
+
})}
|
|
1774
|
+
</nav>
|
|
1775
|
+
|
|
1776
|
+
{/* 右侧内容 */}
|
|
1777
|
+
<main ref={mainRef} className="flex-1 overflow-y-auto p-8">
|
|
1778
|
+
{sections.map((sec) => (
|
|
1779
|
+
<section key={sec.id} id={'section-' + sec.id} className="mb-10 scroll-mt-4">
|
|
1780
|
+
<h2 className="text-lg font-bold mb-4 flex items-center gap-2" style={{ color: s.textPrimary }}>
|
|
1781
|
+
<span>{sec.icon}</span>
|
|
1782
|
+
{sec.title}
|
|
1783
|
+
</h2>
|
|
1784
|
+
<div className="space-y-4" style={{ color: s.textSecondary, fontSize: '13px', lineHeight: '1.8' }}>
|
|
1785
|
+
{sec.content}
|
|
1786
|
+
</div>
|
|
1787
|
+
</section>
|
|
1788
|
+
))}
|
|
1789
|
+
</main>
|
|
1790
|
+
</div>
|
|
1791
|
+
|
|
1792
|
+
{/* 导出主题选择弹窗 */}
|
|
1793
|
+
{exportModalOpen && (
|
|
1794
|
+
<div
|
|
1795
|
+
className="fixed inset-0 z-50 flex items-center justify-center"
|
|
1796
|
+
style={{ backgroundColor: 'rgba(0,0,0,0.5)' }}
|
|
1797
|
+
onClick={() => setExportModalOpen(false)}
|
|
1798
|
+
>
|
|
1799
|
+
<div
|
|
1800
|
+
className="rounded-xl p-5 w-80"
|
|
1801
|
+
style={{ backgroundColor: s.bgSecondary, border: `1px solid ${s.border}` }}
|
|
1802
|
+
onClick={(e) => e.stopPropagation()}
|
|
1803
|
+
>
|
|
1804
|
+
<div className="flex items-center justify-between mb-4">
|
|
1805
|
+
<h3 className="text-sm font-bold" style={{ color: s.textPrimary }}>选择导出主题</h3>
|
|
1806
|
+
<button
|
|
1807
|
+
onClick={() => setExportModalOpen(false)}
|
|
1808
|
+
className="p-1 rounded-lg transition-colors"
|
|
1809
|
+
style={{ color: s.textMuted }}
|
|
1810
|
+
>
|
|
1811
|
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
|
1812
|
+
<line x1="18" y1="6" x2="6" y2="18" /><line x1="6" y1="6" x2="18" y2="18" />
|
|
1813
|
+
</svg>
|
|
1814
|
+
</button>
|
|
1815
|
+
</div>
|
|
1816
|
+
<div className="space-y-2">
|
|
1817
|
+
{/* 当前主题(默认) */}
|
|
1818
|
+
<button
|
|
1819
|
+
onClick={() => { setExportModalOpen(false); exportTutorialHTML(); }}
|
|
1820
|
+
className="w-full text-left px-3 py-2.5 rounded-lg text-xs transition-colors flex items-center gap-3"
|
|
1821
|
+
style={{ backgroundColor: s.bgTertiary, border: `1px solid ${s.accent}`, color: s.textPrimary }}
|
|
1822
|
+
>
|
|
1823
|
+
<span className="text-base">🎨</span>
|
|
1824
|
+
<div className="flex-1">
|
|
1825
|
+
<div className="font-medium">当前主题</div>
|
|
1826
|
+
<div className="text-[10px] mt-0.5" style={{ color: s.textMuted }}>使用页面当前应用的主题导出</div>
|
|
1827
|
+
</div>
|
|
1828
|
+
<span className="text-[10px] px-1.5 py-0.5 rounded" style={{ backgroundColor: s.bgPrimary, color: s.accent }}>默认</span>
|
|
1829
|
+
</button>
|
|
1830
|
+
{/* 预设主题列表 */}
|
|
1831
|
+
{presetThemes.map((theme) => (
|
|
1832
|
+
<button
|
|
1833
|
+
key={theme.id}
|
|
1834
|
+
onClick={() => { setExportModalOpen(false); exportTutorialHTML(theme.id); }}
|
|
1835
|
+
className="w-full text-left px-3 py-2.5 rounded-lg text-xs transition-colors flex items-center gap-3"
|
|
1836
|
+
style={{ border: `1px solid ${s.border}`, color: s.textSecondary }}
|
|
1837
|
+
onMouseEnter={(e) => { e.currentTarget.style.backgroundColor = s.bgTertiary; }}
|
|
1838
|
+
onMouseLeave={(e) => { e.currentTarget.style.backgroundColor = 'transparent'; }}
|
|
1839
|
+
>
|
|
1840
|
+
<span
|
|
1841
|
+
className="w-5 h-5 rounded-full shrink-0"
|
|
1842
|
+
style={{ backgroundColor: theme.variables['--color-accent'], border: `2px solid ${theme.variables['--color-border']}` }}
|
|
1843
|
+
/>
|
|
1844
|
+
<div className="flex-1">
|
|
1845
|
+
<div className="font-medium" style={{ color: s.textPrimary }}>{theme.name}</div>
|
|
1846
|
+
<div className="text-[10px] mt-0.5" style={{ color: s.textMuted }}>{theme.description}</div>
|
|
1847
|
+
</div>
|
|
1848
|
+
<span className="text-[10px]" style={{ color: s.textMuted }}>{theme.isDark ? '🌙 深色' : '☀️ 浅色'}</span>
|
|
1849
|
+
</button>
|
|
1850
|
+
))}
|
|
1851
|
+
</div>
|
|
1852
|
+
</div>
|
|
1853
|
+
</div>
|
|
1854
|
+
)}
|
|
1855
|
+
</div>
|
|
1856
|
+
);
|
|
1857
|
+
}
|
|
1858
|
+
|
|
1859
|
+
// ==================== 教程章节内容 ====================
|
|
1860
|
+
|
|
1861
|
+
function OverviewSection() {
|
|
1862
|
+
return (
|
|
1863
|
+
<div>
|
|
1864
|
+
<p>星语的插件系统允许你扩展游戏的核心功能,包括自定义游戏机制、消息渲染方式、AI 行为和用户界面。</p>
|
|
1865
|
+
<h3 id="sub-what-is-plugin" className="text-sm font-bold mt-6 mb-2 scroll-mt-4" style={{ color: s.textPrimary }}>什么是插件?</h3>
|
|
1866
|
+
<p>插件是一段 JavaScript 代码,通过导出一个 <code style={codeStyle}>setup(xinyu)</code> 函数来注册功能。插件可以:</p>
|
|
1867
|
+
<ul className="list-disc pl-5 space-y-1">
|
|
1868
|
+
<li>注册游戏属性(金币、HP 等)和快捷指令(/roll)</li>
|
|
1869
|
+
<li>自定义 AI 消息的渲染方式(Markdown、代码高亮等)</li>
|
|
1870
|
+
<li>修改 AI 的系统提示词和请求参数</li>
|
|
1871
|
+
<li>在输入框上方添加快捷操作按钮</li>
|
|
1872
|
+
<li>在游戏页面任意位置注入自定义 UI</li>
|
|
1873
|
+
<li>创建弹窗、确认对话框等交互组件</li>
|
|
1874
|
+
<li>注入自定义 CSS 样式</li>
|
|
1875
|
+
<li>监听宿主 UI 事件(消息接收、主题切换等)</li>
|
|
1876
|
+
</ul>
|
|
1877
|
+
<h3 id="sub-plugin-types" className="text-sm font-bold mt-6 mb-2 scroll-mt-4" style={{ color: s.textPrimary }}>插件类型</h3>
|
|
1878
|
+
<table className="w-full text-xs" style={{ borderCollapse: 'collapse' }}>
|
|
1879
|
+
<thead>
|
|
1880
|
+
<tr style={{ borderBottom: `1px solid ${s.border}` }}>
|
|
1881
|
+
<th className="text-left py-2 pr-4" style={{ color: s.textPrimary }}>类型</th>
|
|
1882
|
+
<th className="text-left py-2 pr-4" style={{ color: s.textPrimary }}>说明</th>
|
|
1883
|
+
<th className="text-left py-2" style={{ color: s.textPrimary }}>典型场景</th>
|
|
1884
|
+
</tr>
|
|
1885
|
+
</thead>
|
|
1886
|
+
<tbody>
|
|
1887
|
+
{[
|
|
1888
|
+
['🎮 game-mechanics', '游戏机制', '骰子系统、属性追踪、战斗系统'],
|
|
1889
|
+
['🎨 message-render', '消息渲染', 'Markdown 解析、代码高亮、特殊格式'],
|
|
1890
|
+
['🤖 ai-prompt', 'AI Prompt', 'NPC 人格注入、规则修改、风格调整'],
|
|
1891
|
+
['⌨️ input-enhance', '输入增强', '快捷按钮、指令面板、自动补全'],
|
|
1892
|
+
].map(([type, label, scene]) => (
|
|
1893
|
+
<tr key={type} style={{ borderBottom: `1px solid ${s.border}` }}>
|
|
1894
|
+
<td className="py-2 pr-4"><code style={codeStyle}>{type}</code></td>
|
|
1895
|
+
<td className="py-2 pr-4">{label}</td>
|
|
1896
|
+
<td className="py-2"><NavLink sectionId="examples">{scene}</NavLink></td>
|
|
1897
|
+
</tr>
|
|
1898
|
+
))}
|
|
1899
|
+
</tbody>
|
|
1900
|
+
</table>
|
|
1901
|
+
<h3 id="sub-minimal-plugin" className="text-sm font-bold mt-6 mb-2 scroll-mt-4" style={{ color: s.textPrimary }}>最简插件</h3>
|
|
1902
|
+
<pre style={preStyle}>{CODE_OVERVIEW_MINIMAL}</pre>
|
|
1903
|
+
<p>每个插件<strong>必须</strong>导出一个 <code style={codeStyle}>setup</code> 函数,它接收 <code style={codeStyle}>xinyu</code> 对象作为唯一参数,通过它访问所有 API。</p>
|
|
1904
|
+
</div>
|
|
1905
|
+
);
|
|
1906
|
+
}
|
|
1907
|
+
|
|
1908
|
+
function ArchitectureSection() {
|
|
1909
|
+
return (
|
|
1910
|
+
<div>
|
|
1911
|
+
<p>插件系统的架构分为四个层次:</p>
|
|
1912
|
+
<div className="space-y-3 mt-3">
|
|
1913
|
+
{[
|
|
1914
|
+
{ layer: '类型层', file: 'lib/plugin-types.ts', desc: '定义所有插件相关的 TypeScript 类型(PluginManifest、PluginBinding、UISlotId、UIPermission 等)' },
|
|
1915
|
+
{ layer: '运行时层', file: 'lib/plugin-runtime.ts', desc: '插件引擎核心,负责加载、执行、Hook 调度、错误隔离。通过 createXinyuBridge() 创建插件可用的 API 对象' },
|
|
1916
|
+
{ layer: '安全层', file: 'lib/plugin-security.ts + plugin-html-sanitizer.ts + plugin-dom-sandbox.ts', desc: '权限检查、HTML 净化、DOM 操作沙箱,确保插件不能执行危险操作' },
|
|
1917
|
+
{ layer: 'UI 层', file: 'components/ui/PluginProvider.tsx + PluginSlotRenderer.tsx', desc: 'React Context 桥接运行时与 UI,插槽渲染器、模态框渲染器、浮动层容器' },
|
|
1918
|
+
].map((item) => (
|
|
1919
|
+
<div key={item.layer} className="p-3 rounded-lg" style={{ backgroundColor: s.bgSecondary, border: `1px solid ${s.border}` }}>
|
|
1920
|
+
<div className="flex items-center gap-2 mb-1">
|
|
1921
|
+
<span className="text-xs font-bold" style={{ color: s.accent }}>{item.layer}</span>
|
|
1922
|
+
<code className="text-xs" style={{ color: s.textMuted, fontSize: '11px' }}>{item.file}</code>
|
|
1923
|
+
</div>
|
|
1924
|
+
<p className="text-xs" style={{ color: s.textSecondary }}>{item.desc}</p>
|
|
1925
|
+
</div>
|
|
1926
|
+
))}
|
|
1927
|
+
</div>
|
|
1928
|
+
<h3 id="sub-code-exec" className="text-sm font-bold mt-6 mb-2 scroll-mt-4" style={{ color: s.textPrimary }}>代码执行方式</h3>
|
|
1929
|
+
<p>插件代码通过 <code style={codeStyle}>new Function('xinyu', code)</code> 执行,这是一种轻量级的沙箱机制。插件代码运行在独立的函数作用域中,无法直接访问外部变量。</p>
|
|
1930
|
+
<pre style={preStyle}>{CODE_ARCHITECTURE_EXEC}</pre>
|
|
1931
|
+
<h3 id="sub-dual-scope" className="text-sm font-bold mt-6 mb-2 scroll-mt-4" style={{ color: s.textPrimary }}>双层作用域</h3>
|
|
1932
|
+
<p>插件支持两种作用域的绑定:</p>
|
|
1933
|
+
<ul className="list-disc pl-5 space-y-1">
|
|
1934
|
+
<li><strong>全局作用域</strong>(global):在所有游戏中生效</li>
|
|
1935
|
+
<li><strong>世界作用域</strong>(world):仅在指定游戏中生效,且可覆盖全局配置</li>
|
|
1936
|
+
</ul>
|
|
1937
|
+
</div>
|
|
1938
|
+
);
|
|
1939
|
+
}
|
|
1940
|
+
|
|
1941
|
+
function LifecycleSection() {
|
|
1942
|
+
return (
|
|
1943
|
+
<div>
|
|
1944
|
+
<p>插件有以下生命周期钩子,通过 <code style={codeStyle}>xinyu.plugin.on(hook, handler)</code> 注册:</p>
|
|
1945
|
+
<table className="w-full text-xs mt-3" style={{ borderCollapse: 'collapse' }}>
|
|
1946
|
+
<thead>
|
|
1947
|
+
<tr style={{ borderBottom: `1px solid ${s.border}` }}>
|
|
1948
|
+
<th className="text-left py-2 pr-4" style={{ color: s.textPrimary }}>钩子</th>
|
|
1949
|
+
<th className="text-left py-2 pr-4" style={{ color: s.textPrimary }}>触发时机</th>
|
|
1950
|
+
<th className="text-left py-2 pr-4" style={{ color: s.textPrimary }}>handler 参数</th>
|
|
1951
|
+
<th className="text-left py-2" style={{ color: s.textPrimary }}>用途</th>
|
|
1952
|
+
</tr>
|
|
1953
|
+
</thead>
|
|
1954
|
+
<tbody>
|
|
1955
|
+
{[
|
|
1956
|
+
['onLoad', '插件启用时', '(pluginId)', '初始化属性、注册指令、注入 UI'],
|
|
1957
|
+
['onPluginLoad', '任意插件加载完成时', '(pluginId)', '跨插件协作、检测依赖插件是否就绪'],
|
|
1958
|
+
['onUnload', '插件禁用时', '()', '清理资源(通常自动处理)'],
|
|
1959
|
+
['onGameInit', '游戏初始化(无聊天记录时)', '()', '设置初始属性、发送欢迎消息、加载依赖'],
|
|
1960
|
+
['onPluginBindingWorld', '插件被绑定到世界时', '(worldId)', '根据世界设定初始化插件状态'],
|
|
1961
|
+
['onConfigChange', '插件自身配置变更时', '(config)', '响应配置修改、更新内部状态'],
|
|
1962
|
+
['onGlobalConfigChange', '任意插件配置变更时', '(pluginId, config)', '跨插件协作、响应其他插件配置变化'],
|
|
1963
|
+
['onRenderMessage', '消息渲染前', '({ role, content })', '修改消息文本内容(链式管道)'],
|
|
1964
|
+
].map(([hook, timing, params, usage]) => (
|
|
1965
|
+
<tr key={hook} style={{ borderBottom: `1px solid ${s.border}` }}>
|
|
1966
|
+
<td className="py-2 pr-4"><code style={codeStyle}>{hook}</code></td>
|
|
1967
|
+
<td className="py-2 pr-4">{timing}</td>
|
|
1968
|
+
<td className="py-2 pr-4"><code style={{ ...codeStyle, color: 'var(--color-text-muted)', fontSize: '11px' }}>{params}</code></td>
|
|
1969
|
+
<td className="py-2">{usage}</td>
|
|
1970
|
+
</tr>
|
|
1971
|
+
))}
|
|
1972
|
+
</tbody>
|
|
1973
|
+
</table>
|
|
1974
|
+
<h3 id="sub-lifecycle-example" className="text-sm font-bold mt-6 mb-2 scroll-mt-4" style={{ color: s.textPrimary }}>使用示例</h3>
|
|
1975
|
+
<pre style={preStyle}>{CODE_LIFECYCLE_EXAMPLE}</pre>
|
|
1976
|
+
<div className="mt-6 p-4 rounded-lg" style={{ backgroundColor: s.bgSecondary, border: `1px solid ${s.border}` }}>
|
|
1977
|
+
<h4 className="text-xs font-bold mb-2" style={{ color: s.accent }}>💡 onGameInit 钩子详解</h4>
|
|
1978
|
+
<p className="text-xs leading-relaxed" style={{ color: s.textSecondary }}>
|
|
1979
|
+
<code style={codeStyle}>onGameInit</code> 在玩家进入游戏页面且当前会话没有任何聊天记录时触发,即游戏正式开始的时刻。整个游戏会话中只触发一次。
|
|
1980
|
+
</p>
|
|
1981
|
+
<ul className="list-disc pl-5 mt-2 space-y-1">
|
|
1982
|
+
<li className="text-xs" style={{ color: s.textSecondary }}><strong>与 onLoad 的区别</strong>:onLoad 在插件加载时触发(每次页面刷新),onGameInit 仅在游戏刚开始时触发</li>
|
|
1983
|
+
<li className="text-xs" style={{ color: s.textSecondary }}><strong>典型用途</strong>:设置初始游戏属性、发送欢迎消息、初始化游戏状态、加载依赖插件</li>
|
|
1984
|
+
</ul>
|
|
1985
|
+
<pre className="mt-3 p-3 rounded-lg text-xs overflow-x-auto" style={preStyle}>
|
|
1986
|
+
{`// 游戏初始化示例
|
|
1987
|
+
function setup(xinyu) {
|
|
1988
|
+
xinyu.plugin.on('onGameInit', function() {
|
|
1989
|
+
// 设置初始属性
|
|
1990
|
+
xinyu.game.registerAttribute({
|
|
1991
|
+
key: 'hp', label: '生命值', type: 'number',
|
|
1992
|
+
value: 100, min: 0, max: 100, icon: '❤️',
|
|
1993
|
+
group: '基础属性'
|
|
1994
|
+
});
|
|
1995
|
+
xinyu.game.registerAttribute({
|
|
1996
|
+
key: 'gold', label: '金币', type: 'number',
|
|
1997
|
+
value: 50, min: 0, icon: '💰',
|
|
1998
|
+
group: '基础属性'
|
|
1999
|
+
});
|
|
2000
|
+
// 发送欢迎消息
|
|
2001
|
+
xinyu.chat.insertSystemMessage('🎮 游戏开始!你的冒险即将展开...');
|
|
2002
|
+
});
|
|
2003
|
+
}`}
|
|
2004
|
+
</pre>
|
|
2005
|
+
</div>
|
|
2006
|
+
<div className="mt-6 p-4 rounded-lg" style={{ backgroundColor: s.bgSecondary, border: `1px solid ${s.border}` }}>
|
|
2007
|
+
<h4 className="text-xs font-bold mb-2" style={{ color: s.accent }}>💡 onConfigChange / onGlobalConfigChange 钩子</h4>
|
|
2008
|
+
<p className="text-xs leading-relaxed" style={{ color: s.textSecondary }}>
|
|
2009
|
+
<code style={codeStyle}>onConfigChange</code> 在<strong>插件自身</strong>配置被修改时触发(仅通知自己)。如需监听其他插件的配置变更,使用 <code style={codeStyle}>onGlobalConfigChange</code>。
|
|
2010
|
+
</p>
|
|
2011
|
+
<ul className="list-disc pl-5 mt-2 space-y-1">
|
|
2012
|
+
<li className="text-xs" style={{ color: s.textSecondary }}><strong>onConfigChange 参数</strong>:<code style={codeStyle}>{'(newConfig)'}</code> — 新的完整配置对象</li>
|
|
2013
|
+
<li className="text-xs" style={{ color: s.textSecondary }}><strong>onGlobalConfigChange 参数</strong>:<code style={codeStyle}>{'(pluginId, newConfig)'}</code> — pluginId 为配置变更的插件 ID,newConfig 为新配置</li>
|
|
2014
|
+
<li className="text-xs" style={{ color: s.textSecondary }}><strong>触发时机</strong>:配置变更后立即触发(同步),此时 <code style={codeStyle}>config.get()</code> 已返回新值</li>
|
|
2015
|
+
<li className="text-xs" style={{ color: s.textSecondary }}><strong>典型用途</strong>:根据新配置更新内部缓存、重新初始化依赖配置的模块</li>
|
|
2016
|
+
<li className="text-xs" style={{ color: s.textSecondary }}><strong>注意事项</strong>:不要在 handler 中调用 <code style={codeStyle}>config.set()</code> 修改自身配置,这会导致无限循环</li>
|
|
2017
|
+
</ul>
|
|
2018
|
+
<pre className="mt-3 p-3 rounded-lg text-xs overflow-x-auto" style={preStyle}>
|
|
2019
|
+
{`// 监听自身配置变更
|
|
2020
|
+
function setup(xinyu) {
|
|
2021
|
+
let cachedThreshold = xinyu.plugin.config.get().threshold;
|
|
2022
|
+
|
|
2023
|
+
xinyu.plugin.on('onConfigChange', function(newConfig) {
|
|
2024
|
+
// 更新内部缓存
|
|
2025
|
+
cachedThreshold = newConfig.threshold;
|
|
2026
|
+
xinyu.ui.toast('配置已更新:阈值 = ' + newConfig.threshold, 'info');
|
|
2027
|
+
});
|
|
2028
|
+
|
|
2029
|
+
// 监听任意插件的配置变更(跨插件协作场景)
|
|
2030
|
+
xinyu.plugin.on('onGlobalConfigChange', function(pluginId, newConfig) {
|
|
2031
|
+
console.log('插件 ' + pluginId + ' 的配置已变更', newConfig);
|
|
2032
|
+
});
|
|
2033
|
+
}`}
|
|
2034
|
+
</pre>
|
|
2035
|
+
</div>
|
|
2036
|
+
</div>
|
|
2037
|
+
);
|
|
2038
|
+
}
|
|
2039
|
+
|
|
2040
|
+
function GlobalApiSection() {
|
|
2041
|
+
return (
|
|
2042
|
+
<div>
|
|
2043
|
+
<p><code style={codeStyle}>xinyu</code> 对象包含 6 个命名空间,每个命名空间提供不同领域的 API:</p>
|
|
2044
|
+
<div className="space-y-3 mt-3">
|
|
2045
|
+
{[
|
|
2046
|
+
{ ns: 'xinyu.game', desc: '游戏状态与属性管理', apis: 'getWorldSetting / setWorldSetting / getSessionId / getMessages / getState / setState / registerAttribute / setAttribute / getAttributes', link: 'plugin-storage' },
|
|
2047
|
+
{ ns: 'xinyu.chat', desc: '聊天与消息操作', apis: 'send / insertSystemMessage / insertNotice / injectChatStyle / removeChatStyle / getHistory / clearHistory / registerCommand / registerMessageRenderer / input.*', link: 'api-chat' },
|
|
2048
|
+
{ ns: 'xinyu.ui', desc: 'UI 创建与交互(全局 API 核心)', apis: 'registerSlot / updateSlot / unregisterSlot / showModal / confirm / dom.* / injectStyle / onHostEvent / toast / registerSidebarPanel / registerInputToolbarButton / registerMessageCard / closeModal / onAction / removeStyle / offHostEvent', link: 'api-ui' },
|
|
2049
|
+
{ ns: 'xinyu.ai', desc: 'AI 拦截与修改', apis: 'onPromptBuild / onBeforeSend / onAfterReceive / onRequestConfig / onBeforeComplete', link: 'ai-interception' },
|
|
2050
|
+
{ ns: 'xinyu.plugin', desc: '插件自身管理与跨插件通信', apis: 'getManifest / config.get / config.set / storage / on / off / callPlugin / loadDependency / isPluginAvailable / getPluginExports / getResource / getResourceUrl', link: 'cross-plugin' },
|
|
2051
|
+
{ ns: 'xinyu.utils', desc: '通用工具函数', apis: 'randomInt / rollDice / formatDate / debounce / throttle / eventBus', link: 'api-utils' },
|
|
2052
|
+
].map((item) => (
|
|
2053
|
+
<div key={item.ns} className="p-3 rounded-lg" style={{ backgroundColor: s.bgSecondary, border: `1px solid ${s.border}` }}>
|
|
2054
|
+
<code className="text-xs font-bold" style={{ color: s.accent }}>{item.ns}</code>
|
|
2055
|
+
<NavLink sectionId={item.link!} style={{ fontSize: '11px', marginLeft: '4px' }}>{item.desc}</NavLink>
|
|
2056
|
+
<div className="mt-1 text-xs" style={{ color: s.textMuted, fontSize: '11px' }}>{item.apis}</div>
|
|
2057
|
+
</div>
|
|
2058
|
+
))}
|
|
2059
|
+
</div>
|
|
2060
|
+
<h3 id="sub-common-api" className="text-sm font-bold mt-6 mb-2 scroll-mt-4" style={{ color: s.textPrimary }}>所有插件通用 API</h3>
|
|
2061
|
+
<p>以下 API 在所有类型的插件中都可用,无需额外权限声明:</p>
|
|
2062
|
+
<ul className="list-disc pl-5 space-y-1">
|
|
2063
|
+
<li><NavLink sectionId="plugin-storage">xinyu.plugin.storage</NavLink> — 插件独立存储(get/set/remove/keys,内存,刷新后重置)</li>
|
|
2064
|
+
<li><NavLink sectionId="persistent-storage">xinyu.storage</NavLink> — 持久化存储(get/set/remove/keys,数据库,需 <code style={codeStyle}>storage:plugin</code> 权限)</li>
|
|
2065
|
+
<li><NavLink sectionId="file-api">xinyu.file</NavLink> — 文件操作(read/write/remove/list,需 <code style={codeStyle}>file:read</code>/<code style={codeStyle}>file:write</code> 权限)</li>
|
|
2066
|
+
<li><code style={codeStyle}>xinyu.plugin.on(hook, handler)</code> — 注册生命周期钩子</li>
|
|
2067
|
+
<li><code style={codeStyle}>xinyu.plugin.off(hook, handler)</code> — 注销生命周期钩子</li>
|
|
2068
|
+
<li><code style={codeStyle}>xinyu.utils.eventBus</code> — 插件间通信事件总线</li>
|
|
2069
|
+
<li><code style={codeStyle}>xinyu.utils.rollDice(notation)</code> — 骰子表达式解析</li>
|
|
2070
|
+
<li><code style={codeStyle}>xinyu.utils.randomInt(min, max)</code> — 随机整数</li>
|
|
2071
|
+
<li><code style={codeStyle}>xinyu.utils.debounce(fn, ms)</code> — 防抖</li>
|
|
2072
|
+
<li><code style={codeStyle}>xinyu.utils.throttle(fn, ms)</code> — 节流</li>
|
|
2073
|
+
<li><code style={codeStyle}>xinyu.plugin.callPlugin(targetPluginId, method, ...args)</code> — 跨插件调用(需声明依赖)</li>
|
|
2074
|
+
<li><code style={codeStyle}>xinyu.plugin.loadDependency(targetPluginId, options?)</code> — 运行时加载依赖(支持降级)</li>
|
|
2075
|
+
<li><code style={codeStyle}>xinyu.plugin.isPluginAvailable(pluginId)</code> — 检查插件是否可用</li>
|
|
2076
|
+
<li><code style={codeStyle}>xinyu.plugin.getPluginExports(pluginId)</code> — 获取插件的公开 API 列表</li>
|
|
2077
|
+
<li><NavLink sectionId="sub-resource-api"><code style={codeStyle}>xinyu.plugin.getResource(path)</code></NavLink> — 获取插件资源文件内容(需 <code style={codeStyle}>file:read</code> 权限)</li>
|
|
2078
|
+
<li><NavLink sectionId="sub-resource-api"><code style={codeStyle}>xinyu.plugin.getResourceUrl(path)</code></NavLink> — 获取插件资源文件 HTTP URL(需 <code style={codeStyle}>file:read</code> 权限)</li>
|
|
2079
|
+
</ul>
|
|
2080
|
+
<p className="mt-2">UI 相关 API(registerSlot、showModal、confirm、dom.*、injectStyle、onHostEvent)需要在插件的权限声明(<code style={codeStyle}>commonPermissions</code> / <code style={codeStyle}>exclusivePermissions</code>)中声明对应权限,详见 <NavLink sectionId="permissions">权限系统</NavLink>。</p>
|
|
2081
|
+
</div>
|
|
2082
|
+
);
|
|
2083
|
+
}
|
|
2084
|
+
|
|
2085
|
+
// ==================== Chat API 详解 ====================
|
|
2086
|
+
|
|
2087
|
+
function ChatApiSection() {
|
|
2088
|
+
return (
|
|
2089
|
+
<div>
|
|
2090
|
+
<h3 id="sub-chat-send" className="text-sm font-bold mt-6 mb-2 scroll-mt-4" style={{ color: s.textPrimary }}>send / insertSystemMessage</h3>
|
|
2091
|
+
<p className="text-xs leading-relaxed" style={{ color: s.textSecondary }}>
|
|
2092
|
+
<code style={codeStyle}>xinyu.chat.send(content)</code> — 以用户身份发送一条消息到聊天窗口,触发 AI 回复。
|
|
2093
|
+
</p>
|
|
2094
|
+
<p className="text-xs leading-relaxed mt-1" style={{ color: s.textSecondary }}>
|
|
2095
|
+
<code style={codeStyle}>xinyu.chat.insertSystemMessage(content)</code> — 插入一条系统消息(灰色提示),不触发 AI 回复。
|
|
2096
|
+
</p>
|
|
2097
|
+
<pre className="mt-2 p-3 rounded-lg text-xs overflow-x-auto" style={preStyle}>
|
|
2098
|
+
{`// 插入系统提示
|
|
2099
|
+
xinyu.chat.insertSystemMessage("🎲 骰子大师已加载,输入 /roll 掷骰");
|
|
2100
|
+
|
|
2101
|
+
// 以用户身份发送消息
|
|
2102
|
+
await xinyu.chat.send("帮我生成一个随机 NPC");`}
|
|
2103
|
+
</pre>
|
|
2104
|
+
|
|
2105
|
+
<h3 id="sub-chat-notice" className="text-sm font-bold mt-6 mb-2 scroll-mt-4" style={{ color: s.textPrimary }}>insertNotice</h3>
|
|
2106
|
+
<p className="text-xs leading-relaxed" style={{ color: s.textSecondary }}>
|
|
2107
|
+
<code style={codeStyle}>xinyu.chat.insertNotice(html)</code> — 插入一条通知消息到聊天区域。与 <code style={codeStyle}>insertSystemMessage</code> 的区别:
|
|
2108
|
+
</p>
|
|
2109
|
+
<ul className="list-disc list-inside space-y-1 mt-1 text-xs" style={{ color: s.textSecondary }}>
|
|
2110
|
+
<li><strong>不发送给 AI</strong> — 通知消息在发送请求时自动过滤,不会消耗 token</li>
|
|
2111
|
+
<li><strong>支持自定义 HTML</strong> — 可以传入任意 HTML 实现自定义样式</li>
|
|
2112
|
+
<li><strong>自动持久化</strong> — 复用聊天记录存储机制,刷新页面后仍然存在</li>
|
|
2113
|
+
</ul>
|
|
2114
|
+
<pre className="mt-2 p-3 rounded-lg text-xs overflow-x-auto" style={preStyle}>
|
|
2115
|
+
{`// 纯文本通知
|
|
2116
|
+
xinyu.chat.insertNotice("🎲 骰子大师已加载,输入 /roll 掷骰");
|
|
2117
|
+
|
|
2118
|
+
// 自定义样式通知
|
|
2119
|
+
xinyu.chat.insertNotice(
|
|
2120
|
+
'<div style="background:linear-gradient(135deg,#667eea,#764ba2);' +
|
|
2121
|
+
'color:#fff;padding:12px 20px;border-radius:12px;font-weight:bold">' +
|
|
2122
|
+
'🎲 骰子大师已加载' +
|
|
2123
|
+
'</div>'
|
|
2124
|
+
);`}
|
|
2125
|
+
</pre>
|
|
2126
|
+
|
|
2127
|
+
<h3 id="sub-chat-style" className="text-sm font-bold mt-6 mb-2 scroll-mt-4" style={{ color: s.textPrimary }}>injectChatStyle / removeChatStyle</h3>
|
|
2128
|
+
<p className="text-xs leading-relaxed" style={{ color: s.textSecondary }}>
|
|
2129
|
+
<code style={codeStyle}>xinyu.chat.injectChatStyle(css)</code> — 向消息区域注入自定义 CSS 样式。需要 <code style={codeStyle}>style:inject</code> 权限。
|
|
2130
|
+
</p>
|
|
2131
|
+
<p className="text-xs leading-relaxed mt-1" style={{ color: s.textSecondary }}>
|
|
2132
|
+
消息区域容器的 ID 为 <code style={codeStyle}>#xinyu-chat-window</code>,消息气泡使用 <code style={codeStyle}>.px-4.py-1.5</code> 等类名定位。返回 <code style={codeStyle}>styleId</code>,可通过 <code style={codeStyle}>removeChatStyle(styleId)</code> 移除。
|
|
2133
|
+
</p>
|
|
2134
|
+
<pre className="mt-2 p-3 rounded-lg text-xs overflow-x-auto" style={preStyle}>
|
|
2135
|
+
{`// 修改消息区域背景
|
|
2136
|
+
xinyu.chat.injectChatStyle(\`
|
|
2137
|
+
#xinyu-chat-window {
|
|
2138
|
+
background: linear-gradient(180deg, #0a0a1a 0%, #1a1a2e 100%) !important;
|
|
2139
|
+
}
|
|
2140
|
+
\`);
|
|
2141
|
+
|
|
2142
|
+
// 修改 AI 消息气泡样式
|
|
2143
|
+
xinyu.chat.injectChatStyle(\`
|
|
2144
|
+
#xinyu-chat-window [style*="ai-bubble"] {
|
|
2145
|
+
background: rgba(102, 126, 234, 0.15) !important;
|
|
2146
|
+
border: 1px solid rgba(102, 126, 234, 0.3) !important;
|
|
2147
|
+
border-radius: 16px !important;
|
|
2148
|
+
}
|
|
2149
|
+
\`);`}
|
|
2150
|
+
</pre>
|
|
2151
|
+
|
|
2152
|
+
<h3 id="sub-chat-command" className="text-sm font-bold mt-6 mb-2 scroll-mt-4" style={{ color: s.textPrimary }}>registerCommand</h3>
|
|
2153
|
+
<p className="text-xs leading-relaxed" style={{ color: s.textSecondary }}>
|
|
2154
|
+
注册快捷指令,用户在输入框输入 <code style={codeStyle}>/指令名 参数</code> 即可触发。
|
|
2155
|
+
</p>
|
|
2156
|
+
<div className="mt-2 p-3 rounded-lg text-xs" style={{ background: s.bgTertiary, border: `1px solid ${s.border}` }}>
|
|
2157
|
+
<p className="font-bold mb-1" style={{ color: s.textPrimary }}>命令格式</p>
|
|
2158
|
+
<ul className="list-disc list-inside space-y-1" style={{ color: s.textSecondary }}>
|
|
2159
|
+
<li><code style={codeStyle}>{'/command args'}</code> — 精确匹配:仅匹配注册名与 command 完全一致的指令</li>
|
|
2160
|
+
<li><code style={codeStyle}>{'/pluginId/command args'}</code> — 指定插件:通过插件 ID 前缀精确调用指定插件的命令,避免命名冲突</li>
|
|
2161
|
+
<li>广播模式:当多个插件注册了同名命令时,系统会依次调用所有匹配的 handler,直到某个 handler 返回非 void 值为止</li>
|
|
2162
|
+
</ul>
|
|
2163
|
+
<p className="font-bold mt-3 mb-1" style={{ color: s.textPrimary }}>返回值语义</p>
|
|
2164
|
+
<ul className="list-disc list-inside space-y-1" style={{ color: s.textSecondary }}>
|
|
2165
|
+
<li><code style={codeStyle}>{'string'}</code> — 将返回值显示为用户消息,并阻止该输入继续发送给 AI</li>
|
|
2166
|
+
<li><code style={codeStyle}>{'void / undefined'}</code> — 表示未匹配或无需处理,输入将继续作为普通消息发送</li>
|
|
2167
|
+
<li><code style={codeStyle}>{`'handled'`}</code> — 静默处理:命令已被消费,不显示任何消息,也不继续发送</li>
|
|
2168
|
+
</ul>
|
|
2169
|
+
</div>
|
|
2170
|
+
<table className="w-full mt-2 text-xs">
|
|
2171
|
+
<thead><tr style={{ borderBottom: `1px solid ${s.border}` }}>
|
|
2172
|
+
<th className="py-1.5 text-left font-medium" style={{ color: s.textMuted }}>参数</th>
|
|
2173
|
+
<th className="py-1.5 text-left font-medium" style={{ color: s.textMuted }}>类型</th>
|
|
2174
|
+
<th className="py-1.5 text-left font-medium" style={{ color: s.textMuted }}>说明</th>
|
|
2175
|
+
</tr></thead>
|
|
2176
|
+
<tbody>
|
|
2177
|
+
<tr style={{ borderBottom: `1px solid ${s.border}` }}>
|
|
2178
|
+
<td className="py-1.5"><code style={codeStyle}>name</code></td>
|
|
2179
|
+
<td className="py-1.5" style={{ color: s.textMuted }}>string</td>
|
|
2180
|
+
<td className="py-1.5" style={{ color: s.textSecondary }}>指令名(不含 /)</td>
|
|
2181
|
+
</tr>
|
|
2182
|
+
<tr style={{ borderBottom: `1px solid ${s.border}` }}>
|
|
2183
|
+
<td className="py-1.5"><code style={codeStyle}>description</code></td>
|
|
2184
|
+
<td className="py-1.5" style={{ color: s.textMuted }}>string</td>
|
|
2185
|
+
<td className="py-1.5" style={{ color: s.textSecondary }}>指令描述</td>
|
|
2186
|
+
</tr>
|
|
2187
|
+
<tr style={{ borderBottom: `1px solid ${s.border}` }}>
|
|
2188
|
+
<td className="py-1.5"><code style={codeStyle}>icon</code></td>
|
|
2189
|
+
<td className="py-1.5" style={{ color: s.textMuted }}>string?</td>
|
|
2190
|
+
<td className="py-1.5" style={{ color: s.textSecondary }}>图标(可选)</td>
|
|
2191
|
+
</tr>
|
|
2192
|
+
<tr>
|
|
2193
|
+
<td className="py-1.5"><code style={codeStyle}>handler</code></td>
|
|
2194
|
+
<td className="py-1.5" style={{ color: s.textMuted }}>(args) => string | void | Promise<string | void></td>
|
|
2195
|
+
<td className="py-1.5" style={{ color: s.textSecondary }}>处理函数,返回值会显示为系统消息</td>
|
|
2196
|
+
</tr>
|
|
2197
|
+
</tbody>
|
|
2198
|
+
</table>
|
|
2199
|
+
<pre className="mt-2 p-3 rounded-lg text-xs overflow-x-auto" style={preStyle}>
|
|
2200
|
+
{`xinyu.chat.registerCommand({
|
|
2201
|
+
name: 'roll',
|
|
2202
|
+
description: '掷骰子',
|
|
2203
|
+
icon: '🎲',
|
|
2204
|
+
handler: function(args) {
|
|
2205
|
+
var result = xinyu.utils.rollDice(args || '1d20');
|
|
2206
|
+
return '🎲 掷骰结果: ' + result;
|
|
2207
|
+
}
|
|
2208
|
+
});`}
|
|
2209
|
+
</pre>
|
|
2210
|
+
|
|
2211
|
+
<h3 id="sub-chat-renderer" className="text-sm font-bold mt-6 mb-2 scroll-mt-4" style={{ color: s.textPrimary }}>registerMessageRenderer</h3>
|
|
2212
|
+
<p className="text-xs leading-relaxed" style={{ color: s.textSecondary }}>
|
|
2213
|
+
<code style={codeStyle}>xinyu.chat.registerMessageRenderer(matcher, renderer)</code> — 注册消息渲染器,可以自定义特定消息的显示方式(如将骰子结果渲染为卡片)。当 <code style={codeStyle}>matcher(msg)</code> 返回 <code style={codeStyle}>true</code> 时,使用 <code style={codeStyle}>renderer(msg)</code> 返回的 HTML 替代默认气泡渲染。多个渲染器按注册顺序匹配,第一个命中的生效。
|
|
2214
|
+
</p>
|
|
2215
|
+
<div className="mt-2 p-2 rounded-lg" style={{ backgroundColor: s.bgSecondary, border: `1px solid ${s.border}` }}>
|
|
2216
|
+
<p className="text-xs font-bold mb-1" style={{ color: s.accent }}>renderer 返回值说明</p>
|
|
2217
|
+
<ul className="text-xs space-y-0.5" style={{ color: s.textSecondary }}>
|
|
2218
|
+
<li>返回<strong> HTML 字符串</strong>:替代默认气泡渲染(经过安全净化)</li>
|
|
2219
|
+
<li>返回 <code style={codeStyle}>null</code>:<strong>跳过渲染</strong>,该消息不会显示在聊天区域</li>
|
|
2220
|
+
</ul>
|
|
2221
|
+
</div>
|
|
2222
|
+
<pre className="mt-2 p-3 rounded-lg text-xs overflow-x-auto" style={preStyle}>
|
|
2223
|
+
{`// 示例 1:自定义渲染骰子结果
|
|
2224
|
+
xinyu.chat.registerMessageRenderer(
|
|
2225
|
+
function(msg) {
|
|
2226
|
+
return msg.role === 'assistant' && msg.content.indexOf('🎲') !== -1;
|
|
2227
|
+
},
|
|
2228
|
+
function(msg) {
|
|
2229
|
+
return '<div style="padding:8px;background:var(--color-bg-tertiary);border-radius:8px">'
|
|
2230
|
+
+ msg.content + '</div>';
|
|
2231
|
+
}
|
|
2232
|
+
);
|
|
2233
|
+
|
|
2234
|
+
// 示例 2:跳过渲染(消息不显示)
|
|
2235
|
+
xinyu.chat.registerMessageRenderer(
|
|
2236
|
+
function(msg) {
|
|
2237
|
+
// 匹配以 [隐藏] 开头的消息
|
|
2238
|
+
return msg.content.indexOf('[隐藏]') === 0;
|
|
2239
|
+
},
|
|
2240
|
+
function(msg) {
|
|
2241
|
+
return null; // 跳过渲染,该消息不会显示
|
|
2242
|
+
}
|
|
2243
|
+
);`}
|
|
2244
|
+
</pre>
|
|
2245
|
+
<p className="mt-2 text-xs" style={{ color: s.textMuted }}>
|
|
2246
|
+
💡 <strong>注意</strong>:返回的 HTML 会经过安全净化,仅允许安全的标签和属性。多个渲染器按注册顺序匹配,第一个 <code style={codeStyle}>matcher</code> 返回 <code style={codeStyle}>true</code> 的渲染器生效,后续渲染器不再执行。
|
|
2247
|
+
</p>
|
|
2248
|
+
|
|
2249
|
+
<h3 id="sub-chat-on-render-message" className="text-sm font-bold mt-6 mb-2 scroll-mt-4" style={{ color: s.textPrimary }}>onRenderMessage(消息内容管道)</h3>
|
|
2250
|
+
<p className="text-xs leading-relaxed" style={{ color: s.textSecondary }}>
|
|
2251
|
+
<code style={codeStyle}>xinyu.plugin.on('onRenderMessage', handler)</code> — 注册消息内容修改器。每条消息渲染前,所有已注册的 handler 按**注册顺序依次执行**,每个 handler 接收当前消息对象(含经前一个 handler 修改后的 content),返回修改后的 content 字符串。
|
|
2252
|
+
</p>
|
|
2253
|
+
<p className="mt-1 text-xs" style={{ color: s.textSecondary }}>
|
|
2254
|
+
与 <code style={codeStyle}>registerMessageRenderer</code> 的区别:<code style={codeStyle}>registerMessageRenderer</code> 是**完全接管渲染**(返回 HTML 替代气泡),而 <code style={codeStyle}>onRenderMessage</code> 是**仅修改文本内容**(保留默认气泡样式)。多个插件可以通过 <code style={codeStyle}>onRenderMessage</code> 链式修改同一条消息。
|
|
2255
|
+
</p>
|
|
2256
|
+
<div className="mt-2 p-2 rounded-lg" style={{ backgroundColor: s.bgSecondary, border: `1px solid ${s.border}` }}>
|
|
2257
|
+
<p className="text-xs font-bold mb-1" style={{ color: s.accent }}>返回值说明</p>
|
|
2258
|
+
<ul className="text-xs space-y-0.5" style={{ color: s.textSecondary }}>
|
|
2259
|
+
<li>返回<strong>字符串</strong>:修改消息内容,传递给下一个 handler 继续处理</li>
|
|
2260
|
+
<li>返回 <code style={codeStyle}>null</code>:<strong>跳过渲染</strong>,该消息不会显示在聊天区域,管道立即终止</li>
|
|
2261
|
+
</ul>
|
|
2262
|
+
</div>
|
|
2263
|
+
<pre className="mt-2 p-3 rounded-lg text-xs overflow-x-auto" style={preStyle}>
|
|
2264
|
+
{`// 示例 1:给所有 AI 消息添加前缀
|
|
2265
|
+
xinyu.plugin.on('onRenderMessage', function(msg) {
|
|
2266
|
+
if (msg.role === 'assistant') {
|
|
2267
|
+
return '🌍 ' + msg.content;
|
|
2268
|
+
}
|
|
2269
|
+
return msg.content;
|
|
2270
|
+
});
|
|
2271
|
+
|
|
2272
|
+
// 示例 2:跳过渲染(消息不显示)
|
|
2273
|
+
xinyu.plugin.on('onRenderMessage', function(msg) {
|
|
2274
|
+
// 过滤掉系统提示类消息
|
|
2275
|
+
if (msg.content.indexOf('[系统提示]') === 0) {
|
|
2276
|
+
return null; // 跳过渲染,该消息不会显示
|
|
2277
|
+
}
|
|
2278
|
+
return msg.content;
|
|
2279
|
+
});
|
|
2280
|
+
|
|
2281
|
+
// 示例 3:链式修改(多个插件叠加)
|
|
2282
|
+
// 插件 A 先执行(添加前缀),插件 B 后执行(替换关键词)
|
|
2283
|
+
xinyu.plugin.on('onRenderMessage', function(msg) {
|
|
2284
|
+
return msg.content.replace(/\\[\\[(.+?)\\]\\]/g,
|
|
2285
|
+
'<span style="color:var(--color-accent);font-weight:bold">[$1]</span>');
|
|
2286
|
+
});`}
|
|
2287
|
+
</pre>
|
|
2288
|
+
<p className="mt-2 text-xs" style={{ color: s.textMuted }}>
|
|
2289
|
+
💡 <strong>执行顺序</strong>:handler 按插件加载顺序执行(依赖拓扑排序后的顺序)。返回 <code style={codeStyle}>null</code> 时管道立即终止,后续 handler 不再执行。返回非字符串非 null 值时,该次修改会被跳过(content 保持不变)。
|
|
2290
|
+
</p>
|
|
2291
|
+
</div>
|
|
2292
|
+
);
|
|
2293
|
+
}
|
|
2294
|
+
|
|
2295
|
+
// ==================== 输入框 API 详解 ====================
|
|
2296
|
+
|
|
2297
|
+
function InputApiSection() {
|
|
2298
|
+
return (
|
|
2299
|
+
<div>
|
|
2300
|
+
<h3 id="sub-input-overview" className="text-sm font-bold mt-6 mb-2 scroll-mt-4" style={{ color: s.textPrimary }}>概述</h3>
|
|
2301
|
+
<p className="text-xs leading-relaxed" style={{ color: s.textSecondary }}>
|
|
2302
|
+
<code style={codeStyle}>xinyu.chat.input</code> 是输入框控制 API,允许插件读取和修改聊天输入框的内容、状态、样式,并监听输入相关事件。
|
|
2303
|
+
</p>
|
|
2304
|
+
<p className="text-xs leading-relaxed mt-1" style={{ color: s.textSecondary }}>
|
|
2305
|
+
使用前需要在 manifest 中声明 <code style={codeStyle}>input:control</code> 权限:
|
|
2306
|
+
</p>
|
|
2307
|
+
<pre className="mt-2 p-3 rounded-lg text-xs overflow-x-auto" style={preStyle}>
|
|
2308
|
+
{`// manifest.json
|
|
2309
|
+
{
|
|
2310
|
+
"permissions": {
|
|
2311
|
+
"commonPermissions": ["input:control"]
|
|
2312
|
+
}
|
|
2313
|
+
}`}
|
|
2314
|
+
</pre>
|
|
2315
|
+
<p className="mt-2 text-xs" style={{ color: s.textMuted }}>
|
|
2316
|
+
所有方法在 textarea 不存在时静默返回(不会抛出异常),因此无需手动检查输入框是否存在。
|
|
2317
|
+
</p>
|
|
2318
|
+
|
|
2319
|
+
<h3 id="sub-input-content" className="text-sm font-bold mt-6 mb-2 scroll-mt-4" style={{ color: s.textPrimary }}>内容操作</h3>
|
|
2320
|
+
|
|
2321
|
+
<p className="text-xs leading-relaxed" style={{ color: s.textSecondary }}>
|
|
2322
|
+
<code style={codeStyle}>getContent(): string | null</code> — 获取输入框当前文本内容。输入框为空时返回空字符串,textarea 不存在时返回 <code style={codeStyle}>null</code>。
|
|
2323
|
+
</p>
|
|
2324
|
+
<p className="text-xs leading-relaxed mt-1" style={{ color: s.textSecondary }}>
|
|
2325
|
+
<code style={codeStyle}>setContent(text: string): void</code> — 设置输入框的完整文本内容,会替换所有已有内容。
|
|
2326
|
+
</p>
|
|
2327
|
+
<p className="text-xs leading-relaxed mt-1" style={{ color: s.textSecondary }}>
|
|
2328
|
+
<code style={codeStyle}>appendContent(text: string): void</code> — 在光标位置追加文本,光标会移动到追加内容之后。
|
|
2329
|
+
</p>
|
|
2330
|
+
<p className="text-xs leading-relaxed mt-1" style={{ color: s.textSecondary }}>
|
|
2331
|
+
<code style={codeStyle}>clearContent(): void</code> — 清空输入框所有内容。
|
|
2332
|
+
</p>
|
|
2333
|
+
<p className="text-xs leading-relaxed mt-1" style={{ color: s.textSecondary }}>
|
|
2334
|
+
<code style={codeStyle}>insertAtCursor(text: string): void</code> — 在光标位置插入文本,与 <code style={codeStyle}>appendContent</code> 行为一致。
|
|
2335
|
+
</p>
|
|
2336
|
+
<pre className="mt-2 p-3 rounded-lg text-xs overflow-x-auto" style={preStyle}>
|
|
2337
|
+
{`// 获取当前输入内容
|
|
2338
|
+
var text = xinyu.chat.input.getContent();
|
|
2339
|
+
if (text) {
|
|
2340
|
+
xinyu.ui.toast('当前输入: ' + text, 'info');
|
|
2341
|
+
}
|
|
2342
|
+
|
|
2343
|
+
// 设置输入框内容
|
|
2344
|
+
xinyu.chat.input.setContent('/roll 2d6+3');
|
|
2345
|
+
|
|
2346
|
+
// 在光标位置追加文本
|
|
2347
|
+
xinyu.chat.input.appendContent(' 攻击力检定');
|
|
2348
|
+
|
|
2349
|
+
// 在光标位置插入文本
|
|
2350
|
+
xinyu.chat.input.insertAtCursor('[力量+2] ');
|
|
2351
|
+
|
|
2352
|
+
// 清空输入框
|
|
2353
|
+
xinyu.chat.input.clearContent();`}
|
|
2354
|
+
</pre>
|
|
2355
|
+
|
|
2356
|
+
<h3 id="sub-input-state" className="text-sm font-bold mt-6 mb-2 scroll-mt-4" style={{ color: s.textPrimary }}>状态控制</h3>
|
|
2357
|
+
|
|
2358
|
+
<p className="text-xs leading-relaxed" style={{ color: s.textSecondary }}>
|
|
2359
|
+
<code style={codeStyle}>setPlaceholder(text: string): void</code> — 设置输入框的占位符文本。
|
|
2360
|
+
</p>
|
|
2361
|
+
<p className="text-xs leading-relaxed mt-1" style={{ color: s.textSecondary }}>
|
|
2362
|
+
<code style={codeStyle}>setDisabled(disabled: boolean): void</code> — 启用或禁用输入框。禁用后用户无法编辑内容。
|
|
2363
|
+
</p>
|
|
2364
|
+
<p className="text-xs leading-relaxed mt-1" style={{ color: s.textSecondary }}>
|
|
2365
|
+
<code style={codeStyle}>focus(): void</code> — 聚焦输入框,将光标移入输入区域。
|
|
2366
|
+
</p>
|
|
2367
|
+
<p className="text-xs leading-relaxed mt-1" style={{ color: s.textSecondary }}>
|
|
2368
|
+
<code style={codeStyle}>blur(): void</code> — 使输入框失去焦点。
|
|
2369
|
+
</p>
|
|
2370
|
+
<p className="text-xs leading-relaxed mt-1" style={{ color: s.textSecondary }}>
|
|
2371
|
+
<code style={codeStyle}>setMaxLength(length: number): void</code> — 设置输入框的最大输入长度限制。
|
|
2372
|
+
</p>
|
|
2373
|
+
<p className="text-xs leading-relaxed mt-1" style={{ color: s.textSecondary }}>
|
|
2374
|
+
<code style={codeStyle}>getLength(): number</code> — 获取输入框当前内容的字符长度。
|
|
2375
|
+
</p>
|
|
2376
|
+
<pre className="mt-2 p-3 rounded-lg text-xs overflow-x-auto" style={preStyle}>
|
|
2377
|
+
{`// 设置占位符
|
|
2378
|
+
xinyu.chat.input.setPlaceholder('输入指令或消息...');
|
|
2379
|
+
|
|
2380
|
+
// 禁用输入框(例如在 AI 回复期间)
|
|
2381
|
+
xinyu.chat.input.setDisabled(true);
|
|
2382
|
+
// 稍后恢复
|
|
2383
|
+
xinyu.chat.input.setDisabled(false);
|
|
2384
|
+
|
|
2385
|
+
// 聚焦输入框
|
|
2386
|
+
xinyu.chat.input.focus();
|
|
2387
|
+
|
|
2388
|
+
// 设置最大长度
|
|
2389
|
+
xinyu.chat.input.setMaxLength(500);
|
|
2390
|
+
|
|
2391
|
+
// 获取当前内容长度
|
|
2392
|
+
var len = xinyu.chat.input.getLength();
|
|
2393
|
+
xinyu.ui.toast('已输入 ' + len + ' / 500 字符', 'info');`}
|
|
2394
|
+
</pre>
|
|
2395
|
+
|
|
2396
|
+
<h3 id="sub-input-style" className="text-sm font-bold mt-6 mb-2 scroll-mt-4" style={{ color: s.textPrimary }}>样式修改</h3>
|
|
2397
|
+
|
|
2398
|
+
<p className="text-xs leading-relaxed" style={{ color: s.textSecondary }}>
|
|
2399
|
+
<code style={codeStyle}>setStyle(styles: Record<string, string>): void</code> — 直接设置输入框的内联样式。传入一个样式键值对对象,会合并到现有样式中。
|
|
2400
|
+
</p>
|
|
2401
|
+
<p className="text-xs leading-relaxed mt-1" style={{ color: s.textSecondary }}>
|
|
2402
|
+
<code style={codeStyle}>injectStyle(css: string): string</code> — 注入一段 CSS 规则到页面中,返回一个 <code style={codeStyle}>styleId</code> 用于后续移除。CSS 选择器应使用 <code style={codeStyle}>#xinyu-chat-input</code> 来定位输入框元素。
|
|
2403
|
+
</p>
|
|
2404
|
+
<p className="text-xs leading-relaxed mt-1" style={{ color: s.textSecondary }}>
|
|
2405
|
+
<code style={codeStyle}>removeStyle(styleId: string): void</code> — 根据 <code style={codeStyle}>styleId</code> 移除之前注入的 CSS 样式。
|
|
2406
|
+
</p>
|
|
2407
|
+
<pre className="mt-2 p-3 rounded-lg text-xs overflow-x-auto" style={preStyle}>
|
|
2408
|
+
{`// 设置内联样式
|
|
2409
|
+
xinyu.chat.input.setStyle({
|
|
2410
|
+
fontSize: '16px',
|
|
2411
|
+
fontWeight: 'bold',
|
|
2412
|
+
color: '#ff6600'
|
|
2413
|
+
});
|
|
2414
|
+
|
|
2415
|
+
// 注入 CSS 样式(返回 styleId)
|
|
2416
|
+
var styleId = xinyu.chat.input.injectStyle(\`
|
|
2417
|
+
#xinyu-chat-input {
|
|
2418
|
+
border: 2px solid #ff6600;
|
|
2419
|
+
background-color: rgba(255, 102, 0, 0.05);
|
|
2420
|
+
transition: border-color 0.3s;
|
|
2421
|
+
}
|
|
2422
|
+
#xinyu-chat-input:focus {
|
|
2423
|
+
border-color: #ff9900;
|
|
2424
|
+
box-shadow: 0 0 8px rgba(255, 102, 0, 0.3);
|
|
2425
|
+
}
|
|
2426
|
+
\`);
|
|
2427
|
+
|
|
2428
|
+
// 移除注入的样式
|
|
2429
|
+
xinyu.chat.input.removeStyle(styleId);`}
|
|
2430
|
+
</pre>
|
|
2431
|
+
<p className="mt-2 text-xs" style={{ color: s.textMuted }}>
|
|
2432
|
+
💡 <strong>提示</strong>:样式注入的选择器是 <code style={codeStyle}>#xinyu-chat-input</code>,请确保使用该选择器来定位输入框元素。
|
|
2433
|
+
</p>
|
|
2434
|
+
|
|
2435
|
+
<h3 id="sub-input-event" className="text-sm font-bold mt-6 mb-2 scroll-mt-4" style={{ color: s.textPrimary }}>事件监听</h3>
|
|
2436
|
+
|
|
2437
|
+
<p className="text-xs leading-relaxed" style={{ color: s.textSecondary }}>
|
|
2438
|
+
<code style={codeStyle}>onInput(handler: (value: string) => void): () => void</code> — 监听输入框内容变化,每次内容改变时触发回调,回调参数为当前输入框的完整文本。
|
|
2439
|
+
</p>
|
|
2440
|
+
<p className="text-xs leading-relaxed mt-1" style={{ color: s.textSecondary }}>
|
|
2441
|
+
<code style={codeStyle}>onSend(handler: (content: string) => void): () => void</code> — 监听发送事件,用户点击发送或按回车时触发,回调参数为发送的内容。
|
|
2442
|
+
</p>
|
|
2443
|
+
<p className="text-xs leading-relaxed mt-1" style={{ color: s.textSecondary }}>
|
|
2444
|
+
<code style={codeStyle}>onKeyDown(handler: (event) => void): () => void</code> — 监听按键事件,回调参数为原始键盘事件对象。
|
|
2445
|
+
</p>
|
|
2446
|
+
<p className="mt-2 text-xs" style={{ color: s.textMuted }}>
|
|
2447
|
+
以上三个方法均返回一个取消监听的函数,调用该函数即可移除对应的事件监听器。多个插件可以同时注册监听器,互不干扰。
|
|
2448
|
+
</p>
|
|
2449
|
+
<pre className="mt-2 p-3 rounded-lg text-xs overflow-x-auto" style={preStyle}>
|
|
2450
|
+
{`// 监听输入变化
|
|
2451
|
+
var offInput = xinyu.chat.input.onInput(function(value) {
|
|
2452
|
+
console.log('输入内容变化:', value);
|
|
2453
|
+
// 可以根据输入内容动态更新 UI
|
|
2454
|
+
});
|
|
2455
|
+
|
|
2456
|
+
// 监听发送事件
|
|
2457
|
+
var offSend = xinyu.chat.input.onSend(function(content) {
|
|
2458
|
+
xinyu.ui.toast('用户发送了: ' + content, 'info');
|
|
2459
|
+
});
|
|
2460
|
+
|
|
2461
|
+
// 监听按键事件(例如拦截特定快捷键)
|
|
2462
|
+
var offKey = xinyu.chat.input.onKeyDown(function(event) {
|
|
2463
|
+
if (event.ctrlKey && event.key === 'Enter') {
|
|
2464
|
+
event.preventDefault();
|
|
2465
|
+
xinyu.ui.toast('Ctrl+Enter 被拦截', 'info');
|
|
2466
|
+
}
|
|
2467
|
+
});
|
|
2468
|
+
|
|
2469
|
+
// 需要时取消监听
|
|
2470
|
+
offInput();
|
|
2471
|
+
offSend();
|
|
2472
|
+
offKey();`}
|
|
2473
|
+
</pre>
|
|
2474
|
+
|
|
2475
|
+
<h3 id="sub-input-cursor" className="text-sm font-bold mt-6 mb-2 scroll-mt-4" style={{ color: s.textPrimary }}>光标与选区</h3>
|
|
2476
|
+
|
|
2477
|
+
<p className="text-xs leading-relaxed" style={{ color: s.textSecondary }}>
|
|
2478
|
+
<code style={codeStyle}>getSelection(): { start: number, end: number, text: string } | null</code> — 获取当前选区信息。返回选区的起始位置、结束位置和选中的文本。如果没有选区(光标只是一个插入点),<code style={codeStyle}>start</code> 和 <code style={codeStyle}>end</code> 相等,<code style={codeStyle}>text</code> 为空字符串。textarea 不存在时返回 <code style={codeStyle}>null</code>。
|
|
2479
|
+
</p>
|
|
2480
|
+
<p className="text-xs leading-relaxed mt-1" style={{ color: s.textSecondary }}>
|
|
2481
|
+
<code style={codeStyle}>setSelection(start: number, end: number): void</code> — 设置选区范围。<code style={codeStyle}>start</code> 和 <code style={codeStyle}>end</code> 为字符索引(从 0 开始)。如果两者相等,则仅移动光标位置。
|
|
2482
|
+
</p>
|
|
2483
|
+
<pre className="mt-2 p-3 rounded-lg text-xs overflow-x-auto" style={preStyle}>
|
|
2484
|
+
{`// 获取当前选区
|
|
2485
|
+
var sel = xinyu.chat.input.getSelection();
|
|
2486
|
+
if (sel && sel.text) {
|
|
2487
|
+
xinyu.ui.toast('选中了: ' + sel.text, 'info');
|
|
2488
|
+
}
|
|
2489
|
+
|
|
2490
|
+
// 选中全部内容
|
|
2491
|
+
var content = xinyu.chat.input.getContent();
|
|
2492
|
+
if (content) {
|
|
2493
|
+
xinyu.chat.input.setSelection(0, content.length);
|
|
2494
|
+
}
|
|
2495
|
+
|
|
2496
|
+
// 将光标移动到末尾
|
|
2497
|
+
xinyu.chat.input.setSelection(content.length, content.length);
|
|
2498
|
+
|
|
2499
|
+
// 选中前 5 个字符
|
|
2500
|
+
xinyu.chat.input.setSelection(0, 5);`}
|
|
2501
|
+
</pre>
|
|
2502
|
+
</div>
|
|
2503
|
+
);
|
|
2504
|
+
}
|
|
2505
|
+
|
|
2506
|
+
// ==================== Utils API 详解 ====================
|
|
2507
|
+
|
|
2508
|
+
function UtilsApiSection() {
|
|
2509
|
+
return (
|
|
2510
|
+
<div>
|
|
2511
|
+
<h3 id="sub-utils-random" className="text-sm font-bold mt-6 mb-2 scroll-mt-4" style={{ color: s.textPrimary }}>randomInt / rollDice</h3>
|
|
2512
|
+
<p className="text-xs leading-relaxed" style={{ color: s.textSecondary }}>
|
|
2513
|
+
<code style={codeStyle}>xinyu.utils.randomInt(min, max)</code> — 生成 [min, max] 范围内的随机整数(含两端)。
|
|
2514
|
+
</p>
|
|
2515
|
+
<p className="text-xs leading-relaxed mt-1" style={{ color: s.textSecondary }}>
|
|
2516
|
+
<code style={codeStyle}>xinyu.utils.rollDice(notation)</code> — 解析骰子表达式并返回结果对象。支持标准骰子记号如 <code style={codeStyle}>1d20</code>、<code style={codeStyle}>2d6+3</code>、<code style={codeStyle}>4d8</code> 等。
|
|
2517
|
+
</p>
|
|
2518
|
+
<pre className="mt-2 p-3 rounded-lg text-xs overflow-x-auto" style={preStyle}>
|
|
2519
|
+
{`xinyu.utils.randomInt(1, 100); // → 42
|
|
2520
|
+
|
|
2521
|
+
xinyu.utils.rollDice("1d20"); // → { notation: "1d20", rolls: [15], modifier: 0, total: 15 }
|
|
2522
|
+
xinyu.utils.rollDice("2d6+3"); // → { notation: "2d6+3", rolls: [3, 3], modifier: 3, total: 9 }
|
|
2523
|
+
xinyu.utils.rollDice("4d8"); // → { notation: "4d8", rolls: [5, 7, 6, 5], modifier: 0, total: 23 }`}
|
|
2524
|
+
</pre>
|
|
2525
|
+
|
|
2526
|
+
<h3 id="sub-utils-format" className="text-sm font-bold mt-6 mb-2 scroll-mt-4" style={{ color: s.textPrimary }}>formatDate</h3>
|
|
2527
|
+
<p className="text-xs leading-relaxed" style={{ color: s.textSecondary }}>
|
|
2528
|
+
<code style={codeStyle}>xinyu.utils.formatDate(date, format?)</code> — 格式化日期。支持 Date 对象或 ISO 字符串,format 默认为 <code style={codeStyle}>"YYYY-MM-DD HH:mm:ss"</code>。
|
|
2529
|
+
</p>
|
|
2530
|
+
<pre className="mt-2 p-3 rounded-lg text-xs overflow-x-auto" style={preStyle}>
|
|
2531
|
+
{`xinyu.utils.formatDate(new Date());
|
|
2532
|
+
// → "2026-04-27 15:30:00"
|
|
2533
|
+
|
|
2534
|
+
xinyu.utils.formatDate("2026-01-15T08:00:00Z", "YYYY年MM月DD日");
|
|
2535
|
+
// → "2026年01月15日"`}
|
|
2536
|
+
</pre>
|
|
2537
|
+
|
|
2538
|
+
<h3 id="sub-utils-debounce" className="text-sm font-bold mt-6 mb-2 scroll-mt-4" style={{ color: s.textPrimary }}>debounce / throttle</h3>
|
|
2539
|
+
<p className="text-xs leading-relaxed" style={{ color: s.textSecondary }}>
|
|
2540
|
+
<code style={codeStyle}>xinyu.utils.debounce(fn, ms)</code> — 防抖:在最后一次调用后等待 ms 毫秒再执行,期间的新调用会重置计时器。
|
|
2541
|
+
</p>
|
|
2542
|
+
<p className="text-xs leading-relaxed mt-1" style={{ color: s.textSecondary }}>
|
|
2543
|
+
<code style={codeStyle}>xinyu.utils.throttle(fn, ms)</code> — 节流:在 ms 毫秒内最多执行一次。
|
|
2544
|
+
</p>
|
|
2545
|
+
<pre className="mt-2 p-3 rounded-lg text-xs overflow-x-auto" style={preStyle}>
|
|
2546
|
+
{`// 防抖:用户停止输入 300ms 后再搜索
|
|
2547
|
+
var search = xinyu.utils.debounce(function(keyword) {
|
|
2548
|
+
xinyu.chat.insertSystemMessage("搜索: " + keyword);
|
|
2549
|
+
}, 300);
|
|
2550
|
+
|
|
2551
|
+
// 节流:滚动事件每 200ms 最多处理一次
|
|
2552
|
+
var onScroll = xinyu.utils.throttle(function() {
|
|
2553
|
+
var pos = window.scrollY;
|
|
2554
|
+
// 处理滚动...
|
|
2555
|
+
}, 200);`}
|
|
2556
|
+
</pre>
|
|
2557
|
+
|
|
2558
|
+
<h3 id="sub-utils-eventbus" className="text-sm font-bold mt-6 mb-2 scroll-mt-4" style={{ color: s.textPrimary }}>eventBus</h3>
|
|
2559
|
+
<p className="text-xs leading-relaxed" style={{ color: s.textSecondary }}>
|
|
2560
|
+
<code style={codeStyle}>xinyu.utils.eventBus</code> — 插件间通信的事件总线,用于不同插件之间传递消息。
|
|
2561
|
+
</p>
|
|
2562
|
+
<table className="w-full mt-2 text-xs">
|
|
2563
|
+
<thead><tr style={{ borderBottom: `1px solid ${s.border}` }}>
|
|
2564
|
+
<th className="py-1.5 text-left font-medium" style={{ color: s.textMuted }}>方法</th>
|
|
2565
|
+
<th className="py-1.5 text-left font-medium" style={{ color: s.textMuted }}>说明</th>
|
|
2566
|
+
</tr></thead>
|
|
2567
|
+
<tbody>
|
|
2568
|
+
<tr style={{ borderBottom: `1px solid ${s.border}` }}>
|
|
2569
|
+
<td className="py-1.5"><code style={codeStyle}>on(event, handler)</code></td>
|
|
2570
|
+
<td className="py-1.5" style={{ color: s.textSecondary }}>监听事件</td>
|
|
2571
|
+
</tr>
|
|
2572
|
+
<tr style={{ borderBottom: `1px solid ${s.border}` }}>
|
|
2573
|
+
<td className="py-1.5"><code style={codeStyle}>off(event, handler)</code></td>
|
|
2574
|
+
<td className="py-1.5" style={{ color: s.textSecondary }}>取消监听</td>
|
|
2575
|
+
</tr>
|
|
2576
|
+
<tr>
|
|
2577
|
+
<td className="py-1.5"><code style={codeStyle}>emit(event, ...args)</code></td>
|
|
2578
|
+
<td className="py-1.5" style={{ color: s.textSecondary }}>触发事件</td>
|
|
2579
|
+
</tr>
|
|
2580
|
+
</tbody>
|
|
2581
|
+
</table>
|
|
2582
|
+
<pre className="mt-2 p-3 rounded-lg text-xs overflow-x-auto" style={preStyle}>
|
|
2583
|
+
{`// 插件 A:监听事件
|
|
2584
|
+
xinyu.utils.eventBus.on("dice:rolled", function(result) {
|
|
2585
|
+
xinyu.chat.insertSystemMessage("记录骰子: " + result);
|
|
2586
|
+
});
|
|
2587
|
+
|
|
2588
|
+
// 插件 B:触发事件
|
|
2589
|
+
xinyu.utils.eventBus.emit("dice:rolled", "1d20 = 15");`}
|
|
2590
|
+
</pre>
|
|
2591
|
+
<p className="mt-2 text-xs" style={{ color: s.textMuted }}>
|
|
2592
|
+
💡 <strong>建议</strong>:使用 <code style={codeStyle}>插件名:事件名</code> 的命名格式避免冲突,如 <code style={codeStyle}>"dice:rolled"</code>、<code style={codeStyle}>"timer:tick"</code>。
|
|
2593
|
+
</p>
|
|
2594
|
+
</div>
|
|
2595
|
+
);
|
|
2596
|
+
}
|
|
2597
|
+
|
|
2598
|
+
function UiApiSection() {
|
|
2599
|
+
return (
|
|
2600
|
+
<div>
|
|
2601
|
+
<p><code style={codeStyle}>xinyu.ui</code> 是插件系统的全局 UI API,提供创建自定义界面的能力。</p>
|
|
2602
|
+
|
|
2603
|
+
<h3 id="sub-slot-inject" className="text-sm font-bold mt-6 mb-2 scroll-mt-4" style={{ color: s.textPrimary }}>插槽注入</h3>
|
|
2604
|
+
<p>在游戏页面的 <NavLink sectionId="slots">预定义插槽位置</NavLink>注入 HTML 内容:</p>
|
|
2605
|
+
<pre style={preStyle}>{CODE_UI_SLOT}</pre>
|
|
2606
|
+
|
|
2607
|
+
<h3 id="sub-toolbar-button" className="text-sm font-bold mt-6 mb-2 scroll-mt-4" style={{ color: s.textPrimary }}>工具栏按钮</h3>
|
|
2608
|
+
<p className="text-xs" style={{ color: s.textSecondary }}>
|
|
2609
|
+
<code style={codeStyle}>xinyu.ui.registerInputToolbarButton(button)</code> — 在输入框工具栏中注册按钮。需要 <code style={codeStyle}>slot:input-toolbar</code> 权限。按钮渲染在 <code style={codeStyle}>input-toolbar</code> 插槽内,与 <code style={codeStyle}>registerSlot('input-toolbar', ...)</code> 的内容并列显示。
|
|
2610
|
+
</p>
|
|
2611
|
+
<pre className="mt-1 p-3 rounded-lg text-xs overflow-x-auto" style={preStyle}>
|
|
2612
|
+
{`// 注册单个按钮
|
|
2613
|
+
xinyu.ui.registerInputToolbarButton({
|
|
2614
|
+
id: 'my-button', // 唯一 ID(同 ID 会更新而非重复)
|
|
2615
|
+
label: '我的按钮', // 按钮文本
|
|
2616
|
+
icon: '🎯', // 可选图标(emoji)
|
|
2617
|
+
order: 10, // 可选排序(越小越靠前,默认 100)
|
|
2618
|
+
onClick: function() {
|
|
2619
|
+
xinyu.ui.toast('点击了!', 'info');
|
|
2620
|
+
}
|
|
2621
|
+
});
|
|
2622
|
+
|
|
2623
|
+
// 同一插件可注册多个按钮,不会互相覆盖
|
|
2624
|
+
xinyu.ui.registerInputToolbarButton({
|
|
2625
|
+
id: 'another-button',
|
|
2626
|
+
label: '另一个',
|
|
2627
|
+
icon: '📊',
|
|
2628
|
+
order: 20,
|
|
2629
|
+
onClick: function() { /* ... */ }
|
|
2630
|
+
});`}
|
|
2631
|
+
</pre>
|
|
2632
|
+
<div className="mt-1 p-2 rounded-lg text-xs" style={{ background: s.bgTertiary, border: `1px solid ${s.border}` }}>
|
|
2633
|
+
<p style={{ color: s.accent }}>💡 提示:按钮内部使用 <code style={codeStyle}>data-action</code> + <code style={codeStyle}>onAction</code> 事件委托机制,点击事件会自动路由到对应插件。插件卸载时按钮自动清理。</p>
|
|
2634
|
+
</div>
|
|
2635
|
+
|
|
2636
|
+
<h3 id="sub-modal-confirm" className="text-sm font-bold mt-6 mb-2 scroll-mt-4" style={{ color: s.textPrimary }}>模态框与确认对话框</h3>
|
|
2637
|
+
<pre style={preStyle}>{CODE_UI_MODAL}</pre>
|
|
2638
|
+
|
|
2639
|
+
<h3 id="sub-style-inject" className="text-sm font-bold mt-6 mb-2 scroll-mt-4" style={{ color: s.textPrimary }}>样式注入</h3>
|
|
2640
|
+
<p className="text-xs mb-2" style={{ color: s.textSecondary }}>
|
|
2641
|
+
<code style={codeStyle}>xinyu.ui.injectStyle(css)</code> — 向页面注入全局 CSS 样式,返回 <code style={codeStyle}>styleId</code>(字符串)。需要 <code style={codeStyle}>style:inject</code> 权限。
|
|
2642
|
+
</p>
|
|
2643
|
+
<p className="text-xs mb-2" style={{ color: s.textSecondary }}>
|
|
2644
|
+
<code style={codeStyle}>xinyu.ui.removeStyle(styleId)</code> — 移除之前注入的样式。插件卸载时所有注入的样式会自动清理,通常不需要手动调用。
|
|
2645
|
+
</p>
|
|
2646
|
+
<p>注入的 CSS 建议使用 <NavLink sectionId="css-variables">CSS 变量</NavLink>保持与主题一致:</p>
|
|
2647
|
+
<pre style={preStyle}>{CODE_UI_STYLE}</pre>
|
|
2648
|
+
|
|
2649
|
+
<h3 id="sub-host-event" className="text-sm font-bold mt-6 mb-2 scroll-mt-4" style={{ color: s.textPrimary }}>宿主事件监听</h3>
|
|
2650
|
+
<p className="text-xs mb-2" style={{ color: s.textSecondary }}>
|
|
2651
|
+
<code style={codeStyle}>xinyu.ui.onHostEvent(eventName, handler)</code> — 监听宿主事件。需要 <code style={codeStyle}>event:host</code> 权限。返回值可用于 <code style={codeStyle}>offHostEvent</code> 取消监听。
|
|
2652
|
+
</p>
|
|
2653
|
+
<p className="text-xs mb-2" style={{ color: s.textSecondary }}>
|
|
2654
|
+
<code style={codeStyle}>xinyu.ui.offHostEvent(eventName, handler)</code> — 取消监听宿主事件。传入之前注册时使用的相同 <code style={codeStyle}>eventName</code> 和 <code style={codeStyle}>handler</code> 引用。
|
|
2655
|
+
</p>
|
|
2656
|
+
<pre style={preStyle}>{CODE_UI_HOST_EVENT}</pre>
|
|
2657
|
+
|
|
2658
|
+
<h3 id="sub-message-card" className="text-sm font-bold mt-6 mb-2 scroll-mt-4" style={{ color: s.textPrimary }}>消息卡片注册</h3>
|
|
2659
|
+
<p className="text-xs mb-2" style={{ color: s.textSecondary }}>
|
|
2660
|
+
<code style={codeStyle}>xinyu.ui.registerMessageCard(matcher, render)</code> — 注册自定义消息卡片渲染器。当消息匹配 <code style={codeStyle}>matcher</code> 函数时,使用 <code style={codeStyle}>render</code> 函数返回的 HTML 替代默认消息气泡渲染。
|
|
2661
|
+
</p>
|
|
2662
|
+
<pre style={preStyle}>{`// 注册消息卡片:当消息包含 [card] 标签时使用自定义渲染
|
|
2663
|
+
xinyu.ui.registerMessageCard(
|
|
2664
|
+
function(msg) {
|
|
2665
|
+
return msg.content.indexOf('[card]') !== -1;
|
|
2666
|
+
},
|
|
2667
|
+
function(msg) {
|
|
2668
|
+
var content = msg.content.replace(/\\[card\\]/g, '');
|
|
2669
|
+
return '<div style="padding:12px;border-radius:8px;background:var(--color-bg-tertiary);border:1px solid var(--color-border)">' +
|
|
2670
|
+
'<div style="font-weight:bold;color:var(--color-accent)">📋 自定义卡片</div>' +
|
|
2671
|
+
'<div style="margin-top:8px">' + content + '</div>' +
|
|
2672
|
+
'</div>';
|
|
2673
|
+
}
|
|
2674
|
+
);`}</pre>
|
|
2675
|
+
</div>
|
|
2676
|
+
);
|
|
2677
|
+
}
|
|
2678
|
+
|
|
2679
|
+
function DomSandboxSection() {
|
|
2680
|
+
return (
|
|
2681
|
+
<div>
|
|
2682
|
+
<p>DOM 沙箱提供受限的 DOM 操作能力,允许插件在预定义的插槽容器内创建和管理元素。需要声明 <code style={codeStyle}>dom:free</code> 或 <code style={codeStyle}>dom:query</code> 权限。</p>
|
|
2683
|
+
|
|
2684
|
+
<h3 id="sub-dom-security" className="text-sm font-bold mt-6 mb-2 scroll-mt-4" style={{ color: s.textPrimary }}>安全边界</h3>
|
|
2685
|
+
<ul className="list-disc pl-5 space-y-1 text-xs" style={{ color: s.textSecondary }}>
|
|
2686
|
+
<li><strong>只能操作插件自己创建的元素</strong> — <code style={codeStyle}>query</code> 和 <code style={codeStyle}>on</code> 等方法会检查 <code style={codeStyle}>data-plugin-id</code> 属性,确保插件无法访问其他插件的 DOM</li>
|
|
2687
|
+
<li><strong>只能在预定义的插槽容器内追加元素</strong> — <code style={codeStyle}>append</code> 的第一个参数必须是有效的插槽 ID</li>
|
|
2688
|
+
<li><strong>禁止创建 <code style={codeStyle}><script></code> 标签</strong> — HTML 会经过 sanitize 过滤</li>
|
|
2689
|
+
<li><strong>禁止绑定键盘事件</strong> — <code style={codeStyle}>keydown</code>、<code style={codeStyle}>keyup</code>、<code style={codeStyle}>keypress</code> 被拦截</li>
|
|
2690
|
+
<li><strong>禁止访问浏览器存储</strong> — 无法访问 <code style={codeStyle}>document.cookie</code>、<code style={codeStyle}>localStorage</code> 等</li>
|
|
2691
|
+
<li><strong>插件卸载时自动清理</strong> — 所有创建的元素和事件监听器会被自动移除</li>
|
|
2692
|
+
</ul>
|
|
2693
|
+
|
|
2694
|
+
<h3 id="sub-dom-slots" className="text-sm font-bold mt-6 mb-2 scroll-mt-4" style={{ color: s.textPrimary }}>可用插槽 ID</h3>
|
|
2695
|
+
<p className="text-xs" style={{ color: s.textSecondary }}>
|
|
2696
|
+
<code style={codeStyle}>append</code> 和 <code style={codeStyle}>query</code> 的第一个参数(<code style={codeStyle}>containerId</code>)必须是以下预定义的插槽 ID(具体的插槽位置见<NavLink sectionId="slots">预定义插槽位置</NavLink>):
|
|
2697
|
+
</p>
|
|
2698
|
+
<div className="mt-2 grid grid-cols-2 gap-1 text-xs">
|
|
2699
|
+
{[
|
|
2700
|
+
['header-left / center / right', '顶部导航栏'],
|
|
2701
|
+
['sidebar-left / right', '左右侧边栏'],
|
|
2702
|
+
['message-top / bottom', '消息区域上下方'],
|
|
2703
|
+
['input-toolbar / input-above', '输入框工具栏 / 正上方'],
|
|
2704
|
+
['status-bar', '底部状态栏'],
|
|
2705
|
+
['floating', '浮动层(fixed 定位,覆盖全屏)'],
|
|
2706
|
+
['overlay', '全屏遮罩层'],
|
|
2707
|
+
['modal', '模态框内容区(需先通过 showModal 打开)'],
|
|
2708
|
+
].map(([id, desc]) => (
|
|
2709
|
+
<div key={id} className="flex gap-2">
|
|
2710
|
+
<code className="shrink-0" style={{ color: s.accent }}>{id}</code>
|
|
2711
|
+
<span style={{ color: s.textMuted }}>{desc}</span>
|
|
2712
|
+
</div>
|
|
2713
|
+
))}
|
|
2714
|
+
</div>
|
|
2715
|
+
|
|
2716
|
+
<h3 id="sub-dom-api" className="text-sm font-bold mt-6 mb-2 scroll-mt-4" style={{ color: s.textPrimary }}>API 详解</h3>
|
|
2717
|
+
|
|
2718
|
+
<h4 className="text-xs font-bold mt-4 mb-1" style={{ color: s.textPrimary }}>create(tag, attrs?, children?)</h4>
|
|
2719
|
+
<p className="text-xs" style={{ color: s.textSecondary }}>
|
|
2720
|
+
创建一个 DOM 元素代理。不会立即渲染到页面上,需要调用 <code style={codeStyle}>append</code> 插入到容器中。返回值是一个包含 <code style={codeStyle}>__proxyId</code> 的代理对象,后续操作(update、remove、on)都通过此 ID 引用。
|
|
2721
|
+
</p>
|
|
2722
|
+
<pre className="mt-1 p-3 rounded-lg text-xs overflow-x-auto" style={preStyle}>
|
|
2723
|
+
{`// 创建元素(不立即渲染)
|
|
2724
|
+
var el = xinyu.ui.dom.create('div', {
|
|
2725
|
+
id: 'my-panel',
|
|
2726
|
+
className: 'my-panel',
|
|
2727
|
+
style: 'position:absolute;top:10px;right:10px;padding:8px;'
|
|
2728
|
+
}, 'Hello World');
|
|
2729
|
+
|
|
2730
|
+
// 也可以不传 children
|
|
2731
|
+
var btn = xinyu.ui.dom.create('button', {
|
|
2732
|
+
className: 'my-btn',
|
|
2733
|
+
style: 'background:#667eea;color:#fff;border:none;padding:6px 12px;border-radius:6px;cursor:pointer;'
|
|
2734
|
+
}, '点击我');`}
|
|
2735
|
+
</pre>
|
|
2736
|
+
|
|
2737
|
+
<h4 className="text-xs font-bold mt-4 mb-1" style={{ color: s.textPrimary }}>append(containerId, element)</h4>
|
|
2738
|
+
<p className="text-xs" style={{ color: s.textSecondary }}>
|
|
2739
|
+
将元素插入到指定插槽容器中。支持两种模式:传入 <code style={codeStyle}>create</code> 返回的代理对象,或直接传入 HTML 字符串。返回插入的元素 ID,可用于后续操作。
|
|
2740
|
+
</p>
|
|
2741
|
+
<pre className="mt-1 p-3 rounded-lg text-xs overflow-x-auto" style={preStyle}>
|
|
2742
|
+
{`// 方式 1:追加代理元素
|
|
2743
|
+
var el = xinyu.ui.dom.create('div', { className: 'my-panel' }, 'Hello');
|
|
2744
|
+
xinyu.ui.dom.append('floating', el);
|
|
2745
|
+
console.log(el.__proxyId); // 元素 ID,如 "plugin_123_proxy_0"
|
|
2746
|
+
|
|
2747
|
+
// 方式 2:直接追加 HTML 字符串
|
|
2748
|
+
var id = xinyu.ui.dom.append('floating', '<div class="my-panel">Hello</div>');
|
|
2749
|
+
console.log(id); // 自动生成的 ID,如 "my-plugin_html_1714356789000"`}
|
|
2750
|
+
</pre>
|
|
2751
|
+
|
|
2752
|
+
<h4 className="text-xs font-bold mt-4 mb-1" style={{ color: s.textPrimary }}>update(elementId, props)</h4>
|
|
2753
|
+
<p className="text-xs" style={{ color: s.textSecondary }}>
|
|
2754
|
+
更新元素属性。支持的属性:{`{ style, className, textContent, innerHTML, 以及其他标准 HTML 属性 }`}。
|
|
2755
|
+
</p>
|
|
2756
|
+
<pre className="mt-1 p-3 rounded-lg text-xs overflow-x-auto" style={preStyle}>
|
|
2757
|
+
{`var el = xinyu.ui.dom.create('div', { id: 'counter' }, '0');
|
|
2758
|
+
xinyu.ui.dom.append('status-bar', el);
|
|
2759
|
+
|
|
2760
|
+
// 更新文本内容
|
|
2761
|
+
xinyu.ui.dom.update(el.__proxyId, { textContent: '42' });
|
|
2762
|
+
|
|
2763
|
+
// 更新样式
|
|
2764
|
+
xinyu.ui.dom.update(el.__proxyId, {
|
|
2765
|
+
style: 'color:red;font-weight:bold;'
|
|
2766
|
+
});
|
|
2767
|
+
|
|
2768
|
+
// 更新 HTML 内容(会经过 sanitize 过滤)
|
|
2769
|
+
xinyu.ui.dom.update(el.__proxyId, {
|
|
2770
|
+
innerHTML: '<strong style="color:green">已更新</strong>'
|
|
2771
|
+
});`}
|
|
2772
|
+
</pre>
|
|
2773
|
+
|
|
2774
|
+
<h4 className="text-xs font-bold mt-4 mb-1" style={{ color: s.textPrimary }}>query(containerId, selector)</h4>
|
|
2775
|
+
<p className="text-xs" style={{ color: s.textSecondary }}>
|
|
2776
|
+
在指定插槽容器内查询元素。<strong>仅返回插件自己创建的元素</strong>(通过 <code style={codeStyle}>data-plugin-id</code> 属性过滤)。返回数组,每项包含 <code style={codeStyle}>{'{ id, tag, textContent }'}</code>。
|
|
2777
|
+
</p>
|
|
2778
|
+
<div className="mt-1 p-2 rounded-lg text-xs" style={{ background: s.bgTertiary, border: `1px solid ${s.border}` }}>
|
|
2779
|
+
<p style={{ color: s.accent }}>⚠️ 注意:query 只能查到插件自己拥有(通过 <code style={codeStyle}>data-plugin-id</code> 标记)的元素。支持查询插槽容器和模态框内的元素。</p>
|
|
2780
|
+
</div>
|
|
2781
|
+
<pre className="mt-1 p-3 rounded-lg text-xs overflow-x-auto" style={preStyle}>
|
|
2782
|
+
{`// 先插入元素
|
|
2783
|
+
xinyu.ui.dom.append('floating', '<div class="item" id="item-1">第一项</div>');
|
|
2784
|
+
xinyu.ui.dom.append('floating', '<div class="item" id="item-2">第二项</div>');
|
|
2785
|
+
|
|
2786
|
+
// 查询所有 .item 元素
|
|
2787
|
+
var items = xinyu.ui.dom.query('floating', '.item');
|
|
2788
|
+
console.log(items);
|
|
2789
|
+
// [{ id: 'item-1', tag: 'div', textContent: '第一项' },
|
|
2790
|
+
// { id: 'item-2', tag: 'div', textContent: '第二项' }]
|
|
2791
|
+
|
|
2792
|
+
// 通过 ID 查询
|
|
2793
|
+
var item = xinyu.ui.dom.query('floating', '#item-1');
|
|
2794
|
+
|
|
2795
|
+
// 查询模态框内的元素(containerId 传 'modal')
|
|
2796
|
+
var modalItems = xinyu.ui.dom.query('modal', '#slot-list-container > div');
|
|
2797
|
+
var checkbox = xinyu.ui.dom.query('modal', '[data-payload="status-bar"]');`}
|
|
2798
|
+
</pre>
|
|
2799
|
+
|
|
2800
|
+
<h4 className="text-xs font-bold mt-4 mb-1" style={{ color: s.textPrimary }}>on(elementId, event, handler) / off(elementId, event)</h4>
|
|
2801
|
+
<p className="text-xs" style={{ color: s.textSecondary }}>
|
|
2802
|
+
绑定/解绑事件。<strong>禁止绑定键盘事件</strong>(keydown/keyup/keypress)。handler 中的 <code style={codeStyle}>this</code> 不指向 DOM 元素,如需访问事件对象,请使用 <code style={codeStyle}>registerSlot</code> + <code style={codeStyle}>data-action</code> + <code style={codeStyle}>onAction</code> 模式。
|
|
2803
|
+
</p>
|
|
2804
|
+
<pre className="mt-1 p-3 rounded-lg text-xs overflow-x-auto" style={preStyle}>
|
|
2805
|
+
{`var el = xinyu.ui.dom.create('button', { className: 'my-btn' }, '点击');
|
|
2806
|
+
xinyu.ui.dom.append('floating', el);
|
|
2807
|
+
|
|
2808
|
+
// 绑定点击事件
|
|
2809
|
+
xinyu.ui.dom.on(el.__proxyId, 'click', function() {
|
|
2810
|
+
xinyu.ui.toast('被点击了!', 'info');
|
|
2811
|
+
});
|
|
2812
|
+
|
|
2813
|
+
// 解绑事件
|
|
2814
|
+
xinyu.ui.dom.off(el.__proxyId, 'click');`}
|
|
2815
|
+
</pre>
|
|
2816
|
+
|
|
2817
|
+
<h4 className="text-xs font-bold mt-4 mb-1" style={{ color: s.textPrimary }}>remove(elementId)</h4>
|
|
2818
|
+
<p className="text-xs" style={{ color: s.textSecondary }}>
|
|
2819
|
+
从 DOM 中移除插件创建的元素。只能移除自己创建的元素。
|
|
2820
|
+
</p>
|
|
2821
|
+
<pre className="mt-1 p-3 rounded-lg text-xs overflow-x-auto" style={preStyle}>
|
|
2822
|
+
{`var el = xinyu.ui.dom.create('div', { id: 'temp' }, '临时内容');
|
|
2823
|
+
var id = xinyu.ui.dom.append('floating', el);
|
|
2824
|
+
|
|
2825
|
+
// 3 秒后移除
|
|
2826
|
+
setTimeout(function() {
|
|
2827
|
+
xinyu.ui.dom.remove(id);
|
|
2828
|
+
}, 3000);`}
|
|
2829
|
+
</pre>
|
|
2830
|
+
|
|
2831
|
+
<h4 className="text-xs font-bold mt-4 mb-1" style={{ color: s.textPrimary }}>getContainer(containerId)</h4>
|
|
2832
|
+
<p className="text-xs" style={{ color: s.textSecondary }}>
|
|
2833
|
+
获取容器信息(不返回 DOM 引用)。返回 <code style={codeStyle}>{'{ id, exists, childCount }'}</code> 或 <code style={codeStyle}>null</code>。
|
|
2834
|
+
</p>
|
|
2835
|
+
<pre className="mt-1 p-3 rounded-lg text-xs overflow-x-auto" style={preStyle}>
|
|
2836
|
+
{`var info = xinyu.ui.dom.getContainer('floating');
|
|
2837
|
+
if (info && info.exists) {
|
|
2838
|
+
console.log('容器内子元素数量:', info.childCount);
|
|
2839
|
+
}`}
|
|
2840
|
+
</pre>
|
|
2841
|
+
|
|
2842
|
+
<h3 id="sub-dom-permission" className="text-sm font-bold mt-6 mb-2 scroll-mt-4" style={{ color: s.textPrimary }}>权限说明</h3>
|
|
2843
|
+
<table className="w-full text-xs mt-2" style={{ borderCollapse: 'collapse' }}>
|
|
2844
|
+
<thead>
|
|
2845
|
+
<tr style={{ borderBottom: `1px solid ${s.border}` }}>
|
|
2846
|
+
<th className="text-left py-2 pr-4" style={{ color: s.textPrimary }}>权限</th>
|
|
2847
|
+
<th className="text-left py-2" style={{ color: s.textPrimary }}>允许的操作</th>
|
|
2848
|
+
</tr>
|
|
2849
|
+
</thead>
|
|
2850
|
+
<tbody>
|
|
2851
|
+
<tr style={{ borderBottom: `1px solid ${s.border}` }}>
|
|
2852
|
+
<td className="py-2 pr-4"><code style={codeStyle}>dom:free</code></td>
|
|
2853
|
+
<td className="py-2"><code style={codeStyle}>create、append、update、remove、query、on、off、getContainer</code> — 全部 DOM 操作</td>
|
|
2854
|
+
</tr>
|
|
2855
|
+
<tr style={{ borderBottom: `1px solid ${s.border}` }}>
|
|
2856
|
+
<td className="py-2 pr-4"><code style={codeStyle}>dom:query</code></td>
|
|
2857
|
+
<td className="py-2">仅 <code style={codeStyle}>query</code> 和 <code style={codeStyle}>getContainer</code> — 只读查询,不能创建或修改 DOM</td>
|
|
2858
|
+
</tr>
|
|
2859
|
+
</tbody>
|
|
2860
|
+
</table>
|
|
2861
|
+
|
|
2862
|
+
<h3 id="sub-dom-vs-slot" className="text-sm font-bold mt-6 mb-2 scroll-mt-4" style={{ color: s.textPrimary }}>DOM 沙箱 vs registerSlot</h3>
|
|
2863
|
+
<p className="text-xs" style={{ color: s.textSecondary }}>
|
|
2864
|
+
两种方式都可以向插槽注入内容,适用场景不同:
|
|
2865
|
+
</p>
|
|
2866
|
+
<table className="w-full text-xs mt-2" style={{ borderCollapse: 'collapse' }}>
|
|
2867
|
+
<thead>
|
|
2868
|
+
<tr style={{ borderBottom: `1px solid ${s.border}` }}>
|
|
2869
|
+
<th className="text-left py-2 pr-4" style={{ color: s.textPrimary }}>特性</th>
|
|
2870
|
+
<th className="text-left py-2 pr-4" style={{ color: s.textPrimary }}>Slot</th>
|
|
2871
|
+
<th className="text-left py-2" style={{ color: s.textPrimary }}>DOM</th>
|
|
2872
|
+
</tr>
|
|
2873
|
+
</thead>
|
|
2874
|
+
<tbody>
|
|
2875
|
+
<tr style={{ borderBottom: `1px solid ${s.border}` }}>
|
|
2876
|
+
<td className="py-2 pr-4">内容更新</td>
|
|
2877
|
+
<td className="py-2 pr-4">调用 <code style={codeStyle}>updateSlot</code> 整体替换</td>
|
|
2878
|
+
<td className="py-2">调用 <code style={codeStyle}>update</code> 精确修改属性</td>
|
|
2879
|
+
</tr>
|
|
2880
|
+
<tr style={{ borderBottom: `1px solid ${s.border}` }}>
|
|
2881
|
+
<td className="py-2 pr-4">事件绑定</td>
|
|
2882
|
+
<td className="py-2 pr-4">通过 <code style={codeStyle}>data-action</code> + <code style={codeStyle}>onAction</code></td>
|
|
2883
|
+
<td className="py-2">通过 <code style={codeStyle}>dom.on</code></td>
|
|
2884
|
+
</tr>
|
|
2885
|
+
<tr style={{ borderBottom: `1px solid ${s.border}` }}>
|
|
2886
|
+
<td className="py-2 pr-4">动态增删</td>
|
|
2887
|
+
<td className="py-2 pr-4">需要重新注册/注销</td>
|
|
2888
|
+
<td className="py-2">支持随时 append/remove</td>
|
|
2889
|
+
</tr>
|
|
2890
|
+
<tr>
|
|
2891
|
+
<td className="py-2 pr-4">适用场景</td>
|
|
2892
|
+
<td className="py-2 pr-4">静态 UI、工具栏按钮、面板</td>
|
|
2893
|
+
<td className="py-2">动态列表、计数器、临时提示</td>
|
|
2894
|
+
</tr>
|
|
2895
|
+
</tbody>
|
|
2896
|
+
</table>
|
|
2897
|
+
</div>
|
|
2898
|
+
);
|
|
2899
|
+
}
|
|
2900
|
+
|
|
2901
|
+
function PermissionsSection() {
|
|
2902
|
+
return (
|
|
2903
|
+
<div>
|
|
2904
|
+
<p>UI API 采用三层权限声明机制,插件需要在 manifest 中声明需要的权限。权限分为三种类型:</p>
|
|
2905
|
+
|
|
2906
|
+
<h3 id="sub-perm-overview" className="text-sm font-bold mt-6 mb-2 scroll-mt-4" style={{ color: s.textPrimary }}>三层权限体系</h3>
|
|
2907
|
+
<div className="space-y-3 mt-2">
|
|
2908
|
+
{[
|
|
2909
|
+
{
|
|
2910
|
+
type: 'commonPermissions(共享权限)',
|
|
2911
|
+
desc: '可被多个插件同时声明,不会产生冲突。大多数权限属于此类。',
|
|
2912
|
+
detail: '例如 slot:*、style:inject、modal:*、dom:*、event:* 等'
|
|
2913
|
+
},
|
|
2914
|
+
{
|
|
2915
|
+
type: 'exclusivePermissions(排他权限)',
|
|
2916
|
+
desc: '同时只能被一个插件声明。如果多个插件声明了相同的排他权限,后加载的插件启动失败。',
|
|
2917
|
+
detail: '例如 slot:style:* 系列权限,用于独占某个插槽的样式'
|
|
2918
|
+
},
|
|
2919
|
+
{
|
|
2920
|
+
type: 'requiredPermissions(必要权限)',
|
|
2921
|
+
desc: '没有此权限则插件不允许启动。必须在 commonPermissions 或 exclusivePermissions 中声明过的权限才能出现在这里。',
|
|
2922
|
+
detail: '用于声明插件运行所必需的核心权限,缺失时插件将被禁用'
|
|
2923
|
+
},
|
|
2924
|
+
].map((item) => (
|
|
2925
|
+
<div key={item.type} className="p-3 rounded-lg" style={{ backgroundColor: s.bgSecondary, border: `1px solid ${s.border}` }}>
|
|
2926
|
+
<div className="text-xs font-bold mb-1" style={{ color: s.accent }}>{item.type}</div>
|
|
2927
|
+
<p className="text-xs" style={{ color: s.textSecondary }}>{item.desc}</p>
|
|
2928
|
+
<p className="text-xs mt-1" style={{ color: s.textMuted }}>{item.detail}</p>
|
|
2929
|
+
</div>
|
|
2930
|
+
))}
|
|
2931
|
+
</div>
|
|
2932
|
+
|
|
2933
|
+
<h3 id="sub-perm-list" className="text-sm font-bold mt-6 mb-2 scroll-mt-4" style={{ color: s.textPrimary }}>权限列表</h3>
|
|
2934
|
+
<table className="w-full text-xs mt-2" style={{ borderCollapse: 'collapse' }}>
|
|
2935
|
+
<thead>
|
|
2936
|
+
<tr style={{ borderBottom: `1px solid ${s.border}` }}>
|
|
2937
|
+
<th className="text-left py-2 pr-4" style={{ color: s.textPrimary }}>权限</th>
|
|
2938
|
+
<th className="text-center py-2 pr-4" style={{ color: s.textPrimary }}>推荐声明类型</th>
|
|
2939
|
+
<th className="text-left py-2" style={{ color: s.textPrimary }}>说明</th>
|
|
2940
|
+
</tr>
|
|
2941
|
+
</thead>
|
|
2942
|
+
<tbody>
|
|
2943
|
+
{([
|
|
2944
|
+
['slot:header-left / center / right', '共享', <NavLink key="s1" sectionId="slots">顶部导航栏三个区域</NavLink>],
|
|
2945
|
+
['slot:sidebar-left / right', '共享', <NavLink key="s2" sectionId="slots">左右侧边栏</NavLink>],
|
|
2946
|
+
['slot:message-top / bottom', '共享', <NavLink key="s3" sectionId="slots">消息区域上下方</NavLink>],
|
|
2947
|
+
['slot:input-toolbar / input-above', '共享', <NavLink key="s4" sectionId="slots">输入框工具栏 / 正上方</NavLink>],
|
|
2948
|
+
['slot:status-bar', '共享', <NavLink key="s5" sectionId="slots">底部状态栏</NavLink>],
|
|
2949
|
+
['slot:floating', '共享', <NavLink key="s6" sectionId="slots">浮动层(fixed 定位)</NavLink>],
|
|
2950
|
+
['slot:overlay', '共享', <NavLink key="s7" sectionId="slots">全屏遮罩层</NavLink>],
|
|
2951
|
+
['slot:style:*', '排他', '独占指定插槽的样式,例如 slot:style:status-bar 表示独占状态栏样式'],
|
|
2952
|
+
['dom:free', '共享', <NavLink key="d1" sectionId="api-dom">自由 DOM 操作权限</NavLink>],
|
|
2953
|
+
['dom:query', '共享', <NavLink key="d2" sectionId="api-dom">DOM 查询权限</NavLink>],
|
|
2954
|
+
['style:inject', '共享', <NavLink key="st" sectionId="css-variables">CSS 样式注入权限</NavLink>],
|
|
2955
|
+
['modal:confirm', '共享', <NavLink key="mc1" sectionId="api-ui">确认对话框权限</NavLink>],
|
|
2956
|
+
['modal:custom', '共享', <NavLink key="mc" sectionId="api-ui">自定义模态框权限</NavLink>],
|
|
2957
|
+
['event:host', '共享', <NavLink key="eh" sectionId="api-ui">宿主事件监听权限</NavLink>],
|
|
2958
|
+
['input:control', '共享', <NavLink key="ic" sectionId="api-input">输入框控制权限(控制输入框内容、样式、状态、事件监听)</NavLink>],
|
|
2959
|
+
['storage:plugin', '共享', <NavLink key="sp" sectionId="persistent-storage">持久化存储权限(数据库 key-value 读写)</NavLink>],
|
|
2960
|
+
['file:read', '共享', <NavLink key="fr" sectionId="file-api">文件读取权限(读取插件私有目录文件)</NavLink>],
|
|
2961
|
+
['file:write', '共享', <NavLink key="fw" sectionId="file-api">文件写入权限(写入/删除插件私有目录文件)</NavLink>],
|
|
2962
|
+
['game:info:read', '共享', <NavLink key="gi" sectionId="sub-game-info">游戏信息读取权限(世界设定、会话ID、消息列表、游戏属性)</NavLink>],
|
|
2963
|
+
['game:info:write', '共享', <NavLink key="giw" sectionId="sub-game-info">游戏信息写入权限(修改世界设定,实时生效并持久化)</NavLink>],
|
|
2964
|
+
] as [string, string, React.ReactNode][]).map(([perm, permType, desc]) => (
|
|
2965
|
+
<tr key={perm} style={{ borderBottom: `1px solid ${s.border}` }}>
|
|
2966
|
+
<td className="py-2 pr-4"><code style={codeStyle}>{perm}</code></td>
|
|
2967
|
+
<td className="py-2 pr-4 text-center" style={{ color: permType === '排他' ? '#f97316' : s.textMuted }}>{permType}</td>
|
|
2968
|
+
<td className="py-2">{desc}</td>
|
|
2969
|
+
</tr>
|
|
2970
|
+
))}
|
|
2971
|
+
</tbody>
|
|
2972
|
+
</table>
|
|
2973
|
+
|
|
2974
|
+
<h3 id="sub-perm-default" className="text-sm font-bold mt-6 mb-2 scroll-mt-4" style={{ color: s.textPrimary }}>默认权限</h3>
|
|
2975
|
+
<p>如果插件未声明任何权限,默认拥有以下共享权限:</p>
|
|
2976
|
+
<pre style={preStyle}>{CODE_PERMS_DEFAULT}</pre>
|
|
2977
|
+
|
|
2978
|
+
<h3 id="sub-perm-editor" className="text-sm font-bold mt-6 mb-2 scroll-mt-4" style={{ color: s.textPrimary }}>编辑器中声明权限</h3>
|
|
2979
|
+
|
|
2980
|
+
<h4 className="text-xs font-bold mt-4 mb-2" style={{ color: s.accent }}>方式 1:编辑器中勾选(推荐)</h4>
|
|
2981
|
+
<p>在插件创建/编辑页面的「权限声明」区域勾选所需权限,并选择权限类型(共享/排他/必要)。也可以在代码中使用 <code style={codeStyle}>{'// @permission xxxx'}</code> 注释声明,保存时自动解析。</p>
|
|
2982
|
+
|
|
2983
|
+
<h4 className="text-xs font-bold mt-4 mb-2" style={{ color: s.accent }}>方式 2:代码注释声明</h4>
|
|
2984
|
+
<p>在插件代码顶部使用 <code style={codeStyle}>{'// @key value'}</code> 注释声明 manifest 信息,然后在基础信息区域点击「从代码解析」自动填充:</p>
|
|
2985
|
+
<pre style={preStyle}>{[
|
|
2986
|
+
'// @plugin-id zhangsan.dice-master',
|
|
2987
|
+
'// @name 骰子大师',
|
|
2988
|
+
'// @version 1.0.0',
|
|
2989
|
+
'// @author 张三',
|
|
2990
|
+
'// @icon 🎲',
|
|
2991
|
+
'// @type game-mechanics',
|
|
2992
|
+
'// @description 一个简单的骰子投掷插件',
|
|
2993
|
+
'// @permission slot:input-toolbar',
|
|
2994
|
+
'// @permission style:inject',
|
|
2995
|
+
'// 也可以一行声明多个权限,用逗号分隔',
|
|
2996
|
+
'// @permission slot:status-bar, modal:confirm',
|
|
2997
|
+
'',
|
|
2998
|
+
'// 声明配置项(单个)',
|
|
2999
|
+
'// @config {"key":"defaultSides","label":"默认面数","type":"number","defaultValue":20,"description":"骰子的默认面数"}',
|
|
3000
|
+
'// 声明配置项(批量)',
|
|
3001
|
+
'// @configs [',
|
|
3002
|
+
'// {"key":"autoRoll","label":"自动投掷","type":"boolean","defaultValue":false},',
|
|
3003
|
+
'// {"key":"theme","label":"结果样式","type":"select","defaultValue":"default","options":[{"label":"默认","value":"default"},{"label":"简约","value":"minimal"}]}',
|
|
3004
|
+
'// ]',
|
|
3005
|
+
'',
|
|
3006
|
+
'function setup(xinyu) {',
|
|
3007
|
+
" xinyu.ui.registerSlot('status-bar', '<div>状态信息</div>');",
|
|
3008
|
+
'}',
|
|
3009
|
+
].join('\n')}</pre>
|
|
3010
|
+
<div className="p-3 rounded-lg mt-2" style={{ backgroundColor: s.bgSecondary, border: `1px solid ${s.border}` }}>
|
|
3011
|
+
<div className="text-xs font-bold mb-1" style={{ color: s.accent }}>支持的注释标签</div>
|
|
3012
|
+
<table className="w-full text-xs mt-1" style={{ borderCollapse: 'collapse' }}>
|
|
3013
|
+
<thead>
|
|
3014
|
+
<tr style={{ borderBottom: `1px solid ${s.border}` }}>
|
|
3015
|
+
<th className="text-left py-1 pr-3" style={{ color: s.textPrimary }}>标签</th>
|
|
3016
|
+
<th className="text-left py-1 pr-3" style={{ color: s.textPrimary }}>对应字段</th>
|
|
3017
|
+
<th className="text-left py-1" style={{ color: s.textPrimary }}>说明</th>
|
|
3018
|
+
</tr>
|
|
3019
|
+
</thead>
|
|
3020
|
+
<tbody>
|
|
3021
|
+
{[
|
|
3022
|
+
['@plugin-id', 'id', '插件唯一标识(建议格式 author.plugin-name),解析时自动检查是否重复'],
|
|
3023
|
+
['@name', 'name', '插件名称'],
|
|
3024
|
+
['@version', 'version', '版本号'],
|
|
3025
|
+
['@author', 'author', '作者'],
|
|
3026
|
+
['@icon', 'icon', '图标(emoji)'],
|
|
3027
|
+
['@type', 'type', '插件类型(如 game-mechanics)'],
|
|
3028
|
+
['@description', 'description', '插件描述'],
|
|
3029
|
+
['@permission', 'commonPermissions', '共享权限(支持逗号分隔多个),解析的权限默认映射到 commonPermissions'],
|
|
3030
|
+
['@exclusive', 'exclusivePermissions', '排他权限(支持逗号分隔多个)'],
|
|
3031
|
+
['@required', 'requiredPermissions', '必要权限(支持逗号分隔多个),必须在共享或排他权限中声明过'],
|
|
3032
|
+
['@config', 'configSchema', '单个配置项(JSON 对象)'],
|
|
3033
|
+
['@configs', 'configSchema', '批量配置项(JSON 数组)'],
|
|
3034
|
+
].map(([tag, field, desc]) => (
|
|
3035
|
+
<tr key={tag} style={{ borderBottom: `1px solid ${s.border}` }}>
|
|
3036
|
+
<td className="py-1 pr-3"><code style={codeStyle}>{tag}</code></td>
|
|
3037
|
+
<td className="py-1 pr-3"><code style={{ ...codeStyle, color: 'var(--color-text-muted)' }}>{field}</code></td>
|
|
3038
|
+
<td className="py-1">{desc}</td>
|
|
3039
|
+
</tr>
|
|
3040
|
+
))}
|
|
3041
|
+
</tbody>
|
|
3042
|
+
</table>
|
|
3043
|
+
</div>
|
|
3044
|
+
<p className="mt-2 text-xs" style={{ color: s.textMuted }}>💡 注释只需写在代码顶部即可,点击「从代码解析」按钮会自动识别并填充到对应字段。保存插件时也会自动执行解析,代码注释中的值优先于表单手动填写的值。</p>
|
|
3045
|
+
|
|
3046
|
+
<h3 id="sub-perm-conflict" className="text-sm font-bold mt-6 mb-2 scroll-mt-4" style={{ color: s.textPrimary }}>权限冲突</h3>
|
|
3047
|
+
<p>当多个插件声明了相互冲突的权限时,系统会进行冲突检测。冲突规则如下:</p>
|
|
3048
|
+
<ul className="list-disc pl-5 space-y-2 mt-2">
|
|
3049
|
+
<li><strong>排他权限冲突</strong>:如果一个插件声明了某个排他权限(如 <code style={codeStyle}>slot:style:status-bar</code>),而另一个已加载的插件已经声明了相同的排他权限,则产生冲突。</li>
|
|
3050
|
+
<li><strong>共享/排他交叉冲突</strong>:如果一个插件声明了某个共享权限,但该权限已被另一个插件作为排他权限声明,也会产生冲突。</li>
|
|
3051
|
+
<li><strong>必要权限缺失</strong>:如果插件在 <code style={codeStyle}>requiredPermissions</code> 中声明了某个权限,但该权限未在 <code style={codeStyle}>commonPermissions</code> 或 <code style={codeStyle}>exclusivePermissions</code> 中声明,则该必要权限被视为缺失,插件不允许启动。</li>
|
|
3052
|
+
</ul>
|
|
3053
|
+
<div className="p-3 rounded-lg mt-3 mb-3" style={{ backgroundColor: s.bgSecondary, border: `1px solid ${s.border}` }}>
|
|
3054
|
+
<div className="text-xs font-bold mb-2" style={{ color: s.accent }}>冲突处理流程</div>
|
|
3055
|
+
<ol className="list-decimal pl-5 space-y-1 text-xs" style={{ color: s.textSecondary }}>
|
|
3056
|
+
<li>插件加载时,系统检查其声明的所有权限是否与已加载的插件产生冲突</li>
|
|
3057
|
+
<li>如果检测到冲突,后加载的插件启动失败,弹出冲突弹窗</li>
|
|
3058
|
+
<li>用户可以在弹窗中选择以下操作:
|
|
3059
|
+
<ul className="list-disc pl-5 mt-1 space-y-1">
|
|
3060
|
+
<li><strong>取消冲突权限</strong>:自动从插件中移除产生冲突的权限,插件以降级模式启动</li>
|
|
3061
|
+
<li><strong>禁用插件</strong>:完全禁用该插件,不加载其代码</li>
|
|
3062
|
+
</ul>
|
|
3063
|
+
</li>
|
|
3064
|
+
</ol>
|
|
3065
|
+
</div>
|
|
3066
|
+
<pre style={preStyle}>{[
|
|
3067
|
+
'// 示例:排他权限冲突',
|
|
3068
|
+
'// 插件 A 已声明 slot:style:status-bar(排他权限)',
|
|
3069
|
+
'//',
|
|
3070
|
+
'// 插件 B 尝试声明相同的排他权限:',
|
|
3071
|
+
'// @exclusive slot:style:status-bar',
|
|
3072
|
+
'//',
|
|
3073
|
+
'// 结果:插件 B 启动时检测到冲突,弹出冲突弹窗',
|
|
3074
|
+
'// 用户可选择「取消冲突权限」或「禁用插件」',
|
|
3075
|
+
].join('\n')}</pre>
|
|
3076
|
+
</div>
|
|
3077
|
+
);
|
|
3078
|
+
}
|
|
3079
|
+
|
|
3080
|
+
function SlotsSection() {
|
|
3081
|
+
return (
|
|
3082
|
+
<div>
|
|
3083
|
+
<p>游戏页面提供了 12 个预定义的 UI 插槽位置,插件可以通过 <code style={codeStyle}>xinyu.ui.registerSlot()</code> 在这些位置注入内容。</p>
|
|
3084
|
+
|
|
3085
|
+
<h3 id="sub-slot-diagram" className="text-sm font-bold mt-6 mb-2 scroll-mt-4" style={{ color: s.textPrimary }}>插槽位置示意图</h3>
|
|
3086
|
+
<pre style={{ ...preStyle, fontSize: '11px' }}>{CODE_SLOTS_DIAGRAM}</pre>
|
|
3087
|
+
|
|
3088
|
+
<h3 id="sub-slot-priority" className="text-sm font-bold mt-6 mb-2 scroll-mt-4" style={{ color: s.textPrimary }}>优先级</h3>
|
|
3089
|
+
<p>多个插件可以在同一插槽注册内容,通过 <code style={codeStyle}>priority</code> 控制排序(数值越小越靠前,默认 100):</p>
|
|
3090
|
+
<pre style={preStyle}>{CODE_SLOTS_PRIORITY}</pre>
|
|
3091
|
+
|
|
3092
|
+
<h3 id="sub-slot-update" className="text-sm font-bold mt-6 mb-2 scroll-mt-4" style={{ color: s.textPrimary }}>更新与覆盖</h3>
|
|
3093
|
+
<p>同一插件对同一插槽多次调用 <code style={codeStyle}>registerSlot</code> 会<strong>覆盖</strong>旧内容(保持注册 ID 不变),UI 自动刷新。也可以使用语义化的 <code style={codeStyle}>updateSlot</code> 通过 registrationId 更新:</p>
|
|
3094
|
+
<pre style={preStyle}>{[
|
|
3095
|
+
"var regId = xinyu.ui.registerSlot('status-bar', '<div>初始内容</div>');",
|
|
3096
|
+
'',
|
|
3097
|
+
'// 方式 1:再次 registerSlot(同 pluginId + slotId 自动覆盖)',
|
|
3098
|
+
"xinyu.ui.registerSlot('status-bar', '<div>更新后的内容</div>');",
|
|
3099
|
+
'',
|
|
3100
|
+
'// 方式 2:使用 updateSlot(通过 registrationId 精确更新,语义更清晰)',
|
|
3101
|
+
"xinyu.ui.updateSlot(regId, '<div>更新后的内容</div>');",
|
|
3102
|
+
'',
|
|
3103
|
+
'// 如果需要完全移除,使用返回的 registrationId',
|
|
3104
|
+
'xinyu.ui.unregisterSlot(regId);',
|
|
3105
|
+
].join('\n')}</pre>
|
|
3106
|
+
|
|
3107
|
+
<h3 id="sub-slot-key" className="text-sm font-bold mt-6 mb-2 scroll-mt-4" style={{ color: s.textPrimary }}>key 参数与精确注销</h3>
|
|
3108
|
+
<p>当同一插件需要在<strong>同一插槽</strong>中注册多块独立内容时,必须使用 <code style={codeStyle}>key</code> 参数区分它们。不传 key 时,同一插件在同一插槽只能有一个注册(后注册覆盖先注册)。</p>
|
|
3109
|
+
<div style={{ background: 'rgba(255,255,255,0.03)', border: '1px solid var(--color-border, rgba(255,255,255,0.1))', borderRadius: '8px', padding: '12px', marginBottom: '12px' }}>
|
|
3110
|
+
<p style={{ margin: '0 0 8px 0', fontSize: '13px' }}><strong>⚠️ registrationId 格式规则</strong></p>
|
|
3111
|
+
<ul style={{ margin: 0, paddingLeft: '20px', fontSize: '12px', lineHeight: '1.8' }}>
|
|
3112
|
+
<li>不传 key:<code style={codeStyle}>{'{pluginId}_slot_{timestamp}'}</code>(每次注册生成新 ID)</li>
|
|
3113
|
+
<li>传 key:<code style={codeStyle}>{'{pluginId}_key_{key}'}</code>(固定 ID,用于精确更新/注销)</li>
|
|
3114
|
+
<li><strong>插件不知道自己的 pluginId</strong>,所以注销时必须使用 <code style={codeStyle}>registerSlot</code> 的返回值</li>
|
|
3115
|
+
</ul>
|
|
3116
|
+
</div>
|
|
3117
|
+
<pre style={preStyle}>{[
|
|
3118
|
+
'// 使用 key 注册(推荐用于需要独立控制的插槽内容)',
|
|
3119
|
+
"var slotId = xinyu.ui.registerSlot('input-above',",
|
|
3120
|
+
" '<div>选项按钮</div>',",
|
|
3121
|
+
" { priority: 10, key: 'my-choices' } // key 用于区分同一插槽中的多块内容",
|
|
3122
|
+
');',
|
|
3123
|
+
'',
|
|
3124
|
+
'// 更新内容(通过返回的 registrationId)',
|
|
3125
|
+
"xinyu.ui.updateSlot(slotId, '<div>更新后的选项</div>');",
|
|
3126
|
+
'',
|
|
3127
|
+
'// 注销(通过返回的 registrationId)',
|
|
3128
|
+
'xinyu.ui.unregisterSlot(slotId);',
|
|
3129
|
+
'',
|
|
3130
|
+
'// 错误写法:unregisterSlot 不接受 (slotId, key) 两个参数',
|
|
3131
|
+
"// xinyu.ui.unregisterSlot('input-above', 'my-choices'); // 不会生效!",
|
|
3132
|
+
'',
|
|
3133
|
+
'// 正确写法:保存 registerSlot 的返回值,用返回值注销',
|
|
3134
|
+
"var id = xinyu.ui.registerSlot('input-above', html, { key: 'my-choices' });",
|
|
3135
|
+
'// ... 后续需要移除时:',
|
|
3136
|
+
'xinyu.ui.unregisterSlot(id);',
|
|
3137
|
+
].join('\n')}</pre>
|
|
3138
|
+
|
|
3139
|
+
<h3 id="sub-slot-action" className="text-sm font-bold mt-6 mb-2 scroll-mt-4" style={{ color: s.textPrimary }}>插槽中的交互事件</h3>
|
|
3140
|
+
<p>由于安全原因,HTML 净化器会移除所有 <code style={codeStyle}>onclick</code> 等内联事件属性。插件应使用 <code style={codeStyle}>data-action</code> 属性 + <code style={codeStyle}>xinyu.ui.onAction</code> 来绑定交互:</p>
|
|
3141
|
+
<pre style={preStyle}>{[
|
|
3142
|
+
'// 1. 注册 action 回调',
|
|
3143
|
+
"xinyu.ui.onAction('openSettings', function(e) {",
|
|
3144
|
+
" // e.target: 触发事件的 DOM 元素",
|
|
3145
|
+
" // e.actionName: 'openSettings'",
|
|
3146
|
+
" // e.payload: data-action-payload 属性值(可选)",
|
|
3147
|
+
" xinyu.ui.showModal({",
|
|
3148
|
+
" title: '设置',",
|
|
3149
|
+
" content: '<p>配置项...</p>',",
|
|
3150
|
+
" actions: [",
|
|
3151
|
+
" { text: '保存', primary: true, onClick: function(ctx) { ctx.close(); } }",
|
|
3152
|
+
" ]",
|
|
3153
|
+
" });",
|
|
3154
|
+
"});",
|
|
3155
|
+
'',
|
|
3156
|
+
'// 2. 在 HTML 中使用 data-action(不会被净化器移除)',
|
|
3157
|
+
"xinyu.ui.registerSlot('input-toolbar',",
|
|
3158
|
+
" '<button data-action=\"openSettings\">⚙ 设置</button>' +",
|
|
3159
|
+
" '<button data-action=\"roll\" data-payload=\"d20\">🎲 投骰</button>'",
|
|
3160
|
+
");",
|
|
3161
|
+
'',
|
|
3162
|
+
'// 3. 携带 payload 的 action',
|
|
3163
|
+
"xinyu.ui.onAction('roll', function(e) {",
|
|
3164
|
+
" var diceType = e.payload; // 'd20'",
|
|
3165
|
+
" var result = Math.floor(Math.random() * 20) + 1;",
|
|
3166
|
+
" xinyu.ui.toast('投出了 ' + result + '(' + diceType + ')');",
|
|
3167
|
+
"});",
|
|
3168
|
+
].join('\n')}</pre>
|
|
3169
|
+
<div className="p-3 rounded-lg mt-2" style={{ backgroundColor: s.bgSecondary, border: `1px solid ${s.border}` }}>
|
|
3170
|
+
<div className="text-xs font-bold mb-1" style={{ color: s.accent }}>事件委托机制</div>
|
|
3171
|
+
<ul className="list-disc pl-5 text-xs space-y-1">
|
|
3172
|
+
<li><code style={codeStyle}>{'data-action="name"'}</code> — 声明动作名称</li>
|
|
3173
|
+
<li><code style={codeStyle}>{'data-payload="value"'}</code> — 可选,传递额外数据</li>
|
|
3174
|
+
<li>系统通过事件委托自动分发点击事件到对应插件</li>
|
|
3175
|
+
<li>支持事件冒泡:子元素的 <code style={codeStyle}>data-action</code> 会冒泡到容器被捕获</li>
|
|
3176
|
+
</ul>
|
|
3177
|
+
</div>
|
|
3178
|
+
</div>
|
|
3179
|
+
);
|
|
3180
|
+
}
|
|
3181
|
+
|
|
3182
|
+
// ==================== 新增章节内容 ====================
|
|
3183
|
+
|
|
3184
|
+
function CssVariablesSection() {
|
|
3185
|
+
return (
|
|
3186
|
+
<div>
|
|
3187
|
+
<p>星语提供了完整的 CSS 变量系统,确保插件在不同主题下都能正常显示。所有插件<strong>必须</strong>使用 CSS 变量来引用颜色、字体和圆角,禁止硬编码颜色值。</p>
|
|
3188
|
+
|
|
3189
|
+
<h3 id="sub-css-var-list" className="text-sm font-bold mt-6 mb-2 scroll-mt-4" style={{ color: s.textPrimary }}>完整的 CSS 变量列表</h3>
|
|
3190
|
+
<p>星语定义了 15 个 CSS 变量,分为颜色类(12 个)、字体类(2 个)和布局类(1 个)。</p>
|
|
3191
|
+
|
|
3192
|
+
<h4 className="text-xs font-bold mt-4 mb-2" style={{ color: s.accent }}>颜色类变量(12 个)</h4>
|
|
3193
|
+
<table className="w-full text-xs" style={{ borderCollapse: 'collapse' }}>
|
|
3194
|
+
<thead>
|
|
3195
|
+
<tr style={{ borderBottom: `1px solid ${s.border}` }}>
|
|
3196
|
+
<th className="text-left py-2 pr-3" style={{ color: s.textPrimary }}>变量名</th>
|
|
3197
|
+
<th className="text-left py-2 pr-3" style={{ color: s.textPrimary }}>用途</th>
|
|
3198
|
+
<th className="text-left py-2" style={{ color: s.textPrimary }}>默认值(暗夜紫主题)</th>
|
|
3199
|
+
</tr>
|
|
3200
|
+
</thead>
|
|
3201
|
+
<tbody>
|
|
3202
|
+
{[
|
|
3203
|
+
['--color-bg-primary', '主背景色(页面底色)', '#0f0a1a'],
|
|
3204
|
+
['--color-bg-secondary', '次级背景色(卡片/面板背景)', '#1a1128'],
|
|
3205
|
+
['--color-bg-tertiary', '三级背景色(hover 态、输入框背景)', '#2a1f3d'],
|
|
3206
|
+
['--color-text-primary', '主文字色(标题、正文)', '#e8e0f0'],
|
|
3207
|
+
['--color-text-secondary', '次级文字色(描述、标签)', '#a89bc2'],
|
|
3208
|
+
['--color-text-muted', '弱化文字色(提示、占位符)', '#6b5f85'],
|
|
3209
|
+
['--color-accent', '强调色(按钮、链接、高亮)', '#d4a843'],
|
|
3210
|
+
['--color-accent-hover', '强调色悬停态', '#e6bc5a'],
|
|
3211
|
+
['--color-border', '边框色', '#2a1f3d'],
|
|
3212
|
+
['--color-shadow', '阴影色', 'rgba(0, 0, 0, 0.5)'],
|
|
3213
|
+
['--color-user-bubble', '用户消息气泡色', '#2a1f3d'],
|
|
3214
|
+
['--color-ai-bubble', 'AI 消息气泡色', '#1a1128'],
|
|
3215
|
+
].map(([name, usage, defaultVal]) => (
|
|
3216
|
+
<tr key={name} style={{ borderBottom: `1px solid ${s.border}` }}>
|
|
3217
|
+
<td className="py-2 pr-3"><code style={codeStyle}>{name}</code></td>
|
|
3218
|
+
<td className="py-2 pr-3">{usage}</td>
|
|
3219
|
+
<td className="py-2"><code style={{ ...codeStyle, color: 'var(--color-text-muted)' }}>{defaultVal}</code></td>
|
|
3220
|
+
</tr>
|
|
3221
|
+
))}
|
|
3222
|
+
</tbody>
|
|
3223
|
+
</table>
|
|
3224
|
+
|
|
3225
|
+
<h4 className="text-xs font-bold mt-4 mb-2" style={{ color: s.accent }}>字体类变量(2 个)</h4>
|
|
3226
|
+
<table className="w-full text-xs" style={{ borderCollapse: 'collapse' }}>
|
|
3227
|
+
<thead>
|
|
3228
|
+
<tr style={{ borderBottom: `1px solid ${s.border}` }}>
|
|
3229
|
+
<th className="text-left py-2 pr-3" style={{ color: s.textPrimary }}>变量名</th>
|
|
3230
|
+
<th className="text-left py-2 pr-3" style={{ color: s.textPrimary }}>用途</th>
|
|
3231
|
+
<th className="text-left py-2" style={{ color: s.textPrimary }}>默认值</th>
|
|
3232
|
+
</tr>
|
|
3233
|
+
</thead>
|
|
3234
|
+
<tbody>
|
|
3235
|
+
<tr style={{ borderBottom: `1px solid ${s.border}` }}>
|
|
3236
|
+
<td className="py-2 pr-3"><code style={codeStyle}>--font-body</code></td>
|
|
3237
|
+
<td className="py-2 pr-3">正文字体</td>
|
|
3238
|
+
<td className="py-2"><code style={{ ...codeStyle, color: 'var(--color-text-muted)', fontSize: '11px' }}>{`'Noto Sans SC', 'PingFang SC', 'Microsoft YaHei', sans-serif`}</code></td>
|
|
3239
|
+
</tr>
|
|
3240
|
+
<tr style={{ borderBottom: `1px solid ${s.border}` }}>
|
|
3241
|
+
<td className="py-2 pr-3"><code style={codeStyle}>--font-heading</code></td>
|
|
3242
|
+
<td className="py-2 pr-3">标题字体</td>
|
|
3243
|
+
<td className="py-2"><code style={{ ...codeStyle, color: 'var(--color-text-muted)', fontSize: '11px' }}>{`'Noto Sans SC', 'PingFang SC', 'Microsoft YaHei', serif`}</code></td>
|
|
3244
|
+
</tr>
|
|
3245
|
+
</tbody>
|
|
3246
|
+
</table>
|
|
3247
|
+
|
|
3248
|
+
<h4 className="text-xs font-bold mt-4 mb-2" style={{ color: s.accent }}>布局类变量(1 个)</h4>
|
|
3249
|
+
<table className="w-full text-xs" style={{ borderCollapse: 'collapse' }}>
|
|
3250
|
+
<thead>
|
|
3251
|
+
<tr style={{ borderBottom: `1px solid ${s.border}` }}>
|
|
3252
|
+
<th className="text-left py-2 pr-3" style={{ color: s.textPrimary }}>变量名</th>
|
|
3253
|
+
<th className="text-left py-2 pr-3" style={{ color: s.textPrimary }}>用途</th>
|
|
3254
|
+
<th className="text-left py-2" style={{ color: s.textPrimary }}>默认值</th>
|
|
3255
|
+
</tr>
|
|
3256
|
+
</thead>
|
|
3257
|
+
<tbody>
|
|
3258
|
+
<tr style={{ borderBottom: `1px solid ${s.border}` }}>
|
|
3259
|
+
<td className="py-2 pr-3"><code style={codeStyle}>--border-radius</code></td>
|
|
3260
|
+
<td className="py-2 pr-3">全局圆角大小</td>
|
|
3261
|
+
<td className="py-2"><code style={{ ...codeStyle, color: 'var(--color-text-muted)' }}>12px</code></td>
|
|
3262
|
+
</tr>
|
|
3263
|
+
</tbody>
|
|
3264
|
+
</table>
|
|
3265
|
+
|
|
3266
|
+
<h3 id="sub-css-var-naming" className="text-sm font-bold mt-6 mb-2 scroll-mt-4" style={{ color: s.textPrimary }}>CSS 变量命名规范</h3>
|
|
3267
|
+
<ul className="list-disc pl-5 space-y-1">
|
|
3268
|
+
<li>格式:<code style={codeStyle}>{'--color-{类别}-{层级}'}</code>,如 <code style={codeStyle}>--color-bg-primary</code></li>
|
|
3269
|
+
<li>层级关系:<code style={codeStyle}>primary</code> > <code style={codeStyle}>secondary</code> > <code style={codeStyle}>tertiary</code> / <code style={codeStyle}>muted</code></li>
|
|
3270
|
+
<li>交互状态通过后缀 <code style={codeStyle}>-hover</code> 表达</li>
|
|
3271
|
+
<li>全部使用 kebab-case</li>
|
|
3272
|
+
</ul>
|
|
3273
|
+
|
|
3274
|
+
<h3 id="sub-css-var-usage" className="text-sm font-bold mt-6 mb-2 scroll-mt-4" style={{ color: s.textPrimary }}>插件中使用 CSS 变量的方式</h3>
|
|
3275
|
+
<p>插件有三种方式使用 CSS 变量:</p>
|
|
3276
|
+
<ul className="list-disc pl-5 space-y-1">
|
|
3277
|
+
<li><strong>内联 style</strong>:在 HTML 字符串的 style 属性中直接使用</li>
|
|
3278
|
+
<li><strong>injectStyle</strong>:在注入的 CSS 代码中使用</li>
|
|
3279
|
+
<li><strong>registerSlot</strong>:在注入的 HTML 内容中使用</li>
|
|
3280
|
+
</ul>
|
|
3281
|
+
|
|
3282
|
+
<h4 className="text-xs font-bold mt-4 mb-2" style={{ color: s.accent }}>方式 1:内联 style 中使用</h4>
|
|
3283
|
+
<pre style={preStyle}>{CODE_CSS_VAR_INLINE}</pre>
|
|
3284
|
+
|
|
3285
|
+
<h4 className="text-xs font-bold mt-4 mb-2" style={{ color: s.accent }}>方式 2:injectStyle 注入的 CSS 中使用</h4>
|
|
3286
|
+
<pre style={preStyle}>{CODE_CSS_VAR_INJECT}</pre>
|
|
3287
|
+
|
|
3288
|
+
<h3 id="sub-css-var-guide" className="text-sm font-bold mt-6 mb-2 scroll-mt-4" style={{ color: s.textPrimary }}>使用指南</h3>
|
|
3289
|
+
<div className="space-y-3 mt-2">
|
|
3290
|
+
<div className="p-3 rounded-lg" style={{ backgroundColor: s.bgSecondary, border: `1px solid ${s.border}` }}>
|
|
3291
|
+
<div className="text-xs font-bold mb-1" style={{ color: s.accent }}>背景色选择规则</div>
|
|
3292
|
+
<ul className="list-disc pl-5 text-xs space-y-1">
|
|
3293
|
+
<li>页面级背景:使用 <code style={codeStyle}>--color-bg-primary</code></li>
|
|
3294
|
+
<li>面板/卡片背景:使用 <code style={codeStyle}>--color-bg-secondary</code></li>
|
|
3295
|
+
<li>元素级背景(hover 态、输入框):使用 <code style={codeStyle}>--color-bg-tertiary</code></li>
|
|
3296
|
+
</ul>
|
|
3297
|
+
</div>
|
|
3298
|
+
<div className="p-3 rounded-lg" style={{ backgroundColor: s.bgSecondary, border: `1px solid ${s.border}` }}>
|
|
3299
|
+
<div className="text-xs font-bold mb-1" style={{ color: s.accent }}>文字色选择规则</div>
|
|
3300
|
+
<ul className="list-disc pl-5 text-xs space-y-1">
|
|
3301
|
+
<li>正文/标题:使用 <code style={codeStyle}>--color-text-primary</code></li>
|
|
3302
|
+
<li>辅助文字/描述:使用 <code style={codeStyle}>--color-text-secondary</code></li>
|
|
3303
|
+
<li>弱化文字/提示/占位符:使用 <code style={codeStyle}>--color-text-muted</code></li>
|
|
3304
|
+
</ul>
|
|
3305
|
+
</div>
|
|
3306
|
+
<div className="p-3 rounded-lg" style={{ backgroundColor: s.bgSecondary, border: `1px solid ${s.border}` }}>
|
|
3307
|
+
<div className="text-xs font-bold mb-1" style={{ color: s.accent }}>交互元素</div>
|
|
3308
|
+
<ul className="list-disc pl-5 text-xs space-y-1">
|
|
3309
|
+
<li>按钮/链接/高亮:使用 <code style={codeStyle}>--color-accent</code></li>
|
|
3310
|
+
<li>悬停态:使用 <code style={codeStyle}>--color-accent-hover</code></li>
|
|
3311
|
+
</ul>
|
|
3312
|
+
</div>
|
|
3313
|
+
<div className="p-3 rounded-lg" style={{ backgroundColor: s.bgSecondary, border: `1px solid ${s.border}` }}>
|
|
3314
|
+
<div className="text-xs font-bold mb-1" style={{ color: s.accent }}>其他</div>
|
|
3315
|
+
<ul className="list-disc pl-5 text-xs space-y-1">
|
|
3316
|
+
<li>边框:<code style={codeStyle}>--color-border</code></li>
|
|
3317
|
+
<li>阴影:<code style={codeStyle}>--color-shadow</code></li>
|
|
3318
|
+
<li>圆角:<code style={codeStyle}>--border-radius</code>(保持全局一致)</li>
|
|
3319
|
+
<li>正文字体:<code style={codeStyle}>--font-body</code></li>
|
|
3320
|
+
<li>标题字体:<code style={codeStyle}>--font-heading</code></li>
|
|
3321
|
+
</ul>
|
|
3322
|
+
</div>
|
|
3323
|
+
</div>
|
|
3324
|
+
|
|
3325
|
+
<h3 id="sub-css-var-custom" className="text-sm font-bold mt-6 mb-2 scroll-mt-4" style={{ color: s.textPrimary }}>创建自定义 CSS 变量</h3>
|
|
3326
|
+
<p>插件可以通过 <code style={codeStyle}>injectStyle</code> 定义自己的 CSS 变量。建议使用插件前缀避免冲突,如 <code style={codeStyle}>--my-plugin-primary</code>。</p>
|
|
3327
|
+
<pre style={preStyle}>{CODE_CSS_VAR_CUSTOM}</pre>
|
|
3328
|
+
|
|
3329
|
+
<h3 id="sub-css-var-themes" className="text-sm font-bold mt-6 mb-2 scroll-mt-4" style={{ color: s.textPrimary }}>四个预设主题对比</h3>
|
|
3330
|
+
<table className="w-full text-xs" style={{ borderCollapse: 'collapse' }}>
|
|
3331
|
+
<thead>
|
|
3332
|
+
<tr style={{ borderBottom: `1px solid ${s.border}` }}>
|
|
3333
|
+
<th className="text-left py-2 pr-3" style={{ color: s.textPrimary }}>主题 ID</th>
|
|
3334
|
+
<th className="text-left py-2 pr-3" style={{ color: s.textPrimary }}>名称</th>
|
|
3335
|
+
<th className="text-left py-2 pr-3" style={{ color: s.textPrimary }}>风格</th>
|
|
3336
|
+
<th className="text-left py-2 pr-3" style={{ color: s.textPrimary }}>暗色</th>
|
|
3337
|
+
<th className="text-left py-2 pr-3" style={{ color: s.textPrimary }}>强调色</th>
|
|
3338
|
+
<th className="text-left py-2" style={{ color: s.textPrimary }}>圆角</th>
|
|
3339
|
+
</tr>
|
|
3340
|
+
</thead>
|
|
3341
|
+
<tbody>
|
|
3342
|
+
{[
|
|
3343
|
+
['deep-night', '暗夜紫', '奇幻/史诗', 'true', '#d4a843 金色', '12px'],
|
|
3344
|
+
['moonlight', '月光白', '轻松/日常', 'false', '#2a4a7f 深蓝', '12px'],
|
|
3345
|
+
['cyber-glow', '赛博绿', '科幻/赛博朋克', 'true', '#00ff88 荧光绿', '4px'],
|
|
3346
|
+
['parchment', '羊皮纸', '古典/历史', 'false', '#8b4513 棕色', '8px'],
|
|
3347
|
+
].map(([id, name, style, isDark, accent, radius]) => (
|
|
3348
|
+
<tr key={id} style={{ borderBottom: `1px solid ${s.border}` }}>
|
|
3349
|
+
<td className="py-2 pr-3"><code style={codeStyle}>{id}</code></td>
|
|
3350
|
+
<td className="py-2 pr-3">{name}</td>
|
|
3351
|
+
<td className="py-2 pr-3">{style}</td>
|
|
3352
|
+
<td className="py-2 pr-3">{isDark === 'true' ? '是' : '否'}</td>
|
|
3353
|
+
<td className="py-2 pr-3"><code style={{ ...codeStyle, color: 'var(--color-text-muted)' }}>{accent}</code></td>
|
|
3354
|
+
<td className="py-2">{radius}</td>
|
|
3355
|
+
</tr>
|
|
3356
|
+
))}
|
|
3357
|
+
</tbody>
|
|
3358
|
+
</table>
|
|
3359
|
+
<p className="mt-2">插件使用 CSS 变量后,切换主题时所有 UI 会自动适配,无需额外处理。</p>
|
|
3360
|
+
</div>
|
|
3361
|
+
);
|
|
3362
|
+
}
|
|
3363
|
+
|
|
3364
|
+
function PluginStorageSection() {
|
|
3365
|
+
return (
|
|
3366
|
+
<div>
|
|
3367
|
+
<p>插件系统提供了三种数据存储方式,适用于不同的场景。选择合适的存储方式可以让插件更加健壮和高效。</p>
|
|
3368
|
+
|
|
3369
|
+
<h3 id="sub-storage-plugin" className="text-sm font-bold mt-6 mb-2 scroll-mt-4" style={{ color: s.textPrimary }}>方式 1:xinyu.plugin.storage(推荐)</h3>
|
|
3370
|
+
<p>插件独立持久化存储,数据存储在内存中(页面刷新后重置),以插件 ID 为命名空间隔离。不同插件之间的数据互不干扰。</p>
|
|
3371
|
+
<div className="p-3 rounded-lg mt-2" style={{ backgroundColor: s.bgSecondary, border: `1px solid ${s.border}` }}>
|
|
3372
|
+
<div className="text-xs font-bold mb-1" style={{ color: s.accent }}>API 列表</div>
|
|
3373
|
+
<ul className="list-disc pl-5 text-xs space-y-1">
|
|
3374
|
+
<li><code style={codeStyle}>xinyu.plugin.storage.get(key)</code> — 读取值(不存在返回 null)</li>
|
|
3375
|
+
<li><code style={codeStyle}>xinyu.plugin.storage.set(key, value)</code> — 写入值(自动 JSON 序列化)</li>
|
|
3376
|
+
<li><code style={codeStyle}>xinyu.plugin.storage.remove(key)</code> — 删除指定键</li>
|
|
3377
|
+
<li><code style={codeStyle}>xinyu.plugin.storage.keys()</code> — 获取所有键名数组</li>
|
|
3378
|
+
</ul>
|
|
3379
|
+
</div>
|
|
3380
|
+
<p className="mt-2"><strong>适用场景</strong>:用户偏好设置、插件状态持久化、历史记录等。</p>
|
|
3381
|
+
<pre style={preStyle}>{CODE_STORAGE_PLUGIN}</pre>
|
|
3382
|
+
|
|
3383
|
+
<h3 id="sub-game-info" className="text-sm font-bold mt-6 mb-2 scroll-mt-4" style={{ color: s.textPrimary }}>方式 2:xinyu.game.getWorldSetting / setWorldSetting / getSessionId / getMessages</h3>
|
|
3384
|
+
<p>用于获取和修改当前游戏的世界设定、会话 ID 和对话消息列表。插件可通过这些 API 了解游戏上下文,实现条件判断、数据统计等功能,也可以动态修改世界设定。</p>
|
|
3385
|
+
<p className="mt-1 text-xs" style={{ color: '#f97316' }}>⚠️ 读取需要声明 <code style={codeStyle}>game:info:read</code> 权限,未声明时所有读取 API 静默返回空值。写入需要声明 <code style={codeStyle}>game:info:write</code> 权限,未声明时写入操作静默忽略。</p>
|
|
3386
|
+
<div className="p-3 rounded-lg mt-2" style={{ backgroundColor: s.bgSecondary, border: `1px solid ${s.border}` }}>
|
|
3387
|
+
<div className="text-xs font-bold mb-1" style={{ color: s.accent }}>API 列表</div>
|
|
3388
|
+
<ul className="list-disc pl-5 text-xs space-y-1">
|
|
3389
|
+
<li><code style={codeStyle}>xinyu.game.getWorldSetting()</code> — 获取当前世界设定对象,字段完全由用户自定义(可能包含任意 key-value 对)</li>
|
|
3390
|
+
<li><code style={codeStyle}>xinyu.game.setWorldSetting(data)</code> — 修改世界设定(合并更新),修改实时生效并持久化到数据库。需要 <code style={codeStyle}>game:info:write</code> 权限</li>
|
|
3391
|
+
<li><code style={codeStyle}>xinyu.game.getSessionId()</code> — 获取当前游戏会话 ID(字符串)</li>
|
|
3392
|
+
<li><code style={codeStyle}>xinyu.game.getMessages()</code> — 获取当前对话消息列表,每条消息包含 <code style={codeStyle}>role</code>(user/assistant/system/notice)和 <code style={codeStyle}>content</code></li>
|
|
3393
|
+
</ul>
|
|
3394
|
+
</div>
|
|
3395
|
+
<p className="mt-2"><strong>适用场景</strong>:根据世界设定调整插件行为、统计对话轮数、读取历史消息内容、动态修改世界设定(如更新游戏进度、解锁新区域等)。</p>
|
|
3396
|
+
<pre style={preStyle}>{`// 示例:根据世界设定调整插件行为
|
|
3397
|
+
var world = xinyu.game.getWorldSetting();
|
|
3398
|
+
console.log('当前世界:', world.title);
|
|
3399
|
+
console.log('时代背景:', world.era);
|
|
3400
|
+
|
|
3401
|
+
// 示例:修改世界设定(合并更新,实时生效并持久化)
|
|
3402
|
+
xinyu.game.setWorldSetting({ "当前章节": "第三章·迷雾森林" });
|
|
3403
|
+
xinyu.game.setWorldSetting({ "解锁区域": ["王城", "迷雾森林", "矿洞"] });
|
|
3404
|
+
|
|
3405
|
+
// 示例:获取会话 ID 用于持久化存储的命名
|
|
3406
|
+
var sessionId = xinyu.game.getSessionId();
|
|
3407
|
+
|
|
3408
|
+
// 示例:统计当前对话轮数
|
|
3409
|
+
var messages = xinyu.game.getMessages();
|
|
3410
|
+
var userMsgCount = messages.filter(function(m) { return m.role === 'user'; }).length;
|
|
3411
|
+
console.log('玩家已发言 ' + userMsgCount + ' 次');`}</pre>
|
|
3412
|
+
|
|
3413
|
+
<h3 id="sub-storage-game" className="text-sm font-bold mt-6 mb-2 scroll-mt-4" style={{ color: s.textPrimary }}>方式 3:xinyu.game.setState / getState</h3>
|
|
3414
|
+
<p>游戏状态存储,全局共享,所有插件可读写。状态变更会触发 <code style={codeStyle}>game:stateChange</code> 宿主事件。</p>
|
|
3415
|
+
<div className="p-3 rounded-lg mt-2" style={{ backgroundColor: s.bgSecondary, border: `1px solid ${s.border}` }}>
|
|
3416
|
+
<div className="text-xs font-bold mb-1" style={{ color: s.accent }}>API 列表</div>
|
|
3417
|
+
<ul className="list-disc pl-5 text-xs space-y-1">
|
|
3418
|
+
<li><code style={codeStyle}>xinyu.game.setState(partialState)</code> — 合并更新游戏状态</li>
|
|
3419
|
+
<li><code style={codeStyle}>xinyu.game.getState()</code> — 获取完整游戏状态对象</li>
|
|
3420
|
+
</ul>
|
|
3421
|
+
</div>
|
|
3422
|
+
<p className="mt-2"><strong>适用场景</strong>:游戏运行时数据(HP、金币、等级、当前位置等)。</p>
|
|
3423
|
+
<pre style={preStyle}>{CODE_STORAGE_GAME}</pre>
|
|
3424
|
+
|
|
3425
|
+
<h3 id="sub-storage-attr" className="text-sm font-bold mt-6 mb-2 scroll-mt-4" style={{ color: s.textPrimary }}>方式 4:xinyu.game.registerAttribute / setAttribute</h3>
|
|
3426
|
+
<p>游戏属性注册,适合需要在 UI 上展示的属性。注册的属性会显示在游戏属性面板中,支持标签、图标、颜色等元数据。</p>
|
|
3427
|
+
<div className="p-3 rounded-lg mt-2" style={{ backgroundColor: s.bgSecondary, border: `1px solid ${s.border}` }}>
|
|
3428
|
+
<div className="text-xs font-bold mb-1" style={{ color: s.accent }}>API 列表</div>
|
|
3429
|
+
<ul className="list-disc pl-5 text-xs space-y-1">
|
|
3430
|
+
<li><code style={codeStyle}>xinyu.game.registerAttribute({ key, label, type, value, icon?, color? })</code> — 注册属性</li>
|
|
3431
|
+
<li><code style={codeStyle}>xinyu.game.setAttribute(key, value)</code> — 更新属性值</li>
|
|
3432
|
+
<li><code style={codeStyle}>xinyu.game.getAttributes()</code> — 获取所有属性(需 <code style={codeStyle}>game:info:read</code> 权限)</li>
|
|
3433
|
+
</ul>
|
|
3434
|
+
</div>
|
|
3435
|
+
<p className="mt-2"><strong>适用场景</strong>:需要在游戏属性面板中展示的属性(HP、MP、状态等)。</p>
|
|
3436
|
+
<pre style={preStyle}>{CODE_STORAGE_ATTR}</pre>
|
|
3437
|
+
|
|
3438
|
+
<h3 id="sub-storage-best" className="text-sm font-bold mt-6 mb-2 scroll-mt-4" style={{ color: s.textPrimary }}>存储最佳实践</h3>
|
|
3439
|
+
<ul className="list-disc pl-5 space-y-2">
|
|
3440
|
+
<li><strong>数据结构</strong>:建议使用 JSON 对象组织相关数据,而不是存储大量零散的键值对</li>
|
|
3441
|
+
<li><strong>定期保存</strong>:重要数据应在变更时立即保存,不要等到插件卸载</li>
|
|
3442
|
+
<li><strong>容量限制</strong>:localStorage 有 5MB 限制,不要存储大量数据(如完整的聊天记录)</li>
|
|
3443
|
+
<li><strong>状态检查</strong>:使用 <code style={codeStyle}>keys()</code> 方法检查存储状态,便于调试</li>
|
|
3444
|
+
<li><strong>命名空间隔离</strong>:<code style={codeStyle}>plugin.storage</code> 自动以插件 ID 隔离,无需担心键名冲突</li>
|
|
3445
|
+
<li><strong>游戏状态共享</strong>:<code style={codeStyle}>game.setState</code> 是全局共享的,多个插件可能修改同一字段,注意协调</li>
|
|
3446
|
+
</ul>
|
|
3447
|
+
</div>
|
|
3448
|
+
);
|
|
3449
|
+
}
|
|
3450
|
+
|
|
3451
|
+
function AiInterceptionSection() {
|
|
3452
|
+
return (
|
|
3453
|
+
<div>
|
|
3454
|
+
<p>AI 拦截系统允许插件在 AI 请求的不同阶段介入,修改 Prompt、请求参数或 AI 回复内容。这是实现 NPC 人格注入、规则修改、内容过滤等高级功能的核心机制。</p>
|
|
3455
|
+
|
|
3456
|
+
<h3 id="sub-ai-interceptors" className="text-sm font-bold mt-6 mb-2 scroll-mt-4" style={{ color: s.textPrimary }}>五种拦截器</h3>
|
|
3457
|
+
|
|
3458
|
+
<div className="space-y-3 mt-2">
|
|
3459
|
+
<div className="p-3 rounded-lg" style={{ backgroundColor: s.bgSecondary, border: `1px solid ${s.border}` }}>
|
|
3460
|
+
<div className="flex items-center gap-2 mb-1">
|
|
3461
|
+
<code className="text-xs font-bold" style={{ color: s.accent }}>xinyu.ai.onPromptBuild(handler)</code>
|
|
3462
|
+
</div>
|
|
3463
|
+
<p className="text-xs" style={{ color: s.textSecondary }}>Prompt 构建拦截。handler 接收当前 prompt 字符串和世界设定对象,返回修改后的字符串。</p>
|
|
3464
|
+
<p className="text-xs mt-1" style={{ color: s.textMuted }}>典型用途:注入 NPC 人格、游戏规则、角色状态信息。</p>
|
|
3465
|
+
</div>
|
|
3466
|
+
|
|
3467
|
+
<div className="p-3 rounded-lg" style={{ backgroundColor: s.bgSecondary, border: `1px solid ${s.border}` }}>
|
|
3468
|
+
<div className="flex items-center gap-2 mb-1">
|
|
3469
|
+
<code className="text-xs font-bold" style={{ color: s.accent }}>xinyu.ai.onBeforeSend(handler)</code>
|
|
3470
|
+
</div>
|
|
3471
|
+
<p className="text-xs" style={{ color: s.textSecondary }}>发送前拦截。handler 接收用户消息数组 <code style={codeStyle}>{'[{role, content}]'}</code>,返回修改后的消息数组。</p>
|
|
3472
|
+
<p className="text-xs mt-1" style={{ color: s.textMuted }}>典型用途:修改用户消息内容、过滤敏感词。</p>
|
|
3473
|
+
</div>
|
|
3474
|
+
|
|
3475
|
+
<div className="p-3 rounded-lg" style={{ backgroundColor: s.bgSecondary, border: `1px solid ${s.border}` }}>
|
|
3476
|
+
<div className="flex items-center gap-2 mb-1">
|
|
3477
|
+
<code className="text-xs font-bold" style={{ color: s.accent }}>xinyu.ai.onBeforeComplete(handler)</code>
|
|
3478
|
+
<span className="text-[10px] px-1.5 py-0.5 rounded" style={{ backgroundColor: s.accent, color: s.bgPrimary, fontWeight: 600 }}>NEW</span>
|
|
3479
|
+
</div>
|
|
3480
|
+
<p className="text-xs" style={{ color: s.textSecondary }}>完整消息数组拦截。handler 接收包含 system prompt + 所有用户/助手消息的完整数组,返回修改后的数组。</p>
|
|
3481
|
+
<p className="text-xs mt-1" style={{ color: s.textMuted }}>典型用途:角色合并、消息重排、缓存优化、上下文窗口裁剪。这是实现缓存优化插件的核心钩子。</p>
|
|
3482
|
+
<pre className="mt-2" style={preStyle}>{[
|
|
3483
|
+
"xinyu.ai.onBeforeComplete(function(messages) {",
|
|
3484
|
+
" // messages[0] 是 system prompt",
|
|
3485
|
+
" // messages[1...] 是用户/助手对话历史",
|
|
3486
|
+
" // 可以自由重组,例如合并连续的 user 消息",
|
|
3487
|
+
" return messages;",
|
|
3488
|
+
"});",
|
|
3489
|
+
].join('\n')}</pre>
|
|
3490
|
+
</div>
|
|
3491
|
+
|
|
3492
|
+
<div className="p-3 rounded-lg" style={{ backgroundColor: s.bgSecondary, border: `1px solid ${s.border}` }}>
|
|
3493
|
+
<div className="flex items-center gap-2 mb-1">
|
|
3494
|
+
<code className="text-xs font-bold" style={{ color: s.accent }}>xinyu.ai.onAfterReceive(handler)</code>
|
|
3495
|
+
</div>
|
|
3496
|
+
<p className="text-xs" style={{ color: s.textSecondary }}>接收后拦截。handler 接收 AI 回复内容字符串,返回修改后的内容。</p>
|
|
3497
|
+
<p className="text-xs mt-1" style={{ color: s.textMuted }}>典型用途:内容过滤、格式化、添加元数据标记。</p>
|
|
3498
|
+
</div>
|
|
3499
|
+
|
|
3500
|
+
<div className="p-3 rounded-lg" style={{ backgroundColor: s.bgSecondary, border: `1px solid ${s.border}` }}>
|
|
3501
|
+
<div className="flex items-center gap-2 mb-1">
|
|
3502
|
+
<code className="text-xs font-bold" style={{ color: s.accent }}>xinyu.ai.onRequestConfig(handler)</code>
|
|
3503
|
+
</div>
|
|
3504
|
+
<p className="text-xs" style={{ color: s.textSecondary }}>请求配置拦截。handler 接收 AIRequestConfig 对象,返回修改后的配置。</p>
|
|
3505
|
+
<p className="text-xs mt-1" style={{ color: s.textMuted }}>典型用途:动态调整模型选择、请求超时等全局配置。</p>
|
|
3506
|
+
</div>
|
|
3507
|
+
</div>
|
|
3508
|
+
|
|
3509
|
+
<h3 className="text-sm font-bold mt-6 mb-2" style={{ color: s.textPrimary }}>拦截器示例</h3>
|
|
3510
|
+
<pre style={preStyle}>{CODE_AI_PROMPT}</pre>
|
|
3511
|
+
|
|
3512
|
+
<h3 id="sub-ai-pipeline" className="text-sm font-bold mt-6 mb-2 scroll-mt-4" style={{ color: s.textPrimary }}>管道模式详解</h3>
|
|
3513
|
+
<p>多个插件注册的同类型拦截器会按注册顺序形成管道(Pipeline),前一个 handler 的输出是后一个 handler 的输入。</p>
|
|
3514
|
+
<pre style={preStyle}>{CODE_AI_PIPELINE}</pre>
|
|
3515
|
+
|
|
3516
|
+
<h3 id="sub-ai-npc-example" className="text-sm font-bold mt-6 mb-2 scroll-mt-4" style={{ color: s.textPrimary }}>完整示例:NPC 人格注入插件</h3>
|
|
3517
|
+
<pre style={preStyle}>{CODE_AI_NPC}</pre>
|
|
3518
|
+
|
|
3519
|
+
<h3 id="sub-ai-notes" className="text-sm font-bold mt-6 mb-2 scroll-mt-4" style={{ color: s.textPrimary }}>注意事项</h3>
|
|
3520
|
+
<ul className="list-disc pl-5 space-y-2">
|
|
3521
|
+
<li><strong>保持 Prompt 简洁</strong>:通过拦截器追加的内容应简洁明确,避免浪费 token。冗长的 Prompt 会降低 AI 响应质量并增加成本</li>
|
|
3522
|
+
<li><strong>不要过度修改 AI 回复</strong>:<code style={codeStyle}>onAfterReceive</code> 应谨慎使用,避免破坏 AI 输出的质量和连贯性</li>
|
|
3523
|
+
<li><strong>handler 必须返回值</strong>:每个 handler 都应该返回修改后的值,不要返回 undefined,否则管道会中断</li>
|
|
3524
|
+
<li><strong>异常处理</strong>:handler 内部应自行 try-catch 处理异常,避免一个插件的错误影响整个管道</li>
|
|
3525
|
+
<li><strong>注册顺序</strong>:拦截器按注册顺序执行,如果依赖其他插件的输出,需要注意加载顺序</li>
|
|
3526
|
+
</ul>
|
|
3527
|
+
</div>
|
|
3528
|
+
);
|
|
3529
|
+
}
|
|
3530
|
+
|
|
3531
|
+
function PluginConfigSection() {
|
|
3532
|
+
return (
|
|
3533
|
+
<div>
|
|
3534
|
+
<p>插件配置系统允许插件定义可配置的参数,用户可以在插件管理页面中修改这些参数,无需修改插件代码。</p>
|
|
3535
|
+
|
|
3536
|
+
<h3 id="sub-config-schema" className="text-sm font-bold mt-6 mb-2 scroll-mt-4" style={{ color: s.textPrimary }}>configSchema 定义</h3>
|
|
3537
|
+
<p>通过 <code style={codeStyle}>configSchema</code> 数组定义插件的可配置字段。每个字段遵循 <code style={codeStyle}>PluginConfigField</code> 接口:</p>
|
|
3538
|
+
<div className="p-3 rounded-lg mt-2" style={{ backgroundColor: s.bgSecondary, border: `1px solid ${s.border}` }}>
|
|
3539
|
+
<div className="text-xs font-bold mb-1" style={{ color: s.accent }}>PluginConfigField 接口</div>
|
|
3540
|
+
<table className="w-full text-xs mt-1" style={{ borderCollapse: 'collapse' }}>
|
|
3541
|
+
<thead>
|
|
3542
|
+
<tr style={{ borderBottom: `1px solid ${s.border}` }}>
|
|
3543
|
+
<th className="text-left py-2 pr-3" style={{ color: s.textPrimary }}>字段</th>
|
|
3544
|
+
<th className="text-left py-2 pr-3" style={{ color: s.textPrimary }}>类型</th>
|
|
3545
|
+
<th className="text-left py-2" style={{ color: s.textPrimary }}>说明</th>
|
|
3546
|
+
</tr>
|
|
3547
|
+
</thead>
|
|
3548
|
+
<tbody>
|
|
3549
|
+
{[
|
|
3550
|
+
['key', 'string', '配置键名,用于代码中读取'],
|
|
3551
|
+
['label', 'string', '显示名称'],
|
|
3552
|
+
['type', 'string', '字段类型:text / number / boolean / select / textarea'],
|
|
3553
|
+
['defaultValue', 'any', '默认值'],
|
|
3554
|
+
['description', 'string', '配置说明文字'],
|
|
3555
|
+
['options', 'Array', '仅 select 类型,可选值列表 { value, label }'],
|
|
3556
|
+
].map(([field, type, desc]) => (
|
|
3557
|
+
<tr key={field} style={{ borderBottom: `1px solid ${s.border}` }}>
|
|
3558
|
+
<td className="py-2 pr-3"><code style={codeStyle}>{field}</code></td>
|
|
3559
|
+
<td className="py-2 pr-3"><code style={{ ...codeStyle, color: 'var(--color-text-muted)' }}>{type}</code></td>
|
|
3560
|
+
<td className="py-2">{desc}</td>
|
|
3561
|
+
</tr>
|
|
3562
|
+
))}
|
|
3563
|
+
</tbody>
|
|
3564
|
+
</table>
|
|
3565
|
+
</div>
|
|
3566
|
+
|
|
3567
|
+
<h3 id="sub-config-schema-example" className="text-sm font-bold mt-6 mb-2 scroll-mt-4" style={{ color: s.textPrimary }}>configSchema 示例</h3>
|
|
3568
|
+
<pre style={preStyle}>{CODE_CONFIG_SCHEMA}</pre>
|
|
3569
|
+
|
|
3570
|
+
<h3 id="sub-config-read" className="text-sm font-bold mt-6 mb-2 scroll-mt-4" style={{ color: s.textPrimary }}>在插件代码中读写配置</h3>
|
|
3571
|
+
<div className="p-3 rounded-lg mt-2" style={{ backgroundColor: s.bgSecondary, border: `1px solid ${s.border}` }}>
|
|
3572
|
+
<div className="text-xs font-bold mb-1" style={{ color: s.accent }}>API 列表</div>
|
|
3573
|
+
<ul className="list-disc pl-5 text-xs space-y-1">
|
|
3574
|
+
<li><code style={codeStyle}>xinyu.plugin.config.get()</code> — 获取全部配置对象</li>
|
|
3575
|
+
<li><code style={codeStyle}>xinyu.plugin.config.get(key)</code> — 获取指定键的配置值</li>
|
|
3576
|
+
<li><code style={codeStyle}>xinyu.plugin.config.set(newConfig)</code> — 替换全部配置</li>
|
|
3577
|
+
<li><code style={codeStyle}>xinyu.plugin.config.set(key, value)</code> — 设置单个配置项(合并更新,不影响其他键)</li>
|
|
3578
|
+
<li><code style={codeStyle}>xinyu.plugin.on('onConfigChange', handler)</code> — 监听配置变更,详见 <NavLink sectionId="lifecycle">生命周期钩子</NavLink></li>
|
|
3579
|
+
</ul>
|
|
3580
|
+
<div className="text-xs font-bold mb-1 mt-3" style={{ color: s.accent }}>⚠️ 注意</div>
|
|
3581
|
+
<p className="text-xs" style={{ color: s.textSecondary }}>配置数据(<code style={codeStyle}>xinyu.plugin.config</code>)与运行时存储(<code style={codeStyle}>xinyu.plugin.storage</code>)是独立的。配置会持久化到数据库,storage 仅存在于内存中。</p>
|
|
3582
|
+
</div>
|
|
3583
|
+
<p className="mt-2">配置值来自用户在插件管理页面中的设置。如果用户未修改,则使用 <code style={codeStyle}>defaultValue</code>。插件也可以通过 <code style={codeStyle}>config.set()</code> 自行修改配置,修改后会自动持久化并通知其他标签页。</p>
|
|
3584
|
+
<pre style={preStyle}>{CODE_CONFIG_READ}</pre>
|
|
3585
|
+
|
|
3586
|
+
<h3 id="sub-config-full-example" className="text-sm font-bold mt-6 mb-2 scroll-mt-4" style={{ color: s.textPrimary }}>完整示例:带配置的骰子插件</h3>
|
|
3587
|
+
<pre style={preStyle}>{CODE_CONFIG_FULL}</pre>
|
|
3588
|
+
|
|
3589
|
+
<h3 id="sub-config-ingame" className="text-sm font-bold mt-6 mb-2 scroll-mt-4" style={{ color: s.textPrimary }}>配置作用域</h3>
|
|
3590
|
+
<p>插件配置分为两个作用域:</p>
|
|
3591
|
+
<div className="space-y-2 mt-2">
|
|
3592
|
+
<div className="p-3 rounded-lg" style={{ backgroundColor: s.bgSecondary, border: `1px solid ${s.border}` }}>
|
|
3593
|
+
<div className="text-xs font-bold mb-1" style={{ color: s.accent }}>全局配置(global scope)— 游戏外</div>
|
|
3594
|
+
<p className="text-xs" style={{ color: s.textSecondary }}>在扩展管理页面的插件详情弹窗中修改。作为新游戏的默认配置,所有新建游戏世界都会继承这份配置。</p>
|
|
3595
|
+
</div>
|
|
3596
|
+
<div className="p-3 rounded-lg" style={{ backgroundColor: s.bgSecondary, border: `1px solid ${s.border}` }}>
|
|
3597
|
+
<div className="text-xs font-bold mb-1" style={{ color: s.accent }}>世界配置(world scope)— 游戏内</div>
|
|
3598
|
+
<p className="text-xs" style={{ color: s.textSecondary }}>在游戏内的插件抽屉中修改。仅作用于当前游戏世界,不影响其他游戏,也不影响全局默认配置。修改后自动保存,并通过 <code style={codeStyle}>onConfigChange</code> hook 通知插件代码,同时同步到其他打开的标签页。</p>
|
|
3599
|
+
</div>
|
|
3600
|
+
</div>
|
|
3601
|
+
<p className="mt-2 text-xs" style={{ color: s.textMuted }}>💡 优先级:世界配置 > 全局配置。即游戏内修改会覆盖全局默认值,但不会反向修改全局配置。</p>
|
|
3602
|
+
</div>
|
|
3603
|
+
);
|
|
3604
|
+
}
|
|
3605
|
+
|
|
3606
|
+
function PersistentStorageSection() {
|
|
3607
|
+
const codeStyle = { backgroundColor: s.bgSecondary, padding: '1px 5px', borderRadius: '3px', fontSize: '12px', color: s.accent };
|
|
3608
|
+
return (
|
|
3609
|
+
<div>
|
|
3610
|
+
<h3 id="sub-persistent-storage" className="text-sm font-bold mt-6 mb-2 scroll-mt-4" style={{ color: s.textPrimary }}>持久化存储 xinyu.storage</h3>
|
|
3611
|
+
<p>与 <code style={codeStyle}>xinyu.plugin.storage</code>(内存,刷新后重置)不同,<code style={codeStyle}>xinyu.storage</code> 将数据持久化到数据库,页面刷新后仍然保留。</p>
|
|
3612
|
+
<div className="p-3 rounded-lg mt-2" style={{ backgroundColor: s.bgSecondary, border: `1px solid ${s.border}` }}>
|
|
3613
|
+
<div className="text-xs font-bold mb-1" style={{ color: s.accent }}>🔒 需要权限</div>
|
|
3614
|
+
<p className="text-xs" style={{ color: s.textSecondary }}>使用前必须在插件注释中声明 <code style={codeStyle}>// @permission storage:plugin</code>,否则所有操作静默返回 null/undefined。</p>
|
|
3615
|
+
</div>
|
|
3616
|
+
<div className="mt-2">
|
|
3617
|
+
<p className="text-xs font-bold mb-1">API 列表(均为 async):</p>
|
|
3618
|
+
<ul className="list-disc pl-5 text-xs space-y-0.5">
|
|
3619
|
+
<li><code style={codeStyle}>await xinyu.storage.get(key)</code> — 读取值(不存在返回 null)</li>
|
|
3620
|
+
<li><code style={codeStyle}>await xinyu.storage.set(key, value)</code> — 写入值(自动 JSON 序列化)</li>
|
|
3621
|
+
<li><code style={codeStyle}>await xinyu.storage.remove(key)</code> — 删除指定键</li>
|
|
3622
|
+
<li><code style={codeStyle}>await xinyu.storage.keys()</code> — 获取所有键名数组</li>
|
|
3623
|
+
</ul>
|
|
3624
|
+
</div>
|
|
3625
|
+
<div className="p-3 rounded-lg mt-2" style={{ backgroundColor: s.bgSecondary, border: `1px solid ${s.border}` }}>
|
|
3626
|
+
<p className="text-xs font-bold mb-1" style={{ color: s.accent }}>🌍 数据自动按世界隔离</p>
|
|
3627
|
+
<p className="text-xs" style={{ color: s.textSecondary }}>storage 数据自动关联当前游戏世界 ID,不同世界的同名 key 互不影响。插件无需手动传入 worldId,运行时会自动处理。在非游戏页面(如设置页)中,worldId 为空,数据存储在全局命名空间。</p>
|
|
3628
|
+
</div>
|
|
3629
|
+
<pre style={{ backgroundColor: s.bgSecondary, border: `1px solid ${s.border}`, borderRadius: '8px', padding: '12px', fontSize: '12px', overflowX: 'auto', marginTop: '8px', color: s.textPrimary }}>{[
|
|
3630
|
+
'// 需要声明权限:// @permission storage:plugin',
|
|
3631
|
+
'',
|
|
3632
|
+
'xinyu.plugin.on("onLoad", async function() {',
|
|
3633
|
+
' // 读取上次的计数',
|
|
3634
|
+
' var count = await xinyu.storage.get("visitCount") || 0;',
|
|
3635
|
+
' count++;',
|
|
3636
|
+
' await xinyu.storage.set("visitCount", count);',
|
|
3637
|
+
' xinyu.chat.insertSystemMessage("这是你第 " + count + " 次访问");',
|
|
3638
|
+
'',
|
|
3639
|
+
' // 存储复杂对象',
|
|
3640
|
+
' await xinyu.storage.set("playerData", {',
|
|
3641
|
+
' name: "旅行者",',
|
|
3642
|
+
' level: 5,',
|
|
3643
|
+
' inventory: ["剑", "盾牌", "药水"]',
|
|
3644
|
+
' });',
|
|
3645
|
+
'});',
|
|
3646
|
+
].join('\n')}</pre>
|
|
3647
|
+
</div>
|
|
3648
|
+
);
|
|
3649
|
+
}
|
|
3650
|
+
|
|
3651
|
+
function FileApiSection() {
|
|
3652
|
+
const codeStyle = { backgroundColor: s.bgSecondary, padding: '1px 5px', borderRadius: '3px', fontSize: '12px', color: s.accent };
|
|
3653
|
+
return (
|
|
3654
|
+
<div>
|
|
3655
|
+
<h3 id="sub-file-api" className="text-sm font-bold mt-6 mb-2 scroll-mt-4" style={{ color: s.textPrimary }}>文件操作 xinyu.file</h3>
|
|
3656
|
+
<p>插件可以在自己的私有目录下读写文件。每个插件只能访问 <code style={codeStyle}>data/plugin-files/{'{pluginId}'}/</code> 下的文件,无法访问其他插件或系统的文件。</p>
|
|
3657
|
+
<div className="p-3 rounded-lg mt-2" style={{ backgroundColor: s.bgSecondary, border: `1px solid ${s.border}` }}>
|
|
3658
|
+
<div className="text-xs font-bold mb-1" style={{ color: s.accent }}>🔒 需要权限</div>
|
|
3659
|
+
<ul className="text-xs space-y-0.5" style={{ color: s.textSecondary }}>
|
|
3660
|
+
<li>读取文件:<code style={codeStyle}>// @permission file:read</code></li>
|
|
3661
|
+
<li>写入/删除文件:<code style={codeStyle}>// @permission file:write</code></li>
|
|
3662
|
+
</ul>
|
|
3663
|
+
</div>
|
|
3664
|
+
<div className="mt-2">
|
|
3665
|
+
<p className="text-xs font-bold mb-1">API 列表(均为 async):</p>
|
|
3666
|
+
<ul className="list-disc pl-5 text-xs space-y-0.5">
|
|
3667
|
+
<li><code style={codeStyle}>await xinyu.file.read(fileName)</code> — 读取文件内容(不存在返回 null)</li>
|
|
3668
|
+
<li><code style={codeStyle}>await xinyu.file.write(fileName, content)</code> — 写入文件(自动创建目录)</li>
|
|
3669
|
+
<li><code style={codeStyle}>await xinyu.file.remove(fileName)</code> — 删除文件</li>
|
|
3670
|
+
<li><code style={codeStyle}>await xinyu.file.list()</code> — 列出私有目录下的所有文件名</li>
|
|
3671
|
+
</ul>
|
|
3672
|
+
</div>
|
|
3673
|
+
<pre style={{ backgroundColor: s.bgSecondary, border: `1px solid ${s.border}`, borderRadius: '8px', padding: '12px', fontSize: '12px', overflowX: 'auto', marginTop: '8px', color: s.textPrimary }}>{[
|
|
3674
|
+
'// 需要声明权限:// @permission file:read',
|
|
3675
|
+
'// // @permission file:write',
|
|
3676
|
+
'',
|
|
3677
|
+
'xinyu.plugin.on("onLoad", async function() {',
|
|
3678
|
+
' // 写入日志文件',
|
|
3679
|
+
' await xinyu.file.write("log.txt", "插件已加载\\n");',
|
|
3680
|
+
'',
|
|
3681
|
+
' // 追加内容',
|
|
3682
|
+
' var existing = await xinyu.file.read("log.txt") || "";',
|
|
3683
|
+
' await xinyu.file.write("log.txt", existing + "用户进入了游戏\\n");',
|
|
3684
|
+
'',
|
|
3685
|
+
' // 列出所有文件',
|
|
3686
|
+
' var files = await xinyu.file.list();',
|
|
3687
|
+
' console.log("插件文件:", files);',
|
|
3688
|
+
'});',
|
|
3689
|
+
].join('\n')}</pre>
|
|
3690
|
+
</div>
|
|
3691
|
+
);
|
|
3692
|
+
}
|
|
3693
|
+
|
|
3694
|
+
function ResourceApiSection() {
|
|
3695
|
+
const codeStyle = { backgroundColor: s.bgSecondary, padding: '1px 5px', borderRadius: '3px', fontSize: '12px', color: s.accent };
|
|
3696
|
+
const preStyle = { backgroundColor: s.bgSecondary, border: `1px solid ${s.border}`, borderRadius: '8px', padding: '12px', fontSize: '12px', overflowX: 'auto', color: s.textPrimary };
|
|
3697
|
+
|
|
3698
|
+
return (
|
|
3699
|
+
<div>
|
|
3700
|
+
<h3 id="sub-resource-api" className="text-sm font-bold mt-6 mb-2 scroll-mt-4" style={{ color: s.textPrimary }}>插件资源访问 xinyu.plugin.getResource / getResourceUrl</h3>
|
|
3701
|
+
<p>插件可以访问 .xye 插件包内的资源文件(如图片、音频、JSON 数据等)。资源文件存放在插件的 <code style={codeStyle}>resources/</code> 目录下,通过 HTTP 端点访问。</p>
|
|
3702
|
+
<div className="p-3 rounded-lg mt-2" style={{ backgroundColor: s.bgSecondary, border: `1px solid ${s.border}` }}>
|
|
3703
|
+
<div className="text-xs font-bold mb-1" style={{ color: s.accent }}>🔒 需要权限</div>
|
|
3704
|
+
<ul className="text-xs space-y-0.5" style={{ color: s.textSecondary }}>
|
|
3705
|
+
<li>访问资源:<code style={codeStyle}>// @permission file:read</code></li>
|
|
3706
|
+
</ul>
|
|
3707
|
+
</div>
|
|
3708
|
+
<div className="p-3 rounded-lg mt-2" style={{ backgroundColor: s.bgSecondary, border: `1px solid ${s.border}` }}>
|
|
3709
|
+
<div className="text-xs font-bold mb-1" style={{ color: s.accent }}>📁 resources 目录规则</div>
|
|
3710
|
+
<ul className="text-xs space-y-0.5" style={{ color: s.textSecondary }}>
|
|
3711
|
+
<li>资源文件统一存放在 <code style={codeStyle}>resources/</code> 目录下(如 <code style={codeStyle}>resources/images/icon.png</code>)</li>
|
|
3712
|
+
<li><code style={codeStyle}>resources/</code> 目录本身<strong>禁止删除</strong>(API 层面限制),目录内的文件可正常管理</li>
|
|
3713
|
+
<li>插件代码文件(如 <code style={codeStyle}>index.js</code>)放在插件根目录,不要放在 <code style={codeStyle}>resources/</code> 下</li>
|
|
3714
|
+
</ul>
|
|
3715
|
+
</div>
|
|
3716
|
+
<div className="mt-2">
|
|
3717
|
+
<p className="text-xs font-bold mb-1">API 列表:</p>
|
|
3718
|
+
<ul className="list-disc pl-5 text-xs space-y-0.5">
|
|
3719
|
+
<li><code style={codeStyle}>await xinyu.plugin.getResource(resourcePath)</code> — 异步获取资源文件内容。文本文件返回字符串,二进制文件(图片等)返回 data URL</li>
|
|
3720
|
+
<li><code style={codeStyle}>xinyu.plugin.getResourceUrl(resourcePath)</code> — 同步返回资源的 HTTP URL,可直接用于 <code style={codeStyle}>{'<img src="...">'}</code> 等 HTML 属性</li>
|
|
3721
|
+
</ul>
|
|
3722
|
+
</div>
|
|
3723
|
+
{/* @ts-ignore */}
|
|
3724
|
+
<pre style={preStyle}>{[
|
|
3725
|
+
'// 需要声明权限:// @permission file:read',
|
|
3726
|
+
'',
|
|
3727
|
+
'xinyu.plugin.on("onLoad", async function() {',
|
|
3728
|
+
' // 方式 1:获取资源内容(适用于文本/JSON/小图片)',
|
|
3729
|
+
' var iconData = await xinyu.plugin.getResource("resources/images/icon.png");',
|
|
3730
|
+
' // iconData 为 data URL,如 "data:image/png;base64,..."',
|
|
3731
|
+
'',
|
|
3732
|
+
' // 方式 2:获取资源 URL(适用于 HTML 中直接引用)',
|
|
3733
|
+
' var url = xinyu.plugin.getResourceUrl("resources/images/banner.jpg");',
|
|
3734
|
+
' xinyu.ui.registerSlot("status-bar",',
|
|
3735
|
+
' <img src="url" style="height:20px;border-radius:4px" alt="">',
|
|
3736
|
+
' );',
|
|
3737
|
+
'',
|
|
3738
|
+
' // 读取 JSON 配置',
|
|
3739
|
+
' var config = await xinyu.plugin.getResource("resources/data/config.json");',
|
|
3740
|
+
' if (config) {',
|
|
3741
|
+
' var data = JSON.parse(config);',
|
|
3742
|
+
' console.log("插件配置:", data);',
|
|
3743
|
+
' }',
|
|
3744
|
+
'});',
|
|
3745
|
+
].join('\n')}</pre>
|
|
3746
|
+
<p className="mt-2 text-xs" style={{ color: s.textSecondary }}>
|
|
3747
|
+
<strong>适用场景</strong>:加载插件自带的图标、背景图片、音效、配置数据等资源文件。推荐使用 <code style={codeStyle}>getResourceUrl</code> 在 HTML 中引用大文件(如图片),使用 <code style={codeStyle}>getResource</code> 读取文本或小文件内容。
|
|
3748
|
+
</p>
|
|
3749
|
+
</div>
|
|
3750
|
+
);
|
|
3751
|
+
}
|
|
3752
|
+
|
|
3753
|
+
function PluginImportExportSection() {
|
|
3754
|
+
return (
|
|
3755
|
+
<div>
|
|
3756
|
+
<p>插件系统支持导入导出功能,方便插件分享、备份和迁移。</p>
|
|
3757
|
+
|
|
3758
|
+
<h3 id="sub-export-format" className="text-sm font-bold mt-6 mb-2 scroll-mt-4" style={{ color: s.textPrimary }}>导出格式</h3>
|
|
3759
|
+
<p>支持两种导出格式:</p>
|
|
3760
|
+
<ul className="list-disc pl-5 space-y-1">
|
|
3761
|
+
<li><strong>单个插件导出</strong>:直接返回插件 JSON 对象</li>
|
|
3762
|
+
<li><strong>批量导出</strong>:使用 <code style={codeStyle}>xinyu-extension-v1</code> 格式,包含 format、exportedAt、plugins 字段</li>
|
|
3763
|
+
</ul>
|
|
3764
|
+
<pre style={preStyle}>{CODE_EXPORT_FORMAT}</pre>
|
|
3765
|
+
|
|
3766
|
+
<h3 id="sub-import-format" className="text-sm font-bold mt-6 mb-2 scroll-mt-4" style={{ color: s.textPrimary }}>导入格式</h3>
|
|
3767
|
+
<p>支持 3 种导入格式,系统会自动识别:</p>
|
|
3768
|
+
<div className="space-y-2 mt-2">
|
|
3769
|
+
<div className="p-3 rounded-lg" style={{ backgroundColor: s.bgSecondary, border: `1px solid ${s.border}` }}>
|
|
3770
|
+
<div className="text-xs font-bold mb-1" style={{ color: s.accent }}>格式 1:完整导出格式</div>
|
|
3771
|
+
<p className="text-xs" style={{ color: s.textSecondary }}>包含 <code style={codeStyle}>format: 'xinyu-extension-v1'</code> 和 <code style={codeStyle}>plugins</code> 数组</p>
|
|
3772
|
+
</div>
|
|
3773
|
+
<div className="p-3 rounded-lg" style={{ backgroundColor: s.bgSecondary, border: `1px solid ${s.border}` }}>
|
|
3774
|
+
<div className="text-xs font-bold mb-1" style={{ color: s.accent }}>格式 2:直接数组</div>
|
|
3775
|
+
<p className="text-xs" style={{ color: s.textSecondary }}>JSON 数组,每个元素是一个插件对象</p>
|
|
3776
|
+
</div>
|
|
3777
|
+
<div className="p-3 rounded-lg" style={{ backgroundColor: s.bgSecondary, border: `1px solid ${s.border}` }}>
|
|
3778
|
+
<div className="text-xs font-bold mb-1" style={{ color: s.accent }}>格式 3:单个插件</div>
|
|
3779
|
+
<p className="text-xs" style={{ color: s.textSecondary }}>单个插件 JSON 对象,直接包含 id、code 等字段</p>
|
|
3780
|
+
</div>
|
|
3781
|
+
</div>
|
|
3782
|
+
<pre style={preStyle}>{CODE_IMPORT_FORMAT}</pre>
|
|
3783
|
+
|
|
3784
|
+
<h3 id="sub-import-behavior" className="text-sm font-bold mt-6 mb-2 scroll-mt-4" style={{ color: s.textPrimary }}>导入行为</h3>
|
|
3785
|
+
<ul className="list-disc pl-5 space-y-2">
|
|
3786
|
+
<li><strong>Upsert 策略</strong>:如果已存在相同 ID 的插件,则更新其代码和配置;否则创建新插件</li>
|
|
3787
|
+
<li><strong>自动绑定</strong>:导入的插件会自动创建全局绑定(global scope)</li>
|
|
3788
|
+
<li><strong>必填字段</strong>:<code style={codeStyle}>id</code>、<code style={codeStyle}>name</code>、<code style={codeStyle}>version</code>、<code style={codeStyle}>type</code>、<code style={codeStyle}>code</code></li>
|
|
3789
|
+
<li><strong>可选字段</strong>:<code style={codeStyle}>description</code>、<code style={codeStyle}>author</code>、<code style={codeStyle}>configSchema</code>、<code style={codeStyle}>commonPermissions</code>、<code style={codeStyle}>exclusivePermissions</code>、<code style={codeStyle}>requiredPermissions</code></li>
|
|
3790
|
+
</ul>
|
|
3791
|
+
|
|
3792
|
+
<h3 id="sub-json-spec" className="text-sm font-bold mt-6 mb-2 scroll-mt-4" style={{ color: s.textPrimary }}>JSON 文件格式规范</h3>
|
|
3793
|
+
<pre style={preStyle}>{[
|
|
3794
|
+
'{',
|
|
3795
|
+
' "format": "xinyu-extension-v1",',
|
|
3796
|
+
' "exportedAt": "2026-04-26T12:00:00.000Z",',
|
|
3797
|
+
' "plugins": [{',
|
|
3798
|
+
' "id": "my-plugin",',
|
|
3799
|
+
' "name": "我的插件",',
|
|
3800
|
+
' "version": "1.0.0",',
|
|
3801
|
+
' "type": "game-mechanics",',
|
|
3802
|
+
' "description": "插件描述",',
|
|
3803
|
+
' "author": "作者名",',
|
|
3804
|
+
' "code": "function setup(xinyu) { ... }",',
|
|
3805
|
+
' "configSchema": [...],',
|
|
3806
|
+
' "commonPermissions": [...],',
|
|
3807
|
+
' "exclusivePermissions": [...],',
|
|
3808
|
+
' "requiredPermissions": [...]',
|
|
3809
|
+
' }]',
|
|
3810
|
+
'}',
|
|
3811
|
+
].join('\n')}</pre>
|
|
3812
|
+
</div>
|
|
3813
|
+
);
|
|
3814
|
+
}
|
|
3815
|
+
|
|
3816
|
+
function SecuritySection() {
|
|
3817
|
+
return (
|
|
3818
|
+
<div>
|
|
3819
|
+
<p>插件系统采用多层安全架构,确保插件代码在受限的环境中运行,不会影响主应用的安全性和稳定性。</p>
|
|
3820
|
+
|
|
3821
|
+
<h3 id="sub-security-layers" className="text-sm font-bold mt-6 mb-2 scroll-mt-4" style={{ color: s.textPrimary }}>六层安全架构</h3>
|
|
3822
|
+
<div className="space-y-3 mt-2">
|
|
3823
|
+
{[
|
|
3824
|
+
{
|
|
3825
|
+
layer: '第 1 层:权限声明层',
|
|
3826
|
+
desc: '插件在 commonPermissions / exclusivePermissions / requiredPermissions 中声明权限,未声明的权限无法使用',
|
|
3827
|
+
detail: '在插件创建时由用户确认权限范围,运行时严格检查。排他权限冲突时后加载的插件启动失败'
|
|
3828
|
+
},
|
|
3829
|
+
{
|
|
3830
|
+
layer: '第 2 层:权限检查层',
|
|
3831
|
+
desc: '每次敏感操作前验证权限,无权限则静默失败',
|
|
3832
|
+
detail: '如 registerSlot 检查 slot:* 权限,injectStyle 检查 style:inject 权限'
|
|
3833
|
+
},
|
|
3834
|
+
{
|
|
3835
|
+
layer: '第 3 层:HTML 净化层',
|
|
3836
|
+
desc: '8 步净化处理,移除危险标签和属性',
|
|
3837
|
+
detail: '防止 XSS 攻击,确保注入的 HTML 安全'
|
|
3838
|
+
},
|
|
3839
|
+
{
|
|
3840
|
+
layer: '第 4 层:DOM 沙箱层',
|
|
3841
|
+
desc: '通过代理对象间接操作 DOM,限制操作范围',
|
|
3842
|
+
detail: '只能操作插件自己创建的元素,禁止访问宿主 DOM'
|
|
3843
|
+
},
|
|
3844
|
+
{
|
|
3845
|
+
layer: '第 5 层:资源追踪层',
|
|
3846
|
+
desc: '自动追踪插件创建的所有资源(DOM、样式、事件、定时器)',
|
|
3847
|
+
detail: '插件卸载时自动清理所有追踪的资源'
|
|
3848
|
+
},
|
|
3849
|
+
{
|
|
3850
|
+
layer: '第 6 层:错误隔离层',
|
|
3851
|
+
desc: 'try-catch 包裹所有插件代码,错误不会影响主应用',
|
|
3852
|
+
detail: '错误会被记录到控制台,并显示用户友好的错误提示'
|
|
3853
|
+
},
|
|
3854
|
+
].map((item) => (
|
|
3855
|
+
<div key={item.layer} className="p-3 rounded-lg" style={{ backgroundColor: s.bgSecondary, border: `1px solid ${s.border}` }}>
|
|
3856
|
+
<div className="text-xs font-bold mb-1" style={{ color: s.accent }}>{item.layer}</div>
|
|
3857
|
+
<p className="text-xs" style={{ color: s.textSecondary }}>{item.desc}</p>
|
|
3858
|
+
<p className="text-xs mt-1" style={{ color: s.textMuted }}>{item.detail}</p>
|
|
3859
|
+
</div>
|
|
3860
|
+
))}
|
|
3861
|
+
</div>
|
|
3862
|
+
|
|
3863
|
+
<h3 id="sub-html-sanitize" className="text-sm font-bold mt-6 mb-2 scroll-mt-4" style={{ color: s.textPrimary }}>HTML 净化规则</h3>
|
|
3864
|
+
<p>插件注入的 HTML 会经过 8 步净化处理:</p>
|
|
3865
|
+
<ol className="list-decimal pl-5 space-y-1">
|
|
3866
|
+
<li>移除 <code style={codeStyle}>script</code> / <code style={codeStyle}>style</code> / <code style={codeStyle}>link</code> / <code style={codeStyle}>meta</code> 标签</li>
|
|
3867
|
+
<li>移除 <code style={codeStyle}>iframe</code> / <code style={codeStyle}>object</code> / <code style={codeStyle}>embed</code> / <code style={codeStyle}>applet</code> 标签</li>
|
|
3868
|
+
<li>移除所有 <code style={codeStyle}>on*</code> 事件属性(onclick、onload 等)</li>
|
|
3869
|
+
<li>移除 <code style={codeStyle}>javascript:</code> 协议</li>
|
|
3870
|
+
<li>移除 <code style={codeStyle}>srcdoc</code> / <code style={codeStyle}>formaction</code> / <code style={codeStyle}>xlink:href</code> 属性</li>
|
|
3871
|
+
<li>保留 46 个安全标签</li>
|
|
3872
|
+
<li>清理 HTML 注释</li>
|
|
3873
|
+
<li>规范化 HTML 结构</li>
|
|
3874
|
+
</ol>
|
|
3875
|
+
|
|
3876
|
+
<h3 id="sub-allowed-tags" className="text-sm font-bold mt-6 mb-2 scroll-mt-4" style={{ color: s.textPrimary }}>允许的 HTML 标签完整列表</h3>
|
|
3877
|
+
<pre style={preStyle}>{CODE_SECURITY_TAGS}</pre>
|
|
3878
|
+
|
|
3879
|
+
<h3 id="sub-ai-scan" className="text-sm font-bold mt-6 mb-2 scroll-mt-4" style={{ color: s.textPrimary }}>AI 安全检测</h3>
|
|
3880
|
+
<p>在插件创建/编辑页面中,点击“AI 安全检测”按钮可以对插件代码进行安全分析。检测覆盖 6 个维度:</p>
|
|
3881
|
+
<div className="p-3 rounded-lg mt-2" style={{ backgroundColor: s.bgSecondary, border: `1px solid ${s.border}` }}>
|
|
3882
|
+
<div className="space-y-2">
|
|
3883
|
+
{[
|
|
3884
|
+
{ dim: '代码注入风险', desc: '检测 eval、new Function、innerHTML 等危险操作' },
|
|
3885
|
+
{ dim: '数据泄露风险', desc: '检测对外部 URL 的请求、localStorage 访问等' },
|
|
3886
|
+
{ dim: '权限滥用风险', desc: '检测是否声明了不必要的权限' },
|
|
3887
|
+
{ dim: 'DOM 操作风险', desc: '检测直接 DOM 操作、事件监听等' },
|
|
3888
|
+
{ dim: '资源消耗风险', desc: '检测可能的无限循环、大量定时器等' },
|
|
3889
|
+
{ dim: '内容安全风险', desc: '检测用户输入未过滤直接使用等' },
|
|
3890
|
+
].map((item) => (
|
|
3891
|
+
<div key={item.dim} className="flex items-start gap-2">
|
|
3892
|
+
<span className="text-xs font-bold shrink-0" style={{ color: s.accent }}>{item.dim}:</span>
|
|
3893
|
+
<span className="text-xs" style={{ color: s.textSecondary }}>{item.desc}</span>
|
|
3894
|
+
</div>
|
|
3895
|
+
))}
|
|
3896
|
+
</div>
|
|
3897
|
+
</div>
|
|
3898
|
+
<p className="mt-2">检测结果包含风险等级:<code style={codeStyle}>low</code>(低风险)、<code style={codeStyle}>medium</code>(中风险)、<code style={codeStyle}>high</code>(高风险)、<code style={codeStyle}>critical</code>(严重风险)。返回 <code style={codeStyle}>SecurityScanResult</code> 结构,包含每个维度的评分和总体建议。</p>
|
|
3899
|
+
</div>
|
|
3900
|
+
);
|
|
3901
|
+
}
|
|
3902
|
+
|
|
3903
|
+
// ==================== 原有章节(增强版) ====================
|
|
3904
|
+
|
|
3905
|
+
function BestPracticesSection() {
|
|
3906
|
+
return (
|
|
3907
|
+
<div>
|
|
3908
|
+
<h3 id="sub-bp-general" className="text-sm font-bold mb-2 scroll-mt-4" style={{ color: s.textPrimary }}>通用规范</h3>
|
|
3909
|
+
<ul className="list-disc pl-5 space-y-2">
|
|
3910
|
+
<li><strong>使用 <NavLink sectionId="css-variables">CSS 变量</NavLink></strong>:所有颜色必须使用 <code style={codeStyle}>var(--color-xxx)</code>,不要硬编码颜色值,确保主题切换时正常显示</li>
|
|
3911
|
+
<li><strong>错误隔离</strong>:运行时会自动捕获插件错误,不会影响其他插件或主应用。但建议在关键操作中自行 try-catch</li>
|
|
3912
|
+
<li><strong>资源清理</strong>:插件卸载时,运行时会自动清理 DOM 元素、样式标签、事件监听器和定时器,无需手动处理,详见 <NavLink sectionId="lifecycle">生命周期</NavLink></li>
|
|
3913
|
+
<li><strong>避免内存泄漏</strong>:不要在插件中创建无法被追踪的资源(如闭包中的定时器),使用 <code style={codeStyle}>xinyu.utils.debounce</code> 代替裸 setTimeout</li>
|
|
3914
|
+
<li><strong>使用 'use strict'</strong>:运行时自动为插件代码启用严格模式</li>
|
|
3915
|
+
</ul>
|
|
3916
|
+
|
|
3917
|
+
<h3 id="sub-bp-ui" className="text-sm font-bold mt-6 mb-2 scroll-mt-4" style={{ color: s.textPrimary }}>UI 开发规范</h3>
|
|
3918
|
+
<ul className="list-disc pl-5 space-y-2">
|
|
3919
|
+
<li><strong>优先使用插槽</strong>:尽量使用 <code style={codeStyle}>registerSlot</code> 而非自由 DOM 操作,更安全且易于管理</li>
|
|
3920
|
+
<li><strong>控制 UI 数量</strong>:不要注册过多按钮或面板(建议工具栏按钮不超过 6 个),避免界面拥挤</li>
|
|
3921
|
+
<li><strong>响应式设计</strong>:注入的 UI 应考虑不同屏幕尺寸,避免固定宽度</li>
|
|
3922
|
+
<li><strong>使用语义化 HTML</strong>:使用 div/span/button 等语义标签,不要使用 table 做布局</li>
|
|
3923
|
+
<li><strong>声明最小权限</strong>:只声明实际需要的权限,不要全部勾选。合理使用共享权限和排他权限区分功能</li>
|
|
3924
|
+
</ul>
|
|
3925
|
+
|
|
3926
|
+
<h3 id="sub-bp-ai" className="text-sm font-bold mt-6 mb-2 scroll-mt-4" style={{ color: s.textPrimary }}>AI 拦截规范</h3>
|
|
3927
|
+
<ul className="list-disc pl-5 space-y-2">
|
|
3928
|
+
<li><strong>保持 Prompt 简洁</strong>:通过 <code style={codeStyle}>onPromptBuild</code> 追加的内容应简洁明确,避免浪费 token</li>
|
|
3929
|
+
<li><strong>不要过度修改 AI 回复</strong>:<code style={codeStyle}>onAfterReceive</code> 应谨慎使用,避免破坏 AI 输出的质量</li>
|
|
3930
|
+
<li><strong>链式处理</strong>:多个插件的 <NavLink sectionId="ai-interception">AI 拦截器</NavLink>按注册顺序依次执行,前一个的输出是后一个的输入</li>
|
|
3931
|
+
</ul>
|
|
3932
|
+
|
|
3933
|
+
<h3 id="sub-bp-storage" className="text-sm font-bold mt-6 mb-2 scroll-mt-4" style={{ color: s.textPrimary }}>数据存储规范</h3>
|
|
3934
|
+
<ul className="list-disc pl-5 space-y-2">
|
|
3935
|
+
<li><strong>优先使用 <NavLink sectionId="plugin-storage">plugin.storage</NavLink></strong>:插件私有数据使用 <code style={codeStyle}>xinyu.plugin.storage</code>,自动命名空间隔离</li>
|
|
3936
|
+
<li><strong>游戏状态使用 setState</strong>:需要全局共享的游戏数据使用 <code style={codeStyle}>xinyu.game.setState</code></li>
|
|
3937
|
+
<li><strong>UI 展示属性使用 registerAttribute</strong>:需要在属性面板展示的数据使用 <code style={codeStyle}>xinyu.game.registerAttribute</code></li>
|
|
3938
|
+
<li><strong>注意容量限制</strong>:localStorage 有 5MB 限制,不要存储大量数据</li>
|
|
3939
|
+
</ul>
|
|
3940
|
+
|
|
3941
|
+
<h3 id="sub-bp-config" className="text-sm font-bold mt-6 mb-2 scroll-mt-4" style={{ color: s.textPrimary }}>插件配置规范</h3>
|
|
3942
|
+
<ul className="list-disc pl-5 space-y-2">
|
|
3943
|
+
<li><strong>定义 <NavLink sectionId="plugin-config">configSchema</NavLink></strong>:需要用户可配置的参数应通过 <code style={codeStyle}>configSchema</code> 定义</li>
|
|
3944
|
+
<li><strong>提供合理的默认值</strong>:每个配置字段都应有合理的 <code style={codeStyle}>defaultValue</code></li>
|
|
3945
|
+
<li><strong>清晰的配置说明</strong>:使用 <code style={codeStyle}>description</code> 字段说明配置的作用</li>
|
|
3946
|
+
</ul>
|
|
3947
|
+
|
|
3948
|
+
<h3 id="sub-bp-perf" className="text-sm font-bold mt-6 mb-2 scroll-mt-4" style={{ color: s.textPrimary }}>性能建议</h3>
|
|
3949
|
+
<ul className="list-disc pl-5 space-y-2">
|
|
3950
|
+
<li>避免在 <code style={codeStyle}>onAfterReceive</code> 等高频 Hook 中执行复杂计算</li>
|
|
3951
|
+
<li>使用 <code style={codeStyle}>debounce</code> / <code style={codeStyle}>throttle</code> 控制事件触发频率</li>
|
|
3952
|
+
<li>样式注入尽量一次性完成,避免频繁调用 <code style={codeStyle}>injectStyle</code></li>
|
|
3953
|
+
<li>DOM 操作批量执行,减少重排重绘</li>
|
|
3954
|
+
<li>跨插件调用应做好错误处理,使用 try-catch 包裹 <code style={codeStyle}>callPlugin</code>,并提供降级方案</li>
|
|
3955
|
+
</ul>
|
|
3956
|
+
|
|
3957
|
+
<h3 id="sub-bp-cross-plugin" className="text-sm font-bold mt-6 mb-2 scroll-mt-4" style={{ color: s.textPrimary }}>跨插件通信规范</h3>
|
|
3958
|
+
<ul className="list-disc pl-5 space-y-2">
|
|
3959
|
+
<li><strong>声明最小公开 API</strong>:只在 <code style={codeStyle}>publicExports</code> 中声明确实需要被其他插件调用的方法</li>
|
|
3960
|
+
<li><strong>提供清晰的 API 文档</strong>:每个公开方法都应有 <code style={codeStyle}>description</code>、<code style={codeStyle}>params</code>、<code style={codeStyle}>returns</code> 说明</li>
|
|
3961
|
+
<li><strong>始终处理错误</strong>:调用方必须 try-catch <code style={codeStyle}>callPlugin</code>,并提供降级方案</li>
|
|
3962
|
+
<li><strong>避免循环依赖</strong>:插件 A 依赖 B,B 又依赖 A 会导致加载失败</li>
|
|
3963
|
+
</ul>
|
|
3964
|
+
|
|
3965
|
+
<h3 id="sub-bp-dependency" className="text-sm font-bold mt-6 mb-2 scroll-mt-4" style={{ color: s.textPrimary }}>依赖管理规范</h3>
|
|
3966
|
+
<ul className="list-disc pl-5 space-y-2">
|
|
3967
|
+
<li><strong>使用 loadDependency</strong>:推荐在代码中使用 <code style={codeStyle}>loadDependency()</code> 加载依赖,而非直接使用 <code style={codeStyle}>callPlugin()</code></li>
|
|
3968
|
+
<li><strong>提供降级方案</strong>:依赖加载失败时应有合理的降级逻辑,不要让插件完全失效</li>
|
|
3969
|
+
<li><strong>声明版本范围</strong>:为依赖指定合理的版本范围,避免 API 变更导致兼容性问题</li>
|
|
3970
|
+
<li><strong>可选依赖谨慎使用</strong>:只在功能确实可选时标记为 <code style={codeStyle}>optional: true</code></li>
|
|
3971
|
+
</ul>
|
|
3972
|
+
</div>
|
|
3973
|
+
);
|
|
3974
|
+
}
|
|
3975
|
+
|
|
3976
|
+
function ExamplesSection() {
|
|
3977
|
+
return (
|
|
3978
|
+
<div>
|
|
3979
|
+
<h3 id="sub-ex-1" className="text-sm font-bold mb-2 scroll-mt-4" style={{ color: s.textPrimary }}>示例 1:自定义状态栏插件</h3>
|
|
3980
|
+
<pre style={preStyle}>{CODE_EXAMPLE_1}</pre>
|
|
3981
|
+
|
|
3982
|
+
<h3 id="sub-ex-2" className="text-sm font-bold mt-6 mb-2 scroll-mt-4" style={{ color: s.textPrimary }}>示例 2:消息统计面板</h3>
|
|
3983
|
+
<pre style={preStyle}>{CODE_EXAMPLE_2}</pre>
|
|
3984
|
+
|
|
3985
|
+
<h3 id="sub-ex-3" className="text-sm font-bold mt-6 mb-2 scroll-mt-4" style={{ color: s.textPrimary }}>示例 3:浮动操作面板</h3>
|
|
3986
|
+
<pre style={preStyle}>{CODE_EXAMPLE_3}</pre>
|
|
3987
|
+
|
|
3988
|
+
<h3 id="sub-ex-4" className="text-sm font-bold mt-6 mb-2 scroll-mt-4" style={{ color: s.textPrimary }}>示例 4:带配置的骰子插件</h3>
|
|
3989
|
+
<p>结合了 <NavLink sectionId="plugin-config">插件配置系统</NavLink>和<NavLink sectionId="plugin-storage">插件数据存储</NavLink>的完整示例:</p>
|
|
3990
|
+
<pre style={preStyle}>{CODE_CONFIG_FULL}</pre>
|
|
3991
|
+
|
|
3992
|
+
<h3 id="sub-ex-5" className="text-sm font-bold mt-6 mb-2 scroll-mt-4" style={{ color: s.textPrimary }}>示例 5:NPC 人格注入插件</h3>
|
|
3993
|
+
<p>展示了 <NavLink sectionId="ai-interception">AI 拦截系统</NavLink>的实际应用:</p>
|
|
3994
|
+
<pre style={preStyle}>{CODE_AI_NPC}</pre>
|
|
3995
|
+
</div>
|
|
3996
|
+
);
|
|
3997
|
+
}
|
|
3998
|
+
|
|
3999
|
+
// ==================== 跨插件通信 ====================
|
|
4000
|
+
|
|
4001
|
+
function CrossPluginSection() {
|
|
4002
|
+
return (
|
|
4003
|
+
<div>
|
|
4004
|
+
<h3 id="sub-cp-overview" className="text-sm font-bold mb-2 scroll-mt-4" style={{ color: s.textPrimary }}>概述</h3>
|
|
4005
|
+
<p className="text-xs leading-relaxed" style={{ color: s.textSecondary }}>
|
|
4006
|
+
插件系统支持插件之间的直接通信,采用<strong>“显式导出 + 按需调用”</strong>模型:
|
|
4007
|
+
</p>
|
|
4008
|
+
<ul className="list-disc pl-5 mt-2 space-y-1">
|
|
4009
|
+
<li className="text-xs" style={{ color: s.textSecondary }}><strong>被调用方</strong>:在 manifest 中声明 <code style={codeStyle}>publicExports</code>,在 setup() 的返回值中导出函数</li>
|
|
4010
|
+
<li className="text-xs" style={{ color: s.textSecondary }}><strong>调用方</strong>:在 manifest 中声明 <NavLink sectionId="dependency">dependencies</NavLink>,通过 <code style={codeStyle}>xinyu.plugin.callPlugin()</code> 调用</li>
|
|
4011
|
+
<li className="text-xs" style={{ color: s.textSecondary }}>双方都必须声明,确保通信关系透明、可审计</li>
|
|
4012
|
+
</ul>
|
|
4013
|
+
|
|
4014
|
+
<h3 id="sub-cp-export" className="text-sm font-bold mt-6 mb-2 scroll-mt-4" style={{ color: s.textPrimary }}>被调用方:导出 API</h3>
|
|
4015
|
+
<p className="text-xs leading-relaxed mb-2" style={{ color: s.textSecondary }}>第一步:在 manifest 中声明公开 API</p>
|
|
4016
|
+
<pre className="p-3 rounded-lg text-xs overflow-x-auto" style={preStyle}>
|
|
4017
|
+
{`// manifest 中的 publicExports 字段
|
|
4018
|
+
{
|
|
4019
|
+
"publicExports": {
|
|
4020
|
+
"rollDice": {
|
|
4021
|
+
"description": "投掷骰子",
|
|
4022
|
+
"params": "sides: number - 骰子面数",
|
|
4023
|
+
"returns": "number - 投掷结果"
|
|
4024
|
+
},
|
|
4025
|
+
"getCharacter": {
|
|
4026
|
+
"description": "获取角色信息",
|
|
4027
|
+
"returns": "object - 角色数据"
|
|
4028
|
+
}
|
|
4029
|
+
}
|
|
4030
|
+
}`}
|
|
4031
|
+
</pre>
|
|
4032
|
+
<p className="text-xs leading-relaxed mt-3 mb-2" style={{ color: s.textSecondary }}>第二步:在 setup() 中导出函数</p>
|
|
4033
|
+
<pre className="p-3 rounded-lg text-xs overflow-x-auto" style={preStyle}>
|
|
4034
|
+
{`function setup(xinyu) {
|
|
4035
|
+
// ... 注册钩子、属性等
|
|
4036
|
+
|
|
4037
|
+
return {
|
|
4038
|
+
rollDice: function(sides) {
|
|
4039
|
+
return Math.floor(Math.random() * sides) + 1;
|
|
4040
|
+
},
|
|
4041
|
+
getCharacter: function() {
|
|
4042
|
+
return xinyu.game.getState().character || {};
|
|
4043
|
+
}
|
|
4044
|
+
};
|
|
4045
|
+
}`}
|
|
4046
|
+
</pre>
|
|
4047
|
+
|
|
4048
|
+
<h3 id="sub-cp-call" className="text-sm font-bold mt-6 mb-2 scroll-mt-4" style={{ color: s.textPrimary }}>调用方:使用 API</h3>
|
|
4049
|
+
<p className="text-xs leading-relaxed mb-2" style={{ color: s.textSecondary }}>第一步:在 manifest 中声明依赖</p>
|
|
4050
|
+
<pre className="p-3 rounded-lg text-xs overflow-x-auto" style={preStyle}>
|
|
4051
|
+
{`// manifest 中的 dependencies 字段
|
|
4052
|
+
{
|
|
4053
|
+
"dependencies": [
|
|
4054
|
+
{ "pluginId": "com.example.dice-service", "versionRange": "^1.0.0" }
|
|
4055
|
+
]
|
|
4056
|
+
}`}
|
|
4057
|
+
</pre>
|
|
4058
|
+
<p className="text-xs leading-relaxed mt-3 mb-2" style={{ color: s.textSecondary }}>第二步:在代码中调用</p>
|
|
4059
|
+
<pre className="p-3 rounded-lg text-xs overflow-x-auto" style={preStyle}>
|
|
4060
|
+
{`function setup(xinyu) {
|
|
4061
|
+
xinyu.plugin.on('onGameInit', async function() {
|
|
4062
|
+
try {
|
|
4063
|
+
var result = await xinyu.plugin.callPlugin(
|
|
4064
|
+
'com.example.dice-service',
|
|
4065
|
+
'rollDice',
|
|
4066
|
+
20
|
|
4067
|
+
);
|
|
4068
|
+
xinyu.chat.insertSystemMessage('🎲 投掷 20 面骰子:' + result);
|
|
4069
|
+
} catch (e) {
|
|
4070
|
+
xinyu.chat.insertSystemMessage('骰子服务不可用');
|
|
4071
|
+
}
|
|
4072
|
+
});
|
|
4073
|
+
}`}
|
|
4074
|
+
</pre>
|
|
4075
|
+
|
|
4076
|
+
<h3 id="sub-cp-security" className="text-sm font-bold mt-6 mb-2 scroll-mt-4" style={{ color: s.textPrimary }}>安全机制</h3>
|
|
4077
|
+
<ul className="list-disc pl-5 space-y-1">
|
|
4078
|
+
<li className="text-xs" style={{ color: s.textSecondary }}>调用方必须在 <code style={codeStyle}>dependencies</code> 中声明目标插件,否则调用被拒绝</li>
|
|
4079
|
+
<li className="text-xs" style={{ color: s.textSecondary }}>被调用方必须在 <code style={codeStyle}>publicExports</code> 中声明方法,否则调用被拒绝</li>
|
|
4080
|
+
<li className="text-xs" style={{ color: s.textSecondary }}>目标插件必须已安装并启用</li>
|
|
4081
|
+
<li className="text-xs" style={{ color: s.textSecondary }}>所有跨插件调用都有错误隔离,不会影响其他插件</li>
|
|
4082
|
+
</ul>
|
|
4083
|
+
</div>
|
|
4084
|
+
);
|
|
4085
|
+
}
|
|
4086
|
+
|
|
4087
|
+
// ==================== 依赖管理 ====================
|
|
4088
|
+
|
|
4089
|
+
function DependencySection() {
|
|
4090
|
+
return (
|
|
4091
|
+
<div>
|
|
4092
|
+
<h3 id="sub-dep-overview" className="text-sm font-bold mb-2 scroll-mt-4" style={{ color: s.textPrimary }}>概述</h3>
|
|
4093
|
+
<p className="text-xs leading-relaxed" style={{ color: s.textSecondary }}>
|
|
4094
|
+
插件可以声明对其他插件的依赖。依赖管理采用混合方案:
|
|
4095
|
+
</p>
|
|
4096
|
+
<ul className="list-disc pl-5 mt-2 space-y-1">
|
|
4097
|
+
<li className="text-xs" style={{ color: s.textSecondary }}><strong>代码驱动</strong>:在代码中使用 <code style={codeStyle}>xinyu.plugin.loadDependency()</code> 加载依赖,支持运行时降级</li>
|
|
4098
|
+
<li className="text-xs" style={{ color: s.textSecondary }}><strong>属性记录</strong>:manifest 中的 <code style={codeStyle}>dependencies</code> 字段用于 UI 展示、加载时检查和分享</li>
|
|
4099
|
+
</ul>
|
|
4100
|
+
|
|
4101
|
+
<h3 id="sub-dep-manifest" className="text-sm font-bold mt-6 mb-2 scroll-mt-4" style={{ color: s.textPrimary }}>manifest 声明</h3>
|
|
4102
|
+
<pre className="p-3 rounded-lg text-xs overflow-x-auto" style={preStyle}>
|
|
4103
|
+
{`// manifest 中的 dependencies 字段
|
|
4104
|
+
{
|
|
4105
|
+
"dependencies": [
|
|
4106
|
+
{
|
|
4107
|
+
"pluginId": "com.example.dice-service",
|
|
4108
|
+
"versionRange": "^1.0.0",
|
|
4109
|
+
"optional": false
|
|
4110
|
+
},
|
|
4111
|
+
{
|
|
4112
|
+
"pluginId": "com.example.optional-feature",
|
|
4113
|
+
"optional": true
|
|
4114
|
+
}
|
|
4115
|
+
]
|
|
4116
|
+
}`}
|
|
4117
|
+
</pre>
|
|
4118
|
+
<table className="w-full mt-3 text-xs">
|
|
4119
|
+
<thead><tr style={{ borderBottom: `1px solid ${s.border}` }}>
|
|
4120
|
+
<th className="py-1.5 text-left font-medium" style={{ color: s.textMuted }}>字段</th>
|
|
4121
|
+
<th className="py-1.5 text-left font-medium" style={{ color: s.textMuted }}>类型</th>
|
|
4122
|
+
<th className="py-1.5 text-left font-medium" style={{ color: s.textMuted }}>说明</th>
|
|
4123
|
+
</tr></thead>
|
|
4124
|
+
<tbody>
|
|
4125
|
+
<tr style={{ borderBottom: `1px solid ${s.border}` }}>
|
|
4126
|
+
<td className="py-1.5"><code style={codeStyle}>pluginId</code></td>
|
|
4127
|
+
<td className="py-1.5" style={{ color: s.textSecondary }}>string(必填)</td>
|
|
4128
|
+
<td className="py-1.5" style={{ color: s.textSecondary }}>依赖的插件 ID</td>
|
|
4129
|
+
</tr>
|
|
4130
|
+
<tr style={{ borderBottom: `1px solid ${s.border}` }}>
|
|
4131
|
+
<td className="py-1.5"><code style={codeStyle}>versionRange</code></td>
|
|
4132
|
+
<td className="py-1.5" style={{ color: s.textSecondary }}>string(可选)</td>
|
|
4133
|
+
<td className="py-1.5" style={{ color: s.textSecondary }}>版本范围约束,如 ^1.0.0</td>
|
|
4134
|
+
</tr>
|
|
4135
|
+
<tr style={{ borderBottom: `1px solid ${s.border}` }}>
|
|
4136
|
+
<td className="py-1.5"><code style={codeStyle}>optional</code></td>
|
|
4137
|
+
<td className="py-1.5" style={{ color: s.textSecondary }}>boolean(可选)</td>
|
|
4138
|
+
<td className="py-1.5" style={{ color: s.textSecondary }}>是否为可选依赖,默认 false</td>
|
|
4139
|
+
</tr>
|
|
4140
|
+
</tbody>
|
|
4141
|
+
</table>
|
|
4142
|
+
|
|
4143
|
+
<h3 id="sub-dep-version" className="text-sm font-bold mt-6 mb-2 scroll-mt-4" style={{ color: s.textPrimary }}>版本范围语法</h3>
|
|
4144
|
+
<table className="w-full text-xs">
|
|
4145
|
+
<thead><tr style={{ borderBottom: `1px solid ${s.border}` }}>
|
|
4146
|
+
<th className="py-1.5 text-left font-medium" style={{ color: s.textMuted }}>语法</th>
|
|
4147
|
+
<th className="py-1.5 text-left font-medium" style={{ color: s.textMuted }}>含义</th>
|
|
4148
|
+
<th className="py-1.5 text-left font-medium" style={{ color: s.textMuted }}>示例</th>
|
|
4149
|
+
</tr></thead>
|
|
4150
|
+
<tbody>
|
|
4151
|
+
<tr style={{ borderBottom: `1px solid ${s.border}` }}>
|
|
4152
|
+
<td className="py-1.5">精确匹配</td>
|
|
4153
|
+
<td className="py-1.5" style={{ color: s.textSecondary }}>版本必须完全相同</td>
|
|
4154
|
+
<td className="py-1.5"><code style={codeStyle}>1.0.0</code></td>
|
|
4155
|
+
</tr>
|
|
4156
|
+
<tr style={{ borderBottom: `1px solid ${s.border}` }}>
|
|
4157
|
+
<td className="py-1.5"><code style={codeStyle}>^</code></td>
|
|
4158
|
+
<td className="py-1.5" style={{ color: s.textSecondary }}>主版本号相同,次版本和补丁 >= 指定版本</td>
|
|
4159
|
+
<td className="py-1.5"><code style={codeStyle}>^1.2.0</code> 匹配 1.2.0 ~ 1.99.99</td>
|
|
4160
|
+
</tr>
|
|
4161
|
+
<tr style={{ borderBottom: `1px solid ${s.border}` }}>
|
|
4162
|
+
<td className="py-1.5"><code style={codeStyle}>~</code></td>
|
|
4163
|
+
<td className="py-1.5" style={{ color: s.textSecondary }}>主版本和次版本相同,补丁 >= 指定版本</td>
|
|
4164
|
+
<td className="py-1.5"><code style={codeStyle}>~1.2.0</code> 匹配 1.2.0 ~ 1.2.99</td>
|
|
4165
|
+
</tr>
|
|
4166
|
+
<tr style={{ borderBottom: `1px solid ${s.border}` }}>
|
|
4167
|
+
<td className="py-1.5"><code style={codeStyle}>>=</code></td>
|
|
4168
|
+
<td className="py-1.5" style={{ color: s.textSecondary }}>大于等于指定版本</td>
|
|
4169
|
+
<td className="py-1.5"><code style={codeStyle}>>=1.0.0</code></td>
|
|
4170
|
+
</tr>
|
|
4171
|
+
</tbody>
|
|
4172
|
+
</table>
|
|
4173
|
+
|
|
4174
|
+
<h3 id="sub-dep-runtime" className="text-sm font-bold mt-6 mb-2 scroll-mt-4" style={{ color: s.textPrimary }}>运行时加载(推荐)</h3>
|
|
4175
|
+
<p className="text-xs leading-relaxed mb-2" style={{ color: s.textSecondary }}>
|
|
4176
|
+
使用 <code style={codeStyle}>xinyu.plugin.loadDependency()</code> 在运行时加载依赖,失败时可自行降级处理:
|
|
4177
|
+
</p>
|
|
4178
|
+
<pre className="p-3 rounded-lg text-xs overflow-x-auto" style={preStyle}>
|
|
4179
|
+
{`function setup(xinyu) {
|
|
4180
|
+
xinyu.plugin.on('onGameInit', async function() {
|
|
4181
|
+
// 尝试加载依赖,失败时降级
|
|
4182
|
+
var diceExports = await xinyu.plugin.loadDependency(
|
|
4183
|
+
'com.example.dice-service',
|
|
4184
|
+
{ version: '^1.0.0' }
|
|
4185
|
+
);
|
|
4186
|
+
|
|
4187
|
+
if (diceExports && diceExports.rollDice) {
|
|
4188
|
+
var result = diceExports.rollDice(20);
|
|
4189
|
+
xinyu.chat.insertSystemMessage('🎲 投掷结果:' + result);
|
|
4190
|
+
} else {
|
|
4191
|
+
// 降级方案:使用内置简单骰子
|
|
4192
|
+
var result = Math.floor(Math.random() * 20) + 1;
|
|
4193
|
+
xinyu.chat.insertSystemMessage('🎲 投掷结果(简易):' + result);
|
|
4194
|
+
}
|
|
4195
|
+
});
|
|
4196
|
+
}`}
|
|
4197
|
+
</pre>
|
|
4198
|
+
<div className="mt-3 p-3 rounded-lg" style={{ backgroundColor: s.bgTertiary, border: `1px solid ${s.border}` }}>
|
|
4199
|
+
<p className="text-xs" style={{ color: s.textSecondary }}>
|
|
4200
|
+
<strong>加载时依赖检查</strong>:插件加载时,系统会自动检查 manifest 中声明的依赖。必需依赖缺失时插件不会调用 setup();可选依赖缺失则正常加载但记录警告。
|
|
4201
|
+
</p>
|
|
4202
|
+
</div>
|
|
4203
|
+
|
|
4204
|
+
<h3 id="sub-dep-editor" className="text-sm font-bold mt-6 mb-2 scroll-mt-4" style={{ color: s.textPrimary }}>编辑器中的依赖管理</h3>
|
|
4205
|
+
<p className="text-xs leading-relaxed" style={{ color: s.textSecondary }}>
|
|
4206
|
+
在插件编辑器(<NavLink sectionId="overview">创建</NavLink> / <NavLink sectionId="overview">编辑</NavLink>页面)的中列,提供可视化的依赖管理界面:
|
|
4207
|
+
</p>
|
|
4208
|
+
<ul className="list-disc pl-5 mt-2 space-y-1">
|
|
4209
|
+
<li className="text-xs" style={{ color: s.textSecondary }}><strong>从代码扫描</strong>:自动检测代码中的 <code style={codeStyle}>loadDependency</code>、<code style={codeStyle}>callPlugin</code> 等调用,提取依赖</li>
|
|
4210
|
+
<li className="text-xs" style={{ color: s.textSecondary }}><strong>手动添加</strong>:输入插件 ID 和版本范围,手动添加依赖</li>
|
|
4211
|
+
<li className="text-xs" style={{ color: s.textSecondary }}><strong>状态可视化</strong>:实时显示每个依赖的安装状态(✅ 已安装 / ❌ 未安装 / ⚠️ 版本不匹配)</li>
|
|
4212
|
+
<li className="text-xs" style={{ color: s.textSecondary }}><strong>删除</strong>:移除不需要的依赖项</li>
|
|
4213
|
+
</ul>
|
|
4214
|
+
</div>
|
|
4215
|
+
);
|
|
4216
|
+
}
|
|
4217
|
+
|
|
4218
|
+
function DebugSection() {
|
|
4219
|
+
return (
|
|
4220
|
+
<div>
|
|
4221
|
+
<h3 id="sub-debug-tools" className="text-sm font-bold mb-2 scroll-mt-4" style={{ color: s.textPrimary }}>使用浏览器开发者工具</h3>
|
|
4222
|
+
<p>插件代码运行在浏览器中,可以直接使用 <code style={codeStyle}>console.log</code> 进行调试:</p>
|
|
4223
|
+
<pre style={preStyle}>{CODE_DEBUG_EXAMPLE}</pre>
|
|
4224
|
+
|
|
4225
|
+
<h3 id="sub-debug-faq" className="text-sm font-bold mt-6 mb-2 scroll-mt-4" style={{ color: s.textPrimary }}>常见问题排查</h3>
|
|
4226
|
+
<table className="w-full text-xs mt-2" style={{ borderCollapse: 'collapse' }}>
|
|
4227
|
+
<thead>
|
|
4228
|
+
<tr style={{ borderBottom: `1px solid ${s.border}` }}>
|
|
4229
|
+
<th className="text-left py-2 pr-4" style={{ color: s.textPrimary }}>问题</th>
|
|
4230
|
+
<th className="text-left py-2" style={{ color: s.textPrimary }}>可能原因</th>
|
|
4231
|
+
</tr>
|
|
4232
|
+
</thead>
|
|
4233
|
+
<tbody>
|
|
4234
|
+
{[
|
|
4235
|
+
['API 调用无效果', '未声明对应权限(commonPermissions / exclusivePermissions),或插件未启用'],
|
|
4236
|
+
['registerSlot 内容不显示', '对应的插槽容器不存在于当前页面,或权限不足'],
|
|
4237
|
+
['插件加载报错', '代码语法错误,检查浏览器控制台的错误信息'],
|
|
4238
|
+
['UI 样式异常', '使用了硬编码颜色而非 <NavLink sectionId="css-variables">CSS 变量</NavLink>,或被其他插件样式覆盖'],
|
|
4239
|
+
['模态框不显示', '缺少 modal:custom 权限,或 showModal 返回了 null'],
|
|
4240
|
+
['DOM 操作报错', '缺少 dom:free 权限,或尝试操作不属于自己的元素'],
|
|
4241
|
+
['插件卸载后残留', '检查控制台是否有清理错误,资源追踪器会自动清理'],
|
|
4242
|
+
['AI 拦截不生效', 'handler 未正确返回值,或插件未启用,详见 <NavLink sectionId="ai-interception">AI 拦截系统</NavLink>'],
|
|
4243
|
+
['配置读取为 undefined', 'configSchema 中未定义该字段,或用户未设置且 defaultValue 为 undefined'],
|
|
4244
|
+
['存储数据丢失', 'localStorage 被清除,或浏览器隐私模式下使用'],
|
|
4245
|
+
].map(([problem, cause]) => (
|
|
4246
|
+
<tr key={problem} style={{ borderBottom: `1px solid ${s.border}` }}>
|
|
4247
|
+
<td className="py-2 pr-4" style={{ color: s.textPrimary }}>{problem}</td>
|
|
4248
|
+
<td className="py-2">{cause}</td>
|
|
4249
|
+
</tr>
|
|
4250
|
+
))}
|
|
4251
|
+
</tbody>
|
|
4252
|
+
</table>
|
|
4253
|
+
|
|
4254
|
+
<h3 id="sub-debug-scan" className="text-sm font-bold mt-6 mb-2 scroll-mt-4" style={{ color: s.textPrimary }}>安全检测</h3>
|
|
4255
|
+
<p>在插件创建/编辑页面中,点击“AI 安全检测”按钮可以对插件代码进行安全分析,检测潜在的风险操作,详见 <NavLink sectionId="security">安全机制详解</NavLink>。</p>
|
|
4256
|
+
</div>
|
|
4257
|
+
);
|
|
4258
|
+
}
|