mioku-service-webui 4.4.4 → 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/database.ts +42 -0
- package/package.json +1 -1
- package/routes/ai.ts +274 -19
package/database.ts
CHANGED
|
@@ -5,6 +5,14 @@ import { CHAT_DATA_DIR, readJsonFile, writeJsonFile } from "./utils";
|
|
|
5
5
|
|
|
6
6
|
const DB_PATH = path.join(CHAT_DATA_DIR, "chat.db");
|
|
7
7
|
const SAFE_IDENTIFIER = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
8
|
+
const STICKER_FILE_PATTERN = /\.(png|jpg|jpeg|gif|webp)$/i;
|
|
9
|
+
|
|
10
|
+
export interface StickerCatalogItem {
|
|
11
|
+
id: string;
|
|
12
|
+
character: string;
|
|
13
|
+
label: string;
|
|
14
|
+
url: string;
|
|
15
|
+
}
|
|
8
16
|
|
|
9
17
|
function openDb(): any {
|
|
10
18
|
if (!fs.existsSync(DB_PATH)) {
|
|
@@ -299,6 +307,40 @@ export function listMemeTree(): Record<string, Record<string, string[]>> {
|
|
|
299
307
|
return tree;
|
|
300
308
|
}
|
|
301
309
|
|
|
310
|
+
export function listStickerCatalog(): StickerCatalogItem[] {
|
|
311
|
+
const root = path.join(CHAT_DATA_DIR, "meme");
|
|
312
|
+
if (!fs.existsSync(root)) return [];
|
|
313
|
+
|
|
314
|
+
const items: StickerCatalogItem[] = [];
|
|
315
|
+
const visit = (dir: string, character: string) => {
|
|
316
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
317
|
+
const entryPath = path.join(dir, entry.name);
|
|
318
|
+
if (entry.isDirectory()) {
|
|
319
|
+
visit(entryPath, character);
|
|
320
|
+
continue;
|
|
321
|
+
}
|
|
322
|
+
if (!entry.isFile() || !STICKER_FILE_PATTERN.test(entry.name)) continue;
|
|
323
|
+
|
|
324
|
+
const id = path.relative(root, entryPath).split(path.sep).join("/");
|
|
325
|
+
const label = path
|
|
326
|
+
.basename(entry.name, path.extname(entry.name))
|
|
327
|
+
.replace(/[_-]+/g, " ")
|
|
328
|
+
.trim();
|
|
329
|
+
const url = `/meme/${id
|
|
330
|
+
.split("/")
|
|
331
|
+
.map((segment) => encodeURIComponent(segment))
|
|
332
|
+
.join("/")}`;
|
|
333
|
+
items.push({ id, character, label, url });
|
|
334
|
+
}
|
|
335
|
+
};
|
|
336
|
+
|
|
337
|
+
for (const entry of fs.readdirSync(root, { withFileTypes: true })) {
|
|
338
|
+
if (entry.isDirectory()) visit(path.join(root, entry.name), entry.name);
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
return items.sort((a, b) => a.id.localeCompare(b.id));
|
|
342
|
+
}
|
|
343
|
+
|
|
302
344
|
export function savePluginConfig(pluginName: string, fileName: string, value: any): void {
|
|
303
345
|
const filePath = path.join(process.cwd(), "config", pluginName, `${fileName}.json`);
|
|
304
346
|
writeJsonFile(filePath, value);
|
package/package.json
CHANGED
package/routes/ai.ts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { Hono } from "hono";
|
|
2
2
|
import { logger } from "mioki";
|
|
3
3
|
import { getChatConfig, updateChatConfig } from "../system";
|
|
4
|
+
import { listStickerCatalog } from "../database";
|
|
4
5
|
import aiService from "mioku-service-ai";
|
|
5
|
-
import type { AIUsageRange } from "mioku
|
|
6
|
+
import type { AIModelRole, AIProtocol, AIUsageRange } from "mioku";
|
|
6
7
|
|
|
7
8
|
const DEFAULT_MEMORY_GROUP_HISTORY_LIMIT = 300;
|
|
8
9
|
const DEFAULT_MEMORY_USER_HISTORY_LIMIT = 100;
|
|
@@ -11,6 +12,17 @@ const DEFAULT_TOPIC_HISTORY_WINDOW_COUNT = 3;
|
|
|
11
12
|
const DEFAULT_EXPRESSION_LEARN_AFTER_MESSAGES = 100;
|
|
12
13
|
const DEFAULT_EXPRESSION_SAMPLE_SIZE = 8;
|
|
13
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
|
+
}
|
|
14
26
|
|
|
15
27
|
function normalizePersonalizationConfig(input: any): any {
|
|
16
28
|
const data =
|
|
@@ -96,17 +108,73 @@ function normalizePersonalizationConfig(input: any): any {
|
|
|
96
108
|
delete rawExpression.maxExpressions;
|
|
97
109
|
|
|
98
110
|
data.expression = rawExpression;
|
|
111
|
+
|
|
112
|
+
const rawEmoji =
|
|
113
|
+
data.emoji && typeof data.emoji === "object" && !Array.isArray(data.emoji)
|
|
114
|
+
? { ...data.emoji }
|
|
115
|
+
: {};
|
|
116
|
+
rawEmoji.enabled =
|
|
117
|
+
typeof rawEmoji.enabled === "boolean" ? rawEmoji.enabled : true;
|
|
118
|
+
rawEmoji.characters = Array.isArray(rawEmoji.characters)
|
|
119
|
+
? rawEmoji.characters
|
|
120
|
+
.map(String)
|
|
121
|
+
.map((value: string) => value.trim())
|
|
122
|
+
.filter(Boolean)
|
|
123
|
+
: [];
|
|
124
|
+
rawEmoji.stickers = Array.isArray(rawEmoji.stickers)
|
|
125
|
+
? rawEmoji.stickers
|
|
126
|
+
.map(String)
|
|
127
|
+
.map((value: string) => value.trim())
|
|
128
|
+
.filter(Boolean)
|
|
129
|
+
: [];
|
|
130
|
+
delete rawEmoji.useAISelection;
|
|
131
|
+
data.emoji = rawEmoji;
|
|
132
|
+
|
|
99
133
|
return data;
|
|
100
134
|
}
|
|
101
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
|
+
|
|
102
148
|
export function createAIRoutes() {
|
|
103
149
|
const app = new Hono();
|
|
104
150
|
|
|
105
|
-
app.get("/base", (c) =>
|
|
106
|
-
|
|
107
|
-
|
|
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
|
+
|
|
108
176
|
app.put("/base", async (c) => {
|
|
109
|
-
const body = await c.req.json();
|
|
177
|
+
const body = sanitizeBaseForSave(await c.req.json());
|
|
110
178
|
logger.info(`[webui-action] ai.base.update`);
|
|
111
179
|
return c.json({ ok: true, data: updateChatConfig("base.json", body) });
|
|
112
180
|
});
|
|
@@ -138,48 +206,230 @@ export function createAIRoutes() {
|
|
|
138
206
|
});
|
|
139
207
|
});
|
|
140
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
|
+
|
|
141
376
|
app.get("/instances", (c) => {
|
|
142
|
-
const
|
|
143
|
-
|
|
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 })) });
|
|
144
383
|
});
|
|
145
384
|
|
|
146
385
|
app.post("/instances", async (c) => {
|
|
147
386
|
const body = await c.req.json();
|
|
148
387
|
logger.info(`[webui-action] ai.instance.create`, {
|
|
149
388
|
name: body?.name,
|
|
150
|
-
|
|
151
|
-
|
|
389
|
+
providerId: body?.providerId,
|
|
390
|
+
modelId: body?.modelId,
|
|
152
391
|
});
|
|
153
|
-
|
|
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) {
|
|
154
403
|
return c.json({ ok: false, error: "AI_SERVICE_UNAVAILABLE" }, 503);
|
|
155
404
|
}
|
|
156
|
-
|
|
157
|
-
await aiService.api.create({
|
|
405
|
+
await api.create({
|
|
158
406
|
name: body.name,
|
|
159
407
|
apiUrl: body.apiUrl,
|
|
160
408
|
apiKey: body.apiKey,
|
|
161
409
|
modelType: body.modelType || "text",
|
|
410
|
+
model: body.model,
|
|
162
411
|
});
|
|
163
|
-
return c.json({ ok: true, data:
|
|
412
|
+
return c.json({ ok: true, data: api.list() });
|
|
164
413
|
});
|
|
165
414
|
|
|
166
415
|
app.delete("/instances/:name", (c) => {
|
|
167
416
|
const name = c.req.param("name");
|
|
168
417
|
logger.info(`[webui-action] ai.instance.remove`, { name });
|
|
169
|
-
const ok =
|
|
418
|
+
const ok = getAIApi()?.remove?.(name);
|
|
170
419
|
return c.json({ ok: Boolean(ok) });
|
|
171
420
|
});
|
|
172
421
|
|
|
173
422
|
app.post("/default/:name", (c) => {
|
|
174
423
|
const name = c.req.param("name");
|
|
175
424
|
logger.info(`[webui-action] ai.instance.set-default`, { name });
|
|
176
|
-
const ok =
|
|
425
|
+
const ok = getAIApi()?.setDefault?.(name);
|
|
177
426
|
return c.json({ ok: Boolean(ok) });
|
|
178
427
|
});
|
|
179
428
|
|
|
180
429
|
app.get("/skills", (c) => {
|
|
181
|
-
const
|
|
182
|
-
const
|
|
430
|
+
const api = getAIApi();
|
|
431
|
+
const skills = api?.getAllSkills?.();
|
|
432
|
+
const tools = api?.getAllTools?.();
|
|
183
433
|
return c.json({
|
|
184
434
|
ok: true,
|
|
185
435
|
data: {
|
|
@@ -189,8 +439,13 @@ export function createAIRoutes() {
|
|
|
189
439
|
});
|
|
190
440
|
});
|
|
191
441
|
|
|
442
|
+
app.get("/stickers", (c) =>
|
|
443
|
+
c.json({ ok: true, data: listStickerCatalog() }),
|
|
444
|
+
);
|
|
445
|
+
|
|
192
446
|
app.get("/usage", (c) => {
|
|
193
|
-
|
|
447
|
+
const api = getAIApi();
|
|
448
|
+
if (!api?.getUsageSummary) {
|
|
194
449
|
return c.json({ ok: false, error: "AI_USAGE_UNAVAILABLE" }, 503);
|
|
195
450
|
}
|
|
196
451
|
|
|
@@ -209,7 +464,7 @@ export function createAIRoutes() {
|
|
|
209
464
|
|
|
210
465
|
return c.json({
|
|
211
466
|
ok: true,
|
|
212
|
-
data:
|
|
467
|
+
data: api.getUsageSummary({ range, botId }),
|
|
213
468
|
});
|
|
214
469
|
});
|
|
215
470
|
|