@rexvane/piweb 0.3.11 → 0.3.13
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/.next/BUILD_ID +1 -1
- package/.next/app-path-routes-manifest.json +8 -6
- package/.next/build-manifest.json +6 -6
- package/.next/prerender-manifest.json +7 -7
- package/.next/react-loadable-manifest.json +7 -7
- package/.next/routes-manifest.json +12 -0
- package/.next/server/app/_global-error/page.js +2 -2
- package/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
- package/.next/server/app/_global-error.html +1 -1
- package/.next/server/app/_global-error.rsc +1 -1
- package/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
- package/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/.next/server/app/_not-found/page.js +1 -1
- package/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
- package/.next/server/app/_not-found.html +1 -1
- package/.next/server/app/_not-found.rsc +1 -1
- package/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
- package/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
- package/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
- package/.next/server/app/api/agent/[id]/events/route.js +2 -2
- package/.next/server/app/api/agent/[id]/events/route.js.nft.json +1 -1
- package/.next/server/app/api/agent/[id]/route.js +1 -1
- package/.next/server/app/api/agent/[id]/route.js.nft.json +1 -1
- package/.next/server/app/api/agent/[id]/tree/route.js +1 -1
- package/.next/server/app/api/agent/[id]/tree/route.js.nft.json +1 -1
- package/.next/server/app/api/agent/new/route.js +1 -1
- package/.next/server/app/api/agent/new/route.js.nft.json +1 -1
- package/.next/server/app/api/config/route.js +1 -1
- package/.next/server/app/api/files/route.js +1 -1
- package/.next/server/app/api/git/route.js +2 -2
- package/.next/server/app/api/growth/route.js +1 -1
- package/.next/server/app/api/growth/route.js.nft.json +1 -1
- package/.next/server/app/api/health/route.js +1 -1
- package/.next/server/app/api/models/route.js +1 -1
- package/.next/server/app/api/models/route.js.nft.json +1 -1
- package/.next/server/app/api/pi-settings/route.js +1 -1
- package/.next/server/app/api/pi-settings/route.js.nft.json +1 -1
- package/.next/server/app/api/plugins/route.js +1 -1
- package/.next/server/app/api/plugins/route.js.nft.json +1 -1
- package/.next/server/app/api/prompts/route.js +1 -0
- package/.next/server/app/api/prompts/route.js.nft.json +1 -0
- package/.next/server/app/api/prompts/route_client-reference-manifest.js +1 -0
- package/.next/server/app/api/security/route.js +1 -1
- package/.next/server/app/api/security/route.js.nft.json +1 -1
- package/.next/server/app/api/sessions/[id]/export/route.js +1 -1
- package/.next/server/app/api/sessions/[id]/export/route.js.nft.json +1 -1
- package/.next/server/app/api/sessions/[id]/route.js +1 -1
- package/.next/server/app/api/sessions/[id]/route.js.nft.json +1 -1
- package/.next/server/app/api/sessions/import/route.js +6 -0
- package/.next/server/app/api/sessions/import/route.js.nft.json +1 -0
- package/.next/server/app/api/sessions/import/route_client-reference-manifest.js +1 -0
- package/.next/server/app/api/sessions/route.js +1 -1
- package/.next/server/app/api/sessions/route.js.nft.json +1 -1
- package/.next/server/app/api/skills/route.js +2 -2
- package/.next/server/app/api/skills/route.js.nft.json +1 -1
- package/.next/server/app/api/update/route.js +2 -2
- package/.next/server/app/api/update/route.js.nft.json +1 -1
- package/.next/server/app/api/version/route.js +1 -1
- package/.next/server/app/api/web-auth/route.js +1 -1
- package/.next/server/app/api/workspaces/route.js +1 -1
- package/.next/server/app/api/workspaces/route.js.nft.json +1 -1
- package/.next/server/app/icon.svg/route.js +1 -1
- package/.next/server/app/index.html +1 -1
- package/.next/server/app/index.rsc +3 -3
- package/.next/server/app/index.segments/__PAGE__.segment.rsc +3 -3
- package/.next/server/app/index.segments/_full.segment.rsc +3 -3
- package/.next/server/app/index.segments/_tree.segment.rsc +1 -1
- package/.next/server/app/login/page.js +1 -1
- package/.next/server/app/login/page_client-reference-manifest.js +1 -1
- package/.next/server/app/login.html +1 -1
- package/.next/server/app/login.rsc +2 -2
- package/.next/server/app/login.segments/_full.segment.rsc +2 -2
- package/.next/server/app/login.segments/_tree.segment.rsc +1 -1
- package/.next/server/app/login.segments/login/__PAGE__.segment.rsc +2 -2
- package/.next/server/app/page.js +1 -1
- package/.next/server/app/page_client-reference-manifest.js +1 -1
- package/.next/server/app-paths-manifest.json +8 -6
- package/.next/server/chunks/496.js +1 -1
- package/.next/server/chunks/532.js +1 -0
- package/.next/server/chunks/686.js +2 -2
- package/.next/server/middleware-build-manifest.js +1 -1
- package/.next/server/middleware-react-loadable-manifest.js +1 -1
- package/.next/server/pages/404.html +1 -1
- package/.next/server/pages/500.html +1 -1
- package/.next/server/server-reference-manifest.json +1 -1
- package/.next/static/chunks/2155.2f27fe7098987699.js +1 -0
- package/.next/static/chunks/2608.ec8ff1f5767c9056.js +1 -0
- package/.next/static/chunks/2716.a9e808190a4e2424.js +1 -0
- package/.next/static/chunks/{794-d1a40dcf24d651d9.js → 3794-3679f1c05160977c.js} +1 -1
- package/.next/static/chunks/4632.d574c210ad78d065.js +1 -0
- package/.next/static/chunks/4673.d3d0a7f2599080fb.js +1 -0
- package/.next/static/chunks/4686-0a4155f8488314b8.js +1 -0
- package/.next/static/chunks/4bd1b696-93bf506b236d6248.js +1 -0
- package/.next/static/chunks/app/_global-error/page-f77e9ede9808b18e.js +1 -0
- package/.next/static/chunks/app/_not-found/page-2d40d1d7d916666a.js +1 -0
- package/.next/static/chunks/app/api/agent/[id]/events/route-f77e9ede9808b18e.js +1 -0
- package/.next/static/chunks/app/api/agent/[id]/route-f77e9ede9808b18e.js +1 -0
- package/.next/static/chunks/app/api/agent/[id]/tree/route-f77e9ede9808b18e.js +1 -0
- package/.next/static/chunks/app/api/agent/new/route-f77e9ede9808b18e.js +1 -0
- package/.next/static/chunks/app/api/config/route-f77e9ede9808b18e.js +1 -0
- package/.next/static/chunks/app/api/files/route-f77e9ede9808b18e.js +1 -0
- package/.next/static/chunks/app/api/git/route-f77e9ede9808b18e.js +1 -0
- package/.next/static/chunks/app/api/growth/route-f77e9ede9808b18e.js +1 -0
- package/.next/static/chunks/app/api/health/route-f77e9ede9808b18e.js +1 -0
- package/.next/static/chunks/app/api/models/route-f77e9ede9808b18e.js +1 -0
- package/.next/static/chunks/app/api/pi-settings/route-f77e9ede9808b18e.js +1 -0
- package/.next/static/chunks/app/api/plugins/route-f77e9ede9808b18e.js +1 -0
- package/.next/static/chunks/app/api/prompts/route-f77e9ede9808b18e.js +1 -0
- package/.next/static/chunks/app/api/security/route-f77e9ede9808b18e.js +1 -0
- package/.next/static/chunks/app/api/sessions/[id]/export/route-f77e9ede9808b18e.js +1 -0
- package/.next/static/chunks/app/api/sessions/[id]/route-f77e9ede9808b18e.js +1 -0
- package/.next/static/chunks/app/api/sessions/import/route-f77e9ede9808b18e.js +1 -0
- package/.next/static/chunks/app/api/sessions/route-f77e9ede9808b18e.js +1 -0
- package/.next/static/chunks/app/api/skills/route-f77e9ede9808b18e.js +1 -0
- package/.next/static/chunks/app/api/update/route-f77e9ede9808b18e.js +1 -0
- package/.next/static/chunks/app/api/version/route-f77e9ede9808b18e.js +1 -0
- package/.next/static/chunks/app/api/web-auth/route-f77e9ede9808b18e.js +1 -0
- package/.next/static/chunks/app/api/workspaces/route-f77e9ede9808b18e.js +1 -0
- package/.next/static/chunks/app/layout-50d06e2078d6ce76.js +1 -0
- package/.next/static/chunks/app/login/page-52ef4841cbd199a8.js +1 -0
- package/.next/static/chunks/app/page-9a5440047c5a415c.js +1 -0
- package/.next/static/chunks/framework-c4e651fe89edfa91.js +1 -0
- package/.next/static/chunks/{main-a315d7c58b599962.js → main-6159b22a8cb0b7c5.js} +2 -2
- package/.next/static/chunks/main-app-e9b53f0e1c6591b2.js +1 -0
- package/.next/static/chunks/next/dist/client/components/builtin/app-error-f77e9ede9808b18e.js +1 -0
- package/.next/static/chunks/next/dist/client/components/builtin/forbidden-f77e9ede9808b18e.js +1 -0
- package/.next/static/chunks/next/dist/client/components/builtin/global-error-a7e260f8cc2582d7.js +1 -0
- package/.next/static/chunks/next/dist/client/components/builtin/not-found-f77e9ede9808b18e.js +1 -0
- package/.next/static/chunks/next/dist/client/components/builtin/unauthorized-f77e9ede9808b18e.js +1 -0
- package/.next/static/chunks/webpack-b1a572e319353f5d.js +1 -0
- package/.next/static/olV7jxKRN5VhfEK012WhV/_buildManifest.js +1 -0
- package/.next/types/app/api/prompts/route.ts +353 -0
- package/.next/types/app/api/sessions/import/route.ts +353 -0
- package/.next/types/routes.d.ts +3 -1
- package/.next/types/validator.ts +18 -0
- package/README.md +4 -0
- package/package.json +1 -1
- package/src/app/api/prompts/route.ts +51 -0
- package/src/app/api/sessions/import/route.ts +74 -0
- package/src/components/AppShell.tsx +42 -19
- package/src/components/ChatWindow.tsx +20 -35
- package/src/components/PromptPanel.tsx +259 -0
- package/src/components/SessionImportSection.tsx +333 -0
- package/src/components/SettingsPanel.tsx +63 -57
- package/src/i18n.tsx +98 -4
- package/src/lib/agent-manager.ts +35 -8
- package/src/lib/clipboard.ts +27 -0
- package/src/lib/command-validation.ts +1 -0
- package/src/lib/prompt-sources.ts +229 -0
- package/src/lib/session-import/claude.ts +219 -0
- package/src/lib/session-import/codex.ts +206 -0
- package/src/lib/session-import/dsh-zstd.ts +69 -0
- package/src/lib/session-import/dsh.ts +248 -0
- package/src/lib/session-import/files.ts +84 -0
- package/src/lib/session-import/grok.ts +166 -0
- package/src/lib/session-import/index.ts +229 -0
- package/src/lib/session-import/oc-family.ts +275 -0
- package/src/lib/session-import/opencode.ts +7 -0
- package/src/lib/session-import/paths.ts +37 -0
- package/src/lib/session-import/sqlite.ts +89 -0
- package/src/lib/session-import/types.ts +171 -0
- package/src/lib/session-import/writer.ts +259 -0
- package/src/lib/session-import/zcode.ts +8 -0
- package/src/lib/types.ts +1 -0
- package/.next/server/chunks/118.js +0 -1
- package/.next/static/222KLUIvefiGf7M0Ov-Qz/_buildManifest.js +0 -1
- package/.next/static/chunks/127.c31e688e156605e3.js +0 -1
- package/.next/static/chunks/155.a3c40ae481da1358.js +0 -1
- package/.next/static/chunks/4bd1b696-92152b0f5947070d.js +0 -1
- package/.next/static/chunks/608.5b8aae42a9998ee7.js +0 -1
- package/.next/static/chunks/673.93445c8600bc1dd9.js +0 -1
- package/.next/static/chunks/686-9f15e4e370d26815.js +0 -1
- package/.next/static/chunks/716.5266fe10b5578591.js +0 -1
- package/.next/static/chunks/app/_global-error/page-1dac1b3ca8adb975.js +0 -1
- package/.next/static/chunks/app/_not-found/page-9de30f6f614fce02.js +0 -1
- package/.next/static/chunks/app/api/agent/[id]/events/route-1dac1b3ca8adb975.js +0 -1
- package/.next/static/chunks/app/api/agent/[id]/route-1dac1b3ca8adb975.js +0 -1
- package/.next/static/chunks/app/api/agent/[id]/tree/route-1dac1b3ca8adb975.js +0 -1
- package/.next/static/chunks/app/api/agent/new/route-1dac1b3ca8adb975.js +0 -1
- package/.next/static/chunks/app/api/config/route-1dac1b3ca8adb975.js +0 -1
- package/.next/static/chunks/app/api/files/route-1dac1b3ca8adb975.js +0 -1
- package/.next/static/chunks/app/api/git/route-1dac1b3ca8adb975.js +0 -1
- package/.next/static/chunks/app/api/growth/route-1dac1b3ca8adb975.js +0 -1
- package/.next/static/chunks/app/api/health/route-1dac1b3ca8adb975.js +0 -1
- package/.next/static/chunks/app/api/models/route-1dac1b3ca8adb975.js +0 -1
- package/.next/static/chunks/app/api/pi-settings/route-1dac1b3ca8adb975.js +0 -1
- package/.next/static/chunks/app/api/plugins/route-1dac1b3ca8adb975.js +0 -1
- package/.next/static/chunks/app/api/security/route-1dac1b3ca8adb975.js +0 -1
- package/.next/static/chunks/app/api/sessions/[id]/export/route-1dac1b3ca8adb975.js +0 -1
- package/.next/static/chunks/app/api/sessions/[id]/route-1dac1b3ca8adb975.js +0 -1
- package/.next/static/chunks/app/api/sessions/route-1dac1b3ca8adb975.js +0 -1
- package/.next/static/chunks/app/api/skills/route-1dac1b3ca8adb975.js +0 -1
- package/.next/static/chunks/app/api/update/route-1dac1b3ca8adb975.js +0 -1
- package/.next/static/chunks/app/api/version/route-1dac1b3ca8adb975.js +0 -1
- package/.next/static/chunks/app/api/web-auth/route-1dac1b3ca8adb975.js +0 -1
- package/.next/static/chunks/app/api/workspaces/route-1dac1b3ca8adb975.js +0 -1
- package/.next/static/chunks/app/layout-eb62bcd99abe79f9.js +0 -1
- package/.next/static/chunks/app/login/page-d2308b48d013d1a6.js +0 -1
- package/.next/static/chunks/app/page-708cd3ee4cfe8af5.js +0 -1
- package/.next/static/chunks/framework-0f597d6e68ffafed.js +0 -1
- package/.next/static/chunks/main-app-1a92e20ec190a32b.js +0 -1
- package/.next/static/chunks/next/dist/client/components/builtin/app-error-1dac1b3ca8adb975.js +0 -1
- package/.next/static/chunks/next/dist/client/components/builtin/forbidden-1dac1b3ca8adb975.js +0 -1
- package/.next/static/chunks/next/dist/client/components/builtin/global-error-7bb5c4afcbc1772b.js +0 -1
- package/.next/static/chunks/next/dist/client/components/builtin/not-found-1dac1b3ca8adb975.js +0 -1
- package/.next/static/chunks/next/dist/client/components/builtin/unauthorized-1dac1b3ca8adb975.js +0 -1
- package/.next/static/chunks/webpack-9d84b382d4dec27a.js +0 -1
- /package/.next/static/{222KLUIvefiGf7M0Ov-Qz → olV7jxKRN5VhfEK012WhV}/_ssgManifest.js +0 -0
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Codex CLI(~/.codex/sessions/YYYY/MM/DD/rollout-*.jsonl)
|
|
3
|
+
*
|
|
4
|
+
* 每行统一 `{timestamp, type, payload}`;payload.type = session_meta / response_item / event_msg …
|
|
5
|
+
* response_item.payload 里才有对话:message / reasoning / function_call / custom_tool_call(+output)。
|
|
6
|
+
* 注意首行可达十几 KB(含 base_instructions),必须读到换行再 parse。
|
|
7
|
+
*/
|
|
8
|
+
import path from "node:path";
|
|
9
|
+
import fs from "node:fs/promises";
|
|
10
|
+
import { eachJsonLine, listFiles, readHead, reachedLimit, sortByRecency } from "./files";
|
|
11
|
+
import { codexRoot } from "./paths";
|
|
12
|
+
import type { ExternalSessionSummary, ImportedBlock, ImportedEntry, ImportedSession, ImportSourceModule, ScanOptions } from "./types";
|
|
13
|
+
import { cleanTitle, compactBlocks, outputBlocks, parseArguments, textBlock, toEpochMs } from "./types";
|
|
14
|
+
|
|
15
|
+
const ROOT = codexRoot;
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* 合成输入前缀(与参考实现同口径:精确前缀,不用 "#" 通配,避免误杀用户粘贴的 markdown)
|
|
19
|
+
*/
|
|
20
|
+
const SYNTHETIC_PREFIXES = [
|
|
21
|
+
"<",
|
|
22
|
+
"# AGENTS.md",
|
|
23
|
+
"# Context from my IDE setup",
|
|
24
|
+
"# In app browser:",
|
|
25
|
+
"# Browser comments:",
|
|
26
|
+
"# Files mentioned by the user:",
|
|
27
|
+
"# Diff comments:",
|
|
28
|
+
"# Selected text:",
|
|
29
|
+
"# Review findings:",
|
|
30
|
+
"You are Codex",
|
|
31
|
+
];
|
|
32
|
+
|
|
33
|
+
const isSynthetic = (text: string) => SYNTHETIC_PREFIXES.some((prefix) => text.startsWith(prefix));
|
|
34
|
+
|
|
35
|
+
interface Meta {
|
|
36
|
+
cwd?: string;
|
|
37
|
+
createdAt?: number;
|
|
38
|
+
model?: string;
|
|
39
|
+
provider?: string;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function metaFrom(records: Record<string, unknown>[]): Meta {
|
|
43
|
+
const meta: Meta = {};
|
|
44
|
+
for (const record of records) {
|
|
45
|
+
if (record.type !== "session_meta") continue;
|
|
46
|
+
const payload = record.payload as Record<string, unknown> | undefined;
|
|
47
|
+
if (!payload) continue;
|
|
48
|
+
if (typeof payload.cwd === "string") meta.cwd = payload.cwd;
|
|
49
|
+
meta.createdAt = toEpochMs(payload.timestamp) ?? toEpochMs(record.timestamp);
|
|
50
|
+
if (typeof payload.model_provider === "string") meta.provider = payload.model_provider;
|
|
51
|
+
if (typeof payload.model === "string") meta.model = payload.model;
|
|
52
|
+
}
|
|
53
|
+
// 模型有时只在 turn_context 里出现
|
|
54
|
+
for (const record of records) {
|
|
55
|
+
if (record.type !== "turn_context") continue;
|
|
56
|
+
const payload = record.payload as Record<string, unknown> | undefined;
|
|
57
|
+
if (typeof payload?.model === "string") meta.model = payload.model;
|
|
58
|
+
}
|
|
59
|
+
return meta;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function messageBlocks(payload: Record<string, unknown>): ImportedBlock[] {
|
|
63
|
+
const content = payload.content;
|
|
64
|
+
if (!Array.isArray(content)) return compactBlocks([textBlock(typeof content === "string" ? content : undefined)]);
|
|
65
|
+
return compactBlocks(content.flatMap((raw): Array<ImportedBlock | null> => {
|
|
66
|
+
const block = raw as Record<string, unknown>;
|
|
67
|
+
if (typeof block?.text === "string") return [textBlock(block.text)];
|
|
68
|
+
return [];
|
|
69
|
+
}));
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function reasoningText(payload: Record<string, unknown>): ImportedBlock | null {
|
|
73
|
+
const summary = payload.summary;
|
|
74
|
+
if (typeof summary === "string") return summary.trim() ? { type: "thinking", thinking: summary } : null;
|
|
75
|
+
if (Array.isArray(summary)) {
|
|
76
|
+
const text = summary.map((item) => (item as { text?: unknown })?.text).filter((value): value is string => typeof value === "string").join("\n");
|
|
77
|
+
return text.trim() ? { type: "thinking", thinking: text } : null;
|
|
78
|
+
}
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** 标题:Codex 不写标题字段,用首条真人用户消息(rollout 头几十行里就有) */
|
|
83
|
+
function titleFrom(records: Record<string, unknown>[]): string | undefined {
|
|
84
|
+
for (const record of records) {
|
|
85
|
+
if (record.type !== "response_item") continue;
|
|
86
|
+
const payload = record.payload as Record<string, unknown> | undefined;
|
|
87
|
+
if (payload?.type !== "message" || payload.role !== "user") continue;
|
|
88
|
+
const blocks = messageBlocks(payload);
|
|
89
|
+
const plain = blocks.filter((block) => block.type === "text").map((block) => block.text).join("\n").replace(/\s+/g, " ").trim();
|
|
90
|
+
if (plain && !isSynthetic(plain)) return plain;
|
|
91
|
+
}
|
|
92
|
+
return undefined;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export const codexSource: ImportSourceModule = {
|
|
96
|
+
id: "codex",
|
|
97
|
+
|
|
98
|
+
async scan(options?: ScanOptions): Promise<ExternalSessionSummary[]> {
|
|
99
|
+
const limit = options?.limit ?? 0;
|
|
100
|
+
const candidates = await sortByRecency(await listFiles(ROOT(), { match: (name) => name.startsWith("rollout-") && name.endsWith(".jsonl") }));
|
|
101
|
+
const out: ExternalSessionSummary[] = [];
|
|
102
|
+
for (const { file, mtime } of candidates) {
|
|
103
|
+
if (reachedLimit(out.length, limit)) break; // 只读最近的若干条
|
|
104
|
+
let head: string;
|
|
105
|
+
try {
|
|
106
|
+
head = await readHead(file);
|
|
107
|
+
} catch {
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
const records = [...eachJsonLine(head)].slice(0, 300);
|
|
111
|
+
const meta = metaFrom(records);
|
|
112
|
+
const hasMessage = records.some((record) => record.type === "response_item");
|
|
113
|
+
if (!hasMessage) continue;
|
|
114
|
+
out.push({
|
|
115
|
+
source: "codex",
|
|
116
|
+
externalId: path.basename(file, ".jsonl"),
|
|
117
|
+
title: cleanTitle(titleFrom(records)),
|
|
118
|
+
projectPath: meta.cwd,
|
|
119
|
+
model: meta.model,
|
|
120
|
+
provider: meta.provider,
|
|
121
|
+
createdAt: meta.createdAt ?? mtime,
|
|
122
|
+
updatedAt: mtime,
|
|
123
|
+
location: file,
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
return out;
|
|
127
|
+
},
|
|
128
|
+
|
|
129
|
+
async read(summary: ExternalSessionSummary): Promise<ImportedSession | null> {
|
|
130
|
+
let text: string;
|
|
131
|
+
try {
|
|
132
|
+
text = await fs.readFile(summary.location, "utf8");
|
|
133
|
+
} catch {
|
|
134
|
+
return null;
|
|
135
|
+
}
|
|
136
|
+
const entries: ImportedEntry[] = [];
|
|
137
|
+
const skipped: string[] = [];
|
|
138
|
+
let synthetic = 0;
|
|
139
|
+
let reasoningSkipped = 0;
|
|
140
|
+
let meta: Meta = { cwd: summary.projectPath, createdAt: summary.createdAt, model: summary.model, provider: summary.provider };
|
|
141
|
+
|
|
142
|
+
for (const record of eachJsonLine(text)) {
|
|
143
|
+
if (record.type === "session_meta") {
|
|
144
|
+
meta = { ...meta, ...metaFrom([record]) };
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
if (record.type !== "response_item") continue;
|
|
148
|
+
const payload = record.payload as Record<string, unknown> | undefined;
|
|
149
|
+
if (!payload) continue;
|
|
150
|
+
const timestamp = toEpochMs(record.timestamp) ?? toEpochMs(payload.timestamp) ?? meta.createdAt ?? Date.now();
|
|
151
|
+
|
|
152
|
+
switch (payload.type) {
|
|
153
|
+
case "message": {
|
|
154
|
+
const role = payload.role;
|
|
155
|
+
if (role === "developer") continue; // 系统提示,不进对话
|
|
156
|
+
const blocks = messageBlocks(payload);
|
|
157
|
+
const plain = blocks.filter((block) => block.type === "text").map((block) => block.text).join("\n").trim();
|
|
158
|
+
if (role === "user") {
|
|
159
|
+
if (!plain || isSynthetic(plain)) {
|
|
160
|
+
synthetic += 1;
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
163
|
+
entries.push({ type: "message", message: { role: "user", content: blocks, timestamp } });
|
|
164
|
+
} else if (role === "assistant") {
|
|
165
|
+
if (!blocks.length) continue;
|
|
166
|
+
entries.push({ type: "message", message: { role: "assistant", content: blocks, timestamp, ...(meta.model ? { model: meta.model } : {}) } });
|
|
167
|
+
}
|
|
168
|
+
continue;
|
|
169
|
+
}
|
|
170
|
+
case "reasoning": {
|
|
171
|
+
// 有摘要就保留成 thinking;只有加密内容时跳过
|
|
172
|
+
const thinking = reasoningText(payload);
|
|
173
|
+
if (!thinking) {
|
|
174
|
+
reasoningSkipped += 1;
|
|
175
|
+
continue;
|
|
176
|
+
}
|
|
177
|
+
entries.push({ type: "message", message: { role: "assistant", content: [thinking], timestamp, ...(meta.model ? { model: meta.model } : {}) } });
|
|
178
|
+
continue;
|
|
179
|
+
}
|
|
180
|
+
case "function_call":
|
|
181
|
+
case "custom_tool_call": {
|
|
182
|
+
const callId = typeof payload.call_id === "string" ? payload.call_id : "";
|
|
183
|
+
const name = typeof payload.name === "string" ? payload.name : "";
|
|
184
|
+
if (!callId || !name) continue;
|
|
185
|
+
const args = parseArguments(payload.arguments ?? payload.input ?? {});
|
|
186
|
+
entries.push({ type: "message", message: { role: "assistant", content: [{ type: "toolCall", id: callId, name, arguments: args }], timestamp, stopReason: "toolUse", ...(meta.model ? { model: meta.model } : {}) } });
|
|
187
|
+
continue;
|
|
188
|
+
}
|
|
189
|
+
case "function_call_output":
|
|
190
|
+
case "custom_tool_call_output": {
|
|
191
|
+
const callId = typeof payload.call_id === "string" ? payload.call_id : "";
|
|
192
|
+
if (!callId) continue;
|
|
193
|
+
entries.push({ type: "message", message: { role: "toolResult", toolCallId: callId, content: outputBlocks(payload.output ?? payload.content), timestamp } });
|
|
194
|
+
continue;
|
|
195
|
+
}
|
|
196
|
+
default:
|
|
197
|
+
continue;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
if (synthetic) skipped.push(`跳过 ${synthetic} 条合成输入`);
|
|
202
|
+
if (reasoningSkipped) skipped.push(`跳过 ${reasoningSkipped} 条仅含加密内容的推理记录`);
|
|
203
|
+
if (!entries.some((entry) => entry.type === "message")) return null;
|
|
204
|
+
return { summary: { ...summary, ...meta, projectPath: meta.cwd, model: meta.model, provider: meta.provider }, entries, skipped };
|
|
205
|
+
},
|
|
206
|
+
};
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsh 的会话是 **多帧拼接的 zstd**(`session.jsonl.zstd`):`zstdDecompressSync` 一次只解出第一帧,
|
|
3
|
+
* 必须按 zstd magic(28 B5 2F FD)切帧后逐帧解压再拼起来,否则只能拿到第一行。
|
|
4
|
+
*/
|
|
5
|
+
import fs from "node:fs/promises";
|
|
6
|
+
import { zstdDecompressSync } from "node:zlib";
|
|
7
|
+
|
|
8
|
+
const MAGIC = Buffer.from([0x28, 0xb5, 0x2f, 0xfd]);
|
|
9
|
+
|
|
10
|
+
/** 解压多帧 zstd;帧边界只是"看起来像 magic"的位置,解压失败就把这片并入下一帧 */
|
|
11
|
+
export function decodeZstdFrames(raw: Buffer): Buffer {
|
|
12
|
+
if (raw.length < MAGIC.length || !raw.subarray(0, MAGIC.length).equals(MAGIC)) {
|
|
13
|
+
// 不是多帧流(可能单帧或未压缩):交给单帧解压,失败则原样当作文本
|
|
14
|
+
try {
|
|
15
|
+
return zstdDecompressSync(raw);
|
|
16
|
+
} catch {
|
|
17
|
+
return raw;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
const cuts: number[] = [];
|
|
21
|
+
for (let i = 0; (i = raw.indexOf(MAGIC, i)) !== -1; i += MAGIC.length) cuts.push(i);
|
|
22
|
+
cuts.push(raw.length);
|
|
23
|
+
const parts: Buffer[] = [];
|
|
24
|
+
let start = 0;
|
|
25
|
+
for (let i = 1; i < cuts.length; i += 1) {
|
|
26
|
+
try {
|
|
27
|
+
parts.push(zstdDecompressSync(raw.subarray(cuts[start], cuts[i])));
|
|
28
|
+
start = i;
|
|
29
|
+
} catch {
|
|
30
|
+
// 误判的帧边界:与下一帧合并后重试
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return Buffer.concat(parts);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export async function readZstdJsonl(file: string): Promise<string> {
|
|
37
|
+
return decodeZstdFrames(await fs.readFile(file)).toString("utf8");
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* 只解出前面的行即可判断会话是否值得导入(扫描用):逐帧解压,凑够若干帧就停,
|
|
42
|
+
* 避免为了列个表把 39MB 全解出来。
|
|
43
|
+
*
|
|
44
|
+
* `complete` 表示"解出来的就是全部内容"——真实文件能到 7559 帧,前 40 帧只覆盖开头一小段,
|
|
45
|
+
* 所以只有 complete 为真时才能断言"这个会话里面没有消息"。
|
|
46
|
+
*/
|
|
47
|
+
export async function readZstdHead(file: string, maxFrames = 40): Promise<{ text: string; complete: boolean }> {
|
|
48
|
+
const raw = await fs.readFile(file);
|
|
49
|
+
const cuts: number[] = [];
|
|
50
|
+
for (let i = 0; (i = raw.indexOf(MAGIC, i)) !== -1; i += MAGIC.length) cuts.push(i);
|
|
51
|
+
if (!cuts.length) {
|
|
52
|
+
try {
|
|
53
|
+
return { text: zstdDecompressSync(raw).toString("utf8").slice(0, 64 * 1024), complete: true };
|
|
54
|
+
} catch {
|
|
55
|
+
return { text: "", complete: false };
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
cuts.push(raw.length);
|
|
59
|
+
const total = cuts.length - 1;
|
|
60
|
+
const out: string[] = [];
|
|
61
|
+
for (let i = 1; i < cuts.length && i <= maxFrames; i += 1) {
|
|
62
|
+
try {
|
|
63
|
+
out.push(zstdDecompressSync(raw.subarray(cuts[i - 1], cuts[i])).toString("utf8"));
|
|
64
|
+
} catch {
|
|
65
|
+
// 边界误判:跳过这片
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return { text: out.join(""), complete: total <= maxFrames };
|
|
69
|
+
}
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsh(DeepSeek Harness):`~/.dsh/sessions/<编码 cwd>/session-<uuid>/session.jsonl.zstd`
|
|
3
|
+
*
|
|
4
|
+
* 多帧 zstd 压缩的 JSONL,行形状 `{type, seq, time, data}`。实测要点(照抄真实文件):
|
|
5
|
+
* - assistant/message 的 content 里 **已经带 tool-call 块**,与独立的 tool/call 行是同一批调用
|
|
6
|
+
* (本机 241 : 241 完全对应),所以只取前者,否则工具调用会翻倍。
|
|
7
|
+
* - tool/result 是 `message.content[0] = {type:"tool-result", toolCallId, content:[...], isError}`,
|
|
8
|
+
* 比别的来源深一层。
|
|
9
|
+
* - plugin / agent-instructions 来源的 user/message 是运行时注入(沙箱策略、AGENTS.md 快照、
|
|
10
|
+
* 压缩摘要),pi 端会注入自己的那一套,导入时跳过并计数。
|
|
11
|
+
*/
|
|
12
|
+
import path from "node:path";
|
|
13
|
+
import fs from "node:fs/promises";
|
|
14
|
+
import { listFiles, reachedLimit, sortByRecency } from "./files";
|
|
15
|
+
import { dshRoot } from "./paths";
|
|
16
|
+
import { readZstdHead, readZstdJsonl } from "./dsh-zstd";
|
|
17
|
+
import type { ExternalSessionSummary, ImportedBlock, ImportedEntry, ImportedSession, ImportSourceModule, ScanOptions } from "./types";
|
|
18
|
+
import { cleanTitle, compactBlocks, outputBlocks, parseArguments, textBlock, toEpochMs } from "./types";
|
|
19
|
+
|
|
20
|
+
const ROOT = dshRoot;
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* 目录名是编码后的 cwd(`--D-AIApp-PiWeb--`);非 ASCII 用 `~XXXX` 表示码位
|
|
24
|
+
* (`--D-AIApp-Ordo-~524D~7AEF~9875~9762--` = `D:\AIApp\Ordo-前端页面`)。
|
|
25
|
+
* 还原不出来就返回 undefined,交给 header.cwd。
|
|
26
|
+
*/
|
|
27
|
+
export function decodeCwdDir(name: string): string | undefined {
|
|
28
|
+
const match = /^--(.+)--$/.exec(name);
|
|
29
|
+
if (!match) return undefined;
|
|
30
|
+
const body = match[1].replace(/~([0-9A-Fa-f]{4})/g, (_, hex: string) => String.fromCharCode(parseInt(hex, 16)));
|
|
31
|
+
if (!/^[A-Za-z]-/.test(body)) return undefined; // 只有盘符风格能可靠还原(POSIX 路径无法与 - 区分)
|
|
32
|
+
return `${body[0]}:\\${body.slice(2).split("-").filter(Boolean).join("\\")}`;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
interface DshHeader {
|
|
36
|
+
id?: string;
|
|
37
|
+
cwd?: string;
|
|
38
|
+
createdAt?: number;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function parseRecords(text: string): { header: DshHeader; records: Record<string, unknown>[] } {
|
|
42
|
+
const records: Record<string, unknown>[] = [];
|
|
43
|
+
let header: DshHeader = {};
|
|
44
|
+
for (const line of text.split("\n")) {
|
|
45
|
+
const trimmed = line.trim();
|
|
46
|
+
if (!trimmed) continue;
|
|
47
|
+
let parsed: Record<string, unknown>;
|
|
48
|
+
try {
|
|
49
|
+
parsed = JSON.parse(trimmed) as Record<string, unknown>;
|
|
50
|
+
} catch {
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
if (parsed.type === "session") {
|
|
54
|
+
header = { id: typeof parsed.id === "string" ? parsed.id : undefined, cwd: typeof parsed.cwd === "string" ? parsed.cwd : undefined, createdAt: toEpochMs(parsed.createdAt) };
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
records.push(parsed);
|
|
58
|
+
}
|
|
59
|
+
return { header, records };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** request/header 里带 provider+model;取最后一次生效的作为会话的模型 */
|
|
63
|
+
function configOf(data: Record<string, unknown> | undefined): { provider?: string; model?: string } {
|
|
64
|
+
const config = (data?.header as Record<string, unknown> | undefined)?.config as Record<string, unknown> | undefined;
|
|
65
|
+
return {
|
|
66
|
+
provider: typeof config?.provider === "string" ? config.provider : undefined,
|
|
67
|
+
model: typeof config?.model === "string" ? config.model : undefined,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** assistant/message 的 content 块:reasoning→thinking、text→text、tool-call→toolCall */
|
|
72
|
+
function assistantBlocks(content: unknown): ImportedBlock[] {
|
|
73
|
+
if (!Array.isArray(content)) return [];
|
|
74
|
+
return compactBlocks(content.flatMap((raw): Array<ImportedBlock | null> => {
|
|
75
|
+
const block = raw as Record<string, unknown>;
|
|
76
|
+
if (block?.type === "reasoning" || block?.type === "thinking") {
|
|
77
|
+
return typeof block.text === "string" && block.text.trim() ? [{ type: "thinking", thinking: block.text }] : [];
|
|
78
|
+
}
|
|
79
|
+
if (block?.type === "text") return [textBlock(block.text)];
|
|
80
|
+
if (block?.type === "tool-call") {
|
|
81
|
+
const id = typeof block.id === "string" ? block.id : "";
|
|
82
|
+
const name = typeof block.name === "string" ? block.name : "";
|
|
83
|
+
return id && name ? [{ type: "toolCall", id, name, arguments: parseArguments(block.arguments) }] : [];
|
|
84
|
+
}
|
|
85
|
+
return [];
|
|
86
|
+
}));
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** 用户消息块:只认 text */
|
|
90
|
+
function userBlocks(content: unknown): ImportedBlock[] {
|
|
91
|
+
if (!Array.isArray(content)) return [];
|
|
92
|
+
return compactBlocks(content.flatMap((raw): Array<ImportedBlock | null> => {
|
|
93
|
+
const block = raw as Record<string, unknown>;
|
|
94
|
+
return block?.type === "text" ? [textBlock(block.text)] : [];
|
|
95
|
+
}));
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* 一个会话目录里可能同时存在 `session.jsonl.zstd` 与 `session.v3.jsonl.zstd`(迁移产物,
|
|
100
|
+
* 本机有 6 个这样的目录)。两者是同一个会话、externalId 相同,都列出来就会重复,
|
|
101
|
+
* 导入时还会被判成"重复选择"。选 v3:它是迁移后的规范格式,去掉了流式分片,
|
|
102
|
+
* 实测消息条数反而更多(44/171/241/256 → 46/179/247/262),也确认了 v3 才是最新的完整历史。
|
|
103
|
+
*/
|
|
104
|
+
function pickPerSession(files: string[]): string[] {
|
|
105
|
+
const best = new Map<string, string>();
|
|
106
|
+
for (const file of files) {
|
|
107
|
+
const dir = path.dirname(file);
|
|
108
|
+
const previous = best.get(dir);
|
|
109
|
+
if (!previous || (file.includes(".v3.") && !previous.includes(".v3."))) best.set(dir, file);
|
|
110
|
+
}
|
|
111
|
+
return [...best.values()];
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** tool/result 的输出深一层:{type:"tool-result", toolCallId, content:[{type:"text"}], isError} */
|
|
115
|
+
function toolResultOf(message: Record<string, unknown> | undefined): { callId: string; content: ImportedBlock[]; isError: boolean } {
|
|
116
|
+
const blocks = Array.isArray(message?.content) ? (message?.content as Record<string, unknown>[]) : [];
|
|
117
|
+
const node = (blocks.find((block) => block?.type === "tool-result") ?? blocks[0]) as Record<string, unknown> | undefined;
|
|
118
|
+
const source = message?.source as Record<string, unknown> | undefined;
|
|
119
|
+
const callId = typeof node?.toolCallId === "string" ? node.toolCallId : typeof source?.callId === "string" ? source.callId : "";
|
|
120
|
+
return { callId, content: outputBlocks(node?.content ?? node), isError: node?.isError === true };
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export const dshSource: ImportSourceModule = {
|
|
124
|
+
id: "dsh",
|
|
125
|
+
|
|
126
|
+
async scan(options?: ScanOptions): Promise<ExternalSessionSummary[]> {
|
|
127
|
+
const limit = options?.limit ?? 0;
|
|
128
|
+
const files = await listFiles(ROOT(), { match: (name) => name.endsWith(".jsonl.zstd") });
|
|
129
|
+
const candidates = await sortByRecency(pickPerSession(files));
|
|
130
|
+
const out: ExternalSessionSummary[] = [];
|
|
131
|
+
for (const { file, mtime } of candidates) {
|
|
132
|
+
if (reachedLimit(out.length, limit)) break; // 只解压最近的若干条:其余的连 zstd 都不碰
|
|
133
|
+
let head: { text: string; complete: boolean };
|
|
134
|
+
try {
|
|
135
|
+
head = await readZstdHead(file);
|
|
136
|
+
} catch {
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
139
|
+
const { header, records } = parseRecords(head.text);
|
|
140
|
+
if (!header.id) continue;
|
|
141
|
+
// 建了却从没用过的会话(只有 header 与几条配置):整个文件都解出来却没有消息,不该列进列表
|
|
142
|
+
if (head.complete && !records.some((record) => record.type === "user/message" || record.type === "assistant/message")) continue;
|
|
143
|
+
let title: string | undefined;
|
|
144
|
+
let provider: string | undefined;
|
|
145
|
+
let model: string | undefined;
|
|
146
|
+
for (const record of records) {
|
|
147
|
+
const data = record.data as Record<string, unknown> | undefined;
|
|
148
|
+
if (record.type === "session/title") {
|
|
149
|
+
title = cleanTitle(data?.title ?? data?.name) ?? title;
|
|
150
|
+
}
|
|
151
|
+
if (record.type === "request/header") {
|
|
152
|
+
const config = configOf(data);
|
|
153
|
+
provider = config.provider ?? provider;
|
|
154
|
+
model = config.model ?? model;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
const sessionDir = path.basename(path.dirname(file));
|
|
158
|
+
out.push({
|
|
159
|
+
source: "dsh",
|
|
160
|
+
externalId: sessionDir || header.id,
|
|
161
|
+
title,
|
|
162
|
+
projectPath: header.cwd ?? decodeCwdDir(path.basename(path.dirname(path.dirname(file)))),
|
|
163
|
+
provider,
|
|
164
|
+
model,
|
|
165
|
+
createdAt: header.createdAt ?? mtime,
|
|
166
|
+
updatedAt: mtime,
|
|
167
|
+
// 扫描只解前若干帧,统计整会话条数不可靠:留空由界面显示 —
|
|
168
|
+
location: file,
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
return out;
|
|
172
|
+
},
|
|
173
|
+
|
|
174
|
+
async read(summary: ExternalSessionSummary): Promise<ImportedSession | null> {
|
|
175
|
+
let text: string;
|
|
176
|
+
try {
|
|
177
|
+
text = await readZstdJsonl(summary.location);
|
|
178
|
+
} catch {
|
|
179
|
+
return null;
|
|
180
|
+
}
|
|
181
|
+
const { header, records } = parseRecords(text);
|
|
182
|
+
const entries: ImportedEntry[] = [];
|
|
183
|
+
let title = summary.title;
|
|
184
|
+
let injected = 0;
|
|
185
|
+
let provider = summary.provider;
|
|
186
|
+
let model = summary.model;
|
|
187
|
+
let lastAt = header.createdAt ?? summary.createdAt ?? Date.now();
|
|
188
|
+
|
|
189
|
+
for (const record of records) {
|
|
190
|
+
const type = typeof record.type === "string" ? record.type : "";
|
|
191
|
+
const at = toEpochMs(record.time) ?? lastAt;
|
|
192
|
+
lastAt = at;
|
|
193
|
+
const data = record.data as Record<string, unknown> | undefined;
|
|
194
|
+
|
|
195
|
+
if (type === "user/message") {
|
|
196
|
+
const source = data?.source as Record<string, unknown> | undefined;
|
|
197
|
+
if (source?.kind !== "user") {
|
|
198
|
+
injected += 1; // 插件注入 / AGENTS.md 快照 / 压缩摘要:pi 端会注入自己的版本
|
|
199
|
+
continue;
|
|
200
|
+
}
|
|
201
|
+
const content = userBlocks(data?.content);
|
|
202
|
+
if (!content.length) continue;
|
|
203
|
+
entries.push({ type: "message", message: { role: "user", content, timestamp: at } });
|
|
204
|
+
continue;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
if (type === "assistant/message") {
|
|
208
|
+
const message = data?.message as Record<string, unknown> | undefined;
|
|
209
|
+
const content = assistantBlocks(message?.content);
|
|
210
|
+
if (!content.length) continue;
|
|
211
|
+
const hasCall = content.some((block) => block.type === "toolCall");
|
|
212
|
+
entries.push({ type: "message", message: { role: "assistant", content, timestamp: at, stopReason: hasCall ? "toolUse" : "stop" } });
|
|
213
|
+
continue;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// tool/call 行与 assistant/message 里的 tool-call 块是同一批调用,重复导入会让调用翻倍
|
|
217
|
+
if (type === "tool/call") continue;
|
|
218
|
+
|
|
219
|
+
if (type === "tool/result") {
|
|
220
|
+
const message = data?.message as Record<string, unknown> | undefined;
|
|
221
|
+
const result = toolResultOf(message);
|
|
222
|
+
if (!result.callId || !result.content.length) continue;
|
|
223
|
+
entries.push({ type: "message", message: { role: "toolResult", toolCallId: result.callId, content: result.content, isError: result.isError, timestamp: at } });
|
|
224
|
+
continue;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
if (type === "session/title") {
|
|
228
|
+
title = cleanTitle(data?.title ?? data?.name) ?? title;
|
|
229
|
+
continue;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
if (type === "request/header") {
|
|
233
|
+
const config = configOf(data);
|
|
234
|
+
provider = config.provider ?? provider;
|
|
235
|
+
model = config.model ?? model;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
if (!entries.some((entry) => entry.type === "message")) return null;
|
|
240
|
+
const skipped: string[] = [];
|
|
241
|
+
if (injected) skipped.push(`跳过 ${injected} 条运行时注入消息(沙箱策略 / AGENTS.md 快照 / 压缩摘要)`);
|
|
242
|
+
return {
|
|
243
|
+
summary: { ...summary, title, provider, model, projectPath: header.cwd ?? summary.projectPath },
|
|
244
|
+
entries,
|
|
245
|
+
skipped,
|
|
246
|
+
};
|
|
247
|
+
},
|
|
248
|
+
};
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/** 文件类来源的公共读取:扫描时只读文件头(避免为了列表把几百 MB 全读进来),导入时才整读 */
|
|
2
|
+
import fs from "node:fs/promises";
|
|
3
|
+
|
|
4
|
+
/** 读文件开头若干字节(不切 UTF-8 字符:按行切分后只取完整行) */
|
|
5
|
+
export async function readHead(file: string, bytes = 64 * 1024): Promise<string> {
|
|
6
|
+
const handle = await fs.open(file, "r");
|
|
7
|
+
try {
|
|
8
|
+
const buffer = Buffer.alloc(bytes);
|
|
9
|
+
const { bytesRead } = await handle.read(buffer, 0, bytes, 0);
|
|
10
|
+
return buffer.subarray(0, bytesRead).toString("utf8");
|
|
11
|
+
} finally {
|
|
12
|
+
await handle.close();
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** 逐行解析 JSONL;单行坏掉只跳过该行,不放弃整个文件 */
|
|
17
|
+
export function* eachJsonLine(text: string): Generator<Record<string, unknown>> {
|
|
18
|
+
for (const line of text.split("\n")) {
|
|
19
|
+
const trimmed = line.trim();
|
|
20
|
+
if (!trimmed) continue;
|
|
21
|
+
try {
|
|
22
|
+
const parsed = JSON.parse(trimmed);
|
|
23
|
+
if (parsed && typeof parsed === "object") yield parsed as Record<string, unknown>;
|
|
24
|
+
} catch {
|
|
25
|
+
// 单行损坏(写入中断等):跳过
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** 从文件头里取第一批记录(扫描用) */
|
|
31
|
+
export function headRecords(text: string, limit = 400): Record<string, unknown>[] {
|
|
32
|
+
const out: Record<string, unknown>[] = [];
|
|
33
|
+
for (const record of eachJsonLine(text)) {
|
|
34
|
+
out.push(record);
|
|
35
|
+
if (out.length >= limit) break;
|
|
36
|
+
}
|
|
37
|
+
return out;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** 递归列文件,带深度上限与名字过滤 */
|
|
41
|
+
export async function listFiles(root: string, options: { match: (name: string) => boolean; maxDepth?: number }): Promise<string[]> {
|
|
42
|
+
const out: string[] = [];
|
|
43
|
+
const maxDepth = options.maxDepth ?? 4;
|
|
44
|
+
async function walk(dir: string, depth: number) {
|
|
45
|
+
let entries;
|
|
46
|
+
try {
|
|
47
|
+
entries = await fs.readdir(dir, { withFileTypes: true });
|
|
48
|
+
} catch {
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
for (const entry of entries) {
|
|
52
|
+
const full = `${dir}${dir.endsWith("/") || dir.endsWith("\\") ? "" : "/"}${entry.name}`;
|
|
53
|
+
if (entry.isDirectory()) {
|
|
54
|
+
if (depth < maxDepth) await walk(full, depth + 1);
|
|
55
|
+
} else if (entry.isFile() && options.match(entry.name)) {
|
|
56
|
+
out.push(full);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
await walk(root, 1);
|
|
61
|
+
return out;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export async function statMtime(file: string): Promise<number | undefined> {
|
|
65
|
+
try {
|
|
66
|
+
return (await fs.stat(file)).mtimeMs;
|
|
67
|
+
} catch {
|
|
68
|
+
return undefined;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* 按最近活动倒序:扫描只列最近若干条,所以先按 mtime 排序(只 stat、不读内容),
|
|
74
|
+
* 再从头读——读到够了就停,剩下的文件根本不打开。
|
|
75
|
+
*/
|
|
76
|
+
export async function sortByRecency(files: string[]): Promise<Array<{ file: string; mtime?: number }>> {
|
|
77
|
+
const rows = await Promise.all(files.map(async (file) => ({ file, mtime: await statMtime(file) })));
|
|
78
|
+
return rows.sort((left, right) => (right.mtime ?? 0) - (left.mtime ?? 0));
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** 到达上限就别再往下读了(limit <= 0 表示不限) */
|
|
82
|
+
export function reachedLimit(count: number, limit: number): boolean {
|
|
83
|
+
return limit > 0 && count >= limit;
|
|
84
|
+
}
|