mirror of
https://github.com/danny-avila/LibreChat.git
synced 2026-04-07 00:15:23 +02:00
🕐 fix: Use client timezone for special variable date formatting
`replaceSpecialVars` uses `dayjs()` which defaults to the server's
timezone (UTC in Docker), causing `{{current_datetime}}` and
`{{current_date}}` to always show +00:00 regardless of the user's
location. This is inconsistent with `append_current_datetime` for
assistants which already handles client timezone via `clientTimestamp`.
Add dayjs timezone plugin support and a `timezone` parameter to
`replaceSpecialVars`. The client sends its IANA timezone string
(`Intl.DateTimeFormat().resolvedOptions().timeZone`) alongside the
existing `clientTimestamp`, and the agent initialization flow passes
it through to produce timezone-aware date formatting.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
f277b32030
commit
736960e0a4
6 changed files with 86 additions and 4 deletions
|
|
@ -208,6 +208,7 @@ export default function useChatFunctions({
|
|||
text,
|
||||
sender: 'User',
|
||||
clientTimestamp: new Date().toLocaleString('sv').replace(' ', 'T'),
|
||||
clientTimezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
|
||||
isCreatedByUser: true,
|
||||
parentMessageId,
|
||||
conversationId,
|
||||
|
|
|
|||
|
|
@ -403,6 +403,7 @@ export async function initializeAgent(
|
|||
agent.instructions = replaceSpecialVars({
|
||||
text: agent.instructions,
|
||||
user: req.user ? (req.user as unknown as TUser) : null,
|
||||
timezone: req.body?.clientTimezone,
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ export type RequestBody = {
|
|||
model?: string;
|
||||
key?: string;
|
||||
endpointOption?: Partial<TEndpointOption>;
|
||||
clientTimezone?: string;
|
||||
};
|
||||
|
||||
export type ServerRequest = Request<unknown, unknown, RequestBody> & {
|
||||
|
|
|
|||
|
|
@ -7,13 +7,13 @@ import type { TUser, TConversation } from '../src/types';
|
|||
|
||||
// Mock dayjs module with consistent date/time values regardless of environment
|
||||
jest.mock('dayjs', () => {
|
||||
const mockDayjs = () => ({
|
||||
const makeMock = (offset: string) => ({
|
||||
format: (format: string) => {
|
||||
if (format === 'YYYY-MM-DD') {
|
||||
return '2024-04-29';
|
||||
}
|
||||
if (format === 'YYYY-MM-DD HH:mm:ss Z') {
|
||||
return '2024-04-29 12:34:56 -04:00';
|
||||
return `2024-04-29 12:34:56 ${offset}`;
|
||||
}
|
||||
if (format === 'dddd') {
|
||||
return 'Monday';
|
||||
|
|
@ -23,8 +23,19 @@ jest.mock('dayjs', () => {
|
|||
);
|
||||
},
|
||||
toISOString: () => '2024-04-29T16:34:56.000Z',
|
||||
isValid: () => true,
|
||||
tz: (timezone: string) => {
|
||||
if (timezone === 'America/New_York') {
|
||||
return makeMock('-04:00');
|
||||
}
|
||||
if (timezone === 'Asia/Tokyo') {
|
||||
return makeMock('+09:00');
|
||||
}
|
||||
return { isValid: () => false };
|
||||
},
|
||||
});
|
||||
|
||||
const mockDayjs = () => makeMock('-04:00');
|
||||
mockDayjs.extend = jest.fn();
|
||||
|
||||
return mockDayjs;
|
||||
|
|
@ -126,6 +137,56 @@ describe('replaceSpecialVars', () => {
|
|||
expect(result).toContain('2024-04-29T16:34:56.000Z'); // iso_datetime
|
||||
expect(result).toContain('Test User'); // current_user
|
||||
});
|
||||
|
||||
test('should use provided timezone for date formatting', () => {
|
||||
const result = replaceSpecialVars({
|
||||
text: 'Now is {{current_datetime}}',
|
||||
timezone: 'Asia/Tokyo',
|
||||
});
|
||||
expect(result).toBe('Now is 2024-04-29 12:34:56 +09:00 (Monday)');
|
||||
});
|
||||
|
||||
test('should fall back to default when no timezone is provided', () => {
|
||||
const result = replaceSpecialVars({
|
||||
text: 'Now is {{current_datetime}}',
|
||||
});
|
||||
expect(result).toBe('Now is 2024-04-29 12:34:56 -04:00 (Monday)');
|
||||
});
|
||||
|
||||
test('should fall back to default for invalid timezone', () => {
|
||||
const result = replaceSpecialVars({
|
||||
text: 'Now is {{current_datetime}}',
|
||||
timezone: 'Invalid/Timezone',
|
||||
});
|
||||
expect(result).toBe('Now is 2024-04-29 12:34:56 -04:00 (Monday)');
|
||||
});
|
||||
|
||||
test('should apply timezone to {{current_date}} formatting', () => {
|
||||
const result = replaceSpecialVars({
|
||||
text: 'Today is {{current_date}}',
|
||||
timezone: 'Asia/Tokyo',
|
||||
});
|
||||
expect(result).toBe('Today is 2024-04-29 (Monday)');
|
||||
});
|
||||
|
||||
test('should not affect {{iso_datetime}} regardless of timezone', () => {
|
||||
const result = replaceSpecialVars({
|
||||
text: 'ISO: {{iso_datetime}}',
|
||||
timezone: 'Asia/Tokyo',
|
||||
});
|
||||
expect(result).toBe('ISO: 2024-04-29T16:34:56.000Z');
|
||||
});
|
||||
|
||||
test('should handle all variables with timezone and user combined', () => {
|
||||
const result = replaceSpecialVars({
|
||||
text: '{{current_user}} - {{current_date}} - {{current_datetime}} - {{iso_datetime}}',
|
||||
user: mockUser,
|
||||
timezone: 'Asia/Tokyo',
|
||||
});
|
||||
expect(result).toBe(
|
||||
'Test User - 2024-04-29 (Monday) - 2024-04-29 12:34:56 +09:00 (Monday) - 2024-04-29T16:34:56.000Z',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('parseCompactConvo', () => {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,10 @@
|
|||
import dayjs from 'dayjs';
|
||||
import utc from 'dayjs/plugin/utc';
|
||||
import tz from 'dayjs/plugin/timezone';
|
||||
import type { ZodIssue } from 'zod';
|
||||
|
||||
dayjs.extend(utc);
|
||||
dayjs.extend(tz);
|
||||
import type * as a from './types/assistants';
|
||||
import type * as s from './schemas';
|
||||
import type * as t from './types';
|
||||
|
|
@ -402,13 +407,24 @@ export function findLastSeparatorIndex(text: string, separators = SEPARATORS): n
|
|||
return lastIndex;
|
||||
}
|
||||
|
||||
export function replaceSpecialVars({ text, user }: { text: string; user?: t.TUser | null }) {
|
||||
export function replaceSpecialVars({
|
||||
text,
|
||||
user,
|
||||
timezone,
|
||||
}: {
|
||||
text: string;
|
||||
user?: t.TUser | null;
|
||||
timezone?: string;
|
||||
}) {
|
||||
let result = text;
|
||||
if (!result) {
|
||||
return result;
|
||||
}
|
||||
|
||||
const now = dayjs();
|
||||
let now = timezone ? dayjs().tz(timezone) : dayjs();
|
||||
if (!now.isValid()) {
|
||||
now = dayjs();
|
||||
}
|
||||
const weekdayName = now.format('dddd');
|
||||
|
||||
const currentDate = now.format('YYYY-MM-DD');
|
||||
|
|
|
|||
|
|
@ -611,6 +611,7 @@ export const tMessageSchema = z.object({
|
|||
isCreatedByUser: z.boolean(),
|
||||
error: z.boolean().optional(),
|
||||
clientTimestamp: z.string().optional(),
|
||||
clientTimezone: z.string().optional(),
|
||||
createdAt: z
|
||||
.string()
|
||||
.optional()
|
||||
|
|
@ -686,6 +687,7 @@ export type TMessage = z.input<typeof tMessageSchema> & {
|
|||
siblingIndex?: number;
|
||||
attachments?: TAttachment[];
|
||||
clientTimestamp?: string;
|
||||
clientTimezone?: string;
|
||||
feedback?: TFeedback;
|
||||
};
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue