luckytri 1.0.6 → 1.0.7
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/LICENSE +21 -21
- package/docs/en/CHANGELOG.md +25 -16
- package/docs/en/chat-style.md +28 -9
- package/docs/en/guide.md +5 -5
- package/docs/en/her-life.md +2 -2
- package/docs/en/webui.md +1 -1
- package/docs/zh/CHANGELOG.md +25 -16
- package/docs/zh/chat-style.md +30 -9
- package/docs/zh/guide.md +5 -5
- package/docs/zh/her-life.md +2 -2
- package/docs/zh/webui.md +1 -1
- package/package.json +1 -1
- package/public/app/assets/{ChatsPage-DCYMkJ91.js → ChatsPage-CshdtTzV.js} +2 -2
- package/public/app/assets/ChatsPage-DfyPCS9N.css +1 -0
- package/public/app/assets/{Empty-CLyZkxR5.js → Empty-C7yzOBzy.js} +1 -1
- package/public/app/assets/{Empty-DIobYG3x.css → Empty-DmUB2-Bk.css} +1 -1
- package/public/app/assets/{Field-BpaRwLI_.js → Field-BjFv4Rfn.js} +1 -1
- package/public/app/assets/HeartPage-Bft9wZp5.css +1 -0
- package/public/app/assets/{HeartPage-BIOMTiwr.js → HeartPage-eqo6ru16.js} +1 -1
- package/public/app/assets/LifePage-BUi5euBF.css +1 -0
- package/public/app/assets/{LifePage-BCgM4JkR.js → LifePage-CN8GE5NK.js} +1 -1
- package/public/app/assets/{MemoryPage-vShvRqMt.js → MemoryPage-CCpMN4Wg.js} +1 -1
- package/public/app/assets/MemoryPage-cZvXfkXb.css +1 -0
- package/public/app/assets/{NaturePage-Cy9ptr4G.js → NaturePage-C0JqdBTR.js} +2 -2
- package/public/app/assets/NaturePage-CHkpb5NK.css +1 -0
- package/public/app/assets/PeoplePage-BTU0H1pM.css +1 -0
- package/public/app/assets/{PeoplePage-CUZekB2E.js → PeoplePage-BXGBbO4u.js} +1 -1
- package/public/app/assets/{PluginsPage-1RM4rmJP.js → PluginsPage-Dksp5AV5.js} +1 -1
- package/public/app/assets/{SystemPage-BI2CB5Ig.css → SystemPage-BenIyTvk.css} +1 -1
- package/public/app/assets/{SystemPage-BeT5TmIV.js → SystemPage-Cd7aa8eE.js} +1 -1
- package/public/app/assets/TimePage-BOms_n4I.css +1 -0
- package/public/app/assets/{TimePage-taTmPMM8.js → TimePage-Bs_zImb4.js} +2 -2
- package/public/app/assets/{index-DY5AM0Hz.css → index-BHZcHUMh.css} +1 -1
- package/public/app/assets/{index-DdYwC5ZU.js → index-CyRtYxP3.js} +3 -3
- package/public/app/assets/{knowledge-DJmfu2KN.js → knowledge-BJOIyuJo.js} +1 -1
- package/public/app/assets/{media-BEtnE0Gv.js → media-Bwtx8blz.js} +1 -1
- package/public/app/assets/{mind-Dtiw0-QK.js → mind-DtTVrW6Z.js} +1 -1
- package/public/app/index.html +5 -6
- package/public/guide.en.html +3 -3
- package/public/guide.html +3 -3
- package/scripts/import-data.js +128 -128
- package/scripts/proxy-config.js +167 -167
- package/scripts/update-instance.js +201 -201
- package/server/channels/adapters.js +27 -27
- package/server/channels/onebot/adapter.js +288 -288
- package/server/channels/qqbot/adapter.js +253 -253
- package/server/channels/qqbot/api.js +199 -199
- package/server/channels/qqbot/credentials.js +11 -11
- package/server/channels/qqbot/gateway.js +274 -274
- package/server/channels/qqbot/index.js +389 -389
- package/server/channels/segments.js +40 -40
- package/server/channels/session-key.js +153 -153
- package/server/channels/state.js +42 -42
- package/server/core/api.js +564 -564
- package/server/core/context-builder.js +288 -288
- package/server/core/context-compactor.js +454 -454
- package/server/core/conversation-cues.js +365 -325
- package/server/core/conversation-grounding.js +178 -0
- package/server/core/conversation-manager.js +87 -87
- package/server/core/dialogue-context.js +52 -0
- package/server/core/feedback.js +8 -8
- package/server/core/group-style.js +72 -72
- package/server/core/initiative-context.js +66 -66
- package/server/core/input-budget.js +32 -32
- package/server/core/local-demo.js +98 -98
- package/server/core/message-manager.js +118 -118
- package/server/core/message-scheduler.js +78 -78
- package/server/core/model-json.js +190 -190
- package/server/core/model-presets.js +900 -900
- package/server/core/name-call.js +29 -29
- package/server/core/network.js +157 -157
- package/server/core/person-name.js +22 -22
- package/server/core/persona-manager.js +324 -269
- package/server/core/reply-delivery.js +472 -404
- package/server/core/reply-target-resolver.js +50 -50
- package/server/core/response-generator.js +58 -50
- package/server/core/response-validator.js +30 -27
- package/server/core/retired-prompts.js +51 -51
- package/server/core/sessions.js +109 -109
- package/server/core/speaker-names.js +92 -92
- package/server/core/topic-tracker.js +65 -65
- package/server/core/turn.js +42 -8
- package/server/http.js +18 -18
- package/server/i18n/decisions.js +209 -209
- package/server/i18n/index.js +144 -144
- package/server/i18n/labels.js +0 -5
- package/server/i18n/models.js +111 -111
- package/server/knowledge/api.js +342 -342
- package/server/knowledge/embedding-profile.js +114 -114
- package/server/knowledge/index.js +13 -13
- package/server/knowledge/retrieval.js +93 -93
- package/server/knowledge/schema.js +158 -158
- package/server/knowledge/vector-index.js +90 -90
- package/server/mind/affect.js +265 -265
- package/server/mind/attention.js +200 -200
- package/server/mind/bonds.js +565 -565
- package/server/mind/clock.js +83 -83
- package/server/mind/context-selection.js +16 -4
- package/server/mind/continuity.js +562 -562
- package/server/mind/conversation-origin.js +175 -175
- package/server/mind/days.js +210 -210
- package/server/mind/evidence-read.js +45 -45
- package/server/mind/faces.js +373 -373
- package/server/mind/guard.js +68 -60
- package/server/mind/initiative.js +338 -338
- package/server/mind/life/activities.js +21 -21
- package/server/mind/life/context.js +330 -330
- package/server/mind/life/growth.js +261 -261
- package/server/mind/life/presence.js +305 -305
- package/server/mind/meetings.js +1069 -1069
- package/server/mind/memory-policy.js +48 -48
- package/server/mind/memory.js +888 -888
- package/server/mind/nature.js +358 -358
- package/server/mind/novelty.js +19 -19
- package/server/mind/periods.js +118 -118
- package/server/mind/reading.js +120 -120
- package/server/mind/relationship-context.js +71 -71
- package/server/mind/relationships.js +390 -390
- package/server/mind/salience.js +201 -201
- package/server/mind/schema.js +567 -567
- package/server/mind/self.js +496 -484
- package/server/mind/thoughts.js +274 -274
- package/server/mind/time/agenda.js +242 -242
- package/server/mind/time/api.js +200 -200
- package/server/mind/time/attention.js +34 -34
- package/server/mind/time/delivery-links.js +130 -130
- package/server/mind/time/games.js +7 -3
- package/server/mind/time/index.js +3 -0
- package/server/mind/time/model-material.js +77 -77
- package/server/mind/time/presentation.js +35 -51
- package/server/mind/time/schedule.js +83 -83
- package/server/mind/time/schema.js +84 -84
- package/server/mind/time/search.js +383 -383
- package/server/mind/time/sharing.js +472 -472
- package/server/mind/time/task-links.js +311 -311
- package/server/mind/time/works.js +438 -432
- package/server/mind/traits.js +191 -191
- package/server/mind/view.js +43 -6
- package/server/runtime/installation.js +116 -116
- package/server/runtime/update-manager.js +140 -140
- package/server/storage/data-package.js +298 -298
- package/server/storage/data-transfer-api.js +50 -50
- package/server/storage/data-transfer-worker.js +18 -18
- package/server/storage/data-transfer.js +416 -416
- package/server/studio/display-names.js +139 -139
- package/server/studio/management.js +102 -102
- package/server/studio/updates.js +144 -144
- package/server/version.js +10 -10
- package/public/app/assets/ChatsPage-CFMVozoG.css +0 -1
- package/public/app/assets/HeartPage-C-2jvgzF.css +0 -1
- package/public/app/assets/LifePage-CG1OHbRK.css +0 -1
- package/public/app/assets/MemoryPage-C_Bu4rfb.css +0 -1
- package/public/app/assets/NaturePage-CPZlImT0.css +0 -1
- package/public/app/assets/PeoplePage-3RRUITDz.css +0 -1
- package/public/app/assets/TimePage-C-guwPk2.css +0 -1
|
@@ -1,383 +1,383 @@
|
|
|
1
|
-
import { createHash, randomUUID } from "node:crypto";
|
|
2
|
-
import { dayKey, hasCredential, text } from "../util.js";
|
|
3
|
-
import { describeNetworkError, sanitizeDetail } from "../../core/network.js";
|
|
4
|
-
import { modelMaterial, MODEL_MATERIAL_LABEL } from "./model-material.js";
|
|
5
|
-
export const SEARCH_DEFAULTS = {
|
|
6
|
-
enabled: true,
|
|
7
|
-
provider: "tavily",
|
|
8
|
-
baseUrl: "https://api.tavily.com/search",
|
|
9
|
-
apiKey: "",
|
|
10
|
-
timeoutMs: 20000,
|
|
11
|
-
maxResults: 5,
|
|
12
|
-
dailyLimit: 12,
|
|
13
|
-
};
|
|
14
|
-
const ENDPOINTS = {
|
|
15
|
-
tavily: "https://api.tavily.com/search",
|
|
16
|
-
brave: "https://api.search.brave.com/res/v1/web/search",
|
|
17
|
-
};
|
|
18
|
-
export const SEARCH_TEST_TOPIC = "ATRI 游戏 资料简介";
|
|
19
|
-
export class Search {
|
|
20
|
-
constructor(time) {
|
|
21
|
-
this.time = time;
|
|
22
|
-
this.db = time.db;
|
|
23
|
-
this.fetch = (...args) => fetch(...args);
|
|
24
|
-
this.testing = false;
|
|
25
|
-
this.db
|
|
26
|
-
.prepare(
|
|
27
|
-
"UPDATE mind_time_searches SET state='error',error='实例中断,查询结果未确认' WHERE state='running'",
|
|
28
|
-
)
|
|
29
|
-
.run();
|
|
30
|
-
}
|
|
31
|
-
bind(life) {
|
|
32
|
-
this.life = life;
|
|
33
|
-
this.wake();
|
|
34
|
-
}
|
|
35
|
-
mode(profile = this.profile()) {
|
|
36
|
-
return profile.enabled && profile.apiKey ? "web" : "model";
|
|
37
|
-
}
|
|
38
|
-
wake() {
|
|
39
|
-
const now = this.time.now();
|
|
40
|
-
if (this.ready(now)) return;
|
|
41
|
-
for (const row of this.db
|
|
42
|
-
.prepare(
|
|
43
|
-
"SELECT id,ready_at FROM mind_time_tasks WHERE activity='game' AND state='waiting' AND wait_reason IN ('等待独立搜索密钥','独立搜索尚未启用','今日资料查询次数已用完') AND json_extract(checkpoint,'$.mergedInto') IS NULL",
|
|
44
|
-
)
|
|
45
|
-
.all())
|
|
46
|
-
this.time.tasks.control(
|
|
47
|
-
row.id,
|
|
48
|
-
{
|
|
49
|
-
action: "resume",
|
|
50
|
-
readyAt: Math.max(now, row.ready_at),
|
|
51
|
-
reason: "资料查询条件已恢复,重新按优先级安排",
|
|
52
|
-
},
|
|
53
|
-
now,
|
|
54
|
-
);
|
|
55
|
-
}
|
|
56
|
-
profile(provider) {
|
|
57
|
-
const saved = this.time.mind.repo.config("search-profile", {});
|
|
58
|
-
const chosen = provider || saved.provider || "tavily";
|
|
59
|
-
const profile = {
|
|
60
|
-
...SEARCH_DEFAULTS,
|
|
61
|
-
baseUrl: ENDPOINTS[chosen],
|
|
62
|
-
...saved.profiles?.[chosen],
|
|
63
|
-
provider: chosen,
|
|
64
|
-
};
|
|
65
|
-
// Existing keys may have been pasted with padding before save normalized it.
|
|
66
|
-
// Activity reads and connection tests must use the same credential bytes.
|
|
67
|
-
return { ...profile, apiKey: profile.apiKey.trim() };
|
|
68
|
-
}
|
|
69
|
-
public(provider, testedProfile) {
|
|
70
|
-
const { apiKey, ...profile } = testedProfile || this.profile(provider);
|
|
71
|
-
const usage = this.usage(this.time.now());
|
|
72
|
-
return {
|
|
73
|
-
...profile,
|
|
74
|
-
hasApiKey: !!apiKey,
|
|
75
|
-
mode: this.mode({ ...profile, apiKey }),
|
|
76
|
-
materialLabel:
|
|
77
|
-
this.mode({ ...profile, apiKey }) === "model"
|
|
78
|
-
? MODEL_MATERIAL_LABEL
|
|
79
|
-
: "联网检索资料",
|
|
80
|
-
used: usage.used,
|
|
81
|
-
usage,
|
|
82
|
-
};
|
|
83
|
-
}
|
|
84
|
-
validated(input = {}) {
|
|
85
|
-
const old = this.time.mind.repo.config("search-profile", {}),
|
|
86
|
-
provider = input.provider || old.provider || "tavily";
|
|
87
|
-
if (!ENDPOINTS[provider]) throw Error("搜索提供方无效");
|
|
88
|
-
const next = { ...this.profile(provider), ...input, provider };
|
|
89
|
-
if (typeof next.apiKey === "string") next.apiKey = next.apiKey.trim();
|
|
90
|
-
if (
|
|
91
|
-
typeof next.enabled !== "boolean" ||
|
|
92
|
-
typeof next.apiKey !== "string" ||
|
|
93
|
-
/[\r\n]/.test(next.apiKey) ||
|
|
94
|
-
next.apiKey.length > 2000
|
|
95
|
-
)
|
|
96
|
-
throw Error("搜索配置格式无效");
|
|
97
|
-
if (!next.apiKey) next.apiKey = this.profile(provider).apiKey.trim();
|
|
98
|
-
if (input.clearKey) next.apiKey = "";
|
|
99
|
-
const url = new URL(next.baseUrl);
|
|
100
|
-
if (
|
|
101
|
-
!["https:", "http:"].includes(url.protocol) ||
|
|
102
|
-
url.username ||
|
|
103
|
-
url.password ||
|
|
104
|
-
url.hash ||
|
|
105
|
-
url.search
|
|
106
|
-
)
|
|
107
|
-
throw Error("搜索地址无效");
|
|
108
|
-
for (const [key, min, max] of [
|
|
109
|
-
["timeoutMs", 1000, 120000],
|
|
110
|
-
["maxResults", 1, 20],
|
|
111
|
-
["dailyLimit", 0, 100000],
|
|
112
|
-
])
|
|
113
|
-
if (!Number.isInteger(next[key]) || next[key] < min || next[key] > max)
|
|
114
|
-
throw Error(key + " 超出范围");
|
|
115
|
-
const profile = Object.fromEntries(
|
|
116
|
-
Object.keys(SEARCH_DEFAULTS).map((key) => [key, next[key]]),
|
|
117
|
-
);
|
|
118
|
-
return { ...profile, provider };
|
|
119
|
-
}
|
|
120
|
-
save(input) {
|
|
121
|
-
const profile = this.validated(input);
|
|
122
|
-
const old = this.time.mind.repo.config("search-profile", {});
|
|
123
|
-
this.time.mind.repo.saveConfig("search-profile", {
|
|
124
|
-
provider: profile.provider,
|
|
125
|
-
profiles: { ...old.profiles, [profile.provider]: profile },
|
|
126
|
-
});
|
|
127
|
-
this.wake();
|
|
128
|
-
return this.public();
|
|
129
|
-
}
|
|
130
|
-
usage(now) {
|
|
131
|
-
const day = dayKey(now, this.time.mind.timeZone());
|
|
132
|
-
const rows = this.db
|
|
133
|
-
.prepare(
|
|
134
|
-
"SELECT created,state,purpose FROM mind_time_searches WHERE created<=? AND created>=?",
|
|
135
|
-
)
|
|
136
|
-
.all(now, now - 2 * 86400000)
|
|
137
|
-
.filter((r) => dayKey(r.created, this.time.mind.timeZone()) === day);
|
|
138
|
-
const activity = rows.filter((r) => r.purpose !== "diagnostic"),
|
|
139
|
-
successful = activity.filter((r) => r.state === "done").length,
|
|
140
|
-
running = activity.filter((r) => r.state === "running").length;
|
|
141
|
-
return {
|
|
142
|
-
used: successful + running,
|
|
143
|
-
successful,
|
|
144
|
-
running,
|
|
145
|
-
failed: activity.filter((r) => r.state === "error").length,
|
|
146
|
-
diagnostics: rows.filter((r) => r.purpose === "diagnostic").length,
|
|
147
|
-
};
|
|
148
|
-
}
|
|
149
|
-
used(now) {
|
|
150
|
-
return this.usage(now).used;
|
|
151
|
-
}
|
|
152
|
-
ready(now = this.time.now(), p = this.profile(), diagnostic = false) {
|
|
153
|
-
if (!diagnostic && p.dailyLimit > 0 && this.used(now) >= p.dailyLimit)
|
|
154
|
-
return "今日资料查询次数已用完";
|
|
155
|
-
if (this.mode(p) === "model") {
|
|
156
|
-
try {
|
|
157
|
-
const model = this.life?.profile();
|
|
158
|
-
if (!model || !(model.apiKey || process.env.LLM_API_KEY))
|
|
159
|
-
return "等待可用模型";
|
|
160
|
-
if (this.life.closed) return "等待可用模型";
|
|
161
|
-
} catch {
|
|
162
|
-
return "等待可用模型";
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
return "";
|
|
166
|
-
}
|
|
167
|
-
async query(
|
|
168
|
-
topic,
|
|
169
|
-
{
|
|
170
|
-
projectId = null,
|
|
171
|
-
now = this.time.now(),
|
|
172
|
-
trace,
|
|
173
|
-
valid = () => true,
|
|
174
|
-
profile = this.profile(),
|
|
175
|
-
purpose = "activity",
|
|
176
|
-
} = {},
|
|
177
|
-
) {
|
|
178
|
-
if (!["activity", "diagnostic"].includes(purpose))
|
|
179
|
-
throw Error("查询用途无效");
|
|
180
|
-
const why = this.ready(now, profile, purpose === "diagnostic");
|
|
181
|
-
if (why) throw Error(why);
|
|
182
|
-
topic = text(topic, 180);
|
|
183
|
-
if (
|
|
184
|
-
!topic ||
|
|
185
|
-
hasCredential(topic) ||
|
|
186
|
-
/\b\d{5,}\b|(?:群|私聊|账号)\s*[::=]/.test(topic)
|
|
187
|
-
)
|
|
188
|
-
throw Error("查询只能包含资料主题");
|
|
189
|
-
const p = profile,
|
|
190
|
-
id = Number(
|
|
191
|
-
this.db
|
|
192
|
-
.prepare(
|
|
193
|
-
"INSERT INTO mind_time_searches(created,provider,query,state,purpose) VALUES (?,?,?,'running',?)",
|
|
194
|
-
)
|
|
195
|
-
.run(
|
|
196
|
-
now,
|
|
197
|
-
this.mode(p) === "model" ? "model" : p.provider,
|
|
198
|
-
topic,
|
|
199
|
-
purpose,
|
|
200
|
-
).lastInsertRowid,
|
|
201
|
-
);
|
|
202
|
-
const headers =
|
|
203
|
-
p.provider === "tavily"
|
|
204
|
-
? {
|
|
205
|
-
"Content-Type": "application/json",
|
|
206
|
-
Authorization: "Bearer " + p.apiKey,
|
|
207
|
-
}
|
|
208
|
-
: { "X-Subscription-Token": p.apiKey, Accept: "application/json" };
|
|
209
|
-
const endpoint = new URL(p.baseUrl);
|
|
210
|
-
let body;
|
|
211
|
-
if (p.provider === "tavily")
|
|
212
|
-
body = JSON.stringify({
|
|
213
|
-
query: topic,
|
|
214
|
-
search_depth: "basic",
|
|
215
|
-
max_results: p.maxResults,
|
|
216
|
-
include_answer: false,
|
|
217
|
-
include_raw_content: false,
|
|
218
|
-
});
|
|
219
|
-
else {
|
|
220
|
-
endpoint.searchParams.set("q", topic);
|
|
221
|
-
endpoint.searchParams.set("count", String(p.maxResults));
|
|
222
|
-
endpoint.searchParams.set("extra_snippets", "true");
|
|
223
|
-
}
|
|
224
|
-
try {
|
|
225
|
-
let results;
|
|
226
|
-
if (this.mode(p) === "model") {
|
|
227
|
-
results = await modelMaterial(this.life, topic, {
|
|
228
|
-
trace,
|
|
229
|
-
maxResults: p.maxResults,
|
|
230
|
-
});
|
|
231
|
-
} else {
|
|
232
|
-
const response = await this.fetch(endpoint, {
|
|
233
|
-
method: p.provider === "tavily" ? "POST" : "GET",
|
|
234
|
-
headers,
|
|
235
|
-
...(body ? { body } : {}),
|
|
236
|
-
signal: AbortSignal.timeout(p.timeoutMs),
|
|
237
|
-
redirect: "error",
|
|
238
|
-
});
|
|
239
|
-
if (!response.ok) {
|
|
240
|
-
const data = await response.json().catch(() => ({}));
|
|
241
|
-
const detail =
|
|
242
|
-
data.detail?.error ||
|
|
243
|
-
data.error?.message ||
|
|
244
|
-
(typeof data.error === "string" ? data.error : "");
|
|
245
|
-
const hint = [401, 403].includes(response.status)
|
|
246
|
-
? "请核对独立密钥与权限"
|
|
247
|
-
: [429, 432, 433].includes(response.status)
|
|
248
|
-
? "提供方限制了请求频率或账户额度"
|
|
249
|
-
: [404, 405].includes(response.status)
|
|
250
|
-
? "请核对提供方与完整搜索地址"
|
|
251
|
-
: "请检查搜索服务与请求配置";
|
|
252
|
-
throw Error(`搜索接口 HTTP ${response.status}:${detail || hint}`);
|
|
253
|
-
}
|
|
254
|
-
const data = await response.json().catch(() => {
|
|
255
|
-
throw Error("搜索接口未返回有效 JSON,请核对提供方与完整搜索地址");
|
|
256
|
-
}),
|
|
257
|
-
raw = p.provider === "tavily" ? data?.results : data?.web?.results;
|
|
258
|
-
if (
|
|
259
|
-
!Array.isArray(raw) &&
|
|
260
|
-
!(p.provider === "brave" && data?.type === "search" && !data.web)
|
|
261
|
-
)
|
|
262
|
-
throw Error(
|
|
263
|
-
"搜索返回格式与选定提供方不匹配,请核对提供方与完整搜索地址",
|
|
264
|
-
);
|
|
265
|
-
results = (Array.isArray(raw) ? raw : [])
|
|
266
|
-
.slice(0, p.maxResults)
|
|
267
|
-
.map((row) => ({
|
|
268
|
-
kind: "web",
|
|
269
|
-
model: "",
|
|
270
|
-
uncertainty: "",
|
|
271
|
-
timing: {},
|
|
272
|
-
title: text(row.title, 200),
|
|
273
|
-
url: String(row.url || ""),
|
|
274
|
-
content: text(
|
|
275
|
-
p.provider === "tavily"
|
|
276
|
-
? row.content
|
|
277
|
-
: [row.description, ...(row.extra_snippets || [])]
|
|
278
|
-
.filter(Boolean)
|
|
279
|
-
.join("\n"),
|
|
280
|
-
6000,
|
|
281
|
-
),
|
|
282
|
-
}))
|
|
283
|
-
.filter((row) => {
|
|
284
|
-
try {
|
|
285
|
-
return (
|
|
286
|
-
["https:", "http:"].includes(new URL(row.url).protocol) &&
|
|
287
|
-
row.content &&
|
|
288
|
-
!hasCredential(row.content)
|
|
289
|
-
);
|
|
290
|
-
} catch {
|
|
291
|
-
return false;
|
|
292
|
-
}
|
|
293
|
-
});
|
|
294
|
-
}
|
|
295
|
-
if (!valid()) throw Error("任务已经变化,资料结果未提交");
|
|
296
|
-
const saved = [];
|
|
297
|
-
for (const row of results) {
|
|
298
|
-
const hash = createHash("sha256").update(row.content).digest("hex"),
|
|
299
|
-
sourceId = randomUUID();
|
|
300
|
-
if (projectId) {
|
|
301
|
-
this.db
|
|
302
|
-
.prepare(
|
|
303
|
-
"INSERT OR IGNORE INTO mind_time_sources(id,project_id,created,title,url,content,hash,kind,model,uncertainty,timing) VALUES (?,?,?,?,?,?,?,?,?,?,?)",
|
|
304
|
-
)
|
|
305
|
-
.run(
|
|
306
|
-
sourceId,
|
|
307
|
-
projectId,
|
|
308
|
-
now,
|
|
309
|
-
row.title,
|
|
310
|
-
row.url,
|
|
311
|
-
row.content,
|
|
312
|
-
hash,
|
|
313
|
-
row.kind,
|
|
314
|
-
row.model,
|
|
315
|
-
row.uncertainty,
|
|
316
|
-
JSON.stringify(row.timing || {}),
|
|
317
|
-
);
|
|
318
|
-
saved.push(
|
|
319
|
-
this.db
|
|
320
|
-
.prepare(
|
|
321
|
-
"SELECT * FROM mind_time_sources WHERE project_id=? AND url=? AND hash=?",
|
|
322
|
-
)
|
|
323
|
-
.get(projectId, row.url, hash),
|
|
324
|
-
);
|
|
325
|
-
} else saved.push({ ...row, hash, created: now });
|
|
326
|
-
}
|
|
327
|
-
this.db
|
|
328
|
-
.prepare("UPDATE mind_time_searches SET state='done' WHERE id=?")
|
|
329
|
-
.run(id);
|
|
330
|
-
return saved;
|
|
331
|
-
} catch (error) {
|
|
332
|
-
const network = describeNetworkError(error);
|
|
333
|
-
const message =
|
|
334
|
-
network.code === "TIMEOUT"
|
|
335
|
-
? "搜索连接超时,请检查网络、代理或增加超时"
|
|
336
|
-
: network.code
|
|
337
|
-
? `搜索连接失败(${network.code}${network.detail ? ":" + network.detail : ""}),请检查网络和代理;代理状态改变后可重启实例`
|
|
338
|
-
: String(error.message);
|
|
339
|
-
const reason = sanitizeDetail(
|
|
340
|
-
p.apiKey ? message.split(p.apiKey).join("[密钥已隐藏]") : message,
|
|
341
|
-
300,
|
|
342
|
-
);
|
|
343
|
-
this.db
|
|
344
|
-
.prepare(
|
|
345
|
-
"UPDATE mind_time_searches SET state='error',error=? WHERE id=?",
|
|
346
|
-
)
|
|
347
|
-
.run(reason, id);
|
|
348
|
-
throw Error(reason);
|
|
349
|
-
}
|
|
350
|
-
}
|
|
351
|
-
async test(input = {}) {
|
|
352
|
-
if (this.testing) {
|
|
353
|
-
const error = Error("正在测试搜索连接,请稍后");
|
|
354
|
-
error.status = 429;
|
|
355
|
-
throw error;
|
|
356
|
-
}
|
|
357
|
-
this.testing = true;
|
|
358
|
-
try {
|
|
359
|
-
const profile = this.validated(input);
|
|
360
|
-
const run = () =>
|
|
361
|
-
this.query(SEARCH_TEST_TOPIC, { profile, purpose: "diagnostic" });
|
|
362
|
-
const results =
|
|
363
|
-
this.mode(profile) === "model"
|
|
364
|
-
? await this.time.attention.activity(run)
|
|
365
|
-
: await run();
|
|
366
|
-
if (results === null) throw Error("正在推进活动步骤,请稍后测试");
|
|
367
|
-
return {
|
|
368
|
-
results,
|
|
369
|
-
profile: this.public(profile.provider, profile),
|
|
370
|
-
diagnostic: true,
|
|
371
|
-
};
|
|
372
|
-
} finally {
|
|
373
|
-
this.testing = false;
|
|
374
|
-
}
|
|
375
|
-
}
|
|
376
|
-
sources(projectId) {
|
|
377
|
-
return this.db
|
|
378
|
-
.prepare(
|
|
379
|
-
"SELECT id,created,title,url,length(content) characters,hash,kind,model,uncertainty,timing FROM mind_time_sources WHERE project_id=? ORDER BY created DESC LIMIT 100",
|
|
380
|
-
)
|
|
381
|
-
.all(projectId);
|
|
382
|
-
}
|
|
383
|
-
}
|
|
1
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
2
|
+
import { dayKey, hasCredential, text } from "../util.js";
|
|
3
|
+
import { describeNetworkError, sanitizeDetail } from "../../core/network.js";
|
|
4
|
+
import { modelMaterial, MODEL_MATERIAL_LABEL } from "./model-material.js";
|
|
5
|
+
export const SEARCH_DEFAULTS = {
|
|
6
|
+
enabled: true,
|
|
7
|
+
provider: "tavily",
|
|
8
|
+
baseUrl: "https://api.tavily.com/search",
|
|
9
|
+
apiKey: "",
|
|
10
|
+
timeoutMs: 20000,
|
|
11
|
+
maxResults: 5,
|
|
12
|
+
dailyLimit: 12,
|
|
13
|
+
};
|
|
14
|
+
const ENDPOINTS = {
|
|
15
|
+
tavily: "https://api.tavily.com/search",
|
|
16
|
+
brave: "https://api.search.brave.com/res/v1/web/search",
|
|
17
|
+
};
|
|
18
|
+
export const SEARCH_TEST_TOPIC = "ATRI 游戏 资料简介";
|
|
19
|
+
export class Search {
|
|
20
|
+
constructor(time) {
|
|
21
|
+
this.time = time;
|
|
22
|
+
this.db = time.db;
|
|
23
|
+
this.fetch = (...args) => fetch(...args);
|
|
24
|
+
this.testing = false;
|
|
25
|
+
this.db
|
|
26
|
+
.prepare(
|
|
27
|
+
"UPDATE mind_time_searches SET state='error',error='实例中断,查询结果未确认' WHERE state='running'",
|
|
28
|
+
)
|
|
29
|
+
.run();
|
|
30
|
+
}
|
|
31
|
+
bind(life) {
|
|
32
|
+
this.life = life;
|
|
33
|
+
this.wake();
|
|
34
|
+
}
|
|
35
|
+
mode(profile = this.profile()) {
|
|
36
|
+
return profile.enabled && profile.apiKey ? "web" : "model";
|
|
37
|
+
}
|
|
38
|
+
wake() {
|
|
39
|
+
const now = this.time.now();
|
|
40
|
+
if (this.ready(now)) return;
|
|
41
|
+
for (const row of this.db
|
|
42
|
+
.prepare(
|
|
43
|
+
"SELECT id,ready_at FROM mind_time_tasks WHERE activity='game' AND state='waiting' AND wait_reason IN ('等待独立搜索密钥','独立搜索尚未启用','今日资料查询次数已用完') AND json_extract(checkpoint,'$.mergedInto') IS NULL",
|
|
44
|
+
)
|
|
45
|
+
.all())
|
|
46
|
+
this.time.tasks.control(
|
|
47
|
+
row.id,
|
|
48
|
+
{
|
|
49
|
+
action: "resume",
|
|
50
|
+
readyAt: Math.max(now, row.ready_at),
|
|
51
|
+
reason: "资料查询条件已恢复,重新按优先级安排",
|
|
52
|
+
},
|
|
53
|
+
now,
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
profile(provider) {
|
|
57
|
+
const saved = this.time.mind.repo.config("search-profile", {});
|
|
58
|
+
const chosen = provider || saved.provider || "tavily";
|
|
59
|
+
const profile = {
|
|
60
|
+
...SEARCH_DEFAULTS,
|
|
61
|
+
baseUrl: ENDPOINTS[chosen],
|
|
62
|
+
...saved.profiles?.[chosen],
|
|
63
|
+
provider: chosen,
|
|
64
|
+
};
|
|
65
|
+
// Existing keys may have been pasted with padding before save normalized it.
|
|
66
|
+
// Activity reads and connection tests must use the same credential bytes.
|
|
67
|
+
return { ...profile, apiKey: profile.apiKey.trim() };
|
|
68
|
+
}
|
|
69
|
+
public(provider, testedProfile) {
|
|
70
|
+
const { apiKey, ...profile } = testedProfile || this.profile(provider);
|
|
71
|
+
const usage = this.usage(this.time.now());
|
|
72
|
+
return {
|
|
73
|
+
...profile,
|
|
74
|
+
hasApiKey: !!apiKey,
|
|
75
|
+
mode: this.mode({ ...profile, apiKey }),
|
|
76
|
+
materialLabel:
|
|
77
|
+
this.mode({ ...profile, apiKey }) === "model"
|
|
78
|
+
? MODEL_MATERIAL_LABEL
|
|
79
|
+
: "联网检索资料",
|
|
80
|
+
used: usage.used,
|
|
81
|
+
usage,
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
validated(input = {}) {
|
|
85
|
+
const old = this.time.mind.repo.config("search-profile", {}),
|
|
86
|
+
provider = input.provider || old.provider || "tavily";
|
|
87
|
+
if (!ENDPOINTS[provider]) throw Error("搜索提供方无效");
|
|
88
|
+
const next = { ...this.profile(provider), ...input, provider };
|
|
89
|
+
if (typeof next.apiKey === "string") next.apiKey = next.apiKey.trim();
|
|
90
|
+
if (
|
|
91
|
+
typeof next.enabled !== "boolean" ||
|
|
92
|
+
typeof next.apiKey !== "string" ||
|
|
93
|
+
/[\r\n]/.test(next.apiKey) ||
|
|
94
|
+
next.apiKey.length > 2000
|
|
95
|
+
)
|
|
96
|
+
throw Error("搜索配置格式无效");
|
|
97
|
+
if (!next.apiKey) next.apiKey = this.profile(provider).apiKey.trim();
|
|
98
|
+
if (input.clearKey) next.apiKey = "";
|
|
99
|
+
const url = new URL(next.baseUrl);
|
|
100
|
+
if (
|
|
101
|
+
!["https:", "http:"].includes(url.protocol) ||
|
|
102
|
+
url.username ||
|
|
103
|
+
url.password ||
|
|
104
|
+
url.hash ||
|
|
105
|
+
url.search
|
|
106
|
+
)
|
|
107
|
+
throw Error("搜索地址无效");
|
|
108
|
+
for (const [key, min, max] of [
|
|
109
|
+
["timeoutMs", 1000, 120000],
|
|
110
|
+
["maxResults", 1, 20],
|
|
111
|
+
["dailyLimit", 0, 100000],
|
|
112
|
+
])
|
|
113
|
+
if (!Number.isInteger(next[key]) || next[key] < min || next[key] > max)
|
|
114
|
+
throw Error(key + " 超出范围");
|
|
115
|
+
const profile = Object.fromEntries(
|
|
116
|
+
Object.keys(SEARCH_DEFAULTS).map((key) => [key, next[key]]),
|
|
117
|
+
);
|
|
118
|
+
return { ...profile, provider };
|
|
119
|
+
}
|
|
120
|
+
save(input) {
|
|
121
|
+
const profile = this.validated(input);
|
|
122
|
+
const old = this.time.mind.repo.config("search-profile", {});
|
|
123
|
+
this.time.mind.repo.saveConfig("search-profile", {
|
|
124
|
+
provider: profile.provider,
|
|
125
|
+
profiles: { ...old.profiles, [profile.provider]: profile },
|
|
126
|
+
});
|
|
127
|
+
this.wake();
|
|
128
|
+
return this.public();
|
|
129
|
+
}
|
|
130
|
+
usage(now) {
|
|
131
|
+
const day = dayKey(now, this.time.mind.timeZone());
|
|
132
|
+
const rows = this.db
|
|
133
|
+
.prepare(
|
|
134
|
+
"SELECT created,state,purpose FROM mind_time_searches WHERE created<=? AND created>=?",
|
|
135
|
+
)
|
|
136
|
+
.all(now, now - 2 * 86400000)
|
|
137
|
+
.filter((r) => dayKey(r.created, this.time.mind.timeZone()) === day);
|
|
138
|
+
const activity = rows.filter((r) => r.purpose !== "diagnostic"),
|
|
139
|
+
successful = activity.filter((r) => r.state === "done").length,
|
|
140
|
+
running = activity.filter((r) => r.state === "running").length;
|
|
141
|
+
return {
|
|
142
|
+
used: successful + running,
|
|
143
|
+
successful,
|
|
144
|
+
running,
|
|
145
|
+
failed: activity.filter((r) => r.state === "error").length,
|
|
146
|
+
diagnostics: rows.filter((r) => r.purpose === "diagnostic").length,
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
used(now) {
|
|
150
|
+
return this.usage(now).used;
|
|
151
|
+
}
|
|
152
|
+
ready(now = this.time.now(), p = this.profile(), diagnostic = false) {
|
|
153
|
+
if (!diagnostic && p.dailyLimit > 0 && this.used(now) >= p.dailyLimit)
|
|
154
|
+
return "今日资料查询次数已用完";
|
|
155
|
+
if (this.mode(p) === "model") {
|
|
156
|
+
try {
|
|
157
|
+
const model = this.life?.profile();
|
|
158
|
+
if (!model || !(model.apiKey || process.env.LLM_API_KEY))
|
|
159
|
+
return "等待可用模型";
|
|
160
|
+
if (this.life.closed) return "等待可用模型";
|
|
161
|
+
} catch {
|
|
162
|
+
return "等待可用模型";
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
return "";
|
|
166
|
+
}
|
|
167
|
+
async query(
|
|
168
|
+
topic,
|
|
169
|
+
{
|
|
170
|
+
projectId = null,
|
|
171
|
+
now = this.time.now(),
|
|
172
|
+
trace,
|
|
173
|
+
valid = () => true,
|
|
174
|
+
profile = this.profile(),
|
|
175
|
+
purpose = "activity",
|
|
176
|
+
} = {},
|
|
177
|
+
) {
|
|
178
|
+
if (!["activity", "diagnostic"].includes(purpose))
|
|
179
|
+
throw Error("查询用途无效");
|
|
180
|
+
const why = this.ready(now, profile, purpose === "diagnostic");
|
|
181
|
+
if (why) throw Error(why);
|
|
182
|
+
topic = text(topic, 180);
|
|
183
|
+
if (
|
|
184
|
+
!topic ||
|
|
185
|
+
hasCredential(topic) ||
|
|
186
|
+
/\b\d{5,}\b|(?:群|私聊|账号)\s*[::=]/.test(topic)
|
|
187
|
+
)
|
|
188
|
+
throw Error("查询只能包含资料主题");
|
|
189
|
+
const p = profile,
|
|
190
|
+
id = Number(
|
|
191
|
+
this.db
|
|
192
|
+
.prepare(
|
|
193
|
+
"INSERT INTO mind_time_searches(created,provider,query,state,purpose) VALUES (?,?,?,'running',?)",
|
|
194
|
+
)
|
|
195
|
+
.run(
|
|
196
|
+
now,
|
|
197
|
+
this.mode(p) === "model" ? "model" : p.provider,
|
|
198
|
+
topic,
|
|
199
|
+
purpose,
|
|
200
|
+
).lastInsertRowid,
|
|
201
|
+
);
|
|
202
|
+
const headers =
|
|
203
|
+
p.provider === "tavily"
|
|
204
|
+
? {
|
|
205
|
+
"Content-Type": "application/json",
|
|
206
|
+
Authorization: "Bearer " + p.apiKey,
|
|
207
|
+
}
|
|
208
|
+
: { "X-Subscription-Token": p.apiKey, Accept: "application/json" };
|
|
209
|
+
const endpoint = new URL(p.baseUrl);
|
|
210
|
+
let body;
|
|
211
|
+
if (p.provider === "tavily")
|
|
212
|
+
body = JSON.stringify({
|
|
213
|
+
query: topic,
|
|
214
|
+
search_depth: "basic",
|
|
215
|
+
max_results: p.maxResults,
|
|
216
|
+
include_answer: false,
|
|
217
|
+
include_raw_content: false,
|
|
218
|
+
});
|
|
219
|
+
else {
|
|
220
|
+
endpoint.searchParams.set("q", topic);
|
|
221
|
+
endpoint.searchParams.set("count", String(p.maxResults));
|
|
222
|
+
endpoint.searchParams.set("extra_snippets", "true");
|
|
223
|
+
}
|
|
224
|
+
try {
|
|
225
|
+
let results;
|
|
226
|
+
if (this.mode(p) === "model") {
|
|
227
|
+
results = await modelMaterial(this.life, topic, {
|
|
228
|
+
trace,
|
|
229
|
+
maxResults: p.maxResults,
|
|
230
|
+
});
|
|
231
|
+
} else {
|
|
232
|
+
const response = await this.fetch(endpoint, {
|
|
233
|
+
method: p.provider === "tavily" ? "POST" : "GET",
|
|
234
|
+
headers,
|
|
235
|
+
...(body ? { body } : {}),
|
|
236
|
+
signal: AbortSignal.timeout(p.timeoutMs),
|
|
237
|
+
redirect: "error",
|
|
238
|
+
});
|
|
239
|
+
if (!response.ok) {
|
|
240
|
+
const data = await response.json().catch(() => ({}));
|
|
241
|
+
const detail =
|
|
242
|
+
data.detail?.error ||
|
|
243
|
+
data.error?.message ||
|
|
244
|
+
(typeof data.error === "string" ? data.error : "");
|
|
245
|
+
const hint = [401, 403].includes(response.status)
|
|
246
|
+
? "请核对独立密钥与权限"
|
|
247
|
+
: [429, 432, 433].includes(response.status)
|
|
248
|
+
? "提供方限制了请求频率或账户额度"
|
|
249
|
+
: [404, 405].includes(response.status)
|
|
250
|
+
? "请核对提供方与完整搜索地址"
|
|
251
|
+
: "请检查搜索服务与请求配置";
|
|
252
|
+
throw Error(`搜索接口 HTTP ${response.status}:${detail || hint}`);
|
|
253
|
+
}
|
|
254
|
+
const data = await response.json().catch(() => {
|
|
255
|
+
throw Error("搜索接口未返回有效 JSON,请核对提供方与完整搜索地址");
|
|
256
|
+
}),
|
|
257
|
+
raw = p.provider === "tavily" ? data?.results : data?.web?.results;
|
|
258
|
+
if (
|
|
259
|
+
!Array.isArray(raw) &&
|
|
260
|
+
!(p.provider === "brave" && data?.type === "search" && !data.web)
|
|
261
|
+
)
|
|
262
|
+
throw Error(
|
|
263
|
+
"搜索返回格式与选定提供方不匹配,请核对提供方与完整搜索地址",
|
|
264
|
+
);
|
|
265
|
+
results = (Array.isArray(raw) ? raw : [])
|
|
266
|
+
.slice(0, p.maxResults)
|
|
267
|
+
.map((row) => ({
|
|
268
|
+
kind: "web",
|
|
269
|
+
model: "",
|
|
270
|
+
uncertainty: "",
|
|
271
|
+
timing: {},
|
|
272
|
+
title: text(row.title, 200),
|
|
273
|
+
url: String(row.url || ""),
|
|
274
|
+
content: text(
|
|
275
|
+
p.provider === "tavily"
|
|
276
|
+
? row.content
|
|
277
|
+
: [row.description, ...(row.extra_snippets || [])]
|
|
278
|
+
.filter(Boolean)
|
|
279
|
+
.join("\n"),
|
|
280
|
+
6000,
|
|
281
|
+
),
|
|
282
|
+
}))
|
|
283
|
+
.filter((row) => {
|
|
284
|
+
try {
|
|
285
|
+
return (
|
|
286
|
+
["https:", "http:"].includes(new URL(row.url).protocol) &&
|
|
287
|
+
row.content &&
|
|
288
|
+
!hasCredential(row.content)
|
|
289
|
+
);
|
|
290
|
+
} catch {
|
|
291
|
+
return false;
|
|
292
|
+
}
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
if (!valid()) throw Error("任务已经变化,资料结果未提交");
|
|
296
|
+
const saved = [];
|
|
297
|
+
for (const row of results) {
|
|
298
|
+
const hash = createHash("sha256").update(row.content).digest("hex"),
|
|
299
|
+
sourceId = randomUUID();
|
|
300
|
+
if (projectId) {
|
|
301
|
+
this.db
|
|
302
|
+
.prepare(
|
|
303
|
+
"INSERT OR IGNORE INTO mind_time_sources(id,project_id,created,title,url,content,hash,kind,model,uncertainty,timing) VALUES (?,?,?,?,?,?,?,?,?,?,?)",
|
|
304
|
+
)
|
|
305
|
+
.run(
|
|
306
|
+
sourceId,
|
|
307
|
+
projectId,
|
|
308
|
+
now,
|
|
309
|
+
row.title,
|
|
310
|
+
row.url,
|
|
311
|
+
row.content,
|
|
312
|
+
hash,
|
|
313
|
+
row.kind,
|
|
314
|
+
row.model,
|
|
315
|
+
row.uncertainty,
|
|
316
|
+
JSON.stringify(row.timing || {}),
|
|
317
|
+
);
|
|
318
|
+
saved.push(
|
|
319
|
+
this.db
|
|
320
|
+
.prepare(
|
|
321
|
+
"SELECT * FROM mind_time_sources WHERE project_id=? AND url=? AND hash=?",
|
|
322
|
+
)
|
|
323
|
+
.get(projectId, row.url, hash),
|
|
324
|
+
);
|
|
325
|
+
} else saved.push({ ...row, hash, created: now });
|
|
326
|
+
}
|
|
327
|
+
this.db
|
|
328
|
+
.prepare("UPDATE mind_time_searches SET state='done' WHERE id=?")
|
|
329
|
+
.run(id);
|
|
330
|
+
return saved;
|
|
331
|
+
} catch (error) {
|
|
332
|
+
const network = describeNetworkError(error);
|
|
333
|
+
const message =
|
|
334
|
+
network.code === "TIMEOUT"
|
|
335
|
+
? "搜索连接超时,请检查网络、代理或增加超时"
|
|
336
|
+
: network.code
|
|
337
|
+
? `搜索连接失败(${network.code}${network.detail ? ":" + network.detail : ""}),请检查网络和代理;代理状态改变后可重启实例`
|
|
338
|
+
: String(error.message);
|
|
339
|
+
const reason = sanitizeDetail(
|
|
340
|
+
p.apiKey ? message.split(p.apiKey).join("[密钥已隐藏]") : message,
|
|
341
|
+
300,
|
|
342
|
+
);
|
|
343
|
+
this.db
|
|
344
|
+
.prepare(
|
|
345
|
+
"UPDATE mind_time_searches SET state='error',error=? WHERE id=?",
|
|
346
|
+
)
|
|
347
|
+
.run(reason, id);
|
|
348
|
+
throw Error(reason);
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
async test(input = {}) {
|
|
352
|
+
if (this.testing) {
|
|
353
|
+
const error = Error("正在测试搜索连接,请稍后");
|
|
354
|
+
error.status = 429;
|
|
355
|
+
throw error;
|
|
356
|
+
}
|
|
357
|
+
this.testing = true;
|
|
358
|
+
try {
|
|
359
|
+
const profile = this.validated(input);
|
|
360
|
+
const run = () =>
|
|
361
|
+
this.query(SEARCH_TEST_TOPIC, { profile, purpose: "diagnostic" });
|
|
362
|
+
const results =
|
|
363
|
+
this.mode(profile) === "model"
|
|
364
|
+
? await this.time.attention.activity(run)
|
|
365
|
+
: await run();
|
|
366
|
+
if (results === null) throw Error("正在推进活动步骤,请稍后测试");
|
|
367
|
+
return {
|
|
368
|
+
results,
|
|
369
|
+
profile: this.public(profile.provider, profile),
|
|
370
|
+
diagnostic: true,
|
|
371
|
+
};
|
|
372
|
+
} finally {
|
|
373
|
+
this.testing = false;
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
sources(projectId) {
|
|
377
|
+
return this.db
|
|
378
|
+
.prepare(
|
|
379
|
+
"SELECT id,created,title,url,length(content) characters,hash,kind,model,uncertainty,timing FROM mind_time_sources WHERE project_id=? ORDER BY created DESC LIMIT 100",
|
|
380
|
+
)
|
|
381
|
+
.all(projectId);
|
|
382
|
+
}
|
|
383
|
+
}
|