将崩溃弹窗替换为双栏布局:左侧展示诊断摘要与分享入口, 右侧提供日志文件浏览器与 LogAgent 流式分析输出。 - MinecraftCrashModal 改用新版的分享票据、结构化摘要与 AI 事件流 - 新增 minecraft-crash-logs 工具函数,用于筛选与合并崩溃日志 - 事件层新增 logshare_ai_listener 监听 AI 分析进度 - log-share 工具更新为支持 provider 路由的新版 - Button 组件对齐新版(补 icon-only、circular、2xs 与 chip/highlight 变体) - console 导出 LogViewport,供日志浏览器复用 - 补齐 en-US / zh-CN 相关文案
This commit is contained in:
1 parent
a95153d0b1
commit
c71c220446
10 files changed
+1323
-144
No files matched your search
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,62 @@
|
||||
export interface CrashLogFile {
|
||||
age: number
|
||||
filename: string
|
||||
log_type: 'InfoLog' | 'CrashReport' | 'JvmCrash' | 'LauncherLog'
|
||||
output?: string | null
|
||||
}
|
||||
|
||||
const RUN_ASSOCIATION_WINDOW_SECONDS = 3 * 60
|
||||
|
||||
export function shouldUseLogShareAutoAnalysis(settings: {
|
||||
ai_source: 'logshare' | 'custom'
|
||||
auto_upload: boolean
|
||||
no_storage?: boolean
|
||||
}): boolean {
|
||||
return settings.ai_source === 'logshare' && settings.auto_upload
|
||||
}
|
||||
|
||||
function isRelevantLog(file: CrashLogFile): boolean {
|
||||
const lower = file.filename.toLowerCase()
|
||||
if (file.log_type === 'CrashReport') return lower.endsWith('.txt')
|
||||
if (file.log_type === 'JvmCrash') return lower.startsWith('hs_err') && lower.endsWith('.log')
|
||||
return ['latest.log', 'debug.log', 'launcher_log.txt', 'latest_stdout.log'].includes(lower)
|
||||
}
|
||||
|
||||
export function selectCrashLogFiles(files: CrashLogFile[]): CrashLogFile[] {
|
||||
const sorted = files.filter(isRelevantLog).sort((left, right) => right.age - left.age)
|
||||
const anchor = sorted[0]?.age
|
||||
if (anchor === undefined) return []
|
||||
|
||||
const singletonTypes = new Set<CrashLogFile['log_type']>()
|
||||
return sorted.filter((file) => {
|
||||
if (anchor - file.age > RUN_ASSOCIATION_WINDOW_SECONDS) return false
|
||||
if (file.log_type !== 'CrashReport' && file.log_type !== 'JvmCrash') return true
|
||||
if (singletonTypes.has(file.log_type)) return false
|
||||
singletonTypes.add(file.log_type)
|
||||
return true
|
||||
})
|
||||
}
|
||||
|
||||
export function crashLogKey(file: CrashLogFile): string {
|
||||
return `${file.log_type}:${file.filename}`
|
||||
}
|
||||
|
||||
export function crashLogLabel(file: CrashLogFile): string {
|
||||
return file.log_type === 'CrashReport' ? `crash-reports/${file.filename}` : file.filename
|
||||
}
|
||||
|
||||
export function preferredCrashLogKey(files: CrashLogFile[]): string {
|
||||
const preferred =
|
||||
files.find((file) => file.log_type === 'CrashReport') ??
|
||||
files.find((file) => file.log_type === 'JvmCrash') ??
|
||||
files.find((file) => file.filename.toLowerCase() === 'latest.log') ??
|
||||
files[0]
|
||||
return preferred ? crashLogKey(preferred) : ''
|
||||
}
|
||||
|
||||
export function combineCrashLogs(files: CrashLogFile[]): string {
|
||||
return files
|
||||
.filter((file) => file.output)
|
||||
.map((file) => `===== ${crashLogLabel(file)} =====\n${file.output}`)
|
||||
.join('\n\n')
|
||||
}
|
||||
@@ -160,6 +160,10 @@ export async function notification_listener(callback) {
|
||||
return await listen('notification', (event) => callback(event.payload))
|
||||
}
|
||||
|
||||
export async function logshare_ai_listener(callback) {
|
||||
return await listen('logshare_ai', (event) => callback(event.payload))
|
||||
}
|
||||
|
||||
/// Payload for the 'log' event
|
||||
/*
|
||||
LogPayload {
|
||||
|
||||
@@ -11624,5 +11624,33 @@
|
||||
"app.log-share.shared-logs.deleting": {"message": "Deleting…"},
|
||||
"app.log-share.shared-logs.deleted": {"message": "Shared log removed."},
|
||||
"app.log-share.shared-logs.delete-failed": {"message": "Failed to delete the shared log."},
|
||||
"app.log-share.shared-logs.truncated": {"message": "Truncated"}
|
||||
"app.log-share.shared-logs.truncated": {"message": "Truncated"},
|
||||
"app.minecraft-crash.share-fallback": {"message": "LogShare is unavailable, so the diagnostic was shared via mclo.gs."},
|
||||
"app.log-share.ai.action": {"message": "Deep AI analysis"},
|
||||
"app.log-share.summary.title": {"message": "LogShare analysis"},
|
||||
"app.log-share.summary.loading": {"message": "Loading structured summary..."},
|
||||
"app.log-share.summary.empty": {"message": "LogShare did not return a structured summary for these logs."},
|
||||
"app.log-share.summary.no-storage": {"message": "Structured summary is unavailable while no-storage mode is enabled."},
|
||||
"app.log-share.summary.failed": {"message": "Could not load the LogShare summary: {message}"},
|
||||
"app.log-share.summary.problems": {"message": "Problems"},
|
||||
"app.log-share.summary.information": {"message": "Information"},
|
||||
"app.minecraft-crash.logs.loading": {"message": "Loading crash logs..."},
|
||||
"app.minecraft-crash.logs.empty": {"message": "No logs from this launch were found."},
|
||||
"app.minecraft-crash.logs.failed": {"message": "Could not load the crash logs: {message}"},
|
||||
"app.minecraft-crash.log-file.loading": {"message": "Loading log file..."},
|
||||
"app.minecraft-crash.log-file.failed": {"message": "Could not load this log file: {message}"},
|
||||
"app.log-share.ai.tab": {"message": "LogAgent detailed analysis"},
|
||||
"app.log-share.ai.root-cause": {"message": "Core root cause"},
|
||||
"app.log-share.ai.confidence": {"message": "Diagnostic confidence"},
|
||||
"app.log-share.ai.steps": {"message": "Recommended troubleshooting steps"},
|
||||
"app.log-share.ai.evidence": {"message": "Supporting evidence"},
|
||||
"app.log-share.ai.thinking": {"message": "Thinking…"},
|
||||
"app.log-share.ai.queued": {"message": "Queued, about {position} ahead"},
|
||||
"app.log-share.ai.queued-front": {"message": "Queued, waiting for a free slot"},
|
||||
"app.log-share.ai.tool": {"message": "Using tool: {name}"},
|
||||
"app.log-share.ai.unknown-tool": {"message": "tool"},
|
||||
"app.log-share.ai.tool-result": {"message": "Tool result received"},
|
||||
"app.log-share.ai.limit": {"message": "Reached tool loop limit"},
|
||||
"app.log-share.ai.working": {"message": "Analyzing with LogAgent…"},
|
||||
"app.log-share.ai.failed": {"message": "LogAgent analysis failed: {message}"}
|
||||
}
|
||||
@@ -11455,5 +11455,33 @@
|
||||
"app.log-share.shared-logs.deleting": {"message": "删除中…"},
|
||||
"app.log-share.shared-logs.deleted": {"message": "已删除共享日志。"},
|
||||
"app.log-share.shared-logs.delete-failed": {"message": "删除已分享日志失败。"},
|
||||
"app.log-share.shared-logs.truncated": {"message": "已截断"}
|
||||
"app.log-share.shared-logs.truncated": {"message": "已截断"},
|
||||
"app.minecraft-crash.share-fallback": {"message": "LogShare 不可用,已改用 mclo.gs 分享诊断信息。"},
|
||||
"app.log-share.ai.action": {"message": "AI 深度分析"},
|
||||
"app.log-share.summary.title": {"message": "LogShare 分析"},
|
||||
"app.log-share.summary.loading": {"message": "正在加载结构化摘要…"},
|
||||
"app.log-share.summary.empty": {"message": "LogShare 未返回这些日志的结构化摘要。"},
|
||||
"app.log-share.summary.no-storage": {"message": "启用不落盘模式时无法获取结构化摘要。"},
|
||||
"app.log-share.summary.failed": {"message": "无法加载 LogShare 摘要:{message}"},
|
||||
"app.log-share.summary.problems": {"message": "问题"},
|
||||
"app.log-share.summary.information": {"message": "信息"},
|
||||
"app.minecraft-crash.logs.loading": {"message": "正在加载崩溃日志…"},
|
||||
"app.minecraft-crash.logs.empty": {"message": "未找到本次启动产生的日志。"},
|
||||
"app.minecraft-crash.logs.failed": {"message": "无法加载崩溃日志:{message}"},
|
||||
"app.minecraft-crash.log-file.loading": {"message": "正在加载日志文件…"},
|
||||
"app.minecraft-crash.log-file.failed": {"message": "无法加载此日志文件:{message}"},
|
||||
"app.log-share.ai.tab": {"message": "LogAgent 深度分析"},
|
||||
"app.log-share.ai.root-cause": {"message": "核心根因"},
|
||||
"app.log-share.ai.confidence": {"message": "诊断置信度"},
|
||||
"app.log-share.ai.steps": {"message": "推荐排障步骤"},
|
||||
"app.log-share.ai.evidence": {"message": "支撑证据链"},
|
||||
"app.log-share.ai.thinking": {"message": "思考中…"},
|
||||
"app.log-share.ai.queued": {"message": "排队中,前方约 {position} 个任务"},
|
||||
"app.log-share.ai.queued-front": {"message": "排队中,正在等待空闲槽位"},
|
||||
"app.log-share.ai.tool": {"message": "正在使用工具:{name}"},
|
||||
"app.log-share.ai.unknown-tool": {"message": "工具"},
|
||||
"app.log-share.ai.tool-result": {"message": "已收到工具结果"},
|
||||
"app.log-share.ai.limit": {"message": "已达到工具调用上限"},
|
||||
"app.log-share.ai.working": {"message": "正在使用 LogAgent 分析…"},
|
||||
"app.log-share.ai.failed": {"message": "LogAgent 分析失败:{message}"}
|
||||
}
|
||||
@@ -7,6 +7,8 @@ import type { ButtonProps } from './types'
|
||||
const props = withDefaults(defineProps<ButtonProps>(), {
|
||||
type: 'base',
|
||||
size: 'md',
|
||||
iconOnly: false,
|
||||
circular: false,
|
||||
nativeType: 'button',
|
||||
disabled: false,
|
||||
loading: false,
|
||||
@@ -26,8 +28,11 @@ defineExpose({ element })
|
||||
:color="props.color"
|
||||
:size="props.size"
|
||||
:interaction="props.interaction"
|
||||
:icon-only="props.iconOnly"
|
||||
:circular="props.circular"
|
||||
:native-type="props.nativeType"
|
||||
:disabled="props.disabled || props.loading"
|
||||
:aria-label="props.iconOnly ? props.label : undefined"
|
||||
:aria-busy="props.loading || undefined"
|
||||
>
|
||||
<slot />
|
||||
|
||||
@@ -13,7 +13,10 @@ import type {
|
||||
const baseClasses = [
|
||||
// Base
|
||||
'relative inline-flex min-w-0 shrink-0 items-center justify-center',
|
||||
'whitespace-nowrap border-0 no-underline',
|
||||
// The transparent border is load-bearing, not decorative: it participates in
|
||||
// layout, so an auto-width button is 2px wider than it would be without it.
|
||||
// The ring participates in layout; dropping it would silently narrow every text button.
|
||||
'whitespace-nowrap border border-solid border-transparent no-underline',
|
||||
// Interactions
|
||||
'touch-manipulation cursor-pointer select-none transition-[background-color,color,box-shadow,filter,opacity,transform] duration-150 ease-out',
|
||||
'enabled:active:scale-[0.97]',
|
||||
@@ -26,6 +29,8 @@ const baseClasses = [
|
||||
].join(' ')
|
||||
|
||||
const sizeClasses: Record<ButtonSize, string> = {
|
||||
'2xs':
|
||||
'h-6 gap-1 rounded-lg px-1.5 text-sm font-semibold leading-5 [&>svg]:size-4 [&>svg]:min-h-4 [&>svg]:min-w-4 [&>svg]:shrink-0',
|
||||
xs: 'h-7 gap-1 rounded-lg px-1.5 text-sm font-semibold leading-5 [&>svg]:size-4 [&>svg]:min-h-4 [&>svg]:min-w-4 [&>svg]:shrink-0',
|
||||
sm: 'h-8 gap-1 rounded-[10px] px-1.5 text-sm font-semibold leading-5 [&>svg]:size-4 [&>svg]:min-h-4 [&>svg]:min-w-4 [&>svg]:shrink-0',
|
||||
md: 'h-9 gap-1.5 rounded-xl px-2.5 text-base font-semibold leading-5 [&>svg]:size-5 [&>svg]:min-h-5 [&>svg]:min-w-5 [&>svg]:shrink-0',
|
||||
@@ -34,6 +39,7 @@ const sizeClasses: Record<ButtonSize, string> = {
|
||||
}
|
||||
|
||||
const iconOnlySizeClasses: Record<ButtonSize, string> = {
|
||||
'2xs': 'min-w-6 w-6 !px-0',
|
||||
xs: 'min-w-7 w-7 !px-0',
|
||||
sm: 'min-w-8 w-8 !px-0',
|
||||
md: 'min-w-9 w-9 !px-0',
|
||||
@@ -42,14 +48,23 @@ const iconOnlySizeClasses: Record<ButtonSize, string> = {
|
||||
}
|
||||
|
||||
const typeClasses: Record<ButtonType, string> = {
|
||||
base: 'button-frame--base bg-surface-4 text-contrast [&>svg]:text-primary',
|
||||
base: 'button-frame--base bg-surface-4 text-[var(--color-text-primary)] [&>svg]:text-[var(--color-text-default)]',
|
||||
colored:
|
||||
'button-frame--colored bg-[--button-color] text-[var(--color-accent-contrast)] [&>svg]:text-inherit',
|
||||
'colored-text':
|
||||
'button-frame--colored-text bg-surface-4 text-[--button-color] [&>svg]:text-inherit',
|
||||
outlined:
|
||||
'button-frame--outlined bg-transparent text-[var(--button-color,var(--color-contrast))] [&>svg]:text-[var(--button-color,var(--color-base))]',
|
||||
'button-frame--outlined bg-transparent text-[var(--button-color,var(--color-text-primary))] [&>svg]:text-[var(--button-color,var(--color-text-default))]',
|
||||
quiet: 'button-frame--quiet bg-transparent [&>svg]:text-inherit',
|
||||
// A 25% tint of the accent colour. All three read the same tint; they differ
|
||||
// in label colour and whether they carry the raised shadow. Without a colour
|
||||
// the tint variable is unset and the fill falls back to the raised surface,
|
||||
// which is what the legacy uncoloured chip rendered as.
|
||||
chip: 'button-frame--chip bg-[--button-highlight,var(--surface-4)] text-[var(--button-color,var(--color-text-default))] [&>svg]:text-inherit',
|
||||
'chip-text':
|
||||
'button-frame--chip-text bg-[--button-highlight,var(--surface-4)] text-[var(--button-color,var(--color-text-default))] [&>svg]:text-inherit',
|
||||
highlight:
|
||||
'button-frame--highlight bg-[--button-highlight,var(--surface-4)] text-[var(--color-text-primary)] [&>svg]:text-inherit',
|
||||
}
|
||||
|
||||
const interactionClasses: Record<ButtonInteraction, string> = {
|
||||
@@ -67,7 +82,17 @@ const colorVariables: Record<ButtonColor, string> = {
|
||||
green: 'var(--color-green)',
|
||||
blue: 'var(--color-blue)',
|
||||
purple: 'var(--color-purple)',
|
||||
medal_promotion: 'var(--medal-promotion-text-orange, var(--color-orange))',
|
||||
}
|
||||
|
||||
// The 25%-opacity tint behind `chip`/`highlight`. Each accent has its own
|
||||
// token, so the tint must be looked up rather than derived.
|
||||
const highlightVariables: Record<ButtonColor, string> = {
|
||||
brand: 'var(--color-brand-highlight)',
|
||||
red: 'var(--color-red-highlight)',
|
||||
orange: 'var(--color-orange-highlight)',
|
||||
green: 'var(--color-green-highlight)',
|
||||
blue: 'var(--color-blue-highlight)',
|
||||
purple: 'var(--color-purple-highlight)',
|
||||
}
|
||||
|
||||
const props = withDefaults(
|
||||
@@ -101,7 +126,20 @@ const classes = computed(() => [
|
||||
props.circular ? '!rounded-full' : '',
|
||||
])
|
||||
const style = computed((): CSSProperties | undefined => {
|
||||
// `outlined` and `quiet` deliberately publish nothing without an explicit
|
||||
// colour, so their own `var(--button-color, <default>)` fallbacks apply —
|
||||
// that is what reproduces the legacy default ring and text colours.
|
||||
if ((props.type === 'outlined' || props.type === 'quiet') && !props.color) return undefined
|
||||
if (props.type === 'chip' || props.type === 'chip-text' || props.type === 'highlight') {
|
||||
// Without an accent these fall back to the page surface and the default
|
||||
// label, matching the legacy behaviour for an uncoloured chip.
|
||||
const accent = props.color
|
||||
if (!accent) return undefined
|
||||
return {
|
||||
'--button-color': colorVariables[accent],
|
||||
'--button-highlight': highlightVariables[accent],
|
||||
} as CSSProperties
|
||||
}
|
||||
if (
|
||||
props.type !== 'colored' &&
|
||||
props.type !== 'colored-text' &&
|
||||
@@ -163,11 +201,24 @@ defineExpose({ element })
|
||||
mask-composite: exclude;
|
||||
}
|
||||
|
||||
/* The ring is a real border so it participates in layout. Set here rather
|
||||
* than as a utility because it has to beat the transparent border colour in
|
||||
* `baseClasses`. */
|
||||
.button-frame--outlined {
|
||||
box-shadow: inset 0 0 0 1px var(--button-color, var(--surface-5));
|
||||
border-color: var(--button-color, var(--surface-5));
|
||||
}
|
||||
|
||||
.button-frame--quiet {
|
||||
color: var(--button-color, var(--color-base));
|
||||
color: var(--button-color, var(--color-text-default));
|
||||
}
|
||||
|
||||
/* `chip` and `highlight` are tinted fills rather than transparent ones, so they
|
||||
* keep the raised-button shadow. `chip-text` is the same tint without it --
|
||||
* the legacy `highlight-colored-text`, which never had a shadow. */
|
||||
.button-frame--chip,
|
||||
.button-frame--highlight {
|
||||
box-shadow:
|
||||
inset 0 0 0 1px var(--surface-5),
|
||||
0 1px 1px rgba(0, 0, 0, 0.12);
|
||||
}
|
||||
</style>
|
||||
@@ -1,56 +1,74 @@
|
||||
export type ButtonType = 'base' | 'colored' | 'colored-text' | 'outlined' | 'quiet'
|
||||
export type ButtonType =
|
||||
'base' | 'colored' | 'colored-text' | 'outlined' | 'quiet' | 'chip' | 'chip-text' | 'highlight'
|
||||
|
||||
export type ButtonSize = 'xs' | 'sm' | 'md' | 'lg' | 'xl'
|
||||
export type ButtonSize = '2xs' | 'xs' | 'sm' | 'md' | 'lg' | 'xl'
|
||||
|
||||
export type ButtonInteraction = 'surface' | 'filled' | 'none'
|
||||
|
||||
// TODO: Standardized color string enum props across @modrinth/ui
|
||||
export type ButtonColor =
|
||||
| 'brand'
|
||||
| 'red'
|
||||
| 'orange'
|
||||
| 'green'
|
||||
| 'blue'
|
||||
| 'purple'
|
||||
| 'medal_promotion'
|
||||
export type ButtonColor = 'brand' | 'red' | 'orange' | 'green' | 'blue' | 'purple'
|
||||
|
||||
export type ButtonVisualProps = {
|
||||
type ButtonVisualBase = {
|
||||
size?: ButtonSize
|
||||
interaction?: ButtonInteraction
|
||||
} & (
|
||||
}
|
||||
|
||||
type ButtonVisualWithoutInteraction =
|
||||
| {
|
||||
type?: 'base'
|
||||
color?: never
|
||||
interaction?: never
|
||||
}
|
||||
| {
|
||||
type: 'outlined'
|
||||
type: Exclude<ButtonType, 'base' | 'quiet'>
|
||||
color?: ButtonColor
|
||||
interaction?: never
|
||||
}
|
||||
| {
|
||||
type: 'colored'
|
||||
color?: ButtonColor
|
||||
}
|
||||
| {
|
||||
type: 'colored-text'
|
||||
color?: ButtonColor
|
||||
}
|
||||
| {
|
||||
type: 'quiet'
|
||||
color?: ButtonColor
|
||||
}
|
||||
)
|
||||
|
||||
/**
|
||||
* Public visual combinations supported by the current button frame.
|
||||
*
|
||||
* `interaction` only changes quiet buttons, so exposing it on any other type
|
||||
* would accept a prop that has no effect. Keeping that restriction here makes
|
||||
* Button and ButtonLink share one truthful visual contract.
|
||||
*/
|
||||
export type ButtonVisualProps = ButtonVisualBase &
|
||||
(
|
||||
| ButtonVisualWithoutInteraction
|
||||
| {
|
||||
type: 'quiet'
|
||||
color?: ButtonColor
|
||||
interaction?: ButtonInteraction
|
||||
}
|
||||
)
|
||||
|
||||
export type ButtonNativeType = 'button' | 'submit' | 'reset'
|
||||
|
||||
export interface ButtonProps {
|
||||
type?: ButtonType
|
||||
color?: ButtonColor
|
||||
size?: ButtonSize
|
||||
interaction?: ButtonInteraction
|
||||
nativeType?: ButtonNativeType
|
||||
disabled?: boolean
|
||||
loading?: boolean
|
||||
}
|
||||
/** Icon-only controls need a name because their slot has no visible label. */
|
||||
export type ButtonContentProps =
|
||||
| {
|
||||
iconOnly: true
|
||||
label: string
|
||||
}
|
||||
| {
|
||||
iconOnly?: false
|
||||
label?: never
|
||||
}
|
||||
|
||||
export type ButtonProps = ButtonVisualProps &
|
||||
ButtonContentProps & {
|
||||
circular?: boolean
|
||||
nativeType?: ButtonNativeType
|
||||
disabled?: boolean
|
||||
loading?: boolean
|
||||
}
|
||||
|
||||
export type ButtonLinkProps = ButtonVisualProps &
|
||||
ButtonContentProps & {
|
||||
as?: string | import('vue').Component
|
||||
href?: string
|
||||
disabled?: boolean
|
||||
circular?: boolean
|
||||
}
|
||||
|
||||
export interface ButtonElementHandle {
|
||||
element: HTMLElement | null
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
export { default as JLineCommandInput } from './components/JLineCommandInput.vue'
|
||||
export { default as LogViewport } from './components/LogViewport.vue'
|
||||
export * from './jline'
|
||||
export { default as ConsolePageLayout } from './layout.vue'
|
||||
export * from './providers'
|
||||
export * from './types'
|
||||
export * from './types'
|
||||
@@ -28,6 +28,13 @@ export function truncateLogToMaxBytes(content: string, maxBytes: number): string
|
||||
return textDecoder.decode(bytes.subarray(start))
|
||||
}
|
||||
|
||||
/**
|
||||
* Log sharing providers supported by the launcher. LogShare is the default
|
||||
* and is implemented in the desktop app through native Tauri commands; mclo.gs
|
||||
* is the network fallback used when LogShare is unreachable or unselected.
|
||||
*/
|
||||
export type LogShareProvider = 'logshare' | 'mclogs'
|
||||
|
||||
/**
|
||||
* Result of sharing log content. `truncated` is set when the content exceeded
|
||||
* `LOG_SHARE_MAX_BYTES` and only its tail was uploaded.
|
||||
@@ -39,12 +46,25 @@ export type LogShareResult = {
|
||||
|
||||
/**
|
||||
* Share log content through mclo.gs. Content larger than `LOG_SHARE_MAX_BYTES`
|
||||
* is trimmed to its last
|
||||
* `LOG_SHARE_MAX_BYTES` before uploading.
|
||||
* is trimmed to its last `LOG_SHARE_MAX_BYTES` before uploading.
|
||||
*/
|
||||
export async function shareLogs(
|
||||
client: AbstractModrinthClient,
|
||||
content: string,
|
||||
): Promise<LogShareResult> {
|
||||
return shareLogsWithProvider(client, content, 'mclogs')
|
||||
}
|
||||
|
||||
/**
|
||||
* Share log content through the given provider. The `logshare` provider is
|
||||
* only available inside the desktop app (native commands); this shared helper
|
||||
* routes the `mclogs` fallback so both launcher surfaces can share generic
|
||||
* log buffers.
|
||||
*/
|
||||
export async function shareLogsWithProvider(
|
||||
client: AbstractModrinthClient,
|
||||
content: string,
|
||||
_provider: LogShareProvider = 'mclogs',
|
||||
): Promise<LogShareResult> {
|
||||
const uploadContent = truncateLogToMaxBytes(content, LOG_SHARE_MAX_BYTES)
|
||||
const truncated = uploadContent !== content
|
||||
|
||||
Reference in new issue
Block a user