@aalis/plugin-webui-server 0.1.2 → 0.2.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/gate.d.ts +12 -11
- package/dist/gate.d.ts.map +1 -1
- package/dist/gate.js +16 -20
- package/dist/gate.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +88 -52
- 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 +18 -0
- package/dist/routes/marketplace.d.ts.map +1 -1
- package/dist/routes/marketplace.js +51 -5
- package/dist/routes/marketplace.js.map +1 -1
- package/dist/routes/plugins.d.ts.map +1 -1
- package/dist/routes/plugins.js +32 -32
- package/dist/routes/plugins.js.map +1 -1
- package/dist/routes/proxy.js +1 -1
- 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 +20 -20
package/dist/gate.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { Context } from '@aalis/core';
|
|
2
|
-
import type { UserIdentity } from '@aalis/plugin-authority-api';
|
|
2
|
+
import type { CapabilityVisibility, UserIdentity } from '@aalis/plugin-authority-api';
|
|
3
3
|
/** 从请求解析调用者身份(auth.ts 的 AuthSystem.identify) */
|
|
4
4
|
type IdentifyFn = (req: {
|
|
5
5
|
headers: {
|
|
@@ -18,21 +18,22 @@ interface GateResponse {
|
|
|
18
18
|
}
|
|
19
19
|
type GateMiddleware = (req: GateRequest, res: GateResponse, next: () => void) => void;
|
|
20
20
|
/**
|
|
21
|
-
* REST
|
|
21
|
+
* REST 路由能力闸工厂。
|
|
22
22
|
*
|
|
23
23
|
* 连接级认证(auth middleware)只回答"是否登录";本闸回答"该身份能否做这件事"——
|
|
24
|
-
* 每条 REST 路由声明一个 capability
|
|
25
|
-
*
|
|
26
|
-
*
|
|
24
|
+
* 每条 REST 路由声明一个 capability 与默认可见性,过 authorize 统一闸
|
|
25
|
+
* (deny > owner(*) > public > granted)。owner 拥有一切;可对单账户 grant 某条
|
|
26
|
+
* restricted capability(如 `webui:files:read`)做细粒度委托。
|
|
27
27
|
*
|
|
28
|
-
*
|
|
29
|
-
* -
|
|
30
|
-
* -
|
|
31
|
-
*
|
|
28
|
+
* 可见性约定(颗粒度由 capability 字符串本身承载,owner 之外的细分靠委托 grant):
|
|
29
|
+
* - 'public':公共读(status / pages / 模型列表等,聊天面板可用即可)。
|
|
30
|
+
* - 'restricted':管理读(插件清单含原始配置、日志、文件浏览——可能含密钥)与
|
|
31
|
+
* 一切变更操作(配置写、插件启停装卸、服务偏好、文件删改);默认禁,须 owner /
|
|
32
|
+
* 被委托授予。
|
|
32
33
|
*
|
|
33
|
-
* authority
|
|
34
|
+
* authority 服务缺席时:public 放行、restricted 503(fail-closed,不在权限服务缺位时裸奔)。
|
|
34
35
|
*/
|
|
35
|
-
export declare function createRouteGate(ctx: Context, identify: IdentifyFn): (capability: string,
|
|
36
|
+
export declare function createRouteGate(ctx: Context, identify: IdentifyFn): (capability: string, visibility: CapabilityVisibility) => GateMiddleware;
|
|
36
37
|
/** createRouteGate 返回的闸生成函数类型(传给各 routes/ 注册器) */
|
|
37
38
|
export type RouteGate = ReturnType<typeof createRouteGate>;
|
|
38
39
|
export {};
|
package/dist/gate.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"gate.d.ts","sourceRoot":"","sources":["../src/gate.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,aAAa,CAAC;AAC3C,OAAO,KAAK,EAAoB,YAAY,EAAE,MAAM,6BAA6B,CAAC;
|
|
1
|
+
{"version":3,"file":"gate.d.ts","sourceRoot":"","sources":["../src/gate.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,aAAa,CAAC;AAC3C,OAAO,KAAK,EAAoB,oBAAoB,EAAE,YAAY,EAAE,MAAM,6BAA6B,CAAC;AAExG,gDAAgD;AAChD,KAAK,UAAU,GAAG,CAAC,GAAG,EAAE;IAAE,OAAO,EAAE;QAAE,MAAM,CAAC,EAAE,MAAM,CAAA;KAAE,CAAA;CAAE,KAAK,YAAY,GAAG,SAAS,CAAC;AAKtF,UAAU,WAAW;IACnB,OAAO,EAAE;QAAE,MAAM,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;CAC9B;AACD,UAAU,YAAY;IACpB,MAAM,CAAC,IAAI,EAAE,MAAM,GAAG;QAAE,IAAI,CAAC,IAAI,EAAE,OAAO,GAAG,OAAO,CAAA;KAAE,CAAC;CACxD;AACD,KAAK,cAAc,GAAG,CAAC,GAAG,EAAE,WAAW,EAAE,GAAG,EAAE,YAAY,EAAE,IAAI,EAAE,MAAM,IAAI,KAAK,IAAI,CAAC;AAEtF;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,eAAe,CAAC,GAAG,EAAE,OAAO,EAAE,QAAQ,EAAE,UAAU,IACxD,YAAY,MAAM,EAAE,YAAY,oBAAoB,KAAG,cAAc,CAqB9E;AAED,kDAAkD;AAClD,MAAM,MAAM,SAAS,GAAG,UAAU,CAAC,OAAO,eAAe,CAAC,CAAC"}
|
package/dist/gate.js
CHANGED
|
@@ -1,38 +1,34 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* REST
|
|
2
|
+
* REST 路由能力闸工厂。
|
|
3
3
|
*
|
|
4
4
|
* 连接级认证(auth middleware)只回答"是否登录";本闸回答"该身份能否做这件事"——
|
|
5
|
-
* 每条 REST 路由声明一个 capability
|
|
6
|
-
*
|
|
7
|
-
*
|
|
5
|
+
* 每条 REST 路由声明一个 capability 与默认可见性,过 authorize 统一闸
|
|
6
|
+
* (deny > owner(*) > public > granted)。owner 拥有一切;可对单账户 grant 某条
|
|
7
|
+
* restricted capability(如 `webui:files:read`)做细粒度委托。
|
|
8
8
|
*
|
|
9
|
-
*
|
|
10
|
-
* -
|
|
11
|
-
* -
|
|
12
|
-
*
|
|
9
|
+
* 可见性约定(颗粒度由 capability 字符串本身承载,owner 之外的细分靠委托 grant):
|
|
10
|
+
* - 'public':公共读(status / pages / 模型列表等,聊天面板可用即可)。
|
|
11
|
+
* - 'restricted':管理读(插件清单含原始配置、日志、文件浏览——可能含密钥)与
|
|
12
|
+
* 一切变更操作(配置写、插件启停装卸、服务偏好、文件删改);默认禁,须 owner /
|
|
13
|
+
* 被委托授予。
|
|
13
14
|
*
|
|
14
|
-
* authority
|
|
15
|
+
* authority 服务缺席时:public 放行、restricted 503(fail-closed,不在权限服务缺位时裸奔)。
|
|
15
16
|
*/
|
|
16
17
|
export function createRouteGate(ctx, identify) {
|
|
17
|
-
|
|
18
|
-
// 高于此值的(管理读/变更)一律拒绝——fail-closed,不在权限服务缺位时裸奔。
|
|
19
|
-
const PUBLIC_BASELINE = 1;
|
|
20
|
-
return (capability, declared) => (req, res, next) => {
|
|
21
|
-
const ownerLevel = ctx.config.get('ownerAuthority') ?? 5;
|
|
22
|
-
const declaredAuthority = declared === 'owner' ? ownerLevel : declared;
|
|
18
|
+
return (capability, visibility) => (req, res, next) => {
|
|
23
19
|
const authority = ctx.getService('authority');
|
|
24
20
|
if (!authority) {
|
|
25
|
-
// authority 缺席(未配置/启动失败/bounce 窗口期)→
|
|
26
|
-
//
|
|
27
|
-
if (
|
|
28
|
-
res.status(503).json({ error: '
|
|
21
|
+
// authority 缺席(未配置/启动失败/bounce 窗口期)→ 无法裁决。
|
|
22
|
+
// 公共读放行,受限操作 503(区别于 403:服务暂不可用,非权限不足)。
|
|
23
|
+
if (visibility === 'restricted') {
|
|
24
|
+
res.status(503).json({ error: '权限服务不可用,受限操作暂时被拒绝' });
|
|
29
25
|
return;
|
|
30
26
|
}
|
|
31
27
|
next();
|
|
32
28
|
return;
|
|
33
29
|
}
|
|
34
30
|
const caller = identify(req) ?? { platform: 'webui', userId: 'console' };
|
|
35
|
-
const denied = authority.authorize(caller, {
|
|
31
|
+
const denied = authority.authorize(caller, { capability, visibility });
|
|
36
32
|
if (denied) {
|
|
37
33
|
res.status(403).json({ error: denied });
|
|
38
34
|
return;
|
package/dist/gate.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"gate.js","sourceRoot":"","sources":["../src/gate.ts"],"names":[],"mappings":"AAiBA
|
|
1
|
+
{"version":3,"file":"gate.js","sourceRoot":"","sources":["../src/gate.ts"],"names":[],"mappings":"AAiBA;;;;;;;;;;;;;;;GAeG;AACH,MAAM,UAAU,eAAe,CAAC,GAAY,EAAE,QAAoB;IAChE,OAAO,CAAC,UAAkB,EAAE,UAAgC,EAAkB,EAAE,CAC9E,CAAC,GAAG,EAAE,GAAG,EAAE,IAAI,EAAE,EAAE;QACjB,MAAM,SAAS,GAAG,GAAG,CAAC,UAAU,CAAmB,WAAW,CAAC,CAAC;QAChE,IAAI,CAAC,SAAS,EAAE,CAAC;YACf,2CAA2C;YAC3C,wCAAwC;YACxC,IAAI,UAAU,KAAK,YAAY,EAAE,CAAC;gBAChC,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,mBAAmB,EAAE,CAAC,CAAC;gBACrD,OAAO;YACT,CAAC;YACD,IAAI,EAAE,CAAC;YACP,OAAO;QACT,CAAC;QACD,MAAM,MAAM,GAAG,QAAQ,CAAC,GAAG,CAAC,IAAI,EAAE,QAAQ,EAAE,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,CAAC;QACzE,MAAM,MAAM,GAAG,SAAS,CAAC,SAAS,CAAC,MAAM,EAAE,EAAE,UAAU,EAAE,UAAU,EAAE,CAAC,CAAC;QACvE,IAAI,MAAM,EAAE,CAAC;YACX,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,MAAM,EAAE,CAAC,CAAC;YACxC,OAAO;QACT,CAAC;QACD,IAAI,EAAE,CAAC;IACT,CAAC,CAAC;AACN,CAAC"}
|
package/dist/index.d.ts
CHANGED
|
@@ -16,6 +16,7 @@ export declare const defaultConfig: {
|
|
|
16
16
|
fixedToken: string;
|
|
17
17
|
relationGraphDefaultSpacing: number;
|
|
18
18
|
marketplaceRegistry: string;
|
|
19
|
+
client: string;
|
|
19
20
|
};
|
|
20
21
|
export { type WSIncoming, WSIncomingSchema } from './protocol.js';
|
|
21
22
|
export declare function apply(ctx: Context, config: Record<string, unknown>): Promise<void>;
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EAAc,YAAY,EAAE,OAAO,EAAkC,MAAM,aAAa,CAAC;AAqCrG,eAAO,MAAM,IAAI,+BAA+B,CAAC;AACjD,eAAO,MAAM,WAAW,6BAAc,CAAC;AACvC,eAAO,MAAM,SAAS,aAAa,CAAC;AACpC,eAAO,MAAM,QAAQ,UAAmB,CAAC;AACzC,eAAO,MAAM,MAAM;;CAMlB,CAAC;AAWF,eAAO,MAAM,YAAY,EAAE,YAuD1B,CAAC;AAEF,eAAO,MAAM,aAAa;;;;;;;;;;CAUzB,CAAC;AAsBF,OAAO,EAAE,KAAK,UAAU,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAC;AAqIlE,wBAAsB,KAAK,CAAC,GAAG,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,CAo1CxF"}
|
package/dist/index.js
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
// node:fs/node:path 仅用于发现前端 dist 静态目录(位于工作区外部),
|
|
2
2
|
// 已在 biome.json noRestrictedImports 中作为基础设施例外列出。
|
|
3
3
|
import { Buffer } from 'node:buffer';
|
|
4
|
-
import { existsSync } from 'node:fs';
|
|
4
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
5
5
|
import { readFile } from 'node:fs/promises';
|
|
6
6
|
import { createServer } from 'node:http';
|
|
7
|
+
import { createRequire } from 'node:module';
|
|
7
8
|
import { dirname, resolve } from 'node:path';
|
|
8
|
-
import { fileURLToPath } from 'node:url';
|
|
9
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
9
10
|
import { LogHub, parseLogLine } from '@aalis/core';
|
|
10
11
|
import { aggregatePlatformDetails, getPlatformAdapters, getPlatformNames, } from '@aalis/plugin-platform-api';
|
|
11
12
|
import { createProcessGateway } from '@aalis/plugin-process-api';
|
|
@@ -85,6 +86,12 @@ export const configSchema = {
|
|
|
85
86
|
default: 'https://registry.npmjs.org',
|
|
86
87
|
description: '插件市场检索用的 npm registry 基址。注意 npm 的 search API 并非所有镜像都支持(淘宝等国内源不支持),默认官方源;国内可填支持 search 的镜像或代理。安装走 package-manager(遵循本机 npm 配置)。',
|
|
87
88
|
},
|
|
89
|
+
client: {
|
|
90
|
+
type: 'string',
|
|
91
|
+
label: '活跃前端(多前端时选用)',
|
|
92
|
+
default: '',
|
|
93
|
+
description: '装了多个前端(aalis.client 包)时指定哪个为活跃前端,填包名(如 @aalis/plugin-webui-client)。留空 = 自动选第一个发现到的(默认前端优先)。插件主动 provide(webui-client) 仍优先于此。',
|
|
94
|
+
},
|
|
88
95
|
};
|
|
89
96
|
export const defaultConfig = {
|
|
90
97
|
port: 3000,
|
|
@@ -95,6 +102,7 @@ export const defaultConfig = {
|
|
|
95
102
|
fixedToken: '',
|
|
96
103
|
relationGraphDefaultSpacing: 120,
|
|
97
104
|
marketplaceRegistry: 'https://registry.npmjs.org',
|
|
105
|
+
client: '',
|
|
98
106
|
};
|
|
99
107
|
// ===== WebSocket 消息协议 =====
|
|
100
108
|
// 入站消息类型 + 校验 schema 见 ./protocol.ts(zod 强校验)
|
|
@@ -143,6 +151,7 @@ export async function apply(ctx, config) {
|
|
|
143
151
|
return Number.isFinite(v) && v > 0 ? v : 120;
|
|
144
152
|
})(),
|
|
145
153
|
marketplaceRegistry: config.marketplaceRegistry?.trim() || 'https://registry.npmjs.org',
|
|
154
|
+
client: config.client?.trim() ?? '',
|
|
146
155
|
};
|
|
147
156
|
// 创建 storage gateway;所有文件读写(token、access、文件管理)都走这里
|
|
148
157
|
const storage = createStorageGateway(ctx);
|
|
@@ -219,7 +228,6 @@ export async function apply(ctx, config) {
|
|
|
219
228
|
const authority = ctx.getService('authority');
|
|
220
229
|
res.json({
|
|
221
230
|
identity,
|
|
222
|
-
authority: authority?.getAuthority(identity.platform, identity.userId) ?? null,
|
|
223
231
|
isOwner: authority?.isOwner(identity.platform, identity.userId) ?? false,
|
|
224
232
|
});
|
|
225
233
|
});
|
|
@@ -277,7 +285,7 @@ export async function apply(ctx, config) {
|
|
|
277
285
|
});
|
|
278
286
|
// ---------- REST API ----------
|
|
279
287
|
// 获取系统状态
|
|
280
|
-
expressApp.get('/api/status', gate('webui:status:read',
|
|
288
|
+
expressApp.get('/api/status', gate('webui:status:read', 'public'), (_req, res) => {
|
|
281
289
|
const persona = ctx.getService('persona');
|
|
282
290
|
// 判断上传能力
|
|
283
291
|
const hasMedia = ctx.hasService('media');
|
|
@@ -310,8 +318,7 @@ export async function apply(ctx, config) {
|
|
|
310
318
|
.map(c => ({
|
|
311
319
|
name: c.name,
|
|
312
320
|
description: c.description,
|
|
313
|
-
|
|
314
|
-
safety: c.safety,
|
|
321
|
+
visibility: c.visibility,
|
|
315
322
|
})),
|
|
316
323
|
});
|
|
317
324
|
});
|
|
@@ -320,7 +327,7 @@ export async function apply(ctx, config) {
|
|
|
320
327
|
registerMarketplaceRoutes(expressApp, ctx, getPluginMgr, gate, uiConfig.marketplaceRegistry);
|
|
321
328
|
// 获取历史日志:从 data/latest.log 读尾部 N 条(lazy load)。
|
|
322
329
|
// 单进程内 LogHub 不再缓存 buffer——历史以文件为单一数据源。
|
|
323
|
-
expressApp.get('/api/logs', gate('webui:logs:read',
|
|
330
|
+
expressApp.get('/api/logs', gate('webui:logs:read', 'restricted'), async (_req, res) => {
|
|
324
331
|
try {
|
|
325
332
|
const entries = await readLogFileTail(200);
|
|
326
333
|
res.json(entries);
|
|
@@ -330,7 +337,7 @@ export async function apply(ctx, config) {
|
|
|
330
337
|
}
|
|
331
338
|
});
|
|
332
339
|
// 尾部 N 条(首屏 / 显式刷新)
|
|
333
|
-
expressApp.get('/api/logs/tail', gate('webui:logs:read',
|
|
340
|
+
expressApp.get('/api/logs/tail', gate('webui:logs:read', 'restricted'), async (req, res) => {
|
|
334
341
|
const limit = Math.min(Math.max(Number(req.query.limit) || 200, 1), 5000);
|
|
335
342
|
try {
|
|
336
343
|
res.json(await readLogFileTail(limit));
|
|
@@ -340,7 +347,7 @@ export async function apply(ctx, config) {
|
|
|
340
347
|
}
|
|
341
348
|
});
|
|
342
349
|
// 历史分页:返回 seq < before 的最近 limit 条(向上滚动加载更早)
|
|
343
|
-
expressApp.get('/api/logs/range', gate('webui:logs:read',
|
|
350
|
+
expressApp.get('/api/logs/range', gate('webui:logs:read', 'restricted'), async (req, res) => {
|
|
344
351
|
const before = Number(req.query.before);
|
|
345
352
|
const limit = Math.min(Math.max(Number(req.query.limit) || 200, 1), 5000);
|
|
346
353
|
if (!Number.isFinite(before)) {
|
|
@@ -355,7 +362,7 @@ export async function apply(ctx, config) {
|
|
|
355
362
|
}
|
|
356
363
|
});
|
|
357
364
|
// 获取服务列表(含提供者信息)
|
|
358
|
-
expressApp.get('/api/services', gate('webui:services:read',
|
|
365
|
+
expressApp.get('/api/services', gate('webui:services:read', 'public'), async (_req, res) => {
|
|
359
366
|
const pluginMgr = getPluginMgr();
|
|
360
367
|
const pluginStatus = pluginMgr ? pluginMgr.getStatus() : [];
|
|
361
368
|
const displayNameMap = new Map();
|
|
@@ -387,7 +394,7 @@ export async function apply(ctx, config) {
|
|
|
387
394
|
* 设置服务偏好。body: { contextId: string }
|
|
388
395
|
* 偏好语义:`preferred > priority > 注册顺序`。持久化到 aalis.config.yaml 的 servicePreferences。
|
|
389
396
|
*/
|
|
390
|
-
expressApp.post('/api/services/:name/prefer', gate('webui:services:manage', '
|
|
397
|
+
expressApp.post('/api/services/:name/prefer', gate('webui:services:manage', 'restricted'), async (req, res) => {
|
|
391
398
|
const svcName = String(req.params.name);
|
|
392
399
|
const contextId = String(req.body?.contextId ?? '').trim();
|
|
393
400
|
if (!contextId) {
|
|
@@ -406,7 +413,7 @@ export async function apply(ctx, config) {
|
|
|
406
413
|
res.json({ ok: true });
|
|
407
414
|
});
|
|
408
415
|
/** 清除服务偏好 */
|
|
409
|
-
expressApp.delete('/api/services/:name/prefer', gate('webui:services:manage', '
|
|
416
|
+
expressApp.delete('/api/services/:name/prefer', gate('webui:services:manage', 'restricted'), async (req, res) => {
|
|
410
417
|
const svcName = String(req.params.name);
|
|
411
418
|
ctx.unpreferService(svcName);
|
|
412
419
|
ctx.config.removeServicePreference(svcName);
|
|
@@ -414,11 +421,11 @@ export async function apply(ctx, config) {
|
|
|
414
421
|
res.json({ ok: true });
|
|
415
422
|
});
|
|
416
423
|
// 获取所有平台适配器及其连接状态
|
|
417
|
-
expressApp.get('/api/platforms', gate('webui:status:read',
|
|
424
|
+
expressApp.get('/api/platforms', gate('webui:status:read', 'public'), (_req, res) => {
|
|
418
425
|
res.json({ platforms: aggregatePlatformDetails(ctx) });
|
|
419
426
|
});
|
|
420
427
|
// 获取已注册的工具分组(含元数据 + 各组工具数量 + 贡献插件列表)
|
|
421
|
-
expressApp.get('/api/tool-groups', gate('webui:status:read',
|
|
428
|
+
expressApp.get('/api/tool-groups', gate('webui:status:read', 'public'), (_req, res) => {
|
|
422
429
|
const groups = ctx.getService('tools')?.getGroups() ?? [];
|
|
423
430
|
const allTools = ctx.getService('tools')?.getAll() ?? [];
|
|
424
431
|
const knownNames = new Set(groups.map(g => g.name));
|
|
@@ -447,7 +454,7 @@ export async function apply(ctx, config) {
|
|
|
447
454
|
res.json({ groups: result });
|
|
448
455
|
});
|
|
449
456
|
// 获取服务分组(manifest 驱动:每个插件自己声明 subsystem,本路由仅聚合)
|
|
450
|
-
expressApp.get('/api/service-groups', gate('webui:status:read',
|
|
457
|
+
expressApp.get('/api/service-groups', gate('webui:status:read', 'public'), (_req, res) => {
|
|
451
458
|
const pluginMgr = getPluginMgr();
|
|
452
459
|
const pluginStatus = pluginMgr ? pluginMgr.getStatus() : [];
|
|
453
460
|
// 按 plugin.subsystem 归组(未声明 → 'external')
|
|
@@ -487,7 +494,7 @@ export async function apply(ctx, config) {
|
|
|
487
494
|
res.json({ groups });
|
|
488
495
|
});
|
|
489
496
|
// 获取所有 LLM 模型(枚举所有注册的 per-model entry)
|
|
490
|
-
expressApp.get('/api/llm-models', gate('webui:llm:read',
|
|
497
|
+
expressApp.get('/api/llm-models', gate('webui:llm:read', 'restricted'), async (_req, res) => {
|
|
491
498
|
try {
|
|
492
499
|
const entries = ctx.getAllServices('llm');
|
|
493
500
|
const models = entries.map(e => ({
|
|
@@ -503,7 +510,7 @@ export async function apply(ctx, config) {
|
|
|
503
510
|
}
|
|
504
511
|
});
|
|
505
512
|
// LLM providers + per-provider models(供 schema type='llm-ref' 联动 select 使用)
|
|
506
|
-
expressApp.get('/api/llm-providers', gate('webui:llm:read',
|
|
513
|
+
expressApp.get('/api/llm-providers', gate('webui:llm:read', 'restricted'), async (_req, res) => {
|
|
507
514
|
try {
|
|
508
515
|
const entries = ctx.getAllServices('llm');
|
|
509
516
|
const byProvider = new Map();
|
|
@@ -540,7 +547,7 @@ export async function apply(ctx, config) {
|
|
|
540
547
|
// 触发指定 provider 重新探测远端模型列表(用于 webui 上的"刷新模型"按钮)
|
|
541
548
|
// 仅对在 LLMModel 上实现了 refresh() 的 provider 生效(远端动态发现型,如 Ollama / OpenAI)。
|
|
542
549
|
// 同 provider 下所有 model entries 共享同一份 refresh 闭包,调任一个 entry 即可。
|
|
543
|
-
expressApp.post('/api/llm-providers/:contextId/refresh', gate('webui:llm:manage', '
|
|
550
|
+
expressApp.post('/api/llm-providers/:contextId/refresh', gate('webui:llm:manage', 'restricted'), async (req, res) => {
|
|
544
551
|
const contextId = req.params.contextId;
|
|
545
552
|
if (!contextId) {
|
|
546
553
|
res.status(400).json({ error: 'contextId is required' });
|
|
@@ -563,7 +570,7 @@ export async function apply(ctx, config) {
|
|
|
563
570
|
}
|
|
564
571
|
});
|
|
565
572
|
// 获取某个服务的可用模型/选项列表
|
|
566
|
-
expressApp.get('/api/models/:service', gate('webui:models:read',
|
|
573
|
+
expressApp.get('/api/models/:service', gate('webui:models:read', 'public'), async (req, res) => {
|
|
567
574
|
const serviceName = req.params.service;
|
|
568
575
|
// 特殊处理 platform:通过 helper 获取已注册的平台名称
|
|
569
576
|
if (serviceName === 'platform') {
|
|
@@ -687,13 +694,13 @@ export async function apply(ctx, config) {
|
|
|
687
694
|
res.json({ models: [] });
|
|
688
695
|
}
|
|
689
696
|
});
|
|
690
|
-
// ----------
|
|
697
|
+
// ---------- 受限操作交互式确认(内联对话式;restricted 能力的临时委托) ----------
|
|
691
698
|
const CONFIRM_TIMEOUT = 60_000; // 60 秒超时
|
|
692
699
|
const pendingSessionConfirms = new Map();
|
|
693
700
|
ctx.getService('authority')?.setConfirmHandler('webui', async (request) => {
|
|
694
701
|
const typeLabel = request.type === 'command' ? '指令' : '工具';
|
|
695
702
|
const nameStr = request.type === 'command' ? `/${request.name}` : request.name;
|
|
696
|
-
const prompt = `⚠️ ${typeLabel} ${nameStr}
|
|
703
|
+
const prompt = `⚠️ ${typeLabel} ${nameStr} 是受限操作。回复 Y 仅允许本次;回复 YS 允许本会话 10 分钟;其他任意输入取消。`;
|
|
697
704
|
// 以确认消息形式发送(不影响客户端 loading/streaming 状态)
|
|
698
705
|
const payload = {
|
|
699
706
|
type: 'confirm',
|
|
@@ -726,7 +733,7 @@ export async function apply(ctx, config) {
|
|
|
726
733
|
// 超时后向会话发送提示
|
|
727
734
|
const timeoutPayload = {
|
|
728
735
|
type: 'confirm',
|
|
729
|
-
content: '⏰
|
|
736
|
+
content: '⏰ 受限操作确认已超时,已自动取消。',
|
|
730
737
|
sessionId: request.sessionId,
|
|
731
738
|
};
|
|
732
739
|
const timeoutJson = JSON.stringify(timeoutPayload);
|
|
@@ -836,7 +843,7 @@ export async function apply(ctx, config) {
|
|
|
836
843
|
sessions.set(sessionId, new Set());
|
|
837
844
|
}
|
|
838
845
|
sessions.get(sessionId).add(ws);
|
|
839
|
-
//
|
|
846
|
+
// 检查是否有待确认的受限操作(拦截用户输入作为确认/取消)
|
|
840
847
|
const pending = pendingSessionConfirms.get(sessionId);
|
|
841
848
|
if (pending) {
|
|
842
849
|
clearTimeout(pending.timer);
|
|
@@ -1182,39 +1189,68 @@ export async function apply(ctx, config) {
|
|
|
1182
1189
|
});
|
|
1183
1190
|
// 启动服务器
|
|
1184
1191
|
ctx.on('ready', () => {
|
|
1185
|
-
//
|
|
1192
|
+
// 自动发现 webui-client 包并注册为 webui-client 服务提供者。
|
|
1193
|
+
// 解析以「项目根 package.json」为基准(与 runtime 的 node-modules 加载器同源),
|
|
1194
|
+
// 这样 npm 扁平 / pnpm 隔离 / monorepo 软链三种 node_modules 拓扑都自洽——
|
|
1195
|
+
// client 是项目顶层依赖(create-aalis 选 WebUI 时自动带),从项目根能 resolve 到。
|
|
1196
|
+
// 回退:webui-server 同级目录(cwd≠项目根等边角场景兜底)。
|
|
1186
1197
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
1187
|
-
const
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1198
|
+
const projectRequire = createRequire(pathToFileURL(resolve(process.cwd(), 'package.json')));
|
|
1199
|
+
const resolveClientDir = (id) => {
|
|
1200
|
+
try {
|
|
1201
|
+
return resolve(dirname(projectRequire.resolve(`${id}/package.json`)), 'dist');
|
|
1202
|
+
}
|
|
1203
|
+
catch {
|
|
1204
|
+
const sibling = resolve(__dirname, `../../${id.replace('@aalis/', '')}/dist`);
|
|
1205
|
+
return existsSync(sibling) ? sibling : undefined;
|
|
1206
|
+
}
|
|
1207
|
+
};
|
|
1208
|
+
const clientCandidates = [];
|
|
1209
|
+
const addCandidate = (id, label, dir) => {
|
|
1210
|
+
if (!dir || clientCandidates.some(c => c.id === id))
|
|
1211
|
+
return;
|
|
1212
|
+
if (existsSync(dir) && existsSync(resolve(dir, 'index.html')))
|
|
1213
|
+
clientCandidates.push({ id, label, dir });
|
|
1214
|
+
};
|
|
1215
|
+
// ① 已知 @aalis 前端(保证 monorepo/默认拓扑可用——根 deps 未必列 client)
|
|
1216
|
+
addCandidate('@aalis/plugin-webui-client', 'Aalis 默认前端', resolveClientDir('@aalis/plugin-webui-client'));
|
|
1217
|
+
addCandidate('@aalis/plugin-webui-client-napcat', 'NapCat 前端', resolveClientDir('@aalis/plugin-webui-client-napcat'));
|
|
1218
|
+
// ② 追加发现:项目 deps 里任何带 aalis.client marker 的第三方前端(忒修斯之船可换)
|
|
1219
|
+
try {
|
|
1220
|
+
const rootPkg = JSON.parse(readFileSync(resolve(process.cwd(), 'package.json'), 'utf-8'));
|
|
1221
|
+
const deps = Object.keys({ ...rootPkg.dependencies, ...rootPkg.optionalDependencies });
|
|
1222
|
+
for (const dep of deps) {
|
|
1223
|
+
if (clientCandidates.some(c => c.id === dep))
|
|
1224
|
+
continue;
|
|
1225
|
+
try {
|
|
1226
|
+
const pkgPath = projectRequire.resolve(`${dep}/package.json`);
|
|
1227
|
+
const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
|
|
1228
|
+
if (pkg?.aalis?.client !== true)
|
|
1229
|
+
continue;
|
|
1230
|
+
addCandidate(dep, pkg.displayName ?? pkg.description ?? dep, resolve(dirname(pkgPath), 'dist'));
|
|
1231
|
+
}
|
|
1232
|
+
catch {
|
|
1233
|
+
/* 该 dep 无法解析/读取,跳过 */
|
|
1234
|
+
}
|
|
1235
|
+
}
|
|
1236
|
+
}
|
|
1237
|
+
catch {
|
|
1238
|
+
/* 无项目根 package.json(如 monorepo 直跑),跳过 dep 扫描 */
|
|
1239
|
+
}
|
|
1240
|
+
// 如果已有外部插件主动注册了 webui-client 服务(apply 覆盖路径),则跳过自动发现
|
|
1200
1241
|
const hasExternalClient = ctx.hasService('webui-client');
|
|
1201
|
-
if (!hasExternalClient) {
|
|
1202
|
-
|
|
1242
|
+
if (!hasExternalClient && clientCandidates.length > 0) {
|
|
1243
|
+
// 活跃前端:config.client 指定者优先,否则取第一个(默认前端排在最前)
|
|
1244
|
+
const preferred = uiConfig.client && clientCandidates.find(c => c.id === uiConfig.client);
|
|
1245
|
+
const active = preferred || clientCandidates[0];
|
|
1203
1246
|
for (const candidate of clientCandidates) {
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
getClientDir: () => candidate.dir,
|
|
1208
|
-
}, { label: candidate.label });
|
|
1209
|
-
ctx.logger.info(`发现前端: ${candidate.label} (${candidate.dir})`);
|
|
1210
|
-
if (isFirst) {
|
|
1211
|
-
clientDist = candidate.dir;
|
|
1212
|
-
mountStaticDir(candidate.dir);
|
|
1213
|
-
ctx.logger.info(`活跃前端: ${candidate.label}`);
|
|
1214
|
-
isFirst = false;
|
|
1215
|
-
}
|
|
1216
|
-
}
|
|
1247
|
+
const childCtx = ctx.fork(candidate.id);
|
|
1248
|
+
childCtx.provide('webui-client', { getClientDir: () => candidate.dir }, { label: candidate.label });
|
|
1249
|
+
ctx.logger.info(`发现前端: ${candidate.label} (${candidate.dir})`);
|
|
1217
1250
|
}
|
|
1251
|
+
clientDist = active.dir;
|
|
1252
|
+
mountStaticDir(active.dir);
|
|
1253
|
+
ctx.logger.info(`活跃前端: ${active.label}${preferred ? '(config.client 指定)' : ''}`);
|
|
1218
1254
|
}
|
|
1219
1255
|
// 若已有外部 webui-client 服务,使用其提供的目录
|
|
1220
1256
|
if (hasExternalClient) {
|