hob-bot 1.0.2 → 1.0.4
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 +0 -14
- package/README.zh-CN.md +0 -14
- package/dist/web/assets/KaTeX_AMS-Regular-BQhdFMY1.woff2 +0 -0
- package/dist/web/assets/KaTeX_AMS-Regular-DMm9YOAa.woff +0 -0
- package/dist/web/assets/KaTeX_AMS-Regular-DRggAlZN.ttf +0 -0
- package/dist/web/assets/KaTeX_Caligraphic-Bold-ATXxdsX0.ttf +0 -0
- package/dist/web/assets/KaTeX_Caligraphic-Bold-BEiXGLvX.woff +0 -0
- package/dist/web/assets/KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2 +0 -0
- package/dist/web/assets/KaTeX_Caligraphic-Regular-CTRA-rTL.woff +0 -0
- package/dist/web/assets/KaTeX_Caligraphic-Regular-Di6jR-x-.woff2 +0 -0
- package/dist/web/assets/KaTeX_Caligraphic-Regular-wX97UBjC.ttf +0 -0
- package/dist/web/assets/KaTeX_Fraktur-Bold-BdnERNNW.ttf +0 -0
- package/dist/web/assets/KaTeX_Fraktur-Bold-BsDP51OF.woff +0 -0
- package/dist/web/assets/KaTeX_Fraktur-Bold-CL6g_b3V.woff2 +0 -0
- package/dist/web/assets/KaTeX_Fraktur-Regular-CB_wures.ttf +0 -0
- package/dist/web/assets/KaTeX_Fraktur-Regular-CTYiF6lA.woff2 +0 -0
- package/dist/web/assets/KaTeX_Fraktur-Regular-Dxdc4cR9.woff +0 -0
- package/dist/web/assets/KaTeX_Main-Bold-Cx986IdX.woff2 +0 -0
- package/dist/web/assets/KaTeX_Main-Bold-Jm3AIy58.woff +0 -0
- package/dist/web/assets/KaTeX_Main-Bold-waoOVXN0.ttf +0 -0
- package/dist/web/assets/KaTeX_Main-BoldItalic-DxDJ3AOS.woff2 +0 -0
- package/dist/web/assets/KaTeX_Main-BoldItalic-DzxPMmG6.ttf +0 -0
- package/dist/web/assets/KaTeX_Main-BoldItalic-SpSLRI95.woff +0 -0
- package/dist/web/assets/KaTeX_Main-Italic-3WenGoN9.ttf +0 -0
- package/dist/web/assets/KaTeX_Main-Italic-BMLOBm91.woff +0 -0
- package/dist/web/assets/KaTeX_Main-Italic-NWA7e6Wa.woff2 +0 -0
- package/dist/web/assets/KaTeX_Main-Regular-B22Nviop.woff2 +0 -0
- package/dist/web/assets/KaTeX_Main-Regular-Dr94JaBh.woff +0 -0
- package/dist/web/assets/KaTeX_Main-Regular-ypZvNtVU.ttf +0 -0
- package/dist/web/assets/KaTeX_Math-BoldItalic-B3XSjfu4.ttf +0 -0
- package/dist/web/assets/KaTeX_Math-BoldItalic-CZnvNsCZ.woff2 +0 -0
- package/dist/web/assets/KaTeX_Math-BoldItalic-iY-2wyZ7.woff +0 -0
- package/dist/web/assets/KaTeX_Math-Italic-DA0__PXp.woff +0 -0
- package/dist/web/assets/KaTeX_Math-Italic-flOr_0UB.ttf +0 -0
- package/dist/web/assets/KaTeX_Math-Italic-t53AETM-.woff2 +0 -0
- package/dist/web/assets/KaTeX_SansSerif-Bold-CFMepnvq.ttf +0 -0
- package/dist/web/assets/KaTeX_SansSerif-Bold-D1sUS0GD.woff2 +0 -0
- package/dist/web/assets/KaTeX_SansSerif-Bold-DbIhKOiC.woff +0 -0
- package/dist/web/assets/KaTeX_SansSerif-Italic-C3H0VqGB.woff2 +0 -0
- package/dist/web/assets/KaTeX_SansSerif-Italic-DN2j7dab.woff +0 -0
- package/dist/web/assets/KaTeX_SansSerif-Italic-YYjJ1zSn.ttf +0 -0
- package/dist/web/assets/KaTeX_SansSerif-Regular-BNo7hRIc.ttf +0 -0
- package/dist/web/assets/KaTeX_SansSerif-Regular-CS6fqUqJ.woff +0 -0
- package/dist/web/assets/KaTeX_SansSerif-Regular-DDBCnlJ7.woff2 +0 -0
- package/dist/web/assets/KaTeX_Script-Regular-C5JkGWo-.ttf +0 -0
- package/dist/web/assets/KaTeX_Script-Regular-D3wIWfF6.woff2 +0 -0
- package/dist/web/assets/KaTeX_Script-Regular-D5yQViql.woff +0 -0
- package/dist/web/assets/KaTeX_Size1-Regular-C195tn64.woff +0 -0
- package/dist/web/assets/KaTeX_Size1-Regular-Dbsnue_I.ttf +0 -0
- package/dist/web/assets/KaTeX_Size1-Regular-mCD8mA8B.woff2 +0 -0
- package/dist/web/assets/KaTeX_Size2-Regular-B7gKUWhC.ttf +0 -0
- package/dist/web/assets/KaTeX_Size2-Regular-Dy4dx90m.woff2 +0 -0
- package/dist/web/assets/KaTeX_Size2-Regular-oD1tc_U0.woff +0 -0
- package/dist/web/assets/KaTeX_Size3-Regular-CTq5MqoE.woff +0 -0
- package/dist/web/assets/KaTeX_Size3-Regular-DgpXs0kz.ttf +0 -0
- package/dist/web/assets/KaTeX_Size4-Regular-BF-4gkZK.woff +0 -0
- package/dist/web/assets/KaTeX_Size4-Regular-DWFBv043.ttf +0 -0
- package/dist/web/assets/KaTeX_Size4-Regular-Dl5lxZxV.woff2 +0 -0
- package/dist/web/assets/KaTeX_Typewriter-Regular-C0xS9mPB.woff +0 -0
- package/dist/web/assets/KaTeX_Typewriter-Regular-CO6r4hn1.woff2 +0 -0
- package/dist/web/assets/KaTeX_Typewriter-Regular-D3Ib7_Hf.ttf +0 -0
- package/dist/web/assets/index-BCGEZvgg.css +1 -0
- package/dist/web/assets/index-BqkIAlk9.js +258 -0
- package/dist/web/assets/math-C-EwFotM.css +1 -0
- package/dist/web/assets/math-zZ4JA8eM.js +257 -0
- package/dist/web/index.html +2 -2
- package/dist/web/mock/audio.mp3 +0 -0
- package/dist/web/mock/photo.jpg +0 -0
- package/dist/web/mock/photo2.jpg +0 -0
- package/dist/web/mock/video.mp4 +0 -0
- package/dist/web/mock//347/201/253/346/230/237/346/227/245/350/220/275/347/244/272/346/204/217/345/233/276.md +34 -0
- package/package.json +6 -2
- package/server/src/actions.ts +8 -1
- package/server/src/delegations.ts +20 -2
- package/server/src/http.ts +9 -2
- package/server/src/jobs.ts +52 -20
- package/server/src/prompt.ts +2 -0
- package/server/src/push.ts +3 -2
- package/server/src/router-activity.ts +47 -0
- package/server/src/router.ts +53 -7
- package/server/src/settings.ts +2 -1
- package/server/src/thumbs.ts +49 -0
- package/shared/media.ts +6 -0
- package/shared/plain-text.ts +31 -0
- package/shared/sprites.ts +215 -0
- package/shared/types.ts +8 -0
- package/dist/web/assets/index-D88l35PH.css +0 -1
- package/dist/web/assets/index-DUs8nN3E.js +0 -218
package/dist/web/index.html
CHANGED
|
@@ -23,8 +23,8 @@
|
|
|
23
23
|
<meta name="apple-mobile-web-app-title" content="Hob" />
|
|
24
24
|
<meta name="apple-mobile-web-app-status-bar-style" content="default" />
|
|
25
25
|
<title>Hob</title>
|
|
26
|
-
<script type="module" crossorigin src="/assets/index-
|
|
27
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
26
|
+
<script type="module" crossorigin src="/assets/index-BqkIAlk9.js"></script>
|
|
27
|
+
<link rel="stylesheet" crossorigin href="/assets/index-BCGEZvgg.css">
|
|
28
28
|
</head>
|
|
29
29
|
<body>
|
|
30
30
|
<div id="root"></div>
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# 火星日落为什么是蓝色的
|
|
2
|
+
|
|
3
|
+
火星大气很稀薄,但飘着大量**细小的尘埃**。尘埃颗粒的大小和蓝光波长相近,蓝光会被集中散射到太阳周围,所以日落时太阳附近是一圈蓝。
|
|
4
|
+
|
|
5
|
+
## 和地球对比
|
|
6
|
+
|
|
7
|
+
| 项目 | 地球 | 火星 |
|
|
8
|
+
| --- | --- | --- |
|
|
9
|
+
| 大气压 | 1013 hPa | 约 6 hPa |
|
|
10
|
+
| 主要散射体 | 气体分子 | 尘埃颗粒 |
|
|
11
|
+
| 白天天空 | 蓝色 | 黄褐色 |
|
|
12
|
+
| 日落颜色 | 红橙 | **蓝色** |
|
|
13
|
+
|
|
14
|
+
## 要点
|
|
15
|
+
|
|
16
|
+
- 尘埃颗粒直径约 1.5 μm,对蓝光的**前向散射**最强
|
|
17
|
+
- 太阳越低,光穿过的尘埃越多,蓝晕越明显
|
|
18
|
+
- 好奇号在 2015 年拍到过完整过程
|
|
19
|
+
|
|
20
|
+
1. 白天:尘埃把红光散到全天空,天空发黄
|
|
21
|
+
2. 日落:前向散射的蓝光留在太阳周围
|
|
22
|
+
|
|
23
|
+
> 数据来自 NASA 好奇号任务公开资料。
|
|
24
|
+
|
|
25
|
+
### 示意图源码
|
|
26
|
+
|
|
27
|
+
```mermaid
|
|
28
|
+
graph LR
|
|
29
|
+
太阳 --> 尘埃 --> 蓝晕
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
---
|
|
33
|
+
|
|
34
|
+
验证状态:**明确 token 被拒**,`401 Unauthorized`。
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "hob-bot",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.4",
|
|
4
4
|
"description": "A self-hosted personal assistant that runs real work as background jobs.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -37,8 +37,11 @@
|
|
|
37
37
|
"@juicesharp/rpiv-config": "2.15.0",
|
|
38
38
|
"@juicesharp/rpiv-web-tools": "2.15.0",
|
|
39
39
|
"croner": "^9.0.0",
|
|
40
|
+
"github-markdown-css": "^5.9.0",
|
|
40
41
|
"hono": "^4.6.0",
|
|
41
|
-
"
|
|
42
|
+
"katex": "^0.19.1",
|
|
43
|
+
"marked": "^18.1.3",
|
|
44
|
+
"marked-cjk-friendly": "^0.1.3",
|
|
42
45
|
"pi-memory": "0.4.2",
|
|
43
46
|
"react": "^19.0.0",
|
|
44
47
|
"react-dom": "^19.0.0",
|
|
@@ -47,6 +50,7 @@
|
|
|
47
50
|
"ws": "^8.18.0"
|
|
48
51
|
},
|
|
49
52
|
"devDependencies": {
|
|
53
|
+
"@types/katex": "^0.16.8",
|
|
50
54
|
"@types/node": "^22.0.0",
|
|
51
55
|
"@types/react": "^19.0.0",
|
|
52
56
|
"@types/react-dom": "^19.0.0",
|
package/server/src/actions.ts
CHANGED
|
@@ -34,6 +34,9 @@ export async function answerApproval(runtime: HobRuntime, approvalId: string, in
|
|
|
34
34
|
answeredAt: new Date().toISOString(),
|
|
35
35
|
};
|
|
36
36
|
store.saveApproval(answered);
|
|
37
|
+
// 转交里 Router 问的:答案回到目标助手接着办,办完才算转交结束(任务里的提问由任务自己接着跑)
|
|
38
|
+
const delegation = approval.source === "job" ? undefined : store.delegations().find((item) => item.approvalIds?.includes(approval.id));
|
|
39
|
+
const delegationId = delegation && runtime.delegations.resume(delegation.id) ? delegation.id : undefined;
|
|
37
40
|
bus.emit({ type: "approval", approval: answered });
|
|
38
41
|
|
|
39
42
|
if (option?.always) {
|
|
@@ -59,7 +62,11 @@ export async function answerApproval(runtime: HobRuntime, approvalId: string, in
|
|
|
59
62
|
// 回给模型:自己写的那句要说明是用户写的、没选给出的选项
|
|
60
63
|
const answer = option ? `用户选择了:${option.label}` : `用户没有选给出的选项,而是回复:${text}`;
|
|
61
64
|
if (approval.source === "job" && approval.jobId) jobs.resolveAsk(answered, answer);
|
|
62
|
-
else
|
|
65
|
+
else {
|
|
66
|
+
const reply = option ? `用户选择了 ${option.key}:${option.label}` : answer;
|
|
67
|
+
if (delegationId) void router.relay(approval.assistantId, reply, delegationId).catch(() => runtime.delegations.turnEnded(delegationId, "目标助手处理失败", true));
|
|
68
|
+
else await router.answerEvent(approval.assistantId, reply);
|
|
69
|
+
}
|
|
63
70
|
|
|
64
71
|
return { status: 200, body: { approval: answered } };
|
|
65
72
|
}
|
|
@@ -32,7 +32,7 @@ export class DelegationManager {
|
|
|
32
32
|
throw error;
|
|
33
33
|
}
|
|
34
34
|
this.runtime.bus.emit({ type: "delegation", delegation });
|
|
35
|
-
void this.runtime.router.relay(to, text
|
|
35
|
+
void this.runtime.router.relay(to, `转自 Hob:${text}`, delegation.id).catch((error: unknown) => {
|
|
36
36
|
this.turnEnded(delegation.id, error instanceof Error ? error.message : String(error), true);
|
|
37
37
|
});
|
|
38
38
|
return delegation;
|
|
@@ -54,10 +54,26 @@ export class DelegationManager {
|
|
|
54
54
|
turnEnded(id: string, reply: string, failed = false): void {
|
|
55
55
|
const record = this.runtime.store.delegation(id);
|
|
56
56
|
if (!record || record.turnFinished) return;
|
|
57
|
-
|
|
57
|
+
const replies = [record.reply, reply].filter(Boolean).join("\n");
|
|
58
|
+
this.runtime.store.saveDelegation({ ...record, reply: replies, turnFinished: true, ...(failed ? { turnFailed: true } : {}) });
|
|
58
59
|
this.update(id);
|
|
59
60
|
}
|
|
60
61
|
|
|
62
|
+
/**
|
|
63
|
+
* 转交里派的任务结束了、或用户答了转交里的提问:结果先回到目标助手,它这一轮可能接着再派任务、再问用户。
|
|
64
|
+
* 这一轮跑完、名下没有在跑的任务,转交才算结束,Hob 才收到结果;
|
|
65
|
+
* 否则目标助手还在处理时 Hob 就会收到「已结束」,另开任务和它冲突。
|
|
66
|
+
* 调用方要在发出 job/approval 事件之前调用,再用 router.relay 把结果送给目标助手。
|
|
67
|
+
* 返回 false 表示转交已经回流过(或已退回),结果按普通事件处理。
|
|
68
|
+
*/
|
|
69
|
+
resume(id: string): boolean {
|
|
70
|
+
const record = this.runtime.store.delegation(id);
|
|
71
|
+
if (!record || record.reportedAt || record.status === "returned" || record.status === "interrupted") return false;
|
|
72
|
+
const { turnFailed: _failed, ...rest } = record;
|
|
73
|
+
this.runtime.store.saveDelegation({ ...rest, turnFinished: false });
|
|
74
|
+
return true;
|
|
75
|
+
}
|
|
76
|
+
|
|
61
77
|
returnToHob(id: string, text: string, reason: string): void {
|
|
62
78
|
const record = this.runtime.store.delegation(id);
|
|
63
79
|
if (!record || record.turnFinished) throw new Error("转交已结束,无法退回");
|
|
@@ -102,6 +118,8 @@ export class DelegationManager {
|
|
|
102
118
|
for (const record of this.runtime.store.delegations()) {
|
|
103
119
|
if (record.reportedAt || !["running", "waiting", "returned"].includes(record.status)) continue;
|
|
104
120
|
if (record.jobIds.some((id) => { const job = this.runtime.store.job(id); return job && ["running", "queued", "waiting"].includes(job.status); })) continue;
|
|
121
|
+
// 目标助手在等用户选:审批卡重启后还在,答了照常继续,不算中断
|
|
122
|
+
if (record.approvalIds?.some((id) => this.runtime.store.approval(id)?.status === "pending")) continue;
|
|
105
123
|
const now = new Date().toISOString();
|
|
106
124
|
const next: Delegation = { ...record, status: "interrupted", endedAt: now, reportedAt: now };
|
|
107
125
|
this.runtime.store.saveDelegation(next);
|
package/server/src/http.ts
CHANGED
|
@@ -26,6 +26,7 @@ import { answerApproval, createFromDraft, dismissDraft, mergeAssistant } from ".
|
|
|
26
26
|
import { setPassword } from "./config.ts";
|
|
27
27
|
import { newId } from "./ids.ts";
|
|
28
28
|
import { listJobFiles, lookupFile, MAX_TEXT_BYTES } from "./files.ts";
|
|
29
|
+
import { thumbnail } from "./thumbs.ts";
|
|
29
30
|
import { localLoreHome, readLocalLore, readLocalLoreConnection } from "./imports.ts";
|
|
30
31
|
import { checkImageSettings, generateImage, imageConfigured, imageKey, IMAGE_KEY_SECRET } from "./image.ts";
|
|
31
32
|
import { checkAssistantModels, checkModelsPatch } from "./models.ts";
|
|
@@ -494,7 +495,8 @@ export function createApp(runtime: HobRuntime, options: { webDir?: string; home:
|
|
|
494
495
|
files?: string[];
|
|
495
496
|
};
|
|
496
497
|
const text = (body.text ?? "").trim();
|
|
497
|
-
|
|
498
|
+
// 和 Telegram 一样,只发图片 / 文件不写字也行
|
|
499
|
+
if (!text && !body.files?.length) return c.json({ error: "消息内容为空" }, 400);
|
|
498
500
|
const item = await router.send(id, {
|
|
499
501
|
text,
|
|
500
502
|
...(body.quote ? { quote: body.quote } : {}),
|
|
@@ -709,7 +711,7 @@ export function createApp(runtime: HobRuntime, options: { webDir?: string; home:
|
|
|
709
711
|
return c.json({ files: saved.map((item) => item.path), file: saved[0] });
|
|
710
712
|
});
|
|
711
713
|
|
|
712
|
-
app.get("/api/files", (c) => {
|
|
714
|
+
app.get("/api/files", async (c) => {
|
|
713
715
|
const requested = c.req.query("path");
|
|
714
716
|
const lookup = requested ? lookupFile(options.home, requested) : null;
|
|
715
717
|
const nosniff = { "x-content-type-options": "nosniff" };
|
|
@@ -728,6 +730,11 @@ export function createApp(runtime: HobRuntime, options: { webDir?: string; home:
|
|
|
728
730
|
const name = encodeURIComponent(info.name);
|
|
729
731
|
headers["content-disposition"] = `${c.req.query("inline") === "1" ? "inline" : "attachment"}; filename*=UTF-8''${name}`;
|
|
730
732
|
}
|
|
733
|
+
// 对话里的图片只要缩略图(?thumb=1);缩不了 / 不值得缩就给原图
|
|
734
|
+
if (c.req.query("thumb") === "1" && info.kind === "image") {
|
|
735
|
+
const thumb = await thumbnail(options.home, info);
|
|
736
|
+
if (thumb) return c.body(new Uint8Array(thumb.data), 200, { ...nosniff, "content-type": thumb.mime, "cache-control": "private, max-age=604800" });
|
|
737
|
+
}
|
|
731
738
|
// 文本只拉 1 MB:预览面板对超大文件只显示前 1 MB,剩下的走下载
|
|
732
739
|
const whole = readFileSync(info.path);
|
|
733
740
|
const body = info.text && c.req.query("truncate") === "1" ? whole.subarray(0, MAX_TEXT_BYTES) : whole;
|
package/server/src/jobs.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { homedir } from "node:os";
|
|
2
|
-
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import { existsSync, readFileSync, statSync } from "node:fs";
|
|
3
3
|
import { basename, extname } from "node:path";
|
|
4
4
|
import type { AgentSession, SessionManager } from "@earendil-works/pi-coding-agent";
|
|
5
5
|
import type { ImageContent } from "@earendil-works/pi-ai";
|
|
@@ -50,13 +50,17 @@ export function hasImageFiles(files: string[] | undefined): boolean {
|
|
|
50
50
|
return (files ?? []).some((file) => isImagePath(file));
|
|
51
51
|
}
|
|
52
52
|
|
|
53
|
-
/**
|
|
54
|
-
|
|
53
|
+
/** 一张图超过这个大小就不当图片给模型(只留路径),免得把会话撑得太大。 */
|
|
54
|
+
const MAX_IMAGE_BYTES = 10 * 1024 * 1024;
|
|
55
|
+
|
|
56
|
+
/** 附件里的图片读成 pi 的图片块(读不出来、太大就跳过,不把整个回合拖死)。任务和对话共用。 */
|
|
57
|
+
export function imageContents(files: string[] | undefined): ImageContent[] {
|
|
55
58
|
const out: ImageContent[] = [];
|
|
56
59
|
for (const file of files ?? []) {
|
|
57
60
|
const mimeType = IMAGE_TYPES[extname(file).toLowerCase()];
|
|
58
61
|
if (!mimeType) continue;
|
|
59
62
|
try {
|
|
63
|
+
if (statSync(file).size > MAX_IMAGE_BYTES) continue;
|
|
60
64
|
out.push({ type: "image", data: readFileSync(file).toString("base64"), mimeType });
|
|
61
65
|
} catch {
|
|
62
66
|
// 文件没了或读不了:不带图片,靠任务说明的路径自己去读
|
|
@@ -91,6 +95,8 @@ interface WorkerState {
|
|
|
91
95
|
stopped?: boolean;
|
|
92
96
|
/** 正在跑的工具步骤,按 toolCallId 存:输出更新、跑完都按同一个步骤 id 替换 */
|
|
93
97
|
steps: Map<string, JobStep>;
|
|
98
|
+
/** 你补的话已交给任务、还没被读到(任务做完当前这一步才读);读到时换成正式的一行 */
|
|
99
|
+
queued: JobStep[];
|
|
94
100
|
}
|
|
95
101
|
|
|
96
102
|
/** 推给助手的任务结果最多带这么多字的报告。 */
|
|
@@ -166,10 +172,11 @@ export class JobManager {
|
|
|
166
172
|
transcript(jobId: string): JobStep[] {
|
|
167
173
|
const job = this.store.job(jobId);
|
|
168
174
|
if (!job) return [];
|
|
169
|
-
|
|
175
|
+
const steps = deriveSteps(this.store.workerFile(jobId), {
|
|
170
176
|
cwd: job.cwd ?? "",
|
|
171
177
|
save: (stepId, text) => this.store.writeStepOutput(jobId, stepId, text),
|
|
172
178
|
});
|
|
179
|
+
return [...steps, ...(this.workers.get(jobId)?.queued ?? [])];
|
|
173
180
|
}
|
|
174
181
|
|
|
175
182
|
async start(assistantId: string, input: StartJobInput): Promise<Job> {
|
|
@@ -297,6 +304,7 @@ export class JobManager {
|
|
|
297
304
|
askedFollowUp: false,
|
|
298
305
|
lastActivityAt: Date.now(),
|
|
299
306
|
steps: new Map(),
|
|
307
|
+
queued: [],
|
|
300
308
|
};
|
|
301
309
|
this.workers.set(running.id, worker);
|
|
302
310
|
const images = imageContents(running.inputs);
|
|
@@ -345,6 +353,15 @@ export class JobManager {
|
|
|
345
353
|
}
|
|
346
354
|
if (event.type === "message_start" || event.type === "agent_start") {
|
|
347
355
|
worker.lastActivityAt = Date.now();
|
|
356
|
+
if (event.type === "message_start" && event.message.role === "user") {
|
|
357
|
+
// 补的那句被任务读到了:id 和 transcript 推导出来的一样,前端拿它换掉「等待读取」那行
|
|
358
|
+
const text = textOfContent(event.message.content).trim();
|
|
359
|
+
const index = worker.queued.findIndex((step) => step.text === text);
|
|
360
|
+
if (index >= 0) {
|
|
361
|
+
worker.queued.splice(index, 1);
|
|
362
|
+
this.emitStep(worker, { id: `u${event.message.timestamp}`, kind: "text", at: new Date(event.message.timestamp).toISOString(), from: "user", text });
|
|
363
|
+
}
|
|
364
|
+
}
|
|
348
365
|
return;
|
|
349
366
|
}
|
|
350
367
|
if (event.type === "agent_settled") void this.onSettled(worker);
|
|
@@ -535,6 +552,8 @@ export class JobManager {
|
|
|
535
552
|
...(options.files?.length ? { files: options.files } : {}),
|
|
536
553
|
};
|
|
537
554
|
this.store.saveJob(next);
|
|
555
|
+
// 转交里的任务:先把转交重新打开,结果由目标助手接着处理完才回流 Hob(见 DelegationManager.resume)
|
|
556
|
+
const delegationId = !job.routineId && job.delegationId && this.runtime.delegations.resume(job.delegationId) ? job.delegationId : undefined;
|
|
538
557
|
this.bus.emit({ type: "job", job: next });
|
|
539
558
|
pushJobEnd({ push: this.runtime.push, store: this.store }, next);
|
|
540
559
|
this.publishBusy(job.assistantId);
|
|
@@ -565,13 +584,9 @@ export class JobManager {
|
|
|
565
584
|
.catch(() => undefined);
|
|
566
585
|
}
|
|
567
586
|
} else if (assistant) {
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
`任务「${job.title}」${statusText}${eventSummary ? `:${eventSummary}` : options.reason ? `:${options.reason}` : ""}${filesNote}`,
|
|
572
|
-
cont,
|
|
573
|
-
)
|
|
574
|
-
.catch(() => undefined);
|
|
587
|
+
const text = `任务「${job.title}」${statusText}${eventSummary ? `:${eventSummary}` : options.reason ? `:${options.reason}` : ""}${filesNote}`;
|
|
588
|
+
if (delegationId) void this.runtime.router.relay(job.assistantId, text, delegationId, cont).catch(() => this.runtime.delegations.turnEnded(delegationId, "目标助手处理失败", true));
|
|
589
|
+
else await this.runtime.router.event(job.assistantId, text, cont).catch(() => undefined);
|
|
575
590
|
}
|
|
576
591
|
}
|
|
577
592
|
|
|
@@ -605,8 +620,17 @@ export class JobManager {
|
|
|
605
620
|
async reply(jobId: string, text: string): Promise<void> {
|
|
606
621
|
const worker = this.workers.get(jobId);
|
|
607
622
|
if (!worker?.session) throw new Error("这个任务当前未在运行。");
|
|
608
|
-
|
|
609
|
-
|
|
623
|
+
// 先显示出来:任务在跑命令时要等这一步做完才读到,界面上标着「等待读取」
|
|
624
|
+
const step: JobStep = { id: `q${Date.now()}-${worker.queued.length}`, kind: "text", at: new Date().toISOString(), from: "user", text, queued: true };
|
|
625
|
+
worker.queued.push(step);
|
|
626
|
+
this.emitStep(worker, step);
|
|
627
|
+
try {
|
|
628
|
+
if (worker.session.isStreaming) await worker.session.steer(text);
|
|
629
|
+
else void worker.session.prompt(text).catch(() => { worker.queued = worker.queued.filter((item) => item !== step); });
|
|
630
|
+
} catch (error) {
|
|
631
|
+
worker.queued = worker.queued.filter((item) => item !== step);
|
|
632
|
+
throw error;
|
|
633
|
+
}
|
|
610
634
|
}
|
|
611
635
|
|
|
612
636
|
async stop(jobId: string): Promise<void> {
|
|
@@ -670,23 +694,31 @@ export class JobManager {
|
|
|
670
694
|
if (worker) worker.job = next;
|
|
671
695
|
}
|
|
672
696
|
worker?.ask?.(answer);
|
|
673
|
-
if (worker)
|
|
697
|
+
if (worker) {
|
|
698
|
+
worker.ask = undefined;
|
|
699
|
+
worker.lastActivityAt = Date.now();
|
|
700
|
+
}
|
|
674
701
|
}
|
|
675
702
|
|
|
676
703
|
startWatchdog(): void {
|
|
677
704
|
this.watchdog ??= setInterval(() => {
|
|
678
|
-
|
|
679
|
-
for (const [jobId, worker] of this.workers) {
|
|
680
|
-
if (Date.now() - worker.lastActivityAt > idleMs) {
|
|
681
|
-
void this.finish(jobId, "stuck", undefined, `${Math.round(idleMs / 60_000)} 分钟无响应`);
|
|
682
|
-
}
|
|
683
|
-
}
|
|
705
|
+
this.checkStalls();
|
|
684
706
|
// 并发小了以后(设置里改的),排队的任务还在等,这里补一次泵
|
|
685
707
|
this.pump();
|
|
686
708
|
}, 30_000);
|
|
687
709
|
this.watchdog.unref?.();
|
|
688
710
|
}
|
|
689
711
|
|
|
712
|
+
checkStalls(now = Date.now()): void {
|
|
713
|
+
const idleMs = this.runtime.settings.settings.work.jobStallMinutes * 60_000;
|
|
714
|
+
for (const [jobId, worker] of this.workers) {
|
|
715
|
+
// 在等用户答提问不算没响应:等多久都是用户的事
|
|
716
|
+
if (!worker.ask && now - worker.lastActivityAt > idleMs) {
|
|
717
|
+
void this.finish(jobId, "stuck", undefined, `${Math.round(idleMs / 60_000)} 分钟无响应`);
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
|
|
690
722
|
stopWatchdog(): void {
|
|
691
723
|
if (this.watchdog) clearInterval(this.watchdog);
|
|
692
724
|
this.watchdog = undefined;
|
package/server/src/prompt.ts
CHANGED
|
@@ -169,6 +169,8 @@ export function buildSystemPrompt(store: Store, assistantId: string, settings: P
|
|
|
169
169
|
.join(",")};hob 那个火苗是主助手专用的,不要用。`,
|
|
170
170
|
"- 库里没有贴合职责的道具时,可以自己画一件:sprite 填 custom,prop_art 给出 art、pal、dx 画一件 16×16 的像素道具;画得不合适会返回原因,照着改好重发就行。",
|
|
171
171
|
"- 有更合适的助手时用 message_assistant 把任务转过去。",
|
|
172
|
+
"- 转交时把用户原话转过去,只补充对方需要的背景。不要替用户加验收标准、测试方案或额外要求,把小事写成大工程。",
|
|
173
|
+
"- 转交出去的事由对方负责到底。结果回来之前,不要自己 start_job 做同一件事。结果回来后如果没办完,转回同一个助手接着办,或者如实告诉用户。",
|
|
172
174
|
);
|
|
173
175
|
}
|
|
174
176
|
lines.push("- 用户问你在做什么、有什么在运行,可依据任务事件回答,不要编造进度。");
|
package/server/src/push.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
// HOB_HOME/push.json:VAPID 密钥 + 订阅设备(0600、原子写)。推送走 web-push。
|
|
2
|
+
import { plainText } from "../../shared/plain-text.ts";
|
|
2
3
|
import { chmodSync, existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
|
|
3
4
|
import { createRequire } from "node:module";
|
|
4
5
|
import { dirname, join } from "node:path";
|
|
@@ -269,7 +270,7 @@ export function pushApproval(deps: NotifyDeps, approval: Approval): void {
|
|
|
269
270
|
const assistant = deps.store.assistant(approval.assistantId);
|
|
270
271
|
if (!assistant?.notify) return;
|
|
271
272
|
// 推送失败不能影响审批本身(也不能变成未处理的 rejection 把进程带走)
|
|
272
|
-
void deps.push.send({ title: assistant.name, body: approval.title, url: openUrl(approval.assistantId) }).catch(() => undefined);
|
|
273
|
+
void deps.push.send({ title: assistant.name, body: plainText(approval.title), url: openUrl(approval.assistantId) }).catch(() => undefined);
|
|
273
274
|
}
|
|
274
275
|
|
|
275
276
|
/** 任务受阻或失败:所属助手开了通知才推送。 */
|
|
@@ -278,7 +279,7 @@ export function pushJobEnd(deps: NotifyDeps, job: Job): void {
|
|
|
278
279
|
const assistant = deps.store.assistant(job.assistantId);
|
|
279
280
|
if (!assistant?.notify) return;
|
|
280
281
|
const what = job.status === "stuck" ? "受阻" : "失败";
|
|
281
|
-
const reason = job.statusReason ? `:${job.statusReason}` : "";
|
|
282
|
+
const reason = job.statusReason ? `:${plainText(job.statusReason)}` : "";
|
|
282
283
|
void deps.push
|
|
283
284
|
.send({ title: assistant.name, body: `任务「${job.title}」${what}${reason}`, url: openUrl(job.assistantId) })
|
|
284
285
|
.catch(() => undefined);
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { basename } from "node:path";
|
|
2
|
+
|
|
3
|
+
/** 把 Router 工具和流式参数压成界面上的一句话。 */
|
|
4
|
+
export function activityLabel(name: string, args: Record<string, unknown> = {}, assistantName?: (id: string) => string | undefined): string {
|
|
5
|
+
const value = (key: string) => typeof args[key] === "string" ? (args[key] as string).trim() : "";
|
|
6
|
+
const withValue = (label: string, text: string) => text ? `${label}:${text}` : label;
|
|
7
|
+
let label: string;
|
|
8
|
+
switch (name) {
|
|
9
|
+
case "start_job": label = withValue("派任务", value("title")); break;
|
|
10
|
+
case "reply_job": label = "补充任务要求"; break;
|
|
11
|
+
case "stop_job": label = "停止任务"; break;
|
|
12
|
+
case "ask_user": label = "准备问你"; break;
|
|
13
|
+
case "set_rule": case "remove_rule": label = "改规矩"; break;
|
|
14
|
+
case "create_routine": case "update_routine": case "delete_routine": label = withValue("设定时任务", value("name")); break;
|
|
15
|
+
case "send_file": label = withValue("发文件", basename(value("path"))); break;
|
|
16
|
+
case "set_avatar": label = "换头像"; break;
|
|
17
|
+
case "propose_assistant": label = "提议新助手"; break;
|
|
18
|
+
case "message_assistant": { const id = value("assistantId"); label = id ? `交给「${assistantName?.(id) ?? id}」` : "交给助手"; break; }
|
|
19
|
+
case "hand_back_to_hob": label = "交回 Hob"; break;
|
|
20
|
+
case "show_page": label = withValue("写页面", value("title")); break;
|
|
21
|
+
case "web_search": label = withValue("搜索", value("query")); break;
|
|
22
|
+
case "web_fetch": label = "读取网页"; break;
|
|
23
|
+
case "generate_image": label = "画图"; break;
|
|
24
|
+
case "overview": label = "查看工作概况"; break;
|
|
25
|
+
case "read_skill": label = withValue("看技能", value("name")); break;
|
|
26
|
+
default:
|
|
27
|
+
label = /^(memory_write|memory_forget|memory_restore|lore_create_node|lore_update_node|lore_delete_node|lore_move_node)$/.test(name) ? "记下来"
|
|
28
|
+
: /^(memory_|lore_|scratchpad)/.test(name) ? "翻记忆" : "调用工具";
|
|
29
|
+
}
|
|
30
|
+
return [...label].length > 24 ? `${[...label].slice(0, 23).join("")}…` : label;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* 思考中那一行:模型当前思考的最后一段压成一行(照 Pix 的 Codex 模式,只在想的时候显示,不进对话记录)。
|
|
35
|
+
* 最后一段太短(如「写吧。」)就带上前一段;太长就留最新的尾巴,前面用 … 代替。
|
|
36
|
+
*/
|
|
37
|
+
export function thinkingLine(thinking: string): string | undefined {
|
|
38
|
+
const parts = thinking
|
|
39
|
+
.split(/\n+/)
|
|
40
|
+
.map((part) => part.replace(/^\s*(?:[-*#>]+|\d+[.)])\s*/, "").replace(/\*\*|__|`/g, "").replace(/\s+/g, " ").trim())
|
|
41
|
+
.filter(Boolean);
|
|
42
|
+
let line = parts.pop();
|
|
43
|
+
if (!line) return undefined;
|
|
44
|
+
while ([...line].length < 12 && parts.length) line = `${parts.pop()} ${line}`;
|
|
45
|
+
const chars = [...line];
|
|
46
|
+
return chars.length > 110 ? `…${chars.slice(-109).join("")}` : line;
|
|
47
|
+
}
|
package/server/src/router.ts
CHANGED
|
@@ -15,6 +15,7 @@ import type {
|
|
|
15
15
|
PageLive,
|
|
16
16
|
Profile,
|
|
17
17
|
SendMessageBody,
|
|
18
|
+
RouterActivity,
|
|
18
19
|
} from "../../shared/types.ts";
|
|
19
20
|
import type { HobRuntime } from "./runtime.ts";
|
|
20
21
|
import { refreshSystemPrompt } from "./pi.ts";
|
|
@@ -23,6 +24,8 @@ import { buildRouterTools } from "./router-tools.ts";
|
|
|
23
24
|
import { MEMORY_WRITE_TOOLS, memoryNoteText } from "./memory.ts";
|
|
24
25
|
import { buildSystemPrompt, type PromptModel } from "./prompt.ts";
|
|
25
26
|
import { availableSkills } from "./skills.ts";
|
|
27
|
+
import { activityLabel, thinkingLine } from "./router-activity.ts";
|
|
28
|
+
import { imageContents } from "./jobs.ts";
|
|
26
29
|
|
|
27
30
|
export const ITEM_TYPE = "hob.item";
|
|
28
31
|
export const USER_TYPE = "hob.user";
|
|
@@ -46,6 +49,11 @@ interface RouterState {
|
|
|
46
49
|
idleTimer?: NodeJS.Timeout;
|
|
47
50
|
/** 还有几轮在跑(事件那一轮也要算),用来判断忙闲和等空闲 */
|
|
48
51
|
running: number;
|
|
52
|
+
activity?: RouterActivity | null;
|
|
53
|
+
activityJson?: string;
|
|
54
|
+
activityLast?: number;
|
|
55
|
+
/** 这次思考到目前为止的原文:只用来取最后一段给状态行 */
|
|
56
|
+
thinkingText?: string;
|
|
49
57
|
/** 正在流式的助手消息:条目 id 和 GET /items 推导出来的一致;shown 表示已经在对话里露过面 */
|
|
50
58
|
streaming?: { itemId: string; shown: boolean };
|
|
51
59
|
/** 这一轮最后一次有动静的时间,看门狗用它判断“没响应” */
|
|
@@ -306,9 +314,22 @@ export class RouterManager {
|
|
|
306
314
|
*/
|
|
307
315
|
private onSessionEvent(state: RouterState, assistantId: string, event: AgentSessionEvent): void {
|
|
308
316
|
this.progress(state);
|
|
317
|
+
if (event.type === "agent_start" || (event.type === "message_update" && event.assistantMessageEvent.type === "thinking_start")) {
|
|
318
|
+
state.thinkingText = "";
|
|
319
|
+
this.setActivity(state, { state: "thinking", label: "正在想…" });
|
|
320
|
+
}
|
|
321
|
+
if (event.type === "message_update" && (event.assistantMessageEvent.type === "thinking_delta" || event.assistantMessageEvent.type === "thinking_end")) {
|
|
322
|
+
if (event.assistantMessageEvent.type === "thinking_delta") state.thinkingText = (state.thinkingText ?? "") + event.assistantMessageEvent.delta;
|
|
323
|
+
// 节流;思考结束时补发一次完整的最后一段
|
|
324
|
+
if (event.assistantMessageEvent.type === "thinking_end" || Date.now() - (state.activityLast ?? 0) >= 200) {
|
|
325
|
+
this.setActivity(state, { state: "thinking", label: thinkingLine(state.thinkingText ?? "") ?? "正在想…" });
|
|
326
|
+
state.activityLast = Date.now();
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
if (event.type === "agent_end" || (event.type === "message_end" && event.message.role === "assistant" && ["error", "aborted"].includes(event.message.stopReason))) this.setActivity(state, null);
|
|
309
330
|
if (event.type === "message_start" && event.message.role === "custom" && event.message.customType === EVENT_TYPE) {
|
|
310
331
|
const details = event.message.details as EventDetails | undefined;
|
|
311
|
-
if (details?.delegationId) {
|
|
332
|
+
if (details?.delegationId && details.delegationId !== state.currentDelegationId) {
|
|
312
333
|
state.currentDelegationId = details.delegationId;
|
|
313
334
|
state.delegationReply = [];
|
|
314
335
|
state.terminatingTools = new Set();
|
|
@@ -320,7 +341,7 @@ export class RouterManager {
|
|
|
320
341
|
const failed = event.message.stopReason === "error" || event.message.stopReason === "aborted";
|
|
321
342
|
if (failed || !event.toolResults.length || event.toolResults.every((tool) => state.terminatingTools?.has(tool.toolCallId))) {
|
|
322
343
|
const id = state.currentDelegationId;
|
|
323
|
-
const reply = state.delegationReply?.join("\n") || (failed ? event.message.errorMessage || "目标助手处理失败" : "
|
|
344
|
+
const reply = state.delegationReply?.join("\n") || (failed ? event.message.errorMessage || "目标助手处理失败" : "");
|
|
324
345
|
state.currentDelegationId = undefined;
|
|
325
346
|
state.delegationReply = undefined;
|
|
326
347
|
state.terminatingTools = undefined;
|
|
@@ -349,6 +370,15 @@ export class RouterManager {
|
|
|
349
370
|
if (event.type === "message_update" && (event.assistantMessageEvent.type === "toolcall_start" || event.assistantMessageEvent.type === "toolcall_delta" || event.assistantMessageEvent.type === "toolcall_end")) {
|
|
350
371
|
const update = event.assistantMessageEvent;
|
|
351
372
|
const part = update.partial.content[update.contentIndex];
|
|
373
|
+
if (part?.type === "toolCall") {
|
|
374
|
+
if (update.type === "toolcall_start") state.activityJson = "";
|
|
375
|
+
if (update.type === "toolcall_delta") state.activityJson = (state.activityJson ?? "") + update.delta;
|
|
376
|
+
if (update.type !== "toolcall_delta" || Date.now() - (state.activityLast ?? 0) >= 200) {
|
|
377
|
+
const args = Object.keys(part.arguments).length ? part.arguments : parseStreamingJson(state.activityJson);
|
|
378
|
+
this.setActivity(state, { state: "tool", label: activityLabel(part.name, args, (id) => this.store.assistant(id)?.name) });
|
|
379
|
+
state.activityLast = Date.now();
|
|
380
|
+
}
|
|
381
|
+
}
|
|
352
382
|
if (part?.type === "toolCall" && part.name === "show_page") {
|
|
353
383
|
if (update.type === "toolcall_start") this.liveJson.set(part.id, "");
|
|
354
384
|
if (update.type === "toolcall_delta") this.liveJson.set(part.id, (this.liveJson.get(part.id) ?? "") + update.delta);
|
|
@@ -361,6 +391,7 @@ export class RouterManager {
|
|
|
361
391
|
for (const [id, live] of this.livePages.get(assistantId) ?? []) if (live.page.state === "writing") this.liveFinish(assistantId, id, undefined, reason || "页面生成中断");
|
|
362
392
|
}
|
|
363
393
|
if (event.type === "message_update" && event.assistantMessageEvent.type === "text_delta") {
|
|
394
|
+
this.setActivity(state, null);
|
|
364
395
|
state.streaming ??= { itemId: `a-${this.assistantCount(assistantId) + 1}`, shown: false };
|
|
365
396
|
const streaming = state.streaming;
|
|
366
397
|
if (!streaming.shown) {
|
|
@@ -417,9 +448,16 @@ export class RouterManager {
|
|
|
417
448
|
).length;
|
|
418
449
|
}
|
|
419
450
|
|
|
451
|
+
private setActivity(state: RouterState, activity: RouterActivity | null): void {
|
|
452
|
+
if (state.activity?.state === activity?.state && state.activity?.label === activity?.label) return;
|
|
453
|
+
state.activity = activity;
|
|
454
|
+
this.bus.emit({ type: "router-activity", assistantId: state.assistantId, activity });
|
|
455
|
+
}
|
|
456
|
+
|
|
420
457
|
/** 一轮开始了:清掉上一轮的“已停”标记并起看门狗。 */
|
|
421
458
|
private armStall(state: RouterState): void {
|
|
422
459
|
state.stalled = false;
|
|
460
|
+
if (state.running === 1 && !state.session?.isStreaming) this.setActivity(state, { state: "thinking", label: "正在想…" });
|
|
423
461
|
this.progress(state);
|
|
424
462
|
}
|
|
425
463
|
|
|
@@ -449,6 +487,7 @@ export class RouterManager {
|
|
|
449
487
|
}
|
|
450
488
|
if (state.running === 0 && !state.session?.isStreaming) return;
|
|
451
489
|
state.stalled = true;
|
|
490
|
+
this.setActivity(state, null);
|
|
452
491
|
// 不 await:模型的那次请求可能收不了尾,但这行提示必须先出来
|
|
453
492
|
void state.session?.abort().catch(() => undefined);
|
|
454
493
|
const assistant = this.store.assistant(state.assistantId);
|
|
@@ -567,6 +606,7 @@ export class RouterManager {
|
|
|
567
606
|
return false;
|
|
568
607
|
} finally {
|
|
569
608
|
state.running = Math.max(0, state.running - 1);
|
|
609
|
+
if (!state.running) this.setActivity(state, null);
|
|
570
610
|
this.clearStall(state);
|
|
571
611
|
this.publishBusy(state);
|
|
572
612
|
this.maybeRelease(state);
|
|
@@ -666,7 +706,7 @@ export class RouterManager {
|
|
|
666
706
|
const lines = [body.text];
|
|
667
707
|
if (body.quote) lines.unshift(`(引用我之前的话:「${body.quote.text}」)`);
|
|
668
708
|
if (body.files?.length) lines.push(`附件:${body.files.join("、")}`);
|
|
669
|
-
const promptText = lines.join("\n");
|
|
709
|
+
const promptText = lines.filter(Boolean).join("\n");
|
|
670
710
|
|
|
671
711
|
this.touch(assistantId);
|
|
672
712
|
state.running += 1;
|
|
@@ -676,15 +716,18 @@ export class RouterManager {
|
|
|
676
716
|
try {
|
|
677
717
|
await this.applyConversationModel(state);
|
|
678
718
|
refreshSystemPrompt(state.session!);
|
|
679
|
-
|
|
719
|
+
// 对话模型能看图就把图片直接给它(路径照样写在「附件」里,派任务时还能交出去);看不了图就只有路径
|
|
720
|
+
const images = state.session!.model?.input.includes("image") ? imageContents(body.files) : [];
|
|
721
|
+
if (state.session!.isStreaming) await state.session!.steer(promptText, images.length ? images : undefined);
|
|
680
722
|
else {
|
|
681
|
-
await state.session!.prompt(promptText);
|
|
723
|
+
await state.session!.prompt(promptText, images.length ? { images } : undefined);
|
|
682
724
|
await this.reportTurnError(state);
|
|
683
725
|
}
|
|
684
726
|
} catch (error) {
|
|
685
727
|
await this.fail(state, error);
|
|
686
728
|
} finally {
|
|
687
729
|
state.running = Math.max(0, state.running - 1);
|
|
730
|
+
if (!state.running) this.setActivity(state, null);
|
|
688
731
|
this.clearStall(state);
|
|
689
732
|
this.publishBusy(state);
|
|
690
733
|
this.maybeRelease(state);
|
|
@@ -696,6 +739,7 @@ export class RouterManager {
|
|
|
696
739
|
|
|
697
740
|
async stop(assistantId: string): Promise<void> {
|
|
698
741
|
const state = this.routers.get(assistantId);
|
|
742
|
+
if (state) this.setActivity(state, null);
|
|
699
743
|
if (state?.session) await state.session.abort();
|
|
700
744
|
}
|
|
701
745
|
|
|
@@ -790,6 +834,7 @@ export class RouterManager {
|
|
|
790
834
|
...(last ? { last } : {}),
|
|
791
835
|
unread: this.unread(assistant.id),
|
|
792
836
|
busy: this.isBusy(assistant.id),
|
|
837
|
+
activity: this.routers.get(assistant.id)?.activity ?? null,
|
|
793
838
|
};
|
|
794
839
|
}
|
|
795
840
|
|
|
@@ -827,8 +872,9 @@ export class RouterManager {
|
|
|
827
872
|
}
|
|
828
873
|
|
|
829
874
|
/** 直接走事件的 followUp 队列,实际收到这条消息时才开始关联任务。 */
|
|
830
|
-
|
|
831
|
-
|
|
875
|
+
/** 把事件当作转交的一轮送给目标助手;这一轮没正常收尾就把转交记为受阻,不会一直挂着。 */
|
|
876
|
+
async relay(targetAssistantId: string, text: string, delegationId: string, details?: EventDetails): Promise<void> {
|
|
877
|
+
const success = await this.pushEvent(targetAssistantId, text, { ...details, delegationId });
|
|
832
878
|
if (!success || !this.store.delegation(delegationId)?.turnFinished) {
|
|
833
879
|
this.runtime.delegations.turnEnded(delegationId, "目标助手处理失败或转交被中断", true);
|
|
834
880
|
}
|
package/server/src/settings.ts
CHANGED
|
@@ -84,7 +84,7 @@ export function systemTimezone(): string {
|
|
|
84
84
|
|
|
85
85
|
export function defaultSettings(timezone = systemTimezone(), models: ModelSettings = seedModels()): Settings {
|
|
86
86
|
return {
|
|
87
|
-
general: { theme: "system", timezone, showPage: true, aboutYou: { name: "", language: "", notes: "" } },
|
|
87
|
+
general: { theme: "system", timezone, showPage: true, avatarBackground: true, aboutYou: { name: "", language: "", notes: "" } },
|
|
88
88
|
providers: [],
|
|
89
89
|
models,
|
|
90
90
|
image: { source: "off" },
|
|
@@ -201,6 +201,7 @@ function normalize(raw: unknown, base: Settings): Settings {
|
|
|
201
201
|
theme: THEMES.includes(theme) ? theme : base.general.theme,
|
|
202
202
|
timezone: checkTimezone(text(general.timezone, "") || base.general.timezone),
|
|
203
203
|
showPage: typeof general.showPage === "boolean" ? general.showPage : base.general.showPage !== false,
|
|
204
|
+
avatarBackground: typeof general.avatarBackground === "boolean" ? general.avatarBackground : base.general.avatarBackground !== false,
|
|
204
205
|
aboutYou: {
|
|
205
206
|
name: text(about.name, base.general.aboutYou.name),
|
|
206
207
|
language: text(about.language, base.general.aboutYou.language),
|