|
| 1 | +/** |
| 2 | + * QQ空间(Qzone)发说说/上传图片相关的纯数据构造与解析函数 |
| 3 | + * 不发起任何网络请求,方便单独测试 |
| 4 | + */ |
| 5 | + |
| 6 | +export interface QzoneUploadImageParams { |
| 7 | + uin: string; |
| 8 | + skey: string; |
| 9 | + pskey: string; |
| 10 | + g_tk: string; |
| 11 | + base64: string; |
| 12 | +} |
| 13 | + |
| 14 | +/** |
| 15 | + * 构造上传图片到 QQ 空间的表单请求体 (application/x-www-form-urlencoded) |
| 16 | + * 对齐 https://up.qzone.qq.com/cgi-bin/upload/cgi_upload_image |
| 17 | + */ |
| 18 | +export function buildQzoneUploadImageBody (params: QzoneUploadImageParams): string { |
| 19 | + const { uin, skey, pskey, g_tk, base64 } = params; |
| 20 | + const backUrls = `http://upbak.photo.qzone.qq.com/cgi-bin/upload/cgi_upload_image,http://119.147.64.75/cgi-bin/upload/cgi_upload_image&url=https://up.qzone.qq.com/cgi-bin/upload/cgi_upload_image?g_tk=${g_tk}`; |
| 21 | + const query = new URLSearchParams({ |
| 22 | + filename: 'filename', |
| 23 | + uin, |
| 24 | + skey, |
| 25 | + zzpaneluin: uin, |
| 26 | + p_uin: uin, |
| 27 | + p_skey: pskey, |
| 28 | + uploadtype: '1', |
| 29 | + albumtype: '7', |
| 30 | + exttype: '0', |
| 31 | + refer: 'shuoshuo', |
| 32 | + output_type: 'jsonhtml', |
| 33 | + charset: 'utf-8', |
| 34 | + output_charset: 'utf-8', |
| 35 | + upload_hd: '1', |
| 36 | + hd_width: '2048', |
| 37 | + hd_height: '10000', |
| 38 | + hd_quality: '96', |
| 39 | + backUrls, |
| 40 | + base64: '1', |
| 41 | + jsonhtml_callback: 'callback', |
| 42 | + picfile: base64, |
| 43 | + qzreferrer: `https://user.qzone.qq.com/${uin}/main`, |
| 44 | + }); |
| 45 | + return query.toString(); |
| 46 | +} |
| 47 | + |
| 48 | +export interface QzoneUploadImageData { |
| 49 | + albumid: string; |
| 50 | + lloc: string; |
| 51 | + type: string; |
| 52 | + height: string; |
| 53 | + width: string; |
| 54 | + url?: string; |
| 55 | +} |
| 56 | + |
| 57 | +export interface QzoneUploadImageRawResponse { |
| 58 | + code?: number; |
| 59 | + msg?: string; |
| 60 | + data?: QzoneUploadImageData; |
| 61 | +} |
| 62 | + |
| 63 | +/** |
| 64 | + * 解析上传图片接口返回的 `frameElement.callback({...});</script>` 包裹响应 |
| 65 | + */ |
| 66 | +export function parseQzoneUploadImageResponseText (raw: string): QzoneUploadImageRawResponse { |
| 67 | + const startTag = 'frameElement.callback'; |
| 68 | + const endTag = '</script>'; |
| 69 | + const startIdx = raw.indexOf(startTag); |
| 70 | + const endIdx = raw.indexOf(endTag); |
| 71 | + if (startIdx === -1 || endIdx === -1 || endIdx <= startIdx) { |
| 72 | + throw new Error('QQ空间图片上传响应格式异常'); |
| 73 | + } |
| 74 | + const segment = raw.slice(startIdx + startTag.length, endIdx); |
| 75 | + const jsonStart = segment.indexOf('('); |
| 76 | + const jsonEnd = segment.lastIndexOf(')'); |
| 77 | + if (jsonStart === -1 || jsonEnd === -1 || jsonEnd <= jsonStart) { |
| 78 | + throw new Error('QQ空间图片上传响应解析失败'); |
| 79 | + } |
| 80 | + const jsonText = segment.slice(jsonStart + 1, jsonEnd); |
| 81 | + try { |
| 82 | + return JSON.parse(jsonText) as QzoneUploadImageRawResponse; |
| 83 | + } catch { |
| 84 | + throw new Error('QQ空间图片上传响应JSON解析失败'); |
| 85 | + } |
| 86 | +} |
| 87 | + |
| 88 | +/** |
| 89 | + * 依据上传图片返回数据拼接 richval 字段 |
| 90 | + * 格式: ,albumid,lloc,sloc,type,height,width,,height,width (sloc 与 lloc 一致) |
| 91 | + */ |
| 92 | +export function toQzoneRichval (data: QzoneUploadImageData): string { |
| 93 | + const { albumid, lloc, type, height, width } = data; |
| 94 | + return `,${albumid},${lloc},${lloc},${type},${height},${width},,${height},${width}`; |
| 95 | +} |
| 96 | + |
| 97 | +export interface QzonePublishParams { |
| 98 | + hostuin: string; |
| 99 | + content: string; |
| 100 | + /** 多张图片使用 \t 拼接后的 richval,无图时不传 */ |
| 101 | + richval?: string; |
| 102 | + ugcRight: number; |
| 103 | + /** ugc_right 为 16/128 时必填,多个QQ号使用 | 拼接 */ |
| 104 | + allowUins?: string; |
| 105 | +} |
| 106 | + |
| 107 | +/** |
| 108 | + * 构造发表说说的表单请求体 (application/x-www-form-urlencoded) |
| 109 | + * 对齐 https://user.qzone.qq.com/proxy/domain/taotao.qzone.qq.com/cgi-bin/emotion_cgi_publish_v6 |
| 110 | + */ |
| 111 | +export function buildQzonePublishBody (params: QzonePublishParams): string { |
| 112 | + const { hostuin, content, richval, ugcRight, allowUins } = params; |
| 113 | + const fields: Record<string, string> = { |
| 114 | + syn_tweet_verson: '1', |
| 115 | + paramstr: '1', |
| 116 | + con: content, |
| 117 | + feedversion: '1', |
| 118 | + ver: '1', |
| 119 | + ugc_right: String(ugcRight), |
| 120 | + to_sign: '0', |
| 121 | + hostuin, |
| 122 | + code_version: '1', |
| 123 | + format: 'json', |
| 124 | + qzreferrer: `https://user.qzone.qq.com/${hostuin}/main`, |
| 125 | + }; |
| 126 | + if (richval) { |
| 127 | + fields['richtype'] = '1'; |
| 128 | + fields['richval'] = richval; |
| 129 | + } |
| 130 | + if (allowUins) { |
| 131 | + fields['allow_uins'] = allowUins; |
| 132 | + fields['who'] = '1'; |
| 133 | + } |
| 134 | + return new URLSearchParams(fields).toString(); |
| 135 | +} |
| 136 | + |
| 137 | +export interface QzonePublishResponse { |
| 138 | + subcode?: number; |
| 139 | + code?: number; |
| 140 | + tid?: string; |
| 141 | + t1_tid?: string; |
| 142 | + message?: string; |
| 143 | + msg?: string; |
| 144 | +} |
| 145 | + |
| 146 | +/** |
| 147 | + * 解析发表说说接口返回的 JSON, 提取 tid |
| 148 | + */ |
| 149 | +export function parseQzonePublishResponse (json: QzonePublishResponse): { tid: string; } { |
| 150 | + const subcode = json.subcode ?? json.code; |
| 151 | + if (subcode !== undefined && subcode !== 0) { |
| 152 | + throw new Error(json.message || json.msg || `QQ空间发说说失败, subcode=${subcode}`); |
| 153 | + } |
| 154 | + const tid = json.t1_tid || json.tid; |
| 155 | + if (!tid) { |
| 156 | + throw new Error('QQ空间发说说失败, 未返回tid'); |
| 157 | + } |
| 158 | + return { tid }; |
| 159 | +} |
0 commit comments