hob-bot 1.0.4 → 1.2.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 +76 -6
- package/README.zh-CN.md +76 -6
- package/dist/web/assets/DesktopView-CEBJFAIN.js +5 -0
- package/dist/web/assets/index-Di5lyTHO.css +1 -0
- package/dist/web/assets/index-y09i1RGd.js +267 -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/package.json +7 -1
- package/server/src/actions.ts +16 -56
- package/server/src/assistant-skills.ts +215 -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 +21 -11
- 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 +325 -13
- package/server/src/job-view.ts +52 -0
- package/server/src/jobs.ts +326 -44
- 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 +4 -4
- package/server/src/pages.ts +1 -1
- package/server/src/pi.ts +92 -16
- package/server/src/prompt.ts +51 -156
- package/server/src/providers.ts +3 -2
- package/server/src/push.ts +39 -5
- package/server/src/router-activity.ts +6 -0
- package/server/src/router-tools.ts +184 -39
- package/server/src/router.ts +154 -17
- 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/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/skills.ts +18 -0
- package/shared/types.ts +104 -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-BCGEZvgg.css +0 -1
- package/dist/web/assets/index-BqkIAlk9.js +0 -258
package/server/src/router.ts
CHANGED
|
@@ -2,7 +2,7 @@ import { getSupportedThinkingLevels } from "@earendil-works/pi-ai";
|
|
|
2
2
|
import { parseStreamingJson } from "@earendil-works/pi-ai/utils/json-parse";
|
|
3
3
|
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
4
4
|
import { mkdirSync } from "node:fs";
|
|
5
|
-
import { dirname } from "node:path";
|
|
5
|
+
import { dirname, join, relative, resolve, sep } from "node:path";
|
|
6
6
|
import type { AgentSession, AgentSessionEvent, SessionEntry, SessionManager } from "@earendil-works/pi-coding-agent";
|
|
7
7
|
import { SessionManager as SessionManagerImpl } from "@earendil-works/pi-coding-agent";
|
|
8
8
|
import type {
|
|
@@ -21,22 +21,28 @@ import type { HobRuntime } from "./runtime.ts";
|
|
|
21
21
|
import { refreshSystemPrompt } from "./pi.ts";
|
|
22
22
|
import { conversationPick, defaultWorkRef, jobModel, nameOf, noteOf, thinkingForPick, thinkingLevelsOf, fitThinking, usableRefs, NO_MODEL_NOTE, NoModelError } from "./models.ts";
|
|
23
23
|
import { buildRouterTools } from "./router-tools.ts";
|
|
24
|
+
import { boundKnowledge, formatKnowledge, KNOWLEDGE_INTRO } from "./knowledge-tools.ts";
|
|
24
25
|
import { MEMORY_WRITE_TOOLS, memoryNoteText } from "./memory.ts";
|
|
25
26
|
import { buildSystemPrompt, type PromptModel } from "./prompt.ts";
|
|
26
27
|
import { availableSkills } from "./skills.ts";
|
|
28
|
+
import { usableAssistantSkills } from "./assistant-skills.ts";
|
|
27
29
|
import { activityLabel, thinkingLine } from "./router-activity.ts";
|
|
28
30
|
import { imageContents } from "./jobs.ts";
|
|
31
|
+
import { nowBlock } from "./now.ts";
|
|
32
|
+
import { monthDayTimeText } from "./time.ts";
|
|
29
33
|
|
|
30
34
|
export const ITEM_TYPE = "hob.item";
|
|
35
|
+
export const ITEM_UPDATE_TYPE = "hob.item.update";
|
|
31
36
|
export const USER_TYPE = "hob.user";
|
|
32
37
|
export const EVENT_TYPE = "hob.event";
|
|
33
38
|
|
|
34
39
|
/** 会话条目的 id 和 at 由会话文件给出,写卡片时不用传。 */
|
|
35
40
|
/** 事件附带的信息,落在会话的事件条目上:routine 给回复挂「⏱ 名字」,cont 见 walk。 */
|
|
36
41
|
export interface EventDetails {
|
|
37
|
-
routine?: { id: string; name: string };
|
|
42
|
+
routine?: { id: string; name: string; jobId?: string };
|
|
38
43
|
cont?: true;
|
|
39
44
|
delegationId?: string;
|
|
45
|
+
sourceUserId?: string;
|
|
40
46
|
}
|
|
41
47
|
|
|
42
48
|
export type NewItem = Item extends infer T ? (T extends Item ? Omit<T, "id" | "at"> : never) : never;
|
|
@@ -66,6 +72,10 @@ interface RouterState {
|
|
|
66
72
|
/** 还在跑的记忆写入工具:参数只在 tool_execution_start 里有,按 toolCallId 存着给 note 用 */
|
|
67
73
|
memoryWriteArgs: Map<string, Record<string, unknown>>;
|
|
68
74
|
currentDelegationId?: string;
|
|
75
|
+
currentUserId?: string;
|
|
76
|
+
pendingUsers?: { prompt: string; id: string }[];
|
|
77
|
+
pendingDelivery?: Promise<void>;
|
|
78
|
+
knowledgeNames?: string[];
|
|
69
79
|
delegationReply?: string[];
|
|
70
80
|
terminatingTools?: Set<string>;
|
|
71
81
|
}
|
|
@@ -114,7 +124,7 @@ export function briefModelError(raw: string | undefined): string {
|
|
|
114
124
|
function walk(entries: SessionEntry[]): { items: Item[]; lead: boolean } {
|
|
115
125
|
const items: Item[] = [];
|
|
116
126
|
let assistantIndex = 0;
|
|
117
|
-
let routine:
|
|
127
|
+
let routine: EventDetails["routine"];
|
|
118
128
|
let lead = false;
|
|
119
129
|
const push = (item: Item) => {
|
|
120
130
|
items.push(lead ? { ...item, lead: true } : item);
|
|
@@ -141,6 +151,12 @@ function walk(entries: SessionEntry[]): { items: Item[]; lead: boolean } {
|
|
|
141
151
|
push({ ...(entry.data as object), id: entry.id, at: entry.timestamp } as Item);
|
|
142
152
|
continue;
|
|
143
153
|
}
|
|
154
|
+
if (entry.type === "custom" && entry.customType === ITEM_UPDATE_TYPE) {
|
|
155
|
+
const update = entry.data as { id: string; status: "done" | "failed"; path?: string; error?: string };
|
|
156
|
+
const index = items.findIndex((item) => item.id === update.id && item.kind === "image");
|
|
157
|
+
if (index !== -1) items[index] = { ...items[index], ...update } as Item;
|
|
158
|
+
continue;
|
|
159
|
+
}
|
|
144
160
|
if (entry.type === "custom_message" && entry.customType === EVENT_TYPE) {
|
|
145
161
|
const details = entry.details as EventDetails | undefined;
|
|
146
162
|
routine = details?.routine;
|
|
@@ -203,7 +219,16 @@ export class RouterManager {
|
|
|
203
219
|
/** 测试可改小:不给就看设置里的“回复多久无响应视为受阻” */
|
|
204
220
|
stallOverrideMs?: number;
|
|
205
221
|
|
|
206
|
-
constructor(private readonly runtime: HobRuntime) {
|
|
222
|
+
constructor(private readonly runtime: HobRuntime) {
|
|
223
|
+
for (const assistant of runtime.store.assistants()) {
|
|
224
|
+
const file = runtime.store.routerFile(assistant.id);
|
|
225
|
+
if (!existsSync(file)) continue;
|
|
226
|
+
const pending = deriveItems(readEntries(file)).filter((item) => item.kind === "image" && item.status === "pending");
|
|
227
|
+
if (!pending.length) continue;
|
|
228
|
+
const manager = SessionManagerImpl.open(file, dirname(file), runtime.home);
|
|
229
|
+
for (const item of pending) manager.appendCustomEntry(ITEM_UPDATE_TYPE, { id: item.id, status: "failed", error: "生成被中断" });
|
|
230
|
+
}
|
|
231
|
+
}
|
|
207
232
|
|
|
208
233
|
private get store() {
|
|
209
234
|
return this.runtime.store;
|
|
@@ -245,8 +270,10 @@ export class RouterManager {
|
|
|
245
270
|
// 提示词里要写模型的显示名和定位,所以先把注册表拉一遍
|
|
246
271
|
await this.runtime.models.get();
|
|
247
272
|
const pick = conversationPick(this.runtime.settings.settings.models, assistant);
|
|
273
|
+
state.knowledgeNames = (await boundKnowledge(this.runtime, assistant.id)).map((collection) => collection.name);
|
|
248
274
|
const opened = await this.runtime.pi.open({
|
|
249
275
|
file: this.store.routerFile(assistant.id),
|
|
276
|
+
spillDir: join(dirname(this.store.routerFile(assistant.id)), "spill"),
|
|
250
277
|
cwd: this.store.home,
|
|
251
278
|
model: pick.ref,
|
|
252
279
|
thinking: thinkingForPick(pick, this.runtime.models),
|
|
@@ -259,19 +286,50 @@ export class RouterManager {
|
|
|
259
286
|
memory: this.runtime.settings.memory().provider,
|
|
260
287
|
models: this.promptModels(assistant.id),
|
|
261
288
|
skills: availableSkills(this.runtime.home, undefined, this.runtime.settings.settings.skills?.disabled, this.runtime.settings.memory().provider === "lore" ? this.runtime.pi.loreSkillPaths : []),
|
|
289
|
+
assistantSkills: usableAssistantSkills(this.runtime.home, assistant.id),
|
|
290
|
+
assistantSkillsDir: resolve(this.runtime.home, "assistants", assistant.id, "skills"),
|
|
262
291
|
thinkingChoice: this.runtime.settings.settings.models.thinkingChoice,
|
|
292
|
+
knowledgeNames: state.knowledgeNames,
|
|
263
293
|
});
|
|
264
294
|
},
|
|
265
295
|
tools: buildRouterTools(this.runtime, assistant.id).map((tool) => tool.name),
|
|
266
296
|
customTools: buildRouterTools(this.runtime, assistant.id),
|
|
267
297
|
});
|
|
268
298
|
state.session = opened.session;
|
|
299
|
+
if (!state.knowledgeNames.length) refreshSystemPrompt(opened.session, opened.session.getActiveToolNames().filter((name) => name !== "search_knowledge"));
|
|
269
300
|
state.manager = opened.manager;
|
|
270
301
|
state.opening = undefined;
|
|
271
302
|
opened.session.subscribe((event) => this.onSessionEvent(state, assistant.id, event));
|
|
272
303
|
this.touch(assistant.id);
|
|
273
304
|
}
|
|
274
305
|
|
|
306
|
+
private async refreshTools(state: RouterState): Promise<void> {
|
|
307
|
+
state.knowledgeNames = (await boundKnowledge(this.runtime, state.assistantId)).map((collection) => collection.name);
|
|
308
|
+
const names = state.session!.getActiveToolNames().filter((name) => name !== "search_knowledge");
|
|
309
|
+
if (state.knowledgeNames.length) names.push("search_knowledge");
|
|
310
|
+
refreshSystemPrompt(state.session!, names);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
private async automaticKnowledge(state: RouterState, query: string): Promise<string> {
|
|
314
|
+
const ids = this.store.assistant(state.assistantId)?.knowledge ?? [];
|
|
315
|
+
if ([...query.trim()].length < 4 || !ids.length || this.runtime.settings.settings.knowledge?.embedding.source !== "openai") return "";
|
|
316
|
+
this.setActivity(state, { state: "tool", label: "正在查知识库" });
|
|
317
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
318
|
+
try {
|
|
319
|
+
const results = await Promise.race([
|
|
320
|
+
this.runtime.knowledge.search(query, ids, { limit: 3, gate: true }),
|
|
321
|
+
new Promise<never>((_, reject) => { timer = setTimeout(() => reject(new Error("知识库检索超时")), 3000); }),
|
|
322
|
+
]);
|
|
323
|
+
const text = formatKnowledge(results, 2400 - [...KNOWLEDGE_INTRO].length - 1);
|
|
324
|
+
return text ? `\n\n${KNOWLEDGE_INTRO}\n${text}` : "";
|
|
325
|
+
} catch {
|
|
326
|
+
return "";
|
|
327
|
+
} finally {
|
|
328
|
+
if (timer) clearTimeout(timer);
|
|
329
|
+
if (state.activity?.label === "正在查知识库") this.setActivity(state, null);
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
275
333
|
/** 这个助手能派、当前可用的模型(提示词里那一节)。 */
|
|
276
334
|
private promptModels(assistantId: string): PromptModel[] {
|
|
277
335
|
const assistant = this.store.assistant(assistantId);
|
|
@@ -327,8 +385,16 @@ export class RouterManager {
|
|
|
327
385
|
}
|
|
328
386
|
}
|
|
329
387
|
if (event.type === "agent_end" || (event.type === "message_end" && event.message.role === "assistant" && ["error", "aborted"].includes(event.message.stopReason))) this.setActivity(state, null);
|
|
388
|
+
if (event.type === "message_start" && event.message.role === "user") {
|
|
389
|
+
const prompt = messageText(event.message.content);
|
|
390
|
+
const index = state.pendingUsers?.findIndex((user) => user.prompt.trim() === prompt) ?? -1;
|
|
391
|
+
if (index >= 0) state.currentUserId = state.pendingUsers!.splice(index, 1)[0].id;
|
|
392
|
+
}
|
|
330
393
|
if (event.type === "message_start" && event.message.role === "custom" && event.message.customType === EVENT_TYPE) {
|
|
331
394
|
const details = event.message.details as EventDetails | undefined;
|
|
395
|
+
const users = deriveItems(this.entries(assistantId)).filter((item) => item.kind === "user");
|
|
396
|
+
const source = details?.sourceUserId ? users.find((item) => item.id === details.sourceUserId) : undefined;
|
|
397
|
+
state.currentUserId = (source ?? users.at(-1))?.id;
|
|
332
398
|
if (details?.delegationId && details.delegationId !== state.currentDelegationId) {
|
|
333
399
|
state.currentDelegationId = details.delegationId;
|
|
334
400
|
state.delegationReply = [];
|
|
@@ -529,7 +595,7 @@ export class RouterManager {
|
|
|
529
595
|
}
|
|
530
596
|
}
|
|
531
597
|
|
|
532
|
-
/**
|
|
598
|
+
/** 助手没了(归档):关掉它的会话,内存里也不留 —— 它的目录随后会被移进归档。 */
|
|
533
599
|
forget(assistantId: string): void {
|
|
534
600
|
const state = this.routers.get(assistantId);
|
|
535
601
|
if (!state) return;
|
|
@@ -556,18 +622,67 @@ export class RouterManager {
|
|
|
556
622
|
const id = state.manager!.appendCustomEntry(ITEM_TYPE, item);
|
|
557
623
|
const full = { ...item, ...(lead ? { lead: true } : {}), id, at: this.entryAt(state, id) } as Item;
|
|
558
624
|
this.bus.emit({ type: "item", assistantId, item: full });
|
|
625
|
+
if (full.kind === "file") {
|
|
626
|
+
const rel = relative(join(this.runtime.home, "pages"), resolve(full.path));
|
|
627
|
+
const page = rel !== ".." && !rel.startsWith(`..${sep}`);
|
|
628
|
+
void this.runtime.knowledge.collect({ origin: "output", path: full.path, assistantId, itemId: id, ...(page ? { name: full.name, description: full.meta } : {}) }).catch((error) => console.error("知识库收集失败:", error));
|
|
629
|
+
}
|
|
559
630
|
return full;
|
|
560
631
|
}
|
|
561
632
|
|
|
633
|
+
async updateImage(assistantId: string, id: string, update: { status: "done" | "failed"; path?: string; error?: string }): Promise<void> {
|
|
634
|
+
const state = await this.ensure(assistantId);
|
|
635
|
+
const item = deriveItems(this.entries(assistantId)).find((entry) => entry.id === id && entry.kind === "image");
|
|
636
|
+
if (!item) throw new Error(`图片条目不存在:${id}`);
|
|
637
|
+
state.manager!.appendCustomEntry(ITEM_UPDATE_TYPE, { id, ...update });
|
|
638
|
+
this.bus.emit({ type: "item", assistantId, item: { ...item, ...update } });
|
|
639
|
+
if (item.kind === "image" && update.status === "done" && update.path) {
|
|
640
|
+
void this.runtime.knowledge.collect({ origin: "output", path: update.path, description: item.prompt, assistantId, itemId: id }).catch((error) => console.error("知识库收集失败:", error));
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
|
|
562
644
|
currentDelegation(assistantId: string): string | undefined {
|
|
563
645
|
return this.routers.get(assistantId)?.currentDelegationId;
|
|
564
646
|
}
|
|
565
647
|
|
|
566
|
-
|
|
648
|
+
/** 取当前回合来源及其之前的可见对话;事件取最近用户消息。 */
|
|
649
|
+
originalWords(assistantId: string, note?: string, recent = false, extraFiles: string[] = []): { text: string; files: string[] } {
|
|
650
|
+
const items = deriveItems(this.entries(assistantId));
|
|
651
|
+
const state = this.routers.get(assistantId);
|
|
652
|
+
const source = items.find((item) => item.kind === "user" && item.id === state?.currentUserId)
|
|
653
|
+
?? items.findLast((item) => item.kind === "user");
|
|
654
|
+
const user = source?.kind === "user" ? source : undefined;
|
|
655
|
+
const files = [...new Set([...(user?.files ?? []), ...extraFiles])];
|
|
656
|
+
const timezone = this.runtime.settings.settings.general.timezone;
|
|
657
|
+
const name = this.assistant(assistantId).name;
|
|
658
|
+
const sections: string[] = [];
|
|
659
|
+
if (user) {
|
|
660
|
+
const header = user.from ? `${user.from.name} 转来` : "用户原话";
|
|
661
|
+
const lines = [`${header}(${monthDayTimeText(timezone, new Date(user.at))}):`, user.text];
|
|
662
|
+
if (user.quote) lines.push(`(引用:「${user.quote.text}」)`);
|
|
663
|
+
sections.push(lines.filter(Boolean).join("\n"));
|
|
664
|
+
if (recent) {
|
|
665
|
+
const previous = items.slice(0, items.indexOf(user)).filter((item) => item.kind === "user" || item.kind === "assistant").slice(-6);
|
|
666
|
+
if (previous.length) sections.push(`最近的对话:\n${previous.map((item) => {
|
|
667
|
+
if (item.kind !== "user" && item.kind !== "assistant") return "";
|
|
668
|
+
const clock = new Intl.DateTimeFormat("sv-SE", { timeZone: timezone, hour: "2-digit", minute: "2-digit", hour12: false }).format(new Date(item.at));
|
|
669
|
+
const characters = [...item.text];
|
|
670
|
+
const text = characters.length > 1500 ? `${characters.slice(0, 1500).join("")}…` : item.text;
|
|
671
|
+
return `${item.kind === "user" ? "用户" : name}(${clock}):${text}`;
|
|
672
|
+
}).join("\n")}`);
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
if (files.length) sections.push(`附件:${files.join("、")}`);
|
|
676
|
+
if (note) sections.push(`${name} 的备注:${note}`);
|
|
677
|
+
return { text: sections.join("\n\n"), files };
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
async addForwarded(assistantId: string, text: string, delegationId: string, sourceId = "hob", files: string[] = []): Promise<Item> {
|
|
567
681
|
const state = await this.ensure(assistantId);
|
|
568
682
|
const from = { assistantId: sourceId, name: this.store.assistant(sourceId)?.name ?? sourceId, delegationId };
|
|
569
|
-
const
|
|
570
|
-
const
|
|
683
|
+
const data = { text, from, ...(files.length ? { files } : {}) };
|
|
684
|
+
const id = state.manager!.appendCustomEntry(USER_TYPE, data);
|
|
685
|
+
const item: Item = { kind: "user", id, at: this.entryAt(state, id), ...data };
|
|
571
686
|
this.bus.emit({ type: "item", assistantId, item });
|
|
572
687
|
return item;
|
|
573
688
|
}
|
|
@@ -582,16 +697,18 @@ export class RouterManager {
|
|
|
582
697
|
assistantId: string,
|
|
583
698
|
text: string,
|
|
584
699
|
details: EventDetails | undefined,
|
|
700
|
+
knowledgeQuery?: string,
|
|
585
701
|
): Promise<boolean> {
|
|
586
702
|
const state = await this.ensure(assistantId);
|
|
587
703
|
const session = state.session!;
|
|
588
|
-
const message = { customType: EVENT_TYPE, content: `[hob-event] ${text}`, display: false, details };
|
|
589
704
|
this.touch(assistantId);
|
|
590
705
|
state.running += 1;
|
|
591
706
|
this.armStall(state);
|
|
592
707
|
this.publishBusy(state);
|
|
593
|
-
refreshSystemPrompt(session);
|
|
594
708
|
try {
|
|
709
|
+
const knowledge = knowledgeQuery === undefined ? "" : await this.automaticKnowledge(state, knowledgeQuery);
|
|
710
|
+
const message = { customType: EVENT_TYPE, content: `${nowBlock(this.runtime, assistantId)}\n\n[hob-event] ${text}${knowledge}`, display: false, details };
|
|
711
|
+
await this.refreshTools(state);
|
|
595
712
|
await this.applyConversationModel(state);
|
|
596
713
|
if (session.isStreaming) {
|
|
597
714
|
await session.sendCustomMessage(message, { deliverAs: "followUp" });
|
|
@@ -673,6 +790,7 @@ export class RouterManager {
|
|
|
673
790
|
};
|
|
674
791
|
const id = manager.appendCustomEntry(USER_TYPE, data);
|
|
675
792
|
const item: Item = { kind: "user", id, at: new Date().toISOString(), ...data };
|
|
793
|
+
for (const path of body.files ?? []) void this.runtime.knowledge.collect({ origin: "chat", path, assistantId, itemId: id }).catch((error) => console.error("知识库收集失败:", error));
|
|
676
794
|
this.bus.emit({ type: "item", assistantId, item });
|
|
677
795
|
const noteId = manager.appendCustomEntry(ITEM_TYPE, { kind: "note", icon: "info", text: note });
|
|
678
796
|
this.bus.emit({
|
|
@@ -684,6 +802,13 @@ export class RouterManager {
|
|
|
684
802
|
}
|
|
685
803
|
|
|
686
804
|
async send(assistantId: string, body: SendMessageBody): Promise<Item> {
|
|
805
|
+
this.runtime.delegations.takeOver(assistantId);
|
|
806
|
+
for (const approval of this.store.approvals()) {
|
|
807
|
+
if (approval.assistantId !== assistantId || approval.source !== "router" || approval.status !== "pending") continue;
|
|
808
|
+
const expired = { ...approval, status: "expired" as const };
|
|
809
|
+
this.store.saveApproval(expired);
|
|
810
|
+
this.bus.emit({ type: "approval", approval: expired });
|
|
811
|
+
}
|
|
687
812
|
const gap = await this.modelGap(assistantId);
|
|
688
813
|
if (gap) return this.sendWithoutModel(assistantId, body, gap);
|
|
689
814
|
const state = await this.ensure(assistantId);
|
|
@@ -702,11 +827,16 @@ export class RouterManager {
|
|
|
702
827
|
...(body.files?.length ? { files: body.files } : {}),
|
|
703
828
|
};
|
|
704
829
|
this.bus.emit({ type: "item", assistantId, item });
|
|
830
|
+
for (const path of body.files ?? []) void this.runtime.knowledge.collect({ origin: "chat", path, assistantId, itemId: id }).catch((error) => console.error("知识库收集失败:", error));
|
|
705
831
|
|
|
706
832
|
const lines = [body.text];
|
|
707
833
|
if (body.quote) lines.unshift(`(引用我之前的话:「${body.quote.text}」)`);
|
|
708
834
|
if (body.files?.length) lines.push(`附件:${body.files.join("、")}`);
|
|
709
|
-
const
|
|
835
|
+
const basePrompt = `${nowBlock(this.runtime, assistantId)}\n\n${lines.filter(Boolean).join("\n")}`;
|
|
836
|
+
const previousDelivery = state.pendingDelivery;
|
|
837
|
+
let releaseDelivery!: () => void;
|
|
838
|
+
const delivery = new Promise<void>((resolve) => { releaseDelivery = resolve; });
|
|
839
|
+
state.pendingDelivery = delivery;
|
|
710
840
|
|
|
711
841
|
this.touch(assistantId);
|
|
712
842
|
state.running += 1;
|
|
@@ -714,18 +844,25 @@ export class RouterManager {
|
|
|
714
844
|
this.publishBusy(state);
|
|
715
845
|
void (async () => {
|
|
716
846
|
try {
|
|
847
|
+
const promptText = basePrompt + await this.automaticKnowledge(state, body.text);
|
|
848
|
+
await previousDelivery;
|
|
849
|
+
(state.pendingUsers ??= []).push({ prompt: promptText, id });
|
|
717
850
|
await this.applyConversationModel(state);
|
|
718
|
-
|
|
851
|
+
await this.refreshTools(state);
|
|
719
852
|
// 对话模型能看图就把图片直接给它(路径照样写在「附件」里,派任务时还能交出去);看不了图就只有路径
|
|
720
853
|
const images = state.session!.model?.input.includes("image") ? imageContents(body.files) : [];
|
|
721
|
-
if (state.session!.isStreaming)
|
|
722
|
-
|
|
723
|
-
|
|
854
|
+
if (state.session!.isStreaming) {
|
|
855
|
+
await state.session!.steer(promptText, images.length ? images : undefined);
|
|
856
|
+
releaseDelivery();
|
|
857
|
+
} else {
|
|
858
|
+
await state.session!.prompt(promptText, { ...(images.length ? { images } : {}), preflightResult: releaseDelivery });
|
|
724
859
|
await this.reportTurnError(state);
|
|
725
860
|
}
|
|
726
861
|
} catch (error) {
|
|
727
862
|
await this.fail(state, error);
|
|
728
863
|
} finally {
|
|
864
|
+
releaseDelivery();
|
|
865
|
+
if (state.pendingDelivery === delivery) state.pendingDelivery = undefined;
|
|
729
866
|
state.running = Math.max(0, state.running - 1);
|
|
730
867
|
if (!state.running) this.setActivity(state, null);
|
|
731
868
|
this.clearStall(state);
|
|
@@ -873,8 +1010,8 @@ export class RouterManager {
|
|
|
873
1010
|
|
|
874
1011
|
/** 直接走事件的 followUp 队列,实际收到这条消息时才开始关联任务。 */
|
|
875
1012
|
/** 把事件当作转交的一轮送给目标助手;这一轮没正常收尾就把转交记为受阻,不会一直挂着。 */
|
|
876
|
-
async relay(targetAssistantId: string, text: string, delegationId: string, details?: EventDetails): Promise<void> {
|
|
877
|
-
const success = await this.pushEvent(targetAssistantId, text, { ...details, delegationId });
|
|
1013
|
+
async relay(targetAssistantId: string, text: string, delegationId: string, details?: EventDetails, knowledgeQuery?: string): Promise<void> {
|
|
1014
|
+
const success = await this.pushEvent(targetAssistantId, text, { ...details, delegationId }, knowledgeQuery);
|
|
878
1015
|
if (!success || !this.store.delegation(delegationId)?.turnFinished) {
|
|
879
1016
|
this.runtime.delegations.turnEnded(delegationId, "目标助手处理失败或转交被中断", true);
|
|
880
1017
|
}
|
package/server/src/routines.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { Cron } from "croner";
|
|
|
2
2
|
import type { Job, Routine } from "../../shared/types.ts";
|
|
3
3
|
import type { HobRuntime } from "./runtime.ts";
|
|
4
4
|
import { newId } from "./ids.ts";
|
|
5
|
+
import { listAssistantSkills, skillInstruction, usableAssistantSkills } from "./assistant-skills.ts";
|
|
5
6
|
|
|
6
7
|
export interface RoutineInput {
|
|
7
8
|
assistantId: string;
|
|
@@ -55,13 +56,18 @@ export class RoutineScheduler {
|
|
|
55
56
|
this.timers.set(routine.id, list);
|
|
56
57
|
}
|
|
57
58
|
|
|
59
|
+
private instruction(routine: Routine): string {
|
|
60
|
+
const skill = routine.skill && usableAssistantSkills(this.runtime.home, routine.assistantId).find((item) => item.name === routine.skill);
|
|
61
|
+
return skill ? skillInstruction(skill.title, routine.instruction) : routine.instruction;
|
|
62
|
+
}
|
|
63
|
+
|
|
58
64
|
private async fire(routineId: string): Promise<void> {
|
|
59
65
|
const routine = this.runtime.store.routine(routineId);
|
|
60
66
|
if (!routine || !routine.active) return;
|
|
61
67
|
try {
|
|
62
68
|
await this.runtime.jobs.start(routine.assistantId, {
|
|
63
69
|
title: routine.name,
|
|
64
|
-
instruction:
|
|
70
|
+
instruction: this.instruction(routine),
|
|
65
71
|
routineId: routine.id,
|
|
66
72
|
});
|
|
67
73
|
} catch (error) {
|
|
@@ -91,11 +97,15 @@ export class RoutineScheduler {
|
|
|
91
97
|
return routine;
|
|
92
98
|
}
|
|
93
99
|
|
|
94
|
-
update(id: string, patch: Partial<Pick<Routine, "name" | "instruction" | "schedules" | "active">>): Routine {
|
|
100
|
+
update(id: string, patch: Partial<Pick<Routine, "name" | "instruction" | "schedules" | "active">> & { skill?: string | null }): Routine {
|
|
95
101
|
const routine = this.runtime.store.routine(id);
|
|
96
102
|
if (!routine) throw new Error(`定时任务不存在:${id}`);
|
|
97
103
|
if (patch.schedules) validate(patch.schedules, this.runtime.settings.settings.general.timezone);
|
|
98
|
-
|
|
104
|
+
if (patch.skill !== undefined && patch.skill !== null && (typeof patch.skill !== "string" || !listAssistantSkills(this.runtime.home, routine.assistantId).some((skill) => skill.name === patch.skill))) throw new Error("绑定的技能无效");
|
|
105
|
+
const { skill, ...rest } = patch;
|
|
106
|
+
const next: Routine = { ...routine, ...rest };
|
|
107
|
+
if (skill === null) delete next.skill;
|
|
108
|
+
else if (skill !== undefined) next.skill = skill;
|
|
99
109
|
this.runtime.store.saveRoutine(next);
|
|
100
110
|
this.apply(next);
|
|
101
111
|
this.runtime.bus.emit({ type: "routine", routine: next });
|
|
@@ -122,7 +132,7 @@ export class RoutineScheduler {
|
|
|
122
132
|
if (!routine) throw new Error(`定时任务不存在:${id}`);
|
|
123
133
|
return await this.runtime.jobs.start(routine.assistantId, {
|
|
124
134
|
title: routine.name,
|
|
125
|
-
instruction:
|
|
135
|
+
instruction: this.instruction(routine),
|
|
126
136
|
routineId: routine.id,
|
|
127
137
|
});
|
|
128
138
|
}
|
package/server/src/runtime.ts
CHANGED
|
@@ -1,4 +1,9 @@
|
|
|
1
1
|
import type { Bus } from "./bus.ts";
|
|
2
|
+
import { DesktopService } from "./desktop.ts";
|
|
3
|
+
import { KnowledgeStore } from "./knowledge.ts";
|
|
4
|
+
import { DesktopControl } from "./desktop-control.ts";
|
|
5
|
+
import { DesktopEvents } from "./desktop-events.ts";
|
|
6
|
+
import { DesktopPublicLock } from "./desktop-public.ts";
|
|
2
7
|
import { DelegationManager } from "./delegations.ts";
|
|
3
8
|
import type { HobConfig } from "./config.ts";
|
|
4
9
|
import { memoryDir } from "./config.ts";
|
|
@@ -30,9 +35,14 @@ export interface HobRuntime {
|
|
|
30
35
|
config: HobConfig;
|
|
31
36
|
store: Store;
|
|
32
37
|
bus: Bus;
|
|
38
|
+
knowledge: KnowledgeStore;
|
|
33
39
|
pi: PiHost;
|
|
34
40
|
/** 界面可改的设置(settings.json),每次现读所以改完立即生效 */
|
|
35
41
|
settings: SettingsStore;
|
|
42
|
+
desktop: DesktopService;
|
|
43
|
+
desktopControl: DesktopControl;
|
|
44
|
+
desktopEvents: DesktopEvents;
|
|
45
|
+
desktopPublic: DesktopPublicLock;
|
|
36
46
|
/** 服务商:设置里存不敏感的,Key 存 secrets.json,变动套进引擎 */
|
|
37
47
|
providers: ProviderService;
|
|
38
48
|
/** 引擎注册表里现在能用的模型(设置页、派任务时看) */
|
|
@@ -86,11 +96,16 @@ export function createRuntime(options: CreateRuntimeOptions): HobRuntime {
|
|
|
86
96
|
config: options.config,
|
|
87
97
|
store,
|
|
88
98
|
bus,
|
|
99
|
+
knowledge: new KnowledgeStore(options.home, bus, store, undefined, () => settings.settings.knowledge?.embedding ?? { source: "off" }),
|
|
89
100
|
pi: options.pi,
|
|
90
101
|
settings,
|
|
102
|
+
desktop: new DesktopService(options.home, settings),
|
|
91
103
|
providers,
|
|
92
104
|
models,
|
|
93
105
|
} as HobRuntime;
|
|
106
|
+
runtime.desktopControl = new DesktopControl(runtime);
|
|
107
|
+
runtime.desktopEvents = new DesktopEvents(runtime);
|
|
108
|
+
runtime.desktopPublic = new DesktopPublicLock();
|
|
94
109
|
runtime.router = new RouterManagerImpl(runtime);
|
|
95
110
|
runtime.jobs = new JobManagerImpl(runtime);
|
|
96
111
|
runtime.delegations = new DelegationManager(runtime);
|
package/server/src/settings.ts
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
|
|
3
3
|
import { dirname, join } from "node:path";
|
|
4
4
|
import type {
|
|
5
|
+
ClaudeCodeSettings,
|
|
5
6
|
MemoryConfig,
|
|
6
7
|
MemoryProvider,
|
|
7
8
|
ModelPick,
|
|
@@ -17,10 +18,11 @@ import type {
|
|
|
17
18
|
} from "../../shared/types.ts";
|
|
18
19
|
import { defaultSearch, normalizeSearch } from "../../shared/search.ts";
|
|
19
20
|
import { checkImageSettings, normalizeImageSettings } from "./image.ts";
|
|
21
|
+
import { checkEmbeddingSettings, normalizeEmbeddingSettings } from "./embedding.ts";
|
|
20
22
|
import { checkThinkingMap, isThinkingLevel } from "./models.ts";
|
|
21
23
|
import { normalizeThinkingMap } from "../../shared/thinking.ts";
|
|
22
24
|
import type { ModelScores } from "../../shared/types.ts";
|
|
23
|
-
import { isMemoryProvider, loreConnection, resolveMemoryProvider } from "./memory.ts";
|
|
25
|
+
import { isMemoryProvider, LORE_PLUGIN_VERSION, loreConnection, resolveMemoryProvider } from "./memory.ts";
|
|
24
26
|
import { checkProvider } from "./providers.ts";
|
|
25
27
|
import { checkTimezone } from "./time.ts";
|
|
26
28
|
|
|
@@ -89,6 +91,8 @@ export function defaultSettings(timezone = systemTimezone(), models: ModelSettin
|
|
|
89
91
|
models,
|
|
90
92
|
image: { source: "off" },
|
|
91
93
|
search: defaultSearch(),
|
|
94
|
+
desktop: { url: "", enabled: false, publicAssistants: [] },
|
|
95
|
+
claude: { enabled: false, plan: "pro", executable: "" },
|
|
92
96
|
work: { maxJobs: 2, jobStallMinutes: 15, routerStallMinutes: 6 },
|
|
93
97
|
push: { email: "" },
|
|
94
98
|
};
|
|
@@ -211,7 +215,18 @@ function normalize(raw: unknown, base: Settings): Settings {
|
|
|
211
215
|
models: normalizeModels(input.models, base.models),
|
|
212
216
|
providers: normalizeProviders(input.providers ?? base.providers),
|
|
213
217
|
image: normalizeImageSettings(input.image, base.image),
|
|
218
|
+
knowledge: { embedding: normalizeEmbeddingSettings(input.knowledge?.embedding, base.knowledge?.embedding ?? { source: "off" }) },
|
|
214
219
|
search: normalizeSearch(input.search ?? base.search),
|
|
220
|
+
desktop: {
|
|
221
|
+
url: typeof input.desktop?.url === "string" ? input.desktop.url : (base.desktop?.url ?? ""),
|
|
222
|
+
enabled: typeof input.desktop?.enabled === "boolean" ? input.desktop.enabled : (base.desktop?.enabled ?? false),
|
|
223
|
+
publicAssistants: Array.isArray(input.desktop?.publicAssistants) ? input.desktop.publicAssistants.filter((id): id is string => typeof id === "string") : (base.desktop?.publicAssistants ?? []),
|
|
224
|
+
},
|
|
225
|
+
claude: {
|
|
226
|
+
enabled: input.claude?.enabled === true,
|
|
227
|
+
plan: input.claude?.plan === "max" ? "max" : "pro",
|
|
228
|
+
executable: typeof input.claude?.executable === "string" ? input.claude.executable.trim() : "",
|
|
229
|
+
},
|
|
215
230
|
skills: { disabled: Array.isArray(input.skills?.disabled) ? input.skills.disabled.filter((v): v is string => typeof v === "string") : (base.skills?.disabled ?? []) },
|
|
216
231
|
work: {
|
|
217
232
|
maxJobs: int(work.maxJobs, base.work.maxJobs, 1, 8),
|
|
@@ -317,6 +332,7 @@ export class SettingsStore {
|
|
|
317
332
|
provider: resolveMemoryProvider(this.memoryProvider, lore.configured),
|
|
318
333
|
providerExplicit: this.memoryProvider !== undefined,
|
|
319
334
|
lore,
|
|
335
|
+
lorePlugin: LORE_PLUGIN_VERSION,
|
|
320
336
|
};
|
|
321
337
|
}
|
|
322
338
|
|
|
@@ -368,7 +384,10 @@ export class SettingsStore {
|
|
|
368
384
|
},
|
|
369
385
|
// image / providers 不在这份补丁里(各走自己的接口),别把它们冲回默认
|
|
370
386
|
image: current.image,
|
|
387
|
+
knowledge: current.knowledge,
|
|
371
388
|
search: current.search,
|
|
389
|
+
desktop: current.desktop,
|
|
390
|
+
claude: current.claude,
|
|
372
391
|
skills: patch.skills ?? current.skills,
|
|
373
392
|
providers: current.providers,
|
|
374
393
|
work: { ...current.work, ...(patch.work ?? {}) },
|
|
@@ -392,6 +411,29 @@ export class SettingsStore {
|
|
|
392
411
|
return this.get();
|
|
393
412
|
}
|
|
394
413
|
|
|
414
|
+
setEmbedding(revision: number, embedding: unknown): SettingsFile | undefined {
|
|
415
|
+
if (revision !== this.file.revision) return undefined;
|
|
416
|
+
const next = checkEmbeddingSettings(embedding);
|
|
417
|
+
this.file = { revision: this.file.revision + 1, settings: { ...this.get().settings, knowledge: { embedding: next } } };
|
|
418
|
+
this.write();
|
|
419
|
+
return this.get();
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
setClaude(revision: number, claude: ClaudeCodeSettings): SettingsFile | undefined {
|
|
423
|
+
if (revision !== this.file.revision) return undefined;
|
|
424
|
+
if (typeof claude.enabled !== "boolean" || (claude.plan !== "pro" && claude.plan !== "max") || typeof claude.executable !== "string") throw new Error("Claude Code 设置无效。");
|
|
425
|
+
this.file = { revision: this.file.revision + 1, settings: { ...this.get().settings, claude: { ...claude, executable: claude.executable.trim() } } };
|
|
426
|
+
this.write();
|
|
427
|
+
return this.get();
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
setDesktop(revision: number, desktop: { url: string; enabled: boolean; publicAssistants?: string[] }): SettingsFile | undefined {
|
|
431
|
+
if (revision !== this.file.revision) return undefined;
|
|
432
|
+
this.file = { revision: this.file.revision + 1, settings: { ...this.get().settings, desktop: { ...this.get().settings.desktop, ...desktop } } };
|
|
433
|
+
this.write();
|
|
434
|
+
return this.get();
|
|
435
|
+
}
|
|
436
|
+
|
|
395
437
|
setSearch(revision: number, search: unknown): SettingsFile | undefined {
|
|
396
438
|
if (revision !== this.file.revision) return undefined;
|
|
397
439
|
if (!Array.isArray(search)) throw new Error("联网搜索配置无效。");
|
package/server/src/store.ts
CHANGED
|
@@ -90,10 +90,11 @@ export class Store {
|
|
|
90
90
|
}
|
|
91
91
|
|
|
92
92
|
/**
|
|
93
|
-
*
|
|
94
|
-
* 归档里的 profile.json
|
|
93
|
+
* 归档助手:整个助手目录移进 archive/assistants/<id>-<时间>/。同盘 rename,不复制也不删数据,
|
|
94
|
+
* 归档里的 profile.json 就是它原来的档案;它的定时存成目录里的 routines.json。
|
|
95
95
|
*/
|
|
96
|
-
archiveAssistant(id: string, name: string): string {
|
|
96
|
+
archiveAssistant(id: string, name: string, routines: Routine[] = []): string {
|
|
97
|
+
if (routines.length) writeJsonAtomic(join(this.assistantDir(id), "routines.json"), routines);
|
|
97
98
|
const target = join(this.home, "archive", "assistants", name);
|
|
98
99
|
mkdirSync(dirname(target), { recursive: true });
|
|
99
100
|
renameSync(this.assistantDir(id), target);
|