hob-bot 1.2.0 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -4
- package/README.zh-CN.md +4 -4
- package/dist/web/assets/DesktopView-lixZfbYr.js +5 -0
- package/dist/web/assets/index-BZhge0qe.js +267 -0
- package/dist/web/assets/index-DIo1ArRa.css +1 -0
- package/dist/web/index.html +2 -2
- package/package.json +1 -1
- package/server/src/desktop-tools.ts +45 -10
- package/server/src/http.ts +32 -2
- package/server/src/jobs.ts +18 -49
- package/server/src/now.ts +1 -1
- package/server/src/overview.ts +1 -1
- package/server/src/pages.ts +3 -2
- package/server/src/prompt.ts +68 -5
- package/server/src/router-activity.ts +7 -0
- package/server/src/router-tools.ts +20 -11
- package/server/src/router.ts +91 -40
- package/server/src/store.ts +20 -0
- package/server/src/worker-tools.ts +3 -10
- package/shared/skills.ts +3 -1
- package/shared/types.ts +2 -0
- package/dist/web/assets/DesktopView-CEBJFAIN.js +0 -5
- package/dist/web/assets/index-Di5lyTHO.css +0 -1
- package/dist/web/assets/index-y09i1RGd.js +0 -267
package/server/src/router.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { getSupportedThinkingLevels } from "@earendil-works/pi-ai";
|
|
2
2
|
import { parseStreamingJson } from "@earendil-works/pi-ai/utils/json-parse";
|
|
3
|
-
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { existsSync, readFileSync, statSync, writeFileSync } from "node:fs";
|
|
4
4
|
import { mkdirSync } from "node:fs";
|
|
5
5
|
import { dirname, join, relative, resolve, sep } from "node:path";
|
|
6
6
|
import type { AgentSession, AgentSessionEvent, SessionEntry, SessionManager } from "@earendil-works/pi-coding-agent";
|
|
@@ -30,6 +30,7 @@ import { activityLabel, thinkingLine } from "./router-activity.ts";
|
|
|
30
30
|
import { imageContents } from "./jobs.ts";
|
|
31
31
|
import { nowBlock } from "./now.ts";
|
|
32
32
|
import { monthDayTimeText } from "./time.ts";
|
|
33
|
+
import { DesktopToolSession } from "./desktop-tools.ts";
|
|
33
34
|
|
|
34
35
|
export const ITEM_TYPE = "hob.item";
|
|
35
36
|
export const ITEM_UPDATE_TYPE = "hob.item.update";
|
|
@@ -65,7 +66,7 @@ interface RouterState {
|
|
|
65
66
|
/** 这一轮最后一次有动静的时间,看门狗用它判断“没响应” */
|
|
66
67
|
lastEventAt: number;
|
|
67
68
|
stallTimer?: NodeJS.Timeout;
|
|
68
|
-
/**
|
|
69
|
+
/** 这一轮已经被叫停(没响应,或用户按了停止):之后的报错不再写「回复失败」 */
|
|
69
70
|
stalled: boolean;
|
|
70
71
|
/** 记忆提供者换了:跑着的这一轮结束就释放会话,下一轮按新提供者重新打开 */
|
|
71
72
|
releaseAfterTurn?: boolean;
|
|
@@ -78,6 +79,7 @@ interface RouterState {
|
|
|
78
79
|
knowledgeNames?: string[];
|
|
79
80
|
delegationReply?: string[];
|
|
80
81
|
terminatingTools?: Set<string>;
|
|
82
|
+
desktop?: DesktopToolSession;
|
|
81
83
|
}
|
|
82
84
|
|
|
83
85
|
function readEntries(file: string): SessionEntry[] {
|
|
@@ -186,6 +188,7 @@ export function deriveItems(entries: SessionEntry[]): Item[] {
|
|
|
186
188
|
|
|
187
189
|
export class RouterManager {
|
|
188
190
|
private routers = new Map<string, RouterState>();
|
|
191
|
+
private itemCache = new Map<string, { manager?: SessionManager; version: string; result: ReturnType<typeof walk>; positions: Map<string, number>; assistantTimes: string[]; last?: { text: string; at: Iso } }>();
|
|
189
192
|
private livePages = new Map<string, Map<string, { page: PageLive; timer?: NodeJS.Timeout; last: number }>>();
|
|
190
193
|
private liveJson = new Map<string, string>();
|
|
191
194
|
|
|
@@ -250,6 +253,37 @@ export class RouterManager {
|
|
|
250
253
|
return readEntries(this.store.routerFile(assistantId));
|
|
251
254
|
}
|
|
252
255
|
|
|
256
|
+
private derived(assistantId: string) {
|
|
257
|
+
const manager = this.routers.get(assistantId)?.manager;
|
|
258
|
+
const file = this.store.routerFile(assistantId);
|
|
259
|
+
let version: string;
|
|
260
|
+
if (manager) version = String(manager.getEntryCount());
|
|
261
|
+
else {
|
|
262
|
+
try {
|
|
263
|
+
const stat = statSync(file);
|
|
264
|
+
version = `${stat.size}:${stat.mtimeMs}`;
|
|
265
|
+
} catch (error) {
|
|
266
|
+
if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
|
|
267
|
+
version = "missing";
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
const cached = this.itemCache.get(assistantId);
|
|
271
|
+
if (cached && cached.manager === manager && cached.version === version) return cached;
|
|
272
|
+
const result = walk(manager ? manager.getEntries() : readEntries(file));
|
|
273
|
+
const positions = new Map<string, number>();
|
|
274
|
+
const assistantTimes: string[] = [];
|
|
275
|
+
let last: { text: string; at: Iso } | undefined;
|
|
276
|
+
result.items.forEach((item, index) => {
|
|
277
|
+
positions.set(item.id, index);
|
|
278
|
+
if (item.kind === "assistant") assistantTimes.push(item.at);
|
|
279
|
+
if (item.kind === "assistant" || item.kind === "user" || item.kind === "note") last = { text: item.text, at: item.at };
|
|
280
|
+
});
|
|
281
|
+
assistantTimes.sort();
|
|
282
|
+
const next = { manager, version, result, positions, assistantTimes, last };
|
|
283
|
+
this.itemCache.set(assistantId, next);
|
|
284
|
+
return next;
|
|
285
|
+
}
|
|
286
|
+
|
|
253
287
|
async ensure(assistantId: string): Promise<RouterState> {
|
|
254
288
|
const assistant = this.assistant(assistantId);
|
|
255
289
|
let state = this.routers.get(assistantId);
|
|
@@ -271,6 +305,10 @@ export class RouterManager {
|
|
|
271
305
|
await this.runtime.models.get();
|
|
272
306
|
const pick = conversationPick(this.runtime.settings.settings.models, assistant);
|
|
273
307
|
state.knowledgeNames = (await boundKnowledge(this.runtime, assistant.id)).map((collection) => collection.name);
|
|
308
|
+
state.desktop = new DesktopToolSession(this.runtime, assistant.id, () => !!state.session?.model?.input.includes("image"), undefined, true);
|
|
309
|
+
const tools = [...buildRouterTools(this.runtime, assistant.id), ...state.desktop.tools().filter((tool) => tool.name.startsWith("browser_"))];
|
|
310
|
+
const desktop = this.runtime.desktop.view();
|
|
311
|
+
const enabled = desktop.enabled && !!desktop.url && desktop.tokenConfigured;
|
|
274
312
|
const opened = await this.runtime.pi.open({
|
|
275
313
|
file: this.store.routerFile(assistant.id),
|
|
276
314
|
spillDir: join(dirname(this.store.routerFile(assistant.id)), "spill"),
|
|
@@ -280,10 +318,15 @@ export class RouterManager {
|
|
|
280
318
|
memory: this.runtime.settings.memory().provider,
|
|
281
319
|
systemPrompt: () => {
|
|
282
320
|
const { timezone, aboutYou } = this.runtime.settings.settings.general;
|
|
321
|
+
const desktop = this.runtime.desktop.view();
|
|
283
322
|
return buildSystemPrompt(this.store, assistant.id, {
|
|
284
323
|
timezone,
|
|
285
324
|
aboutYou,
|
|
286
325
|
memory: this.runtime.settings.memory().provider,
|
|
326
|
+
searchEnabled: this.runtime.settings.settings.search.some((provider) => provider.enabled),
|
|
327
|
+
imageEnabled: this.runtime.settings.settings.image.source !== "off",
|
|
328
|
+
desktopEnabled: desktop.enabled && !!desktop.url && desktop.tokenConfigured,
|
|
329
|
+
publicDesktop: desktop.publicAssistants.includes(assistant.id),
|
|
287
330
|
models: this.promptModels(assistant.id),
|
|
288
331
|
skills: availableSkills(this.runtime.home, undefined, this.runtime.settings.settings.skills?.disabled, this.runtime.settings.memory().provider === "lore" ? this.runtime.pi.loreSkillPaths : []),
|
|
289
332
|
assistantSkills: usableAssistantSkills(this.runtime.home, assistant.id),
|
|
@@ -292,8 +335,8 @@ export class RouterManager {
|
|
|
292
335
|
knowledgeNames: state.knowledgeNames,
|
|
293
336
|
});
|
|
294
337
|
},
|
|
295
|
-
tools:
|
|
296
|
-
customTools:
|
|
338
|
+
tools: tools.filter((tool) => enabled || !tool.name.startsWith("browser_")).map((tool) => tool.name),
|
|
339
|
+
customTools: tools,
|
|
297
340
|
});
|
|
298
341
|
state.session = opened.session;
|
|
299
342
|
if (!state.knowledgeNames.length) refreshSystemPrompt(opened.session, opened.session.getActiveToolNames().filter((name) => name !== "search_knowledge"));
|
|
@@ -305,8 +348,10 @@ export class RouterManager {
|
|
|
305
348
|
|
|
306
349
|
private async refreshTools(state: RouterState): Promise<void> {
|
|
307
350
|
state.knowledgeNames = (await boundKnowledge(this.runtime, state.assistantId)).map((collection) => collection.name);
|
|
308
|
-
const names = state.session!.getActiveToolNames().filter((name) => name !== "search_knowledge");
|
|
351
|
+
const names = state.session!.getActiveToolNames().filter((name) => name !== "search_knowledge" && !name.startsWith("browser_"));
|
|
309
352
|
if (state.knowledgeNames.length) names.push("search_knowledge");
|
|
353
|
+
const desktop = this.runtime.desktop.view();
|
|
354
|
+
if (desktop.enabled && desktop.url && desktop.tokenConfigured) names.push(...state.desktop!.tools().filter((tool) => tool.name.startsWith("browser_")).map((tool) => tool.name));
|
|
310
355
|
refreshSystemPrompt(state.session!, names);
|
|
311
356
|
}
|
|
312
357
|
|
|
@@ -392,7 +437,7 @@ export class RouterManager {
|
|
|
392
437
|
}
|
|
393
438
|
if (event.type === "message_start" && event.message.role === "custom" && event.message.customType === EVENT_TYPE) {
|
|
394
439
|
const details = event.message.details as EventDetails | undefined;
|
|
395
|
-
const users =
|
|
440
|
+
const users = this.derived(assistantId).result.items.filter((item) => item.kind === "user");
|
|
396
441
|
const source = details?.sourceUserId ? users.find((item) => item.id === details.sourceUserId) : undefined;
|
|
397
442
|
state.currentUserId = (source ?? users.at(-1))?.id;
|
|
398
443
|
if (details?.delegationId && details.delegationId !== state.currentDelegationId) {
|
|
@@ -471,7 +516,7 @@ export class RouterManager {
|
|
|
471
516
|
at: new Date().toISOString(),
|
|
472
517
|
text: "",
|
|
473
518
|
streaming: true,
|
|
474
|
-
...(
|
|
519
|
+
...(this.derived(assistantId).result.lead ? { lead: true } : {}),
|
|
475
520
|
},
|
|
476
521
|
});
|
|
477
522
|
}
|
|
@@ -502,7 +547,7 @@ export class RouterManager {
|
|
|
502
547
|
}
|
|
503
548
|
|
|
504
549
|
private emitSettledAssistant(assistantId: string, text: string, expectedId?: string): void {
|
|
505
|
-
const list =
|
|
550
|
+
const list = this.derived(assistantId).result.items.filter((item) => item.kind === "assistant");
|
|
506
551
|
const item = list.find((entry) => entry.id === expectedId) ?? list.filter((entry) => entry.text === text).pop();
|
|
507
552
|
if (item) this.bus.emit({ type: "item", assistantId, item });
|
|
508
553
|
this.touch(assistantId);
|
|
@@ -580,8 +625,11 @@ export class RouterManager {
|
|
|
580
625
|
if (state.idleTimer) clearTimeout(state.idleTimer);
|
|
581
626
|
state.idleTimer = undefined;
|
|
582
627
|
state.session?.dispose();
|
|
628
|
+
void state.desktop?.dispose();
|
|
629
|
+
state.desktop = undefined;
|
|
583
630
|
state.session = undefined;
|
|
584
631
|
state.manager = undefined;
|
|
632
|
+
this.itemCache.delete(state.assistantId);
|
|
585
633
|
}
|
|
586
634
|
|
|
587
635
|
/**
|
|
@@ -597,6 +645,7 @@ export class RouterManager {
|
|
|
597
645
|
|
|
598
646
|
/** 助手没了(归档):关掉它的会话,内存里也不留 —— 它的目录随后会被移进归档。 */
|
|
599
647
|
forget(assistantId: string): void {
|
|
648
|
+
this.itemCache.delete(assistantId);
|
|
600
649
|
const state = this.routers.get(assistantId);
|
|
601
650
|
if (!state) return;
|
|
602
651
|
this.close(state);
|
|
@@ -618,7 +667,7 @@ export class RouterManager {
|
|
|
618
667
|
// ---------- 事件与条目 ----------
|
|
619
668
|
async addItem(assistantId: string, item: NewItem): Promise<Item> {
|
|
620
669
|
const state = await this.ensure(assistantId);
|
|
621
|
-
const lead =
|
|
670
|
+
const lead = this.derived(assistantId).result.lead;
|
|
622
671
|
const id = state.manager!.appendCustomEntry(ITEM_TYPE, item);
|
|
623
672
|
const full = { ...item, ...(lead ? { lead: true } : {}), id, at: this.entryAt(state, id) } as Item;
|
|
624
673
|
this.bus.emit({ type: "item", assistantId, item: full });
|
|
@@ -632,7 +681,7 @@ export class RouterManager {
|
|
|
632
681
|
|
|
633
682
|
async updateImage(assistantId: string, id: string, update: { status: "done" | "failed"; path?: string; error?: string }): Promise<void> {
|
|
634
683
|
const state = await this.ensure(assistantId);
|
|
635
|
-
const item =
|
|
684
|
+
const item = this.derived(assistantId).result.items.find((entry) => entry.id === id && entry.kind === "image");
|
|
636
685
|
if (!item) throw new Error(`图片条目不存在:${id}`);
|
|
637
686
|
state.manager!.appendCustomEntry(ITEM_UPDATE_TYPE, { id, ...update });
|
|
638
687
|
this.bus.emit({ type: "item", assistantId, item: { ...item, ...update } });
|
|
@@ -647,7 +696,7 @@ export class RouterManager {
|
|
|
647
696
|
|
|
648
697
|
/** 取当前回合来源及其之前的可见对话;事件取最近用户消息。 */
|
|
649
698
|
originalWords(assistantId: string, note?: string, recent = false, extraFiles: string[] = []): { text: string; files: string[] } {
|
|
650
|
-
const items =
|
|
699
|
+
const items = this.derived(assistantId).result.items;
|
|
651
700
|
const state = this.routers.get(assistantId);
|
|
652
701
|
const source = items.find((item) => item.kind === "user" && item.id === state?.currentUserId)
|
|
653
702
|
?? items.findLast((item) => item.kind === "user");
|
|
@@ -743,7 +792,8 @@ export class RouterManager {
|
|
|
743
792
|
*/
|
|
744
793
|
private async reportTurnError(state: RouterState): Promise<void> {
|
|
745
794
|
const last = state.session?.messages.findLast((message) => message.role === "assistant");
|
|
746
|
-
|
|
795
|
+
// 停下的那一轮:有的服务商(Claude 订阅桥)把停止报成 error,不是回复失败
|
|
796
|
+
if (state.stalled || !last || last.role !== "assistant" || last.stopReason !== "error") return;
|
|
747
797
|
await this.addItem(state.assistantId, { kind: "note", icon: "info", text: `回复失败:${briefModelError(last.errorMessage)}。发送任意消息即可重试。` });
|
|
748
798
|
}
|
|
749
799
|
|
|
@@ -876,7 +926,10 @@ export class RouterManager {
|
|
|
876
926
|
|
|
877
927
|
async stop(assistantId: string): Promise<void> {
|
|
878
928
|
const state = this.routers.get(assistantId);
|
|
879
|
-
if (state)
|
|
929
|
+
if (state) {
|
|
930
|
+
state.stalled = true;
|
|
931
|
+
this.setActivity(state, null);
|
|
932
|
+
}
|
|
880
933
|
if (state?.session) await state.session.abort();
|
|
881
934
|
}
|
|
882
935
|
|
|
@@ -908,14 +961,12 @@ export class RouterManager {
|
|
|
908
961
|
}
|
|
909
962
|
|
|
910
963
|
items(assistantId: string, options: { before?: string; limit?: number } = {}): ItemsPage {
|
|
911
|
-
const
|
|
964
|
+
const derived = this.derived(assistantId);
|
|
965
|
+
const all = derived.result.items;
|
|
912
966
|
const limit = Math.max(1, Math.min(options.limit ?? 50, 200));
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
if (index !== -1) list = all.slice(0, index);
|
|
917
|
-
}
|
|
918
|
-
const page = list.slice(-limit);
|
|
967
|
+
const end = options.before ? derived.positions.get(options.before) ?? all.length : all.length;
|
|
968
|
+
const start = Math.max(0, end - limit);
|
|
969
|
+
const page = all.slice(start, end);
|
|
919
970
|
const jobIds = new Set(page.filter((item) => item.kind === "job").map((item) => (item as { jobId: string }).jobId));
|
|
920
971
|
const draftIds = new Set(
|
|
921
972
|
page.filter((item) => item.kind === "draft").map((item) => (item as { draftId: string }).draftId),
|
|
@@ -929,15 +980,14 @@ export class RouterManager {
|
|
|
929
980
|
for (const id of delegation.jobIds) jobIds.add(id);
|
|
930
981
|
for (const id of delegation.approvalIds ?? []) approvalIds.add(id);
|
|
931
982
|
}
|
|
932
|
-
for (const approval of this.store.approvals()) if (approval.jobId && jobIds.has(approval.jobId)) approvalIds.add(approval.id);
|
|
933
983
|
return {
|
|
934
984
|
...(!options.before ? { pageLive: [...(this.livePages.get(assistantId)?.values() ?? [])].map((live) => live.page) } : {}),
|
|
935
985
|
items: page,
|
|
936
986
|
approvals: this.store.approvals().filter((approval) => approvalIds.has(approval.id)),
|
|
937
|
-
jobs:
|
|
987
|
+
jobs: [...jobIds].flatMap((id) => { const job = this.store.job(id); return job ? [job] : []; }),
|
|
938
988
|
drafts: this.store.drafts().filter((draft) => draftIds.has(draft.id)),
|
|
939
989
|
delegations,
|
|
940
|
-
hasMore:
|
|
990
|
+
hasMore: start > 0,
|
|
941
991
|
};
|
|
942
992
|
}
|
|
943
993
|
|
|
@@ -947,21 +997,23 @@ export class RouterManager {
|
|
|
947
997
|
|
|
948
998
|
unread(assistantId: string): number {
|
|
949
999
|
const readAt = this.store.state(assistantId).readAt;
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
(
|
|
953
|
-
|
|
1000
|
+
// 从没记过已读:从现在开始算,以前的都当读过。不记的话未读永远是 0,前端也就永远不会来标已读
|
|
1001
|
+
if (!readAt) {
|
|
1002
|
+
this.read(assistantId);
|
|
1003
|
+
return 0;
|
|
1004
|
+
}
|
|
1005
|
+
const times = this.derived(assistantId).assistantTimes;
|
|
1006
|
+
let lo = 0, hi = times.length;
|
|
1007
|
+
while (lo < hi) {
|
|
1008
|
+
const mid = (lo + hi) >>> 1;
|
|
1009
|
+
if (times[mid] <= readAt) lo = mid + 1;
|
|
1010
|
+
else hi = mid;
|
|
1011
|
+
}
|
|
1012
|
+
return times.length - lo;
|
|
954
1013
|
}
|
|
955
1014
|
|
|
956
1015
|
last(assistantId: string): { text: string; at: Iso } | undefined {
|
|
957
|
-
|
|
958
|
-
for (let i = items.length - 1; i >= 0; i -= 1) {
|
|
959
|
-
const item = items[i];
|
|
960
|
-
if (item.kind === "user" || item.kind === "assistant" || item.kind === "note") {
|
|
961
|
-
return { text: item.text, at: item.at };
|
|
962
|
-
}
|
|
963
|
-
}
|
|
964
|
-
return undefined;
|
|
1016
|
+
return this.derived(assistantId).last;
|
|
965
1017
|
}
|
|
966
1018
|
|
|
967
1019
|
summary(assistant: Assistant): AssistantSummary {
|
|
@@ -970,6 +1022,7 @@ export class RouterManager {
|
|
|
970
1022
|
...assistant,
|
|
971
1023
|
...(last ? { last } : {}),
|
|
972
1024
|
unread: this.unread(assistant.id),
|
|
1025
|
+
...(this.store.state(assistant.id).readAt ? { readAt: this.store.state(assistant.id).readAt } : {}),
|
|
973
1026
|
busy: this.isBusy(assistant.id),
|
|
974
1027
|
activity: this.routers.get(assistant.id)?.activity ?? null,
|
|
975
1028
|
};
|
|
@@ -993,7 +1046,7 @@ export class RouterManager {
|
|
|
993
1046
|
.jobs()
|
|
994
1047
|
.filter((job) => job.status === "running" || job.status === "queued" || job.status === "waiting" || (job.endedAt ?? job.createdAt) > jobCutoff),
|
|
995
1048
|
routines: this.store.routines(),
|
|
996
|
-
approvals: this.store.approvals().filter((approval) => approval.status === "pending"),
|
|
1049
|
+
approvals: this.store.approvals().filter((approval) => approval.status === "pending" && approval.source !== "job"),
|
|
997
1050
|
delegations: this.store.delegations(),
|
|
998
1051
|
settings: this.runtime.settings.get(),
|
|
999
1052
|
};
|
|
@@ -1001,11 +1054,9 @@ export class RouterManager {
|
|
|
1001
1054
|
|
|
1002
1055
|
/** 进程退出或测试收尾:释放所有 Router 会话。 */
|
|
1003
1056
|
disposeAll(): void {
|
|
1004
|
-
for (const state of this.routers.values())
|
|
1005
|
-
if (state.idleTimer) clearTimeout(state.idleTimer);
|
|
1006
|
-
state.session?.dispose();
|
|
1007
|
-
}
|
|
1057
|
+
for (const state of this.routers.values()) this.close(state);
|
|
1008
1058
|
this.routers.clear();
|
|
1059
|
+
this.itemCache.clear();
|
|
1009
1060
|
}
|
|
1010
1061
|
|
|
1011
1062
|
/** 直接走事件的 followUp 队列,实际收到这条消息时才开始关联任务。 */
|
package/server/src/store.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { existsSync, mkdirSync, readFileSync, readdirSync, renameSync, rmSync, writeFileSync, appendFileSync } from "node:fs";
|
|
2
2
|
import { dirname, join } from "node:path";
|
|
3
|
+
import { newId } from "./ids.ts";
|
|
3
4
|
import type {
|
|
4
5
|
Approval,
|
|
5
6
|
Assistant,
|
|
@@ -56,6 +57,25 @@ export class Store {
|
|
|
56
57
|
for (const dir of ["assistants", "jobs", "uploads"]) {
|
|
57
58
|
mkdirSync(join(this.home, dir), { recursive: true });
|
|
58
59
|
}
|
|
60
|
+
if (!existsSync(join(this.home, "rules.json"))) {
|
|
61
|
+
const changedAt = new Date().toISOString();
|
|
62
|
+
this.saveGlobalRules([
|
|
63
|
+
...["删除或覆盖文件、数据和备份", "对外发送(消息、邮件、发布)、付费、充值、改计费", "停止或重启别人正在使用的服务和设备", "修改可能让你失去远程访问的设置(网络、防火墙、登录方式)"].map((text) => ({ id: newId("r-"), text, mode: "ask" as const, changedAt })),
|
|
64
|
+
...["过去的记录是线索不是现状:动手前先核实实际状态。", "改配置、改数据前先留好退路(备份或记下原值);改完用实际效果验证,不只看命令返回。", "凭据(密码、密钥、令牌、Cookie)只在用的时候读取,不回显,不写进文件和报告,也不存起来。"].map((text) => ({ id: newId("r-"), text, mode: "allow" as const, changedAt })),
|
|
65
|
+
]);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
globalRules(): Rule[] {
|
|
70
|
+
const path = join(this.home, "rules.json");
|
|
71
|
+
if (!existsSync(path)) return [];
|
|
72
|
+
const rules = JSON.parse(readFileSync(path, "utf8")) as unknown;
|
|
73
|
+
if (!Array.isArray(rules)) throw new Error("全局规则文件格式无效");
|
|
74
|
+
return rules as Rule[];
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
saveGlobalRules(rules: Rule[]): void {
|
|
78
|
+
writeJsonAtomic(join(this.home, "rules.json"), rules);
|
|
59
79
|
}
|
|
60
80
|
|
|
61
81
|
// ---------- 助手 ----------
|
|
@@ -19,8 +19,8 @@ export function registerReportTool(runtime: HobRuntime, jobId: string): ToolDefi
|
|
|
19
19
|
outcome: Type.Unsafe<"done" | "stuck" | "need_approval">(
|
|
20
20
|
Type.String({ description: "done 做完 / stuck 做不下去(写明缺什么)/ need_approval 需要用户拍板" }),
|
|
21
21
|
),
|
|
22
|
-
result: Type.String({ description: "
|
|
23
|
-
files: Type.Optional(Type.Array(Type.String(), { description: "
|
|
22
|
+
result: Type.String({ description: "完整结果,按这个顺序写:结论;做了什么;验证了什么、怎么验证的;没验证或不确定的;需要用户决定的。带上数据、链接和文件路径,写给派活的助手。" }),
|
|
23
|
+
files: Type.Optional(Type.Array(Type.String(), { description: "交给助手的文件(绝对路径),比如要给用户看的图" })),
|
|
24
24
|
}),
|
|
25
25
|
execute: async (_id, params: any) => {
|
|
26
26
|
// 不认识的结果不能当成已完成:退回让模型重新调用
|
|
@@ -46,7 +46,7 @@ export function registerAskTool(runtime: HobRuntime, jobId: string): ToolDefinit
|
|
|
46
46
|
return {
|
|
47
47
|
name: "ask_user",
|
|
48
48
|
label: "询问用户",
|
|
49
|
-
description: "
|
|
49
|
+
description: "问你的助手,等它回复:它会自己答,或者去问用户。用于用户定的规则要求先确认的事,或发送、删除、付费这类撤不回的事。其他拿不准的不问,按合理的假设做下去,把假设写进 result。",
|
|
50
50
|
promptSnippet: "规则要求执行前确认的事,执行前必须调用 ask_user。",
|
|
51
51
|
parameters: Type.Object({
|
|
52
52
|
title: Type.String({ description: "问题,一句话" }),
|
|
@@ -57,27 +57,20 @@ export function registerAskTool(runtime: HobRuntime, jobId: string): ToolDefinit
|
|
|
57
57
|
}),
|
|
58
58
|
{ description: "2 到 3 个选项", minItems: 2, maxItems: 3 },
|
|
59
59
|
),
|
|
60
|
-
remember: Type.Optional(Type.String({ description: "同类事以后可以不再问时,填这类事的简称(如「清理临时目录」);卡片会多一个「以后直接做」的选项。" })),
|
|
61
60
|
}),
|
|
62
61
|
execute: async (_id, params: any) => {
|
|
63
62
|
const labels: { label: string }[] = Array.isArray(params.options) ? params.options : [];
|
|
64
63
|
if (labels.length < 2 || labels.length > 3) {
|
|
65
64
|
return { content: [{ type: "text" as const, text: "选项要 2 到 3 个。" }], details: undefined, isError: true };
|
|
66
65
|
}
|
|
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
|
-
}
|
|
71
66
|
const options: ApprovalOption[] = labels.map((option, index) => ({
|
|
72
67
|
key: (["A", "B", "C"] as const)[index],
|
|
73
68
|
label: String(option.label),
|
|
74
69
|
}));
|
|
75
|
-
if (remember) options.push({ key: "C", label: `以后「${remember}」直接做`, always: true });
|
|
76
70
|
const answer = await runtime.jobs.askQuestion(jobId, {
|
|
77
71
|
title: String(params.title),
|
|
78
72
|
...(params.detail ? { detail: String(params.detail) } : {}),
|
|
79
73
|
options,
|
|
80
|
-
...(remember ? { ruleText: remember } : {}),
|
|
81
74
|
});
|
|
82
75
|
// 用户可能没选给出的选项,而是自己写了一段:answer 已经是回给模型的整句
|
|
83
76
|
return ok(answer);
|
package/shared/skills.ts
CHANGED
|
@@ -15,6 +15,8 @@ export interface SkillsView {
|
|
|
15
15
|
skills: SkillView[];
|
|
16
16
|
loreActive: boolean;
|
|
17
17
|
loreUrl: string | null;
|
|
18
|
+
/** Lore 目录没取到的原因;其他技能照常列出。 */
|
|
19
|
+
loreError?: string;
|
|
18
20
|
}
|
|
19
21
|
|
|
20
22
|
export interface ImportSkill {
|
|
@@ -42,7 +44,7 @@ export interface SkillImportResult {
|
|
|
42
44
|
*/
|
|
43
45
|
export interface AssistantSkill {
|
|
44
46
|
name: string; // 目录名:小写字母、数字、连字符
|
|
45
|
-
title: string; //
|
|
47
|
+
title: string; // 界面上显示的名字,如「每周周报」
|
|
46
48
|
description: string; // 一句话:什么时候用
|
|
47
49
|
path: string; // SKILL.md 的绝对路径(预览用)
|
|
48
50
|
version: number; // 每次修补 +1,旧版在 skills/<name>/.history/<版本>/
|
package/shared/types.ts
CHANGED
|
@@ -247,6 +247,7 @@ export type RouterActivity = { state: "thinking" | "tool"; label: string };
|
|
|
247
247
|
export interface AssistantSummary extends Assistant {
|
|
248
248
|
last?: { text: string; at: Iso }; // 对话最后一条(列表第二行)
|
|
249
249
|
unread: number; // 未读条数(>0 时大头像右上角橙点)
|
|
250
|
+
readAt?: Iso; // 上次看这个助手的时间:之前结束的任务不再挂完成/受阻标记
|
|
250
251
|
busy: boolean; // Router 正在回复(在跑的任务不算,任务状态看任务卡)
|
|
251
252
|
activity?: RouterActivity | null;
|
|
252
253
|
}
|
|
@@ -688,6 +689,7 @@ export interface Bootstrap {
|
|
|
688
689
|
}
|
|
689
690
|
|
|
690
691
|
export type HobEvent =
|
|
692
|
+
| { type: "rules" }
|
|
691
693
|
| { type: "desktop-control"; screen: string; taken: boolean }
|
|
692
694
|
| { type: "desktop-screens"; screens: DesktopScreen[] }
|
|
693
695
|
| ({ type: "page-live" } & PageLive)
|