@aalis/plugin-webui-server 0.4.1 → 0.5.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/dist/auth.d.ts +6 -22
- package/dist/auth.d.ts.map +1 -1
- package/dist/auth.js +29 -140
- package/dist/auth.js.map +1 -1
- package/dist/client-discovery.d.ts +32 -0
- package/dist/client-discovery.d.ts.map +1 -0
- package/dist/client-discovery.js +67 -0
- package/dist/client-discovery.js.map +1 -0
- package/dist/client-switch-page.d.ts +3 -0
- package/dist/client-switch-page.d.ts.map +1 -0
- package/dist/client-switch-page.js +106 -0
- package/dist/client-switch-page.js.map +1 -0
- package/dist/gate.d.ts +9 -15
- package/dist/gate.d.ts.map +1 -1
- package/dist/gate.js +11 -29
- package/dist/gate.js.map +1 -1
- package/dist/index.d.ts +0 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +179 -228
- package/dist/index.js.map +1 -1
- package/dist/routes/files.js +5 -5
- package/dist/routes/files.js.map +1 -1
- package/dist/routes/marketplace.d.ts +45 -18
- package/dist/routes/marketplace.d.ts.map +1 -1
- package/dist/routes/marketplace.js +141 -84
- package/dist/routes/marketplace.js.map +1 -1
- package/dist/routes/plugins.d.ts.map +1 -1
- package/dist/routes/plugins.js +27 -48
- package/dist/routes/plugins.js.map +1 -1
- package/dist/routes/proxy.d.ts.map +1 -1
- package/dist/routes/proxy.js +8 -25
- package/dist/routes/proxy.js.map +1 -1
- package/dist/routes/uploaded-files.js +3 -3
- package/dist/routes/uploaded-files.js.map +1 -1
- package/package.json +29 -27
package/dist/index.js
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
// node:fs/node:path 仅用于发现前端 dist 静态目录(位于工作区外部),
|
|
2
2
|
// 已在 biome.json noRestrictedImports 中作为基础设施例外列出。
|
|
3
3
|
import { Buffer } from 'node:buffer';
|
|
4
|
-
import { existsSync, readFileSync } from 'node:fs';
|
|
5
|
-
import { readFile } from 'node:fs/promises';
|
|
4
|
+
import { existsSync, readdirSync, readFileSync } from 'node:fs';
|
|
6
5
|
import { createServer } from 'node:http';
|
|
7
6
|
import { createRequire } from 'node:module';
|
|
8
7
|
import { dirname, resolve } from 'node:path';
|
|
9
8
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
10
9
|
import { LogHub, parseLogLine } from '@aalis/core';
|
|
10
|
+
import { listLLMModels } from '@aalis/plugin-llm-api';
|
|
11
11
|
import { aggregatePlatformDetails, getPlatformAdapters, getPlatformNames, } from '@aalis/plugin-platform-api';
|
|
12
12
|
import { createProcessGateway } from '@aalis/plugin-process-api';
|
|
13
13
|
import { createStorageGateway } from '@aalis/plugin-storage-api';
|
|
@@ -15,6 +15,8 @@ import { DEFAULT_SUBSYSTEM_METADATA } from '@aalis/plugin-webui-api';
|
|
|
15
15
|
import express from 'express';
|
|
16
16
|
import { WebSocket, WebSocketServer } from 'ws';
|
|
17
17
|
import { createAuthSystem, openBrowser } from './auth.js';
|
|
18
|
+
import { collectLocalPackageDeps, discoverClients } from './client-discovery.js';
|
|
19
|
+
import { renderClientSwitchPage } from './client-switch-page.js';
|
|
18
20
|
import { createRouteGate } from './gate.js';
|
|
19
21
|
import { registerFileRoutes } from './routes/files.js';
|
|
20
22
|
import { registerMarketplaceRoutes } from './routes/marketplace.js';
|
|
@@ -64,9 +66,8 @@ export const configSchema = {
|
|
|
64
66
|
{ label: '每次启动随机生成(重启即轮换)', value: 'ephemeral' },
|
|
65
67
|
{ label: '首次生成后持久化(重启不掉登录)', value: 'persist' },
|
|
66
68
|
{ label: '使用下方固定 token', value: 'fixed' },
|
|
67
|
-
{ label: '禁用 token 登录(仅账户密码;无账户时 token 兜底生效)', value: 'disabled' },
|
|
68
69
|
],
|
|
69
|
-
description: 'ephemeral
|
|
70
|
+
description: 'ephemeral=每次启动随机;persist=token 写入 data/.webui-token,读取复用;fixed=使用 fixedToken 字段。所有模式都会写出便利文件 data/webui-access.txt 含访问 URL。',
|
|
70
71
|
},
|
|
71
72
|
fixedToken: {
|
|
72
73
|
type: 'string',
|
|
@@ -86,12 +87,8 @@ export const configSchema = {
|
|
|
86
87
|
default: 'https://registry.npmjs.org',
|
|
87
88
|
description: '插件市场检索用的 npm registry 基址。注意 npm 的 search API 并非所有镜像都支持(淘宝等国内源不支持),默认官方源;国内可填支持 search 的镜像或代理。安装走 package-manager(遵循本机 npm 配置)。',
|
|
88
89
|
},
|
|
89
|
-
client
|
|
90
|
-
|
|
91
|
-
label: '活跃前端(多前端时选用)',
|
|
92
|
-
default: '',
|
|
93
|
-
description: '装了多个前端(aalis.client 包)时指定哪个为活跃前端,填包名(如 @aalis/plugin-webui-client)。留空 = 自动选第一个发现到的(默认前端优先)。插件主动 provide(webui-client) 仍优先于此。',
|
|
94
|
-
},
|
|
90
|
+
// 活跃前端不在此配置:前端即 `webui-client` 服务的多 provider,活跃者由「服务偏好」
|
|
91
|
+
// (servicePreferences['webui-client'])决定——在 WebUI「服务」页的下拉框切换。
|
|
95
92
|
};
|
|
96
93
|
export const defaultConfig = {
|
|
97
94
|
port: 3000,
|
|
@@ -102,20 +99,27 @@ export const defaultConfig = {
|
|
|
102
99
|
fixedToken: '',
|
|
103
100
|
relationGraphDefaultSpacing: 120,
|
|
104
101
|
marketplaceRegistry: 'https://registry.npmjs.org',
|
|
105
|
-
client: '',
|
|
106
102
|
};
|
|
107
103
|
// ===== WebSocket 消息协议 =====
|
|
108
104
|
// 入站消息类型 + 校验 schema 见 ./protocol.ts(zod 强校验)
|
|
109
105
|
import { WSIncomingSchema } from './protocol.js';
|
|
110
106
|
export { WSIncomingSchema } from './protocol.js';
|
|
111
|
-
// ===== 日志文件读取(与
|
|
107
|
+
// ===== 日志文件读取(与 @aalis/runtime 的 file-logger 格式对偶)=====
|
|
112
108
|
// 行格式契约(format ↔ parse)由 @aalis/core 的 parseLogLine 唯一持有,此处只负责
|
|
113
|
-
//
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
109
|
+
// 读取「持久化日志单一数据源」并按 cursor 尾读分页。
|
|
110
|
+
//
|
|
111
|
+
// 路径(宿主目录布局)是「环境知识」,归 storage 的 logs 根所有;本插件只用 storage
|
|
112
|
+
// URI 寻址、对 cwd/落盘位置无知。写入方(runtime/file-logger)跑在 storage 起来之前,
|
|
113
|
+
// 是 bootstrap 期的合法 raw-fs 例外,不在此处管辖(默认 logs 根 = data/,与之对偶)。
|
|
114
|
+
const LOG_FILE_URI = 'logs:/latest.log';
|
|
115
|
+
async function readAllLogEntries(storage) {
|
|
116
|
+
let raw;
|
|
117
|
+
try {
|
|
118
|
+
raw = (await storage.readFile(LOG_FILE_URI, 'utf-8'));
|
|
119
|
+
}
|
|
120
|
+
catch {
|
|
121
|
+
return []; // 文件未就绪 / storage 暂不可用 → 无历史
|
|
122
|
+
}
|
|
119
123
|
const out = [];
|
|
120
124
|
for (const line of raw.split('\n')) {
|
|
121
125
|
if (!line)
|
|
@@ -126,12 +130,12 @@ async function readAllLogEntries() {
|
|
|
126
130
|
}
|
|
127
131
|
return out;
|
|
128
132
|
}
|
|
129
|
-
async function readLogFileTail(limit) {
|
|
130
|
-
const all = await readAllLogEntries();
|
|
133
|
+
async function readLogFileTail(storage, limit) {
|
|
134
|
+
const all = await readAllLogEntries(storage);
|
|
131
135
|
return all.slice(-limit);
|
|
132
136
|
}
|
|
133
|
-
async function readLogFileBefore(beforeSeq, limit) {
|
|
134
|
-
const all = await readAllLogEntries();
|
|
137
|
+
async function readLogFileBefore(storage, beforeSeq, limit) {
|
|
138
|
+
const all = await readAllLogEntries(storage);
|
|
135
139
|
const filtered = all.filter(e => e.seq < beforeSeq);
|
|
136
140
|
return filtered.slice(-limit);
|
|
137
141
|
}
|
|
@@ -142,7 +146,7 @@ export async function apply(ctx, config) {
|
|
|
142
146
|
host: config.host ?? '127.0.0.1',
|
|
143
147
|
fileRoot: config.fileRoot || 'workspace',
|
|
144
148
|
autoOpen: config.autoOpen ?? true,
|
|
145
|
-
tokenMode: ['ephemeral', 'fixed'
|
|
149
|
+
tokenMode: ['ephemeral', 'fixed'].includes(config.tokenMode)
|
|
146
150
|
? config.tokenMode
|
|
147
151
|
: 'persist',
|
|
148
152
|
fixedToken: config.fixedToken ?? '',
|
|
@@ -151,7 +155,6 @@ export async function apply(ctx, config) {
|
|
|
151
155
|
return Number.isFinite(v) && v > 0 ? v : 120;
|
|
152
156
|
})(),
|
|
153
157
|
marketplaceRegistry: config.marketplaceRegistry?.trim() || 'https://registry.npmjs.org',
|
|
154
|
-
client: config.client?.trim() ?? '',
|
|
155
158
|
};
|
|
156
159
|
// 创建 storage gateway;所有文件读写(token、access、文件管理)都走这里
|
|
157
160
|
const storage = createStorageGateway(ctx);
|
|
@@ -166,8 +169,7 @@ export async function apply(ctx, config) {
|
|
|
166
169
|
if (uiConfig.tokenMode === 'fixed' && uiConfig.fixedToken.trim()) {
|
|
167
170
|
return uiConfig.fixedToken.trim();
|
|
168
171
|
}
|
|
169
|
-
|
|
170
|
-
if (uiConfig.tokenMode === 'persist' || uiConfig.tokenMode === 'fixed' || uiConfig.tokenMode === 'disabled') {
|
|
172
|
+
if (uiConfig.tokenMode === 'persist' || uiConfig.tokenMode === 'fixed') {
|
|
171
173
|
try {
|
|
172
174
|
const raw = await storage.readFile(tokenFileUri, 'utf-8');
|
|
173
175
|
const existing = (typeof raw === 'string' ? raw : raw.toString('utf-8')).trim();
|
|
@@ -209,28 +211,13 @@ export async function apply(ctx, config) {
|
|
|
209
211
|
}
|
|
210
212
|
}
|
|
211
213
|
const authToken = await resolveAuthToken();
|
|
212
|
-
|
|
213
|
-
// platform='webui' 且带密码凭据的用户记录。
|
|
214
|
-
const auth = createAuthSystem(authToken, ctx.logger.child('auth'), {
|
|
215
|
-
verify: (username, password) => ctx.getService('authority')?.verifyPassword('webui', username, password) ?? false,
|
|
216
|
-
hasAccounts: () => (ctx.getService('authority')?.listUsers() ?? []).some(u => u.platform === 'webui' && u.hasPassword),
|
|
217
|
-
},
|
|
218
|
-
// disabled:存在账户时 token 全面失效(auth 内部留有"无账户兜底"防锁死)
|
|
219
|
-
() => uiConfig.tokenMode !== 'disabled');
|
|
214
|
+
const auth = createAuthSystem(authToken, ctx.logger.child('auth'));
|
|
220
215
|
const expressApp = express();
|
|
221
216
|
expressApp.use(express.json({ limit: '10mb' }));
|
|
222
217
|
expressApp.use(auth.middleware);
|
|
223
|
-
// REST
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
expressApp.get('/api/auth/me', (req, res) => {
|
|
227
|
-
const identity = auth.identify(req) ?? { platform: 'webui', userId: 'console' };
|
|
228
|
-
const authority = ctx.getService('authority');
|
|
229
|
-
res.json({
|
|
230
|
-
identity,
|
|
231
|
-
isOwner: authority?.isOwner(identity.platform, identity.userId) ?? false,
|
|
232
|
-
});
|
|
233
|
-
});
|
|
218
|
+
// REST 路由 owner 闸(连接级认证之上的显式 owner 复核,见 gate.ts)。
|
|
219
|
+
// 当前身份 / 登录态查询统一走 auth.ts 的 GET /api/auth/status(含 identity)。
|
|
220
|
+
const gate = createRouteGate(auth.identify);
|
|
234
221
|
const server = createServer(expressApp);
|
|
235
222
|
const wss = new WebSocketServer({
|
|
236
223
|
server,
|
|
@@ -274,21 +261,27 @@ export async function apply(ctx, config) {
|
|
|
274
261
|
ctx.logger.warn(`前端目录不存在: ${dir}`);
|
|
275
262
|
}
|
|
276
263
|
}
|
|
277
|
-
//
|
|
264
|
+
// 已发现的前端候选(ready 时填充,逐个注册为 webui-client 服务 provider)。
|
|
278
265
|
const clientCandidates = [];
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
266
|
+
/** 当前活跃前端目录 = 解析后的 webui-client 服务(servicePreferences 偏好 > 优先级 > 注册顺序)。 */
|
|
267
|
+
function currentClientDir() {
|
|
268
|
+
return ctx.getService('webui-client')?.getClientDir?.();
|
|
269
|
+
}
|
|
270
|
+
/**
|
|
271
|
+
* 切换 webui-client 服务偏好后调用:重挂当前活跃前端 + 广播 'reload'——所有客户端进入
|
|
272
|
+
* 「正在切换前端,即将刷新」遮罩后自动 reload(复用现有刷新流程,平滑换前端、不重启进程)。
|
|
273
|
+
*/
|
|
274
|
+
function remountActiveClient() {
|
|
275
|
+
const dir = currentClientDir();
|
|
276
|
+
if (dir) {
|
|
277
|
+
clientDist = dir;
|
|
278
|
+
mountStaticDir(dir);
|
|
279
|
+
}
|
|
280
|
+
const payload = { type: 'reload' };
|
|
281
|
+
const json = JSON.stringify(payload);
|
|
282
|
+
for (const ws of allClients)
|
|
283
|
+
if (ws.readyState === WebSocket.OPEN)
|
|
284
|
+
ws.send(json);
|
|
292
285
|
}
|
|
293
286
|
// 动态静态文件中间件(支持运行时切换前端)
|
|
294
287
|
expressApp.use((req, res, next) => {
|
|
@@ -301,11 +294,11 @@ export async function apply(ctx, config) {
|
|
|
301
294
|
});
|
|
302
295
|
// ---------- REST API ----------
|
|
303
296
|
// 获取系统状态
|
|
304
|
-
expressApp.get('/api/status', gate(
|
|
297
|
+
expressApp.get('/api/status', gate(), (_req, res) => {
|
|
305
298
|
const persona = ctx.getService('persona');
|
|
306
299
|
// 判断上传能力
|
|
307
300
|
const hasMedia = ctx.hasService('media');
|
|
308
|
-
const llmHasVision = ctx.
|
|
301
|
+
const llmHasVision = listLLMModels(ctx).some(e => e.instance.capabilities.includes('vision'));
|
|
309
302
|
const hasFileReader = ctx.hasService('file-reader');
|
|
310
303
|
res.json({
|
|
311
304
|
name: persona?.getPersonaName() ?? ctx.config.get('name'),
|
|
@@ -339,13 +332,41 @@ export async function apply(ctx, config) {
|
|
|
339
332
|
});
|
|
340
333
|
});
|
|
341
334
|
// ---------- 插件管理 + 全局配置 ----------
|
|
335
|
+
// fs 扫描 env:discoverClients(ready 时)与市场「已装」兜底共用一份,避免两处重复。
|
|
336
|
+
const fsScanEnv = {
|
|
337
|
+
existsSync,
|
|
338
|
+
readdirSync: (p) => {
|
|
339
|
+
try {
|
|
340
|
+
return readdirSync(p);
|
|
341
|
+
}
|
|
342
|
+
catch {
|
|
343
|
+
return [];
|
|
344
|
+
}
|
|
345
|
+
},
|
|
346
|
+
readJson: (p) => {
|
|
347
|
+
try {
|
|
348
|
+
return JSON.parse(readFileSync(p, 'utf-8'));
|
|
349
|
+
}
|
|
350
|
+
catch {
|
|
351
|
+
return undefined;
|
|
352
|
+
}
|
|
353
|
+
},
|
|
354
|
+
join: (...parts) => resolve(...parts),
|
|
355
|
+
};
|
|
342
356
|
registerPluginRoutes(expressApp, ctx, getApp, getPluginMgr, auth.identify, gate);
|
|
343
|
-
|
|
357
|
+
// 市场「已装」判定 + 依赖图都吃这一份本地扫描(pnpm 工作区下 require.resolve 从仓库根解析不到工作区包):
|
|
358
|
+
// 与 discoverClients 同套 scanDirs(monorepo packages/ 同级 + node_modules/@aalis),每请求懒扫(量小)。
|
|
359
|
+
// 返回 name→依赖名[]:keys=已装包名,values=依赖图边。
|
|
360
|
+
const localScanDirs = [
|
|
361
|
+
resolve(dirname(fileURLToPath(import.meta.url)), '../../'),
|
|
362
|
+
resolve(process.cwd(), 'node_modules/@aalis'),
|
|
363
|
+
];
|
|
364
|
+
registerMarketplaceRoutes(expressApp, ctx, getPluginMgr, gate, uiConfig.marketplaceRegistry, () => collectLocalPackageDeps(localScanDirs, fsScanEnv));
|
|
344
365
|
// 获取历史日志:从 data/latest.log 读尾部 N 条(lazy load)。
|
|
345
366
|
// 单进程内 LogHub 不再缓存 buffer——历史以文件为单一数据源。
|
|
346
|
-
expressApp.get('/api/logs', gate(
|
|
367
|
+
expressApp.get('/api/logs', gate(), async (_req, res) => {
|
|
347
368
|
try {
|
|
348
|
-
const entries = await readLogFileTail(200);
|
|
369
|
+
const entries = await readLogFileTail(storage, 200);
|
|
349
370
|
res.json(entries);
|
|
350
371
|
}
|
|
351
372
|
catch (err) {
|
|
@@ -353,17 +374,17 @@ export async function apply(ctx, config) {
|
|
|
353
374
|
}
|
|
354
375
|
});
|
|
355
376
|
// 尾部 N 条(首屏 / 显式刷新)
|
|
356
|
-
expressApp.get('/api/logs/tail', gate(
|
|
377
|
+
expressApp.get('/api/logs/tail', gate(), async (req, res) => {
|
|
357
378
|
const limit = Math.min(Math.max(Number(req.query.limit) || 200, 1), 5000);
|
|
358
379
|
try {
|
|
359
|
-
res.json(await readLogFileTail(limit));
|
|
380
|
+
res.json(await readLogFileTail(storage, limit));
|
|
360
381
|
}
|
|
361
382
|
catch (err) {
|
|
362
383
|
res.status(500).json({ error: err.message });
|
|
363
384
|
}
|
|
364
385
|
});
|
|
365
386
|
// 历史分页:返回 seq < before 的最近 limit 条(向上滚动加载更早)
|
|
366
|
-
expressApp.get('/api/logs/range', gate(
|
|
387
|
+
expressApp.get('/api/logs/range', gate(), async (req, res) => {
|
|
367
388
|
const before = Number(req.query.before);
|
|
368
389
|
const limit = Math.min(Math.max(Number(req.query.limit) || 200, 1), 5000);
|
|
369
390
|
if (!Number.isFinite(before)) {
|
|
@@ -371,14 +392,14 @@ export async function apply(ctx, config) {
|
|
|
371
392
|
return;
|
|
372
393
|
}
|
|
373
394
|
try {
|
|
374
|
-
res.json(await readLogFileBefore(before, limit));
|
|
395
|
+
res.json(await readLogFileBefore(storage, before, limit));
|
|
375
396
|
}
|
|
376
397
|
catch (err) {
|
|
377
398
|
res.status(500).json({ error: err.message });
|
|
378
399
|
}
|
|
379
400
|
});
|
|
380
401
|
// 获取服务列表(含提供者信息)
|
|
381
|
-
expressApp.get('/api/services', gate(
|
|
402
|
+
expressApp.get('/api/services', gate(), async (_req, res) => {
|
|
382
403
|
const pluginMgr = getPluginMgr();
|
|
383
404
|
const pluginStatus = pluginMgr ? pluginMgr.getStatus() : [];
|
|
384
405
|
const displayNameMap = new Map();
|
|
@@ -396,7 +417,6 @@ export async function apply(ctx, config) {
|
|
|
396
417
|
services[svcName] = {
|
|
397
418
|
providers: entries.map(e => ({
|
|
398
419
|
contextId: e.contextId,
|
|
399
|
-
capabilities: [...e.capabilities],
|
|
400
420
|
displayName: displayNameMap.get(e.contextId),
|
|
401
421
|
label: e.label,
|
|
402
422
|
priority: e.priority,
|
|
@@ -410,7 +430,7 @@ export async function apply(ctx, config) {
|
|
|
410
430
|
* 设置服务偏好。body: { contextId: string }
|
|
411
431
|
* 偏好语义:`preferred > priority > 注册顺序`。持久化到 aalis.config.yaml 的 servicePreferences。
|
|
412
432
|
*/
|
|
413
|
-
expressApp.post('/api/services/:name/prefer', gate(
|
|
433
|
+
expressApp.post('/api/services/:name/prefer', gate(), async (req, res) => {
|
|
414
434
|
const svcName = String(req.params.name);
|
|
415
435
|
const contextId = String(req.body?.contextId ?? '').trim();
|
|
416
436
|
if (!contextId) {
|
|
@@ -426,38 +446,30 @@ export async function apply(ctx, config) {
|
|
|
426
446
|
ctx.preferService(svcName, contextId);
|
|
427
447
|
ctx.config.setServicePreference(svcName, contextId);
|
|
428
448
|
ctx.config.save();
|
|
449
|
+
// 切换前端:webui-client 是「前端」服务,偏好变更需重挂静态目录 + 通知客户端刷新。
|
|
450
|
+
if (svcName === 'webui-client')
|
|
451
|
+
remountActiveClient();
|
|
429
452
|
res.json({ ok: true });
|
|
430
453
|
});
|
|
431
454
|
/** 清除服务偏好 */
|
|
432
|
-
expressApp.delete('/api/services/:name/prefer', gate(
|
|
455
|
+
expressApp.delete('/api/services/:name/prefer', gate(), async (req, res) => {
|
|
433
456
|
const svcName = String(req.params.name);
|
|
434
457
|
ctx.unpreferService(svcName);
|
|
435
458
|
ctx.config.removeServicePreference(svcName);
|
|
436
459
|
ctx.config.save();
|
|
460
|
+
if (svcName === 'webui-client')
|
|
461
|
+
remountActiveClient();
|
|
437
462
|
res.json({ ok: true });
|
|
438
463
|
});
|
|
439
|
-
//
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
clients: clientCandidates.map(c => ({ id: c.id, label: c.label, active: c.id === activeClientId })),
|
|
443
|
-
active: activeClientId,
|
|
444
|
-
});
|
|
445
|
-
});
|
|
446
|
-
// 设活跃前端(owner):实时重挂 + 持久化;前端切完自行 reload 即加载新前端。
|
|
447
|
-
expressApp.post('/api/clients/active', gate('webui:clients:manage', 'restricted'), (req, res) => {
|
|
448
|
-
const id = String(req.body?.id ?? '').trim();
|
|
449
|
-
if (!activateClient(id)) {
|
|
450
|
-
res.status(404).json({ ok: false, error: `未找到已发现的前端 "${id}"` });
|
|
451
|
-
return;
|
|
452
|
-
}
|
|
453
|
-
res.json({ ok: true, active: id });
|
|
454
|
-
});
|
|
464
|
+
// 前端切换已统一到「服务偏好」:前端是 webui-client 服务的多 provider,切换走
|
|
465
|
+
// POST /api/services/webui-client/prefer(见上),webui-server 在该偏好变更时重挂 + 广播刷新。
|
|
466
|
+
// 故不再有独立的 /api/clients* 路由。
|
|
455
467
|
// 获取所有平台适配器及其连接状态
|
|
456
|
-
expressApp.get('/api/platforms', gate(
|
|
468
|
+
expressApp.get('/api/platforms', gate(), (_req, res) => {
|
|
457
469
|
res.json({ platforms: aggregatePlatformDetails(ctx) });
|
|
458
470
|
});
|
|
459
471
|
// 获取已注册的工具分组(含元数据 + 各组工具数量 + 贡献插件列表)
|
|
460
|
-
expressApp.get('/api/tool-groups', gate(
|
|
472
|
+
expressApp.get('/api/tool-groups', gate(), (_req, res) => {
|
|
461
473
|
const groups = ctx.getService('tools')?.getGroups() ?? [];
|
|
462
474
|
const allTools = ctx.getService('tools')?.getAll() ?? [];
|
|
463
475
|
const knownNames = new Set(groups.map(g => g.name));
|
|
@@ -486,7 +498,7 @@ export async function apply(ctx, config) {
|
|
|
486
498
|
res.json({ groups: result });
|
|
487
499
|
});
|
|
488
500
|
// 获取服务分组(manifest 驱动:每个插件自己声明 subsystem,本路由仅聚合)
|
|
489
|
-
expressApp.get('/api/service-groups', gate(
|
|
501
|
+
expressApp.get('/api/service-groups', gate(), (_req, res) => {
|
|
490
502
|
const pluginMgr = getPluginMgr();
|
|
491
503
|
const pluginStatus = pluginMgr ? pluginMgr.getStatus() : [];
|
|
492
504
|
// 按插件 subsystem 归组(未声明 → 'external')。subsystem 是 WebUI 展示概念、
|
|
@@ -527,12 +539,12 @@ export async function apply(ctx, config) {
|
|
|
527
539
|
res.json({ groups });
|
|
528
540
|
});
|
|
529
541
|
// 获取所有 LLM 模型(枚举所有注册的 per-model entry)
|
|
530
|
-
expressApp.get('/api/llm-models', gate(
|
|
542
|
+
expressApp.get('/api/llm-models', gate(), async (_req, res) => {
|
|
531
543
|
try {
|
|
532
544
|
const entries = ctx.getAllServices('llm');
|
|
533
545
|
const models = entries.map(e => ({
|
|
534
546
|
id: e.instance.id,
|
|
535
|
-
capabilities: e.capabilities,
|
|
547
|
+
capabilities: [...e.instance.capabilities],
|
|
536
548
|
provider: e.instance.providerId,
|
|
537
549
|
contextId: e.contextId,
|
|
538
550
|
}));
|
|
@@ -543,7 +555,7 @@ export async function apply(ctx, config) {
|
|
|
543
555
|
}
|
|
544
556
|
});
|
|
545
557
|
// LLM providers + per-provider models(供 schema type='llm-ref' 联动 select 使用)
|
|
546
|
-
expressApp.get('/api/llm-providers', gate(
|
|
558
|
+
expressApp.get('/api/llm-providers', gate(), async (_req, res) => {
|
|
547
559
|
try {
|
|
548
560
|
const entries = ctx.getAllServices('llm');
|
|
549
561
|
const byProvider = new Map();
|
|
@@ -567,7 +579,7 @@ export async function apply(ctx, config) {
|
|
|
567
579
|
}
|
|
568
580
|
agg.models.push({
|
|
569
581
|
id: e.instance.id,
|
|
570
|
-
capabilities: e.capabilities,
|
|
582
|
+
capabilities: [...e.instance.capabilities],
|
|
571
583
|
contextLength: e.instance.contextLength,
|
|
572
584
|
});
|
|
573
585
|
}
|
|
@@ -580,7 +592,7 @@ export async function apply(ctx, config) {
|
|
|
580
592
|
// 触发指定 provider 重新探测远端模型列表(用于 webui 上的"刷新模型"按钮)
|
|
581
593
|
// 仅对在 LLMModel 上实现了 refresh() 的 provider 生效(远端动态发现型,如 Ollama / OpenAI)。
|
|
582
594
|
// 同 provider 下所有 model entries 共享同一份 refresh 闭包,调任一个 entry 即可。
|
|
583
|
-
expressApp.post('/api/llm-providers/:contextId/refresh', gate(
|
|
595
|
+
expressApp.post('/api/llm-providers/:contextId/refresh', gate(), async (req, res) => {
|
|
584
596
|
const contextId = req.params.contextId;
|
|
585
597
|
if (!contextId) {
|
|
586
598
|
res.status(400).json({ error: 'contextId is required' });
|
|
@@ -603,7 +615,7 @@ export async function apply(ctx, config) {
|
|
|
603
615
|
}
|
|
604
616
|
});
|
|
605
617
|
// 获取某个服务的可用模型/选项列表
|
|
606
|
-
expressApp.get('/api/models/:service', gate(
|
|
618
|
+
expressApp.get('/api/models/:service', gate(), async (req, res) => {
|
|
607
619
|
const serviceName = req.params.service;
|
|
608
620
|
// 特殊处理 platform:通过 helper 获取已注册的平台名称
|
|
609
621
|
if (serviceName === 'platform') {
|
|
@@ -677,7 +689,7 @@ export async function apply(ctx, config) {
|
|
|
677
689
|
model: e.instance.id,
|
|
678
690
|
provider: e.label ?? e.contextId,
|
|
679
691
|
contextId: e.contextId,
|
|
680
|
-
capabilities: e.capabilities,
|
|
692
|
+
capabilities: [...e.instance.capabilities],
|
|
681
693
|
}));
|
|
682
694
|
res.json({ models: aggregated.map(a => a.value), providers: aggregated });
|
|
683
695
|
}
|
|
@@ -704,9 +716,7 @@ export async function apply(ctx, config) {
|
|
|
704
716
|
for (const m of models) {
|
|
705
717
|
const isModelInfo = typeof m === 'object' && m !== null && 'id' in m;
|
|
706
718
|
const modelId = isModelInfo ? m.id : String(m);
|
|
707
|
-
const modelCaps = isModelInfo
|
|
708
|
-
? m.capabilities
|
|
709
|
-
: provider.capabilities;
|
|
719
|
+
const modelCaps = isModelInfo ? m.capabilities : [];
|
|
710
720
|
aggregated.push({
|
|
711
721
|
value: modelId,
|
|
712
722
|
model: modelId,
|
|
@@ -727,57 +737,22 @@ export async function apply(ctx, config) {
|
|
|
727
737
|
res.json({ models: [] });
|
|
728
738
|
}
|
|
729
739
|
});
|
|
730
|
-
// ----------
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
const json = JSON.stringify(payload);
|
|
744
|
-
const sockets = sessions.get(request.sessionId);
|
|
745
|
-
const targets = sockets && sockets.size > 0 ? sockets : allClients;
|
|
746
|
-
let sent = false;
|
|
747
|
-
for (const ws of targets) {
|
|
748
|
-
if (ws.readyState === WebSocket.OPEN) {
|
|
749
|
-
ws.send(json);
|
|
750
|
-
sent = true;
|
|
740
|
+
// ---------- 受限操作交互式确认(复用 session-confirm 协调器;WebUI 传输=WS type:'confirm',流式友好) ----------
|
|
741
|
+
// 协调器逻辑(待确认/超时/解析/文案)由 session-confirm 服务统一实现;WebUI 只注入自己的 WS 投递,
|
|
742
|
+
// 并保留 type:'confirm'(前端「确认模式」信号,抑制富客户端的「打字即打断」)。回复在 WS-onmessage 调 feed(下方)。
|
|
743
|
+
let confirmChannel;
|
|
744
|
+
ctx.whenService('session-confirm', confirmSvc => {
|
|
745
|
+
confirmChannel = confirmSvc.createChannel((request, text) => {
|
|
746
|
+
const payload = { type: 'confirm', content: text, sessionId: request.sessionId };
|
|
747
|
+
const json = JSON.stringify(payload);
|
|
748
|
+
const sockets = sessions.get(request.sessionId);
|
|
749
|
+
const targets = sockets && sockets.size > 0 ? sockets : allClients;
|
|
750
|
+
for (const ws of targets) {
|
|
751
|
+
if (ws.readyState === WebSocket.OPEN)
|
|
752
|
+
ws.send(json);
|
|
751
753
|
}
|
|
752
|
-
}
|
|
753
|
-
if (!sent)
|
|
754
|
-
return false;
|
|
755
|
-
// 同一 session 已有未决确认时,先取消旧的(清 timer + resolve false),避免新
|
|
756
|
-
// set 覆盖后旧 Promise 永远 pending 且旧 timer 误删新条目(审计 HIGH #7 竞态)。
|
|
757
|
-
const stale = pendingSessionConfirms.get(request.sessionId);
|
|
758
|
-
if (stale) {
|
|
759
|
-
clearTimeout(stale.timer);
|
|
760
|
-
pendingSessionConfirms.delete(request.sessionId);
|
|
761
|
-
stale.resolve(false);
|
|
762
|
-
}
|
|
763
|
-
return new Promise(resolve => {
|
|
764
|
-
const timer = setTimeout(() => {
|
|
765
|
-
pendingSessionConfirms.delete(request.sessionId);
|
|
766
|
-
// 超时后向会话发送提示
|
|
767
|
-
const timeoutPayload = {
|
|
768
|
-
type: 'confirm',
|
|
769
|
-
content: '⏰ 受限操作确认已超时,已自动取消。',
|
|
770
|
-
sessionId: request.sessionId,
|
|
771
|
-
};
|
|
772
|
-
const timeoutJson = JSON.stringify(timeoutPayload);
|
|
773
|
-
for (const ws of targets) {
|
|
774
|
-
if (ws.readyState === WebSocket.OPEN)
|
|
775
|
-
ws.send(timeoutJson);
|
|
776
|
-
}
|
|
777
|
-
resolve(false);
|
|
778
|
-
}, CONFIRM_TIMEOUT);
|
|
779
|
-
pendingSessionConfirms.set(request.sessionId, { resolve, timer });
|
|
780
754
|
});
|
|
755
|
+
ctx.getService('authority')?.setConfirmHandler('webui', confirmChannel.handler);
|
|
781
756
|
});
|
|
782
757
|
// ---------- 文件管理 API ----------
|
|
783
758
|
registerFileRoutes(expressApp, ctx, { storage, fileRoot: uiConfig.fileRoot }, gate);
|
|
@@ -876,18 +851,9 @@ export async function apply(ctx, config) {
|
|
|
876
851
|
sessions.set(sessionId, new Set());
|
|
877
852
|
}
|
|
878
853
|
sessions.get(sessionId).add(ws);
|
|
879
|
-
//
|
|
880
|
-
|
|
881
|
-
if (
|
|
882
|
-
clearTimeout(pending.timer);
|
|
883
|
-
pendingSessionConfirms.delete(sessionId);
|
|
884
|
-
const answer = trimmed.toLowerCase();
|
|
885
|
-
if (answer === 'ys' || answer === 'y session') {
|
|
886
|
-
pending.resolve({ allowed: true, grant: { scope: 'session', durationSeconds: 600, maxUses: 30 } });
|
|
887
|
-
}
|
|
888
|
-
else {
|
|
889
|
-
pending.resolve(answer === 'y');
|
|
890
|
-
}
|
|
854
|
+
// 检查待确认的受限操作(拦截输入作为确认/取消;协调器逻辑由 session-confirm 服务统一处理)
|
|
855
|
+
// 传应答者身份:仅确认的触发者本人能应答(防第三方抢答;webui 同账户天然同人)。
|
|
856
|
+
if (confirmChannel?.feed(sessionId, trimmed, wsIdentity.userId)) {
|
|
891
857
|
// 不继续处理此消息,交给原始命令/工具执行流返回结果
|
|
892
858
|
return;
|
|
893
859
|
}
|
|
@@ -1210,6 +1176,13 @@ export async function apply(ctx, config) {
|
|
|
1210
1176
|
}
|
|
1211
1177
|
}
|
|
1212
1178
|
});
|
|
1179
|
+
// 前端切换「逃生页」:由 webui-server 直出、独立于任何可被切换的前端,故任意前端(哪怕
|
|
1180
|
+
// 极简第三方前端无切换 UI)卡住时都能在此切回。受全局 auth 中间件登录保护;切换走既有 owner
|
|
1181
|
+
// 闸的 POST /api/services/webui-client/prefer,本页零新增后端逻辑(见 client-switch-page.ts)。
|
|
1182
|
+
// 注册在 SPA 兜底之前,故 `/__clients` 被本路由先接走、不落到前端 index.html。
|
|
1183
|
+
expressApp.get('/__clients', (_req, res) => {
|
|
1184
|
+
res.type('html').send(renderClientSwitchPage());
|
|
1185
|
+
});
|
|
1213
1186
|
// SPA fallback: 所有非 API 路径返回 index.html
|
|
1214
1187
|
expressApp.get('{*path}', (_req, res) => {
|
|
1215
1188
|
const indexPath = resolve(clientDist, 'index.html');
|
|
@@ -1222,90 +1195,67 @@ export async function apply(ctx, config) {
|
|
|
1222
1195
|
});
|
|
1223
1196
|
// 启动服务器
|
|
1224
1197
|
ctx.on('ready', () => {
|
|
1225
|
-
//
|
|
1226
|
-
//
|
|
1227
|
-
//
|
|
1228
|
-
|
|
1229
|
-
// 回退:webui-server 同级目录(cwd≠项目根等边角场景兜底)。
|
|
1230
|
-
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
1198
|
+
// 全动态发现前端:按 `aalis.client:true` 标记扫描,**不硬编码任何前端包名**
|
|
1199
|
+
// (与 runtime 加载器的 marker 驱动一致;忒修斯之船——任意第三方前端带标记+dist 即被发现)。
|
|
1200
|
+
// 覆盖三种拓扑:monorepo(扫 packages 同级目录)/ 独立项目(扫 node_modules/@aalis + 根 deps)。
|
|
1201
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
1231
1202
|
const projectRequire = createRequire(pathToFileURL(resolve(process.cwd(), 'package.json')));
|
|
1232
|
-
const
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
catch {
|
|
1237
|
-
const sibling = resolve(__dirname, `../../${id.replace('@aalis/', '')}/dist`);
|
|
1238
|
-
return existsSync(sibling) ? sibling : undefined;
|
|
1239
|
-
}
|
|
1240
|
-
};
|
|
1241
|
-
const addCandidate = (id, label, dir) => {
|
|
1242
|
-
if (!dir || clientCandidates.some(c => c.id === id))
|
|
1243
|
-
return;
|
|
1244
|
-
if (existsSync(dir) && existsSync(resolve(dir, 'index.html')))
|
|
1245
|
-
clientCandidates.push({ id, label, dir });
|
|
1246
|
-
};
|
|
1247
|
-
// ① 已知 @aalis 前端(保证 monorepo/默认拓扑可用——根 deps 未必列 client)
|
|
1248
|
-
addCandidate('@aalis/plugin-webui-client', 'Aalis 默认前端', resolveClientDir('@aalis/plugin-webui-client'));
|
|
1249
|
-
addCandidate('@aalis/plugin-webui-client-napcat', 'NapCat 前端', resolveClientDir('@aalis/plugin-webui-client-napcat'));
|
|
1250
|
-
// ② 追加发现:项目 deps 里任何带 aalis.client marker 的第三方前端(忒修斯之船可换)
|
|
1251
|
-
try {
|
|
1252
|
-
const rootPkg = JSON.parse(readFileSync(resolve(process.cwd(), 'package.json'), 'utf-8'));
|
|
1253
|
-
const deps = Object.keys({ ...rootPkg.dependencies, ...rootPkg.optionalDependencies });
|
|
1254
|
-
for (const dep of deps) {
|
|
1255
|
-
if (clientCandidates.some(c => c.id === dep))
|
|
1256
|
-
continue;
|
|
1203
|
+
const env = {
|
|
1204
|
+
...fsScanEnv,
|
|
1205
|
+
dirname,
|
|
1206
|
+
resolvePkgJson: id => {
|
|
1257
1207
|
try {
|
|
1258
|
-
|
|
1259
|
-
const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
|
|
1260
|
-
if (pkg?.aalis?.client !== true)
|
|
1261
|
-
continue;
|
|
1262
|
-
addCandidate(dep, pkg.displayName ?? pkg.description ?? dep, resolve(dirname(pkgPath), 'dist'));
|
|
1208
|
+
return projectRequire.resolve(`${id}/package.json`);
|
|
1263
1209
|
}
|
|
1264
1210
|
catch {
|
|
1265
|
-
|
|
1211
|
+
return undefined;
|
|
1266
1212
|
}
|
|
1267
|
-
}
|
|
1213
|
+
},
|
|
1214
|
+
};
|
|
1215
|
+
// here = <pkg>/dist;其上两级即 monorepo 的 packages 目录。再加项目 node_modules/@aalis 作用域。
|
|
1216
|
+
const scanDirs = [resolve(here, '../../'), resolve(process.cwd(), 'node_modules/@aalis')];
|
|
1217
|
+
let depIds = [];
|
|
1218
|
+
try {
|
|
1219
|
+
const rootPkg = JSON.parse(readFileSync(resolve(process.cwd(), 'package.json'), 'utf-8'));
|
|
1220
|
+
depIds = Object.keys({ ...rootPkg.dependencies, ...rootPkg.optionalDependencies });
|
|
1268
1221
|
}
|
|
1269
1222
|
catch {
|
|
1270
|
-
/* 无项目根 package.json(如 monorepo 直跑),跳过
|
|
1223
|
+
/* 无项目根 package.json(如 monorepo 直跑),跳过 deps 扫描 */
|
|
1271
1224
|
}
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
for (const candidate of clientCandidates) {
|
|
1279
|
-
const childCtx = ctx.fork(candidate.id);
|
|
1280
|
-
childCtx.provide('webui-client', { getClientDir: () => candidate.dir }, { label: candidate.label });
|
|
1281
|
-
ctx.logger.info(`发现前端: ${candidate.label} (${candidate.dir})`);
|
|
1282
|
-
}
|
|
1283
|
-
clientDist = active.dir;
|
|
1284
|
-
mountStaticDir(active.dir);
|
|
1285
|
-
activeClientId = active.id;
|
|
1286
|
-
ctx.logger.info(`活跃前端: ${active.label}${preferred ? '(config.client 指定)' : ''}`);
|
|
1225
|
+
clientCandidates.push(...discoverClients(scanDirs, depIds, env));
|
|
1226
|
+
// 把每个发现到的前端注册为 webui-client 服务的一个 provider(带 label,供「服务」页下拉切换)。
|
|
1227
|
+
// 外部插件在 apply 里主动 provide('webui-client') 的也已在服务池中(注册更早 → 默认胜出,仍可被偏好切换)。
|
|
1228
|
+
for (const candidate of clientCandidates) {
|
|
1229
|
+
ctx.fork(candidate.id).provide('webui-client', { getClientDir: () => candidate.dir }, { label: candidate.label });
|
|
1230
|
+
ctx.logger.info(`发现前端: ${candidate.label} (${candidate.dir})`);
|
|
1287
1231
|
}
|
|
1288
|
-
//
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1232
|
+
// 活跃前端 = 解析后的 webui-client 服务(servicePreferences 偏好 > 优先级 > 注册顺序);零前端则 404。
|
|
1233
|
+
const activeDir = currentClientDir();
|
|
1234
|
+
if (activeDir) {
|
|
1235
|
+
clientDist = activeDir;
|
|
1236
|
+
mountStaticDir(activeDir);
|
|
1237
|
+
ctx.logger.info(`活跃前端: ${activeDir}`);
|
|
1238
|
+
}
|
|
1239
|
+
else {
|
|
1240
|
+
ctx.logger.warn('未发现任何前端(webui-client provider);前端路由将 404,请安装一个 aalis.client 包');
|
|
1297
1241
|
}
|
|
1298
1242
|
server.listen(uiConfig.port, uiConfig.host, () => {
|
|
1299
1243
|
const url = `http://${uiConfig.host}:${uiConfig.port}/`;
|
|
1300
1244
|
const accessUrl = `${url}?token=${authToken}`;
|
|
1301
1245
|
void writeAccessFile(url, authToken);
|
|
1302
1246
|
ctx.logger.info(`WebUI 已启动: ${url}`);
|
|
1247
|
+
// 多前端时提示恢复页 URL:万一切到无切换 UI 的前端被卡住,可在此切回(详见 client-switch-page.ts)。
|
|
1248
|
+
if (clientCandidates.length > 1) {
|
|
1249
|
+
ctx.logger.info(`检测到多个前端;若卡在某前端无切换入口,可访问恢复页切回: ${url}__clients`);
|
|
1250
|
+
}
|
|
1303
1251
|
const tokenHint = uiConfig.tokenMode === 'ephemeral'
|
|
1304
1252
|
? 'token 仅本次启动有效,重启轮换'
|
|
1305
1253
|
: uiConfig.tokenMode === 'fixed'
|
|
1306
1254
|
? 'token 来自配置 fixedToken,固定不变'
|
|
1307
1255
|
: 'token 已持久化到 storage data:/webui/token,重启沿用';
|
|
1308
|
-
|
|
1256
|
+
// 不把 token 打进日志(会落 latest.log / 被 /api/logs 回放 / 贴日志求助时外泄)。
|
|
1257
|
+
// 仅打不带 token 的 URL,完整一键登录链接见 access.txt(该文件应 0o600,见 token 落盘)。
|
|
1258
|
+
ctx.logger.info(`首次访问 URL(${tokenHint}): ${url} —— 完整一键登录链接见 ${accessFileUri}`);
|
|
1309
1259
|
void (async () => {
|
|
1310
1260
|
let absHint = accessFileUri;
|
|
1311
1261
|
try {
|
|
@@ -1323,6 +1273,7 @@ export async function apply(ctx, config) {
|
|
|
1323
1273
|
});
|
|
1324
1274
|
});
|
|
1325
1275
|
ctx.onDispose(() => {
|
|
1276
|
+
confirmChannel?.dispose(); // 清 WebUI 确认通道里挂起的待确认(安全拒),避免 Promise 永挂
|
|
1326
1277
|
removeLogListener();
|
|
1327
1278
|
wss.close();
|
|
1328
1279
|
server.close();
|
|
@@ -1363,7 +1314,7 @@ export async function apply(ctx, config) {
|
|
|
1363
1314
|
}
|
|
1364
1315
|
},
|
|
1365
1316
|
};
|
|
1366
|
-
ctx.provide('platform', adapter
|
|
1317
|
+
ctx.provide('platform', adapter);
|
|
1367
1318
|
// === 注册 WebUI 服务 ===
|
|
1368
1319
|
const registeredPages = new Map();
|
|
1369
1320
|
// 内建页面归属为 webui-server 本身
|
|
@@ -1405,6 +1356,6 @@ export async function apply(ctx, config) {
|
|
|
1405
1356
|
registeredPages.delete(pluginName);
|
|
1406
1357
|
},
|
|
1407
1358
|
};
|
|
1408
|
-
ctx.provide('webui-server', webuiService
|
|
1359
|
+
ctx.provide('webui-server', webuiService);
|
|
1409
1360
|
}
|
|
1410
1361
|
//# sourceMappingURL=index.js.map
|