luckytri 1.0.7 → 1.0.8
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 +7 -3
- package/README.md +7 -3
- package/docs/en/CHANGELOG.md +10 -0
- package/docs/zh/CHANGELOG.md +10 -0
- package/package.json +1 -1
- package/public/guide.en.html +3 -3
- package/public/guide.html +3 -3
- package/server/core/conversation-cues.js +29 -9
- package/server/core/conversation-grounding.js +130 -4
- package/server/core/dialogue-context.js +194 -2
- package/server/core/dialogue-contract.js +35 -0
- package/server/core/initiative-context.js +4 -2
- package/server/core/model-manager.js +5 -8
- package/server/core/persona-manager.js +44 -9
- package/server/core/reader-check.js +62 -0
- package/server/core/reply-delivery.js +68 -3
- package/server/core/response-generator.js +43 -9
- package/server/core/response-validator.js +24 -6
- package/server/core/turn.js +43 -10
- package/server/i18n/errors.js +2 -0
- package/server/i18n/labels.js +6 -5
- package/server/mind/expression-grounding.js +21 -0
- package/server/mind/own-voice.js +35 -0
- package/server/mind/reading.js +15 -3
- package/server/mind/time/availability.js +23 -1
- package/server/mind/time/delivery-links.js +130 -130
- package/server/mind/time/executor.js +4 -2
- package/server/mind/time/games.js +13 -8
- package/server/mind/time/index.js +6 -2
- package/server/mind/time/kinds.js +29 -2
- package/server/mind/time/material-relevance.js +19 -0
- package/server/mind/time/reading-input.js +31 -0
- package/server/mind/time/sharing.js +10 -0
- package/server/mind/time/tasks.js +41 -3
- package/server/mind/time/works.js +104 -15
- package/server/mind/view.js +20 -7
package/server/core/turn.js
CHANGED
|
@@ -2,7 +2,12 @@ import { replyFocus } from "./conversation-cues.js";
|
|
|
2
2
|
import { INITIATIVE_PROMPT } from "../mind/initiative.js";
|
|
3
3
|
import { initiativeContext } from "./initiative-context.js";
|
|
4
4
|
import { conversationGrounding } from "./conversation-grounding.js";
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
currentExchange,
|
|
7
|
+
dialogueContext,
|
|
8
|
+
explicitStop,
|
|
9
|
+
} from "./dialogue-context.js";
|
|
10
|
+
import { readerQuestions } from "./reader-check.js";
|
|
6
11
|
|
|
7
12
|
export const CHOICES = ["speak", "react", "decline", "silent"];
|
|
8
13
|
const LEGACY = {
|
|
@@ -42,7 +47,7 @@ export function normalizeAct(raw, trace) {
|
|
|
42
47
|
|
|
43
48
|
export function recallWording(snapshot) {
|
|
44
49
|
if (snapshot.initiative) return null;
|
|
45
|
-
const continuity = snapshot.inner?.continuity;
|
|
50
|
+
const continuity = dialogueContext(snapshot).inner?.continuity;
|
|
46
51
|
const notes = [];
|
|
47
52
|
if (
|
|
48
53
|
continuity?.people?.some(
|
|
@@ -77,12 +82,6 @@ export async function takeTurn(
|
|
|
77
82
|
trace,
|
|
78
83
|
extra = {},
|
|
79
84
|
) {
|
|
80
|
-
const {
|
|
81
|
-
sourceRows: _rows,
|
|
82
|
-
batch: _batch,
|
|
83
|
-
persona: _persona,
|
|
84
|
-
...context
|
|
85
|
-
} = snapshot;
|
|
86
85
|
const images = extra.images || [];
|
|
87
86
|
const initiating =
|
|
88
87
|
extra.occasion && ["presence", "outreach"].includes(extra.occasion.type);
|
|
@@ -102,7 +101,7 @@ export async function takeTurn(
|
|
|
102
101
|
{
|
|
103
102
|
context: initiating
|
|
104
103
|
? initiativeContext(snapshot, extra.occasion)
|
|
105
|
-
:
|
|
104
|
+
: dialogueContext(snapshot),
|
|
106
105
|
...(extra.occasion
|
|
107
106
|
? {
|
|
108
107
|
occasion: initiating
|
|
@@ -148,6 +147,13 @@ export function normalizeTurn(raw, snapshot, trace) {
|
|
|
148
147
|
.slice(-1)
|
|
149
148
|
.map((m) => m.id);
|
|
150
149
|
}
|
|
150
|
+
if (
|
|
151
|
+
choice === "react" &&
|
|
152
|
+
readerQuestions(snapshot, { choice }).some((q) => targets.includes(q.id))
|
|
153
|
+
) {
|
|
154
|
+
choice = "speak";
|
|
155
|
+
trace?.steps?.push("对方在要求解释,保留完整回答空间,不按极短反应截断");
|
|
156
|
+
}
|
|
151
157
|
let bubbles = [];
|
|
152
158
|
if (Array.isArray(raw.bubbles)) bubbles = raw.bubbles;
|
|
153
159
|
else if (typeof raw.bubbles === "string") bubbles = [raw.bubbles];
|
|
@@ -161,6 +167,12 @@ export function normalizeTurn(raw, snapshot, trace) {
|
|
|
161
167
|
.filter((x) => typeof x === "string")
|
|
162
168
|
.map((x) => x.trim())
|
|
163
169
|
.filter(Boolean);
|
|
170
|
+
if (choice === "react" && bubbles.length === 1 && bubbles[0].length > 20) {
|
|
171
|
+
choice = "speak";
|
|
172
|
+
trace?.steps?.push(
|
|
173
|
+
"这是一句完整回应,按正常发言保留,不为短反应标签改写原意",
|
|
174
|
+
);
|
|
175
|
+
}
|
|
164
176
|
if (choice === "silent") bubbles = [];
|
|
165
177
|
const crisisIds = ids(raw.crisis?.messageIds).filter((id) =>
|
|
166
178
|
batchIds.has(id),
|
|
@@ -178,7 +190,20 @@ export function normalizeTurn(raw, snapshot, trace) {
|
|
|
178
190
|
trace?.steps?.push("有人可能处在危机里,底线要求认真回应");
|
|
179
191
|
}
|
|
180
192
|
const botLoop =
|
|
181
|
-
!crisis &&
|
|
193
|
+
!crisis &&
|
|
194
|
+
!snapshot.initiative &&
|
|
195
|
+
(conversationGrounding(snapshot).botLoop ||
|
|
196
|
+
(raw.contribution?.kind === "none" &&
|
|
197
|
+
!raw.act &&
|
|
198
|
+
!raw.share &&
|
|
199
|
+
!["chat", "rest"].includes(raw.attention?.action) &&
|
|
200
|
+
batch.length > 0 &&
|
|
201
|
+
batch.filter((m) => m.role === "user").length > 0 &&
|
|
202
|
+
batch
|
|
203
|
+
.filter((m) => m.role === "user")
|
|
204
|
+
.every((m) =>
|
|
205
|
+
conversationGrounding(snapshot).botMessageIds.includes(m.id),
|
|
206
|
+
)));
|
|
182
207
|
if (botLoop) {
|
|
183
208
|
choice = "silent";
|
|
184
209
|
bubbles = [];
|
|
@@ -233,6 +258,14 @@ export function normalizeTurn(raw, snapshot, trace) {
|
|
|
233
258
|
},
|
|
234
259
|
}
|
|
235
260
|
: {}),
|
|
261
|
+
...(raw.contribution && typeof raw.contribution === "object"
|
|
262
|
+
? {
|
|
263
|
+
contribution: {
|
|
264
|
+
kind: text(raw.contribution.kind, 20),
|
|
265
|
+
point: text(raw.contribution.point, 180),
|
|
266
|
+
},
|
|
267
|
+
}
|
|
268
|
+
: {}),
|
|
236
269
|
targetMessageIds: targets,
|
|
237
270
|
targetUserIds: [
|
|
238
271
|
...new Set(
|
package/server/i18n/errors.js
CHANGED
|
@@ -457,6 +457,8 @@ export const ERRORS = {
|
|
|
457
457
|
"正在推进活动步骤,请稍后测试":
|
|
458
458
|
"An activity step is in progress. Test again shortly.",
|
|
459
459
|
只有真实完成稿可以交付: "Only a genuinely finished piece can be delivered",
|
|
460
|
+
"这篇记录引用了与作品无关的资料,不能作为游戏阅读进度分享":
|
|
461
|
+
"This note uses material unrelated to the work and cannot be shared as reading progress",
|
|
460
462
|
正文包含其他会话的私下内容:
|
|
461
463
|
"The text contains private content from another session",
|
|
462
464
|
作品来源不允许在此会话分享:
|
package/server/i18n/labels.js
CHANGED
|
@@ -3,6 +3,10 @@
|
|
|
3
3
|
// key, exactly as the code writes it. Only a whole string that equals a key is
|
|
4
4
|
// translated, so what she writes herself is never touched.
|
|
5
5
|
export const LABELS = {
|
|
6
|
+
等待约定的正文或资料: "Waiting for the agreed manuscript or material",
|
|
7
|
+
等待来稿的阅读约定不能当成写作执行:
|
|
8
|
+
"A promise to read an incoming manuscript cannot run as writing",
|
|
9
|
+
指定的正文还不可阅读: "The specified manuscript is not available to read yet",
|
|
6
10
|
存在运行伴随文件: "Runtime sidecar files exist",
|
|
7
11
|
近期自动恢复点: "Recent automatic restore point",
|
|
8
12
|
// feedback.js
|
|
@@ -608,7 +612,6 @@ export const LABEL_PATTERNS = [
|
|
|
608
612
|
"Touched by others' words {0} times; last {1}, and she {2}",
|
|
609
613
|
],
|
|
610
614
|
["第 {0} / {1} 段", "Passage {0} / {1}"],
|
|
611
|
-
["主题片段 {0}", "Topic passage {0}"],
|
|
612
615
|
[
|
|
613
616
|
"正在玩,本段预计约 {0} 分钟",
|
|
614
617
|
"Playing; this span should take about {0} min",
|
|
@@ -618,10 +621,8 @@ export const LABEL_PATTERNS = [
|
|
|
618
621
|
"I finished piece {1} of “{0}”. The text is saved in the works library; the plot and characters in it are made up.",
|
|
619
622
|
],
|
|
620
623
|
["接续《{0}》第 {1} 篇", "Continue piece {1} of “{0}”"],
|
|
621
|
-
[
|
|
622
|
-
|
|
623
|
-
"Played passage {0} and left her own play record",
|
|
624
|
-
],
|
|
624
|
+
["已留下《{0}》这篇阅读笔记", "Saved a reading note titled “{0}”"],
|
|
625
|
+
["已接触一些游戏资料", "Has read some game reference material"],
|
|
625
626
|
["{0},已保存 {1} 字完成稿", "{0}, saved a finished piece of {1} characters"],
|
|
626
627
|
["{0},已保存 {1} 字正文", "{0}, saved {1} characters of text"],
|
|
627
628
|
[
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export function claimsLivedAction(content) {
|
|
2
|
+
return String(content || "")
|
|
3
|
+
.split(/[。!?\n]/)
|
|
4
|
+
.some(
|
|
5
|
+
(clause) =>
|
|
6
|
+
!/想|打算|准备|希望|假如|设想|如果/.test(clause) &&
|
|
7
|
+
/(?:刚才|刚刚|今天|这次|然后|真的|已经)[^。!?]{0,25}(?:读了|读上|读完|翻开|翻出来|写完|完成了|打开了|开始读|动手了)/.test(
|
|
8
|
+
clause,
|
|
9
|
+
),
|
|
10
|
+
);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function claimsSharedHistory(content) {
|
|
14
|
+
return /(?:之前|上次|早些时候)[^。!?\n]{0,25}(?:他|她|对方|那个人|你)[^。!?\n]{0,20}(?:说过|聊过|看过|读过|回话|私下)|(?:他|她|对方|那个人|你)[^。!?\n]{0,15}(?:之前|上次|读过|看过|说过)/.test(
|
|
15
|
+
String(content || ""),
|
|
16
|
+
);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export const EXPRESSION_GROUNDING = `核对一个准备保存的自我念头是否把愿望写成行动,或把别人的话题误认成共同经历。只输出JSON。
|
|
20
|
+
candidate只是草稿,不是事实。notes/self是过去的想法、愿望或理解;它们可以支持新的感受和看法,不能单独证明这次真的开始、读完或做完。actions/currentLife是实际活动记录。sources只表示引用了哪里,不自动证明动作。按实际记录核对这次声称新做过的具体动作和对象;不能用无关作品或一条待办证明完成。声称共同经历时,还须核对实际参与者和各自说过的话;在别处读到另一人的讨论不证明当前对象参与过。自己的新想法、偏好、假设、打算可以没有执行记录,也不需要已采取外部行动。只指出确实无依据的新经历与归属,不评文风。
|
|
21
|
+
输出 {"ok":true或false,"reason":"具体哪项新经历有或没有实际记录"}。`;
|
package/server/mind/own-voice.js
CHANGED
|
@@ -6,6 +6,11 @@ import { isPrivateSession } from "./memory.js";
|
|
|
6
6
|
import { sameRecentTheme } from "./novelty.js";
|
|
7
7
|
import { ownLife, sameSelf } from "./salience.js";
|
|
8
8
|
import { conversationOrigin, originSummary } from "./conversation-origin.js";
|
|
9
|
+
import {
|
|
10
|
+
claimsLivedAction,
|
|
11
|
+
claimsSharedHistory,
|
|
12
|
+
EXPRESSION_GROUNDING,
|
|
13
|
+
} from "./expression-grounding.js";
|
|
9
14
|
|
|
10
15
|
// After this many notes in a row that only take up her own last note, another
|
|
11
16
|
// one has to pick up something new or it is not kept.
|
|
@@ -203,6 +208,36 @@ export class OwnVoice {
|
|
|
203
208
|
sources.some((s) => !valid.has(s))
|
|
204
209
|
)
|
|
205
210
|
throw Error("自己的念头含有无效来源或敏感内容,未保存");
|
|
211
|
+
if (
|
|
212
|
+
claimsLivedAction([content, ...words].join("\n")) ||
|
|
213
|
+
claimsSharedHistory([content, result.reason, ...words].join("\n"))
|
|
214
|
+
) {
|
|
215
|
+
const grounded = await models.call(
|
|
216
|
+
profile,
|
|
217
|
+
"expression_grounding",
|
|
218
|
+
EXPRESSION_GROUNDING,
|
|
219
|
+
{
|
|
220
|
+
candidate: { note: content, words, sources },
|
|
221
|
+
actions: input.actions,
|
|
222
|
+
currentLife: input.currentLife,
|
|
223
|
+
notes: input.notes,
|
|
224
|
+
self: input.self,
|
|
225
|
+
},
|
|
226
|
+
trace,
|
|
227
|
+
);
|
|
228
|
+
if (grounded.ok !== true) {
|
|
229
|
+
status = "empty";
|
|
230
|
+
reason =
|
|
231
|
+
text(grounded.reason, 240) ||
|
|
232
|
+
"新行动经历没有得到实际记录支持,未保存";
|
|
233
|
+
return { status, reason, note: null };
|
|
234
|
+
}
|
|
235
|
+
if (life.closed || this.mind.nature.version() !== nature.version) {
|
|
236
|
+
status = "cancelled";
|
|
237
|
+
reason = "核对期间状态已变化";
|
|
238
|
+
return { status, reason, note: null };
|
|
239
|
+
}
|
|
240
|
+
}
|
|
206
241
|
// Several notes in a row already went round without taking anything
|
|
207
242
|
// new up. Another one that still does not is not kept.
|
|
208
243
|
const drifting =
|
package/server/mind/reading.js
CHANGED
|
@@ -16,7 +16,7 @@ export class Reading {
|
|
|
16
16
|
shared() {
|
|
17
17
|
return this.db
|
|
18
18
|
.prepare(
|
|
19
|
-
`SELECT c.id, c.document_id, c.ordinal, c.heading, c.text, d.title,
|
|
19
|
+
`SELECT c.id, c.document_id, c.ordinal, c.heading, c.text, d.title,d.created documentCreated,
|
|
20
20
|
(SELECT COUNT(*) FROM core_chunks x WHERE x.document_id=c.document_id) total
|
|
21
21
|
FROM core_chunks c
|
|
22
22
|
JOIN core_documents d ON d.id=c.document_id AND d.status='ready'
|
|
@@ -28,8 +28,20 @@ export class Reading {
|
|
|
28
28
|
}
|
|
29
29
|
// The next part of something she already started, or the unread passage
|
|
30
30
|
// closest to what she is living for, then what she likes and wonders about.
|
|
31
|
-
next(now = Date.now()) {
|
|
32
|
-
const
|
|
31
|
+
next(now = Date.now(), { title = "", after = 0 } = {}) {
|
|
32
|
+
const clean = (value) =>
|
|
33
|
+
String(value || "")
|
|
34
|
+
.toLowerCase()
|
|
35
|
+
.replace(/[\s\p{P}]/gu, "");
|
|
36
|
+
const wanted = clean(title);
|
|
37
|
+
const unread = this.shared().filter(
|
|
38
|
+
(chunk) =>
|
|
39
|
+
(!wanted ||
|
|
40
|
+
clean(chunk.title) === wanted ||
|
|
41
|
+
(wanted.length >= 2 && clean(chunk.title).includes(wanted))) &&
|
|
42
|
+
(!after ||
|
|
43
|
+
(chunk.documentCreated >= after && chunk.documentCreated <= now)),
|
|
44
|
+
);
|
|
33
45
|
if (!unread.length) return null;
|
|
34
46
|
const last = this.db
|
|
35
47
|
.prepare(
|
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
import { gameTopic } from "./intent.js";
|
|
2
|
-
import { isExecutable, needsEnergy } from "./kinds.js";
|
|
2
|
+
import { isExecutable, needsEnergy, classifyActivity } from "./kinds.js";
|
|
3
|
+
import {
|
|
4
|
+
nextReadingChunk,
|
|
5
|
+
expectedTextTitle,
|
|
6
|
+
awaitsTextInput,
|
|
7
|
+
} from "./reading-input.js";
|
|
3
8
|
export function unsupportedAction(task) {
|
|
4
9
|
return task.activity === "unknown" &&
|
|
5
10
|
/点.{0,8}外卖|下单|购买|实拍|拍照给|尝.{0,6}(?:味道|好吃)/.test(task.title)
|
|
@@ -18,6 +23,23 @@ export function executionBlock(time, task, now) {
|
|
|
18
23
|
const life = time.search.life;
|
|
19
24
|
const unsupported = unsupportedAction(task);
|
|
20
25
|
if (unsupported) return unsupported;
|
|
26
|
+
if (
|
|
27
|
+
task.activity === "write" &&
|
|
28
|
+
awaitsTextInput(task.title) &&
|
|
29
|
+
classifyActivity(task.title) === "read"
|
|
30
|
+
)
|
|
31
|
+
return "等待来稿的阅读约定不能当成写作执行";
|
|
32
|
+
if (
|
|
33
|
+
task.checkpoint.awaitedText &&
|
|
34
|
+
(task.activity !== "read" || !nextReadingChunk(time.mind, task, now))
|
|
35
|
+
)
|
|
36
|
+
return "等待约定的正文或资料";
|
|
37
|
+
if (
|
|
38
|
+
task.activity === "read" &&
|
|
39
|
+
expectedTextTitle(task) &&
|
|
40
|
+
!nextReadingChunk(time.mind, task, now)
|
|
41
|
+
)
|
|
42
|
+
return "指定的正文还不可阅读";
|
|
21
43
|
if (task.activity === "unknown" || !isExecutable(task.activity))
|
|
22
44
|
return task.activity === "unknown"
|
|
23
45
|
? "需要把行动内容想具体"
|
|
@@ -1,130 +1,130 @@
|
|
|
1
|
-
import { evidence, parse } from "../util.js";
|
|
2
|
-
import { intentRecipient } from "./intent.js";
|
|
3
|
-
|
|
4
|
-
export class DeliveryLinks {
|
|
5
|
-
constructor(tasks) {
|
|
6
|
-
this.tasks = tasks;
|
|
7
|
-
this.time = tasks.time;
|
|
8
|
-
this.db = tasks.db;
|
|
9
|
-
}
|
|
10
|
-
resolve(row, message, turn = {}, now = this.time.now()) {
|
|
11
|
-
if (
|
|
12
|
-
!/(?:发(?:给)?你|给你(?:看|读|发)|这就发|已经发)/.test(message.text || "")
|
|
13
|
-
)
|
|
14
|
-
return null;
|
|
15
|
-
const questions = (message.replyTargetIds || turn.targetMessageIds || [])
|
|
16
|
-
.map((id) =>
|
|
17
|
-
this.db
|
|
18
|
-
.prepare(
|
|
19
|
-
"SELECT payload FROM core_events WHERE seq=? AND session_id=? AND role='user'",
|
|
20
|
-
)
|
|
21
|
-
.get(Number(id), row.session_id),
|
|
22
|
-
)
|
|
23
|
-
.filter(Boolean)
|
|
24
|
-
.map((q) => parse(q.payload, {}).text || "");
|
|
25
|
-
const cues = [message.text, ...questions].join(" ");
|
|
26
|
-
const works = this.db
|
|
27
|
-
.prepare(
|
|
28
|
-
"SELECT w.*,v.sources,v.content FROM mind_time_works w JOIN mind_time_versions v ON v.work_id=w.id AND v.version=w.version WHERE w.state='complete' AND w.created<=? AND w.task_id IS NOT NULL ORDER BY w.updated DESC LIMIT 30",
|
|
29
|
-
)
|
|
30
|
-
.all(now)
|
|
31
|
-
.map((w) => ({ ...w, sources: parse(w.sources, []) }))
|
|
32
|
-
.filter(
|
|
33
|
-
(w) =>
|
|
34
|
-
this.time.visible(w, row.session_id, now) &&
|
|
35
|
-
(!w.session_id || w.session_id === row.session_id),
|
|
36
|
-
);
|
|
37
|
-
const named = works.filter((w) => cues.includes(w.title)),
|
|
38
|
-
candidates = named.length
|
|
39
|
-
? named
|
|
40
|
-
: /小说|短篇|作品|诗|正文|札记/.test(cues)
|
|
41
|
-
? works
|
|
42
|
-
: [];
|
|
43
|
-
if (candidates.length !== 1) return null;
|
|
44
|
-
const work = candidates[0],
|
|
45
|
-
task = this.tasks.get(work.task_id);
|
|
46
|
-
if (!task || task.state !== "done") return null;
|
|
47
|
-
const subject = intentRecipient(this.db, {
|
|
48
|
-
session: row.session_id,
|
|
49
|
-
subject: turn.targetUserIds?.[0],
|
|
50
|
-
title: message.text,
|
|
51
|
-
sources: [row.seq],
|
|
52
|
-
});
|
|
53
|
-
if (!subject) return null;
|
|
54
|
-
const share = this.db
|
|
55
|
-
.prepare(
|
|
56
|
-
"SELECT * FROM mind_time_shares WHERE work_id=? AND version=? AND session_id=?",
|
|
57
|
-
)
|
|
58
|
-
.get(work.id, work.version, row.session_id);
|
|
59
|
-
const state =
|
|
60
|
-
share?.state === "sent" && share.offset >= Array.from(work.content).length
|
|
61
|
-
? "sent"
|
|
62
|
-
: share?.state === "sent" && share.delivery_kind === "report"
|
|
63
|
-
? "reported"
|
|
64
|
-
: share?.state || "waiting";
|
|
65
|
-
const sources = evidence([...task.sources, `m:${row.seq}`]);
|
|
66
|
-
this.db
|
|
67
|
-
.prepare(
|
|
68
|
-
"UPDATE mind_time_tasks SET kind='promise',subject=?,sources=?,share_state=?,updated=? WHERE id=?",
|
|
69
|
-
)
|
|
70
|
-
.run(subject, JSON.stringify(sources), state, now, task.id);
|
|
71
|
-
if (!task.sources.includes(`m:${row.seq}`))
|
|
72
|
-
this.time.event(
|
|
73
|
-
task.id,
|
|
74
|
-
"delivery-linked",
|
|
75
|
-
"分享承诺归回已有作品,未新增创作待办",
|
|
76
|
-
{ source: `m:${row.seq}`, workId: work.id, delivery: state },
|
|
77
|
-
now,
|
|
78
|
-
);
|
|
79
|
-
return task.id;
|
|
80
|
-
}
|
|
81
|
-
reconcile(now = this.time.now()) {
|
|
82
|
-
const fixed = [];
|
|
83
|
-
for (const task of this.db
|
|
84
|
-
.prepare(
|
|
85
|
-
"SELECT * FROM mind_time_tasks WHERE activity='unknown' AND state IN ('todo','waiting') AND json_extract(checkpoint,'$.mergedInto') IS NULL",
|
|
86
|
-
)
|
|
87
|
-
.all()) {
|
|
88
|
-
for (const ref of parse(task.sources, []).filter((r) =>
|
|
89
|
-
r.startsWith("m:"),
|
|
90
|
-
)) {
|
|
91
|
-
const row = this.db
|
|
92
|
-
.prepare(
|
|
93
|
-
"SELECT seq,session_id,time,payload FROM core_events WHERE seq=? AND time<=?",
|
|
94
|
-
)
|
|
95
|
-
.get(Number(ref.slice(2)), now);
|
|
96
|
-
if (!row) continue;
|
|
97
|
-
const canonical = this.resolve(
|
|
98
|
-
row,
|
|
99
|
-
parse(row.payload, {}),
|
|
100
|
-
{ targetUserIds: task.subject ? [task.subject] : [] },
|
|
101
|
-
now,
|
|
102
|
-
);
|
|
103
|
-
if (!canonical || canonical === task.id) continue;
|
|
104
|
-
this.tasks.links.link("task", task.id, canonical, now);
|
|
105
|
-
this.db
|
|
106
|
-
.prepare(
|
|
107
|
-
"UPDATE mind_time_tasks SET state='abandoned',checkpoint=?,wait_reason=?,revision=revision+1 WHERE id=?",
|
|
108
|
-
)
|
|
109
|
-
.run(
|
|
110
|
-
JSON.stringify({
|
|
111
|
-
...parse(task.checkpoint, {}),
|
|
112
|
-
mergedInto: canonical,
|
|
113
|
-
}),
|
|
114
|
-
"已归入已有作品的交付记录",
|
|
115
|
-
task.id,
|
|
116
|
-
);
|
|
117
|
-
this.time.event(
|
|
118
|
-
task.id,
|
|
119
|
-
"merged",
|
|
120
|
-
"交付有真实作品与送达记录,归回原事项",
|
|
121
|
-
{ taskId: canonical },
|
|
122
|
-
now,
|
|
123
|
-
);
|
|
124
|
-
fixed.push({ from: task.id, to: canonical });
|
|
125
|
-
break;
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
return fixed;
|
|
129
|
-
}
|
|
130
|
-
}
|
|
1
|
+
import { evidence, parse } from "../util.js";
|
|
2
|
+
import { intentRecipient } from "./intent.js";
|
|
3
|
+
|
|
4
|
+
export class DeliveryLinks {
|
|
5
|
+
constructor(tasks) {
|
|
6
|
+
this.tasks = tasks;
|
|
7
|
+
this.time = tasks.time;
|
|
8
|
+
this.db = tasks.db;
|
|
9
|
+
}
|
|
10
|
+
resolve(row, message, turn = {}, now = this.time.now()) {
|
|
11
|
+
if (
|
|
12
|
+
!/(?:发(?:给)?你|给你(?:看|读|发)|这就发|已经发)/.test(message.text || "")
|
|
13
|
+
)
|
|
14
|
+
return null;
|
|
15
|
+
const questions = (message.replyTargetIds || turn.targetMessageIds || [])
|
|
16
|
+
.map((id) =>
|
|
17
|
+
this.db
|
|
18
|
+
.prepare(
|
|
19
|
+
"SELECT payload FROM core_events WHERE seq=? AND session_id=? AND role='user'",
|
|
20
|
+
)
|
|
21
|
+
.get(Number(id), row.session_id),
|
|
22
|
+
)
|
|
23
|
+
.filter(Boolean)
|
|
24
|
+
.map((q) => parse(q.payload, {}).text || "");
|
|
25
|
+
const cues = [message.text, ...questions].join(" ");
|
|
26
|
+
const works = this.db
|
|
27
|
+
.prepare(
|
|
28
|
+
"SELECT w.*,v.sources,v.content FROM mind_time_works w JOIN mind_time_versions v ON v.work_id=w.id AND v.version=w.version WHERE w.state='complete' AND w.created<=? AND w.task_id IS NOT NULL ORDER BY w.updated DESC LIMIT 30",
|
|
29
|
+
)
|
|
30
|
+
.all(now)
|
|
31
|
+
.map((w) => ({ ...w, sources: parse(w.sources, []) }))
|
|
32
|
+
.filter(
|
|
33
|
+
(w) =>
|
|
34
|
+
this.time.visible(w, row.session_id, now) &&
|
|
35
|
+
(!w.session_id || w.session_id === row.session_id),
|
|
36
|
+
);
|
|
37
|
+
const named = works.filter((w) => cues.includes(w.title)),
|
|
38
|
+
candidates = named.length
|
|
39
|
+
? named
|
|
40
|
+
: /小说|短篇|作品|诗|正文|札记/.test(cues)
|
|
41
|
+
? works
|
|
42
|
+
: [];
|
|
43
|
+
if (candidates.length !== 1) return null;
|
|
44
|
+
const work = candidates[0],
|
|
45
|
+
task = this.tasks.get(work.task_id);
|
|
46
|
+
if (!task || task.state !== "done") return null;
|
|
47
|
+
const subject = intentRecipient(this.db, {
|
|
48
|
+
session: row.session_id,
|
|
49
|
+
subject: turn.targetUserIds?.[0],
|
|
50
|
+
title: message.text,
|
|
51
|
+
sources: [row.seq],
|
|
52
|
+
});
|
|
53
|
+
if (!subject) return null;
|
|
54
|
+
const share = this.db
|
|
55
|
+
.prepare(
|
|
56
|
+
"SELECT * FROM mind_time_shares WHERE work_id=? AND version=? AND session_id=?",
|
|
57
|
+
)
|
|
58
|
+
.get(work.id, work.version, row.session_id);
|
|
59
|
+
const state =
|
|
60
|
+
share?.state === "sent" && share.offset >= Array.from(work.content).length
|
|
61
|
+
? "sent"
|
|
62
|
+
: share?.state === "sent" && share.delivery_kind === "report"
|
|
63
|
+
? "reported"
|
|
64
|
+
: share?.state || "waiting";
|
|
65
|
+
const sources = evidence([...task.sources, `m:${row.seq}`]);
|
|
66
|
+
this.db
|
|
67
|
+
.prepare(
|
|
68
|
+
"UPDATE mind_time_tasks SET kind='promise',subject=?,sources=?,share_state=?,updated=? WHERE id=?",
|
|
69
|
+
)
|
|
70
|
+
.run(subject, JSON.stringify(sources), state, now, task.id);
|
|
71
|
+
if (!task.sources.includes(`m:${row.seq}`))
|
|
72
|
+
this.time.event(
|
|
73
|
+
task.id,
|
|
74
|
+
"delivery-linked",
|
|
75
|
+
"分享承诺归回已有作品,未新增创作待办",
|
|
76
|
+
{ source: `m:${row.seq}`, workId: work.id, delivery: state },
|
|
77
|
+
now,
|
|
78
|
+
);
|
|
79
|
+
return task.id;
|
|
80
|
+
}
|
|
81
|
+
reconcile(now = this.time.now()) {
|
|
82
|
+
const fixed = [];
|
|
83
|
+
for (const task of this.db
|
|
84
|
+
.prepare(
|
|
85
|
+
"SELECT * FROM mind_time_tasks WHERE activity='unknown' AND state IN ('todo','waiting') AND json_extract(checkpoint,'$.mergedInto') IS NULL",
|
|
86
|
+
)
|
|
87
|
+
.all()) {
|
|
88
|
+
for (const ref of parse(task.sources, []).filter((r) =>
|
|
89
|
+
r.startsWith("m:"),
|
|
90
|
+
)) {
|
|
91
|
+
const row = this.db
|
|
92
|
+
.prepare(
|
|
93
|
+
"SELECT seq,session_id,time,payload FROM core_events WHERE seq=? AND time<=?",
|
|
94
|
+
)
|
|
95
|
+
.get(Number(ref.slice(2)), now);
|
|
96
|
+
if (!row) continue;
|
|
97
|
+
const canonical = this.resolve(
|
|
98
|
+
row,
|
|
99
|
+
parse(row.payload, {}),
|
|
100
|
+
{ targetUserIds: task.subject ? [task.subject] : [] },
|
|
101
|
+
now,
|
|
102
|
+
);
|
|
103
|
+
if (!canonical || canonical === task.id) continue;
|
|
104
|
+
this.tasks.links.link("task", task.id, canonical, now);
|
|
105
|
+
this.db
|
|
106
|
+
.prepare(
|
|
107
|
+
"UPDATE mind_time_tasks SET state='abandoned',checkpoint=?,wait_reason=?,revision=revision+1 WHERE id=?",
|
|
108
|
+
)
|
|
109
|
+
.run(
|
|
110
|
+
JSON.stringify({
|
|
111
|
+
...parse(task.checkpoint, {}),
|
|
112
|
+
mergedInto: canonical,
|
|
113
|
+
}),
|
|
114
|
+
"已归入已有作品的交付记录",
|
|
115
|
+
task.id,
|
|
116
|
+
);
|
|
117
|
+
this.time.event(
|
|
118
|
+
task.id,
|
|
119
|
+
"merged",
|
|
120
|
+
"交付有真实作品与送达记录,归回原事项",
|
|
121
|
+
{ taskId: canonical },
|
|
122
|
+
now,
|
|
123
|
+
);
|
|
124
|
+
fixed.push({ from: task.id, to: canonical });
|
|
125
|
+
break;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
return fixed;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
@@ -7,6 +7,7 @@ import { ACTIVITY_LABELS } from "./tasks.js";
|
|
|
7
7
|
import { activityLabel, isPluginActivity, needsEnergy } from "./kinds.js";
|
|
8
8
|
import { stepPlugin } from "./plugin-activity.js";
|
|
9
9
|
import { executionBlock } from "./availability.js";
|
|
10
|
+
import { nextReadingChunk } from "./reading-input.js";
|
|
10
11
|
|
|
11
12
|
const ACTIVITY_PROMPT =
|
|
12
13
|
'执行一个属于自己的实际活动步骤。write创作一个完整小段,已有草稿时只追加下一段,修改模式则提供修改后的正文;根据篇幅决定本篇是否结束,连载完成的是本章。read只能阅读input.reading实际提供的段落;think留下想法记录。不编造外部操作、游玩、通关或未提供的资料。虚构人物只属于作品,不能进入真实人物记忆。不输出隐藏推理。外部suggestion是别人的建议,你决定采纳或拒绝并保留理由,可以改变方向、暂停或完结项目。输出JSON {"done":false,"title":"标题","content":"最多1800字完整小段","summary":"情节摘要","next":"续接位置","bible":{"characters":"人物设定","world":"世界设定","threads":"未解决线索"},"feeling":{"feeling":"两三个字的心情","valence":0.2},"accepted":true,"reason":"自己的选择理由","projectState":"active|paused|complete","share":{"choice":"send|later|decline","reason":"分享选择"}}。完成本篇时done:true;休息/拒绝时没有content并说明reason。本步骤不发消息、不直接改变人格。';
|
|
@@ -149,8 +150,9 @@ export class TimeExecutor {
|
|
|
149
150
|
}
|
|
150
151
|
}
|
|
151
152
|
chunk =
|
|
152
|
-
candidate.
|
|
153
|
-
|
|
153
|
+
candidate.activity === "read"
|
|
154
|
+
? nextReadingChunk(mind, candidate, now)
|
|
155
|
+
: candidate.checkpoint.pendingStep?.chunk || null;
|
|
154
156
|
if (candidate.activity === "read" && !chunk) {
|
|
155
157
|
time.tasks.wait(candidate, "书架没有可读资料", now, now);
|
|
156
158
|
continue;
|
|
@@ -4,6 +4,7 @@ import { evidence, text, parse } from "../util.js";
|
|
|
4
4
|
import { gameTopic, intentUnit } from "./intent.js";
|
|
5
5
|
import { naturalGameReason, gameContract, gameText } from "./presentation.js";
|
|
6
6
|
import { gameName } from "./availability.js";
|
|
7
|
+
import { belongsToGame } from "./material-relevance.js";
|
|
7
8
|
|
|
8
9
|
export class Games {
|
|
9
10
|
constructor(time) {
|
|
@@ -93,7 +94,7 @@ export class Games {
|
|
|
93
94
|
"SELECT title,content,kind,uncertainty FROM mind_time_sources WHERE id=? AND project_id=?",
|
|
94
95
|
)
|
|
95
96
|
.get(task.checkpoint.sourceIds[0], task.project_id);
|
|
96
|
-
if (!source) return null;
|
|
97
|
+
if (!source || !belongsToGame(source, this.topic(task))) return null;
|
|
97
98
|
const active = this.time.clock.view(task, now).progress;
|
|
98
99
|
const window = task.checkpoint.readWindow;
|
|
99
100
|
const progress = window
|
|
@@ -229,11 +230,7 @@ export class Games {
|
|
|
229
230
|
" 游戏 剧情 资料 " +
|
|
230
231
|
(contract?.unit?.label || "") +
|
|
231
232
|
" " +
|
|
232
|
-
(
|
|
233
|
-
? `主题片段 ${checkpoint.segment + 1}`
|
|
234
|
-
: contract
|
|
235
|
-
? "内容与第一印象"
|
|
236
|
-
: "简介"),
|
|
233
|
+
(contract ? "内容与第一印象" : "剧情与角色"),
|
|
237
234
|
{
|
|
238
235
|
projectId: project.id,
|
|
239
236
|
now,
|
|
@@ -247,7 +244,9 @@ export class Games {
|
|
|
247
244
|
if (!time.valid(task))
|
|
248
245
|
return { status: "cancelled", reason: "资料返回时任务已经变化" };
|
|
249
246
|
const seen = new Set(checkpoint.seenSources || []),
|
|
250
|
-
fresh = sources.filter(
|
|
247
|
+
fresh = sources.filter(
|
|
248
|
+
(s) => !seen.has(s.id) && belongsToGame(s, topic),
|
|
249
|
+
);
|
|
251
250
|
if (!fresh.length) {
|
|
252
251
|
time.tasks.wait(
|
|
253
252
|
task,
|
|
@@ -369,9 +368,15 @@ export class Games {
|
|
|
369
368
|
)
|
|
370
369
|
.get(id, project.id),
|
|
371
370
|
)
|
|
372
|
-
.filter(
|
|
371
|
+
.filter((s) => s && belongsToGame(s, topic));
|
|
373
372
|
if (!sources.length) {
|
|
374
373
|
time.tasks.wait(task, "本段资料已不可用", now + 3600000, now);
|
|
374
|
+
const next = { ...checkpoint, sourceIds: [], stage: "search" };
|
|
375
|
+
delete next.readWindow;
|
|
376
|
+
delete next.activityClock;
|
|
377
|
+
this.db
|
|
378
|
+
.prepare("UPDATE mind_time_tasks SET checkpoint=? WHERE id=?")
|
|
379
|
+
.run(JSON.stringify(next), task.id);
|
|
375
380
|
return { status: "waiting", reason: "资料不可用" };
|
|
376
381
|
}
|
|
377
382
|
const prompt =
|
|
@@ -298,8 +298,8 @@ export class TimeSystem {
|
|
|
298
298
|
}
|
|
299
299
|
visible(row, session, now = this.now()) {
|
|
300
300
|
if (session === undefined) return true;
|
|
301
|
-
if (!this.allowed(row, now)) return false;
|
|
302
301
|
if (row.discretion !== "open" && row.session_id !== session) return false;
|
|
302
|
+
if (!this.allowed(row, now)) return false;
|
|
303
303
|
return this.mind.meetings.stays(
|
|
304
304
|
{ ...row, sources: evidence(row.sources) },
|
|
305
305
|
session,
|
|
@@ -548,6 +548,7 @@ export class TimeSystem {
|
|
|
548
548
|
plannedFor: this.planView(row),
|
|
549
549
|
})),
|
|
550
550
|
works: this.works.fragments({ session, now, cue }),
|
|
551
|
+
activityRecall: this.works.activityEvidence({ session, now, cue }),
|
|
551
552
|
...(() => {
|
|
552
553
|
const actions = this.db
|
|
553
554
|
.prepare(
|
|
@@ -730,9 +731,12 @@ export class TimeSystem {
|
|
|
730
731
|
? task.share_state
|
|
731
732
|
: "none",
|
|
732
733
|
activityKind: task.activity === "game" ? "gaming" : task.activity,
|
|
734
|
+
...(task.activity === "game" ? { experienceMode: "reference" } : {}),
|
|
733
735
|
progress:
|
|
734
736
|
task.activity === "game"
|
|
735
|
-
?
|
|
737
|
+
? work?.title
|
|
738
|
+
? `已留下《${work.title}》这篇阅读笔记`
|
|
739
|
+
: "已接触一些游戏资料"
|
|
736
740
|
: `${work?.title || ""},已保存 ${work?.characters || 0} 字${state === "done" ? "完成稿" : "正文"}`,
|
|
737
741
|
project: task.project_id,
|
|
738
742
|
work: work?.id || null,
|