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
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
writeFileSync,
|
|
6
6
|
readFileSync,
|
|
7
7
|
realpathSync,
|
|
8
|
+
writeSync,
|
|
8
9
|
} from "node:fs";
|
|
9
10
|
import { dirname, join, delimiter } from "node:path";
|
|
10
11
|
import { pathToFileURL } from "node:url";
|
|
@@ -23,6 +24,21 @@ import {
|
|
|
23
24
|
writeUpdateJob,
|
|
24
25
|
} from "../server/runtime/update-manager.js";
|
|
25
26
|
import { updateRoot } from "../server/runtime/installation.js";
|
|
27
|
+
import {
|
|
28
|
+
npmFailureCode,
|
|
29
|
+
updateError,
|
|
30
|
+
} from "../server/runtime/update-errors.js";
|
|
31
|
+
|
|
32
|
+
export function requireUpdateNode(version = process.versions.node) {
|
|
33
|
+
const [major, minor] = String(version).split(".").map(Number);
|
|
34
|
+
if (
|
|
35
|
+
!Number.isInteger(major) ||
|
|
36
|
+
!Number.isInteger(minor) ||
|
|
37
|
+
major < 24 ||
|
|
38
|
+
(major === 24 && minor < 5)
|
|
39
|
+
)
|
|
40
|
+
throw updateError("NODE_UNSUPPORTED");
|
|
41
|
+
}
|
|
26
42
|
|
|
27
43
|
export function installEnvironment(env) {
|
|
28
44
|
const kept = {};
|
|
@@ -35,11 +51,11 @@ export function installEnvironment(env) {
|
|
|
35
51
|
kept[key] = value;
|
|
36
52
|
return kept;
|
|
37
53
|
}
|
|
38
|
-
export function findNpmCli(env = process.env) {
|
|
54
|
+
export function findNpmCli(env = process.env, execPath = process.execPath) {
|
|
39
55
|
const candidates = [
|
|
40
56
|
env.npm_execpath,
|
|
41
|
-
join(dirname(
|
|
42
|
-
join(dirname(
|
|
57
|
+
join(dirname(execPath), "node_modules/npm/bin/npm-cli.js"),
|
|
58
|
+
join(dirname(execPath), "../lib/node_modules/npm/bin/npm-cli.js"),
|
|
43
59
|
...String(env.PATH || env.Path || "")
|
|
44
60
|
.split(delimiter)
|
|
45
61
|
.flatMap((dir) => [
|
|
@@ -60,31 +76,73 @@ export function findNpmCli(env = process.env) {
|
|
|
60
76
|
/* Try the next native Node/npm installation location. */
|
|
61
77
|
}
|
|
62
78
|
}
|
|
63
|
-
throw
|
|
79
|
+
throw updateError("NPM_NOT_FOUND");
|
|
64
80
|
}
|
|
65
|
-
function
|
|
81
|
+
export function runUpdateCommand(
|
|
82
|
+
args,
|
|
83
|
+
{
|
|
84
|
+
cwd,
|
|
85
|
+
env = process.env,
|
|
86
|
+
timeout = 600000,
|
|
87
|
+
log,
|
|
88
|
+
failureCode = "STEP_FAILED",
|
|
89
|
+
npmInstall = false,
|
|
90
|
+
} = {},
|
|
91
|
+
) {
|
|
66
92
|
return new Promise((resolve, reject) => {
|
|
67
93
|
const fd = log ? openSync(log, "a", 0o600) : null;
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
94
|
+
let timer,
|
|
95
|
+
child,
|
|
96
|
+
settled = false,
|
|
97
|
+
timedOut = false,
|
|
98
|
+
outputError,
|
|
99
|
+
tail = "";
|
|
100
|
+
const finish = (error) => {
|
|
101
|
+
if (settled) return;
|
|
102
|
+
settled = true;
|
|
103
|
+
clearTimeout(timer);
|
|
104
|
+
try {
|
|
105
|
+
if (fd !== null) closeSync(fd);
|
|
106
|
+
} catch (closeError) {
|
|
107
|
+
error ||= closeError;
|
|
108
|
+
}
|
|
109
|
+
if (error) reject(error);
|
|
110
|
+
else resolve();
|
|
111
|
+
};
|
|
112
|
+
try {
|
|
113
|
+
child = spawn(process.execPath, args, {
|
|
114
|
+
cwd,
|
|
115
|
+
env,
|
|
116
|
+
windowsHide: true,
|
|
117
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
118
|
+
});
|
|
119
|
+
} catch (error) {
|
|
120
|
+
finish(error);
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
const capture = (chunk) => {
|
|
124
|
+
if (settled) return;
|
|
125
|
+
tail = (tail + chunk.toString("utf8")).slice(-16384);
|
|
126
|
+
try {
|
|
127
|
+
if (fd !== null) writeSync(fd, chunk);
|
|
128
|
+
} catch (error) {
|
|
129
|
+
outputError = error;
|
|
130
|
+
// Wait for exit before rollback can rename a migration database.
|
|
131
|
+
child.kill("SIGKILL");
|
|
132
|
+
}
|
|
133
|
+
};
|
|
134
|
+
child.stdout.on("data", capture);
|
|
135
|
+
child.stderr.on("data", capture);
|
|
136
|
+
timer = setTimeout(() => {
|
|
77
137
|
timedOut = true;
|
|
78
|
-
child.kill();
|
|
138
|
+
child.kill("SIGKILL");
|
|
79
139
|
}, timeout);
|
|
80
|
-
child.once("error",
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
if (timedOut) reject(Error("更新步骤超时"));
|
|
87
|
-
else code === 0 ? resolve() : reject(Error("更新程序校验失败"));
|
|
140
|
+
child.once("error", finish);
|
|
141
|
+
child.once("close", (code) => {
|
|
142
|
+
if (outputError) finish(outputError);
|
|
143
|
+
else if (timedOut) finish(updateError("STEP_TIMEOUT"));
|
|
144
|
+
else if (code === 0) finish();
|
|
145
|
+
else finish(updateError(npmInstall ? npmFailureCode(tail) : failureCode));
|
|
88
146
|
});
|
|
89
147
|
});
|
|
90
148
|
}
|
|
@@ -94,10 +152,11 @@ export function updateExecution(paths) {
|
|
|
94
152
|
const pause = () => new Promise((r) => setTimeout(r, 250));
|
|
95
153
|
return {
|
|
96
154
|
async install(slot, version) {
|
|
155
|
+
requireUpdateNode();
|
|
97
156
|
const cli = findNpmCli();
|
|
98
157
|
const userconfig = join(updateRoot(paths.home), "install.npmrc");
|
|
99
158
|
writeFileSync(userconfig, "", { mode: 0o600 });
|
|
100
|
-
await
|
|
159
|
+
await runUpdateCommand(
|
|
101
160
|
[
|
|
102
161
|
cli,
|
|
103
162
|
"install",
|
|
@@ -112,20 +171,26 @@ export function updateExecution(paths) {
|
|
|
112
171
|
"--engine-strict",
|
|
113
172
|
`luckytri@${version}`,
|
|
114
173
|
],
|
|
115
|
-
{
|
|
174
|
+
{
|
|
175
|
+
cwd: slot,
|
|
176
|
+
env: installEnvironment(process.env),
|
|
177
|
+
log: paths.log,
|
|
178
|
+
npmInstall: true,
|
|
179
|
+
},
|
|
116
180
|
);
|
|
117
181
|
},
|
|
118
182
|
async check(root, version) {
|
|
119
183
|
const script = `import fs from 'node:fs';import {createRequire} from 'node:module';const p=JSON.parse(fs.readFileSync(${JSON.stringify(join(root, "package.json"))},'utf8'));if(p.version!==${JSON.stringify(version)})process.exit(1);await import(${JSON.stringify(pathToFileURL(join(root, "server/app.js")).href)});const sharp=createRequire(${JSON.stringify(pathToFileURL(join(root, "package.json")).href)})('sharp');await sharp({create:{width:1,height:1,channels:3,background:'#fff'}}).png().toBuffer();`;
|
|
120
|
-
await
|
|
184
|
+
await runUpdateCommand(["--input-type=module", "-e", script], {
|
|
121
185
|
cwd: root,
|
|
122
186
|
env: installEnvironment(process.env),
|
|
123
187
|
timeout: 30000,
|
|
124
188
|
log: paths.log,
|
|
189
|
+
failureCode: "PACKAGE_CHECK_FAILED",
|
|
125
190
|
});
|
|
126
191
|
},
|
|
127
192
|
async migrate(root, database, { previous, version, databaseId }) {
|
|
128
|
-
await
|
|
193
|
+
await runUpdateCommand(
|
|
129
194
|
[
|
|
130
195
|
join(root, "scripts", "migrate-instance.js"),
|
|
131
196
|
database,
|
|
@@ -138,6 +203,7 @@ export function updateExecution(paths) {
|
|
|
138
203
|
env: installEnvironment(process.env),
|
|
139
204
|
timeout: 3600000,
|
|
140
205
|
log: paths.log,
|
|
206
|
+
failureCode: "MIGRATION_FAILED",
|
|
141
207
|
},
|
|
142
208
|
);
|
|
143
209
|
},
|
package/server/app.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { mountStickerUpload, mountStickers } from "./stickers/api.js";
|
|
1
2
|
import express from "express";
|
|
2
3
|
import { existsSync } from "node:fs";
|
|
3
4
|
import { join } from "node:path";
|
|
@@ -42,7 +43,11 @@ export function createApp({
|
|
|
42
43
|
if (
|
|
43
44
|
["POST", "PATCH", "PUT"].includes(req.method) &&
|
|
44
45
|
(!req.body || typeof req.body !== "object" || Array.isArray(req.body)) &&
|
|
45
|
-
![
|
|
46
|
+
![
|
|
47
|
+
"/plugins/import/upload",
|
|
48
|
+
"/storage/transfer/upload",
|
|
49
|
+
"/core/stickers/upload",
|
|
50
|
+
].includes(req.path)
|
|
46
51
|
)
|
|
47
52
|
return res.status(400).json({ error: "请提交 JSON 对象" });
|
|
48
53
|
next();
|
|
@@ -61,6 +66,7 @@ export function createApp({
|
|
|
61
66
|
app.use("/api", objectBody);
|
|
62
67
|
mountUpdates(app, updates, runtime.updater);
|
|
63
68
|
mountDataTransfer(app, runtime.transfer);
|
|
69
|
+
if (chatSystem?.stickers) mountStickerUpload(app, chatSystem);
|
|
64
70
|
if (plugins) {
|
|
65
71
|
app.post(
|
|
66
72
|
"/api/plugins/import/upload",
|
|
@@ -74,6 +80,7 @@ export function createApp({
|
|
|
74
80
|
},
|
|
75
81
|
);
|
|
76
82
|
}
|
|
83
|
+
if (chatSystem?.stickers) mountStickers(app, chatSystem);
|
|
77
84
|
mountStudio(app, store, runtime, chatSystem);
|
|
78
85
|
mountManagement(app, store, chatSystem);
|
|
79
86
|
mountCore(app, chatSystem);
|
|
@@ -131,8 +131,32 @@ export const onebot = {
|
|
|
131
131
|
const reply = /^\d+$/.test(String(options?.replyTo || ""))
|
|
132
132
|
? [{ type: "reply", data: { id: String(options.replyTo) } }]
|
|
133
133
|
: [];
|
|
134
|
+
// A sticker goes as an image of its own, its bytes inline so the QQ client
|
|
135
|
+
// needs no path or URL on this machine.
|
|
136
|
+
const body = options?.image
|
|
137
|
+
? [
|
|
138
|
+
{
|
|
139
|
+
type: "image",
|
|
140
|
+
data: {
|
|
141
|
+
file: `base64://${options.image.base64}`,
|
|
142
|
+
sub_type: options.image.sticker ? 1 : 0,
|
|
143
|
+
...(options.image.summary
|
|
144
|
+
? { summary: options.image.summary }
|
|
145
|
+
: {}),
|
|
146
|
+
},
|
|
147
|
+
},
|
|
148
|
+
]
|
|
149
|
+
: [
|
|
150
|
+
...(Array.isArray(options?.at) ? options.at : [])
|
|
151
|
+
.filter((id) => /^\d{5,12}$/.test(String(id)))
|
|
152
|
+
.flatMap((id) => [{ type: "at", data: { qq: String(id) } }]),
|
|
153
|
+
{
|
|
154
|
+
type: "text",
|
|
155
|
+
data: { text: options?.at?.length ? ` ${text}` : text },
|
|
156
|
+
},
|
|
157
|
+
];
|
|
134
158
|
return {
|
|
135
|
-
message: [...reply,
|
|
159
|
+
message: [...reply, ...body],
|
|
136
160
|
[parsed.kind === "group" ? "group_id" : "user_id"]:
|
|
137
161
|
parsed.kind === "group"
|
|
138
162
|
? Number(message.raw?.group_id ?? parsed.nativeId)
|
|
@@ -6,7 +6,8 @@ import { defineChannel } from "../contract.js";
|
|
|
6
6
|
import { parseSessionKey } from "../session-key.js";
|
|
7
7
|
import { applyDirectoryNames } from "../../core/sessions.js";
|
|
8
8
|
import { saveAccountNames } from "../../studio/display-names.js";
|
|
9
|
-
import {
|
|
9
|
+
import { ownerImageLimits } from "../../core/image-preprocessor.js";
|
|
10
|
+
import { mediaSettings } from "../../stickers/api.js";
|
|
10
11
|
|
|
11
12
|
const EVENT_MAX_BYTES = 1024 * 1024;
|
|
12
13
|
const IMAGE_TIMEOUT_MS = 30000;
|
|
@@ -168,7 +169,9 @@ export function createOneBotChannel(
|
|
|
168
169
|
// Only authenticated OneBot clients reach this server. get_image may carry
|
|
169
170
|
// base64 rather than a URL, so its frame must fit the source-image budget.
|
|
170
171
|
// Ordinary events still have the smaller independent limit below.
|
|
171
|
-
const imageBytes =
|
|
172
|
+
const imageBytes = ownerImageLimits(
|
|
173
|
+
chatSystem?.repo ? mediaSettings(chatSystem.repo) : {},
|
|
174
|
+
).maxSourceBytes;
|
|
172
175
|
wss = new WebSocketServer({
|
|
173
176
|
noServer: true,
|
|
174
177
|
maxPayload: Math.ceil(imageBytes / 3) * 4 + EVENT_MAX_BYTES,
|
|
@@ -20,7 +20,7 @@ export const DIALOGUE_TASK = `读懂眼前这轮聊天,再用自己的话回
|
|
|
20
20
|
长短和口吻来自你自己:inner.ownVoice 是你自己一直以来打字的样子,inner.state 是此刻的心情和精力,再加上你和眼前这个人的关系、刚发生的事;别人怎么打字只是在场的情况,不去模仿。像在 QQ 里随手打一句,不写组织好的书面段落;答案不省,意思说够就停,不为活跃气氛加问题。问过的细节对方没接,就回应他继续说的内容,不立刻换词再问。如果这段对话里的人明确要求一段发完,同一话题后续都收进一条消息。列点也可以放在一条消息里换行。被明确要求停止就停止,新的实际问题仍可回应。
|
|
21
21
|
若一轮里同时有人提出表达偏好、另一人分享新内容,先简短确认偏好,再回应新内容;不能让确认格式代替对内容的回应。
|
|
22
22
|
停止的范围按对方原话理解:不要方案,就停止给方案;不要纠结某个细节,就放下那个细节,不把整个话题或交流关掉。对方明确说一条旁枝不是他问的,简短承认后回到主线;不要说“我没理清楚”或“先不乱说”,除非那条信息确实是回答主问题所必需的。
|
|
23
|
-
输出 JSON:{"understanding":{"kind":"question|proposal|feeling|sharing|correction|greeting|closing|other","messageIds":[本轮回应的序号],"point":"对方此刻的具体意思","tone":"serious|joking|teasing|ironic|venting","stance":"fact|guess|opinion|joke|hearsay"},"contribution":{"kind":"answer|reaction|question|idea|correction|none","point":"自己这次实际说出的意思,不是收住/接住之类动作"},"choice":"speak|react|decline|silent","targetMessageIds":[batchIds中的序号],"topic":"当前话题","say":"实际说的话","appraisal":"这件事对自己的实际影响,没有则空","reason":"为何这样选择,用名字不用QQ号","feelings":[{"feeling":"两三个字","intensity":0到1,"valence":-1到1,"arousal":0到1,"cause":[消息序号]}],"bonds":[{"userId":"speaker","change":"warmer|closer|trust_up|trust_down|friction|repair|distance","why":"具体变化","evidence":[消息序号]}],"crisis":{"clear":false,"messageIds":[]}}。silent 时 say 为空;react 是一句很短的话;say 是你在输入框里打的字,换行就是按了发送、另起一条消息,说多少、发几次由你此刻想说什么决定;若对方已要求同一段发完,则把连贯补充收在一条消息里,列点也可在同一条里换行。真实自伤危机认真回应眼前安全,crisis.clear=true。understanding 里的 tone 和 stance 可选:所回应的话是认真、玩笑、调侃、反话还是发泄,说的是事实、猜测、观点、玩笑还是转述;拿不准就不写这两项。working.reading 只是字面提示,以前后文为准。choice、targetMessageIds 等其余字段仍在最外层。`;
|
|
23
|
+
输出 JSON:{"understanding":{"kind":"question|proposal|feeling|sharing|correction|greeting|closing|other","messageIds":[本轮回应的序号],"point":"对方此刻的具体意思","tone":"serious|joking|teasing|ironic|venting","stance":"fact|guess|opinion|joke|hearsay"},"contribution":{"kind":"answer|reaction|question|idea|correction|none","point":"自己这次实际说出的意思,不是收住/接住之类动作"},"choice":"speak|react|decline|silent","targetMessageIds":[batchIds中的序号],"topic":"当前话题","say":"实际说的话","at":["可选,只从 expression.canAt 选 userId"],"sticker":"可选,expression.stickers 里的 ref","appraisal":"这件事对自己的实际影响,没有则空","reason":"为何这样选择,用名字不用QQ号","feelings":[{"feeling":"两三个字","intensity":0到1,"valence":-1到1,"arousal":0到1,"cause":[消息序号]}],"bonds":[{"userId":"speaker","change":"warmer|closer|trust_up|trust_down|friction|repair|distance","why":"具体变化","evidence":[消息序号]}],"crisis":{"clear":false,"messageIds":[]}}。silent 时 say 为空;react 是一句很短的话;say 是你在输入框里打的字,换行就是按了发送、另起一条消息,说多少、发几次由你此刻想说什么决定;若对方已要求同一段发完,则把连贯补充收在一条消息里,列点也可在同一条里换行。真实自伤危机认真回应眼前安全,crisis.clear=true。understanding 里的 tone 和 stance 可选:所回应的话是认真、玩笑、调侃、反话还是发泄,说的是事实、猜测、观点、玩笑还是转述;拿不准就不写这两项。working.reading 只是字面提示,以前后文为准。choice、targetMessageIds 等其余字段仍在最外层。`;
|
|
24
24
|
|
|
25
25
|
export const DIALOGUE_REWRITE = `重新核对 exchange.current 和引用,修改 draft 中被指出的具体问题。decision.understanding 可能也有误,以原话为准。保留成立的内容,直接说这轮真正想说的话;不要把修改写成新的认错套话或内部状态报告。self.livingFor 只作背景,不必提起。输出 {"bubbles":["实际内容"],"reason":"为何这样说"}。`;
|
|
26
26
|
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import sharp from "sharp";
|
|
2
2
|
|
|
3
3
|
const MIB = 1024 * 1024;
|
|
4
|
+
// Largest picture accepted for understanding; bigger ones are shrunk before
|
|
5
|
+
// any model sees them. VISION_SOURCE_CEILING_MB bounds the OneBot frame.
|
|
6
|
+
export const VISION_SOURCE_DEFAULT_MB = 20;
|
|
7
|
+
export const VISION_SOURCE_CEILING_MB = 64;
|
|
4
8
|
const TYPES = {
|
|
5
9
|
jpeg: "image/jpeg",
|
|
6
10
|
png: "image/png",
|
|
@@ -19,7 +23,8 @@ function configured(value, fallback, min, max) {
|
|
|
19
23
|
export function visionImageLimits(env = process.env) {
|
|
20
24
|
return {
|
|
21
25
|
maxSourceBytes: Math.floor(
|
|
22
|
-
configured(env.VISION_SOURCE_MAX_MB,
|
|
26
|
+
configured(env.VISION_SOURCE_MAX_MB, VISION_SOURCE_DEFAULT_MB, 1, 64) *
|
|
27
|
+
MIB,
|
|
23
28
|
),
|
|
24
29
|
maxModelBytes: Math.floor(
|
|
25
30
|
configured(env.VISION_MODEL_MAX_MB, 4, 1, 4) * MIB,
|
|
@@ -33,6 +38,41 @@ export function visionImageLimits(env = process.env) {
|
|
|
33
38
|
};
|
|
34
39
|
}
|
|
35
40
|
|
|
41
|
+
// The owner's image-understanding size: the long side and the encoded size
|
|
42
|
+
// a picture is brought down to before a model sees it. A big photo is shrunk
|
|
43
|
+
// and re-encoded rather than refused; many providers reject large images or
|
|
44
|
+
// time out on them.
|
|
45
|
+
export function modelImageLimits({
|
|
46
|
+
visionMaxSide = 2048,
|
|
47
|
+
visionMaxMB = 3,
|
|
48
|
+
visionSourceMaxMB = VISION_SOURCE_DEFAULT_MB,
|
|
49
|
+
} = {}) {
|
|
50
|
+
const source = Math.min(
|
|
51
|
+
VISION_SOURCE_CEILING_MB,
|
|
52
|
+
Math.max(1, Number(visionSourceMaxMB) || VISION_SOURCE_DEFAULT_MB),
|
|
53
|
+
);
|
|
54
|
+
const side = Math.min(
|
|
55
|
+
8192,
|
|
56
|
+
Math.max(512, Math.floor(Number(visionMaxSide) || 2048)),
|
|
57
|
+
);
|
|
58
|
+
const mb = Math.min(4, Math.max(0.5, Number(visionMaxMB) || 3));
|
|
59
|
+
return {
|
|
60
|
+
maxDimension: side,
|
|
61
|
+
maxOutputPixels: side * side,
|
|
62
|
+
maxModelBytes: Math.floor(mb * MIB),
|
|
63
|
+
maxSourceBytes: Math.floor(source * MIB),
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// The owner's setting, unless the operator pinned the source size in the
|
|
68
|
+
// environment (VISION_SOURCE_MAX_MB), which then wins everywhere.
|
|
69
|
+
export function ownerImageLimits(settings, env = process.env) {
|
|
70
|
+
const limits = modelImageLimits(settings);
|
|
71
|
+
if (env.VISION_SOURCE_MAX_MB != null && env.VISION_SOURCE_MAX_MB !== "")
|
|
72
|
+
limits.maxSourceBytes = visionImageLimits(env).maxSourceBytes;
|
|
73
|
+
return limits;
|
|
74
|
+
}
|
|
75
|
+
|
|
36
76
|
export function imageMime(bytes) {
|
|
37
77
|
if (
|
|
38
78
|
bytes.length >= 3 &&
|
|
@@ -16,6 +16,8 @@ export async function deliver(
|
|
|
16
16
|
// Platform id of an earlier message the first bubble answers, when the
|
|
17
17
|
// conversation has moved past it; the channel may show it as a quote.
|
|
18
18
|
quote = "",
|
|
19
|
+
// People she chose to @, on her first message only, the way one types.
|
|
20
|
+
at = [],
|
|
19
21
|
} = {},
|
|
20
22
|
) {
|
|
21
23
|
const ids = bubbles.map((text, i) => {
|
|
@@ -45,10 +47,16 @@ export async function deliver(
|
|
|
45
47
|
.prepare("UPDATE core_outbox SET status='sending',text=? WHERE id=?")
|
|
46
48
|
.run(outgoing, ids[i]);
|
|
47
49
|
try {
|
|
50
|
+
const first = i === 0 && (quote || at.length);
|
|
48
51
|
const result = await send(
|
|
49
52
|
message,
|
|
50
53
|
outgoing,
|
|
51
|
-
|
|
54
|
+
first
|
|
55
|
+
? {
|
|
56
|
+
...(quote ? { replyTo: quote } : {}),
|
|
57
|
+
...(at.length ? { at: at.map((p) => p.userId) } : {}),
|
|
58
|
+
}
|
|
59
|
+
: undefined,
|
|
52
60
|
);
|
|
53
61
|
repo.db
|
|
54
62
|
.prepare(
|
|
@@ -62,7 +70,9 @@ export async function deliver(
|
|
|
62
70
|
traceId: trace.id,
|
|
63
71
|
replyTargetIds: trace.decision?.targetMessageIds || [],
|
|
64
72
|
},
|
|
65
|
-
|
|
73
|
+
i === 0 && at.length
|
|
74
|
+
? `${at.map((p) => `@${p.name}`).join(" ")} ${outgoing}`
|
|
75
|
+
: outgoing,
|
|
66
76
|
result?.message_id,
|
|
67
77
|
now(),
|
|
68
78
|
);
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { Stickers } from "../stickers/index.js";
|
|
1
2
|
import { settleAftermath } from "../mind/aftermath.js";
|
|
2
3
|
import { randomUUID } from "node:crypto";
|
|
3
4
|
import { Repository } from "./repository.js";
|
|
@@ -64,6 +65,11 @@ export class ChatSystem {
|
|
|
64
65
|
embedding: this.mind.embedding,
|
|
65
66
|
});
|
|
66
67
|
this.compactor = new ContextCompactor(this.repo);
|
|
68
|
+
this.stickers = new Stickers(this.repo, {
|
|
69
|
+
models: this.models,
|
|
70
|
+
embedding: this.mind.embedding,
|
|
71
|
+
now: () => this.now(),
|
|
72
|
+
});
|
|
67
73
|
this.topics = new TopicTracker(this.repo);
|
|
68
74
|
this.capabilities = new Capabilities(this.repo, { now });
|
|
69
75
|
this.perceivers = new Perceivers(this.repo.db);
|
|
@@ -211,6 +217,14 @@ export class ChatSystem {
|
|
|
211
217
|
this.store.settings().memoryCandidates !== false
|
|
212
218
|
)
|
|
213
219
|
this.mind.memory.remember({ ...m, ...event });
|
|
220
|
+
if (!m.simulated)
|
|
221
|
+
try {
|
|
222
|
+
this.stickers.observe({ ...m, ...event }, (id) =>
|
|
223
|
+
this.mind.bonds.name(id),
|
|
224
|
+
);
|
|
225
|
+
} catch {
|
|
226
|
+
// A sticker outcome is a side note; never stop a message for it.
|
|
227
|
+
}
|
|
214
228
|
// Its vector is made while the batch gathers; the turn finds it cached.
|
|
215
229
|
if (!m.simulated && this.enabled(event.sessionId, { simulated: false }))
|
|
216
230
|
this.mind.recall.arrive(event.seq, this.now()).catch(() => false);
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { typedAt } from "../stickers/offer.js";
|
|
2
|
+
import { persistReply } from "./message-manager.js";
|
|
1
3
|
import { PRIVATELY_KNOWN } from "../mind/discretion.js";
|
|
2
4
|
import { replyFocus } from "./conversation-cues.js";
|
|
3
5
|
|
|
@@ -532,6 +534,19 @@ export class ReplyDelivery {
|
|
|
532
534
|
}
|
|
533
535
|
};
|
|
534
536
|
if (outdated()) return staleExit();
|
|
537
|
+
if (!turn.bubbles.length && turn.sticker && !turn.crisis?.clear) {
|
|
538
|
+
// Only a sticker: nothing to check in words, it was chosen from hers.
|
|
539
|
+
trace.response = { bubbles: [], sticker: turn.sticker.ref };
|
|
540
|
+
trace.sent = [];
|
|
541
|
+
const sent = await this.sendSticker(
|
|
542
|
+
batch.at(-1) || c.anchor,
|
|
543
|
+
turn,
|
|
544
|
+
trace,
|
|
545
|
+
session,
|
|
546
|
+
isCurrent,
|
|
547
|
+
);
|
|
548
|
+
return finish(sent ? "sent" : "cancelled", turn.reason);
|
|
549
|
+
}
|
|
535
550
|
let response = turn.bubbles.length
|
|
536
551
|
? normalizeResponse(
|
|
537
552
|
{ bubbles: turn.bubbles, reason: turn.reason },
|
|
@@ -701,6 +716,10 @@ export class ReplyDelivery {
|
|
|
701
716
|
this.owner.now(),
|
|
702
717
|
snapshot.inner?.currentLife?.current,
|
|
703
718
|
);
|
|
719
|
+
// A rewritten or generated reply may type an @ as text; it is made real.
|
|
720
|
+
const typed = typedAt(response.bubbles, turn.at || [], snapshot);
|
|
721
|
+
if (typed.bubbles.length)
|
|
722
|
+
response = { ...response, bubbles: typed.bubbles };
|
|
704
723
|
trace.response = response;
|
|
705
724
|
trace.sent = await deliver(
|
|
706
725
|
this.owner.repo,
|
|
@@ -712,6 +731,7 @@ export class ReplyDelivery {
|
|
|
712
731
|
{
|
|
713
732
|
now: this.owner.now,
|
|
714
733
|
quote: buriedTarget(batch, turn.targetMessageIds, session),
|
|
734
|
+
at: typed.at,
|
|
715
735
|
prepareBubble: (line) =>
|
|
716
736
|
this.owner.mind.time.reconcile(
|
|
717
737
|
{ bubbles: [line] },
|
|
@@ -722,6 +742,14 @@ export class ReplyDelivery {
|
|
|
722
742
|
},
|
|
723
743
|
);
|
|
724
744
|
if (!trace.sent.length && hasRelevantUpdate()) return staleExit();
|
|
745
|
+
if (trace.sent.length && turn.sticker)
|
|
746
|
+
await this.sendSticker(
|
|
747
|
+
batch.at(-1) || c.anchor,
|
|
748
|
+
turn,
|
|
749
|
+
trace,
|
|
750
|
+
session,
|
|
751
|
+
isCurrent,
|
|
752
|
+
);
|
|
725
753
|
if (trace.sent.length && !c.simulatedTurn) {
|
|
726
754
|
if (turn.share?.workId) {
|
|
727
755
|
try {
|
|
@@ -744,6 +772,50 @@ export class ReplyDelivery {
|
|
|
744
772
|
}
|
|
745
773
|
return finish(trace.sent.length ? "sent" : "cancelled", turn.reason);
|
|
746
774
|
}
|
|
775
|
+
// Her sticker, after her words. It is recorded as something she sent, and
|
|
776
|
+
// the use is kept to learn how it landed with this person.
|
|
777
|
+
async sendSticker(message, turn, trace, session, isCurrent) {
|
|
778
|
+
const owner = this.owner;
|
|
779
|
+
const image = owner.stickers?.library.image(turn.sticker.id);
|
|
780
|
+
if (!image || !isCurrent()) return false;
|
|
781
|
+
try {
|
|
782
|
+
await new Promise((r) => setTimeout(r, owner.stickerPause ?? 600));
|
|
783
|
+
const result = await owner.send(message, "", {
|
|
784
|
+
image: {
|
|
785
|
+
base64: Buffer.from(image.bytes).toString("base64"),
|
|
786
|
+
sticker: true,
|
|
787
|
+
summary: "[动画表情]",
|
|
788
|
+
},
|
|
789
|
+
});
|
|
790
|
+
const text = `[表情:${turn.sticker.looks}]`;
|
|
791
|
+
persistReply(
|
|
792
|
+
owner.repo,
|
|
793
|
+
{
|
|
794
|
+
...message,
|
|
795
|
+
traceId: trace.id,
|
|
796
|
+
replyTargetIds: turn.targetMessageIds || [],
|
|
797
|
+
},
|
|
798
|
+
text,
|
|
799
|
+
result?.message_id,
|
|
800
|
+
owner.now(),
|
|
801
|
+
);
|
|
802
|
+
(trace.sent ||= []).push(text);
|
|
803
|
+
if (!message?.simulated) {
|
|
804
|
+
const target = (message && (message.userId ?? message.speaker)) || "";
|
|
805
|
+
owner.stickers.used({
|
|
806
|
+
stickerId: turn.sticker.id,
|
|
807
|
+
session,
|
|
808
|
+
userId: target,
|
|
809
|
+
topic: turn.topic || "",
|
|
810
|
+
});
|
|
811
|
+
}
|
|
812
|
+
trace.steps.push(`发了一张表情:${turn.sticker.looks}`);
|
|
813
|
+
return true;
|
|
814
|
+
} catch (error) {
|
|
815
|
+
trace.steps.push(`表情没有发出去:${error.message}`);
|
|
816
|
+
return false;
|
|
817
|
+
}
|
|
818
|
+
}
|
|
747
819
|
}
|
|
748
820
|
|
|
749
821
|
// In a group, when several other people have spoken since the one message
|
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import { mediaSettings } from "../stickers/api.js";
|
|
2
|
+
import { ownerImageLimits } from "./image-preprocessor.js";
|
|
3
|
+
import { atCandidates, expressiveChannel } from "../stickers/offer.js";
|
|
1
4
|
import { speechLength } from "./speech-length.js";
|
|
2
5
|
import { parseSessionKey } from "../channels/session-key.js";
|
|
3
6
|
import { initiativeContext, initiativeSnapshot } from "./initiative-context.js";
|
|
@@ -455,6 +458,27 @@ export class TurnProcessor {
|
|
|
455
458
|
snapshot.inner = view.inner;
|
|
456
459
|
}
|
|
457
460
|
lap("context");
|
|
461
|
+
if (!ownInitiative && this.owner.stickers && expressiveChannel(session)) {
|
|
462
|
+
// A few stickers that could fit, and who she could @; she decides.
|
|
463
|
+
snapshot.canAt = atCandidates(snapshot, session);
|
|
464
|
+
try {
|
|
465
|
+
const last = snapshot.batch.at(-1) || {};
|
|
466
|
+
snapshot.stickers = await this.owner.stickers.candidates({
|
|
467
|
+
session,
|
|
468
|
+
text: snapshot.batch.map((m) => m.text || "").join("\n"),
|
|
469
|
+
mood: view.inner?.state || "",
|
|
470
|
+
addressee: String(last.speaker ?? last.userId ?? ""),
|
|
471
|
+
memories: (snapshot.memories || [])
|
|
472
|
+
.map((m) => m.content || m.text || "")
|
|
473
|
+
.slice(0, 6),
|
|
474
|
+
});
|
|
475
|
+
} catch (error) {
|
|
476
|
+
snapshot.stickers = [];
|
|
477
|
+
trace.steps.push(`表情候选暂不可用:${error.message}`);
|
|
478
|
+
}
|
|
479
|
+
if (!snapshot.canAt.length) delete snapshot.canAt;
|
|
480
|
+
if (!snapshot.stickers.length) delete snapshot.stickers;
|
|
481
|
+
}
|
|
458
482
|
const visionModel = model.vision
|
|
459
483
|
? model
|
|
460
484
|
: this.owner.fallbackFor(null, model).find((item) => item.vision) ||
|
|
@@ -474,6 +498,7 @@ export class TurnProcessor {
|
|
|
474
498
|
? await this.owner.loadVisionImages(early.pending, {
|
|
475
499
|
sessionId: snapshot.sessionId,
|
|
476
500
|
fetchImage: this.owner.fetchImage,
|
|
501
|
+
imageLimits: ownerImageLimits(mediaSettings(this.owner.repo)),
|
|
477
502
|
})
|
|
478
503
|
: { images: [], unavailable: [] };
|
|
479
504
|
const planned = classifyLoaded(
|
package/server/core/turn.js
CHANGED
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
import {
|
|
2
|
+
chosenAt,
|
|
3
|
+
chosenSticker,
|
|
4
|
+
expressionInput,
|
|
5
|
+
typedAt,
|
|
6
|
+
} from "../stickers/offer.js";
|
|
1
7
|
import { replyFocus } from "./conversation-cues.js";
|
|
2
8
|
import { INITIATIVE_PROMPT } from "../mind/initiative.js";
|
|
3
9
|
import { initiativeContext } from "./initiative-context.js";
|
|
@@ -159,6 +165,7 @@ export async function takeTurn(
|
|
|
159
165
|
...(recallWording(snapshot) ? { guidance: recallWording(snapshot) } : {}),
|
|
160
166
|
...(!initiating ? { exchange: currentExchange(snapshot) } : {}),
|
|
161
167
|
...turnGuides(snapshot),
|
|
168
|
+
...expressionInput(snapshot),
|
|
162
169
|
};
|
|
163
170
|
let raw = await models.call(
|
|
164
171
|
profile,
|
|
@@ -564,8 +571,8 @@ export function normalizeTurn(raw, snapshot, trace, { speech } = {}) {
|
|
|
564
571
|
.slice(0, 4),
|
|
565
572
|
crisis: crisis ? { clear: true, messageIds: crisisIds } : { clear: false },
|
|
566
573
|
act: botYield || stopped ? null : normalizeAct(raw.act, trace),
|
|
567
|
-
bubbles: bubbles.slice(0, bubbleLimit),
|
|
568
574
|
maxBubbles: bubbleLimit,
|
|
575
|
+
...expressionChoice(raw, snapshot, choice, bubbles.slice(0, bubbleLimit)),
|
|
569
576
|
};
|
|
570
577
|
}
|
|
571
578
|
|
|
@@ -639,3 +646,20 @@ export function typedSends(text) {
|
|
|
639
646
|
}
|
|
640
647
|
return sends;
|
|
641
648
|
}
|
|
649
|
+
|
|
650
|
+
// A sticker and @s are part of what she chose, kept only when they point at
|
|
651
|
+
// something she was actually offered this turn.
|
|
652
|
+
function expressionChoice(raw, snapshot, choice, bubbles) {
|
|
653
|
+
if (!["speak", "react"].includes(choice)) return { bubbles };
|
|
654
|
+
const sticker = chosenSticker(raw.sticker, snapshot);
|
|
655
|
+
const typed = typedAt(
|
|
656
|
+
bubbles,
|
|
657
|
+
chosenAt(raw.at, snapshot, snapshot.sessionId, ids(raw.targetMessageIds)),
|
|
658
|
+
snapshot,
|
|
659
|
+
);
|
|
660
|
+
return {
|
|
661
|
+
bubbles: typed.bubbles,
|
|
662
|
+
...(sticker ? { sticker } : {}),
|
|
663
|
+
...(typed.at.length ? { at: typed.at } : {}),
|
|
664
|
+
};
|
|
665
|
+
}
|