hob-bot 1.0.4 → 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/package.json +7 -1
- package/server/src/actions.ts +16 -56
- 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 +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/jobs.ts +280 -41
- 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 -156
- package/server/src/providers.ts +3 -2
- package/server/src/push.ts +39 -5
- package/server/src/router-activity.ts +4 -0
- package/server/src/router-tools.ts +132 -38
- package/server/src/router.ts +153 -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 +103 -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/files.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// /api/files 的取文件规则:路径先解开符号链接,再判断能不能给页面看。
|
|
2
|
-
import { closeSync, openSync, readSync, readdirSync, realpathSync, statSync } from "node:fs";
|
|
3
|
-
import { basename, join, relative, resolve, sep } from "node:path";
|
|
2
|
+
import { closeSync, existsSync, lstatSync, openSync, readSync, readdirSync, realpathSync, statSync } from "node:fs";
|
|
3
|
+
import { basename, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
4
4
|
import { fileKind, isTextKind, mimeOf } from "../../shared/files.ts";
|
|
5
5
|
import type { FileKind, JobFileEntry, JobFilesView } from "../../shared/types.ts";
|
|
6
6
|
|
|
@@ -11,13 +11,15 @@ const DENY_ROOT_DIRS = new Set(["engine", "lore"]);
|
|
|
11
11
|
/** 备份文件一律不给(凭据的备份也在里面)。 */
|
|
12
12
|
const DENY_ANY_FILE = /\.bak/i;
|
|
13
13
|
|
|
14
|
+
/** 任务工作目录里不对页面开放的:密钥目录、git 内部、.env、私钥。 */
|
|
15
|
+
const DENY_WORK_DIRS = /(^|[/\\])\.(ssh|gnupg|aws|git)([/\\]|$)/;
|
|
16
|
+
const DENY_WORK_FILE = /^(\.env.*|id_[a-z0-9]+|.*\.(pem|key))$/i;
|
|
17
|
+
|
|
14
18
|
/** 预览面板最多拉 1 MB 文本。 */
|
|
15
19
|
export const MAX_TEXT_BYTES = 1024 * 1024;
|
|
16
20
|
|
|
17
21
|
/** 任务工作目录里最多列 200 个文件。 */
|
|
18
22
|
const MAX_JOB_FILES = 200;
|
|
19
|
-
/** 递归太深的目录不进去(产出文件不该有这么深)。 */
|
|
20
|
-
const MAX_DEPTH = 8;
|
|
21
23
|
const SKIP_DIRS = new Set(["node_modules", ".git"]);
|
|
22
24
|
|
|
23
25
|
export interface FileInfo {
|
|
@@ -68,10 +70,11 @@ function headOf(path: string): Uint8Array {
|
|
|
68
70
|
}
|
|
69
71
|
|
|
70
72
|
/**
|
|
71
|
-
* 解析一个路径:realpath
|
|
73
|
+
* 解析一个路径:realpath 后必须在数据目录或某个任务的工作目录里(符号链接指出去就是 403),
|
|
72
74
|
* 凭据和内部状态文件不开放,不是普通文件当 404。
|
|
75
|
+
* 包含数据目录的工作目录(家目录、根目录这类)太宽,不算数。
|
|
73
76
|
*/
|
|
74
|
-
export function lookupFile(home: string, requested: string): Lookup {
|
|
77
|
+
export function lookupFile(home: string, requested: string, workDirs: string[] = []): Lookup {
|
|
75
78
|
const root = realpathSync(resolve(home));
|
|
76
79
|
let path: string;
|
|
77
80
|
let size: number;
|
|
@@ -85,58 +88,75 @@ export function lookupFile(home: string, requested: string): Lookup {
|
|
|
85
88
|
} catch {
|
|
86
89
|
return { ok: false, status: 404, error: "文件不存在" };
|
|
87
90
|
}
|
|
88
|
-
|
|
89
|
-
|
|
91
|
+
const inHome = path.startsWith(`${root}${sep}`);
|
|
92
|
+
const work = inHome ? undefined : workRoots(root, workDirs).find((dir) => path.startsWith(`${dir}${sep}`));
|
|
93
|
+
if (!inHome && !work) return { ok: false, status: 403, error: "只允许读 Hob 自己的文件" };
|
|
94
|
+
const relativePath = relative(inHome ? root : work!, path);
|
|
95
|
+
if ((inHome && denied(root, path)) || DENY_WORK_DIRS.test(relativePath) || DENY_WORK_FILE.test(basename(path)) || DENY_ANY_FILE.test(basename(path))) {
|
|
96
|
+
return { ok: false, status: 403, error: "这个文件不对页面开放" };
|
|
97
|
+
}
|
|
90
98
|
const name = basename(path);
|
|
91
99
|
const kind = fileKind(name) ?? (looksText(headOf(path)) ? "text" : "other");
|
|
92
100
|
const text = isTextKind(kind);
|
|
93
101
|
return {
|
|
94
102
|
ok: true,
|
|
95
|
-
info: { path, name, size, modifiedAt, kind, text, mime: mimeOf(name), ...(/^pages[/\\][0-9a-f-]{36}\.html$/i.test(relative(root, path)) ? { page: true } : {}) },
|
|
103
|
+
info: { path, name, size, modifiedAt, kind, text, mime: mimeOf(name), ...(/^(?:pages[/\\][0-9a-f-]{36}|knowledge[/\\]pages[/\\][0-9a-f]{64})\.html$/i.test(relative(root, path)) ? { page: true } : {}) },
|
|
96
104
|
};
|
|
97
105
|
}
|
|
98
106
|
|
|
99
|
-
function
|
|
100
|
-
|
|
107
|
+
function workRoots(root: string, dirs: string[]): string[] {
|
|
108
|
+
const out: string[] = [];
|
|
109
|
+
for (const dir of new Set(dirs)) {
|
|
110
|
+
try {
|
|
111
|
+
const real = realpathSync(resolve(dir));
|
|
112
|
+
if (!`${root}${sep}`.startsWith(`${real === sep ? "" : real}${sep}`)) out.push(real);
|
|
113
|
+
} catch {
|
|
114
|
+
// 目录已经删了:不算
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
return out;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function visibleEntries(dir: string) {
|
|
121
|
+
return readdirSync(dir, { withFileTypes: true }).filter((entry) =>
|
|
122
|
+
!entry.isSymbolicLink() && (entry.isFile() || (entry.isDirectory() && !SKIP_DIRS.has(entry.name))));
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** 一次只列一层;拒绝越界和符号链接。工作目录还没建出来时当作空的。 */
|
|
126
|
+
export function listJobFiles(workDir: string, subdir = ""): JobFilesView {
|
|
127
|
+
const parts = subdir.split(/[\\/]/).filter(Boolean);
|
|
128
|
+
if (isAbsolute(subdir) || parts.some((part) => part === ".." || SKIP_DIRS.has(part))) throw new Error("工作目录路径无效");
|
|
129
|
+
const base = resolve(workDir);
|
|
130
|
+
if (!parts.length && !existsSync(base)) return { files: [], total: 0, fileCount: 0, directoryCount: 0 };
|
|
131
|
+
let dir = base;
|
|
101
132
|
let entries;
|
|
102
133
|
try {
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
134
|
+
for (const part of parts) {
|
|
135
|
+
dir = join(dir, part);
|
|
136
|
+
const info = lstatSync(dir);
|
|
137
|
+
if (info.isSymbolicLink() || !info.isDirectory()) throw new Error("不是文件夹");
|
|
138
|
+
}
|
|
139
|
+
entries = visibleEntries(dir);
|
|
140
|
+
} catch (error) {
|
|
141
|
+
const code = (error as NodeJS.ErrnoException).code;
|
|
142
|
+
throw new Error(code === "ENOENT" ? "这个文件夹已经不在了" : code === "EACCES" ? "没有权限读取这个文件夹" : "工作目录路径无效");
|
|
106
143
|
}
|
|
144
|
+
const all: JobFileEntry[] = [];
|
|
107
145
|
for (const entry of entries) {
|
|
108
|
-
// 符号链接不跟:可能指到目录外面,也可能绕成环
|
|
109
|
-
if (entry.isSymbolicLink()) continue;
|
|
110
146
|
const path = join(dir, entry.name);
|
|
111
|
-
|
|
112
|
-
if (SKIP_DIRS.has(entry.name)) continue;
|
|
113
|
-
if (walk(path, base, depth + 1, out)) return true;
|
|
114
|
-
continue;
|
|
115
|
-
}
|
|
116
|
-
if (!entry.isFile()) continue;
|
|
117
|
-
let stats;
|
|
147
|
+
const common = { path, rel: relative(base, path), name: entry.name };
|
|
118
148
|
try {
|
|
119
|
-
|
|
149
|
+
if (entry.isDirectory()) {
|
|
150
|
+
all.push({ ...common, type: "directory", count: visibleEntries(path).length });
|
|
151
|
+
} else {
|
|
152
|
+
const stats = statSync(path);
|
|
153
|
+
all.push({ ...common, type: "file", size: stats.size, modifiedAt: stats.mtime.toISOString(), kind: fileKind(entry.name) ?? (looksText(headOf(path)) ? "text" : "other") });
|
|
154
|
+
}
|
|
120
155
|
} catch {
|
|
121
|
-
|
|
156
|
+
// 列的时候刚被删掉或读不了(在跑的任务常有临时文件):跳过这一项
|
|
122
157
|
}
|
|
123
|
-
out.push({
|
|
124
|
-
path,
|
|
125
|
-
rel: relative(base, path),
|
|
126
|
-
name: entry.name,
|
|
127
|
-
size: stats.size,
|
|
128
|
-
modifiedAt: stats.mtime.toISOString(),
|
|
129
|
-
kind: fileKind(entry.name) ?? (looksText(headOf(path)) ? "text" : "other"),
|
|
130
|
-
});
|
|
131
|
-
if (out.length >= MAX_JOB_FILES + 1) return true;
|
|
132
158
|
}
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
/** 任务工作目录里的文件:递归、修改时间倒序、最多 200 个,跳过 node_modules 和 .git。 */
|
|
137
|
-
export function listJobFiles(workDir: string): JobFilesView {
|
|
138
|
-
const all: JobFileEntry[] = [];
|
|
139
|
-
walk(workDir, workDir, 0, all);
|
|
140
|
-
all.sort((a, b) => b.modifiedAt.localeCompare(a.modifiedAt));
|
|
141
|
-
return { files: all.slice(0, MAX_JOB_FILES), hasMore: all.length > MAX_JOB_FILES };
|
|
159
|
+
all.sort((a, b) => a.type !== b.type ? (a.type === "directory" ? -1 : 1) : a.type === "file" && b.type === "file" ? b.modifiedAt.localeCompare(a.modifiedAt) || a.name.localeCompare(b.name) : a.name.localeCompare(b.name));
|
|
160
|
+
const directoryCount = all.filter((entry) => entry.type === "directory").length;
|
|
161
|
+
return { files: all.slice(0, MAX_JOB_FILES), total: all.length, directoryCount, fileCount: all.length - directoryCount };
|
|
142
162
|
}
|
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,13 +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";
|
|
29
33
|
import { thumbnail } from "./thumbs.ts";
|
|
30
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";
|
|
31
37
|
import { checkImageSettings, generateImage, imageConfigured, imageKey, IMAGE_KEY_SECRET } from "./image.ts";
|
|
38
|
+
import { checkEmbeddingSettings, embed, embeddingKey, embeddingModel, EMBEDDING_KEY_SECRET } from "./embedding.ts";
|
|
32
39
|
import { checkAssistantModels, checkModelsPatch } from "./models.ts";
|
|
33
40
|
import { saveLoreConnection, testLoreConnection } from "./memory.ts";
|
|
34
41
|
import { verifyPassword } from "./password.ts";
|
|
@@ -38,6 +45,7 @@ import { SEARCH_PROVIDERS } from "../../shared/search.ts";
|
|
|
38
45
|
import { WebSearch } from "./search.ts";
|
|
39
46
|
import { clampDuty } from "./text.ts";
|
|
40
47
|
import { deleteOwnSkill, importSkills, listImportSkills, skillViews } from "./skills.ts";
|
|
48
|
+
import { archiveAssistantSkill, listAssistantSkills, rollbackAssistantSkill, setAssistantSkillEnabled } from "./assistant-skills.ts";
|
|
41
49
|
import type { SkillImportSource } from "../../shared/skills.ts";
|
|
42
50
|
|
|
43
51
|
const COOKIE = "hob";
|
|
@@ -53,7 +61,7 @@ function issueCookie(config: HobRuntime["config"]): string {
|
|
|
53
61
|
return `${token}.${signature(config.cookieSecret, token)}`;
|
|
54
62
|
}
|
|
55
63
|
|
|
56
|
-
function validCookie(config: HobRuntime["config"], value: string | undefined): boolean {
|
|
64
|
+
export function validCookie(config: HobRuntime["config"], value: string | undefined): boolean {
|
|
57
65
|
if (!value) return false;
|
|
58
66
|
const dot = value.lastIndexOf(".");
|
|
59
67
|
if (dot <= 0) return false;
|
|
@@ -70,6 +78,8 @@ function validCookie(config: HobRuntime["config"], value: string | undefined): b
|
|
|
70
78
|
|
|
71
79
|
export function createApp(runtime: HobRuntime, options: { webDir?: string; home: string }): Hono {
|
|
72
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] : []));
|
|
73
83
|
|
|
74
84
|
app.use("/api/*", async (c, next) => {
|
|
75
85
|
if (c.req.path === "/api/login") return next();
|
|
@@ -132,6 +142,66 @@ export function createApp(runtime: HobRuntime, options: { webDir?: string; home:
|
|
|
132
142
|
return c.json(next);
|
|
133
143
|
});
|
|
134
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
|
+
|
|
135
205
|
app.get("/api/skills", async (c) => {
|
|
136
206
|
const memory = runtime.settings.memory();
|
|
137
207
|
try { if (memory.provider === "lore" && !runtime.pi.loreSkillPaths.length) await runtime.pi.discoverLoreSkills(); }
|
|
@@ -153,6 +223,67 @@ export function createApp(runtime: HobRuntime, options: { webDir?: string; home:
|
|
|
153
223
|
catch (error) { return c.json({ error: error instanceof Error ? error.message : String(error) }, 400); }
|
|
154
224
|
});
|
|
155
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
|
+
|
|
156
287
|
// 设置 → 模型:引擎注册表里现在能用的聊天模型 + 名单里哪些已经不可用
|
|
157
288
|
app.get("/api/models", async (c) => {
|
|
158
289
|
const models = await runtime.models.get();
|
|
@@ -187,6 +318,24 @@ export function createApp(runtime: HobRuntime, options: { webDir?: string; home:
|
|
|
187
318
|
});
|
|
188
319
|
|
|
189
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
|
+
});
|
|
190
339
|
app.get("/api/providers", async (c) => c.json(await runtime.providers.view()));
|
|
191
340
|
|
|
192
341
|
// 新建或改一个服务商;Key 可选,留空就沿用已存的
|
|
@@ -296,6 +445,34 @@ export function createApp(runtime: HobRuntime, options: { webDir?: string; home:
|
|
|
296
445
|
} catch (error) { return c.json({ ok: false, error: error instanceof Error ? error.message : String(error) }); }
|
|
297
446
|
});
|
|
298
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
|
+
|
|
299
476
|
// 设置 → 图像生成:接口的 Key 配没配(Key 永不回显)
|
|
300
477
|
app.get("/api/image", (c) => {
|
|
301
478
|
const view: ImageKeyView = { keyConfigured: Boolean(imageKey(options.home)) };
|
|
@@ -316,6 +493,7 @@ export function createApp(runtime: HobRuntime, options: { webDir?: string; home:
|
|
|
316
493
|
if (typeof body.apiKey === "string" && body.apiKey.trim()) {
|
|
317
494
|
writeSecret(options.home, IMAGE_KEY_SECRET, body.apiKey.trim());
|
|
318
495
|
}
|
|
496
|
+
router.releaseSessions();
|
|
319
497
|
const saved: ImageSettingsSaved = { settings, keyConfigured: Boolean(imageKey(options.home)) };
|
|
320
498
|
return c.json(saved);
|
|
321
499
|
});
|
|
@@ -418,6 +596,7 @@ export function createApp(runtime: HobRuntime, options: { webDir?: string; home:
|
|
|
418
596
|
const endpoint = typeof body.endpoint === "string" && body.endpoint ? body.endpoint : undefined;
|
|
419
597
|
const result = await runtime.push.test(endpoint);
|
|
420
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);
|
|
421
600
|
return c.json(result);
|
|
422
601
|
});
|
|
423
602
|
|
|
@@ -472,6 +651,42 @@ export function createApp(runtime: HobRuntime, options: { webDir?: string; home:
|
|
|
472
651
|
return c.json({ job, steps: jobs.transcript(job.id) });
|
|
473
652
|
});
|
|
474
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
|
+
|
|
475
690
|
app.get("/api/assistants/:id/items", (c) => {
|
|
476
691
|
const id = c.req.param("id");
|
|
477
692
|
if (!store.assistant(id)) return c.json({ error: "助手不存在" }, 404);
|
|
@@ -538,6 +753,11 @@ export function createApp(runtime: HobRuntime, options: { webDir?: string; home:
|
|
|
538
753
|
bus.emit({ type: "profile", assistantId: id });
|
|
539
754
|
return c.json({ ok: true, rule });
|
|
540
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
|
+
}
|
|
541
761
|
if (typeof body.sprite === "string" && body.sprite !== assistant.sprite) {
|
|
542
762
|
const parsed = parseSprite(body.sprite);
|
|
543
763
|
// custom 要有画法数据才收(画法在 prop 字段上,PATCH 不改它);否则渲染成空手火苗,不如拒绝
|
|
@@ -567,18 +787,19 @@ export function createApp(runtime: HobRuntime, options: { webDir?: string; home:
|
|
|
567
787
|
...(models ? { models } : {}),
|
|
568
788
|
...(abilities ? { abilities } : {}),
|
|
569
789
|
...(typeof body.instructions === "string" ? { instructions: body.instructions } : {}),
|
|
790
|
+
...(body.knowledge !== undefined ? { knowledge: [...new Set(body.knowledge)] } : {}),
|
|
570
791
|
};
|
|
571
792
|
store.saveAssistant(next);
|
|
572
|
-
//
|
|
573
|
-
if (models) router.releaseSessions();
|
|
793
|
+
// 工具说明和注册取决于模型及助手能力,变化后重开会话
|
|
794
|
+
if (models || abilities) router.releaseSessions();
|
|
574
795
|
const summary = router.summary(next);
|
|
575
796
|
bus.emit({ type: "assistant", assistant: summary });
|
|
576
797
|
return c.json({ assistant: summary });
|
|
577
798
|
});
|
|
578
799
|
|
|
579
|
-
//
|
|
580
|
-
app.post("/api/assistants/:id/
|
|
581
|
-
const result = await
|
|
800
|
+
// 归档助手:档案、对话、规则和定时进归档,助手从列表移除
|
|
801
|
+
app.post("/api/assistants/:id/archive", async (c) => {
|
|
802
|
+
const result = await archiveAssistant(runtime, c.req.param("id"));
|
|
582
803
|
return c.json(result.body as Record<string, unknown>, result.status as 200);
|
|
583
804
|
});
|
|
584
805
|
|
|
@@ -650,6 +871,7 @@ export function createApp(runtime: HobRuntime, options: { webDir?: string; home:
|
|
|
650
871
|
instruction?: string;
|
|
651
872
|
schedules?: string[];
|
|
652
873
|
active?: boolean;
|
|
874
|
+
skill?: string | null;
|
|
653
875
|
};
|
|
654
876
|
try {
|
|
655
877
|
const routine = routines.update(c.req.param("id"), body);
|
|
@@ -678,11 +900,18 @@ export function createApp(runtime: HobRuntime, options: { webDir?: string; home:
|
|
|
678
900
|
}
|
|
679
901
|
});
|
|
680
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
|
+
|
|
681
910
|
app.post("/api/jobs/:id/reply", async (c) => {
|
|
682
911
|
const body = (await c.req.json().catch(() => ({}))) as { text?: string };
|
|
683
912
|
if (!body.text?.trim()) return c.json({ error: "消息是空的" }, 400);
|
|
684
913
|
try {
|
|
685
|
-
await jobs.reply(c.req.param("id"), body.text.trim());
|
|
914
|
+
await jobs.reply(c.req.param("id"), body.text.trim(), "user");
|
|
686
915
|
return c.json({ ok: true });
|
|
687
916
|
} catch (error) {
|
|
688
917
|
return c.json({ error: error instanceof Error ? error.message : String(error) }, 409);
|
|
@@ -694,6 +923,85 @@ export function createApp(runtime: HobRuntime, options: { webDir?: string; home:
|
|
|
694
923
|
return c.json({ ok: true });
|
|
695
924
|
});
|
|
696
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
|
+
|
|
697
1005
|
app.post("/api/uploads", async (c) => {
|
|
698
1006
|
const body = await c.req.parseBody({ all: true });
|
|
699
1007
|
const uploads = join(options.home, "uploads");
|
|
@@ -713,7 +1021,7 @@ export function createApp(runtime: HobRuntime, options: { webDir?: string; home:
|
|
|
713
1021
|
|
|
714
1022
|
app.get("/api/files", async (c) => {
|
|
715
1023
|
const requested = c.req.query("path");
|
|
716
|
-
const lookup = requested ? lookupFile(options.home, requested) : null;
|
|
1024
|
+
const lookup = requested ? lookupFile(options.home, requested, workDirs()) : null;
|
|
717
1025
|
const nosniff = { "x-content-type-options": "nosniff" };
|
|
718
1026
|
if (!lookup) return c.json({ error: "缺 path" }, 400, nosniff);
|
|
719
1027
|
if (!lookup.ok) return c.json({ error: lookup.error }, lookup.status, nosniff);
|
|
@@ -745,7 +1053,7 @@ export function createApp(runtime: HobRuntime, options: { webDir?: string; home:
|
|
|
745
1053
|
app.get("/api/files/meta", (c) => {
|
|
746
1054
|
const requested = c.req.query("path");
|
|
747
1055
|
if (!requested) return c.json({ error: "缺 path" }, 400);
|
|
748
|
-
const lookup = lookupFile(options.home, requested);
|
|
1056
|
+
const lookup = lookupFile(options.home, requested, workDirs());
|
|
749
1057
|
if (!lookup.ok) return c.json({ error: lookup.error }, lookup.status);
|
|
750
1058
|
const { info } = lookup;
|
|
751
1059
|
const meta: FileMeta = {
|
|
@@ -761,11 +1069,15 @@ export function createApp(runtime: HobRuntime, options: { webDir?: string; home:
|
|
|
761
1069
|
return c.json(meta);
|
|
762
1070
|
});
|
|
763
1071
|
|
|
764
|
-
//
|
|
1072
|
+
// 任务工作目录,一次只列一层。
|
|
765
1073
|
app.get("/api/jobs/:id/files", (c) => {
|
|
766
1074
|
const job = store.job(c.req.param("id"));
|
|
767
1075
|
if (!job) return c.json({ error: "任务不存在" }, 404);
|
|
768
|
-
|
|
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
|
+
}
|
|
769
1081
|
});
|
|
770
1082
|
|
|
771
1083
|
app.get("/api/events", (c) => {
|