// 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 };