@geoly-ai/social-hub-cli 0.3.11 → 0.3.13
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 +14 -0
- package/dist/cmd-manifest.json +2 -2
- package/dist/content-review.test.d.ts +2 -0
- package/dist/content-review.test.d.ts.map +1 -0
- package/dist/content-review.test.js +222 -0
- package/dist/content-review.test.js.map +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +39 -15
- package/dist/index.js.map +1 -1
- package/dist/lib/content-review-args.d.ts +43 -0
- package/dist/lib/content-review-args.d.ts.map +1 -0
- package/dist/lib/content-review-args.js +77 -0
- package/dist/lib/content-review-args.js.map +1 -0
- package/dist/lib/content-review-args.test.d.ts +2 -0
- package/dist/lib/content-review-args.test.d.ts.map +1 -0
- package/dist/lib/content-review-args.test.js +126 -0
- package/dist/lib/content-review-args.test.js.map +1 -0
- package/dist/permissions-gates-admin.d.ts.map +1 -1
- package/dist/permissions-gates-admin.js +10 -1
- package/dist/permissions-gates-admin.js.map +1 -1
- package/dist/permissions-gates-admin.test.js +11 -0
- package/dist/permissions-gates-admin.test.js.map +1 -1
- package/dist/register-notify.d.ts +0 -12
- package/dist/register-notify.d.ts.map +1 -1
- package/dist/register-notify.js +30 -3
- package/dist/register-notify.js.map +1 -1
- package/dist/register-subreddit-post-style.d.ts +8 -0
- package/dist/register-subreddit-post-style.d.ts.map +1 -0
- package/dist/register-subreddit-post-style.js +161 -0
- package/dist/register-subreddit-post-style.js.map +1 -0
- package/package.json +3 -3
- 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-content-review/SKILL.md +58 -1
- package/skills/social-hub-notifications/SKILL.md +67 -13
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()
|
|
@@ -6,7 +6,7 @@ description: >-
|
|
|
6
6
|
六维评审(情境锚定/突发性/AI 腔/人设声音/合规红线/自我重复)并可选优化重写。
|
|
7
7
|
不用于:相关性判定(该不该评这帖)、发帖正文审查(用 social-hub-post-review)、账号级风控闸门。
|
|
8
8
|
metadata:
|
|
9
|
-
cliVersion: ">=0.
|
|
9
|
+
cliVersion: ">=0.3.13"
|
|
10
10
|
---
|
|
11
11
|
|
|
12
12
|
# social-hub-content-review
|
|
@@ -51,6 +51,61 @@ handle;已选账号时传真实值。AccountBound 的人设
|
|
|
51
51
|
(styleGuide/toneTags/brandFit/avoidTopics)由 Hub 按 accountId 服务端
|
|
52
52
|
enrich,**不要**在请求里传人设内容;`personaId` 传了只做一致性校验。
|
|
53
53
|
|
|
54
|
+
## 作者关系轴(authorRelation)——服务端派生,调用方声明不了
|
|
55
|
+
|
|
56
|
+
`commentType` 表达**结构位置**(`top_level` / `reply`);`authorRelation` 表达
|
|
57
|
+
**说话人是谁**(`post_author` / `other` / `unknown`)。两者正交,都要读。
|
|
58
|
+
|
|
59
|
+
### 权威来源
|
|
60
|
+
|
|
61
|
+
`authorRelation` 与 `authorRelationEvidence` 只出现在**响应**里,由 Hub 按
|
|
62
|
+
「实际发布账号 handle × 所审原帖 permalink × Hub 侧帖子快照作者」派生。
|
|
63
|
+
请求里的 `authorRelationHint` 是**可选的非权威提示**——它不是身份声明的证明,
|
|
64
|
+
不会被回显为审核结论,**证据永远覆盖 hint**(声称 `other` 但证据显示是作者,
|
|
65
|
+
仍派生 `post_author`)。
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
# permalink 是作者关系轴的唯一校验入口;hint 可省略(省略不影响评分标准)
|
|
69
|
+
social-hub content review -t <team-id> \
|
|
70
|
+
--thread-permalink "https://www.reddit.com/r/buildapc/comments/<id>/<slug>/" \
|
|
71
|
+
-j '{ "accountId": "<真实账号>", "draft": "...", "commentType": "top_level",
|
|
72
|
+
"thread": { "subreddit": "buildapc", "postTitle": "..." } }'
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
`--author-relation-hint post_author|other` **通常省略**。它不是让 Hub 采用某种身份的开关:
|
|
76
|
+
省略不影响评分,也不影响 Hub 按证据派生作者关系。只有当上游工作流必须核验
|
|
77
|
+
「本账号应当就是原帖作者」这一预期时才传 `post_author`——它的作用是让**已验证的账号错配
|
|
78
|
+
变成硬失败**(422),而不是争取某种待遇。证据不可得时不会 422,但会按 `unknown` 审核,
|
|
79
|
+
并在服务端记录一条未核验声明。传 `other` 与证据冲突不会硬失败,证据直接覆盖它。
|
|
80
|
+
|
|
81
|
+
### 证据条件(只给 hint 没有用)
|
|
82
|
+
|
|
83
|
+
只有**发布账号、原帖 permalink、Hub 侧该帖快照、以及快照的 subreddit+title 与本次
|
|
84
|
+
thread 绑定**全部一致时,Hub 才可能派生 `post_author` / `verified_snapshot`。
|
|
85
|
+
只传 hint、或只传 permalink 而 Hub 没有该帖快照,都拿不到这个结果。
|
|
86
|
+
不得改用别的帖子、猜测账号或替代 permalink 去凑一个能验证的组合。
|
|
87
|
+
|
|
88
|
+
### 冲突与不可得
|
|
89
|
+
|
|
90
|
+
| 情况 | 结果 |
|
|
91
|
+
| ---------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
92
|
+
| 显式 `authorRelationHint: post_author`,但证据显示不是该帖作者 | **422 `AUTHOR_RELATION_MISMATCH` 硬失败**:LLM 一次都不调用,服务端落安全审计。不是降级,重试与更换 permalink 都不会自愈;核对发布账号与 permalink,或直接去掉 hint 重跑 |
|
|
93
|
+
| 无 permalink / 无快照 / 作者 `[deleted]` / 无账号 / 绑定校验不过 | `unknown` + `unavailable`:**按普通基线审核,不 block、不惩罚** |
|
|
94
|
+
|
|
95
|
+
`unknown` 不是「证据不足所以从严」,它就是改动前的普通基线。
|
|
96
|
+
|
|
97
|
+
### 评分边界(这是校准,不是加分项)
|
|
98
|
+
|
|
99
|
+
`post_author` **不加分**:rubric 只扩展了「哪些具体进展可以充当有效线程锚点」
|
|
100
|
+
(从原帖开始的进展/实验/结果/失败尝试),**权重、阈值、证据强度一律未变**,
|
|
101
|
+
compliance / 真实性 / 具体性 / 重复度要求一律不放宽。
|
|
102
|
+
`Update in case anyone lands here later` 之类开头被识别为**中性 discourse marker**——
|
|
103
|
+
不因它扣 scripted,**也不因它加分**;只评估其后的具体材料,空泛的 update 照样可能
|
|
104
|
+
撞情境锚定硬红线被 `block`。
|
|
105
|
+
|
|
106
|
+
> **agent 行为规则**:不要为了改变评分而添加 hint、permalink 或 update 开头。
|
|
107
|
+
> 只传本次真实线程里已经存在、且可核对的事实。
|
|
108
|
+
|
|
54
109
|
## 两种审核模式
|
|
55
110
|
|
|
56
111
|
| 模式 | 请求 | 结论边界 |
|
|
@@ -154,6 +209,8 @@ social-hub comment-drafts schedule -t <team-id> --draft <uuid> \
|
|
|
154
209
|
- `executionStatus: "degraded"`(LLM 超时/失败) → verdict 不可信,降级层②自审,
|
|
155
210
|
回执标 `[hub-offline-fallback]`;`degradedReason` 说明原因。
|
|
156
211
|
- `score` 为服务端按版本化权重计算的权威综合分;`modelScore` 仅审计对照。
|
|
212
|
+
- `authorRelation` / `authorRelationEvidence` 是**服务端派生**的作者关系(见上一节),
|
|
213
|
+
记录时按响应原值记,**不要记录或回显你传的 `authorRelationHint`**。
|
|
157
214
|
- 情境锚定与合规是一票否决维度;静默期账号阈值自动收紧 +10。
|
|
158
215
|
- AccountBound 出现 `brandFitReviewRequired=true` 或阻断型账号反馈时暂停发布。
|
|
159
216
|
|
|
@@ -114,6 +114,54 @@ social-hub notify subscriptions delete -t <team> --subscription my-mac-agent --a
|
|
|
114
114
|
|
|
115
115
|
`--subscription` 收订阅名或 uuid。**pause 期间 ack 不会成功**,别在 pause 状态下跑消费循环。
|
|
116
116
|
|
|
117
|
+
### 跨团队聚合订阅(一台机器消费多个团队)
|
|
118
|
+
|
|
119
|
+
事件订阅**允许聚合**:同一个 agent 可以同时订阅多个团队,不必只订自己绑定的那个。
|
|
120
|
+
|
|
121
|
+
> 🔴 **聚合发生在客户端,不在服务端。** 每个团队一条订阅、一个游标 ——
|
|
122
|
+
> `seq` 是 **per-team 单调**的,A 团队的 `seq=1` 和 B 团队的 `seq=1` 是**两条完全不同的事件**。
|
|
123
|
+
> 合并去重的键必须是 **`(teamId, seq)`** 或 `eventId`,**绝不能**只用 `seq`。
|
|
124
|
+
> 服务端**没有**、也不会有跨团队的合并流或聚合拉取端点。
|
|
125
|
+
|
|
126
|
+
> 🔴 **订阅名有命名空间分区。** 你**不是**目标团队成员时,订阅名必须是
|
|
127
|
+
> **`xsub/<你的 userId>/<name>`**(`<name>` 只收 `[A-Za-z0-9][A-Za-z0-9._-]*`);
|
|
128
|
+
> 反过来,你**是**该团队成员时名字**不得**以 `xsub/` 开头。
|
|
129
|
+
> 忘了 userId 也不用猜:随便建一次,400 的 `details.requiredPrefix` 会**直接给出**该用的前缀。
|
|
130
|
+
> 想提前拿:`social-hub auth whoami | jq -r .context.userId`。
|
|
131
|
+
|
|
132
|
+
```bash
|
|
133
|
+
# 每个团队各建一条订阅(-t 换成目标 team 即可,不需要该团队给你授权)
|
|
134
|
+
MY_UID=$(social-hub auth whoami | jq -r .context.userId)
|
|
135
|
+
for T in <team-a> <team-b> <team-c>; do
|
|
136
|
+
social-hub notify subscriptions create -t "$T" \
|
|
137
|
+
--name "xsub/$MY_UID/agent-main" --source post-performance-node.v1
|
|
138
|
+
done
|
|
139
|
+
|
|
140
|
+
# 各拉各的;每条流独立 ack 自己的 receipt
|
|
141
|
+
social-hub notify pull -t <team-b> --subscription "xsub/$MY_UID/agent-main" --limit 10 --wait 0
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
跨团队订阅(= 你**不是**该团队成员时)比本团队订阅**多六条约束**,都是有意的:
|
|
145
|
+
|
|
146
|
+
| 约束 | 原因 |
|
|
147
|
+
| ---------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
148
|
+
| **只能管自己的订阅**(owner-only,manager 也不例外) | ack 会**永久推进游标**;否则你能把该团队别人机器上的事件吞掉 |
|
|
149
|
+
| **不接受 `--require-fields`** | 它会反向钉住**目标团队**的 config,让人家收窄自己的暴露面时激活直接 409。create / PATCH / resume / reset 四个入口都判**最终有效值**:存量非空的共享订阅必须先 `PATCH --require-fields ""` 清空才能恢复消费 |
|
|
150
|
+
| **该团队必须已有 active config**(不会替它派生) | 消费不该偷偷在别人团队建出一条新的暴露面政策行;缺了就稳定 409 `SUBSCRIPTION_SOURCE_NOT_CONFIGURED` |
|
|
151
|
+
| **`notify events list` / `notify schedules` 仍拒** | 那是运维诊断面,不随聚合订阅开放;跨团队要拿事件只有 `pull` 一条路 |
|
|
152
|
+
| **名字必须是 `xsub/<你的 userId>/<name>`** | `(teamId, name)` 是**永久**唯一(软删也不释放名字)。不分区的话,你可以把别人团队的 `default` / `agent-main` 永久占掉;不带 userId 段的话,跨团队调用方之间还会互相抢占 |
|
|
153
|
+
| **有订阅配额**(跨团队与本团队**分开计**) | 无配额时「建了再删」可以无限烧掉共享命名空间里的名字。因此配额有两档:`live`(活订阅数)与 `total`(**含软删**的占名数)——⚠️ **删订阅不会归还 `total` 额度** |
|
|
154
|
+
|
|
155
|
+
**字段暴露面由目标团队的 config 决定**(跟你自己的角色无关,也不做按调用方脱敏):
|
|
156
|
+
B 团队勾了什么字段,你就收到什么字段。若 B 的 config 勾了 `restricted` 敏感度字段
|
|
157
|
+
(如 `account.externalRef`、`browserEnvironment.environmentId`),你会收到
|
|
158
|
+
**403 `SUBSCRIPTION_SHARED_SENSITIVITY_FORBIDDEN`**,`details.restrictedFields` 会列出是哪几个
|
|
159
|
+
—— 这时**补权限没用**,正确处置是请该团队收窄它的 `selectedFields`。
|
|
160
|
+
|
|
161
|
+
凭证要求:交互式 `auth login` 拿到的 CLI token 或后台 session 均可;
|
|
162
|
+
**api_key 一律不行**(它按规格钉死在绑定团队),**受限 token 也不行**
|
|
163
|
+
(`--bind-job` / `--bind-account` / 带动作白名单的 token)。
|
|
164
|
+
|
|
117
165
|
## 拉事件
|
|
118
166
|
|
|
119
167
|
```bash
|
|
@@ -324,19 +372,25 @@ reset 会 **bump `receiptEpoch`**,作废全部在途 receipt。**绝不要**
|
|
|
324
372
|
|
|
325
373
|
## 常见故障对照
|
|
326
374
|
|
|
327
|
-
| 现象
|
|
328
|
-
|
|
|
329
|
-
| `--follow` 一直不动、心跳报同一个 `pendingSeq`
|
|
330
|
-
| 游标永远卡在某条
|
|
331
|
-
| ack 一直 409
|
|
332
|
-
| pull 403 且 details 有 `missingPermissions`
|
|
333
|
-
| 某字段突然不见了
|
|
334
|
-
| 一整段时间没有事件
|
|
335
|
-
| 某个帖子一条通知都没收到
|
|
336
|
-
| 老 schedule 该被清理却一直在
|
|
337
|
-
| 建订阅被拒,文案带 `template_disabled`
|
|
338
|
-
| 建订阅被拒,文案带 `template_missing`
|
|
339
|
-
|
|
|
375
|
+
| 现象 | 原因 | 处理 |
|
|
376
|
+
| ---------------------------------------------------- | --------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
377
|
+
| `--follow` 一直不动、心跳报同一个 `pendingSeq` | 你没 ack(多半是未知 schema 被隔离了) | 看 stderr 的 eventId;升级 handler 后重跑 |
|
|
378
|
+
| 游标永远卡在某条 | 把 tombstone 当业务事件处理失败了 | `payloadStatus: "expired"` **必须** ack |
|
|
379
|
+
| ack 一直 409 | 在重放旧 receipt | 重新 pull 拿新 receipt;409 是 CAS 语义,不做幂等成功 |
|
|
380
|
+
| pull 403 且 details 有 `missingPermissions` | 字段权限不足 | **整个订阅**被拒(不做字段级脱敏):减少 `--require-fields` 或补权限 |
|
|
381
|
+
| 某字段突然不见了 | 后台改了 `selectedFields`(`fieldSetVersion` 会变) | 按 envelope 版本分支;**不要**自己补默认值 |
|
|
382
|
+
| 一整段时间没有事件 | source 被 kill switch 关了 | 先 `notify schedules summary`(**不要**从事件流反推:kill switch 的 missed 不产生事件),再 `notify source-configs list`;disabled 期间到点的节点标 missed,**重新启用不补发** |
|
|
383
|
+
| 某个帖子一条通知都没收到 | 主体被删/未发布/无锚点 → schedule 被 cancel | `notify schedules list --subject <snapshotId> --status cancelled`,看 `cancelReason` |
|
|
384
|
+
| 老 schedule 该被清理却一直在 | 保留期 GC cron 没跑 / 追不上 | `notify schedules purge-status`:`hasRunSinceRegistration: false` = **自本次 worker 注册以来**没观测到运行(**不是**「跑了零删除」;刚重启完看到 false 是正常的);连续多轮 `backlog: true` = 真的追不上,调预算/频率。`protectedGroups` 非零是**正常**的 sentinel 保护,别当积压 |
|
|
385
|
+
| 建订阅被拒,文案带 `template_disabled` | 系统级模板做好了但**自动派生总闸没开** | 运维 `notify templates set-auto-derivation --source <id> --enabled true --apply` |
|
|
386
|
+
| 建订阅被拒,文案带 `template_missing` | 该 source 根本没有系统级模板,该 team 也没人工 config | 运维二选一:建模板(`notify templates create/activate` + 开总闸)或人工建 team config |
|
|
387
|
+
| pull 403 `SUBSCRIPTION_SHARED_SENSITIVITY_FORBIDDEN` | 跨团队/桶订阅碰到目标团队 config 里的 `restricted` 字段 | 看 `details.restrictedFields`;**补权限没用**,请该团队把这几个字段从 `selectedFields` 里去掉 |
|
|
388
|
+
| 跨团队 pull 409,文案提 `requiredFields` | 这条共享订阅带着非空 `requiredFields`(多半是早期建的) | PATCH 清空它的 `requiredFields` 后再消费 |
|
|
389
|
+
| 跨团队 `notify events list` / `schedules` 403 | 诊断面**不随**聚合订阅开放(设计如此) | 用 `notify pull` 消费;确需诊断请在该团队补 membership |
|
|
390
|
+
| 建订阅 400 `SUBSCRIPTION_NAME_PREFIX_REQUIRED` | 你不是目标团队成员,名字没落在保留命名空间里 | 用 `details.requiredPrefix` 拼名字重试(`xsub/<你的 userId>/<name>`)。**换普通名字重试没用**——这与 409 `SUBSCRIPTION_NAME_TAKEN`(名字被占,换名即可)是两回事 |
|
|
391
|
+
| 建订阅 400 `SUBSCRIPTION_NAME_PREFIX_RESERVED` | 你**是**本团队成员却用了 `xsub/` 前缀 | 换一个不以 `xsub/` 开头的名字(大小写/全角/前导空格都算命中) |
|
|
392
|
+
| 建订阅 409 `SUBSCRIPTION_QUOTA_EXCEEDED` | 订阅配额满了,看 `details.dimension` / `basis` | `basis=live`:删/复用一条已有订阅。⚠️ `basis=total`:**删订阅不归还额度**(名字永久唯一),只能复用已有订阅或找运维物理清理 / 调高 `NOTIFICATION_*_SUBSCRIPTION_*_LIMIT`。`dimension=target_team` 是**目标团队**总量满了,不是你的问题 |
|
|
393
|
+
| 激活 409 `SOURCE_CONFIG_ACTIVE_STATE_CONFLICT` | 你的 `--expect-active` 与服务端实际 active 不符(别人抢先改了) | 读 `details.actualActiveConfigId` / `actualActiveVersion`,确认对方的改动后再决定是否重试(**无需**再 GET 一次) |
|
|
340
394
|
|
|
341
395
|
## 运维查看面(`notify events` / `notify schedules`)
|
|
342
396
|
|