Files
eryao/.trellis/tasks/04-28-refactor-unify-language/IMPLEMENTATION_PLAN.md
T

6.3 KiB

IMPLEMENTATION_PLAN:统一语言设置

前置条件

条件 状态
后端 Schema 定义清晰
前端 Model 定义清晰
AI 运行时使用 ai_language
协议文档存在

实现步骤

Step 1: 更新协议文档

文件: docs/protocols/profile/profile-protocol.md

  • interface_languageai_language 合并为 language
  • 更新示例 JSON

Step 2: 后端 Schema

文件: backend/src/schemas/shared/user.py

class PreferenceSettings(BaseModel):
    language: str = "zh-CN"
    timezone: str = "Asia/Shanghai"
    country: str = "US"

    @field_validator("language")
    @classmethod
    def validate_language(cls, value: str) -> str:
        if not _BCP47_PATTERN.fullmatch(value):
            raise ValueError("language must be a valid BCP-47 tag")
        return value

Step 3: 后端 AI 运行时

文件: backend/src/core/agentscope/runtime/runner.py

# 第 268-276 行
language = "zh-CN"
if user_context.settings is not None:
    prefs = getattr(user_context.settings, "preferences", None)
    if prefs is not None:
        language = getattr(prefs, "language", "zh-CN") or "zh-CN"

system_prompt = build_system_prompt(
    agent_type=stage_config.agent_type,
    language=language,
    llm_config=stage_config.llm_config,
    tools=None,
    now_utc=datetime.now(timezone.utc),
)

Step 4: 后端 Prompt 构建

文件: backend/src/core/agentscope/prompts/system_prompt.py

def _build_output_rules(*, language: str) -> str:
    lang_label = _get_language_label(language)
    ...

def build_system_prompt(
    *,
    agent_type: AgentType,
    language: str,
    llm_config: LlmConfig,
    tools: list[ToolSchema] | None,
    now_utc: datetime,
) -> str:
    ...
    _build_output_rules(language=language),

文件: backend/src/core/agentscope/prompts/worker_rules.py

def get_worker_role_playing(language: str) -> str:
    _ = language
    ...

def get_worker_output_rules(language: str) -> str:
    if language.startswith("en"):
        ...

文件: backend/src/core/agentscope/prompts/agent_prompt.py

def build_agent_prompt(
    *,
    language: str = "zh-CN",
) -> str:
    role_playing = get_worker_role_playing(language)
    output_rules = get_worker_output_rules(language)
    ...

Step 5: 后端测试

文件: backend/tests/unit/test_parse_profile_settings.py

  • 字段名 ai_languagelanguage
  • 字段名 interface_languagelanguage

文件: backend/tests/unit/test_agentscope_prompts.py

  • 参数名 ai_languagelanguage

Step 6: 数据库数据更新

直接用 Supabase MCP 执行 SQL(无需迁移脚本):

UPDATE profiles
SET settings = jsonb_set(
    settings - 'interface_language' - 'ai_language',
    '{preferences,language}',
    COALESCE(
        settings->'preferences'->>'interface_language',
        settings->'preferences'->>'ai_language',
        '"zh-CN"'
    )::jsonb
)
WHERE settings->'preferences' ?| array['interface_language', 'ai_language'];

Step 7: 前端 Model

文件: apps/lib/features/settings/data/models/profile_settings.dart

class PreferenceSettings {
  const PreferenceSettings({
    this.language = 'zh-CN',
    this.timezone = 'Asia/Shanghai',
    this.country = 'US',
  });

  final String language;
  final String timezone;
  final String country;

  PreferenceSettings copyWith({
    String? language,
    String? timezone,
    String? country,
  }) {
    return PreferenceSettings(
      language: language ?? this.language,
      timezone: timezone ?? this.timezone,
      country: country ?? this.country,
    );
  }
}

// 更新 defaultsForLocale
factory ProfileSettingsV1.defaultsForLocale(Locale locale) {
  final tag = languageTagFromLocale(locale);
  return ProfileSettingsV1(
    preferences: PreferenceSettings(language: tag),
  );
}

Step 8: 前端 API

文件: apps/lib/features/settings/data/apis/profile_api.dart

// 序列化 (第 45 行)
'language': settings.preferences.language,

// 反序列化 (第 114 行)
language: (preferencesRaw['language'] as String?) ?? 'zh-CN',

Step 9: 前端设置界面

文件: apps/lib/features/settings/presentation/screens/general_settings_screen.dart

移除第 75-95 行的 AI 语言 SettingsMenuTile,修改剩余的语言选项:

SettingsMenuTile(
  icon: Icons.language_rounded,
  title: l10n.settingsLanguage,
  subtitle: displayLanguageLabel(
    l10n,
    _settings.preferences.language,
  ),
  tint: colors.primary,
  background: colors.surfaceContainerHighest,
  showDivider: false,
  onTap: () => _selectLanguage(
    _settings.preferences.language,
    (lang) => setState(() {
      _settings = _settings.copyWith(
        preferences: _settings.preferences.copyWith(
          language: lang,
        ),
      );
    }),
  ),
),

Step 10: 前端 i18n

文件: apps/lib/l10n/app_zh.arb

- "settingsInterfaceLanguage": "界面语言",
- "settingsAiLanguage": "AI回复语言",
- "settingsAiLanguageHint": "该字段将对齐..."
+ "settingsLanguage": "语言",

文件: apps/lib/l10n/app_en.arb

- "settingsInterfaceLanguage": "Interface Language",
- "settingsAiLanguage": "AI Response Language",
- "settingsAiLanguageHint": "This field will align..."
+ "settingsLanguage": "Language",

文件: apps/lib/l10n/app_zh_hant.arb

- "settingsInterfaceLanguage": "介面語言",
- "settingsAiLanguage": "AI 回覆語言",
+ "settingsLanguage": "語言",

Step 11: 重新生成 l10n

cd apps && flutter gen-l10n

Step 12: 更新其他协议文档

文件: docs/protocols/divination/divination-run-protocol.md

第 240 行:

- - Language rule: `conclusion`, `focus_points`, `advice`, `keywords`, `answer` should follow user `ai_language` preference unless user explicitly requests otherwise.
+ - Language rule: `conclusion`, `focus_points`, `advice`, `keywords`, `answer` should follow user `language` preference unless user explicitly requests otherwise.

验证

# 后端
cd backend
uv run pytest tests/unit/test_parse_profile_settings.py tests/unit/test_agentscope_prompts.py -v
./infra/scripts/dev-migrate.sh migrate

# 前端
cd apps
flutter gen-l10n
flutter analyze