@fluxvita/jovida-cli 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +76 -0
- package/README.zh-CN.md +76 -0
- package/SKILL.md +94 -0
- package/dist/api.js +73 -0
- package/dist/cli.js +188 -0
- package/dist/commands/complete.js +31 -0
- package/dist/commands/create.js +82 -0
- package/dist/commands/delete.js +14 -0
- package/dist/commands/list.js +67 -0
- package/dist/commands/login.js +33 -0
- package/dist/commands/shared.js +67 -0
- package/dist/commands/show.js +27 -0
- package/dist/commands/update.js +50 -0
- package/dist/commands/whoami.js +15 -0
- package/dist/config.js +22 -0
- package/dist/core/convert.js +225 -0
- package/dist/core/ids.js +13 -0
- package/dist/core/proto.js +204 -0
- package/dist/core/reminder.js +34 -0
- package/dist/core/types.js +3 -0
- package/dist/ctx.js +20 -0
- package/dist/lib/open-url.js +18 -0
- package/dist/machine-id.js +49 -0
- package/dist/session.js +161 -0
- package/dist/state.js +79 -0
- package/dist/sync.js +109 -0
- package/package.json +35 -0
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.cmdList = cmdList;
|
|
4
|
+
const convert_1 = require("../core/convert");
|
|
5
|
+
const DAY = 86400;
|
|
6
|
+
function startOfTodaySec() {
|
|
7
|
+
const d = new Date();
|
|
8
|
+
d.setHours(0, 0, 0, 0);
|
|
9
|
+
return Math.floor(d.getTime() / 1000);
|
|
10
|
+
}
|
|
11
|
+
/** scope/排序锚:有 due 用 due,否则 belong。 */
|
|
12
|
+
function anchorSec(e) {
|
|
13
|
+
return e.dueAt > 0 ? e.dueAt : e.belongAt;
|
|
14
|
+
}
|
|
15
|
+
function fmtWhen(e) {
|
|
16
|
+
if (e.dueAt > 0)
|
|
17
|
+
return new Date(e.dueAt * 1000).toLocaleString();
|
|
18
|
+
if (e.belongAt > 0)
|
|
19
|
+
return new Date(e.belongAt * 1000).toLocaleDateString();
|
|
20
|
+
return '';
|
|
21
|
+
}
|
|
22
|
+
function fmtLine(e) {
|
|
23
|
+
const box = e.completedAt > 0 ? '[x]' : '[ ]';
|
|
24
|
+
const when = fmtWhen(e);
|
|
25
|
+
const bell = e.reminder && e.reminder.offsetSecs.length ? ' 🔔' : '';
|
|
26
|
+
const pr = e.priority !== 'none' ? ` !${e.priority}` : '';
|
|
27
|
+
return `${box} ${e.title}${when ? ` · ${when}` : ''}${bell}${pr} (${e.entryId})`;
|
|
28
|
+
}
|
|
29
|
+
async function cmdList(ctx, a) {
|
|
30
|
+
const scope = a.scope ?? 'today';
|
|
31
|
+
const status = a.status ?? 'pending';
|
|
32
|
+
await ctx.session.ensureSession();
|
|
33
|
+
const snap = await ctx.sync.pull();
|
|
34
|
+
let items = snap.entries;
|
|
35
|
+
// status 过滤
|
|
36
|
+
items = items.filter((e) => status === 'all' ? true : status === 'completed' ? e.completedAt > 0 : e.completedAt === 0);
|
|
37
|
+
// scope 过滤(客户端,后端无 scoped 查询)
|
|
38
|
+
const todayStart = startOfTodaySec();
|
|
39
|
+
const tomorrowStart = todayStart + DAY;
|
|
40
|
+
if (scope === 'today') {
|
|
41
|
+
items = items.filter((e) => anchorSec(e) > 0 && anchorSec(e) < tomorrowStart); // 今天及更早(含逾期)
|
|
42
|
+
}
|
|
43
|
+
else if (scope === 'upcoming') {
|
|
44
|
+
items = items.filter((e) => anchorSec(e) >= tomorrowStart);
|
|
45
|
+
}
|
|
46
|
+
else if (scope === 'range') {
|
|
47
|
+
const f = a.from ? (0, convert_1.belongDateToSec)(a.from) : 0;
|
|
48
|
+
const t = a.to ? (0, convert_1.belongDateToSec)(a.to) + DAY : Number.POSITIVE_INFINITY;
|
|
49
|
+
items = items.filter((e) => anchorSec(e) >= f && anchorSec(e) < t);
|
|
50
|
+
} // 'all' / 'recent' 不按日期过滤
|
|
51
|
+
// 排序
|
|
52
|
+
if (scope === 'recent')
|
|
53
|
+
items.sort((x, y) => y.updatedAt - x.updatedAt);
|
|
54
|
+
else
|
|
55
|
+
items.sort((x, y) => (anchorSec(x) || Number.POSITIVE_INFINITY) - (anchorSec(y) || Number.POSITIVE_INFINITY));
|
|
56
|
+
items = items.slice(0, a.limit ?? 20);
|
|
57
|
+
if (a.json) {
|
|
58
|
+
console.log(JSON.stringify({ todos: items.map(convert_1.toListItem) }, null, 2));
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
if (!items.length) {
|
|
62
|
+
console.log('(no todos)');
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
for (const e of items)
|
|
66
|
+
console.log(fmtLine(e));
|
|
67
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.cmdLogin = cmdLogin;
|
|
4
|
+
const open_url_1 = require("../lib/open-url");
|
|
5
|
+
/** 设备流发起后,向用户展示 URL + 短码(并尽力开浏览器)。走 stderr,不污染 --json stdout。 */
|
|
6
|
+
function present(d) {
|
|
7
|
+
const opened = (0, open_url_1.tryOpenBrowser)(d.verificationUriComplete);
|
|
8
|
+
process.stderr.write('\nTo sign in, open this URL in a browser:\n');
|
|
9
|
+
process.stderr.write(` ${d.verificationUri}\n`);
|
|
10
|
+
process.stderr.write('and enter the code:\n');
|
|
11
|
+
process.stderr.write(` ${d.userCode}\n\n`);
|
|
12
|
+
if (opened)
|
|
13
|
+
process.stderr.write('(opened your browser automatically)\n');
|
|
14
|
+
process.stderr.write('Waiting for approval…\n');
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* 登录。
|
|
18
|
+
* - 默认 = 设备授权流:device_authorize → 展示 URL+短码 → 轮询 device_token → 落盘 Sign token。
|
|
19
|
+
* - `--token` = 过渡流(开发期):直接粘一枚 Sign 态 vita token。
|
|
20
|
+
*/
|
|
21
|
+
async function cmdLogin(ctx, a) {
|
|
22
|
+
let rec;
|
|
23
|
+
if (a.token) {
|
|
24
|
+
rec = await ctx.session.loginWithToken(a.token);
|
|
25
|
+
}
|
|
26
|
+
else {
|
|
27
|
+
rec = await ctx.session.loginWithDeviceFlow(present);
|
|
28
|
+
}
|
|
29
|
+
if (a.json)
|
|
30
|
+
console.log(JSON.stringify({ vitaId: rec.vitaId, baseUrl: ctx.baseUrl }));
|
|
31
|
+
else
|
|
32
|
+
console.log(`\n✓ signed in vitaId=${rec.vitaId || '(unknown)'} (${ctx.baseUrl})`);
|
|
33
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.NotFoundError = exports.nowSec = void 0;
|
|
4
|
+
exports.draftToEntry = draftToEntry;
|
|
5
|
+
exports.draftToRecurring = draftToRecurring;
|
|
6
|
+
exports.fetchEntry = fetchEntry;
|
|
7
|
+
const ids_1 = require("../core/ids");
|
|
8
|
+
const nowSec = () => Math.floor(Date.now() / 1000);
|
|
9
|
+
exports.nowSec = nowSec;
|
|
10
|
+
/** 目标 entry 不存在(exit code 4)。 */
|
|
11
|
+
class NotFoundError extends Error {
|
|
12
|
+
constructor(id) {
|
|
13
|
+
super(`todo not found: ${id}`);
|
|
14
|
+
this.name = 'NotFoundError';
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
exports.NotFoundError = NotFoundError;
|
|
18
|
+
/** TodoDraft(部分字段)→ 完整 TodoEntry(补 id/时间戳/默认值)。 */
|
|
19
|
+
function draftToEntry(d) {
|
|
20
|
+
const t = (0, exports.nowSec)();
|
|
21
|
+
return {
|
|
22
|
+
entryId: (0, ids_1.newEntryId)(),
|
|
23
|
+
title: d.title,
|
|
24
|
+
description: d.description ?? '',
|
|
25
|
+
category: d.category ?? '',
|
|
26
|
+
priority: d.priority ?? 'none',
|
|
27
|
+
dueAt: d.dueAt ?? 0,
|
|
28
|
+
belongAt: d.belongAt ?? 0,
|
|
29
|
+
recurringId: '',
|
|
30
|
+
occurrenceAt: 0,
|
|
31
|
+
subtasks: d.subtasks ?? [],
|
|
32
|
+
reminder: d.reminder ?? null,
|
|
33
|
+
completedAt: 0,
|
|
34
|
+
createdAt: t,
|
|
35
|
+
updatedAt: t,
|
|
36
|
+
hint: d.hint ?? ''
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
/** 带 repeat 的 TodoDraft → 完整 TodoRecurring「类」(补 id/时间戳/默认值)。dueAt/belongAt = 首次发生(种子)。 */
|
|
40
|
+
function draftToRecurring(d) {
|
|
41
|
+
if (!d.repeat)
|
|
42
|
+
throw new Error('internal: draftToRecurring requires a repeat rule');
|
|
43
|
+
const t = (0, exports.nowSec)();
|
|
44
|
+
return {
|
|
45
|
+
recurringId: (0, ids_1.newRecurringId)(),
|
|
46
|
+
title: d.title,
|
|
47
|
+
description: d.description ?? '',
|
|
48
|
+
category: d.category ?? '',
|
|
49
|
+
priority: d.priority ?? 'none',
|
|
50
|
+
dueAt: d.dueAt ?? 0,
|
|
51
|
+
belongAt: d.belongAt ?? 0,
|
|
52
|
+
subtasks: d.subtasks ?? [],
|
|
53
|
+
reminder: d.reminder ?? null,
|
|
54
|
+
repeat: d.repeat,
|
|
55
|
+
createdAt: t,
|
|
56
|
+
updatedAt: t
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
/** 确保登录 + 拉快照找 entry;未找到 → NotFoundError。 */
|
|
60
|
+
async function fetchEntry(ctx, id) {
|
|
61
|
+
await ctx.session.ensureSession();
|
|
62
|
+
const snap = await ctx.sync.pull();
|
|
63
|
+
const e = snap.entries.find((x) => x.entryId === id);
|
|
64
|
+
if (!e)
|
|
65
|
+
throw new NotFoundError(id);
|
|
66
|
+
return e;
|
|
67
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.cmdShow = cmdShow;
|
|
4
|
+
const convert_1 = require("../core/convert");
|
|
5
|
+
const shared_1 = require("./shared");
|
|
6
|
+
async function cmdShow(ctx, a) {
|
|
7
|
+
if (!a.id)
|
|
8
|
+
throw new Error('entry_id required: jovida show <entry_id>');
|
|
9
|
+
const e = await (0, shared_1.fetchEntry)(ctx, a.id);
|
|
10
|
+
const full = (0, convert_1.toFullTodo)(e);
|
|
11
|
+
if (a.json) {
|
|
12
|
+
console.log(JSON.stringify(full, null, 2));
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
const lines = [
|
|
16
|
+
`${e.completedAt > 0 ? '[x]' : '[ ]'} ${e.title}`,
|
|
17
|
+
` id ${e.entryId}`,
|
|
18
|
+
full.when ? ` when ${full.when}` : '',
|
|
19
|
+
` priority ${e.priority}`,
|
|
20
|
+
e.category ? ` list ${e.category}` : '',
|
|
21
|
+
e.description ? ` note ${e.description}` : '',
|
|
22
|
+
full.remind_at ? ` remind ${full.remind_at.join(', ')}` : '',
|
|
23
|
+
e.subtasks.length ? ` subtasks ${e.subtasks.map((s) => `${s.completedAt > 0 ? '✓' : '·'} ${s.title}`).join(' ')}` : '',
|
|
24
|
+
e.hint ? ` hint ${e.hint}` : ''
|
|
25
|
+
].filter(Boolean);
|
|
26
|
+
console.log(lines.join('\n'));
|
|
27
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.cmdUpdate = cmdUpdate;
|
|
4
|
+
const convert_1 = require("../core/convert");
|
|
5
|
+
const shared_1 = require("./shared");
|
|
6
|
+
const PRIORITIES = ['none', 'low', 'medium', 'high'];
|
|
7
|
+
async function cmdUpdate(ctx, a) {
|
|
8
|
+
if (!a.id)
|
|
9
|
+
throw new Error('entry_id required: jovida update <entry_id> [--title ...] ...');
|
|
10
|
+
if (a.priority && !PRIORITIES.includes(a.priority)) {
|
|
11
|
+
throw new Error(`--priority must be one of: ${PRIORITIES.join(', ')}`);
|
|
12
|
+
}
|
|
13
|
+
const entry = await (0, shared_1.fetchEntry)(ctx, a.id); // 含 ensureSession + pull(追平版本)
|
|
14
|
+
const changes = {};
|
|
15
|
+
if (a.title !== undefined)
|
|
16
|
+
changes.title = a.title;
|
|
17
|
+
if (a.when !== undefined)
|
|
18
|
+
changes.when = a.when;
|
|
19
|
+
if (a.priority !== undefined)
|
|
20
|
+
changes.priority = a.priority;
|
|
21
|
+
if (a.category !== undefined)
|
|
22
|
+
changes.category = a.category;
|
|
23
|
+
if (a.desc !== undefined)
|
|
24
|
+
changes.description = a.desc;
|
|
25
|
+
if (a.remind !== undefined)
|
|
26
|
+
changes.remind_at = a.remind;
|
|
27
|
+
if (a.subtask !== undefined)
|
|
28
|
+
changes.subtasks = a.subtask.map((s) => ({ title: s }));
|
|
29
|
+
if (a.hint !== undefined)
|
|
30
|
+
changes.hint = a.hint;
|
|
31
|
+
const d = (0, convert_1.mergeDraft)(entry, changes);
|
|
32
|
+
const updated = {
|
|
33
|
+
...entry,
|
|
34
|
+
title: d.title,
|
|
35
|
+
description: d.description ?? '',
|
|
36
|
+
category: d.category ?? '',
|
|
37
|
+
priority: d.priority ?? 'none',
|
|
38
|
+
dueAt: d.dueAt ?? 0,
|
|
39
|
+
belongAt: d.belongAt ?? 0,
|
|
40
|
+
subtasks: d.subtasks ?? [],
|
|
41
|
+
reminder: d.reminder ?? null,
|
|
42
|
+
hint: d.hint ?? '',
|
|
43
|
+
updatedAt: (0, shared_1.nowSec)()
|
|
44
|
+
};
|
|
45
|
+
await ctx.sync.putEntries([updated]);
|
|
46
|
+
if (a.json)
|
|
47
|
+
console.log(JSON.stringify({ entry_id: updated.entryId, status: 'updated' }));
|
|
48
|
+
else
|
|
49
|
+
console.log(`✓ updated ${updated.title} (${updated.entryId})`);
|
|
50
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.cmdWhoami = cmdWhoami;
|
|
4
|
+
/** 在线查当前身份(get_user_info)。无凭证 → NotSignedIn(exit 2)。 */
|
|
5
|
+
async function cmdWhoami(ctx, a) {
|
|
6
|
+
const info = await ctx.session.whoami();
|
|
7
|
+
if (a.json) {
|
|
8
|
+
console.log(JSON.stringify({ ...info, baseUrl: ctx.baseUrl }));
|
|
9
|
+
return;
|
|
10
|
+
}
|
|
11
|
+
console.log(`vitaHao: ${info.vitaHao || '(none)'}`);
|
|
12
|
+
console.log(`vitaId: ${info.vitaId || '(none)'}`);
|
|
13
|
+
console.log(`entitlement: ${info.entitlement || '(none)'}`);
|
|
14
|
+
console.log(`baseUrl: ${ctx.baseUrl}`);
|
|
15
|
+
}
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.APP_VERSION = void 0;
|
|
4
|
+
exports.loadConfig = loadConfig;
|
|
5
|
+
exports.platformName = platformName;
|
|
6
|
+
const DEFAULT_BASE_URL = 'https://tapi.jovida.ai'; // 生产(公开默认)
|
|
7
|
+
const DEFAULT_APP_ID = '2012'; // Vita-Aid:复用 Jovida TODO app_id(同 group → 同 vitaID)
|
|
8
|
+
function loadConfig() {
|
|
9
|
+
return {
|
|
10
|
+
baseUrl: process.env['JOVIDA_API_URL'] || DEFAULT_BASE_URL,
|
|
11
|
+
appId: process.env['JOVIDA_APP_ID'] || DEFAULT_APP_ID
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
exports.APP_VERSION = '0.0.1';
|
|
15
|
+
/** process.platform → Vita-Platform 值。 */
|
|
16
|
+
function platformName() {
|
|
17
|
+
if (process.platform === 'darwin')
|
|
18
|
+
return 'macos';
|
|
19
|
+
if (process.platform === 'win32')
|
|
20
|
+
return 'windows';
|
|
21
|
+
return 'linux';
|
|
22
|
+
}
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.isoToSec = isoToSec;
|
|
4
|
+
exports.secToIso = secToIso;
|
|
5
|
+
exports.belongDateToSec = belongDateToSec;
|
|
6
|
+
exports.secToBelongDate = secToBelongDate;
|
|
7
|
+
exports.repeatToOutput = repeatToOutput;
|
|
8
|
+
exports.toDraft = toDraft;
|
|
9
|
+
exports.mergeDraft = mergeDraft;
|
|
10
|
+
exports.toListItem = toListItem;
|
|
11
|
+
exports.toFullTodo = toFullTodo;
|
|
12
|
+
exports.toSeriesTodo = toSeriesTodo;
|
|
13
|
+
const ids_1 = require("./ids");
|
|
14
|
+
const reminder_1 = require("./reminder");
|
|
15
|
+
// ---- 标量 ----
|
|
16
|
+
function isoToSec(iso) {
|
|
17
|
+
const ms = Date.parse(iso);
|
|
18
|
+
if (Number.isNaN(ms))
|
|
19
|
+
throw new Error(`Cannot parse time (ISO 8601 required): ${iso}`);
|
|
20
|
+
return Math.floor(ms / 1000);
|
|
21
|
+
}
|
|
22
|
+
function secToIso(sec) {
|
|
23
|
+
return new Date(sec * 1000).toISOString();
|
|
24
|
+
}
|
|
25
|
+
/** YYYY-MM-DD → 本地时区当天 0 点的 Unix 秒(belong_at 锚点)。 */
|
|
26
|
+
function belongDateToSec(date) {
|
|
27
|
+
const m = /^(\d{4})-(\d{2})-(\d{2})$/.exec(date);
|
|
28
|
+
if (!m)
|
|
29
|
+
throw new Error(`Date must be YYYY-MM-DD: ${date}`);
|
|
30
|
+
return Math.floor(new Date(Number(m[1]), Number(m[2]) - 1, Number(m[3])).getTime() / 1000);
|
|
31
|
+
}
|
|
32
|
+
/** Unix 秒 → 本地时区 YYYY-MM-DD。 */
|
|
33
|
+
function secToBelongDate(sec) {
|
|
34
|
+
const d = new Date(sec * 1000);
|
|
35
|
+
const p = (n) => String(n).padStart(2, '0');
|
|
36
|
+
return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())}`;
|
|
37
|
+
}
|
|
38
|
+
const DATE_ONLY = /^\d{4}-\d{2}-\d{2}$/;
|
|
39
|
+
/**
|
|
40
|
+
* 单个 `when` → 存储的 { dueAt, belongAt }。
|
|
41
|
+
* - 纯日期 `2026-06-05` → 那天的事:belongAt=当天 0 点,无 due。
|
|
42
|
+
* - 带时刻 `2026-06-05T18:00:00+08:00` → 精确截止:dueAt=该时刻,belongAt 派生=due 那天 0 点。
|
|
43
|
+
* 不变量:有 due ⇒ belong = due 那天(belong/due 是同一时间点的两种精度,不允许分属不同天)。
|
|
44
|
+
*/
|
|
45
|
+
function whenToTime(when) {
|
|
46
|
+
if (when === undefined)
|
|
47
|
+
return {};
|
|
48
|
+
if (DATE_ONLY.test(when))
|
|
49
|
+
return { belongAt: belongDateToSec(when) };
|
|
50
|
+
const dueAt = isoToSec(when);
|
|
51
|
+
return { dueAt, belongAt: belongDateToSec(secToBelongDate(dueAt)) };
|
|
52
|
+
}
|
|
53
|
+
// repeat 入参 → 存储 RepeatRule。until(日期或时刻)→ endAt(落在结束日即可,发生计算按「日」比较)。
|
|
54
|
+
function toRepeat(r) {
|
|
55
|
+
if (!r)
|
|
56
|
+
return undefined;
|
|
57
|
+
let endAt = 0;
|
|
58
|
+
if (r.until)
|
|
59
|
+
endAt = DATE_ONLY.test(r.until) ? belongDateToSec(r.until) : isoToSec(r.until);
|
|
60
|
+
return {
|
|
61
|
+
unit: r.unit,
|
|
62
|
+
interval: r.interval && r.interval > 0 ? r.interval : 1,
|
|
63
|
+
weekdays: (r.weekdays ?? []).filter((w) => Number.isInteger(w) && w >= 1 && w <= 7),
|
|
64
|
+
dayOfMonth: r.day_of_month ?? 0,
|
|
65
|
+
monthOfYear: r.month_of_year ?? 0,
|
|
66
|
+
endAt
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
function toSubtasks(items) {
|
|
70
|
+
return items?.map((s) => ({ id: (0, ids_1.newSubtaskId)(), title: s.title, completedAt: 0 }));
|
|
71
|
+
}
|
|
72
|
+
// reminder 触发锚 reminderAnchorSec 见 capability/reminder/triggers.ts(与调度器共用)。
|
|
73
|
+
// remind_at(一个或多个 ISO 绝对时刻)→ Reminder。每个 offset = 锚 − remind_at,须 ≥0(提醒只能在锚前)。
|
|
74
|
+
function toReminder(remindAt, dueAt, belongAt) {
|
|
75
|
+
if (remindAt === undefined)
|
|
76
|
+
return undefined;
|
|
77
|
+
const list = Array.isArray(remindAt) ? remindAt : [remindAt];
|
|
78
|
+
if (list.length === 0)
|
|
79
|
+
return undefined;
|
|
80
|
+
const anchor = (0, reminder_1.reminderAnchorSec)(dueAt, belongAt);
|
|
81
|
+
if (anchor === undefined)
|
|
82
|
+
throw new Error('A reminder needs the todo to have a time first (when: a date or datetime)');
|
|
83
|
+
const offsetSecs = list.map((r) => {
|
|
84
|
+
const off = anchor - isoToSec(r);
|
|
85
|
+
if (off < 0)
|
|
86
|
+
throw new Error(`Reminder time (${r}) is after the todo — it must be at or before the deadline / end of the belong day`);
|
|
87
|
+
return off;
|
|
88
|
+
});
|
|
89
|
+
return { id: (0, ids_1.newReminderId)(), canAlarm: true, offsetSecs };
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* 无 when 但有 remind_at 时的归属兜底(用户定 2026-06-09):belongAt = 【最晚】一条提醒的日期 0 点,
|
|
93
|
+
* 不设 due(提醒≠截止)。否则 toReminder 因无锚抛错 → 提案静默建不出来。
|
|
94
|
+
* 取「最晚」(而非最早):锚 = belong+1天(归属日结束)须 ≥ 所有提醒才都合法、不被「晚于归属」拒。
|
|
95
|
+
*/
|
|
96
|
+
function belongFromReminders(remindAt) {
|
|
97
|
+
if (remindAt === undefined)
|
|
98
|
+
return undefined;
|
|
99
|
+
const list = Array.isArray(remindAt) ? remindAt : [remindAt];
|
|
100
|
+
if (list.length === 0)
|
|
101
|
+
return undefined;
|
|
102
|
+
const latest = Math.max(...list.map(isoToSec));
|
|
103
|
+
return belongDateToSec(secToBelongDate(latest));
|
|
104
|
+
}
|
|
105
|
+
// 存储 Reminder → remind_at 列表(ISO):锚 − 各 offset。用于读工具回显(与入参对称)。
|
|
106
|
+
// 结构化入参(dueAt/belongAt/reminder)→ entry 与 recurring「类」通用。
|
|
107
|
+
function reminderToIsoList(e) {
|
|
108
|
+
if (!e.reminder || e.reminder.offsetSecs.length === 0)
|
|
109
|
+
return undefined;
|
|
110
|
+
const anchor = (0, reminder_1.reminderAnchorSec)(e.dueAt, e.belongAt);
|
|
111
|
+
if (anchor === undefined)
|
|
112
|
+
return undefined;
|
|
113
|
+
return e.reminder.offsetSecs.map((o) => secToIso(anchor - o));
|
|
114
|
+
}
|
|
115
|
+
// 存储 RepeatRule → 输出(与入参对称,按 unit 只给相关字段)。
|
|
116
|
+
function repeatToOutput(r) {
|
|
117
|
+
const out = { unit: r.unit, interval: r.interval };
|
|
118
|
+
if (r.unit === 'week' && r.weekdays.length > 0)
|
|
119
|
+
out.weekdays = r.weekdays;
|
|
120
|
+
if ((r.unit === 'month' || r.unit === 'year') && r.dayOfMonth > 0)
|
|
121
|
+
out.day_of_month = r.dayOfMonth;
|
|
122
|
+
if (r.unit === 'year' && r.monthOfYear > 0)
|
|
123
|
+
out.month_of_year = r.monthOfYear;
|
|
124
|
+
if (r.endAt > 0)
|
|
125
|
+
out.until = secToBelongDate(r.endAt);
|
|
126
|
+
return out;
|
|
127
|
+
}
|
|
128
|
+
// 存储 → 单个 when:有 due 回显完整时刻,否则回显归属日期。
|
|
129
|
+
function toWhen(e) {
|
|
130
|
+
if (e.dueAt > 0)
|
|
131
|
+
return secToIso(e.dueAt);
|
|
132
|
+
if (e.belongAt > 0)
|
|
133
|
+
return secToBelongDate(e.belongAt);
|
|
134
|
+
return undefined;
|
|
135
|
+
}
|
|
136
|
+
function toDraft(item) {
|
|
137
|
+
const { dueAt, belongAt: belongFromWhen } = whenToTime(item.when);
|
|
138
|
+
// 无 when(due/belong 皆无)但有 remind_at → 归属兜底到最晚提醒那天,使 toReminder 有锚可算。
|
|
139
|
+
const belongAt = belongFromWhen ?? belongFromReminders(item.remind_at);
|
|
140
|
+
return {
|
|
141
|
+
title: item.title,
|
|
142
|
+
description: item.description,
|
|
143
|
+
category: item.category,
|
|
144
|
+
priority: item.priority,
|
|
145
|
+
dueAt,
|
|
146
|
+
belongAt,
|
|
147
|
+
subtasks: toSubtasks(item.subtasks),
|
|
148
|
+
reminder: toReminder(item.remind_at, dueAt, belongAt),
|
|
149
|
+
hint: item.hint,
|
|
150
|
+
repeat: toRepeat(item.repeat)
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
/** update:读当前 target、用 changes 覆盖,产出完整 draft(与后端整条覆盖语义一致)。 */
|
|
154
|
+
function mergeDraft(target, changes) {
|
|
155
|
+
// 改了 when 才重新分流;否则沿用 target 的 due/belong。
|
|
156
|
+
const { dueAt, belongAt: belongFromWhen } = changes.when !== undefined
|
|
157
|
+
? whenToTime(changes.when)
|
|
158
|
+
: { dueAt: target.dueAt || undefined, belongAt: target.belongAt || undefined };
|
|
159
|
+
// 仍无 due/belong(target 也没)但本次改了 remind_at → 同 toDraft,兜底到最晚提醒那天。
|
|
160
|
+
const belongAt = belongFromWhen ?? belongFromReminders(changes.remind_at);
|
|
161
|
+
return {
|
|
162
|
+
title: changes.title ?? target.title,
|
|
163
|
+
description: changes.description ?? target.description,
|
|
164
|
+
category: changes.category ?? target.category,
|
|
165
|
+
priority: changes.priority ?? target.priority,
|
|
166
|
+
dueAt,
|
|
167
|
+
belongAt,
|
|
168
|
+
subtasks: changes.subtasks !== undefined ? toSubtasks(changes.subtasks) : target.subtasks,
|
|
169
|
+
// 改了 remind_at 才按新锚重算;否则保留原 offset(锚变则提醒时刻随之平移,符合"提前量"语义)
|
|
170
|
+
reminder: changes.remind_at !== undefined ? toReminder(changes.remind_at, dueAt, belongAt) : (target.reminder ?? undefined),
|
|
171
|
+
hint: changes.hint ?? target.hint
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
// ---- 存储 → 接口返回 ----
|
|
175
|
+
function toListItem(e) {
|
|
176
|
+
return {
|
|
177
|
+
entry_id: e.entryId,
|
|
178
|
+
title: e.title,
|
|
179
|
+
when: toWhen(e),
|
|
180
|
+
priority: e.priority,
|
|
181
|
+
status: e.completedAt > 0 ? 'completed' : 'pending',
|
|
182
|
+
category: e.category,
|
|
183
|
+
// 循环发生(材料化的或虚发生)带回 recurring_id,AI 据此知道这是循环待办的某次发生。
|
|
184
|
+
recurring_id: e.recurringId || undefined
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
/** entry 完整视图。`repeat` 仅当调用方查到所属「类」时传入(材料化/虚发生回显其循环规则)。 */
|
|
188
|
+
function toFullTodo(e, repeat) {
|
|
189
|
+
return {
|
|
190
|
+
entry_id: e.entryId,
|
|
191
|
+
title: e.title,
|
|
192
|
+
description: e.description,
|
|
193
|
+
category: e.category,
|
|
194
|
+
priority: e.priority,
|
|
195
|
+
when: toWhen(e),
|
|
196
|
+
subtasks: e.subtasks.map((s) => ({ title: s.title, completed: s.completedAt > 0 })),
|
|
197
|
+
remind_at: reminderToIsoList(e),
|
|
198
|
+
hint: e.hint || undefined,
|
|
199
|
+
status: e.completedAt > 0 ? 'completed' : 'pending',
|
|
200
|
+
completed_at: e.completedAt > 0 ? secToIso(e.completedAt) : undefined,
|
|
201
|
+
recurring_id: e.recurringId || undefined,
|
|
202
|
+
occurrence_at: e.occurrenceAt > 0 ? secToBelongDate(e.occurrenceAt) : undefined,
|
|
203
|
+
repeat: repeat ? repeatToOutput(repeat) : undefined,
|
|
204
|
+
created_at: secToIso(e.createdAt),
|
|
205
|
+
updated_at: secToIso(e.updatedAt)
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
/** 循环「类」视图(todo_get 传入 recurring_id 时返回)。nextOccurrences = 近几次发生日(YYYY-MM-DD)。 */
|
|
209
|
+
function toSeriesTodo(s, nextOccurrences) {
|
|
210
|
+
return {
|
|
211
|
+
recurring_id: s.recurringId,
|
|
212
|
+
type: 'recurring',
|
|
213
|
+
title: s.title,
|
|
214
|
+
description: s.description,
|
|
215
|
+
category: s.category,
|
|
216
|
+
priority: s.priority,
|
|
217
|
+
when: s.dueAt > 0 ? secToIso(s.dueAt) : secToBelongDate(s.belongAt), // 首次发生(种子)
|
|
218
|
+
subtasks: s.subtasks.map((st) => ({ title: st.title, completed: st.completedAt > 0 })),
|
|
219
|
+
remind_at: reminderToIsoList(s),
|
|
220
|
+
repeat: repeatToOutput(s.repeat),
|
|
221
|
+
next_occurrences: nextOccurrences,
|
|
222
|
+
created_at: secToIso(s.createdAt),
|
|
223
|
+
updated_at: secToIso(s.updatedAt)
|
|
224
|
+
};
|
|
225
|
+
}
|
package/dist/core/ids.js
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.newRecurringId = exports.newReminderId = exports.newSubtaskId = exports.newEntryId = void 0;
|
|
4
|
+
const ulid_1 = require("ulid");
|
|
5
|
+
// id 命名空间:entry=`cli_`+ULID(标明本端来源)、subtask=`sub_`、reminder=`rem_`、循环「类」=`series_`+ULID。
|
|
6
|
+
const newEntryId = () => `cli_${(0, ulid_1.ulid)()}`;
|
|
7
|
+
exports.newEntryId = newEntryId;
|
|
8
|
+
const newSubtaskId = () => `sub_${(0, ulid_1.ulid)()}`;
|
|
9
|
+
exports.newSubtaskId = newSubtaskId;
|
|
10
|
+
const newReminderId = () => `rem_${(0, ulid_1.ulid)()}`;
|
|
11
|
+
exports.newReminderId = newReminderId;
|
|
12
|
+
const newRecurringId = () => `series_${(0, ulid_1.ulid)()}`;
|
|
13
|
+
exports.newRecurringId = newRecurringId;
|