feat: 排除内部员工补偿账号
This commit is contained in:
@@ -204,12 +204,19 @@ export function readCodexPoolConfig(): CodexPoolConfig {
|
||||
function readCreditsConfig(value: unknown): CodexPoolCreditsConfig {
|
||||
if (!isRecord(value)) throw new Error(`${codexPoolConfigPath}.credits must be a YAML object`);
|
||||
const timezone = requiredStringConfigField(value, "timezone", "credits");
|
||||
const excludedUsers = readUniqueStringArray(value.excludedUsers, "credits.excludedUsers", true);
|
||||
if (excludedUsers.some((selector) => selector.length > 256 || /[\r\n\0]/u.test(selector))) {
|
||||
throw new Error(`${codexPoolConfigPath}.credits.excludedUsers contains an unsupported selector`);
|
||||
}
|
||||
if (new Set(excludedUsers.map((selector) => selector.toLowerCase())).size !== excludedUsers.length) {
|
||||
throw new Error(`${codexPoolConfigPath}.credits.excludedUsers must not contain case-insensitive duplicates`);
|
||||
}
|
||||
try {
|
||||
new Intl.DateTimeFormat("en-US", { timeZone: timezone }).format(new Date(0));
|
||||
} catch {
|
||||
throw new Error(`${codexPoolConfigPath}.credits.timezone must be a valid IANA timezone`);
|
||||
}
|
||||
return { timezone };
|
||||
return { timezone, excludedUsers };
|
||||
}
|
||||
|
||||
function readProfitConfig(value: unknown): CodexPoolProfitConfig {
|
||||
|
||||
@@ -13,6 +13,7 @@ interface CreditsOptions {
|
||||
activeStartUtc: string | null;
|
||||
activeEndUtc: string | null;
|
||||
timezone: string;
|
||||
excludedUsers: string[];
|
||||
users: string[];
|
||||
amountUsd: number;
|
||||
reason: string;
|
||||
@@ -24,7 +25,7 @@ interface CreditsOptions {
|
||||
export async function codexPoolCredits(config: UniDeskConfig, args: string[]): Promise<Record<string, unknown> | RenderedCliResult> {
|
||||
if (args.includes("--help")) return renderCreditsHelp();
|
||||
const pool = readCodexPoolConfig();
|
||||
const options = parseCreditsOptions(args, pool.credits.timezone);
|
||||
const options = parseCreditsOptions(args, pool.credits);
|
||||
const target = codexPoolRuntimeTarget(options.targetId);
|
||||
const remote = await runRemoteCodexPoolScript(config, "credits", creditsScript(options, pool, target), target);
|
||||
const report = parseJsonOutput(remote.stdout);
|
||||
@@ -41,6 +42,7 @@ export async function codexPoolCredits(config: UniDeskConfig, args: string[]): P
|
||||
activeStartUtc: options.activeStartUtc,
|
||||
activeEndUtc: options.activeEndUtc,
|
||||
timezone: options.timezone,
|
||||
excludedUsers: options.excludedUsers,
|
||||
users: options.users,
|
||||
amountUsd: options.amountUsd,
|
||||
reason: options.reason,
|
||||
@@ -53,7 +55,8 @@ export async function codexPoolCredits(config: UniDeskConfig, args: string[]): P
|
||||
return options.json ? renderCreditsJson(response) : renderCredits(response);
|
||||
}
|
||||
|
||||
function parseCreditsOptions(args: string[], timezone: string): CreditsOptions {
|
||||
function parseCreditsOptions(args: string[], creditsConfig: { timezone: string; excludedUsers: string[] }): CreditsOptions {
|
||||
const { timezone, excludedUsers } = creditsConfig;
|
||||
const [action = "grant", ...rest] = args;
|
||||
if (action !== "grant") throw new Error("credits usage: grant (--active-since 24h|--active-start <ISO> --active-end <ISO>|--users <id-or-email,...>) --amount-usd <amount> --reason <text> [--confirm] [--target <id>] [--json]");
|
||||
let activeSince: string | null = null;
|
||||
@@ -110,7 +113,7 @@ function parseCreditsOptions(args: string[], timezone: string): CreditsOptions {
|
||||
if (confirm && users.length === 0) throw new Error("confirmed credits require the exact --users selectors returned by an active-user dry-run");
|
||||
if (amountUsd === null) throw new Error("credits grant requires --amount-usd");
|
||||
if (reason === null || reason.length === 0 || reason.length > 500 || /[\r\n\0]/u.test(reason)) throw new Error("credits grant requires a single-line --reason up to 500 characters");
|
||||
return { activeSince, activeStart, activeEnd, activeStartUtc, activeEndUtc, timezone, users, amountUsd, reason, confirm, targetId, json };
|
||||
return { activeSince, activeStart, activeEnd, activeStartUtc, activeEndUtc, timezone, excludedUsers, users, amountUsd, reason, confirm, targetId, json };
|
||||
}
|
||||
|
||||
function isIsoDateTime(value: string): boolean {
|
||||
@@ -183,6 +186,7 @@ function renderCreditsHelp(): RenderedCliResult {
|
||||
"Notes:",
|
||||
" Default mode is dry-run. --active-since selects distinct active non-admin users from native usage.",
|
||||
" Offset-free fixed-window values use config/platform-infra/sub2api-codex-pool.yaml#credits.timezone and the interval is [start,end).",
|
||||
" YAML credits.excludedUsers selectors are excluded from both active-window and exact-user grants.",
|
||||
" --confirm performs additive balance writes and then reads every selected user back.",
|
||||
].join("\n"),
|
||||
contentType: "text/plain",
|
||||
@@ -193,8 +197,9 @@ function renderCredits(response: Record<string, unknown>): RenderedCliResult {
|
||||
const report = record(response.report);
|
||||
const summary = record(report.summary);
|
||||
const rows = arrayOfRecords(report.items);
|
||||
const excludedRows = arrayOfRecords(report.excluded);
|
||||
const lines = [
|
||||
`SUB2API USER CREDITS ok=${response.ok === true} mode=${text(report.mode)} selection=${text(record(report.selection).kind)} selected=${text(summary.selected)} amount-usd=${text(summary.amountUsd)} total-usd=${text(summary.totalAmountUsd)}`,
|
||||
`SUB2API USER CREDITS ok=${response.ok === true} mode=${text(report.mode)} selection=${text(record(report.selection).kind)} selected=${text(summary.selected)} excluded=${text(summary.excluded)} amount-usd=${text(summary.amountUsd)} total-usd=${text(summary.totalAmountUsd)}`,
|
||||
`WRITE attempted=${text(summary.writeAttempted)} succeeded=${text(summary.writeSucceeded)} failed=${text(summary.writeFailed)} reconciled=${text(summary.reconciled)} mutation=${text(report.mutation)}`,
|
||||
];
|
||||
const selection = record(report.selection);
|
||||
@@ -211,10 +216,21 @@ function renderCredits(response: Record<string, unknown>): RenderedCliResult {
|
||||
]),
|
||||
]));
|
||||
}
|
||||
if (excludedRows.length > 0) {
|
||||
lines.push("", "EXCLUDED", renderTable([
|
||||
["EMAIL", "USER_ID", "REQUESTS", "REASON", "MATCHED_SELECTOR"],
|
||||
...excludedRows.map((row) => [
|
||||
text(row.email), text(row.userId), text(row.requestCount), text(row.reason), text(row.matchedSelector),
|
||||
]),
|
||||
]));
|
||||
}
|
||||
if (report.mode === "dry-run") {
|
||||
const selectors = Array.isArray(report.confirmationUserIds) ? report.confirmationUserIds.join(",") : "<exact-user-ids>";
|
||||
const confirmationUserIds = Array.isArray(report.confirmationUserIds) ? report.confirmationUserIds : [];
|
||||
const selectors = confirmationUserIds.length > 0 ? confirmationUserIds.join(",") : "<none>";
|
||||
lines.push("", `CONFIRM SELECTORS --users ${selectors}`);
|
||||
lines.push("NEXT: use these exact selectors with --confirm only after explicit user authorization.");
|
||||
lines.push(confirmationUserIds.length > 0
|
||||
? "NEXT: use these exact selectors with --confirm only after explicit user authorization."
|
||||
: "NEXT: no users remain eligible for confirmation.");
|
||||
}
|
||||
return { ok: response.ok === true, command: "platform-infra sub2api codex-pool credits", renderedText: lines.join("\n"), contentType: "text/plain", projection: response };
|
||||
}
|
||||
|
||||
@@ -69,6 +69,15 @@ def credits_active_usage(token, since, active_start, active_end, active_start_ut
|
||||
}
|
||||
page += 1
|
||||
|
||||
def credits_excluded_selector(user, payload):
|
||||
user_id = str(user.get("id")) if user.get("id") is not None else ""
|
||||
email = str(user.get("email") or "").strip().lower()
|
||||
for selector in payload.get("excludedUsers") or []:
|
||||
normalized = str(selector).strip().lower()
|
||||
if normalized == user_id or normalized == email:
|
||||
return str(selector)
|
||||
return None
|
||||
|
||||
def credits_selected_users(token, payload):
|
||||
users = credits_all_users(token)
|
||||
users_by_id = {str(item.get("id")): item for item in users if item.get("id") is not None}
|
||||
@@ -85,8 +94,14 @@ def credits_selected_users(token, payload):
|
||||
excluded = []
|
||||
for user_id, request_count in requests.items():
|
||||
user = users_by_id.get(user_id)
|
||||
excluded_selector = credits_excluded_selector(user, payload) if isinstance(user, dict) else None
|
||||
if not isinstance(user, dict):
|
||||
excluded.append({"userId": user_id, "reason": "user-not-found"})
|
||||
elif excluded_selector is not None:
|
||||
excluded.append({
|
||||
"userId": user.get("id"), "email": user.get("email"), "requestCount": request_count,
|
||||
"reason": "yaml-excluded-user", "matchedSelector": excluded_selector,
|
||||
})
|
||||
elif user.get("role") == "admin":
|
||||
excluded.append({"userId": user.get("id"), "email": user.get("email"), "reason": "admin-excluded"})
|
||||
elif user.get("status") != "active" or user.get("deleted_at") is not None:
|
||||
@@ -95,6 +110,7 @@ def credits_selected_users(token, payload):
|
||||
selected.append((user, request_count))
|
||||
return selected, excluded, selection
|
||||
selected = []
|
||||
excluded = []
|
||||
seen = set()
|
||||
for selector in payload.get("users") or []:
|
||||
user = users_by_id.get(str(selector)) or users_by_email.get(str(selector).strip().lower())
|
||||
@@ -104,8 +120,15 @@ def credits_selected_users(token, payload):
|
||||
if user_id in seen:
|
||||
raise RuntimeError("duplicate user selector: " + str(selector))
|
||||
seen.add(user_id)
|
||||
selected.append((user, None))
|
||||
return selected, [], {"kind": "exact-users", "selectors": payload.get("users") or []}
|
||||
excluded_selector = credits_excluded_selector(user, payload)
|
||||
if excluded_selector is not None:
|
||||
excluded.append({
|
||||
"userId": user.get("id"), "email": user.get("email"), "requestCount": None,
|
||||
"reason": "yaml-excluded-user", "matchedSelector": excluded_selector,
|
||||
})
|
||||
else:
|
||||
selected.append((user, None))
|
||||
return selected, excluded, {"kind": "exact-users", "selectors": payload.get("users") or []}
|
||||
|
||||
def run_credits_grant():
|
||||
payload = json.loads(base64.b64decode(PAYLOAD_B64).decode("utf-8")) if PAYLOAD_B64 else {}
|
||||
|
||||
@@ -221,6 +221,7 @@ export interface CodexPoolProfitConfig {
|
||||
|
||||
export interface CodexPoolCreditsConfig {
|
||||
timezone: string;
|
||||
excludedUsers: string[];
|
||||
}
|
||||
|
||||
export interface CodexPoolConfig {
|
||||
|
||||
Reference in New Issue
Block a user