feat: replace desktop membership with permanent point wallet

This commit is contained in:
2026-09-22 11:43:04 +08:00
parent 2f82b9f79c
commit a1cce428af
24 changed files with 1343 additions and 1707 deletions

View File

@@ -1,3 +1,4 @@
import { handleWorksBillingRoutes } from './works-billing';
import type { IncomingMessage, ServerResponse } from 'http';
import { randomUUID } from 'node:crypto';
import { app } from 'electron';
@@ -60,35 +61,6 @@ const AGENT_AVATAR_MIME_TYPES = new Set(['image/png', 'image/jpeg', 'image/webp'
const MAX_PROJECT_COVER_BYTES = 10 * 1024 * 1024;
const PROJECT_COVER_MIME_TYPES = new Set(['image/png', 'image/jpeg', 'image/webp']);
const SAFE_PROJECT_STATUSES = new Set(['draft', 'published']);
const TOKEN_POINT_FIELDS = [
'weekly_allowance',
'weekly_used',
'weekly_reserved',
'weekly_remaining',
'permanent_total',
'permanent_used',
'permanent_reserved',
'permanent_remaining',
'total_remaining',
] as const;
const TOKEN_POINT_METADATA_FIELDS = [
'plan_code',
'plan_name',
'cycle_start',
'cycle_end',
'next_refresh_at',
] as const;
const TOKEN_POINT_ENTITLEMENT_SOURCES = new Set(['self', 'family_owner', 'shared_group']);
const TOKEN_POINT_UPGRADE_ACTIONS = new Set(['self_service', 'contact_family_owner']);
const TOKEN_POINT_VALUE_PATTERN = /^(?:0|[1-9]\d*)(?:\.\d{1,2})?$/u;
const RESET_CARD_STATUSES = new Set(['available', 'expired', 'redeemed']);
const RESET_CARD_ERROR_MESSAGES: Record<string, string> = {
reset_card_expired: '这张重置卡已过期。',
token_point_wallet_owner_required: '当前额度由家庭管理员管理,无法使用这张重置卡。',
billing_temporarily_paused: '计费服务正在切换,请稍后再试。',
token_point_transaction_conflict: '重置卡状态刚刚发生变化,请刷新后重试。',
};
function readRequiredString(value: unknown, field: string): string {
if (typeof value !== 'string' || !value.trim()) {
throw new Error(`Missing ${field}`);
@@ -201,31 +173,6 @@ async function sendUpstreamError(
});
}
async function sendResetCardUpstreamError(
res: ServerResponse,
response: Response,
): Promise<void> {
const payload = await readResponsePayload(response);
const upstreamCode = getPayloadErrorCode(payload);
const code = upstreamCode && RESET_CARD_ERROR_MESSAGES[upstreamCode]
? upstreamCode
: undefined;
const status = response.status >= 400 && response.status < 500 ? response.status : 502;
const error = code
? RESET_CARD_ERROR_MESSAGES[code]
: response.status === 404
? '重置卡不存在或不可用。'
: response.status === 409
? '当前有待处理的计费操作,暂时不能使用重置卡。'
: '重置卡服务暂时不可用,请稍后重试。';
sendJson(res, status, {
success: false,
status: response.status,
...(code ? { code } : {}),
error,
});
}
function sendPublishSourceFailure(
res: ServerResponse,
status: number,
@@ -324,112 +271,6 @@ function readNullableStringField(
return typeof value === 'string' ? value : undefined;
}
function readNullablePointField(
source: Record<string, unknown>,
field: string,
): string | null | undefined {
const value = source[field];
if (value === undefined || value === null) return null;
if (typeof value !== 'string') return undefined;
const normalized = value.trim();
return TOKEN_POINT_VALUE_PATTERN.test(normalized) ? normalized : undefined;
}
function projectSafeTokenPointBalance(value: unknown): Record<string, unknown> | null {
if (!isRecord(value)) return null;
const entitlementSource = readOptionalString(value.entitlement_source);
const upgradeAction = readOptionalString(value.upgrade_action);
if (
!entitlementSource
|| !TOKEN_POINT_ENTITLEMENT_SOURCES.has(entitlementSource)
|| typeof value.family_shared !== 'boolean'
|| typeof value.can_manage_membership !== 'boolean'
|| !upgradeAction
|| !TOKEN_POINT_UPGRADE_ACTIONS.has(upgradeAction)
) return null;
if (
value.shared_available !== undefined
&& value.shared_available !== null
&& typeof value.shared_available !== 'boolean'
) return null;
const canManageMembership = value.can_manage_membership;
if (!canManageMembership && typeof value.shared_available !== 'boolean') return null;
const projected: Record<string, unknown> = {};
for (const field of TOKEN_POINT_METADATA_FIELDS) {
const fieldValue = canManageMembership ? readNullableStringField(value, field) : null;
if (fieldValue === undefined) return null;
projected[field] = fieldValue;
}
for (const field of TOKEN_POINT_FIELDS) {
const fieldValue = canManageMembership ? readNullablePointField(value, field) : null;
if (fieldValue === undefined) return null;
projected[field] = fieldValue;
}
return {
...projected,
entitlement_source: entitlementSource,
family_shared: value.family_shared,
can_manage_membership: canManageMembership,
upgrade_action: upgradeAction,
shared_available: canManageMembership ? null : value.shared_available,
};
}
function readResetCardTimestamp(value: unknown): string | null {
if (typeof value !== 'string' || !value.trim()) return null;
const normalized = value.trim();
return Number.isNaN(Date.parse(normalized)) ? null : normalized;
}
function projectSafeResetCard(value: unknown): Record<string, unknown> | null {
if (!isRecord(value)) return null;
const id = readOptionalString(value.id);
const status = readOptionalString(value.status);
const grantedAt = readResetCardTimestamp(value.granted_at);
const expiresAt = readResetCardTimestamp(value.expires_at);
const redeemedAt = value.redeemed_at === null
? null
: readResetCardTimestamp(value.redeemed_at);
const redeemedCycleId = readNullableStringField(value, 'redeemed_cycle_id');
if (
!id
|| !status
|| !RESET_CARD_STATUSES.has(status)
|| !grantedAt
|| !expiresAt
|| redeemedAt === null && value.redeemed_at !== null
|| redeemedCycleId === undefined
) return null;
if (status === 'redeemed') {
if (!redeemedAt || !redeemedCycleId) return null;
} else if (redeemedAt !== null || redeemedCycleId !== null) {
return null;
}
return {
id,
status: status === 'available' && Date.parse(expiresAt) <= Date.now() ? 'expired' : status,
granted_at: grantedAt,
expires_at: expiresAt,
redeemed_at: redeemedAt,
redeemed_cycle_id: redeemedCycleId,
};
}
function projectSafeResetCardPage(value: unknown): Record<string, unknown>[] | null {
if (!isRecord(value) || !Array.isArray(value.items)) return null;
const cards = value.items.map(projectSafeResetCard);
return cards.some((card) => card === null)
? null
: cards as Record<string, unknown>[];
}
function projectSafeProject(
value: unknown,
options: { requireStatus?: boolean } = {},
@@ -777,85 +618,6 @@ async function handleListMyProjects(
sendJson(res, response.status, { success: true, page });
}
async function handleGetBillingTokenPoints(
req: IncomingMessage,
res: ServerResponse,
): Promise<void> {
const accessToken = readRequiredHeader(req, 'x-niancode-access-token');
const response = await proxyAwareFetch(createWorksUrl('/api/billing/points').toString(), {
method: 'GET',
headers: {
Authorization: `Bearer ${accessToken}`,
},
});
if (!response.ok) {
await sendUpstreamError(res, response, `Works Square token points failed (${response.status})`);
return;
}
const points = projectSafeTokenPointBalance(await readResponsePayload(response));
if (!points) {
sendJson(res, 502, { success: false, error: 'Works Square returned an invalid token point balance' });
return;
}
sendJson(res, response.status, { success: true, points });
}
async function handleListBillingResetCards(
req: IncomingMessage,
res: ServerResponse,
): Promise<void> {
const accessToken = readRequiredHeader(req, 'x-niancode-access-token');
const response = await proxyAwareFetch(createWorksUrl('/api/billing/reset-cards').toString(), {
method: 'GET',
headers: {
Authorization: `Bearer ${accessToken}`,
},
});
if (!response.ok) {
await sendResetCardUpstreamError(res, response);
return;
}
const cards = projectSafeResetCardPage(await readResponsePayload(response));
if (!cards) {
sendJson(res, 502, { success: false, error: 'Works Square returned an invalid reset-card list' });
return;
}
sendJson(res, response.status, { success: true, cards });
}
async function handleRedeemBillingResetCard(
req: IncomingMessage,
res: ServerResponse,
cardId: string,
): Promise<void> {
const accessToken = readRequiredHeader(req, 'x-niancode-access-token');
const response = await proxyAwareFetch(
createWorksUrl(`/api/billing/reset-cards/${encodeURIComponent(readRequiredString(cardId, 'card_id'))}/redeem`).toString(),
{
method: 'POST',
headers: {
Authorization: `Bearer ${accessToken}`,
},
},
);
if (!response.ok) {
await sendResetCardUpstreamError(res, response);
return;
}
const card = projectSafeResetCard(await readResponsePayload(response));
if (!card) {
sendJson(res, 502, { success: false, error: 'Works Square returned an invalid reset-card result' });
return;
}
sendJson(res, response.status, { success: true, card });
}
async function handleAgentProfile(
req: IncomingMessage,
res: ServerResponse,
@@ -1620,6 +1382,7 @@ export async function handleWorksRoutes(
}
try {
if (await handleWorksBillingRoutes(req, res, url)) return true;
if (url.pathname === '/api/works/user/avatar' && (req.method === 'POST' || req.method === 'DELETE')) {
await handleAgentAvatar(req, res);
return true;
@@ -1635,22 +1398,6 @@ export async function handleWorksRoutes(
return true;
}
if (url.pathname === '/api/works/billing/points' && req.method === 'GET') {
await handleGetBillingTokenPoints(req, res);
return true;
}
if (url.pathname === '/api/works/billing/reset-cards' && req.method === 'GET') {
await handleListBillingResetCards(req, res);
return true;
}
const resetCardRedeemMatch = url.pathname.match(/^\/api\/works\/billing\/reset-cards\/([^/]+)\/redeem$/);
if (resetCardRedeemMatch && req.method === 'POST') {
await handleRedeemBillingResetCard(req, res, decodeURIComponent(resetCardRedeemMatch[1]));
return true;
}
if (url.pathname === '/api/works/ai-gateway/images/generations' && req.method === 'POST') {
await handleSubmitImageGeneration(req, res);
return true;