File size: 3,185 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
101
/**
 * Submit user feedback to the managed Kimi Code platform.
 *
 * POSTs a JSON body to `{kimiCodeBaseUrl}/feedback` with a Bearer access
 * token. The client tags `version` with a `kimi-code-` prefix so the
 * backend can identify this client.
 */

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

export interface SubmitFeedbackBody {
  readonly session_id: string;
  readonly content: string;
  readonly version: string;
  readonly os: string;
  readonly model: string | null;
  readonly contact?: string;
  readonly info?: Record<string, unknown>;
}

export interface FetchSubmitFeedbackOk {
  readonly kind: 'ok';
  readonly feedbackId: number;
}

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

export type FetchSubmitFeedbackResult = FetchSubmitFeedbackOk | FetchSubmitFeedbackError;

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

export async function fetchSubmitFeedback(
  url: string,
  accessToken: string,
  body: SubmitFeedbackBody,
  opts: { timeoutMs?: number } = {},
): Promise<FetchSubmitFeedbackResult> {
  const controller = new AbortController();
  const timer = setTimeout(() => {
    controller.abort();
  }, opts.timeoutMs ?? 8000);
  try {
    const res = await fetch(url, {
      method: 'POST',
      headers: {
        Authorization: `Bearer ${accessToken}`,
        Accept: 'application/json',
        'Content-Type': 'application/json',
      },
      body: JSON.stringify(body),
      signal: controller.signal,
    });
    if (!res.ok) {
      return {
        kind: 'error',
        status: res.status,
        message: await readApiErrorMessage(
          res,
          `Failed to submit feedback: HTTP ${String(res.status)}`,
        ),
      };
    }
    const feedbackId = parseFeedbackId(await res.json());
    if (feedbackId === undefined) {
      return { kind: 'error', message: 'Failed to submit feedback: missing feedback_id.' };
    }
    return { kind: 'ok', feedbackId };
  } catch (error) {
    if (error instanceof Error && error.name === 'AbortError') {
      return { kind: 'error', message: 'Failed to submit feedback: request timed out.' };
    }
    const msg = error instanceof Error ? error.message : String(error);
    return { kind: 'error', message: `Failed to submit feedback: ${msg}` };
  } finally {
    clearTimeout(timer);
  }
}

function parseFeedbackId(payload: unknown): number | undefined {
  const direct = readFeedbackId(payload);
  if (direct !== undefined) return direct;
  if (typeof payload === 'object' && payload !== null && 'data' in payload) {
    return readFeedbackId((payload as { readonly data: unknown }).data);
  }
  return undefined;
}

function readFeedbackId(payload: unknown): number | undefined {
  if (typeof payload !== 'object' || payload === null) return undefined;
  const record = payload as Record<string, unknown>;
  const value = record['feedback_id'] ?? record['id'];
  return typeof value === 'number' && Number.isInteger(value) ? value : undefined;
}