@keyou007/agent-bot 0.1.24-alpha.1 → 0.1.24-alpha.3
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 +94 -0
- package/README.md +33 -7
- package/README.zh.md +32 -7
- package/dist/acp/AcpJsonRpcConnection.js +3 -1
- package/dist/acp/AcpJsonRpcConnection.js.map +1 -1
- package/dist/cli/SelfUpdateRunner.js +2 -0
- package/dist/cli/SelfUpdateRunner.js.map +1 -1
- package/dist/cli/SelfUpdater.js +2 -0
- package/dist/cli/SelfUpdater.js.map +1 -1
- package/dist/codex/AppServerConnection.js +3 -1
- package/dist/codex/AppServerConnection.js.map +1 -1
- package/dist/codex/CodexEventMapper.js +41 -3
- package/dist/codex/CodexEventMapper.js.map +1 -1
- package/dist/codex/CodexRuntime.js +95 -3
- package/dist/codex/CodexRuntime.js.map +1 -1
- package/dist/commands/CommandRouter.js +5 -0
- package/dist/commands/CommandRouter.js.map +1 -1
- package/dist/feishu/CardRenderer.js +49 -23
- package/dist/feishu/CardRenderer.js.map +1 -1
- package/dist/feishu/FeishuConnector.js +2 -0
- package/dist/feishu/FeishuConnector.js.map +1 -1
- package/dist/feishu/FeishuMarkdown.js +70 -4
- package/dist/feishu/FeishuMarkdown.js.map +1 -1
- package/dist/feishu/FeishuMessageClient.js +44 -0
- package/dist/feishu/FeishuMessageClient.js.map +1 -1
- package/dist/feishu/FeishuTurnPresenter.js +114 -37
- package/dist/feishu/FeishuTurnPresenter.js.map +1 -1
- package/dist/feishu/RecentMessageContext.js +70 -0
- package/dist/feishu/RecentMessageContext.js.map +1 -0
- package/dist/index.js +62 -24
- package/dist/index.js.map +1 -1
- package/dist/local-files/DiagramPreview.js +171 -0
- package/dist/local-files/DiagramPreview.js.map +1 -0
- package/dist/local-files/LocalFileViewerServer.js +141 -3
- package/dist/local-files/LocalFileViewerServer.js.map +1 -1
- package/dist/local-files/PreviewSyntaxHighlight.js +57 -0
- package/dist/local-files/PreviewSyntaxHighlight.js.map +1 -0
- package/dist/local-files/TurnPreviewPage.js +499 -60
- package/dist/local-files/TurnPreviewPage.js.map +1 -1
- package/dist/presentation/OutboundRouter.js +18 -9
- package/dist/presentation/OutboundRouter.js.map +1 -1
- package/dist/presentation/TurnStateReducer.js +157 -8
- package/dist/presentation/TurnStateReducer.js.map +1 -1
- package/dist/presentation/turnReasoning.js +23 -0
- package/dist/presentation/turnReasoning.js.map +1 -0
- package/dist/proxy/ProxySessionController.js +461 -49
- package/dist/proxy/ProxySessionController.js.map +1 -1
- package/dist/state/StateStore.js +43 -2
- package/dist/state/StateStore.js.map +1 -1
- package/dist/state/TurnPreviewJournal.js +469 -0
- package/dist/state/TurnPreviewJournal.js.map +1 -0
- package/dist/state/migrations.js +10 -0
- package/dist/state/migrations.js.map +1 -1
- package/dist/updates/AutomaticUpdatePreparer.js +16 -7
- package/dist/updates/AutomaticUpdatePreparer.js.map +1 -1
- package/dist/updates/ManualUpdateController.js +102 -0
- package/dist/updates/ManualUpdateController.js.map +1 -0
- package/dist/updates/PublishedRelease.js +42 -4
- package/dist/updates/PublishedRelease.js.map +1 -1
- package/dist/utils/ProtocolLineWarning.js +15 -0
- package/dist/utils/ProtocolLineWarning.js.map +1 -0
- package/docs/technical-reference.md +21 -1
- package/docs/technical-reference.zh.md +21 -1
- package/npm-shrinkwrap.json +1401 -10
- package/package.json +3 -1
- package/skills/agent-bot/SKILL.md +9 -2
- package/src/acp/AcpJsonRpcConnection.ts +3 -1
- package/src/cli/SelfUpdateRunner.ts +3 -0
- package/src/cli/SelfUpdater.ts +2 -0
- package/src/codex/AppServerConnection.ts +3 -1
- package/src/codex/CodexEventMapper.ts +40 -3
- package/src/codex/CodexRuntime.ts +91 -4
- package/src/commands/CommandRouter.ts +4 -0
- package/src/commands/commandTypes.ts +1 -0
- package/src/feishu/CardRenderer.ts +47 -21
- package/src/feishu/FeishuConnector.ts +2 -0
- package/src/feishu/FeishuMarkdown.ts +69 -3
- package/src/feishu/FeishuMessageClient.ts +44 -0
- package/src/feishu/FeishuTurnPresenter.ts +107 -31
- package/src/feishu/RecentMessageContext.ts +90 -0
- package/src/feishu/types.ts +5 -0
- package/src/index.ts +62 -22
- package/src/local-files/DiagramPreview.ts +173 -0
- package/src/local-files/LocalFileViewerServer.ts +129 -2
- package/src/local-files/PreviewSyntaxHighlight.ts +58 -0
- package/src/local-files/TurnPreviewPage.ts +539 -57
- package/src/presentation/OutboundRouter.ts +25 -7
- package/src/presentation/TurnStateReducer.ts +153 -9
- package/src/presentation/turnReasoning.ts +21 -0
- package/src/presentation/turnViewTypes.ts +19 -1
- package/src/proxy/ProxySessionController.ts +453 -40
- package/src/runtime/types.ts +21 -0
- package/src/state/StateStore.ts +45 -2
- package/src/state/TurnPreviewJournal.ts +398 -0
- package/src/state/migrations.ts +10 -0
- package/src/updates/AutomaticUpdatePreparer.ts +21 -7
- package/src/updates/ManualUpdateController.ts +116 -0
- package/src/updates/PublishedRelease.ts +49 -3
- package/src/utils/ProtocolLineWarning.ts +13 -0
package/src/index.ts
CHANGED
|
@@ -29,9 +29,10 @@ import { ProxySessionController } from "./proxy/ProxySessionController.js";
|
|
|
29
29
|
import type { AgentEnvironmentContext } from "./runtime/agentEnvironment.js";
|
|
30
30
|
import { createAgentRuntimeRegistry } from "./runtime/createAgentRuntimeRegistry.js";
|
|
31
31
|
import { StateStore } from "./state/StateStore.js";
|
|
32
|
+
import { ManualUpdateController } from "./updates/ManualUpdateController.js";
|
|
32
33
|
import { DailyUpdateMonitor } from "./updates/DailyUpdateMonitor.js";
|
|
33
|
-
import { readLatestStableVersion, readReleaseNotes } from "./updates/PublishedRelease.js";
|
|
34
|
-
import { checkAutomaticUpdateSupport, prepareAutomaticUpdate } from "./updates/AutomaticUpdatePreparer.js";
|
|
34
|
+
import { readLatestStableVersion, readReleaseNotes, readPublishedReleases } from "./updates/PublishedRelease.js";
|
|
35
|
+
import { checkAutomaticUpdateSupport, prepareAutomaticUpdate, prepareSelectedUpdate } from "./updates/AutomaticUpdatePreparer.js";
|
|
35
36
|
import { StartupNotifier } from "./startup/StartupNotifier.js";
|
|
36
37
|
import { SessionMetadataHydrator } from "./startup/SessionMetadataHydrator.js";
|
|
37
38
|
import { startFeishu } from "./startup/startFeishu.js";
|
|
@@ -56,6 +57,7 @@ import {
|
|
|
56
57
|
import { refreshedSystemEnvironment } from "./supervision/systemEnvironment.js";
|
|
57
58
|
import {
|
|
58
59
|
assertSelfUpdatePlanPath,
|
|
60
|
+
finalizeSelfUpdatePlan,
|
|
59
61
|
launchSelfUpdateRunner,
|
|
60
62
|
readPendingSelfUpdate,
|
|
61
63
|
releaseSelfUpdatePlan,
|
|
@@ -75,15 +77,18 @@ const consoleOnly = process.env.AGENT_BOT_CONSOLE_ONLY === "1";
|
|
|
75
77
|
const config = loadConfig();
|
|
76
78
|
const transport = consoleOnly ? "console" : requireServerFeishuTransport(config.feishu);
|
|
77
79
|
const logger = createLogger(config);
|
|
78
|
-
const store = new StateStore(config.storage.sqlitePath);
|
|
80
|
+
const store = new StateStore(config.storage.sqlitePath, (error) => logger.error({ error }, "Failed to persist Turn preview journal."));
|
|
79
81
|
let localFileViewer: LocalFileViewerServer | undefined;
|
|
82
|
+
let loadTurnDetails: ((turnId: string) => Promise<unknown>) | undefined;
|
|
80
83
|
if (config.fileViewer.enabled) {
|
|
81
84
|
const viewer = new LocalFileViewerServer({
|
|
82
85
|
host: config.fileViewer.host,
|
|
83
86
|
port: config.fileViewer.port,
|
|
84
87
|
publicBaseUrl: config.fileViewer.publicBaseUrl,
|
|
85
88
|
stateDirectory: path.join(path.dirname(config.storage.sqlitePath), "file-viewer"),
|
|
89
|
+
previewJournal: store.previews,
|
|
86
90
|
getTurnSnapshot: (turnId) => store.getTurnSnapshot(turnId),
|
|
91
|
+
loadTurnSnapshot: (turnId) => loadTurnDetails?.(turnId) ?? Promise.resolve(store.getTurnSnapshot(turnId)),
|
|
87
92
|
});
|
|
88
93
|
try {
|
|
89
94
|
const address = await viewer.start();
|
|
@@ -153,6 +158,7 @@ const outbound = new OutboundRouter(routes);
|
|
|
153
158
|
let shuttingDown = false;
|
|
154
159
|
let restartRequested = false;
|
|
155
160
|
let pendingSelfUpdatePlanPath: string | undefined;
|
|
161
|
+
let preparingSelfUpdate = false;
|
|
156
162
|
const safeRestart = new SafeRestartScheduler({
|
|
157
163
|
readActivity: () => store.getServerActivityState(),
|
|
158
164
|
onReady: (reason, notificationTargets) => pendingSelfUpdatePlanPath
|
|
@@ -168,29 +174,62 @@ if (feishuOutbound && config.updates?.enabled !== false) {
|
|
|
168
174
|
readLatest: readLatestStableVersion,
|
|
169
175
|
readNotes: readReleaseNotes,
|
|
170
176
|
checkSupport: () => checkAutomaticUpdateSupport({ home: agentBotHome(), configPath: activeConfigPath }),
|
|
171
|
-
hasPendingUpdate: () => shuttingDown || restartRequested || !!pendingSelfUpdatePlanPath || safeRestart.scheduled,
|
|
177
|
+
hasPendingUpdate: () => preparingSelfUpdate || shuttingDown || restartRequested || !!pendingSelfUpdatePlanPath || safeRestart.scheduled,
|
|
172
178
|
pendingVersion: () => pendingSelfUpdatePlanPath ? readPendingSelfUpdate(agentBotHome())?.plan.toVersion : undefined,
|
|
173
|
-
applyUpdate:
|
|
174
|
-
const prepared = await prepareAutomaticUpdate(version, { home: agentBotHome(), configPath: activeConfigPath });
|
|
175
|
-
try {
|
|
176
|
-
if (shuttingDown || restartRequested || pendingSelfUpdatePlanPath || safeRestart.scheduled) {
|
|
177
|
-
throw new Error("服务正在停止或已有更新/重启等待执行,本次自动更新已停止。");
|
|
178
|
-
}
|
|
179
|
-
const target = privateRestartNotificationTarget(config.feishu.userOpenId);
|
|
180
|
-
if (prepared.status === "prepared") {
|
|
181
|
-
assertSelfUpdatePlanPath(prepared.planPath, agentBotHome());
|
|
182
|
-
pendingSelfUpdatePlanPath = prepared.planPath;
|
|
183
|
-
}
|
|
184
|
-
safeRestart.schedule(`Agent Bot 自动更新到 ${version}`, target);
|
|
185
|
-
} catch (error) {
|
|
186
|
-
if (prepared.status === "prepared") releaseSelfUpdatePlan(prepared.planPath);
|
|
187
|
-
throw error;
|
|
188
|
-
}
|
|
189
|
-
},
|
|
179
|
+
applyUpdate: (version) => prepareAndScheduleUpdate(version, privateRestartNotificationTarget(config.feishu.userOpenId)),
|
|
190
180
|
});
|
|
191
181
|
}
|
|
182
|
+
const manualUpdates = new ManualUpdateController({
|
|
183
|
+
store, outbound, logger, currentVersion: agentBotVersion,
|
|
184
|
+
ownerOpenId: () => config.feishu.userOpenId,
|
|
185
|
+
readReleases: readPublishedReleases,
|
|
186
|
+
checkSupport: () => checkAutomaticUpdateSupport({ home: agentBotHome(), configPath: activeConfigPath }),
|
|
187
|
+
hasPendingUpdate: () => preparingSelfUpdate || shuttingDown || restartRequested || !!pendingSelfUpdatePlanPath || safeRestart.scheduled,
|
|
188
|
+
applyUpdate: (version, contextKey, replyTarget) => prepareAndScheduleUpdate(version, {
|
|
189
|
+
contextKey,
|
|
190
|
+
...(replyTarget ? { replyMessageId: replyTarget.messageId } : {}),
|
|
191
|
+
}, true),
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
async function prepareAndScheduleUpdate(version: string, target: RestartNotificationTarget, manual = false): Promise<void> {
|
|
195
|
+
if (preparingSelfUpdate || shuttingDown || restartRequested || pendingSelfUpdatePlanPath || safeRestart.scheduled) {
|
|
196
|
+
throw new Error("已有更新或重启正在进行,请稍后重试。");
|
|
197
|
+
}
|
|
198
|
+
preparingSelfUpdate = true;
|
|
199
|
+
try {
|
|
200
|
+
const prepare = manual ? prepareSelectedUpdate : prepareAutomaticUpdate;
|
|
201
|
+
const prepared = await prepare(version, { home: agentBotHome(), configPath: activeConfigPath });
|
|
202
|
+
try {
|
|
203
|
+
if (shuttingDown || restartRequested || pendingSelfUpdatePlanPath || safeRestart.scheduled) {
|
|
204
|
+
throw new Error("服务正在停止或已有更新/重启等待执行,本次更新已停止。");
|
|
205
|
+
}
|
|
206
|
+
if (prepared.status === "prepared") {
|
|
207
|
+
assertSelfUpdatePlanPath(prepared.planPath, agentBotHome());
|
|
208
|
+
finalizeSelfUpdatePlan(prepared.planPath, {
|
|
209
|
+
controlEndpoint: controlEndpoint(config.storage.sqlitePath),
|
|
210
|
+
databasePath: config.storage.sqlitePath,
|
|
211
|
+
restartService: true,
|
|
212
|
+
notificationTarget: target,
|
|
213
|
+
});
|
|
214
|
+
pendingSelfUpdatePlanPath = prepared.planPath;
|
|
215
|
+
}
|
|
216
|
+
safeRestart.schedule(`Agent Bot ${manual ? "更新" : "自动更新"}到 ${version}`, target);
|
|
217
|
+
} catch (error) {
|
|
218
|
+
if (prepared.status === "prepared") {
|
|
219
|
+
if (pendingSelfUpdatePlanPath === prepared.planPath) pendingSelfUpdatePlanPath = undefined;
|
|
220
|
+
releaseSelfUpdatePlan(prepared.planPath);
|
|
221
|
+
}
|
|
222
|
+
throw error;
|
|
223
|
+
}
|
|
224
|
+
} finally {
|
|
225
|
+
preparingSelfUpdate = false;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
192
229
|
const controller = new ProxySessionController(config, store, runtimes, outbound, logger, {
|
|
193
230
|
supervised,
|
|
231
|
+
showUpdates: (contextKey, userId) => manualUpdates.show(contextKey, userId),
|
|
232
|
+
selectUpdate: (action, replyTarget) => manualUpdates.select(action, replyTarget),
|
|
194
233
|
restart: requestRestart,
|
|
195
234
|
forceSafeRestart: async (scheduleId) => {
|
|
196
235
|
if (shuttingDown || restartRequested) return false;
|
|
@@ -226,6 +265,7 @@ const controller = new ProxySessionController(config, store, runtimes, outbound,
|
|
|
226
265
|
logger.info({ agentName, changed }, "Stored Agent execution defaults.");
|
|
227
266
|
},
|
|
228
267
|
});
|
|
268
|
+
loadTurnDetails = (turnId) => controller.loadTurnDetails(turnId);
|
|
229
269
|
|
|
230
270
|
if (feishuOutbound) {
|
|
231
271
|
feishuConnector = new FeishuConnector(
|
|
@@ -278,7 +318,7 @@ function recoverPendingSelfUpdate(): void {
|
|
|
278
318
|
if (!pending || pendingSelfUpdatePlanPath || restartRequested || safeRestart.scheduled) return;
|
|
279
319
|
let notificationTarget: RestartNotificationTarget | undefined;
|
|
280
320
|
try {
|
|
281
|
-
notificationTarget = inferServerRestartTarget(pending.plan.notificationSessionId);
|
|
321
|
+
notificationTarget = pending.plan.notificationTarget ?? inferServerRestartTarget(pending.plan.notificationSessionId);
|
|
282
322
|
} catch (error) {
|
|
283
323
|
logger.warn({ error }, "Could not restore the self-update notification target; update recovery will continue.");
|
|
284
324
|
}
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import type MarkdownIt from "markdown-it";
|
|
2
|
+
|
|
3
|
+
export function enableDiagramFences(markdown: InstanceType<typeof MarkdownIt>): void {
|
|
4
|
+
const original = markdown.renderer.rules.fence!;
|
|
5
|
+
markdown.renderer.rules.fence = (tokens, index, options, environment, renderer) => {
|
|
6
|
+
const token = tokens[index]!;
|
|
7
|
+
if (!/^(mermaid|flowchart)$/iu.test(token.info.trim())) return original(tokens, index, options, environment, renderer);
|
|
8
|
+
const zh = environment?.language !== "en";
|
|
9
|
+
const source = markdown.utils.escapeHtml(token.content);
|
|
10
|
+
return `<div class="diagram" data-diagram data-diagram-size="fit" data-preview-key="diagram:${index}">
|
|
11
|
+
<div class="diagram-toolbar" role="group" aria-label="${zh ? "图表显示模式" : "Diagram display mode"}">
|
|
12
|
+
<button type="button" data-diagram-mode="preview" aria-pressed="true">${zh ? "预览" : "Preview"}</button>
|
|
13
|
+
<button type="button" data-diagram-mode="source" aria-pressed="false">${zh ? "源码" : "Source"}</button>
|
|
14
|
+
<button type="button" data-diagram-size-toggle hidden>${zh ? "原大" : "Actual size"}</button>
|
|
15
|
+
</div>
|
|
16
|
+
<p class="diagram-notice" role="status">${zh ? "正在渲染图表…" : "Rendering diagram…"}</p>
|
|
17
|
+
<div class="diagram-preview" role="img" aria-label="${zh ? "Mermaid 图表" : "Mermaid diagram"}"></div>
|
|
18
|
+
<pre class="diagram-source" hidden><code>${source}</code></pre>
|
|
19
|
+
<noscript><pre><code>${source}</code></pre></noscript>
|
|
20
|
+
</div>`;
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export const DIAGRAM_PREVIEW_CSS = `
|
|
25
|
+
.diagram { --diagram-max-height:min(640px, 70vh); border:1px solid var(--border, #d8dee8); border-radius:12px; overflow:hidden; margin:12px 0; }
|
|
26
|
+
.diagram-toolbar { display:flex; flex-wrap:wrap; gap:4px; padding:6px 8px; border-bottom:1px solid var(--border, #d8dee8); }
|
|
27
|
+
.diagram-toolbar button { font:inherit; font-size:12px; color:inherit; background:transparent; border:1px solid transparent; border-radius:6px; padding:3px 10px; cursor:pointer; }
|
|
28
|
+
.diagram-toolbar button[aria-pressed="true"] { border-color:currentColor; }
|
|
29
|
+
.diagram-toolbar [data-diagram-size-toggle] { margin-left:auto; }
|
|
30
|
+
.diagram-toolbar button:focus-visible { outline:2px solid #4285d4; outline-offset:1px; }
|
|
31
|
+
.diagram-notice { font-size:12px; opacity:.75; margin:8px 12px!important; }
|
|
32
|
+
.diagram-preview { padding:12px; overflow:auto; max-height:var(--diagram-max-height); }
|
|
33
|
+
.diagram-preview svg { display:block; margin:auto; max-width:none!important; height:auto; width:min(100%, var(--diagram-width, 100%))!important; }
|
|
34
|
+
.diagram[data-diagram-size="actual"] .diagram-preview svg { width:var(--diagram-width, 100%)!important; }
|
|
35
|
+
@media (max-width:640px), (pointer:coarse) {
|
|
36
|
+
.diagram { --diagram-max-height:min(480px, 55vh); }
|
|
37
|
+
@supports (height:1svh) { .diagram { --diagram-max-height:min(480px, 55svh); } }
|
|
38
|
+
}
|
|
39
|
+
.diagram .diagram-source { margin:0; border:0; border-radius:0; max-height:30em; overflow:auto; white-space:pre; }
|
|
40
|
+
.diagram [hidden] { display:none!important; }
|
|
41
|
+
@media (hover:none), (pointer:coarse) { .diagram-preview, .diagram-source { scrollbar-width:none; } .diagram-preview::-webkit-scrollbar, .diagram-source::-webkit-scrollbar { display:none; } }
|
|
42
|
+
`;
|
|
43
|
+
|
|
44
|
+
export const DIAGRAM_PREVIEW_CLIENT_SCRIPT = String.raw`(() => {
|
|
45
|
+
const zh = /^zh/i.test(document.documentElement.lang);
|
|
46
|
+
const loading = zh ? "正在渲染图表…" : "Rendering diagram…";
|
|
47
|
+
const failed = zh ? "图表暂时无法渲染,已显示源码。" : "Diagram could not be rendered. Showing source.";
|
|
48
|
+
const states = new WeakMap();
|
|
49
|
+
let library;
|
|
50
|
+
let sequence = 0;
|
|
51
|
+
const loadLibrary = () => {
|
|
52
|
+
if (!library) library = new Promise((resolve, reject) => {
|
|
53
|
+
const url = document.body.dataset.diagramScript;
|
|
54
|
+
if (!url) { reject(new Error("Diagram renderer unavailable")); return; }
|
|
55
|
+
const script = document.createElement("script");
|
|
56
|
+
script.src = url;
|
|
57
|
+
const timeout = setTimeout(() => {
|
|
58
|
+
script.remove();
|
|
59
|
+
reject(new Error("Diagram renderer timed out"));
|
|
60
|
+
}, 15000);
|
|
61
|
+
script.onload = () => {
|
|
62
|
+
clearTimeout(timeout);
|
|
63
|
+
const mermaid = window.mermaid;
|
|
64
|
+
if (!mermaid) { reject(new Error("Diagram renderer unavailable")); return; }
|
|
65
|
+
try {
|
|
66
|
+
mermaid.initialize({
|
|
67
|
+
startOnLoad: false, securityLevel: "strict", suppressErrorRendering: true,
|
|
68
|
+
maxTextSize: 50000, maxEdges: 500, htmlLabels: false,
|
|
69
|
+
flowchart: { htmlLabels: false },
|
|
70
|
+
theme: window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "default",
|
|
71
|
+
secure: ["securityLevel", "startOnLoad", "maxTextSize", "maxEdges", "suppressErrorRendering", "htmlLabels", "flowchart", "secure"],
|
|
72
|
+
});
|
|
73
|
+
resolve(mermaid);
|
|
74
|
+
} catch (error) { reject(error); }
|
|
75
|
+
};
|
|
76
|
+
script.onerror = () => { clearTimeout(timeout); script.remove(); reject(new Error("Diagram renderer unavailable")); };
|
|
77
|
+
document.head.appendChild(script);
|
|
78
|
+
}).catch((error) => { library = undefined; throw error; });
|
|
79
|
+
return library;
|
|
80
|
+
};
|
|
81
|
+
const show = (block, state) => {
|
|
82
|
+
const mode = state.error ? "source" : state.mode;
|
|
83
|
+
block.querySelector(".diagram-preview").hidden = mode !== "preview";
|
|
84
|
+
block.querySelector(".diagram-source").hidden = mode !== "source";
|
|
85
|
+
block.dataset.diagramSize = state.size;
|
|
86
|
+
const sizeButton = block.querySelector("[data-diagram-size-toggle]");
|
|
87
|
+
sizeButton.hidden = mode !== "preview" || state.rendered !== state.source;
|
|
88
|
+
sizeButton.textContent = state.size === "actual" ? (zh ? "适应" : "Fit") : (zh ? "原大" : "Actual size");
|
|
89
|
+
sizeButton.setAttribute("aria-label", state.size === "actual"
|
|
90
|
+
? (zh ? "适应容器宽度" : "Fit diagram to width")
|
|
91
|
+
: (zh ? "按原始尺寸查看图表" : "View diagram at actual size"));
|
|
92
|
+
const notice = block.querySelector(".diagram-notice");
|
|
93
|
+
notice.hidden = !state.error && (state.rendered === state.source || mode === "source");
|
|
94
|
+
notice.textContent = state.error ? failed : loading;
|
|
95
|
+
block.querySelectorAll("[data-diagram-mode]").forEach((button) => {
|
|
96
|
+
button.setAttribute("aria-pressed", String(button.dataset.diagramMode === mode));
|
|
97
|
+
});
|
|
98
|
+
};
|
|
99
|
+
const render = async (block, state, revision) => {
|
|
100
|
+
const source = state.source;
|
|
101
|
+
let container;
|
|
102
|
+
try {
|
|
103
|
+
// Keep model-authored Mermaid config from weakening the renderer's safety policy.
|
|
104
|
+
if (source.length > 50000 || /^\s*---(?:\r?\n|$)/u.test(source) || /%%\s*\{/u.test(source)) {
|
|
105
|
+
throw new Error("Diagram configuration or size is unsupported");
|
|
106
|
+
}
|
|
107
|
+
const mermaid = await loadLibrary();
|
|
108
|
+
if (!block.isConnected || state.revision !== revision) return;
|
|
109
|
+
container = document.createElement("div");
|
|
110
|
+
container.style.cssText = "position:absolute;left:-100000px;top:0;width:1000px;visibility:hidden;pointer-events:none";
|
|
111
|
+
document.body.appendChild(container);
|
|
112
|
+
const result = await mermaid.render("agentbot-diagram-" + (++sequence), source, container);
|
|
113
|
+
if (!block.isConnected || state.revision !== revision) return;
|
|
114
|
+
const template = document.createElement("template");
|
|
115
|
+
template.innerHTML = result.svg;
|
|
116
|
+
// Mermaid's strict renderer sanitizes SVG. Also prohibit links, images and callbacks.
|
|
117
|
+
template.content.querySelectorAll("script,foreignObject,iframe,object,embed,image").forEach((node) => node.remove());
|
|
118
|
+
template.content.querySelectorAll("*").forEach((node) => {
|
|
119
|
+
for (const attr of Array.from(node.attributes)) {
|
|
120
|
+
if (/^on/i.test(attr.name) || /^(href|xlink:href|src)$/i.test(attr.name) && !attr.value.startsWith("#")) node.removeAttribute(attr.name);
|
|
121
|
+
}
|
|
122
|
+
});
|
|
123
|
+
const svg = template.content.querySelector("svg");
|
|
124
|
+
if (!svg) throw new Error("Missing SVG");
|
|
125
|
+
const viewBox = (svg.getAttribute("viewBox") || "").split(/[ ,]+/).map(Number);
|
|
126
|
+
if (viewBox.length === 4 && viewBox.every(Number.isFinite) && viewBox[2] > 0 && viewBox[3] > 0) {
|
|
127
|
+
svg.style.setProperty("--diagram-width", viewBox[2] + "px");
|
|
128
|
+
}
|
|
129
|
+
block.querySelector(".diagram-preview").replaceChildren(template.content);
|
|
130
|
+
state.rendered = source;
|
|
131
|
+
state.error = false;
|
|
132
|
+
show(block, state);
|
|
133
|
+
} catch {
|
|
134
|
+
if (block.isConnected && state.revision === revision) { state.error = true; show(block, state); }
|
|
135
|
+
} finally { if (container) container.remove(); }
|
|
136
|
+
};
|
|
137
|
+
const schedule = (block, state) => {
|
|
138
|
+
clearTimeout(state.timer);
|
|
139
|
+
state.revision += 1;
|
|
140
|
+
if (state.mode !== "preview" || state.rendered === state.source) return;
|
|
141
|
+
const revision = state.revision;
|
|
142
|
+
state.timer = setTimeout(() => void render(block, state, revision), 180);
|
|
143
|
+
};
|
|
144
|
+
const refresh = (block) => {
|
|
145
|
+
const source = block.querySelector(".diagram-source code").textContent;
|
|
146
|
+
let state = states.get(block);
|
|
147
|
+
if (!state) {
|
|
148
|
+
state = { source, mode: "preview", size: "fit", revision: 0, error: false };
|
|
149
|
+
states.set(block, state);
|
|
150
|
+
block.querySelector("[data-diagram-size-toggle]").addEventListener("click", () => {
|
|
151
|
+
state.size = state.size === "actual" ? "fit" : "actual";
|
|
152
|
+
const preview = block.querySelector(".diagram-preview");
|
|
153
|
+
preview.scrollLeft = 0;
|
|
154
|
+
preview.scrollTop = 0;
|
|
155
|
+
show(block, state);
|
|
156
|
+
});
|
|
157
|
+
block.querySelectorAll("[data-diagram-mode]").forEach((button) => {
|
|
158
|
+
button.addEventListener("click", () => {
|
|
159
|
+
state.mode = button.dataset.diagramMode;
|
|
160
|
+
if (state.mode === "preview") state.error = false;
|
|
161
|
+
show(block, state);
|
|
162
|
+
schedule(block, state);
|
|
163
|
+
});
|
|
164
|
+
});
|
|
165
|
+
} else if (state.source === source) { return; }
|
|
166
|
+
else { state.source = source; state.error = false; }
|
|
167
|
+
show(block, state);
|
|
168
|
+
schedule(block, state);
|
|
169
|
+
};
|
|
170
|
+
const scan = () => document.querySelectorAll("[data-diagram]").forEach(refresh);
|
|
171
|
+
window.agentBotDiagrams = { refresh, scan };
|
|
172
|
+
scan();
|
|
173
|
+
})();`;
|
|
@@ -1,5 +1,7 @@
|
|
|
1
|
+
import { TurnPreviewProjection, type TurnPreviewJournal } from "../state/TurnPreviewJournal.js";
|
|
1
2
|
import { createHmac, randomBytes, timingSafeEqual } from "node:crypto";
|
|
2
3
|
import fs from "node:fs";
|
|
4
|
+
import { createRequire } from "node:module";
|
|
3
5
|
import http, { type IncomingMessage, type ServerResponse } from "node:http";
|
|
4
6
|
import path from "node:path";
|
|
5
7
|
import hljs from "highlight.js/lib/common";
|
|
@@ -16,10 +18,15 @@ import {
|
|
|
16
18
|
isTurnPreviewState,
|
|
17
19
|
detectTurnPreviewLanguage,
|
|
18
20
|
renderTurnPreviewPage,
|
|
21
|
+
renderTurnPreviewPatch,
|
|
22
|
+
renderToolFooter,
|
|
23
|
+
renderTurnPreviewDetail,
|
|
19
24
|
renderTurnPreviewSnapshot,
|
|
20
25
|
TURN_PREVIEW_CLIENT_SCRIPT,
|
|
21
26
|
} from "./TurnPreviewPage.js";
|
|
22
27
|
|
|
28
|
+
const require = createRequire(import.meta.url);
|
|
29
|
+
let mermaidSource: string | undefined;
|
|
23
30
|
const SECRET_FILE = "secret";
|
|
24
31
|
const PORT_FILE = "port";
|
|
25
32
|
const SHORT_TOKEN_BYTES = 12;
|
|
@@ -230,7 +237,9 @@ export interface LocalFileViewerServerOptions {
|
|
|
230
237
|
port: number;
|
|
231
238
|
publicBaseUrl?: string;
|
|
232
239
|
stateDirectory: string;
|
|
240
|
+
previewJournal?: TurnPreviewJournal;
|
|
233
241
|
getTurnSnapshot?: (turnId: string) => unknown;
|
|
242
|
+
loadTurnSnapshot?: (turnId: string) => Promise<unknown>;
|
|
234
243
|
turnPreviewPollIntervalMs?: number;
|
|
235
244
|
}
|
|
236
245
|
|
|
@@ -376,6 +385,11 @@ export class LocalFileViewerServer {
|
|
|
376
385
|
this.sendJavascript(response, request.method === "HEAD", TURN_PREVIEW_CLIENT_SCRIPT);
|
|
377
386
|
return;
|
|
378
387
|
}
|
|
388
|
+
if (route === "/assets/mermaid.js") {
|
|
389
|
+
mermaidSource ??= fs.readFileSync(require.resolve("mermaid/dist/mermaid.min.js"), "utf8");
|
|
390
|
+
this.sendJavascript(response, request.method === "HEAD", mermaidSource);
|
|
391
|
+
return;
|
|
392
|
+
}
|
|
379
393
|
const turnPreviewMatch = new RegExp(`^/turn-preview/([A-Za-z0-9_-]{${SHORT_TOKEN_LENGTH}})$`, "u").exec(route);
|
|
380
394
|
if (turnPreviewMatch) {
|
|
381
395
|
const turnId = this.verifyTurnPreviewToken(
|
|
@@ -386,23 +400,68 @@ export class LocalFileViewerServer {
|
|
|
386
400
|
this.sendHtml(response, 403, errorPage("Turn 链接无效", "签名校验失败,Agent Bot 已拒绝该请求。"), request.method === "HEAD");
|
|
387
401
|
return;
|
|
388
402
|
}
|
|
389
|
-
const
|
|
403
|
+
const journal = this.options.previewJournal;
|
|
404
|
+
const recorded = journal?.has(turnId);
|
|
405
|
+
if (recorded && requestUrl.searchParams.get("events") === "1") {
|
|
406
|
+
this.serveJournalEvents(request, response, turnId, journal!);
|
|
407
|
+
return;
|
|
408
|
+
}
|
|
409
|
+
const detailKey = requestUrl.searchParams.get("detail");
|
|
410
|
+
let snapshot = recorded
|
|
411
|
+
? journal!.load(turnId, detailKey ?? false)
|
|
412
|
+
: this.options.getTurnSnapshot?.(turnId);
|
|
413
|
+
if (!recorded && this.options.loadTurnSnapshot && request.method !== "HEAD"
|
|
414
|
+
&& requestUrl.searchParams.get("events") !== "1" && !requestUrl.searchParams.has("detail")) {
|
|
415
|
+
const saved = snapshot;
|
|
416
|
+
try {
|
|
417
|
+
snapshot = await this.options.loadTurnSnapshot(turnId);
|
|
418
|
+
} catch (error) {
|
|
419
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
420
|
+
if (!isTurnPreviewState(saved)) {
|
|
421
|
+
this.sendHtml(response, 502, errorPage("历史执行详情读取失败", `${message} 刷新页面重试;任务状态未改变。`), false);
|
|
422
|
+
return;
|
|
423
|
+
}
|
|
424
|
+
snapshot = { ...saved, historyDetailError: message };
|
|
425
|
+
}
|
|
426
|
+
}
|
|
390
427
|
if (!isTurnPreviewState(snapshot)) {
|
|
391
428
|
this.sendHtml(response, 404, errorPage("Turn 不存在", "这次执行可能尚未保存或已被清理。"), request.method === "HEAD");
|
|
392
429
|
return;
|
|
393
430
|
}
|
|
431
|
+
if (requestUrl.searchParams.has("detail")) {
|
|
432
|
+
const language = detectTurnPreviewLanguage(request.headers["accept-language"]);
|
|
433
|
+
const key = requestUrl.searchParams.get("detail") ?? "";
|
|
434
|
+
const tool = snapshot.activities.find((a) => a.kind === "tool" && `tool:${a.id}` === key);
|
|
435
|
+
const after = requestUrl.searchParams.get("detailAfter");
|
|
436
|
+
const outputAppend = recorded && after && tool?.kind === "tool" && tool.tool.kind === "command" && !tool.tool.error
|
|
437
|
+
&& !/repl/i.test(tool.tool.title) && snapshot.previewCursor !== undefined
|
|
438
|
+
? journal!.outputAppend(turnId, tool.id, Number(after), snapshot.previewCursor) : undefined;
|
|
439
|
+
const detail = outputAppend !== undefined && tool?.kind === "tool"
|
|
440
|
+
&& (snapshot.fullToolOutputs?.[tool.id]?.length ?? 0) > outputAppend.length
|
|
441
|
+
? { key, revision: String(tool.tool.previewRevision), content: "", outputAppend,
|
|
442
|
+
footer: renderToolFooter(tool.tool, snapshot.fullToolOutputs?.[tool.id]?.length ?? 0, language) }
|
|
443
|
+
: renderTurnPreviewDetail(snapshot, key, (filePath) => this.createFileUrl(filePath), language);
|
|
444
|
+
setSecurityHeaders(response);
|
|
445
|
+
response.statusCode = detail ? 200 : 404;
|
|
446
|
+
response.setHeader("Cache-Control", "no-store");
|
|
447
|
+
response.setHeader("Content-Type", "application/json; charset=utf-8");
|
|
448
|
+
response.end(request.method === "HEAD" ? undefined : JSON.stringify(detail ? { ...detail, cursor: snapshot.previewCursor } : { error: "Turn detail not found." }));
|
|
449
|
+
return;
|
|
450
|
+
}
|
|
394
451
|
if (requestUrl.searchParams.get("events") === "1") {
|
|
395
452
|
this.serveTurnEvents(request, response, turnId);
|
|
396
453
|
return;
|
|
397
454
|
}
|
|
398
455
|
const eventsUrl = new URL(this.createTurnPreviewUrl(turnId)!);
|
|
399
456
|
eventsUrl.searchParams.set("events", "1");
|
|
457
|
+
if (snapshot.previewCursor !== undefined) eventsUrl.searchParams.set("after", String(snapshot.previewCursor));
|
|
400
458
|
const language = detectTurnPreviewLanguage(request.headers["accept-language"]);
|
|
401
459
|
const html = renderTurnPreviewPage({
|
|
402
460
|
state: snapshot,
|
|
403
461
|
eventsUrl: eventsUrl.toString(),
|
|
404
462
|
scriptPath: `${this.basePath}/assets/turn-preview.js`.replace(/\/{2,}/gu, "/"),
|
|
405
463
|
localFileUrl: (filePath) => this.createFileUrl(filePath),
|
|
464
|
+
diagramScriptPath: `${this.basePath}/assets/mermaid.js`.replace(/\/{2,}/gu, "/"),
|
|
406
465
|
language,
|
|
407
466
|
});
|
|
408
467
|
this.sendHtml(response, 200, html, request.method === "HEAD");
|
|
@@ -683,6 +742,74 @@ export class LocalFileViewerServer {
|
|
|
683
742
|
response.once("close", cleanup);
|
|
684
743
|
}
|
|
685
744
|
|
|
745
|
+
private serveJournalEvents(request: IncomingMessage, response: ServerResponse, turnId: string, journal: TurnPreviewJournal): void {
|
|
746
|
+
journal.flush(turnId);
|
|
747
|
+
setSecurityHeaders(response);
|
|
748
|
+
response.writeHead(200, { "Content-Type": "text/event-stream; charset=utf-8", "Cache-Control": "no-cache, no-store", "X-Accel-Buffering": "no" });
|
|
749
|
+
if (request.method === "HEAD") { response.end(); return; }
|
|
750
|
+
response.flushHeaders();
|
|
751
|
+
const language = detectTurnPreviewLanguage(request.headers["accept-language"]);
|
|
752
|
+
const projection = new TurnPreviewProjection(false);
|
|
753
|
+
let offset = 0;
|
|
754
|
+
let closed = false;
|
|
755
|
+
let initial = true;
|
|
756
|
+
const cursor = request.headers["last-event-id"] ?? new URL(request.url!, "http://localhost").searchParams.get("after");
|
|
757
|
+
if (typeof cursor === "string" && /^\d+$/u.test(cursor) && Number.isSafeInteger(Number(cursor)) && Number(cursor) > 0
|
|
758
|
+
&& Number(cursor) <= fs.statSync(journal.file(turnId)).size) {
|
|
759
|
+
while (offset < Number(cursor)) {
|
|
760
|
+
const batch = journal.read(turnId, offset, 256 * 1024, Number(cursor));
|
|
761
|
+
for (const record of batch.records) projection.apply(record, ++projection.revision);
|
|
762
|
+
if (batch.offset === offset) break;
|
|
763
|
+
offset = batch.offset;
|
|
764
|
+
}
|
|
765
|
+
initial = offset !== Number(cursor);
|
|
766
|
+
}
|
|
767
|
+
let waitingDrain = false;
|
|
768
|
+
const poll = () => {
|
|
769
|
+
if (closed || waitingDrain) return;
|
|
770
|
+
try {
|
|
771
|
+
projection.changed.clear();
|
|
772
|
+
projection.removed.clear();
|
|
773
|
+
const batch = journal.read(turnId, offset);
|
|
774
|
+
if (!batch.records.length) {
|
|
775
|
+
const state = projection.state;
|
|
776
|
+
if (state && ["completed", "cancelled", "failed"].includes(state.status)) {
|
|
777
|
+
const update = renderTurnPreviewPatch(state, new Set(), new Set(), (file) => this.createFileUrl(file), language);
|
|
778
|
+
response.write(`id: ${offset}\nevent: patch\ndata: ${JSON.stringify(update)}\n\n`);
|
|
779
|
+
cleanup(); response.end();
|
|
780
|
+
}
|
|
781
|
+
return;
|
|
782
|
+
}
|
|
783
|
+
offset = batch.offset;
|
|
784
|
+
for (const record of batch.records) projection.apply(record, ++projection.revision);
|
|
785
|
+
const state = projection.state;
|
|
786
|
+
if (!state) return;
|
|
787
|
+
const render = initial
|
|
788
|
+
? renderTurnPreviewSnapshot(state, (file) => this.createFileUrl(file), language, { deferDetails: true })
|
|
789
|
+
: renderTurnPreviewPatch(state, projection.changed, projection.removed, (file) => this.createFileUrl(file), language);
|
|
790
|
+
// A terminal record can share a batch with later metadata. Drain the captured prefix first.
|
|
791
|
+
render.terminal = render.terminal && batch.end;
|
|
792
|
+
waitingDrain = !response.write(`id: ${offset}\nevent: ${initial ? "update" : "patch"}\ndata: ${JSON.stringify(render)}\n\n`);
|
|
793
|
+
initial = false;
|
|
794
|
+
if (render.terminal) { cleanup(); response.end(); }
|
|
795
|
+
else if (!batch.end) setImmediate(poll);
|
|
796
|
+
} catch (error) {
|
|
797
|
+
writeServerSentEvent(response, "unavailable", JSON.stringify({ message: "Turn preview could not be read." }));
|
|
798
|
+
cleanup();
|
|
799
|
+
response.end();
|
|
800
|
+
}
|
|
801
|
+
};
|
|
802
|
+
const poller = setInterval(poll, Math.max(50, this.options.turnPreviewPollIntervalMs ?? 1_000));
|
|
803
|
+
poller.unref();
|
|
804
|
+
const keepAlive = setInterval(() => { if (!closed && !waitingDrain) response.write(": keep-alive\n\n"); }, 15_000);
|
|
805
|
+
keepAlive.unref();
|
|
806
|
+
const cleanup = () => { closed = true; clearInterval(poller); clearInterval(keepAlive); };
|
|
807
|
+
response.on("drain", () => { waitingDrain = false; poll(); });
|
|
808
|
+
response.once("close", cleanup);
|
|
809
|
+
request.once("aborted", cleanup);
|
|
810
|
+
poll();
|
|
811
|
+
}
|
|
812
|
+
|
|
686
813
|
private serveTurnEvents(request: IncomingMessage, response: ServerResponse, turnId: string): void {
|
|
687
814
|
setSecurityHeaders(response);
|
|
688
815
|
response.statusCode = 200;
|
|
@@ -710,7 +837,7 @@ export class LocalFileViewerServer {
|
|
|
710
837
|
const serialized = JSON.stringify(state);
|
|
711
838
|
if (serialized === previousSnapshot) return;
|
|
712
839
|
previousSnapshot = serialized;
|
|
713
|
-
const snapshot = renderTurnPreviewSnapshot(state, (filePath) => this.createFileUrl(filePath), language);
|
|
840
|
+
const snapshot = renderTurnPreviewSnapshot(state, (filePath) => this.createFileUrl(filePath), language, { deferDetails: true });
|
|
714
841
|
writeServerSentEvent(response, "update", JSON.stringify(snapshot));
|
|
715
842
|
};
|
|
716
843
|
sendSnapshot();
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import hljs from "highlight.js/lib/common";
|
|
2
|
+
import dockerfile from "highlight.js/lib/languages/dockerfile";
|
|
3
|
+
import dos from "highlight.js/lib/languages/dos";
|
|
4
|
+
import powershell from "highlight.js/lib/languages/powershell";
|
|
5
|
+
|
|
6
|
+
hljs.registerLanguage("dockerfile", dockerfile);
|
|
7
|
+
hljs.registerLanguage("dos", dos);
|
|
8
|
+
hljs.registerLanguage("powershell", powershell);
|
|
9
|
+
|
|
10
|
+
const LANGUAGE_ALIASES: Readonly<Record<string, string>> = { zsh: "bash", pwsh: "powershell" };
|
|
11
|
+
export const MAX_PREVIEW_HIGHLIGHT_BYTES = 64 * 1024;
|
|
12
|
+
const MAX_CACHE_BYTES = 1024 * 1024;
|
|
13
|
+
const MAX_CACHE_ENTRIES = 32;
|
|
14
|
+
const cache = new Map<string, { html: string; bytes: number }>();
|
|
15
|
+
let cacheBytes = 0;
|
|
16
|
+
|
|
17
|
+
// Returning an empty string asks MarkdownIt to escape the original code unchanged.
|
|
18
|
+
export function highlightPreviewCode(value: string, language: string): string {
|
|
19
|
+
const name = language.trim().toLowerCase();
|
|
20
|
+
const resolved = Object.hasOwn(LANGUAGE_ALIASES, name) ? LANGUAGE_ALIASES[name]! : name;
|
|
21
|
+
if (!resolved || !hljs.getLanguage(resolved) || Buffer.byteLength(value, "utf8") > MAX_PREVIEW_HIGHLIGHT_BYTES) return "";
|
|
22
|
+
const key = `${resolved}\0${value}`;
|
|
23
|
+
const cached = cache.get(key);
|
|
24
|
+
if (cached) {
|
|
25
|
+
cache.delete(key);
|
|
26
|
+
cache.set(key, cached);
|
|
27
|
+
return cached.html;
|
|
28
|
+
}
|
|
29
|
+
try {
|
|
30
|
+
const html = hljs.highlight(value, { language: resolved, ignoreIllegals: true }).value;
|
|
31
|
+
const bytes = Buffer.byteLength(key, "utf8") + Buffer.byteLength(html, "utf8");
|
|
32
|
+
if (bytes <= MAX_CACHE_BYTES) {
|
|
33
|
+
while (cache.size >= MAX_CACHE_ENTRIES || cacheBytes + bytes > MAX_CACHE_BYTES) {
|
|
34
|
+
const oldest = cache.keys().next().value!;
|
|
35
|
+
cacheBytes -= cache.get(oldest)!.bytes;
|
|
36
|
+
cache.delete(oldest);
|
|
37
|
+
}
|
|
38
|
+
cache.set(key, { html, bytes });
|
|
39
|
+
cacheBytes += bytes;
|
|
40
|
+
}
|
|
41
|
+
return html;
|
|
42
|
+
} catch {
|
|
43
|
+
return "";
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export const PREVIEW_SYNTAX_CSS = `
|
|
48
|
+
.markdown pre { --syntax-comment:#667080; --syntax-keyword:#9639a5; --syntax-string:#087f5b; --syntax-number:#986801; --syntax-title:#005cc5; --syntax-deletion:#b31d28; }
|
|
49
|
+
.markdown pre .hljs-comment, .markdown pre .hljs-quote { color:var(--syntax-comment); }
|
|
50
|
+
.markdown pre .hljs-keyword, .markdown pre .hljs-selector-tag, .markdown pre .hljs-literal, .markdown pre .hljs-section, .markdown pre .hljs-link { color:var(--syntax-keyword); }
|
|
51
|
+
.markdown pre .hljs-string, .markdown pre .hljs-regexp, .markdown pre .hljs-name, .markdown pre .hljs-type, .markdown pre .hljs-attribute, .markdown pre .hljs-symbol, .markdown pre .hljs-bullet, .markdown pre .hljs-addition, .markdown pre .hljs-variable, .markdown pre .hljs-template-tag, .markdown pre .hljs-template-variable { color:var(--syntax-string); }
|
|
52
|
+
.markdown pre .hljs-number, .markdown pre .hljs-meta, .markdown pre .hljs-built_in, .markdown pre .hljs-builtin-name, .markdown pre .hljs-params, .markdown pre .hljs-attr { color:var(--syntax-number); }
|
|
53
|
+
.markdown pre .hljs-title { color:var(--syntax-title); }
|
|
54
|
+
.markdown pre .hljs-deletion { color:var(--syntax-deletion); }
|
|
55
|
+
@media (prefers-color-scheme:dark) {
|
|
56
|
+
.markdown pre { --syntax-comment:#8b949e; --syntax-keyword:#ff7b72; --syntax-string:#a5d6ff; --syntax-number:#d2a8ff; --syntax-title:#79c0ff; --syntax-deletion:#ffdcd7; }
|
|
57
|
+
}
|
|
58
|
+
`;
|