luckytri 1.0.7 → 1.0.9
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 +45 -24
- package/docs/en/guide.md +3 -1
- package/docs/zh/CHANGELOG.md +45 -24
- package/docs/zh/guide.md +3 -1
- package/package.json +1 -1
- package/public/guide.en.html +13 -5
- package/public/guide.html +12 -5
- package/public/tutorial.en.md +3 -1
- package/public/tutorial.md +3 -1
- package/server/core/context-builder.js +22 -3
- package/server/core/contribution-check.js +50 -0
- package/server/core/conversation-cues.js +29 -17
- package/server/core/conversation-grounding.js +377 -8
- package/server/core/dialogue-context.js +197 -2
- package/server/core/dialogue-contract.js +59 -0
- package/server/core/initiative-context.js +40 -18
- package/server/core/model-json.js +36 -3
- package/server/core/model-manager.js +7 -10
- package/server/core/model-presets.js +169 -24
- package/server/core/persona-manager.js +52 -12
- package/server/core/reader-check.js +166 -0
- package/server/core/reply-delivery.js +216 -10
- package/server/core/repository.js +33 -0
- package/server/core/response-generator.js +53 -10
- package/server/core/response-validator.js +42 -9
- package/server/core/turn-processor.js +26 -4
- package/server/core/turn.js +298 -51
- package/server/i18n/decisions.js +16 -0
- package/server/i18n/errors.js +4 -0
- package/server/i18n/labels.js +6 -5
- package/server/i18n/models.js +11 -2
- package/server/mind/expression-grounding.js +24 -0
- package/server/mind/own-voice.js +35 -0
- package/server/mind/reading.js +15 -3
- package/server/mind/time/activity-recall.js +65 -0
- 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 +11 -3
- 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 +39 -3
- package/server/mind/time/works.js +105 -15
- package/server/mind/view.js +29 -9
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(
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
const OWN_REFERENCE =
|
|
2
|
+
/你.{0,16}(?:不是说|明明说|说过|提过|答应过)|你跟.{0,16}(?:说|提过|答应过)|(?:上次|之前|刚才|昨天).{0,18}(?:你|我).{0,10}(?:说|提|答应)/;
|
|
3
|
+
|
|
4
|
+
// A same-person follow-up may point to what she just said without repeating
|
|
5
|
+
// a work's title. Use only replies actually addressed to this speaker as extra
|
|
6
|
+
// search terms; the private reply itself never enters the memory result.
|
|
7
|
+
export function activityRecallCue(
|
|
8
|
+
batch = [],
|
|
9
|
+
messages = [],
|
|
10
|
+
{ excludedSpeakers = new Set() } = {},
|
|
11
|
+
) {
|
|
12
|
+
const current = batch.filter(
|
|
13
|
+
(message) =>
|
|
14
|
+
message.role === "user" &&
|
|
15
|
+
!excludedSpeakers.has(String(message.speaker ?? message.userId ?? "")) &&
|
|
16
|
+
OWN_REFERENCE.test(message.text || ""),
|
|
17
|
+
);
|
|
18
|
+
if (!current.length) return batch;
|
|
19
|
+
|
|
20
|
+
const speakers = new Set(
|
|
21
|
+
current
|
|
22
|
+
.map((message) => String(message.speaker ?? message.userId ?? ""))
|
|
23
|
+
.filter(Boolean),
|
|
24
|
+
);
|
|
25
|
+
if (!speakers.size) return batch;
|
|
26
|
+
const byId = new Map(
|
|
27
|
+
messages.map((message) => [Number(message.seq ?? message.id), message]),
|
|
28
|
+
);
|
|
29
|
+
const cutoff = Math.min(
|
|
30
|
+
...batch
|
|
31
|
+
.map((message) => Number(message.id ?? message.seq))
|
|
32
|
+
.filter(Number.isFinite),
|
|
33
|
+
);
|
|
34
|
+
const now = Math.max(
|
|
35
|
+
...batch.map((message) => Number(message.time)).filter(Number.isFinite),
|
|
36
|
+
);
|
|
37
|
+
const replies = messages.filter((message) => {
|
|
38
|
+
if (message.role !== "assistant" || message.referenceOnly) return false;
|
|
39
|
+
if (Number.isFinite(message.id ?? message.seq) && Number.isFinite(cutoff)) {
|
|
40
|
+
const id = Number(message.id ?? message.seq);
|
|
41
|
+
if (
|
|
42
|
+
id >= cutoff &&
|
|
43
|
+
(!Number.isFinite(message.time) || message.time > now)
|
|
44
|
+
)
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
const targets = [
|
|
48
|
+
...(Array.isArray(message.replyTargets) ? message.replyTargets : []),
|
|
49
|
+
...(Array.isArray(message.replyTargetIds)
|
|
50
|
+
? message.replyTargetIds.map((id) => byId.get(Number(id)))
|
|
51
|
+
: []),
|
|
52
|
+
];
|
|
53
|
+
return targets.some((target) =>
|
|
54
|
+
speakers.has(String(target?.speaker ?? target?.userId ?? "")),
|
|
55
|
+
);
|
|
56
|
+
});
|
|
57
|
+
return [
|
|
58
|
+
...batch,
|
|
59
|
+
...replies.slice(-2).map((message) => ({
|
|
60
|
+
role: "assistant",
|
|
61
|
+
activityEvidence: true,
|
|
62
|
+
text: message.text || "",
|
|
63
|
+
})),
|
|
64
|
+
];
|
|
65
|
+
}
|
|
@@ -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,
|
|
@@ -422,7 +422,7 @@ export class TimeSystem {
|
|
|
422
422
|
}),
|
|
423
423
|
};
|
|
424
424
|
}
|
|
425
|
-
view({ session, now = this.now(), cue = [] } = {}) {
|
|
425
|
+
view({ session, now = this.now(), cue = [], activityCue = cue } = {}) {
|
|
426
426
|
const primary = this.primary(),
|
|
427
427
|
task = primary?.created <= now ? primary : null;
|
|
428
428
|
let current =
|
|
@@ -548,6 +548,11 @@ 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({
|
|
552
|
+
session,
|
|
553
|
+
now,
|
|
554
|
+
cue: activityCue,
|
|
555
|
+
}),
|
|
551
556
|
...(() => {
|
|
552
557
|
const actions = this.db
|
|
553
558
|
.prepare(
|
|
@@ -730,9 +735,12 @@ export class TimeSystem {
|
|
|
730
735
|
? task.share_state
|
|
731
736
|
: "none",
|
|
732
737
|
activityKind: task.activity === "game" ? "gaming" : task.activity,
|
|
738
|
+
...(task.activity === "game" ? { experienceMode: "reference" } : {}),
|
|
733
739
|
progress:
|
|
734
740
|
task.activity === "game"
|
|
735
|
-
?
|
|
741
|
+
? work?.title
|
|
742
|
+
? `已留下《${work.title}》这篇阅读笔记`
|
|
743
|
+
: "已接触一些游戏资料"
|
|
736
744
|
: `${work?.title || ""},已保存 ${work?.characters || 0} 字${state === "done" ? "完成稿" : "正文"}`,
|
|
737
745
|
project: task.project_id,
|
|
738
746
|
work: work?.id || null,
|
|
@@ -114,11 +114,38 @@ export function pluginActivities() {
|
|
|
114
114
|
return [...registry.values()].filter((def) => def.plugin);
|
|
115
115
|
}
|
|
116
116
|
|
|
117
|
-
//
|
|
117
|
+
// Identify her action before matching topic nouns. "She writes a novel and
|
|
118
|
+
// I promised to read it" is reading; the word "novel" is not a writing plan.
|
|
118
119
|
export function classifyActivity(words) {
|
|
119
120
|
const source = String(words || "");
|
|
121
|
+
const own =
|
|
122
|
+
source.match(/我(?:会|要|想|打算|准备|答应|先|来|再)([^]*)/) ||
|
|
123
|
+
source.match(/我(?=读|阅读|看|写|玩|游玩)([^]*)/);
|
|
124
|
+
const action = own?.[1] || source;
|
|
125
|
+
if (/^(?:让|请|希望)(?:你|他|她|对方)/.test(action)) return "unknown";
|
|
126
|
+
const verbs = [
|
|
127
|
+
["write", /写|续写|改写|创作/],
|
|
128
|
+
[
|
|
129
|
+
"read",
|
|
130
|
+
/阅读|读(?:书|稿|正文|完|了|过|一|这|那|你|她|他|它|《)|看.{0,12}(?:书|稿|正文|小说|文章|资料)/,
|
|
131
|
+
],
|
|
132
|
+
["game", /游玩|盲开|玩(?:一|个|下|这|那|《)|通关|打完.{0,10}章/],
|
|
133
|
+
["think", /思考|想一想|想想|整理思路/],
|
|
134
|
+
]
|
|
135
|
+
.map(([kind, pattern]) => ({ kind, at: action.search(pattern) }))
|
|
136
|
+
.filter((item) => item.at >= 0)
|
|
137
|
+
.sort((a, b) => a.at - b.at);
|
|
138
|
+
if (verbs.length) {
|
|
139
|
+
if (
|
|
140
|
+
verbs[0].kind === "read" &&
|
|
141
|
+
registry.get("game").pattern.test(action) &&
|
|
142
|
+
!/稿|正文|小说|故事|书|文章|诗/.test(action)
|
|
143
|
+
)
|
|
144
|
+
return "game";
|
|
145
|
+
return verbs[0].kind;
|
|
146
|
+
}
|
|
120
147
|
for (const kind of ["write", "game", "read", "think"])
|
|
121
|
-
if (registry.get(kind).pattern.test(
|
|
148
|
+
if (registry.get(kind).pattern.test(action)) return kind;
|
|
122
149
|
for (const def of registry.values())
|
|
123
150
|
if (def.plugin && def.enabled && def.pattern?.test(source)) return def.kind;
|
|
124
151
|
return "unknown";
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
const normalized = (value) =>
|
|
2
|
+
String(value || "")
|
|
3
|
+
.normalize("NFKC")
|
|
4
|
+
.toLowerCase()
|
|
5
|
+
.replace(/[\p{P}\p{Z}\s]/gu, "");
|
|
6
|
+
|
|
7
|
+
// A result rank or internal segment number is never evidence that a web page
|
|
8
|
+
// belongs to this game. Require its actual title in the source, not the query.
|
|
9
|
+
export function belongsToGame(source, topic) {
|
|
10
|
+
const name = normalized(topic);
|
|
11
|
+
if (name.length < 2) return false;
|
|
12
|
+
const title = normalized(source.title);
|
|
13
|
+
const content = normalized(source.content);
|
|
14
|
+
let url = "";
|
|
15
|
+
try {
|
|
16
|
+
url = normalized(decodeURIComponent(source.url || ""));
|
|
17
|
+
} catch {}
|
|
18
|
+
return title.includes(name) || content.includes(name) || url.includes(name);
|
|
19
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
export function awaitsTextInput(words) {
|
|
2
|
+
const value = String(words || "");
|
|
3
|
+
if (
|
|
4
|
+
/(?:已经|刚才|刚刚)(?:收到|拿到)|正文(?:已|已经)?(?:到手|收到了)/.test(
|
|
5
|
+
value,
|
|
6
|
+
)
|
|
7
|
+
)
|
|
8
|
+
return false;
|
|
9
|
+
return /等(?:你|他|她|对方).{0,35}(?:发来|发过来|递来|递过来|提供)|等.{0,25}(?:稿子|稿件|正文|全文|资料|题目|要求).{0,18}(?:发|递|到|来|齐|提供)/.test(
|
|
10
|
+
value,
|
|
11
|
+
);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function expectedTextTitle(task) {
|
|
15
|
+
return (
|
|
16
|
+
task.checkpoint?.expectedTextTitle ||
|
|
17
|
+
String(task.title || "").match(/[《「]([^》」]+)[》」]/)?.[1] ||
|
|
18
|
+
""
|
|
19
|
+
);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function nextReadingChunk(mind, task, now) {
|
|
23
|
+
const title = expectedTextTitle(task);
|
|
24
|
+
const pending = task.checkpoint?.pendingStep?.chunk;
|
|
25
|
+
if (pending) return !title || pending.title?.includes(title) ? pending : null;
|
|
26
|
+
if (task.checkpoint?.awaitedText && !title) return null;
|
|
27
|
+
return mind.reading.next(now, {
|
|
28
|
+
title,
|
|
29
|
+
after: task.checkpoint?.awaitedAfter || 0,
|
|
30
|
+
});
|
|
31
|
+
}
|
|
@@ -31,6 +31,8 @@ export class Sharing {
|
|
|
31
31
|
const work = this.time.works.get(workId);
|
|
32
32
|
if (!work || work.state !== "complete")
|
|
33
33
|
throw Error("只有真实完成稿可以交付");
|
|
34
|
+
if (!this.time.works.materialSupported(work))
|
|
35
|
+
throw Error("这篇记录引用了与作品无关的资料,不能作为游戏阅读进度分享");
|
|
34
36
|
if (
|
|
35
37
|
leaks([work.content], this.time.mind.meetings.privateSayings(session))
|
|
36
38
|
.length
|
|
@@ -154,6 +156,14 @@ export class Sharing {
|
|
|
154
156
|
if (!current || current.state !== "pending") return null;
|
|
155
157
|
share = current;
|
|
156
158
|
const work = this.time.works.get(share.work_id, share.version);
|
|
159
|
+
if (work && !this.time.works.materialSupported(work)) {
|
|
160
|
+
this.db
|
|
161
|
+
.prepare(
|
|
162
|
+
"UPDATE mind_time_shares SET state='declined',reason=? WHERE id=?",
|
|
163
|
+
)
|
|
164
|
+
.run("引用资料与作品无关,保留记录但停止分享", share.id);
|
|
165
|
+
return { status: "declined", reason: "引用资料与作品无关" };
|
|
166
|
+
}
|
|
157
167
|
if (
|
|
158
168
|
!work ||
|
|
159
169
|
!life.chat.enabled(share.session_id, { simulated: false }) ||
|