Local branch had diverged from origin/master (sibling commits on the same base). Rewrote local history linearly on top of origin/master, folding in all local content: exa/hot-platform discovery hunters, leaks ledger, nightly sweep orchestrator, updated .gitignore and skill docs, plus the latest hunt output and vault state. Remote-only files (vault keys, channel scripts) were restored rather than dropped, so the resulting tree is a full union of both sides.
1 line
15 KiB
JSON
1 line
15 KiB
JSON
{"repo": "362663496/gaowang", "files": {"apps/api/internal/services/lark_ai.go": {"faeeaa08ecb027b956536b2e3e0c7d951d6b1523": {"t": 1786067571.9266026, "n": 8677, "h": "a22bd342b7aaf3a3", "c": "package services\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"strings\"\n\t\"time\"\n)\n\nconst (\n\tdeepSeekEndpoint = \"https://api.deepseek.com/chat/completions\"\n\tdeepSeekResponseMaxBytes = 32 << 10\n\tlarkAIInputMaxRunes = 300\n)\n\nvar errDeepSeekIntent = errors.New(\"deepseek intent parsing failed\")\n\nconst deepSeekIntentPrompt = `你是库存机器人的查询计划生成器。只输出一个 JSON 对象,不要回答用户问题。\n允许的 intent:inventory、low_stock、out_of_stock、inventory_summary、movements、today_changes、analytics、help、unknown。\ninventory 用于查询某个商品的库存、当前采购价、库存金额或状态,必须提取 keyword。\nlow_stock 用于查询低库存或补货预警,可选 keyword;out_of_stock 用于列出哪些商品缺货或库存为零,可选 keyword。\ninventory_summary 只用于同时查看多项库存概览,或商品种类数、低库存/缺货种类数;单项库存数量或金额统计用 analytics,询问“哪些缺货”用 out_of_stock。\nmovements 只用于列出最近流水或操作记录,可选 keyword;today_changes 只用于同时查看今天入库、出库和调整的概览,单项数量/笔数用 analytics。\nhelp 用于询问机器人会什么或怎么用;非库存相关问题返回 unknown。只有 inventory、low_stock、out_of_stock、movements 可以包含 keyword。\n\n统计、排行、比较、按条件求总量一律使用 analytics,并组合以下字段:\n- metric:inventory_quantity、inventory_value、movement_quantity、movement_count、movement_value。\n- group_by:none、product、shop、operator。必须保留用户问的维度,问店铺就用 shop,问商品就用 product,问谁就用 operator。\n- movement_type:all、inbound、sales_outbound、adjustment;inventory_* 指标留空。\n- time_range:current、today、yesterday、last_n_days、current_month、all。流水没有时间词用 all;inventory_* 只用 current。\n- days:仅 last_n_days 使用,1 到 365;sort:分组时 asc 或 desc;limit:分组时 1 到 5。\n- product_keyword、shop_keyword、operator_keyword:只保留用户明确给出的名称或编码。\n当前库存指标只允许 group_by 为 none 或 product;其余统计使用 movement_* 指标。\n\n示例:\n“哪个店铺出货最多” => {\"intent\":\"analytics\",\"metric\":\"movement_quantity\",\"group_by\":\"shop\",\"movement_type\":\"sales_outbound\",\"time_range\":\"all\",\"sort\":\"desc\",\"limit\":1}\n“今天哪个商品卖得最好” => {\"intent\":\"analytics\",\"metric\":\"movement_quantity\",\"group_by\":\"product\",\"movement_type\":\"sales_outbound\",\"time_range\":\"today\",\"sort\":\"desc\",\"limit\":1}\n“近7天张三出库多少” => {\"intent\":\"analytics\",\"metric\":\"movement_quantity\",\"group_by\":\"none\",\"movement_type\":\"sales_outbound\",\"time_range\":\"last_n_days\",\"days\":7,\"operator_keyword\":\"张三\"}\n“库存金额最高的商品” => {\"intent\":\"analytics\",\"metric\":\"inventory_value\",\"group_by\":\"product\",\"time_range\":\"current\",\"sort\":\"desc\",\"limit\":5}\n不要把不支持的维度替换成相近维度,不得生成 SQL。`\n\ntype deepSeekIntentParser struct {\n\tapiKey string\n\tmodel string\n\tendpoint string\n\tclient *http.Client\n}\n\nfunc newDeepSeekIntentParser(apiKey string, model string) *deepSeekIntentParser {\n\tif strings.TrimSpace(apiKey) == \"\" {\n\t\treturn nil\n\t}\n\treturn &deepSeekIntentParser{\n\t\tapiKey: apiKey,\n\t\tmodel: model,\n\t\tendpoint: deepSeekEndpoint,\n\t\tclient: &http.Client{Timeout: 6 * time.Second},\n\t}\n}\n\nfunc (p *deepSeekIntentParser) Parse(ctx context.Context, input string) (larkCommand, error) {\n\tinput = strings.TrimSpace(input)\n\tif input == \"\" {\n\t\treturn larkCommand{}, errDeepSeekIntent\n\t}\n\tinputRunes := []rune(input)\n\tif len(inputRunes) > larkAIInputMaxRunes {\n\t\tinput = string(inputRunes[:larkAIInputMaxRunes])\n\t}\n\n\tpayload := struct {\n\t\tModel string `json:\"model\"`\n\t\tMessages []struct {\n\t\t\tRole string `json:\"role\"`\n\t\t\tContent string `json:\"content\"`\n\t\t} `json:\"messages\"`\n\t\tThinking struct {\n\t\t\tType string `json:\"type\"`\n\t\t} `json:\"thinking\"`\n\t\tResponseFormat struct {\n\t\t\tType string `json:\"type\"`\n\t\t} `json:\"response_format\"`\n\t\tMaxTokens int `json:\"max_tokens\"`\n\t}{\n\t\tModel: p.model,\n\t\tMaxTokens: 220,\n\t}\n\tpayload.Messages = append(payload.Messages,\n\t\tstruct {\n\t\t\tRole string `json:\"role\"`\n\t\t\tContent string `json:\"content\"`\n\t\t}{Role: \"system\", Content: deepSeekIntentPrompt},\n\t\tstruct {\n\t\t\tRole string `json:\"role\"`\n\t\t\tContent string `json:\"content\"`\n\t\t}{Role: \"user\", Content: input},\n\t)\n\tpayload.Thinking.Type = \"disabled\"\n\tpayload.ResponseFormat.Type = \"json_object\"\n\n\tbody, err := json.Marshal(payload)\n\tif err != nil {\n\t\treturn larkCommand{}, errDeepSeekIntent\n\t}\n\trequest, err := http.NewRequestWithContext(ctx, http.MethodPost, p.endpoint, bytes.NewReader(body))\n\tif err != nil {\n\t\treturn larkCommand{}, errDeepSeekIntent\n\t}\n\trequest.Header.Set(\"Authorization\", \"Bearer \"+p.apiKey)\n\trequest.Header.Set(\"Content-Type\", \"application/json\")\n\n\tresponse, err := p.client.Do(request)\n\tif err != nil {\n\t\treturn larkCommand{}, errDeepSeekIntent\n\t}\n\tdefer response.Body.Close()\n\tif response.StatusCode < http.StatusOK || response.StatusCode >= http.StatusMultipleChoices {\n\t\treturn larkCommand{}, errDeepSeekIntent\n\t}\n\n\tvar completion struct {\n\t\tChoices []struct {\n\t\t\tFinishReason string `json:\"finish_reason\"`\n\t\t\tMessage struct {\n\t\t\t\tContent string `json:\"content\"`\n\t\t\t} `json:\"message\"`\n\t\t} `json:\"choices\"`\n\t}\n\tdecoder := json.NewDecoder(io.LimitReader(response.Body, deepSeekResponseMaxBytes))\n\tif err := decoder.Decode(&completion); err != nil || len(completion.Choices) == 0 {\n\t\treturn larkCommand{}, errDeepSeekIntent\n\t}\n\tchoice := completion.Choices[0]\n\tif choice.FinishReason != \"stop\" || strings.TrimSpace(choice.Message.Content) == \"\" {\n\t\treturn larkCommand{}, errDeepSeekIntent\n\t}\n\treturn deepSeekResultCommand(choice.Message.Content)\n}\n\nfunc deepSeekResultCommand(content string) (larkCommand, error) {\n\tvar result struct {\n\t\tIntent string `json:\"intent\"`\n\t\tKeyword string `json:\"keyword\"`\n\t\tMetric string `json:\"metric\"`\n\t\tGroupBy string `json:\"group_by\"`\n\t\tMovementType string `json:\"movement_type\"`\n\t\tTimeRange string `json:\"time_range\"`\n\t\tDays int `json:\"days\"`\n\t\tSort string `json:\"sort\"`\n\t\tLimit int `json:\"limit\"`\n\t\tProductKeyword string `json:\"product_keyword\"`\n\t\tShopKeyword string `json:\"shop_keyword\"`\n\t\tOperatorKeyword string `json:\"operator_keyword\"`\n\t}\n\tdecoder := json.NewDecoder(strings.NewReader(content))\n\tdecoder.DisallowUnknownFields()\n\tif err := decoder.Decode(&result); err != nil {\n\t\treturn larkCommand{}, errDeepSeekIntent\n\t}\n\tif err := decoder.Decode(&struct{}{}); err != io.EOF {\n\t\treturn larkCommand{}, errDeepSeekIntent\n\t}\n\n\tresult.Intent = strings.TrimSpace(result.Intent)\n\tresult.Keyword = strings.TrimSpace(result.Keyword)\n\tif len([]rune(result.Keyword)) > larkKeywordMaxRunes ||\n\t\tlen([]rune(result.ProductKeyword)) > larkKeywordMaxRunes ||\n\t\tlen([]rune(result.ShopKeyword)) > larkKeywordMaxRunes ||\n\t\tlen([]rune(result.OperatorKeyword)) > larkKeywordMaxRunes {\n\t\treturn larkCommand{}, errDeepSeekIntent\n\t}\n\thasAnalyticsFields := result.Metric != \"\" || result.GroupBy != \"\" || result.MovementType != \"\" ||\n\t\tresult.TimeRange != \"\" || result.Days != 0 || result.Sort != \"\" || result.Limit != 0 ||\n\t\tresult.ProductKeyword != \"\" || result.ShopKeyword != \"\" || result.OperatorKeyword != \"\"\n\tif result.Intent != \"analytics\" && hasAnalyticsFields {\n\t\treturn larkCommand{}, errDeepSeekIntent\n\t}\n\n\tswitch result.Intent {\n\tcase \"inventory\":\n\t\tif result.Keyword == \"\" {\n\t\t\treturn larkCommand{}, errDeepSeekIntent\n\t\t}\n\t\treturn larkCommand{Action: larkActionInventory, Name: \"自然语言·查库存\", Keyword: result.Keyword}, nil\n\tcase \"low_stock\":\n\t\treturn larkCommand{Action: larkActionLowStock, Name: \"自然语言·低库存\", Keyword: result.Keyword}, nil\n\tcase \"out_of_stock\":\n\t\treturn larkCommand{Action: larkActionOutOfStock, Name: \"自然语言·缺货清单\", Keyword: result.Keyword}, nil\n\tcase \"inventory_summary\":\n\t\tif result.Keyword != \"\" {\n\t\t\treturn larkCommand{}, errDeepSeekIntent\n\t\t}\n\t\treturn larkCommand{Action: larkActionSummary, Name: \"自然语言·库存概览\"}, nil\n\tcase \"movements\":\n\t\treturn larkCommand{Action: larkActionMovements, Name: \"自然语言·查流水\", Keyword: result.Keyword}, nil\n\tcase \"today_changes\":\n\t\tif result.Keyword != \"\" {\n\t\t\treturn larkCommand{}, errDeepSeekIntent\n\t\t}\n\t\treturn larkCommand{Action: larkActionTodayChanges, Name: \"自然语言·今日变动\"}, nil\n\tcase \"analytics\":\n\t\tif result.Keyword != \"\" {\n\t\t\treturn larkCommand{}, errDeepSeekIntent\n\t\t}\n\t\tplan, err := normalizeLarkAnalyticsPlan(larkAnalyticsPlan{\n\t\t\tMetric: result.Metric,\n\t\t\tGroupBy: result.GroupBy,\n\t\t\tMovementType: result.MovementType,\n\t\t\tTimeRange: result.TimeRange,\n\t\t\tDays: result.Days,\n\t\t\tSort: result.Sort,\n\t\t\tLimit: result.Limit,\n\t\t\tProductKeyword: result.ProductKeyword,\n\t\t\tShopKeyword: result.ShopKeyword,\n\t\t\tOperatorKeyword: result.OperatorKeyword,\n\t\t})\n\t\tif err != nil {\n\t\t\treturn larkCommand{}, errDeepSeekIntent\n\t\t}\n\t\treturn larkCommand{Action: larkActionAnalytics, Name: \"自然语言·统计查询\", Analytics: &plan}, nil\n\tcase \"help\":\n\t\tif result.Keyword != \"\" {\n\t\t\treturn larkCommand{}, errDeepSeekIntent\n\t\t}\n\t\treturn larkCommand{Action: larkActionHelp, Name: \"自然语言·帮助\"}, nil\n\tcase \"unknown\":\n\t\tif result.Keyword != \"\" {\n\t\t\treturn larkCommand{}, errDeepSeekIntent\n\t\t}\n\t\treturn larkCommand{Action: larkActionUnknown, Name: \"自然语言·未识别\"}, nil\n\tdefault:\n\t\treturn larkCommand{}, fmt.Errorf(\"%w: unsupported intent\", errDeepSeekIntent)\n\t}\n}\n"}}, "apps/api/internal/config/config.go": {"2f01833189c2c98197bf7c186039f14ec08b1225": {"t": 1786068291.4629364, "n": 4397, "h": "5955dd923c800778", "c": "package config\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"os\"\n\t\"strconv\"\n\t\"strings\"\n)\n\nconst minAuthSecretBytes = 32\n\nvar (\n\tErrMissingDatabaseURL = errors.New(\"DATABASE_URL is required\")\n\tErrAuthSecretTooShort = errors.New(\"AUTH_SECRET must be at least 32 bytes\")\n\tErrIncompleteLarkConfig = errors.New(\"LARK_APP_ID, LARK_APP_SECRET, and LARK_CHAT_ID must be configured together\")\n)\n\ntype Config struct {\n\tAPIAddr string\n\tDatabaseURL string\n\tAuthSecret string\n\tUploadDir string\n\tBackupDir string\n\tBackupRetentionDays int\n\tBackupAttachmentLimitMB int\n\tSMTPHost string\n\tSMTPPort int\n\tSMTPUsername string\n\tSMTPPassword string\n\tSMTPFrom string\n\tSMTPTo string\n\tSMTPTLS string\n\tInitialAdminName string\n\tInitialAdminEmail string\n\tInitialAdminPassword string\n\tSessionCookieSecure bool\n\tLarkAppID string\n\tLarkAppSecret string\n\tLarkChatID string\n\tDeepSeekAPIKey string\n\tDeepSeekModel string\n}\n\nfunc Load() (Config, error) {\n\tdatabaseURL := strings.TrimSpace(os.Getenv(\"DATABASE_URL\"))\n\tif databaseURL == \"\" {\n\t\treturn Config{}, ErrMissingDatabaseURL\n\t}\n\n\tbackupRetentionDays, err := envInt(\"BACKUP_RETENTION_DAYS\", 7)\n\tif err != nil {\n\t\treturn Config{}, err\n\t}\n\n\tbackupAttachmentLimitMB, err := envInt(\"BACKUP_ATTACHMENT_LIMIT_MB\", 20)\n\tif err != nil {\n\t\treturn Config{}, err\n\t}\n\n\tsmtpPort, err := envInt(\"SMTP_PORT\", 587)\n\tif err != nil {\n\t\treturn Config{}, err\n\t}\n\n\tsessionCookieSecure, err := envBool(\"SESSION_COOKIE_SECURE\", false)\n\tif err != nil {\n\t\treturn Config{}, err\n\t}\n\n\tlarkAppID := envString(\"LARK_APP_ID\", \"\")\n\tlarkAppSecret := envString(\"LARK_APP_SECRET\", \"\")\n\tlarkChatID := envString(\"LARK_CHAT_ID\", \"\")\n\tlarkValues := 0\n\tfor _, value := range []string{larkAppID, larkAppSecret, larkChatID} {\n\t\tif value != \"\" {\n\t\t\tlarkValues++\n\t\t}\n\t}\n\tif larkValues != 0 && larkValues != 3 {\n\t\treturn Config{}, ErrIncompleteLarkConfig\n\t}\n\n\tcfg := Config{\n\t\tAPIAddr: envString(\"API_ADDR\", \":8080\"),\n\t\tDatabaseURL: databaseURL,\n\t\tAuthSecret: envString(\"AUTH_SECRET\", \"\"),\n\t\tUploadDir: envString(\"UPLOAD_DIR\", \"/app/uploads\"),\n\t\tBackupDir: envString(\"BACKUP_DIR\", \"/app/backups\"),\n\t\tBackupRetentionDays: backupRetentionDays,\n\t\tBackupAttachmentLimitMB: backupAttachmentLimitMB,\n\t\tSMTPHost: envString(\"SMTP_HOST\", \"smtp.example.com\"),\n\t\tSMTPPort: smtpPort,\n\t\tSMTPUsername: envString(\"SMTP_USERNAME\", \"backup@example.com\"),\n\t\tSMTPPassword: envString(\"SMTP_PASSWORD\", \"example-smtp-password\"),\n\t\tSMTPFrom: envString(\"SMTP_FROM\", \"backup@example.com\"),\n\t\tSMTPTo: envString(\"SMTP_TO\", \"owner@example.com\"),\n\t\tSMTPTLS: envString(\"SMTP_TLS\", \"starttls\"),\n\t\tInitialAdminName: envString(\"INITIAL_ADMIN_NAME\", \"\"),\n\t\tInitialAdminEmail: envString(\"INITIAL_ADMIN_EMAIL\", \"\"),\n\t\tInitialAdminPassword: envString(\"INITIAL_ADMIN_PASSWORD\", \"\"),\n\t\tSessionCookieSecure: sessionCookieSecure,\n\t\tLarkAppID: larkAppID,\n\t\tLarkAppSecret: larkAppSecret,\n\t\tLarkChatID: larkChatID,\n\t\tDeepSeekAPIKey: envString(\"DEEPSEEK_API_KEY\", \"\"),\n\t\tDeepSeekModel: envString(\"DEEPSEEK_MODEL\", \"deepseek-v4-flash\"),\n\t}\n\n\tif len([]byte(cfg.AuthSecret)) < minAuthSecretBytes {\n\t\treturn Config{}, ErrAuthSecretTooShort\n\t}\n\n\treturn cfg, nil\n}\n\nfunc (c Config) LarkEnabled() bool {\n\treturn c.LarkAppID != \"\" && c.LarkAppSecret != \"\" && c.LarkChatID != \"\"\n}\n\nfunc envString(key string, fallback string) string {\n\tvalue := strings.TrimSpace(os.Getenv(key))\n\tif value == \"\" {\n\t\treturn fallback\n\t}\n\treturn value\n}\n\nfunc envInt(key string, fallback int) (int, error) {\n\traw := strings.TrimSpace(os.Getenv(key))\n\tif raw == \"\" {\n\t\treturn fallback, nil\n\t}\n\n\tvalue, err := strconv.Atoi(raw)\n\tif err != nil {\n\t\treturn 0, fmt.Errorf(\"%s must be an integer: %w\", key, err)\n\t}\n\treturn value, nil\n}\n\nfunc envBool(key string, fallback bool) (bool, error) {\n\traw := strings.TrimSpace(os.Getenv(key))\n\tif raw == \"\" {\n\t\treturn fallback, nil\n\t}\n\tvalue, err := strconv.ParseBool(raw)\n\tif err != nil {\n\t\treturn false, fmt.Errorf(\"%s must be a boolean: %w\", key, err)\n\t}\n\treturn value, nil\n}\n"}}}} |