Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
5928a3f
feat(ai): support llms.txt, per-page Markdown and ChatGPT/Claude open
Alice39s Aug 9, 2026
91e97ca
fix(ai): align AI tool buttons with Material conventions, graceful de…
Alice39s Aug 9, 2026
e44e21c
fix(ui): restore homepage rendering and add Open menu
Alice39s Aug 9, 2026
e99ae4e
feat(ai): expand Open menu providers
Alice39s Aug 9, 2026
33be6ad
feat(ui): add provider icons to Open button
Alice39s Aug 9, 2026
352b92f
feat(ai): add copy Markdown action
Alice39s Aug 9, 2026
408ddcc
feat(ai): localize article prompt
Alice39s Aug 10, 2026
bfc0e79
feat(ui): use Grok brand icon and stack action buttons below 890px
Alice39s Aug 10, 2026
4eda588
fix(ui): reword open label and place actions under title below 768px
Alice39s Aug 10, 2026
aacc20a
fix(ui): unify action layout breakpoint at 768px, render home images,…
Alice39s Aug 10, 2026
3c0b9ac
feat(ui): hide actions on home, reword buttons, smooth scroll
Alice39s Aug 10, 2026
d0b100b
feat(ui): side-by-side outlined actions on mobile
Alice39s Aug 10, 2026
7a0bfe0
fix(ui): lower actions z-index, left-align on small screens
Alice39s Aug 10, 2026
8aa41b0
feat(serve): serve markdown as text/plain utf-8
Alice39s Aug 10, 2026
f06ca45
fix(test): write fixture with LF line endings on all platforms
Alice39s Aug 10, 2026
b77dc7d
fix(serve): propagate shutdown interrupts
Alice39s Aug 10, 2026
4a7148d
Merge pull request #37 from nm-Team/stack-v2/serve-markdown-plain
Alice39s Aug 10, 2026
187fefc
Merge pull request #36 from nm-Team/stack-v2/ui-article-actions
Alice39s Aug 10, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
91 changes: 52 additions & 39 deletions AGENTS.md

Large diffs are not rendered by default.

24 changes: 24 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,14 +61,38 @@ uv run nmteam build
Markdown 中仍使用普通图片语法,构建工具会自动输出 WebP
优先的 `<picture>` 元素。外部 URL 不会被下载或镜像。

## AI 支持

站点面向语言模型提供以下能力:

- `/llms.txt`:按 [llmstxt.org](https://llmstxt.org/) 规范生成的站点索引,
每个链接指向页面的 Markdown 版本。
- 每页 Markdown 版本:`nmteam build` 时在每个页面旁生成同路径的 `.md` 文件
(如 `/nmbot-telegram/mcp.md`)。
- 页面顶部的文章操作区(首页不显示):**复制 Markdown** 直接复制当前页面
原文;**使用[品牌图标]打开** 菜单提供 GitHub 源文件、Markdown 版本,
以及 Perplexity、Grok、ChatGPT、Claude Web、Claude Desktop、
Claude Code、OpenAI Codex、Cursor 八种 AI 打开方式。

注意:`llms.txt` 与 `.md` 版本由 `nmteam build` 输出到 `site/`;开发模式
(`nmteam dev`)下复制和 View as Markdown 会提示先构建,AI 操作仍可通过
当前页面 URL 打开。

### 其他命令

```bash
uv run nmteam generate # 仅重新生成文档结构
uv run nmteam clean # 清理 cache/、generated/ 和 site/
uv run nmteam serve # 预览 site/(.md 以 text/plain; charset=utf-8 提供)
uv run nmteam --help # 显示完整命令帮助
```

`nmteam serve` 默认监听 `127.0.0.1:8124`,可用 `--port` / `--bind`
调整。与裸 `python -m http.server` 不同,它把 `.md` 文件的
`Content-Type` 显式设为 `text/plain; charset=utf-8`(Python 3.13+ 的
`mimetypes` 会把 `.md` 判为 `text/markdown`,部分客户端会下载而非内联
显示),保证每页 Markdown 版本在任何浏览器中都能直接阅读。

## 平台启动器

直接运行 `uv run nmteam` 是推荐方式。`scripts/` 也提供不包含业务逻辑的薄启动器;它们会自动定位仓库根目录并原样传递参数。
Expand Down
250 changes: 250 additions & 0 deletions assets/js/ai-tools.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,250 @@
// Fumadocs-style page actions menu.
(function () {
"use strict";

var ENDPOINTS = {
perplexity: "https://www.perplexity.ai/search?q=",
grok: "https://grok.com/?q=",
chatgpt: "https://chatgpt.com/?prompt=",
claude: "https://claude.ai/new?q=",
"claude-desktop": "claude://claude.ai/new?q=",
"claude-code": "https://claude.ai/code/new?q=",
codex: "codex://new?prompt=",
cursor: "https://cursor.com/link/prompt?text=",
};

var CLAUDE_CODE_REPOSITORY = "nm-Team/Support";
var CLAUDE_CODE_BRANCH = "main";

function mdUrl(raw) {
if (!raw) {
return "/index.md";
}
return "/" + raw.replace(/\/+$/, "") + ".md";
}

function pageUrl(raw) {
return location.origin + "/" + (raw || "");
}

function providerPrompt(url) {
return (
"请阅读以下 nmTeam 帮助文档,并依据原文回答我接下来的问题;" +
"若文档未提及,请如实说明:\n\n" +
url
);
}

function providerUrl(provider, prompt) {
var url = ENDPOINTS[provider] + encodeURIComponent(prompt);
if (provider === "chatgpt") {
return url + "&hints=search";
}
if (provider === "claude-code") {
return (
url +
"&repo=" +
encodeURIComponent(CLAUDE_CODE_REPOSITORY) +
"&branch=" +
encodeURIComponent(CLAUDE_CODE_BRANCH)
);
}
return url;
}

function setOpen(box, open, restoreFocus) {
var trigger = box.querySelector(".ai-tools__trigger");
var menu = box.querySelector(".ai-tools__menu");
box.classList.toggle("is-open", open);
trigger.setAttribute("aria-expanded", String(open));
menu.hidden = !open;
if (!open && restoreFocus) {
trigger.focus();
}
}

function showMarkdownUnavailable() {
window.alert(
"Markdown 版本在构建产物中提供。请先运行 `uv run nmteam build`," +
"或使用其他打开方式。"
);
}

function showCopyFailed() {
window.alert("无法复制 Markdown,请稍后重试。");
}

function copyWithSelection(text) {
var textarea = document.createElement("textarea");
textarea.value = text;
textarea.setAttribute("readonly", "");
textarea.style.position = "fixed";
textarea.style.opacity = "0";
document.body.appendChild(textarea);
textarea.select();

var copied = document.execCommand("copy");
textarea.remove();
if (!copied) {
return Promise.reject(new Error("Copy command failed"));
}
return Promise.resolve();
}

function copyText(text) {
if (navigator.clipboard && navigator.clipboard.writeText) {
return navigator.clipboard.writeText(text).catch(function () {
return copyWithSelection(text);
});
}
return copyWithSelection(text);
}

function setCopyState(button, copied) {
button.querySelector(".ai-tools__copy-icon--idle").hidden = copied;
button.querySelector(".ai-tools__copy-icon--success").hidden = !copied;
button.querySelector(".ai-tools__copy-label").textContent = copied
? "已复制"
: "复制 Markdown";
}

function wireCopy(button, md, availability) {
var resetTimer;

button.addEventListener("click", function () {
button.disabled = true;
availability
.then(function (available) {
if (!available) {
showMarkdownUnavailable();
return null;
}
return fetch(md);
})
.then(function (response) {
if (!response) {
return null;
}
var contentType = response.headers.get("Content-Type") || "";
if (!response.ok || contentType.indexOf("html") !== -1) {
throw new Error("Markdown is unavailable");
}
return response.text();
})
.then(function (markdown) {
if (markdown === null) {
return;
}
return copyText(markdown).then(function () {
window.clearTimeout(resetTimer);
setCopyState(button, true);
resetTimer = window.setTimeout(function () {
setCopyState(button, false);
}, 1800);
});
})
.catch(showCopyFailed)
.then(function () {
button.disabled = false;
});
});
}

function wireMarkdown(link, md) {
var availability = fetch(md, { method: "HEAD" })
.then(function (response) {
var contentType = response.headers.get("Content-Type") || "";
return response.ok && contentType.indexOf("html") === -1;
})
.catch(function () {
return false;
})
.then(function (available) {
link.dataset.rawAvailable = String(available);
return available;
});

link.href = md;
link.addEventListener("click", function (event) {
if (link.dataset.rawAvailable === "true") {
return;
}

event.preventDefault();
if (link.dataset.rawAvailable === "false") {
showMarkdownUnavailable();
return;
}

var target = window.open("about:blank", "_blank");
if (target) {
target.opener = null;
}
availability.then(function (available) {
if (available && target) {
target.location.replace(md);
return;
}
if (target) {
target.close();
}
showMarkdownUnavailable();
});
});

return availability;
}

function wireMenu(box) {
var trigger = box.querySelector(".ai-tools__trigger");
var copy = box.querySelector(".ai-tools__copy");
var menu = box.querySelector(".ai-tools__menu");
var raw = box.getAttribute("data-md-url") || "";
var markdown = menu.querySelector('[data-ai="markdown"]');
var md = mdUrl(raw);
var prompt = providerPrompt(pageUrl(raw));
var availability = wireMarkdown(markdown, md);

wireCopy(copy, md, availability);
[
"perplexity",
"grok",
"chatgpt",
"claude",
"claude-desktop",
"claude-code",
"codex",
"cursor",
].forEach(function (provider) {
menu.querySelector('[data-ai="' + provider + '"]').href =
providerUrl(provider, prompt);
});

trigger.addEventListener("click", function () {
setOpen(box, menu.hidden, false);
});

menu.addEventListener("click", function (event) {
if (event.target.closest(".ai-tools__item")) {
setOpen(box, false, false);
}
});

document.addEventListener("click", function (event) {
if (!menu.hidden && !box.contains(event.target)) {
setOpen(box, false, false);
}
});

document.addEventListener("keydown", function (event) {
if (event.key === "Escape" && !menu.hidden) {
event.preventDefault();
setOpen(box, false, true);
}
});
}

document.addEventListener("DOMContentLoaded", function () {
document.querySelectorAll(".ai-tools").forEach(wireMenu);
});
})();
Loading