luckytri 1.0.0 → 1.0.2

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.
Files changed (139) hide show
  1. package/.github/SECURITY.md +4 -4
  2. package/README.en.md +252 -250
  3. package/README.md +252 -250
  4. package/bin/luckytri.js +5 -1
  5. package/docs/en/CHANGELOG.md +8 -0
  6. package/docs/en/connect.md +18 -18
  7. package/docs/en/guide.md +202 -216
  8. package/docs/en/npm-release.md +25 -25
  9. package/docs/zh/CHANGELOG.md +8 -0
  10. package/docs/zh/connect.md +17 -17
  11. package/docs/zh/guide.md +202 -216
  12. package/docs/zh/npm-release.md +33 -33
  13. package/package.json +110 -108
  14. package/plugins/sdk/index.d.ts +155 -155
  15. package/plugins/sdk/testing.js +58 -58
  16. package/plugins/weather/index.js +82 -82
  17. package/plugins/weather/luckytri-plugin.json +29 -29
  18. package/public/app/assets/ChatsPage-BhlJHseG.js +3 -0
  19. package/public/app/assets/Empty-DP6WMGHi.js +2 -0
  20. package/public/app/assets/Field-CtbdqaiB.js +1 -0
  21. package/public/app/assets/HeartPage-32BeTPn2.js +1 -0
  22. package/public/app/assets/LifePage-CxSm0XKI.js +1 -0
  23. package/public/app/assets/MemoryPage-BeKBcPCY.js +1 -0
  24. package/public/app/assets/NaturePage-NEPYl2HS.js +3 -0
  25. package/public/app/assets/PeoplePage-DxtpekwM.js +1 -0
  26. package/public/app/assets/PluginsPage-CVVQwEBI.css +1 -0
  27. package/public/app/assets/PluginsPage-DksEZbUU.js +2 -0
  28. package/public/app/assets/{SystemPage-CAMPoYqM.css → SystemPage-BkRu0Rvx.css} +1 -1
  29. package/public/app/assets/SystemPage-BvzJlHuY.js +1 -0
  30. package/public/app/assets/TimePage-7-awH5XL.js +3 -0
  31. package/public/app/assets/index-4rItIkD-.js +4 -0
  32. package/public/app/assets/{index-YDz1ree1.css → index-DY4e15Kh.css} +1 -1
  33. package/public/app/assets/{knowledge-DG0e0uKw.js → knowledge-C6_HvoyB.js} +1 -1
  34. package/public/app/assets/media-SMfTT-yc.js +1 -0
  35. package/public/app/index.html +3 -3
  36. package/public/guide.en.html +72 -99
  37. package/public/guide.html +44 -86
  38. package/public/tutorial.en.md +25 -39
  39. package/public/tutorial.md +25 -39
  40. package/scripts/backup.js +311 -311
  41. package/scripts/browser.js +41 -41
  42. package/scripts/import-data.js +128 -0
  43. package/scripts/launch.js +101 -94
  44. package/scripts/plugin.js +150 -150
  45. package/scripts/reset-password.js +22 -0
  46. package/scripts/runtime.js +25 -25
  47. package/scripts/service.js +56 -47
  48. package/scripts/setup.js +50 -50
  49. package/scripts/start-server.js +41 -41
  50. package/scripts/stop.js +36 -36
  51. package/server/app.js +112 -96
  52. package/server/auth.js +217 -0
  53. package/server/channels/contract.js +57 -57
  54. package/server/channels/hub.js +165 -165
  55. package/server/channels/index.js +28 -25
  56. package/server/channels/onebot/index.js +335 -336
  57. package/server/channels/onebot/token.js +22 -3
  58. package/server/core/capabilities.js +208 -208
  59. package/server/core/events.js +64 -60
  60. package/server/core/image-preprocessor.js +208 -208
  61. package/server/core/model-manager.js +1213 -1213
  62. package/server/core/net-guard.js +49 -49
  63. package/server/core/orchestrator.js +569 -569
  64. package/server/core/perceivers.js +91 -91
  65. package/server/core/persona-manager.js +243 -243
  66. package/server/core/repository.js +293 -293
  67. package/server/core/response-validator.js +508 -508
  68. package/server/core/turn-processor.js +630 -630
  69. package/server/core/turn.js +236 -236
  70. package/server/core/vision-manager.js +633 -633
  71. package/server/i18n/errors.js +548 -497
  72. package/server/i18n/labels.js +649 -653
  73. package/server/index.js +151 -102
  74. package/server/knowledge/manager.js +386 -386
  75. package/server/knowledge/schema.js +12 -8
  76. package/server/local-access.js +32 -0
  77. package/server/mind/anticipations.js +507 -507
  78. package/server/mind/api.js +703 -703
  79. package/server/mind/budget.js +152 -152
  80. package/server/mind/context-selection.js +61 -61
  81. package/server/mind/day-planner.js +324 -324
  82. package/server/mind/evidence.js +148 -148
  83. package/server/mind/index.js +445 -445
  84. package/server/mind/life/diary.js +786 -786
  85. package/server/mind/life/index.js +1049 -1049
  86. package/server/mind/observations.js +84 -84
  87. package/server/mind/own-day.js +324 -324
  88. package/server/mind/own-voice.js +318 -318
  89. package/server/mind/senses.js +53 -53
  90. package/server/mind/time/activity-clock.js +196 -196
  91. package/server/mind/time/availability.js +69 -69
  92. package/server/mind/time/commitment-review.js +198 -198
  93. package/server/mind/time/executor.js +512 -512
  94. package/server/mind/time/experiences.js +128 -128
  95. package/server/mind/time/games.js +562 -562
  96. package/server/mind/time/index.js +739 -739
  97. package/server/mind/time/intent.js +166 -166
  98. package/server/mind/time/kinds.js +171 -171
  99. package/server/mind/time/plugin-activity.js +207 -207
  100. package/server/mind/time/tasks.js +587 -587
  101. package/server/mind/util.js +110 -110
  102. package/server/mind/view.js +273 -273
  103. package/server/paths.js +102 -102
  104. package/server/plugins/api.js +204 -204
  105. package/server/plugins/host.js +967 -967
  106. package/server/plugins/install.js +295 -295
  107. package/server/plugins/manifest.js +201 -201
  108. package/server/plugins/permissions.js +42 -42
  109. package/server/plugins/runner/guard.mjs +36 -36
  110. package/server/plugins/runner/worker.mjs +250 -250
  111. package/server/plugins/schema.js +91 -91
  112. package/server/plugins/zip.js +112 -112
  113. package/server/startup-lock.js +118 -118
  114. package/server/storage/archive.js +242 -241
  115. package/server/storage/backup-cleanup-worker.js +8 -8
  116. package/server/storage/backup-cleanup.js +415 -414
  117. package/server/storage/backup-lock.js +76 -76
  118. package/server/storage/backup-scheduler.js +131 -131
  119. package/server/storage/data-package.js +298 -0
  120. package/server/storage/data-transfer-api.js +50 -0
  121. package/server/storage/data-transfer-worker.js +18 -0
  122. package/server/storage/data-transfer.js +415 -0
  123. package/server/storage/store.js +202 -199
  124. package/server/studio/readiness.js +181 -190
  125. package/server/studio/settings.js +236 -236
  126. package/public/app/assets/ChatsPage-C-FcRapR.js +0 -3
  127. package/public/app/assets/Empty-CpP5kQHH.js +0 -2
  128. package/public/app/assets/Field-D4gY-INU.js +0 -1
  129. package/public/app/assets/HeartPage-CPy_thQd.js +0 -1
  130. package/public/app/assets/LifePage-CxWvwmSf.js +0 -1
  131. package/public/app/assets/MemoryPage-DC4rhQ68.js +0 -1
  132. package/public/app/assets/NaturePage-IvYw04Zq.js +0 -3
  133. package/public/app/assets/PeoplePage-Cqefm9-q.js +0 -1
  134. package/public/app/assets/PluginsPage-DKSaKvXb.js +0 -2
  135. package/public/app/assets/PluginsPage-HyBe5D8w.css +0 -1
  136. package/public/app/assets/SystemPage-CJmtJkrK.js +0 -1
  137. package/public/app/assets/TimePage-Cw44inM4.js +0 -3
  138. package/public/app/assets/index-BheRW-1t.js +0 -4
  139. package/public/app/assets/media-BzPDBm7e.js +0 -1
@@ -1,512 +1,512 @@
1
- import { randomUUID } from "node:crypto";
2
- import { localClock } from "../../core/conversation-cues.js";
3
- import { withFallback } from "../../core/model-manager.js";
4
- import { prompts, replyPrompt } from "../../core/persona-manager.js";
5
- import { evidence, hasCredential, text } from "../util.js";
6
- import { ACTIVITY_LABELS } from "./tasks.js";
7
- import { activityLabel, isPluginActivity, needsEnergy } from "./kinds.js";
8
- import { stepPlugin } from "./plugin-activity.js";
9
- import { executionBlock } from "./availability.js";
10
-
11
- const ACTIVITY_PROMPT =
12
- '执行一个属于自己的实际活动步骤。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。本步骤不发消息、不直接改变人格。';
13
-
14
- export class TimeExecutor {
15
- constructor(life) {
16
- this.life = life;
17
- this.time = life.mind.time;
18
- this.db = life.db;
19
- }
20
- run(now = this.life.now()) {
21
- return this.time.attention.activity(() => this.step(now));
22
- }
23
- async step(now) {
24
- const life = this.life,
25
- mind = life.mind,
26
- time = this.time;
27
- time.tasks.sync(now);
28
- if (
29
- life.closed ||
30
- life.busy ||
31
- life.repo.store.settings().demo ||
32
- !life.settings().solitude ||
33
- life.phase(now).key === "asleep"
34
- )
35
- return null;
36
- let task, chunk;
37
- let primary = time.primary();
38
- if (primary && !primary.lease) {
39
- const urgent = time.tasks
40
- .ready(now)
41
- .find(
42
- (t) =>
43
- t.priority > primary.priority &&
44
- !executionBlock(time, t, now) &&
45
- (time.fixtureImmediate ||
46
- life.planner.disabledForTest ||
47
- (t.checkpoint.schedule?.chosenAt && !t.checkpoint.reschedule)),
48
- );
49
- if (urgent) {
50
- time.tasks.control(
51
- primary.id,
52
- {
53
- action: "pause",
54
- readyAt: now + 60000,
55
- reason: "先处理一件优先级更高的事,保留续接位置",
56
- },
57
- now,
58
- );
59
- time.event(
60
- primary.id,
61
- "preempted",
62
- "更高优先级事项先做",
63
- { forTask: urgent.id, checkpoint: primary.checkpoint.next || "" },
64
- now,
65
- );
66
- primary = null;
67
- }
68
- }
69
- for (const candidate of primary ? [primary] : time.tasks.ready(now)) {
70
- if (candidate.next_step > now) continue;
71
- if (
72
- !time.fixtureImmediate &&
73
- !life.planner.disabledForTest &&
74
- (!candidate.checkpoint.schedule?.chosenAt ||
75
- candidate.checkpoint.reschedule)
76
- )
77
- continue;
78
- const block = executionBlock(time, candidate, now);
79
- if (
80
- block &&
81
- block !== "来源已经撤销" &&
82
- !(candidate.activity === "read" && block === "书架没有可读资料")
83
- ) {
84
- time.tasks.wait(
85
- candidate,
86
- block,
87
- /选定|想具体|执行能力/.test(block)
88
- ? Number.MAX_SAFE_INTEGER
89
- : now + 10 * 60000,
90
- now,
91
- );
92
- continue;
93
- }
94
- if (
95
- candidate.checkpoint.pendingStep?.chunk &&
96
- !this.db
97
- .prepare("SELECT 1 FROM core_chunks WHERE id=?")
98
- .get(candidate.checkpoint.pendingStep.chunk.id)
99
- ) {
100
- time.tasks.wait(
101
- candidate,
102
- "书架这段内容已不可用",
103
- Number.MAX_SAFE_INTEGER,
104
- now,
105
- );
106
- continue;
107
- }
108
- if (!time.allowed(candidate, now)) {
109
- time.tasks.control(
110
- candidate.id,
111
- { action: "abandon", reason: "来源已经撤销" },
112
- now,
113
- );
114
- continue;
115
- }
116
- if (
117
- candidate.session_id &&
118
- !life.chat.enabled(candidate.session_id, { simulated: false })
119
- ) {
120
- time.tasks.wait(candidate, "来源会话目前未启用", now + 60000, now);
121
- continue;
122
- }
123
- if (
124
- !candidate.checkpoint.pendingStep &&
125
- !mind.budget.allows("inner", now)
126
- ) {
127
- time.tasks.wait(candidate, "今日独处预算不足", now + 3600000, now);
128
- continue;
129
- }
130
- if (
131
- needsEnergy(candidate.activity) &&
132
- mind.affect.state(now).energy < 0.18
133
- ) {
134
- time.tasks.wait(
135
- candidate,
136
- "精力较低,先恢复一会儿再投入",
137
- now + 10 * 60000,
138
- now,
139
- );
140
- continue;
141
- }
142
- if (candidate.activity === "game") {
143
- const blocked = candidate.checkpoint.sourceIds?.length
144
- ? ""
145
- : time.search.ready(now);
146
- if (blocked) {
147
- time.tasks.wait(candidate, blocked, now + 60000, now);
148
- continue;
149
- }
150
- }
151
- chunk =
152
- candidate.checkpoint.pendingStep?.chunk ||
153
- (candidate.activity === "read" ? mind.reading.next(now) : null);
154
- if (candidate.activity === "read" && !chunk) {
155
- time.tasks.wait(candidate, "书架没有可读资料", now, now);
156
- continue;
157
- }
158
- task = candidate;
159
- break;
160
- }
161
- if (!task) return time.sharing.run(life, now);
162
- let profile;
163
- try {
164
- if (!task.checkpoint.pendingStep) profile = life.profile();
165
- } catch {
166
- time.tasks.wait(task, "等待可用模型", now + 60000, now);
167
- return null;
168
- }
169
- task = time.start(task, now);
170
- if (!task) return null;
171
- const runId = life.run(
172
- "activity",
173
- `${activityLabel(task.activity, ACTIVITY_LABELS[task.activity] || "活动")}:${task.title}`,
174
- ),
175
- trace = life.repo.trace("__mind__", "activity"),
176
- version = mind.nature.version();
177
- let status = "empty",
178
- reason = "这次还不想做",
179
- creation = null;
180
- try {
181
- if (task.activity === "game") {
182
- const outcome = await time.games.step(task, life, trace, runId);
183
- status = outcome.status;
184
- reason = outcome.reason;
185
- return { ...outcome, runId };
186
- }
187
- if (isPluginActivity(task.activity)) {
188
- const outcome = await stepPlugin(task, life, trace, runId);
189
- status = outcome.status;
190
- reason = outcome.reason;
191
- return { ...outcome, runId };
192
- }
193
- const nature = mind.traits.effective(mind.nature.current(now), now);
194
- const pending = task.checkpoint.pendingStep;
195
- if (pending && time.clock.remaining(task, now) > 0) {
196
- status = "engaged";
197
- reason = "接着做这一段";
198
- time.clock.release(task, task.checkpoint, now);
199
- return {
200
- status: "engaged",
201
- reason: "接着做这一段",
202
- task: task.id,
203
- runId,
204
- };
205
- }
206
- const result =
207
- pending?.result ||
208
- (await withFallback(
209
- life.chat.models,
210
- life.chat.fallbackFor(null, profile, trace),
211
- ).call(
212
- profile,
213
- "reflection",
214
- replyPrompt(
215
- nature,
216
- {
217
- ...prompts(life.repo),
218
- activity:
219
- ACTIVITY_PROMPT +
220
- '\n为本段行动估计通常需要的投入时长,输出duration:{minutes:15,basis:"按本段内容与行动估计的理由"}。只估计这一段阅读、写作或思考需要的时间,不用API处理秒数;她的实际节奏会按统一速度稍作加快。',
221
- },
222
- "activity",
223
- ),
224
- {
225
- clock: localClock(now, mind.timeZone()),
226
- activity: task.activity,
227
- affect: mind.affect.state(now),
228
- task: {
229
- id: task.id,
230
- title: task.title,
231
- why: task.why,
232
- checkpoint: task.checkpoint,
233
- },
234
- plan: {
235
- ref: task.anticipation_id
236
- ? `a:${task.anticipation_id}`
237
- : task.sources[0],
238
- content: task.title,
239
- },
240
- self: life
241
- .selfView(now, { room: task.session_id || "" })
242
- .slice(0, 8),
243
- ...(chunk ? { reading: mind.reading.passage(chunk) } : {}),
244
- creation: time.works.context(task),
245
- currentLife: time.view({ session: task.session_id, now }),
246
- },
247
- trace,
248
- ));
249
- const finished = life.now();
250
- if (
251
- task.activity === "write" &&
252
- result?.done === true &&
253
- /(?:未完待续|[((]待续[))]|尚未完成|只是开头草稿)\s*[。.!!]?\s*$/.test(
254
- String(result.content || ""),
255
- )
256
- )
257
- result.done = false;
258
- if (
259
- life.closed ||
260
- mind.nature.version() !== version ||
261
- !time.valid(task)
262
- ) {
263
- status = "cancelled";
264
- reason = "任务或服务状态已经变化";
265
- } else if (
266
- (result?.done === false && !result.content) ||
267
- result?.skip ||
268
- (task.kind === "suggestion" && result?.accepted === false)
269
- ) {
270
- reason = text(result.reason, 120) || reason;
271
- if (task.kind === "suggestion" && result?.accepted === false) {
272
- time.event(task.id, "suggestion-declined", reason, {}, finished);
273
- time.tasks.control(task.id, { action: "abandon", reason }, finished);
274
- } else
275
- time.tasks.control(
276
- task.id,
277
- {
278
- action: "pause",
279
- readyAt: finished + time.settings().breakMinutes * 60000,
280
- reason,
281
- },
282
- finished,
283
- );
284
- } else {
285
- const title = text(result?.title, 80),
286
- content = text(result?.content, 1800);
287
- if (
288
- typeof result?.done !== "boolean" ||
289
- !title ||
290
- !content ||
291
- hasCredential(title + content)
292
- )
293
- throw Error("活动成果格式无效");
294
- const sources = evidence([
295
- ...(task.anticipation_id ? [`a:${task.anticipation_id}`] : []),
296
- ...(chunk ? [`r:${chunk.id}`] : []),
297
- ...task.sources,
298
- ]);
299
- if (!time.fixtureImmediate && !pending) {
300
- this.db.exec("SAVEPOINT paced_draft");
301
- try {
302
- if (task.kind === "suggestion")
303
- time.event(
304
- task.id,
305
- "suggestion-accepted",
306
- text(result.reason, 240),
307
- {},
308
- finished,
309
- );
310
- const work = time.works.save(
311
- task,
312
- { ...result, done: false },
313
- { sources, runId, now: finished },
314
- );
315
- const checkpoint = time.tasks.get(task.id).checkpoint;
316
- time.clock.release(
317
- task,
318
- {
319
- ...checkpoint,
320
- activityClock: time.clock.plan(task, result.duration, finished),
321
- pendingStep: {
322
- result: { ...result, title, content },
323
- workId: work.id,
324
- chunk,
325
- sources,
326
- runId,
327
- },
328
- next: result.next || "接着推敲与整理这一段",
329
- },
330
- finished,
331
- );
332
- status = "draft";
333
- reason = "草稿已保存,接着完成这一段";
334
- return { status, reason, task: task.id, runId, workId: work.id };
335
- } catch (error) {
336
- this.db.exec("ROLLBACK TO paced_draft");
337
- throw error;
338
- } finally {
339
- this.db.exec("RELEASE paced_draft");
340
- }
341
- }
342
- this.db.exec("SAVEPOINT time_execution");
343
- try {
344
- if (task.kind === "suggestion" && !pending)
345
- time.event(
346
- task.id,
347
- "suggestion-accepted",
348
- text(result.reason, 240),
349
- {},
350
- finished,
351
- );
352
- if (pending) {
353
- const checkpoint = {
354
- ...task.checkpoint,
355
- activityClock: {
356
- ...task.checkpoint.activityClock,
357
- phase: "finished",
358
- },
359
- };
360
- delete checkpoint.pendingStep;
361
- this.db
362
- .prepare("UPDATE mind_time_tasks SET checkpoint=? WHERE id=?")
363
- .run(JSON.stringify(checkpoint), task.id);
364
- }
365
- const work = pending
366
- ? time.works.settle(task, pending.workId, result.done, finished)
367
- : time.works.save(task, result, {
368
- sources,
369
- runId,
370
- now: finished,
371
- });
372
- if (chunk)
373
- mind.reading.record(chunk, text(content, 120), runId, finished);
374
- if (result.done) {
375
- creation = randomUUID();
376
- this.db
377
- .prepare(
378
- "INSERT INTO mind_creations VALUES (?,?,?,?,?,?,?,?,?,?)",
379
- )
380
- .run(
381
- creation,
382
- finished,
383
- task.anticipation_id || task.id,
384
- task.activity,
385
- title,
386
- work.content,
387
- JSON.stringify(sources),
388
- task.session_id,
389
- task.discretion,
390
- runId,
391
- );
392
- mind.thoughts.add({
393
- // Narrative remains in the work. Only the act becomes an experience.
394
- kind: "reflection",
395
- content:
396
- task.activity === "write"
397
- ? `我完成了《${title}》的第 ${work.ordinal} 篇,正文保存在作品库;其中情节和人物是创作。`
398
- : text(`${title}:${content}`, 600),
399
- sources: evidence([`x:${task.id}`, ...sources]),
400
- sessions: task.session_id ? [task.session_id] : [],
401
- runId,
402
- time: finished,
403
- importance: 0.4,
404
- });
405
- time.complete(
406
- task,
407
- { creation, workId: work.id, sources },
408
- finished,
409
- );
410
- this.db
411
- .prepare(
412
- "UPDATE mind_time_works SET legacy_creation=? WHERE id=?",
413
- )
414
- .run(creation, work.id);
415
- const project = time.works.project(task.project_id);
416
- if (
417
- project.serial &&
418
- result.next &&
419
- (!result.projectState || result.projectState === "active")
420
- )
421
- time.tasks.add(
422
- {
423
- kind: "plan",
424
- activity: task.activity,
425
- title: `接续《${project.title}》第 ${work.ordinal + 1} 篇`,
426
- why: text(result.reason || result.next, 240),
427
- sources: [`x:${task.id}`],
428
- session: task.session_id,
429
- discretion: task.discretion,
430
- projectId: project.id,
431
- readyAt: finished + time.settings().breakMinutes * 60000,
432
- activeOnly: true,
433
- },
434
- finished,
435
- );
436
- if (result.share?.choice && task.session_id)
437
- time.sharing.choose(
438
- work.id,
439
- task.session_id,
440
- result.share,
441
- finished,
442
- );
443
- } else time.progress(task, work, result, finished);
444
- if (result.feeling?.feeling)
445
- mind.affect.feel({
446
- ...result.feeling,
447
- intensity: 0.2,
448
- cause: `做自己的${activityLabel(task.activity, ACTIVITY_LABELS[task.activity] || "事")}时的感受`,
449
- sources: [`x:${task.id}`],
450
- session: task.session_id,
451
- origin: "activity",
452
- time: finished,
453
- });
454
- if (["paused", "complete"].includes(result.projectState))
455
- this.db
456
- .prepare("UPDATE mind_time_projects SET state=? WHERE id=?")
457
- .run(result.projectState, task.project_id);
458
- if (result.projectState === "paused" && !result.done)
459
- time.tasks.control(
460
- task.id,
461
- {
462
- action: "pause",
463
- reason: text(result.reason, 240) || "自己选择暂时放下这个项目",
464
- },
465
- finished,
466
- );
467
- this.db.exec("RELEASE time_execution");
468
- } catch (error) {
469
- this.db.exec("ROLLBACK TO time_execution");
470
- this.db.exec("RELEASE time_execution");
471
- creation = null;
472
- throw error;
473
- }
474
- status = result.done ? "written" : "draft";
475
- reason = `${activityLabel(task.activity, ACTIVITY_LABELS[task.activity] || "活动")},保存了《${title}》${result.done ? "完成稿" : "草稿"}`;
476
- }
477
- } catch (error) {
478
- status = "error";
479
- reason = text(error.message, 200);
480
- trace.error = reason;
481
- if (time.valid(task))
482
- time.tasks.wait(task, reason, life.now() + 15 * 60000, life.now());
483
- } finally {
484
- if (time.valid(task))
485
- time.tasks.control(
486
- task.id,
487
- {
488
- action: "pause",
489
- readyAt: life.now() + time.settings().breakMinutes * 60000,
490
- reason,
491
- },
492
- life.now(),
493
- );
494
- trace.reason = reason;
495
- life.chat.finishQuietly(trace, status === "error" ? "error" : "complete");
496
- life.end(runId, status, reason, trace, {
497
- plan: task.anticipation_id,
498
- task: task.id,
499
- activity: task.activity,
500
- creation,
501
- });
502
- }
503
- return {
504
- status,
505
- reason,
506
- runId,
507
- activity: task.activity,
508
- creation,
509
- task: task.id,
510
- };
511
- }
512
- }
1
+ import { randomUUID } from "node:crypto";
2
+ import { localClock } from "../../core/conversation-cues.js";
3
+ import { withFallback } from "../../core/model-manager.js";
4
+ import { prompts, replyPrompt } from "../../core/persona-manager.js";
5
+ import { evidence, hasCredential, text } from "../util.js";
6
+ import { ACTIVITY_LABELS } from "./tasks.js";
7
+ import { activityLabel, isPluginActivity, needsEnergy } from "./kinds.js";
8
+ import { stepPlugin } from "./plugin-activity.js";
9
+ import { executionBlock } from "./availability.js";
10
+
11
+ const ACTIVITY_PROMPT =
12
+ '执行一个属于自己的实际活动步骤。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。本步骤不发消息、不直接改变人格。';
13
+
14
+ export class TimeExecutor {
15
+ constructor(life) {
16
+ this.life = life;
17
+ this.time = life.mind.time;
18
+ this.db = life.db;
19
+ }
20
+ run(now = this.life.now()) {
21
+ return this.time.attention.activity(() => this.step(now));
22
+ }
23
+ async step(now) {
24
+ const life = this.life,
25
+ mind = life.mind,
26
+ time = this.time;
27
+ time.tasks.sync(now);
28
+ if (
29
+ life.closed ||
30
+ life.busy ||
31
+ life.repo.store.settings().demo ||
32
+ !life.settings().solitude ||
33
+ life.phase(now).key === "asleep"
34
+ )
35
+ return null;
36
+ let task, chunk;
37
+ let primary = time.primary();
38
+ if (primary && !primary.lease) {
39
+ const urgent = time.tasks
40
+ .ready(now)
41
+ .find(
42
+ (t) =>
43
+ t.priority > primary.priority &&
44
+ !executionBlock(time, t, now) &&
45
+ (time.fixtureImmediate ||
46
+ life.planner.disabledForTest ||
47
+ (t.checkpoint.schedule?.chosenAt && !t.checkpoint.reschedule)),
48
+ );
49
+ if (urgent) {
50
+ time.tasks.control(
51
+ primary.id,
52
+ {
53
+ action: "pause",
54
+ readyAt: now + 60000,
55
+ reason: "先处理一件优先级更高的事,保留续接位置",
56
+ },
57
+ now,
58
+ );
59
+ time.event(
60
+ primary.id,
61
+ "preempted",
62
+ "更高优先级事项先做",
63
+ { forTask: urgent.id, checkpoint: primary.checkpoint.next || "" },
64
+ now,
65
+ );
66
+ primary = null;
67
+ }
68
+ }
69
+ for (const candidate of primary ? [primary] : time.tasks.ready(now)) {
70
+ if (candidate.next_step > now) continue;
71
+ if (
72
+ !time.fixtureImmediate &&
73
+ !life.planner.disabledForTest &&
74
+ (!candidate.checkpoint.schedule?.chosenAt ||
75
+ candidate.checkpoint.reschedule)
76
+ )
77
+ continue;
78
+ const block = executionBlock(time, candidate, now);
79
+ if (
80
+ block &&
81
+ block !== "来源已经撤销" &&
82
+ !(candidate.activity === "read" && block === "书架没有可读资料")
83
+ ) {
84
+ time.tasks.wait(
85
+ candidate,
86
+ block,
87
+ /选定|想具体|执行能力/.test(block)
88
+ ? Number.MAX_SAFE_INTEGER
89
+ : now + 10 * 60000,
90
+ now,
91
+ );
92
+ continue;
93
+ }
94
+ if (
95
+ candidate.checkpoint.pendingStep?.chunk &&
96
+ !this.db
97
+ .prepare("SELECT 1 FROM core_chunks WHERE id=?")
98
+ .get(candidate.checkpoint.pendingStep.chunk.id)
99
+ ) {
100
+ time.tasks.wait(
101
+ candidate,
102
+ "书架这段内容已不可用",
103
+ Number.MAX_SAFE_INTEGER,
104
+ now,
105
+ );
106
+ continue;
107
+ }
108
+ if (!time.allowed(candidate, now)) {
109
+ time.tasks.control(
110
+ candidate.id,
111
+ { action: "abandon", reason: "来源已经撤销" },
112
+ now,
113
+ );
114
+ continue;
115
+ }
116
+ if (
117
+ candidate.session_id &&
118
+ !life.chat.enabled(candidate.session_id, { simulated: false })
119
+ ) {
120
+ time.tasks.wait(candidate, "来源会话目前未启用", now + 60000, now);
121
+ continue;
122
+ }
123
+ if (
124
+ !candidate.checkpoint.pendingStep &&
125
+ !mind.budget.allows("inner", now)
126
+ ) {
127
+ time.tasks.wait(candidate, "今日独处预算不足", now + 3600000, now);
128
+ continue;
129
+ }
130
+ if (
131
+ needsEnergy(candidate.activity) &&
132
+ mind.affect.state(now).energy < 0.18
133
+ ) {
134
+ time.tasks.wait(
135
+ candidate,
136
+ "精力较低,先恢复一会儿再投入",
137
+ now + 10 * 60000,
138
+ now,
139
+ );
140
+ continue;
141
+ }
142
+ if (candidate.activity === "game") {
143
+ const blocked = candidate.checkpoint.sourceIds?.length
144
+ ? ""
145
+ : time.search.ready(now);
146
+ if (blocked) {
147
+ time.tasks.wait(candidate, blocked, now + 60000, now);
148
+ continue;
149
+ }
150
+ }
151
+ chunk =
152
+ candidate.checkpoint.pendingStep?.chunk ||
153
+ (candidate.activity === "read" ? mind.reading.next(now) : null);
154
+ if (candidate.activity === "read" && !chunk) {
155
+ time.tasks.wait(candidate, "书架没有可读资料", now, now);
156
+ continue;
157
+ }
158
+ task = candidate;
159
+ break;
160
+ }
161
+ if (!task) return time.sharing.run(life, now);
162
+ let profile;
163
+ try {
164
+ if (!task.checkpoint.pendingStep) profile = life.profile();
165
+ } catch {
166
+ time.tasks.wait(task, "等待可用模型", now + 60000, now);
167
+ return null;
168
+ }
169
+ task = time.start(task, now);
170
+ if (!task) return null;
171
+ const runId = life.run(
172
+ "activity",
173
+ `${activityLabel(task.activity, ACTIVITY_LABELS[task.activity] || "活动")}:${task.title}`,
174
+ ),
175
+ trace = life.repo.trace("__mind__", "activity"),
176
+ version = mind.nature.version();
177
+ let status = "empty",
178
+ reason = "这次还不想做",
179
+ creation = null;
180
+ try {
181
+ if (task.activity === "game") {
182
+ const outcome = await time.games.step(task, life, trace, runId);
183
+ status = outcome.status;
184
+ reason = outcome.reason;
185
+ return { ...outcome, runId };
186
+ }
187
+ if (isPluginActivity(task.activity)) {
188
+ const outcome = await stepPlugin(task, life, trace, runId);
189
+ status = outcome.status;
190
+ reason = outcome.reason;
191
+ return { ...outcome, runId };
192
+ }
193
+ const nature = mind.traits.effective(mind.nature.current(now), now);
194
+ const pending = task.checkpoint.pendingStep;
195
+ if (pending && time.clock.remaining(task, now) > 0) {
196
+ status = "engaged";
197
+ reason = "接着做这一段";
198
+ time.clock.release(task, task.checkpoint, now);
199
+ return {
200
+ status: "engaged",
201
+ reason: "接着做这一段",
202
+ task: task.id,
203
+ runId,
204
+ };
205
+ }
206
+ const result =
207
+ pending?.result ||
208
+ (await withFallback(
209
+ life.chat.models,
210
+ life.chat.fallbackFor(null, profile, trace),
211
+ ).call(
212
+ profile,
213
+ "reflection",
214
+ replyPrompt(
215
+ nature,
216
+ {
217
+ ...prompts(life.repo),
218
+ activity:
219
+ ACTIVITY_PROMPT +
220
+ '\n为本段行动估计通常需要的投入时长,输出duration:{minutes:15,basis:"按本段内容与行动估计的理由"}。只估计这一段阅读、写作或思考需要的时间,不用API处理秒数;她的实际节奏会按统一速度稍作加快。',
221
+ },
222
+ "activity",
223
+ ),
224
+ {
225
+ clock: localClock(now, mind.timeZone()),
226
+ activity: task.activity,
227
+ affect: mind.affect.state(now),
228
+ task: {
229
+ id: task.id,
230
+ title: task.title,
231
+ why: task.why,
232
+ checkpoint: task.checkpoint,
233
+ },
234
+ plan: {
235
+ ref: task.anticipation_id
236
+ ? `a:${task.anticipation_id}`
237
+ : task.sources[0],
238
+ content: task.title,
239
+ },
240
+ self: life
241
+ .selfView(now, { room: task.session_id || "" })
242
+ .slice(0, 8),
243
+ ...(chunk ? { reading: mind.reading.passage(chunk) } : {}),
244
+ creation: time.works.context(task),
245
+ currentLife: time.view({ session: task.session_id, now }),
246
+ },
247
+ trace,
248
+ ));
249
+ const finished = life.now();
250
+ if (
251
+ task.activity === "write" &&
252
+ result?.done === true &&
253
+ /(?:未完待续|[((]待续[))]|尚未完成|只是开头草稿)\s*[。.!!]?\s*$/.test(
254
+ String(result.content || ""),
255
+ )
256
+ )
257
+ result.done = false;
258
+ if (
259
+ life.closed ||
260
+ mind.nature.version() !== version ||
261
+ !time.valid(task)
262
+ ) {
263
+ status = "cancelled";
264
+ reason = "任务或服务状态已经变化";
265
+ } else if (
266
+ (result?.done === false && !result.content) ||
267
+ result?.skip ||
268
+ (task.kind === "suggestion" && result?.accepted === false)
269
+ ) {
270
+ reason = text(result.reason, 120) || reason;
271
+ if (task.kind === "suggestion" && result?.accepted === false) {
272
+ time.event(task.id, "suggestion-declined", reason, {}, finished);
273
+ time.tasks.control(task.id, { action: "abandon", reason }, finished);
274
+ } else
275
+ time.tasks.control(
276
+ task.id,
277
+ {
278
+ action: "pause",
279
+ readyAt: finished + time.settings().breakMinutes * 60000,
280
+ reason,
281
+ },
282
+ finished,
283
+ );
284
+ } else {
285
+ const title = text(result?.title, 80),
286
+ content = text(result?.content, 1800);
287
+ if (
288
+ typeof result?.done !== "boolean" ||
289
+ !title ||
290
+ !content ||
291
+ hasCredential(title + content)
292
+ )
293
+ throw Error("活动成果格式无效");
294
+ const sources = evidence([
295
+ ...(task.anticipation_id ? [`a:${task.anticipation_id}`] : []),
296
+ ...(chunk ? [`r:${chunk.id}`] : []),
297
+ ...task.sources,
298
+ ]);
299
+ if (!time.fixtureImmediate && !pending) {
300
+ this.db.exec("SAVEPOINT paced_draft");
301
+ try {
302
+ if (task.kind === "suggestion")
303
+ time.event(
304
+ task.id,
305
+ "suggestion-accepted",
306
+ text(result.reason, 240),
307
+ {},
308
+ finished,
309
+ );
310
+ const work = time.works.save(
311
+ task,
312
+ { ...result, done: false },
313
+ { sources, runId, now: finished },
314
+ );
315
+ const checkpoint = time.tasks.get(task.id).checkpoint;
316
+ time.clock.release(
317
+ task,
318
+ {
319
+ ...checkpoint,
320
+ activityClock: time.clock.plan(task, result.duration, finished),
321
+ pendingStep: {
322
+ result: { ...result, title, content },
323
+ workId: work.id,
324
+ chunk,
325
+ sources,
326
+ runId,
327
+ },
328
+ next: result.next || "接着推敲与整理这一段",
329
+ },
330
+ finished,
331
+ );
332
+ status = "draft";
333
+ reason = "草稿已保存,接着完成这一段";
334
+ return { status, reason, task: task.id, runId, workId: work.id };
335
+ } catch (error) {
336
+ this.db.exec("ROLLBACK TO paced_draft");
337
+ throw error;
338
+ } finally {
339
+ this.db.exec("RELEASE paced_draft");
340
+ }
341
+ }
342
+ this.db.exec("SAVEPOINT time_execution");
343
+ try {
344
+ if (task.kind === "suggestion" && !pending)
345
+ time.event(
346
+ task.id,
347
+ "suggestion-accepted",
348
+ text(result.reason, 240),
349
+ {},
350
+ finished,
351
+ );
352
+ if (pending) {
353
+ const checkpoint = {
354
+ ...task.checkpoint,
355
+ activityClock: {
356
+ ...task.checkpoint.activityClock,
357
+ phase: "finished",
358
+ },
359
+ };
360
+ delete checkpoint.pendingStep;
361
+ this.db
362
+ .prepare("UPDATE mind_time_tasks SET checkpoint=? WHERE id=?")
363
+ .run(JSON.stringify(checkpoint), task.id);
364
+ }
365
+ const work = pending
366
+ ? time.works.settle(task, pending.workId, result.done, finished)
367
+ : time.works.save(task, result, {
368
+ sources,
369
+ runId,
370
+ now: finished,
371
+ });
372
+ if (chunk)
373
+ mind.reading.record(chunk, text(content, 120), runId, finished);
374
+ if (result.done) {
375
+ creation = randomUUID();
376
+ this.db
377
+ .prepare(
378
+ "INSERT INTO mind_creations VALUES (?,?,?,?,?,?,?,?,?,?)",
379
+ )
380
+ .run(
381
+ creation,
382
+ finished,
383
+ task.anticipation_id || task.id,
384
+ task.activity,
385
+ title,
386
+ work.content,
387
+ JSON.stringify(sources),
388
+ task.session_id,
389
+ task.discretion,
390
+ runId,
391
+ );
392
+ mind.thoughts.add({
393
+ // Narrative remains in the work. Only the act becomes an experience.
394
+ kind: "reflection",
395
+ content:
396
+ task.activity === "write"
397
+ ? `我完成了《${title}》的第 ${work.ordinal} 篇,正文保存在作品库;其中情节和人物是创作。`
398
+ : text(`${title}:${content}`, 600),
399
+ sources: evidence([`x:${task.id}`, ...sources]),
400
+ sessions: task.session_id ? [task.session_id] : [],
401
+ runId,
402
+ time: finished,
403
+ importance: 0.4,
404
+ });
405
+ time.complete(
406
+ task,
407
+ { creation, workId: work.id, sources },
408
+ finished,
409
+ );
410
+ this.db
411
+ .prepare(
412
+ "UPDATE mind_time_works SET legacy_creation=? WHERE id=?",
413
+ )
414
+ .run(creation, work.id);
415
+ const project = time.works.project(task.project_id);
416
+ if (
417
+ project.serial &&
418
+ result.next &&
419
+ (!result.projectState || result.projectState === "active")
420
+ )
421
+ time.tasks.add(
422
+ {
423
+ kind: "plan",
424
+ activity: task.activity,
425
+ title: `接续《${project.title}》第 ${work.ordinal + 1} 篇`,
426
+ why: text(result.reason || result.next, 240),
427
+ sources: [`x:${task.id}`],
428
+ session: task.session_id,
429
+ discretion: task.discretion,
430
+ projectId: project.id,
431
+ readyAt: finished + time.settings().breakMinutes * 60000,
432
+ activeOnly: true,
433
+ },
434
+ finished,
435
+ );
436
+ if (result.share?.choice && task.session_id)
437
+ time.sharing.choose(
438
+ work.id,
439
+ task.session_id,
440
+ result.share,
441
+ finished,
442
+ );
443
+ } else time.progress(task, work, result, finished);
444
+ if (result.feeling?.feeling)
445
+ mind.affect.feel({
446
+ ...result.feeling,
447
+ intensity: 0.2,
448
+ cause: `做自己的${activityLabel(task.activity, ACTIVITY_LABELS[task.activity] || "事")}时的感受`,
449
+ sources: [`x:${task.id}`],
450
+ session: task.session_id,
451
+ origin: "activity",
452
+ time: finished,
453
+ });
454
+ if (["paused", "complete"].includes(result.projectState))
455
+ this.db
456
+ .prepare("UPDATE mind_time_projects SET state=? WHERE id=?")
457
+ .run(result.projectState, task.project_id);
458
+ if (result.projectState === "paused" && !result.done)
459
+ time.tasks.control(
460
+ task.id,
461
+ {
462
+ action: "pause",
463
+ reason: text(result.reason, 240) || "自己选择暂时放下这个项目",
464
+ },
465
+ finished,
466
+ );
467
+ this.db.exec("RELEASE time_execution");
468
+ } catch (error) {
469
+ this.db.exec("ROLLBACK TO time_execution");
470
+ this.db.exec("RELEASE time_execution");
471
+ creation = null;
472
+ throw error;
473
+ }
474
+ status = result.done ? "written" : "draft";
475
+ reason = `${activityLabel(task.activity, ACTIVITY_LABELS[task.activity] || "活动")},保存了《${title}》${result.done ? "完成稿" : "草稿"}`;
476
+ }
477
+ } catch (error) {
478
+ status = "error";
479
+ reason = text(error.message, 200);
480
+ trace.error = reason;
481
+ if (time.valid(task))
482
+ time.tasks.wait(task, reason, life.now() + 15 * 60000, life.now());
483
+ } finally {
484
+ if (time.valid(task))
485
+ time.tasks.control(
486
+ task.id,
487
+ {
488
+ action: "pause",
489
+ readyAt: life.now() + time.settings().breakMinutes * 60000,
490
+ reason,
491
+ },
492
+ life.now(),
493
+ );
494
+ trace.reason = reason;
495
+ life.chat.finishQuietly(trace, status === "error" ? "error" : "complete");
496
+ life.end(runId, status, reason, trace, {
497
+ plan: task.anticipation_id,
498
+ task: task.id,
499
+ activity: task.activity,
500
+ creation,
501
+ });
502
+ }
503
+ return {
504
+ status,
505
+ reason,
506
+ runId,
507
+ activity: task.activity,
508
+ creation,
509
+ task: task.id,
510
+ };
511
+ }
512
+ }