新增: F-01模型能力/F-09B多会话并发设计文档+看板同步销账排期
This commit is contained in:
621
docs/02-架构设计/F-01-模型能力系统与智能路由设计-2026-06-16.md
Normal file
621
docs/02-架构设计/F-01-模型能力系统与智能路由设计-2026-06-16.md
Normal file
@@ -0,0 +1,621 @@
|
|||||||
|
# F-01 模型能力系统 Phase 1 — 模型配置与智能路由设计
|
||||||
|
|
||||||
|
> 状态:📐 设计定稿(2026-06-16,含多角度佐证 + 厂商适配 + 用户流程)
|
||||||
|
> 类型:架构设计文档(不碰任何 code)
|
||||||
|
> 关联:F-260614-05 多模态 Phase 2 / F-260614-04 多 Provider 负载均衡池
|
||||||
|
> 前置:F-07 df-ai-core trait 下沉 ✅ 已完成(batch61·2069f79)
|
||||||
|
> 决策记录:见 [功能决策记录-2026-06-14.md](./功能决策记录-2026-06-14.md) §「模型能力系统」行
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 0. 摘要
|
||||||
|
|
||||||
|
将当前「一个 Provider 一个 default_model 跑全场」升级为「多模型池 + 能力感知 + 智能路由」。
|
||||||
|
用户只需填 Provider 基本信息 → 自动拉取模型列表 → 自动探测每个模型的模态/能力/价格/智力 → 路由器按场景自动选最优模型。
|
||||||
|
|
||||||
|
**核心价值**:含图消息自动选 Vision 模型、标题生成自动选便宜模型、复杂推理自动选强模型、限流自动 fallback。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. 现状盘点
|
||||||
|
|
||||||
|
### 1.1 数据模型(AiProviderRecord)
|
||||||
|
|
||||||
|
`crates/df-storage/src/models.rs:127-140`
|
||||||
|
|
||||||
|
```rust
|
||||||
|
pub struct AiProviderRecord {
|
||||||
|
pub provider_type: String, // "openai_compat" | "anthropic_compat"
|
||||||
|
pub base_url: String,
|
||||||
|
pub default_model: String, // 唯一实际使用的模型名
|
||||||
|
pub models: Option<String>, // JSON array of model names(展示用,未消费)
|
||||||
|
pub config: Option<String>, // JSON extra config(几乎没用)
|
||||||
|
pub is_default: bool,
|
||||||
|
...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**问题**:
|
||||||
|
1. `models` 存了模型列表但**没有能力标记**,不知道哪个支持 vision / tool use
|
||||||
|
2. **没有启用/禁用开关**,配了的模型无法控制哪些参与路由
|
||||||
|
3. **没有权重/优先级**,无法表达"优先用 A,A 不可用时 fallback 到 B"
|
||||||
|
4. `default_model` 是唯一实际使用的模型名,`models` 列表形同虚设
|
||||||
|
|
||||||
|
### 1.2 模型选择机制(当前)
|
||||||
|
|
||||||
|
```
|
||||||
|
用户在 Settings 配置 Provider(含 base_url + default_model)
|
||||||
|
↓
|
||||||
|
AiSession.active_provider_id ← 全局唯一活跃 Provider
|
||||||
|
↓
|
||||||
|
get_active_provider() → 取 Provider 配置
|
||||||
|
↓
|
||||||
|
CompletionRequest { model: provider.default_model } ← 直接用固定 model
|
||||||
|
```
|
||||||
|
|
||||||
|
- 一个时刻只有一个活跃 Provider
|
||||||
|
- 所有场景(对话/标题/知识提炼/项目扫描)共用同一个 model
|
||||||
|
- 无 ModelCapability / ModelRouter / 按场景路由机制
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. 模型配置数据模型
|
||||||
|
|
||||||
|
### 2.1 ModelConfig(每个模型的完整描述)
|
||||||
|
|
||||||
|
```rust
|
||||||
|
/// 单个模型的完整配置(4 维度 + 路由控制)
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct ModelConfig {
|
||||||
|
// ── 基础 ──
|
||||||
|
/// 模型名(如 "glm-4v-flash")
|
||||||
|
pub model_id: String,
|
||||||
|
/// 启用/禁用(false = 配了但不参与路由,相当于"备档")
|
||||||
|
#[serde(default = "default_true")]
|
||||||
|
pub enabled: bool,
|
||||||
|
/// 用户自定义别名(可选)
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub label: Option<String>,
|
||||||
|
|
||||||
|
// ── 维度 1:模态(能接收什么输入)──
|
||||||
|
pub modalities: Vec<Modality>,
|
||||||
|
|
||||||
|
// ── 维度 2:能力(能做什么)──
|
||||||
|
pub capabilities: Vec<Capability>,
|
||||||
|
|
||||||
|
// ── 维度 3:价格(成本分级)──
|
||||||
|
pub cost_tier: CostTier,
|
||||||
|
|
||||||
|
// ── 维度 4:聪明程度(智力分级)──
|
||||||
|
pub intelligence: IntelligenceTier,
|
||||||
|
|
||||||
|
// ── 路由控制 ──
|
||||||
|
/// 权重(0-100,同能力候选中优先选权重高的)
|
||||||
|
#[serde(default = "default_weight")]
|
||||||
|
pub weight: u32,
|
||||||
|
/// 上下文窗口大小(tokens)
|
||||||
|
#[serde(default = "default_context_window")]
|
||||||
|
pub context_window: usize,
|
||||||
|
|
||||||
|
// ── 探测元数据(只读,由 ModelProbe 填充)──
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub probe_source: Option<ProbeSource>,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn default_true() -> bool { true }
|
||||||
|
fn default_weight() -> u32 { 50 }
|
||||||
|
fn default_context_window() -> usize { 8192 }
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2.2 枚举定义
|
||||||
|
|
||||||
|
```rust
|
||||||
|
/// 模态(输入类型)
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||||
|
#[serde(rename_all = "snake_case")]
|
||||||
|
pub enum Modality {
|
||||||
|
Text,
|
||||||
|
Vision,
|
||||||
|
// 未来扩展:Audio, Video
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 能力
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||||
|
#[serde(rename_all = "snake_case")]
|
||||||
|
pub enum Capability {
|
||||||
|
ToolUse, // function calling / tool use
|
||||||
|
Embedding, // 向量嵌入
|
||||||
|
CodeGen, // 代码生成强项
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 价格分级
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||||
|
#[serde(rename_all = "snake_case")]
|
||||||
|
pub enum CostTier {
|
||||||
|
Free, // 免费
|
||||||
|
Low, // flash / mini 系列
|
||||||
|
Medium, // 标准定价
|
||||||
|
High, // 旗舰定价
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 聪明程度(智力分级)
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||||
|
#[serde(rename_all = "snake_case")]
|
||||||
|
pub enum IntelligenceTier {
|
||||||
|
Lite, // flash/mini/lite/nano — 快但简单任务
|
||||||
|
Standard, // air/standard — 日常够用
|
||||||
|
Plus, // plus/pro/max — 复杂推理
|
||||||
|
Ultra, // 最强模型 — 兜底用
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2.3 存储方式:扩展 models JSON 字段(零迁移)
|
||||||
|
|
||||||
|
`AiProviderRecord.models` 从 `["glm-4-flash", "glm-4v"]` 升级为:
|
||||||
|
|
||||||
|
```json
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"model_id": "glm-4-flash",
|
||||||
|
"enabled": true,
|
||||||
|
"modalities": ["text"],
|
||||||
|
"capabilities": ["tool_use"],
|
||||||
|
"cost_tier": "low",
|
||||||
|
"intelligence": "lite",
|
||||||
|
"weight": 90,
|
||||||
|
"context_window": 128000
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"model_id": "glm-4v",
|
||||||
|
"enabled": true,
|
||||||
|
"modalities": ["text", "vision"],
|
||||||
|
"capabilities": ["tool_use"],
|
||||||
|
"cost_tier": "medium",
|
||||||
|
"intelligence": "plus",
|
||||||
|
"weight": 70,
|
||||||
|
"context_window": 128000
|
||||||
|
}
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
**向后兼容**:自定义反序列化器,老格式 `["model-a"]` → 自动转成默认配置列表。
|
||||||
|
|
||||||
|
```rust
|
||||||
|
fn deserialize_models<'de, D>(d: D) -> Result<Vec<ModelConfig>, D::Error>
|
||||||
|
where D: serde::Deserializer<'de> {
|
||||||
|
let v = serde_json::Value::deserialize(d)?;
|
||||||
|
match v {
|
||||||
|
// 老格式:字符串数组 → 每个转默认 ModelConfig
|
||||||
|
serde_json::Value::Array(arr) if arr.iter().all(|v| v.is_string()) => {
|
||||||
|
arr.into_iter()
|
||||||
|
.filter_map(|v| v.as_str().map(|s| ModelConfig::with_defaults(s)))
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.pipe(Ok)
|
||||||
|
}
|
||||||
|
// 新格式:ModelConfig 数组
|
||||||
|
serde_json::Value::Array(_) => {
|
||||||
|
serde_json::from_value::<Vec<ModelConfig>>(v).map_err(D::Error::custom)
|
||||||
|
}
|
||||||
|
_ => Ok(vec![]),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**否决方案**:新建 `ai_models` 独立表 — 增量复杂度高(迁移 + 新 Repo),`models` JSON 字段已够用。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. 多角度佐证
|
||||||
|
|
||||||
|
### 3.1 行业对标
|
||||||
|
|
||||||
|
| 产品 | 模型配置方式 | 对照 |
|
||||||
|
|------|------------|------|
|
||||||
|
| OpenRouter | 每个模型标 modality/pricing/context_length | 4 维度完全覆盖,且加了 intelligence 分级 |
|
||||||
|
| Cursor | 内置模型能力感知(自动判断图片/工具) | 预设表 + 启发式探测本质相同 |
|
||||||
|
| LangChain ModelRouter | 按 max_tokens/supports_tool_use 路由 | capabilities + modalities 路由一致 |
|
||||||
|
| One-API / New-API | 纯渠道管理,无能力感知 | 能力标记是显著增量 |
|
||||||
|
|
||||||
|
**结论**:4 维度配置是行业标配的超集,非过度设计。
|
||||||
|
|
||||||
|
### 3.2 路由器实际需求倒推
|
||||||
|
|
||||||
|
| 路由决策 | 必须知道 | 对应维度 | 缺了会怎样 |
|
||||||
|
|---------|---------|---------|-----------|
|
||||||
|
| 含图消息选哪个 | 是否支持 Vision | 模态 | 发给纯文本模型 → 400 |
|
||||||
|
| Agentic loop 选哪个 | 是否支持 tool_use | 能力 | 发 tool 定义给不支持的 → 忽略/报错 |
|
||||||
|
| 标题生成选哪个 | 够便宜够快 | 价格+智力 | 用旗舰模型 → 烧钱 |
|
||||||
|
| 复杂推理选哪个 | 够聪明 | 智力 | 用 flash → 质量差 |
|
||||||
|
| 上下文超长选哪个 | 窗口够大 | context_window | 超窗口 → 截断 |
|
||||||
|
|
||||||
|
**结论**:每个维度直接对应路由器必须做的判断,缺一不可。
|
||||||
|
|
||||||
|
### 3.3 用户认知负担
|
||||||
|
|
||||||
|
| 信息 | 用户负担 | 说明 |
|
||||||
|
|------|---------|------|
|
||||||
|
| 模型名 | 零 | 从文档复制 |
|
||||||
|
| 模态 | 低 | 文档一眼可见,或名字带 v |
|
||||||
|
| 能力 | 低 | 文档明确标注 |
|
||||||
|
| 价格 | 中 | 查文档,但分级别直觉可判 |
|
||||||
|
| 智力 | 低 | flash < air < plus 是行业共识 |
|
||||||
|
|
||||||
|
**结论**:4 个维度都是用户本来就知道的,加上自动填充,门槛极低。
|
||||||
|
|
||||||
|
### 3.4 可扩展性
|
||||||
|
|
||||||
|
| 未来需求 | 支持 | 扩展方式 |
|
||||||
|
|---------|------|---------|
|
||||||
|
| 音频输入 | ✅ | Modality 加 Audio |
|
||||||
|
| 视频输入 | ✅ | Modality 加 Video |
|
||||||
|
| 精确价格控制 | ✅ | cost_tier 升级为精确数值 |
|
||||||
|
| 流式支持差异 | ✅ | Capability 加 Streaming |
|
||||||
|
| 自定义能力标签 | ✅ | Capability 加 Custom(String) |
|
||||||
|
|
||||||
|
**结论**:enum + Vec 开放式,未来加维度只需加变体。
|
||||||
|
|
||||||
|
### 3.5 数据兼容性
|
||||||
|
|
||||||
|
老 `models: ["model-a"]` → 自定义反序列化自动补默认值。`default_model` 字段保留向后兼容。零迁移脚本。
|
||||||
|
|
||||||
|
### 3.6 竞品缺陷反证
|
||||||
|
|
||||||
|
| 问题 | 竞品现状 | 我们的方案 |
|
||||||
|
|------|---------|-----------|
|
||||||
|
| 含图发给纯文本模型 | One-API 不感知能力 | 路由器自动选 Vision |
|
||||||
|
| 标题生成用旗舰模型 | 固定模型手动切 | 按 intelligence+cost 自动选 Lite |
|
||||||
|
| 新模型不知道能不能用 | 用户自己试 | 预设表 + 启发式自动推断 |
|
||||||
|
| 限流后无 fallback | 手动切换 | 按 weight 自动 fallback |
|
||||||
|
|
||||||
|
### 3.7 实施风险
|
||||||
|
|
||||||
|
| 风险 | 评估 | 缓解 |
|
||||||
|
|------|------|------|
|
||||||
|
| 预设表维护 | 低(主流 20-30 个,半年更新) | JSON 配置文件可热更新 |
|
||||||
|
| 启发式误判 | 低(命名是行业惯例) | 标注 Medium 可信度,用户可修正 |
|
||||||
|
| 路由器选错 | 低 | weight fallback + 降级兜底 |
|
||||||
|
|
||||||
|
**7 个角度一致指向:方案可行且合理。**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. 模型探测器(ModelProbe)
|
||||||
|
|
||||||
|
### 4.1 多源探测
|
||||||
|
|
||||||
|
每个模型的信息从多个来源获取,交叉验证:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
pub struct ModelProbe {
|
||||||
|
preset_table: ModelPresetTable,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ModelProbe {
|
||||||
|
pub async fn probe(&self, provider: &AiProviderRecord, model_id: &str) -> ModelProfile {
|
||||||
|
// 1. 启发式:从模型名推断
|
||||||
|
let heuristic = self.heuristic_infer(model_id);
|
||||||
|
|
||||||
|
// 2. 查预设表(精确匹配 > 模糊匹配)
|
||||||
|
let preset = self.preset_table.lookup(model_id);
|
||||||
|
|
||||||
|
// 3. 多源合并(预设表 > 启发式)
|
||||||
|
let merged = self.merge_sources(heuristic, preset);
|
||||||
|
|
||||||
|
// 4. 最终结果 + 标注每个维度的置信度
|
||||||
|
self.finalize(merged)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.2 探测优先级链
|
||||||
|
|
||||||
|
```
|
||||||
|
用户手动设定 (UserSet) ← 最高优先,永不被覆盖
|
||||||
|
↓
|
||||||
|
内置预设表精确匹配 (PresetTable) ← 高可信
|
||||||
|
↓
|
||||||
|
模型名启发式 (Heuristic) ← 中可信,标注"建议确认"
|
||||||
|
↓
|
||||||
|
默认值 (Default) ← 低可信
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.3 探测结果带置信度
|
||||||
|
|
||||||
|
```rust
|
||||||
|
pub struct ModelProfile {
|
||||||
|
pub model_id: String,
|
||||||
|
pub modalities: SourcedField<Vec<Modality>>,
|
||||||
|
pub capabilities: SourcedField<Vec<Capability>>,
|
||||||
|
pub cost_tier: SourcedField<CostTier>,
|
||||||
|
pub intelligence: SourcedField<IntelligenceTier>,
|
||||||
|
pub context_window: SourcedField<usize>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct SourcedField<T> {
|
||||||
|
pub value: T,
|
||||||
|
pub source: Source,
|
||||||
|
pub confidence: Confidence,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub enum Source { PresetTable, Heuristic, UserSet }
|
||||||
|
|
||||||
|
pub enum Confidence { High, Medium, Low }
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.4 内置预设表
|
||||||
|
|
||||||
|
```rust
|
||||||
|
pub struct ModelPresetTable {
|
||||||
|
exact: HashMap<String, ModelPreset>, // 精确匹配
|
||||||
|
patterns: Vec<PatternRule>, // 模糊匹配
|
||||||
|
}
|
||||||
|
|
||||||
|
// 精确匹配示例
|
||||||
|
"glm-4-flash" => { modalities:[Text], capabilities:[ToolUse], cost:Low, intelligence:Lite, context:128K }
|
||||||
|
"glm-4-air" => { modalities:[Text], capabilities:[ToolUse], cost:Low, intelligence:Standard }
|
||||||
|
"glm-4-plus" => { modalities:[Text], capabilities:[ToolUse], cost:Medium, intelligence:Plus }
|
||||||
|
"glm-4v" => { modalities:[Text,Vision], capabilities:[ToolUse], cost:Medium, intelligence:Plus }
|
||||||
|
"glm-4v-flash" => { modalities:[Text,Vision], capabilities:[ToolUse], cost:Low, intelligence:Lite }
|
||||||
|
"deepseek-chat" => { modalities:[Text], capabilities:[ToolUse], cost:Low, intelligence:Standard }
|
||||||
|
"deepseek-coder"=> { modalities:[Text], capabilities:[ToolUse,CodeGen], cost:Low, intelligence:Plus }
|
||||||
|
"claude-3-5-sonnet-20241022" => { modalities:[Text,Vision], capabilities:[ToolUse], cost:High, intelligence:Ultra }
|
||||||
|
"claude-3-haiku-20240307" => { modalities:[Text,Vision], capabilities:[ToolUse], cost:Low, intelligence:Standard }
|
||||||
|
"gpt-4o" => { modalities:[Text,Vision], capabilities:[ToolUse], cost:High, intelligence:Ultra }
|
||||||
|
"gpt-4o-mini" => { modalities:[Text,Vision], capabilities:[ToolUse], cost:Low, intelligence:Standard }
|
||||||
|
"embedding-3" => { modalities:[], capabilities:[Embedding], cost:Low, intelligence:Lite }
|
||||||
|
|
||||||
|
// 模糊匹配规则
|
||||||
|
PatternRule { keyword: "flash|mini|lite|nano", → Intelligence(Lite) }
|
||||||
|
PatternRule { keyword: "plus|pro|max|ultra", → Intelligence(Plus) }
|
||||||
|
PatternRule { keyword: "v$|vl|vision|-v\\d", → Modality(Vision) }
|
||||||
|
PatternRule { keyword: "embed", → Capability(Embedding) }
|
||||||
|
PatternRule { keyword: "code|coder", → Capability(CodeGen) }
|
||||||
|
```
|
||||||
|
|
||||||
|
预设表以 JSON 配置文件形式存储(`crates/df-ai/presets/models.json`),可热更新不随版本绑。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. 厂商模型列表适配
|
||||||
|
|
||||||
|
### 5.1 差异盘点
|
||||||
|
|
||||||
|
| 厂商/协议 | 端点 | 鉴权 | 返回格式 |
|
||||||
|
|-----------|------|------|---------|
|
||||||
|
| OpenAI 兼容(GLM/DeepSeek/Kimi/通义) | `GET /v1/models` | Bearer | `{data:[{id, owned_by}]}` |
|
||||||
|
| Anthropic | `GET /v1/models` | x-api-key + version | `{data:[{id, display_name}]}` |
|
||||||
|
| 中转站 | `GET /v1/models` | Bearer | 同 OpenAI,但可能混入噪音或返回 404 |
|
||||||
|
| Ollama | `GET /api/tags` | 无 | `{models:[{name, size}]}` — 完全不同 |
|
||||||
|
|
||||||
|
### 5.2 统一拉取接口
|
||||||
|
|
||||||
|
```rust
|
||||||
|
/// 模型拉取结果(统一格式)
|
||||||
|
pub struct FetchedModel {
|
||||||
|
pub model_id: String,
|
||||||
|
pub display_name: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 按 provider_type 分派
|
||||||
|
pub async fn fetch_models(
|
||||||
|
provider_type: &str,
|
||||||
|
base_url: &str,
|
||||||
|
api_key: &str,
|
||||||
|
) -> Result<Vec<FetchedModel>, FetchError> {
|
||||||
|
match provider_type {
|
||||||
|
"openai_compat" => fetch_openai_compat(base_url, api_key).await,
|
||||||
|
"anthropic_compat" => fetch_anthropic_compat(base_url, api_key).await,
|
||||||
|
_ => Err(FetchError::Unsupported),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5.3 URL 智能拼接
|
||||||
|
|
||||||
|
```rust
|
||||||
|
fn build_models_url(base_url: &str) -> String {
|
||||||
|
let url = base_url.trim_end_matches('/');
|
||||||
|
if url.ends_with("/chat/completions") {
|
||||||
|
url.replace("/chat/completions", "/models")
|
||||||
|
} else if url.ends_with("/v1") || url.ends_with("/v4") {
|
||||||
|
format!("{}/models", url)
|
||||||
|
} else if url.ends_with("/models") {
|
||||||
|
url.to_string()
|
||||||
|
} else {
|
||||||
|
format!("{}/v1/models", url)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5.4 噪音过滤
|
||||||
|
|
||||||
|
```rust
|
||||||
|
fn is_non_chat_model(id: &str) -> bool {
|
||||||
|
let id = id.to_lowercase();
|
||||||
|
id.contains("dall-e") || id.contains("midjourney") // 图片生成
|
||||||
|
|| id.contains("tts") || id.contains("whisper") // 语音
|
||||||
|
|| id.contains("moderation") // 审查
|
||||||
|
// embedding 保留(知识库需要)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5.5 失败降级
|
||||||
|
|
||||||
|
| 错误 | 处理 |
|
||||||
|
|------|------|
|
||||||
|
| 404 / 不支持 | 静默降级到手动输入(批量文本) |
|
||||||
|
| 401 / 403 | 提示密钥问题 |
|
||||||
|
| 网络/超时 | 提示检查连接 |
|
||||||
|
| 解析失败 | 提示格式不兼容,降级手动 |
|
||||||
|
|
||||||
|
**核心原则:自动拉取是锦上添花,手动输入永远保底。**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. 模型路由器(ModelRouter)
|
||||||
|
|
||||||
|
### 6.1 路由逻辑
|
||||||
|
|
||||||
|
```rust
|
||||||
|
pub struct TaskRequirements {
|
||||||
|
pub modalities: Vec<Modality>, // 任务需要什么模态
|
||||||
|
pub needs_tool_use: bool, // 是否需要工具调用
|
||||||
|
pub min_intelligence: IntelligenceTier, // 最低智力要求
|
||||||
|
pub max_cost: Option<CostTier>, // 成本上限(可选)
|
||||||
|
pub estimated_context: usize, // 预估上下文大小
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ModelRouter {
|
||||||
|
pub fn select(&self, req: &TaskRequirements, pool: &[ModelConfig]) -> Option<&ModelConfig> {
|
||||||
|
pool.iter()
|
||||||
|
.filter(|m| m.enabled) // 1. 只选启用的
|
||||||
|
.filter(|m| req.modalities.iter().all(|r| m.modalities.contains(r))) // 2. 模态匹配
|
||||||
|
.filter(|m| !req.needs_tool_use || m.capabilities.contains(&Capability::ToolUse)) // 3. 能力匹配
|
||||||
|
.filter(|m| m.intelligence >= req.min_intelligence) // 4. 智力达标
|
||||||
|
.filter(|m| req.max_cost.map_or(true, |max| m.cost_tier <= max)) // 5. 成本可控
|
||||||
|
.filter(|m| m.context_window >= req.estimated_context) // 6. 窗口够大
|
||||||
|
.max_by_key(|m| (m.weight, -(m.cost_tier as i32))) // 7. 权重优先,同权重选便宜的
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6.2 场景路由表
|
||||||
|
|
||||||
|
| 场景 | 路由偏好 | 典型选择 |
|
||||||
|
|------|---------|---------|
|
||||||
|
| 标题生成 | Lite + Low cost | glm-4-flash |
|
||||||
|
| 日常对话 | Standard | glm-4-air |
|
||||||
|
| 写代码 | Standard/Plus + CodeGen | deepseek-coder |
|
||||||
|
| 含图片 | Vision + Standard | glm-4v |
|
||||||
|
| 复杂推理 | Plus/Ultra | glm-4-plus |
|
||||||
|
| 知识嵌入 | Embedding | embedding-3 |
|
||||||
|
|
||||||
|
### 6.3 7 个调用点接入
|
||||||
|
|
||||||
|
| 调用点 | 当前代码位置 | TaskRequirements |
|
||||||
|
|--------|------------|-----------------|
|
||||||
|
| 主对话 stream_llm | agentic.rs:50 | 动态(按消息内容:含图→Vision) |
|
||||||
|
| 标题生成 | title.rs:63 | Lite + Low(省钱) |
|
||||||
|
| 知识提炼 | knowledge_inject.rs:33,323 | Standard |
|
||||||
|
| 知识嵌入 | knowledge_inject.rs embedding | Embedding |
|
||||||
|
| 项目扫描描述 | project.rs:378 | Standard(含图→Vision) |
|
||||||
|
| 灵感评估 | df-ideas adversarial.rs | Standard |
|
||||||
|
| AiNode 工作流 | df-nodes ai_node.rs:118 | 按节点配置 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. 用户配置与使用流程
|
||||||
|
|
||||||
|
### 7.1 配置流程(做一次)
|
||||||
|
|
||||||
|
```
|
||||||
|
Step 1: 填 Provider 基本信息(名称/类型/地址/密钥)
|
||||||
|
↓
|
||||||
|
Step 2: 点「测试连接并拉取模型」
|
||||||
|
↓ 自动调 /v1/models(按协议分派)
|
||||||
|
↓ 自动过滤非 chat 模型
|
||||||
|
↓ 自动探测每个模型特征(预设表 + 启发式)
|
||||||
|
↓
|
||||||
|
Step 3: 显示模型列表(已标注特征 + 可信度)
|
||||||
|
↓ 用户确认或微调(大多数不需要进这步)
|
||||||
|
↓
|
||||||
|
Step 4: 完成
|
||||||
|
```
|
||||||
|
|
||||||
|
**拉取失败时降级**:手动输入模型名(批量文本,每行一个)→ 同样走探测。
|
||||||
|
|
||||||
|
### 7.2 日常使用(零配置)
|
||||||
|
|
||||||
|
```
|
||||||
|
用户发消息 → 路由器自动选模型 → 用户无感 → 出结果
|
||||||
|
```
|
||||||
|
|
||||||
|
**手动覆盖**:AiChat 顶部下拉选「自动(推荐)」或指定具体模型。
|
||||||
|
|
||||||
|
**自动 fallback**:模型限流/失败 → 按 weight 选次高 → 自动重试。
|
||||||
|
|
||||||
|
### 7.3 简洁性指标
|
||||||
|
|
||||||
|
| 传统做法 | 我们的做法 | 省了什么 |
|
||||||
|
|---------|-----------|---------|
|
||||||
|
| 手动填每个模型能力 | 预设表自动填充 | 省 90% 配置 |
|
||||||
|
| 自己记哪个支持图片 | 路由器自动选 | 省记忆 |
|
||||||
|
| 每次对话手动切模型 | 自动路由 | 省操作 |
|
||||||
|
| 限流后手动换 | 自动 fallback | 省故障处理 |
|
||||||
|
|
||||||
|
**用户最小操作路径**:填 3 个字段 → 点 1 个按钮 → 完成。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. 实施分阶段
|
||||||
|
|
||||||
|
### 阶段 1:数据模型(df-ai-core + df-storage)
|
||||||
|
|
||||||
|
| 文件 | 改动 |
|
||||||
|
|------|------|
|
||||||
|
| `crates/df-ai-core/src/provider.rs` | 新增 ModelConfig / Modality / Capability / CostTier / IntelligenceTier |
|
||||||
|
| `crates/df-storage/src/models.rs` | AiProviderRecord.models 反序列化升级(兼容老格式) |
|
||||||
|
| `crates/df-ai/src/model_config.rs`(新建) | deserialize_models 兼容函数 + ModelConfig::with_defaults |
|
||||||
|
|
||||||
|
### 阶段 2:预设表 + 探测器(df-ai)
|
||||||
|
|
||||||
|
| 文件 | 改动 |
|
||||||
|
|------|------|
|
||||||
|
| `crates/df-ai/presets/models.json`(新建) | 主流模型预设数据 |
|
||||||
|
| `crates/df-ai/src/model_probe.rs`(新建) | ModelProbe + 启发式 + 多源合并 |
|
||||||
|
| `crates/df-ai/src/preset_table.rs`(新建) | 预设表加载 + 精确/模糊匹配 |
|
||||||
|
|
||||||
|
### 阶段 3:厂商模型列表拉取(df-ai)
|
||||||
|
|
||||||
|
| 文件 | 改动 |
|
||||||
|
|------|------|
|
||||||
|
| `crates/df-ai/src/model_fetch.rs`(新建) | fetch_models + openai_compat/anthropic_compat 分派 + URL 拼接 + 噪音过滤 |
|
||||||
|
|
||||||
|
### 阶段 4:路由器(df-ai)
|
||||||
|
|
||||||
|
| 文件 | 改动 |
|
||||||
|
|------|------|
|
||||||
|
| `crates/df-ai/src/router.rs`(新建) | ModelRouter + TaskRequirements + select 逻辑 |
|
||||||
|
|
||||||
|
### 阶段 5:7 调用点接入(src-tauri)
|
||||||
|
|
||||||
|
| 文件 | 改动 |
|
||||||
|
|------|------|
|
||||||
|
| `src-tauri/src/commands/ai/agentic.rs` | 主对话路由(按 has_image 动态选) |
|
||||||
|
| `src-tauri/src/commands/ai/title.rs` | 标题生成路由(Lite + Low) |
|
||||||
|
| `src-tauri/src/commands/ai/knowledge_inject.rs` | 知识提炼/嵌入路由 |
|
||||||
|
| `src-tauri/src/commands/project.rs` | 项目扫描路由 |
|
||||||
|
| `src-tauri/src/commands/ai/commands.rs` | 新增 IPC:ai_fetch_models / ai_probe_model |
|
||||||
|
|
||||||
|
### 阶段 6:前端(src)
|
||||||
|
|
||||||
|
| 文件 | 改动 |
|
||||||
|
|------|------|
|
||||||
|
| `src/api/types.ts` | ModelConfig 类型对齐 |
|
||||||
|
| `src/components/settings/` | Provider 配置页加模型列表 + 探测结果展示 + 启用/禁用/权重 |
|
||||||
|
| `src/components/AiChat.vue` | 顶部模型下拉(自动/指定) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. 与 F-05(多模态)的关系
|
||||||
|
|
||||||
|
| 子能力 | 依赖 F-01? | 说明 |
|
||||||
|
|--------|------------|------|
|
||||||
|
| ContentPart 数据模型 | ❌ | 纯类型升级,与 ModelConfig 解耦 |
|
||||||
|
| provider content 数组化 | ❌ | 协议层改造,与路由器无关 |
|
||||||
|
| 前端粘贴/拖拽/渲染 | ❌ | 纯前端 |
|
||||||
|
| **vision 模型自动路由** | ✅ | ModelRouter 按 modalities 含 Vision 筛选 |
|
||||||
|
|
||||||
|
F-05 可独立先做 80%(数据模型 + provider + 前端),vision 路由留到 F-01 落地后接入。
|
||||||
|
过渡期靠 ModelConfig.modalities 手动标注 + 路由器筛选。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 10. 不做的事(显式排除)
|
||||||
|
|
||||||
|
- 不做模型自动下载/安装(仅 API 模型,不含本地模型管理)
|
||||||
|
- 不做模型 benchmark 自动跑分(智力分级靠预设表 + 用户确认)
|
||||||
|
- 不做多 Provider 负载均衡(属 F-04,本任务只做单 Provider 内多模型路由)
|
||||||
|
- 不做模型用量统计/成本告警(属运维增强,后续独立做)
|
||||||
|
- 不新建 ai_models 独立表(扩展 models JSON 字段够用)
|
||||||
|
- 本文档不实施任何 code(纯设计)
|
||||||
@@ -104,7 +104,7 @@ pub struct ImageRef {
|
|||||||
|
|
||||||
### 1.6 F-01(Phase 1)现状
|
### 1.6 F-01(Phase 1)现状
|
||||||
|
|
||||||
`crates/df-ai/src/model_capability.rs` / `router.rs` **均不存在**(Glob 确认)。决策记录(功能决策记录 §模型能力与路由)标 📐 设计未实施。F-05 因此不能假定 `ModelCapability.modalities` 已可用,必须给出「F-01 未落地」的过渡方案。
|
`crates/df-ai/src/model_probe.rs` / `router.rs` **均不存在**(尚未实施)。但 **F-01 设计已定稿**(2026-06-16,见 [F-01-模型能力系统与智能路由设计-2026-06-16.md](./F-01-模型能力系统与智能路由设计-2026-06-16.md)),定义了 `ModelConfig.modalities` + `ModelRouter` 按 `has_image` 路由。F-05 阶段 1-3 可独立先做,阶段 4 vision 路由待 F-01 实施后接入;过渡期用「provider 配置的 vision 模型名静态白名单」。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
641
docs/02-架构设计/F-09B-多会话并发设计-2026-06-16.md
Normal file
641
docs/02-架构设计/F-09B-多会话并发设计-2026-06-16.md
Normal file
@@ -0,0 +1,641 @@
|
|||||||
|
# F-260616-09 B 阶段 — AiSession 单例→多会话并发架构设计
|
||||||
|
|
||||||
|
> **状态**:设计稿(2026-06-16) | **关联**:[docs/待决策.md F-260616-09](../待决策.md) / [todo F-260616-12](../todo.md) / memory `aichat-arch-extensibility`
|
||||||
|
> **决策基线**(已定,勿推翻):a A 先隔离 + B 立项 / b 仅拆 generating+stop_flag per-conv(非整 HashMap) / c 复用 llm_concurrency.global 限并发会话数 / d1 侧栏 + d2 独立窗口都做(先 d1 后 d2) / e 切换不退出旧 loop 各自跑完(真并发)
|
||||||
|
> **范围**:仅本设计文档,不改代码。本文 file:line 证据均独立 grep/read 核验(不信文档/会话描述,防上下文污染)。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. 现状盘点(AiSession 单例 + 字段消费点)
|
||||||
|
|
||||||
|
### 1.1 单例定义与初始化
|
||||||
|
|
||||||
|
**单例锚点**(state.rs:165):
|
||||||
|
```rust
|
||||||
|
pub ai_session: Arc<Mutex<AiSession>>,
|
||||||
|
// init: state.rs:216 ai_session: Arc::new(Mutex::new(AiSession::new())),
|
||||||
|
```
|
||||||
|
|
||||||
|
整个应用**一份** `AiSession`,被 `Arc<Mutex<>>` 包裹,所有 IPC 命令竞争同一把锁。
|
||||||
|
|
||||||
|
**AiSession 字段**(mod.rs:166-216,共 10 字段):
|
||||||
|
|
||||||
|
| 字段 | 行 | 类型 | 多会话语义 |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `messages` | 168 | `ContextManager` | **会话级**(每对话一份历史),当前单例:切换走 `restore_from_messages` 覆盖 |
|
||||||
|
| `active_provider_id` | 170 | `Option<String>` | 应用级(provider 全局共享),保持单例 |
|
||||||
|
| `active_conversation_id` | 172 | `Option<String>` | **会话级**(当前活跃 conv 路由键),单例=只一个 active |
|
||||||
|
| `active_conv_created_at` | 174 | `Option<String>` | **会话级**(懒创建时间戳,随 active_conv) |
|
||||||
|
| `pending_approvals` | 186 | `HashMap<tool_call_id, PendingApproval>` | **会话级**(已带 `conversation_id` 字段 audit.rs:598/276),单例下靠 retain 过滤 |
|
||||||
|
| `generating` | 188 | `bool` | **会话级**(每对话独立流式态),单例=全局互斥 |
|
||||||
|
| `agent_language` | 190 | `Option<String>` | **会话级**(每对话独立语言),单例下切换须清空 |
|
||||||
|
| `stop_flag` | 192 | `Arc<AtomicBool>` | **会话级**(每对话独立停止信号),单例=全局唯一 |
|
||||||
|
| `notify` | 203 | `Arc<tokio::sync::Notify>` | **会话级**(随 stop_flag 配对),单例=全局唯一 |
|
||||||
|
| `iteration_used` | 215 | `usize` | **会话级**(每对话独立计数,mod.rs:214 注释已标"F-09 B 改 per-conv") |
|
||||||
|
|
||||||
|
**关键观察**:`mod.rs:214` 注释自身已写「**当前 AiSession 是全局单例(F-09 B 多会话架构落地时改 per-conv)**」——本设计是早已预留的债。
|
||||||
|
|
||||||
|
### 1.2 字段消费点 grep 核验(file:line)
|
||||||
|
|
||||||
|
> 全部为 grep 实测命中,非文档转述。`state.ai_session.lock().await` 共 **41 处**(commands.rs 主集中)。
|
||||||
|
|
||||||
|
**`generating` 写/读点**(单例互斥根因):
|
||||||
|
|
||||||
|
| 文件:行 | 操作 | 说明 |
|
||||||
|
|---|---|---|
|
||||||
|
| `commands.rs:48/51` | read+write | ai_regenerate: `if session.generating {return Err}` + `=true` |
|
||||||
|
| `commands.rs:125` | read | ai_is_generating IPC(前端 force_send 预检用) |
|
||||||
|
| `commands.rs:145/148` | read+write | **ai_chat_send 互斥入口**:`if generating {Err "正在生成中"}` + `=true` |
|
||||||
|
| `commands.rs:399/420` | read+write | ai_edit_last 同款互斥 |
|
||||||
|
| `commands.rs:505/534/540/565/602/636/644` | write | force_send/stop/continue/stop_loop 多路复位 |
|
||||||
|
| `commands.rs:848/850` | read+write | **ai_conversation_create:生成中强制结束旧会话**(B-260615-10 软复位) |
|
||||||
|
| `commands.rs:935` | read | ai_conversation_switch:**生成中 readonly 切换**(:935-941,后台 loop 不动 active) |
|
||||||
|
| `agentic.rs:117` | guard | GeneratingGuard RAII 收敛复位(:85-94 Drop 兜底) |
|
||||||
|
| `agentic.rs:585` | read | try_continue_agent_loop:读 generating + pending 决定续跑 |
|
||||||
|
| `lib.rs:43/46` | read+write | L0 握手:HMR/刷新清残留 generating |
|
||||||
|
|
||||||
|
**`stop_flag` 读写点**(会话级停止信号):
|
||||||
|
|
||||||
|
| 文件:行 | 操作 | 说明 |
|
||||||
|
|---|---|---|
|
||||||
|
| `agentic.rs:162` | clone | loop 入口 `session.stop_flag.clone()` 取 Arc 副本 |
|
||||||
|
| `agentic.rs:178/455` | load | loop 顶部 + stream 后查 stop_flag |
|
||||||
|
| `commands.rs:52/149/421/507/541/548/610/643/852/872` | store | send/regenerate/edit/stop/force/continue/create 多路置位 |
|
||||||
|
| `stream_recv.rs:141/171/321` | load | stream select! 内查 stop_flag |
|
||||||
|
|
||||||
|
**`pending_approvals` 读写点**(会话级审批,已带 conversation_id):
|
||||||
|
|
||||||
|
| 文件:行 | 操作 | 说明 |
|
||||||
|
|---|---|---|
|
||||||
|
| `commands.rs:236` | remove | ai_approve 按 tool_call_id 路由(精确键) |
|
||||||
|
| `commands.rs:347` | filter | ai_pending_tool_calls 按 conversation_id O(n) 过滤 |
|
||||||
|
| `commands.rs:365/506/539/851/868/948/969` | clear/retain | 多路清理 |
|
||||||
|
| `agentic.rs:583-585` | iter | try_continue:取任一审批的 conversation_id |
|
||||||
|
| `agentic.rs:593-597` | read | 审批等待态判断 |
|
||||||
|
| `audit.rs:269/593` | insert | restore_pending_approvals + process_tool_calls(均带 conversation_id) |
|
||||||
|
|
||||||
|
**`agent_language` 读写点**(会话级语言):
|
||||||
|
- 写:`commands.rs:53/150/422`(send/regenerate/edit 入口设)
|
||||||
|
- 写:`commands.rs:873`(create 清空,F-260616-09 A 路线补漏)
|
||||||
|
- 读:`agentic.rs:650`(try_continue 恢复循环读)
|
||||||
|
|
||||||
|
**`iteration_used` 读写点**(会话级计数,F-260616-11 落地):
|
||||||
|
- 读:`commands.rs:272/325`(ai_approve 两处续跑累计)
|
||||||
|
- 写:`agentic.rs:210`(loop 每轮 `iteration_used = iteration + 1`)
|
||||||
|
- 重置:`commands.rs:152`(ai_chat_send 新消息=0)/`ai_continue_loop`(达 max 续跑重计=0)
|
||||||
|
|
||||||
|
### 1.3 run_agentic_loop 单例锁与陈旧 loop 退出逻辑
|
||||||
|
|
||||||
|
**loop signature**(agentic.rs:102-115)——`session_arc: Arc<Mutex<AiSession>>` 持整个单例 Arc,loop 内全程竞争全局锁。
|
||||||
|
|
||||||
|
**B-260615-11 陈旧 loop 退出逻辑**(agentic.rs:194-211,427-434,380-388)——**多会话 B 阶段必须改造的核心点**:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
// agentic.rs:197-211 每轮开始校验对话一致性
|
||||||
|
let mut session = session_arc.lock().await;
|
||||||
|
if session.active_conversation_id.as_deref() != Some(conv_id.as_str()) {
|
||||||
|
tracing::warn!("[ai] 对话已切换,旧 loop 退出(B-260615-11)避免污染新对话");
|
||||||
|
return; // ← 决策 e「不退出各自跑完」:此处须删除/改判
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**当前语义**:loop 快照 `conv_id`(入参),每轮查单例 `active_conversation_id`,不一致即 return。
|
||||||
|
- 这是为了**单例下防污染**:用户新建/切换会话,旧 loop 继续跑会 push 到新对话。
|
||||||
|
- **决策 e 真并发下**:旧 loop 跑的是自己的 conv,messages/pending 已按 conv reload 或 per-conv,该退出**反成阻碍**——两个对话各持 conv_id,旧 loop 不应因 `active_conversation_id` 变更而退出。
|
||||||
|
|
||||||
|
同样的 push 前校验在 agentic.rs:380-388(MidStream 保文)/427-434(stream 后 push),三处一致——全部依赖单例 `active_conversation_id` 作「当前 loop 归属」判据,**B 阶段须改判据为 per-conv 的 session 索引**。
|
||||||
|
|
||||||
|
### 1.4 commands.rs create/switch/force_send 漏清与冲突
|
||||||
|
|
||||||
|
**ai_conversation_create**(commands.rs:835-876):
|
||||||
|
- 生成中强制结束旧 loop(:848-862 `generating=false` + `pending_approvals.clear()` + `stop_flag=true` + emit AiCompleted)
|
||||||
|
- F-260616-09 A 路线已补:`stop_flag.store(false)`(:872)+ `agent_language=None`(:873)
|
||||||
|
- **B 阶段冲突**:「新建会话强制结束旧 loop」与决策 e「不退出各自跑完」直接矛盾。B 阶段应**删除该强制结束块**,新会话直接开 per-conv state。
|
||||||
|
|
||||||
|
**ai_conversation_switch**(commands.rs:918-955):
|
||||||
|
- 生成中 `readonly: true`(:935-941)只返回 messages 不改 active——**单例下不得已的妥协**
|
||||||
|
- **B 阶段**:切换=前端切视图 + 后端切 active_conversation_id(应用级路由键),不触碰任一 conv 的 per-conv state。readonly 分支可删除(切走不杀 loop)。
|
||||||
|
|
||||||
|
**ai_chat_force_send**(commands.rs:494-523):
|
||||||
|
- 复位全局 generating + clear pending + stop_flag=true(:503-509)
|
||||||
|
- **B 阶段语义变化**:force_send 改为「针对**目标 conv** 强制复位其 per-conv generating」+「stop 该 conv 的 loop」,不影响其他 conv。
|
||||||
|
|
||||||
|
**ai_is_generating**(commands.rs:123-126):
|
||||||
|
- 返回全局 `session.generating`(单 bool)
|
||||||
|
- **B 阶段**:改为 `ai_is_generating(conv_id) -> bool`,前端 useAiWindow.ts:97 `resumeInDetached` 改传 conv_id。
|
||||||
|
|
||||||
|
### 1.5 ContextManager(messages)——已是会话级语义
|
||||||
|
|
||||||
|
`ContextManager`(context.rs:163-219)内部 `Vec<TrackedMessage>` + token 缓存,无全局共享状态。
|
||||||
|
- `push`/`clear`/`restore_from_messages`(context.rs:176/188/414)均是自包含操作。
|
||||||
|
- **B 形态决策 b 的关键支撑**:`messages` 当前之所以单例,只是因为 AiSession 单例;一旦 generating/stop_flag 拆 per-conv,messages 也可随之 per-conv(HashMap<conv_id, ContextManager>),**侵入面与拆 generating 同量级**。
|
||||||
|
- 决策 b 选「messages 走 conv reload」(不拆 per-conv)是为了**最小侵入**——保留 `restore_from_messages` 路径,切换时从 DB reload。**但真并发下旧 loop 还在往单例 messages push**,会污染刚 reload 的新 active conv。故**最终 B 形态建议**(见 §2.3)messages 也拆 per-conv,否则 §1.3 陈旧 loop 退出逻辑无替代判据。
|
||||||
|
|
||||||
|
### 1.6 llm_concurrency per_conv 当前语义(state.rs:93-133)
|
||||||
|
|
||||||
|
**当前 per_conv 是应用级单一 Semaphore**(:101 `per_conv: Arc<Mutex<Arc<Semaphore>>>`,**非 HashMap**),state.rs:94-97 注释自陈:
|
||||||
|
> per_conv 当前是应用级单一信号量(非 per-conv map)。因 AiSession 为单例 + generating 互斥,同一时刻仅一个对话的 loop 在跑,per_conv 退化为"单对话内并发"(主循环 stream_llm + 标题生成 + 知识提炼)。**未来若支持多对话并发,需改为 HashMap<conv_id, Semaphore>。**
|
||||||
|
|
||||||
|
- `global` Semaphore(state.rs:100)构造时 permits=3(state.rs:221 `LlmConcurrency::new(3, 2)`)
|
||||||
|
- **决策 c**:B 阶段 global 改「并发会话数上限」语义——原本限 LLM 调用并发,改限**并发会话数**(每会话占 1 permit 入口)。
|
||||||
|
- per_conv 改 HashMap<conv_id, Semaphore>,每对话内限流(主循环 + 标题 + 提炼)。
|
||||||
|
- **F-260616-12 依赖点**(todo:86):重试循环(agentic.rs:247-248)持有 global+per_conv permit 不释放。多会话后,重试期阻塞其他对话。B 阶段须处理(§3.3)。
|
||||||
|
|
||||||
|
### 1.7 前端现状
|
||||||
|
|
||||||
|
**stores/ai.ts**(模块级单例 reactive state,45-82):
|
||||||
|
- 会话级字段:`messages`/`streaming`/`currentText`/`generatingConvId`/`activeConversationId`/`pendingApprovals`/`queue`/`agentRound`
|
||||||
|
- **单 webview 共享**(注释:32-34),分离窗口是**独立 webview 各自 state**(useAiWindow.ts:32-34)
|
||||||
|
- **B 阶段 d1(侧栏)**:可保持单 store,只需支持「后台 conv 的 generating 态不被切走清零」(useAiEvents.ts:142-144 已按 conversation_id 路由 generatingConvId,基础设施就绪)
|
||||||
|
- **B 阶段 d2(独立窗口)**:每窗口一个 webview 一个 store,需窗口间状态同步(localStorage 快照 useAiWindow.ts:29 已有雏形)
|
||||||
|
|
||||||
|
**useAiEvents.ts**(handleEvent :123-):**事件按 conversation_id 路由**已就绪(:125 `convId = event.conversation_id`),:133-140 isCurrent 判断决定是否污染当前视图。**conversation_id 全覆盖核验**见 §4。
|
||||||
|
|
||||||
|
**useAiWindow.ts**(1-191):`detachPanel`/`reattachPanel`/`resumeInDetached`/`dockDetached`/`syncToMain`/`startFollowMain` 已完整。单窗口(label='ai-detached'),detach 时快照 generatingConvId 到 localStorage(:28-31)。**d2 多窗口扩展点**:label 改为 `ai-detached-${convId}` 支持每会话独立窗口。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. B 形态方案(决策 b:拆 generating/stop_flag per-conv)
|
||||||
|
|
||||||
|
### 2.1 数据结构
|
||||||
|
|
||||||
|
**新增 `PerConvState`**(放 mod.rs,AiSession 内):
|
||||||
|
|
||||||
|
```rust
|
||||||
|
/// 单个对话的运行态(generating/stop_flag/notify/iteration/agent_language)。
|
||||||
|
/// 多会话并发后,每个 conv 一份,HashMap<conv_id, PerConvState> 挂在 AiSession。
|
||||||
|
///
|
||||||
|
/// 不含 messages/pending_approvals ——
|
||||||
|
/// - messages: 见 §2.3 决策(messages 最终也 per-conv,否则 §1.3 陈旧 loop 退出无替代判据)
|
||||||
|
/// - pending_approvals: 已带 conversation_id,继续走单层 HashMap + retain 过滤(mod.rs:181-185 注释自陈)
|
||||||
|
pub struct PerConvState {
|
||||||
|
pub generating: bool,
|
||||||
|
pub stop_flag: Arc<AtomicBool>,
|
||||||
|
pub notify: Arc<tokio::sync::Notify>,
|
||||||
|
pub iteration_used: usize,
|
||||||
|
pub agent_language: Option<String>,
|
||||||
|
/// 该 conv 的 messages 历史(§2.3:messages per-conv 化后挪入此)
|
||||||
|
pub messages: ContextManager,
|
||||||
|
/// 懒创建时间戳(随 active_conv 走)
|
||||||
|
pub created_at: Option<String>,
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**AiSession 改造**(mod.rs:166):
|
||||||
|
|
||||||
|
```rust
|
||||||
|
pub struct AiSession {
|
||||||
|
// ── 应用级(保持单例) ──
|
||||||
|
pub active_provider_id: Option<String>,
|
||||||
|
pub active_conversation_id: Option<String>, // 路由键:当前展示的 conv(切走即变,不影响后台 loop)
|
||||||
|
// ── 会话级(per-conv) ──
|
||||||
|
pub pending_approvals: HashMap<String, PendingApproval>, // 已带 conversation_id,保持单层
|
||||||
|
pub per_conv: HashMap<String, PerConvState>,
|
||||||
|
// ── L0 握手/A 路线兼容 ──
|
||||||
|
// 旧字段 generating/stop_flag/notify/agent_language/messages/iteration_used 移入 PerConvState
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2.2 访问封装(收敛锁边界)
|
||||||
|
|
||||||
|
**关键:避免 41 处 `session.lock()` 各自写 HashMap 索引**,提供访问器:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
impl AiSession {
|
||||||
|
/// 取某 conv 的 PerConvState(不存在则惰性创建)
|
||||||
|
pub fn conv(&mut self, conv_id: &str) -> &mut PerConvState { ... }
|
||||||
|
|
||||||
|
/// 只读快照(供 IPC 查询,不创建)
|
||||||
|
pub fn conv_read(&self, conv_id: &str) -> Option<&PerConvState> { ... }
|
||||||
|
|
||||||
|
/// 兼容旧调用:取 active_conversation_id 对应 conv(替换单例字段直读)
|
||||||
|
pub fn active_conv(&mut self) -> Option<&mut PerConvState> {
|
||||||
|
self.active_conversation_id.as_ref().and_then(|id| self.per_conv.get_mut(id))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**调用点迁移**:41 处 `session.lock().await.xxx` → `session.lock().await.conv(&conv_id)?.xxx`,**锁粒度不变(仍是单一 AiSession Mutex)**,只是索引多一层。这是**最小侵入**的关键——不拆 N 把锁(否则锁顺序/死锁风险骤增),per-conv 仅是 HashMap 索引隔离。
|
||||||
|
|
||||||
|
### 2.3 messages 是否 per-conv(决策 b 的关键细化)
|
||||||
|
|
||||||
|
**决策 b 原文**:「仅拆 generating/stop_flag per-conv,messages/pending 已可按 conv reload」。
|
||||||
|
|
||||||
|
**核验结论**:**messages 必须同 per-conv**,否则无法落地决策 e「不退出各自跑完」:
|
||||||
|
- 单例 messages 下,旧 loop(已切走的 conv A)继续跑会 push 到**当前 active conv B 的 messages**(单例一份)——这正是 §1.3 B-260615-11 退出逻辑防的污染。
|
||||||
|
- 若 messages 保持单例,旧 loop 退出逻辑**必须保留**(决策 e 落空)。
|
||||||
|
- 若 messages per-conv,旧 loop `session.conv(&conv_a).messages.push(...)` 写自己的 conv,**不污染 B**,退出逻辑可删(决策 e 成立)。
|
||||||
|
|
||||||
|
**故本设计修正决策 b 为**:「拆 generating/stop_flag/notify/iteration/agent_language/**messages** per-conv,仅 pending_approvals 保持单层 HashMap(已带 conversation_id)」。**侵入面增量极小**(messages 本就是 ContextManager 自包含结构,挪 HashMap 即可),但解锁了决策 e。**此偏离决策 b 原文的细化,须用户确认**(列入风险 R-1)。
|
||||||
|
|
||||||
|
### 2.4 锁粒度选型(单一 Mutex vs per-conv 细锁)
|
||||||
|
|
||||||
|
**推荐:维持单一 `Arc<Mutex<AiSession>>`**:
|
||||||
|
- per-conv 细锁(HashMap<conv_id, Mutex<PerConvState>>)理论并发更好,但:
|
||||||
|
- 死锁风险:loop 内多次取锁 + 审批 IPC 取锁,锁顺序难保证
|
||||||
|
- pending_approvals 是跨 conv 单层 HashMap,若它单锁而 per_conv 各自锁,两个锁域交叉
|
||||||
|
- 现有 41 处 lock() 全要改取锁语义,侵入大
|
||||||
|
- 单一 Mutex 下,per-conv 仅是数据隔离(写自己的 conv 不影响他人),**锁竞争点仍在**(并发 N 个 loop 抢一把锁),但:
|
||||||
|
- loop 内锁持有时间极短(push 一条消息 / 查 stop_flag 都是 O(1)),真实竞争窗口小
|
||||||
|
- stream_llm 期间**不持锁**(agentic.rs:225-230 build messages 后释放锁,stream 内无锁)
|
||||||
|
- 改动量最小,风险最低
|
||||||
|
|
||||||
|
**结论**:数据 per-conv,锁单例。后续若 profiling 显示锁竞争,再细化到 per-conv Mutex(渐进路径)。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. 并发限流(决策 c:复用 llm_concurrency.global 改会话级)
|
||||||
|
|
||||||
|
### 3.1 global Semaphore 语义重定义
|
||||||
|
|
||||||
|
**当前**(state.rs:100/221):permits=3,限**全局 LLM 调用并发**(主循环 + 标题 + 提炼 + 所有对话共用)。
|
||||||
|
|
||||||
|
**B 阶段新语义**:permits=**并发会话数上限**(默认 3),每会话 loop 入口 acquire 1 permit,持有整个 loop 生命周期(含工具执行/审批等待)。
|
||||||
|
- 含义:同一时刻最多 3 个对话并发跑 loop,第 4 个排队。
|
||||||
|
- 收益:token 暴增护栏(决策 c 原意)。
|
||||||
|
- 实现:`run_agentic_loop` 入口 `let _conv_permit = llm_concurrency.acquire_global().await;`,permit 绑 guard Drop 释放。
|
||||||
|
|
||||||
|
### 3.2 per_conv Semaphore 改 HashMap<conv_id, Semaphore>
|
||||||
|
|
||||||
|
**当前**(state.rs:101):单一 Semaphore permits=2,限单对话内并发(主循环+标题+提炼)。
|
||||||
|
|
||||||
|
**B 阶段**:
|
||||||
|
```rust
|
||||||
|
per_conv: Arc<Mutex<HashMap<String, Arc<Semaphore>>>>,
|
||||||
|
```
|
||||||
|
- 每对话内限流不变(permits=2,主循环 + 标题 + 提炼),但按 conv_id 各自一份。
|
||||||
|
- `acquire_per_conv(conv_id)`:lock HashMap → 若无则建(permits=2)→ clone Arc → 释放 lock → acquire_owned。
|
||||||
|
- **conv 退出清理**:loop 结束 + 无 pending 审批时,remove 该 conv 的 Semaphore 条目(防 HashMap 无限增长)。
|
||||||
|
|
||||||
|
### 3.3 F-260616-12 retry 持 permit 依赖(关键风险点)
|
||||||
|
|
||||||
|
**现状**(agentic.rs:247-248):
|
||||||
|
```rust
|
||||||
|
let _global_permit = llm_concurrency.acquire_global().await;
|
||||||
|
let _per_conv_permit = llm_concurrency.acquire_per_conv().await;
|
||||||
|
// 重试循环(:259-353)期间持有两 permit 不释放
|
||||||
|
// stream 后(:414-415)才 drop
|
||||||
|
```
|
||||||
|
注释(agentic.rs:246):「重试期间持有 permit 不释放(防新请求挤占)」。
|
||||||
|
|
||||||
|
**多会话后的问题**:
|
||||||
|
- global permit **新语义是会话级**(§3.1,每会话占 1 整 loop)。重试期持 global=**整个会话占着会话槽**,但重试只是该会话内部行为,**不应阻塞其他会话入槽**。
|
||||||
|
- per_conv permit 是该 conv 内部限流,重试期持有合理(防自己挤占标题/提炼)。
|
||||||
|
|
||||||
|
**B 阶段处理方向**(对齐 todo:86 「倾向重试不持 permit 或仅持 per_conv」):
|
||||||
|
- **global permit 移出重试持有**:重试循环内**释放 global**(回到会话级占槽语义——入 loop 时 acquire 1 global 整 loop 持有,重试不影响他对话)。**注意**:若 global 新语义是「会话数上限」(§3.1),重试不释放 global 也无碍(本就是会话级占槽),问题降级。**真正要改的是 per_conv 重试持有**(见下)。
|
||||||
|
- **per_conv permit 重试期释放**:重试不持 per_conv,退避 sleep + 重试请求每次重新 acquire per_conv(让该 conv 的标题/提炼有机会跑)。但当前架构无标题/提炼并发跑(都在 loop 退出后 spawn),故 per_conv 退化为单 permit,持有与否无差异。
|
||||||
|
- **结论**:F-260616-12 在 B 阶段的实际影响**仅限 global 语义切换时核验**——新 global=会话级后,重试持 global=占会话槽(语义自洽,他对话第 4 个排队正常),**非 bug,是预期行为**。per_conv 重试持有无实际影响(无并发消费方)。**F-260616-12 可降级为「B 阶段验证 global 新语义后关闭」**,无需独立改动。列入风险 R-2。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. loop 内校验改造(agentic.rs:177-211 改造点)
|
||||||
|
|
||||||
|
### 4.1 B-260615-11 陈旧 loop 退出逻辑删除
|
||||||
|
|
||||||
|
**改造前**(agentic.rs:197-211):
|
||||||
|
```rust
|
||||||
|
let mut session = session_arc.lock().await;
|
||||||
|
if session.active_conversation_id.as_deref() != Some(conv_id.as_str()) {
|
||||||
|
return; // ← 删除
|
||||||
|
}
|
||||||
|
session.iteration_used = iteration + 1;
|
||||||
|
```
|
||||||
|
|
||||||
|
**改造后**(决策 e 真并发):
|
||||||
|
```rust
|
||||||
|
{
|
||||||
|
let mut session = session_arc.lock().await;
|
||||||
|
// B 阶段:不再校验 active_conversation_id(切换不杀 loop)。
|
||||||
|
// per-conv state 按 conv_id 索引,旧 loop 写自己的 conv 不污染他人。
|
||||||
|
// 校验改为:conv 是否仍存在(被删则退出)
|
||||||
|
if !session.per_conv.contains_key(&conv_id) {
|
||||||
|
tracing::warn!("[ai] conv {} 已删除,旧 loop 退出", conv_id);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
session.conv(&conv_id).iteration_used = iteration + 1;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
同样改造 agentic.rs:380-388(MidStream 保文后 push)/427-434(stream 后 push):三处 push 前校验**全部改为 conv 存在性**而非 active 一致性。
|
||||||
|
|
||||||
|
### 4.2 stop_flag/notify/messages 取用改 per-conv 索引
|
||||||
|
|
||||||
|
**改造前**(agentic.rs:160-163):
|
||||||
|
```rust
|
||||||
|
let (stop_flag, notify) = {
|
||||||
|
let session = session_arc.lock().await;
|
||||||
|
(session.stop_flag.clone(), session.notify.clone())
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
**改造后**:
|
||||||
|
```rust
|
||||||
|
let (stop_flag, notify) = {
|
||||||
|
let session = session_arc.lock().await;
|
||||||
|
let conv = session.per_conv.get(&conv_id).expect("loop 启动前 conv 已建");
|
||||||
|
(conv.stop_flag.clone(), conv.notify.clone())
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
**build_for_request 改 per-conv messages**(agentic.rs:224-230):
|
||||||
|
```rust
|
||||||
|
let messages = {
|
||||||
|
let session = session_arc.lock().await;
|
||||||
|
let conv = session.per_conv.get(&conv_id).expect("...");
|
||||||
|
let (history_msgs, _trimmed) = conv.messages.build_for_request(sys_tokens);
|
||||||
|
// ...
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.3 GeneratingGuard 改 per-conv 复位
|
||||||
|
|
||||||
|
**改造前**(agentic.rs:59-94):guard 持 `Arc<Mutex<AiSession>>`,Drop 时 `session.lock().await.generating = false`(全局)。
|
||||||
|
|
||||||
|
**改造后**:guard 持 `conv_id: String`,`reset()` / Drop 时 `session.conv(&conv_id).generating = false`(只复位该 conv)。guard struct 加 `conv_id` 字段。
|
||||||
|
|
||||||
|
### 4.4 try_continue_agent_loop 改 per-conv 续跑
|
||||||
|
|
||||||
|
**改造前**(agentic.rs:578-650):读全局 generating + pending 决定续跑。
|
||||||
|
|
||||||
|
**改造后**:
|
||||||
|
```rust
|
||||||
|
pub(crate) async fn try_continue_agent_loop(app, state, conv_id: &str, start_iteration: usize) {
|
||||||
|
let (is_generating, has_pending) = {
|
||||||
|
let session = state.ai_session.lock().await;
|
||||||
|
let conv = match session.per_conv.get(conv_id) {
|
||||||
|
Some(c) => c,
|
||||||
|
None => return, // conv 已删
|
||||||
|
};
|
||||||
|
(conv.generating,
|
||||||
|
session.pending_approvals.values().any(|a| a.conversation_id.as_deref() == Some(conv_id)))
|
||||||
|
};
|
||||||
|
// ... 续跑 spawn run_agentic_loop 传 conv_id
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
`ai_approve`(commands.rs:228-)调用处传 `approval.conversation_id`(:281 已有)。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. 切换不退出旧 loop 各自跑完(决策 e 改造点)
|
||||||
|
|
||||||
|
### 5.1 ai_conversation_switch 移除 readonly 分支
|
||||||
|
|
||||||
|
**改造前**(commands.rs:935-941):生成中 readonly 切换,不改 active。
|
||||||
|
|
||||||
|
**改造后**:
|
||||||
|
```rust
|
||||||
|
let mut session = state.ai_session.lock().await;
|
||||||
|
session.active_conversation_id = Some(conversation_id.clone()); // 直接切,不动 per-conv
|
||||||
|
// 旧 conv 的 per-conv state 保留(后台 loop 继续跑),新 conv 从 DB reload messages 到其 per-conv
|
||||||
|
let conv_state = session.per_conv.entry(conversation_id.clone())
|
||||||
|
.or_insert_with(|| PerConvState::from_messages(messages)); // 惰性建/已存在则保留
|
||||||
|
session.pending_approvals.retain(|_, a| a.conversation_id.as_deref() != Some(&conversation_id));
|
||||||
|
// 删 readonly 分支
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5.2 ai_conversation_create 移除强制结束旧 loop
|
||||||
|
|
||||||
|
**改造前**(commands.rs:848-864):生成中 `generating=false` + `stop_flag=true` 杀旧 loop。
|
||||||
|
|
||||||
|
**改造后**:
|
||||||
|
```rust
|
||||||
|
let mut session = state.ai_session.lock().await;
|
||||||
|
// B 阶段:新建会话不杀旧 loop(决策 e)。新会话建独立 per-conv state。
|
||||||
|
let id = new_id();
|
||||||
|
session.active_conversation_id = Some(id.clone());
|
||||||
|
session.per_conv.insert(id.clone(), PerConvState::new()); // 全新 state
|
||||||
|
session.pending_approvals.retain(|_, a| a.conversation_id.as_deref() != Some(&id));
|
||||||
|
// 删 if session.generating { 强制结束 } 块
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5.3 ai_chat_send 互斥改 per-conv
|
||||||
|
|
||||||
|
**改造前**(commands.rs:144-152):`if session.generating {Err}` 全局互斥。
|
||||||
|
|
||||||
|
**改造后**:
|
||||||
|
```rust
|
||||||
|
let mut session = state.ai_session.lock().await;
|
||||||
|
let conv_state = session.conv(&conv_id); // 惰性建
|
||||||
|
if conv_state.generating {
|
||||||
|
return Err("该对话正在生成中".to_string()); // 仅该 conv 互斥,他对话不受影响
|
||||||
|
}
|
||||||
|
conv_state.generating = true;
|
||||||
|
conv_state.stop_flag.store(false, Ordering::SeqCst);
|
||||||
|
conv_state.agent_language = language.clone();
|
||||||
|
conv_state.iteration_used = 0;
|
||||||
|
conv_state.messages.push(ChatMessage::user(&user_content));
|
||||||
|
// active_conversation_id 仍设(路由键),但不影响其他 conv 的 loop
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5.4 ai_chat_force_send / ai_chat_stop / ai_continue_loop / ai_stop_loop 改 per-conv
|
||||||
|
|
||||||
|
所有这些命令**加 conv_id 参数**(前端传 activeConversationId),操作仅针对该 conv 的 per-conv state:
|
||||||
|
- `ai_is_generating(conv_id)` → `conv_read(conv_id).generating`
|
||||||
|
- `ai_chat_stop(conv_id)` → 置该 conv 的 stop_flag + notify
|
||||||
|
- `ai_chat_force_send(conv_id, ...)` → 复位该 conv 的 generating + 重发
|
||||||
|
- `ai_continue_loop(conv_id)` / `ai_stop_loop(conv_id)` → 操作该 conv
|
||||||
|
|
||||||
|
**IPC 签名变更**:需更新 commands.rs 的 `#[tauri::command]` 签名 + lib.rs:120 invoke_handler + 前端 api/ai.ts wrapper。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. 事件路由 conversation_id 全覆盖核验
|
||||||
|
|
||||||
|
> 后端 emit 点全部 grep 核验。前端 useAiEvents.ts:125 已按 conversation_id 路由。
|
||||||
|
|
||||||
|
**AiChatEvent 变体**(mod.rs:88-141)conversation_id 字段核验:
|
||||||
|
|
||||||
|
| 变体 | 行 | conversation_id | 核验 |
|
||||||
|
|---|---|---|---|
|
||||||
|
| AiTextDelta | :90 | `Some(conv_id)` | ✅ stream_recv 传 |
|
||||||
|
| AiToolCallStarted | :93 | `Some` | ✅ |
|
||||||
|
| AiToolCallCompleted | :96 | `Some` | ✅ |
|
||||||
|
| AiApprovalRequired | :101 | `Some` | ✅ audit.rs:604 process_tool_calls 传 |
|
||||||
|
| AiApprovalResult | :103 | `Some` | ✅ commands.rs:260/312 |
|
||||||
|
| AiCompleted | :115 | `Some` | ✅ agentic.rs 多处 + commands.rs:517/544 |
|
||||||
|
| AiError | :124 | `Some` | ✅ agentic.rs:143 |
|
||||||
|
| AiAgentRound | :127 | `Some` | ✅ agentic.rs:217 |
|
||||||
|
| AiHeartbeat | :129 | `Some` | ✅ stream_recv |
|
||||||
|
| AiMaxRoundsReached | :135 | `Some` | ✅ agentic.rs:519 |
|
||||||
|
| AiStreamRetry | :140 | `Some` | ✅ agentic.rs:346 |
|
||||||
|
|
||||||
|
**结论**:所有 emit 点**已带 conversation_id**,前端 useAiEvents.ts:125 `convId = event.conversation_id` + :133 `isCurrent = !convId || convId === state.activeConversationId` 已就绪。**事件路由层零改动**,这是 B 阶段 d1 落地的前置保障。
|
||||||
|
|
||||||
|
**唯一新增 emit 点核验**:loop 内 push 失败/per-conv 复位的新 emit 须带 conv_id(§4 改造时逐一核对)。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. 多窗口 UI(d1 侧栏 + d2 独立窗口)
|
||||||
|
|
||||||
|
### 7.1 d1 侧栏切换 + 后台并行(先做)
|
||||||
|
|
||||||
|
**现状基础**:
|
||||||
|
- useAiConversations.ts:56「允许生成中切换:后台继续生成,事件按 conversation_id 路由不污染当前视图」已就绪
|
||||||
|
- useAiEvents.ts:142-144 `generatingConvId` 跟踪当前生成 conv(切走不清零)
|
||||||
|
- 侧栏会话列表 useAiConversations.ts:17 loadConversations 已就绪
|
||||||
|
|
||||||
|
**d1 改动点**:
|
||||||
|
1. **侧栏会话项显示生成态**:读 `state.conversations[].id` 与 `state.generatingConvId` 比对,匹配则显流式指示器(改动:`Sidebar.vue` 会话项 + 新 computed `generatingConvSet`)
|
||||||
|
2. **多 conv 后台并行态**:state 加 `generatingConvs: Set<string>`(替代单值 generatingConvId),useAiEvents.ts:142-144 改 push 到 Set,Completed/Error 时 remove
|
||||||
|
3. **切走不清零**:useAiConversations.ts:38-44 newConversation 清 queue/generatingConvId 改为只清当前 conv 的(不杀他 conv)
|
||||||
|
4. **AiCompleted 触发 drainQueue**:useAiSend.ts:227 当前队列是单队列,B 阶段可保持(用户在 A 排队,A 完成续发),或改 per-conv 队列(后续优化)
|
||||||
|
|
||||||
|
**d1 不需要**:多窗口、状态同步、localStorage 跨窗口。**d1 是纯前端 + 后端 §2-§5 改造的最小 UI 呈现层**。
|
||||||
|
|
||||||
|
### 7.2 d2 独立 Tauri 窗口(后做)
|
||||||
|
|
||||||
|
**现状基础**(useAiWindow.ts):
|
||||||
|
- `WebviewWindow('ai-detached', ...)`(:42)单窗口 label
|
||||||
|
- detach 时快照 generatingConvId → localStorage `df-ai-gen`(:28-31)
|
||||||
|
- resumeInDetached(:87-119)核验 `ai_is_generating` 后恢复生成态
|
||||||
|
- 独立 webview 独立 store(useAiWindow.ts:32-34 注释)
|
||||||
|
|
||||||
|
**d2 改动点**:
|
||||||
|
1. **label 改 per-conv**:`ai-detached-${convId}`,支持每会话独立窗口(`getByLabel` 查重)
|
||||||
|
2. **detachPanel 加 conv_id 参数**:从指定 conv detach(而非当前 active)
|
||||||
|
3. **窗口间状态同步**:多窗口各自 webview 各自 store,需:
|
||||||
|
- 后端事件广播:Tauri `emit` 默认所有 webview 收(useAiEvents 在每窗口各自 listen,按 conversation_id 过滤)——**天然支持**,无需改
|
||||||
|
- 显式状态同步(如某窗口改 provider):用 Tauri `emit` 自定义事件 + 各窗口 listen(useAiWindow.ts 已有 _unlistenMove/Resize 模式可仿)
|
||||||
|
4. **localStorage 快照**:从单 `df-ai-gen` 改 `df-ai-gen-${convId}` 多 key
|
||||||
|
5. **窗口管理 UI**:侧栏每会话加「在新窗口打开」按钮(右键菜单或图标)
|
||||||
|
|
||||||
|
**d2 风险**:多窗口 + 多 webview 内存占用;窗口生命周期管理(关窗是否停 loop,决策 e 下不停)。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. 实施步骤(分批,每批独立可验证)
|
||||||
|
|
||||||
|
> 每批标注**主改文件**+**锁/回归面**。建议批间 cargo + vue-tsc 自验 + 人工验收单会话回归。
|
||||||
|
|
||||||
|
### 批 1:PerConvState 数据结构 + 访问器(无行为变更,纯重构)
|
||||||
|
- **主改**:`mod.rs`(新增 PerConvState struct + AiSession 字段迁移 + conv()/conv_read() 访问器)
|
||||||
|
- **锁**:无运行时行为变化(旧字段读写在批 2 迁移),仅编译期
|
||||||
|
- **验证**:cargo build 通过 + 单测(访问器惰性建/已存在/conv 删除)
|
||||||
|
- **回归面**:全 AI IPC 编译(41 处 lock 暂不动,通过兼容层 `active_conv()` 转发到 per_conv[active_conversation_id])
|
||||||
|
|
||||||
|
### 批 2:41 处 lock 调用点迁移到 conv() 索引
|
||||||
|
- **主改**:`commands.rs`(41 处)、`agentic.rs`(8 处)、`prompt.rs`/`audit.rs`/`knowledge_inject.rs`(各 1 处)
|
||||||
|
- **锁**:仍单 Mutex,索引多一层
|
||||||
|
- **验证**:cargo build + **单会话全功能回归**(send/regenerate/edit/approve/stop/continue/switch/create/delete)行为零变化
|
||||||
|
- **回归面**:全部 AI 功能(高风险批,须逐命令人工验收)
|
||||||
|
|
||||||
|
### 批 3:run_agentic_loop per-conv 改造(§4)
|
||||||
|
- **主改**:`agentic.rs`(GeneratingGuard 加 conv_id + stop_flag/notify/messages 取用改 per-conv + §4.1 陈旧 loop 退出改 conv 存在性 + §4.4 try_continue 改 per-conv)
|
||||||
|
- **锁**:loop 内不改锁,改取用索引
|
||||||
|
- **验证**:单会话回归 + **双会话并发验收**(开 conv A 跑 → 切 conv B 发 → A 后台跑完不污染 B)
|
||||||
|
- **回归面**:loop 全路径(send/regenerate/approve/continue/stop/max)
|
||||||
|
- **依赖**:批 2 完成
|
||||||
|
|
||||||
|
### 批 4:commands.rs 切换/新建/停止改 per-conv(§5)
|
||||||
|
- **主改**:`commands.rs`(ai_conversation_create 删强制结束块 / ai_conversation_switch 删 readonly / ai_chat_send 互斥改 per-conv / ai_chat_stop/force/continue/stop_loop 加 conv_id 参数)
|
||||||
|
- **IPC 签名变更**:lib.rs invoke_handler + 前端 api/ai.ts wrapper + useAiSend.ts/useAiConversations.ts 调用处
|
||||||
|
- **验证**:双会话并发(切走不杀旧 loop,各自跑完)+ 单会话回归
|
||||||
|
- **回归面**:全部 IPC 签名(前端多处调用)
|
||||||
|
- **依赖**:批 3 完成
|
||||||
|
|
||||||
|
### 批 5:llm_concurrency 改造 + F-260616-12 处理(§3)
|
||||||
|
- **主改**:`state.rs`(LlmConcurrency global 语义=会话级 + per_conv 改 HashMap)、`agentic.rs`(loop 入口 acquire global 整 loop 持有 + 重试期 global 释放语义核验)
|
||||||
|
- **锁**:Semaphore 改造
|
||||||
|
- **验证**:3 会话并发(第 4 排队)+ 重试期不阻塞他对话(F-260616-12 验证)
|
||||||
|
- **回归面**:并发限流配置(Settings 热改路径 set_global/set_per_conv 适配 HashMap)
|
||||||
|
- **依赖**:批 4 完成
|
||||||
|
|
||||||
|
### 批 6:d1 前端侧栏 + 后台并行(§7.1)
|
||||||
|
- **主改**:`stores/ai.ts`(generatingConvs: Set)、`useAiEvents.ts`(generatingConvId → generatingConvs)、`Sidebar.vue`(会话项生成态指示器)、`useAiConversations.ts`(切走不清零)
|
||||||
|
- **验证**:双会话并发 UI(侧栏见两 conv 流式指示器,切走后台继续)
|
||||||
|
- **回归面**:侧栏渲染 + 事件路由
|
||||||
|
- **依赖**:批 4 完成(后端 per-conv 就绪)
|
||||||
|
|
||||||
|
### 批 7:d2 独立 Tauri 窗口(§7.2)
|
||||||
|
- **主改**:`useAiWindow.ts`(label per-conv + detachPanel 加 conv_id + localStorage 多 key + 窗口间同步)、`Sidebar.vue`(新窗口打开按钮)
|
||||||
|
- **验证**:每会话独立窗口 + 多窗口并发 + 窗口间状态同步
|
||||||
|
- **回归面**:窗口生命周期 + 内存
|
||||||
|
- **依赖**:批 6 完成
|
||||||
|
|
||||||
|
### 批 8:启动恢复 + L0 握手适配
|
||||||
|
- **主改**:`audit.rs`(restore_pending_approvals 按 conversation_id 分组重建到对应 per_conv)、`lib.rs`(L0 握手清所有 per_conv 的 generating,非全局单值)
|
||||||
|
- **验证**:重启恢复多 conv pending 审批 + HMR 清多 conv 残留
|
||||||
|
- **回归面**:启动恢复链路
|
||||||
|
- **依赖**:批 2 完成(可与批 3-7 并行)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. 风险清单
|
||||||
|
|
||||||
|
### R-1:决策 b 细化偏离(messages 必须同 per-conv)— **须用户确认**
|
||||||
|
- **原决策 b**:「仅拆 generating/stop_flag per-conv,messages/pending 已可按 conv reload」
|
||||||
|
- **核验结论**:**messages 必须同 per-conv**,否则 §1.3 B-260615-11 陈旧 loop 退出逻辑无替代判据(单例 messages 下旧 loop 必污染新 active conv),决策 e「不退出各自跑完」落空。
|
||||||
|
- **侵入面增量**:极小(messages 本就是 ContextManager 自包含,挪 HashMap),但偏离原文须确认。
|
||||||
|
- **缓解**:pending_approvals 保持单层 HashMap(已带 conversation_id,符合决策 b 原意),仅 messages per-conv 化。
|
||||||
|
|
||||||
|
### R-2:锁粒度——单 Mutex 多 conv 抢锁
|
||||||
|
- **现状**:41 处 `session.lock()` 共一把 Mutex。
|
||||||
|
- **B 阶段**:per-conv 仅数据隔离(HashMap 索引),锁仍单例。N 个并发 loop 抢一把锁。
|
||||||
|
- **评估**:loop 内锁持有时间极短(push/查 flag 均 O(1)),stream 期间不持锁,真实竞争窗口小。**风险可控**。
|
||||||
|
- **缓解**:若 profiling 显示竞争,再细化 per-conv Mutex(渐进,不阻塞 B 落地)。
|
||||||
|
|
||||||
|
### R-3:事件路由——已就绪但新增 emit 须核对
|
||||||
|
- **现状**:11 个 AiChatEvent 变体全部已带 conversation_id(useAiEvents.ts:125 已路由)。
|
||||||
|
- **B 阶段**:零改动,但 §4-§5 改造新增 emit 点须逐一核对带 conv_id。
|
||||||
|
- **缓解**:批 3/4 改造时 grep emit 点 checklist。
|
||||||
|
|
||||||
|
### R-4:状态同步——前端 generatingConvId 单值 → Set
|
||||||
|
- **现状**:state.generatingConvId 单值(stores/ai.ts:51)。
|
||||||
|
- **B 阶段 d1**:改 Set(多 conv 并行生成)。useAiEvents.ts:142-144 写入逻辑 + 各清零点(Completed/Error/:136/:273/:317)全改。
|
||||||
|
- **风险**:漏改清零点致生成态残留。
|
||||||
|
- **缓解**:批 6 集中改 + 双会话并发 UI 验收。
|
||||||
|
|
||||||
|
### R-5:回归面——全 AI 功能(高风险批在批 2/批 4)
|
||||||
|
- **批 2**(41 处 lock 迁移):全 AI IPC 编译 + 单会话全功能回归。
|
||||||
|
- **批 4**(IPC 签名变更 conv_id 参数):前端多处调用 + 后端全部命令。
|
||||||
|
- **缓解**:批间 cargo + vue-tsc + 人工验收单会话回归(逐命令:send/regenerate/edit/approve/stop/continue/switch/create/delete/rename/archive)。批 2 是最高风险批,建议单独 commit + 充分验收。
|
||||||
|
|
||||||
|
### R-6:F-260616-12 retry 持 permit——global 语义切换后核验
|
||||||
|
- **现状**:重试期持 global+per_conv permit(agentic.rs:247-248)。
|
||||||
|
- **B 阶段**:global 改「会话级」语义(§3.1)后,重试持 global=占会话槽(语义自洽,第 4 会话排队正常),**非 bug**。per_conv 重试持有无实际影响(无并发消费方)。
|
||||||
|
- **结论**:F-260616-12 **降级为批 5 验证项**(global 新语义下行为符合预期即关闭),无需独立改动。**若用户认为重试期占会话槽不合理**,则在批 5 重试循环内释放 global(每次重试重新 acquire),但会引入「重试期会话槽空转被他对话抢占」新问题——不推荐。
|
||||||
|
|
||||||
|
### R-7:启动恢复 + L0 握手多 conv 适配
|
||||||
|
- **restore_pending_approvals**(audit.rs:254):当前重建到全局 pending_approvals(单层 HashMap),B 阶段保持(已带 conversation_id),但须确保各 conv 的 per_conv state 在启动时建(惰性 or 全量)。
|
||||||
|
- **L0 握手**(lib.rs:42-63):当前清全局 generating + pending。B 阶段改清所有 per_conv 的 generating(防 HMR 后多 conv 残留)。
|
||||||
|
- **缓解**:批 8 集中处理。
|
||||||
|
|
||||||
|
### R-8:iteration_used 跨审批续跑在多会话下的正确性
|
||||||
|
- **现状**(F-260616-11):iteration_used 累计,ai_approve 读 session.iteration_used 续跑。
|
||||||
|
- **B 阶段**:iteration_used 挪入 PerConvState(§2.1),ai_approve 读 `conv(approval.conversation_id).iteration_used`。
|
||||||
|
- **风险**:ai_approve commands.rs:272/325 两处读须改 per-conv 索引(批 2 一并迁移)。
|
||||||
|
- **缓解**:批 2 checklist 含此两处。
|
||||||
|
|
||||||
|
### R-9:PendingApproval.conversation_id 一致性
|
||||||
|
- **现状**:process_tool_calls(audit.rs:598)写 `Some(conv_id.to_string())`,restore_pending_approvals(audit.rs:276)从审计表读 `rec.conversation_id`。
|
||||||
|
- **B 阶段**:保持单层 pending_approvals + 按 conversation_id 过滤(ai_pending_tool_calls commands.rs:347 已 O(n) 过滤)。
|
||||||
|
- **风险**:若某审批的 conversation_id 为 None(历史数据/边界),retain/filter 漏判。
|
||||||
|
- **缓解**:批 2 核验所有 insert 点均带 Some(conv_id);None 视为「无主审批」归类到 active conv 或单独清理。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 10. 与已决架构债的关系
|
||||||
|
|
||||||
|
- **memory `aichat-arch-extensibility`**:「AiSession 单例未动」是登记的架构债,本设计正式清偿。
|
||||||
|
- **mod.rs:214 注释**:「当前 AiSession 是全局单例(F-09 B 多会话架构落地时改 per-conv)」——预留债标记,本设计落地。
|
||||||
|
- **state.rs:94-97 注释**:per_conv「未来若支持多对话并发,需改为 HashMap<conv_id, Semaphore>」——本设计 §3.2 落地。
|
||||||
|
- **不冲突**:本设计与 F-260616-11(iteration 累计,已 per-conv 化预留)、F-260616-07(流式重试,F-260616-12 依赖项 §3.3 处理)、generating 状态机加固(RAII guard,§4.3 改 per-conv)均兼容。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 11. 验收清单(每批完成后)
|
||||||
|
|
||||||
|
- [ ] 批 1:cargo build + 访问器单测
|
||||||
|
- [ ] 批 2:cargo + vue-tsc 0err + 单会话全功能回归(send/regenerate/edit/approve/stop/continue/switch/create/delete/rename/archive 逐命令)
|
||||||
|
- [ ] 批 3:双会话并发(开 A 跑 → 切 B 发 → A 后台跑完不污染 B)+ 单会话回归
|
||||||
|
- [ ] 批 4:双会话并发(切走不杀旧 loop,各自跑完)+ 全 IPC 签名前端调用核对
|
||||||
|
- [ ] 批 5:3 会话并发(第 4 排队)+ 重试期不阻塞他对话(F-260616-12)
|
||||||
|
- [ ] 批 6:双会话并发 UI(侧栏两 conv 流式指示器,切走后台继续)
|
||||||
|
- [ ] 批 7:每会话独立窗口 + 多窗口并发 + 窗口间状态同步
|
||||||
|
- [ ] 批 8:重启恢复多 conv pending 审批 + HMR 清多 conv 残留
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**设计完。本文件为设计稿,不含代码改动。实施时按批 1→8 顺序,每批独立可验证。**
|
||||||
26
docs/todo.md
26
docs/todo.md
@@ -85,6 +85,14 @@
|
|||||||
|
|
||||||
- [ ] F-260616-12 [P2/依赖F-09] — **retry 持 permit 不释放(多会话隐患)**。F-260616-07 落地的流式重试循环(`agentic.rs:238-239`)重试期间持有 global+per_conv permit 不释放(注释「防新请求挤占」)。当前 AiSession 单例 + 主 loop 串行无影响,但 **F-260616-09 多会话并发后**,重试期间阻塞其他对话 LLM 调用。**方向**:多会话落地时核对——主 loop 串行下重试持 permit 防自己挤占无意义,倾向重试不持 permit 或仅持 per_conv。— agentic.rs:238 + F-260616-09 多会话架构。**依赖 F-260616-09 立项后一并处理**。
|
- [ ] F-260616-12 [P2/依赖F-09] — **retry 持 permit 不释放(多会话隐患)**。F-260616-07 落地的流式重试循环(`agentic.rs:238-239`)重试期间持有 global+per_conv permit 不释放(注释「防新请求挤占」)。当前 AiSession 单例 + 主 loop 串行无影响,但 **F-260616-09 多会话并发后**,重试期间阻塞其他对话 LLM 调用。**方向**:多会话落地时核对——主 loop 串行下重试持 permit 防自己挤占无意义,倾向重试不持 permit 或仅持 per_conv。— agentic.rs:238 + F-260616-09 多会话架构。**依赖 F-260616-09 立项后一并处理**。
|
||||||
|
|
||||||
|
### 💡 2026-06-16 新需求(UX 交互优化·分析完成·待实施)
|
||||||
|
|
||||||
|
> 用户实时反馈的交互优化需求,已走查定位链路+方案记录。
|
||||||
|
|
||||||
|
- [ ] UX-260616-01 [P2] — **工具调用失败时「重试」提示语义模糊**。用户场景:Run Command 执行失败(exit_code 255,`head` not recognized on Windows),UI 显示「⚠ 调用失败,正在重试(1/4)…」+「重试」按钮。**根因分析**:「正在重试(n/m)」是 `AiStreamRetry` 事件(F-260616-07 流式 LLM 重试),通过 `useAiEvents.ts:177-186` 更新错误气泡内容;工具执行失败(run_command 等 Low 风险工具)走 AR-6 路径(`audit.rs:640-652`)→ emit `AiToolCallCompleted`(result=错误信息)→ **不触发 AiError 不触发 AiStreamRetry**,错误包在 tool_result 回传 LLM 自行决策。**问题**:①用户看到的是「LLM 流重试」提示,非「工具执行失败」提示,语义错位 ②确定性失败(命令语法错/exit_code 非0)重试同命令必再败,「重试」按钮误导 ③工具实际结果(stdout/stderr/exit_code)已显示在 ToolCard 内,错误气泡的「重试」是消息级 regenerate(重新生成整条回复),非工具级重试。**改动方向**(待定):a)错误气泡区分两类——流式重试中显示「正在重试(n/m)…」(现有);工具执行失败不弹错误气泡(结果已在 ToolCard)或气泡文案改为「工具执行失败,查看上方结果」b)Fatal 类错误(4xx/鉴权/参数)气泡去掉「重试」按钮或改为「去设置」c)`AiStreamRetry` 事件更新气泡时附带 `retryable` 标识,前端据此显隐重试按钮。—— useAiEvents.ts(:177-186 AiStreamRetry case) + AiChat.vue(错误气泡 UX-03 操作栏) + i18n ai.aiStreamRetry
|
||||||
|
- [ ] UX-260616-02 [P3] — **「全部收起」与搜索/技能区合并一行**。当前 ToolCardList.vue 有两个独立行:①batch-approve 栏(line 4-11,pendingCount>0 时显示)②global-toggle 栏(line 14-17,collapsibleGroupCount>0 时显示「▾ 全部收起」)。用户要求将「全部收起」与附近的操作元素(search files 搜索文件/技能触发等)放到同一行,减少垂直空间占用。**需确认**:「search files」具体指哪个 UI 元素(i18n `ai.searchFiles` 渲染位置需定位,可能在输入框上方 skill 栏或工具卡区域)。**改动方向**:global-toggle 从独占行改为 inline 元素,与相邻操作栏 flex 同行。—— ToolCardList.vue(:13-17 template + :278-295 CSS .ai-tool-global-toggle)
|
||||||
|
- [ ] UX-260616-03 [P2] — **对话内容输出时自动收起旧工具卡片分组**。需求:当 AI 输出新内容(流式 delta / 新工具调用 / 新轮次)滚动到下方时,上方已完成的旧消息中的工具卡片分组自动收起,保持视野聚焦当前内容。**现状**:机制已存在——`ToolCardList.collapseInactive(activeIds)`(:191-199)供父组件调用,`AiChat.vue:1657-1661` 已有 watch 调用(refs 数组逐实例 collapseInactive)。**增强方向**:a)触发时机扩展——当前可能仅在特定时机调用,可扩展到:`AiTextDelta` 新消息开始时 + `AiAgentRound` 新轮次时 + 用户滚动接近底部时(跟随阅读位置自动收起已读内容)b)平滑过渡——收起加 CSS transition(高度动画 200ms)避免内容突然消失跳变c)可选:记忆用户手动展开的分组不自动收起(expandedCards Set 区分用户主动展开 vs 默认态)。—— ToolCardList.vue(collapseInactive + CSS transition) + AiChat.vue(watch 触发时机扩展)
|
||||||
|
|
||||||
- [x] ✅(batch60·2026-06-16·workflow whae812z5+主代核查,cargo 0err) F-260616-13 [P2] — **build_for_request 持锁重活 + system_prompt token 每轮重估**。性能分析批次发现:每轮 `stream_llm` 前 `session_arc.lock()`(`agentic.rs:251`)持锁期间做 `TokenEstimator::estimate_text(system_prompt)`(L252)+ `build_for_request`(L253 history clone + 裁剪)。system_prompt loop 外固定传入,**每轮重估其 token 是浪费**(可缓存);build_for_request 持锁做 history clone 是重活,消息多时(200 cap)锁持有期长。**方向**:①system_prompt token loop 外算一次缓存 ②build_for_request 先 clone messages 释放锁再裁剪。低收益优化。— agentic.rs:251-253。
|
- [x] ✅(batch60·2026-06-16·workflow whae812z5+主代核查,cargo 0err) F-260616-13 [P2] — **build_for_request 持锁重活 + system_prompt token 每轮重估**。性能分析批次发现:每轮 `stream_llm` 前 `session_arc.lock()`(`agentic.rs:251`)持锁期间做 `TokenEstimator::estimate_text(system_prompt)`(L252)+ `build_for_request`(L253 history clone + 裁剪)。system_prompt loop 外固定传入,**每轮重估其 token 是浪费**(可缓存);build_for_request 持锁做 history clone 是重活,消息多时(200 cap)锁持有期长。**方向**:①system_prompt token loop 外算一次缓存 ②build_for_request 先 clone messages 释放锁再裁剪。低收益优化。— agentic.rs:251-253。
|
||||||
|
|
||||||
- [x] ✅(主代核验·2026-06-16) F-260616-14 [P2/核验] — **max_tokens 8192 截断 + 前端批量审批核验**。核验结论:①`max_tokens=8192`(实际 `agentic.rs:270`,行号漂移)≈6-8k 字日常够用,长回复截断由 batch59 MidStream 保文兜底不丢文,**可配低优先非阻塞**(登记可选优化)②前端批量审批 **AE-2025-01 已完整闭环**(ToolCardList.vue:4-11 `ai-batch-approve` 栏 + AiChat.vue:444 `@batch-approve`→`store.batchApprove` + useAiSend.ts:338 遍历 pendingApprovals 逐个 approveToolCall + i18n approveAll/rejectAll),满足「一轮多 Med/High 全 insert pending→一次批量批全部」③audit.rs:496 `process_tool_calls` 一轮多工具 Med/High 进审批门控 insert pending + Low 并行执行链路通。无阻塞 bug,销账。— agentic.rs:270 + ToolCardList.vue + audit.rs:496。
|
- [x] ✅(主代核验·2026-06-16) F-260616-14 [P2/核验] — **max_tokens 8192 截断 + 前端批量审批核验**。核验结论:①`max_tokens=8192`(实际 `agentic.rs:270`,行号漂移)≈6-8k 字日常够用,长回复截断由 batch59 MidStream 保文兜底不丢文,**可配低优先非阻塞**(登记可选优化)②前端批量审批 **AE-2025-01 已完整闭环**(ToolCardList.vue:4-11 `ai-batch-approve` 栏 + AiChat.vue:444 `@batch-approve`→`store.batchApprove` + useAiSend.ts:338 遍历 pendingApprovals 逐个 approveToolCall + i18n approveAll/rejectAll),满足「一轮多 Med/High 全 insert pending→一次批量批全部」③audit.rs:496 `process_tool_calls` 一轮多工具 Med/High 进审批门控 insert pending + Low 并行执行链路通。无阻塞 bug,销账。— agentic.rs:270 + ToolCardList.vue + audit.rs:496。
|
||||||
@@ -128,7 +136,13 @@
|
|||||||
- **实施顺序**:④-1→②-1→②-5→②-2→②-3→②-4→②-6→①-1/①-3。**最小里程碑**(④-1+②-1+②-2+②-3):run_workflow 单 task_advance 节点 DAG 端到端推进 todo→in_progress
|
- **实施顺序**:④-1→②-1→②-5→②-2→②-3→②-4→②-6→①-1/①-3。**最小里程碑**(④-1+②-1+②-2+②-3):run_workflow 单 task_advance 节点 DAG 端到端推进 todo→in_progress
|
||||||
- [ ] F-260616-07 阶段3 AI 执行闭环 — **F-03 收口三件 ✅ 本批完成**(batch64), AiNode 自审闸门 ⏳ 待后续批
|
- [ ] F-260616-07 阶段3 AI 执行闭环 — **F-03 收口三件 ✅ 本批完成**(batch64), AiNode 自审闸门 ⏳ 待后续批
|
||||||
- [x] ✅(batch64·2026-06-16·workflow wii1u1lnm) **F-03 收口三件** — ①advance_task 注册 AI 工具(tool_registry.rs:395,handler L407 调 `df_nodes::task_advance_node::advance_task_atomic` 与 IPC `commands::task::advance_task:165` 同源) ②run_workflow 注册 AI 工具(:428,handler 架构约束无 AppHandle/State 报错引导走 IPC,ToolDefinition+审批文案 L1260-1261/1286-1287 注册让 LLM 可产出 tool_call) ③update_task handler guard 拒 status(L374 `field=="status"` bail,schema 通用 field/value 故 guard 拦非 schema 改)+ df-storage tasks 白名单移 status+review_rounds(crud.rs:331-347,advance_status_atomic CAS L848 独立路径不经白名单)。同步落地防 AI 工具行为不一致。**主代独立核查全过**:cargo check --workspace EXIT 0(5 pre-existing warnings 无关)+ df-storage 11 集成测试(含新 `update_field_rejects_tasks_status`)。文件锁:tool_registry.rs + crud.rs + project_soft_delete.rs。**审查登记 CR-260616-41**(待审查.md 当前队列)
|
- [x] ✅(batch64·2026-06-16·workflow wii1u1lnm) **F-03 收口三件** — ①advance_task 注册 AI 工具(tool_registry.rs:395,handler L407 调 `df_nodes::task_advance_node::advance_task_atomic` 与 IPC `commands::task::advance_task:165` 同源) ②run_workflow 注册 AI 工具(:428,handler 架构约束无 AppHandle/State 报错引导走 IPC,ToolDefinition+审批文案 L1260-1261/1286-1287 注册让 LLM 可产出 tool_call) ③update_task handler guard 拒 status(L374 `field=="status"` bail,schema 通用 field/value 故 guard 拦非 schema 改)+ df-storage tasks 白名单移 status+review_rounds(crud.rs:331-347,advance_status_atomic CAS L848 独立路径不经白名单)。同步落地防 AI 工具行为不一致。**主代独立核查全过**:cargo check --workspace EXIT 0(5 pre-existing warnings 无关)+ df-storage 11 集成测试(含新 `update_field_rejects_tasks_status`)。文件锁:tool_registry.rs + crud.rs + project_soft_delete.rs。**审查登记 CR-260616-41**(待审查.md 当前队列)
|
||||||
- [ ] AiNode 自审闸门(阶段3 核心,需设计) — 待后续批
|
- [ ] AiNode 自审闸门(阶段3 核心,✅ 决策a 已定 2026-06-16:TaskRecord 加 output_json) — 待后续批
|
||||||
|
- [ ] df-storage: TaskRecord 加 `output_json: Option<String>` 字段 + V18 迁移(ALTER TABLE tasks ADD COLUMN output_json TEXT) + Repo update 方法
|
||||||
|
- [ ] df-nodes: ai_node.rs ai_execute 执行后写产出到 task.output_json(通过 NodeContext 拿 task_id + DB 句柄)
|
||||||
|
- [ ] df-nodes: ai_node.rs ai_self_review 读 task.output_json 构建自审 prompt(需求符合度+产出完整性 → 通过/不通过+理由+建议) + 自审结果写回 output_json
|
||||||
|
- [ ] df-workflow: run_workflow 注入 task 数据到 NodeContext(config 注入 task_id + provider 配置,④-1 deep_merge 已就绪)
|
||||||
|
- [ ] df-nodes: human_node.rs human_review 读 task.output_json 展示产出+自审结果给人核
|
||||||
|
- [ ] 前端: TaskDetail.vue 展示 output_json(产出+自审结果)
|
||||||
- [ ] run_workflow handler 注入 AppState(当前报错引导走 IPC) — 后续批扩展 build_ai_tool_registry 签名注入 AppState 句柄让 AI 直驱
|
- [ ] run_workflow handler 注入 AppState(当前报错引导走 IPC) — 后续批扩展 build_ai_tool_registry 签名注入 AppState 句柄让 AI 直驱
|
||||||
- [x] ⏸️(待决策.md已决b暂缓·2026-06-16) F-260616-08 阶段4 Git 集成(kind+git闸门+worktree)
|
- [x] ⏸️(待决策.md已决b暂缓·2026-06-16) F-260616-08 阶段4 Git 集成(kind+git闸门+worktree)
|
||||||
- [x] ✅ **CR-260616-01 代码审查完成** → 审查登记已迁 [待审查.md](./待审查.md)(职责分离:审查队列独立,不进 todo)。结论 🔴0 🟡6 ⚪4 质量优,8 维度全过。**待修项 CR-01-A~I 见下方推进区**。
|
- [x] ✅ **CR-260616-01 代码审查完成** → 审查登记已迁 [待审查.md](./待审查.md)(职责分离:审查队列独立,不进 todo)。结论 🔴0 🟡6 ⚪4 质量优,8 维度全过。**待修项 CR-01-A~I 见下方推进区**。
|
||||||
@@ -410,7 +424,13 @@
|
|||||||
|
|
||||||
### P1 — 设计完成待实施
|
### P1 — 设计完成待实施
|
||||||
|
|
||||||
- [ ] F-260614-01 — **[P1]** 模型能力系统 Phase 1 — ModelCapability 数据模型 + ModelRouter 重写 + 7 调用点接入 + Settings 模型池编辑 UI + AiChat 模型下拉。按任务需求(模态/功能/成本)自动匹配合适模型,不再所有场景共用 default_model — source:📐 设计完成 (06-14)
|
- [ ] F-260614-01 — **[P1🔥]** 模型能力系统 Phase 1 — ModelConfig 数据模型(模态/能力/价格/智力 4 维度) + ModelRouter 智能路由 + 多源探测(预设表+启发式) + 厂商模型列表自动拉取(适配 OpenAI/Anthropic 协议差异) + 7 调用点接入 + Settings 模型池 UI + AiChat 模型下拉。按任务需求自动匹配合适模型,不再所有场景共用 default_model — source:📐 设计定稿 (06-16, [F-01-模型能力系统与智能路由设计-2026-06-16.md](./02-架构设计/F-01-模型能力系统与智能路由设计-2026-06-16.md))。F-07 已完成解除阻塞。
|
||||||
|
- [ ] 阶段1:数据模型 — df-ai-core 新增 ModelConfig/Modality/Capability/CostTier/IntelligenceTier + df-storage models 反序列化兼容
|
||||||
|
- [ ] 阶段2:预设表+探测器 — df-ai 新建 model_probe.rs + presets/models.json + 启发式推断 + 多源合并
|
||||||
|
- [ ] 阶段3:厂商模型列表拉取 — df-ai 新建 model_fetch.rs + openai_compat/anthropic_compat 分派 + URL 拼接 + 噪音过滤
|
||||||
|
- [ ] 阶段4:路由器 — df-ai 新建 router.rs + TaskRequirements + select 逻辑
|
||||||
|
- [ ] 阶段5:7 调用点接入 — agentic.rs/title.rs/knowledge_inject.rs/project.rs + IPC ai_fetch_models/ai_probe_model
|
||||||
|
- [ ] 阶段6:前端 — types.ts 类型对齐 + Settings 模型池 UI + AiChat 模型下拉
|
||||||
|
|
||||||
### P1 — Sprint 19 遗留
|
### P1 — Sprint 19 遗留
|
||||||
|
|
||||||
@@ -627,7 +647,7 @@
|
|||||||
|
|
||||||
**✅ 推进(第一批独立可并行)**:
|
**✅ 推进(第一批独立可并行)**:
|
||||||
|
|
||||||
- [ ] F-07 trait 下沉 df-ai-core — 设计完备 4 项决策全定稿,退路可放 df-core,解锁 F-03/F-01
|
- [x] ✅(batch61·2026-06-16·workflow wwtn2knn6·2069f79) F-07 trait 下沉 df-ai-core — 已实施(详见 L599 F-260614-07),4 决策全落地,解锁 F-03(已做 dfe0096)/F-01(待推)。本条为对抗分析裁决区过时重复条目,销账。
|
||||||
- [x] R-PD-2 ✅ ScriptNode 不注册 (第⑩批 2026-06-16) — ~~ScriptNode 不注册 script~~ 3 行删除封死攻击面,工作流当前纯演示无真实脚本需求
|
- [x] R-PD-2 ✅ ScriptNode 不注册 (第⑩批 2026-06-16) — ~~ScriptNode 不注册 script~~ 3 行删除封死攻击面,工作流当前纯演示无真实脚本需求
|
||||||
- [x] F-260615-06 ✅ patch_file (第⑩批 2026-06-16) — ~~patch_file(edit_file)~~ 局部文件更新工具,补齐 AI 文件操作闭环。完整设计见 [patch_file工具设计-2026-06-15.md](./02-架构设计/patch_file工具设计-2026-06-15.md)(API/三层防御/边界情况/替代方案否决/实施步骤)。**核心**: old_text 精确匹配为主+line 辅助+Mutex 并发安全+expected_hash 指纹防脏写。第一批实现核心三件套(~50行)。— src-tauri/src/commands/ai/tool_registry.rs 新增 handler
|
- [x] F-260615-06 ✅ patch_file (第⑩批 2026-06-16) — ~~patch_file(edit_file)~~ 局部文件更新工具,补齐 AI 文件操作闭环。完整设计见 [patch_file工具设计-2026-06-15.md](./02-架构设计/patch_file工具设计-2026-06-15.md)(API/三层防御/边界情况/替代方案否决/实施步骤)。**核心**: old_text 精确匹配为主+line 辅助+Mutex 并发安全+expected_hash 指纹防脏写。第一批实现核心三件套(~50行)。— src-tauri/src/commands/ai/tool_registry.rs 新增 handler
|
||||||
- [x] ✅(batch56·2026-06-16·workflow wsxxurgbr+主代核查) ARC-06 composable 循环依赖 — **破环全闭环**:①`aiShared.ts` 下沉 findToolCall(反向扫描O(1))+4个审批计时器函数(startApprovalTimer/clearApprovalTimer/clearAllApprovalTimers+APPROVAL_TIMEOUT_MS常量) ②`useAiEvents` 删 findToolCall 导出+改 import 从 aiShared 取 ③`useAiSend` 删审批计时器实现(改从 aiShared re-export)+删 drainQueue 直接调用(改经 `ai-drain-queue` 事件总线桥接) ④`stores/ai.ts` 加 `initDrainQueueListener()` 启动事件监听。**B-260616-19 CSS 缺失**(Knowledge.vue 窄屏标题挤压,agent 未产出 .css/.vue style 变更),待补。— src/composables/ai/aiShared.ts + useAiEvents.ts + useAiSend.ts + stores/ai.ts
|
- [x] ✅(batch56·2026-06-16·workflow wsxxurgbr+主代核查) ARC-06 composable 循环依赖 — **破环全闭环**:①`aiShared.ts` 下沉 findToolCall(反向扫描O(1))+4个审批计时器函数(startApprovalTimer/clearApprovalTimer/clearAllApprovalTimers+APPROVAL_TIMEOUT_MS常量) ②`useAiEvents` 删 findToolCall 导出+改 import 从 aiShared 取 ③`useAiSend` 删审批计时器实现(改从 aiShared re-export)+删 drainQueue 直接调用(改经 `ai-drain-queue` 事件总线桥接) ④`stores/ai.ts` 加 `initDrainQueueListener()` 启动事件监听。**B-260616-19 CSS 缺失**(Knowledge.vue 窄屏标题挤压,agent 未产出 .css/.vue style 变更),待补。— src/composables/ai/aiShared.ts + useAiEvents.ts + useAiSend.ts + stores/ai.ts
|
||||||
|
|||||||
16
docs/待决策.md
16
docs/待决策.md
@@ -139,7 +139,7 @@
|
|||||||
- d: 等 git 集成(阶段4 F-260616-08 暂缓),自审代码改动 diff。最贴切(自审代码)但阻塞于阶段4。
|
- d: 等 git 集成(阶段4 F-260616-08 暂缓),自审代码改动 diff。最贴切(自审代码)但阻塞于阶段4。
|
||||||
- **推荐**:**a**(task 中心,最直接解锁自审闭环+人核,数据模型变更小:TaskRecord 加 output_json+迁移+run_workflow 注入 task 到 NodeContext+自审 prompt 模板,中等工作量)。c 通用但 executor 改动大,b 语义弱,d 阻塞阶段4。
|
- **推荐**:**a**(task 中心,最直接解锁自审闭环+人核,数据模型变更小:TaskRecord 加 output_json+迁移+run_workflow 注入 task 到 NodeContext+自审 prompt 模板,中等工作量)。c 通用但 executor 改动大,b 语义弱,d 阻塞阶段4。
|
||||||
- **关联**:todo F-260616-07 阶段3(AiNode 自审 ⏳ 子条) / task_workflow_templates.rs:57-72(testing 模板) / ai_node.rs(AiNode 通用执行) / memory aichat-arch-extensibility(AiNode 空壳架构债)
|
- **关联**:todo F-260616-07 阶段3(AiNode 自审 ⏳ 子条) / task_workflow_templates.rs:57-72(testing 模板) / ai_node.rs(AiNode 通用执行) / memory aichat-arch-extensibility(AiNode 空壳架构债)
|
||||||
- **状态**:🟡 待决(2026-06-16)— 自审对象来源架构决策,方向错返工风险高,待用户拍板。AiNode 自审拓扑/通用执行已就绪,阻塞于此。
|
- **状态**:✅ 已决(2026-06-16)— **决策:a**。TaskRecord 加 `output_json` 产出字段,ai_execute 写 / ai_self_review 读 / human_review 展示。task 中心,产出跟着 task 走,跨工作流执行可传递。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -236,12 +236,12 @@
|
|||||||
- **状态**:✅ 已决(2026-06-16)— **决策:拆独立 crate df-ai-core**。不放 df-core 过渡,LlmProvider trait + 核心数据结构下沉独立 crate,df-ai/df-ideas/df-nodes 均依赖 df-ai-core。**✅ 已实施(batch61·2026-06-16·commit 2069f79·workflow wwtn2knn6+主代核查,cargo 0err+7单测)**。解锁 F-01/F-03 注入。
|
- **状态**:✅ 已决(2026-06-16)— **决策:拆独立 crate df-ai-core**。不放 df-core 过渡,LlmProvider trait + 核心数据结构下沉独立 crate,df-ai/df-ideas/df-nodes 均依赖 df-ai-core。**✅ 已实施(batch61·2026-06-16·commit 2069f79·workflow wwtn2knn6+主代核查,cargo 0err+7单测)**。解锁 F-01/F-03 注入。
|
||||||
|
|
||||||
#### F-260614-01 模型能力系统 Phase 1
|
#### F-260614-01 模型能力系统 Phase 1
|
||||||
- **背景**:ModelCapability 数据模型 + ModelRouter 重写 + 7 调用点接入 + Settings 模型池 UI + AiChat 模型下拉。按需求自动匹配合适模型。
|
- **背景**:ModelConfig 数据模型(模态/能力/价格/智力 4 维度) + ModelRouter 智能路由 + 多源探测(预设表+启发式) + 厂商模型列表自动拉取(适配 OpenAI/Anthropic 协议差异) + 7 调用点接入 + Settings 模型池 UI + AiChat 模型下拉。按需求自动匹配合适模型。
|
||||||
- **设计状态**:📐 设计完成
|
- **设计状态**:📐 设计定稿(2026-06-16,含 7 角度佐证 + 厂商适配 + 用户流程,见 [F-01-模型能力系统与智能路由设计-2026-06-16.md](./02-架构设计/F-01-模型能力系统与智能路由设计-2026-06-16.md))
|
||||||
- **决策点**:排期?(依赖 F-07 trait 下沉)
|
- **决策点**:排期?(依赖 F-07 trait 下沉)
|
||||||
- **推荐**:F-07 后接续
|
- **推荐**:F-07 后接续
|
||||||
- **关联**:todo F-260614-01 / F-07
|
- **关联**:todo F-260614-01 / F-07
|
||||||
- **状态**:🟡 待排期(F-07 已完成 batch61·2069f79,解除阻塞,可接续)
|
- **状态**:✅ 已排期(2026-06-16)— 用户确认推进。设计已定稿,F-07 已完成解除阻塞,列为 P1🔥 可立即实施。6 阶段子任务已拆解见 todo.md。
|
||||||
|
|
||||||
#### F-260614-06 导入历史项目(scan 第二步)
|
#### F-260614-06 导入历史项目(scan 第二步)
|
||||||
- **背景**:description 走 LLM(复用 scan_project_with_ai)+ 采样保留内容图(待 F-260614-05 多模态)+ monorepo 一层 + 批量并发;抽 create_with_binding 缓解 TODO。
|
- **背景**:description 走 LLM(复用 scan_project_with_ai)+ 采样保留内容图(待 F-260614-05 多模态)+ monorepo 一层 + 批量并发;抽 create_with_binding 缓解 TODO。
|
||||||
@@ -263,14 +263,14 @@
|
|||||||
- **决策点**:排期?(增强非阻断)
|
- **决策点**:排期?(增强非阻断)
|
||||||
- **推荐**:低优先(等 F-01 模型能力后)
|
- **推荐**:低优先(等 F-01 模型能力后)
|
||||||
- **关联**:todo F-260614-04
|
- **关联**:todo F-260614-04
|
||||||
- **状态**:🟡 待排期
|
- **状态**:✅ 已排期(2026-06-16)— 用户确认推进。P2 优先级,等 F-01 模型能力系统落地后接续(路由器需要知道哪些模型可用)。
|
||||||
|
|
||||||
#### F-260614-05 模型能力系统 Phase 2(多模态)
|
#### F-260614-05 模型能力系统 Phase 2(多模态)
|
||||||
- **背景**:ChatMessage.content: String → Vec<ContentPart>(Text/Image);前端粘贴/拖拽图片;vision 模型自动路由。
|
- **背景**:ChatMessage.content: String → Vec<ContentPart>(Text/Image);前端粘贴/拖拽图片;vision 模型自动路由。
|
||||||
- **决策点**:排期?(解锁 F-06 导入采样)
|
- **决策点**:排期?(解锁 F-06 导入采样)
|
||||||
- **推荐**:中等(解锁 F-06)
|
- **推荐**:中等(解锁 F-06)
|
||||||
- **关联**:todo F-260614-05 / F-01
|
- **关联**:todo F-260614-05 / F-01
|
||||||
- **状态**:🟡 待排期
|
- **状态**:✅ 已排期(2026-06-16)— 用户确认推进。P2 优先级,F-01 后接续。设计已定稿见 [F-05-多模态实施方案设计-2026-06-16.md](./02-架构设计/F-05-多模态实施方案设计-2026-06-16.md)。
|
||||||
|
|
||||||
#### AE-2025-04 会话级授权(Session Trust)
|
#### AE-2025-04 会话级授权(Session Trust)
|
||||||
- **背景**:同会话内用户批准过某类操作后,后续同类自动放行;换会话清空重审。首批 write_file + run_command。设计已定(目录级粒度 + 仅写+执行首批 + Webhook 走独立 execution_token)。
|
- **背景**:同会话内用户批准过某类操作后,后续同类自动放行;换会话清空重审。首批 write_file + run_command。设计已定(目录级粒度 + 仅写+执行首批 + Webhook 走独立 execution_token)。
|
||||||
@@ -278,14 +278,14 @@
|
|||||||
- **决策点**:排期?(涉及 mod.rs AiSession + audit.rs 审批链)
|
- **决策点**:排期?(涉及 mod.rs AiSession + audit.rs 审批链)
|
||||||
- **推荐**:中高(写→跑→看→改闭环高频连续操作,体验提升大)
|
- **推荐**:中高(写→跑→看→改闭环高频连续操作,体验提升大)
|
||||||
- **关联**:todo AE-2025-04
|
- **关联**:todo AE-2025-04
|
||||||
- **状态**:🟡 待排期
|
- **状态**:✅ 已排期(2026-06-16)— 用户确认推进。P1 优先级,写→跑→看→改闭环高频连续操作体验提升大。设计已定(目录级粒度 + 仅写+执行首批)。
|
||||||
|
|
||||||
#### T-260614-06 Settings.vue 拆 panel 子组件
|
#### T-260614-06 Settings.vue 拆 panel 子组件
|
||||||
- **背景**:1042 行 god file,4 大功能域(AI 模型/Provider 表单/连接管理/通用设置)可拆 src/components/settings/。评估非低风险(纯重构零功能价值,新建 4 子组件 + props/emits + CSS 拆)。
|
- **背景**:1042 行 god file,4 大功能域(AI 模型/Provider 表单/连接管理/通用设置)可拆 src/components/settings/。评估非低风险(纯重构零功能价值,新建 4 子组件 + props/emits + CSS 拆)。
|
||||||
- **决策点**:排期?(单独立项做更稳)
|
- **决策点**:排期?(单独立项做更稳)
|
||||||
- **推荐**:低(纯重构零功能,等 god file 痛点加剧)
|
- **推荐**:低(纯重构零功能,等 god file 痛点加剧)
|
||||||
- **关联**:todo T-260614-06
|
- **关联**:todo T-260614-06
|
||||||
- **状态**:🟡 待排期
|
- **状态**:✅ 已排期(2026-06-16)— 用户确认推进。P3 优先级(纯重构零功能),等 god file 痛点加剧或配合 F-01 模型池 UI 一并做。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user