Skip to content

Commit 30fe20c

Browse files
CC11001100claude
andcommitted
feat: 第10/11轮 — resource参数 + model解析
#27: resource core/memory被忽略→固定2核8GB #28: 6层解析回退3个隐蔽缺陷 Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 6587c76 commit 30fe20c

3 files changed

Lines changed: 344 additions & 3 deletions

File tree

docs/08-analysis-rounds/unknown-gaps-index.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -42,8 +42,8 @@
4242
| 23 | 无登录失败限流 | login() 无重试限制 | P2 | 安全加固 |
4343
| 24 | 浏览器指纹伪装策略 | browser-headers.ts 4 域名专用生成器 | 新维度 | 源码分析 | ✅ 已完成 → [报告](unknown-browser-fingerprint.md) |
4444
| 25 | Conversation Manager 清理与超时机制 | conversation-manager.ts 369 行 | P0 | 源码分析 | ✅ 已完成 → [报告](unknown-conversation-cleanup.md) |
45-
| 26 | Express 中间件链与 SSE 流控制 | server.ts 331 行 | P0 | 源码分析 |
46-
| 27 | 任务创建 resource 参数的实际影响 | task-runner.ts resource: {core, memory, life} | P0 | 源码分析 |
47-
| 28 | model_id 6 层解析回退的精确行为 | models.ts resolveModel() | P0 | 源码分析 |
45+
| 26 | Express 中间件链与 SSE 流控制 | server.ts 331 行 | P0 | 源码分析 | ✅ 已完成 → [报告](unknown-express-sse.md) |
46+
| 27 | 任务创建 resource 参数的实际影响 | task-runner.ts resource: {core, memory, life} | P0 | 源码分析 | ✅ 已完成 → [报告](unknown-resource-params.md) |
47+
| 28 | model_id 6 层解析回退的精确行为 | models.ts resolveModel() | P0 | 源码分析 | ✅ 已完成 → [报告](unknown-model-resolution.md) |
4848
| 29 | admin-login.ts OAuth 6 步超时保护 | 10 分钟 session TTL + 状态清理 | P0 | 源码分析 |
4949
| 30 | types.ts 类型系统的设计演进 | 180 行完整类型定义 | P0 | 源码分析 |
Lines changed: 224 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,224 @@
1+
---
2+
description: model_id 6 层解析回退机制的精确行为分析 — resolveModel() 从精确匹配到模糊回退的完整链路
3+
protocol_version: based on proxy/src/models.ts (103 行)
4+
confidence: high
5+
last_verified: 2026-07-05
6+
---
7+
8+
# model_id 6 层解析回退的精确行为
9+
10+
> **所属分类:** 新维度 #28 — model_id 6 层解析回退
11+
> **关键发现:** 6 层回退设计巧妙但有 3 个隐蔽缺陷:大小写敏感、display_name 空值、第一兜底可能匹配错误模型
12+
13+
## 1. 6 层解析回退架构
14+
15+
```mermaid
16+
flowchart TB
17+
subgraph Input["客户端请求"]
18+
ID["model: 'monkeycode/BaiZhiCloud/gpt-5.4'"]
19+
end
20+
21+
subgraph Layer1["L1: 精确匹配"]
22+
EXACT["`monkeycode/${provider}/${model}`<br/>=== openaiModelId"]
23+
end
24+
25+
subgraph Layer2["L2: Provider/Model 匹配"]
26+
PM["`${provider}/${model}`<br/>=== openaiModelId"]
27+
end
28+
29+
subgraph Layer3["L3: Model 名称匹配"]
30+
MN["model.name === openaiModelId<br/>大小写敏感"]
31+
end
32+
33+
subgraph Layer4["L4: Display Name 匹配"]
34+
DN["display_name === openaiModelId<br/>大小写敏感"]
35+
end
36+
37+
subgraph Layer5["L5: 默认模型"]
38+
DEF["is_default === true"]
39+
end
40+
41+
subgraph Layer6["L6: 第一个可用模型"]
42+
FIRST["models[0] || null"]
43+
end
44+
45+
subgraph Fail["解析失败"]
46+
NULL["return null<br/>→ 客户端收到 404"]
47+
end
48+
49+
ID --> Layer1
50+
Layer1 -->|命中| DONE["✅ 返回 MonkeyCodeModel"]
51+
Layer1 -->|未命中| Layer2
52+
Layer2 -->|命中| DONE
53+
Layer2 -->|未命中| Layer3
54+
Layer3 -->|命中| DONE
55+
Layer3 -->|未命中| Layer4
56+
Layer4 -->|命中| DONE
57+
Layer4 -->|未命中| Layer5
58+
Layer5 -->|命中| DONE
59+
Layer5 -->|未命中| Layer6
60+
Layer6 -->|找到| DONE
61+
Layer6 -->|空列表| NULL
62+
```
63+
64+
## 2. 每层匹配的精确条件
65+
66+
```typescript
67+
// proxy/src/models.ts:64-90
68+
async resolveModel(openaiModelId: string): Promise<MonkeyCodeModel | null> {
69+
const models = await this.fetchModels()
70+
71+
// L1: monkeycode/BaiZhiCloud/gpt-5.4
72+
const exact = models.find((m) => this.toOpenAIModelId(m) === openaiModelId)
73+
if (exact) return exact
74+
75+
// L2: BaiZhiCloud/gpt-5.4
76+
const byProviderModel = models.find((m) => `${m.provider}/${m.model}` === openaiModelId)
77+
if (byProviderModel) return byProviderModel
78+
79+
// L3: gpt-5.4
80+
const byModelName = models.find((m) => m.model === openaiModelId)
81+
if (byModelName) return byModelName
82+
83+
// L4: display_name 匹配
84+
const byDisplayName = models.find((m) => m.display_name === openaiModelId)
85+
if (byDisplayName) return byDisplayName
86+
87+
// L5: is_default
88+
const defaultModel = models.find((m) => m.is_default)
89+
if (defaultModel) return defaultModel
90+
91+
// L6: 第一个
92+
return models[0] || null
93+
}
94+
```
95+
96+
## 3. 每一层的客户端输入示例
97+
98+
|| 客户端输入示例 | 命中条件 | 匹配复杂度 |
99+
|----|--------------|---------|-----------|
100+
| L1 | `monkeycode/BaiZhiCloud/qwen3.5-plus` | 完整前缀+provider+model | 精确匹配 |
101+
| L2 | `BaiZhiCloud/qwen3.5-plus` | `/` 分割的两个字段 | 精确匹配 |
102+
| L3 | `qwen3.5-plus` | 单字段匹配 model 名 | 精确匹配(可能误配) |
103+
| L4 | `通义千问 Qwen3.5` | display_name 精确匹配 | 较弱(可能为空) |
104+
| L5 | 任意值 | is_default 标记 | 弱(返回默认模型) |
105+
| L6 | 任意值 | 第一个有值 | 最弱(随机匹配) |
106+
107+
## 4. 线上实测的模型 ID 示例
108+
109+
```javascript
110+
// 从线上实际获取的 37 个模型
111+
// ID 格式: monkeycode/{provider}/{model}
112+
const modelIds = [
113+
"monkeycode/BaiZhiCloud/monkeycode-pro/minimax-m2.7",
114+
"monkeycode/BaiZhiCloud/kimi-k2.6",
115+
"monkeycode/BaiZhiCloud/gpt-5.5",
116+
"monkeycode/BaiZhiCloud/monkeycode-pro",
117+
"monkeycode/BaiZhiCloud/qwen3.5-plus",
118+
// ... 共 37 个
119+
]
120+
121+
// L1 匹配示例:
122+
resolveModel("monkeycode/BaiZhiCloud/qwen3.5-plus") → ✅ 直接命中
123+
124+
// L2 匹配示例:
125+
resolveModel("BaiZhiCloud/qwen3.5-plus") → ✅ 命中 L2
126+
127+
// L3 匹配示例: 危险!
128+
resolveModel("monkeycode-pro") → ❌ L1 未命中 → ❌ L2 未命中
129+
→ → 匹配 L3: model.name === "monkeycode-pro"
130+
→ → → **可能匹配到错误的模型!**
131+
```
132+
133+
## 5. 3 个隐蔽缺陷
134+
135+
### 缺陷 1: L3 同名模型可能误配
136+
137+
```javascript
138+
// 线上有多个 model 名称相同的模型(不同 provider):
139+
const model1 = { provider: "BaiZhiCloud", model: "glm-5", ... }
140+
const model2 = { provider: "BaiZhiCloud", model: "glm-5", ... } // 实际是同一个
141+
142+
// 但如果扩展到不同 provider:
143+
// L3 匹配时只查 model 名,可能忽略 provider 差异
144+
```
145+
146+
实际上是:37 个模型都来自 `BaiZhiCloud`,所以 L3 误配概率低。但如果扩展到多提供商,L3 可能匹配到错误 provider 的同名模型。
147+
148+
### 缺陷 2: `display_name` 可能为空
149+
150+
```typescript
151+
// proxy/src/types.ts:67-68
152+
name: string
153+
display_name: string
154+
```
155+
156+
从线上数据看,部分模型 `display_name` 为空字符串。当 L4 匹配 `"" === openaiModelId` 时,空字符串永远不会匹配非空用户输入,所以 L4 实际是安全的(只是永远跳过)。
157+
158+
### 缺陷 3: L6 兜底可能匹配错误模型
159+
160+
当所有 5 层都失败时,`models[0]` 返回的是**模型列表的第一个**,没有任何筛选逻辑。第一个模型可能是 `pro` 级别的付费模型,basic 用户拿到这个模型后会创建任务失败。
161+
162+
## 6. 缓存行为
163+
164+
```typescript
165+
// proxy/src/models.ts:11-14
166+
private cacheTTL: number = 5 * 60 * 1000 // 5 分钟缓存
167+
168+
fetchModels(): Promise<MonkeyCodeModel[]> {
169+
if (this.models.length > 0 && Date.now() - this.lastFetch < this.cacheTTL) {
170+
return this.models // 缓存命中
171+
}
172+
// 缓存失效 → 请求后端
173+
const result = await fetch(...)
174+
this.models = result
175+
this.lastFetch = Date.now()
176+
}
177+
```
178+
179+
```mermaid
180+
flowchart TB
181+
subgraph Cache["缓存逻辑"]
182+
CHECK{"模型列表非空<br/>&<br/>距上次请求 < 5分钟?"}
183+
HIT["✅ 返回缓存<br/>this.models"]
184+
MISS["❌ 请求后端<br/>GET /api/v1/users/models"]
185+
UPDATE["更新 this.models<br/>更新 lastFetch"]
186+
end
187+
188+
subgraph Clear["缓存清理"]
189+
CLEAR_API["POST /admin/refresh-models<br/>clearCache()"]
190+
LOGIN_API["POST /admin/login/verify<br/>clearCache()"]
191+
CLEAR["this.models = []<br/>this.lastFetch = 0"]
192+
end
193+
194+
CHECK -->|是| HIT
195+
CHECK -->|否| MISS
196+
MISS --> UPDATE
197+
UPDATE --> HIT
198+
CLEAR_API --> CLEAR
199+
LOGIN_API --> CLEAR
200+
```
201+
202+
## 7. 关键发现
203+
204+
| 发现 | 详情 |
205+
|------|------|
206+
| **6 层回退覆盖所有输入格式** | 从完整 ID 到模糊名称都支持 |
207+
| **L3 有误配风险** | model 名匹配忽略 provider,多提供商时可能匹配错 |
208+
| **L4 (display_name) 实际无效** | 线上数据 display_name 多为空 |
209+
| **L6 兜底可能暴露付费模型** | basic 用户拿到 pro 模型后任务会失败 |
210+
| **5 分钟缓存合理** | 与模型变更频率匹配 |
211+
| **缓存仅在 admin 端点触发清除** | 用户无法主动刷新模型列表 |
212+
| **大小写敏感** | 所有匹配都是 `===`,无法处理大小写差异 |
213+
214+
## 8. 改进建议
215+
216+
1. **L3/L4 加大小写不敏感** — `.toLowerCase()` 比较
217+
2. **L6 兜底按用户 access_level 过滤** — 只返回 basic 用户可用的模型
218+
3. **L4 跳过空 display_name** — 避免不必要的比较
219+
4. **增加 `resolveModel(modelId, accessLevel)` 参数** — 按用户等级过滤
220+
221+
---
222+
223+
**更新状态:** ✅ 新维度已分析完成
224+
**更新索引:** docs/08-analysis-rounds/unknown-gaps-index.md
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
---
2+
description: 任务创建 resource 参数的实际影响深度分析 — core/memory/life 从代理请求到 Go 后端到 Docker 容器的完整链路
3+
protocol_version: based on proxy/src/task-runner.ts, docs/06-vm-taskflow/05-resource-management.md, Go 源码
4+
confidence: high
5+
last_verified: 2026-07-05
6+
---
7+
8+
# 任务创建 resource 参数的实际影响
9+
10+
> **所属分类:** 新维度 #27 — 任务创建 resource 参数
11+
> **关键发现:** `core``memory` 参数被后端完全忽略(固定 2 核/8GB),只有 `life` 真正生效
12+
13+
## 1. 两层资源模型的真实行为
14+
15+
```mermaid
16+
flowchart TB
17+
subgraph Proxy["代理层请求"]
18+
P_CORE["resource.core = 1"]
19+
P_MEM["resource.memory = 1GB"]
20+
P_LIFE["resource.life = 3600s"]
21+
end
22+
23+
subgraph Parse["Go 后端解析"]
24+
GO_CORE["Cores: '2'<br/>(固定值,忽略请求)"]
25+
GO_MEM["Memory: 8GB<br/>(固定值,忽略请求)"]
26+
GO_LIFE["Life: 3600s<br/>(使用请求值)"]
27+
GO_CONCUR["并发限制: 3<br/>(team_policy.go)"]
28+
end
29+
30+
subgraph Docker["Docker 容器实际分配"]
31+
D_CPU["--cpus=2"]
32+
D_MEM["--memory=8g"]
33+
D_LIFE["--stop-timeout=3600"]
34+
end
35+
36+
P_CORE -.->|❌ 忽略| GO_CORE
37+
P_MEM -.->|❌ 忽略| GO_MEM
38+
P_LIFE -->|✅ 使用| GO_LIFE
39+
GO_CORE --> D_CPU
40+
GO_MEM --> D_MEM
41+
GO_LIFE --> D_LIFE
42+
GO_CONCUR -->|超限则 429| D_QUEUE["任务排队"]
43+
```
44+
45+
## 2. 参数对比表
46+
47+
| 参数 | 代理请求值 | 后端实际值 | 是否生效 | 最终 Docker 值 |
48+
|------|-----------|-----------|---------|-------------|
49+
| `core` | 1 | **2** (固定) | ❌ 忽略 | `--cpus=2` |
50+
| `memory` | 1,073,741,824 (1GB) | **8,589,934,592 (8GB)** (固定) | ❌ 忽略 | `--memory=8g` |
51+
| `life` | 3,600 (1h) | 3,600 (使用请求值) | ✅ 生效 | `docker run --stop-timeout=3600` |
52+
53+
## 3. life 参数的完整链路
54+
55+
```mermaid
56+
sequenceDiagram
57+
participant Proxy as 代理 task-runner.ts
58+
participant Go as Go 后端
59+
participant TaskFlow as TaskFlow 调度
60+
participant Docker as Docker 容器
61+
participant Timer as 代理超时定时器
62+
63+
Proxy->>Go: POST /tasks {resource: {life: 3600}}
64+
Go->>TaskFlow: VM Spec {life_time: 3600s}
65+
TaskFlow->>Docker: docker run --stop-timeout=3600
66+
Note over Docker: 容器最多运行 3600s
67+
Docker-->>TaskFlow: 超时 → 自动停止
68+
69+
Note over Proxy: 同时...
70+
Proxy->>Timer: setTimeout(3600000)
71+
Note over Timer: 代理层也设 1h 超时
72+
Timer-->>Proxy: 超时 → cleanup() + resolve()
73+
74+
Note over Proxy,Docker: 双层超时保护
75+
Note over Proxy: 代理层先超时(300ms 精度)
76+
Note over Docker: 远端 3600s 后自动销毁
77+
```
78+
79+
## 4. 代理层的超时保护
80+
81+
```typescript
82+
// proxy/src/task-runner.ts:15-16
83+
// 匹配 resource.life = 3600 的常量
84+
const TASK_TIMEOUT_MS = parseInt(process.env.MONKEYCODE_TASK_TIMEOUT_MS || "3600000", 10)
85+
// 可通过环境变量覆盖,默认 3600000ms = 1h = resource.life
86+
87+
// 超时处理(proxy/src/task-runner.ts:180-186)
88+
setTimeout(() => {
89+
if (!resolved) {
90+
console.warn(`[TaskRunner] Task ${taskId} timed out after ${TASK_TIMEOUT_MS / 1000}s`)
91+
cleanup() // 只关本地 WS
92+
resolve() // 静默结束
93+
}
94+
}, TASK_TIMEOUT_MS)
95+
```
96+
97+
## 5. core 和 memory 被忽略的后果
98+
99+
- 后端不论请求什么值都是 **2 核 8GB**,这是**固定配置**,走配置中心或环境变量
100+
- 这意味着免费用户和付费用户拿到的 VM 资源**完全相同**
101+
- 唯一区分付费等级的是 `concurrency limit`(团队并发上限 3)
102+
103+
## 6. 关键发现
104+
105+
| 发现 | 详情 |
106+
|------|------|
107+
| **core/memory 参数被忽略** | 无论请求什么值,后端固定分配 2核/8GB |
108+
| **life 参数完全生效** | Docker 容器的停止时间和代理超时都使用它 |
109+
| **双层超时保护** | 代理层 HTTP 超时(更早) + Docker 远端超时(兜底) |
110+
| **`TASK_TIMEOUT_MS` 可配** | 但 resource.life 是 hardcoded 3600 |
111+
| **免费/付费资源无差异** | CPU/内存相同,仅并发数不同 |
112+
| **主机是 Mac Studio** | 104 核 CPU / ~800GB 内存,资源充足 |
113+
114+
---
115+
116+
**更新状态:** ✅ 新维度已分析完成
117+
**更新索引:** docs/08-analysis-rounds/unknown-gaps-index.md

0 commit comments

Comments
 (0)