新增通知任务运营MCP:手动标记、失败列表与失败重试

This commit is contained in:
2026-03-03 11:57:27 +08:00
parent 237b528f08
commit 5e0543668e
5 changed files with 216 additions and 4 deletions
+127 -1
View File
@@ -37,6 +37,9 @@ import {
FavoriteSchema,
ReplyNotificationSchema,
GetUnprocessedNotificationsSchema,
MarkNotificationTaskSchema,
ListFailedNotificationTasksSchema,
RetryNotificationTaskSchema,
} from './schemas.js';
import type { SearchFilters } from './types.js';
import type { PlatformPlugin } from '../../server/app.js';
@@ -601,7 +604,7 @@ export const xiaohongshuPlugin: PlatformPlugin = {
);
// =====================================================================
// Notifications (2 tools)
// Notifications (5 tools)
// =====================================================================
// -----------------------------------------------------------------------
@@ -639,6 +642,129 @@ export const xiaohongshuPlugin: PlatformPlugin = {
},
);
// -----------------------------------------------------------------------
// xhs_mark_notification_task
// -----------------------------------------------------------------------
server.tool(
'xhs_mark_notification_task',
'Manually mark a notification task status (new/pending/ignored/replied/failed)',
MarkNotificationTaskSchema,
async (args) => {
return withErrorHandling('xhs_mark_notification_task', async () => {
const store = getNotificationStateStore();
const existing = store.getByFingerprint(args.fingerprint);
if (!existing) {
throw new Error(`Notification task not found: ${args.fingerprint}`);
}
store.setStatus(args.fingerprint, args.status, args.note);
const updated = store.getByFingerprint(args.fingerprint);
return {
content: [{
type: 'text' as const,
text: JSON.stringify({
success: true,
task: updated,
}),
}],
};
});
},
);
// -----------------------------------------------------------------------
// xhs_list_failed_notification_tasks
// -----------------------------------------------------------------------
server.tool(
'xhs_list_failed_notification_tasks',
'List failed notification tasks from local state store for retry/triage',
ListFailedNotificationTasksSchema,
async (args) => {
return withErrorHandling('xhs_list_failed_notification_tasks', async () => {
const tasks = getNotificationStateStore().listByStatuses(['failed'], args.max_count);
return {
content: [{
type: 'text' as const,
text: JSON.stringify({ tasks }),
}],
};
});
},
);
// -----------------------------------------------------------------------
// xhs_retry_notification_task
// -----------------------------------------------------------------------
server.tool(
'xhs_retry_notification_task',
'Retry a failed notification task by fingerprint, optionally overriding reply content',
RetryNotificationTaskSchema,
async (args) => {
return withErrorHandling('xhs_retry_notification_task', async () => {
const store = getNotificationStateStore();
const task = store.getByFingerprint(args.fingerprint);
if (!task) {
throw new Error(`Notification task not found: ${args.fingerprint}`);
}
if (task.status !== 'failed') {
throw new Error(`Task status must be failed to retry, got: ${task.status}`);
}
const replyContent = args.reply_content ?? task.replyContent;
if (!replyContent) {
throw new Error(
'Retry requires reply_content when task has no stored replyContent',
);
}
store.markPending(args.fingerprint);
const timeoutMs =
config.operationTimeouts['reply'] ??
config.operationTimeouts['default'] ??
20_000;
try {
const result = await browser.withPage(
PLATFORM,
async (page) =>
replyNotification(
page,
task.notification.userId,
task.notification.content,
replyContent,
),
timeoutMs,
);
if (result.success) {
store.markReplied(args.fingerprint, replyContent);
} else {
store.markFailed(args.fingerprint, 'Retry reply returned success=false');
}
return {
content: [{
type: 'text' as const,
text: JSON.stringify({
...result,
fingerprint: args.fingerprint,
}),
}],
};
} catch (err) {
const message = err instanceof Error ? err.message : String(err);
store.markFailed(args.fingerprint, message);
throw err;
}
});
},
);
// -----------------------------------------------------------------------
// xhs_reply_notification
// -----------------------------------------------------------------------
@@ -219,6 +219,20 @@ export class NotificationStateStore {
return rows.map((r) => this.rowToTask(r));
}
getByFingerprint(fingerprint: string): NotificationTask | null {
const stmt = this.db.prepare(`
SELECT
fingerprint, user_id, nickname, avatar, content, type, time,
feed_id, xsec_token, note_image, status, first_seen_at, last_seen_at,
retry_count, last_attempt_at, replied_at, reply_content, error_message
FROM notification_tasks
WHERE fingerprint = ?
LIMIT 1
`);
const row = stmt.get(fingerprint) as NotificationRow | undefined;
return row ? this.rowToTask(row) : null;
}
findOpenFingerprint(userId: string, content: string): string | null {
const stmt = this.db.prepare(`
SELECT fingerprint
@@ -281,6 +295,38 @@ export class NotificationStateStore {
stmt.run(reason ?? 'Ignored by operator', fingerprint);
}
setStatus(
fingerprint: string,
status: NotificationTaskStatus,
note?: string,
): void {
const now = Date.now();
const stmt = this.db.prepare(`
UPDATE notification_tasks
SET
status = ?,
last_attempt_at = CASE WHEN ? IN ('pending', 'failed', 'replied') THEN ? ELSE last_attempt_at END,
replied_at = CASE WHEN ? = 'replied' THEN ? ELSE replied_at END,
error_message = CASE WHEN ? = 'failed' THEN COALESCE(?, 'Marked as failed') WHEN ? = 'ignored' THEN COALESCE(?, 'Ignored by operator') ELSE error_message END,
reply_content = CASE WHEN ? = 'replied' THEN COALESCE(?, reply_content) ELSE reply_content END
WHERE fingerprint = ?
`);
stmt.run(
status,
status,
now,
status,
now,
status,
note ?? null,
status,
note ?? null,
status,
note ?? null,
fingerprint,
);
}
private rowToTask(row: NotificationRow): NotificationTask {
return {
fingerprint: row.fingerprint,
+35 -1
View File
@@ -145,7 +145,7 @@ export const LikeSchema = {
/** xhs_list_my_notes — no parameters. */
export const ListMyNotesSchema = {};
// -- Phase 5: Notifications (2 tools) --------------------------------------
// -- Phase 5: Notifications & automation -----------------------------------
/** xhs_get_comment_notifications */
export const GetCommentNotificationsSchema = {
@@ -191,6 +191,40 @@ export const GetUnprocessedNotificationsSchema = {
.describe('Whether to sync latest notifications from Xiaohongshu before querying local state'),
};
/** xhs_mark_notification_task */
export const MarkNotificationTaskSchema = {
fingerprint: z.string().describe('Notification task fingerprint'),
status: z
.enum(['new', 'pending', 'ignored', 'replied', 'failed'])
.describe('Target status for this notification task'),
note: z
.string()
.optional()
.describe('Optional note/reason (used as reply_content for replied, or error_message for failed/ignored)'),
};
/** xhs_list_failed_notification_tasks */
export const ListFailedNotificationTasksSchema = {
max_count: z
.number()
.int()
.min(1)
.max(200)
.optional()
.default(20)
.describe('Maximum number of failed tasks to return (1200, default 20)'),
};
/** xhs_retry_notification_task */
export const RetryNotificationTaskSchema = {
fingerprint: z.string().describe('Notification task fingerprint to retry'),
reply_content: z
.string()
.min(1)
.optional()
.describe('Optional override reply text. If omitted, uses stored reply_content from previous attempt.'),
};
/** xhs_favorite */
export const FavoriteSchema = {
feed_id: z.string().describe('Feed ID to toggle favorite'),