feat: ship self-hosted KOC LOOP workflows
This commit is contained in:
@@ -225,7 +225,7 @@ async function createMcpSession(
|
||||
timeoutMs,
|
||||
);
|
||||
const sessionId = initialize.response.headers.get("mcp-session-id");
|
||||
if (!sessionId) throw new Error("MCP采集服务未返回会话标识");
|
||||
if (!sessionId) return undefined;
|
||||
|
||||
await postMcp(
|
||||
fetchImpl,
|
||||
@@ -242,10 +242,10 @@ async function createMcpSession(
|
||||
return sessionId;
|
||||
}
|
||||
|
||||
async function callMcpTool(
|
||||
async function invokeMcpTool(
|
||||
fetchImpl: typeof fetch,
|
||||
endpoint: string,
|
||||
sessionId: string,
|
||||
sessionId: string | undefined,
|
||||
timeoutMs: number,
|
||||
name: string,
|
||||
args: Record<string, unknown>,
|
||||
@@ -272,6 +272,12 @@ async function callMcpTool(
|
||||
try {
|
||||
payload = JSON.parse(text);
|
||||
} catch {
|
||||
if (result.envelope?.result?.isError === true) {
|
||||
return {
|
||||
isError: true,
|
||||
payload: { message: text },
|
||||
};
|
||||
}
|
||||
throw new Error("MCP采集工具返回了无法解析的数据");
|
||||
}
|
||||
return {
|
||||
@@ -280,6 +286,40 @@ async function callMcpTool(
|
||||
};
|
||||
}
|
||||
|
||||
function isToolArgumentShapeError(result: ToolResult) {
|
||||
if (!result.isError) return false;
|
||||
const root = asRecord(result.payload);
|
||||
const message = String(root?.message ?? "");
|
||||
return /input validation error/i.test(message);
|
||||
}
|
||||
|
||||
async function callMcpTool(
|
||||
fetchImpl: typeof fetch,
|
||||
endpoint: string,
|
||||
sessionId: string | undefined,
|
||||
timeoutMs: number,
|
||||
name: string,
|
||||
args: Record<string, unknown>,
|
||||
): Promise<ToolResult> {
|
||||
const nested = await invokeMcpTool(
|
||||
fetchImpl,
|
||||
endpoint,
|
||||
sessionId,
|
||||
timeoutMs,
|
||||
name,
|
||||
{ request: args },
|
||||
);
|
||||
if (!isToolArgumentShapeError(nested)) return nested;
|
||||
return invokeMcpTool(
|
||||
fetchImpl,
|
||||
endpoint,
|
||||
sessionId,
|
||||
timeoutMs,
|
||||
name,
|
||||
args,
|
||||
);
|
||||
}
|
||||
|
||||
function metricsFromToolResult(result: ToolResult): XhsPublicMetrics {
|
||||
const root = asRecord(result.payload);
|
||||
const response = asRecord(root?.response) ?? root;
|
||||
@@ -579,7 +619,7 @@ export async function resolveXhsPublicAccountDetails(
|
||||
try {
|
||||
parsed = new URL(profileUrl);
|
||||
} catch {
|
||||
return { redId: "", followers: null, ipLocation: "" };
|
||||
return { nickname: "", redId: "", followers: null, ipLocation: "" };
|
||||
}
|
||||
if (
|
||||
parsed.protocol !== "https:" ||
|
||||
@@ -589,7 +629,7 @@ export async function resolveXhsPublicAccountDetails(
|
||||
) ||
|
||||
!parsed.pathname.startsWith("/user/profile/")
|
||||
) {
|
||||
return { redId: "", followers: null, ipLocation: "" };
|
||||
return { nickname: "", redId: "", followers: null, ipLocation: "" };
|
||||
}
|
||||
try {
|
||||
const response = await fetchImpl(parsed.toString(), {
|
||||
@@ -602,10 +642,12 @@ export async function resolveXhsPublicAccountDetails(
|
||||
},
|
||||
});
|
||||
if (!response.ok) {
|
||||
return { redId: "", followers: null, ipLocation: "" };
|
||||
return { nickname: "", redId: "", followers: null, ipLocation: "" };
|
||||
}
|
||||
const html = await response.text();
|
||||
return {
|
||||
nickname:
|
||||
html.match(/"(?:nickname|nickName)":"([^"]+)"/)?.[1] ?? "",
|
||||
redId:
|
||||
html.match(/"(?:redId|red_id)":"([^"]+)"/)?.[1] ??
|
||||
html.match(/小红书号[::]\s*([^<"\s]+)/)?.[1] ??
|
||||
@@ -620,7 +662,7 @@ export async function resolveXhsPublicAccountDetails(
|
||||
ipLocation: "",
|
||||
};
|
||||
} catch {
|
||||
return { redId: "", followers: null, ipLocation: "" };
|
||||
return { nickname: "", redId: "", followers: null, ipLocation: "" };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -650,6 +692,9 @@ function profileDetailsFromToolResult(result: ToolResult) {
|
||||
const payload =
|
||||
asRecord(response?.data) ?? asRecord(root?.data) ?? result.payload;
|
||||
const followers = followerCountFromPayload(payload);
|
||||
const nickname =
|
||||
findStringByKey(payload, "nickname") ||
|
||||
findStringByKey(payload, "nickName");
|
||||
const redId =
|
||||
findStringByKey(payload, "red_id") ||
|
||||
findStringByKey(payload, "redId") ||
|
||||
@@ -658,10 +703,10 @@ function profileDetailsFromToolResult(result: ToolResult) {
|
||||
const ipLocation =
|
||||
findStringByKey(payload, "ip_location") ||
|
||||
findStringByKey(payload, "ipLocation");
|
||||
if (followers === null && !redId && !ipLocation) {
|
||||
if (followers === null && !nickname && !redId && !ipLocation) {
|
||||
throw new Error("账号主页采集结果缺少可用字段");
|
||||
}
|
||||
return { followers, redId, ipLocation };
|
||||
return { nickname, followers, redId, ipLocation };
|
||||
}
|
||||
|
||||
function accountProfileFromToolResult(
|
||||
@@ -717,7 +762,7 @@ async function completeAccountProfile(
|
||||
profile: XhsAccountProfile,
|
||||
fetchImpl: typeof fetch,
|
||||
endpoint: string,
|
||||
sessionId: string,
|
||||
sessionId: string | undefined,
|
||||
timeoutMs: number,
|
||||
) {
|
||||
let completed = profile;
|
||||
|
||||
Reference in New Issue
Block a user