@ai-zen/agents-host 0.0.0-stage → 0.1.0-alpha.1
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/CHANGELOG.md +39 -0
- package/LICENSE +21 -0
- package/README.md +759 -2
- package/dist/app.d.ts +384 -0
- package/dist/app.js +1148 -0
- package/dist/cli.d.ts +22 -0
- package/dist/cli.js +354 -0
- package/dist/config.d.ts +97 -0
- package/dist/config.js +171 -0
- package/dist/handlers/agentHandlers.d.ts +18 -0
- package/dist/handlers/agentHandlers.js +20 -0
- package/dist/handlers/assetHandlers.d.ts +29 -0
- package/dist/handlers/assetHandlers.js +35 -0
- package/dist/handlers/capabilities.d.ts +26 -0
- package/dist/handlers/capabilities.js +138 -0
- package/dist/handlers/completionHandlers.d.ts +18 -0
- package/dist/handlers/completionHandlers.js +13 -0
- package/dist/handlers/configHandlers.d.ts +31 -0
- package/dist/handlers/configHandlers.js +59 -0
- package/dist/handlers/conversationHandlers.d.ts +118 -0
- package/dist/handlers/conversationHandlers.js +314 -0
- package/dist/handlers/handoffHandlers.d.ts +17 -0
- package/dist/handlers/handoffHandlers.js +14 -0
- package/dist/handlers/hostHandlers.d.ts +39 -0
- package/dist/handlers/hostHandlers.js +56 -0
- package/dist/handlers/index.d.ts +128 -0
- package/dist/handlers/index.js +92 -0
- package/dist/handlers/messageHandlers.d.ts +11 -0
- package/dist/handlers/messageHandlers.js +10 -0
- package/dist/handlers/modelHandlers.d.ts +17 -0
- package/dist/handlers/modelHandlers.js +19 -0
- package/dist/handlers/noticeHandlers.d.ts +24 -0
- package/dist/handlers/noticeHandlers.js +24 -0
- package/dist/handlers/questionHandlers.d.ts +16 -0
- package/dist/handlers/questionHandlers.js +16 -0
- package/dist/handlers/searchHandlers.d.ts +12 -0
- package/dist/handlers/searchHandlers.js +12 -0
- package/dist/handlers/workspaceHandlers.d.ts +29 -0
- package/dist/handlers/workspaceHandlers.js +47 -0
- package/dist/i18n/en.d.ts +243 -0
- package/dist/i18n/en.js +270 -0
- package/dist/i18n/index.d.ts +43 -0
- package/dist/i18n/index.js +72 -0
- package/dist/i18n/zh.d.ts +240 -0
- package/dist/i18n/zh.js +257 -0
- package/dist/index.d.ts +17 -0
- package/dist/index.js +15 -0
- package/dist/net/UploadReceiver.d.ts +186 -0
- package/dist/net/UploadReceiver.js +494 -0
- package/dist/net/WsHub.d.ts +231 -0
- package/dist/net/WsHub.js +689 -0
- package/dist/net/auth.d.ts +73 -0
- package/dist/net/auth.js +74 -0
- package/dist/net/discovery.d.ts +72 -0
- package/dist/net/discovery.js +171 -0
- package/dist/net/index.d.ts +14 -0
- package/dist/net/index.js +10 -0
- package/dist/runtime/ChatRunner.d.ts +374 -0
- package/dist/runtime/ChatRunner.js +1204 -0
- package/dist/runtime/ConversationDrafts.d.ts +43 -0
- package/dist/runtime/ConversationDrafts.js +47 -0
- package/dist/runtime/RunRegistry.d.ts +79 -0
- package/dist/runtime/RunRegistry.js +108 -0
- package/dist/runtime/ScopeRegistry.d.ts +153 -0
- package/dist/runtime/ScopeRegistry.js +259 -0
- package/dist/runtime/TransientRunner.d.ts +90 -0
- package/dist/runtime/TransientRunner.js +179 -0
- package/dist/runtime/index.d.ts +14 -0
- package/dist/runtime/index.js +10 -0
- package/dist/services/AgentService.d.ts +150 -0
- package/dist/services/AgentService.js +718 -0
- package/dist/services/AssetService.d.ts +338 -0
- package/dist/services/AssetService.js +739 -0
- package/dist/services/AutoNaming.d.ts +92 -0
- package/dist/services/AutoNaming.js +235 -0
- package/dist/services/CompletionService.d.ts +122 -0
- package/dist/services/CompletionService.js +442 -0
- package/dist/services/ConfigService.d.ts +171 -0
- package/dist/services/ConfigService.js +695 -0
- package/dist/services/ConversationService.d.ts +274 -0
- package/dist/services/ConversationService.js +902 -0
- package/dist/services/CredentialService.d.ts +33 -0
- package/dist/services/CredentialService.js +86 -0
- package/dist/services/DiscoveryService.d.ts +182 -0
- package/dist/services/DiscoveryService.js +998 -0
- package/dist/services/HandoffService.d.ts +189 -0
- package/dist/services/HandoffService.js +414 -0
- package/dist/services/Logger.d.ts +130 -0
- package/dist/services/Logger.js +256 -0
- package/dist/services/MessageService.d.ts +88 -0
- package/dist/services/MessageService.js +280 -0
- package/dist/services/ModelService.d.ts +89 -0
- package/dist/services/ModelService.js +489 -0
- package/dist/services/NoticeService.d.ts +93 -0
- package/dist/services/NoticeService.js +217 -0
- package/dist/services/QuestionService.d.ts +184 -0
- package/dist/services/QuestionService.js +457 -0
- package/dist/services/SearchService.d.ts +54 -0
- package/dist/services/SearchService.js +117 -0
- package/dist/services/WorkspaceService.d.ts +96 -0
- package/dist/services/WorkspaceService.js +213 -0
- package/dist/services/imageSize.d.ts +41 -0
- package/dist/services/imageSize.js +146 -0
- package/dist/services/index.d.ts +20 -0
- package/dist/services/index.js +15 -0
- package/dist/services/secretMask.d.ts +24 -0
- package/dist/services/secretMask.js +34 -0
- package/dist/storage/AssetRepository.d.ts +87 -0
- package/dist/storage/AssetRepository.js +180 -0
- package/dist/storage/ConversationRepository.d.ts +55 -0
- package/dist/storage/ConversationRepository.js +128 -0
- package/dist/storage/CredentialRepository.d.ts +28 -0
- package/dist/storage/CredentialRepository.js +47 -0
- package/dist/storage/Database.d.ts +95 -0
- package/dist/storage/Database.js +180 -0
- package/dist/storage/MessageRepository.d.ts +104 -0
- package/dist/storage/MessageRepository.js +323 -0
- package/dist/storage/MetaRepository.d.ts +21 -0
- package/dist/storage/MetaRepository.js +33 -0
- package/dist/storage/NoticeRepository.d.ts +31 -0
- package/dist/storage/NoticeRepository.js +89 -0
- package/dist/storage/QuestionRepository.d.ts +34 -0
- package/dist/storage/QuestionRepository.js +96 -0
- package/dist/storage/WorkspaceRepository.d.ts +25 -0
- package/dist/storage/WorkspaceRepository.js +44 -0
- package/dist/storage/dbWorker.d.ts +10 -0
- package/dist/storage/dbWorker.js +132 -0
- package/dist/storage/index.d.ts +20 -0
- package/dist/storage/index.js +16 -0
- package/dist/storage/migrations.d.ts +26 -0
- package/dist/storage/migrations.js +207 -0
- package/dist/storage/types.d.ts +298 -0
- package/dist/storage/types.js +14 -0
- package/dist/wire/errors.d.ts +105 -0
- package/dist/wire/errors.js +143 -0
- package/dist/wire/events.d.ts +102 -0
- package/dist/wire/events.js +13 -0
- package/dist/wire/frames.d.ts +106 -0
- package/dist/wire/frames.js +156 -0
- package/dist/wire/index.d.ts +31 -0
- package/dist/wire/index.js +31 -0
- package/dist/wire/methods.d.ts +1896 -0
- package/dist/wire/methods.js +99 -0
- package/dist/wire/states.d.ts +487 -0
- package/dist/wire/states.js +61 -0
- package/package.json +63 -3
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 日志落文件(PRD F-J2):`<dataRoot>/logs/host.log` + 按大小轮转 + warn/error 同时上通知。
|
|
3
|
+
*
|
|
4
|
+
* 为什么要有它:界面被一段对话占满时,`process.stdout` 里的话没人看得到。宿主是常驻后台
|
|
5
|
+
* 进程,出事的那一刻只有磁盘上的文件是还在的(PRD F-J2 的原话就是这个理由)。
|
|
6
|
+
*
|
|
7
|
+
* 三件事在这里定死:
|
|
8
|
+
*
|
|
9
|
+
* 1. **按大小轮转**:单份到 `maxBytes`(缺省 5MB)就换下一份,保留 `maxFiles` 份(缺省 5,含
|
|
10
|
+
* 正在写的那份)。命名是 `host.log`(正在写)、`host.log.1`(上一份)……,最老的一份在
|
|
11
|
+
* 轮转时删掉。不引依赖:`node:fs` 的 `rename` / `rm` 就够。
|
|
12
|
+
* 2. **warn / error 上通知**(F-J2):调注入的 `notifier`(组装处接的是 `NoticeService.log`,
|
|
13
|
+
* 永久通知)。**防回环**靠一个重入标记:发通知这件事本身产生的日志只落文件、不再上通知,
|
|
14
|
+
* 否则通知写库失败会再产生一条通知,一路循环下去。
|
|
15
|
+
* 3. **退出时 flush**(`close()`):先停止接纳新行,再把已经排队的行写完,最后关流。
|
|
16
|
+
* 进程退出前没写出去的行等于没写,所以 `HostApp.stop()` 里等它。
|
|
17
|
+
*
|
|
18
|
+
* 行格式:`<ISO 时间> <级别> <来源> <消息> [detail JSON]`。级别定宽(`INFO ` / `WARN ` /
|
|
19
|
+
* `ERROR`),于是 grep 一列就能筛。时间取注入的时钟,缺省 `Date.now`。
|
|
20
|
+
*/
|
|
21
|
+
import type { HubLogger } from "../net/WsHub.js";
|
|
22
|
+
/** 日志级别(三档:信息 / 警告 / 错误;warn 与 error 会同时上通知) */
|
|
23
|
+
export type LogLevel = "info" | "warn" | "error";
|
|
24
|
+
/** 单份日志文件的上限(5MB;到点换下一份) */
|
|
25
|
+
export declare const DEFAULT_MAX_BYTES: number;
|
|
26
|
+
/** 保留几份(含正在写的那一份) */
|
|
27
|
+
export declare const DEFAULT_MAX_FILES = 5;
|
|
28
|
+
/** 日志文件的缺省名字 */
|
|
29
|
+
export declare const DEFAULT_LOG_FILE_NAME = "host.log";
|
|
30
|
+
/**
|
|
31
|
+
* 「这条 warn / error 也让用户在通知栏里看到」(PRD F-J2)。
|
|
32
|
+
*
|
|
33
|
+
* 组装处接的是 `NoticeService.log`(永久通知)。这里刻意不传 `Error` 对象:通知只带一句
|
|
34
|
+
* 给人看的摘要,栈留在文件里。
|
|
35
|
+
*/
|
|
36
|
+
export type LogNotifier = (level: "warning" | "error", summary: string) => void | Promise<void>;
|
|
37
|
+
/** 带来源的日志口子(`withSource` 的返回类型):比 `HubLogger` 多一档 info */
|
|
38
|
+
export interface SourceLogger extends HubLogger {
|
|
39
|
+
info(message: string, detail?: unknown): void;
|
|
40
|
+
}
|
|
41
|
+
export interface LoggerOptions {
|
|
42
|
+
/** 日志目录(`HostPaths.logsDir` = `<dataRoot>/logs`);首次落盘时创建 */
|
|
43
|
+
dir: string;
|
|
44
|
+
/** 文件名,缺省 `host.log` */
|
|
45
|
+
fileName?: string;
|
|
46
|
+
/** 单份上限(字节),缺省 5MB */
|
|
47
|
+
maxBytes?: number;
|
|
48
|
+
/** 保留几份(含正在写的那份),缺省 5 */
|
|
49
|
+
maxFiles?: number;
|
|
50
|
+
/** 缺省来源标签(`withSource` 之外的写法用它),缺省 `host` */
|
|
51
|
+
source?: string;
|
|
52
|
+
/** warn / error 上通知的出口;不给时只落文件 */
|
|
53
|
+
notifier?: LogNotifier;
|
|
54
|
+
/** 时钟,用例注入用;缺省 `Date.now` */
|
|
55
|
+
now?: () => number;
|
|
56
|
+
}
|
|
57
|
+
export declare class Logger implements HubLogger, SourceLogger {
|
|
58
|
+
private readonly dir;
|
|
59
|
+
private readonly fileName;
|
|
60
|
+
private readonly maxBytes;
|
|
61
|
+
private readonly maxFiles;
|
|
62
|
+
private readonly source;
|
|
63
|
+
private readonly now;
|
|
64
|
+
private notifier;
|
|
65
|
+
/** 正在发通知:这期间产生的 warn / error 只落文件(防回环) */
|
|
66
|
+
private notifying;
|
|
67
|
+
private stream;
|
|
68
|
+
/** 正在写的那份已经写了多少字节(轮转判据) */
|
|
69
|
+
private bytes;
|
|
70
|
+
/** 写盘按到达顺序串行(轮转要等当前流关掉,不能被打断) */
|
|
71
|
+
private queue;
|
|
72
|
+
/** 收尾已经开始 / 结束:之后的行不再落盘(否则 `close()` 之后还会长出新内容) */
|
|
73
|
+
private closed;
|
|
74
|
+
private closing;
|
|
75
|
+
constructor(options: LoggerOptions);
|
|
76
|
+
info(message: string, detail?: unknown): void;
|
|
77
|
+
warn(message: string, detail?: unknown): void;
|
|
78
|
+
error(message: string, detail?: unknown): void;
|
|
79
|
+
/**
|
|
80
|
+
* 写一行(级别 + 来源 + 消息 + 可选的 detail),warn / error 顺带上通知。
|
|
81
|
+
*
|
|
82
|
+
* 落盘是排队的(`queue`),所以调用方不必等:一行日志的代价不该被写盘拖住。
|
|
83
|
+
*/
|
|
84
|
+
log(level: LogLevel, source: string, message: string, detail?: unknown): void;
|
|
85
|
+
/** 换一个来源标签的视图:接线层写 `ws`、单根写 `host`、素材层写 `assets`,落盘时带来源 */
|
|
86
|
+
withSource(source: string): SourceLogger;
|
|
87
|
+
/**
|
|
88
|
+
* 接上(或换掉)通知出口。
|
|
89
|
+
*
|
|
90
|
+
* 单根里日志口子比通知服务先造出来(早造的那些服务也要能记日志),所以这里允许后接一次。
|
|
91
|
+
*/
|
|
92
|
+
setNotifier(notifier: LogNotifier | null): void;
|
|
93
|
+
/** 现在这份日志写到哪(`host.log` 的完整路径),供启动提示与用例读 */
|
|
94
|
+
get filePath(): string;
|
|
95
|
+
/** 收尾是否已经做完(用例断言「退出后流关闭」用它) */
|
|
96
|
+
get isClosed(): boolean;
|
|
97
|
+
/**
|
|
98
|
+
* 收尾:停止接纳新行 → 把排队的行写完 → 关流。
|
|
99
|
+
*
|
|
100
|
+
* 幂等:`HostApp.stop()` 可能被多个触发源先后叫到。
|
|
101
|
+
*/
|
|
102
|
+
close(): Promise<void>;
|
|
103
|
+
private doClose;
|
|
104
|
+
private writeLine;
|
|
105
|
+
/** 拿当前要写的流(懒开:没写过日志的进程不必在数据根下留一个空文件) */
|
|
106
|
+
private ensureStream;
|
|
107
|
+
/**
|
|
108
|
+
* 轮转:关掉当前流 → `host.log` 改名为 `.1`,其余依次后移 → 最老的一份删掉。
|
|
109
|
+
*
|
|
110
|
+
* 下一行由调用方重新开流(`ensureStream` 会按新文件从 0 开始计数)。
|
|
111
|
+
*/
|
|
112
|
+
private rotate;
|
|
113
|
+
/** 关闭当前流并把它写完(这一步就是 flush) */
|
|
114
|
+
private endStream;
|
|
115
|
+
/** 第 index 份文件的路径;`0` 是正在写的那份 */
|
|
116
|
+
private pathOf;
|
|
117
|
+
/**
|
|
118
|
+
* warn / error 也上通知栏。
|
|
119
|
+
*
|
|
120
|
+
* 重入标记是防回环的那一处:发通知会写库、会推事件,那些动作里万一又冒出日志,
|
|
121
|
+
* 这条判断把它们挡在「只落文件」这一侧。通知本身失败只吞掉——再记一条日志就是回环。
|
|
122
|
+
*/
|
|
123
|
+
private raiseNotice;
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* 造一行日志。
|
|
127
|
+
*
|
|
128
|
+
* 级别定宽(5 列),`detail` 压成一行 JSON —— 一行一条记录,`tail -f` 与 grep 都直接可用。
|
|
129
|
+
*/
|
|
130
|
+
export declare function formatLine(timestamp: number, level: LogLevel, source: string, message: string, detail?: unknown): string;
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 日志落文件(PRD F-J2):`<dataRoot>/logs/host.log` + 按大小轮转 + warn/error 同时上通知。
|
|
3
|
+
*
|
|
4
|
+
* 为什么要有它:界面被一段对话占满时,`process.stdout` 里的话没人看得到。宿主是常驻后台
|
|
5
|
+
* 进程,出事的那一刻只有磁盘上的文件是还在的(PRD F-J2 的原话就是这个理由)。
|
|
6
|
+
*
|
|
7
|
+
* 三件事在这里定死:
|
|
8
|
+
*
|
|
9
|
+
* 1. **按大小轮转**:单份到 `maxBytes`(缺省 5MB)就换下一份,保留 `maxFiles` 份(缺省 5,含
|
|
10
|
+
* 正在写的那份)。命名是 `host.log`(正在写)、`host.log.1`(上一份)……,最老的一份在
|
|
11
|
+
* 轮转时删掉。不引依赖:`node:fs` 的 `rename` / `rm` 就够。
|
|
12
|
+
* 2. **warn / error 上通知**(F-J2):调注入的 `notifier`(组装处接的是 `NoticeService.log`,
|
|
13
|
+
* 永久通知)。**防回环**靠一个重入标记:发通知这件事本身产生的日志只落文件、不再上通知,
|
|
14
|
+
* 否则通知写库失败会再产生一条通知,一路循环下去。
|
|
15
|
+
* 3. **退出时 flush**(`close()`):先停止接纳新行,再把已经排队的行写完,最后关流。
|
|
16
|
+
* 进程退出前没写出去的行等于没写,所以 `HostApp.stop()` 里等它。
|
|
17
|
+
*
|
|
18
|
+
* 行格式:`<ISO 时间> <级别> <来源> <消息> [detail JSON]`。级别定宽(`INFO ` / `WARN ` /
|
|
19
|
+
* `ERROR`),于是 grep 一列就能筛。时间取注入的时钟,缺省 `Date.now`。
|
|
20
|
+
*/
|
|
21
|
+
import { createWriteStream, mkdirSync, renameSync, rmSync, statSync } from "node:fs";
|
|
22
|
+
import { join } from "node:path";
|
|
23
|
+
/** 单份日志文件的上限(5MB;到点换下一份) */
|
|
24
|
+
export const DEFAULT_MAX_BYTES = 5 * 1024 * 1024;
|
|
25
|
+
/** 保留几份(含正在写的那一份) */
|
|
26
|
+
export const DEFAULT_MAX_FILES = 5;
|
|
27
|
+
/** 日志文件的缺省名字 */
|
|
28
|
+
export const DEFAULT_LOG_FILE_NAME = "host.log";
|
|
29
|
+
export class Logger {
|
|
30
|
+
dir;
|
|
31
|
+
fileName;
|
|
32
|
+
maxBytes;
|
|
33
|
+
maxFiles;
|
|
34
|
+
source;
|
|
35
|
+
now;
|
|
36
|
+
notifier;
|
|
37
|
+
/** 正在发通知:这期间产生的 warn / error 只落文件(防回环) */
|
|
38
|
+
notifying = false;
|
|
39
|
+
stream = null;
|
|
40
|
+
/** 正在写的那份已经写了多少字节(轮转判据) */
|
|
41
|
+
bytes = 0;
|
|
42
|
+
/** 写盘按到达顺序串行(轮转要等当前流关掉,不能被打断) */
|
|
43
|
+
queue = Promise.resolve();
|
|
44
|
+
/** 收尾已经开始 / 结束:之后的行不再落盘(否则 `close()` 之后还会长出新内容) */
|
|
45
|
+
closed = false;
|
|
46
|
+
closing = null;
|
|
47
|
+
constructor(options) {
|
|
48
|
+
this.dir = options.dir;
|
|
49
|
+
this.fileName = options.fileName ?? DEFAULT_LOG_FILE_NAME;
|
|
50
|
+
this.maxBytes = options.maxBytes ?? DEFAULT_MAX_BYTES;
|
|
51
|
+
this.maxFiles = Math.max(1, options.maxFiles ?? DEFAULT_MAX_FILES);
|
|
52
|
+
this.source = options.source ?? "host";
|
|
53
|
+
this.notifier = options.notifier ?? null;
|
|
54
|
+
this.now = options.now ?? Date.now;
|
|
55
|
+
}
|
|
56
|
+
// -------------------------------------------------------------------------
|
|
57
|
+
// 对外:三档写法
|
|
58
|
+
// -------------------------------------------------------------------------
|
|
59
|
+
info(message, detail) {
|
|
60
|
+
this.log("info", this.source, message, detail);
|
|
61
|
+
}
|
|
62
|
+
warn(message, detail) {
|
|
63
|
+
this.log("warn", this.source, message, detail);
|
|
64
|
+
}
|
|
65
|
+
error(message, detail) {
|
|
66
|
+
this.log("error", this.source, message, detail);
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* 写一行(级别 + 来源 + 消息 + 可选的 detail),warn / error 顺带上通知。
|
|
70
|
+
*
|
|
71
|
+
* 落盘是排队的(`queue`),所以调用方不必等:一行日志的代价不该被写盘拖住。
|
|
72
|
+
*/
|
|
73
|
+
log(level, source, message, detail) {
|
|
74
|
+
if (this.closed)
|
|
75
|
+
return;
|
|
76
|
+
const line = formatLine(this.now(), level, source, message, detail);
|
|
77
|
+
this.queue = this.queue.then(() => this.writeLine(line)).catch(() => undefined);
|
|
78
|
+
if (level !== "info")
|
|
79
|
+
this.raiseNotice(level, message);
|
|
80
|
+
}
|
|
81
|
+
/** 换一个来源标签的视图:接线层写 `ws`、单根写 `host`、素材层写 `assets`,落盘时带来源 */
|
|
82
|
+
withSource(source) {
|
|
83
|
+
return {
|
|
84
|
+
info: (message, detail) => this.log("info", source, message, detail),
|
|
85
|
+
warn: (message, detail) => this.log("warn", source, message, detail),
|
|
86
|
+
error: (message, detail) => this.log("error", source, message, detail),
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* 接上(或换掉)通知出口。
|
|
91
|
+
*
|
|
92
|
+
* 单根里日志口子比通知服务先造出来(早造的那些服务也要能记日志),所以这里允许后接一次。
|
|
93
|
+
*/
|
|
94
|
+
setNotifier(notifier) {
|
|
95
|
+
this.notifier = notifier;
|
|
96
|
+
}
|
|
97
|
+
/** 现在这份日志写到哪(`host.log` 的完整路径),供启动提示与用例读 */
|
|
98
|
+
get filePath() {
|
|
99
|
+
return join(this.dir, this.fileName);
|
|
100
|
+
}
|
|
101
|
+
/** 收尾是否已经做完(用例断言「退出后流关闭」用它) */
|
|
102
|
+
get isClosed() {
|
|
103
|
+
return this.closed;
|
|
104
|
+
}
|
|
105
|
+
// -------------------------------------------------------------------------
|
|
106
|
+
// 收尾
|
|
107
|
+
// -------------------------------------------------------------------------
|
|
108
|
+
/**
|
|
109
|
+
* 收尾:停止接纳新行 → 把排队的行写完 → 关流。
|
|
110
|
+
*
|
|
111
|
+
* 幂等:`HostApp.stop()` 可能被多个触发源先后叫到。
|
|
112
|
+
*/
|
|
113
|
+
close() {
|
|
114
|
+
this.closing ??= this.doClose();
|
|
115
|
+
return this.closing;
|
|
116
|
+
}
|
|
117
|
+
async doClose() {
|
|
118
|
+
this.closed = true;
|
|
119
|
+
await this.queue;
|
|
120
|
+
await this.endStream();
|
|
121
|
+
}
|
|
122
|
+
// -------------------------------------------------------------------------
|
|
123
|
+
// 落盘
|
|
124
|
+
// -------------------------------------------------------------------------
|
|
125
|
+
async writeLine(line) {
|
|
126
|
+
const size = Buffer.byteLength(line);
|
|
127
|
+
// 这一行自己就超上限时照写:写不进去比多占一份文件更糟
|
|
128
|
+
if (this.bytes > 0 && this.bytes + size > this.maxBytes)
|
|
129
|
+
await this.rotate();
|
|
130
|
+
const stream = this.ensureStream();
|
|
131
|
+
if (stream === null)
|
|
132
|
+
return;
|
|
133
|
+
this.bytes += size;
|
|
134
|
+
stream.write(line);
|
|
135
|
+
}
|
|
136
|
+
/** 拿当前要写的流(懒开:没写过日志的进程不必在数据根下留一个空文件) */
|
|
137
|
+
ensureStream() {
|
|
138
|
+
if (this.stream !== null)
|
|
139
|
+
return this.stream;
|
|
140
|
+
try {
|
|
141
|
+
mkdirSync(this.dir, { recursive: true });
|
|
142
|
+
this.bytes = sizeOf(this.filePath);
|
|
143
|
+
const stream = createWriteStream(this.filePath, { flags: "a" });
|
|
144
|
+
// 写盘本身失败时没有更好的去处(日志不能再抱怨日志),关掉它、后续行直接丢
|
|
145
|
+
stream.on("error", () => {
|
|
146
|
+
this.stream = null;
|
|
147
|
+
});
|
|
148
|
+
this.stream = stream;
|
|
149
|
+
return stream;
|
|
150
|
+
}
|
|
151
|
+
catch {
|
|
152
|
+
return null;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* 轮转:关掉当前流 → `host.log` 改名为 `.1`,其余依次后移 → 最老的一份删掉。
|
|
157
|
+
*
|
|
158
|
+
* 下一行由调用方重新开流(`ensureStream` 会按新文件从 0 开始计数)。
|
|
159
|
+
*/
|
|
160
|
+
async rotate() {
|
|
161
|
+
await this.endStream();
|
|
162
|
+
const oldest = this.pathOf(this.maxFiles - 1);
|
|
163
|
+
if (this.maxFiles > 1)
|
|
164
|
+
rmSync(oldest, { force: true });
|
|
165
|
+
for (let index = this.maxFiles - 2; index >= 1; index -= 1) {
|
|
166
|
+
const from = this.pathOf(index);
|
|
167
|
+
if (!exists(from))
|
|
168
|
+
continue;
|
|
169
|
+
renameSync(from, this.pathOf(index + 1));
|
|
170
|
+
}
|
|
171
|
+
if (exists(this.filePath))
|
|
172
|
+
renameSync(this.filePath, this.pathOf(1));
|
|
173
|
+
}
|
|
174
|
+
/** 关闭当前流并把它写完(这一步就是 flush) */
|
|
175
|
+
async endStream() {
|
|
176
|
+
const stream = this.stream;
|
|
177
|
+
this.stream = null;
|
|
178
|
+
this.bytes = 0;
|
|
179
|
+
if (stream === null)
|
|
180
|
+
return;
|
|
181
|
+
await new Promise((resolve) => {
|
|
182
|
+
stream.end(() => resolve());
|
|
183
|
+
// 已经坏掉的流不会再触发 finish:不让收尾挂在这里
|
|
184
|
+
stream.once("error", () => resolve());
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
/** 第 index 份文件的路径;`0` 是正在写的那份 */
|
|
188
|
+
pathOf(index) {
|
|
189
|
+
return index === 0 ? this.filePath : `${this.filePath}.${index}`;
|
|
190
|
+
}
|
|
191
|
+
// -------------------------------------------------------------------------
|
|
192
|
+
// 通知(F-J2)
|
|
193
|
+
// -------------------------------------------------------------------------
|
|
194
|
+
/**
|
|
195
|
+
* warn / error 也上通知栏。
|
|
196
|
+
*
|
|
197
|
+
* 重入标记是防回环的那一处:发通知会写库、会推事件,那些动作里万一又冒出日志,
|
|
198
|
+
* 这条判断把它们挡在「只落文件」这一侧。通知本身失败只吞掉——再记一条日志就是回环。
|
|
199
|
+
*/
|
|
200
|
+
raiseNotice(level, summary) {
|
|
201
|
+
const notifier = this.notifier;
|
|
202
|
+
if (notifier === null || this.notifying)
|
|
203
|
+
return;
|
|
204
|
+
const noticeLevel = level === "error" ? "error" : "warning";
|
|
205
|
+
this.notifying = true;
|
|
206
|
+
void Promise.resolve()
|
|
207
|
+
.then(() => notifier(noticeLevel, summary))
|
|
208
|
+
.catch(() => undefined)
|
|
209
|
+
.finally(() => {
|
|
210
|
+
this.notifying = false;
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
// ---------------------------------------------------------------------------
|
|
215
|
+
// 纯函数
|
|
216
|
+
// ---------------------------------------------------------------------------
|
|
217
|
+
/**
|
|
218
|
+
* 造一行日志。
|
|
219
|
+
*
|
|
220
|
+
* 级别定宽(5 列),`detail` 压成一行 JSON —— 一行一条记录,`tail -f` 与 grep 都直接可用。
|
|
221
|
+
*/
|
|
222
|
+
export function formatLine(timestamp, level, source, message, detail) {
|
|
223
|
+
const head = `${new Date(timestamp).toISOString()} ${level.toUpperCase().padEnd(5)} ${source} ${message}`;
|
|
224
|
+
const tail = detail === undefined ? "" : ` ${stringify(detail)}`;
|
|
225
|
+
return `${head}${tail}\n`;
|
|
226
|
+
}
|
|
227
|
+
function stringify(detail) {
|
|
228
|
+
if (detail instanceof Error) {
|
|
229
|
+
return JSON.stringify({ message: detail.message, stack: detail.stack });
|
|
230
|
+
}
|
|
231
|
+
try {
|
|
232
|
+
const text = JSON.stringify(detail);
|
|
233
|
+
return text === undefined ? String(detail) : text;
|
|
234
|
+
}
|
|
235
|
+
catch {
|
|
236
|
+
// 循环引用之类结构:给一个能看出「有这么个东西」的占位,不让整行丢掉
|
|
237
|
+
return "[unserializable]";
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
function exists(path) {
|
|
241
|
+
try {
|
|
242
|
+
statSync(path);
|
|
243
|
+
return true;
|
|
244
|
+
}
|
|
245
|
+
catch {
|
|
246
|
+
return false;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
function sizeOf(path) {
|
|
250
|
+
try {
|
|
251
|
+
return statSync(path).size;
|
|
252
|
+
}
|
|
253
|
+
catch {
|
|
254
|
+
return 0;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 消息服务:按对话读消息,以及把库里的一行还原成 core 的消息对象。
|
|
3
|
+
*
|
|
4
|
+
* 对应能力编号:A12 / L5(按消息 id 续拉:命中则从它开始且含它自身的最新内容,不命中返回全部)、
|
|
5
|
+
* F-C6(`around` 定位取窗,供客户端把搜索命中的那条摆到眼前)、F-I7(`toolCallId` 取子 Agent
|
|
6
|
+
* 自己的消息)、F-D9(会话创建时那条 `hidden` 的 cwd 消息默认不出网)、
|
|
7
|
+
* R21 / R22(消息原样用 core 的 `AgentNS.Message`,不额外挂时间戳这类外壳字段)。
|
|
8
|
+
*
|
|
9
|
+
* `toAgentMessage` 是这个仓库里「存储行 → core 消息」的唯一一份实现:对话服务算用量、判断
|
|
10
|
+
* 历史里有没有图也要走它,免得同一行在两处被读出两个样子。
|
|
11
|
+
*
|
|
12
|
+
* 正文不做转换(R30):仓储反序列化出来的就是 core 的 `AgentNS.MessageContent`,这里原样
|
|
13
|
+
* 搬上消息。剩下要做的是把「列里的字符串」收敛成 core 的取值(角色 / 状态 / 结束原因)
|
|
14
|
+
* 与把 `data` 列的 JSON 逐字段收窄到 core 的类型。
|
|
15
|
+
*/
|
|
16
|
+
import { AgentNS } from "@ai-zen/agents-core";
|
|
17
|
+
import type { ConversationRepository } from "../storage/ConversationRepository.js";
|
|
18
|
+
import type { MessageRepository } from "../storage/MessageRepository.js";
|
|
19
|
+
import type { Conversation, StoredMessage } from "../storage/types.js";
|
|
20
|
+
import type { MessageListParams, MessageListResult } from "../wire/index.js";
|
|
21
|
+
/**
|
|
22
|
+
* 草稿(R56):刚建出来、还没发过消息的对话在库里没有行。
|
|
23
|
+
*
|
|
24
|
+
* 只暴露「按 id 取」,取到的就是存储层的行形状 —— 它没有消息可读,但存在本身足以让
|
|
25
|
+
* 读取路径不把一段刚建好的对话当成「不存在」。
|
|
26
|
+
*/
|
|
27
|
+
export interface MessageDraftLookup {
|
|
28
|
+
get(conversationId: string): Conversation | undefined;
|
|
29
|
+
}
|
|
30
|
+
/** 消息服务依赖 */
|
|
31
|
+
export interface MessageServiceDeps {
|
|
32
|
+
conversations: ConversationRepository;
|
|
33
|
+
messages: MessageRepository;
|
|
34
|
+
/** 草稿注册表(R56);不注入时只认库里已有的行 */
|
|
35
|
+
drafts?: MessageDraftLookup;
|
|
36
|
+
}
|
|
37
|
+
/** 库里的角色字符串 → core 的角色;搜索命中项也要用它,故对外导出 */
|
|
38
|
+
export declare function toCoreRole(value: string): AgentNS.Role;
|
|
39
|
+
/**
|
|
40
|
+
* 库里一行 → core 的消息对象。
|
|
41
|
+
*
|
|
42
|
+
* 正文原样透传(R30):仓储反序列化出来的就是 core 的 `MessageContent`,逐段重建会把 core
|
|
43
|
+
* 新增的段类型 / 字段丢掉,也会让同一条消息在不同读取路径上读出两个样子。
|
|
44
|
+
*
|
|
45
|
+
* 其余字段名与取值也全部照 core(R21):`role` / `status` / `finish_reason` 从列里的字符串
|
|
46
|
+
* 收敛成 core 的取值,`data` 列的 JSON 逐字段收窄,认不出的不往消息上搬。
|
|
47
|
+
*/
|
|
48
|
+
export declare function toAgentMessage(row: StoredMessage): AgentNS.Message;
|
|
49
|
+
/** 从消息的其余字段里读最近一次请求的用量;没有就是 `null`(对话服务算上下文占用用) */
|
|
50
|
+
export declare function readUsage(data: Record<string, unknown> | null | undefined): AgentNS.Usage | null;
|
|
51
|
+
export declare class MessageService {
|
|
52
|
+
private readonly conversations;
|
|
53
|
+
private readonly messages;
|
|
54
|
+
private readonly drafts?;
|
|
55
|
+
constructor(deps: MessageServiceDeps);
|
|
56
|
+
/**
|
|
57
|
+
* 读一段对话的消息(architecture A12 / PRD F-C6)。
|
|
58
|
+
*
|
|
59
|
+
* - 不给 `after`、也不给 `around`:整段历史;
|
|
60
|
+
* - 给了 `after` 且该 id 就在这段对话里:**从这条开始**(含它,内容是最新的,可能是流式中那条的
|
|
61
|
+
* 部分内容)+ 其后全部,并把 `anchor` 报成它;
|
|
62
|
+
* - 给了 `after` 但这段对话里没有这个 id(已删、或换过库):退回整段历史,`anchor` 为空 ——
|
|
63
|
+
* 客户端据此知道这是一次全量;
|
|
64
|
+
* - 给了 `around`:以这条为中心取 {@link AROUND_WINDOW} 条(命中那条一定在里面),
|
|
65
|
+
* `anchor` 报成它;命中不在可见集里时给空页 + 空 `anchor`,**不报错**(消息可能刚被撤回)。
|
|
66
|
+
* `around` 与 `after` 同时给时**只认 `around`**:两者是两种取法(一个「从这里往下」、一个
|
|
67
|
+
* 「围绕这里」),混在一起没有唯一答案。
|
|
68
|
+
*
|
|
69
|
+
* `toolCallId` 取某个子 Agent 自己的消息(PRD F-I7):它按消息落库时记下的
|
|
70
|
+
* `parentToolCallId` 取(那次委派的工具调用 id),走的是另一条查询 —— 子 Agent 的消息不在
|
|
71
|
+
* 主线历史里(`listByConversation` 只给主线),所以两个参数同时给时 `after` 只用来定 `anchor`,
|
|
72
|
+
* 结果集仍是那个子 Agent 的全部消息。`around` 同此:`toolCallId` 决定读哪一段,`around`
|
|
73
|
+
* 只在那一段里定位。
|
|
74
|
+
*
|
|
75
|
+
* `hidden` 的消息(会话创建时那条 cwd 消息、交接摘要)默认不回:core 的这个字段就是
|
|
76
|
+
* 「这条不给人看」。要看到它们(排查、或客户端显式要)就传 `includeHidden`。
|
|
77
|
+
*/
|
|
78
|
+
list(params: MessageListParams): Promise<MessageListResult>;
|
|
79
|
+
/** 把一次读到的行拼成出参;`around` 给了就收成一窗(命中不在里面就是空页) */
|
|
80
|
+
private respond;
|
|
81
|
+
/**
|
|
82
|
+
* 这段对话存不存在:库里那一行,或者一份还没落库的草稿(R56)。
|
|
83
|
+
*
|
|
84
|
+
* 草稿还没有任何消息,所以读它总是得到空集 —— 但它不该被读成「找不到这段对话」:
|
|
85
|
+
* 客户端刚 `create` 完就会拉一次消息与现场。
|
|
86
|
+
*/
|
|
87
|
+
private requireConversation;
|
|
88
|
+
}
|