hob-bot 1.3.0 → 1.5.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 +42 -23
- package/README.zh-CN.md +43 -24
- package/bin/hob.mjs +8 -2
- package/dist/web/assets/{DesktopView-lixZfbYr.js → DesktopView-3QDYo0UU.js} +1 -1
- package/dist/web/assets/index-Dzxrt9T3.js +283 -0
- package/dist/web/assets/index-FUtuyxyO.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 +2 -1
- package/server/src/actions.ts +2 -1
- package/server/src/assistant-skills.ts +2 -2
- package/server/src/boot.ts +8 -0
- package/server/src/config.ts +52 -11
- package/server/src/cron-text.ts +14 -1
- package/server/src/delete-skill.ts +19 -0
- package/server/src/desktop-tools.ts +6 -6
- package/server/src/files.ts +27 -7
- package/server/src/http.ts +41 -9
- package/server/src/job-view.ts +2 -2
- package/server/src/jobs.ts +108 -62
- package/server/src/knowledge-tools.ts +1 -1
- package/server/src/knowledge.ts +9 -4
- package/server/src/layout-cli.ts +25 -0
- package/server/src/layout.ts +749 -0
- package/server/src/main.ts +5 -0
- package/server/src/mcp.ts +3 -2
- package/server/src/memory.ts +2 -2
- package/server/src/now.ts +1 -1
- package/server/src/overview.ts +1 -2
- package/server/src/pages.ts +9 -7
- package/server/src/prompt.ts +47 -110
- package/server/src/push.ts +3 -2
- package/server/src/router-tools.ts +96 -40
- package/server/src/router.ts +5 -4
- package/server/src/routines.ts +177 -22
- package/server/src/runtime.ts +12 -1
- package/server/src/search.ts +1 -1
- package/server/src/secrets.ts +3 -2
- package/server/src/settings.ts +44 -6
- package/server/src/store.ts +123 -40
- package/server/src/transcript.ts +25 -7
- package/server/src/usage-prices.ts +166 -0
- package/server/src/usage.ts +291 -0
- package/server/src/worker-tools.ts +6 -9
- package/shared/types.ts +110 -3
- package/dist/web/assets/index-BZhge0qe.js +0 -267
- package/dist/web/assets/index-DIo1ArRa.css +0 -1
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { Type } from "@earendil-works/pi-ai";
|
|
2
2
|
import { createReadToolDefinition, type ToolDefinition } from "@earendil-works/pi-coding-agent";
|
|
3
3
|
import { existsSync, readFileSync, statSync } from "node:fs";
|
|
4
|
-
import { basename,
|
|
4
|
+
import { basename, extname, join, relative, resolve } from "node:path";
|
|
5
5
|
import { readFile } from "node:fs/promises";
|
|
6
6
|
import { IMAGE_TYPES } from "../../shared/media.ts";
|
|
7
|
-
import { lookupFile } from "./files.ts";
|
|
7
|
+
import { lookupFile, isInPagesDir } from "./files.ts";
|
|
8
|
+
import { resolveMovedPath } from "./layout.ts";
|
|
8
9
|
import {
|
|
9
10
|
COLORS,
|
|
10
11
|
COLOR_IDS,
|
|
@@ -17,8 +18,8 @@ import {
|
|
|
17
18
|
validateCustomProp,
|
|
18
19
|
} from "../../shared/sprites.ts";
|
|
19
20
|
import type { ColorId } from "../../shared/sprites.ts";
|
|
20
|
-
import type { Approval, ApprovalOption, CustomProp, Draft, Routine } from "../../shared/types.ts";
|
|
21
|
-
import { cronToChinese } from "./cron-text.ts";
|
|
21
|
+
import type { Approval, ApprovalOption, CustomProp, Draft, Routine, RoutineCheck } from "../../shared/types.ts";
|
|
22
|
+
import { cronToChinese, timePointText } from "./cron-text.ts";
|
|
22
23
|
import { newId } from "./ids.ts";
|
|
23
24
|
import { findKnowledgeTool, searchKnowledgeTool } from "./knowledge-tools.ts";
|
|
24
25
|
import { generateImage, imageConfigured } from "./image.ts";
|
|
@@ -31,9 +32,35 @@ import type { HobRuntime } from "./runtime.ts";
|
|
|
31
32
|
import { clampDuty } from "./text.ts";
|
|
32
33
|
import { listAssistantSkills, readAssistantSkillBody, saveAssistantSkill, skillInstruction, usableAssistantSkills } from "./assistant-skills.ts";
|
|
33
34
|
import { jobView } from "./job-view.ts";
|
|
35
|
+
import { deleteSkill } from "./delete-skill.ts";
|
|
34
36
|
|
|
35
37
|
type Tool = ToolDefinition<any, any>;
|
|
36
38
|
|
|
39
|
+
/** 执行时间的参数说明:cron 表达式和具体时间都收。 */
|
|
40
|
+
const SCHEDULES_DESCRIPTION = "一个或多个;可以是 cron 表达式,也可以是具体时间(如 2026-11-08T09:00),到点只跑一次。";
|
|
41
|
+
|
|
42
|
+
/** once 的参数说明。 */
|
|
43
|
+
const ONCE_DESCRIPTION = "可选。为 true 时做成一次就停:没有检查脚本的,跑完一次就停;有检查脚本的,第一次有输出就停。停了的定时任务还在,可以再打开。";
|
|
44
|
+
|
|
45
|
+
/** 检查脚本参数:create_routine 和 update_routine 共用。 */
|
|
46
|
+
const CHECK_DESCRIPTION = "可选。到点先跑的 bash 脚本,填脚本全文:没有输出就这次什么都不做;有输出时,then 为 notify 就把输出作为提醒发到对话,为 job 就带着输出开始任务。脚本在这个定时任务自己的目录里运行,状态文件存在当前目录,记住上次看到哪里;第一次跑还没有状态时只记下现状,不输出。非零退出算检查失败。脚本用到的其他文件也放在这个目录,跟着定时任务走。";
|
|
47
|
+
|
|
48
|
+
const CHECK_PARAM = Type.Object({
|
|
49
|
+
script: Type.String({ description: "bash 脚本全文" }),
|
|
50
|
+
then: Type.Union([Type.Literal("notify"), Type.Literal("job")], { description: "有输出时:notify 把输出作为提醒发到对话,job 带着输出开始任务" }),
|
|
51
|
+
}, { description: CHECK_DESCRIPTION });
|
|
52
|
+
|
|
53
|
+
/** 工具返回里说的「每 10 分钟」这类:具体时间只说那一刻,有检查脚本的说明有新内容会怎样,开了 once 的说明会停。 */
|
|
54
|
+
function routineFact(schedules: string[], check: RoutineCheck | undefined, once: boolean | undefined): string {
|
|
55
|
+
const points = schedules.map((schedule) => timePointText(schedule));
|
|
56
|
+
const labels = schedules.map((schedule, index) => points[index] ?? cronToChinese(schedule));
|
|
57
|
+
const facts: string[] = [];
|
|
58
|
+
if (points.every(Boolean)) facts.push(`${labels.join("、")}跑一次`);
|
|
59
|
+
if (check) facts.push(`${labels.join("、")}检查一次${check.then === "notify" ? ",有新内容就提醒你" : ",有新内容就开任务"}`);
|
|
60
|
+
if (once) facts.push("触发一次就停");
|
|
61
|
+
return facts.length ? `,${facts.join(";")}` : "";
|
|
62
|
+
}
|
|
63
|
+
|
|
37
64
|
function ok(text: string) {
|
|
38
65
|
return { content: [{ type: "text" as const, text }], details: undefined };
|
|
39
66
|
}
|
|
@@ -99,6 +126,7 @@ export function buildRouterTools(runtime: HobRuntime, assistantId: string): Tool
|
|
|
99
126
|
readFile: async (path) => readFile(checkFile(path)),
|
|
100
127
|
detectImageMimeType: async (path) => IMAGE_TYPES[extname(checkFile(path)).toLowerCase()],
|
|
101
128
|
} }) as unknown as Tool;
|
|
129
|
+
delete (read as { promptSnippet?: unknown }).promptSnippet; // 用了自定义系统提示词,引擎自带的这段不进提示词
|
|
102
130
|
const tools: Tool[] = [
|
|
103
131
|
{
|
|
104
132
|
name: "check_job",
|
|
@@ -116,7 +144,7 @@ export function buildRouterTools(runtime: HobRuntime, assistantId: string): Tool
|
|
|
116
144
|
{
|
|
117
145
|
name: "edit_skill",
|
|
118
146
|
label: "改技能",
|
|
119
|
-
description: "
|
|
147
|
+
description: "改一个技能的正文(SKILL.md):把一段原文换成新的文字,旧版会保留。脚本要改的,交给任务改。",
|
|
120
148
|
parameters: Type.Object({
|
|
121
149
|
name: Type.String({ description: "技能名" }),
|
|
122
150
|
edits: Type.Array(Type.Object({ old: Type.String(), new: Type.String() }), { description: "一处或几处修改:old 是正文里的原文,要一字不差、只出现一次;new 是换成的文字" }),
|
|
@@ -140,15 +168,28 @@ export function buildRouterTools(runtime: HobRuntime, assistantId: string): Tool
|
|
|
140
168
|
} catch (error) { return fail(error instanceof Error ? error.message : String(error)); }
|
|
141
169
|
},
|
|
142
170
|
},
|
|
171
|
+
{
|
|
172
|
+
name: "delete_skill",
|
|
173
|
+
label: "删除技能",
|
|
174
|
+
description: "删除一个技能。技能会移到归档,不会彻底删掉。",
|
|
175
|
+
parameters: Type.Object({ name: Type.String({ description: "技能名。" }), replaced_by: Type.Optional(Type.String({ description: "可选,被哪个技能接替,填技能名。用到它的定时任务会改用接替的技能。" })) }),
|
|
176
|
+
execute: async (_id, params: { name: string; replaced_by?: string }) => {
|
|
177
|
+
try {
|
|
178
|
+
const text = deleteSkill(runtime, assistantId, params.name, params.replaced_by || undefined);
|
|
179
|
+
await router.addItem(assistantId, { kind: "note", icon: "skill", text });
|
|
180
|
+
return ok(text);
|
|
181
|
+
} catch (error) { return fail(error instanceof Error ? error.message : String(error)); }
|
|
182
|
+
},
|
|
183
|
+
},
|
|
143
184
|
{
|
|
144
185
|
name: "save_job_as_skill",
|
|
145
186
|
label: "沉淀技能",
|
|
146
|
-
description: "
|
|
147
|
-
parameters: Type.Object({ jobId: Type.String() }),
|
|
148
|
-
execute: async (_id, params: { jobId: string }) => {
|
|
187
|
+
description: "让一个已完成的任务把它的做法写成技能。它在自己的会话里写,能把脚本和命令真跑一遍验证;进度显示在任务卡片上,写好以后结果会回到这里。",
|
|
188
|
+
parameters: Type.Object({ jobId: Type.String(), into: Type.Optional(Type.String({ description: "可选,在哪个已有技能的基础上改,填技能名;不填就新建。" })), note: Type.Optional(Type.String({ description: "可选,要交代给任务的话,比如用户的反馈和纠正、要一起并进来的技能。" })) }),
|
|
189
|
+
execute: async (_id, params: { jobId: string; into?: string; note?: string }) => {
|
|
149
190
|
const job = store.job(params.jobId);
|
|
150
191
|
if (!job || job.assistantId !== assistantId) return fail("任务不存在");
|
|
151
|
-
try { jobs.startSediment(job.id,
|
|
192
|
+
try { jobs.startSediment(job.id, { into: params.into || undefined, note: params.note || undefined }); return ok("已开始沉淀,进度显示在任务卡片上。"); }
|
|
152
193
|
catch (error) { return fail(error instanceof Error ? error.message : String(error)); }
|
|
153
194
|
},
|
|
154
195
|
},
|
|
@@ -186,8 +227,7 @@ export function buildRouterTools(runtime: HobRuntime, assistantId: string): Tool
|
|
|
186
227
|
{
|
|
187
228
|
name: "start_job",
|
|
188
229
|
label: "创建任务",
|
|
189
|
-
description: "
|
|
190
|
-
promptSnippet: "把需要执行的任务交给后台任务。",
|
|
230
|
+
description: "把事情交给后台任务去做。用户这次的原话、引用、附件和最近几条对话会自动带给任务,note 只补原话里看不出的背景。",
|
|
191
231
|
parameters: Type.Object({
|
|
192
232
|
title: Type.String({ description: "任务标题,短,如「查磁盘占用」" }),
|
|
193
233
|
note: Type.Optional(Type.String({ description: "任务从原话里看不出、又需要知道的背景,最多 200 字。" })),
|
|
@@ -202,7 +242,7 @@ export function buildRouterTools(runtime: HobRuntime, assistantId: string): Tool
|
|
|
202
242
|
description: "要交给任务的文件路径(用户发来的附件);图片会直接给模型看",
|
|
203
243
|
}),
|
|
204
244
|
),
|
|
205
|
-
|
|
245
|
+
skills: Type.Optional(Type.Array(Type.String(), { description: "给任务用的技能,填技能名。你的技能,任务只看得到你给的这些。" })),
|
|
206
246
|
expect_minutes: Type.Optional(Type.Number({ description: "可选,预计多少分钟能交结果。超过了会通知你;不填按 10 分钟。" })),
|
|
207
247
|
model: Type.Optional(Type.String({ description: `可选,不填用默认。聪明、成本、速度 0–100,越高越聪明、越贵、越快:\n${modelText}` })),
|
|
208
248
|
thinking: Type.Optional(
|
|
@@ -215,15 +255,17 @@ export function buildRouterTools(runtime: HobRuntime, assistantId: string): Tool
|
|
|
215
255
|
const note = params.note == null ? "" : String(params.note);
|
|
216
256
|
if ([...note].length > 200) return fail("备注最多 200 字;用户原话和最近的对话会自动带上,备注只写从原话里看不出的背景。");
|
|
217
257
|
// 有的模型会把没用的可选参数填成空串:空串当没填
|
|
218
|
-
const wanted = params.
|
|
258
|
+
const wanted: string[] = Array.isArray(params.skills) ? [...new Set<string>(params.skills.map(String))] : [];
|
|
219
259
|
const skills = usableAssistantSkills(runtime.home, assistantId);
|
|
220
|
-
const
|
|
221
|
-
if (
|
|
260
|
+
const missing = wanted.find((name) => !skills.some((item) => item.name === name));
|
|
261
|
+
if (missing) return fail(`没有这个技能:${missing}。能用的有:${skills.map((item) => item.name).join("、") || "无"}`);
|
|
262
|
+
const selected = wanted.map((name) => skills.find((item) => item.name === name)!);
|
|
222
263
|
const original = router.originalWords(assistantId, note, true, Array.isArray(params.files) ? params.files.map(String) : []);
|
|
223
264
|
const job = await jobs.start(assistantId, {
|
|
224
265
|
title: String(params.title),
|
|
225
266
|
...(params.expect_minutes != null ? { expectMinutes: params.expect_minutes } : {}),
|
|
226
|
-
instruction:
|
|
267
|
+
instruction: selected.length ? skillInstruction(selected.map((skill) => skill.title), original.text) : original.text,
|
|
268
|
+
...(wanted.length ? { skills: wanted } : {}),
|
|
227
269
|
...(params.cwd ? { cwd: String(params.cwd) } : {}),
|
|
228
270
|
...(original.files.length ? { files: original.files } : {}),
|
|
229
271
|
...(params.model ? { model: String(params.model) } : {}),
|
|
@@ -242,7 +284,7 @@ export function buildRouterTools(runtime: HobRuntime, assistantId: string): Tool
|
|
|
242
284
|
{
|
|
243
285
|
name: "reply_job",
|
|
244
286
|
label: "补充任务说明",
|
|
245
|
-
description: "
|
|
287
|
+
description: "把话交给一个已有的任务:补充、纠正、追问,或者回答它的提问。它带着原来的上下文和目录接着做,已经结束的任务也能接着做。",
|
|
246
288
|
parameters: Type.Object({
|
|
247
289
|
jobId: Type.String({ description: "任务 id" }),
|
|
248
290
|
text: Type.String({ description: "要补充的话" }),
|
|
@@ -275,17 +317,17 @@ export function buildRouterTools(runtime: HobRuntime, assistantId: string): Tool
|
|
|
275
317
|
{
|
|
276
318
|
name: "ask_user",
|
|
277
319
|
label: "询问用户",
|
|
278
|
-
description: "
|
|
320
|
+
description: "在对话里出一张选择题卡片(2 到 3 个选项),用户选完,答案会回到对话。",
|
|
279
321
|
parameters: Type.Object({
|
|
280
322
|
title: Type.String({ description: "问题,一句话" }),
|
|
281
|
-
detail: Type.Optional(Type.String({ description: "
|
|
323
|
+
detail: Type.Optional(Type.String({ description: "补充说明,写清后果。" })),
|
|
282
324
|
options: Type.Array(
|
|
283
325
|
Type.Object({
|
|
284
326
|
label: Type.String({ description: "选项文字,如「暂不发布」" }),
|
|
285
327
|
}),
|
|
286
328
|
{ description: "2 到 3 个选项", minItems: 2, maxItems: 3 },
|
|
287
329
|
),
|
|
288
|
-
remember: Type.Optional(Type.String({ description: "
|
|
330
|
+
remember: Type.Optional(Type.String({ description: "这类事以后可以不再问时,填这类事的简称(如「清理临时文件」);卡片上会多一个「以后直接做」的选项。" })),
|
|
289
331
|
}),
|
|
290
332
|
execute: async (_id, params: any) => {
|
|
291
333
|
const labels: { label: string }[] = Array.isArray(params.options) ? params.options : [];
|
|
@@ -320,7 +362,7 @@ export function buildRouterTools(runtime: HobRuntime, assistantId: string): Tool
|
|
|
320
362
|
{
|
|
321
363
|
name: "set_rule",
|
|
322
364
|
label: "记录规则",
|
|
323
|
-
description: "
|
|
365
|
+
description: "记下用户对某类事的处理方式:ask 是做之前先问,allow 是直接做。",
|
|
324
366
|
parameters: Type.Object({
|
|
325
367
|
text: Type.String({ description: "规则内容,如「发布公众号」" }),
|
|
326
368
|
mode: Type.Unsafe<"ask" | "allow">(Type.String({ description: "ask 或 allow" })),
|
|
@@ -364,11 +406,14 @@ export function buildRouterTools(runtime: HobRuntime, assistantId: string): Tool
|
|
|
364
406
|
{
|
|
365
407
|
name: "create_routine",
|
|
366
408
|
label: "新增定时任务",
|
|
367
|
-
description: "
|
|
409
|
+
description: "新增一个定时任务。执行时间用 cron 表达式,如「0 9 * * *」每天 9 点;只跑一次就填具体时间;多个执行时间就传多个。",
|
|
368
410
|
parameters: Type.Object({
|
|
369
411
|
name: Type.String({ description: "定时任务名称,如「每日简报」" }),
|
|
370
412
|
instruction: Type.String({ description: "到点要执行的内容" }),
|
|
371
|
-
schedules: Type.Array(Type.String({ description: "cron
|
|
413
|
+
schedules: Type.Array(Type.String({ description: "cron 表达式或具体时间" }), { description: SCHEDULES_DESCRIPTION }),
|
|
414
|
+
skill: Type.Optional(Type.String({ description: "可选,按哪个技能做,填技能名。" })),
|
|
415
|
+
check: Type.Optional(CHECK_PARAM),
|
|
416
|
+
once: Type.Optional(Type.Boolean({ description: ONCE_DESCRIPTION })),
|
|
372
417
|
}),
|
|
373
418
|
execute: async (_id, params: any) => {
|
|
374
419
|
try {
|
|
@@ -377,10 +422,13 @@ export function buildRouterTools(runtime: HobRuntime, assistantId: string): Tool
|
|
|
377
422
|
name: String(params.name),
|
|
378
423
|
instruction: String(params.instruction),
|
|
379
424
|
schedules: (params.schedules as string[]) ?? [],
|
|
425
|
+
...(params.skill ? { skill: String(params.skill) } : {}),
|
|
426
|
+
...(params.check ? { check: params.check as RoutineCheck } : {}),
|
|
427
|
+
...(params.once ? { once: true } : {}),
|
|
380
428
|
});
|
|
381
429
|
await note("routine", `新增定时任务 · ${routine.name} · ${routine.schedules.map(cronToChinese).join("、")}`);
|
|
382
430
|
profileChanged();
|
|
383
|
-
return ok(`定时任务「${routine.name}
|
|
431
|
+
return ok(`定时任务「${routine.name}」已创建${routineFact(routine.schedules, routine.check, routine.once)}。它的目录是 ${routines.dir(routine)},检查脚本在这里运行;脚本要用的其他文件,可以派任务写到这个目录。`);
|
|
384
432
|
} catch (error) {
|
|
385
433
|
return fail(error instanceof Error ? error.message : String(error));
|
|
386
434
|
}
|
|
@@ -389,13 +437,16 @@ export function buildRouterTools(runtime: HobRuntime, assistantId: string): Tool
|
|
|
389
437
|
{
|
|
390
438
|
name: "update_routine",
|
|
391
439
|
label: "修改定时任务",
|
|
392
|
-
description: "修改一个定时任务:名称、内容、执行时间(schedules
|
|
440
|
+
description: "修改一个定时任务:名称、内容、执行时间(schedules 会整体替换)、按哪个技能做、检查脚本、启用状态。",
|
|
393
441
|
parameters: Type.Object({
|
|
394
442
|
routineId: Type.String(),
|
|
395
443
|
name: Type.Optional(Type.String()),
|
|
396
444
|
instruction: Type.Optional(Type.String()),
|
|
397
|
-
schedules: Type.Optional(Type.Array(Type.String())),
|
|
445
|
+
schedules: Type.Optional(Type.Array(Type.String({ description: "cron 表达式或具体时间" }), { description: SCHEDULES_DESCRIPTION })),
|
|
398
446
|
active: Type.Optional(Type.Boolean()),
|
|
447
|
+
skill: Type.Optional(Type.String({ description: "按哪个技能做,填技能名;填空字符串表示不用技能。" })),
|
|
448
|
+
check: Type.Optional(Type.Union([CHECK_PARAM, Type.Null()], { description: `${CHECK_DESCRIPTION}传 null 去掉检查。` })),
|
|
449
|
+
once: Type.Optional(Type.Boolean({ description: ONCE_DESCRIPTION })),
|
|
399
450
|
}),
|
|
400
451
|
execute: async (_id, params: any) => {
|
|
401
452
|
try {
|
|
@@ -404,10 +455,13 @@ export function buildRouterTools(runtime: HobRuntime, assistantId: string): Tool
|
|
|
404
455
|
...(params.instruction ? { instruction: String(params.instruction) } : {}),
|
|
405
456
|
...(params.schedules ? { schedules: params.schedules as string[] } : {}),
|
|
406
457
|
...(typeof params.active === "boolean" ? { active: params.active } : {}),
|
|
458
|
+
...(typeof params.skill === "string" ? { skill: params.skill } : {}),
|
|
459
|
+
...(params.check === null || params.check ? { check: params.check as RoutineCheck | null } : {}),
|
|
460
|
+
...(typeof params.once === "boolean" ? { once: params.once } : {}),
|
|
407
461
|
});
|
|
408
462
|
await note("routine", `修改定时任务 · ${routine.name} · ${routine.schedules.map(cronToChinese).join("、")}`);
|
|
409
463
|
profileChanged();
|
|
410
|
-
return ok(
|
|
464
|
+
return ok(`已修改${routineFact(routine.schedules, routine.check, routine.once)}。目录是 ${routines.dir(routine)}。`);
|
|
411
465
|
} catch (error) {
|
|
412
466
|
return fail(error instanceof Error ? error.message : String(error));
|
|
413
467
|
}
|
|
@@ -432,14 +486,16 @@ export function buildRouterTools(runtime: HobRuntime, assistantId: string): Tool
|
|
|
432
486
|
{
|
|
433
487
|
name: "send_file",
|
|
434
488
|
label: "发送文件",
|
|
435
|
-
description: "
|
|
489
|
+
description: "把一个文件(任务交来的文件、截图、任务生成的图)作为卡片发到对话里,图片直接显示。",
|
|
436
490
|
parameters: Type.Object({ path: Type.String({ description: "文件的绝对路径" }) }),
|
|
437
491
|
execute: async (_id, params: any) => {
|
|
438
|
-
const
|
|
439
|
-
|
|
492
|
+
const requested = String(params.path);
|
|
493
|
+
// 迁移后的老路径直接换到新位置(不给模型提示),卡片里存新路径
|
|
494
|
+
const path = existsSync(requested) ? requested : resolveMovedPath(runtime.home, requested);
|
|
495
|
+
if (!existsSync(path)) return fail(`文件不存在:${requested}`);
|
|
440
496
|
let name = basename(path);
|
|
441
497
|
let meta = fileMeta(path);
|
|
442
|
-
if (
|
|
498
|
+
if (isInPagesDir(relative(runtime.home, resolve(path))) && extname(path).toLowerCase() === ".html") {
|
|
443
499
|
const html = await readFile(path, "utf8");
|
|
444
500
|
const title = html.match(/<title>([\s\S]*?)<\/title>/)?.[1];
|
|
445
501
|
const summary = html.match(/<header class="hd"><h1>[\s\S]*?<\/h1><p>([\s\S]*?)<\/p><\/header>/)?.[1];
|
|
@@ -525,7 +581,7 @@ export function buildRouterTools(runtime: HobRuntime, assistantId: string): Tool
|
|
|
525
581
|
tools.push({
|
|
526
582
|
name: "generate_image",
|
|
527
583
|
label: "生成图片",
|
|
528
|
-
description: "
|
|
584
|
+
description: "按文字生成或修改图片,直接显示在对话里:生成时有进度卡片,画好自动显示。返回的路径可以作为之后改图的参考图。",
|
|
529
585
|
parameters: Type.Object({
|
|
530
586
|
prompt: Type.String({ description: "要画什么,越具体越好" }),
|
|
531
587
|
size: Type.Optional(Type.String({ description: "尺寸,如 1024x1024;不填用设置里的默认" })),
|
|
@@ -537,13 +593,13 @@ export function buildRouterTools(runtime: HobRuntime, assistantId: string): Tool
|
|
|
537
593
|
const size = params.size ? String(params.size) : config.source === "openai" ? config.size : undefined;
|
|
538
594
|
const item = await router.addItem(assistantId, { kind: "image", status: "pending", prompt, ...(size ? { size } : {}), startedAt: new Date().toISOString() });
|
|
539
595
|
try {
|
|
540
|
-
const path = await generateImage({ home: runtime.home, dir: join(runtime.home, "images"), config }, {
|
|
596
|
+
const path = await generateImage({ home: runtime.home, dir: join(runtime.home, "assistants", assistantId, "files", "images"), config }, {
|
|
541
597
|
prompt,
|
|
542
598
|
...(params.size ? { size: String(params.size) } : {}),
|
|
543
599
|
...(Array.isArray(params.refs) && params.refs.length ? { refs: params.refs.map(String) } : {}),
|
|
544
600
|
});
|
|
545
601
|
await router.updateImage(assistantId, item.id, { status: "done", path });
|
|
546
|
-
return ok(
|
|
602
|
+
return ok(`图片已经显示在对话里。路径:${path},之后改图可以作为参考图。`);
|
|
547
603
|
} catch (error) {
|
|
548
604
|
const message = error instanceof Error ? error.message : String(error);
|
|
549
605
|
await router.updateImage(assistantId, item.id, { status: "failed", error: message });
|
|
@@ -561,10 +617,10 @@ export function buildRouterTools(runtime: HobRuntime, assistantId: string): Tool
|
|
|
561
617
|
{
|
|
562
618
|
name: "propose_assistant",
|
|
563
619
|
label: "提议创建专属助手",
|
|
564
|
-
description: "
|
|
620
|
+
description: "提议新建一个专属助手:对话里出一张草稿卡,用户点「创建」才会建。做事的方法由所有助手共用的模板和全局规则决定,草稿里只写这个助手特有的东西。",
|
|
565
621
|
parameters: Type.Object({
|
|
566
|
-
name: Type.String({ description: "
|
|
567
|
-
duty: Type.String({ description: "左栏头像下面那行小字,最多 8
|
|
622
|
+
name: Type.String({ description: "助手名字,职责加一个亲切的昵称,四五个字,如「写稿搭子」「记账管家」。" }),
|
|
623
|
+
duty: Type.String({ description: "左栏头像下面那行小字,最多 8 个字,如「周报和文章」「账单和预算」。" }),
|
|
568
624
|
dutyLong: Type.String({ description: "草稿卡里的职责说明,两三句话,写完整一些" }),
|
|
569
625
|
sprite: Type.String({
|
|
570
626
|
description: `内置道具:${PROP_IDS.filter((id) => id !== "hob").map((id) => `${id} ${PROPS[id].label}`).join(",")};或 custom(同时填 prop_art,自己画一件)`,
|
|
@@ -591,7 +647,7 @@ export function buildRouterTools(runtime: HobRuntime, assistantId: string): Tool
|
|
|
591
647
|
routines: Type.Optional(
|
|
592
648
|
Type.Array(
|
|
593
649
|
Type.Object({ name: Type.String(), instruction: Type.String(), schedule: Type.String() }),
|
|
594
|
-
{ description: "
|
|
650
|
+
{ description: "可选。这个助手要定期做的事,不是每个助手都需要;拿不准就先问用户,问清楚再加。" },
|
|
595
651
|
),
|
|
596
652
|
),
|
|
597
653
|
askFirst: Type.Optional(Type.Array(Type.String(), { description: "这个领域特有、做之前要先问用户的事,每项一条;全局规则已经列的不重复。" })),
|
|
@@ -645,7 +701,7 @@ export function buildRouterTools(runtime: HobRuntime, assistantId: string): Tool
|
|
|
645
701
|
{
|
|
646
702
|
name: "message_assistant",
|
|
647
703
|
label: "转交其他助手",
|
|
648
|
-
description: "
|
|
704
|
+
description: "把一件事转给另一个助手(见「助手」一节),由它负责到底。用户的原话和附件会自动转过去,对方的回复和结果会回到这里。",
|
|
649
705
|
parameters: Type.Object({
|
|
650
706
|
assistantId: Type.String({ description: "目标助手 id" }),
|
|
651
707
|
note: Type.Optional(Type.String({ description: "可选,对方需要知道的背景,最多 200 字。" })),
|
|
@@ -671,7 +727,7 @@ export function buildRouterTools(runtime: HobRuntime, assistantId: string): Tool
|
|
|
671
727
|
tools.push({
|
|
672
728
|
name: "hand_back_to_hob",
|
|
673
729
|
label: "交回 Hob",
|
|
674
|
-
description: "
|
|
730
|
+
description: "把不归你管的事交回 Hob,由它转给合适的助手。",
|
|
675
731
|
parameters: Type.Object({ text: Type.String({ description: "用户要什么" }), reason: Type.String({ description: "为什么不归你,一句话" }) }),
|
|
676
732
|
execute: async (_id, params: any) => {
|
|
677
733
|
const text = String(params.text ?? "").trim();
|
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, statSync, writeFileSync } from "node:fs";
|
|
4
4
|
import { mkdirSync } from "node:fs";
|
|
5
|
-
import { dirname, join, relative, resolve
|
|
5
|
+
import { dirname, join, relative, resolve } 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,6 +21,7 @@ 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 { isInPagesDir } from "./files.ts";
|
|
24
25
|
import { boundKnowledge, formatKnowledge, KNOWLEDGE_INTRO } from "./knowledge-tools.ts";
|
|
25
26
|
import { MEMORY_WRITE_TOOLS, memoryNoteText } from "./memory.ts";
|
|
26
27
|
import { buildSystemPrompt, type PromptModel } from "./prompt.ts";
|
|
@@ -324,7 +325,8 @@ export class RouterManager {
|
|
|
324
325
|
aboutYou,
|
|
325
326
|
memory: this.runtime.settings.memory().provider,
|
|
326
327
|
searchEnabled: this.runtime.settings.settings.search.some((provider) => provider.enabled),
|
|
327
|
-
imageEnabled:
|
|
328
|
+
imageEnabled: tools.some((tool) => tool.name === "generate_image"),
|
|
329
|
+
showPageEnabled: tools.some((tool) => tool.name === "show_page"),
|
|
328
330
|
desktopEnabled: desktop.enabled && !!desktop.url && desktop.tokenConfigured,
|
|
329
331
|
publicDesktop: desktop.publicAssistants.includes(assistant.id),
|
|
330
332
|
models: this.promptModels(assistant.id),
|
|
@@ -672,8 +674,7 @@ export class RouterManager {
|
|
|
672
674
|
const full = { ...item, ...(lead ? { lead: true } : {}), id, at: this.entryAt(state, id) } as Item;
|
|
673
675
|
this.bus.emit({ type: "item", assistantId, item: full });
|
|
674
676
|
if (full.kind === "file") {
|
|
675
|
-
const
|
|
676
|
-
const page = rel !== ".." && !rel.startsWith(`..${sep}`);
|
|
677
|
+
const page = isInPagesDir(relative(this.runtime.home, resolve(full.path)));
|
|
677
678
|
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));
|
|
678
679
|
}
|
|
679
680
|
return full;
|