From ba7331c4764da85c11527215be4158f8d88e03c3 Mon Sep 17 00:00:00 2001
From: n33 <1741170479@qq.com>
Date: Tue, 13 Oct 2026 21:56:52 +0800
Subject: [PATCH] =?UTF-8?q?fix(models):=20=E7=BB=9F=E4=B8=80=E4=B8=8A?=
=?UTF-8?q?=E4=B8=8B=E6=96=87=E7=AA=97=E5=8F=A3=E8=A6=86=E7=9B=96=E4=B8=8E?=
=?UTF-8?q?=E5=AF=BC=E5=85=A5=E8=A7=84=E5=88=99?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
apps/codex-plus-manager/src/App.tsx | 5 ++
apps/codex-plus-manager/src/i18n-en.ts | 1 +
.../src/model-metadata.test.ts | 10 +++-
apps/codex-plus-manager/src/model-metadata.ts | 6 ++-
assets/inject/renderer-inject.js | 5 +-
.../renderer-inject/70-model-catalog.js | 5 +-
assets/inject/renderer-inject/manifest.json | 2 +-
crates/codex-plus-core/src/model_catalog.rs | 51 ++++++++++++++++++-
crates/codex-plus-core/src/model_suffix.rs | 47 ++++++++++++++---
crates/codex-plus-core/src/relay_config.rs | 9 ++--
crates/codex-plus-core/src/vision.rs | 38 +-------------
crates/codex-plus-core/tests/model_catalog.rs | 32 ++++++++++++
docs/model-window-overrides.md | 11 ++++
13 files changed, 165 insertions(+), 57 deletions(-)
create mode 100644 docs/model-window-overrides.md
diff --git a/apps/codex-plus-manager/src/App.tsx b/apps/codex-plus-manager/src/App.tsx
index 10834322d..45092050e 100644
--- a/apps/codex-plus-manager/src/App.tsx
+++ b/apps/codex-plus-manager/src/App.tsx
@@ -9669,6 +9669,11 @@ function RelayProfileEditor({
rows={7}
/>
{metadataImportError ?
{metadataImportError}
: null}
+ {metadataImportPreview?.contextWindow && row.window.trim() && metadataImportPreview.contextWindow !== row.window.trim() ? (
+
+ {tf("文档里的上下文窗口是 {0},当前列填的是 {1},已保留当前值;要用文档的值请先清空窗口。", [metadataImportPreview.contextWindow, row.window.trim()])}
+
+ ) : null}
{metadataImportPreview?.ignoredFields.length ? (
{tf("以下字段由 Codex++ 计算或维护,导入不会覆盖:{0}", [metadataImportPreview.ignoredFields.join(", ")])}
diff --git a/apps/codex-plus-manager/src/i18n-en.ts b/apps/codex-plus-manager/src/i18n-en.ts
index dc2e183ca..70f9a99be 100644
--- a/apps/codex-plus-manager/src/i18n-en.ts
+++ b/apps/codex-plus-manager/src/i18n-en.ts
@@ -1444,6 +1444,7 @@ export const EN_PLAIN: Record = {
// Interpolated strings: tf("前缀 {0}", [x]) -> EN_TEMPLATE["前缀 {0}"] with {0} filled.
export const EN_TEMPLATE: Record = {
+ "文档里的上下文窗口是 {0},当前列填的是 {1},已保留当前值;要用文档的值请先清空窗口。": "The document's context window is {0}, but the column holds {1}. The current value was kept; clear the window to use the document's value.",
"{0} 个插件": "{0} plugins",
"{0} / {1} 个插件": "{0} / {1} plugins",
"查看插件 {0} 的详情": "View details for plugin {0}",
diff --git a/apps/codex-plus-manager/src/model-metadata.test.ts b/apps/codex-plus-manager/src/model-metadata.test.ts
index 3fccb876a..0a19279f2 100644
--- a/apps/codex-plus-manager/src/model-metadata.test.ts
+++ b/apps/codex-plus-manager/src/model-metadata.test.ts
@@ -467,7 +467,7 @@ describe("model metadata helpers", () => {
});
it("导入文档写回模型行的规则覆盖", () => {
- // 窗口不同才写;相同不写(避免多余 state 更新)
+ // 窗口只补空列
assert.deepStrictEqual(
importDocumentSyncPatch({ window: "", autoCompact: "" }, { contextWindow: "500000", autoCompactPercent: null }),
{ window: "500000" },
@@ -476,6 +476,12 @@ describe("model metadata helpers", () => {
importDocumentSyncPatch({ window: "500000", autoCompact: "" }, { contextWindow: "500000", autoCompactPercent: null }),
{},
);
+ // 手工填过的窗口是显式覆盖层:文档里的值不能把它换掉,否则「配好 1M 的模型
+ // 粘贴供应商 models.json 后被上游 256K 覆盖」会再次出现。
+ assert.deepStrictEqual(
+ importDocumentSyncPatch({ window: "1m", autoCompact: "50%" }, { contextWindow: "262144", autoCompactPercent: null }),
+ {},
+ );
// 压缩比不同才写
assert.deepStrictEqual(
importDocumentSyncPatch({ window: "500000", autoCompact: "90%" }, { contextWindow: "500000", autoCompactPercent: "80%" }),
@@ -483,7 +489,7 @@ describe("model metadata helpers", () => {
);
// JSON 未声明压缩比(null):不动行里的值
assert.deepStrictEqual(
- importDocumentSyncPatch({ window: "500000", autoCompact: "90%" }, { contextWindow: "600000", autoCompactPercent: null }),
+ importDocumentSyncPatch({ window: "", autoCompact: "90%" }, { contextWindow: "600000", autoCompactPercent: null }),
{ window: "600000" },
);
// 空预览(粘贴清空/粘贴失败):整体 no-op
diff --git a/apps/codex-plus-manager/src/model-metadata.ts b/apps/codex-plus-manager/src/model-metadata.ts
index 1d92c12fb..42794424a 100644
--- a/apps/codex-plus-manager/src/model-metadata.ts
+++ b/apps/codex-plus-manager/src/model-metadata.ts
@@ -436,7 +436,9 @@ export function metadataSourceTags(options: {
export type ModelRowSyncPatch = { window?: string; autoCompact?: string };
/// 导入文档解析结果 → 模型行补丁(JSON→行 的实时写回规则):
-/// - 窗口:解析出有效值且与行现值不同才写(相同不写,避免多余 state 更新)
+/// - 窗口:**只补空列**。手工填过的窗口是显式覆盖层,导入文档不能把它换掉
+/// (曾出现「配好 1M 的模型,粘贴供应商 models.json 后被上游 256K 覆盖」);
+/// 想用文档的值就先把行内窗口清空。
/// - 压缩比:解析出显示值且与行现值不同才写;null(JSON 未声明)不动行,
/// 避免粘贴别的模型 JSON 时清掉用户行里的值
export function importDocumentSyncPatch(
@@ -444,7 +446,7 @@ export function importDocumentSyncPatch(
preview: { contextWindow: string | null; autoCompactPercent: string | null },
): ModelRowSyncPatch {
const patch: ModelRowSyncPatch = {};
- if (preview.contextWindow && preview.contextWindow !== row.window) {
+ if (preview.contextWindow && !row.window.trim()) {
patch.window = preview.contextWindow;
}
if (preview.autoCompactPercent && preview.autoCompactPercent !== row.autoCompact) {
diff --git a/assets/inject/renderer-inject.js b/assets/inject/renderer-inject.js
index 70ab7d7f5..daa142ac9 100644
--- a/assets/inject/renderer-inject.js
+++ b/assets/inject/renderer-inject.js
@@ -11178,8 +11178,9 @@
const metadata = codexPlusModelMetadata(modelName);
if (!descriptor || !metadata) return false;
let changed = false;
- for (const key of ["displayName", "description", "defaultReasoningEffort"]) {
- if (typeof metadata[key] === "string" && metadata[key] && descriptor[key] !== metadata[key]) {
+ for (const key of ["displayName", "description", "defaultReasoningEffort", "contextWindow", "maxContextWindow", "context_window", "max_context_window"]) {
+ const valid = typeof metadata[key] === "string" ? !!metadata[key] : Number.isFinite(metadata[key]) && metadata[key] > 0;
+ if (valid && descriptor[key] !== metadata[key]) {
descriptor[key] = metadata[key];
changed = true;
}
diff --git a/assets/inject/renderer-inject/70-model-catalog.js b/assets/inject/renderer-inject/70-model-catalog.js
index f3f2ad9b9..05216134d 100644
--- a/assets/inject/renderer-inject/70-model-catalog.js
+++ b/assets/inject/renderer-inject/70-model-catalog.js
@@ -200,8 +200,9 @@
const metadata = codexPlusModelMetadata(modelName);
if (!descriptor || !metadata) return false;
let changed = false;
- for (const key of ["displayName", "description", "defaultReasoningEffort"]) {
- if (typeof metadata[key] === "string" && metadata[key] && descriptor[key] !== metadata[key]) {
+ for (const key of ["displayName", "description", "defaultReasoningEffort", "contextWindow", "maxContextWindow", "context_window", "max_context_window"]) {
+ const valid = typeof metadata[key] === "string" ? !!metadata[key] : Number.isFinite(metadata[key]) && metadata[key] > 0;
+ if (valid && descriptor[key] !== metadata[key]) {
descriptor[key] = metadata[key];
changed = true;
}
diff --git a/assets/inject/renderer-inject/manifest.json b/assets/inject/renderer-inject/manifest.json
index a2fb5f529..f65d41453 100644
--- a/assets/inject/renderer-inject/manifest.json
+++ b/assets/inject/renderer-inject/manifest.json
@@ -53,7 +53,7 @@
{
"name": "70-model-catalog.js",
"description": "全局状态、模型目录、模型白名单与解锁",
- "lines": 1146
+ "lines": 1147
},
{
"name": "71-model-picker-groups.js",
diff --git a/crates/codex-plus-core/src/model_catalog.rs b/crates/codex-plus-core/src/model_catalog.rs
index 7853f9b89..dea3a7215 100644
--- a/crates/codex-plus-core/src/model_catalog.rs
+++ b/crates/codex-plus-core/src/model_catalog.rs
@@ -98,7 +98,7 @@ fn relay_profile_model_catalog_value(home: &Path, profile: &RelayProfile) -> Val
profile.name.trim()
};
let model_count = models.len();
- let model_metadata = model_ui_metadata_map_with_overrides(&models, &profile.model_metadata);
+ let model_metadata = relay_profile_model_ui_metadata_map(profile, &models);
json!({
"status": if models.is_empty() { "not_configured" } else { "ok" },
"path": home.join("config.toml").to_string_lossy(),
@@ -215,6 +215,55 @@ fn model_ui_metadata_map_with_overrides(models: &[String], user_json: &str) -> V
Value::Object(metadata)
}
+fn relay_profile_model_ui_metadata_map(profile: &RelayProfile, models: &[String]) -> Value {
+ let mut metadata = model_ui_metadata_map_with_overrides(models, &profile.model_metadata)
+ .as_object().cloned().unwrap_or_default();
+ let windows: std::collections::BTreeMap =
+ serde_json::from_str::(&profile.model_windows)
+ .ok()
+ .and_then(|value| value.as_object().cloned())
+ .map(|object| object.into_iter()
+ .filter_map(|(key, value)| value.as_str().map(|text| (key, text.to_string())))
+ .collect())
+ .unwrap_or_default();
+ let fallback = crate::model_suffix::parse_window_token(&profile.context_window);
+ for model in models {
+ let window = crate::model_suffix::lookup_model_map(&windows, model)
+ .and_then(|token| crate::model_suffix::parse_window_token(token))
+ .or(fallback);
+ if let Some(window) = window {
+ let entry = metadata.entry(model.clone()).or_insert_with(|| json!({}));
+ for field in ["contextWindow", "maxContextWindow", "context_window", "max_context_window"] {
+ entry[field] = json!(window);
+ }
+ }
+ }
+ Value::Object(metadata)
+}
+
+#[cfg(test)]
+mod window_override_tests {
+ use super::*;
+
+ #[test]
+ fn manual_window_precedes_profile_and_builtin_metadata() {
+ let models = vec!["gpt-5.5".to_string(), "vendor-model".to_string()];
+ let profile = RelayProfile {
+ model_windows: r#"{"GPT-5.5[1M]":"1M"}"#.into(),
+ context_window: "128K".into(),
+ ..Default::default()
+ };
+ let metadata = relay_profile_model_ui_metadata_map(&profile, &models);
+ for field in ["contextWindow", "maxContextWindow", "context_window", "max_context_window"] {
+ assert_eq!(metadata["gpt-5.5"][field], 1_000_000);
+ assert_eq!(metadata["vendor-model"][field], 128_000);
+ }
+ let metadata = relay_profile_model_ui_metadata_map(&RelayProfile::default(), &models);
+ assert!(metadata["gpt-5.5"]["contextWindow"].as_u64().is_some());
+ assert!(metadata.get("vendor-model").is_none());
+ }
+}
+
pub async fn read_codex_model_catalog_from_home(
home: &Path,
env: &HashMap,
diff --git a/crates/codex-plus-core/src/model_suffix.rs b/crates/codex-plus-core/src/model_suffix.rs
index e20da5e6c..bc2a4b4fb 100644
--- a/crates/codex-plus-core/src/model_suffix.rs
+++ b/crates/codex-plus-core/src/model_suffix.rs
@@ -20,6 +20,29 @@ pub struct ModelCatalogEntry {
pub auto_compact_percent: Option,
}
+/// 剥掉合法窗口后缀,保留模型 ID 的大小写。
+pub(crate) fn normalize_model_key(raw: &str) -> String {
+ parse_model_suffix(raw).0
+}
+
+/// 优先精确匹配,再剥后缀,最后按大小写不敏感匹配历史键。
+/// BTreeMap 保证同名大小写变体的回退结果稳定。
+pub(crate) fn lookup_model_map<'a, T>(
+ map: &'a std::collections::BTreeMap,
+ model: &str,
+) -> Option<&'a T> {
+ if let Some(value) = map.get(model) {
+ return Some(value);
+ }
+ let slug = normalize_model_key(model);
+ if let Some(value) = map.get(&slug) {
+ return Some(value);
+ }
+ map.iter()
+ .find(|(key, _)| normalize_model_key(key).eq_ignore_ascii_case(&slug))
+ .map(|(_, value)| value)
+}
+
/// 解析单个模型条目的后缀,返回 (slug, 可选窗口)。
/// 括号内非合法窗口 token 时,整串作为 slug 且 window=None(不剥离括号)。
pub fn parse_model_suffix(raw: &str) -> (String, Option) {
@@ -117,6 +140,8 @@ pub fn collect_catalog_entries(
current_model: &str,
) -> Vec {
// 先解析 model_list,保留顺序并去重;后缀已从 model_list 剥离,窗口来自 model_windows map。
+ let windows = model_windows.iter().map(|(key, value)| (key.clone(), value.clone())).collect();
+ let auto_compacts = model_auto_compact.iter().map(|(key, value)| (key.clone(), value.clone())).collect();
let mut seen = HashSet::new();
let mut list_entries: Vec = Vec::new();
for raw in model_list
@@ -132,12 +157,10 @@ pub fn collect_catalog_entries(
continue;
}
let suffix_window = suffix_window.or_else(|| {
- model_windows
- .get(&slug)
+ lookup_model_map(&windows, &slug)
.and_then(|token| parse_window_token(token))
});
- let auto_compact_percent = model_auto_compact
- .get(&slug)
+ let auto_compact_percent = lookup_model_map(&auto_compacts, &slug)
.and_then(|token| parse_compact_percent(token));
list_entries.push(ModelCatalogEntry {
display_name: slug.clone(),
@@ -154,12 +177,10 @@ pub fn collect_catalog_entries(
let (slug, suffix_window) = parse_model_suffix(current_model);
if !slug.is_empty() {
let suffix_window = suffix_window.or_else(|| {
- model_windows
- .get(&slug)
+ lookup_model_map(&windows, &slug)
.and_then(|token| parse_window_token(token))
});
- let auto_compact_percent = model_auto_compact
- .get(&slug)
+ let auto_compact_percent = lookup_model_map(&auto_compacts, &slug)
.and_then(|token| parse_compact_percent(token));
entries.push(ModelCatalogEntry {
display_name: slug.clone(),
@@ -408,6 +429,16 @@ pub fn model_ui_metadata_with_override(slug: &str, user_override: Option<&Value>
if metadata.get("codex_plus_default").and_then(Value::as_bool) == Some(true) {
ui["codex_plus_default"] = json!(true);
}
+ // 注入侧与原生目录使用同一份窗口信息。
+ for (source, camel, snake) in [
+ ("context_window", "contextWindow", "context_window"),
+ ("max_context_window", "maxContextWindow", "max_context_window"),
+ ] {
+ if let Some(window) = metadata.get(source) {
+ ui[camel] = window.clone();
+ ui[snake] = window.clone();
+ }
+ }
if !has_builtin {
// 单独声明 Fast 不能替原生描述符制造名称、描述或推理默认档。
for (ui_field, source_field) in [
diff --git a/crates/codex-plus-core/src/relay_config.rs b/crates/codex-plus-core/src/relay_config.rs
index dbf0f2cfa..694c0031f 100644
--- a/crates/codex-plus-core/src/relay_config.rs
+++ b/crates/codex-plus-core/src/relay_config.rs
@@ -3584,8 +3584,8 @@ fn copy_standard_responses_catalog(
let mut changed = false;
let configured_windows = entries
.iter()
- .map(|entry| (entry.slug.as_str(), entry.suffix_window.or(fallback_window)))
- .collect::>();
+ .map(|entry| (entry.slug.clone(), entry.suffix_window.or(fallback_window)))
+ .collect::>();
for model in models {
// Lite 能力跟随上游:官方登录 + ChatGPT 后端保留原值,
// 其余上游强制关闭(外部目录的 Lite 标记可能照抄官方目录,
@@ -3599,7 +3599,10 @@ fn copy_standard_responses_catalog(
let Some(slug) = model.get("slug").and_then(Value::as_str) else {
continue;
};
- let Some(Some(window)) = configured_windows.get(slug) else {
+ // 外部 catalog 的 slug 与本地行的写法/大小写可能不一致,走三级回退查表,
+ // 否则配置好的窗口会在外部目录里整条失效、保留上游给的值。
+ let Some(Some(window)) = crate::model_suffix::lookup_model_map(&configured_windows, slug)
+ else {
continue;
};
if model.get("context_window").and_then(Value::as_u64) != Some(*window) {
diff --git a/crates/codex-plus-core/src/vision.rs b/crates/codex-plus-core/src/vision.rs
index 962329192..a24a33a22 100644
--- a/crates/codex-plus-core/src/vision.rs
+++ b/crates/codex-plus-core/src/vision.rs
@@ -10,6 +10,8 @@ use std::collections::HashMap;
use std::sync::{LazyLock, Mutex};
use std::time::{Duration, Instant};
+use crate::model_suffix::lookup_model_map;
+
const BATCH_SIZE: usize = 5;
/// 黄金窗口:Phase 1 同步补全的最近 N 轮 user 消息。
const GOLDEN_WINDOW_DEPTH: usize = 10;
@@ -118,42 +120,6 @@ pub enum ImageHandling {
Vlm,
}
-/// 归一化 per-model map 的查表 key:剥掉 `[1M]` 窗口后缀,**保留原大小写**。
-///
-/// 后缀剥离复用 `model_suffix::parse_model_suffix`(与 catalog 生成同一套语义),
-/// 与前端 `model-windows.ts` 的 `modelMapKeyFromRowName` 逐字对齐:
-/// 只剥「合法窗口后缀」,`x[abc]`/`x[0]` 这类非法后缀整串保留。
-fn normalize_model_key(raw: &str) -> String {
- crate::model_suffix::parse_model_suffix(raw).0
-}
-
-/// 在 per-model map 中逐级查表:原始名 → 规范 slug(剥后缀)→ 剥后缀 + 大小写不敏感。
-///
-/// 三级回退与前端 `model-windows.ts::lookupModelMapEntry` 同源(issue #2345):
-/// - 直接命中:上游请求里的 model 字符串与 map key 逐字相同;
-/// - 规范 slug:请求带 `[1M]` 后缀而 key 不带(前端落盘的新数据);
-/// - 归一化后大小写不敏感:**两侧都先剥后缀再比大小写**——只剥请求侧不够,
-/// 历史数据的 key 是带后缀的行名原样(`deepseek-v4-pro[1M]`),请求侧不带后缀时
-/// 会整个漏掉。同时覆盖供应商 slug 大小写不统一(`GLM-5.3` vs `glm-5.3`)。
-///
-/// 大小写不敏感一级按 BTreeMap 的字典序取第一个匹配,保证同一份配置的多次请求
-/// 解析结果稳定(不会因 HashMap 迭代序在两个同名不同 case 的 key 之间抖动)。
-fn lookup_model_map<'a, T>(
- map: &'a std::collections::BTreeMap,
- model: &str,
-) -> Option<&'a T> {
- if let Some(value) = map.get(model) {
- return Some(value);
- }
- let slug = normalize_model_key(model);
- if let Some(value) = map.get(&slug) {
- return Some(value);
- }
- map.iter()
- .find(|(key, _)| normalize_model_key(key).to_lowercase() == slug.to_lowercase())
- .map(|(_, value)| value)
-}
-
/// 解析 model_vlm JSON,返回该模型的图片处理模式。
///
/// 未命中任何一级回退时返回 `SendAsIs`(默认行为不变,per-profile 单值配置也
diff --git a/crates/codex-plus-core/tests/model_catalog.rs b/crates/codex-plus-core/tests/model_catalog.rs
index 809c8bfc4..ea2bfb5d8 100644
--- a/crates/codex-plus-core/tests/model_catalog.rs
+++ b/crates/codex-plus-core/tests/model_catalog.rs
@@ -7,11 +7,43 @@ use std::thread;
use codex_plus_core::model_catalog::{
read_codex_model_catalog, read_codex_model_catalog_from_home,
};
+use codex_plus_core::model_suffix::collect_catalog_entries;
use codex_plus_core::settings::{
BackendSettings, RelayMode, RelayProfile, RelayProtocol, SettingsStore,
};
use serde_json::json;
+#[test]
+fn catalog_entries_apply_manual_windows_case_insensitively() {
+ // 上游 /models 返回的 id 与本地保存的 key 常只差大小写;精确查表会让用户
+ // 配好的窗口整条落空,最后露出上游/Codex 默认的窗口。
+ let windows = HashMap::from([("DeepSeek-Flash".to_string(), "1m".to_string())]);
+ let compacts = HashMap::from([("DeepSeek-Flash".to_string(), "50%".to_string())]);
+ let entries = collect_catalog_entries("deepseek-flash\nglm-5.3", &windows, &compacts, "");
+ let deepseek = entries
+ .iter()
+ .find(|entry| entry.slug == "deepseek-flash")
+ .expect("模型条目缺失");
+ assert_eq!(deepseek.suffix_window, Some(1_000_000));
+ assert_eq!(deepseek.auto_compact_percent, Some(50_000_000));
+ // 没有配置窗口的模型不该被牵连。
+ let glm = entries
+ .iter()
+ .find(|entry| entry.slug == "glm-5.3")
+ .expect("模型条目缺失");
+ assert_eq!(glm.suffix_window, None);
+}
+
+#[test]
+fn catalog_entries_strip_suffix_from_row_name_and_look_up_window() {
+ // 行名带 [1M] 后缀时窗口来自后缀本身;没有后缀时来自 model_windows。
+ let entries =
+ collect_catalog_entries("deepseek-v4-pro[1M]", &HashMap::new(), &HashMap::new(), "");
+ assert_eq!(entries.len(), 1);
+ assert_eq!(entries[0].slug, "deepseek-v4-pro");
+ assert_eq!(entries[0].suffix_window, Some(1_000_000));
+}
+
#[tokio::test]
async fn model_catalog_fetches_models_from_codex_config_provider() {
let temp = tempfile::tempdir().unwrap();
diff --git a/docs/model-window-overrides.md b/docs/model-window-overrides.md
new file mode 100644
index 000000000..2b0c404d8
--- /dev/null
+++ b/docs/model-window-overrides.md
@@ -0,0 +1,11 @@
+# 模型窗口覆盖
+
+手工 `modelWindows` 优先于供应商顶层 `contextWindow`,未指定时沿用内置元数据。
+生成目录、外部目录覆盖和渲染端描述符使用同一份窗口信息。
+
+模型键先精确匹配,再剥合法窗口后缀,最后按大小写不敏感匹配历史键。
+保存时保留原始 ID;同名大小写变体的回退按键的字典序决定。
+
+导入 `models.json` 只补空窗口列,已填写的窗口保持不变。窗口不一致时,
+导入面板显示当前值与文档值;清空窗口后可采用文档值。
+自动压缩默认值及模型显示顺序保持原行为。