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:
2026-07-10 01:35:08 +08:00
parent 347cea1296
commit 5463319b40
4 changed files with 143 additions and 38 deletions
@@ -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 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@geo/desktop-client",
"version": "0.1.9",
"version": "0.1.10",
"private": true,
"description": "省心推客户端 — 连接和管理您的媒体账号,轻松发布和监控内容表现。",
"author": {
+60 -30
View File
@@ -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;