@steerable/agent-shell 0.6.21 → 0.6.24
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +34 -0
- package/dist/auth/index.d.ts +38 -0
- package/dist/auth/index.js +20 -0
- package/dist/host/pack-assembly.d.ts +7 -7
- package/dist/host/runtime.d.ts +6 -2
- package/dist/host/runtime.js +54 -14
- package/dist/insights/flush.d.ts +5 -4
- package/dist/insights/flush.js +14 -15
- package/dist/insights/record.d.ts +6 -5
- package/dist/insights/record.js +9 -10
- package/dist/llm/index.d.ts +6 -2
- package/dist/llm/index.js +19 -9
- package/dist/local-backend/prompt-builder.d.ts +10 -4
- package/dist/local-backend/prompt-builder.js +13 -12
- package/dist/local-backend/router.d.ts +18 -4
- package/dist/local-backend/router.js +133 -98
- package/dist/local-backend/skills/00-identity/SKILL.md +1 -1
- package/dist/local-backend/task-service.d.ts +14 -13
- package/dist/local-backend/task-service.js +32 -29
- package/dist/local-backend/worktree-service.d.ts +1 -1
- package/dist/local-backend/worktree-service.js +6 -6
- package/dist/main.js +43 -26
- package/dist/server/http-server.d.ts +13 -1
- package/dist/server/http-server.js +37 -8
- package/dist/server/index.js +4 -90
- package/dist/server/start.d.ts +26 -0
- package/dist/server/start.js +111 -0
- package/dist/sidecar/boot.d.ts +3 -1
- package/dist/sidecar/boot.js +6 -7
- package/dist/sidecar/reverse-spawn.js +12 -4
- package/dist/sidecar/reverse-tools.d.ts +1 -1
- package/dist/sidecar/reverse-tools.js +1 -1
- package/dist/sidecar/storage-path.js +2 -3
- package/dist/storage/driver.d.ts +57 -0
- package/dist/storage/driver.js +104 -0
- package/dist/storage/index.d.ts +75 -68
- package/dist/storage/index.js +506 -301
- package/dist/storage/pack-migrations.d.ts +2 -4
- package/dist/storage/pack-migrations.js +2 -4
- package/dist/storage/scoped-store.d.ts +125 -0
- package/dist/storage/scoped-store.js +1 -0
- package/dist/storage/sqlite-driver.d.ts +16 -0
- package/dist/storage/sqlite-driver.js +135 -0
- package/dist/storage/write-lease-error.d.ts +11 -0
- package/dist/storage/write-lease-error.js +18 -0
- package/package.json +14 -5
- package/dist/storage/local-store-singleton.d.ts +0 -28
- package/dist/storage/local-store-singleton.js +0 -38
package/dist/storage/index.js
CHANGED
|
@@ -1,17 +1,10 @@
|
|
|
1
1
|
import { randomUUID } from 'crypto';
|
|
2
|
-
import path from 'path';
|
|
3
|
-
import Database from 'better-sqlite3';
|
|
4
|
-
import { getUserDataDir } from '../runtime.js';
|
|
5
|
-
import { getProductConfig } from '../product-config.js';
|
|
6
2
|
import { ALL_ROUND_ASSISTANT_AGENT_ID, LOCAL_ASSISTANT_AGENT_ID, getBrand, } from '../brand.js';
|
|
7
3
|
import { DEFAULT_LLM_SETTINGS, llmSettingsCarryExpiredBakedKey, llmSettingsMatchOllamaDefaults, mergeLlmSettings, } from './llm-settings.js';
|
|
8
|
-
import {
|
|
9
|
-
import { LIST_EMPTY_CHAT_IDS_SQL } from './empty-chats.js';
|
|
4
|
+
import { MESSAGE_ORDER_DESC } from './message-order.js';
|
|
10
5
|
import { mergeTelemetrySettings, } from './telemetry-settings.js';
|
|
11
6
|
import { mergeWebSearchSettings, } from './web-search-settings.js';
|
|
12
7
|
import { mergeInsightsProfile, mergeInsightsSettings, } from './insights-settings.js';
|
|
13
|
-
import { acquireWriteLease, lockPathForDb } from './write-lease.js';
|
|
14
|
-
import { createLocalStore } from './local-store-singleton.js';
|
|
15
8
|
import { getPackMigrations } from './pack-migrations.js';
|
|
16
9
|
import { getPackAgentSeeds, isUntouchedSeed, matchesPreviousIdentity, } from './pack-seeds.js';
|
|
17
10
|
import { buildUsageSummary, } from './usage-summary.js';
|
|
@@ -38,31 +31,89 @@ const DEFAULT_LOCAL_USER_ID = 'local';
|
|
|
38
31
|
// 线协议常量:insights 导出包 schema 名由遥测接收端(产品注入的
|
|
39
32
|
// insightsApiBase 对端)定义,改名会破坏对端解析。
|
|
40
33
|
const INSIGHTS_EXPORT_SCHEMA = 'deeppath-agent-insights/v1'; // shell-neutral:allow(线协议常量,见上行)
|
|
41
|
-
export class
|
|
34
|
+
export class SqliteScopedStore {
|
|
42
35
|
db;
|
|
43
|
-
|
|
44
|
-
constructor() {
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
this.
|
|
51
|
-
// The schema declares `ON DELETE CASCADE` foreign keys (chat_messages,
|
|
52
|
-
// harness_traces -> chat_sessions), but SQLite ignores FK constraints
|
|
53
|
-
// unless explicitly enabled per-connection. Without this, deleteChat()
|
|
54
|
-
// only removed the session row and left orphaned messages/traces behind.
|
|
55
|
-
this.db.pragma('foreign_keys = ON');
|
|
56
|
-
this.migrate();
|
|
36
|
+
scope;
|
|
37
|
+
constructor(db, scope) {
|
|
38
|
+
this.db = db;
|
|
39
|
+
this.scope = scope;
|
|
40
|
+
}
|
|
41
|
+
/** Initializes schema, cleanup, migrations, and seeds for this scope. */
|
|
42
|
+
async initialize() {
|
|
43
|
+
this.migrateScopedSchema();
|
|
57
44
|
this.cleanupOrphanedRows();
|
|
58
|
-
this.seedDefaults();
|
|
45
|
+
await this.seedDefaults();
|
|
46
|
+
}
|
|
47
|
+
migrateScopedSchema() {
|
|
48
|
+
const tables = [
|
|
49
|
+
'chat_sessions',
|
|
50
|
+
'chat_messages',
|
|
51
|
+
'chat_agents',
|
|
52
|
+
'settings_kv',
|
|
53
|
+
'harness_traces',
|
|
54
|
+
'usage_events',
|
|
55
|
+
'insights_outbox',
|
|
56
|
+
'tasks',
|
|
57
|
+
];
|
|
58
|
+
const existing = tables.filter((table) => this.tableColumns(table).length > 0);
|
|
59
|
+
const requiresRebuild = existing.some((table) => {
|
|
60
|
+
const columns = this.tableColumns(table);
|
|
61
|
+
const primaryKey = columns
|
|
62
|
+
.filter((column) => column.pk > 0)
|
|
63
|
+
.sort((a, b) => a.pk - b.pk)
|
|
64
|
+
.map((column) => column.name);
|
|
65
|
+
return !columns.some((column) => column.name === 'tenant_id')
|
|
66
|
+
|| !columns.some((column) => column.name === 'user_id')
|
|
67
|
+
|| primaryKey[0] !== 'tenant_id'
|
|
68
|
+
|| primaryKey[1] !== 'user_id';
|
|
69
|
+
});
|
|
70
|
+
if (!requiresRebuild) {
|
|
71
|
+
this.migrate();
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
this.db.pragma('foreign_keys = OFF');
|
|
75
|
+
this.db.exec('BEGIN IMMEDIATE');
|
|
76
|
+
try {
|
|
77
|
+
for (const table of existing) {
|
|
78
|
+
this.db.exec(`ALTER TABLE ${table} RENAME TO ${table}__scope_legacy`);
|
|
79
|
+
}
|
|
80
|
+
this.migrate();
|
|
81
|
+
for (const table of existing)
|
|
82
|
+
this.copyLegacyScopeRows(table);
|
|
83
|
+
for (const table of [...existing].reverse()) {
|
|
84
|
+
this.db.exec(`DROP TABLE ${table}__scope_legacy`);
|
|
85
|
+
}
|
|
86
|
+
// Legacy index names disappear with their tables; recreate them.
|
|
87
|
+
this.migrate();
|
|
88
|
+
this.db.exec('COMMIT');
|
|
89
|
+
}
|
|
90
|
+
catch (error) {
|
|
91
|
+
this.db.exec('ROLLBACK');
|
|
92
|
+
throw error;
|
|
93
|
+
}
|
|
94
|
+
finally {
|
|
95
|
+
this.db.pragma('foreign_keys = ON');
|
|
96
|
+
}
|
|
59
97
|
}
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
98
|
+
copyLegacyScopeRows(table) {
|
|
99
|
+
const legacy = `${table}__scope_legacy`;
|
|
100
|
+
const oldColumns = this.tableColumns(legacy);
|
|
101
|
+
const newNames = new Set(this.tableColumns(table).map((column) => column.name));
|
|
102
|
+
const oldNames = new Set(oldColumns.map((column) => column.name));
|
|
103
|
+
const copied = oldColumns
|
|
104
|
+
.map((column) => column.name)
|
|
105
|
+
.filter((name) => name !== 'tenant_id' && name !== 'user_id' && newNames.has(name));
|
|
106
|
+
const target = ['tenant_id', 'user_id', ...copied];
|
|
107
|
+
const select = [
|
|
108
|
+
oldNames.has('tenant_id') ? `COALESCE(tenant_id, 'local')` : `'local'`,
|
|
109
|
+
oldNames.has('user_id') ? `COALESCE(user_id, 'local')` : `'local'`,
|
|
110
|
+
...copied,
|
|
111
|
+
];
|
|
112
|
+
this.db.exec(`INSERT OR IGNORE INTO ${table} (${target.join(', ')})
|
|
113
|
+
SELECT ${select.join(', ')} FROM ${legacy}`);
|
|
114
|
+
}
|
|
115
|
+
tableColumns(table) {
|
|
116
|
+
return this.db.prepare(`PRAGMA table_info(${table})`).all();
|
|
66
117
|
}
|
|
67
118
|
/**
|
|
68
119
|
* One-time cleanup for rows that became orphaned while `foreign_keys` was
|
|
@@ -70,41 +121,95 @@ export class LocalStore {
|
|
|
70
121
|
* chat_messages/harness_traces behind for deleted chat_sessions).
|
|
71
122
|
*/
|
|
72
123
|
cleanupOrphanedRows() {
|
|
124
|
+
for (const table of [
|
|
125
|
+
'chat_sessions',
|
|
126
|
+
'chat_messages',
|
|
127
|
+
'chat_agents',
|
|
128
|
+
'settings_kv',
|
|
129
|
+
'harness_traces',
|
|
130
|
+
'usage_events',
|
|
131
|
+
'insights_outbox',
|
|
132
|
+
'tasks',
|
|
133
|
+
]) {
|
|
134
|
+
this.ensureColumn(table, 'tenant_id', `TEXT NOT NULL DEFAULT 'local'`);
|
|
135
|
+
this.ensureColumn(table, 'user_id', `TEXT NOT NULL DEFAULT 'local'`);
|
|
136
|
+
}
|
|
73
137
|
this.db.exec(`
|
|
74
138
|
DELETE FROM chat_messages
|
|
75
|
-
WHERE
|
|
139
|
+
WHERE NOT EXISTS (
|
|
140
|
+
SELECT 1 FROM chat_sessions
|
|
141
|
+
WHERE chat_sessions.tenant_id = chat_messages.tenant_id
|
|
142
|
+
AND chat_sessions.user_id = chat_messages.user_id
|
|
143
|
+
AND chat_sessions.id = chat_messages.chat_id
|
|
144
|
+
);
|
|
76
145
|
DELETE FROM harness_traces
|
|
77
|
-
WHERE
|
|
146
|
+
WHERE NOT EXISTS (
|
|
147
|
+
SELECT 1 FROM chat_sessions
|
|
148
|
+
WHERE chat_sessions.tenant_id = harness_traces.tenant_id
|
|
149
|
+
AND chat_sessions.user_id = harness_traces.user_id
|
|
150
|
+
AND chat_sessions.id = harness_traces.chat_id
|
|
151
|
+
);
|
|
78
152
|
DELETE FROM tasks
|
|
79
|
-
WHERE
|
|
153
|
+
WHERE NOT EXISTS (
|
|
154
|
+
SELECT 1 FROM chat_sessions
|
|
155
|
+
WHERE chat_sessions.tenant_id = tasks.tenant_id
|
|
156
|
+
AND chat_sessions.user_id = tasks.user_id
|
|
157
|
+
AND chat_sessions.id = tasks.chat_id
|
|
158
|
+
);
|
|
159
|
+
`);
|
|
160
|
+
this.db.exec(`
|
|
161
|
+
CREATE INDEX IF NOT EXISTS idx_chat_sessions_scope
|
|
162
|
+
ON chat_sessions(tenant_id, user_id, updated_at DESC);
|
|
163
|
+
CREATE INDEX IF NOT EXISTS idx_chat_messages_scope
|
|
164
|
+
ON chat_messages(tenant_id, user_id, chat_id, created_at);
|
|
165
|
+
CREATE INDEX IF NOT EXISTS idx_chat_agents_scope
|
|
166
|
+
ON chat_agents(tenant_id, user_id, sort_order);
|
|
167
|
+
CREATE INDEX IF NOT EXISTS idx_settings_scope
|
|
168
|
+
ON settings_kv(tenant_id, user_id, key);
|
|
169
|
+
CREATE INDEX IF NOT EXISTS idx_traces_scope
|
|
170
|
+
ON harness_traces(tenant_id, user_id, id);
|
|
171
|
+
CREATE INDEX IF NOT EXISTS idx_usage_scope
|
|
172
|
+
ON usage_events(tenant_id, user_id, created_at DESC);
|
|
173
|
+
CREATE INDEX IF NOT EXISTS idx_insights_scope
|
|
174
|
+
ON insights_outbox(tenant_id, user_id, created_at DESC);
|
|
175
|
+
CREATE INDEX IF NOT EXISTS idx_tasks_scope
|
|
176
|
+
ON tasks(tenant_id, user_id, updated_at DESC);
|
|
80
177
|
`);
|
|
81
178
|
}
|
|
82
179
|
migrate() {
|
|
83
180
|
this.db.exec(`
|
|
84
181
|
CREATE TABLE IF NOT EXISTS chat_sessions (
|
|
85
|
-
|
|
86
|
-
title TEXT NOT NULL,
|
|
182
|
+
tenant_id TEXT NOT NULL,
|
|
87
183
|
user_id TEXT NOT NULL,
|
|
184
|
+
id TEXT NOT NULL,
|
|
185
|
+
title TEXT NOT NULL,
|
|
88
186
|
agent_id TEXT,
|
|
89
187
|
is_pinned INTEGER NOT NULL DEFAULT 0,
|
|
90
188
|
system_prompt TEXT,
|
|
91
189
|
pinned_refs TEXT,
|
|
92
190
|
created_at TEXT NOT NULL,
|
|
93
|
-
updated_at TEXT NOT NULL
|
|
191
|
+
updated_at TEXT NOT NULL,
|
|
192
|
+
PRIMARY KEY (tenant_id, user_id, id)
|
|
94
193
|
);
|
|
95
194
|
|
|
96
195
|
CREATE TABLE IF NOT EXISTS chat_messages (
|
|
97
|
-
|
|
196
|
+
tenant_id TEXT NOT NULL,
|
|
197
|
+
user_id TEXT NOT NULL,
|
|
198
|
+
id TEXT NOT NULL,
|
|
98
199
|
chat_id TEXT NOT NULL,
|
|
99
200
|
role TEXT NOT NULL,
|
|
100
201
|
content TEXT NOT NULL,
|
|
101
202
|
message_metadata TEXT,
|
|
102
203
|
created_at TEXT NOT NULL,
|
|
103
|
-
|
|
204
|
+
PRIMARY KEY (tenant_id, user_id, id),
|
|
205
|
+
FOREIGN KEY(tenant_id, user_id, chat_id)
|
|
206
|
+
REFERENCES chat_sessions(tenant_id, user_id, id) ON DELETE CASCADE
|
|
104
207
|
);
|
|
105
208
|
|
|
106
209
|
CREATE TABLE IF NOT EXISTS chat_agents (
|
|
107
|
-
|
|
210
|
+
tenant_id TEXT NOT NULL,
|
|
211
|
+
user_id TEXT NOT NULL,
|
|
212
|
+
id TEXT NOT NULL,
|
|
108
213
|
slug TEXT,
|
|
109
214
|
name TEXT NOT NULL,
|
|
110
215
|
icon TEXT,
|
|
@@ -119,16 +224,22 @@ export class LocalStore {
|
|
|
119
224
|
is_archived INTEGER NOT NULL DEFAULT 0,
|
|
120
225
|
sort_order INTEGER NOT NULL DEFAULT 0,
|
|
121
226
|
created_at TEXT NOT NULL,
|
|
122
|
-
updated_at TEXT NOT NULL
|
|
227
|
+
updated_at TEXT NOT NULL,
|
|
228
|
+
PRIMARY KEY (tenant_id, user_id, id)
|
|
123
229
|
);
|
|
124
230
|
|
|
125
231
|
CREATE TABLE IF NOT EXISTS settings_kv (
|
|
126
|
-
|
|
127
|
-
|
|
232
|
+
tenant_id TEXT NOT NULL,
|
|
233
|
+
user_id TEXT NOT NULL,
|
|
234
|
+
key TEXT NOT NULL,
|
|
235
|
+
value TEXT NOT NULL,
|
|
236
|
+
PRIMARY KEY (tenant_id, user_id, key)
|
|
128
237
|
);
|
|
129
238
|
|
|
130
239
|
CREATE TABLE IF NOT EXISTS harness_traces (
|
|
131
|
-
|
|
240
|
+
tenant_id TEXT NOT NULL,
|
|
241
|
+
user_id TEXT NOT NULL,
|
|
242
|
+
id TEXT NOT NULL,
|
|
132
243
|
chat_id TEXT NOT NULL,
|
|
133
244
|
message_id TEXT,
|
|
134
245
|
started_at_ms INTEGER NOT NULL,
|
|
@@ -136,13 +247,17 @@ export class LocalStore {
|
|
|
136
247
|
status TEXT NOT NULL,
|
|
137
248
|
payload TEXT NOT NULL,
|
|
138
249
|
created_at TEXT NOT NULL,
|
|
139
|
-
|
|
250
|
+
PRIMARY KEY (tenant_id, user_id, id),
|
|
251
|
+
FOREIGN KEY(tenant_id, user_id, chat_id)
|
|
252
|
+
REFERENCES chat_sessions(tenant_id, user_id, id) ON DELETE CASCADE
|
|
140
253
|
);
|
|
141
254
|
|
|
142
255
|
CREATE INDEX IF NOT EXISTS idx_harness_traces_chat ON harness_traces(chat_id, started_at_ms DESC);
|
|
143
256
|
|
|
144
257
|
CREATE TABLE IF NOT EXISTS usage_events (
|
|
145
|
-
|
|
258
|
+
tenant_id TEXT NOT NULL,
|
|
259
|
+
user_id TEXT NOT NULL,
|
|
260
|
+
id TEXT NOT NULL,
|
|
146
261
|
chat_id TEXT,
|
|
147
262
|
kind TEXT NOT NULL,
|
|
148
263
|
provider TEXT,
|
|
@@ -152,24 +267,30 @@ export class LocalStore {
|
|
|
152
267
|
total_tokens INTEGER NOT NULL DEFAULT 0,
|
|
153
268
|
cached_prompt_tokens INTEGER NOT NULL DEFAULT 0,
|
|
154
269
|
cost_usd REAL,
|
|
155
|
-
created_at TEXT NOT NULL
|
|
270
|
+
created_at TEXT NOT NULL,
|
|
271
|
+
PRIMARY KEY (tenant_id, user_id, id)
|
|
156
272
|
);
|
|
157
273
|
|
|
158
274
|
CREATE INDEX IF NOT EXISTS idx_usage_events_created ON usage_events(created_at DESC);
|
|
159
275
|
|
|
160
276
|
CREATE TABLE IF NOT EXISTS insights_outbox (
|
|
161
|
-
|
|
277
|
+
tenant_id TEXT NOT NULL,
|
|
278
|
+
user_id TEXT NOT NULL,
|
|
279
|
+
id TEXT NOT NULL,
|
|
162
280
|
kind TEXT NOT NULL,
|
|
163
281
|
payload TEXT NOT NULL,
|
|
164
282
|
created_at TEXT NOT NULL,
|
|
165
283
|
uploaded_at TEXT,
|
|
166
|
-
upload_error TEXT
|
|
284
|
+
upload_error TEXT,
|
|
285
|
+
PRIMARY KEY (tenant_id, user_id, id)
|
|
167
286
|
);
|
|
168
287
|
|
|
169
288
|
CREATE INDEX IF NOT EXISTS idx_insights_outbox_pending ON insights_outbox(kind, uploaded_at, created_at);
|
|
170
289
|
|
|
171
290
|
CREATE TABLE IF NOT EXISTS tasks (
|
|
172
|
-
|
|
291
|
+
tenant_id TEXT NOT NULL,
|
|
292
|
+
user_id TEXT NOT NULL,
|
|
293
|
+
id TEXT NOT NULL,
|
|
173
294
|
chat_id TEXT NOT NULL,
|
|
174
295
|
task TEXT NOT NULL,
|
|
175
296
|
status TEXT NOT NULL,
|
|
@@ -182,7 +303,9 @@ export class LocalStore {
|
|
|
182
303
|
trace_id TEXT,
|
|
183
304
|
created_at TEXT NOT NULL,
|
|
184
305
|
updated_at TEXT NOT NULL,
|
|
185
|
-
|
|
306
|
+
PRIMARY KEY (tenant_id, user_id, id),
|
|
307
|
+
FOREIGN KEY(tenant_id, user_id, chat_id)
|
|
308
|
+
REFERENCES chat_sessions(tenant_id, user_id, id) ON DELETE CASCADE
|
|
186
309
|
);
|
|
187
310
|
|
|
188
311
|
CREATE INDEX IF NOT EXISTS idx_tasks_chat ON tasks(chat_id, datetime(updated_at) DESC);
|
|
@@ -207,29 +330,44 @@ export class LocalStore {
|
|
|
207
330
|
// 由用户在智能体管理页自由开关(不会每次启动被重置)。
|
|
208
331
|
if (this.ensureColumn('chat_agents', 'load_all_skills', 'INTEGER NOT NULL DEFAULT 0')) {
|
|
209
332
|
this.db
|
|
210
|
-
.prepare(`UPDATE chat_agents SET load_all_skills = 1
|
|
211
|
-
|
|
333
|
+
.prepare(`UPDATE chat_agents SET load_all_skills = 1
|
|
334
|
+
WHERE tenant_id = ? AND user_id = ? AND id = ?`)
|
|
335
|
+
.run(this.scope.tenantId, this.scope.userId, ALL_ROUND_ASSISTANT_AGENT_ID);
|
|
212
336
|
}
|
|
213
|
-
// 场景包迁移(0.3b):构造时应用一次已注册的包迁移(兼容注册先于
|
|
214
|
-
// 构造的路径);注册晚于构造的容错路径见 applyRegisteredPackMigrations
|
|
215
|
-
// 模块级函数(host/runtime 在包装配前调用)。
|
|
216
|
-
this.applyPackMigrations();
|
|
217
337
|
}
|
|
218
|
-
appliedPackMigrations = new Set();
|
|
219
338
|
/**
|
|
220
|
-
*
|
|
339
|
+
* Applies registered migrations once per driver-persisted pack generation.
|
|
221
340
|
*/
|
|
222
341
|
applyPackMigrations() {
|
|
342
|
+
this.db.exec(`
|
|
343
|
+
CREATE TABLE IF NOT EXISTS storage_pack_migrations (
|
|
344
|
+
pack_id TEXT PRIMARY KEY,
|
|
345
|
+
version INTEGER NOT NULL
|
|
346
|
+
)
|
|
347
|
+
`);
|
|
223
348
|
for (const { packId, migration } of getPackMigrations()) {
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
if (
|
|
227
|
-
|
|
228
|
-
}
|
|
229
|
-
for (const col of migration.ensureColumns ?? []) {
|
|
230
|
-
this.ensureColumn(col.table, col.column, col.type);
|
|
349
|
+
const version = migration.version ?? 1;
|
|
350
|
+
const applied = this.db.prepare(`SELECT version FROM storage_pack_migrations WHERE pack_id = ?`).get(packId);
|
|
351
|
+
if (applied && applied.version > version) {
|
|
352
|
+
throw new Error(`[storage] pack ${packId} schema ${applied.version} is newer than supported ${version}`);
|
|
231
353
|
}
|
|
232
|
-
|
|
354
|
+
if (applied?.version === version)
|
|
355
|
+
continue;
|
|
356
|
+
this.db.transaction(() => {
|
|
357
|
+
if (migration.beforeDdl && migration.beforeDdl.length > 0) {
|
|
358
|
+
this.db.exec(migration.beforeDdl.join(';\n'));
|
|
359
|
+
}
|
|
360
|
+
for (const col of migration.ensureColumns ?? []) {
|
|
361
|
+
this.ensureColumn(col.table, col.column, col.type);
|
|
362
|
+
}
|
|
363
|
+
if (migration.ddl && migration.ddl.length > 0) {
|
|
364
|
+
this.db.exec(migration.ddl.join(';\n'));
|
|
365
|
+
}
|
|
366
|
+
this.db.prepare(`
|
|
367
|
+
INSERT INTO storage_pack_migrations (pack_id, version) VALUES (?, ?)
|
|
368
|
+
ON CONFLICT(pack_id) DO UPDATE SET version = excluded.version
|
|
369
|
+
`).run(packId, version);
|
|
370
|
+
})();
|
|
233
371
|
}
|
|
234
372
|
}
|
|
235
373
|
/**
|
|
@@ -239,18 +377,22 @@ export class LocalStore {
|
|
|
239
377
|
*/
|
|
240
378
|
applyPackAgentSeed(seed, active, insertAgent, now) {
|
|
241
379
|
const row = this.db
|
|
242
|
-
.prepare(`SELECT id, name, description, role_prompt, is_archived FROM chat_agents
|
|
243
|
-
|
|
380
|
+
.prepare(`SELECT id, name, description, role_prompt, is_archived FROM chat_agents
|
|
381
|
+
WHERE tenant_id = ? AND user_id = ? AND id = ?`)
|
|
382
|
+
.get(this.scope.tenantId, this.scope.userId, seed.id);
|
|
244
383
|
if (!active) {
|
|
245
384
|
if (row && row.is_archived === 0 && isUntouchedSeed(seed, row)) {
|
|
246
385
|
this.db
|
|
247
|
-
.prepare(`UPDATE chat_agents SET is_archived = 1, updated_at = ?
|
|
248
|
-
|
|
386
|
+
.prepare(`UPDATE chat_agents SET is_archived = 1, updated_at = ?
|
|
387
|
+
WHERE tenant_id = ? AND user_id = ? AND id = ?`)
|
|
388
|
+
.run(now, this.scope.tenantId, this.scope.userId, seed.id);
|
|
249
389
|
}
|
|
250
390
|
return;
|
|
251
391
|
}
|
|
252
392
|
if (!row) {
|
|
253
393
|
insertAgent.run({
|
|
394
|
+
tenantId: this.scope.tenantId,
|
|
395
|
+
userId: this.scope.userId,
|
|
254
396
|
id: seed.id,
|
|
255
397
|
slug: seed.slug,
|
|
256
398
|
name: seed.name,
|
|
@@ -279,20 +421,23 @@ export class LocalStore {
|
|
|
279
421
|
description = @description,
|
|
280
422
|
role_prompt = @rolePrompt,
|
|
281
423
|
updated_at = @updatedAt
|
|
282
|
-
WHERE id = @id`)
|
|
424
|
+
WHERE tenant_id = @tenantId AND user_id = @userId AND id = @id`)
|
|
283
425
|
.run({
|
|
284
426
|
name: seed.name,
|
|
285
427
|
description: seed.description ?? null,
|
|
286
428
|
rolePrompt: seed.rolePrompt,
|
|
287
429
|
updatedAt: now,
|
|
430
|
+
tenantId: this.scope.tenantId,
|
|
431
|
+
userId: this.scope.userId,
|
|
288
432
|
id: seed.id,
|
|
289
433
|
});
|
|
290
434
|
}
|
|
291
435
|
// 从未定制过、却被归档的行:包装回该产品后重新露出。
|
|
292
436
|
if (row.is_archived === 1 && isUntouchedSeed(seed, row)) {
|
|
293
437
|
this.db
|
|
294
|
-
.prepare(`UPDATE chat_agents SET is_archived = 0, updated_at = ?
|
|
295
|
-
|
|
438
|
+
.prepare(`UPDATE chat_agents SET is_archived = 0, updated_at = ?
|
|
439
|
+
WHERE tenant_id = ? AND user_id = ? AND id = ?`)
|
|
440
|
+
.run(now, this.scope.tenantId, this.scope.userId, seed.id);
|
|
296
441
|
}
|
|
297
442
|
}
|
|
298
443
|
/**
|
|
@@ -307,20 +452,22 @@ export class LocalStore {
|
|
|
307
452
|
this.db.exec(`ALTER TABLE ${table} ADD COLUMN ${column} ${type}`);
|
|
308
453
|
return true;
|
|
309
454
|
}
|
|
310
|
-
seedDefaults() {
|
|
455
|
+
async seedDefaults() {
|
|
311
456
|
const now = new Date().toISOString();
|
|
312
457
|
const hasBuiltin = this.db
|
|
313
|
-
.prepare(`SELECT id FROM chat_agents WHERE id = ?`)
|
|
314
|
-
.get(LOCAL_ASSISTANT_AGENT_ID);
|
|
458
|
+
.prepare(`SELECT id FROM chat_agents WHERE tenant_id = ? AND user_id = ? AND id = ?`)
|
|
459
|
+
.get(this.scope.tenantId, this.scope.userId, LOCAL_ASSISTANT_AGENT_ID);
|
|
315
460
|
// 激活包中 sortOrder 0 的主打种子占据列表首位时,shell 默认智能体
|
|
316
461
|
// 让位到 1(0.3d 起由包种子驱动,不再按 flavor 硬编码)。
|
|
317
462
|
const hasHeroSeed = getPackAgentSeeds().some((r) => r.active && r.seeds.some((s) => s.sortOrder === 0));
|
|
318
463
|
const insertAgent = this.db.prepare(`
|
|
319
464
|
INSERT INTO chat_agents (
|
|
465
|
+
tenant_id, user_id,
|
|
320
466
|
id, slug, name, icon, color, description, role_prompt, forbidden_prompt,
|
|
321
467
|
skill_ids, tool_policy, allow_external_skills, load_all_skills, is_builtin, is_archived,
|
|
322
468
|
sort_order, created_at, updated_at
|
|
323
469
|
) VALUES (
|
|
470
|
+
@tenantId, @userId,
|
|
324
471
|
@id, @slug, @name, @icon, @color, @description, @rolePrompt, @forbiddenPrompt,
|
|
325
472
|
@skillIds, @toolPolicy, @allowExternalSkills, @loadAllSkills, @isBuiltin, @isArchived,
|
|
326
473
|
@sortOrder, @createdAt, @updatedAt
|
|
@@ -328,13 +475,15 @@ export class LocalStore {
|
|
|
328
475
|
`);
|
|
329
476
|
if (!hasBuiltin) {
|
|
330
477
|
insertAgent.run({
|
|
478
|
+
tenantId: this.scope.tenantId,
|
|
479
|
+
userId: this.scope.userId,
|
|
331
480
|
id: LOCAL_ASSISTANT_AGENT_ID,
|
|
332
481
|
slug: LOCAL_ASSISTANT_AGENT_ID,
|
|
333
482
|
name: '电脑操作员',
|
|
334
483
|
icon: 'Cpu',
|
|
335
484
|
color: '#4f46e5',
|
|
336
485
|
description: '默认本地助手,可调用 shell、文件与 MCP 工具。',
|
|
337
|
-
rolePrompt: '
|
|
486
|
+
rolePrompt: '你是 **电脑操作员**,本地离线助手,回答时清晰、可执行。',
|
|
338
487
|
forbiddenPrompt: null,
|
|
339
488
|
skillIds: JSON.stringify([]),
|
|
340
489
|
toolPolicy: JSON.stringify({ mode: 'all', tools: [] }),
|
|
@@ -349,13 +498,27 @@ export class LocalStore {
|
|
|
349
498
|
});
|
|
350
499
|
}
|
|
351
500
|
else {
|
|
352
|
-
this.db.prepare(`UPDATE chat_agents SET name = ?
|
|
501
|
+
this.db.prepare(`UPDATE chat_agents SET name = ?
|
|
502
|
+
WHERE tenant_id = ? AND user_id = ? AND id = ?`)
|
|
503
|
+
.run('电脑操作员', this.scope.tenantId, this.scope.userId, LOCAL_ASSISTANT_AGENT_ID);
|
|
504
|
+
const local = this.db
|
|
505
|
+
.prepare(`SELECT role_prompt FROM chat_agents
|
|
506
|
+
WHERE tenant_id = ? AND user_id = ? AND id = ?`)
|
|
507
|
+
.get(this.scope.tenantId, this.scope.userId, LOCAL_ASSISTANT_AGENT_ID);
|
|
508
|
+
if (local?.role_prompt === '你是本地离线助手,回答时清晰、可执行。') {
|
|
509
|
+
this.db
|
|
510
|
+
.prepare(`UPDATE chat_agents SET role_prompt = ?
|
|
511
|
+
WHERE tenant_id = ? AND user_id = ? AND id = ?`)
|
|
512
|
+
.run('你是 **电脑操作员**,本地离线助手,回答时清晰、可执行。', this.scope.tenantId, this.scope.userId, LOCAL_ASSISTANT_AGENT_ID);
|
|
513
|
+
}
|
|
353
514
|
}
|
|
354
515
|
const hasAllRound = this.db
|
|
355
|
-
.prepare(`SELECT id FROM chat_agents WHERE id = ?`)
|
|
356
|
-
.get(ALL_ROUND_ASSISTANT_AGENT_ID);
|
|
516
|
+
.prepare(`SELECT id FROM chat_agents WHERE tenant_id = ? AND user_id = ? AND id = ?`)
|
|
517
|
+
.get(this.scope.tenantId, this.scope.userId, ALL_ROUND_ASSISTANT_AGENT_ID);
|
|
357
518
|
if (!hasAllRound) {
|
|
358
519
|
insertAgent.run({
|
|
520
|
+
tenantId: this.scope.tenantId,
|
|
521
|
+
userId: this.scope.userId,
|
|
359
522
|
id: ALL_ROUND_ASSISTANT_AGENT_ID,
|
|
360
523
|
slug: ALL_ROUND_ASSISTANT_AGENT_ID,
|
|
361
524
|
name: '智能助手',
|
|
@@ -382,13 +545,18 @@ export class LocalStore {
|
|
|
382
545
|
});
|
|
383
546
|
}
|
|
384
547
|
else {
|
|
385
|
-
const agent = this.db.prepare(`SELECT role_prompt FROM chat_agents
|
|
548
|
+
const agent = this.db.prepare(`SELECT role_prompt FROM chat_agents
|
|
549
|
+
WHERE tenant_id = ? AND user_id = ? AND id = ?`).get(this.scope.tenantId, this.scope.userId, ALL_ROUND_ASSISTANT_AGENT_ID);
|
|
386
550
|
if (agent) {
|
|
387
551
|
const updatedPrompt = agent.role_prompt.replace(/全能专家助手/g, '智能助手');
|
|
388
|
-
this.db.prepare(`UPDATE chat_agents SET name = ?, role_prompt = ?
|
|
552
|
+
this.db.prepare(`UPDATE chat_agents SET name = ?, role_prompt = ?
|
|
553
|
+
WHERE tenant_id = ? AND user_id = ? AND id = ?`)
|
|
554
|
+
.run('智能助手', updatedPrompt, this.scope.tenantId, this.scope.userId, ALL_ROUND_ASSISTANT_AGENT_ID);
|
|
389
555
|
}
|
|
390
556
|
else {
|
|
391
|
-
this.db.prepare(`UPDATE chat_agents SET name = ?
|
|
557
|
+
this.db.prepare(`UPDATE chat_agents SET name = ?
|
|
558
|
+
WHERE tenant_id = ? AND user_id = ? AND id = ?`)
|
|
559
|
+
.run('智能助手', this.scope.tenantId, this.scope.userId, ALL_ROUND_ASSISTANT_AGENT_ID);
|
|
392
560
|
}
|
|
393
561
|
}
|
|
394
562
|
// ─── 场景包智能体种子(0.3d) ───
|
|
@@ -409,38 +577,42 @@ export class LocalStore {
|
|
|
409
577
|
if (seed.sortOrder !== 0)
|
|
410
578
|
continue;
|
|
411
579
|
const localSort = this.db
|
|
412
|
-
.prepare(`SELECT sort_order FROM chat_agents
|
|
413
|
-
|
|
580
|
+
.prepare(`SELECT sort_order FROM chat_agents
|
|
581
|
+
WHERE tenant_id = ? AND user_id = ? AND id = ?`)
|
|
582
|
+
.get(this.scope.tenantId, this.scope.userId, LOCAL_ASSISTANT_AGENT_ID);
|
|
414
583
|
const seedSort = this.db
|
|
415
|
-
.prepare(`SELECT sort_order FROM chat_agents
|
|
416
|
-
|
|
584
|
+
.prepare(`SELECT sort_order FROM chat_agents
|
|
585
|
+
WHERE tenant_id = ? AND user_id = ? AND id = ?`)
|
|
586
|
+
.get(this.scope.tenantId, this.scope.userId, seed.id);
|
|
417
587
|
if (localSort &&
|
|
418
588
|
seedSort &&
|
|
419
589
|
localSort.sort_order === 0 &&
|
|
420
590
|
(seedSort.sort_order === 0 || seedSort.sort_order === 1)) {
|
|
421
591
|
this.db
|
|
422
|
-
.prepare(`UPDATE chat_agents SET sort_order = 1, updated_at = ?
|
|
423
|
-
|
|
592
|
+
.prepare(`UPDATE chat_agents SET sort_order = 1, updated_at = ?
|
|
593
|
+
WHERE tenant_id = ? AND user_id = ? AND id = ?`)
|
|
594
|
+
.run(now, this.scope.tenantId, this.scope.userId, LOCAL_ASSISTANT_AGENT_ID);
|
|
424
595
|
this.db
|
|
425
|
-
.prepare(`UPDATE chat_agents SET sort_order = 0, updated_at = ?
|
|
426
|
-
|
|
596
|
+
.prepare(`UPDATE chat_agents SET sort_order = 0, updated_at = ?
|
|
597
|
+
WHERE tenant_id = ? AND user_id = ? AND id = ?`)
|
|
598
|
+
.run(now, this.scope.tenantId, this.scope.userId, seed.id);
|
|
427
599
|
}
|
|
428
600
|
}
|
|
429
601
|
}
|
|
430
|
-
const hasSettings = this.getLlmSettings();
|
|
602
|
+
const hasSettings = await this.getLlmSettings();
|
|
431
603
|
if (!hasSettings) {
|
|
432
604
|
// 全新安装:直接写出厂默认 (DEFAULT_LLM_SETTINGS = OpenAI 兼容 / DeepSeek)。
|
|
433
|
-
this.setLlmSettings(DEFAULT_LLM_SETTINGS);
|
|
605
|
+
await this.setLlmSettings(DEFAULT_LLM_SETTINGS);
|
|
434
606
|
}
|
|
435
607
|
else if (llmSettingsMatchOllamaDefaults(hasSettings)) {
|
|
436
608
|
// 老安装但用户从未改过 LLM 设置(依然停留在出厂 Ollama 预设):静默迁移
|
|
437
609
|
// 到 DeepSeek 默认,避免 Windows 用户卡在"本机没装 ollama → 上来就不能聊"。
|
|
438
|
-
this.setLlmSettings(DEFAULT_LLM_SETTINGS);
|
|
610
|
+
await this.setLlmSettings(DEFAULT_LLM_SETTINGS);
|
|
439
611
|
}
|
|
440
612
|
else if (llmSettingsCarryExpiredBakedKey(hasSettings)) {
|
|
441
613
|
// 老安装保存过已作废的出厂内置 key(≤0.0.35):静默清掉(保留其它自定义
|
|
442
614
|
// 字段),设置页会引导用户填自己的 key。不清则用户永远拿着死 key 撞 401。
|
|
443
|
-
this.setLlmSettings({ ...hasSettings, apiKey: undefined });
|
|
615
|
+
await this.setLlmSettings({ ...hasSettings, apiKey: undefined });
|
|
444
616
|
}
|
|
445
617
|
// 其它情况(用户改过 provider/model/baseUrl/apiKey 任一项):尊重自定义,不动。
|
|
446
618
|
}
|
|
@@ -456,24 +628,25 @@ export class LocalStore {
|
|
|
456
628
|
return fallback;
|
|
457
629
|
return Math.max(min, Math.min(Math.trunc(value), max));
|
|
458
630
|
}
|
|
459
|
-
listChats(page = 1, limit = 50) {
|
|
460
|
-
const safePage =
|
|
461
|
-
const safeLimit =
|
|
631
|
+
async listChats(page = 1, limit = 50) {
|
|
632
|
+
const safePage = SqliteScopedStore.clampInt(page, 1, 1, Number.MAX_SAFE_INTEGER);
|
|
633
|
+
const safeLimit = SqliteScopedStore.clampInt(limit, 50, 1, 200);
|
|
462
634
|
const offset = (safePage - 1) * safeLimit;
|
|
463
|
-
const totalRow = this.db.prepare(`SELECT COUNT(*) as count FROM chat_sessions
|
|
635
|
+
const totalRow = this.db.prepare(`SELECT COUNT(*) as count FROM chat_sessions WHERE tenant_id = ? AND user_id = ?`).get(this.scope.tenantId, this.scope.userId);
|
|
464
636
|
const rows = this.db.prepare(`
|
|
465
637
|
SELECT *
|
|
466
638
|
FROM chat_sessions
|
|
639
|
+
WHERE tenant_id = ? AND user_id = ?
|
|
467
640
|
ORDER BY is_pinned DESC, datetime(updated_at) DESC
|
|
468
641
|
LIMIT ? OFFSET ?
|
|
469
|
-
`).all(safeLimit, offset);
|
|
642
|
+
`).all(this.scope.tenantId, this.scope.userId, safeLimit, offset);
|
|
470
643
|
return {
|
|
471
644
|
chats: rows.map(row => this.mapChatSession(row)),
|
|
472
645
|
total: totalRow.count,
|
|
473
646
|
};
|
|
474
647
|
}
|
|
475
|
-
createChat(title = '新对话', agentId = getBrand().defaultAgentId, projectId = null) {
|
|
476
|
-
return this.createChatWithId(randomUUID(), title, agentId, projectId);
|
|
648
|
+
async createChat(title = '新对话', agentId = getBrand().defaultAgentId, projectId = null) {
|
|
649
|
+
return await this.createChatWithId(randomUUID(), title, agentId, projectId);
|
|
477
650
|
}
|
|
478
651
|
/**
|
|
479
652
|
* 用调用方给定的 id 建会话(**幂等**:已存在则原样返回,不覆盖)。
|
|
@@ -484,35 +657,37 @@ export class LocalStore {
|
|
|
484
657
|
* 按 URL 里的 id 现场补建,才不会把一次本来能跑的任务打断在
|
|
485
658
|
* `chat not found`。
|
|
486
659
|
*/
|
|
487
|
-
createChatWithId(chatId, title = '新对话', agentId = 'local-assistant', projectId = null) {
|
|
488
|
-
const existing = this.getChat(chatId);
|
|
660
|
+
async createChatWithId(chatId, title = '新对话', agentId = 'local-assistant', projectId = null) {
|
|
661
|
+
const existing = await this.getChat(chatId);
|
|
489
662
|
if (existing)
|
|
490
663
|
return existing;
|
|
491
664
|
const now = new Date().toISOString();
|
|
492
665
|
// INSERT OR IGNORE:两个请求同时补建同一个 id(并发首发送)时,后到的
|
|
493
666
|
// 那条不会因主键冲突被打成 500——两边拿到的都是同一条会话。
|
|
494
667
|
this.db.prepare(`
|
|
495
|
-
INSERT OR IGNORE INTO chat_sessions
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
668
|
+
INSERT OR IGNORE INTO chat_sessions
|
|
669
|
+
(tenant_id, user_id, id, title, agent_id, project_id, created_at, updated_at)
|
|
670
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
671
|
+
`).run(this.scope.tenantId, this.scope.userId, chatId, title, agentId, projectId, now, now);
|
|
672
|
+
const row = this.db.prepare(`SELECT * FROM chat_sessions WHERE tenant_id = ? AND user_id = ? AND id = ?`).get(this.scope.tenantId, this.scope.userId, chatId);
|
|
499
673
|
return this.mapChatSession(row);
|
|
500
674
|
}
|
|
501
675
|
/** 删除项目时调用:把该项目下所有会话降级为无项目对话(不删会话)。 */
|
|
502
|
-
clearProjectAssignment(projectId) {
|
|
676
|
+
async clearProjectAssignment(projectId) {
|
|
503
677
|
const info = this.db
|
|
504
|
-
.prepare(`UPDATE chat_sessions SET project_id = NULL
|
|
505
|
-
|
|
678
|
+
.prepare(`UPDATE chat_sessions SET project_id = NULL
|
|
679
|
+
WHERE tenant_id = ? AND user_id = ? AND project_id = ?`)
|
|
680
|
+
.run(this.scope.tenantId, this.scope.userId, projectId);
|
|
506
681
|
return info.changes;
|
|
507
682
|
}
|
|
508
|
-
getChat(chatId) {
|
|
509
|
-
const row = this.db.prepare(`SELECT * FROM chat_sessions WHERE id = ?`).get(chatId);
|
|
683
|
+
async getChat(chatId) {
|
|
684
|
+
const row = this.db.prepare(`SELECT * FROM chat_sessions WHERE tenant_id = ? AND user_id = ? AND id = ?`).get(this.scope.tenantId, this.scope.userId, chatId);
|
|
510
685
|
if (!row)
|
|
511
686
|
return null;
|
|
512
687
|
return this.mapChatSession(row);
|
|
513
688
|
}
|
|
514
|
-
updateChat(chatId, updates) {
|
|
515
|
-
const existing = this.getChat(chatId);
|
|
689
|
+
async updateChat(chatId, updates) {
|
|
690
|
+
const existing = await this.getChat(chatId);
|
|
516
691
|
if (!existing)
|
|
517
692
|
return null;
|
|
518
693
|
// 过滤 undefined:调用方对"未提供的字段"传 undefined,直接展开会把
|
|
@@ -527,20 +702,21 @@ export class LocalStore {
|
|
|
527
702
|
this.db.prepare(`
|
|
528
703
|
UPDATE chat_sessions
|
|
529
704
|
SET title = ?, system_prompt = ?, pinned_refs = ?, is_pinned = ?, project_id = ?, updated_at = ?
|
|
530
|
-
WHERE id = ?
|
|
531
|
-
`).run(next.title, next.systemPrompt, JSON.stringify(next.pinnedRefs ?? null), next.isPinned ? 1 : 0, next.projectId ?? null, next.updatedAt, chatId);
|
|
532
|
-
return this.getChat(chatId);
|
|
533
|
-
}
|
|
534
|
-
deleteChat(chatId) {
|
|
535
|
-
const info = this.db.prepare(`DELETE FROM chat_sessions WHERE id = ?`).run(chatId);
|
|
536
|
-
this.db.prepare(`DELETE FROM settings_kv WHERE key = ?`).run(`chat_record:${chatId}`);
|
|
537
|
-
this.db.prepare(`DELETE FROM settings_kv WHERE key = ?`).run(`turn_active:${chatId}`);
|
|
705
|
+
WHERE tenant_id = ? AND user_id = ? AND id = ?
|
|
706
|
+
`).run(next.title, next.systemPrompt, JSON.stringify(next.pinnedRefs ?? null), next.isPinned ? 1 : 0, next.projectId ?? null, next.updatedAt, this.scope.tenantId, this.scope.userId, chatId);
|
|
707
|
+
return await this.getChat(chatId);
|
|
708
|
+
}
|
|
709
|
+
async deleteChat(chatId) {
|
|
710
|
+
const info = this.db.prepare(`DELETE FROM chat_sessions WHERE tenant_id = ? AND user_id = ? AND id = ?`).run(this.scope.tenantId, this.scope.userId, chatId);
|
|
711
|
+
this.db.prepare(`DELETE FROM settings_kv WHERE tenant_id = ? AND user_id = ? AND key = ?`).run(this.scope.tenantId, this.scope.userId, `chat_record:${chatId}`);
|
|
712
|
+
this.db.prepare(`DELETE FROM settings_kv WHERE tenant_id = ? AND user_id = ? AND key = ?`).run(this.scope.tenantId, this.scope.userId, `turn_active:${chatId}`);
|
|
538
713
|
return info.changes > 0;
|
|
539
714
|
}
|
|
540
|
-
chatHasMessages(chatId) {
|
|
715
|
+
async chatHasMessages(chatId) {
|
|
541
716
|
const row = this.db
|
|
542
|
-
.prepare(`SELECT 1 FROM chat_messages
|
|
543
|
-
|
|
717
|
+
.prepare(`SELECT 1 FROM chat_messages
|
|
718
|
+
WHERE tenant_id = ? AND user_id = ? AND chat_id = ? LIMIT 1`)
|
|
719
|
+
.get(this.scope.tenantId, this.scope.userId, chatId);
|
|
544
720
|
return Boolean(row);
|
|
545
721
|
}
|
|
546
722
|
/**
|
|
@@ -548,21 +724,30 @@ export class LocalStore {
|
|
|
548
724
|
* "新对话" composer without sending — empty sessions must not stay in the
|
|
549
725
|
* sidebar. Missing or already-populated chats are a no-op (`false`).
|
|
550
726
|
*/
|
|
551
|
-
deleteChatIfEmpty(chatId) {
|
|
552
|
-
if (!this.getChat(chatId) || this.chatHasMessages(chatId))
|
|
727
|
+
async deleteChatIfEmpty(chatId) {
|
|
728
|
+
if (!(await this.getChat(chatId)) || (await this.chatHasMessages(chatId)))
|
|
553
729
|
return false;
|
|
554
|
-
return this.deleteChat(chatId);
|
|
730
|
+
return await this.deleteChat(chatId);
|
|
555
731
|
}
|
|
556
732
|
/**
|
|
557
733
|
* Delete every session with zero messages, optionally keeping `exceptChatId`
|
|
558
734
|
* (the open composer). Returns the removed ids.
|
|
559
735
|
*/
|
|
560
|
-
deleteEmptyChats(exceptChatId) {
|
|
736
|
+
async deleteEmptyChats(exceptChatId) {
|
|
561
737
|
const except = exceptChatId ?? null;
|
|
562
|
-
const rows = this.db.prepare(
|
|
738
|
+
const rows = this.db.prepare(`
|
|
739
|
+
SELECT id FROM chat_sessions
|
|
740
|
+
WHERE tenant_id = ? AND user_id = ?
|
|
741
|
+
AND (? IS NULL OR id != ?)
|
|
742
|
+
AND NOT EXISTS (
|
|
743
|
+
SELECT 1 FROM chat_messages
|
|
744
|
+
WHERE tenant_id = ? AND user_id = ?
|
|
745
|
+
AND chat_id = chat_sessions.id
|
|
746
|
+
)
|
|
747
|
+
`).all(this.scope.tenantId, this.scope.userId, except, except, this.scope.tenantId, this.scope.userId);
|
|
563
748
|
const ids = rows.map((row) => row.id);
|
|
564
749
|
for (const id of ids)
|
|
565
|
-
this.deleteChat(id);
|
|
750
|
+
await this.deleteChat(id);
|
|
566
751
|
return ids;
|
|
567
752
|
}
|
|
568
753
|
/**
|
|
@@ -571,17 +756,11 @@ export class LocalStore {
|
|
|
571
756
|
* a branch), so a chat's live record moves from `chatId` to its branch;
|
|
572
757
|
* null means the chatId itself.
|
|
573
758
|
*/
|
|
574
|
-
getChatRecordId(chatId) {
|
|
575
|
-
|
|
576
|
-
.prepare(`SELECT value FROM settings_kv WHERE key = ?`)
|
|
577
|
-
.get(`chat_record:${chatId}`);
|
|
578
|
-
return row?.value ?? null;
|
|
759
|
+
async getChatRecordId(chatId) {
|
|
760
|
+
return this.getSetting(`chat_record:${chatId}`);
|
|
579
761
|
}
|
|
580
|
-
setChatRecordId(chatId, recordId) {
|
|
581
|
-
this.
|
|
582
|
-
INSERT INTO settings_kv (key, value) VALUES (?, ?)
|
|
583
|
-
ON CONFLICT(key) DO UPDATE SET value = excluded.value
|
|
584
|
-
`).run(`chat_record:${chatId}`, recordId);
|
|
762
|
+
async setChatRecordId(chatId, recordId) {
|
|
763
|
+
this.setSetting(`chat_record:${chatId}`, recordId);
|
|
585
764
|
}
|
|
586
765
|
/**
|
|
587
766
|
* W7-1: durable "a turn is in flight" marker, written BEFORE the stream
|
|
@@ -595,23 +774,18 @@ export class LocalStore {
|
|
|
595
774
|
* stale marker the detector dismisses once it sees the trailing
|
|
596
775
|
* assistant message.
|
|
597
776
|
*/
|
|
598
|
-
setTurnActive(chatId) {
|
|
599
|
-
this.
|
|
600
|
-
INSERT INTO settings_kv (key, value) VALUES (?, ?)
|
|
601
|
-
ON CONFLICT(key) DO UPDATE SET value = excluded.value
|
|
602
|
-
`).run(`turn_active:${chatId}`, JSON.stringify({ startedAt: new Date().toISOString() }));
|
|
777
|
+
async setTurnActive(chatId) {
|
|
778
|
+
this.setSetting(`turn_active:${chatId}`, JSON.stringify({ startedAt: new Date().toISOString() }));
|
|
603
779
|
}
|
|
604
|
-
clearTurnActive(chatId) {
|
|
605
|
-
this.db.prepare(`DELETE FROM settings_kv WHERE key = ?`).run(`turn_active:${chatId}`);
|
|
780
|
+
async clearTurnActive(chatId) {
|
|
781
|
+
this.db.prepare(`DELETE FROM settings_kv WHERE tenant_id = ? AND user_id = ? AND key = ?`).run(this.scope.tenantId, this.scope.userId, `turn_active:${chatId}`);
|
|
606
782
|
}
|
|
607
|
-
getTurnActive(chatId) {
|
|
608
|
-
const
|
|
609
|
-
|
|
610
|
-
.get(`turn_active:${chatId}`);
|
|
611
|
-
if (!row)
|
|
783
|
+
async getTurnActive(chatId) {
|
|
784
|
+
const value = this.getSetting(`turn_active:${chatId}`);
|
|
785
|
+
if (!value)
|
|
612
786
|
return null;
|
|
613
787
|
try {
|
|
614
|
-
const parsed = JSON.parse(
|
|
788
|
+
const parsed = JSON.parse(value);
|
|
615
789
|
return typeof parsed.startedAt === 'string' ? { startedAt: parsed.startedAt } : null;
|
|
616
790
|
}
|
|
617
791
|
catch {
|
|
@@ -622,20 +796,21 @@ export class LocalStore {
|
|
|
622
796
|
* Newest first under {@link MESSAGE_ORDER_DESC}, capped at `limit`; callers
|
|
623
797
|
* that want transcript order reverse it.
|
|
624
798
|
*/
|
|
625
|
-
listMessages(chatId, limit = 200) {
|
|
799
|
+
async listMessages(chatId, limit = 200) {
|
|
626
800
|
const rows = this.db.prepare(`
|
|
627
801
|
SELECT *
|
|
628
802
|
FROM chat_messages
|
|
629
|
-
WHERE chat_id = ?
|
|
803
|
+
WHERE tenant_id = ? AND user_id = ? AND chat_id = ?
|
|
630
804
|
ORDER BY ${MESSAGE_ORDER_DESC}
|
|
631
805
|
LIMIT ?
|
|
632
|
-
`).all(chatId,
|
|
806
|
+
`).all(this.scope.tenantId, this.scope.userId, chatId, SqliteScopedStore.clampInt(limit, 200, 1, 1000));
|
|
633
807
|
return rows.map(row => this.mapChatMessage(row));
|
|
634
808
|
}
|
|
635
|
-
getMessage(chatId, messageId) {
|
|
809
|
+
async getMessage(chatId, messageId) {
|
|
636
810
|
const row = this.db
|
|
637
|
-
.prepare(`SELECT * FROM chat_messages
|
|
638
|
-
|
|
811
|
+
.prepare(`SELECT * FROM chat_messages
|
|
812
|
+
WHERE tenant_id = ? AND user_id = ? AND id = ? AND chat_id = ?`)
|
|
813
|
+
.get(this.scope.tenantId, this.scope.userId, messageId, chatId);
|
|
639
814
|
return row ? this.mapChatMessage(row) : null;
|
|
640
815
|
}
|
|
641
816
|
/**
|
|
@@ -648,25 +823,30 @@ export class LocalStore {
|
|
|
648
823
|
* "After" is {@link MESSAGE_CUT_FROM_ID}, the same key `listMessages` orders
|
|
649
824
|
* by, so the cut matches what the user sees below the regenerated reply.
|
|
650
825
|
*/
|
|
651
|
-
deleteMessagesFrom(chatId, messageId) {
|
|
826
|
+
async deleteMessagesFrom(chatId, messageId) {
|
|
652
827
|
const info = this.db
|
|
653
828
|
.prepare(`
|
|
654
829
|
DELETE FROM chat_messages
|
|
655
|
-
WHERE chat_id = ?
|
|
656
|
-
AND
|
|
830
|
+
WHERE tenant_id = ? AND user_id = ? AND chat_id = ?
|
|
831
|
+
AND (created_at, rowid) >= (
|
|
832
|
+
SELECT created_at, rowid FROM chat_messages
|
|
833
|
+
WHERE tenant_id = ? AND user_id = ? AND id = ? AND chat_id = ?
|
|
834
|
+
)
|
|
657
835
|
`)
|
|
658
|
-
.run(chatId, messageId, chatId);
|
|
836
|
+
.run(this.scope.tenantId, this.scope.userId, chatId, this.scope.tenantId, this.scope.userId, messageId, chatId);
|
|
659
837
|
return info.changes;
|
|
660
838
|
}
|
|
661
|
-
addMessage(chatId, role, content, messageMetadata = null) {
|
|
839
|
+
async addMessage(chatId, role, content, messageMetadata = null) {
|
|
662
840
|
const id = randomUUID();
|
|
663
841
|
const now = new Date().toISOString();
|
|
664
842
|
this.db.prepare(`
|
|
665
|
-
INSERT INTO chat_messages
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
843
|
+
INSERT INTO chat_messages
|
|
844
|
+
(tenant_id, user_id, id, chat_id, role, content, message_metadata, created_at)
|
|
845
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
846
|
+
`).run(this.scope.tenantId, this.scope.userId, id, chatId, role, content, messageMetadata, now);
|
|
847
|
+
this.db.prepare(`UPDATE chat_sessions SET updated_at = ?
|
|
848
|
+
WHERE tenant_id = ? AND user_id = ? AND id = ?`).run(now, this.scope.tenantId, this.scope.userId, chatId);
|
|
849
|
+
const row = this.db.prepare(`SELECT * FROM chat_messages WHERE tenant_id = ? AND user_id = ? AND id = ?`).get(this.scope.tenantId, this.scope.userId, id);
|
|
670
850
|
return this.mapChatMessage(row);
|
|
671
851
|
}
|
|
672
852
|
/**
|
|
@@ -674,8 +854,8 @@ export class LocalStore {
|
|
|
674
854
|
* 不碰 updated_at:这类补丁不是用户可见的会话活动。
|
|
675
855
|
* metadata 损坏或非对象时从空对象开始合。
|
|
676
856
|
*/
|
|
677
|
-
patchMessageMetadata(chatId, messageId, patch) {
|
|
678
|
-
const msg = this.getMessage(chatId, messageId);
|
|
857
|
+
async patchMessageMetadata(chatId, messageId, patch) {
|
|
858
|
+
const msg = await this.getMessage(chatId, messageId);
|
|
679
859
|
if (!msg)
|
|
680
860
|
return null;
|
|
681
861
|
let current = {};
|
|
@@ -692,9 +872,10 @@ export class LocalStore {
|
|
|
692
872
|
}
|
|
693
873
|
const next = JSON.stringify({ ...current, ...patch });
|
|
694
874
|
this.db
|
|
695
|
-
.prepare(`UPDATE chat_messages SET message_metadata = ?
|
|
696
|
-
|
|
697
|
-
|
|
875
|
+
.prepare(`UPDATE chat_messages SET message_metadata = ?
|
|
876
|
+
WHERE tenant_id = ? AND user_id = ? AND id = ? AND chat_id = ?`)
|
|
877
|
+
.run(next, this.scope.tenantId, this.scope.userId, messageId, chatId);
|
|
878
|
+
return await this.getMessage(chatId, messageId);
|
|
698
879
|
}
|
|
699
880
|
/**
|
|
700
881
|
* W1.2.1: replace the chat's whole message list with a branch projection.
|
|
@@ -704,54 +885,58 @@ export class LocalStore {
|
|
|
704
885
|
* Timestamps are synthesized (base + index ms) to preserve projection
|
|
705
886
|
* order under the created_at ordering.
|
|
706
887
|
*/
|
|
707
|
-
replaceChatMessages(chatId, messages) {
|
|
888
|
+
async replaceChatMessages(chatId, messages) {
|
|
708
889
|
const base = Date.now();
|
|
709
890
|
const insert = this.db.prepare(`
|
|
710
|
-
INSERT INTO chat_messages
|
|
711
|
-
|
|
891
|
+
INSERT INTO chat_messages
|
|
892
|
+
(tenant_id, user_id, id, chat_id, role, content, message_metadata, created_at)
|
|
893
|
+
VALUES (?, ?, ?, ?, ?, ?, NULL, ?)
|
|
712
894
|
`);
|
|
713
895
|
this.db.transaction(() => {
|
|
714
|
-
this.db.prepare(`DELETE FROM chat_messages WHERE chat_id = ?`).run(chatId);
|
|
896
|
+
this.db.prepare(`DELETE FROM chat_messages WHERE tenant_id = ? AND user_id = ? AND chat_id = ?`).run(this.scope.tenantId, this.scope.userId, chatId);
|
|
715
897
|
messages.forEach((m, i) => {
|
|
716
|
-
insert.run(randomUUID(), chatId, m.role, m.content, new Date(base + i).toISOString());
|
|
898
|
+
insert.run(this.scope.tenantId, this.scope.userId, randomUUID(), chatId, m.role, m.content, new Date(base + i).toISOString());
|
|
717
899
|
});
|
|
718
900
|
this.db
|
|
719
|
-
.prepare(`UPDATE chat_sessions SET updated_at = ?
|
|
720
|
-
|
|
901
|
+
.prepare(`UPDATE chat_sessions SET updated_at = ?
|
|
902
|
+
WHERE tenant_id = ? AND user_id = ? AND id = ?`)
|
|
903
|
+
.run(new Date(base + messages.length).toISOString(), this.scope.tenantId, this.scope.userId, chatId);
|
|
721
904
|
})();
|
|
722
905
|
}
|
|
723
|
-
listChatAgents(includeArchived = false) {
|
|
906
|
+
async listChatAgents(includeArchived = false) {
|
|
724
907
|
const rows = this.db.prepare(`
|
|
725
908
|
SELECT *
|
|
726
909
|
FROM chat_agents
|
|
727
|
-
|
|
910
|
+
WHERE tenant_id = ? AND user_id = ?
|
|
911
|
+
${includeArchived ? '' : 'AND is_archived = 0'}
|
|
728
912
|
ORDER BY sort_order ASC, created_at ASC, rowid ASC
|
|
729
|
-
`).all();
|
|
913
|
+
`).all(this.scope.tenantId, this.scope.userId);
|
|
730
914
|
return rows.map(row => this.mapChatAgent(row));
|
|
731
915
|
}
|
|
732
|
-
getChatAgent(agentId) {
|
|
733
|
-
const row = this.db.prepare(`SELECT * FROM chat_agents WHERE id = ?`).get(agentId);
|
|
916
|
+
async getChatAgent(agentId) {
|
|
917
|
+
const row = this.db.prepare(`SELECT * FROM chat_agents WHERE tenant_id = ? AND user_id = ? AND id = ?`).get(this.scope.tenantId, this.scope.userId, agentId);
|
|
734
918
|
if (!row)
|
|
735
919
|
return null;
|
|
736
920
|
return this.mapChatAgent(row);
|
|
737
921
|
}
|
|
738
|
-
createChatAgent(input) {
|
|
922
|
+
async createChatAgent(input) {
|
|
739
923
|
const id = input.id || randomUUID();
|
|
740
924
|
const now = new Date().toISOString();
|
|
741
925
|
this.db.prepare(`
|
|
742
926
|
INSERT INTO chat_agents (
|
|
927
|
+
tenant_id, user_id,
|
|
743
928
|
id, slug, name, icon, color, description, role_prompt, forbidden_prompt,
|
|
744
929
|
skill_ids, tool_policy, allow_external_skills, load_all_skills,
|
|
745
930
|
is_builtin, is_archived, sort_order, created_at, updated_at
|
|
746
|
-
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
747
|
-
`).run(id, input.slug ?? null, input.name, input.icon ?? null, input.color ?? null, input.description ?? null, input.rolePrompt ?? null, input.forbiddenPrompt ?? null, JSON.stringify(input.skillIds ?? []), JSON.stringify(input.toolPolicy ?? { mode: 'all', tools: [] }), input.allowExternalSkills ? 1 : 0, input.loadAllSkills ? 1 : 0, input.isBuiltin ? 1 : 0, input.isArchived ? 1 : 0, input.sortOrder ?? 0, now, now);
|
|
748
|
-
const created = this.getChatAgent(id);
|
|
931
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
932
|
+
`).run(this.scope.tenantId, this.scope.userId, id, input.slug ?? null, input.name, input.icon ?? null, input.color ?? null, input.description ?? null, input.rolePrompt ?? null, input.forbiddenPrompt ?? null, JSON.stringify(input.skillIds ?? []), JSON.stringify(input.toolPolicy ?? { mode: 'all', tools: [] }), input.allowExternalSkills ? 1 : 0, input.loadAllSkills ? 1 : 0, input.isBuiltin ? 1 : 0, input.isArchived ? 1 : 0, input.sortOrder ?? 0, now, now);
|
|
933
|
+
const created = await this.getChatAgent(id);
|
|
749
934
|
if (!created)
|
|
750
935
|
throw new Error('Failed to create chat agent');
|
|
751
936
|
return created;
|
|
752
937
|
}
|
|
753
|
-
updateChatAgent(agentId, updates) {
|
|
754
|
-
const existing = this.getChatAgent(agentId);
|
|
938
|
+
async updateChatAgent(agentId, updates) {
|
|
939
|
+
const existing = await this.getChatAgent(agentId);
|
|
755
940
|
if (!existing)
|
|
756
941
|
return null;
|
|
757
942
|
// 过滤 undefined(同 updateChat):PATCH 路由对"未提供的字段"传 undefined,
|
|
@@ -768,74 +953,82 @@ export class LocalStore {
|
|
|
768
953
|
SET slug = ?, name = ?, icon = ?, color = ?, description = ?, role_prompt = ?, forbidden_prompt = ?,
|
|
769
954
|
skill_ids = ?, tool_policy = ?, allow_external_skills = ?, load_all_skills = ?,
|
|
770
955
|
is_builtin = ?, is_archived = ?, sort_order = ?, updated_at = ?
|
|
771
|
-
WHERE id = ?
|
|
772
|
-
`).run(merged.slug, merged.name, merged.icon, merged.color, merged.description, merged.rolePrompt, merged.forbiddenPrompt, JSON.stringify(merged.skillIds), JSON.stringify(merged.toolPolicy), merged.allowExternalSkills ? 1 : 0, merged.loadAllSkills ? 1 : 0, merged.isBuiltin ? 1 : 0, merged.isArchived ? 1 : 0, merged.sortOrder, merged.updatedAt, agentId);
|
|
773
|
-
return this.getChatAgent(agentId);
|
|
956
|
+
WHERE tenant_id = ? AND user_id = ? AND id = ?
|
|
957
|
+
`).run(merged.slug, merged.name, merged.icon, merged.color, merged.description, merged.rolePrompt, merged.forbiddenPrompt, JSON.stringify(merged.skillIds), JSON.stringify(merged.toolPolicy), merged.allowExternalSkills ? 1 : 0, merged.loadAllSkills ? 1 : 0, merged.isBuiltin ? 1 : 0, merged.isArchived ? 1 : 0, merged.sortOrder, merged.updatedAt, this.scope.tenantId, this.scope.userId, agentId);
|
|
958
|
+
return await this.getChatAgent(agentId);
|
|
774
959
|
}
|
|
775
|
-
archiveChatAgent(agentId) {
|
|
776
|
-
const updated = this.updateChatAgent(agentId, { isArchived: true });
|
|
960
|
+
async archiveChatAgent(agentId) {
|
|
961
|
+
const updated = await this.updateChatAgent(agentId, { isArchived: true });
|
|
777
962
|
return Boolean(updated);
|
|
778
963
|
}
|
|
779
|
-
saveTrace(input) {
|
|
964
|
+
async saveTrace(input) {
|
|
780
965
|
const now = new Date().toISOString();
|
|
781
966
|
this.db.prepare(`
|
|
782
|
-
INSERT INTO harness_traces
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
967
|
+
INSERT INTO harness_traces
|
|
968
|
+
(tenant_id, user_id, id, chat_id, message_id, started_at_ms, duration_ms, status, payload, created_at)
|
|
969
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
970
|
+
`).run(this.scope.tenantId, this.scope.userId, input.id, input.chatId, input.messageId ?? null, input.startedAtMs, input.durationMs ?? null, input.status, JSON.stringify(input.payload), now);
|
|
971
|
+
const row = this.db.prepare(`SELECT * FROM harness_traces WHERE tenant_id = ? AND user_id = ? AND id = ?`).get(this.scope.tenantId, this.scope.userId, input.id);
|
|
786
972
|
return this.mapHarnessTrace(row);
|
|
787
973
|
}
|
|
788
|
-
listTracesByChat(chatId, limit = 50) {
|
|
974
|
+
async listTracesByChat(chatId, limit = 50) {
|
|
789
975
|
const rows = this.db.prepare(`
|
|
790
976
|
SELECT *
|
|
791
977
|
FROM harness_traces
|
|
792
|
-
WHERE chat_id = ?
|
|
978
|
+
WHERE tenant_id = ? AND user_id = ? AND chat_id = ?
|
|
793
979
|
ORDER BY started_at_ms DESC
|
|
794
980
|
LIMIT ?
|
|
795
|
-
`).all(chatId,
|
|
981
|
+
`).all(this.scope.tenantId, this.scope.userId, chatId, SqliteScopedStore.clampInt(limit, 50, 1, 500));
|
|
796
982
|
return rows.map(row => this.mapHarnessTrace(row));
|
|
797
983
|
}
|
|
798
|
-
getTrace(traceId) {
|
|
799
|
-
const row = this.db.prepare(`SELECT * FROM harness_traces WHERE id = ?`).get(traceId);
|
|
984
|
+
async getTrace(traceId) {
|
|
985
|
+
const row = this.db.prepare(`SELECT * FROM harness_traces WHERE tenant_id = ? AND user_id = ? AND id = ?`).get(this.scope.tenantId, this.scope.userId, traceId);
|
|
800
986
|
if (!row)
|
|
801
987
|
return null;
|
|
802
988
|
return this.mapHarnessTrace(row);
|
|
803
989
|
}
|
|
804
990
|
// ─── 4.6a 跨 turn 后台任务 ───────────────────────────────────────────
|
|
805
|
-
createTask(input) {
|
|
991
|
+
async createTask(input) {
|
|
806
992
|
const id = randomUUID();
|
|
807
993
|
const now = new Date().toISOString();
|
|
808
994
|
this.db.prepare(`
|
|
809
995
|
INSERT INTO tasks (
|
|
996
|
+
tenant_id, user_id,
|
|
810
997
|
id, chat_id, task, status, answer, error,
|
|
811
998
|
worktree_path, worktree_branch, worktree_state, record_id, trace_id,
|
|
812
999
|
depends_on, created_at, updated_at
|
|
813
|
-
) VALUES (?, ?, ?, ?, NULL, NULL, ?, ?, ?, ?, NULL, ?, ?, ?)
|
|
814
|
-
`).run(id, input.chatId, input.task, input.initialStatus ?? 'running', input.worktreePath ?? null, input.worktreeBranch ?? null,
|
|
1000
|
+
) VALUES (?, ?, ?, ?, ?, ?, NULL, NULL, ?, ?, ?, ?, NULL, ?, ?, ?)
|
|
1001
|
+
`).run(this.scope.tenantId, this.scope.userId, id, input.chatId, input.task, input.initialStatus ?? 'running', input.worktreePath ?? null, input.worktreeBranch ?? null,
|
|
815
1002
|
// worktree 任务一创建就进入 pending(等待终态后合并/丢弃);
|
|
816
1003
|
// 非 worktree 任务该列无意义,保持 NULL。
|
|
817
1004
|
input.worktreePath ? 'pending' : null, input.recordId ?? null, input.dependsOn?.length ? JSON.stringify(input.dependsOn) : null, now, now);
|
|
818
|
-
const row = this.db.prepare(`SELECT * FROM tasks WHERE id = ?`).get(id);
|
|
1005
|
+
const row = this.db.prepare(`SELECT * FROM tasks WHERE tenant_id = ? AND user_id = ? AND id = ?`).get(this.scope.tenantId, this.scope.userId, id);
|
|
819
1006
|
return this.mapTask(row);
|
|
820
1007
|
}
|
|
821
|
-
getTask(taskId) {
|
|
822
|
-
const row = this.db.prepare(`SELECT * FROM tasks WHERE id = ?`).get(taskId);
|
|
1008
|
+
async getTask(taskId) {
|
|
1009
|
+
const row = this.db.prepare(`SELECT * FROM tasks WHERE tenant_id = ? AND user_id = ? AND id = ?`).get(this.scope.tenantId, this.scope.userId, taskId);
|
|
823
1010
|
return row ? this.mapTask(row) : null;
|
|
824
1011
|
}
|
|
825
1012
|
/** 按 chat 列任务(updated_at 新→旧);chatId 缺省时列全部(任务面板的全局视图)。 */
|
|
826
|
-
listTasks(chatId, limit = 100) {
|
|
827
|
-
const safeLimit =
|
|
1013
|
+
async listTasks(chatId, limit = 100) {
|
|
1014
|
+
const safeLimit = SqliteScopedStore.clampInt(limit, 100, 1, 500);
|
|
828
1015
|
const rows = (chatId
|
|
829
|
-
? this.db.prepare(`SELECT * FROM tasks
|
|
830
|
-
|
|
1016
|
+
? this.db.prepare(`SELECT * FROM tasks
|
|
1017
|
+
WHERE tenant_id = ? AND user_id = ? AND chat_id = ?
|
|
1018
|
+
ORDER BY datetime(updated_at) DESC LIMIT ?`)
|
|
1019
|
+
.all(this.scope.tenantId, this.scope.userId, chatId, safeLimit)
|
|
1020
|
+
: this.db.prepare(`SELECT * FROM tasks
|
|
1021
|
+
WHERE tenant_id = ? AND user_id = ?
|
|
1022
|
+
ORDER BY datetime(updated_at) DESC LIMIT ?`)
|
|
1023
|
+
.all(this.scope.tenantId, this.scope.userId, safeLimit));
|
|
831
1024
|
return rows.map(row => this.mapTask(row));
|
|
832
1025
|
}
|
|
833
1026
|
/**
|
|
834
1027
|
* 终态/进度回写。只写传入的字段(undefined 过滤),updated_at 总是刷新。
|
|
835
1028
|
* 与 updateChat 同约定:显式传 null 才是置空。
|
|
836
1029
|
*/
|
|
837
|
-
updateTask(taskId, updates) {
|
|
838
|
-
const existing = this.getTask(taskId);
|
|
1030
|
+
async updateTask(taskId, updates) {
|
|
1031
|
+
const existing = await this.getTask(taskId);
|
|
839
1032
|
if (!existing)
|
|
840
1033
|
return null;
|
|
841
1034
|
const clean = Object.fromEntries(Object.entries(updates).filter(([, value]) => value !== undefined));
|
|
@@ -847,36 +1040,41 @@ export class LocalStore {
|
|
|
847
1040
|
this.db.prepare(`
|
|
848
1041
|
UPDATE tasks
|
|
849
1042
|
SET status = ?, answer = ?, error = ?, worktree_state = ?, trace_id = ?, record_id = ?, updated_at = ?
|
|
850
|
-
WHERE id = ?
|
|
851
|
-
`).run(next.status, next.answer, next.error, next.worktreeState, next.traceId, next.recordId, next.updatedAt, taskId);
|
|
852
|
-
return this.getTask(taskId);
|
|
1043
|
+
WHERE tenant_id = ? AND user_id = ? AND id = ?
|
|
1044
|
+
`).run(next.status, next.answer, next.error, next.worktreeState, next.traceId, next.recordId, next.updatedAt, this.scope.tenantId, this.scope.userId, taskId);
|
|
1045
|
+
return await this.getTask(taskId);
|
|
853
1046
|
}
|
|
854
1047
|
/**
|
|
855
1048
|
* 回写推理时间线。故意不碰 updated_at——流过程中的增量不应把任务
|
|
856
1049
|
* 面板按「最近活动」反复顶到最上。
|
|
857
1050
|
*/
|
|
858
|
-
saveTaskProcess(taskId, processJson) {
|
|
859
|
-
this.db.prepare(`UPDATE tasks SET process_json = ?
|
|
1051
|
+
async saveTaskProcess(taskId, processJson) {
|
|
1052
|
+
this.db.prepare(`UPDATE tasks SET process_json = ?
|
|
1053
|
+
WHERE tenant_id = ? AND user_id = ? AND id = ?`)
|
|
1054
|
+
.run(processJson, this.scope.tenantId, this.scope.userId, taskId);
|
|
860
1055
|
}
|
|
861
1056
|
/**
|
|
862
1057
|
* 启动清扫:running 任务只可能属于上一个已死的进程(任务流不跨进程
|
|
863
1058
|
* 存活),全部落成 failed/interrupted——与 W7-1 turn_active 检测器同
|
|
864
1059
|
* 理,残留 running 是崩溃签名,不是真相。
|
|
865
1060
|
*/
|
|
866
|
-
failRunningTasks(reason) {
|
|
1061
|
+
async failRunningTasks(reason) {
|
|
867
1062
|
const info = this.db
|
|
868
|
-
.prepare(`UPDATE tasks SET status = 'failed', error = ?, updated_at = ?
|
|
869
|
-
|
|
1063
|
+
.prepare(`UPDATE tasks SET status = 'failed', error = ?, updated_at = ?
|
|
1064
|
+
WHERE tenant_id = ? AND user_id = ? AND status = 'running'`)
|
|
1065
|
+
.run(reason, new Date().toISOString(), this.scope.tenantId, this.scope.userId);
|
|
870
1066
|
// 级联:blocked 任务的点火能力在 TaskService(进程内),不跨进程存活;
|
|
871
1067
|
// 重启后统一标 failed,模型经 task_status 看到原因后可重跑。
|
|
872
1068
|
const blocked = this.db
|
|
873
|
-
.prepare(`SELECT id FROM tasks
|
|
874
|
-
|
|
1069
|
+
.prepare(`SELECT id FROM tasks
|
|
1070
|
+
WHERE tenant_id = ? AND user_id = ? AND status = 'blocked'`)
|
|
1071
|
+
.all(this.scope.tenantId, this.scope.userId);
|
|
875
1072
|
const now = new Date().toISOString();
|
|
876
1073
|
for (const row of blocked) {
|
|
877
1074
|
this.db
|
|
878
|
-
.prepare(`UPDATE tasks SET status = 'failed', error = ?, updated_at = ?
|
|
879
|
-
|
|
1075
|
+
.prepare(`UPDATE tasks SET status = 'failed', error = ?, updated_at = ?
|
|
1076
|
+
WHERE tenant_id = ? AND user_id = ? AND id = ?`)
|
|
1077
|
+
.run(`进程重启中断了编排等待(依赖任务已随进程终止);请重新 task_run。`, now, this.scope.tenantId, this.scope.userId, row.id);
|
|
880
1078
|
}
|
|
881
1079
|
return info.changes + blocked.length;
|
|
882
1080
|
}
|
|
@@ -885,19 +1083,19 @@ export class LocalStore {
|
|
|
885
1083
|
* 记录一条用量事件(一轮 chat / 一次标题生成 / 一次摘要压缩等)。
|
|
886
1084
|
* `costUsd` 为 null 表示该模型无单价(本地/未知)——存 NULL,面板渲染为 "—"。
|
|
887
1085
|
*/
|
|
888
|
-
recordUsageEvent(input) {
|
|
1086
|
+
async recordUsageEvent(input) {
|
|
889
1087
|
this.db.prepare(`
|
|
890
1088
|
INSERT INTO usage_events
|
|
891
|
-
(id, chat_id, kind, provider, model, prompt_tokens, completion_tokens, total_tokens, cached_prompt_tokens, cost_usd, created_at)
|
|
892
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
893
|
-
`).run(randomUUID(), input.chatId ?? null, input.kind, input.provider ?? null, input.model ?? null, Math.max(0, Math.floor(input.promptTokens)), Math.max(0, Math.floor(input.completionTokens)), Math.max(0, Math.floor(input.totalTokens)), Math.max(0, Math.floor(input.cachedPromptTokens ?? 0)), input.costUsd ?? null, new Date().toISOString());
|
|
1089
|
+
(tenant_id, user_id, id, chat_id, kind, provider, model, prompt_tokens, completion_tokens, total_tokens, cached_prompt_tokens, cost_usd, created_at)
|
|
1090
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
1091
|
+
`).run(this.scope.tenantId, this.scope.userId, randomUUID(), input.chatId ?? null, input.kind, input.provider ?? null, input.model ?? null, Math.max(0, Math.floor(input.promptTokens)), Math.max(0, Math.floor(input.completionTokens)), Math.max(0, Math.floor(input.totalTokens)), Math.max(0, Math.floor(input.cachedPromptTokens ?? 0)), input.costUsd ?? null, new Date().toISOString());
|
|
894
1092
|
}
|
|
895
1093
|
/**
|
|
896
1094
|
* 聚合最近 `sinceDays` 天的用量:按 model 分桶( token 合计 + 成本合计),
|
|
897
1095
|
* 外加总计。成本只统计有单价的模型(cost_usd 非 NULL);无单价模型的
|
|
898
1096
|
* token 照常计入,但成本列不计。返回按 totalTokens 降序。
|
|
899
1097
|
*/
|
|
900
|
-
getUsageSummary(sinceDays = 30) {
|
|
1098
|
+
async getUsageSummary(sinceDays = 30) {
|
|
901
1099
|
const since = new Date(Date.now() - sinceDays * 24 * 60 * 60 * 1000).toISOString();
|
|
902
1100
|
const rows = this.db.prepare(`
|
|
903
1101
|
SELECT
|
|
@@ -910,104 +1108,92 @@ export class LocalStore {
|
|
|
910
1108
|
SUM(cached_prompt_tokens) AS cached_prompt_tokens,
|
|
911
1109
|
SUM(cost_usd) AS cost_usd
|
|
912
1110
|
FROM usage_events
|
|
913
|
-
WHERE created_at >= ?
|
|
1111
|
+
WHERE tenant_id = ? AND user_id = ? AND created_at >= ?
|
|
914
1112
|
GROUP BY model, provider
|
|
915
1113
|
ORDER BY total_tokens DESC
|
|
916
|
-
`).all(since);
|
|
1114
|
+
`).all(this.scope.tenantId, this.scope.userId, since);
|
|
917
1115
|
return buildUsageSummary(rows, sinceDays);
|
|
918
1116
|
}
|
|
919
|
-
getLlmSettings() {
|
|
920
|
-
const
|
|
921
|
-
if (!
|
|
1117
|
+
async getLlmSettings() {
|
|
1118
|
+
const value = this.getSetting('llm_settings');
|
|
1119
|
+
if (!value)
|
|
922
1120
|
return null;
|
|
923
1121
|
try {
|
|
924
|
-
return JSON.parse(
|
|
1122
|
+
return JSON.parse(value);
|
|
925
1123
|
}
|
|
926
1124
|
catch {
|
|
927
1125
|
return null;
|
|
928
1126
|
}
|
|
929
1127
|
}
|
|
930
|
-
setLlmSettings(settings) {
|
|
1128
|
+
async setLlmSettings(settings) {
|
|
931
1129
|
const merged = mergeLlmSettings(settings);
|
|
932
|
-
this.
|
|
933
|
-
INSERT INTO settings_kv (key, value) VALUES ('llm_settings', ?)
|
|
934
|
-
ON CONFLICT(key) DO UPDATE SET value = excluded.value
|
|
935
|
-
`).run(JSON.stringify(merged));
|
|
1130
|
+
this.setSetting('llm_settings', JSON.stringify(merged));
|
|
936
1131
|
return merged;
|
|
937
1132
|
}
|
|
938
1133
|
// ─── W6-6 遥测(OTLP collector + 隐私模式)────────────────────────────
|
|
939
1134
|
/** 读取遥测设置;从未配置过时返回 null(调用方按"关"处理)。 */
|
|
940
|
-
getTelemetrySettings() {
|
|
941
|
-
const
|
|
942
|
-
if (!
|
|
1135
|
+
async getTelemetrySettings() {
|
|
1136
|
+
const value = this.getSetting('telemetry_settings');
|
|
1137
|
+
if (!value)
|
|
943
1138
|
return null;
|
|
944
1139
|
try {
|
|
945
|
-
return mergeTelemetrySettings(JSON.parse(
|
|
1140
|
+
return mergeTelemetrySettings(JSON.parse(value));
|
|
946
1141
|
}
|
|
947
1142
|
catch {
|
|
948
1143
|
return null;
|
|
949
1144
|
}
|
|
950
1145
|
}
|
|
951
|
-
setTelemetrySettings(settings) {
|
|
1146
|
+
async setTelemetrySettings(settings) {
|
|
952
1147
|
const merged = mergeTelemetrySettings(settings);
|
|
953
|
-
this.
|
|
954
|
-
INSERT INTO settings_kv (key, value) VALUES ('telemetry_settings', ?)
|
|
955
|
-
ON CONFLICT(key) DO UPDATE SET value = excluded.value
|
|
956
|
-
`).run(JSON.stringify(merged));
|
|
1148
|
+
this.setSetting('telemetry_settings', JSON.stringify(merged));
|
|
957
1149
|
return merged;
|
|
958
1150
|
}
|
|
959
|
-
getWebSearchSettings() {
|
|
960
|
-
const
|
|
961
|
-
if (!
|
|
1151
|
+
async getWebSearchSettings() {
|
|
1152
|
+
const value = this.getSetting('web_search_settings');
|
|
1153
|
+
if (!value)
|
|
962
1154
|
return null;
|
|
963
1155
|
try {
|
|
964
|
-
return mergeWebSearchSettings(JSON.parse(
|
|
1156
|
+
return mergeWebSearchSettings(JSON.parse(value));
|
|
965
1157
|
}
|
|
966
1158
|
catch {
|
|
967
1159
|
return null;
|
|
968
1160
|
}
|
|
969
1161
|
}
|
|
970
|
-
setWebSearchSettings(settings) {
|
|
1162
|
+
async setWebSearchSettings(settings) {
|
|
971
1163
|
const merged = mergeWebSearchSettings(settings);
|
|
972
|
-
this.
|
|
973
|
-
INSERT INTO settings_kv (key, value) VALUES ('web_search_settings', ?)
|
|
974
|
-
ON CONFLICT(key) DO UPDATE SET value = excluded.value
|
|
975
|
-
`).run(JSON.stringify(merged));
|
|
1164
|
+
this.setSetting('web_search_settings', JSON.stringify(merged));
|
|
976
1165
|
return merged;
|
|
977
1166
|
}
|
|
978
1167
|
// ─── 帮助改进产品(行为 / 对话 / 资料 分项同意 + 本地队列)──────────
|
|
979
|
-
getInsightsSettings() {
|
|
980
|
-
const
|
|
981
|
-
if (!
|
|
1168
|
+
async getInsightsSettings() {
|
|
1169
|
+
const value = this.getSetting('insights_settings');
|
|
1170
|
+
if (!value)
|
|
982
1171
|
return null;
|
|
983
1172
|
try {
|
|
984
|
-
return mergeInsightsSettings(JSON.parse(
|
|
1173
|
+
return mergeInsightsSettings(JSON.parse(value));
|
|
985
1174
|
}
|
|
986
1175
|
catch {
|
|
987
1176
|
return null;
|
|
988
1177
|
}
|
|
989
1178
|
}
|
|
990
|
-
ensureInsightsSettings() {
|
|
991
|
-
const existing = this.getInsightsSettings();
|
|
1179
|
+
async ensureInsightsSettings() {
|
|
1180
|
+
const existing = await this.getInsightsSettings();
|
|
992
1181
|
if (existing)
|
|
993
1182
|
return existing;
|
|
994
|
-
return this.setInsightsSettings({});
|
|
1183
|
+
return await this.setInsightsSettings({});
|
|
995
1184
|
}
|
|
996
|
-
setInsightsSettings(settings) {
|
|
997
|
-
const current = this.getInsightsSettings();
|
|
1185
|
+
async setInsightsSettings(settings) {
|
|
1186
|
+
const current = await this.getInsightsSettings();
|
|
998
1187
|
const merged = mergeInsightsSettings({
|
|
999
1188
|
...current,
|
|
1000
1189
|
...settings,
|
|
1001
1190
|
installId: current?.installId ?? settings.installId,
|
|
1002
1191
|
profile: mergeInsightsProfile({ ...current?.profile, ...settings.profile }),
|
|
1003
1192
|
});
|
|
1004
|
-
this.
|
|
1005
|
-
INSERT INTO settings_kv (key, value) VALUES ('insights_settings', ?)
|
|
1006
|
-
ON CONFLICT(key) DO UPDATE SET value = excluded.value
|
|
1007
|
-
`).run(JSON.stringify(merged));
|
|
1193
|
+
this.setSetting('insights_settings', JSON.stringify(merged));
|
|
1008
1194
|
return merged;
|
|
1009
1195
|
}
|
|
1010
|
-
enqueueInsight(kind, payload) {
|
|
1196
|
+
async enqueueInsight(kind, payload) {
|
|
1011
1197
|
const row = {
|
|
1012
1198
|
id: randomUUID(),
|
|
1013
1199
|
kind,
|
|
@@ -1017,16 +1203,17 @@ export class LocalStore {
|
|
|
1017
1203
|
uploadError: null,
|
|
1018
1204
|
};
|
|
1019
1205
|
this.db.prepare(`
|
|
1020
|
-
INSERT INTO insights_outbox
|
|
1021
|
-
|
|
1022
|
-
|
|
1206
|
+
INSERT INTO insights_outbox
|
|
1207
|
+
(tenant_id, user_id, id, kind, payload, created_at, uploaded_at, upload_error)
|
|
1208
|
+
VALUES (?, ?, ?, ?, ?, ?, NULL, NULL)
|
|
1209
|
+
`).run(this.scope.tenantId, this.scope.userId, row.id, row.kind, JSON.stringify(row.payload), row.createdAt);
|
|
1023
1210
|
return row;
|
|
1024
1211
|
}
|
|
1025
|
-
listInsightOutbox(opts = {}) {
|
|
1212
|
+
async listInsightOutbox(opts = {}) {
|
|
1026
1213
|
const limit = Math.max(1, Math.min(opts.limit ?? 200, 500));
|
|
1027
1214
|
let sql = `SELECT * FROM insights_outbox`;
|
|
1028
|
-
const where = [];
|
|
1029
|
-
const params = [];
|
|
1215
|
+
const where = ['tenant_id = ?', 'user_id = ?'];
|
|
1216
|
+
const params = [this.scope.tenantId, this.scope.userId];
|
|
1030
1217
|
if (opts.kind) {
|
|
1031
1218
|
where.push('kind = ?');
|
|
1032
1219
|
params.push(opts.kind);
|
|
@@ -1037,20 +1224,22 @@ export class LocalStore {
|
|
|
1037
1224
|
else if (opts.uploaded === false) {
|
|
1038
1225
|
where.push('uploaded_at IS NULL');
|
|
1039
1226
|
}
|
|
1040
|
-
|
|
1041
|
-
sql += ` WHERE ${where.join(' AND ')}`;
|
|
1227
|
+
sql += ` WHERE ${where.join(' AND ')}`;
|
|
1042
1228
|
sql += ` ORDER BY created_at DESC LIMIT ?`;
|
|
1043
1229
|
params.push(limit);
|
|
1044
1230
|
const rows = this.db.prepare(sql).all(...params);
|
|
1045
1231
|
return rows.map((row) => this.mapInsightOutbox(row));
|
|
1046
1232
|
}
|
|
1047
|
-
markInsightUploaded(id) {
|
|
1048
|
-
this.db.prepare(`UPDATE insights_outbox SET uploaded_at = ?, upload_error = NULL
|
|
1233
|
+
async markInsightUploaded(id) {
|
|
1234
|
+
this.db.prepare(`UPDATE insights_outbox SET uploaded_at = ?, upload_error = NULL
|
|
1235
|
+
WHERE tenant_id = ? AND user_id = ? AND id = ?`).run(new Date().toISOString(), this.scope.tenantId, this.scope.userId, id);
|
|
1049
1236
|
}
|
|
1050
|
-
markInsightUploadError(id, error) {
|
|
1051
|
-
this.db.prepare(`UPDATE insights_outbox SET upload_error = ?
|
|
1237
|
+
async markInsightUploadError(id, error) {
|
|
1238
|
+
this.db.prepare(`UPDATE insights_outbox SET upload_error = ?
|
|
1239
|
+
WHERE tenant_id = ? AND user_id = ? AND id = ?`)
|
|
1240
|
+
.run(error.slice(0, 200), this.scope.tenantId, this.scope.userId, id);
|
|
1052
1241
|
}
|
|
1053
|
-
insightStats() {
|
|
1242
|
+
async insightStats() {
|
|
1054
1243
|
const row = this.db.prepare(`
|
|
1055
1244
|
SELECT
|
|
1056
1245
|
SUM(CASE WHEN kind = 'event' THEN 1 ELSE 0 END) AS events,
|
|
@@ -1058,7 +1247,8 @@ export class LocalStore {
|
|
|
1058
1247
|
SUM(CASE WHEN kind = 'profile' THEN 1 ELSE 0 END) AS profile,
|
|
1059
1248
|
SUM(CASE WHEN uploaded_at IS NULL THEN 1 ELSE 0 END) AS pending
|
|
1060
1249
|
FROM insights_outbox
|
|
1061
|
-
|
|
1250
|
+
WHERE tenant_id = ? AND user_id = ?
|
|
1251
|
+
`).get(this.scope.tenantId, this.scope.userId);
|
|
1062
1252
|
return {
|
|
1063
1253
|
events: Number(row?.events ?? 0),
|
|
1064
1254
|
turns: Number(row?.turns ?? 0),
|
|
@@ -1066,8 +1256,8 @@ export class LocalStore {
|
|
|
1066
1256
|
pending: Number(row?.pending ?? 0),
|
|
1067
1257
|
};
|
|
1068
1258
|
}
|
|
1069
|
-
exportInsightsBundle() {
|
|
1070
|
-
const settings = this.ensureInsightsSettings();
|
|
1259
|
+
async exportInsightsBundle() {
|
|
1260
|
+
const settings = await this.ensureInsightsSettings();
|
|
1071
1261
|
return {
|
|
1072
1262
|
schema: INSIGHTS_EXPORT_SCHEMA,
|
|
1073
1263
|
exportedAt: new Date().toISOString(),
|
|
@@ -1078,8 +1268,8 @@ export class LocalStore {
|
|
|
1078
1268
|
shareProfile: settings.shareProfile,
|
|
1079
1269
|
},
|
|
1080
1270
|
profile: settings.profile,
|
|
1081
|
-
stats: this.insightStats(),
|
|
1082
|
-
records: this.listInsightOutbox({ limit: 500 }),
|
|
1271
|
+
stats: await this.insightStats(),
|
|
1272
|
+
records: await this.listInsightOutbox({ limit: 500 }),
|
|
1083
1273
|
};
|
|
1084
1274
|
}
|
|
1085
1275
|
mapInsightOutbox(row) {
|
|
@@ -1094,7 +1284,7 @@ export class LocalStore {
|
|
|
1094
1284
|
}
|
|
1095
1285
|
// ─── 场景包存储 ─────────────────────────────────────────────────
|
|
1096
1286
|
// 包数据访问收敛进包内存储(0.4,如 packages/pack-*/src/store.ts)。
|
|
1097
|
-
//
|
|
1287
|
+
// Scenario packs receive driver-neutral SQL access; raw handles stay here.
|
|
1098
1288
|
mapChatSession(row) {
|
|
1099
1289
|
return {
|
|
1100
1290
|
id: String(row.id),
|
|
@@ -1171,6 +1361,19 @@ export class LocalStore {
|
|
|
1171
1361
|
updatedAt: String(row.updated_at),
|
|
1172
1362
|
};
|
|
1173
1363
|
}
|
|
1364
|
+
getSetting(key) {
|
|
1365
|
+
const row = this.db.prepare(`SELECT value FROM settings_kv
|
|
1366
|
+
WHERE tenant_id = ? AND user_id = ? AND key = ?`).get(this.scope.tenantId, this.scope.userId, key);
|
|
1367
|
+
return row?.value ?? null;
|
|
1368
|
+
}
|
|
1369
|
+
setSetting(key, value) {
|
|
1370
|
+
this.db.prepare(`
|
|
1371
|
+
INSERT INTO settings_kv (tenant_id, user_id, key, value)
|
|
1372
|
+
VALUES (?, ?, ?, ?)
|
|
1373
|
+
ON CONFLICT(tenant_id, user_id, key)
|
|
1374
|
+
DO UPDATE SET value = excluded.value
|
|
1375
|
+
`).run(this.scope.tenantId, this.scope.userId, key, value);
|
|
1376
|
+
}
|
|
1174
1377
|
safeJson(value, fallback = null) {
|
|
1175
1378
|
if (typeof value !== 'string')
|
|
1176
1379
|
return fallback;
|
|
@@ -1182,4 +1385,6 @@ export class LocalStore {
|
|
|
1182
1385
|
}
|
|
1183
1386
|
}
|
|
1184
1387
|
}
|
|
1185
|
-
export
|
|
1388
|
+
export { LOCAL_SCOPE, closeStorage, getPackDbAccess, getScopedStore, initializeStorage, selectStorageDriver, } from './driver.js';
|
|
1389
|
+
export { registerStorageDriver } from './driver.js';
|
|
1390
|
+
export { SqliteStorageDriver } from './sqlite-driver.js';
|