@@ -172,6 +172,7 @@ export interface AiAgentClientState {
172172 selectedSourceIds ?: string [ ] ;
173173 selectedTags ?: string [ ] ;
174174 } | null ;
175+ clientContext ?: AiClientRequestContext | null ;
175176 stateVersion ?: number ;
176177}
177178
@@ -243,10 +244,87 @@ export type AiChatPayload = {
243244 excludeIds ?: string [ ] ;
244245 history ?: { role : 'user' | 'assistant' ; content : string } [ ] ;
245246 state ?: AiAgentClientState | null ;
247+ clientContext ?: AiClientRequestContext | null ;
246248 debug ?: boolean ;
247249 enableThinking ?: boolean ;
248250} ;
249251
252+ export interface AiClientRequestContext {
253+ nowIso : string ;
254+ localDate : string ;
255+ localDateTime : string ;
256+ timeZone ?: string ;
257+ utcOffsetMinutes : number ;
258+ locale ?: string ;
259+ calendar ?: string ;
260+ }
261+
262+ function padDatePart ( value : number ) : string {
263+ return String ( value ) . padStart ( 2 , '0' ) ;
264+ }
265+
266+ function formatUtcOffset ( minutes : number ) : string {
267+ const sign = minutes >= 0 ? '+' : '-' ;
268+ const absolute = Math . abs ( minutes ) ;
269+ return `${ sign } ${ padDatePart ( Math . floor ( absolute / 60 ) ) } :${ padDatePart ( absolute % 60 ) } ` ;
270+ }
271+
272+ export function createAiClientRequestContext ( now = new Date ( ) ) : AiClientRequestContext {
273+ const resolvedOptions = Intl . DateTimeFormat ( ) . resolvedOptions ( ) ;
274+ const utcOffsetMinutes = - now . getTimezoneOffset ( ) ;
275+ const localDate = [
276+ now . getFullYear ( ) ,
277+ padDatePart ( now . getMonth ( ) + 1 ) ,
278+ padDatePart ( now . getDate ( ) ) ,
279+ ] . join ( '-' ) ;
280+ const localTime = [
281+ padDatePart ( now . getHours ( ) ) ,
282+ padDatePart ( now . getMinutes ( ) ) ,
283+ padDatePart ( now . getSeconds ( ) ) ,
284+ ] . join ( ':' ) ;
285+
286+ return {
287+ nowIso : now . toISOString ( ) ,
288+ localDate,
289+ localDateTime : `${ localDate } T${ localTime } ${ formatUtcOffset ( utcOffsetMinutes ) } ` ,
290+ timeZone : resolvedOptions . timeZone ,
291+ utcOffsetMinutes,
292+ locale : typeof navigator !== 'undefined' ? navigator . language : resolvedOptions . locale ,
293+ calendar : resolvedOptions . calendar ,
294+ } ;
295+ }
296+
297+ export function buildAiClientTimeContextMessage ( context : AiClientRequestContext ) : string {
298+ return [
299+ 'Use the current request time context for relative date phrases.' ,
300+ `Client now (UTC): ${ context . nowIso } ` ,
301+ `Client local date: ${ context . localDate } ` ,
302+ `Client local date/time: ${ context . localDateTime } ` ,
303+ context . timeZone ? `Client time zone: ${ context . timeZone } ` : null ,
304+ `Client UTC offset minutes: ${ context . utcOffsetMinutes } ` ,
305+ context . locale ? `Client locale: ${ context . locale } ` : null ,
306+ context . calendar ? `Client calendar: ${ context . calendar } ` : null ,
307+ 'Resolve relative date phrases such as today, yesterday, this month, last month, 最近, 本月, and 上月 using this context.' ,
308+ 'For Rote search tools, prefer structured timeRange preset/rolling/relative_between or pass the original phrase as timeExpression. Use from/to only for explicit absolute dates.' ,
309+ ]
310+ . filter ( ( line ) : line is string => Boolean ( line ) )
311+ . join ( '\n' ) ;
312+ }
313+
314+ export function withAiClientRequestContext ( payload : AiChatPayload ) : AiChatPayload {
315+ const clientContext = payload . clientContext || createAiClientRequestContext ( ) ;
316+ return {
317+ ...payload ,
318+ clientContext,
319+ state : payload . state
320+ ? {
321+ ...payload . state ,
322+ clientContext : payload . state . clientContext || clientContext ,
323+ }
324+ : payload . state ,
325+ } ;
326+ }
327+
250328export const getClientAgentBootstrap = ( ) =>
251329 get ( '/ai/client-agent/bootstrap' ) . then ( ( res ) => res . data as ClientAgentBootstrap ) ;
252330
@@ -256,14 +334,26 @@ export const executeClientAgentTool = (payload: {
256334 request : AiChatPayload ;
257335 state ?: AiAgentClientState | null ;
258336 sourceKeys ?: string [ ] ;
259- } ) =>
260- post ( '/ai/client-agent/tools/execute' , payload ) . then ( ( res ) => res . data as ClientAgentToolResult ) ;
337+ } ) => {
338+ const request = withAiClientRequestContext ( payload . request ) ;
339+ return post ( '/ai/client-agent/tools/execute' , {
340+ ...payload ,
341+ request,
342+ state : payload . state
343+ ? {
344+ ...payload . state ,
345+ clientContext : payload . state . clientContext || request . clientContext ,
346+ }
347+ : payload . state ,
348+ } ) . then ( ( res ) => res . data as ClientAgentToolResult ) ;
349+ } ;
261350
262351async function createAiStreamRequest (
263352 endpoint : '/ai/chat/stream' | '/ai/agent/stream' ,
264353 payload : AiChatPayload ,
265354 signal ?: AbortSignal
266355) {
356+ const requestPayload = withAiClientRequestContext ( payload ) ;
267357 let token = authService . getAccessToken ( ) ;
268358 const request = ( ) =>
269359 fetch ( `${ getApiUrl ( ) } ${ endpoint } ` , {
@@ -272,7 +362,7 @@ async function createAiStreamRequest(
272362 'Content-Type' : 'application/json' ,
273363 ...( token ? { Authorization : `Bearer ${ token } ` } : { } ) ,
274364 } ,
275- body : JSON . stringify ( payload ) ,
365+ body : JSON . stringify ( requestPayload ) ,
276366 signal,
277367 } ) ;
278368
0 commit comments