feat(mindspace): 0630004 空间 UI、聊天连接、微信分享与 Agent 能力

含 MindSpace 三列布局与统计修复、聊天加载态与连接降级、平台页脚标记与 og:site_name 微信卡片、勾选资料删除 Agent 接口及内部话术过滤。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
john
2026-06-30 09:30:51 +08:00
parent b1b8d3afc6
commit 722b18326f
53 changed files with 2450 additions and 313 deletions
+64 -2
View File
@@ -174,15 +174,31 @@ function resolveScheduleTimestamp({
epochMs = null,
localString = null,
timezone = DEFAULT_TIMEZONE,
fieldName = "\u65F6\u95F4"
fieldName = "\u65F6\u95F4",
now = Date.now()
} = {}) {
const local = String(localString ?? "").trim();
if (local) return parseLocalDateTimeString(local, timezone);
if (epochMs == null || epochMs === "") return null;
return assertReasonableScheduleEpoch(epochMs, { now, fieldName });
}
function assertReasonableScheduleEpoch(epochMs, { now = Date.now(), fieldName = "\u65F6\u95F4", maxPastDays = 2, maxFutureDays = 400 } = {}) {
const safe = Number(epochMs);
if (!Number.isFinite(safe) || safe <= 0) {
throw new Error(`${fieldName}\u65E0\u6548\uFF0C\u8BF7\u6539\u7528 YYYY-MM-DD HH:mm \u7684 local \u5B57\u6BB5`);
}
const min = now - maxPastDays * 864e5;
const max = now + maxFutureDays * 864e5;
if (safe < min || safe > max) {
const label = new Intl.DateTimeFormat("zh-CN", {
timeZone: normalizeTimezone(process.env.H5_DEFAULT_TIMEZONE),
dateStyle: "short",
timeStyle: "short"
}).format(new Date(safe));
throw new Error(
`${fieldName}\uFF08${label}\uFF09\u8D85\u51FA\u5408\u7406\u8303\u56F4\u3002\u8BF7\u6539\u7528 startLocal/remindLocal\uFF08YYYY-MM-DD HH:mm\uFF0C\u7528\u6237\u65F6\u533A\uFF09\uFF0C\u7981\u6B62\u81EA\u884C\u4F30\u7B97 Unix \u6BEB\u79D2\u3002`
);
}
return safe;
}
@@ -467,7 +483,7 @@ function createScheduleService(pool, options = {}) {
FROM h5_schedule_reminders r
INNER JOIN h5_schedule_items i ON i.id = r.item_id
WHERE r.user_id = ?
AND r.status IN ('pending', 'locked')
AND r.status IN ('pending', 'locked', 'sent')
AND r.remind_at >= ?
AND r.remind_at < ?
AND i.deleted_at IS NULL
@@ -494,6 +510,48 @@ function createScheduleService(pool, options = {}) {
);
return rowToItem(rows[0]);
};
const getReminder = async ({ userId, reminderId }) => {
if (!userId || !reminderId) throw new Error("\u7F3A\u5C11\u63D0\u9192\u53C2\u6570");
const [rows] = await pool.query(
`SELECT r.*,
i.title AS item_title,
i.kind AS item_kind,
i.timezone AS item_timezone,
i.start_at AS item_start_at,
i.end_at AS item_end_at
FROM h5_schedule_reminders r
INNER JOIN h5_schedule_items i ON i.id = r.item_id
WHERE r.id = ? AND r.user_id = ? AND i.deleted_at IS NULL
LIMIT 1`,
[reminderId, userId]
);
return rowToReminderWithItem(rows[0]);
};
const cancelReminder = async ({ userId, reminderId, reason = "\u7528\u6237\u5FFD\u7565" } = {}) => {
const reminder = await getReminder({ userId, reminderId });
if (!reminder) throw new Error("\u63D0\u9192\u4E0D\u5B58\u5728\u6216\u65E0\u6743\u8BBF\u95EE");
if (reminder.status === "cancelled") return reminder;
if (reminder.status === "sent") throw new Error("\u5DF2\u901A\u77E5\u7684\u63D0\u9192\u4E0D\u80FD\u5FFD\u7565");
return markReminderCancelled(reminder, reason);
};
const deleteReminder = async ({ userId, reminderId }) => {
if (!userId || !reminderId) throw new Error("\u7F3A\u5C11\u63D0\u9192\u53C2\u6570");
const [result] = await pool.query(
`DELETE FROM h5_schedule_reminders WHERE id = ? AND user_id = ?`,
[reminderId, userId]
);
return Number(result?.affectedRows ?? 0) > 0;
};
const deleteReminders = async ({ userId, reminderIds }) => {
if (!userId) throw new Error("\u7F3A\u5C11\u7528\u6237");
const ids = [...new Set((reminderIds ?? []).map((id) => String(id ?? "").trim()).filter(Boolean))];
if (ids.length === 0) return 0;
const [result] = await pool.query(
`DELETE FROM h5_schedule_reminders WHERE user_id = ? AND id IN (${ids.map(() => "?").join(", ")})`,
[userId, ...ids]
);
return Number(result?.affectedRows ?? 0);
};
const createReminder = async ({
userId,
itemId,
@@ -1041,6 +1099,10 @@ function createScheduleService(pool, options = {}) {
listTodayTodoItems,
listUpcomingItems,
listUpcomingReminders,
getReminder,
cancelReminder,
deleteReminder,
deleteReminders,
listDigestSubscriptions,
createDailyTodoDigest,
createBalanceLowAlert,
+448 -57
View File
@@ -5747,14 +5747,14 @@ function createMindSpaceService(pool2, options = {}) {
c.ai_access_policy, c.publish_policy, c.is_system, c.sort_order,
CASE
WHEN c.category_code = 'draft' THEN COALESCE(pc.item_count, 0)
WHEN c.category_code = 'public' THEN COALESCE(pub.item_count, 0)
WHEN c.category_code IN ('oa', 'public') THEN COALESCE(pc.item_count, 0) + COALESCE(ac.item_count, 0)
ELSE COALESCE(ac.item_count, 0)
END AS item_count
FROM h5_space_categories c
LEFT JOIN (
SELECT category_id, user_id, COUNT(*) AS item_count
FROM h5_assets
WHERE user_id = ? AND status <> 'deleted' AND source_type = 'upload'
WHERE user_id = ? AND status <> 'deleted'
GROUP BY category_id, user_id
) ac ON ac.category_id = c.id AND ac.user_id = c.user_id
LEFT JOIN (
@@ -5763,15 +5763,9 @@ function createMindSpaceService(pool2, options = {}) {
WHERE user_id = ? AND status <> 'deleted'
GROUP BY category_id, user_id
) pc ON pc.category_id = c.id AND pc.user_id = c.user_id
LEFT JOIN (
SELECT user_id, COUNT(DISTINCT page_id) AS item_count
FROM h5_publish_records
WHERE user_id = ? AND status = 'online'
GROUP BY user_id
) pub ON pub.user_id = c.user_id
WHERE c.user_id = ? AND c.space_id = ?
ORDER BY c.sort_order, c.category_name`,
[userId, userId, userId, userId, row.id]
[userId, userId, userId, row.id]
);
const quotaBytes = asNumber(row.quota_bytes);
const usedBytes = asNumber(row.used_bytes);
@@ -7503,7 +7497,7 @@ function renderBrandingBlock(userAddressName) {
- \u4F60\u662F **TKMind** \u52A9\u624B\uFF1B\u4ECB\u7ECD\u4EA7\u54C1\u65F6\u7528 TKMind\uFF0C\u4E0D\u8981\u79F0 goose\u3001Goose\u3001goosed
- \u4E0E\u7528\u6237\u5BF9\u8BDD\u65F6\uFF0C\u7528 **${name}** \u79F0\u547C\u7528\u6237\uFF08\u53EF\u8F85\u4EE5\u300C\u4F60/\u60A8\u300D\uFF09\uFF0C**\u7981\u6B62**\u628A\u7528\u6237\u53EB\u4F5C TKMind
- \u95EE\u5019\u793A\u4F8B\uFF1A\u300C${name}\uFF0C\u4E0B\u5348\u597D\u300D\u2014\u2014\u4E0D\u8981\u7528\u300CTKMind\uFF0C\u4E0B\u5348\u597D\u300D
- \u4EC5\u5728\u5F00\u573A\u6216\u7528\u6237\u6253\u62DB\u547C\u65F6\u4F7F\u7528\u65F6\u6BB5\u95EE\u5019\uFF0C\u4E14\u987B\u4E0E\u300CTKMind \u5F53\u524D\u65F6\u95F4\u57FA\u51C6\u300D\u4E00\u81F4\uFF08\u5982\u300C${name}\uFF0C\u65E9\u4E0A\u597D\u300D\uFF09\uFF1B\u666E\u901A\u56DE\u590D\u76F4\u63A5\u4F5C\u7B54\uFF0C\u4E0D\u8981\u6BCF\u6761\u90FD\u52A0\u95EE\u5019\uFF1B\u7981\u6B62\u628A\u7528\u6237\u53EB\u4F5C TKMind
- \u4E0D\u8981\u63CF\u8FF0\u672C\u5DE5\u4F5C\u533A\u4E3A\u300CRust goose \u9879\u76EE\u300D\u6216\u300Cgoose AI \u6846\u67B6\u300D
- \u672C\u5DE5\u4F5C\u533A\u662F TKMind **MindSpace \u7528\u6237\u7A7A\u95F4**\uFF0C\u7528\u4E8E\u6587\u4EF6\u7BA1\u7406\u4E0E\u9759\u6001\u9875\u9762\u751F\u6210
`;
@@ -7662,6 +7656,13 @@ description: \u5728\u4E13\u5C5E MindSpace \u76EE\u5F55\u751F\u6210\u53EF\u516C\u
- \u5B8C\u6574 \`<!DOCTYPE html>\`\uFF0C\`lang="zh-CN"\`
- \u79FB\u52A8\u7AEF\u53CB\u597D\uFF1A\`<meta name="viewport" content="width=device-width, initial-scale=1">\`
- \u6DF1\u8272/\u6D45\u8272\u4E0E\u5185\u5BB9\u4E00\u81F4\uFF1B\u4E3B\u8272\u5728 CSS \u4E0E mindspace-cover \u4E2D\u4FDD\u6301\u4E00\u81F4
- \u9875\u811A\u5E73\u53F0\u8054\u7CFB\u884C**\u5FC5\u987B**\u4F7F\u7528 \`data-mindspace-page-tag="platform-brand"\` \u6807\u8BB0\uFF0C\u4E14\u90AE\u7BB1/\u57DF\u540D\u53EA\u7528 **tkmind.cn**\uFF08\u5982 \`contact@tkmind.cn\`\uFF09\uFF0C**\u7981\u6B62** \`tkmind.ai\`
\`\`\`html
<p data-mindspace-page-tag="platform-brand">\u{1F4E7} contact@tkmind.cn</p>
\`\`\`
- \u5E26 \`data-mindspace-page-tag\` \u7684\u533A\u57DF\u4E3A\u5E73\u53F0\u56FA\u5B9A\u4FE1\u606F\uFF1A\u7528\u6237\u5728\u7F16\u8F91\u6A21\u5F0F\u4E2D\u4E0D\u53EF\u89C1\u3001\u4E0D\u53EF\u6539\uFF1B\u9884\u89C8\u4E0E\u53D1\u5E03\u540E\u6B63\u5E38\u663E\u793A
## \u67E5\u627E\u6587\u4EF6\uFF08CSV\u3001\u6587\u6863\u7B49\uFF09
@@ -7763,6 +7764,7 @@ function buildPublishConstraints({ slug, username, publicBaseUrl, publishDir, di
"- **\u7981\u6B62**\uFF1A\u8BBF\u95EE `assets/` \u5185\u90E8\u8DEF\u5F84\u3001\u5176\u5B83\u7528\u6237\u76EE\u5F55\u3001\u4E3B\u673A\u7EDD\u5BF9\u8DEF\u5F84\uFF1B\u7981\u6B62\u7528\u516C\u7F51 URL \u5217\u76EE\u5F55\u6216\u8BFB CSV",
"- **\u8DEF\u5F84\u89C4\u5219**\uFF1A\u53EA\u7528\u76F8\u5BF9\u8DEF\u5F84\uFF1B\u7981\u6B62 `../`\uFF1B\u5DE5\u4F5C\u533A\u5916\u7684\u8DEF\u5F84\u4F1A\u88AB\u7CFB\u7EDF\u62D2\u7EDD\uFF08OS \u5C42\u5F3A\u5236\uFF0C\u975E\u8F6F\u7EA6\u675F\uFF09",
"- **\u751F\u6210\u9875\u9762\uFF08\u5FC5\u987B\u4EB2\u81EA\u5B8C\u6210\uFF09**\uFF1A\u5148 `load_skill` \u2192 `static-page-publish`\uFF0C\u518D\u7528 `write_file`/`edit_file` \u5199\u5165 `public/\u9875\u9762.html`",
"- **\u7528\u6237\u53EF\u89C1\u56DE\u590D**\uFF1A\u4E0D\u8981\u5411\u7528\u6237\u590D\u8FF0 load_skill\u3001\u6280\u80FD\u66F4\u65B0\u3001\u9875\u811A\u6807\u8BB0\u3001mindspace-cover \u7B49\u5185\u90E8\u5B9E\u73B0\uFF1B\u5B8C\u6210\u540E\u76F4\u63A5\u7ED9\u51FA\u9875\u9762\u94FE\u63A5\u6216\u7ED3\u679C",
"- **\u7981\u6B62**\u7528 shell \u5199\u5165 HTML\uFF1B**\u7981\u6B62**\u8BA9\u7528\u6237\u300C\u624B\u52A8\u4FDD\u5B58\u5230 public \u76EE\u5F55\u300D\u6216\u8BF4\u300C\u6211\u65E0\u6CD5\u751F\u6210\u9875\u9762\u300D\u2014\u2014\u9664\u975E write_file \u5DF2\u5931\u8D25\u5E76\u62A5\u544A\u9519\u8BEF",
"- \u5B8C\u6210\u540E\u7ED9\u51FA Markdown \u53EF\u70B9\u51FB\u516C\u7F51\u94FE\u63A5 `[\u6807\u9898](URL)`\uFF1B\u5199\u5165 `public/\u9875\u9762.html` \u65F6 URL \u4E3A `.../MindSpace/<\u7528\u6237ID>/public/\u9875\u9762.html`",
`- \u53D1\u5E03\u6280\u80FD\uFF1A\`${PUBLISH_SKILL_NAME}\`\uFF08\u751F\u6210\u9875\u9762\u524D\u5E94 load_skill\uFF09`
@@ -7975,8 +7977,35 @@ function formatDateParts(now, timezone) {
day: parts.day ?? "01"
};
}
function formatLocalClockParts(now, timezone) {
const formatter = new Intl.DateTimeFormat("en-GB", {
timeZone: timezone,
hour: "2-digit",
minute: "2-digit",
hour12: false
});
const parts = Object.fromEntries(
formatter.formatToParts(new Date(now)).map((part) => [part.type, part.value])
);
return {
hour: Number(parts.hour ?? 0),
minute: Number(parts.minute ?? 0)
};
}
function resolveTimeOfDayPeriod(hour) {
const h = Number(hour);
if (h < 5) return { period: "\u51CC\u6668", greeting: "\u4F60\u597D" };
if (h < 9) return { period: "\u65E9\u4E0A", greeting: "\u65E9\u4E0A\u597D" };
if (h < 12) return { period: "\u4E0A\u5348", greeting: "\u4E0A\u5348\u597D" };
if (h < 14) return { period: "\u4E2D\u5348", greeting: "\u4E2D\u5348\u597D" };
if (h < 18) return { period: "\u4E0B\u5348", greeting: "\u4E0B\u5348\u597D" };
return { period: "\u665A\u4E0A", greeting: "\u665A\u4E0A\u597D" };
}
function renderCurrentTimeAnchor({ now = Date.now(), timezone = DEFAULT_TIMEZONE } = {}) {
const { year, month, day } = formatDateParts(now, timezone);
const { hour, minute } = formatLocalClockParts(now, timezone);
const { period, greeting } = resolveTimeOfDayPeriod(hour);
const clock = `${String(hour).padStart(2, "0")}:${String(minute).padStart(2, "0")}`;
const weekday = new Intl.DateTimeFormat("zh-CN", {
timeZone: timezone,
weekday: "long"
@@ -7986,7 +8015,9 @@ function renderCurrentTimeAnchor({ now = Date.now(), timezone = DEFAULT_TIMEZONE
"",
`- \u5F53\u524D\u65F6\u533A\uFF1A${timezone}`,
`- \u5F53\u524D\u65E5\u671F\uFF1A${year}-${month}-${day}\uFF08${weekday}\uFF09`,
"- \u56DE\u7B54\u4E2D\u6D89\u53CA\u201C\u4ECA\u5929 / \u660E\u5929 / \u540E\u5929 / \u5468\u51E0\u201D\u65F6\uFF0C\u5FC5\u987B\u4EE5\u4E0A\u8FF0\u65E5\u671F\u4E3A\u51C6\u7EE7\u7EED\u63A8\u7B97\uFF0C\u4E0D\u8981\u81EA\u884C\u5047\u8BBE\u5F53\u524D\u65E5\u671F\u3002"
`- \u5F53\u524D\u65F6\u523B\uFF1A${clock}\uFF08${period}\uFF09`,
`- \u82E5\u9700\u65F6\u6BB5\u95EE\u5019\u53EF\u53C2\u8003\uFF1A${greeting}\uFF08\u4EC5\u65B0\u4F1A\u8BDD\u5F00\u573A\u6216\u7528\u6237\u4E3B\u52A8\u6253\u62DB\u547C\u65F6\u4F7F\u7528\uFF0C\u666E\u901A\u4EFB\u52A1\u56DE\u590D\u4E0D\u8981\u6BCF\u6761\u90FD\u52A0\uFF09`,
"- \u56DE\u7B54\u4E2D\u6D89\u53CA\u201C\u4ECA\u5929 / \u660E\u5929 / \u540E\u5929 / \u5468\u51E0 / \u65E9\u4E0A / \u4E0B\u5348 / \u665A\u4E0A\u201D\u7B49\u65F6\u95F4\u8868\u8FF0\u65F6\uFF0C\u5FC5\u987B\u4EE5\u4E0A\u8FF0\u65E5\u671F\u4E0E\u65F6\u523B\u4E3A\u51C6\uFF0C\u7981\u6B62\u81EA\u884C\u5047\u8BBE\u5F53\u524D\u65F6\u95F4\u6216\u4F7F\u7528 UTC \u7B49\u5176\u5B83\u65F6\u533A\u3002"
].join("\n");
}
function buildSessionMemoryEntries({
@@ -8018,6 +8049,18 @@ function buildSessionMemoryEntries({
title: "TKMind \u5F53\u524D\u65F6\u95F4\u57FA\u51C6",
content: renderCurrentTimeAnchor({ now, timezone })
});
const scheduleTools = (sessionPolicy?.extensionOverrides ?? []).find((ext) => ext.name === "sandbox-fs");
if ((scheduleTools?.available_tools ?? []).includes("schedule_create_item")) {
entries.push({
title: "TKMind \u65E5\u7A0B\u5199\u5165\u89C4\u5219",
content: [
"\u521B\u5EFA\u6216\u63D0\u9192\u5F85\u529E/\u65E5\u7A0B\u65F6\uFF1A",
"- \u5FC5\u987B\u4F7F\u7528 startLocal / endLocal / remindLocal\uFF0C\u683C\u5F0F YYYY-MM-DD HH:mm\uFF08\u7528\u6237\u65F6\u533A\u5899\u4E0A\u65F6\u949F\uFF09\u3002",
"- \u7981\u6B62\u81EA\u884C\u4F30\u7B97 Unix \u6BEB\u79D2\u65F6\u95F4\u6233\uFF1B\u4F20\u9519\u4F1A\u88AB\u5DE5\u5177\u62D2\u7EDD\u3002",
"- \u5199\u5165\u540E\u8C03\u7528 schedule_list_items \u6838\u5BF9\u65E5\u671F\u662F\u5426\u4E0E\u7528\u6237\u8868\u8FF0\u4E00\u81F4\u3002"
].join("\n")
});
}
if (!hasMemoryStore(sessionPolicy)) {
return entries;
}
@@ -9328,7 +9371,7 @@ function createTkmindProxy({
try {
const upstream = await apiFetch(target, apiSecret, "/sessions", {
method: "GET",
signal: AbortSignal.timeout(3e3)
signal: AbortSignal.timeout(12e3)
});
const text = await upstream.text();
if (!upstream.ok) {
@@ -10016,7 +10059,7 @@ function renderUserSpaceBrandingBlock(userAddressName) {
- \u4F60\u662F **TKMind** \u52A9\u624B\uFF1B\u4ECB\u7ECD\u4EA7\u54C1\u65F6\u7528 TKMind\uFF0C\u4E0D\u8981\u79F0 goose\u3001Goose\u3001goosed
- \u4E0E\u7528\u6237\u5BF9\u8BDD\u65F6\uFF0C\u7528 **${name}** \u79F0\u547C\u7528\u6237\uFF08\u53EF\u8F85\u4EE5\u300C\u4F60/\u60A8\u300D\uFF09\uFF0C**\u7981\u6B62**\u628A\u7528\u6237\u53EB\u4F5C TKMind
- \u95EE\u5019\u793A\u4F8B\uFF1A\u300C${name}\uFF0C\u4E0B\u5348\u597D\u300D\u2014\u2014\u4E0D\u8981\u7528\u300CTKMind\uFF0C\u4E0B\u5348\u597D\u300D
- \u4EC5\u5728\u5F00\u573A\u6216\u7528\u6237\u6253\u62DB\u547C\u65F6\u4F7F\u7528\u65F6\u6BB5\u95EE\u5019\uFF0C\u4E14\u987B\u4E0E\u300CTKMind \u5F53\u524D\u65F6\u95F4\u57FA\u51C6\u300D\u4E00\u81F4\uFF08\u5982\u300C${name}\uFF0C\u65E9\u4E0A\u597D\u300D\uFF09\uFF1B\u666E\u901A\u56DE\u590D\u76F4\u63A5\u4F5C\u7B54\uFF0C\u4E0D\u8981\u6BCF\u6761\u90FD\u52A0\u95EE\u5019\uFF1B\u7981\u6B62\u628A\u7528\u6237\u53EB\u4F5C TKMind
- \u4E0D\u8981\u63CF\u8FF0\u672C\u5DE5\u4F5C\u533A\u4E3A\u300CRust goose \u9879\u76EE\u300D\u6216\u300Cgoose AI \u6846\u67B6\u300D
- \u672C\u5DE5\u4F5C\u533A\u662F TKMind **MindSpace \u7528\u6237\u7A7A\u95F4**\uFF0C\u7528\u4E8E OA/\u516C\u5F00\u6587\u4EF6\u7BA1\u7406\u4E0E\u9759\u6001\u9875\u9762\u751F\u6210
`;
@@ -16398,6 +16441,45 @@ function resolveWorkspaceRelativeFilePath(htmlRelativePath, relativeRef) {
if (!htmlDir || htmlDir === ".") return `public/${ref}`;
return path18.posix.join(htmlDir, ref).replace(/\\/g, "/");
}
function resolveSiblingDocxPath(htmlRelativePath) {
const htmlPath = String(htmlRelativePath ?? "").replace(/^\/+/, "");
if (!htmlPath.toLowerCase().endsWith(".html")) return null;
return htmlPath.replace(/\.html$/i, ".docx");
}
function inferWorkspaceHtmlRelativePath({
snapshotRelativePath = null,
pageTitle = null,
htmlContent = null,
publishDir = null
} = {}) {
const fromSnapshot = String(snapshotRelativePath ?? "").trim();
if (fromSnapshot) {
const normalized = fromSnapshot.replace(/^\/+/, "");
if (normalized.toLowerCase().startsWith("public/")) return normalized;
if (normalized.toLowerCase().endsWith(".html")) return `public/${path18.posix.basename(normalized)}`;
return normalized;
}
const publicDir = publishDir ? path18.join(publishDir, "public") : null;
if (!publicDir || !fs14.existsSync(publicDir) || !htmlContent) {
return "public/index.html";
}
const normalizedContent = String(htmlContent);
const title = String(pageTitle ?? "").trim();
for (const name of fs14.readdirSync(publicDir)) {
if (!name.toLowerCase().endsWith(".html")) continue;
const absolutePath = path18.join(publicDir, name);
try {
const diskContent = fs14.readFileSync(absolutePath, "utf8");
if (diskContent === normalizedContent) return `public/${name}`;
if (title) {
const titleMatch = diskContent.match(/<title[^>]*>([^<]+)<\/title>/i);
if (titleMatch?.[1]?.trim() === title) return `public/${name}`;
}
} catch {
}
}
return "public/index.html";
}
function buildWorkspaceDownloadLinkIndex(assets = []) {
const byBasename = /* @__PURE__ */ new Map();
const byPath = /* @__PURE__ */ new Map();
@@ -16409,6 +16491,8 @@ function buildWorkspaceDownloadLinkIndex(assets = []) {
byBasename.set(path18.posix.basename(filename), id);
if (filename.startsWith("public/")) {
byPath.set(filename.slice("public/".length), id);
} else if (!filename.includes("/")) {
byPath.set(`public/${filename}`, id);
}
}
return { byBasename, byPath };
@@ -16434,6 +16518,19 @@ function workspaceFileExists(publishDir, workspaceRelativePath) {
return false;
}
}
function collectDownloadWorkspaceCandidates(htmlRelativePath, relativeRef, publishDir) {
const workspacePath = resolveWorkspaceRelativeFilePath(htmlRelativePath, relativeRef);
const candidates = /* @__PURE__ */ new Set([
workspacePath,
path18.posix.join("public", path18.posix.basename(workspacePath))
]);
const siblingDocx = resolveSiblingDocxPath(htmlRelativePath);
if (siblingDocx) {
candidates.add(siblingDocx);
candidates.add(path18.posix.join("public", path18.posix.basename(siblingDocx)));
}
return [...candidates];
}
function resolveDownloadTargetUrl({
relativeRef,
htmlRelativePath = "public/index.html",
@@ -16444,24 +16541,25 @@ function resolveDownloadTargetUrl({
preferAssetDownload = true
}) {
const { suffix } = splitReferenceParts(relativeRef);
const workspacePath = resolveWorkspaceRelativeFilePath(htmlRelativePath, relativeRef);
const assetId = lookupAssetIdForWorkspacePath(linkIndex, workspacePath);
const publicCandidates = [
workspacePath,
path18.posix.join("public", path18.posix.basename(workspacePath))
];
if (preferAssetDownload && assetId) {
return `${buildAssetDownloadUrl(assetId)}${suffix}`;
const candidates = collectDownloadWorkspaceCandidates(htmlRelativePath, relativeRef, publishDir);
for (const candidate of candidates) {
const assetId = lookupAssetIdForWorkspacePath(linkIndex, candidate);
if (preferAssetDownload && assetId) {
return `${buildAssetDownloadUrl(assetId)}${suffix}`;
}
}
if (publishDir && publishKey) {
for (const candidate of publicCandidates) {
for (const candidate of candidates) {
if (workspaceFileExists(publishDir, candidate)) {
return `${buildPublicWorkspaceFileUrl(publicBaseUrl, publishKey, candidate)}${suffix}`;
}
}
}
if (assetId) {
return `${buildAssetDownloadUrl(assetId)}${suffix}`;
for (const candidate of candidates) {
const assetId = lookupAssetIdForWorkspacePath(linkIndex, candidate);
if (assetId) {
return `${buildAssetDownloadUrl(assetId)}${suffix}`;
}
}
return null;
}
@@ -16492,6 +16590,37 @@ async function prepareHtmlDownloadLinks(pool2, userId, html, options = {}) {
return rewriteRelativeDownloadLinks(html, { ...options, linkIndex });
}
// mindspace-page-tag.mjs
var MINDSPACE_PAGE_TAG_ATTR = "data-mindspace-page-tag";
var MINDSPACE_PAGE_TAG_PLATFORM_BRAND = "platform-brand";
var PLATFORM_BRAND_INNER_RE = /(?:contact@tkmind\.(?:ai|cn)|📧[^<]*tkmind)/i;
var SIMPLE_BRAND_ELEMENT_RE = /<(p|div|span|small|footer|a)(\s[^>]*)?>([^<]*(?:contact@tkmind\.(?:ai|cn)|📧[^<]*tkmind)[^<]*)<\/\1>/gi;
function normalizePlatformDomainText(text) {
return String(text ?? "").replace(/tkmind\.ai/gi, "tkmind.cn");
}
function normalizePlatformBrandHtml(html) {
let next = normalizePlatformDomainText(html);
next = next.replace(/mailto:([^"'>\s]+@tkmind)\.ai/gi, "mailto:$1.cn");
next = next.replace(
/https?:\/\/([a-z0-9.-]*\.)?tkmind\.ai/gi,
(match) => match.replace(/tkmind\.ai/gi, "tkmind.cn")
);
return next;
}
function ensurePlatformBrandPageTags(html) {
const normalized = normalizePlatformBrandHtml(html);
return normalized.replace(SIMPLE_BRAND_ELEMENT_RE, (match, tag, attrs, inner) => {
const attrStr = attrs ?? "";
if (attrStr.includes(MINDSPACE_PAGE_TAG_ATTR)) return match;
if (!PLATFORM_BRAND_INNER_RE.test(inner)) return match;
const spacer = attrStr ? "" : " ";
return `<${tag}${attrStr}${spacer}${MINDSPACE_PAGE_TAG_ATTR}="${MINDSPACE_PAGE_TAG_PLATFORM_BRAND}">${inner}</${tag}>`;
});
}
function prepareHtmlPageBrandMarkers(html) {
return ensurePlatformBrandPageTags(html);
}
// mindspace-page-purge.mjs
import fs15 from "node:fs/promises";
import path19 from "node:path";
@@ -16623,6 +16752,13 @@ function parseJsonColumn(value, fallback = {}) {
function pageError(message, code, details) {
return Object.assign(new Error(message), { code, details });
}
function normalizeWorkspaceRelativePath2(relativePath) {
const parts = String(relativePath ?? "").replace(/^\/+/, "").split("/").filter((part) => part && part !== "." && part !== "..");
if (parts.length === 0) return "";
if (parts[0].toLowerCase() === "public") return parts.join("/");
if (parts.length === 1 && parts[0].toLowerCase().endsWith(".html")) return `public/${parts[0]}`;
return parts.join("/");
}
function normalizeText(value, maxLength, fieldName) {
const text = String(value ?? "").normalize("NFKC").trim();
if (!text) throw pageError(`${fieldName}\u4E0D\u80FD\u4E3A\u7A7A`, "invalid_page_input");
@@ -16633,13 +16769,16 @@ function normalizeText(value, maxLength, fieldName) {
}
function normalizePageInput(input) {
const title = normalizeText(input.title, MAX_TITLE_LENGTH, "\u6807\u9898");
const content = normalizeText(input.content, MAX_CONTENT_BYTES, "\u9875\u9762\u5185\u5BB9");
let content = normalizeText(input.content, MAX_CONTENT_BYTES, "\u9875\u9762\u5185\u5BB9");
const contentFormat = input.contentFormat === "html" ? "html" : "markdown";
if (contentFormat === "html") {
content = prepareHtmlPageBrandMarkers(content);
}
const contentBytes = Buffer.byteLength(content, "utf8");
if (contentBytes > MAX_CONTENT_BYTES) {
throw pageError("\u9875\u9762\u5185\u5BB9\u8D85\u8FC7 1 MB \u9650\u5236", "page_content_too_large");
}
const summary = String(input.summary ?? "").normalize("NFKC").trim().slice(0, MAX_SUMMARY_LENGTH);
const contentFormat = input.contentFormat === "html" ? "html" : "markdown";
const templateId = contentFormat === "html" ? "static-html" : TEMPLATE_IDS.has(input.templateId) ? input.templateId : "editorial";
const plainSummary = contentFormat === "html" ? content.replace(/<script[\s\S]*?<\/script>/gi, " ").replace(/<style[\s\S]*?<\/style>/gi, " ").replace(/<[^>]+>/g, " ").replace(/\s+/g, " ").trim().slice(0, 180) : content.replace(/\s+/g, " ").slice(0, 180);
return {
@@ -17105,6 +17244,24 @@ function createPageService(pool2, options = {}) {
);
return rows[0] ? pageResponse(rows[0]) : null;
};
const findPageByRelativePath = async (userId, relativePath) => {
const normalized = normalizeWorkspaceRelativePath2(relativePath);
if (!normalized) return null;
const [rows] = await pool2.query(
`SELECT p.*, c.category_code, pv.version_no, pr.access_mode AS pub_access_mode, pr.public_url AS pub_public_url
FROM h5_page_records p
JOIN h5_space_categories c ON c.id = p.category_id AND c.user_id = p.user_id
JOIN h5_page_versions pv ON pv.id = p.current_version_id
LEFT JOIN h5_publish_records pr ON pr.id = p.current_publish_id AND pr.status = 'online'
WHERE p.user_id = ?
AND p.status <> 'deleted'
AND JSON_UNQUOTE(JSON_EXTRACT(pv.source_snapshot_json, '$.relative_path')) = ?
ORDER BY p.updated_at DESC, p.id DESC
LIMIT 1`,
[userId, normalized]
);
return rows[0] ? pageResponse(rows[0]) : null;
};
const listPages = async (userId, filters = {}) => {
const clauses = [`p.user_id = ?`, `p.status <> 'deleted'`];
const params = [userId];
@@ -17250,9 +17407,9 @@ ${updated.content ?? ""}`,
changes
};
};
const loadPageWorkspaceContext = async (userId, pageId) => {
const loadPageWorkspaceContext = async (userId, pageId, htmlContent = null) => {
const [rows] = await pool2.query(
`SELECT pv.source_snapshot_json
`SELECT pv.source_snapshot_json, p.title
FROM h5_page_records p
JOIN h5_page_versions pv ON pv.id = p.current_version_id
WHERE p.id = ? AND p.user_id = ? AND p.status <> 'deleted'
@@ -17265,18 +17422,34 @@ ${updated.content ?? ""}`,
} catch {
snapshot = {};
}
const workspaceHtmlRelativePath = snapshot.relative_path ?? "public/index.html";
const workspacePublishDir = h5Root ? resolvePublishDir(h5Root, { id: userId }) : null;
const workspaceHtmlRelativePath = inferWorkspaceHtmlRelativePath({
snapshotRelativePath: snapshot.relative_path,
pageTitle: rows[0]?.title,
htmlContent,
publishDir: workspacePublishDir
});
return {
workspaceHtmlRelativePath,
workspacePublishDir,
publishKey: userId
};
};
const rewriteHtmlDownloadLinksForPage = async (userId, pageId, html) => {
const ctx = await loadPageWorkspaceContext(userId, pageId, html);
const { html: rewritten } = await prepareHtmlDownloadLinks(pool2, userId, html, {
htmlRelativePath: ctx.workspaceHtmlRelativePath,
publishDir: ctx.workspacePublishDir,
publishKey: ctx.publishKey,
publicBaseUrl: "",
preferAssetDownload: true
});
return rewritten;
};
const finalizeHtmlPreview = async (userId, pageId, html) => {
const shell = renderHtmlPreview(html);
try {
const ctx = await loadPageWorkspaceContext(userId, pageId);
const ctx = await loadPageWorkspaceContext(userId, pageId, html);
const { html: rewritten } = await prepareHtmlDownloadLinks(pool2, userId, shell, {
htmlRelativePath: ctx.workspaceHtmlRelativePath,
publishDir: ctx.workspacePublishDir,
@@ -17815,10 +17988,12 @@ ${updated.content ?? ""}`,
listPages,
findPageBySourceAsset,
findPageBySourceMessage,
findPageByRelativePath,
getPage,
getDeletePreview,
deletePage,
listVersions,
rewriteHtmlDownloadLinksForPage,
renderPreview: async (userId, pageId) => {
const page = await getPage(userId, pageId);
const html = page.contentFormat === "html" ? await finalizeHtmlPreview(userId, pageId, page.content) : renderPreviewHtml(page);
@@ -18090,6 +18265,33 @@ function createPageLiveEditService({ pageService, resolveUserIdForAgentSession }
};
}
// mindspace-asset-agent.mjs
function createAssetAgentService({ assetService, resolveUserIdForAgentSession }) {
return {
async applyAgentDelete(input = {}) {
const sessionId = String(input?.sessionId ?? input?.session_id ?? "").trim();
const assetId = String(input?.assetId ?? input?.asset_id ?? "").trim();
const confirmed = input?.confirmed === true || String(input?.confirmed ?? "").toLowerCase() === "true";
if (!sessionId || !assetId) {
throw Object.assign(new Error("\u7F3A\u5C11 session_id \u6216 asset_id"), { code: "invalid_request" });
}
if (!confirmed) {
throw Object.assign(new Error("\u5220\u9664\u524D\u987B\u5DF2\u5728\u5BF9\u8BDD\u4E2D\u83B7\u5F97\u7528\u6237\u786E\u8BA4"), { code: "confirmation_required" });
}
const userId = await resolveUserIdForAgentSession(sessionId);
if (!userId) {
throw Object.assign(new Error("\u65E0\u6548\u7684 Agent \u4F1A\u8BDD"), { code: "forbidden" });
}
const result = await assetService.deleteAsset(userId, assetId);
return {
assetId,
userId,
...result
};
}
};
}
// mindspace-page-edit-session.mjs
import { Agent as Agent2, fetch as undiciFetch2 } from "undici";
@@ -20530,7 +20732,8 @@ async function prepareHtmlPublishContent({
html,
ownerSlug,
urlSlug,
htmlRelativePath = `public/${urlSlug}.html`,
htmlRelativePath = null,
pageTitle = null,
absoluteStoragePath: absoluteStoragePath2,
imgproxySigner = null,
h5Root = null
@@ -20547,11 +20750,16 @@ async function prepareHtmlPublishContent({
absoluteStoragePath: absoluteStoragePath2,
imgproxySigner
});
publishContent = rewriteWorkspacePublicAssetReferences(publishContent, htmlRelativePath);
const publishDir = h5Root ? resolvePublishDir(h5Root, { id: userId }) : null;
const resolvedHtmlRelativePath = htmlRelativePath ?? inferWorkspaceHtmlRelativePath({
pageTitle,
htmlContent: publishContent,
publishDir
});
publishContent = rewriteWorkspacePublicAssetReferences(publishContent, resolvedHtmlRelativePath);
const linkIndex = await loadWorkspaceDownloadLinkIndex(pool2, userId);
publishContent = rewriteRelativeDownloadLinks(publishContent, {
htmlRelativePath,
htmlRelativePath: resolvedHtmlRelativePath,
publishDir,
linkIndex,
publishKey: userId,
@@ -20629,7 +20837,8 @@ function createPublicationService(pool2, options = {}) {
const [rows] = await pool2.query(
`SELECT p.id AS page_id, p.title, p.summary, p.page_type, p.template_id, p.current_version_id,
p.user_id, p.space_id, pv.id AS page_version_id, pv.version_no,
pv.bundle_asset_id, av.storage_key
pv.bundle_asset_id, av.storage_key,
JSON_UNQUOTE(JSON_EXTRACT(pv.source_snapshot_json, '$.relative_path')) AS source_relative_path
FROM h5_page_records p
JOIN h5_page_versions pv ON pv.page_id = p.id
JOIN h5_asset_versions av ON av.asset_id = pv.content_asset_id AND av.version_no = 1
@@ -20660,12 +20869,20 @@ function createPublicationService(pool2, options = {}) {
const preparePublishContent = async (page, ownerSlug, urlSlug) => {
let publishContent = page.content;
if (page.page_type !== "html") return publishContent;
const publishDir = h5Root ? resolvePublishDir(h5Root, { id: page.user_id }) : null;
return prepareHtmlPublishContent({
pool: pool2,
userId: page.user_id,
html: publishContent,
ownerSlug,
urlSlug,
pageTitle: page.title,
htmlRelativePath: inferWorkspaceHtmlRelativePath({
snapshotRelativePath: page.source_relative_path,
pageTitle: page.title,
htmlContent: publishContent,
publishDir
}),
absoluteStoragePath: absoluteStoragePath2,
h5Root,
imgproxySigner: imgproxySigner ? { buildUrl: (path31, preset) => imgproxySigner.buildUrl(imgproxySigner.baseUrl, path31, preset) } : null
@@ -21117,16 +21334,37 @@ ${publishContent}`, {
now
]
);
const html = await fs18.readFile(await resolveReadableStoragePath(row.storage_key), "utf8");
return {
html: await fs18.readFile(await resolveReadableStoragePath(row.storage_key), "utf8"),
html: await refreshPublishedHtmlDownloadLinks(row, html),
publication: publicationResponse({ ...row, view_count: Number(row.view_count) + 1 })
};
};
const refreshPublishedHtmlDownloadLinks = async (row, html) => {
if (!h5Root || !row?.owner_id) return html;
const source = String(html ?? "");
if (!source.trim()) return html;
const publishDir = resolvePublishDir(h5Root, { id: row.owner_id });
const linkIndex = await loadWorkspaceDownloadLinkIndex(pool2, row.owner_id);
return rewriteRelativeDownloadLinks(html, {
htmlRelativePath: inferWorkspaceHtmlRelativePath({
pageTitle: row.title,
htmlContent: html,
publishDir
}),
publishDir,
linkIndex,
publishKey: row.owner_id,
publicBaseUrl: resolvePublicBaseUrl(),
preferAssetDownload: false
}).html;
};
const resolvePublic = async (ownerSlug, urlSlug, viewerId, password, requestMeta) => {
const [rows] = await pool2.query(
`SELECT pr.*, u.id AS owner_id, av.storage_key
`SELECT pr.*, u.id AS owner_id, p.title, av.storage_key
FROM h5_publish_records pr
JOIN h5_users u ON u.id = pr.user_id
JOIN h5_page_records p ON p.id = pr.page_id
JOIN h5_page_versions pv ON pv.id = pr.page_version_id AND pv.immutable = 1
JOIN h5_asset_versions av ON av.asset_id = pv.bundle_asset_id AND av.version_no = 1
WHERE COALESCE(u.slug, u.username) = ? AND pr.url_slug = ? AND pr.status = 'online'
@@ -21139,9 +21377,10 @@ ${publishContent}`, {
const resolvePrivateLink = async (token, viewerId, requestMeta) => {
const tokenHash = crypto14.createHash("sha256").update(String(token)).digest("hex");
const [rows] = await pool2.query(
`SELECT pr.*, u.id AS owner_id, av.storage_key
`SELECT pr.*, u.id AS owner_id, p.title, av.storage_key
FROM h5_publish_records pr
JOIN h5_users u ON u.id = pr.user_id
JOIN h5_page_records p ON p.id = pr.page_id
JOIN h5_page_versions pv ON pv.id = pr.page_version_id AND pv.immutable = 1
JOIN h5_asset_versions av ON av.asset_id = pv.bundle_asset_id AND av.version_no = 1
WHERE pr.token_hash = ? AND pr.status = 'online' AND pr.access_mode = 'private_link'
@@ -26514,11 +26753,7 @@ var PUBLIC_HTML_SKIP_DIRS = /* @__PURE__ */ new Set([
"node_modules"
]);
function normalizePublicHtmlPath(relativePath) {
const parts = String(relativePath ?? "").replace(/^\/+/, "").split("/").filter((part) => part && part !== "." && part !== "..");
if (parts.length === 0) return "";
if (parts[0].toLowerCase() === "public") return parts.join("/");
if (parts.length === 1 && parts[0].toLowerCase().endsWith(".html")) return `public/${parts[0]}`;
return parts.join("/");
return normalizeWorkspaceRelativePath2(relativePath);
}
function titleFromRelativePath(relativePath) {
const basename = path27.basename(String(relativePath ?? ""), ".html");
@@ -31442,7 +31677,7 @@ function createScheduleService(pool2, options = {}) {
FROM h5_schedule_reminders r
INNER JOIN h5_schedule_items i ON i.id = r.item_id
WHERE r.user_id = ?
AND r.status IN ('pending', 'locked')
AND r.status IN ('pending', 'locked', 'sent')
AND r.remind_at >= ?
AND r.remind_at < ?
AND i.deleted_at IS NULL
@@ -31469,6 +31704,48 @@ function createScheduleService(pool2, options = {}) {
);
return rowToItem(rows[0]);
};
const getReminder = async ({ userId, reminderId }) => {
if (!userId || !reminderId) throw new Error("\u7F3A\u5C11\u63D0\u9192\u53C2\u6570");
const [rows] = await pool2.query(
`SELECT r.*,
i.title AS item_title,
i.kind AS item_kind,
i.timezone AS item_timezone,
i.start_at AS item_start_at,
i.end_at AS item_end_at
FROM h5_schedule_reminders r
INNER JOIN h5_schedule_items i ON i.id = r.item_id
WHERE r.id = ? AND r.user_id = ? AND i.deleted_at IS NULL
LIMIT 1`,
[reminderId, userId]
);
return rowToReminderWithItem(rows[0]);
};
const cancelReminder = async ({ userId, reminderId, reason = "\u7528\u6237\u5FFD\u7565" } = {}) => {
const reminder = await getReminder({ userId, reminderId });
if (!reminder) throw new Error("\u63D0\u9192\u4E0D\u5B58\u5728\u6216\u65E0\u6743\u8BBF\u95EE");
if (reminder.status === "cancelled") return reminder;
if (reminder.status === "sent") throw new Error("\u5DF2\u901A\u77E5\u7684\u63D0\u9192\u4E0D\u80FD\u5FFD\u7565");
return markReminderCancelled(reminder, reason);
};
const deleteReminder = async ({ userId, reminderId }) => {
if (!userId || !reminderId) throw new Error("\u7F3A\u5C11\u63D0\u9192\u53C2\u6570");
const [result] = await pool2.query(
`DELETE FROM h5_schedule_reminders WHERE id = ? AND user_id = ?`,
[reminderId, userId]
);
return Number(result?.affectedRows ?? 0) > 0;
};
const deleteReminders = async ({ userId, reminderIds }) => {
if (!userId) throw new Error("\u7F3A\u5C11\u7528\u6237");
const ids = [...new Set((reminderIds ?? []).map((id) => String(id ?? "").trim()).filter(Boolean))];
if (ids.length === 0) return 0;
const [result] = await pool2.query(
`DELETE FROM h5_schedule_reminders WHERE user_id = ? AND id IN (${ids.map(() => "?").join(", ")})`,
[userId, ...ids]
);
return Number(result?.affectedRows ?? 0);
};
const createReminder = async ({
userId,
itemId,
@@ -32016,6 +32293,10 @@ function createScheduleService(pool2, options = {}) {
listTodayTodoItems,
listUpcomingItems,
listUpcomingReminders,
getReminder,
cancelReminder,
deleteReminder,
deleteReminders,
listDigestSubscriptions,
createDailyTodoDigest,
createBalanceLowAlert,
@@ -33475,6 +33756,7 @@ var mindSpaceAssets = null;
var mindSpaceAudit = null;
var mindSpacePages = null;
var mindSpacePageLiveEdit = null;
var mindSpaceAssetAgent = null;
var mindSpacePageEditSession = null;
var mindSpacePublications = null;
var plazaPosts = null;
@@ -33527,15 +33809,20 @@ async function bootstrapUserAuth() {
h5Root: __dirname5,
storageRoot: process.env.MINDSPACE_STORAGE_ROOT ?? path30.join(__dirname5, "data", "mindspace")
});
const resolveUserIdForAgentSession = async (sessionId) => {
const [rows] = await pool2.query(
`SELECT user_id FROM h5_user_sessions WHERE agent_session_id = ? LIMIT 1`,
[sessionId]
);
return rows[0]?.user_id ?? null;
};
mindSpacePageLiveEdit = createPageLiveEditService({
pageService: mindSpacePages,
resolveUserIdForAgentSession: async (sessionId) => {
const [rows] = await pool2.query(
`SELECT user_id FROM h5_user_sessions WHERE agent_session_id = ? LIMIT 1`,
[sessionId]
);
return rows[0]?.user_id ?? null;
}
resolveUserIdForAgentSession
});
mindSpaceAssetAgent = createAssetAgentService({
assetService: mindSpaceAssets,
resolveUserIdForAgentSession
});
mindSpacePublications = createPublicationService(pool2, {
h5Root: __dirname5,
@@ -34805,6 +35092,7 @@ api.use(async (req, res, next) => {
if (req.path === "/status") return next();
if (req.path.startsWith("/internal/agent/")) return next();
if (req.path === "/agent/mindspace_page_patch") return next();
if (req.path === "/agent/mindspace_asset_delete") return next();
if (req.path === "/config/blocked-words") return next();
if (req.method === "GET" && /^\/mindspace\/v1\/assets\/[^/]+\/download$/.test(req.path)) {
return next();
@@ -34953,6 +35241,37 @@ api.get("/mindspace/v1/space", async (req, res) => {
if (!space) return sendError(res, req, 404, "resource_not_found", "\u7528\u6237\u7A7A\u95F4\u4E0D\u5B58\u5728");
return sendData(res, req, space);
});
api.post("/mindspace/v1/schedule/reminders/:reminderId/ignore", async (req, res) => {
if (!ensureMindSpaceEnabled(res, req) || !scheduleService) {
return sendError(res, req, 503, "feature_disabled", "\u65E5\u7A0B\u670D\u52A1\u672A\u542F\u7528");
}
try {
const reminder = await scheduleService.cancelReminder({
userId: req.currentUser.id,
reminderId: req.params.reminderId
});
return sendData(res, req, reminder);
} catch (err) {
const message = err instanceof Error ? err.message : "\u5FFD\u7565\u63D0\u9192\u5931\u8D25";
return sendError(res, req, 400, "invalid_schedule_input", message);
}
});
api.post("/mindspace/v1/schedule/reminders/bulk-delete", async (req, res) => {
if (!ensureMindSpaceEnabled(res, req) || !scheduleService) {
return sendError(res, req, 503, "feature_disabled", "\u65E5\u7A0B\u670D\u52A1\u672A\u542F\u7528");
}
try {
const ids = Array.isArray(req.body?.ids) ? req.body.ids.map(String) : [];
const deleted = await scheduleService.deleteReminders({
userId: req.currentUser.id,
reminderIds: ids
});
return sendData(res, req, { deleted });
} catch (err) {
const message = err instanceof Error ? err.message : "\u5220\u9664\u63D0\u9192\u5931\u8D25";
return sendError(res, req, 400, "invalid_schedule_input", message);
}
});
api.get("/mindspace/v1/space/quota", async (req, res) => {
if (!mindSpace) return res.status(503).json({ message: "MindSpace \u672A\u542F\u7528" });
const quota = await mindSpace.getQuota(req.currentUser.id);
@@ -35586,8 +35905,24 @@ api.post("/mindspace/v1/pages/from-asset", async (req, res) => {
req.currentUser.id,
assetId
);
if (asset.mimeType === "text/html") {
const relativePath = normalizeWorkspaceRelativePath2(
String(asset.originalFilename ?? asset.original_filename ?? "").includes("/") ? asset.originalFilename ?? asset.original_filename : `public/${asset.originalFilename ?? asset.original_filename ?? ""}`
);
const existingByPath = relativePath ? await mindSpacePages.findPageByRelativePath(req.currentUser.id, relativePath).catch(() => null) : null;
if (existingByPath) {
return sendData(res, req, {
kind: "page",
categoryCode: existingByPath.categoryCode ?? "draft",
page: existingByPath
});
}
}
const content = await fs28.promises.readFile(assetPath, "utf8");
const contentFormat = asset.mimeType === "text/html" ? "html" : "markdown";
const htmlRelativePath = contentFormat === "html" ? normalizeWorkspaceRelativePath2(
String(asset.originalFilename ?? asset.original_filename ?? "").includes("/") ? asset.originalFilename ?? asset.original_filename : `public/${asset.originalFilename ?? asset.original_filename ?? ""}`
) : null;
const page = await mindSpacePages.createFromChat(
req.currentUser.id,
{
@@ -35603,7 +35938,8 @@ api.post("/mindspace/v1/pages/from-asset", async (req, res) => {
snapshot: {
source_asset_id: asset.id,
source_category: asset.categoryCode,
content_mode: contentFormat
content_mode: contentFormat,
...htmlRelativePath ? { relative_path: htmlRelativePath } : {}
}
}
);
@@ -35659,6 +35995,14 @@ async function resolveOwnedAssistantMessage(userId, sessionId, messageId) {
}
return { session, message, content };
}
async function resolveExistingSavedPage(userId, { sessionId, messageId, relativePath } = {}) {
if (!mindSpacePages || !userId) return null;
const byMessage = await mindSpacePages.findPageBySourceMessage(userId, sessionId, messageId).catch(() => null);
if (byMessage) return byMessage;
const normalizedPath = normalizeWorkspaceRelativePath2(relativePath);
if (!normalizedPath) return null;
return mindSpacePages.findPageByRelativePath(userId, normalizedPath).catch(() => null);
}
var SAVE_TARGET_CATEGORIES = /* @__PURE__ */ new Set(["draft", "oa", "public"]);
var pageSyncInFlight = /* @__PURE__ */ new Map();
async function syncUserGeneratedPages(userId) {
@@ -35837,11 +36181,11 @@ api.post("/mindspace/v1/pages/analyze-chat-save", async (req, res) => {
thumbnailReady = false;
}
}
const existingPage = await mindSpacePages.findPageBySourceMessage(
req.currentUser.id,
const existingPage = await resolveExistingSavedPage(req.currentUser.id, {
sessionId,
messageId
).catch(() => null);
messageId,
relativePath: resolvedHtml?.relativePath ?? analysis.relativePath
});
return sendData(res, req, {
contentMode: analysis.contentMode,
links: analysis.links,
@@ -35923,7 +36267,7 @@ api.post("/mindspace/v1/pages/save-from-chat", async (req, res) => {
role: source.message.role,
content_mode: analysis.contentMode,
public_url: analysis.previewUrl,
relative_path: analysis.relativePath
relative_path: analysis.relativePath ? normalizeWorkspaceRelativePath2(analysis.relativePath) : analysis.relativePath
};
let resolvedHtml = null;
if (analysis.contentMode === "static_html") {
@@ -35994,7 +36338,12 @@ api.post("/mindspace/v1/pages/save-from-chat", async (req, res) => {
}).catch(() => {
});
}
const replacePageId = req.body?.replace_page_id ? String(req.body.replace_page_id).trim() : null;
const saveAsNew = Boolean(req.body?.save_as_new);
let replacePageId = req.body?.replace_page_id ? String(req.body.replace_page_id).trim() : null;
if (!replacePageId && !saveAsNew && analysis.contentMode === "static_html" && analysis.relativePath) {
const existingByPath = await mindSpacePages.findPageByRelativePath(req.currentUser.id, analysis.relativePath).catch(() => null);
if (existingByPath) replacePageId = existingByPath.id;
}
let page;
if (replacePageId) {
const existingPage = await mindSpacePages.getPage(req.currentUser.id, replacePageId);
@@ -36253,6 +36602,31 @@ api.post("/agent/mindspace_page_patch", async (req, res) => {
return mindSpaceError(res, req, error);
}
});
api.post("/agent/mindspace_asset_delete", async (req, res) => {
if (!mindSpaceAssetAgent) {
return res.status(503).json({ message: "MindSpace \u672A\u542F\u7528" });
}
try {
const result = await mindSpaceAssetAgent.applyAgentDelete(req.body ?? {});
await mindSpaceAudit?.write({
userId: result.userId ?? null,
action: "asset.delete",
objectType: "asset",
objectId: result.assetId,
ip: req.ip,
metadata: { via: "agent" }
});
return sendData(res, req, result);
} catch (error) {
if (error?.code === "forbidden") {
return sendError(res, req, 403, error.code, error.message);
}
if (error?.code === "invalid_request" || error?.code === "confirmation_required") {
return sendError(res, req, 400, error.code, error.message);
}
return mindSpaceError(res, req, error);
}
});
api.get("/mindspace/v1/pages/:pageId/thumbnail", async (req, res) => {
if (!mindSpacePages) return res.status(503).json({ message: "MindSpace \u672A\u542F\u7528" });
try {
@@ -36310,6 +36684,23 @@ api.post("/mindspace/v1/pages/:pageId/thumbnail/regenerate", async (req, res) =>
return mindSpaceError(res, req, error);
}
});
api.post("/mindspace/v1/pages/:pageId/rewrite-download-links", async (req, res) => {
if (!mindSpacePages) return res.status(503).json({ message: "MindSpace \u672A\u542F\u7528" });
try {
const content = String(req.body?.content ?? "").trim();
if (!content) {
throw Object.assign(new Error("\u7F3A\u5C11\u9875\u9762\u5185\u5BB9"), { code: "invalid_page_input" });
}
const html = await mindSpacePages.rewriteHtmlDownloadLinksForPage(
req.currentUser.id,
req.params.pageId,
content
);
return sendData(res, req, { html });
} catch (error) {
return mindSpaceError(res, req, error);
}
});
api.get("/mindspace/v1/pages/:pageId/preview", async (req, res) => {
if (!mindSpacePages) return res.status(503).json({ message: "MindSpace \u672A\u542F\u7528" });
try {
@@ -78,6 +78,16 @@ description: 在专属 MindSpace 目录生成可公开访问的静态 HTML 报
本地对比示例:`node scripts/thumbnail-preview-demo.mjs``/thumbnail-demo/`
## 平台页脚标记(必须)
页脚平台联系行**必须**使用 `data-mindspace-page-tag="platform-brand"`,且邮箱/域名只用 **tkmind.cn**(如 `contact@tkmind.cn`),**禁止** `tkmind.ai`
```html
<p data-mindspace-page-tag="platform-brand">📧 contact@tkmind.cn</p>
```
`data-mindspace-page-tag` 的区域为平台固定信息:用户在编辑模式中不可见、不可改;预览与发布后正常显示。
## 附带文件下载(Word / PDF
- 二进制文件用 `docx-generate` 脚本或平台允许的方式**单独生成**,保存到 `public/`(或 `oa/` 再复制到 `public/`