@keyou007/agent-bot 0.1.22 → 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.
Files changed (47) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/README.md +12 -2
  3. package/README.zh.md +12 -2
  4. package/config.example.yaml +5 -0
  5. package/dist/config/loadConfig.js +4 -0
  6. package/dist/config/loadConfig.js.map +1 -1
  7. package/dist/config/schema.js +1 -0
  8. package/dist/config/schema.js.map +1 -1
  9. package/dist/feishu/CardRenderer.js +31 -2
  10. package/dist/feishu/CardRenderer.js.map +1 -1
  11. package/dist/feishu/FeishuMessageClient.js +2 -2
  12. package/dist/feishu/FeishuMessageClient.js.map +1 -1
  13. package/dist/index.js +44 -0
  14. package/dist/index.js.map +1 -1
  15. package/dist/local-files/TurnPreviewPage.js +106 -87
  16. package/dist/local-files/TurnPreviewPage.js.map +1 -1
  17. package/dist/proxy/ProxySessionController.js +12 -2
  18. package/dist/proxy/ProxySessionController.js.map +1 -1
  19. package/dist/state/StateStore.js +25 -0
  20. package/dist/state/StateStore.js.map +1 -1
  21. package/dist/state/migrations.js +12 -0
  22. package/dist/state/migrations.js.map +1 -1
  23. package/dist/updates/AutomaticUpdatePreparer.js +66 -0
  24. package/dist/updates/AutomaticUpdatePreparer.js.map +1 -0
  25. package/dist/updates/DailyUpdateMonitor.js +261 -0
  26. package/dist/updates/DailyUpdateMonitor.js.map +1 -0
  27. package/dist/updates/PublishedRelease.js +43 -0
  28. package/dist/updates/PublishedRelease.js.map +1 -0
  29. package/dist/updates/types.js +2 -0
  30. package/dist/updates/types.js.map +1 -0
  31. package/npm-shrinkwrap.json +2 -2
  32. package/package.json +1 -1
  33. package/skills/agent-bot/SKILL.md +2 -0
  34. package/src/config/loadConfig.ts +4 -0
  35. package/src/config/schema.ts +4 -1
  36. package/src/feishu/CardRenderer.ts +34 -2
  37. package/src/feishu/FeishuMessageClient.ts +2 -1
  38. package/src/feishu/types.ts +1 -1
  39. package/src/index.ts +40 -0
  40. package/src/local-files/TurnPreviewPage.ts +105 -88
  41. package/src/proxy/ProxySessionController.ts +14 -2
  42. package/src/state/StateStore.ts +31 -0
  43. package/src/state/migrations.ts +12 -0
  44. package/src/updates/AutomaticUpdatePreparer.ts +76 -0
  45. package/src/updates/DailyUpdateMonitor.ts +252 -0
  46. package/src/updates/PublishedRelease.ts +44 -0
  47. package/src/updates/types.ts +19 -0
@@ -2,7 +2,7 @@ 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
6
  import { isFileUrl, rewriteMarkdownFileLinks } from "./MarkdownFileLinks.js";
7
7
 
8
8
  const MARKDOWN = new MarkdownIt({
@@ -164,7 +164,12 @@ export const TURN_PREVIEW_CLIENT_SCRIPT = `(() => {
164
164
  const duration = hours > 0
165
165
  ? String(hours).padStart(2, "0") + ":" + String(minutes).padStart(2, "0") + ":" + String(remainder).padStart(2, "0")
166
166
  : String(minutes).padStart(2, "0") + ":" + String(remainder).padStart(2, "0");
167
- element.textContent = labels.elapsed + duration;
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;
168
173
  });
169
174
  };
170
175
  updateElapsed();
@@ -288,7 +293,7 @@ export function renderTurnPreviewSnapshot(
288
293
  return localFileUrl(path.resolve(state.projectCwd, filePath));
289
294
  } : undefined;
290
295
  const renderText = (value: string): string => renderMarkdown(value, state.projectCwd, localFileUrl);
291
- const timeline = renderTimeline(state.activities ?? [], renderText, resolveFileUrl, state.fullToolOutputs, state.fullToolErrors, language);
296
+ const timeline = renderTimeline(state.activities ?? [], renderText, resolveFileUrl, state.fullToolOutputs, state.fullToolErrors, language, state.projectCwd);
292
297
  const sections = [
293
298
  state.prompt ? renderMessageActivity("prompt", state.prompt, "user prompt", renderText) : "",
294
299
  state.plan.length > 0 ? renderPlan(state, language) : "",
@@ -331,10 +336,11 @@ function renderTimeline(
331
336
  fullToolOutputs?: Record<string, string>,
332
337
  fullToolErrors?: Record<string, string>,
333
338
  language: TurnPreviewLanguage = "zh",
339
+ projectCwd?: string,
334
340
  ): string {
335
341
  return activities
336
342
  .filter((activity): activity is Exclude<TurnActivity, { kind: "reasoning" }> => activity.kind !== "reasoning")
337
- .map((activity) => renderActivity(activity, renderText, localFileUrl, fullToolOutputs, fullToolErrors, language))
343
+ .map((activity) => renderActivity(activity, renderText, localFileUrl, fullToolOutputs, fullToolErrors, language, projectCwd))
338
344
  .join("");
339
345
  }
340
346
 
@@ -345,6 +351,7 @@ function renderActivity(
345
351
  fullToolOutputs?: Record<string, string>,
346
352
  fullToolErrors?: Record<string, string>,
347
353
  language: TurnPreviewLanguage = "zh",
354
+ projectCwd?: string,
348
355
  ): string {
349
356
  switch (activity.kind) {
350
357
  case "assistant":
@@ -352,7 +359,7 @@ function renderActivity(
352
359
  case "user":
353
360
  return renderMessageActivity(activity.id, activity.text, "user", renderText);
354
361
  case "tool":
355
- return renderToolActivity(activity.id, activity.tool, localFileUrl, fullToolOutputs, fullToolErrors, language);
362
+ return renderToolActivity(activity.id, activity.tool, localFileUrl, fullToolOutputs, fullToolErrors, language, projectCwd);
356
363
  }
357
364
  }
358
365
 
@@ -367,6 +374,7 @@ function renderToolActivity(
367
374
  fullToolOutputs?: Record<string, string>,
368
375
  fullToolErrors?: Record<string, string>,
369
376
  language: TurnPreviewLanguage = "zh",
377
+ projectCwd?: string,
370
378
  ): string {
371
379
  const labels = previewLabels(language);
372
380
  const command = tool.command ? (displayToolCommand(tool.command) || tool.command.trim()) : undefined;
@@ -376,29 +384,40 @@ function renderToolActivity(
376
384
  : command
377
385
  ? formatToolCommand(command)
378
386
  : undefined;
379
- const commandInHeader = Boolean(displayCommand);
380
- const title = commandInHeader ? displayCommand! : toolTitle(tool, command, repl, language);
387
+ const title = displayCommand ? summarizeToolCommand(displayCommand) : toolTitle(tool, command, repl, language);
381
388
  const output = fullToolOutputs?.[tool.id] ?? tool.output;
382
389
  const error = fullToolErrors?.[tool.id] ?? tool.error;
383
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;
384
393
  const detailParts = [
385
- displayCommand && !commandInHeader ? `<pre class="command-block${repl ? " repl-code" : ""}" data-scroll-id="${escapeAttribute(`${activityId}:command`)}">${escapeHtml(displayCommand)}</pre>` : "",
386
- output
387
- ? renderToolOutput(activityId, "output", sameError ? labels.resultError : repl ? labels.result : labels.output, output, sameError, repl, language, tool)
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)
388
397
  : "",
389
- error && !sameError
390
- ? renderToolOutput(activityId, "error", labels.error, error, true, repl, language, tool)
398
+ displayError
399
+ ? renderToolOutput(labels.error, displayError, true, repl)
391
400
  : "",
392
- !output && !error && !tool.files?.length ? renderToolStatusRow(tool, language) : "",
393
401
  renderToolImage(tool, localFileUrl, language),
394
402
  tool.files?.length
395
- ? `<details class="tool-output" data-activity-id="${escapeAttribute(`${activityId}:files`)}"><summary><span class="tool-output-label">${escapeHtml(labels.files)}</span><span class="tool-output-meta">${renderToolTiming(tool, language)}<span class="tool-state ${tool.status}" aria-label="${escapeAttribute(tool.status)}">${toolStatusIcon(tool.status, language)}</span><span>${tool.files.length}</span></span></summary><ul class="files">${tool.files.map((file) => renderFileEntry(file, localFileUrl)).join("")}</ul></details>`
403
+ ? `<ul class="files" aria-label="${escapeAttribute(labels.files)}">${tool.files.map((file) => renderFileEntry(file, localFileUrl, projectCwd)).join("")}</ul>`
396
404
  : "",
397
405
  ].filter(Boolean).join("");
398
- const titleMarkup = commandInHeader
406
+ const titleMarkup = displayCommand
399
407
  ? `<code class="tool-command-title">${escapeHtml(title)}</code>`
400
408
  : `<span class="tool-title">${escapeHtml(title)}</span>`;
401
- return `<article class="activity tool"><section class="tool-step" data-activity-id="${escapeAttribute(activityId)}"><div class="tool-header" data-scroll-id="${escapeAttribute(`${activityId}:command`)}">${titleMarkup}</div><div class="tool-body">${detailParts || `<div class="muted">${escapeHtml(labels.noToolDetails)}</div>`}</div></section></article>`;
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>`;
402
421
  }
403
422
 
404
423
  function toolTitle(tool: ToolState, command?: string, repl = false, language: TurnPreviewLanguage = "zh"): string {
@@ -408,6 +427,12 @@ function toolTitle(tool: ToolState, command?: string, repl = false, language: Tu
408
427
  return title || tool.kind;
409
428
  }
410
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
+
411
436
  function isReplTool(tool: ToolState): boolean {
412
437
  const title = tool.title.trim();
413
438
  const titleLooksLikeToolName = title.length <= 120 && !/[\s"'`]/u.test(title);
@@ -518,36 +543,29 @@ function normalizeReplText(value: string): string {
518
543
  }
519
544
 
520
545
  function renderToolOutput(
521
- activityId: string,
522
- kind: string,
523
546
  label: string,
524
547
  value: string,
525
548
  error = false,
526
549
  repl = false,
527
- language: TurnPreviewLanguage = "zh",
528
- tool?: ToolState,
529
550
  ): string {
530
- const id = escapeAttribute(`${activityId}:${kind}`);
531
- const displayValue = repl && !error ? formatReplOutput(value) : value;
532
- const labels = previewLabels(language);
533
- const status = tool?.status ?? "completed";
534
- const timing = tool ? renderToolTiming(tool, language) : "";
535
- 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>`;
536
552
  }
537
553
 
538
- function renderToolStatusRow(tool: ToolState, language: TurnPreviewLanguage): string {
539
- const timing = renderToolTiming(tool, language);
540
- return `<div class="tool-status-row"><span class="tool-output-meta">${timing}<span class="tool-state ${tool.status}" aria-label="${escapeAttribute(tool.status)}">${toolStatusIcon(tool.status, language)}</span></span></div>`;
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>`;
541
562
  }
542
563
 
543
- function renderToolTiming(tool: ToolState, language: TurnPreviewLanguage): string {
544
- const labels = previewLabels(language);
545
- const start = tool.startedAt === undefined ? labels.unknown : formatTimestamp(tool.startedAt);
546
- const duration = toolDuration(tool);
547
- const liveDuration = duration === undefined
548
- ? `<span>${escapeHtml(labels.elapsed.trim())}${language === "zh" ? "" : " "}${escapeHtml(labels.unknown)}</span>`
549
- : `<span${tool.status === "running" && tool.completedAt === undefined ? ` data-live-tool-duration data-started-at="${tool.startedAt}"` : ""}>${escapeHtml(labels.elapsed)}${escapeHtml(duration)}</span>`;
550
- 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` : "";
551
569
  }
552
570
 
553
571
  function renderToolImage(
@@ -573,12 +591,12 @@ function renderFileSummary(
573
591
  localFileUrl?: (filePath: string) => string | undefined,
574
592
  ): string {
575
593
  const labels = previewLabels(language);
576
- const files = state.fileSummary.map((file) => renderFileEntry(file, localFileUrl)).join("");
577
- return `<section class="files-summary"><h2>${escapeHtml(labels.fileChanges)} <span>${state.fileSummary.length}</span></h2><ul class="files">${files}</ul></section>`;
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>`;
578
596
  }
579
597
 
580
- function renderFileEntry(file: FileSummary, localFileUrl?: (filePath: string) => string | undefined): string {
581
- 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>`;
582
600
  const url = localFileUrl?.(file.path);
583
601
  const link = url
584
602
  ? `<a class="file-link" href="${escapeAttribute(url)}" target="_blank" rel="noreferrer noopener">${label}</a>`
@@ -630,7 +648,7 @@ function statusLabel(status: TurnViewStatus, language: TurnPreviewLanguage): str
630
648
  return previewLabels(language).statuses[status];
631
649
  }
632
650
 
633
- function toolStatusIcon(status: ToolState["status"], language: TurnPreviewLanguage): string {
651
+ function toolStatusLabel(status: ToolState["status"], language: TurnPreviewLanguage): string {
634
652
  return status === "completed"
635
653
  ? previewLabels(language).toolStatuses.completed
636
654
  : status === "failed"
@@ -654,15 +672,6 @@ function formatDuration(durationMs: number): string {
654
672
  : `${String(minutes).padStart(2, "0")}:${String(remainder).padStart(2, "0")}`;
655
673
  }
656
674
 
657
- function formatTimestamp(timestampMs: number): string {
658
- return new Intl.DateTimeFormat(undefined, {
659
- hour: "2-digit",
660
- minute: "2-digit",
661
- second: "2-digit",
662
- hour12: false,
663
- }).format(new Date(timestampMs));
664
- }
665
-
666
675
  function formatNumber(value: number): string {
667
676
  return Math.max(0, Math.floor(value)).toLocaleString("en-US");
668
677
  }
@@ -716,14 +725,19 @@ h2 span { margin-left: 6px; color: var(--muted); font-size: 12px; font-weight: 4
716
725
  .metadata > span + span::before { content: "·"; position: absolute; left: 0; top: 0; }
717
726
  .metadata strong { color: var(--text); font-weight: 500; font-variant-numeric: tabular-nums; }
718
727
  main { max-width: 1320px; margin: 0 auto; padding: 0 20px 40px; }
719
- .plan, .files-summary, .result { padding: 14px 0; border-bottom: 1px solid var(--line); }
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; }
720
732
  .plan ol { display: grid; gap: 4px; margin: 0; padding: 0; list-style: none; }
721
733
  .plan li { display: grid; grid-template-columns: 16px minmax(0, 1fr); gap: 6px; overflow-wrap: anywhere; }
722
734
  .plan li.completed { color: var(--muted); }
723
735
  .plan li.in_progress { color: var(--accent); }
724
736
  .timeline { padding-top: 2px; }
725
737
  .activity { min-width: 0; }
726
- .message { padding: 12px 0; border-bottom: 1px solid var(--line); }
738
+ .message { padding: 12px 0; }
739
+ .prompt { border-bottom: 1px solid var(--line); }
740
+ .timeline .markdown hr { display: none; }
727
741
  .markdown { min-width: 0; overflow-wrap: anywhere; }
728
742
  .message > .markdown, .result > .markdown { font-size: 14px; font-weight: 400; }
729
743
  .markdown > :first-child { margin-top: 0; }
@@ -748,47 +762,52 @@ summary:hover { background: transparent; }
748
762
  summary:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }
749
763
  .tool { margin: 8px 0; }
750
764
  .tool-step { border: 1px solid var(--line); border-radius: 8px; overflow: hidden; background: transparent; }
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; }
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; }
753
775
  @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); }
776
+ .tool-content { --tool-scrollbar-thumb: transparent; }
777
+ .tool-content:is(:hover, :focus-visible, :focus-within) { --tool-scrollbar-thumb: var(--scrollbar-thumb); }
757
778
  }
758
779
  @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
- }
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; }
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; }
768
794
  .tool-state.completed { color: var(--success); }
769
795
  .tool-state.running { color: var(--accent); }
770
796
  .tool-state.failed { color: var(--danger); }
771
- .tool-title { min-width: 0; flex: 1 0 auto; overflow-wrap: normal; font-size: 12px; font-weight: 400; line-height: 1.45; white-space: pre; }
772
- .tool-command-title { display: block; min-width: 0; flex: 1 0 auto; margin: 0; padding: 0; overflow-wrap: normal; background: transparent; color: var(--text); font: 12px/1.55 "Cascadia Mono", "SFMono-Regular", Consolas, monospace; font-weight: 400; white-space: pre; }
773
- .tool-meta { display: flex; flex: 0 1 auto; flex-wrap: nowrap; gap: 12px; max-width: 48%; overflow: hidden; color: var(--muted); font-size: 11px; font-weight: 400; font-variant-numeric: tabular-nums; white-space: nowrap; }
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; }
774
802
  .tool-meta span { white-space: nowrap; }
775
- .tool-body { border-top: 1px solid var(--line); }
776
- .tool-body > .muted { padding: 8px 12px; }
777
- .tool-output { border-top: 1px solid var(--line); }
778
- .tool-output:first-child { border-top: 0; }
779
- .tool-output > summary { padding: 4px 12px; background: transparent; color: var(--muted); font-size: 12px; }
780
- .tool-output-label { min-width: 0; }
781
- .tool-output-meta { display: inline-flex; align-items: center; gap: 12px; margin-left: auto; font-size: 11px; }
782
- .tool-status-row { display: flex; justify-content: flex-end; padding: 5px 12px; border-top: 1px solid var(--line); }
783
- .tool-output > pre { max-height: none; border-top: 1px solid var(--line); background: transparent; }
784
- .repl-code { background: transparent; }
785
- .repl-result > pre { white-space: pre; overflow-wrap: normal; background: transparent; }
786
- .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); }
787
806
  .muted, .empty { color: var(--muted); }
788
- .notice, .empty { padding: 10px 0; border-bottom: 1px solid var(--line); }
807
+ .notice, .empty { padding: 10px 0; }
789
808
  .notice { color: #926a08; }
790
809
  .files { display: grid; gap: 4px; margin: 0; padding: 0; list-style: none; }
791
- .tool-output .files { padding: 8px 12px; }
810
+ .tool-content .files code { background: transparent; }
792
811
  .files li { display: flex; align-items: baseline; justify-content: space-between; gap: 12px; }
793
812
  .files code { min-width: 0; }
794
813
  .files .file-link { min-width: 0; color: var(--accent); text-underline-offset: 2px; }
@@ -796,7 +815,6 @@ summary:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px;
796
815
  .additions { color: var(--success); }
797
816
  .deletions { color: var(--danger); }
798
817
  .file-delta { display: inline-flex; flex: none; gap: 6px; font: 12px "Cascadia Mono", Consolas, monospace; }
799
- .tool-image { padding: 8px 12px; border-top: 1px solid var(--line); }
800
818
  .tool-image img { display: block; max-width: 100%; max-height: 720px; }
801
819
  .final-result h2 { color: var(--success); }
802
820
  @media (max-width: 700px) {
@@ -809,9 +827,8 @@ summary:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px;
809
827
  .message { padding: 10px 0; }
810
828
  .tool-header { gap: 6px; padding: 7px 9px; }
811
829
  .tool-state { font-size: 11px; }
812
- .tool-meta { flex: 0 1 auto; gap: 10px; max-width: 50%; font-size: 11px; }
813
- .tool-output > summary { min-height: 40px; padding: 6px 10px; }
814
- .tool-output-meta { gap: 10px; }
830
+ .tool-meta { gap: 10px; font-size: 11px; }
831
+ .tool-content, .tool-footer { padding-left: 10px; padding-right: 10px; }
815
832
  pre { padding: 8px 10px; }
816
833
  }
817
834
  @media (prefers-color-scheme: dark) {
@@ -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 === "help_command") {
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
- return loaded.runtime.setExecutionSettings(loaded.record.localSessionId, settings, persist);
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}`
@@ -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(`
@@ -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
+ }