Files
apache--hertzbeat/web-app/src/app/service/ai-chat.service.ts
T
wehub-resource-sync c8cebdfeee
MCP Bash Server CI / Test MCP Bash Server (dev) (push) Has been cancelled
MCP Bash Server CI / Test MCP Bash Server (release) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:11:39 +08:00

268 lines
7.6 KiB
TypeScript

/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { Observable, Subject } from 'rxjs';
import { Message } from '../pojo/Message';
import { LocalStorageService } from './local-storage.service';
export interface ChatMessage {
content: string;
role: 'user' | 'assistant' | 'system_push';
gmtCreate: Date;
securityForm: SecurityForm;
/** Flag indicating this message is a skill report that should be displayed directly */
isSkillReport?: boolean;
}
export interface SecurityForm {
show: Boolean;
param: string;
content: string;
complete: boolean;
}
export const DEFAULT_SECURITY_FORM: SecurityForm = {
show: false,
param: '',
content: '',
complete: false
};
export interface ChatConversation {
id: number;
title: string;
gmtCreated: Date;
gmtUpdate: Date;
messages: ChatMessage[];
}
export interface ChatRequestContext {
message: string;
conversationId?: number;
}
export interface SopSchedule {
id?: number;
conversationId: number;
sopName: string;
sopParams?: string;
cronExpression: string;
enabled: boolean;
lastRunTime?: Date;
nextRunTime?: Date;
gmtCreate?: Date;
gmtUpdate?: Date;
}
export interface SkillInfo {
name: string;
description: string;
}
const chat_uri = '/chat';
const schedule_uri = '/ai/schedule';
@Injectable({
providedIn: 'root'
})
export class AiChatService {
constructor(private http: HttpClient, private localStorageService: LocalStorageService) {}
/**
* Create a new conversation
*/
createConversation(): Observable<Message<ChatConversation>> {
return this.http.post<Message<ChatConversation>>(`${chat_uri}/conversations`, {});
}
/**
* Get all conversations
*/
getConversations(): Observable<Message<ChatConversation[]>> {
return this.http.get<Message<ChatConversation[]>>(`${chat_uri}/conversations`);
}
/**
* Get a specific conversation with its history
*/
getConversation(conversationId: number): Observable<Message<ChatConversation>> {
return this.http.get<Message<ChatConversation>>(`${chat_uri}/conversations/${conversationId}`);
}
/**
* Delete a conversation
*/
deleteConversation(conversationId: number): Observable<Message<void>> {
return this.http.delete<Message<void>>(`${chat_uri}/conversations/${conversationId}`);
}
/**
* Send a message and get streaming response
*/
streamChat(message: string, conversationId?: number): Observable<ChatMessage> {
const responseSubject = new Subject<ChatMessage>();
const requestBody: ChatRequestContext = { message };
if (conversationId) {
requestBody.conversationId = conversationId;
}
const token = this.localStorageService.getAuthorizationToken();
const headers: Record<string, string> = {
'Content-Type': 'application/json',
Accept: 'text/event-stream',
'Cache-Control': 'no-cache'
};
// Add Authorization header like the interceptor does
if (token) {
headers['Authorization'] = `Bearer ${token}`;
}
// Use fetch for SSE streaming (HttpClient doesn't support true streaming)
fetch(`/api${chat_uri}/stream`, {
method: 'POST',
headers,
body: JSON.stringify(requestBody)
})
.then(response => {
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const reader = response.body?.getReader();
if (!reader) {
throw new Error('No reader available');
}
const decoder = new TextDecoder();
let buffer = '';
const readStream = (): Promise<void> => {
if (!reader) {
return Promise.resolve();
}
return reader.read().then(({ value, done }) => {
if (done) {
responseSubject.complete();
return;
}
const chunk = decoder.decode(value, { stream: true });
buffer += chunk;
const lines = buffer.split('\n');
buffer = lines.pop() || '';
for (const line of lines) {
const trimmedLine = line.trim();
if (trimmedLine.startsWith('data:')) {
const jsonStr = trimmedLine.substring(5).trim();
if (jsonStr && jsonStr !== '[DONE]') {
try {
const data = JSON.parse(jsonStr);
if (data.response !== undefined) {
responseSubject.next({
content: data.response || '',
role: 'assistant',
securityForm: DEFAULT_SECURITY_FORM,
gmtCreate: data.timestamp ? new Date(data.timestamp) : new Date()
});
}
} catch (parseError) {
console.error('Error parsing SSE data:', parseError, 'Raw data:', jsonStr);
if (jsonStr) {
responseSubject.next({
content: jsonStr,
role: 'assistant',
securityForm: DEFAULT_SECURITY_FORM,
gmtCreate: new Date()
});
}
}
}
}
}
return readStream();
});
};
return readStream();
})
.catch(error => {
console.error('Chat stream error:', error);
responseSubject.error(error);
});
return responseSubject.asObservable();
}
saveSecurityData(body: any): Observable<Message<any>> {
return this.http.post<Message<any>>(`${chat_uri}/security`, body);
}
// ===== Schedule API Methods =====
/**
* Get all schedules for a conversation
*/
getSchedules(conversationId: number): Observable<Message<SopSchedule[]>> {
return this.http.get<Message<SopSchedule[]>>(`${schedule_uri}/conversation/${conversationId}`);
}
/**
* Create a new schedule
*/
createSchedule(schedule: SopSchedule): Observable<Message<SopSchedule>> {
return this.http.post<Message<SopSchedule>>(schedule_uri, schedule);
}
/**
* Update a schedule
*/
updateSchedule(id: number, schedule: SopSchedule): Observable<Message<SopSchedule>> {
return this.http.put<Message<SopSchedule>>(`${schedule_uri}/${id}`, schedule);
}
/**
* Delete a schedule
*/
deleteSchedule(id: number): Observable<Message<void>> {
return this.http.delete<Message<void>>(`${schedule_uri}/${id}`);
}
/**
* Toggle schedule enabled status
*/
toggleSchedule(id: number, enabled: boolean): Observable<Message<SopSchedule>> {
return this.http.put<Message<SopSchedule>>(`${schedule_uri}/${id}/toggle?enabled=${enabled}`, {});
}
/**
* Get available SOP skills
*/
getAvailableSkills(): Observable<Message<SkillInfo[]>> {
return this.http.get<Message<SkillInfo[]>>(`${schedule_uri}/skills`);
}
}