mioku-service-webui 2.1.0 → 4.0.1

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.
@@ -6,7 +6,7 @@ import { logger } from "mioki";
6
6
  export interface ConfigField {
7
7
  key: string;
8
8
  label: string;
9
- type: "text" | "textarea" | "number" | "switch" | "select" | "multi-select" | "secret" | "json";
9
+ type: "text" | "textarea" | "number" | "switch" | "select" | "multi-select" | "secret" | "json" | "array";
10
10
  description?: string;
11
11
  placeholder?: string;
12
12
  required?: boolean;
@@ -14,6 +14,7 @@ export interface ConfigField {
14
14
  source?: string;
15
15
  options?: Array<{ value: string; label: string }>;
16
16
  defaultValue?: any;
17
+ itemFields?: ConfigField[];
17
18
  }
18
19
 
19
20
  export interface ConfigPageManifest {
@@ -26,10 +27,25 @@ export interface ConfigPageManifest {
26
27
  }
27
28
 
28
29
  export function loadPluginConfigPage(pluginName: string): ConfigPageManifest | null {
29
- const pluginDir = path.join(process.cwd(), "plugins", pluginName);
30
- const configMdPath = path.join(pluginDir, "config.md");
30
+ // 支持两种路径:本地 plugins 目录和 npm 包
31
+ const possiblePaths = [
32
+ path.join(process.cwd(), "plugins", pluginName),
33
+ path.join(process.cwd(), "node_modules", `mioku-plugin-${pluginName}`),
34
+ ];
35
+
36
+ let configMdPath: string | null = null;
37
+ let pluginDir: string | null = null;
38
+
39
+ for (const dir of possiblePaths) {
40
+ const candidate = path.join(dir, "config.md");
41
+ if (fs.existsSync(candidate)) {
42
+ configMdPath = candidate;
43
+ pluginDir = dir;
44
+ break;
45
+ }
46
+ }
31
47
 
32
- if (!fs.existsSync(configMdPath)) {
48
+ if (!configMdPath || !pluginDir) {
33
49
  return null;
34
50
  }
35
51
 
package/database.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import * as fs from "node:fs";
2
2
  import * as path from "node:path";
3
- import Database from "better-sqlite3";
3
+ import { Database } from "bun:sqlite";
4
4
  import { CHAT_DATA_DIR, readJsonFile, writeJsonFile } from "./utils";
5
5
 
6
6
  const DB_PATH = path.join(CHAT_DATA_DIR, "chat.db");
@@ -10,7 +10,7 @@ function openDb(): any {
10
10
  if (!fs.existsSync(DB_PATH)) {
11
11
  throw new Error("数据库不存在");
12
12
  }
13
- return new Database(DB_PATH, { readonly: false });
13
+ return new Database(DB_PATH, { readwrite: true });
14
14
  }
15
15
 
16
16
  function getTableColumns(db: any, table: string): string[] {
@@ -71,7 +71,7 @@ export function queryMessages(input: {
71
71
  }
72
72
  if (input.userId && columns.includes("user_id")) {
73
73
  conditions.push("user_id = @userId");
74
- params.userId = input.userId;
74
+ params.userId = String(input.userId);
75
75
  }
76
76
  if (input.sessionId && columns.includes("session_id")) {
77
77
  conditions.push("session_id = @sessionId");
@@ -106,8 +106,8 @@ export function updateMessage(input: { table?: string; idField?: string; id: str
106
106
  try {
107
107
  const table = ensureSafeIdentifier(input.table || "messages", "表名");
108
108
  const idField = ensureSafeIdentifier(input.idField || "id", "主键字段");
109
- const stmt = db.prepare(`UPDATE ${table} SET content = ? WHERE ${idField} = ?`);
110
- const result = stmt.run(input.content, input.id);
109
+ const stmt = db.prepare(`UPDATE ${table} SET content = $content WHERE ${idField} = $id`);
110
+ const result = stmt.run({ $content: input.content, $id: input.id });
111
111
  return { changed: result.changes };
112
112
  } finally {
113
113
  db.close();
@@ -123,8 +123,8 @@ export function deleteMessagesByRange(input: { table?: string; startTime: number
123
123
  if (!timeColumn) {
124
124
  throw new Error(`${table} 不包含时间字段(created_at/timestamp/updated_at)`);
125
125
  }
126
- const stmt = db.prepare(`DELETE FROM ${table} WHERE ${timeColumn} >= ? AND ${timeColumn} <= ?`);
127
- const result = stmt.run(input.startTime, input.endTime);
126
+ const stmt = db.prepare(`DELETE FROM ${table} WHERE ${timeColumn} >= $startTime AND ${timeColumn} <= $endTime`);
127
+ const result = stmt.run({ $startTime: input.startTime, $endTime: input.endTime });
128
128
  return { deleted: result.changes };
129
129
  } finally {
130
130
  db.close();
package/index.ts CHANGED
@@ -5,7 +5,7 @@ import { Hono } from "hono";
5
5
  import { serve } from "@hono/node-server";
6
6
  import { serveStatic } from "@hono/node-server/serve-static";
7
7
  import { createNodeWebSocket } from "@hono/node-ws";
8
- import type { MiokuService } from "../../core/types";
8
+ import type { MiokuService } from "mioku";
9
9
  import type { WebUISettings } from "./types";
10
10
  import { ensureAuthConfig, loginWithToken, requireAuth } from "./auth";
11
11
  import { getWebUISettings, getSystemOverview, getSaying } from "./system";
@@ -20,6 +20,7 @@ import {
20
20
  createMemeRoutes,
21
21
  createManageRoutes,
22
22
  createStoreRoutes,
23
+ createDataManagementRoutes,
23
24
  } from "./routes";
24
25
 
25
26
  export interface WebUIServiceAPI {
@@ -98,6 +99,7 @@ class WebUIRuntime {
98
99
  this.app.route("/api/db", createDBRoutes());
99
100
  this.app.route("/api/plugin-config", createPluginConfigRoutes());
100
101
  this.app.route("/api/meme", createMemeRoutes());
102
+ this.app.route("/api/data-management", createDataManagementRoutes());
101
103
 
102
104
  this.app.get(
103
105
  "/api/ws/logs",
@@ -218,8 +220,6 @@ const webUIService: MiokuService = {
218
220
  async init() {
219
221
  initBuiltinDatasources();
220
222
  ensureAuthConfig();
221
- fs.mkdirSync(CHAT_CONFIG_DIR, { recursive: true });
222
- fs.mkdirSync(CHAT_DATA_DIR, { recursive: true });
223
223
  runtime.initRoutes();
224
224
  },
225
225
 
package/package.json CHANGED
@@ -1,8 +1,13 @@
1
1
  {
2
2
  "name": "mioku-service-webui",
3
- "version": "2.1.0",
3
+ "version": "4.0.1",
4
4
  "description": "Mioku WebUI 服务",
5
5
  "main": "index.ts",
6
+ "type": "module",
7
+ "scripts": {
8
+ "webui:dev": "cd ../mioku-webui && bun run dev",
9
+ "webui:build": "cd ../mioku-webui && bun run build"
10
+ },
6
11
  "keywords": [
7
12
  "mioku"
8
13
  ],
@@ -13,8 +18,16 @@
13
18
  "dependencies": {
14
19
  "@hono/node-server": "1.19.13",
15
20
  "@hono/node-ws": "^1.2.0",
16
- "better-sqlite3": "^11.8.1",
21
+ "fflate": "^0.8.2",
17
22
  "gray-matter": "^4.0.3",
18
23
  "hono": "4.12.14"
24
+ },
25
+ "peerDependencies": {
26
+ "mioku-service-ai": "workspace:*"
27
+ },
28
+ "devDependencies": {
29
+ "@types/bun": "^1.3.14",
30
+ "mioku": "workspace:*",
31
+ "mioku-service-ai": "workspace:*"
19
32
  }
20
33
  }
package/routes/ai.ts CHANGED
@@ -1,7 +1,8 @@
1
1
  import { Hono } from "hono";
2
2
  import { logger } from "mioki";
3
3
  import { getChatConfig, updateChatConfig } from "../system";
4
- import aiService from "../../ai";
4
+ import aiService from "mioku-service-ai";
5
+ import type { AIUsageRange } from "mioku-service-ai/usage/types";
5
6
 
6
7
  const DEFAULT_MEMORY_GROUP_HISTORY_LIMIT = 300;
7
8
  const DEFAULT_MEMORY_USER_HISTORY_LIMIT = 100;
@@ -9,6 +10,7 @@ const DEFAULT_TOPIC_WINDOW_HOURS = 5;
9
10
  const DEFAULT_TOPIC_HISTORY_WINDOW_COUNT = 3;
10
11
  const DEFAULT_EXPRESSION_LEARN_AFTER_MESSAGES = 100;
11
12
  const DEFAULT_EXPRESSION_SAMPLE_SIZE = 8;
13
+ const usageRanges = new Set<AIUsageRange>(["today", "7d", "30d"]);
12
14
 
13
15
  function normalizePersonalizationConfig(input: any): any {
14
16
  const data =
@@ -187,5 +189,29 @@ export function createAIRoutes() {
187
189
  });
188
190
  });
189
191
 
192
+ app.get("/usage", (c) => {
193
+ if (!aiService?.api?.getUsageSummary) {
194
+ return c.json({ ok: false, error: "AI_USAGE_UNAVAILABLE" }, 503);
195
+ }
196
+
197
+ const rawRange = c.req.query("range");
198
+ const range: AIUsageRange =
199
+ rawRange && usageRanges.has(rawRange as AIUsageRange)
200
+ ? (rawRange as AIUsageRange)
201
+ : "today";
202
+ const rawBotId = c.req.query("botId");
203
+ const botId =
204
+ rawBotId && rawBotId !== "all" ? Number(rawBotId) : undefined;
205
+
206
+ if (botId !== undefined && !Number.isFinite(botId)) {
207
+ return c.json({ ok: false, error: "INVALID_BOT_ID" }, 400);
208
+ }
209
+
210
+ return c.json({
211
+ ok: true,
212
+ data: aiService.api.getUsageSummary({ range, botId }),
213
+ });
214
+ });
215
+
190
216
  return app;
191
217
  }