update:脚本初始化

This commit is contained in:
lily 2026-09-20 03:47:41 +08:00
parent 226224162f
commit 73b1a0fddc
9 changed files with 856 additions and 0 deletions

1
.gitignore vendored
View File

@ -3,6 +3,7 @@ config/config.dev.yaml
# generated DML dumps # generated DML dumps
haoxian_hisense/agentic_project/check_agentic_project_name/dml_*.sql haoxian_hisense/agentic_project/check_agentic_project_name/dml_*.sql
haoxian_hisense/agentic_project/check_agentic_project_metric/dml_*.sql
# Go # Go
*.exe *.exe

View File

@ -0,0 +1,59 @@
# check_agentic_project_metric
检查生产环境**项目表露出率**与 **L1 snapshot 露出率**是否一致。
## 数据源
1. 项目表
`GET https://geo-api.hisense.com/api/agentic-project/project?page=1&page_size=999&listed=all`
Header: `x-token`
2. 项目指标(L1 snapshot)
`GET https://geo-api.hisense.com/api/geo_label/l1/snapshot?project_id={id}&platform=ALL&batch_date_from={date}&batch_date_to={date}&page=1&page_size=20`
Header: `x-token`
## 项目过滤
与月度表对齐,须同时满足:
- `listed = 1`
- `status = 'running'`
- `end_month = '2026-09'`
- `target_metric_name = '露出率'`
## 比对项
| 项目表 | snapshot |
|--------|----------|
| `current_metric_value`(指标名=露出率) | `exposure_rate` |
不一致时输出批量回写 SQL(以 snapshot 为准):
```sql
UPDATE `agentic`.agentic_project
SET current_metric_value = CASE id
WHEN ... THEN ...
END
WHERE company_id = 13 AND id IN (...);
```
默认 `batch_date = 2026-09-12`,可用环境变量覆盖:
```bash
BATCH_DATE=2026-09-12 go run ./haoxian_hisense/agentic_project/check_agentic_project_metric
```
## 运行
```bash
go run ./haoxian_hisense/agentic_project/check_agentic_project_metric
```
## 配置
`config/config.dev.yaml`(已 gitignore):
```yaml
hisense_geo:
base_url: "https://geo-api.hisense.com"
token: "" # x-token,勿提交仓库
```

View File

@ -0,0 +1,417 @@
package main
import (
"context"
"encoding/json"
"fmt"
"io"
"log"
"net/http"
"os"
"strconv"
"strings"
"sync"
"sync/atomic"
"time"
"ai_script/config"
)
// 与月度表对齐的项目过滤;批次日期可用环境变量 BATCH_DATE 覆盖。
const (
requiredListed = 1
requiredStatus = "running"
requiredEndMonth = "2026-09"
defaultBatchDate = "2026-09-12"
metricNameExposure = "露出率"
snapshotPlatform = "ALL"
maxConcurrency = 10
)
func main() {
cfg, err := config.Load()
if err != nil {
log.Fatalf("加载配置失败: %v", err)
}
batchDate := strings.TrimSpace(os.Getenv("BATCH_DATE"))
if batchDate == "" {
batchDate = defaultBatchDate
}
ctx := context.Background()
// ---------------------------------------------------------------------------
// 1、拉取生产环境项目表
// GET {base_url}/api/agentic-project/project?page=1&page_size=999&listed=all
// ---------------------------------------------------------------------------
projects, err := fetchProdProjects(ctx, cfg)
if err != nil {
log.Fatalf("拉取生产环境项目失败: %v", err)
}
log.Printf("[1] 生产环境项目数: %d", len(projects))
eligible := make([]prodProject, 0, len(projects))
for _, p := range projects {
if !eligibleProject(p) {
continue
}
if p.TargetMetricName != metricNameExposure {
continue
}
eligible = append(eligible, p)
}
log.Printf("[1] 待比对项目: listed=%d AND status=%s AND end_month=%s AND target_metric_name=%s → %d 条",
requiredListed, requiredStatus, requiredEndMonth, metricNameExposure, len(eligible))
// ---------------------------------------------------------------------------
// 2、按 project_id 拉取 l1/snapshot(platform=ALL,指定 batch_date)
// GET {base_url}/api/geo_label/l1/snapshot?project_id=...&platform=ALL&batch_date_from=...&batch_date_to=...
// ---------------------------------------------------------------------------
log.Printf("[2] 拉取 snapshot: platform=%s batch_date=%s concurrency=%d",
snapshotPlatform, batchDate, maxConcurrency)
snaps := fetchSnapshots(ctx, cfg, eligible, batchDate)
log.Printf("[2] snapshot 命中: %d / %d", len(snaps), len(eligible))
// ---------------------------------------------------------------------------
// 3、比对:项目表 current_metric_value ↔ snapshot.exposure_rate
// ---------------------------------------------------------------------------
compareAndLog(eligible, snaps, batchDate)
}
type prodProject struct {
ID string
CompanyID string
ProjectName string
Brand string
Category string
Status string
Listed int
EndMonth string
TargetMetricName string
CurrentMetricValue float64
TargetUnit string
}
func eligibleProject(p prodProject) bool {
return p.Listed == requiredListed &&
strings.EqualFold(p.Status, requiredStatus) &&
p.EndMonth == requiredEndMonth
}
type snapshotMetric struct {
ProjectID string
BatchDate string
ExposureRate float64
Platform string
}
func fetchProdProjects(ctx context.Context, cfg *config.Config) ([]prodProject, error) {
if cfg.HisenseGeo.Token == "" {
return nil, fmt.Errorf("配置缺少 hisense_geo.token")
}
base := strings.TrimRight(cfg.HisenseGeo.BaseURL, "/")
url := fmt.Sprintf("%s/api/agentic-project/project?page=1&page_size=999&listed=all", base)
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil {
return nil, err
}
setCommonHeaders(req, cfg.HisenseGeo.Token)
body, err := doJSON(req)
if err != nil {
return nil, err
}
var raw struct {
Code int `json:"code"`
Message string `json:"message"`
Data struct {
Total int `json:"total"`
List []struct {
ID json.Number `json:"id"`
CompanyID json.Number `json:"company_id"`
ProjectName string `json:"project_name"`
Brand string `json:"brand"`
Category string `json:"category"`
Status string `json:"status"`
Listed int `json:"listed"`
EndMonth string `json:"end_month"`
TargetMetricName string `json:"target_metric_name"`
CurrentMetricValue json.RawMessage `json:"current_metric_value"`
TargetUnit string `json:"target_unit"`
} `json:"list"`
} `json:"data"`
}
if err := json.Unmarshal(body, &raw); err != nil {
return nil, fmt.Errorf("解析 agentic-project 响应失败: %w", err)
}
if raw.Code != 0 {
return nil, fmt.Errorf("agentic-project 接口业务错误 code=%d message=%s", raw.Code, raw.Message)
}
out := make([]prodProject, 0, len(raw.Data.List))
for _, w := range raw.Data.List {
companyID := w.CompanyID.String()
if companyID == "" {
companyID = "13"
}
cur, err := parseFloatJSON(w.CurrentMetricValue)
if err != nil {
log.Printf("[1] 警告: id=%s current_metric_value 解析失败: %v raw=%s",
w.ID.String(), err, string(w.CurrentMetricValue))
}
out = append(out, prodProject{
ID: w.ID.String(),
CompanyID: companyID,
ProjectName: strings.TrimSpace(w.ProjectName),
Brand: strings.TrimSpace(w.Brand),
Category: strings.TrimSpace(w.Category),
Status: strings.TrimSpace(w.Status),
Listed: w.Listed,
EndMonth: strings.TrimSpace(w.EndMonth),
TargetMetricName: strings.TrimSpace(w.TargetMetricName),
CurrentMetricValue: cur,
TargetUnit: strings.TrimSpace(w.TargetUnit),
})
}
if raw.Data.Total > len(out) {
log.Printf("[1] 警告: 接口 total=%d,本页仅返回 %d 条,可能未拉全", raw.Data.Total, len(out))
}
return out, nil
}
func fetchSnapshots(ctx context.Context, cfg *config.Config, projects []prodProject, batchDate string) map[string]snapshotMetric {
out := make(map[string]snapshotMetric, len(projects))
var mu sync.Mutex
var wg sync.WaitGroup
sem := make(chan struct{}, maxConcurrency)
var done int64
total := int64(len(projects))
httpClient := &http.Client{Timeout: 60 * time.Second}
base := strings.TrimRight(cfg.HisenseGeo.BaseURL, "/")
for _, p := range projects {
p := p
wg.Add(1)
go func() {
defer wg.Done()
sem <- struct{}{}
defer func() { <-sem }()
snap, err := fetchOneSnapshot(ctx, httpClient, base, cfg.HisenseGeo.Token, p.ID, batchDate)
n := atomic.AddInt64(&done, 1)
if n%50 == 0 || n == total {
log.Printf("[2] snapshot 进度 %d / %d", n, total)
}
if err != nil {
log.Printf("[2] snapshot 失败 project_id=%s: %v", p.ID, err)
return
}
if snap == nil {
return
}
mu.Lock()
out[p.ID] = *snap
mu.Unlock()
}()
}
wg.Wait()
return out
}
func fetchOneSnapshot(ctx context.Context, client *http.Client, base, token, projectID, batchDate string) (*snapshotMetric, error) {
url := fmt.Sprintf(
"%s/api/geo_label/l1/snapshot?project_id=%s&platform=%s&batch_date_from=%s&batch_date_to=%s&page=1&page_size=20",
base, projectID, snapshotPlatform, batchDate, batchDate,
)
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil {
return nil, err
}
setCommonHeaders(req, token)
resp, err := client.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
}
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("HTTP %d: %s", resp.StatusCode, truncate(string(body), 200))
}
var raw struct {
Code int `json:"code"`
Message string `json:"message"`
Data struct {
Total int `json:"total"`
Items []struct {
ProjectID json.Number `json:"project_id"`
BatchDate string `json:"batch_date"`
Platform string `json:"platform"`
ExposureRate json.RawMessage `json:"exposure_rate"`
} `json:"items"`
} `json:"data"`
}
if err := json.Unmarshal(body, &raw); err != nil {
return nil, fmt.Errorf("解析 snapshot 失败: %w", err)
}
if raw.Code != 0 {
return nil, fmt.Errorf("业务错误 code=%d message=%s", raw.Code, raw.Message)
}
if len(raw.Data.Items) == 0 {
return nil, nil
}
item := raw.Data.Items[0]
rate, err := parseFloatJSON(item.ExposureRate)
if err != nil {
return nil, fmt.Errorf("exposure_rate 解析失败: %w raw=%s", err, string(item.ExposureRate))
}
return &snapshotMetric{
ProjectID: projectID,
BatchDate: strings.TrimSpace(item.BatchDate),
ExposureRate: rate,
Platform: strings.TrimSpace(item.Platform),
}, nil
}
func compareAndLog(projects []prodProject, snaps map[string]snapshotMetric, batchDate string) {
var (
okList []prodProject
mismatch []prodProject
missingSnap []prodProject
)
for _, p := range projects {
snap, ok := snaps[p.ID]
if !ok {
missingSnap = append(missingSnap, p)
continue
}
if sameMetric(p.CurrentMetricValue, snap.ExposureRate) {
okList = append(okList, p)
continue
}
mismatch = append(mismatch, p)
}
log.Printf("========== 比对概况 ==========")
log.Printf("比对: 项目表 current_metric_value(target_metric_name=露出率) ↔ snapshot.exposure_rate")
log.Printf("batch_date=%s platform=%s", batchDate, snapshotPlatform)
log.Printf("待比对=%d, 一致=%d, 不一致=%d, 无snapshot=%d",
len(projects), len(okList), len(mismatch), len(missingSnap))
log.Printf("========== 不一致(共 %d 条)==========", len(mismatch))
for _, p := range mismatch {
snap := snaps[p.ID]
log.Printf("[不一致] id=%s company_id=%s project_name=%q brand=%q category=%q",
p.ID, p.CompanyID, p.ProjectName, p.Brand, p.Category)
log.Printf(" 项目表 current_metric_value=%v%s", formatNum(p.CurrentMetricValue), p.TargetUnit)
log.Printf(" snapshot exposure_rate=%v (batch_date=%s platform=%s)",
formatNum(snap.ExposureRate), snap.BatchDate, snap.Platform)
}
log.Printf("---------- 以 snapshot.exposure_rate 回写 current_metric_value SQL(共 %d 条)----------", len(mismatch))
if len(mismatch) > 0 {
fmt.Println(buildBatchUpdateCurrentMetricSQL(mismatch, snaps))
}
log.Printf("========== 无 snapshot(共 %d 条)==========", len(missingSnap))
for _, p := range missingSnap {
log.Printf("[缺失] id=%s project_name=%q current_metric_value=%v%s",
p.ID, p.ProjectName, formatNum(p.CurrentMetricValue), p.TargetUnit)
}
log.Printf("比对完成: 一致=%d, 不一致=%d, 无snapshot=%d",
len(okList), len(mismatch), len(missingSnap))
}
// buildBatchUpdateCurrentMetricSQL 以 snapshot 露出率为准,批量回写项目表 current_metric_value。
func buildBatchUpdateCurrentMetricSQL(mismatch []prodProject, snaps map[string]snapshotMetric) string {
var b strings.Builder
b.WriteString("-- 批量回写:snapshot.exposure_rate → current_metric_value(company_id=13)\n")
b.WriteString("UPDATE `agentic`.agentic_project\n")
b.WriteString("SET current_metric_value = CASE id\n")
ids := make([]string, 0, len(mismatch))
for _, p := range mismatch {
snap := snaps[p.ID]
b.WriteString(fmt.Sprintf(" WHEN %s THEN %s\n", p.ID, formatNum(snap.ExposureRate)))
ids = append(ids, p.ID)
}
b.WriteString("END\n")
b.WriteString("WHERE company_id = 13\n")
b.WriteString(fmt.Sprintf(" AND id IN (%s);", strings.Join(ids, ", ")))
return b.String()
}
func sameMetric(a, b float64) bool {
const eps = 1e-6
diff := a - b
if diff < 0 {
diff = -diff
}
return diff < eps
}
func formatNum(v float64) string {
return strconv.FormatFloat(v, 'f', -1, 64)
}
func parseFloatJSON(raw json.RawMessage) (float64, error) {
s := strings.TrimSpace(string(raw))
if s == "" || s == "null" {
return 0, nil
}
var f float64
if err := json.Unmarshal(raw, &f); err == nil {
return f, nil
}
var str string
if err := json.Unmarshal(raw, &str); err != nil {
return 0, fmt.Errorf("非数字: %s", s)
}
str = strings.TrimSpace(strings.TrimSuffix(str, "%"))
if str == "" {
return 0, nil
}
return strconv.ParseFloat(str, 64)
}
func setCommonHeaders(req *http.Request, token string) {
req.Header.Set("Accept", "application/json")
req.Header.Set("Content-Type", "application/json; charset=utf-8")
req.Header.Set("Origin", "https://geo.hisense.com")
req.Header.Set("Referer", "https://geo.hisense.com/")
req.Header.Set("x-token", token)
}
func doJSON(req *http.Request) ([]byte, error) {
client := &http.Client{Timeout: 60 * time.Second}
resp, err := client.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
}
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("HTTP %d: %s", resp.StatusCode, truncate(string(body), 300))
}
return body, nil
}
func truncate(s string, n int) string {
if len(s) <= n {
return s
}
return s[:n] + "..."
}

View File

@ -0,0 +1,132 @@
---
name: l1-geo-html-screen-diff
description: >-
Compare an L1 project GEO HTML report with the haoxian geo-monitor page
(level=project) by on-screen names, percentages, copy, and source-title
order, then write a numbered test report. Supports one URL pair or a JSON
batch of project_id, html_url, and url. Use when the user asks for an L1
or 项目级 diff of HTML vs 平台, geo-monitor, 露出率, 前三率, 中正面率, 分平台,
露出排行, AI 认知词云, 信源内容类型, 信源发布时间, 媒体类型, 媒体名称,
关联信源, 引用信源榜, 画面值, 批量, or a project-level numerical test report.
---
# L1 HTML 与平台画面对照
对照 L1 项目监测 HTML 与监测页。只比映射板块上写出来的名称、百分比和指定文案。除第 12 项外,不比位置、颜色、条形图还是方块。
计算、接口、排序细则见 [reference.md](reference.md)。示例只说明参数怎么传,见 [examples.md](examples.md)。示例 JSON 见 [batch.example.json](batch.example.json)。不要把示例里的地址、日期、目录和 JSON 当成默认值。
## 参数
两种入口。共享参数缺了就停下来问。不要沿用上次对话、示例或本文件里的地址、日期和目录。用户同时给了单条地址和 JSON 时,以 JSON 为准,按数组逐项做,不要只做消息里那一条。
### 单条
| 参数 | 用户怎么叫 | 用途 |
|---|---|---|
| `platform_url` | url地址 | 监测页完整 URL,含 `batchDate`、`level=project`、`project` |
| `html_url` | html地址 | 历史 HTML 完整 URL,有版本参数时原样使用 |
| `platform_period` | url日期 | 平台监测周期,如 `9月w38`。只核对页面日期文案 |
| `html_monitor_date` | html日期 | HTML 监测日期,如 `2026-09-15`。只核对页面日期文案 |
| `report_dir` | 测试报告目录 | 绝对路径。报告只写在这个目录下 |
### 批量
用户给 JSON 数组,或给 JSON 文件路径。没给地址、也没给 JSON,就停下来问。数组为空也停下来问。
每一项三个字段:
| 字段 | 用途 |
|---|---|
| `project_id` | 项目 id。必须等于该项 `url` 查询参数 `project` |
| `html_url` | 该项历史 HTML,当作 `html_url` |
| `url` | 该项监测页,当作 `platform_url` |
`platform_period`、`html_monitor_date`、`report_dir` 仍由用户本次给出,整批共用。不要从示例 JSON 补这三个。
`project_id` 和 `url` 里的 `project` 不一致时,停下来问,不要改 URL 去凑。按数组顺序逐项做完 12 项。一项失败不跳过其余项。每项单独编号、单独报告。
用户给定的日期对应关系不是差异。不要因为一边是日期、一边是周次就判失败。
`batchDate`、公司、批次号、`project` 从本次 `platform_url` 和当次登录会话解析。不要写死批次号、公司号、手机号或密码,也不要把凭据写进报告。
## 板块对应
用户原文,按这个对照,不要改映射:
- 露出率:HTML「平均露出率」= 平台「指定露出词露出率」
- 前三率:HTML「平均前三率」= 平台「前三率」
- 正面提及率:HTML「平均正面提及率」= 平台「中正面率」
- HTML「各平台核心指标趋势」露出率 = 平台「指定露出词露出率 · 分平台」
- HTML「各平台核心指标趋势」前三率 = 平台「前三率 · 分平台」
- HTML「各平台核心指标趋势」正面提及率 = 平台「中正面率 · 分平台」
- HTML「行业露出率排行榜」= 平台「露出排行 · 各平台明细」
- HTML「AI 回答关键词」= 平台「AI 认知词云」
- HTML「AI 回答正面关键词」= 平台「AI 认知词云」下的正面词
- HTML「AI 回答负面关键词」= 平台「AI 认知词云」下的负面词
- HTML「信源内容类型分布」= 平台「TMC 信源分析」下的信源内容类型
- HTML「信源发布时间分布」= 平台「TMC 信源分析」下的信源发布时间
- HTML「媒体类型」= 平台「媒体类型」
- HTML「媒体名称」= 平台「媒体名称」
- HTML「关联信源」= 平台「该问题的引用信源榜」
- HTML 标题 = 平台信源标题
- HTML 信源 = 平台渠道
- HTML 占比这一列的对照口径 = 平台被引次数。不要把占比百分数和次数当成同一个画面字段
HTML「综合」= 平台「全平台」。这是约定,不算第 2 项失败。
平台选项:综合、DeepSeek、豆包、通义、元宝、文心、Kimi。接口值:`ALL`、`deepseek`、`doubao`、`qwen`、`yuanbao`、`baidu_wenxin`、`kimi`。
## 测试范围
12 项都要做。每一项标题写成 `测试通过✅` 或 `测试失败❌`,不要只写相同或不同。
1. 确认两个 URL 能否打开,截图进报告。
2. 七个 AI 平台选项是否相同。HTML 里面综合 = 全平台,通义 = 通义千问,元宝 = 腾讯元宝,文心 = 百度文心
3. 各选项的露出率、前三率、正面率是否相同。
4. 各选项上,趋势露出率 / 前三率 / 正面提及率是否等于平台分平台三项。
5. HTML「行业露出率排行榜」= 平台「露出排行 · 各平台明细」。平台这一张表同时列出各选项,不要切换平台选项。只比各自 Top 10。三步都要做,任一步不同则该项测试失败❌。
1. 综合的平均露出率 = 平台全平台列的平均露出率。
2. HTML 综合排名品牌 Top 10 = 平台全平台排名品牌 Top 10。名次和品牌名都要相同,名称逐字相同。第 11 名及以后不比。
3. 这 10 个品牌在 HTML 的 DeepSeek、豆包、通义、元宝、文心、Kimi 上的平均露出率,等于平台同一张表里对应列。列对应为通义=通义千问,元宝=腾讯元宝,文心=百度文心。未露出时 HTML 为空、平台为「-」,不算不同。差超过 0.05 算不同。
6. 正面关键词、负面关键词。词云不随 AI 平台切换,七个选项同一套。下面三组文案是约定,写法不同不算失败:
1. HTML「AI 回答关键词」= 平台「AI 认知词云」
2. HTML「AI 回答正面关键词」= 平台「正面词」
3. HTML「未监测到负面词」= 平台「中正面率100%,暂无负面词」
正面词还要比图片素材:HTML 词云图画出来的词,和平台正面词图画出来的词,词条相同才算相同。不比词的大小、颜色和摆放位置。有负面词时比词条,不比空态文案。
7. 各选项的信源内容类型是否相同。
8. 各选项的信源发布时间是否相同。
9. 各选项的媒体类型:占比和展示名称,只取前 10。
10. 各选项的媒体名称:占比和展示名称,只取前 10。
11. 各选项的关联信源:标题、渠道、被引次数,只取前 30。不比序号。
12. 各选项的关联信源:标题内容,以及序号上展示的标题是否相同。只取前 30。
数据不同时给出平台和 HTML 的对比。每个平台都要输出:同名但百分比不同的名称、HTML 画面值、平台画面值。没有这种差异的平台写「画面上同名占比无差异」,不要省略该平台。露出率若同名百分比相同,写条数,不要把相同行展开成差异表。第 5 项只比 Top 10,名次或品牌名不同要展开,不能只写条数。第 11 名及以后不报。
差超过 0.05 个点才算不同。0.05 是百分点,不是相对误差。两边按 1 位小数比,最小能看出来的差别是 0.1。13.80% 和 13.8% 差是 0,判相同。
## 只在一边展示
不要写成两张互不对照的「仅 HTML / 仅平台」名单。改成对照表,写清原因。
- 同一条、只是名称写法不同、百分比相同:一行写 HTML 画面名、平台画面名、百分比。例如 HTML「车」、平台「车300」。第 5 项不适用这条例外,品牌名必须逐字相同。
- 一边因为 Top 10 或 Top 30 没印出来:写「前 10 之外,未印」或「前 30 之外,未印」,不要说成数值算错。第 5 项只比 Top 10,第 11 名及以后不报。
- HTML 不画「其他」,平台前 10 有「其他」:单独一张表写各平台的平台画面值。若没有把 HTML 的名称挤出前 10,写明没有「仅 HTML」的类型。
- 选项文案不同(通义 / 通义千问,元宝 / 腾讯元宝,文心 / 百度文心):放进第 2 项对照表。综合 = 全平台不记失败。其余写法不同则第 2 项为测试失败❌。
- 平台多出来、且没有出现在本次对照画面里的字段,不报。
媒体类型、媒体名称只在前 10 条里判缺失。第 11 条及以后不报。
## 报告文件
目录不存在就创建。
项目 id 取本次该项 `platform_url` 的 `project` 参数,记为 `project`。批量时每一项各自取,不要共用上一项的 id。
在 `report_dir` 里找已有的 `测试报告-N-{project}.md`,只计本次这个 `project`。下一个编号是这些文件里最大 N 加 1。该项目还没有则从 1 开始。`测试报告.md`、`测试报告-N.md` 和其他项目 id 的文件不算本次编号。批量里各项互不影响编号。
写成 `{report_dir}/测试报告-{id}-{project}.md`。截图放在 `{report_dir}/shots-{id}-{project}/`,报告里用相对路径 `shots-{id}-{project}/文件名`。不要覆盖已有报告,不要写到 `report_dir` 以外。
文件开头先给结论:一致,或不完全一致。然后是 12 项结论表,每项一个结果:`测试通过✅` 或 `测试失败❌`。
写完后在回复里给出报告的绝对路径。批量时按 JSON 顺序逐项给结论和绝对路径。对话里只保留结论和路径,同名差异表以该文件为准。文件里的日期、URL、保存目录用本次参数,不要留示例值。

View File

@ -0,0 +1,47 @@
[
{
"project_id": 1169,
"html_url": "https://cemeta-resource-1313090634.cos.ap-beijing.myqcloud.com/workflow/3899e673-cd92-4670-8203-566cee53a121.html",
"url": "https://haoxian.cemeta.cn/geo-monitor?batchDate=2026-09-17&reportType=monitor&level=project&project=1169&line=%E5%8D%A1%E6%B3%B0%E9%A9%B0::%E6%B1%BD%E8%BD%A6%E5%B9%B3%E5%8F%B0"
},
{
"project_id": 1170,
"html_url": "https://cemeta-resource-1313090634.cos.ap-beijing.myqcloud.com/workflow/b4007bdb-5138-4097-ba18-79b53fdc9c35.html",
"url": "https://haoxian.cemeta.cn/geo-monitor?batchDate=2026-09-17&reportType=monitor&level=project&project=1170&line=%E5%8D%A1%E6%B3%B0%E9%A9%B0::%E6%B1%BD%E8%BD%A6%E5%B9%B3%E5%8F%B0"
},
{
"project_id": 1171,
"html_url": "https://cemeta-resource-1313090634.cos.ap-beijing.myqcloud.com/workflow/90f82b55-5c3d-480e-991f-c96d6dfc96dc.html",
"url": "https://haoxian.cemeta.cn/geo-monitor?batchDate=2026-09-17&reportType=monitor&level=project&project=1171&line=%E5%8D%A1%E6%B3%B0%E9%A9%B0::%E6%B1%BD%E8%BD%A6%E5%B9%B3%E5%8F%B0"
},
{
"project_id": 1173,
"html_url": "https://cemeta-resource-1313090634.cos.ap-beijing.myqcloud.com/workflow/8507f96c-d167-45f9-bd78-0958bc99fba0.html",
"url": "https://haoxian.cemeta.cn/geo-monitor?batchDate=2026-09-17&reportType=monitor&level=project&project=1173&line=%E5%8D%A1%E6%B3%B0%E9%A9%B0::%E6%B1%BD%E8%BD%A6%E5%B9%B3%E5%8F%B0"
},
{
"project_id": 1174,
"html_url": "https://cemeta-resource-1313090634.cos.ap-beijing.myqcloud.com/workflow/03a274fd-74e2-4fc8-bcc5-88b5286016f8.html",
"url": "https://haoxian.cemeta.cn/geo-monitor?batchDate=2026-09-17&reportType=monitor&level=project&project=1174&line=%E5%8D%A1%E6%B3%B0%E9%A9%B0::%E6%B1%BD%E8%BD%A6%E5%B9%B3%E5%8F%B0"
},
{
"project_id": 1177,
"html_url": "https://cemeta-resource-1313090634.cos.ap-beijing.myqcloud.com/workflow/a5fc2516-2de6-41ab-85a1-67a94bde2181.html",
"url": "https://haoxian.cemeta.cn/geo-monitor?batchDate=2026-09-17&reportType=monitor&level=project&project=1177&line=%E5%8D%A1%E6%B3%B0%E9%A9%B0::%E6%B1%BD%E8%BD%A6%E5%B9%B3%E5%8F%B0"
},
{
"project_id": 1179,
"html_url": "https://cemeta-resource-1313090634.cos.ap-beijing.myqcloud.com/workflow/74a34826-4088-4b8e-8b7d-4fdf12007e8b.html",
"url": "https://haoxian.cemeta.cn/geo-monitor?batchDate=2026-09-17&reportType=monitor&level=project&project=1179&line=%E5%8D%A1%E6%B3%B0%E9%A9%B0::%E6%B1%BD%E8%BD%A6%E5%B9%B3%E5%8F%B0"
},
{
"project_id": 1187,
"html_url": "https://cemeta-resource-1313090634.cos.ap-beijing.myqcloud.com/workflow/ef43ba7c-5d22-4df7-99b9-9697d5095f67.html",
"url": "https://haoxian.cemeta.cn/geo-monitor?batchDate=2026-09-17&reportType=monitor&level=project&project=1187&line=%E5%8D%A1%E6%B3%B0%E9%A9%B0::%E6%B1%BD%E8%BD%A6%E5%B9%B3%E5%8F%B0"
},
{
"project_id": 1199,
"html_url": "https://cemeta-resource-1313090634.cos.ap-beijing.myqcloud.com/workflow/bc21a4ab-c882-4868-adfb-829c649bf6a0.html",
"url": "https://haoxian.cemeta.cn/geo-monitor?batchDate=2026-09-17&reportType=monitor&level=project&project=1199&line=%E5%8D%A1%E6%B3%B0%E9%A9%B0::%E6%B1%BD%E8%BD%A6%E5%B9%B3%E5%8F%B0"
}
]

View File

@ -0,0 +1,28 @@
# 调用示例
示例只说明参数怎么传。执行时用用户本次给的值,不要把下面的地址、日期、目录和 JSON 当成默认值。
## 单条
```text
platform_url: https://haoxian.cemeta.cn/geo-monitor?batchDate=2026-09-17&reportType=monitor&level=project&project=1174&line=%E5%8D%A1%E6%B3%B0%E9%A9%B0::%E6%B1%BD%E8%BD%A6%E5%B9%B3%E5%8F%B0&tab=exposure&platform=doubao
html_url: https://cemeta-resource-1313090634.cos.ap-beijing.myqcloud.com/workflow/03a274fd-74e2-4fc8-bcc5-88b5286016f8.html
platform_period: 9月w38
html_monitor_date: 2026-09-15
report_dir: /Users/lily/Desktop/haoxian_L0L1L2L3/好现灌数/卡泰驰/0917/01-L1-项目级/monitor-0917-test
```
`platform_period` 对应用户说的 url日期。`html_monitor_date` 对应用户说的 html日期。`project` 从 `platform_url` 的 `project` 参数取。报告写成该目录下的 `测试报告-1-1174.md`;同一项目已有编号时,编号加 1。
## 批量
地址对在 [batch.example.json](batch.example.json)。每一项的 `url` 当作 `platform_url`,`html_url` 原样使用。`project_id` 必须等于该项 `url` 里的 `project`。日期和报告目录整批共用,不写进 JSON。
```text
batch_json: /Users/lily/Desktop/haoxian_L0L1L2L3/好现灌数/.cursor/skills/l1-geo-html-screen-diff/batch.example.json
platform_period: 9月w38
html_monitor_date: 2026-09-15
report_dir: /Users/lily/Desktop/haoxian_L0L1L2L3/好现灌数/卡泰驰/0917/01-L1-项目级/monitor-0917-test
```
按数组顺序逐项做完 12 项。每项单独编号,例如 `测试报告-1-1169.md`、`测试报告-1-1170.md`。同一项目已有 `测试报告-N-{project}.md` 时,只给该项编号加 1。消息里如果还写了一条 url地址和 html地址,仍以 JSON 为准,不要只做那一条。

View File

@ -0,0 +1,59 @@
# L1 画面值与接口
本文件只在执行 `l1-geo-html-screen-diff` 时读。不要把这里的企业、批次、项目号写成死值。
## 数据从哪读
HTML 从 `var DATA` 读。L1 报告用 `brand`(`mode=simple` 时没有品牌/产品切换)。综合用 `brand.summary`、`brand.tmc_aggregate`、`brand.source_top30`、`brand.core_competitors`、`brand.wordcloud`。单个 AI 平台用 `brand.platforms[key]`。
平台是 L1,不是 L3。`project` 从 `platform_url` 取。
1. 用当次登录会话打 `resource-api.cemeta.cn`。
2. `GET /api/geo_label/project/batch?companyId={companyId}&reportType=monitor`,用 URL 里的 `batchDate` 找到对应项,再按 `project` 取 `l1CrawlBatchIds[].batchId`。
3. `GET /api/geo-monitor/metrics/l1?table={table}&batchIds={batchId}&projectId={project}&reportType=monitor`。`table` 用 `snapshot`、`competitor`、`tmc`。信源再加 `&platform={平台值}`,`table=source`。
4. 词云:`GET /api/geo-monitor/projects/{project}/wordcloud?batchId={batchId}&sentiment=all`。这个接口不按 AI 平台拆。HTML 词云也是写死综合,七个选项同一套词。
登录账号从项目认证注册表取,跟本次企业走。不要把手机号、密码、token 写进报告。
## 画面百分比
保留 1 位小数。差超过 0.05 个点才算不同。
- 露出率、前三率、正面率、排行榜露出率:页面直接印的字段。HTML `fmt` 对非 0、非 100 印两位小数,平台印一位。比数值,不比小数位数。
- 分平台:HTML 趋势序列最后一个监测日的点,对平台该平台 snapshot 的 `exposure_rate`、`top3_rate`、`positive_rate`。综合要覆盖六个平台的这三个点。
- 信源内容类型、信源发布时间:HTML 环图图例印 `value / 正值之和 × 100`,并去掉名为「无时间」的项。平台环图同样用 `value / 全部正值之和 × 100`,图例最多 8 条,按值从高到低。
- 媒体类型:HTML 方块去掉「其他」「未知」,再按剩余 `value` 之和重算。平台条形图按值从高到低取前 10;这 10 条 `value` 之和 ≤ 101.5 时直接印 `value`,否则 `value / 该和 × 100`。
- 媒体名称:HTML 没有占比字段,为 `value / 本组 value 之和 × 100`,去掉「其他」「未知」后取前 10。平台与媒体类型同一条形图规则。
## 排行榜
第 5 项:HTML「行业露出率排行榜」对平台「露出排行 · 各平台明细」。不要切换 AI 平台选项。只比 Top 10,第 11 名及以后不报。
平台全平台列是平均露出率。其余列是 DeepSeek、豆包、通义千问、腾讯元宝、百度文心、Kimi。某品牌在某选项没有露出时,HTML 为空,平台格子是「-」,这不算不同。
HTML 综合榜用 `core_competitors`,本品用 `summary.avg_exposure_rate`。分平台用 `brand.platforms[key].industry_ranking`。画面名称截断后对不上(「车」对「车300」)算名称不同。
三步都要做,任一步不同则第 5 项测试失败❌:
1. 综合平均露出率对平台全平台列。差超过 0.05 算不同。
2. 综合 Top 10 的名次和品牌名,对平台全平台 Top 10。名称逐字相同。
3. 这 10 个品牌在 DeepSeek、豆包、通义、元宝、文心、Kimi 上的露出率,对平台对应列。通义对通义千问,元宝对腾讯元宝,文心对百度文心。
## 词云
第 6 项不随 AI 平台切换。HTML 词在 `brand.wordcloud`,平台用词云接口,两边都是综合一套。
文案约定,不算失败:HTML「AI 回答关键词」= 平台「AI 认知词云」;HTML「AI 回答正面关键词」= 平台「正面词」;HTML「未监测到负面词」= 平台「中正面率100%,暂无负面词」。
正面词图片:HTML 用词云画布,平台用正面词图。比画出来的词条是否相同,不比大小、颜色和位置。负面有词时比词条;两边都空时用上面的空态文案。
## 关联信源
第 11 项按标题对齐,比渠道和被引次数,不比行号。集合相同就是测试通过✅。通义不足 30 条时,有几条比几条,两边条数不同才失败。
第 12 项比画面序号上的标题。
- HTML 顺序:`source_top30` 数组顺序,取前 30。
- 平台顺序:接口返回的行按 `cite_count` 从高到低做稳定排序,次数相同则保持接口原顺序。不要用 `rank` 字段顺序,那个顺序和平台表格不一致。
同一序号标题不同、但被引次数相同:记测试失败❌,表里写序号、HTML 标题、平台标题、被引次数,并写明是并列换位,不是缺标题。

View File

@ -0,0 +1,100 @@
---
name: l3-geo-html-screen-diff
description: >-
Compare an L3 enterprise GEO HTML report with the haoxian geo-monitor page
by on-screen names and percentages, then write a numbered test report. Use
when the user asks for an L3 or 企业级 diff of HTML vs 平台, geo-monitor,
露出率, 行业露出率排行榜, 信源内容类型, 信源发布时间, 媒体类型, 媒体名称,
画面值, or an enterprise two-URL numerical test report. Do not use for L1
项目级 pages.
---
# L3 HTML 与平台画面数值对照
这是 L3 企业级技能。对照历史 HTML 与监测页。只比映射板块上写出来的名称和百分比。不比位置、颜色、条形图还是方块。
## 必填参数
缺任一项就停下来问,不要沿用上次对话、示例或本文件里的地址和日期。
| 参数 | 用户传入 | 用途 |
|---|---|---|
| `platform_url` | 监测页完整 URL | 含 `batchDate`、`reportType`。企业级,不是 `level=project` |
| `html_url` | 历史 HTML 完整 URL | 含版本参数时原样使用 |
| `html_monitor_date` | HTML 监测日期,如 `2026-09-15` | 只用于核对页面日期文案 |
| `platform_period` | 平台监测周期,如 `9月W38` | 只用于核对页面日期文案 |
| `report_dir` | 测试报告保存目录的绝对路径 | 报告写在这个目录下,不写到别的位置 |
用户给定的日期对应关系不是差异。不要因为两边文案一个是日期、一个是周次就判失败。
`batchDate`、公司、批次号从本次 `platform_url` 和当次登录会话解析。不要写死批次号、公司号、手机号或密码,也不要把凭据写进报告。
## 板块对应
- HTML「综合」= 平台「全平台」
- HTML「品牌综合,竞争格局」= 平台「露出情况概括」
- HTML「AI 引用信源分析」= 平台「引用信源分析」
- HTML「信源内容类型分布」= 平台「信源内容类型」
- HTML「信源发布时间分布」= 平台「信源发布时间」
- HTML「媒体类型」= 平台「媒体类型」
- HTML「媒体名称」= 平台「媒体名称」
平台选项:综合、DeepSeek、豆包、通义、元宝、文心、Kimi。接口平台值:`ALL`、`deepseek`、`doubao`、`qwen`、`yuanbao`、`baidu_wenxin`、`kimi`。
## 测试范围
8 项都要做。每一项标题写成 `测试通过✅` 或 `测试失败❌`,不要只写相同或不同。
1. 确认两个 URL 页面能否正常打开展示,截图到测试报告
2. 覆盖综合、DeepSeek、豆包、通义、元宝、文心、Kimi。HTML 里面综合 = 全平台。
3. 各平台的露出率、前三率、正面率是否相同。
4. 各平台的行业露出率排行榜是否相同。
5. 各平台的信源内容类型是否相同。
6. 各平台的信源发布时间是否相同。
7. 各平台的媒体类型:两边都按画面占比从高到低只取前 10 条,再比这 10 条的占比是否相同、展示名称是否相同。第 11 条及以后不比。
8. 各平台的媒体名称:两边都按画面占比从高到低只取前 10 条,再比这 10 条的占比是否相同、展示名称是否相同。第 11 条及以后不比。
数据不同时给出平台和 HTML 的对比。每个平台都要输出:同名但百分比不同的名称、HTML 画面值、平台画面值。
## 画面值
比的是图例、方块、条形图上印出的百分比,不是加总前的字段。字段相同而画面不同,记为不同。
平台多出来、且没有出现在本次对照画面里的字段,不报。媒体类型和媒体名称只在前 10 条里判缺失;第 11 条及以后不报,也不写进只在一边展示。
HTML 从 `var DATA` 读取。平台从 `resource-api.cemeta.cn` 的 `/api/geo-monitor/metrics/l3` 读取,`table` 取 `snapshot`、`competitor`、`tmc`,`batchIds` 用本次解析出的批次。
画面百分比按下面计算,保留 1 位小数。差超过 0.05 才算不同。0.05 是百分点,不是相对误差。1 位小数上能看出来的最小差别是 0.1。13.80% 和 13.8% 差是 0,判相同。
- 露出率、前三率、正面率、排行榜露出率:页面直接印字段,两边直接比。
- 信源内容类型、信源发布时间:HTML 图例印存储的 `pct`。平台环图印 `value / 全部正值之和 × 100`,图例最多 8 条。
- 媒体类型、媒体名称:先各自算出画面占比,再按占比从高到低只留前 10 条,只比这 10 条的名称和百分比。
- 媒体类型画面占比:HTML 为 `pct / 全部 pct 之和 × 100`;平台为条形图印出的 `value`(这 10 条 `value` 之和 ≤ 101.5 时直接用 `value`,否则 `value / 该和 × 100`)。
- 媒体名称画面占比:HTML 没有占比字段,为 `次数 / 本组次数之和 × 100`;平台与媒体类型同一条形图规则。
## 只在一边展示
不要写成两张互不对照的「仅 HTML / 仅平台」名单。改成对照表,写清原因。
- 同一条、只是名称写法不同、百分比相同:一行写 HTML 画面名、平台画面名、百分比。
- 名称进了前 10、把另一边的名称挤掉:表里两边都写。被挤出的那一侧写名称和画面值,另一侧写「前 10 之外,未印」。
- HTML 不展示、平台前 10 有的名称:HTML 列写「不展示」,平台列写名称和画面值。并写明有没有把 HTML 的名称挤出前 10。
- 平台多出来、且没有出现在本次对照画面里的字段,不报。
## 报告
先给结论:一致,或不完全一致。然后按 8 项写结论表,每项一个结果:`测试通过✅` 或 `测试失败❌`。
每个有差异的平台单独列出:
| 平台 | 名称 | HTML 画面值 | 平台画面值 |
|---|---|---|---|
| 用户本次的平台名 | 同名条目 | 画面百分比 | 画面百分比 |
没有同名百分比差异的平台写「画面上同名占比无差异」,不要省略该平台。露出率、排行榜若相同,写条数,不要把相同行展开成差异表。
在 `report_dir` 里找已有的 `测试报告.md` 和 `测试报告-N.md`。下一个编号是已有最大 N 加 1。都没有则从 1 开始。`测试报告.md` 不算已有编号。
写成 `{report_dir}/测试报告-{id}.md`。目录不存在就创建。截图放在 `{report_dir}/shots-{id}/`,报告里用相对路径 `shots-{id}/文件名`。不要覆盖已有报告,不要写到 `report_dir` 以外,也不要改用上次的目录。
写完后在回复里给出报告的绝对路径。对话里只保留结论和路径,同名差异表以该文件为准。文件里的日期、URL、保存目录用本次参数,不要留示例值。

View File

@ -0,0 +1,13 @@
# 调用示例
示例只说明参数怎么传。执行时用用户本次给的值,不要把下面的地址和日期当成默认值。
```text
platform_url: https://haoxian.cemeta.cn/geo-monitor?batchDate=2026-09-17&reportType=monitor&tab=citation
html_url: https://cemeta-resource-1313090634.cos.ap-beijing.myqcloud.com/workflow/28757ffe-f284-48bb-8af6-c7739de56fa8.html?hxv=4&hxts=2026-09-16T00%3A00%3A00
html_monitor_date: 2026-09-15
platform_period: 9月W38
report_dir: /Users/lily/Desktop/haoxian_L0L1L2L3/好现灌数/卡泰驰/0917/03-L3-企业级/monitor-0917-test
```
这次示例里,HTML 监测日期 `2026-09-15` 对应平台监测周期 `9月W38`。报告写成该目录下的 `测试报告-1.md`。目录里已有 `测试报告-N.md` 时,编号加 1。`测试报告.md` 不算已有编号。换一批数据时,五个参数一起换。