Skip to content

Commit 8486912

Browse files
asmh1989asmh1989lyingbug
authored
fix(i18n): 修复内置智能体名称与描述不随界面语言切换的问题 (#2828)
* fix(i18n): 修复内置智能体名称与描述不随界面语言切换的问题 问题现象:切换界面语言后,内置智能体的名称/描述仍显示旧语言, 需要强制刷新(Ctrl+F5)才能恢复正确语言。 35820eb 已在 GetAgentByID / ListAgents 读取路径按请求语言重新本地化, 本提交补全剩余场景: 后端: - 共享智能体读取(sharedAgentInfo / GetSharedAgentForTenant)与组织 共享列表(ListOrgAgentShares)补 ApplyBuiltinAgentLocalization; - IM 渠道列表(ListChannelsByTenant)改传请求 ctx,内置渠道的 AgentName 按调用方语言从 YAML i18n 重新解析(DB 列可能为空或为 落库时语言的残留值); - 写入侧收敛:内置智能体配置落库时持久化 YAML default 语言的展示 字段(GetBuiltinAgent 而非 GetBuiltinAgentWithContext,与 locale 无关),更新/创建接口返回值再按调用方语言本地化,避免未覆盖的 读取路径显示随机语言。 前端: - utils/request.ts 导出 getCurrentLanguage; - chatResources 的 agents 缓存与 orgStore 的 sharedAgents 缓存在 新鲜度判断中加入"加载时语言 == 当前语言",切换语言后缓存立即 失效,下次访问按新 Accept-Language 重新拉取并响应式更新。 * fix(i18n): 按请求发起时的语言给智能体缓存打戳,避免切换语言时命中旧数据 落地预取尚未返回就改界面语言时,原先会把 zh-CN 响应标成 en-US 新鲜缓存,最多冻 60s。in-flight 只复用同语言请求,写入前校验代际,并补共享/IM 本地化回归测试。 --------- Co-authored-by: asmh1989 <minhua.sun@velavigo.com> Co-authored-by: wizardchen <wizardchen@tencent.com>
1 parent 2a20733 commit 8486912

15 files changed

Lines changed: 330 additions & 36 deletions

frontend/src/api/agent/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -329,7 +329,7 @@ export interface IMChannelOverview {
329329
id: string;
330330
tenant_id: number;
331331
agent_id: string;
332-
agent_name: string; // empty string for built-in agents
332+
agent_name: string; // localized built-in name when the agent is built-in
333333
platform: IMChannel['platform'];
334334
name: string;
335335
enabled: boolean;

frontend/src/stores/chatResources.ts

Lines changed: 60 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,17 @@
11
import { defineStore } from 'pinia'
2-
import { ref, computed } from 'vue'
2+
import { ref, computed, watch } from 'vue'
33
import { listKnowledgeBases, getKnowledgeBaseById } from '@/api/knowledge-base'
44
import { listAgents, type CustomAgent } from '@/api/agent'
55
import { listModels, type ModelConfig } from '@/api/model'
66
import { listWebSearchProviders, type WebSearchProviderEntity } from '@/api/web-search-provider'
77
import { isNamedSandboxBackend, listSandboxConfigs, type SandboxConfigRecord } from '@/api/system'
88
import { useOrganizationStore } from '@/stores/organization'
9+
import { getCurrentLanguage } from '@/utils/request'
10+
import {
11+
isLocalizedCacheFresh,
12+
shouldCommitLocalizedGeneration,
13+
shouldReuseLocalizedInflight,
14+
} from './localizedResourceCache'
915

1016
/** 空间级资源缓存 TTL */
1117
const CACHE_TTL_MS = 60_000
@@ -49,11 +55,37 @@ export const useChatResourcesStore = defineStore('chatResources', () => {
4955
const validKnowledgeBases = computed(() => rawKnowledgeBases.value.filter(isKbModelReady))
5056
const chatModels = computed(() => allModels.value.filter((m) => m.type === 'KnowledgeQA'))
5157

58+
// 内置智能体名称/描述由后端按 Accept-Language 本地化返回;切换 UI 语言后
59+
// 旧缓存必须立即失效,否则要等 TTL 过期或强刷才能看到正确语言。
60+
// agentsLoadedLocale 必须是「请求发起时」的语言,不能在 await 之后再读当前语言。
61+
let agentsLoadedLocale = ''
62+
let agentsAllInflightLocale = ''
63+
5264
function isFresh(key: ResourceKey): boolean {
53-
const at = loadedAt.value[key]
54-
return !!at && Date.now() - at < CACHE_TTL_MS
65+
const at = loadedAt.value[key] ?? 0
66+
if (key === 'agents') {
67+
return isLocalizedCacheFresh(at, agentsLoadedLocale, getCurrentLanguage(), CACHE_TTL_MS)
68+
}
69+
return at > 0 && Date.now() - at < CACHE_TTL_MS
5570
}
5671

72+
function bumpAgentsGeneration() {
73+
agentsAllGen++
74+
agentsAllInflight = null
75+
agentsAllInflightLocale = ''
76+
}
77+
78+
watch(
79+
() => getCurrentLanguage(),
80+
(locale) => {
81+
if (agentsLoadedLocale && agentsLoadedLocale !== locale) {
82+
delete loadedAt.value.agents
83+
agentsLoadedLocale = ''
84+
bumpAgentsGeneration()
85+
}
86+
},
87+
)
88+
5789
async function runOnce(key: ResourceKey, force: boolean, loader: () => Promise<void>): Promise<void> {
5890
if (!force && isFresh(key)) return
5991
const existing = inflight.get(key)
@@ -125,12 +157,20 @@ export const useChatResourcesStore = defineStore('chatResources', () => {
125157
return { data: res.data || [], disabled_own_agent_ids: res.disabled_own_agent_ids || [] }
126158
}
127159

160+
const locale = getCurrentLanguage()
128161
if (!force && isFresh('agents')) {
129162
return { data: agents.value, disabled_own_agent_ids: disabledOwnAgentIds.value }
130163
}
131-
if (!force && agentsAllInflight) return agentsAllInflight
164+
if (
165+
!force &&
166+
shouldReuseLocalizedInflight(!!agentsAllInflight, agentsAllInflightLocale, locale)
167+
) {
168+
return agentsAllInflight as Promise<{ data: CustomAgent[]; disabled_own_agent_ids: string[] }>
169+
}
132170

133171
const gen = ++agentsAllGen
172+
const requestLocale = locale
173+
agentsAllInflightLocale = requestLocale
134174
agentsAllInflight = (async () => {
135175
try {
136176
const [agentsRes] = await Promise.all([
@@ -139,12 +179,19 @@ export const useChatResourcesStore = defineStore('chatResources', () => {
139179
])
140180
const res = agentsRes as { data?: CustomAgent[]; disabled_own_agent_ids?: string[] }
141181
const data = res.data || []
142-
agents.value = data
143-
disabledOwnAgentIds.value = res.disabled_own_agent_ids || []
144-
loadedAt.value.agents = Date.now()
145-
return { data, disabled_own_agent_ids: res.disabled_own_agent_ids || [] }
182+
const disabled = res.disabled_own_agent_ids || []
183+
if (shouldCommitLocalizedGeneration(gen, agentsAllGen)) {
184+
agents.value = data
185+
disabledOwnAgentIds.value = disabled
186+
loadedAt.value.agents = Date.now()
187+
agentsLoadedLocale = requestLocale
188+
}
189+
return { data, disabled_own_agent_ids: disabled }
146190
} finally {
147-
if (agentsAllGen === gen) agentsAllInflight = null
191+
if (shouldCommitLocalizedGeneration(gen, agentsAllGen)) {
192+
agentsAllInflight = null
193+
agentsAllInflightLocale = ''
194+
}
148195
}
149196
})()
150197
return agentsAllInflight
@@ -290,7 +337,8 @@ export const useChatResourcesStore = defineStore('chatResources', () => {
290337
inflight.clear()
291338
agentKbInflight.clear()
292339
kbAllInflight = null
293-
agentsAllInflight = null
340+
agentsLoadedLocale = ''
341+
bumpAgentsGeneration()
294342
invalidateKnowledgeBaseDetail()
295343
return
296344
}
@@ -305,7 +353,8 @@ export const useChatResourcesStore = defineStore('chatResources', () => {
305353
invalidateKnowledgeBaseDetail()
306354
}
307355
if (keys.includes('agents')) {
308-
agentsAllInflight = null
356+
agentsLoadedLocale = ''
357+
bumpAgentsGeneration()
309358
}
310359
}
311360

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import assert from 'node:assert/strict'
2+
import test from 'node:test'
3+
import {
4+
isLocalizedCacheFresh,
5+
shouldCommitLocalizedGeneration,
6+
shouldForceLocalizedRefetch,
7+
shouldReuseLocalizedInflight,
8+
} from './localizedResourceCache.ts'
9+
10+
const TTL_MS = 60_000
11+
12+
test('localized cache is fresh only when TTL and request locale both match', () => {
13+
const loadedAt = 1_000
14+
assert.equal(isLocalizedCacheFresh(loadedAt, 'zh-CN', 'zh-CN', TTL_MS, 2_000), true)
15+
assert.equal(isLocalizedCacheFresh(loadedAt, 'zh-CN', 'en-US', TTL_MS, 2_000), false)
16+
assert.equal(isLocalizedCacheFresh(loadedAt, 'zh-CN', 'zh-CN', TTL_MS, loadedAt + TTL_MS), false)
17+
assert.equal(isLocalizedCacheFresh(0, 'zh-CN', 'zh-CN', TTL_MS, 2_000), false)
18+
})
19+
20+
test('landing prefetch must not be reused after the UI language changes', () => {
21+
// Request started as zh-CN; user switched to en-US before it settled.
22+
assert.equal(shouldReuseLocalizedInflight(true, 'zh-CN', 'en-US'), false)
23+
assert.equal(shouldForceLocalizedRefetch(true, 'zh-CN', 'en-US'), true)
24+
25+
assert.equal(shouldReuseLocalizedInflight(true, 'en-US', 'en-US'), true)
26+
assert.equal(shouldForceLocalizedRefetch(true, 'en-US', 'en-US'), false)
27+
assert.equal(shouldReuseLocalizedInflight(false, 'zh-CN', 'zh-CN'), false)
28+
assert.equal(shouldReuseLocalizedInflight(true, '', 'en-US'), false)
29+
})
30+
31+
test('stamping the request locale keeps a late zh-CN payload from looking fresh for en-US', () => {
32+
const requestLocale = 'zh-CN'
33+
const loadedAt = Date.now()
34+
// Wrong (old bug): stamp getCurrentLanguage() after await → 'en-US'.
35+
assert.equal(isLocalizedCacheFresh(loadedAt, 'en-US', 'en-US', TTL_MS), true)
36+
// Correct: stamp the locale the HTTP call was started with.
37+
assert.equal(isLocalizedCacheFresh(loadedAt, requestLocale, 'en-US', TTL_MS), false)
38+
})
39+
40+
test('a superseded generation must not write the cache', () => {
41+
assert.equal(shouldCommitLocalizedGeneration(1, 1), true)
42+
assert.equal(shouldCommitLocalizedGeneration(1, 2), false)
43+
})
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
/**
2+
* Helpers for short-TTL caches of API payloads that vary by Accept-Language
3+
* (built-in agent names/descriptions, shared-agent lists, …).
4+
*
5+
* The request must stamp the locale it was *started* with. Stamping
6+
* getCurrentLanguage() after await lets a zh-CN response be marked fresh for
7+
* en-US when the user switches language while the call is in flight.
8+
*/
9+
10+
export function isLocalizedCacheFresh(
11+
loadedAt: number,
12+
loadedLocale: string,
13+
currentLocale: string,
14+
ttlMs: number,
15+
now = Date.now(),
16+
): boolean {
17+
return loadedAt > 0 && now - loadedAt < ttlMs && loadedLocale === currentLocale
18+
}
19+
20+
/** Reuse an in-flight list request only when it was started for this UI language. */
21+
export function shouldReuseLocalizedInflight(
22+
hasInflight: boolean,
23+
inflightLocale: string,
24+
currentLocale: string,
25+
): boolean {
26+
return hasInflight && inflightLocale !== '' && inflightLocale === currentLocale
27+
}
28+
29+
/**
30+
* Force a follow-up fetch when the in-flight request was started for a
31+
* different UI language. versionedRequestCoordinator reuses in-flight on
32+
* non-force fetch, so locale mismatch must opt into force.
33+
*/
34+
export function shouldForceLocalizedRefetch(
35+
hasInflight: boolean,
36+
inflightLocale: string,
37+
currentLocale: string,
38+
): boolean {
39+
return hasInflight && inflightLocale !== '' && inflightLocale !== currentLocale
40+
}
41+
42+
/** Drop a completed payload when a newer generation (or language switch) superseded it. */
43+
export function shouldCommitLocalizedGeneration(
44+
requestGen: number,
45+
currentGen: number,
46+
): boolean {
47+
return requestGen === currentGen
48+
}

frontend/src/stores/organization.ts

Lines changed: 48 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { defineStore } from 'pinia'
2-
import { ref, computed } from 'vue'
2+
import { ref, computed, watch } from 'vue'
33
import type {
44
Organization,
55
OrganizationMember,
@@ -43,7 +43,12 @@ import {
4343
reviewJoinRequest as reviewJoinRequestApi,
4444
requestRoleUpgrade as requestRoleUpgradeApi
4545
} from '@/api/organization'
46+
import { getCurrentLanguage } from '@/utils/request'
4647
import { createVersionedRequestCoordinator } from './versionedRequest'
48+
import {
49+
isLocalizedCacheFresh,
50+
shouldForceLocalizedRefetch,
51+
} from './localizedResourceCache'
4752
import {
4853
applyOrganizationResourceDelta,
4954
upsertById,
@@ -71,6 +76,9 @@ export const useOrganizationStore = defineStore('organization', () => {
7176
const SEARCHABLE_ORGANIZATION_TTL_MS = 5 * 60_000
7277
let sharedKbLoadedAt = 0
7378
let sharedAgentsLoadedAt = 0
79+
/** 共享智能体含按请求语言本地化的内置名称;切换 UI 语言后缓存随之失效 */
80+
let sharedAgentsLoadedLocale = ''
81+
let sharedAgentsInflightLocale = ''
7482
let searchableOrganizationsQuery = ''
7583
const searchableOrganizationCache = new Map<
7684
string,
@@ -491,25 +499,55 @@ export const useOrganizationStore = defineStore('organization', () => {
491499
* 去重 + 短期缓存。
492500
*/
493501
const sharedAgentsRequest = createVersionedRequestCoordinator(
494-
listSharedAgents,
495-
(response) => {
502+
async () => {
503+
const locale = getCurrentLanguage()
504+
sharedAgentsInflightLocale = locale
505+
const response = await listSharedAgents()
506+
return { response, locale }
507+
},
508+
({ response, locale }) => {
496509
if (response.success && response.data) {
497510
sharedAgents.value = response.data.filter(s => s.agent != null)
498511
sharedAgentsLoadedAt = Date.now()
512+
sharedAgentsLoadedLocale = locale
499513
}
500514
}
501515
)
502516

517+
watch(
518+
() => getCurrentLanguage(),
519+
(locale) => {
520+
if (sharedAgentsLoadedLocale && sharedAgentsLoadedLocale !== locale) {
521+
sharedAgentsRequest.invalidate()
522+
sharedAgentsLoadedAt = 0
523+
sharedAgentsLoadedLocale = ''
524+
sharedAgentsInflightLocale = ''
525+
}
526+
},
527+
)
528+
503529
async function fetchSharedAgents(options?: { force?: boolean }) {
530+
const locale = getCurrentLanguage()
504531
const force = options?.force ?? false
505532
if (
506533
!force &&
507-
sharedAgentsLoadedAt > 0 &&
508-
Date.now() - sharedAgentsLoadedAt < SHARED_RESOURCE_TTL_MS
534+
isLocalizedCacheFresh(
535+
sharedAgentsLoadedAt,
536+
sharedAgentsLoadedLocale,
537+
locale,
538+
SHARED_RESOURCE_TTL_MS,
539+
)
509540
) {
510541
return sharedAgents.value
511542
}
512-
await sharedAgentsRequest.fetch(force)
543+
const mustForce =
544+
force ||
545+
shouldForceLocalizedRefetch(
546+
sharedAgentsRequest.hasInFlightRequest(),
547+
sharedAgentsInflightLocale,
548+
locale,
549+
)
550+
await sharedAgentsRequest.fetch(mustForce)
513551
return sharedAgents.value
514552
}
515553

@@ -542,6 +580,8 @@ export const useOrganizationStore = defineStore('organization', () => {
542580
if (options.sharedAgents) {
543581
sharedAgentsRequest.invalidate()
544582
sharedAgentsLoadedAt = 0
583+
sharedAgentsLoadedLocale = ''
584+
sharedAgentsInflightLocale = ''
545585
}
546586
if (options.searchableOrganizations) {
547587
invalidateSearchableOrganizations(options.excludeSearchableOrganizationId)
@@ -796,6 +836,8 @@ export const useOrganizationStore = defineStore('organization', () => {
796836
error.value = null
797837
sharedKbLoadedAt = 0
798838
sharedAgentsLoadedAt = 0
839+
sharedAgentsLoadedLocale = ''
840+
sharedAgentsInflightLocale = ''
799841
organizationsLoadedAt = 0
800842
searchableOrganizationsQuery = ''
801843
searchableOrganizationCache.clear()

frontend/src/utils/request.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ const instance = axios.create({
2121
});
2222

2323
// 获取当前用户语言(用于 Accept-Language header)
24-
function getCurrentLanguage(): string {
24+
export function getCurrentLanguage(): string {
2525
return i18n.global.locale?.value || localStorage.getItem('locale') || 'zh-CN'
2626
}
2727

internal/application/service/agent_share.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,9 @@ func (s *agentShareService) sharedAgentInfo(
104104
SharedAt: share.CreatedAt,
105105
SharedByUserID: share.SharedByUserID,
106106
}
107+
if share.Agent != nil {
108+
types.ApplyBuiltinAgentLocalization(ctx, share.Agent)
109+
}
107110
if share.Organization != nil {
108111
info.OrgName = share.Organization.Name
109112
}
@@ -474,6 +477,7 @@ func (s *agentShareService) GetSharedAgentForTenant(
474477
if err != nil || agent == nil {
475478
return nil, ErrAgentNotFoundForShare
476479
}
480+
types.ApplyBuiltinAgentLocalization(ctx, agent)
477481
_ = callerTenantRole
478482
return agent, nil
479483
}
@@ -491,6 +495,7 @@ func (s *agentShareService) GetSharedAgentForTenant(
491495
}
492496
return nil, err
493497
}
498+
types.ApplyBuiltinAgentLocalization(ctx, agent)
494499
_ = callerTenantRole
495500
return agent, nil
496501
}

0 commit comments

Comments
 (0)