@keyou007/agent-bot 0.1.21 → 0.1.23
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 +13 -0
- package/README.md +16 -2
- package/README.zh.md +16 -2
- package/config.example.yaml +5 -0
- package/dist/config/loadConfig.js +4 -0
- package/dist/config/loadConfig.js.map +1 -1
- package/dist/config/schema.js +1 -0
- package/dist/config/schema.js.map +1 -1
- package/dist/feishu/CardRenderer.js +32 -3
- package/dist/feishu/CardRenderer.js.map +1 -1
- package/dist/feishu/FeishuMessageClient.js +2 -2
- package/dist/feishu/FeishuMessageClient.js.map +1 -1
- package/dist/index.js +44 -0
- package/dist/index.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 +133 -87
- package/dist/local-files/TurnPreviewPage.js.map +1 -1
- package/dist/proxy/ProxySessionController.js +12 -2
- package/dist/proxy/ProxySessionController.js.map +1 -1
- package/dist/state/StateStore.js +25 -0
- package/dist/state/StateStore.js.map +1 -1
- package/dist/state/migrations.js +12 -0
- package/dist/state/migrations.js.map +1 -1
- package/dist/updates/AutomaticUpdatePreparer.js +66 -0
- package/dist/updates/AutomaticUpdatePreparer.js.map +1 -0
- package/dist/updates/DailyUpdateMonitor.js +261 -0
- package/dist/updates/DailyUpdateMonitor.js.map +1 -0
- package/dist/updates/PublishedRelease.js +43 -0
- package/dist/updates/PublishedRelease.js.map +1 -0
- package/dist/updates/types.js +2 -0
- package/dist/updates/types.js.map +1 -0
- package/npm-shrinkwrap.json +2 -2
- package/package.json +1 -1
- package/skills/agent-bot/SKILL.md +2 -0
- package/src/config/loadConfig.ts +4 -0
- package/src/config/schema.ts +4 -1
- package/src/feishu/CardRenderer.ts +35 -3
- package/src/feishu/FeishuMessageClient.ts +2 -1
- package/src/feishu/types.ts +1 -1
- package/src/index.ts +40 -0
- package/src/local-files/LocalFileViewerServer.ts +23 -11
- package/src/local-files/MarkdownFileLinks.ts +12 -7
- package/src/local-files/TurnPreviewPage.ts +134 -88
- package/src/proxy/ProxySessionController.ts +14 -2
- package/src/state/StateStore.ts +31 -0
- package/src/state/migrations.ts +12 -0
- package/src/updates/AutomaticUpdatePreparer.ts +76 -0
- package/src/updates/DailyUpdateMonitor.ts +252 -0
- package/src/updates/PublishedRelease.ts +44 -0
- package/src/updates/types.ts +19 -0
|
@@ -2,7 +2,8 @@ import path from "node:path";
|
|
|
2
2
|
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
|
-
import { displayToolCommand, formatShellCommandForDisplay } from "../feishu/CardRenderer.js";
|
|
5
|
+
import { displayFilePath, displayToolCommand, formatShellCommandForDisplay, toolStatusIcon } 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) => {
|
|
@@ -161,7 +164,12 @@ export const TURN_PREVIEW_CLIENT_SCRIPT = `(() => {
|
|
|
161
164
|
const duration = hours > 0
|
|
162
165
|
? String(hours).padStart(2, "0") + ":" + String(minutes).padStart(2, "0") + ":" + String(remainder).padStart(2, "0")
|
|
163
166
|
: String(minutes).padStart(2, "0") + ":" + String(remainder).padStart(2, "0");
|
|
164
|
-
|
|
167
|
+
const shortDuration = hours > 0
|
|
168
|
+
? String(hours * 60 + minutes) + ":" + String(remainder).padStart(2, "0")
|
|
169
|
+
: minutes > 0
|
|
170
|
+
? String(minutes) + ":" + String(remainder).padStart(2, "0")
|
|
171
|
+
: remainder > 0 ? String(remainder) + "s" : "";
|
|
172
|
+
element.textContent = element.dataset.shortDuration === "1" ? shortDuration : labels.elapsed + duration;
|
|
165
173
|
});
|
|
166
174
|
};
|
|
167
175
|
updateElapsed();
|
|
@@ -284,9 +292,10 @@ export function renderTurnPreviewSnapshot(
|
|
|
284
292
|
if (!state.projectCwd || !path.isAbsolute(state.projectCwd)) return undefined;
|
|
285
293
|
return localFileUrl(path.resolve(state.projectCwd, filePath));
|
|
286
294
|
} : undefined;
|
|
287
|
-
const
|
|
295
|
+
const renderText = (value: string): string => renderMarkdown(value, state.projectCwd, localFileUrl);
|
|
296
|
+
const timeline = renderTimeline(state.activities ?? [], renderText, resolveFileUrl, state.fullToolOutputs, state.fullToolErrors, language, state.projectCwd);
|
|
288
297
|
const sections = [
|
|
289
|
-
state.prompt ? renderMessageActivity("prompt", state.prompt, "user prompt") : "",
|
|
298
|
+
state.prompt ? renderMessageActivity("prompt", state.prompt, "user prompt", renderText) : "",
|
|
290
299
|
state.plan.length > 0 ? renderPlan(state, language) : "",
|
|
291
300
|
state.activitiesTruncated
|
|
292
301
|
? `<div class="notice">${escapeHtml(labels.truncated)}</div>`
|
|
@@ -295,9 +304,9 @@ export function renderTurnPreviewSnapshot(
|
|
|
295
304
|
state.fileSummary.length > 0 ? renderFileSummary(state, language, resolveFileUrl) : "",
|
|
296
305
|
state.error ? `<section class="result error-result"><h2>${escapeHtml(labels.error)}</h2><pre>${escapeHtml(state.error)}</pre></section>` : "",
|
|
297
306
|
state.finalResponse
|
|
298
|
-
? `<section class="result final-result"><h2>${escapeHtml(labels.finalAnswer)}</h2><div class="markdown">${
|
|
307
|
+
? `<section class="result final-result"><h2>${escapeHtml(labels.finalAnswer)}</h2><div class="markdown">${renderText(state.finalResponse)}</div></section>`
|
|
299
308
|
: state.assistantText
|
|
300
|
-
? `<section class="result"><h2>${escapeHtml(labels.generating)}</h2><div class="markdown">${
|
|
309
|
+
? `<section class="result"><h2>${escapeHtml(labels.generating)}</h2><div class="markdown">${renderText(state.assistantText)}</div></section>`
|
|
301
310
|
: "",
|
|
302
311
|
].filter(Boolean).join("");
|
|
303
312
|
const terminal = isTerminal(state.status);
|
|
@@ -322,36 +331,40 @@ function renderPlan(state: TurnViewState, language: TurnPreviewLanguage): string
|
|
|
322
331
|
|
|
323
332
|
function renderTimeline(
|
|
324
333
|
activities: TurnActivity[],
|
|
334
|
+
renderText: (value: string) => string,
|
|
325
335
|
localFileUrl?: (filePath: string) => string | undefined,
|
|
326
336
|
fullToolOutputs?: Record<string, string>,
|
|
327
337
|
fullToolErrors?: Record<string, string>,
|
|
328
338
|
language: TurnPreviewLanguage = "zh",
|
|
339
|
+
projectCwd?: string,
|
|
329
340
|
): string {
|
|
330
341
|
return activities
|
|
331
342
|
.filter((activity): activity is Exclude<TurnActivity, { kind: "reasoning" }> => activity.kind !== "reasoning")
|
|
332
|
-
.map((activity) => renderActivity(activity, localFileUrl, fullToolOutputs, fullToolErrors, language))
|
|
343
|
+
.map((activity) => renderActivity(activity, renderText, localFileUrl, fullToolOutputs, fullToolErrors, language, projectCwd))
|
|
333
344
|
.join("");
|
|
334
345
|
}
|
|
335
346
|
|
|
336
347
|
function renderActivity(
|
|
337
348
|
activity: Exclude<TurnActivity, { kind: "reasoning" }>,
|
|
349
|
+
renderText: (value: string) => string,
|
|
338
350
|
localFileUrl?: (filePath: string) => string | undefined,
|
|
339
351
|
fullToolOutputs?: Record<string, string>,
|
|
340
352
|
fullToolErrors?: Record<string, string>,
|
|
341
353
|
language: TurnPreviewLanguage = "zh",
|
|
354
|
+
projectCwd?: string,
|
|
342
355
|
): string {
|
|
343
356
|
switch (activity.kind) {
|
|
344
357
|
case "assistant":
|
|
345
|
-
return renderMessageActivity(activity.id, activity.text, "commentary");
|
|
358
|
+
return renderMessageActivity(activity.id, activity.text, "commentary", renderText);
|
|
346
359
|
case "user":
|
|
347
|
-
return renderMessageActivity(activity.id, activity.text, "user");
|
|
360
|
+
return renderMessageActivity(activity.id, activity.text, "user", renderText);
|
|
348
361
|
case "tool":
|
|
349
|
-
return renderToolActivity(activity.id, activity.tool, localFileUrl, fullToolOutputs, fullToolErrors, language);
|
|
362
|
+
return renderToolActivity(activity.id, activity.tool, localFileUrl, fullToolOutputs, fullToolErrors, language, projectCwd);
|
|
350
363
|
}
|
|
351
364
|
}
|
|
352
365
|
|
|
353
|
-
function renderMessageActivity(id: string, text: string, kind: string): string {
|
|
354
|
-
return `<article class="activity message ${kind}" data-activity="${escapeAttribute(id)}"><div class="markdown">${
|
|
366
|
+
function renderMessageActivity(id: string, text: string, kind: string, renderText: (value: string) => string): string {
|
|
367
|
+
return `<article class="activity message ${kind}" data-activity="${escapeAttribute(id)}"><div class="markdown">${renderText(text)}</div></article>`;
|
|
355
368
|
}
|
|
356
369
|
|
|
357
370
|
function renderToolActivity(
|
|
@@ -361,6 +374,7 @@ function renderToolActivity(
|
|
|
361
374
|
fullToolOutputs?: Record<string, string>,
|
|
362
375
|
fullToolErrors?: Record<string, string>,
|
|
363
376
|
language: TurnPreviewLanguage = "zh",
|
|
377
|
+
projectCwd?: string,
|
|
364
378
|
): string {
|
|
365
379
|
const labels = previewLabels(language);
|
|
366
380
|
const command = tool.command ? (displayToolCommand(tool.command) || tool.command.trim()) : undefined;
|
|
@@ -370,29 +384,40 @@ function renderToolActivity(
|
|
|
370
384
|
: command
|
|
371
385
|
? formatToolCommand(command)
|
|
372
386
|
: undefined;
|
|
373
|
-
const
|
|
374
|
-
const title = commandInHeader ? displayCommand! : toolTitle(tool, command, repl, language);
|
|
387
|
+
const title = displayCommand ? summarizeToolCommand(displayCommand) : toolTitle(tool, command, repl, language);
|
|
375
388
|
const output = fullToolOutputs?.[tool.id] ?? tool.output;
|
|
376
389
|
const error = fullToolErrors?.[tool.id] ?? tool.error;
|
|
377
390
|
const sameError = Boolean(output && error && normalizeOutput(output) === normalizeOutput(error));
|
|
391
|
+
const displayOutput = output && repl && !sameError ? formatReplOutput(output) : output;
|
|
392
|
+
const displayError = sameError ? undefined : error;
|
|
378
393
|
const detailParts = [
|
|
379
|
-
displayCommand
|
|
380
|
-
|
|
381
|
-
? renderToolOutput(
|
|
394
|
+
displayCommand ? `<div class="tool-command"><span class="tool-command-prefix" aria-hidden="true">$</span><pre class="command-block${repl ? " repl-code" : ""}">${escapeHtml(displayCommand)}</pre></div>` : "",
|
|
395
|
+
displayOutput
|
|
396
|
+
? renderToolOutput(sameError ? labels.resultError : repl ? labels.result : labels.output, displayOutput, sameError, repl)
|
|
382
397
|
: "",
|
|
383
|
-
|
|
384
|
-
? renderToolOutput(
|
|
398
|
+
displayError
|
|
399
|
+
? renderToolOutput(labels.error, displayError, true, repl)
|
|
385
400
|
: "",
|
|
386
|
-
!output && !error && !tool.files?.length ? renderToolStatusRow(tool, language) : "",
|
|
387
401
|
renderToolImage(tool, localFileUrl, language),
|
|
388
402
|
tool.files?.length
|
|
389
|
-
? `<
|
|
403
|
+
? `<ul class="files" aria-label="${escapeAttribute(labels.files)}">${tool.files.map((file) => renderFileEntry(file, localFileUrl, projectCwd)).join("")}</ul>`
|
|
390
404
|
: "",
|
|
391
405
|
].filter(Boolean).join("");
|
|
392
|
-
const titleMarkup =
|
|
406
|
+
const titleMarkup = displayCommand
|
|
393
407
|
? `<code class="tool-command-title">${escapeHtml(title)}</code>`
|
|
394
408
|
: `<span class="tool-title">${escapeHtml(title)}</span>`;
|
|
395
|
-
|
|
409
|
+
const timing = renderToolTiming(tool, "header");
|
|
410
|
+
const statusLabel = toolStatusLabel(tool.status, language);
|
|
411
|
+
const icon = `<span class="tool-status-icon" role="img" aria-label="${escapeAttribute(statusLabel)}" title="${escapeAttribute(statusLabel)}">${toolStatusIcon(tool.status)}</span>`;
|
|
412
|
+
const status = `<span class="tool-state ${tool.status}">${escapeHtml(statusLabel)}</span>`;
|
|
413
|
+
const header = `<summary class="tool-header">${icon}<span class="tool-summary-content">${titleMarkup}</span><span class="tool-meta">${timing}</span></summary>`;
|
|
414
|
+
const startedAt = tool.startedAt === undefined ? undefined : new Date(tool.startedAt);
|
|
415
|
+
const startTime = startedAt
|
|
416
|
+
? `<time datetime="${startedAt.toISOString()}" title="${escapeAttribute(labels.startingTime)}">${new Intl.DateTimeFormat(language === "zh" ? "zh-CN" : "en-GB", { hour: "2-digit", minute: "2-digit", second: "2-digit", hourCycle: "h23" }).format(startedAt)}</time>`
|
|
417
|
+
: `<span title="${escapeAttribute(labels.startingTime)}">${escapeHtml(labels.unknown)}</span>`;
|
|
418
|
+
const characterCount = (displayOutput?.length ?? 0) + (displayError?.length ?? 0);
|
|
419
|
+
const footer = `<div class="tool-footer">${status}${startTime}${renderToolTiming(tool, "footer")}<span>${formatNumber(characterCount)} ${escapeHtml(labels.characters)}</span></div>`;
|
|
420
|
+
return `<article class="activity tool"><details class="tool-step" data-tool-status="${tool.status}" data-activity-id="${escapeAttribute(activityId)}">${header}<div class="tool-body"><div class="tool-content" data-scroll-id="${escapeAttribute(`${activityId}:content`)}" tabindex="0">${detailParts || `<div class="muted">${escapeHtml(labels.noToolDetails)}</div>`}</div>${footer}</div></details></article>`;
|
|
396
421
|
}
|
|
397
422
|
|
|
398
423
|
function toolTitle(tool: ToolState, command?: string, repl = false, language: TurnPreviewLanguage = "zh"): string {
|
|
@@ -402,6 +427,12 @@ function toolTitle(tool: ToolState, command?: string, repl = false, language: Tu
|
|
|
402
427
|
return title || tool.kind;
|
|
403
428
|
}
|
|
404
429
|
|
|
430
|
+
function summarizeToolCommand(command: string): string {
|
|
431
|
+
const summary = command.replace(/[\\`^][ \t]*\r?\n/gu, " ").replace(/\s+/gu, " ").trim();
|
|
432
|
+
if (summary.length <= 160) return summary;
|
|
433
|
+
return `${summary.slice(0, 157).trimEnd()}…`;
|
|
434
|
+
}
|
|
435
|
+
|
|
405
436
|
function isReplTool(tool: ToolState): boolean {
|
|
406
437
|
const title = tool.title.trim();
|
|
407
438
|
const titleLooksLikeToolName = title.length <= 120 && !/[\s"'`]/u.test(title);
|
|
@@ -512,36 +543,29 @@ function normalizeReplText(value: string): string {
|
|
|
512
543
|
}
|
|
513
544
|
|
|
514
545
|
function renderToolOutput(
|
|
515
|
-
activityId: string,
|
|
516
|
-
kind: string,
|
|
517
546
|
label: string,
|
|
518
547
|
value: string,
|
|
519
548
|
error = false,
|
|
520
549
|
repl = false,
|
|
521
|
-
language: TurnPreviewLanguage = "zh",
|
|
522
|
-
tool?: ToolState,
|
|
523
550
|
): string {
|
|
524
|
-
|
|
525
|
-
const displayValue = repl && !error ? formatReplOutput(value) : value;
|
|
526
|
-
const labels = previewLabels(language);
|
|
527
|
-
const status = tool?.status ?? "completed";
|
|
528
|
-
const timing = tool ? renderToolTiming(tool, language) : "";
|
|
529
|
-
return `<details class="tool-output${error ? " error-output" : ""}${repl ? " repl-result" : ""}" data-activity-id="${id}"><summary><span class="tool-output-label">${escapeHtml(label)}</span><span class="tool-output-meta">${timing}<span class="tool-state ${status}" aria-label="${escapeAttribute(status)}">${toolStatusIcon(status, language)}</span><span>${formatNumber(displayValue.length)} ${escapeHtml(labels.characters)}</span></span></summary><pre data-scroll-id="${id}">${escapeHtml(displayValue)}</pre></details>`;
|
|
551
|
+
return `<pre class="tool-output${error ? " error-output" : ""}${repl ? " repl-result" : ""}" aria-label="${escapeAttribute(label)}">${escapeHtml(value)}</pre>`;
|
|
530
552
|
}
|
|
531
553
|
|
|
532
|
-
function
|
|
533
|
-
const
|
|
534
|
-
|
|
554
|
+
function renderToolTiming(tool: ToolState, placement: "header" | "footer"): string {
|
|
555
|
+
const className = `tool-${placement}-timing`;
|
|
556
|
+
const duration = toolDuration(tool);
|
|
557
|
+
if (duration === undefined) {
|
|
558
|
+
return `<span class="${className}">?</span>`;
|
|
559
|
+
}
|
|
560
|
+
const live = tool.status === "running" && tool.completedAt === undefined;
|
|
561
|
+
return `<span class="${className}"${live ? ` data-live-tool-duration data-started-at="${tool.startedAt}" data-short-duration="1"` : ""}>${formatShortDuration(duration)}</span>`;
|
|
535
562
|
}
|
|
536
563
|
|
|
537
|
-
function
|
|
538
|
-
const
|
|
539
|
-
const
|
|
540
|
-
const
|
|
541
|
-
|
|
542
|
-
? `<span>${escapeHtml(labels.elapsed.trim())}${language === "zh" ? "" : " "}${escapeHtml(labels.unknown)}</span>`
|
|
543
|
-
: `<span${tool.status === "running" && tool.completedAt === undefined ? ` data-live-tool-duration data-started-at="${tool.startedAt}"` : ""}>${escapeHtml(labels.elapsed)}${escapeHtml(duration)}</span>`;
|
|
544
|
-
return `<span title="${escapeAttribute(labels.startingTime)}">${escapeHtml(start)}</span>${liveDuration}`;
|
|
564
|
+
function formatShortDuration(duration: string): string {
|
|
565
|
+
const parts = duration.split(":").map(Number);
|
|
566
|
+
const seconds = parts.at(-1) ?? 0;
|
|
567
|
+
const minutes = parts.length > 2 ? (parts.at(-2) ?? 0) + (parts.at(-3) ?? 0) * 60 : parts.at(-2) ?? 0;
|
|
568
|
+
return minutes > 0 ? `${minutes}:${String(seconds).padStart(2, "0")}` : seconds > 0 ? `${seconds}s` : "";
|
|
545
569
|
}
|
|
546
570
|
|
|
547
571
|
function renderToolImage(
|
|
@@ -567,12 +591,12 @@ function renderFileSummary(
|
|
|
567
591
|
localFileUrl?: (filePath: string) => string | undefined,
|
|
568
592
|
): string {
|
|
569
593
|
const labels = previewLabels(language);
|
|
570
|
-
const files = state.fileSummary.map((file) => renderFileEntry(file, localFileUrl)).join("");
|
|
571
|
-
return `<
|
|
594
|
+
const files = state.fileSummary.map((file) => renderFileEntry(file, localFileUrl, state.projectCwd)).join("");
|
|
595
|
+
return `<details class="files-summary" data-activity-id="turn:files-summary"><summary><h2>${escapeHtml(labels.fileChanges)} <span>${state.fileSummary.length}</span></h2></summary><ul class="files">${files}</ul></details>`;
|
|
572
596
|
}
|
|
573
597
|
|
|
574
|
-
function renderFileEntry(file: FileSummary, localFileUrl?: (filePath: string) => string | undefined): string {
|
|
575
|
-
const label = `<code>${escapeHtml(file.path)}</code>`;
|
|
598
|
+
function renderFileEntry(file: FileSummary, localFileUrl?: (filePath: string) => string | undefined, projectCwd?: string): string {
|
|
599
|
+
const label = `<code>${escapeHtml(displayFilePath(file.path, projectCwd))}</code>`;
|
|
576
600
|
const url = localFileUrl?.(file.path);
|
|
577
601
|
const link = url
|
|
578
602
|
? `<a class="file-link" href="${escapeAttribute(url)}" target="_blank" rel="noreferrer noopener">${label}</a>`
|
|
@@ -608,15 +632,23 @@ function previewTitle(state: TurnViewState): string {
|
|
|
608
632
|
return value.length > 80 ? `${value.slice(0, 79)}…` : value;
|
|
609
633
|
}
|
|
610
634
|
|
|
611
|
-
function renderMarkdown(value: string): string {
|
|
612
|
-
|
|
635
|
+
function renderMarkdown(value: string, projectCwd?: string, localFileUrl?: (filePath: string) => string | undefined): string {
|
|
636
|
+
const environment = {};
|
|
637
|
+
const tokens = MARKDOWN.parse(value, environment);
|
|
638
|
+
// Only the directory is used to resolve relative links; no Markdown file is created.
|
|
639
|
+
const markdownPath = projectCwd && path.isAbsolute(projectCwd) ? path.join(projectCwd, "turn.md") : undefined;
|
|
640
|
+
rewriteMarkdownFileLinks(tokens, markdownPath, (filePath) => {
|
|
641
|
+
const url = localFileUrl?.(filePath);
|
|
642
|
+
return url ? new URL(url) : undefined;
|
|
643
|
+
});
|
|
644
|
+
return MARKDOWN.renderer.render(tokens, MARKDOWN.options, environment);
|
|
613
645
|
}
|
|
614
646
|
|
|
615
647
|
function statusLabel(status: TurnViewStatus, language: TurnPreviewLanguage): string {
|
|
616
648
|
return previewLabels(language).statuses[status];
|
|
617
649
|
}
|
|
618
650
|
|
|
619
|
-
function
|
|
651
|
+
function toolStatusLabel(status: ToolState["status"], language: TurnPreviewLanguage): string {
|
|
620
652
|
return status === "completed"
|
|
621
653
|
? previewLabels(language).toolStatuses.completed
|
|
622
654
|
: status === "failed"
|
|
@@ -640,15 +672,6 @@ function formatDuration(durationMs: number): string {
|
|
|
640
672
|
: `${String(minutes).padStart(2, "0")}:${String(remainder).padStart(2, "0")}`;
|
|
641
673
|
}
|
|
642
674
|
|
|
643
|
-
function formatTimestamp(timestampMs: number): string {
|
|
644
|
-
return new Intl.DateTimeFormat(undefined, {
|
|
645
|
-
hour: "2-digit",
|
|
646
|
-
minute: "2-digit",
|
|
647
|
-
second: "2-digit",
|
|
648
|
-
hour12: false,
|
|
649
|
-
}).format(new Date(timestampMs));
|
|
650
|
-
}
|
|
651
|
-
|
|
652
675
|
function formatNumber(value: number): string {
|
|
653
676
|
return Math.max(0, Math.floor(value)).toLocaleString("en-US");
|
|
654
677
|
}
|
|
@@ -673,7 +696,7 @@ function escapeAttribute(value: string): string {
|
|
|
673
696
|
}
|
|
674
697
|
|
|
675
698
|
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; }
|
|
699
|
+
: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
700
|
* { box-sizing: border-box; letter-spacing: 0; }
|
|
678
701
|
body { margin: 0; background: var(--page); color: var(--text); font-size: 13px; line-height: 1.55; }
|
|
679
702
|
.page-header { position: sticky; top: 0; z-index: 3; background: var(--page); border-bottom: 1px solid var(--line); }
|
|
@@ -702,16 +725,21 @@ h2 span { margin-left: 6px; color: var(--muted); font-size: 12px; font-weight: 4
|
|
|
702
725
|
.metadata > span + span::before { content: "·"; position: absolute; left: 0; top: 0; }
|
|
703
726
|
.metadata strong { color: var(--text); font-weight: 500; font-variant-numeric: tabular-nums; }
|
|
704
727
|
main { max-width: 1320px; margin: 0 auto; padding: 0 20px 40px; }
|
|
705
|
-
.plan, .files-summary, .result { padding: 14px 0;
|
|
728
|
+
.plan, .files-summary, .result { padding: 14px 0; }
|
|
729
|
+
.files-summary, .result { border-top: 1px solid var(--line); }
|
|
730
|
+
.files-summary > summary h2 { margin: 0; }
|
|
731
|
+
.files-summary > .files { padding-top: 8px; }
|
|
706
732
|
.plan ol { display: grid; gap: 4px; margin: 0; padding: 0; list-style: none; }
|
|
707
733
|
.plan li { display: grid; grid-template-columns: 16px minmax(0, 1fr); gap: 6px; overflow-wrap: anywhere; }
|
|
708
734
|
.plan li.completed { color: var(--muted); }
|
|
709
735
|
.plan li.in_progress { color: var(--accent); }
|
|
710
736
|
.timeline { padding-top: 2px; }
|
|
711
737
|
.activity { min-width: 0; }
|
|
712
|
-
.message { padding: 12px 0;
|
|
738
|
+
.message { padding: 12px 0; }
|
|
739
|
+
.prompt { border-bottom: 1px solid var(--line); }
|
|
740
|
+
.timeline .markdown hr { display: none; }
|
|
713
741
|
.markdown { min-width: 0; overflow-wrap: anywhere; }
|
|
714
|
-
.
|
|
742
|
+
.message > .markdown, .result > .markdown { font-size: 14px; font-weight: 400; }
|
|
715
743
|
.markdown > :first-child { margin-top: 0; }
|
|
716
744
|
.markdown > :last-child { margin-bottom: 0; }
|
|
717
745
|
.markdown p, .markdown ul, .markdown ol, .markdown pre, .markdown blockquote { margin: 0 0 6px; }
|
|
@@ -734,32 +762,52 @@ summary:hover { background: transparent; }
|
|
|
734
762
|
summary:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }
|
|
735
763
|
.tool { margin: 8px 0; }
|
|
736
764
|
.tool-step { border: 1px solid var(--line); border-radius: 8px; overflow: hidden; background: transparent; }
|
|
737
|
-
.tool-
|
|
738
|
-
.tool-
|
|
765
|
+
.tool-step[data-tool-status="running"] { border-color: var(--accent); }
|
|
766
|
+
.tool-header { display: flex; align-items: center; gap: 8px; min-height: 32px; padding: 7px 10px; overflow: hidden; background: transparent; font-weight: 400; }
|
|
767
|
+
.tool-content { max-height: calc(30 * 12px * 1.55 + 16px); padding: 8px 12px; overflow: auto; --tool-scrollbar-thumb: var(--scrollbar-thumb); scrollbar-width: thin; scrollbar-color: var(--tool-scrollbar-thumb) transparent; }
|
|
768
|
+
.tool-content > * + * { margin-top: 8px; }
|
|
769
|
+
.tool-content pre { max-width: none; max-height: none; padding: 0; overflow: visible; background: transparent; }
|
|
770
|
+
.tool-command { display: flex; align-items: flex-start; gap: 8px; width: max-content; min-width: 100%; }
|
|
771
|
+
.tool-command-prefix { flex: none; color: var(--muted); font: 12px/1.55 "Cascadia Mono", "SFMono-Regular", Consolas, monospace; }
|
|
772
|
+
.tool-footer { display: flex; flex-wrap: wrap; align-items: center; gap: 4px 8px; padding: 6px 12px; color: var(--muted); font-size: 11px; font-variant-numeric: tabular-nums; }
|
|
773
|
+
.tool-footer > * { white-space: nowrap; }
|
|
774
|
+
.tool-footer > * + *::before { content: "·"; margin-right: 8px; }
|
|
775
|
+
@media (hover: hover) and (pointer: fine) {
|
|
776
|
+
.tool-content { --tool-scrollbar-thumb: transparent; }
|
|
777
|
+
.tool-content:is(:hover, :focus-visible, :focus-within) { --tool-scrollbar-thumb: var(--scrollbar-thumb); }
|
|
778
|
+
}
|
|
779
|
+
@supports selector(::-webkit-scrollbar) {
|
|
780
|
+
.tool-content { scrollbar-width: auto; scrollbar-color: auto; }
|
|
781
|
+
.tool-content::-webkit-scrollbar { width: 6px; height: 6px; }
|
|
782
|
+
.tool-content::-webkit-scrollbar-track,
|
|
783
|
+
.tool-content::-webkit-scrollbar-corner { background: transparent; }
|
|
784
|
+
.tool-content::-webkit-scrollbar-thumb { background: var(--tool-scrollbar-thumb); border-radius: 3px; }
|
|
785
|
+
.tool-content::-webkit-scrollbar-thumb:hover { background: var(--scrollbar-thumb-hover); }
|
|
786
|
+
.tool-content::-webkit-scrollbar-button { display: none; width: 0; height: 0; }
|
|
787
|
+
}
|
|
788
|
+
@media (hover: none), (pointer: coarse) {
|
|
789
|
+
.tool-content { scrollbar-width: none; }
|
|
790
|
+
.tool-content::-webkit-scrollbar { display: none; width: 0; height: 0; }
|
|
791
|
+
}
|
|
792
|
+
.tool-status-icon { display: inline-flex; flex: 0 0 16px; align-items: center; justify-content: center; font-size: 13px; line-height: 18px; }
|
|
793
|
+
.tool-state { display: inline-flex; flex: none; align-items: center; justify-content: center; color: var(--muted); font-size: 11px; font-weight: 400; white-space: nowrap; }
|
|
739
794
|
.tool-state.completed { color: var(--success); }
|
|
740
795
|
.tool-state.running { color: var(--accent); }
|
|
741
796
|
.tool-state.failed { color: var(--danger); }
|
|
742
|
-
.tool-
|
|
743
|
-
.tool-command-title { display: block; min-width: 0;
|
|
744
|
-
.tool-
|
|
797
|
+
.tool-summary-content { min-width: 0; flex: 1 1 auto; overflow: hidden; }
|
|
798
|
+
.tool-title, .tool-command-title { display: block; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
799
|
+
.tool-title { font-size: 12px; font-weight: 400; line-height: 1.45; }
|
|
800
|
+
.tool-command-title { margin: 0; padding: 0; background: transparent; color: var(--text); font: 12px/1.55 "Cascadia Mono", "SFMono-Regular", Consolas, monospace; font-weight: 400; }
|
|
801
|
+
.tool-meta { display: flex; flex: 0 0 auto; flex-wrap: nowrap; align-items: center; gap: 12px; max-width: none; margin-left: auto; color: var(--muted); font-size: 11px; font-weight: 400; font-variant-numeric: tabular-nums; white-space: nowrap; }
|
|
745
802
|
.tool-meta span { white-space: nowrap; }
|
|
746
|
-
.tool-
|
|
747
|
-
.tool-
|
|
748
|
-
.
|
|
749
|
-
.tool-output:first-child { border-top: 0; }
|
|
750
|
-
.tool-output > summary { padding: 4px 12px; background: transparent; color: var(--muted); font-size: 12px; }
|
|
751
|
-
.tool-output-label { min-width: 0; }
|
|
752
|
-
.tool-output-meta { display: inline-flex; align-items: center; gap: 12px; margin-left: auto; font-size: 11px; }
|
|
753
|
-
.tool-status-row { display: flex; justify-content: flex-end; padding: 5px 12px; border-top: 1px solid var(--line); }
|
|
754
|
-
.tool-output > pre { max-height: none; border-top: 1px solid var(--line); background: transparent; }
|
|
755
|
-
.repl-code { background: transparent; }
|
|
756
|
-
.repl-result > pre { white-space: pre; overflow-wrap: normal; background: transparent; }
|
|
757
|
-
.error-output > summary, .error-result h2 { color: var(--danger); }
|
|
803
|
+
.tool-header-timing { min-width: 0; overflow: hidden; text-overflow: ellipsis; }
|
|
804
|
+
.tool-header-timing:empty, .tool-footer-timing:empty { display: none; }
|
|
805
|
+
.error-output, .error-result h2 { color: var(--danger); }
|
|
758
806
|
.muted, .empty { color: var(--muted); }
|
|
759
|
-
.notice, .empty { padding: 10px 0;
|
|
807
|
+
.notice, .empty { padding: 10px 0; }
|
|
760
808
|
.notice { color: #926a08; }
|
|
761
809
|
.files { display: grid; gap: 4px; margin: 0; padding: 0; list-style: none; }
|
|
762
|
-
.tool-
|
|
810
|
+
.tool-content .files code { background: transparent; }
|
|
763
811
|
.files li { display: flex; align-items: baseline; justify-content: space-between; gap: 12px; }
|
|
764
812
|
.files code { min-width: 0; }
|
|
765
813
|
.files .file-link { min-width: 0; color: var(--accent); text-underline-offset: 2px; }
|
|
@@ -767,7 +815,6 @@ summary:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px;
|
|
|
767
815
|
.additions { color: var(--success); }
|
|
768
816
|
.deletions { color: var(--danger); }
|
|
769
817
|
.file-delta { display: inline-flex; flex: none; gap: 6px; font: 12px "Cascadia Mono", Consolas, monospace; }
|
|
770
|
-
.tool-image { padding: 8px 12px; border-top: 1px solid var(--line); }
|
|
771
818
|
.tool-image img { display: block; max-width: 100%; max-height: 720px; }
|
|
772
819
|
.final-result h2 { color: var(--success); }
|
|
773
820
|
@media (max-width: 700px) {
|
|
@@ -780,13 +827,12 @@ summary:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px;
|
|
|
780
827
|
.message { padding: 10px 0; }
|
|
781
828
|
.tool-header { gap: 6px; padding: 7px 9px; }
|
|
782
829
|
.tool-state { font-size: 11px; }
|
|
783
|
-
.tool-meta {
|
|
784
|
-
.tool-
|
|
785
|
-
.tool-output-meta { gap: 10px; }
|
|
830
|
+
.tool-meta { gap: 10px; font-size: 11px; }
|
|
831
|
+
.tool-content, .tool-footer { padding-left: 10px; padding-right: 10px; }
|
|
786
832
|
pre { padding: 8px 10px; }
|
|
787
833
|
}
|
|
788
834
|
@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; }
|
|
835
|
+
: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
836
|
.notice, .status.waiting_for_approval { color: #e1bc6f; }
|
|
791
837
|
}
|
|
792
838
|
`;
|
|
@@ -487,6 +487,7 @@ export interface ProxyLifecycle {
|
|
|
487
487
|
supervised?: boolean;
|
|
488
488
|
restart(contextKey: string, force: boolean, replyTarget?: MessageReplyTarget): Promise<void>;
|
|
489
489
|
cancelSafeRestart?(scheduleId: number): Promise<boolean>;
|
|
490
|
+
cancelAutomaticUpdate?(action: CardAction): Promise<void>;
|
|
490
491
|
rememberFeishuUserOpenId?(userOpenId: string): Promise<void> | void;
|
|
491
492
|
persistAgentExecutionDefaults?(
|
|
492
493
|
agentName: string,
|
|
@@ -975,7 +976,10 @@ export class ProxySessionController {
|
|
|
975
976
|
await this.outbound.withReplyTarget(contextKey, replyTarget, async () => {
|
|
976
977
|
try {
|
|
977
978
|
const kind = String(scopedAction.value.action ?? "");
|
|
978
|
-
if (kind === "
|
|
979
|
+
if (kind === "agentbot_update_cancel") {
|
|
980
|
+
if (!this.lifecycle?.cancelAutomaticUpdate) throw new Error("自动更新功能未启用。");
|
|
981
|
+
await this.lifecycle.cancelAutomaticUpdate(scopedAction);
|
|
982
|
+
} else if (kind === "help_command") {
|
|
979
983
|
await this.executeHelpCommandAction(scopedAction, contextKey, replyTarget);
|
|
980
984
|
} else if (kind === "turn_details") {
|
|
981
985
|
await this.outbound.showDetails(contextKey, String(scopedAction.value.turnId ?? ""));
|
|
@@ -1708,6 +1712,7 @@ export class ProxySessionController {
|
|
|
1708
1712
|
await loaded.runtime.setReasoningEffort(record.localSessionId, nextEffort);
|
|
1709
1713
|
}
|
|
1710
1714
|
this.store.updateRuntimeSession(record.localSessionId, { model: nextValue, reasoningEffort: nextEffort });
|
|
1715
|
+
this.outbound.updateSessionModel(record.localSessionId, nextValue);
|
|
1711
1716
|
} else if (setting === "thinking" && nextValue) {
|
|
1712
1717
|
const models = await loaded.runtime.listModels(loaded.session.modelProvider, loaded.session.model);
|
|
1713
1718
|
const currentModel = models.find((candidate) => candidate.id === loaded.session.model)
|
|
@@ -3734,6 +3739,10 @@ export class ProxySessionController {
|
|
|
3734
3739
|
while (true) {
|
|
3735
3740
|
let pendingTurnId: string | undefined;
|
|
3736
3741
|
try {
|
|
3742
|
+
this.outbound.updateSessionModel(
|
|
3743
|
+
loaded.record.localSessionId,
|
|
3744
|
+
loaded.runtime.getSession(loaded.record.localSessionId)?.model ?? loaded.session.model,
|
|
3745
|
+
);
|
|
3737
3746
|
pendingTurnId = await this.outbound.startPendingTurn(
|
|
3738
3747
|
loaded.record.localSessionId,
|
|
3739
3748
|
loaded.record.contextKey,
|
|
@@ -5187,7 +5196,9 @@ export class ProxySessionController {
|
|
|
5187
5196
|
throw error;
|
|
5188
5197
|
}
|
|
5189
5198
|
};
|
|
5190
|
-
|
|
5199
|
+
const session = await loaded.runtime.setExecutionSettings(loaded.record.localSessionId, settings, persist);
|
|
5200
|
+
this.outbound.updateSessionModel(loaded.record.localSessionId, session.model);
|
|
5201
|
+
return session;
|
|
5191
5202
|
}
|
|
5192
5203
|
|
|
5193
5204
|
private async assertProviderModelSettings(
|
|
@@ -5249,6 +5260,7 @@ export class ProxySessionController {
|
|
|
5249
5260
|
await loaded.runtime.setReasoningEffort(loaded.record.localSessionId, nextEffort);
|
|
5250
5261
|
}
|
|
5251
5262
|
this.store.updateRuntimeSession(loaded.record.localSessionId, { model, reasoningEffort: nextEffort });
|
|
5263
|
+
this.outbound.updateSessionModel(loaded.record.localSessionId, model);
|
|
5252
5264
|
await this.persistAgentExecutionDefaults(loaded.record.localSessionId);
|
|
5253
5265
|
const effortMessage = nextEffort && nextEffort !== currentEffort
|
|
5254
5266
|
? `,思考强度已自动调整为 ${nextEffort}`
|
package/src/state/StateStore.ts
CHANGED
|
@@ -2,6 +2,7 @@ import fs from "node:fs";
|
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import Database from "better-sqlite3";
|
|
4
4
|
import { migrations } from "./migrations.js";
|
|
5
|
+
import type { UpdateCheckSchedule, UpdateNotice } from "../updates/types.js";
|
|
5
6
|
|
|
6
7
|
export type SessionStatus = "starting" | "ready" | "running" | "closed" | "failed";
|
|
7
8
|
|
|
@@ -287,6 +288,36 @@ export class StateStore {
|
|
|
287
288
|
`);
|
|
288
289
|
}
|
|
289
290
|
|
|
291
|
+
getUpdateCheckSchedule(): UpdateCheckSchedule | undefined {
|
|
292
|
+
const row = this.db.prepare("SELECT value_json FROM update_check_schedule WHERE id = 1")
|
|
293
|
+
.get() as { value_json: string } | undefined;
|
|
294
|
+
return row ? JSON.parse(row.value_json) as UpdateCheckSchedule : undefined;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
saveUpdateCheckSchedule(schedule: UpdateCheckSchedule): void {
|
|
298
|
+
this.db.prepare(`INSERT INTO update_check_schedule (id, value_json) VALUES (1, ?)
|
|
299
|
+
ON CONFLICT(id) DO UPDATE SET value_json = excluded.value_json`).run(JSON.stringify(schedule));
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
getUpdateNotice(version: string): UpdateNotice | undefined {
|
|
303
|
+
const row = this.db.prepare("SELECT value_json FROM update_notices WHERE version = ?")
|
|
304
|
+
.get(version) as { value_json: string } | undefined;
|
|
305
|
+
return row ? JSON.parse(row.value_json) as UpdateNotice : undefined;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
saveUpdateNotice(notice: UpdateNotice): void {
|
|
309
|
+
this.db.prepare(`INSERT INTO update_notices (version, status, value_json) VALUES (?, ?, ?)
|
|
310
|
+
ON CONFLICT(version) DO UPDATE SET status = excluded.status, value_json = excluded.value_json`)
|
|
311
|
+
.run(notice.version, notice.status, JSON.stringify(notice));
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
listPendingUpdateNotices(): UpdateNotice[] {
|
|
315
|
+
const rows = this.db.prepare(`SELECT value_json FROM update_notices
|
|
316
|
+
WHERE status IN ('announcing', 'countdown', 'preparing', 'scheduled')`)
|
|
317
|
+
.all() as Array<{ value_json: string }>;
|
|
318
|
+
return rows.map((row) => JSON.parse(row.value_json) as UpdateNotice);
|
|
319
|
+
}
|
|
320
|
+
|
|
290
321
|
saveGoalCardDelivery(localSessionId: string, contextKey: string, messageId: string): void {
|
|
291
322
|
const updatedAt = new Date().toISOString();
|
|
292
323
|
this.db.prepare(`
|
package/src/state/migrations.ts
CHANGED
|
@@ -195,4 +195,16 @@ export const migrations = [
|
|
|
195
195
|
CREATE INDEX IF NOT EXISTS idx_card_action_bindings_created
|
|
196
196
|
ON card_action_bindings(created_at);
|
|
197
197
|
`,
|
|
198
|
+
`
|
|
199
|
+
CREATE TABLE IF NOT EXISTS update_check_schedule (
|
|
200
|
+
id INTEGER PRIMARY KEY CHECK (id = 1),
|
|
201
|
+
value_json TEXT NOT NULL
|
|
202
|
+
);
|
|
203
|
+
CREATE TABLE IF NOT EXISTS update_notices (
|
|
204
|
+
version TEXT PRIMARY KEY,
|
|
205
|
+
status TEXT NOT NULL,
|
|
206
|
+
value_json TEXT NOT NULL
|
|
207
|
+
);
|
|
208
|
+
CREATE INDEX IF NOT EXISTS idx_update_notices_status ON update_notices(status);
|
|
209
|
+
`,
|
|
198
210
|
] as const;
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { execFile, type ExecFileOptionsWithStringEncoding } from "node:child_process";
|
|
2
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
3
|
+
import { promisify } from "node:util";
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
import { loadConfig } from "../config/loadConfig.js";
|
|
6
|
+
import { controlEndpoint } from "../cli/controlProtocol.js";
|
|
7
|
+
import { finalizeSelfUpdatePlan, prepareSelfUpdate, releaseSelfUpdatePlan, requireNpmSelfUpdateInstallation } from "../cli/SelfUpdater.js";
|
|
8
|
+
import { stableVersionSchema } from "./PublishedRelease.js";
|
|
9
|
+
|
|
10
|
+
const resultSchema = z.discriminatedUnion("status", [
|
|
11
|
+
z.object({ status: z.literal("current") }),
|
|
12
|
+
z.object({ status: z.literal("prepared"), planPath: z.string().min(1), targetVersion: stableVersionSchema }),
|
|
13
|
+
]);
|
|
14
|
+
const runFile = promisify(execFile);
|
|
15
|
+
type RunPreparation = (file: string, args: string[], options: ExecFileOptionsWithStringEncoding) => Promise<{ stdout: string }>;
|
|
16
|
+
interface UpdateProfile { home: string; configPath: string }
|
|
17
|
+
|
|
18
|
+
export async function checkAutomaticUpdateSupport(profile: UpdateProfile, run: RunPreparation = runFile): Promise<void> {
|
|
19
|
+
z.object({ status: z.literal("supported") }).parse(await runPreparer("--check", profile, run));
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export async function prepareAutomaticUpdate(
|
|
23
|
+
version: string,
|
|
24
|
+
profile: UpdateProfile,
|
|
25
|
+
run: RunPreparation = runFile,
|
|
26
|
+
): Promise<z.infer<typeof resultSchema>> {
|
|
27
|
+
stableVersionSchema.parse(version);
|
|
28
|
+
const result = resultSchema.parse(await runPreparer(version, profile, run));
|
|
29
|
+
if (result.status === "prepared" && result.targetVersion !== version) {
|
|
30
|
+
throw new Error("The prepared update does not match the announced version.");
|
|
31
|
+
}
|
|
32
|
+
return result;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async function runPreparer(argument: string, profile: UpdateProfile, run: RunPreparation): Promise<unknown> {
|
|
36
|
+
const environment: NodeJS.ProcessEnv = { ...process.env, AGENT_BOT_HOME: profile.home, AGENT_BOT_CONFIG: profile.configPath };
|
|
37
|
+
delete environment.AGENT_BOT;
|
|
38
|
+
// npm preparation is synchronous and can take minutes; keep it off the live worker's event loop.
|
|
39
|
+
const { stdout } = await run(process.execPath, [fileURLToPath(import.meta.url), argument], {
|
|
40
|
+
env: environment, windowsHide: true, encoding: "utf8", maxBuffer: 1_000_000,
|
|
41
|
+
});
|
|
42
|
+
return JSON.parse(String(stdout)) as unknown;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async function main(): Promise<void> {
|
|
46
|
+
if (process.argv[2] === "--check") {
|
|
47
|
+
requireNpmSelfUpdateInstallation();
|
|
48
|
+
process.stdout.write(JSON.stringify({ status: "supported" }));
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
const version = stableVersionSchema.parse(process.argv[2]);
|
|
52
|
+
const config = loadConfig();
|
|
53
|
+
const prepared = await prepareSelfUpdate({ version, channel: "latest" });
|
|
54
|
+
if (prepared.status === "prepared") {
|
|
55
|
+
try {
|
|
56
|
+
finalizeSelfUpdatePlan(prepared.planPath, {
|
|
57
|
+
controlEndpoint: controlEndpoint(config.storage.sqlitePath),
|
|
58
|
+
databasePath: config.storage.sqlitePath,
|
|
59
|
+
restartService: true,
|
|
60
|
+
workingDirectory: process.cwd(),
|
|
61
|
+
reason: `Agent Bot 自动更新到 ${version}`,
|
|
62
|
+
});
|
|
63
|
+
} catch (error) {
|
|
64
|
+
releaseSelfUpdatePlan(prepared.planPath);
|
|
65
|
+
throw error;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
process.stdout.write(JSON.stringify(prepared));
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (process.argv[1] && pathToFileURL(process.argv[1]).href === import.meta.url) {
|
|
72
|
+
void main().catch((error: unknown) => {
|
|
73
|
+
process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
|
|
74
|
+
process.exitCode = 1;
|
|
75
|
+
});
|
|
76
|
+
}
|