|
|
@@ -0,0 +1,313 @@
|
|
|
+const { readVocToken } = require('../core/credentials');
|
|
|
+const {
|
|
|
+ listEndpoints,
|
|
|
+ findEndpoint,
|
|
|
+ searchEndpoints,
|
|
|
+ summarizeEndpoint,
|
|
|
+ loadCatalog
|
|
|
+} = require('../core/api-catalog');
|
|
|
+const { callSocialGateway, redactSecret } = require('../providers/voc-gateway');
|
|
|
+const {
|
|
|
+ buildVocRechargeInfo,
|
|
|
+ buildMissingTokenMessage,
|
|
|
+ buildRechargeRequiredMessage,
|
|
|
+ buildWrongTokenTypeMessage
|
|
|
+} = require('../core/payment-links');
|
|
|
+const { okResult, errorResult } = require('../core/result-envelope');
|
|
|
+
|
|
|
+function isSkToken(token) {
|
|
|
+ return Boolean(token) && /^\s*sk-/i.test(token);
|
|
|
+}
|
|
|
+
|
|
|
+function buildCallTemplate(endpoint) {
|
|
|
+ const args = { id: endpoint.id, params: {} };
|
|
|
+ for (const param of endpoint.params || []) {
|
|
|
+ if (param.required || param.default !== undefined) {
|
|
|
+ args.params[param.name] = param.default !== undefined ? param.default : `<${param.name}>`;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return args;
|
|
|
+}
|
|
|
+
|
|
|
+async function searchVocApis(input = {}) {
|
|
|
+ const catalog = loadCatalog();
|
|
|
+ const query = input.query || input.q || input.keyword || '';
|
|
|
+ const matches = (query || input.platform || input.tag)
|
|
|
+ ? searchEndpoints({ query, platform: input.platform, tag: input.tag, limit: input.limit || 20 })
|
|
|
+ : listEndpoints();
|
|
|
+ const results = matches.map(summarizeEndpoint);
|
|
|
+ const lines = [
|
|
|
+ '## VOC 转发接口清单',
|
|
|
+ '',
|
|
|
+ `网关:${catalog.gateway?.social?.baseUrl || ''}(通用转发,任意 proxyPath 透传到上游 TikHub;每次调用计费 1 次)。`,
|
|
|
+ query ? `匹配关键词「${query}」的接口(${results.length} 条):` : `全部已登记接口(${results.length} 条):`,
|
|
|
+ '',
|
|
|
+ ...results.map(
|
|
|
+ item =>
|
|
|
+ `- ${item.id} | ${item.title}(${item.method} ${item.proxyPath})必填参数: ${item.requiredParams.join(', ') || '无'}`
|
|
|
+ ),
|
|
|
+ '',
|
|
|
+ '下一步:用 voc_api_doc 读取某个接口的详细参数文档,再用 voc_api_call 传参调用。',
|
|
|
+ '清单里没有的接口,可直接用 voc_api_call 传 rawPath + method + params 调用。'
|
|
|
+ ];
|
|
|
+ return okResult({
|
|
|
+ assistantMessage: lines.join('\n'),
|
|
|
+ summary: { total: results.length, query: query || null },
|
|
|
+ data: { endpoints: results, gateway: catalog.gateway, platforms: catalog.platforms }
|
|
|
+ });
|
|
|
+}
|
|
|
+
|
|
|
+async function getVocApiDoc(input = {}) {
|
|
|
+ const idOrPath = input.id || input.endpointId || input.proxyPath || input.rawPath || input.path;
|
|
|
+ if (!idOrPath) {
|
|
|
+ return errorResult('请提供接口 id 或 proxyPath(可先用 voc_api_search 查清单)。', {
|
|
|
+ data: { endpoints: listEndpoints().map(summarizeEndpoint) }
|
|
|
+ });
|
|
|
+ }
|
|
|
+ const endpoint = findEndpoint(idOrPath);
|
|
|
+ if (!endpoint) {
|
|
|
+ return errorResult(`清单里没有找到接口「${idOrPath}」。可用 voc_api_search 查询,或直接用 voc_api_call 传 rawPath 调用未登记接口。`, {
|
|
|
+ data: { endpoints: listEndpoints().map(summarizeEndpoint) }
|
|
|
+ });
|
|
|
+ }
|
|
|
+ const paramLines = (endpoint.params || []).map(p => {
|
|
|
+ const flag = p.required ? '必填' : '可选';
|
|
|
+ const def = p.default !== undefined ? `,默认 ${JSON.stringify(p.default)}` : '';
|
|
|
+ return `- ${p.name}(${p.in}, ${p.type}, ${flag}${def}):${p.desc || ''}`;
|
|
|
+ });
|
|
|
+ const template = buildCallTemplate(endpoint);
|
|
|
+ const lines = [
|
|
|
+ `## ${endpoint.title}(${endpoint.id})`,
|
|
|
+ '',
|
|
|
+ endpoint.summary || '',
|
|
|
+ '',
|
|
|
+ `请求:${endpoint.method} ${endpoint.proxyPath}(计费 ${endpoint.billing || 1} 次)`,
|
|
|
+ '',
|
|
|
+ '参数:',
|
|
|
+ ...paramLines,
|
|
|
+ '',
|
|
|
+ endpoint.responseHint ? `返回:${endpoint.responseHint}` : '',
|
|
|
+ '',
|
|
|
+ '调用示例(传给 voc_api_call):',
|
|
|
+ '```json',
|
|
|
+ JSON.stringify(template, null, 2),
|
|
|
+ '```',
|
|
|
+ '',
|
|
|
+ 'token 用从充值/开通页复制的、以 r: 开头的会话 token(不要用 sk- 开头的 AIGate Key)。'
|
|
|
+ ].filter(line => line !== '');
|
|
|
+ return okResult({
|
|
|
+ assistantMessage: lines.join('\n'),
|
|
|
+ summary: { id: endpoint.id, method: endpoint.method, proxyPath: endpoint.proxyPath },
|
|
|
+ data: { endpoint, callTemplate: template }
|
|
|
+ });
|
|
|
+}
|
|
|
+
|
|
|
+function resolveParamValue(name, params, input) {
|
|
|
+ if (params && Object.prototype.hasOwnProperty.call(params, name)) return params[name];
|
|
|
+ if (Object.prototype.hasOwnProperty.call(input, name)) return input[name];
|
|
|
+ return undefined;
|
|
|
+}
|
|
|
+
|
|
|
+function buildRequestParts(endpoint, input) {
|
|
|
+ const params = input.params && typeof input.params === 'object' ? input.params : {};
|
|
|
+ const query = {};
|
|
|
+ const body = {};
|
|
|
+ const missing = [];
|
|
|
+ const target = (param) => {
|
|
|
+ const where = param.in || endpoint.paramsIn || (endpoint.method === 'GET' ? 'query' : 'body');
|
|
|
+ return where === 'query' ? query : body;
|
|
|
+ };
|
|
|
+ for (const param of endpoint.params || []) {
|
|
|
+ let value = resolveParamValue(param.name, params, input);
|
|
|
+ if ((value === undefined || value === null || value === '') && param.default !== undefined) {
|
|
|
+ value = param.default;
|
|
|
+ }
|
|
|
+ if (value === undefined || value === null || value === '') {
|
|
|
+ if (param.required) missing.push(param.name);
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ target(param)[param.name] = value;
|
|
|
+ }
|
|
|
+ return { query, body: endpoint.method === 'GET' ? undefined : body, missing };
|
|
|
+}
|
|
|
+
|
|
|
+function buildRawRequestParts(input) {
|
|
|
+ const method = String(input.method || 'GET').toUpperCase();
|
|
|
+ const explicitQuery = input.query && typeof input.query === 'object' ? input.query : null;
|
|
|
+ const explicitBody = input.body && typeof input.body === 'object' ? input.body : null;
|
|
|
+ const params = input.params && typeof input.params === 'object' ? input.params : {};
|
|
|
+ if (method === 'GET') {
|
|
|
+ return { method, query: explicitQuery || params, body: undefined };
|
|
|
+ }
|
|
|
+ return { method, query: explicitQuery || {}, body: explicitBody || params };
|
|
|
+}
|
|
|
+
|
|
|
+async function callVocApi(input = {}) {
|
|
|
+ const idOrPath = input.id || input.endpointId;
|
|
|
+ const rawPath = input.rawPath || input.proxyPath || (idOrPath ? null : input.path);
|
|
|
+ const endpoint = idOrPath || rawPath ? findEndpoint(idOrPath || rawPath) : undefined;
|
|
|
+
|
|
|
+ if (!endpoint && !rawPath) {
|
|
|
+ return errorResult('请提供接口 id(先用 voc_api_search 查清单),或提供 rawPath + method 调用未登记接口。', {
|
|
|
+ data: { endpoints: listEndpoints().map(summarizeEndpoint) }
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ const token = readVocToken(input);
|
|
|
+ if (!token) {
|
|
|
+ const recharge = await buildVocRechargeInfo();
|
|
|
+ return {
|
|
|
+ status: 'needs_token',
|
|
|
+ assistantMessage: buildMissingTokenMessage(recharge.paymentUrl, recharge, { platformLabel: '社交平台' }),
|
|
|
+ summary: { endpoint: endpoint?.id || rawPath },
|
|
|
+ data: { recharge },
|
|
|
+ files: [],
|
|
|
+ nextActions: [
|
|
|
+ `打开充值/开通链接:${recharge.paymentUrl}`,
|
|
|
+ '配置以 r: 开头的会话 token 后重新调用'
|
|
|
+ ],
|
|
|
+ warnings: [],
|
|
|
+ errors: []
|
|
|
+ };
|
|
|
+ }
|
|
|
+ if (isSkToken(token)) {
|
|
|
+ const recharge = await buildVocRechargeInfo();
|
|
|
+ return {
|
|
|
+ status: 'needs_valid_token',
|
|
|
+ assistantMessage: buildWrongTokenTypeMessage(recharge.paymentUrl, recharge, { platformLabel: '社交平台' }),
|
|
|
+ summary: { endpoint: endpoint?.id || rawPath, tokenType: 'sk-' },
|
|
|
+ data: { recharge },
|
|
|
+ files: [],
|
|
|
+ nextActions: [
|
|
|
+ '从充值/开通页复制以 r: 开头的会话 token(不是 sk- 开头的 AIGate API Key)',
|
|
|
+ `打开充值/开通链接:${recharge.paymentUrl}`
|
|
|
+ ],
|
|
|
+ warnings: [],
|
|
|
+ errors: []
|
|
|
+ };
|
|
|
+ }
|
|
|
+
|
|
|
+ let proxyPath;
|
|
|
+ let method;
|
|
|
+ let query;
|
|
|
+ let body;
|
|
|
+ if (endpoint) {
|
|
|
+ proxyPath = endpoint.proxyPath;
|
|
|
+ method = endpoint.method;
|
|
|
+ const parts = buildRequestParts(endpoint, input);
|
|
|
+ if (parts.missing.length) {
|
|
|
+ return errorResult(
|
|
|
+ `缺少必填参数:${parts.missing.join(', ')}。这不是类目/关键词/余额问题,请补齐参数后再调用(可用 voc_api_doc 查看参数说明)。`,
|
|
|
+ {
|
|
|
+ status: 'needs_input',
|
|
|
+ summary: { endpoint: endpoint.id, missing: parts.missing },
|
|
|
+ data: { endpoint: summarizeEndpoint(endpoint), missing: parts.missing }
|
|
|
+ }
|
|
|
+ );
|
|
|
+ }
|
|
|
+ query = parts.query;
|
|
|
+ body = parts.body;
|
|
|
+ } else {
|
|
|
+ proxyPath = rawPath;
|
|
|
+ const parts = buildRawRequestParts(input);
|
|
|
+ method = parts.method;
|
|
|
+ query = parts.query;
|
|
|
+ body = parts.body;
|
|
|
+ }
|
|
|
+
|
|
|
+ try {
|
|
|
+ const result = await callSocialGateway({
|
|
|
+ proxyPath,
|
|
|
+ method,
|
|
|
+ query,
|
|
|
+ body,
|
|
|
+ token,
|
|
|
+ baseUrl: input.baseUrl,
|
|
|
+ retries: Number.isFinite(input.retries) ? input.retries : 3
|
|
|
+ });
|
|
|
+ return okResult({
|
|
|
+ assistantMessage: `调用成功:${proxyPath}(${method})。`,
|
|
|
+ summary: {
|
|
|
+ endpoint: endpoint?.id || proxyPath,
|
|
|
+ method,
|
|
|
+ proxyPath,
|
|
|
+ billing: endpoint?.billing || 1,
|
|
|
+ httpStatus: result.httpStatus
|
|
|
+ },
|
|
|
+ data: { result: result.data, raw: result.json }
|
|
|
+ });
|
|
|
+ } catch (error) {
|
|
|
+ const kind = String(error && (error.kind || error.errorKind) || 'upstream');
|
|
|
+ const httpStatus = Number(error && error.httpStatus) || 0;
|
|
|
+ const safeMessage = redactSecret(error && error.message ? error.message : 'request failed');
|
|
|
+
|
|
|
+ if (kind === 'auth') {
|
|
|
+ const recharge = await buildVocRechargeInfo({ token });
|
|
|
+ return {
|
|
|
+ status: 'needs_valid_token',
|
|
|
+ assistantMessage: [
|
|
|
+ '当前 VOC-AI 数据服务 token 没有通过校验,暂时不能调用该接口。',
|
|
|
+ '',
|
|
|
+ '这是 token 无效或类型不对,不是关键词/类目/参数问题:需要从充值/开通页复制的、以 r: 开头的会话 token(不要用 sk- 开头的 AIGate API Key)。',
|
|
|
+ '',
|
|
|
+ `请打开充值/开通链接:${recharge.paymentUrl}`
|
|
|
+ ].join('\n'),
|
|
|
+ summary: { endpoint: endpoint?.id || proxyPath, errorKind: kind, httpStatus },
|
|
|
+ data: { recharge },
|
|
|
+ files: [],
|
|
|
+ nextActions: [`打开充值/开通链接:${recharge.paymentUrl}`, '配置以 r: 开头的 session token 后重试'],
|
|
|
+ warnings: [],
|
|
|
+ errors: []
|
|
|
+ };
|
|
|
+ }
|
|
|
+ if (kind === 'billing') {
|
|
|
+ const recharge = await buildVocRechargeInfo({ token });
|
|
|
+ return {
|
|
|
+ status: 'needs_recharge',
|
|
|
+ assistantMessage: buildRechargeRequiredMessage(recharge.paymentUrl, recharge, { platformLabel: '社交平台' }),
|
|
|
+ summary: { endpoint: endpoint?.id || proxyPath, errorKind: kind, httpStatus },
|
|
|
+ data: { recharge },
|
|
|
+ files: [],
|
|
|
+ nextActions: [`打开充值链接补充额度:${recharge.paymentUrl}`],
|
|
|
+ warnings: [],
|
|
|
+ errors: []
|
|
|
+ };
|
|
|
+ }
|
|
|
+ if (kind === 'request') {
|
|
|
+ return {
|
|
|
+ status: 'needs_input',
|
|
|
+ assistantMessage: [
|
|
|
+ `接口 ${proxyPath} 返回了入参/请求错误(${safeMessage})。`,
|
|
|
+ '',
|
|
|
+ '这不是余额不足,也不是类目不支持:请检查参数是否符合接口文档(可用 voc_api_doc 查看),修正后重试。'
|
|
|
+ ].join('\n'),
|
|
|
+ summary: { endpoint: endpoint?.id || proxyPath, errorKind: kind, httpStatus },
|
|
|
+ data: { message: safeMessage },
|
|
|
+ files: [],
|
|
|
+ nextActions: ['用 voc_api_doc 核对参数', '修正参数后重新调用'],
|
|
|
+ warnings: [],
|
|
|
+ errors: [{ message: safeMessage, kind, httpStatus }]
|
|
|
+ };
|
|
|
+ }
|
|
|
+ return {
|
|
|
+ status: 'upstream_unstable',
|
|
|
+ assistantMessage: [
|
|
|
+ `接口 ${proxyPath} 这次调用失败,是上游数据接口返回错误或暂时不稳定(如 5xx、连接超时、fetch failed)。`,
|
|
|
+ '',
|
|
|
+ '这不是关键词问题,也不是类目不支持、也不是余额不足:通常稍后重试即可(本工具已自动重试若干次)。'
|
|
|
+ ].join('\n'),
|
|
|
+ summary: { endpoint: endpoint?.id || proxyPath, errorKind: kind, httpStatus },
|
|
|
+ data: { message: safeMessage },
|
|
|
+ files: [],
|
|
|
+ nextActions: ['稍后重试该接口调用'],
|
|
|
+ warnings: [],
|
|
|
+ errors: [{ message: safeMessage, kind, httpStatus }]
|
|
|
+ };
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+module.exports = {
|
|
|
+ searchVocApis,
|
|
|
+ getVocApiDoc,
|
|
|
+ callVocApi
|
|
|
+};
|