mirror of
https://github.com/nagisa77/OpenIsle.git
synced 2026-02-07 15:41:02 +08:00
Compare commits
15 Commits
codex/refa
...
codex/fix-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
83bf8c1d5e | ||
|
|
34e206f05d | ||
|
|
dc349923e9 | ||
|
|
0d44c9a823 | ||
|
|
02645af321 | ||
|
|
c3a175f13f | ||
|
|
0821d447f7 | ||
|
|
257794ca00 | ||
|
|
6a527de3eb | ||
|
|
2313f90eb3 | ||
|
|
7fde984e7d | ||
|
|
fc41e605e4 | ||
|
|
042e5fdbe6 | ||
|
|
629442bff6 | ||
|
|
7798910be0 |
29
.github/workflows/coffee-bot.yml
vendored
Normal file
29
.github/workflows/coffee-bot.yml
vendored
Normal file
@@ -0,0 +1,29 @@
|
||||
name: Coffee Bot
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: "0 1 * * *"
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
run-coffee-bot:
|
||||
environment: Bots
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: "20"
|
||||
cache: "npm"
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm install --no-save @openai/agents tsx typescript
|
||||
|
||||
- name: Run coffee bot
|
||||
env:
|
||||
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
|
||||
OPENISLE_TOKEN: ${{ secrets.OPENISLE_TOKEN }}
|
||||
run: npx tsx bots/instance/coffee_bot.ts
|
||||
2
.github/workflows/reply-bots.yml
vendored
2
.github/workflows/reply-bots.yml
vendored
@@ -26,4 +26,4 @@ jobs:
|
||||
env:
|
||||
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
|
||||
OPENISLE_TOKEN: ${{ secrets.OPENISLE_TOKEN }}
|
||||
run: npx tsx bots/reply_bots.ts
|
||||
run: npx tsx bots/instance/reply_bot.ts
|
||||
|
||||
@@ -5,22 +5,19 @@ export type WorkflowInput = { input_as_text: string };
|
||||
export abstract class BotFather {
|
||||
protected readonly allowedMcpTools = [
|
||||
"search",
|
||||
"create_post",
|
||||
"reply_to_post",
|
||||
"reply_to_comment",
|
||||
"recent_posts",
|
||||
"get_post",
|
||||
"list_unread_messages",
|
||||
"mark_notifications_read",
|
||||
"create_post",
|
||||
];
|
||||
|
||||
protected readonly mcp = hostedMcpTool({
|
||||
serverLabel: "openisle_mcp",
|
||||
serverUrl: "https://www.open-isle.com/mcp",
|
||||
allowedTools: this.allowedMcpTools,
|
||||
requireApproval: "never",
|
||||
});
|
||||
protected readonly openisleToken = (process.env.OPENISLE_TOKEN ?? "").trim();
|
||||
|
||||
protected readonly openisleToken = process.env.OPENISLE_TOKEN ?? "";
|
||||
protected readonly mcp = this.createHostedMcpTool();
|
||||
protected readonly agent: Agent;
|
||||
|
||||
constructor(protected readonly name: string) {
|
||||
@@ -32,8 +29,8 @@ export abstract class BotFather {
|
||||
|
||||
console.log(
|
||||
this.openisleToken
|
||||
? "🔑 OPENISLE_TOKEN detected in environment."
|
||||
: "🔓 OPENISLE_TOKEN not set; agent will request it if required."
|
||||
? "🔑 OPENISLE_TOKEN detected in environment; it will be attached to MCP requests."
|
||||
: "🔓 OPENISLE_TOKEN not set; authenticated MCP tools may be unavailable."
|
||||
);
|
||||
|
||||
this.agent = new Agent({
|
||||
@@ -64,13 +61,29 @@ export abstract class BotFather {
|
||||
"You are a helpful assistant for https://www.open-isle.com.",
|
||||
"Finish tasks end-to-end before replying. If multiple MCP tools are needed, call them sequentially until the task is truly done.",
|
||||
"When presenting the result, reply in Chinese with a concise summary and include any important URLs or IDs.",
|
||||
this.openisleToken
|
||||
? `If tools require auth, use this token exactly where the tool schema expects it: ${this.openisleToken}`
|
||||
: "If a tool requires auth, ask me to provide OPENISLE_TOKEN via env.",
|
||||
"After finishing replies, call mark_notifications_read with all processed notification IDs to keep the inbox clean.",
|
||||
];
|
||||
}
|
||||
|
||||
private createHostedMcpTool() {
|
||||
const token = this.openisleToken;
|
||||
const authConfig = token
|
||||
? {
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
}
|
||||
: {};
|
||||
|
||||
return hostedMcpTool({
|
||||
serverLabel: "openisle_mcp",
|
||||
serverUrl: "https://www.open-isle.com/mcp",
|
||||
allowedTools: this.allowedMcpTools,
|
||||
requireApproval: "never",
|
||||
...authConfig,
|
||||
});
|
||||
}
|
||||
|
||||
protected getAdditionalInstructions(): string[] {
|
||||
return [];
|
||||
}
|
||||
|
||||
63
bots/instance/coffee_bot.ts
Normal file
63
bots/instance/coffee_bot.ts
Normal file
@@ -0,0 +1,63 @@
|
||||
import { BotFather, WorkflowInput } from "../bot_father";
|
||||
|
||||
const WEEKDAY_NAMES = ["日", "一", "二", "三", "四", "五", "六"] as const;
|
||||
|
||||
class CoffeeBot extends BotFather {
|
||||
constructor() {
|
||||
super("Coffee Bot");
|
||||
}
|
||||
|
||||
protected override getAdditionalInstructions(): string[] {
|
||||
return [
|
||||
"You are responsible for 发布每日抽奖早安贴。",
|
||||
"创建帖子时,确保标题、奖品信息、开奖时间以及领奖方式完全符合 CLI 查询提供的细节。",
|
||||
"正文需亲切友好,简洁明了,鼓励社区成员互动。",
|
||||
"开奖说明需明确告知中奖者需私聊站长 @nagisa 领取奖励。",
|
||||
"确保只发布一个帖子,避免重复调用 create_post。",
|
||||
];
|
||||
}
|
||||
|
||||
protected override getCliQuery(): string {
|
||||
const now = new Date();
|
||||
const beijingNow = new Date(
|
||||
now.toLocaleString("en-US", { timeZone: "Asia/Shanghai" })
|
||||
);
|
||||
const weekday = WEEKDAY_NAMES[beijingNow.getDay()];
|
||||
|
||||
const drawTime = new Date(beijingNow);
|
||||
drawTime.setHours(15, 0, 0, 0);
|
||||
const drawTimeText = drawTime
|
||||
.toLocaleTimeString("zh-CN", {
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
hour12: false,
|
||||
timeZone: "Asia/Shanghai",
|
||||
})
|
||||
.replace(/^24:/, "00:");
|
||||
|
||||
return `
|
||||
请立即在 https://www.open-isle.com 使用 create_post 发表一篇全新帖子,遵循以下要求:
|
||||
1. 标题固定为「大家星期${weekday}早安--抽一杯咖啡」。
|
||||
2. 正文包含:
|
||||
- 亲切的早安问候;
|
||||
- 明确奖品写作“Coffee x 1”;
|
||||
- 奖品图片链接:https://openisle-1307107697.cos.accelerate.myqcloud.com/dynamic_assert/0d6a9b33e9ca4fe5a90540187d3f9ecb.png;
|
||||
- 公布开奖时间为今天下午 15:00(北京时间,写成 ${drawTimeText});
|
||||
- 标注“领奖请私聊站长 @nagisa”;
|
||||
- 鼓励大家留言互动。
|
||||
3. 帖子语言使用简体中文,格式可用 Markdown,使关键信息醒目。
|
||||
4. 完成后只输出“已发布咖啡抽奖贴”,不额外生成总结。
|
||||
`.trim();
|
||||
}
|
||||
}
|
||||
|
||||
const coffeeBot = new CoffeeBot();
|
||||
|
||||
export const runWorkflow = async (workflow: WorkflowInput) => {
|
||||
return coffeeBot.runWorkflow(workflow);
|
||||
};
|
||||
|
||||
if (require.main === module) {
|
||||
coffeeBot.runCli();
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// reply_bot.ts
|
||||
import { BotFather, WorkflowInput } from "./bot_father";
|
||||
import { BotFather, WorkflowInput } from "../bot_father";
|
||||
|
||||
class ReplyBot extends BotFather {
|
||||
constructor() {
|
||||
@@ -8,8 +8,10 @@ class ReplyBot extends BotFather {
|
||||
|
||||
protected override getAdditionalInstructions(): string[] {
|
||||
return [
|
||||
"You are a helpful and cute assistant for https://www.open-isle.com. Please use plenty of kawaii kaomoji (颜表情), such as (๑˃ᴗ˂)ﻭ, (•̀ω•́)✧, (。•ᴗ-)_♡, (⁎⁍̴̛ᴗ⁍̴̛⁎), etc., in your replies to create a friendly, adorable vibe.",
|
||||
"When presenting the result, reply in Chinese with a concise, cute summary filled with kaomoji and include any important URLs or IDs.",
|
||||
"You are a helpful and cute assistant for https://www.open-isle.com. Keep the lovable tone with plentiful kawaii kaomoji (颜表情) such as (๑˃ᴗ˂)ﻭ, (•̀ω•́)✧, (。•ᴗ-)_♡, (⁎⁍̴̛ᴗ⁍̴̛⁎), etc., while staying professional and informative.",
|
||||
"OpenIsle 是一个由 Spring Boot + Vue 3 打造的开源社区平台,提供注册登录、OAuth 登录(Google/GitHub/Discord/Twitter)、帖子与评论互动、标签分类、草稿、统计分析、通知消息、全局搜索、Markdown 支持、图片上传(默认腾讯云 COS)、浏览器推送、DiceBear 头像等功能,旨在帮助团队快速搭建属于自己的技术社区。",
|
||||
"回复时请主动结合上述站点背景,为用户提供有洞察力、可执行的建议或答案,并在需要时引用官网 https://www.open-isle.com、GitHub 仓库 https://github.com/nagisa77/OpenIsle 或相关文档链接,避免空泛的安慰或套话。",
|
||||
"When presenting the result, reply in Chinese with a concise yet content-rich summary filled with kaomoji,并清晰列出关键结论、操作步骤、重要 URL 或 ID,确保用户能直接采取行动。",
|
||||
];
|
||||
}
|
||||
|
||||
@@ -31,6 +31,7 @@ By default the server listens on port `8085` and serves MCP over Streamable HTTP
|
||||
| Tool | Description |
|
||||
| --- | --- |
|
||||
| `search` | Perform a global search against the OpenIsle backend. |
|
||||
| `create_post` | Publish a new post using a JWT token. |
|
||||
| `reply_to_post` | Create a new comment on a post using a JWT token. |
|
||||
| `reply_to_comment` | Reply to an existing comment using a JWT token. |
|
||||
| `recent_posts` | Retrieve posts created within the last *N* minutes. |
|
||||
|
||||
@@ -192,6 +192,12 @@ class CommentCreateResult(BaseModel):
|
||||
comment: CommentData = Field(description="Comment returned by the backend.")
|
||||
|
||||
|
||||
class PostCreateResult(BaseModel):
|
||||
"""Structured response returned when creating a new post."""
|
||||
|
||||
post: PostDetail = Field(description="Detailed post payload returned by the backend.")
|
||||
|
||||
|
||||
class PostSummary(BaseModel):
|
||||
"""Summary information for a post."""
|
||||
|
||||
|
||||
@@ -173,6 +173,33 @@ class SearchClient:
|
||||
logger.info("Reply to post_id=%s succeeded with id=%s", post_id, body.get("id"))
|
||||
return body
|
||||
|
||||
async def create_post(
|
||||
self,
|
||||
payload: dict[str, Any],
|
||||
*,
|
||||
token: str | None = None,
|
||||
) -> dict[str, Any]:
|
||||
"""Create a new post and return the detailed backend payload."""
|
||||
|
||||
client = self._get_client()
|
||||
resolved_token = self._require_token(token)
|
||||
headers = self._build_headers(token=resolved_token, include_json=True)
|
||||
|
||||
logger.debug(
|
||||
"Creating post with category_id=%s and %d tag(s)",
|
||||
payload.get("categoryId"),
|
||||
len(payload.get("tagIds", []) if isinstance(payload.get("tagIds"), list) else []),
|
||||
)
|
||||
response = await client.post(
|
||||
"/api/posts",
|
||||
json=payload,
|
||||
headers=headers,
|
||||
)
|
||||
response.raise_for_status()
|
||||
body = self._ensure_dict(response.json())
|
||||
logger.info("Post creation succeeded with id=%s", body.get("id"))
|
||||
return body
|
||||
|
||||
async def recent_posts(self, minutes: int) -> list[dict[str, Any]]:
|
||||
"""Return posts created within the given timeframe."""
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@ from .schemas import (
|
||||
NotificationCleanupResult,
|
||||
UnreadNotificationsResponse,
|
||||
PostDetail,
|
||||
PostCreateResult,
|
||||
PostSummary,
|
||||
RecentPostsResponse,
|
||||
SearchResponse,
|
||||
@@ -66,9 +67,9 @@ async def lifespan(_: FastMCP):
|
||||
app = FastMCP(
|
||||
name="openisle-mcp",
|
||||
instructions=(
|
||||
"Use this server to search OpenIsle content, reply to posts and comments with an "
|
||||
"authentication token, retrieve details for a specific post, list posts created "
|
||||
"within a recent time window, and review unread notification messages."
|
||||
"Use this server to search OpenIsle content, create new posts, reply to posts and "
|
||||
"comments with an authentication token, retrieve details for a specific post, list "
|
||||
"posts created within a recent time window, and review unread notification messages."
|
||||
),
|
||||
host=settings.host,
|
||||
port=settings.port,
|
||||
@@ -341,6 +342,343 @@ async def reply_to_comment(
|
||||
return CommentReplyResult(comment=comment)
|
||||
|
||||
|
||||
@app.tool(
|
||||
name="create_post",
|
||||
description="Publish a new post using an authentication token.",
|
||||
structured_output=True,
|
||||
)
|
||||
async def create_post(
|
||||
title: Annotated[
|
||||
str,
|
||||
PydanticField(description="Title of the post to be created."),
|
||||
],
|
||||
content: Annotated[
|
||||
str,
|
||||
PydanticField(description="Markdown content of the post."),
|
||||
],
|
||||
category_id: Annotated[
|
||||
int | None,
|
||||
PydanticField(
|
||||
default=None,
|
||||
ge=1,
|
||||
description="Optional category identifier for the post.",
|
||||
),
|
||||
] = None,
|
||||
tag_ids: Annotated[
|
||||
list[int] | None,
|
||||
PydanticField(
|
||||
default=None,
|
||||
min_length=1,
|
||||
description="Optional list of tag identifiers to assign to the post.",
|
||||
),
|
||||
] = None,
|
||||
post_type: Annotated[
|
||||
str | None,
|
||||
PydanticField(
|
||||
default=None,
|
||||
description="Optional post type value (e.g. LOTTERY, POLL).",
|
||||
),
|
||||
] = None,
|
||||
visible_scope: Annotated[
|
||||
str | None,
|
||||
PydanticField(
|
||||
default=None,
|
||||
description="Optional visibility scope for the post.",
|
||||
),
|
||||
] = None,
|
||||
prize_description: Annotated[
|
||||
str | None,
|
||||
PydanticField(
|
||||
default=None,
|
||||
description="Description of the prize for lottery posts.",
|
||||
),
|
||||
] = None,
|
||||
prize_icon: Annotated[
|
||||
str | None,
|
||||
PydanticField(
|
||||
default=None,
|
||||
description="Icon URL for the lottery prize.",
|
||||
),
|
||||
] = None,
|
||||
prize_count: Annotated[
|
||||
int | None,
|
||||
PydanticField(
|
||||
default=None,
|
||||
ge=1,
|
||||
description="Total number of prizes available for lottery posts.",
|
||||
),
|
||||
] = None,
|
||||
point_cost: Annotated[
|
||||
int | None,
|
||||
PydanticField(
|
||||
default=None,
|
||||
ge=0,
|
||||
description="Point cost required to participate in the post, when applicable.",
|
||||
),
|
||||
] = None,
|
||||
start_time: Annotated[
|
||||
str | None,
|
||||
PydanticField(
|
||||
default=None,
|
||||
description="ISO 8601 start time for lottery or poll posts.",
|
||||
),
|
||||
] = None,
|
||||
end_time: Annotated[
|
||||
str | None,
|
||||
PydanticField(
|
||||
default=None,
|
||||
description="ISO 8601 end time for lottery or poll posts.",
|
||||
),
|
||||
] = None,
|
||||
options: Annotated[
|
||||
list[str] | None,
|
||||
PydanticField(
|
||||
default=None,
|
||||
min_length=1,
|
||||
description="Poll options when creating a poll post.",
|
||||
),
|
||||
] = None,
|
||||
multiple: Annotated[
|
||||
bool | None,
|
||||
PydanticField(
|
||||
default=None,
|
||||
description="Whether the poll allows selecting multiple options.",
|
||||
),
|
||||
] = None,
|
||||
proposed_name: Annotated[
|
||||
str | None,
|
||||
PydanticField(
|
||||
default=None,
|
||||
description="Proposed category name for suggestion posts.",
|
||||
),
|
||||
] = None,
|
||||
proposal_description: Annotated[
|
||||
str | None,
|
||||
PydanticField(
|
||||
default=None,
|
||||
description="Supporting description for the proposed category.",
|
||||
),
|
||||
] = None,
|
||||
captcha: Annotated[
|
||||
str | None,
|
||||
PydanticField(
|
||||
default=None,
|
||||
description="Captcha solution if the backend requires one to create posts.",
|
||||
),
|
||||
] = None,
|
||||
token: Annotated[
|
||||
str | None,
|
||||
PydanticField(
|
||||
default=None,
|
||||
description=(
|
||||
"Optional JWT bearer token. When omitted the configured access token is used."
|
||||
),
|
||||
),
|
||||
] = None,
|
||||
ctx: Context | None = None,
|
||||
) -> PostCreateResult:
|
||||
"""Create a new post in OpenIsle and return the detailed backend payload."""
|
||||
|
||||
sanitized_title = title.strip()
|
||||
if not sanitized_title:
|
||||
raise ValueError("Post title must not be empty.")
|
||||
|
||||
sanitized_content = content.strip()
|
||||
if not sanitized_content:
|
||||
raise ValueError("Post content must not be empty.")
|
||||
|
||||
sanitized_token = token.strip() if isinstance(token, str) else None
|
||||
if sanitized_token == "":
|
||||
sanitized_token = None
|
||||
|
||||
sanitized_category_id: int | None = None
|
||||
if category_id is not None:
|
||||
if isinstance(category_id, bool):
|
||||
raise ValueError("Category identifier must be an integer, not a boolean.")
|
||||
try:
|
||||
sanitized_category_id = int(category_id)
|
||||
except (TypeError, ValueError) as exc:
|
||||
raise ValueError("Category identifier must be an integer.") from exc
|
||||
if sanitized_category_id <= 0:
|
||||
raise ValueError("Category identifier must be a positive integer.")
|
||||
|
||||
sanitized_tag_ids: list[int] | None = None
|
||||
if tag_ids is not None:
|
||||
sanitized_tag_ids = []
|
||||
for value in tag_ids:
|
||||
if isinstance(value, bool):
|
||||
raise ValueError("Tag identifiers must be integers, not booleans.")
|
||||
try:
|
||||
converted = int(value)
|
||||
except (TypeError, ValueError) as exc:
|
||||
raise ValueError("Tag identifiers must be integers.") from exc
|
||||
if converted <= 0:
|
||||
raise ValueError("Tag identifiers must be positive integers.")
|
||||
sanitized_tag_ids.append(converted)
|
||||
if not sanitized_tag_ids:
|
||||
sanitized_tag_ids = None
|
||||
|
||||
sanitized_post_type = post_type.strip() if isinstance(post_type, str) else None
|
||||
if sanitized_post_type == "":
|
||||
sanitized_post_type = None
|
||||
|
||||
sanitized_visible_scope = (
|
||||
visible_scope.strip() if isinstance(visible_scope, str) else None
|
||||
)
|
||||
if sanitized_visible_scope == "":
|
||||
sanitized_visible_scope = None
|
||||
|
||||
sanitized_prize_description = (
|
||||
prize_description.strip() if isinstance(prize_description, str) else None
|
||||
)
|
||||
if sanitized_prize_description == "":
|
||||
sanitized_prize_description = None
|
||||
|
||||
sanitized_prize_icon = prize_icon.strip() if isinstance(prize_icon, str) else None
|
||||
if sanitized_prize_icon == "":
|
||||
sanitized_prize_icon = None
|
||||
|
||||
sanitized_prize_count: int | None = None
|
||||
if prize_count is not None:
|
||||
if isinstance(prize_count, bool):
|
||||
raise ValueError("Prize count must be an integer, not a boolean.")
|
||||
try:
|
||||
sanitized_prize_count = int(prize_count)
|
||||
except (TypeError, ValueError) as exc:
|
||||
raise ValueError("Prize count must be an integer.") from exc
|
||||
if sanitized_prize_count <= 0:
|
||||
raise ValueError("Prize count must be a positive integer.")
|
||||
|
||||
sanitized_point_cost: int | None = None
|
||||
if point_cost is not None:
|
||||
if isinstance(point_cost, bool):
|
||||
raise ValueError("Point cost must be an integer, not a boolean.")
|
||||
try:
|
||||
sanitized_point_cost = int(point_cost)
|
||||
except (TypeError, ValueError) as exc:
|
||||
raise ValueError("Point cost must be an integer.") from exc
|
||||
if sanitized_point_cost < 0:
|
||||
raise ValueError("Point cost cannot be negative.")
|
||||
|
||||
sanitized_start_time = start_time.strip() if isinstance(start_time, str) else None
|
||||
if sanitized_start_time == "":
|
||||
sanitized_start_time = None
|
||||
|
||||
sanitized_end_time = end_time.strip() if isinstance(end_time, str) else None
|
||||
if sanitized_end_time == "":
|
||||
sanitized_end_time = None
|
||||
|
||||
sanitized_options: list[str] | None = None
|
||||
if options is not None:
|
||||
sanitized_options = []
|
||||
for option in options:
|
||||
if option is None:
|
||||
continue
|
||||
stripped_option = option.strip()
|
||||
if stripped_option:
|
||||
sanitized_options.append(stripped_option)
|
||||
if not sanitized_options:
|
||||
sanitized_options = None
|
||||
|
||||
sanitized_multiple = bool(multiple) if isinstance(multiple, bool) else None
|
||||
|
||||
sanitized_proposed_name = (
|
||||
proposed_name.strip() if isinstance(proposed_name, str) else None
|
||||
)
|
||||
if sanitized_proposed_name == "":
|
||||
sanitized_proposed_name = None
|
||||
|
||||
sanitized_proposal_description = (
|
||||
proposal_description.strip() if isinstance(proposal_description, str) else None
|
||||
)
|
||||
if sanitized_proposal_description == "":
|
||||
sanitized_proposal_description = None
|
||||
|
||||
sanitized_captcha = captcha.strip() if isinstance(captcha, str) else None
|
||||
if sanitized_captcha == "":
|
||||
sanitized_captcha = None
|
||||
|
||||
payload: dict[str, object] = {
|
||||
"title": sanitized_title,
|
||||
"content": sanitized_content,
|
||||
}
|
||||
if sanitized_category_id is not None:
|
||||
payload["categoryId"] = sanitized_category_id
|
||||
if sanitized_tag_ids is not None:
|
||||
payload["tagIds"] = sanitized_tag_ids
|
||||
if sanitized_post_type is not None:
|
||||
payload["type"] = sanitized_post_type
|
||||
if sanitized_visible_scope is not None:
|
||||
payload["postVisibleScopeType"] = sanitized_visible_scope
|
||||
if sanitized_prize_description is not None:
|
||||
payload["prizeDescription"] = sanitized_prize_description
|
||||
if sanitized_prize_icon is not None:
|
||||
payload["prizeIcon"] = sanitized_prize_icon
|
||||
if sanitized_prize_count is not None:
|
||||
payload["prizeCount"] = sanitized_prize_count
|
||||
if sanitized_point_cost is not None:
|
||||
payload["pointCost"] = sanitized_point_cost
|
||||
if sanitized_start_time is not None:
|
||||
payload["startTime"] = sanitized_start_time
|
||||
if sanitized_end_time is not None:
|
||||
payload["endTime"] = sanitized_end_time
|
||||
if sanitized_options is not None:
|
||||
payload["options"] = sanitized_options
|
||||
if sanitized_multiple is not None:
|
||||
payload["multiple"] = sanitized_multiple
|
||||
if sanitized_proposed_name is not None:
|
||||
payload["proposedName"] = sanitized_proposed_name
|
||||
if sanitized_proposal_description is not None:
|
||||
payload["proposalDescription"] = sanitized_proposal_description
|
||||
if sanitized_captcha is not None:
|
||||
payload["captcha"] = sanitized_captcha
|
||||
|
||||
try:
|
||||
logger.info("Creating post with title='%s'", sanitized_title)
|
||||
raw_post = await search_client.create_post(payload, token=sanitized_token)
|
||||
except httpx.HTTPStatusError as exc: # pragma: no cover - network errors
|
||||
status_code = exc.response.status_code
|
||||
if status_code == 400:
|
||||
message = (
|
||||
"Post creation failed due to invalid input or captcha verification errors."
|
||||
)
|
||||
elif status_code == 401:
|
||||
message = "Authentication failed while creating the post. Please verify the token."
|
||||
elif status_code == 403:
|
||||
message = "The provided token is not authorized to create posts."
|
||||
else:
|
||||
message = (
|
||||
"OpenIsle backend returned HTTP "
|
||||
f"{status_code} while creating the post."
|
||||
)
|
||||
if ctx is not None:
|
||||
await ctx.error(message)
|
||||
raise ValueError(message) from exc
|
||||
except httpx.RequestError as exc: # pragma: no cover - network errors
|
||||
message = f"Unable to reach OpenIsle backend post service: {exc}."
|
||||
if ctx is not None:
|
||||
await ctx.error(message)
|
||||
raise ValueError(message) from exc
|
||||
|
||||
try:
|
||||
post = PostDetail.model_validate(raw_post)
|
||||
except ValidationError as exc:
|
||||
message = "Received malformed data from the post creation endpoint."
|
||||
if ctx is not None:
|
||||
await ctx.error(message)
|
||||
raise ValueError(message) from exc
|
||||
|
||||
if ctx is not None:
|
||||
await ctx.info(f"Post '{post.title}' created successfully.")
|
||||
logger.debug(
|
||||
"Validated created post payload with id=%s and title='%s'",
|
||||
post.id,
|
||||
post.title,
|
||||
)
|
||||
|
||||
return PostCreateResult(post=post)
|
||||
|
||||
|
||||
@app.tool(
|
||||
name="recent_posts",
|
||||
description="Retrieve posts created in the last N minutes.",
|
||||
|
||||
Reference in New Issue
Block a user