重构: 前端架构审查降技术债·i18n收口(高ROI)

- 新建 src/i18n/i18n-helpers.ts: 类型安全 t + currentLocale(any中转内部1处带注释)
- 12文件改 i18n as any 样板收口: utils/time + stores(knowledge/project/{ideas,projects,tasks,workflow}) + composables(aiShared/useAiConversations/useAiEvents/useAiSend/useAiStream/useToolCard)
- as any 22→10(i18n 13→0对外), 签名统一(key, named?)
- useToolCard文档块同步(防AI被过时注释误导)
This commit is contained in:
2026-06-19 12:25:10 +08:00
parent cfae6027d2
commit 11b579eb87
13 changed files with 48 additions and 52 deletions

View File

@@ -13,13 +13,11 @@
import { aiApi } from '@/api' import { aiApi } from '@/api'
import { state } from '@/stores/ai' import { state } from '@/stores/ai'
import { useAppSettingsStore } from '@/stores/appSettings' import { useAppSettingsStore } from '@/stores/appSettings'
import i18n from '@/i18n' import { t } from '@/i18n/i18n-helpers'
import type { AiToolCallInfo } from '@/api/types' import type { AiToolCallInfo } from '@/api/types'
const appSettings = useAppSettingsStore() const appSettings = useAppSettingsStore()
const t = ((i18n as any).global.t as (k: string, named?: Record<string, unknown>) => string).bind((i18n as any).global)
/** /**
* 解析 AI 回复语言(useAiSend.sendMessage / useAiContext.compressContext 共用)。 * 解析 AI 回复语言(useAiSend.sendMessage / useAiContext.compressContext 共用)。
* *

View File

@@ -10,14 +10,11 @@ import { state } from '@/stores/ai'
import { notifyConversationChanged } from './useAiEvents' import { notifyConversationChanged } from './useAiEvents'
import { persistUiState } from './useAiPanel' import { persistUiState } from './useAiPanel'
import { nextMsgId } from './aiShared' import { nextMsgId } from './aiShared'
import i18n from '@/i18n' import { t } from '@/i18n/i18n-helpers'
import type { AiMessage, AiToolCallInfo } from '@/api/types' import type { AiMessage, AiToolCallInfo } from '@/api/types'
const appSettings = useAppSettingsStore() const appSettings = useAppSettingsStore()
// composable 无组件上下文,借全局 i18n 实例翻译错误文案(同 useAiSend/aiShared 模式)
const t = ((i18n as any).global.t as (k: string, named?: Record<string, unknown>) => string).bind((i18n as any).global)
/** 拉取会话列表;首次加载时若 appSettings 仍有有效 active-conv 且当前无活跃对话则恢复 */ /** 拉取会话列表;首次加载时若 appSettings 仍有有效 active-conv 且当前无活跃对话则恢复 */
async function loadConversations() { async function loadConversations() {
try { try {

View File

@@ -16,17 +16,12 @@ import { ref } from 'vue'
import { aiApi } from '@/api' import { aiApi } from '@/api'
import { useAppSettingsStore } from '@/stores/appSettings' import { useAppSettingsStore } from '@/stores/appSettings'
import { state } from '@/stores/ai' import { state } from '@/stores/ai'
import i18n from '@/i18n' import { t } from '@/i18n/i18n-helpers'
import { nextMsgId, findToolCall, startApprovalTimer, clearApprovalTimer, clearAllApprovalTimers } from './aiShared' import { nextMsgId, findToolCall, startApprovalTimer, clearApprovalTimer, clearAllApprovalTimers } from './aiShared'
import { resetStreamWatchdog, clearStreamWatchdog } from './useAiStream' import { resetStreamWatchdog, clearStreamWatchdog } from './useAiStream'
import { loadConversations } from './useAiConversations' import { loadConversations } from './useAiConversations'
import type { AiChatEvent, AiMessage, AiToolCallInfo } from '@/api/types' import type { AiChatEvent, AiMessage, AiToolCallInfo } from '@/api/types'
// composable 内非组件上下文(无 setup),用 vue-i18n 全局实例的 t 而非 useI18n()。
// 通过 any 中转规避 vue-i18n 深度 message schema 泛型导致的 TS2589(类型实例化过深)。
// CR-30-2: 签名放宽支持插值参数(复用现有 t('ai.xxx', {...}) 模式,如 aiShared.ts:75)。
const t = ((i18n as any).global.t as (key: string, params?: Record<string, unknown>) => string).bind((i18n as any).global)
let _unlistenAiEvent: (() => void) | null = null let _unlistenAiEvent: (() => void) | null = null
let _unlistenConvChanged: (() => void) | null = null let _unlistenConvChanged: (() => void) | null = null
// AE-2025-06: onStreamTimeout 广播的审批计时器清理事件 unlistener // AE-2025-06: onStreamTimeout 广播的审批计时器清理事件 unlistener

View File

@@ -21,13 +21,11 @@ import { ref } from 'vue'
import { aiApi } from '@/api' import { aiApi } from '@/api'
import { state } from '@/stores/ai' import { state } from '@/stores/ai'
import type { ContentPart, AiMessage } from '@/api/types' import type { ContentPart, AiMessage } from '@/api/types'
import i18n from '@/i18n' import { t } from '@/i18n/i18n-helpers'
import { resetStreamWatchdog, clearStreamWatchdog } from './useAiStream' import { resetStreamWatchdog, clearStreamWatchdog } from './useAiStream'
import { startListener } from './useAiEvents' import { startListener } from './useAiEvents'
import { nextMsgId, findToolCall, startApprovalTimer, clearApprovalTimer, clearAllApprovalTimers, resolveAiLang } from './aiShared' import { nextMsgId, findToolCall, startApprovalTimer, clearApprovalTimer, clearAllApprovalTimers, resolveAiLang } from './aiShared'
const t = ((i18n as any).global.t as (k: string, named?: Record<string, unknown>) => string).bind((i18n as any).global)
/// 待发送队列上限(超过抛错提示用户) /// 待发送队列上限(超过抛错提示用户)
const QUEUE_LIMIT = 10 const QUEUE_LIMIT = 10

View File

@@ -12,14 +12,10 @@
//! 循环依赖;下沉到 aiShared 后本模块不再 import useAiEvents,环消除) //! 循环依赖;下沉到 aiShared 后本模块不再 import useAiEvents,环消除)
import { state } from '@/stores/ai' import { state } from '@/stores/ai'
import i18n from '@/i18n' import { t } from '@/i18n/i18n-helpers'
import { nextMsgId } from './aiShared' import { nextMsgId } from './aiShared'
import { emit } from '@tauri-apps/api/event' import { emit } from '@tauri-apps/api/event'
// composable 内非组件上下文(无 setup),用 vue-i18n 全局实例的 t 而非 useI18n()。
// 通过 any 中转规避 vue-i18n 深度 message schema 泛型导致的 TS2589(类型实例化过深)。
const t = ((i18n as any).global.t as (key: string) => string).bind((i18n as any).global)
/// ≥ 后端 STREAM_IDLE_TIMEOUT(120s, ai.rs:848)+余量; /// ≥ 后端 STREAM_IDLE_TIMEOUT(120s, ai.rs:848)+余量;
/// 前端若短于后端,慢首 token 会被前端先误杀 /// 前端若短于后端,慢首 token 会被前端先误杀
export const STREAM_TIMEOUT_MS = 130000 export const STREAM_TIMEOUT_MS = 130000

View File

@@ -11,22 +11,18 @@
//! 及原 <script setup> 中仅依赖 i18n.t 的 toolResultSummary)。零行为变更,纯搬迁。 //! 及原 <script setup> 中仅依赖 i18n.t 的 toolResultSummary)。零行为变更,纯搬迁。
//! //!
//! 设计: //! 设计:
//! - composable 内非组件上下文(无 setup),用 vue-i18n 全局实例的 t(对齐 aiShared.ts/ //! - 非组件上下文(无 setup),用 @/i18n/i18n-helpers 的类型安全 t(规避 vue-i18n TS2589)。
//! useAiEvents.ts/useAiStream.ts 等同款约定),而非 useI18n()。
//! - 全部为纯函数(入参出参,无响应式无副作用),供 ToolCard.vue 模板/setup 直接 import 调用。 //! - 全部为纯函数(入参出参,无响应式无副作用),供 ToolCard.vue 模板/setup 直接 import 调用。
//! - store 依赖的工具显示名(toolDisplayName/displayArgValue/projectNameById/taskNameById) //! - store 依赖的工具显示名(toolDisplayName/displayArgValue/projectNameById/taskNameById)
//! 仍留 ToolCard.vue setup(耦合 projectStore,非纯函数)。 //! 仍留 ToolCard.vue setup(耦合 projectStore,非纯函数)。
//! //!
//! 耦合: //! 耦合:
//! - i18n.global.t(翻译) //! - i18n 翻译(经 @/i18n/i18n-helpers)
//! - AiToolCallInfo(@/api/types) //! - AiToolCallInfo(@/api/types)
import i18n from '@/i18n' import { t } from '@/i18n/i18n-helpers'
import type { AiToolCallInfo } from '@/api/types' import type { AiToolCallInfo } from '@/api/types'
// composable 内非组件上下文(无 setup),用 vue-i18n 全局实例的 t 而非 useI18n()。
const t = ((i18n as any).global.t as (k: string, p?: Record<string, unknown>) => string).bind((i18n as any).global)
/** 工具结果 JSON 已知字段(全可选;list_* 运行时返回数组,靠 Array.isArray 区分) */ /** 工具结果 JSON 已知字段(全可选;list_* 运行时返回数组,靠 Array.isArray 区分) */
export interface ToolResult { export interface ToolResult {
path?: string path?: string

32
src/i18n/i18n-helpers.ts Normal file
View File

@@ -0,0 +1,32 @@
//! 类型安全的 i18n 访问助手 — 供 store / composable / util 在非 setup 上下文使用
//!
//! 背景:vue-i18n 在 composition 模式下,`i18n.global.t` 的泛型会递归展开全部 message
//! schema,触发 TS2589(类型实例化过深)。历史上每个 store/composable 都各自写一段
//! `(i18n as any).global.t as (...)` 样板绕过,共 13 处,签名还微差(`(k:string)` /
//! `(k:string, named?)` / `(key, params?)`)。本模块收口为一处:仅在内部用一次 any 中转,
//! 对外暴露统一签名的 `t` 与 `currentLocale`。
//!
//! 用法:
//! import { t, currentLocale } from '@/i18n/i18n-helpers'
//! t('common.justNow')
//! t('common.minutesAgo', { n: 5 })
//! currentLocale() === 'en'
import i18n from '@/i18n'
/** 统一的翻译函数签名(键 + 可选命名参数) */
type TranslateFn = (key: string, named?: Record<string, unknown>) => string
// any 中转仅在内部出现一次,对外不可见。规避 TS2589。
const _global = (i18n as unknown as { global: { t: TranslateFn; locale: { value: string } } }).global
/**
* 类型安全的翻译函数(绑定到全局 i18n 实例)。
* 非组件上下文(store/composable/util 模块顶层)使用;组件内仍优先用 useI18n()。
*/
export const t: TranslateFn = _global.t.bind(_global)
/** 当前 locale('zh-CN' | 'en' 等),供时间格式化等按语言分支的逻辑读取 */
export function currentLocale(): string {
return _global.locale.value
}

View File

@@ -1,6 +1,6 @@
import { reactive, computed } from 'vue' import { reactive, computed } from 'vue'
import { knowledgeApi } from '@/api' import { knowledgeApi } from '@/api'
import i18n from '@/i18n' import { t } from '@/i18n/i18n-helpers'
import type { import type {
KnowledgeRecord, KnowledgeRecord,
KnowledgeDetailPayload, KnowledgeDetailPayload,
@@ -10,9 +10,6 @@ import type {
KnowledgeConfig, KnowledgeConfig,
} from '@/api/types' } from '@/api/types'
// composable 外全局 i18n 实例(非 setup 上下文)
const t = ((i18n as any).global.t as (k: string) => string).bind((i18n as any).global)
// ── 全局响应式状态(单例) ── // ── 全局响应式状态(单例) ──
const state = reactive({ const state = reactive({

View File

@@ -1,10 +1,7 @@
import { ideaApi } from '@/api' import { ideaApi } from '@/api'
import i18n from '@/i18n' import { t } from '@/i18n/i18n-helpers'
import { state } from './state' import { state } from './state'
// composable 外全局 i18n 实例(非 setup 上下文)
const t = ((i18n as any).global.t as (k: string) => string).bind((i18n as any).global)
/** 灵感 CRUD 子 store(共享全局 state) */ /** 灵感 CRUD 子 store(共享全局 state) */
export function createIdeasStore() { export function createIdeasStore() {
async function loadIdeas() { async function loadIdeas() {

View File

@@ -1,11 +1,8 @@
import { projectApi } from '@/api' import { projectApi } from '@/api'
import type { ImportProjectInput } from '@/api/project' import type { ImportProjectInput } from '@/api/project'
import i18n from '@/i18n' import { t } from '@/i18n/i18n-helpers'
import { state, clearError } from './state' import { state, clearError } from './state'
// composable 外全局 i18n 实例(非 setup 上下文)
const t = ((i18n as any).global.t as (k: string) => string).bind((i18n as any).global)
/** 项目 CRUD 子 store(共享全局 state) */ /** 项目 CRUD 子 store(共享全局 state) */
export function createProjectsStore() { export function createProjectsStore() {
async function loadProjects() { async function loadProjects() {

View File

@@ -1,10 +1,7 @@
import { taskApi } from '@/api' import { taskApi } from '@/api'
import i18n from '@/i18n' import { t } from '@/i18n/i18n-helpers'
import { state } from './state' import { state } from './state'
// composable 外全局 i18n 实例(非 setup 上下文)
const t = ((i18n as any).global.t as (k: string) => string).bind((i18n as any).global)
/** 任务 CRUD 子 store(共享全局 state) */ /** 任务 CRUD 子 store(共享全局 state) */
export function createTasksStore() { export function createTasksStore() {
async function loadTasks(projectId?: string) { async function loadTasks(projectId?: string) {

View File

@@ -1,10 +1,7 @@
import { workflowApi } from '@/api' import { workflowApi } from '@/api'
import i18n from '@/i18n' import { t } from '@/i18n/i18n-helpers'
import { state, _eventUnlisten, setEventUnlisten } from './state' import { state, _eventUnlisten, setEventUnlisten } from './state'
// composable 外全局 i18n 实例(非 setup 上下文)
const t = ((i18n as any).global.t as (k: string) => string).bind((i18n as any).global)
// B-03b-R10(⑦ 互斥): 审批响应/取消 IPC 进行中标记(模块级单例,createWorkflowStore 每会话只跑一次)。 // B-03b-R10(⑦ 互斥): 审批响应/取消 IPC 进行中标记(模块级单例,createWorkflowStore 每会话只跑一次)。
// 防双击/连点在同一轮 IPC 未返回前发起第二次(后端无幂等键,第二次会重复消费 Request 触发 // 防双击/连点在同一轮 IPC 未返回前发起第二次(后端无幂等键,第二次会重复消费 Request 触发
// "node already completed" 类报错)。store 层守卫优于按钮 disabled: 共享单例 state,跨调用方统一。 // "node already completed" 类报错)。store 层守卫优于按钮 disabled: 共享单例 state,跨调用方统一。

View File

@@ -11,11 +11,10 @@
* 正确做法:先转 number 再 new Date。本模块统一处理毫秒字符串 / 秒级 / ISO / number / 空。 * 正确做法:先转 number 再 new Date。本模块统一处理毫秒字符串 / 秒级 / ISO / number / 空。
*/ */
import i18n from '@/i18n' import { t, currentLocale } from '@/i18n/i18n-helpers'
// CR-260615-08(P1-10):相对时间走 i18n(原硬编码中文,en locale 时间恒中文)。 // CR-260615-08(P1-10):相对时间走 i18n(原硬编码中文,en locale 时间恒中文)。
// utils 纯函数无 setup,用 vue-i18n 全局实例(参考 useAiEvents.ts:16-25),any 中转规避 TS2589。 // 类型安全访问经 i18n-helpers 收口(规避 vue-i18n TS2589)
const t = ((i18n as any).global.t as (k: string, named?: Record<string, unknown>) => string).bind((i18n as any).global)
/** 解析任意时间值为毫秒时间戳;无效或空返回 null */ /** 解析任意时间值为毫秒时间戳;无效或空返回 null */
export function parseTs(v: string | number | null | undefined): number | null { export function parseTs(v: string | number | null | undefined): number | null {
@@ -41,7 +40,7 @@ export function formatDate(v: string | number | null | undefined): string {
const ms = parseTs(v) const ms = parseTs(v)
if (ms == null) return '—' if (ms == null) return '—'
const d = new Date(ms) const d = new Date(ms)
const locale = (i18n as any).global.locale.value === 'en' ? 'en-US' : 'zh-CN' const locale = currentLocale() === 'en' ? 'en-US' : 'zh-CN'
return d.toLocaleDateString(locale) + ' ' + d.toLocaleTimeString(locale, { hour: '2-digit', minute: '2-digit' }) return d.toLocaleDateString(locale) + ' ' + d.toLocaleTimeString(locale, { hour: '2-digit', minute: '2-digit' })
} }