hob-bot 1.2.0 → 1.4.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 +32 -24
- package/README.zh-CN.md +33 -25
- package/dist/web/assets/DesktopView-A11jmUwu.js +5 -0
- package/dist/web/assets/index-B-YlVbpo.js +270 -0
- package/dist/web/assets/index-DkSQ-ygR.css +1 -0
- package/dist/web/index.html +2 -2
- package/dist/web/mock/readme-cover.svg +13 -0
- package/dist/web/mock/readme-page.html +7 -0
- package/package.json +1 -1
- package/server/src/assistant-skills.ts +2 -2
- package/server/src/delete-skill.ts +19 -0
- package/server/src/desktop-tools.ts +48 -14
- package/server/src/http.ts +56 -6
- package/server/src/job-view.ts +2 -2
- package/server/src/jobs.ts +120 -108
- package/server/src/knowledge-tools.ts +1 -1
- package/server/src/main.ts +2 -0
- package/server/src/memory.ts +2 -2
- package/server/src/now.ts +2 -2
- package/server/src/overview.ts +2 -3
- package/server/src/pages.ts +7 -6
- package/server/src/prompt.ts +57 -58
- package/server/src/router-activity.ts +7 -0
- package/server/src/router-tools.ts +65 -36
- package/server/src/router.ts +92 -40
- package/server/src/routines.ts +8 -3
- package/server/src/runtime.ts +12 -1
- package/server/src/settings.ts +37 -0
- package/server/src/store.ts +20 -0
- package/server/src/transcript.ts +14 -4
- package/server/src/usage-prices.ts +165 -0
- package/server/src/usage.ts +281 -0
- package/server/src/worker-tools.ts +7 -17
- package/shared/skills.ts +3 -1
- package/shared/types.ts +85 -2
- package/dist/web/assets/DesktopView-CEBJFAIN.js +0 -5
- package/dist/web/assets/index-Di5lyTHO.css +0 -1
- package/dist/web/assets/index-y09i1RGd.js +0 -267
package/server/src/http.ts
CHANGED
|
@@ -21,6 +21,8 @@ import type {
|
|
|
21
21
|
ModelsView,
|
|
22
22
|
PushSubscriptionJson,
|
|
23
23
|
SettingsPatch,
|
|
24
|
+
UsagePricePatch,
|
|
25
|
+
UsagePriceSaved,
|
|
24
26
|
} from "../../shared/types.ts";
|
|
25
27
|
import { parseSprite } from "../../shared/sprites.ts";
|
|
26
28
|
import { answerApproval, archiveAssistant, createFromDraft, dismissDraft } from "./actions.ts";
|
|
@@ -45,7 +47,8 @@ import { SEARCH_PROVIDERS } from "../../shared/search.ts";
|
|
|
45
47
|
import { WebSearch } from "./search.ts";
|
|
46
48
|
import { clampDuty } from "./text.ts";
|
|
47
49
|
import { deleteOwnSkill, importSkills, listImportSkills, skillViews } from "./skills.ts";
|
|
48
|
-
import {
|
|
50
|
+
import { listAssistantSkills, rollbackAssistantSkill, setAssistantSkillEnabled } from "./assistant-skills.ts";
|
|
51
|
+
import { deleteSkill } from "./delete-skill.ts";
|
|
49
52
|
import type { SkillImportSource } from "../../shared/skills.ts";
|
|
50
53
|
|
|
51
54
|
const COOKIE = "hob";
|
|
@@ -110,6 +113,24 @@ export function createApp(runtime: HobRuntime, options: { webDir?: string; home:
|
|
|
110
113
|
|
|
111
114
|
app.get("/api/bootstrap", (c) => c.json(router.bootstrap()));
|
|
112
115
|
|
|
116
|
+
app.get("/api/rules", (c) => c.json({ rules: store.globalRules() }));
|
|
117
|
+
app.put("/api/rules", async (c) => {
|
|
118
|
+
const body = await c.req.json().catch(() => null) as { rules?: unknown } | null;
|
|
119
|
+
if (!Array.isArray(body?.rules) || body.rules.some((rule) => !rule || typeof rule.text !== "string" || !rule.text.trim() || (rule.mode !== "ask" && rule.mode !== "allow") || (rule.id !== undefined && (typeof rule.id !== "string" || !rule.id.trim())))) {
|
|
120
|
+
return c.json({ error: "规则格式无效,文字不能为空" }, 400);
|
|
121
|
+
}
|
|
122
|
+
const changedAt = new Date().toISOString();
|
|
123
|
+
const existing = new Map(store.globalRules().map((rule) => [rule.id, rule]));
|
|
124
|
+
const rules = body.rules.map((rule) => {
|
|
125
|
+
const previous = existing.get(rule.id);
|
|
126
|
+
const text = rule.text.trim();
|
|
127
|
+
return { id: rule.id ?? newId("r-"), text, mode: rule.mode as "ask" | "allow", changedAt: previous && previous.text === text && previous.mode === rule.mode ? previous.changedAt : changedAt };
|
|
128
|
+
});
|
|
129
|
+
store.saveGlobalRules(rules);
|
|
130
|
+
bus.emit({ type: "rules" });
|
|
131
|
+
return c.json({ rules });
|
|
132
|
+
});
|
|
133
|
+
|
|
113
134
|
// 设置页:整份设置(带 revision)与按节合并的写入(revision 不对 → 409)
|
|
114
135
|
app.get("/api/settings", (c) => c.json(runtime.settings.get()));
|
|
115
136
|
|
|
@@ -152,6 +173,7 @@ export function createApp(runtime: HobRuntime, options: { webDir?: string; home:
|
|
|
152
173
|
const saved = runtime.settings.setDesktop(body.revision, { url, enabled: body.enabled });
|
|
153
174
|
if (!saved) return c.json({ error: "设置已在别处修改,已刷新" }, 409);
|
|
154
175
|
if (body.token !== undefined) writeSecret(options.home, "desktop:token", body.token);
|
|
176
|
+
router.releaseSessions();
|
|
155
177
|
runtime.desktopEvents.restart();
|
|
156
178
|
return c.json(runtime.desktop.view());
|
|
157
179
|
} catch (error) { return c.json({ error: error instanceof Error ? error.message : String(error) }, 400); }
|
|
@@ -162,6 +184,7 @@ export function createApp(runtime: HobRuntime, options: { webDir?: string; home:
|
|
|
162
184
|
const current = runtime.settings.settings.desktop ?? { url: "", enabled: false };
|
|
163
185
|
const saved = runtime.settings.setDesktop(body.revision, { ...current, publicAssistants: [...new Set(body.assistants)] });
|
|
164
186
|
if (!saved) return c.json({ error: "设置已在别处修改,已刷新" }, 409);
|
|
187
|
+
router.releaseSessions();
|
|
165
188
|
return c.json(runtime.desktop.view());
|
|
166
189
|
});
|
|
167
190
|
const validScreen = (id: string) => id === "public" || runtime.store.assistants().some((assistant) => desktopProfile(assistant.id) === id);
|
|
@@ -189,6 +212,15 @@ export function createApp(runtime: HobRuntime, options: { webDir?: string; home:
|
|
|
189
212
|
return c.json(runtime.desktopControl.state(body.client ?? "", screen));
|
|
190
213
|
} catch (error) { return c.json({ error: error instanceof Error ? error.message : String(error) }, 409); }
|
|
191
214
|
});
|
|
215
|
+
app.post("/api/desktop/paste", async (c) => {
|
|
216
|
+
const body = await c.req.json().catch(() => ({})) as { client?: unknown; screen?: unknown; text?: unknown };
|
|
217
|
+
if (typeof body.client !== "string" || typeof body.screen !== "string" || !validScreen(body.screen) || typeof body.text !== "string" || !body.text || body.text.includes("\0") || new TextEncoder().encode(body.text).length > 512 * 1024) return c.json({ error: "文字或屏幕无效。" }, 400);
|
|
218
|
+
if (!runtime.desktopControl.state(body.client, body.screen).mine) return c.json({ error: "当前设备没有接管云桌面。" }, 403);
|
|
219
|
+
try {
|
|
220
|
+
await runtime.desktop.request(`/api/screens/${body.screen}/paste`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ text: body.text }) });
|
|
221
|
+
return c.json({ ok: true });
|
|
222
|
+
} catch (error) { return c.json({ error: error instanceof Error ? error.message : String(error) }, 502); }
|
|
223
|
+
});
|
|
192
224
|
// 接管后要自己登录网站:在云桌面里打开某个助手的浏览器(每个助手的登录状态分开)
|
|
193
225
|
app.post("/api/desktop/browser", async (c) => {
|
|
194
226
|
const body = await c.req.json().catch(() => ({})) as { assistantId?: unknown };
|
|
@@ -204,9 +236,10 @@ export function createApp(runtime: HobRuntime, options: { webDir?: string; home:
|
|
|
204
236
|
|
|
205
237
|
app.get("/api/skills", async (c) => {
|
|
206
238
|
const memory = runtime.settings.memory();
|
|
239
|
+
let loreError: string | undefined;
|
|
207
240
|
try { if (memory.provider === "lore" && !runtime.pi.loreSkillPaths.length) await runtime.pi.discoverLoreSkills(); }
|
|
208
|
-
catch (error) {
|
|
209
|
-
return c.json({ skills: skillViews(options.home, runtime.settings.settings.skills?.disabled ?? [], memory.provider === "lore" ? runtime.pi.loreSkillPaths : []), loreActive: memory.provider === "lore", loreUrl: memory.lore.baseUrl });
|
|
241
|
+
catch (error) { loreError = error instanceof Error ? error.message : String(error); }
|
|
242
|
+
return c.json({ skills: skillViews(options.home, runtime.settings.settings.skills?.disabled ?? [], memory.provider === "lore" ? runtime.pi.loreSkillPaths : []), loreActive: memory.provider === "lore", loreUrl: memory.lore.baseUrl, loreError });
|
|
210
243
|
});
|
|
211
244
|
app.get("/api/skills/import/:source", (c) => {
|
|
212
245
|
try { return c.json(listImportSkills(options.home, c.req.param("source") as SkillImportSource)); }
|
|
@@ -317,6 +350,24 @@ export function createApp(runtime: HobRuntime, options: { webDir?: string; home:
|
|
|
317
350
|
return c.json({ settings: next, thinking } satisfies ModelThinkingSaved);
|
|
318
351
|
});
|
|
319
352
|
|
|
353
|
+
// 设置 → 消耗:会话文件里的用量和价格表;改单价走 settings.json,price 为 null 恢复默认
|
|
354
|
+
app.get("/api/usage", async (c) => c.json(await runtime.usage.view()));
|
|
355
|
+
|
|
356
|
+
app.put("/api/usage/price", async (c) => {
|
|
357
|
+
const body = (await c.req.json().catch(() => ({}))) as Partial<UsagePricePatch>;
|
|
358
|
+
if (typeof body.revision !== "number") return c.json({ error: "缺 revision" }, 400);
|
|
359
|
+
const model = typeof body.model === "string" ? body.model.trim() : "";
|
|
360
|
+
if (!model) return c.json({ error: "未指定模型。" }, 400);
|
|
361
|
+
let next;
|
|
362
|
+
try {
|
|
363
|
+
next = runtime.settings.setUsagePrice(body.revision, model, body.price ?? null);
|
|
364
|
+
} catch (error) {
|
|
365
|
+
return c.json({ error: error instanceof Error ? error.message : String(error) }, 400);
|
|
366
|
+
}
|
|
367
|
+
if (!next) return c.json({ error: "设置已在别处修改,已刷新" }, 409);
|
|
368
|
+
return c.json({ settings: next, price: await runtime.usage.priceOf(model) } satisfies UsagePriceSaved);
|
|
369
|
+
});
|
|
370
|
+
|
|
320
371
|
// 设置 → 服务商:列表(Key 只报配没配)、引擎自带的服务商目录、整份设置
|
|
321
372
|
app.get("/api/providers/claude-status", async (c) => c.json(await claudeStatus(runtime.home, runtime.settings.settings.claude?.executable || undefined)));
|
|
322
373
|
app.get("/api/claude", async (c) => {
|
|
@@ -681,8 +732,7 @@ export function createApp(runtime: HobRuntime, options: { webDir?: string; home:
|
|
|
681
732
|
const id = c.req.param("id");
|
|
682
733
|
if (!store.assistant(id)) return c.json({ error: "助手不存在" }, 404);
|
|
683
734
|
try {
|
|
684
|
-
|
|
685
|
-
bus.emit({ type: "skills", assistantId: id });
|
|
735
|
+
deleteSkill(runtime, id, c.req.param("name"));
|
|
686
736
|
return c.json({ ok: true });
|
|
687
737
|
} catch (error) { return c.json({ error: error instanceof Error ? error.message : String(error) }, 400); }
|
|
688
738
|
});
|
|
@@ -903,7 +953,7 @@ export function createApp(runtime: HobRuntime, options: { webDir?: string; home:
|
|
|
903
953
|
app.post("/api/jobs/:id/sediment", (c) => {
|
|
904
954
|
const id = c.req.param("id");
|
|
905
955
|
if (!store.job(id)) return c.json({ error: "任务不存在" }, 404);
|
|
906
|
-
try { jobs.startSediment(id
|
|
956
|
+
try { jobs.startSediment(id); return c.json({ ok: true }); }
|
|
907
957
|
catch (error) { return c.json({ error: error instanceof Error ? error.message : String(error) }, 409); }
|
|
908
958
|
});
|
|
909
959
|
|
package/server/src/job-view.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { Approval, Job, JobStatus, JobStep } from "../../shared/types.ts";
|
|
2
2
|
import type { AssistantSkill } from "../../shared/skills.ts";
|
|
3
3
|
|
|
4
|
-
const statusText: Record<JobStatus, string> = { queued: "排队中", running: "进行中", waiting: "
|
|
4
|
+
const statusText: Record<JobStatus, string> = { queued: "排队中", running: "进行中", waiting: "等你回复", done: "已完成", stuck: "受阻", failed: "失败", stopped: "已停止" };
|
|
5
5
|
|
|
6
6
|
export function duration(ms: number): string {
|
|
7
7
|
const seconds = Math.max(0, Math.floor(ms / 1000));
|
|
@@ -35,7 +35,7 @@ export function jobView(job: Job, steps: JobStep[], approvals: Approval[], skill
|
|
|
35
35
|
: `${statusText[job.status]},已运行 ${elapsed < 60000 ? "不到 1 分钟" : `${Math.floor(elapsed / 60000)} 分钟`}`;
|
|
36
36
|
const lines = [`任务「${job.title}」(${job.id}):${state}${job.rounds?.length ? `(第 ${job.rounds.length + 1} 轮)` : ""}`];
|
|
37
37
|
if (job.statusReason) lines.push(`原因:${job.statusReason}`);
|
|
38
|
-
for (const approval of approvals.filter((item) => item.jobId === job.id && item.status === "pending")) lines.push(
|
|
38
|
+
for (const approval of approvals.filter((item) => item.jobId === job.id && item.status === "pending")) lines.push(`在等你回复:${approval.title}`);
|
|
39
39
|
if (job.skillsUsed?.length) lines.push(`用到的技能:${job.skillsUsed.map((name) => { const skill = skills.find((item) => item.name === name); return skill ? `${skill.title}(第 ${skill.version} 版)` : name; }).join("、")}`);
|
|
40
40
|
if (ended) {
|
|
41
41
|
if (job.summary) {
|
package/server/src/jobs.ts
CHANGED
|
@@ -14,6 +14,7 @@ import type {
|
|
|
14
14
|
JobStatus,
|
|
15
15
|
JobStep,
|
|
16
16
|
RoutineRun,
|
|
17
|
+
SedimentStatus,
|
|
17
18
|
} from "../../shared/types.ts";
|
|
18
19
|
import { IMAGE_TYPES, isImagePath } from "../../shared/media.ts";
|
|
19
20
|
import { newId } from "./ids.ts";
|
|
@@ -24,8 +25,7 @@ import { MCP_SESSION_TOOLS, mcpEnabled, mcpServerDisplay } from "./mcp.ts";
|
|
|
24
25
|
import { jobModel } from "./models.ts";
|
|
25
26
|
import { duration, jobProcessLines } from "./job-view.ts";
|
|
26
27
|
import { overviewTool } from "./overview.ts";
|
|
27
|
-
import {
|
|
28
|
-
import { pushApproval, pushJobEnd } from "./push.ts";
|
|
28
|
+
import { pushJobEnd } from "./push.ts";
|
|
29
29
|
import { buildWorkerPrompt, type Abilities, type PromptSettings } from "./prompt.ts";
|
|
30
30
|
import { availableSkills } from "./skills.ts";
|
|
31
31
|
import { enabledAssistantSkills, listAssistantSkills, readSkillFiles, recordAssistantSkillUse, saveAssistantSkill, usableAssistantSkills } from "./assistant-skills.ts";
|
|
@@ -84,6 +84,7 @@ export interface StartJobInput {
|
|
|
84
84
|
model?: string;
|
|
85
85
|
thinking?: string;
|
|
86
86
|
routineId?: string;
|
|
87
|
+
skills?: string[];
|
|
87
88
|
expectMinutes?: number;
|
|
88
89
|
delegationId?: string;
|
|
89
90
|
}
|
|
@@ -116,18 +117,32 @@ interface WorkerState {
|
|
|
116
117
|
/** 推给助手的任务结果最多带这么多字的报告。 */
|
|
117
118
|
const EVENT_SUMMARY_MAX = 3000;
|
|
118
119
|
|
|
119
|
-
export function jobResultEvent(job: Job, prefix = `任务「${job.title}
|
|
120
|
+
export function jobResultEvent(job: Job, prefix = `任务「${job.title}」`, skillTitles: string[] = [], toolCount = job.stats.reads + job.stats.commands + job.stats.edits, hint?: string): string {
|
|
120
121
|
const status = job.status === "done" ? "已完成" : job.status === "stuck" ? "受阻" : job.status === "stopped" ? "已停止" : "失败";
|
|
121
122
|
const result = job.summary;
|
|
122
|
-
const header = job.
|
|
123
|
-
? `${prefix}(${job.id})${status},用时 ${duration(job.durationMs)},预计 ${job.expect.minutes} 分钟。`
|
|
124
|
-
: `${prefix}(${job.id})${status}。`;
|
|
123
|
+
const header = `${prefix}(${job.id})${status}${job.durationMs !== undefined ? `,用时 ${duration(job.durationMs)}${job.expect ? `,预计 ${job.expect.minutes} 分钟` : ""}` : ""}。\n过程:动手 ${toolCount} 次,${skillTitles.length ? `用了技能${skillTitles.map((title) => `「${title}」`).join("")}` : "没用技能"}。${hint ? `\n${hint}` : ""}`;
|
|
125
124
|
if (!result) return `${header}\n${job.statusReason ?? "没有结果"}`;
|
|
126
125
|
return result.length > EVENT_SUMMARY_MAX
|
|
127
126
|
? `${header}\n结果(共 ${result.length} 字,以下是前 3,000 字;全文:${job.reportPath}):\n${result.slice(0, EVENT_SUMMARY_MAX)}`
|
|
128
127
|
: `${header}\n结果:\n${result}`;
|
|
129
128
|
}
|
|
130
129
|
|
|
130
|
+
/** 没带技能的任务做完了,就在结果里提醒助手安排一次沉淀;有从这个任务沉淀出来的技能时,末尾告诉它 into 填哪份。 */
|
|
131
|
+
export function sedimentHint(home: string, job: Job, skills = usableAssistantSkills(home, job.assistantId)): string | undefined {
|
|
132
|
+
if (job.status !== "done" || job.skills?.length) return undefined;
|
|
133
|
+
const last = skills.filter((skill) => skill.fromJobId === job.id).sort((a, b) => Date.parse(b.updatedAt) - Date.parse(a.updatedAt))[0];
|
|
134
|
+
return `这个任务没带技能。用 save_job_as_skill 让它回顾一遍,把能复用的做法沉淀下来,有没有值得沉淀的由它判断;已经有同类的技能,into 填那份。${last ? `上次从这个任务沉淀出了技能「${last.title}」(${last.name}),into 填它。` : ""}`;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/** 发给任务所属助手的结果事件。转交结束发给 Hob 的汇总用它不带提醒的那份(delegations.ts)。 */
|
|
138
|
+
export function assistantJobResultEvent(home: string, job: Job, options: { prefix?: string; toolCount?: number } = {}): string {
|
|
139
|
+
const skills = usableAssistantSkills(home, job.assistantId);
|
|
140
|
+
const skillTitles = (job.skillsUsed ?? []).map((name) => skills.find((skill) => skill.name === name)?.title ?? name);
|
|
141
|
+
const filesNote = job.files?.length ? `\n任务交来的文件:${job.files.join("、")}。` : "";
|
|
142
|
+
const text = jobResultEvent(job, options.prefix ?? `任务「${job.title}」`, skillTitles, options.toolCount ?? job.stats.reads + job.stats.commands + job.stats.edits, sedimentHint(home, job, skills));
|
|
143
|
+
return `${text}${filesNote}`;
|
|
144
|
+
}
|
|
145
|
+
|
|
131
146
|
function trim(value: string, max = 240): string {
|
|
132
147
|
const oneLine = value.replace(/\s+/g, " ").trim();
|
|
133
148
|
return oneLine.length > max ? `${oneLine.slice(0, max - 1)}…` : oneLine;
|
|
@@ -162,7 +177,6 @@ export class JobManager {
|
|
|
162
177
|
private followUps = new Map<string, string>();
|
|
163
178
|
private finishing = new Map<string, Promise<void>>();
|
|
164
179
|
private sediments = new Map<string, { cancelled: boolean; session?: AgentSession; task?: Promise<void> }>();
|
|
165
|
-
private replied = new Set<string>();
|
|
166
180
|
private watchdog?: NodeJS.Timeout;
|
|
167
181
|
|
|
168
182
|
constructor(private readonly runtime: HobRuntime) {}
|
|
@@ -263,6 +277,7 @@ export class JobManager {
|
|
|
263
277
|
...(picked.modelReason ? { modelReason: picked.modelReason } : {}),
|
|
264
278
|
...(input.files?.length ? { inputs: input.files } : {}),
|
|
265
279
|
...(input.routineId ? { routineId: input.routineId } : {}),
|
|
280
|
+
...(input.skills?.length ? { skills: input.skills } : {}),
|
|
266
281
|
...(input.delegationId ? { delegationId: input.delegationId } : {}),
|
|
267
282
|
};
|
|
268
283
|
this.store.saveJob(job);
|
|
@@ -331,16 +346,16 @@ export class JobManager {
|
|
|
331
346
|
const customTools = [registerReportTool(this.runtime, running.id), registerAskTool(this.runtime, running.id), findKnowledgeTool(this.runtime)];
|
|
332
347
|
if (knowledge.length) customTools.push(searchKnowledgeTool(this.runtime, running.assistantId));
|
|
333
348
|
if (abilities.image) customTools.push(registerImageTool(this.runtime, running.id));
|
|
334
|
-
if (this.runtime.settings.settings.general.showPage !== false) customTools.push(pageTool(this.runtime, running.assistantId, false));
|
|
335
349
|
customTools.push(...searchTools(this.runtime.home, () => this.runtime.settings.settings.search));
|
|
336
|
-
const desktop = await availableDesktopTools(this.runtime, running.assistantId, !!this.runtime.models.find(running.model ?? this.runtime.settings.settings.models.work.ref)?.input.includes("image"));
|
|
350
|
+
const desktop = await availableDesktopTools(this.runtime, running.assistantId, !!this.runtime.models.find(running.model ?? this.runtime.settings.settings.models.work.ref)?.input.includes("image"), running.title, join(this.store.workDir(running.id), "screenshots"));
|
|
337
351
|
if (this.store.job(running.id)?.status !== "running") { await desktop?.dispose(); return; }
|
|
338
352
|
if (desktop) customTools.push(...desktop.tools());
|
|
339
353
|
// 定时跑的就是 Worker:主助手的任务要看得到各助这段时间的动静才能写早报
|
|
340
354
|
if (assistant?.primary) customTools.push(overviewTool(this.runtime));
|
|
341
355
|
// MCP:配置里有启用中的服务才加载(改完配置,之后新开的任务生效)
|
|
342
356
|
const mcp = mcpEnabled(this.runtime.home);
|
|
343
|
-
const
|
|
357
|
+
const allOwnSkills = enabledAssistantSkills(this.runtime.home, running.assistantId);
|
|
358
|
+
const ownSkills = allOwnSkills.filter((skill) => running.skills?.includes(skill.name));
|
|
344
359
|
const ownNames = new Set(ownSkills.map((skill) => skill.name));
|
|
345
360
|
const opened = await this.runtime.pi.open({
|
|
346
361
|
file: this.store.workerFile(running.id),
|
|
@@ -351,7 +366,7 @@ export class JobManager {
|
|
|
351
366
|
skills: [...ownSkills, ...availableSkills(this.runtime.home, undefined, this.runtime.settings.settings.skills?.disabled).filter((skill) => !ownNames.has(skill.name))],
|
|
352
367
|
disabledSkills: this.runtime.settings.settings.skills?.disabled,
|
|
353
368
|
systemPrompt: () =>
|
|
354
|
-
buildWorkerPrompt(this.store, running.id, { ...this.promptSettings(), abilities, knowledgeNames: knowledge.map((collection) => collection.name) }),
|
|
369
|
+
buildWorkerPrompt(this.store, running.id, { ...this.promptSettings(), abilities, desktopEnabled: !!desktop, knowledgeNames: knowledge.map((collection) => collection.name) }),
|
|
355
370
|
tools: [
|
|
356
371
|
...workerTools(abilities),
|
|
357
372
|
...customTools.map((tool) => tool.name),
|
|
@@ -570,7 +585,7 @@ export class JobManager {
|
|
|
570
585
|
await this.finish(worker.job.id, "stuck", undefined, "未提交结果");
|
|
571
586
|
return;
|
|
572
587
|
}
|
|
573
|
-
await session.prompt("
|
|
588
|
+
await session.prompt("这一轮结束了,还没有交结果。做完了就调用 report 交结果;没做完,也用 report 说明做到哪、卡在哪。");
|
|
574
589
|
}
|
|
575
590
|
|
|
576
591
|
/** report 工具收到结果后由 worker-tools 调用。 */
|
|
@@ -586,7 +601,7 @@ export class JobManager {
|
|
|
586
601
|
reportFromTool(jobId: string, data: { outcome: "done" | "stuck" | "need_approval"; result: string; files?: string[] }): void {
|
|
587
602
|
const worker = this.workers.get(jobId);
|
|
588
603
|
if (!worker) return;
|
|
589
|
-
worker.reported = data;
|
|
604
|
+
worker.reported = { ...data, ...(data.files ? { files: data.files.map((path) => resolve(worker.job.cwd ?? this.store.workDir(jobId), path)) } : {}) };
|
|
590
605
|
worker.finishTimer ??= setTimeout(() => void this.applyReport(jobId), 200);
|
|
591
606
|
}
|
|
592
607
|
|
|
@@ -602,13 +617,6 @@ export class JobManager {
|
|
|
602
617
|
const task = this.finishOnce(jobId, status, summary, reason, files);
|
|
603
618
|
this.finishing.set(jobId, task);
|
|
604
619
|
try { await task; } finally { this.finishing.delete(jobId); }
|
|
605
|
-
const job = this.store.job(jobId);
|
|
606
|
-
const steps = status === "done" && job?.status === "done" ? this.transcript(jobId) : [];
|
|
607
|
-
// 自动沉淀:用户中途接着说过话;或者没照着技能做、而且是定时执行或动手 20 次以上。
|
|
608
|
-
// 照着技能顺利做完的不再沉淀,免得每天跑完都改一版。
|
|
609
|
-
const followed = this.replied.has(jobId) || steps.some((step) => step.from === "user" && step.text !== "沉淀技能");
|
|
610
|
-
const fresh = !job?.skillsUsed?.length && (Boolean(job?.routineId) || steps.filter((step) => step.kind === "tool").length >= 20);
|
|
611
|
-
if (job?.status === "done" && status === "done" && (followed || fresh)) this.startSediment(jobId);
|
|
612
620
|
}
|
|
613
621
|
|
|
614
622
|
private async finishOnce(jobId: string, status: JobStatus, summary?: string, reason?: string, files?: string[]): Promise<void> {
|
|
@@ -679,9 +687,7 @@ export class JobManager {
|
|
|
679
687
|
jobId: job.id,
|
|
680
688
|
});
|
|
681
689
|
this.bus.emit({ type: "profile", assistantId: job.assistantId });
|
|
682
|
-
const
|
|
683
|
-
// 图片已经开了这一组,回复接在图片后面
|
|
684
|
-
const cont = posted ? ({ cont: true } as const) : undefined;
|
|
690
|
+
const toolCount = this.transcript(job.id).filter((step) => step.kind === "tool" && (!next.startedAt || step.at >= next.startedAt)).length;
|
|
685
691
|
|
|
686
692
|
if (job.routineId) {
|
|
687
693
|
const routine = this.store.routine(job.routineId);
|
|
@@ -697,44 +703,16 @@ export class JobManager {
|
|
|
697
703
|
this.store.saveRoutine(updated);
|
|
698
704
|
this.bus.emit({ type: "routine", routine: updated });
|
|
699
705
|
await this.runtime.router
|
|
700
|
-
.event(job.assistantId,
|
|
706
|
+
.event(job.assistantId, assistantJobResultEvent(this.runtime.home, next, { prefix: `定时任务「${routine.name}」`, toolCount }), {
|
|
701
707
|
routine: { id: routine.id, name: routine.name, jobId: job.id },
|
|
702
|
-
...cont,
|
|
703
708
|
})
|
|
704
709
|
.catch(() => undefined);
|
|
705
710
|
}
|
|
706
711
|
} else if (assistant) {
|
|
707
|
-
const text =
|
|
708
|
-
if (delegationId) void this.runtime.router.relay(job.assistantId, text, delegationId
|
|
709
|
-
else await this.runtime.router.event(job.assistantId, text
|
|
710
|
-
}
|
|
711
|
-
}
|
|
712
|
-
|
|
713
|
-
/**
|
|
714
|
-
* 任务产出的图片先直接发到对话里(前端渲染成缩略图),再告诉助手发了什么、还有哪些别的文件,
|
|
715
|
-
* 免得它再派一个任务去"取回"图片。
|
|
716
|
-
*/
|
|
717
|
-
private async postFiles(job: Job): Promise<{ note: string; posted: number }> {
|
|
718
|
-
const posted: string[] = [];
|
|
719
|
-
const others: string[] = [];
|
|
720
|
-
for (const path of job.files ?? []) {
|
|
721
|
-
if (isImagePath(path) && existsSync(path)) {
|
|
722
|
-
// 第一张开新的一组:任务结果不跟在派任务那轮回复后面
|
|
723
|
-
await this.runtime.router.addItem(job.assistantId, {
|
|
724
|
-
kind: "file",
|
|
725
|
-
path,
|
|
726
|
-
name: basename(path),
|
|
727
|
-
...(posted.length ? {} : { lead: true as const }),
|
|
728
|
-
});
|
|
729
|
-
posted.push(basename(path));
|
|
730
|
-
} else {
|
|
731
|
-
others.push(path);
|
|
732
|
-
}
|
|
712
|
+
const text = assistantJobResultEvent(this.runtime.home, next, { toolCount });
|
|
713
|
+
if (delegationId) void this.runtime.router.relay(job.assistantId, text, delegationId).catch(() => this.runtime.delegations.turnEnded(delegationId, "目标助手处理失败", true));
|
|
714
|
+
else await this.runtime.router.event(job.assistantId, text).catch(() => undefined);
|
|
733
715
|
}
|
|
734
|
-
const lines: string[] = [];
|
|
735
|
-
if (posted.length) lines.push(`图片已发到对话里:${posted.join("、")}`);
|
|
736
|
-
if (others.length) lines.push(`文件:${others.join("、")}`);
|
|
737
|
-
return { note: lines.length ? `\n${lines.join("\n")}` : "", posted: posted.length };
|
|
738
716
|
}
|
|
739
717
|
|
|
740
718
|
async reply(jobId: string, text: string, source: "user" | "router" = "user"): Promise<void> {
|
|
@@ -745,11 +723,10 @@ export class JobManager {
|
|
|
745
723
|
await sediment.session?.abort().catch(() => undefined);
|
|
746
724
|
await sediment.task;
|
|
747
725
|
}
|
|
748
|
-
this.replied.add(jobId);
|
|
749
726
|
const job = this.store.job(jobId);
|
|
750
727
|
if (!job) throw new Error("没有这个任务。");
|
|
751
728
|
if (ENDED.includes(job.status)) {
|
|
752
|
-
const { endedAt, statusReason, durationMs, summary, files, reportPath, ...rest } = job;
|
|
729
|
+
const { endedAt, statusReason, durationMs, summary, files, reportPath, sediment, ...rest } = job;
|
|
753
730
|
const previous = job.rounds ?? [];
|
|
754
731
|
// 这一轮的报告另存一份:下一轮写报告会覆盖原文件。原文件不在了就不存,不让续做因此失败
|
|
755
732
|
const savedReport = reportPath && existsSync(reportPath) ? join(dirname(reportPath), `report.round-${previous.length + 1}.md`) : undefined;
|
|
@@ -766,7 +743,7 @@ export class JobManager {
|
|
|
766
743
|
this.store.saveJob(job);
|
|
767
744
|
throw error;
|
|
768
745
|
}
|
|
769
|
-
this.followUps.set(jobId,
|
|
746
|
+
this.followUps.set(jobId, `${source === "router" ? `「${this.store.assistant(job.assistantId)?.name ?? job.assistantId}」` : "用户"}接着说(${monthDayTimeText(this.runtime.settings.settings.general.timezone)}):${text}`);
|
|
770
747
|
this.bus.emit({ type: "job", job: queued });
|
|
771
748
|
this.publishBusy(job.assistantId);
|
|
772
749
|
this.queue.push(jobId);
|
|
@@ -783,7 +760,7 @@ export class JobManager {
|
|
|
783
760
|
: { ...asking, status: "expired" };
|
|
784
761
|
this.store.saveApproval(answered);
|
|
785
762
|
this.bus.emit({ type: "approval", approval: answered });
|
|
786
|
-
this.resolveAsk(answered, source === "user" ? `用户没有选给出的选项,而是在对话里回复:${text}` :
|
|
763
|
+
this.resolveAsk(answered, source === "user" ? `用户没有选给出的选项,而是在对话里回复:${text}` : `「${this.store.assistant(job.assistantId)?.name ?? job.assistantId}」回复:${text}`);
|
|
787
764
|
return;
|
|
788
765
|
}
|
|
789
766
|
// 先显示出来:任务在跑命令时要等这一步做完才读到,界面上标着「等待读取」
|
|
@@ -799,45 +776,66 @@ export class JobManager {
|
|
|
799
776
|
}
|
|
800
777
|
}
|
|
801
778
|
|
|
802
|
-
|
|
779
|
+
private setSediment(jobId: string, sediment: SedimentStatus): void {
|
|
780
|
+
const job = this.store.job(jobId);
|
|
781
|
+
if (!job) return;
|
|
782
|
+
const next = { ...job, sediment };
|
|
783
|
+
this.store.saveJob(next);
|
|
784
|
+
this.bus.emit({ type: "job", job: next });
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
startSediment(jobId: string, options: { into?: string; note?: string } = {}): void {
|
|
803
788
|
const job = this.store.job(jobId);
|
|
804
789
|
if (!job) throw new Error("任务不存在");
|
|
805
790
|
if (job.status !== "done" || this.finishing.has(jobId) || this.workers.has(jobId)) throw new Error("只有已完成的任务可以沉淀");
|
|
806
791
|
if (this.sediments.has(jobId)) throw new Error("任务正在沉淀技能");
|
|
792
|
+
const skills = usableAssistantSkills(this.runtime.home, job.assistantId);
|
|
793
|
+
const into = options.into ? skills.find((skill) => skill.name === options.into) : undefined;
|
|
794
|
+
if (options.into && !into) throw new Error(`没有这个技能:${options.into}。现有的:${skills.map((skill) => skill.name).join("、") || "无"}`);
|
|
807
795
|
const state: { cancelled: boolean; session?: AgentSession; task?: Promise<void> } = { cancelled: false };
|
|
808
796
|
this.sediments.set(jobId, state);
|
|
809
|
-
|
|
797
|
+
this.setSediment(jobId, { status: "running" });
|
|
798
|
+
state.task = this.sedimentRound(job, state, into, options.note).catch(async (error) => {
|
|
810
799
|
if (state.cancelled) return;
|
|
811
800
|
const message = error instanceof Error ? error.message : String(error);
|
|
812
801
|
console.error("沉淀技能失败:", error);
|
|
802
|
+
this.setSediment(jobId, { status: "failed", reason: message });
|
|
813
803
|
await this.runtime.router.addItem(job.assistantId, { kind: "note", icon: "info", text: `技能沉淀失败:${message}` }).catch((noteError) => console.error("写入沉淀失败提示失败:", noteError));
|
|
814
|
-
}).finally(() => {
|
|
815
|
-
|
|
816
|
-
|
|
804
|
+
}).finally(async () => {
|
|
805
|
+
try {
|
|
806
|
+
const sediment = this.store.job(jobId)?.sediment;
|
|
807
|
+
if (!sediment || sediment.status === "running") return;
|
|
808
|
+
const result = sediment.status === "saved" ? (sediment.updated ? `已更新技能「${sediment.title}」(${sediment.name})· 第 ${sediment.version} 版` : `已沉淀技能「${sediment.title}」(${sediment.name})`) : sediment.status === "empty" ? `没有沉淀,${sediment.reason}` : `沉淀失败,${sediment.reason}`;
|
|
809
|
+
await this.runtime.router.event(job.assistantId, `任务「${job.title}」(${job.id})沉淀完了:${result}。`).catch((error) => console.error("发送沉淀结果失败:", error));
|
|
810
|
+
} finally { this.sediments.delete(jobId); }
|
|
817
811
|
});
|
|
818
812
|
}
|
|
819
813
|
|
|
820
|
-
private async sedimentRound(job: Job, state: { cancelled: boolean; session?: AgentSession },
|
|
821
|
-
let
|
|
814
|
+
private async sedimentRound(job: Job, state: { cancelled: boolean; session?: AgentSession }, into: ReturnType<typeof usableAssistantSkills>[number] | undefined, note?: string): Promise<void> {
|
|
815
|
+
let saved: Extract<SedimentStatus, { status: "saved" }> | undefined;
|
|
822
816
|
const savedSkills = new Set<string>();
|
|
823
817
|
let lastText = "";
|
|
824
818
|
const skillTool = {
|
|
825
819
|
name: "save_skill", label: "保存技能",
|
|
826
|
-
description: "
|
|
820
|
+
description: "保存技能。content 是 SKILL.md 的正文,开头的 name 和 description 由 Hob 写。files 放脚本和参考资料,路径以 scripts/ 或 references/ 开头:给 from(工作目录里已有文件的路径,直接复制),或者给 content。",
|
|
827
821
|
parameters: Type.Object({
|
|
828
|
-
name: Type.String({ description: "
|
|
829
|
-
title: Type.String({ description: "
|
|
830
|
-
description: Type.String({ description: "
|
|
822
|
+
...(!into ? { name: Type.String({ description: "技能的目录名,只用小写字母、数字和连字符,如 weekly-report。" }) } : {}),
|
|
823
|
+
title: Type.String({ description: "中文名,如「每周周报」。" }),
|
|
824
|
+
description: Type.String({ description: "什么时候用,一句话,120 字以内。" }),
|
|
831
825
|
content: Type.String({ description: "SKILL.md 正文。" }),
|
|
832
826
|
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(工作目录里已有文件的路径,直接复制)。" })),
|
|
833
827
|
}),
|
|
834
828
|
execute: async (_id: string, params: any) => {
|
|
835
|
-
calledSave = true;
|
|
836
829
|
if (state.cancelled || this.store.job(job.id)?.status !== "done") throw new Error("任务已继续,取消沉淀");
|
|
830
|
+
const name = into?.name ?? params.name;
|
|
831
|
+
const overwriteVersion = savedSkills.has(name);
|
|
832
|
+
if (!into && !overwriteVersion && listAssistantSkills(this.runtime.home, job.assistantId).some((skill) => skill.name === name)) {
|
|
833
|
+
return { content: [{ type: "text" as const, text: `「${name}」已经有了,新技能换个名字。` }], details: undefined, isError: true };
|
|
834
|
+
}
|
|
837
835
|
const files = readSkillFiles(job.cwd ?? this.store.workDir(job.id), params.files);
|
|
838
|
-
const
|
|
839
|
-
const skill = saveAssistantSkill(this.runtime.home, job.assistantId, { ...params, files }, job.id, false, { overwriteVersion });
|
|
836
|
+
const skill = saveAssistantSkill(this.runtime.home, job.assistantId, { ...params, name, files }, job.id, false, { overwriteVersion });
|
|
840
837
|
savedSkills.add(skill.name);
|
|
838
|
+
saved = { status: "saved", name: skill.name, title: skill.title, version: skill.version, updated: !!into };
|
|
841
839
|
if (!overwriteVersion) {
|
|
842
840
|
let text = skill.version === 1 ? `已沉淀技能「${skill.title}」` : `已更新技能「${skill.title}」· 第 ${skill.version} 版`;
|
|
843
841
|
const routine = job.routineId && this.store.routine(job.routineId);
|
|
@@ -851,45 +849,56 @@ export class JobManager {
|
|
|
851
849
|
return { content: [{ type: "text" as const, text: "技能已保存。" }], details: undefined };
|
|
852
850
|
},
|
|
853
851
|
};
|
|
854
|
-
const
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
- 能写成脚本的写成只读脚本,放在 scripts/,一次输出要看的全部结果。写好后用 bash 真跑一遍,输出对得上再保存。偶尔才用得上的细节放 references/,按主题起文件名。
|
|
865
|
-
- 会改东西的步骤只写进文档,不写进脚本,也不要执行。
|
|
866
|
-
|
|
867
|
-
不写进技能:
|
|
868
|
-
- 临时故障和网络错误。环境没装好、没配好导致的失败,只写怎么装好、配好。
|
|
869
|
-
- 「某个工具不能用」这类结论:环境修好以后,它还会一直挡着你。
|
|
870
|
-
- 这次没走通的尝试:不能写成推荐做法。
|
|
871
|
-
|
|
872
|
-
下面已有同类技能的,修补它,不要另建;新建时,名字和说明按这一类事来起,不按这一次。没有值得沉淀的,就回复「没有需要沉淀的」,不调用 save_skill。
|
|
873
|
-
|
|
874
|
-
这个助手已有的技能:${existing}`;
|
|
852
|
+
const message = `${SEDIMENT_PROMPT_HEAD},下次接到同类的事,照着它就能一次做成用户要的样子。
|
|
853
|
+
|
|
854
|
+
下一次的情况会和这次不一样,所以技能写这一类事怎么做,不写这一次的经过和数据。按做事的顺序写步骤,带上要用的命令、查询和判断点,怎么判断结果对不对,写在对应的那一步里。用户在过程中纠正过的做法、对结果提过的要求,最能帮下次少走弯路,要写进去。踩过的坑写成一条规则加一句原因,放在它影响的那一步;会变的情况写怎么查,不写死。每条经验只写一次,修补时直接改掉写错的那句。
|
|
855
|
+
|
|
856
|
+
技能要照着能跑通。写进去的只读命令和查询,存之前再跑一遍,确认照着写的就能用;能写成脚本的步骤,写成只读脚本放在 scripts/,一次输出要看的全部结果,跑一遍、输出对得上再保存。会改东西的步骤只写在正文里,留给以后的任务确认后再执行。偶尔才用得上的细节放 references/,按主题起文件名。
|
|
857
|
+
|
|
858
|
+
有几类东西留在技能外面:临时故障和网络错误,环境没装好、没配好导致的失败只写怎么装好、配好;「某个工具不能用」这类结论,环境修好以后它还会一直挡路;这次没走通的尝试。助手的规则和工具说明里已经有的,也不用重复。
|
|
859
|
+
|
|
860
|
+
${into ? `这次在技能「${into.title}」(${into.name},第 ${into.version} 版)的基础上改,先读它现在的内容。` : "这次新建一个技能,名字和说明按这一类事来起,不按这一次。"}${note ? `\n「${this.store.assistant(job.assistantId)?.name ?? job.assistantId}」交代:${note}` : ""}
|
|
861
|
+
一次性的事、几步就做完的事,或者照常做就不会走弯路的事,没有值得沉淀的,回复「没有需要沉淀的」就好。`;
|
|
875
862
|
if (state.cancelled) return;
|
|
863
|
+
// 沉淀时只补上助手明确指定的目标技能,以及交代中点名的技能。
|
|
864
|
+
const mentioned = usableAssistantSkills(this.runtime.home, job.assistantId).filter((skill) => job.skills?.includes(skill.name) || skill.name === into?.name || (!!note && (note.includes(skill.name) || note.includes(skill.title))));
|
|
865
|
+
const own = enabledAssistantSkills(this.runtime.home, job.assistantId).filter((skill) => mentioned.some((item) => item.name === skill.name));
|
|
866
|
+
const ownNames = new Set(own.map((skill) => skill.name));
|
|
867
|
+
const common = availableSkills(this.runtime.home, undefined, this.runtime.settings.settings.skills?.disabled).filter((skill) => !ownNames.has(skill.name));
|
|
876
868
|
const opened = await this.runtime.pi.open({
|
|
877
869
|
file: this.store.workerFile(job.id), cwd: job.cwd ?? homedir(),
|
|
878
870
|
model: job.model ?? this.runtime.settings.settings.models.work.ref,
|
|
879
871
|
thinking: job.thinking ?? this.runtime.settings.settings.models.work.thinking,
|
|
880
872
|
memory: "off", systemPrompt: () => buildWorkerPrompt(this.store, job.id, this.promptSettings()),
|
|
881
873
|
tools: ["read", "ls", "grep", "find", "bash", "save_skill"], customTools: [skillTool],
|
|
874
|
+
skills: [...common, ...own],
|
|
882
875
|
});
|
|
883
876
|
state.session = opened.session;
|
|
877
|
+
const steps = new Map<string, JobStep>();
|
|
884
878
|
opened.session.subscribe((event) => {
|
|
885
|
-
if (event.type === "
|
|
879
|
+
if (event.type === "message_start" && event.message.role === "user") {
|
|
880
|
+
this.bus.emit({ type: "job_step", jobId: job.id, step: { id: `u${event.message.timestamp}`, kind: "text", at: new Date(event.message.timestamp).toISOString(), from: "user", text: "沉淀技能" } });
|
|
881
|
+
} else if (event.type === "tool_execution_start") {
|
|
882
|
+
const step = toolStep({ id: event.toolCallId, name: event.toolName, args: event.args ?? {}, at: new Date().toISOString(), cwd: job.cwd ?? "", running: true });
|
|
883
|
+
steps.set(event.toolCallId, step);
|
|
884
|
+
this.bus.emit({ type: "job_step", jobId: job.id, step });
|
|
885
|
+
} else if (event.type === "tool_execution_update" || event.type === "tool_execution_end") {
|
|
886
|
+
const step = steps.get(event.toolCallId);
|
|
887
|
+
if (step) {
|
|
888
|
+
const text = textOfContent(event.type === "tool_execution_update" ? event.partialResult?.content : event.result?.content);
|
|
889
|
+
const next = withOutput(step, text, { limit: STEP_EVENT_LIMIT, ...(event.type === "tool_execution_end" ? { isError: event.isError, durationMs: event.durationMs } : {}), save: (id, value) => this.store.writeStepOutput(job.id, id, value) });
|
|
890
|
+
this.bus.emit({ type: "job_step", jobId: job.id, step: next });
|
|
891
|
+
if (event.type === "tool_execution_end") steps.delete(event.toolCallId);
|
|
892
|
+
}
|
|
893
|
+
} else if (event.type === "message_end" && event.message.role === "assistant") {
|
|
894
|
+
lastText = textOfContent(event.message.content).trim();
|
|
895
|
+
const worker = { job } as WorkerState;
|
|
896
|
+
this.recordSteps(worker, event.message);
|
|
897
|
+
}
|
|
886
898
|
});
|
|
887
899
|
try {
|
|
888
900
|
if (!state.cancelled) await opened.session.prompt(message);
|
|
889
|
-
if (!state.cancelled
|
|
890
|
-
const reason = lastText && lastText !== "没有需要沉淀的" ? `:${lastText}` : "";
|
|
891
|
-
await this.runtime.router.addItem(job.assistantId, { kind: "note", icon: "skill", text: `「${job.title}」没有可沉淀的做法${reason}` });
|
|
892
|
-
}
|
|
901
|
+
if (!state.cancelled) this.setSediment(job.id, saved ?? { status: "empty", reason: lastText || "没有需要沉淀的" });
|
|
893
902
|
} finally { opened.session.dispose(); }
|
|
894
903
|
}
|
|
895
904
|
|
|
@@ -929,18 +938,19 @@ export class JobManager {
|
|
|
929
938
|
createdAt: new Date().toISOString(),
|
|
930
939
|
};
|
|
931
940
|
this.store.saveApproval(approval);
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
this.
|
|
935
|
-
pushApproval({ push: this.runtime.push, store: this.store }, approval);
|
|
941
|
+
if (!worker) { this.store.saveApproval({ ...approval, status: "expired" }); return "(任务未在运行)"; }
|
|
942
|
+
const answer = new Promise<string>((resolve) => { worker.ask = resolve; });
|
|
943
|
+
const delegationId = !job.routineId && job.delegationId && this.runtime.delegations.resume(job.delegationId) ? job.delegationId : undefined;
|
|
936
944
|
const waiting: Job = { ...job, status: "waiting" };
|
|
937
945
|
this.store.saveJob(waiting);
|
|
946
|
+
worker.job = waiting;
|
|
947
|
+
worker.pausedAt = Date.now();
|
|
938
948
|
this.bus.emit({ type: "job", job: waiting });
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
949
|
+
const sentence = (value: string) => /[。!?.!?]\s*$/.test(value) ? value : `${value}。`;
|
|
950
|
+
const text = `任务「${job.title}」(${job.id})在等你回复:${sentence(input.title)}${input.detail ? sentence(input.detail) : ""}选项:${input.options.map((option) => `${option.key} ${option.label}`).join("、")}。`;
|
|
951
|
+
if (delegationId) void this.runtime.router.relay(job.assistantId, text, delegationId).catch(() => this.runtime.delegations.turnEnded(delegationId, "目标助手处理失败", true));
|
|
952
|
+
else void this.runtime.router.event(job.assistantId, text).catch((error) => console.error("任务提问投递失败:", error));
|
|
953
|
+
return await answer;
|
|
944
954
|
}
|
|
945
955
|
|
|
946
956
|
/** Worker 的 ask_user 等答案时,审批被答复(answer 是回给模型的那句)。 */
|
|
@@ -1012,6 +1022,8 @@ export class JobManager {
|
|
|
1012
1022
|
const next: Job = { ...job, status: "failed", endedAt: now, statusReason: "Hob 重启,任务中断" };
|
|
1013
1023
|
this.store.saveJob(next);
|
|
1014
1024
|
this.bus.emit({ type: "job", job: next });
|
|
1025
|
+
} else if (job.sediment?.status === "running") {
|
|
1026
|
+
this.setSediment(job.id, { status: "failed", reason: "Hob 重启,沉淀中断" });
|
|
1015
1027
|
}
|
|
1016
1028
|
}
|
|
1017
1029
|
for (const approval of this.store.approvals()) {
|
|
@@ -38,7 +38,7 @@ export function searchKnowledgeTool(runtime: HobRuntime, assistantId: string): T
|
|
|
38
38
|
return {
|
|
39
39
|
name: "search_knowledge",
|
|
40
40
|
label: "查知识库",
|
|
41
|
-
description: "
|
|
41
|
+
description: "在绑定给你的知识库里查内容,返回和问题最相关的几段原文,每段带出处链接。",
|
|
42
42
|
parameters: Type.Object({
|
|
43
43
|
query: Type.String({ description: "要查的问题或关键词,写成完整的一句效果更好。" }),
|
|
44
44
|
collection: Type.Optional(Type.String({ description: "可选,只查这个知识库,填名称。" })),
|
package/server/src/main.ts
CHANGED
|
@@ -41,6 +41,8 @@ export async function startHob(options: StartOptions = {}): Promise<HobServer> {
|
|
|
41
41
|
runtime.routines.start();
|
|
42
42
|
runtime.jobs.startWatchdog();
|
|
43
43
|
runtime.desktopEvents.restart();
|
|
44
|
+
// 消耗统计第一次要扫一遍会话文件(几百 MB):先异步跑起来,服务不等它
|
|
45
|
+
runtime.usage.warmup();
|
|
44
46
|
|
|
45
47
|
const app = createApp(runtime, { webDir: fileURLToPath(new URL("../../dist/web", import.meta.url)), home });
|
|
46
48
|
const port = options.port ?? Number(env.HOB_PORT ?? config.port);
|
package/server/src/memory.ts
CHANGED
|
@@ -238,13 +238,13 @@ export function memoryPromptLines(provider: MemoryProvider): string[] {
|
|
|
238
238
|
if (provider === "local") {
|
|
239
239
|
return [
|
|
240
240
|
"## 记忆",
|
|
241
|
-
"
|
|
241
|
+
"你和用户是长期合作。用户过去的偏好、决定和约定,先用 memory_read 查,免得让用户再说一遍;新出现的、以后还用得上的事实,用 memory_write 记下来。记忆写短一些,过时的及时改掉或删掉。",
|
|
242
242
|
];
|
|
243
243
|
}
|
|
244
244
|
if (provider === "lore") {
|
|
245
245
|
return [
|
|
246
246
|
"## 记忆",
|
|
247
|
-
"长期记忆在 Lore
|
|
247
|
+
"长期记忆在 Lore。用户过去的偏好、决定和约定,先去 Lore 查,免得让用户再说一遍;新出现的、以后还用得上的事实,记进 Lore。读到的记忆和你刚核实的情况对不上,当场改对;已经不成立的,删掉,免得以后被它误导。",
|
|
248
248
|
];
|
|
249
249
|
}
|
|
250
250
|
return [];
|