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.
- package/.github/SECURITY.md +71 -0
- package/LICENSE +21 -0
- package/README.en.md +250 -0
- package/README.md +250 -0
- package/bin/luckytri.js +107 -0
- package/docs/brand/banner.png +0 -0
- package/docs/brand/palette.png +0 -0
- package/docs/brand/portrait.png +0 -0
- package/docs/en/CHANGELOG.md +113 -0
- package/docs/en/architecture.md +152 -0
- package/docs/en/chat-style.md +62 -0
- package/docs/en/connect.md +63 -0
- package/docs/en/guide.md +216 -0
- package/docs/en/her-life.md +189 -0
- package/docs/en/npm-release.md +25 -0
- package/docs/en/plugins.md +82 -0
- package/docs/en/recovery.md +28 -0
- package/docs/en/time.md +71 -0
- package/docs/en/webui.md +73 -0
- package/docs/zh/CHANGELOG.md +113 -0
- package/docs/zh/architecture.md +152 -0
- package/docs/zh/chat-style.md +62 -0
- package/docs/zh/connect.md +63 -0
- package/docs/zh/guide.md +216 -0
- package/docs/zh/her-life.md +189 -0
- package/docs/zh/npm-release.md +33 -0
- package/docs/zh/plugins.md +82 -0
- package/docs/zh/recovery.md +28 -0
- package/docs/zh/time.md +71 -0
- package/docs/zh/webui.md +73 -0
- package/package.json +108 -0
- package/plugins/sdk/index.d.ts +155 -0
- package/plugins/sdk/testing.js +58 -0
- package/plugins/weather/index.js +82 -0
- package/plugins/weather/luckytri-plugin.json +29 -0
- package/public/app/assets/ChatsPage-C-FcRapR.js +3 -0
- package/public/app/assets/ChatsPage-DfyPCS9N.css +1 -0
- package/public/app/assets/Empty-CpP5kQHH.js +2 -0
- package/public/app/assets/Empty-DmUB2-Bk.css +1 -0
- package/public/app/assets/Field-D4gY-INU.js +1 -0
- package/public/app/assets/HeartPage-Bft9wZp5.css +1 -0
- package/public/app/assets/HeartPage-CPy_thQd.js +1 -0
- package/public/app/assets/LifePage-BUi5euBF.css +1 -0
- package/public/app/assets/LifePage-CxWvwmSf.js +1 -0
- package/public/app/assets/MemoryPage-DC4rhQ68.js +1 -0
- package/public/app/assets/MemoryPage-cZvXfkXb.css +1 -0
- package/public/app/assets/NaturePage-CTXHCE2D.css +1 -0
- package/public/app/assets/NaturePage-IvYw04Zq.js +3 -0
- package/public/app/assets/PeoplePage-BmUxkrOU.css +1 -0
- package/public/app/assets/PeoplePage-Cqefm9-q.js +1 -0
- package/public/app/assets/PluginsPage-DKSaKvXb.js +2 -0
- package/public/app/assets/PluginsPage-HyBe5D8w.css +1 -0
- package/public/app/assets/SystemPage-CAMPoYqM.css +1 -0
- package/public/app/assets/SystemPage-CJmtJkrK.js +1 -0
- package/public/app/assets/TimePage-BOms_n4I.css +1 -0
- package/public/app/assets/TimePage-Cw44inM4.js +3 -0
- package/public/app/assets/formatters-ZmW_xFHw.js +1 -0
- package/public/app/assets/index-BheRW-1t.js +4 -0
- package/public/app/assets/index-YDz1ree1.css +1 -0
- package/public/app/assets/knowledge-DG0e0uKw.js +1 -0
- package/public/app/assets/liquid-garden-IMoHBwzc.png +0 -0
- package/public/app/assets/luckytri-home-95QKeMtc.png +0 -0
- package/public/app/assets/media-BzPDBm7e.js +1 -0
- package/public/app/index.html +27 -0
- package/public/guide.en.html +1053 -0
- package/public/guide.html +893 -0
- package/public/plugin-kit/v1/bridge.js +30 -0
- package/public/plugin-kit/v1/kit.css +14 -0
- package/public/tutorial.en.md +214 -0
- package/public/tutorial.md +214 -0
- package/scripts/backup.js +311 -0
- package/scripts/browser.js +41 -0
- package/scripts/launch.js +94 -0
- package/scripts/plugin.js +150 -0
- package/scripts/proxy-config.js +167 -0
- package/scripts/recover.js +26 -0
- package/scripts/runtime.js +25 -0
- package/scripts/service.js +47 -0
- package/scripts/setup.js +50 -0
- package/scripts/start-server.js +41 -0
- package/scripts/stop.js +36 -0
- package/server/app.js +96 -0
- package/server/channels/adapters.js +27 -0
- package/server/channels/contract.js +57 -0
- package/server/channels/hub.js +165 -0
- package/server/channels/index.js +25 -0
- package/server/channels/onebot/adapter.js +288 -0
- package/server/channels/onebot/index.js +336 -0
- package/server/channels/onebot/token.js +3 -0
- package/server/channels/qqbot/adapter.js +253 -0
- package/server/channels/qqbot/api.js +199 -0
- package/server/channels/qqbot/credentials.js +11 -0
- package/server/channels/qqbot/gateway.js +274 -0
- package/server/channels/qqbot/index.js +389 -0
- package/server/channels/segments.js +40 -0
- package/server/channels/session-key.js +153 -0
- package/server/channels/state.js +42 -0
- package/server/core/api.js +564 -0
- package/server/core/capabilities.js +208 -0
- package/server/core/context-builder.js +288 -0
- package/server/core/context-compactor.js +454 -0
- package/server/core/conversation-cues.js +325 -0
- package/server/core/conversation-manager.js +87 -0
- package/server/core/events.js +60 -0
- package/server/core/feedback.js +8 -0
- package/server/core/group-style.js +72 -0
- package/server/core/image-preprocessor.js +208 -0
- package/server/core/initiative-context.js +66 -0
- package/server/core/input-budget.js +32 -0
- package/server/core/local-demo.js +98 -0
- package/server/core/message-manager.js +118 -0
- package/server/core/message-scheduler.js +78 -0
- package/server/core/model-json.js +190 -0
- package/server/core/model-manager.js +1213 -0
- package/server/core/model-presets.js +900 -0
- package/server/core/name-call.js +29 -0
- package/server/core/net-guard.js +49 -0
- package/server/core/network.js +157 -0
- package/server/core/orchestrator.js +569 -0
- package/server/core/perceivers.js +91 -0
- package/server/core/person-name.js +22 -0
- package/server/core/persona-manager.js +243 -0
- package/server/core/reply-delivery.js +393 -0
- package/server/core/reply-target-resolver.js +50 -0
- package/server/core/repository.js +293 -0
- package/server/core/response-generator.js +50 -0
- package/server/core/response-validator.js +508 -0
- package/server/core/retired-prompts.js +51 -0
- package/server/core/sessions.js +109 -0
- package/server/core/speaker-names.js +92 -0
- package/server/core/topic-tracker.js +65 -0
- package/server/core/turn-processor.js +630 -0
- package/server/core/turn.js +236 -0
- package/server/core/vision-manager.js +633 -0
- package/server/http.js +18 -0
- package/server/i18n/decisions.js +206 -0
- package/server/i18n/errors.js +497 -0
- package/server/i18n/index.js +144 -0
- package/server/i18n/labels.js +653 -0
- package/server/i18n/models.js +111 -0
- package/server/index.js +102 -0
- package/server/knowledge/api.js +342 -0
- package/server/knowledge/embedding-profile.js +114 -0
- package/server/knowledge/index.js +13 -0
- package/server/knowledge/manager.js +386 -0
- package/server/knowledge/retrieval.js +93 -0
- package/server/knowledge/schema.js +154 -0
- package/server/knowledge/vector-index.js +90 -0
- package/server/mind/affect.js +265 -0
- package/server/mind/anticipations.js +507 -0
- package/server/mind/api.js +703 -0
- package/server/mind/attention.js +200 -0
- package/server/mind/bonds.js +558 -0
- package/server/mind/budget.js +152 -0
- package/server/mind/clock.js +83 -0
- package/server/mind/context-selection.js +61 -0
- package/server/mind/continuity.js +562 -0
- package/server/mind/conversation-origin.js +175 -0
- package/server/mind/day-planner.js +324 -0
- package/server/mind/days.js +210 -0
- package/server/mind/evidence-read.js +45 -0
- package/server/mind/evidence.js +148 -0
- package/server/mind/faces.js +138 -0
- package/server/mind/guard.js +60 -0
- package/server/mind/index.js +445 -0
- package/server/mind/initiative.js +333 -0
- package/server/mind/life/activities.js +21 -0
- package/server/mind/life/context.js +309 -0
- package/server/mind/life/diary.js +786 -0
- package/server/mind/life/growth.js +252 -0
- package/server/mind/life/index.js +1049 -0
- package/server/mind/life/presence.js +305 -0
- package/server/mind/meetings.js +1069 -0
- package/server/mind/memory-policy.js +48 -0
- package/server/mind/memory.js +888 -0
- package/server/mind/nature.js +262 -0
- package/server/mind/novelty.js +19 -0
- package/server/mind/observations.js +84 -0
- package/server/mind/own-day.js +324 -0
- package/server/mind/own-voice.js +318 -0
- package/server/mind/periods.js +118 -0
- package/server/mind/reading.js +120 -0
- package/server/mind/salience.js +201 -0
- package/server/mind/schema.js +562 -0
- package/server/mind/self.js +484 -0
- package/server/mind/senses.js +53 -0
- package/server/mind/thoughts.js +274 -0
- package/server/mind/time/activity-clock.js +196 -0
- package/server/mind/time/agenda.js +242 -0
- package/server/mind/time/api.js +200 -0
- package/server/mind/time/attention.js +34 -0
- package/server/mind/time/availability.js +69 -0
- package/server/mind/time/commitment-review.js +198 -0
- package/server/mind/time/delivery-links.js +130 -0
- package/server/mind/time/executor.js +512 -0
- package/server/mind/time/experiences.js +128 -0
- package/server/mind/time/games.js +562 -0
- package/server/mind/time/index.js +739 -0
- package/server/mind/time/intent.js +166 -0
- package/server/mind/time/kinds.js +171 -0
- package/server/mind/time/model-material.js +77 -0
- package/server/mind/time/plugin-activity.js +207 -0
- package/server/mind/time/presentation.js +51 -0
- package/server/mind/time/schedule.js +83 -0
- package/server/mind/time/schema.js +84 -0
- package/server/mind/time/search.js +383 -0
- package/server/mind/time/sharing.js +472 -0
- package/server/mind/time/task-links.js +311 -0
- package/server/mind/time/tasks.js +587 -0
- package/server/mind/time/works.js +432 -0
- package/server/mind/traits.js +191 -0
- package/server/mind/util.js +110 -0
- package/server/mind/view.js +273 -0
- package/server/paths.js +102 -0
- package/server/plugins/api.js +204 -0
- package/server/plugins/host.js +967 -0
- package/server/plugins/install.js +295 -0
- package/server/plugins/manifest.js +201 -0
- package/server/plugins/permissions.js +42 -0
- package/server/plugins/runner/guard.mjs +36 -0
- package/server/plugins/runner/worker.mjs +250 -0
- package/server/plugins/schema.js +91 -0
- package/server/plugins/zip.js +112 -0
- package/server/startup-lock.js +118 -0
- package/server/storage/archive.js +241 -0
- package/server/storage/backup-cleanup-worker.js +8 -0
- package/server/storage/backup-cleanup.js +414 -0
- package/server/storage/backup-lock.js +76 -0
- package/server/storage/backup-scheduler.js +131 -0
- package/server/storage/store.js +199 -0
- package/server/studio/display-names.js +138 -0
- package/server/studio/management.js +102 -0
- package/server/studio/readiness.js +190 -0
- package/server/studio/settings.js +236 -0
- package/server/version.js +10 -0
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { dayKey, zonedTime } from "../util.js";
|
|
2
|
+
import { localClock } from "../../core/conversation-cues.js";
|
|
3
|
+
import { gameText } from "./presentation.js";
|
|
4
|
+
export function spokenDate(words, at, zone) {
|
|
5
|
+
const absolute = String(words).match(/20\d{2}-\d{2}-\d{2}(?: \d{2}:\d{2})?/);
|
|
6
|
+
if (absolute)
|
|
7
|
+
return {
|
|
8
|
+
dueAt: zonedTime(absolute[0], zone),
|
|
9
|
+
duePrecision: absolute[0].includes(":") ? "time" : "day",
|
|
10
|
+
};
|
|
11
|
+
if (/后天|明天|明晚|今晚|今天/.test(words)) {
|
|
12
|
+
const offset = /后天/.test(words) ? 2 : /明天|明晚/.test(words) ? 1 : 0;
|
|
13
|
+
return {
|
|
14
|
+
dueAt: zonedTime(dayKey(at + offset * 86400000, zone) + " 23:59", zone),
|
|
15
|
+
duePrecision: "day",
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
return { dueAt: null, duePrecision: "none" };
|
|
19
|
+
}
|
|
20
|
+
export function nextAwakeSlot(mind, now) {
|
|
21
|
+
const zone = mind.timeZone(),
|
|
22
|
+
rhythm = mind.nature.current(now).rhythm;
|
|
23
|
+
let at = Math.ceil(now / 300000) * 300000;
|
|
24
|
+
if (rhythm?.enabled) {
|
|
25
|
+
const clock = localClock(now, zone),
|
|
26
|
+
wake = zonedTime(dayKey(now, zone) + " " + rhythm.wake, zone);
|
|
27
|
+
if (wake > now) at = wake;
|
|
28
|
+
else if (
|
|
29
|
+
mind.affect.state(now).phase === "asleep" ||
|
|
30
|
+
mind.affect.state(now).phase === "sleepy"
|
|
31
|
+
)
|
|
32
|
+
at = zonedTime(dayKey(now + 86400000, zone) + " " + rhythm.wake, zone);
|
|
33
|
+
}
|
|
34
|
+
return at;
|
|
35
|
+
}
|
|
36
|
+
export function deadlineAt(at, precision, zone) {
|
|
37
|
+
if (!Number.isFinite(at)) return null;
|
|
38
|
+
return precision === "day"
|
|
39
|
+
? zonedTime(dayKey(at, zone) + " 23:59", zone) + 59999
|
|
40
|
+
: at;
|
|
41
|
+
}
|
|
42
|
+
export function taskSchedule(task, time, now = time.now()) {
|
|
43
|
+
const plan = task.checkpoint?.schedule;
|
|
44
|
+
const waiting = task.state === "waiting";
|
|
45
|
+
const focus =
|
|
46
|
+
plan?.durationMinutes || plan?.focusMinutes || time.settings().focusMinutes;
|
|
47
|
+
const proposed =
|
|
48
|
+
plan?.chosenAt || plan?.proposedAt > now
|
|
49
|
+
? plan.proposedAt
|
|
50
|
+
: task.state === "scheduled" && task.ready_at > now
|
|
51
|
+
? task.ready_at
|
|
52
|
+
: null;
|
|
53
|
+
const live =
|
|
54
|
+
task.state === "doing"
|
|
55
|
+
? time.db
|
|
56
|
+
.prepare(
|
|
57
|
+
"SELECT started FROM mind_time_spans WHERE task_id=? AND ended IS NULL",
|
|
58
|
+
)
|
|
59
|
+
.get(task.id)
|
|
60
|
+
: null;
|
|
61
|
+
return {
|
|
62
|
+
earliestAt: task.ready_at,
|
|
63
|
+
proposedAt: proposed,
|
|
64
|
+
proposedEnd: proposed ? proposed + focus * 60000 : null,
|
|
65
|
+
startedAt: live?.started || null,
|
|
66
|
+
focusMinutes: focus,
|
|
67
|
+
chosenAt: plan?.chosenAt || null,
|
|
68
|
+
chosenBy: plan?.chosenBy || null,
|
|
69
|
+
durationMinutes: plan?.durationMinutes || null,
|
|
70
|
+
reason: plan?.reason || "",
|
|
71
|
+
conditional: waiting,
|
|
72
|
+
deadlinePrecision: task.due_precision || "time",
|
|
73
|
+
deadlineAt: task.due_at,
|
|
74
|
+
nextAttemptAt:
|
|
75
|
+
task.next_step > now && task.next_step < 8e15 ? task.next_step : null,
|
|
76
|
+
outcome:
|
|
77
|
+
task.activity === "game"
|
|
78
|
+
? gameText(plan?.outcome || task.checkpoint?.contract?.outcome || "")
|
|
79
|
+
: plan?.outcome || task.checkpoint?.contract?.outcome || "",
|
|
80
|
+
estimatedMs: task.checkpoint?.activityClock?.plannedMs || null,
|
|
81
|
+
condition: waiting ? task.wait_reason : "",
|
|
82
|
+
};
|
|
83
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
export function migrateTime(db) {
|
|
2
|
+
db.exec(`
|
|
3
|
+
CREATE TABLE IF NOT EXISTS mind_time_projects(id TEXT PRIMARY KEY,created INTEGER NOT NULL,updated INTEGER NOT NULL,kind TEXT NOT NULL,title TEXT NOT NULL,why TEXT NOT NULL DEFAULT '',state TEXT NOT NULL DEFAULT 'active',session_id TEXT,discretion TEXT NOT NULL DEFAULT 'open',sources TEXT NOT NULL DEFAULT '[]',bible TEXT NOT NULL DEFAULT '{}',summary TEXT NOT NULL DEFAULT '',serial INTEGER NOT NULL DEFAULT 0,revision INTEGER NOT NULL DEFAULT 1);
|
|
4
|
+
CREATE TABLE IF NOT EXISTS mind_time_tasks(id TEXT PRIMARY KEY,created INTEGER NOT NULL,updated INTEGER NOT NULL,kind TEXT NOT NULL,activity TEXT NOT NULL,title TEXT NOT NULL,why TEXT NOT NULL DEFAULT '',state TEXT NOT NULL DEFAULT 'todo',ready_at INTEGER NOT NULL,due_at INTEGER,session_id TEXT,subject TEXT,discretion TEXT NOT NULL DEFAULT 'open',sources TEXT NOT NULL DEFAULT '[]',anticipation_id TEXT UNIQUE,project_id TEXT,work_id TEXT,checkpoint TEXT NOT NULL DEFAULT '{}',revision INTEGER NOT NULL DEFAULT 1,next_step INTEGER NOT NULL DEFAULT 0,wait_reason TEXT NOT NULL DEFAULT '',share_state TEXT NOT NULL DEFAULT 'none',share_reason TEXT NOT NULL DEFAULT '',depends_on TEXT,lease TEXT,lease_at INTEGER,completed INTEGER);
|
|
5
|
+
CREATE INDEX IF NOT EXISTS mind_time_tasks_queue ON mind_time_tasks(state,ready_at,next_step);
|
|
6
|
+
CREATE UNIQUE INDEX IF NOT EXISTS mind_time_single_main ON mind_time_tasks((1)) WHERE state='doing';
|
|
7
|
+
CREATE TABLE IF NOT EXISTS mind_time_spans(id TEXT PRIMARY KEY,task_id TEXT NOT NULL,started INTEGER NOT NULL,updated INTEGER NOT NULL,ended INTEGER,active_ms INTEGER NOT NULL DEFAULT 0);
|
|
8
|
+
CREATE INDEX IF NOT EXISTS mind_time_spans_task ON mind_time_spans(task_id,started);
|
|
9
|
+
CREATE TABLE IF NOT EXISTS mind_time_events(id INTEGER PRIMARY KEY,created INTEGER NOT NULL,task_id TEXT,kind TEXT NOT NULL,reason TEXT NOT NULL DEFAULT '',data TEXT NOT NULL DEFAULT '{}');
|
|
10
|
+
CREATE INDEX IF NOT EXISTS mind_time_events_time ON mind_time_events(created);
|
|
11
|
+
CREATE TABLE IF NOT EXISTS mind_time_works(id TEXT PRIMARY KEY,created INTEGER NOT NULL,updated INTEGER NOT NULL,task_id TEXT,project_id TEXT,ordinal INTEGER NOT NULL DEFAULT 1,title TEXT NOT NULL,state TEXT NOT NULL DEFAULT 'draft',version INTEGER NOT NULL DEFAULT 0,session_id TEXT,discretion TEXT NOT NULL DEFAULT 'open',legacy_creation TEXT UNIQUE,UNIQUE(project_id,ordinal));
|
|
12
|
+
CREATE TABLE IF NOT EXISTS mind_time_versions(id TEXT PRIMARY KEY,work_id TEXT NOT NULL,version INTEGER NOT NULL,created INTEGER NOT NULL,title TEXT NOT NULL,content TEXT NOT NULL,summary TEXT NOT NULL DEFAULT '',sources TEXT NOT NULL DEFAULT '[]',run_id TEXT,UNIQUE(work_id,version));
|
|
13
|
+
CREATE INDEX IF NOT EXISTS mind_time_versions_work ON mind_time_versions(work_id,version);
|
|
14
|
+
CREATE INDEX IF NOT EXISTS mind_time_works_recent ON mind_time_works(updated DESC);
|
|
15
|
+
CREATE INDEX IF NOT EXISTS mind_time_works_task ON mind_time_works(task_id);
|
|
16
|
+
CREATE INDEX IF NOT EXISTS mind_time_projects_recent ON mind_time_projects(updated DESC);
|
|
17
|
+
CREATE TABLE IF NOT EXISTS mind_time_sources(id TEXT PRIMARY KEY,project_id TEXT NOT NULL,created INTEGER NOT NULL,title TEXT NOT NULL,url TEXT NOT NULL,content TEXT NOT NULL,hash TEXT NOT NULL,UNIQUE(project_id,url,hash));
|
|
18
|
+
CREATE TABLE IF NOT EXISTS mind_time_searches(id INTEGER PRIMARY KEY,created INTEGER NOT NULL,provider TEXT NOT NULL,query TEXT NOT NULL,state TEXT NOT NULL,error TEXT NOT NULL DEFAULT '');
|
|
19
|
+
CREATE TABLE IF NOT EXISTS mind_time_shares(id TEXT PRIMARY KEY,created INTEGER NOT NULL,updated INTEGER NOT NULL,work_id TEXT NOT NULL,version INTEGER NOT NULL,session_id TEXT NOT NULL,offset INTEGER NOT NULL DEFAULT 0,state TEXT NOT NULL DEFAULT 'pending',reason TEXT NOT NULL DEFAULT '',trace_id TEXT,UNIQUE(work_id,version,session_id));
|
|
20
|
+
CREATE TABLE IF NOT EXISTS mind_time_task_aliases(kind TEXT NOT NULL,alias_id TEXT NOT NULL,task_id TEXT NOT NULL,created INTEGER NOT NULL,PRIMARY KEY(kind,alias_id));
|
|
21
|
+
CREATE INDEX IF NOT EXISTS mind_time_task_aliases_task ON mind_time_task_aliases(task_id);
|
|
22
|
+
CREATE TABLE IF NOT EXISTS mind_time_activity_ticks(span_id TEXT NOT NULL,started INTEGER NOT NULL,ended INTEGER NOT NULL,PRIMARY KEY(span_id,ended));
|
|
23
|
+
CREATE INDEX IF NOT EXISTS mind_time_activity_ticks_time ON mind_time_activity_ticks(ended,started);
|
|
24
|
+
CREATE TABLE IF NOT EXISTS mind_time_commitment_reviews(seq INTEGER PRIMARY KEY,reviewed INTEGER NOT NULL,task_ids TEXT NOT NULL DEFAULT '[]');
|
|
25
|
+
CREATE INDEX IF NOT EXISTS mind_time_assistant_review ON core_events(seq,time,session_id) WHERE role='assistant';
|
|
26
|
+
CREATE INDEX IF NOT EXISTS mind_time_source_lookup ON mind_time_task_aliases(kind,substr(alias_id,instr(alias_id,':')+1));
|
|
27
|
+
`);
|
|
28
|
+
const columns = new Set(
|
|
29
|
+
db
|
|
30
|
+
.prepare("PRAGMA table_info(mind_time_tasks)")
|
|
31
|
+
.all()
|
|
32
|
+
.map((c) => c.name),
|
|
33
|
+
);
|
|
34
|
+
if (!columns.has("intent_key"))
|
|
35
|
+
db.exec("ALTER TABLE mind_time_tasks ADD COLUMN intent_key TEXT");
|
|
36
|
+
if (!columns.has("due_precision"))
|
|
37
|
+
db.exec(
|
|
38
|
+
"ALTER TABLE mind_time_tasks ADD COLUMN due_precision TEXT NOT NULL DEFAULT 'time'",
|
|
39
|
+
);
|
|
40
|
+
if (!columns.has("priority"))
|
|
41
|
+
db.exec(
|
|
42
|
+
"ALTER TABLE mind_time_tasks ADD COLUMN priority INTEGER NOT NULL DEFAULT 1 CHECK(priority BETWEEN 0 AND 3); UPDATE mind_time_tasks SET priority=2 WHERE kind='promise';",
|
|
43
|
+
);
|
|
44
|
+
db.exec(
|
|
45
|
+
"CREATE INDEX IF NOT EXISTS mind_time_tasks_intent ON mind_time_tasks(intent_key);",
|
|
46
|
+
);
|
|
47
|
+
const sourceColumns = new Set(
|
|
48
|
+
db
|
|
49
|
+
.prepare("PRAGMA table_info(mind_time_sources)")
|
|
50
|
+
.all()
|
|
51
|
+
.map((c) => c.name),
|
|
52
|
+
);
|
|
53
|
+
for (const [name, fallback] of [
|
|
54
|
+
["kind", "web"],
|
|
55
|
+
["model", ""],
|
|
56
|
+
["uncertainty", ""],
|
|
57
|
+
])
|
|
58
|
+
if (!sourceColumns.has(name))
|
|
59
|
+
db.exec(
|
|
60
|
+
`ALTER TABLE mind_time_sources ADD COLUMN ${name} TEXT NOT NULL DEFAULT '${fallback}'`,
|
|
61
|
+
);
|
|
62
|
+
for (const [table, column, type, fallback] of [
|
|
63
|
+
["mind_time_sources", "timing", "TEXT", "'{}'"],
|
|
64
|
+
["mind_time_versions", "provenance", "TEXT", "'{}'"],
|
|
65
|
+
["mind_time_spans", "engaged_ms", "INTEGER", "0"],
|
|
66
|
+
["mind_time_spans", "clocked", "INTEGER", "0"],
|
|
67
|
+
["mind_time_shares", "review_offset", "INTEGER", "0"],
|
|
68
|
+
["mind_time_shares", "review_notes", "TEXT", "'[]'"],
|
|
69
|
+
["mind_time_shares", "report", "TEXT", "'[]'"],
|
|
70
|
+
["mind_time_shares", "send_index", "INTEGER", "0"],
|
|
71
|
+
["mind_time_shares", "delivery_kind", "TEXT", "'body'"],
|
|
72
|
+
["mind_time_shares", "next_step", "INTEGER", "0"],
|
|
73
|
+
["mind_time_searches", "purpose", "TEXT", "'activity'"],
|
|
74
|
+
])
|
|
75
|
+
if (
|
|
76
|
+
!db
|
|
77
|
+
.prepare(`PRAGMA table_info(${table})`)
|
|
78
|
+
.all()
|
|
79
|
+
.some((c) => c.name === column)
|
|
80
|
+
)
|
|
81
|
+
db.exec(
|
|
82
|
+
`ALTER TABLE ${table} ADD COLUMN ${column} ${type} NOT NULL DEFAULT ${fallback}`,
|
|
83
|
+
);
|
|
84
|
+
}
|
|
@@ -0,0 +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
|
+
}
|