Back to skills
SKILL.md
Databrain Ai Gallery Upload
ASecurity在 AI-Gallery 创建或替换作品(单 HTML / 代码包均可)。自动从 HTML 推断 name/desc 中英文,列出系统 tag 让用户选择,支持通过旧 URL 一键替换源文件并保留访问地址。 当用户提到 上传作品 / 上传到DataBrain / 上传到 AI-Gallery / 替换 Gallery 文件 时使用。
- 279 stars
- 0 votes
- 0 copies
- 2 views
- Added September 8, 2026
Works with
Security analysis
92/100- Installs packages at runtime which could introduce malicious dependencies
Pro scans all 10 files and shows the line behind each finding
npx -y skills add infometa/workbuddyskills --skill databrain-ai-gallery-upload --agent claude-codeAre you the author of Databrain Ai Gallery Upload?
Add the live security badge to your README. It updates with every re-scan.
[](https://www.skillsdirectory.com/skills/infometa-databrain-ai-gallery-upload)---
name: databrain-ai-gallery-upload
version: 1.0.0
description: >-
在 AI-Gallery 创建或替换作品(单 HTML / 代码包均可)。自动从 HTML 推断 name/desc
中英文,列出系统 tag 让用户选择,支持通过旧 URL 一键替换源文件并保留访问地址。
当用户提到 上传作品 / 上传到DataBrain / 上传到 AI-Gallery / 替换 Gallery 文件 时使用。
metadata: {"openclaw": {"requires": {"env": ["DATABRAIN_TOKEN"]}}}
---
# AI-Gallery 上传 / 替换作品
在 AI-Gallery 创建新作品(CREATE)或替换现有作品的源文件(REPLACE)。所有 HTTP 调用
**只能**走本目录 `scripts/` 下的 Python 脚本,agent 不得自写 `curl` / `wget` /
`requests` 等任何方式直接访问后端。
## 环境变量
- `DATABRAIN_HOST`(可选):API 入口域名。默认 `https://databrain-global.intlgame.com`,显式设置以显式值为准。
- `DATABRAIN_TOKEN`(必填):Bearer token。
- `DATABRAIN_DISPLAY_HOST`(可选):用户访问作品的展示域名,默认 `https://databrain-global.intlgame.com`,仅用于拼接最终访问 URL(不发请求)。
> 所有接口(Gallery `/api/ai-gallery/*` + 埋点 `/api/v1/*`)共享同一 host,
> 脚本中无独立 host 旁路。
所有脚本统一从这几个环境变量取值,agent 不需要在命令行重复传 host / token;
缺 `DATABRAIN_TOKEN` 时脚本会以 exit 2 报错。
## Step -1:初始化 `${SKILL_DIR}`(执行任何脚本前必须先做)
本 skill 设计为**可独立安装、可被复制到任意位置**。所有脚本调用都用
`${SKILL_DIR}/scripts/<name>.py` 表达,agent 第一次进入本 skill 时必须先把
`SKILL_DIR` 解析为本 SKILL.md 文件**所在目录的绝对路径**,之后整条会话内复用。
### -1.1 已设环境变量时直接用
如果 shell 环境变量 `SKILL_DIR` 已经存在且指向一个含 `SKILL.md` + `scripts/`
的目录 → 直接用,跳过 -1.2。
### -1.2 自动探测(按下面顺序取第一个命中)
```bash
candidates=(
"${PWD}/.cursor/skills/databrain-ai-gallery-upload/global" # 项目级安装(当前 workspace · global)
"${HOME}/.cursor/skills/databrain-ai-gallery-upload/global" # 用户级安装(global)
)
SKILL_DIR=""
for c in "${candidates[@]}"; do
if [ -f "${c}/SKILL.md" ] && [ -d "${c}/scripts" ]; then
SKILL_DIR="${c}"
break
fi
done
export SKILL_DIR
echo "SKILL_DIR=${SKILL_DIR}"
```
### -1.3 兜底:让用户告知
如果 -1.2 两个候选都不命中(例如 skill 被复制到自定义路径)→ 追问用户:
```
未能在标准位置定位到本 skill,请提供本 SKILL.md 文件所在目录的绝对路径
(应包含 SKILL.md 与 scripts/ 子目录):
```
拿到路径后:
```bash
export SKILL_DIR="<用户给的路径>"
[ -f "${SKILL_DIR}/SKILL.md" ] && [ -d "${SKILL_DIR}/scripts" ] || {
echo "SKILL_DIR 无效(缺 SKILL.md 或 scripts/)" >&2; exit 2;
}
```
### -1.4 后续命令统一写法
所有脚本调用一律:
```bash
python "${SKILL_DIR}/scripts/<name>.py" <flags...>
```
Cursor 在同一会话内 shell 是 stateful 的,`export SKILL_DIR` 后续命令可见。
本 SKILL.md 之后所有示例命令都按这个约定写,agent 不要再贴 `.cursor/skills/...`
形式的硬编码路径。
## Step 0:硬约束(执行任何脚本前必须满足)
本 skill 的所有网络调用 **必须** 走 `scripts/` 下的 Python 脚本。脚本的 CLI 签名
本身就是字段白名单:危险字段(如访问权限相关)**物理上不存在**于任何脚本入参,
通过脚本调用根本无法越权。
### 0.1 工具白名单(仅允许执行下列脚本)
```
${SKILL_DIR}/scripts/gallery_list_tags.py
${SKILL_DIR}/scripts/gallery_get_dashboard.py
${SKILL_DIR}/scripts/gallery_create.py
${SKILL_DIR}/scripts/gallery_replace.py
${SKILL_DIR}/scripts/gallery_update_dashboard.py
${SKILL_DIR}/scripts/gallery_operation_log.py
${SKILL_DIR}/scripts/extract_entry_html.py
${SKILL_DIR}/scripts/extract_html_text.py
```
本 skill **零跨 skill 依赖**,所有脚本都在 `${SKILL_DIR}/scripts/` 下;
执行任何脚本前先按 Step -1 确认 `SKILL_DIR` 已正确导出。
**显式禁止**:
- 任何形式的 `curl` / `wget` / `httpie` / 自写 Python `requests.post(...)` / `urllib.request.urlopen(...)`
等直接对任何后端域名发请求。
- 调用 `gallery_*` 脚本但传入本文档未列出的额外参数(argparse 会用 `allow_abbrev=False`
+ 严格 flag 名拒绝未知参数)。
- 改写 / 绕过脚本(如 `python -c "import _gallery_client; ..."`)—— `_` 前缀模块是
internal helper,禁止直接 `import` 或调用。
- 调用 0.1 工具白名单以外的任何脚本 / 命令 / 接口。0.2 表格之外的能力一律视为
本 skill 不支持,参 0.4 拒答模板。
- **跨 skill 复用 CREATE / REPLACE 选择**:每次进入本 skill 必须按 Step 1 重新追问,
禁止根据上下文里浮动的旧 URL 自动判定。
### 0.2 能力边界(这就是本 skill 的全部能力)
| 场景 | 脚本 | CLI 参数 |
|---|---|---|
| 浏览系统标签 | `gallery_list_tags.py` | 无参 |
| 查看作品信息 | `gallery_get_dashboard.py` | `--rule-key` |
| 新上传作品 | `gallery_create.py` | `--file --name-cn --name-en [--desc-cn] [--desc-en] --tags` |
| 替换作品源文件 | `gallery_replace.py` | `--rule-key --file` |
| 修改作品信息 | `gallery_update_dashboard.py` | `--rule-key [--name-cn] [--name-en] [--desc-cn] [--desc-en] [--tags]` |
| 埋点上报 | `gallery_operation_log.py` | `--rule-key --flow-type --upload-paths` |
| 抽取入口 HTML | `extract_entry_html.py` | `--input` |
**0.2 表格列出的就是本 skill 的全部能力**。表格外的任何需求(包括但不限于访问
权限 / 可见范围 / 分享对象的调整)请用户去 Gallery 前端 UI 操作。agent 在本 skill
内不感知任何其它后端能力,遇到表格外的请求一律按 0.4 拒答模板处理。
### 0.3 Pre-flight self-check(执行任何脚本前 3 问)
1. 要发的命令第一个 token 是不是 `python` / `python3`?否 → **中止**。
2. 脚本路径是否在 0.1 工具白名单内?否 → **中止**。
3. 传给脚本的参数是否只用本文档对应步骤里写出来的 flag?否 → **中止**,把完整
命令贴给用户看。
任一中止后,agent **不得**自行重写命令;必须先把违规情况摆给用户,由用户显式确认。
### 0.4 拒答模板(用户要求 0.2 不支持的能力时一字不差照回)
当用户在任何阶段说出「让 xxx 也能看 / 公开 / 内部可访问 / DataBrain 用户可访问 /
加白名单 / share / 分享给 / 给 xxx 权限 / 让别人看」等意图,agent **必须**回这句
字面文本,**禁止自由发挥**:
> 本 skill 上传 / 替换的作品默认仅本人可访问。访问权限 / 分享相关调整不在本 skill
> 范围内,如需开放,请到 Gallery 前端作品详情页修改。
回完此句后,继续按默认走 CREATE / REPLACE / UPDATE 流程;用户坚持要改 → 提示
用户结束本 skill 自行去前端。
### 0.5 反例(禁止 / 允许 对照)
禁止:
```bash
# 直接 curl 绕过脚本
curl -X POST $DATABRAIN_HOST/api/ai-gallery/dashboards -F "file=..." ...
# 自写 Python requests 拼请求
python -c "import requests; requests.post(...)"
# 给脚本塞本文档没列出来的 flag
python gallery_create.py --file x.html --some-unknown-flag value ...
# 调白名单外脚本
python gallery_set_xxx.py ...
```
允许:
```bash
python "${SKILL_DIR}/scripts/gallery_create.py" \
--file ./report.html \
--name-cn "..." --name-en "..." \
--desc-cn "..." --desc-en "..." \
--tags '[{"id":1}]'
python "${SKILL_DIR}/scripts/gallery_update_dashboard.py" \
--rule-key g-xxx \
--name-cn "新名称" \
--tags '[{"id":2}]'
python "${SKILL_DIR}/scripts/gallery_replace.py" \
--rule-key g-xxx --file ./new.html
```
### 0.6 局限性自述
- 脚本封装把 0.2 列出的能力以外的所有动作**物理上挡在外面**——脚本 CLI 不存在
对应 flag。
- 仍存在的软约束部分:用户用自然语言直接指挥 agent 写新 Python / curl 这条路径
靠 0.3 自检 + 0.4 拒答模板防住;不能 100% 杜绝。
## Step 1:解析用户意图(CREATE vs REPLACE,每次必追问)
> **铁律**:每次进入本 skill,无论上下文里出现过多少 URL 或之前对话里用户表达过
> 什么意图,agent **必须**在动手前先追问用户一次本次的操作类型。不允许复用历史
> 选择直接进 REPLACE。
### 1.1 扫描候选 URL(仅作参考,不作判定)
在**当前用户消息**(即触发本次 skill 的那条消息)里识别两种 URL 形态,抽出所有
`(url, rule_key)` 对:
- **旧形态(直访)**:正则 `(?:https?://[^/\s]+)?/as/report/([^/\s]+)/[^\s]*`,捕获组 1 即为 `rule_key`。
- **新形态(前端 `/aigallery/report` 中转页)**:正则
`(?:https?://[^/\s]+)?/aigallery/report\?[^\s]*?\bpath=([^&\s]+)`,捕获组 1 是
URL 编码的 path(形如 `%2Fas%2Freport%2Fg-xxx%2F...`)。agent 先把 `%XX` 序列
还原为对应字符(典型 `%2F` → `/`),再对解码结果套用旧形态正则提 `rule_key`。
注意:
- 只扫"当前消息",**不**扫之前的对话历史。如果用户本次没主动贴 URL → 候选列表为空。
- 找到的 URL 仅作"候选展示",不直接进入 REPLACE。
- assistant 自己之前生成的输出里包含的 URL(例如同一会话里前一次 CREATE 后展示
的访问 URL)**严格不采信**——只看当前用户消息纯文本。
### 1.2 强制追问
用单选题向用户追问一次(选项按 1.1 扫到的候选 URL 动态生成):
```
本次是新上传,还是替换某条已上传作品的源文件?
[ ] 新上传作品(CREATE)
[ ] 替换:<候选 URL 1,若有>
[ ] 替换:<候选 URL 2,若有>
[ ] 替换:其它 URL(让我手动粘 URL / rule_key)
```
- 候选 URL 列表来自 1.1;没候选则只展示「新上传」+「替换:手动粘」两项。
- 用户选 REPLACE 但选「其它 URL」→ 让用户粘 URL / rule_key;agent **同样套用 1.1
里的两套正则**(旧形态 `/as/report/<key>/...` 直接拿 `rule_key`;新形态
`/aigallery/report?path=<encoded>...` 先把 `path` 参数做 URL 解码再二次提
`rule_key`);两套均不命中且**也不是**纯 `g-` 形态 rule_key 字符串 →
提示用户检查格式并允许**最多重试 1 次**;二次失败 → 中止 skill。
- 用户选 CREATE → 进入 Step 2 → Step 3 → Step 4 → Step 5 → Step 6.5 → ...
- 用户选 REPLACE + 具体 URL → 抽 `rule_key` 后进入 Step 2 → Step 6 → Step 6.5 → ...
**agent 在本次 skill 内部记录 `flow_type` 状态**(CREATE → `flow_type=create`,
REPLACE → `flow_type=replace`),后续 Step 8 operationLog 要用。
### 1.3 不缓存选择
本次 skill 完成 / 用户结束后,agent **不得**在记忆 / 上下文里写下"用户偏好
REPLACE / CREATE"之类的痕迹。下次再触发本 skill 时**从头再问**一遍 1.2。
## Step 2:识别本地输入类型(CREATE / REPLACE 共用)
判定用户给的本地路径,输出本次要上传的 `file_path`(以及 CREATE 流程下用于
AI 推断元数据的 `entry_html_path`)。
### 2.1 主判定分支
| 用户输入 | 处理 | `file_path` |
|---|---|---|
| `.html` / `.htm` 文件 | single HTML,**进入 2.2 同级资源探测** | `<用户路径>` |
| `.zip` 文件 | package(跳过本地校验,服务端校验) | `<用户路径>` |
| 目录,**剔除 `__MACOSX/` `.DS_Store` 后总文件数 == 1 且是 `.html`** | single HTML,跳过 2.2 | 该 `.html` 路径 |
| 目录,含其它非 html 资源 / 多个 html | 打 ZIP 到 `/tmp/upload_<random>.zip`,跳过 2.2 | 临时 zip 路径 |
| 目录,无任何 `.html` | 中止报错 | — |
| 其它扩展名 / 不存在的路径 | 中止报错 | — |
目录打包命令:
```bash
random_id=$(python3 -c "import random,string; print(''.join(random.choices(string.ascii_lowercase+string.digits,k=8)))")
file_path="/tmp/upload_${random_id}.zip"
(cd "${user_dir}" && zip -q -r "${file_path}" . -x '__MACOSX/*' -x '.DS_Store')
```
### 2.2 单 HTML 同级资源探测(仅 2.1 走 single HTML 分支 + 路径是 `.html` 文件时触发)
防 footgun:用户经常拖一个 `index.html` 进来,实际上同级目录还有 `style.css` /
`app.js` / `assets/` 等被该 HTML 引用的资源 → 直接当单 HTML 上传后访问时 404。
判定(只看 `dirname` 的第一层,不递归):
```bash
parent_dir=$(dirname "${file_path}")
sibling_count=$(ls -A "${parent_dir}" \
| grep -v -F -x "$(basename "${file_path}")" \
| grep -v -F -x '__MACOSX' \
| grep -v -F -x '.DS_Store' \
| wc -l \
| tr -d ' ')
```
`sibling_count > 0` → 展示 warning + 追问一次:
```
检测到 ${file_path} 同级目录还有 ${sibling_count} 项(如 css / js / 图片 / 子目录),
当前会按【单 HTML】上传,这些资源不会被携带。
是否改为打包整个目录 ${parent_dir} 上传?
[ ] 改为打包整个目录上传(推荐:避免引用断链)
[ ] 继续按单 HTML 上传(HTML 自含,不依赖同级资源)
```
- 用户选「改为打包」→ 按 2.1 目录分支的打包逻辑执行:`file_path` 切换为
`/tmp/upload_<random>.zip`(zip 整个 `parent_dir`);同时**记住用户原始的那个
`.html` 路径**作为 `entry_html_path`,Step 3 直接用它,**不再调**
`extract_entry_html.py`(避免 zip 解包后挑错入口)。
- 用户选「继续按单 HTML」/ 不明确回复 → 保留 `file_path = <原 .html 路径>`,
按单 HTML 流程走。
- 探测命令本身失败(如目录权限)→ 静默跳过 2.2,按 2.1 结果继续,不阻塞主流程。
### 2.3 入口 HTML 抽取(仅 CREATE 流程需要,用于 Step 3 推断 name/desc)
若 2.2 已经设置了 `entry_html_path`(用户从单 HTML 升级到打包目录的情况)→
**直接复用,跳过此步**。
否则按 `file_path` 类型抽取:
```bash
entry_html_path=$(python "${SKILL_DIR}/scripts/extract_entry_html.py" --input "${file_path}")
```
脚本行为:
- `.html` / `.htm` → 直接回显路径。
- `.zip` → 列 entries 过滤 `__MACOSX/` `.DS_Store`,按 `(层级浅, index.html 优先, 路径短)`
顺序取第一个 `.html?` 解到 `/tmp/extract_entry_<random>.html`,回显临时路径。
- 目录 → `os.walk` 同样规则取第一个 `.html?`,回显原始路径(不复制)。
REPLACE 流程不需要这一步,直接跳到 Step 6。
### 2.4 临时文件清理
流程末尾(**Step 8 operationLog 上报之后、Step 9 追问之前**)统一清理:
```bash
rm -f /tmp/upload_*.zip /tmp/extract_entry_*.html
```
## Step 3:HTML 元数据 AI 推断(CREATE 专属)
1. 入口 HTML 路径由 Step 2 的 `entry_html_path` 提供。
2. 调用:
```bash
python "${SKILL_DIR}/scripts/extract_html_text.py" "${entry_html_path}"
```
stdout 第 1 行是 `<title>` 文本,第 2 行起是正文,总长度 ≤ 4KB。
3. **agent 内部**根据 title + 正文推断 4 个字段(不走脚本)。
### 3.1 字段长度硬限(后端 `class-validator @MaxLength`,超 1 个字符直接 400)
| 字段 | 上限 | 内容指引 |
|---|---|---|
| `name_cn` | **≤ 40** | 中文表达,避免营销话术 |
| `name_en` | **≤ 60** | Title Case,避免无意义堆砌 |
| `desc_cn` | **≤ 200** | 一句话概括,不分段、不堆 emoji |
| `desc_en` | **≤ 300** | 与 `desc_cn` 语义对齐,**英文容易超**,特别留意 |
### 3.2 字符计数算法 + safety buffer
- 后端用 JavaScript `String.prototype.length`(UTF-16 code unit 数,emoji 占 2 个、中文占 1 个)。
- agent 估算时按 **Python `len(s)` 等价 = code point 数(emoji 算 1)**;
**混入 emoji / 罕见辅助平面字符时实际可能超后端硬限**。
- 为安全起见,**采用 ~10% safety buffer 自我设限**:
- `name_cn` 目标 ≤ 36(硬限 40)
- `name_en` 目标 ≤ 54(硬限 60)
- `desc_cn` 目标 ≤ 180(硬限 200)
- `desc_en` 目标 ≤ 270(硬限 300)
- **agent 生成后必须自检长度**;超出目标值 → 主动截断/改写到目标范围内,
**不要原样送出去赌后端宽容**。
### 3.3 强制自检步骤(生成 → 检查 → 超限就改写,再展示)
1. 第一稿生成 4 个字段。
2. 对每个字段计算字符数(Python `len(s)`)。
3. 若任一字段 > 3.2 表里的"目标值":
- **优先改写而非粗暴 cut**:去掉冗余形容词 / 长定语,保留核心语义。
- 不行再从尾部截断到目标值并补 `…`。
4. 改写后再次自检;仍超 → 继续压缩,直到全部 ≤ 目标值。
5. **不允许把超限稿直接展示给用户**——用户可能直接「OK」就送出去触发后端 400。
4. 展示推断结果给用户,**追问一次**:
> 我从 HTML 里推出来这 4 个字段(已按长度限制压缩到目标范围内):
> name_cn=「...」(N/40) / name_en=「...」(N/60) /
> desc_cn=「...」(N/200) / desc_en=「...」(N/300)。
> 直接用还是给一份修改稿?
每个字段后**括号里附实际字符数 / 硬限**,方便用户直观感知。
- 用户回「OK / 直接用 / 沉默」→ 用推断稿。
- 给修订稿 → 用修订稿,但 agent **仍要对修订稿做一次 3.3 自检**;
用户给的修订稿超限时不要静默上传,要把超限字段提示给用户改短,**最多重试 1 次**;
仍超 → 截断后告知用户已截断。
5. 抽取失败兜底:`extract_html_text.py` 返回非 0 或 title + 正文 < 100 字符 →
不推断,让用户手填 4 个字段;手填时同样按 3.2 / 3.3 校验。
## Step 4:tag 选择(CREATE 专属)
```bash
python "${SKILL_DIR}/scripts/gallery_list_tags.py"
```
输出 JSON 含 `items[]`,每项 `{ id, name_cn, name_en, type, is_mine, count }`。
1. agent 按可读表展示给用户,把 `is_mine=true` 的自建标签拉前面便于复用。
2. 让用户选 1-5 个 id(**后端要求至少 1 个、最多 5 个**)。
3. 用户表示不想选 / 选了 0 个 → 告知后端硬约束,并 agent **主动建议**最贴近
HTML 内容的 tag(用 Step 3 拿到的 title + 正文做匹配)。用户确认后继续。
4. 把用户选定的 id 列表拼成 JSON 字符串:
```
tags_json='[{"id":1},{"id":3}]'
```
作为 `--tags` 参数传给 Step 5。
## Step 5:上传(CREATE 路径)
- 用户若提到「DataBrain 用户可访问 / 完全公开 / 给 xxx 权限 / 让 xxx 也能看」
等意图 → 按 **Step 0.4 拒答模板**字面回复,仍按默认流程上传。
- 调脚本:
```bash
python "${SKILL_DIR}/scripts/gallery_create.py" \
--file "${file_path}" \
--name-cn "${name_cn}" --name-en "${name_en}" \
--desc-cn "${desc_cn}" --desc-en "${desc_en}" \
--tags "${tags_json}"
```
`--desc-cn` / `--desc-en` 若用户没填,省略对应 flag 即可(不要传空串)。
- 脚本 stdout 解析:
- `ok: true` → 拿 `rule_key`,进入 **Step 6.5 合并节点**。
- `ok: false` → 把 `code` / `msg` / `errors` / `detail` 原文透回用户后中止。
## Step 6:替换(REPLACE 路径)
### 6.1 owner 预检
```bash
python "${SKILL_DIR}/scripts/gallery_get_dashboard.py" --rule-key "${rule_key}"
```
解析输出:
- `ok: false` → 透出错误中止(404 / 403 等)。
- `ok: true` 且 `is_mine == true` → 通过。
- `ok: true` 但 `is_mine == false` → 中止「非 owner,无权替换」。
### 6.2 单 HTML 文件名提示
仅当本次上传是单 HTML 模式触发:从 6.1 输出的 `link` 拆 basename
(`/as/report/<key>/<html_path>` 末段)。与用户上传 HTML 文件名比对:
- 不同 → 展示警告并**追问一次**:
> Gallery 替换接口会强制按旧文件名 `<old>` 落盘,你提交的 `<new>` 不会
> 出现在访问 URL 中。是否继续?
用户明确拒绝才中止;其它回复(含「继续」/「OK」/沉默)按继续处理。
### 6.3 执行替换
```bash
python "${SKILL_DIR}/scripts/gallery_replace.py" \
--rule-key "${rule_key}" --file "${file_path}"
```
- `ok: false` → 错误原文透出(`source_mode_mismatch` / `invalid_zip_entry` /
`backup_partial_failure` 等)。
- `ok: true` → 记下 `backup_path` 等响应字段,进入 **Step 6.5 合并节点**。
### 6.4 范围声明
REPLACE 流程 **不做** Step 3 AI 推断 / Step 4 tag 选择(与「只替换文件」语义对齐)。
如果用户在替换后想改 name / desc / tags,统一收口在 Step 9 追问环节。
## Step 6.5:GET 详情拿 link(CREATE / REPLACE 都必须执行)
这是 CREATE / REPLACE 两路的**强制合并节点**:
- **CREATE 路径**下,`gallery_create.py` 的输出**只有** `rule_key` / `id`,没有
`link`,必须再 GET 一次才能拼 Step 7 的访问 URL。
- **REPLACE 路径**下,虽然 6.1 已 GET 过一次,但 replace 之后 `link` /
`html_files` 可能变化(例如单 HTML 重命名),**必须再 GET 一次**以拿最新值。
```bash
python "${SKILL_DIR}/scripts/gallery_get_dashboard.py" --rule-key "${rule_key}"
```
记下输出的 `link` / `name_cn` / `name_en` / `desc_cn` / `desc_en` / `tags`,**以及
脚本派生的 `display_url` / `legacy_url` 两个字段**:
- `display_url` = `${DATABRAIN_DISPLAY_HOST}/aigallery/report?path=<encoded link>&name=<encoded name>`,
对齐前端 `encodeURIComponent` 编码风格,是 Step 7 展示给用户的访问地址。
- `legacy_url` = `${DATABRAIN_DISPLAY_HOST}${link}`,旧形态直访 URL,供 Step 8
operationLog 双上报。
这份缓存同时支撑 Step 7 / Step 8 / Step 9,**不要再让 agent 自己手工拼 URL**——
脚本已经做好编码(safe 字符集对齐前端 `encodeURIComponent`),手工拼容易遗漏 `path`
里 `/` → `%2F`、`name` 里空格 → `%20` 等细节,导致与前端 UI 字面不一致。
## Step 7:输出访问地址
- 直接用 Step 6.5 缓存的 `display_url` 字面展示给用户(已是完整 URL,前端
`/aigallery/report` 中转页形态,括号 / 单引号等字符严格保留字面,与浏览器地址栏
一致)。**不要**自己用 `${HOST}${link}` 之类公式手工拼,避免编码风格漂移。
- 单 HTML / ZIP 都只展示主入口(`link` 即主入口;用户问其它入口让 ta 去
Gallery 详情页看)。
- REPLACE 模式额外展示 `backup_path`(旧版本备份位置,可忽略)。
样例输出(注意 `(002878.SZ)` 等括号保留字面,对齐前端 `encodeURIComponent` 行为):
```
上传成功,访问地址:
- https://databrain-global.intlgame.com/aigallery/report?path=%2Fas%2Freport%2Fg-xxx%2Fyuanlongyatu_report.html&name=Yuanlong%20Yatu%20(002878.SZ)%20Recent%20Performance%20Snapshot
访问权限:仅本人可访问。如需开放,请到 Gallery 前端作品详情页修改。
```
REPLACE 模式追加一行:
```
旧版本已由后端自动备份,无需手工处理。
```
## Step 8:上报 operationLog(CREATE / REPLACE 共用,非关键路径)
```bash
python "${SKILL_DIR}/scripts/gallery_operation_log.py" \
--rule-key "${rule_key}" \
--flow-type "${flow_type}" \
--upload-paths "${upload_paths_json}"
```
- `flow_type` 在 CREATE 流程里设 `create`,REPLACE 流程里设 `replace`(脚本
argparse `choices` 强约束)。
- `upload_paths_json` 同时上报 Step 6.5 缓存的 `display_url`(新中转 URL)与
`legacy_url`(旧直访 URL)两条 —— **新 URL 在前、旧 URL 在后**:
```
upload_paths_json='["<display_url>","<legacy_url>"]'
```
双 URL 上报的目的:让前端用户实际打开的链接(新中转页)和下游历史埋点统计对
`/as/report/` 前缀的识别同时可用,避免切换 URL 形态后老的报表统计断流。
- 脚本失败一律 exit 0(非关键),SKILL.md 不显式处理。
Step 8 结束后按 Step 2.4 清理临时文件:
```bash
rm -f /tmp/upload_*.zip /tmp/extract_entry_*.html
```
## Step 9:上传 / 替换后追问改报表信息(仅 name / desc / tags)
### 9.1 展示当前元数据
用 Step 6.5 缓存的字段展示给用户,**仅展示** `name_cn` / `name_en` / `desc_cn` /
`desc_en` / `tags` 5 项。脚本输出只有这些字段,agent 在此环节看不到其它字段,
也不应展示给用户。
### 9.2 追问一次
> 是否需要修改报表信息(名称 / 描述 / 标签)?
> 也可以直接到 Gallery 首页编辑:`${DATABRAIN_DISPLAY_HOST:-https://databrain-global.intlgame.com}/aigallery/home`
附 Gallery 首页链接是给用户一条 UI 出口(找到对应作品后在前端直接改);host 严格
走 `DATABRAIN_DISPLAY_HOST`,pre / 生产环境自动对齐,与 Step 7 / Step 8 同一份变量。
该链接不涉及编码、与 `rule_key` 无关,agent 按上式字面拼即可,**不需要**调任何脚本。
### 9.3 用户回「不用」/「没问题」/明确拒绝
流程结束,不再追问。
### 9.4 用户表达修改意图
- **场景 A:用户提到访问权限 / 分享 / 让别人看 / 加白名单等** → 按 **Step 0.4
拒答模板**字面回复,并请用户去 Gallery 前端,**不调** update 脚本。
- **场景 B:用户要改 tags**:
- 先调一次 `gallery_list_tags.py` 把可选 tag 列出给用户(用户记不住 id 是常态,
REPLACE 流程下用户甚至从未在本次 skill 里见过 tag 列表):
```bash
python "${SKILL_DIR}/scripts/gallery_list_tags.py"
```
- 让用户给一个新 id 列表(**1-5 个**);后端要求至少 1 个、最多 5 个,给
`tags=[]` 会被 40001 拒绝。如用户明确要清空 tag,告知该后端限制并请用户给
至少 1 个 id。
- 把 id 列表拼成 JSON `[{"id":<int>}, ...]`,作为 `--tags` 传给下面的 update 调用。
- **场景 C:用户改 name / desc**:直接收集用户给的新值。
**每个字段都要按 Step 3.2 / 3.3 自检长度上限**(name_cn ≤ 40 / name_en ≤ 60 /
desc_cn ≤ 200 / desc_en ≤ 300):超限不要静默送上去触发后端 400,
先提示用户压缩,最多重试 1 次后由 agent 截断处理。
收集完毕后:
```bash
python "${SKILL_DIR}/scripts/gallery_update_dashboard.py" \
--rule-key "${rule_key}" \
--name-cn "${new_name_cn}" \
--name-en "${new_name_en}" \
--desc-cn "${new_desc_cn}" \
--desc-en "${new_desc_en}" \
--tags "${new_tags_json}"
```
**仅传用户明确改的字段**,其它 flag 省略。脚本 CLI 没有其它 flag。
### 9.5 回展确认
`ok: true` 后再调一次 `gallery_get_dashboard.py` 拿最新值回展给用户:
```bash
python "${SKILL_DIR}/scripts/gallery_get_dashboard.py" --rule-key "${rule_key}"
```
`ok: false` → 错误原文透出,**不再二次追问**。
### 9.6 不主动猜
用户没明确需求 → 不主动猜测要改什么;只在用户回复里明确的字段动手。
## Step 10:错误码 / exit code 速查
脚本退出码(所有脚本统一):
| exit | 含义 |
|---|---|
| 0 | 成功(含 `gallery_operation_log.py` 静默失败) |
| 1 | 后端业务错误(透出 `code` + `msg` + `errors` + `detail`) |
| 2 | 入参错误 / 本地预检失败(缺 token、文件 > 50MB、MIME 不支持、找不到入口 HTML、`gallery_update_dashboard.py` 啥也没改 等) |
后端业务错误码:
| code | 含义 / 处理 |
|---|---|
| 40001 | 参数校验失败(`tags` 长度 1-5、文件类型、字符串长度超限等)→ 透出 `errors[]` / `message` |
| 40101 | 鉴权失败 → 提示用户重新拿 token |
| 40301 / 40302 | 无权访问该作品 → 中止 |
| 40401 | dashboard 不存在 → 中止 |
| 400 | DTO 字段级校验失败(NestJS 默认信封,msg 形如 `"desc_en must be shorter than or equal to 300 characters"`)→ **大概率是 Step 3 长度自检漏了**,按 Step 3.2 / 3.3 改短后重试;不要重复发同样的请求 |
| `source_mode_mismatch` | 单 HTML 上传到 ZIP dashboard(反之),按 `detail` / `errors[0].msg` 原样展示 |
| `invalid_zip_entry` | ZIP entry 含非法路径 → 原样展示 |
| `backup_partial_failure` | 文件备份阶段失败 → 原样展示 |
脚本 stdout 失败行示例:
```json
{"ok": false, "code": 40001, "msg": "tags must contain 1-5 items", "errors": [...], "detail": null}
```
把这行原文转给用户,不要二次包装。
## Step 11:平台兼容性 / 环境要求
- macOS / Linux 原生支持。Windows 走 Git Bash / WSL(`zip` / `find` / `/tmp/` /
`python3` 都需可用)。
- Python ≥ 3.8,**零外部依赖**(脚本均纯 stdlib:`urllib` + `ssl` + `json` +
`zipfile` + `argparse`),不需要 `pip install` 任何包。
- 如果终端启动时设置了 `SSL_CERT_FILE` 指向内网专用 PEM,脚本会自动用候选系统
cafile 兜底重试,首次触发时往 stderr 打一行诊断信息。
## 流程图
```mermaid
flowchart TD
Start[用户消息] --> ScanUrls["Step 1.1: 扫描当前消息里的候选 URL"]
ScanUrls --> AskMode["Step 1.2: 必追问 CREATE / REPLACE"]
AskMode -->|新上传| Create[CREATE 模式]
AskMode -->|替换 + 候选 URL| Replace[REPLACE 模式 + rule_key]
AskMode -->|替换 + 手动粘| AskManualUrl[再问用户粘 URL / rule_key]
AskManualUrl --> Replace
AskManualUrl -->|无法获取| Abort[中止 skill]
Create --> DetectType["Step 2.1: 单 HTML / ZIP / 目录?"]
Replace --> DetectType
DetectType -->|单 HTML 文件| SiblingCheck["Step 2.2: 同级资源探测"]
DetectType -->|.zip / 目录-多文件 / 目录-唯一 html| EntryExtract["Step 2.3: extract_entry_html.py (CREATE only)"]
SiblingCheck -->|同级有资源 + 用户选打包| Repack["改打 zip, entry_html 复用原 .html"]
SiblingCheck -->|无同级 / 用户选单 HTML| EntryExtract
Repack --> EntryExtract
EntryExtract --> ModeFork{CREATE or REPLACE?}
ModeFork -->|CREATE| InferMeta["Step 3: extract_html_text.py + LLM 推 name/desc"]
InferMeta --> ConfirmMeta[追问一次确认 name/desc]
ConfirmMeta --> ListTags["Step 4: gallery_list_tags.py"]
ListTags --> UploadCreate["Step 5: gallery_create.py"]
ModeFork -->|REPLACE| ReplacePrecheck["Step 6.1: gallery_get_dashboard.py (owner + basename 警告 6.2)"]
ReplacePrecheck --> UploadReplace["Step 6.3: gallery_replace.py"]
UploadCreate --> Refetch["Step 6.5: gallery_get_dashboard.py 拿 link/元数据 + 派生 display_url/legacy_url"]
UploadReplace --> Refetch
Refetch --> OutputUrl["Step 7: 输出 /aigallery/report 中转 URL (display_url)"]
OutputUrl --> OpLog["Step 8: gallery_operation_log.py 双 URL 上报"]
OpLog --> Cleanup["Step 2.4: 清理 /tmp 临时文件"]
Cleanup --> AskUpdate["Step 9.1-9.2: 展示元数据 + 追问 (附 /aigallery/home 链接)"]
AskUpdate -->|要改 tags| RelistTags["9.4 场景B: gallery_list_tags.py"]
AskUpdate -->|要改 name/desc| Patch["9.4 场景C: gallery_update_dashboard.py"]
AskUpdate -->|要改访问权限 / 分享| Refuse["Step 0.4 拒答模板"]
AskUpdate -->|不改| Done[结束]
RelistTags --> Patch
Patch --> RefetchAfter["9.5: gallery_get_dashboard.py 回展确认"]
RefetchAfter --> Done
Refuse --> Done
```
Files in this skill
- SKILL.md
- scripts/_gallery_client.py
- scripts/extract_entry_html.py
- scripts/extract_html_text.py
- scripts/gallery_create.py
- scripts/gallery_get_dashboard.py
- scripts/gallery_list_tags.py
- scripts/gallery_operation_log.py
- scripts/gallery_replace.py
- scripts/gallery_update_dashboard.py
Attribution
Comments
Loading comments…