ework-web 0.10.85 → 0.10.86
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/package.json +1 -1
- package/src/index.ts +9 -0
- package/src/pi-sessions.ts +85 -0
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { buildPiSessionPage } from "./pi-sessions";
|
|
1
2
|
import { join, dirname } from "path";
|
|
2
3
|
import { fileURLToPath } from "url";
|
|
3
4
|
import { readFileSync, appendFileSync, existsSync } from "fs";
|
|
@@ -724,6 +725,14 @@ async function handle(req: Request, url: URL, ip: string, ctx: { authed: boolean
|
|
|
724
725
|
if (uidInfo) {
|
|
725
726
|
return html(errorPage("等待启动", "会话已创建,后端会话 ID 尚未生成(正在准备工作目录)。稍后刷新此页。"), 200);
|
|
726
727
|
}
|
|
728
|
+
const piLimit = Math.min(5000, Math.max(10, Number(url.searchParams.get("limit")) || 200));
|
|
729
|
+
const piPage = buildPiSessionPage(rawSid, piLimit);
|
|
730
|
+
if (piPage) return html(piPage, 200);
|
|
731
|
+
} else {
|
|
732
|
+
// non-ses_ id that resolved to nothing: try the pi session file before 404
|
|
733
|
+
const piLimit = Math.min(5000, Math.max(10, Number(url.searchParams.get("limit")) || 200));
|
|
734
|
+
const piPage = buildPiSessionPage(rawSid, piLimit);
|
|
735
|
+
if (piPage) return html(piPage, 200);
|
|
727
736
|
}
|
|
728
737
|
const desc = url.searchParams.get("asc") !== "1";
|
|
729
738
|
const all = url.searchParams.get("all") === "1";
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { readdirSync, readFileSync, existsSync } from "fs";
|
|
2
|
+
import { join } from "path";
|
|
3
|
+
import { homedir } from "os";
|
|
4
|
+
import { THEME_CSS, escapeHtml } from "./render/layout";
|
|
5
|
+
|
|
6
|
+
interface PiEvent {
|
|
7
|
+
type?: string;
|
|
8
|
+
timestamp?: string;
|
|
9
|
+
message?: {
|
|
10
|
+
role?: string;
|
|
11
|
+
content?: Array<{ type?: string; text?: string; thinking?: string; name?: string }>;
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function findPiSessionFile(sessionId: string): string | null {
|
|
16
|
+
if (!/^[0-9a-f-]{36}$/i.test(sessionId)) return null;
|
|
17
|
+
const root = process.env.PI_CODING_AGENT_SESSION_DIR ?? join(homedir(), ".pi/agent/sessions");
|
|
18
|
+
if (!existsSync(root)) return null;
|
|
19
|
+
for (const dir of readdirSync(root, { withFileTypes: true })) {
|
|
20
|
+
if (!dir.isDirectory()) continue;
|
|
21
|
+
const full = join(root, dir.name);
|
|
22
|
+
try {
|
|
23
|
+
const hit = readdirSync(full).find((f) => f.endsWith(`_${sessionId}.jsonl`));
|
|
24
|
+
if (hit) return join(full, hit);
|
|
25
|
+
} catch {
|
|
26
|
+
// unreadable project dir — skip
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function buildPiSessionPage(sessionId: string, limit = 200): string | null {
|
|
33
|
+
const file = findPiSessionFile(sessionId);
|
|
34
|
+
if (!file) return null;
|
|
35
|
+
|
|
36
|
+
const entries: { ts: string; role: string; text: string }[] = [];
|
|
37
|
+
const raw = readFileSync(file, "utf8");
|
|
38
|
+
for (const line of raw.split("\n")) {
|
|
39
|
+
if (!line.trim()) continue;
|
|
40
|
+
let ev: PiEvent;
|
|
41
|
+
try {
|
|
42
|
+
ev = JSON.parse(line) as PiEvent;
|
|
43
|
+
} catch {
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
if (ev.type !== "message" || !ev.message?.role) continue;
|
|
47
|
+
const role = ev.message.role;
|
|
48
|
+
const parts = ev.message.content ?? [];
|
|
49
|
+
let text = "";
|
|
50
|
+
for (const p of parts) {
|
|
51
|
+
if (p.type === "text" && p.text) text += p.text + "\n";
|
|
52
|
+
else if (p.type === "toolCall") text += `→ [tool] ${p.name ?? "?"}\n`;
|
|
53
|
+
}
|
|
54
|
+
if (role === "toolResult") text = text || "[tool output]\n";
|
|
55
|
+
if (!text.trim()) continue;
|
|
56
|
+
entries.push({ ts: (ev.timestamp ?? "").slice(11, 19), role, text: text.trim() });
|
|
57
|
+
}
|
|
58
|
+
const tail = entries.slice(-limit);
|
|
59
|
+
const rows = tail
|
|
60
|
+
.map(
|
|
61
|
+
(e) =>
|
|
62
|
+
`<div class="pi-row pi-${escapeHtml(e.role)}"><span class="pi-ts">${escapeHtml(e.ts)}</span><span class="pi-role">${escapeHtml(e.role)}</span><pre>${escapeHtml(e.text.slice(0, 4000))}</pre></div>`,
|
|
63
|
+
)
|
|
64
|
+
.join("\n");
|
|
65
|
+
const shown = tail.length;
|
|
66
|
+
const total = entries.length;
|
|
67
|
+
return `<!doctype html><html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
|
|
68
|
+
<title>pi session ${escapeHtml(sessionId)}</title>
|
|
69
|
+
<style>${THEME_CSS}
|
|
70
|
+
.pi-wrap{max-width:980px;margin:0 auto;padding:.6rem 1rem 3rem}
|
|
71
|
+
.pi-row{border:1px solid var(--border);border-radius:8px;padding:.5rem .7rem;margin:.45rem 0;background:var(--bg-elev)}
|
|
72
|
+
.pi-row pre{margin:.3rem 0 0;white-space:pre-wrap;word-break:break-word;font:12px/1.5 ui-monospace,monospace}
|
|
73
|
+
.pi-ts{color:var(--text-muted);font-size:11px;margin-right:.6rem}
|
|
74
|
+
.pi-role{font-size:11px;font-weight:600;color:var(--accent)}
|
|
75
|
+
.pi-assistant{border-left:3px solid var(--accent)}
|
|
76
|
+
.pi-user{border-left:3px solid var(--green)}
|
|
77
|
+
.pi-toolResult{border-left:3px solid var(--border);opacity:.85}
|
|
78
|
+
</style></head><body>
|
|
79
|
+
<header class="topbar"><a href="/" title="ework 主页" style="color:var(--header-text)">🏠</a></header>
|
|
80
|
+
<div class="pi-wrap">
|
|
81
|
+
<h2>pi 会话 <code>${escapeHtml(sessionId)}</code></h2>
|
|
82
|
+
<p style="color:var(--text-muted);font-size:12px">显示最近 ${shown} / 共 ${total} 条消息事件 · <a href="?limit=2000">加载更多</a></p>
|
|
83
|
+
${rows || '<p style="color:var(--text-muted)">(会话文件为空)</p>'}
|
|
84
|
+
</div></body></html>`;
|
|
85
|
+
}
|