dsh-session-ops 0.1.4 → 0.2.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 +7 -3
- package/README.zh.md +7 -3
- package/lib/admin.js +723 -774
- package/lib/client.js +1767 -2065
- package/lib/engine.js +181 -182
- package/lib/index.js +125 -166
- package/lib/memory.js +137 -132
- package/lib/move.js +630 -644
- package/lib/sessionlog.js +2 -18
- package/lib/store/frames.js +183 -170
- package/lib/store/host.js +41 -42
- package/lib/store/http.js +64 -59
- package/lib/store/index.js +5 -6
- package/lib/store/paths.js +79 -76
- package/lib/store/registry.js +100 -89
- package/lib/store/util.js +19 -0
- package/lib/types/admin.d.ts +187 -0
- package/lib/types/engine.d.ts +136 -0
- package/lib/types/index.d.ts +281 -0
- package/lib/types/memory.d.ts +106 -0
- package/lib/types/move.d.ts +272 -0
- package/lib/types/sessionlog.d.ts +14 -0
- package/lib/types/store/frames.d.ts +128 -0
- package/lib/types/store/host.d.ts +88 -0
- package/lib/types/store/http.d.ts +105 -0
- package/lib/types/store/index.d.ts +30 -0
- package/lib/types/store/paths.d.ts +100 -0
- package/lib/types/store/registry.d.ts +206 -0
- package/lib/types/store/util.d.ts +16 -0
- package/package.json +12 -2
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 会话日志(`session[.vN].jsonl[.zstd]`)的**帧级读写原语**。
|
|
3
|
+
*
|
|
4
|
+
* ## 帧格式
|
|
5
|
+
*
|
|
6
|
+
* 日志是**独立 Zstandard 帧的标准拼接**:帧 0 只含 header 行(DSH 的
|
|
7
|
+
* `assertIndependentHeaderFrame` 强制),之后每帧是一个事件批次。于是改写只需重压
|
|
8
|
+
* 帧 0、再把剩余字节原样接上——后续帧一个字节都不动。
|
|
9
|
+
*
|
|
10
|
+
* 两个必须对齐的细节:
|
|
11
|
+
* - DSH 压缩时带内容校验和(`params: {[ZSTD_c_checksumFlag]: 1}`),重压帧 0
|
|
12
|
+
* 也必须带(见 {@link CHECKSUM_OPTIONS});
|
|
13
|
+
* - 帧边界要自己走一遍(Node 不暴露「已消费输入字节数」),就是下面的
|
|
14
|
+
* {@link scanZstdFrames}。
|
|
15
|
+
*
|
|
16
|
+
* ## 为什么头部这么要紧
|
|
17
|
+
*
|
|
18
|
+
* header 承载 `cwd` / `origin` / `parentSession` / `isSeeded`:
|
|
19
|
+
* - 「会话属于哪个工作区」在 DSH 里**只**由 `header.cwd` 决定
|
|
20
|
+
* (`WorkspaceEntity.sessionIds` 每次读取都用 `realpath(header.cwd) === workspace.path`
|
|
21
|
+
* 过滤),加上 `assertStoredIdentity` 要求物理路径等于
|
|
22
|
+
* `logPath(root, header.cwd, id)`,所以搬迁 == 改 cwd + 挪目录;
|
|
23
|
+
* - 「是不是子代理会话」只能从 `origin` 拿,projcache 里没有。
|
|
24
|
+
*
|
|
25
|
+
* 于是本模块同时提供两种读法,**语义不同、不要互相替换**:
|
|
26
|
+
* - {@link readHeaderFromBuffer}:只解**帧 0**(帧结构严格,给搬迁用);
|
|
27
|
+
* - {@link readSessionHeader}:按目录找日志文件、整段解压取第一行(宽松,给扫描/删除用)。
|
|
28
|
+
*
|
|
29
|
+
* @module dsh-session-ops/store/frames
|
|
30
|
+
*/
|
|
31
|
+
/** zstd 帧魔数(小端)。 */
|
|
32
|
+
export declare const ZSTD_MAGIC = 4247762216;
|
|
33
|
+
/** zstd 压缩选项(带内容校验和时才有 `params`)。 */
|
|
34
|
+
export interface ZstdCompressOptions {
|
|
35
|
+
params?: Record<number, number>;
|
|
36
|
+
}
|
|
37
|
+
/** 与 DSH 一致的压缩选项:带内容校验和。 */
|
|
38
|
+
export declare const CHECKSUM_OPTIONS: ZstdCompressOptions;
|
|
39
|
+
/** `zstdCompressSync` 需要 Node ≥22.15 / ≥23.8;缺失时插件拒绝搬迁而不是写坏日志。 */
|
|
40
|
+
export declare const hasZstd: boolean;
|
|
41
|
+
/** 会话头(日志第一行)。字段按 DSH 实际写入的形状声明,其余键原样保留。 */
|
|
42
|
+
export interface SessionHeader {
|
|
43
|
+
type?: string;
|
|
44
|
+
version?: number;
|
|
45
|
+
id?: string;
|
|
46
|
+
cwd?: string;
|
|
47
|
+
origin?: string;
|
|
48
|
+
parentSession?: string;
|
|
49
|
+
isSeeded?: boolean;
|
|
50
|
+
delegationDepth?: number;
|
|
51
|
+
createdAt?: number;
|
|
52
|
+
[key: string]: unknown;
|
|
53
|
+
}
|
|
54
|
+
/** 拼接帧里的一个帧范围。 */
|
|
55
|
+
export interface ZstdFrameRange {
|
|
56
|
+
start: number;
|
|
57
|
+
end: number;
|
|
58
|
+
}
|
|
59
|
+
/** {@link scanZstdFrames} 的扫描结果。 */
|
|
60
|
+
export interface ZstdFrameScan {
|
|
61
|
+
frames: ZstdFrameRange[];
|
|
62
|
+
incomplete: boolean;
|
|
63
|
+
reason?: string;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* 走一遍拼接帧,给出每帧的字节范围。
|
|
67
|
+
*
|
|
68
|
+
* 按 zstd 规范解析帧头(魔数 → Frame_Header_Descriptor → 可选窗口描述符 /
|
|
69
|
+
* 字典 id / 内容长度)与块序列(3 字节块头 → 块体),直到 Last_Block,
|
|
70
|
+
* 再按标志位跳过 4 字节内容校验和。
|
|
71
|
+
*
|
|
72
|
+
* @param buffer - 整个日志文件的内容。
|
|
73
|
+
* @returns `{frames, incomplete, reason}`;`frames` 每项含 `{start, end}`。
|
|
74
|
+
*/
|
|
75
|
+
export declare function scanZstdFrames(buffer: Buffer): ZstdFrameScan;
|
|
76
|
+
/**
|
|
77
|
+
* 读出日志第一帧里的 header 对象(只解析,不改写)。
|
|
78
|
+
* @param buffer - 日志内容。
|
|
79
|
+
* @param compressed - `true`(默认)表示 zstd 帧日志,`false` 表示未压缩的行式日志。
|
|
80
|
+
* @returns header 对象;不是合法会话日志时 `undefined`。
|
|
81
|
+
*/
|
|
82
|
+
export declare function readHeaderFromBuffer(buffer: Buffer, compressed?: boolean): SessionHeader | undefined;
|
|
83
|
+
/**
|
|
84
|
+
* 把一个会话目录里的会话头读出来(**宽松**读法:扫目录找日志文件,取解压后的第一行)。
|
|
85
|
+
*
|
|
86
|
+
* 与 {@link readHeaderFromBuffer} 的区别是它不校验帧结构、也不要求 `type === 'session'`:
|
|
87
|
+
* 目录里可能有半截写入或手工改名的日志,扫描/删除路径宁可拿到一个「像头部」的对象,
|
|
88
|
+
* 也不要因为格式不严格就把整条会话当成不可读。
|
|
89
|
+
*
|
|
90
|
+
* 解压失败(截断、缺 zstd 支持)返回 `undefined`,调用方按「未知」处理。
|
|
91
|
+
* @param dir - 会话目录。
|
|
92
|
+
* @param cache - 以 `路径:mtime:size` 为键的头部缓存(可选;不传就现建一个)。
|
|
93
|
+
* @returns 头部对象,或 undefined。
|
|
94
|
+
*/
|
|
95
|
+
export declare function readSessionHeader(dir: string, cache?: Map<string, SessionHeader | undefined>): SessionHeader | undefined;
|
|
96
|
+
/** {@link rewriteHeaderCwd} 成功时的结果。 */
|
|
97
|
+
export interface HeaderRewriteSuccess {
|
|
98
|
+
buffer: Buffer;
|
|
99
|
+
rewritten: true;
|
|
100
|
+
header: SessionHeader;
|
|
101
|
+
frames?: number;
|
|
102
|
+
reason?: undefined;
|
|
103
|
+
}
|
|
104
|
+
/** {@link rewriteHeaderCwd} 无法改写时的结果(原样返回 buffer)。 */
|
|
105
|
+
export interface HeaderRewriteFailure {
|
|
106
|
+
buffer: Buffer;
|
|
107
|
+
rewritten: false;
|
|
108
|
+
header?: SessionHeader;
|
|
109
|
+
frames?: number;
|
|
110
|
+
reason?: string;
|
|
111
|
+
}
|
|
112
|
+
/** {@link rewriteHeaderCwd} 的结果。 */
|
|
113
|
+
export type HeaderRewriteResult = HeaderRewriteSuccess | HeaderRewriteFailure;
|
|
114
|
+
/**
|
|
115
|
+
* 把会话头的 `cwd` 改写成另一条绝对路径。
|
|
116
|
+
*
|
|
117
|
+
* 只动 `cwd` 一个键,其余键(含该 generation 自己的字段)按原顺序逐字保留;
|
|
118
|
+
* 帧 0 之后的字节**原样接上**,所以事件批次一个字节都没变。
|
|
119
|
+
*
|
|
120
|
+
* @param buffer - 日志内容。
|
|
121
|
+
* @param newCwd - 改写后的绝对路径。
|
|
122
|
+
* @param options - `{compressed}`:帧日志(默认 `true`)或未压缩行式日志。
|
|
123
|
+
* @returns `{buffer, rewritten, header, frames?, reason?}`;无需改写或无法改写时
|
|
124
|
+
* `rewritten: false` 并原样返回。
|
|
125
|
+
*/
|
|
126
|
+
export declare function rewriteHeaderCwd(buffer: Buffer, newCwd: string, options?: {
|
|
127
|
+
compressed?: boolean;
|
|
128
|
+
}): HeaderRewriteResult;
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 宿主侧的公共底座:`$DSH_HOME`、尽力而为的文件日志、以及 volatile 配置的解包。
|
|
3
|
+
*
|
|
4
|
+
* 这几段代码在两个插件里逐字重复——它们都是「DSH 宿主约定」而不是插件策略:
|
|
5
|
+
* - `dshHome()` 认 `DSH_HOME` 环境变量(测试靠它把整个沙箱指到临时目录);
|
|
6
|
+
* - 宿主 stdout 不落盘,排障只能靠 `$DSH_HOME/logs/<name>.log`;
|
|
7
|
+
* - 标了 `.volatile()` 的配置字段,Loader 交给 `apply` 的是活引用(`{get()}`)
|
|
8
|
+
* 而不是值,值在写盘后原地更新,所以**解包必须做两次**(进 schema 之前一次、
|
|
9
|
+
* schema 出来之后再一次,后者是因为 schemastery 对 volatile 字段输出的也是活引用)。
|
|
10
|
+
*
|
|
11
|
+
* 文件日志的**行格式两边不同**(admin 是 `[时间] 级别 正文`,move 是 `时间 [级别] 正文`),
|
|
12
|
+
* 这是对外可见的排障产物,所以这里不做统一,而是把两种格式都作为预设导出,
|
|
13
|
+
* 由调用方显式选择(见 {@link LOG_LINE_LEVEL_FIRST} / {@link LOG_LINE_TIME_FIRST})。
|
|
14
|
+
*
|
|
15
|
+
* @module dsh-session-ops/store/host
|
|
16
|
+
*/
|
|
17
|
+
/** 一行文件日志的 `{iso, level, message}`。 */
|
|
18
|
+
export interface LogEntry {
|
|
19
|
+
iso: string;
|
|
20
|
+
level: string;
|
|
21
|
+
message: string;
|
|
22
|
+
}
|
|
23
|
+
/** 宿主 cordis 上下文里本模块用到的那一小块(只在 `createFileLogger` 里取 `logger`)。 */
|
|
24
|
+
export interface LoggerContext {
|
|
25
|
+
logger?: Record<string, ((message: string) => unknown) | undefined> | undefined;
|
|
26
|
+
}
|
|
27
|
+
/** 日志行格式:`(entry) => 一整行(含结尾换行)`。 */
|
|
28
|
+
export type LogLineFormatter = (entry: LogEntry) => string;
|
|
29
|
+
/** 插件里到处用的日志函数。 */
|
|
30
|
+
export type LogFn = (level: string, message: string) => void;
|
|
31
|
+
/** `$DSH_HOME`:环境变量优先,默认 `~/.dsh`。 */
|
|
32
|
+
export declare function dshHome(): string;
|
|
33
|
+
/**
|
|
34
|
+
* 文件日志行格式之一:`[ISO 时间] 级别 正文`(`dsh-session-admin` 在用)。
|
|
35
|
+
* @param entry - `{iso, level, message}`。
|
|
36
|
+
* @returns 一整行(含结尾换行)。
|
|
37
|
+
*/
|
|
38
|
+
export declare const LOG_LINE_LEVEL_FIRST: LogLineFormatter;
|
|
39
|
+
/**
|
|
40
|
+
* 文件日志行格式之一:`ISO 时间 [级别] 正文`(`dsh-session-move` 在用)。
|
|
41
|
+
* @param entry - `{iso, level, message}`。
|
|
42
|
+
* @returns 一整行(含结尾换行)。
|
|
43
|
+
*/
|
|
44
|
+
export declare const LOG_LINE_TIME_FIRST: LogLineFormatter;
|
|
45
|
+
/** {@link createFileLogger} 的选项。 */
|
|
46
|
+
export interface CreateFileLoggerOptions {
|
|
47
|
+
/** 日志文件名(不含 `.log`;默认 `dsh-session-ops`)。 */
|
|
48
|
+
name?: string;
|
|
49
|
+
/** 行格式(默认 {@link LOG_LINE_LEVEL_FIRST})。 */
|
|
50
|
+
formatLine?: LogLineFormatter;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* 尽力而为的文件日志:写 `$DSH_HOME/logs/<name>.log`,并顺带喂宿主 logger。
|
|
54
|
+
*
|
|
55
|
+
* 任何一步失败都静默吞掉——日志本身永远不该成为故障源(建目录失败、磁盘满了、
|
|
56
|
+
* 宿主 logger 不可用,都不影响插件机制)。
|
|
57
|
+
* @param ctx - Host cordis 上下文(只用 `ctx?.logger`,可为 undefined)。
|
|
58
|
+
* @param options - `{name, formatLine}`:日志文件名(不含 `.log`;默认
|
|
59
|
+
* `dsh-session-ops`)与行格式(默认 {@link LOG_LINE_LEVEL_FIRST})。
|
|
60
|
+
* @returns `(level, message) => void`,即插件里的 `log`。
|
|
61
|
+
*/
|
|
62
|
+
export declare function createFileLogger(ctx: LoggerContext | undefined, options?: CreateFileLoggerOptions): LogFn;
|
|
63
|
+
/** 解包后的普通配置对象。 */
|
|
64
|
+
export type PlainConfig = Record<string, unknown>;
|
|
65
|
+
/**
|
|
66
|
+
* 解包 entry 配置里的 `.volatile()` 活引用。
|
|
67
|
+
* @param raw - entry 配置或普通配置对象。
|
|
68
|
+
* @returns 值已解包的浅拷贝。
|
|
69
|
+
*/
|
|
70
|
+
export declare function plainConfig(raw?: unknown): PlainConfig;
|
|
71
|
+
/**
|
|
72
|
+
* 「可调用、收普通对象、返回普通对象」的 schema 形状(schemastery 的 `Config` 就是这种)。
|
|
73
|
+
*
|
|
74
|
+
* 本包不依赖 `@deepseek-ai/schemastery` 的运行时类型,只要求这个最小形状。
|
|
75
|
+
*/
|
|
76
|
+
export type ConfigSchemaLike = (raw?: any) => unknown;
|
|
77
|
+
/**
|
|
78
|
+
* 解析一份 entry 配置:先解包输入的活引用,再交给 schema 校验并补默认值,最后**再解包
|
|
79
|
+
* 一次**——schemastery 对 `.volatile()` 字段输出的也是活引用,不收敛的话读到的就是
|
|
80
|
+
* `{ get() }` 对象(HTTP 面上会变成一个空对象)。
|
|
81
|
+
*
|
|
82
|
+
* schema 由调用方传入(本包只把 `@deepseek-ai/schemastery` 当类型来源),传进来的只要是
|
|
83
|
+
* 「可调用、收普通对象、返回带默认值的普通对象」即可。
|
|
84
|
+
* @param schema - 插件自己的 `Config`(schemastery 对象,可调用)。
|
|
85
|
+
* @param raw - entry 配置或普通配置对象。
|
|
86
|
+
* @returns 普通值配置。
|
|
87
|
+
*/
|
|
88
|
+
export declare function resolveVolatileConfig(schema: ConfigSchemaLike, raw?: unknown): PlainConfig;
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 两个会话插件共用的 **HTTP 面**:JSON 响应封装、请求体读取、以及前缀路由的骨架。
|
|
3
|
+
*
|
|
4
|
+
* 路由协议(响应码与响应体)是两个插件对浏览器半边的**公开契约**,必须逐字一致,
|
|
5
|
+
* 所以它只在写一份:
|
|
6
|
+
* - 403 `{ok:false,error:{code:'forbidden',message:'forbidden'}}` —— 信任栅栏拒绝;
|
|
7
|
+
* - 404 `{ok:false,error:{code:'not-found',message:'unknown method'}}` —— 路径没落在
|
|
8
|
+
* 前缀下,或方法名里还有 `/`;
|
|
9
|
+
* - 404 `{ok:false,error:{code:'not-found',message:'unknown method <m>'}}` —— 方法不在表里;
|
|
10
|
+
* - 200 `{ok:true,value}`;
|
|
11
|
+
* - 400 `{ok:false,error:{code:'bad-request',message:<err.message>}}` —— 处理器抛错。
|
|
12
|
+
* `GET` 的载荷是 query string,其余方法读 `JSON` 请求体(1 MiB 上限)。
|
|
13
|
+
*
|
|
14
|
+
* **信任栅栏本身刻意不在这里**:两个插件的判定依据不同(`dsh-session-admin` 看
|
|
15
|
+
* `Host` 头 + `sec-fetch-site`/`Origin` + `allowHosts` 的 host/hostname 匹配;
|
|
16
|
+
* `dsh-session-move` 看 socket 的 `remoteAddress` + `allowHosts` 精确字符串匹配),
|
|
17
|
+
* 语义不同就不能合并——合并必然改动其中一边的对外行为。这里只收一个
|
|
18
|
+
* `trustRequest(req) => boolean` 回调。
|
|
19
|
+
*
|
|
20
|
+
* 同理,`writeJson` 的 `content-length` 与 `readJsonBody` 的严格程度两边也不同,
|
|
21
|
+
* 都用显式开关保留原状(见各参数)。
|
|
22
|
+
*
|
|
23
|
+
* @module dsh-session-ops/store/http
|
|
24
|
+
*/
|
|
25
|
+
/**
|
|
26
|
+
* 本模块用到的请求形状(Node 的 `IncomingMessage` 与测试里的假请求都满足它)。
|
|
27
|
+
*
|
|
28
|
+
* 刻意写成最小本地接口而不是直接引用 `node:http`:两个插件只用到这几个字段,
|
|
29
|
+
* 测试里的假请求也不实现完整的 `IncomingMessage`。
|
|
30
|
+
*/
|
|
31
|
+
export interface HttpRequest extends AsyncIterable<Buffer> {
|
|
32
|
+
url?: string | undefined;
|
|
33
|
+
method?: string | undefined;
|
|
34
|
+
headers?: Record<string, string | string[] | undefined> | undefined;
|
|
35
|
+
socket?: {
|
|
36
|
+
remoteAddress?: string | undefined;
|
|
37
|
+
} | undefined;
|
|
38
|
+
}
|
|
39
|
+
/** 本模块用到的响应形状(Node 的 `ServerResponse` 与测试里的假响应都满足它)。 */
|
|
40
|
+
export interface HttpResponse {
|
|
41
|
+
writeHead(status: number, headers: Record<string, string | number>): unknown;
|
|
42
|
+
end(body?: string): unknown;
|
|
43
|
+
}
|
|
44
|
+
/** 路由方法表里的一个处理器:收载荷,返回值包成 `{ok:true,value}`。 */
|
|
45
|
+
export type ApiMethod = (payload: any) => unknown | Promise<unknown>;
|
|
46
|
+
/** {@link createApiHandler} 的选项。 */
|
|
47
|
+
export interface CreateApiHandlerOptions {
|
|
48
|
+
/** 路由前缀,如 `/session-admin/api`。 */
|
|
49
|
+
prefix: string;
|
|
50
|
+
/** 方法名 → `async (payload) => value`。 */
|
|
51
|
+
methods: Record<string, ApiMethod | undefined>;
|
|
52
|
+
/** `(req) => boolean`:请求是否来自受信的浏览器(各插件自己的栅栏)。 */
|
|
53
|
+
trustRequest?: ((req: HttpRequest) => boolean) | undefined;
|
|
54
|
+
/** `(level, message) => void`(可为 undefined)。 */
|
|
55
|
+
log?: ((level: string, message: string) => void) | undefined;
|
|
56
|
+
/** 读请求体,默认 {@link readJsonBody}(宽松)。 */
|
|
57
|
+
readBody?: (req: HttpRequest) => Promise<unknown>;
|
|
58
|
+
/** 透传给 {@link writeJson}。 */
|
|
59
|
+
contentLength?: boolean;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* 写一个 JSON 响应。
|
|
63
|
+
* @param res - Node HTTP 响应。
|
|
64
|
+
* @param status - 状态码。
|
|
65
|
+
* @param body - 任意可 JSON 序列化的值。
|
|
66
|
+
* @param options - `{contentLength}`:是否带上 `content-length` 头。默认 `false`
|
|
67
|
+
* (`dsh-session-move` 的原样行为,响应走 chunked);`dsh-session-admin` 传 `true`
|
|
68
|
+
* 以保留它原有的 `content-length`。
|
|
69
|
+
* @returns 无。
|
|
70
|
+
*/
|
|
71
|
+
export declare function writeJson(res: HttpResponse, status: number, body: unknown, options?: {
|
|
72
|
+
contentLength?: boolean;
|
|
73
|
+
}): void;
|
|
74
|
+
/**
|
|
75
|
+
* 读一个 JSON 请求体(上限 1 MiB)。
|
|
76
|
+
*
|
|
77
|
+
* 严格程度由调用方选,因为两个插件原本就不一样:
|
|
78
|
+
* - `requireObject: false`(默认,`dsh-session-admin` 的行为):不 trim、不校验类型,
|
|
79
|
+
* 空 body 返回 `{}`,其余原样交给 `JSON.parse`(一个空白 body 会抛出
|
|
80
|
+
* `Unexpected end of JSON input`,进而变成 400);
|
|
81
|
+
* - `requireObject: true`(`dsh-session-move` 的行为):trim 后空串返回 `{}`,
|
|
82
|
+
* 解析结果必须是**普通对象**,否则抛 `request body must be a JSON object`。
|
|
83
|
+
* @param req - Node HTTP 请求。
|
|
84
|
+
* @param options - `{requireObject}`。
|
|
85
|
+
* @returns 解析出来的载荷对象。
|
|
86
|
+
*/
|
|
87
|
+
export declare function readJsonBody(req: HttpRequest, options?: {
|
|
88
|
+
requireObject?: boolean;
|
|
89
|
+
}): Promise<unknown>;
|
|
90
|
+
/**
|
|
91
|
+
* 造一个 `kind: 'prefix'` 路由处理器(`ctx.webServer.register({kind:'prefix', path, handler})` 的 `handler`)。
|
|
92
|
+
*
|
|
93
|
+
* 方法表里的每个处理器收到 `payload`,返回值包成 `{ok:true,value}`;抛错变成 400 并写一行
|
|
94
|
+
* `<method> failed: <message>` 日志。`GET` 的载荷是 query string(全部是字符串)。
|
|
95
|
+
* @param options - 见下。
|
|
96
|
+
* @param options.prefix - 路由前缀,如 `/session-admin/api`。
|
|
97
|
+
* @param options.methods - 方法名 → `async (payload) => value`。
|
|
98
|
+
* @param options.trustRequest - `(req) => boolean`:请求是否来自受信的浏览器(各插件自己的栅栏)。
|
|
99
|
+
* @param options.log - `(level, message) => void`(可为 undefined)。
|
|
100
|
+
* @param options.readBody - 读请求体,默认 {@link readJsonBody}(宽松);`dsh-session-move`
|
|
101
|
+
* 传 `(req) => readJsonBody(req, {requireObject: true})`。
|
|
102
|
+
* @param options.contentLength - 透传给 {@link writeJson}。
|
|
103
|
+
* @returns `async (req, res) => void`。
|
|
104
|
+
*/
|
|
105
|
+
export declare function createApiHandler(options: CreateApiHandlerOptions): (req: HttpRequest, res: HttpResponse) => Promise<void>;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `dsh-session-ops/lib/store` —— 本包内部的**会话存储原语**。
|
|
3
|
+
*
|
|
4
|
+
* 这一层只做库:没有模型工具、没有槽位注册、没有 DSH 插件生命周期代码,
|
|
5
|
+
* 也**零 `@deepseek-ai/*` 运行时依赖**(只用 node 内置模块)。删除半边与搬迁半边
|
|
6
|
+
* 共用它,避免两边逐字重复同一套会话存储知识。
|
|
7
|
+
*
|
|
8
|
+
* 五个子模块:
|
|
9
|
+
* - {@link module:dsh-session-ops/store/paths}:项目目录键 `--C-proj--` 的编解码、
|
|
10
|
+
* 会话目录与 generation 文件名的命名规则;
|
|
11
|
+
* - {@link module:dsh-session-ops/store/frames}:`session[.vN].jsonl[.zstd]` 的帧扫描、
|
|
12
|
+
* 头部读取(两种严格度)与 cwd 改写;
|
|
13
|
+
* - {@link module:dsh-session-ops/store/registry}:工作区注册表成员表 / 归档集合、
|
|
14
|
+
* 驻留会话、「正在跑一轮」判定、内存里三处 cwd 的重定向;
|
|
15
|
+
* - {@link module:dsh-session-ops/store/host}:`$DSH_HOME`、尽力而为的文件日志、
|
|
16
|
+
* volatile 配置解包;
|
|
17
|
+
* - {@link module:dsh-session-ops/store/http}:两个半边共用的 HTTP 面(JSON 响应封装、
|
|
18
|
+
* 请求体读取、前缀路由骨架;**信任栅栏按插件语义不同,不在这里**)。
|
|
19
|
+
*
|
|
20
|
+
* 有意**没有**放进来的东西(语义不同或只有一边有):两套信任栅栏、迁移的三步改名策略
|
|
21
|
+
* (`planMove`/`applyPlan`)、磁盘扫描与范围筛选(删除半边专有)、项目记忆跟随
|
|
22
|
+
* (搬迁半边专有)、模型工具与槽位定义。
|
|
23
|
+
*
|
|
24
|
+
* @module dsh-session-ops/store
|
|
25
|
+
*/
|
|
26
|
+
export * from './paths.js';
|
|
27
|
+
export * from './frames.js';
|
|
28
|
+
export * from './registry.js';
|
|
29
|
+
export * from './host.js';
|
|
30
|
+
export * from './http.js';
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 会话存储的**路径命名**知识:项目目录键(`--C-proj--`)、会话目录、generation 文件名。
|
|
3
|
+
*
|
|
4
|
+
* 这些命名事实原本分散在两个插件里,而且是同一份知识的**两个方向**:
|
|
5
|
+
* - `dsh-session-move` 需要**正向**编码(`projectKey` / `encodeSegment`)——搬迁时
|
|
6
|
+
* 要把会话日志放到目标 cwd 对应的 project 目录;
|
|
7
|
+
* - `dsh-session-admin` 需要**反向**还原(`decodeProjectKey`)——删除面板要给出
|
|
8
|
+
* 「这条会话大概是哪个目录的」,而头部读不出来时只有目录键可猜。
|
|
9
|
+
* 两份实现各自维护同一套规则,改动任何一边都要记得另一边——这就是本模块存在的理由。
|
|
10
|
+
*
|
|
11
|
+
* 规则(与 `dsh-session-persistence-jsonl` 的 `format.js` 逐字一致):
|
|
12
|
+
* - 路径里每个非 `[A-Za-z0-9._-]` 的码位写成 `~XXXX`(大写十六进制、四位);
|
|
13
|
+
* - `/` `\` `:` 三种分隔符**折叠成 `-`**(盘符的冒号也因此变成 `-`);
|
|
14
|
+
* - 目录键形如 `--<readable>--`,`readable` 截断到 251 字符;
|
|
15
|
+
* - 会话目录 = `<root>/<projectKey(cwd)>/<encodeSegment(id)>`。
|
|
16
|
+
*
|
|
17
|
+
* @module dsh-session-ops/store/paths
|
|
18
|
+
*/
|
|
19
|
+
/**
|
|
20
|
+
* 把任意字符串编码成一个安全路径段(与会话 id 共用同一套转义)。
|
|
21
|
+
* @param raw - 原始字符串。
|
|
22
|
+
* @returns 单个文件系统安全路径段。
|
|
23
|
+
*/
|
|
24
|
+
export declare function encodeSegment(raw: string): string;
|
|
25
|
+
/**
|
|
26
|
+
* 项目目录键:分隔符折叠成 `-`,不安全码位 `~XXXX`,截断到 251 字符。
|
|
27
|
+
* @param cwd - 会话的项目目录。
|
|
28
|
+
* @returns 单个文件系统安全的项目目录名。
|
|
29
|
+
*/
|
|
30
|
+
export declare function projectKey(cwd: string): string;
|
|
31
|
+
/**
|
|
32
|
+
* 把 `sessions/` 下的项目目录键还原成路径。
|
|
33
|
+
*
|
|
34
|
+
* 只做「够用」的反解:分隔符在磁盘上已经全部退化成 `-`,无法区分 `/` 与 `\`
|
|
35
|
+
* (这里一律还原成 `\`),盘符的冒号也没有单独转义(`C:\a` → `--C-a--`,这里
|
|
36
|
+
* 按 `^[A-Za-z]\\` 补回来)。**真正的 cwd 以会话头部里的记录为准**,这个只作为兜底。
|
|
37
|
+
* @param key - 项目目录名。
|
|
38
|
+
* @returns 反解出的路径。
|
|
39
|
+
*/
|
|
40
|
+
export declare function decodeProjectKey(key: string): string;
|
|
41
|
+
/**
|
|
42
|
+
* **宽松**的会话日志文件名判定:`.jsonl.zstd` 或 `.jsonl` 结尾就算。
|
|
43
|
+
*
|
|
44
|
+
* 与 {@link parseGenerationName}(**规范** generation 名)是两件事,别互相替换:
|
|
45
|
+
* 删除路径要能看见并清掉任何形状的日志文件(哪怕是手工改名或半截写入的),
|
|
46
|
+
* 而搬迁路径必须只认规范名,否则会把临时文件当成真日志搬走。
|
|
47
|
+
* @param name - 目录项名。
|
|
48
|
+
* @returns 是否像一份会话日志。
|
|
49
|
+
*/
|
|
50
|
+
export declare function isSessionLogFile(name: string): boolean;
|
|
51
|
+
/** 一个规范 generation 名的解析结果。 */
|
|
52
|
+
export interface GenerationName {
|
|
53
|
+
/** 版本号(v0 为 0)。 */
|
|
54
|
+
version: number;
|
|
55
|
+
/** 是否压缩。 */
|
|
56
|
+
compressed: boolean;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* 目录里是不是规范 generation 名。
|
|
60
|
+
* @param name - 目录项名。
|
|
61
|
+
* @returns 版本号(v0 为 0)与是否压缩;不是规范名时 `undefined`。
|
|
62
|
+
*/
|
|
63
|
+
export declare function parseGenerationName(name: string): GenerationName | undefined;
|
|
64
|
+
/** 会话目录里的一个规范 generation 文件。 */
|
|
65
|
+
export interface GenerationEntry extends GenerationName {
|
|
66
|
+
/** 目录项名(规范 generation 文件名)。 */
|
|
67
|
+
name: string;
|
|
68
|
+
}
|
|
69
|
+
/** 找到了规范 generation 的扫描结果。 */
|
|
70
|
+
export interface GenerationFound {
|
|
71
|
+
selected: GenerationEntry;
|
|
72
|
+
all: GenerationEntry[];
|
|
73
|
+
/** 目录里不是规范名、也不会被搬迁的其它文件。 */
|
|
74
|
+
others: string[];
|
|
75
|
+
}
|
|
76
|
+
/** 没找到规范 generation 的扫描结果;错误文案被对外可见报告引用。 */
|
|
77
|
+
export interface GenerationMissing {
|
|
78
|
+
error: string;
|
|
79
|
+
others?: string[];
|
|
80
|
+
}
|
|
81
|
+
/** {@link findGeneration} 的结果(`error` 与 `selected` 互斥)。 */
|
|
82
|
+
export type GenerationScan = GenerationFound | GenerationMissing;
|
|
83
|
+
/**
|
|
84
|
+
* 选出会话目录里版本最高的规范 generation 文件。
|
|
85
|
+
*
|
|
86
|
+
* 临时名与备份名(`*.session-move-tmp`、`*.session-move-old`)不匹配规范名,
|
|
87
|
+
* 一律进 `others`——搬迁必须保证「任何时刻都不存在两份规范名日志」。
|
|
88
|
+
* @param dir - 会话目录。
|
|
89
|
+
* @returns `{selected, all, others}`;列不了目录或没有规范名时 `{error, others?}`
|
|
90
|
+
* (错误文案被两个插件的用户可见报告引用,改动前先看测试)。
|
|
91
|
+
*/
|
|
92
|
+
export declare function findGeneration(dir: string): GenerationScan;
|
|
93
|
+
/** 物理编码后缀。 */
|
|
94
|
+
export declare function compressionSuffix(compression: string): string;
|
|
95
|
+
/** 一个 generation 的规范文件名(v0 不带 `vN` 段)。 */
|
|
96
|
+
export declare function generationLogFilename(version: number, compression: string): string;
|
|
97
|
+
/** 会话目录:`<root>/<projectKey>/<encodeSegment(id)>`。 */
|
|
98
|
+
export declare function sessionDir(root: string, cwd: string | undefined, id: string): string;
|
|
99
|
+
/** 一个 generation 的绝对日志路径。 */
|
|
100
|
+
export declare function logPath(root: string, cwd: string | undefined, id: string, version: number, compression: string): string;
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 会话**归属**知识:工作区注册表(`ctx.workspaceRegistry`)、驻留会话、以及「正在跑一轮」。
|
|
3
|
+
*
|
|
4
|
+
* 这份知识在两个插件里各有一半,写在一处才不会走形:
|
|
5
|
+
* - `dsh-session-admin` 要判断「这条会话属于哪个工作区 / 归档没有」才能决定能不能删,
|
|
6
|
+
* 并在删文件**之前**把它从登记表里摘掉;
|
|
7
|
+
* - `dsh-session-move` 要判断「属于谁 / 目标工作区是哪一个 / 正在跑没有」才能搬,
|
|
8
|
+
* 并在搬完之后 attach 目标、detach 原属、把内存里缓存的 cwd 一起改掉。
|
|
9
|
+
*
|
|
10
|
+
* ## 三个容易踩的坑(都是实测结论,不是推测)
|
|
11
|
+
*
|
|
12
|
+
* 1. **`workspace.sessionIds` 是「按会话头索引过滤过的视图」**,不是完整记账:
|
|
13
|
+
* DSH 每次读取都用 `realpath(header.cwd) === workspace.path` 过滤一遍。完整记账在
|
|
14
|
+
* `workspace.record.sessionIds`(见 {@link accountedSessionIds})。
|
|
15
|
+
* 会话目录一删,`sessionIds` 就再也查不到这条会话——所以「摘登记表」必须早于「删文件」。
|
|
16
|
+
* 2. **「挂着」≠「正在跑」**:会话在创建 agent 时被 enter,只在 agent dispose 时才 detach,
|
|
17
|
+
* 长期运行的 `dsh web` 里只要碰过就基本一直挂着。删/搬的守卫只认「正在跑一轮」。
|
|
18
|
+
* 3. **`workspace.json` 里的工作区对象自身不带 id**,id 只存在于外层键上;从磁盘读表时
|
|
19
|
+
* 必须重新组装视图,否则所有守卫会因为 `owner.workspaceId === undefined` 静默失效。
|
|
20
|
+
*
|
|
21
|
+
* 出错时的策略刻意**不统一**,两边各自保留(见各函数的文档):
|
|
22
|
+
* 拿不到 `agents` 服务时 admin 保守地把全部驻留会话当成「在跑」(宁可拒绝),
|
|
23
|
+
* move 则把状态当成未知(`undefined`,界面照常显示)。
|
|
24
|
+
*
|
|
25
|
+
* 这里声明的都是**最小本地接口**:Cordis/DSH 的宿主对象在编译期拿不到类型,
|
|
26
|
+
* 但本模块只用到这几个字段,把它们写清楚比整片 `any` 更能挡住拼写错误。
|
|
27
|
+
*
|
|
28
|
+
* @module dsh-session-ops/store/registry
|
|
29
|
+
*/
|
|
30
|
+
/** agent 表示「这一轮还在跑」的状态值。 */
|
|
31
|
+
export declare const SESSION_RUNNING_STATUS = "running";
|
|
32
|
+
/** 驻留会话对象里本模块用到的部分(`ctx.get('sessions').get(id)`)。 */
|
|
33
|
+
export interface ResidentSessionLike {
|
|
34
|
+
id?: unknown;
|
|
35
|
+
header?: Record<string, unknown> | undefined;
|
|
36
|
+
}
|
|
37
|
+
/** `ctx.get('sessions')` 拿到的会话服务(可缺方法)。 */
|
|
38
|
+
export interface SessionsService {
|
|
39
|
+
list?: () => unknown[];
|
|
40
|
+
get?: (sessionId: string) => ResidentSessionLike | undefined;
|
|
41
|
+
}
|
|
42
|
+
/** `ctx.get('agents').get(id)` 拿到的 agent 对象。 */
|
|
43
|
+
export interface AgentLike {
|
|
44
|
+
status?: unknown;
|
|
45
|
+
}
|
|
46
|
+
/** `ctx.get('agents')` 拿到的 agent 服务(可缺方法)。 */
|
|
47
|
+
export interface AgentsService {
|
|
48
|
+
get?: (sessionId: string) => AgentLike | undefined;
|
|
49
|
+
}
|
|
50
|
+
/** 工作区的完整记账视图(`workspace.record`)。 */
|
|
51
|
+
export interface WorkspaceRecord {
|
|
52
|
+
sessionIds?: unknown;
|
|
53
|
+
}
|
|
54
|
+
/** 工作区实体(`ctx.workspaceRegistry.list()` 的元素)。 */
|
|
55
|
+
export interface WorkspaceEntity {
|
|
56
|
+
id?: unknown;
|
|
57
|
+
title?: unknown;
|
|
58
|
+
path?: unknown;
|
|
59
|
+
sessionIds: unknown[];
|
|
60
|
+
record?: WorkspaceRecord;
|
|
61
|
+
attachSession(sessionId: string): Promise<unknown>;
|
|
62
|
+
detachSession(sessionId: string): Promise<unknown>;
|
|
63
|
+
}
|
|
64
|
+
/** `ctx.workspaceRegistry`(宿主的权威登记表)。 */
|
|
65
|
+
export interface WorkspaceRegistryLike {
|
|
66
|
+
list: () => WorkspaceEntity[];
|
|
67
|
+
archivedSessionIds?: unknown;
|
|
68
|
+
headers?: Map<string, Record<string, unknown>> | undefined;
|
|
69
|
+
unarchiveSession(sessionId: string): Promise<unknown>;
|
|
70
|
+
unpinSession(sessionId: string): Promise<unknown>;
|
|
71
|
+
}
|
|
72
|
+
/** 持久化写入句柄(`persistence.tracker.writers` 的值)。 */
|
|
73
|
+
export interface SessionWriteHandle {
|
|
74
|
+
header?: Record<string, unknown> | undefined;
|
|
75
|
+
flush(): Promise<unknown>;
|
|
76
|
+
}
|
|
77
|
+
/** 持久化后端里本模块用到的部分(`ctx.sessionPersistence`)。 */
|
|
78
|
+
export interface PersistenceLike {
|
|
79
|
+
root?: unknown;
|
|
80
|
+
compression?: unknown;
|
|
81
|
+
tracker?: {
|
|
82
|
+
writers?: unknown;
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Host 进程里仍挂着的会话 id 集合(`ctx.sessions.list()`)。
|
|
87
|
+
*
|
|
88
|
+
* 服务缺失或抛错时退化成空集合——**这个集合只用于界面提示,不要拿它当删除守卫**
|
|
89
|
+
* (理由见模块注释第 2 条)。
|
|
90
|
+
* @param sessions - `ctx.get('sessions')` 拿到的会话服务(可为 undefined)。
|
|
91
|
+
* @returns `Set<string>`。
|
|
92
|
+
*/
|
|
93
|
+
export declare function residentSessionIds(sessions: SessionsService | undefined): Set<string>;
|
|
94
|
+
/**
|
|
95
|
+
* 一条会话的 agent 状态(`ctx.get('agents').get(id)?.status`)。
|
|
96
|
+
* @param agents - `ctx.get('agents')` 拿到的 agent 服务(可为 undefined)。
|
|
97
|
+
* @param sessionId - 会话 id。
|
|
98
|
+
* @returns 状态字符串;没有 agent、拿不到服务或抛错时 `undefined`(**未知**)。
|
|
99
|
+
*/
|
|
100
|
+
export declare function agentSessionStatus(agents: AgentsService | undefined, sessionId: string): string | undefined;
|
|
101
|
+
/**
|
|
102
|
+
* 真正「正在跑一轮」的会话:驻留集合里 agent 状态为 {@link SESSION_RUNNING_STATUS} 的那部分。
|
|
103
|
+
*
|
|
104
|
+
* 拿不到 agents 服务时**保守退化**为「全部驻留会话都算在跑」——宁可拒绝,不可误删/误搬。
|
|
105
|
+
* 单条查询抛错也算在跑(同一条原则)。
|
|
106
|
+
* @param residents - {@link residentSessionIds} 的结果。
|
|
107
|
+
* @param agents - `ctx.get('agents')`。
|
|
108
|
+
* @returns `Set<string>`(可能就是 `residents` 本身)。
|
|
109
|
+
*/
|
|
110
|
+
export declare function runningSessionIds(residents: Set<string>, agents: AgentsService | undefined): Set<string>;
|
|
111
|
+
/**
|
|
112
|
+
* 工作区的**完整记账**成员表(`workspace.record.sessionIds`)。
|
|
113
|
+
* @param workspace - 工作区实体。
|
|
114
|
+
* @returns id 数组;实体没有 record 视图时 `undefined`。
|
|
115
|
+
*/
|
|
116
|
+
export declare function accountedSessionIds(workspace: {
|
|
117
|
+
record?: WorkspaceRecord;
|
|
118
|
+
} | undefined): unknown[] | undefined;
|
|
119
|
+
/**
|
|
120
|
+
* 归档集合(`registry.archivedSessionIds`)。
|
|
121
|
+
* @param registry - `ctx.workspaceRegistry`。
|
|
122
|
+
* @returns `Set<string>`(id 归一成字符串)。
|
|
123
|
+
*/
|
|
124
|
+
export declare function archivedSessionIds(registry: {
|
|
125
|
+
archivedSessionIds?: unknown;
|
|
126
|
+
}): Set<string>;
|
|
127
|
+
/**
|
|
128
|
+
* 按**完整记账**找出会话所属的工作区。
|
|
129
|
+
* @param workspaces - 工作区实体数组(通常是 `registry.list()`)。
|
|
130
|
+
* @param sessionId - 会话 id。
|
|
131
|
+
* @returns 工作区实体,或 `undefined`。
|
|
132
|
+
*/
|
|
133
|
+
export declare function workspaceOwningSession<T extends {
|
|
134
|
+
record?: WorkspaceRecord;
|
|
135
|
+
}>(workspaces: readonly T[], sessionId: string): T | undefined;
|
|
136
|
+
/**
|
|
137
|
+
* 按 id → path → title 的顺序找一个工作区(后两者不区分大小写)。
|
|
138
|
+
* @param workspaces - 工作区实体数组(通常是 `registry.list()`)。
|
|
139
|
+
* @param key - id、绝对路径或标题;空值一律不匹配。
|
|
140
|
+
* @returns 工作区实体,或 `undefined`。
|
|
141
|
+
*/
|
|
142
|
+
export declare function findWorkspace<T extends {
|
|
143
|
+
id?: unknown;
|
|
144
|
+
path?: unknown;
|
|
145
|
+
title?: unknown;
|
|
146
|
+
}>(workspaces: readonly T[], key: unknown): T | undefined;
|
|
147
|
+
/**
|
|
148
|
+
* 把一个即将被删除的会话从它登记过的每个持久结构里摘掉。
|
|
149
|
+
*
|
|
150
|
+
* 走 `workspaceRegistry`(宿主的权威登记表)而不是直接改 `storages/workspace.json`:
|
|
151
|
+
* 宿主自己也把这份表放在内存里,手改文件要么被下一次写入覆盖,要么让内存与磁盘脱节。
|
|
152
|
+
*
|
|
153
|
+
* 覆盖三处:工作区成员表、归档集合、置顶集合。后两个是幂等的(不在集合里就不写盘),
|
|
154
|
+
* 所以无条件调用它们不会产生多余的写入。
|
|
155
|
+
*
|
|
156
|
+
* **必须在删文件之前调用**:`Workspace.sessionIds` 是按会话头索引过滤过的视图,
|
|
157
|
+
* 目录一删,成员关系就再也查不到了。
|
|
158
|
+
* @param registry - `ctx.workspaceRegistry`。
|
|
159
|
+
* @param sessionId - 即将删除的会话 id。
|
|
160
|
+
* @returns 全部持久写入完成之后 resolve;值是真正被 detach 的工作区 id 列表。
|
|
161
|
+
*/
|
|
162
|
+
export declare function releaseSessionEverywhere(registry: WorkspaceRegistryLike, sessionId: string): Promise<string[]>;
|
|
163
|
+
/**
|
|
164
|
+
* 持久化后端的**写入句柄表**(`persistence.tracker.writers`)。
|
|
165
|
+
*
|
|
166
|
+
* 有句柄 = 这条会话随时可能落盘。搬迁必须抢在它下一次 append 之前把 header 改掉。
|
|
167
|
+
* @param persistence - `ctx.sessionPersistence`(可为 undefined)。
|
|
168
|
+
* @returns `Map<id, handle>`,或 `undefined`(后端不暴露 tracker 时)。
|
|
169
|
+
*/
|
|
170
|
+
export declare function sessionWriteHandles(persistence: PersistenceLike | undefined): Map<string, SessionWriteHandle> | undefined;
|
|
171
|
+
/** {@link redirectSessionWriters} 的选项。 */
|
|
172
|
+
export interface RedirectSessionWritersOptions {
|
|
173
|
+
writers: Map<string, SessionWriteHandle> | undefined;
|
|
174
|
+
sessions: SessionsService | undefined;
|
|
175
|
+
registry: {
|
|
176
|
+
headers?: Map<string, Record<string, unknown>> | undefined;
|
|
177
|
+
};
|
|
178
|
+
sessionId: string;
|
|
179
|
+
cwd: string;
|
|
180
|
+
}
|
|
181
|
+
/** {@link redirectSessionWriters} 的结果。 */
|
|
182
|
+
export interface RedirectSessionWritersOutcome {
|
|
183
|
+
writeHandleRedirected: boolean;
|
|
184
|
+
liveHeaderUpdated: boolean;
|
|
185
|
+
registryHeaderCacheUpdated: boolean;
|
|
186
|
+
liveSession: ResidentSessionLike | undefined;
|
|
187
|
+
flushError?: string;
|
|
188
|
+
}
|
|
189
|
+
/**
|
|
190
|
+
* 把「这条会话以后往哪写」在**内存里**也改成新 cwd。
|
|
191
|
+
*
|
|
192
|
+
* 搬迁只搬了磁盘上的目录,内存里还有三处记着旧 cwd,漏掉任何一处都会让下一次 append
|
|
193
|
+
* 写到不存在的位置(ENOENT):
|
|
194
|
+
* 1. 持久化写入句柄的 `header`——它每次 append 都从自己的 header 重新推 logPath
|
|
195
|
+
* (`appendLines → logPath(meta.cwd)`),所以改 header 就够了;改之前先 `flush()`,
|
|
196
|
+
* 免得把还没落盘的缓冲写到旧路径;
|
|
197
|
+
* 2. 驻留会话对象自身的 `header`(工具/界面从它解析 cwd);
|
|
198
|
+
* 3. 注册表里的头部缓存(`registry.headers`,`Map<id, header>`)。
|
|
199
|
+
*
|
|
200
|
+
* 每一步都独立尽力而为:句柄 flush 失败只记录,不阻断其余改写。
|
|
201
|
+
* @param options - `{writers, sessions, registry, sessionId, cwd}`;`writers` 来自
|
|
202
|
+
* {@link sessionWriteHandles},`sessions` 是 `ctx.get('sessions')`。
|
|
203
|
+
* @returns `{writeHandleRedirected, liveHeaderUpdated, registryHeaderCacheUpdated, flushError?, liveSession}`
|
|
204
|
+
* ——`flushError` 只在 flush 抛错时出现;`liveSession` 是驻留会话对象(没有则 undefined)。
|
|
205
|
+
*/
|
|
206
|
+
export declare function redirectSessionWriters(options: RedirectSessionWritersOptions): Promise<RedirectSessionWritersOutcome>;
|