Initial commit: GovAI 政务AI平台

This commit is contained in:
freedakgmail
2026-06-15 23:48:37 +08:00
commit 0f490f72a9
245 changed files with 51669 additions and 0 deletions
+74
View File
@@ -0,0 +1,74 @@
package dify
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"time"
)
type Client struct {
baseURL string
httpClient *http.Client
}
func NewClient(baseURL string) *Client {
return &Client{
baseURL: baseURL,
httpClient: &http.Client{
Timeout: 120 * time.Second,
},
}
}
func (c *Client) do(ctx context.Context, method, path, apiKey string, body any) (*http.Response, error) {
var bodyReader io.Reader
if body != nil {
data, err := json.Marshal(body)
if err != nil {
return nil, fmt.Errorf("marshal request: %w", err)
}
bodyReader = bytes.NewReader(data)
}
req, err := http.NewRequestWithContext(ctx, method, c.baseURL+path, bodyReader)
if err != nil {
return nil, fmt.Errorf("create request: %w", err)
}
req.Header.Set("Authorization", "Bearer "+apiKey)
req.Header.Set("Content-Type", "application/json")
resp, err := c.httpClient.Do(req)
if err != nil {
return nil, fmt.Errorf("execute request: %w", err)
}
if resp.StatusCode >= 400 {
defer resp.Body.Close()
var apiErr APIError
if err := json.NewDecoder(resp.Body).Decode(&apiErr); err != nil {
return nil, fmt.Errorf("dify API error (status %d)", resp.StatusCode)
}
apiErr.Status = resp.StatusCode
return nil, &apiErr
}
return resp, nil
}
func (c *Client) doJSON(ctx context.Context, method, path, apiKey string, body any, result any) error {
resp, err := c.do(ctx, method, path, apiKey, body)
if err != nil {
return err
}
defer resp.Body.Close()
if result != nil {
return json.NewDecoder(resp.Body).Decode(result)
}
return nil
}
+102
View File
@@ -0,0 +1,102 @@
package dify
import (
"bufio"
"context"
"encoding/json"
"fmt"
"io"
"strings"
)
// ChatStream sends a chat message and returns a reader for SSE events.
// Caller is responsible for closing the returned io.ReadCloser.
func (c *Client) ChatStream(ctx context.Context, apiKey string, req *ChatRequest) (io.ReadCloser, error) {
req.ResponseMode = "streaming"
resp, err := c.do(ctx, "POST", "/chat-messages", apiKey, req)
if err != nil {
return nil, err
}
return resp.Body, nil
}
// ChatBlocking sends a chat message and waits for the complete response.
func (c *Client) ChatBlocking(ctx context.Context, apiKey string, req *ChatRequest) (*ChatStreamEvent, error) {
req.ResponseMode = "blocking"
var result ChatStreamEvent
if err := c.doJSON(ctx, "POST", "/chat-messages", apiKey, req, &result); err != nil {
return nil, err
}
return &result, nil
}
// ParseSSEStream parses a Dify SSE stream and calls handler for each event.
func ParseSSEStream(reader io.Reader, handler func(event ChatStreamEvent) error) error {
scanner := bufio.NewScanner(reader)
scanner.Buffer(make([]byte, 64*1024), 256*1024)
for scanner.Scan() {
line := scanner.Text()
if !strings.HasPrefix(line, "data: ") {
continue
}
data := strings.TrimPrefix(line, "data: ")
if data == "[DONE]" {
break
}
var event ChatStreamEvent
if err := json.Unmarshal([]byte(data), &event); err != nil {
continue
}
if err := handler(event); err != nil {
return err
}
}
return scanner.Err()
}
// ListConversations returns the user's conversation list for an app.
func (c *Client) ListConversations(ctx context.Context, apiKey, user string, limit int, firstID string) (*ConversationListResponse, error) {
path := fmt.Sprintf("/conversations?user=%s&limit=%d", user, limit)
if firstID != "" {
path += "&first_id=" + firstID
}
var result ConversationListResponse
if err := c.doJSON(ctx, "GET", path, apiKey, nil, &result); err != nil {
return nil, err
}
return &result, nil
}
// ListMessages returns messages in a conversation.
func (c *Client) ListMessages(ctx context.Context, apiKey, user, conversationID string, limit int, firstID string) (*MessageListResponse, error) {
path := fmt.Sprintf("/messages?user=%s&conversation_id=%s&limit=%d", user, conversationID, limit)
if firstID != "" {
path += "&first_id=" + firstID
}
var result MessageListResponse
if err := c.doJSON(ctx, "GET", path, apiKey, nil, &result); err != nil {
return nil, err
}
return &result, nil
}
// DeleteConversation deletes a conversation.
func (c *Client) DeleteConversation(ctx context.Context, apiKey, user, conversationID string) error {
body := map[string]string{"user": user}
return c.doJSON(ctx, "DELETE", "/conversations/"+conversationID, apiKey, body, nil)
}
// SubmitFeedback submits feedback for a message.
func (c *Client) SubmitFeedback(ctx context.Context, apiKey, messageID string, req *FeedbackRequest) error {
return c.doJSON(ctx, "POST", "/messages/"+messageID+"/feedbacks", apiKey, req, nil)
}
+96
View File
@@ -0,0 +1,96 @@
package dify
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"mime/multipart"
"net/http"
)
// CreateDataset creates a new knowledge base (dataset) in Dify.
func (c *Client) CreateDataset(ctx context.Context, apiKey string, req *DatasetCreateRequest) (*Dataset, error) {
var result Dataset
if err := c.doJSON(ctx, "POST", "/datasets", apiKey, req, &result); err != nil {
return nil, err
}
return &result, nil
}
// DeleteDataset deletes a knowledge base.
func (c *Client) DeleteDataset(ctx context.Context, apiKey, datasetID string) error {
return c.doJSON(ctx, "DELETE", "/datasets/"+datasetID, apiKey, nil, nil)
}
// UploadDocument uploads a file to a dataset for indexing.
func (c *Client) UploadDocument(ctx context.Context, apiKey, datasetID string, filename string, fileReader io.Reader) (*DocumentIndexingStatus, error) {
var buf bytes.Buffer
writer := multipart.NewWriter(&buf)
part, err := writer.CreateFormFile("file", filename)
if err != nil {
return nil, fmt.Errorf("create form file: %w", err)
}
if _, err := io.Copy(part, fileReader); err != nil {
return nil, fmt.Errorf("copy file: %w", err)
}
// indexing mode
if err := writer.WriteField("indexing_technique", "high_quality"); err != nil {
return nil, err
}
if err := writer.WriteField("process_rule", `{"mode": "automatic"}`); err != nil {
return nil, err
}
writer.Close()
path := fmt.Sprintf("/datasets/%s/document/create_by_file", datasetID)
req, err := http.NewRequestWithContext(ctx, "POST", c.baseURL+path, &buf)
if err != nil {
return nil, err
}
req.Header.Set("Authorization", "Bearer "+apiKey)
req.Header.Set("Content-Type", writer.FormDataContentType())
resp, err := c.httpClient.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode >= 400 {
body, _ := io.ReadAll(resp.Body)
return nil, fmt.Errorf("upload failed (status %d): %s", resp.StatusCode, string(body))
}
// Dify returns document info in response
var result struct {
Document DocumentIndexingStatus `json:"document"`
}
if err := decodeJSON(resp.Body, &result); err != nil {
return nil, err
}
return &result.Document, nil
}
// DeleteDocument deletes a document from a dataset.
func (c *Client) DeleteDocument(ctx context.Context, apiKey, datasetID, documentID string) error {
path := fmt.Sprintf("/datasets/%s/documents/%s", datasetID, documentID)
return c.doJSON(ctx, "DELETE", path, apiKey, nil, nil)
}
// GetDocumentIndexingStatus checks the indexing status of a document.
func (c *Client) GetDocumentIndexingStatus(ctx context.Context, apiKey, datasetID, batch string) (*DocumentIndexingStatus, error) {
path := fmt.Sprintf("/datasets/%s/documents/%s/indexing-status", datasetID, batch)
var result DocumentIndexingStatus
if err := c.doJSON(ctx, "GET", path, apiKey, nil, &result); err != nil {
return nil, err
}
return &result, nil
}
func decodeJSON(r io.Reader, v any) error {
return json.NewDecoder(r).Decode(v)
}
+115
View File
@@ -0,0 +1,115 @@
package dify
import "time"
// --- Request types ---
type ChatRequest struct {
Query string `json:"query"`
Inputs map[string]any `json:"inputs,omitempty"`
ConversationID string `json:"conversation_id,omitempty"`
User string `json:"user"`
ResponseMode string `json:"response_mode"`
}
type CompletionRequest struct {
Inputs map[string]any `json:"inputs"`
User string `json:"user"`
ResponseMode string `json:"response_mode"`
}
type FeedbackRequest struct {
Rating string `json:"rating"` // "like", "dislike", null
User string `json:"user"`
}
// --- Response types ---
type ChatStreamEvent struct {
Event string `json:"event"`
TaskID string `json:"task_id"`
MessageID string `json:"message_id"`
ConversationID string `json:"conversation_id"`
Answer string `json:"answer"`
Metadata map[string]any `json:"metadata,omitempty"`
CreatedAt int64 `json:"created_at"`
}
type MessageEndMetadata struct {
Usage TokenUsage `json:"usage"`
}
type TokenUsage struct {
PromptTokens int `json:"prompt_tokens"`
CompletionTokens int `json:"completion_tokens"`
TotalTokens int `json:"total_tokens"`
}
type Conversation struct {
ID string `json:"id"`
Name string `json:"name"`
Inputs any `json:"inputs"`
Status string `json:"status"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
type ConversationListResponse struct {
Data []Conversation `json:"data"`
HasMore bool `json:"has_more"`
Limit int `json:"limit"`
}
type Message struct {
ID string `json:"id"`
ConversationID string `json:"conversation_id"`
Query string `json:"query"`
Answer string `json:"answer"`
Feedback *Feedback `json:"feedback"`
CreatedAt int64 `json:"created_at"`
Inputs map[string]any `json:"inputs"`
}
type Feedback struct {
Rating string `json:"rating"`
}
type MessageListResponse struct {
Data []Message `json:"data"`
HasMore bool `json:"has_more"`
Limit int `json:"limit"`
}
// --- Dataset/Knowledge types ---
type DatasetCreateRequest struct {
Name string `json:"name"`
Description string `json:"description,omitempty"`
}
type Dataset struct {
ID string `json:"id"`
Name string `json:"name"`
Description string `json:"description"`
DocCount int `json:"document_count"`
CreatedAt time.Time `json:"created_at"`
}
type DocumentIndexingStatus struct {
ID string `json:"id"`
IndexingStatus string `json:"indexing_status"`
ProcessingStart string `json:"processing_started_at"`
CompletedAt string `json:"completed_at"`
}
// --- Error ---
type APIError struct {
Code string `json:"code"`
Message string `json:"message"`
Status int `json:"status"`
}
func (e *APIError) Error() string {
return e.Message
}