File size: 3,287 Bytes
f0634fb
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
/**
 * Managed-platform `/tools` dispatch: POSTs `{method, params}` to
 * `{kimiCodeBaseUrl}/tools` with a Bearer access token, the same wire
 * shape the backend tool surface expects (see `chat_title` below).
 *
 * `chat_title` generates a short session title from a chat excerpt:
 *
 *   { "method": "chat_title", "params": { "chat_content": "user: ...\nassistant: ..." } }
 *   → { "title": "..." }
 */

import { readApiErrorMessage } from './api-error';
import { kimiCodeBaseUrl } from './managed-usage';
import { isRecord } from './utils';

export interface FetchChatTitleOk {
  readonly kind: 'ok';
  readonly title: string;
}

export interface FetchChatTitleError {
  readonly kind: 'error';
  readonly status?: number;
  readonly message: string;
}

export type FetchChatTitleResult = FetchChatTitleOk | FetchChatTitleError;

export function kimiCodeToolsUrl(baseUrl?: string): string {
  return `${(baseUrl ?? kimiCodeBaseUrl()).replace(/\/+$/, '')}/tools`;
}

export async function fetchChatTitle(
  url: string,
  accessToken: string,
  chatContent: string,
  opts: { timeoutMs?: number; headers?: Record<string, string>; signal?: AbortSignal } = {},
): Promise<FetchChatTitleResult> {
  const controller = new AbortController();
  const onExternalAbort = () => {
    controller.abort();
  };
  if (opts.signal !== undefined) {
    if (opts.signal.aborted) controller.abort();
    else opts.signal.addEventListener('abort', onExternalAbort, { once: true });
  }
  const timer = setTimeout(() => {
    controller.abort();
  }, opts.timeoutMs ?? 8000);
  try {
    const headers = new Headers(opts.headers);
    headers.set('Authorization', `Bearer ${accessToken}`);
    headers.set('Accept', 'application/json');
    headers.set('Content-Type', 'application/json');
    const res = await fetch(url, {
      method: 'POST',
      headers,
      body: JSON.stringify({
        method: 'chat_title',
        params: { chat_content: chatContent },
      }),
      signal: controller.signal,
    });
    if (!res.ok) {
      return {
        kind: 'error',
        status: res.status,
        message: await readApiErrorMessage(
          res,
          `Failed to generate session title: HTTP ${String(res.status)}`,
        ),
      };
    }
    const title = parseChatTitle(await res.json());
    if (title === undefined) {
      return { kind: 'error', message: 'Failed to generate session title: missing title.' };
    }
    return { kind: 'ok', title };
  } catch (error) {
    if (error instanceof Error && error.name === 'AbortError') {
      const reason =
        opts.signal?.aborted === true ? 'request aborted.' : 'request timed out.';
      return { kind: 'error', message: `Failed to generate session title: ${reason}` };
    }
    const msg = error instanceof Error ? error.message : String(error);
    return { kind: 'error', message: `Failed to generate session title: ${msg}` };
  } finally {
    clearTimeout(timer);
    opts.signal?.removeEventListener('abort', onExternalAbort);
  }
}

function parseChatTitle(payload: unknown): string | undefined {
  if (!isRecord(payload)) return undefined;
  const value = payload['title'];
  if (typeof value !== 'string') return undefined;
  const title = value.trim();
  return title.length > 0 ? title : undefined;
}