@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
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: identity
|
|
3
|
-
description: Defines the
|
|
3
|
+
description: Defines the core role, working environment, and language convention. Always loaded as the foundational skill. {agentName} is the current chat agent display name (product brand if none).
|
|
4
4
|
priority: 1000
|
|
5
5
|
tags: [identity, base]
|
|
6
6
|
---
|
|
@@ -29,7 +29,7 @@ import type { ToolRouter } from '../tool-router.js';
|
|
|
29
29
|
import { streamCoreLoopTurn } from './coreloop-stream.js';
|
|
30
30
|
import type { WorktreeService } from './worktree-service.js';
|
|
31
31
|
import { type PersistedTurnBlock } from './turn-timeline.js';
|
|
32
|
-
/**
|
|
32
|
+
/** 任务表的最小异步读写面(生产注入 scoped store;测试注入内存实现)。 */
|
|
33
33
|
export interface TaskStore {
|
|
34
34
|
createTask(input: {
|
|
35
35
|
chatId: string;
|
|
@@ -39,22 +39,22 @@ export interface TaskStore {
|
|
|
39
39
|
recordId?: string | null;
|
|
40
40
|
dependsOn?: string[] | null;
|
|
41
41
|
initialStatus?: 'blocked' | 'running';
|
|
42
|
-
}): TaskRecord
|
|
43
|
-
getTask(taskId: string): TaskRecord | null
|
|
44
|
-
listTasks(chatId?: string, limit?: number): TaskRecord[]
|
|
45
|
-
updateTask(taskId: string, updates: Partial<Pick<TaskRecord, 'status' | 'answer' | 'error' | 'worktreeState' | 'traceId' | 'recordId'>>): TaskRecord | null
|
|
42
|
+
}): Promise<TaskRecord>;
|
|
43
|
+
getTask(taskId: string): Promise<TaskRecord | null>;
|
|
44
|
+
listTasks(chatId?: string, limit?: number): Promise<TaskRecord[]>;
|
|
45
|
+
updateTask(taskId: string, updates: Partial<Pick<TaskRecord, 'status' | 'answer' | 'error' | 'worktreeState' | 'traceId' | 'recordId'>>): Promise<TaskRecord | null>;
|
|
46
46
|
/** 回写推理时间线;缺省实现可为空(测试假 store 内存覆盖)。 */
|
|
47
|
-
saveTaskProcess?(taskId: string, processJson: string): void
|
|
47
|
+
saveTaskProcess?(taskId: string, processJson: string): Promise<void>;
|
|
48
48
|
}
|
|
49
49
|
export interface TaskServiceDeps {
|
|
50
|
-
store: TaskStore;
|
|
50
|
+
store: TaskStore | (() => TaskStore);
|
|
51
51
|
toolRouter: ToolRouter;
|
|
52
52
|
worktreeService: WorktreeService;
|
|
53
53
|
/** chatId → 绑定项目(无项目对话返回 null)。与 router.resolveChatProject 同源。 */
|
|
54
|
-
resolveChatProject: (chatId: string) => {
|
|
54
|
+
resolveChatProject: (chatId: string) => Promise<{
|
|
55
55
|
name: string;
|
|
56
56
|
folderPath: string;
|
|
57
|
-
} | null
|
|
57
|
+
} | null>;
|
|
58
58
|
/** 任务终态 / 推理过程广播(renderer 任务面板与右侧过程栏)。 */
|
|
59
59
|
broadcast?: (eventName: 'task-updated' | 'task-process', payload: Record<string, unknown>) => void;
|
|
60
60
|
/** 测试缝:替换真实流驱动(默认 streamCoreLoopTurn)。 */
|
|
@@ -68,6 +68,7 @@ export declare class TaskService {
|
|
|
68
68
|
private readonly deps;
|
|
69
69
|
private readonly running;
|
|
70
70
|
constructor(deps: TaskServiceDeps);
|
|
71
|
+
private get store();
|
|
71
72
|
/**
|
|
72
73
|
* task_run 工具的实现:落任务表 + 点火独立流,立即返回。
|
|
73
74
|
* 任何同步可知的失败(无 sidecar、worktree 建不起来)都抛给工具层,
|
|
@@ -113,21 +114,21 @@ export declare class TaskService {
|
|
|
113
114
|
private cancelProcessFlush;
|
|
114
115
|
private executeTask;
|
|
115
116
|
/** task_status 工具:单任务或本 chat 全部任务的快照。 */
|
|
116
|
-
status(chatId: string, taskId?: string): Record<string, unknown
|
|
117
|
+
status(chatId: string, taskId?: string): Promise<Record<string, unknown>>;
|
|
117
118
|
/** task_result 工具:取终态任务的完整结果。 */
|
|
118
|
-
result(chatId: string, taskId: string): Record<string, unknown
|
|
119
|
+
result(chatId: string, taskId: string): Promise<Record<string, unknown>>;
|
|
119
120
|
/** 喂给模型的任务视图(完整字段,answer 截断防爆上下文)。 */
|
|
120
121
|
private toModelView;
|
|
121
122
|
/**
|
|
122
123
|
* 右侧过程栏:当前推理时间线。running 且进程内有流 = live;
|
|
123
124
|
* 表上 running 但本进程没流 = 上次崩溃残留(stale)。
|
|
124
125
|
*/
|
|
125
|
-
getProcess(taskId: string): {
|
|
126
|
+
getProcess(taskId: string): Promise<{
|
|
126
127
|
task: TaskRecord;
|
|
127
128
|
timeline: PersistedTurnBlock[];
|
|
128
129
|
live: boolean;
|
|
129
130
|
stale: boolean;
|
|
130
|
-
} | null
|
|
131
|
+
} | null>;
|
|
131
132
|
/** 合并任务 worktree 到主仓(UI「合并到主仓」按钮的路由实现)。 */
|
|
132
133
|
mergeTaskWorktree(taskId: string): Promise<TaskRecord>;
|
|
133
134
|
/** 丢弃任务 worktree(UI「丢弃」按钮的路由实现)。 */
|
|
@@ -55,6 +55,9 @@ export class TaskService {
|
|
|
55
55
|
constructor(deps) {
|
|
56
56
|
this.deps = deps;
|
|
57
57
|
}
|
|
58
|
+
get store() {
|
|
59
|
+
return typeof this.deps.store === 'function' ? this.deps.store() : this.deps.store;
|
|
60
|
+
}
|
|
58
61
|
/**
|
|
59
62
|
* task_run 工具的实现:落任务表 + 点火独立流,立即返回。
|
|
60
63
|
* 任何同步可知的失败(无 sidecar、worktree 建不起来)都抛给工具层,
|
|
@@ -76,13 +79,13 @@ export class TaskService {
|
|
|
76
79
|
const dependsOn = input.dependsOn?.length ? input.dependsOn : null;
|
|
77
80
|
let blockedBy = [];
|
|
78
81
|
if (dependsOn) {
|
|
79
|
-
const deps = dependsOn.map((id) => {
|
|
80
|
-
const dep = this.
|
|
82
|
+
const deps = await Promise.all(dependsOn.map(async (id) => {
|
|
83
|
+
const dep = await this.store.getTask(id);
|
|
81
84
|
if (!dep || dep.chatId !== input.chatId) {
|
|
82
85
|
throw new Error(`task_run: 依赖任务不存在或不属于本会话:${id}`);
|
|
83
86
|
}
|
|
84
87
|
return dep;
|
|
85
|
-
});
|
|
88
|
+
}));
|
|
86
89
|
const failed = deps.filter((d) => d.status === 'failed');
|
|
87
90
|
if (failed.length) {
|
|
88
91
|
throw new Error(`task_run: 依赖任务已失败(${failed.map((d) => d.id.slice(0, 8)).join('、')})——`
|
|
@@ -108,7 +111,7 @@ export class TaskService {
|
|
|
108
111
|
}
|
|
109
112
|
}
|
|
110
113
|
const blocked = blockedBy.length > 0;
|
|
111
|
-
const record = this.
|
|
114
|
+
const record = await this.store.createTask({
|
|
112
115
|
chatId: input.chatId,
|
|
113
116
|
task,
|
|
114
117
|
worktreePath,
|
|
@@ -117,7 +120,7 @@ export class TaskService {
|
|
|
117
120
|
initialStatus: blocked ? 'blocked' : 'running',
|
|
118
121
|
});
|
|
119
122
|
const recordId = `task:${record.id}`;
|
|
120
|
-
this.
|
|
123
|
+
await this.store.updateTask(record.id, { recordId });
|
|
121
124
|
if (!blocked) {
|
|
122
125
|
this.ignite(record.id, input.chatId, task, recordId, worktreePath);
|
|
123
126
|
}
|
|
@@ -137,7 +140,7 @@ export class TaskService {
|
|
|
137
140
|
const text = message.trim();
|
|
138
141
|
if (!text)
|
|
139
142
|
return { success: false, error: 'task_send: message 不能为空' };
|
|
140
|
-
const task = this.
|
|
143
|
+
const task = await this.store.getTask(taskId);
|
|
141
144
|
if (!task || task.chatId !== chatId) {
|
|
142
145
|
return { success: false, error: `任务不存在:${taskId}` };
|
|
143
146
|
}
|
|
@@ -178,15 +181,15 @@ export class TaskService {
|
|
|
178
181
|
* completed 的点火;任一依赖 failed 的标 failed(fail fast,错误
|
|
179
182
|
* 指明是哪个依赖挂了)。
|
|
180
183
|
*/
|
|
181
|
-
maybeUnblock(chatId) {
|
|
182
|
-
const blocked = this.
|
|
183
|
-
.listTasks(chatId, 100)
|
|
184
|
+
async maybeUnblock(chatId) {
|
|
185
|
+
const blocked = (await this.store
|
|
186
|
+
.listTasks(chatId, 100))
|
|
184
187
|
.filter((t) => t.status === 'blocked' && t.dependsOn?.length);
|
|
185
188
|
for (const task of blocked) {
|
|
186
|
-
const deps = task.dependsOn.map((id) => this.
|
|
189
|
+
const deps = await Promise.all(task.dependsOn.map((id) => this.store.getTask(id)));
|
|
187
190
|
const failedDep = deps.find((d) => d?.status === 'failed');
|
|
188
191
|
if (failedDep) {
|
|
189
|
-
this.
|
|
192
|
+
await this.store.updateTask(task.id, {
|
|
190
193
|
status: 'failed',
|
|
191
194
|
error: `依赖任务失败:${failedDep.id.slice(0, 8)}(${failedDep.error ?? '无错误信息'})`,
|
|
192
195
|
});
|
|
@@ -195,7 +198,7 @@ export class TaskService {
|
|
|
195
198
|
}
|
|
196
199
|
const allCompleted = deps.every((d) => d?.status === 'completed');
|
|
197
200
|
if (allCompleted) {
|
|
198
|
-
this.
|
|
201
|
+
await this.store.updateTask(task.id, { status: 'running' });
|
|
199
202
|
this.ignite(task.id, task.chatId, task.task, task.recordId ?? `task:${task.id}`, task.worktreePath);
|
|
200
203
|
this.deps.broadcast?.('task-updated', { chatId, taskId: task.id });
|
|
201
204
|
}
|
|
@@ -217,7 +220,7 @@ export class TaskService {
|
|
|
217
220
|
.finally(() => {
|
|
218
221
|
this.running.delete(taskId);
|
|
219
222
|
// 一个任务到终态,可能让同 chat 的 blocked 任务依赖就绪——调度点火。
|
|
220
|
-
this.maybeUnblock(chatId);
|
|
223
|
+
void this.maybeUnblock(chatId);
|
|
221
224
|
});
|
|
222
225
|
this.running.set(taskId, entry);
|
|
223
226
|
}
|
|
@@ -235,7 +238,7 @@ export class TaskService {
|
|
|
235
238
|
timeline: entry.timeline,
|
|
236
239
|
live: true,
|
|
237
240
|
});
|
|
238
|
-
this.
|
|
241
|
+
void this.store.saveTaskProcess?.(taskId, JSON.stringify(entry.timeline));
|
|
239
242
|
};
|
|
240
243
|
if (when === 'immediate') {
|
|
241
244
|
this.cancelProcessFlush(taskId);
|
|
@@ -259,7 +262,7 @@ export class TaskService {
|
|
|
259
262
|
const getSupervisor = this.deps.getSupervisor ?? getSidecarSupervisor;
|
|
260
263
|
const supervisor = getSupervisor();
|
|
261
264
|
if (!supervisor) {
|
|
262
|
-
this.
|
|
265
|
+
await this.store.updateTask(taskId, {
|
|
263
266
|
status: 'failed',
|
|
264
267
|
error: 'sidecar 未运行',
|
|
265
268
|
});
|
|
@@ -267,7 +270,7 @@ export class TaskService {
|
|
|
267
270
|
return;
|
|
268
271
|
}
|
|
269
272
|
const settings = llmService.getSettings();
|
|
270
|
-
const project = this.deps.resolveChatProject(chatId);
|
|
273
|
+
const project = await this.deps.resolveChatProject(chatId);
|
|
271
274
|
// 4.6b 整合:worktree 任务的围栏根 = worktree 路径(更紧——任务摸不到
|
|
272
275
|
// 主检出);否则沿用项目根。writableRoots 同理收窄。
|
|
273
276
|
const fenceRoot = worktreePath ?? project?.folderPath ?? null;
|
|
@@ -397,14 +400,14 @@ export class TaskService {
|
|
|
397
400
|
traceId = failureTraceId;
|
|
398
401
|
}
|
|
399
402
|
}
|
|
400
|
-
this.
|
|
403
|
+
await this.store.updateTask(taskId, {
|
|
401
404
|
status,
|
|
402
405
|
answer: answer.trim() || null,
|
|
403
406
|
error,
|
|
404
407
|
...(traceId ? { traceId } : {}),
|
|
405
408
|
});
|
|
406
409
|
this.cancelProcessFlush(taskId);
|
|
407
|
-
this.
|
|
410
|
+
await this.store.saveTaskProcess?.(taskId, JSON.stringify(entry.timeline));
|
|
408
411
|
this.deps.broadcast?.('task-updated', { chatId, taskId });
|
|
409
412
|
this.deps.broadcast?.('task-process', {
|
|
410
413
|
chatId,
|
|
@@ -414,15 +417,15 @@ export class TaskService {
|
|
|
414
417
|
});
|
|
415
418
|
}
|
|
416
419
|
/** task_status 工具:单任务或本 chat 全部任务的快照。 */
|
|
417
|
-
status(chatId, taskId) {
|
|
420
|
+
async status(chatId, taskId) {
|
|
418
421
|
if (taskId) {
|
|
419
|
-
const task = this.
|
|
422
|
+
const task = await this.store.getTask(taskId);
|
|
420
423
|
if (!task || task.chatId !== chatId) {
|
|
421
424
|
return { success: false, error: `任务不存在:${taskId}` };
|
|
422
425
|
}
|
|
423
426
|
return { success: true, task: this.toModelView(task) };
|
|
424
427
|
}
|
|
425
|
-
const tasks = this.
|
|
428
|
+
const tasks = await this.store.listTasks(chatId, 50);
|
|
426
429
|
return {
|
|
427
430
|
success: true,
|
|
428
431
|
total: tasks.length,
|
|
@@ -430,8 +433,8 @@ export class TaskService {
|
|
|
430
433
|
};
|
|
431
434
|
}
|
|
432
435
|
/** task_result 工具:取终态任务的完整结果。 */
|
|
433
|
-
result(chatId, taskId) {
|
|
434
|
-
const task = this.
|
|
436
|
+
async result(chatId, taskId) {
|
|
437
|
+
const task = await this.store.getTask(taskId);
|
|
435
438
|
if (!task || task.chatId !== chatId) {
|
|
436
439
|
return { success: false, error: `任务不存在:${taskId}` };
|
|
437
440
|
}
|
|
@@ -473,8 +476,8 @@ export class TaskService {
|
|
|
473
476
|
* 右侧过程栏:当前推理时间线。running 且进程内有流 = live;
|
|
474
477
|
* 表上 running 但本进程没流 = 上次崩溃残留(stale)。
|
|
475
478
|
*/
|
|
476
|
-
getProcess(taskId) {
|
|
477
|
-
const task = this.
|
|
479
|
+
async getProcess(taskId) {
|
|
480
|
+
const task = await this.store.getTask(taskId);
|
|
478
481
|
if (!task)
|
|
479
482
|
return null;
|
|
480
483
|
const entry = this.running.get(taskId);
|
|
@@ -509,7 +512,7 @@ export class TaskService {
|
|
|
509
512
|
}
|
|
510
513
|
/** 合并任务 worktree 到主仓(UI「合并到主仓」按钮的路由实现)。 */
|
|
511
514
|
async mergeTaskWorktree(taskId) {
|
|
512
|
-
const task = this.
|
|
515
|
+
const task = await this.store.getTask(taskId);
|
|
513
516
|
if (!task)
|
|
514
517
|
throw new Error(`任务不存在:${taskId}`);
|
|
515
518
|
if (!task.worktreePath || !task.worktreeBranch) {
|
|
@@ -523,13 +526,13 @@ export class TaskService {
|
|
|
523
526
|
}
|
|
524
527
|
const name = path.basename(task.worktreePath);
|
|
525
528
|
await this.deps.worktreeService.mergeWorktree(task.chatId, name, `task(${taskId.slice(0, 8)}): ${task.task.slice(0, 60)}`);
|
|
526
|
-
const updated = this.
|
|
529
|
+
const updated = await this.store.updateTask(taskId, { worktreeState: 'merged' });
|
|
527
530
|
this.deps.broadcast?.('task-updated', { chatId: task.chatId, taskId });
|
|
528
531
|
return updated;
|
|
529
532
|
}
|
|
530
533
|
/** 丢弃任务 worktree(UI「丢弃」按钮的路由实现)。 */
|
|
531
534
|
async discardTaskWorktree(taskId) {
|
|
532
|
-
const task = this.
|
|
535
|
+
const task = await this.store.getTask(taskId);
|
|
533
536
|
if (!task)
|
|
534
537
|
throw new Error(`任务不存在:${taskId}`);
|
|
535
538
|
if (!task.worktreePath) {
|
|
@@ -545,7 +548,7 @@ export class TaskService {
|
|
|
545
548
|
await this.deps.worktreeService.removeWorktree(task.chatId, name, {
|
|
546
549
|
deleteBranch: true,
|
|
547
550
|
});
|
|
548
|
-
const updated = this.
|
|
551
|
+
const updated = await this.store.updateTask(taskId, { worktreeState: 'discarded' });
|
|
549
552
|
this.deps.broadcast?.('task-updated', { chatId: task.chatId, taskId });
|
|
550
553
|
return updated;
|
|
551
554
|
}
|
|
@@ -35,7 +35,7 @@ export declare function slugifyWorktreeName(raw: string): string;
|
|
|
35
35
|
export declare class WorktreeService {
|
|
36
36
|
private readonly deps;
|
|
37
37
|
constructor(deps: {
|
|
38
|
-
resolveProject: (chatId: string) => WorktreeProject | null
|
|
38
|
+
resolveProject: (chatId: string) => Promise<WorktreeProject | null>;
|
|
39
39
|
runGit?: GitRunner;
|
|
40
40
|
});
|
|
41
41
|
private git;
|
|
@@ -49,8 +49,8 @@ export class WorktreeService {
|
|
|
49
49
|
managedRoot(projectRoot) {
|
|
50
50
|
return path.join(projectRoot, '.steerable', 'worktrees');
|
|
51
51
|
}
|
|
52
|
-
requireProject(chatId) {
|
|
53
|
-
const project = this.deps.resolveProject(chatId);
|
|
52
|
+
async requireProject(chatId) {
|
|
53
|
+
const project = await this.deps.resolveProject(chatId);
|
|
54
54
|
if (!project) {
|
|
55
55
|
throw new Error('worktree 需要对话绑定项目(git 仓库)——请先把本对话关联到一个项目。');
|
|
56
56
|
}
|
|
@@ -91,7 +91,7 @@ export class WorktreeService {
|
|
|
91
91
|
* 同名已存在时直接复用(幂等——模型重试不该炸掉已建好的工作区)。
|
|
92
92
|
*/
|
|
93
93
|
async createWorktree(chatId, rawName) {
|
|
94
|
-
const project = this.requireProject(chatId);
|
|
94
|
+
const project = await this.requireProject(chatId);
|
|
95
95
|
await this.requireGitRepo(project.folderPath);
|
|
96
96
|
const name = slugifyWorktreeName(rawName ?? '');
|
|
97
97
|
const suffix = name === 'wt' ? `-${Date.now().toString(36)}` : '';
|
|
@@ -117,7 +117,7 @@ export class WorktreeService {
|
|
|
117
117
|
}
|
|
118
118
|
/** 列出本项目的托管 worktree(只认 .steerable/worktrees 下的)。 */
|
|
119
119
|
async listWorktrees(chatId) {
|
|
120
|
-
const project = this.requireProject(chatId);
|
|
120
|
+
const project = await this.requireProject(chatId);
|
|
121
121
|
await this.requireGitRepo(project.folderPath);
|
|
122
122
|
const { stdout } = await this.git(project.folderPath, [
|
|
123
123
|
'worktree',
|
|
@@ -168,7 +168,7 @@ export class WorktreeService {
|
|
|
168
168
|
* 但默认删掉保持分支列表干净——已合并的分支删除是安全的)。
|
|
169
169
|
*/
|
|
170
170
|
async removeWorktree(chatId, name, options = {}) {
|
|
171
|
-
const project = this.requireProject(chatId);
|
|
171
|
+
const project = await this.requireProject(chatId);
|
|
172
172
|
const worktrees = await this.listWorktrees(chatId);
|
|
173
173
|
const target = worktrees.find((w) => w.name === name || w.path === name);
|
|
174
174
|
if (!target) {
|
|
@@ -203,7 +203,7 @@ export class WorktreeService {
|
|
|
203
203
|
* 可稍后重试或手工处理。
|
|
204
204
|
*/
|
|
205
205
|
async mergeWorktree(chatId, name, commitMessage) {
|
|
206
|
-
const project = this.requireProject(chatId);
|
|
206
|
+
const project = await this.requireProject(chatId);
|
|
207
207
|
const worktrees = await this.listWorktrees(chatId);
|
|
208
208
|
const target = worktrees.find((w) => w.name === name || w.path === name);
|
|
209
209
|
if (!target) {
|
package/dist/main.js
CHANGED
|
@@ -39,7 +39,6 @@ const __dirname = path.dirname(__filename);
|
|
|
39
39
|
app.setPath('userData', getUserDataDir());
|
|
40
40
|
import { getActiveCoreLoopStreamId } from './local-backend/coreloop-stream.js';
|
|
41
41
|
import { getSidecarSupervisor } from './llm/index.js';
|
|
42
|
-
import { localStore } from './storage/index.js';
|
|
43
42
|
import { createHostRuntime } from './host/runtime.js';
|
|
44
43
|
import { registerPackIpc } from './host/ipc.js';
|
|
45
44
|
const store = createJsonStore({ name: 'config' });
|
|
@@ -62,20 +61,16 @@ const RETRY_DELAYS = [2000, 5000, 10000, 30000];
|
|
|
62
61
|
// 全部宿主服务由共享 HostRuntime 装配(与 BS server 同一份,见
|
|
63
62
|
// src/host/runtime.ts 模块头——历史上两处逐行重复,BS 曾漏接 TaskService
|
|
64
63
|
// 导致模型退化成"假后台")。CS 的差异只剩广播(IPC)与生命周期。
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
onLog: (line) => log.info('[sidecar]', line),
|
|
76
|
-
taskSweepReason: '应用重启,任务流已中断',
|
|
77
|
-
});
|
|
78
|
-
const { localExecutor, localScriptRegistry, terminalManager, packHandles, localBackendRouter, approvalBridge, askUserBridge, maybeExecInTerminal, } = runtime;
|
|
64
|
+
let runtime;
|
|
65
|
+
let hostStore;
|
|
66
|
+
let localExecutor;
|
|
67
|
+
let localScriptRegistry;
|
|
68
|
+
let terminalManager;
|
|
69
|
+
let packHandles;
|
|
70
|
+
let localBackendRouter;
|
|
71
|
+
let approvalBridge;
|
|
72
|
+
let askUserBridge;
|
|
73
|
+
let maybeExecInTerminal;
|
|
79
74
|
let routeWindow = null;
|
|
80
75
|
// 进行中的 agent 流(streamId → AbortController)。cancelStream IPC、
|
|
81
76
|
// 窗口销毁、应用退出时统一 abort,保证不会有"关了还在跑"的幽灵循环。
|
|
@@ -90,15 +85,6 @@ function broadcastTerminalEvent(channel, payload) {
|
|
|
90
85
|
win.webContents.send(channel, payload);
|
|
91
86
|
}
|
|
92
87
|
}
|
|
93
|
-
terminalManager.on('data', (sessionId, chunk) => {
|
|
94
|
-
broadcastTerminalEvent('terminal:data', { sessionId, chunk });
|
|
95
|
-
});
|
|
96
|
-
terminalManager.on('exit', (sessionId, code, signal) => {
|
|
97
|
-
broadcastTerminalEvent('terminal:exit', { sessionId, code, signal });
|
|
98
|
-
});
|
|
99
|
-
terminalManager.on('spawned', session => {
|
|
100
|
-
broadcastTerminalEvent('terminal:spawned', session);
|
|
101
|
-
});
|
|
102
88
|
function getStartPath() {
|
|
103
89
|
if (!START_PATH)
|
|
104
90
|
return '/agent';
|
|
@@ -748,7 +734,7 @@ function setupIpcHandlers() {
|
|
|
748
734
|
// between the turn's user message and the assistant reply persisted at
|
|
749
735
|
// turn end. Only on acceptance: a soft failure degrades into the
|
|
750
736
|
// follow-up queue, which persists through the normal send path.
|
|
751
|
-
|
|
737
|
+
await hostStore.addMessage(chatId, 'user', content);
|
|
752
738
|
}
|
|
753
739
|
return { ok, ...(ok ? {} : { reason: 'stream_not_active' }) };
|
|
754
740
|
});
|
|
@@ -806,10 +792,41 @@ function setupIpcHandlers() {
|
|
|
806
792
|
}
|
|
807
793
|
app.whenReady().then(async () => {
|
|
808
794
|
applyStrictCsp();
|
|
795
|
+
runtime = await createHostRuntime({
|
|
796
|
+
broadcast: (channel, payload) => broadcastTerminalEvent(channel, payload),
|
|
797
|
+
broadcastMain: (eventName, payload) => {
|
|
798
|
+
if (!mainWindow || mainWindow.isDestroyed())
|
|
799
|
+
return;
|
|
800
|
+
mainWindow.webContents.send(eventName, payload);
|
|
801
|
+
},
|
|
802
|
+
hasWindow: () => BrowserWindow.getAllWindows().some((win) => !win.isDestroyed()),
|
|
803
|
+
onLog: (line) => log.info('[sidecar]', line),
|
|
804
|
+
taskSweepReason: '应用重启,任务流已中断',
|
|
805
|
+
});
|
|
806
|
+
({
|
|
807
|
+
store: hostStore,
|
|
808
|
+
localExecutor,
|
|
809
|
+
localScriptRegistry,
|
|
810
|
+
terminalManager,
|
|
811
|
+
packHandles,
|
|
812
|
+
localBackendRouter,
|
|
813
|
+
approvalBridge,
|
|
814
|
+
askUserBridge,
|
|
815
|
+
maybeExecInTerminal,
|
|
816
|
+
} = runtime);
|
|
817
|
+
terminalManager.on('data', (sessionId, chunk) => {
|
|
818
|
+
broadcastTerminalEvent('terminal:data', { sessionId, chunk });
|
|
819
|
+
});
|
|
820
|
+
terminalManager.on('exit', (sessionId, code, signal) => {
|
|
821
|
+
broadcastTerminalEvent('terminal:exit', { sessionId, code, signal });
|
|
822
|
+
});
|
|
823
|
+
terminalManager.on('spawned', session => {
|
|
824
|
+
broadcastTerminalEvent('terminal:spawned', session);
|
|
825
|
+
});
|
|
809
826
|
setupIpcHandlers();
|
|
810
827
|
// 任务清扫 / mock 广播 / sidecar 启动 / MCP 刷新 / 终端预热都在
|
|
811
828
|
// runtime.start() 里(与 BS 同一份)。
|
|
812
|
-
runtime.start();
|
|
829
|
+
await runtime.start();
|
|
813
830
|
createWindow();
|
|
814
831
|
setSecondInstanceHandler(() => {
|
|
815
832
|
if (mainWindow && !mainWindow.isDestroyed()) {
|
|
@@ -23,15 +23,21 @@
|
|
|
23
23
|
* 不要绑到非回环地址,那会把 shell / 文件读写 / 场景包工具控制暴露给整个
|
|
24
24
|
* 网络(token 只能挡没有 token 的人)。
|
|
25
25
|
*/
|
|
26
|
-
import { type Server } from 'node:http';
|
|
26
|
+
import { type IncomingMessage, type Server, type ServerResponse } from 'node:http';
|
|
27
27
|
import type { LocalBackendRouter } from '../local-backend/router.js';
|
|
28
|
+
import type { ScopedStore } from '../storage/scoped-store.js';
|
|
28
29
|
import type { LocalExecutor, LocalExecRequest, LocalExecResult } from '../local-executor.js';
|
|
29
30
|
import type { LocalScriptRegistry } from '../local-script-registry.js';
|
|
30
31
|
import type { TerminalManager } from '../terminal-manager.js';
|
|
31
32
|
import type { createApprovalBridge } from '../sidecar/reverse-approval.js';
|
|
32
33
|
import type { createAskUserBridge } from '../sidecar/reverse-ask-user.js';
|
|
33
34
|
import type { SseBus } from './sse-bus.js';
|
|
35
|
+
/** Runs before built-in routing; `handled` short-circuits the request. */
|
|
36
|
+
export interface BsMiddleware {
|
|
37
|
+
(req: IncomingMessage, res: ServerResponse): Promise<'handled' | 'pass'>;
|
|
38
|
+
}
|
|
34
39
|
export interface BsServerDeps {
|
|
40
|
+
store: ScopedStore;
|
|
35
41
|
localBackendRouter: LocalBackendRouter;
|
|
36
42
|
localExecutor: LocalExecutor;
|
|
37
43
|
localScriptRegistry: LocalScriptRegistry;
|
|
@@ -44,5 +50,11 @@ export interface BsServerDeps {
|
|
|
44
50
|
webDistDir: string;
|
|
45
51
|
/** Bearer token;缺省每次启动随机生成。测试注入固定值。 */
|
|
46
52
|
authToken?: string;
|
|
53
|
+
/**
|
|
54
|
+
* Trusted product middleware, invoked in order after Host validation and
|
|
55
|
+
* before authentication. This position lets a product own public sign-in
|
|
56
|
+
* and identity-provider callback routes without weakening built-in routes.
|
|
57
|
+
*/
|
|
58
|
+
middleware?: readonly BsMiddleware[];
|
|
47
59
|
}
|
|
48
60
|
export declare function createBsServer(deps: BsServerDeps): Server;
|
|
@@ -31,10 +31,10 @@ import path from 'node:path';
|
|
|
31
31
|
import log from 'electron-log';
|
|
32
32
|
import { getActiveCoreLoopStreamId } from '../local-backend/coreloop-stream.js';
|
|
33
33
|
import { getSidecarSupervisor } from '../llm/index.js';
|
|
34
|
-
import { localStore } from '../storage/index.js';
|
|
35
34
|
import { matchPackHttpRoute } from '../host/http-routes.js';
|
|
36
35
|
import { getBrand } from '../brand.js';
|
|
37
36
|
import { saveAttachmentFiles } from '../attachments.js';
|
|
37
|
+
import { getAuthProvider } from '../auth/index.js';
|
|
38
38
|
/** 与 router.handleStream 内部的路由正则保持一致——只有这些路径是流式。 */
|
|
39
39
|
const STREAM_PATH_PATTERNS = [
|
|
40
40
|
/^\/api\/v2\/chats\/[^/]+\/(send|run|agent)$/,
|
|
@@ -91,6 +91,7 @@ function asRecord(body) {
|
|
|
91
91
|
}
|
|
92
92
|
export function createBsServer(deps) {
|
|
93
93
|
const { localBackendRouter, localExecutor, localScriptRegistry, terminalManager, approvalBridge, askUserBridge, maybeExecInTerminal, bus, webDistDir, } = deps;
|
|
94
|
+
const middleware = deps.middleware ?? [];
|
|
94
95
|
const authToken = deps.authToken ?? randomBytes(24).toString('base64url');
|
|
95
96
|
/** `/api/v2/*` 与 `/host/*` 的 Bearer 门;EventSource 不能设头,收 query token。 */
|
|
96
97
|
function isAuthorized(req, url) {
|
|
@@ -98,13 +99,18 @@ export function createBsServer(deps) {
|
|
|
98
99
|
return true;
|
|
99
100
|
return url.searchParams.get('token') === authToken;
|
|
100
101
|
}
|
|
101
|
-
async function handleApi(req, res, pathname) {
|
|
102
|
+
async function handleApi(req, res, pathname, principal) {
|
|
102
103
|
const isStream = req.method === 'POST' && STREAM_PATH_PATTERNS.some((p) => p.test(pathname));
|
|
103
104
|
const body = req.method === 'POST' || req.method === 'PATCH' || req.method === 'PUT'
|
|
104
105
|
? await readJsonBody(req)
|
|
105
106
|
: undefined;
|
|
106
107
|
const search = new URL(req.url ?? '/', 'http://bs.local').search;
|
|
107
|
-
const request = {
|
|
108
|
+
const request = {
|
|
109
|
+
method: req.method ?? 'GET',
|
|
110
|
+
path: `${pathname}${search}`,
|
|
111
|
+
body,
|
|
112
|
+
...(principal ? { principal } : {}),
|
|
113
|
+
};
|
|
108
114
|
if (isStream) {
|
|
109
115
|
const controller = new AbortController();
|
|
110
116
|
// 浏览器关页/取消 fetch → 中止 agent 循环(对齐 Electron 的 cancelStream)。
|
|
@@ -221,7 +227,7 @@ export function createBsServer(deps) {
|
|
|
221
227
|
const ok = await supervisor.steerChat(streamId, content);
|
|
222
228
|
// 与 main.ts 同理:接受即落库,重开对话能看到这条注入的用户消息。
|
|
223
229
|
if (ok)
|
|
224
|
-
|
|
230
|
+
await deps.store.addMessage(chatId, 'user', content);
|
|
225
231
|
sendJson(res, 200, { ok, ...(ok ? {} : { reason: 'stream_not_active' }) });
|
|
226
232
|
return;
|
|
227
233
|
}
|
|
@@ -365,19 +371,42 @@ export function createBsServer(deps) {
|
|
|
365
371
|
}
|
|
366
372
|
const url = new URL(req.url ?? '/', 'http://bs.local');
|
|
367
373
|
const pathname = url.pathname;
|
|
374
|
+
for (const handle of middleware) {
|
|
375
|
+
const result = await handle(req, res);
|
|
376
|
+
if (result !== 'handled' && result !== 'pass') {
|
|
377
|
+
throw new Error(`BS middleware returned invalid result: ${String(result)}`);
|
|
378
|
+
}
|
|
379
|
+
if (result === 'handled' || res.writableEnded)
|
|
380
|
+
return;
|
|
381
|
+
if (res.headersSent) {
|
|
382
|
+
throw new Error('BS middleware returned pass after starting a response');
|
|
383
|
+
}
|
|
384
|
+
}
|
|
368
385
|
// 门 2:API 与宿主能力要 Bearer token;静态资源不验(浏览器靠它
|
|
369
386
|
// 拿 token),静态面不暴露任何能力。
|
|
370
387
|
const needsAuth = pathname.startsWith('/api/v2/') || pathname.startsWith('/host/');
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
388
|
+
let principal;
|
|
389
|
+
if (needsAuth) {
|
|
390
|
+
const authProvider = getAuthProvider();
|
|
391
|
+
if (authProvider) {
|
|
392
|
+
const decision = await authProvider.authenticate(req.headers);
|
|
393
|
+
if (!decision.ok) {
|
|
394
|
+
sendJson(res, decision.status, decision.body ?? { detail: decision.status === 403 ? 'forbidden' : 'unauthorized' });
|
|
395
|
+
return;
|
|
396
|
+
}
|
|
397
|
+
principal = decision.principal;
|
|
398
|
+
}
|
|
399
|
+
else if (!isAuthorized(req, url)) {
|
|
400
|
+
sendJson(res, 401, { detail: 'unauthorized' });
|
|
401
|
+
return;
|
|
402
|
+
}
|
|
374
403
|
}
|
|
375
404
|
if (pathname === '/api/v2/events' && req.method === 'GET') {
|
|
376
405
|
bus.attach(res);
|
|
377
406
|
return;
|
|
378
407
|
}
|
|
379
408
|
if (pathname.startsWith('/api/v2/')) {
|
|
380
|
-
await handleApi(req, res, pathname);
|
|
409
|
+
await handleApi(req, res, pathname, principal);
|
|
381
410
|
return;
|
|
382
411
|
}
|
|
383
412
|
if (pathname.startsWith('/host/')) {
|