@geoly-ai/social-hub-cli 0.3.44 → 0.3.45
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +6 -0
- package/dist/cli-error-channels.test.js +40 -0
- package/dist/cli-error-channels.test.js.map +1 -1
- package/dist/client.d.ts +7 -1
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +19 -2
- package/dist/client.js.map +1 -1
- package/dist/cmd-manifest.json +11 -2
- package/dist/cmd-manifest.test.js +13 -0
- package/dist/cmd-manifest.test.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/output.d.ts +12 -0
- package/dist/output.d.ts.map +1 -1
- package/dist/output.js +42 -0
- package/dist/output.js.map +1 -1
- package/dist/permissions-gates-restrictions.d.ts +10 -0
- package/dist/permissions-gates-restrictions.d.ts.map +1 -0
- package/dist/permissions-gates-restrictions.js +53 -0
- package/dist/permissions-gates-restrictions.js.map +1 -0
- package/dist/permissions-gates-restrictions.test.d.ts +2 -0
- package/dist/permissions-gates-restrictions.test.d.ts.map +1 -0
- package/dist/permissions-gates-restrictions.test.js +37 -0
- package/dist/permissions-gates-restrictions.test.js.map +1 -0
- package/dist/permissions.d.ts.map +1 -1
- package/dist/permissions.js +2 -0
- package/dist/permissions.js.map +1 -1
- package/dist/register-restrictions.d.ts +64 -0
- package/dist/register-restrictions.d.ts.map +1 -0
- package/dist/register-restrictions.js +431 -0
- package/dist/register-restrictions.js.map +1 -0
- package/dist/register-restrictions.test.d.ts +2 -0
- package/dist/register-restrictions.test.d.ts.map +1 -0
- package/dist/register-restrictions.test.js +315 -0
- package/dist/register-restrictions.test.js.map +1 -0
- package/package.json +3 -3
- package/skills/integrity.json +9 -9
- package/skills/manifest.json +1 -1
- package/skills/reddit-voc-volume/services/firecrawl/examples/o1_web_crawler/o1_web_crawler.py +152 -152
- package/skills/social-hub-admin/SKILL.md +33 -0
- package/skills/social-hub-calendar-directives/SKILL.md +14 -1
- package/skills/social-hub-calendar-jobs/SKILL.md +14 -0
- package/skills/social-hub-cli/SKILL.md +2 -0
- package/skills/social-hub-graph-compliance/SKILL.md +42 -0
- package/skills/social-hub-ops-runtime/SKILL.md +44 -0
- package/skills/social-hub-publishing/SKILL.md +12 -0
package/skills/reddit-voc-volume/services/firecrawl/examples/o1_web_crawler/o1_web_crawler.py
CHANGED
|
@@ -1,152 +1,152 @@
|
|
|
1
|
-
import os
|
|
2
|
-
from firecrawl import FirecrawlApp
|
|
3
|
-
import json
|
|
4
|
-
from dotenv import load_dotenv
|
|
5
|
-
from openai import OpenAI
|
|
6
|
-
|
|
7
|
-
# ANSI color codes
|
|
8
|
-
class Colors:
|
|
9
|
-
CYAN = '\033[96m'
|
|
10
|
-
YELLOW = '\033[93m'
|
|
11
|
-
GREEN = '\033[92m'
|
|
12
|
-
RED = '\033[91m'
|
|
13
|
-
MAGENTA = '\033[95m'
|
|
14
|
-
BLUE = '\033[94m'
|
|
15
|
-
RESET = '\033[0m'
|
|
16
|
-
|
|
17
|
-
# Load environment variables
|
|
18
|
-
load_dotenv()
|
|
19
|
-
|
|
20
|
-
# Retrieve API keys from environment variables
|
|
21
|
-
firecrawl_api_key = os.getenv("FIRECRAWL_API_KEY")
|
|
22
|
-
openai_api_key = os.getenv("OPENAI_API_KEY")
|
|
23
|
-
|
|
24
|
-
# Initialize the FirecrawlApp and OpenAI client
|
|
25
|
-
app = FirecrawlApp(api_key=firecrawl_api_key)
|
|
26
|
-
client = OpenAI(api_key=openai_api_key)
|
|
27
|
-
|
|
28
|
-
# Find the page that most likely contains the objective
|
|
29
|
-
def find_relevant_page_via_map(objective, url, app, client):
|
|
30
|
-
try:
|
|
31
|
-
print(f"{Colors.CYAN}Understood. The objective is: {objective}{Colors.RESET}")
|
|
32
|
-
print(f"{Colors.CYAN}Initiating search on the website: {url}{Colors.RESET}")
|
|
33
|
-
|
|
34
|
-
map_prompt = f"""
|
|
35
|
-
The map function generates a list of URLs from a website and it accepts a search parameter. Based on the objective of: {objective}, come up with a 1-2 word search parameter that will help us find the information we need. Only respond with 1-2 words nothing else.
|
|
36
|
-
"""
|
|
37
|
-
|
|
38
|
-
print(f"{Colors.YELLOW}Analyzing objective to determine optimal search parameter...{Colors.RESET}")
|
|
39
|
-
completion = client.chat.completions.create(
|
|
40
|
-
model="o1-preview",
|
|
41
|
-
messages=[
|
|
42
|
-
{
|
|
43
|
-
"role": "user",
|
|
44
|
-
"content": [
|
|
45
|
-
{
|
|
46
|
-
"type": "text",
|
|
47
|
-
"text": map_prompt
|
|
48
|
-
}
|
|
49
|
-
]
|
|
50
|
-
}
|
|
51
|
-
]
|
|
52
|
-
)
|
|
53
|
-
|
|
54
|
-
map_search_parameter = completion.choices[0].message.content
|
|
55
|
-
print(f"{Colors.GREEN}Optimal search parameter identified: {map_search_parameter}{Colors.RESET}")
|
|
56
|
-
|
|
57
|
-
print(f"{Colors.YELLOW}Mapping website using the identified search parameter...{Colors.RESET}")
|
|
58
|
-
map_website = app.map_url(url, params={"search": map_search_parameter})
|
|
59
|
-
print(f"{Colors.GREEN}Website mapping completed successfully.{Colors.RESET}")
|
|
60
|
-
print(f"{Colors.GREEN}Located {len(map_website)} relevant links.{Colors.RESET}")
|
|
61
|
-
return map_website
|
|
62
|
-
except Exception as e:
|
|
63
|
-
print(f"{Colors.RED}Error encountered during relevant page identification: {str(e)}{Colors.RESET}")
|
|
64
|
-
return None
|
|
65
|
-
|
|
66
|
-
# Scrape the top 3 pages and see if the objective is met, if so return in json format else return None
|
|
67
|
-
def find_objective_in_top_pages(map_website, objective, app, client):
|
|
68
|
-
try:
|
|
69
|
-
# Get top 3 links from the map result
|
|
70
|
-
top_links = map_website[:3] if isinstance(map_website, list) else []
|
|
71
|
-
print(f"{Colors.CYAN}Proceeding to analyze top {len(top_links)} links: {top_links}{Colors.RESET}")
|
|
72
|
-
|
|
73
|
-
for link in top_links:
|
|
74
|
-
print(f"{Colors.YELLOW}Initiating scrape of page: {link}{Colors.RESET}")
|
|
75
|
-
# Scrape the page
|
|
76
|
-
scrape_result = app.scrape_url(link, params={'formats': ['markdown']})
|
|
77
|
-
print(f"{Colors.GREEN}Page scraping completed successfully.{Colors.RESET}")
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
# Check if objective is met
|
|
81
|
-
check_prompt = f"""
|
|
82
|
-
Given the following scraped content and objective, determine if the objective is met.
|
|
83
|
-
If it is, extract the relevant information in a simple and concise JSON format. Use only the necessary fields and avoid nested structures if possible.
|
|
84
|
-
If the objective is not met with confidence, respond with 'Objective not met'.
|
|
85
|
-
|
|
86
|
-
Objective: {objective}
|
|
87
|
-
Scraped content: {scrape_result['markdown']}
|
|
88
|
-
|
|
89
|
-
Remember:
|
|
90
|
-
1. Only return JSON if you are confident the objective is fully met.
|
|
91
|
-
2. Keep the JSON structure as simple and flat as possible.
|
|
92
|
-
3. Do not include any explanations or markdown formatting in your response.
|
|
93
|
-
"""
|
|
94
|
-
|
|
95
|
-
completion = client.chat.completions.create(
|
|
96
|
-
model="o1-preview",
|
|
97
|
-
messages=[
|
|
98
|
-
{
|
|
99
|
-
"role": "user",
|
|
100
|
-
"content": [
|
|
101
|
-
{
|
|
102
|
-
"type": "text",
|
|
103
|
-
"text": check_prompt
|
|
104
|
-
}
|
|
105
|
-
]
|
|
106
|
-
}
|
|
107
|
-
]
|
|
108
|
-
)
|
|
109
|
-
|
|
110
|
-
result = completion.choices[0].message.content
|
|
111
|
-
|
|
112
|
-
if result != "Objective not met":
|
|
113
|
-
print(f"{Colors.GREEN}Objective potentially fulfilled. Relevant information identified.{Colors.RESET}")
|
|
114
|
-
try:
|
|
115
|
-
return json.loads(result)
|
|
116
|
-
except json.JSONDecodeError:
|
|
117
|
-
print(f"{Colors.RED}Error in parsing response. Proceeding to next page...{Colors.RESET}")
|
|
118
|
-
else:
|
|
119
|
-
print(f"{Colors.YELLOW}Objective not met on this page. Proceeding to next link...{Colors.RESET}")
|
|
120
|
-
|
|
121
|
-
print(f"{Colors.RED}All available pages analyzed. Objective not fulfilled in examined content.{Colors.RESET}")
|
|
122
|
-
return None
|
|
123
|
-
|
|
124
|
-
except Exception as e:
|
|
125
|
-
print(f"{Colors.RED}Error encountered during page analysis: {str(e)}{Colors.RESET}")
|
|
126
|
-
return None
|
|
127
|
-
|
|
128
|
-
# Main function to execute the process
|
|
129
|
-
def main():
|
|
130
|
-
# Get user input
|
|
131
|
-
url = input(f"{Colors.BLUE}Enter the website to crawl: {Colors.RESET}")
|
|
132
|
-
objective = input(f"{Colors.BLUE}Enter your objective: {Colors.RESET}")
|
|
133
|
-
|
|
134
|
-
print(f"{Colors.YELLOW}Initiating web crawling process...{Colors.RESET}")
|
|
135
|
-
# Find the relevant page
|
|
136
|
-
map_website = find_relevant_page_via_map(objective, url, app, client)
|
|
137
|
-
|
|
138
|
-
if map_website:
|
|
139
|
-
print(f"{Colors.GREEN}Relevant pages identified. Proceeding with detailed analysis...{Colors.RESET}")
|
|
140
|
-
# Find objective in top pages
|
|
141
|
-
result = find_objective_in_top_pages(map_website, objective, app, client)
|
|
142
|
-
|
|
143
|
-
if result:
|
|
144
|
-
print(f"{Colors.GREEN}Objective successfully fulfilled. Extracted information:{Colors.RESET}")
|
|
145
|
-
print(f"{Colors.MAGENTA}{json.dumps(result, indent=2)}{Colors.RESET}")
|
|
146
|
-
else:
|
|
147
|
-
print(f"{Colors.RED}Unable to fulfill the objective with the available content.{Colors.RESET}")
|
|
148
|
-
else:
|
|
149
|
-
print(f"{Colors.RED}No relevant pages identified. Consider refining the search parameters or trying a different website.{Colors.RESET}")
|
|
150
|
-
|
|
151
|
-
if __name__ == "__main__":
|
|
152
|
-
main()
|
|
1
|
+
import os
|
|
2
|
+
from firecrawl import FirecrawlApp
|
|
3
|
+
import json
|
|
4
|
+
from dotenv import load_dotenv
|
|
5
|
+
from openai import OpenAI
|
|
6
|
+
|
|
7
|
+
# ANSI color codes
|
|
8
|
+
class Colors:
|
|
9
|
+
CYAN = '\033[96m'
|
|
10
|
+
YELLOW = '\033[93m'
|
|
11
|
+
GREEN = '\033[92m'
|
|
12
|
+
RED = '\033[91m'
|
|
13
|
+
MAGENTA = '\033[95m'
|
|
14
|
+
BLUE = '\033[94m'
|
|
15
|
+
RESET = '\033[0m'
|
|
16
|
+
|
|
17
|
+
# Load environment variables
|
|
18
|
+
load_dotenv()
|
|
19
|
+
|
|
20
|
+
# Retrieve API keys from environment variables
|
|
21
|
+
firecrawl_api_key = os.getenv("FIRECRAWL_API_KEY")
|
|
22
|
+
openai_api_key = os.getenv("OPENAI_API_KEY")
|
|
23
|
+
|
|
24
|
+
# Initialize the FirecrawlApp and OpenAI client
|
|
25
|
+
app = FirecrawlApp(api_key=firecrawl_api_key)
|
|
26
|
+
client = OpenAI(api_key=openai_api_key)
|
|
27
|
+
|
|
28
|
+
# Find the page that most likely contains the objective
|
|
29
|
+
def find_relevant_page_via_map(objective, url, app, client):
|
|
30
|
+
try:
|
|
31
|
+
print(f"{Colors.CYAN}Understood. The objective is: {objective}{Colors.RESET}")
|
|
32
|
+
print(f"{Colors.CYAN}Initiating search on the website: {url}{Colors.RESET}")
|
|
33
|
+
|
|
34
|
+
map_prompt = f"""
|
|
35
|
+
The map function generates a list of URLs from a website and it accepts a search parameter. Based on the objective of: {objective}, come up with a 1-2 word search parameter that will help us find the information we need. Only respond with 1-2 words nothing else.
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
print(f"{Colors.YELLOW}Analyzing objective to determine optimal search parameter...{Colors.RESET}")
|
|
39
|
+
completion = client.chat.completions.create(
|
|
40
|
+
model="o1-preview",
|
|
41
|
+
messages=[
|
|
42
|
+
{
|
|
43
|
+
"role": "user",
|
|
44
|
+
"content": [
|
|
45
|
+
{
|
|
46
|
+
"type": "text",
|
|
47
|
+
"text": map_prompt
|
|
48
|
+
}
|
|
49
|
+
]
|
|
50
|
+
}
|
|
51
|
+
]
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
map_search_parameter = completion.choices[0].message.content
|
|
55
|
+
print(f"{Colors.GREEN}Optimal search parameter identified: {map_search_parameter}{Colors.RESET}")
|
|
56
|
+
|
|
57
|
+
print(f"{Colors.YELLOW}Mapping website using the identified search parameter...{Colors.RESET}")
|
|
58
|
+
map_website = app.map_url(url, params={"search": map_search_parameter})
|
|
59
|
+
print(f"{Colors.GREEN}Website mapping completed successfully.{Colors.RESET}")
|
|
60
|
+
print(f"{Colors.GREEN}Located {len(map_website)} relevant links.{Colors.RESET}")
|
|
61
|
+
return map_website
|
|
62
|
+
except Exception as e:
|
|
63
|
+
print(f"{Colors.RED}Error encountered during relevant page identification: {str(e)}{Colors.RESET}")
|
|
64
|
+
return None
|
|
65
|
+
|
|
66
|
+
# Scrape the top 3 pages and see if the objective is met, if so return in json format else return None
|
|
67
|
+
def find_objective_in_top_pages(map_website, objective, app, client):
|
|
68
|
+
try:
|
|
69
|
+
# Get top 3 links from the map result
|
|
70
|
+
top_links = map_website[:3] if isinstance(map_website, list) else []
|
|
71
|
+
print(f"{Colors.CYAN}Proceeding to analyze top {len(top_links)} links: {top_links}{Colors.RESET}")
|
|
72
|
+
|
|
73
|
+
for link in top_links:
|
|
74
|
+
print(f"{Colors.YELLOW}Initiating scrape of page: {link}{Colors.RESET}")
|
|
75
|
+
# Scrape the page
|
|
76
|
+
scrape_result = app.scrape_url(link, params={'formats': ['markdown']})
|
|
77
|
+
print(f"{Colors.GREEN}Page scraping completed successfully.{Colors.RESET}")
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
# Check if objective is met
|
|
81
|
+
check_prompt = f"""
|
|
82
|
+
Given the following scraped content and objective, determine if the objective is met.
|
|
83
|
+
If it is, extract the relevant information in a simple and concise JSON format. Use only the necessary fields and avoid nested structures if possible.
|
|
84
|
+
If the objective is not met with confidence, respond with 'Objective not met'.
|
|
85
|
+
|
|
86
|
+
Objective: {objective}
|
|
87
|
+
Scraped content: {scrape_result['markdown']}
|
|
88
|
+
|
|
89
|
+
Remember:
|
|
90
|
+
1. Only return JSON if you are confident the objective is fully met.
|
|
91
|
+
2. Keep the JSON structure as simple and flat as possible.
|
|
92
|
+
3. Do not include any explanations or markdown formatting in your response.
|
|
93
|
+
"""
|
|
94
|
+
|
|
95
|
+
completion = client.chat.completions.create(
|
|
96
|
+
model="o1-preview",
|
|
97
|
+
messages=[
|
|
98
|
+
{
|
|
99
|
+
"role": "user",
|
|
100
|
+
"content": [
|
|
101
|
+
{
|
|
102
|
+
"type": "text",
|
|
103
|
+
"text": check_prompt
|
|
104
|
+
}
|
|
105
|
+
]
|
|
106
|
+
}
|
|
107
|
+
]
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
result = completion.choices[0].message.content
|
|
111
|
+
|
|
112
|
+
if result != "Objective not met":
|
|
113
|
+
print(f"{Colors.GREEN}Objective potentially fulfilled. Relevant information identified.{Colors.RESET}")
|
|
114
|
+
try:
|
|
115
|
+
return json.loads(result)
|
|
116
|
+
except json.JSONDecodeError:
|
|
117
|
+
print(f"{Colors.RED}Error in parsing response. Proceeding to next page...{Colors.RESET}")
|
|
118
|
+
else:
|
|
119
|
+
print(f"{Colors.YELLOW}Objective not met on this page. Proceeding to next link...{Colors.RESET}")
|
|
120
|
+
|
|
121
|
+
print(f"{Colors.RED}All available pages analyzed. Objective not fulfilled in examined content.{Colors.RESET}")
|
|
122
|
+
return None
|
|
123
|
+
|
|
124
|
+
except Exception as e:
|
|
125
|
+
print(f"{Colors.RED}Error encountered during page analysis: {str(e)}{Colors.RESET}")
|
|
126
|
+
return None
|
|
127
|
+
|
|
128
|
+
# Main function to execute the process
|
|
129
|
+
def main():
|
|
130
|
+
# Get user input
|
|
131
|
+
url = input(f"{Colors.BLUE}Enter the website to crawl: {Colors.RESET}")
|
|
132
|
+
objective = input(f"{Colors.BLUE}Enter your objective: {Colors.RESET}")
|
|
133
|
+
|
|
134
|
+
print(f"{Colors.YELLOW}Initiating web crawling process...{Colors.RESET}")
|
|
135
|
+
# Find the relevant page
|
|
136
|
+
map_website = find_relevant_page_via_map(objective, url, app, client)
|
|
137
|
+
|
|
138
|
+
if map_website:
|
|
139
|
+
print(f"{Colors.GREEN}Relevant pages identified. Proceeding with detailed analysis...{Colors.RESET}")
|
|
140
|
+
# Find objective in top pages
|
|
141
|
+
result = find_objective_in_top_pages(map_website, objective, app, client)
|
|
142
|
+
|
|
143
|
+
if result:
|
|
144
|
+
print(f"{Colors.GREEN}Objective successfully fulfilled. Extracted information:{Colors.RESET}")
|
|
145
|
+
print(f"{Colors.MAGENTA}{json.dumps(result, indent=2)}{Colors.RESET}")
|
|
146
|
+
else:
|
|
147
|
+
print(f"{Colors.RED}Unable to fulfill the objective with the available content.{Colors.RESET}")
|
|
148
|
+
else:
|
|
149
|
+
print(f"{Colors.RED}No relevant pages identified. Consider refining the search parameters or trying a different website.{Colors.RESET}")
|
|
150
|
+
|
|
151
|
+
if __name__ == "__main__":
|
|
152
|
+
main()
|
|
@@ -155,6 +155,39 @@ social-hub brand-auth revoke --account <ref> --brand <brandKey> [-t <team-id>] [
|
|
|
155
155
|
|
|
156
156
|
发布前三点 gate 语义见 [`../../docs/brand-mention-gate-operations.md`](../../docs/brand-mention-gate-operations.md)。
|
|
157
157
|
|
|
158
|
+
## 板块限制(restrictions,系统级整板限制)
|
|
159
|
+
|
|
160
|
+
整板 banned / private / quarantined / unavailable 的系统级限制。**一条限制会让全部 team 在该板块的写稿、排期、
|
|
161
|
+
发布同时停下**,所以管理面只给 **admin**,并且只接受**不受限的登录**(`auth login` 的 CLI token 或网页 session):
|
|
162
|
+
API key、绑定 job/account 或带动作白名单的 CLI token **读写都 403**(权限资源 `systemSubredditRestriction`,
|
|
163
|
+
override 只能收紧,manager 拿不到任何一格)。
|
|
164
|
+
|
|
165
|
+
```bash
|
|
166
|
+
# 读(系统级 admin 视图:list / fanout 必须显式 --system;不带 --system 走的是 team 只读路由,见下)
|
|
167
|
+
social-hub restrictions list --system [--state provisional,verification_overdue] [--updated-since <iso>] [--subreddit <name>] [--limit 50] [--offset 0]
|
|
168
|
+
social-hub restrictions get <id> # 含全量证据;list 只回证据摘要 evidenceSummary
|
|
169
|
+
social-hub restrictions fanout <id> --system [--version <n>] [--outcome blocked] [--team-id <uuid>]
|
|
170
|
+
# 全部 team 的扇出汇总计数 + complete + 回执明细。阶段 B(扇出 worker)上线前恒为空、complete:false —— 不是故障。
|
|
171
|
+
|
|
172
|
+
# 写:一律 --dry-run 预览 / --apply 执行
|
|
173
|
+
social-hub restrictions create --subreddit r/<name> --kind banned --evidence-level strong \
|
|
174
|
+
--reason "..." [--state provisional --expires-at <iso>] [--evidence '<json 数组>'] --apply
|
|
175
|
+
# 默认直接 confirmed;provisional 必须带 --expires-at。已有活跃限制 → 409 details.code=RESTRICTION_EXISTS
|
|
176
|
+
social-hub restrictions confirm <id> [--expected-version <n>] --apply # provisional / verification_overdue → confirmed
|
|
177
|
+
social-hub restrictions revoke <id> --reason "..." [--expected-version <n>] --apply # 唯一的解封路径
|
|
178
|
+
# 版本不符 → 409 VERSION_MISMATCH;状态不对 → 409 INVALID_STATE(都在 error.details.code)
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
状态机:`provisional`(已上报待复核,**照样拦**)→ 复核期限过了无人确认 → `verification_overdue`(**继续拦**);
|
|
182
|
+
admin `confirm` → `confirmed`;admin `revoke` → `revoked`(唯一不拦的状态,可再建新限制)。
|
|
183
|
+
revoke **不会**恢复已经被拦成 `blocked` 的 job,需要人重新排期。
|
|
184
|
+
|
|
185
|
+
**运营侧 agent 拉取状态(再去私信 admin)不走这里**:用 team 只读路由
|
|
186
|
+
`restrictions list -t <team-id>` / `restrictions fanout <id> -t <team-id>`(`compliance:read`,manager 可用,
|
|
187
|
+
受限 token 也放行;fanout 只含本 team 的回执),见 `social-hub-graph-compliance`。
|
|
188
|
+
runner 侧的 `restrictions check` / `restrictions report` 同样见 `social-hub-graph-compliance`;
|
|
189
|
+
执行链路上 409 `TARGET_BLOCKED` / `JOB_BLOCKED` 的识别与 agent 停止规则见 `social-hub-ops-runtime`。
|
|
190
|
+
|
|
158
191
|
## Agent teams
|
|
159
192
|
|
|
160
193
|
```bash
|
|
@@ -421,10 +421,23 @@ PATCH { "status": "succeeded", "permalink": "…", "executionToken": "<刚才那
|
|
|
421
421
|
| `execution_token_not_expected` | 初次抢占时带了 token | 代码 bug:抢占不该带 token |
|
|
422
422
|
| `expected_revision_not_applicable` | 终态回写带了 `expectedRevision` | 代码 bug:去掉它 |
|
|
423
423
|
| `revision_stale` / `revision_unknown` | 抢占时的版本钉子对不上 | 同 preflight 同名 reason 的处置 |
|
|
424
|
+
| `target_blocked` | 目标板块/账号受限(见下) | 删 cron、安静退出。**不重试**、不回写终态 |
|
|
424
425
|
|
|
425
|
-
⚠️ 这些都走 **HTTP 409**、`code = "EXECUTION_NOT_ACQUIRED"
|
|
426
|
+
⚠️ 这些都走 **HTTP 409**、`details.reason` 取上表的值;前六个的 `code = "EXECUTION_NOT_ACQUIRED"`。
|
|
426
427
|
**按 reason 编程分流,别拿人类文案做子串匹配。**
|
|
427
428
|
|
|
429
|
+
#### `target_blocked`:板块受限
|
|
430
|
+
|
|
431
|
+
抢 `scheduled → running` 时,若目标板块已被系统级限制(整板 banned / private / quarantined / unavailable,
|
|
432
|
+
含待复核的 provisional 与逾期未复核的 verification_overdue)或该账号在该板块有生效 sanction / bot_bouncer 名单,
|
|
433
|
+
服务端拒绝抢占:HTTP 409,`error.code = "CONFLICT"`,
|
|
434
|
+
`details = { code: "TARGET_BLOCKED", reason: "target_blocked", source, id, kind?, subreddit }`。
|
|
435
|
+
|
|
436
|
+
- 识别看 `details.reason === "target_blocked"`(或 `details.code === "TARGET_BLOCKED"`),不看 message。
|
|
437
|
+
- 你**没有**拿到 token,这一轮什么都没发生:删本机 cron、退出。**不要重试**——只有 admin 撤销限制才会解除。
|
|
438
|
+
- 已经持有 token 的在途执行不受影响:终态回写照常放行(服务端只在抢占那一步判定)。
|
|
439
|
+
- 条目随后会由服务端以 `cancel`(cancel reason `target_blocked`)广播撤销,按常规 cancel 处置即可。
|
|
440
|
+
|
|
428
441
|
## ack 时机(本 source 局部改写通用规则)
|
|
429
442
|
|
|
430
443
|
通用 notifications skill 说"副作用完成后才 ack"——这条仍然成立,但**这个 source 的副作用是
|
|
@@ -43,6 +43,20 @@ social-hub jobs retry -t <team-id> -i <jobId>
|
|
|
43
43
|
social-hub jobs cancel -t <team-id> -i <jobId>
|
|
44
44
|
```
|
|
45
45
|
|
|
46
|
+
## 🔴 板块受限:建/改排期被拒(409 `TARGET_BLOCKED`)
|
|
47
|
+
|
|
48
|
+
目标板块受系统级限制,或该账号在该板块有生效 sanction / bot_bouncer 名单时,
|
|
49
|
+
`jobs create` / `jobs batch` / `jobs retry`(以及建计划/排期、改期、切 delivery mode)会被拒:
|
|
50
|
+
HTTP **409**,`error.code = "CONFLICT"`,`error.details = { code: "TARGET_BLOCKED", reason: "target_blocked",
|
|
51
|
+
source, id, kind?, subreddit }`。
|
|
52
|
+
|
|
53
|
+
- **按 `error.details.code` 识别,不要匹配 message。**
|
|
54
|
+
- **不要重试、不要换个 runAt 再建**:重试只会拿到同一个 409。换一个目标板块,或跳过。解除路径看 `details.source`:`restriction` 只有 admin `restrictions revoke`;`sanction`(团队/账号级)由有权限的人把该 sanction 改为 lifted;`blocklist` 由 admin 维护(不是 agent 的事,报告即可)。
|
|
55
|
+
- 已被拦下的 job 状态是 `blocked`(`lastError` 形如 `target_blocked:<source>:<id>`),是**永久终态**:
|
|
56
|
+
`jobs retry` 只接受 failed / cancelled,解封后要**新建** job。
|
|
57
|
+
- 执行侧(heartbeat / complete / fail / skip)的受限语义与停止规则见 `social-hub-ops-runtime`;
|
|
58
|
+
自查与上报见 `social-hub-graph-compliance` 的 `restrictions check` / `restrictions report`。
|
|
59
|
+
|
|
46
60
|
## OpenClaw / Agent 运行时
|
|
47
61
|
|
|
48
62
|
优先 ops 链路(见 `social-hub-ops-runtime`):
|
|
@@ -46,6 +46,8 @@ social-hub version --json
|
|
|
46
46
|
| 帖子发布前审核/优化(post-review) | `social-hub-post-review` |
|
|
47
47
|
| 事件 / dashboard / audit | `social-hub-events-observability` |
|
|
48
48
|
| 图谱 / 合规 / 风控 | `social-hub-graph-compliance` |
|
|
49
|
+
| 板块限制拉取/自查/上报 restrictions list/fanout/check -t | `social-hub-graph-compliance` |
|
|
50
|
+
| 板块限制管理 restrictions create/revoke/--system | `social-hub-admin` |
|
|
49
51
|
| settings / invites / members / api-keys | `social-hub-admin` |
|
|
50
52
|
| brand-registry / brand-fit / trigger 注册表四眼 / brand-auth | `social-hub-admin` |
|
|
51
53
|
| 飞书身份映射同步(告警 @ 谁) | `social-hub-admin` |
|
|
@@ -73,6 +73,48 @@ social-hub compliance rule-caches-upsert -t <team-id> --sub example -j '{
|
|
|
73
73
|
}'
|
|
74
74
|
```
|
|
75
75
|
|
|
76
|
+
## 板块限制:runner 自查与上报(`restrictions check` / `restrictions report`)
|
|
77
|
+
|
|
78
|
+
整板限制(banned / private / quarantined / unavailable)存在系统级 `subreddit_restrictions`,
|
|
79
|
+
**只有 admin 能直接建 / 确认 / 撤销**(见 `social-hub-admin`)。agent / runner 用下面这些 **team 级**入口:
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
# 拉取限制状态(compliance:read;manager 可用,受限 token 也放行)。Hub 不推送、不发群:
|
|
83
|
+
# agent 按 --updated-since 增量拉取,自行私信 admin。
|
|
84
|
+
social-hub restrictions list -t <team-id> [--state provisional,verification_overdue] [--updated-since <iso>] [--subreddit <name>] [--limit 50] [--offset 0]
|
|
85
|
+
# → {items,total,limit,offset,hasMore};每项带 state / expiresAt / lastVerifiedAt / createdBy / updatedAt / evidenceSummary(不含全量证据)
|
|
86
|
+
social-hub restrictions fanout <restriction-id> -t <team-id> [--version <n>] [--outcome blocked]
|
|
87
|
+
# → 只统计、只返回**本 team** 的回执(counts / receipts);complete 按全局扇出判断;
|
|
88
|
+
# 阶段 B(扇出 worker)上线前恒为空、complete:false —— 不是故障。
|
|
89
|
+
# 缺省 -t 用当前 context 的 team;两者都没有会报错(不会改走 admin 面)。--system 是 admin 专用的系统级视图。
|
|
90
|
+
|
|
91
|
+
# 动手前自查(compliance:read)。不带 --account 只判系统级;带上才同时判该账号的 sanction。
|
|
92
|
+
social-hub restrictions check -t <team-id> --subreddit <name> [--account <accountId>]
|
|
93
|
+
# → {"allowed":true,...} 或 {"allowed":false,"scope":"system|account","source":"restriction|blocklist|sanction",
|
|
94
|
+
# "id":"...","kind":"...","state":"provisional|confirmed|verification_overdue|null","expiresAt":...}
|
|
95
|
+
# allowed:false = 本轮跳过这个目标,不要重试。
|
|
96
|
+
|
|
97
|
+
# 发现受限时上报(compliance:create;写命令,需 --apply,可先 --dry-run)
|
|
98
|
+
social-hub restrictions report -t <team-id> --subreddit <name> --account <accountId> \
|
|
99
|
+
--kind banned --evidence '[{"type":"banned_page","url":"https://www.reddit.com/r/<name>","observedAt":"<iso>"}]' \
|
|
100
|
+
--evidence-level strong --reason "banner: r/<name> has been banned" \
|
|
101
|
+
--expires-at <复核期限 iso> [--scope system] --apply
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
`report` 的落点(第一版刻意保守,**服务端封禁页解析器在阶段 C 才做**,现在信的是你的声明):
|
|
105
|
+
|
|
106
|
+
| 你传的 | 实际写入 | 响应 |
|
|
107
|
+
| -------------------------------------------- | ------------------------------------------------------------------------------------ | ------------------------------------------------------------------------ |
|
|
108
|
+
| `--scope system` + `--evidence-level strong` | 系统级 **provisional**(立即对全部 team 生效,要求 manager/admin;supervisor → 403) | `target:"system"`,`created:false` = 板块已有限制,原样返回现行行摘要 |
|
|
109
|
+
| `--scope system` + `weak` | 降级为该 team+账号的 sanction | `target:"account"`,`downgraded:true`,`downgradeReason:"weak_evidence"` |
|
|
110
|
+
| 缺省 / `--scope account` | 该 team+账号的 sanction(banned → `banned`,其余 → `restricted`) | `target:"account"`,`downgraded:false` |
|
|
111
|
+
|
|
112
|
+
- 证据至少一条、`--expires-at` 必填(复核期限)。provisional 过期**不会**解封,而是转 `verification_overdue` 继续拦,
|
|
113
|
+
只有 admin `restrictions revoke` 才解封。
|
|
114
|
+
- 落 sanction 时**不会**把复核期限写成 sanction 过期(那会自动解封);期限记在 reason 前缀和事件里。
|
|
115
|
+
- 只有「整板对所有人都不可用」才用 `--scope system`。账号自己被 ban(「you've been banned from r/X」)用默认 account。
|
|
116
|
+
- 执行链路上被拦(409 `TARGET_BLOCKED` / `JOB_BLOCKED`)的识别与停止规则见 `social-hub-ops-runtime`。
|
|
117
|
+
|
|
76
118
|
## 系统级板块黑名单(Bot Bouncer 等)
|
|
77
119
|
|
|
78
120
|
与 **per-account** `sanctions-*` 不同:名单为 **系统级**(`/v1/system/subreddit-blocklists`),全 team 共享,表示 Reddit 平台客观事实(如 bot-bouncer 安装 sub),无需 `-t`。
|
|
@@ -70,6 +70,50 @@ social-hub ops skip -t <team-id> --job <jobId> --agent "$AGENT" --reason manual_
|
|
|
70
70
|
|
|
71
71
|
Hub 能保证:client 不能预置/篡改服务端字段、官方 worker executor 只收到冻结内容、complete 校验 hash/generation/pair、未 claim/错 holder/旧 generation 的 complete 被拒。**Hub 挡不住**:拿到 Reddit 会话的 agent 主动忽略 `executionContent`、自行输入别的正文,或用另一 token 重读草稿。真·强制依赖 agent(OpenClaw)遵守本契约——Hub 侧是「冻结 + 校验 + 审计漂移」,不是对平台写动作的物理拦截。
|
|
72
72
|
|
|
73
|
+
## 🔴 板块受限拦截(409 `JOB_BLOCKED` / `TARGET_BLOCKED`)
|
|
74
|
+
|
|
75
|
+
板块被系统级限制(整板 banned / private / quarantined / unavailable)或该账号在该板块有生效
|
|
76
|
+
sanction / bot_bouncer 名单时,Hub **在服务端拦住执行**,并把 job 置为终态 `blocked`
|
|
77
|
+
(`lastError` 形如 `target_blocked:<source>:<id>`)。你会看到的形状(HTTP **409**,AppError 信封):
|
|
78
|
+
|
|
79
|
+
```json
|
|
80
|
+
{
|
|
81
|
+
"error": {
|
|
82
|
+
"code": "CONFLICT",
|
|
83
|
+
"message": "…",
|
|
84
|
+
"details": {
|
|
85
|
+
"code": "JOB_BLOCKED",
|
|
86
|
+
"reason": "target_blocked",
|
|
87
|
+
"source": "restriction",
|
|
88
|
+
"id": "<uuid>",
|
|
89
|
+
"kind": "banned",
|
|
90
|
+
"subreddit": "adhdliving"
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
**识别方式:只看 `error.details.code`(`JOB_BLOCKED` / `TARGET_BLOCKED`)或 `details.reason === "target_blocked"`,
|
|
97
|
+
不要匹配 `message`**(措辞随时会改)。CLI 命中时 stderr 直接输出上面这段 JSON(含 `details`),退出码 1;
|
|
98
|
+
SDK 用 `getSubredditRestrictionBlock(err)`。
|
|
99
|
+
|
|
100
|
+
| 调用 | 受限时的行为 |
|
|
101
|
+
| ------------------------------------ | ------------------------------------------------------------------------------------------------- |
|
|
102
|
+
| `ops claim-next` | **永远不会**返回 blocked job;没有可领的就照旧 `no_claimable_job` |
|
|
103
|
+
| `ops heartbeat` | 409 `JOB_BLOCKED` —— 这一单已被拦 |
|
|
104
|
+
| `ops complete`(job 在排队时就被拦) | 409 `JOB_BLOCKED`(没有合法执行,没什么可回执的) |
|
|
105
|
+
| `ops complete`(领到后才被拦) | 只留一条 blocked 回执 + permalink 存档,**不会**把 job 改成 succeeded、不会标 published、不会重发 |
|
|
106
|
+
| `ops fail` / `ops skip` | 对已 blocked 的 job 是 **200 no-op**(不改状态) |
|
|
107
|
+
|
|
108
|
+
**agent 停止规则(必须遵守):**
|
|
109
|
+
|
|
110
|
+
1. 命中即**本轮跳过这个目标**:停止对该 job 的一切后续动作(不续租、不发帖、不评论)。
|
|
111
|
+
2. **不重试**:重试只会拿到同一个 409。解除路径看 `details.source`:`restriction` 只有 admin `restrictions revoke`;`sanction`(团队/账号级)由有权限的人把该 sanction 改为 lifted;`blocklist` 由 admin 维护(不是 agent 的事)。
|
|
112
|
+
3. **不要再 `fail` / `skip` 重报**(它们是 no-op,只会制造噪音),也不要把它当 Hub 故障告警。
|
|
113
|
+
4. 已被拦的 job 是永久终态:解封后也**不会**自动恢复,需要人重新排期(新建 job)。
|
|
114
|
+
5. 动手前可以先自查:`social-hub restrictions check -t <team-id> --subreddit <name> --account <accountId>`,
|
|
115
|
+
返回 `allowed:false` 就直接跳过(用法见 `social-hub-graph-compliance`)。
|
|
116
|
+
|
|
73
117
|
## `--reason` 枚举(仅允许下列值)
|
|
74
118
|
|
|
75
119
|
`ip_drift`, `env_start_timeout`, `login_expired`, `wrong_account_logged_in`, `reddit_connection_failed`, `rte_failed`, `subreddit_banned`, `bot_bouncer_blocked`, `rule_blocked`, `manual_intervention_required`, `unknown`
|
|
@@ -86,6 +86,18 @@ social-hub plans create -t <team-id> -j '{"brandId":"<uuid>","name":"Week 1","en
|
|
|
86
86
|
social-hub plans cancel -t <team-id> --plan <uuid> # 破坏性:先 list 确认 plan id
|
|
87
87
|
```
|
|
88
88
|
|
|
89
|
+
## 🔴 板块受限(409 `TARGET_BLOCKED`)
|
|
90
|
+
|
|
91
|
+
目标板块受系统级限制(整板 banned / private / quarantined / unavailable,待复核与逾期未复核的同样拦),
|
|
92
|
+
或该账号在该板块有生效 sanction / bot_bouncer 名单时:
|
|
93
|
+
|
|
94
|
+
- `drafts create` / `drafts update` **显式**把 subreddit 设为受限板块 → 409;改投别的板块、清空、读取都放行。
|
|
95
|
+
- `plans create`、建排期 / 改期 / job 创建 → 409。
|
|
96
|
+
- 形状:`error.code = "CONFLICT"`,`error.details = { code: "TARGET_BLOCKED", reason: "target_blocked", source, id, kind?, subreddit }`。
|
|
97
|
+
**按 `error.details.code` 识别,不要匹配 message;不要重试**——换板块或跳过。解除路径看 `details.source`:`restriction` 只有 admin `restrictions revoke`;`sanction`(团队/账号级)由有权限的人把该 sanction 改为 lifted;`blocklist` 由 admin 维护。
|
|
98
|
+
- 事前自查:`social-hub restrictions check -t <team-id> --subreddit <name> [--account <accountId>]`(见 `social-hub-graph-compliance`)。
|
|
99
|
+
- 执行侧(ops heartbeat / complete)的受限语义与停止规则见 `social-hub-ops-runtime`。
|
|
100
|
+
|
|
89
101
|
## 日历与任务
|
|
90
102
|
|
|
91
103
|
调度列表与 patch 见 **`social-hub-calendar-jobs`**(本 skill 不重复展开 jobs 全量命令)。
|