merge: integrate upstream main with local Makelore changes
This commit is contained in:
@@ -56,10 +56,16 @@ type MainSessionResponse = AuthActionResponse & {
|
||||
session?: MainSession | null;
|
||||
};
|
||||
|
||||
type ModuleAccessResponse = AuthActionResponse & {
|
||||
type CurrentAccountResponse = AuthActionResponse & {
|
||||
user?: unknown;
|
||||
moduleAccess?: unknown;
|
||||
};
|
||||
|
||||
type CurrentAccount = {
|
||||
user: AuthUser | null;
|
||||
moduleAccess: ModuleAccess;
|
||||
};
|
||||
|
||||
type RefreshSessionOptions = {
|
||||
forceRefresh?: boolean;
|
||||
};
|
||||
@@ -112,6 +118,7 @@ const DEFAULT_CLIENT_ID = NIANCODE_AUTH_CLIENT_ID;
|
||||
const DEFAULT_AUTH_BASE = NIANCODE_AUTH_GATEWAY_URL;
|
||||
const TOKEN_EXPIRY_SKEW_MS = 30_000;
|
||||
const IDLE_LOGIN_MESSAGE = '登录已超过 7 天未使用,请重新授权。';
|
||||
const INCOMPLETE_IDENTITY_MESSAGE = '登录身份无法确认,请重新登录。';
|
||||
let authSessionEpoch = 0;
|
||||
|
||||
function advanceAuthSessionEpoch(): void {
|
||||
@@ -137,19 +144,39 @@ function asStringOrNumber(value: unknown): string | number | null {
|
||||
|
||||
function asAuthorities(value: unknown): string[] {
|
||||
if (!Array.isArray(value)) return [];
|
||||
return value.filter((item): item is string => typeof item === 'string' && item.length > 0);
|
||||
return value
|
||||
.map((item) => asString(item))
|
||||
.filter((item): item is string => item !== null);
|
||||
}
|
||||
|
||||
function createUserFromToken(token: AuthTokenPayload): AuthUser {
|
||||
function asStringId(value: unknown): string | null {
|
||||
if (typeof value === 'number' && Number.isFinite(value)) return String(value);
|
||||
return asString(value);
|
||||
}
|
||||
|
||||
function parseAuthUser(value: unknown): AuthUser | null {
|
||||
if (!value || typeof value !== 'object' || Array.isArray(value)) return null;
|
||||
const user = value as Record<string, unknown>;
|
||||
const username = asString(user.username);
|
||||
if (!username) return null;
|
||||
|
||||
return {
|
||||
username: asString(token.username) ?? '',
|
||||
userId: asString(token.user_id),
|
||||
tenantId: asStringOrNumber(token.tenant_id),
|
||||
deptId: asStringOrNumber(token.dept_id),
|
||||
authorities: asAuthorities(token.authorities),
|
||||
username,
|
||||
userId: asStringId(user.userId ?? user.user_id),
|
||||
tenantId: asStringOrNumber(user.tenantId ?? user.tenant_id),
|
||||
deptId: asStringOrNumber(user.deptId ?? user.dept_id),
|
||||
authorities: asAuthorities(user.authorities),
|
||||
};
|
||||
}
|
||||
|
||||
function createUserFromToken(token: AuthTokenPayload): AuthUser | null {
|
||||
return parseAuthUser(token);
|
||||
}
|
||||
|
||||
function hasAuthenticatedIdentity(user: AuthUser | null): user is AuthUser {
|
||||
return Boolean(user && asString(user.username));
|
||||
}
|
||||
|
||||
function isSessionIdle(lastActiveAt: number | null, nowMs = Date.now()): boolean {
|
||||
return lastActiveAt != null
|
||||
&& nowMs - lastActiveAt >= WORKS_SQUARE_SESSION_IDLE_TIMEOUT_MS;
|
||||
@@ -243,14 +270,37 @@ async function syncMainSession(session: {
|
||||
}
|
||||
}
|
||||
|
||||
async function readCurrentModuleAccess(fallback: ModuleAccess): Promise<ModuleAccess> {
|
||||
async function readCurrentAccount(
|
||||
fallbackModuleAccess: ModuleAccess,
|
||||
fallbackUser: AuthUser | null,
|
||||
): Promise<CurrentAccount> {
|
||||
const normalizedFallbackUser = parseAuthUser(fallbackUser);
|
||||
try {
|
||||
const response = await hostApiFetch<ModuleAccessResponse>('/api/auth/me');
|
||||
if (!response.success) return fallback;
|
||||
return normalizeModuleAccess(response.moduleAccess);
|
||||
const response = await hostApiFetch<CurrentAccountResponse>('/api/auth/me');
|
||||
if (!response.success) {
|
||||
return {
|
||||
user: normalizedFallbackUser,
|
||||
moduleAccess: normalizeModuleAccess(fallbackModuleAccess),
|
||||
};
|
||||
}
|
||||
return {
|
||||
user: parseAuthUser(response.user) ?? normalizedFallbackUser,
|
||||
moduleAccess: normalizeModuleAccess(response.moduleAccess),
|
||||
};
|
||||
} catch (error) {
|
||||
if (isTerminalAuthError(error)) throw error;
|
||||
return fallback;
|
||||
return {
|
||||
user: normalizedFallbackUser,
|
||||
moduleAccess: normalizeModuleAccess(fallbackModuleAccess),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
async function clearMainSessionBestEffort(): Promise<void> {
|
||||
try {
|
||||
await hostApiFetch<AuthActionResponse>('/api/auth/session/clear', { method: 'POST' });
|
||||
} catch {
|
||||
// Login will clean any remaining Main-owned runtime before accepting a new session.
|
||||
}
|
||||
}
|
||||
|
||||
@@ -273,8 +323,16 @@ async function loginViaHost(
|
||||
}
|
||||
if (!isCurrentAuthSessionEpoch(operationEpoch)) return;
|
||||
|
||||
const moduleAccess = await readCurrentModuleAccess({ ...DEFAULT_MODULE_ACCESS });
|
||||
const account = await readCurrentAccount(
|
||||
{ ...DEFAULT_MODULE_ACCESS },
|
||||
createUserFromToken(response.token),
|
||||
);
|
||||
if (!isCurrentAuthSessionEpoch(operationEpoch)) return;
|
||||
if (!account.user) {
|
||||
await clearMainSessionBestEffort();
|
||||
if (!isCurrentAuthSessionEpoch(operationEpoch)) return;
|
||||
throw new Error(INCOMPLETE_IDENTITY_MESSAGE);
|
||||
}
|
||||
|
||||
set({
|
||||
initialized: true,
|
||||
@@ -283,8 +341,8 @@ async function loginViaHost(
|
||||
authBase: trimTrailingSlash(DEFAULT_AUTH_BASE),
|
||||
clientId: DEFAULT_CLIENT_ID,
|
||||
...sessionFieldsFromMain(session),
|
||||
user: createUserFromToken(response.token),
|
||||
moduleAccess,
|
||||
user: account.user,
|
||||
moduleAccess: account.moduleAccess,
|
||||
});
|
||||
} catch (error) {
|
||||
if (!isCurrentAuthSessionEpoch(operationEpoch)) return;
|
||||
@@ -320,6 +378,7 @@ export const useAuthStore = create<AuthState>()(
|
||||
const restoredLastActiveAt = state.accessToken
|
||||
? (state.lastActiveAt ?? nowMs)
|
||||
: null;
|
||||
const restoredUser = parseAuthUser(state.user);
|
||||
|
||||
if (authBaseChanged) {
|
||||
advanceAuthSessionEpoch();
|
||||
@@ -347,6 +406,7 @@ export const useAuthStore = create<AuthState>()(
|
||||
clientId: DEFAULT_CLIENT_ID,
|
||||
lastActiveAt: restoredLastActiveAt,
|
||||
canRefresh: state.canRefresh || Boolean(state.legacyRefreshToken),
|
||||
user: restoredUser,
|
||||
});
|
||||
|
||||
const synchronized = await syncMainSession({
|
||||
@@ -369,6 +429,18 @@ export const useAuthStore = create<AuthState>()(
|
||||
return;
|
||||
}
|
||||
if (synchronized.kind === 'unavailable') {
|
||||
if (state.accessToken && !restoredUser) {
|
||||
await clearMainSessionBestEffort();
|
||||
if (!isCurrentAuthSessionEpoch(operationEpoch)) return;
|
||||
advanceAuthSessionEpoch();
|
||||
set({
|
||||
initialized: true,
|
||||
loading: false,
|
||||
error: INCOMPLETE_IDENTITY_MESSAGE,
|
||||
...getClearedSession(),
|
||||
});
|
||||
return;
|
||||
}
|
||||
set({ initialized: true, loading: false });
|
||||
return;
|
||||
}
|
||||
@@ -404,10 +476,11 @@ export const useAuthStore = create<AuthState>()(
|
||||
return;
|
||||
}
|
||||
|
||||
let moduleAccess: ModuleAccess;
|
||||
let account: CurrentAccount;
|
||||
try {
|
||||
moduleAccess = await readCurrentModuleAccess(
|
||||
account = await readCurrentAccount(
|
||||
normalizeModuleAccess(state.moduleAccess),
|
||||
restoredUser,
|
||||
);
|
||||
} catch (error) {
|
||||
if (!isCurrentAuthSessionEpoch(operationEpoch)) return;
|
||||
@@ -421,10 +494,31 @@ export const useAuthStore = create<AuthState>()(
|
||||
});
|
||||
return;
|
||||
}
|
||||
moduleAccess = normalizeModuleAccess(state.moduleAccess);
|
||||
account = {
|
||||
user: restoredUser,
|
||||
moduleAccess: normalizeModuleAccess(state.moduleAccess),
|
||||
};
|
||||
}
|
||||
if (!isCurrentAuthSessionEpoch(operationEpoch)) return;
|
||||
set({ initialized: true, loading: false, error: null, moduleAccess });
|
||||
if (!account.user) {
|
||||
await clearMainSessionBestEffort();
|
||||
if (!isCurrentAuthSessionEpoch(operationEpoch)) return;
|
||||
advanceAuthSessionEpoch();
|
||||
set({
|
||||
initialized: true,
|
||||
loading: false,
|
||||
error: INCOMPLETE_IDENTITY_MESSAGE,
|
||||
...getClearedSession(),
|
||||
});
|
||||
return;
|
||||
}
|
||||
set({
|
||||
initialized: true,
|
||||
loading: false,
|
||||
error: null,
|
||||
user: account.user,
|
||||
moduleAccess: account.moduleAccess,
|
||||
});
|
||||
},
|
||||
|
||||
loginWithPassword: (input) => loginViaHost('/api/auth/login', input, set),
|
||||
@@ -459,15 +553,28 @@ export const useAuthStore = create<AuthState>()(
|
||||
throw new Error(response.error || 'Refresh failed');
|
||||
}
|
||||
|
||||
const moduleAccess = await readCurrentModuleAccess(state.moduleAccess);
|
||||
const account = await readCurrentAccount(state.moduleAccess, state.user);
|
||||
if (!isCurrentAuthSessionEpoch(operationEpoch)) return null;
|
||||
if (!account.user) {
|
||||
await clearMainSessionBestEffort();
|
||||
if (!isCurrentAuthSessionEpoch(operationEpoch)) return null;
|
||||
advanceAuthSessionEpoch();
|
||||
set({
|
||||
initialized: true,
|
||||
loading: false,
|
||||
error: INCOMPLETE_IDENTITY_MESSAGE,
|
||||
...getClearedSession(),
|
||||
});
|
||||
return null;
|
||||
}
|
||||
|
||||
set({
|
||||
initialized: true,
|
||||
loading: false,
|
||||
error: null,
|
||||
...sessionFieldsFromMain(session),
|
||||
moduleAccess,
|
||||
user: account.user,
|
||||
moduleAccess: account.moduleAccess,
|
||||
});
|
||||
return session.accessToken;
|
||||
} catch (error) {
|
||||
@@ -488,13 +595,18 @@ export const useAuthStore = create<AuthState>()(
|
||||
},
|
||||
|
||||
getValidAccessToken: async () => {
|
||||
const { accessToken, expiresAt, canRefresh } = get();
|
||||
const {
|
||||
accessToken,
|
||||
expiresAt,
|
||||
canRefresh,
|
||||
user,
|
||||
} = get();
|
||||
if (!accessToken || !hasAuthenticatedIdentity(user)) return null;
|
||||
const expired = Boolean(
|
||||
accessToken
|
||||
&& expiresAt
|
||||
expiresAt
|
||||
&& expiresAt <= Date.now() + TOKEN_EXPIRY_SKEW_MS,
|
||||
);
|
||||
if (!accessToken || expired) {
|
||||
if (expired) {
|
||||
return canRefresh
|
||||
? await get().refreshSession({ forceRefresh: false })
|
||||
: null;
|
||||
@@ -504,7 +616,7 @@ export const useAuthStore = create<AuthState>()(
|
||||
|
||||
markActivity: async () => {
|
||||
const state = get();
|
||||
if (!state.accessToken) return;
|
||||
if (!state.accessToken || !hasAuthenticatedIdentity(state.user)) return;
|
||||
const operationEpoch = authSessionEpoch;
|
||||
|
||||
const nowMs = Date.now();
|
||||
@@ -555,7 +667,7 @@ export const useAuthStore = create<AuthState>()(
|
||||
|
||||
maintainSession: async () => {
|
||||
let state = get();
|
||||
if (!state.accessToken) return;
|
||||
if (!state.accessToken || !hasAuthenticatedIdentity(state.user)) return;
|
||||
const operationEpoch = authSessionEpoch;
|
||||
|
||||
if (state.legacyRefreshToken) {
|
||||
@@ -655,8 +767,16 @@ export const useAuthStore = create<AuthState>()(
|
||||
},
|
||||
|
||||
isAuthenticated: () => {
|
||||
const { accessToken, expiresAt, lastActiveAt, canRefresh } = get();
|
||||
if (!accessToken || isSessionIdle(lastActiveAt)) return false;
|
||||
const {
|
||||
accessToken,
|
||||
expiresAt,
|
||||
lastActiveAt,
|
||||
canRefresh,
|
||||
user,
|
||||
} = get();
|
||||
if (!accessToken || !hasAuthenticatedIdentity(user) || isSessionIdle(lastActiveAt)) {
|
||||
return false;
|
||||
}
|
||||
if (expiresAt && expiresAt <= Date.now() + TOKEN_EXPIRY_SKEW_MS) {
|
||||
return canRefresh;
|
||||
}
|
||||
@@ -665,7 +785,7 @@ export const useAuthStore = create<AuthState>()(
|
||||
}),
|
||||
{
|
||||
name: 'niancode-auth',
|
||||
version: 2,
|
||||
version: 3,
|
||||
migrate: (persistedState: unknown) => {
|
||||
const state = persistedState && typeof persistedState === 'object'
|
||||
? persistedState as Record<string, unknown>
|
||||
@@ -677,6 +797,7 @@ export const useAuthStore = create<AuthState>()(
|
||||
...rest,
|
||||
canRefresh: state.canRefresh === true || Boolean(legacyRefreshToken),
|
||||
legacyRefreshToken,
|
||||
user: parseAuthUser(state.user),
|
||||
moduleAccess: normalizeModuleAccess(state.moduleAccess),
|
||||
};
|
||||
},
|
||||
|
||||
@@ -36,6 +36,8 @@ export type CodingConversationLoadState =
|
||||
| 'recovering'
|
||||
| 'error';
|
||||
|
||||
export type CodingConversationSnapshotLoadMode = 'loading' | 'recovering' | 'silent';
|
||||
|
||||
export interface CodingConversationEntry {
|
||||
reducer: ConversationReducerState;
|
||||
loadState: CodingConversationLoadState;
|
||||
@@ -72,7 +74,10 @@ export interface CodingConversationStoreState {
|
||||
primeConversation(snapshot: ConversationSnapshot): void;
|
||||
clearConversationSelection(): void;
|
||||
selectConversation(conversationId: string): Promise<void>;
|
||||
loadSnapshot(conversationId: string, recovering?: boolean): Promise<ConversationSnapshot>;
|
||||
loadSnapshot(
|
||||
conversationId: string,
|
||||
mode?: CodingConversationSnapshotLoadMode,
|
||||
): Promise<ConversationSnapshot>;
|
||||
recoverConversation(conversationId: string): Promise<void>;
|
||||
connectEvents(): Promise<void>;
|
||||
disconnectEvents(): void;
|
||||
@@ -434,7 +439,7 @@ export function createCodingConversationStore(
|
||||
recoveryRefreshKeys.set(conversationId, refreshKey);
|
||||
queueMicrotask(() => {
|
||||
if (!snapshotLoads.has(conversationId)) {
|
||||
void store.getState().loadSnapshot(conversationId, true).catch(() => undefined);
|
||||
void store.getState().loadSnapshot(conversationId, 'recovering').catch(() => undefined);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -509,27 +514,32 @@ export function createCodingConversationStore(
|
||||
const snapshotFlight = !entry?.reducer.snapshot
|
||||
|| entry.reducer.invalidation
|
||||
|| entry.loadState !== 'live'
|
||||
? get().loadSnapshot(conversationId, Boolean(entry?.reducer.invalidation))
|
||||
? get().loadSnapshot(
|
||||
conversationId,
|
||||
entry?.reducer.invalidation ? 'recovering' : 'loading',
|
||||
)
|
||||
: Promise.resolve(entry.reducer.snapshot);
|
||||
await Promise.all([snapshotFlight, get().connectEvents()]);
|
||||
},
|
||||
|
||||
loadSnapshot(conversationId, recovering = false) {
|
||||
loadSnapshot(conversationId, mode = 'loading') {
|
||||
const prior = snapshotLoads.get(conversationId);
|
||||
if (prior) return prior;
|
||||
set((state) => {
|
||||
const entry = state.entriesByConversationId[conversationId] ?? emptyEntry();
|
||||
return {
|
||||
entriesByConversationId: {
|
||||
...state.entriesByConversationId,
|
||||
[conversationId]: {
|
||||
...entry,
|
||||
loadState: recovering ? 'recovering' : 'loading',
|
||||
error: null,
|
||||
if (mode !== 'silent') {
|
||||
set((state) => {
|
||||
const entry = state.entriesByConversationId[conversationId] ?? emptyEntry();
|
||||
return {
|
||||
entriesByConversationId: {
|
||||
...state.entriesByConversationId,
|
||||
[conversationId]: {
|
||||
...entry,
|
||||
loadState: mode,
|
||||
error: null,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
});
|
||||
};
|
||||
});
|
||||
}
|
||||
const flight = withPreparationDeadline(
|
||||
deps.getSnapshot(conversationId),
|
||||
deps.preparationTimeoutMs,
|
||||
@@ -545,16 +555,18 @@ export function createCodingConversationStore(
|
||||
return snapshot;
|
||||
})
|
||||
.catch((error) => {
|
||||
const failure = errorDetails(error);
|
||||
set((state) => {
|
||||
const entry = state.entriesByConversationId[conversationId] ?? emptyEntry();
|
||||
return {
|
||||
entriesByConversationId: {
|
||||
...state.entriesByConversationId,
|
||||
[conversationId]: { ...entry, loadState: 'error', error: failure.message },
|
||||
},
|
||||
};
|
||||
});
|
||||
if (mode !== 'silent') {
|
||||
const failure = errorDetails(error);
|
||||
set((state) => {
|
||||
const entry = state.entriesByConversationId[conversationId] ?? emptyEntry();
|
||||
return {
|
||||
entriesByConversationId: {
|
||||
...state.entriesByConversationId,
|
||||
[conversationId]: { ...entry, loadState: 'error', error: failure.message },
|
||||
},
|
||||
};
|
||||
});
|
||||
}
|
||||
throw error;
|
||||
})
|
||||
.finally(() => {
|
||||
@@ -579,7 +591,7 @@ export function createCodingConversationStore(
|
||||
});
|
||||
try {
|
||||
await deps.recover(conversationId);
|
||||
await get().loadSnapshot(conversationId, true);
|
||||
await get().loadSnapshot(conversationId, 'recovering');
|
||||
} catch (error) {
|
||||
const failure = errorDetails(error);
|
||||
set((state) => {
|
||||
@@ -689,7 +701,10 @@ export function createCodingConversationStore(
|
||||
async submitPrompt(input) {
|
||||
let entry = get().entriesByConversationId[input.conversationId];
|
||||
if (!entry?.reducer.snapshot || entry.reducer.invalidation) {
|
||||
await get().loadSnapshot(input.conversationId, Boolean(entry?.reducer.invalidation));
|
||||
await get().loadSnapshot(
|
||||
input.conversationId,
|
||||
entry?.reducer.invalidation ? 'recovering' : 'loading',
|
||||
);
|
||||
entry = get().entriesByConversationId[input.conversationId];
|
||||
}
|
||||
if (!entry?.reducer.snapshot) throw new Error('Conversation snapshot is unavailable');
|
||||
@@ -915,30 +930,43 @@ export function createCodingConversationStore(
|
||||
|| Boolean(currentBeforeBatch?.reducer.invalidation);
|
||||
if (!validRange) {
|
||||
if (!snapshotLoads.has(event.conversationId)) {
|
||||
void get().loadSnapshot(event.conversationId, true).catch(() => undefined);
|
||||
void get().loadSnapshot(event.conversationId, 'recovering').catch(() => undefined);
|
||||
}
|
||||
return;
|
||||
}
|
||||
let applicableEvent = event;
|
||||
if (currentSnapshot
|
||||
&& event.workerGeneration === currentSnapshot.cursor.workerGeneration
|
||||
&& event.fromSeq <= currentSnapshot.cursor.seq) {
|
||||
const unseenItems = event.items.filter((item) => item.seq > currentSnapshot.cursor.seq);
|
||||
if (unseenItems.length === 0) return;
|
||||
applicableEvent = {
|
||||
...event,
|
||||
fromSeq: unseenItems[0]!.seq,
|
||||
toSeq: unseenItems.at(-1)!.seq,
|
||||
items: unseenItems,
|
||||
};
|
||||
}
|
||||
if (recovering
|
||||
|| !currentSnapshot
|
||||
|| event.workerGeneration !== currentSnapshot.cursor.workerGeneration
|
||||
|| event.fromSeq !== currentSnapshot.cursor.seq + 1) {
|
||||
bufferRecoveryBatch(event);
|
||||
if (!snapshotLoads.has(event.conversationId)) {
|
||||
void get().loadSnapshot(event.conversationId, true).catch(() => undefined);
|
||||
|| applicableEvent.workerGeneration !== currentSnapshot.cursor.workerGeneration
|
||||
|| applicableEvent.fromSeq !== currentSnapshot.cursor.seq + 1) {
|
||||
bufferRecoveryBatch(applicableEvent);
|
||||
if (!snapshotLoads.has(applicableEvent.conversationId)) {
|
||||
void get().loadSnapshot(applicableEvent.conversationId, 'recovering').catch(() => undefined);
|
||||
}
|
||||
return;
|
||||
}
|
||||
const shouldSyncSettledSession = completesOrdinaryPrompt(event, currentSnapshot);
|
||||
const shouldSyncSettledSession = completesOrdinaryPrompt(applicableEvent, currentSnapshot);
|
||||
let recover = false;
|
||||
let applied = false;
|
||||
set((state) => {
|
||||
const current = state.entriesByConversationId[event.conversationId] ?? emptyEntry();
|
||||
const current = state.entriesByConversationId[applicableEvent.conversationId] ?? emptyEntry();
|
||||
let reducer = current.reducer;
|
||||
for (const item of event.items) {
|
||||
for (const item of applicableEvent.items) {
|
||||
reducer = reduceConversationPatch(reducer, {
|
||||
conversationId: event.conversationId,
|
||||
workerGeneration: event.workerGeneration,
|
||||
conversationId: applicableEvent.conversationId,
|
||||
workerGeneration: applicableEvent.workerGeneration,
|
||||
...(item.runId ? { runId: item.runId } : {}),
|
||||
seq: item.seq,
|
||||
at: item.at,
|
||||
@@ -951,10 +979,10 @@ export function createCodingConversationStore(
|
||||
}
|
||||
if (reducer === current.reducer) return state;
|
||||
applied = true;
|
||||
const incomingMessages = event.items.flatMap((item) => (
|
||||
const incomingMessages = applicableEvent.items.flatMap((item) => (
|
||||
item.patch.op === 'message.upsert' ? [item.patch.node] : []
|
||||
));
|
||||
const unread = state.selectedConversationId !== event.conversationId
|
||||
const unread = state.selectedConversationId !== applicableEvent.conversationId
|
||||
&& incomingMessages.some((message) => message.role === 'assistant')
|
||||
? true
|
||||
: current.unread;
|
||||
@@ -965,7 +993,7 @@ export function createCodingConversationStore(
|
||||
error: null,
|
||||
unread,
|
||||
};
|
||||
const requests = state.requestsByConversationId[event.conversationId] ?? {};
|
||||
const requests = state.requestsByConversationId[applicableEvent.conversationId] ?? {};
|
||||
const nextRequests = reducer.snapshot
|
||||
? withoutReconciledRequests(requests, reducer.snapshot)
|
||||
: requests;
|
||||
@@ -973,21 +1001,21 @@ export function createCodingConversationStore(
|
||||
return {
|
||||
entriesByConversationId: {
|
||||
...state.entriesByConversationId,
|
||||
[event.conversationId]: entry,
|
||||
[applicableEvent.conversationId]: entry,
|
||||
},
|
||||
summariesByConversationId: summariesWithEntry(state.summariesByConversationId, entry),
|
||||
requestsByConversationId: !requestsChanged
|
||||
? state.requestsByConversationId
|
||||
: {
|
||||
...state.requestsByConversationId,
|
||||
[event.conversationId]: nextRequests,
|
||||
[applicableEvent.conversationId]: nextRequests,
|
||||
},
|
||||
};
|
||||
});
|
||||
if (recover) {
|
||||
void get().loadSnapshot(event.conversationId, true).catch(() => undefined);
|
||||
void get().loadSnapshot(applicableEvent.conversationId, 'recovering').catch(() => undefined);
|
||||
} else if (applied && shouldSyncSettledSession) {
|
||||
const settledSnapshot = selectCodingConversationSnapshot(event.conversationId)(get());
|
||||
const settledSnapshot = selectCodingConversationSnapshot(applicableEvent.conversationId)(get());
|
||||
if (settledSnapshot) deps.queueSettledSessionSync(settledSnapshot);
|
||||
}
|
||||
},
|
||||
|
||||
@@ -6,7 +6,6 @@ import {
|
||||
getCodingProjectConfig,
|
||||
listCodingProjectConversations,
|
||||
listCodingProjects,
|
||||
acknowledgeLegacyCodingConversationNotice,
|
||||
openCodingProject,
|
||||
patchCodingProjectConversation,
|
||||
removeCodingProject,
|
||||
@@ -48,11 +47,6 @@ interface CodingWorkspaceDependencies {
|
||||
}): Promise<{ project: CodingProjectSummary; config: CodingProjectConfig; knowledgeFiles: string[] }>;
|
||||
setActiveProject(projectId: string): Promise<CodingProjectSummary>;
|
||||
removeProject(projectId: string): Promise<void>;
|
||||
acknowledgeLegacyNotice(projectId: string): Promise<{
|
||||
project: CodingProjectSummary;
|
||||
config: CodingProjectConfig;
|
||||
knowledgeFiles: string[];
|
||||
}>;
|
||||
}
|
||||
|
||||
export interface CodingWorkspaceState {
|
||||
@@ -77,7 +71,6 @@ export interface CodingWorkspaceState {
|
||||
}): Promise<CodingProjectSummary>;
|
||||
setActiveProject(projectId: string): Promise<CodingProjectSummary>;
|
||||
removeProject(projectId: string): Promise<void>;
|
||||
acknowledgeLegacyNotice(): Promise<void>;
|
||||
selectAgent(agentId: string): void;
|
||||
ensureConversation(agentId: string): Promise<CodingConversationMetadata>;
|
||||
createConversation(agentId: string): Promise<CodingConversationMetadata>;
|
||||
@@ -136,7 +129,6 @@ function defaultDependencies(): CodingWorkspaceDependencies {
|
||||
createProject: createCodingProject,
|
||||
setActiveProject: setActiveCodingProject,
|
||||
removeProject: removeCodingProject,
|
||||
acknowledgeLegacyNotice: acknowledgeLegacyCodingConversationNotice,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -238,13 +230,6 @@ export function createCodingWorkspaceStore(
|
||||
await get().load();
|
||||
},
|
||||
|
||||
async acknowledgeLegacyNotice() {
|
||||
const projectId = get().activeProjectId;
|
||||
if (!projectId) return;
|
||||
const snapshot = await deps.acknowledgeLegacyNotice(projectId);
|
||||
if (get().activeProjectId === projectId) set({ config: snapshot.config });
|
||||
},
|
||||
|
||||
selectAgent(agentId) {
|
||||
if (!enabledAgent(get().config, agentId)) return;
|
||||
set({ selectedAgentId: agentId });
|
||||
@@ -259,7 +244,7 @@ export function createCodingWorkspaceStore(
|
||||
async createConversation(agentId) {
|
||||
const state = get();
|
||||
if (!state.activeProject || !enabledAgent(state.config, agentId)) {
|
||||
throw new Error('当前项目没有可用的伙伴。');
|
||||
throw new Error('当前项目没有可用的智能体。');
|
||||
}
|
||||
const flightKey = `${state.activeProject.id}:${agentId}`;
|
||||
const existingFlight = conversationFlights.get(flightKey);
|
||||
|
||||
Reference in New Issue
Block a user