@minniexcode/codex-switch 0.0.11 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.AI.md +110 -152
- package/README.CN.md +179 -215
- package/README.md +183 -217
- package/dist/app/add-provider.js +9 -3
- package/dist/app/edit-provider.js +17 -3
- package/dist/app/get-status.js +8 -3
- package/dist/app/list-providers.js +48 -1
- package/dist/app/run-doctor.js +4 -0
- package/dist/cli/output.js +153 -18
- package/dist/commands/handlers.js +10 -6
- package/dist/commands/help.js +9 -5
- package/dist/commands/registry.js +22 -14
- package/dist/domain/config.js +26 -1
- package/dist/domain/providers.js +16 -0
- package/dist/domain/runtime-state.js +30 -8
- package/dist/infra/config-repo.js +16 -206
- package/dist/interaction/interactive.js +16 -6
- package/dist/runtime/copilot-bridge.js +2 -1
- package/dist/storage/config-repo.js +0 -23
- package/docs/Design/codex-switch-v0.0.12-design.md +343 -0
- package/docs/Design/codex-switch-v0.1.0-design.md +152 -0
- package/docs/PRD/codex-switch-prd-v0.0.12.md +279 -0
- package/docs/PRD/codex-switch-prd-v0.1.0.md +217 -317
- package/docs/Tests/testing.md +31 -151
- package/docs/cli-usage.md +223 -524
- package/docs/codex-switch-command-design.md +649 -646
- package/docs/codex-switch-product-overview.md +86 -241
- package/docs/codex-switch-technical-architecture.md +1115 -1112
- package/package.json +51 -51
- package/dist/app/rollback-latest.js +0 -26
|
@@ -1,208 +1,18 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
-
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
-
if (k2 === undefined) k2 = k;
|
|
4
|
-
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
-
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
-
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
-
}
|
|
8
|
-
Object.defineProperty(o, k2, desc);
|
|
9
|
-
}) : (function(o, m, k, k2) {
|
|
10
|
-
if (k2 === undefined) k2 = k;
|
|
11
|
-
o[k2] = m[k];
|
|
12
|
-
}));
|
|
13
|
-
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
-
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
-
}) : function(o, v) {
|
|
16
|
-
o["default"] = v;
|
|
17
|
-
});
|
|
18
|
-
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
-
var ownKeys = function(o) {
|
|
20
|
-
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
-
var ar = [];
|
|
22
|
-
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
-
return ar;
|
|
24
|
-
};
|
|
25
|
-
return ownKeys(o);
|
|
26
|
-
};
|
|
27
|
-
return function (mod) {
|
|
28
|
-
if (mod && mod.__esModule) return mod;
|
|
29
|
-
var result = {};
|
|
30
|
-
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
-
__setModuleDefault(result, mod);
|
|
32
|
-
return result;
|
|
33
|
-
};
|
|
34
|
-
})();
|
|
35
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
-
exports.readConfigFile =
|
|
37
|
-
|
|
38
|
-
exports
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
exports.
|
|
42
|
-
exports.
|
|
43
|
-
exports.
|
|
44
|
-
exports.
|
|
45
|
-
exports.
|
|
46
|
-
exports.
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
const codex_paths_1 = require("./codex-paths");
|
|
53
|
-
const fs_utils_1 = require("./fs-utils");
|
|
54
|
-
/**
|
|
55
|
-
* Reads config.toml and throws a typed error when the file is missing.
|
|
56
|
-
*/
|
|
57
|
-
function readConfigFile(configPath) {
|
|
58
|
-
return (0, fs_utils_1.readRequiredFile)(configPath, "CONFIG_NOT_FOUND", "config.toml");
|
|
59
|
-
}
|
|
60
|
-
/**
|
|
61
|
-
* Reads and parses config.toml into the managed structured document shape.
|
|
62
|
-
*/
|
|
63
|
-
function readStructuredConfig(configPath) {
|
|
64
|
-
const content = readConfigFile(configPath);
|
|
65
|
-
try {
|
|
66
|
-
return (0, config_1.parseStructuredConfig)(content);
|
|
67
|
-
}
|
|
68
|
-
catch (error) {
|
|
69
|
-
throw (0, errors_1.cliError)("CONFIG_PARSE_ERROR", "Failed to parse config.toml.", {
|
|
70
|
-
file: configPath,
|
|
71
|
-
cause: (0, errors_1.normalizeError)(error).message,
|
|
72
|
-
});
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
/**
|
|
76
|
-
* Reads the active top-level profile from config.toml.
|
|
77
|
-
*/
|
|
78
|
-
function readCurrentProfile(configPath) {
|
|
79
|
-
const profile = readStructuredConfig(configPath).activeProfile ?? (0, config_1.parseTopLevelProfile)(readConfigFile(configPath));
|
|
80
|
-
if (!profile) {
|
|
81
|
-
throw (0, errors_1.cliError)("PROFILE_NOT_FOUND", "No top-level profile is set in config.toml.", {
|
|
82
|
-
file: configPath,
|
|
83
|
-
});
|
|
84
|
-
}
|
|
85
|
-
return profile;
|
|
86
|
-
}
|
|
87
|
-
/**
|
|
88
|
-
* Lists all named profile sections declared in config.toml.
|
|
89
|
-
*/
|
|
90
|
-
function listConfigProfiles(configPath) {
|
|
91
|
-
return new Set(readStructuredConfig(configPath).profiles.map((profile) => profile.name));
|
|
92
|
-
}
|
|
93
|
-
/**
|
|
94
|
-
* Verifies that a provider's target profile exists before a switch operation proceeds.
|
|
95
|
-
*/
|
|
96
|
-
function ensureProfileExists(configPath, profile, provider) {
|
|
97
|
-
const document = readStructuredConfig(configPath);
|
|
98
|
-
if (!document.profiles.some((entry) => entry.name === profile)) {
|
|
99
|
-
throw (0, errors_1.cliError)("PROFILE_NOT_FOUND", `Profile "${profile}" does not exist in config.toml.`, {
|
|
100
|
-
file: configPath,
|
|
101
|
-
provider,
|
|
102
|
-
profile,
|
|
103
|
-
});
|
|
104
|
-
}
|
|
105
|
-
return document;
|
|
106
|
-
}
|
|
107
|
-
/**
|
|
108
|
-
* Resolves one profile view and enforces the managed model_provider contract.
|
|
109
|
-
*/
|
|
110
|
-
function requireManagedProfileRuntime(document, providers, profile) {
|
|
111
|
-
const view = (0, config_1.buildManagedProfileViews)(document, providers).find((entry) => entry.name === profile);
|
|
112
|
-
if (!view) {
|
|
113
|
-
throw (0, errors_1.cliError)("PROFILE_NOT_FOUND", `Profile "${profile}" does not exist in config.toml.`, {
|
|
114
|
-
profile,
|
|
115
|
-
});
|
|
116
|
-
}
|
|
117
|
-
if (!view.modelProvider) {
|
|
118
|
-
throw (0, errors_1.cliError)("MANAGED_PROFILE_FIELDS_MISSING", `Managed profile "${profile}" requires model_provider.`, {
|
|
119
|
-
profile,
|
|
120
|
-
missingFields: ["model_provider"],
|
|
121
|
-
});
|
|
122
|
-
}
|
|
123
|
-
if (view.modelProvider !== profile) {
|
|
124
|
-
throw (0, errors_1.cliError)("INVALID_ARGUMENT", `Managed profile "${profile}" must use the same model_provider name.`, {
|
|
125
|
-
profile,
|
|
126
|
-
modelProvider: view.modelProvider,
|
|
127
|
-
});
|
|
128
|
-
}
|
|
129
|
-
const modelProviderSection = document.modelProviders.find((entry) => entry.name === view.modelProvider);
|
|
130
|
-
if (!modelProviderSection) {
|
|
131
|
-
throw (0, errors_1.cliError)("PROFILE_NOT_FOUND", `Model provider "${view.modelProvider}" does not exist in config.toml.`, {
|
|
132
|
-
profile,
|
|
133
|
-
modelProvider: view.modelProvider,
|
|
134
|
-
});
|
|
135
|
-
}
|
|
136
|
-
if (!modelProviderSection.baseUrl) {
|
|
137
|
-
throw (0, errors_1.cliError)("MANAGED_PROFILE_FIELDS_MISSING", `Model provider "${view.modelProvider}" requires base_url.`, {
|
|
138
|
-
profile,
|
|
139
|
-
modelProvider: view.modelProvider,
|
|
140
|
-
missingFields: ["base_url"],
|
|
141
|
-
});
|
|
142
|
-
}
|
|
143
|
-
return view;
|
|
144
|
-
}
|
|
145
|
-
/**
|
|
146
|
-
* Verifies that a same-named model_provider runtime section exists and has base_url.
|
|
147
|
-
*/
|
|
148
|
-
function requireModelProviderRuntimeSection(document, profile) {
|
|
149
|
-
const modelProviderSection = document.modelProviders.find((entry) => entry.name === profile);
|
|
150
|
-
if (!modelProviderSection) {
|
|
151
|
-
throw (0, errors_1.cliError)("PROFILE_NOT_FOUND", `Model provider "${profile}" does not exist in config.toml.`, {
|
|
152
|
-
profile,
|
|
153
|
-
modelProvider: profile,
|
|
154
|
-
});
|
|
155
|
-
}
|
|
156
|
-
if (!modelProviderSection.baseUrl) {
|
|
157
|
-
throw (0, errors_1.cliError)("MANAGED_PROFILE_FIELDS_MISSING", `Model provider "${profile}" requires base_url.`, {
|
|
158
|
-
profile,
|
|
159
|
-
modelProvider: profile,
|
|
160
|
-
missingFields: ["base_url"],
|
|
161
|
-
});
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
|
-
/**
|
|
165
|
-
* Rewrites config.toml so the requested profile becomes the active top-level profile.
|
|
166
|
-
*/
|
|
167
|
-
function updateTopLevelProfile(configPath, configContent, profile) {
|
|
168
|
-
(0, fs_utils_1.writeTextFileAtomic)(configPath, (0, config_1.applyPatchOperations)(configContent, (0, config_1.planConfigMutation)((0, config_1.parseStructuredConfig)(configContent), {
|
|
169
|
-
setActiveProfile: profile,
|
|
170
|
-
}).operations));
|
|
171
|
-
}
|
|
172
|
-
/**
|
|
173
|
-
* Exposes the config mutation planner to application services.
|
|
174
|
-
*/
|
|
175
|
-
function createConfigMutationPlan(document, args) {
|
|
176
|
-
return (0, config_1.planConfigMutation)(document, args);
|
|
177
|
-
}
|
|
178
|
-
/**
|
|
179
|
-
* Applies a previously generated mutation plan to config.toml in one write.
|
|
180
|
-
*/
|
|
181
|
-
function applyConfigMutation(configPath, document, plan) {
|
|
182
|
-
(0, fs_utils_1.writeTextFileAtomic)(configPath, (0, config_1.applyPatchOperations)(document.rawText, plan.operations));
|
|
183
|
-
}
|
|
184
|
-
/**
|
|
185
|
-
* Finds candidate Codex directories in a stable, non-recursive order.
|
|
186
|
-
*/
|
|
187
|
-
function findCodexDirCandidates(explicitCodexDir) {
|
|
188
|
-
if (explicitCodexDir) {
|
|
189
|
-
return [(0, codex_paths_1.resolveCodexDir)(explicitCodexDir)];
|
|
190
|
-
}
|
|
191
|
-
const candidates = new Set();
|
|
192
|
-
const ordered = [];
|
|
193
|
-
const envCandidate = process.env[codex_paths_1.CODEX_DIR_ENV_NAME];
|
|
194
|
-
if (envCandidate) {
|
|
195
|
-
ordered.push((0, codex_paths_1.resolveCodexDir)(envCandidate));
|
|
196
|
-
}
|
|
197
|
-
if (process.env.NODE_ENV === "development") {
|
|
198
|
-
ordered.push(path.resolve(process.cwd(), "dev-codex", "local-sandbox"));
|
|
199
|
-
}
|
|
200
|
-
ordered.push(path.join(os.homedir(), ".codex"));
|
|
201
|
-
for (const candidate of ordered) {
|
|
202
|
-
if (!candidate || candidates.has(candidate) || !fs.existsSync(candidate)) {
|
|
203
|
-
continue;
|
|
204
|
-
}
|
|
205
|
-
candidates.add(candidate);
|
|
206
|
-
}
|
|
207
|
-
return [...candidates];
|
|
208
|
-
}
|
|
3
|
+
exports.updateTopLevelProfile = exports.requireModelProviderRuntimeSection = exports.requireManagedProfileRuntime = exports.readStructuredConfig = exports.readCurrentProfile = exports.readConfigFile = exports.listConfigProfiles = exports.findCodexDirCandidates = exports.ensureProfileExists = exports.createConfigMutationPlan = exports.applyConfigMutation = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* Compatibility facade that re-exports config repository helpers from storage.
|
|
6
|
+
*/
|
|
7
|
+
var config_repo_1 = require("../storage/config-repo");
|
|
8
|
+
Object.defineProperty(exports, "applyConfigMutation", { enumerable: true, get: function () { return config_repo_1.applyConfigMutation; } });
|
|
9
|
+
Object.defineProperty(exports, "createConfigMutationPlan", { enumerable: true, get: function () { return config_repo_1.createConfigMutationPlan; } });
|
|
10
|
+
Object.defineProperty(exports, "ensureProfileExists", { enumerable: true, get: function () { return config_repo_1.ensureProfileExists; } });
|
|
11
|
+
Object.defineProperty(exports, "findCodexDirCandidates", { enumerable: true, get: function () { return config_repo_1.findCodexDirCandidates; } });
|
|
12
|
+
Object.defineProperty(exports, "listConfigProfiles", { enumerable: true, get: function () { return config_repo_1.listConfigProfiles; } });
|
|
13
|
+
Object.defineProperty(exports, "readConfigFile", { enumerable: true, get: function () { return config_repo_1.readConfigFile; } });
|
|
14
|
+
Object.defineProperty(exports, "readCurrentProfile", { enumerable: true, get: function () { return config_repo_1.readCurrentProfile; } });
|
|
15
|
+
Object.defineProperty(exports, "readStructuredConfig", { enumerable: true, get: function () { return config_repo_1.readStructuredConfig; } });
|
|
16
|
+
Object.defineProperty(exports, "requireManagedProfileRuntime", { enumerable: true, get: function () { return config_repo_1.requireManagedProfileRuntime; } });
|
|
17
|
+
Object.defineProperty(exports, "requireModelProviderRuntimeSection", { enumerable: true, get: function () { return config_repo_1.requireModelProviderRuntimeSection; } });
|
|
18
|
+
Object.defineProperty(exports, "updateTopLevelProfile", { enumerable: true, get: function () { return config_repo_1.updateTopLevelProfile; } });
|
|
@@ -52,7 +52,10 @@ const fs = __importStar(require("node:fs"));
|
|
|
52
52
|
const path = __importStar(require("node:path"));
|
|
53
53
|
const errors_1 = require("../domain/errors");
|
|
54
54
|
const backups_1 = require("../domain/backups");
|
|
55
|
+
const providers_1 = require("../domain/providers");
|
|
56
|
+
const runtime_state_1 = require("../domain/runtime-state");
|
|
55
57
|
const codex_paths_1 = require("../storage/codex-paths");
|
|
58
|
+
const config_repo_1 = require("../storage/config-repo");
|
|
56
59
|
const providers_repo_1 = require("../storage/providers-repo");
|
|
57
60
|
const backup_repo_1 = require("../storage/backup-repo");
|
|
58
61
|
const add_interactive_1 = require("./add-interactive");
|
|
@@ -65,15 +68,22 @@ function canPrompt(runtime, jsonMode) {
|
|
|
65
68
|
/**
|
|
66
69
|
* Prompts the user to choose one configured provider when a command omitted its target.
|
|
67
70
|
*/
|
|
68
|
-
async function promptForProviderSelection(runtime, providersPath, message) {
|
|
71
|
+
async function promptForProviderSelection(runtime, providersPath, configPath, message) {
|
|
69
72
|
const providers = (0, providers_repo_1.readProvidersFile)(providersPath);
|
|
73
|
+
const currentProfile = fs.existsSync(configPath) ? (0, config_repo_1.readStructuredConfig)(configPath).activeProfile : null;
|
|
74
|
+
const liveState = (0, runtime_state_1.inspectLiveStateDrift)(currentProfile, providers);
|
|
70
75
|
const choices = Object.entries(providers.providers)
|
|
71
76
|
.sort(([left], [right]) => left.localeCompare(right))
|
|
72
|
-
.map(([providerName, provider]) =>
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
+
.map(([providerName, provider]) => {
|
|
78
|
+
const providerType = (0, providers_1.isCopilotBridgeProvider)(provider) ? "copilot" : "direct";
|
|
79
|
+
const currentMarker = liveState.providerResolvable && liveState.mappedProvider === providerName ? " | current" : "";
|
|
80
|
+
const ambiguousMarker = !liveState.providerResolvable && liveState.mappedProviders.includes(providerName) ? " | current=ambiguous" : "";
|
|
81
|
+
return {
|
|
82
|
+
value: providerName,
|
|
83
|
+
label: providerName,
|
|
84
|
+
hint: `profile=${provider.profile} | type=${providerType}${currentMarker}${ambiguousMarker}`,
|
|
85
|
+
};
|
|
86
|
+
});
|
|
77
87
|
if (choices.length === 0) {
|
|
78
88
|
throw (0, errors_1.cliError)("PROVIDER_NOT_FOUND", "No providers are configured.");
|
|
79
89
|
}
|
|
@@ -221,7 +221,8 @@ async function startOrReuseCopilotBridge(providerName, provider, runtimeDir) {
|
|
|
221
221
|
}
|
|
222
222
|
child.unref();
|
|
223
223
|
const startedAt = new Date().toISOString();
|
|
224
|
-
|
|
224
|
+
// The worker can take a little longer to become healthy on Windows or under loaded test runs.
|
|
225
|
+
const healthy = await waitForCopilotBridgeStartup(child, runtime.bridgeHost, selectedPort, 25, 200);
|
|
225
226
|
if (!healthy.ok) {
|
|
226
227
|
(0, runtime_state_repo_1.clearCopilotBridgeState)(runtimeDir);
|
|
227
228
|
if (healthy.reason === "start-failed") {
|
|
@@ -40,7 +40,6 @@ exports.listConfigProfiles = listConfigProfiles;
|
|
|
40
40
|
exports.ensureProfileExists = ensureProfileExists;
|
|
41
41
|
exports.requireManagedProfileRuntime = requireManagedProfileRuntime;
|
|
42
42
|
exports.requireModelProviderRuntimeSection = requireModelProviderRuntimeSection;
|
|
43
|
-
exports.resolveActiveProviderName = resolveActiveProviderName;
|
|
44
43
|
exports.updateTopLevelProfile = updateTopLevelProfile;
|
|
45
44
|
exports.createConfigMutationPlan = createConfigMutationPlan;
|
|
46
45
|
exports.applyConfigMutation = applyConfigMutation;
|
|
@@ -50,7 +49,6 @@ const os = __importStar(require("node:os"));
|
|
|
50
49
|
const path = __importStar(require("node:path"));
|
|
51
50
|
const config_1 = require("../domain/config");
|
|
52
51
|
const errors_1 = require("../domain/errors");
|
|
53
|
-
const providers_1 = require("../domain/providers");
|
|
54
52
|
const codex_paths_1 = require("./codex-paths");
|
|
55
53
|
const fs_utils_1 = require("./fs-utils");
|
|
56
54
|
/**
|
|
@@ -163,27 +161,6 @@ function requireModelProviderRuntimeSection(document, profile) {
|
|
|
163
161
|
});
|
|
164
162
|
}
|
|
165
163
|
}
|
|
166
|
-
/**
|
|
167
|
-
* Resolves the current active provider and requires the mapping to be unique.
|
|
168
|
-
*/
|
|
169
|
-
function resolveActiveProviderName(document, providers) {
|
|
170
|
-
if (!document.activeProfile) {
|
|
171
|
-
throw (0, errors_1.cliError)("PROFILE_NOT_FOUND", "No top-level profile is set in config.toml.");
|
|
172
|
-
}
|
|
173
|
-
const matches = (0, providers_1.findProvidersByProfile)(providers, document.activeProfile);
|
|
174
|
-
if (matches.length === 0) {
|
|
175
|
-
throw (0, errors_1.cliError)("UNMANAGED_ACTIVE_PROFILE", `Active profile "${document.activeProfile}" is not mapped by providers.json.`, {
|
|
176
|
-
profile: document.activeProfile,
|
|
177
|
-
});
|
|
178
|
-
}
|
|
179
|
-
if (matches.length > 1) {
|
|
180
|
-
throw (0, errors_1.cliError)("ACTIVE_PROVIDER_UNRESOLVED", `Active profile "${document.activeProfile}" maps to multiple providers, so the active managed provider is ambiguous.`, {
|
|
181
|
-
profile: document.activeProfile,
|
|
182
|
-
providers: matches,
|
|
183
|
-
});
|
|
184
|
-
}
|
|
185
|
-
return matches[0];
|
|
186
|
-
}
|
|
187
164
|
/**
|
|
188
165
|
* Rewrites config.toml so the requested profile becomes the active top-level profile.
|
|
189
166
|
*/
|
|
@@ -0,0 +1,343 @@
|
|
|
1
|
+
# codex-switch `0.0.12` 设计文档
|
|
2
|
+
|
|
3
|
+
## 文档信息
|
|
4
|
+
|
|
5
|
+
- 文档类型:实现约束设计文档
|
|
6
|
+
- 适用版本:`0.0.12`
|
|
7
|
+
- 目标范围:`0.0.11 -> 0.0.12`
|
|
8
|
+
- 版本角色:beta / internal-test / release-hardening
|
|
9
|
+
- 对应 PRD:[`../PRD/codex-switch-prd-v0.0.12.md`](../PRD/codex-switch-prd-v0.0.12.md)
|
|
10
|
+
- 关联发布门槛:[`../PRD/codex-switch-prd-v0.1.0.md`](../PRD/codex-switch-prd-v0.1.0.md)
|
|
11
|
+
- 关联上一版设计:[`./codex-switch-v0.0.11-design.md`](./codex-switch-v0.0.11-design.md)
|
|
12
|
+
- 关联路线图:[`./codex-switch-v0.0.9-to-v0.0.12-roadmap.md`](./codex-switch-v0.0.9-to-v0.0.12-roadmap.md)
|
|
13
|
+
|
|
14
|
+
## 1. 文档目标
|
|
15
|
+
|
|
16
|
+
本设计文档不是泛泛而谈的产品说明,而是 `0.0.12` 的实现前约束文档。实现者必须把它视为本版变更边界的唯一直接规格来源之一,并按本文固定:
|
|
17
|
+
|
|
18
|
+
- 哪些文件必须改
|
|
19
|
+
- 每个文件改什么、改到什么程度
|
|
20
|
+
- 哪些内部清理允许做,但不强制
|
|
21
|
+
- 哪些内容本版明确不碰
|
|
22
|
+
|
|
23
|
+
`0.0.12` 的目标不是展开 `0.1.0` 之后的平台化设想,也不是继续追加新的顶级能力,而是把已经可用的 `0.0.11` 收束成一个适合内测验证的 beta 版本:主工作流清晰、help 一致、输出语义一致、测试与发布检查可执行、历史长期文档不再冒充当前事实源。
|
|
24
|
+
|
|
25
|
+
## 2. 版本定位
|
|
26
|
+
|
|
27
|
+
`0.0.12` 固定为 `beta / internal-test / release-hardening` 版本,不是新增顶级命令版本,不是新 upstream 版本,也不是继续扩展 feature surface 的版本。
|
|
28
|
+
|
|
29
|
+
本版只解决以下问题:
|
|
30
|
+
|
|
31
|
+
- 主工作流是否已经足够清楚,可以作为内测用户的默认使用路径
|
|
32
|
+
- README、CLI help、CLI usage、产品概览、测试说明、changelog、版本号是否讲的是同一套事实
|
|
33
|
+
- `init`、`login copilot`、`status`、`doctor` 的人类可读输出是否已经体现 tool-home-first 与 dual-path model
|
|
34
|
+
- `migrate` 与 `setup` 是否已经被放回正确产品位置
|
|
35
|
+
|
|
36
|
+
本版不解决以下问题:
|
|
37
|
+
|
|
38
|
+
- 新命令族
|
|
39
|
+
- 新 upstream
|
|
40
|
+
- 平台化抽象
|
|
41
|
+
- 自动迁移兼容层
|
|
42
|
+
- 历史大文档全文重写
|
|
43
|
+
|
|
44
|
+
## 3. 设计原则
|
|
45
|
+
|
|
46
|
+
`0.0.12` 必须遵循以下原则:
|
|
47
|
+
|
|
48
|
+
1. 主工作流优先于高级 adopt 流程。
|
|
49
|
+
2. 文档、help、输出、测试必须讲同一套事实。
|
|
50
|
+
3. `migrate` 保留,但产品权重下调为 advanced adopt helper。
|
|
51
|
+
4. `setup` 保留,但只保留 deprecation contract,不再作为主入口。
|
|
52
|
+
5. 不改 `--json` 顶层 envelope:`ok / command / data / warnings / error`。
|
|
53
|
+
6. 不为开发版本引入自动迁移、双读双写或旧布局兼容层。
|
|
54
|
+
7. `0.0.12` 的优先级是发布面收口,而不是继续扩实现自由度。
|
|
55
|
+
8. 若 `src/cli/output.ts` 已能表达目标语义,则不为了“更漂亮的数据结构”去改公开 JSON contract。
|
|
56
|
+
|
|
57
|
+
## 4. 变更矩阵
|
|
58
|
+
|
|
59
|
+
下表定义 `0.0.12` 的必改落点。未出现在“必须修改内容”中的扩展实现,不属于本版默认范围。
|
|
60
|
+
|
|
61
|
+
| 文件 | 当前问题 | 必须修改内容 | 修改边界 | 类型 |
|
|
62
|
+
| --- | --- | --- | --- | --- |
|
|
63
|
+
| `README.md` | Quick Start 仍容易让 `migrate` 与 fresh install 混淆,版本线与文档入口未完全对齐 `0.0.12` | `Current version` 改为 `0.0.12`;Quick Start 顺序改为 `init -> add -> switch -> status -> doctor`;补 Copilot 主路径 `init -> login copilot -> add --copilot -> switch -> status -> doctor`;将 `migrate` 改写为已有运行态时使用的 adopt helper;文档链接切到 `0.0.12` PRD/design | 不重写全文,只收口主入口、版本、链接与命令定位 | 必改文档 |
|
|
64
|
+
| `README.CN.md` | 中文版仍可能把 adopt 路径和主路径混写,版本与弃用表述可能漂移 | 调整示例顺序;明确 direct 主路径与 adopt 路径;收口 `setup` 的弃用表述;更新版本号、文档链接、最近版本更新中的 `0.0.12` 条目 | 不做风格性全文重写,重点修正文案事实 | 必改文档 |
|
|
65
|
+
| `README.AI.md` | 仍可能把 `migrate` 置于主入口附近,对 agent 的稳定操作摘要不够收束 | 将 Main Command Surface 改为 direct / Copilot 主路径优先;更新 `Notes For Agents` 推荐顺序;`Current Version Context` 改为 `0.0.12`;明确 `login copilot` 的真实实现是 SDK + official CLI invocation + auth recheck | 保持“给 agent 的摘要”定位,不展开成人类长文档 | 必改文档 |
|
|
66
|
+
| `docs/cli-usage.md` | 更像命令字典,主工作流不够靠前,命令契约与真实实现表述需收口 | 版本改为 `0.0.12`;在总览前部新增 direct 与 Copilot 两个固定工作流小节;将 `migrate` 改为 `Advanced Adopt`;将 `setup` 放入 deprecated-only section;收口 `init` / `login copilot` / `status` / `doctor` 契约 | 仍是 usage/contract 文档,不扩成新的架构设计稿 | 必改文档 |
|
|
67
|
+
| `docs/codex-switch-product-overview.md` | 仍残留旧 `~/.codex/providers.json` / `backups/` 单目录叙事 | 将产品工作方式改为 dual-path model;主流程改为 direct 主路径 / Copilot 主路径 / `migrate` adopt helper;去掉 `providers.json` 和 `backups/` 位于 `~/.codex` 的叙述;明确这是本地 provider 管理器,而不是旧 setup 工具 | 只纠正活跃产品叙述,不扩成长篇 roadmap | 必改文档 |
|
|
68
|
+
| `docs/Tests/testing.md` | 更像一般测试说明,尚未收口为 beta release checklist | `Version under test` 改为 `0.0.12`;新增 direct 主路径 / Copilot 主路径 / help / version / pack 检查项;说明 `migrate` 与 `login copilot` 的真实测试限制;保留 suite 说明但更新名称与职责 | 重点是发布前检查,而不是历史测试报告汇编 | 必改文档 |
|
|
69
|
+
| `CHANGELOG.md` | 缺少 `0.0.12` 发布记录或定位不够准确 | 新增 `0.0.12` 条目,包含标题日期、发布定位、`Changed` / `Docs` / `Verification`,并说明这是 beta hardening release,不是 feature expansion release | 只追加版本记录,不改历史条目语义 | 必改文档 |
|
|
70
|
+
| `package.json` | 版本号需与 `0.0.12` 版本线一致 | 仅修改 `version` 为 `0.0.12` | 不改 `files`、`engines`、`bin`、包名、发布访问级别 | 必改元数据 |
|
|
71
|
+
| `src/commands/help.ts` | 顶层帮助示例仍未完全体现主工作流优先级 | 顶层 examples 改为 direct 主路径优先;`migrate` 不再作为前三个示例之一;顶层说明加入 `primary workflows / advanced adopt` 语义;保留 `setup` 但不放进主入口示例 | 不新增命令,不重做 help renderer | 必改代码 |
|
|
72
|
+
| `src/commands/registry.ts` | 多个命令的 summary/details/examples 未收口到 `0.0.12` 叙事 | 更新 `init`、`login`、`migrate`、`setup`、`add`、`switch`、`status`、`doctor` 的 `summary/details/examples`;其中 `migrate` 明确 adopt helper,`setup` 明确 deprecation-only,`login` 明确 SDK install + official Copilot CLI invocation + recheck,`status` / `doctor` 明确 dual-path 诊断语义 | 只改文案契约,不改 command id、flags 或命令名 | 必改代码 |
|
|
73
|
+
| `src/cli/output.ts` | 人类可读输出仍带有旧 `codexDir` 中心语义,`login` 缺少清晰成功摘要 | `init` 输出改为 tool home 语义;`status` 改为 `tool home / target runtime / active provider / runtime health / next step` 结构;`doctor` 改为健康结论 + 面向修复的 issue 输出;补 `login` 简洁成功摘要;仅改 human-readable 渲染 | 不改顶层 JSON envelope;优先复用现有 `data` 字段 | 必改代码 |
|
|
74
|
+
| `src/app/list-providers.ts` | 当前 `list` 只返回 `name/profile/note/tags`,无法表达 current provider 和 provider type | 将 `list` 扩展为只读地结合当前 runtime 状态,补充每个 provider 的 `providerType`、`isActive`,并补充列表级 current-provider 元数据 | 不改变顶层 JSON envelope;保留 `count/providers` 基本结构;只追加字段 | 必改代码 |
|
|
75
|
+
| `src/interaction/interactive.ts` | 共享 provider 选择器只显示 provider 名和 profile,无法区分 direct/Copilot,也无法提示 current | 统一 provider 选择器 hint,至少包含 `profile`、`providerType` 和 current 标记;ambiguous 场景不对任何单个 provider 打 current 标记 | 不新增交互步骤;只增强现有选择器可见信息 | 必改代码 |
|
|
76
|
+
| `docs/codex-switch-command-design.md` | 长期设计文档仍可能被误读为当前 release contract | 在顶部增加状态说明:这是历史跨版本参考,不是当前 release contract,并指向 `docs/cli-usage.md`、`docs/PRD/codex-switch-prd-v0.0.12.md`、`docs/Design/codex-switch-v0.0.12-design.md` | 只加状态说明与跳转,不全文重写 | 状态说明 |
|
|
77
|
+
| `docs/codex-switch-technical-architecture.md` | 长期架构文档仍可能被误读为当前版本规范 | 在顶部增加状态说明:这是历史跨版本参考,不是当前 release contract,并指向 `docs/cli-usage.md`、`docs/PRD/codex-switch-prd-v0.0.12.md`、`docs/Design/codex-switch-v0.0.12-design.md` | 只加状态说明与跳转,不全文重写 | 状态说明 |
|
|
78
|
+
|
|
79
|
+
## 5. 输出与帮助语义设计
|
|
80
|
+
|
|
81
|
+
本节固定 `0.0.12` 的目标表达,避免实现时自由发挥。
|
|
82
|
+
|
|
83
|
+
### 5.1 顶层 help
|
|
84
|
+
|
|
85
|
+
顶层 help 必须在第一屏就让用户看出两条主路径:
|
|
86
|
+
|
|
87
|
+
- direct provider 主路径:`init -> add -> switch -> status -> doctor`
|
|
88
|
+
- Copilot provider 主路径:`init -> login copilot -> add --copilot -> switch -> status -> doctor`
|
|
89
|
+
|
|
90
|
+
同时必须让用户看出:
|
|
91
|
+
|
|
92
|
+
- `migrate` 是 advanced adopt,不是 fresh install 默认第一步
|
|
93
|
+
- `setup` 仍保留,但只作为 deprecated entry 被说明
|
|
94
|
+
|
|
95
|
+
### 5.2 `init`
|
|
96
|
+
|
|
97
|
+
`init` 的人类可读成功输出不要求逐字一致,但语义必须包括:
|
|
98
|
+
|
|
99
|
+
- `toolHomeDir`
|
|
100
|
+
- `toolConfigPath`
|
|
101
|
+
- `providersPath`
|
|
102
|
+
- 是否新建或是否已存在
|
|
103
|
+
- 下一步推荐
|
|
104
|
+
|
|
105
|
+
`init` 不应继续以“创建/初始化目标 `codexDir`”作为中心表述。它初始化的是 `codex-switch` 的 tool home 与最小管理态,而不是把 `codexDir` 讲成工具自身的 home。
|
|
106
|
+
|
|
107
|
+
### 5.3 `list` 与 provider 选择器
|
|
108
|
+
|
|
109
|
+
`list` 与共享 provider 选择器必须让用户快速回答以下问题:
|
|
110
|
+
|
|
111
|
+
- 当前有哪些 managed provider
|
|
112
|
+
- 每个 provider 属于 `direct` 还是 `copilot`
|
|
113
|
+
- 当前 active runtime 唯一映射到哪个 provider
|
|
114
|
+
- 若当前 active profile 有歧义,为什么没有 current 标记
|
|
115
|
+
|
|
116
|
+
`list --json` 必须保持现有顶层 envelope 不变,并维持 `data.count` 与 `data.providers` 两个既有入口。
|
|
117
|
+
|
|
118
|
+
每个 provider 项必须追加:
|
|
119
|
+
|
|
120
|
+
- `providerType`
|
|
121
|
+
- 公开值固定为 `direct | copilot`
|
|
122
|
+
- `isActive`
|
|
123
|
+
- 仅在当前 active provider 可唯一解析,且该 provider 正是当前项时为 `true`
|
|
124
|
+
|
|
125
|
+
`list` 的列表级只读元数据必须追加:
|
|
126
|
+
|
|
127
|
+
- `currentProfile`
|
|
128
|
+
- `activeProvider`
|
|
129
|
+
- `activeProviderResolvable`
|
|
130
|
+
- `activeProviderCandidates`
|
|
131
|
+
|
|
132
|
+
判定规则固定为:
|
|
133
|
+
|
|
134
|
+
- `runtime.kind === "copilot-sdk-bridge"` => `providerType = "copilot"`
|
|
135
|
+
- 其他 provider => `providerType = "direct"`
|
|
136
|
+
- 若当前 active profile 映射到多个 provider,则:
|
|
137
|
+
- `activeProviderResolvable = false`
|
|
138
|
+
- 不允许把任何单个 provider 的 `isActive` 置为 `true`
|
|
139
|
+
- human-readable 输出与交互选择器应单独提示 ambiguous 状态
|
|
140
|
+
|
|
141
|
+
human-readable `list` 的目标语义不要求逐字一致,但必须同时体现:
|
|
142
|
+
|
|
143
|
+
- provider 名
|
|
144
|
+
- provider 类型
|
|
145
|
+
- current 标记
|
|
146
|
+
- profile
|
|
147
|
+
|
|
148
|
+
共享 provider 选择器必须复用同一套 provider 可见性语义,至少在 hint 中展示:
|
|
149
|
+
|
|
150
|
+
- `profile`
|
|
151
|
+
- `providerType`
|
|
152
|
+
- `current` 标记,仅在唯一解析时出现
|
|
153
|
+
|
|
154
|
+
### 5.4 `login copilot`
|
|
155
|
+
|
|
156
|
+
`login copilot` 的 help 与成功输出必须同时体现真实实现边界:
|
|
157
|
+
|
|
158
|
+
- upstream:`copilot`
|
|
159
|
+
- 是否需要或已完成 SDK 安装
|
|
160
|
+
- 是否调用过 official Copilot CLI 登录流程
|
|
161
|
+
- 是否通过 auth recheck 确认 ready
|
|
162
|
+
|
|
163
|
+
外部文案必须明确当前实现是:
|
|
164
|
+
|
|
165
|
+
- bundled runtime CLI 优先
|
|
166
|
+
- PATH fallback
|
|
167
|
+
- recheck 成功才算登录成功
|
|
168
|
+
|
|
169
|
+
### 5.5 `status`
|
|
170
|
+
|
|
171
|
+
`status` 的人类可读输出不要求逐字一致,但必须能让用户快速回答以下问题:
|
|
172
|
+
|
|
173
|
+
- 当前 target `codexDir` 是什么
|
|
174
|
+
- 当前 tool home root 是什么
|
|
175
|
+
- 当前 profile 是什么
|
|
176
|
+
- 当前映射 provider 是什么
|
|
177
|
+
- 当前是 direct provider 还是 Copilot provider 路径
|
|
178
|
+
- runtime health 是否正常
|
|
179
|
+
- 是否存在 warning,以及下一步建议是什么
|
|
180
|
+
|
|
181
|
+
`status` 不应继续只是字段堆砌。它必须是一个面向人类的“当前配置与健康摘要”。
|
|
182
|
+
|
|
183
|
+
### 5.6 `doctor`
|
|
184
|
+
|
|
185
|
+
`doctor` 的人类可读输出必须先给整体健康结论,再给逐条 issue,并且 issue 文案面向下一步修复动作,而不是只输出“发现问题”。
|
|
186
|
+
|
|
187
|
+
目标语义:
|
|
188
|
+
|
|
189
|
+
- 先给 overall health / summary
|
|
190
|
+
- 再列 issue
|
|
191
|
+
- 每条 issue 尽量说明影响和建议动作
|
|
192
|
+
|
|
193
|
+
### 5.7 `migrate` 与 `setup`
|
|
194
|
+
|
|
195
|
+
`migrate` 的 help 语义必须固定为:
|
|
196
|
+
|
|
197
|
+
- interactive adopt helper
|
|
198
|
+
- 面向已有运行态
|
|
199
|
+
- 不是所有新用户的起步命令
|
|
200
|
+
|
|
201
|
+
`setup` 的 help 语义必须固定为:
|
|
202
|
+
|
|
203
|
+
- deprecated
|
|
204
|
+
- 保留现有 contract
|
|
205
|
+
- 不再出现在主路径示例中
|
|
206
|
+
|
|
207
|
+
## 6. 允许的内部清理
|
|
208
|
+
|
|
209
|
+
以下内部清理在 `0.0.12` 允许进行,但它们是可选项,不是本版主交付。
|
|
210
|
+
|
|
211
|
+
### 6.1 `src/commands/dispatch.ts`
|
|
212
|
+
|
|
213
|
+
允许继续保持“先 resolve tool home,再读 tool config,再 resolve codexDir”的唯一入口模型,也允许做小范围收口以强化这一点。
|
|
214
|
+
|
|
215
|
+
不允许:
|
|
216
|
+
|
|
217
|
+
- 把这套解析逻辑重新散回各个 handler
|
|
218
|
+
- 借清理之名改变命令交互边界
|
|
219
|
+
|
|
220
|
+
### 6.2 `src/infra/codex-paths.ts` 与同类 facade
|
|
221
|
+
|
|
222
|
+
允许继续保留,也允许在不扩散影响的前提下做小范围收拢。
|
|
223
|
+
|
|
224
|
+
不允许:
|
|
225
|
+
|
|
226
|
+
- 把这类清理扩成跨仓库重命名工程
|
|
227
|
+
- 借机重做整个 infra 分层
|
|
228
|
+
|
|
229
|
+
### 6.3 `src/commands/handlers.ts`
|
|
230
|
+
|
|
231
|
+
若实现时需要拆小 helper,可以做。
|
|
232
|
+
|
|
233
|
+
前提:
|
|
234
|
+
|
|
235
|
+
- 不改变 command id
|
|
236
|
+
- 不改变公开 flags
|
|
237
|
+
- 不改变错误码
|
|
238
|
+
- 不改变交互边界
|
|
239
|
+
|
|
240
|
+
### 6.4 仅在输出数据不够时允许补最小数据
|
|
241
|
+
|
|
242
|
+
以下文件不是默认必改,但 design 必须明确只有在 `src/cli/output.ts` 无法表达目标语义时才允许触碰:
|
|
243
|
+
|
|
244
|
+
- `src/commands/handlers.ts`
|
|
245
|
+
- `src/app/get-status.ts`
|
|
246
|
+
- `src/app/run-doctor.ts`
|
|
247
|
+
|
|
248
|
+
允许修改的前提:
|
|
249
|
+
|
|
250
|
+
1. 仅当 `output.ts` 依赖的现有 `data` 不足以表达本设计要求。
|
|
251
|
+
2. 只允许追加字段,或收紧 message / warning 文案。
|
|
252
|
+
3. 不允许改顶层 JSON envelope。
|
|
253
|
+
4. 不允许改 issue code / error code 的分类空间。
|
|
254
|
+
5. 不允许重做 `doctor` / `status` 的 data shape。
|
|
255
|
+
|
|
256
|
+
优先级固定为:
|
|
257
|
+
|
|
258
|
+
1. 先只改 `src/cli/output.ts`
|
|
259
|
+
2. 不够再补 app data
|
|
260
|
+
3. 绝不为了“更好看”重构公开 JSON
|
|
261
|
+
|
|
262
|
+
## 7. 测试与发布检查
|
|
263
|
+
|
|
264
|
+
`0.0.12` 的测试与验证必须写成可执行清单,不能只写“跑一下 test”。
|
|
265
|
+
|
|
266
|
+
### 7.1 自动化测试修改点
|
|
267
|
+
|
|
268
|
+
`tests/commands.spec.js` 必须增加或更新:
|
|
269
|
+
|
|
270
|
+
- 顶层 help 示例顺序
|
|
271
|
+
- `migrate` 的降权表述
|
|
272
|
+
- `login` help 说明中的 bundled / PATH fallback 语义
|
|
273
|
+
- `setup` 仍为 deprecation-only
|
|
274
|
+
|
|
275
|
+
`tests/cli-e2e.spec.js` 必须增加或更新:
|
|
276
|
+
|
|
277
|
+
- built CLI `--help` 对主路径的可见性
|
|
278
|
+
- `list --json` 返回 `providerType` 与 `isActive`
|
|
279
|
+
- human-readable `list` 显示 provider type 与 current 标记
|
|
280
|
+
- human-readable `init` 输出不再出现旧 `createdCodexDir` 语义
|
|
281
|
+
- human-readable `status` 输出出现 tool home / target runtime
|
|
282
|
+
- human-readable `doctor` 输出具有修复导向
|
|
283
|
+
- `--version` 等于 `package.json.version`
|
|
284
|
+
|
|
285
|
+
`tests/workflows.spec.js` 只补行为不回归断言:
|
|
286
|
+
|
|
287
|
+
- `status.data.storage` 仍稳定
|
|
288
|
+
- `doctor.data.issues` code 仍稳定
|
|
289
|
+
- current provider ambiguous 时,`list` 不伪造 current 标记
|
|
290
|
+
- direct provider / Copilot provider 主路径行为不回归
|
|
291
|
+
|
|
292
|
+
### 7.2 人工验证与发布检查
|
|
293
|
+
|
|
294
|
+
发布前必须执行并记录结果:
|
|
295
|
+
|
|
296
|
+
- `npm run build`
|
|
297
|
+
- `npm test`
|
|
298
|
+
- `npx tsc --noEmit`
|
|
299
|
+
- `npm pack --dry-run`
|
|
300
|
+
- built CLI `--help`
|
|
301
|
+
- built CLI `--version`
|
|
302
|
+
- fresh tool home direct path
|
|
303
|
+
- fresh tool home Copilot path
|
|
304
|
+
- read commands in `--json`
|
|
305
|
+
- write commands in sandbox
|
|
306
|
+
|
|
307
|
+
### 7.3 测试限制说明
|
|
308
|
+
|
|
309
|
+
测试文档必须明确:
|
|
310
|
+
|
|
311
|
+
- `login copilot` 涉及 SDK、official CLI invocation 与 auth recheck,自动化覆盖要区分可模拟部分与真实环境依赖部分
|
|
312
|
+
- `migrate` 是高级 adopt helper,其测试重点是定位与边界,不要求把它包装成完整无交互主流程
|
|
313
|
+
|
|
314
|
+
## 8. 验收标准
|
|
315
|
+
|
|
316
|
+
`0.0.12` 完成的判断标准必须是用户可感知结果,而不是“改了哪些文件”。
|
|
317
|
+
|
|
318
|
+
达到以下条件时,本版才算完成:
|
|
319
|
+
|
|
320
|
+
- README 和 `codexs --help` 第一屏能看出 direct / Copilot 主路径
|
|
321
|
+
- `migrate` 不再和 fresh install 主入口混淆
|
|
322
|
+
- 不再有活跃文档声称 `providers.json` / `backups/` 位于 `~/.codex`
|
|
323
|
+
- `list` 和共享 provider 选择器能看出 provider 属于 direct 还是 Copilot
|
|
324
|
+
- 只有当前 active provider 可唯一解析时,当前 provider 才会被标记
|
|
325
|
+
- `init` / `status` / `doctor` 的人类输出不再带旧语义
|
|
326
|
+
- `package.json`、README、CHANGELOG、PRD、design 的版本线一致
|
|
327
|
+
- 历史长期文档被降格为参考资料,不再冒充当前事实源
|
|
328
|
+
|
|
329
|
+
## 9. 明确不碰
|
|
330
|
+
|
|
331
|
+
以下边界在 `0.0.12` 必须写死,避免实现越界:
|
|
332
|
+
|
|
333
|
+
- 不新增顶级命令族
|
|
334
|
+
- 不新增 upstream
|
|
335
|
+
- 不把 `migrate` 做成完整非交互产品
|
|
336
|
+
- 不删除 `migrate`
|
|
337
|
+
- 不删除 `setup`
|
|
338
|
+
- 不改 `--json` 顶层 envelope:`ok / command / data / warnings / error`
|
|
339
|
+
- 不改 command ids、公开 flags、命令名
|
|
340
|
+
- 不改 Copilot bridge 运行机制
|
|
341
|
+
- 不加自动迁移、双读双写、旧布局兼容层
|
|
342
|
+
- 不完整重写历史 PRD、历史 versioned docs、旧 test report
|
|
343
|
+
- 不改 `docs/PRD/codex-switch-prd.md` 正文内容,只通过活跃文档绕开它作为最新事实源
|