优化运营MCP:新增状态型点赞收藏、评论幂等与通知游标分页
This commit is contained in:
@@ -101,8 +101,8 @@ Add this in `claude_desktop_config.json`:
|
|||||||
| `xhs_publish_video` | Publish a video note |
|
| `xhs_publish_video` | Publish a video note |
|
||||||
| `xhs_post_comment` | Post a comment on a note |
|
| `xhs_post_comment` | Post a comment on a note |
|
||||||
| `xhs_reply_comment` | Reply to a comment |
|
| `xhs_reply_comment` | Reply to a comment |
|
||||||
| `xhs_like` | Toggle like state on a note |
|
| `xhs_set_like_state` | Set like state on a note (idempotent) |
|
||||||
| `xhs_favorite` | Toggle favorite state on a note |
|
| `xhs_set_favorite_state` | Set favorite state on a note (idempotent) |
|
||||||
| `xhs_get_unprocessed_notifications` | Get unprocessed notification tasks from local SQLite state |
|
| `xhs_get_unprocessed_notifications` | Get unprocessed notification tasks from local SQLite state |
|
||||||
| `xhs_mark_notification_task` | Manually mark notification task status (new/pending/ignored/replied/failed) |
|
| `xhs_mark_notification_task` | Manually mark notification task status (new/pending/ignored/replied/failed) |
|
||||||
| `xhs_mark_notification_tasks` | Batch mark notification task statuses |
|
| `xhs_mark_notification_tasks` | Batch mark notification task statuses |
|
||||||
|
|||||||
+2
-2
@@ -101,8 +101,8 @@ pnpm test
|
|||||||
| `xhs_publish_video` | 发布视频笔记 |
|
| `xhs_publish_video` | 发布视频笔记 |
|
||||||
| `xhs_post_comment` | 发表评论 |
|
| `xhs_post_comment` | 发表评论 |
|
||||||
| `xhs_reply_comment` | 回复评论 |
|
| `xhs_reply_comment` | 回复评论 |
|
||||||
| `xhs_like` | 切换点赞状态 |
|
| `xhs_set_like_state` | 设置点赞状态(幂等) |
|
||||||
| `xhs_favorite` | 切换收藏状态 |
|
| `xhs_set_favorite_state` | 设置收藏状态(幂等) |
|
||||||
| `xhs_get_unprocessed_notifications` | 从本地 SQLite 获取“未处理”通知任务 |
|
| `xhs_get_unprocessed_notifications` | 从本地 SQLite 获取“未处理”通知任务 |
|
||||||
| `xhs_mark_notification_task` | 手动标记通知任务状态(new/pending/ignored/replied/failed) |
|
| `xhs_mark_notification_task` | 手动标记通知任务状态(new/pending/ignored/replied/failed) |
|
||||||
| `xhs_mark_notification_tasks` | 批量标记通知任务状态 |
|
| `xhs_mark_notification_tasks` | 批量标记通知任务状态 |
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ import { publishImageNote } from './publish.js';
|
|||||||
import { publishVideoNote } from './publish-video.js';
|
import { publishVideoNote } from './publish-video.js';
|
||||||
import { listMyNotes } from './my-notes.js';
|
import { listMyNotes } from './my-notes.js';
|
||||||
import { postComment, replyComment } from './comment.js';
|
import { postComment, replyComment } from './comment.js';
|
||||||
import { toggleLike, toggleFavorite } from './interaction.js';
|
import { setLikeState, setFavoriteState } from './interaction.js';
|
||||||
import { replyNotification } from './notification.js';
|
import { replyNotification } from './notification.js';
|
||||||
import {
|
import {
|
||||||
getNotificationStateStore,
|
getNotificationStateStore,
|
||||||
@@ -41,8 +41,8 @@ import {
|
|||||||
ListMyNotesSchema,
|
ListMyNotesSchema,
|
||||||
PostCommentSchema,
|
PostCommentSchema,
|
||||||
ReplyCommentSchema,
|
ReplyCommentSchema,
|
||||||
LikeSchema,
|
SetLikeStateSchema,
|
||||||
FavoriteSchema,
|
SetFavoriteStateSchema,
|
||||||
ReplyNotificationSchema,
|
ReplyNotificationSchema,
|
||||||
GetUnprocessedNotificationsSchema,
|
GetUnprocessedNotificationsSchema,
|
||||||
MarkNotificationTaskSchema,
|
MarkNotificationTaskSchema,
|
||||||
@@ -632,18 +632,29 @@ export const xiaohongshuPlugin: PlatformPlugin = {
|
|||||||
PostCommentSchema,
|
PostCommentSchema,
|
||||||
async (args) => {
|
async (args) => {
|
||||||
return withErrorHandling('xhs_post_comment', async () => {
|
return withErrorHandling('xhs_post_comment', async () => {
|
||||||
|
const { data, meta } = await runWithIdempotency(
|
||||||
|
'xhs_post_comment',
|
||||||
|
args.request_id,
|
||||||
|
{
|
||||||
|
feed_id: args.feed_id,
|
||||||
|
xsec_token: args.xsec_token,
|
||||||
|
content: args.content,
|
||||||
|
},
|
||||||
|
async () => {
|
||||||
const timeoutMs =
|
const timeoutMs =
|
||||||
config.operationTimeouts['comment'] ??
|
config.operationTimeouts['comment'] ??
|
||||||
config.operationTimeouts['default'] ??
|
config.operationTimeouts['default'] ??
|
||||||
20_000;
|
20_000;
|
||||||
|
|
||||||
const result = await browser.withPage(
|
return await browser.withPage(
|
||||||
PLATFORM,
|
PLATFORM,
|
||||||
async (page) =>
|
async (page) =>
|
||||||
postComment(page, args.feed_id, args.xsec_token, args.content),
|
postComment(page, args.feed_id, args.xsec_token, args.content),
|
||||||
timeoutMs,
|
timeoutMs,
|
||||||
);
|
);
|
||||||
return ok(result);
|
},
|
||||||
|
);
|
||||||
|
return ok(data, meta);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@@ -695,15 +706,15 @@ export const xiaohongshuPlugin: PlatformPlugin = {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// -----------------------------------------------------------------------
|
// -----------------------------------------------------------------------
|
||||||
// xhs_like
|
// xhs_set_like_state
|
||||||
// -----------------------------------------------------------------------
|
// -----------------------------------------------------------------------
|
||||||
|
|
||||||
server.tool(
|
server.tool(
|
||||||
'xhs_like',
|
'xhs_set_like_state',
|
||||||
'Toggle like on a Xiaohongshu note',
|
'Set like state on a Xiaohongshu note (idempotent)',
|
||||||
LikeSchema,
|
SetLikeStateSchema,
|
||||||
async (args) => {
|
async (args) => {
|
||||||
return withErrorHandling('xhs_like', async () => {
|
return withErrorHandling('xhs_set_like_state', async () => {
|
||||||
const timeoutMs =
|
const timeoutMs =
|
||||||
config.operationTimeouts['like'] ??
|
config.operationTimeouts['like'] ??
|
||||||
config.operationTimeouts['default'] ??
|
config.operationTimeouts['default'] ??
|
||||||
@@ -712,7 +723,7 @@ export const xiaohongshuPlugin: PlatformPlugin = {
|
|||||||
const result = await browser.withPage(
|
const result = await browser.withPage(
|
||||||
PLATFORM,
|
PLATFORM,
|
||||||
async (page) =>
|
async (page) =>
|
||||||
toggleLike(page, args.feed_id, args.xsec_token),
|
setLikeState(page, args.feed_id, args.xsec_token, args.liked),
|
||||||
timeoutMs,
|
timeoutMs,
|
||||||
);
|
);
|
||||||
return ok(result);
|
return ok(result);
|
||||||
@@ -721,15 +732,15 @@ export const xiaohongshuPlugin: PlatformPlugin = {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// -----------------------------------------------------------------------
|
// -----------------------------------------------------------------------
|
||||||
// xhs_favorite
|
// xhs_set_favorite_state
|
||||||
// -----------------------------------------------------------------------
|
// -----------------------------------------------------------------------
|
||||||
|
|
||||||
server.tool(
|
server.tool(
|
||||||
'xhs_favorite',
|
'xhs_set_favorite_state',
|
||||||
'Toggle favorite on a Xiaohongshu note',
|
'Set favorite state on a Xiaohongshu note (idempotent)',
|
||||||
FavoriteSchema,
|
SetFavoriteStateSchema,
|
||||||
async (args) => {
|
async (args) => {
|
||||||
return withErrorHandling('xhs_favorite', async () => {
|
return withErrorHandling('xhs_set_favorite_state', async () => {
|
||||||
const timeoutMs =
|
const timeoutMs =
|
||||||
config.operationTimeouts['favorite'] ??
|
config.operationTimeouts['favorite'] ??
|
||||||
config.operationTimeouts['default'] ??
|
config.operationTimeouts['default'] ??
|
||||||
@@ -738,7 +749,7 @@ export const xiaohongshuPlugin: PlatformPlugin = {
|
|||||||
const result = await browser.withPage(
|
const result = await browser.withPage(
|
||||||
PLATFORM,
|
PLATFORM,
|
||||||
async (page) =>
|
async (page) =>
|
||||||
toggleFavorite(page, args.feed_id, args.xsec_token),
|
setFavoriteState(page, args.feed_id, args.xsec_token, args.favorited),
|
||||||
timeoutMs,
|
timeoutMs,
|
||||||
);
|
);
|
||||||
return ok(result);
|
return ok(result);
|
||||||
@@ -770,8 +781,24 @@ export const xiaohongshuPlugin: PlatformPlugin = {
|
|||||||
? args.statuses
|
? args.statuses
|
||||||
: ['new', 'failed'];
|
: ['new', 'failed'];
|
||||||
|
|
||||||
const tasks = getNotificationStateStore().listByStatuses(statuses, args.max_count);
|
const store = getNotificationStateStore();
|
||||||
return ok(tasks, syncResult ? { synced: syncResult } : undefined);
|
const limit = clampPageSize(args.max_count);
|
||||||
|
const offset = parseCursor(args.cursor);
|
||||||
|
const total = store.countByStatuses(statuses);
|
||||||
|
const tasks = store.listByStatuses(statuses, limit, offset);
|
||||||
|
const nextOffset = offset + tasks.length;
|
||||||
|
const nextCursor = nextOffset < total ? String(nextOffset) : undefined;
|
||||||
|
|
||||||
|
return ok(tasks, {
|
||||||
|
...(syncResult ? { synced: syncResult } : {}),
|
||||||
|
pagination: {
|
||||||
|
cursor: args.cursor ?? '0',
|
||||||
|
max_count: limit,
|
||||||
|
returned: tasks.length,
|
||||||
|
total,
|
||||||
|
...(nextCursor ? { next_cursor: nextCursor } : {}),
|
||||||
|
},
|
||||||
|
});
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@@ -858,8 +885,24 @@ export const xiaohongshuPlugin: PlatformPlugin = {
|
|||||||
ListFailedNotificationTasksSchema,
|
ListFailedNotificationTasksSchema,
|
||||||
async (args) => {
|
async (args) => {
|
||||||
return withErrorHandling('xhs_list_failed_notification_tasks', async () => {
|
return withErrorHandling('xhs_list_failed_notification_tasks', async () => {
|
||||||
const tasks = getNotificationStateStore().listByStatuses(['failed'], args.max_count);
|
const store = getNotificationStateStore();
|
||||||
return ok(tasks);
|
const statuses: NotificationTaskStatus[] = ['failed'];
|
||||||
|
const limit = clampPageSize(args.max_count);
|
||||||
|
const offset = parseCursor(args.cursor);
|
||||||
|
const total = store.countByStatuses(statuses);
|
||||||
|
const tasks = store.listByStatuses(statuses, limit, offset);
|
||||||
|
const nextOffset = offset + tasks.length;
|
||||||
|
const nextCursor = nextOffset < total ? String(nextOffset) : undefined;
|
||||||
|
|
||||||
|
return ok(tasks, {
|
||||||
|
pagination: {
|
||||||
|
cursor: args.cursor ?? '0',
|
||||||
|
max_count: limit,
|
||||||
|
returned: tasks.length,
|
||||||
|
total,
|
||||||
|
...(nextCursor ? { next_cursor: nextCursor } : {}),
|
||||||
|
},
|
||||||
|
});
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -48,6 +48,11 @@ async function readState(page: Page, btnSelector: string, activeHref: string): P
|
|||||||
.catch(() => false);
|
.catch(() => false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function openFeedOverlay(page: Page, feedId: string, xsecToken: string): Promise<void> {
|
||||||
|
await page.goto(buildFeedUrl(feedId, xsecToken), { waitUntil: 'domcontentloaded' });
|
||||||
|
await page.waitForSelector(selDetail.noteContainer, { timeout: 10_000 });
|
||||||
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// toggleLike — pure toggle, clicks the like button once
|
// toggleLike — pure toggle, clicks the like button once
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
@@ -59,8 +64,7 @@ export async function toggleLike(
|
|||||||
): Promise<{ success: boolean; liked: boolean }> {
|
): Promise<{ success: boolean; liked: boolean }> {
|
||||||
log.info({ feedId }, 'Toggling like on note');
|
log.info({ feedId }, 'Toggling like on note');
|
||||||
|
|
||||||
await page.goto(buildFeedUrl(feedId, xsecToken), { waitUntil: 'domcontentloaded' });
|
await openFeedOverlay(page, feedId, xsecToken);
|
||||||
await page.waitForSelector(selDetail.noteContainer, { timeout: 10_000 });
|
|
||||||
|
|
||||||
const clicked = await clickLastMatch(page, '.engage-bar-style .like-wrapper');
|
const clicked = await clickLastMatch(page, '.engage-bar-style .like-wrapper');
|
||||||
if (!clicked) {
|
if (!clicked) {
|
||||||
@@ -86,8 +90,7 @@ export async function toggleFavorite(
|
|||||||
): Promise<{ success: boolean; favorited: boolean }> {
|
): Promise<{ success: boolean; favorited: boolean }> {
|
||||||
log.info({ feedId }, 'Toggling favorite on note');
|
log.info({ feedId }, 'Toggling favorite on note');
|
||||||
|
|
||||||
await page.goto(buildFeedUrl(feedId, xsecToken), { waitUntil: 'domcontentloaded' });
|
await openFeedOverlay(page, feedId, xsecToken);
|
||||||
await page.waitForSelector(selDetail.noteContainer, { timeout: 10_000 });
|
|
||||||
|
|
||||||
const clicked = await clickLastMatch(page, '.engage-bar-style .collect-wrapper');
|
const clicked = await clickLastMatch(page, '.engage-bar-style .collect-wrapper');
|
||||||
if (!clicked) {
|
if (!clicked) {
|
||||||
@@ -101,3 +104,71 @@ export async function toggleFavorite(
|
|||||||
log.info({ feedId, favorited }, 'Favorite toggle complete');
|
log.info({ feedId, favorited }, 'Favorite toggle complete');
|
||||||
return { success: true, favorited };
|
return { success: true, favorited };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// setLikeState / setFavoriteState — idempotent state-setting operations
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
export async function setLikeState(
|
||||||
|
page: Page,
|
||||||
|
feedId: string,
|
||||||
|
xsecToken: string,
|
||||||
|
targetLiked: boolean,
|
||||||
|
): Promise<{ success: boolean; liked: boolean; changed: boolean }> {
|
||||||
|
log.info({ feedId, targetLiked }, 'Setting like state on note');
|
||||||
|
|
||||||
|
await openFeedOverlay(page, feedId, xsecToken);
|
||||||
|
|
||||||
|
const currentLiked = await readState(page, '.engage-bar-style .like-wrapper', '#liked');
|
||||||
|
if (currentLiked === targetLiked) {
|
||||||
|
return { success: true, liked: currentLiked, changed: false };
|
||||||
|
}
|
||||||
|
|
||||||
|
const clicked = await clickLastMatch(page, '.engage-bar-style .like-wrapper');
|
||||||
|
if (!clicked) {
|
||||||
|
log.warn('Like button not found in note detail overlay');
|
||||||
|
return { success: false, liked: currentLiked, changed: false };
|
||||||
|
}
|
||||||
|
|
||||||
|
await page.waitForTimeout(TOGGLE_SETTLE_MS);
|
||||||
|
const liked = await readState(page, '.engage-bar-style .like-wrapper', '#liked');
|
||||||
|
return {
|
||||||
|
success: liked === targetLiked,
|
||||||
|
liked,
|
||||||
|
changed: liked !== currentLiked,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function setFavoriteState(
|
||||||
|
page: Page,
|
||||||
|
feedId: string,
|
||||||
|
xsecToken: string,
|
||||||
|
targetFavorited: boolean,
|
||||||
|
): Promise<{ success: boolean; favorited: boolean; changed: boolean }> {
|
||||||
|
log.info({ feedId, targetFavorited }, 'Setting favorite state on note');
|
||||||
|
|
||||||
|
await openFeedOverlay(page, feedId, xsecToken);
|
||||||
|
|
||||||
|
const currentFavorited = await readState(
|
||||||
|
page,
|
||||||
|
'.engage-bar-style .collect-wrapper',
|
||||||
|
'#collected',
|
||||||
|
);
|
||||||
|
if (currentFavorited === targetFavorited) {
|
||||||
|
return { success: true, favorited: currentFavorited, changed: false };
|
||||||
|
}
|
||||||
|
|
||||||
|
const clicked = await clickLastMatch(page, '.engage-bar-style .collect-wrapper');
|
||||||
|
if (!clicked) {
|
||||||
|
log.warn('Favorite button not found in note detail overlay');
|
||||||
|
return { success: false, favorited: currentFavorited, changed: false };
|
||||||
|
}
|
||||||
|
|
||||||
|
await page.waitForTimeout(TOGGLE_SETTLE_MS);
|
||||||
|
const favorited = await readState(page, '.engage-bar-style .collect-wrapper', '#collected');
|
||||||
|
return {
|
||||||
|
success: favorited === targetFavorited,
|
||||||
|
favorited,
|
||||||
|
changed: favorited !== currentFavorited,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|||||||
@@ -199,6 +199,7 @@ export class NotificationStateStore {
|
|||||||
listByStatuses(
|
listByStatuses(
|
||||||
statuses: NotificationTaskStatus[],
|
statuses: NotificationTaskStatus[],
|
||||||
maxCount: number,
|
maxCount: number,
|
||||||
|
offset = 0,
|
||||||
): NotificationTask[] {
|
): NotificationTask[] {
|
||||||
if (statuses.length === 0) return [];
|
if (statuses.length === 0) return [];
|
||||||
|
|
||||||
@@ -212,13 +213,28 @@ export class NotificationStateStore {
|
|||||||
WHERE status IN (${placeholders})
|
WHERE status IN (${placeholders})
|
||||||
ORDER BY first_seen_at ASC
|
ORDER BY first_seen_at ASC
|
||||||
LIMIT ?
|
LIMIT ?
|
||||||
|
OFFSET ?
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const stmt = this.db.prepare(query);
|
const stmt = this.db.prepare(query);
|
||||||
const rows = stmt.all(...statuses, maxCount) as unknown as NotificationRow[];
|
const rows = stmt.all(...statuses, maxCount, offset) as unknown as NotificationRow[];
|
||||||
return rows.map((r) => this.rowToTask(r));
|
return rows.map((r) => this.rowToTask(r));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
countByStatuses(statuses: NotificationTaskStatus[]): number {
|
||||||
|
if (statuses.length === 0) return 0;
|
||||||
|
|
||||||
|
const placeholders = statuses.map(() => '?').join(', ');
|
||||||
|
const query = `
|
||||||
|
SELECT COUNT(1) AS count
|
||||||
|
FROM notification_tasks
|
||||||
|
WHERE status IN (${placeholders})
|
||||||
|
`;
|
||||||
|
const stmt = this.db.prepare(query);
|
||||||
|
const row = stmt.get(...statuses) as { count?: number } | undefined;
|
||||||
|
return row?.count ?? 0;
|
||||||
|
}
|
||||||
|
|
||||||
getByFingerprint(fingerprint: string): NotificationTask | null {
|
getByFingerprint(fingerprint: string): NotificationTask | null {
|
||||||
const stmt = this.db.prepare(`
|
const stmt = this.db.prepare(`
|
||||||
SELECT
|
SELECT
|
||||||
|
|||||||
@@ -159,6 +159,12 @@ export const PublishVideoSchema = {
|
|||||||
|
|
||||||
/** xhs_post_comment */
|
/** xhs_post_comment */
|
||||||
export const PostCommentSchema = {
|
export const PostCommentSchema = {
|
||||||
|
request_id: z
|
||||||
|
.string()
|
||||||
|
.min(1)
|
||||||
|
.max(128)
|
||||||
|
.optional()
|
||||||
|
.describe('Optional idempotency key for comment request'),
|
||||||
feed_id: z.string().describe('Feed ID to comment on'),
|
feed_id: z.string().describe('Feed ID to comment on'),
|
||||||
xsec_token: z.string().describe('Security token for the feed'),
|
xsec_token: z.string().describe('Security token for the feed'),
|
||||||
content: z.string().min(1).describe('Comment text'),
|
content: z.string().min(1).describe('Comment text'),
|
||||||
@@ -179,10 +185,17 @@ export const ReplyCommentSchema = {
|
|||||||
content: z.string().min(1).describe('Reply text'),
|
content: z.string().min(1).describe('Reply text'),
|
||||||
};
|
};
|
||||||
|
|
||||||
/** xhs_like */
|
/** xhs_set_like_state */
|
||||||
export const LikeSchema = {
|
export const SetLikeStateSchema = {
|
||||||
feed_id: z.string().describe('Feed ID to toggle like'),
|
feed_id: z.string().describe('Feed ID to set like state'),
|
||||||
xsec_token: z.string().describe('Security token for the feed'),
|
xsec_token: z.string().describe('Security token for the feed'),
|
||||||
|
liked: z.boolean().describe('Target like state'),
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Legacy schema used by REST toggle endpoint. */
|
||||||
|
export const LikeSchema = {
|
||||||
|
feed_id: SetLikeStateSchema.feed_id,
|
||||||
|
xsec_token: SetLikeStateSchema.xsec_token,
|
||||||
};
|
};
|
||||||
|
|
||||||
/** xhs_list_my_notes */
|
/** xhs_list_my_notes */
|
||||||
@@ -242,6 +255,10 @@ export const GetUnprocessedNotificationsSchema = {
|
|||||||
.optional()
|
.optional()
|
||||||
.default(20)
|
.default(20)
|
||||||
.describe('Maximum number of unprocessed notifications to return (1–200, default 20)'),
|
.describe('Maximum number of unprocessed notifications to return (1–200, default 20)'),
|
||||||
|
cursor: z
|
||||||
|
.string()
|
||||||
|
.optional()
|
||||||
|
.describe('Pagination cursor returned by previous call'),
|
||||||
statuses: z
|
statuses: z
|
||||||
.array(z.enum(['new', 'pending', 'failed']))
|
.array(z.enum(['new', 'pending', 'failed']))
|
||||||
.optional()
|
.optional()
|
||||||
@@ -275,6 +292,10 @@ export const ListFailedNotificationTasksSchema = {
|
|||||||
.optional()
|
.optional()
|
||||||
.default(20)
|
.default(20)
|
||||||
.describe('Maximum number of failed tasks to return (1–200, default 20)'),
|
.describe('Maximum number of failed tasks to return (1–200, default 20)'),
|
||||||
|
cursor: z
|
||||||
|
.string()
|
||||||
|
.optional()
|
||||||
|
.describe('Pagination cursor returned by previous call'),
|
||||||
};
|
};
|
||||||
|
|
||||||
/** xhs_retry_notification_task */
|
/** xhs_retry_notification_task */
|
||||||
@@ -329,8 +350,15 @@ export const RetryNotificationTasksSchema = {
|
|||||||
.describe('Continue processing remaining tasks after one task fails'),
|
.describe('Continue processing remaining tasks after one task fails'),
|
||||||
};
|
};
|
||||||
|
|
||||||
/** xhs_favorite */
|
/** xhs_set_favorite_state */
|
||||||
export const FavoriteSchema = {
|
export const SetFavoriteStateSchema = {
|
||||||
feed_id: z.string().describe('Feed ID to toggle favorite'),
|
feed_id: z.string().describe('Feed ID to set favorite state'),
|
||||||
xsec_token: z.string().describe('Security token for the feed'),
|
xsec_token: z.string().describe('Security token for the feed'),
|
||||||
|
favorited: z.boolean().describe('Target favorite state'),
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Legacy schema used by REST toggle endpoint. */
|
||||||
|
export const FavoriteSchema = {
|
||||||
|
feed_id: SetFavoriteStateSchema.feed_id,
|
||||||
|
xsec_token: SetFavoriteStateSchema.xsec_token,
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user