@zhushanwen/pi-session-reader 0.6.1 → 0.8.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 +100 -0
- package/package.json +6 -3
- package/src/__tests__/entry-anchor.test.ts +370 -0
- package/src/__tests__/execution-tree.test.ts +12 -12
- package/src/__tests__/family-zcode.test.ts +255 -0
- package/src/__tests__/family.test.ts +1 -1
- package/src/__tests__/find.test.ts +32 -49
- package/src/__tests__/fixtures.ts +15 -0
- package/src/__tests__/parser.test.ts +37 -13
- package/src/__tests__/render.test.ts +22 -19
- package/src/__tests__/result.test.ts +2 -2
- package/src/__tests__/roots.test.ts +32 -34
- package/src/__tests__/subagents.test.ts +131 -108
- package/src/__tests__/tool-handler.test.ts +506 -153
- package/src/__tests__/toolcall.test.ts +1 -1
- package/src/__tests__/tree.test.ts +1 -1
- package/src/__tests__/turns.test.ts +21 -20
- package/src/__tests__/workflow.test.ts +319 -23
- package/src/__tests__/zcode-manifest.test.ts +361 -0
- package/src/__tests__/zcode-routing.test.ts +751 -0
- package/src/__tests__/zcode-whitelist.test.ts +119 -0
- package/src/core/execution-tree.ts +3 -1
- package/src/core/family.ts +80 -1
- package/src/core/render.ts +1 -1
- package/src/core/toolcall.ts +2 -2
- package/src/core/tree.ts +1 -1
- package/src/core/turns.ts +1 -1
- package/src/core/workflow.ts +196 -6
- package/src/discovery/entry-anchor.ts +139 -0
- package/src/discovery/roots.ts +1 -2
- package/src/discovery/session-header.ts +30 -69
- package/src/discovery/subagents.ts +9 -2
- package/src/discovery/whitelist.ts +87 -0
- package/src/discovery/workflows.ts +232 -22
- package/src/discovery/zcode-manifest.ts +277 -0
- package/src/index.ts +7 -0
- package/src/result-action.ts +14 -5
- package/src/search-across.ts +1 -1
- package/src/tool-format.ts +455 -0
- package/src/tool-handler.ts +400 -344
- package/src/zcode-anchor-classify.ts +74 -0
- package/src/__tests__/real-data.ts +0 -96
- package/src/core/parser.ts +0 -167
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* [§3.5 entry 兜底归因] zcode subagent-record「在场但锚不完整」的缺失归因判定
|
|
3
|
+
*(classifyIncompleteEntryAnchor 的 entry 级纯逻辑提取;文件扫描与 readFile I/O
|
|
4
|
+
* 留在 tool-handler.ts)。零 I/O 纯函数,可完全单测。
|
|
5
|
+
*
|
|
6
|
+
* 判定与 entry-anchor.ts 的 zcodeAnchorOfEntry 双键判据同构(v1 快照 / v2 终态条
|
|
7
|
+
* 双版本门),协议字面量同值(写侧 subagent-core record-entry.ts
|
|
8
|
+
* SUBAGENT_RECORD_CUSTOM_TYPE,漂移由 entry-anchor.test.ts 守卫)。
|
|
9
|
+
*/
|
|
10
|
+
import type { Entry } from '@zhushanwen/session-core'
|
|
11
|
+
import type { ZcodeAnchorMissingReason } from './discovery/zcode-manifest.js'
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* v2 条目 data 的认识版本词表(W1 起 = 2)。与写侧 subagent-core
|
|
15
|
+
* SUBAGENT_RECORD_ENTRY_VERSION 同值——本地持有 + entry-anchor.test.ts 跨包断言
|
|
16
|
+
* 守卫漂移(生产依赖面不引 subagent-core,同 entry-anchor.ts 的协议字面量模式)。
|
|
17
|
+
*/
|
|
18
|
+
const SUBAGENT_RECORD_ENTRY_V2 = 2
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* 目标 sa-id 的 zcode 形态 subagent-record data:type/customType/版本关/id/engine
|
|
22
|
+
* 全过才返回。版本关(W1 v2 学习,entry-anchor.ts zcodeAnchorOfEntry 同口径):
|
|
23
|
+
* v1 全量快照(`d.v === 1`,兼容读面)或 v2 终态条(`d.v === 2` ∧
|
|
24
|
+
* `d.kind === 'settled'`——engineHandle 只在终态条,registered 不携带不归因)。
|
|
25
|
+
* engine 判别(D5)在此——`d.engine !== 'zcode'` 的记录(pi 形态:
|
|
26
|
+
* engine 缺省、sessionRef 无 dbPath)不是「zcode 锚不完整」,返回 undefined 跳过
|
|
27
|
+
* 不归因:误归因 missing-dbPath 会把「pi record 先于 manifest settle 落盘」的正常
|
|
28
|
+
* 窗口错报成 zcode_anchor_missing(旧版本产物指引不适用);正确归因 = undefined
|
|
29
|
+
* → 上层落 zcode_record_not_found。
|
|
30
|
+
*/
|
|
31
|
+
function zcodeRecordDataOf(entry: Entry, saId: string): Record<string, unknown> | undefined {
|
|
32
|
+
if (entry.type !== 'custom' || entry.customType !== 'subagent-record') return undefined
|
|
33
|
+
const data: unknown = entry.data
|
|
34
|
+
if (typeof data !== 'object' || data === null) return undefined
|
|
35
|
+
const d = data as Record<string, unknown>
|
|
36
|
+
if (d.id !== saId) return undefined
|
|
37
|
+
if (d.v === SUBAGENT_RECORD_ENTRY_V2) {
|
|
38
|
+
if (d.kind !== 'settled') return undefined
|
|
39
|
+
} else if (d.v !== 1) {
|
|
40
|
+
return undefined
|
|
41
|
+
}
|
|
42
|
+
if (d.engine !== 'zcode') return undefined
|
|
43
|
+
return d
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** 已匹配 zcode record 的锚完整性链:返回第一个缺失键的归因;锚完整返回 undefined。 */
|
|
47
|
+
function anchorMissingReasonOf(record: Record<string, unknown>): ZcodeAnchorMissingReason | undefined {
|
|
48
|
+
const handle = record.engineHandle
|
|
49
|
+
if (typeof handle !== 'object' || handle === null) return 'missing-engineHandle'
|
|
50
|
+
const ref = (handle as Record<string, unknown>).sessionRef
|
|
51
|
+
if (typeof ref !== 'object' || ref === null) return 'missing-sessionRef'
|
|
52
|
+
const r = ref as Record<string, unknown>
|
|
53
|
+
if (typeof r.sessionId !== 'string' || r.sessionId === '') return 'missing-sessionId'
|
|
54
|
+
if (typeof r.dbPath !== 'string' || r.dbPath === '') return 'missing-dbPath'
|
|
55
|
+
return undefined
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* entries 中第一条「在场但锚不完整」的 zcode subagent-record 的缺失归因;记录完全
|
|
60
|
+
* 不在场(或命中的记录锚全完整)返回 undefined。锚完整的命中不终止扫描——同 sa-id
|
|
61
|
+
* 多条记录时后续残缺记录仍可归因(与提取前的整循环行为一致)。
|
|
62
|
+
*/
|
|
63
|
+
export function firstIncompleteAnchorReason(
|
|
64
|
+
entries: readonly Entry[],
|
|
65
|
+
saId: string,
|
|
66
|
+
): ZcodeAnchorMissingReason | undefined {
|
|
67
|
+
for (const entry of entries) {
|
|
68
|
+
const record = zcodeRecordDataOf(entry, saId)
|
|
69
|
+
if (record === undefined) continue
|
|
70
|
+
const reason = anchorMissingReasonOf(record)
|
|
71
|
+
if (reason !== undefined) return reason
|
|
72
|
+
}
|
|
73
|
+
return undefined
|
|
74
|
+
}
|
|
@@ -1,96 +0,0 @@
|
|
|
1
|
-
import { existsSync } from 'node:fs'
|
|
2
|
-
import { execSync } from 'node:child_process'
|
|
3
|
-
import { homedir } from 'node:os'
|
|
4
|
-
import { join } from 'node:path'
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* 真实 pi agent 数据常量 + 存在性探测(MF-1:CI 无本机 ~/.pi/agent 目录,
|
|
8
|
-
* 依赖真实数据的用例必须 skipIf 守卫,否则 CI `pnpm extensions:test` 必红)。
|
|
9
|
-
*
|
|
10
|
-
* 用法:测试文件 `import { REAL_AGENT_DIR, HAS_E6 } from './real-data.js'`,
|
|
11
|
-
* 真实数据用例套 `it.skipIf(!HAS_X)` / `describe.skipIf(!HAS_X)`;
|
|
12
|
-
* fixture 用例保持无条件跑。
|
|
13
|
-
*
|
|
14
|
-
* 本文件无 .test.ts 后缀,vitest include 只收集 `src/__tests__/` 下以 .test.ts 结尾的文件,不收集本文件。
|
|
15
|
-
*/
|
|
16
|
-
|
|
17
|
-
/** 真实 pi agent 目录(pi 官方家目录布局,随宿主 homedir 推导),用于集成测试。 */
|
|
18
|
-
export const REAL_AGENT_DIR = join(homedir(), '.pi', 'agent')
|
|
19
|
-
|
|
20
|
-
/**
|
|
21
|
-
* 全量扫描类真实数据用例的统一超时标定(90s)。
|
|
22
|
-
*
|
|
23
|
-
* 这类用例遍历本机 ~/.pi/agent 全量会话语料(实测 4600+ 文件),空载单用例已需 6-10s;
|
|
24
|
-
* 38 包并行全量测试时 CPU/IO 争抢可降速 3-4 倍,30s 标定必假红。90s ≈ 空载 9 倍余量,
|
|
25
|
-
* 仍能兜住真正的挂死。仅用于遍历全量语料的用例(findSessions / buildFamilyFromFs /
|
|
26
|
-
* resolveSessionRoots 等);读已知单文件的用例不需要放宽。
|
|
27
|
-
*/
|
|
28
|
-
export const REAL_DATA_TIMEOUT_MS = 90_000
|
|
29
|
-
|
|
30
|
-
/** 5.4MB / 32 turn / 1204 entry 的真实 session(feat-plugin-arch-3 目录)。 */
|
|
31
|
-
export const E6 = '019e6c96-0a0c-74b8-a73f-d1854d88e2a7'
|
|
32
|
-
/** 真实 fork 家族根(fork 子代 019fe632,隔代 subagent 019fe635 挂在 019fe632 下)。 */
|
|
33
|
-
export const FAM = '019fe620-8ae1-78a7-b76a-43a1ba4cc3c7'
|
|
34
|
-
|
|
35
|
-
/**
|
|
36
|
-
* E6 的完整文件路径(parser/turns/render 直接 parseSessionFile 用)。
|
|
37
|
-
* 按 sid 运行时定位而非硬编码宿主 workspace 目录名:真实文件挂在哪个 slug 目录随采集机器而异,
|
|
38
|
-
* 硬编码会绑死本机路径形态;定位不到时回落相对布局,由下方 HAS_REAL_SESSION 守卫 skip。
|
|
39
|
-
*/
|
|
40
|
-
export const REAL_SESSION = (() => {
|
|
41
|
-
const fallback = join(REAL_AGENT_DIR, 'sessions', `2026-05-28T03-17-12-844Z_${E6}.jsonl`)
|
|
42
|
-
if (!existsSync(REAL_AGENT_DIR)) return fallback
|
|
43
|
-
try {
|
|
44
|
-
return (
|
|
45
|
-
execSync(
|
|
46
|
-
`find ${REAL_AGENT_DIR}/sessions -name '*${E6}*' -name '*.jsonl' ! -name '*.finalized' 2>/dev/null | head -1`,
|
|
47
|
-
{ encoding: 'utf8' },
|
|
48
|
-
).trim() || fallback
|
|
49
|
-
)
|
|
50
|
-
} catch {
|
|
51
|
-
return fallback
|
|
52
|
-
}
|
|
53
|
-
})()
|
|
54
|
-
|
|
55
|
-
/** 同步探测真实 session 文件是否存在(不存在则 skip,避免在无该数据的机器上硬失败)。 */
|
|
56
|
-
export function hasRealSession(sid: string): boolean {
|
|
57
|
-
if (!existsSync(REAL_AGENT_DIR)) return false
|
|
58
|
-
try {
|
|
59
|
-
return (
|
|
60
|
-
execSync(
|
|
61
|
-
`find ${REAL_AGENT_DIR}/sessions -name '*${sid}*' -name '*.jsonl' ! -name '*.finalized' 2>/dev/null | head -1`,
|
|
62
|
-
{ encoding: 'utf8' },
|
|
63
|
-
).trim().length > 0
|
|
64
|
-
)
|
|
65
|
-
} catch {
|
|
66
|
-
return false
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
/**
|
|
71
|
-
* 同步探测真实数据中任意 session 文件存在(main sessions/ 与 subagents/ 双目录)。
|
|
72
|
-
* 与 hasRealSession 的区别:subagent session 文件位于 subagents/<cwd>/sessions/ 下,
|
|
73
|
-
* hasRealSession 只扫主 sessions/ 目录扫不到。用于对活跃数据目录中具体文件(如 fork
|
|
74
|
-
* 子代/隔代 subagent)存在性的守卫探测。
|
|
75
|
-
*/
|
|
76
|
-
export function hasAnyRealSession(fragment: string): boolean {
|
|
77
|
-
if (!existsSync(REAL_AGENT_DIR)) return false
|
|
78
|
-
try {
|
|
79
|
-
return (
|
|
80
|
-
execSync(
|
|
81
|
-
`find ${REAL_AGENT_DIR}/sessions ${REAL_AGENT_DIR}/subagents -name '*${fragment}*' -name '*.jsonl' ! -name '*.finalized' 2>/dev/null | head -1`,
|
|
82
|
-
{ encoding: 'utf8' },
|
|
83
|
-
).trim().length > 0
|
|
84
|
-
)
|
|
85
|
-
} catch {
|
|
86
|
-
return false
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
export const HAS_REAL_AGENT_DIR = existsSync(REAL_AGENT_DIR)
|
|
91
|
-
export const HAS_REAL_SUBAGENTS_DIR = existsSync(join(REAL_AGENT_DIR, 'subagents'))
|
|
92
|
-
export const HAS_REAL_SESSION = existsSync(REAL_SESSION)
|
|
93
|
-
export const HAS_E6 = hasRealSession(E6)
|
|
94
|
-
export const HAS_FAM = hasRealSession(FAM)
|
|
95
|
-
/** tool-handler 的 handleSessionRead 套件同时依赖 E6 + FAM。 */
|
|
96
|
-
export const HAS_REAL = HAS_E6 && HAS_FAM
|
package/src/core/parser.ts
DELETED
|
@@ -1,167 +0,0 @@
|
|
|
1
|
-
import { readFile } from 'node:fs/promises'
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* session JSONL 文件解析后的单行 entry(冻结接口,M2-M5 契约)。
|
|
5
|
-
*
|
|
6
|
-
* 字段对齐 pi session JSONL 各 type 的 payload:
|
|
7
|
-
* - type / id / parentId:树结构字段。root(session header)在文件里无 parentId,
|
|
8
|
-
* 归一化为 null(design §3.5 算法 2 的 root 判定依据)。
|
|
9
|
-
* - message:仅 type=message(role/content/toolCalls)。
|
|
10
|
-
* toolName/toolCallId:仅 role=toolResult 的 message 透出(probe 实测 515/515 带,
|
|
11
|
-
* O2/O3 据此精确关联同 turn 内 toolCall 取参数,替代 v1 脆弱的顺序关联)。
|
|
12
|
-
* - customType / data:仅 type=custom。
|
|
13
|
-
* - parentSession / cwd:仅 type=session;parentSession 是 fork 文件指向来源的路径指针。
|
|
14
|
-
* - summary:仅 type=compaction。
|
|
15
|
-
*
|
|
16
|
-
* 注:冻结接口未列的 per-type 附加字段(如 model_change.provider、
|
|
17
|
-
* compaction.firstKeptEntryId、session.version)不在此暴露——M2+ 若需消费,
|
|
18
|
-
* 扩展 Entry 接口并同步 design.md §3.4,不在 parser 层私自保留。
|
|
19
|
-
*/
|
|
20
|
-
export interface Entry {
|
|
21
|
-
type: string
|
|
22
|
-
id: string
|
|
23
|
-
parentId: string | null
|
|
24
|
-
timestamp?: string
|
|
25
|
-
message?: {
|
|
26
|
-
role: 'user' | 'assistant' | 'toolResult'
|
|
27
|
-
content: unknown
|
|
28
|
-
toolCalls?: unknown[]
|
|
29
|
-
/** toolResult 的工具名(probe 实测 515/515 带,仅 role=toolResult 时存在) */
|
|
30
|
-
toolName?: string
|
|
31
|
-
/** toolResult 关联的 toolCall.id(probe 实测 515/515 全部匹配 toolCall.id) */
|
|
32
|
-
toolCallId?: string
|
|
33
|
-
}
|
|
34
|
-
customType?: string
|
|
35
|
-
data?: unknown
|
|
36
|
-
parentSession?: string
|
|
37
|
-
cwd?: string
|
|
38
|
-
summary?: unknown
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
export interface ParseResult {
|
|
42
|
-
entries: Entry[]
|
|
43
|
-
/** JSON 解析失败(含缺必填结构字段)的行数 */
|
|
44
|
-
skippedLines: number
|
|
45
|
-
totalBytes: number
|
|
46
|
-
/** 最后一行疑似半行(活跃 session 写入中),区别于中间坏行 */
|
|
47
|
-
lastLinePartial: boolean
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
function isMessageRole(v: unknown): v is 'user' | 'assistant' | 'toolResult' {
|
|
51
|
-
return v === 'user' || v === 'assistant' || v === 'toolResult'
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
/**
|
|
55
|
-
* 归一化 message 字段(阶段 helper:role 经值守卫收窄,缺/非法 role 时
|
|
56
|
-
* 返回 undefined,调用方丢弃 message 字段——接口 role 必填)。
|
|
57
|
-
*/
|
|
58
|
-
function parseEntryMessage(raw: unknown): Entry['message'] | undefined {
|
|
59
|
-
if (raw === null || typeof raw !== 'object') return undefined
|
|
60
|
-
const m = raw as Record<string, unknown>
|
|
61
|
-
if (!isMessageRole(m.role)) return undefined
|
|
62
|
-
const message: NonNullable<Entry['message']> = { role: m.role, content: m.content }
|
|
63
|
-
if (Array.isArray(m.toolCalls)) message.toolCalls = m.toolCalls
|
|
64
|
-
// toolResult 自带的工具关联字段(O2/O3 用,additive,实测 515/515 存在)
|
|
65
|
-
if (typeof m.toolName === 'string') message.toolName = m.toolName
|
|
66
|
-
if (typeof m.toolCallId === 'string') message.toolCallId = m.toolCallId
|
|
67
|
-
return message
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
/** 归一化剩余可选字段(阶段 helper:customType/data/parentSession/cwd/summary)。 */
|
|
71
|
-
function assignOptionalFields(obj: Record<string, unknown>, entry: Entry): void {
|
|
72
|
-
if (typeof obj.customType === 'string') entry.customType = obj.customType
|
|
73
|
-
if (obj.data !== undefined) entry.data = obj.data
|
|
74
|
-
if (typeof obj.parentSession === 'string') entry.parentSession = obj.parentSession
|
|
75
|
-
if (typeof obj.cwd === 'string') entry.cwd = obj.cwd
|
|
76
|
-
if (obj.summary !== undefined) entry.summary = obj.summary
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
/**
|
|
80
|
-
* 把单个已 JSON.parse 成功的原始对象归一化为 Entry。
|
|
81
|
-
* 缺必填结构字段(type/id)返回 undefined,调用方计为坏行(skippedLines++)。
|
|
82
|
-
*/
|
|
83
|
-
function toEntry(raw: unknown): Entry | undefined {
|
|
84
|
-
if (typeof raw !== 'object' || raw === null) return undefined
|
|
85
|
-
const obj = raw as Record<string, unknown>
|
|
86
|
-
if (typeof obj.type !== 'string') return undefined
|
|
87
|
-
|
|
88
|
-
// id 解析:pi appendCustomEntry 恒写顶层 id(session-manager.js:820-828)——
|
|
89
|
-
// 无需 fallback。data.id 是扩展业务字段(如 subagent-identity payload),不是 entry id。
|
|
90
|
-
// [W4 删除] 原 data.id fallback 分支对 pi 写出的 entry 是死分支(对历史/异构文件
|
|
91
|
-
// 也无必要——entry id 语义以顶层为准)。
|
|
92
|
-
const id: unknown = obj.id
|
|
93
|
-
if (typeof id !== 'string') return undefined
|
|
94
|
-
|
|
95
|
-
const entry: Entry = {
|
|
96
|
-
type: obj.type,
|
|
97
|
-
id: id as string,
|
|
98
|
-
parentId: typeof obj.parentId === 'string' ? obj.parentId : null,
|
|
99
|
-
}
|
|
100
|
-
if (typeof obj.timestamp === 'string') entry.timestamp = obj.timestamp
|
|
101
|
-
|
|
102
|
-
const message = parseEntryMessage(obj.message)
|
|
103
|
-
if (message !== undefined) entry.message = message
|
|
104
|
-
|
|
105
|
-
assignOptionalFields(obj, entry)
|
|
106
|
-
|
|
107
|
-
return entry
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
/**
|
|
111
|
-
* 解析 session JSONL 文本为 entries。
|
|
112
|
-
*
|
|
113
|
-
* 逐行 JSON.parse,坏行(语法错误或缺必填结构字段)计入 skippedLines 并跳过,
|
|
114
|
-
* 不中断整体解析——pi 坏 session 容错(design §2 失败模式)。
|
|
115
|
-
*
|
|
116
|
-
* 末尾换行产生的空行忽略(不计 skipped、不计 partial)。最后一行 parse 失败时
|
|
117
|
-
* lastLinePartial=true(活跃 session 写到一半的半行),区别于中间坏行。
|
|
118
|
-
*/
|
|
119
|
-
export function parseSessionContent(content: string): ParseResult {
|
|
120
|
-
const entries: Entry[] = []
|
|
121
|
-
let skippedLines = 0
|
|
122
|
-
let lastLinePartial = false
|
|
123
|
-
|
|
124
|
-
const lines = content.split('\n')
|
|
125
|
-
// 移除末尾因 trailing newline 产生的空行(非真实行)
|
|
126
|
-
while (lines.length > 0 && lines[lines.length - 1] === '') {
|
|
127
|
-
lines.pop()
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
for (let i = 0; i < lines.length; i++) {
|
|
131
|
-
const line = lines[i]
|
|
132
|
-
const isLast = i === lines.length - 1
|
|
133
|
-
|
|
134
|
-
// 中间空行容错:pi 正常 jsonl 无空行,防御文件损坏;不计 skipped
|
|
135
|
-
if (line.trim() === '') continue
|
|
136
|
-
|
|
137
|
-
let raw: unknown
|
|
138
|
-
try {
|
|
139
|
-
raw = JSON.parse(line)
|
|
140
|
-
} catch {
|
|
141
|
-
skippedLines++
|
|
142
|
-
if (isLast) lastLinePartial = true
|
|
143
|
-
continue
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
const entry = toEntry(raw)
|
|
147
|
-
if (entry === undefined) {
|
|
148
|
-
skippedLines++
|
|
149
|
-
if (isLast) lastLinePartial = true
|
|
150
|
-
continue
|
|
151
|
-
}
|
|
152
|
-
entries.push(entry)
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
return {
|
|
156
|
-
entries,
|
|
157
|
-
skippedLines,
|
|
158
|
-
totalBytes: Buffer.byteLength(content, 'utf8'),
|
|
159
|
-
lastLinePartial,
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
/** 读取 session 文件并解析。文件不存在按 Node fs 原生错误抛出(ENOENT)。 */
|
|
164
|
-
export async function parseSessionFile(filePath: string): Promise<ParseResult> {
|
|
165
|
-
const content = await readFile(filePath, 'utf8')
|
|
166
|
-
return parseSessionContent(content)
|
|
167
|
-
}
|