mioku-service-webui 4.4.5 → 4.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/package.json +1 -1
- package/routes/ai.ts +247 -19
package/package.json
CHANGED
package/routes/ai.ts
CHANGED
|
@@ -3,7 +3,7 @@ import { logger } from "mioki";
|
|
|
3
3
|
import { getChatConfig, updateChatConfig } from "../system";
|
|
4
4
|
import { listStickerCatalog } from "../database";
|
|
5
5
|
import aiService from "mioku-service-ai";
|
|
6
|
-
import type { AIUsageRange } from "mioku
|
|
6
|
+
import type { AIModelRole, AIProtocol, AIUsageRange } from "mioku";
|
|
7
7
|
|
|
8
8
|
const DEFAULT_MEMORY_GROUP_HISTORY_LIMIT = 300;
|
|
9
9
|
const DEFAULT_MEMORY_USER_HISTORY_LIMIT = 100;
|
|
@@ -12,6 +12,17 @@ const DEFAULT_TOPIC_HISTORY_WINDOW_COUNT = 3;
|
|
|
12
12
|
const DEFAULT_EXPRESSION_LEARN_AFTER_MESSAGES = 100;
|
|
13
13
|
const DEFAULT_EXPRESSION_SAMPLE_SIZE = 8;
|
|
14
14
|
const usageRanges = new Set<AIUsageRange>(["today", "7d", "30d"]);
|
|
15
|
+
const protocols = new Set<AIProtocol>([
|
|
16
|
+
"openai-chat",
|
|
17
|
+
"openai-response",
|
|
18
|
+
"anthropic",
|
|
19
|
+
"gemini",
|
|
20
|
+
]);
|
|
21
|
+
const roles = new Set<AIModelRole>(["main", "working", "vision"]);
|
|
22
|
+
|
|
23
|
+
function getAIApi() {
|
|
24
|
+
return aiService?.api as any;
|
|
25
|
+
}
|
|
15
26
|
|
|
16
27
|
function normalizePersonalizationConfig(input: any): any {
|
|
17
28
|
const data =
|
|
@@ -122,14 +133,48 @@ function normalizePersonalizationConfig(input: any): any {
|
|
|
122
133
|
return data;
|
|
123
134
|
}
|
|
124
135
|
|
|
136
|
+
function sanitizeBaseForSave(body: any): any {
|
|
137
|
+
if (!body || typeof body !== "object") return body;
|
|
138
|
+
const next = { ...body };
|
|
139
|
+
delete next.apiUrl;
|
|
140
|
+
delete next.apiKey;
|
|
141
|
+
delete next.model;
|
|
142
|
+
delete next.workingModel;
|
|
143
|
+
delete next.multimodalWorkingModel;
|
|
144
|
+
delete next.isMultimodal;
|
|
145
|
+
return next;
|
|
146
|
+
}
|
|
147
|
+
|
|
125
148
|
export function createAIRoutes() {
|
|
126
149
|
const app = new Hono();
|
|
127
150
|
|
|
128
|
-
app.get("/base", (c) =>
|
|
129
|
-
|
|
130
|
-
|
|
151
|
+
app.get("/base", (c) => {
|
|
152
|
+
const data = getChatConfig("base.json") || {};
|
|
153
|
+
const api = getAIApi();
|
|
154
|
+
const rolesData = api?.getRoleBindings?.() ?? {};
|
|
155
|
+
const models = api?.listModels?.() ?? [];
|
|
156
|
+
const pickModelId = (full?: string) => {
|
|
157
|
+
if (!full) return "";
|
|
158
|
+
const idx = full.indexOf("/");
|
|
159
|
+
return idx > 0 ? full.slice(idx + 1) : full;
|
|
160
|
+
};
|
|
161
|
+
const vision = models.find((item: any) => item.id === rolesData.vision);
|
|
162
|
+
return c.json({
|
|
163
|
+
ok: true,
|
|
164
|
+
data: {
|
|
165
|
+
...data,
|
|
166
|
+
model: pickModelId(rolesData.main) || data.model || "",
|
|
167
|
+
workingModel: pickModelId(rolesData.working) || data.workingModel || "",
|
|
168
|
+
multimodalWorkingModel:
|
|
169
|
+
pickModelId(rolesData.vision) || data.multimodalWorkingModel || "",
|
|
170
|
+
isMultimodal: vision?.capabilities?.includes?.("vision") ?? true,
|
|
171
|
+
providersManaged: true,
|
|
172
|
+
},
|
|
173
|
+
});
|
|
174
|
+
});
|
|
175
|
+
|
|
131
176
|
app.put("/base", async (c) => {
|
|
132
|
-
const body = await c.req.json();
|
|
177
|
+
const body = sanitizeBaseForSave(await c.req.json());
|
|
133
178
|
logger.info(`[webui-action] ai.base.update`);
|
|
134
179
|
return c.json({ ok: true, data: updateChatConfig("base.json", body) });
|
|
135
180
|
});
|
|
@@ -161,48 +206,230 @@ export function createAIRoutes() {
|
|
|
161
206
|
});
|
|
162
207
|
});
|
|
163
208
|
|
|
209
|
+
app.get("/providers", (c) => {
|
|
210
|
+
const api = getAIApi();
|
|
211
|
+
if (!api?.listProviders) {
|
|
212
|
+
return c.json({ ok: false, error: "AI_SERVICE_UNAVAILABLE" }, 503);
|
|
213
|
+
}
|
|
214
|
+
const providers = api.listProviders().map((item: any) => ({
|
|
215
|
+
...item,
|
|
216
|
+
apiKey: item.apiKey ? "***" : "",
|
|
217
|
+
hasApiKey: Boolean(item.apiKey),
|
|
218
|
+
}));
|
|
219
|
+
return c.json({ ok: true, data: providers });
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
app.post("/providers", async (c) => {
|
|
223
|
+
const api = getAIApi();
|
|
224
|
+
if (!api?.createProvider) {
|
|
225
|
+
return c.json({ ok: false, error: "AI_SERVICE_UNAVAILABLE" }, 503);
|
|
226
|
+
}
|
|
227
|
+
const body = await c.req.json();
|
|
228
|
+
const protocol = String(body?.protocol || "openai-chat") as AIProtocol;
|
|
229
|
+
if (!protocols.has(protocol)) {
|
|
230
|
+
return c.json({ ok: false, error: "INVALID_PROTOCOL" }, 400);
|
|
231
|
+
}
|
|
232
|
+
logger.info(`[webui-action] ai.provider.create`, {
|
|
233
|
+
name: body?.name,
|
|
234
|
+
protocol,
|
|
235
|
+
});
|
|
236
|
+
const created = await api.createProvider({
|
|
237
|
+
id: body?.id,
|
|
238
|
+
name: body?.name,
|
|
239
|
+
protocol,
|
|
240
|
+
apiUrl: body?.apiUrl,
|
|
241
|
+
apiKey: body?.apiKey,
|
|
242
|
+
enabled: body?.enabled !== false,
|
|
243
|
+
headers: body?.headers,
|
|
244
|
+
});
|
|
245
|
+
return c.json({
|
|
246
|
+
ok: true,
|
|
247
|
+
data: {
|
|
248
|
+
...created,
|
|
249
|
+
apiKey: created.apiKey ? "***" : "",
|
|
250
|
+
hasApiKey: Boolean(created.apiKey),
|
|
251
|
+
},
|
|
252
|
+
});
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
app.put("/providers/:id", async (c) => {
|
|
256
|
+
const api = getAIApi();
|
|
257
|
+
if (!api?.updateProvider) {
|
|
258
|
+
return c.json({ ok: false, error: "AI_SERVICE_UNAVAILABLE" }, 503);
|
|
259
|
+
}
|
|
260
|
+
const id = c.req.param("id");
|
|
261
|
+
const body = await c.req.json();
|
|
262
|
+
logger.info(`[webui-action] ai.provider.update`, { id });
|
|
263
|
+
const patch: Record<string, unknown> = { ...body };
|
|
264
|
+
if (patch.apiKey === "***" || patch.apiKey === "") delete patch.apiKey;
|
|
265
|
+
if (patch.protocol && !protocols.has(patch.protocol as AIProtocol)) {
|
|
266
|
+
return c.json({ ok: false, error: "INVALID_PROTOCOL" }, 400);
|
|
267
|
+
}
|
|
268
|
+
const updated = await api.updateProvider(id, patch);
|
|
269
|
+
return c.json({
|
|
270
|
+
ok: true,
|
|
271
|
+
data: {
|
|
272
|
+
...updated,
|
|
273
|
+
apiKey: updated.apiKey ? "***" : "",
|
|
274
|
+
hasApiKey: Boolean(updated.apiKey),
|
|
275
|
+
},
|
|
276
|
+
});
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
app.delete("/providers/:id", (c) => {
|
|
280
|
+
const api = getAIApi();
|
|
281
|
+
if (!api?.removeProvider) {
|
|
282
|
+
return c.json({ ok: false, error: "AI_SERVICE_UNAVAILABLE" }, 503);
|
|
283
|
+
}
|
|
284
|
+
const id = c.req.param("id");
|
|
285
|
+
logger.info(`[webui-action] ai.provider.remove`, { id });
|
|
286
|
+
return c.json({ ok: Boolean(api.removeProvider(id)) });
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
app.post("/providers/:id/test", async (c) => {
|
|
290
|
+
const api = getAIApi();
|
|
291
|
+
if (!api?.testProvider) {
|
|
292
|
+
return c.json({ ok: false, error: "AI_SERVICE_UNAVAILABLE" }, 503);
|
|
293
|
+
}
|
|
294
|
+
const id = c.req.param("id");
|
|
295
|
+
logger.info(`[webui-action] ai.provider.test`, { id });
|
|
296
|
+
const result = await api.testProvider(id);
|
|
297
|
+
return c.json({ ok: true, data: result });
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
app.post("/providers/:id/models/refresh", async (c) => {
|
|
301
|
+
const api = getAIApi();
|
|
302
|
+
if (!api?.refreshModels) {
|
|
303
|
+
return c.json({ ok: false, error: "AI_SERVICE_UNAVAILABLE" }, 503);
|
|
304
|
+
}
|
|
305
|
+
const id = c.req.param("id");
|
|
306
|
+
logger.info(`[webui-action] ai.models.refresh`, { id });
|
|
307
|
+
const models = await api.refreshModels(id);
|
|
308
|
+
return c.json({ ok: true, data: models });
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
app.get("/models", (c) => {
|
|
312
|
+
const api = getAIApi();
|
|
313
|
+
if (!api?.listModels) {
|
|
314
|
+
return c.json({ ok: false, error: "AI_SERVICE_UNAVAILABLE" }, 503);
|
|
315
|
+
}
|
|
316
|
+
const providerId = c.req.query("providerId") || undefined;
|
|
317
|
+
return c.json({ ok: true, data: api.listModels(providerId) });
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
app.post("/models", async (c) => {
|
|
321
|
+
const api = getAIApi();
|
|
322
|
+
if (!api?.registerCustomModel) {
|
|
323
|
+
return c.json({ ok: false, error: "AI_SERVICE_UNAVAILABLE" }, 503);
|
|
324
|
+
}
|
|
325
|
+
const body = await c.req.json();
|
|
326
|
+
logger.info(`[webui-action] ai.model.create`, {
|
|
327
|
+
providerId: body?.providerId,
|
|
328
|
+
modelId: body?.modelId,
|
|
329
|
+
});
|
|
330
|
+
const model = api.registerCustomModel({
|
|
331
|
+
providerId: body?.providerId,
|
|
332
|
+
modelId: body?.modelId,
|
|
333
|
+
name: body?.name,
|
|
334
|
+
capabilities: body?.capabilities,
|
|
335
|
+
});
|
|
336
|
+
return c.json({ ok: true, data: model });
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
app.delete("/models/:id", (c) => {
|
|
340
|
+
const api = getAIApi();
|
|
341
|
+
if (!api?.removeCustomModel) {
|
|
342
|
+
return c.json({ ok: false, error: "AI_SERVICE_UNAVAILABLE" }, 503);
|
|
343
|
+
}
|
|
344
|
+
const id = decodeURIComponent(c.req.param("id"));
|
|
345
|
+
logger.info(`[webui-action] ai.model.remove`, { id });
|
|
346
|
+
return c.json({ ok: Boolean(api.removeCustomModel(id)) });
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
app.get("/roles", (c) => {
|
|
350
|
+
const api = getAIApi();
|
|
351
|
+
if (!api?.getRoleBindings) {
|
|
352
|
+
return c.json({ ok: false, error: "AI_SERVICE_UNAVAILABLE" }, 503);
|
|
353
|
+
}
|
|
354
|
+
return c.json({ ok: true, data: api.getRoleBindings() });
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
app.put("/roles", async (c) => {
|
|
358
|
+
const api = getAIApi();
|
|
359
|
+
if (!api?.setRoleBinding) {
|
|
360
|
+
return c.json({ ok: false, error: "AI_SERVICE_UNAVAILABLE" }, 503);
|
|
361
|
+
}
|
|
362
|
+
const body = await c.req.json();
|
|
363
|
+
logger.info(`[webui-action] ai.roles.update`, body);
|
|
364
|
+
for (const role of ["main", "working", "vision"] as AIModelRole[]) {
|
|
365
|
+
if (!(role in (body || {}))) continue;
|
|
366
|
+
const value = body[role];
|
|
367
|
+
if (value === null || value === undefined || value === "") {
|
|
368
|
+
api.setRoleBinding(role, undefined);
|
|
369
|
+
} else {
|
|
370
|
+
api.setRoleBinding(role, String(value));
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
return c.json({ ok: true, data: api.getRoleBindings() });
|
|
374
|
+
});
|
|
375
|
+
|
|
164
376
|
app.get("/instances", (c) => {
|
|
165
|
-
const
|
|
166
|
-
|
|
377
|
+
const api = getAIApi();
|
|
378
|
+
if (api?.listInstances) {
|
|
379
|
+
return c.json({ ok: true, data: api.listInstances() });
|
|
380
|
+
}
|
|
381
|
+
const names = api?.list?.() ?? [];
|
|
382
|
+
return c.json({ ok: true, data: names.map((name: string) => ({ name })) });
|
|
167
383
|
});
|
|
168
384
|
|
|
169
385
|
app.post("/instances", async (c) => {
|
|
170
386
|
const body = await c.req.json();
|
|
171
387
|
logger.info(`[webui-action] ai.instance.create`, {
|
|
172
388
|
name: body?.name,
|
|
173
|
-
|
|
174
|
-
|
|
389
|
+
providerId: body?.providerId,
|
|
390
|
+
modelId: body?.modelId,
|
|
175
391
|
});
|
|
176
|
-
|
|
392
|
+
const api = getAIApi();
|
|
393
|
+
if (api?.createInstance && body?.providerId && body?.modelId) {
|
|
394
|
+
await api.createInstance({
|
|
395
|
+
name: body.name,
|
|
396
|
+
providerId: body.providerId,
|
|
397
|
+
modelId: body.modelId,
|
|
398
|
+
role: roles.has(body.role) ? body.role : undefined,
|
|
399
|
+
});
|
|
400
|
+
return c.json({ ok: true, data: api.listInstances?.() ?? api.list?.() });
|
|
401
|
+
}
|
|
402
|
+
if (!api?.create) {
|
|
177
403
|
return c.json({ ok: false, error: "AI_SERVICE_UNAVAILABLE" }, 503);
|
|
178
404
|
}
|
|
179
|
-
|
|
180
|
-
await aiService.api.create({
|
|
405
|
+
await api.create({
|
|
181
406
|
name: body.name,
|
|
182
407
|
apiUrl: body.apiUrl,
|
|
183
408
|
apiKey: body.apiKey,
|
|
184
409
|
modelType: body.modelType || "text",
|
|
410
|
+
model: body.model,
|
|
185
411
|
});
|
|
186
|
-
return c.json({ ok: true, data:
|
|
412
|
+
return c.json({ ok: true, data: api.list() });
|
|
187
413
|
});
|
|
188
414
|
|
|
189
415
|
app.delete("/instances/:name", (c) => {
|
|
190
416
|
const name = c.req.param("name");
|
|
191
417
|
logger.info(`[webui-action] ai.instance.remove`, { name });
|
|
192
|
-
const ok =
|
|
418
|
+
const ok = getAIApi()?.remove?.(name);
|
|
193
419
|
return c.json({ ok: Boolean(ok) });
|
|
194
420
|
});
|
|
195
421
|
|
|
196
422
|
app.post("/default/:name", (c) => {
|
|
197
423
|
const name = c.req.param("name");
|
|
198
424
|
logger.info(`[webui-action] ai.instance.set-default`, { name });
|
|
199
|
-
const ok =
|
|
425
|
+
const ok = getAIApi()?.setDefault?.(name);
|
|
200
426
|
return c.json({ ok: Boolean(ok) });
|
|
201
427
|
});
|
|
202
428
|
|
|
203
429
|
app.get("/skills", (c) => {
|
|
204
|
-
const
|
|
205
|
-
const
|
|
430
|
+
const api = getAIApi();
|
|
431
|
+
const skills = api?.getAllSkills?.();
|
|
432
|
+
const tools = api?.getAllTools?.();
|
|
206
433
|
return c.json({
|
|
207
434
|
ok: true,
|
|
208
435
|
data: {
|
|
@@ -217,7 +444,8 @@ export function createAIRoutes() {
|
|
|
217
444
|
);
|
|
218
445
|
|
|
219
446
|
app.get("/usage", (c) => {
|
|
220
|
-
|
|
447
|
+
const api = getAIApi();
|
|
448
|
+
if (!api?.getUsageSummary) {
|
|
221
449
|
return c.json({ ok: false, error: "AI_USAGE_UNAVAILABLE" }, 503);
|
|
222
450
|
}
|
|
223
451
|
|
|
@@ -236,7 +464,7 @@ export function createAIRoutes() {
|
|
|
236
464
|
|
|
237
465
|
return c.json({
|
|
238
466
|
ok: true,
|
|
239
|
-
data:
|
|
467
|
+
data: api.getUsageSummary({ range, botId }),
|
|
240
468
|
});
|
|
241
469
|
});
|
|
242
470
|
|