mirror of
https://github.com/Wei-Shaw/sub2api.git
synced 2026-10-30 00:21:34 +00:00
feat(codex-detect): codex_cli_only 检测加固 + 引擎指纹统一信号列表 + 账号级 app-server
将 codex_cli_only 客户端识别从「单一 strict 开关 + 固定 OR 头集合」重构为 可逐项管理的引擎指纹信号列表,加固整条判定链,并补齐账号级 app-server 控制、 对齐前端设置文案。 判定链(每步可短路): - 账号未开 codex_cli_only → 不限制;gateway.force_codex_cli → 旁路放行 - 全局黑名单命中(OR 宽 deny)→ 立即拒 - 身份候选:官方 UA(strict,仅前缀)/ 官方 originator(OR)/ 全局白名单(双因子 AND) / 全局 app-server 开关 OR 账号 app-server 开关;均不命中 → 拒 - 版本门(仅官方候选):UA 须可解析引擎版本,再校验 [min,max] 区间 - 引擎指纹 AND 硬门:按信号列表逐条勾选 AND、每条行内变体 OR;无 Required 信号 → 放行 引擎指纹信号列表(唯一真源) - 新增 openai.EngineFingerprintSignal 类型 + EvaluateEngineFingerprint 求值器 (勾选 AND / 行内变体 OR / 无勾选 → 放行) - CodexRestrictionPolicy 增 EngineFingerprintSignals;信号列表单一决定是否启用指纹门, 不再保留独立「要求引擎指纹」总开关(与「信号全不选」语义重复) - 新设置键 codex_cli_only_engine_fingerprint_signals(默认只勾 x-codex- 前缀); 旧 body 指纹开关幂等迁移并入信号列表;wire 接线 - 黑/白名单自由条目、命名预设、版本区间 全局设置管线 - gateway 缺 settingService(仅测试/误配可达)时指纹门回退默认种子信号、失败关闭, 不再因零值 policy(nil 信号)失败开放 账号级 Codex app-server(替换已失效的 ClaudeCode 放行机制) - account.IsCodexCLIOnlyAppServerAllowed() 读 extra.codex_cli_only_allow_app_server, 仅在 codex_cli_only 开启时生效;候选身份门「全局 OR 账号」,与旧系统双层控制对齐 - 移除已无入口的 claude_code 预设机制(allowedClientRegistry / MatchAllowedClients / 账号 GetCodexCLIOnlyAllowedClients / reason);白名单 AllowedClientEntry / IsAllowedClientMatch 保留 门加固(反伪 + 写入校验) - 官方 UA 访问门改 strict:IsCodexOfficialClientRequestStrict 仅前缀匹配,收窄「浏览器前缀 + 中段 codex token」伪造面(strict 仍保留 Codex 家族前缀与 UA 尾部兜底,故对「任意前缀 + 官方尾部 (name;ver)」仍放行——与 UA 可伪造、真正反伪靠引擎指纹门的设计一致) - 官方客户端识别扩展:新增 codex-tui/、codex_vscode_copilot/ 前缀 + UA 尾部 (name;ver) 兜底 (恢复 CODEX_INTERNAL_ORIGINATOR_OVERRIDE 的真实 client,如 cccc→codex-tui),originator 改 精确集。该识别经 IsCodexOfficialClientByHeaders 被 passthrough 复用,故透传的官方判定一并 修正(codex-tui 等不再被误改写 UA)——非「行为不变」,属有意修正 - 白名单写入校验 ValidateCodexWhitelistEntriesJSON + AllowedClientEntry.IsWhitelistable: 双因子 AND 条目须可命中(非空 originator + 非空 ua_contains),拒绝写入会静默失效的死规则; 黑名单(OR 宽 deny,允许 originator-only)不受约束 管理端 / 前端 - handler / DTO / settings_view / 契约测试;gateway 接入判定链 - 信号列表编辑器(替换 body 开关)、api 类型、SettingsView;无勾选给常驻警告 - Create/Edit/Bulk 三弹窗「Codex Only」下新增 app-server 开关(OR 合并全局) - 文案:UA/Originator → User-Agent/Originator;黑/白名单重命名为 User-Agent/Originator 黑/白名单; 「允许 App Server 第三方客户端」→「Codex app-server」+ 简介示例;i18n zh/en 同步 - 移除死代码 HasCodex*Fingerprint helper 测试:引擎指纹求值器 / 账号 app-server(OR 语义)/ detector(含 N1 strict、失败关闭)/ 白名单写入校验 / BulkEdit spec 等;后端 build + service/openai/admin 单测全绿,前端 vue-tsc + vitest 全绿。 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
f93a6c50ce
commit
819fda34d9
@@ -3724,6 +3724,254 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Codex Settings -->
|
||||
<div class="card">
|
||||
<div
|
||||
class="border-b border-gray-100 px-6 py-4 dark:border-dark-700"
|
||||
>
|
||||
<h2 class="text-lg font-semibold text-gray-900 dark:text-white">
|
||||
{{ t("admin.settings.gatewayForwarding.codexHardeningTitle") }}
|
||||
</h2>
|
||||
</div>
|
||||
<div class="p-6 space-y-4">
|
||||
<div>
|
||||
<h3 class="text-base font-semibold text-gray-900 dark:text-white">
|
||||
{{ t("admin.settings.gatewayForwarding.codexClientRestrictionTitle") }}
|
||||
</h3>
|
||||
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">
|
||||
{{ t("admin.settings.gatewayForwarding.codexHardeningDesc") }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="grid gap-4 sm:grid-cols-2">
|
||||
<div>
|
||||
<label
|
||||
class="mb-2 block text-sm font-medium text-gray-700 dark:text-gray-300"
|
||||
>
|
||||
{{ t("admin.settings.gatewayForwarding.minCodexVersion") }}
|
||||
</label>
|
||||
<input
|
||||
v-model="form.min_codex_version"
|
||||
type="text"
|
||||
class="input w-full font-mono text-sm"
|
||||
:placeholder="
|
||||
t(
|
||||
'admin.settings.gatewayForwarding.minCodexVersionPlaceholder',
|
||||
)
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label
|
||||
class="mb-2 block text-sm font-medium text-gray-700 dark:text-gray-300"
|
||||
>
|
||||
{{ t("admin.settings.gatewayForwarding.maxCodexVersion") }}
|
||||
</label>
|
||||
<input
|
||||
v-model="form.max_codex_version"
|
||||
type="text"
|
||||
class="input w-full font-mono text-sm"
|
||||
:placeholder="
|
||||
t(
|
||||
'admin.settings.gatewayForwarding.maxCodexVersionPlaceholder',
|
||||
)
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400">
|
||||
{{ t("admin.settings.gatewayForwarding.codexVersionHint") }}
|
||||
</p>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||
{{ t("admin.settings.gatewayForwarding.codexFingerprintSignals") }}
|
||||
</label>
|
||||
<p class="mb-2 mt-1 text-xs text-gray-500 dark:text-gray-400">
|
||||
{{ t("admin.settings.gatewayForwarding.codexFingerprintSignalsDesc") }}
|
||||
</p>
|
||||
<div
|
||||
v-for="(row, i) in codexFingerprintRows"
|
||||
:key="`codex-fp-${i}`"
|
||||
class="mb-2 flex items-center gap-2"
|
||||
>
|
||||
<select v-model="row.type" class="input w-32 text-sm">
|
||||
<option value="header_exact">{{ t("admin.settings.gatewayForwarding.codexFpTypeHeaderExact") }}</option>
|
||||
<option value="header_prefix">{{ t("admin.settings.gatewayForwarding.codexFpTypeHeaderPrefix") }}</option>
|
||||
<option value="body_path">{{ t("admin.settings.gatewayForwarding.codexFpTypeBodyPath") }}</option>
|
||||
</select>
|
||||
<input
|
||||
v-model="row.match"
|
||||
type="text"
|
||||
class="input flex-1 font-mono text-sm"
|
||||
:placeholder="t('admin.settings.gatewayForwarding.codexFpMatchPlaceholder')"
|
||||
/>
|
||||
<label class="flex shrink-0 items-center gap-1 text-xs text-gray-600 dark:text-gray-400">
|
||||
<input v-model="row.required" type="checkbox" />
|
||||
{{ t("admin.settings.gatewayForwarding.codexFpRequired") }}
|
||||
</label>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-secondary btn-sm shrink-0 text-red-600 hover:text-red-700 dark:text-red-400"
|
||||
@click="removeCodexFingerprintRow(i)"
|
||||
>
|
||||
{{ t("admin.settings.gatewayForwarding.codexRemoveRow") }}
|
||||
</button>
|
||||
</div>
|
||||
<button type="button" class="btn btn-secondary btn-sm" @click="addCodexFingerprintRow">
|
||||
{{ t("admin.settings.gatewayForwarding.codexAddRow") }}
|
||||
</button>
|
||||
<p
|
||||
v-if="codexFingerprintNoRequired"
|
||||
class="mt-2 text-xs text-amber-600 dark:text-amber-500"
|
||||
>
|
||||
{{ t("admin.settings.gatewayForwarding.codexFingerprintNoRequiredWarn") }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="pr-4">
|
||||
<label
|
||||
class="block text-sm font-medium text-gray-700 dark:text-gray-300"
|
||||
>
|
||||
{{
|
||||
t("admin.settings.gatewayForwarding.codexAllowAppServer")
|
||||
}}
|
||||
</label>
|
||||
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">
|
||||
{{
|
||||
t(
|
||||
"admin.settings.gatewayForwarding.codexAllowAppServerDesc",
|
||||
)
|
||||
}}
|
||||
</p>
|
||||
</div>
|
||||
<Toggle
|
||||
v-model="form.codex_cli_only_allow_app_server_clients"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label
|
||||
class="block text-sm font-medium text-gray-700 dark:text-gray-300"
|
||||
>
|
||||
{{ t("admin.settings.gatewayForwarding.codexBlacklist") }}
|
||||
</label>
|
||||
<p class="mb-2 mt-1 text-xs text-gray-500 dark:text-gray-400">
|
||||
{{ t("admin.settings.gatewayForwarding.codexBlacklistDesc") }}
|
||||
</p>
|
||||
<div
|
||||
v-for="(row, i) in codexBlacklistRows"
|
||||
:key="`codex-bl-${i}`"
|
||||
class="mb-2 flex gap-2"
|
||||
>
|
||||
<input
|
||||
v-model="row.originator"
|
||||
type="text"
|
||||
class="input w-1/3 font-mono text-sm"
|
||||
:placeholder="
|
||||
t(
|
||||
'admin.settings.gatewayForwarding.codexOriginatorPlaceholder',
|
||||
)
|
||||
"
|
||||
/>
|
||||
<input
|
||||
v-model="row.uaContains"
|
||||
type="text"
|
||||
class="input flex-1 font-mono text-sm"
|
||||
:placeholder="
|
||||
t(
|
||||
'admin.settings.gatewayForwarding.codexUaContainsPlaceholder',
|
||||
)
|
||||
"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-secondary btn-sm shrink-0 text-red-600 hover:text-red-700 dark:text-red-400"
|
||||
@click="removeCodexBlacklistRow(i)"
|
||||
>
|
||||
{{ t("admin.settings.gatewayForwarding.codexRemoveRow") }}
|
||||
</button>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-secondary btn-sm"
|
||||
@click="addCodexBlacklistRow"
|
||||
>
|
||||
{{ t("admin.settings.gatewayForwarding.codexAddRow") }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label
|
||||
class="block text-sm font-medium text-gray-700 dark:text-gray-300"
|
||||
>
|
||||
{{ t("admin.settings.gatewayForwarding.codexWhitelist") }}
|
||||
</label>
|
||||
<p class="mb-2 mt-1 text-xs text-gray-500 dark:text-gray-400">
|
||||
{{ t("admin.settings.gatewayForwarding.codexWhitelistDesc") }}
|
||||
</p>
|
||||
<div
|
||||
v-for="(row, i) in codexWhitelistRows"
|
||||
:key="`codex-wl-${i}`"
|
||||
class="mb-2 flex gap-2"
|
||||
>
|
||||
<input
|
||||
v-model="row.originator"
|
||||
type="text"
|
||||
class="input w-1/3 font-mono text-sm"
|
||||
:placeholder="
|
||||
t(
|
||||
'admin.settings.gatewayForwarding.codexOriginatorPlaceholder',
|
||||
)
|
||||
"
|
||||
/>
|
||||
<input
|
||||
v-model="row.uaContains"
|
||||
type="text"
|
||||
class="input flex-1 font-mono text-sm"
|
||||
:placeholder="
|
||||
t(
|
||||
'admin.settings.gatewayForwarding.codexUaContainsPlaceholder',
|
||||
)
|
||||
"
|
||||
/>
|
||||
<label
|
||||
class="flex shrink-0 items-center gap-1 text-xs text-gray-600 dark:text-gray-400"
|
||||
:title="
|
||||
t(
|
||||
'admin.settings.gatewayForwarding.codexWhitelistSkipFingerprintTooltip',
|
||||
)
|
||||
"
|
||||
>
|
||||
<input
|
||||
v-model="row.skipEngineFingerprint"
|
||||
type="checkbox"
|
||||
/>
|
||||
{{
|
||||
t(
|
||||
'admin.settings.gatewayForwarding.codexWhitelistSkipFingerprint',
|
||||
)
|
||||
}}
|
||||
</label>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-secondary btn-sm shrink-0 text-red-600 hover:text-red-700 dark:text-red-400"
|
||||
@click="removeCodexWhitelistRow(i)"
|
||||
>
|
||||
{{ t("admin.settings.gatewayForwarding.codexRemoveRow") }}
|
||||
</button>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-secondary btn-sm"
|
||||
@click="addCodexWhitelistRow"
|
||||
>
|
||||
{{ t("admin.settings.gatewayForwarding.codexAddRow") }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Gateway Scheduling Settings -->
|
||||
<div class="card">
|
||||
<div
|
||||
@@ -4180,20 +4428,9 @@
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- 是否允许在 Claude Code 中使用 Codex 插件(全局开关) -->
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="pr-4">
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||
{{ t("admin.settings.gatewayForwarding.openaiAllowClaudeCodeCodexPlugin") }}
|
||||
</label>
|
||||
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">
|
||||
{{ t("admin.settings.gatewayForwarding.openaiAllowClaudeCodeCodexPluginDesc") }}
|
||||
</p>
|
||||
</div>
|
||||
<Toggle v-model="form.openai_allow_claude_code_codex_plugin" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Web Search Emulation -->
|
||||
<div class="card">
|
||||
<div
|
||||
@@ -7007,6 +7244,12 @@ import {
|
||||
normalizeRegistrationEmailSuffixDomains,
|
||||
parseRegistrationEmailSuffixWhitelistInput,
|
||||
} from "@/utils/registrationEmailPolicy";
|
||||
import {
|
||||
parseFingerprintSignalsToRows,
|
||||
serializeFingerprintRowsToJSON,
|
||||
defaultFingerprintSignalRows,
|
||||
type FingerprintSignalRow,
|
||||
} from "./codexFingerprintSignals";
|
||||
|
||||
const { t, locale } = useI18n();
|
||||
const appStore = useAppStore();
|
||||
@@ -7837,7 +8080,13 @@ const form = reactive<SettingsForm>({
|
||||
rewrite_message_cache_control: false,
|
||||
antigravity_user_agent_version: "",
|
||||
openai_codex_user_agent: "",
|
||||
openai_allow_claude_code_codex_plugin: false,
|
||||
// codex_cli_only 加固
|
||||
min_codex_version: "",
|
||||
max_codex_version: "",
|
||||
codex_cli_only_blacklist: "",
|
||||
codex_cli_only_whitelist: "",
|
||||
codex_cli_only_allow_app_server_clients: false,
|
||||
codex_cli_only_engine_fingerprint_signals: "",
|
||||
// 余额、订阅到期与账号限额通知
|
||||
balance_low_notify_enabled: false,
|
||||
balance_low_notify_threshold: 0,
|
||||
@@ -8443,6 +8692,82 @@ function parseTablePageSizeOptionsInput(raw: string): number[] | null {
|
||||
return deduped;
|
||||
}
|
||||
|
||||
// ── codex_cli_only 黑/白名单结构化编辑(行 ↔ JSON)──
|
||||
interface CodexClientRow {
|
||||
originator: string;
|
||||
uaContains: string; // 逗号分隔,序列化时拆成 ua_contains 数组
|
||||
skipEngineFingerprint?: boolean; // 仅白名单:命中即跳过引擎指纹门
|
||||
}
|
||||
const codexBlacklistRows = ref<CodexClientRow[]>([]);
|
||||
const codexWhitelistRows = ref<CodexClientRow[]>([]);
|
||||
const codexFingerprintRows = ref<FingerprintSignalRow[]>([]);
|
||||
const codexFingerprintNoRequired = computed(
|
||||
() => !codexFingerprintRows.value.some((r) => r.required),
|
||||
);
|
||||
function addCodexFingerprintRow(): void {
|
||||
codexFingerprintRows.value.push({ type: "header_exact", match: "", required: false });
|
||||
}
|
||||
function removeCodexFingerprintRow(i: number): void {
|
||||
codexFingerprintRows.value.splice(i, 1);
|
||||
}
|
||||
|
||||
function parseCodexEntriesToRows(raw: string): CodexClientRow[] {
|
||||
if (!raw || !raw.trim()) return [];
|
||||
try {
|
||||
const arr = JSON.parse(raw);
|
||||
if (!Array.isArray(arr)) return [];
|
||||
return arr.map((e) => ({
|
||||
originator: typeof e?.originator === "string" ? e.originator : "",
|
||||
uaContains: Array.isArray(e?.ua_contains)
|
||||
? e.ua_contains
|
||||
.filter((x: unknown) => typeof x === "string")
|
||||
.join(", ")
|
||||
: "",
|
||||
skipEngineFingerprint: e?.skip_engine_fingerprint === true,
|
||||
}));
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
function serializeCodexRowsToJSON(rows: CodexClientRow[]): string {
|
||||
const entries = rows
|
||||
.map((r) => {
|
||||
const entry: {
|
||||
originator: string;
|
||||
ua_contains: string[];
|
||||
skip_engine_fingerprint?: boolean;
|
||||
} = {
|
||||
originator: r.originator.trim(),
|
||||
ua_contains: r.uaContains
|
||||
.split(",")
|
||||
.map((s) => s.trim())
|
||||
.filter((s) => s.length > 0),
|
||||
};
|
||||
if (r.skipEngineFingerprint) entry.skip_engine_fingerprint = true;
|
||||
return entry;
|
||||
})
|
||||
.filter((e) => e.originator !== "" || e.ua_contains.length > 0);
|
||||
return entries.length > 0 ? JSON.stringify(entries) : "";
|
||||
}
|
||||
|
||||
function addCodexBlacklistRow(): void {
|
||||
codexBlacklistRows.value.push({ originator: "", uaContains: "" });
|
||||
}
|
||||
function removeCodexBlacklistRow(i: number): void {
|
||||
codexBlacklistRows.value.splice(i, 1);
|
||||
}
|
||||
function addCodexWhitelistRow(): void {
|
||||
codexWhitelistRows.value.push({
|
||||
originator: "",
|
||||
uaContains: "",
|
||||
skipEngineFingerprint: false,
|
||||
});
|
||||
}
|
||||
function removeCodexWhitelistRow(i: number): void {
|
||||
codexWhitelistRows.value.splice(i, 1);
|
||||
}
|
||||
|
||||
async function loadSettings() {
|
||||
loading.value = true;
|
||||
loadFailed.value = false;
|
||||
@@ -8465,6 +8790,15 @@ async function loadSettings() {
|
||||
form.claude_oauth_system_prompt,
|
||||
);
|
||||
syncClaudeOAuthSystemPromptBlocksFormField();
|
||||
codexBlacklistRows.value = parseCodexEntriesToRows(
|
||||
form.codex_cli_only_blacklist,
|
||||
);
|
||||
codexWhitelistRows.value = parseCodexEntriesToRows(
|
||||
form.codex_cli_only_whitelist,
|
||||
);
|
||||
codexFingerprintRows.value = form.codex_cli_only_engine_fingerprint_signals
|
||||
? parseFingerprintSignalsToRows(form.codex_cli_only_engine_fingerprint_signals)
|
||||
: defaultFingerprintSignalRows();
|
||||
form.login_agreement_mode =
|
||||
settings.login_agreement_mode === "checkbox" ? "checkbox" : "modal";
|
||||
form.login_agreement_updated_at =
|
||||
@@ -8966,7 +9300,19 @@ async function saveSettings() {
|
||||
form.antigravity_user_agent_version?.trim() || "",
|
||||
openai_codex_user_agent:
|
||||
form.openai_codex_user_agent?.trim() || "",
|
||||
openai_allow_claude_code_codex_plugin: form.openai_allow_claude_code_codex_plugin,
|
||||
min_codex_version: form.min_codex_version?.trim() || "",
|
||||
max_codex_version: form.max_codex_version?.trim() || "",
|
||||
codex_cli_only_allow_app_server_clients:
|
||||
form.codex_cli_only_allow_app_server_clients,
|
||||
codex_cli_only_engine_fingerprint_signals: serializeFingerprintRowsToJSON(
|
||||
codexFingerprintRows.value,
|
||||
),
|
||||
codex_cli_only_blacklist: serializeCodexRowsToJSON(
|
||||
codexBlacklistRows.value,
|
||||
),
|
||||
codex_cli_only_whitelist: serializeCodexRowsToJSON(
|
||||
codexWhitelistRows.value,
|
||||
),
|
||||
// Payment configuration
|
||||
payment_enabled: form.payment_enabled,
|
||||
risk_control_enabled: form.risk_control_enabled,
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import {
|
||||
parseFingerprintSignalsToRows,
|
||||
serializeFingerprintRowsToJSON,
|
||||
} from "../codexFingerprintSignals";
|
||||
|
||||
describe("codex fingerprint signals 行编解码", () => {
|
||||
it("解析: 变体数组 → / 合并字符串", () => {
|
||||
const rows = parseFingerprintSignalsToRows(
|
||||
'[{"type":"header_exact","match":["session-id","session_id"],"required":true}]',
|
||||
);
|
||||
expect(rows).toEqual([
|
||||
{ type: "header_exact", match: "session-id / session_id", required: true },
|
||||
]);
|
||||
});
|
||||
it("序列化: / 合并 → 变体数组, required 透传", () => {
|
||||
const json = serializeFingerprintRowsToJSON([
|
||||
{ type: "header_prefix", match: "x-codex-", required: true },
|
||||
{ type: "body_path", match: " a / b ", required: false },
|
||||
]);
|
||||
expect(JSON.parse(json)).toEqual([
|
||||
{ type: "header_prefix", match: ["x-codex-"], required: true },
|
||||
{ type: "body_path", match: ["a", "b"], required: false },
|
||||
]);
|
||||
});
|
||||
it("空/非法 → 空数组 / [] 串", () => {
|
||||
expect(parseFingerprintSignalsToRows("")).toEqual([]);
|
||||
expect(parseFingerprintSignalsToRows("nope")).toEqual([]);
|
||||
expect(serializeFingerprintRowsToJSON([])).toBe("[]");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,58 @@
|
||||
export type FingerprintSignalType = "header_exact" | "header_prefix" | "body_path";
|
||||
|
||||
export interface FingerprintSignalRow {
|
||||
type: FingerprintSignalType;
|
||||
match: string; // 变体用 " / " 展示与录入
|
||||
required: boolean;
|
||||
}
|
||||
|
||||
const VALID_TYPES: FingerprintSignalType[] = [
|
||||
"header_exact",
|
||||
"header_prefix",
|
||||
"body_path",
|
||||
];
|
||||
|
||||
export function parseFingerprintSignalsToRows(raw: string): FingerprintSignalRow[] {
|
||||
if (!raw || !raw.trim()) return [];
|
||||
try {
|
||||
const arr = JSON.parse(raw);
|
||||
if (!Array.isArray(arr)) return [];
|
||||
return arr.map((e) => ({
|
||||
type: VALID_TYPES.includes(e?.type) ? e.type : "header_exact",
|
||||
match: Array.isArray(e?.match)
|
||||
? e.match.filter((x: unknown) => typeof x === "string").join(" / ")
|
||||
: "",
|
||||
required: e?.required === true,
|
||||
}));
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
export function serializeFingerprintRowsToJSON(rows: FingerprintSignalRow[]): string {
|
||||
const entries = rows
|
||||
.map((r) => ({
|
||||
type: r.type,
|
||||
match: r.match
|
||||
.split("/")
|
||||
.map((s) => s.trim())
|
||||
.filter((s) => s.length > 0),
|
||||
required: r.required === true,
|
||||
}))
|
||||
.filter((e) => e.match.length > 0);
|
||||
return JSON.stringify(entries);
|
||||
}
|
||||
|
||||
export function defaultFingerprintSignalRows(): FingerprintSignalRow[] {
|
||||
return [
|
||||
{ type: "header_prefix", match: "x-codex-", required: true },
|
||||
{ type: "header_exact", match: "session-id / session_id", required: false },
|
||||
{ type: "header_exact", match: "thread-id / thread_id", required: false },
|
||||
{
|
||||
type: "body_path",
|
||||
match:
|
||||
"client_metadata.x-codex-window-id / client_metadata.x-codex-installation-id",
|
||||
required: false,
|
||||
},
|
||||
];
|
||||
}
|
||||
Reference in New Issue
Block a user