Initial commit: KOC LOOP platform
This commit is contained in:
16
tests/date-utils.test.mjs
Normal file
16
tests/date-utils.test.mjs
Normal file
@@ -0,0 +1,16 @@
|
||||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
import {
|
||||
formatShanghaiDate,
|
||||
parseStoredDate,
|
||||
} from "../lib/date-utils.ts";
|
||||
|
||||
test("treats D1 CURRENT_TIMESTAMP values as UTC and displays Beijing time", () => {
|
||||
const stored = "2026-07-29 05:36:00";
|
||||
assert.equal(parseStoredDate(stored).toISOString(), "2026-07-29T05:36:00.000Z");
|
||||
assert.match(formatShanghaiDate(stored, true), /07\/29.*13:36/);
|
||||
});
|
||||
|
||||
test("keeps date-only deadlines on the intended Shanghai calendar date", () => {
|
||||
assert.match(formatShanghaiDate("2026-08-12"), /08\/12/);
|
||||
});
|
||||
177
tests/feishu-client.test.mjs
Normal file
177
tests/feishu-client.test.mjs
Normal file
@@ -0,0 +1,177 @@
|
||||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
import {
|
||||
clearFeishuAccessTokenCacheForTests,
|
||||
readFeishuSource,
|
||||
} from "../lib/feishu-client.ts";
|
||||
|
||||
const bindings = {
|
||||
FEISHU_APP_ID: "cli_test",
|
||||
FEISHU_APP_SECRET: "secret_test",
|
||||
};
|
||||
|
||||
function apiResponse(data) {
|
||||
return Response.json({ code: 0, msg: "success", data });
|
||||
}
|
||||
|
||||
function fakeFeishu(options = {}) {
|
||||
const calls = [];
|
||||
const fetchImpl = async (input, init = {}) => {
|
||||
const url = new URL(String(input));
|
||||
calls.push({ url, init });
|
||||
if (url.pathname.endsWith("/tenant_access_token/internal")) {
|
||||
return Response.json({
|
||||
code: 0,
|
||||
msg: "success",
|
||||
tenant_access_token: "tenant-test",
|
||||
expire: 7200,
|
||||
});
|
||||
}
|
||||
if (url.pathname.endsWith("/wiki/v2/spaces/get_node")) {
|
||||
return apiResponse({
|
||||
node: {
|
||||
obj_type: "sheet",
|
||||
obj_token: "spreadsheet-test",
|
||||
},
|
||||
});
|
||||
}
|
||||
if (url.pathname.endsWith("/sheets/query")) {
|
||||
return apiResponse({
|
||||
sheets:
|
||||
options.sheets ??
|
||||
[
|
||||
{
|
||||
sheet_id: "sheet-one",
|
||||
title: "内容池",
|
||||
resource_type: "sheet",
|
||||
hidden: false,
|
||||
grid_properties: { row_count: 20, column_count: 10 },
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
if (url.pathname.endsWith("/values_batch_get")) {
|
||||
return apiResponse({
|
||||
valueRanges: [
|
||||
{
|
||||
values: [
|
||||
["作品ID", "标题", "标签", "正文", null, null],
|
||||
[
|
||||
7,
|
||||
"一篇测试笔记",
|
||||
"#测试 #KOC",
|
||||
"测试正文",
|
||||
{
|
||||
type: "embed-image",
|
||||
fileToken: "file-token-one",
|
||||
width: 1080,
|
||||
height: 1440,
|
||||
},
|
||||
[
|
||||
{
|
||||
type: "embed-image",
|
||||
fileToken: "file-token-two",
|
||||
width: 1080,
|
||||
height: 1440,
|
||||
},
|
||||
],
|
||||
],
|
||||
[8, "", "", "空标题不会导入"],
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
throw new Error(`Unexpected request: ${url.pathname}`);
|
||||
};
|
||||
return { calls, fetchImpl };
|
||||
}
|
||||
|
||||
test("resolves a wiki sheet and imports title, body, tags, and all images", async () => {
|
||||
clearFeishuAccessTokenCacheForTests();
|
||||
const { calls, fetchImpl } = fakeFeishu();
|
||||
const source = await readFeishuSource(
|
||||
"https://tenant.feishu.cn/wiki/wiki-test",
|
||||
bindings,
|
||||
fetchImpl,
|
||||
);
|
||||
|
||||
assert.equal(source.spreadsheetToken, "spreadsheet-test");
|
||||
assert.equal(source.sheetId, "sheet-one");
|
||||
assert.equal(source.sheetName, "内容池");
|
||||
assert.equal(source.rows.length, 1);
|
||||
assert.equal(source.rows[0].sourceRow, 7);
|
||||
assert.equal(source.rows[0].body, "测试正文\n\n#测试 #KOC");
|
||||
assert.deepEqual(
|
||||
source.rows[0].images.map((image) => image.fileToken),
|
||||
["file-token-one", "file-token-two"],
|
||||
);
|
||||
assert.deepEqual(source.columns, [
|
||||
"作品ID",
|
||||
"标题",
|
||||
"标签",
|
||||
"正文",
|
||||
"图片1",
|
||||
"图片2",
|
||||
]);
|
||||
const valuesCall = calls.find((call) =>
|
||||
call.url.pathname.endsWith("/values_batch_get"),
|
||||
);
|
||||
assert.equal(valuesCall.url.searchParams.get("ranges"), "sheet-one!A1:J20");
|
||||
});
|
||||
|
||||
test("requires an exact sheet link when a workbook has multiple visible sheets", async () => {
|
||||
clearFeishuAccessTokenCacheForTests();
|
||||
const { fetchImpl } = fakeFeishu({
|
||||
sheets: [
|
||||
{
|
||||
sheet_id: "one",
|
||||
title: "内容A",
|
||||
resource_type: "sheet",
|
||||
hidden: false,
|
||||
},
|
||||
{
|
||||
sheet_id: "two",
|
||||
title: "内容B",
|
||||
resource_type: "sheet",
|
||||
hidden: false,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
await assert.rejects(
|
||||
readFeishuSource(
|
||||
"https://tenant.feishu.cn/wiki/wiki-test",
|
||||
bindings,
|
||||
fetchImpl,
|
||||
),
|
||||
/包含多个工作表/,
|
||||
);
|
||||
});
|
||||
|
||||
test("rejects a wiki node that is not a spreadsheet", async () => {
|
||||
clearFeishuAccessTokenCacheForTests();
|
||||
const fetchImpl = async (input) => {
|
||||
const url = new URL(String(input));
|
||||
if (url.pathname.endsWith("/tenant_access_token/internal")) {
|
||||
return Response.json({
|
||||
code: 0,
|
||||
msg: "success",
|
||||
tenant_access_token: "tenant-test",
|
||||
expire: 7200,
|
||||
});
|
||||
}
|
||||
return apiResponse({
|
||||
node: { obj_type: "docx", obj_token: "document-test" },
|
||||
});
|
||||
};
|
||||
|
||||
await assert.rejects(
|
||||
readFeishuSource(
|
||||
"https://tenant.feishu.cn/wiki/wiki-test",
|
||||
bindings,
|
||||
fetchImpl,
|
||||
),
|
||||
/不是电子表格/,
|
||||
);
|
||||
});
|
||||
510
tests/mcp-collection-client.test.mjs
Normal file
510
tests/mcp-collection-client.test.mjs
Normal file
@@ -0,0 +1,510 @@
|
||||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
import {
|
||||
collectXhsMetricsFromMcp,
|
||||
resolveCollectionMcpConfig,
|
||||
resolveXhsPublicAccountDetails,
|
||||
resolveXhsPublicAccountId,
|
||||
resolveXhsAccountProfileFromMcp,
|
||||
resolveXhsProfileDetailsFromMcp,
|
||||
xhsNoteIdFromUrl,
|
||||
} from "../lib/mcp-collection-client.ts";
|
||||
|
||||
function sse(payload, options = {}) {
|
||||
return new Response(
|
||||
`event: message\ndata: ${JSON.stringify(payload)}\n\n`,
|
||||
{
|
||||
status: 200,
|
||||
...options,
|
||||
headers: {
|
||||
"content-type": "text/event-stream",
|
||||
...(options.headers ?? {}),
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
function toolEnvelope(payload, isError = false) {
|
||||
return {
|
||||
jsonrpc: "2.0",
|
||||
id: "tool-call",
|
||||
result: {
|
||||
isError,
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: JSON.stringify(payload),
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function createFakeMcp(toolResults) {
|
||||
let toolIndex = 0;
|
||||
const calls = [];
|
||||
const fetchImpl = async (url, init) => {
|
||||
const body = JSON.parse(init.body);
|
||||
calls.push({ url: String(url), body, headers: new Headers(init.headers) });
|
||||
if (body.method === "initialize") {
|
||||
return sse(
|
||||
{
|
||||
jsonrpc: "2.0",
|
||||
id: body.id,
|
||||
result: {
|
||||
protocolVersion: "2025-03-26",
|
||||
capabilities: { tools: { listChanged: false } },
|
||||
serverInfo: { name: "ai-tool-center", version: "test" },
|
||||
},
|
||||
},
|
||||
{ headers: { "mcp-session-id": "session-test" } },
|
||||
);
|
||||
}
|
||||
if (body.method === "notifications/initialized") {
|
||||
return new Response("", { status: 202 });
|
||||
}
|
||||
if (body.method === "tools/call") {
|
||||
const result = toolResults[toolIndex];
|
||||
toolIndex += 1;
|
||||
return sse(result);
|
||||
}
|
||||
throw new Error(`Unexpected MCP method: ${body.method}`);
|
||||
};
|
||||
return { calls, fetchImpl };
|
||||
}
|
||||
|
||||
test("collects likes, comments and favorites from the verified MCP shape", async () => {
|
||||
const { calls, fetchImpl } = createFakeMcp([
|
||||
toolEnvelope({
|
||||
http_status: 200,
|
||||
response: {
|
||||
code: 200,
|
||||
success: true,
|
||||
msg: "获取内容详情成功",
|
||||
data: {
|
||||
likes: "483",
|
||||
comments: "41",
|
||||
collects: "519",
|
||||
},
|
||||
},
|
||||
}),
|
||||
]);
|
||||
|
||||
const result = await collectXhsMetricsFromMcp(
|
||||
"https://www.xiaohongshu.com/discovery/item/test?xsec_token=valid",
|
||||
{
|
||||
endpoint: "https://collector.example/mcp",
|
||||
key: "test-key",
|
||||
},
|
||||
fetchImpl,
|
||||
);
|
||||
|
||||
assert.deepEqual(result, {
|
||||
likes: 483,
|
||||
comments: 41,
|
||||
collects: 519,
|
||||
});
|
||||
assert.equal(calls.length, 3);
|
||||
assert.equal(calls[2].body.params.name, "fetch_content_detail");
|
||||
assert.equal(calls[2].body.params.arguments.include_comments, false);
|
||||
assert.equal(calls[2].headers.get("mcp-session-id"), "session-test");
|
||||
assert.equal(new URL(calls[0].url).searchParams.get("key"), "test-key");
|
||||
});
|
||||
|
||||
test("resolves the real XHS account profile from a submitted note link", async () => {
|
||||
const { calls, fetchImpl } = createFakeMcp([
|
||||
toolEnvelope({
|
||||
response: {
|
||||
code: 200,
|
||||
success: true,
|
||||
data: {
|
||||
note_detail: {
|
||||
products: [
|
||||
{
|
||||
content_tags: [
|
||||
{
|
||||
notes: [
|
||||
{
|
||||
ip_location: "重庆",
|
||||
note_id: "6a671108000000000f004bef",
|
||||
user: {
|
||||
nickname: "N我的麻辣烫好了吗",
|
||||
profile_url:
|
||||
"https://www.xiaohongshu.com/user/profile/6905cbca0000000037009f49",
|
||||
red_id: "94329495984",
|
||||
user_id: "6905cbca0000000037009f49",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
toolEnvelope({
|
||||
response: {
|
||||
code: 200,
|
||||
success: true,
|
||||
data: {
|
||||
fans_count: "734",
|
||||
},
|
||||
},
|
||||
}),
|
||||
]);
|
||||
|
||||
const profile = await resolveXhsAccountProfileFromMcp(
|
||||
"https://www.xiaohongshu.com/discovery/item/6a671108000000000f004bef",
|
||||
"回填昵称",
|
||||
{
|
||||
endpoint: "https://collector.example/mcp",
|
||||
key: "test-key",
|
||||
},
|
||||
fetchImpl,
|
||||
);
|
||||
|
||||
assert.deepEqual(profile, {
|
||||
platformUid: "6905cbca0000000037009f49",
|
||||
nickname: "N我的麻辣烫好了吗",
|
||||
profileUrl:
|
||||
"https://www.xiaohongshu.com/user/profile/6905cbca0000000037009f49",
|
||||
redId: "94329495984",
|
||||
ipLocation: "重庆",
|
||||
followers: 734,
|
||||
});
|
||||
assert.equal(calls.length, 4);
|
||||
assert.equal(
|
||||
calls[2].body.params.name,
|
||||
"collect_xhs_wen_note_detail",
|
||||
);
|
||||
assert.equal(
|
||||
calls[2].body.params.arguments.note_id,
|
||||
"6a671108000000000f004bef",
|
||||
);
|
||||
assert.equal(calls[3].body.params.name, "parse_xhs_user_summary");
|
||||
assert.equal(
|
||||
calls[3].body.params.arguments.url,
|
||||
"https://www.xiaohongshu.com/user/profile/6905cbca0000000037009f49",
|
||||
);
|
||||
assert.equal(
|
||||
xhsNoteIdFromUrl(
|
||||
"https://www.xiaohongshu.com/explore/6a671108000000000f004bef",
|
||||
),
|
||||
"6a671108000000000f004bef",
|
||||
);
|
||||
});
|
||||
|
||||
test("resolves followers directly from the supported XHS user summary tool", async () => {
|
||||
const { calls, fetchImpl } = createFakeMcp([
|
||||
toolEnvelope({
|
||||
response: {
|
||||
code: 200,
|
||||
success: true,
|
||||
msg: "用户摘要解析成功",
|
||||
data: {
|
||||
user: {
|
||||
fansCount: 6,
|
||||
ipLocation: "福建",
|
||||
nickname: "555 五",
|
||||
userId: "1020668113",
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
]);
|
||||
|
||||
const details = await resolveXhsProfileDetailsFromMcp(
|
||||
"https://www.xiaohongshu.com/user/profile/5fb21d32000000000101c23e",
|
||||
{
|
||||
endpoint: "https://collector.example/mcp",
|
||||
key: "test-key",
|
||||
},
|
||||
fetchImpl,
|
||||
);
|
||||
|
||||
assert.deepEqual(details, {
|
||||
followers: 6,
|
||||
redId: "1020668113",
|
||||
ipLocation: "福建",
|
||||
});
|
||||
assert.equal(calls[2].body.params.name, "parse_xhs_user_summary");
|
||||
});
|
||||
|
||||
test("resolves an xhslink short URL before requesting the author profile", async () => {
|
||||
const fakeMcp = createFakeMcp([
|
||||
toolEnvelope({
|
||||
response: {
|
||||
code: 200,
|
||||
success: true,
|
||||
data: {
|
||||
note_detail: {
|
||||
note_id: "6a572da40000000021018bd2",
|
||||
ip_location: "上海",
|
||||
user: {
|
||||
nickname: "短链作者",
|
||||
user_id: "6905cbca0000000037009f49",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
toolEnvelope({
|
||||
response: {
|
||||
code: 200,
|
||||
success: true,
|
||||
data: {
|
||||
red_id: "1020668113",
|
||||
followers: "1.2万",
|
||||
},
|
||||
},
|
||||
}),
|
||||
]);
|
||||
const fetchImpl = async (url, init) => {
|
||||
if (String(url) === "http://xhslink.cn/o/AJFyP5dnj7O") {
|
||||
return new Response(null, {
|
||||
status: 302,
|
||||
headers: {
|
||||
location:
|
||||
"https://www.xiaohongshu.com/discovery/item/6a572da40000000021018bd2?xsec_token=valid",
|
||||
},
|
||||
});
|
||||
}
|
||||
return fakeMcp.fetchImpl(url, init);
|
||||
};
|
||||
|
||||
const profile = await resolveXhsAccountProfileFromMcp(
|
||||
"http://xhslink.cn/o/AJFyP5dnj7O",
|
||||
"回填昵称",
|
||||
{
|
||||
endpoint: "https://collector.example/mcp",
|
||||
key: "test-key",
|
||||
},
|
||||
fetchImpl,
|
||||
);
|
||||
|
||||
assert.equal(profile.nickname, "短链作者");
|
||||
assert.equal(profile.platformUid, "6905cbca0000000037009f49");
|
||||
assert.equal(profile.redId, "1020668113");
|
||||
assert.equal(profile.followers, 12_000);
|
||||
assert.equal(
|
||||
profile.profileUrl,
|
||||
"https://www.xiaohongshu.com/user/profile/6905cbca0000000037009f49",
|
||||
);
|
||||
assert.equal(
|
||||
fakeMcp.calls[2].body.params.arguments.note_id,
|
||||
"6a572da40000000021018bd2",
|
||||
);
|
||||
});
|
||||
|
||||
test("uses the public note author when MCP profile lookup fails", async () => {
|
||||
const fakeMcp = createFakeMcp([
|
||||
toolEnvelope(
|
||||
{
|
||||
response: {
|
||||
code: 500,
|
||||
success: false,
|
||||
msg: "作者详情暂时不可用",
|
||||
},
|
||||
},
|
||||
true,
|
||||
),
|
||||
]);
|
||||
const fetchImpl = async (url, init) => {
|
||||
if (String(url) === "http://xhslink.cn/o/AJFyP5dnj7O") {
|
||||
return new Response(
|
||||
'<script>window.__STATE__={"noteData":{"desc":"测试","user":{"userId":"5fb21d32000000000101c23e","nickName":"555 五"}}}</script>',
|
||||
{
|
||||
status: 200,
|
||||
headers: {
|
||||
"content-type": "text/html; charset=utf-8",
|
||||
location:
|
||||
"https://www.xiaohongshu.com/discovery/item/6a572da40000000021018bd2?xsec_token=valid",
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
if (
|
||||
String(url) ===
|
||||
"https://www.xiaohongshu.com/user/profile/5fb21d32000000000101c23e"
|
||||
) {
|
||||
return new Response(
|
||||
'<script>{"redId":"1020668113","interactions":[{"name":"粉丝","count":"6","i18nCount":"6"}]}</script>',
|
||||
{ status: 200 },
|
||||
);
|
||||
}
|
||||
return fakeMcp.fetchImpl(url, init);
|
||||
};
|
||||
|
||||
const profile = await resolveXhsAccountProfileFromMcp(
|
||||
"http://xhslink.cn/o/AJFyP5dnj7O",
|
||||
"回填昵称",
|
||||
{
|
||||
endpoint: "https://collector.example/mcp",
|
||||
key: "test-key",
|
||||
},
|
||||
fetchImpl,
|
||||
);
|
||||
|
||||
assert.deepEqual(profile, {
|
||||
platformUid: "5fb21d32000000000101c23e",
|
||||
nickname: "555 五",
|
||||
profileUrl:
|
||||
"https://www.xiaohongshu.com/user/profile/5fb21d32000000000101c23e",
|
||||
redId: "1020668113",
|
||||
ipLocation: "待识别",
|
||||
followers: 6,
|
||||
});
|
||||
assert.equal(
|
||||
fakeMcp.calls[2].body.params.name,
|
||||
"collect_xhs_wen_note_detail",
|
||||
);
|
||||
});
|
||||
|
||||
test("reads the user-visible Xiaohongshu number from a public profile", async () => {
|
||||
const fetchImpl = async () =>
|
||||
new Response(
|
||||
'<div class="redId">小红书号:1020668113</div><script>{"redId":"1020668113","interactions":[{"name":"粉丝","count":"10+","i18nCount":"10+"}]}</script>',
|
||||
{ status: 200 },
|
||||
);
|
||||
const accountId = await resolveXhsPublicAccountId(
|
||||
"https://www.xiaohongshu.com/user/profile/5fb21d32000000000101c23e",
|
||||
fetchImpl,
|
||||
);
|
||||
const details = await resolveXhsPublicAccountDetails(
|
||||
"https://www.xiaohongshu.com/user/profile/5fb21d32000000000101c23e",
|
||||
fetchImpl,
|
||||
);
|
||||
|
||||
assert.equal(accountId, "1020668113");
|
||||
assert.deepEqual(details, {
|
||||
redId: "1020668113",
|
||||
followers: 10,
|
||||
ipLocation: "",
|
||||
});
|
||||
});
|
||||
|
||||
test("falls back to parse_xhs_note when the primary tool fails", async () => {
|
||||
const { calls, fetchImpl } = createFakeMcp([
|
||||
toolEnvelope(
|
||||
{
|
||||
response: {
|
||||
code: 400,
|
||||
success: false,
|
||||
msg: "获取内容详情失败",
|
||||
data: null,
|
||||
},
|
||||
},
|
||||
true,
|
||||
),
|
||||
toolEnvelope({
|
||||
response: {
|
||||
code: 200,
|
||||
success: true,
|
||||
data: {
|
||||
likes: "1.2万",
|
||||
comments: 32,
|
||||
collects: "2,345",
|
||||
},
|
||||
},
|
||||
}),
|
||||
]);
|
||||
|
||||
const result = await collectXhsMetricsFromMcp(
|
||||
"https://www.xiaohongshu.com/explore/test",
|
||||
{
|
||||
endpoint: "https://collector.example/mcp?key=test-key",
|
||||
},
|
||||
fetchImpl,
|
||||
);
|
||||
|
||||
assert.deepEqual(result, {
|
||||
likes: 12_000,
|
||||
comments: 32,
|
||||
collects: 2_345,
|
||||
});
|
||||
assert.equal(calls[3].body.params.name, "parse_xhs_note");
|
||||
});
|
||||
|
||||
test("requires the MCP key without sending a network request", async () => {
|
||||
let requested = false;
|
||||
await assert.rejects(
|
||||
collectXhsMetricsFromMcp(
|
||||
"https://www.xiaohongshu.com/explore/test",
|
||||
resolveCollectionMcpConfig({}),
|
||||
async () => {
|
||||
requested = true;
|
||||
return new Response();
|
||||
},
|
||||
),
|
||||
/MCP采集密钥未配置/,
|
||||
);
|
||||
assert.equal(requested, false);
|
||||
});
|
||||
|
||||
test("rebuilds the MCP session after a gateway session miss", async () => {
|
||||
let initializeCount = 0;
|
||||
const fetchImpl = async (_url, init) => {
|
||||
const body = JSON.parse(init.body);
|
||||
if (body.method === "initialize") {
|
||||
initializeCount += 1;
|
||||
return sse(
|
||||
{
|
||||
jsonrpc: "2.0",
|
||||
id: body.id,
|
||||
result: {
|
||||
protocolVersion: "2025-03-26",
|
||||
capabilities: { tools: { listChanged: false } },
|
||||
serverInfo: { name: "ai-tool-center", version: "test" },
|
||||
},
|
||||
},
|
||||
{
|
||||
headers: {
|
||||
"mcp-session-id": `session-${initializeCount}`,
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
if (
|
||||
body.method === "notifications/initialized" &&
|
||||
initializeCount === 1
|
||||
) {
|
||||
return Response.json(
|
||||
{
|
||||
jsonrpc: "2.0",
|
||||
id: "server-error",
|
||||
error: { code: -32600, message: "Session not found" },
|
||||
},
|
||||
{ status: 404 },
|
||||
);
|
||||
}
|
||||
if (body.method === "notifications/initialized") {
|
||||
return new Response("", { status: 202 });
|
||||
}
|
||||
if (body.method === "tools/call") {
|
||||
return sse(
|
||||
toolEnvelope({
|
||||
response: {
|
||||
code: 200,
|
||||
success: true,
|
||||
data: { likes: 8, comments: 2, collects: 5 },
|
||||
},
|
||||
}),
|
||||
);
|
||||
}
|
||||
throw new Error(`Unexpected MCP method: ${body.method}`);
|
||||
};
|
||||
|
||||
const result = await collectXhsMetricsFromMcp(
|
||||
"https://www.xiaohongshu.com/explore/test",
|
||||
{
|
||||
endpoint: "https://collector.example/mcp",
|
||||
key: "test-key",
|
||||
},
|
||||
fetchImpl,
|
||||
);
|
||||
|
||||
assert.deepEqual(result, { likes: 8, comments: 2, collects: 5 });
|
||||
assert.equal(initializeCount, 2);
|
||||
});
|
||||
30
tests/partner-utils.test.mjs
Normal file
30
tests/partner-utils.test.mjs
Normal file
@@ -0,0 +1,30 @@
|
||||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
import {
|
||||
extractXhsPublishUrl,
|
||||
} from "../lib/publish-url.ts";
|
||||
|
||||
const longShareText =
|
||||
"81 【汕头街头的夏天,有点可爱 - pyeong | 小红书 - 你的生活兴趣社区】 😆 qpF2vquW4v6E0uq 😆 https://www.xiaohongshu.com/discovery/item/6a5a407d000000000e0363c7?source=webshare&xhsshare=pc_web&xsec_token=ABQTS76SGS2MKA1rYfSay-1cyULvb_8k7kNIw4mKVrcDA=&xsec_source=pc_share";
|
||||
const shortShareText =
|
||||
"汕头街头的夏天,有点可爱 今天的关键词: 浅蓝色、草... http://xhslink.cn/o/6UDG4oUB8kR 保留这段,去【小红书】逛逛吧~";
|
||||
|
||||
test("extracts Xiaohongshu long and short URLs from full share text", () => {
|
||||
const longUrl = extractXhsPublishUrl(longShareText);
|
||||
const shortUrl = extractXhsPublishUrl(shortShareText);
|
||||
|
||||
assert.equal(
|
||||
new URL(longUrl).pathname,
|
||||
"/discovery/item/6a5a407d000000000e0363c7",
|
||||
);
|
||||
assert.equal(new URL(longUrl).searchParams.get("source"), "webshare");
|
||||
assert.equal(shortUrl, "http://xhslink.cn/o/6UDG4oUB8kR");
|
||||
});
|
||||
|
||||
test("rejects text that does not contain a Xiaohongshu URL", () => {
|
||||
assert.equal(extractXhsPublishUrl("只有文案,没有链接"), "");
|
||||
assert.equal(
|
||||
extractXhsPublishUrl("https://example.com/not-xhs"),
|
||||
"",
|
||||
);
|
||||
});
|
||||
261
tests/rendered-html.test.mjs
Normal file
261
tests/rendered-html.test.mjs
Normal file
@@ -0,0 +1,261 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { access, readFile } from "node:fs/promises";
|
||||
import test from "node:test";
|
||||
|
||||
test("builds the KOC LOOP product shell", async () => {
|
||||
const [page, adminApp, layout] = await Promise.all([
|
||||
readFile(new URL("../app/page.tsx", import.meta.url), "utf8"),
|
||||
readFile(new URL("../app/admin-app.tsx", import.meta.url), "utf8"),
|
||||
readFile(new URL("../app/layout.tsx", import.meta.url), "utf8"),
|
||||
]);
|
||||
assert.match(page, /requireChatGPTUser/);
|
||||
assert.match(page, /isAdminEmail/);
|
||||
assert.match(adminApp, /KOC LOOP/);
|
||||
assert.match(adminApp, /内容分发闭环/);
|
||||
assert.match(adminApp, /分发工作台/);
|
||||
assert.match(adminApp, /获取KOC领取链接/);
|
||||
assert.match(layout, /KOC LOOP|内容分发闭环/);
|
||||
assert.doesNotMatch(adminApp, /codex-preview|Your site is taking shape/);
|
||||
await access(new URL("../dist/server/index.js", import.meta.url));
|
||||
await access(new URL("../dist/client/assets", import.meta.url));
|
||||
});
|
||||
|
||||
test("ships persistence, uploads, metadata, and no starter preview", async () => {
|
||||
const [adminApp, layout, packageJson, hosting] = await Promise.all([
|
||||
readFile(new URL("../app/admin-app.tsx", import.meta.url), "utf8"),
|
||||
readFile(new URL("../app/layout.tsx", import.meta.url), "utf8"),
|
||||
readFile(new URL("../package.json", import.meta.url), "utf8"),
|
||||
readFile(new URL("../.openai/hosting.json", import.meta.url), "utf8"),
|
||||
]);
|
||||
|
||||
assert.doesNotMatch(adminApp, /批量回填发布链接/);
|
||||
assert.match(adminApp, /复制领取链接/);
|
||||
assert.match(adminApp, /compressScreenshot/);
|
||||
assert.match(layout, /KOC LOOP|内容分发闭环/);
|
||||
assert.match(layout, /\/og\.png/);
|
||||
assert.doesNotMatch(packageJson, /react-loading-skeleton/);
|
||||
assert.match(hosting, /"d1": "DB"/);
|
||||
assert.match(hosting, /"r2": "UPLOADS"/);
|
||||
await access(new URL("../public/og.png", import.meta.url));
|
||||
});
|
||||
|
||||
test("organizes distribution and recovery by task and imports the verified Feishu source", async () => {
|
||||
const [adminApp, snapshotText, migration, imageMigration] = await Promise.all([
|
||||
readFile(new URL("../app/admin-app.tsx", import.meta.url), "utf8"),
|
||||
readFile(new URL("../lib/feishu-source-snapshot.json", import.meta.url), "utf8"),
|
||||
readFile(new URL("../drizzle/0001_sloppy_blue_blade.sql", import.meta.url), "utf8"),
|
||||
readFile(new URL("../drizzle/0003_needy_doctor_strange.sql", import.meta.url), "utf8"),
|
||||
]);
|
||||
const snapshot = JSON.parse(snapshotText);
|
||||
|
||||
assert.match(adminApp, /选择一个分发任务/);
|
||||
assert.match(adminApp, /选择一个数据回收任务/);
|
||||
assert.match(adminApp, /读取表格/);
|
||||
assert.match(adminApp, /当前仅展示/);
|
||||
assert.equal(snapshot.sheetId, "954953");
|
||||
assert.equal(snapshot.rows.length, 61);
|
||||
assert.equal(
|
||||
snapshot.rows.reduce((total, row) => total + row.images.length, 0),
|
||||
64,
|
||||
);
|
||||
assert.ok(snapshot.rows.every((row) => row.images.length > 0));
|
||||
assert.match(migration, /source_sheet_id/);
|
||||
assert.match(migration, /source_row/);
|
||||
assert.match(imageMigration, /image_assets/);
|
||||
assert.match(imageMigration, /claims_task_partner_created_idx/);
|
||||
});
|
||||
|
||||
test("issues external task links and supports one-to-one note submissions", async () => {
|
||||
const [
|
||||
adminApp,
|
||||
actionRoute,
|
||||
partnerRoute,
|
||||
uploadRoute,
|
||||
imageRoute,
|
||||
imageUploadRoute,
|
||||
cors,
|
||||
migration,
|
||||
accountEnrichment,
|
||||
partnerUtils,
|
||||
publishUrlUtils,
|
||||
creatorScreenshotRoute,
|
||||
] = await Promise.all([
|
||||
readFile(new URL("../app/admin-app.tsx", import.meta.url), "utf8"),
|
||||
readFile(new URL("../app/api/action/route.ts", import.meta.url), "utf8"),
|
||||
readFile(new URL("../app/api/partner/route.ts", import.meta.url), "utf8"),
|
||||
readFile(new URL("../app/api/partner-upload/route.ts", import.meta.url), "utf8"),
|
||||
readFile(new URL("../app/api/partner-image/route.ts", import.meta.url), "utf8"),
|
||||
readFile(new URL("../app/api/content-image-upload/route.ts", import.meta.url), "utf8"),
|
||||
readFile(new URL("../lib/partner-cors.ts", import.meta.url), "utf8"),
|
||||
readFile(new URL("../drizzle/0002_wealthy_maelstrom.sql", import.meta.url), "utf8"),
|
||||
readFile(new URL("../lib/account-enrichment-service.ts", import.meta.url), "utf8"),
|
||||
readFile(new URL("../lib/partner-utils.ts", import.meta.url), "utf8"),
|
||||
readFile(new URL("../lib/publish-url.ts", import.meta.url), "utf8"),
|
||||
readFile(new URL("../app/api/creator-screenshot/route.ts", import.meta.url), "utf8"),
|
||||
]);
|
||||
|
||||
assert.match(adminApp, /复制领取链接/);
|
||||
assert.match(adminApp, /小红书号/);
|
||||
assert.match(adminApp, /public_account_id/);
|
||||
assert.doesNotMatch(adminApp, /title=\{account\.platform_uid\}/);
|
||||
assert.doesNotMatch(adminApp, /平均阅读/);
|
||||
assert.doesNotMatch(adminApp, /批量回填发布链接/);
|
||||
assert.doesNotMatch(actionRoute, /submit_links/);
|
||||
assert.match(partnerRoute, /publish_screenshot_key/);
|
||||
assert.match(partnerRoute, /creator_screenshot_key/);
|
||||
assert.match(partnerRoute, /submit_creator_metrics/);
|
||||
assert.match(partnerRoute, /ocr_status = 'manual'/);
|
||||
assert.match(partnerRoute, /请填写正确的曝光量和阅读量/);
|
||||
assert.match(partnerRoute, /笔记与领取凭证不匹配/);
|
||||
assert.match(partnerRoute, /action === "recover"/);
|
||||
assert.match(partnerRoute, /extractXhsPublishUrl/);
|
||||
assert.match(partnerRoute, /小红书长链或短链/);
|
||||
assert.match(partnerRoute, /没有找到领取记录/);
|
||||
assert.match(partnerRoute, /publicImageAssets/);
|
||||
assert.match(partnerRoute, /withPartnerCors/);
|
||||
assert.match(partnerRoute, /enrichDistributionAccount/);
|
||||
assert.match(partnerRoute, /getRequestExecutionContext/);
|
||||
assert.match(partnerRoute, /executionContext\.waitUntil\(enrichment\)/);
|
||||
assert.match(accountEnrichment, /profile_url = excluded\.profile_url/);
|
||||
assert.match(accountEnrichment, /resolveXhsAccountProfileFromMcp/);
|
||||
assert.match(accountEnrichment, /resolveXhsProfileDetailsFromMcp/);
|
||||
assert.match(accountEnrichment, /isVerifiedXhsProfileUrl/);
|
||||
assert.match(accountEnrichment, /endsWith\("\.xiaohongshu\.com"\)/);
|
||||
assert.match(accountEnrichment, /DELETE FROM accounts/);
|
||||
assert.match(accountEnrichment, /a\.followers/);
|
||||
assert.match(accountEnrichment, /followers = CASE/);
|
||||
assert.match(accountEnrichment, /Number\(row\.followers \?\? 0\) === 0/);
|
||||
assert.match(partnerUtils, /pending-\$\{hashText/);
|
||||
assert.match(partnerUtils, /profileUrl:\s*""/);
|
||||
assert.match(publishUrlUtils, /xhslink\.cn/);
|
||||
assert.doesNotMatch(partnerUtils, /user\/profile\/\$\{platformUid\}/);
|
||||
assert.match(uploadRoute, /publish-evidence/);
|
||||
assert.match(uploadRoute, /creator-center/);
|
||||
assert.match(uploadRoute, /ELSE 'uploaded'/);
|
||||
assert.doesNotMatch(uploadRoute, /ocrMetric/);
|
||||
assert.match(uploadRoute, /x-koc-upload-kind/);
|
||||
assert.match(uploadRoute, /x-koc-distribution/);
|
||||
assert.match(uploadRoute, /request\.arrayBuffer/);
|
||||
assert.match(imageRoute, /content-assets\//);
|
||||
assert.match(imageRoute, /cl\.claim_token/);
|
||||
assert.match(imageUploadRoute, /isAdminRequest/);
|
||||
assert.match(cors, /KOC_PORTAL_URL/);
|
||||
assert.match(cors, /X-KOC-Distribution/);
|
||||
assert.match(cors, /X-KOC-Upload-Kind/);
|
||||
assert.match(cors, /Access-Control-Allow-Origin/);
|
||||
assert.match(adminApp, /hasCreatorMetrics/);
|
||||
assert.match(adminApp, /待KOC填写数据/);
|
||||
assert.match(creatorScreenshotRoute, /getUploadBucket/);
|
||||
assert.match(creatorScreenshotRoute, /Content-Disposition/);
|
||||
assert.match(creatorScreenshotRoute, /isAdminRequest/);
|
||||
assert.match(migration, /share_token/);
|
||||
assert.match(migration, /claim_token/);
|
||||
});
|
||||
|
||||
test("supports task collection schedules and latest public metrics", async () => {
|
||||
const [
|
||||
adminApp,
|
||||
actionRoute,
|
||||
bootstrapRoute,
|
||||
collectionService,
|
||||
worker,
|
||||
viteConfig,
|
||||
migration,
|
||||
accountMigration,
|
||||
deployConfig,
|
||||
] = await Promise.all([
|
||||
readFile(new URL("../app/admin-app.tsx", import.meta.url), "utf8"),
|
||||
readFile(new URL("../app/api/action/route.ts", import.meta.url), "utf8"),
|
||||
readFile(new URL("../app/api/bootstrap/route.ts", import.meta.url), "utf8"),
|
||||
readFile(new URL("../lib/collection-service.ts", import.meta.url), "utf8"),
|
||||
readFile(new URL("../worker/index.ts", import.meta.url), "utf8"),
|
||||
readFile(new URL("../vite.config.ts", import.meta.url), "utf8"),
|
||||
readFile(new URL("../drizzle/0004_sharp_the_liberteens.sql", import.meta.url), "utf8"),
|
||||
readFile(new URL("../drizzle/0005_foamy_sage.sql", import.meta.url), "utf8"),
|
||||
readFile(new URL("../dist/server/wrangler.json", import.meta.url), "utf8"),
|
||||
]);
|
||||
|
||||
for (const label of [
|
||||
"点赞",
|
||||
"收藏",
|
||||
"评论",
|
||||
"总互动",
|
||||
"数据更新时间",
|
||||
"采集状态",
|
||||
"发布时间",
|
||||
]) {
|
||||
assert.match(adminApp, new RegExp(label));
|
||||
}
|
||||
assert.match(adminApp, /选择开始日期与采集日/);
|
||||
assert.match(adminApp, /第1天到第7天/);
|
||||
assert.match(adminApp, /内容 \/ 发布账号/);
|
||||
assert.match(adminApp, /recovery-title-link/);
|
||||
assert.match(adminApp, /打开小红书笔记/);
|
||||
assert.match(adminApp, /noopener noreferrer/);
|
||||
assert.match(actionRoute, /save_collection_schedule/);
|
||||
assert.match(actionRoute, /collect_now/);
|
||||
assert.match(actionRoute, /backfill_account_profiles/);
|
||||
assert.match(actionRoute, /set_public_account_ids/);
|
||||
assert.match(actionRoute, /run_due_collections/);
|
||||
assert.match(actionRoute, /retry_failed_collections/);
|
||||
assert.match(actionRoute, /createCollectionRunTasks/);
|
||||
assert.match(bootstrapRoute, /runDueScheduledCollections/);
|
||||
assert.match(collectionService, /INSERT OR IGNORE INTO collection_runs/);
|
||||
assert.match(collectionService, /collectXhsMetricsFromMcp/);
|
||||
assert.doesNotMatch(collectionService, /hashText/);
|
||||
assert.match(collectionService, /runScheduledCollections/);
|
||||
assert.match(collectionService, /runDueScheduledCollections/);
|
||||
assert.match(collectionService, /retryFailedCollections/);
|
||||
assert.match(collectionService, /exposure IS NOT NULL AND views IS NOT NULL/);
|
||||
assert.match(collectionService, /等待第\$\{scheduleDay\}天 10:00自动采集/);
|
||||
assert.doesNotMatch(collectionService, /latestDueSchedule/);
|
||||
assert.match(collectionService, /自动追采/);
|
||||
assert.match(worker, /async scheduled/);
|
||||
assert.match(worker, /backfillAccountProfiles/);
|
||||
assert.match(bootstrapRoute, /backfillAccountProfiles/);
|
||||
assert.match(viteConfig, /"0 2 \* \* \*"/);
|
||||
assert.match(migration, /latest_likes/);
|
||||
assert.match(migration, /collection_runs_distribution_date_idx/);
|
||||
assert.match(accountMigration, /public_account_id/);
|
||||
assert.match(deployConfig, /"crons":\["0 2 \* \* \*"\]/);
|
||||
});
|
||||
|
||||
test("supports anonymous partner delegation without creating a second data flow", async () => {
|
||||
const [
|
||||
adminApp,
|
||||
partnerRoute,
|
||||
uploadRoute,
|
||||
imageRoute,
|
||||
cors,
|
||||
schema,
|
||||
runtimeSchema,
|
||||
migration,
|
||||
] = await Promise.all([
|
||||
readFile(new URL("../app/admin-app.tsx", import.meta.url), "utf8"),
|
||||
readFile(new URL("../app/api/partner/route.ts", import.meta.url), "utf8"),
|
||||
readFile(new URL("../app/api/partner-upload/route.ts", import.meta.url), "utf8"),
|
||||
readFile(new URL("../app/api/partner-image/route.ts", import.meta.url), "utf8"),
|
||||
readFile(new URL("../lib/partner-cors.ts", import.meta.url), "utf8"),
|
||||
readFile(new URL("../db/schema.ts", import.meta.url), "utf8"),
|
||||
readFile(new URL("../lib/mvp-db.ts", import.meta.url), "utf8"),
|
||||
readFile(new URL("../drizzle/0006_moaning_dark_phoenix.sql", import.meta.url), "utf8"),
|
||||
]);
|
||||
|
||||
assert.match(schema, /delegationBundles/);
|
||||
assert.match(schema, /delegationBundleId/);
|
||||
assert.match(runtimeSchema, /CREATE TABLE IF NOT EXISTS delegation_bundles/);
|
||||
assert.match(migration, /delegation_bundles/);
|
||||
assert.match(migration, /delegation_bundle_id/);
|
||||
assert.match(partnerRoute, /action === "create_delegation"/);
|
||||
assert.match(partnerRoute, /action === "revoke_delegation"/);
|
||||
assert.match(partnerRoute, /findAccessibleAssignment/);
|
||||
assert.match(partnerRoute, /b\.status = 'active'/);
|
||||
assert.match(partnerRoute, /部分笔记刚刚已被转派/);
|
||||
assert.match(partnerRoute, /分享链接只能用于查看和回填包内笔记/);
|
||||
assert.match(partnerRoute, /private, no-store/);
|
||||
assert.match(uploadRoute, /x-koc-delegation/);
|
||||
assert.match(uploadRoute, /delegation_bundles/);
|
||||
assert.match(imageRoute, /delegation_bundles/);
|
||||
assert.match(cors, /X-KOC-Delegation/);
|
||||
assert.match(adminApp, /合作社资源 · 不可直联/);
|
||||
});
|
||||
Reference in New Issue
Block a user