Files
OpenCode 68a9071cf1 feat: reconcile local master with origin (exa/hot-platform hunters, leaks ledger, latest results)
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.
2026-08-07 12:53:47 +08:00

1 line
15 KiB
JSON
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{"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"}}}}