hob-bot 1.0.3 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +75 -5
- package/README.zh-CN.md +75 -5
- package/dist/web/assets/DesktopView-rVxacveF.js +5 -0
- package/dist/web/assets/index-B-8ISOr1.js +267 -0
- package/dist/web/assets/index-Cu1Pf8yX.css +1 -0
- package/dist/web/assets/{math-zZ4JA8eM.js → math-weKGDUmL.js} +2 -2
- package/dist/web/index.html +2 -2
- package/dist/web/mock/desktop.jpg +0 -0
- package/dist/web/mock/photo.jpg +0 -0
- package/dist/web/mock/photo2.jpg +0 -0
- package/dist/web/mock/sunset.html +90 -0
- package/dist/web/mock//347/201/253/346/230/237/346/227/245/350/220/275/347/244/272/346/204/217/345/233/276.md +34 -0
- package/package.json +10 -2
- package/server/src/actions.ts +24 -57
- package/server/src/assistant-skills.ts +201 -0
- package/server/src/boot.ts +1 -0
- package/server/src/claude-status.ts +47 -0
- package/server/src/config.ts +3 -1
- package/server/src/delegations.ts +40 -12
- package/server/src/desktop-control.ts +152 -0
- package/server/src/desktop-events.ts +63 -0
- package/server/src/desktop-public.ts +43 -0
- package/server/src/desktop-tools.ts +161 -0
- package/server/src/desktop.ts +76 -0
- package/server/src/embedding.ts +52 -0
- package/server/src/files.ts +63 -43
- package/server/src/http.ts +334 -15
- package/server/src/jobs.ts +330 -59
- package/server/src/knowledge-index.ts +123 -0
- package/server/src/knowledge-tools.ts +94 -0
- package/server/src/knowledge.ts +451 -0
- package/server/src/lore-entry.ts +10 -0
- package/server/src/main.ts +6 -0
- package/server/src/mcp.ts +745 -0
- package/server/src/memory.ts +11 -4
- package/server/src/now.ts +33 -0
- package/server/src/overview.ts +1 -1
- package/server/src/pages.ts +1 -1
- package/server/src/pi.ts +92 -16
- package/server/src/prompt.ts +46 -154
- package/server/src/providers.ts +3 -2
- package/server/src/push.ts +42 -7
- package/server/src/router-activity.ts +4 -0
- package/server/src/router-tools.ts +132 -38
- package/server/src/router.ts +159 -19
- package/server/src/routines.ts +14 -4
- package/server/src/runtime.ts +15 -0
- package/server/src/settings.ts +43 -1
- package/server/src/store.ts +4 -3
- package/server/src/thumbs.ts +49 -0
- package/server/src/transcript.ts +57 -4
- package/server/src/worker-tools.ts +14 -12
- package/shared/knowledge.ts +92 -0
- package/shared/mcp.ts +91 -0
- package/shared/media.ts +6 -0
- package/shared/plain-text.ts +31 -0
- package/shared/skills.ts +18 -0
- package/shared/sprites.ts +198 -0
- package/shared/types.ts +105 -20
- package/vendor/claude-bridge/HOB-PATCH.md +10 -0
- package/vendor/claude-bridge/LICENSE +21 -0
- package/vendor/claude-bridge/package.json +62 -0
- package/vendor/claude-bridge/src/agents-md.ts +14 -0
- package/vendor/claude-bridge/src/askclaude-schema.ts +92 -0
- package/vendor/claude-bridge/src/askclaude-ui.ts +90 -0
- package/vendor/claude-bridge/src/attachments.ts +135 -0
- package/vendor/claude-bridge/src/config.ts +105 -0
- package/vendor/claude-bridge/src/convert.ts +222 -0
- package/vendor/claude-bridge/src/extract-tool-results.ts +47 -0
- package/vendor/claude-bridge/src/index.ts +2503 -0
- package/vendor/claude-bridge/src/log-paths.ts +19 -0
- package/vendor/claude-bridge/src/mcp-server.ts +169 -0
- package/vendor/claude-bridge/src/models.ts +164 -0
- package/vendor/claude-bridge/src/prompt-capture.ts +420 -0
- package/vendor/claude-bridge/src/prompt-stream.ts +102 -0
- package/vendor/claude-bridge/src/query-state.ts +118 -0
- package/vendor/claude-bridge/src/rate-limit.ts +48 -0
- package/vendor/claude-bridge/src/session-cwd.ts +38 -0
- package/vendor/claude-bridge/src/session-verify.ts +38 -0
- package/vendor/claude-bridge/src/skills.ts +20 -0
- package/vendor/claude-bridge/src/transcript.ts +86 -0
- package/vendor/claude-bridge/src/usage.ts +53 -0
- package/vendor/lore-pi-extension/package.json +1 -1
- package/vendor/lore-pi-extension/tools.ts +8 -5
- package/dist/web/assets/index-C72gAwgN.css +0 -1
- package/dist/web/assets/index-Dyw5G8Pg.js +0 -230
package/server/src/http.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { createHmac, randomBytes, timingSafeEqual } from "node:crypto";
|
|
2
|
-
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
import { Hono } from "hono";
|
|
5
5
|
import { getCookie, setCookie } from "hono/cookie";
|
|
@@ -7,6 +7,7 @@ import { streamSSE } from "hono/streaming";
|
|
|
7
7
|
import { serveStatic } from "@hono/node-server/serve-static";
|
|
8
8
|
import type {
|
|
9
9
|
AboutInfo,
|
|
10
|
+
ClaudeCodeSettings,
|
|
10
11
|
Assistant,
|
|
11
12
|
HobEvent,
|
|
12
13
|
ImageKeyView,
|
|
@@ -22,12 +23,19 @@ import type {
|
|
|
22
23
|
SettingsPatch,
|
|
23
24
|
} from "../../shared/types.ts";
|
|
24
25
|
import { parseSprite } from "../../shared/sprites.ts";
|
|
25
|
-
import { answerApproval, createFromDraft, dismissDraft
|
|
26
|
+
import { answerApproval, archiveAssistant, createFromDraft, dismissDraft } from "./actions.ts";
|
|
26
27
|
import { setPassword } from "./config.ts";
|
|
27
28
|
import { newId } from "./ids.ts";
|
|
29
|
+
import { claudeStatus } from "./claude-status.ts";
|
|
30
|
+
import { checkDesktopUrl } from "./desktop.ts";
|
|
31
|
+
import { desktopProfile, openDesktopBrowser } from "./desktop-tools.ts";
|
|
28
32
|
import { listJobFiles, lookupFile, MAX_TEXT_BYTES } from "./files.ts";
|
|
33
|
+
import { thumbnail } from "./thumbs.ts";
|
|
29
34
|
import { localLoreHome, readLocalLore, readLocalLoreConnection } from "./imports.ts";
|
|
35
|
+
import { importMcpServers, listMcpImports, mcpView, patchMcpServer, removeMcpServer, saveMcpServer, testMcpServer } from "./mcp.ts";
|
|
36
|
+
import type { McpServerPatch, McpServerSave } from "../../shared/mcp.ts";
|
|
30
37
|
import { checkImageSettings, generateImage, imageConfigured, imageKey, IMAGE_KEY_SECRET } from "./image.ts";
|
|
38
|
+
import { checkEmbeddingSettings, embed, embeddingKey, embeddingModel, EMBEDDING_KEY_SECRET } from "./embedding.ts";
|
|
31
39
|
import { checkAssistantModels, checkModelsPatch } from "./models.ts";
|
|
32
40
|
import { saveLoreConnection, testLoreConnection } from "./memory.ts";
|
|
33
41
|
import { verifyPassword } from "./password.ts";
|
|
@@ -37,6 +45,7 @@ import { SEARCH_PROVIDERS } from "../../shared/search.ts";
|
|
|
37
45
|
import { WebSearch } from "./search.ts";
|
|
38
46
|
import { clampDuty } from "./text.ts";
|
|
39
47
|
import { deleteOwnSkill, importSkills, listImportSkills, skillViews } from "./skills.ts";
|
|
48
|
+
import { archiveAssistantSkill, listAssistantSkills, rollbackAssistantSkill, setAssistantSkillEnabled } from "./assistant-skills.ts";
|
|
40
49
|
import type { SkillImportSource } from "../../shared/skills.ts";
|
|
41
50
|
|
|
42
51
|
const COOKIE = "hob";
|
|
@@ -52,7 +61,7 @@ function issueCookie(config: HobRuntime["config"]): string {
|
|
|
52
61
|
return `${token}.${signature(config.cookieSecret, token)}`;
|
|
53
62
|
}
|
|
54
63
|
|
|
55
|
-
function validCookie(config: HobRuntime["config"], value: string | undefined): boolean {
|
|
64
|
+
export function validCookie(config: HobRuntime["config"], value: string | undefined): boolean {
|
|
56
65
|
if (!value) return false;
|
|
57
66
|
const dot = value.lastIndexOf(".");
|
|
58
67
|
if (dot <= 0) return false;
|
|
@@ -69,6 +78,8 @@ function validCookie(config: HobRuntime["config"], value: string | undefined): b
|
|
|
69
78
|
|
|
70
79
|
export function createApp(runtime: HobRuntime, options: { webDir?: string; home: string }): Hono {
|
|
71
80
|
const { store, bus, router, jobs, routines, config } = runtime; const app = new Hono();
|
|
81
|
+
// ponytail: 每次取文件都扫一遍 job.json(几十上百个),慢了再缓存
|
|
82
|
+
const workDirs = () => store.jobs().flatMap((job) => (job.cwd ? [job.cwd] : []));
|
|
72
83
|
|
|
73
84
|
app.use("/api/*", async (c, next) => {
|
|
74
85
|
if (c.req.path === "/api/login") return next();
|
|
@@ -131,6 +142,66 @@ export function createApp(runtime: HobRuntime, options: { webDir?: string; home:
|
|
|
131
142
|
return c.json(next);
|
|
132
143
|
});
|
|
133
144
|
|
|
145
|
+
app.get("/api/desktop", (c) => c.json(runtime.desktop.view()));
|
|
146
|
+
app.put("/api/desktop", async (c) => {
|
|
147
|
+
if (runtime.desktop.deployed) return c.json({ error: "云桌面由部署配置,不能在此修改。" }, 403);
|
|
148
|
+
const body = await c.req.json().catch(() => ({})) as { revision?: unknown; url?: unknown; enabled?: unknown; token?: unknown };
|
|
149
|
+
if (typeof body.revision !== "number" || typeof body.url !== "string" || typeof body.enabled !== "boolean" || (body.token !== undefined && typeof body.token !== "string")) return c.json({ error: "云桌面配置无效。" }, 400);
|
|
150
|
+
try {
|
|
151
|
+
const url = checkDesktopUrl(body.url);
|
|
152
|
+
const saved = runtime.settings.setDesktop(body.revision, { url, enabled: body.enabled });
|
|
153
|
+
if (!saved) return c.json({ error: "设置已在别处修改,已刷新" }, 409);
|
|
154
|
+
if (body.token !== undefined) writeSecret(options.home, "desktop:token", body.token);
|
|
155
|
+
runtime.desktopEvents.restart();
|
|
156
|
+
return c.json(runtime.desktop.view());
|
|
157
|
+
} catch (error) { return c.json({ error: error instanceof Error ? error.message : String(error) }, 400); }
|
|
158
|
+
});
|
|
159
|
+
app.put("/api/desktop/public", async (c) => {
|
|
160
|
+
const body = await c.req.json().catch(() => ({})) as { revision?: unknown; assistants?: unknown };
|
|
161
|
+
if (typeof body.revision !== "number" || !Array.isArray(body.assistants) || !body.assistants.every((id) => typeof id === "string" && !!runtime.store.assistant(id))) return c.json({ error: "助手列表无效。" }, 400);
|
|
162
|
+
const current = runtime.settings.settings.desktop ?? { url: "", enabled: false };
|
|
163
|
+
const saved = runtime.settings.setDesktop(body.revision, { ...current, publicAssistants: [...new Set(body.assistants)] });
|
|
164
|
+
if (!saved) return c.json({ error: "设置已在别处修改,已刷新" }, 409);
|
|
165
|
+
return c.json(runtime.desktop.view());
|
|
166
|
+
});
|
|
167
|
+
const validScreen = (id: string) => id === "public" || runtime.store.assistants().some((assistant) => desktopProfile(assistant.id) === id);
|
|
168
|
+
app.get("/api/desktop/screens", (c) => c.json({ screens: runtime.desktopEvents.screens, profiles: Object.fromEntries(runtime.store.assistants().map((assistant) => [assistant.id, desktopProfile(assistant.id)])) }));
|
|
169
|
+
app.post("/api/desktop/screens/:id/start", async (c) => {
|
|
170
|
+
const id = c.req.param("id");
|
|
171
|
+
if (!/^[a-z0-9-]{1,64}$/.test(id)) return c.json({ error: "屏幕 id 无效。" }, 400);
|
|
172
|
+
if (!validScreen(id)) return c.json({ error: "屏幕不存在。" }, 404);
|
|
173
|
+
try { await runtime.desktop.request(`/api/screens/${id}/start`, { method: "POST" }); return c.json({ ok: true }); }
|
|
174
|
+
catch (error) { return c.json({ error: error instanceof Error ? error.message : String(error) }, 502); }
|
|
175
|
+
});
|
|
176
|
+
app.get("/api/desktop/control", (c) => {
|
|
177
|
+
const screen = c.req.query("screen") ?? "hob";
|
|
178
|
+
if (!validScreen(screen)) return c.json({ error: "屏幕不存在。" }, 404);
|
|
179
|
+
return c.json(runtime.desktopControl.state(c.req.query("client") ?? "", screen));
|
|
180
|
+
});
|
|
181
|
+
app.post("/api/desktop/control", async (c) => {
|
|
182
|
+
const body = await c.req.json().catch(() => ({})) as { client?: string; action?: string; screen?: string };
|
|
183
|
+
const screen = typeof body.screen === "string" ? body.screen : "hob";
|
|
184
|
+
if (!validScreen(screen)) return c.json({ error: "屏幕不存在。" }, 404);
|
|
185
|
+
try {
|
|
186
|
+
if (body.action === "take") runtime.desktopControl.take(body.client ?? "", screen);
|
|
187
|
+
else if (body.action === "release") runtime.desktopControl.release(body.client ?? "", screen);
|
|
188
|
+
else throw new Error("操作无效。");
|
|
189
|
+
return c.json(runtime.desktopControl.state(body.client ?? "", screen));
|
|
190
|
+
} catch (error) { return c.json({ error: error instanceof Error ? error.message : String(error) }, 409); }
|
|
191
|
+
});
|
|
192
|
+
// 接管后要自己登录网站:在云桌面里打开某个助手的浏览器(每个助手的登录状态分开)
|
|
193
|
+
app.post("/api/desktop/browser", async (c) => {
|
|
194
|
+
const body = await c.req.json().catch(() => ({})) as { assistantId?: unknown };
|
|
195
|
+
const id = typeof body.assistantId === "string" ? body.assistantId : "";
|
|
196
|
+
if (!runtime.store.assistant(id)) return c.json({ error: "助手不存在" }, 404);
|
|
197
|
+
try { await openDesktopBrowser(runtime, id); return c.json({ ok: true }); }
|
|
198
|
+
catch (error) { return c.json({ error: error instanceof Error ? error.message : String(error) }, 502); }
|
|
199
|
+
});
|
|
200
|
+
app.post("/api/desktop/test", async (c) => {
|
|
201
|
+
try { return c.json({ ok: true, health: await (await runtime.desktop.request("/api/health")).json() }); }
|
|
202
|
+
catch (error) { return c.json({ ok: false, error: error instanceof Error ? error.message : String(error) }); }
|
|
203
|
+
});
|
|
204
|
+
|
|
134
205
|
app.get("/api/skills", async (c) => {
|
|
135
206
|
const memory = runtime.settings.memory();
|
|
136
207
|
try { if (memory.provider === "lore" && !runtime.pi.loreSkillPaths.length) await runtime.pi.discoverLoreSkills(); }
|
|
@@ -152,6 +223,67 @@ export function createApp(runtime: HobRuntime, options: { webDir?: string; home:
|
|
|
152
223
|
catch (error) { return c.json({ error: error instanceof Error ? error.message : String(error) }, 400); }
|
|
153
224
|
});
|
|
154
225
|
|
|
226
|
+
// 设置 → MCP:列表(密钥不回传)。文件坏了返回错误,不当成空列表
|
|
227
|
+
app.get("/api/mcp", (c) => {
|
|
228
|
+
try {
|
|
229
|
+
return c.json(mcpView(options.home));
|
|
230
|
+
} catch (error) {
|
|
231
|
+
return c.json({ error: error instanceof Error ? error.message : String(error) }, 500);
|
|
232
|
+
}
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
// 新建或改一个服务(名称在地址里;env / headers 逐键保留、替换、删除)
|
|
236
|
+
app.put("/api/mcp/:name", async (c) => {
|
|
237
|
+
const body = (await c.req.json().catch(() => ({}))) as McpServerSave;
|
|
238
|
+
try {
|
|
239
|
+
return c.json(await saveMcpServer(options.home, c.req.param("name"), body));
|
|
240
|
+
} catch (error) {
|
|
241
|
+
return c.json({ error: error instanceof Error ? error.message : String(error) }, 400);
|
|
242
|
+
}
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
// 启用开关、提供方式、单独关掉的工具
|
|
246
|
+
app.patch("/api/mcp/:name", async (c) => {
|
|
247
|
+
const body = (await c.req.json().catch(() => ({}))) as McpServerPatch;
|
|
248
|
+
try {
|
|
249
|
+
return c.json(await patchMcpServer(options.home, c.req.param("name"), body));
|
|
250
|
+
} catch (error) {
|
|
251
|
+
return c.json({ error: error instanceof Error ? error.message : String(error) }, 400);
|
|
252
|
+
}
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
app.delete("/api/mcp/:name", async (c) => {
|
|
256
|
+
try {
|
|
257
|
+
return c.json(await removeMcpServer(options.home, c.req.param("name")));
|
|
258
|
+
} catch (error) {
|
|
259
|
+
return c.json({ error: error instanceof Error ? error.message : String(error) }, 400);
|
|
260
|
+
}
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
// 测试连接:连接 → 列工具 → 关闭;失败也是 200,原因在返回值里
|
|
264
|
+
app.post("/api/mcp/:name/test", async (c) => {
|
|
265
|
+
const body = (await c.req.json().catch(() => ({}))) as { refresh?: boolean };
|
|
266
|
+
try {
|
|
267
|
+
return c.json(await testMcpServer(options.home, c.req.param("name"), body.refresh !== false));
|
|
268
|
+
} catch (error) {
|
|
269
|
+
return c.json({ error: error instanceof Error ? error.message : String(error) }, 400);
|
|
270
|
+
}
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
// 从本机导入:只读 pi / Claude Code / Cursor 的配置,同名跳过
|
|
274
|
+
app.get("/api/mcp/import", (c) => c.json(listMcpImports(options.home)));
|
|
275
|
+
|
|
276
|
+
app.post("/api/mcp/import", async (c) => {
|
|
277
|
+
const body = (await c.req.json().catch(() => ({}))) as { ids?: unknown };
|
|
278
|
+
const ids = Array.isArray(body.ids) ? body.ids.filter((id): id is string => typeof id === "string") : [];
|
|
279
|
+
if (!ids.length) return c.json({ error: "请选择要导入的服务。" }, 400);
|
|
280
|
+
try {
|
|
281
|
+
return c.json(await importMcpServers(options.home, ids));
|
|
282
|
+
} catch (error) {
|
|
283
|
+
return c.json({ error: error instanceof Error ? error.message : String(error) }, 400);
|
|
284
|
+
}
|
|
285
|
+
});
|
|
286
|
+
|
|
155
287
|
// 设置 → 模型:引擎注册表里现在能用的聊天模型 + 名单里哪些已经不可用
|
|
156
288
|
app.get("/api/models", async (c) => {
|
|
157
289
|
const models = await runtime.models.get();
|
|
@@ -186,6 +318,24 @@ export function createApp(runtime: HobRuntime, options: { webDir?: string; home:
|
|
|
186
318
|
});
|
|
187
319
|
|
|
188
320
|
// 设置 → 服务商:列表(Key 只报配没配)、引擎自带的服务商目录、整份设置
|
|
321
|
+
app.get("/api/providers/claude-status", async (c) => c.json(await claudeStatus(runtime.home, runtime.settings.settings.claude?.executable || undefined)));
|
|
322
|
+
app.get("/api/claude", async (c) => {
|
|
323
|
+
await runtime.providers.apply();
|
|
324
|
+
return c.json({ settings: runtime.settings.get(), status: await claudeStatus(runtime.home, runtime.settings.settings.claude?.executable || undefined) });
|
|
325
|
+
});
|
|
326
|
+
app.put("/api/claude", async (c) => {
|
|
327
|
+
const body = await c.req.json().catch(() => ({})) as { revision?: number; claude?: ClaudeCodeSettings };
|
|
328
|
+
try {
|
|
329
|
+
if (!body.claude) throw new Error("Claude Code 设置无效。");
|
|
330
|
+
const settings = runtime.settings.setClaude(body.revision ?? -1, body.claude);
|
|
331
|
+
if (!settings) return c.json({ error: "设置已更改,请刷新后重试。" }, 409);
|
|
332
|
+
await runtime.providers.apply();
|
|
333
|
+
router.releaseSessions();
|
|
334
|
+
return c.json({ settings, status: await claudeStatus(runtime.home, settings.settings.claude?.executable || undefined) });
|
|
335
|
+
} catch (error) {
|
|
336
|
+
return c.json({ error: error instanceof Error ? error.message : String(error) }, 400);
|
|
337
|
+
}
|
|
338
|
+
});
|
|
189
339
|
app.get("/api/providers", async (c) => c.json(await runtime.providers.view()));
|
|
190
340
|
|
|
191
341
|
// 新建或改一个服务商;Key 可选,留空就沿用已存的
|
|
@@ -295,6 +445,34 @@ export function createApp(runtime: HobRuntime, options: { webDir?: string; home:
|
|
|
295
445
|
} catch (error) { return c.json({ ok: false, error: error instanceof Error ? error.message : String(error) }); }
|
|
296
446
|
});
|
|
297
447
|
|
|
448
|
+
app.get("/api/knowledge/embedding", (c) => c.json({ keyConfigured: Boolean(embeddingKey(options.home)) }));
|
|
449
|
+
|
|
450
|
+
app.put("/api/knowledge/embedding", async (c) => {
|
|
451
|
+
const body = await c.req.json().catch(() => ({})) as { revision?: number; embedding?: unknown; apiKey?: unknown };
|
|
452
|
+
if (typeof body.revision !== "number") return c.json({ error: "缺 revision" }, 400);
|
|
453
|
+
try {
|
|
454
|
+
const previous = embeddingModel(runtime.settings.settings.knowledge?.embedding ?? { source: "off" });
|
|
455
|
+
const settings = runtime.settings.setEmbedding(body.revision, body.embedding);
|
|
456
|
+
if (!settings) return c.json({ error: "设置已在别处修改,已刷新" }, 409);
|
|
457
|
+
const keyChanged = typeof body.apiKey === "string" && Boolean(body.apiKey.trim());
|
|
458
|
+
if (keyChanged) writeSecret(options.home, EMBEDDING_KEY_SECRET, String(body.apiKey).trim());
|
|
459
|
+
// 换模型要全部重算;只换 Key 时向量还能用,只重试之前失败的
|
|
460
|
+
if (previous !== embeddingModel(settings.settings.knowledge?.embedding ?? { source: "off" })) await runtime.knowledge.refreshIndexes(true);
|
|
461
|
+
else if (keyChanged) await runtime.knowledge.reindex();
|
|
462
|
+
return c.json({ settings, keyConfigured: Boolean(embeddingKey(options.home)) });
|
|
463
|
+
} catch (error) { return c.json({ error: error instanceof Error ? error.message : String(error) }, 400); }
|
|
464
|
+
});
|
|
465
|
+
|
|
466
|
+
app.post("/api/knowledge/embedding/test", async (c) => {
|
|
467
|
+
const body = await c.req.json().catch(() => ({})) as { embedding?: unknown; apiKey?: unknown };
|
|
468
|
+
try {
|
|
469
|
+
const config = body.embedding ? checkEmbeddingSettings(body.embedding) : runtime.settings.settings.knowledge?.embedding ?? { source: "off" };
|
|
470
|
+
const key = typeof body.apiKey === "string" && body.apiKey.trim() ? body.apiKey.trim() : embeddingKey(options.home);
|
|
471
|
+
const vectors = await embed(config, key, ["知识库连接测试"]);
|
|
472
|
+
return c.json({ ok: true, dims: vectors[0]!.length });
|
|
473
|
+
} catch (error) { return c.json({ ok: false, error: error instanceof Error ? error.message : String(error) }); }
|
|
474
|
+
});
|
|
475
|
+
|
|
298
476
|
// 设置 → 图像生成:接口的 Key 配没配(Key 永不回显)
|
|
299
477
|
app.get("/api/image", (c) => {
|
|
300
478
|
const view: ImageKeyView = { keyConfigured: Boolean(imageKey(options.home)) };
|
|
@@ -315,6 +493,7 @@ export function createApp(runtime: HobRuntime, options: { webDir?: string; home:
|
|
|
315
493
|
if (typeof body.apiKey === "string" && body.apiKey.trim()) {
|
|
316
494
|
writeSecret(options.home, IMAGE_KEY_SECRET, body.apiKey.trim());
|
|
317
495
|
}
|
|
496
|
+
router.releaseSessions();
|
|
318
497
|
const saved: ImageSettingsSaved = { settings, keyConfigured: Boolean(imageKey(options.home)) };
|
|
319
498
|
return c.json(saved);
|
|
320
499
|
});
|
|
@@ -417,6 +596,7 @@ export function createApp(runtime: HobRuntime, options: { webDir?: string; home:
|
|
|
417
596
|
const endpoint = typeof body.endpoint === "string" && body.endpoint ? body.endpoint : undefined;
|
|
418
597
|
const result = await runtime.push.test(endpoint);
|
|
419
598
|
if (endpoint && result.skipped === "no-devices") return c.json({ error: "这台设备尚未订阅推送" }, 404);
|
|
599
|
+
if (!result.sent && result.error) return c.json({ error: `发送失败:${result.error}` }, 502);
|
|
420
600
|
return c.json(result);
|
|
421
601
|
});
|
|
422
602
|
|
|
@@ -471,6 +651,42 @@ export function createApp(runtime: HobRuntime, options: { webDir?: string; home:
|
|
|
471
651
|
return c.json({ job, steps: jobs.transcript(job.id) });
|
|
472
652
|
});
|
|
473
653
|
|
|
654
|
+
app.get("/api/assistants/:id/skills", (c) => {
|
|
655
|
+
const id = c.req.param("id");
|
|
656
|
+
if (!store.assistant(id)) return c.json({ error: "助手不存在" }, 404);
|
|
657
|
+
try { return c.json(listAssistantSkills(options.home, id)); }
|
|
658
|
+
catch (error) { return c.json({ error: error instanceof Error ? error.message : String(error) }, 400); }
|
|
659
|
+
});
|
|
660
|
+
app.patch("/api/assistants/:id/skills/:name", async (c) => {
|
|
661
|
+
const id = c.req.param("id");
|
|
662
|
+
if (!store.assistant(id)) return c.json({ error: "助手不存在" }, 404);
|
|
663
|
+
const body = await c.req.json().catch(() => ({})) as { enabled?: unknown };
|
|
664
|
+
if (typeof body.enabled !== "boolean") return c.json({ error: "启用状态无效" }, 400);
|
|
665
|
+
try {
|
|
666
|
+
const skill = setAssistantSkillEnabled(options.home, id, c.req.param("name"), body.enabled);
|
|
667
|
+
bus.emit({ type: "skills", assistantId: id });
|
|
668
|
+
return c.json(skill);
|
|
669
|
+
} catch (error) { return c.json({ error: error instanceof Error ? error.message : String(error) }, 400); }
|
|
670
|
+
});
|
|
671
|
+
app.post("/api/assistants/:id/skills/:name/rollback", (c) => {
|
|
672
|
+
const id = c.req.param("id");
|
|
673
|
+
if (!store.assistant(id)) return c.json({ error: "助手不存在" }, 404);
|
|
674
|
+
try {
|
|
675
|
+
const skill = rollbackAssistantSkill(options.home, id, c.req.param("name"));
|
|
676
|
+
bus.emit({ type: "skills", assistantId: id });
|
|
677
|
+
return c.json(skill);
|
|
678
|
+
} catch (error) { return c.json({ error: error instanceof Error ? error.message : String(error) }, 400); }
|
|
679
|
+
});
|
|
680
|
+
app.delete("/api/assistants/:id/skills/:name", (c) => {
|
|
681
|
+
const id = c.req.param("id");
|
|
682
|
+
if (!store.assistant(id)) return c.json({ error: "助手不存在" }, 404);
|
|
683
|
+
try {
|
|
684
|
+
archiveAssistantSkill(options.home, id, c.req.param("name"));
|
|
685
|
+
bus.emit({ type: "skills", assistantId: id });
|
|
686
|
+
return c.json({ ok: true });
|
|
687
|
+
} catch (error) { return c.json({ error: error instanceof Error ? error.message : String(error) }, 400); }
|
|
688
|
+
});
|
|
689
|
+
|
|
474
690
|
app.get("/api/assistants/:id/items", (c) => {
|
|
475
691
|
const id = c.req.param("id");
|
|
476
692
|
if (!store.assistant(id)) return c.json({ error: "助手不存在" }, 404);
|
|
@@ -494,7 +710,8 @@ export function createApp(runtime: HobRuntime, options: { webDir?: string; home:
|
|
|
494
710
|
files?: string[];
|
|
495
711
|
};
|
|
496
712
|
const text = (body.text ?? "").trim();
|
|
497
|
-
|
|
713
|
+
// 和 Telegram 一样,只发图片 / 文件不写字也行
|
|
714
|
+
if (!text && !body.files?.length) return c.json({ error: "消息内容为空" }, 400);
|
|
498
715
|
const item = await router.send(id, {
|
|
499
716
|
text,
|
|
500
717
|
...(body.quote ? { quote: body.quote } : {}),
|
|
@@ -536,6 +753,11 @@ export function createApp(runtime: HobRuntime, options: { webDir?: string; home:
|
|
|
536
753
|
bus.emit({ type: "profile", assistantId: id });
|
|
537
754
|
return c.json({ ok: true, rule });
|
|
538
755
|
}
|
|
756
|
+
if (body.knowledge !== undefined) {
|
|
757
|
+
if (!Array.isArray(body.knowledge) || body.knowledge.some((id) => typeof id !== "string")) return c.json({ error: "知识库绑定无效。" }, 400);
|
|
758
|
+
const collections = (await runtime.knowledge.list()).collections;
|
|
759
|
+
if (body.knowledge.some((id) => !collections.some((item) => item.id === id))) return c.json({ error: "知识库不存在。" }, 400);
|
|
760
|
+
}
|
|
539
761
|
if (typeof body.sprite === "string" && body.sprite !== assistant.sprite) {
|
|
540
762
|
const parsed = parseSprite(body.sprite);
|
|
541
763
|
// custom 要有画法数据才收(画法在 prop 字段上,PATCH 不改它);否则渲染成空手火苗,不如拒绝
|
|
@@ -565,18 +787,19 @@ export function createApp(runtime: HobRuntime, options: { webDir?: string; home:
|
|
|
565
787
|
...(models ? { models } : {}),
|
|
566
788
|
...(abilities ? { abilities } : {}),
|
|
567
789
|
...(typeof body.instructions === "string" ? { instructions: body.instructions } : {}),
|
|
790
|
+
...(body.knowledge !== undefined ? { knowledge: [...new Set(body.knowledge)] } : {}),
|
|
568
791
|
};
|
|
569
792
|
store.saveAssistant(next);
|
|
570
|
-
//
|
|
571
|
-
if (models) router.releaseSessions();
|
|
793
|
+
// 工具说明和注册取决于模型及助手能力,变化后重开会话
|
|
794
|
+
if (models || abilities) router.releaseSessions();
|
|
572
795
|
const summary = router.summary(next);
|
|
573
796
|
bus.emit({ type: "assistant", assistant: summary });
|
|
574
797
|
return c.json({ assistant: summary });
|
|
575
798
|
});
|
|
576
799
|
|
|
577
|
-
//
|
|
578
|
-
app.post("/api/assistants/:id/
|
|
579
|
-
const result = await
|
|
800
|
+
// 归档助手:档案、对话、规则和定时进归档,助手从列表移除
|
|
801
|
+
app.post("/api/assistants/:id/archive", async (c) => {
|
|
802
|
+
const result = await archiveAssistant(runtime, c.req.param("id"));
|
|
580
803
|
return c.json(result.body as Record<string, unknown>, result.status as 200);
|
|
581
804
|
});
|
|
582
805
|
|
|
@@ -648,6 +871,7 @@ export function createApp(runtime: HobRuntime, options: { webDir?: string; home:
|
|
|
648
871
|
instruction?: string;
|
|
649
872
|
schedules?: string[];
|
|
650
873
|
active?: boolean;
|
|
874
|
+
skill?: string | null;
|
|
651
875
|
};
|
|
652
876
|
try {
|
|
653
877
|
const routine = routines.update(c.req.param("id"), body);
|
|
@@ -676,11 +900,18 @@ export function createApp(runtime: HobRuntime, options: { webDir?: string; home:
|
|
|
676
900
|
}
|
|
677
901
|
});
|
|
678
902
|
|
|
903
|
+
app.post("/api/jobs/:id/sediment", (c) => {
|
|
904
|
+
const id = c.req.param("id");
|
|
905
|
+
if (!store.job(id)) return c.json({ error: "任务不存在" }, 404);
|
|
906
|
+
try { jobs.startSediment(id, "manual"); return c.json({ ok: true }); }
|
|
907
|
+
catch (error) { return c.json({ error: error instanceof Error ? error.message : String(error) }, 409); }
|
|
908
|
+
});
|
|
909
|
+
|
|
679
910
|
app.post("/api/jobs/:id/reply", async (c) => {
|
|
680
911
|
const body = (await c.req.json().catch(() => ({}))) as { text?: string };
|
|
681
912
|
if (!body.text?.trim()) return c.json({ error: "消息是空的" }, 400);
|
|
682
913
|
try {
|
|
683
|
-
await jobs.reply(c.req.param("id"), body.text.trim());
|
|
914
|
+
await jobs.reply(c.req.param("id"), body.text.trim(), "user");
|
|
684
915
|
return c.json({ ok: true });
|
|
685
916
|
} catch (error) {
|
|
686
917
|
return c.json({ error: error instanceof Error ? error.message : String(error) }, 409);
|
|
@@ -692,6 +923,85 @@ export function createApp(runtime: HobRuntime, options: { webDir?: string; home:
|
|
|
692
923
|
return c.json({ ok: true });
|
|
693
924
|
});
|
|
694
925
|
|
|
926
|
+
app.get("/api/knowledge", async (c) => {
|
|
927
|
+
const limit = Number(c.req.query("limit"));
|
|
928
|
+
return c.json(await runtime.knowledge.list({ origin: c.req.query("origin"), collection: c.req.query("collection"), kind: c.req.query("kind"), assistantId: c.req.query("assistantId"), q: c.req.query("q"), before: c.req.query("before"), ...(Number.isFinite(limit) && limit > 0 ? { limit: Math.floor(limit) } : {}) }));
|
|
929
|
+
});
|
|
930
|
+
|
|
931
|
+
app.post("/api/knowledge/collections", async (c) => {
|
|
932
|
+
const body = await c.req.json().catch(() => ({})) as { name?: unknown };
|
|
933
|
+
if (typeof body.name !== "string" || !body.name.trim()) return c.json({ error: "名称不能为空" }, 400);
|
|
934
|
+
const collection = await runtime.knowledge.createCollection(body.name.trim());
|
|
935
|
+
return c.json({ collection: { ...collection, count: 0 } });
|
|
936
|
+
});
|
|
937
|
+
|
|
938
|
+
app.patch("/api/knowledge/collections/:id", async (c) => {
|
|
939
|
+
const body = await c.req.json().catch(() => ({})) as { name?: unknown };
|
|
940
|
+
if (typeof body.name !== "string" || !body.name.trim()) return c.json({ error: "名称不能为空" }, 400);
|
|
941
|
+
const collection = await runtime.knowledge.renameCollection(c.req.param("id"), body.name.trim());
|
|
942
|
+
if (!collection) return c.json({ error: "知识库不存在" }, 404);
|
|
943
|
+
const count = (await runtime.knowledge.list()).collections.find((item) => item.id === collection.id)?.count ?? 0;
|
|
944
|
+
return c.json({ collection: { ...collection, count } });
|
|
945
|
+
});
|
|
946
|
+
|
|
947
|
+
app.delete("/api/knowledge/collections/:id", async (c) => {
|
|
948
|
+
const deleted = await runtime.knowledge.deleteCollection(c.req.param("id"));
|
|
949
|
+
if (deleted === undefined) return c.json({ error: "知识库不存在" }, 404);
|
|
950
|
+
return c.json({ deleted });
|
|
951
|
+
});
|
|
952
|
+
|
|
953
|
+
app.get("/api/knowledge/entries/:id", async (c) => {
|
|
954
|
+
const raw = c.req.query("chunk");
|
|
955
|
+
if (raw !== undefined && !/^\d+$/.test(raw)) return c.json({ error: "段号无效" }, 400);
|
|
956
|
+
const view = await runtime.knowledge.entry(c.req.param("id"), raw === undefined ? undefined : Number(raw));
|
|
957
|
+
return view ? c.json(view) : c.json({ error: "条目不存在" }, 404);
|
|
958
|
+
});
|
|
959
|
+
|
|
960
|
+
app.post("/api/knowledge/collections/:id/reindex", async (c) => {
|
|
961
|
+
return await runtime.knowledge.reindex(c.req.param("id")) ? c.json({ ok: true }) : c.json({ error: "知识库不存在" }, 404);
|
|
962
|
+
});
|
|
963
|
+
|
|
964
|
+
app.patch("/api/knowledge/entries/:id", async (c) => {
|
|
965
|
+
const body = await c.req.json().catch(() => ({})) as { name?: unknown; collections?: unknown };
|
|
966
|
+
if ((body.name !== undefined && (typeof body.name !== "string" || !body.name.trim())) || (body.collections !== undefined && (!Array.isArray(body.collections) || !body.collections.every((id) => typeof id === "string")))) return c.json({ error: "条目参数无效" }, 400);
|
|
967
|
+
try {
|
|
968
|
+
const entry = await runtime.knowledge.updateEntry(c.req.param("id"), { ...(typeof body.name === "string" ? { name: body.name.trim() } : {}), ...(Array.isArray(body.collections) ? { collections: body.collections as string[] } : {}) });
|
|
969
|
+
return entry ? c.json({ entry }) : c.json({ error: "条目不存在" }, 404);
|
|
970
|
+
} catch (error) {
|
|
971
|
+
if (error instanceof Error && error.message === "知识库不存在") return c.json({ error: error.message }, 404);
|
|
972
|
+
throw error;
|
|
973
|
+
}
|
|
974
|
+
});
|
|
975
|
+
|
|
976
|
+
app.delete("/api/knowledge/entries/:id", async (c) => {
|
|
977
|
+
return await runtime.knowledge.deleteEntry(c.req.param("id")) ? c.json({ ok: true }) : c.json({ error: "条目不存在" }, 404);
|
|
978
|
+
});
|
|
979
|
+
|
|
980
|
+
app.post("/api/knowledge/upload", async (c) => {
|
|
981
|
+
const collectionId = c.req.query("collection");
|
|
982
|
+
if (!collectionId || !(await runtime.knowledge.list()).collections.some((item) => item.id === collectionId)) return c.json({ error: "知识库不存在" }, 404);
|
|
983
|
+
const body = await c.req.parseBody({ all: true });
|
|
984
|
+
const files = Object.values(body).flatMap((value) => Array.isArray(value) ? value : [value]).filter((value): value is File => value instanceof File);
|
|
985
|
+
if (!files.length) return c.json({ error: "没有收到文件" }, 400);
|
|
986
|
+
if (files.some((file) => file.size > 100 * 1024 * 1024)) return c.json({ error: "文件超过 100 MB" }, 400);
|
|
987
|
+
const dir = join(options.home, "knowledge", "incoming");
|
|
988
|
+
mkdirSync(dir, { recursive: true });
|
|
989
|
+
const entries = [];
|
|
990
|
+
for (const file of files) {
|
|
991
|
+
const name = file.name.replace(/[/\\\\]/g, "_") || "file";
|
|
992
|
+
const path = join(dir, `${newId("u-")}-${name}`);
|
|
993
|
+
try {
|
|
994
|
+
writeFileSync(path, Buffer.from(await file.arrayBuffer()), { mode: 0o600 });
|
|
995
|
+
const entry = await runtime.knowledge.upload({ path, name }, collectionId);
|
|
996
|
+
if (entry) entries.push(entry);
|
|
997
|
+
} catch (error) {
|
|
998
|
+
if (error instanceof Error && error.message === "知识库不存在") return c.json({ error: error.message }, 404);
|
|
999
|
+
throw error;
|
|
1000
|
+
} finally { rmSync(path, { force: true }); }
|
|
1001
|
+
}
|
|
1002
|
+
return c.json({ entries });
|
|
1003
|
+
});
|
|
1004
|
+
|
|
695
1005
|
app.post("/api/uploads", async (c) => {
|
|
696
1006
|
const body = await c.req.parseBody({ all: true });
|
|
697
1007
|
const uploads = join(options.home, "uploads");
|
|
@@ -709,9 +1019,9 @@ export function createApp(runtime: HobRuntime, options: { webDir?: string; home:
|
|
|
709
1019
|
return c.json({ files: saved.map((item) => item.path), file: saved[0] });
|
|
710
1020
|
});
|
|
711
1021
|
|
|
712
|
-
app.get("/api/files", (c) => {
|
|
1022
|
+
app.get("/api/files", async (c) => {
|
|
713
1023
|
const requested = c.req.query("path");
|
|
714
|
-
const lookup = requested ? lookupFile(options.home, requested) : null;
|
|
1024
|
+
const lookup = requested ? lookupFile(options.home, requested, workDirs()) : null;
|
|
715
1025
|
const nosniff = { "x-content-type-options": "nosniff" };
|
|
716
1026
|
if (!lookup) return c.json({ error: "缺 path" }, 400, nosniff);
|
|
717
1027
|
if (!lookup.ok) return c.json({ error: lookup.error }, lookup.status, nosniff);
|
|
@@ -728,6 +1038,11 @@ export function createApp(runtime: HobRuntime, options: { webDir?: string; home:
|
|
|
728
1038
|
const name = encodeURIComponent(info.name);
|
|
729
1039
|
headers["content-disposition"] = `${c.req.query("inline") === "1" ? "inline" : "attachment"}; filename*=UTF-8''${name}`;
|
|
730
1040
|
}
|
|
1041
|
+
// 对话里的图片只要缩略图(?thumb=1);缩不了 / 不值得缩就给原图
|
|
1042
|
+
if (c.req.query("thumb") === "1" && info.kind === "image") {
|
|
1043
|
+
const thumb = await thumbnail(options.home, info);
|
|
1044
|
+
if (thumb) return c.body(new Uint8Array(thumb.data), 200, { ...nosniff, "content-type": thumb.mime, "cache-control": "private, max-age=604800" });
|
|
1045
|
+
}
|
|
731
1046
|
// 文本只拉 1 MB:预览面板对超大文件只显示前 1 MB,剩下的走下载
|
|
732
1047
|
const whole = readFileSync(info.path);
|
|
733
1048
|
const body = info.text && c.req.query("truncate") === "1" ? whole.subarray(0, MAX_TEXT_BYTES) : whole;
|
|
@@ -738,7 +1053,7 @@ export function createApp(runtime: HobRuntime, options: { webDir?: string; home:
|
|
|
738
1053
|
app.get("/api/files/meta", (c) => {
|
|
739
1054
|
const requested = c.req.query("path");
|
|
740
1055
|
if (!requested) return c.json({ error: "缺 path" }, 400);
|
|
741
|
-
const lookup = lookupFile(options.home, requested);
|
|
1056
|
+
const lookup = lookupFile(options.home, requested, workDirs());
|
|
742
1057
|
if (!lookup.ok) return c.json({ error: lookup.error }, lookup.status);
|
|
743
1058
|
const { info } = lookup;
|
|
744
1059
|
const meta: FileMeta = {
|
|
@@ -754,11 +1069,15 @@ export function createApp(runtime: HobRuntime, options: { webDir?: string; home:
|
|
|
754
1069
|
return c.json(meta);
|
|
755
1070
|
});
|
|
756
1071
|
|
|
757
|
-
//
|
|
1072
|
+
// 任务工作目录,一次只列一层。
|
|
758
1073
|
app.get("/api/jobs/:id/files", (c) => {
|
|
759
1074
|
const job = store.job(c.req.param("id"));
|
|
760
1075
|
if (!job) return c.json({ error: "任务不存在" }, 404);
|
|
761
|
-
|
|
1076
|
+
try {
|
|
1077
|
+
return c.json(listJobFiles(job.cwd ?? store.workDir(job.id), c.req.query("path")));
|
|
1078
|
+
} catch (error) {
|
|
1079
|
+
return c.json({ error: error instanceof Error ? error.message : "无法读取工作目录" }, 400);
|
|
1080
|
+
}
|
|
762
1081
|
});
|
|
763
1082
|
|
|
764
1083
|
app.get("/api/events", (c) => {
|