hob-bot 1.0.3 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +75 -5
- package/README.zh-CN.md +75 -5
- package/dist/web/assets/DesktopView-rVxacveF.js +5 -0
- package/dist/web/assets/index-B-8ISOr1.js +267 -0
- package/dist/web/assets/index-Cu1Pf8yX.css +1 -0
- package/dist/web/assets/{math-zZ4JA8eM.js → math-weKGDUmL.js} +2 -2
- package/dist/web/index.html +2 -2
- package/dist/web/mock/desktop.jpg +0 -0
- package/dist/web/mock/photo.jpg +0 -0
- package/dist/web/mock/photo2.jpg +0 -0
- package/dist/web/mock/sunset.html +90 -0
- package/dist/web/mock//347/201/253/346/230/237/346/227/245/350/220/275/347/244/272/346/204/217/345/233/276.md +34 -0
- package/package.json +10 -2
- package/server/src/actions.ts +24 -57
- package/server/src/assistant-skills.ts +201 -0
- package/server/src/boot.ts +1 -0
- package/server/src/claude-status.ts +47 -0
- package/server/src/config.ts +3 -1
- package/server/src/delegations.ts +40 -12
- package/server/src/desktop-control.ts +152 -0
- package/server/src/desktop-events.ts +63 -0
- package/server/src/desktop-public.ts +43 -0
- package/server/src/desktop-tools.ts +161 -0
- package/server/src/desktop.ts +76 -0
- package/server/src/embedding.ts +52 -0
- package/server/src/files.ts +63 -43
- package/server/src/http.ts +334 -15
- package/server/src/jobs.ts +330 -59
- package/server/src/knowledge-index.ts +123 -0
- package/server/src/knowledge-tools.ts +94 -0
- package/server/src/knowledge.ts +451 -0
- package/server/src/lore-entry.ts +10 -0
- package/server/src/main.ts +6 -0
- package/server/src/mcp.ts +745 -0
- package/server/src/memory.ts +11 -4
- package/server/src/now.ts +33 -0
- package/server/src/overview.ts +1 -1
- package/server/src/pages.ts +1 -1
- package/server/src/pi.ts +92 -16
- package/server/src/prompt.ts +46 -154
- package/server/src/providers.ts +3 -2
- package/server/src/push.ts +42 -7
- package/server/src/router-activity.ts +4 -0
- package/server/src/router-tools.ts +132 -38
- package/server/src/router.ts +159 -19
- package/server/src/routines.ts +14 -4
- package/server/src/runtime.ts +15 -0
- package/server/src/settings.ts +43 -1
- package/server/src/store.ts +4 -3
- package/server/src/thumbs.ts +49 -0
- package/server/src/transcript.ts +57 -4
- package/server/src/worker-tools.ts +14 -12
- package/shared/knowledge.ts +92 -0
- package/shared/mcp.ts +91 -0
- package/shared/media.ts +6 -0
- package/shared/plain-text.ts +31 -0
- package/shared/skills.ts +18 -0
- package/shared/sprites.ts +198 -0
- package/shared/types.ts +105 -20
- package/vendor/claude-bridge/HOB-PATCH.md +10 -0
- package/vendor/claude-bridge/LICENSE +21 -0
- package/vendor/claude-bridge/package.json +62 -0
- package/vendor/claude-bridge/src/agents-md.ts +14 -0
- package/vendor/claude-bridge/src/askclaude-schema.ts +92 -0
- package/vendor/claude-bridge/src/askclaude-ui.ts +90 -0
- package/vendor/claude-bridge/src/attachments.ts +135 -0
- package/vendor/claude-bridge/src/config.ts +105 -0
- package/vendor/claude-bridge/src/convert.ts +222 -0
- package/vendor/claude-bridge/src/extract-tool-results.ts +47 -0
- package/vendor/claude-bridge/src/index.ts +2503 -0
- package/vendor/claude-bridge/src/log-paths.ts +19 -0
- package/vendor/claude-bridge/src/mcp-server.ts +169 -0
- package/vendor/claude-bridge/src/models.ts +164 -0
- package/vendor/claude-bridge/src/prompt-capture.ts +420 -0
- package/vendor/claude-bridge/src/prompt-stream.ts +102 -0
- package/vendor/claude-bridge/src/query-state.ts +118 -0
- package/vendor/claude-bridge/src/rate-limit.ts +48 -0
- package/vendor/claude-bridge/src/session-cwd.ts +38 -0
- package/vendor/claude-bridge/src/session-verify.ts +38 -0
- package/vendor/claude-bridge/src/skills.ts +20 -0
- package/vendor/claude-bridge/src/transcript.ts +86 -0
- package/vendor/claude-bridge/src/usage.ts +53 -0
- package/vendor/lore-pi-extension/package.json +1 -1
- package/vendor/lore-pi-extension/tools.ts +8 -5
- package/dist/web/assets/index-C72gAwgN.css +0 -1
- package/dist/web/assets/index-Dyw5G8Pg.js +0 -230
package/server/src/jobs.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { homedir } from "node:os";
|
|
2
|
-
import { existsSync, readFileSync } from "node:fs";
|
|
3
|
-
import { basename, extname } from "node:path";
|
|
2
|
+
import { copyFileSync, existsSync, readFileSync, realpathSync, statSync } from "node:fs";
|
|
3
|
+
import { basename, dirname, extname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
4
4
|
import type { AgentSession, SessionManager } from "@earendil-works/pi-coding-agent";
|
|
5
|
-
import type
|
|
5
|
+
import { Type, type ImageContent } from "@earendil-works/pi-ai";
|
|
6
6
|
import { commandSummary } from "../../shared/command.ts";
|
|
7
7
|
import type {
|
|
8
8
|
ActivityLine,
|
|
@@ -10,23 +10,28 @@ import type {
|
|
|
10
10
|
ApprovalOption,
|
|
11
11
|
Job,
|
|
12
12
|
JobsPage,
|
|
13
|
+
JobRound,
|
|
13
14
|
JobStatus,
|
|
14
15
|
JobStep,
|
|
15
16
|
RoutineRun,
|
|
16
17
|
} from "../../shared/types.ts";
|
|
17
18
|
import { IMAGE_TYPES, isImagePath } from "../../shared/media.ts";
|
|
18
19
|
import { newId } from "./ids.ts";
|
|
20
|
+
import { availableDesktopTools, type DesktopToolSession } from "./desktop-tools.ts";
|
|
19
21
|
import { imageConfigured } from "./image.ts";
|
|
22
|
+
import { boundKnowledge, findKnowledgeTool, searchKnowledgeTool } from "./knowledge-tools.ts";
|
|
23
|
+
import { MCP_SESSION_TOOLS, mcpEnabled, mcpServerDisplay } from "./mcp.ts";
|
|
20
24
|
import { jobModel } from "./models.ts";
|
|
21
25
|
import { overviewTool } from "./overview.ts";
|
|
22
26
|
import { pageTool } from "./pages.ts";
|
|
23
27
|
import { pushApproval, pushJobEnd } from "./push.ts";
|
|
24
28
|
import { buildWorkerPrompt, type Abilities, type PromptSettings } from "./prompt.ts";
|
|
25
29
|
import { availableSkills } from "./skills.ts";
|
|
30
|
+
import { enabledAssistantSkills, listAssistantSkills, readSkillFiles, recordAssistantSkillUse, saveAssistantSkill, usableAssistantSkills } from "./assistant-skills.ts";
|
|
26
31
|
import type { HobRuntime } from "./runtime.ts";
|
|
27
32
|
import { showPath } from "./text.ts";
|
|
28
33
|
import { monthDayTimeText } from "./time.ts";
|
|
29
|
-
import { STEP_EVENT_LIMIT, deriveSteps, textOfContent, toolStep, withOutput } from "./transcript.ts";
|
|
34
|
+
import { SEDIMENT_PROMPT_HEAD, STEP_EVENT_LIMIT, deriveSteps, textOfContent, toolStep, withOutput } from "./transcript.ts";
|
|
30
35
|
import { registerAskTool, registerImageTool, registerReportTool } from "./worker-tools.ts";
|
|
31
36
|
import { searchTools } from "./search.ts";
|
|
32
37
|
|
|
@@ -50,13 +55,17 @@ export function hasImageFiles(files: string[] | undefined): boolean {
|
|
|
50
55
|
return (files ?? []).some((file) => isImagePath(file));
|
|
51
56
|
}
|
|
52
57
|
|
|
53
|
-
/**
|
|
54
|
-
|
|
58
|
+
/** 一张图超过这个大小就不当图片给模型(只留路径),免得把会话撑得太大。 */
|
|
59
|
+
const MAX_IMAGE_BYTES = 10 * 1024 * 1024;
|
|
60
|
+
|
|
61
|
+
/** 附件里的图片读成 pi 的图片块(读不出来、太大就跳过,不把整个回合拖死)。任务和对话共用。 */
|
|
62
|
+
export function imageContents(files: string[] | undefined): ImageContent[] {
|
|
55
63
|
const out: ImageContent[] = [];
|
|
56
64
|
for (const file of files ?? []) {
|
|
57
65
|
const mimeType = IMAGE_TYPES[extname(file).toLowerCase()];
|
|
58
66
|
if (!mimeType) continue;
|
|
59
67
|
try {
|
|
68
|
+
if (statSync(file).size > MAX_IMAGE_BYTES) continue;
|
|
60
69
|
out.push({ type: "image", data: readFileSync(file).toString("base64"), mimeType });
|
|
61
70
|
} catch {
|
|
62
71
|
// 文件没了或读不了:不带图片,靠任务说明的路径自己去读
|
|
@@ -81,7 +90,7 @@ interface WorkerState {
|
|
|
81
90
|
job: Job;
|
|
82
91
|
session?: AgentSession;
|
|
83
92
|
manager?: SessionManager;
|
|
84
|
-
reported?: { outcome: "done" | "stuck" | "need_approval";
|
|
93
|
+
reported?: { outcome: "done" | "stuck" | "need_approval"; result: string; files?: string[] };
|
|
85
94
|
lastText?: string;
|
|
86
95
|
askedFollowUp: boolean;
|
|
87
96
|
lastActivityAt: number;
|
|
@@ -91,10 +100,26 @@ interface WorkerState {
|
|
|
91
100
|
stopped?: boolean;
|
|
92
101
|
/** 正在跑的工具步骤,按 toolCallId 存:输出更新、跑完都按同一个步骤 id 替换 */
|
|
93
102
|
steps: Map<string, JobStep>;
|
|
103
|
+
readPaths: Map<string, string>;
|
|
104
|
+
/** 你补的话已交给任务、还没被读到(任务做完当前这一步才读);读到时换成正式的一行 */
|
|
105
|
+
queued: JobStep[];
|
|
106
|
+
/** 任务过程里 MCP 工具的显示名(转义后的服务名 → 配置里的原名) */
|
|
107
|
+
mcpServers: Record<string, string>;
|
|
108
|
+
desktop?: DesktopToolSession;
|
|
94
109
|
}
|
|
95
110
|
|
|
96
111
|
/** 推给助手的任务结果最多带这么多字的报告。 */
|
|
97
|
-
const EVENT_SUMMARY_MAX =
|
|
112
|
+
const EVENT_SUMMARY_MAX = 3000;
|
|
113
|
+
|
|
114
|
+
export function jobResultEvent(job: Job, prefix = `任务「${job.title}」`): string {
|
|
115
|
+
const status = job.status === "done" ? "已完成" : job.status === "stuck" ? "受阻" : job.status === "stopped" ? "已停止" : "失败";
|
|
116
|
+
const result = job.summary;
|
|
117
|
+
const header = `${prefix}(${job.id})${status}。`;
|
|
118
|
+
if (!result) return `${header}\n${job.statusReason ?? "没有结果"}`;
|
|
119
|
+
return result.length > EVENT_SUMMARY_MAX
|
|
120
|
+
? `${header}\n结果(共 ${result.length} 字,以下是前 3,000 字;全文:${job.reportPath}):\n${result.slice(0, EVENT_SUMMARY_MAX)}`
|
|
121
|
+
: `${header}\n结果:\n${result}`;
|
|
122
|
+
}
|
|
98
123
|
|
|
99
124
|
function trim(value: string, max = 240): string {
|
|
100
125
|
const oneLine = value.replace(/\s+/g, " ").trim();
|
|
@@ -127,6 +152,10 @@ function activityFor(toolName: string, args: Record<string, unknown>, cwd = ""):
|
|
|
127
152
|
export class JobManager {
|
|
128
153
|
private workers = new Map<string, WorkerState>();
|
|
129
154
|
private queue: string[] = [];
|
|
155
|
+
private followUps = new Map<string, string>();
|
|
156
|
+
private finishing = new Map<string, Promise<void>>();
|
|
157
|
+
private sediments = new Map<string, { cancelled: boolean; session?: AgentSession; task?: Promise<void> }>();
|
|
158
|
+
private replied = new Set<string>();
|
|
130
159
|
private watchdog?: NodeJS.Timeout;
|
|
131
160
|
|
|
132
161
|
constructor(private readonly runtime: HobRuntime) {}
|
|
@@ -166,15 +195,29 @@ export class JobManager {
|
|
|
166
195
|
transcript(jobId: string): JobStep[] {
|
|
167
196
|
const job = this.store.job(jobId);
|
|
168
197
|
if (!job) return [];
|
|
169
|
-
|
|
198
|
+
const steps = deriveSteps(this.store.workerFile(jobId), {
|
|
170
199
|
cwd: job.cwd ?? "",
|
|
171
200
|
save: (stepId, text) => this.store.writeStepOutput(jobId, stepId, text),
|
|
201
|
+
mcpServers: mcpServerDisplay(this.runtime.home),
|
|
172
202
|
});
|
|
203
|
+
return [...steps, ...(this.workers.get(jobId)?.queued ?? [])];
|
|
173
204
|
}
|
|
174
205
|
|
|
175
206
|
async start(assistantId: string, input: StartJobInput): Promise<Job> {
|
|
176
207
|
const assistant = this.store.assistant(assistantId);
|
|
177
208
|
if (!assistant) throw new Error(`助手不存在:${assistantId}`);
|
|
209
|
+
if (input.cwd?.trim()) {
|
|
210
|
+
const canonical = (path: string): string => {
|
|
211
|
+
const absolute = resolve(path);
|
|
212
|
+
let parent = absolute;
|
|
213
|
+
while (!existsSync(parent) && dirname(parent) !== parent) parent = dirname(parent);
|
|
214
|
+
return resolve(realpathSync(parent), relative(parent, absolute));
|
|
215
|
+
};
|
|
216
|
+
const path = relative(canonical(this.runtime.home), canonical(input.cwd));
|
|
217
|
+
if (!path || (path !== ".." && !path.startsWith(`..${sep}`) && !isAbsolute(path))) {
|
|
218
|
+
throw new Error("不能在 Hob 的数据目录里干活;不填 cwd 就在任务自己的目录里做。");
|
|
219
|
+
}
|
|
220
|
+
}
|
|
178
221
|
await this.runtime.models.get();
|
|
179
222
|
const requested =
|
|
180
223
|
input.model || input.thinking
|
|
@@ -208,8 +251,7 @@ export class JobManager {
|
|
|
208
251
|
};
|
|
209
252
|
this.store.saveJob(job);
|
|
210
253
|
if (job.delegationId) this.runtime.delegations.addJob(job.delegationId, job.id);
|
|
211
|
-
|
|
212
|
-
if (!input.routineId) await this.runtime.router.addItem(assistantId, { kind: "job", jobId: job.id });
|
|
254
|
+
await this.runtime.router.addItem(assistantId, { kind: "job", jobId: job.id });
|
|
213
255
|
this.bus.emit({ type: "job", job });
|
|
214
256
|
this.publishBusy(assistantId);
|
|
215
257
|
this.queue.push(job.id);
|
|
@@ -242,7 +284,9 @@ export class JobManager {
|
|
|
242
284
|
const id = this.queue.shift()!;
|
|
243
285
|
const job = this.store.job(id);
|
|
244
286
|
if (!job || job.status !== "queued") continue;
|
|
245
|
-
|
|
287
|
+
const followUp = this.followUps.get(id);
|
|
288
|
+
this.followUps.delete(id);
|
|
289
|
+
void this.launch(job, followUp).catch((error) => this.failLaunch(job, error));
|
|
246
290
|
}
|
|
247
291
|
}
|
|
248
292
|
|
|
@@ -251,7 +295,7 @@ export class JobManager {
|
|
|
251
295
|
await this.finish(job.id, "failed", undefined, message);
|
|
252
296
|
}
|
|
253
297
|
|
|
254
|
-
private async launch(job: Job): Promise<void> {
|
|
298
|
+
private async launch(job: Job, followUp?: string): Promise<void> {
|
|
255
299
|
const running: Job = {
|
|
256
300
|
...job,
|
|
257
301
|
status: "running",
|
|
@@ -267,28 +311,41 @@ export class JobManager {
|
|
|
267
311
|
...baseAbilities,
|
|
268
312
|
image: baseAbilities.image && imageConfigured(this.runtime.settings.settings.image),
|
|
269
313
|
};
|
|
270
|
-
const
|
|
314
|
+
const knowledge = await boundKnowledge(this.runtime, running.assistantId);
|
|
315
|
+
const customTools = [registerReportTool(this.runtime, running.id), registerAskTool(this.runtime, running.id), findKnowledgeTool(this.runtime)];
|
|
316
|
+
if (knowledge.length) customTools.push(searchKnowledgeTool(this.runtime, running.assistantId));
|
|
271
317
|
if (abilities.image) customTools.push(registerImageTool(this.runtime, running.id));
|
|
272
318
|
if (this.runtime.settings.settings.general.showPage !== false) customTools.push(pageTool(this.runtime, running.assistantId, false));
|
|
273
319
|
customTools.push(...searchTools(this.runtime.home, () => this.runtime.settings.settings.search));
|
|
320
|
+
const desktop = await availableDesktopTools(this.runtime, running.assistantId, !!this.runtime.models.find(running.model ?? this.runtime.settings.settings.models.work.ref)?.input.includes("image"));
|
|
321
|
+
if (this.store.job(running.id)?.status !== "running") { await desktop?.dispose(); return; }
|
|
322
|
+
if (desktop) customTools.push(...desktop.tools());
|
|
274
323
|
// 定时跑的就是 Worker:主助手的任务要看得到各助这段时间的动静才能写早报
|
|
275
324
|
if (assistant?.primary) customTools.push(overviewTool(this.runtime));
|
|
325
|
+
// MCP:配置里有启用中的服务才加载(改完配置,之后新开的任务生效)
|
|
326
|
+
const mcp = mcpEnabled(this.runtime.home);
|
|
327
|
+
const ownSkills = enabledAssistantSkills(this.runtime.home, running.assistantId);
|
|
328
|
+
const ownNames = new Set(ownSkills.map((skill) => skill.name));
|
|
276
329
|
const opened = await this.runtime.pi.open({
|
|
277
330
|
file: this.store.workerFile(running.id),
|
|
278
331
|
cwd: running.cwd ?? homedir(),
|
|
279
332
|
model: running.model ?? this.runtime.settings.settings.models.work.ref,
|
|
280
333
|
thinking: running.thinking ?? this.runtime.settings.settings.models.work.thinking,
|
|
281
334
|
memory: this.runtime.settings.memory().provider,
|
|
282
|
-
skills: availableSkills(this.runtime.home, undefined, this.runtime.settings.settings.skills?.disabled),
|
|
335
|
+
skills: [...ownSkills, ...availableSkills(this.runtime.home, undefined, this.runtime.settings.settings.skills?.disabled).filter((skill) => !ownNames.has(skill.name))],
|
|
283
336
|
disabledSkills: this.runtime.settings.settings.skills?.disabled,
|
|
284
337
|
systemPrompt: () =>
|
|
285
|
-
buildWorkerPrompt(this.store, running.id, { ...this.promptSettings(), abilities }),
|
|
338
|
+
buildWorkerPrompt(this.store, running.id, { ...this.promptSettings(), abilities, knowledgeNames: knowledge.map((collection) => collection.name) }),
|
|
286
339
|
tools: [
|
|
287
340
|
...workerTools(abilities),
|
|
288
341
|
...customTools.map((tool) => tool.name),
|
|
342
|
+
...(mcp ? MCP_SESSION_TOOLS : []),
|
|
289
343
|
],
|
|
290
344
|
customTools,
|
|
345
|
+
mcp,
|
|
291
346
|
});
|
|
347
|
+
if (this.store.job(running.id)?.status !== "running") { opened.session.dispose(); await desktop?.dispose(); return; }
|
|
348
|
+
const mcpServers = mcpServerDisplay(this.runtime.home);
|
|
292
349
|
|
|
293
350
|
const worker: WorkerState = {
|
|
294
351
|
job: running,
|
|
@@ -297,12 +354,23 @@ export class JobManager {
|
|
|
297
354
|
askedFollowUp: false,
|
|
298
355
|
lastActivityAt: Date.now(),
|
|
299
356
|
steps: new Map(),
|
|
357
|
+
readPaths: new Map(),
|
|
358
|
+
queued: [],
|
|
359
|
+
mcpServers,
|
|
360
|
+
desktop,
|
|
300
361
|
};
|
|
301
362
|
this.workers.set(running.id, worker);
|
|
302
|
-
|
|
363
|
+
if (followUp !== undefined) {
|
|
364
|
+
const step: JobStep = { id: `q${Date.now()}-0`, kind: "text", at: new Date().toISOString(), from: "user", text: followUp, queued: true };
|
|
365
|
+
worker.queued.push(step);
|
|
366
|
+
this.emitStep(worker, step);
|
|
367
|
+
}
|
|
368
|
+
const images = followUp === undefined ? imageContents(running.inputs) : [];
|
|
303
369
|
|
|
304
370
|
opened.session.subscribe((event) => {
|
|
371
|
+
if (worker.stopped) return;
|
|
305
372
|
if (event.type === "tool_execution_start") {
|
|
373
|
+
if (event.toolName === "read" && typeof event.args?.path === "string") worker.readPaths.set(event.toolCallId, event.args.path);
|
|
306
374
|
this.recordActivity(worker, event.toolName, event.args ?? {});
|
|
307
375
|
const step = toolStep({
|
|
308
376
|
id: event.toolCallId,
|
|
@@ -310,6 +378,7 @@ export class JobManager {
|
|
|
310
378
|
args: event.args ?? {},
|
|
311
379
|
at: new Date().toISOString(),
|
|
312
380
|
cwd: worker.job.cwd ?? "",
|
|
381
|
+
mcpServers: worker.mcpServers,
|
|
313
382
|
running: true,
|
|
314
383
|
});
|
|
315
384
|
worker.steps.set(event.toolCallId, step);
|
|
@@ -322,9 +391,17 @@ export class JobManager {
|
|
|
322
391
|
return;
|
|
323
392
|
}
|
|
324
393
|
if (event.type === "tool_execution_end") {
|
|
394
|
+
if (event.toolName === "read" && !event.isError) {
|
|
395
|
+
const readPath = worker.readPaths.get(event.toolCallId);
|
|
396
|
+
if (readPath) {
|
|
397
|
+
try { this.recordSkillUse(worker, readPath); }
|
|
398
|
+
catch (error) { console.error("记录技能用量失败:", error); }
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
worker.readPaths.delete(event.toolCallId);
|
|
325
402
|
const step =
|
|
326
403
|
worker.steps.get(event.toolCallId) ??
|
|
327
|
-
toolStep({ id: event.toolCallId, name: event.toolName, args: {}, cwd: worker.job.cwd ?? "" });
|
|
404
|
+
toolStep({ id: event.toolCallId, name: event.toolName, args: {}, cwd: worker.job.cwd ?? "", mcpServers: worker.mcpServers });
|
|
328
405
|
worker.steps.delete(event.toolCallId);
|
|
329
406
|
this.emitStep(
|
|
330
407
|
worker,
|
|
@@ -345,18 +422,43 @@ export class JobManager {
|
|
|
345
422
|
}
|
|
346
423
|
if (event.type === "message_start" || event.type === "agent_start") {
|
|
347
424
|
worker.lastActivityAt = Date.now();
|
|
425
|
+
if (event.type === "message_start" && event.message.role === "user") {
|
|
426
|
+
// 补的那句被任务读到了:id 和 transcript 推导出来的一样,前端拿它换掉「等待读取」那行
|
|
427
|
+
const text = textOfContent(event.message.content).trim();
|
|
428
|
+
const index = worker.queued.findIndex((step) => step.text === text);
|
|
429
|
+
if (index >= 0) {
|
|
430
|
+
worker.queued.splice(index, 1);
|
|
431
|
+
this.emitStep(worker, { id: `u${event.message.timestamp}`, kind: "text", at: new Date(event.message.timestamp).toISOString(), from: "user", text });
|
|
432
|
+
}
|
|
433
|
+
}
|
|
348
434
|
return;
|
|
349
435
|
}
|
|
350
436
|
if (event.type === "agent_settled") void this.onSettled(worker);
|
|
351
437
|
});
|
|
352
438
|
|
|
353
439
|
void opened.session
|
|
354
|
-
.prompt(running.instruction, images.length ? { images } : undefined)
|
|
440
|
+
.prompt(followUp ?? running.instruction, images.length ? { images } : undefined)
|
|
355
441
|
.catch(async (error: unknown) => {
|
|
356
|
-
await this.finish(running.id, "failed", undefined, error instanceof Error ? error.message : String(error));
|
|
442
|
+
if (!worker.stopped) await this.finish(running.id, "failed", undefined, error instanceof Error ? error.message : String(error));
|
|
357
443
|
});
|
|
358
444
|
}
|
|
359
445
|
|
|
446
|
+
private recordSkillUse(worker: WorkerState, path: string): void {
|
|
447
|
+
const job = this.store.job(worker.job.id);
|
|
448
|
+
if (!job) return;
|
|
449
|
+
let absolute: string;
|
|
450
|
+
try { absolute = realpathSync(resolve(job.cwd ?? homedir(), path)); }
|
|
451
|
+
catch { return; }
|
|
452
|
+
const skill = usableAssistantSkills(this.runtime.home, job.assistantId).find((item) => existsSync(item.path) && realpathSync(item.path) === absolute);
|
|
453
|
+
if (!skill || job.skillsUsed?.includes(skill.name)) return;
|
|
454
|
+
recordAssistantSkillUse(this.runtime.home, job.assistantId, skill.name);
|
|
455
|
+
const next = { ...job, skillsUsed: [...(job.skillsUsed ?? []), skill.name] };
|
|
456
|
+
this.store.saveJob(next);
|
|
457
|
+
worker.job = next;
|
|
458
|
+
this.bus.emit({ type: "job", job: next });
|
|
459
|
+
this.bus.emit({ type: "skills", assistantId: job.assistantId });
|
|
460
|
+
}
|
|
461
|
+
|
|
360
462
|
private recordActivity(worker: WorkerState, toolName: string, args: Record<string, unknown>): void {
|
|
361
463
|
worker.lastActivityAt = Date.now();
|
|
362
464
|
const line = activityFor(toolName, args);
|
|
@@ -450,7 +552,7 @@ export class JobManager {
|
|
|
450
552
|
await this.finish(worker.job.id, "stuck", undefined, "未提交结果");
|
|
451
553
|
return;
|
|
452
554
|
}
|
|
453
|
-
await session.prompt("你还没有调用 report 提交结果。请现在调用 report(outcome,
|
|
555
|
+
await session.prompt("你还没有调用 report 提交结果。请现在调用 report(outcome, result)。");
|
|
454
556
|
}
|
|
455
557
|
|
|
456
558
|
/** report 工具收到结果后由 worker-tools 调用。 */
|
|
@@ -460,10 +562,10 @@ export class JobManager {
|
|
|
460
562
|
const status: JobStatus = worker.reported.outcome === "done" ? "done" : "stuck";
|
|
461
563
|
// 不写原因:outcome 只说明模型自己认为没做完,状态词(受阻、待确认)卡片上已经写过一遍,
|
|
462
564
|
// 当原因存下来就会重复。真正的原因由看门狗和停止操作给。
|
|
463
|
-
await this.finish(workerId, status, worker.reported.
|
|
565
|
+
await this.finish(workerId, status, worker.reported.result, undefined, worker.reported.files);
|
|
464
566
|
}
|
|
465
567
|
|
|
466
|
-
reportFromTool(jobId: string, data: { outcome: "done" | "stuck" | "need_approval";
|
|
568
|
+
reportFromTool(jobId: string, data: { outcome: "done" | "stuck" | "need_approval"; result: string; files?: string[] }): void {
|
|
467
569
|
const worker = this.workers.get(jobId);
|
|
468
570
|
if (!worker) return;
|
|
469
571
|
worker.reported = data;
|
|
@@ -477,10 +579,28 @@ export class JobManager {
|
|
|
477
579
|
reason?: string,
|
|
478
580
|
files?: string[],
|
|
479
581
|
): Promise<void> {
|
|
582
|
+
const pending = this.finishing.get(jobId);
|
|
583
|
+
if (pending) return pending;
|
|
584
|
+
const task = this.finishOnce(jobId, status, summary, reason, files);
|
|
585
|
+
this.finishing.set(jobId, task);
|
|
586
|
+
try { await task; } finally { this.finishing.delete(jobId); }
|
|
587
|
+
const job = this.store.job(jobId);
|
|
588
|
+
const steps = status === "done" && job?.status === "done" ? this.transcript(jobId) : [];
|
|
589
|
+
// 自动沉淀:用户中途接着说过话;或者没照着技能做、而且是定时执行或动手 20 次以上。
|
|
590
|
+
// 照着技能顺利做完的不再沉淀,免得每天跑完都改一版。
|
|
591
|
+
const followed = this.replied.has(jobId) || steps.some((step) => step.from === "user" && step.text !== "沉淀技能");
|
|
592
|
+
const fresh = !job?.skillsUsed?.length && (Boolean(job?.routineId) || steps.filter((step) => step.kind === "tool").length >= 20);
|
|
593
|
+
if (job?.status === "done" && status === "done" && (followed || fresh)) this.startSediment(jobId);
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
private async finishOnce(jobId: string, status: JobStatus, summary?: string, reason?: string, files?: string[]): Promise<void> {
|
|
480
597
|
const job = this.store.job(jobId);
|
|
481
598
|
if (!job || ENDED.includes(job.status)) return;
|
|
482
599
|
const worker = this.workers.get(jobId);
|
|
600
|
+
if (worker) worker.stopped = true;
|
|
483
601
|
if (worker?.finishTimer) clearTimeout(worker.finishTimer);
|
|
602
|
+
this.followUps.delete(jobId);
|
|
603
|
+
await worker?.desktop?.dispose();
|
|
484
604
|
await this.settle(job, status, { summary, reason, files, lastText: worker?.lastText });
|
|
485
605
|
|
|
486
606
|
if (worker?.session) {
|
|
@@ -498,10 +618,7 @@ export class JobManager {
|
|
|
498
618
|
): Promise<void> {
|
|
499
619
|
const endedAt = new Date().toISOString();
|
|
500
620
|
// 报告原样保留(不压成一行、不截断):任务详情和 report.md 要完整的;只有推给助手的事件太长时才截
|
|
501
|
-
const cleanSummary = options.summary?.trim()
|
|
502
|
-
const eventSummary = cleanSummary && cleanSummary.length > EVENT_SUMMARY_MAX
|
|
503
|
-
? `${cleanSummary.slice(0, EVENT_SUMMARY_MAX)}…(报告过长已截断,完整内容在任务详情)`
|
|
504
|
-
: cleanSummary;
|
|
621
|
+
const cleanSummary = options.summary?.trim() ? options.summary : undefined;
|
|
505
622
|
const statusText = status === "done" ? "已完成" : status === "stuck" ? "受阻" : status === "stopped" ? "已停止" : "失败";
|
|
506
623
|
const reportPath = this.store.writeReport(
|
|
507
624
|
job.id,
|
|
@@ -511,13 +628,9 @@ export class JobManager {
|
|
|
511
628
|
`状态:${statusText}${options.reason ? `(${options.reason})` : ""}`,
|
|
512
629
|
`结束时间:${monthDayTimeText(this.runtime.settings.settings.general.timezone)}`,
|
|
513
630
|
"",
|
|
514
|
-
"##
|
|
515
|
-
"",
|
|
516
|
-
cleanSummary ?? "(没有摘要)",
|
|
631
|
+
"## 结果",
|
|
517
632
|
"",
|
|
518
|
-
|
|
519
|
-
"",
|
|
520
|
-
options.lastText ?? "(没有回复)",
|
|
633
|
+
cleanSummary ?? options.lastText ?? "(没有回复)",
|
|
521
634
|
"",
|
|
522
635
|
"## 任务说明",
|
|
523
636
|
"",
|
|
@@ -530,11 +643,14 @@ export class JobManager {
|
|
|
530
643
|
endedAt,
|
|
531
644
|
reportPath,
|
|
532
645
|
...(job.startedAt ? { durationMs: Date.parse(endedAt) - Date.parse(job.startedAt) } : {}),
|
|
533
|
-
...(
|
|
646
|
+
...(options.summary !== undefined ? { summary: cleanSummary } : {}),
|
|
534
647
|
...(options.reason ? { statusReason: options.reason } : {}),
|
|
535
|
-
...(options.
|
|
648
|
+
...(options.summary !== undefined ? { files: options.files ?? [] } : {}),
|
|
536
649
|
};
|
|
537
650
|
this.store.saveJob(next);
|
|
651
|
+
for (const path of options.files ?? []) void this.runtime.knowledge.collect({ origin: "output", path, description: job.title, assistantId: job.assistantId, jobId: job.id }).catch((error) => console.error("知识库收集失败:", error));
|
|
652
|
+
// 转交里的任务:先把转交重新打开,结果由目标助手接着处理完才回流 Hob(见 DelegationManager.resume)
|
|
653
|
+
const delegationId = !job.routineId && job.delegationId && this.runtime.delegations.resume(job.delegationId) ? job.delegationId : undefined;
|
|
538
654
|
this.bus.emit({ type: "job", job: next });
|
|
539
655
|
pushJobEnd({ push: this.runtime.push, store: this.store }, next);
|
|
540
656
|
this.publishBusy(job.assistantId);
|
|
@@ -552,26 +668,27 @@ export class JobManager {
|
|
|
552
668
|
if (job.routineId) {
|
|
553
669
|
const routine = this.store.routine(job.routineId);
|
|
554
670
|
if (routine) {
|
|
555
|
-
const run: RoutineRun = {
|
|
671
|
+
const run: RoutineRun = {
|
|
672
|
+
jobId: job.id, at: endedAt, status,
|
|
673
|
+
...(next.durationMs !== undefined ? { durationMs: next.durationMs } : {}),
|
|
674
|
+
tools: next.stats.reads + next.stats.commands + next.stats.edits,
|
|
675
|
+
...(next.usage ? { tokens: next.usage.input + next.usage.output } : {}),
|
|
676
|
+
};
|
|
556
677
|
const runs = [run, ...routine.runs.filter((item) => item.jobId !== job.id)].slice(0, 20);
|
|
557
678
|
const updated = { ...routine, runs };
|
|
558
679
|
this.store.saveRoutine(updated);
|
|
559
680
|
this.bus.emit({ type: "routine", routine: updated });
|
|
560
681
|
await this.runtime.router
|
|
561
|
-
.event(job.assistantId, `定时任务「${routine.name}
|
|
562
|
-
routine: { id: routine.id, name: routine.name },
|
|
682
|
+
.event(job.assistantId, `${jobResultEvent(next, `定时任务「${routine.name}」`)}${filesNote}`, {
|
|
683
|
+
routine: { id: routine.id, name: routine.name, jobId: job.id },
|
|
563
684
|
...cont,
|
|
564
685
|
})
|
|
565
686
|
.catch(() => undefined);
|
|
566
687
|
}
|
|
567
688
|
} else if (assistant) {
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
`任务「${job.title}」${statusText}${eventSummary ? `:${eventSummary}` : options.reason ? `:${options.reason}` : ""}${filesNote}`,
|
|
572
|
-
cont,
|
|
573
|
-
)
|
|
574
|
-
.catch(() => undefined);
|
|
689
|
+
const text = `${jobResultEvent(next)}${filesNote}`;
|
|
690
|
+
if (delegationId) void this.runtime.router.relay(job.assistantId, text, delegationId, cont).catch(() => this.runtime.delegations.turnEnded(delegationId, "目标助手处理失败", true));
|
|
691
|
+
else await this.runtime.router.event(job.assistantId, text, cont).catch(() => undefined);
|
|
575
692
|
}
|
|
576
693
|
}
|
|
577
694
|
|
|
@@ -597,16 +714,160 @@ export class JobManager {
|
|
|
597
714
|
}
|
|
598
715
|
}
|
|
599
716
|
const lines: string[] = [];
|
|
600
|
-
if (posted.length) lines.push(
|
|
601
|
-
if (others.length) lines.push(
|
|
717
|
+
if (posted.length) lines.push(`图片已发到对话里:${posted.join("、")}`);
|
|
718
|
+
if (others.length) lines.push(`文件:${others.join("、")}`);
|
|
602
719
|
return { note: lines.length ? `\n${lines.join("\n")}` : "", posted: posted.length };
|
|
603
720
|
}
|
|
604
721
|
|
|
605
|
-
async reply(jobId: string, text: string): Promise<void> {
|
|
722
|
+
async reply(jobId: string, text: string, source: "user" | "router" = "user"): Promise<void> {
|
|
723
|
+
await this.finishing.get(jobId);
|
|
724
|
+
const sediment = this.sediments.get(jobId);
|
|
725
|
+
if (sediment) {
|
|
726
|
+
sediment.cancelled = true;
|
|
727
|
+
await sediment.session?.abort().catch(() => undefined);
|
|
728
|
+
await sediment.task;
|
|
729
|
+
}
|
|
730
|
+
this.replied.add(jobId);
|
|
731
|
+
const job = this.store.job(jobId);
|
|
732
|
+
if (!job) throw new Error("没有这个任务。");
|
|
733
|
+
if (ENDED.includes(job.status)) {
|
|
734
|
+
const { endedAt, statusReason, durationMs, summary, files, reportPath, ...rest } = job;
|
|
735
|
+
const previous = job.rounds ?? [];
|
|
736
|
+
// 这一轮的报告另存一份:下一轮写报告会覆盖原文件。原文件不在了就不存,不让续做因此失败
|
|
737
|
+
const savedReport = reportPath && existsSync(reportPath) ? join(dirname(reportPath), `report.round-${previous.length + 1}.md`) : undefined;
|
|
738
|
+
if (reportPath && savedReport) copyFileSync(reportPath, savedReport);
|
|
739
|
+
const used = previous.reduce((sum, round) => ({ input: sum.input + (round.usage?.input ?? 0), output: sum.output + (round.usage?.output ?? 0), cost: sum.cost + (round.usage?.cost ?? 0) }), { input: 0, output: 0, cost: 0 });
|
|
740
|
+
const usage = job.usage && previous.every((round) => round.usage) ? { input: job.usage.input - used.input, output: job.usage.output - used.output, cost: Number((job.usage.cost - used.cost).toFixed(6)) } : undefined;
|
|
741
|
+
const ended: JobRound = { status: job.status, statusReason, startedAt: job.startedAt, endedAt, durationMs, summary, files, usage, reportPath: savedReport, followUp: text };
|
|
742
|
+
const rounds = [...previous, ended];
|
|
743
|
+
const queued: Job = { ...rest, rounds, status: "queued", startedAt: new Date().toISOString() };
|
|
744
|
+
this.store.saveJob(queued);
|
|
745
|
+
try {
|
|
746
|
+
await this.runtime.router.addItem(job.assistantId, { kind: "job", jobId, round: rounds.length });
|
|
747
|
+
} catch (error) {
|
|
748
|
+
this.store.saveJob(job);
|
|
749
|
+
throw error;
|
|
750
|
+
}
|
|
751
|
+
this.followUps.set(jobId, `用户接着说(${monthDayTimeText(this.runtime.settings.settings.general.timezone)}):${text}`);
|
|
752
|
+
this.bus.emit({ type: "job", job: queued });
|
|
753
|
+
this.publishBusy(job.assistantId);
|
|
754
|
+
this.queue.push(jobId);
|
|
755
|
+
this.pump();
|
|
756
|
+
return;
|
|
757
|
+
}
|
|
606
758
|
const worker = this.workers.get(jobId);
|
|
607
759
|
if (!worker?.session) throw new Error("这个任务当前未在运行。");
|
|
608
|
-
|
|
609
|
-
|
|
760
|
+
// 任务卡在 ask_user 等答复:用户在对话里回的话就是答复,不然插话要等提问返回才读得到,任务永远等下去
|
|
761
|
+
const asking = worker.ask ? this.store.approvals().find((item) => item.jobId === jobId && item.status === "pending") : undefined;
|
|
762
|
+
if (asking) {
|
|
763
|
+
const answered: Approval = source === "user"
|
|
764
|
+
? { ...asking, status: "answered", answerText: text, answeredAt: new Date().toISOString() }
|
|
765
|
+
: { ...asking, status: "expired" };
|
|
766
|
+
this.store.saveApproval(answered);
|
|
767
|
+
this.bus.emit({ type: "approval", approval: answered });
|
|
768
|
+
this.resolveAsk(answered, source === "user" ? `用户没有选给出的选项,而是在对话里回复:${text}` : `助手转达:${text}`);
|
|
769
|
+
return;
|
|
770
|
+
}
|
|
771
|
+
// 先显示出来:任务在跑命令时要等这一步做完才读到,界面上标着「等待读取」
|
|
772
|
+
const step: JobStep = { id: `q${Date.now()}-${worker.queued.length}`, kind: "text", at: new Date().toISOString(), from: "user", text, queued: true };
|
|
773
|
+
worker.queued.push(step);
|
|
774
|
+
this.emitStep(worker, step);
|
|
775
|
+
try {
|
|
776
|
+
if (worker.session.isStreaming) await worker.session.steer(text);
|
|
777
|
+
else void worker.session.prompt(text).catch(() => { worker.queued = worker.queued.filter((item) => item !== step); });
|
|
778
|
+
} catch (error) {
|
|
779
|
+
worker.queued = worker.queued.filter((item) => item !== step);
|
|
780
|
+
throw error;
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
startSediment(jobId: string, source: "manual" | "auto" = "auto"): void {
|
|
785
|
+
const job = this.store.job(jobId);
|
|
786
|
+
if (!job) throw new Error("任务不存在");
|
|
787
|
+
if (job.status !== "done" || this.finishing.has(jobId) || this.workers.has(jobId)) throw new Error("只有已完成的任务可以沉淀");
|
|
788
|
+
if (this.sediments.has(jobId)) throw new Error("任务正在沉淀技能");
|
|
789
|
+
const state: { cancelled: boolean; session?: AgentSession; task?: Promise<void> } = { cancelled: false };
|
|
790
|
+
this.sediments.set(jobId, state);
|
|
791
|
+
state.task = this.sedimentRound(job, state, source).catch(async (error) => {
|
|
792
|
+
if (state.cancelled) return;
|
|
793
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
794
|
+
console.error("沉淀技能失败:", error);
|
|
795
|
+
await this.runtime.router.addItem(job.assistantId, { kind: "note", icon: "info", text: `技能沉淀失败:${message}` }).catch((noteError) => console.error("写入沉淀失败提示失败:", noteError));
|
|
796
|
+
}).finally(() => {
|
|
797
|
+
this.sediments.delete(jobId);
|
|
798
|
+
this.bus.emit({ type: "sediment-finished", jobId });
|
|
799
|
+
});
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
private async sedimentRound(job: Job, state: { cancelled: boolean; session?: AgentSession }, source: "manual" | "auto"): Promise<void> {
|
|
803
|
+
let calledSave = false;
|
|
804
|
+
let lastText = "";
|
|
805
|
+
const skillTool = {
|
|
806
|
+
name: "save_skill", label: "保存技能",
|
|
807
|
+
description: "保存或修补这个助手的技能。name 只用小写字母、数字和连字符,和已有技能同名就是修补,旧版会保留。content 是 SKILL.md 的正文,不用写开头的 name 和 description。files 放脚本和参考资料,路径以 scripts/ 或 references/ 开头。",
|
|
808
|
+
parameters: Type.Object({
|
|
809
|
+
name: Type.String({ description: "技能的目录名,如 gateway-daily-check。" }),
|
|
810
|
+
title: Type.String({ description: "中文名,如「网关每日巡检」。" }),
|
|
811
|
+
description: Type.String({ description: "一句话写什么时候用,120 字以内。" }),
|
|
812
|
+
content: Type.String({ description: "SKILL.md 正文。" }),
|
|
813
|
+
files: Type.Optional(Type.Array(Type.Object({ path: Type.String(), content: Type.Optional(Type.String()), from: Type.Optional(Type.String()) }), { description: "可选,脚本和参考资料:path 加 content,或 path 加 from(工作目录里已有文件的路径,直接复制)。" })),
|
|
814
|
+
}),
|
|
815
|
+
execute: async (_id: string, params: any) => {
|
|
816
|
+
calledSave = true;
|
|
817
|
+
if (state.cancelled || this.store.job(job.id)?.status !== "done") throw new Error("任务已继续,取消沉淀");
|
|
818
|
+
const files = readSkillFiles(job.cwd ?? this.store.workDir(job.id), params.files);
|
|
819
|
+
const skill = saveAssistantSkill(this.runtime.home, job.assistantId, { ...params, files }, job.id);
|
|
820
|
+
let text = skill.version === 1 ? `已沉淀技能「${skill.title}」` : `已更新技能「${skill.title}」· 第 ${skill.version} 版`;
|
|
821
|
+
const routine = job.routineId && this.store.routine(job.routineId);
|
|
822
|
+
if (routine && !routine.skill) {
|
|
823
|
+
this.runtime.routines.update(routine.id, { skill: skill.name });
|
|
824
|
+
text = `已沉淀技能「${skill.title}」,以后定时执行按它做`;
|
|
825
|
+
}
|
|
826
|
+
await this.runtime.router.addItem(job.assistantId, { kind: "note", icon: "skill", text, path: skill.path });
|
|
827
|
+
this.bus.emit({ type: "skills", assistantId: job.assistantId });
|
|
828
|
+
return { content: [{ type: "text" as const, text: "技能已保存。" }], details: undefined };
|
|
829
|
+
},
|
|
830
|
+
};
|
|
831
|
+
const skills = listAssistantSkills(this.runtime.home, job.assistantId);
|
|
832
|
+
const existing = skills.map((skill) => `${skill.name}:${skill.title} —— ${skill.description}`).join("\n") || "无";
|
|
833
|
+
const message = `${SEDIMENT_PROMPT_HEAD},下次照着做,更快、更稳。
|
|
834
|
+
|
|
835
|
+
技能写的是这一类事怎么做,不是这一次的经过。下次接到同类的事,照着它就能一次做成用户要的样子。
|
|
836
|
+
- 按做事的顺序写步骤,带上要用的命令、查询和判断点;怎么判断结果对不对,写在对应的那一步里。
|
|
837
|
+
- 用户在过程中纠正过的做法、对结果提过的要求,写进去。
|
|
838
|
+
- 坑写成一条规则加一句原因,放在它影响的那一步。规则要离开这一次也成立:不写事情经过和这一次的数据;会变的情况不写死,写怎么查。
|
|
839
|
+
- 同一条经验只写一次。修补时直接改掉写错的那句,不在后面追加更正。
|
|
840
|
+
- 助手的规矩、工具说明里已经有的,不再重复。
|
|
841
|
+
- 能写成脚本的写成只读脚本,放在 scripts/,一次输出要看的全部结果。写好后用 bash 真跑一遍,输出对得上再保存。偶尔才用得上的细节放 references/,按主题起文件名。
|
|
842
|
+
- 会改东西的步骤只写进文档,不写进脚本,也不要执行。
|
|
843
|
+
|
|
844
|
+
不写进技能:
|
|
845
|
+
- 临时故障和网络错误。环境没装好、没配好导致的失败,只写怎么装好、配好。
|
|
846
|
+
- 「某个工具不能用」这类结论:环境修好以后,它还会一直挡着你。
|
|
847
|
+
- 这次没走通的尝试:不能写成推荐做法。
|
|
848
|
+
|
|
849
|
+
下面已有同类技能的,修补它,不要另建;新建时,名字和说明按这一类事来起,不按这一次。没有值得沉淀的,就回复「没有需要沉淀的」,不调用 save_skill。
|
|
850
|
+
|
|
851
|
+
这个助手已有的技能:${existing}`;
|
|
852
|
+
if (state.cancelled) return;
|
|
853
|
+
const opened = await this.runtime.pi.open({
|
|
854
|
+
file: this.store.workerFile(job.id), cwd: job.cwd ?? homedir(),
|
|
855
|
+
model: job.model ?? this.runtime.settings.settings.models.work.ref,
|
|
856
|
+
thinking: job.thinking ?? this.runtime.settings.settings.models.work.thinking,
|
|
857
|
+
memory: "off", systemPrompt: () => buildWorkerPrompt(this.store, job.id, this.promptSettings()),
|
|
858
|
+
tools: ["read", "ls", "grep", "find", "bash", "save_skill"], customTools: [skillTool],
|
|
859
|
+
});
|
|
860
|
+
state.session = opened.session;
|
|
861
|
+
opened.session.subscribe((event) => {
|
|
862
|
+
if (event.type === "message_end" && event.message.role === "assistant") lastText = textOfContent(event.message.content).trim();
|
|
863
|
+
});
|
|
864
|
+
try {
|
|
865
|
+
if (!state.cancelled) await opened.session.prompt(message);
|
|
866
|
+
if (!state.cancelled && source === "manual" && !calledSave) {
|
|
867
|
+
const reason = lastText && lastText !== "没有需要沉淀的" ? `:${lastText}` : "";
|
|
868
|
+
await this.runtime.router.addItem(job.assistantId, { kind: "note", icon: "skill", text: `「${job.title}」没有可沉淀的做法${reason}` });
|
|
869
|
+
}
|
|
870
|
+
} finally { opened.session.dispose(); }
|
|
610
871
|
}
|
|
611
872
|
|
|
612
873
|
async stop(jobId: string): Promise<void> {
|
|
@@ -670,23 +931,31 @@ export class JobManager {
|
|
|
670
931
|
if (worker) worker.job = next;
|
|
671
932
|
}
|
|
672
933
|
worker?.ask?.(answer);
|
|
673
|
-
if (worker)
|
|
934
|
+
if (worker) {
|
|
935
|
+
worker.ask = undefined;
|
|
936
|
+
worker.lastActivityAt = Date.now();
|
|
937
|
+
}
|
|
674
938
|
}
|
|
675
939
|
|
|
676
940
|
startWatchdog(): void {
|
|
677
941
|
this.watchdog ??= setInterval(() => {
|
|
678
|
-
|
|
679
|
-
for (const [jobId, worker] of this.workers) {
|
|
680
|
-
if (Date.now() - worker.lastActivityAt > idleMs) {
|
|
681
|
-
void this.finish(jobId, "stuck", undefined, `${Math.round(idleMs / 60_000)} 分钟无响应`);
|
|
682
|
-
}
|
|
683
|
-
}
|
|
942
|
+
this.checkStalls();
|
|
684
943
|
// 并发小了以后(设置里改的),排队的任务还在等,这里补一次泵
|
|
685
944
|
this.pump();
|
|
686
945
|
}, 30_000);
|
|
687
946
|
this.watchdog.unref?.();
|
|
688
947
|
}
|
|
689
948
|
|
|
949
|
+
checkStalls(now = Date.now()): void {
|
|
950
|
+
const idleMs = this.runtime.settings.settings.work.jobStallMinutes * 60_000;
|
|
951
|
+
for (const [jobId, worker] of this.workers) {
|
|
952
|
+
// 在等用户答提问不算没响应:等多久都是用户的事
|
|
953
|
+
if (!worker.ask && now - worker.lastActivityAt > idleMs) {
|
|
954
|
+
void this.finish(jobId, "stuck", undefined, `${Math.round(idleMs / 60_000)} 分钟无响应`);
|
|
955
|
+
}
|
|
956
|
+
}
|
|
957
|
+
}
|
|
958
|
+
|
|
690
959
|
stopWatchdog(): void {
|
|
691
960
|
if (this.watchdog) clearInterval(this.watchdog);
|
|
692
961
|
this.watchdog = undefined;
|
|
@@ -708,11 +977,13 @@ export class JobManager {
|
|
|
708
977
|
}
|
|
709
978
|
}
|
|
710
979
|
this.queue = [];
|
|
980
|
+
this.followUps.clear();
|
|
711
981
|
}
|
|
712
982
|
|
|
713
983
|
disposeAll(): void {
|
|
714
984
|
this.stopWatchdog();
|
|
715
|
-
for (const
|
|
985
|
+
for (const state of this.sediments.values()) { state.cancelled = true; void state.session?.abort(); }
|
|
986
|
+
for (const worker of this.workers.values()) { worker.session?.dispose(); void worker.desktop?.dispose(); }
|
|
716
987
|
this.workers.clear();
|
|
717
988
|
}
|
|
718
989
|
|
|
@@ -723,7 +994,7 @@ export class JobManager {
|
|
|
723
994
|
const active = this.store
|
|
724
995
|
.jobs()
|
|
725
996
|
.filter((job) => job.status === "running" || job.status === "queued" || job.status === "waiting");
|
|
726
|
-
if (!active.length) return;
|
|
997
|
+
if (!active.length && !this.finishing.size && !this.sediments.size) return;
|
|
727
998
|
await new Promise((resolve) => setTimeout(resolve, 25));
|
|
728
999
|
}
|
|
729
1000
|
}
|