@rexvane/piweb 0.3.12 → 0.3.14
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 -7
- package/.next/build-manifest.json +3 -3
- package/.next/prerender-manifest.json +3 -3
- package/.next/react-loadable-manifest.json +2 -2
- package/.next/routes-manifest.json +6 -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 -1
- package/.next/server/app/api/prompts/route.js.nft.json +1 -1
- 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 -7
- 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/LOFl9bqqMV2goBV8erESO/_buildManifest.js +1 -0
- package/.next/static/chunks/4632.112d2e893d48d188.js +1 -0
- package/.next/static/chunks/4686-7d694def920ff093.js +1 -0
- package/.next/static/chunks/app/_global-error/page-f77e9ede9808b18e.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/page-9a5440047c5a415c.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/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-05a9e85529a18b93.js → webpack-d28e132e1b8c100c.js} +1 -1
- package/.next/types/app/api/sessions/import/route.ts +353 -0
- package/.next/types/routes.d.ts +2 -1
- package/.next/types/validator.ts +9 -0
- package/README.md +6 -2
- package/package.json +1 -1
- package/src/app/api/sessions/import/route.ts +74 -0
- package/src/components/PromptPanel.tsx +33 -31
- package/src/components/SessionImportSection.tsx +370 -0
- package/src/components/SettingsPanel.tsx +4 -1
- package/src/i18n.tsx +68 -6
- package/src/lib/prompt-sources.ts +29 -1
- package/src/lib/session-import/claude.ts +229 -0
- package/src/lib/session-import/codex.ts +223 -0
- package/src/lib/session-import/dsh-zstd.ts +69 -0
- package/src/lib/session-import/dsh.ts +259 -0
- package/src/lib/session-import/files.ts +84 -0
- package/src/lib/session-import/grok.ts +169 -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/skills-service.ts +51 -14
- package/.next/server/chunks/118.js +0 -1
- package/.next/static/2kt2UiWjv_viXpNlfjvBT/_buildManifest.js +0 -1
- package/.next/static/chunks/4127.dff5c3b33c6bfd1b.js +0 -1
- package/.next/static/chunks/4686-5bbee099126af797.js +0 -1
- package/.next/static/chunks/app/_global-error/page-961c055baebb3872.js +0 -1
- package/.next/static/chunks/app/api/agent/[id]/events/route-961c055baebb3872.js +0 -1
- package/.next/static/chunks/app/api/agent/[id]/route-961c055baebb3872.js +0 -1
- package/.next/static/chunks/app/api/agent/[id]/tree/route-961c055baebb3872.js +0 -1
- package/.next/static/chunks/app/api/agent/new/route-961c055baebb3872.js +0 -1
- package/.next/static/chunks/app/api/config/route-961c055baebb3872.js +0 -1
- package/.next/static/chunks/app/api/files/route-961c055baebb3872.js +0 -1
- package/.next/static/chunks/app/api/git/route-961c055baebb3872.js +0 -1
- package/.next/static/chunks/app/api/growth/route-961c055baebb3872.js +0 -1
- package/.next/static/chunks/app/api/health/route-961c055baebb3872.js +0 -1
- package/.next/static/chunks/app/api/models/route-961c055baebb3872.js +0 -1
- package/.next/static/chunks/app/api/pi-settings/route-961c055baebb3872.js +0 -1
- package/.next/static/chunks/app/api/plugins/route-961c055baebb3872.js +0 -1
- package/.next/static/chunks/app/api/prompts/route-961c055baebb3872.js +0 -1
- package/.next/static/chunks/app/api/security/route-961c055baebb3872.js +0 -1
- package/.next/static/chunks/app/api/sessions/[id]/export/route-961c055baebb3872.js +0 -1
- package/.next/static/chunks/app/api/sessions/[id]/route-961c055baebb3872.js +0 -1
- package/.next/static/chunks/app/api/sessions/route-961c055baebb3872.js +0 -1
- package/.next/static/chunks/app/api/skills/route-961c055baebb3872.js +0 -1
- package/.next/static/chunks/app/api/update/route-961c055baebb3872.js +0 -1
- package/.next/static/chunks/app/api/version/route-961c055baebb3872.js +0 -1
- package/.next/static/chunks/app/api/web-auth/route-961c055baebb3872.js +0 -1
- package/.next/static/chunks/app/api/workspaces/route-961c055baebb3872.js +0 -1
- package/.next/static/chunks/app/page-00c8ebf40ec5eaca.js +0 -1
- package/.next/static/chunks/next/dist/client/components/builtin/app-error-961c055baebb3872.js +0 -1
- package/.next/static/chunks/next/dist/client/components/builtin/forbidden-961c055baebb3872.js +0 -1
- package/.next/static/chunks/next/dist/client/components/builtin/not-found-961c055baebb3872.js +0 -1
- package/.next/static/chunks/next/dist/client/components/builtin/unauthorized-961c055baebb3872.js +0 -1
- /package/.next/static/{2kt2UiWjv_viXpNlfjvBT → LOFl9bqqMV2goBV8erESO}/_ssgManifest.js +0 -0
|
@@ -0,0 +1,259 @@
|
|
|
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
|
+
* 主代理为 0;> 0 表示这条会话是主代理派给子代理的活(本机 198 个会话里有 55 个)。
|
|
41
|
+
* 那是子任务,不该混进"我的对话"列表里。
|
|
42
|
+
*/
|
|
43
|
+
delegationDepth?: number;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function parseRecords(text: string): { header: DshHeader; records: Record<string, unknown>[] } {
|
|
47
|
+
const records: Record<string, unknown>[] = [];
|
|
48
|
+
let header: DshHeader = {};
|
|
49
|
+
for (const line of text.split("\n")) {
|
|
50
|
+
const trimmed = line.trim();
|
|
51
|
+
if (!trimmed) continue;
|
|
52
|
+
let parsed: Record<string, unknown>;
|
|
53
|
+
try {
|
|
54
|
+
parsed = JSON.parse(trimmed) as Record<string, unknown>;
|
|
55
|
+
} catch {
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
if (parsed.type === "session") {
|
|
59
|
+
header = {
|
|
60
|
+
id: typeof parsed.id === "string" ? parsed.id : undefined,
|
|
61
|
+
cwd: typeof parsed.cwd === "string" ? parsed.cwd : undefined,
|
|
62
|
+
createdAt: toEpochMs(parsed.createdAt),
|
|
63
|
+
delegationDepth: typeof parsed.delegationDepth === "number" ? parsed.delegationDepth : undefined,
|
|
64
|
+
};
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
records.push(parsed);
|
|
68
|
+
}
|
|
69
|
+
return { header, records };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** request/header 里带 provider+model;取最后一次生效的作为会话的模型 */
|
|
73
|
+
function configOf(data: Record<string, unknown> | undefined): { provider?: string; model?: string } {
|
|
74
|
+
const config = (data?.header as Record<string, unknown> | undefined)?.config as Record<string, unknown> | undefined;
|
|
75
|
+
return {
|
|
76
|
+
provider: typeof config?.provider === "string" ? config.provider : undefined,
|
|
77
|
+
model: typeof config?.model === "string" ? config.model : undefined,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** assistant/message 的 content 块:reasoning→thinking、text→text、tool-call→toolCall */
|
|
82
|
+
function assistantBlocks(content: unknown): ImportedBlock[] {
|
|
83
|
+
if (!Array.isArray(content)) return [];
|
|
84
|
+
return compactBlocks(content.flatMap((raw): Array<ImportedBlock | null> => {
|
|
85
|
+
const block = raw as Record<string, unknown>;
|
|
86
|
+
if (block?.type === "reasoning" || block?.type === "thinking") {
|
|
87
|
+
return typeof block.text === "string" && block.text.trim() ? [{ type: "thinking", thinking: block.text }] : [];
|
|
88
|
+
}
|
|
89
|
+
if (block?.type === "text") return [textBlock(block.text)];
|
|
90
|
+
if (block?.type === "tool-call") {
|
|
91
|
+
const id = typeof block.id === "string" ? block.id : "";
|
|
92
|
+
const name = typeof block.name === "string" ? block.name : "";
|
|
93
|
+
return id && name ? [{ type: "toolCall", id, name, arguments: parseArguments(block.arguments) }] : [];
|
|
94
|
+
}
|
|
95
|
+
return [];
|
|
96
|
+
}));
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** 用户消息块:只认 text */
|
|
100
|
+
function userBlocks(content: unknown): ImportedBlock[] {
|
|
101
|
+
if (!Array.isArray(content)) return [];
|
|
102
|
+
return compactBlocks(content.flatMap((raw): Array<ImportedBlock | null> => {
|
|
103
|
+
const block = raw as Record<string, unknown>;
|
|
104
|
+
return block?.type === "text" ? [textBlock(block.text)] : [];
|
|
105
|
+
}));
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* 一个会话目录里可能同时存在 `session.jsonl.zstd` 与 `session.v3.jsonl.zstd`(迁移产物,
|
|
110
|
+
* 本机有 6 个这样的目录)。两者是同一个会话、externalId 相同,都列出来就会重复,
|
|
111
|
+
* 导入时还会被判成"重复选择"。选 v3:它是迁移后的规范格式,去掉了流式分片,
|
|
112
|
+
* 实测消息条数反而更多(44/171/241/256 → 46/179/247/262),也确认了 v3 才是最新的完整历史。
|
|
113
|
+
*/
|
|
114
|
+
function pickPerSession(files: string[]): string[] {
|
|
115
|
+
const best = new Map<string, string>();
|
|
116
|
+
for (const file of files) {
|
|
117
|
+
const dir = path.dirname(file);
|
|
118
|
+
const previous = best.get(dir);
|
|
119
|
+
if (!previous || (file.includes(".v3.") && !previous.includes(".v3."))) best.set(dir, file);
|
|
120
|
+
}
|
|
121
|
+
return [...best.values()];
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** tool/result 的输出深一层:{type:"tool-result", toolCallId, content:[{type:"text"}], isError} */
|
|
125
|
+
function toolResultOf(message: Record<string, unknown> | undefined): { callId: string; content: ImportedBlock[]; isError: boolean } {
|
|
126
|
+
const blocks = Array.isArray(message?.content) ? (message?.content as Record<string, unknown>[]) : [];
|
|
127
|
+
const node = (blocks.find((block) => block?.type === "tool-result") ?? blocks[0]) as Record<string, unknown> | undefined;
|
|
128
|
+
const source = message?.source as Record<string, unknown> | undefined;
|
|
129
|
+
const callId = typeof node?.toolCallId === "string" ? node.toolCallId : typeof source?.callId === "string" ? source.callId : "";
|
|
130
|
+
return { callId, content: outputBlocks(node?.content ?? node), isError: node?.isError === true };
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export const dshSource: ImportSourceModule = {
|
|
134
|
+
id: "dsh",
|
|
135
|
+
|
|
136
|
+
async scan(options?: ScanOptions): Promise<ExternalSessionSummary[]> {
|
|
137
|
+
const limit = options?.limit ?? 0;
|
|
138
|
+
const files = await listFiles(ROOT(), { match: (name) => name.endsWith(".jsonl.zstd") });
|
|
139
|
+
const candidates = await sortByRecency(pickPerSession(files));
|
|
140
|
+
const out: ExternalSessionSummary[] = [];
|
|
141
|
+
for (const { file, mtime } of candidates) {
|
|
142
|
+
if (reachedLimit(out.length, limit)) break; // 只解压最近的若干条:其余的连 zstd 都不碰
|
|
143
|
+
let head: { text: string; complete: boolean };
|
|
144
|
+
try {
|
|
145
|
+
head = await readZstdHead(file);
|
|
146
|
+
} catch {
|
|
147
|
+
continue;
|
|
148
|
+
}
|
|
149
|
+
const { header, records } = parseRecords(head.text);
|
|
150
|
+
if (!header.id) continue;
|
|
151
|
+
if (header.delegationDepth) continue; // 主代理派给子代理的会话,不算"我的对话"
|
|
152
|
+
// 建了却从没用过的会话(只有 header 与几条配置):整个文件都解出来却没有消息,不该列进列表
|
|
153
|
+
if (head.complete && !records.some((record) => record.type === "user/message" || record.type === "assistant/message")) continue;
|
|
154
|
+
let title: string | undefined;
|
|
155
|
+
let provider: string | undefined;
|
|
156
|
+
let model: string | undefined;
|
|
157
|
+
for (const record of records) {
|
|
158
|
+
const data = record.data as Record<string, unknown> | undefined;
|
|
159
|
+
if (record.type === "session/title") {
|
|
160
|
+
title = cleanTitle(data?.title ?? data?.name) ?? title;
|
|
161
|
+
}
|
|
162
|
+
if (record.type === "request/header") {
|
|
163
|
+
const config = configOf(data);
|
|
164
|
+
provider = config.provider ?? provider;
|
|
165
|
+
model = config.model ?? model;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
const sessionDir = path.basename(path.dirname(file));
|
|
169
|
+
out.push({
|
|
170
|
+
source: "dsh",
|
|
171
|
+
externalId: sessionDir || header.id,
|
|
172
|
+
title,
|
|
173
|
+
projectPath: header.cwd ?? decodeCwdDir(path.basename(path.dirname(path.dirname(file)))),
|
|
174
|
+
provider,
|
|
175
|
+
model,
|
|
176
|
+
createdAt: header.createdAt ?? mtime,
|
|
177
|
+
updatedAt: mtime,
|
|
178
|
+
// 扫描只解前若干帧,统计整会话条数不可靠:留空由界面显示 —
|
|
179
|
+
location: file,
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
return out;
|
|
183
|
+
},
|
|
184
|
+
|
|
185
|
+
async read(summary: ExternalSessionSummary): Promise<ImportedSession | null> {
|
|
186
|
+
let text: string;
|
|
187
|
+
try {
|
|
188
|
+
text = await readZstdJsonl(summary.location);
|
|
189
|
+
} catch {
|
|
190
|
+
return null;
|
|
191
|
+
}
|
|
192
|
+
const { header, records } = parseRecords(text);
|
|
193
|
+
const entries: ImportedEntry[] = [];
|
|
194
|
+
let title = summary.title;
|
|
195
|
+
let injected = 0;
|
|
196
|
+
let provider = summary.provider;
|
|
197
|
+
let model = summary.model;
|
|
198
|
+
let lastAt = header.createdAt ?? summary.createdAt ?? Date.now();
|
|
199
|
+
|
|
200
|
+
for (const record of records) {
|
|
201
|
+
const type = typeof record.type === "string" ? record.type : "";
|
|
202
|
+
const at = toEpochMs(record.time) ?? lastAt;
|
|
203
|
+
lastAt = at;
|
|
204
|
+
const data = record.data as Record<string, unknown> | undefined;
|
|
205
|
+
|
|
206
|
+
if (type === "user/message") {
|
|
207
|
+
const source = data?.source as Record<string, unknown> | undefined;
|
|
208
|
+
if (source?.kind !== "user") {
|
|
209
|
+
injected += 1; // 插件注入 / AGENTS.md 快照 / 压缩摘要:pi 端会注入自己的版本
|
|
210
|
+
continue;
|
|
211
|
+
}
|
|
212
|
+
const content = userBlocks(data?.content);
|
|
213
|
+
if (!content.length) continue;
|
|
214
|
+
entries.push({ type: "message", message: { role: "user", content, timestamp: at } });
|
|
215
|
+
continue;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
if (type === "assistant/message") {
|
|
219
|
+
const message = data?.message as Record<string, unknown> | undefined;
|
|
220
|
+
const content = assistantBlocks(message?.content);
|
|
221
|
+
if (!content.length) continue;
|
|
222
|
+
const hasCall = content.some((block) => block.type === "toolCall");
|
|
223
|
+
entries.push({ type: "message", message: { role: "assistant", content, timestamp: at, stopReason: hasCall ? "toolUse" : "stop" } });
|
|
224
|
+
continue;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// tool/call 行与 assistant/message 里的 tool-call 块是同一批调用,重复导入会让调用翻倍
|
|
228
|
+
if (type === "tool/call") continue;
|
|
229
|
+
|
|
230
|
+
if (type === "tool/result") {
|
|
231
|
+
const message = data?.message as Record<string, unknown> | undefined;
|
|
232
|
+
const result = toolResultOf(message);
|
|
233
|
+
if (!result.callId || !result.content.length) continue;
|
|
234
|
+
entries.push({ type: "message", message: { role: "toolResult", toolCallId: result.callId, content: result.content, isError: result.isError, timestamp: at } });
|
|
235
|
+
continue;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
if (type === "session/title") {
|
|
239
|
+
title = cleanTitle(data?.title ?? data?.name) ?? title;
|
|
240
|
+
continue;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
if (type === "request/header") {
|
|
244
|
+
const config = configOf(data);
|
|
245
|
+
provider = config.provider ?? provider;
|
|
246
|
+
model = config.model ?? model;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
if (!entries.some((entry) => entry.type === "message")) return null;
|
|
251
|
+
const skipped: string[] = [];
|
|
252
|
+
if (injected) skipped.push(`跳过 ${injected} 条运行时注入消息(沙箱策略 / AGENTS.md 快照 / 压缩摘要)`);
|
|
253
|
+
return {
|
|
254
|
+
summary: { ...summary, title, provider, model, projectPath: header.cwd ?? summary.projectPath },
|
|
255
|
+
entries,
|
|
256
|
+
skipped,
|
|
257
|
+
};
|
|
258
|
+
},
|
|
259
|
+
};
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Grok CLI(~/.grok/sessions/<URL 编码的 cwd>/<session-uuid>/)
|
|
3
|
+
*
|
|
4
|
+
* 会话目录里有 chat_history.jsonl(正文)与 summary.json(标题/模型/时间/消息数)。
|
|
5
|
+
* chat_history 每行 `{type, content, ...}`:system / user / assistant(含 tool_calls[])/ reasoning / tool_result。
|
|
6
|
+
*/
|
|
7
|
+
import path from "node:path";
|
|
8
|
+
import fs from "node:fs/promises";
|
|
9
|
+
import { eachJsonLine, listFiles, readHead, reachedLimit, sortByRecency } from "./files";
|
|
10
|
+
import { grokRoot } from "./paths";
|
|
11
|
+
import type { ExternalSessionSummary, ImportedBlock, ImportedEntry, ImportedSession, ImportSourceModule, ScanOptions } from "./types";
|
|
12
|
+
import { cleanTitle, compactBlocks, outputBlocks, parseArguments, textBlock, toEpochMs } from "./types";
|
|
13
|
+
|
|
14
|
+
const ROOT = grokRoot;
|
|
15
|
+
|
|
16
|
+
/** 目录名是 URL 编码的 cwd(D%3A%5CAIApp%5CPiWeb),summary.json 缺失时用它兜底 */
|
|
17
|
+
function decodeProjectDir(name: string): string | undefined {
|
|
18
|
+
try {
|
|
19
|
+
const decoded = decodeURIComponent(name);
|
|
20
|
+
return /^[A-Za-z]:[\\/]/.test(decoded) || decoded.startsWith("/") ? decoded : undefined;
|
|
21
|
+
} catch {
|
|
22
|
+
return undefined;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* 判断一段 chat_history 里是否有真正能导入的对话:至少一条 assistant,或一条不受合成标记影响的用户消息。
|
|
28
|
+
* 与 read() 的接受口径一致——只列"read 一定能读出来"的会话。
|
|
29
|
+
*/
|
|
30
|
+
function importable(records: Record<string, unknown>[]): boolean {
|
|
31
|
+
for (const record of records) {
|
|
32
|
+
if (record.type === "assistant" || record.type === "tool_result") return true;
|
|
33
|
+
if (record.type === "reasoning" && typeof record.summary === "string" && record.summary.trim()) return true;
|
|
34
|
+
if (record.type !== "user" || record.synthetic_reason) continue;
|
|
35
|
+
const blocks = userBlocks(record.content);
|
|
36
|
+
if (blocks.some((block) => block.type === "text" && block.text.replace(/<[^>]+>/g, "").trim())) return true;
|
|
37
|
+
}
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async function hasRealMessage(history: string): Promise<boolean> {
|
|
42
|
+
try {
|
|
43
|
+
return importable([...eachJsonLine(await readHead(history))]);
|
|
44
|
+
} catch {
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** user 的 content 可能是字符串或块数组 */function userBlocks(content: unknown): ImportedBlock[] {
|
|
50
|
+
if (typeof content === "string") return compactBlocks([textBlock(content)]);
|
|
51
|
+
if (!Array.isArray(content)) return [];
|
|
52
|
+
return compactBlocks(content.flatMap((raw): Array<ImportedBlock | null> => {
|
|
53
|
+
if (typeof raw === "string") return [textBlock(raw)];
|
|
54
|
+
const block = raw as Record<string, unknown>;
|
|
55
|
+
if (typeof block?.text === "string") return [textBlock(block.text)];
|
|
56
|
+
return [];
|
|
57
|
+
}));
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export const grokSource: ImportSourceModule = {
|
|
61
|
+
id: "grok",
|
|
62
|
+
|
|
63
|
+
async scan(options?: ScanOptions): Promise<ExternalSessionSummary[]> {
|
|
64
|
+
// 先按 chat_history 的 mtime 排序(只 stat,不读内容),再从最近往下读:
|
|
65
|
+
// summary.json 里有标题/模型/时间/消息数,读它即可,正文只在确认有真实对话时碰一下头部
|
|
66
|
+
const limit = options?.limit ?? 0;
|
|
67
|
+
const files = await listFiles(ROOT(), { match: (name) => name === "summary.json" });
|
|
68
|
+
const candidates = await sortByRecency(files.map((file) => path.join(path.dirname(file), "chat_history.jsonl")));
|
|
69
|
+
const out: ExternalSessionSummary[] = [];
|
|
70
|
+
for (const { file: history, mtime } of candidates) {
|
|
71
|
+
if (reachedLimit(out.length, limit)) break; // 只读最近的若干条
|
|
72
|
+
const dir = path.dirname(history);
|
|
73
|
+
let summary: Record<string, unknown>;
|
|
74
|
+
try {
|
|
75
|
+
summary = JSON.parse(await fs.readFile(path.join(dir, "summary.json"), "utf8")) as Record<string, unknown>;
|
|
76
|
+
} catch {
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
if (mtime === undefined) continue; // 没有正文就没什么可导
|
|
80
|
+
const info = summary.info as Record<string, unknown> | undefined;
|
|
81
|
+
const numMessages = Number(summary.num_messages ?? summary.num_chat_messages);
|
|
82
|
+
// 子代理会话(grok 的 spawn_subagent:subagent / subagent_fork / subagent_resume,本机 24 条)
|
|
83
|
+
// 以及它们的派生:agent_name 是 general-purpose / explore 之类,不是你在用的那条对话
|
|
84
|
+
if (typeof summary.session_kind === "string" && summary.session_kind.startsWith("subagent")) continue;
|
|
85
|
+
if (numMessages === 0) continue; // 开了没用的空会话
|
|
86
|
+
// 只有系统提示与合成提醒(system_reminder)的会话读不出对话,别列进列表
|
|
87
|
+
if (!(await hasRealMessage(history))) continue;
|
|
88
|
+
out.push({
|
|
89
|
+
source: "grok",
|
|
90
|
+
externalId: path.basename(dir),
|
|
91
|
+
title: cleanTitle(summary.session_summary ?? summary.generated_title),
|
|
92
|
+
projectPath: typeof info?.cwd === "string" ? info.cwd : decodeProjectDir(path.basename(path.dirname(dir))),
|
|
93
|
+
model: typeof summary.current_model_id === "string" ? summary.current_model_id : undefined,
|
|
94
|
+
createdAt: toEpochMs(summary.created_at) ?? mtime,
|
|
95
|
+
updatedAt: toEpochMs(summary.updated_at ?? summary.last_active_at) ?? mtime,
|
|
96
|
+
...(Number.isFinite(numMessages) ? { messageCount: numMessages } : {}),
|
|
97
|
+
location: history,
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
return out;
|
|
101
|
+
},
|
|
102
|
+
|
|
103
|
+
async read(summary: ExternalSessionSummary): Promise<ImportedSession | null> {
|
|
104
|
+
let text: string;
|
|
105
|
+
try {
|
|
106
|
+
text = await fs.readFile(summary.location, "utf8");
|
|
107
|
+
} catch {
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
const entries: ImportedEntry[] = [];
|
|
111
|
+
const skipped: string[] = [];
|
|
112
|
+
let encryptedReasoning = 0;
|
|
113
|
+
let synthetic = 0;
|
|
114
|
+
let model = summary.model;
|
|
115
|
+
|
|
116
|
+
for (const record of eachJsonLine(text)) {
|
|
117
|
+
const timestamp = toEpochMs(record.timestamp ?? record.time ?? record.created_at) ?? summary.createdAt ?? Date.now();
|
|
118
|
+
switch (record.type) {
|
|
119
|
+
case "system":
|
|
120
|
+
continue; // 系统提示不进对话
|
|
121
|
+
case "user": {
|
|
122
|
+
const blocks = userBlocks(record.content);
|
|
123
|
+
const plain = blocks.map((block) => (block.type === "text" ? block.text : "")).join("\n").trim();
|
|
124
|
+
if (record.synthetic_reason || !plain) {
|
|
125
|
+
synthetic += 1;
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
entries.push({ type: "message", message: { role: "user", content: blocks, timestamp } });
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
case "assistant": {
|
|
132
|
+
if (typeof record.model_id === "string" && record.model_id.trim()) model = record.model_id;
|
|
133
|
+
const blocks: Array<ImportedBlock | null> = [typeof record.content === "string" ? textBlock(record.content) : null];
|
|
134
|
+
for (const call of (record.tool_calls as Array<Record<string, unknown>> | undefined) ?? []) {
|
|
135
|
+
const id = typeof call.id === "string" ? call.id : "";
|
|
136
|
+
const name = typeof call.name === "string" ? call.name : "";
|
|
137
|
+
if (id && name) blocks.push({ type: "toolCall", id, name, arguments: parseArguments(call.arguments ?? {}) });
|
|
138
|
+
}
|
|
139
|
+
const content = compactBlocks(blocks);
|
|
140
|
+
if (!content.length) continue;
|
|
141
|
+
entries.push({
|
|
142
|
+
type: "message",
|
|
143
|
+
message: { role: "assistant", content, timestamp, ...(model ? { model } : {}), ...(content.some((block) => block.type === "toolCall") ? { stopReason: "toolUse" } : {}) },
|
|
144
|
+
});
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
case "reasoning": {
|
|
148
|
+
const text = typeof record.summary === "string" ? record.summary : "";
|
|
149
|
+
if (text.trim()) entries.push({ type: "message", message: { role: "assistant", content: [{ type: "thinking", thinking: text }], timestamp, ...(model ? { model } : {}) } });
|
|
150
|
+
else encryptedReasoning += 1; // 加密推理无正文可导:计数后汇总成一行,不要刷屏
|
|
151
|
+
continue;
|
|
152
|
+
}
|
|
153
|
+
case "tool_result": {
|
|
154
|
+
const callId = typeof record.tool_call_id === "string" ? record.tool_call_id : "";
|
|
155
|
+
if (!callId) continue;
|
|
156
|
+
entries.push({ type: "message", message: { role: "toolResult", toolCallId: callId, content: outputBlocks(record.content), timestamp } });
|
|
157
|
+
continue;
|
|
158
|
+
}
|
|
159
|
+
default:
|
|
160
|
+
continue;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if (synthetic) skipped.push(`跳过 ${synthetic} 条合成输入`);
|
|
165
|
+
if (encryptedReasoning) skipped.push(`跳过 ${encryptedReasoning} 条仅含加密内容的推理记录`);
|
|
166
|
+
if (!entries.some((entry) => entry.type === "message")) return null;
|
|
167
|
+
return { summary: { ...summary, model }, entries, skipped };
|
|
168
|
+
},
|
|
169
|
+
};
|