Loading...
Loading...
Interactively manage Sub-Store node subscriptions and multi-source collections; orchestrate operator pipelines (filtering/renaming/flagging/sorting/deduplication); author and mount JavaScript node-processing scripts on demand; and export clean node subscriptions or Proxy Provider links. Strictly follow the Sub-Store frontend Web UI two-way-binding conventions by constructing complete Schemas, fully preventing issues where “backend execution takes effect but frontend radio controls become desynchronized, input fields remain blank, or configuration is overwritten and lost by the UI.”
npx skill4agent add skyxim/skills sub-store-copilot[!NOTE] This Skill focuses on producing clean, standardized, optimized pure node sets or Proxy Provider subscriptions; it does not cover generating complete client configuration documents that include full DNS and complex routing rules. Reject rigid workflows: In practice, most requests involve temporary, one-off changes. Every capability is designed as an atomic operation, can be triggered independently at any time, and can also be freely combined as needed.
/api/subs/api/collections/api/file/api/settings/api/sync/artifacts127.0.0.1:3000SUB_STORE_FRONTEND_BACKEND_PATH/<SECRET_PATH>https://sub.example.com/<SECRET_PATH>/api/...http://sub.storesub.storeNXDOMAINhttp_proxy======================= 形态一:独立服务器形态 (Standalone Server) =======================
+------------------------------+ HTTP / HTTPS REST API +------------------------------+
| 管理端 (curl / Python / | ============================> | Sub-Store 独立服务器 |
| Copilot / 客户端拉取) | <============================ | (独立 HTTP 服务 / 反代端点) |
+------------------------------+ +------------------------------+
|
| 直连拉取
v
+------------------------------+
| 外部机场与自建节点上游源 |
+------------------------------+
================= 形态二:代理 MITM 下的脚本劫持形态 (Proxy MITM Script Hijack) =================
+------------------------------+
| 管理端 (curl / Python / |
| Copilot / 浏览器) |
+------------------------------+
|
| 1. 发起请求: http://sub.store/api/... (必须经由代理客户端捕获接管)
v
+--------------------------------------------------------------+
| 代理客户端 (Surge / Loon / Quantumult X / Shadowrocket) |
| |
| +--------------------------------------------------------+ |
| | 流量接管层 (TUN 增强模式 / 操作系统代理 / 本地代理端口) | |
| +--------------------------------------------------------+ |
| | |
| | 2. 匹配虚拟域名 sub.store |
| v |
| +--------------------------------------------------------+ |
| | 内置 MITM 拦截与 JS 脚本引擎 (执行 Sub-Store 模块) | |
| +--------------------------------------------------------+ |
+--------------------------------------------------------------+
|
| 3. 直连或代理分流拉取
v
+------------------------------+
| 外部机场与自建节点上游源 |
+------------------------------+urlpathmodecontentactionkeepmode: "link"|"script"contentaction: "keep"|"drop"keep: true|falseprocess/api/settingsgistTokenproxysubscriptionsremarktagignoreFailedRemoteSubGET/api/sub/.../api/collection/.../api/settingscontentprocessargsSUB_STORE_PREFIX_URL/<SECRET_PATH>http://127.0.0.1:3000http://sub.store/api/subshttps://sub.store/api/subshttp://sub.storeif [ -n "$SUB_STORE_PREFIX_URL" ]; then
SUB_STORE_URL="${SUB_STORE_PREFIX_URL%/}"
echo "使用指定的独立服务器端点 (Prefix URL): <SECRET_PATH>"
elif curl -s --max-time 1.5 "http://127.0.0.1:3000/api/subs" > /dev/null 2>&1; then
SUB_STORE_URL="http://127.0.0.1:3000"
echo "已探测到本地独立服务器: $SUB_STORE_URL"
elif curl -s --max-time 1.5 "http://sub.store/api/subs" > /dev/null 2>&1; then
SUB_STORE_URL="http://sub.store"
echo "已探测到代理客户端 MITM 虚拟域名 (直连接管 HTTP): $SUB_STORE_URL"
elif curl -k -s --max-time 1.5 "https://sub.store/api/subs" > /dev/null 2>&1; then
SUB_STORE_URL="https://sub.store"
echo "已探测到代理客户端 MITM 虚拟域名 (直连接管 HTTPS): $SUB_STORE_URL"
elif curl -x "http://127.0.0.1:6152" -s --max-time 1.5 "http://sub.store/api/subs" > /dev/null 2>&1; then
SUB_STORE_URL="http://sub.store"
export http_proxy="http://127.0.0.1:6152"
export https_proxy="http://127.0.0.1:6152"
echo "已通过本地代理端口 (6152) 接入代理客户端 MITM 虚拟域名: $SUB_STORE_URL"
elif curl -x "http://127.0.0.1:7890" -s --max-time 1.5 "http://sub.store/api/subs" > /dev/null 2>&1; then
SUB_STORE_URL="http://sub.store"
export http_proxy="http://127.0.0.1:7890"
export https_proxy="http://127.0.0.1:7890"
echo "已通过本地代理端口 (7890) 接入代理客户端 MITM 虚拟域名: $SUB_STORE_URL"
else
echo "未探测到可用 Sub-Store 端点。请根据运行形态处理:"
echo "1. 独立服务器:请执行 export SUB_STORE_PREFIX_URL=\"https://domain.com/<SECRET_PATH>\""
echo "2. 代理 MITM 劫持:请确保代理客户端(Surge/Loon 等)已启动且当前环境流量已交由代理捕获"
ficurl -s "${SUB_STORE_URL}/api/subs" | jq .contenturlcurl -s -X POST "${SUB_STORE_URL}/api/subs" \
-H "Content-Type: application/json" \
-d '{
"name": "My-Manual-Nodes",
"type": "manual",
"source": "local",
"url": "",
"enabled": true,
"content": "trojan://password@1.1.1.1:443#%E6%97%A5%E6%9C%AC-01\nhysteria2://user:pass@3.3.3.3:443/?sni=example.com#%E7%BE%8E%E5%9B%BD-01"
}'content\nSUB_NAME="My-Manual-Nodes"
NEW_NODE="hysteria2://user:pass@4.4.4.4:443/?sni=example.com#%E6%96%B0%E8%8A%82%E7%82%B9-02"
curl -s "${SUB_STORE_URL}/api/sub/${SUB_NAME}" | jq --arg n "$NEW_NODE" '
.content = (if .content and (.content | length > 0) then (.content + "\n" + $n) else $n end)
' | curl -s -X POST "${SUB_STORE_URL}/api/subs" \
-H "Content-Type: application/json" \
--data-binary @-curl -s -X POST "${SUB_STORE_URL}/api/subs" \
-H "Content-Type: application/json" \
-d '{
"name": "Airport-Main",
"type": "subscription",
"source": "remote",
"url": "https://airport.example.com/sub?token=<SECRET>",
"enabled": true
}'[!IMPORTANT]
- Unified storage directory: All ad hoc JavaScript processing scripts and temporary Python helper scripts must be stored under
; never create files indiscriminately in the main workspace, and keep the project environment clean./tmp/sub-store-copilot/- File naming format:
./tmp/sub-store-copilot/<capability-name>.js- Self-explanatory naming principle:
must have clear, self-explanatory semantics that accurately reflect the script’s specific capability, such as<capability-name>,optimize-udp-tfo.js,clean-ads-expire.js, orinject-singbox-tags.js. Meaningless names such asrename-region-flags.js,test.js, andtemp.jsare strictly prohibited.1.js- Standard workflow: Author and validate the script locally at
→ upload it in binary form to Sub-Store’s virtual storage pool at/tmp/sub-store-copilot/<capability-name>.jsusing/api/file/<capability-name>.js→ reference it in the operator pipeline.curl --data-binary
operator(proxies, targetPlatform, context)function operator(proxies = [], targetPlatform) {
return proxies.map(p => {
p.udp = true;
p.tfo = true;
if (p.port === 443 || p.tls === true) p["skip-cert-verify"] = true;
if (p.type === "hysteria2") p.alpn = p.alpn || ["h3"];
return p;
});
}# 1. 创建规范临时目录并写入自解释命名的处理脚本
mkdir -p /tmp/sub-store-copilot
cat << 'EOF' > /tmp/sub-store-copilot/optimize-udp-tfo.js
function operator(proxies = [], targetPlatform) {
return proxies.map(p => {
p.udp = true;
p.tfo = true;
if (p.port === 443 || p.tls === true) p["skip-cert-verify"] = true;
if (p.type === "hysteria2") p.alpn = p.alpn || ["h3"];
return p;
});
}
EOF
# 2. 二进制上传推送至虚拟文件存储
curl -s -X POST "${SUB_STORE_URL}/api/file/optimize-udp-tfo.js" \
-H "Content-Type: text/javascript" \
--data-binary @/tmp/sub-store-copilot/optimize-udp-tfo.jscurl -s -X POST "${SUB_STORE_URL}/api/file/optimize-udp-tfo.js" \
-H "Content-Type: text/javascript" \
--data-binary @- << 'EOF'
function operator(proxies = [], targetPlatform) {
return proxies.map(p => {
p.udp = true;
p.tfo = true;
return p;
});
}
EOF[!CAUTION] Never submit a single-operator array blindly: When appending or adjusting an operator in an existing collection, directly POSTing aarray containing only one new operator will completely overwrite and erase every other existing operator—such as Quick Setting, Flag, Regex, and Sort—as well as subscription associations. The operator must always be incrementally appended or precisely patched onto the existingprocessarray.process
curl -s -X POST "${SUB_STORE_URL}/api/collections" \
-H "Content-Type: application/json" \
-d '{
"name": "Clean-Nodes",
"type": "collection",
"subscriptions": ["Airport-Main", "My-Manual-Nodes"],
"process": [
{ "type": "Script Operator", "args": { "mode": "link", "content": "/api/file/optimize-udp-tfo.js", "arguments": {} } },
{ "type": "Regex Filter", "args": { "pattern": "(?i)官网|到期|剩余|通知", "action": "drop", "keep": false } },
{ "type": "Flag", "args": { "mode": "add" } },
{ "type": "Sort", "args": { "field": "name", "order": "asc" } },
{ "type": "Deduplicate", "args": { "field": "name" } }
]
}'processCOLL_NAME="Clean-Nodes"
NEW_OP='{"type": "Script Operator", "args": {"mode": "link", "content": "/api/file/optimize-udp-tfo.js", "arguments": {}}}'
curl -s "${SUB_STORE_URL}/api/collection/${COLL_NAME}" | jq --argjson op "$NEW_OP" '
.process = (.process // []) + [$op]
' | curl -s -X POST "${SUB_STORE_URL}/api/collections" \
-H "Content-Type: application/json" \
--data-binary @-# 1. 预览 Mihomo (Clash.Meta) YAML 纯节点列表(适合 proxy-provider 对接)
curl -s "${SUB_STORE_URL}/download/collection/Clean-Nodes?target=clash-meta" | head -n 30
# 2. 预览 Sing-Box JSON outbounds
curl -s "${SUB_STORE_URL}/download/collection/Clean-Nodes?target=singbox" | head -n 30
# 3. 预览 Surge [Proxy] 节点清单
curl -s "${SUB_STORE_URL}/download/collection/Clean-Nodes?target=surge" | head -n 30
# 4. 获取通用原始 URI / Base64 列表
curl -s "${SUB_STORE_URL}/download/collection/Clean-Nodes"proxy-providers:
Clean-Nodes-Provider:
type: http
url: "${SUB_STORE_URL}/download/collection/Clean-Nodes?target=clash-meta"
path: ./providers/clean-nodes.yaml
interval: 3600
health-check:
enable: true
url: http://www.gstatic.com/generate_204
interval: 300curl -s "${SUB_STORE_URL}/api/settings" | jq .curl -s "${SUB_STORE_URL}/api/settings" | jq '
.proxy = "http://127.0.0.1:7890" |
.timeout = 15000 |
.concurrency = 5 |
.concurrencyWaitTime = 200
' | curl -s -X POST "${SUB_STORE_URL}/api/settings" \
-H "Content-Type: application/json" \
--data-binary @-# 调用备份端点立即触发全量配置上传(支持 GET 或 POST)
curl -s -X POST "${SUB_STORE_URL}/api/sync/artifacts"mkdir -p /tmp/sub-store-copilot
HTTP_CODE=$(curl -s -o /tmp/sub-store-copilot/sync.log -w "%{http_code}" --max-time 15 "${SUB_STORE_URL}/api/sync/artifacts")
if [ "$HTTP_CODE" = "200" ]; then
echo "Gist 备份同步成功"
else
echo "Gist 备份同步失败 (HTTP $HTTP_CODE):"
cat /tmp/sub-store-copilot/sync.log
fiimport os, urllib.request
url = f"{os.environ.get('SUB_STORE_PREFIX_URL', 'http://127.0.0.1:3000').rstrip('/')}/api/sync/artifacts"
try:
with urllib.request.urlopen(urllib.request.Request(url, method="POST"), timeout=15) as r:
print(f"备份同步完成 [{r.status}]")
except Exception as e:
print(f"备份同步异常: {e}")/<SECRET_PATH>/api/sync/artifactsgist<GITHUB_TOKEN><GIST_ID>Regex Filterargs.patternsubscriptionsCOLL_NAME="Clean-Nodes"
curl -s "${SUB_STORE_URL}/api/collection/${COLL_NAME}" | jq '
.process = (.process | map(
if .type == "Regex Filter" then
.args.pattern = "(?i)官网|到期|剩余|通知|重置|活动|公告"
else . end
))
' | curl -s -X POST "${SUB_STORE_URL}/api/collections" \
-H "Content-Type: application/json" \
--data-binary @-mkdir -p /tmp/sub-store-copilot
cat << 'EOF' > /tmp/sub-store-copilot/fix-sni-domain.js
function operator(proxies = []) {
return proxies.map(p => {
if (p.name && p.name.includes("特殊")) p.sni = "custom.sni.com";
return p;
});
}
EOF
curl -s -X POST "${SUB_STORE_URL}/api/file/fix-sni-domain.js" \
-H "Content-Type: text/javascript" \
--data-binary @/tmp/sub-store-copilot/fix-sni-domain.jsScript OperatorprocessNEW_OP='{"type": "Script Operator", "args": {"mode": "link", "content": "/api/file/fix-sni-domain.js", "arguments": {}}}'
curl -s "${SUB_STORE_URL}/api/collection/Clean-Nodes" | jq --argjson op "$NEW_OP" '
.process = [$op] + (.process // [])
' | curl -s -X POST "${SUB_STORE_URL}/api/collections" \
-H "Content-Type: application/json" \
--data-binary @-${SUB_STORE_URL}/download/collection/Clean-Nodes?target=clash-metacontentSUB_NAME="My-Manual-Nodes"
NEW_NODE="hysteria2://user:pass@4.4.4.4:443/?sni=new.vps.com#%E6%96%B0%E8%8A%82%E7%82%B9"
curl -s "${SUB_STORE_URL}/api/sub/${SUB_NAME}" | jq --arg n "$NEW_NODE" '
.content = (if .content and (.content | length > 0) then (.content + "\n" + $n) else $n end)
' | curl -s -X POST "${SUB_STORE_URL}/api/subs" \
-H "Content-Type: application/json" \
--data-binary @-/download/collection/<name>?target=clash-metaGET /api/settingsproxytimeoutconcurrencyconcurrencyWaitTimegistTokencurl -s "${SUB_STORE_URL}/api/settings" | jq '
.proxy = "http://127.0.0.1:7890" |
.timeout = 15000 |
.concurrency = 5 |
.concurrencyWaitTime = 200
' | curl -s -X POST "${SUB_STORE_URL}/api/settings" \
-H "Content-Type: application/json" \
--data-binary @-curljq[!TIP]
- Unified storage directory: All temporary Python scripts and JavaScript processing scripts must be stored under
; never create files indiscriminately in the main workspace./tmp/sub-store-copilot/- Self-explanatory naming format: Name files
./tmp/sub-store-copilot/<capability-name>.pymust accurately describe the script’s specific capability, such as<capability-name>,clean-and-dedup-nodes.py, orbatch-inject-pipeline.py. Vague names such asdiff-sync-nodes.pyandtest.pyare strictly prohibited.temp.py- Zero-dependency guarantee: Temporary scripts use only the Python 3 standard library (
,urllib.request,json,os,re, etc.), require nobase64, live inpip install, and are discarded after use./tmp/sub-store-copilot/
import os, json, urllib.request
BASE_URL = os.environ.get("SUB_STORE_PREFIX_URL", "http://127.0.0.1:3000").rstrip("/")
def api(path, method="GET", data=None):
headers = {"Content-Type": "application/json"} if data is not None else {}
body = json.dumps(data).encode("utf-8") if data is not None else None
req = urllib.request.Request(f"{BASE_URL}{path}", data=body, headers=headers, method=method)
with urllib.request.urlopen(req) as resp:
res = resp.read().decode("utf-8")
return json.loads(res) if res and resp.headers.get_content_type() == "application/json" else res| Object Type | Endpoint | Required Core Fields | Key Notes |
|---|---|---|---|
| Self-hosted node | | | Leave |
| Remote subscription | | | Leave |
| Aggregation collection | | | Aggregates multiple subscriptions and executes the |
| Processing script | | Raw JavaScript text | Supports custom cleanup and is mounted in the operator pipeline through the |
typeprocess// 1. 挂载执行自定义 JS 脚本(Remote Link 模式,UI 与后端完全兼容)
{ "type": "Script Operator", "args": { "mode": "link", "content": "/api/file/optimize-udp-tfo.js", "arguments": {} } }
// 2. 正则保留特定地区(如港日新美)
{ "type": "Regex Filter", "args": { "pattern": "(?i)港|hk|日|jp|新|sg|美|us", "action": "keep", "keep": true } }
// 3. 正则剔除垃圾与广告节点
{ "type": "Regex Filter", "args": { "pattern": "(?i)官网|到期|剩余|通知|重置", "action": "drop", "keep": false } }
// 4. 正则替换重命名(统一加前缀)
{ "type": "Rename", "args": { "pattern": "^(.*)$", "replace": "[专线] $1" } }
// 5. 自动补全国旗 Emoji
{ "type": "Flag", "args": { "mode": "add" } }
// 6. 节点按名称升序排序
{ "type": "Sort", "args": { "field": "name", "order": "asc" } }
// 7. 按节点名称去重
{ "type": "Deduplicate", "args": { "field": "name" } }| Operational Scenario | Destructive High-Risk Practice (Strictly Prohibited) | Incremental Safe Operation Standard |
|---|---|---|
| Append a new self-hosted node | Construct and POST JSON containing only the new node, causing all existing nodes to be lost | First GET the existing subscription, append the new link after a line break at the end of the existing |
| Add a processing operator | Submit a | First GET the existing collection, append or prepend the operator to |
| Adjust an operator parameter | Reconstruct and overwrite the collection object, clearing | First GET the existing collection, locate the target operator and modify its corresponding |
Modify global configuration ( | Submit JSON containing only the fields to be modified, causing the existing Gist Token, outbound proxy, and other settings to be overwritten and cleared | Strictly follow read-then-update: first GET the complete settings, incrementally merge the target fields in memory or with jq, and then write back the complete object |
| Author a temporary script | Casually create | Use self-explanatory filenames under |
# 1. 设定目标集合名称并执行一键转换覆写
COLL_NAME="Clean-Nodes"
curl -s "${SUB_STORE_URL}/api/collection/${COLL_NAME}" | jq '
.process = (.process | map(
if .type == "Script Operator" or .type == "Script" then
.type = "Script Operator" |
.args = {
mode: (.args.mode // (if .args.content and (.args.content | contains("\n")) then "script" else "link" end)),
content: (.args.content // .args.url // .args.path // "")
}
elif .type == "Regex Filter" then
.args.action = (.args.action // (if .args.keep == false then "drop" else "keep" end)) |
.args.keep = (if .args.keep != null then .args.keep elif .args.action == "drop" then false else true end)
else . end
))
' | curl -s -X POST "${SUB_STORE_URL}/api/collections" \
-H "Content-Type: application/json" \
--data-binary @-
# 2. 验证修复后的 process 结构
curl -s "${SUB_STORE_URL}/api/collection/${COLL_NAME}" | jq .processcurl -s -X POST "${SUB_STORE_URL}/api/collections" \
-H "Content-Type: application/json" \
-d '{
"name": "Clean-Nodes",
"type": "collection",
"subscriptions": ["Airport-Main", "My-Manual-Nodes"],
"process": [
{
"type": "Script Operator",
"args": {
"mode": "link",
"content": "/api/file/optimize-udp-tfo.js",
"arguments": {}
}
},
{
"type": "Regex Filter",
"args": {
"pattern": "(?i)官网|到期|剩余|通知",
"action": "drop",
"keep": false
}
}
]
}'/tmp/sub-store-copilot/fix-ui-schema-doctor.pyimport os, json, urllib.request
BASE_URL = os.environ.get("SUB_STORE_PREFIX_URL", "http://127.0.0.1:3000").rstrip("/")
def api(path, data=None):
headers = {"Content-Type": "application/json"} if data else {}
body = json.dumps(data).encode("utf-8") if data else None
req = urllib.request.Request(f"{BASE_URL}{path}", data=body, headers=headers, method="POST" if data else "GET")
with urllib.request.urlopen(req) as resp:
res = resp.read().decode("utf-8")
return json.loads(res) if res else {}
collections = api("/api/collections")
print(f"开始巡检 {len(collections)} 个集合...")
for col in collections:
name = col.get("name")
process = col.get("process", [])
changed = False
for op in process:
op_type = op.get("type", "")
args = op.setdefault("args", {})
# 1. 修复 Script Operator / Script 算子
if op_type in ("Script Operator", "Script"):
op["type"] = "Script Operator"
if "mode" not in args or "content" not in args:
content = args.get("content") or args.get("url") or args.get("path") or ""
mode = args.get("mode") or ("script" if "\n" in content else "link")
op["args"] = {"mode": mode, "content": content}
changed = True
# 2. 修复 Regex Filter 算子联动
elif op_type == "Regex Filter":
action = args.get("action")
keep = args.get("keep")
if action is None and keep is not None:
args["action"] = "keep" if keep else "drop"
changed = True
elif keep is None and action is not None:
args["keep"] = (action == "keep")
changed = True
if changed:
api("/api/collections", data=col)
print(f"已修复集合 [{name}] 中的非标准算子并写回 Sub-Store!")
print("全量集合巡检与修复完成。")/api/subs/api/collections/api/file/:name/api/files/download/...targetGET /api/settingsPOST /api/settingsPOST /api/sync/artifactsproxy