Add complete schedule functionality including: - Schedule screen with weekly course table view - Course detail screen with transparent modal presentation - New ScheduleStack navigator integrated into main tab bar - Schedule service for API interactions - Type definitions for course entities Also includes bug fixes for group invite/request handlers to include required groupId parameter.
264 lines
9.2 KiB
Go
264 lines
9.2 KiB
Go
package main
|
||
|
||
import (
|
||
"bytes"
|
||
"encoding/json"
|
||
"flag"
|
||
"fmt"
|
||
"io"
|
||
"net/http"
|
||
"strings"
|
||
"time"
|
||
)
|
||
|
||
const moderationSystemPrompt = `你是中文社区的内容审核助手,负责对"帖子标题、正文、配图"做联合审核。目标是平衡社区安全与正常交流:必须拦截高风险违规内容,但不要误伤正常玩梗、二创、吐槽和轻度调侃。请只输出指定JSON。
|
||
|
||
审核流程:
|
||
1) 先判断是否命中硬性违规;
|
||
2) 再判断语境(玩笑/自嘲/朋友间互动/作品讨论);
|
||
3) 做文图交叉判断(文本+图片合并理解);
|
||
4) 给出 approved 与简短 reason。
|
||
|
||
硬性违规(命中任一项必须 approved=false):
|
||
A. 宣传对立与煽动撕裂:
|
||
- 明确煽动群体对立、地域对立、性别对立、民族宗教对立,鼓动仇恨、排斥、报复。
|
||
B. 严重人身攻击与网暴引导:
|
||
- 持续性侮辱贬损、羞辱人格、号召围攻/骚扰/挂人/线下冲突。
|
||
C. 开盒/人肉/隐私暴露:
|
||
- 故意公开、拼接、索取他人可识别隐私信息(姓名+联系方式、身份证号、住址、学校单位、车牌、定位轨迹等);
|
||
- 图片/截图中出现可识别隐私信息并伴随曝光意图,也按违规处理。
|
||
D. 其他高危违规:
|
||
- 违法犯罪、暴力威胁、极端仇恨、色情低俗、诈骗引流、恶意广告等。
|
||
|
||
放行规则(以下通常 approved=true):
|
||
- 正常玩梗、表情包、谐音梗、二次创作、无恶意的吐槽;
|
||
- 非定向、轻度口语化吐槽(无明确攻击对象、无网暴号召、无隐私暴露);
|
||
- 对社会事件/作品的理性讨论、观点争论(即使语气尖锐,但未煽动对立或人身攻击)。
|
||
|
||
边界判定:
|
||
- 若只是"梗文化表达"且不指向现实伤害,优先通过;
|
||
- 若存在明确伤害意图(煽动、围攻、曝光隐私),必须拒绝;
|
||
- 对模糊内容不因个别粗口直接拒绝,需结合对象、意图、号召性和可执行性综合判断。
|
||
|
||
reason 要求:
|
||
- approved=false 时:中文10-30字,说明核心违规点;
|
||
- approved=true 时:reason 为空字符串。
|
||
|
||
输出格式(严格):
|
||
仅输出一行JSON对象,不要Markdown,不要额外解释:
|
||
{"approved": true/false, "reason": "..."}`
|
||
|
||
type chatMessage struct {
|
||
Role string `json:"role"`
|
||
Content interface{} `json:"content"`
|
||
}
|
||
|
||
type contentPart struct {
|
||
Type string `json:"type"`
|
||
Text string `json:"text,omitempty"`
|
||
}
|
||
|
||
type chatCompletionsRequest struct {
|
||
Model string `json:"model"`
|
||
Messages []chatMessage `json:"messages"`
|
||
Temperature float64 `json:"temperature,omitempty"`
|
||
MaxTokens int `json:"max_tokens,omitempty"`
|
||
EnableThinking *bool `json:"enable_thinking,omitempty"` // qwen3.5思考模式控制
|
||
ThinkingBudget *int `json:"thinking_budget,omitempty"` // 思考过程最大token数
|
||
ResponseFormat *responseFormatConfig `json:"response_format,omitempty"` // 响应格式
|
||
}
|
||
|
||
type responseFormatConfig struct {
|
||
Type string `json:"type"` // "text" or "json_object"
|
||
}
|
||
|
||
type chatCompletionsResponse struct {
|
||
Choices []struct {
|
||
Message struct {
|
||
Content string `json:"content"`
|
||
} `json:"message"`
|
||
FinishReason string `json:"finish_reason"`
|
||
} `json:"choices"`
|
||
Usage struct {
|
||
PromptTokens int `json:"prompt_tokens"`
|
||
CompletionTokens int `json:"completion_tokens"`
|
||
TotalTokens int `json:"total_tokens"`
|
||
} `json:"usage"`
|
||
}
|
||
|
||
func main() {
|
||
baseURL := flag.String("url", "https://api.littlelan.cn/", "API base URL")
|
||
apiKey := flag.String("key", "", "API key")
|
||
model := flag.String("model", "qwen3.5-plus", "Model name")
|
||
maxTokens := flag.Int("max-tokens", 220, "Max tokens for completion")
|
||
enableThinking := flag.Bool("enable-thinking", false, "Enable thinking mode for qwen3.5")
|
||
flag.Parse()
|
||
|
||
if *apiKey == "" {
|
||
fmt.Println("Error: API key is required. Use -key flag")
|
||
return
|
||
}
|
||
|
||
// 测试用例
|
||
testCases := []struct {
|
||
name string
|
||
content string
|
||
}{
|
||
{
|
||
name: "简单正常内容",
|
||
content: "帖子标题:今天天气真好\n帖子内容:出门散步,心情愉快!",
|
||
},
|
||
{
|
||
name: "中等长度内容",
|
||
content: "帖子标题:分享我的学习经验\n帖子内容:最近在学习Go语言,发现这门语言真的很适合后端开发。并发处理特别方便,goroutine和channel的设计非常优雅。有一起学习的小伙伴吗?",
|
||
},
|
||
{
|
||
name: "较长内容",
|
||
content: "帖子标题:关于校园生活的一些思考\n帖子内容:大学四年转眼就过去了,回想起来有很多感慨。刚入学的时候什么都不懂,现在感觉自己成长了很多。在这里想分享一些自己的经验,希望能对学弟学妹们有所帮助。首先是学习方面,一定要认真听课,做好笔记。其次是社交方面,多参加社团活动,结交志同道合的朋友。最后是规划方面,早点想清楚自己想做什么,为之努力。",
|
||
},
|
||
}
|
||
|
||
client := &http.Client{Timeout: 120 * time.Second}
|
||
|
||
fmt.Println("============================================")
|
||
fmt.Printf("模型: %s\n", *model)
|
||
fmt.Printf("API URL: %s\n", *baseURL)
|
||
fmt.Printf("MaxTokens 设置: %d\n", *maxTokens)
|
||
fmt.Printf("EnableThinking: %v\n", *enableThinking)
|
||
fmt.Println("============================================")
|
||
|
||
for _, tc := range testCases {
|
||
fmt.Printf("\n========== 测试: %s ==========\n", tc.name)
|
||
fmt.Printf("内容长度: %d 字符\n", len(tc.content))
|
||
|
||
userPrompt := fmt.Sprintf("%s\n图片批次:1/1(本次仅提供当前批次图片)", tc.content)
|
||
|
||
reqBody := chatCompletionsRequest{
|
||
Model: *model,
|
||
Messages: []chatMessage{
|
||
{Role: "system", Content: moderationSystemPrompt},
|
||
{Role: "user", Content: []contentPart{{Type: "text", Text: userPrompt}}},
|
||
},
|
||
Temperature: 0.1,
|
||
MaxTokens: *maxTokens,
|
||
}
|
||
// 设置思考模式
|
||
if !*enableThinking {
|
||
reqBody.EnableThinking = enableThinking
|
||
// 设置思考预算为0,完全禁用思考
|
||
zero := 0
|
||
reqBody.ThinkingBudget = &zero
|
||
}
|
||
// 使用JSON输出格式
|
||
reqBody.ResponseFormat = &responseFormatConfig{Type: "json_object"}
|
||
|
||
data, err := json.Marshal(reqBody)
|
||
if err != nil {
|
||
fmt.Printf("Error marshaling request: %v\n", err)
|
||
continue
|
||
}
|
||
|
||
endpoint := strings.TrimRight(*baseURL, "/") + "/v1/chat/completions"
|
||
if strings.HasSuffix(strings.TrimRight(*baseURL, "/"), "/v1") {
|
||
endpoint = strings.TrimRight(*baseURL, "/") + "/chat/completions"
|
||
}
|
||
|
||
req, err := http.NewRequest(http.MethodPost, endpoint, bytes.NewReader(data))
|
||
if err != nil {
|
||
fmt.Printf("Error creating request: %v\n", err)
|
||
continue
|
||
}
|
||
req.Header.Set("Content-Type", "application/json")
|
||
req.Header.Set("Authorization", "Bearer "+*apiKey)
|
||
|
||
start := time.Now()
|
||
resp, err := client.Do(req)
|
||
if err != nil {
|
||
fmt.Printf("Error sending request: %v\n", err)
|
||
continue
|
||
}
|
||
|
||
body, err := io.ReadAll(resp.Body)
|
||
resp.Body.Close()
|
||
if err != nil {
|
||
fmt.Printf("Error reading response: %v\n", err)
|
||
continue
|
||
}
|
||
|
||
elapsed := time.Since(start)
|
||
|
||
if resp.StatusCode >= 400 {
|
||
fmt.Printf("API Error: status=%d, body=%s\n", resp.StatusCode, string(body))
|
||
continue
|
||
}
|
||
|
||
var parsed chatCompletionsResponse
|
||
if err := json.Unmarshal(body, &parsed); err != nil {
|
||
fmt.Printf("Error parsing response: %v\n", err)
|
||
fmt.Printf("Raw response: %s\n", string(body))
|
||
continue
|
||
}
|
||
|
||
if len(parsed.Choices) == 0 {
|
||
fmt.Println("No choices in response")
|
||
fmt.Printf("Raw response: %s\n", string(body))
|
||
continue
|
||
}
|
||
|
||
fmt.Printf("响应时间: %v\n", elapsed)
|
||
fmt.Printf("Finish Reason: %s\n", parsed.Choices[0].FinishReason)
|
||
fmt.Printf("Token使用情况:\n")
|
||
fmt.Printf(" - PromptTokens: %d\n", parsed.Usage.PromptTokens)
|
||
fmt.Printf(" - CompletionTokens: %d\n", parsed.Usage.CompletionTokens)
|
||
fmt.Printf(" - TotalTokens: %d\n", parsed.Usage.TotalTokens)
|
||
|
||
output := parsed.Choices[0].Message.Content
|
||
fmt.Printf("输出内容长度: %d 字符\n", len(output))
|
||
|
||
// 检查输出是否符合预期
|
||
if parsed.Usage.CompletionTokens > *maxTokens {
|
||
fmt.Printf("\n⚠️ 警告: CompletionTokens (%d) 超过了 max_tokens 设置 (%d)!\n",
|
||
parsed.Usage.CompletionTokens, *maxTokens)
|
||
}
|
||
|
||
if len(output) > 500 {
|
||
fmt.Printf("\n⚠️ 警告: 输出内容过长! 长度=%d\n", len(output))
|
||
fmt.Printf("前500字符:\n%s...\n", output[:min(500, len(output))])
|
||
} else {
|
||
fmt.Printf("输出内容: %s\n", output)
|
||
}
|
||
|
||
// 尝试解析JSON
|
||
extractJSONObject := func(raw string) string {
|
||
text := strings.TrimSpace(raw)
|
||
start := strings.Index(text, "{")
|
||
end := strings.LastIndex(text, "}")
|
||
if start >= 0 && end > start {
|
||
return text[start : end+1]
|
||
}
|
||
return text
|
||
}
|
||
|
||
jsonStr := extractJSONObject(output)
|
||
var result struct {
|
||
Approved bool `json:"approved"`
|
||
Reason string `json:"reason"`
|
||
}
|
||
if err := json.Unmarshal([]byte(jsonStr), &result); err != nil {
|
||
fmt.Printf("\n⚠️ 警告: 无法解析JSON输出: %v\n", err)
|
||
fmt.Printf("提取的JSON: %s\n", jsonStr)
|
||
} else {
|
||
fmt.Printf("\n✓ 解析成功: approved=%v, reason=\"%s\"\n", result.Approved, result.Reason)
|
||
}
|
||
}
|
||
|
||
fmt.Println("\n========== 测试完成 ==========")
|
||
}
|
||
|
||
func min(a, b int) int {
|
||
if a < b {
|
||
return a
|
||
}
|
||
return b
|
||
}
|