@steerable/agent-shell 0.6.21 → 0.6.22
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
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { randomUUID } from 'crypto';
|
|
2
2
|
import { fileURLToPath } from 'node:url';
|
|
3
3
|
import path from 'node:path';
|
|
4
|
+
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
4
5
|
import os from 'node:os';
|
|
5
6
|
import { getAppRootDir, shellOpenPath } from '../runtime.js';
|
|
6
7
|
import { llmService, getSidecarSupervisor, whenSidecarSupervisor } from '../llm/index.js';
|
|
@@ -10,7 +11,7 @@ import { driveWithAutoContinue, resolveAutoContinueMax, } from './auto-continue-
|
|
|
10
11
|
import { builtinSubagentParam } from './subagent-profiles.js';
|
|
11
12
|
import { appendTimelineDelta, syncTimelineTools, } from './turn-timeline.js';
|
|
12
13
|
import { turnDurationMs } from './turn-duration.js';
|
|
13
|
-
import {
|
|
14
|
+
import { DEFAULT_SYSTEM_PROMPT, telemetryEnabled } from '../storage/index.js';
|
|
14
15
|
import { isSkillPinned, isToolAllowed, mergeAgentCapabilities, normalizeToolPolicy, resolveSkillExcludes, } from './agent-capability.js';
|
|
15
16
|
import { getBrand } from '../brand.js';
|
|
16
17
|
import { recordInsightEvent, recordInsightTurn, recordInsightProfile } from '../insights/record.js';
|
|
@@ -21,7 +22,7 @@ import { allowEgressForBaseUrl, getActiveEgressBroker, getEgressPosture } from '
|
|
|
21
22
|
import { buildExecSandbox, parseExecPolicy } from '../sidecar/exec-sandbox.js';
|
|
22
23
|
import { SidecarSupervisor } from '../sidecar/index.js';
|
|
23
24
|
import { diagnoseLlmConnection } from './llm-diagnose.js';
|
|
24
|
-
import { buildSystemPrompt, buildForcedSkillMessage, conditionsFromTools, } from './prompt-builder.js';
|
|
25
|
+
import { brandSkillVars, buildSystemPrompt, buildForcedSkillMessage, conditionsFromTools, } from './prompt-builder.js';
|
|
25
26
|
import { parseUserMessageTriggers } from './message-triggers.js';
|
|
26
27
|
import { findSkill, loadSkills, classifySkillOrigin, listSkillRoots } from './skill-loader.js';
|
|
27
28
|
import { installSkillFromDirectory } from './skill-install.js';
|
|
@@ -40,6 +41,7 @@ import { beginPackTurnObservers, collectPackExecWritableRoots, collectPackForced
|
|
|
40
41
|
import { matchPackBackendRoute } from './pack-backend-routes.js';
|
|
41
42
|
import { collectTurnFiles } from './turn-files.js';
|
|
42
43
|
import { resolveMentionedPaths } from './mentioned-paths.js';
|
|
44
|
+
import { getAuthProvider } from '../auth/index.js';
|
|
43
45
|
/**
|
|
44
46
|
* 把 CoreLoop/LLM 的底层错误翻译成适合直接显示在助手消息里的中文提示。
|
|
45
47
|
* 原始错误仍保存在 messageMetadata.completionReason 里,便于排查。
|
|
@@ -62,7 +64,7 @@ export function userFacingCoreLoopFailure(reason) {
|
|
|
62
64
|
/**
|
|
63
65
|
* Parses a pagination-style query param (`?page=`, `?limit=`) into a finite
|
|
64
66
|
* positive integer, falling back to `fallback` for missing/non-numeric input
|
|
65
|
-
* (e.g. `?page=abc`) instead of letting `NaN` leak into
|
|
67
|
+
* (e.g. `?page=abc`) instead of letting `NaN` leak into storage queries
|
|
66
68
|
* and the echoed-back `pagination` block of the response.
|
|
67
69
|
*/
|
|
68
70
|
function parsePositiveIntParam(raw, fallback) {
|
|
@@ -148,19 +150,44 @@ function buildLocalAgent() {
|
|
|
148
150
|
}
|
|
149
151
|
export class LocalBackendRouter {
|
|
150
152
|
toolRouter;
|
|
153
|
+
defaultStore;
|
|
154
|
+
storeContext = new AsyncLocalStorage();
|
|
155
|
+
resolveStore;
|
|
151
156
|
broadcast;
|
|
152
157
|
/** 4.6a/4.6c:任务面板的路由入口(列表 + 合并/丢弃)。未注入时任务路由 503。 */
|
|
153
158
|
taskService;
|
|
154
|
-
constructor(toolRouter, options
|
|
159
|
+
constructor(toolRouter, options) {
|
|
155
160
|
this.toolRouter = toolRouter;
|
|
161
|
+
this.defaultStore = options.store;
|
|
162
|
+
this.resolveStore = options.resolveStore ?? (() => options.store);
|
|
156
163
|
this.broadcast = options.broadcast ?? null;
|
|
157
164
|
this.taskService = options.taskService ?? null;
|
|
158
165
|
}
|
|
159
|
-
|
|
166
|
+
get store() {
|
|
167
|
+
return this.storeContext.getStore() ?? this.defaultStore;
|
|
168
|
+
}
|
|
169
|
+
/** Store selected for the active request, or the host default outside one. */
|
|
170
|
+
get activeStore() {
|
|
171
|
+
return this.store;
|
|
172
|
+
}
|
|
173
|
+
handle(request) {
|
|
174
|
+
return this.storeContext.run(this.resolveStore(request.principal), () => this.handleScoped(request));
|
|
175
|
+
}
|
|
176
|
+
async handleScoped(request) {
|
|
160
177
|
const { method } = request;
|
|
161
178
|
const url = new URL(request.path, 'http://local.backend');
|
|
162
179
|
const pathname = url.pathname;
|
|
163
180
|
if (method === 'GET' && pathname === '/api/v2/auth/me') {
|
|
181
|
+
const authProvider = getAuthProvider();
|
|
182
|
+
if (authProvider) {
|
|
183
|
+
if (!request.principal) {
|
|
184
|
+
return { status: 401, data: { detail: 'unauthorized' } };
|
|
185
|
+
}
|
|
186
|
+
return {
|
|
187
|
+
status: 200,
|
|
188
|
+
data: await authProvider.describeSelf(request.principal),
|
|
189
|
+
};
|
|
190
|
+
}
|
|
164
191
|
return { status: 200, data: buildLocalApiUser() };
|
|
165
192
|
}
|
|
166
193
|
// Desktop Agent —— 在本地模式下虚拟一个永远在线的 agent,所有 exec 直接走 ToolRouter
|
|
@@ -218,7 +245,7 @@ export class LocalBackendRouter {
|
|
|
218
245
|
if (method === 'GET' && pathname === '/api/v2/chats') {
|
|
219
246
|
const page = parsePositiveIntParam(url.searchParams.get('page'), 1);
|
|
220
247
|
const limit = parsePositiveIntParam(url.searchParams.get('limit'), 50);
|
|
221
|
-
const { chats, total } =
|
|
248
|
+
const { chats, total } = await this.store.listChats(page, limit);
|
|
222
249
|
return {
|
|
223
250
|
status: 200,
|
|
224
251
|
data: {
|
|
@@ -250,10 +277,10 @@ export class LocalBackendRouter {
|
|
|
250
277
|
const branchesMatch = pathname.match(/^\/api\/v2\/chats\/([^/]+)\/branches$/);
|
|
251
278
|
if (method === 'GET' && branchesMatch) {
|
|
252
279
|
const chatId = branchesMatch[1];
|
|
253
|
-
const chat =
|
|
280
|
+
const chat = await this.store.getChat(chatId);
|
|
254
281
|
if (!chat)
|
|
255
282
|
return { status: 404, data: { detail: 'chat not found' } };
|
|
256
|
-
const activeRecordId =
|
|
283
|
+
const activeRecordId = await this.store.getChatRecordId(chatId) ?? chatId;
|
|
257
284
|
const supervisor = getSidecarSupervisor();
|
|
258
285
|
const branches = supervisor ? await supervisor.sessionBranches(activeRecordId) : null;
|
|
259
286
|
return {
|
|
@@ -271,10 +298,10 @@ export class LocalBackendRouter {
|
|
|
271
298
|
const branchTreeMatch = pathname.match(/^\/api\/v2\/chats\/([^/]+)\/branches\/tree$/);
|
|
272
299
|
if (method === 'GET' && branchTreeMatch) {
|
|
273
300
|
const chatId = branchTreeMatch[1];
|
|
274
|
-
const chat =
|
|
301
|
+
const chat = await this.store.getChat(chatId);
|
|
275
302
|
if (!chat)
|
|
276
303
|
return { status: 404, data: { detail: 'chat not found' } };
|
|
277
|
-
const activeRecordId =
|
|
304
|
+
const activeRecordId = await this.store.getChatRecordId(chatId) ?? chatId;
|
|
278
305
|
const supervisor = getSidecarSupervisor();
|
|
279
306
|
const family = supervisor ? await supervisor.sessionTree(activeRecordId) : null;
|
|
280
307
|
return {
|
|
@@ -295,7 +322,7 @@ export class LocalBackendRouter {
|
|
|
295
322
|
const activateMatch = pathname.match(/^\/api\/v2\/chats\/([^/]+)\/branches\/activate$/);
|
|
296
323
|
if (method === 'POST' && activateMatch) {
|
|
297
324
|
const chatId = activateMatch[1];
|
|
298
|
-
const chat =
|
|
325
|
+
const chat = await this.store.getChat(chatId);
|
|
299
326
|
if (!chat)
|
|
300
327
|
return { status: 404, data: { detail: 'chat not found' } };
|
|
301
328
|
const payload = this.toRecord(request.body);
|
|
@@ -307,7 +334,7 @@ export class LocalBackendRouter {
|
|
|
307
334
|
if (!supervisor) {
|
|
308
335
|
return { status: 503, data: { detail: 'sidecar unavailable' } };
|
|
309
336
|
}
|
|
310
|
-
const activeRecordId =
|
|
337
|
+
const activeRecordId = await this.store.getChatRecordId(chatId) ?? chatId;
|
|
311
338
|
const family = await supervisor.sessionTree(activeRecordId);
|
|
312
339
|
const activation = resolveBranchActivation(activeRecordId, family?.tree ?? null, targetRecordId);
|
|
313
340
|
if (!activation.ok) {
|
|
@@ -317,8 +344,8 @@ export class LocalBackendRouter {
|
|
|
317
344
|
if (!projection) {
|
|
318
345
|
return { status: 404, data: { detail: `record not found: ${targetRecordId}` } };
|
|
319
346
|
}
|
|
320
|
-
|
|
321
|
-
|
|
347
|
+
await this.store.setChatRecordId(chatId, targetRecordId);
|
|
348
|
+
await this.store.replaceChatMessages(chatId, projection.messages
|
|
322
349
|
.filter((m) => m.role === 'user' || m.role === 'assistant')
|
|
323
350
|
.map((m) => ({ role: m.role, content: m.content })));
|
|
324
351
|
return {
|
|
@@ -342,7 +369,7 @@ export class LocalBackendRouter {
|
|
|
342
369
|
};
|
|
343
370
|
}
|
|
344
371
|
}
|
|
345
|
-
const created =
|
|
372
|
+
const created = await this.store.createChat('新对话', typeof payload.agentId === 'string' ? payload.agentId : getBrand().defaultAgentId, projectId);
|
|
346
373
|
return {
|
|
347
374
|
status: 200,
|
|
348
375
|
data: {
|
|
@@ -362,7 +389,7 @@ export class LocalBackendRouter {
|
|
|
362
389
|
const exceptChatId = typeof payload.exceptChatId === 'string' && payload.exceptChatId.trim()
|
|
363
390
|
? payload.exceptChatId.trim()
|
|
364
391
|
: null;
|
|
365
|
-
const deletedChatIds =
|
|
392
|
+
const deletedChatIds = await this.store.deleteEmptyChats(exceptChatId);
|
|
366
393
|
return { status: 200, data: { deletedChatIds } };
|
|
367
394
|
}
|
|
368
395
|
// ───── 项目模式:projects CRUD ─────
|
|
@@ -422,7 +449,7 @@ export class LocalBackendRouter {
|
|
|
422
449
|
if (!ok)
|
|
423
450
|
return this.notFound('项目不存在');
|
|
424
451
|
// 会话不删,只解绑——它们变成"无项目对话"。
|
|
425
|
-
const detached =
|
|
452
|
+
const detached = await this.store.clearProjectAssignment(projectId);
|
|
426
453
|
return { status: 200, data: { success: true, detachedChats: detached } };
|
|
427
454
|
}
|
|
428
455
|
}
|
|
@@ -458,17 +485,17 @@ export class LocalBackendRouter {
|
|
|
458
485
|
return { status: 503, data: { detail: 'task service unavailable' } };
|
|
459
486
|
}
|
|
460
487
|
const chatId = chatTasksMatch[1];
|
|
461
|
-
const chat =
|
|
488
|
+
const chat = await this.store.getChat(chatId);
|
|
462
489
|
if (!chat)
|
|
463
490
|
return { status: 404, data: { detail: 'chat not found' } };
|
|
464
|
-
return { status: 200, data: { tasks:
|
|
491
|
+
return { status: 200, data: { tasks: await this.store.listTasks(chatId) } };
|
|
465
492
|
}
|
|
466
493
|
const taskProcessMatch = pathname.match(/^\/api\/v2\/tasks\/([^/]+)\/process$/);
|
|
467
494
|
if (method === 'GET' && taskProcessMatch) {
|
|
468
495
|
if (!this.taskService) {
|
|
469
496
|
return { status: 503, data: { detail: 'task service unavailable' } };
|
|
470
497
|
}
|
|
471
|
-
const snapshot = this.taskService.getProcess(taskProcessMatch[1]);
|
|
498
|
+
const snapshot = await this.taskService.getProcess(taskProcessMatch[1]);
|
|
472
499
|
if (!snapshot)
|
|
473
500
|
return { status: 404, data: { detail: '任务不存在' } };
|
|
474
501
|
return {
|
|
@@ -506,7 +533,7 @@ export class LocalBackendRouter {
|
|
|
506
533
|
const projectContextMatch = pathname.match(/^\/api\/v2\/chats\/([^/]+)\/project-context$/);
|
|
507
534
|
if (projectContextMatch && method === 'GET') {
|
|
508
535
|
const chatId = projectContextMatch[1];
|
|
509
|
-
const chat =
|
|
536
|
+
const chat = await this.store.getChat(chatId);
|
|
510
537
|
if (!chat)
|
|
511
538
|
return this.notFound('Chat not found');
|
|
512
539
|
const registry = this.toolRouter.projectRegistry;
|
|
@@ -531,7 +558,7 @@ export class LocalBackendRouter {
|
|
|
531
558
|
if (chatMatch) {
|
|
532
559
|
const chatId = chatMatch[1];
|
|
533
560
|
if (method === 'GET') {
|
|
534
|
-
const chat =
|
|
561
|
+
const chat = await this.store.getChat(chatId);
|
|
535
562
|
if (!chat)
|
|
536
563
|
return this.notFound('Chat not found');
|
|
537
564
|
return {
|
|
@@ -552,13 +579,13 @@ export class LocalBackendRouter {
|
|
|
552
579
|
}
|
|
553
580
|
if (method === 'DELETE') {
|
|
554
581
|
if (url.searchParams.get('onlyIfEmpty') === '1') {
|
|
555
|
-
const deleted =
|
|
582
|
+
const deleted = await this.store.deleteChatIfEmpty(chatId);
|
|
556
583
|
return {
|
|
557
584
|
status: 200,
|
|
558
585
|
data: { success: true, deleted, chatId },
|
|
559
586
|
};
|
|
560
587
|
}
|
|
561
|
-
const ok =
|
|
588
|
+
const ok = await this.store.deleteChat(chatId);
|
|
562
589
|
if (!ok)
|
|
563
590
|
return this.notFound('Chat not found');
|
|
564
591
|
return {
|
|
@@ -575,7 +602,7 @@ export class LocalBackendRouter {
|
|
|
575
602
|
if (chatPinMatch && method === 'PUT') {
|
|
576
603
|
const chatId = chatPinMatch[1];
|
|
577
604
|
const payload = this.toRecord(request.body);
|
|
578
|
-
const updated =
|
|
605
|
+
const updated = await this.store.updateChat(chatId, { isPinned: Boolean(payload.isPinned) });
|
|
579
606
|
if (!updated)
|
|
580
607
|
return this.notFound('Chat not found');
|
|
581
608
|
return {
|
|
@@ -610,7 +637,7 @@ export class LocalBackendRouter {
|
|
|
610
637
|
return this.badRequest('projectId 必须是项目 id 字符串或 null');
|
|
611
638
|
}
|
|
612
639
|
}
|
|
613
|
-
const updated =
|
|
640
|
+
const updated = await this.store.updateChat(chatId, {
|
|
614
641
|
title: typeof payload.title === 'string' ? payload.title : undefined,
|
|
615
642
|
systemPrompt: typeof payload.systemPrompt === 'string' ? payload.systemPrompt : undefined,
|
|
616
643
|
pinnedRefs: Array.isArray(payload.pinnedRefs) ? payload.pinnedRefs : undefined,
|
|
@@ -638,7 +665,7 @@ export class LocalBackendRouter {
|
|
|
638
665
|
if (messageListMatch && method === 'GET') {
|
|
639
666
|
const chatId = messageListMatch[1];
|
|
640
667
|
const limit = parsePositiveIntParam(url.searchParams.get('limit'), 200);
|
|
641
|
-
const records =
|
|
668
|
+
const records = await this.store.listMessages(chatId, limit);
|
|
642
669
|
const messages = records.map(item => ({
|
|
643
670
|
id: item.id,
|
|
644
671
|
chatId: item.chatId,
|
|
@@ -655,7 +682,7 @@ export class LocalBackendRouter {
|
|
|
655
682
|
// 同一 record 上开出第二个写者)。listMessages 是 DESC,records[0]
|
|
656
683
|
// 即最后一条。
|
|
657
684
|
const interrupted = detectInterruptedTurn({
|
|
658
|
-
turnActive:
|
|
685
|
+
turnActive: await this.store.getTurnActive(chatId) !== null,
|
|
659
686
|
streamActive: getActiveCoreLoopStreamId(chatId) !== undefined,
|
|
660
687
|
lastMessageRole: records[0]?.role ?? null,
|
|
661
688
|
});
|
|
@@ -711,7 +738,7 @@ export class LocalBackendRouter {
|
|
|
711
738
|
}
|
|
712
739
|
if (method === 'GET' && pathname === '/api/v2/chat-agents') {
|
|
713
740
|
const includeArchived = url.searchParams.get('include_archived') === 'true';
|
|
714
|
-
const agents =
|
|
741
|
+
const agents = await this.store.listChatAgents(includeArchived);
|
|
715
742
|
return {
|
|
716
743
|
status: 200,
|
|
717
744
|
data: {
|
|
@@ -722,7 +749,7 @@ export class LocalBackendRouter {
|
|
|
722
749
|
}
|
|
723
750
|
if (method === 'POST' && pathname === '/api/v2/chat-agents') {
|
|
724
751
|
const payload = this.toRecord(request.body);
|
|
725
|
-
const agent =
|
|
752
|
+
const agent = await this.store.createChatAgent({
|
|
726
753
|
name: String(payload.name || '新助手'),
|
|
727
754
|
icon: typeof payload.icon === 'string' ? payload.icon : null,
|
|
728
755
|
color: typeof payload.color === 'string' ? payload.color : null,
|
|
@@ -1147,14 +1174,14 @@ export class LocalBackendRouter {
|
|
|
1147
1174
|
if (chatAgentMatch) {
|
|
1148
1175
|
const agentId = chatAgentMatch[1];
|
|
1149
1176
|
if (method === 'GET') {
|
|
1150
|
-
const agent =
|
|
1177
|
+
const agent = await this.store.getChatAgent(agentId);
|
|
1151
1178
|
if (!agent)
|
|
1152
1179
|
return this.notFound('Agent not found');
|
|
1153
1180
|
return { status: 200, data: agent };
|
|
1154
1181
|
}
|
|
1155
1182
|
if (method === 'PATCH') {
|
|
1156
1183
|
const payload = this.toRecord(request.body);
|
|
1157
|
-
const agent =
|
|
1184
|
+
const agent = await this.store.updateChatAgent(agentId, {
|
|
1158
1185
|
name: typeof payload.name === 'string' ? payload.name : undefined,
|
|
1159
1186
|
icon: typeof payload.icon === 'string' ? payload.icon : undefined,
|
|
1160
1187
|
color: typeof payload.color === 'string' ? payload.color : undefined,
|
|
@@ -1173,7 +1200,7 @@ export class LocalBackendRouter {
|
|
|
1173
1200
|
return { status: 200, data: { agent } };
|
|
1174
1201
|
}
|
|
1175
1202
|
if (method === 'DELETE') {
|
|
1176
|
-
const ok =
|
|
1203
|
+
const ok = await this.store.archiveChatAgent(agentId);
|
|
1177
1204
|
if (!ok)
|
|
1178
1205
|
return this.notFound('Agent not found');
|
|
1179
1206
|
return {
|
|
@@ -1376,12 +1403,12 @@ export class LocalBackendRouter {
|
|
|
1376
1403
|
if (method === 'GET') {
|
|
1377
1404
|
return {
|
|
1378
1405
|
status: 200,
|
|
1379
|
-
data:
|
|
1406
|
+
data: await this.store.getLlmSettings(),
|
|
1380
1407
|
};
|
|
1381
1408
|
}
|
|
1382
1409
|
if (method === 'POST') {
|
|
1383
1410
|
const payload = this.toRecord(request.body);
|
|
1384
|
-
const saved = llmService.setSettings({
|
|
1411
|
+
const saved = await llmService.setSettings({
|
|
1385
1412
|
provider: sanitizeLlmProvider(payload.provider),
|
|
1386
1413
|
vendorId: sanitizeVendorId(payload.vendorId),
|
|
1387
1414
|
model: String(payload.model || 'llama3.1:8b'),
|
|
@@ -1401,7 +1428,7 @@ export class LocalBackendRouter {
|
|
|
1401
1428
|
: undefined,
|
|
1402
1429
|
compat: sanitizeCompatOverrides(payload.compat),
|
|
1403
1430
|
presets: sanitizePresetsChoice(payload.presets),
|
|
1404
|
-
});
|
|
1431
|
+
}, this.store);
|
|
1405
1432
|
// 立即让新默认超时对后续 local_exec_shell 生效(重启后由 main.ts 启动时恢复)。
|
|
1406
1433
|
setDefaultExecTimeoutMs(saved.execTimeoutSeconds ? saved.execTimeoutSeconds * 1000 : null);
|
|
1407
1434
|
// 代理白名单是 boot 时按当时的 baseUrl 派生的,换了网关后本会话(含
|
|
@@ -1421,12 +1448,12 @@ export class LocalBackendRouter {
|
|
|
1421
1448
|
if (method === 'GET') {
|
|
1422
1449
|
return {
|
|
1423
1450
|
status: 200,
|
|
1424
|
-
data:
|
|
1451
|
+
data: await this.store.getTelemetrySettings(),
|
|
1425
1452
|
};
|
|
1426
1453
|
}
|
|
1427
1454
|
if (method === 'POST') {
|
|
1428
1455
|
const payload = this.toRecord(request.body);
|
|
1429
|
-
const saved =
|
|
1456
|
+
const saved = await this.store.setTelemetrySettings({
|
|
1430
1457
|
endpoint: typeof payload.endpoint === 'string' ? payload.endpoint : undefined,
|
|
1431
1458
|
privacyMode: payload.privacyMode === 'full' ? 'full' : 'metadata',
|
|
1432
1459
|
serviceName: typeof payload.serviceName === 'string' ? payload.serviceName : undefined,
|
|
@@ -1441,12 +1468,12 @@ export class LocalBackendRouter {
|
|
|
1441
1468
|
if (method === 'GET') {
|
|
1442
1469
|
return {
|
|
1443
1470
|
status: 200,
|
|
1444
|
-
data:
|
|
1471
|
+
data: await this.store.getWebSearchSettings(),
|
|
1445
1472
|
};
|
|
1446
1473
|
}
|
|
1447
1474
|
if (method === 'POST') {
|
|
1448
1475
|
const payload = this.toRecord(request.body);
|
|
1449
|
-
const saved =
|
|
1476
|
+
const saved = await this.store.setWebSearchSettings({
|
|
1450
1477
|
provider: payload.provider === 'ddg' ? 'ddg' : 'tavily',
|
|
1451
1478
|
apiKey: typeof payload.apiKey === 'string' ? payload.apiKey : undefined,
|
|
1452
1479
|
});
|
|
@@ -1458,10 +1485,10 @@ export class LocalBackendRouter {
|
|
|
1458
1485
|
}
|
|
1459
1486
|
if (pathname === '/api/v2/local-settings/insights') {
|
|
1460
1487
|
if (method === 'GET') {
|
|
1461
|
-
const settings =
|
|
1488
|
+
const settings = await this.store.ensureInsightsSettings();
|
|
1462
1489
|
return {
|
|
1463
1490
|
status: 200,
|
|
1464
|
-
data: { ...settings, stats:
|
|
1491
|
+
data: { ...settings, stats: await this.store.insightStats() },
|
|
1465
1492
|
};
|
|
1466
1493
|
}
|
|
1467
1494
|
if (method === 'POST') {
|
|
@@ -1490,20 +1517,20 @@ export class LocalBackendRouter {
|
|
|
1490
1517
|
patch.apiBase = payload.apiBase;
|
|
1491
1518
|
if (Object.keys(profilePatch).length)
|
|
1492
1519
|
patch.profile = profilePatch;
|
|
1493
|
-
const saved =
|
|
1520
|
+
const saved = await this.store.setInsightsSettings(patch);
|
|
1494
1521
|
const hasProfileFields = Boolean(saved.profile.displayName ||
|
|
1495
1522
|
saved.profile.email ||
|
|
1496
1523
|
saved.profile.company ||
|
|
1497
1524
|
saved.profile.note);
|
|
1498
1525
|
if (hasProfileFields) {
|
|
1499
|
-
recordInsightProfile(saved.profile);
|
|
1526
|
+
await recordInsightProfile(this.store, saved.profile);
|
|
1500
1527
|
}
|
|
1501
|
-
void flushInsightsOutbox().catch((err) => {
|
|
1528
|
+
void flushInsightsOutbox(this.store).catch((err) => {
|
|
1502
1529
|
console.warn('[insights] flush after settings save failed', err);
|
|
1503
1530
|
});
|
|
1504
1531
|
return {
|
|
1505
1532
|
status: 200,
|
|
1506
|
-
data: { ...saved, stats:
|
|
1533
|
+
data: { ...saved, stats: await this.store.insightStats() },
|
|
1507
1534
|
};
|
|
1508
1535
|
}
|
|
1509
1536
|
}
|
|
@@ -1516,19 +1543,19 @@ export class LocalBackendRouter {
|
|
|
1516
1543
|
const properties = payload.properties && typeof payload.properties === 'object'
|
|
1517
1544
|
? payload.properties
|
|
1518
1545
|
: {};
|
|
1519
|
-
recordInsightEvent(eventName, properties);
|
|
1546
|
+
await recordInsightEvent(this.store, eventName, properties);
|
|
1520
1547
|
return { status: 200, data: { status: 'ok' } };
|
|
1521
1548
|
}
|
|
1522
1549
|
if (method === 'GET' && pathname === '/api/v2/insights/export') {
|
|
1523
|
-
return { status: 200, data: buildInsightsExportPayload() };
|
|
1550
|
+
return { status: 200, data: await buildInsightsExportPayload(this.store) };
|
|
1524
1551
|
}
|
|
1525
1552
|
if (method === 'POST' && pathname === '/api/v2/insights/upload-local') {
|
|
1526
|
-
const ok = await uploadInsightsBundle();
|
|
1553
|
+
const ok = await uploadInsightsBundle(this.store);
|
|
1527
1554
|
return {
|
|
1528
1555
|
status: ok ? 200 : 502,
|
|
1529
1556
|
data: {
|
|
1530
1557
|
ok,
|
|
1531
|
-
stats:
|
|
1558
|
+
stats: await this.store.insightStats(),
|
|
1532
1559
|
detail: ok ? 'uploaded' : 'upload_failed_kept_local',
|
|
1533
1560
|
},
|
|
1534
1561
|
};
|
|
@@ -1537,7 +1564,7 @@ export class LocalBackendRouter {
|
|
|
1537
1564
|
const days = parsePositiveIntParam(url.searchParams.get('days'), 30);
|
|
1538
1565
|
return {
|
|
1539
1566
|
status: 200,
|
|
1540
|
-
data:
|
|
1567
|
+
data: await this.store.getUsageSummary(days),
|
|
1541
1568
|
};
|
|
1542
1569
|
}
|
|
1543
1570
|
if (method === 'POST' && pathname.match(/^\/api\/v2\/chats\/[^/]+\/suggest-replies$/)) {
|
|
@@ -1623,7 +1650,7 @@ export class LocalBackendRouter {
|
|
|
1623
1650
|
if (!chatId) {
|
|
1624
1651
|
return { status: 400, data: { detail: 'chatId is required' } };
|
|
1625
1652
|
}
|
|
1626
|
-
const traces =
|
|
1653
|
+
const traces = (await this.store.listTracesByChat(chatId, limit)).map(item => ({
|
|
1627
1654
|
...item,
|
|
1628
1655
|
payload: this.safeJson(item.payload, {}),
|
|
1629
1656
|
}));
|
|
@@ -1634,7 +1661,7 @@ export class LocalBackendRouter {
|
|
|
1634
1661
|
}
|
|
1635
1662
|
const traceMatch = pathname.match(/^\/api\/v2\/local\/traces\/([^/]+)$/);
|
|
1636
1663
|
if (traceMatch && method === 'GET') {
|
|
1637
|
-
const trace =
|
|
1664
|
+
const trace = await this.store.getTrace(traceMatch[1]);
|
|
1638
1665
|
if (!trace)
|
|
1639
1666
|
return this.notFound('Trace not found');
|
|
1640
1667
|
return {
|
|
@@ -1672,7 +1699,7 @@ export class LocalBackendRouter {
|
|
|
1672
1699
|
return { status: 200, data: { resolved: [] } };
|
|
1673
1700
|
}
|
|
1674
1701
|
const chatId = typeof payload.chatId === 'string' ? payload.chatId : '';
|
|
1675
|
-
const baseDir = (chatId ? this.resolveChatProject(chatId)?.folderPath : null) ?? os.homedir();
|
|
1702
|
+
const baseDir = (chatId ? (await this.resolveChatProject(chatId))?.folderPath : null) ?? os.homedir();
|
|
1676
1703
|
const resolved = await resolveMentionedPaths({ candidates, baseDir });
|
|
1677
1704
|
return { status: 200, data: { resolved } };
|
|
1678
1705
|
}
|
|
@@ -1734,7 +1761,10 @@ export class LocalBackendRouter {
|
|
|
1734
1761
|
* - 错误:保留 `event:error` + `data: {"message":"..."}`(前端会忽略,但日志/未来 UI 用)
|
|
1735
1762
|
* - 结束:标准 SSE `[DONE]` 串。
|
|
1736
1763
|
*/
|
|
1737
|
-
|
|
1764
|
+
handleStream(request, emit, options = {}) {
|
|
1765
|
+
return this.storeContext.run(this.resolveStore(request.principal), () => this.handleStreamScoped(request, emit, options));
|
|
1766
|
+
}
|
|
1767
|
+
async handleStreamScoped(request, emit, options = {}) {
|
|
1738
1768
|
const signal = options.signal;
|
|
1739
1769
|
const url = new URL(request.path, 'http://local.backend');
|
|
1740
1770
|
const pathname = url.pathname;
|
|
@@ -1750,7 +1780,7 @@ export class LocalBackendRouter {
|
|
|
1750
1780
|
}
|
|
1751
1781
|
const chatId = sendMatch ? sendMatch[1] : regenerateMatch[1];
|
|
1752
1782
|
const payload = this.toRecord(request.body);
|
|
1753
|
-
let chat =
|
|
1783
|
+
let chat = await this.store.getChat(chatId);
|
|
1754
1784
|
if (!chat) {
|
|
1755
1785
|
// 会话 URL 即会话身份:外部平台(自动化/eval)把一个 chatId 当成一次
|
|
1756
1786
|
// 会话任务反复打开运行,本地会话却可能已经被用户清理、被空会话 prune、
|
|
@@ -1766,7 +1796,7 @@ export class LocalBackendRouter {
|
|
|
1766
1796
|
const agentId = typeof payload.agentId === 'string' && payload.agentId.trim()
|
|
1767
1797
|
? payload.agentId.trim()
|
|
1768
1798
|
: 'local-assistant';
|
|
1769
|
-
chat =
|
|
1799
|
+
chat = await this.store.createChatWithId(chatId, '新对话', agentId);
|
|
1770
1800
|
console.warn('[local-backend] stream: chat missing, auto-provisioned', {
|
|
1771
1801
|
chatId,
|
|
1772
1802
|
agentId,
|
|
@@ -1804,7 +1834,7 @@ export class LocalBackendRouter {
|
|
|
1804
1834
|
// the *real* request instead of a generic placeholder (matters for
|
|
1805
1835
|
// deferred-exec detection, mode preamble, etc., which all key off the
|
|
1806
1836
|
// actual ask).
|
|
1807
|
-
const priorMessages =
|
|
1837
|
+
const priorMessages = (await this.store.listMessages(chatId, 200)).reverse();
|
|
1808
1838
|
const resolution = resolveRegenerateContext(priorMessages, targetMessageId);
|
|
1809
1839
|
if (!resolution.ok) {
|
|
1810
1840
|
emit(this.sse('error', { message: 'regenerate target message not found or not an assistant message' }));
|
|
@@ -1823,13 +1853,13 @@ export class LocalBackendRouter {
|
|
|
1823
1853
|
const supervisor = getSidecarSupervisor();
|
|
1824
1854
|
const outcome = supervisor
|
|
1825
1855
|
? await supervisor.forkSession({
|
|
1826
|
-
recordId:
|
|
1856
|
+
recordId: await this.store.getChatRecordId(chatId) ?? chatId,
|
|
1827
1857
|
beforeUserIndex: resolveRegenerateForkOrdinal(priorMessages, targetMessageId),
|
|
1828
1858
|
newRecordId: `${chatId}:r${Date.now().toString(36)}`,
|
|
1829
1859
|
})
|
|
1830
1860
|
: null;
|
|
1831
1861
|
if (outcome?.ok) {
|
|
1832
|
-
|
|
1862
|
+
await this.store.setChatRecordId(chatId, outcome.fork.recordId);
|
|
1833
1863
|
console.log('[local-backend] regenerate: record forked (old tail preserved)', {
|
|
1834
1864
|
chatId,
|
|
1835
1865
|
branchRecordId: outcome.fork.recordId,
|
|
@@ -1848,7 +1878,7 @@ export class LocalBackendRouter {
|
|
|
1848
1878
|
emit(this.sse('error', { message: plan.message }));
|
|
1849
1879
|
return { status: 409 };
|
|
1850
1880
|
}
|
|
1851
|
-
const deleted =
|
|
1881
|
+
const deleted = await this.store.deleteMessagesFrom(chatId, targetMessageId);
|
|
1852
1882
|
console.log('[local-backend] regenerate: truncated history', {
|
|
1853
1883
|
chatId,
|
|
1854
1884
|
targetMessageId,
|
|
@@ -1860,9 +1890,9 @@ export class LocalBackendRouter {
|
|
|
1860
1890
|
// sidecar 回放 durable record 的投影作为循环种子。只在检测确为
|
|
1861
1891
|
// interrupted 时接受:对一个已正常完结的 turn 续跑只会给完整对话
|
|
1862
1892
|
// 凭空多接一段回复。
|
|
1863
|
-
const prior =
|
|
1893
|
+
const prior = await this.store.listMessages(chatId, 200);
|
|
1864
1894
|
const interrupted = detectInterruptedTurn({
|
|
1865
|
-
turnActive:
|
|
1895
|
+
turnActive: await this.store.getTurnActive(chatId) !== null,
|
|
1866
1896
|
streamActive: getActiveCoreLoopStreamId(chatId) !== undefined,
|
|
1867
1897
|
lastMessageRole: prior[0]?.role ?? null,
|
|
1868
1898
|
});
|
|
@@ -1893,8 +1923,8 @@ export class LocalBackendRouter {
|
|
|
1893
1923
|
// 3. 历史里目前没有任何 assistant 消息——也就是说,本轮即将产生的是第一条助手回复
|
|
1894
1924
|
// 这避免在已经聊到一半的对话里突然把标题换掉。listMessages 是 DESC,limit 拉
|
|
1895
1925
|
// 大一点确保看到完整历史(200 已经覆盖绝大多数本地会话)。
|
|
1896
|
-
const existingAssistant =
|
|
1897
|
-
.listMessages(chatId, 200)
|
|
1926
|
+
const existingAssistant = (await this.store
|
|
1927
|
+
.listMessages(chatId, 200))
|
|
1898
1928
|
.some((m) => m.role === 'assistant');
|
|
1899
1929
|
const shouldGenerateTitle = !regenerateMatch &&
|
|
1900
1930
|
!isResume &&
|
|
@@ -1915,7 +1945,7 @@ export class LocalBackendRouter {
|
|
|
1915
1945
|
// persisted (and already in the durable record).
|
|
1916
1946
|
let currentUserMessageId;
|
|
1917
1947
|
if (!regenerateMatch && !isResume) {
|
|
1918
|
-
const userMessage =
|
|
1948
|
+
const userMessage = await this.store.addMessage(chatId, 'user', userMessageText);
|
|
1919
1949
|
currentUserMessageId = userMessage.id;
|
|
1920
1950
|
emit(this.sseData({ type: 'user_message', message: userMessage }));
|
|
1921
1951
|
}
|
|
@@ -1924,7 +1954,7 @@ export class LocalBackendRouter {
|
|
|
1924
1954
|
const chatMode = payload.mode === 'plan' ? 'plan' : 'agent';
|
|
1925
1955
|
// 本轮生效的智能体:人设、技能勾选、工具策略同源。必须先解析——工具
|
|
1926
1956
|
// 策略决定工具列表,工具列表又决定技能的触发条件。
|
|
1927
|
-
const turnAgents = this.resolveTurnAgents(chatId, payload);
|
|
1957
|
+
const turnAgents = await this.resolveTurnAgents(chatId, payload);
|
|
1928
1958
|
// Wave 2 工具分层:模型可见列表只出 direct 层(内置工具 + tool_search
|
|
1929
1959
|
// 发现缝);MCP 动态工具全在 deferred 层,经 tool_search 命中即调。
|
|
1930
1960
|
// 附带收益:MCP 目录增删不再改动系统提示词里的工具名录,前缀更稳。
|
|
@@ -2036,7 +2066,7 @@ export class LocalBackendRouter {
|
|
|
2036
2066
|
});
|
|
2037
2067
|
return;
|
|
2038
2068
|
}
|
|
2039
|
-
const fresh =
|
|
2069
|
+
const fresh = await this.store.getChat(chatId);
|
|
2040
2070
|
if (!fresh) {
|
|
2041
2071
|
console.log('[local-backend] title-gen skip', {
|
|
2042
2072
|
chatId,
|
|
@@ -2052,7 +2082,7 @@ export class LocalBackendRouter {
|
|
|
2052
2082
|
});
|
|
2053
2083
|
return;
|
|
2054
2084
|
}
|
|
2055
|
-
const updated =
|
|
2085
|
+
const updated = await this.store.updateChat(chatId, { title: result.title });
|
|
2056
2086
|
if (!updated) {
|
|
2057
2087
|
console.warn('[local-backend] title-gen write failed', { chatId });
|
|
2058
2088
|
return;
|
|
@@ -2081,8 +2111,8 @@ export class LocalBackendRouter {
|
|
|
2081
2111
|
* Public: main.ts 的反向通道(reverse-tools.ts)经它给 CoreLoop 路径的
|
|
2082
2112
|
* 工具调用补上 projectRoot 围栏。
|
|
2083
2113
|
*/
|
|
2084
|
-
resolveChatProject(chatId) {
|
|
2085
|
-
const projectId =
|
|
2114
|
+
async resolveChatProject(chatId) {
|
|
2115
|
+
const projectId = (await this.store.getChat(chatId))?.projectId;
|
|
2086
2116
|
if (!projectId)
|
|
2087
2117
|
return null;
|
|
2088
2118
|
const project = this.toolRouter.projectRegistry?.get(projectId);
|
|
@@ -2103,9 +2133,9 @@ export class LocalBackendRouter {
|
|
|
2103
2133
|
*
|
|
2104
2134
|
* @param chatId 当前对话 id。
|
|
2105
2135
|
* @param payload 前端提交的流请求体。
|
|
2106
|
-
* @returns
|
|
2136
|
+
* @returns 本轮的智能体顺序、人设智能体、自称,以及合并后的能力面。
|
|
2107
2137
|
*/
|
|
2108
|
-
resolveTurnAgents(chatId, payload) {
|
|
2138
|
+
async resolveTurnAgents(chatId, payload) {
|
|
2109
2139
|
const mentionedAgentIds = Array.isArray(payload.mentionedAgentIds)
|
|
2110
2140
|
? payload.mentionedAgentIds.filter((s) => typeof s === 'string')
|
|
2111
2141
|
: [];
|
|
@@ -2117,19 +2147,20 @@ export class LocalBackendRouter {
|
|
|
2117
2147
|
].filter((id, idx, arr) => arr.indexOf(id) === idx);
|
|
2118
2148
|
if (orderedAgentIds.length === 0) {
|
|
2119
2149
|
const fallbackId = (typeof payload.agentId === 'string' && payload.agentId) ||
|
|
2120
|
-
|
|
2150
|
+
(await this.store.getChat(chatId))?.agentId ||
|
|
2121
2151
|
null;
|
|
2122
2152
|
if (fallbackId)
|
|
2123
2153
|
orderedAgentIds.push(fallbackId);
|
|
2124
2154
|
}
|
|
2125
|
-
const agents = orderedAgentIds
|
|
2126
|
-
.map((id) => localStore.getChatAgent(id))
|
|
2155
|
+
const agents = (await Promise.all(orderedAgentIds.map((id) => this.store.getChatAgent(id))))
|
|
2127
2156
|
.filter((agent) => Boolean(agent));
|
|
2128
2157
|
return {
|
|
2129
2158
|
orderedAgentIds,
|
|
2130
2159
|
// 能力面来自全部解析到的智能体,人设前言只用配了 rolePrompt 的那些——
|
|
2131
|
-
//
|
|
2160
|
+
// 自建智能体可以只勾技能、不写人设。自称始终跟第一个解析到的智能体,
|
|
2161
|
+
// 不要求写了人设——否则没 rolePrompt 的角色会回落成产品品牌。
|
|
2132
2162
|
personaAgents: agents.filter((agent) => Boolean(agent.rolePrompt)),
|
|
2163
|
+
identityName: agents[0]?.name.trim() || null,
|
|
2133
2164
|
capability: mergeAgentCapabilities(agents),
|
|
2134
2165
|
};
|
|
2135
2166
|
}
|
|
@@ -2140,7 +2171,7 @@ export class LocalBackendRouter {
|
|
|
2140
2171
|
// 摘要,也不再按 40 条窗口截断——让框架 record 累积完整对话并按需压缩。
|
|
2141
2172
|
// 上限即存储层 listMessages 的 1000 条硬上限;超出时最旧历史随
|
|
2142
2173
|
// host_revision 优雅退化。
|
|
2143
|
-
let rawHistory =
|
|
2174
|
+
let rawHistory = (await this.store.listMessages(chatId, HISTORY_SEED_LIMIT)).reverse();
|
|
2144
2175
|
// 剔除刚刚写入数据库的当前轮用户消息,避免在上下文历史中与我们显式添加的
|
|
2145
2176
|
// latestUserMessage 发生重复。send 路径按 id 精确剔除——上一条 assistant
|
|
2146
2177
|
// 回复在流结束后异步落库(createdAt 取落库时刻),快速连续发送时它会排到
|
|
@@ -2185,7 +2216,7 @@ export class LocalBackendRouter {
|
|
|
2185
2216
|
})))
|
|
2186
2217
|
.filter(Boolean);
|
|
2187
2218
|
const settings = llmService.getSettings();
|
|
2188
|
-
const { personaAgents, capability } = turnAgents;
|
|
2219
|
+
const { personaAgents, capability, identityName } = turnAgents;
|
|
2189
2220
|
let personaPreamble = '';
|
|
2190
2221
|
if (personaAgents.length === 1) {
|
|
2191
2222
|
personaPreamble = `【当前角色】${personaAgents[0].name}\n${personaAgents[0].rolePrompt}`;
|
|
@@ -2253,6 +2284,7 @@ export class LocalBackendRouter {
|
|
|
2253
2284
|
excludeSkillNames,
|
|
2254
2285
|
pinnedSkillNames: capability.pinnedSkills,
|
|
2255
2286
|
personaPreamble: effectivePersonaPreamble,
|
|
2287
|
+
identityName,
|
|
2256
2288
|
realityCheckSuffix: realityCheck,
|
|
2257
2289
|
forcedMcpTool,
|
|
2258
2290
|
ignoreConditions: capability.loadAllSkills,
|
|
@@ -2281,7 +2313,7 @@ export class LocalBackendRouter {
|
|
|
2281
2313
|
// 项目模式:绑定项目的对话在系统提示末尾追加沙箱约定(两条拼装路径
|
|
2282
2314
|
// 都追加)。硬围栏在 ToolRouter/LocalExecutor,这里让模型事先知道边界,
|
|
2283
2315
|
// 减少越界尝试。
|
|
2284
|
-
const chatProject = this.resolveChatProject(chatId);
|
|
2316
|
+
const chatProject = await this.resolveChatProject(chatId);
|
|
2285
2317
|
if (chatProject) {
|
|
2286
2318
|
systemPrompt +=
|
|
2287
2319
|
`\n\n【项目模式】当前对话绑定项目「${chatProject.name}」,根目录:${chatProject.folderPath}\n` +
|
|
@@ -2293,7 +2325,7 @@ export class LocalBackendRouter {
|
|
|
2293
2325
|
// 仅在用户显式信任该项目后才注入模型上下文——未信任一律不读取、不注入
|
|
2294
2326
|
// (打开恶意仓库时,一段构造的规则文件不能劫持 agent)。信任状态持久化、
|
|
2295
2327
|
// 可在设置里撤销;每回合重新读盘,规则保存即生效。
|
|
2296
|
-
const projectId =
|
|
2328
|
+
const projectId = (await this.store.getChat(chatId))?.projectId;
|
|
2297
2329
|
const registry = this.toolRouter.projectRegistry;
|
|
2298
2330
|
if (projectId && registry?.isTrusted(projectId)) {
|
|
2299
2331
|
const rules = loadProjectRuleFiles(chatProject.folderPath);
|
|
@@ -2319,7 +2351,10 @@ export class LocalBackendRouter {
|
|
|
2319
2351
|
const forcedSkill = await findSkill(forcedSkillName, { exclude: forcedSkillExcludes });
|
|
2320
2352
|
if (forcedSkill) {
|
|
2321
2353
|
// 场景包的正文占位符变量(如包技能正文里的工作区路径占位符)。
|
|
2322
|
-
finalUserContent = `${buildForcedSkillMessage(forcedSkill,
|
|
2354
|
+
finalUserContent = `${buildForcedSkillMessage(forcedSkill, {
|
|
2355
|
+
...brandSkillVars({ identityName }),
|
|
2356
|
+
...collectPackForcedSkillVars(chatId),
|
|
2357
|
+
})}\n\n---\n\n${latestUserMessage}`;
|
|
2323
2358
|
}
|
|
2324
2359
|
}
|
|
2325
2360
|
// W6-3 多模态:渲染进程把图片附件以 metadata.images({path,name})传上来,
|
|
@@ -2371,14 +2406,14 @@ export class LocalBackendRouter {
|
|
|
2371
2406
|
.slice(0, MAX_REFERENCED_CHATS);
|
|
2372
2407
|
const out = [];
|
|
2373
2408
|
for (const refChatId of ids) {
|
|
2374
|
-
const refChat =
|
|
2409
|
+
const refChat = await this.store.getChat(refChatId);
|
|
2375
2410
|
if (!refChat) {
|
|
2376
2411
|
console.warn('[local-backend] referenced chat not found, skipping', { refChatId });
|
|
2377
2412
|
continue;
|
|
2378
2413
|
}
|
|
2379
2414
|
const sections = [];
|
|
2380
|
-
const recent = (await Promise.all(
|
|
2381
|
-
.listMessages(refChatId, RECENT_MESSAGES_PER_CHAT)
|
|
2415
|
+
const recent = (await Promise.all((await this.store
|
|
2416
|
+
.listMessages(refChatId, RECENT_MESSAGES_PER_CHAT))
|
|
2382
2417
|
.reverse()
|
|
2383
2418
|
.map(async (item) => {
|
|
2384
2419
|
if (item.role !== 'user' && item.role !== 'assistant')
|
|
@@ -2753,7 +2788,7 @@ export class LocalBackendRouter {
|
|
|
2753
2788
|
// empty list, so writes confine to system scratch dirs only.
|
|
2754
2789
|
// `execPolicy: 'full'`(输入框「完整权限」)关闭这一层,让 mkdir
|
|
2755
2790
|
// Downloads 这类项目外写入不再被 Seatbelt 拦成 Operation not permitted。
|
|
2756
|
-
const chatProject = this.resolveChatProject(chatId);
|
|
2791
|
+
const chatProject = await this.resolveChatProject(chatId);
|
|
2757
2792
|
const execSandbox = buildExecSandbox([
|
|
2758
2793
|
// 场景包声明的每会话可写根(如文档包在项目根之外落盘产物的
|
|
2759
2794
|
// 工作区)。
|
|
@@ -2789,7 +2824,7 @@ export class LocalBackendRouter {
|
|
|
2789
2824
|
// 下方的清除点,残留的标记就是「上次回复被中断」的签名(与活进程写下的
|
|
2790
2825
|
// cancelled/failed 终态区分)。标记只覆盖「已交给 sidecar」的阶段:503
|
|
2791
2826
|
// (sidecar 未运行)在上方已 return,record 里没有这一轮,无可续跑。
|
|
2792
|
-
|
|
2827
|
+
await this.store.setTurnActive(chatId);
|
|
2793
2828
|
// 实时快照:executedActions / timeline / children 传引用(原地 mutate),
|
|
2794
2829
|
// content 由 onText 逐段更新。切走再切回的 renderer 靠 GET /live-stream 读它。
|
|
2795
2830
|
const live = registerLiveStream(chatId, { executedActions, timeline, children });
|
|
@@ -2809,7 +2844,7 @@ export class LocalBackendRouter {
|
|
|
2809
2844
|
chatId,
|
|
2810
2845
|
// W5-2: after a regenerate-fork the chat's live record is the
|
|
2811
2846
|
// branch; undefined falls back to chatId on the sidecar.
|
|
2812
|
-
recordId:
|
|
2847
|
+
recordId: await this.store.getChatRecordId(chatId) ?? undefined,
|
|
2813
2848
|
systemPrompt,
|
|
2814
2849
|
messages,
|
|
2815
2850
|
resume,
|
|
@@ -2993,14 +3028,14 @@ export class LocalBackendRouter {
|
|
|
2993
3028
|
: // resume 要求 messages 为空:记录是唯一权威,再喂一份历史会
|
|
2994
3029
|
// 静默分叉(sidecar 对此 fail loud)。
|
|
2995
3030
|
{ ...coreLoopOptions, resume: true, messages: [] }),
|
|
2996
|
-
afterPass: (outcome) => {
|
|
3031
|
+
afterPass: async (outcome) => {
|
|
2997
3032
|
if (outcome.traceId)
|
|
2998
3033
|
passTraceIds.push(outcome.traceId);
|
|
2999
3034
|
// W6-9 用量归因:落一条 chat 用量事件(含成本估算,无单价模型 costUsd=null)。
|
|
3000
3035
|
// 每趟各落一条——续跑的花费是真实发生的,不该被合并掉。
|
|
3001
3036
|
if (outcome.usage) {
|
|
3002
3037
|
try {
|
|
3003
|
-
|
|
3038
|
+
await this.store.recordUsageEvent({
|
|
3004
3039
|
chatId,
|
|
3005
3040
|
kind: 'chat',
|
|
3006
3041
|
provider: settings.provider,
|
|
@@ -3054,7 +3089,7 @@ export class LocalBackendRouter {
|
|
|
3054
3089
|
passTraceIds.push(failureTraceId);
|
|
3055
3090
|
}
|
|
3056
3091
|
}
|
|
3057
|
-
const durationMs = turnDurationMs(
|
|
3092
|
+
const durationMs = turnDurationMs((await this.store.getTurnActive(chatId))?.startedAt, Date.now());
|
|
3058
3093
|
// 回合产物文件列表(成功/失败/取消都收集——半截回合写出的文件同样是
|
|
3059
3094
|
// 产物)。收集失败只记日志,永远不该拖垮回合收尾。
|
|
3060
3095
|
let turnFiles = [];
|
|
@@ -3069,7 +3104,7 @@ export class LocalBackendRouter {
|
|
|
3069
3104
|
catch (turnFilesErr) {
|
|
3070
3105
|
console.warn('[local-backend] collect turn files failed', turnFilesErr);
|
|
3071
3106
|
}
|
|
3072
|
-
const assistant =
|
|
3107
|
+
const assistant = await this.store.addMessage(chatId, 'assistant', assistantText, JSON.stringify({
|
|
3073
3108
|
completionStatus,
|
|
3074
3109
|
completionReason,
|
|
3075
3110
|
executedActions,
|
|
@@ -3082,7 +3117,7 @@ export class LocalBackendRouter {
|
|
|
3082
3117
|
...(turnFiles.length > 0 ? { turnFiles } : {}),
|
|
3083
3118
|
}));
|
|
3084
3119
|
try {
|
|
3085
|
-
recordInsightTurn({
|
|
3120
|
+
await recordInsightTurn(this.store, {
|
|
3086
3121
|
chatId,
|
|
3087
3122
|
mode: chatMode,
|
|
3088
3123
|
modelId: overrideModel || settings.model,
|
|
@@ -3099,7 +3134,7 @@ export class LocalBackendRouter {
|
|
|
3099
3134
|
// W7-1: 回复落库后才清标记(顺序不能反——先清后写的话,两者之间崩溃
|
|
3100
3135
|
// 会丢回复且无任何续跑入口;这个顺序的最坏情况是残留标记被检测器的
|
|
3101
3136
|
// 「末尾已是 assistant」规则判定为已完结)。
|
|
3102
|
-
|
|
3137
|
+
await this.store.clearTurnActive(chatId);
|
|
3103
3138
|
// 回合落库即结束——移除实时快照,后续 GET /live-stream 返回 active=false。
|
|
3104
3139
|
removeLiveStream(chatId);
|
|
3105
3140
|
// Persist the sidecar-recorded traces into harness_traces so the CoreLoop
|
|
@@ -3108,7 +3143,7 @@ export class LocalBackendRouter {
|
|
|
3108
3143
|
// 一趟一条:自动续跑的每一趟都是一次独立的 sidecar 运行,全部挂到本轮
|
|
3109
3144
|
// 那条 assistant 消息上(message_id 无唯一约束),否则中间趟的 trace 就
|
|
3110
3145
|
// 从消息侧不可达了。
|
|
3111
|
-
const telemetry =
|
|
3146
|
+
const telemetry = await this.store.getTelemetrySettings();
|
|
3112
3147
|
for (const [index, traceId] of passTraceIds.entries()) {
|
|
3113
3148
|
// 只有最后一趟携带本轮终态;之前每一趟都是撞墙截停才有下一趟。
|
|
3114
3149
|
const passStatus = index === passTraceIds.length - 1 ? completionStatus : 'budget_exhausted';
|
|
@@ -3118,7 +3153,7 @@ export class LocalBackendRouter {
|
|
|
3118
3153
|
const durationMs = typeof sidecarTrace.durationMs === 'number'
|
|
3119
3154
|
? sidecarTrace.durationMs
|
|
3120
3155
|
: null;
|
|
3121
|
-
|
|
3156
|
+
await this.store.saveTrace({
|
|
3122
3157
|
id: traceId,
|
|
3123
3158
|
chatId,
|
|
3124
3159
|
messageId: assistant.id,
|
|
@@ -3200,11 +3235,11 @@ export class LocalBackendRouter {
|
|
|
3200
3235
|
}
|
|
3201
3236
|
})();
|
|
3202
3237
|
}
|
|
3203
|
-
publishSuggestedReplies(chatId, messageId, suggestions) {
|
|
3238
|
+
async publishSuggestedReplies(chatId, messageId, suggestions) {
|
|
3204
3239
|
if (suggestions.length === 0)
|
|
3205
3240
|
return;
|
|
3206
3241
|
try {
|
|
3207
|
-
|
|
3242
|
+
await this.store.patchMessageMetadata(chatId, messageId, { suggestedReplies: suggestions });
|
|
3208
3243
|
}
|
|
3209
3244
|
catch (err) {
|
|
3210
3245
|
console.warn('[local-backend] suggested-replies persist failed', { chatId, messageId, err });
|