mirror of
https://github.com/QwenLM/qwen-code.git
synced 2026-11-07 06:19:43 -10:00
* fix(web-shell): wait for session before checking subscription commands * fix(cli): keep signed-out subscription startup in the composer * fix(subscription): preserve model selection across sign-in * fix(subscription): align sign-in with model selection scope * fix(subscription): apply reasoning defaults across models * fix(subscription): tag Qwen web links by client * fix(subscription): prefix Qwen web client tags * fix(subscription): attribute Qwen web links by paywall scene * fix(subscription): recover historical sessions with saved API models * fix(cli): remove the signed-out subscription startup notice * fix(subscription): support standalone desktop activation and expose errors * fix(subscription): isolate activation and session model selection * fix(subscription): enforce auth policy and restore home login * test(cli): preserve the default initializer mock on restore --------- Co-authored-by: luoyun <[email protected]> Co-authored-by: 易良 <[email protected]>
213 lines
6.8 KiB
TypeScript
213 lines
6.8 KiB
TypeScript
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
import {
|
|
useConnection,
|
|
useDaemonSessionOwnerGuard,
|
|
useWorkspaceActions,
|
|
useActions,
|
|
} from '@qwen-code/web-shell/daemon-react-sdk';
|
|
import type { useI18n } from '../i18n';
|
|
import { isDesktopShell, openExternalUrl } from '../utils/externalOpen';
|
|
|
|
interface Options {
|
|
t: ReturnType<typeof useI18n>['t'];
|
|
workspaceCwd?: string;
|
|
isBlocked: () => boolean;
|
|
onBlocked: () => boolean;
|
|
onNotice: (kind: 'info' | 'success' | 'warning', message: string) => void;
|
|
onError: (error: unknown, message: string) => void;
|
|
onCommand?: (text: string) => boolean;
|
|
}
|
|
|
|
export function useSubscriptionLogout(options: Options) {
|
|
const connection = useConnection();
|
|
const connectionRef = useRef(connection);
|
|
connectionRef.current = connection;
|
|
const actions = useWorkspaceActions();
|
|
const sessionActions = useActions();
|
|
const ownerGuard = useDaemonSessionOwnerGuard();
|
|
const callbacks = useRef(options);
|
|
callbacks.current = options;
|
|
const pending = useRef(false);
|
|
const [available, setAvailable] = useState(false);
|
|
const { workspaceCwd, t } = options;
|
|
const sessionAuthType = connection.sessionId
|
|
? connection.context?.state._meta?.['authType']
|
|
: undefined;
|
|
const canCheckAvailability =
|
|
!connection.sessionId || connection.status === 'connected';
|
|
const loadAuthType = useCallback(
|
|
async (opts?: { silent?: boolean }) => {
|
|
if (connectionRef.current.sessionId) {
|
|
const context = await sessionActions.getContext(opts);
|
|
return context.state._meta?.['authType'];
|
|
}
|
|
if (!workspaceCwd) return undefined;
|
|
return (await actions.loadProviders(workspaceCwd)).current?.authType;
|
|
},
|
|
[actions, sessionActions, workspaceCwd],
|
|
);
|
|
|
|
useEffect(() => {
|
|
const abort = new AbortController();
|
|
const owner = ownerGuard.capture();
|
|
setAvailable(false);
|
|
if (workspaceCwd && connection.providers && canCheckAvailability) {
|
|
void actions
|
|
.getSubscriptionConfig({ signal: abort.signal })
|
|
.then(async (config) => {
|
|
if (
|
|
abort.signal.aborted ||
|
|
!owner.isCurrent() ||
|
|
!config.enabled ||
|
|
!config.signedIn
|
|
)
|
|
return;
|
|
const authType = await loadAuthType({ silent: true });
|
|
const latest = await actions.getSubscriptionConfig({
|
|
signal: abort.signal,
|
|
});
|
|
if (!abort.signal.aborted && owner.isCurrent())
|
|
setAvailable(
|
|
latest.enabled &&
|
|
latest.signedIn === true &&
|
|
authType === 'qwen-subscription',
|
|
);
|
|
})
|
|
.catch(() => undefined);
|
|
}
|
|
return () => abort.abort();
|
|
}, [
|
|
actions,
|
|
ownerGuard,
|
|
workspaceCwd,
|
|
connection.sessionId,
|
|
connection.currentModel,
|
|
connection.providers,
|
|
canCheckAvailability,
|
|
sessionAuthType,
|
|
loadAuthType,
|
|
]);
|
|
|
|
const handleCommand = useCallback(
|
|
(text: string): boolean | undefined => {
|
|
const command = /^\/(logout|upgrade)(?:\s|$)/i
|
|
.exec(text.trim())?.[1]
|
|
?.toLowerCase();
|
|
if (
|
|
!command ||
|
|
connection.commands?.some(
|
|
(entry) =>
|
|
entry.name === command &&
|
|
entry.source &&
|
|
entry.source !== 'builtin-command',
|
|
)
|
|
)
|
|
return undefined;
|
|
if (!workspaceCwd) return false;
|
|
if (callbacks.current.isBlocked()) return callbacks.current.onBlocked();
|
|
if (pending.current) return true;
|
|
const owner = ownerGuard.capture();
|
|
const model = connectionRef.current.currentModel;
|
|
const authType = connectionRef.current.context?.state._meta?.['authType'];
|
|
const isCurrent = () =>
|
|
owner.isCurrent() &&
|
|
connectionRef.current.currentModel === model &&
|
|
connectionRef.current.context?.state._meta?.['authType'] === authType;
|
|
pending.current = true;
|
|
void actions
|
|
.getSubscriptionConfig()
|
|
.then(async (config) => {
|
|
if (!isCurrent()) return;
|
|
if (!config.enabled) {
|
|
callbacks.current.onNotice('info', t('subscription.unavailable'));
|
|
return;
|
|
}
|
|
const authType = await loadAuthType();
|
|
if (!isCurrent()) return;
|
|
const latest = await actions.getSubscriptionConfig();
|
|
if (!isCurrent()) return;
|
|
if (
|
|
!latest.enabled ||
|
|
!latest.signedIn ||
|
|
authType !== 'qwen-subscription'
|
|
) {
|
|
setAvailable(false);
|
|
callbacks.current.onNotice(
|
|
'info',
|
|
t(
|
|
command === 'logout'
|
|
? 'subscription.logoutNotRequired'
|
|
: 'subscription.commandUnavailable',
|
|
),
|
|
);
|
|
return;
|
|
}
|
|
if (callbacks.current.isBlocked()) {
|
|
callbacks.current.onBlocked();
|
|
return;
|
|
}
|
|
if (callbacks.current.onCommand?.(text)) return;
|
|
if (command === 'upgrade') {
|
|
if (latest.plansUrl) {
|
|
if (isDesktopShell()) await openExternalUrl(latest.plansUrl);
|
|
else
|
|
window.open(latest.plansUrl, '_blank', 'noopener,noreferrer');
|
|
}
|
|
return;
|
|
}
|
|
await actions.signOutSubscription();
|
|
setAvailable(false);
|
|
if (!owner.isCurrent()) return;
|
|
await sessionActions.refreshModels(undefined, {
|
|
reloadProviders: false,
|
|
});
|
|
if (owner.isCurrent()) setAvailable(false);
|
|
})
|
|
.catch((error: unknown) => {
|
|
if (!owner.isCurrent()) return;
|
|
const failure = error as { body?: { errorStage?: string } };
|
|
if (failure?.body?.errorStage === 'revoke') {
|
|
setAvailable(false);
|
|
void sessionActions
|
|
.refreshModels(undefined, { reloadProviders: false })
|
|
.catch((refreshError: unknown) => {
|
|
if (owner.isCurrent())
|
|
callbacks.current.onError(
|
|
refreshError,
|
|
t('subscription.signOutFailed'),
|
|
);
|
|
});
|
|
callbacks.current.onNotice(
|
|
'warning',
|
|
t('subscription.signedOutLocally'),
|
|
);
|
|
} else {
|
|
callbacks.current.onError(
|
|
error,
|
|
t(
|
|
command === 'logout'
|
|
? 'subscription.signOutFailed'
|
|
: 'subscription.upgradeFailed',
|
|
),
|
|
);
|
|
}
|
|
})
|
|
.finally(() => {
|
|
pending.current = false;
|
|
});
|
|
return true;
|
|
},
|
|
[
|
|
actions,
|
|
sessionActions,
|
|
connection.commands,
|
|
ownerGuard,
|
|
t,
|
|
workspaceCwd,
|
|
loadAuthType,
|
|
],
|
|
);
|
|
|
|
return { available, handleCommand };
|
|
}
|