package ai import ( "context" "encoding/json" "fmt" "strings" ) type Service struct { client *Client } type Usage struct { ProviderRequestID string Model string PromptTokens int CompletionTokens int TotalTokens int LatencyMS int64 } type PolishResult struct { Text string Usage Usage } type DocumentResult struct { Title string ContentMarkdown string ContentPlain string Sections []DocumentSection Summary string Usage Usage } type DocumentSection struct { Heading string `json:"heading"` Content string `json:"content"` } type TableAnalysisInput struct { Topic string `json:"topic"` FileID string `json:"file_id"` FileName string `json:"file_name"` FileType string `json:"file_type"` FileSize int64 `json:"file_size"` Profile TableProfile `json:"profile"` } type TableAnalysisResult struct { Title string Metrics []TableMetric Preview TablePreview Alerts []TableAlert Interpretation string Summary string Usage Usage } type TableMetric struct { Name string `json:"name"` Value string `json:"value"` ChangeText string `json:"change_text"` Trend string `json:"trend"` } type TablePreview struct { Columns []string `json:"columns"` Rows [][]string `json:"rows"` TotalRows int `json:"total_rows"` } type TableAlert struct { Level string `json:"level"` Title string `json:"title"` Description string `json:"description"` } type TableProfile struct { Columns any `json:"columns,omitempty"` SampleRows any `json:"sample_rows,omitempty"` RowCount int `json:"row_count,omitempty"` Stats any `json:"stats,omitempty"` Outliers any `json:"outliers,omitempty"` Parsed bool `json:"parsed"` Message string `json:"message,omitempty"` } type MeetingSummaryInput struct { MeetingTitle string StartAt string EndAt string Participants []string Files []string } type MeetingSummaryResult struct { Conclusions []string Decisions []string ActionItems []MeetingActionItem Usage Usage } type MeetingActionItem struct { OwnerName string `json:"owner_name"` Content string `json:"content"` DueText string `json:"due_text"` Status string `json:"status"` } func NewService(client *Client) *Service { return &Service{client: client} } func (s *Service) Configured() bool { return s != nil && s.client != nil && s.client.Configured() } func (s *Service) Polish(ctx context.Context, sourceText, scene, tone string, maxOutputChars int) (PolishResult, error) { if s == nil || s.client == nil { return PolishResult{}, ErrNotConfigured } if maxOutputChars <= 0 { maxOutputChars = 300 } system := "你是 IM 工作台的企业办公文案助手。请只输出 JSON,不要输出 Markdown,不要输出额外解释。" user := fmt.Sprintf(`请润色下面的中文工作文案。 要求: 1. 语气:%s。 2. 场景:%s。 3. 输出不超过 %d 个中文字符。 4. 保持事实不变,不编造数据、人名、时间和结论。 5. 只输出 JSON:{"polished_text":"..."}。 原文: %s`, firstNonEmpty(tone, "professional"), firstNonEmpty(scene, "general"), maxOutputChars, sourceText) resp, err := s.client.Generate(ctx, GenerateRequest{ Feature: "text_polish", Model: s.client.DefaultModel(), Messages: []Message{ {Role: "system", Content: system}, {Role: "user", Content: user}, }, MaxTokens: maxOutputChars * 3, }) if err != nil { return PolishResult{}, err } var out struct { PolishedText string `json:"polished_text"` } if err := json.Unmarshal([]byte(ExtractJSONObject(resp.Content)), &out); err != nil { return PolishResult{}, fmt.Errorf("%w: %v", ErrBadOutput, err) } out.PolishedText = strings.TrimSpace(out.PolishedText) if out.PolishedText == "" { return PolishResult{}, ErrBadOutput } return PolishResult{Text: out.PolishedText, Usage: usageFromResponse(resp)}, nil } func (s *Service) GenerateDocument(ctx context.Context, sourceText, titleHint, documentType, language string) (DocumentResult, error) { if s == nil || s.client == nil { return DocumentResult{}, ErrNotConfigured } system := "你是 IM 工作台的企业办公文档助手。请严格输出 JSON,不要输出 Markdown code fence,不要输出额外解释。" user := fmt.Sprintf(`请根据用户输入生成一份结构化办公文档。 要求: 1. 输出语言:%s。 2. 文档类型:%s。 3. 标题提示:%s。 4. 内容要忠于原文,不编造事实。 5. sections 建议 3 到 6 个,每个 section 包含 heading 和 content。 6. content_markdown 要适合直接展示,使用二级标题和列表。 7. 只输出 JSON,格式: { "title": "string", "summary": "string", "content_markdown": "string", "content_plain": "string", "sections": [{"heading":"string","content":"string"}] } 用户输入: %s`, firstNonEmpty(language, "zh-CN"), firstNonEmpty(documentType, "general"), strings.TrimSpace(titleHint), sourceText) resp, err := s.client.Generate(ctx, GenerateRequest{ Feature: "smart_document", Model: s.client.DefaultModel(), Messages: []Message{ {Role: "system", Content: system}, {Role: "user", Content: user}, }, MaxTokens: 2400, }) if err != nil { return DocumentResult{}, err } var out struct { Title string `json:"title"` Summary string `json:"summary"` ContentMarkdown string `json:"content_markdown"` ContentPlain string `json:"content_plain"` Sections []DocumentSection `json:"sections"` } if err := json.Unmarshal([]byte(ExtractJSONObject(resp.Content)), &out); err != nil { return DocumentResult{}, fmt.Errorf("%w: %v", ErrBadOutput, err) } out.Title = strings.TrimSpace(firstNonEmpty(out.Title, titleHint, "智能文档")) out.ContentMarkdown = strings.TrimSpace(out.ContentMarkdown) if out.ContentMarkdown == "" && len(out.Sections) > 0 { out.ContentMarkdown = markdownFromSections(out.Sections) } if strings.TrimSpace(out.ContentPlain) == "" { out.ContentPlain = plainFromSections(out.Sections) } if out.ContentMarkdown == "" { return DocumentResult{}, ErrBadOutput } return DocumentResult{ Title: out.Title, ContentMarkdown: out.ContentMarkdown, ContentPlain: strings.TrimSpace(out.ContentPlain), Sections: out.Sections, Summary: strings.TrimSpace(out.Summary), Usage: usageFromResponse(resp), }, nil } func (s *Service) AnalyzeTable(ctx context.Context, input TableAnalysisInput) (TableAnalysisResult, error) { if s == nil || s.client == nil { return TableAnalysisResult{}, ErrNotConfigured } system := "你是 IM 工作台的数据分析助手。请严格输出 JSON,不要输出 Markdown code fence,不要输出额外解释。" profileJSON, _ := json.Marshal(input.Profile) user := fmt.Sprintf(`请根据报表信息生成一份简洁的数据解读报告。 注意: 1. 若 profile.parsed=true,请优先依据 columns、sample_rows、stats、outliers 做分析。 2. 若 profile.parsed=false,说明当前没有可用表格内容,只能基于文件元信息给出分析方向,不要编造具体真实数值。 报表主题:%s 文件名:%s 文件类型:%s 文件大小:%d bytes 表格画像:%s 只输出 JSON: { "title": "string", "summary": "string", "metrics": [{"name":"string","value":"string","change_text":"string","trend":"up|down|flat"}], "preview": {"columns":["string"],"rows":[["string"]],"total_rows":0}, "alerts": [{"level":"info|warning|danger","title":"string","description":"string"}], "interpretation": "string" }`, input.Topic, input.FileName, input.FileType, input.FileSize, string(profileJSON)) resp, err := s.client.Generate(ctx, GenerateRequest{ Feature: "smart_table", Model: s.client.ComplexModel(), Messages: []Message{ {Role: "system", Content: system}, {Role: "user", Content: user}, }, MaxTokens: 1800, }) if err != nil { return TableAnalysisResult{}, err } var out struct { Title string `json:"title"` Summary string `json:"summary"` Metrics []TableMetric `json:"metrics"` Preview TablePreview `json:"preview"` Alerts []TableAlert `json:"alerts"` Interpretation string `json:"interpretation"` } if err := json.Unmarshal([]byte(ExtractJSONObject(resp.Content)), &out); err != nil { return TableAnalysisResult{}, fmt.Errorf("%w: %v", ErrBadOutput, err) } out.Title = firstNonEmpty(out.Title, input.Topic, input.FileName, "智能解读报告") if strings.TrimSpace(out.Interpretation) == "" { return TableAnalysisResult{}, ErrBadOutput } return TableAnalysisResult{ Title: out.Title, Metrics: out.Metrics, Preview: out.Preview, Alerts: out.Alerts, Interpretation: strings.TrimSpace(out.Interpretation), Summary: strings.TrimSpace(out.Summary), Usage: usageFromResponse(resp), }, nil } func (s *Service) SummarizeMeeting(ctx context.Context, input MeetingSummaryInput) (MeetingSummaryResult, error) { if s == nil || s.client == nil { return MeetingSummaryResult{}, ErrNotConfigured } system := "你是 IM 工作台的会议纪要助手。请严格输出 JSON,不要输出 Markdown code fence,不要输出额外解释。" payload, _ := json.Marshal(input) user := fmt.Sprintf(`请根据会议上下文生成会议 AI 总结。 要求: 1. 不编造会议中没有出现的事实。 2. 若附件内容不足,请基于标题和上下文给出谨慎总结。 3. action_items 的 status 固定为 pending。 4. 只输出 JSON: { "conclusions": ["string"], "decisions": ["string"], "action_items": [{"owner_name":"string","content":"string","due_text":"string","status":"pending"}] } 会议上下文: %s`, string(payload)) resp, err := s.client.Generate(ctx, GenerateRequest{ Feature: "meeting_summary", Model: s.client.ComplexModel(), Messages: []Message{ {Role: "system", Content: system}, {Role: "user", Content: user}, }, MaxTokens: 1800, }) if err != nil { return MeetingSummaryResult{}, err } var out struct { Conclusions []string `json:"conclusions"` Decisions []string `json:"decisions"` ActionItems []MeetingActionItem `json:"action_items"` } if err := json.Unmarshal([]byte(ExtractJSONObject(resp.Content)), &out); err != nil { return MeetingSummaryResult{}, fmt.Errorf("%w: %v", ErrBadOutput, err) } if len(out.Conclusions) == 0 && len(out.Decisions) == 0 && len(out.ActionItems) == 0 { return MeetingSummaryResult{}, ErrBadOutput } for i := range out.ActionItems { if strings.TrimSpace(out.ActionItems[i].Status) == "" { out.ActionItems[i].Status = "pending" } } return MeetingSummaryResult{Conclusions: out.Conclusions, Decisions: out.Decisions, ActionItems: out.ActionItems, Usage: usageFromResponse(resp)}, nil } func usageFromResponse(resp GenerateResponse) Usage { return Usage{ ProviderRequestID: resp.ProviderRequestID, Model: resp.Model, PromptTokens: resp.PromptTokens, CompletionTokens: resp.CompletionTokens, TotalTokens: resp.TotalTokens, LatencyMS: resp.LatencyMS, } } func markdownFromSections(sections []DocumentSection) string { var b strings.Builder for _, section := range sections { heading := strings.TrimSpace(section.Heading) content := strings.TrimSpace(section.Content) if heading == "" && content == "" { continue } if heading != "" { b.WriteString("## ") b.WriteString(heading) b.WriteString("\n") } if content != "" { b.WriteString(content) b.WriteString("\n\n") } } return strings.TrimSpace(b.String()) } func plainFromSections(sections []DocumentSection) string { parts := make([]string, 0, len(sections)*2) for _, section := range sections { if heading := strings.TrimSpace(section.Heading); heading != "" { parts = append(parts, heading) } if content := strings.TrimSpace(section.Content); content != "" { parts = append(parts, content) } } return strings.Join(parts, "\n") }