# 语音回拨开关功能实现总结

## 一、需求来源

在已有回拨功能（参见《语音助手回拨功能实现方案》）的基础上，新增"语音回拨"开关，允许用户在 kylin-aiassistant 设置中控制是否启用语音回拨：

| 需求编号 | 描述 |
|---------|------|
| 1 | 在 kylin-aiassistant 设置-常规页新增"语音回拨"设置项，位于"关闭窗口"与"文件保存"之间 |
| 2 | UI 完全参照"辅助功能"（长按空格语音输入）的实现：标题 + 正文 + 副文 + SwitchButton |
| 3 | 标题"语音回拨"，正文"语音回拨"，副文"语音聊天任务完成后，应用主动来电播报结果内容" |
| 4 | 后端 ConfigManager 存储配置，键名 `voice-callback-enabled`，默认值 `true`（向后兼容） |
| 5 | ConfigManager 需做兼容性处理：升级用户首次启动时自动补写默认值 |
| 6 | voice-assistant 在决定是否回拨时读取 kylin-aiassistant 的配置文件 |
| 7 | 语音回拨禁用时，AI 任务完成后仅发送系统通知（"任务完成"），不弹窗不响铃不回拨 |

## 二、改动前现状分析

### 已实现

- kylin-aiassistant 设置-常规页已有"辅助功能"开关（长按空格语音输入），UI 模式为标题 Label + Rectangle(ColumnLayout + Switch + Connections)
- ConfigManager 单例，使用 QSettings INI 格式，路径 `~/.config/kylin-aiassistant/settings.ini`
- 兼容性模式：构造函数中 `!m_settings->contains(key)` 检查 → 设置默认值 → `sync()`（如 `is-sidebar-close`）
- Globals (kaglobal) 提供 Q_INVOKABLE getter/setter + 信号桥接 QML（如 SpaceVoiceInput）
- voice-assistant 已有完整回拨机制：`m_callbackPending` → `handleAiReplyFinished()` → `beginCallbackAttempts()`
- voice-assistant 的 GatewayChatClient 已通过 `QSettings` 读取 `~/.config/kylin-aiassistant/settings.ini`

### 未实现/有问题

1. **回拨无开关**：voice-assistant 回拨始终触发，用户无法关闭
2. **无配置项**：ConfigManager 中不存在 `voice-callback-enabled` 键
3. **无 UI 入口**：设置页无对应开关
4. **跨进程配置读取**：voice-assistant 的 MainWindow 未读取 kylin-aiassistant 的配置

## 三、设计方案

### 3.1 配置存储

- **键名**：`voice-callback-enabled`
- **类型**：bool
- **默认值**：`true`（保持向后兼容，升级用户行为不变）
- **存储位置**：`~/.config/kylin-aiassistant/settings.ini`

### 3.2 兼容性处理

参照 `is-sidebar-close` 模式，在 ConfigManager 构造函数中添加：

```cpp
if (!m_settings->contains("voice-callback-enabled")) {
    m_settings->setValue("voice-callback-enabled", true);
    m_settings->sync();
    KyInfo() << "配置文件中无voice-callback-enabled，已设置默认值true";
}
```

同时在 `createDefaultConfig()` 中添加默认值，确保新安装用户创建配置文件时即包含该键。

### 3.3 Globals QML 桥接

参照 SpaceVoiceInput 模式，在 Globals 类中新增：

| 声明 | 类型 | 用途 |
|------|------|------|
| `Q_INVOKABLE bool getVoiceCallbackEnabled()` | 方法 | QML 读取配置 |
| `Q_INVOKABLE void setVoiceCallbackEnabled(bool enabled)` | 方法 | QML 写入配置 |
| `void sigVoiceCallbackEnabledChanged(bool enabled)` | 信号 | 通知 QML 状态变化 |
| `bool mVoiceCallbackEnabled = true` | 成员 | 缓存状态 |

**Getter 实现**：每次从 ConfigManager 读取（与 SpaceVoiceInput 一致）

**Setter 实现**：调用 `ConfigManager::updateConfig()` → 更新成员变量 → emit 信号

### 3.4 QML UI

在"关闭窗口"与"文件保存"之间插入，结构完全复制"辅助功能"区域：

```
┌─────────────────────────────────────────────────┐
│ Voice Callback (标题)                            │
├─────────────────────────────────────────────────┤
│ ┌─────────────────────────────────────────────┐ │
│ │ Voice Callback          [SwitchButton]      │ │
│ │ After voice chat task...                    │ │
│ └─────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────┘
```

- 标题 Label：`qsTr("Voice Callback")`，`#333333`
- 正文 Label：`qsTr("Voice Callback")`，`#333333`
- 副文 Label：`qsTr("After voice chat task is completed, the app actively calls back to report the result")`，`#999999`，fontsize - 2
- Switch：`checked: true`，`onCheckedChanged` 调用 `globals.setVoiceCallbackEnabled(checked)`
- Connections：监听 `onSigVoiceCallbackEnabledChanged` 同步状态
- Rectangle：`color: #F6F6F6`，`radius: 6`，`preferredHeight: 72`
- Component.onCompleted：调用 `globals.getVoiceCallbackEnabled()` 初始化

### 3.5 voice-assistant 回拨门控

在 `handleAiReplyFinished()` 中，原回拨触发点：

```cpp
if (m_callbackPending && !isVisible()) {
    m_callbackPending = false;
    setState(Idle);
    beginCallbackAttempts();  // ← 原逻辑：始终回拨
    return;
}
```

改为：

```cpp
if (m_callbackPending && !isVisible()) {
    m_callbackPending = false;
    setState(Idle);

    QSettings callbackSettings(
        QDir::homePath() + "/.config/kylin-aiassistant/settings.ini",
        QSettings::IniFormat);
    bool callbackEnabled = callbackSettings.value("voice-callback-enabled", true).toBool();

    if (!callbackEnabled) {
        if (m_notificationManager) {
            m_notificationManager->sendNotification(
                tr("Voice assistant"),
                tr("The task is complete. Please enter the conversation to view details."));
        }
        return;
    }

    beginCallbackAttempts();
    return;
}
```

**设计要点**：

- 使用 QSettings 直接读取，不缓存（每次 `handleAiReplyFinished` 调用时读取，频率低无需缓存）
- 默认值 `true`：配置文件缺失或键不存在时，保持原有行为
- 禁用时发送通知但不回拨：用户仍可通过通知得知任务完成
- 不修改 `beginCallbackAttempts()` 本身：门控在调用之前

## 四、文件改动详情

### 4.1 kylin-aiassistant/configmanager.cpp

| 改动点 | 改动内容 |
|--------|---------|
| 构造函数（~line 66） | 新增 `!m_settings->contains("voice-callback-enabled")` 兼容性检查 |
| `createDefaultConfig()`（~line 147） | 新增 `m_settings->setValue("voice-callback-enabled", true)` |

### 4.2 kylin-aiassistant/global/kaglobal.h

| 改动点 | 改动内容 |
|--------|---------|
| Q_INVOKABLE 声明区（~line 374） | 新增 `getVoiceCallbackEnabled()` / `setVoiceCallbackEnabled()` |
| signals 区（~line 392） | 新增 `sigVoiceCallbackEnabledChanged(bool enabled)` |
| 成员变量区（~line 429） | 新增 `bool mVoiceCallbackEnabled = true` |

### 4.3 kylin-aiassistant/global/kaglobal.cpp

| 改动点 | 改动内容 |
|--------|---------|
| 构造函数（~line 138） | 新增 `mVoiceCallbackEnabled` 初始化 |
| 新增函数（~line 1308） | `getVoiceCallbackEnabled()` 实现 |
| 新增函数（~line 1314） | `setVoiceCallbackEnabled()` 实现 |

### 4.4 kylin-aiassistant/res/qml/SettingNormalPage.qml

| 改动点 | 改动内容 |
|--------|---------|
| 关闭窗口组之后（~line 680） | 插入"语音回拨"标题 Label + Rectangle 设置组 |
| Component.onCompleted（~line 100） | 新增 `voiceCallbackSwitch` 初始化 |

### 4.5 voice-assistant/mainwindow.cpp

| 改动点 | 改动内容 |
|--------|---------|
| `handleAiReplyFinished()`（~line 863） | 在 `beginCallbackAttempts()` 前插入配置读取和门控逻辑 |

## 五、关键流程图

### 5.1 语音回拨开启（默认行为）

```
[Processing/Answering] --挂断--> m_callbackPending=true, hide()
                                       |
                              AI 后台继续处理
                                       |
                          handleAiReplyFinished()
                                       |
                              读取 voice-callback-enabled=true
                                       |
                              beginCallbackAttempts()
                                       |
                              [回拨界面/振铃] → 参见《语音助手回拨功能实现方案》
```

### 5.2 语音回拨禁用

```
[Processing/Answering] --挂断--> m_callbackPending=true, hide()
                                       |
                              AI 后台继续处理
                                       |
                          handleAiReplyFinished()
                                       |
                              读取 voice-callback-enabled=false
                                       |
                              sendNotification("任务完成，请进入会话查看")
                              return（不回拨）
```

### 5.3 配置缺失（兼容性）

```
settings.ini 中无 voice-callback-enabled 键
        |
   QSettings.value("voice-callback-enabled", true) → 返回 true
        |
   保持原有回拨行为（向后兼容）
```

### 5.4 设置页开关切换

```
用户点击 SwitchButton
        |
   globals.setVoiceCallbackEnabled(checked)
        |
   ConfigManager::updateConfig("voice-callback-enabled", checked)
        |
   m_settings->sync() → 写入 settings.ini
        |
   emit sigVoiceCallbackEnabledChanged(checked)
        |
   QML Connections 更新 Switch 状态
```

## 六、跨进程配置联动

```
┌─────────────────────────┐         ┌─────────────────────────┐
│   kylin-aiassistant     │         │    voice-assistant      │
│                         │         │                         │
│  SettingNormalPage.qml  │         │  mainwindow.cpp         │
│       │                 │         │       │                 │
│  globals.set*()         │         │  QSettings.value()      │
│       │                 │         │       │                 │
│  ConfigManager          │         │  读取                   │
│  updateConfig()         │         │       │                 │
│       │                 │         │  门控 beginCallback*()  │
│  settings.ini ──────────┼────────►│                         │
│  (QSettings INI)        │  读取   │                         │
└─────────────────────────┘         └─────────────────────────┘
```

- kylin-aiassistant **写入** `voice-callback-enabled` 到 `settings.ini`
- voice-assistant **读取** `voice-callback-enabled` 从 `settings.ini`
- 两个进程通过同一 INI 文件共享配置，无需 D-Bus 或其他 IPC
- voice-assistant 每次回拨决策时读取（不缓存），保证配置实时性

## 七、风险与缓解

| 风险 | 缓解措施 |
|------|----------|
| 升级用户 settings.ini 缺少 `voice-callback-enabled` 键 | ConfigManager 构造函数中 `!contains()` 兼容性检查，自动补写默认值 `true` |
| 新安装用户 settings.ini 缺少该键 | `createDefaultConfig()` 中包含该键，新安装时即写入 |
| 两个进程同时写 settings.ini | 仅 kylin-aiassistant 写入此键，voice-assistant 只读，无写冲突 |
| QSettings INI 格式布尔值处理 | 统一使用 `.toBool()`，QSettings 正确处理 `"true"`/`"false"` 字符串 |
| m_callbackPending 在禁用时未清理 | 门控逻辑在 `m_callbackPending = false` 之后，`setState(Idle)` 之后执行，状态已清理 |
| 禁用时用户不知道任务完成 | 发送系统通知"任务完成，请进入会话查看详细结果" |
| 配置读取时机 | 每次回拨决策时读取，频率低（每次 AI 回复一次），无需文件监视器 |

## 八、与已有回拨功能的关系

本功能是在《语音助手回拨功能实现方案》基础上的**配置层增强**，不改变回拨的核心逻辑：

- `beginCallbackAttempts()` / `startCallbackAttempt()` / `handleCallbackRingTimeout()` / `scheduleCallbackRetry()` / `finishCallbackFailure()` / `cancelCallbackFlow()` — **均未修改**
- `setCallbackMode()` / `onAnswerClicked()` — **均未修改**
- 唯一修改点：`handleAiReplyFinished()` 中 `beginCallbackAttempts()` 调用前新增配置门控
