Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
138 changes: 83 additions & 55 deletions apps/codex-plus-manager/src/App.tsx

Large diffs are not rendered by default.

50 changes: 31 additions & 19 deletions apps/codex-plus-manager/src/model-windows.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,14 @@ import { describe, it } from "node:test";
import type { RelayProfile } from "./App.tsx";
import {
buildModelWindows,
createModelWindowRow,
modelWindowRowsFromProfile,
modelWindowsMapToText,
modelWindowsTextToMap,
serializeModelWindowRows,
mergeModelWindowRows,
modelWindowTokenError,
validateModelWindowRows,
} from "./model-windows.ts";

// 类型检查:确保 RelayProfile 包含 modelWindows 字段
Expand Down Expand Up @@ -81,21 +84,18 @@ describe("model-windows helpers", () => {

it("modelWindowRowsFromProfile 把模型和窗口合成同一组行", () => {
assert.deepStrictEqual(
modelWindowRowsFromProfile("a\nb\nc", '{"a":"1M","c":"200K"}'),
[
{ model: "a", window: "1M" },
{ model: "b", window: "" },
{ model: "c", window: "200K" },
],
modelWindowRowsFromProfile("a\nb\nc", '{"a":"1M","c":"200K"}')
.map((row) => `${row.model}:${row.window}`),
["a:1M", "b:", "c:200K"],
);
});

it("serializeModelWindowRows 从行控件生成 modelList 和 modelWindows", () => {
assert.deepStrictEqual(
serializeModelWindowRows([
{ model: "a", window: "1M" },
{ model: "", window: "400K" },
{ model: "b", window: "" },
createModelWindowRow("a", "1M"),
createModelWindowRow("", "400K"),
createModelWindowRow("b", ""),
]),
{
modelList: "a\nb",
Expand All @@ -108,19 +108,31 @@ describe("model-windows helpers", () => {
assert.deepStrictEqual(
mergeModelWindowRows(
[
{ model: "deepseek-v4-flash", window: "1M" },
{ model: " ", window: "" },
createModelWindowRow("deepseek-v4-flash", "1M"),
createModelWindowRow(" ", ""),
],
[
{ model: "deepseek-v4-flash", window: "" },
{ model: "deepseek-v4-pro", window: "" },
{ model: " deepseek-v4-pro ", window: "200K" },
createModelWindowRow("deepseek-v4-flash", ""),
createModelWindowRow("deepseek-v4-pro", ""),
createModelWindowRow(" deepseek-v4-pro ", "200K"),
],
),
[
{ model: "deepseek-v4-flash", window: "1M" },
{ model: "deepseek-v4-pro", window: "" },
],
).map((row) => `${row.model}:${row.window}`),
["deepseek-v4-flash:1M", "deepseek-v4-pro:"],
);
});

it("严格接受整数和 K/M 后缀,拒绝小数及其他后缀", () => {
assert.strictEqual(modelWindowTokenError("200K"), null);
assert.strictEqual(modelWindowTokenError("1m"), null);
assert.strictEqual(modelWindowTokenError("1000000"), null);
assert.ok(modelWindowTokenError("200KB"));
assert.ok(modelWindowTokenError("1.5M"));
assert.ok(modelWindowTokenError("0"));
});

it("拒绝没有模型名称的窗口,并保留已有行 id", () => {
const row = createModelWindowRow("a", "1M");
assert.strictEqual(mergeModelWindowRows([row], [])[0].id, row.id);
assert.ok(validateModelWindowRows([createModelWindowRow("", "1M")]));
});
});
40 changes: 36 additions & 4 deletions apps/codex-plus-manager/src/model-windows.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,42 @@ export function modelWindowsTextToMap(modelList: string, modelWindowsText: strin
}

export type ModelWindowRow = {
id: string;
model: string;
window: string;
};

let nextModelWindowRowId = 0;

export function createModelWindowRow(model = "", window = ""): ModelWindowRow {
nextModelWindowRowId += 1;
return { id: `model-window-${nextModelWindowRowId}`, model, window };
}

export function modelWindowTokenError(value: string): string | null {
const token = value.trim();
if (!token) return null;
if (!/^[1-9]\d*(?:K|M)?$/i.test(token)) {
return `上下文窗口“${value}”格式无效,请填写正整数或 K/M 后缀(例如 200K、1M)。`;
}
const suffix = token.slice(-1).toUpperCase();
const multiplier = suffix === "K" ? 1_000n : suffix === "M" ? 1_000_000n : 1n;
const numberText = suffix === "K" || suffix === "M" ? token.slice(0, -1) : token;
if (BigInt(numberText) * multiplier > 9_223_372_036_854_775_807n) {
return `上下文窗口“${value}”超出支持范围。`;
}
return null;
}

export function validateModelWindowRows(rows: ModelWindowRow[]): string | null {
for (const row of rows) {
const error = modelWindowTokenError(row.window);
if (error) return row.model.trim() ? `模型 ${row.model.trim()}:${error}` : error;
if (!row.model.trim() && row.window.trim()) return "填写上下文窗口前必须先填写模型名称。";
}
return null;
}

export function mergeModelWindowRows(
currentRows: ModelWindowRow[],
incomingRows: ModelWindowRow[],
Expand All @@ -39,11 +71,11 @@ export function mergeModelWindowRows(
const model = row.model.trim();
if (!model || seen.has(model)) return;
seen.add(model);
rows.push({ model, window: row.window.trim() });
rows.push({ ...row, model, window: row.window.trim() });
};
currentRows.forEach(append);
incomingRows.forEach(append);
return rows.length ? rows : [{ model: "", window: "" }];
return rows.length ? rows : [createModelWindowRow()];
}

export function modelWindowRowsFromProfile(modelList: string, modelWindows: string): ModelWindowRow[] {
Expand All @@ -57,8 +89,8 @@ export function modelWindowRowsFromProfile(modelList: string, modelWindows: stri
.split("\n")
.map((model) => model.trim())
.filter(Boolean)
.map((model) => ({ model, window: map[model] ?? "" }));
return rows.length ? rows : [{ model: "", window: "" }];
.map((model) => createModelWindowRow(model, map[model] ?? ""));
return rows.length ? rows : [createModelWindowRow()];
}

export function serializeModelWindowRows(rows: ModelWindowRow[]): { modelList: string; modelWindows: string } {
Expand Down
23 changes: 13 additions & 10 deletions crates/codex-plus-core/src/app_paths.rs
Original file line number Diff line number Diff line change
Expand Up @@ -376,6 +376,11 @@ pub(crate) fn is_supported_windows_app_package_name(package_name: &str) -> bool
codex_package_parts(package_name).is_some()
}

pub(crate) fn is_supported_windows_watcher_package_name(package_name: &str) -> bool {
is_supported_windows_app_package_name(package_name)
|| package_name_parts(package_name, "OpenAI.ChatGPT-Desktop").is_some()
}

pub(crate) fn is_supported_app_executable_name(name: &str) -> bool {
name.eq_ignore_ascii_case("Codex.exe") || name.eq_ignore_ascii_case("ChatGPT.exe")
}
Expand Down Expand Up @@ -435,23 +440,21 @@ fn executable_in_dir(dir: &Path) -> Option<PathBuf> {

fn codex_package_parts(package_name: &str) -> Option<(AppPackageSpec, &str, &str)> {
for spec in APP_PACKAGE_SPECS {
let Some(rest) = strip_prefix_ignore_ascii_case(package_name, spec.identity) else {
continue;
};
let Some(rest) = rest.strip_prefix('_') else {
continue;
};
let Some((version, rest)) = rest.split_once('_') else {
continue;
};
let Some((_, publisher_id)) = rest.rsplit_once("__") else {
let Some((version, publisher_id)) = package_name_parts(package_name, spec.identity) else {
continue;
};
return Some((*spec, version, publisher_id));
}
None
}

fn package_name_parts<'a>(package_name: &'a str, identity: &str) -> Option<(&'a str, &'a str)> {
let rest = strip_prefix_ignore_ascii_case(package_name, identity)?.strip_prefix('_')?;
let (version, rest) = rest.split_once('_')?;
let (_, publisher_id) = rest.rsplit_once("__")?;
(!version.is_empty() && !publisher_id.is_empty()).then_some((version, publisher_id))
}

fn strip_prefix_ignore_ascii_case<'a>(value: &'a str, prefix: &str) -> Option<&'a str> {
if value.len() < prefix.len() {
return None;
Expand Down
22 changes: 13 additions & 9 deletions crates/codex-plus-core/src/model_suffix.rs
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ fn parse_window_token(token: &str) -> Option<u64> {
.trim()
.parse::<u64>()
.ok()
.map(|value| value * multiplier)
.and_then(|value| value.checked_mul(multiplier))
.filter(|value| *value > 0)
}

Expand All @@ -92,16 +92,18 @@ pub fn collect_catalog_entries(
.map(str::trim)
.filter(|value| !value.is_empty())
{
let (slug, _) = parse_model_suffix(raw);
let (slug, explicit_window) = parse_model_suffix(raw);
if slug.is_empty() {
continue;
}
if !seen.insert(slug.clone()) {
continue;
}
let suffix_window = model_windows
.get(&slug)
.and_then(|token| parse_window_token(token));
let suffix_window = explicit_window.or_else(|| {
model_windows
.get(&slug)
.and_then(|token| parse_window_token(token))
});
list_entries.push(ModelCatalogEntry {
display_name: slug.clone(),
slug,
Expand All @@ -113,11 +115,13 @@ pub fn collect_catalog_entries(
let current_model = current_model.trim();
let mut entries = Vec::new();
if !current_model.is_empty() {
let (slug, _) = parse_model_suffix(current_model);
let (slug, explicit_window) = parse_model_suffix(current_model);
if !slug.is_empty() {
let suffix_window = model_windows
.get(&slug)
.and_then(|token| parse_window_token(token));
let suffix_window = explicit_window.or_else(|| {
model_windows
.get(&slug)
.and_then(|token| parse_window_token(token))
});
entries.push(ModelCatalogEntry {
display_name: slug.clone(),
slug: slug.clone(),
Expand Down
Loading