feat(desktop): default Kimi to fast mode and show release notes on update
- kimi adapter: parameterize mode switching (fast/thinking) and default to K2.6 快速; fix snapshot filtering so fast-mode "搜索网页" chips no longer wipe the answer body or get misclassified as reasoning/sidebar - desktop shell: display release notes in the client update modal - admin-web: fix publish-records table layout with fixed columns and ellipsis - bump desktop-client to 0.1.10 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -99,30 +99,31 @@ const publishRecordColumns = computed<TableColumnsType<PublishRecord>>(() => [
|
||||
title: t('media.records.platform'),
|
||||
dataIndex: 'platform_name',
|
||||
key: 'platform_name',
|
||||
width: 140,
|
||||
width: 128,
|
||||
},
|
||||
{
|
||||
title: t('media.records.status'),
|
||||
dataIndex: 'status',
|
||||
key: 'status',
|
||||
width: 130,
|
||||
width: 112,
|
||||
},
|
||||
{
|
||||
title: t('media.records.publishedAt'),
|
||||
dataIndex: 'published_at',
|
||||
key: 'published_at',
|
||||
width: 168,
|
||||
width: 126,
|
||||
},
|
||||
{
|
||||
title: '错误信息',
|
||||
dataIndex: 'error_message',
|
||||
key: 'error_message',
|
||||
width: 220,
|
||||
width: 190,
|
||||
},
|
||||
{
|
||||
title: t('media.records.link'),
|
||||
dataIndex: 'external_article_url',
|
||||
key: 'external_article_url',
|
||||
width: 244,
|
||||
},
|
||||
])
|
||||
|
||||
@@ -288,7 +289,7 @@ async function copyPublishLink(record: PublishRecord): Promise<void> {
|
||||
<template>
|
||||
<a-drawer
|
||||
:open="open"
|
||||
width="840"
|
||||
width="min(100vw, 960px)"
|
||||
:title="t('article.drawerTitle')"
|
||||
class="article-drawer"
|
||||
@close="handleClose"
|
||||
@@ -401,12 +402,15 @@ async function copyPublishLink(record: PublishRecord): Promise<void> {
|
||||
|
||||
<a-tab-pane key="published" :tab="t('media.records.tabTitle')">
|
||||
<a-table
|
||||
class="article-drawer__published-table"
|
||||
:columns="publishRecordColumns"
|
||||
:data-source="publishRecordsQuery.data.value || []"
|
||||
:loading="publishRecordsQuery.isPending.value"
|
||||
:pagination="false"
|
||||
row-key="id"
|
||||
size="small"
|
||||
table-layout="fixed"
|
||||
:scroll="{ x: 800 }"
|
||||
>
|
||||
<template #emptyText>{{ t('media.records.empty') }}</template>
|
||||
<template #bodyCell="{ column, record }">
|
||||
@@ -439,6 +443,7 @@ async function copyPublishLink(record: PublishRecord): Promise<void> {
|
||||
:href="resolvePublishLink(record)"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
:title="resolvePublishLink(record)"
|
||||
>
|
||||
{{ resolvePublishLink(record) }}
|
||||
</a>
|
||||
@@ -585,10 +590,18 @@ async function copyPublishLink(record: PublishRecord): Promise<void> {
|
||||
|
||||
.article-drawer__record-platform {
|
||||
display: flex;
|
||||
min-width: 0;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.article-drawer__record-platform strong,
|
||||
.article-drawer__record-platform span {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.article-drawer__record-platform span,
|
||||
.article-drawer__record-empty {
|
||||
color: var(--muted);
|
||||
@@ -613,6 +626,7 @@ async function copyPublishLink(record: PublishRecord): Promise<void> {
|
||||
}
|
||||
|
||||
.article-drawer__record-link {
|
||||
display: block;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
@@ -626,6 +640,8 @@ async function copyPublishLink(record: PublishRecord): Promise<void> {
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
min-width: 0;
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.article-drawer__record-copy.ant-btn {
|
||||
@@ -638,6 +654,27 @@ async function copyPublishLink(record: PublishRecord): Promise<void> {
|
||||
background: #e6f4ff;
|
||||
}
|
||||
|
||||
.article-drawer__published-table {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.article-drawer__published-table :deep(.ant-table) {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.article-drawer__published-table :deep(.ant-table table) {
|
||||
table-layout: fixed;
|
||||
}
|
||||
|
||||
.article-drawer__published-table :deep(.ant-table-thead > tr > th) {
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.article-drawer__published-table :deep(.ant-table-cell) {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
@media (max-width: 960px) {
|
||||
.article-drawer__hero {
|
||||
flex-direction: column;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@geo/desktop-client",
|
||||
"version": "0.1.9",
|
||||
"version": "0.1.10",
|
||||
"private": true,
|
||||
"description": "省心推客户端 — 连接和管理您的媒体账号,轻松发布和监控内容表现。",
|
||||
"author": {
|
||||
|
||||
@@ -7,7 +7,8 @@ import { normalizeText } from './common'
|
||||
const KIMI_BOOTSTRAP_URL = 'https://www.kimi.com/'
|
||||
const KIMI_PAGE_READY_TIMEOUT_MS = 20_000
|
||||
const KIMI_MODE_SWITCH_TIMEOUT_MS = 8_000
|
||||
// kimi 现在算力不够需要更多时间来生成回答了,尤其是思考模式,所以把默认的查询超时从 90s 提高到 250s。
|
||||
// kimi 算力紧张时回答生成慢(思考模式尤甚),查询超时保留 180s 兜底;
|
||||
// 现在默认切快速模式,正常情况下答案稳定后轮询会提前返回。
|
||||
const KIMI_QUERY_TIMEOUT_MS = 180_000
|
||||
const KIMI_QUERY_POLL_INTERVAL_MS = 1_200
|
||||
const KIMI_STABLE_POLLS_REQUIRED = 5
|
||||
@@ -15,7 +16,17 @@ const KIMI_INCOMPLETE_ANSWER_GRACE_MS = 18_000
|
||||
const KIMI_EDITOR_SELECTOR =
|
||||
'div[role="textbox"].chat-input-editor, .chat-input-editor[role="textbox"], [role="textbox"][class*="chat-input-editor"]'
|
||||
const KIMI_SEND_BUTTON_SELECTOR = '.send-button-container'
|
||||
const KIMI_THINKING_MODE_LABEL = 'K2.6 思考'
|
||||
|
||||
type KimiModeTarget = {
|
||||
label: string
|
||||
keyword: string
|
||||
}
|
||||
|
||||
// Kimi 网页端模型切换器的两档模式。思考档切换逻辑保留备用;
|
||||
// 采集现在默认切到快速档,要回退思考档时把 KIMI_TARGET_MODE 指回 KIMI_THINKING_MODE 即可。
|
||||
const KIMI_THINKING_MODE: KimiModeTarget = { label: 'K2.6 思考', keyword: '思考' }
|
||||
const KIMI_FAST_MODE: KimiModeTarget = { label: 'K2.6 快速', keyword: '快速' }
|
||||
const KIMI_TARGET_MODE: KimiModeTarget = KIMI_FAST_MODE
|
||||
const KIMI_REDIRECT_QUERY_KEYS = [
|
||||
'url',
|
||||
'u',
|
||||
@@ -1070,7 +1081,7 @@ function answerQualityScore(snapshot: KimiPageSnapshot | null): number {
|
||||
score += snapshot.reasoningBlocks.length * 80
|
||||
score += snapshot.citationLinks.length * 180
|
||||
score += Math.min(snapshot.searchResultLinks.length, 80) * 18
|
||||
if (snapshot.currentModelLabel?.includes('思考')) {
|
||||
if (snapshot.currentModelLabel?.includes(KIMI_TARGET_MODE.keyword)) {
|
||||
score += 120
|
||||
}
|
||||
if (snapshot.questionMatched) {
|
||||
@@ -1247,9 +1258,12 @@ async function submitKimiQuestion(
|
||||
await editor.press('Enter').catch(() => undefined)
|
||||
}
|
||||
|
||||
async function ensureKimiThinkingMode(page: PlaywrightPage): Promise<KimiModeSwitchResult> {
|
||||
async function ensureKimiMode(
|
||||
page: PlaywrightPage,
|
||||
target: KimiModeTarget,
|
||||
): Promise<KimiModeSwitchResult> {
|
||||
let result = await page.evaluate(
|
||||
async ({ preferredModelLabel, timeoutMs }) => {
|
||||
async ({ preferredModelLabel, modeKeyword, timeoutMs }) => {
|
||||
const wait = (timeMs: number) =>
|
||||
new Promise<void>((resolve) => {
|
||||
window.setTimeout(resolve, timeMs)
|
||||
@@ -1337,7 +1351,7 @@ async function ensureKimiThinkingMode(page: PlaywrightPage): Promise<KimiModeSwi
|
||||
return null
|
||||
}
|
||||
|
||||
const findThinkingOption = () => {
|
||||
const findModeOption = () => {
|
||||
const candidates = Array.from(
|
||||
document.querySelectorAll('button, [role="button"], [role="menuitem"], li, div, span'),
|
||||
)
|
||||
@@ -1358,7 +1372,7 @@ async function ensureKimiThinkingMode(page: PlaywrightPage): Promise<KimiModeSwi
|
||||
score = 10_000
|
||||
} else if (text.includes(preferredModelLabel)) {
|
||||
score = 8_000
|
||||
} else if (text.includes('思考')) {
|
||||
} else if (text.includes(modeKeyword)) {
|
||||
score = 3_000
|
||||
}
|
||||
|
||||
@@ -1385,7 +1399,7 @@ async function ensureKimiThinkingMode(page: PlaywrightPage): Promise<KimiModeSwi
|
||||
const deadline = Date.now() + timeoutMs
|
||||
while (Date.now() <= deadline) {
|
||||
const currentModelLabel = readModelLabel()
|
||||
if (currentModelLabel?.includes('思考')) {
|
||||
if (currentModelLabel?.includes(modeKeyword)) {
|
||||
return {
|
||||
ok: true as const,
|
||||
currentModelLabel,
|
||||
@@ -1411,20 +1425,20 @@ async function ensureKimiThinkingMode(page: PlaywrightPage): Promise<KimiModeSwi
|
||||
await wait(200)
|
||||
|
||||
const nextLabel = readModelLabel()
|
||||
if (nextLabel?.includes('思考')) {
|
||||
if (nextLabel?.includes(modeKeyword)) {
|
||||
return {
|
||||
ok: true as const,
|
||||
currentModelLabel: nextLabel,
|
||||
}
|
||||
}
|
||||
|
||||
const option = findThinkingOption()
|
||||
const option = findModeOption()
|
||||
if (option && clickElement(option)) {
|
||||
await wait(250)
|
||||
}
|
||||
|
||||
const resolvedLabel = readModelLabel()
|
||||
if (resolvedLabel?.includes('思考')) {
|
||||
if (resolvedLabel?.includes(modeKeyword)) {
|
||||
return {
|
||||
ok: true as const,
|
||||
currentModelLabel: resolvedLabel,
|
||||
@@ -1434,16 +1448,15 @@ async function ensureKimiThinkingMode(page: PlaywrightPage): Promise<KimiModeSwi
|
||||
|
||||
return {
|
||||
ok: false as const,
|
||||
error: readModelLabel()
|
||||
? 'kimi_thinking_mode_not_selected'
|
||||
: 'kimi_model_switch_trigger_missing',
|
||||
error: readModelLabel() ? 'kimi_mode_not_selected' : 'kimi_model_switch_trigger_missing',
|
||||
detail: buildDetail(),
|
||||
currentModelLabel: readModelLabel(),
|
||||
url: window.location.href || null,
|
||||
}
|
||||
},
|
||||
{
|
||||
preferredModelLabel: KIMI_THINKING_MODE_LABEL,
|
||||
preferredModelLabel: target.label,
|
||||
modeKeyword: target.keyword,
|
||||
timeoutMs: KIMI_MODE_SWITCH_TIMEOUT_MS,
|
||||
},
|
||||
)
|
||||
@@ -1460,10 +1473,10 @@ async function ensureKimiThinkingMode(page: PlaywrightPage): Promise<KimiModeSwi
|
||||
await trigger.click().catch(() => undefined)
|
||||
await sleep(250).catch(() => undefined)
|
||||
|
||||
for (const text of [KIMI_THINKING_MODE_LABEL, '思考']) {
|
||||
for (const text of [target.label, target.keyword]) {
|
||||
const option = page
|
||||
.getByText(text, {
|
||||
exact: text === KIMI_THINKING_MODE_LABEL,
|
||||
exact: text === target.label,
|
||||
})
|
||||
.first()
|
||||
if (await option.isVisible().catch(() => false)) {
|
||||
@@ -1473,7 +1486,7 @@ async function ensureKimiThinkingMode(page: PlaywrightPage): Promise<KimiModeSwi
|
||||
}
|
||||
}
|
||||
|
||||
result = await page.evaluate(() => {
|
||||
result = await page.evaluate(({ modeKeyword }) => {
|
||||
const normalize = (value: unknown): string | null => {
|
||||
if (typeof value !== 'string') {
|
||||
return null
|
||||
@@ -1493,7 +1506,7 @@ async function ensureKimiThinkingMode(page: PlaywrightPage): Promise<KimiModeSwi
|
||||
document.querySelector('.current-model')?.textContent,
|
||||
)
|
||||
|
||||
if (currentModelLabel?.includes('思考')) {
|
||||
if (currentModelLabel?.includes(modeKeyword)) {
|
||||
return {
|
||||
ok: true as const,
|
||||
currentModelLabel,
|
||||
@@ -1502,12 +1515,12 @@ async function ensureKimiThinkingMode(page: PlaywrightPage): Promise<KimiModeSwi
|
||||
|
||||
return {
|
||||
ok: false as const,
|
||||
error: 'kimi_thinking_mode_not_selected',
|
||||
error: 'kimi_mode_not_selected',
|
||||
detail: normalize(document.body?.innerText)?.slice(0, 200) ?? null,
|
||||
currentModelLabel,
|
||||
url: window.location.href || null,
|
||||
}
|
||||
})
|
||||
}, { modeKeyword: target.keyword })
|
||||
|
||||
return result
|
||||
}
|
||||
@@ -1598,6 +1611,13 @@ async function readKimiPageSnapshot(
|
||||
}
|
||||
|
||||
const isAuxiliaryPanelElement = (element: Element | null | undefined): boolean => {
|
||||
// 快速模式下"搜索网页 <query> N 个结果"chip 行渲染在回答顶部,assistant 消息
|
||||
// 的 innerText 以"搜索网页"开头;如果对会话区内的节点也套下面的面板文本规则,
|
||||
// 会把整条回答连同内部所有答案块一起排除,导致答案永远为空、任务拖满超时。
|
||||
// 面板文本规则只对会话区之外的节点生效(真正的面板另有 side-console 类名兜底)。
|
||||
const insideChatContent = Boolean(
|
||||
element?.closest?.('.chat-content-item, .chat-content-list, .chat-content-container'),
|
||||
)
|
||||
let current: Element | null = element ?? null
|
||||
for (let depth = 0; current && depth < 10; depth += 1) {
|
||||
const descriptor = [
|
||||
@@ -1610,13 +1630,19 @@ async function readKimiPageSnapshot(
|
||||
current instanceof HTMLElement
|
||||
? (normalizeInline(current.innerText)?.slice(0, 160) ?? '')
|
||||
: ''
|
||||
// 应用左侧边栏(aside.sidebar.next-sidebar)里的会话历史标题就是监控问题原文,
|
||||
// 正文未渲染完时会被当成最高分答案候选采走,所以把侧边栏整体视为辅助面板。
|
||||
// 注意不能用 /sidebar/ 子串匹配祖先:应用根节点挂着 has-sidebar / sidebar-interactive
|
||||
// 状态类,会把正文一起误杀,这里只认 aside/nav 标签和侧栏专属类名。
|
||||
if (
|
||||
/(side-console|console|drawer|modal|popover|popup|tooltip|floating|hover-card|dropdown|search-result|search-panel|reference-panel|ref-panel|citation-panel|dialog|alertdialog)/i.test(
|
||||
/(side-console|console|drawer|modal|popover|popup|tooltip|floating|hover-card|dropdown|search-result|search-panel|reference-panel|ref-panel|citation-panel|dialog|alertdialog|next-sidebar|sidebar-nav|history-list|history-item|session-list)/i.test(
|
||||
descriptor,
|
||||
) ||
|
||||
current.tagName === 'DIALOG' ||
|
||||
current.tagName === 'ASIDE' ||
|
||||
current.tagName === 'NAV' ||
|
||||
current.getAttribute('aria-modal') === 'true' ||
|
||||
/^(引用来源|搜索网页)\s*\d*/.test(text)
|
||||
(!insideChatContent && /^(引用来源|搜索网页)\s*\d*/.test(text))
|
||||
) {
|
||||
return true
|
||||
}
|
||||
@@ -2468,11 +2494,12 @@ async function readKimiPageSnapshot(
|
||||
|
||||
const resolveCandidateKind = (element: HTMLElement, text: string): CandidateKind => {
|
||||
const className = normalizeInline(element.getAttribute('class'))?.toLowerCase() ?? ''
|
||||
const merged = `${className}\n${text}`
|
||||
// 不能用包含匹配:快速模式的"搜索网页 …"chip 在正文顶部,会话容器的文本
|
||||
// 必然含这串字样,包含匹配会把整个正文容器误判成 reasoning、答案清零。
|
||||
// 只有文本以检索/思考标记开头(或类名带 think/reason)才算 reasoning 块。
|
||||
if (
|
||||
/think|reason|analysis|thought/.test(className) ||
|
||||
/^思考已完成/.test(text) ||
|
||||
/(搜索网页|搜索结果|检索关键词|思考过程)/.test(merged)
|
||||
/^(思考已完成|搜索网页|搜索结果|检索关键词|思考过程)/.test(text)
|
||||
) {
|
||||
return 'reasoning'
|
||||
}
|
||||
@@ -3091,14 +3118,14 @@ export const kimiAdapter: MonitorAdapter = {
|
||||
}
|
||||
|
||||
context.reportProgress('kimi.mode_switch')
|
||||
const modeSwitchResult = await ensureKimiThinkingMode(page)
|
||||
const modeSwitchResult = await ensureKimiMode(page, KIMI_TARGET_MODE)
|
||||
if (!modeSwitchResult.ok) {
|
||||
const isAuthFailure = modeSwitchResult.error === 'kimi_login_required'
|
||||
return {
|
||||
status: 'failed',
|
||||
summary: isAuthFailure
|
||||
? 'Kimi 账号未登录或登录态已失效,请先在 desktop client 中重新授权。'
|
||||
: 'Kimi 未能切换到 K2.6 思考,已中止本次监控任务。',
|
||||
: `Kimi 未能切换到 ${KIMI_TARGET_MODE.label},已中止本次监控任务。`,
|
||||
error: buildAdapterError(
|
||||
modeSwitchResult.error,
|
||||
modeSwitchResult.detail ?? modeSwitchResult.error,
|
||||
@@ -3152,7 +3179,7 @@ export const kimiAdapter: MonitorAdapter = {
|
||||
const reasoning = normalizeText(finalSnapshot.reasoningText)
|
||||
const answerComplete = isKimiAnswerComplete(finalSnapshot)
|
||||
const keywordBlobDetected = answer ? isKimiKeywordBlob(answer) : false
|
||||
const providerModel = normalizeText(finalSnapshot.currentModelLabel) ?? KIMI_THINKING_MODE_LABEL
|
||||
const providerModel = normalizeText(finalSnapshot.currentModelLabel) ?? KIMI_TARGET_MODE.label
|
||||
const conversationID = extractConversationID(finalSnapshot.url)
|
||||
|
||||
if (!queryResult.ok && queryResult.error === 'kimi_login_expired') {
|
||||
@@ -3236,7 +3263,7 @@ export const kimiAdapter: MonitorAdapter = {
|
||||
page_url: finalSnapshot.url,
|
||||
page_title: finalSnapshot.title,
|
||||
model_label: finalSnapshot.currentModelLabel,
|
||||
thinking_mode_requested: KIMI_THINKING_MODE_LABEL,
|
||||
mode_requested: KIMI_TARGET_MODE.label,
|
||||
login_required: finalSnapshot.loginRequired,
|
||||
busy: finalSnapshot.busy,
|
||||
busy_signals: finalSnapshot.busySignals,
|
||||
@@ -3284,6 +3311,9 @@ export const kimiAdapter: MonitorAdapter = {
|
||||
}
|
||||
|
||||
export const __kimiTestUtils = {
|
||||
KIMI_FAST_MODE,
|
||||
KIMI_TARGET_MODE,
|
||||
KIMI_THINKING_MODE,
|
||||
buildSourceItem,
|
||||
classifyKimiSources,
|
||||
dedupeSourceItems,
|
||||
|
||||
@@ -39,6 +39,7 @@ const updateModalOpen = ref(false)
|
||||
const updateModalVersion = ref('')
|
||||
const updateModalForce = ref(false)
|
||||
const updateModalStarted = ref(false)
|
||||
const updateModalNotes = ref('')
|
||||
const bugReportOpen = ref(false)
|
||||
const bugReportSubmitting = ref(false)
|
||||
type BugReportSeverity = 'low' | 'medium' | 'high' | 'critical'
|
||||
@@ -227,9 +228,14 @@ async function startClientReleaseUpdate() {
|
||||
}
|
||||
}
|
||||
|
||||
function notifyClientUpdate(latestVersion: string, forceUpdate: boolean) {
|
||||
function notifyClientUpdate(
|
||||
latestVersion: string,
|
||||
forceUpdate: boolean,
|
||||
releaseNotes?: string | null,
|
||||
) {
|
||||
updateModalVersion.value = latestVersion
|
||||
updateModalForce.value = forceUpdate
|
||||
updateModalNotes.value = releaseNotes?.trim() ?? ''
|
||||
updateModalStarted.value = false
|
||||
updateModalOpen.value = true
|
||||
}
|
||||
@@ -262,7 +268,7 @@ async function checkClientReleaseOnce() {
|
||||
if (!result?.update_available) {
|
||||
return
|
||||
}
|
||||
notifyClientUpdate(result.latest_version, result.force_update)
|
||||
notifyClientUpdate(result.latest_version, result.force_update, result.release_notes)
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
@@ -402,6 +408,11 @@ watch(
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div v-if="updateModalNotes && !isUpdateFailed" class="client-update-notes">
|
||||
<strong>版本介绍</strong>
|
||||
<p>{{ updateModalNotes }}</p>
|
||||
</div>
|
||||
|
||||
<div v-if="updateModalStarted && !isUpdateFailed" class="client-update-progress">
|
||||
<div class="client-update-progress__meta">
|
||||
<strong>{{ updateProgressLabel }}</strong>
|
||||
@@ -996,6 +1007,33 @@ h1 {
|
||||
color: #0958d9;
|
||||
}
|
||||
|
||||
.client-update-notes {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
max-height: 180px;
|
||||
overflow-y: auto;
|
||||
padding: 14px 16px;
|
||||
border: 1px solid #eef2f7;
|
||||
border-radius: 10px;
|
||||
background: #f8fafc;
|
||||
}
|
||||
|
||||
.client-update-notes strong {
|
||||
color: #334155;
|
||||
font-size: 13px;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.client-update-notes p {
|
||||
margin: 0;
|
||||
color: #475569;
|
||||
font-size: 14px;
|
||||
line-height: 1.7;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.client-update-progress {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
Reference in New Issue
Block a user