luckytri 1.2.1 → 1.2.3
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/README.en.md +1 -1
- package/README.md +1 -1
- package/docs/en/CHANGELOG.md +16 -0
- package/docs/en/architecture.md +10 -0
- package/docs/en/guide.md +2 -0
- package/docs/en/npm-release.md +6 -0
- package/docs/zh/CHANGELOG.md +16 -0
- package/docs/zh/architecture.md +10 -0
- package/docs/zh/guide.md +2 -0
- package/docs/zh/npm-release.md +8 -0
- package/package.json +2 -2
- package/public/app/assets/{ChatsPage-eDqFIg97.js → ChatsPage-o-exjdFu.js} +1 -1
- package/public/app/assets/{Empty-CAGOJsfD.js → Empty-uSyRBXOT.js} +1 -1
- package/public/app/assets/{Field-CRz-JmN1.js → Field-Bfa5dCkk.js} +1 -1
- package/public/app/assets/{HeartPage-BGWoeMrC.js → HeartPage-Q5ts7Ko6.js} +1 -1
- package/public/app/assets/{LifePage-iao3o3wt.js → LifePage-B4AsD_DM.js} +1 -1
- package/public/app/assets/{MemoryPage-Dwi2yi7E.js → MemoryPage-ijD6lsxo.js} +1 -1
- package/public/app/assets/{NaturePage-DniImo9U.js → NaturePage-B9-dv8vz.js} +1 -1
- package/public/app/assets/{PeoplePage-D-MC3JEt.js → PeoplePage-BG_XPian.js} +1 -1
- package/public/app/assets/{PluginsPage-D7dJTf96.js → PluginsPage-CPYYMown.js} +1 -1
- package/public/app/assets/SystemPage-B9M9g95D.css +1 -0
- package/public/app/assets/{SystemPage-C5CUHjGh.js → SystemPage-CTduZhr3.js} +1 -1
- package/public/app/assets/{TimePage-Dwhr3-92.js → TimePage-DScn8IN-.js} +1 -1
- package/public/app/assets/{index-L0bHax8C.js → index-5FMs9YYQ.js} +3 -3
- package/public/app/assets/{index-BHZcHUMh.css → index-cDDC-1dI.css} +1 -1
- package/public/app/assets/{knowledge-BTDNafzl.js → knowledge-Ces-Bx70.js} +1 -1
- package/public/app/assets/{media-BBzOAfNG.js → media-BsG-i0Lv.js} +1 -1
- package/public/app/assets/{mind-CBTkFje2.js → mind-D3mdwFW3.js} +1 -1
- package/public/app/index.html +4 -4
- package/public/guide.en.html +14 -3
- package/public/guide.html +15 -3
- package/public/tutorial.en.md +2 -0
- package/public/tutorial.md +2 -0
- package/scripts/service-supervisor.js +8 -13
- package/scripts/start-server.js +8 -1
- package/scripts/update-instance.js +93 -27
- package/server/app.js +8 -1
- package/server/channels/onebot/adapter.js +25 -1
- package/server/channels/onebot/index.js +5 -2
- package/server/core/dialogue-contract.js +1 -1
- package/server/core/image-preprocessor.js +41 -1
- package/server/core/message-scheduler.js +12 -2
- package/server/core/orchestrator.js +14 -0
- package/server/core/reply-delivery.js +72 -0
- package/server/core/turn-processor.js +25 -0
- package/server/core/turn.js +25 -1
- package/server/i18n/errors.js +50 -0
- package/server/runtime/update-errors.js +73 -0
- package/server/runtime/update-execution.js +13 -8
- package/server/runtime/update-manager.js +30 -8
- package/server/stickers/api.js +149 -0
- package/server/stickers/index.js +363 -0
- package/server/stickers/library.js +199 -0
- package/server/stickers/offer.js +123 -0
- package/server/stickers/schema.js +25 -0
- package/public/app/assets/SystemPage-C_Y5M7JU.css +0 -1
package/server/i18n/errors.js
CHANGED
|
@@ -2,6 +2,20 @@
|
|
|
2
2
|
// key, exactly as the code writes it. `{0}` stands for a value the code puts
|
|
3
3
|
// into the message; patterns are used when a message carries one.
|
|
4
4
|
export const ERRORS = {
|
|
5
|
+
表情开关无效: "Invalid sticker switch",
|
|
6
|
+
"图片理解尺寸应在 512–8192 像素之间":
|
|
7
|
+
"Image understanding size must be between 512 and 8192 pixels",
|
|
8
|
+
"图片理解大小应在 0.5–4 MB 之间":
|
|
9
|
+
"Image understanding size must be between 0.5 and 4 MB",
|
|
10
|
+
"可接收的图片大小应在 1–64 MB 之间":
|
|
11
|
+
"The accepted image size must be between 1 and 64 MB",
|
|
12
|
+
表情不存在: "The sticker does not exist",
|
|
13
|
+
文件为空: "The file is empty",
|
|
14
|
+
"表情超过 2MB,请压缩后再上传":
|
|
15
|
+
"The sticker is larger than 2 MB; compress it and upload again",
|
|
16
|
+
"只支持 PNG、JPEG、GIF、WebP 图片":
|
|
17
|
+
"Only PNG, JPEG, GIF and WebP images are supported",
|
|
18
|
+
"表情尺寸应在 4096 像素以内": "Stickers must be within 4096 pixels",
|
|
5
19
|
复审超过截止时间: "The reply review passed its deadline",
|
|
6
20
|
"JSON 同一对象含重复字段,不能确定哪份内容有效":
|
|
7
21
|
"A JSON object contains duplicate fields; the intended value is ambiguous",
|
|
@@ -34,6 +48,42 @@ export const ERRORS = {
|
|
|
34
48
|
"The update did not complete. The original program and data were retained. Check the runtime log before trying again.",
|
|
35
49
|
"自动恢复未完成,更新前的校验备份已保留,请查看运行日志并手动恢复。":
|
|
36
50
|
"Automatic recovery did not complete. The verified pre-update backup was retained. Check the runtime log and recover manually.",
|
|
51
|
+
"Node.js 版本不满足新版本要求,请先升级 Node.js 后重试。":
|
|
52
|
+
"This Node.js version does not meet the new release requirements. Upgrade Node.js and try again.",
|
|
53
|
+
"当前服务找不到 npm,请安装 npm 并检查服务的 PATH 配置。":
|
|
54
|
+
"The service cannot find npm. Install npm and check the service PATH.",
|
|
55
|
+
"npm 暂时无法下载该版本或其运行依赖,请稍后重新检查更新。":
|
|
56
|
+
"npm cannot download this release or its runtime dependencies yet. Check for updates again later.",
|
|
57
|
+
"连接 npm 失败,请检查服务器的网络、DNS 和代理配置。":
|
|
58
|
+
"Cannot reach npm. Check the server network, DNS and proxy settings.",
|
|
59
|
+
"连接 npm 时证书校验失败,请检查服务器的证书和代理配置。":
|
|
60
|
+
"Certificate verification failed when connecting to npm. Check the server certificates and proxy settings.",
|
|
61
|
+
"npm 拒绝了下载请求,请检查服务器的网络或代理访问限制。":
|
|
62
|
+
"npm rejected the download request. Check network or proxy access restrictions.",
|
|
63
|
+
"磁盘空间不足,请为新程序、数据库快照和迁移副本留出空间。":
|
|
64
|
+
"There is not enough disk space for the new program, database snapshot and migration copy.",
|
|
65
|
+
"服务账号没有所需的读写权限,请检查实例和更新目录的权限。":
|
|
66
|
+
"The service account lacks read or write access. Check the instance and update directory permissions.",
|
|
67
|
+
"当前更新步骤超时,请检查运行日志后重试。":
|
|
68
|
+
"The update step timed out. Check the runtime log and try again.",
|
|
69
|
+
"新版本安装失败,旧实例仍在运行,请查看运行日志中的 npm 报错。":
|
|
70
|
+
"Installation failed while the previous instance remains running. Check the npm errors in the runtime log.",
|
|
71
|
+
"新程序或本机运行依赖校验失败,请查看运行日志。":
|
|
72
|
+
"The new program or its local runtime dependencies failed validation. Check the runtime log.",
|
|
73
|
+
"旧实例未能安全停止,请检查进程管理和实例端口。":
|
|
74
|
+
"The previous instance could not be stopped safely. Check process management and the instance port.",
|
|
75
|
+
"更新前的数据库快照未能完成或通过校验,请查看运行日志。":
|
|
76
|
+
"The pre-update database snapshot could not be completed or verified. Check the runtime log.",
|
|
77
|
+
"记忆与数据迁移未通过校验,请查看运行日志。":
|
|
78
|
+
"Memory and data migration failed validation. Check the runtime log.",
|
|
79
|
+
"新实例未能正常启动,请检查运行日志和端口占用。":
|
|
80
|
+
"The new instance could not start correctly. Check the runtime log and port usage.",
|
|
81
|
+
"更新进程意外退出,请检查服务管理器、进程退出记录和运行日志。":
|
|
82
|
+
"The update process exited unexpectedly. Check the service manager, process exit records and runtime log.",
|
|
83
|
+
"旧版本或数据库恢复未完成,请使用更新前的校验备份恢复。":
|
|
84
|
+
"The previous program or database could not be restored. Recover from the verified pre-update backup.",
|
|
85
|
+
"当前更新步骤未完成,请查看运行日志。":
|
|
86
|
+
"The update step did not complete. Check the runtime log.",
|
|
37
87
|
"缺少候选版本的数据迁移器,已停止更新":
|
|
38
88
|
"The candidate release has no data migrator. The update was stopped.",
|
|
39
89
|
迁移目标不是现有的绝对路径数据库文件:
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
// Only these descriptions reach the management UI. Subprocess output can
|
|
2
|
+
// contain local paths or credentials and belongs in the local log only.
|
|
3
|
+
export const UPDATE_FAILURES = Object.freeze({
|
|
4
|
+
NODE_UNSUPPORTED: "Node.js 版本不满足新版本要求,请先升级 Node.js 后重试。",
|
|
5
|
+
NPM_NOT_FOUND: "当前服务找不到 npm,请安装 npm 并检查服务的 PATH 配置。",
|
|
6
|
+
VERSION_UNAVAILABLE:
|
|
7
|
+
"npm 暂时无法下载该版本或其运行依赖,请稍后重新检查更新。",
|
|
8
|
+
NETWORK_ERROR: "连接 npm 失败,请检查服务器的网络、DNS 和代理配置。",
|
|
9
|
+
TLS_ERROR: "连接 npm 时证书校验失败,请检查服务器的证书和代理配置。",
|
|
10
|
+
REGISTRY_ACCESS: "npm 拒绝了下载请求,请检查服务器的网络或代理访问限制。",
|
|
11
|
+
DISK_FULL: "磁盘空间不足,请为新程序、数据库快照和迁移副本留出空间。",
|
|
12
|
+
PERMISSION_DENIED: "服务账号没有所需的读写权限,请检查实例和更新目录的权限。",
|
|
13
|
+
STEP_TIMEOUT: "当前更新步骤超时,请检查运行日志后重试。",
|
|
14
|
+
INSTALL_FAILED:
|
|
15
|
+
"新版本安装失败,旧实例仍在运行,请查看运行日志中的 npm 报错。",
|
|
16
|
+
PACKAGE_CHECK_FAILED: "新程序或本机运行依赖校验失败,请查看运行日志。",
|
|
17
|
+
STOP_FAILED: "旧实例未能安全停止,请检查进程管理和实例端口。",
|
|
18
|
+
BACKUP_FAILED: "更新前的数据库快照未能完成或通过校验,请查看运行日志。",
|
|
19
|
+
MIGRATION_FAILED: "记忆与数据迁移未通过校验,请查看运行日志。",
|
|
20
|
+
START_FAILED: "新实例未能正常启动,请检查运行日志和端口占用。",
|
|
21
|
+
UPDATE_INTERRUPTED:
|
|
22
|
+
"更新进程意外退出,请检查服务管理器、进程退出记录和运行日志。",
|
|
23
|
+
RECOVERY_FAILED: "旧版本或数据库恢复未完成,请使用更新前的校验备份恢复。",
|
|
24
|
+
STEP_FAILED: "当前更新步骤未完成,请查看运行日志。",
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
export function updateError(code) {
|
|
28
|
+
const known = Object.hasOwn(UPDATE_FAILURES, code) ? code : "STEP_FAILED";
|
|
29
|
+
return Object.assign(Error(UPDATE_FAILURES[known]), { updateCode: known });
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function npmFailureCode(output) {
|
|
33
|
+
for (const [pattern, code] of [
|
|
34
|
+
[/\bEBADENGINE\b/, "NODE_UNSUPPORTED"],
|
|
35
|
+
[/\bENOSPC\b/, "DISK_FULL"],
|
|
36
|
+
[/\b(?:EACCES|EPERM)\b/, "PERMISSION_DENIED"],
|
|
37
|
+
[/\b(?:E404|ETARGET)\b/, "VERSION_UNAVAILABLE"],
|
|
38
|
+
[/\b(?:E401|E403)\b/, "REGISTRY_ACCESS"],
|
|
39
|
+
[
|
|
40
|
+
/\b(?:CERT_[A-Z_]+|[A-Z_]*CERTIFICATE[A-Z_]*|SELF_SIGNED_CERT_IN_CHAIN|UNABLE_TO_VERIFY_LEAF_SIGNATURE)\b/,
|
|
41
|
+
"TLS_ERROR",
|
|
42
|
+
],
|
|
43
|
+
[
|
|
44
|
+
/\b(?:ENOTFOUND|EAI_AGAIN|ECONNRESET|ECONNREFUSED|ETIMEDOUT|ESOCKETTIMEDOUT|ENETUNREACH|EHOSTUNREACH)\b/,
|
|
45
|
+
"NETWORK_ERROR",
|
|
46
|
+
],
|
|
47
|
+
])
|
|
48
|
+
if (pattern.test(output)) return code;
|
|
49
|
+
return "INSTALL_FAILED";
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function updateFailure(error, stage) {
|
|
53
|
+
const osCode = {
|
|
54
|
+
ENOSPC: "DISK_FULL",
|
|
55
|
+
EACCES: "PERMISSION_DENIED",
|
|
56
|
+
EPERM: "PERMISSION_DENIED",
|
|
57
|
+
}[error?.code];
|
|
58
|
+
const fallback =
|
|
59
|
+
{
|
|
60
|
+
downloading: "INSTALL_FAILED",
|
|
61
|
+
checking: "PACKAGE_CHECK_FAILED",
|
|
62
|
+
stopping: "STOP_FAILED",
|
|
63
|
+
backup: "BACKUP_FAILED",
|
|
64
|
+
migrating: "MIGRATION_FAILED",
|
|
65
|
+
restarting: "START_FAILED",
|
|
66
|
+
verifying: "START_FAILED",
|
|
67
|
+
restoring: "RECOVERY_FAILED",
|
|
68
|
+
}[stage] || "STEP_FAILED";
|
|
69
|
+
const failureCode = Object.hasOwn(UPDATE_FAILURES, error?.updateCode)
|
|
70
|
+
? error.updateCode
|
|
71
|
+
: osCode || fallback;
|
|
72
|
+
return { failureCode, detail: UPDATE_FAILURES[failureCode] };
|
|
73
|
+
}
|
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
} from "./update-manager.js";
|
|
16
16
|
import { backupDatabase } from "../../scripts/backup.js";
|
|
17
17
|
import { restoreToNewFile, verifyArchive } from "../storage/archive.js";
|
|
18
|
+
import { updateFailure } from "./update-errors.js";
|
|
18
19
|
|
|
19
20
|
// Executed outside the server so a large verified snapshot does not stall the
|
|
20
21
|
// WebUI. Dependencies are explicit for failure, restart and rollback tests.
|
|
@@ -105,12 +106,9 @@ export async function performUpdate(
|
|
|
105
106
|
return job;
|
|
106
107
|
} catch (error) {
|
|
107
108
|
const failedStage = job.status;
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
failedStage,
|
|
112
|
-
error.code || error.cause?.code || "STEP_FAILED",
|
|
113
|
-
);
|
|
109
|
+
const failure = updateFailure(error, failedStage);
|
|
110
|
+
job = { ...job, failedStage, ...failure };
|
|
111
|
+
console.error("更新步骤未完成:", failedStage, failure.failureCode, error);
|
|
114
112
|
if (!stopped) {
|
|
115
113
|
save("error", { error: UPDATE_FAILED, finished: Date.now() });
|
|
116
114
|
return job;
|
|
@@ -150,6 +148,7 @@ export async function performUpdate(
|
|
|
150
148
|
`${paths.database}.update-failed-${id}.migration${suffix}`,
|
|
151
149
|
);
|
|
152
150
|
const restored = await start(paths.package);
|
|
151
|
+
save("restoring", { childPid: restored });
|
|
153
152
|
await health(restored, job.previous);
|
|
154
153
|
save("error", {
|
|
155
154
|
error: UPDATE_FAILED,
|
|
@@ -157,8 +156,14 @@ export async function performUpdate(
|
|
|
157
156
|
childPid: restored,
|
|
158
157
|
finished: Date.now(),
|
|
159
158
|
});
|
|
160
|
-
} catch {
|
|
161
|
-
|
|
159
|
+
} catch (recoveryError) {
|
|
160
|
+
const recovery = updateFailure(recoveryError, "restoring");
|
|
161
|
+
console.error("更新恢复未完成:", recovery.failureCode, recoveryError);
|
|
162
|
+
save("error", {
|
|
163
|
+
error: UPDATE_RECOVERY_FAILED,
|
|
164
|
+
recoveryCode: recovery.failureCode,
|
|
165
|
+
finished: Date.now(),
|
|
166
|
+
});
|
|
162
167
|
}
|
|
163
168
|
return job;
|
|
164
169
|
} finally {
|
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
2
|
import { mkdirSync, readFileSync } from "node:fs";
|
|
3
3
|
import { join } from "node:path";
|
|
4
|
+
import {
|
|
5
|
+
updateError,
|
|
6
|
+
updateFailure,
|
|
7
|
+
UPDATE_FAILURES,
|
|
8
|
+
} from "./update-errors.js";
|
|
4
9
|
import {
|
|
5
10
|
atomicJson,
|
|
6
11
|
UPDATE_ID,
|
|
@@ -11,7 +16,7 @@ export const UPDATE_FAILED =
|
|
|
11
16
|
"更新未完成,原程序和数据已保留,请查看运行日志后重试。";
|
|
12
17
|
export const UPDATE_RECOVERY_FAILED =
|
|
13
18
|
"自动恢复未完成,更新前的校验备份已保留,请查看运行日志并手动恢复。";
|
|
14
|
-
const
|
|
19
|
+
export const UPDATE_ACTIVE_STAGES = Object.freeze([
|
|
15
20
|
"queued",
|
|
16
21
|
"downloading",
|
|
17
22
|
"checking",
|
|
@@ -22,6 +27,12 @@ const ACTIVE = new Set([
|
|
|
22
27
|
"verifying",
|
|
23
28
|
"restoring",
|
|
24
29
|
]);
|
|
30
|
+
const ACTIVE = new Set(UPDATE_ACTIVE_STAGES);
|
|
31
|
+
export const isUpdateActive = (stage) => ACTIVE.has(stage);
|
|
32
|
+
export const isUpdateFinished = (stage) =>
|
|
33
|
+
stage === "done" || stage === "error";
|
|
34
|
+
const beforeStop = (stage) =>
|
|
35
|
+
["queued", "downloading", "checking"].includes(stage);
|
|
25
36
|
export const processAlive = (pid) => {
|
|
26
37
|
if (!Number.isInteger(pid) || pid <= 0) return false;
|
|
27
38
|
try {
|
|
@@ -70,18 +81,17 @@ export class InstanceUpdater {
|
|
|
70
81
|
return null;
|
|
71
82
|
}
|
|
72
83
|
if (
|
|
73
|
-
|
|
84
|
+
isUpdateActive(job.status) &&
|
|
74
85
|
job.workerPid &&
|
|
75
86
|
!processAlive(job.workerPid)
|
|
76
87
|
) {
|
|
88
|
+
const failedStage = job.status;
|
|
77
89
|
job = writeUpdateJob(this.paths.home, {
|
|
78
90
|
...job,
|
|
79
91
|
status: "error",
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
)
|
|
83
|
-
? UPDATE_FAILED
|
|
84
|
-
: UPDATE_RECOVERY_FAILED,
|
|
92
|
+
failedStage,
|
|
93
|
+
...updateFailure(updateError("UPDATE_INTERRUPTED"), failedStage),
|
|
94
|
+
error: beforeStop(failedStage) ? UPDATE_FAILED : UPDATE_RECOVERY_FAILED,
|
|
85
95
|
finished: this.now(),
|
|
86
96
|
});
|
|
87
97
|
}
|
|
@@ -95,10 +105,22 @@ export class InstanceUpdater {
|
|
|
95
105
|
finished: job.finished,
|
|
96
106
|
error: job.error || "",
|
|
97
107
|
restored: !!job.restored,
|
|
108
|
+
...(isUpdateActive(job.failedStage)
|
|
109
|
+
? { failedStage: job.failedStage }
|
|
110
|
+
: {}),
|
|
111
|
+
...(Object.hasOwn(UPDATE_FAILURES, job.failureCode)
|
|
112
|
+
? {
|
|
113
|
+
failureCode: job.failureCode,
|
|
114
|
+
detail: UPDATE_FAILURES[job.failureCode],
|
|
115
|
+
}
|
|
116
|
+
: {}),
|
|
117
|
+
...(Object.hasOwn(UPDATE_FAILURES, job.recoveryCode)
|
|
118
|
+
? { recoveryDetail: UPDATE_FAILURES[job.recoveryCode] }
|
|
119
|
+
: {}),
|
|
98
120
|
};
|
|
99
121
|
}
|
|
100
122
|
get busy() {
|
|
101
|
-
return this.starting ||
|
|
123
|
+
return this.starting || isUpdateActive(this.status()?.status);
|
|
102
124
|
}
|
|
103
125
|
async install(version) {
|
|
104
126
|
if (this.busy) return this.status();
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import express from "express";
|
|
2
|
+
|
|
3
|
+
// The owner's sticker library. Uploads arrive as raw bytes, one file per
|
|
4
|
+
// request; everything else is JSON like the rest of the studio API.
|
|
5
|
+
export function mountStickerUpload(app, system) {
|
|
6
|
+
app.post(
|
|
7
|
+
"/api/core/stickers/upload",
|
|
8
|
+
express.raw({ type: "*/*", limit: "3mb" }),
|
|
9
|
+
async (req, res) => {
|
|
10
|
+
try {
|
|
11
|
+
const item = await system.stickers.library.add(req.body, {
|
|
12
|
+
name: String(req.query.name || "").replace(/\.[a-z0-9]+$/i, ""),
|
|
13
|
+
category: String(req.query.category || ""),
|
|
14
|
+
});
|
|
15
|
+
if (!item.duplicate) queueDescribe(system, item.id);
|
|
16
|
+
res.json(item);
|
|
17
|
+
} catch (error) {
|
|
18
|
+
res.status(400).json({ error: error.message });
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const queue = [];
|
|
25
|
+
let running = false;
|
|
26
|
+
function queueDescribe(system, id) {
|
|
27
|
+
queue.push(id);
|
|
28
|
+
if (running) return;
|
|
29
|
+
running = true;
|
|
30
|
+
(async () => {
|
|
31
|
+
while (queue.length) {
|
|
32
|
+
const next = queue.shift();
|
|
33
|
+
try {
|
|
34
|
+
await system.stickers.describe(next, { force: true });
|
|
35
|
+
} catch {
|
|
36
|
+
// The item records its own failure; the owner can retry.
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
running = false;
|
|
40
|
+
})();
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const MEDIA_DEFAULTS = {
|
|
44
|
+
visionMaxSide: 2048,
|
|
45
|
+
visionMaxMB: 3,
|
|
46
|
+
visionSourceMaxMB: 20,
|
|
47
|
+
};
|
|
48
|
+
export function mediaSettings(repo) {
|
|
49
|
+
return { ...MEDIA_DEFAULTS, ...(repo.config("media") || {}) };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function mountStickers(app, system) {
|
|
53
|
+
const { library } = system.stickers;
|
|
54
|
+
const wrap = (fn) => async (req, res) => {
|
|
55
|
+
try {
|
|
56
|
+
await fn(req, res);
|
|
57
|
+
} catch (error) {
|
|
58
|
+
res.status(400).json({ error: error.message });
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
app.get(
|
|
62
|
+
"/api/core/stickers",
|
|
63
|
+
wrap((req, res) =>
|
|
64
|
+
res.json({
|
|
65
|
+
items: library.list({
|
|
66
|
+
q: req.query.q || "",
|
|
67
|
+
category: req.query.category || "",
|
|
68
|
+
}),
|
|
69
|
+
categories: library.categories(),
|
|
70
|
+
settings: system.stickers.settings(),
|
|
71
|
+
pending: queue.length,
|
|
72
|
+
}),
|
|
73
|
+
),
|
|
74
|
+
);
|
|
75
|
+
app.get(
|
|
76
|
+
"/api/core/stickers/:id/image",
|
|
77
|
+
wrap((req, res) => {
|
|
78
|
+
const image = library.image(req.params.id);
|
|
79
|
+
if (!image) return res.status(404).json({ error: "表情不存在" });
|
|
80
|
+
res.setHeader("Content-Type", image.mime);
|
|
81
|
+
res.setHeader("X-Content-Type-Options", "nosniff");
|
|
82
|
+
res.setHeader("Cache-Control", "private, max-age=86400");
|
|
83
|
+
res.end(Buffer.from(image.bytes));
|
|
84
|
+
}),
|
|
85
|
+
);
|
|
86
|
+
app.patch(
|
|
87
|
+
"/api/core/stickers/:id",
|
|
88
|
+
wrap((req, res) => res.json(library.update(req.params.id, req.body))),
|
|
89
|
+
);
|
|
90
|
+
app.delete(
|
|
91
|
+
"/api/core/stickers/:id",
|
|
92
|
+
wrap((req, res) => {
|
|
93
|
+
library.remove(req.params.id);
|
|
94
|
+
res.json({ ok: true });
|
|
95
|
+
}),
|
|
96
|
+
);
|
|
97
|
+
app.post(
|
|
98
|
+
"/api/core/stickers/:id/describe",
|
|
99
|
+
wrap((req, res) => {
|
|
100
|
+
if (!library.get(req.params.id)) throw Error("表情不存在");
|
|
101
|
+
library.update(req.params.id, { status: "pending" });
|
|
102
|
+
queueDescribe(system, req.params.id);
|
|
103
|
+
res.json({ ok: true });
|
|
104
|
+
}),
|
|
105
|
+
);
|
|
106
|
+
app.put(
|
|
107
|
+
"/api/core/stickers-settings",
|
|
108
|
+
wrap((req, res) => {
|
|
109
|
+
const { enabled, taggingModelId } = req.body;
|
|
110
|
+
if (enabled !== undefined && typeof enabled !== "boolean")
|
|
111
|
+
throw Error("表情开关无效");
|
|
112
|
+
system.repo.saveConfig("stickers", {
|
|
113
|
+
...system.stickers.settings(),
|
|
114
|
+
...(enabled !== undefined ? { enabled } : {}),
|
|
115
|
+
...(taggingModelId !== undefined
|
|
116
|
+
? { taggingModelId: String(taggingModelId || "") }
|
|
117
|
+
: {}),
|
|
118
|
+
});
|
|
119
|
+
res.json(system.stickers.settings());
|
|
120
|
+
}),
|
|
121
|
+
);
|
|
122
|
+
app.get(
|
|
123
|
+
"/api/core/media-settings",
|
|
124
|
+
wrap((req, res) => res.json(mediaSettings(system.repo))),
|
|
125
|
+
);
|
|
126
|
+
app.put(
|
|
127
|
+
"/api/core/media-settings",
|
|
128
|
+
wrap((req, res) => {
|
|
129
|
+
const side = Number(req.body.visionMaxSide);
|
|
130
|
+
const mb = Number(req.body.visionMaxMB);
|
|
131
|
+
const source = Number(
|
|
132
|
+
req.body.visionSourceMaxMB ??
|
|
133
|
+
mediaSettings(system.repo).visionSourceMaxMB,
|
|
134
|
+
);
|
|
135
|
+
if (!Number.isFinite(source) || source < 1 || source > 64)
|
|
136
|
+
throw Error("可接收的图片大小应在 1–64 MB 之间");
|
|
137
|
+
if (!Number.isInteger(side) || side < 512 || side > 8192)
|
|
138
|
+
throw Error("图片理解尺寸应在 512–8192 像素之间");
|
|
139
|
+
if (!Number.isFinite(mb) || mb < 0.5 || mb > 4)
|
|
140
|
+
throw Error("图片理解大小应在 0.5–4 MB 之间");
|
|
141
|
+
system.repo.saveConfig("media", {
|
|
142
|
+
visionMaxSide: side,
|
|
143
|
+
visionMaxMB: mb,
|
|
144
|
+
visionSourceMaxMB: source,
|
|
145
|
+
});
|
|
146
|
+
res.json(mediaSettings(system.repo));
|
|
147
|
+
}),
|
|
148
|
+
);
|
|
149
|
+
}
|