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/providers.ts
CHANGED
|
@@ -296,7 +296,7 @@ export class ProviderService {
|
|
|
296
296
|
);
|
|
297
297
|
const builtin = runtime
|
|
298
298
|
.getProviders()
|
|
299
|
-
.filter((provider) => !customIds.has(provider.id))
|
|
299
|
+
.filter((provider) => provider.id !== "claude-bridge" && !customIds.has(provider.id))
|
|
300
300
|
.map((provider) => ({ id: provider.id, name: provider.name }))
|
|
301
301
|
.sort((a, b) => a.name.localeCompare(b.name));
|
|
302
302
|
const models: Record<string, ModelThinkingView[]> = {};
|
|
@@ -341,6 +341,7 @@ export class ProviderService {
|
|
|
341
341
|
runtime.registerProvider(provider.id, config);
|
|
342
342
|
}
|
|
343
343
|
await this.applyBuiltinThinking(runtime);
|
|
344
|
+
await this.options.pi.configureClaude(runtime, this.options.home, this.settings.settings.claude);
|
|
344
345
|
}
|
|
345
346
|
|
|
346
347
|
/**
|
|
@@ -400,7 +401,7 @@ export class ProviderService {
|
|
|
400
401
|
const builtin = new Map(
|
|
401
402
|
runtime
|
|
402
403
|
.getProviders()
|
|
403
|
-
.filter((provider) => !customIds.has(provider.id))
|
|
404
|
+
.filter((provider) => provider.id !== "claude-bridge" && !customIds.has(provider.id))
|
|
404
405
|
.map((provider) => [provider.id, provider] as const),
|
|
405
406
|
);
|
|
406
407
|
const source = localPiDir();
|
package/server/src/push.ts
CHANGED
|
@@ -57,10 +57,14 @@ const webPushSender: PushSender = async (target, payload, vapid) => {
|
|
|
57
57
|
|
|
58
58
|
export interface PushResult {
|
|
59
59
|
sent: number;
|
|
60
|
-
removed: number; // 订阅已失效(404/410
|
|
60
|
+
removed: number; // 订阅已失效(404/410、密钥对不上)被删掉的条数
|
|
61
61
|
skipped?: "visible" | "no-devices"; // 没发的原因
|
|
62
|
+
error?: string; // 发送失败的原因(几台都失败时取第一台)
|
|
62
63
|
}
|
|
63
64
|
|
|
65
|
+
/** 没填邮箱时的 VAPID subject:Apple 不认 mailto:…@localhost(回 403 BadJwtToken) */
|
|
66
|
+
const DEFAULT_SUBJECT = "https://github.com/FFatTiger/hob";
|
|
67
|
+
|
|
64
68
|
export interface PushServiceOptions {
|
|
65
69
|
home: string;
|
|
66
70
|
/** 设置 → 通知里填的邮箱(VAPID subject 用) */
|
|
@@ -90,6 +94,29 @@ function statusOf(error: unknown): number | undefined {
|
|
|
90
94
|
return typeof status === "number" ? status : undefined;
|
|
91
95
|
}
|
|
92
96
|
|
|
97
|
+
/** 推送服务回的原因:Apple 是 {"reason":"…"},其他服务给一段文字 */
|
|
98
|
+
function reasonOf(error: unknown): string {
|
|
99
|
+
const body = (error as { body?: unknown })?.body;
|
|
100
|
+
if (typeof body !== "string" || !body.trim()) return "";
|
|
101
|
+
try {
|
|
102
|
+
const reason = (JSON.parse(body) as { reason?: unknown }).reason;
|
|
103
|
+
if (typeof reason === "string") return reason;
|
|
104
|
+
} catch {
|
|
105
|
+
// 不是 JSON 就用原文
|
|
106
|
+
}
|
|
107
|
+
return body.trim().slice(0, 120);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** 订阅已经用不了:没了(404/410),或是用旧的 VAPID 密钥订的(HOB_HOME 重置过) */
|
|
111
|
+
function isGone(status: number | undefined, reason: string): boolean {
|
|
112
|
+
return status === 404 || status === 410 || (status === 400 && reason === "VapidPkHashMismatch");
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function describeFailure(error: unknown, status: number | undefined, reason: string): string {
|
|
116
|
+
if (status) return `推送服务拒绝(${status}${reason ? ` ${reason}` : ""})`;
|
|
117
|
+
return `连不上推送服务(${error instanceof Error ? error.message : String(error)})`;
|
|
118
|
+
}
|
|
119
|
+
|
|
93
120
|
/** 推送服务(可见性只在内存里:进程重启后没有前台客户端)。 */
|
|
94
121
|
export class PushService {
|
|
95
122
|
private data?: PushFile;
|
|
@@ -136,7 +163,7 @@ export class PushService {
|
|
|
136
163
|
|
|
137
164
|
private subject(): string {
|
|
138
165
|
const email = this.options.email().trim();
|
|
139
|
-
return `mailto:${email
|
|
166
|
+
return email ? `mailto:${email}` : DEFAULT_SUBJECT;
|
|
140
167
|
}
|
|
141
168
|
|
|
142
169
|
view(): PushView {
|
|
@@ -210,6 +237,7 @@ export class PushService {
|
|
|
210
237
|
const body = JSON.stringify(payload);
|
|
211
238
|
const vapid = { ...file.vapid, subject: this.subject() };
|
|
212
239
|
const gone: string[] = [];
|
|
240
|
+
const failures: string[] = [];
|
|
213
241
|
let sent = 0;
|
|
214
242
|
await Promise.all(
|
|
215
243
|
devices.map(async (device) => {
|
|
@@ -221,9 +249,15 @@ export class PushService {
|
|
|
221
249
|
);
|
|
222
250
|
sent += 1;
|
|
223
251
|
} catch (error) {
|
|
224
|
-
//
|
|
252
|
+
// 订阅没了(卸载了 PWA、清过数据)或密钥对不上:删掉,别一直发空;其他失败如实报出去
|
|
225
253
|
const status = statusOf(error);
|
|
226
|
-
|
|
254
|
+
const reason = reasonOf(error);
|
|
255
|
+
if (isGone(status, reason)) gone.push(device.endpoint);
|
|
256
|
+
else {
|
|
257
|
+
const failure = describeFailure(error, status, reason);
|
|
258
|
+
console.error(`推送到「${device.label}」失败:${failure}`);
|
|
259
|
+
failures.push(failure);
|
|
260
|
+
}
|
|
227
261
|
}
|
|
228
262
|
}),
|
|
229
263
|
);
|
|
@@ -231,7 +265,7 @@ export class PushService {
|
|
|
231
265
|
file.devices = file.devices.filter((device) => !gone.includes(device.endpoint));
|
|
232
266
|
this.write();
|
|
233
267
|
}
|
|
234
|
-
return { sent, removed: gone.length };
|
|
268
|
+
return { sent, removed: gone.length, ...(failures.length ? { error: failures[0] } : {}) };
|
|
235
269
|
}
|
|
236
270
|
|
|
237
271
|
/** 该推的时候推:有可见的客户端、或一台设备都没订阅就不发。 */
|
|
@@ -6,7 +6,10 @@ export function activityLabel(name: string, args: Record<string, unknown> = {},
|
|
|
6
6
|
const withValue = (label: string, text: string) => text ? `${label}:${text}` : label;
|
|
7
7
|
let label: string;
|
|
8
8
|
switch (name) {
|
|
9
|
+
case "read": label = withValue("正在读", basename(value("path"))); break;
|
|
9
10
|
case "start_job": label = withValue("派任务", value("title")); break;
|
|
11
|
+
case "check_job": label = "看任务"; break;
|
|
12
|
+
case "edit_skill": label = "改技能"; break;
|
|
10
13
|
case "reply_job": label = "补充任务要求"; break;
|
|
11
14
|
case "stop_job": label = "停止任务"; break;
|
|
12
15
|
case "ask_user": label = "准备问你"; break;
|
|
@@ -18,11 +21,14 @@ export function activityLabel(name: string, args: Record<string, unknown> = {},
|
|
|
18
21
|
case "message_assistant": { const id = value("assistantId"); label = id ? `交给「${assistantName?.(id) ?? id}」` : "交给助手"; break; }
|
|
19
22
|
case "hand_back_to_hob": label = "交回 Hob"; break;
|
|
20
23
|
case "show_page": label = withValue("写页面", value("title")); break;
|
|
24
|
+
case "find_knowledge": label = withValue("正在找资料", value("query")); break;
|
|
25
|
+
case "search_knowledge": label = withValue("正在查知识库", value("query")); break;
|
|
21
26
|
case "web_search": label = withValue("搜索", value("query")); break;
|
|
22
27
|
case "web_fetch": label = "读取网页"; break;
|
|
23
28
|
case "generate_image": label = "画图"; break;
|
|
24
29
|
case "overview": label = "查看工作概况"; break;
|
|
25
30
|
case "read_skill": label = withValue("看技能", value("name")); break;
|
|
31
|
+
case "save_job_as_skill": label = "沉淀技能"; break;
|
|
26
32
|
default:
|
|
27
33
|
label = /^(memory_write|memory_forget|memory_restore|lore_create_node|lore_update_node|lore_delete_node|lore_move_node)$/.test(name) ? "记下来"
|
|
28
34
|
: /^(memory_|lore_|scratchpad)/.test(name) ? "翻记忆" : "调用工具";
|
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import { Type } from "@earendil-works/pi-ai";
|
|
2
|
-
import type
|
|
2
|
+
import { createReadToolDefinition, type ToolDefinition } from "@earendil-works/pi-coding-agent";
|
|
3
3
|
import { existsSync, readFileSync, statSync } from "node:fs";
|
|
4
|
-
import { basename } from "node:path";
|
|
4
|
+
import { basename, extname, join, resolve } from "node:path";
|
|
5
|
+
import { readFile } from "node:fs/promises";
|
|
6
|
+
import { IMAGE_TYPES } from "../../shared/media.ts";
|
|
7
|
+
import { lookupFile } from "./files.ts";
|
|
5
8
|
import {
|
|
6
9
|
COLORS,
|
|
7
10
|
COLOR_IDS,
|
|
@@ -17,6 +20,8 @@ import type { ColorId } from "../../shared/sprites.ts";
|
|
|
17
20
|
import type { Approval, ApprovalOption, CustomProp, Draft, Routine } from "../../shared/types.ts";
|
|
18
21
|
import { cronToChinese } from "./cron-text.ts";
|
|
19
22
|
import { newId } from "./ids.ts";
|
|
23
|
+
import { findKnowledgeTool, searchKnowledgeTool } from "./knowledge-tools.ts";
|
|
24
|
+
import { generateImage, imageConfigured } from "./image.ts";
|
|
20
25
|
import { defaultWorkRef, jobModel, noteOf, thinkingLevelsOf, usableRefs } from "./models.ts";
|
|
21
26
|
import { overviewTool } from "./overview.ts";
|
|
22
27
|
import { pageTool } from "./pages.ts";
|
|
@@ -24,6 +29,8 @@ import { pushApproval } from "./push.ts";
|
|
|
24
29
|
import { searchTools } from "./search.ts";
|
|
25
30
|
import type { HobRuntime } from "./runtime.ts";
|
|
26
31
|
import { clampDuty } from "./text.ts";
|
|
32
|
+
import { listAssistantSkills, readAssistantSkillBody, saveAssistantSkill, skillInstruction, usableAssistantSkills } from "./assistant-skills.ts";
|
|
33
|
+
import { jobView } from "./job-view.ts";
|
|
27
34
|
|
|
28
35
|
type Tool = ToolDefinition<any, any>;
|
|
29
36
|
|
|
@@ -82,23 +89,112 @@ export function buildRouterTools(runtime: HobRuntime, assistantId: string): Tool
|
|
|
82
89
|
});
|
|
83
90
|
const modelText = modelList.length ? modelList.join("\n") : "(现在没有可用的模型)";
|
|
84
91
|
|
|
92
|
+
const checkFile = (path: string) => {
|
|
93
|
+
const found = lookupFile(runtime.home, path, store.jobs().flatMap((job) => job.cwd ? [job.cwd] : []));
|
|
94
|
+
if (!found.ok) throw new Error(found.error);
|
|
95
|
+
return found.info.path;
|
|
96
|
+
};
|
|
97
|
+
const read = createReadToolDefinition(runtime.home, { operations: {
|
|
98
|
+
access: async (path) => { checkFile(path); },
|
|
99
|
+
readFile: async (path) => readFile(checkFile(path)),
|
|
100
|
+
detectImageMimeType: async (path) => IMAGE_TYPES[extname(checkFile(path)).toLowerCase()],
|
|
101
|
+
} }) as unknown as Tool;
|
|
85
102
|
const tools: Tool[] = [
|
|
103
|
+
{
|
|
104
|
+
name: "check_job",
|
|
105
|
+
label: "看任务",
|
|
106
|
+
description: "看一个任务现在的状态和过程:做到哪一步、跑过哪些命令、说过什么、用了哪个技能、在等什么。",
|
|
107
|
+
parameters: Type.Object({ jobId: Type.String({ description: "任务 id" }) }),
|
|
108
|
+
execute: async (_id, params: { jobId: string }) => {
|
|
109
|
+
const job = store.job(params.jobId);
|
|
110
|
+
if (!job || (!assistant().primary && job.assistantId !== assistantId)) return fail(`没有这个任务:${params.jobId}`);
|
|
111
|
+
try {
|
|
112
|
+
return ok(jobView({ ...job, cwd: job.cwd ?? store.workDir(job.id) }, jobs.transcript(job.id), store.approvals(), listAssistantSkills(runtime.home, job.assistantId), runtime.settings.settings.general.timezone));
|
|
113
|
+
} catch (error) { return fail(error instanceof Error ? error.message : String(error)); }
|
|
114
|
+
},
|
|
115
|
+
},
|
|
116
|
+
{
|
|
117
|
+
name: "edit_skill",
|
|
118
|
+
label: "改技能",
|
|
119
|
+
description: "改这个助手某个技能的正文(SKILL.md):把一段原文换成新的文字,旧版会保留。要改脚本的,交给任务去改。",
|
|
120
|
+
parameters: Type.Object({
|
|
121
|
+
name: Type.String({ description: "技能名" }),
|
|
122
|
+
edits: Type.Array(Type.Object({ old: Type.String(), new: Type.String() }), { description: "一处或几处修改:old 是正文里的原文,要一字不差、只出现一次;new 是换成的文字" }),
|
|
123
|
+
}),
|
|
124
|
+
execute: async (_id, params: { name: string; edits: { old: string; new: string }[] }) => {
|
|
125
|
+
try {
|
|
126
|
+
const skill = listAssistantSkills(runtime.home, assistantId).find((item) => item.name === params.name);
|
|
127
|
+
if (!skill) return fail(`没有这个技能:${params.name}`);
|
|
128
|
+
let body = readAssistantSkillBody(skill.path);
|
|
129
|
+
for (const edit of params.edits) {
|
|
130
|
+
if (!edit.old) return fail("原文不能为空");
|
|
131
|
+
const count = body.split(edit.old).length - 1;
|
|
132
|
+
if (!count) return fail(`正文里找不到这段原文:${[...edit.old].slice(0, 30).join("")}`);
|
|
133
|
+
if (count > 1) return fail(`这段原文出现了 ${count} 次,多带几个字`);
|
|
134
|
+
body = body.replace(edit.old, () => edit.new);
|
|
135
|
+
}
|
|
136
|
+
const saved = saveAssistantSkill(runtime.home, assistantId, { name: skill.name, title: skill.title, description: skill.description, content: body });
|
|
137
|
+
await router.addItem(assistantId, { kind: "note", icon: "skill", text: `已更新技能「${saved.title}」· 第 ${saved.version} 版`, path: saved.path });
|
|
138
|
+
bus.emit({ type: "skills", assistantId });
|
|
139
|
+
return ok(`已更新技能「${saved.title}」,第 ${saved.version} 版。`);
|
|
140
|
+
} catch (error) { return fail(error instanceof Error ? error.message : String(error)); }
|
|
141
|
+
},
|
|
142
|
+
},
|
|
143
|
+
{
|
|
144
|
+
name: "save_job_as_skill",
|
|
145
|
+
label: "沉淀技能",
|
|
146
|
+
description: "把一个已完成的任务的做法沉淀成这个助手的技能,在任务自己的会话里做,做完对话里会有提示。",
|
|
147
|
+
parameters: Type.Object({ jobId: Type.String() }),
|
|
148
|
+
execute: async (_id, params: { jobId: string }) => {
|
|
149
|
+
const job = store.job(params.jobId);
|
|
150
|
+
if (!job || job.assistantId !== assistantId) return fail("任务不存在");
|
|
151
|
+
try { jobs.startSediment(job.id, "manual"); return ok("已开始沉淀技能。"); }
|
|
152
|
+
catch (error) { return fail(error instanceof Error ? error.message : String(error)); }
|
|
153
|
+
},
|
|
154
|
+
},
|
|
155
|
+
{
|
|
156
|
+
...read,
|
|
157
|
+
description: "读文件或看图片:用户发来的附件、任务的目录和产出、页面。长文件用 offset 分段读。",
|
|
158
|
+
execute: async (id, args: { path: string; offset?: number; limit?: number }, signal, update, ctx) => {
|
|
159
|
+
try {
|
|
160
|
+
const path = checkFile(resolve(runtime.home, args.path));
|
|
161
|
+
if (IMAGE_TYPES[extname(path).toLowerCase()]) {
|
|
162
|
+
if (ctx?.model && !ctx.model.input.includes("image")) return fail("当前对话模型看不了图片");
|
|
163
|
+
return await read.execute(id, { ...args, path }, signal, update, ctx);
|
|
164
|
+
}
|
|
165
|
+
const lines = (await readFile(checkFile(path), "utf8")).split("\n");
|
|
166
|
+
const start = Math.max(0, Math.floor(args.offset ?? 1) - 1);
|
|
167
|
+
if (start >= lines.length) return fail(`Offset ${args.offset} is beyond end of file (${lines.length} lines total)`);
|
|
168
|
+
const end = Math.min(lines.length, start + Math.max(0, Math.floor(args.limit ?? lines.length)));
|
|
169
|
+
let count = start;
|
|
170
|
+
let size = 0;
|
|
171
|
+
while (count < end && size + lines[count].length + (count > start ? 1 : 0) <= 3900) {
|
|
172
|
+
size += lines[count].length + (count > start ? 1 : 0);
|
|
173
|
+
count++;
|
|
174
|
+
}
|
|
175
|
+
if (count === start && start < end) {
|
|
176
|
+
const next = start + 2 <= lines.length ? `;接着读用 offset=${start + 2}` : "";
|
|
177
|
+
const text = `${lines[start].slice(0, 3900)}\n(第 ${start + 1} 行共 ${lines[start].length} 字,这里只有前 3,900 字${next})`;
|
|
178
|
+
return { ...ok(text), structuredContent: text };
|
|
179
|
+
}
|
|
180
|
+
const content = lines.slice(start, count).join("\n");
|
|
181
|
+
const text = count < lines.length ? `${content}\n(共 ${lines.length} 行,这里到第 ${count} 行;接着读用 offset=${count + 1})` : content;
|
|
182
|
+
return { ...ok(text), structuredContent: text };
|
|
183
|
+
} catch (error) { return fail(error instanceof Error ? error.message : String(error)); }
|
|
184
|
+
},
|
|
185
|
+
},
|
|
86
186
|
{
|
|
87
187
|
name: "start_job",
|
|
88
188
|
label: "创建任务",
|
|
89
|
-
description:
|
|
90
|
-
"把需要动手的任务交给后台任务执行:读写文件、运行命令、修改内容都只能通过这个工具完成。立即返回任务 id,完成后会有一条事件回到对话。调用后这一轮就结束了:工具返回后不要再输出任何文字。" +
|
|
91
|
-
(modelList.length > 1
|
|
92
|
-
? `可按任务难度和类型指定模型:${modelText};不确定就不填,使用默认值。`
|
|
93
|
-
: `模型用 ${modelText},不用指定。`),
|
|
189
|
+
description: "把要动手的事交给后台任务:跑命令、改文件、操作设备、多步调研。用户这次的原话、引用、附件和最近几条对话会自动带给任务;任务随时可以停。",
|
|
94
190
|
promptSnippet: "把需要执行的任务交给后台任务。",
|
|
95
191
|
parameters: Type.Object({
|
|
96
192
|
title: Type.String({ description: "任务标题,短,如「查磁盘占用」" }),
|
|
97
|
-
|
|
193
|
+
note: Type.Optional(Type.String({ description: "任务从原话里看不出、又需要知道的背景,最多 200 字。" })),
|
|
98
194
|
cwd: Type.Optional(
|
|
99
195
|
Type.String({
|
|
100
196
|
description:
|
|
101
|
-
"
|
|
197
|
+
"可选,改某个项目时填项目目录;不填就在任务自己的目录里做。",
|
|
102
198
|
}),
|
|
103
199
|
),
|
|
104
200
|
files: Type.Optional(
|
|
@@ -106,25 +202,37 @@ export function buildRouterTools(runtime: HobRuntime, assistantId: string): Tool
|
|
|
106
202
|
description: "要交给任务的文件路径(用户发来的附件);图片会直接给模型看",
|
|
107
203
|
}),
|
|
108
204
|
),
|
|
109
|
-
|
|
205
|
+
skill: Type.Optional(Type.String({ description: "可选,按这个助手的哪个技能做,填技能名。" })),
|
|
206
|
+
expect_minutes: Type.Optional(Type.Number({ description: "可选,预计多少分钟能交结果。超过了会通知你;不填按 10 分钟。" })),
|
|
207
|
+
model: Type.Optional(Type.String({ description: `可选,不填用默认。聪明、成本、速度 0–100,越高越聪明、越贵、越快:\n${modelText}` })),
|
|
110
208
|
thinking: Type.Optional(
|
|
111
209
|
Type.String({ description: modelSettings.thinkingChoice === "fixed" ? "当前设置为固定思考档,此参数会被忽略,不要填。" : "可选:思考档以可分派模型列表里每个模型标的为准。不填用模型的默认思考档。" }),
|
|
112
210
|
),
|
|
113
211
|
}),
|
|
114
212
|
execute: async (_id, params: any) => {
|
|
115
213
|
try {
|
|
214
|
+
if (params.expect_minutes != null && (!Number.isInteger(params.expect_minutes) || params.expect_minutes < 1 || params.expect_minutes > 10080)) return fail("expect_minutes 要在 1 到 10080 之间");
|
|
215
|
+
const note = params.note == null ? "" : String(params.note);
|
|
216
|
+
if ([...note].length > 200) return fail("备注最多 200 字;用户原话和最近的对话会自动带上,备注只写从原话里看不出的背景。");
|
|
217
|
+
// 有的模型会把没用的可选参数填成空串:空串当没填
|
|
218
|
+
const wanted = params.skill ? String(params.skill) : "";
|
|
219
|
+
const skills = usableAssistantSkills(runtime.home, assistantId);
|
|
220
|
+
const skill = wanted ? skills.find((item) => item.name === wanted) : undefined;
|
|
221
|
+
if (wanted && !skill) return fail(`没有这个技能:${wanted}。能用的有:${skills.map((item) => item.name).join("、") || "无"}`);
|
|
222
|
+
const original = router.originalWords(assistantId, note, true, Array.isArray(params.files) ? params.files.map(String) : []);
|
|
116
223
|
const job = await jobs.start(assistantId, {
|
|
117
224
|
title: String(params.title),
|
|
118
|
-
|
|
225
|
+
...(params.expect_minutes != null ? { expectMinutes: params.expect_minutes } : {}),
|
|
226
|
+
instruction: skill ? skillInstruction(skill.title, original.text) : original.text,
|
|
119
227
|
...(params.cwd ? { cwd: String(params.cwd) } : {}),
|
|
120
|
-
...(
|
|
228
|
+
...(original.files.length ? { files: original.files } : {}),
|
|
121
229
|
...(params.model ? { model: String(params.model) } : {}),
|
|
122
230
|
...(params.thinking ? { thinking: String(params.thinking) } : {}),
|
|
123
231
|
...(router.currentDelegation(assistantId) ? { delegationId: router.currentDelegation(assistantId) } : {}),
|
|
124
232
|
});
|
|
125
233
|
if (job.status === "failed") return fail(`任务启动失败:${job.statusReason ?? "原因不明"}`);
|
|
126
234
|
return endTurn(
|
|
127
|
-
`任务「${job.title}」(${job.id}
|
|
235
|
+
`任务「${job.title}」(${job.id})已开始,预计 ${job.expect?.minutes ?? 10} 分钟${params.expect_minutes == null ? "(默认)" : ""}。`,
|
|
128
236
|
);
|
|
129
237
|
} catch (error) {
|
|
130
238
|
return fail(error instanceof Error ? error.message : String(error));
|
|
@@ -134,15 +242,17 @@ export function buildRouterTools(runtime: HobRuntime, assistantId: string): Tool
|
|
|
134
242
|
{
|
|
135
243
|
name: "reply_job",
|
|
136
244
|
label: "补充任务说明",
|
|
137
|
-
description: "
|
|
245
|
+
description: "把补充、纠正或追问交给已有的任务,它带着原来的上下文和目录接着做;已经结束的任务也可以。",
|
|
138
246
|
parameters: Type.Object({
|
|
139
247
|
jobId: Type.String({ description: "任务 id" }),
|
|
140
248
|
text: Type.String({ description: "要补充的话" }),
|
|
141
249
|
}),
|
|
142
250
|
execute: async (_id, params: any) => {
|
|
143
251
|
try {
|
|
144
|
-
|
|
145
|
-
|
|
252
|
+
const jobId = String(params.jobId);
|
|
253
|
+
await jobs.reply(jobId, String(params.text), "router");
|
|
254
|
+
const job = store.job(jobId)!;
|
|
255
|
+
return ok(`已交给任务「${job.title}」(${jobId}),它会接着做。`);
|
|
146
256
|
} catch (error) {
|
|
147
257
|
return fail(error instanceof Error ? error.message : String(error));
|
|
148
258
|
}
|
|
@@ -155,7 +265,7 @@ export function buildRouterTools(runtime: HobRuntime, assistantId: string): Tool
|
|
|
155
265
|
parameters: Type.Object({ jobId: Type.String({ description: "任务 id" }) }),
|
|
156
266
|
execute: async (_id, params: any) => {
|
|
157
267
|
try {
|
|
158
|
-
await jobs.stop(String(params.jobId));
|
|
268
|
+
await jobs.stop(String(params.jobId), `${assistant().name}叫停了`);
|
|
159
269
|
return ok("已停止。");
|
|
160
270
|
} catch (error) {
|
|
161
271
|
return fail(error instanceof Error ? error.message : String(error));
|
|
@@ -165,28 +275,28 @@ export function buildRouterTools(runtime: HobRuntime, assistantId: string): Tool
|
|
|
165
275
|
{
|
|
166
276
|
name: "ask_user",
|
|
167
277
|
label: "询问用户",
|
|
168
|
-
description:
|
|
169
|
-
"在对话里出选择题卡片让用户确认(最多 3 个选项)。用于规则要求执行前确认、或者操作不可逆的时候。选完答案会作为事件回到对话。调用后这一轮就结束了:工具返回后不要再输出任何文字。",
|
|
278
|
+
description: "在对话里出选择题卡片(2 到 3 个选项),等用户选,答案会回到对话。用于用户定的规则要求先确认的事,或发送、删除、付费这类撤不回的事。",
|
|
170
279
|
parameters: Type.Object({
|
|
171
280
|
title: Type.String({ description: "问题,一句话" }),
|
|
172
281
|
detail: Type.Optional(Type.String({ description: "补充说明,写清后果" })),
|
|
173
282
|
options: Type.Array(
|
|
174
283
|
Type.Object({
|
|
175
284
|
label: Type.String({ description: "选项文字,如「暂不发布」" }),
|
|
176
|
-
always: Type.Optional(Type.Boolean({ description: "选中后将这句话记为规则" })),
|
|
177
285
|
}),
|
|
178
286
|
{ description: "2 到 3 个选项", minItems: 2, maxItems: 3 },
|
|
179
287
|
),
|
|
180
|
-
|
|
288
|
+
remember: Type.Optional(Type.String({ description: "同类事以后可以不再问时,填这类事的简称(如「清理临时目录」);卡片会多一个「以后直接做」的选项。" })),
|
|
181
289
|
}),
|
|
182
290
|
execute: async (_id, params: any) => {
|
|
183
|
-
const labels: { label: string
|
|
291
|
+
const labels: { label: string }[] = Array.isArray(params.options) ? params.options : [];
|
|
184
292
|
if (labels.length < 2 || labels.length > 3) return fail("选项要 2 到 3 个。");
|
|
293
|
+
const remember = typeof params.remember === "string" ? params.remember.trim() : "";
|
|
294
|
+
if (remember && labels.length > 2) return fail("带 remember 时最多给 2 个选项。");
|
|
185
295
|
const options: ApprovalOption[] = labels.map((option, index) => ({
|
|
186
296
|
key: (["A", "B", "C"] as const)[index],
|
|
187
297
|
label: String(option.label),
|
|
188
|
-
...(option.always ? { always: true } : {}),
|
|
189
298
|
}));
|
|
299
|
+
if (remember) options.push({ key: "C", label: `以后「${remember}」直接做`, always: true });
|
|
190
300
|
const approval: Approval = {
|
|
191
301
|
id: newId("q-"),
|
|
192
302
|
assistantId,
|
|
@@ -195,7 +305,7 @@ export function buildRouterTools(runtime: HobRuntime, assistantId: string): Tool
|
|
|
195
305
|
options,
|
|
196
306
|
status: "pending",
|
|
197
307
|
source: "router",
|
|
198
|
-
...(
|
|
308
|
+
...(remember ? { ruleText: remember } : {}),
|
|
199
309
|
createdAt: new Date().toISOString(),
|
|
200
310
|
};
|
|
201
311
|
store.saveApproval(approval);
|
|
@@ -204,13 +314,13 @@ export function buildRouterTools(runtime: HobRuntime, assistantId: string): Tool
|
|
|
204
314
|
await router.addItem(assistantId, { kind: "question", approvalId: approval.id });
|
|
205
315
|
bus.emit({ type: "approval", approval });
|
|
206
316
|
pushApproval({ push: runtime.push, store }, approval);
|
|
207
|
-
return endTurn("
|
|
317
|
+
return endTurn("问题卡已显示给用户。");
|
|
208
318
|
},
|
|
209
319
|
},
|
|
210
320
|
{
|
|
211
321
|
name: "set_rule",
|
|
212
322
|
label: "记录规则",
|
|
213
|
-
description: "
|
|
323
|
+
description: "记下用户对某类事的确认方式:ask 先问,allow 直接做。",
|
|
214
324
|
parameters: Type.Object({
|
|
215
325
|
text: Type.String({ description: "规则内容,如「发布公众号」" }),
|
|
216
326
|
mode: Type.Unsafe<"ask" | "allow">(Type.String({ description: "ask 或 allow" })),
|
|
@@ -403,6 +513,38 @@ export function buildRouterTools(runtime: HobRuntime, assistantId: string): Tool
|
|
|
403
513
|
},
|
|
404
514
|
];
|
|
405
515
|
|
|
516
|
+
if (assistant().abilities?.image !== false && imageConfigured(runtime.settings.settings.image)) {
|
|
517
|
+
tools.push({
|
|
518
|
+
name: "generate_image",
|
|
519
|
+
label: "生成图片",
|
|
520
|
+
description: "直接在对话里按文字生成或修改图片,生成时显示卡片,完成后图片自动显示,不需要派任务或用 send_file 发送。返回的路径可作为后续改图的参考图。",
|
|
521
|
+
parameters: Type.Object({
|
|
522
|
+
prompt: Type.String({ description: "要画什么,越具体越好" }),
|
|
523
|
+
size: Type.Optional(Type.String({ description: "尺寸,如 1024x1024;不填用设置里的默认" })),
|
|
524
|
+
refs: Type.Optional(Type.Array(Type.String(), { description: "参考图的绝对路径(改图、按图生成的时候给)" })),
|
|
525
|
+
}),
|
|
526
|
+
execute: async (_id, params: any) => {
|
|
527
|
+
const config = runtime.settings.settings.image;
|
|
528
|
+
const prompt = String(params.prompt ?? "");
|
|
529
|
+
const size = params.size ? String(params.size) : config.source === "openai" ? config.size : undefined;
|
|
530
|
+
const item = await router.addItem(assistantId, { kind: "image", status: "pending", prompt, ...(size ? { size } : {}), startedAt: new Date().toISOString() });
|
|
531
|
+
try {
|
|
532
|
+
const path = await generateImage({ home: runtime.home, dir: join(runtime.home, "images"), config }, {
|
|
533
|
+
prompt,
|
|
534
|
+
...(params.size ? { size: String(params.size) } : {}),
|
|
535
|
+
...(Array.isArray(params.refs) && params.refs.length ? { refs: params.refs.map(String) } : {}),
|
|
536
|
+
});
|
|
537
|
+
await router.updateImage(assistantId, item.id, { status: "done", path });
|
|
538
|
+
return ok(`图片已经显示在对话里,不要再用 send_file 发一遍。图片路径:${path}`);
|
|
539
|
+
} catch (error) {
|
|
540
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
541
|
+
await router.updateImage(assistantId, item.id, { status: "failed", error: message });
|
|
542
|
+
return fail(message);
|
|
543
|
+
}
|
|
544
|
+
},
|
|
545
|
+
});
|
|
546
|
+
}
|
|
547
|
+
|
|
406
548
|
if (runtime.settings.settings.general.showPage !== false) tools.push(pageTool(runtime, assistantId, true));
|
|
407
549
|
|
|
408
550
|
if (assistant().primary) {
|
|
@@ -411,14 +553,13 @@ export function buildRouterTools(runtime: HobRuntime, assistantId: string): Tool
|
|
|
411
553
|
{
|
|
412
554
|
name: "propose_assistant",
|
|
413
555
|
label: "提议创建专属助手",
|
|
414
|
-
description:
|
|
415
|
-
"提议创建一个专属助手。会生成一张草稿卡,用户点「创建」后才会真正创建。用于同类任务反复出现时。调用后这一轮就结束了:工具返回后不要再输出任何文字。",
|
|
556
|
+
description: "同类事情反复出现、又没有合适的助手时,提议新建一个专属助手。生成草稿卡,用户点「创建」才会建。",
|
|
416
557
|
parameters: Type.Object({
|
|
417
558
|
name: Type.String({ description: "助手名字:职责加一个亲切的昵称,四五个字,如「运维小子」「写稿搭子」「代理管家」;不要只写「代理」「运维」这种光秃秃的词" }),
|
|
418
559
|
duty: Type.String({ description: "左栏头像下面那行小字,最多 8 个字,如「线上和机器」「模型和 agent」" }),
|
|
419
560
|
dutyLong: Type.String({ description: "草稿卡里的职责说明,两三句话,写完整一些" }),
|
|
420
561
|
sprite: Type.String({
|
|
421
|
-
description:
|
|
562
|
+
description: `内置道具:${PROP_IDS.filter((id) => id !== "hob").map((id) => `${id} ${PROPS[id].label}`).join(",")};或 custom(同时填 prop_art,自己画一件)`,
|
|
422
563
|
}),
|
|
423
564
|
prop_art: Type.Optional(
|
|
424
565
|
Type.Object(
|
|
@@ -442,7 +583,7 @@ export function buildRouterTools(runtime: HobRuntime, assistantId: string): Tool
|
|
|
442
583
|
routines: Type.Optional(
|
|
443
584
|
Type.Array(
|
|
444
585
|
Type.Object({ name: Type.String(), instruction: Type.String(), schedule: Type.String() }),
|
|
445
|
-
{ description: "
|
|
586
|
+
{ description: "可选。根据上下文判断这个助手需不需要定期做的事,不是每个助手都要有。拿不准就先问用户,问清楚再加。" },
|
|
446
587
|
),
|
|
447
588
|
),
|
|
448
589
|
askFirst: Type.Optional(Type.Array(Type.String(), { description: "执行前需确认的事项" })),
|
|
@@ -455,6 +596,7 @@ export function buildRouterTools(runtime: HobRuntime, assistantId: string): Tool
|
|
|
455
596
|
const checked = params.prop_art !== undefined ? validateCustomProp(params.prop_art) : undefined;
|
|
456
597
|
if (checked && !checked.ok) return fail(`prop_art 画得不对:${checked.error};照着原因改好,再调用一次。`);
|
|
457
598
|
const given = parseSprite(String(params.sprite ?? ""));
|
|
599
|
+
if (given?.prop === "hob") return fail("hob 火苗是主助手专用的。");
|
|
458
600
|
if (checked?.ok && given && given.prop !== "custom") {
|
|
459
601
|
return fail("sprite 填了内置道具就不要传 prop_art;要自画一件就把 sprite 填 custom。");
|
|
460
602
|
}
|
|
@@ -487,17 +629,17 @@ export function buildRouterTools(runtime: HobRuntime, assistantId: string): Tool
|
|
|
487
629
|
await router.addItem(assistantId, { kind: "draft", draftId: draft.id });
|
|
488
630
|
bus.emit({ type: "draft", draft });
|
|
489
631
|
return endTurn(
|
|
490
|
-
"
|
|
632
|
+
"草稿卡已显示给用户。",
|
|
491
633
|
);
|
|
492
634
|
},
|
|
493
635
|
},
|
|
494
636
|
{
|
|
495
637
|
name: "message_assistant",
|
|
496
638
|
label: "转交其他助手",
|
|
497
|
-
description: "
|
|
639
|
+
description: "把一件事交给更合适的助手(见「其他助手」),由它负责到底;用户的原话和附件会自动转过去,对方的回复和结果会回到这里。",
|
|
498
640
|
parameters: Type.Object({
|
|
499
641
|
assistantId: Type.String({ description: "目标助手 id" }),
|
|
500
|
-
|
|
642
|
+
note: Type.Optional(Type.String({ description: "可选,对方需要知道的背景,最多 200 字。" })),
|
|
501
643
|
}),
|
|
502
644
|
execute: async (_id, params: any) => {
|
|
503
645
|
const targetId = String(params.assistantId);
|
|
@@ -505,8 +647,11 @@ export function buildRouterTools(runtime: HobRuntime, assistantId: string): Tool
|
|
|
505
647
|
if (!target) return fail(`没有这个助手:${targetId}`);
|
|
506
648
|
if (targetId === assistantId) return fail("不能转交给自己。");
|
|
507
649
|
try {
|
|
508
|
-
|
|
509
|
-
|
|
650
|
+
const note = params.note == null ? "" : String(params.note);
|
|
651
|
+
if ([...note].length > 200) return fail("备注最多 200 字;用户原话和最近的对话会自动带上,备注只写从原话里看不出的背景。");
|
|
652
|
+
const original = router.originalWords(assistantId, note);
|
|
653
|
+
await runtime.delegations.start(targetId, original.text, original.files);
|
|
654
|
+
return endTurn(`已转交给「${target.name}」。`);
|
|
510
655
|
} catch (error) {
|
|
511
656
|
return fail(error instanceof Error ? error.message : String(error));
|
|
512
657
|
}
|
|
@@ -517,8 +662,8 @@ export function buildRouterTools(runtime: HobRuntime, assistantId: string): Tool
|
|
|
517
662
|
tools.push({
|
|
518
663
|
name: "hand_back_to_hob",
|
|
519
664
|
label: "交回 Hob",
|
|
520
|
-
description: "
|
|
521
|
-
parameters: Type.Object({ text: Type.String(), reason: Type.String() }),
|
|
665
|
+
description: "这件事不归你管时交回 Hob,由它转给合适的助手。",
|
|
666
|
+
parameters: Type.Object({ text: Type.String({ description: "用户要什么" }), reason: Type.String({ description: "为什么不归你,一句话" }) }),
|
|
522
667
|
execute: async (_id, params: any) => {
|
|
523
668
|
const text = String(params.text ?? "").trim();
|
|
524
669
|
const reason = String(params.reason ?? "").trim();
|
|
@@ -534,7 +679,7 @@ export function buildRouterTools(runtime: HobRuntime, assistantId: string): Tool
|
|
|
534
679
|
await router.addForwarded("hob", text, handbackId, assistantId);
|
|
535
680
|
await router.event("hob", `转自「${assistant().name}」:${text}\n交回原因:${reason}`);
|
|
536
681
|
}
|
|
537
|
-
return endTurn("已交回 Hob
|
|
682
|
+
return endTurn("已交回 Hob。");
|
|
538
683
|
} catch (error) {
|
|
539
684
|
return fail(error instanceof Error ? error.message : String(error));
|
|
540
685
|
}
|
|
@@ -542,7 +687,7 @@ export function buildRouterTools(runtime: HobRuntime, assistantId: string): Tool
|
|
|
542
687
|
});
|
|
543
688
|
}
|
|
544
689
|
|
|
545
|
-
tools.push(...searchTools(runtime.home, () => runtime.settings.settings.search));
|
|
690
|
+
tools.push(findKnowledgeTool(runtime), searchKnowledgeTool(runtime, assistantId), ...searchTools(runtime.home, () => runtime.settings.settings.search));
|
|
546
691
|
return tools;
|
|
547
692
|
}
|
|
548
693
|
|