Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 14 additions & 7 deletions src/Providers/xProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import { CommandsType } from "../../types/commands";
import { xAJAX, xItem, xUser } from "../../types/x";
import { BaseWebviewProvider, IncomingMessage } from "./baseWebviewProvider";
import { downloadImageWithSaveDialog } from "../utils/imageDownload";
import { getXhsLikedNotes } from "../api/xhs";

// X 转换器
function mapXTweetToXItem(tweet: any): xItem | null {
Expand Down Expand Up @@ -344,8 +345,8 @@ export class XProvider extends BaseWebviewProvider {
isRefresh = !!payload.refresh;
seenTweetIds = Array.isArray(payload.seenTweetIds)
? payload.seenTweetIds
.map((id: unknown) => String(id))
.filter(Boolean)
.map((id: unknown) => String(id))
.filter(Boolean)
: [];
if (isNextPage) {
this.currentCursor =
Expand Down Expand Up @@ -455,7 +456,7 @@ export class XProvider extends BaseWebviewProvider {

if (res.code === 0 && res.data) {
const userResult = res.data.user?.result;

let finalUser = userResult;
// 严格按照 JSON 路径:data.user.result.timeline.timeline.instructions
if (!finalUser?.legacy) {
Expand Down Expand Up @@ -491,9 +492,9 @@ export class XProvider extends BaseWebviewProvider {
id: finalUser.rest_id || userId,
name: legacy.name || (finalUser as any).core?.name || "",
screen_name_raw: legacy.screen_name || (finalUser as any).core?.screen_name || "",
screen_name: (legacy.name || (finalUser as any).core?.name)
? `${legacy.name || (finalUser as any).core?.name} (@${legacy.screen_name || (finalUser as any).core?.screen_name})`
: (legacy.screen_name || (finalUser as any).core?.screen_name),
screen_name: (legacy.name || (finalUser as any).core?.name)
? `${legacy.name || (finalUser as any).core?.name} (@${legacy.screen_name || (finalUser as any).core?.screen_name})`
: (legacy.screen_name || (finalUser as any).core?.screen_name),
avatar_hd: avatarUrl?.replace(/_normal/, "_400x400"),
followers_count_str: String(legacy.followers_count || 0),
friends_count_str: String(legacy.friends_count || 0),
Expand All @@ -503,7 +504,7 @@ export class XProvider extends BaseWebviewProvider {
},
};
} else {
mappedData = { ok: 1, data: { id: userId, name: "我的个人页面", isOwner: true } };
mappedData = { ok: 1, data: { id: userId, name: "我的个人页面", isOwner: true } };
}
}

Expand Down Expand Up @@ -921,6 +922,12 @@ export class XProvider extends BaseWebviewProvider {
});
break;
}
case "XHS_GET_LIKED_NOTES": {
const likedPayload = payload as any;
const data = await getXhsLikedNotes(likedPayload);
webviewView.webview.postMessage({ payload: data, uuid });
break;
}

// Fallback handlers to avoid errors
default: {
Expand Down
7 changes: 7 additions & 0 deletions src/Providers/xhsWebProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import {
getXhsUserMe,
uploadXhsImage,
publishXhsNote,
getXhsLikedNotes,
} from "../api/xhs";
import { BaseWebviewProvider, IncomingMessage } from "./baseWebviewProvider";

Expand Down Expand Up @@ -214,6 +215,12 @@ export class XhsWebProvider extends BaseWebviewProvider {
webviewView.webview.postMessage({ payload: result, uuid });
break;
}
case "XHS_GET_LIKED_NOTES": {
const likedPayload = payload as any;
const data = await getXhsLikedNotes(likedPayload);
webviewView.webview.postMessage({ payload: data, uuid });
break;
}
case "XHS_DOWNLOAD_IMAGE": {
const { url, fileName } = (payload || {}) as {
url: string;
Expand Down
50 changes: 50 additions & 0 deletions src/api/xhs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import * as vscode from "vscode";
import xhsHttp from "../core/xhsHttp";
import { getXhsSignature, XhsSignature } from "../utils/signature";
import * as crypto from "crypto";
import type { XhsGetLikedNotesParams } from "../../types/xhs";

// 稳定序列化,确保签名与发送体 key 顺序一致(避免后端校验差异)
function stableStringify(value: any): string {
Expand Down Expand Up @@ -1271,3 +1272,52 @@ export const publishXhsNote = async (params: {
msg: "发布成功",
};
};

export const getXhsLikedNotes = async (params: XhsGetLikedNotesParams) => {
const cookie = await getOrSetXhsCookie();
if (!cookie) throw new Error("请先设置小红书 Cookie");
const apiPath = "/api/sns/web/v1/note/like/page";
const queryObj = {
num: params.num || 30,
cursor: params.cursor || '',
user_id: params.user_id,
image_formats: "jpg,webp,avif",
xsec_token: '',
xsec_source: '',
};
const pathWithQuery = buildGetPath(apiPath, queryObj);
let signObj: XhsSignature;
try {
signObj = await getXhsSignature(pathWithQuery, "", cookie, "GET");
} catch (e: any) {
throw new Error(`小红书签名生成失败: ${e?.message || "请检查 Cookie"}`);
}
const url = buildGetUrl(pathWithQuery);
const headers = buildXhsHeaders({ cookie, signObj });
const resp = await xhsHttp.get(url, { headers, timeout: 10000 });
const raw = resp.data?.data;
const notes: any[] = raw?.notes || [];
const items = notes.map((note: any) => ({
ignore: false,
xsec_token: note.xsec_token,
id: note.note_id || note.id,
model_type: "note",
track_id: "",
note_card: {
user: note.user,
interact_info: note.interact_info,
cover: note.cover,
display_title: note.display_title || note.title,
title: note.display_title || note.title,
type: note.type,
note_id: note.note_id || note.id,
xsec_token: note.xsec_token,
},
}));
return {
items,
cursor: raw?.cursor || "",
has_more: raw?.has_more,
_raw: raw,
};
};
1 change: 1 addition & 0 deletions types/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ export type CommandList =
| "SEND_TRANSLATION"
| "X_DOWNLOAD_IMAGE"
// XHS commands
| "XHS_GET_LIKED_NOTES"
| "XHS_GET_HOME_FEED"
| "XHS_SEARCH"
| "XHS_FEED_DETAIL"
Expand Down
27 changes: 20 additions & 7 deletions types/xhs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@
* @LastEditTime: 2025-11-20 09:22:50
* @LastEditors: YangLiwei 1280426581@qq.com
* @FilePath: \touchfish\types\xhs.ts
* Copyright (c) 2025 by YangLiwei, All Rights Reserved.
* @Description:
* Copyright (c) 2025 by YangLiwei, All Rights Reserved.
* @Description:
*/
// 小红书 XHS 相关类型
export interface XhsCoverInfo {
Expand Down Expand Up @@ -96,7 +96,7 @@ export interface XhsNoteVideo {
thumbnail_fileid: string;
[key: string]: any;
};
consumer: { origin_video_key: string; [key: string]: any };
consumer: { origin_video_key: string;[key: string]: any };
[key: string]: any;
}
export interface XhsImageListItem {
Expand Down Expand Up @@ -266,13 +266,13 @@ export interface XhsUnfollowResponse {
[key: string]: any;
}
export interface XhsLikeNoteParams { note_oid: string }
export interface XhsLikeNoteResponse { new_like: boolean; [k: string]: any }
export interface XhsLikeNoteResponse { new_like: boolean;[k: string]: any }
export interface XhsDislikeNoteParams { note_oid: string }
export interface XhsDislikeNoteResponse { like_count: number; [k: string]: any }
export interface XhsDislikeNoteResponse { like_count: number;[k: string]: any }
export interface XhsCollectNoteParams { note_id: string }
export interface XhsCollectNoteResponse { code: number; success: boolean; msg: string; [k: string]: any }
export interface XhsCollectNoteResponse { code: number; success: boolean; msg: string;[k: string]: any }
export interface XhsUncollectNoteParams { note_ids: string }
export interface XhsUncollectNoteResponse { code: number; success: boolean; msg: string; [k: string]: any }
export interface XhsUncollectNoteResponse { code: number; success: boolean; msg: string;[k: string]: any }
export interface XhsPostCommentParams { note_id: string; content: string; at_users?: any[] }
export interface XhsPostCommentResponse {
code: number;
Expand Down Expand Up @@ -421,3 +421,16 @@ export interface XhsPublishNoteResponse {
msg?: string;
[k: string]: any;
}

// touchFish/types/xhs.ts (在文件末尾追加)
export interface XhsGetLikedNotesParams {
user_id: string;
cursor?: string;
num?: number;
}
export interface XhsGetLikedNotesResponse {
cursor: string;
has_more: boolean;
notes: any[];
[key: string]: any;
}
12 changes: 11 additions & 1 deletion xhs/src/api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ type RequestFunc = <T = any>(
) => Promise<T>;

export class XhsApi {
constructor(private request: RequestFunc) {}
constructor(private request: RequestFunc) { }

/**
* 获取首页推荐Feed
Expand Down Expand Up @@ -215,6 +215,16 @@ export class XhsApi {
);
}

// touchFish/xhs/src/api/index.ts
/** 获取历史点赞记录 */
getLikedNotes(payload: { user_id: string; cursor?: string; num?: number }) {
return this.request<any>(
"XHS_GET_LIKED_NOTES" as CommandList,
payload,
payload.cursor ? "加载更多点赞记录..." : "加载点赞记录中..."
);
}

/**
* 上传图片(用于发布笔记)
*/
Expand Down
50 changes: 50 additions & 0 deletions xhs/src/components/Feed.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import {
FormOutlined,
EyeOutlined,
EyeInvisibleOutlined,
HeartOutlined,
} from "@ant-design/icons";
import useXhsFeed from "../hooks/useXhsFeed";
import { loaderFunc } from "../utils/loader";
Expand All @@ -35,6 +36,7 @@ import {
import FeedDetailDrawer from "./FeedDetailDrawer";
import XhsSearchDrawer from "./XhsSearchDrawer";
import UserPostedDrawer from "./UserPostedDrawer";
import UserLikedDrawer from "./UserLikedDrawer";
import XhsSendDrawer from "./XhsSendDrawer";
import { useRequest } from "../hooks/useRequest";
import { createXhsApi } from "../api";
Expand All @@ -53,6 +55,9 @@ export default function Feed() {
const [userOpen, setUserOpen] = useState(false);
// 发布抽屉状态
const [sendDrawerOpen, setSendDrawerOpen] = useState(false);
// 点赞抽屉状态
const [likedDrawerOpen, setLikedDrawerOpen] = useState(false);
const [likedUserId, setLikedUserId] = useState<string>("");
const [sendLoading, setSendLoading] = useState(false);
const [userParams, setUserParams] = useState<{
cursor: string;
Expand Down Expand Up @@ -150,6 +155,20 @@ export default function Feed() {
setUserOpen(true);
}, []);

const handleOpenLiked = useCallback(async () => {
try {
const data: any = await apiRef.current.getMyUserInfo();
if (data && data.user_id) {
setLikedUserId(data.user_id);
setLikedDrawerOpen(true);
} else {
messageApi.error("获取用户信息失败");
}
} catch (e: any) {
messageApi.error(e.message || "获取用户信息失败");
}
}, [messageApi]);

// 处理图片上传
const handleUploadImage = useCallback(
async (file: File) => {
Expand Down Expand Up @@ -204,6 +223,26 @@ export default function Feed() {
},
[messageApi],
);
// ===== 新增:向宿主环境发送点赞/取消点赞命令 =====
const handleLikeToggle = useCallback(async (raw: any, targetStatus: boolean) => {
if (!raw?.id) return false;
try {
// 【关键修正】这里依据 api/xhs.ts 要求,参数必须名为 note_oid
const payload = {
note_oid: raw.id
};

if (targetStatus) {
await request("XHS_NOTE_LIKE", payload);
} else {
await request("XHS_NOTE_DISLIKE", payload);
}
return true;
} catch (e: any) {
messageApi.error(e.message || "点赞操作失败");
return false;
}
}, [request, messageApi]);

return (
<div
Expand Down Expand Up @@ -246,6 +285,11 @@ export default function Feed() {
icon={<UserOutlined style={{ color: "#faad14" }} />}
tooltip={{ title: "我的", placement: "left" }}
/>
<FloatButton
onClick={handleOpenLiked}
icon={<HeartOutlined style={{ color: "#ff2442" }} />}
tooltip={{ title: "我的点赞", placement: "left" }}
/>
<FloatButton
onClick={() => setSendDrawerOpen(true)}
icon={<FormOutlined style={{ color: "#52c41a" }} />}
Expand Down Expand Up @@ -307,6 +351,7 @@ export default function Feed() {
data={raw}
onClick={handleOpenDetail}
onUserClick={handleOpenUser}
onLikeToggle={handleLikeToggle}
/>
</div>
))}
Expand All @@ -318,6 +363,11 @@ export default function Feed() {
onClose={() => setUserOpen(false)}
initParams={userParams as any}
/>
<UserLikedDrawer
userId={likedUserId}
visible={likedDrawerOpen}
onClose={() => setLikedDrawerOpen(false)}
/>
<XhsSendDrawer
open={sendDrawerOpen}
onClose={() => setSendDrawerOpen(false)}
Expand Down
Loading