@keyou007/agent-bot 0.1.21 → 0.1.22
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 +7 -0
- package/README.md +5 -1
- package/README.zh.md +5 -1
- package/dist/feishu/CardRenderer.js +1 -1
- package/dist/feishu/CardRenderer.js.map +1 -1
- package/dist/local-files/LocalFileViewerServer.js +24 -10
- package/dist/local-files/LocalFileViewerServer.js.map +1 -1
- package/dist/local-files/MarkdownFileLinks.js +12 -5
- package/dist/local-files/MarkdownFileLinks.js.map +1 -1
- package/dist/local-files/TurnPreviewPage.js +44 -17
- package/dist/local-files/TurnPreviewPage.js.map +1 -1
- package/npm-shrinkwrap.json +2 -2
- package/package.json +1 -1
- package/src/feishu/CardRenderer.ts +1 -1
- package/src/local-files/LocalFileViewerServer.ts +23 -11
- package/src/local-files/MarkdownFileLinks.ts +12 -7
- package/src/local-files/TurnPreviewPage.ts +44 -15
|
@@ -622,10 +622,12 @@ export class LocalFileViewerServer {
|
|
|
622
622
|
|
|
623
623
|
let closed = false;
|
|
624
624
|
let pendingUpdate: NodeJS.Timeout | undefined;
|
|
625
|
+
let previousStat: fs.Stats | undefined;
|
|
625
626
|
const sendSnapshot = () => {
|
|
626
627
|
if (closed || response.destroyed) return;
|
|
627
628
|
try {
|
|
628
629
|
const stat = fs.statSync(filePath);
|
|
630
|
+
previousStat = stat;
|
|
629
631
|
if (!stat.isFile()) throw new Error("Path is no longer a file");
|
|
630
632
|
const snapshot = this.renderFileSnapshot(filePath, stat);
|
|
631
633
|
writeServerSentEvent(response, "update", JSON.stringify({
|
|
@@ -644,17 +646,27 @@ export class LocalFileViewerServer {
|
|
|
644
646
|
pendingUpdate = setTimeout(sendSnapshot, 100);
|
|
645
647
|
pendingUpdate.unref();
|
|
646
648
|
};
|
|
647
|
-
const watcher = (current: fs.Stats, previous: fs.Stats) => {
|
|
648
|
-
if (
|
|
649
|
-
current.mtimeMs === previous.mtimeMs
|
|
650
|
-
&& current.ctimeMs === previous.ctimeMs
|
|
651
|
-
&& current.size === previous.size
|
|
652
|
-
&& current.nlink === previous.nlink
|
|
653
|
-
) return;
|
|
654
|
-
scheduleSnapshot();
|
|
655
|
-
};
|
|
656
|
-
fs.watchFile(filePath, { interval: 1_000, persistent: false }, watcher);
|
|
657
649
|
sendSnapshot();
|
|
650
|
+
// Compare against the sent snapshot so edits during watcher startup cannot be missed.
|
|
651
|
+
let checking = false;
|
|
652
|
+
const poller = setInterval(() => {
|
|
653
|
+
if (closed || checking) return;
|
|
654
|
+
checking = true;
|
|
655
|
+
fs.stat(filePath, (error, stat) => {
|
|
656
|
+
checking = false;
|
|
657
|
+
if (closed) return;
|
|
658
|
+
const current = error ? undefined : stat;
|
|
659
|
+
if (
|
|
660
|
+
current?.mtimeMs === previousStat?.mtimeMs
|
|
661
|
+
&& current?.ctimeMs === previousStat?.ctimeMs
|
|
662
|
+
&& current?.size === previousStat?.size
|
|
663
|
+
&& current?.nlink === previousStat?.nlink
|
|
664
|
+
) return;
|
|
665
|
+
previousStat = current;
|
|
666
|
+
scheduleSnapshot();
|
|
667
|
+
});
|
|
668
|
+
}, 1_000);
|
|
669
|
+
poller.unref();
|
|
658
670
|
const keepAlive = setInterval(() => {
|
|
659
671
|
if (!closed && !response.destroyed) response.write(": keep-alive\n\n");
|
|
660
672
|
}, 15_000);
|
|
@@ -665,7 +677,7 @@ export class LocalFileViewerServer {
|
|
|
665
677
|
closed = true;
|
|
666
678
|
if (pendingUpdate) clearTimeout(pendingUpdate);
|
|
667
679
|
clearInterval(keepAlive);
|
|
668
|
-
|
|
680
|
+
clearInterval(poller);
|
|
669
681
|
};
|
|
670
682
|
request.once("aborted", cleanup);
|
|
671
683
|
response.once("close", cleanup);
|
|
@@ -8,8 +8,8 @@ export function isFileUrl(value: string): boolean {
|
|
|
8
8
|
|
|
9
9
|
export function rewriteMarkdownFileLinks(
|
|
10
10
|
tokens: Token[],
|
|
11
|
-
markdownPath: string,
|
|
12
|
-
createPreviewUrl: (filePath: string) => URL,
|
|
11
|
+
markdownPath: string | undefined,
|
|
12
|
+
createPreviewUrl: (filePath: string) => URL | undefined,
|
|
13
13
|
): void {
|
|
14
14
|
for (const token of tokens) {
|
|
15
15
|
const attribute = token.type === "link_open" ? "href" : token.type === "image" ? "src" : undefined;
|
|
@@ -18,9 +18,13 @@ export function rewriteMarkdownFileLinks(
|
|
|
18
18
|
const local = resolveLocalTarget(target, markdownPath);
|
|
19
19
|
if (local) {
|
|
20
20
|
const url = createPreviewUrl(local.filePath);
|
|
21
|
-
url
|
|
22
|
-
|
|
23
|
-
|
|
21
|
+
if (url) {
|
|
22
|
+
url.hash = local.hash;
|
|
23
|
+
if (token.type === "image") url.searchParams.set("raw", "1");
|
|
24
|
+
token.attrSet(attribute, url.toString());
|
|
25
|
+
} else {
|
|
26
|
+
token.attrs = token.attrs?.filter(([name]) => name !== attribute) ?? null;
|
|
27
|
+
}
|
|
24
28
|
} else if (isFileUrl(target)) {
|
|
25
29
|
// File URLs are accepted by the parser only to convert them to signed HTTP URLs.
|
|
26
30
|
token.attrs = token.attrs?.filter(([name]) => name !== attribute) ?? null;
|
|
@@ -30,7 +34,7 @@ export function rewriteMarkdownFileLinks(
|
|
|
30
34
|
}
|
|
31
35
|
}
|
|
32
36
|
|
|
33
|
-
function resolveLocalTarget(target: string, markdownPath: string): { filePath: string; hash: string } | undefined {
|
|
37
|
+
function resolveLocalTarget(target: string, markdownPath: string | undefined): { filePath: string; hash: string } | undefined {
|
|
34
38
|
if (/^(?:#|\?|\/\/)/u.test(target)) return undefined;
|
|
35
39
|
const windows = process.platform === "win32";
|
|
36
40
|
const normalized = windows ? target.replace(/%5c/giu, "/").replaceAll("\\", "/") : target;
|
|
@@ -44,11 +48,12 @@ function resolveLocalTarget(target: string, markdownPath: string): { filePath: s
|
|
|
44
48
|
? normalized.slice(0, reference.index) + normalized.slice(reference.index + reference[0].length)
|
|
45
49
|
: normalized;
|
|
46
50
|
if (drivePath && !windows) return undefined;
|
|
51
|
+
if (!markdownPath && !drivePath && !isFileUrl(normalized) && !path.isAbsolute(normalized)) return undefined;
|
|
47
52
|
|
|
48
53
|
try {
|
|
49
54
|
const url = drivePath
|
|
50
55
|
? new URL(`file:///${withoutReference.replace(/^\//u, "")}`)
|
|
51
|
-
: new URL(withoutReference, pathToFileURL(markdownPath));
|
|
56
|
+
: new URL(withoutReference, markdownPath ? pathToFileURL(markdownPath) : "file:///");
|
|
52
57
|
if (url.protocol !== "file:") return undefined;
|
|
53
58
|
const filePath = fileURLToPath(url);
|
|
54
59
|
if (!path.isAbsolute(filePath) || /[\x00-\x1f\x7f]/u.test(filePath)) return undefined;
|
|
@@ -3,6 +3,7 @@ import MarkdownIt from "markdown-it";
|
|
|
3
3
|
import type { ToolState } from "../runtime/types.js";
|
|
4
4
|
import type { FileSummary, TurnActivity, TurnViewState, TurnViewStatus } from "../presentation/turnViewTypes.js";
|
|
5
5
|
import { displayToolCommand, formatShellCommandForDisplay } from "../feishu/CardRenderer.js";
|
|
6
|
+
import { isFileUrl, rewriteMarkdownFileLinks } from "./MarkdownFileLinks.js";
|
|
6
7
|
|
|
7
8
|
const MARKDOWN = new MarkdownIt({
|
|
8
9
|
breaks: true,
|
|
@@ -10,6 +11,8 @@ const MARKDOWN = new MarkdownIt({
|
|
|
10
11
|
linkify: true,
|
|
11
12
|
typographer: false,
|
|
12
13
|
});
|
|
14
|
+
const validateLink = MARKDOWN.validateLink.bind(MARKDOWN);
|
|
15
|
+
MARKDOWN.validateLink = (url) => isFileUrl(url) || validateLink(url);
|
|
13
16
|
const defaultLinkOpen = MARKDOWN.renderer.rules.link_open
|
|
14
17
|
?? ((tokens, index, options, _environment, renderer) => renderer.renderToken(tokens, index, options));
|
|
15
18
|
MARKDOWN.renderer.rules.link_open = (tokens, index, options, environment, renderer) => {
|
|
@@ -284,9 +287,10 @@ export function renderTurnPreviewSnapshot(
|
|
|
284
287
|
if (!state.projectCwd || !path.isAbsolute(state.projectCwd)) return undefined;
|
|
285
288
|
return localFileUrl(path.resolve(state.projectCwd, filePath));
|
|
286
289
|
} : undefined;
|
|
287
|
-
const
|
|
290
|
+
const renderText = (value: string): string => renderMarkdown(value, state.projectCwd, localFileUrl);
|
|
291
|
+
const timeline = renderTimeline(state.activities ?? [], renderText, resolveFileUrl, state.fullToolOutputs, state.fullToolErrors, language);
|
|
288
292
|
const sections = [
|
|
289
|
-
state.prompt ? renderMessageActivity("prompt", state.prompt, "user prompt") : "",
|
|
293
|
+
state.prompt ? renderMessageActivity("prompt", state.prompt, "user prompt", renderText) : "",
|
|
290
294
|
state.plan.length > 0 ? renderPlan(state, language) : "",
|
|
291
295
|
state.activitiesTruncated
|
|
292
296
|
? `<div class="notice">${escapeHtml(labels.truncated)}</div>`
|
|
@@ -295,9 +299,9 @@ export function renderTurnPreviewSnapshot(
|
|
|
295
299
|
state.fileSummary.length > 0 ? renderFileSummary(state, language, resolveFileUrl) : "",
|
|
296
300
|
state.error ? `<section class="result error-result"><h2>${escapeHtml(labels.error)}</h2><pre>${escapeHtml(state.error)}</pre></section>` : "",
|
|
297
301
|
state.finalResponse
|
|
298
|
-
? `<section class="result final-result"><h2>${escapeHtml(labels.finalAnswer)}</h2><div class="markdown">${
|
|
302
|
+
? `<section class="result final-result"><h2>${escapeHtml(labels.finalAnswer)}</h2><div class="markdown">${renderText(state.finalResponse)}</div></section>`
|
|
299
303
|
: state.assistantText
|
|
300
|
-
? `<section class="result"><h2>${escapeHtml(labels.generating)}</h2><div class="markdown">${
|
|
304
|
+
? `<section class="result"><h2>${escapeHtml(labels.generating)}</h2><div class="markdown">${renderText(state.assistantText)}</div></section>`
|
|
301
305
|
: "",
|
|
302
306
|
].filter(Boolean).join("");
|
|
303
307
|
const terminal = isTerminal(state.status);
|
|
@@ -322,6 +326,7 @@ function renderPlan(state: TurnViewState, language: TurnPreviewLanguage): string
|
|
|
322
326
|
|
|
323
327
|
function renderTimeline(
|
|
324
328
|
activities: TurnActivity[],
|
|
329
|
+
renderText: (value: string) => string,
|
|
325
330
|
localFileUrl?: (filePath: string) => string | undefined,
|
|
326
331
|
fullToolOutputs?: Record<string, string>,
|
|
327
332
|
fullToolErrors?: Record<string, string>,
|
|
@@ -329,12 +334,13 @@ function renderTimeline(
|
|
|
329
334
|
): string {
|
|
330
335
|
return activities
|
|
331
336
|
.filter((activity): activity is Exclude<TurnActivity, { kind: "reasoning" }> => activity.kind !== "reasoning")
|
|
332
|
-
.map((activity) => renderActivity(activity, localFileUrl, fullToolOutputs, fullToolErrors, language))
|
|
337
|
+
.map((activity) => renderActivity(activity, renderText, localFileUrl, fullToolOutputs, fullToolErrors, language))
|
|
333
338
|
.join("");
|
|
334
339
|
}
|
|
335
340
|
|
|
336
341
|
function renderActivity(
|
|
337
342
|
activity: Exclude<TurnActivity, { kind: "reasoning" }>,
|
|
343
|
+
renderText: (value: string) => string,
|
|
338
344
|
localFileUrl?: (filePath: string) => string | undefined,
|
|
339
345
|
fullToolOutputs?: Record<string, string>,
|
|
340
346
|
fullToolErrors?: Record<string, string>,
|
|
@@ -342,16 +348,16 @@ function renderActivity(
|
|
|
342
348
|
): string {
|
|
343
349
|
switch (activity.kind) {
|
|
344
350
|
case "assistant":
|
|
345
|
-
return renderMessageActivity(activity.id, activity.text, "commentary");
|
|
351
|
+
return renderMessageActivity(activity.id, activity.text, "commentary", renderText);
|
|
346
352
|
case "user":
|
|
347
|
-
return renderMessageActivity(activity.id, activity.text, "user");
|
|
353
|
+
return renderMessageActivity(activity.id, activity.text, "user", renderText);
|
|
348
354
|
case "tool":
|
|
349
355
|
return renderToolActivity(activity.id, activity.tool, localFileUrl, fullToolOutputs, fullToolErrors, language);
|
|
350
356
|
}
|
|
351
357
|
}
|
|
352
358
|
|
|
353
|
-
function renderMessageActivity(id: string, text: string, kind: string): string {
|
|
354
|
-
return `<article class="activity message ${kind}" data-activity="${escapeAttribute(id)}"><div class="markdown">${
|
|
359
|
+
function renderMessageActivity(id: string, text: string, kind: string, renderText: (value: string) => string): string {
|
|
360
|
+
return `<article class="activity message ${kind}" data-activity="${escapeAttribute(id)}"><div class="markdown">${renderText(text)}</div></article>`;
|
|
355
361
|
}
|
|
356
362
|
|
|
357
363
|
function renderToolActivity(
|
|
@@ -608,8 +614,16 @@ function previewTitle(state: TurnViewState): string {
|
|
|
608
614
|
return value.length > 80 ? `${value.slice(0, 79)}…` : value;
|
|
609
615
|
}
|
|
610
616
|
|
|
611
|
-
function renderMarkdown(value: string): string {
|
|
612
|
-
|
|
617
|
+
function renderMarkdown(value: string, projectCwd?: string, localFileUrl?: (filePath: string) => string | undefined): string {
|
|
618
|
+
const environment = {};
|
|
619
|
+
const tokens = MARKDOWN.parse(value, environment);
|
|
620
|
+
// Only the directory is used to resolve relative links; no Markdown file is created.
|
|
621
|
+
const markdownPath = projectCwd && path.isAbsolute(projectCwd) ? path.join(projectCwd, "turn.md") : undefined;
|
|
622
|
+
rewriteMarkdownFileLinks(tokens, markdownPath, (filePath) => {
|
|
623
|
+
const url = localFileUrl?.(filePath);
|
|
624
|
+
return url ? new URL(url) : undefined;
|
|
625
|
+
});
|
|
626
|
+
return MARKDOWN.renderer.render(tokens, MARKDOWN.options, environment);
|
|
613
627
|
}
|
|
614
628
|
|
|
615
629
|
function statusLabel(status: TurnViewStatus, language: TurnPreviewLanguage): string {
|
|
@@ -673,7 +687,7 @@ function escapeAttribute(value: string): string {
|
|
|
673
687
|
}
|
|
674
688
|
|
|
675
689
|
const TURN_PREVIEW_CSS = `
|
|
676
|
-
:root { color-scheme: light dark; font-family: "Segoe UI", "PingFang SC", "Microsoft YaHei", system-ui, sans-serif; --page: #fff; --surface: #f5f6f7; --code: #fafafa; --text: #25272b; --muted: #62666d; --line: #e0e2e5; --accent: #245bc0; --success: #257449; --danger: #b52c34; }
|
|
690
|
+
:root { color-scheme: light dark; font-family: "Segoe UI", "PingFang SC", "Microsoft YaHei", system-ui, sans-serif; --page: #fff; --surface: #f5f6f7; --code: #fafafa; --text: #25272b; --muted: #62666d; --line: #e0e2e5; --accent: #245bc0; --success: #257449; --danger: #b52c34; --scrollbar-thumb: #d4d7dc; --scrollbar-thumb-hover: #afb4bc; }
|
|
677
691
|
* { box-sizing: border-box; letter-spacing: 0; }
|
|
678
692
|
body { margin: 0; background: var(--page); color: var(--text); font-size: 13px; line-height: 1.55; }
|
|
679
693
|
.page-header { position: sticky; top: 0; z-index: 3; background: var(--page); border-bottom: 1px solid var(--line); }
|
|
@@ -711,7 +725,7 @@ main { max-width: 1320px; margin: 0 auto; padding: 0 20px 40px; }
|
|
|
711
725
|
.activity { min-width: 0; }
|
|
712
726
|
.message { padding: 12px 0; border-bottom: 1px solid var(--line); }
|
|
713
727
|
.markdown { min-width: 0; overflow-wrap: anywhere; }
|
|
714
|
-
.
|
|
728
|
+
.message > .markdown, .result > .markdown { font-size: 14px; font-weight: 400; }
|
|
715
729
|
.markdown > :first-child { margin-top: 0; }
|
|
716
730
|
.markdown > :last-child { margin-bottom: 0; }
|
|
717
731
|
.markdown p, .markdown ul, .markdown ol, .markdown pre, .markdown blockquote { margin: 0 0 6px; }
|
|
@@ -734,7 +748,22 @@ summary:hover { background: transparent; }
|
|
|
734
748
|
summary:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }
|
|
735
749
|
.tool { margin: 8px 0; }
|
|
736
750
|
.tool-step { border: 1px solid var(--line); border-radius: 8px; overflow: hidden; background: transparent; }
|
|
737
|
-
.tool-header { display: flex; align-items: flex-start; gap: 8px; min-height: 32px; padding: 7px 10px; overflow
|
|
751
|
+
.tool-header { display: flex; align-items: flex-start; gap: 8px; min-height: 32px; max-height: calc(15 * 12px * 1.55 + 14px); padding: 7px 10px; overflow: auto; background: transparent; font-weight: 400; }
|
|
752
|
+
.tool-header, .tool-body pre { --tool-scrollbar-thumb: var(--scrollbar-thumb); scrollbar-width: thin; scrollbar-color: var(--tool-scrollbar-thumb) transparent; }
|
|
753
|
+
@media (hover: hover) and (pointer: fine) {
|
|
754
|
+
.tool-header, .tool-body pre { --tool-scrollbar-thumb: transparent; }
|
|
755
|
+
.tool-header:is(:hover, :focus-visible, :focus-within),
|
|
756
|
+
.tool-body pre:is(:hover, :focus-visible, :focus-within) { --tool-scrollbar-thumb: var(--scrollbar-thumb); }
|
|
757
|
+
}
|
|
758
|
+
@supports selector(::-webkit-scrollbar) {
|
|
759
|
+
.tool-header, .tool-body pre { scrollbar-width: auto; scrollbar-color: auto; }
|
|
760
|
+
:is(.tool-header, .tool-body pre)::-webkit-scrollbar { width: 6px; height: 6px; }
|
|
761
|
+
:is(.tool-header, .tool-body pre)::-webkit-scrollbar-track,
|
|
762
|
+
:is(.tool-header, .tool-body pre)::-webkit-scrollbar-corner { background: transparent; }
|
|
763
|
+
:is(.tool-header, .tool-body pre)::-webkit-scrollbar-thumb { background: var(--tool-scrollbar-thumb); border-radius: 3px; }
|
|
764
|
+
:is(.tool-header, .tool-body pre)::-webkit-scrollbar-thumb:hover { background: var(--scrollbar-thumb-hover); }
|
|
765
|
+
:is(.tool-header, .tool-body pre)::-webkit-scrollbar-button { display: none; width: 0; height: 0; }
|
|
766
|
+
}
|
|
738
767
|
.tool-state { display: inline-flex; flex: none; align-items: center; justify-content: center; color: var(--muted); font-size: 11px; font-weight: 650; white-space: nowrap; }
|
|
739
768
|
.tool-state.completed { color: var(--success); }
|
|
740
769
|
.tool-state.running { color: var(--accent); }
|
|
@@ -786,7 +815,7 @@ summary:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px;
|
|
|
786
815
|
pre { padding: 8px 10px; }
|
|
787
816
|
}
|
|
788
817
|
@media (prefers-color-scheme: dark) {
|
|
789
|
-
:root { --page: #18191b; --surface: #232427; --code: #1d1e20; --text: #e0e1e4; --muted: #a0a3ab; --line: #35373c; --accent: #8aaff7; --success: #83c69c; --danger: #f58c94; }
|
|
818
|
+
:root { --page: #18191b; --surface: #232427; --code: #1d1e20; --text: #e0e1e4; --muted: #a0a3ab; --line: #35373c; --accent: #8aaff7; --success: #83c69c; --danger: #f58c94; --scrollbar-thumb: #45494f; --scrollbar-thumb-hover: #60666e; }
|
|
790
819
|
.notice, .status.waiting_for_approval { color: #e1bc6f; }
|
|
791
820
|
}
|
|
792
821
|
`;
|