Skip to content

Commit 6a67fd7

Browse files
committed
refactor: 提取 api_enabled 至公共模块并优化同步逻辑
1 parent 0ad24ea commit 6a67fd7

14 files changed

Lines changed: 111 additions & 164 deletions

File tree

‎env/platforms/claude.json‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,10 @@
77
"ANTHROPIC_AUTH_TOKEN": "${claude.token}",
88
"ANTHROPIC_BASE_URL": "${claude.url}",
99
"CLAUDE_CODE_EFFORT_LEVEL": "medium",
10-
"CLAUDE_CODE_DISABLE_EXPERIMENTAL_BETAS": "1"
10+
"CLAUDE_CODE_DISABLE_EXPERIMENTAL_BETAS": "1",
11+
"ANTHROPIC_DEFAULT_OPUS_MODEL": "claude-opus-4-8",
12+
"ANTHROPIC_DEFAULT_SONNET_MODEL": "claude-sonnet-5",
13+
"ANTHROPIC_DEFAULT_HAIKU_MODEL": "claude-haiku-4-5-20251001-thinking"
1114
},
1215
"preamble": {
1316
"target": "CLAUDE.md",

‎sync/cli/validate_env_schema.py‎

Lines changed: 13 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,9 @@
1212
import json
1313
from pathlib import Path
1414

15+
from platforms.claude import _HOST_SKIP as _CLAUDE_HOST_SKIP
16+
from platforms.codex import _HOST_SKIP as _CODEX_HOST_SKIP
17+
1518
REPO_ROOT = Path(__file__).resolve().parents[2]
1619
ENV_DIR = REPO_ROOT / "env"
1720
MCP_DIR = ENV_DIR / "mcp"
@@ -87,38 +90,22 @@ def validate_mcp_file(path: Path) -> list[str]:
8790
}
8891

8992
PLATFORM_FIELDS = {
90-
# Claude-specific
93+
# Claude-specific: team-shared fields not covered by _HOST_SKIP, unioned
94+
# with the platform's own host-specific set (kept in sync with the real
95+
# skip list instead of hand-duplicating it — see platforms/claude.py).
9196
"claude": {
9297
"model", "effortLevel", "alwaysThinkingEnabled", "outputStyle",
9398
"includeGitInstructions", "respectGitignore", "fileCheckpointingEnabled",
9499
"autoCompactEnabled", "autoMemoryEnabled", "respondToBashCommands",
95100
"permissions", "hooks", "_hostSettings",
96-
"apiKeyHelper", "theme", "tui", "editorMode", "preferredNotifChannel",
97-
"statusLine", "voice", "voiceEnabled", "viewMode", "prefersReducedMotion",
98-
"syntaxHighlightingDisabled", "terminalProgressBarEnabled",
99-
"wheelScrollAccelerationEnabled", "axScreenReaderRender", "showTurnDuration",
100-
"showThinkingSummaries", "showClearContextOnPlanAccept", "autoScrollEnabled",
101-
"spinnerTipsEnabled", "spinnerTipsOverride", "spinnerVerbs", "companyAnnouncements",
102-
"footerLinksRegexes", "language", "ultracode", "fastModePerSessionOptIn",
103-
"autoConnectIde", "autoInstallIdeExtension", "externalEditorContext",
104-
"fileSuggestion", "feedbackSurveyRate", "cleanupPeriodDays", "defaultShell",
105-
"prUrlTemplate", "autoUpdatesChannel", "sshConfigs", "worktree", "plansDirectory",
106-
"autoMemoryDirectory", "teammateMode", "teammateDefaultModel", "disableAgentView",
107-
"agent", "agentPushNotifEnabled", "inputNeededNotifEnabled", "remoteControlAtStartup",
108-
"awsAuthRefresh", "awsCredentialExport", "gcpAuthRefresh", "otelHeadersHelper",
109-
"claudeMd", "claudeMdExcludes", "policyHelper", "skipWebFetchPreflight",
110-
},
111-
# Codex-specific
101+
} | _CLAUDE_HOST_SKIP,
102+
# Codex-specific: team-shared fields not covered by _HOST_SKIP, unioned
103+
# with the platform's own host-specific set (see platforms/codex.py).
112104
"codex": {
113-
"model", "model_provider", "model_providers", "personality",
114-
"model_reasoning_effort", "model_verbosity", "model_reasoning_summary",
115-
"plan_mode_reasoning_effort", "sandbox_mode", "approval_policy",
116-
"allow_login_shell", "default_permissions", "project_doc_max_bytes",
117-
"project_doc_fallback_filenames", "sandbox_workspace_write", "features",
118-
"projects", "hide_agent_reasoning", "web_search", "file_opener", "history",
119-
"tools", "shell_environment_policy", "tui", "agents", "memories",
120-
"analytics", "feedback",
121-
},
105+
"model", "model_provider", "model_providers", "sandbox_mode",
106+
"approval_policy", "allow_login_shell", "default_permissions",
107+
"sandbox_workspace_write", "projects",
108+
} | _CODEX_HOST_SKIP,
122109
# CodeBuddy-specific
123110
"codebuddy": {"models", "availableModels"},
124111
# Qwen-specific

‎sync/cli/validate_platform_keys.py‎

Lines changed: 3 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -27,34 +27,6 @@
2727
"continue": {"path", "recall"},
2828
}
2929

30-
# Canonical host-specific (personal) keys per platform. A key from this set that
31-
# appears in env/platforms/<platform>.json but is NOT declared in the platform's
32-
# _HOST_SKIP would leak into team-shared settings. Keep in sync with each
33-
# platform module's _HOST_SKIP / host-specific definitions.
34-
HOST_SPECIFIC_KEYS = {
35-
"claude": {
36-
"apiKeyHelper", "theme", "tui", "editorMode", "preferredNotifChannel",
37-
"statusLine", "voice", "voiceEnabled", "viewMode", "prefersReducedMotion",
38-
"syntaxHighlightingDisabled", "terminalProgressBarEnabled",
39-
"wheelScrollAccelerationEnabled", "axScreenReaderRender", "showTurnDuration",
40-
"showThinkingSummaries", "showClearContextOnPlanAccept", "autoScrollEnabled",
41-
"spinnerTipsEnabled", "spinnerTipsOverride", "spinnerVerbs", "companyAnnouncements",
42-
"footerLinksRegexes", "language", "ultracode", "fastModePerSessionOptIn",
43-
"autoConnectIde", "autoInstallIdeExtension", "externalEditorContext",
44-
"fileSuggestion", "feedbackSurveyRate", "cleanupPeriodDays", "defaultShell",
45-
"prUrlTemplate", "autoUpdatesChannel", "sshConfigs", "worktree", "plansDirectory",
46-
"autoMemoryDirectory", "teammateMode", "teammateDefaultModel", "disableAgentView",
47-
"agent", "agentPushNotifEnabled", "inputNeededNotifEnabled", "remoteControlAtStartup",
48-
"awsAuthRefresh", "awsCredentialExport", "gcpAuthRefresh", "otelHeadersHelper",
49-
"claudeMd", "claudeMdExcludes", "policyHelper", "skipWebFetchPreflight",
50-
},
51-
"codex": {
52-
"hide_agent_reasoning", "web_search", "file_opener", "history", "tools",
53-
"shell_environment_policy", "tui", "agents", "memories", "analytics", "feedback",
54-
},
55-
}
56-
57-
5830
def load_platform_json(platform: str) -> dict:
5931
path = REPO_ROOT / "env" / "platforms" / f"{platform}.json"
6032
if not path.is_file():
@@ -78,19 +50,19 @@ def check_platform(platform: str) -> list[str]:
7850
- internal (starts with '_'),
7951
- engine-handled (e.g. env, hooks, export_env_to_zshrc, _hostSettings),
8052
- declared in the platform's _HOST_SKIP (excluded from team settings),
81-
- a known host-specific key that IS in _HOST_SKIP (leak guard),
8253
- a known team-shared key for this platform.
8354
8455
Any other key (unknown/typo, or a host-specific key missing from _HOST_SKIP)
85-
produces a warning, making the check fail-closed instead of always passing.
56+
produces a warning through the schema allowlist, making the check
57+
fail-closed instead of relying on another hand-maintained host-specific
58+
list.
8659
"""
8760
cfg = load_platform_json(platform)
8861
if not cfg:
8962
return [f" {platform}: no config file found"]
9063

9164
warnings: list[str] = []
9265
host_skip = get_host_skip(platform)
93-
host_specific = HOST_SPECIFIC_KEYS.get(platform, set())
9466
engine_handled = ENGINE_HANDLED_KEYS | ENGINE_HANDLED_BY_PLATFORM.get(platform, set())
9567
known_fields = known_fields_for_platform(platform)
9668

@@ -106,12 +78,6 @@ def check_platform(platform: str) -> list[str]:
10678
if key in host_skip:
10779
skip_count += 1
10880
continue
109-
if key in host_specific:
110-
warnings.append(
111-
f" {platform}: key '{key}' is host-specific but NOT in _HOST_SKIP "
112-
f"— would leak to team-shared settings."
113-
)
114-
continue
11581
if key not in known_fields:
11682
warnings.append(
11783
f" {platform}: key '{key}' is not in the schema allowlist and not in "

‎sync/core/common.py‎

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -384,6 +384,19 @@ def merge_object(existing: Any, updates: dict[str, Any]) -> dict[str, Any]:
384384
return {**base, **updates}
385385

386386

387+
def api_enabled(cfg: dict[str, Any]) -> bool:
388+
"""Third-party API sync toggle, shared by every platform's sync engine.
389+
390+
A missing ``api`` block or missing ``api.enabled`` defaults to enabled,
391+
preserving the historical always-sync behavior. Only an explicit
392+
``false`` disables synced API fields.
393+
"""
394+
api = cfg.get("api")
395+
if not isinstance(api, dict):
396+
return True
397+
return api.get("enabled", True) is True
398+
399+
387400
# ── Path helpers (imported from centralized paths module) ────────────────────
388401

389402
from .paths import ( # noqa: F401

‎sync/platforms/claude.py‎

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
from typing import Any
44

55
from core.common import (
6+
api_enabled as _api_enabled,
67
merge_object,
78
prune_managed_keys_via_sidecar,
89
read_json_object,
@@ -33,12 +34,6 @@ def _repo_hooks_dir() -> Path:
3334
_API_SIDECAR = ".managed_api_fields.json"
3435

3536

36-
def _api_enabled(cfg: dict[str, Any]) -> bool:
37-
api = cfg.get("api")
38-
if not isinstance(api, dict):
39-
return True
40-
return api.get("enabled", True) is True
41-
4237
# ── Host-specific keys ──
4338
# These keys are kept in env/platforms/claude.json as reference but excluded
4439
# from managed (team-shared) settings — each developer sets them individually.

‎sync/platforms/cline.py‎

Lines changed: 23 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
from pathlib import Path
44
from typing import Any
55

6-
from core.common import read_json_object, write_json
6+
from core.common import api_enabled as _api_enabled, read_json_object, write_json
77
from core.paths import (
88
claude_skills_base,
99
cline_data_dir,
@@ -53,20 +53,6 @@ def _save_managed_keys(global_state_keys: set[str], secret_keys: set[str]) -> No
5353
})
5454

5555

56-
def _api_enabled(cfg: dict[str, Any]) -> bool:
57-
"""Cline third-party API sync toggle.
58-
59-
Missing ``api`` block or missing ``api.enabled`` defaults to enabled,
60-
preserving the historical always-sync behavior (Cline previously always
61-
merged globalState + secrets). Only an explicit ``false`` disables synced
62-
API fields (globalState + secrets) and cleans the keys the syncer owns.
63-
"""
64-
api = cfg.get("api")
65-
if not isinstance(api, dict):
66-
return True
67-
return api.get("enabled", True) is True
68-
69-
7056
def _sync_mcp(servers: dict[str, Any]) -> None:
7157
targets = [p for p in cline_mcp_candidate_paths() if p.parent.exists()]
7258
if not targets:
@@ -94,9 +80,29 @@ def _sync_skills() -> None:
9480
continue
9581

9682
dest = cline_skills_dir / skill_dir.name
83+
tmp = cline_skills_dir / f".{skill_dir.name}.tmp-sync"
84+
backup = cline_skills_dir / f".{skill_dir.name}.backup-sync"
85+
86+
if tmp.exists():
87+
shutil.rmtree(tmp)
88+
if backup.exists():
89+
shutil.rmtree(backup)
90+
91+
shutil.copytree(skill_dir, tmp)
92+
9793
if dest.exists():
98-
shutil.rmtree(dest)
99-
shutil.copytree(skill_dir, dest)
94+
dest.rename(backup)
95+
try:
96+
tmp.rename(dest)
97+
except OSError:
98+
if backup.exists() and not dest.exists():
99+
backup.rename(dest)
100+
raise
101+
finally:
102+
if tmp.exists():
103+
shutil.rmtree(tmp)
104+
if backup.exists():
105+
shutil.rmtree(backup)
100106
synced.append(skill_dir.name)
101107

102108
print(f"Synced {len(synced)} skills to {cline_skills_dir}: {', '.join(synced) or '(none)'}.")

‎sync/platforms/codebuddy.py‎

Lines changed: 1 addition & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
from typing import Any
44

55
from core import recall
6-
from core.common import read_json_object, sync_json_mcp, write_json
6+
from core.common import api_enabled as _api_enabled, read_json_object, sync_json_mcp, write_json
77
from core.paths import (
88
claude_skills_base,
99
codebuddy_mcp_path,
@@ -54,19 +54,6 @@ def _merge_recall_block(target: Path, block: str) -> None:
5454
recall.merge_recall_block_markdown(target, block)
5555

5656

57-
def _api_enabled(cfg: dict[str, Any]) -> bool:
58-
"""CodeBuddy third-party API sync toggle.
59-
60-
Like Claude, a missing ``api`` block or missing ``api.enabled`` defaults to
61-
enabled so the historical always-sync behavior is preserved. Only an explicit
62-
``false`` disables synced API model fields.
63-
"""
64-
api = cfg.get("api")
65-
if not isinstance(api, dict):
66-
return True
67-
return api.get("enabled", True) is True
68-
69-
7057
def _validate_model_entries(value: Any) -> list[dict[str, Any]]:
7158
if not isinstance(value, list):
7259
raise ValueError("platforms.codebuddy.models must be a list.")

‎sync/platforms/codex.py‎

Lines changed: 1 addition & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
from typing import Any
44

55
from core.common import (
6+
api_enabled as _api_enabled,
67
codex_config_path,
78
codex_root_dir,
89
load_platform_config,
@@ -35,19 +36,6 @@
3536
)
3637

3738

38-
def _api_enabled(cfg: dict[str, Any]) -> bool:
39-
"""Codex third-party API sync toggle.
40-
41-
A missing ``api`` block or missing ``api.enabled`` defaults to enabled,
42-
preserving the historical always-sync behavior. Only an explicit
43-
``false`` disables synced API fields.
44-
"""
45-
api = cfg.get("api")
46-
if not isinstance(api, dict):
47-
return True
48-
return api.get("enabled", True) is True
49-
50-
5139
def generate_mcp_toml(servers: dict[str, Any]) -> str:
5240
"""Generate TOML for MCP servers (platform-agnostic)."""
5341
lines: list[str] = ["# AUTOGENERATED from env/mcp/", ""]

‎sync/platforms/continue.py‎

Lines changed: 1 addition & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
from typing import Any
55

66
from core import recall
7+
from core.common import api_enabled as _api_enabled
78
from core.paths import continue_root_dir
89

910

@@ -311,19 +312,6 @@ def _repo_root() -> Path:
311312
return here.parents[2]
312313

313314

314-
def _api_enabled(cfg: dict[str, Any]) -> bool:
315-
"""Continue third-party API sync toggle.
316-
317-
Missing ``api`` or missing ``api.enabled`` defaults to enabled, preserving
318-
the historical always-sync behavior for the managed ``models`` block. Only
319-
an explicit ``false`` disables synced API model fields.
320-
"""
321-
api = cfg.get("api")
322-
if not isinstance(api, dict):
323-
return True
324-
return api.get("enabled", True) is True
325-
326-
327315
def _remove_yaml_root_key(yaml_text: str, key_name: str) -> str:
328316
"""Remove a top-level key (and its nested block) from YAML text.
329317

‎sync/platforms/gemini.py‎

Lines changed: 6 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,12 @@
11
from pathlib import Path
22
from typing import Any
33

4-
from core.common import prune_managed_keys_via_sidecar, read_json_object, write_json
4+
from core.common import (
5+
api_enabled as _api_enabled,
6+
prune_managed_keys_via_sidecar,
7+
read_json_object,
8+
write_json,
9+
)
510
from core.paths import (
611
gemini_root_dir,
712
gemini_settings_path,
@@ -19,19 +24,6 @@
1924
_API_MODEL_FIELDS = {"model"}
2025

2126

22-
def _api_enabled(cfg: dict[str, Any]) -> bool:
23-
"""Gemini third-party API sync toggle.
24-
25-
Missing ``api`` or missing ``api.enabled`` defaults to enabled, preserving
26-
the historical always-sync behavior. Only an explicit ``false`` disables
27-
synced API fields.
28-
"""
29-
api = cfg.get("api")
30-
if not isinstance(api, dict):
31-
return True
32-
return api.get("enabled", True) is True
33-
34-
3527
def _extract_settings(cfg: dict[str, Any], api_enabled: bool = True) -> dict[str, Any]:
3628
"""Extract Gemini CLI settings from platform config, stripping internal keys.
3729

0 commit comments

Comments
 (0)