hob-bot 1.0.4 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (82) hide show
  1. package/README.md +76 -6
  2. package/README.zh-CN.md +76 -6
  3. package/dist/web/assets/DesktopView-CEBJFAIN.js +5 -0
  4. package/dist/web/assets/index-Di5lyTHO.css +1 -0
  5. package/dist/web/assets/index-y09i1RGd.js +267 -0
  6. package/dist/web/assets/{math-zZ4JA8eM.js → math-weKGDUmL.js} +2 -2
  7. package/dist/web/index.html +2 -2
  8. package/dist/web/mock/desktop.jpg +0 -0
  9. package/dist/web/mock/photo.jpg +0 -0
  10. package/dist/web/mock/photo2.jpg +0 -0
  11. package/dist/web/mock/sunset.html +90 -0
  12. package/package.json +7 -1
  13. package/server/src/actions.ts +16 -56
  14. package/server/src/assistant-skills.ts +215 -0
  15. package/server/src/boot.ts +1 -0
  16. package/server/src/claude-status.ts +47 -0
  17. package/server/src/config.ts +3 -1
  18. package/server/src/delegations.ts +21 -11
  19. package/server/src/desktop-control.ts +152 -0
  20. package/server/src/desktop-events.ts +63 -0
  21. package/server/src/desktop-public.ts +43 -0
  22. package/server/src/desktop-tools.ts +161 -0
  23. package/server/src/desktop.ts +76 -0
  24. package/server/src/embedding.ts +52 -0
  25. package/server/src/files.ts +63 -43
  26. package/server/src/http.ts +325 -13
  27. package/server/src/job-view.ts +52 -0
  28. package/server/src/jobs.ts +326 -44
  29. package/server/src/knowledge-index.ts +123 -0
  30. package/server/src/knowledge-tools.ts +94 -0
  31. package/server/src/knowledge.ts +451 -0
  32. package/server/src/lore-entry.ts +10 -0
  33. package/server/src/main.ts +6 -0
  34. package/server/src/mcp.ts +745 -0
  35. package/server/src/memory.ts +11 -4
  36. package/server/src/now.ts +33 -0
  37. package/server/src/overview.ts +4 -4
  38. package/server/src/pages.ts +1 -1
  39. package/server/src/pi.ts +92 -16
  40. package/server/src/prompt.ts +51 -156
  41. package/server/src/providers.ts +3 -2
  42. package/server/src/push.ts +39 -5
  43. package/server/src/router-activity.ts +6 -0
  44. package/server/src/router-tools.ts +184 -39
  45. package/server/src/router.ts +154 -17
  46. package/server/src/routines.ts +14 -4
  47. package/server/src/runtime.ts +15 -0
  48. package/server/src/settings.ts +43 -1
  49. package/server/src/store.ts +4 -3
  50. package/server/src/transcript.ts +57 -4
  51. package/server/src/worker-tools.ts +14 -12
  52. package/shared/knowledge.ts +92 -0
  53. package/shared/mcp.ts +91 -0
  54. package/shared/skills.ts +18 -0
  55. package/shared/types.ts +104 -20
  56. package/vendor/claude-bridge/HOB-PATCH.md +10 -0
  57. package/vendor/claude-bridge/LICENSE +21 -0
  58. package/vendor/claude-bridge/package.json +62 -0
  59. package/vendor/claude-bridge/src/agents-md.ts +14 -0
  60. package/vendor/claude-bridge/src/askclaude-schema.ts +92 -0
  61. package/vendor/claude-bridge/src/askclaude-ui.ts +90 -0
  62. package/vendor/claude-bridge/src/attachments.ts +135 -0
  63. package/vendor/claude-bridge/src/config.ts +105 -0
  64. package/vendor/claude-bridge/src/convert.ts +222 -0
  65. package/vendor/claude-bridge/src/extract-tool-results.ts +47 -0
  66. package/vendor/claude-bridge/src/index.ts +2503 -0
  67. package/vendor/claude-bridge/src/log-paths.ts +19 -0
  68. package/vendor/claude-bridge/src/mcp-server.ts +169 -0
  69. package/vendor/claude-bridge/src/models.ts +164 -0
  70. package/vendor/claude-bridge/src/prompt-capture.ts +420 -0
  71. package/vendor/claude-bridge/src/prompt-stream.ts +102 -0
  72. package/vendor/claude-bridge/src/query-state.ts +118 -0
  73. package/vendor/claude-bridge/src/rate-limit.ts +48 -0
  74. package/vendor/claude-bridge/src/session-cwd.ts +38 -0
  75. package/vendor/claude-bridge/src/session-verify.ts +38 -0
  76. package/vendor/claude-bridge/src/skills.ts +20 -0
  77. package/vendor/claude-bridge/src/transcript.ts +86 -0
  78. package/vendor/claude-bridge/src/usage.ts +53 -0
  79. package/vendor/lore-pi-extension/package.json +1 -1
  80. package/vendor/lore-pi-extension/tools.ts +8 -5
  81. package/dist/web/assets/index-BCGEZvgg.css +0 -1
  82. package/dist/web/assets/index-BqkIAlk9.js +0 -258
@@ -4,6 +4,9 @@ import { commandSummary } from "../../shared/command.ts";
4
4
  import type { JobStep } from "../../shared/types.ts";
5
5
  import { showPath } from "./text.ts";
6
6
 
7
+ /** 沉淀技能那一轮发给任务的话的开头:任务详情里这句显示成「沉淀技能」。 */
8
+ export const SEDIMENT_PROMPT_HEAD = "这件事以后还会再做。回顾刚才的过程,把能复用的做法沉淀成技能";
9
+
7
10
  /** transcript 里单步输出最多这么多字,超出的整段落盘,页面按路径去拉。 */
8
11
  export const STEP_OUTPUT_LIMIT = 20 * 1024;
9
12
  /** 实时事件里的单步输出最多这么多字(全文落盘,给路径)。 */
@@ -84,8 +87,19 @@ const MEMORY_TOOLS: Record<string, string> = {
84
87
  lore_status: "查看记忆状态",
85
88
  };
86
89
 
90
+ /** MCP 工具(mcp__<服务>__<工具>)显示成「服务名 · 工具名」,不露原始名。 */
91
+ function mcpToolDisplay(name: string, mcpServers: Record<string, string> | undefined): string | undefined {
92
+ if (!name.startsWith("mcp__")) return undefined;
93
+ const rest = name.slice("mcp__".length);
94
+ const split = rest.indexOf("__");
95
+ if (split <= 0) return undefined;
96
+ const server = mcpServers?.[rest.slice(0, split)] ?? rest.slice(0, split);
97
+ const tool = rest.slice(split + 2) || "?";
98
+ return `${server} · ${tool}`;
99
+ }
100
+
87
101
  /** 一行中文:「运行 `npm test`」「修改 server/src/jobs.ts」「读取 README.md」。 */
88
- export function toolTitle(name: string, args: Record<string, unknown>): string {
102
+ export function toolTitle(name: string, args: Record<string, unknown>, mcpServers?: Record<string, string>): string {
89
103
  const path = String(args.path ?? "");
90
104
  switch (name) {
91
105
  case "bash":
@@ -104,6 +118,8 @@ export function toolTitle(name: string, args: Record<string, unknown>): string {
104
118
  return `列出 ${path}`;
105
119
  case "report":
106
120
  return "提交结果";
121
+ case "save_skill":
122
+ return `保存技能:${oneLine(String(args.title ?? ""), 60)}`;
107
123
  case "ask_user":
108
124
  return `询问用户:${oneLine(String(args.title ?? ""), 60)}`;
109
125
  case "overview":
@@ -112,9 +128,40 @@ export function toolTitle(name: string, args: Record<string, unknown>): string {
112
128
  return `生成图片:${oneLine(String(args.prompt ?? ""), 60)}`;
113
129
  case "web_search":
114
130
  return `联网搜索:${oneLine(String(args.query ?? ""), 60)}`;
131
+ case "find_knowledge":
132
+ return args.query ? `找资料:${oneLine(String(args.query), 60)}` : "找资料";
133
+ case "search_knowledge":
134
+ return args.query ? `查知识库:${oneLine(String(args.query), 60)}` : "查知识库";
115
135
  case "web_fetch":
116
136
  return `读取网页:${oneLine(String(args.url ?? ""), 60)}`;
137
+ case "browser_open": {
138
+ let target = oneLine(String(args.url ?? ""), 60);
139
+ try { target = new URL(target).hostname; } catch { /* 无效地址保留原样 */ }
140
+ return `浏览器 · 打开 ${target}`;
141
+ }
142
+ case "browser_snapshot": return "浏览器 · 读取页面";
143
+ case "browser_click": return "浏览器 · 点击";
144
+ case "browser_type": return "浏览器 · 输入";
145
+ case "browser_press": return "浏览器 · 按键";
146
+ case "browser_back": return "浏览器 · 返回";
147
+ case "browser_screenshot": return "浏览器 · 截图";
148
+ case "desktop_screenshot": return "桌面 · 截图";
149
+ case "desktop_action": {
150
+ const names: Record<string, string> = { click: "点击", move: "移动", drag: "拖动", type: "输入", key: "按键", scroll: "滚动", wait: "等待" };
151
+ const first = Array.isArray(args.actions) ? args.actions[0] as { type?: string } | undefined : undefined;
152
+ return `桌面 · ${names[first?.type ?? ""] ?? "操作"}`;
153
+ }
154
+ case "tool_search":
155
+ return `搜索可用工具:${oneLine(String(args.query ?? ""), 60)}`;
156
+ case "list_mcp_resources":
157
+ return "列出 MCP 资源";
158
+ case "list_mcp_resource_templates":
159
+ return "列出 MCP 资源模板";
160
+ case "read_mcp_resource":
161
+ return `读取 MCP 资源:${oneLine(String(args.uri ?? ""), 90)}`;
117
162
  default: {
163
+ const mcp = mcpToolDisplay(name, mcpServers);
164
+ if (mcp) return `调用 ${mcp}`;
118
165
  const label = MEMORY_TOOLS[name];
119
166
  if (!label) return name;
120
167
  const target = String(args.uri ?? args.query ?? "");
@@ -148,17 +195,20 @@ export interface ToolStepInput {
148
195
  at?: string;
149
196
  cwd?: string;
150
197
  running?: boolean;
198
+ /** MCP 工具的显示名映射(转义后的服务名 → 配置里的原名) */
199
+ mcpServers?: Record<string, string>;
151
200
  }
152
201
 
153
202
  /** 一次工具调用 → 一个步骤(还没有结果)。实时事件和落盘推导都用它,页面按 id 合并。 */
154
203
  export function toolStep(input: ToolStepInput): JobStep {
155
204
  const args = toolArgs(input.name, input.args, input.cwd ?? "");
205
+ const mcp = mcpToolDisplay(input.name, input.mcpServers);
156
206
  return {
157
207
  id: input.id,
158
208
  kind: "tool",
159
209
  at: input.at ?? new Date().toISOString(),
160
- name: input.name,
161
- title: toolTitle(input.name, args),
210
+ name: mcp ?? input.name,
211
+ title: toolTitle(input.name, args, input.mcpServers),
162
212
  args,
163
213
  ...(input.running ? { running: true } : {}),
164
214
  };
@@ -193,6 +243,8 @@ export interface DeriveOptions {
193
243
  limit?: number;
194
244
  /** 输出过长时落盘,返回路径 */
195
245
  save?: (stepId: string, text: string) => string;
246
+ /** MCP 工具的显示名映射(转义后的服务名 → 配置里的原名) */
247
+ mcpServers?: Record<string, string>;
196
248
  }
197
249
 
198
250
  /** 从任务会话文件推导整个过程:说明文字、思考摘要、工具调用(含结果)。 */
@@ -237,6 +289,7 @@ export function deriveSteps(file: string, options: DeriveOptions = {}): JobStep[
237
289
  args: block.arguments ?? {},
238
290
  at,
239
291
  cwd: options.cwd ?? "",
292
+ mcpServers: options.mcpServers,
240
293
  running: true,
241
294
  });
242
295
  toolIndex.set(step.id, steps.length);
@@ -265,7 +318,7 @@ export function deriveSteps(file: string, options: DeriveOptions = {}): JobStep[
265
318
  seenUser = true; // 第一句是任务说明,详情头部已经有
266
319
  continue;
267
320
  }
268
- steps.push({ id: `u${message.timestamp ?? at}`, kind: "text", at, from: "user", text });
321
+ steps.push({ id: `u${message.timestamp ?? at}`, kind: "text", at, from: "user", text: text.startsWith(SEDIMENT_PROMPT_HEAD) ? "沉淀技能" : text });
269
322
  }
270
323
  }
271
324
  return steps;
@@ -13,15 +13,14 @@ export function registerReportTool(runtime: HobRuntime, jobId: string): ToolDefi
13
13
  return {
14
14
  name: "report",
15
15
  label: "提交结果",
16
- description:
17
- "任务结束时必须调用一次,提交结果。summary 用中文写给用户:一两句结论 + 关键数字,不要罗列过程。",
16
+ description: "任务结束时调用一次,提交结果。",
18
17
  promptSnippet: "任务结束必须调用 report 提交结果。",
19
18
  parameters: Type.Object({
20
19
  outcome: Type.Unsafe<"done" | "stuck" | "need_approval">(
21
- Type.String({ description: "done 已完成 / stuck 受阻 / need_approval 待确认" }),
20
+ Type.String({ description: "done 做完 / stuck 做不下去(写明缺什么)/ need_approval 需要用户拍板" }),
22
21
  ),
23
- summary: Type.String({ description: "中文摘要,一两句结论 + 关键数字,不超过 600 字" }),
24
- files: Type.Optional(Type.Array(Type.String(), { description: "产出文件的绝对路径" })),
22
+ result: Type.String({ description: "完整结果:结论、数据、链接、文件路径;写给派活的助手。" }),
23
+ files: Type.Optional(Type.Array(Type.String(), { description: "要给用户看的文件(绝对路径)" })),
25
24
  }),
26
25
  execute: async (_id, params: any) => {
27
26
  // 不认识的结果不能当成已完成:退回让模型重新调用
@@ -34,7 +33,7 @@ export function registerReportTool(runtime: HobRuntime, jobId: string): ToolDefi
34
33
  }
35
34
  runtime.jobs.reportFromTool(jobId, {
36
35
  outcome: params.outcome,
37
- summary: String(params.summary ?? ""),
36
+ result: String(params.result ?? ""),
38
37
  ...(Array.isArray(params.files) && params.files.length ? { files: params.files as string[] } : {}),
39
38
  });
40
39
  return ok("结果已记录。");
@@ -47,7 +46,7 @@ export function registerAskTool(runtime: HobRuntime, jobId: string): ToolDefinit
47
46
  return {
48
47
  name: "ask_user",
49
48
  label: "询问用户",
50
- description: "遇到规则要求执行前确认、或者不可逆的操作时,用这个询问用户,会一直等到用户选择。返回用户选择的选项文字。",
49
+ description: "在对话里出选择题卡片(2 到 3 个选项),等用户选,返回用户的选择。用于用户定的规则要求先确认的事,或发送、删除、付费这类撤不回的事。",
51
50
  promptSnippet: "规则要求执行前确认的事,执行前必须调用 ask_user。",
52
51
  parameters: Type.Object({
53
52
  title: Type.String({ description: "问题,一句话" }),
@@ -55,27 +54,30 @@ export function registerAskTool(runtime: HobRuntime, jobId: string): ToolDefinit
55
54
  options: Type.Array(
56
55
  Type.Object({
57
56
  label: Type.String({ description: "选项文字" }),
58
- always: Type.Optional(Type.Boolean({ description: "选中后将这句话记为规则" })),
59
57
  }),
60
58
  { description: "2 到 3 个选项", minItems: 2, maxItems: 3 },
61
59
  ),
62
- ruleText: Type.Optional(Type.String({ description: "always 选项要写进规则的文字" })),
60
+ remember: Type.Optional(Type.String({ description: "同类事以后可以不再问时,填这类事的简称(如「清理临时目录」);卡片会多一个「以后直接做」的选项。" })),
63
61
  }),
64
62
  execute: async (_id, params: any) => {
65
- const labels: { label: string; always?: boolean }[] = Array.isArray(params.options) ? params.options : [];
63
+ const labels: { label: string }[] = Array.isArray(params.options) ? params.options : [];
66
64
  if (labels.length < 2 || labels.length > 3) {
67
65
  return { content: [{ type: "text" as const, text: "选项要 2 到 3 个。" }], details: undefined, isError: true };
68
66
  }
67
+ const remember = typeof params.remember === "string" ? params.remember.trim() : "";
68
+ if (remember && labels.length > 2) {
69
+ return { content: [{ type: "text" as const, text: "带 remember 时最多给 2 个选项。" }], details: undefined, isError: true };
70
+ }
69
71
  const options: ApprovalOption[] = labels.map((option, index) => ({
70
72
  key: (["A", "B", "C"] as const)[index],
71
73
  label: String(option.label),
72
- ...(option.always ? { always: true } : {}),
73
74
  }));
75
+ if (remember) options.push({ key: "C", label: `以后「${remember}」直接做`, always: true });
74
76
  const answer = await runtime.jobs.askQuestion(jobId, {
75
77
  title: String(params.title),
76
78
  ...(params.detail ? { detail: String(params.detail) } : {}),
77
79
  options,
78
- ...(params.ruleText ? { ruleText: String(params.ruleText) } : {}),
80
+ ...(remember ? { ruleText: remember } : {}),
79
81
  });
80
82
  // 用户可能没选给出的选项,而是自己写了一段:answer 已经是回给模型的整句
81
83
  return ok(answer);
@@ -0,0 +1,92 @@
1
+ // 知识库的数据契约:服务端(server/src/knowledge.ts、http.ts)和页面共用。设计见 .scratch/v2/design-knowledge.md。
2
+
3
+ import type { FileKind } from "./types.ts";
4
+
5
+ /** 怎么进来的:助手产出、对话上传(这两类自动收,不建索引),或在知识库里上传。 */
6
+ export type KnowledgeOrigin = "output" | "chat" | "upload";
7
+
8
+ /** 页面上的类型筛选;一条只属于一组,由 FileKind 决定(见 knowledgeGroup)。 */
9
+ export type KnowledgeGroup = "image" | "document" | "table" | "page" | "other";
10
+
11
+ export interface KnowledgeVersion {
12
+ hash: string;
13
+ path: string;
14
+ size: number;
15
+ at: string;
16
+ }
17
+
18
+ export interface KnowledgeEntry {
19
+ id: string; // k-xxxx
20
+ origin: KnowledgeOrigin;
21
+ name: string; // 显示名:原文件名或页面标题,用户可改
22
+ description?: string; // 页面摘要、生图提示词或任务标题
23
+ kind: FileKind;
24
+ path: string; // 当前版本在知识库目录里的绝对路径,经 /api/files 取
25
+ hash: string;
26
+ size: number;
27
+ createdAt: string;
28
+ updatedAt: string; // 只跟内容走(新版本时更新),改名和整理不动;列表按它排序
29
+ versions?: KnowledgeVersion[]; // 旧版本,旧的在前
30
+ source?: { assistantId: string; itemId?: string; jobId?: string }; // 出处
31
+ collections: string[]; // 所在的用户知识库 id
32
+ index?: KnowledgeIndex; // 二期:只在条目属于至少一个用户知识库时有
33
+ }
34
+
35
+ export interface KnowledgeCollection {
36
+ id: string; // kc-xxxx
37
+ name: string;
38
+ createdAt: string;
39
+ count: number; // 里面有几条
40
+ indexed?: number; // 二期:索引已建好的条数
41
+ failed?: number; // 二期:建索引失败的条数
42
+ unsupported?: number; // 二期:这类文件还不能建索引的条数(不算进「已建索引 a/b」的 b)
43
+ }
44
+
45
+ /** 二期:用户知识库里文件的索引状态。queued/indexing/failed 的文件查不到;unsupported = 这类文件还不能建索引。 */
46
+ export type KnowledgeIndexState = "queued" | "indexing" | "ready" | "failed" | "unsupported";
47
+
48
+ export interface KnowledgeIndex {
49
+ state: KnowledgeIndexState;
50
+ chunks?: number; // ready 时有几段
51
+ error?: string; // failed / unsupported 的原因,直接给用户看
52
+ }
53
+
54
+ /** 一段原文。出处链接 #k/<条目 id>/<index> 指到这里。 */
55
+ export interface KnowledgeChunk {
56
+ index: number; // 在这个文件里是第几段,从 0 起
57
+ heading: string; // 标题路径:「文件名 › 一级 › 二级」
58
+ text: string;
59
+ page?: number; // PDF 的页码,从 1 起
60
+ }
61
+
62
+ /** GET /api/knowledge/entries/:id[?chunk=N] 的返回。 */
63
+ export interface KnowledgeEntryView {
64
+ entry: KnowledgeEntry;
65
+ chunk?: KnowledgeChunk; // 带了 chunk 且这段存在时才有
66
+ }
67
+
68
+ /** 出处链接:工具和自动检索给出,助手照抄进回答,界面点开定位。 */
69
+ export function knowledgeLink(entryId: string, chunk: number): string {
70
+ return `#k/${entryId}/${chunk}`;
71
+ }
72
+
73
+ export function parseKnowledgeLink(href: string): { entryId: string; chunk?: number } | null {
74
+ const match = /^#k\/(k-[\w-]+)(?:\/(\d+))?$/.exec(href.trim());
75
+ return match ? { entryId: match[1]!, ...(match[2] !== undefined ? { chunk: Number(match[2]) } : {}) } : null;
76
+ }
77
+
78
+ /** GET /api/knowledge 的返回。 */
79
+ export interface KnowledgePage {
80
+ entries: (KnowledgeEntry & { snippet?: string })[]; // snippet:关键词命中文字时的前后片段
81
+ hasMore: boolean;
82
+ collections: KnowledgeCollection[];
83
+ }
84
+
85
+ /** 类型筛选的分组。 */
86
+ export function knowledgeGroup(kind: FileKind): KnowledgeGroup {
87
+ if (kind === "image" || kind === "svg") return "image";
88
+ if (kind === "csv" || kind === "json") return "table";
89
+ if (kind === "html") return "page";
90
+ if (kind === "markdown" || kind === "text" || kind === "pdf" || kind === "code" || kind === "xml") return "document";
91
+ return "other";
92
+ }
package/shared/mcp.ts ADDED
@@ -0,0 +1,91 @@
1
+ // MCP(Model Context Protocol)服务的契约:设置 → MCP 页和 REST 接口共用。
2
+ // 配置文件就是引擎读的那份 HOB_HOME/engine/mcp.json;说明见 docs/architecture.md 的 MCP 一节。
3
+
4
+ /** 服务里每个工具怎么给模型:direct = 直接提供;deferred = 按需搜索。 */
5
+ export type McpExposureChoice = "direct" | "deferred";
6
+
7
+ /**
8
+ * 环境变量 / 请求头的值:密钥永不回传。redacted = 只知道「已设置」;
9
+ * 整值 `${VAR}` 引用或 `!command` 不含密钥,原样回显。
10
+ */
11
+ export interface McpSecretValue {
12
+ redacted: boolean;
13
+ value?: string;
14
+ }
15
+
16
+ /** GET /api/mcp:列表里的一项(不含密钥明文)。valid = false 表示手改坏了,只可编辑或删除。 */
17
+ export interface McpServerView {
18
+ name: string;
19
+ valid: boolean;
20
+ invalidReason?: string;
21
+ kind?: "stdio" | "http";
22
+ enabled: boolean;
23
+ exposure: McpExposureChoice;
24
+ description?: string;
25
+ /** 本地命令(stdio) */
26
+ command?: string;
27
+ args?: string[];
28
+ env?: Record<string, McpSecretValue>;
29
+ cwd?: string;
30
+ /** 远程地址(streamable HTTP) */
31
+ url?: string;
32
+ headers?: Record<string, McpSecretValue>;
33
+ /** 单独关掉的工具名(toolExposure 里设为 hidden 的) */
34
+ hiddenTools?: string[];
35
+ }
36
+
37
+ /** GET /api/mcp。 */
38
+ export interface McpView {
39
+ servers: McpServerView[];
40
+ }
41
+
42
+ /** POST /api/mcp/:name/test(失败也是 200,原因在这里)。 */
43
+ export type McpTestResult =
44
+ | { ok: true; tools: { name: string; description: string }[] }
45
+ | { ok: false; state: "failed" | "needs_auth"; error: string; stderrTail: string[] };
46
+
47
+ /** 环境变量 / 请求头逐键的改法:保留已存的值 / 删除 / 换成新值。 */
48
+ export type McpSecretMutation = { mode: "preserve" } | { mode: "remove" } | { mode: "replace"; value: string };
49
+
50
+ /** PUT /api/mcp/:name:新建或改一个服务。提供方式、启用状态不在这里改。 */
51
+ export interface McpServerSave {
52
+ kind: "stdio" | "http";
53
+ command?: string;
54
+ args?: string[];
55
+ cwd?: string;
56
+ url?: string;
57
+ description?: string;
58
+ env?: Record<string, McpSecretMutation>;
59
+ headers?: Record<string, McpSecretMutation>;
60
+ }
61
+
62
+ /** PATCH /api/mcp/:name:启用开关、提供方式、单独关掉的工具(完整列表,全量替换)。 */
63
+ export interface McpServerPatch {
64
+ enabled?: boolean;
65
+ exposure?: McpExposureChoice;
66
+ hiddenTools?: string[];
67
+ }
68
+
69
+ /** GET /api/mcp/import:本机其他客户端里配的 MCP 服务(只读来源文件)。 */
70
+ export interface McpImportItem {
71
+ /** 勾选时回传的标识:来源:服务名 */
72
+ id: string;
73
+ name: string;
74
+ source: "pi" | "claude" | "cursor";
75
+ kind?: "stdio" | "http";
76
+ exists: boolean; // Hob 里已有同名(- 和 _ 视为同名),导入会跳过
77
+ selectable: boolean;
78
+ note?: string; // 不可选的原因
79
+ }
80
+
81
+ export interface McpImportView {
82
+ /** 读了哪些位置(界面显示用) */
83
+ sources: string[];
84
+ items: McpImportItem[];
85
+ }
86
+
87
+ /** POST /api/mcp/import {ids}。 */
88
+ export interface McpImportResult {
89
+ imported: string[];
90
+ skipped: string[]; // 跳过的,带说明
91
+ }
package/shared/skills.ts CHANGED
@@ -35,3 +35,21 @@ export interface SkillImportResult {
35
35
  skipped: string[];
36
36
  errors: string[];
37
37
  }
38
+
39
+ /**
40
+ * 助手自己沉淀的技能:HOB_HOME/assistants/<id>/skills/<name>/SKILL.md(加 scripts/、references/),
41
+ * 元数据在 assistants/<id>/skills/skills.json。只有这个助手的任务加载。
42
+ */
43
+ export interface AssistantSkill {
44
+ name: string; // 目录名:小写字母、数字、连字符
45
+ title: string; // 界面上显示的名字,如「网关每日巡检」
46
+ description: string; // 一句话:什么时候用
47
+ path: string; // SKILL.md 的绝对路径(预览用)
48
+ version: number; // 每次修补 +1,旧版在 skills/<name>/.history/<版本>/
49
+ createdAt: string;
50
+ updatedAt: string;
51
+ fromJobId?: string; // 最近一次从哪个任务沉淀或修补
52
+ uses: number; // 被任务读过几次
53
+ lastUsedAt?: string;
54
+ enabled: boolean;
55
+ }
package/shared/types.ts CHANGED
@@ -126,6 +126,28 @@ export interface ModelThinkingView {
126
126
  custom: boolean; // 存过自定义档位(可以恢复默认)
127
127
  }
128
128
 
129
+ export interface ClaudeCodeSettings {
130
+ enabled: boolean;
131
+ plan: "pro" | "max";
132
+ executable: string;
133
+ }
134
+
135
+ export interface ClaudeCodeView {
136
+ settings: SettingsFile;
137
+ status: ClaudeCodeStatus;
138
+ }
139
+
140
+ /** 本机 Claude Code 状态,不包含凭据或程序原始输出。 */
141
+ export interface ClaudeCodeStatus {
142
+ found: boolean;
143
+ executable?: string;
144
+ version?: string;
145
+ loggedIn: boolean;
146
+ email?: string;
147
+ plan?: string;
148
+ error?: string;
149
+ }
150
+
129
151
  /** GET /api/providers:已配的服务商、引擎自带的服务商目录、跟着一起回的整份设置(前端要新 revision)。 */
130
152
  export interface ProvidersView {
131
153
  providers: ProviderView[];
@@ -214,6 +236,8 @@ export interface Assistant {
214
236
  };
215
237
  /** 派出去的活能用的能力;缺省全开(读总是有) */
216
238
  abilities?: { exec: boolean; write: boolean; image: boolean };
239
+ /** 知识库二期:绑定的用户知识库 id。缺省 = 没绑定:不自动查,也没有 search_knowledge */
240
+ knowledge?: string[];
217
241
  createdAt: Iso;
218
242
  splitFrom?: { from: string; at: Iso; conversations: number };
219
243
  }
@@ -245,6 +269,9 @@ export interface RoutineRun {
245
269
  jobId: string;
246
270
  at: Iso;
247
271
  status: JobStatus;
272
+ durationMs?: number; // 技能二期:这次用时
273
+ tools?: number; // 技能二期:工具调用次数
274
+ tokens?: number; // 技能二期:输入加输出 token
248
275
  }
249
276
 
250
277
  export interface Routine {
@@ -256,6 +283,7 @@ export interface Routine {
256
283
  active: boolean;
257
284
  createdAt: Iso;
258
285
  runs: RoutineRun[]; // 最近 20 次,新的在前
286
+ skill?: string; // 绑定这个助手自己的技能(AssistantSkill.name):每次执行按它做
259
287
  }
260
288
 
261
289
  export interface Delegation {
@@ -264,7 +292,7 @@ export interface Delegation {
264
292
  to: string;
265
293
  text: string;
266
294
  createdAt: Iso;
267
- status: "running" | "waiting" | "done" | "stuck" | "interrupted" | "returned";
295
+ status: "running" | "waiting" | "done" | "stuck" | "interrupted" | "returned" | "takenOver";
268
296
  jobIds: string[];
269
297
  reply?: string;
270
298
  returnedText?: string; // 退回时补充的用户要求
@@ -342,7 +370,7 @@ export interface JobsPage {
342
370
  export interface Job {
343
371
  id: string; // "t-" + 4 位以上
344
372
  assistantId: string;
345
- assistantName?: string; // 建任务时的助手名:助手并回 Hob 后,任务详情里照样显示
373
+ assistantName?: string; // 建任务时的助手名:助手归档后,任务详情里照样显示
346
374
  title: string; // 如 "查页面卡顿"
347
375
  instruction: string;
348
376
  status: JobStatus;
@@ -352,8 +380,9 @@ export interface Job {
352
380
  endedAt?: Iso;
353
381
  usage?: JobUsage; // 输入/输出 token 和花费
354
382
  durationMs?: number; // 用时(结束时间 − 开始时间)
383
+ expect?: { minutes: number; source: "assistant" | "routine" | "default"; runs?: number };
355
384
  stats: { reads: number; commands: number; edits: number };
356
- summary?: string; // report 的摘要(≤600 字)
385
+ summary?: string; // report 的完整结果
357
386
  reportPath?: string; // 完整报告
358
387
  files?: string[]; // 产出文件(绝对路径)
359
388
  cwd?: string; // 工作目录(缺省为 HOB_HOME/jobs/<id>/work)
@@ -362,10 +391,26 @@ export interface Job {
362
391
  modelReason?: string; // 换了模型的原因,如「需要看图,换成了 X」
363
392
  inputs?: string[]; // 交给任务的附件(绝对路径)
364
393
  routineId?: string; // 由定时触发
394
+ skillsUsed?: string[]; // 这次读过的本助手技能(AssistantSkill.name)
365
395
  delegationId?: string; // 转交回合创建的任务
396
+ /** 续做前每一轮结束时的样子(第 0 轮起)。对话里前面几张任务卡照它显示,最后一张跟着任务实时变 */
397
+ rounds?: JobRound[];
366
398
  activity: ActivityLine[]; // 最近 200 行
367
399
  }
368
400
 
401
+ export interface JobRound {
402
+ summary?: string;
403
+ files?: string[];
404
+ usage?: JobUsage;
405
+ reportPath?: string;
406
+ followUp?: string;
407
+ status: JobStatus;
408
+ statusReason?: string;
409
+ startedAt?: Iso;
410
+ endedAt?: Iso;
411
+ durationMs?: number;
412
+ }
413
+
369
414
  export interface ApprovalOption {
370
415
  key: "A" | "B" | "C";
371
416
  label: string; // 如 "以后同类内容直接发布,无需确认"
@@ -408,16 +453,17 @@ export interface Draft {
408
453
  createdAt: Iso;
409
454
  }
410
455
 
411
- export type NoteIcon = "memory" | "rule" | "routine" | "split" | "info";
456
+ export type NoteIcon = "memory" | "rule" | "routine" | "split" | "info" | "skill";
412
457
 
413
458
  export type Item = (
414
459
  | { kind: "user"; id: string; at: Iso; text: string; quote?: { itemId: string; text: string }; files?: string[]; from?: { assistantId: string; name: string; delegationId: string } }
415
- | { kind: "assistant"; id: string; at: Iso; text: string; streaming?: boolean; routine?: { id: string; name: string } }
416
- | { kind: "note"; id: string; at: Iso; icon: NoteIcon; text: string; handbackId?: string } // 居中灰色一行,如 "记忆已更新 · 不看融资新闻"
460
+ | { kind: "assistant"; id: string; at: Iso; text: string; streaming?: boolean; routine?: { id: string; name: string; jobId?: string } }
461
+ | { kind: "note"; id: string; at: Iso; icon: NoteIcon; text: string; handbackId?: string; path?: string } // path:点了预览这个文件(如技能的 SKILL.md) // 居中灰色一行,如 "记忆已更新 · 不看融资新闻"
417
462
  | { kind: "question"; id: string; at: Iso; approvalId: string }
418
- | { kind: "job"; id: string; at: Iso; jobId: string }
463
+ | { kind: "job"; id: string; at: Iso; jobId: string; round?: number } // 第几轮(续做一次 +1),缺省是第 0 轮
419
464
  | { kind: "delegation"; id: string; at: Iso; delegationId: string }
420
465
  | { kind: "draft"; id: string; at: Iso; draftId: string }
466
+ | { kind: "image"; id: string; at: Iso; prompt: string; size?: string; startedAt: Iso; status: "pending" | "done" | "failed"; path?: string; error?: string }
421
467
  | { kind: "file"; id: string; at: Iso; path: string; name: string; meta?: string } // meta 如 "3200 字 · 4 张图"
422
468
  ) & {
423
469
  /** 这条开启新的一组,前端在它上面拉开间距:事件触发的新一轮回复,或任务直接发进对话的图片、提问卡 */
@@ -468,6 +514,7 @@ export interface MemoryConfig {
468
514
  provider: MemoryProvider;
469
515
  providerExplicit: boolean; // 用户在设置里明确选过(没选过就是按有没有配好 Lore 推出的)
470
516
  lore: LoreConnectionInfo;
517
+ lorePlugin: string; // Hob 自带的 Lore 插件版本(Lore 正式版的版本号)
471
518
  }
472
519
 
473
520
  /** POST /api/memory/lore-test:测试失败也是 200,原因在这里。 */
@@ -508,13 +555,47 @@ export interface Settings {
508
555
  models: ModelSettings;
509
556
  image: ImageSettings;
510
557
  search: SearchSettings;
558
+ /** 云桌面令牌保存在 secrets.json,不包含在设置中。 */
559
+ desktop?: { url: string; enabled: boolean; publicAssistants?: string[] };
560
+ claude?: ClaudeCodeSettings;
511
561
  /** 停用的技能,键为来源和技能名。 */
512
562
  skills?: { disabled: string[] };
513
563
  work: WorkSettings;
514
564
  push: PushSettings;
565
+ /** 知识库二期:向量模型(Key 在 secrets.json 的 embedding);缺省 = 关闭,只按关键词查 */
566
+ knowledge?: { embedding: EmbeddingSettings };
515
567
  }
516
568
 
569
+ /** 设置 → 知识库:向量模型,照图像生成的做法(兼容接口的地址 + 模型,Key 单独存)。 */
570
+ export type EmbeddingSettings = { source: "off" } | { source: "openai"; baseUrl: string; model: string };
571
+
572
+ /** GET /api/knowledge/embedding:Key 配没配(永不回显)。 */
573
+ export interface EmbeddingKeyView {
574
+ keyConfigured: boolean;
575
+ }
576
+
577
+ /** PUT /api/knowledge/embedding 的返回:整份设置和 Key 状态。 */
578
+ export interface EmbeddingSettingsSaved {
579
+ settings: SettingsFile;
580
+ keyConfigured: boolean;
581
+ }
582
+
583
+ /** POST /api/knowledge/embedding/test:失败也是 200,原因在这里。 */
584
+ export type EmbeddingTestResult = { ok: true; dims: number } | { ok: false; error: string };
585
+
517
586
  /** HOB_HOME/settings.json:界面可改的设置,写入走 revision 比对(CAS)。 */
587
+ export interface DesktopView {
588
+ revision: number;
589
+ url: string;
590
+ enabled: boolean;
591
+ deployed: boolean;
592
+ tokenConfigured: boolean;
593
+ publicAssistants: string[];
594
+ }
595
+
596
+ export interface DesktopScreen { id: string; running: boolean; browserRunning: boolean; viewers: number; lastActiveAt: string | null }
597
+ export type DesktopTestResult = { ok: true; health: { version: string; display: { width: number; height: number }; screens: DesktopScreen[] } } | { ok: false; error: string };
598
+
518
599
  export interface SettingsFile {
519
600
  revision: number;
520
601
  settings: Settings;
@@ -583,20 +664,18 @@ export interface FileMeta {
583
664
  page?: boolean; // 展示页面目录中的渲染产物
584
665
  }
585
666
 
586
- /** GET /api/jobs/:id/files:任务工作目录里的一个文件。 */
587
- export interface JobFileEntry {
588
- path: string; // 绝对路径
589
- rel: string; // 相对任务工作目录,如 "src/index.ts"
590
- name: string;
591
- size: number;
592
- modifiedAt: Iso;
593
- kind: FileKind;
594
- }
667
+ /** 工作目录中的一项,目录的 count 是其下一层可见项数。 */
668
+ export type JobFileEntry = { path: string; rel: string; name: string } & (
669
+ | { type: "file"; size: number; modifiedAt: Iso; kind: FileKind }
670
+ | { type: "directory"; count: number }
671
+ );
595
672
 
596
- /** GET /api/jobs/:id/files:任务工作目录(HOB_HOME/jobs/<id>/work)里的文件,最多 200 个。 */
673
+ /** 一层最多返回 200 项,total 为截断前的总数。 */
597
674
  export interface JobFilesView {
598
- files: JobFileEntry[]; // 修改时间倒序
599
- hasMore: boolean;
675
+ files: JobFileEntry[];
676
+ total: number;
677
+ fileCount: number;
678
+ directoryCount: number;
600
679
  }
601
680
 
602
681
  export interface Bootstrap {
@@ -609,12 +688,15 @@ export interface Bootstrap {
609
688
  }
610
689
 
611
690
  export type HobEvent =
691
+ | { type: "desktop-control"; screen: string; taken: boolean }
692
+ | { type: "desktop-screens"; screens: DesktopScreen[] }
612
693
  | ({ type: "page-live" } & PageLive)
613
694
  | { type: "assistant"; assistant: AssistantSummary } // 新建或变化
614
695
  | { type: "assistant_removed"; id: string }
615
696
  | { type: "item"; assistantId: string; item: Item } // 新条目,或按 id 替换
616
697
  | { type: "delta"; assistantId: string; itemId: string; text: string } // 流式追加到 assistant 条目
617
698
  | { type: "job"; job: Job }
699
+ | { type: "sediment-finished"; jobId: string }
618
700
  | { type: "delegation"; delegation: Delegation }
619
701
  | { type: "job_step"; jobId: string; step: JobStep } // 新的一步,或按 id 替换(输出流式更新、跑完补结果)
620
702
  | { type: "router-activity"; assistantId: string; activity: RouterActivity | null }
@@ -623,7 +705,9 @@ export type HobEvent =
623
705
  | { type: "draft"; draft: Draft }
624
706
  | { type: "routine"; routine: Routine }
625
707
  | { type: "routine_removed"; id: string }
626
- | { type: "profile"; assistantId: string }; // rules/memories/worklog 变了,前端重拉 profile
708
+ | { type: "profile"; assistantId: string } // rules/memories/worklog 变了,前端重拉 profile
709
+ | { type: "knowledge" } // 知识库有变化,打开着的知识库页面重拉
710
+ | { type: "skills"; assistantId: string }; // 这个助手的技能有变化,打开着的技能列表重拉
627
711
 
628
712
  /** POST /api/assistants/:id/messages 的请求体 */
629
713
  export interface SendMessageBody {
@@ -0,0 +1,10 @@
1
+ # Hob 补丁
2
+
3
+ 来源:`pi-claude-bridge@0.9.3`,MIT,原许可证见 `LICENSE`。
4
+
5
+ - 只读取 Hob 引擎目录的 `claude-bridge.json`,不合并任务工作目录的配置。
6
+ - Claude Code 普通请求和摘要请求都传 `--safe-mode`;摘要请求不再传空的 `settingSources`,因为当前 Claude Code 登录在该参数下不可用。
7
+ - 不通过全局环境设置安全模式,避免影响用户在任务中启动的 Claude Code。
8
+ - Hob 在模型目录初始化时预注册模型,所有会话复用同一个扩展模块;生命周期清理仅影响当前会话,避免并行对话和任务互相清除状态。
9
+ - 不修改 Hob 进程的 Claude Code 环境变量;启用桥调试日志时记录实际追加的系统提示词,供隔离验收。
10
+ - 移除桥直接读取、重建和删除 Claude Code 会话文件的路径。无法直接续接时,以 SDK 消息重放 Hob 自己的历史(包含图片);正常续接仍由 Claude Code 处理。这可能增加重启或上下文改写后的 token 用量。