feat: notion-poller v3.0 select类型修复 + testConnection导出 +-0

This commit is contained in:
root 2026-05-30 22:10:30 +08:00
parent 4a85cd977a
commit f3af355018

View File

@ -0,0 +1,174 @@
// HLDP-ZY://agents/zhuyuan-dev-agent/modules/notion-poller
// @guardian: ICE-GL-ZY001 · 铸渊
// @sovereign: TCS-0002∞ · 冰朔
// @copyright: 国作登字-2026-A-00037559
// Notion 任务轮询模块 v3.0 — select 类型修复 + 新数据库
const https = require("https");
const fs = require("fs");
const path = require("path");
const NOTION_BASE = "https://api.notion.com/v1";
const NOTION_VERSION = "2022-06-28";
function getHeaders() {
const token = process.env.NOTION_TOKEN;
if (!token) return null;
return {
"Authorization": "Bearer " + token,
"Content-Type": "application/json",
"Notion-Version": NOTION_VERSION
};
}
function notionPost(endpoint, body, method) {
return new Promise((resolve, reject) => {
const headers = getHeaders();
if (!headers) return reject(new Error("NOTION_TOKEN 未配置"));
const url = new URL(NOTION_BASE + endpoint);
const data = JSON.stringify(body);
const req = https.request({
hostname: url.hostname, path: url.pathname, method: method || "POST",
headers: { ...headers, "Content-Length": Buffer.byteLength(data) },
timeout: 15000
}, (res) => {
let d = "";
res.on("data", c => d += c);
res.on("end", () => {
try {
const json = JSON.parse(d);
if (res.statusCode >= 400) reject(new Error("Notion " + res.statusCode + ": " + (json.message || d.slice(0,200))));
else resolve(json);
} catch(e) { reject(new Error("Notion parse: " + d.slice(0,200))); }
});
});
req.on("error", reject);
req.on("timeout", () => { req.destroy(); reject(new Error("Notion timeout")); });
req.write(data);
req.end();
});
}
async function testConnection() {
const dbId = process.env.NOTION_PLAN_DB_ID;
const token = process.env.NOTION_TOKEN;
if (!token) return { ok: false, error: "NOTION_TOKEN 未配置" };
if (!dbId) return { ok: false, error: "NOTION_PLAN_DB_ID 未配置" };
try {
const result = await notionPost("/databases/" + dbId + "/query", { page_size: 1 });
return { ok: true, has_results: (result.results || []).length > 0, db_id: dbId };
} catch (err) {
return { ok: false, error: err.message, db_id: dbId };
}
}
async function fetchPendingTasks(options = {}) {
const dbId = options.dbId || process.env.NOTION_PLAN_DB_ID;
if (!dbId) throw new Error("NOTION_PLAN_DB_ID 未配置");
// v3.0: 新数据库状态是 select 类型,不是 status
const result = await notionPost("/databases/" + dbId + "/query", {
sorts: [{ property: "任务标题", direction: "descending" }],
filter: { or: [
{ property: "状态", select: { equals: "待开发" } },
{ property: "状态", select: { equals: "开发中" } },
{ property: "状态", select: { equals: "已审查" } }
]},
page_size: options.pageSize || 10
});
return (result.results || []).map(page => {
const props = page.properties || {};
const titleProp = props["任务标题"]?.title;
const stepsProp = props["开发步骤"]?.rich_text;
const constraintsProp = props["约束"]?.rich_text;
return {
notion_page_id: page.id,
title: Array.isArray(titleProp) && titleProp.length > 0 ? (titleProp[0]?.plain_text || "无标题") : "无标题",
status: props["状态"]?.select?.name || "unknown",
task_id: page.id.replace(/-/g,"").slice(0,12),
priority: props["优先级"]?.select?.name || "中等",
steps: Array.isArray(stepsProp) ? stepsProp.map(t => t.plain_text).join("\n") : "",
constraints: Array.isArray(constraintsProp) ? constraintsProp.map(t => t.plain_text).join("\n") : "",
developer: (props["开发者"]?.rich_text || [{}])[0]?.plain_text || "",
server: (props["服务器"]?.rich_text || [{}])[0]?.plain_text || "",
created: page.created_time,
url: page.url
};
});
}
async function markInProgress(pageId) {
return notionPost("/pages/" + pageId, { properties: { "状态": { select: { name: "开发中" } } } }, "PATCH");
}
async function markDone(pageId, details = {}) {
return notionPost("/pages/" + pageId, { properties: { "状态": { select: { name: "已完成" } } } }, "PATCH");
}
async function wakeAndCheck(options = {}) {
const log = options.log || console.log;
log("");
log("=== BD001 唤醒 · Notion 任务轮询 v3.0 ===");
log("时间: " + new Date().toISOString());
const token = process.env.NOTION_TOKEN;
if (!token) {
log("NOTION_TOKEN 未配置");
return { ok: false, error: "NOTION_TOKEN 未配置", tasks: [] };
}
let tasks = [];
try {
tasks = await fetchPendingTasks({ dbId: options.dbId, pageSize: options.pageSize || 5 });
log("查询到 " + tasks.length + " 个任务 (待开发+开发中+已审查)");
} catch (err) {
log("Notion查询失败: " + err.message);
return { ok: false, error: "Notion查询失败: " + err.message, tasks: [] };
}
let marked = 0;
for (const task of tasks) {
if (task.status === "待开发") {
try {
await markInProgress(task.notion_page_id);
marked++;
log(" ✅ " + task.title + " (" + task.priority + ")");
} catch (err) {
log(" ⚠️ 标记失败: " + task.title + " - " + err.message);
}
}
}
const plansDir = options.plansDir || path.join(__dirname, "..", "plans");
if (!fs.existsSync(plansDir)) fs.mkdirSync(plansDir, { recursive: true });
for (const task of tasks) {
const planFile = path.join(plansDir, task.task_id + ".json");
if (!fs.existsSync(planFile)) {
fs.writeFileSync(planFile, JSON.stringify({
plan_id: task.task_id,
title: task.title,
priority: task.priority,
steps: task.steps,
constraints: task.constraints,
notion_page_id: task.notion_page_id,
notion_url: task.url,
_received: new Date().toISOString(),
_source: "notion",
_status: "received",
tasks: []
}, null, 2), "utf-8");
log(" 📋 " + task.task_id);
}
}
return {
ok: true,
tasks_found: tasks.length,
tasks_marked: marked,
tasks: tasks.map(t => ({ id: t.task_id, title: t.title, priority: t.priority })),
time: new Date().toISOString()
};
}
module.exports = { wakeAndCheck, fetchPendingTasks, markInProgress, markDone, testConnection };