fix: TikTok API endpoints and privacy level handling
- Fix publish endpoint: /post/video/init/ → /post/publish/video/init/ - Replace broken /video/list/ with /post/publish/creator_info/query/ - Auto-select valid privacy level from creator options (sandbox fix) - Update tools, manifest, REST routes for creator_info
This commit is contained in:
@@ -82,43 +82,34 @@ export async function getUserProfile(
|
||||
};
|
||||
}
|
||||
|
||||
export async function getUserVideos(
|
||||
args: { max_count?: number; account?: string },
|
||||
export async function getCreatorInfo(
|
||||
args: { account?: string },
|
||||
customer?: Customer
|
||||
): Promise<Array<{
|
||||
id: string;
|
||||
title?: string;
|
||||
video_description?: string;
|
||||
duration: number;
|
||||
cover_image_url?: string;
|
||||
share_url?: string;
|
||||
view_count: number;
|
||||
like_count: number;
|
||||
comment_count: number;
|
||||
share_count: number;
|
||||
create_time: number;
|
||||
}>> {
|
||||
): Promise<{
|
||||
creator_username: string;
|
||||
creator_nickname: string;
|
||||
creator_avatar_url?: string;
|
||||
privacy_level_options: string[];
|
||||
max_video_post_duration_sec: number;
|
||||
comment_disabled: boolean;
|
||||
duet_disabled: boolean;
|
||||
stitch_disabled: boolean;
|
||||
}> {
|
||||
const accessToken = await resolveToken(args, customer);
|
||||
|
||||
const fields = 'id,title,video_description,duration,cover_image_url,share_url,view_count,like_count,comment_count,share_count,create_time';
|
||||
const data = await tiktokRequest('/video/list/', accessToken, 'POST', {
|
||||
max_count: Math.min(args.max_count ?? 10, 20),
|
||||
fields: fields.split(','),
|
||||
});
|
||||
const data = await tiktokRequest('/post/publish/creator_info/query/', accessToken, 'POST', {});
|
||||
const c = data;
|
||||
|
||||
return (data.videos ?? []).map((v: Record<string, unknown>) => ({
|
||||
id: String(v.id ?? ''),
|
||||
title: v.title as string | undefined,
|
||||
video_description: v.video_description as string | undefined,
|
||||
duration: Number(v.duration ?? 0),
|
||||
cover_image_url: v.cover_image_url as string | undefined,
|
||||
share_url: v.share_url as string | undefined,
|
||||
view_count: Number(v.view_count ?? 0),
|
||||
like_count: Number(v.like_count ?? 0),
|
||||
comment_count: Number(v.comment_count ?? 0),
|
||||
share_count: Number(v.share_count ?? 0),
|
||||
create_time: Number(v.create_time ?? 0),
|
||||
}));
|
||||
return {
|
||||
creator_username: c.creator_username ?? '',
|
||||
creator_nickname: c.creator_nickname ?? '',
|
||||
creator_avatar_url: c.creator_avatar_url,
|
||||
privacy_level_options: c.privacy_level_options ?? [],
|
||||
max_video_post_duration_sec: c.max_video_post_duration_sec ?? 0,
|
||||
comment_disabled: c.comment_disabled ?? false,
|
||||
duet_disabled: c.duet_disabled ?? false,
|
||||
stitch_disabled: c.stitch_disabled ?? false,
|
||||
};
|
||||
}
|
||||
|
||||
export async function createVideo(
|
||||
@@ -129,15 +120,21 @@ export async function createVideo(
|
||||
const auditArgs = { title: args.title };
|
||||
const accessToken = await resolveToken(args, customer);
|
||||
|
||||
// Step 1: initialise upload
|
||||
const init = await tiktokRequest('/post/video/init/', accessToken, 'POST', {
|
||||
// Step 1: query creator info to get valid privacy levels (sandbox may not support PUBLIC_TO_EVERYONE)
|
||||
const creatorInfo = await getCreatorInfo(args, customer);
|
||||
const privacyLevel = creatorInfo.privacy_level_options.includes('PUBLIC_TO_EVERYONE')
|
||||
? 'PUBLIC_TO_EVERYONE'
|
||||
: creatorInfo.privacy_level_options[0] ?? 'SELF_ONLY';
|
||||
|
||||
// Step 2: initialise upload
|
||||
const init = await tiktokRequest('/post/publish/video/init/', accessToken, 'POST', {
|
||||
post_info: {
|
||||
title: args.title ?? '',
|
||||
description: args.description ?? '',
|
||||
disable_duet: false,
|
||||
disable_comment: false,
|
||||
disable_stitch: false,
|
||||
privacy_level: 'PUBLIC_TO_EVERYONE',
|
||||
privacy_level: privacyLevel,
|
||||
},
|
||||
source_info: {
|
||||
source: 'PULL_FROM_URL',
|
||||
|
||||
@@ -594,8 +594,8 @@ app.get('/auth/tiktok/callback', async (req, res) => {
|
||||
`Open ID: ${openId || 'not returned'}`,
|
||||
`Scopes: ${scope || 'not returned'}`,
|
||||
`Expires in: ${expiresIn || 'not returned'} seconds`,
|
||||
`Access token captured: ${accessToken ? `${accessToken.slice(0, 8)}...` : 'no'}`,
|
||||
'Next step: use this token with /api/connect/tiktok or set TIKTOK_DEFAULT_ACCESS_TOKEN for server-side publishing.',
|
||||
`Access token: ${accessToken}`,
|
||||
'Copy this token and paste it to your AI assistant to store it for future TikTok API calls.',
|
||||
...(state ? [`State: ${state}`] : []),
|
||||
],
|
||||
})
|
||||
@@ -1350,11 +1350,10 @@ app.get('/api/tiktok/profile', requireAuth, async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
app.get('/api/tiktok/videos', requireAuth, async (req, res) => {
|
||||
const max_count = req.query.max_count ? parseInt(req.query.max_count as string, 10) : undefined;
|
||||
app.get('/api/tiktok/creator-info', requireAuth, async (req, res) => {
|
||||
const account = req.query.account as string | undefined;
|
||||
try {
|
||||
const result = await handleToolCall('tiktok_get_videos', { max_count, account });
|
||||
const result = await handleToolCall('tiktok_get_creator_info', { account });
|
||||
res.json(parseToolResult(result));
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: (err as Error).message });
|
||||
|
||||
@@ -681,15 +681,14 @@ export function getOpenApiSpec(serverUrl: string) {
|
||||
responses: { '200': { description: 'Profile info' } },
|
||||
},
|
||||
},
|
||||
'/api/tiktok/videos': {
|
||||
'/api/tiktok/creator-info': {
|
||||
get: {
|
||||
operationId: 'tiktok_get_videos',
|
||||
summary: 'Get TikTok videos',
|
||||
operationId: 'tiktok_get_creator_info',
|
||||
summary: 'Get TikTok creator info',
|
||||
parameters: [
|
||||
{ name: 'max_count', in: 'query', schema: { type: 'integer' } },
|
||||
{ name: 'account', in: 'query', schema: { type: 'string' } },
|
||||
],
|
||||
responses: { '200': { description: 'Video list' } },
|
||||
responses: { '200': { description: 'Creator publishing info' } },
|
||||
},
|
||||
},
|
||||
'/api/tiktok/video': {
|
||||
@@ -1822,31 +1821,30 @@ export function getManifest(serverUrl: string, authEnabled: boolean) {
|
||||
examples: [{ account: 'default' }],
|
||||
},
|
||||
{
|
||||
name: 'tiktok_get_videos',
|
||||
name: 'tiktok_get_creator_info',
|
||||
category: 'tiktok',
|
||||
description: 'List recent videos from the authenticated TikTok account',
|
||||
when_to_use: 'User wants to see their recent TikTok videos and performance stats.',
|
||||
description: 'Get TikTok creator publishing info including privacy levels and max video duration',
|
||||
when_to_use: 'User wants to check their TikTok publishing capabilities before posting a video.',
|
||||
input_schema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
max_count: { type: 'number', description: 'Max videos to return (default: 10, max: 20)' },
|
||||
account: { type: 'string', description: 'Which TikTok account to use (default: "default")' },
|
||||
},
|
||||
},
|
||||
returns: {
|
||||
type: 'array',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: { type: 'string' },
|
||||
title: { type: 'string' },
|
||||
view_count: { type: 'number' },
|
||||
like_count: { type: 'number' },
|
||||
share_url: { type: 'string' },
|
||||
},
|
||||
type: 'object',
|
||||
properties: {
|
||||
creator_username: { type: 'string' },
|
||||
creator_nickname: { type: 'string' },
|
||||
creator_avatar_url: { type: 'string' },
|
||||
privacy_level_options: { type: 'array', items: { type: 'string' } },
|
||||
max_video_post_duration_sec: { type: 'number' },
|
||||
comment_disabled: { type: 'boolean' },
|
||||
duet_disabled: { type: 'boolean' },
|
||||
stitch_disabled: { type: 'boolean' },
|
||||
},
|
||||
},
|
||||
examples: [{ max_count: 10, account: 'default' }],
|
||||
examples: [{ account: 'default' }],
|
||||
},
|
||||
{
|
||||
name: 'tiktok_create_video',
|
||||
|
||||
12
src/tools.ts
12
src/tools.ts
@@ -9,7 +9,7 @@ import { getMe as getTelegramMe, sendMessage as sendTelegramMessage, sendPhoto a
|
||||
import { getMe as getDiscordMe, getGuilds, getChannels, sendMessage as sendDiscordMessage, getMessages as getDiscordMessages } from './clients/discord.js';
|
||||
import { getProfile as getInstagramProfile, getMedia as getInstagramMedia, createImagePost as createInstagramPost, createReel as createInstagramReel } from './clients/instagram.js';
|
||||
import { searchTweets, getUserProfile, getUserTweets, createTweet, uploadVideoAndTweet } from './clients/twitter.js';
|
||||
import { getUserProfile as getTikTokProfile, getUserVideos, createVideo, getVideoStatus } from './clients/tiktok.js';
|
||||
import { getUserProfile as getTikTokProfile, getCreatorInfo, createVideo, getVideoStatus } from './clients/tiktok.js';
|
||||
import { getMe as getSnapchatMe, createSnap, getAdAccounts } from './clients/snapchat.js';
|
||||
import { getPage, getPosts, createPost as createFacebookPost, createPhotoPost, createVideoPost as createFacebookVideoPost } from './clients/facebook.js';
|
||||
|
||||
@@ -577,13 +577,12 @@ export const tools: Tool[] = [
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'tiktok_get_videos',
|
||||
name: 'tiktok_get_creator_info',
|
||||
description:
|
||||
'List recent videos from the authenticated TikTok account with view, like, comment, and share counts.',
|
||||
'Get TikTok creator publishing info: username, avatar, available privacy levels, and max video duration. Required before publishing.',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
max_count: { type: 'number', description: 'Max videos to return (default: 10, max: 20)' },
|
||||
account: { type: 'string', description: 'Which TikTok account to use (default: "default")' },
|
||||
},
|
||||
},
|
||||
@@ -1018,9 +1017,8 @@ export async function handleToolCall(
|
||||
}, customer);
|
||||
break;
|
||||
|
||||
case 'tiktok_get_videos':
|
||||
result = await getUserVideos({
|
||||
max_count: (args.max_count as number) ?? 10,
|
||||
case 'tiktok_get_creator_info':
|
||||
result = await getCreatorInfo({
|
||||
account: args.account as string | undefined,
|
||||
}, customer);
|
||||
break;
|
||||
|
||||
Reference in New Issue
Block a user