mioku-service-webui 4.4.5 → 4.5.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.
- package/database.ts +3 -1
- package/package.json +1 -1
- package/port-utils.ts +26 -1
- package/routes/ai.ts +247 -19
- package/routes/db.ts +4 -1
- package/system.ts +24 -89
- package/utils.ts +2 -29
package/database.ts
CHANGED
|
@@ -300,7 +300,9 @@ export function listMemeTree(): Record<string, Record<string, string[]>> {
|
|
|
300
300
|
tree[character][emotion] = fs
|
|
301
301
|
.readdirSync(emotionDir)
|
|
302
302
|
.filter((name) => /\.(png|jpg|jpeg|gif|webp)$/i.test(name))
|
|
303
|
-
.map((name) =>
|
|
303
|
+
.map((name) =>
|
|
304
|
+
path.posix.join("data", "chat", "meme", character, emotion, name),
|
|
305
|
+
);
|
|
304
306
|
}
|
|
305
307
|
}
|
|
306
308
|
|
package/package.json
CHANGED
package/port-utils.ts
CHANGED
|
@@ -41,9 +41,33 @@ function parsePids(raw: string): number[] {
|
|
|
41
41
|
return Array.from(pids);
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
+
function parseNetstatPids(raw: string, port: number): number[] {
|
|
45
|
+
const pids = new Set<number>();
|
|
46
|
+
for (const line of raw.split(/\r?\n/)) {
|
|
47
|
+
const cols = line.trim().split(/\s+/);
|
|
48
|
+
if (cols.length < 5 || cols[0].toUpperCase() !== "TCP") continue;
|
|
49
|
+
if (!/^LISTENING$/i.test(cols[3])) continue;
|
|
50
|
+
// Local address is host:port — IPv6 forms look like [::]:3339.
|
|
51
|
+
const localPort = cols[1].slice(cols[1].lastIndexOf(":") + 1);
|
|
52
|
+
if (Number(localPort) !== port) continue;
|
|
53
|
+
const pid = Number(cols[4]);
|
|
54
|
+
if (Number.isInteger(pid) && pid > 0) pids.add(pid);
|
|
55
|
+
}
|
|
56
|
+
return Array.from(pids);
|
|
57
|
+
}
|
|
58
|
+
|
|
44
59
|
async function listListeningPids(
|
|
45
60
|
port: number,
|
|
46
61
|
): Promise<{ pids: number[]; lsofAvailable: boolean }> {
|
|
62
|
+
if (process.platform === "win32") {
|
|
63
|
+
try {
|
|
64
|
+
const { stdout } = await execFileAsync("netstat", ["-ano", "-p", "TCP"]);
|
|
65
|
+
return { pids: parseNetstatPids(stdout, port), lsofAvailable: true };
|
|
66
|
+
} catch (err: any) {
|
|
67
|
+
return { pids: [], lsofAvailable: err?.code !== "ENOENT" };
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
47
71
|
try {
|
|
48
72
|
const { stdout } = await execFileAsync("lsof", [
|
|
49
73
|
"-nP",
|
|
@@ -84,8 +108,9 @@ export async function freePort(
|
|
|
84
108
|
const initial = await listListeningPids(port);
|
|
85
109
|
|
|
86
110
|
if (!initial.lsofAvailable) {
|
|
111
|
+
const missing = process.platform === "win32" ? "netstat" : "lsof";
|
|
87
112
|
log.warn(
|
|
88
|
-
`未检测到
|
|
113
|
+
`未检测到 ${missing} 命令,无法定位端口 ${port} 的占用进程,建议安装 ${missing}`,
|
|
89
114
|
);
|
|
90
115
|
return {
|
|
91
116
|
port,
|
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
|
|
package/routes/db.ts
CHANGED
|
@@ -231,7 +231,10 @@ export function createMemeRoutes() {
|
|
|
231
231
|
const buffer = Buffer.from(await file.arrayBuffer());
|
|
232
232
|
fs.writeFileSync(filePath, buffer);
|
|
233
233
|
|
|
234
|
-
return c.json({
|
|
234
|
+
return c.json({
|
|
235
|
+
ok: true,
|
|
236
|
+
path: path.relative(process.cwd(), filePath).split(path.sep).join("/"),
|
|
237
|
+
});
|
|
235
238
|
});
|
|
236
239
|
|
|
237
240
|
app.delete("/", async (c) => {
|
package/system.ts
CHANGED
|
@@ -33,7 +33,7 @@ import {
|
|
|
33
33
|
WEBUI_DIST,
|
|
34
34
|
writeJsonFile,
|
|
35
35
|
} from "./utils";
|
|
36
|
-
import
|
|
36
|
+
import * as fsp from "node:fs/promises";
|
|
37
37
|
|
|
38
38
|
async function fetchJson(url: string): Promise<any> {
|
|
39
39
|
const res = await fetch(url, {
|
|
@@ -553,44 +553,6 @@ function getRepositoryFromPackage(pkg: any): string {
|
|
|
553
553
|
return "";
|
|
554
554
|
}
|
|
555
555
|
|
|
556
|
-
async function commandExists(cmd: string): Promise<boolean> {
|
|
557
|
-
const result = await runCommand("which", [cmd], process.cwd());
|
|
558
|
-
return result.code === 0;
|
|
559
|
-
}
|
|
560
|
-
|
|
561
|
-
async function ensureUnzipCommand(): Promise<void> {
|
|
562
|
-
if (await commandExists("unzip")) {
|
|
563
|
-
return;
|
|
564
|
-
}
|
|
565
|
-
|
|
566
|
-
const platform = os.platform();
|
|
567
|
-
if (platform === "darwin") {
|
|
568
|
-
const installRes = await runCommand(
|
|
569
|
-
"brew",
|
|
570
|
-
["install", "unzip"],
|
|
571
|
-
process.cwd(),
|
|
572
|
-
);
|
|
573
|
-
if (installRes.code !== 0) {
|
|
574
|
-
throw new Error(
|
|
575
|
-
`未找到 unzip 且自动安装失败: ${installRes.stderr || installRes.stdout}。\n` +
|
|
576
|
-
`请手动运行: brew install unzip`,
|
|
577
|
-
);
|
|
578
|
-
}
|
|
579
|
-
} else {
|
|
580
|
-
const installRes = await runCommand(
|
|
581
|
-
"sudo",
|
|
582
|
-
["apt", "install", "-y", "unzip"],
|
|
583
|
-
process.cwd(),
|
|
584
|
-
);
|
|
585
|
-
if (installRes.code !== 0) {
|
|
586
|
-
throw new Error(
|
|
587
|
-
`未找到 unzip 且自动安装失败: ${installRes.stderr || installRes.stdout}。\n` +
|
|
588
|
-
`请手动运行: sudo apt install unzip`,
|
|
589
|
-
);
|
|
590
|
-
}
|
|
591
|
-
}
|
|
592
|
-
}
|
|
593
|
-
|
|
594
556
|
async function getGitOriginUrl(dir: string): Promise<string> {
|
|
595
557
|
const res = await runCommand("git", ["remote", "get-url", "origin"], dir);
|
|
596
558
|
if (res.code !== 0) return "";
|
|
@@ -1912,58 +1874,31 @@ export async function updateWebUIDistFromRelease(): Promise<
|
|
|
1912
1874
|
try {
|
|
1913
1875
|
logger.info(`[webui] 正在下载 WebUI dist...`);
|
|
1914
1876
|
|
|
1915
|
-
await
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
"-A",
|
|
1923
|
-
"mioku-webui-updater/1.0",
|
|
1924
|
-
"--silent",
|
|
1925
|
-
"--show-error",
|
|
1926
|
-
downloadUrl,
|
|
1927
|
-
],
|
|
1928
|
-
{
|
|
1929
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
1930
|
-
},
|
|
1931
|
-
);
|
|
1932
|
-
|
|
1933
|
-
let stderr = "";
|
|
1934
|
-
curl.stderr.on("data", (chunk) => {
|
|
1935
|
-
stderr += String(chunk);
|
|
1936
|
-
});
|
|
1877
|
+
const res = await fetch(downloadUrl, {
|
|
1878
|
+
headers: { "User-Agent": "mioku-webui-updater/1.0" },
|
|
1879
|
+
redirect: "follow",
|
|
1880
|
+
});
|
|
1881
|
+
if (!res.ok || !res.body) {
|
|
1882
|
+
throw new Error(`下载失败: HTTP ${res.status}`);
|
|
1883
|
+
}
|
|
1937
1884
|
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
// Periodically log progress by checking file size
|
|
1951
|
-
const progressTimer = setInterval(() => {
|
|
1952
|
-
if (fs.existsSync(zipPath)) {
|
|
1953
|
-
const stat = fs.statSync(zipPath);
|
|
1954
|
-
const now = Date.now();
|
|
1955
|
-
if (now - lastLogTime >= LOG_INTERVAL_MS) {
|
|
1956
|
-
logger.info(
|
|
1957
|
-
`[webui] 正在下载 WebUI dist... ${(stat.size / 1024 / 1024).toFixed(1)} MB 已下载`,
|
|
1958
|
-
);
|
|
1959
|
-
lastLogTime = now;
|
|
1960
|
-
}
|
|
1885
|
+
let downloaded = 0;
|
|
1886
|
+
const handle = await fsp.open(zipPath, "w");
|
|
1887
|
+
try {
|
|
1888
|
+
for await (const chunk of res.body as unknown as AsyncIterable<Uint8Array>) {
|
|
1889
|
+
await handle.write(chunk);
|
|
1890
|
+
downloaded += chunk.byteLength;
|
|
1891
|
+
const now = Date.now();
|
|
1892
|
+
if (now - lastLogTime >= LOG_INTERVAL_MS) {
|
|
1893
|
+
logger.info(
|
|
1894
|
+
`[webui] 正在下载 WebUI dist... ${(downloaded / 1024 / 1024).toFixed(1)} MB 已下载`,
|
|
1895
|
+
);
|
|
1896
|
+
lastLogTime = now;
|
|
1961
1897
|
}
|
|
1962
|
-
}
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
});
|
|
1898
|
+
}
|
|
1899
|
+
} finally {
|
|
1900
|
+
await handle.close();
|
|
1901
|
+
}
|
|
1967
1902
|
|
|
1968
1903
|
const zipStat = fs.statSync(zipPath);
|
|
1969
1904
|
logger.info(
|
package/utils.ts
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import * as fs from "node:fs";
|
|
2
2
|
import * as path from "node:path";
|
|
3
3
|
import { fileURLToPath } from "node:url";
|
|
4
|
-
import { spawn } from "node:child_process";
|
|
5
4
|
import type { PackageManager, WebUISettings } from "./types";
|
|
6
5
|
import {
|
|
7
6
|
getServiceConfigDir,
|
|
8
7
|
getServiceDataDir,
|
|
9
8
|
getConfigDir,
|
|
10
9
|
getDataDir,
|
|
10
|
+
runCommand as runMiokuCommand,
|
|
11
11
|
} from "mioku";
|
|
12
12
|
|
|
13
13
|
const __filename = fileURLToPath(import.meta.url);
|
|
@@ -86,34 +86,7 @@ export async function runCommand(
|
|
|
86
86
|
args: string[],
|
|
87
87
|
cwd: string,
|
|
88
88
|
): Promise<{ stdout: string; stderr: string; code: number }> {
|
|
89
|
-
return
|
|
90
|
-
const child = spawn(command, args, {
|
|
91
|
-
cwd,
|
|
92
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
93
|
-
});
|
|
94
|
-
let stdout = "";
|
|
95
|
-
let stderr = "";
|
|
96
|
-
|
|
97
|
-
child.stdout.on("data", (chunk) => {
|
|
98
|
-
stdout += String(chunk);
|
|
99
|
-
});
|
|
100
|
-
|
|
101
|
-
child.stderr.on("data", (chunk) => {
|
|
102
|
-
stderr += String(chunk);
|
|
103
|
-
});
|
|
104
|
-
|
|
105
|
-
child.on("close", (code) => {
|
|
106
|
-
resolve({ stdout, stderr, code: code ?? 1 });
|
|
107
|
-
});
|
|
108
|
-
|
|
109
|
-
child.on("error", (error) => {
|
|
110
|
-
resolve({
|
|
111
|
-
stdout,
|
|
112
|
-
stderr: `${stderr}\n${error.message}`.trim(),
|
|
113
|
-
code: 1,
|
|
114
|
-
});
|
|
115
|
-
});
|
|
116
|
-
});
|
|
89
|
+
return runMiokuCommand(command, args, { cwd });
|
|
117
90
|
}
|
|
118
91
|
|
|
119
92
|
export function getInstallCommand(): { cmd: string; args: string[] } {
|