UNPKG

polyv-live-cli

Version:

CLI tool for managing PolyV live streaming services.

716 lines 33.1 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.ViewerHandler = void 0; const base_handler_1 = require("./base.handler"); const viewer_service_1 = require("../services/viewer-service"); const errors_1 = require("../utils/errors"); const api_command_1 = require("../utils/api-command"); class ViewerHandler extends base_handler_1.BaseHandler { constructor(authConfig, serviceConfig) { super(); this.viewerService = new viewer_service_1.ViewerServiceSdk(authConfig, serviceConfig); } async getViewer(options) { return this.executeWithErrorHandling(async () => { this.validateGetOptions(options); const result = await this.viewerService.getViewerRecord({ viewerUnionId: options.viewerId, }); this.displayGetResult(result, options); }, 'viewer.get'); } async listViewers(options) { return this.executeWithErrorHandling(async () => { this.validatePaginationOptions(options); const params = { pageNumber: options.page ?? 1, pageSize: options.size ?? 10, }; if (options.source) { params.source = options.source; } if (options.mobile) { params.mobile = options.mobile; } if (options.email) { params.email = options.email; } if (options.area) { params.area = options.area; } const result = await this.viewerService.listViewerRecords(params); this.displayListResult(result, options); }, 'viewer.list'); } async createViewer(options) { return this.executeWithErrorHandling(async () => { this.validateRequiredStringOptions(options, ['nickname', 'mobile']); this.validateOutputOption(options.output); await (0, api_command_1.confirmWrite)(options.force, `Create viewer "${options.nickname}"?`); const params = this.compactParams({ nickname: options.nickname, mobile: options.mobile, name: options.name, lastCollectMobile: options.lastCollectMobile, email: options.email, area: options.area, latestAccessIp: options.latestAccessIp, device: options.device, followUsers: options.followUsers ? this.parseJsonObject(options.followUsers, 'followUsers') : undefined, }); const result = await this.viewerService.createViewerRecord(params); this.displayWriteResult('观众创建成功', result, options.output); }, 'viewer.create'); } async updateViewer(options) { return this.executeWithErrorHandling(async () => { this.validateRequiredStringOptions(options, ['viewerUnionId']); this.validateOutputOption(options.output); await (0, api_command_1.confirmWrite)(options.force, `Update viewer ${options.viewerUnionId}?`); const params = this.compactParams({ viewerUnionId: options.viewerUnionId, nickname: options.nickname, mobile: options.mobile, name: options.name, lastCollectMobile: options.lastCollectMobile, email: options.email, area: options.area, latestAccessIp: options.latestAccessIp, device: options.device, }); await this.viewerService.updateViewerRecord(params); this.displayWriteResult('观众更新成功', params, options.output); }, 'viewer.update'); } async deleteViewer(options) { return this.executeWithErrorHandling(async () => { this.validateRequiredStringOptions(options, ['viewerUnionId']); this.validateOutputOption(options.output); await (0, api_command_1.confirmWrite)(options.force, `Delete viewer ${options.viewerUnionId}?`); await this.viewerService.deleteViewerRecord({ viewerUnionId: options.viewerUnionId }); this.displayWriteResult('观众删除成功', { viewerUnionId: options.viewerUnionId }, options.output); }, 'viewer.delete'); } async importExternalViewers(options) { return this.executeWithErrorHandling(async () => { this.validateOutputOption(options.output); const viewers = this.buildExternalViewers(options); await (0, api_command_1.confirmWrite)(options.force, `Import ${viewers.length} external viewer(s)?`); const result = await this.viewerService.importExternalViewer(viewers); this.displayWriteResult('外部观众导入成功', result, options.output); }, 'viewer.importExternal'); } async updateViewerConfig(options) { return this.executeWithErrorHandling(async () => { this.validateRequiredStringOptions(options, ['mobileLoginEnabled', 'wxWorkLoginEnabled']); this.validateYnOptions(options, [ 'mobileLoginEnabled', 'wxWorkLoginEnabled', 'collectMobileEnabled', 'guestModeEnabled', 'touristExternalHrefEnabled', ]); this.validateViewerConfigOptions(options); this.validateOutputOption(options.output); await (0, api_command_1.confirmWrite)(options.force, 'Update viewer user system config?'); const params = this.compactParams({ mobileLoginEnabled: options.mobileLoginEnabled, wxWorkLoginEnabled: options.wxWorkLoginEnabled, viewerWeixinAuthExpired: options.viewerWeixinAuthExpired, collectMobileEnabled: options.collectMobileEnabled, guestModeEnabled: options.guestModeEnabled, touristExternalHrefEnabled: options.touristExternalHrefEnabled, touristExternalHrefConfig: options.touristExternalHrefConfig ? this.parseJsonObject(options.touristExternalHrefConfig, 'touristExternalHrefConfig') : undefined, }); await this.viewerService.updateViewerUserSystemConfig(params); this.displayWriteResult('观众系统配置更新成功', params, options.output); }, 'viewer.config.update'); } async listViewerLotteryWins(options) { return this.executeWithErrorHandling(async () => { this.validateRequiredStringOptions(options, ['viewerId']); this.validatePaginationOptions(options); const result = await this.viewerService.viewerLotteryWin({ viewerId: options.viewerId, pageNumber: options.page ?? 1, pageSize: options.size ?? 10, }); this.displayViewerLotteryWins(result, options); }, 'viewer.lotteryWins'); } async listViewerTags(options) { return this.executeWithErrorHandling(async () => { this.validatePaginationOptions(options); const result = await this.viewerService.listViewerLabels(); let contents = (result?.contents || []).map(label => this.normalizeViewerLabel(label)); if (options.keyword) { const keywordLower = options.keyword.toLowerCase(); contents = contents.filter((label) => label.labelName?.toLowerCase().includes(keywordLower)); } const page = options.page ?? 1; const size = options.size ?? 10; const startIndex = (page - 1) * size; const paginatedContents = contents.slice(startIndex, startIndex + size); this.displayTagListResult({ contents: paginatedContents, totalItems: contents.length, pageNumber: page, pageSize: size, totalPages: Math.ceil(contents.length / size), }, options); }, 'viewer.tag.list'); } async createViewerTag(options) { return this.executeWithErrorHandling(async () => { this.validateRequiredStringOptions(options, ['labels']); this.validateOutputOption(options.output); const labels = this.parseStringList(options.labels, '标签名称列表'); await (0, api_command_1.confirmWrite)(options.force, `Create ${labels.length} viewer tag(s)?`); const result = await this.viewerService.createViewerLabel({ labels }); this.displayWriteResult('观众标签创建成功', result, options.output); }, 'viewer.tag.create'); } async updateViewerTag(options) { return this.executeWithErrorHandling(async () => { this.validatePositiveNumber(options.id, '标签ID'); this.validateOutputOption(options.output); await (0, api_command_1.confirmWrite)(options.force, `Update viewer tag ${options.id}?`); const params = this.compactParams({ id: options.id, label: options.label }); await this.viewerService.updateViewerLabel(params); this.displayWriteResult('观众标签更新成功', params, options.output); }, 'viewer.tag.update'); } async deleteViewerTag(options) { return this.executeWithErrorHandling(async () => { this.validatePositiveNumber(options.id, '标签ID'); this.validateOutputOption(options.output); await (0, api_command_1.confirmWrite)(options.force, `Delete viewer tag ${options.id}?`); await this.viewerService.deleteViewerLabel({ id: options.id }); this.displayWriteResult('观众标签删除成功', { id: options.id }, options.output); }, 'viewer.tag.delete'); } async addViewerTag(options) { return this.executeWithErrorHandling(async () => { const { viewerIds, labelIds } = this.parseAndValidateTagOptions(options); await (0, api_command_1.confirmWrite)(options.force, `Add ${labelIds.length} tag(s) to ${viewerIds.length} viewer(s)?`); const total = viewerIds.length * labelIds.length; if (total > 10) { this.displayInfo(`正在为 ${viewerIds.length} 个观众添加 ${labelIds.length} 个标签 (共 ${total} 个操作)...`); } const summary = await this.viewerService.addViewersLabels(viewerIds, labelIds, total > 50 ? (completed, total) => { if (completed % 10 === 0 || completed === total) { this.displayInfo(`进度: ${completed}/${total}`); } } : undefined); this.displayTagActionResult('添加', summary, options); }, 'viewer.tag.add'); } async removeViewerTag(options) { return this.executeWithErrorHandling(async () => { const { viewerIds, labelIds } = this.parseAndValidateTagOptions(options); await (0, api_command_1.confirmWrite)(options.force, `Remove ${labelIds.length} tag(s) from ${viewerIds.length} viewer(s)?`); const total = viewerIds.length * labelIds.length; if (total > 10) { this.displayInfo(`正在为 ${viewerIds.length} 个观众移除 ${labelIds.length} 个标签 (共 ${total} 个操作)...`); } const summary = await this.viewerService.removeViewersLabels(viewerIds, labelIds, total > 50 ? (completed, total) => { if (completed % 10 === 0 || completed === total) { this.displayInfo(`进度: ${completed}/${total}`); } } : undefined); this.displayTagActionResult('移除', summary, options); }, 'viewer.tag.remove'); } async listLabels(options) { return this.executeWithErrorHandling(async () => { this.validatePaginationOptions(options); const result = await this.viewerService.listLabels({ pageNumber: options.page ?? 1, pageSize: options.size ?? 10, }); this.displayAccountLabels(result, options); }, 'viewer.label.list'); } async createLabel(options) { return this.executeWithErrorHandling(async () => { this.validateRequiredStringOptions(options, ['labelName']); this.validateAccountLabelName(options.labelName); this.validateOutputOption(options.output); await (0, api_command_1.confirmWrite)(options.force, `Create account label "${options.labelName}"?`); const result = await this.viewerService.createLabel({ labelName: options.labelName }); this.displayWriteResult('账号标签创建成功', result, options.output); }, 'viewer.label.create'); } async updateLabel(options) { return this.executeWithErrorHandling(async () => { this.validateRequiredStringOptions({ labelId: options.labelId }, ['labelId']); this.validateRequiredStringOptions(options, ['labelName']); this.validateOutputOption(options.output); await (0, api_command_1.confirmWrite)(options.force, `Update account label ${options.labelId}?`); const params = { labelId: String(options.labelId), labelName: options.labelName }; await this.viewerService.updateLabel(params); this.displayWriteResult('账号标签更新成功', params, options.output); }, 'viewer.label.update'); } async deleteLabel(options) { return this.executeWithErrorHandling(async () => { this.validateRequiredStringOptions({ labelId: options.labelId }, ['labelId']); this.validateOutputOption(options.output); await (0, api_command_1.confirmWrite)(options.force, `Delete account label ${options.labelId}?`); await this.viewerService.deleteLabel({ labelId: String(options.labelId) }); this.displayWriteResult('账号标签删除成功', { labelId: options.labelId }, options.output); }, 'viewer.label.delete'); } async addChannelLabelRefs(options) { return this.executeWithErrorHandling(async () => { this.validateRequiredStringOptions(options, ['channelIds', 'labelIds']); this.validateOutputOption(options.output); const channelIds = this.parseStringList(options.channelIds, '频道ID列表'); const labelIds = this.parseStringList(options.labelIds, '标签ID列表'); this.validateChannelLabelRefs(channelIds, labelIds); await (0, api_command_1.confirmWrite)(options.force, `Add ${labelIds.length} label(s) to ${channelIds.length} channel(s)?`); const params = { channelIds, labelIds }; await this.viewerService.addChannelLabelRefs(params); this.displayWriteResult('频道标签关联成功', params, options.output); }, 'viewer.label.channelRef.add'); } compactParams(params) { return Object.fromEntries(Object.entries(params).filter(([, value]) => value !== undefined && value !== '')); } validateRequiredStringOptions(options, fields) { const record = options; const errors = fields .filter(field => typeof record[field] !== 'string' || String(record[field]).trim() === '') .map(field => `${field} 是必需的`); if (errors.length > 0) { throw new errors_1.PolyVValidationError(errors.join(', '), 'options', options, 'validation_failed'); } } validateOutputOption(output) { if (output && !['table', 'json'].includes(output)) { throw new errors_1.PolyVValidationError('输出格式必须是 "table" 或 "json"', 'output', output, 'validation_failed'); } } validateYnOptions(options, fields) { const record = options; const invalid = fields.filter(field => { const value = record[field]; return value !== undefined && value !== 'Y' && value !== 'N'; }); if (invalid.length > 0) { throw new errors_1.PolyVValidationError(`${invalid.join(', ')} 必须是 Y 或 N`, 'options', options, 'validation_failed'); } } validatePositiveNumber(value, fieldName) { if (typeof value !== 'number' || !Number.isInteger(value) || value <= 0) { throw new errors_1.PolyVValidationError(`${fieldName}必须是正整数`, fieldName, value, 'validation_failed'); } } validateAccountLabelName(labelName) { if (Array.from(labelName.trim()).length > 8) { throw new errors_1.PolyVValidationError('标签名称最大长度为 8', 'labelName', labelName, 'length_exceeded'); } } validateViewerConfigOptions(options) { const errors = []; if (options.viewerWeixinAuthExpired !== undefined) { const value = options.viewerWeixinAuthExpired; if (!Number.isInteger(value) || value < 0 || value > 180) { errors.push('微信授权有效期必须是 0 到 180 之间的整数'); } } if (options.touristExternalHrefConfig) { try { this.parseJsonObject(options.touristExternalHrefConfig, 'touristExternalHrefConfig'); } catch (error) { errors.push(error instanceof Error ? error.message : String(error)); } } if (errors.length > 0) { throw new errors_1.PolyVValidationError(errors.join(', '), 'options', options, 'validation_failed'); } } validateChannelLabelRefs(channelIds, labelIds) { const errors = []; if (channelIds.length > 100) { errors.push('频道ID数量不能超过 100'); } if (labelIds.length > 50) { errors.push('标签ID数量不能超过 50'); } if (errors.length > 0) { throw new errors_1.PolyVValidationError(errors.join(', '), 'options', { channelIds, labelIds }, 'validation_failed'); } } parseJsonObject(value, fieldName) { try { const parsed = JSON.parse(value); if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) { throw new Error(`${fieldName} 必须是 JSON 对象`); } return parsed; } catch (error) { if (error instanceof errors_1.PolyVValidationError) { throw error; } throw new errors_1.PolyVValidationError(error instanceof Error ? error.message : `${fieldName} JSON 解析失败`, fieldName, value, 'validation_failed'); } } parseStringList(value, fieldName) { const list = value.split(',').map(item => item.trim()).filter(Boolean); if (list.length === 0) { throw new errors_1.PolyVValidationError(`${fieldName}不能为空`, fieldName, value, 'validation_failed'); } return Array.from(new Set(list)); } buildExternalViewers(options) { const errors = []; if (options.viewers) { try { const parsed = JSON.parse(options.viewers); if (!Array.isArray(parsed) || parsed.length === 0) { errors.push('viewers 必须是非空 JSON 数组'); } else { for (const [index, item] of parsed.entries()) { if (!item || typeof item !== 'object' || Array.isArray(item)) { errors.push(`viewers[${index}] 必须是 JSON 对象`); continue; } const viewer = item; if (typeof viewer.externalViewerId !== 'string' || viewer.externalViewerId.trim() === '') { errors.push(`viewers[${index}].externalViewerId 是必需的`); } if (typeof viewer.nickname !== 'string' || viewer.nickname.trim() === '') { errors.push(`viewers[${index}].nickname 是必需的`); } } if (errors.length === 0) { return parsed; } } } catch (error) { errors.push(error instanceof Error ? error.message : 'viewers JSON 解析失败'); } } else { if (!options.externalViewerId || options.externalViewerId.trim() === '') { errors.push('externalViewerId 是必需的'); } if (!options.nickname || options.nickname.trim() === '') { errors.push('nickname 是必需的'); } if (errors.length === 0) { const viewer = { externalViewerId: options.externalViewerId, nickname: options.nickname, }; if (options.labelIds) { viewer.labelIds = this.parseStringList(options.labelIds, '标签ID列表'); } if (options.followUserId) { viewer.followUsers = { userId: options.followUserId, ...(options.followUserType ? { type: options.followUserType } : {}), }; } return [viewer]; } } throw new errors_1.PolyVValidationError(errors.join(', '), 'options', options, 'validation_failed'); } validateGetOptions(options) { const errors = []; if (!options.viewerId || options.viewerId.trim() === '') { errors.push('观众ID是必需的'); } if (options.output && !['table', 'json'].includes(options.output)) { errors.push('输出格式必须是 "table" 或 "json"'); } if (errors.length > 0) { throw new errors_1.PolyVValidationError(errors.join(', '), 'options', options, 'validation_failed'); } } validatePaginationOptions(options) { const errors = []; if (options.page !== undefined) { if (typeof options.page !== 'number' || !Number.isInteger(options.page) || options.page < 1) { errors.push('页码必须是正整数'); } } if (options.size !== undefined) { if (typeof options.size !== 'number' || !Number.isInteger(options.size) || options.size < 1) { errors.push('每页数量必须是正整数'); } if (options.size > 1000) { errors.push('每页数量不能超过 1000'); } } if (options.output && !['table', 'json'].includes(options.output)) { errors.push('输出格式必须是 "table" 或 "json"'); } if (errors.length > 0) { throw new errors_1.PolyVValidationError(errors.join(', '), 'options', options, 'validation_failed'); } } parseAndValidateTagOptions(options) { const errors = []; if (!options.viewerIds || options.viewerIds.trim() === '') { errors.push('观众ID列表是必需的'); } if (!options.labelIds || options.labelIds.trim() === '') { errors.push('标签ID列表是必需的'); } let viewerIds = []; if (options.viewerIds && options.viewerIds.trim() !== '') { viewerIds = options.viewerIds .split(',') .map(id => id.trim()) .filter(id => id.length > 0); if (viewerIds.length === 0) { errors.push('观众ID列表不能为空'); } } let labelIds = []; if (options.labelIds && options.labelIds.trim() !== '') { const rawLabelIds = Array.from(new Set(options.labelIds .split(',') .map(id => id.trim()) .filter(id => id.length > 0))); if (rawLabelIds.length === 0) { errors.push('标签ID列表不能为空'); } rawLabelIds.forEach(id => { const numericId = Number(id); if (!Number.isInteger(numericId) || numericId <= 0) { errors.push(`无效的标签ID: ${id},请使用 viewer tag list 返回的数字标签ID`); } else { labelIds.push(numericId); } }); } if (options.output && !['table', 'json'].includes(options.output)) { errors.push('输出格式必须是 "table" 或 "json"'); } if (errors.length > 0) { throw new errors_1.PolyVValidationError(errors.join(', '), 'options', options, 'validation_failed'); } return { viewerIds, labelIds }; } displayGetResult(result, options) { if (!result) { this.displayInfo(`未找到观众: ${options.viewerId}`); return; } const output = options.output || 'table'; if (output === 'json') { this.displayData(result, 'json'); } else { console.log('观众详情:'); const tableData = { '观众ID': this.truncate(result.viewerUnionId || '-', 30), '昵称': result.nickname || '-', '手机号': result.mobile || '-', '来源': result.source || '-', '姓名': result.name || '-', '邮箱': result.email || '-', '地区': result.area || '-', '观看时长': this.formatDuration(result.watchDuration), '观看频道数': result.watchChannelCount?.toString() || '-', '创建时间': this.formatTime(result.createTime), }; this.displayAsTable(tableData); } } displayListResult(result, options) { const contents = result?.contents || []; const totalItems = result?.totalItems || 0; if (totalItems === 0) { this.displayInfo('未找到观众'); return; } const output = options.output || 'table'; if (output === 'json') { this.displayData({ pageNumber: result.pageNumber, pageSize: result.pageSize, totalPages: result.totalPages, totalItems: result.totalItems, contents: contents }, 'json'); } else { const tableData = contents.map((item) => ({ '观众ID': this.truncate(item.viewerUnionId || '-', 20), '昵称': this.truncate(item.nickname || '-', 20), '手机号': item.mobile || '-', '来源': item.source || '-', '观看时长': this.formatDuration(item.watchDuration), '频道数': item.watchChannelCount?.toString() || '-', '创建时间': this.formatTime(item.createTime), })); console.log(`找到 ${totalItems} 个观众`); console.log(`页码: ${result.pageNumber}, 每页: ${result.pageSize}`); console.log(`总页数: ${result.totalPages}`); if (tableData.length > 0) { this.displayAsTable(tableData); } } } displayWriteResult(message, data, output) { const format = output || 'table'; if (format === 'json') { this.displayData({ success: true, data, }, 'json'); } else { this.displaySuccess(message, data, 'table'); } } displayViewerLotteryWins(result, options) { const contents = result?.contents || []; const totalItems = result?.totalItems || 0; const output = options.output || 'table'; if (output === 'json') { this.displayData(result, 'json'); return; } if (totalItems === 0) { this.displayInfo('未找到中奖记录'); return; } console.log(`找到 ${totalItems} 条中奖记录`); console.log(`页码: ${result.pageNumber}, 每页: ${result.pageSize}`); this.displayAsTable(contents.map(item => ({ '频道ID': item.channelId?.toString() || '-', '频道名称': this.truncate(item.channelName || '-', 24), '活动名称': this.truncate(item.activityName || '-', 24), '奖品': this.truncate(item.prize || '-', 24), '中奖码': item.winnerCode || '-', '已领奖': item.received === undefined ? '-' : (item.received ? '是' : '否'), '中奖时间': this.formatTime(item.createdTime), }))); } displayAccountLabels(result, options) { const contents = result?.contents || []; const totalItems = result?.totalItems || 0; const output = options.output || 'table'; if (output === 'json') { this.displayData(result, 'json'); return; } if (totalItems === 0) { this.displayInfo('未找到账号标签'); return; } console.log(`找到 ${totalItems} 个账号标签`); console.log(`页码: ${result.pageNumber}, 每页: ${result.pageSize}`); this.displayAsTable(contents.map(item => ({ '标签ID': item.id?.toString() || '-', '标签名称': this.truncate(item.name || '-', 30), }))); } displayTagListResult(result, options) { const contents = result?.contents || []; const totalItems = result?.totalItems || 0; if (totalItems === 0) { const message = options.keyword ? `未找到包含关键词 "${options.keyword}" 的标签` : '未找到标签'; this.displayInfo(message); return; } const output = options.output || 'table'; if (output === 'json') { this.displayData({ pageNumber: result.pageNumber, pageSize: result.pageSize, totalPages: result.totalPages, totalItems: result.totalItems, contents: contents }, 'json'); } else { const tableData = contents.map((item) => ({ '标签ID': item.labelId?.toString() || '-', '标签名称': this.truncate(item.labelName || '-', 30), })); console.log(`找到 ${totalItems} 个标签`); console.log(`页码: ${result.pageNumber}, 每页: ${result.pageSize}`); if (tableData.length > 0) { this.displayAsTable(tableData); } } } displayTagActionResult(action, summary, options) { const output = options.output || 'table'; if (output === 'json') { this.displayData({ action, succeeded: summary.succeeded, failed: summary.failed, total: summary.total, results: summary.results, }, 'json'); } else { if (summary.failed === 0) { this.displaySuccess(`成功${action} ${summary.succeeded} 个操作`); } else { this.displayWarning(`${action}完成: 成功 ${summary.succeeded} 个, 失败 ${summary.failed} 个`); const failures = summary.results.filter(r => !r.success); if (failures.length > 0 && failures.length <= 5) { for (const f of failures) { console.log(` - 观众 ${f.viewerUnionId}, 标签 ${f.labelId}: ${f.error}`); } } } } } normalizeViewerLabel(label) { const id = label.id; const labelId = typeof id === 'number' ? id : String(id); return { labelId, labelName: label.label, }; } truncate(str, maxLength) { if (!str) return '-'; return str.length > maxLength ? str.substring(0, maxLength) + '...' : str; } formatDuration(seconds) { if (!seconds) return '-'; const hours = Math.floor(seconds / 3600); const minutes = Math.floor((seconds % 3600) / 60); const secs = seconds % 60; if (hours > 0) { return `${hours}小时 ${minutes}分钟`; } else if (minutes > 0) { return `${minutes}分钟 ${secs}秒`; } else { return `${secs}秒`; } } formatTime(timestamp) { if (!timestamp) return '-'; return new Date(timestamp).toLocaleString(); } } exports.ViewerHandler = ViewerHandler; //# sourceMappingURL=viewer.handler.js.map