Skip to content

Commit aed8c4c

Browse files
committed
fix: sync default welcome text with display language
1 parent beffeb2 commit aed8c4c

19 files changed

Lines changed: 574 additions & 97 deletions

File tree

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
"""Exercise the actual UI schema and save handler with an isolated session.
2+
3+
Empty welcome strings must overwrite old custom text; omitted fields must not.
4+
This does not exercise FastAPI authentication, file uploads or a real database.
5+
"""
6+
7+
import ast
8+
import asyncio
9+
import json
10+
from pathlib import Path
11+
from types import SimpleNamespace
12+
from typing import Optional
13+
from unittest.mock import AsyncMock, Mock
14+
15+
import pytest
16+
from pydantic import BaseModel, Field
17+
18+
ROOT = Path(__file__).resolve().parents[1]
19+
20+
21+
def load_node(path, name, namespace):
22+
node = next(
23+
n for n in ast.parse(path.read_text()).body if getattr(n, "name", None) == name
24+
)
25+
if isinstance(node, ast.AsyncFunctionDef):
26+
node.decorator_list = []
27+
exec(
28+
compile(ast.Module(body=[node], type_ignores=[]), str(path), "exec"), namespace
29+
)
30+
31+
32+
@pytest.mark.parametrize(
33+
"fields,expected",
34+
[
35+
({"welcome": "", "welcome_desc": ""}, {"welcome": "", "welcome_desc": ""}),
36+
(
37+
{"welcome": "New custom title"},
38+
{"welcome": "New custom title", "welcome_desc": "Custom description"},
39+
),
40+
(
41+
{"theme": "#123456"},
42+
{"welcome": "Custom title", "welcome_desc": "Custom description"},
43+
),
44+
],
45+
)
46+
def test_save_welcome_defaults(fields, expected):
47+
model = SimpleNamespace(
48+
id=42,
49+
configuration=json.dumps(
50+
{
51+
"welcome": "Custom title",
52+
"welcome_desc": "Custom description",
53+
"auto_ds": True,
54+
}
55+
),
56+
)
57+
namespace = {
58+
"BaseModel": BaseModel,
59+
"Field": Field,
60+
"Optional": Optional,
61+
"List": list,
62+
"json": json,
63+
"SessionDep": object,
64+
"CurrentUser": object,
65+
"Trans": object,
66+
"UploadFile": object,
67+
"Form": lambda: None,
68+
"get_ws_assistant": Mock(return_value=model),
69+
"clear_ui_cache": AsyncMock(),
70+
}
71+
load_node(ROOT / "common/core/schemas.py", "BaseCreatorDTO", namespace)
72+
load_node(
73+
ROOT / "apps/system/schemas/system_schema.py", "AssistantUiSchema", namespace
74+
)
75+
load_node(ROOT / "apps/system/api/assistant.py", "ui", namespace)
76+
session, user, trans = Mock(), object(), object()
77+
result = asyncio.run(
78+
namespace["ui"](
79+
session, user, trans, json.dumps({"id": 42, **fields}), files=[]
80+
)
81+
)
82+
stored = json.loads(result.configuration)
83+
assert {key: stored[key] for key in expected} == expected
84+
assert stored["auto_ds"] is True
85+
if "theme" in fields:
86+
assert stored["theme"] == fields["theme"]
87+
namespace["get_ws_assistant"].assert_called_once_with(session, 42, user, trans)
88+
session.add.assert_called_once_with(model)
89+
session.commit.assert_called_once()
90+
namespace["clear_ui_cache"].assert_awaited_once_with(42)

‎docs/agents/i18n.md‎

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,21 @@
1616

1717
可以用脚本比较键路径,但不要为了普通文案变更引入新依赖。
1818

19+
## 欢迎语与恢复默认
20+
21+
- 助手的 `welcome` / `welcome_desc` 和全局外观的 `pc_welcome` / `pc_welcome_desc` / `slogan`
22+
使用空字符串表示默认文案。保存接口保留空字符串;不要改为省略字段或传 `null`,
23+
助手 PATCH 会忽略这两种输入,无法覆盖旧配置。
24+
- 显示和编辑入口共用 `frontend/src/i18n/welcome.ts`。默认文案按当前 locale 解析;
25+
编辑框用翻译后的默认文案作为 placeholder,预览显示解析结果,恢复默认后保存空字符串。
26+
- 兼容旧编辑器保存的默认文本:仅当内容与五种语言的系统默认文案完全一致时,
27+
按默认模式处理,包括两种历史说明文案。其余自定义文本原样保留,不 trim 或自动翻译。
28+
历史数据没有来源标记,因此手工输入且恰好等于系统默认文案的内容也按默认处理。
29+
- 普通问数首页和页面嵌入共用全局外观文案;基础、高级助手的浮窗和全屏共用助手文案。
30+
修改时覆盖这些入口及恢复默认、保存、重新打开、切换语言的链路。
31+
- 外观设置的登录区域与平台区域分别恢复各自的默认值。登录欢迎语 `slogan` 的预览和
32+
实际登录页也按上述规则解析;是否显示仍由 `showSlogan` 开关控制。
33+
1934
## 新增语言
2035

2136
新增语言不是只添加 JSON 文件。以提交 `21961e2d`(Thai / `th-TH`)为完整示例,逐项检查:

‎frontend/src/i18n/ko-KR.json‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -142,7 +142,7 @@
142142
"invalid_embedding_parameter": "유사도는 0~1 사이의 유한한 수여야 하며 후보 수는 양의 정수여야 합니다."
143143
},
144144
"prompt": {
145-
"default_password:": "기본 비밀번호:{msg}",
145+
"default_password": "기본 비밀번호:{msg}",
146146
"no_sql_sample": "현재 사용 가능한 SQL 샘플 라이브러리가 없습니다",
147147
"add_sql_sample": "SQL 샘플 라이브러리를 추가하세요",
148148
"no_prompt_words": "프롬프트가 없습니다",

‎frontend/src/i18n/welcome.ts‎

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import en from './en.json'
2+
import zhCN from './zh-CN.json'
3+
import zhTW from './zh-TW.json'
4+
import koKR from './ko-KR.json'
5+
import thTH from './th-TH.json'
6+
7+
type WelcomeKey =
8+
| 'embedded.i_am_sqlbot'
9+
| 'embedded.data_analysis_now'
10+
| 'qa.hint_description'
11+
| 'common.intelligent_questioning_platform'
12+
13+
const messages = [en, zhCN, zhTW, koKR, thTH]
14+
const defaultTitles = new Set(messages.map((message) => message.embedded.i_am_sqlbot))
15+
const defaultDescriptions = new Set(
16+
messages.flatMap((message) => [message.embedded.data_analysis_now, message.qa.hint_description])
17+
)
18+
const defaultSlogans = new Set(
19+
messages.map((message) => message.common.intelligent_questioning_platform)
20+
)
21+
const defaultsByKey: Record<WelcomeKey, Set<string>> = {
22+
'embedded.i_am_sqlbot': defaultTitles,
23+
'embedded.data_analysis_now': defaultDescriptions,
24+
'qa.hint_description': defaultDescriptions,
25+
'common.intelligent_questioning_platform': defaultSlogans,
26+
}
27+
28+
// An empty string means "follow the display language" in both existing save APIs.
29+
// Older editors persisted translated defaults. Match only exact stock strings;
30+
// never trim, translate or otherwise rewrite a user's custom text.
31+
export function normalizeWelcome(value: string | null | undefined, key: WelcomeKey): string {
32+
const defaults = defaultsByKey[key]
33+
return value == null || defaults.has(value) ? '' : value
34+
}
35+
36+
export function resolveWelcome(
37+
value: string | null | undefined,
38+
key: WelcomeKey,
39+
translate: (key: WelcomeKey) => string
40+
): string {
41+
return normalizeWelcome(value, key) || translate(key)
42+
}

‎frontend/src/i18n/zh-TW.json‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -883,8 +883,8 @@
883883
"version_num": "版本號",
884884
"standard": "社區版",
885885
"enterprise": "企業版",
886-
"professional": "專業版",
887-
"embedded": "嵌入式版",
886+
"Professional": "專業版",
887+
"Embedded": "嵌入式版",
888888
"support": "取得技術支援",
889889
"update_success": "更新成功,請重新登入",
890890
"serial_no": "序號",

‎frontend/src/views/chat/index.vue‎

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -120,13 +120,10 @@
120120
><custom_small v-if="appearanceStore.themeColor !== 'default'"></custom_small>
121121
<LOGO_fold v-else></LOGO_fold
122122
></el-icon>
123-
<span>{{ appearanceStore.pc_welcome ?? '你好,我是 SQLBot' }}</span>
123+
<span>{{ pageWelcome }}</span>
124124
</div>
125125
<div class="sub">
126-
{{
127-
appearanceStore.pc_welcome_desc ??
128-
'我可以查询数据、生成图表、检测数据异常、预测数据等赶快开启智能问数吧~'
129-
}}
126+
{{ pageWelcomeDesc }}
130127
</div>
131128
</template>
132129

@@ -467,6 +464,7 @@ import ErrorInfo from './ErrorInfo.vue'
467464
import ChatToolBar from './ChatToolBar.vue'
468465
import { dsTypeWithImg } from '@/views/ds/js/ds-type'
469466
import { useI18n } from 'vue-i18n'
467+
import { resolveWelcome } from '@/i18n/welcome'
470468
import { find, forEach } from 'lodash-es'
471469
import custom_small from '@/assets/svg/logo-custom_small.svg'
472470
import LOGO_fold from '@/assets/LOGO-fold.svg'
@@ -519,6 +517,12 @@ const customName = computed(() => {
519517
return ''
520518
})
521519
const { t } = useI18n()
520+
const pageWelcome = computed(() =>
521+
resolveWelcome(appearanceStore.pc_welcome, 'embedded.i_am_sqlbot', t)
522+
)
523+
const pageWelcomeDesc = computed(() =>
524+
resolveWelcome(appearanceStore.pc_welcome_desc, 'qa.hint_description', t)
525+
)
522526
523527
const chatConfig = useChatConfigStore()
524528

‎frontend/src/views/embedded/index.vue‎

Lines changed: 9 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -22,25 +22,16 @@
2222
<chat-component
2323
v-if="!loading"
2424
ref="chatRef"
25-
:welcome="customSet.welcome"
26-
:welcome-desc="customSet.welcome_desc"
25+
:welcome="welcome"
26+
:welcome-desc="welcomeDesc"
2727
:logo-assistant="logo"
2828
/>
2929
</div>
3030
<div class="drawer-assistant" @click="openHistory"></div>
3131
</div>
3232
</template>
3333
<script setup lang="ts">
34-
import {
35-
onBeforeMount,
36-
nextTick,
37-
onBeforeUnmount,
38-
ref,
39-
onMounted,
40-
reactive,
41-
computed,
42-
watch,
43-
} from 'vue'
34+
import { onBeforeMount, nextTick, onBeforeUnmount, ref, onMounted, reactive, computed } from 'vue'
4435
import ChatComponent from '@/views/chat/index.vue'
4536
import LOGO from '@/assets/svg/logo-custom_small.svg'
4637
import icon_new_chat_outlined from '@/assets/svg/icon_new_chat_outlined.svg'
@@ -50,7 +41,7 @@ import { useRoute } from 'vue-router'
5041
import { useAssistantStore } from '@/stores/assistant'
5142
import { setCurrentColor } from '@/utils/utils'
5243
import { useI18n } from 'vue-i18n'
53-
import { i18n } from '@/i18n'
44+
import { resolveWelcome } from '@/i18n/welcome'
5445
import { useUserStore } from '@/stores/user'
5546
const userStore = useUserStore()
5647
@@ -147,27 +138,15 @@ onMounted(() => {
147138
})
148139
const customSet = reactive({
149140
name: '',
150-
welcome: t('embedded.i_am_sqlbot'),
151-
welcome_desc: t('embedded.data_analysis_now'),
141+
welcome: '',
142+
welcome_desc: '',
152143
theme: '#1CBA90',
153144
header_font_color: '#1F2329',
154145
}) as { [key: string]: any }
155146
156-
// 记录哪些字段被服务端 API 配置覆盖过,避免被 locale watcher 覆盖
157-
const configuredKeys = new Set<string>()
158-
159-
// 监听 locale 变化,动态更新未被子定义覆盖的翻译字段
160-
watch(
161-
() => i18n.global.locale.value,
162-
() => {
163-
if (!configuredKeys.has('welcome')) {
164-
customSet.welcome = t('embedded.i_am_sqlbot')
165-
}
166-
if (!configuredKeys.has('welcome_desc')) {
167-
customSet.welcome_desc = t('embedded.data_analysis_now')
168-
}
169-
},
170-
{ immediate: true }
147+
const welcome = computed(() => resolveWelcome(customSet.welcome, 'embedded.i_am_sqlbot', t))
148+
const welcomeDesc = computed(() =>
149+
resolveWelcome(customSet.welcome_desc, 'embedded.data_analysis_now', t)
171150
)
172151
173152
const logo = ref()
@@ -243,7 +222,6 @@ onBeforeMount(async () => {
243222
![null, undefined].includes(rawData[key])
244223
) {
245224
customSet[key] = rawData[key]
246-
configuredKeys.add(key)
247225
}
248226
}
249227

‎frontend/src/views/embedded/page.vue‎

Lines changed: 9 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,8 @@
66
<chat-component
77
v-if="!loading && tokenReady"
88
ref="chatRef"
9-
:welcome="customSet.welcome"
10-
:welcome-desc="customSet.welcome_desc"
9+
:welcome="welcome"
10+
:welcome-desc="welcomeDesc"
1111
:logo-assistant="logo"
1212
:page-embedded="true"
1313
:app-name="customSet.name"
@@ -16,13 +16,13 @@
1616
</template>
1717
<script setup lang="ts">
1818
import ChatComponent from '@/views/chat/index.vue'
19-
import { nextTick, onBeforeMount, onBeforeUnmount, reactive, ref, watch } from 'vue'
19+
import { nextTick, onBeforeMount, onBeforeUnmount, reactive, ref, watch, computed } from 'vue'
2020
import { useRoute } from 'vue-router'
2121
// import { assistantApi } from '@/api/assistant'
2222
import { useAssistantStore } from '@/stores/assistant'
2323
import { useAppearanceStoreWithOut } from '@/stores/appearance'
2424
import { useI18n } from 'vue-i18n'
25-
import { i18n } from '@/i18n'
25+
import { resolveWelcome } from '@/i18n/welcome'
2626
import { setCurrentColor } from '@/utils/utils'
2727
import { useUserStore } from '@/stores/user'
2828
const userStore = useUserStore()
@@ -36,27 +36,15 @@ const assistantName = ref('')
3636
const dynamicType = ref(0)
3737
const customSet = reactive({
3838
name: '',
39-
welcome: t('embedded.i_am_sqlbot'),
40-
welcome_desc: t('embedded.data_analysis_now'),
39+
welcome: '',
40+
welcome_desc: '',
4141
theme: '#1CBA90',
4242
header_font_color: '#1F2329',
4343
}) as { [key: string]: any }
4444
45-
// 记录哪些字段被服务端 API 配置覆盖过,避免被 locale watcher 覆盖
46-
const configuredKeys = new Set<string>()
47-
48-
// 监听 locale 变化,动态更新未被子定义覆盖的翻译字段
49-
watch(
50-
() => i18n.global.locale.value,
51-
() => {
52-
if (!configuredKeys.has('welcome')) {
53-
customSet.welcome = t('embedded.i_am_sqlbot')
54-
}
55-
if (!configuredKeys.has('welcome_desc')) {
56-
customSet.welcome_desc = t('embedded.data_analysis_now')
57-
}
58-
},
59-
{ immediate: true }
45+
const welcome = computed(() => resolveWelcome(customSet.welcome, 'embedded.i_am_sqlbot', t))
46+
const welcomeDesc = computed(() =>
47+
resolveWelcome(customSet.welcome_desc, 'embedded.data_analysis_now', t)
6048
)
6149
6250
const logo = ref()
@@ -244,7 +232,6 @@ onBeforeMount(async () => {
244232
![null, undefined].includes(rawData[key])
245233
) {
246234
customSet[key] = rawData[key]
247-
configuredKeys.add(key)
248235
}
249236
}
250237

‎frontend/src/views/login/index.vue‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@
2828
}}</span>
2929
</div>
3030
<div v-if="appearanceStore.getShowSlogan" class="welcome">
31-
{{ appearanceStore.slogan ?? $t('common.intelligent_questioning_platform') }}
31+
{{ loginSlogan }}
3232
</div>
3333
<div v-else class="welcome" style="height: 0"></div>
3434
<div class="login-form">
@@ -88,6 +88,7 @@ import { ref, computed, onMounted, nextTick } from 'vue'
8888
import { useRouter } from 'vue-router'
8989
import { useUserStore } from '@/stores/user'
9090
import { useI18n } from 'vue-i18n'
91+
import { resolveWelcome } from '@/i18n/welcome'
9192
import custom_small from '@/assets/svg/logo-custom_small.svg'
9293
import LOGO_fold from '@/assets/LOGO-fold.svg'
9394
import login_image from '@/assets/embedded/login_image.png'
@@ -103,6 +104,9 @@ const router = useRouter()
103104
const userStore = useUserStore()
104105
const appearanceStore = useAppearanceStoreWithOut()
105106
const { t } = useI18n()
107+
const loginSlogan = computed(() =>
108+
resolveWelcome(appearanceStore.slogan, 'common.intelligent_questioning_platform', t)
109+
)
106110
const loginForm = ref({
107111
username: '',
108112
password: '',

0 commit comments

Comments
 (0)