luckytri 1.0.0

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 (235) hide show
  1. package/.github/SECURITY.md +71 -0
  2. package/LICENSE +21 -0
  3. package/README.en.md +250 -0
  4. package/README.md +250 -0
  5. package/bin/luckytri.js +107 -0
  6. package/docs/brand/banner.png +0 -0
  7. package/docs/brand/palette.png +0 -0
  8. package/docs/brand/portrait.png +0 -0
  9. package/docs/en/CHANGELOG.md +113 -0
  10. package/docs/en/architecture.md +152 -0
  11. package/docs/en/chat-style.md +62 -0
  12. package/docs/en/connect.md +63 -0
  13. package/docs/en/guide.md +216 -0
  14. package/docs/en/her-life.md +189 -0
  15. package/docs/en/npm-release.md +25 -0
  16. package/docs/en/plugins.md +82 -0
  17. package/docs/en/recovery.md +28 -0
  18. package/docs/en/time.md +71 -0
  19. package/docs/en/webui.md +73 -0
  20. package/docs/zh/CHANGELOG.md +113 -0
  21. package/docs/zh/architecture.md +152 -0
  22. package/docs/zh/chat-style.md +62 -0
  23. package/docs/zh/connect.md +63 -0
  24. package/docs/zh/guide.md +216 -0
  25. package/docs/zh/her-life.md +189 -0
  26. package/docs/zh/npm-release.md +33 -0
  27. package/docs/zh/plugins.md +82 -0
  28. package/docs/zh/recovery.md +28 -0
  29. package/docs/zh/time.md +71 -0
  30. package/docs/zh/webui.md +73 -0
  31. package/package.json +108 -0
  32. package/plugins/sdk/index.d.ts +155 -0
  33. package/plugins/sdk/testing.js +58 -0
  34. package/plugins/weather/index.js +82 -0
  35. package/plugins/weather/luckytri-plugin.json +29 -0
  36. package/public/app/assets/ChatsPage-C-FcRapR.js +3 -0
  37. package/public/app/assets/ChatsPage-DfyPCS9N.css +1 -0
  38. package/public/app/assets/Empty-CpP5kQHH.js +2 -0
  39. package/public/app/assets/Empty-DmUB2-Bk.css +1 -0
  40. package/public/app/assets/Field-D4gY-INU.js +1 -0
  41. package/public/app/assets/HeartPage-Bft9wZp5.css +1 -0
  42. package/public/app/assets/HeartPage-CPy_thQd.js +1 -0
  43. package/public/app/assets/LifePage-BUi5euBF.css +1 -0
  44. package/public/app/assets/LifePage-CxWvwmSf.js +1 -0
  45. package/public/app/assets/MemoryPage-DC4rhQ68.js +1 -0
  46. package/public/app/assets/MemoryPage-cZvXfkXb.css +1 -0
  47. package/public/app/assets/NaturePage-CTXHCE2D.css +1 -0
  48. package/public/app/assets/NaturePage-IvYw04Zq.js +3 -0
  49. package/public/app/assets/PeoplePage-BmUxkrOU.css +1 -0
  50. package/public/app/assets/PeoplePage-Cqefm9-q.js +1 -0
  51. package/public/app/assets/PluginsPage-DKSaKvXb.js +2 -0
  52. package/public/app/assets/PluginsPage-HyBe5D8w.css +1 -0
  53. package/public/app/assets/SystemPage-CAMPoYqM.css +1 -0
  54. package/public/app/assets/SystemPage-CJmtJkrK.js +1 -0
  55. package/public/app/assets/TimePage-BOms_n4I.css +1 -0
  56. package/public/app/assets/TimePage-Cw44inM4.js +3 -0
  57. package/public/app/assets/formatters-ZmW_xFHw.js +1 -0
  58. package/public/app/assets/index-BheRW-1t.js +4 -0
  59. package/public/app/assets/index-YDz1ree1.css +1 -0
  60. package/public/app/assets/knowledge-DG0e0uKw.js +1 -0
  61. package/public/app/assets/liquid-garden-IMoHBwzc.png +0 -0
  62. package/public/app/assets/luckytri-home-95QKeMtc.png +0 -0
  63. package/public/app/assets/media-BzPDBm7e.js +1 -0
  64. package/public/app/index.html +27 -0
  65. package/public/guide.en.html +1053 -0
  66. package/public/guide.html +893 -0
  67. package/public/plugin-kit/v1/bridge.js +30 -0
  68. package/public/plugin-kit/v1/kit.css +14 -0
  69. package/public/tutorial.en.md +214 -0
  70. package/public/tutorial.md +214 -0
  71. package/scripts/backup.js +311 -0
  72. package/scripts/browser.js +41 -0
  73. package/scripts/launch.js +94 -0
  74. package/scripts/plugin.js +150 -0
  75. package/scripts/proxy-config.js +167 -0
  76. package/scripts/recover.js +26 -0
  77. package/scripts/runtime.js +25 -0
  78. package/scripts/service.js +47 -0
  79. package/scripts/setup.js +50 -0
  80. package/scripts/start-server.js +41 -0
  81. package/scripts/stop.js +36 -0
  82. package/server/app.js +96 -0
  83. package/server/channels/adapters.js +27 -0
  84. package/server/channels/contract.js +57 -0
  85. package/server/channels/hub.js +165 -0
  86. package/server/channels/index.js +25 -0
  87. package/server/channels/onebot/adapter.js +288 -0
  88. package/server/channels/onebot/index.js +336 -0
  89. package/server/channels/onebot/token.js +3 -0
  90. package/server/channels/qqbot/adapter.js +253 -0
  91. package/server/channels/qqbot/api.js +199 -0
  92. package/server/channels/qqbot/credentials.js +11 -0
  93. package/server/channels/qqbot/gateway.js +274 -0
  94. package/server/channels/qqbot/index.js +389 -0
  95. package/server/channels/segments.js +40 -0
  96. package/server/channels/session-key.js +153 -0
  97. package/server/channels/state.js +42 -0
  98. package/server/core/api.js +564 -0
  99. package/server/core/capabilities.js +208 -0
  100. package/server/core/context-builder.js +288 -0
  101. package/server/core/context-compactor.js +454 -0
  102. package/server/core/conversation-cues.js +325 -0
  103. package/server/core/conversation-manager.js +87 -0
  104. package/server/core/events.js +60 -0
  105. package/server/core/feedback.js +8 -0
  106. package/server/core/group-style.js +72 -0
  107. package/server/core/image-preprocessor.js +208 -0
  108. package/server/core/initiative-context.js +66 -0
  109. package/server/core/input-budget.js +32 -0
  110. package/server/core/local-demo.js +98 -0
  111. package/server/core/message-manager.js +118 -0
  112. package/server/core/message-scheduler.js +78 -0
  113. package/server/core/model-json.js +190 -0
  114. package/server/core/model-manager.js +1213 -0
  115. package/server/core/model-presets.js +900 -0
  116. package/server/core/name-call.js +29 -0
  117. package/server/core/net-guard.js +49 -0
  118. package/server/core/network.js +157 -0
  119. package/server/core/orchestrator.js +569 -0
  120. package/server/core/perceivers.js +91 -0
  121. package/server/core/person-name.js +22 -0
  122. package/server/core/persona-manager.js +243 -0
  123. package/server/core/reply-delivery.js +393 -0
  124. package/server/core/reply-target-resolver.js +50 -0
  125. package/server/core/repository.js +293 -0
  126. package/server/core/response-generator.js +50 -0
  127. package/server/core/response-validator.js +508 -0
  128. package/server/core/retired-prompts.js +51 -0
  129. package/server/core/sessions.js +109 -0
  130. package/server/core/speaker-names.js +92 -0
  131. package/server/core/topic-tracker.js +65 -0
  132. package/server/core/turn-processor.js +630 -0
  133. package/server/core/turn.js +236 -0
  134. package/server/core/vision-manager.js +633 -0
  135. package/server/http.js +18 -0
  136. package/server/i18n/decisions.js +206 -0
  137. package/server/i18n/errors.js +497 -0
  138. package/server/i18n/index.js +144 -0
  139. package/server/i18n/labels.js +653 -0
  140. package/server/i18n/models.js +111 -0
  141. package/server/index.js +102 -0
  142. package/server/knowledge/api.js +342 -0
  143. package/server/knowledge/embedding-profile.js +114 -0
  144. package/server/knowledge/index.js +13 -0
  145. package/server/knowledge/manager.js +386 -0
  146. package/server/knowledge/retrieval.js +93 -0
  147. package/server/knowledge/schema.js +154 -0
  148. package/server/knowledge/vector-index.js +90 -0
  149. package/server/mind/affect.js +265 -0
  150. package/server/mind/anticipations.js +507 -0
  151. package/server/mind/api.js +703 -0
  152. package/server/mind/attention.js +200 -0
  153. package/server/mind/bonds.js +558 -0
  154. package/server/mind/budget.js +152 -0
  155. package/server/mind/clock.js +83 -0
  156. package/server/mind/context-selection.js +61 -0
  157. package/server/mind/continuity.js +562 -0
  158. package/server/mind/conversation-origin.js +175 -0
  159. package/server/mind/day-planner.js +324 -0
  160. package/server/mind/days.js +210 -0
  161. package/server/mind/evidence-read.js +45 -0
  162. package/server/mind/evidence.js +148 -0
  163. package/server/mind/faces.js +138 -0
  164. package/server/mind/guard.js +60 -0
  165. package/server/mind/index.js +445 -0
  166. package/server/mind/initiative.js +333 -0
  167. package/server/mind/life/activities.js +21 -0
  168. package/server/mind/life/context.js +309 -0
  169. package/server/mind/life/diary.js +786 -0
  170. package/server/mind/life/growth.js +252 -0
  171. package/server/mind/life/index.js +1049 -0
  172. package/server/mind/life/presence.js +305 -0
  173. package/server/mind/meetings.js +1069 -0
  174. package/server/mind/memory-policy.js +48 -0
  175. package/server/mind/memory.js +888 -0
  176. package/server/mind/nature.js +262 -0
  177. package/server/mind/novelty.js +19 -0
  178. package/server/mind/observations.js +84 -0
  179. package/server/mind/own-day.js +324 -0
  180. package/server/mind/own-voice.js +318 -0
  181. package/server/mind/periods.js +118 -0
  182. package/server/mind/reading.js +120 -0
  183. package/server/mind/salience.js +201 -0
  184. package/server/mind/schema.js +562 -0
  185. package/server/mind/self.js +484 -0
  186. package/server/mind/senses.js +53 -0
  187. package/server/mind/thoughts.js +274 -0
  188. package/server/mind/time/activity-clock.js +196 -0
  189. package/server/mind/time/agenda.js +242 -0
  190. package/server/mind/time/api.js +200 -0
  191. package/server/mind/time/attention.js +34 -0
  192. package/server/mind/time/availability.js +69 -0
  193. package/server/mind/time/commitment-review.js +198 -0
  194. package/server/mind/time/delivery-links.js +130 -0
  195. package/server/mind/time/executor.js +512 -0
  196. package/server/mind/time/experiences.js +128 -0
  197. package/server/mind/time/games.js +562 -0
  198. package/server/mind/time/index.js +739 -0
  199. package/server/mind/time/intent.js +166 -0
  200. package/server/mind/time/kinds.js +171 -0
  201. package/server/mind/time/model-material.js +77 -0
  202. package/server/mind/time/plugin-activity.js +207 -0
  203. package/server/mind/time/presentation.js +51 -0
  204. package/server/mind/time/schedule.js +83 -0
  205. package/server/mind/time/schema.js +84 -0
  206. package/server/mind/time/search.js +383 -0
  207. package/server/mind/time/sharing.js +472 -0
  208. package/server/mind/time/task-links.js +311 -0
  209. package/server/mind/time/tasks.js +587 -0
  210. package/server/mind/time/works.js +432 -0
  211. package/server/mind/traits.js +191 -0
  212. package/server/mind/util.js +110 -0
  213. package/server/mind/view.js +273 -0
  214. package/server/paths.js +102 -0
  215. package/server/plugins/api.js +204 -0
  216. package/server/plugins/host.js +967 -0
  217. package/server/plugins/install.js +295 -0
  218. package/server/plugins/manifest.js +201 -0
  219. package/server/plugins/permissions.js +42 -0
  220. package/server/plugins/runner/guard.mjs +36 -0
  221. package/server/plugins/runner/worker.mjs +250 -0
  222. package/server/plugins/schema.js +91 -0
  223. package/server/plugins/zip.js +112 -0
  224. package/server/startup-lock.js +118 -0
  225. package/server/storage/archive.js +241 -0
  226. package/server/storage/backup-cleanup-worker.js +8 -0
  227. package/server/storage/backup-cleanup.js +414 -0
  228. package/server/storage/backup-lock.js +76 -0
  229. package/server/storage/backup-scheduler.js +131 -0
  230. package/server/storage/store.js +199 -0
  231. package/server/studio/display-names.js +138 -0
  232. package/server/studio/management.js +102 -0
  233. package/server/studio/readiness.js +190 -0
  234. package/server/studio/settings.js +236 -0
  235. package/server/version.js +10 -0
@@ -0,0 +1,200 @@
1
+ import { wrap } from "../../http.js";
2
+ import { displayNames } from "../../studio/display-names.js";
3
+ import { zonedTime } from "../util.js";
4
+ import { experiences } from "./experiences.js";
5
+
6
+ // What a waiting task is waiting for, as a code the page can rely on in any
7
+ // language. The wording of `wait_reason` is for people, not for logic.
8
+ function waitKind(task) {
9
+ if (task.state !== "waiting") return "";
10
+ const reason = String(task.wait_reason || "");
11
+ if (/执行能力|调整.*约定|仍未兑现/.test(reason)) return "adjust";
12
+ if (/选定|想具体|明确.*内容/.test(reason)) return "specify";
13
+ return "condition";
14
+ }
15
+
16
+ export function mountTime(app, life) {
17
+ const time = life.mind.time;
18
+ app.get(
19
+ "/api/mind/time/agenda",
20
+ wrap((req, res) => res.json(time.agenda.view(life.now()))),
21
+ );
22
+ app.post(
23
+ "/api/mind/time/plan",
24
+ wrap((req, res) => res.json(life.planner.request())),
25
+ );
26
+ app.get(
27
+ "/api/mind/time/care",
28
+ wrap((req, res) => res.json(life.ownDay.status())),
29
+ );
30
+ app.post(
31
+ "/api/mind/time/care",
32
+ wrap((req, res) => {
33
+ life.ownDay.request();
34
+ res.json(life.ownDay.status());
35
+ }),
36
+ );
37
+ app.get(
38
+ "/api/mind/time/search",
39
+ wrap((req, res) => {
40
+ if (
41
+ req.query.provider &&
42
+ !["tavily", "brave"].includes(req.query.provider)
43
+ )
44
+ throw Error("搜索提供方无效");
45
+ res.json(time.search.public(req.query.provider));
46
+ }),
47
+ );
48
+ app.patch(
49
+ "/api/mind/time/search",
50
+ wrap((req, res) => res.json(time.search.save(req.body))),
51
+ );
52
+ app.post(
53
+ "/api/mind/time/search/test",
54
+ wrap(async (req, res) => {
55
+ res.json(await time.search.test(req.body));
56
+ }),
57
+ );
58
+ app.get(
59
+ "/api/mind/time/projects/:id/sources",
60
+ wrap((req, res) => res.json(time.search.sources(req.params.id))),
61
+ );
62
+ app.get(
63
+ "/api/mind/time/sources/:id",
64
+ wrap((req, res) =>
65
+ res.json(
66
+ life.db
67
+ .prepare("SELECT * FROM mind_time_sources WHERE id=?")
68
+ .get(req.params.id) || null,
69
+ ),
70
+ ),
71
+ );
72
+ app.get(
73
+ "/api/mind/time/experiences",
74
+ wrap((req, res) => res.json(experiences(life.db, req.query))),
75
+ );
76
+ app.get(
77
+ "/api/mind/time/game-mode",
78
+ wrap((req, res) =>
79
+ res.json({
80
+ mode: "reference",
81
+ label: "正在玩",
82
+ realEnabled: false,
83
+ realStatus: "开发中",
84
+ }),
85
+ ),
86
+ );
87
+ app.get(
88
+ "/api/mind/time/projects",
89
+ wrap((req, res) => res.json(time.works.projects(req.query))),
90
+ );
91
+ app.get(
92
+ "/api/mind/time/projects/:id",
93
+ wrap((req, res) => res.json(time.works.project(req.params.id))),
94
+ );
95
+ app.post(
96
+ "/api/mind/time/projects/:id/suggest",
97
+ wrap((req, res) => res.json(time.works.suggest(req.params.id, req.body))),
98
+ );
99
+ app.get(
100
+ "/api/mind/time/works",
101
+ wrap((req, res) => res.json(time.works.list(req.query))),
102
+ );
103
+ app.get(
104
+ "/api/mind/time/works/:id",
105
+ wrap((req, res) =>
106
+ res.json(time.works.get(req.params.id, req.query.version)),
107
+ ),
108
+ );
109
+ app.get(
110
+ "/api/mind/time/works/:id/export",
111
+ wrap((req, res) => {
112
+ const work = time.works.get(req.params.id, req.query.version);
113
+ if (!work) throw Error("作品不存在");
114
+ res
115
+ .type("text/plain")
116
+ .attachment("luckytri-work.txt")
117
+ .send(work.title + "\n\n" + work.content);
118
+ }),
119
+ );
120
+ app.get(
121
+ "/api/mind/time/shares",
122
+ wrap((req, res) => res.json(time.sharing.list())),
123
+ );
124
+ app.get(
125
+ "/api/mind/time",
126
+ wrap((req, res) => {
127
+ time.tasks.sync(life.now());
128
+ res.json({
129
+ ...time.view(),
130
+ today: time.today(),
131
+ agenda: time.agenda.view(life.now()),
132
+ care: life.ownDay.status(),
133
+ next: time.tasks
134
+ .ready()
135
+ .slice(0, 5)
136
+ .map((t) => ({
137
+ id: t.id,
138
+ title: t.title,
139
+ state: t.state,
140
+ why: t.why,
141
+ due_at: t.due_at,
142
+ priority: t.priority,
143
+ })),
144
+ settings: time.settings(),
145
+ events: time.events({ limit: 20 }),
146
+ affect: life.mind.affect.state(life.now()),
147
+ counts: life.db
148
+ .prepare(
149
+ "SELECT state,COUNT(*) count FROM mind_time_tasks WHERE json_extract(checkpoint,'$.mergedInto') IS NULL GROUP BY state",
150
+ )
151
+ .all(),
152
+ });
153
+ }),
154
+ );
155
+ app.get(
156
+ "/api/mind/time/tasks",
157
+ wrap((req, res) => {
158
+ const names = displayNames(life.db, life.now());
159
+ res.json(
160
+ (req.query.id
161
+ ? [time.tasks.present(time.tasks.get(String(req.query.id)))].filter(
162
+ Boolean,
163
+ )
164
+ : time.tasks.list(req.query)
165
+ ).map((task) => ({
166
+ ...task,
167
+ person: task.subject
168
+ ? names.get(String(task.subject)) || "相关的人"
169
+ : null,
170
+ waitKind: waitKind(task),
171
+ elapsedMs: time.elapsed(task.id),
172
+ })),
173
+ );
174
+ }),
175
+ );
176
+ app.get(
177
+ "/api/mind/time/tasks/:id",
178
+ wrap((req, res) => res.json(time.tasks.get(req.params.id))),
179
+ );
180
+ app.post(
181
+ "/api/mind/time/tasks/:id/control",
182
+ wrap((req, res) => {
183
+ const input = { ...req.body };
184
+ if (typeof input.readyAt === "string") {
185
+ input.readyAt = zonedTime(input.readyAt, life.mind.timeZone());
186
+ if (input.readyAt === null)
187
+ throw Error("开始时间格式应为 YYYY-MM-DD HH:mm");
188
+ }
189
+ res.json(time.tasks.control(req.params.id, input));
190
+ }),
191
+ );
192
+ app.put(
193
+ "/api/mind/time/settings",
194
+ wrap((req, res) => res.json(time.save(req.body))),
195
+ );
196
+ app.get(
197
+ "/api/mind/time/events",
198
+ wrap((req, res) => res.json(time.events(req.query))),
199
+ );
200
+ }
@@ -0,0 +1,34 @@
1
+ // One main activity can coexist with one conversation, never two conversations
2
+ // or two activity steps. Waiting on another room does not invent another self.
3
+ export class Attention {
4
+ constructor() {
5
+ this.tail = Promise.resolve();
6
+ this.activityBusy = false;
7
+ this.closed = false;
8
+ }
9
+ async chat(run) {
10
+ const previous = this.tail;
11
+ let release;
12
+ this.tail = new Promise((resolve) => {
13
+ release = resolve;
14
+ });
15
+ await previous;
16
+ try {
17
+ if (!this.closed) return await run();
18
+ } finally {
19
+ release();
20
+ }
21
+ }
22
+ async activity(run) {
23
+ if (this.activityBusy || this.closed) return null;
24
+ this.activityBusy = true;
25
+ try {
26
+ return await run();
27
+ } finally {
28
+ this.activityBusy = false;
29
+ }
30
+ }
31
+ close() {
32
+ this.closed = true;
33
+ }
34
+ }
@@ -0,0 +1,69 @@
1
+ import { gameTopic } from "./intent.js";
2
+ import { isExecutable, needsEnergy } from "./kinds.js";
3
+ export function unsupportedAction(task) {
4
+ return task.activity === "unknown" &&
5
+ /点.{0,8}外卖|下单|购买|实拍|拍照给|尝.{0,6}(?:味道|好吃)/.test(task.title)
6
+ ? "除了她此刻真正能做的事,没有下单或实拍的执行能力,需要调整这条约定;仍未兑现"
7
+ : "";
8
+ }
9
+ export function gameName(time, task) {
10
+ return (
11
+ task.checkpoint.topic ||
12
+ gameTopic(task.title) ||
13
+ (task.project_id && time.works.project(task.project_id)?.bible?.topic) ||
14
+ ""
15
+ );
16
+ }
17
+ export function executionBlock(time, task, now) {
18
+ const life = time.search.life;
19
+ const unsupported = unsupportedAction(task);
20
+ if (unsupported) return unsupported;
21
+ if (task.activity === "unknown" || !isExecutable(task.activity))
22
+ return task.activity === "unknown"
23
+ ? "需要把行动内容想具体"
24
+ : "所需插件已停用";
25
+ if (!time.allowed(task, now)) return "来源已经撤销";
26
+ if (task.depends_on && time.tasks.get(task.depends_on)?.state !== "done")
27
+ return "等待前一件事完成";
28
+ if (
29
+ task.checkpoint.pendingStep?.chunk &&
30
+ !time.db
31
+ .prepare("SELECT 1 FROM core_chunks WHERE id=?")
32
+ .get(task.checkpoint.pendingStep.chunk.id)
33
+ )
34
+ return "书架这段内容已不可用";
35
+ if (life?.closed) return "实例已停止";
36
+ if (life && (!life.settings().solitude || life.repo.store.settings().demo))
37
+ return "自己的活动目前关闭";
38
+ if (
39
+ task.session_id &&
40
+ life &&
41
+ !life.chat.enabled(task.session_id, { simulated: false })
42
+ )
43
+ return "来源会话目前未启用";
44
+ if (!task.checkpoint.pendingStep && !time.mind.budget.allows("inner", now))
45
+ return "今日独处预算不足";
46
+ if (needsEnergy(task.activity) && time.mind.affect.state(now).energy < 0.18)
47
+ return "精力较低,先恢复一会儿再投入";
48
+ if (
49
+ task.activity === "read" &&
50
+ !task.checkpoint.pendingStep?.chunk &&
51
+ !time.mind.reading.unreadCount()
52
+ )
53
+ return "书架没有可读资料";
54
+ if (task.activity === "game") {
55
+ if (!gameName(time, task)) return "需要自己选定要玩的游戏";
56
+ if (!task.checkpoint.sourceIds?.length) {
57
+ const why = time.search.ready(now);
58
+ if (why) return why;
59
+ }
60
+ }
61
+ if (life && !task.checkpoint.pendingStep) {
62
+ try {
63
+ life.profile();
64
+ } catch {
65
+ return "等待可用模型";
66
+ }
67
+ }
68
+ return "";
69
+ }
@@ -0,0 +1,198 @@
1
+ import { parse, text, hasCredential } from "../util.js";
2
+ import { intentRecipient, intentMessageSources } from "./intent.js";
3
+ import { spokenDate } from "./schedule.js";
4
+ import { isExecutable } from "./kinds.js";
5
+ const ACTION =
6
+ /写|读|玩|看|整理|想一想|解释|分析|发|给你|做|聊|陪|提醒|联系|分享|检查/;
7
+ const FUTURE =
8
+ /(?:我(?:会|要|想|打算|准备|答应|来)|回头|晚点|稍后|过会|等会|下次|改天|明天|后天|明晚|今晚|这周|周末|以后|到时候|等我|给你|帮你)/;
9
+ export class CommitmentReview {
10
+ constructor(life) {
11
+ this.life = life;
12
+ this.time = life.mind.time;
13
+ this.db = life.db;
14
+ }
15
+ batch(now) {
16
+ const cursor = this.life.repo.config("own-day", {}).reviewSeq || 0;
17
+ const rows = this.db
18
+ .prepare(
19
+ "SELECT seq,time,session_id,payload FROM core_events WHERE role='assistant' AND seq>? AND time<=? AND seq NOT IN (SELECT seq FROM mind_unlived) AND seq NOT IN (SELECT seq FROM mind_time_commitment_reviews) AND COALESCE(json_extract(payload,'$.simulated'),0)=0 AND json_extract(payload,'$.artifact') IS NULL ORDER BY seq LIMIT 48",
20
+ )
21
+ .all(cursor, now);
22
+ if (!rows.length) return null;
23
+ const rooms = new Set(this.life.living().map((s) => s.id));
24
+ const enabled = rows.filter((r) => {
25
+ const words = String(parse(r.payload, {}).text || "");
26
+ const candidate =
27
+ rooms.has(r.session_id) &&
28
+ ((FUTURE.test(words) && ACTION.test(words)) ||
29
+ /^(?:嗯[,,]?|好|行|可以|没问题|会的|答应)/.test(words));
30
+ if (!candidate)
31
+ this.db
32
+ .prepare(
33
+ "INSERT OR IGNORE INTO mind_time_commitment_reviews(seq,reviewed) VALUES(?,?)",
34
+ )
35
+ .run(r.seq, now);
36
+ return candidate;
37
+ });
38
+ if (!enabled.length)
39
+ return {
40
+ through: rows.at(-1).seq,
41
+ rows: [],
42
+ session: null,
43
+ messages: [],
44
+ };
45
+ const session = enabled[0].session_id,
46
+ same = enabled.filter((r) => r.session_id === session);
47
+ const selected = same.slice(0, 12);
48
+ const chosen = new Set(selected.map((r) => r.seq));
49
+ const firstOther = enabled.find((r) => !chosen.has(r.seq));
50
+ const messages = selected.map((r) => {
51
+ const m = parse(r.payload, {}),
52
+ refs = Array.isArray(m.replyTargetIds) ? m.replyTargetIds : [];
53
+ const earlier = this.db
54
+ .prepare(
55
+ "SELECT seq,role,payload FROM core_events WHERE session_id=? AND seq<? AND time<=? AND seq NOT IN (SELECT seq FROM mind_unlived) AND COALESCE(json_extract(payload,'$.simulated'),0)=0 ORDER BY seq DESC LIMIT 3",
56
+ )
57
+ .all(session, r.seq, now)
58
+ .reverse();
59
+ const targets = refs
60
+ .map((seq) =>
61
+ this.db
62
+ .prepare(
63
+ "SELECT seq,role,payload FROM core_events WHERE seq=? AND session_id=? AND seq<? AND time<=? AND seq NOT IN (SELECT seq FROM mind_unlived) AND COALESCE(json_extract(payload,'$.simulated'),0)=0 AND json_extract(payload,'$.artifact') IS NULL",
64
+ )
65
+ .get(seq, session, r.seq, now),
66
+ )
67
+ .filter(Boolean);
68
+ return {
69
+ source: `m:${r.seq}`,
70
+ said: text(m.text, 700),
71
+ earlier: [
72
+ ...new Map([...earlier, ...targets].map((x) => [x.seq, x])).values(),
73
+ ].map((x) => ({
74
+ ref: `m:${x.seq}`,
75
+ role: x.role,
76
+ text: text(parse(x.payload, {}).text, 240),
77
+ })),
78
+ };
79
+ });
80
+ return {
81
+ session,
82
+ rows: selected,
83
+ messages,
84
+ through: firstOther ? firstOther.seq - 1 : rows.at(-1).seq,
85
+ };
86
+ }
87
+ apply(batch, result, now) {
88
+ if (!Array.isArray(result?.commitments)) throw Error("约定回看格式无效");
89
+ const added = [],
90
+ perSource = new Map();
91
+ for (const item of result.commitments.slice(0, 6)) {
92
+ const row = batch.rows.find((r) => `m:${r.seq}` === item.source),
93
+ quote = text(item.quote, 240),
94
+ title = text(item.title, 240);
95
+ if (
96
+ !row ||
97
+ item.accepted !== true ||
98
+ !quote ||
99
+ !title ||
100
+ hasCredential(title + quote)
101
+ )
102
+ continue;
103
+ const m = parse(row.payload, {}),
104
+ message = batch.messages.find((x) => x.source === item.source);
105
+ if (
106
+ !String(m.text || "").includes(quote) ||
107
+ !ACTION.test(title) ||
108
+ /(?:开玩笑|角色台词|小说里|故事里|假如|假设|不答应|没答应|不能答应)/.test(
109
+ m.text,
110
+ )
111
+ )
112
+ continue;
113
+ const future = FUTURE.test(m.text) && ACTION.test(m.text);
114
+ const accepted =
115
+ /^(?:嗯[,,]?|好|行|可以|没问题|会的|答应)/.test(m.text) &&
116
+ message.earlier.some(
117
+ (e) =>
118
+ e.role === "user" &&
119
+ /(?:帮我|给我|能不能|你能|你可以|请你|你来)/.test(e.text) &&
120
+ ACTION.test(e.text),
121
+ );
122
+ if (!future && !accepted) continue;
123
+ if (this.time.tasks.delivery.resolve(row, m, {}, now)) continue;
124
+ const linked = this.db
125
+ .prepare(
126
+ "SELECT task_id FROM mind_time_task_aliases WHERE kind='source' AND substr(alias_id,instr(alias_id,':')+1)=?",
127
+ )
128
+ .all(item.source)
129
+ .map((r) =>
130
+ this.time.tasks.get(this.time.tasks.links.canonical(r.task_id)),
131
+ )
132
+ .filter(Boolean);
133
+ if (linked.length) {
134
+ added.push(...linked.map((t) => t.id));
135
+ perSource.set(
136
+ row.seq,
137
+ linked.map((t) => t.id),
138
+ );
139
+ continue;
140
+ }
141
+ if (item.activity !== "unknown" && !isExecutable(item.activity)) continue;
142
+ const kind = item.kind === "plan" ? "plan" : "promise",
143
+ subject =
144
+ kind === "promise"
145
+ ? intentRecipient(this.db, {
146
+ session: row.session_id,
147
+ title: m.text,
148
+ sources: [row.seq],
149
+ accepted: true,
150
+ })
151
+ : null;
152
+ const result = this.time.tasks.add(
153
+ {
154
+ kind,
155
+ activity: item.activity,
156
+ title,
157
+ why: "独处回看时补回自己实际说出口的约定",
158
+ sources: intentMessageSources(this.db, row, m),
159
+ session: row.session_id,
160
+ subject,
161
+ origin: "commitment-review",
162
+ ...spokenDate(m.text, row.time, this.time.mind.timeZone()),
163
+ },
164
+ now,
165
+ );
166
+ const id = result.id || result.duplicate;
167
+ if (id) {
168
+ added.push(id);
169
+ perSource.set(row.seq, [...(perSource.get(row.seq) || []), id]);
170
+ if (result.id && kind === "promise" && !subject)
171
+ this.time.tasks.wait(
172
+ this.time.tasks.get(id),
173
+ "等待明确交付给谁",
174
+ Number.MAX_SAFE_INTEGER,
175
+ now,
176
+ );
177
+ this.time.event(
178
+ id,
179
+ "commitment-recovered",
180
+ "从实际说出口的话中补回约定",
181
+ { source: item.source, quote },
182
+ now,
183
+ );
184
+ }
185
+ }
186
+ for (const row of batch.rows)
187
+ this.db
188
+ .prepare(
189
+ "INSERT OR IGNORE INTO mind_time_commitment_reviews(seq,reviewed,task_ids) VALUES(?,?,?)",
190
+ )
191
+ .run(
192
+ row.seq,
193
+ now,
194
+ JSON.stringify([...new Set(perSource.get(row.seq) || [])]),
195
+ );
196
+ return [...new Set(added)];
197
+ }
198
+ }
@@ -0,0 +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
+ }