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( export async function getCreatorInfo(
args: { max_count?: number; account?: string }, args: { account?: string },
customer?: Customer customer?: Customer
): Promise<Array<{ ): Promise<{
id: string; creator_username: string;
title?: string; creator_nickname: string;
video_description?: string; creator_avatar_url?: string;
duration: number; privacy_level_options: string[];
cover_image_url?: string; max_video_post_duration_sec: number;
share_url?: string; comment_disabled: boolean;
view_count: number; duet_disabled: boolean;
like_count: number; stitch_disabled: boolean;
comment_count: number; }> {
share_count: number;
create_time: number;
}>> {
const accessToken = await resolveToken(args, customer); 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('/post/publish/creator_info/query/', accessToken, 'POST', {});
const data = await tiktokRequest('/video/list/', accessToken, 'POST', { const c = data;
max_count: Math.min(args.max_count ?? 10, 20),
fields: fields.split(','),
});
return (data.videos ?? []).map((v: Record<string, unknown>) => ({ return {
id: String(v.id ?? ''), creator_username: c.creator_username ?? '',
title: v.title as string | undefined, creator_nickname: c.creator_nickname ?? '',
video_description: v.video_description as string | undefined, creator_avatar_url: c.creator_avatar_url,
duration: Number(v.duration ?? 0), privacy_level_options: c.privacy_level_options ?? [],
cover_image_url: v.cover_image_url as string | undefined, max_video_post_duration_sec: c.max_video_post_duration_sec ?? 0,
share_url: v.share_url as string | undefined, comment_disabled: c.comment_disabled ?? false,
view_count: Number(v.view_count ?? 0), duet_disabled: c.duet_disabled ?? false,
like_count: Number(v.like_count ?? 0), stitch_disabled: c.stitch_disabled ?? false,
comment_count: Number(v.comment_count ?? 0), };
share_count: Number(v.share_count ?? 0),
create_time: Number(v.create_time ?? 0),
}));
} }
export async function createVideo( export async function createVideo(
@@ -129,15 +120,21 @@ export async function createVideo(
const auditArgs = { title: args.title }; const auditArgs = { title: args.title };
const accessToken = await resolveToken(args, customer); const accessToken = await resolveToken(args, customer);
// Step 1: initialise upload // Step 1: query creator info to get valid privacy levels (sandbox may not support PUBLIC_TO_EVERYONE)
const init = await tiktokRequest('/post/video/init/', accessToken, 'POST', { 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: { post_info: {
title: args.title ?? '', title: args.title ?? '',
description: args.description ?? '', description: args.description ?? '',
disable_duet: false, disable_duet: false,
disable_comment: false, disable_comment: false,
disable_stitch: false, disable_stitch: false,
privacy_level: 'PUBLIC_TO_EVERYONE', privacy_level: privacyLevel,
}, },
source_info: { source_info: {
source: 'PULL_FROM_URL', source: 'PULL_FROM_URL',

View File

@@ -594,8 +594,8 @@ app.get('/auth/tiktok/callback', async (req, res) => {
`Open ID: ${openId || 'not returned'}`, `Open ID: ${openId || 'not returned'}`,
`Scopes: ${scope || 'not returned'}`, `Scopes: ${scope || 'not returned'}`,
`Expires in: ${expiresIn || 'not returned'} seconds`, `Expires in: ${expiresIn || 'not returned'} seconds`,
`Access token captured: ${accessToken ? `${accessToken.slice(0, 8)}...` : 'no'}`, `Access token: ${accessToken}`,
'Next step: use this token with /api/connect/tiktok or set TIKTOK_DEFAULT_ACCESS_TOKEN for server-side publishing.', 'Copy this token and paste it to your AI assistant to store it for future TikTok API calls.',
...(state ? [`State: ${state}`] : []), ...(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) => { app.get('/api/tiktok/creator-info', requireAuth, async (req, res) => {
const max_count = req.query.max_count ? parseInt(req.query.max_count as string, 10) : undefined;
const account = req.query.account as string | undefined; const account = req.query.account as string | undefined;
try { try {
const result = await handleToolCall('tiktok_get_videos', { max_count, account }); const result = await handleToolCall('tiktok_get_creator_info', { account });
res.json(parseToolResult(result)); res.json(parseToolResult(result));
} catch (err) { } catch (err) {
res.status(500).json({ error: (err as Error).message }); 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' } }, responses: { '200': { description: 'Profile info' } },
}, },
}, },
'/api/tiktok/videos': { '/api/tiktok/creator-info': {
get: { get: {
operationId: 'tiktok_get_videos', operationId: 'tiktok_get_creator_info',
summary: 'Get TikTok videos', summary: 'Get TikTok creator info',
parameters: [ parameters: [
{ name: 'max_count', in: 'query', schema: { type: 'integer' } },
{ name: 'account', in: 'query', schema: { type: 'string' } }, { name: 'account', in: 'query', schema: { type: 'string' } },
], ],
responses: { '200': { description: 'Video list' } }, responses: { '200': { description: 'Creator publishing info' } },
}, },
}, },
'/api/tiktok/video': { '/api/tiktok/video': {
@@ -1822,31 +1821,30 @@ export function getManifest(serverUrl: string, authEnabled: boolean) {
examples: [{ account: 'default' }], examples: [{ account: 'default' }],
}, },
{ {
name: 'tiktok_get_videos', name: 'tiktok_get_creator_info',
category: 'tiktok', category: 'tiktok',
description: 'List recent videos from the authenticated TikTok account', description: 'Get TikTok creator publishing info including privacy levels and max video duration',
when_to_use: 'User wants to see their recent TikTok videos and performance stats.', when_to_use: 'User wants to check their TikTok publishing capabilities before posting a video.',
input_schema: { input_schema: {
type: 'object', type: 'object',
properties: { 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")' }, account: { type: 'string', description: 'Which TikTok account to use (default: "default")' },
}, },
}, },
returns: { returns: {
type: 'array', type: 'object',
items: { properties: {
type: 'object', creator_username: { type: 'string' },
properties: { creator_nickname: { type: 'string' },
id: { type: 'string' }, creator_avatar_url: { type: 'string' },
title: { type: 'string' }, privacy_level_options: { type: 'array', items: { type: 'string' } },
view_count: { type: 'number' }, max_video_post_duration_sec: { type: 'number' },
like_count: { type: 'number' }, comment_disabled: { type: 'boolean' },
share_url: { type: 'string' }, duet_disabled: { type: 'boolean' },
}, stitch_disabled: { type: 'boolean' },
}, },
}, },
examples: [{ max_count: 10, account: 'default' }], examples: [{ account: 'default' }],
}, },
{ {
name: 'tiktok_create_video', 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 { 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 { 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 { 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 { getMe as getSnapchatMe, createSnap, getAdAccounts } from './clients/snapchat.js';
import { getPage, getPosts, createPost as createFacebookPost, createPhotoPost, createVideoPost as createFacebookVideoPost } from './clients/facebook.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: 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: { inputSchema: {
type: 'object', type: 'object',
properties: { 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")' }, account: { type: 'string', description: 'Which TikTok account to use (default: "default")' },
}, },
}, },
@@ -1018,9 +1017,8 @@ export async function handleToolCall(
}, customer); }, customer);
break; break;
case 'tiktok_get_videos': case 'tiktok_get_creator_info':
result = await getUserVideos({ result = await getCreatorInfo({
max_count: (args.max_count as number) ?? 10,
account: args.account as string | undefined, account: args.account as string | undefined,
}, customer); }, customer);
break; break;