diff --git a/backend/services/license.go b/backend/services/license.go index c309129..eef98f5 100644 --- a/backend/services/license.go +++ b/backend/services/license.go @@ -306,14 +306,17 @@ func deriveInvalidReason(lic *models.License) string { // LicenseStatusPayload 给前端:始终带回库存证的 sanitized + 是否有效 Pro。 // 无效时带 invalidReason(expired|over_instance|revoked),且不清 LicenseKey。 +// instanceId 始终返回本机指纹(DATA_DIR/instance.id),供离线 LICOFF- 绑码展示。 func LicenseStatusPayload() (map[string]interface{}, bool) { RefreshOpsxLicenseIfDue() current := CurrentLicense() stored := StoredLicense() + instanceID := GetOrCreateInstanceID() if stored == nil { - return map[string]interface{}{"plan": "free"}, false + return map[string]interface{}{"plan": "free", "instanceId": instanceID}, false } out := stored.Sanitized() + out["instanceId"] = instanceID if current != nil { delete(out, "invalidReason") return out, true diff --git a/frontend/src/components/InstanceIdRow.tsx b/frontend/src/components/InstanceIdRow.tsx new file mode 100644 index 0000000..1ad9696 --- /dev/null +++ b/frontend/src/components/InstanceIdRow.tsx @@ -0,0 +1,39 @@ +import { memo, useCallback } from 'react' +import { useTranslation } from 'react-i18next' +import { Copy } from 'lucide-react' +import { toast } from 'sonner' +import { Button } from '@/components/ui/button' + +type Props = { + instanceId?: string | null +} + +/** 实例 ID 展示行:设置授权区与 ProGate 共用;全文 + 一键复制,无 PII。 */ +export const InstanceIdRow = memo(function InstanceIdRow({ instanceId }: Props) { + const { t } = useTranslation('license') + const id = instanceId?.trim() || '' + + const onCopy = useCallback(() => { + if (!id) return + void navigator.clipboard?.writeText(id).then( + () => toast.success(t('instanceIdCopied')), + () => toast.error(t('instanceIdCopyFailed')), + ) + }, [id, t]) + + if (!id) return null + + return ( +
{id}
+
+ {t('instanceIdHint')}
+{t('pricing.teamDesc')}
{t('activateHint')}
+{t('gate.freeHint')}
diff --git a/frontend/src/i18n/locales/en/license.ts b/frontend/src/i18n/locales/en/license.ts index a8ac777..448dac5 100644 --- a/frontend/src/i18n/locales/en/license.ts +++ b/frontend/src/i18n/locales/en/license.ts @@ -7,6 +7,12 @@ export default { expiry: 'Valid until', customer: 'Licensed to', instances: 'Instance limit', + instanceId: 'Instance ID', + instanceIdHint: 'Provide this ID for offline licensing. Machine fingerprint only — no personal data.', + instanceIdCopied: 'Instance ID copied', + instanceIdCopyFailed: 'Copy failed — select manually', + copy: 'Copy', + activateHint: 'Online keys start with LIC-; offline LICOFF- keys must bind the Instance ID above', permanent: 'perpetual', personalTitle: 'Personal', teamTitle: 'Team', diff --git a/frontend/src/i18n/locales/zh/license.ts b/frontend/src/i18n/locales/zh/license.ts index 59cc8dd..59584be 100644 --- a/frontend/src/i18n/locales/zh/license.ts +++ b/frontend/src/i18n/locales/zh/license.ts @@ -7,6 +7,12 @@ export default { expiry: '有效期', customer: '授权对象', instances: '实例上限', + instanceId: '实例 ID', + instanceIdHint: '离线授权需提供此 ID;仅本机指纹,不含个人信息', + instanceIdCopied: '实例 ID 已复制', + instanceIdCopyFailed: '复制失败,请手动选择', + copy: '复制', + activateHint: '在线码以 LIC- 开头;离线码 LICOFF-,须与上方实例 ID 绑定', permanent: '永久', personalTitle: '个人版', teamTitle: '团队版', diff --git a/frontend/src/pages/settings/components/LicenseSection.tsx b/frontend/src/pages/settings/components/LicenseSection.tsx index 6764458..0db1ce1 100644 --- a/frontend/src/pages/settings/components/LicenseSection.tsx +++ b/frontend/src/pages/settings/components/LicenseSection.tsx @@ -6,6 +6,7 @@ import { Button } from '@/components/ui/button' import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card' import { Input } from '@/components/ui/input' import { KeyRound } from 'lucide-react' +import { InstanceIdRow } from '@/components/InstanceIdRow' import { LicenseStatusBanners } from '@/components/LicenseStatusBanners' import { licenseApi } from '@/api/license' import { useLicense } from '@/hooks/useLicense' @@ -61,6 +62,7 @@ export const LicenseSection = memo(function LicenseSection() {{t('activateHint')}
+