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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/routes/ai.ts +247 -19
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mioku-service-webui",
3
- "version": "4.4.5",
3
+ "version": "4.5.0",
4
4
  "description": "Mioku WebUI 服务",
5
5
  "main": "index.ts",
6
6
  "type": "module",
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-service-ai/usage/types";
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
- c.json({ ok: true, data: getChatConfig("base.json") }),
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 names = aiService?.api?.list?.() ?? [];
166
- return c.json({ ok: true, data: names });
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
- apiUrl: body?.apiUrl,
174
- modelType: body?.modelType,
389
+ providerId: body?.providerId,
390
+ modelId: body?.modelId,
175
391
  });
176
- if (!aiService?.api?.create) {
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: aiService.api.list() });
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 = aiService?.api?.remove?.(name);
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 = aiService?.api?.setDefault?.(name);
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 skills = aiService?.api?.getAllSkills?.();
205
- const tools = aiService?.api?.getAllTools?.();
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
- if (!aiService?.api?.getUsageSummary) {
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: aiService.api.getUsageSummary({ range, botId }),
467
+ data: api.getUsageSummary({ range, botId }),
240
468
  });
241
469
  });
242
470