hob-bot 1.4.0 → 1.5.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 +19 -8
- package/README.zh-CN.md +19 -8
- package/bin/hob.mjs +8 -2
- package/dist/web/assets/{DesktopView-A11jmUwu.js → DesktopView-3QDYo0UU.js} +1 -1
- package/dist/web/assets/index-Dzxrt9T3.js +283 -0
- package/dist/web/assets/index-FUtuyxyO.css +1 -0
- package/dist/web/index.html +2 -2
- package/package.json +2 -1
- package/server/src/actions.ts +2 -1
- package/server/src/boot.ts +8 -0
- package/server/src/config.ts +52 -11
- package/server/src/cron-text.ts +14 -1
- package/server/src/desktop-tools.ts +2 -1
- package/server/src/files.ts +27 -7
- package/server/src/http.ts +17 -5
- package/server/src/jobs.ts +3 -0
- package/server/src/knowledge.ts +9 -4
- package/server/src/layout-cli.ts +25 -0
- package/server/src/layout.ts +749 -0
- package/server/src/main.ts +3 -0
- package/server/src/mcp.ts +3 -2
- package/server/src/pages.ts +5 -3
- package/server/src/prompt.ts +4 -3
- package/server/src/push.ts +3 -2
- package/server/src/router-tools.ts +50 -14
- package/server/src/router.ts +3 -3
- package/server/src/routines.ts +171 -21
- package/server/src/search.ts +1 -1
- package/server/src/secrets.ts +3 -2
- package/server/src/settings.ts +7 -6
- package/server/src/store.ts +123 -40
- package/server/src/transcript.ts +11 -3
- package/server/src/usage-prices.ts +5 -4
- package/server/src/usage.ts +15 -5
- package/shared/types.ts +27 -1
- package/dist/web/assets/index-B-YlVbpo.js +0 -270
- package/dist/web/assets/index-DkSQ-ygR.css +0 -1
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 { archiveAssistantsDir, assistantDir, assistantJobsDir } from "./config.ts";
|
|
3
4
|
import { newId } from "./ids.ts";
|
|
4
5
|
import type {
|
|
5
6
|
Approval,
|
|
@@ -49,14 +50,19 @@ export interface AssistantState {
|
|
|
49
50
|
|
|
50
51
|
/**
|
|
51
52
|
* HOB_HOME 下的全部 JSON / JSONL 数据。文件都小,直接同步读写,写入走临时文件 + rename。
|
|
53
|
+
*
|
|
54
|
+
* v2 布局里任务和审批、定时都跟着所属助手放(assistants/<id>/jobs|approvals.json|routines.json),
|
|
55
|
+
* 进不到目录的地方要知道 jobId 落在哪个助手下面,所以这里维护一份内存索引,启动时扫一遍、
|
|
56
|
+
* 新建任务时登记;不存索引文件,免得和实际目录对不上。
|
|
52
57
|
*/
|
|
53
58
|
export class Store {
|
|
59
|
+
/** jobId → 任务目录(含归档助手目录里的) */
|
|
60
|
+
private jobIndex: Map<string, string> | null = null;
|
|
61
|
+
|
|
54
62
|
constructor(readonly home: string) {}
|
|
55
63
|
|
|
56
64
|
init(): void {
|
|
57
|
-
|
|
58
|
-
mkdirSync(join(this.home, dir), { recursive: true });
|
|
59
|
-
}
|
|
65
|
+
mkdirSync(join(this.home, "assistants"), { recursive: true });
|
|
60
66
|
if (!existsSync(join(this.home, "rules.json"))) {
|
|
61
67
|
const changedAt = new Date().toISOString();
|
|
62
68
|
this.saveGlobalRules([
|
|
@@ -80,7 +86,7 @@ export class Store {
|
|
|
80
86
|
|
|
81
87
|
// ---------- 助手 ----------
|
|
82
88
|
private assistantDir(id: string): string {
|
|
83
|
-
return
|
|
89
|
+
return assistantDir(this.home, id);
|
|
84
90
|
}
|
|
85
91
|
|
|
86
92
|
assistants(): Assistant[] {
|
|
@@ -110,14 +116,16 @@ export class Store {
|
|
|
110
116
|
}
|
|
111
117
|
|
|
112
118
|
/**
|
|
113
|
-
*
|
|
114
|
-
*
|
|
119
|
+
* 归档助手:整个助手目录(含自己的任务、定时、审批、文件)移进 archive/assistants/<id>-<时间>/。
|
|
120
|
+
* 同盘 rename,不复制也不删数据。
|
|
115
121
|
*/
|
|
116
122
|
archiveAssistant(id: string, name: string, routines: Routine[] = []): string {
|
|
117
123
|
if (routines.length) writeJsonAtomic(join(this.assistantDir(id), "routines.json"), routines);
|
|
118
|
-
const target = join(this.home,
|
|
124
|
+
const target = join(archiveAssistantsDir(this.home), name);
|
|
119
125
|
mkdirSync(dirname(target), { recursive: true });
|
|
120
126
|
renameSync(this.assistantDir(id), target);
|
|
127
|
+
// 任务跟着搬走了,索引要重建
|
|
128
|
+
this.jobIndex = null;
|
|
121
129
|
return target;
|
|
122
130
|
}
|
|
123
131
|
|
|
@@ -151,13 +159,45 @@ export class Store {
|
|
|
151
159
|
appendFileSync(join(dir, "worklog.jsonl"), `${JSON.stringify(entry)}\n`);
|
|
152
160
|
}
|
|
153
161
|
|
|
154
|
-
// ----------
|
|
155
|
-
|
|
156
|
-
|
|
162
|
+
// ---------- 定时和审批:跟着所属助手 ----------
|
|
163
|
+
/** 每个助手目录下的一份记录文件(含归档目录里的),有就给路径。 */
|
|
164
|
+
private recordFiles(name: string, archived: boolean): string[] {
|
|
165
|
+
const out: string[] = [];
|
|
166
|
+
const live = join(this.home, "assistants");
|
|
167
|
+
if (existsSync(live)) {
|
|
168
|
+
for (const id of readdirSync(live)) {
|
|
169
|
+
const file = join(live, id, name);
|
|
170
|
+
if (existsSync(file)) out.push(file);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
if (!archived) return out;
|
|
174
|
+
const root = archiveAssistantsDir(this.home);
|
|
175
|
+
if (existsSync(root)) {
|
|
176
|
+
for (const folder of readdirSync(root)) {
|
|
177
|
+
const file = join(root, folder, name);
|
|
178
|
+
if (existsSync(file)) out.push(file);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
return out;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/** 写这个助手的记录文件:助手归档了就跟到归档目录,不然就地建。 */
|
|
185
|
+
private ownerFile(assistantId: string, name: string): string {
|
|
186
|
+
const live = this.assistantDir(assistantId);
|
|
187
|
+
if (existsSync(join(live, "profile.json"))) return join(live, name);
|
|
188
|
+
const root = archiveAssistantsDir(this.home);
|
|
189
|
+
if (existsSync(root)) {
|
|
190
|
+
for (const folder of readdirSync(root)) {
|
|
191
|
+
const dir = join(root, folder);
|
|
192
|
+
const profile = readJson<{ id?: unknown }>(join(dir, "profile.json"), {});
|
|
193
|
+
if (profile.id === assistantId) return join(dir, name);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
return join(live, name);
|
|
157
197
|
}
|
|
158
198
|
|
|
159
199
|
routines(): Routine[] {
|
|
160
|
-
return readJson<Routine[]>(
|
|
200
|
+
return this.recordFiles("routines.json", false).flatMap((file) => readJson<Routine[]>(file, []));
|
|
161
201
|
}
|
|
162
202
|
|
|
163
203
|
routine(id: string): Routine | undefined {
|
|
@@ -165,22 +205,29 @@ export class Store {
|
|
|
165
205
|
}
|
|
166
206
|
|
|
167
207
|
saveRoutine(routine: Routine): void {
|
|
168
|
-
const
|
|
169
|
-
|
|
170
|
-
|
|
208
|
+
const target = this.ownerFile(routine.assistantId, "routines.json");
|
|
209
|
+
for (const file of this.recordFiles("routines.json", true)) {
|
|
210
|
+
const list = readJson<Routine[]>(file, []);
|
|
211
|
+
const next = list.filter((r) => r.id !== routine.id);
|
|
212
|
+
if (next.length !== list.length) writeJsonAtomic(file, next);
|
|
213
|
+
}
|
|
214
|
+
const list = readJson<Routine[]>(target, []).filter((r) => r.id !== routine.id);
|
|
215
|
+
list.push(routine);
|
|
216
|
+
writeJsonAtomic(target, list);
|
|
171
217
|
}
|
|
172
218
|
|
|
219
|
+
/** 只从还在的助手里拿掉;归档目录里那份是历史,留着。 */
|
|
173
220
|
deleteRoutine(id: string): void {
|
|
174
|
-
|
|
221
|
+
for (const file of this.recordFiles("routines.json", false)) {
|
|
222
|
+
const list = readJson<Routine[]>(file, []);
|
|
223
|
+
const next = list.filter((r) => r.id !== id);
|
|
224
|
+
if (next.length !== list.length) writeJsonAtomic(file, next);
|
|
225
|
+
}
|
|
175
226
|
}
|
|
176
227
|
|
|
177
228
|
// ---------- 审批 ----------
|
|
178
|
-
private approvalsFile(): string {
|
|
179
|
-
return join(this.home, "approvals.json");
|
|
180
|
-
}
|
|
181
|
-
|
|
182
229
|
approvals(): Approval[] {
|
|
183
|
-
return readJson<Approval[]>(
|
|
230
|
+
return this.recordFiles("approvals.json", true).flatMap((file) => readJson<Approval[]>(file, []));
|
|
184
231
|
}
|
|
185
232
|
|
|
186
233
|
approval(id: string): Approval | undefined {
|
|
@@ -188,14 +235,24 @@ export class Store {
|
|
|
188
235
|
}
|
|
189
236
|
|
|
190
237
|
saveApproval(approval: Approval): void {
|
|
191
|
-
const
|
|
192
|
-
|
|
193
|
-
|
|
238
|
+
const target = this.ownerFile(approval.assistantId, "approvals.json");
|
|
239
|
+
for (const file of this.recordFiles("approvals.json", true)) {
|
|
240
|
+
const list = readJson<Approval[]>(file, []);
|
|
241
|
+
const next = list.filter((a) => a.id !== approval.id);
|
|
242
|
+
if (next.length !== list.length) writeJsonAtomic(file, next);
|
|
243
|
+
}
|
|
244
|
+
const list = readJson<Approval[]>(target, []).filter((a) => a.id !== approval.id);
|
|
245
|
+
list.push(approval);
|
|
246
|
+
writeJsonAtomic(target, list);
|
|
194
247
|
}
|
|
195
248
|
|
|
196
249
|
// ---------- 委派 ----------
|
|
250
|
+
private hobFile(name: string): string {
|
|
251
|
+
return join(this.assistantDir("hob"), name);
|
|
252
|
+
}
|
|
253
|
+
|
|
197
254
|
delegations(): Delegation[] {
|
|
198
|
-
return readJson<Delegation[]>(
|
|
255
|
+
return readJson<Delegation[]>(this.hobFile("delegations.json"), []);
|
|
199
256
|
}
|
|
200
257
|
|
|
201
258
|
delegation(id: string): Delegation | undefined {
|
|
@@ -205,16 +262,12 @@ export class Store {
|
|
|
205
262
|
saveDelegation(delegation: Delegation): void {
|
|
206
263
|
const all = this.delegations().filter((item) => item.id !== delegation.id);
|
|
207
264
|
all.push(delegation);
|
|
208
|
-
writeJsonAtomic(
|
|
265
|
+
writeJsonAtomic(this.hobFile("delegations.json"), all);
|
|
209
266
|
}
|
|
210
267
|
|
|
211
268
|
// ---------- 草稿 ----------
|
|
212
|
-
private draftsFile(): string {
|
|
213
|
-
return join(this.home, "drafts.json");
|
|
214
|
-
}
|
|
215
|
-
|
|
216
269
|
drafts(): Draft[] {
|
|
217
|
-
return readJson<Draft[]>(this.
|
|
270
|
+
return readJson<Draft[]>(this.hobFile("drafts.json"), []);
|
|
218
271
|
}
|
|
219
272
|
|
|
220
273
|
draft(id: string): Draft | undefined {
|
|
@@ -224,31 +277,61 @@ export class Store {
|
|
|
224
277
|
saveDraft(draft: Draft): void {
|
|
225
278
|
const all = this.drafts().filter((d) => d.id !== draft.id);
|
|
226
279
|
all.push(draft);
|
|
227
|
-
writeJsonAtomic(this.
|
|
280
|
+
writeJsonAtomic(this.hobFile("drafts.json"), all);
|
|
228
281
|
}
|
|
229
282
|
|
|
230
283
|
// ---------- 任务 ----------
|
|
284
|
+
/** 所有任务目录的根:在的助手和归档的助手都算。 */
|
|
285
|
+
private jobRoots(): string[] {
|
|
286
|
+
const out: string[] = [];
|
|
287
|
+
const live = join(this.home, "assistants");
|
|
288
|
+
if (existsSync(live)) for (const id of readdirSync(live)) out.push(assistantJobsDir(this.home, id));
|
|
289
|
+
const archived = archiveAssistantsDir(this.home);
|
|
290
|
+
if (existsSync(archived)) for (const folder of readdirSync(archived)) out.push(join(archived, folder, "jobs"));
|
|
291
|
+
return out.filter((dir) => existsSync(dir));
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
private rebuildJobIndex(): Map<string, string> {
|
|
295
|
+
const index = new Map<string, string>();
|
|
296
|
+
for (const root of this.jobRoots()) {
|
|
297
|
+
for (const id of readdirSync(root).sort()) {
|
|
298
|
+
const dir = join(root, id);
|
|
299
|
+
if (existsSync(join(dir, "job.json"))) index.set(id, dir);
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
this.jobIndex = index;
|
|
303
|
+
return index;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
private findJobDir(id: string): string | undefined {
|
|
307
|
+
const hit = (this.jobIndex ?? this.rebuildJobIndex()).get(id);
|
|
308
|
+
if (hit) return hit;
|
|
309
|
+
return this.rebuildJobIndex().get(id);
|
|
310
|
+
}
|
|
311
|
+
|
|
231
312
|
jobDir(id: string): string {
|
|
232
|
-
return join(this.home, "jobs", id);
|
|
313
|
+
return this.findJobDir(id) ?? join(this.home, "assistants", "hob", "jobs", id);
|
|
233
314
|
}
|
|
234
315
|
|
|
235
316
|
jobs(): Job[] {
|
|
236
|
-
const root = join(this.home, "jobs");
|
|
237
|
-
if (!existsSync(root)) return [];
|
|
238
317
|
const out: Job[] = [];
|
|
239
|
-
for (const
|
|
240
|
-
const job = readJson<Job | null>(join(
|
|
318
|
+
for (const dir of this.rebuildJobIndex().values()) {
|
|
319
|
+
const job = readJson<Job | null>(join(dir, "job.json"), null);
|
|
241
320
|
if (job) out.push(job);
|
|
242
321
|
}
|
|
243
322
|
return out.sort((a, b) => a.createdAt.localeCompare(b.createdAt));
|
|
244
323
|
}
|
|
245
324
|
|
|
246
325
|
job(id: string): Job | undefined {
|
|
247
|
-
|
|
326
|
+
const dir = this.findJobDir(id);
|
|
327
|
+
return dir ? readJson<Job | null>(join(dir, "job.json"), null) ?? undefined : undefined;
|
|
248
328
|
}
|
|
249
329
|
|
|
250
330
|
saveJob(job: Job): void {
|
|
251
|
-
|
|
331
|
+
const dir = this.findJobDir(job.id) ?? join(assistantJobsDir(this.home, job.assistantId), job.id);
|
|
332
|
+
writeJsonAtomic(join(dir, "job.json"), job);
|
|
333
|
+
this.jobIndex ??= new Map();
|
|
334
|
+
this.jobIndex.set(job.id, dir);
|
|
252
335
|
}
|
|
253
336
|
|
|
254
337
|
writeReport(jobId: string, text: string): string {
|
|
@@ -285,10 +368,10 @@ export class Store {
|
|
|
285
368
|
*/
|
|
286
369
|
migrateMemories(): void {
|
|
287
370
|
type OldDraft = Draft & { memories?: string[]; carry: Draft["carry"] & { memories?: number } };
|
|
288
|
-
const drafts = readJson<OldDraft[]>(this.
|
|
371
|
+
const drafts = readJson<OldDraft[]>(this.hobFile("drafts.json"), []);
|
|
289
372
|
if (drafts.some((draft) => !Array.isArray(draft.instructions))) {
|
|
290
373
|
writeJsonAtomic(
|
|
291
|
-
this.
|
|
374
|
+
this.hobFile("drafts.json"),
|
|
292
375
|
drafts.map(({ memories, carry: { memories: _carried, ...carry }, ...draft }) => ({
|
|
293
376
|
...draft,
|
|
294
377
|
instructions: draft.instructions ?? memories ?? [],
|
package/server/src/transcript.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// 任务过程(transcript):从任务的 worker.jsonl 推导步骤列表,实时事件和落盘推导共用一套拼装。
|
|
2
2
|
import { existsSync, readFileSync } from "node:fs";
|
|
3
|
+
import { isAbsolute } from "node:path";
|
|
3
4
|
import { commandSummary } from "../../shared/command.ts";
|
|
4
5
|
import type { JobStep } from "../../shared/types.ts";
|
|
5
6
|
import { showPath } from "./text.ts";
|
|
@@ -48,8 +49,10 @@ function clipEdit(edits: unknown): { oldText: string; newText: string }[] {
|
|
|
48
49
|
}
|
|
49
50
|
|
|
50
51
|
/** 只留详情页要看的那几个参数(write 的正文不整段传,只给长度)。 */
|
|
51
|
-
export function toolArgs(name: string, raw: Record<string, unknown>, cwd = ""): Record<string, unknown> {
|
|
52
|
-
const
|
|
52
|
+
export function toolArgs(name: string, raw: Record<string, unknown>, cwd = "", resolvePath?: (path: string) => string): Record<string, unknown> {
|
|
53
|
+
const given = String(raw.path ?? "");
|
|
54
|
+
// 迁移前写进会话的绝对路径先按 moves.json 换成新位置再和 cwd 比:老步骤照样显示成短文件名
|
|
55
|
+
const path = showPath(isAbsolute(given) && resolvePath ? resolvePath(given) : given, cwd);
|
|
53
56
|
switch (name) {
|
|
54
57
|
case "bash":
|
|
55
58
|
return { command: String(raw.command ?? "") };
|
|
@@ -207,11 +210,13 @@ export interface ToolStepInput {
|
|
|
207
210
|
running?: boolean;
|
|
208
211
|
/** MCP 工具的显示名映射(转义后的服务名 → 配置里的原名) */
|
|
209
212
|
mcpServers?: Record<string, string>;
|
|
213
|
+
/** 会话里的老绝对路径换到新位置(迁移后显示老步骤用) */
|
|
214
|
+
resolvePath?: (path: string) => string;
|
|
210
215
|
}
|
|
211
216
|
|
|
212
217
|
/** 一次工具调用 → 一个步骤(还没有结果)。实时事件和落盘推导都用它,页面按 id 合并。 */
|
|
213
218
|
export function toolStep(input: ToolStepInput): JobStep {
|
|
214
|
-
const args = toolArgs(input.name, input.args, input.cwd ?? "");
|
|
219
|
+
const args = toolArgs(input.name, input.args, input.cwd ?? "", input.resolvePath);
|
|
215
220
|
const mcp = mcpToolDisplay(input.name, input.mcpServers);
|
|
216
221
|
return {
|
|
217
222
|
id: input.id,
|
|
@@ -255,6 +260,8 @@ export interface DeriveOptions {
|
|
|
255
260
|
save?: (stepId: string, text: string) => string;
|
|
256
261
|
/** MCP 工具的显示名映射(转义后的服务名 → 配置里的原名) */
|
|
257
262
|
mcpServers?: Record<string, string>;
|
|
263
|
+
/** 会话里的老绝对路径换到新位置(迁移后显示老步骤用) */
|
|
264
|
+
resolvePath?: (path: string) => string;
|
|
258
265
|
}
|
|
259
266
|
|
|
260
267
|
/** 从任务会话文件推导整个过程:说明文字、思考摘要、工具调用(含结果)。 */
|
|
@@ -300,6 +307,7 @@ export function deriveSteps(file: string, options: DeriveOptions = {}): JobStep[
|
|
|
300
307
|
at,
|
|
301
308
|
cwd: options.cwd ?? "",
|
|
302
309
|
mcpServers: options.mcpServers,
|
|
310
|
+
resolvePath: options.resolvePath,
|
|
303
311
|
running: true,
|
|
304
312
|
});
|
|
305
313
|
toolIndex.set(step.id, steps.length);
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
// 模型单价:默认从 models.dev 拉,一天最多更新一次;存在 HOB_HOME/usage-prices.json,拉不到就用上次的。
|
|
1
|
+
// 模型单价:默认从 models.dev 拉,一天最多更新一次;存在 HOB_HOME/cache/usage-prices.json,拉不到就用上次的。
|
|
2
2
|
import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
|
|
3
3
|
import { dirname, join } from "node:path";
|
|
4
4
|
import type { TokenPrice } from "../../shared/types.ts";
|
|
5
|
+
import { cacheDir } from "./config.ts";
|
|
5
6
|
|
|
6
7
|
const SOURCE = "https://models.dev/api.json";
|
|
7
8
|
const FILE = "usage-prices.json";
|
|
@@ -13,7 +14,7 @@ const TIMEOUT = 10_000;
|
|
|
13
14
|
/** 模型厂商自己那一家:同一个模型 id 出现在多个服务商里时优先它。 */
|
|
14
15
|
const VENDOR_PROVIDERS = new Set(["openai", "anthropic", "deepseek", "google", "xai", "mistral", "zhipuai", "moonshotai"]);
|
|
15
16
|
|
|
16
|
-
/** HOB_HOME/usage-prices.json:拉到的单价 + 上次更新和上次尝试的时间。 */
|
|
17
|
+
/** HOB_HOME/cache/usage-prices.json:拉到的单价 + 上次更新和上次尝试的时间。 */
|
|
17
18
|
interface PriceFile {
|
|
18
19
|
fetchedAt: string | null;
|
|
19
20
|
attemptAt: string | null;
|
|
@@ -24,7 +25,7 @@ interface PriceFile {
|
|
|
24
25
|
const EMPTY: PriceFile = { fetchedAt: null, attemptAt: null, error: null, prices: {} };
|
|
25
26
|
|
|
26
27
|
function readFile(home: string): PriceFile {
|
|
27
|
-
const path = join(home, FILE);
|
|
28
|
+
const path = join(cacheDir(home), FILE);
|
|
28
29
|
if (!existsSync(path)) return EMPTY;
|
|
29
30
|
try {
|
|
30
31
|
const stored = JSON.parse(readFileSync(path, "utf8")) as Partial<PriceFile>;
|
|
@@ -156,7 +157,7 @@ export class UsagePrices {
|
|
|
156
157
|
}
|
|
157
158
|
|
|
158
159
|
private write(): void {
|
|
159
|
-
const path = join(this.options.home, FILE);
|
|
160
|
+
const path = join(cacheDir(this.options.home), FILE);
|
|
160
161
|
mkdirSync(dirname(path), { recursive: true });
|
|
161
162
|
const tmp = `${path}.${process.pid}.tmp`;
|
|
162
163
|
writeFileSync(tmp, `${JSON.stringify(this.file, null, 2)}\n`);
|
package/server/src/usage.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// 消耗:不另外记账,直接从会话文件里 assistant 消息的 usage 统计(见 docs/architecture.md“消耗”)。
|
|
2
2
|
import { createReadStream, existsSync, readFileSync, readdirSync, statSync } from "node:fs";
|
|
3
|
-
import { join } from "node:path";
|
|
3
|
+
import { basename, join } from "node:path";
|
|
4
4
|
import type { TokenPrice, UsageAssistant, UsageModelPrice, UsagePrices, UsageRow, UsageSource, UsageView } from "../../shared/types.ts";
|
|
5
5
|
import type { SettingsStore } from "./settings.ts";
|
|
6
6
|
import type { Store } from "./store.ts";
|
|
@@ -106,6 +106,17 @@ function readDir(path: string): string[] {
|
|
|
106
106
|
return existsSync(path) ? readdirSync(path) : [];
|
|
107
107
|
}
|
|
108
108
|
|
|
109
|
+
/** 所有任务的会话目录:在的助手和归档的助手都算。 */
|
|
110
|
+
function jobSessionDirs(home: string): string[] {
|
|
111
|
+
const out: string[] = [];
|
|
112
|
+
const roots = [
|
|
113
|
+
...readDir(join(home, "assistants")).map((id) => join(home, "assistants", id, "jobs")),
|
|
114
|
+
...readDir(join(home, "archive", "assistants")).map((folder) => join(home, "archive", "assistants", folder, "jobs")),
|
|
115
|
+
];
|
|
116
|
+
for (const root of roots) for (const id of readDir(root)) out.push(join(root, id));
|
|
117
|
+
return out;
|
|
118
|
+
}
|
|
119
|
+
|
|
109
120
|
function readJson(path: string): Record<string, unknown> | undefined {
|
|
110
121
|
try {
|
|
111
122
|
return JSON.parse(readFileSync(path, "utf8")) as Record<string, unknown>;
|
|
@@ -228,12 +239,11 @@ export class UsageService {
|
|
|
228
239
|
if (!assistants.has(id)) assistants.set(id, { id, name, archived: true });
|
|
229
240
|
await this.sync({ file, assistantId: id, assistantName: name, source: "chat" }, seen);
|
|
230
241
|
}
|
|
231
|
-
for (const
|
|
232
|
-
const file = join(
|
|
242
|
+
for (const jobDir of jobSessionDirs(home)) {
|
|
243
|
+
const file = join(jobDir, "worker.jsonl");
|
|
233
244
|
if (!existsSync(file)) continue;
|
|
234
|
-
// 没有缓存时才读 job.json:任务的所属助手建好就不变
|
|
235
245
|
const cached = this.files.get(file);
|
|
236
|
-
const job = cached ? undefined : this.options.store.job(
|
|
246
|
+
const job = cached ? undefined : this.options.store.job(basename(jobDir));
|
|
237
247
|
const assistantId = cached?.assistantId ?? job?.assistantId;
|
|
238
248
|
if (!assistantId) continue;
|
|
239
249
|
const live = this.options.store.assistant(assistantId);
|
package/shared/types.ts
CHANGED
|
@@ -225,6 +225,8 @@ export interface Assistant {
|
|
|
225
225
|
prop?: CustomProp;
|
|
226
226
|
primary: boolean; // 只有 hob 为 true
|
|
227
227
|
pinned: boolean; // 左栏顶部大头像
|
|
228
|
+
/** 置顶区里的位置:小的在前,拖动头像换位置时改它;没排过的排在排过的后面,再按创建时间 */
|
|
229
|
+
pinOrder?: number;
|
|
228
230
|
hidden: boolean; // 收起的助手
|
|
229
231
|
notify: boolean; // 需要确认时通知我
|
|
230
232
|
instructions: string; // 补充说明:用户可直接编辑该助手需要知道的专属事实
|
|
@@ -275,16 +277,40 @@ export interface RoutineRun {
|
|
|
275
277
|
tokens?: number; // 技能二期:输入加输出 token
|
|
276
278
|
}
|
|
277
279
|
|
|
280
|
+
export interface RoutineCheck {
|
|
281
|
+
script: string; // 到点先跑的 bash 脚本:没有输出就跳过这一次
|
|
282
|
+
then: "notify" | "job"; // 有输出时:notify 把输出作为提醒发到对话;job 带着输出开始任务
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
export interface RoutineChecks {
|
|
286
|
+
total: number; // 检查了几次
|
|
287
|
+
hits: number; // 触发了几次
|
|
288
|
+
failing?: string; // 最近一次失败的原因,下次成功就清掉
|
|
289
|
+
lastAt?: Iso; // 上次检查的时间
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/** 定时到点(或点「立即运行」)这一次的结果。 */
|
|
293
|
+
export interface RoutineTick {
|
|
294
|
+
outcome: "job" | "notify" | "quiet" | "failed"; // job 开了任务;notify 已发到对话;quiet 没变化;failed 检查失败
|
|
295
|
+
job?: Job;
|
|
296
|
+
reason?: string; // failed 时的原因
|
|
297
|
+
}
|
|
298
|
+
|
|
278
299
|
export interface Routine {
|
|
279
300
|
id: string;
|
|
280
301
|
assistantId: string;
|
|
281
302
|
name: string; // 如 "每日简报"
|
|
282
303
|
instruction: string; // 要做什么
|
|
283
|
-
schedules: string[]; // cron
|
|
304
|
+
schedules: string[]; // cron 表达式(如 "0 9 * * *")或具体时间(如 "2026-11-08T09:00");可多个("再加一个")
|
|
284
305
|
active: boolean;
|
|
285
306
|
createdAt: Iso;
|
|
286
307
|
runs: RoutineRun[]; // 最近 20 次,新的在前
|
|
287
308
|
skill?: string; // 绑定这个助手自己的技能(AssistantSkill.name):每次执行按它做
|
|
309
|
+
check?: RoutineCheck; // 到点先跑脚本,多数时候没变化就不开任务
|
|
310
|
+
checks?: RoutineChecks; // 检查计数和最近一次失败的原因
|
|
311
|
+
once?: boolean; // 做成一次就停:没有检查脚本的跑完就停,有检查的第一次有输出就停
|
|
312
|
+
stoppedAt?: Iso; // 上次停下的时间
|
|
313
|
+
stopReason?: "hit" | "finished"; // hit=按 once 停了;finished=所有执行时间都跑完了
|
|
288
314
|
}
|
|
289
315
|
|
|
290
316
|
export interface Delegation {
|