9b395f5cc3
E2E Headed Chrome / e2e-headed (macos-15) (push) Has been cancelled
E2E Headed Chrome / e2e-headed (ubuntu-latest) (push) Has been cancelled
E2E Headed Chrome / e2e-headed (windows-latest) (push) Has been cancelled
CI / build (macos-latest) (push) Has been cancelled
CI / build (ubuntu-latest) (push) Has been cancelled
CI / build (windows-latest) (push) Has been cancelled
CI / unit-test (push) Has been cancelled
CI / bun-test (push) Has been cancelled
CI / adapter-test (push) Has been cancelled
CI / smoke-test (macos-latest) (push) Has been cancelled
CI / smoke-test (ubuntu-latest) (push) Has been cancelled
Security Audit / audit (push) Has been cancelled
Build Chrome Extension / build (push) Has been cancelled
Trigger Website Rebuild (Docs Updated) / dispatch (push) Has been cancelled
246 lines
7.5 KiB
JavaScript
246 lines
7.5 KiB
JavaScript
import { cli, Strategy } from '@jackwener/opencli/registry';
|
||
import { ArgumentError } from '@jackwener/opencli/errors';
|
||
import { mubuPost, nodesToMarkdown, nodesToText, htmlToText } from './utils.js';
|
||
|
||
// ── 日期工具 ──────────────────────────────────────────────
|
||
|
||
function localToday() {
|
||
const d = new Date();
|
||
return { year: d.getFullYear(), month: d.getMonth() + 1, day: d.getDate() };
|
||
}
|
||
|
||
function lastDayOfMonth(year, month) {
|
||
return new Date(year, month, 0).getDate();
|
||
}
|
||
|
||
function validateYear(year, label = '年份') {
|
||
if (!Number.isInteger(year) || year < 1) {
|
||
throw new ArgumentError(`${label} 非法:${year},应为正整数`);
|
||
}
|
||
}
|
||
|
||
function validateMonth(month) {
|
||
if (!Number.isInteger(month) || month < 1 || month > 12) {
|
||
throw new ArgumentError(`月份非法:${month},应为 1-12`);
|
||
}
|
||
}
|
||
|
||
function validateDay(year, month, day) {
|
||
const maxDay = lastDayOfMonth(year, month);
|
||
if (!Number.isInteger(day) || day < 1 || day > maxDay) {
|
||
throw new ArgumentError(`日期非法:${year}-${month}-${day}(${year} 年 ${month} 月共 ${maxDay} 天)`);
|
||
}
|
||
}
|
||
|
||
function parseDate(s) {
|
||
const parts = s.split('-').map(Number);
|
||
if (parts.length !== 3 || parts.some(isNaN)) {
|
||
throw new ArgumentError(`日期格式错误:${s},应为 YYYY-MM-DD`);
|
||
}
|
||
const [year, month, day] = parts;
|
||
validateYear(year);
|
||
validateMonth(month);
|
||
validateDay(year, month, day);
|
||
return { year, month, day };
|
||
}
|
||
|
||
function parseMonth(s) {
|
||
const parts = s.split('-').map(Number);
|
||
if (parts.length !== 2 || parts.some(isNaN)) {
|
||
throw new ArgumentError(`月份格式错误:${s},应为 YYYY-MM`);
|
||
}
|
||
const [year, month] = parts;
|
||
validateYear(year);
|
||
validateMonth(month);
|
||
return { year, month };
|
||
}
|
||
|
||
function dateToKey(d) {
|
||
return `${d.year}-${String(d.month).padStart(2, '0')}-${String(d.day).padStart(2, '0')}`;
|
||
}
|
||
|
||
/** 将各种时间参数统一解析为 {start, end} */
|
||
function resolveRange(kwargs) {
|
||
const dateStr = kwargs.date;
|
||
const monthStr = kwargs.month;
|
||
const yearArg = kwargs.year;
|
||
const fromStr = kwargs.from;
|
||
const toStr = kwargs.to;
|
||
|
||
// --from / --to 优先级最高
|
||
if (fromStr || toStr) {
|
||
if (!fromStr) throw new ArgumentError('使用 --to 时必须同时指定 --from');
|
||
const start = parseDate(fromStr);
|
||
const end = toStr ? parseDate(toStr) : localToday();
|
||
if (dateToKey(start) > dateToKey(end)) throw new ArgumentError('--from 不能晚于 --to');
|
||
return { start, end };
|
||
}
|
||
|
||
if (yearArg !== undefined && yearArg !== null) {
|
||
validateYear(yearArg, '--year');
|
||
return {
|
||
start: { year: yearArg, month: 1, day: 1 },
|
||
end: { year: yearArg, month: 12, day: 31 },
|
||
};
|
||
}
|
||
|
||
if (monthStr) {
|
||
const { year, month } = parseMonth(monthStr);
|
||
return {
|
||
start: { year, month, day: 1 },
|
||
end: { year, month, day: lastDayOfMonth(year, month) },
|
||
};
|
||
}
|
||
|
||
if (dateStr) {
|
||
const d = parseDate(dateStr);
|
||
return { start: d, end: d };
|
||
}
|
||
|
||
// 默认:今天
|
||
const today = localToday();
|
||
return { start: today, end: today };
|
||
}
|
||
|
||
// ── API 工具 ──────────────────────────────────────────────
|
||
|
||
async function getYearDocId(page, year) {
|
||
const raw = await page.evaluate(`localStorage.getItem('daily_notes_doc_list')`);
|
||
if (!raw) return null;
|
||
const list = JSON.parse(raw);
|
||
return list.find((d) => d.name === `${year}年`)?.id ?? null;
|
||
}
|
||
|
||
async function getYearNodes(page, docId) {
|
||
const data = await mubuPost(page, '/document/edit/get', { docId });
|
||
const def = JSON.parse(data.definition);
|
||
return def.nodes ?? [];
|
||
}
|
||
|
||
/** 加载某年的所有 day 节点,返回带 dateKey 的列表 */
|
||
async function loadYearEntries(page, year) {
|
||
const docId = await getYearDocId(page, year);
|
||
if (!docId) return [];
|
||
|
||
const yearNodes = await getYearNodes(page, docId);
|
||
const entries = [];
|
||
|
||
for (const monthNode of yearNodes) {
|
||
const monthNum = parseInt(htmlToText(monthNode.text), 10);
|
||
if (!monthNode.children?.length) continue;
|
||
|
||
for (const dayNode of monthNode.children) {
|
||
const plain = htmlToText(dayNode.text).replace(/\s+/g, ' ').trim();
|
||
const compact = plain.replace(/\s/g, '');
|
||
const match = compact.match(/^(\d+)月(\d+)日/);
|
||
if (!match) continue;
|
||
const m = parseInt(match[1], 10);
|
||
const d = parseInt(match[2], 10);
|
||
if (m !== monthNum) continue;
|
||
|
||
const dateKey = dateToKey({ year, month: m, day: d });
|
||
entries.push({ dateKey, label: plain, node: dayNode });
|
||
}
|
||
}
|
||
|
||
return entries;
|
||
}
|
||
|
||
/** 收集 [start, end] 范围内涉及的所有年份 */
|
||
function yearsInRange(start, end) {
|
||
const years = [];
|
||
for (let y = start.year; y <= end.year; y++) years.push(y);
|
||
return years;
|
||
}
|
||
|
||
// ── 命令 ──────────────────────────────────────────────────
|
||
|
||
cli({
|
||
site: 'mubu',
|
||
name: 'notes',
|
||
access: 'read',
|
||
description: '读取幕布速记(默认今天)。支持 --date/--month/--year/--from/--to 指定时间范围,--list 为概览模式(日期+条数)。',
|
||
domain: 'mubu.com',
|
||
strategy: Strategy.COOKIE,
|
||
args: [
|
||
{
|
||
name: 'list',
|
||
type: 'bool',
|
||
default: false,
|
||
help: '概览模式:只输出日期和条数,不含速记内容。可与任意时间范围参数组合。',
|
||
},
|
||
{
|
||
name: 'date',
|
||
help: '单日,格式 YYYY-MM-DD。不指定时间范围则默认今天(系统本地时间)。',
|
||
},
|
||
{
|
||
name: 'month',
|
||
help: '整月,格式 YYYY-MM。',
|
||
},
|
||
{
|
||
name: 'year',
|
||
type: 'int',
|
||
help: '整年,格式 YYYY(整数)。',
|
||
},
|
||
{
|
||
name: 'from',
|
||
help: '范围起始日,格式 YYYY-MM-DD。须与 --to 同时使用。',
|
||
},
|
||
{
|
||
name: 'to',
|
||
help: '范围截止日,格式 YYYY-MM-DD。须与 --from 同时使用。',
|
||
},
|
||
{
|
||
name: 'output',
|
||
default: 'md',
|
||
help: '输出格式:md(默认,Markdown)或 text(纯文本)',
|
||
},
|
||
],
|
||
columns: ['date', 'content'],
|
||
func: async (page, kwargs) => {
|
||
const isList = kwargs.list;
|
||
const format = kwargs.output;
|
||
if (format !== 'md' && format !== 'text') {
|
||
throw new ArgumentError(`--output 只接受 md 或 text,收到:${format}`);
|
||
}
|
||
|
||
await page.goto('https://mubu.com/app');
|
||
|
||
const { start, end } = resolveRange(kwargs);
|
||
const startKey = dateToKey(start);
|
||
const endKey = dateToKey(end);
|
||
|
||
// 并行加载所有涉及年份的 day 节点,按范围过滤
|
||
const yearResults = await Promise.all(
|
||
yearsInRange(start, end).map((year) => loadYearEntries(page, year)),
|
||
);
|
||
const allEntries = yearResults
|
||
.flat()
|
||
.filter((e) => e.dateKey >= startKey && e.dateKey <= endKey);
|
||
|
||
if (allEntries.length === 0) {
|
||
const label = startKey === endKey ? startKey : `${startKey} ~ ${endKey}`;
|
||
return [{ date: label, content: '该时间段暂无速记' }];
|
||
}
|
||
|
||
// 概览模式
|
||
if (isList) {
|
||
return allEntries.map((e) => ({
|
||
date: e.label,
|
||
content: `${e.node.children?.length ?? 0} 条记录`,
|
||
}));
|
||
}
|
||
|
||
// 内容模式
|
||
const render = (children) =>
|
||
format === 'text' ? nodesToText(children) : nodesToMarkdown(children);
|
||
|
||
return allEntries
|
||
.filter((e) => e.node.children?.length)
|
||
.map((e) => ({
|
||
date: e.label,
|
||
content: render(e.node.children ?? []) || '(空)',
|
||
}));
|
||
},
|
||
});
|