diff --git a/agents/zhuyuan-dev-agent/modules/notion-poller.js b/agents/zhuyuan-dev-agent/modules/notion-poller.js new file mode 100644 index 0000000..81f2092 --- /dev/null +++ b/agents/zhuyuan-dev-agent/modules/notion-poller.js @@ -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 };