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:
Garfield
2026-05-12 00:53:55 -04:00
parent 30232e3ef8
commit 7796de12bf
4 changed files with 60 additions and 68 deletions

View File

@@ -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',

View File

@@ -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 });

View File

@@ -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',

View File

@@ -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;