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.
Files changed (86) hide show
  1. package/README.md +75 -5
  2. package/README.zh-CN.md +75 -5
  3. package/dist/web/assets/DesktopView-rVxacveF.js +5 -0
  4. package/dist/web/assets/index-B-8ISOr1.js +267 -0
  5. package/dist/web/assets/index-Cu1Pf8yX.css +1 -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/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
  13. package/package.json +10 -2
  14. package/server/src/actions.ts +24 -57
  15. package/server/src/assistant-skills.ts +201 -0
  16. package/server/src/boot.ts +1 -0
  17. package/server/src/claude-status.ts +47 -0
  18. package/server/src/config.ts +3 -1
  19. package/server/src/delegations.ts +40 -12
  20. package/server/src/desktop-control.ts +152 -0
  21. package/server/src/desktop-events.ts +63 -0
  22. package/server/src/desktop-public.ts +43 -0
  23. package/server/src/desktop-tools.ts +161 -0
  24. package/server/src/desktop.ts +76 -0
  25. package/server/src/embedding.ts +52 -0
  26. package/server/src/files.ts +63 -43
  27. package/server/src/http.ts +334 -15
  28. package/server/src/jobs.ts +330 -59
  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 +1 -1
  38. package/server/src/pages.ts +1 -1
  39. package/server/src/pi.ts +92 -16
  40. package/server/src/prompt.ts +46 -154
  41. package/server/src/providers.ts +3 -2
  42. package/server/src/push.ts +42 -7
  43. package/server/src/router-activity.ts +4 -0
  44. package/server/src/router-tools.ts +132 -38
  45. package/server/src/router.ts +159 -19
  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/thumbs.ts +49 -0
  51. package/server/src/transcript.ts +57 -4
  52. package/server/src/worker-tools.ts +14 -12
  53. package/shared/knowledge.ts +92 -0
  54. package/shared/mcp.ts +91 -0
  55. package/shared/media.ts +6 -0
  56. package/shared/plain-text.ts +31 -0
  57. package/shared/skills.ts +18 -0
  58. package/shared/sprites.ts +198 -0
  59. package/shared/types.ts +105 -20
  60. package/vendor/claude-bridge/HOB-PATCH.md +10 -0
  61. package/vendor/claude-bridge/LICENSE +21 -0
  62. package/vendor/claude-bridge/package.json +62 -0
  63. package/vendor/claude-bridge/src/agents-md.ts +14 -0
  64. package/vendor/claude-bridge/src/askclaude-schema.ts +92 -0
  65. package/vendor/claude-bridge/src/askclaude-ui.ts +90 -0
  66. package/vendor/claude-bridge/src/attachments.ts +135 -0
  67. package/vendor/claude-bridge/src/config.ts +105 -0
  68. package/vendor/claude-bridge/src/convert.ts +222 -0
  69. package/vendor/claude-bridge/src/extract-tool-results.ts +47 -0
  70. package/vendor/claude-bridge/src/index.ts +2503 -0
  71. package/vendor/claude-bridge/src/log-paths.ts +19 -0
  72. package/vendor/claude-bridge/src/mcp-server.ts +169 -0
  73. package/vendor/claude-bridge/src/models.ts +164 -0
  74. package/vendor/claude-bridge/src/prompt-capture.ts +420 -0
  75. package/vendor/claude-bridge/src/prompt-stream.ts +102 -0
  76. package/vendor/claude-bridge/src/query-state.ts +118 -0
  77. package/vendor/claude-bridge/src/rate-limit.ts +48 -0
  78. package/vendor/claude-bridge/src/session-cwd.ts +38 -0
  79. package/vendor/claude-bridge/src/session-verify.ts +38 -0
  80. package/vendor/claude-bridge/src/skills.ts +20 -0
  81. package/vendor/claude-bridge/src/transcript.ts +86 -0
  82. package/vendor/claude-bridge/src/usage.ts +53 -0
  83. package/vendor/lore-pi-extension/package.json +1 -1
  84. package/vendor/lore-pi-extension/tools.ts +8 -5
  85. package/dist/web/assets/index-C72gAwgN.css +0 -1
  86. package/dist/web/assets/index-Dyw5G8Pg.js +0 -230
@@ -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/media.ts CHANGED
@@ -15,3 +15,9 @@ export function isImagePath(path: string): boolean {
15
15
  if (dot === -1) return false;
16
16
  return path.slice(dot).toLowerCase() in IMAGE_TYPES;
17
17
  }
18
+
19
+ /** 上传的附件存成 u-xxxxxx-原名(server 的 /api/uploads),给人看时去掉前面的编号。 */
20
+ export function attachmentName(path: string): string {
21
+ const base = path.split("/").pop() ?? path;
22
+ return base.replace(/^u-[a-z0-9]{6}-/, "");
23
+ }
@@ -0,0 +1,31 @@
1
+ // 把 Markdown 收成一行纯文字:首页列表的预览、引用条、推送通知都用它,不显示 ** # ` 这些记号。
2
+ // 只去记号、留内容;蛇形命名里的下划线(snake_case)这种不在词边上的不动。
3
+
4
+ export function plainText(md: string): string {
5
+ return (
6
+ md
7
+ // 代码块:留代码,去围栏和语言名
8
+ .replace(/```[^\n]*\n?([\s\S]*?)```/g, "$1")
9
+ // 图片留说明文字,链接留链接文字
10
+ .replace(/!\[([^\]]*)\]\([^)]*\)/g, "$1")
11
+ .replace(/\[([^\]]+)\]\([^)]*\)/g, "$1")
12
+ // 行首:标题、引用、列表记号、任务框
13
+ .replace(/^[ \t]*#{1,6}[ \t]+/gm, "")
14
+ .replace(/^[ \t]*>[ \t]?/gm, "")
15
+ .replace(/^[ \t]*(?:[-*+]|\d+[.)])[ \t]+(?:\[[ xX]\][ \t]+)?/gm, "")
16
+ // 表格:分隔行整行去掉,竖线换成空格
17
+ .replace(/^[ \t]*\|?[ \t]*:?-{3,}:?[ \t]*(?:\|[ \t]*:?-{3,}:?[ \t]*)*\|?[ \t]*$/gm, "")
18
+ .replace(/[ \t]*\|[ \t]*/g, " ")
19
+ // 分隔线
20
+ .replace(/^[ \t]*(?:[-*_][ \t]*){3,}$/gm, "")
21
+ // 粗体、斜体、删除线、行内代码
22
+ .replace(/(\*\*|__)(?=\S)([\s\S]*?\S)\1/g, "$2")
23
+ .replace(/~~(?=\S)([\s\S]*?\S)~~/g, "$1")
24
+ .replace(/(^|[^\w*])\*(?=\S)([^*\n]*?\S)\*(?![\w*])/g, "$1$2")
25
+ .replace(/(^|[^\w])_(?=\S)([^_\n]*?\S)_(?!\w)/g, "$1$2")
26
+ .replace(/`([^`\n]+)`/g, "$1")
27
+ // 多行收成一行
28
+ .replace(/\s+/g, " ")
29
+ .trim()
30
+ );
31
+ }
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/sprites.ts CHANGED
@@ -275,6 +275,204 @@ export const PROPS = {
275
275
  pal: { r: "#ff3b5c" },
276
276
  art: at(9, ["...krrrrrrrrk...", "..krrrrrrrrrrkr.", "...............r"]),
277
277
  },
278
+ // 软件工程和敏捷开发里的角色
279
+ sticky: {
280
+ label: "需求/产品负责人",
281
+ color: "purple",
282
+ dx: 0,
283
+ pal: { n: "#ffe066", d: "#c9a227" },
284
+ art: at(9, [
285
+ ".........kkkkkkk",
286
+ ".........knnnnnk",
287
+ ".........kdddnnk",
288
+ ".........knnnnnk",
289
+ ".........kddddnk",
290
+ ".........knnnnkk",
291
+ ".........kkkkkk.",
292
+ ]),
293
+ },
294
+ timer: {
295
+ label: "迭代/敏捷教练",
296
+ color: "teal",
297
+ dx: 0,
298
+ pal: { w: "#ffffff", r: "#ff5a3c" },
299
+ art: at(8, [
300
+ "...........kkk..",
301
+ "............k...",
302
+ "..........kkkkk.",
303
+ ".........kwwrwwk",
304
+ ".........kwwrwwk",
305
+ ".........kwwkkwk",
306
+ ".........kwwwwwk",
307
+ "..........kkkkk.",
308
+ ]),
309
+ },
310
+ bug: {
311
+ label: "测试/质量",
312
+ color: "green",
313
+ dx: 0,
314
+ pal: { r: "#ff4d4d" },
315
+ art: at(8, [
316
+ "..........k...k.",
317
+ "...........kkk..",
318
+ "..........krrrk.",
319
+ ".........krkrkrk",
320
+ ".........krrrrrk",
321
+ ".........krkrkrk",
322
+ "..........krrrk.",
323
+ "...........kkk..",
324
+ ]),
325
+ },
326
+ magnifier: {
327
+ label: "代码审查/调研",
328
+ color: "blue",
329
+ dx: 0,
330
+ pal: { l: "#cfe8ff", w: "#ffffff", h: "#8a5a3c" },
331
+ art: at(8, [
332
+ "..........kkk...",
333
+ ".........kwllk..",
334
+ ".........kllllk.",
335
+ ".........kllllk.",
336
+ "..........kkkk..",
337
+ ".............hh.",
338
+ "..............hh",
339
+ ]),
340
+ },
341
+ blocks: {
342
+ label: "架构/系统设计",
343
+ color: "slate",
344
+ dx: 0,
345
+ pal: { a: "#ff8b6a", b: "#4ea3ff", c: "#ffc21a" },
346
+ art: at(9, [
347
+ "...........kkkk.",
348
+ "...........kaak.",
349
+ "...........kaak.",
350
+ ".........kkkkkkk",
351
+ ".........kbbkcck",
352
+ ".........kbbkcck",
353
+ ".........kkkkkkk",
354
+ ]),
355
+ },
356
+ rocket: {
357
+ label: "发布/部署",
358
+ color: "purple",
359
+ dx: 0,
360
+ pal: { w: "#e8edf3", r: "#ff5a3c", b: "#4ea3ff", f: "#ffb020" },
361
+ art: at(6, [
362
+ "............k...",
363
+ "...........krk..",
364
+ "...........krk..",
365
+ "..........kwwwk.",
366
+ "..........kwbwk.",
367
+ "..........kwwwk.",
368
+ "..........kwwwk.",
369
+ ".........krwwwrk",
370
+ ".........kkkkkkk",
371
+ "...........kfk..",
372
+ ]),
373
+ },
374
+ shield: {
375
+ label: "安全/权限",
376
+ color: "blue",
377
+ dx: 0,
378
+ pal: { s: "#5b8def", w: "#ffffff" },
379
+ art: at(9, [
380
+ ".........kkkkkkk",
381
+ ".........kssswsk",
382
+ ".........kwswssk",
383
+ ".........kswsssk",
384
+ "..........ksssk.",
385
+ "...........ksk..",
386
+ "............k...",
387
+ ]),
388
+ },
389
+ database: {
390
+ label: "数据库/数据",
391
+ color: "teal",
392
+ dx: 0,
393
+ pal: { d: "#5fa8ff", t: "#cfe6ff" },
394
+ art: at(8, [
395
+ "..........kkkkk.",
396
+ ".........ktttttk",
397
+ ".........kkkkkkk",
398
+ ".........kdddddk",
399
+ ".........kkkkkkk",
400
+ ".........kdddddk",
401
+ "..........kkkkk.",
402
+ ]),
403
+ },
404
+ palette: {
405
+ label: "界面/视觉设计",
406
+ color: "pink",
407
+ dx: 0,
408
+ pal: { p: "#f2c58f", a: "#ff4d4d", b: "#4ea3ff", c: "#5ccf6b" },
409
+ art: at(9, [
410
+ "..........kkkkk.",
411
+ ".........kpppppk",
412
+ ".........kpapbpk",
413
+ ".........kppppk.",
414
+ ".........kpcppk.",
415
+ "..........kkkk..",
416
+ ]),
417
+ },
418
+ siren: {
419
+ label: "值班/告警",
420
+ color: "red",
421
+ dx: 2,
422
+ pal: { r: "#ff3b3b", w: "#ffd0d0" },
423
+ art: at(1, [
424
+ "..k...kkkk...k..",
425
+ "...k.krwrrk.k...",
426
+ ".....krrrrk.....",
427
+ "....kkkkkkkk....",
428
+ ]),
429
+ },
430
+ kanban: {
431
+ label: "项目管理/看板",
432
+ color: "green",
433
+ dx: 0,
434
+ pal: { w: "#ffffff", a: "#ff8b6a", b: "#ffc21a", c: "#5ccf6b" },
435
+ art: at(9, [
436
+ ".........kkkkkkk",
437
+ ".........kakbkck",
438
+ ".........kakwkck",
439
+ ".........kwkbkwk",
440
+ ".........kwkwkwk",
441
+ ".........kkkkkkk",
442
+ ]),
443
+ },
444
+ wrench: {
445
+ label: "维护/修复",
446
+ color: "slate",
447
+ dx: 0,
448
+ pal: { g: "#b9c0c8" },
449
+ art: at(8, [
450
+ "............k.k.",
451
+ "...........kgkgk",
452
+ "...........kgggk",
453
+ "..........kgggk.",
454
+ ".........kggk...",
455
+ "........kggk....",
456
+ "........kgk.....",
457
+ "........kk......",
458
+ ]),
459
+ },
460
+ bolt: {
461
+ label: "性能/优化",
462
+ color: "purple",
463
+ dx: 0,
464
+ pal: { b: "#ffc21a" },
465
+ art: at(8, [
466
+ "............kkk.",
467
+ "...........kbbk.",
468
+ "..........kbbk..",
469
+ ".........kbbbbk.",
470
+ "..........kkbbk.",
471
+ "...........kbk..",
472
+ "..........kbk...",
473
+ "..........kk....",
474
+ ]),
475
+ },
278
476
  } satisfies Record<string, Prop>;
279
477
  export type PropId = keyof typeof PROPS;
280
478