@keyou007/agent-bot 0.1.24-alpha.4 → 0.1.24-alpha.5
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/CHANGELOG.md +33 -0
- package/README.md +15 -3
- package/README.zh.md +15 -3
- package/dist/codex/AppServerPermissions.js +34 -0
- package/dist/codex/AppServerPermissions.js.map +1 -0
- package/dist/codex/CodexLocalActivityDetector.js +37 -5
- package/dist/codex/CodexLocalActivityDetector.js.map +1 -1
- package/dist/codex/CodexRuntime.js +37 -13
- package/dist/codex/CodexRuntime.js.map +1 -1
- package/dist/codex/ThreadWriterProcess.js +19 -5
- package/dist/codex/ThreadWriterProcess.js.map +1 -1
- package/dist/feishu/CardRenderer.js +80 -64
- package/dist/feishu/CardRenderer.js.map +1 -1
- package/dist/local-files/CsvPreview.js +148 -0
- package/dist/local-files/CsvPreview.js.map +1 -0
- package/dist/local-files/LocalFileViewerServer.js +53 -10
- package/dist/local-files/LocalFileViewerServer.js.map +1 -1
- package/dist/local-files/TurnPreviewPage.js +122 -11
- package/dist/local-files/TurnPreviewPage.js.map +1 -1
- package/dist/presentation/TurnStateReducer.js +39 -0
- package/dist/presentation/TurnStateReducer.js.map +1 -1
- package/dist/proxy/ProxySessionController.js +152 -46
- package/dist/proxy/ProxySessionController.js.map +1 -1
- package/docs/technical-reference.md +5 -3
- package/docs/technical-reference.zh.md +4 -2
- package/npm-shrinkwrap.json +2 -2
- package/package.json +1 -1
- package/skills/agent-bot/SKILL.md +6 -0
- package/src/codex/AppServerPermissions.ts +39 -0
- package/src/codex/CodexLocalActivityDetector.ts +34 -4
- package/src/codex/CodexRuntime.ts +44 -17
- package/src/codex/ThreadWriterProcess.ts +17 -9
- package/src/feishu/CardRenderer.ts +78 -64
- package/src/local-files/CsvPreview.ts +132 -0
- package/src/local-files/LocalFileViewerServer.ts +54 -11
- package/src/local-files/TurnPreviewPage.ts +125 -12
- package/src/presentation/TurnStateReducer.ts +41 -0
- package/src/presentation/turnViewTypes.ts +4 -0
- package/src/proxy/ProxySessionController.ts +137 -48
- package/src/runtime/types.ts +2 -0
|
@@ -4,6 +4,7 @@ import type { JsonValue } from "../acp/acpTypes.js";
|
|
|
4
4
|
import type { RuntimeSession } from "../acp/AcpSessionManager.js";
|
|
5
5
|
import type {
|
|
6
6
|
ApprovalDecision,
|
|
7
|
+
ApprovalRequest,
|
|
7
8
|
ModelOption,
|
|
8
9
|
ModelProviderOption,
|
|
9
10
|
PermissionMode,
|
|
@@ -197,6 +198,7 @@ export interface SessionTaskCardEntry {
|
|
|
197
198
|
reference: string;
|
|
198
199
|
summary: string;
|
|
199
200
|
detailLines: string[];
|
|
201
|
+
stopAction?: TaskListCardAction;
|
|
200
202
|
actions?: TaskListCardAction[];
|
|
201
203
|
current?: boolean;
|
|
202
204
|
}
|
|
@@ -1698,38 +1700,7 @@ function renderTurnElements(
|
|
|
1698
1700
|
elements.push(...renderActivities(visibleActivities, state.projectCwd));
|
|
1699
1701
|
if (state.fileSummary.length > 0) elements.push(fileSummaryPanel(state));
|
|
1700
1702
|
|
|
1701
|
-
if (state.approval)
|
|
1702
|
-
const request = state.approval;
|
|
1703
|
-
elements.push(markdown([
|
|
1704
|
-
`**${request.title}**`,
|
|
1705
|
-
request.command ? codeBlock(request.command, 800) : undefined,
|
|
1706
|
-
request.reason,
|
|
1707
|
-
].filter(Boolean).join("\n")));
|
|
1708
|
-
elements.push({
|
|
1709
|
-
tag: "column_set",
|
|
1710
|
-
flex_mode: "flow",
|
|
1711
|
-
horizontal_spacing: "8px",
|
|
1712
|
-
columns: request.options.map((option) => ({
|
|
1713
|
-
tag: "column",
|
|
1714
|
-
width: "auto",
|
|
1715
|
-
elements: [{
|
|
1716
|
-
tag: "button",
|
|
1717
|
-
text: { tag: "plain_text", content: request.kind === "mode_change" ? option.label : approvalDecisionLabel(option.id) },
|
|
1718
|
-
type: option.id === "accept" || option.id === "acceptForSession" ? "primary" : option.id === "cancel" ? "danger" : "default",
|
|
1719
|
-
behaviors: [{
|
|
1720
|
-
type: "callback",
|
|
1721
|
-
value: {
|
|
1722
|
-
action: "approval",
|
|
1723
|
-
sessionId: state.sessionId,
|
|
1724
|
-
turnId: state.turnId,
|
|
1725
|
-
requestId: request.id,
|
|
1726
|
-
decision: option.id,
|
|
1727
|
-
},
|
|
1728
|
-
}],
|
|
1729
|
-
}],
|
|
1730
|
-
})),
|
|
1731
|
-
});
|
|
1732
|
-
}
|
|
1703
|
+
if (state.approval) elements.push(...renderApprovalElements(state, state.approval));
|
|
1733
1704
|
if (state.error) elements.push(markdown(codeBlock(state.error, 2_000)));
|
|
1734
1705
|
const showAssistantText = state.assistantText && assistantTextMode === "always";
|
|
1735
1706
|
if (showAssistantText) {
|
|
@@ -1769,38 +1740,7 @@ function renderGroupedTurnElements(
|
|
|
1769
1740
|
elements.push(...renderGroupedActivityGroups(visibleGroups, state.projectCwd));
|
|
1770
1741
|
if (state.fileSummary.length > 0) elements.push(fileSummaryPanel(state));
|
|
1771
1742
|
|
|
1772
|
-
if (state.approval)
|
|
1773
|
-
const request = state.approval;
|
|
1774
|
-
elements.push(markdown([
|
|
1775
|
-
`**${request.title}**`,
|
|
1776
|
-
request.command ? codeBlock(request.command, 800) : undefined,
|
|
1777
|
-
request.reason,
|
|
1778
|
-
].filter(Boolean).join("\n")));
|
|
1779
|
-
elements.push({
|
|
1780
|
-
tag: "column_set",
|
|
1781
|
-
flex_mode: "flow",
|
|
1782
|
-
horizontal_spacing: "8px",
|
|
1783
|
-
columns: request.options.map((option) => ({
|
|
1784
|
-
tag: "column",
|
|
1785
|
-
width: "auto",
|
|
1786
|
-
elements: [{
|
|
1787
|
-
tag: "button",
|
|
1788
|
-
text: { tag: "plain_text", content: request.kind === "mode_change" ? option.label : approvalDecisionLabel(option.id) },
|
|
1789
|
-
type: option.id === "accept" || option.id === "acceptForSession" ? "primary" : option.id === "cancel" ? "danger" : "default",
|
|
1790
|
-
behaviors: [{
|
|
1791
|
-
type: "callback",
|
|
1792
|
-
value: {
|
|
1793
|
-
action: "approval",
|
|
1794
|
-
sessionId: state.sessionId,
|
|
1795
|
-
turnId: state.turnId,
|
|
1796
|
-
requestId: request.id,
|
|
1797
|
-
decision: option.id,
|
|
1798
|
-
},
|
|
1799
|
-
}],
|
|
1800
|
-
}],
|
|
1801
|
-
})),
|
|
1802
|
-
});
|
|
1803
|
-
}
|
|
1743
|
+
if (state.approval) elements.push(...renderApprovalElements(state, state.approval));
|
|
1804
1744
|
if (state.error) elements.push(markdown(codeBlock(state.error, 2_000)));
|
|
1805
1745
|
const showAssistantText = state.assistantText && assistantTextMode === "always";
|
|
1806
1746
|
if (showAssistantText) {
|
|
@@ -1812,6 +1752,79 @@ function renderGroupedTurnElements(
|
|
|
1812
1752
|
return elements;
|
|
1813
1753
|
}
|
|
1814
1754
|
|
|
1755
|
+
function renderApprovalElements(state: TurnViewState, request: ApprovalRequest): Record<string, unknown>[] {
|
|
1756
|
+
const elements: Record<string, unknown>[] = [];
|
|
1757
|
+
const command = request.command?.trim();
|
|
1758
|
+
if (command && request.kind !== "mode_change") {
|
|
1759
|
+
elements.push(markdown("**命令执行确认**"));
|
|
1760
|
+
const reason = request.reason?.trim()
|
|
1761
|
+
|| (request.title.trim() !== command ? request.title.trim() : "");
|
|
1762
|
+
if (reason && reason !== command) {
|
|
1763
|
+
const preview = approvalTextPreview(reason, 1_000, escapeApprovalText);
|
|
1764
|
+
elements.push(markdown(escapeApprovalText(preview)));
|
|
1765
|
+
if (preview !== reason) elements.push(markdown("<font color='grey'>说明较长,请在 Preview 中查看完整内容。</font>"));
|
|
1766
|
+
}
|
|
1767
|
+
const display = displayToolCommand(command) ?? command;
|
|
1768
|
+
const summary = truncateText(display.replace(/\s+/gu, " "), 64);
|
|
1769
|
+
const preview = approvalTextPreview(command, 2_200);
|
|
1770
|
+
elements.push(collapsiblePanel(`查看命令 · ${summary}`, [
|
|
1771
|
+
markdown(codeBlock(preview, preview.length)),
|
|
1772
|
+
...(preview !== command ? [markdown("<font color='grey'>命令过长,卡片仅显示开头。请在 Preview 中查看完整命令后再授权。</font>")] : []),
|
|
1773
|
+
], {
|
|
1774
|
+
elementId: `approval_${createHash("sha256").update(request.id).digest("hex").slice(0, 11)}`,
|
|
1775
|
+
compact: true,
|
|
1776
|
+
borderColor: "grey",
|
|
1777
|
+
}));
|
|
1778
|
+
} else {
|
|
1779
|
+
elements.push(markdown([
|
|
1780
|
+
`**${request.title}**`,
|
|
1781
|
+
request.command ? codeBlock(request.command, 800) : undefined,
|
|
1782
|
+
request.reason,
|
|
1783
|
+
].filter(Boolean).join("\n")));
|
|
1784
|
+
}
|
|
1785
|
+
elements.push({
|
|
1786
|
+
tag: "column_set",
|
|
1787
|
+
flex_mode: "flow",
|
|
1788
|
+
horizontal_spacing: "8px",
|
|
1789
|
+
columns: request.options.map((option) => ({
|
|
1790
|
+
tag: "column",
|
|
1791
|
+
width: "auto",
|
|
1792
|
+
elements: [{
|
|
1793
|
+
tag: "button",
|
|
1794
|
+
text: { tag: "plain_text", content: request.kind === "mode_change" ? option.label : approvalDecisionLabel(option.id) },
|
|
1795
|
+
type: option.id === "accept" || option.id === "acceptForSession" ? "primary" : option.id === "cancel" ? "danger" : "default",
|
|
1796
|
+
behaviors: [{
|
|
1797
|
+
type: "callback",
|
|
1798
|
+
value: {
|
|
1799
|
+
action: "approval",
|
|
1800
|
+
sessionId: state.sessionId,
|
|
1801
|
+
turnId: state.turnId,
|
|
1802
|
+
requestId: request.id,
|
|
1803
|
+
decision: option.id,
|
|
1804
|
+
},
|
|
1805
|
+
}],
|
|
1806
|
+
}],
|
|
1807
|
+
})),
|
|
1808
|
+
});
|
|
1809
|
+
return elements;
|
|
1810
|
+
}
|
|
1811
|
+
|
|
1812
|
+
function approvalTextPreview(value: string, maxBytes: number, encode = (text: string): string => text): string {
|
|
1813
|
+
if (Buffer.byteLength(JSON.stringify(encode(value)), "utf8") <= maxBytes) return value;
|
|
1814
|
+
let bytes = 5;
|
|
1815
|
+
let result = "";
|
|
1816
|
+
for (const character of value) {
|
|
1817
|
+
bytes += Buffer.byteLength(JSON.stringify(encode(character)), "utf8") - 2;
|
|
1818
|
+
if (bytes > maxBytes) break;
|
|
1819
|
+
result += character;
|
|
1820
|
+
}
|
|
1821
|
+
return `${result}...`;
|
|
1822
|
+
}
|
|
1823
|
+
|
|
1824
|
+
function escapeApprovalText(value: string): string {
|
|
1825
|
+
return escapeCardHtml(value).replace(/[\\`*_[\]#]/gu, (character) => `&#${character.codePointAt(0)};`);
|
|
1826
|
+
}
|
|
1827
|
+
|
|
1815
1828
|
function emptyTurnText(status: TurnViewStatus, agentLabel?: string): string {
|
|
1816
1829
|
const label = agentLabel?.trim().replace(/[\r\n]+/g, " ") || "Agent";
|
|
1817
1830
|
if (status === "starting") return `正在连接 ${label}…`;
|
|
@@ -2298,6 +2311,7 @@ function collapsiblePanel(
|
|
|
2298
2311
|
|
|
2299
2312
|
function sessionTaskPanel(entry: SessionTaskCardEntry): Record<string, unknown> {
|
|
2300
2313
|
const elements: Record<string, unknown>[] = [markdown(entry.detailLines.join("\n"))];
|
|
2314
|
+
if (entry.stopAction) elements.push(sessionActionButton(entry.stopAction));
|
|
2301
2315
|
if (entry.actions?.length) elements.push(sessionActionOverflow(entry.actions));
|
|
2302
2316
|
return collapsiblePanel(entry.summary, elements, {
|
|
2303
2317
|
elementId: sessionTaskPanelElementId(entry.reference),
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
const MAX_ROWS = 500;
|
|
2
|
+
const MAX_COLUMNS = 50;
|
|
3
|
+
const MAX_CELL_CHARACTERS = 4_000;
|
|
4
|
+
|
|
5
|
+
export interface CsvPreview {
|
|
6
|
+
rows: string[][];
|
|
7
|
+
notices: string[];
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function csvSourcePreview(source: string): { text: string; truncated: boolean } {
|
|
11
|
+
const text = source.replace(/\r\n?/gu, "\n");
|
|
12
|
+
let offset = 0;
|
|
13
|
+
for (let line = 0; line < 2_000; line += 1) {
|
|
14
|
+
const newline = text.indexOf("\n", offset);
|
|
15
|
+
if (newline < 0 || newline === text.length - 1) return { text, truncated: false };
|
|
16
|
+
offset = newline + 1;
|
|
17
|
+
}
|
|
18
|
+
return { text: text.slice(0, offset - 1), truncated: true };
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Parse records rather than physical lines: quoted CSV fields may contain newlines.
|
|
22
|
+
export function parseCsvPreview(source: string, sourceTruncated = false): CsvPreview {
|
|
23
|
+
const text = source.charCodeAt(0) === 0xfeff ? source.slice(1) : source;
|
|
24
|
+
const rows: string[][] = [];
|
|
25
|
+
const notices = new Set<string>();
|
|
26
|
+
let row: string[] = [];
|
|
27
|
+
let field = "";
|
|
28
|
+
let fieldCharacters = 0;
|
|
29
|
+
let state: "start" | "plain" | "quoted" | "closed" = "start";
|
|
30
|
+
let recordStarted = false;
|
|
31
|
+
const append = (character: string): void => {
|
|
32
|
+
if (row.length >= MAX_COLUMNS) return;
|
|
33
|
+
if (fieldCharacters < MAX_CELL_CHARACTERS) { field += character; fieldCharacters += 1; }
|
|
34
|
+
else notices.add(`过长单元格仅显示前 ${MAX_CELL_CHARACTERS} 个字符。`);
|
|
35
|
+
};
|
|
36
|
+
const endField = (): void => {
|
|
37
|
+
if (row.length < MAX_COLUMNS) row.push(field);
|
|
38
|
+
else notices.add(`表格仅显示前 ${MAX_COLUMNS} 列。`);
|
|
39
|
+
field = "";
|
|
40
|
+
fieldCharacters = 0;
|
|
41
|
+
state = "start";
|
|
42
|
+
};
|
|
43
|
+
if (sourceTruncated) notices.add("文件较大,仅读取开头 2 MiB;末尾不完整的记录不显示。可下载完整文件。");
|
|
44
|
+
let invalid = false;
|
|
45
|
+
for (let index = 0; index < text.length; index += 1) {
|
|
46
|
+
const character = String.fromCodePoint(text.codePointAt(index)!);
|
|
47
|
+
if (character.length === 2) index += 1;
|
|
48
|
+
recordStarted = true;
|
|
49
|
+
if (state === "quoted") {
|
|
50
|
+
if (character === '"') state = "closed";
|
|
51
|
+
else append(character);
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
if (character === '"') {
|
|
55
|
+
if (state === "start") state = "quoted";
|
|
56
|
+
else if (state === "closed") { append(character); state = "quoted"; }
|
|
57
|
+
else { invalid = true; break; }
|
|
58
|
+
} else if (character === ",") {
|
|
59
|
+
endField();
|
|
60
|
+
} else if (character === "\r" || character === "\n") {
|
|
61
|
+
if (character === "\r" && text[index + 1] === "\n") index += 1;
|
|
62
|
+
endField();
|
|
63
|
+
rows.push(row);
|
|
64
|
+
row = [];
|
|
65
|
+
recordStarted = false;
|
|
66
|
+
if (rows.length === MAX_ROWS && index + 1 < text.length) {
|
|
67
|
+
notices.add(`表格仅显示前 ${MAX_ROWS} 行。`);
|
|
68
|
+
return { rows, notices: [...notices] };
|
|
69
|
+
}
|
|
70
|
+
} else if (state === "closed") {
|
|
71
|
+
invalid = true;
|
|
72
|
+
break;
|
|
73
|
+
} else {
|
|
74
|
+
state = "plain";
|
|
75
|
+
append(character);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
if (invalid || (!sourceTruncated && state === "quoted")) {
|
|
79
|
+
notices.add(`第 ${rows.length + 1} 条记录的引号格式不完整或不合法,已停止解析。请切换代码查看原文。`);
|
|
80
|
+
} else if (recordStarted && !sourceTruncated) {
|
|
81
|
+
endField();
|
|
82
|
+
rows.push(row);
|
|
83
|
+
}
|
|
84
|
+
return { rows, notices: [...notices] };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function renderCsvPreview(source: string, sourceTruncated = false): string {
|
|
88
|
+
const { rows, notices } = parseCsvPreview(source, sourceTruncated);
|
|
89
|
+
const columns = Math.max(0, ...rows.map((row) => row.length));
|
|
90
|
+
const noticesHtml = notices.map((notice) => `<div class="notice">${escapeHtml(notice)}</div>`).join("");
|
|
91
|
+
if (!rows.length) {
|
|
92
|
+
return `<section data-view-panel="rendered">${noticesHtml}<div class="csv-summary">${notices.length ? "没有可显示的完整记录。" : "CSV 文件为空。"}</div></section>`;
|
|
93
|
+
}
|
|
94
|
+
const headings = Array.from({ length: columns }, (_, index) => {
|
|
95
|
+
const label = index < 26 ? String.fromCharCode(65 + index) : `A${String.fromCharCode(65 + index - 26)}`;
|
|
96
|
+
return `<th scope="col">${label}</th>`;
|
|
97
|
+
}).join("");
|
|
98
|
+
const body = rows.map((row, index) => {
|
|
99
|
+
const cells = row.map((value) => `<td><div class="csv-cell">${escapeHtml(value)}</div></td>`).join("");
|
|
100
|
+
const missing = columns - row.length;
|
|
101
|
+
return `<tr><th scope="row">${index + 1}</th>${cells}${missing ? `<td colspan="${missing}" class="csv-missing"></td>` : ""}</tr>`;
|
|
102
|
+
}).join("");
|
|
103
|
+
return `<section data-view-panel="rendered">${noticesHtml}<div class="csv-summary">已显示 ${rows.length} 行 · 最多 ${columns} 列 · 首行保留为数据</div><div class="csv-table-scroll" role="region" aria-label="CSV 表格" tabindex="0"><table class="csv-table"><thead><tr><th scope="col" aria-label="行号">#</th>${headings}</tr></thead><tbody>${body}</tbody></table></div></section>`;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function escapeHtml(value: string): string {
|
|
107
|
+
return value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """).replaceAll("'", "'");
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export const CSV_PREVIEW_CSS = `
|
|
111
|
+
.csv-summary { padding: 10px 12px; color: #646a73; font-size: 12px; }
|
|
112
|
+
.csv-table-scroll { isolation: isolate; max-width: 100%; max-height: calc(100dvh - var(--viewer-header-offset) - 40px); overflow: auto; background: #fff; }
|
|
113
|
+
.csv-table-scroll:focus-visible { outline: 2px solid #1456f0; outline-offset: -2px; }
|
|
114
|
+
.csv-table { width: max-content; min-width: 100%; border-collapse: separate; border-spacing: 0; font-size: 13px; line-height: 1.5; }
|
|
115
|
+
.csv-table th, .csv-table td { padding: 7px 12px; text-align: left; vertical-align: top; border-right: 1px solid #dfe3e8; border-bottom: 1px solid #dfe3e8; }
|
|
116
|
+
.csv-table th { background: #f0f3f8; font-weight: 600; }
|
|
117
|
+
.csv-table thead th { position: sticky; top: 0; z-index: 1; }
|
|
118
|
+
.csv-table tr > th:first-child { position: sticky; left: 0; text-align: right; min-width: 3.5em; }
|
|
119
|
+
.csv-table thead th:first-child { z-index: 2; }
|
|
120
|
+
.csv-table tbody tr:nth-child(even) td { background: #f9fafc; }
|
|
121
|
+
.csv-table tbody tr:hover td { background: #edf3ff; }
|
|
122
|
+
.csv-cell { min-width: 4em; max-width: 32rem; white-space: pre-wrap; overflow-wrap: anywhere; }
|
|
123
|
+
@media (max-width: 640px) { .csv-cell { max-width: 20rem; } }
|
|
124
|
+
@media (prefers-color-scheme: dark) {
|
|
125
|
+
.csv-summary { color: #a6a9ad; }
|
|
126
|
+
.csv-table-scroll { background: #202124; }
|
|
127
|
+
.csv-table th, .csv-table td { border-color: #55585c; }
|
|
128
|
+
.csv-table th { background: #292a2d; }
|
|
129
|
+
.csv-table tbody tr:nth-child(even) td { background: #252629; }
|
|
130
|
+
.csv-table tbody tr:hover td { background: #292f3d; }
|
|
131
|
+
}
|
|
132
|
+
`;
|
|
@@ -9,6 +9,8 @@ import dos from "highlight.js/lib/languages/dos";
|
|
|
9
9
|
import dockerfile from "highlight.js/lib/languages/dockerfile";
|
|
10
10
|
import powershell from "highlight.js/lib/languages/powershell";
|
|
11
11
|
import MarkdownIt from "markdown-it";
|
|
12
|
+
import { CSV_PREVIEW_CSS, renderCsvPreview, csvSourcePreview } from "./CsvPreview.js";
|
|
13
|
+
import { enableDiagramFences, DIAGRAM_PREVIEW_CLIENT_SCRIPT, DIAGRAM_PREVIEW_CSS } from "./DiagramPreview.js";
|
|
12
14
|
import { isFileUrl, rewriteMarkdownFileLinks } from "./MarkdownFileLinks.js";
|
|
13
15
|
import {
|
|
14
16
|
selectPreferredNetworkAddress,
|
|
@@ -56,6 +58,7 @@ const MARKDOWN_RENDERER = new MarkdownIt({
|
|
|
56
58
|
return `<pre class="markdown-code-block hljs"><code${languageAttribute}>${highlighted}</code></pre>`;
|
|
57
59
|
},
|
|
58
60
|
});
|
|
61
|
+
enableDiagramFences(MARKDOWN_RENDERER);
|
|
59
62
|
MARKDOWN_RENDERER.renderer.rules.table_open = () =>
|
|
60
63
|
'<div class="markdown-table-scroll" role="region" aria-label="Markdown 表格" tabindex="0"><table>\n';
|
|
61
64
|
MARKDOWN_RENDERER.renderer.rules.table_close = () => '</table></div>\n';
|
|
@@ -79,7 +82,7 @@ const HIGHLIGHT_LANGUAGE_BY_FILENAME: Readonly<Record<string, string>> = {
|
|
|
79
82
|
"dockerfile": "dockerfile",
|
|
80
83
|
"makefile": "makefile",
|
|
81
84
|
};
|
|
82
|
-
const VIEWER_CLIENT_SCRIPT = `(() => {
|
|
85
|
+
const VIEWER_CLIENT_SCRIPT = DIAGRAM_PREVIEW_CLIENT_SCRIPT + `(() => {
|
|
83
86
|
const eventsUrl = document.body.dataset.eventsUrl;
|
|
84
87
|
const content = document.getElementById("viewer-content");
|
|
85
88
|
const metadata = document.getElementById("viewer-metadata");
|
|
@@ -157,14 +160,15 @@ const VIEWER_CLIENT_SCRIPT = `(() => {
|
|
|
157
160
|
requestAnimationFrame(positionHashTarget);
|
|
158
161
|
if (!eventsUrl || typeof EventSource !== "function") return;
|
|
159
162
|
|
|
160
|
-
const restoreScroll = (top, left, atBottom, codeScrollLeft,
|
|
163
|
+
const restoreScroll = (top, left, atBottom, codeScrollLeft, tablePositions) => {
|
|
161
164
|
requestAnimationFrame(() => {
|
|
162
165
|
const maxTop = Math.max(0, document.documentElement.scrollHeight - window.innerHeight);
|
|
163
166
|
window.scrollTo(left, atBottom ? maxTop : Math.min(top, maxTop));
|
|
164
167
|
const code = content.querySelector(".code");
|
|
165
168
|
if (code) code.scrollLeft = codeScrollLeft;
|
|
166
|
-
content.querySelectorAll(".markdown-table-scroll").forEach((table, index) => {
|
|
167
|
-
table.scrollLeft =
|
|
169
|
+
content.querySelectorAll(".markdown-table-scroll, .csv-table-scroll").forEach((table, index) => {
|
|
170
|
+
table.scrollLeft = tablePositions[index]?.left ?? 0;
|
|
171
|
+
table.scrollTop = tablePositions[index]?.top ?? 0;
|
|
168
172
|
});
|
|
169
173
|
});
|
|
170
174
|
};
|
|
@@ -174,12 +178,40 @@ const VIEWER_CLIENT_SCRIPT = `(() => {
|
|
|
174
178
|
const maxTop = Math.max(0, document.documentElement.scrollHeight - window.innerHeight);
|
|
175
179
|
const atBottom = maxTop - top <= 24;
|
|
176
180
|
const codeScrollLeft = content.querySelector(".code")?.scrollLeft ?? 0;
|
|
177
|
-
const
|
|
178
|
-
|
|
181
|
+
const tablePositions = Array.from(content.querySelectorAll(".markdown-table-scroll, .csv-table-scroll"), (table) => ({ left: table.scrollLeft, top: table.scrollTop }));
|
|
182
|
+
const template = document.createElement("template");
|
|
183
|
+
template.innerHTML = nextContent;
|
|
184
|
+
const previousDiagrams = new Set(content.querySelectorAll("[data-diagram]"));
|
|
185
|
+
const nextDiagrams = Array.from(template.content.querySelectorAll("[data-diagram]"));
|
|
186
|
+
const retainedDiagrams = new Map();
|
|
187
|
+
const diagramSource = (block) => block.querySelector(".diagram-source code").textContent;
|
|
188
|
+
// Match unchanged diagrams first so inserted text or diagrams do not reset their state.
|
|
189
|
+
for (const next of nextDiagrams) {
|
|
190
|
+
const current = Array.from(previousDiagrams).find((block) => diagramSource(block) === diagramSource(next));
|
|
191
|
+
if (current) { retainedDiagrams.set(next, current); previousDiagrams.delete(current); }
|
|
192
|
+
}
|
|
193
|
+
for (const next of nextDiagrams) {
|
|
194
|
+
if (retainedDiagrams.has(next)) continue;
|
|
195
|
+
const current = Array.from(previousDiagrams).find((block) => block.dataset.previewKey === next.dataset.previewKey);
|
|
196
|
+
if (current) { retainedDiagrams.set(next, current); previousDiagrams.delete(current); }
|
|
197
|
+
}
|
|
198
|
+
const diagramScroll = [];
|
|
199
|
+
for (const [next, current] of retainedDiagrams) {
|
|
200
|
+
for (const panel of current.querySelectorAll(".diagram-preview, .diagram-source")) {
|
|
201
|
+
diagramScroll.push([panel, panel.scrollLeft, panel.scrollTop]);
|
|
202
|
+
}
|
|
203
|
+
const source = current.querySelector(".diagram-source code");
|
|
204
|
+
if (source.textContent !== diagramSource(next)) source.textContent = diagramSource(next);
|
|
205
|
+
current.dataset.previewKey = next.dataset.previewKey;
|
|
206
|
+
next.replaceWith(current);
|
|
207
|
+
}
|
|
208
|
+
content.replaceChildren(template.content);
|
|
179
209
|
metadata.innerHTML = nextMetadata;
|
|
180
210
|
syncViewMode();
|
|
211
|
+
window.agentBotDiagrams.scan();
|
|
212
|
+
for (const [panel, left, top] of diagramScroll) { panel.scrollLeft = left; panel.scrollTop = top; }
|
|
181
213
|
highlightHashTarget();
|
|
182
|
-
restoreScroll(top, left, atBottom, codeScrollLeft,
|
|
214
|
+
restoreScroll(top, left, atBottom, codeScrollLeft, tablePositions);
|
|
183
215
|
};
|
|
184
216
|
|
|
185
217
|
const source = new EventSource(eventsUrl);
|
|
@@ -187,7 +219,7 @@ const VIEWER_CLIENT_SCRIPT = `(() => {
|
|
|
187
219
|
try {
|
|
188
220
|
const update = JSON.parse(event.data);
|
|
189
221
|
if (typeof update.content === "string" && typeof update.metadata === "string") {
|
|
190
|
-
if (viewSwitch) viewSwitch.hidden = update.viewMode !== "markdown" && update.viewMode !== "html";
|
|
222
|
+
if (viewSwitch) viewSwitch.hidden = update.viewMode !== "markdown" && update.viewMode !== "html" && update.viewMode !== "csv";
|
|
191
223
|
replaceContent(update.content, update.metadata);
|
|
192
224
|
}
|
|
193
225
|
} catch {
|
|
@@ -229,7 +261,7 @@ interface DirectoryViewerEntry {
|
|
|
229
261
|
interface RenderedFileSnapshot {
|
|
230
262
|
content: string;
|
|
231
263
|
metadata: string[];
|
|
232
|
-
viewMode?: "markdown" | "html";
|
|
264
|
+
viewMode?: "markdown" | "html" | "csv";
|
|
233
265
|
}
|
|
234
266
|
|
|
235
267
|
export interface LocalFileViewerServerOptions {
|
|
@@ -611,6 +643,7 @@ export class LocalFileViewerServer {
|
|
|
611
643
|
],
|
|
612
644
|
content: snapshot.content,
|
|
613
645
|
viewMode: snapshot.viewMode,
|
|
646
|
+
diagramScriptPath: isMarkdownFile(filePath) ? `${this.basePath}/assets/mermaid.js`.replace(/\/{2,}/gu, "/") : undefined,
|
|
614
647
|
liveUpdates: {
|
|
615
648
|
eventsUrl: eventsUrl.toString(),
|
|
616
649
|
scriptPath: `${this.basePath}/assets/viewer.js`.replace(/\/{2,}/gu, "/"),
|
|
@@ -640,6 +673,11 @@ export class LocalFileViewerServer {
|
|
|
640
673
|
appendUrlQueryParameter(renderUrl, "render", "html");
|
|
641
674
|
appendUrlQueryParameter(renderUrl, "version", `${stat.mtimeMs}-${stat.ctimeMs}-${stat.size}`);
|
|
642
675
|
content = `<iframe class="html-preview" data-view-panel="rendered" title="HTML 预览" sandbox="allow-scripts" referrerpolicy="no-referrer" src="${escapeAttribute(renderUrl.toString())}"></iframe><section data-view-panel="code">${notice}${renderText(preview.text, filePath)}</section>`;
|
|
676
|
+
} else if (extension === ".csv") {
|
|
677
|
+
viewMode = "csv";
|
|
678
|
+
const source = csvSourcePreview(preview.text);
|
|
679
|
+
const sourceNotice = source.truncated ? '<div class="notice">代码仅显示前 2000 个物理行,可打开原始文件或下载完整文件。</div>' : "";
|
|
680
|
+
content = `${renderCsvPreview(preview.text, preview.truncated)}<section data-view-panel="code">${notice}${sourceNotice}${renderText(source.text, filePath)}</section>`;
|
|
643
681
|
} else {
|
|
644
682
|
viewMode = isMarkdownFile(filePath) ? "markdown" : undefined;
|
|
645
683
|
const previewContent = viewMode === "markdown"
|
|
@@ -984,6 +1022,7 @@ function renderViewerPage(input: {
|
|
|
984
1022
|
actions: Array<{ href: string; label: string }>;
|
|
985
1023
|
content: string;
|
|
986
1024
|
viewMode?: RenderedFileSnapshot["viewMode"];
|
|
1025
|
+
diagramScriptPath?: string;
|
|
987
1026
|
liveUpdates?: { eventsUrl: string; scriptPath: string };
|
|
988
1027
|
}): string {
|
|
989
1028
|
const metadata = renderMetadata(input.metadata);
|
|
@@ -1008,7 +1047,7 @@ function renderViewerPage(input: {
|
|
|
1008
1047
|
<meta charset="utf-8">
|
|
1009
1048
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
1010
1049
|
<title>${escapeHtml(input.title)} · Agent Bot</title>
|
|
1011
|
-
<style
|
|
1050
|
+
<style>${input.diagramScriptPath ? DIAGRAM_PREVIEW_CSS : ""}
|
|
1012
1051
|
:root { --viewer-header-offset: 56px; --viewer-code-font: "Cascadia Mono", "JetBrains Mono", "SFMono-Regular", Consolas, "Liberation Mono", "Sarasa Mono SC", "Noto Sans Mono CJK SC", "Microsoft YaHei Mono", NSimSun, monospace; color-scheme: light dark; font-family: Inter, "Segoe UI", system-ui, sans-serif; }
|
|
1013
1052
|
* { box-sizing: border-box; }
|
|
1014
1053
|
body { margin: 0; background: #f5f7fa; color: #1f2329; }
|
|
@@ -1056,6 +1095,8 @@ function renderViewerPage(input: {
|
|
|
1056
1095
|
.markdown-body code { padding: .14em .35em; border-radius: 4px; background: #f2f3f5; font-family: var(--viewer-code-font); font-size: .9em; }
|
|
1057
1096
|
.markdown-body .markdown-code-block { margin: 0 0 1em; padding: 14px 16px; overflow: auto; border-radius: 6px; background: #f6f8fa; line-height: 1.55; }
|
|
1058
1097
|
.markdown-body .markdown-code-block code { padding: 0; background: transparent; font-size: 13px; }
|
|
1098
|
+
.markdown-body .diagram-source { padding: 14px 16px; }
|
|
1099
|
+
.markdown-body .diagram-source code { padding: 0; background: transparent; font-size: 13px; }
|
|
1059
1100
|
.markdown-table-scroll { max-width: 100%; margin: 0 0 1em; overflow-x: auto; }
|
|
1060
1101
|
.markdown-table-scroll:focus-visible { outline: 2px solid #1456f0; outline-offset: 2px; }
|
|
1061
1102
|
.markdown-body table { width: max-content; margin: 0; border-collapse: collapse; white-space: nowrap; overflow-wrap: normal; word-break: normal; }
|
|
@@ -1063,6 +1104,7 @@ function renderViewerPage(input: {
|
|
|
1063
1104
|
.markdown-body th { background: #f5f7fa; }
|
|
1064
1105
|
.markdown-body img { max-width: 100%; height: auto; }
|
|
1065
1106
|
.markdown-body hr { height: 1px; margin: 1.5em 0; border: 0; background: #dfe3e8; }
|
|
1107
|
+
${CSV_PREVIEW_CSS}
|
|
1066
1108
|
.media { display: grid; place-items: center; min-height: 220px; }
|
|
1067
1109
|
.media img, .media video { max-width: 100%; max-height: calc(100vh - 70px); }
|
|
1068
1110
|
.media audio { width: min(720px, 100%); }
|
|
@@ -1092,6 +1134,7 @@ function renderViewerPage(input: {
|
|
|
1092
1134
|
.view-switch button.is-active { background: #3a3b3d; color: #8ab4ff; box-shadow: none; }
|
|
1093
1135
|
.code, .directory-list { background: #202124; color: #e8eaed; border-color: #3a3b3d; }
|
|
1094
1136
|
.markdown-body { background: #202124; color: #e8eaed; }
|
|
1137
|
+
.markdown-body .diagram { --border: #55585c; }
|
|
1095
1138
|
.markdown-body h1, .markdown-body h2 { border-color: #3a3b3d; }
|
|
1096
1139
|
.markdown-body blockquote { color: #a6a9ad; border-color: #55585c; }
|
|
1097
1140
|
.markdown-body a { color: #8ab4ff; }
|
|
@@ -1115,7 +1158,7 @@ function renderViewerPage(input: {
|
|
|
1115
1158
|
}
|
|
1116
1159
|
</style>
|
|
1117
1160
|
</head>
|
|
1118
|
-
<body${input.viewMode ? ' data-view-mode="rendered"' : ""}${liveAttributes}>
|
|
1161
|
+
<body${input.viewMode ? ' data-view-mode="rendered"' : ""}${liveAttributes}${input.diagramScriptPath ? ` data-diagram-script="${escapeAttribute(input.diagramScriptPath)}"` : ""}>
|
|
1119
1162
|
<header id="viewer-header">
|
|
1120
1163
|
<h1 id="viewer-title" data-file-path="${escapeAttribute(input.filePath)}">${escapeHtml(input.filePath)}</h1>
|
|
1121
1164
|
<div class="meta"><span class="metadata-values" id="viewer-metadata">${metadata}</span>${toolbar}</div>
|