@iflyrpa/actions 4.1.0-beta.5 → 4.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
 
2
- !function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="1de33b63-4038-5f51-813c-0899b138a5ca")}catch(e){}}();
2
+ !function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="028a0eb4-9179-5708-bae0-75174165ae68")}catch(e){}}();
3
3
  var __webpack_modules__ = {
4
4
  "./src/utils/XhsXsCommonEnc.js": function(module) {
5
5
  var encrypt_lookup = [
@@ -4403,7 +4403,9 @@ var __webpack_exports__ = {};
4403
4403
  });
4404
4404
  const package_json_namespaceObject = require("@iflyrpa/share/package.json");
4405
4405
  var package_json_default = /*#__PURE__*/ __webpack_require__.n(package_json_namespaceObject);
4406
- var package_namespaceObject = JSON.parse('{"i8":"4.1.0-beta.5"}');
4406
+ var package_namespaceObject = {
4407
+ i8: "4.1.0"
4408
+ };
4407
4409
  const share_namespaceObject = require("@iflyrpa/share");
4408
4410
  const external_node_fs_namespaceObject = require("node:fs");
4409
4411
  var external_node_fs_default = /*#__PURE__*/ __webpack_require__.n(external_node_fs_namespaceObject);
@@ -4452,6 +4454,11 @@ var __webpack_exports__ = {};
4452
4454
  const RPA_ERROR_WEBHOOK_URL = "https://open.xfchat.iflytek.com/open-apis/bot/v2/hook/d202c0dc-5af5-40bc-83ed-abc677caa4a5";
4453
4455
  const ALARM_THROTTLE_MS = 60000;
4454
4456
  const lastSentAt = new Map();
4457
+ const THROTTLE_MAP_MAX_KEYS = 500;
4458
+ const pruneThrottleMap = (now)=>{
4459
+ if (lastSentAt.size < THROTTLE_MAP_MAX_KEYS) return;
4460
+ for (const [key, at] of lastSentAt)if (now - at >= ALARM_THROTTLE_MS) lastSentAt.delete(key);
4461
+ };
4455
4462
  const postFeishuWebhook = (webhookUrl, payload)=>external_axios_default().post(webhookUrl, payload, {
4456
4463
  headers: {
4457
4464
  "Content-Type": "application/json"
@@ -4529,10 +4536,11 @@ var __webpack_exports__ = {};
4529
4536
  };
4530
4537
  const reportFeishuAlarm = (report)=>{
4531
4538
  try {
4532
- const key = `${report.platform}|${report.source}|${report.errorType}|${report.code ?? ""}`;
4539
+ const key = `${report.platform}|${report.source}|${report.stage}|${report.errorType}|${report.code ?? ""}`;
4533
4540
  const now = Date.now();
4534
4541
  const last = lastSentAt.get(key);
4535
4542
  if (last && now - last < ALARM_THROTTLE_MS) return;
4543
+ pruneThrottleMap(now);
4536
4544
  lastSentAt.set(key, now);
4537
4545
  postFeishuWebhook(RPA_ERROR_WEBHOOK_URL, buildFeishuPostMessage(report)).catch(()=>{});
4538
4546
  } catch {}
@@ -4679,6 +4687,19 @@ var __webpack_exports__ = {};
4679
4687
  461,
4680
4688
  471
4681
4689
  ]);
4690
+ function isLocalAddress(url) {
4691
+ let hostname;
4692
+ try {
4693
+ hostname = new URL(url).hostname.toLowerCase();
4694
+ } catch {
4695
+ return false;
4696
+ }
4697
+ if ("localhost" === hostname || "::1" === hostname || "[::1]" === hostname) return true;
4698
+ if (hostname.endsWith(".localhost") || hostname.endsWith(".local")) return true;
4699
+ if (/^127\.\d{1,3}\.\d{1,3}\.\d{1,3}$/.test(hostname)) return true;
4700
+ if ("0.0.0.0" === hostname) return true;
4701
+ return false;
4702
+ }
4682
4703
  const HTTP_STATUS_MESSAGE = {
4683
4704
  400: "请求参数错误,请检查参数格式是否正确!",
4684
4705
  401: "登录状态已失效,请重新登录后重试!",
@@ -4715,7 +4736,14 @@ var __webpack_exports__ = {};
4715
4736
  };
4716
4737
  class Http {
4717
4738
  static handleApiError(error) {
4718
- if (error && "object" == typeof error && "code" in error && "message" in error) return error;
4739
+ if (error && "object" == typeof error && "code" in error && "message" in error) {
4740
+ const resp = error;
4741
+ if ("string" != typeof resp.message || !resp.message.trim()) return {
4742
+ ...resp,
4743
+ message: USER_MESSAGE.SYSTEM_ERROR
4744
+ };
4745
+ return resp;
4746
+ }
4719
4747
  return {
4720
4748
  code: 500,
4721
4749
  message: USER_MESSAGE.SYSTEM_ERROR,
@@ -4853,25 +4881,47 @@ var __webpack_exports__ = {};
4853
4881
  errorResponse.message = message;
4854
4882
  }
4855
4883
  if (error.message.includes("Proxy connection ended")) errorResponse.message = "所在区域代理连接超时,请更换区域或稍后重试!";
4856
- const status = error.response?.status;
4857
- reportFeishuAlarm({
4858
- level: status && ALARM_STATUS.has(status) ? "alarm" : "warning",
4859
- platform: this.platform || "unknown",
4860
- source: "http",
4861
- stage: `${(error.config?.method || "get").toUpperCase()} ${error.config?.url || "-"}`,
4862
- errorType: status ? `HTTP_${status}` : error.code || "NETWORK_ERROR",
4863
- code: errorResponse.code,
4864
- msg: errorResponse.message,
4865
- url: error.config?.url,
4866
- title: "RPA接口异常"
4867
- });
4884
+ errorResponse.extra = {
4885
+ ...errorResponse.extra,
4886
+ alarmStatus: error.response?.status,
4887
+ alarmErrorCode: error.code
4888
+ };
4868
4889
  throw errorResponse;
4869
4890
  });
4870
4891
  }
4892
+ reportRequestFailure(config, error, attempts) {
4893
+ const status = error.extra?.alarmStatus;
4894
+ const axiosCode = error.extra?.alarmErrorCode;
4895
+ const method = (config.method || "get").toUpperCase();
4896
+ const baseURL = config.baseURL || this.apiClient.defaults.baseURL || "";
4897
+ const fullUrl = config.url ? /^https?:\/\//.test(config.url) ? config.url : `${baseURL.replace(/\/$/, "")}${config.url}` : baseURL;
4898
+ if (!fullUrl) {
4899
+ this.logger?.debug(`[告警跳过] 请求失败但无 URL,不上报: ${error.message}`);
4900
+ return;
4901
+ }
4902
+ if (isLocalAddress(fullUrl)) {
4903
+ this.logger?.debug(`[告警跳过] 本机地址不上报: ${fullUrl}`);
4904
+ return;
4905
+ }
4906
+ const rawMsg = "string" == typeof error.message && error.message.trim() ? error.message : "";
4907
+ const baseMsg = rawMsg || `请求失败(无错误文案,code=${error.code ?? "unknown"})`;
4908
+ const retriedSuffix = attempts > 0 ? `(已重试${attempts}次仍失败)` : "";
4909
+ reportFeishuAlarm({
4910
+ level: status && ALARM_STATUS.has(status) ? "alarm" : "warning",
4911
+ platform: "@iflyrpa/playwright",
4912
+ source: "http",
4913
+ stage: `${method} ${fullUrl}`,
4914
+ errorType: status ? `HTTP_${status}` : axiosCode || "NETWORK_ERROR",
4915
+ code: error.code,
4916
+ msg: `${baseMsg}${retriedSuffix}`,
4917
+ url: fullUrl,
4918
+ title: "RPA接口异常"
4919
+ });
4920
+ }
4871
4921
  async api(config, options) {
4872
4922
  const retries = options?.retries ?? 0;
4873
4923
  const retryDelay = options?.retryDelay ?? 500;
4874
- const reqTimeout = options?.timeout ?? 30000;
4924
+ const reqTimeout = options?.timeout ?? 60000;
4875
4925
  const externalSignal = options?.signal;
4876
4926
  let agent;
4877
4927
  const sessionRt = async (Rtimes)=>{
@@ -4912,10 +4962,12 @@ var __webpack_exports__ = {};
4912
4962
  ].includes(handledError.code);
4913
4963
  if (Rtimes < retries && isRetry) {
4914
4964
  const url = config.url || "";
4915
- this.logger?.warn(`进入第${Rtimes + 1}次重试!错误码: ${handledError.code}, 请求地址: ${url}`);
4916
- await new Promise((resolve)=>setTimeout(resolve, retryDelay));
4965
+ const backoff = Math.min(retryDelay * 2 ** Rtimes, 5000);
4966
+ this.logger?.warn(`进入第${Rtimes + 1}次重试!错误码: ${handledError.code}, 等待: ${backoff}ms, 请求地址: ${url}`);
4967
+ await new Promise((resolve)=>setTimeout(resolve, backoff));
4917
4968
  return sessionRt(Rtimes + 1);
4918
4969
  }
4970
+ this.reportRequestFailure(config, handledError, Rtimes);
4919
4971
  return Promise.reject(handledError);
4920
4972
  }
4921
4973
  };
@@ -6031,7 +6083,7 @@ var __webpack_exports__ = {};
6031
6083
  const cidrv6 = /^(([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}|::|([0-9a-fA-F]{1,4})?::([0-9a-fA-F]{1,4}:?){0,6})\/(12[0-8]|1[01][0-9]|[1-9]?[0-9])$/;
6032
6084
  const regexes_base64 = /^$|^(?:[0-9a-zA-Z+/]{4})*(?:(?:[0-9a-zA-Z+/]{2}==)|(?:[0-9a-zA-Z+/]{3}=))?$/;
6033
6085
  const regexes_base64url = /^[A-Za-z0-9_-]*$/;
6034
- const hostname = /^(?=.{1,253}\.?$)[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[-0-9a-zA-Z]{0,61}[0-9a-zA-Z])?)*\.?$/;
6086
+ const regexes_hostname = /^(?=.{1,253}\.?$)[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[-0-9a-zA-Z]{0,61}[0-9a-zA-Z])?)*\.?$/;
6035
6087
  const e164 = /^\+(?:[0-9]){6,14}[0-9]$/;
6036
6088
  const dateSource = "(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))";
6037
6089
  const regexes_date = /*@__PURE__*/ new RegExp(`^${dateSource}$`);
@@ -6627,7 +6679,7 @@ var __webpack_exports__ = {};
6627
6679
  code: "invalid_format",
6628
6680
  format: "url",
6629
6681
  note: "Invalid hostname",
6630
- pattern: hostname.source,
6682
+ pattern: regexes_hostname.source,
6631
6683
  input: payload.value,
6632
6684
  inst,
6633
6685
  continue: !def.abort
@@ -8638,6 +8690,14 @@ var __webpack_exports__ = {};
8638
8690
  };
8639
8691
  return new ZodObject(def);
8640
8692
  }
8693
+ function looseObject(shape, params) {
8694
+ return new ZodObject({
8695
+ type: "object",
8696
+ shape,
8697
+ catchall: unknown(),
8698
+ ...normalizeParams(params)
8699
+ });
8700
+ }
8641
8701
  const ZodUnion = /*@__PURE__*/ $constructor("ZodUnion", (inst, def)=>{
8642
8702
  $ZodUnion.init(inst, def);
8643
8703
  ZodType.init(inst, def);
@@ -9332,7 +9392,7 @@ var __webpack_exports__ = {};
9332
9392
  ".png"
9333
9393
  ].includes(ext)) throw {
9334
9394
  code: 414,
9335
- message: `图片格式不支持:${fileName}。百家号仅支持 jpg、png 格式,请转换后重试。`,
9395
+ message: "图片格式不支持,百家号仅支持 jpg、png 格式,请转换后重试。",
9336
9396
  data: ""
9337
9397
  };
9338
9398
  const image = await (0, share_namespaceObject.downloadImage)(url, external_node_path_default().join(tmpCachePath, fileName));
@@ -10015,7 +10075,8 @@ var __webpack_exports__ = {};
10015
10075
  },
10016
10076
  _task.logger,
10017
10077
  params.proxyLoc,
10018
- params.accountId
10078
+ params.accountId,
10079
+ "xiaohongshu"
10019
10080
  ];
10020
10081
  const http = new Http(...args);
10021
10082
  const fans = {
@@ -10029,8 +10090,8 @@ var __webpack_exports__ = {};
10029
10090
  url: "https://creator.xiaohongshu.com/api/galaxy/creator/home/personal_info"
10030
10091
  }, {
10031
10092
  retries: 3,
10032
- retryDelay: 20,
10033
- timeout: 3000
10093
+ retryDelay: 300,
10094
+ timeout: 30000
10034
10095
  });
10035
10096
  fans.fans_count = Number(res.data.fans_count);
10036
10097
  fans.digg_count = Number(res.data.faved_count);
@@ -11167,7 +11228,8 @@ var __webpack_exports__ = {};
11167
11228
  },
11168
11229
  _task.logger,
11169
11230
  params.proxyLoc,
11170
- params.accountId
11231
+ params.accountId,
11232
+ "xiaohongshu"
11171
11233
  ];
11172
11234
  const http = new Http(...args);
11173
11235
  http.addResponseInterceptor((response)=>{
@@ -11215,8 +11277,8 @@ var __webpack_exports__ = {};
11215
11277
  headers: loginBaseXsHeader
11216
11278
  }, {
11217
11279
  retries: 3,
11218
- retryDelay: 20,
11219
- timeout: 5000
11280
+ retryDelay: 300,
11281
+ timeout: 30000
11220
11282
  }).catch((e)=>{
11221
11283
  const clientTimestamp = Date.now();
11222
11284
  const serverDate = e?.extra?.serverDate;
@@ -11248,8 +11310,8 @@ var __webpack_exports__ = {};
11248
11310
  headers: webSessionXsHeader
11249
11311
  }, {
11250
11312
  retries: 3,
11251
- retryDelay: 20,
11252
- timeout: 5000
11313
+ retryDelay: 300,
11314
+ timeout: 30000
11253
11315
  });
11254
11316
  const [baseInfo, web_session] = await Promise.all([
11255
11317
  _baseInfo,
@@ -11341,6 +11403,10 @@ var __webpack_exports__ = {};
11341
11403
  };
11342
11404
  return (0, share_namespaceObject.success)(data, message);
11343
11405
  };
11406
+ const extractEncFileKey = (downloadUrl)=>{
11407
+ if (!downloadUrl) return "";
11408
+ return downloadUrl.split("encfilekey=")[1]?.split("&")[0] || "";
11409
+ };
11344
11410
  const rid = ()=>`${Math.floor(Date.now() / 1e3).toString(16)}-${[
11345
11411
  ...Array(8)
11346
11412
  ].map(()=>Math.floor(16 * Math.random()).toString(16)).join("")}`;
@@ -15500,7 +15566,8 @@ var __webpack_exports__ = {};
15500
15566
  },
15501
15567
  _task.logger,
15502
15568
  params.proxyLoc,
15503
- params.accountId
15569
+ params.accountId,
15570
+ "xiaohongshu"
15504
15571
  ];
15505
15572
  const http = new Http(...args);
15506
15573
  let unreadCount = {
@@ -15529,8 +15596,8 @@ var __webpack_exports__ = {};
15529
15596
  headers: xsHeader
15530
15597
  }, {
15531
15598
  retries: 3,
15532
- retryDelay: 20,
15533
- timeout: 3000
15599
+ retryDelay: 300,
15600
+ timeout: 30000
15534
15601
  });
15535
15602
  const isSuccess = 0 === res.code;
15536
15603
  if (isSuccess) unreadCount = res.data;
@@ -15943,7 +16010,8 @@ var __webpack_exports__ = {};
15943
16010
  },
15944
16011
  _task.logger,
15945
16012
  params.proxyLoc,
15946
- params.accountId
16013
+ params.accountId,
16014
+ "xiaohongshu"
15947
16015
  ];
15948
16016
  const http = new Http(...args);
15949
16017
  const xsEncrypt = new Xhshow();
@@ -15959,8 +16027,8 @@ var __webpack_exports__ = {};
15959
16027
  url: "https://creator.xiaohongshu.com/api/galaxy/creator/home/personal_info"
15960
16028
  }, {
15961
16029
  retries: 3,
15962
- retryDelay: 20,
15963
- timeout: 3000
16030
+ retryDelay: 300,
16031
+ timeout: 30000
15964
16032
  }),
15965
16033
  http.api({
15966
16034
  method: "get",
@@ -15969,8 +16037,8 @@ var __webpack_exports__ = {};
15969
16037
  headers: sevenDataXsHeader
15970
16038
  }, {
15971
16039
  retries: 3,
15972
- retryDelay: 20,
15973
- timeout: 3000
16040
+ retryDelay: 300,
16041
+ timeout: 30000
15974
16042
  })
15975
16043
  ]);
15976
16044
  const xhsData = {
@@ -18832,7 +18900,7 @@ var __webpack_exports__ = {};
18832
18900
  name: params.music.name,
18833
18901
  artist: params.music.authorName,
18834
18902
  mediaStreamingUrl: params.music.url,
18835
- docType: params.music.raw.playableInfo.type
18903
+ docType: 2 === params.music.raw.bgmSource ? 0 : 1
18836
18904
  },
18837
18905
  groupId: params.music.id,
18838
18906
  hasBgm: 1,
@@ -19009,7 +19077,7 @@ var __webpack_exports__ = {};
19009
19077
  const resultMsg = publishResult.data?.baseResp?.errmsg ?? publishResult.errMsg;
19010
19078
  if (0 === resultCode) {
19011
19079
  task.logger.info("[shipinhaoPublish] 发布成功");
19012
- const publishId = uploadedImages[0]?.thumbUrl?.split("encfilekey=")[1]?.split("&")[0] || "";
19080
+ const publishId = extractEncFileKey(uploadedImages[0]?.thumbUrl);
19013
19081
  await updateTaskState?.({
19014
19082
  state: share_namespaceObject.TaskState.SUCCESS,
19015
19083
  result: {
@@ -19801,13 +19869,110 @@ var __webpack_exports__ = {};
19801
19869
  pictureFileType
19802
19870
  };
19803
19871
  }
19872
+ const MENTION_TEXT_SUFFIX = "\u0020";
19873
+ const MENTION_XML_SUFFIX = "\u2005";
19874
+ function payload_buildDescription(params) {
19875
+ let description = params.description || "";
19876
+ for (const topic of params.topics || [])description += `#${topic}`;
19877
+ for (const user of params.mentionedUsers || [])description += `@${user.nickname}${MENTION_TEXT_SUFFIX}`;
19878
+ return description;
19879
+ }
19880
+ function buildMentionedUser(mentionedUsers) {
19881
+ return (mentionedUsers || []).map((user)=>({
19882
+ nickname: `${user.nickname}${MENTION_TEXT_SUFFIX}`
19883
+ }));
19884
+ }
19885
+ function payload_buildTopicXml(params) {
19886
+ const values = [];
19887
+ let atIndex = null;
19888
+ if (params.description) values.push(`<![CDATA[${params.description}]]>`);
19889
+ for (const topic of params.topics || [])values.push(`<topic><![CDATA[#${topic}#]]></topic>`);
19890
+ for (const user of params.mentionedUsers || []){
19891
+ if (null === atIndex) atIndex = values.length;
19892
+ values.push(`<![CDATA[@${user.nickname}${MENTION_XML_SUFFIX}]]>`);
19893
+ }
19894
+ let xml = "<finder>";
19895
+ xml += "<version>1</version>";
19896
+ xml += `<valuecount>${values.length}</valuecount>`;
19897
+ xml += `<style><at>${atIndex ?? ""}</at></style>`;
19898
+ values.forEach((value, index)=>{
19899
+ xml += `<value${index}>${value}</value${index}>`;
19900
+ });
19901
+ xml += "</finder>";
19902
+ return xml;
19903
+ }
19904
+ function payload_buildLocation(location) {
19905
+ if (!location) return {
19906
+ latitude: 0,
19907
+ longitude: 0,
19908
+ city: "",
19909
+ poiName: "",
19910
+ address: "",
19911
+ poiClassifyId: ""
19912
+ };
19913
+ return {
19914
+ latitude: location.latitude,
19915
+ longitude: location.longitude,
19916
+ city: location.city,
19917
+ poiName: location.poiName || "",
19918
+ address: location.address || "",
19919
+ poiClassifyId: location.poiClassifyId || ""
19920
+ };
19921
+ }
19922
+ function buildTopic(params) {
19923
+ const topic = {
19924
+ finderTopicInfo: payload_buildTopicXml(params)
19925
+ };
19926
+ if (params.collection) {
19927
+ topic.collectionId = params.collection.collectionId;
19928
+ topic.collectionName = params.collection.collectionName;
19929
+ }
19930
+ return topic;
19931
+ }
19932
+ function buildEvent(event) {
19933
+ if (!event) return {};
19934
+ return {
19935
+ eventTopicId: event.eventTopicId,
19936
+ eventName: event.eventName,
19937
+ eventCreatorNickname: event.eventCreatorNickname || ""
19938
+ };
19939
+ }
19940
+ function buildExtReading(link) {
19941
+ if (!link) return {
19942
+ link: "",
19943
+ title: "",
19944
+ urlType: 1
19945
+ };
19946
+ return {
19947
+ link: link.link.replace(/[\s\u200b]/g, ""),
19948
+ title: link.title,
19949
+ urlType: link.urlType ?? 1
19950
+ };
19951
+ }
19952
+ function buildTagInfo(tagInfo, tagKey) {
19953
+ return {
19954
+ ...tagInfo,
19955
+ tagKey
19956
+ };
19957
+ }
19804
19958
  const CHUNK_SIZE = 8388608;
19959
+ const UPLOAD_STAGE_TIMEOUT = 180000;
19960
+ const DEFAULT_TUNING = {
19961
+ metaTimeout: UPLOAD_STAGE_TIMEOUT,
19962
+ partTimeout: UPLOAD_STAGE_TIMEOUT,
19963
+ partRetries: 3,
19964
+ completeTimeout: UPLOAD_STAGE_TIMEOUT
19965
+ };
19805
19966
  async function uploader_uploadFile(opts) {
19806
- const { filePath, fileType, uin, authKey, http, logger } = opts;
19967
+ const { filePath, fileType, uin, authKey, http, logger, tuning } = opts;
19807
19968
  const stat = external_node_fs_default().statSync(filePath);
19808
19969
  const fileSize = stat.size;
19809
19970
  const fileName = filePath.split(/[\\/]/).pop() || "file";
19810
- logger?.info(`开始上传: ${fileName} (${fileSize} B, filetype=${fileType})`);
19971
+ const metaTimeout = tuning?.metaTimeout ?? DEFAULT_TUNING.metaTimeout;
19972
+ const partTimeout = tuning?.partTimeout ?? DEFAULT_TUNING.partTimeout;
19973
+ const partRetries = tuning?.partRetries ?? DEFAULT_TUNING.partRetries;
19974
+ const completeTimeout = tuning?.completeTimeout ?? DEFAULT_TUNING.completeTimeout;
19975
+ logger?.info(`[shipinhaoPublishVideo] 开始上传: ${fileName} (${fileSize} B, filetype=${fileType})`);
19811
19976
  const fileMd5 = await computeFileMd5(filePath);
19812
19977
  const taskId = generateTaskId(fileName, fileSize, fileMd5);
19813
19978
  const baseUrl = "https://finderassistancea.video.qq.com";
@@ -19818,7 +19983,7 @@ var __webpack_exports__ = {};
19818
19983
  const chunkCount = Math.ceil(fileSize / CHUNK_SIZE);
19819
19984
  const blockPartLength = [];
19820
19985
  for(let i = 0; i < chunkCount; i++)blockPartLength.push(Math.min((i + 1) * CHUNK_SIZE, fileSize));
19821
- logger?.info(`申请 UploadID: ${chunkCount} 片`);
19986
+ logger?.info(`[shipinhaoPublishVideo] 视频分片: ${chunkCount} 片`);
19822
19987
  const applyRes = await http.api({
19823
19988
  method: "PUT",
19824
19989
  url: `${baseUrl}/applyuploaddfs`,
@@ -19830,12 +19995,15 @@ var __webpack_exports__ = {};
19830
19995
  BlockSum: chunkCount,
19831
19996
  BlockPartLength: blockPartLength
19832
19997
  }
19998
+ }, {
19999
+ timeout: metaTimeout,
20000
+ retries: 2,
20001
+ retryDelay: 2000
19833
20002
  });
19834
20003
  if (!applyRes.UploadID && !applyRes.ListPartsResult) throw new Error("申请 UploadID 失败: " + JSON.stringify(applyRes));
19835
20004
  let uploadId = applyRes.UploadID;
19836
20005
  const uploadedParts = new Set();
19837
20006
  if (applyRes.ListPartsResult) {
19838
- logger?.info("检测到已上传分片,执行续传");
19839
20007
  const parts = Array.isArray(applyRes.ListPartsResult.Part) ? applyRes.ListPartsResult.Part : applyRes.ListPartsResult.Part ? [
19840
20008
  applyRes.ListPartsResult.Part
19841
20009
  ] : [];
@@ -19851,6 +20019,10 @@ var __webpack_exports__ = {};
19851
20019
  BlockSum: chunkCount,
19852
20020
  BlockPartLength: blockPartLength
19853
20021
  }
20022
+ }, {
20023
+ timeout: metaTimeout,
20024
+ retries: 2,
20025
+ retryDelay: 2000
19854
20026
  });
19855
20027
  uploadId = retryRes.UploadID;
19856
20028
  if (!uploadId) throw new Error("续传获取 UploadID 失败");
@@ -19867,7 +20039,6 @@ var __webpack_exports__ = {};
19867
20039
  PartNumber: partNumber,
19868
20040
  ETag: existing.ETag
19869
20041
  });
19870
- logger?.info(`分片 ${partNumber}/${chunkCount} 已存在,跳过`);
19871
20042
  continue;
19872
20043
  }
19873
20044
  }
@@ -19877,6 +20048,7 @@ var __webpack_exports__ = {};
19877
20048
  const chunk = Buffer.alloc(chunkSize);
19878
20049
  external_node_fs_default().readSync(fd, chunk, 0, chunkSize, start);
19879
20050
  const chunkMd5 = external_node_crypto_default().createHash("md5").update(chunk).digest("hex");
20051
+ const partStart = Date.now();
19880
20052
  await http.api({
19881
20053
  method: "PUT",
19882
20054
  url: `${baseUrl}/uploadpartdfs?PartNumber=${partNumber}&UploadID=${encodeURIComponent(uploadId)}`,
@@ -19886,17 +20058,20 @@ var __webpack_exports__ = {};
19886
20058
  "Content-MD5": chunkMd5
19887
20059
  },
19888
20060
  data: chunk
20061
+ }, {
20062
+ timeout: partTimeout,
20063
+ retries: partRetries,
20064
+ retryDelay: 2000
19889
20065
  });
20066
+ Date.now();
19890
20067
  partInfo.push({
19891
20068
  PartNumber: partNumber,
19892
20069
  ETag: `"${chunkMd5}"`
19893
20070
  });
19894
- logger?.info(`分片 ${partNumber}/${chunkCount} 完成 (${chunkSize} B)`);
19895
20071
  }
19896
20072
  } finally{
19897
20073
  external_node_fs_default().closeSync(fd);
19898
20074
  }
19899
- logger?.info("合并分片...");
19900
20075
  const completeRes = await http.api({
19901
20076
  method: "POST",
19902
20077
  url: `${baseUrl}/completepartuploaddfs?UploadID=${encodeURIComponent(uploadId)}`,
@@ -19908,9 +20083,13 @@ var __webpack_exports__ = {};
19908
20083
  TransFlag: "0_0",
19909
20084
  PartInfo: partInfo
19910
20085
  }
20086
+ }, {
20087
+ timeout: completeTimeout,
20088
+ retries: 1,
20089
+ retryDelay: 3000
19911
20090
  });
19912
20091
  if (!completeRes.DownloadURL) throw new Error("合并分片失败: " + JSON.stringify(completeRes));
19913
- logger?.info(`上传成功: ${completeRes.DownloadURL.slice(0, 80)}...`);
20092
+ logger?.info(`[shipinhaoPublishVideo] ${fileName} 上传成功`);
19914
20093
  return {
19915
20094
  downloadUrl: completeRes.DownloadURL,
19916
20095
  md5: fileMd5,
@@ -19930,85 +20109,233 @@ var __webpack_exports__ = {};
19930
20109
  const input = `${fileName}-${fileSize}-${fileMd5}`;
19931
20110
  return external_node_crypto_default().createHash("md5").update(input).digest("hex").slice(0, 32);
19932
20111
  }
20112
+ const MAX_METADATA_BOX_SIZE = 268435456;
20113
+ function readBoxHeader(fd, offset, limit) {
20114
+ if (offset + 8 > limit) return null;
20115
+ const head = Buffer.alloc(16);
20116
+ const read = external_node_fs_default().readSync(fd, head, 0, 16, offset);
20117
+ if (read < 8) return null;
20118
+ let size = head.readUInt32BE(0);
20119
+ const type = head.toString("latin1", 4, 8);
20120
+ let headerSize = 8;
20121
+ if (1 === size) {
20122
+ if (read < 16) return null;
20123
+ size = head.readUInt32BE(8) * 2 ** 32 + head.readUInt32BE(12);
20124
+ headerSize = 16;
20125
+ } else if (0 === size) size = limit - offset;
20126
+ if (size < headerSize || offset + size > limit) return null;
20127
+ return {
20128
+ type,
20129
+ size,
20130
+ headerSize
20131
+ };
20132
+ }
19933
20133
  function parseVideoMeta(filePath) {
19934
- let buf;
20134
+ let fd;
20135
+ let fileSize;
20136
+ try {
20137
+ fd = external_node_fs_default().openSync(filePath, "r");
20138
+ } catch {
20139
+ return null;
20140
+ }
19935
20141
  try {
19936
- buf = external_node_fs_default().readFileSync(filePath);
20142
+ fileSize = external_node_fs_default().fstatSync(fd).size;
20143
+ const state = {
20144
+ movie: null,
20145
+ tracks: [],
20146
+ trexDefaults: new Map(),
20147
+ fragmentDurations: new Map(),
20148
+ trafTrackId: 0,
20149
+ trafDefaultSampleDuration: 0
20150
+ };
20151
+ let offset = 0;
20152
+ while(offset + 8 <= fileSize){
20153
+ const header = readBoxHeader(fd, offset, fileSize);
20154
+ if (!header) break;
20155
+ if ("moov" === header.type || "moof" === header.type) {
20156
+ const bodyLength = header.size - header.headerSize;
20157
+ if (bodyLength > 0 && bodyLength <= MAX_METADATA_BOX_SIZE) {
20158
+ const body = Buffer.alloc(bodyLength);
20159
+ const read = external_node_fs_default().readSync(fd, body, 0, bodyLength, offset + header.headerSize);
20160
+ state.trafTrackId = 0;
20161
+ state.trafDefaultSampleDuration = 0;
20162
+ walkBoxes(body.subarray(0, read), 0, read, state);
20163
+ }
20164
+ }
20165
+ offset += header.size;
20166
+ }
20167
+ const { movie, tracks } = state;
20168
+ const video = tracks.find((t)=>"vide" === t.handler) || tracks.find((t)=>t.width > 0 && t.height > 0);
20169
+ if (!video) return null;
20170
+ let { width, height } = video;
20171
+ if (90 === video.rotation || 270 === video.rotation) [width, height] = [
20172
+ height,
20173
+ width
20174
+ ];
20175
+ return {
20176
+ width: Math.round(width),
20177
+ height: Math.round(height),
20178
+ duration: resolveDuration(movie, video, state.fragmentDurations),
20179
+ rotation: video.rotation,
20180
+ fileSize,
20181
+ codec: video.codec
20182
+ };
19937
20183
  } catch {
19938
20184
  return null;
20185
+ } finally{
20186
+ try {
20187
+ external_node_fs_default().closeSync(fd);
20188
+ } catch {}
19939
20189
  }
19940
- let movie = null;
19941
- const tracks = [];
19942
- function walk(start, end) {
19943
- let off = start;
19944
- while(off + 8 <= end){
19945
- let size = buf.readUInt32BE(off);
19946
- const type = buf.toString("latin1", off + 4, off + 8);
19947
- let headerSize = 8;
19948
- if (1 === size) {
19949
- if (off + 16 > end) break;
19950
- const hi = buf.readUInt32BE(off + 8);
19951
- const lo = buf.readUInt32BE(off + 12);
19952
- size = hi * 2 ** 32 + lo;
19953
- headerSize = 16;
19954
- } else if (0 === size) size = end - off;
19955
- if (size < headerSize || off + size > end) break;
19956
- const bodyStart = off + headerSize;
19957
- const bodyEnd = off + size;
19958
- switch(type){
19959
- case "moov":
19960
- case "trak":
19961
- case "mdia":
19962
- case "minf":
19963
- case "stbl":
19964
- walk(bodyStart, bodyEnd);
20190
+ }
20191
+ function walkBoxes(buf, start, end, state) {
20192
+ let off = start;
20193
+ while(off + 8 <= end){
20194
+ let size = buf.readUInt32BE(off);
20195
+ const type = buf.toString("latin1", off + 4, off + 8);
20196
+ let headerSize = 8;
20197
+ if (1 === size) {
20198
+ if (off + 16 > end) break;
20199
+ const hi = buf.readUInt32BE(off + 8);
20200
+ const lo = buf.readUInt32BE(off + 12);
20201
+ size = hi * 2 ** 32 + lo;
20202
+ headerSize = 16;
20203
+ } else if (0 === size) size = end - off;
20204
+ if (size < headerSize || off + size > end) break;
20205
+ const bodyStart = off + headerSize;
20206
+ const bodyEnd = off + size;
20207
+ switch(type){
20208
+ case "trak":
20209
+ case "mdia":
20210
+ case "minf":
20211
+ case "stbl":
20212
+ case "mvex":
20213
+ case "traf":
20214
+ walkBoxes(buf, bodyStart, bodyEnd, state);
20215
+ break;
20216
+ case "mvhd":
20217
+ state.movie = parseMvhd(buf, bodyStart, bodyEnd);
20218
+ break;
20219
+ case "tkhd":
20220
+ state.tracks.push({
20221
+ ...parseTkhd(buf, bodyStart, bodyEnd),
20222
+ handler: null,
20223
+ codec: null,
20224
+ timescale: 0,
20225
+ mdhdDuration: 0,
20226
+ sttsDuration: 0
20227
+ });
20228
+ break;
20229
+ case "mdhd":
20230
+ {
20231
+ const mdhd = parseMvhd(buf, bodyStart, bodyEnd);
20232
+ if (mdhd && state.tracks.length) {
20233
+ const track = state.tracks[state.tracks.length - 1];
20234
+ track.timescale = mdhd.timescale;
20235
+ track.mdhdDuration = mdhd.duration;
20236
+ }
19965
20237
  break;
19966
- case "mvhd":
19967
- movie = parseMvhd(buf, bodyStart, bodyEnd);
20238
+ }
20239
+ case "hdlr":
20240
+ if (bodyEnd - bodyStart >= 12 && state.tracks.length) state.tracks[state.tracks.length - 1].handler = buf.toString("latin1", bodyStart + 8, bodyStart + 12);
20241
+ break;
20242
+ case "stsd":
20243
+ {
20244
+ const codec = parseStsdCodec(buf, bodyStart, bodyEnd);
20245
+ if (codec && state.tracks.length) state.tracks[state.tracks.length - 1].codec = codec;
19968
20246
  break;
19969
- case "tkhd":
19970
- tracks.push({
19971
- ...parseTkhd(buf, bodyStart, bodyEnd),
19972
- handler: null,
19973
- codec: null
19974
- });
20247
+ }
20248
+ case "stts":
20249
+ if (state.tracks.length) state.tracks[state.tracks.length - 1].sttsDuration = parseSttsDuration(buf, bodyStart, bodyEnd);
20250
+ break;
20251
+ case "trex":
20252
+ if (bodyStart + 20 > bodyEnd) break;
20253
+ state.trexDefaults.set(buf.readUInt32BE(bodyStart + 4), buf.readUInt32BE(bodyStart + 12));
20254
+ break;
20255
+ case "tfhd":
20256
+ {
20257
+ const tfhd = parseTfhd(buf, bodyStart, bodyEnd);
20258
+ state.trafTrackId = tfhd.trackId;
20259
+ state.trafDefaultSampleDuration = tfhd.defaultSampleDuration || state.trexDefaults.get(tfhd.trackId) || 0;
19975
20260
  break;
19976
- case "hdlr":
19977
- if (bodyEnd - bodyStart >= 12) {
19978
- const handler = buf.toString("latin1", bodyStart + 8, bodyStart + 12);
19979
- if (tracks.length) tracks[tracks.length - 1].handler = handler;
19980
- }
20261
+ }
20262
+ case "trun":
20263
+ {
20264
+ const duration = parseTrunDuration(buf, bodyStart, bodyEnd, state.trafDefaultSampleDuration);
20265
+ state.fragmentDurations.set(state.trafTrackId, (state.fragmentDurations.get(state.trafTrackId) || 0) + duration);
19981
20266
  break;
19982
- case "stsd":
19983
- {
19984
- const codec = parseStsdCodec(buf, bodyStart, bodyEnd);
19985
- if (codec && tracks.length) tracks[tracks.length - 1].codec = codec;
19986
- break;
19987
- }
19988
- }
19989
- off += size;
20267
+ }
19990
20268
  }
20269
+ off += size;
19991
20270
  }
19992
- walk(0, buf.length);
19993
- const video = tracks.find((t)=>"vide" === t.handler) || tracks.find((t)=>t.width > 0 && t.height > 0);
19994
- if (!movie || !video) return null;
19995
- const movieData = movie;
19996
- let { width, height } = video;
19997
- if (90 === video.rotation || 270 === video.rotation) [width, height] = [
19998
- height,
19999
- width
20000
- ];
20271
+ }
20272
+ const UNKNOWN_DURATION_32 = 0xffffffff;
20273
+ function isUsableDuration(duration) {
20274
+ return duration > 0 && duration !== UNKNOWN_DURATION_32;
20275
+ }
20276
+ function resolveDuration(movie, video, fragmentDurations) {
20277
+ if (movie && movie.timescale && isUsableDuration(movie.duration)) return movie.duration / movie.timescale;
20278
+ if (video.timescale) {
20279
+ if (isUsableDuration(video.mdhdDuration)) return video.mdhdDuration / video.timescale;
20280
+ if (video.sttsDuration > 0) return video.sttsDuration / video.timescale;
20281
+ const fragment = fragmentDurations.get(video.trackId) ?? (1 === fragmentDurations.size ? [
20282
+ ...fragmentDurations.values()
20283
+ ][0] : 0);
20284
+ if (fragment > 0) return fragment / video.timescale;
20285
+ }
20286
+ return 0;
20287
+ }
20288
+ function parseSttsDuration(buf, start, end) {
20289
+ if (start + 8 > end) return 0;
20290
+ const entryCount = buf.readUInt32BE(start + 4);
20291
+ let total = 0;
20292
+ for(let i = 0; i < entryCount; i++){
20293
+ const off = start + 8 + 8 * i;
20294
+ if (off + 8 > end) break;
20295
+ total += buf.readUInt32BE(off) * buf.readUInt32BE(off + 4);
20296
+ }
20297
+ return total;
20298
+ }
20299
+ function parseTfhd(buf, start, end) {
20300
+ if (start + 8 > end) return {
20301
+ trackId: 0,
20302
+ defaultSampleDuration: 0
20303
+ };
20304
+ const flags = buf.readUIntBE(start + 1, 3);
20305
+ const trackId = buf.readUInt32BE(start + 4);
20306
+ let off = start + 8;
20307
+ if (0x000001 & flags) off += 8;
20308
+ if (0x000002 & flags) off += 4;
20309
+ if (0x000008 & flags && off + 4 <= end) return {
20310
+ trackId,
20311
+ defaultSampleDuration: buf.readUInt32BE(off)
20312
+ };
20001
20313
  return {
20002
- width: Math.round(width),
20003
- height: Math.round(height),
20004
- duration: movieData.timescale ? movieData.duration / movieData.timescale : 0,
20005
- rotation: video.rotation,
20006
- fileSize: buf.length,
20007
- codec: video.codec
20314
+ trackId,
20315
+ defaultSampleDuration: 0
20008
20316
  };
20009
20317
  }
20318
+ function parseTrunDuration(buf, start, end, defaultSampleDuration) {
20319
+ if (start + 8 > end) return 0;
20320
+ const flags = buf.readUIntBE(start + 1, 3);
20321
+ const sampleCount = buf.readUInt32BE(start + 4);
20322
+ let off = start + 8;
20323
+ if (0x000001 & flags) off += 4;
20324
+ if (0x000004 & flags) off += 4;
20325
+ const hasDuration = (0x000100 & flags) !== 0;
20326
+ if (!hasDuration) return sampleCount * defaultSampleDuration;
20327
+ const entrySize = 4 + ((0x000200 & flags) !== 0 ? 4 : 0) + ((0x000400 & flags) !== 0 ? 4 : 0) + ((0x000800 & flags) !== 0 ? 4 : 0);
20328
+ let total = 0;
20329
+ for(let i = 0; i < sampleCount; i++){
20330
+ const entryOff = off + i * entrySize;
20331
+ if (entryOff + 4 > end) break;
20332
+ total += buf.readUInt32BE(entryOff);
20333
+ }
20334
+ return total;
20335
+ }
20010
20336
  function parseStsdCodec(buf, start, end) {
20011
20337
  if (start + 16 > end) return null;
20338
+ if (0 === buf.readUInt32BE(start + 4)) return null;
20012
20339
  return buf.toString("latin1", start + 12, start + 16).toLowerCase();
20013
20340
  }
20014
20341
  function parseMvhd(buf, start, end) {
@@ -20034,7 +20361,10 @@ var __webpack_exports__ = {};
20034
20361
  const afterDuration = 1 === version ? 36 : 24;
20035
20362
  const matrixOff = start + afterDuration + 16;
20036
20363
  const whOff = matrixOff + 36;
20364
+ const trackIdOff = start + (1 === version ? 20 : 12);
20365
+ const trackId = trackIdOff + 4 <= end ? buf.readUInt32BE(trackIdOff) : 0;
20037
20366
  if (whOff + 8 > end) return {
20367
+ trackId,
20038
20368
  width: 0,
20039
20369
  height: 0,
20040
20370
  rotation: 0
@@ -20048,11 +20378,25 @@ var __webpack_exports__ = {};
20048
20378
  else if (Math.abs(a + 1) < 0.01 && Math.abs(b) < 0.01) rotation = 180;
20049
20379
  else if (Math.abs(a) < 0.01 && Math.abs(b + 1) < 0.01) rotation = 270;
20050
20380
  return {
20381
+ trackId,
20051
20382
  width,
20052
20383
  height,
20053
20384
  rotation
20054
20385
  };
20055
20386
  }
20387
+ function parseVideoCodec(filePath) {
20388
+ return parseVideoMeta(filePath)?.codec ?? null;
20389
+ }
20390
+ function buildVideoMetaFromParams(filePath, metadata) {
20391
+ return {
20392
+ width: Math.round(metadata.width),
20393
+ height: Math.round(metadata.height),
20394
+ duration: metadata.duration,
20395
+ rotation: 0,
20396
+ fileSize: metadata.fileSize,
20397
+ codec: parseVideoCodec(filePath)
20398
+ };
20399
+ }
20056
20400
  const MAX_DURATION_SECONDS = 28800;
20057
20401
  const MAX_FILE_SIZE = 21474836480;
20058
20402
  const ALLOWED_EXTENSIONS = [
@@ -20063,6 +20407,7 @@ var __webpack_exports__ = {};
20063
20407
  "avc3"
20064
20408
  ];
20065
20409
  const MIN_TITLE_LENGTH = 6;
20410
+ const MAX_TITLE_LENGTH = 16;
20066
20411
  function formatFileSize(bytes) {
20067
20412
  if (bytes < 1048576) return `${(bytes / 1024).toFixed(2)} KB`;
20068
20413
  if (bytes < 1073741824) return `${(bytes / 1024 / 1024).toFixed(2)} MB`;
@@ -20089,26 +20434,62 @@ var __webpack_exports__ = {};
20089
20434
  function validateShipinhaoTitle(title) {
20090
20435
  if (void 0 === title) return null;
20091
20436
  const trimmed = title.trim();
20437
+ if ("" === trimmed) return null;
20092
20438
  const length = [
20093
20439
  ...trimmed
20094
20440
  ].length;
20095
- if (length < MIN_TITLE_LENGTH) return `视频号标题至少需要 ${MIN_TITLE_LENGTH} 个字符,请补充后重试。`;
20441
+ if (length < MIN_TITLE_LENGTH || length > MAX_TITLE_LENGTH) return `视频号标题需要在 ${MIN_TITLE_LENGTH}-${MAX_TITLE_LENGTH} 个字符之间,当前 ${length} 个字符,请调整后重试。`;
20096
20442
  return null;
20097
20443
  }
20098
- async function getTraceKey(auth, http, logger) {
20099
- logger.info("[getTraceKey] 开始获取 traceKey...");
20444
+ const POST_CREATE_PAGE_URL = "https://channels.weixin.qq.com/micro/content/post/create";
20445
+ const MICRO_CONTENT_BASE = "https://channels.weixin.qq.com/micro/content/cgi-bin/mmfinderassistant-bin";
20446
+ function resolveClientContext(params, fallbackUin) {
20447
+ const extra = params.extraParam || {};
20448
+ const deviceIdCookie = params.cookies.find((c)=>"device_id" === c.name || "finger_print_device_id" === c.name)?.value;
20449
+ return {
20450
+ aId: "string" == typeof extra.aId ? extra.aId : "",
20451
+ fingerPrintDeviceId: "string" == typeof extra.fingerPrintDeviceId ? extra.fingerPrintDeviceId : deviceIdCookie || "",
20452
+ uin: "string" == typeof extra.uin ? extra.uin : String(fallbackUin)
20453
+ };
20454
+ }
20455
+ function buildPublishHeaders(cookieString, client) {
20456
+ const headers = {
20457
+ cookie: cookieString,
20458
+ referer: POST_CREATE_PAGE_URL,
20459
+ origin: "https://channels.weixin.qq.com",
20460
+ "content-type": "application/json"
20461
+ };
20462
+ if (client.fingerPrintDeviceId) headers["finger-print-device-id"] = client.fingerPrintDeviceId;
20463
+ if (client.uin) headers["x-wechat-uin"] = client.uin;
20464
+ return headers;
20465
+ }
20466
+ function buildPublishQuery(client) {
20467
+ const query = {
20468
+ _rid: rid(),
20469
+ _pageUrl: POST_CREATE_PAGE_URL
20470
+ };
20471
+ if (client.aId) query._aid = client.aId;
20472
+ return query;
20473
+ }
20474
+ function buildCommonBody(finderUsername) {
20475
+ return {
20476
+ timestamp: String(Date.now()),
20477
+ _log_finder_uin: "",
20478
+ _log_finder_id: finderUsername,
20479
+ rawKeyBuff: "",
20480
+ pluginSessionId: null,
20481
+ scene: 7,
20482
+ reqScene: 7
20483
+ };
20484
+ }
20485
+ async function getTraceKey(auth, client, http, logger) {
20100
20486
  const res = await http.api({
20101
20487
  method: "POST",
20102
- url: "https://channels.weixin.qq.com/micro/content/cgi-bin/mmfinderassistant-bin/post/get-finder-post-trace-key",
20488
+ url: `${MICRO_CONTENT_BASE}/post/get-finder-post-trace-key`,
20489
+ params: buildPublishQuery(client),
20103
20490
  data: {
20104
20491
  objectId: "",
20105
- timestamp: String(Date.now()),
20106
- _log_finder_uin: "",
20107
- _log_finder_id: auth.finderUsername,
20108
- rawKeyBuff: "",
20109
- pluginSessionId: null,
20110
- scene: 7,
20111
- reqScene: 7
20492
+ ...buildCommonBody(auth.finderUsername)
20112
20493
  },
20113
20494
  defaultErrorMsg: "获取 traceKey 失败"
20114
20495
  });
@@ -20116,18 +20497,38 @@ var __webpack_exports__ = {};
20116
20497
  logger.error("[getTraceKey] 获取失败:", JSON.stringify(res));
20117
20498
  throw new Error(`获取 traceKey 失败: ${JSON.stringify(res)}`);
20118
20499
  }
20119
- logger.info(`[getTraceKey] 获取成功: ${res.data.traceKey}`);
20120
20500
  return res.data.traceKey;
20121
20501
  }
20502
+ async function getObjectTagKey(auth, client, http, logger) {
20503
+ try {
20504
+ const res = await http.api({
20505
+ method: "POST",
20506
+ url: `${MICRO_CONTENT_BASE}/post/finder_get_object_tag_list`,
20507
+ params: buildPublishQuery(client),
20508
+ data: {
20509
+ source: 1,
20510
+ ...buildCommonBody(auth.finderUsername)
20511
+ },
20512
+ defaultErrorMsg: "获取内容声明标注失败"
20513
+ });
20514
+ if (0 !== res.errCode || !res.data?.tagKey) {
20515
+ logger.warn(`[getObjectTagKey] 未取到 tagKey: ${JSON.stringify(res)}`);
20516
+ return null;
20517
+ }
20518
+ return res.data.tagKey;
20519
+ } catch (error) {
20520
+ logger.warn(`[getObjectTagKey] 获取 tagKey 异常: ${stringifyError(error)}`);
20521
+ return null;
20522
+ }
20523
+ }
20122
20524
  async function submitAndPollTranscode(opts) {
20123
- const { videoUrl, videoMeta, traceKey, uploadStartTime, uploadEndTime, finderUsername, http, logger } = opts;
20124
- logger.info("[submitAndPollTranscode] 开始提交转码任务...");
20525
+ const { videoUrl, videoMeta, traceKey, uploadStartTime, uploadEndTime, finderUsername, client, http, logger } = opts;
20125
20526
  const finderUrl = videoUrl.includes("wxapp.tc.qq.com") ? `https://finder.video.qq.com${videoUrl.split("qq.com")[1]}` : videoUrl;
20126
- logger.info(`[submitAndPollTranscode] 视频URL: ${finderUrl}`);
20127
20527
  logger.info(`[submitAndPollTranscode] 视频尺寸: ${videoMeta.width}x${videoMeta.height}, 时长: ${videoMeta.duration}s`);
20128
20528
  const submitRes = await http.api({
20129
20529
  method: "POST",
20130
- url: "https://channels.weixin.qq.com/micro/content/cgi-bin/mmfinderassistant-bin/post/post_clip_video",
20530
+ url: `${MICRO_CONTENT_BASE}/post/post_clip_video`,
20531
+ params: buildPublishQuery(client),
20131
20532
  data: {
20132
20533
  url: finderUrl,
20133
20534
  timeStart: 0,
@@ -20151,13 +20552,7 @@ var __webpack_exports__ = {};
20151
20552
  targetHeight: videoMeta.height,
20152
20553
  type: 4,
20153
20554
  useAstraThumbCover: 1,
20154
- timestamp: String(Date.now()),
20155
- _log_finder_uin: "",
20156
- _log_finder_id: finderUsername,
20157
- rawKeyBuff: "",
20158
- pluginSessionId: null,
20159
- scene: 7,
20160
- reqScene: 7
20555
+ ...buildCommonBody(finderUsername)
20161
20556
  },
20162
20557
  defaultErrorMsg: "提交转码失败"
20163
20558
  });
@@ -20166,43 +20561,39 @@ var __webpack_exports__ = {};
20166
20561
  throw new Error(`提交转码失败: ${JSON.stringify(submitRes)}`);
20167
20562
  }
20168
20563
  const { clipKey, draftId } = submitRes.data;
20169
- logger.info(`[submitAndPollTranscode] 转码任务已提交,clipKey: ${clipKey}, draftId: ${draftId}`);
20170
- const maxPolls = 60;
20171
20564
  const pollInterval = 5000;
20565
+ const pollBudget = Math.min(1800000, 300000 + 1000 * Math.ceil(1.5 * videoMeta.duration));
20566
+ const maxPolls = Math.ceil(pollBudget / pollInterval);
20172
20567
  let pollCount = 0;
20173
- logger.info(`[submitAndPollTranscode] 开始轮询转码结果,最多 ${maxPolls} 次,间隔 ${pollInterval / 1000}s`);
20174
20568
  while(pollCount < maxPolls){
20175
20569
  await sleep(pollInterval);
20176
20570
  pollCount++;
20177
20571
  const pollRes = await http.api({
20178
20572
  method: "POST",
20179
- url: "https://channels.weixin.qq.com/micro/content/cgi-bin/mmfinderassistant-bin/post/post_clip_video_result",
20573
+ url: `${MICRO_CONTENT_BASE}/post/post_clip_video_result`,
20574
+ params: buildPublishQuery(client),
20180
20575
  data: {
20181
20576
  clipKey,
20182
20577
  draftId,
20183
- timestamp: String(Date.now()),
20184
- _log_finder_uin: "",
20185
- _log_finder_id: finderUsername,
20186
- rawKeyBuff: "",
20187
- pluginSessionId: null,
20188
- scene: 7,
20189
- reqScene: 7
20578
+ ...buildCommonBody(finderUsername)
20190
20579
  },
20191
20580
  defaultErrorMsg: "转码轮询失败"
20581
+ }, {
20582
+ timeout: 60000,
20583
+ retries: 2,
20584
+ retryDelay: 3000
20192
20585
  });
20193
20586
  if (0 !== pollRes.errCode) {
20194
20587
  logger.error(`[submitAndPollTranscode] 转码轮询失败 (poll ${pollCount}):`, JSON.stringify(pollRes));
20195
20588
  throw new Error(`转码轮询失败 (poll ${pollCount}): ${JSON.stringify(pollRes)}`);
20196
20589
  }
20197
20590
  const { flag, url, width, height, duration, md5, fileSize } = pollRes.data || {};
20198
- logger.info(`[submitAndPollTranscode] 轮询第 ${pollCount} 次,flag=${flag}`);
20199
20591
  if (1 === flag) {
20200
20592
  if (!url || !width || !height || !duration || !md5 || !fileSize) {
20201
20593
  logger.error("[submitAndPollTranscode] 转码完成但返回数据不完整:", JSON.stringify(pollRes.data));
20202
20594
  throw new Error(`转码完成但返回数据不完整: ${JSON.stringify(pollRes.data)}`);
20203
20595
  }
20204
20596
  logger.info(`[submitAndPollTranscode] 转码完成! 用时: ${pollCount * pollInterval / 1000}s`);
20205
- logger.info(`[submitAndPollTranscode] 视频信息: ${width}x${height}, 时长: ${duration}s, 大小: ${fileSize}`);
20206
20597
  return {
20207
20598
  clipKey,
20208
20599
  url,
@@ -20213,7 +20604,7 @@ var __webpack_exports__ = {};
20213
20604
  fileSize
20214
20605
  };
20215
20606
  }
20216
- if (2 === flag) logger.info(`[submitAndPollTranscode] 转码中... (${pollCount}/${maxPolls})`);
20607
+ if (2 === flag) ;
20217
20608
  else {
20218
20609
  logger.error(`[submitAndPollTranscode] 转码失败,未知 flag=${flag}:`, JSON.stringify(pollRes.data));
20219
20610
  throw new Error(`转码失败,未知 flag=${flag}: ${JSON.stringify(pollRes.data)}`);
@@ -20223,162 +20614,151 @@ var __webpack_exports__ = {};
20223
20614
  throw new Error(`转码超时 (${maxPolls * pollInterval / 1000}s)`);
20224
20615
  }
20225
20616
  async function publishVideo(opts) {
20226
- const { params, auth, clipResult, videoUpload, coverUpload, traceKey, uploadStartTime, uploadEndTime, proxyHttp, logger } = opts;
20227
- logger.info("[publishVideo] 开始构建发布请求...");
20228
- const coverFinderUrl = coverUpload.downloadUrl.includes("wxapp.tc.qq.com") ? `https://finder.video.qq.com${coverUpload.downloadUrl.split("qq.com")[1]}` : coverUpload.downloadUrl;
20229
- logger.info(`[publishVideo] 封面URL: ${coverFinderUrl}`);
20617
+ const { params, auth, client, clipResult, videoUpload, coverUpload, verticalCoverUpload, videoMeta, traceKey, tagKey, uploadStartTime, uploadEndTime, proxyHttp, logger } = opts;
20618
+ const toFinderUrl = (url)=>url.includes("wxapp.tc.qq.com") ? `https://finder.video.qq.com${url.split("qq.com")[1]}` : url;
20619
+ const thumbFinderUrl = toFinderUrl(coverUpload.downloadUrl);
20620
+ const coverFinderUrl = toFinderUrl(verticalCoverUpload.downloadUrl);
20230
20621
  const md5sumUuid = external_node_crypto_default().randomUUID();
20231
- logger.info("[publishVideo] 发布参数:");
20232
- logger.info(` - 标题: ${params.title || "(无)"}`);
20233
- logger.info(` - 描述: ${params.description?.substring(0, 50)}${params.description?.length > 50 ? "..." : ""}`);
20234
- logger.info(` - 定时发布: ${params.scheduleTime ? new Date(1000 * params.scheduleTime).toLocaleString() : "立即发布"}`);
20235
- logger.info(` - 地理位置: ${params.location ? `${params.location.city} (${params.location.latitude},${params.location.longitude})` : "(无)"}`);
20236
- const publishRes = await proxyHttp.api({
20237
- method: "POST",
20238
- url: "https://channels.weixin.qq.com/micro/content/cgi-bin/mmfinderassistant-bin/post/post_create",
20239
- data: {
20240
- objectType: 0,
20241
- longitude: params.location?.longitude || 0,
20242
- latitude: params.location?.latitude || 0,
20243
- feedLongitude: 0,
20244
- feedLatitude: 0,
20245
- originalFlag: 0,
20246
- topics: [],
20247
- isFullPost: 1,
20248
- handleFlag: 2,
20249
- videoClipTaskId: clipResult.clipKey,
20250
- traceInfo: {
20251
- traceKey,
20252
- uploadCdnStart: uploadStartTime,
20253
- uploadCdnEnd: uploadEndTime
20254
- },
20255
- objectDesc: {
20256
- mpTitle: "",
20257
- description: params.description,
20258
- shortTitle: params.title || "",
20259
- extReading: {},
20622
+ const description = params.description;
20623
+ const objectDesc = {
20624
+ mpTitle: "",
20625
+ description,
20626
+ extReading: buildExtReading(params.link),
20627
+ mediaType: 4,
20628
+ location: payload_buildLocation(params.location),
20629
+ topic: buildTopic({
20630
+ description: params.description,
20631
+ topics: params.topics,
20632
+ mentionedUsers: params.mentionedUsers,
20633
+ collection: params.collection
20634
+ }),
20635
+ event: buildEvent(params.event),
20636
+ mentionedUser: buildMentionedUser(params.mentionedUsers),
20637
+ media: [
20638
+ {
20639
+ url: clipResult.url,
20640
+ fileSize: clipResult.fileSize,
20641
+ thumbUrl: thumbFinderUrl,
20642
+ fullThumbUrl: thumbFinderUrl,
20643
+ coverUrl: coverFinderUrl,
20644
+ fullCoverUrl: coverFinderUrl,
20645
+ shareCoverUrl: coverFinderUrl,
20260
20646
  mediaType: 4,
20261
- location: params.location ? {
20262
- latitude: params.location.latitude,
20263
- longitude: params.location.longitude,
20264
- city: params.location.city,
20265
- poiClassifyId: params.location.poiClassifyId || ""
20266
- } : {
20267
- latitude: 0,
20268
- longitude: 0,
20269
- city: "",
20270
- poiClassifyId: ""
20271
- },
20272
- topic: {
20273
- finderTopicInfo: `<finder><version>1</version><valuecount>1</valuecount><style><at></at></style><value0><![CDATA[${params.description}]]></value0></finder>`
20274
- },
20275
- event: {},
20276
- mentionedUser: [],
20277
- media: [
20278
- {
20279
- url: clipResult.url,
20280
- fileSize: clipResult.fileSize,
20281
- thumbUrl: coverFinderUrl,
20282
- fullThumbUrl: coverFinderUrl,
20283
- coverUrl: coverFinderUrl,
20284
- fullCoverUrl: coverFinderUrl,
20285
- shareCoverUrl: coverFinderUrl,
20286
- mediaType: 4,
20287
- videoPlayLen: Math.round(clipResult.duration),
20288
- width: clipResult.width,
20289
- height: clipResult.height,
20290
- md5sum: md5sumUuid,
20291
- cardShowStyle: 2,
20292
- urlCdnTaskId: clipResult.clipKey
20293
- }
20294
- ],
20295
- member: {}
20296
- },
20297
- effectiveTime: params.scheduleTime || 0,
20298
- report: {
20299
- clipKey: clipResult.clipKey,
20300
- draftId: clipResult.clipKey,
20301
- timestamp: String(Date.now()),
20302
- _log_finder_uin: "",
20303
- _log_finder_id: auth.finderUsername,
20304
- rawKeyBuff: "",
20305
- pluginSessionId: null,
20306
- scene: 7,
20307
- reqScene: 7,
20308
- height: clipResult.height,
20647
+ videoPlayLen: Math.round(clipResult.duration),
20309
20648
  width: clipResult.width,
20310
- duration: clipResult.duration,
20311
- fileSize: videoUpload.fileSize,
20312
- uploadCost: (uploadEndTime - uploadStartTime) * 1000
20313
- },
20314
- postFlag: 0,
20315
- mode: 1,
20316
- clientid: external_node_crypto_default().randomUUID(),
20317
- timestamp: String(Date.now()),
20318
- _log_finder_uin: "",
20319
- _log_finder_id: auth.finderUsername,
20320
- rawKeyBuff: "",
20321
- pluginSessionId: null,
20322
- scene: 7,
20323
- reqScene: 7
20649
+ height: clipResult.height,
20650
+ md5sum: md5sumUuid,
20651
+ cardShowStyle: 2,
20652
+ urlCdnTaskId: clipResult.clipKey
20653
+ }
20654
+ ],
20655
+ shortTitle: params.title ? [
20656
+ {
20657
+ shortTitle: params.title
20658
+ }
20659
+ ] : [],
20660
+ member: {}
20661
+ };
20662
+ const publishData = {
20663
+ objectType: 0,
20664
+ longitude: 0,
20665
+ latitude: 0,
20666
+ feedLongitude: 0,
20667
+ feedLatitude: 0,
20668
+ originalFlag: params.originalFlag ?? 0,
20669
+ topics: params.topics || [],
20670
+ isFullPost: 1,
20671
+ handleFlag: 2,
20672
+ videoClipTaskId: clipResult.clipKey,
20673
+ traceInfo: {
20674
+ traceKey,
20675
+ uploadCdnStart: uploadStartTime,
20676
+ uploadCdnEnd: uploadEndTime
20324
20677
  },
20678
+ objectDesc,
20679
+ report: {
20680
+ clipKey: clipResult.clipKey,
20681
+ draftId: clipResult.clipKey,
20682
+ ...buildCommonBody(auth.finderUsername),
20683
+ height: videoMeta.height,
20684
+ width: videoMeta.width,
20685
+ duration: videoMeta.duration,
20686
+ fileSize: videoUpload.fileSize,
20687
+ uploadCost: (uploadEndTime - uploadStartTime) * 1000
20688
+ },
20689
+ postFlag: 0,
20690
+ mode: 1,
20691
+ clientid: external_node_crypto_default().randomUUID(),
20692
+ ...buildCommonBody(auth.finderUsername)
20693
+ };
20694
+ if (params.scheduledTime) publishData.effectiveTime = params.scheduledTime;
20695
+ if (params.tagInfo && tagKey) publishData.tagInfo = buildTagInfo(params.tagInfo, tagKey);
20696
+ const publishRes = await proxyHttp.api({
20697
+ method: "POST",
20698
+ url: `${MICRO_CONTENT_BASE}/post/post_create`,
20699
+ params: buildPublishQuery(client),
20700
+ data: publishData,
20325
20701
  defaultErrorMsg: "发布视频失败"
20326
20702
  });
20327
- logger.info(`[publishVideo] 发布响应: errCode=${publishRes.errCode}, baseResp.errcode=${publishRes.data?.baseResp?.errcode}`);
20703
+ logger.info(`[publishVideo] 发布结果: errCode=${publishRes.errCode}, baseResp.errcode=${publishRes.data?.baseResp?.errcode}`);
20328
20704
  return publishRes;
20329
20705
  }
20330
20706
  function sleep(ms) {
20331
20707
  return new Promise((resolve)=>setTimeout(resolve, ms));
20332
20708
  }
20709
+ async function resolveLocalCoverPath(coverPath, label, tmpCachePath, logger) {
20710
+ if (!/^https?:\/\//i.test(coverPath)) return coverPath;
20711
+ const fileName = (0, share_namespaceObject.getFilenameFromUrl)(coverPath);
20712
+ const savePath = external_node_path_default().join(tmpCachePath, `${Date.now()}-${label}-${fileName}`);
20713
+ await (0, share_namespaceObject.downloadImage)(coverPath, savePath);
20714
+ return savePath;
20715
+ }
20333
20716
  const shipinhaoPublishVideo_mock_mockAction = async (task, params)=>{
20334
- task.logger.info("[shipinhaoPublishVideo] 开始执行视频号视频发布 - Mock API 方式");
20335
20717
  const updateTaskState = task.taskStageStore?.update?.bind(task.taskStageStore, task.taskId || "");
20336
20718
  let currentStep = "初始化";
20337
20719
  try {
20338
20720
  currentStep = "解析认证信息";
20339
20721
  const cookieString = params.cookies.map((c)=>`${c.name}=${c.value}`).join("; ");
20340
- const headers = {
20341
- cookie: cookieString
20342
- };
20722
+ const http = new Http({
20723
+ headers: {
20724
+ cookie: cookieString
20725
+ }
20726
+ });
20727
+ currentStep = "验证发布参数";
20728
+ if (!params.videoPath) return (0, share_namespaceObject.response)(414, "视频文件路径不能为空", "");
20729
+ if (!params.coverPath) return (0, share_namespaceObject.response)(414, "横屏封面图片路径不能为空", "");
20730
+ currentStep = "获取上传认证";
20731
+ const auth = await getShipinhaoUploadAuth(cookieString, http);
20732
+ const client = resolveClientContext(params, auth.uin);
20733
+ const publishHeaders = buildPublishHeaders(cookieString, client);
20734
+ const microHttp = new Http({
20735
+ headers: publishHeaders
20736
+ });
20343
20737
  const args = [
20344
20738
  {
20345
- headers
20739
+ headers: publishHeaders
20346
20740
  },
20347
20741
  task.logger,
20348
20742
  params.proxyLoc,
20349
20743
  params.accountId,
20350
20744
  "shipinhao"
20351
20745
  ];
20352
- const http = new Http({
20353
- headers
20354
- });
20355
20746
  const proxyHttp = new Http(...args);
20356
- currentStep = "验证发布参数";
20357
- if (!params.videoPath) return (0, share_namespaceObject.response)(414, "视频文件路径不能为空", "");
20358
- if (!params.coverPath) return (0, share_namespaceObject.response)(414, "封面图片路径不能为空", "");
20359
- currentStep = "获取上传认证";
20360
- task.logger.info("[shipinhaoPublishVideo] 获取上传认证...");
20361
- const auth = await getShipinhaoUploadAuth(cookieString, http);
20362
- currentStep = "解析视频元数据";
20363
- task.logger.info("[shipinhaoPublishVideo] 解析视频元数据...");
20364
- const videoMeta = parseVideoMeta(params.videoPath);
20365
- if (!videoMeta) return (0, share_namespaceObject.response)(414, "视频文件解析失败,请检查文件格式(仅支持 MP4)", "");
20747
+ currentStep = "组装视频元数据";
20748
+ const videoMeta = buildVideoMetaFromParams(params.videoPath, params.videoMetadata);
20366
20749
  task.logger.info(`[shipinhaoPublishVideo] 视频: ${videoMeta.width}×${videoMeta.height}, ${videoMeta.duration.toFixed(2)}s, ${(videoMeta.fileSize / 1024 / 1024).toFixed(2)}MB, codec=${videoMeta.codec || "unknown"}`);
20367
20750
  currentStep = "校验视频限制";
20368
20751
  const validationError = validateShipinhaoVideo(params.videoPath, videoMeta);
20369
20752
  if (validationError) {
20370
- task.logger.error(`[shipinhaoPublishVideo] 视频校验未通过: ${validationError}`);
20371
20753
  await updateTaskState?.({
20372
20754
  state: share_namespaceObject.TaskState.FAILED,
20373
20755
  error: validationError
20374
20756
  });
20375
20757
  return (0, share_namespaceObject.response)(414, validationError, "");
20376
20758
  }
20377
- task.logger.info("[shipinhaoPublishVideo] 视频校验通过");
20378
20759
  currentStep = "校验标题";
20379
20760
  const titleError = validateShipinhaoTitle(params.title);
20380
20761
  if (titleError) {
20381
- task.logger.error(`[shipinhaoPublishVideo] 标题校验未通过: ${titleError}`);
20382
20762
  await updateTaskState?.({
20383
20763
  state: share_namespaceObject.TaskState.FAILED,
20384
20764
  error: titleError
@@ -20386,14 +20766,14 @@ var __webpack_exports__ = {};
20386
20766
  return (0, share_namespaceObject.response)(414, titleError, "");
20387
20767
  }
20388
20768
  currentStep = "获取 traceKey";
20389
- task.logger.info("[shipinhaoPublishVideo] 获取 traceKey...");
20390
- const traceKey = await getTraceKey(auth, http, task.logger);
20769
+ const traceKey = await getTraceKey(auth, client, microHttp, task.logger);
20770
+ let tagKey = null;
20771
+ if (params.tagInfo) {
20772
+ currentStep = "获取内容声明 tagKey";
20773
+ tagKey = await getObjectTagKey(auth, client, microHttp, task.logger);
20774
+ }
20391
20775
  const uploadStartTime = Math.floor(Date.now() / 1000);
20392
- task.logger.info(`[shipinhaoPublishVideo] 上传开始时间: ${uploadStartTime}`);
20393
20776
  currentStep = "上传视频";
20394
- task.logger.info("[shipinhaoPublishVideo] 上传视频...");
20395
- task.logger.info(`[shipinhaoPublishVideo] 视频路径: ${params.videoPath}`);
20396
- task.logger.info(`[shipinhaoPublishVideo] 视频文件类型: ${auth.videoFileType}`);
20397
20777
  const videoUpload = await uploader_uploadFile({
20398
20778
  filePath: params.videoPath,
20399
20779
  fileType: auth.videoFileType,
@@ -20402,24 +20782,32 @@ var __webpack_exports__ = {};
20402
20782
  http,
20403
20783
  logger: task.logger
20404
20784
  });
20405
- task.logger.info(`[shipinhaoPublishVideo] 视频上传完成,URL: ${videoUpload.downloadUrl}`);
20406
20785
  const uploadEndTime = Math.floor(Date.now() / 1000);
20407
- task.logger.info(`[shipinhaoPublishVideo] 上传结束时间: ${uploadEndTime}, 耗时: ${uploadEndTime - uploadStartTime}s`);
20408
- currentStep = "上传封面";
20409
- task.logger.info("[shipinhaoPublishVideo] 上传封面...");
20410
- task.logger.info(`[shipinhaoPublishVideo] 封面路径: ${params.coverPath}`);
20411
- task.logger.info(`[shipinhaoPublishVideo] 封面文件类型: ${auth.pictureFileType}`);
20786
+ task.logger.info(`[shipinhaoPublishVideo] 耗时: ${uploadEndTime - uploadStartTime}s`);
20787
+ currentStep = "上传横屏封面";
20788
+ const localCoverPath = await resolveLocalCoverPath(params.coverPath, "横屏封面", task.getTmpPath(), task.logger);
20412
20789
  const coverUpload = await uploader_uploadFile({
20413
- filePath: params.coverPath,
20790
+ filePath: localCoverPath,
20414
20791
  fileType: auth.pictureFileType,
20415
20792
  uin: auth.uin,
20416
20793
  authKey: auth.authKey,
20417
20794
  http,
20418
20795
  logger: task.logger
20419
20796
  });
20420
- task.logger.info(`[shipinhaoPublishVideo] 封面上传完成,URL: ${coverUpload.downloadUrl}`);
20797
+ let verticalCoverUpload = coverUpload;
20798
+ if (params.verticalCoverPath) {
20799
+ currentStep = "上传竖屏封面";
20800
+ const localVerticalPath = await resolveLocalCoverPath(params.verticalCoverPath, "竖屏封面", task.getTmpPath(), task.logger);
20801
+ verticalCoverUpload = await uploader_uploadFile({
20802
+ filePath: localVerticalPath,
20803
+ fileType: auth.pictureFileType,
20804
+ uin: auth.uin,
20805
+ authKey: auth.authKey,
20806
+ http,
20807
+ logger: task.logger
20808
+ });
20809
+ }
20421
20810
  currentStep = "提交转码";
20422
- task.logger.info("[shipinhaoPublishVideo] 提交转码...");
20423
20811
  const clipResult = await submitAndPollTranscode({
20424
20812
  videoUrl: videoUpload.downloadUrl,
20425
20813
  videoMeta,
@@ -20427,21 +20815,24 @@ var __webpack_exports__ = {};
20427
20815
  uploadStartTime,
20428
20816
  uploadEndTime,
20429
20817
  finderUsername: auth.finderUsername,
20430
- http,
20818
+ client,
20819
+ http: microHttp,
20431
20820
  logger: task.logger
20432
20821
  });
20433
20822
  currentStep = "发布视频";
20434
- task.logger.info("[shipinhaoPublishVideo] 发布视频...");
20435
- task.logger.info(`[shipinhaoPublishVideo] clipKey: ${clipResult.clipKey}`);
20436
20823
  let publishResult;
20437
20824
  try {
20438
20825
  publishResult = await publishVideo({
20439
20826
  params,
20440
20827
  auth,
20828
+ client,
20441
20829
  clipResult,
20442
20830
  videoUpload,
20443
20831
  coverUpload,
20832
+ verticalCoverUpload,
20833
+ videoMeta,
20444
20834
  traceKey,
20835
+ tagKey,
20445
20836
  uploadStartTime,
20446
20837
  uploadEndTime,
20447
20838
  proxyHttp,
@@ -20453,7 +20844,6 @@ var __webpack_exports__ = {};
20453
20844
  const classified = classifyPublishError(handledError);
20454
20845
  if (classified) {
20455
20846
  const message = `${classified.userMessage}${task.debug ? ` ${http.proxyInfo}` : ""}`;
20456
- task.logger.error(`[shipinhaoPublishVideo] ${classified.category},直接返回: ${handledError.message} (code=${handledError.code})`, stringifyError(handledError));
20457
20847
  await updateTaskState?.({
20458
20848
  state: share_namespaceObject.TaskState.FAILED,
20459
20849
  error: message
@@ -20468,12 +20858,10 @@ var __webpack_exports__ = {};
20468
20858
  }
20469
20859
  task.logger.info(`[shipinhaoPublishVideo] publishResult: ${JSON.stringify(publishResult)}`);
20470
20860
  const resultCode = publishResult.data?.baseResp?.errcode ?? publishResult.errCode;
20471
- const resultMsg = publishResult.data?.baseResp?.errmsg ?? "发布成功";
20861
+ const resultMsg = publishResult.data?.baseResp?.errmsg ?? (0 === resultCode ? "发布成功" : `发布失败(errCode=${resultCode})`);
20472
20862
  if (0 === resultCode) {
20473
- task.logger.info("[shipinhaoPublishVideo] 发布成功");
20474
- task.logger.info(`[shipinhaoPublishVideo] 作品ID: ${clipResult.clipKey}`);
20475
- task.logger.info(`[shipinhaoPublishVideo] 视频URL: ${clipResult.url}`);
20476
- task.logger.info(`[shipinhaoPublishVideo] 封面URL: ${coverUpload.downloadUrl}`);
20863
+ const publishId = extractEncFileKey(verticalCoverUpload.downloadUrl);
20864
+ if (!publishId) task.logger.error(`[shipinhaoPublishVideo] 封面 DownloadURL 中未解析到 encfilekey,关联 id 为空: ${verticalCoverUpload.downloadUrl}`);
20477
20865
  await updateTaskState?.({
20478
20866
  state: share_namespaceObject.TaskState.SUCCESS,
20479
20867
  result: {
@@ -20489,41 +20877,49 @@ var __webpack_exports__ = {};
20489
20877
  uid: params.uid,
20490
20878
  publishParams: {
20491
20879
  videoPath: params.videoPath,
20492
- coverPath: params.coverPath
20880
+ coverPath: params.coverPath,
20881
+ verticalCoverPath: params.verticalCoverPath,
20882
+ title: params.title,
20883
+ topics: params.topics,
20884
+ mentionedUsers: params.mentionedUsers,
20885
+ collection: params.collection,
20886
+ event: params.event,
20887
+ link: params.link,
20888
+ tagInfo: params.tagInfo,
20889
+ originalFlag: params.originalFlag,
20890
+ scheduledTime: params.scheduledTime
20493
20891
  },
20494
20892
  platform: "shipinhao"
20495
20893
  });
20496
- task.logger.info("[shipinhaoPublishVideo] 日志上报完成");
20497
- return (0, share_namespaceObject.response)(0, "发布成功", clipResult.clipKey);
20894
+ return (0, share_namespaceObject.response)(0, "发布成功", publishId);
20498
20895
  }
20499
20896
  let errorMessage = resultMsg;
20500
20897
  if (-11224 === resultCode) errorMessage = "视频号管理员完成实名且绑定手机号后才可以发表";
20501
20898
  else if (300333 === resultCode || 300334 === resultCode) errorMessage = "登录失效";
20502
20899
  else if (300330 === resultCode) errorMessage = "未登录";
20900
+ else if (300002 === resultCode) errorMessage = "官方平台在校验音乐/位置/定时信息时失败了,请重新编辑后发布";
20503
20901
  task.logger.error(`[shipinhaoPublishVideo] 发布失败: ${errorMessage} (errCode=${resultCode})`);
20504
20902
  await updateTaskState?.({
20505
20903
  state: share_namespaceObject.TaskState.FAILED,
20506
20904
  error: errorMessage
20507
20905
  });
20508
- return (0, share_namespaceObject.response)(resultCode || 414, errorMessage, "");
20906
+ return (0, share_namespaceObject.response)(414, errorMessage, "");
20509
20907
  } catch (error) {
20510
20908
  const handledError = Http.handleApiError(error);
20511
20909
  const errorMsg = handledError.message || "发布失败,请稍后重试";
20512
- const errorCode = handledError.code || 414;
20513
20910
  task.logger.error(`[shipinhaoPublishVideo] 发布流程异常 [${currentStep}]: ${errorMsg}`, stringifyError(error), handledError.extra);
20514
- task.logger.error(`[shipinhaoPublishVideo] 错误码: ${errorCode}, 当前步骤: ${currentStep}`);
20515
20911
  await updateTaskState?.({
20516
20912
  state: share_namespaceObject.TaskState.FAILED,
20517
20913
  error: errorMsg
20518
20914
  });
20519
- return (0, share_namespaceObject.response)(errorCode, errorMsg, "");
20915
+ return (0, share_namespaceObject.response)(414, errorMsg, "");
20520
20916
  }
20521
20917
  };
20522
20918
  const shipinhaoPublishVideo_rpa_rpaAction = async (task, params)=>{
20523
20919
  task.logger.info("开始微信视频号视频发布(RPA 模式)");
20524
- const videoMeta = parseVideoMeta(params.videoPath);
20525
- if (!videoMeta) return (0, share_namespaceObject.response)(414, "视频文件解析失败,请检查文件格式(仅支持 MP4)", "");
20920
+ const videoMeta = buildVideoMetaFromParams(params.videoPath, params.videoMetadata);
20526
20921
  task.logger.info(`视频: ${videoMeta.width}×${videoMeta.height}, ${videoMeta.duration.toFixed(2)}s, ${(videoMeta.fileSize / 1024 / 1024).toFixed(2)}MB, codec=${videoMeta.codec || "unknown"}`);
20922
+ if (!videoMeta.codec) task.logger.warn("未能读取视频编码格式,跳过 H.264 预检,交由服务端判断");
20527
20923
  const validationError = validateShipinhaoVideo(params.videoPath, videoMeta);
20528
20924
  if (validationError) {
20529
20925
  task.logger.error(`视频校验未通过: ${validationError}`);
@@ -20535,6 +20931,17 @@ var __webpack_exports__ = {};
20535
20931
  task.logger.error(`标题校验未通过: ${titleError}`);
20536
20932
  return (0, share_namespaceObject.response)(414, titleError, "");
20537
20933
  }
20934
+ const unsupported = [
20935
+ params.verticalCoverPath && "verticalCoverPath",
20936
+ params.collection && "collection",
20937
+ params.originalFlag && "originalFlag",
20938
+ params.postWithMemberZoneLink && "postWithMemberZoneLink"
20939
+ ].filter(Boolean);
20940
+ if (unsupported.length) task.logger.warn(`RPA 模式不支持以下参数,将被忽略: ${unsupported.join("、")};如需生效请使用 mockApi 模式`);
20941
+ if (params.tagInfo && 5 === params.tagInfo.tagType) {
20942
+ const shootInfo = params.tagInfo.shootInfo;
20943
+ if (shootInfo && (shootInfo.provinceCode || shootInfo.cityCode)) task.logger.warn("tagInfo.tagType=5 在 RPA 模式下仅支持拍摄时间和国家选择,省市选择需要使用 mockApi 模式");
20944
+ }
20538
20945
  const tmpCachePath = task.getTmpPath();
20539
20946
  const page = await task.createPage({
20540
20947
  url: "https://channels.weixin.qq.com/platform/post/create",
@@ -20645,7 +21052,12 @@ var __webpack_exports__ = {};
20645
21052
  }
20646
21053
  });
20647
21054
  }
20648
- if (params.description) {
21055
+ const descriptionText = payload_buildDescription({
21056
+ description: params.description,
21057
+ topics: params.topics,
21058
+ mentionedUsers: params.mentionedUsers
21059
+ });
21060
+ if (descriptionText) {
20649
21061
  task.logger.info("填写视频描述");
20650
21062
  await retryAction(async ()=>{
20651
21063
  const descEditor = await waitForElement(".input-editor", 10000);
@@ -20657,7 +21069,7 @@ var __webpack_exports__ = {};
20657
21069
  });
20658
21070
  await page.waitForTimeout(300);
20659
21071
  task.logger.info("已清空编辑器内容");
20660
- await descEditor.pressSequentially(params.description, {
21072
+ await descEditor.pressSequentially(descriptionText, {
20661
21073
  delay: 10
20662
21074
  });
20663
21075
  await page.waitForTimeout(500);
@@ -20692,7 +21104,59 @@ var __webpack_exports__ = {};
20692
21104
  await poperInstance.nth(1).click();
20693
21105
  task.logger.info("地点选择完成");
20694
21106
  }
20695
- if (params.scheduleTime) {
21107
+ if (params.collection) {
21108
+ task.logger.info(`选择合集: ${params.collection.collectionName}`);
21109
+ const instanceCollection = page.locator(".post-album-display-wrap");
21110
+ await instanceCollection.click();
21111
+ await page.waitForTimeout(1000);
21112
+ page.locator(".post-album-wrap .option-item").filter({
21113
+ hasText: params.collection.collectionName
21114
+ }).first().click({
21115
+ force: true
21116
+ });
21117
+ }
21118
+ if (params.link) {
21119
+ task.logger.info(`设置扩展阅读链接: ${params.link.title}`);
21120
+ await page.locator(".post-link-wrap .link-display-wrap").click();
21121
+ await page.waitForTimeout(300);
21122
+ const linkTypeText = 2 === params.link.urlType ? "红包封面" : "公众号文章";
21123
+ await page.locator(".link-option-item .title-wrap span").filter({
21124
+ hasText: linkTypeText
21125
+ }).click();
21126
+ await page.waitForTimeout(300);
21127
+ const placeholder = 2 === params.link.urlType ? "粘贴红包封面链接" : "粘贴公众号文章链接";
21128
+ await page.locator(`.link-input-wrap input[placeholder="${placeholder}"]`).fill(params.link.link);
21129
+ await page.waitForTimeout(500);
21130
+ task.logger.info(`已设置${linkTypeText}: ${params.link.link}`);
21131
+ }
21132
+ if (params.event) {
21133
+ task.logger.info(`选择活动: ${params.event.eventName}`);
21134
+ await page.locator(".post-activity-wrap .activity-display").click();
21135
+ await page.waitForTimeout(500);
21136
+ await page.locator(".activity-filter-wrap .weui-desktop-form__input[placeholder='搜索活动']").fill(params.event.eventName);
21137
+ await page.waitForTimeout(500);
21138
+ const searchLoading = page.locator(".search-loading");
21139
+ await searchLoading.waitFor({
21140
+ state: "hidden",
21141
+ timeout: 5000
21142
+ }).catch(()=>{
21143
+ task.logger.warn("活动搜索加载超时,继续尝试选择");
21144
+ });
21145
+ const activityItem = page.locator(".option-item .activity-item .activity-item-info .name").filter({
21146
+ hasText: params.event.eventName
21147
+ });
21148
+ const count = await activityItem.count();
21149
+ if (count > 0) {
21150
+ await activityItem.first().click();
21151
+ await page.waitForTimeout(300);
21152
+ task.logger.info(`已选择活动: ${params.event.eventName}`);
21153
+ } else {
21154
+ task.logger.warn(`未找到活动: ${params.event.eventName},将不参与活动`);
21155
+ await page.locator(".post-activity-wrap .activity-display").click();
21156
+ await page.waitForTimeout(300);
21157
+ }
21158
+ }
21159
+ if (params.scheduledTime) {
20696
21160
  task.logger.info("设置定时发布");
20697
21161
  const timingRadio = page.locator(".weui-desktop-form__check-label").filter({
20698
21162
  hasNotText: "不定时"
@@ -20701,10 +21165,10 @@ var __webpack_exports__ = {};
20701
21165
  await page.waitForTimeout(500);
20702
21166
  const instance = page.locator(".weui-desktop-picker__date");
20703
21167
  await instance.click();
20704
- const dateD = utils_TimeFormatter.format(1000 * params.scheduleTime, "d");
21168
+ const dateD = utils_TimeFormatter.format(1000 * params.scheduledTime, "d");
20705
21169
  const nowMonth = utils_TimeFormatter.format(Date.now(), "MM月");
20706
21170
  const nowMonthText = utils_TimeFormatter.format(Date.now(), "M月");
20707
- const month = utils_TimeFormatter.format(1000 * params.scheduleTime, "MM月");
21171
+ const month = utils_TimeFormatter.format(1000 * params.scheduledTime, "MM月");
20708
21172
  const monthLocator = await page.locator("weui-desktop-picker__panel__label").filter({
20709
21173
  hasText: month
20710
21174
  }).first();
@@ -20721,14 +21185,85 @@ var __webpack_exports__ = {};
20721
21185
  await page.locator(".weui-desktop-picker__table-row td a").filter({
20722
21186
  hasText: dateD
20723
21187
  }).first().click();
20724
- await page.locator(".weui-desktop-form__input-wrp input[placeholder*='请选择时间']").fill(utils_TimeFormatter.format(1000 * params.scheduleTime, "hh:mm"));
21188
+ await page.locator(".weui-desktop-form__input-wrp input[placeholder*='请选择时间']").fill(utils_TimeFormatter.format(1000 * params.scheduledTime, "hh:mm"));
20725
21189
  await page.locator("i.weui-desktop-icon__time").click();
20726
21190
  await page.locator(".post-time-wrap .form-item .label").filter({
20727
21191
  hasText: "发表时间"
20728
21192
  }).click();
20729
21193
  }
21194
+ if (params.tagInfo) {
21195
+ task.logger.info(`设置视频标注: tagType=${params.tagInfo.tagType}`);
21196
+ await page.locator(".mark-tag-select").click();
21197
+ await page.waitForTimeout(300);
21198
+ const tagTypeTextMap = {
21199
+ 0: "无需标注",
21200
+ 1: "含AI生成内容",
21201
+ 2: "内容包含营销广告",
21202
+ 3: "内容为虚构剧情,仅供娱乐",
21203
+ 5: "内容为自行拍摄",
21204
+ 7: "内容为转载",
21205
+ 8: "个人观点,仅供参考"
21206
+ };
21207
+ const tagText = tagTypeTextMap[params.tagInfo.tagType];
21208
+ if (tagText) {
21209
+ await page.locator(".mark-tag-option .option-main").filter({
21210
+ hasText: tagText
21211
+ }).click();
21212
+ await page.waitForTimeout(300);
21213
+ if (5 === params.tagInfo.tagType) {
21214
+ const shootInfo = params.tagInfo.shootInfo;
21215
+ if (shootInfo) {
21216
+ task.logger.info("填写拍摄时间和地点...");
21217
+ await page.waitForTimeout(500);
21218
+ if (shootInfo.postTimestamp) {
21219
+ task.logger.info(`设置拍摄时间: ${shootInfo.postTimestamp}`);
21220
+ const timestamp = 1000 * parseInt(shootInfo.postTimestamp, 10);
21221
+ const date = new Date(timestamp);
21222
+ await page.locator(".original-dialog-content .weui-desktop-picker__date input[placeholder*='请选择拍摄时间']").click();
21223
+ await page.waitForTimeout(300);
21224
+ const dayNum = date.getDate();
21225
+ await page.locator(".weui-desktop-picker__table a").filter({
21226
+ hasText: new RegExp(`^\\s*${dayNum}\\s*$`)
21227
+ }).first().click();
21228
+ await page.waitForTimeout(300);
21229
+ }
21230
+ if (shootInfo.countryCode || shootInfo.provinceCode || shootInfo.cityCode) {
21231
+ task.logger.info("设置拍摄地点...");
21232
+ await page.locator(".original-dialog-content .weui-desktop-form__dropdowncascade .weui-desktop-form__dropdowncascade__dt").click();
21233
+ await page.waitForTimeout(300);
21234
+ if (1156 === shootInfo.countryCode) {
21235
+ await page.locator(".weui-desktop-dropdown__list-ele .weui-desktop-dropdown__list-ele__text").filter({
21236
+ hasText: "中国"
21237
+ }).click();
21238
+ await page.waitForTimeout(300);
21239
+ task.logger.warn("RPA 模式下暂不支持选择具体省份和城市,仅选择了国家");
21240
+ } else task.logger.warn(`不支持的国家代码: ${shootInfo.countryCode},跳过地点设置`);
21241
+ }
21242
+ const confirmBtn = await waitForElement(".weui-desktop-dialog .weui-desktop-btn_primary");
21243
+ if (confirmBtn) {
21244
+ await confirmBtn.click();
21245
+ await page.waitForTimeout(300);
21246
+ }
21247
+ } else task.logger.warn("tagType=5 需要提供 shootInfo 字段(拍摄时间和地点)");
21248
+ }
21249
+ if (7 === params.tagInfo.tagType) {
21250
+ const repostSource = params.tagInfo.repostSource;
21251
+ if (repostSource) {
21252
+ task.logger.info(`填写转载来源: ${repostSource}`);
21253
+ await page.waitForTimeout(500);
21254
+ await page.locator(".repost-dialog-content .repost-textarea").fill(repostSource);
21255
+ await page.waitForTimeout(300);
21256
+ const confirmBtn = await waitForElement(".weui-desktop-dialog .weui-desktop-btn_primary");
21257
+ if (confirmBtn) {
21258
+ await confirmBtn.click();
21259
+ await page.waitForTimeout(300);
21260
+ }
21261
+ } else task.logger.warn("tagType=7 需要提供 repostSource 字段(转载来源)");
21262
+ }
21263
+ } else task.logger.warn(`未知的 tagType: ${params.tagInfo.tagType},跳过标注设置`);
21264
+ }
20730
21265
  task.logger.info("准备发布...");
20731
- await page.waitForTimeout(300);
21266
+ await page.waitForTimeout(500);
20732
21267
  let videoId = "";
20733
21268
  const handleResponse = async (response)=>{
20734
21269
  const url = response.url();
@@ -20794,19 +21329,60 @@ var __webpack_exports__ = {};
20794
21329
  };
20795
21330
  const ShipinhaoPublishVideoParamsSchema = ActionCommonParamsSchema.extend({
20796
21331
  videoPath: schemas_string().min(1),
21332
+ videoMetadata: schemas_object({
21333
+ duration: schemas_number().positive(),
21334
+ width: schemas_number().int().positive(),
21335
+ height: schemas_number().int().positive(),
21336
+ fileSize: schemas_number().int().positive(),
21337
+ path: schemas_string().min(1).optional(),
21338
+ fileName: schemas_string().min(1)
21339
+ }),
20797
21340
  coverPath: schemas_string().min(1),
21341
+ verticalCoverPath: schemas_string().min(1).optional(),
20798
21342
  description: schemas_string(),
20799
21343
  title: schemas_string().optional(),
20800
- scheduleTime: schemas_number().int().positive().optional(),
21344
+ scheduledTime: schemas_number().int().positive().optional(),
21345
+ isImmediatelyPublish: schemas_boolean().optional(),
21346
+ topics: schemas_array(schemas_string()).optional(),
21347
+ mentionedUsers: schemas_array(schemas_object({
21348
+ nickname: schemas_string()
21349
+ })).optional(),
21350
+ collection: schemas_object({
21351
+ collectionId: schemas_string(),
21352
+ collectionName: schemas_string()
21353
+ }).optional(),
21354
+ event: schemas_object({
21355
+ eventTopicId: schemas_string(),
21356
+ eventName: schemas_string(),
21357
+ eventCreatorNickname: schemas_string().optional()
21358
+ }).optional(),
21359
+ link: schemas_object({
21360
+ link: schemas_string(),
21361
+ title: schemas_string(),
21362
+ urlType: schemas_number().int().default(1)
21363
+ }).optional(),
21364
+ tagInfo: looseObject({
21365
+ tagType: schemas_number().int()
21366
+ }).optional(),
21367
+ originalFlag: union([
21368
+ literal(0),
21369
+ literal(1)
21370
+ ]).optional(),
21371
+ postWithMemberZoneLink: union([
21372
+ literal(0),
21373
+ literal(1)
21374
+ ]).optional(),
20801
21375
  location: schemas_object({
20802
21376
  latitude: schemas_number(),
20803
21377
  longitude: schemas_number(),
20804
21378
  city: schemas_string(),
21379
+ poiName: schemas_string().optional(),
21380
+ address: schemas_string().optional(),
20805
21381
  poiClassifyId: schemas_string().optional()
20806
21382
  }).optional()
20807
21383
  });
20808
21384
  const shipinhaoPublishVideo = async (task, params)=>{
20809
- task.logger.info(`shipinhaoPublishVideo actionType: ${params.actionType}`);
21385
+ task.logger.info(`[shipinhaoPublishVideo] actionType: ${params.actionType}`);
20810
21386
  if ("rpa" === params.actionType) return shipinhaoPublishVideo_rpa_rpaAction(task, params);
20811
21387
  if ("mockApi" === params.actionType) return shipinhaoPublishVideo_mock_mockAction(task, params);
20812
21388
  return executeAction(shipinhaoPublishVideo_mock_mockAction, shipinhaoPublishVideo_rpa_rpaAction)(task, params);
@@ -26908,4 +27484,4 @@ if (__webpack_exports__.__esModule) Object.defineProperty(__webpack_export_targe
26908
27484
  });
26909
27485
 
26910
27486
  //# sourceMappingURL=index.js.map
26911
- //# debugId=1de33b63-4038-5f51-813c-0899b138a5ca
27487
+ //# debugId=028a0eb4-9179-5708-bae0-75174165ae68