feat(wangyihao): record public article URL on publish and back-fill responses

Build https://www.163.com/dy/article/<docId>.html when wangyihao tasks finish, and normalize publish-record responses + monitoring alias inputs to surface the same public URL when the doc id is known.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-30 20:55:53 +08:00
parent add918ff4c
commit cdd63db7f1
8 changed files with 208 additions and 64 deletions
@@ -0,0 +1,6 @@
const WANGYI_PUBLIC_ARTICLE_URL_PREFIX = "https://www.163.com/dy/article";
export function wangyihaoPublicArticleUrl(articleId: string): string | null {
const normalized = articleId.trim();
return normalized ? `${WANGYI_PUBLIC_ARTICLE_URL_PREFIX}/${encodeURIComponent(normalized)}.html` : null;
}
@@ -1,6 +1,7 @@
import { describe, expect, it } from "vitest";
import { prepareWangyihaoArticleHtml } from "./wangyihao-content";
import { wangyihaoPublicArticleUrl } from "./wangyihao-links";
describe("wangyihao article content", () => {
it("downgrades markdown tables to paragraph rows before saving drafts", () => {
@@ -24,3 +25,15 @@ describe("wangyihao article content", () => {
expect(html).toContain("<p>下一段中文说明</p>");
});
});
describe("wangyihao publish result", () => {
it("builds the public article URL from the NetEase doc id", () => {
expect(wangyihaoPublicArticleUrl(" IK5K461I05561G2M ")).toBe(
"https://www.163.com/dy/article/IK5K461I05561G2M.html",
);
});
it("does not build an external URL without a doc id", () => {
expect(wangyihaoPublicArticleUrl(" ")).toBeNull();
});
});
@@ -10,6 +10,7 @@ import {
import { fetchImageAssetBlob } from "./media-image";
import type { PublishAdapter, PublishAdapterContext } from "./base";
import { prepareWangyihaoArticleHtml } from "./wangyihao-content";
import { wangyihaoPublicArticleUrl } from "./wangyihao-links";
import {
WANGYIHAO_DRAFT_SAVE_PARAMETER_MESSAGE,
WANGYIHAO_PUBLISH_CLICK_MESSAGE,
@@ -820,7 +821,7 @@ function buildResultPayload(articleId: string, mediaName: string): Record<string
media_name: mediaName,
external_article_id: articleId,
external_manage_url: WANGYI_MANAGE_URL,
external_article_url: null,
external_article_url: wangyihaoPublicArticleUrl(articleId),
publish_type: "publish",
};
}
@@ -950,7 +951,7 @@ export const wangyihaoAdapter: PublishAdapter = {
return {
status: "succeeded",
summary: "网易号发布成功。",
summary: "网易号发布成功,已记录公开链接。",
payload: buildResultPayload(draftId, account.name),
};
} catch (error) {
@@ -2,8 +2,8 @@
import {
ClockCircleOutlined,
CopyOutlined,
DesktopOutlined,
ExportOutlined,
LinkOutlined,
ReloadOutlined,
SearchOutlined,
SendOutlined,
@@ -153,6 +153,19 @@ function buildSohuArticleUrl(task: DesktopTaskInfo, result: Record<string, JsonV
return `https://www.sohu.com/a/${encodeURIComponent(articleId)}_${encodeURIComponent(accountUid)}`;
}
function buildWangyihaoArticleUrl(task: DesktopTaskInfo, result: Record<string, JsonValue>): string | null {
if (task.platform !== "wangyihao" || extractString(result.publish_type) === "draft") {
return null;
}
const articleId = extractStringId(result.external_article_id);
if (!articleId) {
return null;
}
return `https://www.163.com/dy/article/${encodeURIComponent(articleId)}.html`;
}
function normalizePublishTaskStatus(status: DesktopTaskInfo["status"]): DesktopTaskInfo["status"] {
return status === "unknown" ? "failed" : status;
}
@@ -208,13 +221,6 @@ function summaryForTask(
}
}
function manageLinkLabel(task: PublishTaskItem): string {
if (task.publishType === "draft" && task.platform === "weixin_gzh") {
return "打开草稿箱";
}
return "打开管理页";
}
function parseTimestamp(value: string | null): number {
if (!value) {
return Date.now();
@@ -432,7 +438,7 @@ const publishTasks = computed<PublishTaskItem[]>(() =>
updatedAt: parseTimestamp(task.updated_at),
externalArticleUrl: publishType === "draft"
? null
: extractString(result.external_article_url) ?? buildSohuArticleUrl(task, result),
: extractString(result.external_article_url) ?? buildWangyihaoArticleUrl(task, result) ?? buildSohuArticleUrl(task, result),
externalManageUrl: extractString(result.external_manage_url),
publishType,
fallbackReason,
@@ -455,7 +461,7 @@ const tableColumns = [
{ title: "状态", key: "status", dataIndex: "status", width: 96 },
{ title: "任务信息", key: "meta", dataIndex: "meta", width: 280 },
{ title: "时间", key: "time", dataIndex: "time", width: 132 },
{ title: "", key: "actions", align: "right" as const, width: 64, fixed: "right" as const },
{ title: "操作", key: "actions", align: "right" as const, width: 132, fixed: "right" as const },
];
const tableScroll = { x: 1000 } as const;
@@ -604,37 +610,6 @@ const tableScroll = { x: 1000 } as const;
<span class="meta-divider">·</span>
<span>租约 {{ formatRelativeTime(record.leaseExpiresAt) }} 到期</span>
</template>
<template v-else-if="record.externalArticleUrl">
<span class="meta-divider">·</span>
<a-tooltip :title="record.externalArticleUrl" overlay-class-name="external-url-tooltip">
<a
class="external-task-link"
:href="record.externalArticleUrl"
target="_blank"
rel="noopener noreferrer"
@click.prevent.stop="openExternalUrl(record.externalArticleUrl)"
>
<ExportOutlined />
打开外链
</a>
</a-tooltip>
</template>
<template v-else-if="record.externalManageUrl">
<span class="meta-divider">·</span>
<a-tooltip :title="`打开 ${record.accountName} 的${translatePlatform(record.platform)}工作台`" overlay-class-name="external-url-tooltip">
<a
class="external-task-link"
:class="{ 'is-loading': consolePendingTaskId === record.id }"
href="#"
rel="noopener noreferrer"
:aria-busy="consolePendingTaskId === record.id"
@click.prevent.stop="openAccountWorkbench(record)"
>
<LinkOutlined />
{{ manageLinkLabel(record) }}
</a>
</a-tooltip>
</template>
</span>
</div>
</template>
@@ -653,6 +628,32 @@ const tableScroll = { x: 1000 } as const;
<template v-else-if="column.key === 'actions'">
<div class="action-buttons">
<a-tooltip
v-if="record.externalArticleUrl"
:title="record.externalArticleUrl"
placement="top"
overlay-class-name="external-url-tooltip"
>
<a-button
type="text"
class="icon-action-btn"
:aria-label="`打开外链:${record.title}`"
@click.stop="openExternalUrl(record.externalArticleUrl)"
>
<template #icon><ExportOutlined /></template>
</a-button>
</a-tooltip>
<a-tooltip :title="`打开 ${record.accountName} 的${translatePlatform(record.platform)}工作台`" placement="top">
<a-button
type="text"
class="icon-action-btn"
:loading="consolePendingTaskId === record.id"
:aria-label="`打开 ${record.accountName} ${translatePlatform(record.platform)}工作台`"
@click.stop="openAccountWorkbench(record)"
>
<template #icon><DesktopOutlined /></template>
</a-button>
</a-tooltip>
<a-popconfirm
v-if="!isPendingStatus(record.status)"
title="确定要重试本次发布吗?"