From 51a1e22edff5b8445ebbddfcab396d262e245bf3 Mon Sep 17 00:00:00 2001 From: Alice39s Date: Sun, 9 Aug 2026 15:36:26 +0900 Subject: [PATCH 01/57] chore(tooling): initialize uv project with Python 3.14 --- .python-version | 1 + pyproject.toml | 26 ++ src/nmteam_support/__init__.py | 3 + uv.lock | 619 +++++++++++++++++++++++++++++++++ 4 files changed, 649 insertions(+) create mode 100644 .python-version create mode 100644 pyproject.toml create mode 100644 src/nmteam_support/__init__.py create mode 100644 uv.lock diff --git a/.python-version b/.python-version new file mode 100644 index 0000000..6324d40 --- /dev/null +++ b/.python-version @@ -0,0 +1 @@ +3.14 diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..5c6dc6f --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,26 @@ +[project] +name = "nmteam-support" +version = "0.1.0" +description = "nmTeam Support 文档站构建工具链" +requires-python = ">=3.14" +dependencies = [ + "mkdocs==1.6.1", + "mkdocs-material==9.7.7", + "pyyaml>=6.0.2", +] + +[dependency-groups] +dev = [ + "mdformat>=1.0", + "mdformat-front-matters>=2.0", + "mdformat-mkdocs>=5.2.2", + "pytest>=9.0", + "ruff>=0.16", +] + +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[tool.hatch.build.targets.wheel] +packages = ["src/nmteam_support"] diff --git a/src/nmteam_support/__init__.py b/src/nmteam_support/__init__.py new file mode 100644 index 0000000..d64644a --- /dev/null +++ b/src/nmteam_support/__init__.py @@ -0,0 +1,3 @@ +"""nmTeam Support 文档站工具包。""" + +__version__ = "0.1.0" diff --git a/uv.lock b/uv.lock new file mode 100644 index 0000000..464a4ef --- /dev/null +++ b/uv.lock @@ -0,0 +1,619 @@ +version = 1 +revision = 3 +requires-python = ">=3.14" + +[[package]] +name = "babel" +version = "2.18.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7d/b2/51899539b6ceeeb420d40ed3cd4b7a40519404f9baf3d4ac99dc413a834b/babel-2.18.0.tar.gz", hash = "sha256:b80b99a14bd085fcacfa15c9165f651fbb3406e66cc603abf11c5750937c992d", size = 9959554, upload-time = "2026-02-01T12:30:56.078Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/77/f5/21d2de20e8b8b0408f0681956ca2c69f1320a3848ac50e6e7f39c6159675/babel-2.18.0-py3-none-any.whl", hash = "sha256:e2b422b277c2b9a9630c1d7903c2a00d0830c409c59ac8cae9081c92f1aeba35", size = 10196845, upload-time = "2026-02-01T12:30:53.445Z" }, +] + +[[package]] +name = "backrefs" +version = "8.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ec/56/4744bcd0c82184e80c52b0ac4076c261a8ffa1f1b343ff2f6e89ce0e1cef/backrefs-8.0.tar.gz", hash = "sha256:b556cd7d36c3a3a2f256b89590b176b8eddfb73bcfaee3a3ddd84ea66d21ce50", size = 7013081, upload-time = "2026-07-26T19:54:24.638Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e3/fd/9bf53b6a6f6f519ffaac765df2f2a25e5c2fc6d32cfd2b2747099e72c911/backrefs-8.0-py310-none-any.whl", hash = "sha256:4a627b817fd2dce43b79ab48da63613340509381cd8ce0897078a0bce79a2ab8", size = 380377, upload-time = "2026-07-26T19:54:17.457Z" }, + { url = "https://files.pythonhosted.org/packages/e1/29/4bd7ae72a2634da00379c2b3bcc5439e7c94620235c6afea8af15229a973/backrefs-8.0-py311-none-any.whl", hash = "sha256:f0c35cf0102ba6b6070c12a492be3c1c1d3f5839529784b9a9565d6d04569a01", size = 392169, upload-time = "2026-07-26T19:54:18.782Z" }, + { url = "https://files.pythonhosted.org/packages/29/13/232505664e8e2a0c7a2eb0c505cfade9d715538f89a5d62bc4c272968f62/backrefs-8.0-py312-none-any.whl", hash = "sha256:87f0fae8c5f207fe9f4b2887efc71d42f4900ac78faa1af08d675ef303692dc5", size = 398084, upload-time = "2026-07-26T19:54:19.954Z" }, + { url = "https://files.pythonhosted.org/packages/8a/69/47a3dc20abc4fa5486655fde681bd55e63211b46c886d8c02223d6468431/backrefs-8.0-py313-none-any.whl", hash = "sha256:601ce68ca12385dbda06ce264406b4c4210cf5b79fd0fd627592365c92f29a88", size = 400040, upload-time = "2026-07-26T19:54:21.194Z" }, + { url = "https://files.pythonhosted.org/packages/1c/cf/e5f9b68a5b0e939a2fb933a66c20180d0c9241bf8927f7a47fa48c1675e9/backrefs-8.0-py314-none-any.whl", hash = "sha256:9ec96efa080938be92323e8e730e57718c9c88eb15ad70bbef4e1766df591408", size = 411903, upload-time = "2026-07-26T19:54:23.221Z" }, +] + +[[package]] +name = "certifi" +version = "2026.7.22" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a3/c2/24167ea9858356b47a87a50d39908bfdb72ceeefe0041586e704e5376b3a/certifi-2026.7.22.tar.gz", hash = "sha256:741e2c3b351ddf169a738da9f2c048608ff7f2c5cc02f1ebc6b118bb090d5d55", size = 138112, upload-time = "2026-07-22T03:35:12.644Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0b/a7/71ac2cff56fec219ed242bb11b8efb69fcc4bec75db06fb7bfe35de520e6/certifi-2026.7.22-py3-none-any.whl", hash = "sha256:62f22742b58a1a33014a2b6b706588a8d7e2a88ae7bd1a6ebe8c992928483775", size = 136983, upload-time = "2026-07-22T03:35:11.276Z" }, +] + +[[package]] +name = "charset-normalizer" +version = "3.4.9" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/bd/2a/23f34ec9d04624958e137efdc394888716353190e75f25dd22c7a2c7a8aa/charset_normalizer-3.4.9.tar.gz", hash = "sha256:673611bbd43f0810bec0b0f028ddeaaa501190339cac411f347ac76917c3ae7b", size = 152439, upload-time = "2026-07-07T14:34:58.454Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/8d/496817fa0944239ecae662dd57ea765cfeaec6a735f9f025d4b7b72e7143/charset_normalizer-3.4.9-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:0327fcd59a935777d83410750c50600ee9571af2846f71ce40f25b13da1ef380", size = 317253, upload-time = "2026-07-07T14:33:54.994Z" }, + { url = "https://files.pythonhosted.org/packages/2b/f9/ef4a69ea338ad3c0deceea0f5f7d2380ae8b52132b06d652cb0d2cd86706/charset_normalizer-3.4.9-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8a79d9f4d8001473a30c163556b3c3bfebec837495a412dde78b51672f6134f9", size = 215898, upload-time = "2026-07-07T14:33:56.334Z" }, + { url = "https://files.pythonhosted.org/packages/8c/e7/5ddfd76fc061eb52de219658a4aa431cbacadf0a0219c8854f00da50d289/charset_normalizer-3.4.9-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:33bdcc2a32c0a0e861f60841a512c8acc658c87c2ac59d89e3a46dacf7d866e4", size = 236718, upload-time = "2026-07-07T14:33:57.9Z" }, + { url = "https://files.pythonhosted.org/packages/49/ba/768fa3f36048d81c477a0ce61f813bc1454d80917ccfe550abd9f44f5e24/charset_normalizer-3.4.9-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f840ed6d8ecba8255df8c42b87fadeda98ddfc6eeec05e2dc66e26d46dd6f58a", size = 232519, upload-time = "2026-07-07T14:33:59.811Z" }, + { url = "https://files.pythonhosted.org/packages/f4/c4/b3e049d2aa3766180c78507110543d9d50894cc97f57de543f1be521dcdc/charset_normalizer-3.4.9-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c25fe15c70c59eb7c5ce8c06a1f3fa1da0ecc5ea1e7a5922c40fd2fa9b0d5046", size = 223143, upload-time = "2026-07-07T14:34:01.517Z" }, + { url = "https://files.pythonhosted.org/packages/19/79/55c32d06d76ae4feafe053f061f3e3ab70bcf19f4007797ce8c3efda7830/charset_normalizer-3.4.9-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:f7fb7d750cfa0a070d2c24e831fd3481019a60dd317ea2b39acbcebc08b6ed81", size = 206742, upload-time = "2026-07-07T14:34:03.04Z" }, + { url = "https://files.pythonhosted.org/packages/10/e0/47c079dd82d217c807479cd59ffd30af56307ea31c108b75758970459ad3/charset_normalizer-3.4.9-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4d1c96a7a18b9690a4d46df09e3e3382406ae3213727cd1019ebade1c4a81917", size = 219191, upload-time = "2026-07-07T14:34:04.657Z" }, + { url = "https://files.pythonhosted.org/packages/42/ab/b9bc2e77d6b44a7e46ef62ec5cac1c9a6ba7b9135a5d560f002696ec9995/charset_normalizer-3.4.9-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a4cfde78a9f2880208d16a93b795726a3017d5977e08d1e162a7a31322479c41", size = 218328, upload-time = "2026-07-07T14:34:06.115Z" }, + { url = "https://files.pythonhosted.org/packages/f1/78/c9c71d599f5aa2d42bcdd35cbbd46d7f535351a57e40ff7d8e5a7e219401/charset_normalizer-3.4.9-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:d4d6fcde76f94f5cb9e43e9e9a61f16dacefd228cbbf6f1a09bd9b219a92f1a1", size = 207406, upload-time = "2026-07-07T14:34:07.554Z" }, + { url = "https://files.pythonhosted.org/packages/f6/39/c914445c321a845097ce4f6ac7de9a18228a77b766272125a1ce00d851eb/charset_normalizer-3.4.9-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:898f0e9068ca27d37f8e83a5b962821df851532e6c4a7d615c1c033f9da6eedf", size = 225157, upload-time = "2026-07-07T14:34:09.061Z" }, + { url = "https://files.pythonhosted.org/packages/9b/f2/c0d4b8508565a36bc5c624e88ed297f5b0b1095011034d7f5b83a69908b5/charset_normalizer-3.4.9-cp314-cp314-win32.whl", hash = "sha256:c1c948747b03be832dceed96ca815cef7360de9aa19d37c730f8e3f6101aca48", size = 151095, upload-time = "2026-07-07T14:34:10.901Z" }, + { url = "https://files.pythonhosted.org/packages/49/fd/a1d26144398c67486422a72bf5812cda22cb4ccfcd95a290fb41ceb4b8e2/charset_normalizer-3.4.9-cp314-cp314-win_amd64.whl", hash = "sha256:16b65ea0f2465b6fb52aa22de5eca612aa964ddfec00a912e26f4656cbef890b", size = 162796, upload-time = "2026-07-07T14:34:12.47Z" }, + { url = "https://files.pythonhosted.org/packages/20/95/d75e82f8ce9fd323ebf059c16c9aadefb22a1ecde13b7840b35835e4886c/charset_normalizer-3.4.9-cp314-cp314-win_arm64.whl", hash = "sha256:40a126142a56b2dfc0aacbad1de8310cbf60da7656db0e6b16eebd48e3e93519", size = 153334, upload-time = "2026-07-07T14:34:14.044Z" }, + { url = "https://files.pythonhosted.org/packages/00/5e/17398df3a139985ba9d11ed072531986f408c8fca952835ef1ab1820c02b/charset_normalizer-3.4.9-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:609b3ba8fcc0fb5ab7af00719d0fb6ad0cb518e48e7712d12fd68f1327951198", size = 338848, upload-time = "2026-07-07T14:34:15.688Z" }, + { url = "https://files.pythonhosted.org/packages/cd/91/7253a32e86b7e1d1239b1b36ba6dd0f021a21107ab33054b53119cc083b9/charset_normalizer-3.4.9-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:51447e9aa2684679af07ca5021c3db526e0284347ebf4ffcec1154c3350cfe32", size = 223022, upload-time = "2026-07-07T14:34:17.248Z" }, + { url = "https://files.pythonhosted.org/packages/cb/32/2e64bd2be10e89c61e57ebe6a93fd98ae88eb7ebe414b5121f22c96c69eb/charset_normalizer-3.4.9-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:cc1b0fff8ead343dae06305f954eb8468ba0ec1a97881f42489d198e4ce3c632", size = 241590, upload-time = "2026-07-07T14:34:18.813Z" }, + { url = "https://files.pythonhosted.org/packages/3d/ef/d96ec496cfea0c21db43b0ad03891308b02388d054cc902cf0e5a1ad6a88/charset_normalizer-3.4.9-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:fa36ec09ef71d158186bc79e359ff5fdd6e7996fe8ab638f00d6b93139ba4fcf", size = 239584, upload-time = "2026-07-07T14:34:20.52Z" }, + { url = "https://files.pythonhosted.org/packages/d4/ce/9af95f7876194bd7a14e3dfe4a4de2e0bff02666a3910d72beafd06cc297/charset_normalizer-3.4.9-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:df115d4d83168fdf2cae48ef1ff6d1cb4c466364e30861b37121de0f3bf1b990", size = 230224, upload-time = "2026-07-07T14:34:22.189Z" }, + { url = "https://files.pythonhosted.org/packages/52/94/af74dde74a3996bd959c350709bfe50e297823d70a8c1cbd54b838880863/charset_normalizer-3.4.9-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:f86c6358749bd4fda175388691e3ba8c46e24c5347d0afd20f9b7edfc9faf07d", size = 212667, upload-time = "2026-07-07T14:34:23.857Z" }, + { url = "https://files.pythonhosted.org/packages/ee/f0/f1c4fe746c395922961b5916ed1d7d6e7d4c84851d19ed43cc89980ec953/charset_normalizer-3.4.9-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:32286a2c8d167e897177b673176c1e3e00d4057caf5d2b64eef9a3666b03018e", size = 227179, upload-time = "2026-07-07T14:34:25.586Z" }, + { url = "https://files.pythonhosted.org/packages/e4/56/6c745619ac397e8871e2bcd3cea1eec86b877488f33888b3aef5c3ed506e/charset_normalizer-3.4.9-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:83aed2c10721ddd90f68140685391b50811a880af20654c59af6b6c66c40513c", size = 225372, upload-time = "2026-07-07T14:34:27.212Z" }, + { url = "https://files.pythonhosted.org/packages/78/ad/98aae8630ac71f16711968e38a5acfecce41b778bf2f0312851020f565a8/charset_normalizer-3.4.9-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:cd6c3d4b783c556fa00bf540854e42f135e2f256abd29669fcd0da0f2dec79c2", size = 215222, upload-time = "2026-07-07T14:34:28.774Z" }, + { url = "https://files.pythonhosted.org/packages/f7/40/9593d54209765207a7f11073c06494c1721e4ca4a0a426c597679bf7f91e/charset_normalizer-3.4.9-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ee2f2a527e3c1a6e6411eb4209642e138b544a2d72fe5d0d76daf77b24063534", size = 231958, upload-time = "2026-07-07T14:34:30.345Z" }, + { url = "https://files.pythonhosted.org/packages/b1/27/693ee5e8a18191eb38647360c51cd505013e2bd3b366aa43fd5344c21e3c/charset_normalizer-3.4.9-cp314-cp314t-win32.whl", hash = "sha256:0d861473f743244d349b50f850d10eb87aeb22bbdcc8e64f79273c94af5a8226", size = 155580, upload-time = "2026-07-07T14:34:31.884Z" }, + { url = "https://files.pythonhosted.org/packages/80/3f/bd97d3d9c613013d07cb7733d299385b41df37f0471310f5a73dc359f0b8/charset_normalizer-3.4.9-cp314-cp314t-win_amd64.whl", hash = "sha256:9b8e0f3107e2200b76f6054de99016eac3ee6762713587b36baaa7e4bd2ae177", size = 167620, upload-time = "2026-07-07T14:34:33.438Z" }, + { url = "https://files.pythonhosted.org/packages/3d/c6/eee9dca4439b1061f76373f06ea855678cc4a64c1c3c90b50e479edbb8eb/charset_normalizer-3.4.9-cp314-cp314t-win_arm64.whl", hash = "sha256:19ac87f93086ce37b86e098888555c4b4bc48102279bae3350098c0ed664b501", size = 158037, upload-time = "2026-07-07T14:34:35.018Z" }, + { url = "https://files.pythonhosted.org/packages/98/2b/f97f1c193fb855c345d678f5077d6926034db0722df74c8f057020e05a25/charset_normalizer-3.4.9-py3-none-any.whl", hash = "sha256:68e5f26a1ad57ded6d1cfb85331d1c1a195314756471d97758c48498bb4dcdf5", size = 64538, upload-time = "2026-07-07T14:34:56.993Z" }, +] + +[[package]] +name = "click" +version = "8.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/76/d4/81420972a676e8ffea40450d8c8c92943e7218a78fe9b64359836cc9876b/click-8.4.2.tar.gz", hash = "sha256:9a6cea6e60b17ebe0a44c5cc636d94f09bd66142c1cd7d8b4cd731c4917a15f6", size = 338000, upload-time = "2026-06-24T17:45:15.148Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fb/e2/79c688af8b210d232694e31e59da9f6ec747bae31c3f5946e4e9b98860d5/click-8.4.2-py3-none-any.whl", hash = "sha256:e6f9f66136c816745b9d65817da91d61d957fb16e02e4dcd0552553c5a197b76", size = 119243, upload-time = "2026-06-24T17:45:13.73Z" }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "ghp-import" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "python-dateutil" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d9/29/d40217cbe2f6b1359e00c6c307bb3fc876ba74068cbab3dde77f03ca0dc4/ghp-import-2.1.0.tar.gz", hash = "sha256:9c535c4c61193c2df8871222567d7fd7e5014d835f97dc7b7439069e2413d343", size = 10943, upload-time = "2022-05-02T15:47:16.11Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f7/ec/67fbef5d497f86283db54c22eec6f6140243aae73265799baaaa19cd17fb/ghp_import-2.1.0-py3-none-any.whl", hash = "sha256:8337dd7b50877f163d4c0289bc1f1c7f127550241988d568c1db512c4324a619", size = 11034, upload-time = "2022-05-02T15:47:14.552Z" }, +] + +[[package]] +name = "idna" +version = "3.18" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/cd/63/9496c57188a2ee585e0f1db071d75089a11e98aa86eb99d9d7618fc1edce/idna-3.18.tar.gz", hash = "sha256:ffb385a7e039654cef1ab9ef32c6fafe283c0c0467bba1d9029738ce4a14a848", size = 196711, upload-time = "2026-06-02T14:34:07.794Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/5e/d4e9f1a599fb8e573b7b87160658329fbf28d19eac2718f51fc3def3aa5a/idna-3.18-py3-none-any.whl", hash = "sha256:7f952cbe720b688055e3f87de14f5c3e5fdaa8bc3928985c4077ca689de849a2", size = 65455, upload-time = "2026-06-02T14:34:06.319Z" }, +] + +[[package]] +name = "iniconfig" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, +] + +[[package]] +name = "jinja2" +version = "3.1.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115, upload-time = "2025-03-05T20:05:02.478Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" }, +] + +[[package]] +name = "markdown" +version = "3.10.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/29/6f/da4c6aea59b3001f2e8c0ec7497475aadaf3b021c10cab5b2858f0f32b26/markdown-3.10.3.tar.gz", hash = "sha256:3589362618f743188b4d955b874402bc814f4f83f544dc207719f4baa7d9c45f", size = 372596, upload-time = "2026-07-30T19:05:29.005Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/64/69/4a5af2bc115a9a33fefe51709749de8262be3f9ba063d1753a837cdbc49c/markdown-3.10.3-py3-none-any.whl", hash = "sha256:fa6c92a00a4a3c98b22728c64a935ae1928250ae65058a6ded814d2cc29a4cea", size = 110757, upload-time = "2026-07-30T19:05:27.883Z" }, +] + +[[package]] +name = "markdown-it-py" +version = "4.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mdurl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/ff/7841249c247aa650a76b9ee4bbaeae59370dc8bfd2f6c01f3630c35eb134/markdown_it_py-4.2.0.tar.gz", hash = "sha256:04a21681d6fbb623de53f6f364d352309d4094dd4194040a10fd51833e418d49", size = 82454, upload-time = "2026-05-07T12:08:28.36Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/81/4da04ced5a082363ecfa159c010d200ecbd959ae410c10c0264a38cac0f5/markdown_it_py-4.2.0-py3-none-any.whl", hash = "sha256:9f7ebbcd14fe59494226453aed97c1070d83f8d24b6fc3a3bcf9a38092641c4a", size = 91687, upload-time = "2026-05-07T12:08:27.182Z" }, +] + +[[package]] +name = "markupsafe" +version = "3.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7e/99/7690b6d4034fffd95959cbe0c02de8deb3098cc577c67bb6a24fe5d7caa7/markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698", size = 80313, upload-time = "2025-09-27T18:37:40.426Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/33/8a/8e42d4838cd89b7dde187011e97fe6c3af66d8c044997d2183fbd6d31352/markupsafe-3.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe", size = 11619, upload-time = "2025-09-27T18:37:06.342Z" }, + { url = "https://files.pythonhosted.org/packages/b5/64/7660f8a4a8e53c924d0fa05dc3a55c9cee10bbd82b11c5afb27d44b096ce/markupsafe-3.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026", size = 12029, upload-time = "2025-09-27T18:37:07.213Z" }, + { url = "https://files.pythonhosted.org/packages/da/ef/e648bfd021127bef5fa12e1720ffed0c6cbb8310c8d9bea7266337ff06de/markupsafe-3.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737", size = 24408, upload-time = "2025-09-27T18:37:09.572Z" }, + { url = "https://files.pythonhosted.org/packages/41/3c/a36c2450754618e62008bf7435ccb0f88053e07592e6028a34776213d877/markupsafe-3.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97", size = 23005, upload-time = "2025-09-27T18:37:10.58Z" }, + { url = "https://files.pythonhosted.org/packages/bc/20/b7fdf89a8456b099837cd1dc21974632a02a999ec9bf7ca3e490aacd98e7/markupsafe-3.0.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d", size = 22048, upload-time = "2025-09-27T18:37:11.547Z" }, + { url = "https://files.pythonhosted.org/packages/9a/a7/591f592afdc734f47db08a75793a55d7fbcc6902a723ae4cfbab61010cc5/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda", size = 23821, upload-time = "2025-09-27T18:37:12.48Z" }, + { url = "https://files.pythonhosted.org/packages/7d/33/45b24e4f44195b26521bc6f1a82197118f74df348556594bd2262bda1038/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf", size = 21606, upload-time = "2025-09-27T18:37:13.485Z" }, + { url = "https://files.pythonhosted.org/packages/ff/0e/53dfaca23a69fbfbbf17a4b64072090e70717344c52eaaaa9c5ddff1e5f0/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe", size = 23043, upload-time = "2025-09-27T18:37:14.408Z" }, + { url = "https://files.pythonhosted.org/packages/46/11/f333a06fc16236d5238bfe74daccbca41459dcd8d1fa952e8fbd5dccfb70/markupsafe-3.0.3-cp314-cp314-win32.whl", hash = "sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9", size = 14747, upload-time = "2025-09-27T18:37:15.36Z" }, + { url = "https://files.pythonhosted.org/packages/28/52/182836104b33b444e400b14f797212f720cbc9ed6ba34c800639d154e821/markupsafe-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581", size = 15341, upload-time = "2025-09-27T18:37:16.496Z" }, + { url = "https://files.pythonhosted.org/packages/6f/18/acf23e91bd94fd7b3031558b1f013adfa21a8e407a3fdb32745538730382/markupsafe-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4", size = 14073, upload-time = "2025-09-27T18:37:17.476Z" }, + { url = "https://files.pythonhosted.org/packages/3c/f0/57689aa4076e1b43b15fdfa646b04653969d50cf30c32a102762be2485da/markupsafe-3.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab", size = 11661, upload-time = "2025-09-27T18:37:18.453Z" }, + { url = "https://files.pythonhosted.org/packages/89/c3/2e67a7ca217c6912985ec766c6393b636fb0c2344443ff9d91404dc4c79f/markupsafe-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175", size = 12069, upload-time = "2025-09-27T18:37:19.332Z" }, + { url = "https://files.pythonhosted.org/packages/f0/00/be561dce4e6ca66b15276e184ce4b8aec61fe83662cce2f7d72bd3249d28/markupsafe-3.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634", size = 25670, upload-time = "2025-09-27T18:37:20.245Z" }, + { url = "https://files.pythonhosted.org/packages/50/09/c419f6f5a92e5fadde27efd190eca90f05e1261b10dbd8cbcb39cd8ea1dc/markupsafe-3.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50", size = 23598, upload-time = "2025-09-27T18:37:21.177Z" }, + { url = "https://files.pythonhosted.org/packages/22/44/a0681611106e0b2921b3033fc19bc53323e0b50bc70cffdd19f7d679bb66/markupsafe-3.0.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e", size = 23261, upload-time = "2025-09-27T18:37:22.167Z" }, + { url = "https://files.pythonhosted.org/packages/5f/57/1b0b3f100259dc9fffe780cfb60d4be71375510e435efec3d116b6436d43/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5", size = 24835, upload-time = "2025-09-27T18:37:23.296Z" }, + { url = "https://files.pythonhosted.org/packages/26/6a/4bf6d0c97c4920f1597cc14dd720705eca0bf7c787aebc6bb4d1bead5388/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523", size = 22733, upload-time = "2025-09-27T18:37:24.237Z" }, + { url = "https://files.pythonhosted.org/packages/14/c7/ca723101509b518797fedc2fdf79ba57f886b4aca8a7d31857ba3ee8281f/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc", size = 23672, upload-time = "2025-09-27T18:37:25.271Z" }, + { url = "https://files.pythonhosted.org/packages/fb/df/5bd7a48c256faecd1d36edc13133e51397e41b73bb77e1a69deab746ebac/markupsafe-3.0.3-cp314-cp314t-win32.whl", hash = "sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d", size = 14819, upload-time = "2025-09-27T18:37:26.285Z" }, + { url = "https://files.pythonhosted.org/packages/1a/8a/0402ba61a2f16038b48b39bccca271134be00c5c9f0f623208399333c448/markupsafe-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9", size = 15426, upload-time = "2025-09-27T18:37:27.316Z" }, + { url = "https://files.pythonhosted.org/packages/70/bc/6f1c2f612465f5fa89b95bead1f44dcb607670fd42891d8fdcd5d039f4f4/markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa", size = 14146, upload-time = "2025-09-27T18:37:28.327Z" }, +] + +[[package]] +name = "mdformat" +version = "1.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown-it-py" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3f/05/32b5e14b192b0a8a309f32232c580aefedd9d06017cb8fe8fce34bec654c/mdformat-1.0.0.tar.gz", hash = "sha256:4954045fcae797c29f86d4ad879e43bb151fa55dbaf74ac6eaeacf1d45bb3928", size = 56953, upload-time = "2025-10-16T12:05:03.695Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/9a/8fe71b95985ca7a4001effbcc58e5a07a1f2a2884203f74dcf48a3b08315/mdformat-1.0.0-py3-none-any.whl", hash = "sha256:bca015d65a1d063a02e885a91daee303057bc7829c2cd37b2075a50dbb65944b", size = 53288, upload-time = "2025-10-16T12:05:02.607Z" }, +] + +[[package]] +name = "mdformat-front-matters" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mdformat" }, + { name = "mdit-py-plugins" }, + { name = "ruamel-yaml" }, + { name = "toml" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/2c/53/008805510d3006d9aec6c602fa40305750cb96daba8ea9abae477149909b/mdformat_front_matters-2.0.0.tar.gz", hash = "sha256:46efabe93707699120c67d4b33b51fdb4cfe0e040acfb0f8212eb33c9a3e8a9a", size = 12576, upload-time = "2025-12-04T11:44:52.265Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/00/05/4c197c000dab41f1f58290221a4c02bd57804ff42e9374d3dd0f5408aa40/mdformat_front_matters-2.0.0-py3-none-any.whl", hash = "sha256:8ea92d23d1e9427fe6548b9044e5fbf7bcf6c217941f1a9442e0efd8b0a76e61", size = 13411, upload-time = "2025-12-04T11:44:51.209Z" }, +] + +[[package]] +name = "mdformat-gfm" +version = "1.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown-it-py" }, + { name = "mdformat" }, + { name = "mdit-py-plugins" }, + { name = "wcwidth" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/56/6f/a626ebb142a290474401b67e2d61e73ce096bf7798ee22dfe6270f924b3f/mdformat_gfm-1.0.0.tar.gz", hash = "sha256:d1d49a409a6acb774ce7635c72d69178df7dce1dc8cdd10e19f78e8e57b72623", size = 10112, upload-time = "2025-10-16T09:12:22.402Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e6/18/6bc2189b744dd383cad03764f41f30352b1278d2205096f77a29c0b327ad/mdformat_gfm-1.0.0-py3-none-any.whl", hash = "sha256:7305a50efd2a140d7c83505b58e3ac5df2b09e293f9bbe72f6c7bee8c678b005", size = 10970, upload-time = "2025-10-16T09:12:21.276Z" }, +] + +[[package]] +name = "mdformat-mkdocs" +version = "5.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mdformat" }, + { name = "mdformat-gfm" }, + { name = "mdit-py-plugins" }, + { name = "more-itertools" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/2e/8c/ca9c13017fcb224e9a0c17c214279eb7273318d1890cd0adc80a3c30e443/mdformat_mkdocs-5.3.0.tar.gz", hash = "sha256:9ae35940cfc1d350c41dda717963c90c669937fbbe3be32412a2b975e4bf891d", size = 33319, upload-time = "2026-08-02T18:38:07.954Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c0/71/1e1a81c7ed1629ac41f5b3d7dd7bc93c91e106398a88f0e2949c39e04b8e/mdformat_mkdocs-5.3.0-py3-none-any.whl", hash = "sha256:46938724df5892f517130a42d5652276f9a40e80ad0bae4a2af6bde71b53f861", size = 43928, upload-time = "2026-08-02T18:38:06.347Z" }, +] + +[[package]] +name = "mdit-py-plugins" +version = "0.6.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown-it-py" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/59/fc/f8d0863f8862f25602c0404d75568e89fb6b4109804645e5cdfb1be5cf56/mdit_py_plugins-0.6.1.tar.gz", hash = "sha256:a2bca0f039f39dbd35fb74ae1b5f998608c437463371f0ff7f49a19a17a114d0", size = 56114, upload-time = "2026-05-13T09:03:38.91Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a5/69/6da5581c6a7fede7dc261bf4e67d6adca4196f176b43288b55b3db395b6e/mdit_py_plugins-0.6.1-py3-none-any.whl", hash = "sha256:214c82fb2ac524472ab6a5bcab1de80f73b50443e187f401bfd77efbc7c6481d", size = 66663, upload-time = "2026-05-13T09:03:37.76Z" }, +] + +[[package]] +name = "mdurl" +version = "0.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" }, +] + +[[package]] +name = "mergedeep" +version = "1.3.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/3a/41/580bb4006e3ed0361b8151a01d324fb03f420815446c7def45d02f74c270/mergedeep-1.3.4.tar.gz", hash = "sha256:0096d52e9dad9939c3d975a774666af186eda617e6ca84df4c94dec30004f2a8", size = 4661, upload-time = "2021-02-05T18:55:30.623Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/19/04f9b178c2d8a15b076c8b5140708fa6ffc5601fb6f1e975537072df5b2a/mergedeep-1.3.4-py3-none-any.whl", hash = "sha256:70775750742b25c0d8f36c55aed03d24c3384d17c951b3175d898bd778ef0307", size = 6354, upload-time = "2021-02-05T18:55:29.583Z" }, +] + +[[package]] +name = "mkdocs" +version = "1.6.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "ghp-import" }, + { name = "jinja2" }, + { name = "markdown" }, + { name = "markupsafe" }, + { name = "mergedeep" }, + { name = "mkdocs-get-deps" }, + { name = "packaging" }, + { name = "pathspec" }, + { name = "pyyaml" }, + { name = "pyyaml-env-tag" }, + { name = "watchdog" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/bc/c6/bbd4f061bd16b378247f12953ffcb04786a618ce5e904b8c5a01a0309061/mkdocs-1.6.1.tar.gz", hash = "sha256:7b432f01d928c084353ab39c57282f29f92136665bdd6abf7c1ec8d822ef86f2", size = 3889159, upload-time = "2024-08-30T12:24:06.899Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/22/5b/dbc6a8cddc9cfa9c4971d59fb12bb8d42e161b7e7f8cc89e49137c5b279c/mkdocs-1.6.1-py3-none-any.whl", hash = "sha256:db91759624d1647f3f34aa0c3f327dd2601beae39a366d6e064c03468d35c20e", size = 3864451, upload-time = "2024-08-30T12:24:05.054Z" }, +] + +[[package]] +name = "mkdocs-get-deps" +version = "0.2.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mergedeep" }, + { name = "platformdirs" }, + { name = "pyyaml" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ce/25/b3cccb187655b9393572bde9b09261d267c3bf2f2cdabe347673be5976a6/mkdocs_get_deps-0.2.2.tar.gz", hash = "sha256:8ee8d5f316cdbbb2834bc1df6e69c08fe769a83e040060de26d3c19fad3599a1", size = 11047, upload-time = "2026-03-10T02:46:33.632Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/29/744136411e785c4b0b744d5413e56555265939ab3a104c6a4b719dad33fd/mkdocs_get_deps-0.2.2-py3-none-any.whl", hash = "sha256:e7878cbeac04860b8b5e0ca31d3abad3df9411a75a32cde82f8e44b6c16ff650", size = 9555, upload-time = "2026-03-10T02:46:32.256Z" }, +] + +[[package]] +name = "mkdocs-material" +version = "9.7.7" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "babel" }, + { name = "backrefs" }, + { name = "colorama" }, + { name = "jinja2" }, + { name = "markdown" }, + { name = "mkdocs" }, + { name = "mkdocs-material-extensions" }, + { name = "paginate" }, + { name = "pygments" }, + { name = "pymdown-extensions" }, + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f1/cd/c05d3a530ba7934f144fb45f7203cd236adc25c7bdcc34673d202f4b0278/mkdocs_material-9.7.7.tar.gz", hash = "sha256:c0649c065b1b0512d60aad8c10f947f8e455284475239b364b610f2deb4d0855", size = 4097923, upload-time = "2026-07-17T16:21:33.156Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ad/21/17c1bc9e6f47c972ad66fb2ac2568f99f90f1207eeb6fc3b34d094dba7b5/mkdocs_material-9.7.7-py3-none-any.whl", hash = "sha256:8ea9bb1737a5b524a5f9dcf2e1b4ebda8274ae3008aa7845720a97083bef708f", size = 9305438, upload-time = "2026-07-17T16:21:30.017Z" }, +] + +[[package]] +name = "mkdocs-material-extensions" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/79/9b/9b4c96d6593b2a541e1cb8b34899a6d021d208bb357042823d4d2cabdbe7/mkdocs_material_extensions-1.3.1.tar.gz", hash = "sha256:10c9511cea88f568257f960358a467d12b970e1f7b2c0e5fb2bb48cab1928443", size = 11847, upload-time = "2023-11-22T19:09:45.208Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5b/54/662a4743aa81d9582ee9339d4ffa3c8fd40a4965e033d77b9da9774d3960/mkdocs_material_extensions-1.3.1-py3-none-any.whl", hash = "sha256:adff8b62700b25cb77b53358dad940f3ef973dd6db797907c49e3c2ef3ab4e31", size = 8728, upload-time = "2023-11-22T19:09:43.465Z" }, +] + +[[package]] +name = "more-itertools" +version = "11.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/de/1d/f4da6f02cdffe04d6362210b807146a26044c88d839208aec273bb0d9184/more_itertools-11.1.0.tar.gz", hash = "sha256:48e8f4d9e7e5878571ecf6f2b4e57634f93cd474cc8cfbd2376f2d11b396e30d", size = 145772, upload-time = "2026-05-22T14:14:29.909Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e8/3d/1087453384dbde46a8c7f9356eead2c58be8a7bf156bca40243377c85715/more_itertools-11.1.0-py3-none-any.whl", hash = "sha256:4b65538ae22f6fed0ce4874efd317463a7489796a0939fa66824dd542125a192", size = 72226, upload-time = "2026-05-22T14:14:28.824Z" }, +] + +[[package]] +name = "nmteam-support" +version = "0.1.0" +source = { editable = "." } +dependencies = [ + { name = "mkdocs" }, + { name = "mkdocs-material" }, + { name = "pyyaml" }, +] + +[package.dev-dependencies] +dev = [ + { name = "mdformat" }, + { name = "mdformat-front-matters" }, + { name = "mdformat-mkdocs" }, + { name = "pytest" }, + { name = "ruff" }, +] + +[package.metadata] +requires-dist = [ + { name = "mkdocs", specifier = "==1.6.1" }, + { name = "mkdocs-material", specifier = "==9.7.7" }, + { name = "pyyaml", specifier = ">=6.0.2" }, +] + +[package.metadata.requires-dev] +dev = [ + { name = "mdformat", specifier = ">=1.0" }, + { name = "mdformat-front-matters", specifier = ">=2.0" }, + { name = "mdformat-mkdocs", specifier = ">=5.2.2" }, + { name = "pytest", specifier = ">=9.0" }, + { name = "ruff", specifier = ">=0.16" }, +] + +[[package]] +name = "packaging" +version = "26.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7d/fa/3944b40b07da9ce895c0e6303a5ab7d53da063554f534556b134a54d6093/packaging-26.3.tar.gz", hash = "sha256:94edc256424af38762eb31306eed28beb9f0efc50a8837492c9d6fd6004aed79", size = 313412, upload-time = "2026-08-04T18:15:28.737Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/63/34/ba1c580383c9eada3711951fef0795c80b829a078d72188184bcab9dd527/packaging-26.3-py3-none-any.whl", hash = "sha256:d7193f7c8e4e93f444fde0262bf90af30e16fa0ad0ad44cb553c87339b23cd1c", size = 129956, upload-time = "2026-08-04T18:15:27.159Z" }, +] + +[[package]] +name = "paginate" +version = "0.5.7" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ec/46/68dde5b6bc00c1296ec6466ab27dddede6aec9af1b99090e1107091b3b84/paginate-0.5.7.tar.gz", hash = "sha256:22bd083ab41e1a8b4f3690544afb2c60c25e5c9a63a30fa2f483f6c60c8e5945", size = 19252, upload-time = "2024-08-25T14:17:24.139Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/90/96/04b8e52da071d28f5e21a805b19cb9390aa17a47462ac87f5e2696b9566d/paginate-0.5.7-py2.py3-none-any.whl", hash = "sha256:b885e2af73abcf01d9559fd5216b57ef722f8c42affbb63942377668e35c7591", size = 13746, upload-time = "2024-08-25T14:17:22.55Z" }, +] + +[[package]] +name = "pathspec" +version = "1.1.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5a/82/42f767fc1c1143d6fd36efb827202a2d997a375e160a71eb2888a925aac1/pathspec-1.1.1.tar.gz", hash = "sha256:17db5ecd524104a120e173814c90367a96a98d07c45b2e10c2f3919fff91bf5a", size = 135180, upload-time = "2026-04-27T01:46:08.907Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f1/d9/7fb5aa316bc299258e68c73ba3bddbc499654a07f151cba08f6153988714/pathspec-1.1.1-py3-none-any.whl", hash = "sha256:a00ce642f577bf7f473932318056212bc4f8bfdf53128c78bbd5af0b9b20b189", size = 57328, upload-time = "2026-04-27T01:46:07.06Z" }, +] + +[[package]] +name = "platformdirs" +version = "4.11.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/36/0a/062135c9a98dac804265073cc3afdbec5ae1aa37980bb354f461bafe81b4/platformdirs-4.11.1.tar.gz", hash = "sha256:bb1af68078f25e2f3e111e2d43b8d536df41b73c8a684b40bb018223b66fae27", size = 32396, upload-time = "2026-08-07T23:06:48.516Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4c/85/9b31b44296cfa3bb56cddb35e6a0f6578bab0b490c0806c0245e32c6110c/platformdirs-4.11.1-py3-none-any.whl", hash = "sha256:2efd27d363e8dd2e661639ffb398865a5e0a46442a11d266bf375a0e0c10e386", size = 23261, upload-time = "2026-08-07T23:06:47.219Z" }, +] + +[[package]] +name = "pluggy" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, +] + +[[package]] +name = "pygments" +version = "2.20.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c3/b2/bc9c9196916376152d655522fdcebac55e66de6603a76a02bca1b6414f6c/pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f", size = 4955991, upload-time = "2026-03-29T13:29:33.898Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" }, +] + +[[package]] +name = "pymdown-extensions" +version = "11.0.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown" }, + { name = "pyyaml" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/21/a9/5f0c535ba3b08fe09270c16808e053a968868242ecbd5676d4e3a488bf28/pymdown_extensions-11.0.1.tar.gz", hash = "sha256:dd2905ae6fc5b75582fafb139a1266ffc754705efa902aa50067fa7ff4f94ec0", size = 857113, upload-time = "2026-07-02T17:59:22.955Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d6/54/da572c98c0b77626a91b5d3b89f0231d8bff5125c225420908632f8b342d/pymdown_extensions-11.0.1-py3-none-any.whl", hash = "sha256:db3943a62bab7e03af1364f0c4083e64b91fb097675a4b6cceccfbe9a77e5eb2", size = 269455, upload-time = "2026-07-02T17:59:21.271Z" }, +] + +[[package]] +name = "pytest" +version = "9.1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e4/47/b9efed96c114afcfa3c9d3fe98a76a1d14c74a9e266d397cf6eb64be5e01/pytest-9.1.1.tar.gz", hash = "sha256:1088fbde8f2b49d95a549a195707afa7a76a3ce9bcadc26b6d71f0ffda5fe313", size = 1636369, upload-time = "2026-06-19T10:58:32.857Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/24/25/1de2678b631f5a49215c6c96fff41ba892b0a34df68d6d80292b1b48aa7f/pytest-9.1.1-py3-none-any.whl", hash = "sha256:37a86b45efb9a47a61a36449063e8e18d0cab3161329fc099eb21783169c4f0c", size = 386536, upload-time = "2026-06-19T10:58:31.347Z" }, +] + +[[package]] +name = "python-dateutil" +version = "2.9.0.post0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "six" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432, upload-time = "2024-03-01T18:36:20.211Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" }, +] + +[[package]] +name = "pyyaml" +version = "6.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9d/8c/f4bd7f6465179953d3ac9bc44ac1a8a3e6122cf8ada906b4f96c60172d43/pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac", size = 181814, upload-time = "2025-09-25T21:32:35.712Z" }, + { url = "https://files.pythonhosted.org/packages/bd/9c/4d95bb87eb2063d20db7b60faa3840c1b18025517ae857371c4dd55a6b3a/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310", size = 173809, upload-time = "2025-09-25T21:32:36.789Z" }, + { url = "https://files.pythonhosted.org/packages/92/b5/47e807c2623074914e29dabd16cbbdd4bf5e9b2db9f8090fa64411fc5382/pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7", size = 766454, upload-time = "2025-09-25T21:32:37.966Z" }, + { url = "https://files.pythonhosted.org/packages/02/9e/e5e9b168be58564121efb3de6859c452fccde0ab093d8438905899a3a483/pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788", size = 836355, upload-time = "2025-09-25T21:32:39.178Z" }, + { url = "https://files.pythonhosted.org/packages/88/f9/16491d7ed2a919954993e48aa941b200f38040928474c9e85ea9e64222c3/pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5", size = 794175, upload-time = "2025-09-25T21:32:40.865Z" }, + { url = "https://files.pythonhosted.org/packages/dd/3f/5989debef34dc6397317802b527dbbafb2b4760878a53d4166579111411e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764", size = 755228, upload-time = "2025-09-25T21:32:42.084Z" }, + { url = "https://files.pythonhosted.org/packages/d7/ce/af88a49043cd2e265be63d083fc75b27b6ed062f5f9fd6cdc223ad62f03e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35", size = 789194, upload-time = "2025-09-25T21:32:43.362Z" }, + { url = "https://files.pythonhosted.org/packages/23/20/bb6982b26a40bb43951265ba29d4c246ef0ff59c9fdcdf0ed04e0687de4d/pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac", size = 156429, upload-time = "2025-09-25T21:32:57.844Z" }, + { url = "https://files.pythonhosted.org/packages/f4/f4/a4541072bb9422c8a883ab55255f918fa378ecf083f5b85e87fc2b4eda1b/pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3", size = 143912, upload-time = "2025-09-25T21:32:59.247Z" }, + { url = "https://files.pythonhosted.org/packages/7c/f9/07dd09ae774e4616edf6cda684ee78f97777bdd15847253637a6f052a62f/pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3", size = 189108, upload-time = "2025-09-25T21:32:44.377Z" }, + { url = "https://files.pythonhosted.org/packages/4e/78/8d08c9fb7ce09ad8c38ad533c1191cf27f7ae1effe5bb9400a46d9437fcf/pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba", size = 183641, upload-time = "2025-09-25T21:32:45.407Z" }, + { url = "https://files.pythonhosted.org/packages/7b/5b/3babb19104a46945cf816d047db2788bcaf8c94527a805610b0289a01c6b/pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c", size = 831901, upload-time = "2025-09-25T21:32:48.83Z" }, + { url = "https://files.pythonhosted.org/packages/8b/cc/dff0684d8dc44da4d22a13f35f073d558c268780ce3c6ba1b87055bb0b87/pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702", size = 861132, upload-time = "2025-09-25T21:32:50.149Z" }, + { url = "https://files.pythonhosted.org/packages/b1/5e/f77dc6b9036943e285ba76b49e118d9ea929885becb0a29ba8a7c75e29fe/pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c", size = 839261, upload-time = "2025-09-25T21:32:51.808Z" }, + { url = "https://files.pythonhosted.org/packages/ce/88/a9db1376aa2a228197c58b37302f284b5617f56a5d959fd1763fb1675ce6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065", size = 805272, upload-time = "2025-09-25T21:32:52.941Z" }, + { url = "https://files.pythonhosted.org/packages/da/92/1446574745d74df0c92e6aa4a7b0b3130706a4142b2d1a5869f2eaa423c6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65", size = 829923, upload-time = "2025-09-25T21:32:54.537Z" }, + { url = "https://files.pythonhosted.org/packages/f0/7a/1c7270340330e575b92f397352af856a8c06f230aa3e76f86b39d01b416a/pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9", size = 174062, upload-time = "2025-09-25T21:32:55.767Z" }, + { url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" }, +] + +[[package]] +name = "pyyaml-env-tag" +version = "1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyyaml" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/eb/2e/79c822141bfd05a853236b504869ebc6b70159afc570e1d5a20641782eaa/pyyaml_env_tag-1.1.tar.gz", hash = "sha256:2eb38b75a2d21ee0475d6d97ec19c63287a7e140231e4214969d0eac923cd7ff", size = 5737, upload-time = "2025-05-13T15:24:01.64Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/11/432f32f8097b03e3cd5fe57e88efb685d964e2e5178a48ed61e841f7fdce/pyyaml_env_tag-1.1-py3-none-any.whl", hash = "sha256:17109e1a528561e32f026364712fee1264bc2ea6715120891174ed1b980d2e04", size = 4722, upload-time = "2025-05-13T15:23:59.629Z" }, +] + +[[package]] +name = "requests" +version = "2.34.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "charset-normalizer" }, + { name = "idna" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ac/c3/e2a2b89f2d3e2179abd6d00ebd70bff6273f37fb3e0cc209f48b39d00cbf/requests-2.34.2.tar.gz", hash = "sha256:f288924cae4e29463698d6d60bc6a4da69c89185ad1e0bcc4104f584e960b9ed", size = 142856, upload-time = "2026-05-14T19:25:27.735Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/f4/c67b0b3f1b9245e8d266f0f112c500d50e5b4e83cb6f3b71b6528104182a/requests-2.34.2-py3-none-any.whl", hash = "sha256:2a0d60c172f83ac6ab31e4554906c0f3b3588d37b5cb939b1c061f4907e278e0", size = 73075, upload-time = "2026-05-14T19:25:26.443Z" }, +] + +[[package]] +name = "ruamel-yaml" +version = "0.19.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c7/3b/ebda527b56beb90cb7652cb1c7e4f91f48649fbcd8d2eb2fb6e77cd3329b/ruamel_yaml-0.19.1.tar.gz", hash = "sha256:53eb66cd27849eff968ebf8f0bf61f46cdac2da1d1f3576dd4ccee9b25c31993", size = 142709, upload-time = "2026-01-02T16:50:31.84Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b8/0c/51f6841f1d84f404f92463fc2b1ba0da357ca1e3db6b7fbda26956c3b82a/ruamel_yaml-0.19.1-py3-none-any.whl", hash = "sha256:27592957fedf6e0b62f281e96effd28043345e0e66001f97683aa9a40c667c93", size = 118102, upload-time = "2026-01-02T16:50:29.201Z" }, +] + +[[package]] +name = "ruff" +version = "0.16.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/73/e1/4508a569211b35599016e84ba65c1a992b7a4004b4b6c4bea02a851cba1b/ruff-0.16.2.tar.gz", hash = "sha256:c3d7828d12e8927a6fc65fe38e2c2541b9e762d360a1786d752cb1b8883b3c9c", size = 4885811, upload-time = "2026-08-07T13:31:01.432Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/14/57/db19951540f98859c956b50bdb4d31089b4d91e9f15e2968e7d5193806d5/ruff-0.16.2-py3-none-linux_armv6l.whl", hash = "sha256:3c8de4cf2181f01d57946d87d777aa52916976fc09942aed89938fab5e013318", size = 10847925, upload-time = "2026-08-07T13:30:14.468Z" }, + { url = "https://files.pythonhosted.org/packages/13/5a/995fe85a8470d3e391ac0f7fa8054bb454eaf33ee138196d6172ed1079c0/ruff-0.16.2-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:9a48cc05c6fbc811ca81b5d7ba95375affea6582d1b8024e455e41afbbf55344", size = 11072662, upload-time = "2026-08-07T13:30:18.143Z" }, + { url = "https://files.pythonhosted.org/packages/32/53/370d767c61c71a971a4ace36703a7ecd8c393956349a7325d7fab2b56827/ruff-0.16.2-py3-none-macosx_11_0_arm64.whl", hash = "sha256:a2c0d14fcbb26c91f0f867a6dc9bd71bbc30b1b6151829c884f23faeab2e5700", size = 10566771, upload-time = "2026-08-07T13:30:20.899Z" }, + { url = "https://files.pythonhosted.org/packages/85/d6/9d96948caf5a632be62d62202d5ec914d6856f204fd79eb036e5915e79ea/ruff-0.16.2-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:335c621622c4650330be50842561c6586ac6971bb8ab5407fe34dcc9efb16bbe", size = 10975825, upload-time = "2026-08-07T13:30:23.517Z" }, + { url = "https://files.pythonhosted.org/packages/3b/92/ea87129b3414acb0b5770563779c51804d37ac67675c7ba35447ddb14773/ruff-0.16.2-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:20e66910f2c37cc753f9ef6580c914a621b80c4fa3549d3e3521e29d0f5bfc3f", size = 10649437, upload-time = "2026-08-07T13:30:26.097Z" }, + { url = "https://files.pythonhosted.org/packages/ac/43/f8f291dcd4af5bb7872b74fdfa41a7cd7c856ca1d4069670971cf1b9f5cb/ruff-0.16.2-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c7e36fbfba65510548156902bcf1350a979a958ce0347ce0f90d73894036b39f", size = 11446761, upload-time = "2026-08-07T13:30:28.752Z" }, + { url = "https://files.pythonhosted.org/packages/71/4a/ef991fb2fcf516ab71f0808adcdd8da5e18c8cde447f4ceaf5f47a5132a5/ruff-0.16.2-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f0eab35f80df8f134aae5d1630e751901321d317cc8e50dc39e36fa3ed34cd12", size = 12336364, upload-time = "2026-08-07T13:30:31.468Z" }, + { url = "https://files.pythonhosted.org/packages/f3/24/f615e74f307e6ca0e56a482872477b856c70d530aa356abfb6dfe5ca8a80/ruff-0.16.2-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:40ea8c0594feb894e89c8c61ab9c103d38b0ea72dfde6c594107147ca31b1140", size = 11630720, upload-time = "2026-08-07T13:30:34.426Z" }, + { url = "https://files.pythonhosted.org/packages/c5/d3/8ef50149e8412a77f7ab409efdef0e2b23803707a3863da4fc64cb23d459/ruff-0.16.2-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ab3d62dde0b19facdd632008cc4827fc28ada7736c6bd35ab6f1050f0bfed53f", size = 11466130, upload-time = "2026-08-07T13:30:36.958Z" }, + { url = "https://files.pythonhosted.org/packages/dd/a7/a19334985c4dea8c381981fa252cd854c7ee52dc4b1686dc16f4a911c702/ruff-0.16.2-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:e43e1f5b8388da9eca1b9e88328d47a5cec794633ccf6f7484ac2dd15eee92c0", size = 11523634, upload-time = "2026-08-07T13:30:39.822Z" }, + { url = "https://files.pythonhosted.org/packages/6e/6c/96d192b0e742412ceda08c0a50f9669b253dde9fd6a60ea1a10c9fa79a63/ruff-0.16.2-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:c24788a980581e1d7ea3a0cbe4344c4fbeb0a6a9b1f4713aa46bb104f8294690", size = 10949807, upload-time = "2026-08-07T13:30:42.745Z" }, + { url = "https://files.pythonhosted.org/packages/fa/51/e26599ceca11e79ee255c7df515995561edf87e9ca1893284e44d98f5a86/ruff-0.16.2-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:81806b08329130005dd4a8a8394a0c9da8c6f4cafb16ba438d2a2ee6a18bedf1", size = 10646891, upload-time = "2026-08-07T13:30:45.522Z" }, + { url = "https://files.pythonhosted.org/packages/68/01/800c4b1f97bc8d7c6029e06b1f20473a3cf1e13c4933d8f3342add83fc55/ruff-0.16.2-py3-none-musllinux_1_2_i686.whl", hash = "sha256:4ce4e02bad779bef557f541a1b31f20d6abeae1cc05ed1b1ac019d4ffd1044c8", size = 11162063, upload-time = "2026-08-07T13:30:48.131Z" }, + { url = "https://files.pythonhosted.org/packages/e4/d0/1477ea50fc5a0d4b0b71d1d63d50770bdd794d90b43e37a7618e63ec9894/ruff-0.16.2-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:e0422abdf70070255fc4073ce9dfc814cc03db577013761ddd09bc1e4a9a4fbd", size = 11556038, upload-time = "2026-08-07T13:30:50.686Z" }, + { url = "https://files.pythonhosted.org/packages/b8/76/a7776f32048d991e16d4fa8ff91790b877342d3596cc3ed04acdbf1aaedc/ruff-0.16.2-py3-none-win32.whl", hash = "sha256:bf3a63d78fb39f4bf5ac8ae52051c5520505301abe19ba4e204c453b3f09bb0b", size = 10872850, upload-time = "2026-08-07T13:30:53.471Z" }, + { url = "https://files.pythonhosted.org/packages/00/0d/929c800d920e61397d82a01b60bffc68da3052c17d31de59efaad2e4ed75/ruff-0.16.2-py3-none-win_amd64.whl", hash = "sha256:bcabe2f6d0fc7819f1431793005af4e4de7371927d037345bf941252b195b9fa", size = 12023338, upload-time = "2026-08-07T13:30:56.193Z" }, + { url = "https://files.pythonhosted.org/packages/5b/6c/93e26c22c5f78ff87363e07da49c84955affbeb1098bd1936bf3b3f293bf/ruff-0.16.2-py3-none-win_arm64.whl", hash = "sha256:d614e95cedf38a2053fd351c55b103ba30d017d61688fdbfd40ee0412852a99f", size = 11374065, upload-time = "2026-08-07T13:30:58.775Z" }, +] + +[[package]] +name = "six" +version = "1.17.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" }, +] + +[[package]] +name = "toml" +version = "0.10.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/be/ba/1f744cdc819428fc6b5084ec34d9b30660f6f9daaf70eead706e3203ec3c/toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f", size = 22253, upload-time = "2020-11-01T01:40:22.204Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/44/6f/7120676b6d73228c96e17f1f794d8ab046fc910d781c8d151120c3f1569e/toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b", size = 16588, upload-time = "2020-11-01T01:40:20.672Z" }, +] + +[[package]] +name = "urllib3" +version = "2.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/53/0c/06f8b233b8fd13b9e5ee11424ef85419ba0d8ba0b3138bf360be2ff56953/urllib3-2.7.0.tar.gz", hash = "sha256:231e0ec3b63ceb14667c67be60f2f2c40a518cb38b03af60abc813da26505f4c", size = 433602, upload-time = "2026-05-07T16:13:18.596Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7f/3e/5db95bcf282c52709639744ca2a8b149baccf648e39c8cc87553df9eae0c/urllib3-2.7.0-py3-none-any.whl", hash = "sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897", size = 131087, upload-time = "2026-05-07T16:13:17.151Z" }, +] + +[[package]] +name = "watchdog" +version = "6.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/db/7d/7f3d619e951c88ed75c6037b246ddcf2d322812ee8ea189be89511721d54/watchdog-6.0.0.tar.gz", hash = "sha256:9ddf7c82fda3ae8e24decda1338ede66e1c99883db93711d8fb941eaa2d8c282", size = 131220, upload-time = "2024-11-01T14:07:13.037Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a9/c7/ca4bf3e518cb57a686b2feb4f55a1892fd9a3dd13f470fca14e00f80ea36/watchdog-6.0.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:7607498efa04a3542ae3e05e64da8202e58159aa1fa4acddf7678d34a35d4f13", size = 79079, upload-time = "2024-11-01T14:06:59.472Z" }, + { url = "https://files.pythonhosted.org/packages/5c/51/d46dc9332f9a647593c947b4b88e2381c8dfc0942d15b8edc0310fa4abb1/watchdog-6.0.0-py3-none-manylinux2014_armv7l.whl", hash = "sha256:9041567ee8953024c83343288ccc458fd0a2d811d6a0fd68c4c22609e3490379", size = 79078, upload-time = "2024-11-01T14:07:01.431Z" }, + { url = "https://files.pythonhosted.org/packages/d4/57/04edbf5e169cd318d5f07b4766fee38e825d64b6913ca157ca32d1a42267/watchdog-6.0.0-py3-none-manylinux2014_i686.whl", hash = "sha256:82dc3e3143c7e38ec49d61af98d6558288c415eac98486a5c581726e0737c00e", size = 79076, upload-time = "2024-11-01T14:07:02.568Z" }, + { url = "https://files.pythonhosted.org/packages/ab/cc/da8422b300e13cb187d2203f20b9253e91058aaf7db65b74142013478e66/watchdog-6.0.0-py3-none-manylinux2014_ppc64.whl", hash = "sha256:212ac9b8bf1161dc91bd09c048048a95ca3a4c4f5e5d4a7d1b1a7d5752a7f96f", size = 79077, upload-time = "2024-11-01T14:07:03.893Z" }, + { url = "https://files.pythonhosted.org/packages/2c/3b/b8964e04ae1a025c44ba8e4291f86e97fac443bca31de8bd98d3263d2fcf/watchdog-6.0.0-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:e3df4cbb9a450c6d49318f6d14f4bbc80d763fa587ba46ec86f99f9e6876bb26", size = 79078, upload-time = "2024-11-01T14:07:05.189Z" }, + { url = "https://files.pythonhosted.org/packages/62/ae/a696eb424bedff7407801c257d4b1afda455fe40821a2be430e173660e81/watchdog-6.0.0-py3-none-manylinux2014_s390x.whl", hash = "sha256:2cce7cfc2008eb51feb6aab51251fd79b85d9894e98ba847408f662b3395ca3c", size = 79077, upload-time = "2024-11-01T14:07:06.376Z" }, + { url = "https://files.pythonhosted.org/packages/b5/e8/dbf020b4d98251a9860752a094d09a65e1b436ad181faf929983f697048f/watchdog-6.0.0-py3-none-manylinux2014_x86_64.whl", hash = "sha256:20ffe5b202af80ab4266dcd3e91aae72bf2da48c0d33bdb15c66658e685e94e2", size = 79078, upload-time = "2024-11-01T14:07:07.547Z" }, + { url = "https://files.pythonhosted.org/packages/07/f6/d0e5b343768e8bcb4cda79f0f2f55051bf26177ecd5651f84c07567461cf/watchdog-6.0.0-py3-none-win32.whl", hash = "sha256:07df1fdd701c5d4c8e55ef6cf55b8f0120fe1aef7ef39a1c6fc6bc2e606d517a", size = 79065, upload-time = "2024-11-01T14:07:09.525Z" }, + { url = "https://files.pythonhosted.org/packages/db/d9/c495884c6e548fce18a8f40568ff120bc3a4b7b99813081c8ac0c936fa64/watchdog-6.0.0-py3-none-win_amd64.whl", hash = "sha256:cbafb470cf848d93b5d013e2ecb245d4aa1c8fd0504e863ccefa32445359d680", size = 79070, upload-time = "2024-11-01T14:07:10.686Z" }, + { url = "https://files.pythonhosted.org/packages/33/e8/e40370e6d74ddba47f002a32919d91310d6074130fe4e17dabcafc15cbf1/watchdog-6.0.0-py3-none-win_ia64.whl", hash = "sha256:a1914259fa9e1454315171103c6a30961236f508b9b623eae470268bbcc6a22f", size = 79067, upload-time = "2024-11-01T14:07:11.845Z" }, +] + +[[package]] +name = "wcwidth" +version = "0.8.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/34/74/c6428f875774288bec1396f5bfcbc2d925700a4dad61727fd5f2b12f249d/wcwidth-0.8.2.tar.gz", hash = "sha256:91fbef97204b96a3d4d421609b80340b760cf33e26da123ff243d76b1fda8dda", size = 1466253, upload-time = "2026-06-29T18:11:11.601Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/96/42/3e5985a0a7e57de470b320c6d6a1a67c844f6737a587f3d44dd13d1819e7/wcwidth-0.8.2-py3-none-any.whl", hash = "sha256:d63947694a0539a1d51e01eda7caf800c291020e6cdd7e28ad7b14dd33ad4f85", size = 323166, upload-time = "2026-06-29T18:11:09.888Z" }, +] From 20e2623b731776abeb8e72bb3013784dadc24486 Mon Sep 17 00:00:00 2001 From: Alice39s Date: Sun, 9 Aug 2026 15:49:32 +0900 Subject: [PATCH 02/57] test(models): add pytest fixtures and data model coverage --- pyproject.toml | 5 +++++ src/nmteam_support/models.py | 34 ++++++++++++++++++++++++++++++ tests/conftest.py | 40 ++++++++++++++++++++++++++++++++++++ tests/test_models.py | 19 +++++++++++++++++ 4 files changed, 98 insertions(+) create mode 100644 src/nmteam_support/models.py create mode 100644 tests/conftest.py create mode 100644 tests/test_models.py diff --git a/pyproject.toml b/pyproject.toml index 5c6dc6f..a7f8aca 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -24,3 +24,8 @@ build-backend = "hatchling.build" [tool.hatch.build.targets.wheel] packages = ["src/nmteam_support"] + +[tool.pytest.ini_options] +testpaths = ["tests"] +pythonpath = ["src"] +addopts = "-ra" diff --git a/src/nmteam_support/models.py b/src/nmteam_support/models.py new file mode 100644 index 0000000..865ac74 --- /dev/null +++ b/src/nmteam_support/models.py @@ -0,0 +1,34 @@ +"""Core data models shared across the generator.""" + +from __future__ import annotations + +from dataclasses import dataclass + + +@dataclass(frozen=True) +class PageMetadata: + """Metadata parsed from a page's YAML frontmatter (or derived defaults).""" + + title: str + description: str = "" + index: int = 0 + hide_docs_list: bool = False + hide_contributing_note: bool = False + hide_navigation: bool = False + + +@dataclass(frozen=True) +class DocEntry: + """A single nav/docsList entry: a document or a sub-folder. + + ``path`` is relative to the docs root, e.g. ``nmbot-telegram/mcp.md`` + (documents) or ``nmbot-telegram/panel`` (folders). + """ + + title: str + description: str + path: str + name: str # file name or folder name + index: int = 0 + kind: str = "doc" # "doc" | "folder" + hide_contributing_note: bool = False diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..fa88588 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,40 @@ +"""Shared fixtures.""" + +from __future__ import annotations + +from pathlib import Path + +import pytest + + +@pytest.fixture +def docs_dir(tmp_path: Path) -> Path: + """A minimal docs tree: root page + two product folders.""" + docs = tmp_path / "docs" + (docs / "nmbot-telegram").mkdir(parents=True) + (docs / "contact-us").mkdir() + (docs / "index.md").write_text( + "---\ntitle: nmTeam 支持\ndescription: 支持中心。\nhide_docs_list: true\n---\n\n# nmTeam 支持\n\n正文。\n", + encoding="utf-8", + ) + (docs / "about.md").write_text( + "---\ntitle: 关于\nindex: 200\ndescription: 了解此文档。\n---\n\n# 关于\n", + encoding="utf-8", + ) + (docs / "nmbot-telegram" / "index.md").write_text( + "---\ntitle: nmBot Telegram\n---\n\n# nmBot\n\n简介。\n", + encoding="utf-8", + ) + (docs / "nmbot-telegram" / "mcp.md").write_text( + "---\ntitle: MCP 配置\ndescription: 配置 MCP。\n---\n\n# MCP 配置\n\n正文。\n", + encoding="utf-8", + ) + (docs / "contact-us" / "index.md").write_text( + "---\ntitle: 联系我们\nindex: 100\n---\n\n# 联系\n\n方式。\n", + encoding="utf-8", + ) + (docs / "contact-us" / "forum.md").write_text( + "---\ntitle: 论坛\nindex: -1\n---\n\n# 论坛\n", + encoding="utf-8", + ) + return docs diff --git a/tests/test_models.py b/tests/test_models.py new file mode 100644 index 0000000..3e63ea5 --- /dev/null +++ b/tests/test_models.py @@ -0,0 +1,19 @@ +"""Model smoke tests.""" + +from nmteam_support.models import DocEntry, PageMetadata + + +def test_page_metadata_defaults(): + meta = PageMetadata(title="t") + assert meta.description == "" + assert meta.index == 0 + assert not meta.hide_docs_list + assert not meta.hide_contributing_note + assert not meta.hide_navigation + + +def test_doc_entry_fields(): + entry = DocEntry(title="A", description="d", path="a/b.md", name="b.md", index=3, kind="doc") + assert entry.path == "a/b.md" + assert entry.kind == "doc" + assert not entry.hide_contributing_note From 9a35d62be9c3047e7723c76b5a26fe0af5689df0 Mon Sep 17 00:00:00 2001 From: Alice39s Date: Sun, 9 Aug 2026 15:50:28 +0900 Subject: [PATCH 03/57] feat(frontmatter): parse page metadata and descriptions --- src/nmteam_support/frontmatter.py | 119 ++++++++++++++++++++++++++++++ tests/test_frontmatter.py | 22 ++++++ 2 files changed, 141 insertions(+) create mode 100644 src/nmteam_support/frontmatter.py create mode 100644 tests/test_frontmatter.py diff --git a/src/nmteam_support/frontmatter.py b/src/nmteam_support/frontmatter.py new file mode 100644 index 0000000..08c48bc --- /dev/null +++ b/src/nmteam_support/frontmatter.py @@ -0,0 +1,119 @@ +"""Frontmatter parsing and metadata extraction. + +Mirrors the behavior of the original generate.py while fixing two defects: +- an unterminated frontmatter fence no longer crashes (old code raised IndexError); +- description extraction is bounded and deterministic (old code could loop forever + and produced concatenated garbage for pages without a ``description:`` field). +""" + +from __future__ import annotations + +import re + +from nmteam_support.models import PageMetadata + +_MARKDOWN_CHARS = re.compile(r"[\[\]\(\)#`>*_]|\|") + + +def split_frontmatter(text: str) -> tuple[str, str]: + """Split ``text`` into ``(metadata_raw, body)``. + + Mirrors the original ``text.split("---", 2)`` behavior used when injecting + contributing notes: only a leading ``---`` triggers a split, and an + unterminated fence degrades to ``("", text)``. + """ + if not text.startswith("---"): + return ("", text) + parts = text.split("---", 2) + if len(parts) < 3: + return ("", text) + return (parts[1], parts[2]) + + +def split_frontmatter_lines(text: str) -> tuple[str, str]: + """Line-based split used for metadata extraction. + + Returns ``(metadata_raw, body)``; ``body`` is everything after the closing + fence (or the whole text when there is no leading fence). + """ + lines = text.split("\n") + if not lines or not lines[0].startswith("---"): + return ("", text) + i = 1 + while i < len(lines) and not lines[i].startswith("---"): + i += 1 + if i >= len(lines): + return ("", text) + metadata_raw = "\n".join(lines[1:i]) + return (metadata_raw, "\n".join(lines[i + 1 :])) + + +def extract_title(metadata_raw: str, body: str, filename: str) -> str: + """Title from ``title:`` frontmatter, else the first ``#`` heading, else the file name.""" + if metadata_raw: + match = re.search(r"title: (.*)", metadata_raw) + if match: + return match.group(1).strip() + match = re.search(r"# (.*)", body) + if match: + return match.group(1).strip() + return filename.replace(".md", "").replace("-", " ").title() + + +def extract_description(metadata_raw: str, body: str) -> str: + """Description from ``description:`` frontmatter, else the first prose line of the body.""" + if metadata_raw: + match = re.search(r"description: (.*)", metadata_raw) + if match: + return match.group(1).strip() + for line in body.split("\n"): + stripped = line.strip() + if not stripped: + continue + if stripped.startswith(("#", "![", "<", "!!!")): + continue + text = _MARKDOWN_CHARS.sub("", line).strip() + if not text: + continue + return text[:100] + "..." if len(text) > 100 else text + return "" + + +def extract_index(metadata_raw: str) -> int: + if metadata_raw: + match = re.search(r"index: (.*)", metadata_raw) + if match: + try: + return int(match.group(1)) + except ValueError: + return 0 + return 0 + + +def extract_hide_docs_list(metadata_raw: str) -> bool: + match = re.search(r"hide_docs_list: (.*)", metadata_raw) + return bool(match and match.group(1).strip() == "true") + + +def extract_hide_contributing_note(metadata_raw: str) -> bool: + return bool(metadata_raw and re.search(r"hideContributingNote", metadata_raw, re.IGNORECASE)) + + +def extract_hide_navigation(metadata_raw: str) -> bool: + return bool(metadata_raw and re.search(r"- navigation", metadata_raw)) + + +def parse_page(text: str, filename: str) -> tuple[PageMetadata, str]: + """Parse a full markdown page into its metadata and body (after frontmatter).""" + metadata_raw, body = split_frontmatter_lines(text) + return ( + PageMetadata( + title=extract_title(metadata_raw, body, filename), + description=extract_description(metadata_raw, body), + index=extract_index(metadata_raw), + hide_docs_list=extract_hide_docs_list(metadata_raw), + hide_contributing_note=extract_hide_contributing_note(metadata_raw), + hide_navigation=extract_hide_navigation(metadata_raw), + ), + body, + ) diff --git a/tests/test_frontmatter.py b/tests/test_frontmatter.py new file mode 100644 index 0000000..040d432 --- /dev/null +++ b/tests/test_frontmatter.py @@ -0,0 +1,22 @@ +"""Frontmatter parsing smoke tests.""" + +from nmteam_support.frontmatter import parse_page, split_frontmatter, split_frontmatter_lines + + +def test_split_frontmatter_roundtrip(): + text = "---\ntitle: X\n---\n\n# Body\n" + metadata_raw, body = split_frontmatter(text) + assert "title: X" in metadata_raw + assert body == "\n\n# Body\n" + + +def test_split_frontmatter_without_fence(): + assert split_frontmatter("no frontmatter\n") == ("", "no frontmatter\n") + + +def test_parse_page_extracts_metadata(): + meta, body = parse_page("---\ntitle: T\ndescription: D\nindex: 5\n---\n\n# T\n", "x.md") + assert meta.title == "T" + assert meta.description == "D" + assert meta.index == 5 + assert body == "\n# T\n" From 1c81af6024c1b39d87b8d05844be6c5d52d6c87a Mon Sep 17 00:00:00 2001 From: Alice39s Date: Sun, 9 Aug 2026 15:51:26 +0900 Subject: [PATCH 04/57] feat(scanner): scan documentation trees --- src/nmteam_support/scanner.py | 97 +++++++++++++++++++++++++++++++++++ tests/test_scanner.py | 37 +++++++++++++ 2 files changed, 134 insertions(+) create mode 100644 src/nmteam_support/scanner.py create mode 100644 tests/test_scanner.py diff --git a/src/nmteam_support/scanner.py b/src/nmteam_support/scanner.py new file mode 100644 index 0000000..73f6cb8 --- /dev/null +++ b/src/nmteam_support/scanner.py @@ -0,0 +1,97 @@ +"""Recursive scanning of the docs directory tree.""" + +from __future__ import annotations + +import os +from dataclasses import dataclass, field +from pathlib import Path + +from nmteam_support.frontmatter import parse_page +from nmteam_support.models import DocEntry, PageMetadata + +# Directories copied verbatim into the output (never scanned or indexed). +SKIP_DIRS = frozenset({"img"}) + +# Internal directories that are never published to the generated site. +INTERNAL_DIRS = frozenset({"superpowers"}) + + +@dataclass +class ScannedDir: + """Everything the generator needs to know about one directory under docs/.""" + + rel_path: str # path relative to the docs root; "" for the root itself + has_index: bool + index_meta: PageMetadata # metadata from index.md (or name-derived defaults) + index_body: str # index.md body after frontmatter ("" when absent/empty) + docs: list[DocEntry] = field(default_factory=list) # non-index .md files + subdirs: list["ScannedDir"] = field(default_factory=list) + other_files: list[str] = field(default_factory=list) # non-.md files, relative paths + image_dirs: list[str] = field(default_factory=list) # dirs named "img", relative paths + + +def scan_docs(docs_dir: Path) -> ScannedDir: + """Scan ``docs_dir`` recursively and return the resulting tree.""" + return _scan(docs_dir, "") + + +def _scan(dir_path: Path, rel_path: str) -> ScannedDir: + docs: list[DocEntry] = [] + subdirs: list[ScannedDir] = [] + other_files: list[str] = [] + image_dirs: list[str] = [] + index_meta = PageMetadata(title=dir_path.name, description="") + index_body = "" + has_index = False + + for name in os.listdir(dir_path): # keep listdir order for stable ties + entry = dir_path / name + child_rel = f"{rel_path}/{name}" if rel_path else name + if entry.is_dir(): + if name in SKIP_DIRS: + image_dirs.append(child_rel) + elif name in INTERNAL_DIRS: + continue + else: + subdirs.append(_scan(entry, child_rel)) + elif name.endswith(".md"): + text = entry.read_text(encoding="utf-8", errors="ignore") + if not text: # empty files are skipped entirely + continue + meta, body = parse_page(text, name) + if name == "index.md": + has_index = True + index_meta = meta + index_body = _index_body(text, body) + else: + docs.append( + DocEntry( + title=meta.title, + description=meta.description, + path=child_rel, + name=name, + index=meta.index, + kind="doc", + hide_contributing_note=meta.hide_contributing_note, + ) + ) + else: + other_files.append(child_rel) + + return ScannedDir( + rel_path=rel_path, + has_index=has_index, + index_meta=index_meta, + index_body=index_body, + docs=docs, + subdirs=subdirs, + other_files=other_files, + image_dirs=image_dirs, + ) + + +def _index_body(text: str, body: str) -> str: + """index.md body: only kept when at least one non-empty line survives.""" + if not text.startswith("---"): + return text # whole file is the body + return body if any(line for line in body.split("\n")) else "" diff --git a/tests/test_scanner.py b/tests/test_scanner.py new file mode 100644 index 0000000..aa3004b --- /dev/null +++ b/tests/test_scanner.py @@ -0,0 +1,37 @@ +"""Directory scanning tests.""" + +from nmteam_support.scanner import scan_docs + + +def test_scan_collects_docs_and_folders(docs_dir): + root = scan_docs(docs_dir) + assert root.rel_path == "" + assert [d.name for d in root.docs] == ["about.md"] + assert {s.rel_path for s in root.subdirs} == {"nmbot-telegram", "contact-us"} + nmbot = next(s for s in root.subdirs if s.rel_path == "nmbot-telegram") + assert {d.name for d in nmbot.docs} == {"mcp.md"} + assert nmbot.index_meta.title == "nmBot Telegram" + assert nmbot.index_body.startswith("\n# nmBot") + + +def test_scan_skips_img_dirs(docs_dir): + img = docs_dir / "nmbot-telegram" / "img" + img.mkdir() + (img / "a.png").write_bytes(b"x") + root = scan_docs(docs_dir) + nmbot = next(s for s in root.subdirs if s.rel_path == "nmbot-telegram") + assert nmbot.image_dirs == ["nmbot-telegram/img"] + + +def test_scan_skips_superpowers(docs_dir): + internal = docs_dir / "superpowers" + internal.mkdir() + (internal / "plan.md").write_text("# 计划\n", encoding="utf-8") + root = scan_docs(docs_dir) + assert {s.rel_path for s in root.subdirs} == {"nmbot-telegram", "contact-us"} + + +def test_scan_skips_empty_md_files(docs_dir): + (docs_dir / "empty.md").write_text("", encoding="utf-8") + root = scan_docs(docs_dir) + assert "empty.md" not in [d.name for d in root.docs] From fd3fa22f2e2e8c683c4338c03b1c646ce6875986 Mon Sep 17 00:00:00 2001 From: Alice39s Date: Sun, 9 Aug 2026 15:52:06 +0900 Subject: [PATCH 05/57] feat(contributing): inject contribution notices --- src/nmteam_support/contributing.py | 42 ++++++++++++++++++++++++++++++ tests/test_contributing.py | 33 +++++++++++++++++++++++ 2 files changed, 75 insertions(+) create mode 100644 src/nmteam_support/contributing.py create mode 100644 tests/test_contributing.py diff --git a/src/nmteam_support/contributing.py b/src/nmteam_support/contributing.py new file mode 100644 index 0000000..c22a77d --- /dev/null +++ b/src/nmteam_support/contributing.py @@ -0,0 +1,42 @@ +"""Contributing-note injection for generated documents.""" + +from __future__ import annotations + +import re + +from nmteam_support.models import DocEntry + +GITHUB_EDIT_BASE = "https://github.com/nm-Team/Support/edit/main/docs/" + +# Path prefixes that never receive a contributing note. +EXCLUDED_PATH_PATTERNS = ( + r"^legal/", + r"/legal/", + r"/update-log/", +) + + +def render_contributing_note(doc_path: str) -> str: + """Return the '帮助我们改进此文档' admonition for ``doc_path`` (relative to docs/).""" + github_edit_url = f"{GITHUB_EDIT_BASE}{doc_path}" + return ( + "\n" + f'!!! tip "帮助我们改进此文档"\n' + " 发现文档有错误或需要改进的地方?您可以:\n" + "\n" + f" - [在 GitHub 上直接编辑此页面]({github_edit_url})\n" + " - [提交 Issue 报告问题](https://github.com/nm-Team/Support/issues/new)\n" + " - [加入我们的讨论](https://github.com/nm-Team/Support/discussions)\n" + "\n" + " 您的贡献将帮助更多用户获得更好的体验!\n" + "\n" + ) + + +def should_hide_contributing_note(entry: DocEntry) -> bool: + """True when a doc must be copied verbatim (no note injected).""" + if entry.hide_contributing_note: + return True + if entry.name == "index.md": + return True + return any(re.search(pattern, entry.path) for pattern in EXCLUDED_PATH_PATTERNS) diff --git a/tests/test_contributing.py b/tests/test_contributing.py new file mode 100644 index 0000000..8737e89 --- /dev/null +++ b/tests/test_contributing.py @@ -0,0 +1,33 @@ +"""Contributing note tests.""" + +from nmteam_support.contributing import render_contributing_note, should_hide_contributing_note +from nmteam_support.models import DocEntry + + +def _entry(path: str, hide: bool = False) -> DocEntry: + name = path.rsplit("/", 1)[-1] + return DocEntry(title="t", description="", path=path, name=name, hide_contributing_note=hide) + + +def test_render_contributing_note_contains_edit_url(): + note = render_contributing_note("nmbot-telegram/mcp.md") + assert "https://github.com/nm-Team/Support/edit/main/docs/nmbot-telegram/mcp.md" in note + assert '!!! tip "帮助我们改进此文档"' in note + + +def test_hide_when_flag_set(): + assert should_hide_contributing_note(_entry("a/b.md", hide=True)) + + +def test_hide_for_index_md(): + assert should_hide_contributing_note(_entry("contact-us/index.md")) + + +def test_hide_for_legal_and_update_log(): + assert should_hide_contributing_note(_entry("legal/terms.md")) + assert should_hide_contributing_note(_entry("nmbot-telegram/legal/terms.md")) + assert should_hide_contributing_note(_entry("nmbot-telegram/update-log/2026-01.md")) + + +def test_show_for_normal_doc(): + assert not should_hide_contributing_note(_entry("nmbot-telegram/mcp.md")) From 6f6f60e36dc0ab0021f251b57e3f787c3291576c Mon Sep 17 00:00:00 2001 From: Alice39s Date: Sun, 9 Aug 2026 15:52:38 +0900 Subject: [PATCH 06/57] feat(docslist): render documentation cards --- src/nmteam_support/docslist.py | 29 +++++++++++++++++++++++++++++ tests/test_docslist.py | 26 ++++++++++++++++++++++++++ 2 files changed, 55 insertions(+) create mode 100644 src/nmteam_support/docslist.py create mode 100644 tests/test_docslist.py diff --git a/src/nmteam_support/docslist.py b/src/nmteam_support/docslist.py new file mode 100644 index 0000000..ca6e480 --- /dev/null +++ b/src/nmteam_support/docslist.py @@ -0,0 +1,29 @@ +"""docsList HTML card generation for directory index pages.""" + +from __future__ import annotations + +import html + +from nmteam_support.models import DocEntry + + +def render_docs_list(entries: list[DocEntry]) -> str: + """Render the docsList card HTML for already-sorted ``entries``.""" + html_parts = ['\n\n\n
'] + for entry in entries: + html_parts.append( + '\n
\n' + ' \n' + '
\n' + ' {title}\n' + '

{description}

\n' + "
\n" + "
".format( + path=html.escape("/" + entry.path.replace(".md", "")), + title=html.escape(entry.title), + description=html.escape(entry.description), + type=entry.kind, + ) + ) + html_parts.append("\n
") + return "".join(html_parts) diff --git a/tests/test_docslist.py b/tests/test_docslist.py new file mode 100644 index 0000000..3b2ea83 --- /dev/null +++ b/tests/test_docslist.py @@ -0,0 +1,26 @@ +"""docsList HTML tests.""" + +from nmteam_support.docslist import render_docs_list +from nmteam_support.models import DocEntry + + +def test_render_docs_list_structure(): + entries = [DocEntry(title="A", description="d", path="a.md", name="a.md", kind="doc")] + out = render_docs_list(entries) + assert out.startswith('\n\n\n
') + assert out.endswith("\n
") + assert 'A' in out + assert '' in out + + +def test_render_docs_list_strips_md_and_escapes(): + entries = [DocEntry(title="A&B", description='say "hi"', path="n/b.md", name="b.md", kind="doc")] + out = render_docs_list(entries) + assert 'href="/n/b"' in out + assert "A&B" in out + assert "say "hi"" in out + + +def test_render_docs_list_folder_icon(): + entries = [DocEntry(title="F", description="", path="nm", name="nm", kind="folder")] + assert '' in render_docs_list(entries) From f91917a0b9aa5a1afdd47e1f5a48555494672994 Mon Sep 17 00:00:00 2001 From: Alice39s Date: Sun, 9 Aug 2026 15:53:03 +0900 Subject: [PATCH 07/57] feat(redirects): generate client-side redirect script --- src/nmteam_support/redirects.py | 38 +++++++++++++++++++++++++++++++++ tests/test_redirects.py | 27 +++++++++++++++++++++++ 2 files changed, 65 insertions(+) create mode 100644 src/nmteam_support/redirects.py create mode 100644 tests/test_redirects.py diff --git a/src/nmteam_support/redirects.py b/src/nmteam_support/redirects.py new file mode 100644 index 0000000..93c883b --- /dev/null +++ b/src/nmteam_support/redirects.py @@ -0,0 +1,38 @@ +"""Redirects.js generation from redirects.json.""" + +from __future__ import annotations + +import json +from pathlib import Path + + +def load_redirects(path: Path) -> dict[str, str] | None: + """Load the redirect map; return None when the file is missing.""" + if not path.exists(): + return None + with path.open(encoding="utf-8") as f: + return json.load(f)["redirects"] + + +def render_redirects_js(redirects: dict[str, str]) -> str: + """Render the redirects.js content for a redirect map.""" + return ( + "// 自动生成的重定向脚本\n" + "(function() {\n" + " // 重定向映射\n" + " const redirects = " + json.dumps(redirects, ensure_ascii=False, indent=8) + ";\n" + "\n" + " // 获取当前路径\n" + " const currentPath = window.location.pathname;\n" + " \n" + " // 检查是否需要重定向\n" + " for (const oldPath in redirects) {\n" + " if (currentPath === oldPath || currentPath.startsWith(oldPath)) {\n" + " const newPath = redirects[oldPath];\n" + " // 执行重定向\n" + " window.location.replace(newPath);\n" + " break;\n" + " }\n" + " }\n" + "})();" + ) diff --git a/tests/test_redirects.py b/tests/test_redirects.py new file mode 100644 index 0000000..b9989ea --- /dev/null +++ b/tests/test_redirects.py @@ -0,0 +1,27 @@ +"""Redirects generation tests.""" + +import json + +from nmteam_support.redirects import load_redirects, render_redirects_js + + +def test_render_redirects_js_contains_mapping(): + js = render_redirects_js({"/old/": "/new/"}) + assert "const redirects = {" in js + assert '"/old/": "/new/"' in js + assert "window.location.replace(newPath);" in js + + +def test_render_redirects_js_keeps_unicode(): + js = render_redirects_js({"/旧/": "/新/"}) + assert "/旧/" in js + + +def test_load_redirects_missing_file(tmp_path): + assert load_redirects(tmp_path / "nope.json") is None + + +def test_load_redirects_reads_map(tmp_path): + p = tmp_path / "redirects.json" + p.write_text(json.dumps({"redirects": {"/a/": "/b/"}}), encoding="utf-8") + assert load_redirects(p) == {"/a/": "/b/"} From 069a0f2167caec1089b4add9b9f3a0b43aef98a7 Mon Sep 17 00:00:00 2001 From: Alice39s Date: Sun, 9 Aug 2026 15:56:16 +0900 Subject: [PATCH 08/57] feat(nav): generate ordered navigation --- src/nmteam_support/nav.py | 55 +++++++++++++++++++++++++++++++++++++++ tests/test_nav.py | 48 ++++++++++++++++++++++++++++++++++ 2 files changed, 103 insertions(+) create mode 100644 src/nmteam_support/nav.py create mode 100644 tests/test_nav.py diff --git a/src/nmteam_support/nav.py b/src/nmteam_support/nav.py new file mode 100644 index 0000000..9dfa4ba --- /dev/null +++ b/src/nmteam_support/nav.py @@ -0,0 +1,55 @@ +"""MkDocs nav YAML generation from a scanned docs tree.""" + +from __future__ import annotations + +from nmteam_support.models import DocEntry +from nmteam_support.scanner import ScannedDir + + +def sort_entries(entries: list[DocEntry]) -> list[DocEntry]: + """Order entries: index ascending, folders before docs, then scan order (stable).""" + return sorted(entries, key=lambda e: (e.index, 0 if e.kind == "folder" else 1)) + + +def folder_entries(scan: ScannedDir) -> list[DocEntry]: + """Folder entries of ``scan`` that are renderable (have a non-empty nav block).""" + return [ + DocEntry( + title=sub.index_meta.title, + description=sub.index_meta.description, + path=sub.rel_path, + name=sub.rel_path.rsplit("/", 1)[-1], + index=sub.index_meta.index, + kind="folder", + ) + for sub in scan.subdirs + if is_renderable(sub) + ] + + +def is_renderable(scan: ScannedDir) -> bool: + """A directory produces output when it has an index body, docs, or renderable subdirs.""" + return bool(scan.index_body or scan.docs or any(is_renderable(sub) for sub in scan.subdirs)) + + +def build_nav_yaml(root: ScannedDir) -> str: + """Render the nav block (without the ``nav:`` key) for the whole tree.""" + return _render(root, 0) + + +def _render(scan: ScannedDir, depth: int) -> str: + indent = " " * (depth + 1) + lines: list[str] = [] + if scan.index_body: + link = "" if scan.rel_path == "" else scan.rel_path + "/" + lines.append(f"{indent}- {scan.index_meta.title}: '{link}index.md'") + for entry in sort_entries(scan.docs + folder_entries(scan)): + if entry.kind == "folder": + sub = next(s for s in scan.subdirs if s.rel_path == entry.path) + sub_block = _render(sub, depth + 1) + if sub_block: # empty folders are skipped entirely + lines.append(f"{indent}- {entry.title}:") + lines.append(sub_block) + else: + lines.append(f"{indent}- {entry.title}: '{entry.path}'") + return "\n".join(lines) diff --git a/tests/test_nav.py b/tests/test_nav.py new file mode 100644 index 0000000..1877e35 --- /dev/null +++ b/tests/test_nav.py @@ -0,0 +1,48 @@ +"""Nav YAML generation tests.""" + +from nmteam_support.models import DocEntry +from nmteam_support.nav import build_nav_yaml, sort_entries +from nmteam_support.scanner import scan_docs + + +def _entry(path, name, index=0, kind="doc"): + return DocEntry(title=name, description="", path=path, name=name, index=index, kind=kind) + + +def test_sort_index_ascending_then_folders_first_then_scan_order(): + entries = [ + _entry("doc2.md", "doc2", index=0), + _entry("folder1", "folder1", index=0, kind="folder"), + _entry("doc1.md", "doc1", index=-1), + _entry("folder2", "folder2", index=5, kind="folder"), + _entry("doc0.md", "doc0", index=0), + ] + assert [e.name for e in sort_entries(entries)] == ["doc1", "folder1", "doc2", "doc0", "folder2"] + + +def test_build_nav_yaml_shape(docs_dir): + nav = build_nav_yaml(scan_docs(docs_dir)) + assert nav == ( + " - nmTeam 支持: 'index.md'\n" + " - nmBot Telegram:\n" + " - nmBot Telegram: 'nmbot-telegram/index.md'\n" + " - MCP 配置: 'nmbot-telegram/mcp.md'\n" + " - 联系我们:\n" + " - 联系我们: 'contact-us/index.md'\n" + " - 论坛: 'contact-us/forum.md'\n" + " - 关于: 'about.md'" + ) + + +def test_empty_folder_skipped(docs_dir): + (docs_dir / "nmbot-telegram" / "empty-dir").mkdir() + nav = build_nav_yaml(scan_docs(docs_dir)) + assert "empty-dir" not in nav + + +def test_folder_with_empty_index_skipped(docs_dir): + d = docs_dir / "nmbot-telegram" / "notes" + d.mkdir() + (d / "index.md").write_text("---\n---\n", encoding="utf-8") + nav = build_nav_yaml(scan_docs(docs_dir)) + assert "notes" not in nav From 1799db6039124d58f276752437330eafb95482d5 Mon Sep 17 00:00:00 2001 From: Alice39s Date: Sun, 9 Aug 2026 15:56:56 +0900 Subject: [PATCH 09/57] feat(index): render directory index pages --- src/nmteam_support/index.py | 34 ++++++++++++++++++++++++++++++ tests/test_index.py | 41 +++++++++++++++++++++++++++++++++++++ 2 files changed, 75 insertions(+) create mode 100644 src/nmteam_support/index.py create mode 100644 tests/test_index.py diff --git a/src/nmteam_support/index.py b/src/nmteam_support/index.py new file mode 100644 index 0000000..003f170 --- /dev/null +++ b/src/nmteam_support/index.py @@ -0,0 +1,34 @@ +"""Directory index.md page generation.""" + +from __future__ import annotations + +from nmteam_support.docslist import render_docs_list +from nmteam_support.nav import folder_entries, sort_entries +from nmteam_support.scanner import ScannedDir + + +def render_index_page(scan: ScannedDir) -> str: + """Render the complete index.md content (frontmatter + body + docsList) for a directory.""" + head_lines = [ + "---", + "automatically_generated: Don't edit this file directly, it's auto generated.", + f"title: {scan.index_meta.title}", + "", + "hide:", + " - toc", + ] + if scan.index_meta.hide_navigation: + head_lines.append(" - navigation") + head_lines.append("---") + head_lines.append("") + head = "\n".join(head_lines) + "\n" + + if scan.index_body: + content = scan.index_body + else: + content = f"# {scan.index_meta.title}\n{scan.index_meta.description}\n" + + if not scan.index_meta.hide_docs_list: + entries = sort_entries(scan.docs + folder_entries(scan)) + content += render_docs_list(entries) + return head + content diff --git a/tests/test_index.py b/tests/test_index.py new file mode 100644 index 0000000..8eecc2e --- /dev/null +++ b/tests/test_index.py @@ -0,0 +1,41 @@ +"""Index page generation tests.""" + +from nmteam_support.index import render_index_page +from nmteam_support.scanner import scan_docs + + +def test_index_page_frontmatter_and_no_docs_list(docs_dir): + root = scan_docs(docs_dir) + page = render_index_page(root) + assert page.startswith( + "---\nautomatically_generated: Don't edit this file directly, it's auto generated.\n" + "title: nmTeam 支持\n\nhide:\n - toc\n---" + ) + assert "正文。" in page + # root index.md sets hide_docs_list: true -> no card list + assert '
' not in page + + +def test_index_page_docs_list_for_subdir(docs_dir): + root = scan_docs(docs_dir) + nmbot = next(s for s in root.subdirs if s.rel_path == "nmbot-telegram") + page = render_index_page(nmbot) + assert '
' in page + assert 'href="/nmbot-telegram/mcp"' in page + + +def test_index_page_navigation_hide(docs_dir): + (docs_dir / "nmbot-telegram" / "index.md").write_text( + "---\ntitle: nmBot\nhide:\n - navigation\n---\n\n# nmBot\n", encoding="utf-8" + ) + root = scan_docs(docs_dir) + nmbot = next(s for s in root.subdirs if s.rel_path == "nmbot-telegram") + assert " - navigation" in render_index_page(nmbot) + + +def test_index_page_default_content_without_index_md(tmp_path): + d = tmp_path / "docs2" + d.mkdir() + page = render_index_page(scan_docs(d)) + assert "# docs2\n" in page + assert '
' in page # empty list div is still emitted From cb5af877b56bfa76f23c154939e5c56000465c3e Mon Sep 17 00:00:00 2001 From: Alice39s Date: Sun, 9 Aug 2026 15:58:25 +0900 Subject: [PATCH 10/57] feat(generator): orchestrate documentation generation --- src/nmteam_support/__main__.py | 6 ++ src/nmteam_support/generator.py | 116 ++++++++++++++++++++++++++++++++ src/nmteam_support/template.py | 16 +++++ tests/test_generator.py | 96 ++++++++++++++++++++++++++ 4 files changed, 234 insertions(+) create mode 100644 src/nmteam_support/__main__.py create mode 100644 src/nmteam_support/generator.py create mode 100644 src/nmteam_support/template.py create mode 100644 tests/test_generator.py diff --git a/src/nmteam_support/__main__.py b/src/nmteam_support/__main__.py new file mode 100644 index 0000000..cc83b5d --- /dev/null +++ b/src/nmteam_support/__main__.py @@ -0,0 +1,6 @@ +"""Entry point: ``uv run python -m nmteam_support`` regenerates the site.""" + +from nmteam_support.generator import default_options, generate + +if __name__ == "__main__": + generate(default_options()) diff --git a/src/nmteam_support/generator.py b/src/nmteam_support/generator.py new file mode 100644 index 0000000..a81ce2f --- /dev/null +++ b/src/nmteam_support/generator.py @@ -0,0 +1,116 @@ +"""End-to-end documentation generation orchestration.""" + +from __future__ import annotations + +import shutil +from dataclasses import dataclass +from pathlib import Path + +from nmteam_support.contributing import render_contributing_note, should_hide_contributing_note +from nmteam_support.frontmatter import split_frontmatter +from nmteam_support.index import render_index_page +from nmteam_support.nav import build_nav_yaml +from nmteam_support.redirects import load_redirects, render_redirects_js +from nmteam_support.scanner import ScannedDir, scan_docs +from nmteam_support.template import render_mkdocs_yml + + +@dataclass(frozen=True) +class GeneratorOptions: + """All paths the generator touches; tests inject tmp_path-based values.""" + + docs_dir: Path + template_path: Path + redirects_path: Path + cache_dir: Path + generated_dir: Path + mkdocs_yml_path: Path + + +def default_options(root: Path | None = None) -> GeneratorOptions: + root = root or Path.cwd() + return GeneratorOptions( + docs_dir=root / "docs", + template_path=root / "mkdocs-template.yml", + redirects_path=root / "redirects.json", + cache_dir=root / "cache", + generated_dir=root / "generated", + mkdocs_yml_path=root / "mkdocs.yml", + ) + + +def generate(options: GeneratorOptions) -> None: + """Regenerate ``cache/``, ``generated/`` and ``mkdocs.yml`` from ``docs/``.""" + print("Generating documentation...") + cache_dir = options.cache_dir + if cache_dir.exists(): + shutil.rmtree(cache_dir) + cache_dir.mkdir() + + root = scan_docs(options.docs_dir) + + _write_tree(options.docs_dir, cache_dir, root) + _write_indexes(cache_dir, root) + + nav_yaml = build_nav_yaml(root) + template = options.template_path.read_text(encoding="UTF-8", errors="ignore") + options.mkdocs_yml_path.write_text(render_mkdocs_yml(template, nav_yaml), encoding="UTF-8") + + redirects = load_redirects(options.redirects_path) + if redirects is None: + print("Warning: redirects.json not found. Skipping redirects generation.") + else: + assets_js = cache_dir / "assets" / "js" + assets_js.mkdir(parents=True, exist_ok=True) + (assets_js / "redirects.js").write_text(render_redirects_js(redirects), encoding="utf-8") + print("Redirects script generated.") + + if options.generated_dir.exists(): + shutil.rmtree(options.generated_dir) + shutil.copytree(cache_dir, options.generated_dir) + print("Documentation generated.") + + +def _write_tree(docs_dir: Path, cache_dir: Path, scan: ScannedDir) -> None: + for doc in scan.docs: + source = docs_dir / doc.path + target = cache_dir / doc.path + target.parent.mkdir(parents=True, exist_ok=True) + if should_hide_contributing_note(doc): + shutil.copyfile(source, target) + else: + text = source.read_text(encoding="UTF-8", errors="ignore") + target.write_text(render_doc_file(text, doc.path), encoding="UTF-8") + for rel in scan.other_files: + source = docs_dir / rel + target = cache_dir / rel + target.parent.mkdir(parents=True, exist_ok=True) + shutil.copyfile(source, target) + for rel in scan.image_dirs: + target = cache_dir / rel + target.parent.mkdir(parents=True, exist_ok=True) + shutil.copytree(docs_dir / rel, target) + for sub in scan.subdirs: + _write_tree(docs_dir, cache_dir, sub) + + +def render_doc_file(text: str, doc_path: str) -> str: + """Return ``text`` with the contributing note injected after the first heading.""" + metadata_raw, body = split_frontmatter(text) + note = render_contributing_note(doc_path) + lines = body.split("\n") + for i, line in enumerate(lines): + if line.strip().startswith("#"): + lines.insert(i + 1, note) + break + else: + return (f"---{metadata_raw}---" if metadata_raw else "") + note + body + return (f"---{metadata_raw}---" if metadata_raw else "") + "\n".join(lines) + + +def _write_indexes(cache_dir: Path, scan: ScannedDir) -> None: + target = cache_dir / scan.rel_path / "index.md" + target.parent.mkdir(parents=True, exist_ok=True) + target.write_text(render_index_page(scan), encoding="UTF-8") + for sub in scan.subdirs: + _write_indexes(cache_dir, sub) diff --git a/src/nmteam_support/template.py b/src/nmteam_support/template.py new file mode 100644 index 0000000..7a049d7 --- /dev/null +++ b/src/nmteam_support/template.py @@ -0,0 +1,16 @@ +"""mkdocs-template.yml nav injection.""" + +from __future__ import annotations + +import re + +NAV_START = "# NAV_ARIA_START" +NAV_END = "# NAV_ARIA_END" + +_NAV_PATTERN = re.compile(r"# NAV_ARIA_START.*# NAV_ARIA_END", re.S) + + +def render_mkdocs_yml(template: str, nav_yaml: str) -> str: + """Replace the nav placeholder block with ``nav_yaml``.""" + block = f"{NAV_START}\n{nav_yaml}\n{NAV_END}" if nav_yaml else f"{NAV_START}\n{NAV_END}" + return _NAV_PATTERN.sub(block, template) diff --git a/tests/test_generator.py b/tests/test_generator.py new file mode 100644 index 0000000..94f9017 --- /dev/null +++ b/tests/test_generator.py @@ -0,0 +1,96 @@ +"""End-to-end generator tests.""" + +from pathlib import Path + +import pytest + +from nmteam_support.generator import GeneratorOptions, generate, render_doc_file + + +def _full_options(tmp_path: Path, docs_dir: Path, redirects: str | None = None) -> GeneratorOptions: + (tmp_path / "mkdocs-template.yml").write_text( + "site_name: Test\nnav:\n# NAV_ARIA_START\n# NAV_ARIA_END\n", encoding="utf-8" + ) + if redirects is not None: + (tmp_path / "redirects.json").write_text(redirects, encoding="utf-8") + return GeneratorOptions( + docs_dir=docs_dir, + template_path=tmp_path / "mkdocs-template.yml", + redirects_path=tmp_path / "redirects.json", + cache_dir=tmp_path / "cache", + generated_dir=tmp_path / "generated", + mkdocs_yml_path=tmp_path / "mkdocs.yml", + ) + + +def test_generate_writes_all_outputs(tmp_path, docs_dir): + options = _full_options(tmp_path, docs_dir, redirects='{"redirects": {"/old/": "/new/"}}') + generate(options) + assert (options.generated_dir / "index.md").exists() + assert (options.generated_dir / "nmbot-telegram" / "mcp.md").exists() + mkdocs_yml = options.mkdocs_yml_path.read_text(encoding="utf-8") + assert "NAV_ARIA_START" in mkdocs_yml + assert "nmbot-telegram/mcp.md" in mkdocs_yml + + +def test_generate_injects_contributing_note(tmp_path, docs_dir): + options = _full_options(tmp_path, docs_dir) + generate(options) + content = (options.generated_dir / "nmbot-telegram" / "mcp.md").read_text(encoding="utf-8") + assert "帮助我们改进此文档" in content + + +def test_generate_skips_contributing_note_for_update_log(tmp_path, docs_dir): + d = docs_dir / "nmbot-telegram" / "update-log" + d.mkdir() + (d / "2026-01.md").write_text("---\nindex: -2601\n---\n\n# 2026-01\n\n内容。\n", encoding="utf-8") + options = _full_options(tmp_path, docs_dir) + generate(options) + content = (options.generated_dir / "nmbot-telegram" / "update-log" / "2026-01.md").read_text( + encoding="utf-8" + ) + assert "帮助我们改进此文档" not in content + + +def test_generate_writes_redirects_before_copy(tmp_path, docs_dir): + """Regression: redirects.js must land in generated/, not only in cache/.""" + options = _full_options(tmp_path, docs_dir, redirects='{"redirects": {"/a/": "/b/"}}') + generate(options) + js = options.generated_dir / "assets" / "js" / "redirects.js" + assert js.exists() + assert "/a/" in js.read_text(encoding="utf-8") + + +def test_generate_skips_superpowers_in_output(tmp_path, docs_dir): + internal = docs_dir / "superpowers" + internal.mkdir() + (internal / "plan.md").write_text("# 计划\n", encoding="utf-8") + options = _full_options(tmp_path, docs_dir) + generate(options) + assert not (options.generated_dir / "superpowers").exists() + + +def test_render_doc_file_no_heading(): + out = render_doc_file("---\ntitle: X\n---\n\n正文。\n", "x.md") + assert "帮助我们改进此文档" in out + assert out.startswith("---\ntitle: X\n---") + + +def test_generate_real_docs_tree(tmp_path): + """Generate from the repository's actual docs/ tree into a temp output (must not hang).""" + repo_root = Path(__file__).resolve().parents[1] + docs = repo_root / "docs" + if not docs.is_dir(): + pytest.skip("docs/ not present") + options = GeneratorOptions( + docs_dir=docs, + template_path=repo_root / "mkdocs-template.yml", + redirects_path=repo_root / "redirects.json", + cache_dir=tmp_path / "cache", + generated_dir=tmp_path / "generated", + mkdocs_yml_path=tmp_path / "mkdocs.yml", + ) + generate(options) + assert list(options.generated_dir.rglob("index.md")) + assert (options.generated_dir / "assets" / "js" / "redirects.js").exists() + assert not (options.generated_dir / "superpowers").exists() From c04c40dd251c8aa636766ee78aa2652bce97a222 Mon Sep 17 00:00:00 2001 From: Alice39s Date: Sun, 9 Aug 2026 16:12:26 +0900 Subject: [PATCH 11/57] fix(redirects): tolerate invalid redirect configuration --- src/nmteam_support/redirects.py | 9 +++++---- tests/test_redirects.py | 12 ++++++++++++ 2 files changed, 17 insertions(+), 4 deletions(-) diff --git a/src/nmteam_support/redirects.py b/src/nmteam_support/redirects.py index 93c883b..c34acca 100644 --- a/src/nmteam_support/redirects.py +++ b/src/nmteam_support/redirects.py @@ -7,11 +7,12 @@ def load_redirects(path: Path) -> dict[str, str] | None: - """Load the redirect map; return None when the file is missing.""" - if not path.exists(): + """Load the redirect map; return None when the file is missing or invalid.""" + try: + with path.open(encoding="utf-8") as f: + return json.load(f)["redirects"] + except (json.JSONDecodeError, KeyError, OSError): return None - with path.open(encoding="utf-8") as f: - return json.load(f)["redirects"] def render_redirects_js(redirects: dict[str, str]) -> str: diff --git a/tests/test_redirects.py b/tests/test_redirects.py index b9989ea..5116cff 100644 --- a/tests/test_redirects.py +++ b/tests/test_redirects.py @@ -25,3 +25,15 @@ def test_load_redirects_reads_map(tmp_path): p = tmp_path / "redirects.json" p.write_text(json.dumps({"redirects": {"/a/": "/b/"}}), encoding="utf-8") assert load_redirects(p) == {"/a/": "/b/"} + + +def test_load_redirects_invalid_json(tmp_path): + p = tmp_path / "redirects.json" + p.write_text("{ not valid json", encoding="utf-8") + assert load_redirects(p) is None + + +def test_load_redirects_missing_key(tmp_path): + p = tmp_path / "redirects.json" + p.write_text(json.dumps({"foo": 1}), encoding="utf-8") + assert load_redirects(p) is None From 0dcab6ce4a5dbce6032883c7e2b2b7257c8a5047 Mon Sep 17 00:00:00 2001 From: Alice39s Date: Sun, 9 Aug 2026 16:55:30 +0900 Subject: [PATCH 12/57] fix(frontmatter): skip indented description content --- src/nmteam_support/frontmatter.py | 2 ++ tests/test_frontmatter.py | 49 +++++++++++++++++++++++++++++++ tests/test_nav.py | 21 +++++++++++++ 3 files changed, 72 insertions(+) diff --git a/src/nmteam_support/frontmatter.py b/src/nmteam_support/frontmatter.py index 08c48bc..d032985 100644 --- a/src/nmteam_support/frontmatter.py +++ b/src/nmteam_support/frontmatter.py @@ -67,6 +67,8 @@ def extract_description(metadata_raw: str, body: str) -> str: if match: return match.group(1).strip() for line in body.split("\n"): + if line[:1] in (" ", "\t"): + continue stripped = line.strip() if not stripped: continue diff --git a/tests/test_frontmatter.py b/tests/test_frontmatter.py index 040d432..ba62d1c 100644 --- a/tests/test_frontmatter.py +++ b/tests/test_frontmatter.py @@ -20,3 +20,52 @@ def test_parse_page_extracts_metadata(): assert meta.description == "D" assert meta.index == 5 assert body == "\n# T\n" + + +def test_unterminated_frontmatter_does_not_crash(): + meta, body = parse_page("---\ntitle: X\n", "x.md") + assert meta.title == "X" + assert body == "---\ntitle: X\n" + + +def test_title_falls_back_to_heading(): + meta, _ = parse_page("---\ndescription: D\n---\n\n# 标题\n\n正文\n", "x.md") + assert meta.title == "标题" + + +def test_title_falls_back_to_filename(): + meta, _ = parse_page("", "my-doc.md") + assert meta.title == "My Doc" + + +def test_index_garbage_defaults_to_zero(): + meta, _ = parse_page("---\nindex: nope\n---\n\n# T\n", "x.md") + assert meta.index == 0 + + +def test_description_skips_images_html_and_admonitions(): + meta, _ = parse_page( + "---\n---\n\n# T\n\n![图](./img/a.png)\n\n
HTML
\n\n!!! note \"注\"\n 内容\n\n正文第一句。\n", + "x.md", + ) + assert meta.description == "正文第一句。" + + +def test_description_truncates_at_100(): + meta, _ = parse_page("---\n---\n\n# T\n\n" + "字" * 120 + "\n", "x.md") + assert meta.description == "字" * 100 + "..." + + +def test_hide_flags(): + meta, _ = parse_page( + "---\nhide_docs_list: true\nhideContributingNote: true\nhide:\n - navigation\n---\n\n# T\n", + "x.md", + ) + assert meta.hide_docs_list + assert meta.hide_contributing_note + assert meta.hide_navigation + + +def test_hide_contributing_note_is_case_insensitive(): + meta, _ = parse_page("---\nhidecontributingnote: true\n---\n\n# T\n", "x.md") + assert meta.hide_contributing_note diff --git a/tests/test_nav.py b/tests/test_nav.py index 1877e35..07f38aa 100644 --- a/tests/test_nav.py +++ b/tests/test_nav.py @@ -46,3 +46,24 @@ def test_folder_with_empty_index_skipped(docs_dir): (d / "index.md").write_text("---\n---\n", encoding="utf-8") nav = build_nav_yaml(scan_docs(docs_dir)) assert "notes" not in nav + + +def test_folder_index_comes_from_its_index_md(docs_dir): + # contact-us/index.md has index: 100 -> folder sorts after nmbot-telegram (0) + nav = build_nav_yaml(scan_docs(docs_dir)) + assert nav.index("nmbot-telegram") < nav.index("contact-us") + + +def test_negative_index_doc_precedes_default_index_doc(docs_dir): + # forum.md has index: -1 -> sorts before a same-level default-index (0) doc, + # while the folder's index.md overview still renders first. + (docs_dir / "contact-us" / "support.md").write_text( + "---\ntitle: 支持文档\nindex: 0\n---\n\n# 支持文档\n", + encoding="utf-8", + ) + nav = build_nav_yaml(scan_docs(docs_dir)) + assert ( + nav.index("'contact-us/index.md'") + < nav.index("'contact-us/forum.md'") + < nav.index("'contact-us/support.md'") + ) From 9737417e861ce7c969e711999ea9e2482602f3d9 Mon Sep 17 00:00:00 2001 From: Alice39s Date: Sun, 9 Aug 2026 17:03:49 +0900 Subject: [PATCH 13/57] refactor(cli): centralize development and build commands --- generate.py | 432 -------------------------------------- manage.py | 244 +-------------------- pyproject.toml | 3 + src/nmteam_support/cli.py | 98 +++++++++ tests/test_cli.py | 28 +++ 5 files changed, 137 insertions(+), 668 deletions(-) delete mode 100644 generate.py mode change 100644 => 100755 manage.py create mode 100644 src/nmteam_support/cli.py create mode 100644 tests/test_cli.py diff --git a/generate.py b/generate.py deleted file mode 100644 index 236631b..0000000 --- a/generate.py +++ /dev/null @@ -1,432 +0,0 @@ -# Generate nmTeam documentation. -# This script should be run before building the documentation. - -import html -import json -import os -import re -import shutil - - -def generate(): - # Generate the documentation. - print("Generating documentation...") - - # Create cache folder. If exists delete it first - if os.path.exists('cache'): - shutil.rmtree('cache') - os.mkdir('cache') - - yaml = "" - - # Read all the folders/files in /docs - yaml += read('docs')[0] - - # Update mkdocs.yml, replacing content between "# NAV_ARIA_START" and "# NAV_ARIA_END" - with open('mkdocs-template.yml', encoding='UTF-8', errors='ignore') as f: - mkdocs = f.read() - mkdocs = re.sub(r"# NAV_ARIA_START.*# NAV_ARIA_END", - f"# NAV_ARIA_START\n{yaml}# NAV_ARIA_END", mkdocs, flags=re.S) - # Write the mkdocs.yml in cache - with open('mkdocs.yml', 'w', encoding='UTF-8', errors='ignore') as f: - f.write(mkdocs) - - # Copy cache to generated - if os.path.exists('generated'): - shutil.rmtree('generated') - - shutil.copytree('cache', 'generated') - - # Generate redirects script - generate_redirects() - - print("Documentation generated. ") - - -def read(path): - path_relative = path.replace('docs/', '').replace('docs', '') - yaml = "" - index_dir_list = [] - docs = os.listdir(path) - hide_docs_list = False # Initialize here to avoid UnboundLocalError - - # Create path folder in cache - if not os.path.exists(f"cache/{path_relative}"): - os.makedirs(f"cache/{path_relative}") - - for doc in docs: - # If the file is a markdown file - if doc.endswith(".md"): - # If is a normal markdown file - # Try to read markdown metadata - metadata = "" - with open(f"{path}/{doc}", 'r', encoding='UTF-8', errors='ignore') as f: - lines = f.readlines() - # If the file is empty, skip - if len(lines) == 0: - continue - # If the file is not empty, read the metadata - if lines[0].startswith("---"): - # If the file has metadata - i = 1 - while not lines[i].startswith("---"): - i += 1 - metadata = "".join(lines[1:i]) - else: - # If the file has no metadata - metadata = "" - - # Deal with data - # Title - if metadata and re.search(r"title: (.*)", metadata): - title = re.search(r"title: (.*)", metadata).group(1) - elif re.search(r"# (.*)", "".join(lines)): - title = re.search(r"# (.*)", "".join(lines)).group(1) - else: - title = doc.replace(".md", "").replace("-", " ").title() - - # Description - if metadata and re.search(r"description: (.*)", metadata): - description = re.search( - r"description: (.*)", metadata).group(1) - elif len(lines) > len(metadata) + 1: - # else get 100 first characters after metadata - description_lines = lines[len(metadata):] - # Remove lines starting with # - description_lines = [ - line for line in description_lines if not line.startswith("#")] - description = "".join(description_lines) - # Remove special characters: []()#`>*_| - description = re.sub(r"[\[\]\(\)#`>*_]|\|", "", description) - i = 0 - while len(description) < 100 and len(metadata) < len(lines): - description += lines[len(metadata)] - i += 1 - description = description[:100]+'...' - else: - description = "" - - # Index - if metadata and re.search(r"index: (.*)", metadata): - index = int(re.search(r"index: (.*)", metadata).group(1)) - else: - index = 0 - - # For index.md - hide_docs_list = False - if doc == "index.md": - hide_docs_list = re.search(r"hide_docs_list: (.*)", metadata) and re.search( - r"hide_docs_list: (.*)", metadata).group(1) == 'true' - - # Doc path, removing "docs/" - doc_path = re.search(r"docs/(.*)", f"{path}/{doc}").group(1) - - # Add to index list - index_dir_list.append({"title": title, - "description": description, - "path": doc_path, - "metadata": metadata, - "doc": doc, - "index": index, - "type": "doc", - }) - - # Copy the file to cache if is not index.md - if doc != "index.md": - # Check if we should add contributing note - if not should_hide_contributing_note(metadata, doc_path, doc): - # Read original file content - with open(f"{path}/{doc}", 'r', encoding='UTF-8', errors='ignore') as f: - original_content = f.read() - - # Split content into metadata and body - if original_content.startswith("---"): - parts = original_content.split("---", 2) - if len(parts) >= 3: - metadata_part = f"---{parts[1]}---" - body_part = parts[2] - else: - metadata_part = "" - body_part = original_content - else: - metadata_part = "" - body_part = original_content - - # Find the first heading and insert contributing note after it - lines = body_part.split('\n') - heading_found = False - insert_index = 0 - - for i, line in enumerate(lines): - if line.strip().startswith('#'): - heading_found = True - insert_index = i + 1 - break - - if heading_found: - # Insert contributing note after the heading - contributing_note = generate_contributing_note( - doc_path) - lines.insert(insert_index, contributing_note) - modified_body = '\n'.join(lines) - else: - # If no heading found, insert at the beginning - contributing_note = generate_contributing_note( - doc_path) - modified_body = contributing_note + body_part - - # Write modified content to cache - modified_content = metadata_part + modified_body - with open(f"cache/{path_relative}/{doc}", 'w', encoding='UTF-8', errors='ignore') as f: - f.write(modified_content) - else: - # Just copy the file as is - shutil.copyfile(f"{path}/{doc}", - f"cache/{path_relative}/{doc}") - - # If the file is a folder - elif os.path.isdir(f"{path}/{doc}"): - # If is the assets folder, skip - if doc == "img": - # Copy the folder to cache - shutil.copytree(f"{path}/{doc}", - f"cache/{path_relative}/{doc}") - continue - # If is a normal folder - sub = read(f"{path}/{doc}") - sub_folder = sub[0] - sub_index_title = sub[1] - sub_index_description = sub[2] - sub_parent_index = sub[3] - if sub_folder: - # add yaml tab - space_count = len(path.split("/")) - space = "" - for i in range(space_count): - space += " " - sub_folder = space + sub_folder.replace("\n", f"\n{space}") - # remove the last space - sub_folder = sub_folder[:-len(space)] - sub_yaml = f" - {sub_index_title}:\n{sub_folder}" - # Read the - # Add to index list - index_dir_list.append({"title": sub_index_title, - "description": sub_index_description, - "path": f"{path}/{doc}", - "metadata": "", - "doc": doc, - "index": sub_parent_index, - "type": "folder", - "yaml": sub_yaml}) - - else: - # Copy the file to cache - shutil.copyfile(f"{path}/{doc}", - f"cache/{path_relative}/{doc}") - - # Sort index_dir_list, folders first - index_dir_list.sort(key=lambda x: x["type"], reverse=True) - # Sort index_dir_list by index, keep original order if index is the same - index_dir_list_index_sorted = sorted( - index_dir_list, key=lambda x: (x["index"], index_dir_list.index(x))) - # print(f"index_dir_list: {index_dir_list_index_sorted}") - - index_dir_list = index_dir_list_index_sorted - - # Generate yaml - for i in index_dir_list: - if i["type"] == 'folder': - yaml += i["yaml"] - continue - elif i["doc"] == "index.md": - continue - yaml += f" - {i['title']}: '{i['path']}'\n" - - # Generate index.md - # Get index title from index.md if exists - index_title = path.split("/")[-1] - index_description = "" - for i in index_dir_list: - if i["doc"] == "index.md": - index_title = i["title"] - index_description = i["description"] - break - - # Generate new index.md content - index_content_default = f"# {index_title}\n\ -{index_description}\n" - index_content = "" - - # If has index.md, copy its content - hide_navigation = False - parent_index = 0 - if os.path.exists(f"{path}/index.md"): - with open(f"{path}/index.md", 'r', encoding='UTF-8', errors='ignore') as f: - all = f.read() - - # Only copy content after metadata - if all.startswith("---"): - i = 1 - lines = all.split("\n") - while not lines[i].startswith("---"): - i += 1 - # Only if has lines not empty - for j in range(i+1, len(lines)): - if len(lines[j]) > 0: - index_content += "\n".join(lines[i+1:]) - break - metadata = "".join(lines[1:i]) - - # If has navigation hide - if re.search(r"- navigation", metadata): - hide_navigation = True - - # If has parent index - if re.search(r"index: (.*)", metadata): - parent_index = int(re.search( - r"index: (.*)", metadata).group(1)) - - else: - index_content = all - - index_metadata = f"---\n\ -automatically_generated: Don't edit this file directly, it's auto generated.\n\ -title: {index_title}\n" - index_metadata += """ -hide: - - toc\n""" - # If page set hide navigation - if hide_navigation: - index_metadata += " - navigation\n" - index_metadata += "---\n\n" - - # If index.md has content, add to yaml - if index_content: - if path == 'docs': - link = '' - else: - link = f"{path.replace('docs/', '')}/" - yaml = f" - {index_title}: '{link}index.md'\n" + yaml - else: - # If index.md has no content, use default content - index_content = index_content_default.format( - index_title=index_title, index_description=index_description) - - # Geneate index docs list - docs_list_html = '\n\n\n
' - for i in index_dir_list: - if i['doc'] == 'index.md': - continue - docs_list_html += '\n\ -
\n\ - \n\ -
\n\ - {title}\n\ -

{description}

\n\ -
\n\ -
'.format(path=html.escape('/'+i['path'].replace('docs/', '').replace('.md', '')), - title=html.escape(i['title']), - description=html.escape(i['description']), - type=i['type']) - docs_list_html += '\n
' - - if hide_docs_list != True: - index_content += docs_list_html - - # Create index.md to cache - with open(f"cache/{path_relative}/index.md", 'w', encoding='UTF-8', errors='ignore') as f: - f.write(index_metadata + index_content) - - return (yaml, index_title, index_description, parent_index) - - -def generate_contributing_note(doc_path): - """Generate contributing note HTML for a document""" - github_edit_url = f"https://github.com/nm-Team/Support/edit/main/docs/{doc_path}" - - contributing_note = f''' -!!! tip "帮助我们改进此文档" - 发现文档有错误或需要改进的地方?您可以: - - - [在 GitHub 上直接编辑此页面]({github_edit_url}) - - [提交 Issue 报告问题](https://github.com/nm-Team/Support/issues/new) - - [加入我们的讨论](https://github.com/nm-Team/Support/discussions) - - 您的贡献将帮助更多用户获得更好的体验! - -''' - return contributing_note - - -def should_hide_contributing_note(metadata, doc_path, doc_name): - """Check if contributing note should be hidden for this document""" - # Check if metadata contains Hide Contributing Note - if metadata and re.search(r"hideContributingNote", metadata, re.IGNORECASE): - return True - - # Check if it's an index.md file - if doc_name == "index.md": - return True - - # Define excluded path patterns (regex patterns) - excluded_patterns = [ - r"^legal/", # Files directly in legal folder - r"/legal/", # Files in any legal subfolder - r"/update-log/", # Files in update-log folder - # Add more patterns here as needed - ] - - # Check if the document path matches any excluded pattern - for pattern in excluded_patterns: - if re.search(pattern, doc_path): - return True - - return False - - -def generate_redirects(): - """Generate redirects JavaScript from redirects.json""" - try: - # Read redirects configuration - with open('redirects.json', 'r', encoding='utf-8') as f: - config = json.load(f) - - # Generate JavaScript content - js_content = '''// 自动生成的重定向脚本 -(function() { - // 重定向映射 - const redirects = ''' + json.dumps(config['redirects'], ensure_ascii=False, indent=8) + '''; - - // 获取当前路径 - const currentPath = window.location.pathname; - - // 检查是否需要重定向 - for (const oldPath in redirects) { - if (currentPath === oldPath || currentPath.startsWith(oldPath)) { - const newPath = redirects[oldPath]; - // 执行重定向 - window.location.replace(newPath); - break; - } - } -})();''' - - # Ensure assets/js directory exists - assets_dir = 'cache/assets/js' - if not os.path.exists(assets_dir): - os.makedirs(assets_dir) - - # Write the JavaScript file - with open('cache/assets/js/redirects.js', 'w', encoding='utf-8') as f: - f.write(js_content) - - print("Redirects script generated.") - - except FileNotFoundError: - print("Warning: redirects.json not found. Skipping redirects generation.") - except Exception as e: - print(f"Warning: Failed to generate redirects: {e}") - - -if __name__ == "__main__": - generate() diff --git a/manage.py b/manage.py old mode 100644 new mode 100755 index d5a9028..64cd570 --- a/manage.py +++ b/manage.py @@ -1,244 +1,16 @@ #!/usr/bin/env python -# -*- coding: utf-8 -*- -""" -nmTeam Documentation Management Tool -管理脚本,提供开发和构建指令 -""" - -import os -import sys -import time -import subprocess -import threading -from pathlib import Path -from watchdog.observers import Observer -from watchdog.events import FileSystemEventHandler -from generate import generate - - -class DocumentationHandler(FileSystemEventHandler): - """文档变更监听器""" - - def __init__(self): - self.last_modified = {} - - def on_modified(self, event): - if event.is_directory: - return - - # 只监听 .md 文件和配置文件 - if not (event.src_path.endswith('.md') or - event.src_path.endswith('.yml') or - event.src_path.endswith('.yaml')): - return - - # 防止频繁触发 - now = time.time() - if event.src_path in self.last_modified: - if now - self.last_modified[event.src_path] < 1: - return - self.last_modified[event.src_path] = now - - print(f"检测到文件变更: {event.src_path}") - self.rebuild_docs() - - def rebuild_docs(self): - """重新构建文档""" - try: - print("🔄 重新生成文档...") - generate() - print("✅ 文档重新生成完成") - except Exception as e: - print(f"❌ 文档生成失败: {e}") - - -def run_mkdocs_serve(): - """运行 MkDocs 开发服务器""" - try: - # 首先生成文档 - print("🚀 启动开发服务器...") - generate() - - # 启动 MkDocs 服务器 - process = subprocess.Popen( - [sys.executable, "-m", "mkdocs", "serve", "--dev-addr", "127.0.0.1:8000"], - stdout=subprocess.PIPE, - stderr=subprocess.STDOUT, - text=True, - bufsize=1, - universal_newlines=True - ) - - # 实时输出日志 - for line in iter(process.stdout.readline, ''): - print(line.rstrip()) - - except KeyboardInterrupt: - print("\n🛑 开发服务器已停止") - if 'process' in locals(): - process.terminate() - except Exception as e: - print(f"❌ 启动开发服务器失败: {e}") - - -def dev_command(): - """开发模式:启动文件监听和开发服务器""" - print("🔥 启动开发模式...") - print("📁 监听 docs/ 目录变化...") - - # 设置文件监听 - event_handler = DocumentationHandler() - observer = Observer() - observer.schedule(event_handler, "docs", recursive=True) - observer.schedule(event_handler, "mkdocs-template.yml", recursive=False) - - # 启动监听 - observer.start() - - try: - # 在单独线程中运行 MkDocs 服务器 - server_thread = threading.Thread(target=run_mkdocs_serve) - server_thread.daemon = True - server_thread.start() - - print("✅ 开发环境已启动") - print("🌐 访问 http://127.0.0.1:8000 查看文档") - print("📝 修改 docs/ 目录下的文件将自动重新生成") - print("💡 按 Ctrl+C 停止服务") - - # 保持主线程运行 - server_thread.join() - - except KeyboardInterrupt: - print("\n🛑 停止开发模式...") - finally: - observer.stop() - observer.join() - +# manage.py +"""Backward-compatible CLI wrapper (delegates to the nmteam package). -def build_command(): - """构建模式:生成文档并构建静态站点""" - print("🏗️ 开始构建...") - - try: - # 步骤1: 生成文档 - print("📝 第1步: 生成文档结构...") - generate() - print("✅ 文档生成完成") - - # 步骤2: 构建静态站点 - print("🔨 第2步: 构建静态站点...") - result = subprocess.run( - [sys.executable, "-m", "mkdocs", "build", "--clean"], - capture_output=True, - text=True - ) - - if result.returncode == 0: - print("✅ 静态站点构建完成") - print("📁 输出目录: site/") - else: - print(f"❌ 构建失败: {result.stderr}") - return False - - except Exception as e: - print(f"❌ 构建过程出错: {e}") - return False - - return True +Kept so ``dev.sh`` / ``dev.ps1`` / ``dev.bat`` and muscle memory for +``python manage.py `` keep working. Run it through uv: - -def clean_command(): - """清理生成的文件""" - print("🧹 清理中...") - - dirs_to_clean = ['cache', 'generated', 'site'] - - for dir_name in dirs_to_clean: - if os.path.exists(dir_name): - import shutil - shutil.rmtree(dir_name) - print(f"🗑️ 已删除: {dir_name}/") - - print("✅ 清理完成") - - -def install_command(): - """安装依赖""" - print("📦 安装依赖...") - - try: - # 更新 requirements.txt 以包含 watchdog - requirements_path = "requirements.txt" - with open(requirements_path, 'r', encoding='utf-8') as f: - content = f.read() - - if 'watchdog' not in content: - with open(requirements_path, 'a', encoding='utf-8') as f: - f.write('watchdog\n') - print("📝 已更新 requirements.txt") - - # 安装依赖 - result = subprocess.run( - [sys.executable, "-m", "pip", "install", "-r", "requirements.txt"], - capture_output=True, - text=True - ) - - if result.returncode == 0: - print("✅ 依赖安装完成") - else: - print(f"❌ 依赖安装失败: {result.stderr}") - - except Exception as e: - print(f"❌ 安装过程出错: {e}") - - -def show_help(): - """显示帮助信息""" - help_text = """ -nmTeam Documentation Management Tool - -用法: - python manage.py - -可用命令: - dev 启动开发模式 (文件监听 + 热更新服务器) - build 构建静态站点 (生成文档 + 构建) - clean 清理生成的文件 - install 安装依赖包 - help 显示此帮助信息 - -示例: - python manage.py dev # 启动开发服务器 - python manage.py build # 构建生产版本 - python manage.py clean # 清理临时文件 + uv run python manage.py dev """ - print(help_text) +import sys -def main(): - """主函数""" - if len(sys.argv) < 2: - show_help() - return - - command = sys.argv[1].lower() - - commands = { - 'dev': dev_command, - 'build': build_command, - 'clean': clean_command, - 'install': install_command, - 'help': show_help, - } - - if command in commands: - commands[command]() - else: - print(f"❌ 未知命令: {command}") - show_help() - +from nmteam_support.cli import main if __name__ == "__main__": - main() + sys.exit(main()) diff --git a/pyproject.toml b/pyproject.toml index a7f8aca..34347aa 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -9,6 +9,9 @@ dependencies = [ "pyyaml>=6.0.2", ] +[project.scripts] +nmteam = "nmteam_support.cli:main" + [dependency-groups] dev = [ "mdformat>=1.0", diff --git a/src/nmteam_support/cli.py b/src/nmteam_support/cli.py new file mode 100644 index 0000000..d764952 --- /dev/null +++ b/src/nmteam_support/cli.py @@ -0,0 +1,98 @@ +"""Command-line interface: generate / dev / build / clean / install.""" + +from __future__ import annotations + +import argparse +import shutil +import subprocess +import sys +import threading +from pathlib import Path + +from nmteam_support.generator import GeneratorOptions, default_options, generate + + +def _rm(path: Path) -> None: + if path.exists(): + if path.is_dir(): + shutil.rmtree(path) + else: + path.unlink() + + +def cmd_clean(options: GeneratorOptions) -> int: + """Remove cache/, generated/ and site/.""" + for path in (options.cache_dir, options.generated_dir, options.mkdocs_yml_path.parent / "site"): + _rm(path) + print("✅ 清理完成") + return 0 + + +def cmd_build(options: GeneratorOptions) -> int: + """Regenerate and build the static site into site/.""" + generate(options) + return subprocess.call([sys.executable, "-m", "mkdocs", "build", "--strict", "--clean"]) + + +def cmd_dev(options: GeneratorOptions) -> int: + """Regenerate, then serve with live-reload and auto-regeneration.""" + generate(options) + stop = threading.Event() + watcher = threading.Thread(target=_watch_and_regenerate, args=(options, stop), daemon=True) + watcher.start() + try: + return subprocess.call( + [sys.executable, "-m", "mkdocs", "serve", "--dirtyreload", "--dev-addr", "127.0.0.1:8000"] + ) + finally: + stop.set() + + +def _watch_and_regenerate(options: GeneratorOptions, stop: threading.Event) -> None: + """Regenerate when docs/ or the template changes; mkdocs live-reloads the rest.""" + last = _snapshot(options) + while not stop.wait(1.0): + current = _snapshot(options) + if current != last: + last = current + print("🔄 检测到变更,重新生成...") + generate(options) + + +def _snapshot(options: GeneratorOptions) -> tuple[tuple[int, int], ...]: + stamps: list[tuple[int, int]] = [] + for root in (options.docs_dir, options.template_path): + if root.is_file(): + stamps.append((root.stat().st_mtime_ns, root.stat().st_size)) + elif root.is_dir(): + for path in sorted(root.rglob("*")): + if path.is_file(): + stamps.append((path.stat().st_mtime_ns, path.stat().st_size)) + return tuple(stamps) + + +def cmd_install() -> int: + """Install dependencies via uv (equivalent to ``uv sync``).""" + return subprocess.call(["uv", "sync"]) + + +def main(argv: list[str] | None = None) -> int: + parser = argparse.ArgumentParser(prog="nmteam", description="nmTeam Support 文档站工具链") + parser.add_argument( + "command", + nargs="?", + default="generate", + choices=["generate", "dev", "build", "clean", "install"], + ) + args = parser.parse_args(argv) + options = default_options() + if args.command == "generate": + generate(options) + return 0 + if args.command == "dev": + return cmd_dev(options) + if args.command == "build": + return cmd_build(options) + if args.command == "clean": + return cmd_clean(options) + return cmd_install() diff --git a/tests/test_cli.py b/tests/test_cli.py new file mode 100644 index 0000000..3b76b90 --- /dev/null +++ b/tests/test_cli.py @@ -0,0 +1,28 @@ +"""CLI command tests.""" + +from pathlib import Path + +from nmteam_support.cli import cmd_clean +from nmteam_support.generator import GeneratorOptions + + +def _options(tmp_path: Path) -> GeneratorOptions: + return GeneratorOptions( + docs_dir=tmp_path / "docs", + template_path=tmp_path / "mkdocs-template.yml", + redirects_path=tmp_path / "redirects.json", + cache_dir=tmp_path / "cache", + generated_dir=tmp_path / "generated", + mkdocs_yml_path=tmp_path / "mkdocs.yml", + ) + + +def test_cmd_clean_removes_output_dirs(tmp_path): + opts = _options(tmp_path) + opts.cache_dir.mkdir() + opts.generated_dir.mkdir() + (tmp_path / "site").mkdir() + assert cmd_clean(opts) == 0 + assert not opts.cache_dir.exists() + assert not opts.generated_dir.exists() + assert not (tmp_path / "site").exists() From 5d56988f42c232240b4dd0eb51848be597f63d41 Mon Sep 17 00:00:00 2001 From: Alice39s Date: Sun, 9 Aug 2026 17:05:56 +0900 Subject: [PATCH 14/57] chore(ruff): configure linting and format Python sources --- pyproject.toml | 18 +++++++++++ redirects_manager.py | 53 +++++++++++++++++++-------------- src/nmteam_support/cli.py | 10 ++++++- src/nmteam_support/index.py | 5 +--- src/nmteam_support/redirects.py | 2 +- src/nmteam_support/scanner.py | 2 +- tests/test_docslist.py | 4 ++- tests/test_frontmatter.py | 4 +-- tests/test_generator.py | 4 ++- 9 files changed, 68 insertions(+), 34 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 34347aa..1ef3041 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -32,3 +32,21 @@ packages = ["src/nmteam_support"] testpaths = ["tests"] pythonpath = ["src"] addopts = "-ra" + +[tool.ruff] +line-length = 100 +target-version = "py314" +src = ["src", "tests"] +exclude = ["docs"] + +[tool.ruff.lint] +select = ["E", "F", "I", "UP", "B", "SIM", "W", "C4", "RUF"] +ignore = ["E501"] +allowed-confusables = [",", "。", "?", ":", "!"] + +[tool.ruff.lint.isort] +known-first-party = ["nmteam_support"] + +[tool.ruff.format] +quote-style = "double" +indent-style = "space" diff --git a/redirects_manager.py b/redirects_manager.py index e94b8b7..295dc51 100644 --- a/redirects_manager.py +++ b/redirects_manager.py @@ -8,80 +8,87 @@ """ import json -import sys import os +import sys + +REDIRECTS_FILE = "redirects.json" -REDIRECTS_FILE = 'redirects.json' def load_redirects(): """加载重定向配置""" if not os.path.exists(REDIRECTS_FILE): return {"redirects": {}} - + try: - with open(REDIRECTS_FILE, 'r', encoding='utf-8') as f: + with open(REDIRECTS_FILE, encoding="utf-8") as f: return json.load(f) - except: + except OSError, ValueError: return {"redirects": {}} + def save_redirects(config): """保存重定向配置""" - with open(REDIRECTS_FILE, 'w', encoding='utf-8') as f: + with open(REDIRECTS_FILE, "w", encoding="utf-8") as f: json.dump(config, f, ensure_ascii=False, indent=2) + def add_redirect(old_path, new_path): """添加重定向""" config = load_redirects() - config['redirects'][old_path] = new_path + config["redirects"][old_path] = new_path save_redirects(config) print(f"已添加重定向: {old_path} -> {new_path}") + def remove_redirect(old_path): """删除重定向""" config = load_redirects() - if old_path in config['redirects']: - del config['redirects'][old_path] + if old_path in config["redirects"]: + del config["redirects"][old_path] save_redirects(config) print(f"已删除重定向: {old_path}") else: print(f"未找到重定向: {old_path}") + def list_redirects(): """列出所有重定向""" config = load_redirects() - if not config['redirects']: + if not config["redirects"]: print("当前没有配置任何重定向") return - + print("当前重定向规则:") - for old_path, new_path in config['redirects'].items(): + for old_path, new_path in config["redirects"].items(): print(f" {old_path} -> {new_path}") + def main(): if len(sys.argv) < 2: print(__doc__) return - + command = sys.argv[1] - - if command == 'add': + + if command == "add": if len(sys.argv) != 4: - print("用法: python redirects_manager.py add \"/old-path/\" \"/new-path/\"") + print('用法: python redirects_manager.py add "/old-path/" "/new-path/"') return add_redirect(sys.argv[2], sys.argv[3]) - - elif command == 'remove': + + elif command == "remove": if len(sys.argv) != 3: - print("用法: python redirects_manager.py remove \"/old-path/\"") + print('用法: python redirects_manager.py remove "/old-path/"') return remove_redirect(sys.argv[2]) - - elif command == 'list': + + elif command == "list": list_redirects() - + else: print(f"未知命令: {command}") print(__doc__) -if __name__ == '__main__': + +if __name__ == "__main__": main() diff --git a/src/nmteam_support/cli.py b/src/nmteam_support/cli.py index d764952..422e7cf 100644 --- a/src/nmteam_support/cli.py +++ b/src/nmteam_support/cli.py @@ -42,7 +42,15 @@ def cmd_dev(options: GeneratorOptions) -> int: watcher.start() try: return subprocess.call( - [sys.executable, "-m", "mkdocs", "serve", "--dirtyreload", "--dev-addr", "127.0.0.1:8000"] + [ + sys.executable, + "-m", + "mkdocs", + "serve", + "--dirtyreload", + "--dev-addr", + "127.0.0.1:8000", + ] ) finally: stop.set() diff --git a/src/nmteam_support/index.py b/src/nmteam_support/index.py index 003f170..1091b56 100644 --- a/src/nmteam_support/index.py +++ b/src/nmteam_support/index.py @@ -23,10 +23,7 @@ def render_index_page(scan: ScannedDir) -> str: head_lines.append("") head = "\n".join(head_lines) + "\n" - if scan.index_body: - content = scan.index_body - else: - content = f"# {scan.index_meta.title}\n{scan.index_meta.description}\n" + content = scan.index_body or f"# {scan.index_meta.title}\n{scan.index_meta.description}\n" if not scan.index_meta.hide_docs_list: entries = sort_entries(scan.docs + folder_entries(scan)) diff --git a/src/nmteam_support/redirects.py b/src/nmteam_support/redirects.py index c34acca..85ee12e 100644 --- a/src/nmteam_support/redirects.py +++ b/src/nmteam_support/redirects.py @@ -11,7 +11,7 @@ def load_redirects(path: Path) -> dict[str, str] | None: try: with path.open(encoding="utf-8") as f: return json.load(f)["redirects"] - except (json.JSONDecodeError, KeyError, OSError): + except json.JSONDecodeError, KeyError, OSError: return None diff --git a/src/nmteam_support/scanner.py b/src/nmteam_support/scanner.py index 73f6cb8..60e98d6 100644 --- a/src/nmteam_support/scanner.py +++ b/src/nmteam_support/scanner.py @@ -25,7 +25,7 @@ class ScannedDir: index_meta: PageMetadata # metadata from index.md (or name-derived defaults) index_body: str # index.md body after frontmatter ("" when absent/empty) docs: list[DocEntry] = field(default_factory=list) # non-index .md files - subdirs: list["ScannedDir"] = field(default_factory=list) + subdirs: list[ScannedDir] = field(default_factory=list) other_files: list[str] = field(default_factory=list) # non-.md files, relative paths image_dirs: list[str] = field(default_factory=list) # dirs named "img", relative paths diff --git a/tests/test_docslist.py b/tests/test_docslist.py index 3b2ea83..1190750 100644 --- a/tests/test_docslist.py +++ b/tests/test_docslist.py @@ -14,7 +14,9 @@ def test_render_docs_list_structure(): def test_render_docs_list_strips_md_and_escapes(): - entries = [DocEntry(title="A&B", description='say "hi"', path="n/b.md", name="b.md", kind="doc")] + entries = [ + DocEntry(title="A&B", description='say "hi"', path="n/b.md", name="b.md", kind="doc") + ] out = render_docs_list(entries) assert 'href="/n/b"' in out assert "A&B" in out diff --git a/tests/test_frontmatter.py b/tests/test_frontmatter.py index ba62d1c..2fa6417 100644 --- a/tests/test_frontmatter.py +++ b/tests/test_frontmatter.py @@ -1,6 +1,6 @@ """Frontmatter parsing smoke tests.""" -from nmteam_support.frontmatter import parse_page, split_frontmatter, split_frontmatter_lines +from nmteam_support.frontmatter import parse_page, split_frontmatter def test_split_frontmatter_roundtrip(): @@ -45,7 +45,7 @@ def test_index_garbage_defaults_to_zero(): def test_description_skips_images_html_and_admonitions(): meta, _ = parse_page( - "---\n---\n\n# T\n\n![图](./img/a.png)\n\n
HTML
\n\n!!! note \"注\"\n 内容\n\n正文第一句。\n", + '---\n---\n\n# T\n\n![图](./img/a.png)\n\n
HTML
\n\n!!! note "注"\n 内容\n\n正文第一句。\n', "x.md", ) assert meta.description == "正文第一句。" diff --git a/tests/test_generator.py b/tests/test_generator.py index 94f9017..c716710 100644 --- a/tests/test_generator.py +++ b/tests/test_generator.py @@ -43,7 +43,9 @@ def test_generate_injects_contributing_note(tmp_path, docs_dir): def test_generate_skips_contributing_note_for_update_log(tmp_path, docs_dir): d = docs_dir / "nmbot-telegram" / "update-log" d.mkdir() - (d / "2026-01.md").write_text("---\nindex: -2601\n---\n\n# 2026-01\n\n内容。\n", encoding="utf-8") + (d / "2026-01.md").write_text( + "---\nindex: -2601\n---\n\n# 2026-01\n\n内容。\n", encoding="utf-8" + ) options = _full_options(tmp_path, docs_dir) generate(options) content = (options.generated_dir / "nmbot-telegram" / "update-log" / "2026-01.md").read_text( From 6c4f3b87a1a3e615ad92cc6a79316fb12b96b843 Mon Sep 17 00:00:00 2001 From: Alice39s Date: Sun, 9 Aug 2026 17:09:38 +0900 Subject: [PATCH 15/57] chore(docs): format Markdown with mdformat --- .mdformat.toml | 11 + docs/about.md | 5 +- docs/contact-us/contact-support.md | 18 +- docs/contact-us/index.md | 3 +- docs/contact-us/social-media.md | 4 + docs/nmbot-telegram/about.md | 2 +- docs/nmbot-telegram/beta.md | 2 + .../nmbot-telegram/business/scheduled-task.md | 10 +- .../business/set-business-chatbot.md | 34 +-- docs/nmbot-telegram/credit/credit.md | 17 +- .../faq/about-message-content-of-bot.md | 16 +- docs/nmbot-telegram/faq/buy-stars.md | 22 +- .../nmbot-telegram/faq/cannot-access-panel.md | 11 +- .../faq/cannot-log-in-to-panel.md | 22 +- .../faq/channel-verification-not-working.md | 5 + .../faq/why-my-message-deleted.md | 12 +- docs/nmbot-telegram/group/action-log.md | 19 +- .../group/admin-rights-and-features.md | 5 +- docs/nmbot-telegram/group/anti-spam.md | 11 +- docs/nmbot-telegram/group/banme.md | 2 +- .../group/channel-perspective-eye.md | 10 +- .../group/delete-message-by-type.md | 2 +- docs/nmbot-telegram/group/emergency-mode.md | 2 + docs/nmbot-telegram/group/group-setup.md | 16 +- .../nmbot-telegram/group/join-verification.md | 1 + docs/nmbot-telegram/group/key-word-reply.md | 1 + .../group/members-interaction.md | 32 ++- docs/nmbot-telegram/group/reg-panel.md | 19 +- docs/nmbot-telegram/group/reply-channel.md | 26 ++- docs/nmbot-telegram/group/scheduled-task.md | 13 +- .../group/unban-users-banned-by-anti-spam.md | 6 +- docs/nmbot-telegram/group/warn.md | 10 +- .../img/update-pictures/index.md | 15 +- docs/nmbot-telegram/launch-panel.md | 3 +- .../legal/group-member-nmbot-agreement.md | 11 +- .../nmbot-telegram/legal/plus-terms-of-use.md | 8 +- docs/nmbot-telegram/legal/privacy-policy.md | 6 +- docs/nmbot-telegram/legal/terms-of-use.md | 6 +- docs/nmbot-telegram/mcp.md | 10 +- docs/nmbot-telegram/message-template.md | 24 +- docs/nmbot-telegram/nmartchat.md | 14 +- .../nmbot-intelligence/feedback.md | 2 +- .../panel/how-to-launch-panel.md | 2 +- docs/nmbot-telegram/plus/benefits.md | 22 +- docs/nmbot-telegram/plus/payment-methods.md | 2 +- docs/nmbot-telegram/set-language.md | 6 +- docs/nmbot-telegram/support.md | 3 +- docs/nmbot-telegram/tools/dc.md | 6 +- docs/nmbot-telegram/update-log/2023-01.md | 13 +- docs/nmbot-telegram/update-log/2023-02.md | 10 +- docs/nmbot-telegram/update-log/2023-03.md | 7 +- docs/nmbot-telegram/update-log/2023-04.md | 15 +- docs/nmbot-telegram/update-log/2023-05.md | 12 +- docs/nmbot-telegram/update-log/2023-06.md | 10 +- docs/nmbot-telegram/update-log/2023-07.md | 11 +- docs/nmbot-telegram/update-log/2023-08.md | 6 + docs/nmbot-telegram/update-log/2023-09.md | 10 +- docs/nmbot-telegram/update-log/2023-10.md | 12 +- docs/nmbot-telegram/update-log/2023-11.md | 8 + docs/nmbot-telegram/update-log/2023-12.md | 7 +- docs/nmbot-telegram/update-log/2024-01.md | 9 +- docs/nmbot-telegram/update-log/2024-02.md | 13 +- docs/nmbot-telegram/update-log/2024-07.md | 17 +- docs/nmbot-telegram/update-log/2024-08.md | 10 + docs/nmbot-telegram/update-log/2024-09.md | 4 + docs/nmbot-telegram/update-log/2024-10.md | 8 +- docs/nmbot-telegram/update-log/2024-12.md | 3 + docs/nmbot-telegram/update-log/2025-01.md | 10 +- docs/nmbot-telegram/update-log/2025-02.md | 3 +- docs/nmbot-telegram/update-log/2025-03.md | 2 +- docs/nmbot-telegram/update-log/2025-04.md | 7 +- docs/nmbot-telegram/update-log/2025-05.md | 12 +- docs/nmbot-telegram/update-log/2025-10.md | 6 +- docs/nmbot-telegram/update-log/2025-12.md | 2 +- docs/nmbot-telegram/update-log/index.md | 2 +- docs/nmbot-telegram/update-log/nmbot-next.md | 16 ++ docs/nmbot-telegram/update-log/nmbot-plus.md | 25 ++- .../nmbot-telegram/update-log/previous-log.md | 207 ++++++++++++++++-- docs/nmteam-account/index.md | 3 + docs/nmteam-account/issues.md | 17 +- 80 files changed, 737 insertions(+), 269 deletions(-) create mode 100644 .mdformat.toml diff --git a/.mdformat.toml b/.mdformat.toml new file mode 100644 index 0000000..4d9e26f --- /dev/null +++ b/.mdformat.toml @@ -0,0 +1,11 @@ +# mdformat 配置:仓库根目录,适用于 docs/ 下所有 .md 文件 +# 插件(mdformat-mkdocs、mdformat-front-matters)通过 entry point 自动启用, +# 因此 admonition、content tabs、YAML frontmatter 均受保护,无需 extensions 列表。 +wrap = "keep" +number = false +end_of_line = "lf" +validate = true + +[plugin.mkdocs] +# 保持 mdformat-mkdocs 默认行为(4 空格列表缩进、admonition 保护); +# 如后续需要 --align-semantic-breaks-in-lists 等开关,在此追加配置项。 diff --git a/docs/about.md b/docs/about.md index 24152bf..326b531 100644 --- a/docs/about.md +++ b/docs/about.md @@ -5,11 +5,13 @@ index: 200 --- ## nmTeam 帮助文档 + nmTeam 帮助文档由 nmTeam 成员和社区志愿者共同编辑。 您可以在 [GitHub](https://github.com/nm-Team/Support) 上参与贡献此文档。 ## 贡献者 + +
\ No newline at end of file + diff --git a/docs/contact-us/contact-support.md b/docs/contact-us/contact-support.md index d14b9d1..85e1670 100644 --- a/docs/contact-us/contact-support.md +++ b/docs/contact-us/contact-support.md @@ -4,6 +4,7 @@ description: 了解如何联系 nmTeam 支持获取帮助。 --- # 联系 nmTeam 支持 + 我们欢迎您联系 nmTeam 支持获取帮助。我们的支持团队将尽力为您提供帮助。 ## 联系 nmTeam 支持前,请注意 @@ -11,26 +12,29 @@ description: 了解如何联系 nmTeam 支持获取帮助。 - nmTeam 帮助文档中包含许多常见问题的解答。提出问题前,请先查阅帮助文档,并尝试使用搜索功能查找有关您的问题的解答。 - 如果您在使用 nmTeam 产品时遇到问题,请尽量提供详细的信息,以便我们更快地为您解决问题。 - nmTeam 支持包括志愿者和员工。 -我们的支持团队会尽力为您提供帮助,但无法保证能够解决所有问题。 + 我们的支持团队会尽力为您提供帮助,但无法保证能够解决所有问题。 - 联系 nmTeam 支持时,请尽量使用英文或中文。 -我们的支持团队会尽量使用您提供的语言回复您。 + 我们的支持团队会尽量使用您提供的语言回复您。 - 联系 nmTeam 时请保持礼貌。我们的支持团队会尽力为您提供帮助,但不接受任何形式的辱骂或威胁。 -若您的行为不当,我们将拒绝为您提供帮助。 + 若您的行为不当,我们将拒绝为您提供帮助。 - 由于 nmTeam 支持团队的业务繁忙,我们可能无法立即回复您的问题。 -请耐心等待,我们会尽快回复您。 -有时我们会在不回复的情况下解决您的问题,因此您可以在提交问题后等待一段时间查看您的问题是否已解决。 + 请耐心等待,我们会尽快回复您。 + 有时我们会在不回复的情况下解决您的问题,因此您可以在提交问题后等待一段时间查看您的问题是否已解决。 - nmTeam 支持团队不会要求您提供 nmTeam 账号的密码或其他敏感信息。 -请勿向任何人提供您的 nmTeam 账号密码或其他敏感信息。 + 请勿向任何人提供您的 nmTeam 账号密码或其他敏感信息。 - 由于 nmTeam 支持团队在多个平台上均提供帮助,我们建议您尽可能在遇到问题的平台上联系 nmTeam 支持。 -例如,如果您在 nmBot Telegram 中遇到问题,请在 Telegram 中联系 nmTeam 支持,以便我们更快地为您解决问题。 + 例如,如果您在 nmBot Telegram 中遇到问题,请在 Telegram 中联系 nmTeam 支持,以便我们更快地为您解决问题。 ## 联系 nmTeam 支持 + 您可以通过以下方式联系 nmTeam 支持: ### 电子邮件支持 + 您可以通过电子邮件联系 nmTeam 支持。 请发送邮件至 [support@nmteam.xyz](mailto:support@nmteam.xyz)。 ### Telegram 支持 + 您可以在 Telegram 中联系 nmTeam 支持。 我们的 Telegram 账号是 [@nmteamsupportbot](https://t.me/nmteamsupportbot)。 diff --git a/docs/contact-us/index.md b/docs/contact-us/index.md index 738e0ed..0eb11e8 100644 --- a/docs/contact-us/index.md +++ b/docs/contact-us/index.md @@ -5,4 +5,5 @@ index: 100 --- # 联系 nmTeam -了解如何联系 nmTeam,包括在社交网站上关注我们,或联系 nmTeam 支持获取帮助。 \ No newline at end of file + +了解如何联系 nmTeam,包括在社交网站上关注我们,或联系 nmTeam 支持获取帮助。 diff --git a/docs/contact-us/social-media.md b/docs/contact-us/social-media.md index e290b4a..1e11412 100644 --- a/docs/contact-us/social-media.md +++ b/docs/contact-us/social-media.md @@ -4,13 +4,16 @@ description: 了解如何在社交媒体上关注 nmTeam,获取最新消息和 --- # 社交媒体上的 nmTeam + 您可以在社交媒体上关注 nmTeam,获取最新消息和帮助。 ## Twitter + 您可以在 Twitter 上关注 nmTeam,获取最新消息和帮助。 我们的 Twitter 账号是 [@nmnmfun](https://twitter.com/nmnmfun)。 ## Telegram + 您可以订阅 nmTeam Newsroom 频道,获取 nmTeam 的最新消息。 我们的 Telegram 频道是 [@nmteamnewsroom](https://t.me/nmteamnewsroom)。 @@ -21,5 +24,6 @@ nmTeam Development Channel 是极客和铁粉观测 nmTeam 最新产品开发动 nmTeam Development Channel 是 [@nmteampush](https://t.me/nmteampush)。 ## Discord + 您可以加入 nmTeam Discord 服务器,与其他用户交流。 我们的 Discord 服务器是 [nmTeam](https://discord.gg/kRegqfMx)。 diff --git a/docs/nmbot-telegram/about.md b/docs/nmbot-telegram/about.md index 48ea8c4..51af28c 100644 --- a/docs/nmbot-telegram/about.md +++ b/docs/nmbot-telegram/about.md @@ -10,7 +10,7 @@ title: 关于 nmBot nmBot 是由 nmTeam 出品的 Telegram 机器人,兼具扎实可靠的群组管理功能和温暖搞笑的互动娱乐功能。 -nmBot 高效安全、保障隐私。欢迎在您的群组免费使用 nmBot。 +nmBot 高效安全、保障隐私。欢迎在您的群组免费使用 nmBot。 ## 关于 nmBot 帮助文档 diff --git a/docs/nmbot-telegram/beta.md b/docs/nmbot-telegram/beta.md index 72849ec..96e5730 100644 --- a/docs/nmbot-telegram/beta.md +++ b/docs/nmbot-telegram/beta.md @@ -3,6 +3,7 @@ nmBot 预览版是 nmBot 的官方公测版本,用于测试 nmBot 的新功能和改进。nmBot 的部分新功能将首先在 nmBot 预览版中发布,以便用户提前体验和反馈,及供 nmBot 开发团队进行测试和改进。 ## 预览版与稳定版的区别 + nmBot 预览版为发烧友和早期用户提供了一种抢先体验 nmBot 新功能和改进的机会。相较于 nmBot 稳定版而言,nmBot 预览版可能存在以下区别: - **新功能和改进**:nmBot 预览版可能包含 nmBot 的新功能和改进,这些功能和改进可能尚未在 nmBot 稳定版中发布。 @@ -11,6 +12,7 @@ nmBot 预览版为发烧友和早期用户提供了一种抢先体验 nmBot 新 - **数据丢失和泄露**:由于 nmBot 预览版的不稳定性,用户可能遭遇数据丢失、数据泄露或其他问题的风险。 ## 注意事项 + 使用 nmBot 预览版时,用户应注意以下事项: - **不建议用于生产环境**:nmBot 预览版仅供测试和体验新功能,**不建议在生产环境中使用**。用户应在测试环境中使用 nmBot 预览版,以避免对生产数据和业务造成影响。 diff --git a/docs/nmbot-telegram/business/scheduled-task.md b/docs/nmbot-telegram/business/scheduled-task.md index a82f2ad..9fe75f6 100644 --- a/docs/nmbot-telegram/business/scheduled-task.md +++ b/docs/nmbot-telegram/business/scheduled-task.md @@ -12,7 +12,7 @@ description: 了解如何使用 nmBot 商业定时任务,按时间自动更新 使用商业定时任务前,请先完成以下准备: 1. 个人账号已开通 Telegram Business。 -2. 已将 nmBot 设置为商业机器人。 +1. 已将 nmBot 设置为商业机器人。 如未完成设置,请先阅读[设置 nmBot 为商业机器人](./set-business-chatbot.md)。 @@ -31,10 +31,10 @@ description: 了解如何使用 nmBot 商业定时任务,按时间自动更新 典型流程如下: 1. 打开 nmBot 面板并进入“商业”页面。 -2. 进入“商业定时任务”配置区域。 -3. 新建任务并设置执行时间与执行日期。 -4. 填写需要变更的资料项并保存。 -5. 在下一次触发时间观察变更是否符合预期。 +1. 进入“商业定时任务”配置区域。 +1. 新建任务并设置执行时间与执行日期。 +1. 填写需要变更的资料项并保存。 +1. 在下一次触发时间观察变更是否符合预期。 ## 常见使用场景 diff --git a/docs/nmbot-telegram/business/set-business-chatbot.md b/docs/nmbot-telegram/business/set-business-chatbot.md index c693bfd..0b21fa8 100644 --- a/docs/nmbot-telegram/business/set-business-chatbot.md +++ b/docs/nmbot-telegram/business/set-business-chatbot.md @@ -10,28 +10,28 @@ description: 了解如何将 nmBot 设置为商业机器人,让 nmBot 以您 ## 在 Android 版 Telegram 中设置 1. 打开 Telegram 应用后,从屏幕的左侧边缘向右滑动或点击左上角的菜单按钮,打开侧边栏。 -2. 在侧边栏中向下滚动并点击`设置 (Settings)`。 -3. 在设置页面中向下滚动并点击 `Telegram Business`。 -4. 在`Telegram Business`页面中向下滚动并点击`聊天机器人 (Chatbot)`。 -5. 在打开的页面中,在`机器人 URL 或用户名 (Bot URL or Username)` 字段中输入 `nmnmfunbot` 并点击 `nmBot`。 -6. 向下滚动并启用您需要的权限。 -7. 返回页面,更改将自动保存。 +1. 在侧边栏中向下滚动并点击`设置 (Settings)`。 +1. 在设置页面中向下滚动并点击 `Telegram Business`。 +1. 在`Telegram Business`页面中向下滚动并点击`聊天机器人 (Chatbot)`。 +1. 在打开的页面中,在`机器人 URL 或用户名 (Bot URL or Username)` 字段中输入 `nmnmfunbot` 并点击 `nmBot`。 +1. 向下滚动并启用您需要的权限。 +1. 返回页面,更改将自动保存。 ## 在 iOS 版 Telegram 中设置 1. 打开 Telegram app 后,点按屏幕右下角的`设置 (Settings)` 按钮。该按钮也可能显示为您的头像。 -2. 在设置页面中向下滚动并点击 `Telegram Business`。 -3. 在`Telegram Business`页面中向下滚动并点按`聊天机器人 (Chatbot)`。 -4. 在打开的页面中,在`机器人 URL 或用户名 (Bot URL or Username)` 字段中输入 `nmnmfunbot` 并点按 `nmBot`。 -5. 向下滚动并启用您需要的权限。 -6. 返回页面,更改将自动保存。 +1. 在设置页面中向下滚动并点击 `Telegram Business`。 +1. 在`Telegram Business`页面中向下滚动并点按`聊天机器人 (Chatbot)`。 +1. 在打开的页面中,在`机器人 URL 或用户名 (Bot URL or Username)` 字段中输入 `nmnmfunbot` 并点按 `nmBot`。 +1. 向下滚动并启用您需要的权限。 +1. 返回页面,更改将自动保存。 ## 在 Telegram Desktop 中设置 1. 打开 Telegram Desktop 应用后,点击左上角的菜单按钮。 -2. 在菜单中选择`设置 (Settings)`。 -3. 在设置页面中向下滚动并点击 `Telegram Business`。 -4. 在`Telegram Business`页面中向下滚动并点击`聊天机器人 (Chatbot)`。 -5. 在打开的页面中,在`机器人 URL 或用户名 (Bot URL or Username)` 字段中输入 `nmnmfunbot` 并点击 `nmBot`。 -6. 向下滚动并启用您需要的权限。 -7. 返回页面,更改将自动保存。 +1. 在菜单中选择`设置 (Settings)`。 +1. 在设置页面中向下滚动并点击 `Telegram Business`。 +1. 在`Telegram Business`页面中向下滚动并点击`聊天机器人 (Chatbot)`。 +1. 在打开的页面中,在`机器人 URL 或用户名 (Bot URL or Username)` 字段中输入 `nmnmfunbot` 并点击 `nmBot`。 +1. 向下滚动并启用您需要的权限。 +1. 返回页面,更改将自动保存。 diff --git a/docs/nmbot-telegram/credit/credit.md b/docs/nmbot-telegram/credit/credit.md index 9f3feca..25675ff 100644 --- a/docs/nmbot-telegram/credit/credit.md +++ b/docs/nmbot-telegram/credit/credit.md @@ -1,17 +1,20 @@ # nmBot 积分 + nmBot 现已引入积分功能,您可以通过参与聊天获取积分,并在 nmBot 中参与积分活动。 ## 如何获取积分 + 您可以通过以下方式获取积分: -| 获取方式 | 获取的积分 | 说明 | -| :------------------------------ | :-------------- | :---------------- | +| 获取方式 | 获取的积分 | 说明 | +| :-------------------------------------------------------- | :-------------- | :---------------- | | 在 nmBot 已加入的群组中发送消息(除非群组关闭了积分功能) | 每条消息 1 积分 | 每日上限 100 积分 | -| 对 nmBot 签到 | 10-20 积分 | | -| 在 nmBot 面板签到 | 10-20 积分 | | -| 在 nmBot 面板使用星币赞助 | 1 星币=100 积分 | | -| 在 nmBot 中体验常驻功能 | | | -| 在 nmBot 面板参与积分活动 | | | +| 对 nmBot 签到 | 10-20 积分 | | +| 在 nmBot 面板签到 | 10-20 积分 | | +| 在 nmBot 面板使用星币赞助 | 1 星币=100 积分 | | +| 在 nmBot 中体验常驻功能 | | | +| 在 nmBot 面板参与积分活动 | | | ## 积分功能与隐私 + nmBot 会记录您的积分信息,但不会记录您的聊天记录。 diff --git a/docs/nmbot-telegram/faq/about-message-content-of-bot.md b/docs/nmbot-telegram/faq/about-message-content-of-bot.md index 88208bd..fc27a2b 100644 --- a/docs/nmbot-telegram/faq/about-message-content-of-bot.md +++ b/docs/nmbot-telegram/faq/about-message-content-of-bot.md @@ -1,13 +1,15 @@ # 关于看起来由 nmBot 发送的消息 !!! 概要 + 有时,您会在对话中看到看起来由 nmBot 发送的消息,这些消息由于内容存在明显的某种倾向、存在较大争议、涉及敏感问题或用语不当等,而被转发、转载或截图传播。有些言论可能很轻易识别为是或不是 nmBot 发送的消息,但另一些言论可能很难识别。 - + 看起来像是 nmBot 发送的消息可能不是由 nmBot 发送的。这些消息可能是由仿冒者发送的,也可能是用户自行编纂的。nmBot 无法对所有消息进行完全可靠的审查,因此 nmBot 无法保证通过其账号发送的所有消息都是合适的。 我们将在这篇文章中阐述这类消息可能的来源,以及如何识别这类消息。 ## 识别仿冒 nmBot 发送的消息 + 部分情况下,您可能会在对话中看到看起来由 nmBot 账号发送,但实际上并非由 nmBot 发送的消息。这些消息可能发送自仿冒者,他们将其个人账号、频道或机器人的名称和头像设置为与 nmBot 相似,以此来误导您。 并非所有仿冒 nmBot 的消息都是恶意的,但有些仿冒者可能会发送包含恶意链接、虚假信息或其他不当内容的消息。因此,我们建议您在收到看起来由 nmBot 发送的消息时,仔细核实消息的来源。 @@ -15,12 +17,13 @@ 要检查消息的来源,请按照以下步骤操作: 1. 若消息转发自 nmBot,单击“转发自 nmBot”文本中的 nmBot,以查看消息的来源。若消息并非转发,单击发送者的头像或名称,以查看消息的来源。 -2. 若 Telegram 客户端提示“此用户隐藏了自己的账号身份”,则该消息是由仿冒 nmBot 发送的。请注意,nmBot 技术上无法隐藏自己的账号身份。若没有看到此提示,转到下一步。 -3. 检查消息发送者的用户名。nmBot 发送的消息总是来自用户名 `@nmnmfunbot` 的账号,仿冒者则无法使用此用户名。若消息发送者的用户名不是 `@nmnmfunbot`,或没有用户名,则该消息是由仿冒者发送的。请仔细确认拼写,因为仿冒者可能会使用十分相似的用户名。 +1. 若 Telegram 客户端提示“此用户隐藏了自己的账号身份”,则该消息是由仿冒 nmBot 发送的。请注意,nmBot 技术上无法隐藏自己的账号身份。若没有看到此提示,转到下一步。 +1. 检查消息发送者的用户名。nmBot 发送的消息总是来自用户名 `@nmnmfunbot` 的账号,仿冒者则无法使用此用户名。若消息发送者的用户名不是 `@nmnmfunbot`,或没有用户名,则该消息是由仿冒者发送的。请仔细确认拼写,因为仿冒者可能会使用十分相似的用户名。 若您确认消息是由仿冒者发送的,请不要相信该消息中的文本。若您认为消息中包含不当内容,请提请管理员立即删除该消息,并及时报告到 [nmTeam 支持](../support.md)。 ## 来自 nmBot 的消息不应与 nmBot 本身混淆 + 一条可以验证由 nmBot 发送的消息不代表是由 nmBot 和 nmTeam 认可的消息。 任何人都可以轻易地通过 nmBot 的各项功能发送他们想发送的内容,例如通过“关键词回复”“定时任务(原锁定模式)”等群组管理功能、“群友互动”等互动功能和“行内功能集”等行内功能。 @@ -30,6 +33,7 @@ 基于此,nmBot 发送的任何消息都不应被视为 nmBot、nmTeam 或第三方的立场或意见。 ## nmBot 功能生成的消息可能包含意外内容 + nmBot 中存在一些功能,可根据用户传入的相关内容自动生成消息内容。 例如“nmartChat”“同频气氛组”功能可以使 nmBot 根据消息内容自动创建回复消息。这些消息由相应算法和第三方服务生成,其生成的消息可能包含意外内容,例如包含敏感词、涉及敏感问题、存在明显的某种倾向、存在较大争议或用语不当等。 @@ -37,12 +41,14 @@ nmBot 中存在一些功能,可根据用户传入的相关内容自动生成 由于技术手段的局限性,nmBot 无法保证通过其账号发送的所有消息都是合适的。若您确认消息为 nmBot 的某项功能自动生成的,且您认为消息中包含不当内容,您可以将相关情况报告到 [nmTeam 支持](../support.md)。 ## 区分用户自行编纂的消息和 nmBot 功能生成的消息 + 目前,对于转发的消息来说,用户无法准确地判断该消息是由用户自行编纂还是由 nmBot 功能生成。您可以联系 [nmTeam 支持](../support.md)辅助您作出判断。但某些情况下,通过以下方面可能可以帮助您作出判断: - **消息中大量的谩骂或辱骂性词汇**:一般而言,nmBot 功能生成的消息不会包含大量的谩骂或辱骂性词汇。nmBot 功能生成的消息可能会存在讽刺、批评,或偶尔存在少量辱骂性词汇,但不会包含大量的谩骂或辱骂性词汇。若消息中包含大量的谩骂或辱骂性词汇,较大可能是由用户自行编纂的。 - **触发消息的方式是否为 nmBot 功能**:一般情况下,您可以通过消息的上下文判断消息是否是由 nmBot 功能生成的。例如,若 nmBot 发送的消息回复了一条群组关联频道的消息,则有一定可能,该消息是“同频气氛组”功能生成的;若 nmBot 发送的消息回复的消息并非回复 nmBot 的消息,也不包含其他可能的 nmartChat 触发方式,则这条消息可能是触发了群组设置的“关键词回复”。 ## 我们如何控制 nmBot 发送的消息 + 用户通过 nmBot 发送的由其自行编纂的消息和 nmBot 功能生成的消息,均可能存在包含不当内容的情况。 根据我们的使用条款,我们不对 nmBot 服务上的内容的准确性、完整性或可靠性作出任何陈述或保证。尽管如此,我们仍然采取了措施避免 nmBot 发送不当内容。 @@ -50,6 +56,7 @@ nmBot 中存在一些功能,可根据用户传入的相关内容自动生成 对于用户在 nmBot 上设置的消息内容,您可以通过 [nmTeam 支持](../support.md)报告不当内容。如果我们确认消息内容不当,我们将立即删除该消息。 ## 避免受到仿冒消息和用户自行编纂的消息的影响 + 为了避免受到仿冒消息和用户自行编纂的消息的影响,我们建议您: - 仔细核实消息的来源,以确保消息是由 nmBot,而不是仿冒账号发送的。 @@ -58,10 +65,11 @@ nmBot 中存在一些功能,可根据用户传入的相关内容自动生成 - 必要时,联系 [nmTeam 支持](../support.md)寻求帮助。 ## 避免传播不合适的消息 + 许多情况下,nmBot 发送的消息可能会被转发、转载或截图传播。为了减少不当内容的传播对我们和其他用户的影响,我们建议您: - 在确保消息内容合适的前提下,您可以传播 nmBot 发送的消息,除非对话的设置,或更高级别的来源禁止您这样做。 - 若该消息由 nmBot 的某项功能生成,而消息中出现了略有不当,但尚不严重的内容,您可以选择传播,但我们建议您不要传播该消息,并将相关情况报告到 [nmTeam 支持](../support.md)。 - 不要传播仿冒 nmBot 的用户发送的消息。若您确认消息是由仿冒者发送的,请不要相信该消息中的文本,也不要将该消息保存或传播。 - 不要传播用户自行编纂的包含不当内容消息。若您确认消息是由用户自行编纂的,且包含不当内容,请不要相信该消息中的文本,也不要将该消息保存或传播,并将相关情况报告到 [nmTeam 支持](../support.md)。 -- 不要传播 nmBot 生成的不当消息。若您确认消息是由 nmBot 功能生成的,且包含不当内容,请不要相信该消息中的文本,也不要将该消息保存或传播,并将相关情况报告到 [nmTeam 支持](../support.md)。 \ No newline at end of file +- 不要传播 nmBot 生成的不当消息。若您确认消息是由 nmBot 功能生成的,且包含不当内容,请不要相信该消息中的文本,也不要将该消息保存或传播,并将相关情况报告到 [nmTeam 支持](../support.md)。 diff --git a/docs/nmbot-telegram/faq/buy-stars.md b/docs/nmbot-telegram/faq/buy-stars.md index 49416c9..35f91a0 100644 --- a/docs/nmbot-telegram/faq/buy-stars.md +++ b/docs/nmbot-telegram/faq/buy-stars.md @@ -30,16 +30,16 @@ Telegram 使用 TON 作为加密货币支付方式。 在购买 Telegram 星币之前,您需要一个 TON 钱包。如果您还没有 TON 钱包,请按照如下步骤创建: 1. 下载并安装 TONKeeper 应用。 -请使用您的手机下载 TONKeeper 应用。以下是下载链接: + 请使用您的手机下载 TONKeeper 应用。以下是下载链接: - [App Store](https://apps.apple.com/us/app/tonkeeper-ton-wallet/id1587742107) - [Google Play](https://play.google.com/store/apps/details?id=com.ton_keeper) -2. 创建 TON 钱包。 -打开 TONKeeper 应用,按照应用提示创建 TON 钱包。 +1. 创建 TON 钱包。 + 打开 TONKeeper 应用,按照应用提示创建 TON 钱包。 -3. 转入余额到 TON 钱包。 -在 TONKeeper 应用中,您可以通过扫描二维码或输入地址的方式转入余额到 TON 钱包。 +1. 转入余额到 TON 钱包。 + 在 TONKeeper 应用中,您可以通过扫描二维码或输入地址的方式转入余额到 TON 钱包。 点按应用主页的“接收”按钮,即可查看您的 TON 钱包地址。 请从您的加密货币交易所或其他钱包转入 TON 余额到 TON 钱包。 @@ -49,9 +49,9 @@ Telegram 使用 TON 作为加密货币支付方式。 在您的 TON 钱包中有余额后,您可以购买 Telegram 星币。 1. 在浏览器中打开链接 [https://fragment.com/stars](https://fragment.com/stars)。 -2. 在页面右上角点按“Connect TON”按钮(在手机上,您可能需要先点击右上角的菜单按钮),按照页面提示连接您的 TON 钱包。 -3. 在“Choose recipient”输入框,输入您的 Telegram 用户名。 -如果您没有用户名或者不想输入,可以点按右上角的“Connect Telegram”按钮(在手机上,您需要先点击右上角的菜单按钮),按照页面提示连接您的 Telegram 账号。 -4. 在页面上输入或选择您要购买的星币数量。 -5. 滚动页面到底部,点按“Buy xx Telegram Stars”按钮。 -6. 按照页面提示完成支付。 +1. 在页面右上角点按“Connect TON”按钮(在手机上,您可能需要先点击右上角的菜单按钮),按照页面提示连接您的 TON 钱包。 +1. 在“Choose recipient”输入框,输入您的 Telegram 用户名。 + 如果您没有用户名或者不想输入,可以点按右上角的“Connect Telegram”按钮(在手机上,您需要先点击右上角的菜单按钮),按照页面提示连接您的 Telegram 账号。 +1. 在页面上输入或选择您要购买的星币数量。 +1. 滚动页面到底部,点按“Buy xx Telegram Stars”按钮。 +1. 按照页面提示完成支付。 diff --git a/docs/nmbot-telegram/faq/cannot-access-panel.md b/docs/nmbot-telegram/faq/cannot-access-panel.md index d2b8b1b..95cc8a9 100644 --- a/docs/nmbot-telegram/faq/cannot-access-panel.md +++ b/docs/nmbot-telegram/faq/cannot-access-panel.md @@ -1,16 +1,17 @@ # 如果您无法访问 nmBot 面板 + > 了解如果您无法访问 nmBot 面板,该怎么做。 如果您无法访问 nmBot 面板,以下是一些可能的解决方案,希望能帮助您解决问题: -- **检查网络连接:**请确保您的设备已连接到互联网,可以访问其他网站或服务。 +- \*\*检查网络连接:\*\*请确保您的设备已连接到互联网,可以访问其他网站或服务。 -- **清除浏览器缓存:**可能是您的浏览器缓存导致 nmBot 面板无法打开。请清除浏览器缓存并尝试重新打开 nmBot 面板。 +- \*\*清除浏览器缓存:\*\*可能是您的浏览器缓存导致 nmBot 面板无法打开。请清除浏览器缓存并尝试重新打开 nmBot 面板。 -- **尝试使用其他浏览器:**有时,浏览器的一些设置或插件可能会阻止 nmBot 面板的加载。尝试在其他浏览器中打开 nmBot 面板,以查看是否存在同样的问题。 +- \*\*尝试使用其他浏览器:\*\*有时,浏览器的一些设置或插件可能会阻止 nmBot 面板的加载。尝试在其他浏览器中打开 nmBot 面板,以查看是否存在同样的问题。 -- **检查防火墙设置:**如果您使用防火墙软件,可能会阻止 nmBot 面板的访问。请检查防火墙设置以确保 nmBot 面板被允许通过。 +- \*\*检查防火墙设置:\*\*如果您使用防火墙软件,可能会阻止 nmBot 面板的访问。请检查防火墙设置以确保 nmBot 面板被允许通过。 -- **查看 nmBot 面板服务状态:**如果 nmBot 面板服务出现问题,则你可能无法访问 nmBot 面板。你可以在 https://status.nmteam.xyz 查询 nmTeam 服务的系统状态。 +- \*\*查看 nmBot 面板服务状态:\*\*如果 nmBot 面板服务出现问题,则你可能无法访问 nmBot 面板。你可以在 https://status.nmteam.xyz 查询 nmTeam 服务的系统状态。 如果上述解决方案不能解决问题,请提供更多详细信息,例如错误消息、操作系统、浏览器版本等。我们将尽快为您提供更多帮助。 diff --git a/docs/nmbot-telegram/faq/cannot-log-in-to-panel.md b/docs/nmbot-telegram/faq/cannot-log-in-to-panel.md index fd9df0b..cfea67c 100644 --- a/docs/nmbot-telegram/faq/cannot-log-in-to-panel.md +++ b/docs/nmbot-telegram/faq/cannot-log-in-to-panel.md @@ -1,10 +1,13 @@ # 如果您无法在 nmBot 面板登录 + > 了解如果您无法在 nmBot 面板登录,该怎么做。 ## 卡在加载图标 + 如果你卡在加载图标,并且未看到“使用 Telegram 登录”按钮,你需要确认你的网络连接正常,并可以访问 Telegram 网站。如果你无法访问 Telegram 网站,你可能需要使用代理或 VPN 来访问 Telegram 网站。 ## 在 Telegram 迷你 App 内打开时,出现了登录页面 + 有小概率用户会偶尔遇到在迷你 App 中打开 nmBot 面板时,nmBot 面板没有正常接收到 Telegram 传递的初始化数据,导致无法通过迷你 App 数据登录的情况。 若在 Telegram 迷你 App 内打开 nmBot 面板时,没有显示登录中的页面,而是出现了登录页面,则表明出现了错误。遇到这种情况时,请关闭迷你 App 后再重新打开。在 /panel 消息或者入群验证时,一般也会提供在浏览器中打开的选项,可以尝试使用该选项在浏览器中打开页面。 @@ -12,17 +15,20 @@ 该问题与 nmTeam 账号无关。若要为 Telegram 账号绑定 nmTeam 账号,您必须先使用 Telegram 登录 nmBot 面板。 ## 出现提示“由于服务器错误,登录失败” -首先请确认你的网络连接正常,并尝试多试几次。 + +首先请确认你的网络连接正常,并尝试多试几次。\ 另请确认 nmTeam 服务的系统状态正常。你可以在 https://nmteam.xyz/status 查询 nmTeam 服务的系统状态。 ## 允许第三方 Cookie + 出于隐私和安全性考虑,许多浏览器禁止第三方 Cookie。但 Telegram 可能需要使用第三方 Cookie 来验证您的身份。如果您的浏览器禁用了第三方 Cookie,您可能无法在 nmBot 面板登录。请允许第三方 Cookie 并尝试重新登录。 ## 其他可用措施 -- **检查网络连接:**首先,请确保您的设备已连接到互联网,并且网络连接是稳定的。尝试打开其他网页或应用程序,以确保您的网络连接正常工作。 -- **确认你可以访问 Telegram:**某些国家或地区可能会阻止访问 Telegram。请尝试在浏览器中打开 Telegram 网站,以查看是否可以访问。如果您无法访问 Telegram 网站,则可能需要代理才能访问 Telegram。您可以尝试使用代理或 VPN 来访问 Telegram 网站。 -- **清除浏览器缓存:**有时,浏览器缓存可能导致登录问题。请尝试清除浏览器缓存并删除历史记录。在清除缓存后,重新启动浏览器并尝试登录 nmBot 面板。 -- **更新浏览器:**如果您正在使用过时的浏览器版本,可能会导致登录问题。尝试更新您的浏览器到最新版本,并再次尝试登录 nmBot 面板。 -- **禁用浏览器插件:**某些浏览器插件可能与 nmBot 面板的登录功能不兼容。尝试禁用或暂时移除浏览器插件,并重新加载 nmBot 面板,然后尝试登录。 -- **使用其他浏览器或设备:**如果您尝试了以上步骤仍然无法登录,可以尝试使用其他浏览器或设备登录 nmBot 面板。有时,特定的浏览器或设备可能与平台不兼容,更换设备或使用不同的浏览器可能解决问题。 -- **联系技术支持:**如果您尝试了以上所有步骤仍然无法解决登录问题,请不要担心。您可以联系 nmTeam 支持团队,详细描述您遇到的问题,我们将竭诚为您提供进一步的帮助和指导。 + +- \*\*检查网络连接:\*\*首先,请确保您的设备已连接到互联网,并且网络连接是稳定的。尝试打开其他网页或应用程序,以确保您的网络连接正常工作。 +- \*\*确认你可以访问 Telegram:\*\*某些国家或地区可能会阻止访问 Telegram。请尝试在浏览器中打开 Telegram 网站,以查看是否可以访问。如果您无法访问 Telegram 网站,则可能需要代理才能访问 Telegram。您可以尝试使用代理或 VPN 来访问 Telegram 网站。 +- \*\*清除浏览器缓存:\*\*有时,浏览器缓存可能导致登录问题。请尝试清除浏览器缓存并删除历史记录。在清除缓存后,重新启动浏览器并尝试登录 nmBot 面板。 +- \*\*更新浏览器:\*\*如果您正在使用过时的浏览器版本,可能会导致登录问题。尝试更新您的浏览器到最新版本,并再次尝试登录 nmBot 面板。 +- \*\*禁用浏览器插件:\*\*某些浏览器插件可能与 nmBot 面板的登录功能不兼容。尝试禁用或暂时移除浏览器插件,并重新加载 nmBot 面板,然后尝试登录。 +- \*\*使用其他浏览器或设备:\*\*如果您尝试了以上步骤仍然无法登录,可以尝试使用其他浏览器或设备登录 nmBot 面板。有时,特定的浏览器或设备可能与平台不兼容,更换设备或使用不同的浏览器可能解决问题。 +- \*\*联系技术支持:\*\*如果您尝试了以上所有步骤仍然无法解决登录问题,请不要担心。您可以联系 nmTeam 支持团队,详细描述您遇到的问题,我们将竭诚为您提供进一步的帮助和指导。 diff --git a/docs/nmbot-telegram/faq/channel-verification-not-working.md b/docs/nmbot-telegram/faq/channel-verification-not-working.md index 8fb175d..1e05f66 100644 --- a/docs/nmbot-telegram/faq/channel-verification-not-working.md +++ b/docs/nmbot-telegram/faq/channel-verification-not-working.md @@ -1,22 +1,27 @@ # 如果“频道验证”在您的群组中不工作 + > 如果您的群组中的“频道验证”不起作用,了解要采取的操作。 ## 检查 nmBot 入群验证的设置 + 请确保您的群组中已启用 nmBot 的“频道验证”功能。如果没有启用该功能,则“频道验证”功能将不会开启。 要确认“频道验证”功能是否已启用,请在要启用“频道验证”的群组中发送 `/config` 指令并打开“入群认证”,或在 nmBot 面板中打开群组设置 > “群组管理功能” > “入群验证”。如果验证类型为“频道验证”,则“频道验证”功能已启用。否则,如果设置列表中显示了其他“入群验证”设置项或“入群验证”字样,则“频道验证”功能未启用。要启用“频道验证”功能,请选择“入群验证”设置,再选择“频道验证”。 ## 检查入群验证频道是否设置 + 请确保您的群组中已设置入群验证频道。如果没有设置入群验证频道,则“频道验证”功能将不会开启。 要确认是否设置了入群验证频道,你可以在 nmBot 面板中打开群组设置 > “群组管理功能” > “入群验证”并查看验证频道设置。如果设置列表中显示的“入群验证频道”是一个非 0 的负数,则你的设置正确。否则,你需要在要启用“频道验证”的群组中设置入群验证频道。要了解如何设置入群验证频道,请参阅[入群验证](../group/join-verification.md)文档。 ## 检查 nmBot 在群组中是否有足够的权限 + 请确保 nmBot 在群组中有足够的权限。如果 nmBot 在群组中没有足够的权限,则“频道验证”功能将无法启用。 如果你能够在群组中启用 nmBot 群组管理功能,则 nmBot 在群组中应该有足够的权限。 ## 检查 nmBot 在频道中是否有足够的权限 + 请确保 nmBot 在频道中有足够的权限。如果 nmBot 在频道中没有足够的权限,则“频道验证”功能将无法启用。 如果你能够通过 `/sc` 指令设置入群验证频道,则 nmBot 在频道中应该有足够的权限。 diff --git a/docs/nmbot-telegram/faq/why-my-message-deleted.md b/docs/nmbot-telegram/faq/why-my-message-deleted.md index a5d255a..c68ba1e 100644 --- a/docs/nmbot-telegram/faq/why-my-message-deleted.md +++ b/docs/nmbot-telegram/faq/why-my-message-deleted.md @@ -11,7 +11,7 @@ - **[关键词回复](../group/key-word-reply.md)**:您发送的消息触发了群组的关键词回复,且该关键词回复设置了删除触发的消息。 - **[发言频率限制](../group/speak-limitation.md)**:您发送的消息过于频繁,超出了群组管理员设置的发言频率限制。 - **[骚扰拦截](../group/anti-spam.md)**:您发送的消息被 nmBot 识别为骚扰消息,因此被删除。 -若您曾联系过 nmBot,nmBot 将向您发送私信告知您这一情况。 + 若您曾联系过 nmBot,nmBot 将向您发送私信告知您这一情况。 - **自定义骚扰拦截规则**:群组管理员设置了自定义骚扰拦截规则,您发送的消息被识别为骚扰消息,因此被删除。 - **群组管理员的操作**:群组管理员可能使用 nmBot 的功能手动删除了您的消息,但您对此并不知情。 @@ -31,12 +31,12 @@ 大多数群组中的限制功能对群组管理员是豁免的。但在以下情况下,群组管理员的消息也可能被删除: - **[关键词回复](../group/key-word-reply.md)**:群组管理员发送的消息触发了群组的关键词回复,且该关键词回复未设置“忽略群组管理员”并设置了删除触发的消息。 -在这种情况下,群组管理员的消息也会被删除。 + 在这种情况下,群组管理员的消息也会被删除。 - **使用了匿名频道身份发送消息**:群组管理员使用了频道身份匿名发送消息。 -在这种情况下,即使启用了“[频道透视眼](../group/channel-perspective-eye.md)”功能,该消息也不会受到管理员豁免。 -消息删除的具体原因与[普通用户](#对于普通用户)相同。 -若希望以频道身份发送消息,您可以使用 /white 指令将频道身份添加到白名单中。 + 在这种情况下,即使启用了“[频道透视眼](../group/channel-perspective-eye.md)”功能,该消息也不会受到管理员豁免。 + 消息删除的具体原因与[普通用户](#%E5%AF%B9%E4%BA%8E%E6%99%AE%E9%80%9A%E7%94%A8%E6%88%B7)相同。 + 若希望以频道身份发送消息,您可以使用 /white 指令将频道身份添加到白名单中。 ### 解决措施 -请检查上述情况,以确定您的消息被删除的原因。 \ No newline at end of file +请检查上述情况,以确定您的消息被删除的原因。 diff --git a/docs/nmbot-telegram/group/action-log.md b/docs/nmbot-telegram/group/action-log.md index 287c660..ecbcf76 100644 --- a/docs/nmbot-telegram/group/action-log.md +++ b/docs/nmbot-telegram/group/action-log.md @@ -1,25 +1,30 @@ # 操作记录 -nmBot 可以记录群组管理员通过 nmBot 完成的操作以及部分群组中的其他变动。群组管理员可以在 nmBot 面板查找这些操作。 +nmBot 可以记录群组管理员通过 nmBot 完成的操作以及部分群组中的其他变动。群组管理员可以在 nmBot 面板查找这些操作。 ## 开启条件 + 群组中有至少 200 名成员后,方可开启操作记录功能。 ## 开启方法 + 群组管理员可以在 nmBot `/config` 菜单或 nmBot 面板打开“操作记录”功能。 ## 提示 + 并非所有操作都将记录到操作记录中。 ## 绑定操作记录频道 -群组管理员可以为 nmBot 绑定操作记录频道。当有管理员通过 nmBot 完成操作时,nmBot 将会在操作记录频道中发送操作记录消息。 + +群组管理员可以为 nmBot 绑定操作记录频道。当有管理员通过 nmBot 完成操作时,nmBot 将会在操作记录频道中发送操作记录消息。\ 要绑定操作记录频道,请按照此步骤操作: 1. 在要绑定的频道中邀请 nmBot,并授予发送消息权限。 -2. 获取频道的 ID。可以将频道消息转发至 nmBot 后对转发的消息回复 `/id` 来获取频道 ID。 -3. 打开 nmBot 面板,选择群组,向下滚动并点击“设置操作记录频道”,并输入频道 ID。 -4. nmBot 面板将提示您操作结果。 +1. 获取频道的 ID。可以将频道消息转发至 nmBot 后对转发的消息回复 `/id` 来获取频道 ID。 +1. 打开 nmBot 面板,选择群组,向下滚动并点击“设置操作记录频道”,并输入频道 ID。 +1. nmBot 面板将提示您操作结果。 ## 在 nmBot 面板查看操作记录 -群组管理员可以在 nmBot 面板查看操作记录。 -打开 nmBot 面板,进入群组,向下滚动,找到并点击“操作记录”按钮即可查看操作记录。 + +群组管理员可以在 nmBot 面板查看操作记录。\ +打开 nmBot 面板,进入群组,向下滚动,找到并点击“操作记录”按钮即可查看操作记录。 diff --git a/docs/nmbot-telegram/group/admin-rights-and-features.md b/docs/nmbot-telegram/group/admin-rights-and-features.md index 964440b..797a38c 100644 --- a/docs/nmbot-telegram/group/admin-rights-and-features.md +++ b/docs/nmbot-telegram/group/admin-rights-and-features.md @@ -5,6 +5,7 @@ nmBot 不需要管理员权限即可在群组中正常工作,但许多高级 下面列出了群组内 nmBot 相关功能和权限的对应关系。 ## 不需要管理员权限 + - nmartChat (不支持关闭) - 呼叫群组管理员 - 欢乐复读 @@ -12,6 +13,7 @@ nmBot 不需要管理员权限即可在群组中正常工作,但许多高级 - 入群欢迎 ## 需要基础管理员权限(删除消息和限制用户) + - 管理功能(/ban, /block, /warn 等) - 自动删除消息功能(删除频道身份消息、删除特定类型消息等) - banme @@ -20,5 +22,6 @@ nmBot 不需要管理员权限即可在群组中正常工作,但许多高级 - 设置入群验证 ## 额外需要置顶消息权限 + - 自动解除关联频道置顶 -- /pin 和 /unpin 指令置顶消息 \ No newline at end of file +- /pin 和 /unpin 指令置顶消息 diff --git a/docs/nmbot-telegram/group/anti-spam.md b/docs/nmbot-telegram/group/anti-spam.md index cb66824..a7ca325 100644 --- a/docs/nmbot-telegram/group/anti-spam.md +++ b/docs/nmbot-telegram/group/anti-spam.md @@ -3,9 +3,11 @@ nmBot 可在群组自动识别消息内容。您可以设置在 nmBot 识别到骚扰消息时,删除消息,或踢出发送消息的用户。 ## 配置 -nmBot 内置了多个智能识别系统,您可以在 nmBot 群组管理功能中配置。 -在群组中发送 `/config`,或登录 nmBot 面板并打开目标群组的配置页面,在启用 nmBot 群组管理功能后,即可在“消息限制与智能识别”->“智能识别和骚扰拦截”中配置。 + +nmBot 内置了多个智能识别系统,您可以在 nmBot 群组管理功能中配置。\ +在群组中发送 `/config`,或登录 nmBot 面板并打开目标群组的配置页面,在启用 nmBot 群组管理功能后,即可在“消息限制与智能识别”->“智能识别和骚扰拦截”中配置。\ 您可以设置识别到骚扰消息采取操作时是否发送提示消息,也可以针对不同消息识别系统设置不同的处理方式,例如删除或封禁。 + ## nmBot 智能骚扰拦截 nmBot 智能现可驱动骚扰拦截功能,将骚扰拦截效果提升约 5 倍。 @@ -27,10 +29,12 @@ nmBot 智能现可驱动骚扰拦截功能,将骚扰拦截效果提升约 5 nmBot 骚扰拦截功能可识别以下内容: ### 用户信息 + - 用户名称和简介中的骚扰内容 - 入群用户的表情状态所属表情包名称 ### 消息内容 + - 消息文本内容 - 用户发送的贴纸是否为骚扰消息 - 用户发送的消息所引用的其他对话是否为骚扰消息 @@ -41,6 +45,7 @@ nmBot 骚扰拦截功能可识别以下内容: 订阅 nmBot+ 的群组可以在骚扰拦截和入群验证自动拒绝骚扰用户入群功能中补充自己的匹配规则。 自定义规则支持识别以下内容: + - 用户名称 - 用户简介 - 用户发送的贴纸所属贴纸包名称 @@ -51,8 +56,10 @@ nmBot 骚扰拦截功能可识别以下内容: nmBot 支持集成 Combot Anti-Spam (CAS) 骚扰拦截功能,可自动识别并处理被 CAS 标记的骚扰用户。 ## 允许 nmTeam 支持解封误封用户 + 有时,nmBot 的识别功能可能会误封用户。您可以在 nmBot 群组管理功能中配置,允许 nmTeam 支持解封误封用户。 请访问这篇文章了解该功能的详细信息:[解封误封用户](./unban-users-banned-by-anti-spam.md) ## 关于骚扰拦截功能的常见问题 + 您可以[在此](../faq/spam.md)查看关于骚扰拦截功能的常见问题。 diff --git a/docs/nmbot-telegram/group/banme.md b/docs/nmbot-telegram/group/banme.md index a809156..f52b1f8 100644 --- a/docs/nmbot-telegram/group/banme.md +++ b/docs/nmbot-telegram/group/banme.md @@ -4,4 +4,4 @@ banme 是一项趣味功能,允许用户在群组内发送 `/banme` 指令获 ## 启用 banme -赋予 nmBot 删除消息和封禁用户权限后,群组管理员可以在 /config 菜单和 nmBot 面板的“群组娱乐功能”设置中启用或关闭 banme。 \ No newline at end of file +赋予 nmBot 删除消息和封禁用户权限后,群组管理员可以在 /config 菜单和 nmBot 面板的“群组娱乐功能”设置中启用或关闭 banme。 diff --git a/docs/nmbot-telegram/group/channel-perspective-eye.md b/docs/nmbot-telegram/group/channel-perspective-eye.md index 0fa1824..1c7fb38 100644 --- a/docs/nmbot-telegram/group/channel-perspective-eye.md +++ b/docs/nmbot-telegram/group/channel-perspective-eye.md @@ -8,13 +8,15 @@ nmBot 新增了一项全新功能,允许您使用频道身份执行在之前 启用该功能时,您可以使用 `/ban`, `/block` 等命令,也可以执行 `/banme` 等操作。nmBot 将会将消息发送者视为您的个人账号(在特定功能中除外)。 -> 💡 “频道透视眼”功能可能使其他人能看到您的个人账号身份。若您不希望他人知道您的个人账号身份,请勿启用该功能。 -> 💡 “频道透视眼”功能对所有群组均有效。 -> 💡 “频道透视眼”功能获取的数据有一定缓存期限。在转让频道前,请务必关闭“频道透视眼”功能。 +> 💡 “频道透视眼”功能可能使其他人能看到您的个人账号身份。若您不希望他人知道您的个人账号身份,请勿启用该功能。\ +> 💡 “频道透视眼”功能对所有群组均有效。\ +> 💡 “频道透视眼”功能获取的数据有一定缓存期限。在转让频道前,请务必关闭“频道透视眼”功能。 ## 启用和关闭“频道透视眼”功能 + 在群组中发送 `/pe` 指令即可启用或关闭“频道透视眼”功能。 ## 初始化“频道透视眼”功能 + 您需要将 nmBot 添加到您的频道中(不需要任何权限),才能使用“频道透视眼”功能。 -若 nmBot 检测到您的频道中没有 nmBot,“频道透视眼”功能将自动关闭。 \ No newline at end of file +若 nmBot 检测到您的频道中没有 nmBot,“频道透视眼”功能将自动关闭。 diff --git a/docs/nmbot-telegram/group/delete-message-by-type.md b/docs/nmbot-telegram/group/delete-message-by-type.md index e8030a4..8941e95 100644 --- a/docs/nmbot-telegram/group/delete-message-by-type.md +++ b/docs/nmbot-telegram/group/delete-message-by-type.md @@ -45,4 +45,4 @@ nmBot+ 用户可以设置让 nmBot 在删除消息的同时,在群组中自动 新增的“主动封禁入群用户频道”功能可在新成员加入群组时即尝试封禁其频道,避免成员尝试以频道身份发言。 -此功能可有效防止用户以频道身份在群组中发送骚扰消息或广告。 \ No newline at end of file +此功能可有效防止用户以频道身份在群组中发送骚扰消息或广告。 diff --git a/docs/nmbot-telegram/group/emergency-mode.md b/docs/nmbot-telegram/group/emergency-mode.md index b2d33fd..0637b07 100644 --- a/docs/nmbot-telegram/group/emergency-mode.md +++ b/docs/nmbot-telegram/group/emergency-mode.md @@ -18,6 +18,7 @@ nmBot 提供两类群组紧急模式: 当群组出现消息刷屏等情况时,可以启用静默模式及时阻止 nmBot 发送消息。 !!! warning "注意" + “入群验证”等关键功能的消息也将停止发送,这可能导致新成员无法通过验证。 ### 离线模式 @@ -26,6 +27,7 @@ nmBot 提供两类群组紧急模式: 启用离线模式后,nmBot 将完全停止响应群组中的所有活动。 !!! note "提示" + 启用紧急模式时,部分不基于群组事件的功能操作可能仍会执行。nmBot 会在群组中发送消息以告知该模式已启用。 ## 启用紧急模式 diff --git a/docs/nmbot-telegram/group/group-setup.md b/docs/nmbot-telegram/group/group-setup.md index 45d866a..46891fd 100644 --- a/docs/nmbot-telegram/group/group-setup.md +++ b/docs/nmbot-telegram/group/group-setup.md @@ -1,18 +1,18 @@ # 授予 nmBot 管理员权限并配置功能 -nmBot 管理功能仅在授予 nmBot 必要的管理员权限后可用。 +nmBot 管理功能仅在授予 nmBot 必要的管理员权限后可用。 -若要使用 nmBot 管理功能,请为 nmBot 授予删除消息、封禁用户、置顶消息(如果需要)等权限。 +若要使用 nmBot 管理功能,请为 nmBot 授予删除消息、封禁用户、置顶消息(如果需要)等权限。 -若 nmBot 已拥有管理员权限,拥有更改群组信息权限的管理员可使用 `/config` 命令打开 nmBot 设置菜单以启用和关闭 nmBot 功能。 +若 nmBot 已拥有管理员权限,拥有更改群组信息权限的管理员可使用 `/config` 命令打开 nmBot 设置菜单以启用和关闭 nmBot 功能。 ->💡 启用“管理功能”后,才可以控制 nmBot 聊天功能(nmartChat)的开启和关闭。 +> 💡 启用“管理功能”后,才可以控制 nmBot 聊天功能(nmartChat)的开启和关闭。 ->💡 启用“管理功能”后,nmBot 将响应 `/ban`, `/block`, `/pin` 等群组管理命令。 +> 💡 启用“管理功能”后,nmBot 将响应 `/ban`, `/block`, `/pin` 等群组管理命令。 ## 在 Telegram 中授予 nmBot 管理员权限 ->💡 您需要是有添加管理员权限的群组管理员,才能授予 nmBot 管理员权限。 +> 💡 您需要是有添加管理员权限的群组管理员,才能授予 nmBot 管理员权限。 以 Telegram Desktop 为例说明操作方法,您可以参考以下步骤设置。 @@ -40,9 +40,9 @@ nmBot 管理功能仅在授予 nmBot 必要的管理员权限后可用。 您可以在添加 nmBot 到群组时,直接设置 nmBot 为管理员。 ->💡 您需要是有添加管理员权限的群组管理员,才能通过这种方式添加 nmBot。 +> 💡 您需要是有添加管理员权限的群组管理员,才能通过这种方式添加 nmBot。 ->💡 若要在 Telegram 官方翻译平台关联群组使用 nmBot,必须通过这种方式来初始化。 +> 💡 若要在 Telegram 官方翻译平台关联群组使用 nmBot,必须通过这种方式来初始化。 以 Telegram iOS 版为例说明操作方法,您可以参考以下步骤设置。 diff --git a/docs/nmbot-telegram/group/join-verification.md b/docs/nmbot-telegram/group/join-verification.md index 176acf8..1eea264 100644 --- a/docs/nmbot-telegram/group/join-verification.md +++ b/docs/nmbot-telegram/group/join-verification.md @@ -50,4 +50,5 @@ nmBot 支持设置自动通过 Telegram Premium 用户入群或拒绝非 Telegra 订阅 nmBot+ 的群组可在 nmBot 面板中查看入群验证历史记录,可查看 15 天内的历史记录。 ## 疑难解答 + - [如果“频道验证”在您的群组中不工作](../faq/channel-verification-not-working.md) diff --git a/docs/nmbot-telegram/group/key-word-reply.md b/docs/nmbot-telegram/group/key-word-reply.md index 1b47c96..9e90626 100644 --- a/docs/nmbot-telegram/group/key-word-reply.md +++ b/docs/nmbot-telegram/group/key-word-reply.md @@ -43,6 +43,7 @@ 链接看起来像这样:`https://t.me/addstickers/UtyaDuck`,其中 `UtyaDuck` 就是贴纸包名称。 !!! note + 请不要将贴纸包名称与贴纸包标题混淆。贴纸包标题是显示在贴纸包界面上方的名称。 ### 匹配消息发送者 diff --git a/docs/nmbot-telegram/group/members-interaction.md b/docs/nmbot-telegram/group/members-interaction.md index 812fde9..f0307a8 100644 --- a/docs/nmbot-telegram/group/members-interaction.md +++ b/docs/nmbot-telegram/group/members-interaction.md @@ -1,30 +1,38 @@ # 群友互动 -在群组中,以 nmBot 为桥梁,与其他群成员进行亲切友好的互动——无需担心密接感染 COVID-19。 + +在群组中,以 nmBot 为桥梁,与其他群成员进行亲切友好的互动——无需担心密接感染 COVID-19。\ 你可能在其他机器人上见过这种形式,但 nmBot 有一些新的东西——让我们一睹为快。 ## “群友互动”的触发方式 -如果你所在群组的管理员启用了“群友互动”功能,你只需要尝试回复给你想互动的对象,并且随便写点什么,就能立刻看到效果。 + +如果你所在群组的管理员启用了“群友互动”功能,你只需要尝试回复给你想互动的对象,并且随便写点什么,就能立刻看到效果。\ ![](../img/interaction_example.png) -看,它就这样工作了。 +看,它就这样工作了。\ 当然,你不一定非要对别人出这招,你还可以对自己使用,谁都不回复就可以了——对于社恐人士非常友好! ![](../img/interaction_example2.png) 你回复 nmBot 发送的消息时,nmBot 会自动识别到你要回复给群友,所以你不必回复给群友发送的消息,回复 `xx xx 了 xx!` 就可以啦。 ## 让我们来看看高级点的操作 -不觉得感叹号前面空空的吗?让我们试试在那里加些什么。 + +不觉得感叹号前面空空的吗?让我们试试在那里加些什么。\ 只需要打一个空格,并在空格后写点什么,就能把文字加到群友后面了。 ![](../img/interaction_example3.png) -……虽然不知道什么路由器那么贵,不过我们已经达到目标了。 -可是,如果要在前半部分想打一个空格,该怎么办? +……虽然不知道什么路由器那么贵,不过我们已经达到目标了。\ +可是,如果要在前半部分想打一个空格,该怎么办?\ 你先别急,看完下一节,你一定会明白的。 ## 往消息里再加点料 + 话不多说,我们来看这个例子: ![](../img/interaction_example4.png) -```/$he的道歉书:由于我买了$space4299$space的路由器,让 非常生气,本人诚挚道歉,但死不悔改。 -$from -$date``` -哦,这些 `$` 开头的都是什么东西?为什么我没有在 nmBot 的消息中看到? + +```text +/$he的道歉书:由于我买了$space4299$space的路由器,让 非常生气,本人诚挚道歉,但死不悔改。 +$from +$date +``` + +哦,这些 `$` 开头的都是什么东西?为什么我没有在 nmBot 的消息中看到?\ 不要惊讶,这些是可在消息中任意插入的特殊参数。 | 参数 | 作用 | @@ -38,6 +46,6 @@ $date``` | `$hl`, `$hidele` | 忽略消息自带的“了” | | `$space` | 插入空格 | -* [“群友互动”功能不再支持参数 $hf ($hidefrom) 和 $ht ($hideto, $hidetarget)。](https://nmbotdoc.nmnm.fun/update-log/2023-04/#2023-4-19-1454) +- [“群友互动”功能不再支持参数 $hf ($hidefrom) 和 $ht ($hideto, $hidetarget)。](https://nmbotdoc.nmnm.fun/update-log/2023-04/#2023-4-19-1454) -我想,现在你应该会插入空格了。可不要眼高手低,快去群组试试手吧! \ No newline at end of file +我想,现在你应该会插入空格了。可不要眼高手低,快去群组试试手吧! diff --git a/docs/nmbot-telegram/group/reg-panel.md b/docs/nmbot-telegram/group/reg-panel.md index f3a250f..ac5bf7c 100644 --- a/docs/nmbot-telegram/group/reg-panel.md +++ b/docs/nmbot-telegram/group/reg-panel.md @@ -1,14 +1,21 @@ # 在 nmBot 面板管理群组(若面板中找不到群组) + 您需要先在群组中注册,然后才能在 nmBot 面板管理该群组。 + ## 自动注册 -默认情况下,您在群组中被授予管理员权限时,nmBot 会自动为您在面板注册群组。 + +默认情况下,您在群组中被授予管理员权限时,nmBot 会自动为您在面板注册群组。\ 发送 `/config`, `/panel` 等指令时,也会自动为您在面板注册群组。 + ## 在 nmBot 面板直接注册(新版 nmBot 面板) -在新版 nmBot 面板中你可以直接在面板上注册新群组。 -“手动添加群组”按钮显示在“群组”页面的右上角。 -![image](https://github.com/nm-Team/Support/assets/55237525/00486fd9-719d-44aa-a7fd-24cc80a471ec) -点击该按钮后,你需要按照提示输入要添加的群组的 ID。你可以在群组中发送 /id 来获取群组 ID。 -![image](https://github.com/nm-Team/Support/assets/55237525/e6adedeb-1a63-4529-8173-f408b43a0790) + +在新版 nmBot 面板中你可以直接在面板上注册新群组。\ +“手动添加群组”按钮显示在“群组”页面的右上角。\ +![image](https://github.com/nm-Team/Support/assets/55237525/00486fd9-719d-44aa-a7fd-24cc80a471ec)\ +点击该按钮后,你需要按照提示输入要添加的群组的 ID。你可以在群组中发送 /id 来获取群组 ID。\ +![image](https://github.com/nm-Team/Support/assets/55237525/e6adedeb-1a63-4529-8173-f408b43a0790)\ 如果这是你管理的群组,你将可以在“群组”页面的最下方看到你新添加的群组。 + ## 手动注册 + 请在需要注册的群组中发送 /regadmin 命令,再次进入 nmBot 面板即可找到群组。 diff --git a/docs/nmbot-telegram/group/reply-channel.md b/docs/nmbot-telegram/group/reply-channel.md index b6e5454..cbad0eb 100644 --- a/docs/nmbot-telegram/group/reply-channel.md +++ b/docs/nmbot-telegram/group/reply-channel.md @@ -1,6 +1,7 @@ -# 同频气氛组 +# 同频气氛组 !!! note "功能介绍" + 使用“同频气氛组”功能可以使 nmBot 第一时间根据消息内容回复群组关联频道发送的消息,增强频道和关联群组的聊天氛围。 您可以在 nmBot `/config` 菜单和 nmBot 面板启用“同频气氛组”功能。 @@ -8,7 +9,8 @@ 启用后,nmBot 将在群组关联频道广播包含文本或贴纸的帖子后,根据帖子内容智能生成回复,并发送到消息评论区中。 !!! success "启用“同频气氛组”功能实例" - ![](../img/reply-channel_eg.png) + + ![](../img/reply-channel_eg.png) ## nmBot 智能同频气氛组 (Beta 版) @@ -23,16 +25,16 @@ nmBot 同频气氛组现已支持选择语气偏好。 目前支持的语气偏好有: -| 语气偏好 | 描述 | -| --- | --- | -| 随机(默认) | nmBot 将随机选择以下回复频道消息的语气。 | -| 称赞 | nmBot 将赞同频道消息的内容。 | -| 讽刺 | nmBot 将使用反对和讽刺的语气回复频道消息。 | -| 热情 | nmBot 将使用热情的语气回复频道消息。 | -| 猫娘 | nmBot 将使用猫娘语气回复频道消息。 | -| 病娇 | nmBot 将使用病娇语气回复频道消息。 | -| 吧友 | nmBot 将使用贴吧吧友的语气回复频道消息。 | -| 小红薯 | nmBot 将使用小红书用户的语气回复频道消息。 | +| 语气偏好 | 描述 | +| ------------ | ------------------------------------------ | +| 随机(默认) | nmBot 将随机选择以下回复频道消息的语气。 | +| 称赞 | nmBot 将赞同频道消息的内容。 | +| 讽刺 | nmBot 将使用反对和讽刺的语气回复频道消息。 | +| 热情 | nmBot 将使用热情的语气回复频道消息。 | +| 猫娘 | nmBot 将使用猫娘语气回复频道消息。 | +| 病娇 | nmBot 将使用病娇语气回复频道消息。 | +| 吧友 | nmBot 将使用贴吧吧友的语气回复频道消息。 | +| 小红薯 | nmBot 将使用小红书用户的语气回复频道消息。 | ### 多语气选择 diff --git a/docs/nmbot-telegram/group/scheduled-task.md b/docs/nmbot-telegram/group/scheduled-task.md index 19e5eea..28eb915 100644 --- a/docs/nmbot-telegram/group/scheduled-task.md +++ b/docs/nmbot-telegram/group/scheduled-task.md @@ -21,9 +21,9 @@ description: 了解如何在 nmBot 中设置群组定时任务(原锁定模式 典型流程如下: 1. 打开目标群组的设置页面。 -2. 进入“定时任务(原锁定模式)”功能页。 -3. 新建任务并设置执行时间、执行日期和任务内容。 -4. 保存后观察下一次触发是否符合预期。 +1. 进入“定时任务(原锁定模式)”功能页。 +1. 新建任务并设置执行时间、执行日期和任务内容。 +1. 保存后观察下一次触发是否符合预期。 ## 任务中常见的设置项 @@ -36,6 +36,7 @@ description: 了解如何在 nmBot 中设置群组定时任务(原锁定模式 - **消息模板**:当任务需要发送消息时,选择要发送的模板。 !!! note + 定时任务的可用设置项会随版本更新持续调整,请以 nmBot 面板中实际显示为准。 ## 常见使用场景 @@ -57,9 +58,9 @@ description: 了解如何在 nmBot 中设置群组定时任务(原锁定模式 可按以下顺序排查: 1. 任务是否处于启用状态。 -2. 群组中 nmBot 是否仍有执行该任务所需的管理员权限。 -3. 执行时间与执行日期是否设置正确。 -4. 群组管理功能是否处于启用状态。 +1. 群组中 nmBot 是否仍有执行该任务所需的管理员权限。 +1. 执行时间与执行日期是否设置正确。 +1. 群组管理功能是否处于启用状态。 ### 任务执行了,但发送消息不符合预期 diff --git a/docs/nmbot-telegram/group/unban-users-banned-by-anti-spam.md b/docs/nmbot-telegram/group/unban-users-banned-by-anti-spam.md index 89614dd..902715d 100644 --- a/docs/nmbot-telegram/group/unban-users-banned-by-anti-spam.md +++ b/docs/nmbot-telegram/group/unban-users-banned-by-anti-spam.md @@ -3,6 +3,7 @@ nmBot 内置的“自动识别并删除骚扰消息”和入群验证“自动拒绝骚扰用户入群”功能采用精心设计的识别方法,致力于有效识别并封禁在群组中发送骚扰消息的用户。然而,“自动识别并删除骚扰消息”和入群验证“自动拒绝骚扰用户入群”功能在特定情况下,可能会误封禁用户。我们设计了“允许 nmTeam 支持解封误封用户”选项,您可以在 nmBot 群组管理功能中配置,允许 nmTeam 支持解封误封用户。 ## 关于该功能 + 当用户被 nmBot 在群组中封禁时,该用户既不能查看群组中的信息,也无法联系群组管理员。因此,当用户被误封时,用户无法联系群组管理员解释情况。同时,鉴于误封禁是 nmBot 的责任,与群组管理员的沟通不被我们视为一个首选项,也无法彻底解决误封问题(若 nmBot 封禁是基于用户本身的标识符)。 我们在 [nmTeam 支持](https://nmteam.xyz/support)中持续提供解除误封用户的服务。但即便用户不再被 nmBot 封禁,也仍然被群组封禁。虽然在群组中解封在技术上是可行的,但群组管理员未授权的情况下,nmBot 开发组不能授权 nmTeam 支持解封用户。 @@ -10,16 +11,19 @@ nmBot 内置的“自动识别并删除骚扰消息”和入群验证“自动 因此,我们设计了“允许 nmTeam 支持解封误封用户”选项,启用该选项后,nmBot 将授予 nmTeam 支持权限在被封禁用户受影响群组中解封该用户。 ## 我需要启用该功能吗? + 在 nmBot 2024 年 1 月功能更新后首次使用 nmBot 的群组,该选项已默认启用。且修改“自动识别并删除骚扰消息”功能时,可见该选项的设置开关。若您不希望 nmTeam 支持解封误封用户,您也可以在 nmBot 群组管理功能中关闭该选项。 在 nmBot 2024 年 1 月功能更新前已经在使用 nmBot 的群组,该选项默认关闭。若您希望 nmTeam 支持解封误封用户,您可以在 nmBot 群组管理功能中启用该选项。 ## 如何启用/关闭该功能 + 您可以在 nmBot /config 菜单的“消息限制与智能识别”->“智能识别并删除骚扰消息”中启用或关闭“允许 nmTeam 支持解封误封用户”选项;或在 nmBot 面板群组设置页面->“群组管理功能”->“智能识别并删除骚扰消息”设置项列表中启用或关闭“允许 nmTeam 支持解封误封用户”选项。 ->💡 启用“群组管理功能”后,才可以修改该设置。 +> 💡 启用“群组管理功能”后,才可以修改该设置。 ## 我们如何保证该功能不被滥用 + nmBot 处理骚扰消息时,在群组中和该用户私信中,会发送一条带有“操作 ID”的消息。 若用户认为 nmBot 的骚扰拦截功能误封禁了该用户,该用户可以联系 nmTeam 支持。nmTeam 支持会要求用户提供“操作 ID”,并通过“操作 ID”确认该用户的标识符和违反的具体规则。若 nmTeam 支持确认该用户被误封禁,nmTeam 支持将移除“操作 ID”中记录的违反规则,并通过“操作 ID”尝试在“操作 ID”所记录的群组中解封该用户。 diff --git a/docs/nmbot-telegram/group/warn.md b/docs/nmbot-telegram/group/warn.md index 12845e4..6e22959 100644 --- a/docs/nmbot-telegram/group/warn.md +++ b/docs/nmbot-telegram/group/warn.md @@ -6,11 +6,11 @@ `/warn` 支持如下参数: -| 参数名称 | 参数说明 | -| :--- | :--- | -| (回复消息) | 将该消息的发布者视作处理对象。| -| 在指令后紧跟的用户 ID、频道 ID 或 username | 若没有回复消息,则将传入的参数视作处理对象。| -| 在指令后(若未传入用户信息参数)或用户信息参数后(若传入了用户信息参数)紧跟的警告次数。| 若未传入,则视为警告 1 次。| +| 参数名称 | 参数说明 | +| :--------------------------------------------------------------------------------------- | :------------------------------------------- | +| (回复消息) | 将该消息的发布者视作处理对象。 | +| 在指令后紧跟的用户 ID、频道 ID 或 username | 若没有回复消息,则将传入的参数视作处理对象。 | +| 在指令后(若未传入用户信息参数)或用户信息参数后(若传入了用户信息参数)紧跟的警告次数。 | 若未传入,则视为警告 1 次。 | > 💡 仅当 nmBot 返回消息确认操作成功时,才能表明警告成功。 diff --git a/docs/nmbot-telegram/img/update-pictures/index.md b/docs/nmbot-telegram/img/update-pictures/index.md index 520d7e3..b7b7406 100644 --- a/docs/nmbot-telegram/img/update-pictures/index.md +++ b/docs/nmbot-telegram/img/update-pictures/index.md @@ -1,7 +1,8 @@ ---- -title: update-pictures -description: ---- - -# {index_title} -{index_description} +--- +title: update-pictures +description: +--- + +# {index_title} + +{index_description} diff --git a/docs/nmbot-telegram/launch-panel.md b/docs/nmbot-telegram/launch-panel.md index b80ed03..d77fcf6 100644 --- a/docs/nmbot-telegram/launch-panel.md +++ b/docs/nmbot-telegram/launch-panel.md @@ -1,4 +1,5 @@ # 启动 nmBot 面板 + ## 在 Telegram 中启动 nmBot 面板 您可以在任意对话中向 nmBot 发送 /panel,然后根据提示启动 nmBot 面板。 @@ -11,7 +12,7 @@ ## 在 Web 浏览器中启动 nmBot 面板 -nmBot 面板支持在现代 Web 浏览器中使用,提供独立于 Telegram Web App 的优质体验,并且在平板电脑和电脑设备上充分利用空间。 +nmBot 面板支持在现代 Web 浏览器中使用,提供独立于 Telegram Web App 的优质体验,并且在平板电脑和电脑设备上充分利用空间。 您可以访问 [https://nmbot.nmnm.fun](https://nmbot.nmnm.fun) 来启动 nmBot 面板。 diff --git a/docs/nmbot-telegram/legal/group-member-nmbot-agreement.md b/docs/nmbot-telegram/legal/group-member-nmbot-agreement.md index 855e5c6..c426d6f 100644 --- a/docs/nmbot-telegram/legal/group-member-nmbot-agreement.md +++ b/docs/nmbot-telegram/legal/group-member-nmbot-agreement.md @@ -1,7 +1,8 @@ # nmBot 群组使用三方协议 -发布日期:2024 年 11 月 13 日 -修改日期:2025 年 2 月 15 日 -生效日期:2025 年 2 月 15 日 + +发布日期:2024 年 11 月 13 日\ +修改日期:2025 年 2 月 15 日\ +生效日期:2025 年 2 月 15 日 本协议由群组管理方、群组成员和 nmBot 服务提供方(以下简称 nmBot)共同缔结,旨在规范三方在使用 nmBot 服务过程中应履行的义务和享有的权利。该协议适用于群组管理方选择启用 nmBot 功能后,群组成员在加入群组时被动使用 nmBot 的情境。 @@ -54,8 +55,8 @@ **5.4.** 在以下情况下,nmBot 服务提供方有权暂停或终止对群组的服务: 1. 群组管理方未按规定使用 nmBot 功能,导致群组成员投诉或纠纷; -2. 群组管理方滥用 nmBot 功能对群组成员进行骚扰、侵犯隐私或其他违规行为; -3. 群组管理方未按规定支付 nmBot 服务费用或违反《nmBot 服务使用条款》的其他规定。 +1. 群组管理方滥用 nmBot 功能对群组成员进行骚扰、侵犯隐私或其他违规行为; +1. 群组管理方未按规定支付 nmBot 服务费用或违反《nmBot 服务使用条款》的其他规定。 **6. 隐私与数据使用** diff --git a/docs/nmbot-telegram/legal/plus-terms-of-use.md b/docs/nmbot-telegram/legal/plus-terms-of-use.md index 79b2932..088ffe2 100644 --- a/docs/nmbot-telegram/legal/plus-terms-of-use.md +++ b/docs/nmbot-telegram/legal/plus-terms-of-use.md @@ -1,8 +1,8 @@ # nmBot+ 服务使用条款 -发布日期:2024 年 11 月 17 日 -修改日期:2025 年 2 月 4 日 -生效日期:2025 年 2 月 4 日 +发布日期:2024 年 11 月 17 日\ +修改日期:2025 年 2 月 4 日\ +生效日期:2025 年 2 月 4 日 本《nmBot+ 服务使用条款》(简称“本条款”)是对《[nmBot 服务使用条款](terms-of-use.md)》的补充,适用于选择购买 nmBot+ 订阅服务的用户。请您仔细阅读以下条款并确保完全理解: @@ -62,7 +62,7 @@ nmTeam 不对由于价格调整导致用户购买的 nmBot+ 服务价格变化 **4. 用户权益** -**4.1.** 高级功能:nmBot+ 服务提供专属的高级功能,用户在服务有效期内可按照权益范围使用这些功能。 +**4.1.** 高级功能:nmBot+ 服务提供专属的高级功能,用户在服务有效期内可按照权益范围使用这些功能。 **4.2.** 权限时长:一次性购买的用户按照购买时长享有使用权限,订阅用户在订阅期内享有使用权限。 diff --git a/docs/nmbot-telegram/legal/privacy-policy.md b/docs/nmbot-telegram/legal/privacy-policy.md index d588292..be939b2 100644 --- a/docs/nmbot-telegram/legal/privacy-policy.md +++ b/docs/nmbot-telegram/legal/privacy-policy.md @@ -1,8 +1,8 @@ # nmBot 服务隐私政策 -发布日期:2023 年 10 月 3 日 -更新日期:2024 年 9 月 16 日 -生效日期:2024 年 9 月 16 日 +发布日期:2023 年 10 月 3 日\ +更新日期:2024 年 9 月 16 日\ +生效日期:2024 年 9 月 16 日 **1. 引言** diff --git a/docs/nmbot-telegram/legal/terms-of-use.md b/docs/nmbot-telegram/legal/terms-of-use.md index fca5ab7..85d24ad 100644 --- a/docs/nmbot-telegram/legal/terms-of-use.md +++ b/docs/nmbot-telegram/legal/terms-of-use.md @@ -1,8 +1,8 @@ # nmBot 服务使用条款 -发布日期:2023 年 10 月 3 日 -修改日期:2025 年 2 月 15 日 -生效日期:2025 年 2 月 15 日 +发布日期:2023 年 10 月 3 日\ +修改日期:2025 年 2 月 15 日\ +生效日期:2025 年 2 月 15 日 欢迎使用“nmBot”服务(下称“本服务”)。以下使用条款规定了您与 nmTeam 之间的法律协议。在使用本服务之前,请仔细阅读以下条款和条件。 diff --git a/docs/nmbot-telegram/mcp.md b/docs/nmbot-telegram/mcp.md index d721fba..60c044b 100644 --- a/docs/nmbot-telegram/mcp.md +++ b/docs/nmbot-telegram/mcp.md @@ -38,11 +38,11 @@ description: 创建和管理 nmBot MCP Access Token,并在 VS Code 等外部 请按以下步骤操作: 1. 打开 nmBot 面板中的 `MCP 配置` 页面。 -2. 点击 `创建 Token`。 -3. 在弹出的窗口中输入一个便于识别的 `Token 名称`,例如 `My VS Code`。 -4. 点击 `确认`。 -5. 创建成功后,系统会弹出提示框并展示完整 Token。 -6. 立即复制该 Token,并保存在安全的位置。 +1. 点击 `创建 Token`。 +1. 在弹出的窗口中输入一个便于识别的 `Token 名称`,例如 `My VS Code`。 +1. 点击 `确认`。 +1. 创建成功后,系统会弹出提示框并展示完整 Token。 +1. 立即复制该 Token,并保存在安全的位置。 ![创建 MCP Access Token](img/mcp/mcp-create-token-dialog.png){ width="360" } diff --git a/docs/nmbot-telegram/message-template.md b/docs/nmbot-telegram/message-template.md index 9a57cfe..8b10a96 100644 --- a/docs/nmbot-telegram/message-template.md +++ b/docs/nmbot-telegram/message-template.md @@ -5,6 +5,7 @@ 消息模板是预设置的通用消息,可在同一对话的多个设置中互通。 例如,您希望某群组中的入群欢迎消息反应群规,同时希望群组中有人发送“群规”时也向他们展示群规,则您可以为入群欢迎和关键词回复设置相同的消息模板。 + ## 消息模板变量 在消息模板中,您可以使用以下变量来插入动态内容: @@ -49,7 +50,7 @@ Telegram 客户端会相应地渲染它们。 请注意,Telegram 客户端在打开超链接前会向用户显示一个**提示**(“`打开此链接?`”以及完整的 URL)。 消息实体[^1]可以嵌套,但须满足以下限制: -[^1]: “消息实体”指消息中使用的格式化选项,例如粗体、斜体、下划线等。 +\[^1\]: “消息实体”指消息中使用的格式化选项,例如粗体、斜体、下划线等。 - 如果两个实体有公共字符,则其中一个必须完全包含在另一个内。 - 粗体、斜体、下划线、删除线和剧透实体中可以包含其他任何实体,也可以成为其他实体的一部分,预格式化和代码实体除外。 @@ -57,10 +58,10 @@ Telegram 客户端会相应地渲染它们。 - 所有其他实体不能相互包含。 链接 `tg://user?id=<user_id>` 可用于通过用户 ID[^2] 而不使用用户名来提及用户。请注意: -[^2]: 用户 ID 是 Telegram 中唯一标识用户的数字 ID。您可以通过 nmBot 的 `/id` 指令获取用户 ID。 +\[^2\]: 用户 ID 是 Telegram 中唯一标识用户的数字 ID。您可以通过 nmBot 的 `/id` 指令获取用户 ID。 - 这些链接仅在超链接或内联键盘按钮中使用时有效。 -例如,当在消息文本中使用时,它们将无法工作。 + 例如,当在消息文本中使用时,它们将无法工作。 - 除非该被提及的用户是对话成员,否则只有当用户曾经私下联系过机器人或通过内联按钮向机器人发送过回调查询,并且未对机器人启用转发消息隐私设置时,这些提及才能得到保证。 你可以在 [libprisma#supported-languages](https://github.com/TelegramMessenger/libprisma#supported-languages) 中找到支持语法高亮的编程和标记语言列表。 @@ -103,13 +104,13 @@ __下划线__ 请注意: - 任何字符(代码介于 1 到 126 之间)均可在任意位置用前导 '\\' 转义,此时该字符被视为普通字符而非标记的一部分。 -这意味着 '\\' 字符通常必须用前导 '\\' 进行转义。 -在预格式化和代码实体内,所有 '\`' 和 '\\' 字符必须用前导 '\\' 进行转义。 -在超链接和自定义表情定义的 (...) 部分内,所有 ')' 和 '\\' 必须用前导 '\\' 进行转义。 -- 在所有其他地方,字符 '\_', '\*', '\[', '\]', '\(', '\)', '\~', '\`', '\>', '\#', '\+', '\-', '\=', '\|', '\{', '\}', '\.', '\!' 均必须用前导 '\\' 进行转义。 + 这意味着 '\\' 字符通常必须用前导 '\\' 进行转义。 + 在预格式化和代码实体内,所有 '\`' 和 '\\' 字符必须用前导 '\\' 进行转义。 + 在超链接和自定义表情定义的 (...) 部分内,所有 ')' 和 '\\' 必须用前导 '\\' 进行转义。 +- 在所有其他地方,字符 '\_', '\*', '[', ']', '\(', '\)', '~', '\`', '>', '#', '+', '-', '=', '|', '{', '}', '.', '!' 均必须用前导 '\\' 进行转义。 - 如果斜体和下划线实体之间存在歧义,`__` 总是从左到右优先视为下划线实体的开始或结束,因此请使用 `___斜体 下划线_**__` 替代 `___斜体 下划线___`,在中间添加一个空的粗体实体作为分隔。 - 必须为自定义表情提供一个有效的备用表情。自定义表情在无法显示自定义表情(例如系统通知)或当消息被非 Premium 用户转发时,会显示该备用表情。 -建议使用自定义表情贴纸的 emoji 字段中的表情。 + 建议使用自定义表情贴纸的 emoji 字段中的表情。 - 自定义表情实体仅可被购买了额外用户名的机器人使用。[^3] ### HTML 风格 @@ -137,9 +138,9 @@ __下划线__ 请注意: - 仅支持上述标签。 -- 所有不属于标签或 HTML 实体部分的 <, > 和 & 符号必须替换为相应的 HTML 实体(< 替换为 &lt;,> 替换为 &gt;,& 替换为 &amp;)。 +- 所有不属于标签或 HTML 实体部分的 \<, > 和 & 符号必须替换为相应的 HTML 实体(< 替换为 \<,> 替换为 \>,& 替换为 \&)。 - 支持所有数值 HTML 实体。 -- 当前 API 仅支持以下命名 HTML 实体:&lt;, &gt;, &amp; 和 &quot;。 +- 当前 API 仅支持以下命名 HTML 实体:\<, \>, \& 和 \"。 - 使用嵌套的 `pre` 和 `code` 标签来为 `pre` 实体定义编程语言。 - 单独的 `code` 标签无法指定编程语言。 - 必须在 `tg-emoji` 标签的内容中使用有效的表情符号。如果无法显示自定义表情(例如系统通知)或消息被非 Premium 用户转发时,将显示该表情。建议使用自定义表情贴纸的 emoji 字段中的表情。 @@ -172,8 +173,6 @@ _斜体 文本_ - 在实体外部转义字符 '\_', '\*', '\`', '\[' 时,请在它们前面加上 '\\'。 - 实体内部不允许转义,因此必须先关闭实体再重新打开:例如,对于斜体 `snake_case`,请使用 `_snake_\__case_`;对于粗体 `2*2=4`,请使用 `*2*\**2=4*`。 -[^3]: 目前仅“商业关键词回复”功能中支持自定义表情。 - ## 消息模板常见问题 ### 消息模板发送失败 @@ -182,3 +181,4 @@ _斜体 文本_ nmBot 通常会在消息模板发送失败时向您发送错误消息。根据功能的不同,错误消息可能会有所不同。 +[^3]: %E7%9B%AE%E5%89%8D%E4%BB%85%E2%80%9C%E5%95%86%E4%B8%9A%E5%85%B3%E9%94%AE%E8%AF%8D%E5%9B%9E%E5%A4%8D%E2%80%9D%E5%8A%9F%E8%83%BD%E4%B8%AD%E6%94%AF%E6%8C%81%E8%87%AA%E5%AE%9A%E4%B9%89%E8%A1%A8%E6%83%85%E3%80%82 diff --git a/docs/nmbot-telegram/nmartchat.md b/docs/nmbot-telegram/nmartchat.md index 7483104..c6486de 100644 --- a/docs/nmbot-telegram/nmartchat.md +++ b/docs/nmbot-telegram/nmartchat.md @@ -6,7 +6,7 @@ ### nmartChat 贴纸回复逻辑 -nmBot 在收到私信和打开 nmartChat 功能的群组回复的贴纸消息时,将回复贴纸。 +nmBot 在收到私信和打开 nmartChat 功能的群组回复的贴纸消息时,将回复贴纸。 具体回复逻辑如下: @@ -16,20 +16,22 @@ nmBot 在收到私信和打开 nmartChat 功能的群组回复的贴纸消息时 ## 群聊中如何开启 nmartChat -您可以在 [nmBot 面板](./launch-panel.md)中开启此功能,但您必须为此群聊的管理员。 +您可以在 [nmBot 面板](./launch-panel.md)中开启此功能,但您必须为此群聊的管理员。\ 具体步骤如下: 1.遵循 [启动 nmBot 面板](./launch-panel.md)中的步骤,进入 nmBot 面板。 -2.选择您要开启 nmartChat 的群聊。 +2.选择您要开启 nmartChat 的群聊。 !!! faq "没有在面板中找到您的群聊?" + 您必须为此群聊的**管理员**, 且需要在群内发送 `/regadmin@nmnmfunbot` 注册您的群聊。 ![regadmin](img/nmartchat_regadmin.png) -3.在群聊设置中开启 nmartChat 功能。 - ![enablechat](img/nmartchat_panel_enablechat.png) +3.在群聊设置中开启 nmartChat 功能。\ +![enablechat](img/nmartchat_panel_enablechat.png) !!! faq "为什么在频道(或在群聊内使用频道身份)发送以 nm 开头的消息,nmBot 不回复我?" - 请注意:nmartChat 仅支持与个人身份的用户进行对话,并不支持频道身份。 + + 请注意:nmartChat 仅支持与个人身份的用户进行对话,并不支持频道身份。\ 如果您需要在频道内使用 nmartChat,可以试试 nmBot 的[“同频气氛组”](./group/reply-channel.md)功能。 diff --git a/docs/nmbot-telegram/nmbot-intelligence/feedback.md b/docs/nmbot-telegram/nmbot-intelligence/feedback.md index 0353d72..280fd89 100644 --- a/docs/nmbot-telegram/nmbot-intelligence/feedback.md +++ b/docs/nmbot-telegram/nmbot-intelligence/feedback.md @@ -7,5 +7,5 @@ title: nmBot 智能反馈 若您在使用 nmBot 智能时遇到使用问题,错误信息,或有任何建议和意见,请通过以下方式反馈: - 发送消息到 [Telegram 中的 nmTeam 支持机器人](https://t.me/nmteamsupportbot)。 -请在消息中包含您的问题描述和相关的错误信息,并包含标签 `#nmBotIntelligence`。 + 请在消息中包含您的问题描述和相关的错误信息,并包含标签 `#nmBotIntelligence`。 - 通过[社区](../../contact-us/forum.md)与其他用户交流,获取帮助和建议。 diff --git a/docs/nmbot-telegram/panel/how-to-launch-panel.md b/docs/nmbot-telegram/panel/how-to-launch-panel.md index 6fcbdf9..0a91ca3 100644 --- a/docs/nmbot-telegram/panel/how-to-launch-panel.md +++ b/docs/nmbot-telegram/panel/how-to-launch-panel.md @@ -24,4 +24,4 @@ nmBot 面板提供了多种方式启动。 您可以向 nmBot 发送 /panel 指令来获取面板链接。 -您可以根据返回消息中的说明,在[浏览器](#在浏览器中打开)或[迷你 App](#在迷你-App-中打开-推荐)中打开面板。 +您可以根据返回消息中的说明,在[浏览器](#%E5%9C%A8%E6%B5%8F%E8%A7%88%E5%99%A8%E4%B8%AD%E6%89%93%E5%BC%80)或[迷你 App](#%E5%9C%A8%E8%BF%B7%E4%BD%A0-App-%E4%B8%AD%E6%89%93%E5%BC%80-%E6%8E%A8%E8%8D%90)中打开面板。 diff --git a/docs/nmbot-telegram/plus/benefits.md b/docs/nmbot-telegram/plus/benefits.md index 5f31d5f..9841d8b 100644 --- a/docs/nmbot-telegram/plus/benefits.md +++ b/docs/nmbot-telegram/plus/benefits.md @@ -11,26 +11,32 @@ nmBot+ 可为群组和个人用户带来一系列独享的权益。订阅 nmBot+ ## 群组 -### nmBot 配置权限管理 +### nmBot 配置权限管理 + - 群组的所有者可以为不同用户设定对 nmBot 设置的管理权限。 - 对于各项功能,权限可设为“默认”“可更改群组信息的管理员”“可添加管理员的管理员”或“仅群主”。 - 群主还可为特定群组管理员针对某项功能添加例外。 ### 自定义骚扰规则 + - 群组管理员可在 nmBot 骚扰消息拦截和入群验证自动拒绝骚扰用户入群功能中补充自己的匹配规则。 -### 群组指令列表设置 +### 群组指令列表设置 + - 群组管理员现在可以自定义在 Telegram App 中聊天时,消息输入框中出现的指令列表。 ### 呼叫群组管理员 + - 支持自定义呼叫关键词:群组管理员可通过正则表达式自定义触发“呼叫群组管理员”功能的消息内容。 - 支持在私信中接收通知:群组成员呼叫 nmBot 时,在私信中向所有群组管理员(适用时包括匿名管理员)发送提示消息。 - 启用在私信中接收通知时,可关闭群组中呼叫消息中对群组管理员的提及。 ### 封禁并删除最近消息 + - 新增 /dban, /dmute, /dkick 和 /dwarn 指令,供群组管理员封禁/禁言/踢出/警告用户并删除该用户最近在群组中的消息。 ### 入群验证 + - 支持自定义入群验证消息:群组管理员可在入群验证消息中添加自定义文本。 - nmBot 面板中支持查看入群验证历史记录。可查看 15 天内的历史记录 - 入群验证可自定义更多验证超时时间和封禁用户时间。 @@ -39,30 +45,38 @@ nmBot+ 可为群组和个人用户带来一系列独享的权益。订阅 nmBot+ - 优化了 nmBot 面板“入群验证”页面设置项的排列。 ### 入群请求通知消息 + - nmBot 可在有用户申请加入群组时,在私信中向该用户发送预设消息。 ### 封禁退出群组的用户时长 + - 群组“封禁退出群组的用户”新增时长选择。 ### 新成员权限限制 + - 支持限制新加入群组的成员在一段时间内可发送消息的类型。 ### 定时任务 + - 支持在群组中发送定时消息。 ### 关键词回复 + - 支持使用 nmBot AI 自动生成关键词回复。 - 支持通过关键词回复警告用户。 - 支持设置关键词回复随机发送消息的数量。 - 支持设置优先跨消息回复。 ### 入群欢迎 + - 支持自定义入群欢迎消息的自动删除时间。 ### 按消息类型警告用户 + - nmBot 支持在删除群组中特定类型消息的同时,对发送消息的用户予以警告。 ### 解锁限制 + - nmBot+ 为群组解锁一系列设置权限: - 同频气氛组功能的每日最大可回复消息数增加。 - 关键词回复、定时任务的最大可设置数量增加。 @@ -72,9 +86,11 @@ nmBot+ 可为群组和个人用户带来一系列独享的权益。订阅 nmBot+ ## 用户 ### 解锁限制 + - nmBot+ 为个人用户解锁设置权限: - 关键词回复的最大可设置数量增加。 - 关键词回复功能的最大可选择消息模板数量增加。 ### 封禁并删除最近消息 -- 新增 /dban, /dmute, /dkick 和 /dwarn 指令,供群组管理员封禁/禁言/踢出/警告用户并删除该用户最近在群组中的消息。 \ No newline at end of file + +- 新增 /dban, /dmute, /dkick 和 /dwarn 指令,供群组管理员封禁/禁言/踢出/警告用户并删除该用户最近在群组中的消息。 diff --git a/docs/nmbot-telegram/plus/payment-methods.md b/docs/nmbot-telegram/plus/payment-methods.md index 9096164..87fb283 100644 --- a/docs/nmbot-telegram/plus/payment-methods.md +++ b/docs/nmbot-telegram/plus/payment-methods.md @@ -2,4 +2,4 @@ 您可以通过如下方式订阅 nmBot+: -- 通过 Telegram 星币订阅:请在 Telegram app 中打开 nmBot 面板迷你 app 来获取订阅方式。[了解如何购买 Telegram 星币](../faq/buy-stars.md) \ No newline at end of file +- 通过 Telegram 星币订阅:请在 Telegram app 中打开 nmBot 面板迷你 app 来获取订阅方式。[了解如何购买 Telegram 星币](../faq/buy-stars.md) diff --git a/docs/nmbot-telegram/set-language.md b/docs/nmbot-telegram/set-language.md index 41e8cd4..f5050de 100644 --- a/docs/nmbot-telegram/set-language.md +++ b/docs/nmbot-telegram/set-language.md @@ -6,17 +6,17 @@ nmBot 现已支持多种语言。 nmBot 将自动匹配您的 Telegram 语言,若支持,将以该语言为您服务;若该语言与实际语言不匹配,或您想使用其他语言,您可以发送 /lang 指令来自定义语言。 -* 并非所有功能都将以设置的语言显示。 +- 并非所有功能都将以设置的语言显示。 nmBot now supports multiple languages. nmBot will automatically match your Telegram language, and if it's supported, it will serve you in that language; if the language does not match the actual language you speak or you want to use another language, just send the /lang command to customize the language. -* All the functions will not be displayed in the language you set. +- All the functions will not be displayed in the language you set. ## nmBot 面板语言 -nmBot 面板将自动识别您的浏览器语言。您也可以自定义 nmBot 面板的语言。 +nmBot 面板将自动识别您的浏览器语言。您也可以自定义 nmBot 面板的语言。 转到 我的 > 语言,即可自定义 nmBot 面板的语言。 diff --git a/docs/nmbot-telegram/support.md b/docs/nmbot-telegram/support.md index 38118d4..b6a5539 100644 --- a/docs/nmbot-telegram/support.md +++ b/docs/nmbot-telegram/support.md @@ -1,6 +1,7 @@ # nmBot 支持 + nmBot 提供免费的用户支持。 您可以通过 Telegram 机器人联系 nmTeam 支持:[@nmteamsupport](https://nmteamsupport.t.me) -您还可以加入 nmTeam 官方群组 [@nmteamchat](https://nmteamchat.t.me) 与 nmBot 用户一同交流。 \ No newline at end of file +您还可以加入 nmTeam 官方群组 [@nmteamchat](https://nmteamchat.t.me) 与 nmBot 用户一同交流。 diff --git a/docs/nmbot-telegram/tools/dc.md b/docs/nmbot-telegram/tools/dc.md index 9349641..bae500e 100644 --- a/docs/nmbot-telegram/tools/dc.md +++ b/docs/nmbot-telegram/tools/dc.md @@ -7,6 +7,7 @@ nmBot 的 Telegram 数据中心查询功能可以帮助您快速查找账号所 您可以在与 nmBot 的私信或 nmBot 所在群组中使用 `/dc` 指令查询您账号所在的数据中心。 !!! note + 若群组管理员启用了“限制群成员使用指令”功能,您可能无法在群组中使用 `/dc` 指令。 同时,我们建议您尽可能在私信中使用 `/dc` 指令,以免影响群组中的其他用户。 @@ -15,6 +16,7 @@ nmBot 的 Telegram 数据中心查询功能可以帮助您快速查找账号所 您还可以在 `/dc` 指令后添加一个 Telegram 用户 ID 或群组/频道用户名,以查询该用户的数据中心。 !!! note + 所有对话必须设置头像才能查询数据中心。 ## 关于 Telegram 数据中心 @@ -28,8 +30,8 @@ Telegram 有 5 个数据中心 (Data Center, DC),其中 DC1 与 DC3 位于美 对于个人用户、群组和频道,nmBot 通过获取并解码头像的文件标识符,来判断该文件所在的数据中心,进而得出该对话的数据中心。 这一判断并不一定总是准确的,例如,对群组和频道而言,其头像的数据中心可能会随设置头像的对话管理员变化而变化。 -若要了解关于 Telegram 数据中心的详情,您可以参考这篇第三方文章: +若要了解关于 Telegram 数据中心的详情,您可以参考这篇第三方文章:\ [Telegram DC 之都市传说 - Coxxs](https://dev.moe/2564)[(在互联网档案馆查看)](http://web.archive.org/web/20230211164632/https://dev.moe/2564) -若要了解关于查询的细节,您可以参考这篇第三方文章: +若要了解关于查询的细节,您可以参考这篇第三方文章:\ [通过 Telegram file_id 判断文件存储的数据中心 - WooMai](https://woomai.me/talk/telegram-determine-dc-by-file-id/#%E6%9F%A5%E8%AF%A2%E6%95%B0%E6%8D%AE%E4%B8%AD%E5%BF%83)[(在互联网档案馆查看)](http://web.archive.org/web/20221206175516/https://woomai.me/talk/telegram-determine-dc-by-file-id/#%E6%9F%A5%E8%AF%A2%E6%95%B0%E6%8D%AE%E4%B8%AD%E5%BF%83) diff --git a/docs/nmbot-telegram/update-log/2023-01.md b/docs/nmbot-telegram/update-log/2023-01.md index ca3cfd8..df31291 100644 --- a/docs/nmbot-telegram/update-log/2023-01.md +++ b/docs/nmbot-telegram/update-log/2023-01.md @@ -4,37 +4,42 @@ index: -2301 # nmBot 2023 年 1 月功能更新 -亲爱的用户,您期盼已久的 nmBot 功能更新现已推出。 +亲爱的用户,您期盼已久的 nmBot 功能更新现已推出。\ 本更新包含对 nmBot 底层的重构和对现有功能的改进。本更新还包含错误修复和稳定性提升。 ## 2023 年 1 月 18 日 15:45 + nmBot 版本号:23.1.4 - 用户进行入群验证出现未知错误时,nmBot 将进行提示并放宽验证时间。 - 优化了入群验证出现未知错误时的相关报错。 ## 2023 年 1 月 13 日 23:49 + nmBot 版本号:23.1.3 - 修复了特定情况下群组管理员无法通过按钮关闭其他群成员触发的 nmBot 消息的问题。 ## 2023 年 1 月 13 日 12:14 + nmBot 版本号:23.1.2 - 修复了特定情况下,用户名称获取出错时,操作无法继续的问题。 - 关闭了用户在群组中禁止 nmBot 发送消息时,nmBot 的部分告警。 ## 2023 年 1 月 13 日 01:42 + nmBot 版本号:23.1.1 - 修复了入群验证超时、失败或被管理员拒绝后,nmBot 未正常编辑验证消息的问题。 - 关闭了用户同时发送多条消息时,nmBot 初始化出现数据库重复插入错误时的告警。 ## 2023 年 1 月 13 日 00:35 + - 重构了 nmBot 底层代码,通过 nmTeam 完全自主研发的全新架构,优化了 nmBot 流畅度,提升了 nmBot 稳定性。 - nmBot 新增多语言支持。nmBot 将自动匹配用户常用的 Telegram 语言;用户也可以发送 /lang 指令切换语言。 - nmBot 新增语言:繁体中文、英语。 - \* 并非所有功能都将以设定的语言呈现。 + \* 并非所有功能都将以设定的语言呈现。 - 新增“图片转 Emoji”功能:可以使用 /te (/toemoji) 指令将图片转换为贴纸,并可以通过按钮快速转发到其他对话或转发给 @Stickers 等贴纸包机器人。 - nmartChat 需要一段时间组织回复内容时,将通过 Telegram 显示输入状态提示。 - 通过按钮在浏览器同时以多个账号身份打开 nmBot 面板时,用户身份将不再覆盖,可在多个选项卡中同时使用不同用户身份工作。 @@ -55,7 +60,7 @@ nmBot 版本号:23.1.1 - 群组管理员可以使用 /dt 指令将 Telegram #General 话题设为 nmBot 默认话题。 - nmBot 面板中启用话题功能的群组头像的显示效果现已与 Telegram App 中同步。 - 替换了 nmBot 面板更新日志为 nmBot 帮助文档中的对应章节。 -- 调整了“群友互动”功能对消息内容的识别:若互动消息的空格前含有“了”字,消息中将不再附加“了”字。 +- 调整了“群友互动”功能对消息内容的识别:若互动消息的空格前含有“了”字,消息中将不再附加“了”字。 - 优化了 nmBot 部分功能的中文文本。 - 优化了 nmBot 群组关键词回复禁言功能的时间配置。 - 更新了 nmBot 的软件包版本。 @@ -74,4 +79,4 @@ nmBot 版本号:23.1.1 - 修复了 nmBot 未正确复读群组中包含特殊格式的消息和包含自定义 Emoji 的消息的问题。(由于 Telegram 限制,nmBot 将不会以非转发形式复读包含自定义 Emoji 的消息,这些消息将始终以转发形式复读。) - 修复了 nmBot 面板的部分提示未正确显示本地化文本的问题。 - 修复了 nmBot 智能识别并删除骚扰消息功能的“操作时发送提示消息”功能不生效的问题。 -- 删除了 nmBot 代码中部分未使用的数据库请求,以节省服务器资源。 \ No newline at end of file +- 删除了 nmBot 代码中部分未使用的数据库请求,以节省服务器资源。 diff --git a/docs/nmbot-telegram/update-log/2023-02.md b/docs/nmbot-telegram/update-log/2023-02.md index 9c1c33a..3852095 100644 --- a/docs/nmbot-telegram/update-log/2023-02.md +++ b/docs/nmbot-telegram/update-log/2023-02.md @@ -8,38 +8,46 @@ index: -2302 本更新为群组管理员推出了“操作记录”功能,为查询管理员操作提供了有力依据;本更新新增了“专注管理”模式,使 nmBot 的娱乐类功能自动关闭,专注于群组管理。本更新还推出了“文本处理”行内功能集和新的过塑模板,为用户聊天提供了一系列娱乐工具。本更新还包含错误修复和稳定性提升。 ## 2023 年 2 月 20 日 11:11 + nmBot 版本号:23.2.5 - 修复了特定情况下专注管理模式不起作用的问题。 ## 2023 年 2 月 16 日 15:05 + nmBot 版本号:23.2.4 - 修复了群组踢出 nmBot 后的询问消息中,按钮内容未正常显示的问题。 ## 2023 年 2 月 9 日 23:52 + - 由于 2023 年 2 月功能更新中“特定情况下,管理员状态失效时将自动在 nmBot 面板取消注册”的更新导致部分用户在数据库中的群组数据出现错误,该条更新内容已回退。我们已为受影响的用户修复了相关错误。 ## 2023 年 2 月 9 日 17:40 + nmBot 版本号:23.2.3 - 入群欢迎消息发送失败时,nmBot 将发送提示消息,提示群组管理员确认入群验证消息的消息格式是否正确。 - 修复了在群组中,特定情况下 nmBot 加载或创建触发事件用户的群组配置数据,并可能导致特定情况下行内指令和入群验证未触发的问题。该问题不会导致用户进行入群验证时被意外封禁。错误创建的群组数据将在修复后删除,不会影响 nmBot 的正常使用。 ## 2023 年 2 月 4 日 21:13 + - 更正了 nmBot 面板操作记录设置页面中的一处文本描述错误。 ## 2023 年 2 月 4 日 18:20 + nmBot 版本号:23.2.2 - 修复了使用 /ban、/block,/warn 等部分功能,并通过提供用户 ID 完成操作时,nmBot 操作记录写入失败的问题。 ## 2023 年 2 月 4 日 17:59 + nmBot 版本号:23.2.1 - 修复了特定情况下 nmBot 进入入群验证流程失败的问题。 ## 2023 年 2 月 4 日 17:30 + - 新增“群组操作记录”功能:群组管理员执行操作时,nmBot 将记录操作时间和内容。群组管理员可在 nmBot 面板查看并搜索操作记录,还可以设置操作记录发送频道,将操作记录实时同步到 Telegram 频道内。该功能默认关闭;群组管理员可以在 /config 菜单或 nmBot 面板启用。未开启该功能时,nmBot 将不会记录对应信息;部分操作尚未记入操作记录。(群组人数达 200 人后可开启该功能。) - 新增“专注管理”模式:nmBot 将仅响应群组管理指令,自动关闭 nmartChat、群组娱乐功能、积分功能和其他互动功能。该模式默认关闭;群组管理员可以在 /config 菜单或 nmBot 面板启用。 - 行内请求新增“文本处理”功能集:用户通过在消息输入框输入 @nmnmfunbot 触发的菜单中包括一系列快速文本处理功能。 @@ -89,4 +97,4 @@ nmBot 版本号:23.2.1 - 修复了特定情况下 nmartChat 对消息回复预期之外的文本的问题。 - 修复了 2022 年 12 月 12 日更新的新 nmartChat 功能导致部分原有 nmartChat 功能无法触发的问题。 - 关闭了 nmBot 的部分日志内容。 -- nmBot 入群欢迎消息无权限发送时,关闭了相应的告警。 \ No newline at end of file +- nmBot 入群欢迎消息无权限发送时,关闭了相应的告警。 diff --git a/docs/nmbot-telegram/update-log/2023-03.md b/docs/nmbot-telegram/update-log/2023-03.md index b5f1437..9304142 100644 --- a/docs/nmbot-telegram/update-log/2023-03.md +++ b/docs/nmbot-telegram/update-log/2023-03.md @@ -8,21 +8,25 @@ index: -2303 本更新优化了群组管理员以匿名身份管理群组的体验。本更新还包含错误修复和稳定性提升。 ## 2023 年 3 月 17 日 20:06 + nmBot 版本号:23.3.4 - 修复了使用繁体中文时,特定情况下 /banme 操作无法正常完成的问题。 ## 2023 年 3 月 16 日 20:38 + nmBot 版本号:23.3.3 - 修复了在举报骚扰处理结果通知中点击“不再反馈举报结果”后,操作成功结果无法正常显示的问题。 ## 2023 年 3 月 10 日 21:54 + nmBot 版本号:23.3.2 - 优化了测试版“同频气氛组”和“nmartChat”的整体效果,并解决了已知问题。我们正在测试这项更改并获取反馈,因此并非所有用户将体验到新的效果。 ## 2023 年 3 月 10 日 16:45 + nmBot 版本号:23.3.1 - 在此版本中引入了 nmBot 灰度测试模块。在后续更新中,我们将通过 A/B 测试的方法在向所有用户作出更改前预览功能或更改的效果。 @@ -33,6 +37,7 @@ nmBot 版本号:23.3.1 - 精简了 nmBot 运行日志内容。 ## 2023 年 3 月 9 日 14:30 + - 匿名管理员通过 nmBot 执行管理操作时,若该匿名管理员具有在所有匿名管理员中唯一的自定义头衔,nmBot 将识别他的权限来完成操作。 - 匿名管理员通过 nmBot 执行管理操作时,若所有匿名管理员均拥有所需权限,nmBot 将完成此操作。 - 匿名管理员在群组中通过按钮取消封禁成员时,管理员身份将显示为“匿名管理员”,并不再添加链接和 ID 记录。操作记录中记录的管理员身份不变。 @@ -98,4 +103,4 @@ nmBot 版本号:23.3.1 - 修复了作为 Telegram Web App 打开时,nmBot 面板语言设置页面、开放源代码页面未正确应用深色模式的问题。 - 修复了深色模式下,在特定浏览器中,nmBot 面板中部分下拉选项中的文本显示为黑色的问题。 - 修复了深色模式下,nmBot 面板中操作记录详情页面中的文本显示为黑色的问题。 -- 修复了处理骚扰举报时,举报文件未发送的问题。 \ No newline at end of file +- 修复了处理骚扰举报时,举报文件未发送的问题。 diff --git a/docs/nmbot-telegram/update-log/2023-04.md b/docs/nmbot-telegram/update-log/2023-04.md index f86f169..c1b68f7 100644 --- a/docs/nmbot-telegram/update-log/2023-04.md +++ b/docs/nmbot-telegram/update-log/2023-04.md @@ -4,24 +4,28 @@ index: -2304 # nmBot 2023 年 4 月功能更新 -亲爱的用户,您期盼已久的 nmBot 功能更新现已推出。 +亲爱的用户,您期盼已久的 nmBot 功能更新现已推出。\ 本更新推出了群组加权发言频率限制功能,为入群验证、关键词回复等群组功能推出了全新升级,还为 nmBot 积分功能推出了群组开关。本更新还包含错误修复和稳定性提升。 ## 2023 年 4 月 19 日 14:54 + nmBot 版本号:23.4.3 - “群友互动”功能不再支持参数 $hf ($hidefrom) 和 $ht ($hideto, $hidetarget)。 ## 2023 年 4 月 13 日 19:30 + - 修复了 2023 年 4 月更新后 nmBot 面板关键词回复编辑页面中“匹配消息发送者”设置项未正常显示的问题。 ## 2023 年 4 月 13 日 14:17 + nmBot 版本号:23.4.2 - 修复了关键词回复中设置了禁言时间时,nmBot 拷贝消息失败的问题。 - 修复了特定情况下,群组管理员无法使用关闭按钮关闭群成员打开的旧版 nmBot 帮助文档的问题。 ## 2023 年 4 月 12 日 13:10 + nmBot 版本号:23.4.1 - 修复了入群验证消息和封禁骚扰用户的提示消息中,“解除封禁并加入白名单”按钮无法正常使用的问题。 @@ -31,12 +35,14 @@ nmBot 版本号:23.4.1 ## 2023 年 4 月 11 日 20:00 ### 加权发言频率限制 + - 新增“加权发言频率限制”功能:nmBot 将在群成员每分钟内发送消息超过一定限制时限制成员在一分钟内继续发言。 - 群组管理员还可以为不同类型的消息设置权重。例如,若群组管理员将媒体权重设置为 2,则成员每发送一条媒体消息相当于发送 2 条其他消息。 - 群组管理员还可以设置是否要将媒体组视为一条消息。 - 该功能可在 nmBot /config 菜单和 nmBot 面板设置。 ### 入群验证大优化 + - 新增可选的入群验证方式: - 发送贴纸验证:nmBot 授予用户发送贴纸权限,用户可发送一张贴纸来完成验证。用户可能需要较新的 Telegram 版本才能完成验证。 - nmBot 入群验证支持跳转到私信验证。 @@ -48,6 +54,7 @@ nmBot 版本号:23.4.1 - nmBot /config 菜单中,入群验证设置页面的部分选项调整为双栏显示。 ### 关键词回复更强大 + - 关键词回复新增匹配文本内容设置:可以设置触发关键词回复的文本类型。目前可供设置的类型有:消息文本和媒体描述、文件名、贴纸 Emoji、贴纸包名称。 - 关键词回复新增选项“匹配编辑后的消息”。启用后,nmBot 将在消息内容被编辑后,再次匹配关键词。此前匹配关键词所进行的操作不会被撤销。 - 关键词回复新增选项“禁用链接预览”。启用后,在回复内容中插入链接时,发送的回复消息中,将不会显示链接预览。该选项可在 nmBot 面板的关键词回复设置中设置。 @@ -64,14 +71,17 @@ nmBot 版本号:23.4.1 - 关键词回复拷贝消息失败时,新增了相应的错误提示。 ### 同频气氛组可选语气 (Beta) + - 为“同频气氛组”功能推出了语气设置选项:群组管理员可以设置“同频气氛组”功能的语气。 - 此版本中可设置的“同频气氛组”语气有:随机 (默认)、称赞 (Beta)、讽刺、热情 (Beta)、猫娘 (Beta)。更新后,群组默认的“同频气氛组”语气为随机 (默认)。 - 新版“同频气氛组”体验面向所有使用 nmBot 的群组推出。该功能可在 nmBot /config 菜单和 nmBot 面板设置。 ### 指令运用更随心 + - 新增 /whatis 指令来查询 nmBot 可用指令的详情。 ### nmBot 面板偏好设置 + - 在 nmBot 面板新增了“偏好设置”设置页面。页面中目前包含的设置内容如下: - 自定义缩放比例:自定义 nmBot 面板字体、图标和按钮的大小。 - 禁用动画效果:禁用 nmBot 面板中的动画效果。 @@ -79,12 +89,15 @@ nmBot 版本号:23.4.1 - 启用滑动返回:在触屏设备上,从左到右滑动来返回上一页面。 ### 积分记录开关 + - 为 nmBot 积分功能推出独立设置:群组管理员可以在 nmBot /config 菜单和 nmBot 面板启用或关闭积分功能;关闭积分功能后,签到指令和积分查询指令将无法使用,成员在群组中发送消息也不会增加积分。 ### Telegram 机器人信息适配 + - 新增 /settings 和 /privacy 指令帮助用户了解 nmBot 的设置方法和隐私政策。用户也会在 nmBot 的机器人信息页面中看到“机器人设置”和“机器人隐私政策”选项。 ### 其他更新和修复 + - 使用 nmBot 的行内功能时,对应功能将显示图标。 - nmBot 面板在双栏模式下显示时,右侧栏空白时,新增了 nmBot 面板主要功能提示。 - 群成员使用“呼叫群组管理员”功能时,若回复了一条消息,群组管理员现在可以直接通过按钮对回复消息执行删除,或对用户执行封禁、举报骚扰等操作。 diff --git a/docs/nmbot-telegram/update-log/2023-05.md b/docs/nmbot-telegram/update-log/2023-05.md index 548c0f8..d01849a 100644 --- a/docs/nmbot-telegram/update-log/2023-05.md +++ b/docs/nmbot-telegram/update-log/2023-05.md @@ -4,27 +4,31 @@ index: -2305 # nmBot 2023 年 5 月功能更新 -亲爱的用户,您期盼已久的 nmBot 功能更新现已推出。 +亲爱的用户,您期盼已久的 nmBot 功能更新现已推出。\ 本更新推出了“指令映射”功能,允许用户使用自定义的指令文本触发常用的 nmBot 指令。本更新还包含错误修复和稳定性提升。 ## 2023 年 5 月 19 日 13:39 + nmBot 版本号:23.5.4 - 修复了 nmBot 更新至版本 23.5.2 后,“欢乐复读”在群组中工作状态异常的问题。 - 修复了 nmBot 更新至版本 23.5.2 后,/json 功能的返回消息中输出部分错误参数的问题。 ## 2023 年 5 月 18 日 21:30 + nmBot 版本号:23.5.3 - 优化了按消息类别自动删除消息功能的内部实现,以尝试解决特定情况下 nmBot 回复部分群组消息时缓慢的问题。 ## 2023 年 5 月 16 日 23:58 + nmBot 版本号:23.5.2 - nmBot 在 Telegram 中显示的指令列表中新增 /whatis, /mapping 和 /dc 指令。 - 我们正在调查特定情况下 nmBot 回复部分群组消息时缓慢的问题。为进一步排查问题,此版本中新增了部分 nmBot 运行日志。 ## 2023 年 5 月 13 日 01:23 + nmBot 版本号:23.5.1 - 修复了特定情况下,入群验证超时后,入群验证提示消息未在一段时间后自动删除的问题。 @@ -32,24 +36,29 @@ nmBot 版本号:23.5.1 ## 2023 年 5 月 11 日 13:20 ### 指令映射 + - 你现在可以使用 /mapping 指令设置自定义指令映射。设置指令映射后,你可以将你设置的自定义指令代替官方指令使用。 - 你可以使用 /unmapping 指令来取消指令映射。 - 使用 /whatis 指令查询指令信息时,支持使用你自定义的指令来查询。 - 使用 /whatis 指令查询指令信息时,返回的指令信息中现在将显示你自定义的指令。 ### 设置对话标题 + - 群组管理员现在可以通过发送 /st (/settitle) 指令快速设置对话标题。 ### 深度适配网页 App + - nmBot 面板可以通过 Telegram 直达链接 https://t.me/nmnmfunbot/panel 打开。 - 全新设计了 /panel 指令,在保留通过浏览器打开 nmBot 面板能力的基础上,新增通过 Telegram 直达链接直接在对话中作为网页 App 启动 nmBot 面板的能力。 ### 更多亮点 + - 新增数据中心查询功能:你可以使用 /dc (/datacenter) 指令查询自己、回复的用户、回复消息中的文件、回复消息的转发来源用户和在参数中提供的用户所在的 Telegram 数据中心。 - nmBot 自动拒绝骚扰用户入群功能和举报骚扰功能现在支持识别入群用户的个人签名。 - /id 功能支持显示骰子类消息的类型和骰子 (🎲)、打靶 (🎯)、保龄球 (🎳)、投篮 (🏀) 和射门 (⚽️) 消息的结果。 ### 其他更新和修复 + - 为 nmBot 面板推出了新的页面加载中视图。 - nmBot 面板“管理”页面中,对话列表中的对话现在可以通过浏览器原生支持的方式(例如长按或按住 Ctrl 键并点击)在新浏览器选项卡中打开。 - 本次更新后,群组管理员权限变化时,将自动在 nmBot 面板中注册或取消注册。 @@ -82,4 +91,3 @@ nmBot 版本号:23.5.1 - 修复了特定情况下,若 nmartChat 组织完消息前源消息被删除,nmartChat 发送返回消息失败的问题。 - 修复了特定情况下,用户被 nmBot 错误注册群组配置的问题。 - 修复了未在 nmBot 面板修改显示语言时,语言设置页面不显示正在使用的语言的问题。 - diff --git a/docs/nmbot-telegram/update-log/2023-06.md b/docs/nmbot-telegram/update-log/2023-06.md index a495229..e0334eb 100644 --- a/docs/nmbot-telegram/update-log/2023-06.md +++ b/docs/nmbot-telegram/update-log/2023-06.md @@ -4,18 +4,21 @@ index: -2306 # nmBot 2023 年 6 月功能更新 -亲爱的用户,您期盼已久的 nmBot 功能更新现已推出。 +亲爱的用户,您期盼已久的 nmBot 功能更新现已推出。\ 本更新推出了全新的警告功能,为群组管理员提供了更高的自由度和更好的使用体验。本更新强化了群组中自动删除消息功能,推出了 4 个新自动删除消息类别。本更新还包含错误修复和稳定性提升。 ## 2023 年 6 月 30 日 19:47 + nmBot 版本号:23.6.2 - 提高了 nmartChat 部分功能的性能。 ## 2023 年 6 月 14 日 23:23 + - 修正了 nmBot 面板群组管理配置 » 操作记录页面中一处提示指令错误的问题。 ## 2023 年 6 月 14 日 00:53 + nmBot 版本号:23.6.1 - 优化了特定情况下 nmBot 的运行速度。 @@ -23,6 +26,7 @@ nmBot 版本号:23.6.1 ## 2023 年 6 月 11 日 23:40 ### 全新警告:可能性无限大 + - 警告功能经过完全全新设计,带来更快的响应速度和更优质的用户体验。 - 新警告功能提供的按钮可让你快速封禁用户、解封用户或调整警告次数。 - 新警告功能新增对无限警告次数和负警告次数的支持。你可以在 nmBot /config 菜单和 nmBot 面板将警告次数调整为“无限”,使得无论警告次数多少,nmBot 都不会主动封禁用户。 @@ -31,6 +35,7 @@ nmBot 版本号:23.6.1 - 制作组将在 6 月更新完成后,将群组管理员 6 月前警告的历史记录导入新版警告中。 ### 自动删除更多类型的消息 + - 自动删除消息功能新增对以下类型消息的支持: - 长文本消息。 - 其他服务消息。 @@ -39,6 +44,7 @@ nmBot 版本号:23.6.1 - nmBot /config 菜单和 nmBot 面板“设置禁止发送的消息类别”设置集中新增了相关选项。 ### 其他更新和修复 + - 在关键词回复和入群欢迎中通过 {username} 指代用户时,若关键词回复内容解析模式为 HTML、Markdown 或 MarkdownV2 或是入群欢迎,则回复消息或欢迎消息中现在添加到用户的链接。 - 在关键词回复和入群欢迎中通过 {username} 指代用户时,回复消息或欢迎消息中将显示用户的 Telegram 会员标志。 - /dc 指令的返回消息中新增了“好”按钮,点击即可快速删除指令消息和返回消息。 @@ -87,4 +93,4 @@ nmBot 版本号:23.6.1 - 修复了 nmBot /config 菜单警告次数设置页面中,“关闭”选项选中时,对号 Emoji 和文本间缺少空格的问题。 - 修复了入群验证超时提示消息中,“举报骚扰”按钮无法使用的问题。 - 修复了举报含有某些字符组合的骚扰消息时,操作可能无法完成的问题。 -- 修复了 nmBot 管理员无法添加禁止入群用户简介的问题。 \ No newline at end of file +- 修复了 nmBot 管理员无法添加禁止入群用户简介的问题。 diff --git a/docs/nmbot-telegram/update-log/2023-07.md b/docs/nmbot-telegram/update-log/2023-07.md index e147452..32bb729 100644 --- a/docs/nmbot-telegram/update-log/2023-07.md +++ b/docs/nmbot-telegram/update-log/2023-07.md @@ -7,15 +7,16 @@ index: -2307 亲爱的用户,您期盼已久的 nmBot 功能更新现已推出。 本更新升级了“同频气氛组”功能的智能体验。本更新还包含错误修复和稳定性提升。 -**〓 补偿内容 〓** -nmBot 积分*100 +**〓 补偿内容 〓**\ +nmBot 积分\*100 -**〓 补偿范围 〓** +**〓 补偿范围 〓**\ 2023 年 6 月 22 日 12:00 前曾提交过名片申请,且已注册 nmBot 积分系统的用户。 我们将在 nmBot 2023 年 7 月版本更新后 2 个工作日内将补偿发放到受影响用户的 nmBot 积分账户。 ## 2023 年 7 月 31 日 15:37 + nmBot 版本号:23.7.1 - 更新了后续功能的所需资源。 @@ -23,10 +24,12 @@ nmBot 版本号:23.7.1 ## 2023 年 7 月 7 日 11:30 ### 同频气氛组增智慧 + - 特定情况下,“同频气氛组”功能会识别频道的名称和简介,以提供更符合频道主题的评论。 - 优化了同频气氛组部分语气的效果。 ### 其他更新和修复 + - nmBot 自动删除骚扰消息与自动拒绝骚扰用户入群功能对用户执行操作时,将尝试向该用户发送一条提示消息,提供触发规则的具体情况和解除封禁的渠道。 - nmBot /config 菜单“警告”子菜单中,“关闭”和“无限次”按钮调整为单独在新行显示。 - 对 nmBot 内容本地化做出了如下调整和修复: @@ -41,6 +44,6 @@ nmBot 版本号:23.7.1 - nmBot 行内功能菜单中不再显示“发送名片”选项。 - nmBot 面板“我的名片”页面中文本框调整为只读,并显示相应提示。 - 删除了 nmBot 名片功能的相关资源。 -- 下线了旧版 nmBot 帮助文档及其打开指令 /help_legacy。在更新前打开的旧版 nmBot 帮助文档中的部分按钮也将失效。 +- 下线了旧版 nmBot 帮助文档及其打开指令 /help_legacy。在更新前打开的旧版 nmBot 帮助文档中的部分按钮也将失效。 - 删除了旧版 nmBot 帮助文档的相关资源。 - 更新了后续功能的所需资源。 diff --git a/docs/nmbot-telegram/update-log/2023-08.md b/docs/nmbot-telegram/update-log/2023-08.md index ca6ee9b..d708791 100644 --- a/docs/nmbot-telegram/update-log/2023-08.md +++ b/docs/nmbot-telegram/update-log/2023-08.md @@ -8,16 +8,19 @@ index: -2308 本更新为 nmBot 引入了全新的面板后端代码库,还推出了全新版本 nmBot 面板(测试版)。本更新还包含错误修复和稳定性提升。 ## 2023 年 8 月 19 日 22:57 + nmBot 版本号:23.8.3 - 修复了特定情况下无法完成入群验证频道设置的问题。 ## 2023 年 8 月 16 日 17:07 + nmBot 版本号:23.8.2 - 修复了 banme 功能开关失效,导致该功能在群组中始终开启的问题。 ## 2023 年 8 月 9 日 17:44 + nmBot 版本号:23.8.1 - 修复了 2023 年 8 月功能更新后,使用个人身份进行“群友互动”时 nmBot 未正常发送消息的问题。 @@ -25,15 +28,18 @@ nmBot 版本号:23.8.1 ## 2023 年 8 月 9 日 17:00 ### 启用统一代码库 + - 我们正在将 nmBot 面板后端的代码库与 nmBot 服务端统一。其中的一部分改动已于 7 月更新在服务器端部署,本次更新则包含补丁包。 - 新的 nmBot 面板后端采用更先进的技术,现在是 nmBot 服务端的一部分。此更改使得我们仅需编写一次代码即可同时在 nmBot 和 nmBot 面板中应用某些更改,例如,新版 nmBot 面板后端在记录操作记录时,可以直接调用 nmBot 服务端的响应代码来完成操作,这也能解决旧版 nmBot 面板后端在记录操作记录时,部分字符乱码的问题。 ### 全新 nmBot 面板(测试版) + - 新的代码库为我们提供了更加非凡的创造力,因此我们开发了全新的 nmBot 面板。 - 全新 nmBot 面板采用更现代的 Web 技术,并经过重新设计,使得您无论是调整群组配置、修改关键词回复,还是查询自己的积分信息,操作都更加明了和便捷。 - 您现在就可以加入新 nmBot 面板候补名单。nmBot 面板将在今年晚些时候对所有用户可用。 ### 其他更新和修复 + - nmBot /config 菜单入群验证设置页面中新增了一则提示:若在消息限制与智能识别 > 设置禁止发送的消息类别设置中启用了入群系统消息,验证消息中将隐藏新成员的全名。(该文本调整不影响实际功能。) - 删除了警告功能的警告对象必须在群组中的限制。 - Telegram 数据中心查询功能的帮助中,新增了第三方链接在互联网档案馆的存档链接。 diff --git a/docs/nmbot-telegram/update-log/2023-09.md b/docs/nmbot-telegram/update-log/2023-09.md index 83923aa..99d3205 100644 --- a/docs/nmbot-telegram/update-log/2023-09.md +++ b/docs/nmbot-telegram/update-log/2023-09.md @@ -4,35 +4,42 @@ index: -2309 # nmBot 2023 年 9 月功能更新 -亲爱的用户,您期盼已久的 nmBot 功能更新现已推出。 +亲爱的用户,您期盼已久的 nmBot 功能更新现已推出。\ 本更新推出了“限制群成员使用指令”功能,为群组管理员提供了自定义机器人的有力工具;本更新还包含错误修复和稳定性提升。 ## 2023 年 9 月 21 日 23:30 + nmBot 版本号:23.9.3 - 修复了特定情况下 nmBot 管理员无法查询骚扰举报详情的问题。 ## 2023 年 9 月 17 日 23:25 + nmBot 版本号:23.9.2 - 解决了一些已知问题。 ## 2023 年 9 月 15 日 00:50 + nmBot 版本号:23.9.1 - 修复了特定情况下部分功能无法正确获取群组配置的问题。 - 更新了性能分析工具。 ## 2023 年 9 月 9 日 14:45 + ### 限制群成员使用指令 + - 针对人数超过 2000 人的群组,群组管理员现在可以自定 nmBot 响应的指令。 - 群组管理员可以在新版 nmBot 面板(测试版)“群组”»(要启用该功能的群组)»“群组管理功能”中自定义群成员可使用的指令;如果群成员发送不可使用的指令,nmBot 将不会发送任何消息,而不是提醒用户该功能无法使用。 - 所有 nmBot 的可用指令全部都可以受该设置控制,同时即使用户设置了指令映射,他发送映射的指令也不会被响应。群组管理员自身不受设置影响。 ### 本地化更上一层楼 + - nmBot 的名称、简介和“此机器人能做什么?”简介现在可以用户 Telegram 客户端设置的语言显示。目前支持的语言:简体中文、英文。 ### 其他更新和修复 + - 为“同频气氛组”功能的消息添加了评分按钮。 - 由于不受 Telegram 支持,删除了 nmBot 指令列表的配置文件中繁体中文相关的内容。 - 对 nmBot 内容本地化做出了如下调整和修复: @@ -49,6 +56,7 @@ nmBot 版本号:23.9.1 - 优化了 nmBot 程序的内存利用率。 ### 新版 nmBot 面板同步更新 + - 首次启动 nmBot 面板时现在将显示欢迎页面。 - 通过 Web App 启动新版 nmBot 面板时,页面主题色设置为“根据 Telegram 设置”时,页面背景色可在 Telegram 主题色切换时自动切换。 - 在“群组”页面中搜索群组时,现在支持通过群组的用户名搜索群组。 diff --git a/docs/nmbot-telegram/update-log/2023-10.md b/docs/nmbot-telegram/update-log/2023-10.md index 752794b..c48d68d 100644 --- a/docs/nmbot-telegram/update-log/2023-10.md +++ b/docs/nmbot-telegram/update-log/2023-10.md @@ -4,29 +4,35 @@ index: -2310 # nmBot 2023 年 10 月功能更新 -亲爱的用户,您期盼已久的 nmBot 功能更新现已推出。 +亲爱的用户,您期盼已久的 nmBot 功能更新现已推出。\ 本更新包含新版 nmBot 面板,为您修改 nmBot 配置和查询信息提供了 nmBot 史上最佳的渠道。本更新还包含重要的系统优化、错误修复和稳定性提升。 ## 2023 年 10 月 25 日 22:57 + - 修复了名称中含有部分特殊字符的用户在登录新版 nmBot 面板时,会遇到页面提示验证失败,无法正常登录的问题。 ## 2023 年 10 月 20 日 16:16 + nmBot(API 端)版本号:23.10.3 - 修复了 23.10.2 版本更新后特定情况下,通过 nmBot 面板添加或修改关键词回复时,关键词回复无法正确保存的问题。 ## 2023 年 10 月 20 日 00:53 + nmBot(API 端)版本号:23.10.2 - 修复了特定情况下,通过 nmBot 面板首次添加关键词回复时,关键词回复无法正确保存的问题。 ## 2023 年 10 月 8 日 01:18 + nmBot(API 端)版本号:23.10.1 - 修复了特定违规操作下每天可以在 nmBot 面板重复签到的问题。 ## 2023 年 10 月 4 日 21:00 + ### 正式推出新版 nmBot 面板 + - 全新版本的 nmBot 面板现已正式面向所有用户推出。 - 全新 nmBot 面板采用更现代的 Web 技术,并经过重新设计,使得您无论是调整群组配置、修改关键词回复,还是查询自己的积分信息,操作都更加明了和便捷。 - 以下功能中的相应链接现已替换为新版 nmBot 面板: @@ -36,6 +42,7 @@ nmBot(API 端)版本号:23.10.1 - nmBot 被邀请到新群组中时,发送的欢迎消息。 ### 系统优化 + - “同频气氛组”功能新增选项“禁止拷贝和转发回复”:启用该选项后,“同频气氛组”功能发送的回复消息将不再能拷贝和转发。这有助于禁止群组成员粘贴和复读“同频气氛组”功能发送的回复消息。 - nmBot 面板“群组”页面中,有群组信息加载失败时,页面显示的另有群组加载失败信息中的“重试”按钮已替换为“详情”。用户可以点击该按钮了解详细信息。 - nmBot 面板“群组”页面中,有群组信息加载失败时,详细信息中新增删除加载失败群组的选项。 @@ -46,6 +53,7 @@ nmBot(API 端)版本号:23.10.1 - 调整了 nmBot 面板“群组”页面中“已启用群组管理功能”提示文本的英文大小写。 ### 其他更新和修复 + - 优化了 /id 功能的消息类型判断逻辑,以提高性能。 - 重新设计了事件处理逻辑中的数据使用逻辑,以减少内存占用。 - 优化了用户语言、群组的管理员列表、用户管理群组列表的内存回收机制。 @@ -56,4 +64,4 @@ nmBot(API 端)版本号:23.10.1 - 修复了新版 nmBot 面板欢迎页面中,部分图片资源加载失败的问题。 - 删除了调试群组功能。 - 删除了旧版日志的相关资源。 -- 删除了旧版 /about 功能的相关资源。 \ No newline at end of file +- 删除了旧版 /about 功能的相关资源。 diff --git a/docs/nmbot-telegram/update-log/2023-11.md b/docs/nmbot-telegram/update-log/2023-11.md index 0815e3a..df89db1 100644 --- a/docs/nmbot-telegram/update-log/2023-11.md +++ b/docs/nmbot-telegram/update-log/2023-11.md @@ -8,11 +8,13 @@ index: -2311 本更新推出的“自动删除指令消息”功能为管控群组消息提供了简单而强大的解决方案,好懂好方便;“个性化面板颜色”将您的多彩性格真情真释放;全新的积分页面使得您的积分更靓更显眼。本更新还包含对 nmBot 面板的优化、错误修复和稳定性提升。 ## 2023 年 11 月 9 日 16:42 + nmBot 版本号:23.11.2 - 修复了“自动删除指令消息”功能设为“删除所有指令消息”时,有概率错误删除群组中非指令消息的问题。 ## 2023 年 11 月 7 日 22:00 + nmBot 版本号:23.11.1 - 修复了特定情况下,对部分名称中特定位置(如末尾)含有 Emoji(如“生如夏花🌸”)的用户返回部分隐藏的用户名中,Emoji 可能被错误截断,导致 Emoji 显示不完整或无法正常显示的问题。[目前针对部分特殊 Emoji(如 🏳️‍⚧️)仍可能存在 Emoji 被错误截断,显示的 Emoji 与实际不匹配的问题,该问题不影响显示的 Emoji 的完整性,我们将尝试在后续修复。] @@ -22,21 +24,26 @@ nmBot 版本号:23.11.1 - 删除了部分程序调试输出。 ## 2023 年 11 月 7 日 20:40 + ### 自动删除指令消息 + - 推出“自动删除指令消息”功能:nmBot 将识别群组中发送的形如 action@usernameofbot 的指令消息。 - 您可以控制三种 nmBot 识别和删除指令消息的方式:仅删除呼唤 nmBot 的指令消息、删除呼唤 nmBot 和未指定机器人的指令消息、删除所有指令消息。 ### 个性化面板颜色 + - 您可以更换 nmBot 面板设置项图标的背景颜色,使其和您的个人风格匹配。 - 使用部分背景颜色时,设置项中部分其他元素的颜色也可以发生相应变化,与所选颜色相得益彰。 - 目前提供的设置项图标背景颜色选项:蓝色(默认)、骄傲、彩虹。 - 您可以在 nmBot 面板“设置”页面下“外观”设置项中更改“设置项图标背景颜色”选项。 ### 积分页面换新颜 + - nmBot 面板“积分”页面中现在显示新的卡片视图,您的积分信息更显眼。 - 您可以点击卡片了解您 nmBot 积分的详细信息。 ### 系统优化 + - 通过迷你 App 启动 nmBot 面板时,迷你 App 的返回按钮现在将返回到当前页面的上一级,而不是页面历史中的上一个页面。 - nmBot 面板新增服务器选择功能:用户可以在登录页面和“设置”页面选择要使用的服务器。(在当前版本中没有其他服务器供选择。) - 登录 nmBot 面板时可以取消登录过程。 @@ -47,6 +54,7 @@ nmBot 版本号:23.11.1 - 为 nmBot 面板“群组管理功能”中所有原先为添加图标的“入群验证”设置项、群组页面中同频气氛组“语气偏好”设置项、“编辑关键词回复”页面“匹配以下消息内容”中的设置项添加了图标。 ### 其他更新和修复 + - 更新了 /privacy 指令的返回消息:该消息中现在将提示关于 nmBot 服务隐私政策的信息。 - 更新了 /about 指令的返回消息:该消息中现在将提示使用 nmBot 即表明您已阅读并同意 nmBot 服务使用条款和隐私政策的信息。 - 优化了“自动识别并删除骚扰消息”和入群验证“自动拒绝骚扰用户入群”功能向用户发送的提示消息:现在可以通过消息内容确认触发的是“自动识别并删除骚扰消息”功能或入群验证“自动拒绝骚扰用户入群”功能,以便 nmBot 管理员执行操作。 diff --git a/docs/nmbot-telegram/update-log/2023-12.md b/docs/nmbot-telegram/update-log/2023-12.md index a8a76d4..3a472f6 100644 --- a/docs/nmbot-telegram/update-log/2023-12.md +++ b/docs/nmbot-telegram/update-log/2023-12.md @@ -6,18 +6,22 @@ index: -2312 ![更新宣传图](../img/update-pictures/nmbot-2312-christmas.jpg) -亲爱的用户,您期盼已久的 nmBot 功能更新现已推出。 +亲爱的用户,您期盼已久的 nmBot 功能更新现已推出。\ 本更新推出新的同频气氛组可选语气“病娇”。本更新还优化了 nmBot 面板的体验,使这款遥遥领先的面板体验更进一步。本更新还包含错误修复和稳定性提升。 ## 2023 年 12 月 7 日 21:40 + ### 同频气氛组新语气 + - 推出新的同频气氛组可选语气“病娇”。新语气可在 nmBot /config 菜单和 nmBot 面板设置。 - 优化了同频气氛组语气“称赞”“讽刺”“热情”和“猫娘”的消息回复效果。 ### 快速打开 nmBot 面板配置群组 + - 在 nmBot /config 指令的返回消息中增设了“在 nmBot 面板中配置此群组”按钮:通过该按钮,群组管理员可以直接进入 nmBot 面板的此群组配置页面。 ### nmBot 面板体验优化 + - 新增了对从右向左语言的支持。 - 增加了 nmBot 面板中以下编辑框的高度: - “编辑关键词回复”页面中“关键词”和“回复”编辑框。 @@ -29,6 +33,7 @@ index: -2312 - nmBot 面板“设置”页面现在将显示当前版本 nmBot 面板的版本号。 ### 其他更新和修复 + - 更改了 nmBot 内,http 请求的相关实现。 - 更改了“图片转贴纸”和“图片转 Emoji”功能的相关实现。 - 对 nmBot 内容本地化做出了如下调整和修复: diff --git a/docs/nmbot-telegram/update-log/2024-01.md b/docs/nmbot-telegram/update-log/2024-01.md index b6b09e2..e20c2d5 100644 --- a/docs/nmbot-telegram/update-log/2024-01.md +++ b/docs/nmbot-telegram/update-log/2024-01.md @@ -6,31 +6,36 @@ index: -2401 ![更新宣传图](../img/update-pictures/nmbot-2401-winter.jpg) -亲爱的用户,您期盼已久的 nmBot 功能更新现已推出。 +亲爱的用户,您期盼已久的 nmBot 功能更新现已推出。\ 本更新为 nmBot 和 nmBot 面板带来了颇具特色的火星文;本更新还包含错误修复和稳定性提升。 ## 2024 年 1 月 20 日 23:37 + nmBot 版本号:24.1.1 - 修复了特定情况下,“自动识别并删除骚扰消息”和入群验证“自动拒绝骚扰用户入群”功能的部分日志数据中,群组 ID 记录错误的问题。 ## 2024 年 1 月 20 日 23:19 + ### 火星文强势加入 nmBot + - 带有品牌特色的火星文现已全面加入 nmBot 和 nmBot 面板。 - 您可以向 nmBot 发送 /lang 指令来设置 nmBot 语言,并在 nmBot 面板的“设置”页面中设置 nmBot 面板语言。 ### 自动删除提示与解封流程优化 + - 新增了 nmTeam 支持帮助被“自动识别并删除骚扰消息”和入群验证“自动拒绝骚扰用户入群”功能误封禁的用户在被封禁群组解除封禁的能力: - 在 nmBot /config 菜单的“消息限制与智能识别”“自动识别并删除骚扰消息”功能和 nmBot 面板的“群组管理功能”页面中,可启用或关闭“允许 nmTeam 支持解封误封用户”设置。 - 若群组启用了设置,群组中被 nmBot“自动识别并删除骚扰消息”和入群验证“自动拒绝骚扰用户入群”功能封禁的用户,在联系 nmTeam 支持,并经由 nmTeam 支持认定为误封的用户,nmTeam 支持将为该用户在该群组中解除封禁。 - nmTeam 支持为该用户在该群组中解除封禁时,会在群组中发送提示。 - 该能力经过严格限制,nmTeam 支持仅能在通过“自动识别并删除骚扰消息”和入群验证“自动拒绝骚扰用户入群”功能生成的操作 ID 帮助实际涉及用户解封。 - - 在新加入的群组中,该选项将默认开启;功能更新前已使用 nmBot 的群组,该选项将默认关闭。 + - 在新加入的群组中,该选项将默认开启;功能更新前已使用 nmBot 的群组,该选项将默认关闭。 - 调整了“自动识别并删除骚扰消息”和入群验证“自动拒绝骚扰用户入群”功能在群组中和对用户发送的消息: - 新增了“操作 ID”显示,取代了“触犯规则”显示。 - 更新了对用户发送的消息中,关于解除封禁的提示内容。 ### 其他更新和修复 + - 通过迷你 App 启动 nmBot 面板时,Telegram App 标题栏中现在支持显示“设置”选项。 - nmBot 面板“设置”页面中新增了 nmBot 面板 API 版本的显示。 - 我们已将 nmBot 面板“入群验证”设置调整到位于“群组管理功能”的独立页面。 diff --git a/docs/nmbot-telegram/update-log/2024-02.md b/docs/nmbot-telegram/update-log/2024-02.md index 7c090c7..934b30c 100644 --- a/docs/nmbot-telegram/update-log/2024-02.md +++ b/docs/nmbot-telegram/update-log/2024-02.md @@ -6,10 +6,11 @@ index: -2402 ![更新宣传图](../img/update-pictures/nmbot-2402-spring-festival.jpg) -亲爱的用户,您期盼已久的 nmBot 功能更新现已推出。 +亲爱的用户,您期盼已久的 nmBot 功能更新现已推出。\ 本更新推出了“赠品活动追踪”功能,帮助您管理参与的赠品活动;本更新还包含错误修复和稳定性提升。 ## 2024 年 5 月 26 日 11:51 + nmBot 版本号:24.2.9 - 新增了一个新的探测方式,由于识别并删除群组中特定类型的骚扰消息。 @@ -17,6 +18,7 @@ nmBot 版本号:24.2.9 - 更新了“同频气氛组”功能的内部配置项。 ## 2024 年 5 月 8 日 19:30 + nmBot 版本号:24.2.8 - 优化了数据中心查询功能:在启用话题功能的群组话题中使用该功能,未回复消息时,话题的创建者不再被视为查询对象。 @@ -24,37 +26,44 @@ nmBot 版本号:24.2.8 - 调整了使用 /spam 指令时,建议群组启用“允许 nmTeam 支持解封误封用户”功能的提示出现的概率。 ## 2024 年 4 月 25 日 01:00 + nmBot(面板端)版本号:24.2.7 - 修复了 24.2.5 版本更新后单屏模式下页面内容无法正常显示的问题。 ## 2024 年 4 月 25 日 00:54 + nmBot(面板端)版本号:24.2.6 - 缓解了 24.2.5 版本更新后单屏模式下页面内容无法正常显示的问题。目前仍有部分显示问题将于后续更新修复。 ## 2024 年 4 月 25 日 00:43 + nmBot 版本号:24.2.5 - 修复了匿名管理员通过频道透视眼使用 /warn 指令时,返回消息中会显示管理员身份的问题。 - 更新了上游依赖版本,消除 CVE-2024-21508, CVE-2024-21509, CVE-2024-21507, CVE-2024-21511 的潜在风险。 ## 2024 年 3 月 31 日 20:25 + nmBot 版本号:24.2.4 - 更新了后续功能的所需资源。 ## 2024 年 3 月 27 日 20:37 + nmBot(API 端)版本号:24.2.3 - 修复了在 nmBot 面板“操作记录”页面中搜索时有概率出现预期外结果的问题,并恢复了“操作记录”页面的搜索功能。 ## 2024 年 3 月 27 日 15:39 + nmBot(API 端)版本号:24.2.2 - 由于在 nmBot 面板“操作记录”页面中搜索时有概率出现预期外结果,暂时关闭了“操作记录”页面的搜索功能。我们将在相关问题修复后重新启用“操作记录页面的搜索功能。 ## 2024 年 2 月 29 日 22:16 + nmBot 版本号:24.2.1 - 修复了在 Android 版 Telegram、iOS 版 Telegram 等部分 Telegram App 中,“赠品活动追踪”功能发送的活动已开奖提示中,到活动频道或群组的按钮点击时不起作用的问题。(修复前发送的消息,其中的按钮仍然不起作用。对于修复前发送的消息,可以通过 Telegram Desktop 点击按钮,或长按拷贝按钮链接后,将链接粘贴并在链接后添加“/1”,发送后再尝试打开链接。) @@ -62,10 +71,12 @@ nmBot 版本号:24.2.1 ## 2024 年 2 月 22 日 14:00 ### 赠品活动追踪 + - nmBot 可以帮助您管理参与的赠品活动。 - 仅需将赠品活动转发到与 nmBot 的私信中,nmBot 便会在该赠品活动结束后提示您。 ### 其他更新和修复 + - 调整了 /spam 功能的返回消息: - 新增了建议群组启用“允许 nmTeam 支持解封误封用户”功能的提示。 - 调整了消息中“提示:可以在 /spam 指令后追加举报原因,帮助 nmBot 团队了解具体情况”提示的出现概率。 diff --git a/docs/nmbot-telegram/update-log/2024-07.md b/docs/nmbot-telegram/update-log/2024-07.md index 98c7c6f..830db46 100644 --- a/docs/nmbot-telegram/update-log/2024-07.md +++ b/docs/nmbot-telegram/update-log/2024-07.md @@ -5,6 +5,7 @@ index: -2407 # nmBot 2024 年 7 月功能更新 ## 新鲜事儿 + ### 正则助手、自动登录、迷你 App 优化和更多——2024 年 7 月功能更新 ![](https://telegra.ph/file/18a416fb3659359b991ea.png) @@ -12,25 +13,31 @@ index: -2407 nmBot 已更新到 2024 年 7 月版本,这是自 nmBot NEXT 上线以来的首次功能更新。 #### 正则助手 + 你是否曾经因正则表达式不工作而烦恼?在 2024 年 7 月更新中,我们带来了”正则助手“。这款最新增加的小工具在你编辑正则表达式的同时就能指出你正则表达式中的错误。写对正则表达式后,你还可以快速检查正则表达式合不合你的心意。 #### 自动登录 + nmBot NEXT 带来了对通过 nmTeam 账号登录和保存会话的支持,今天我们让登录体验更进一步。您可以在设置中启用自动登录功能,无需在登录页面点击即可登录。 我们还优化了登录的多处体验,让整个流程更加清晰明了。 #### 迷你 App 优化 + nmBot 面板与迷你 App 继续深度融合,带来了沉浸顶栏颜色、自动展开和多处操作体验优化。 #### 更多更新 + 我们更新了多个用户期待已久的功能,还修复了一些问题。要了解更多更新内容,你可以在下方查看完整的更新日志。 ## 2024 年 7 月 18 日 19:23 + nmBot 版本号:24.7.2-wine+5156 - 新增了一项工具,用于批量为被误封的用户在群组解除封禁。 ## 2024 年 7 月 15 日 11:20 + nmBot 版本号:24.7.1-wine+5154 - 修复了使用 /admin 指令时“呼叫群组管理员”功能无法正常使用的问题。 @@ -38,7 +45,9 @@ nmBot 版本号:24.7.1-wine+5154 - nmBot 面板支持隐藏已结束的积分活动。 ## 2024 年 7 月 12 日 15:50 + ### 正则助手 + - ”在“关键词回复”功能中新增“正则助手”,帮助用户撰写匹配关键词。 - 在 nmBot 面板编辑关键词回复匹配的正则表达式时,“正则助手”将实时提示用户撰写的正则表达式是否正确。 - 用户撰写的正则表达式合法时,通过“测试匹配”按钮,用户可快速使用文本测试能否匹配正则表达式。 @@ -46,6 +55,7 @@ nmBot 版本号:24.7.1-wine+5154 - 用户撰写的正则表达式非法时,无法保存关键词回复。 ### 自动登录 & 登录体验优化 + - nmBot 面板新增“自动登录”功能,以便在仅一个账号登录 nmBot 面板时无需点按头像即可登录。 - nmBot 面板登录页面新增了未绑定 Telegram 的 nmTeam 账号请使用 Telegram 登录的提示。 - 优化了长名称和长用户名在 nmBot 面板登录页面的显示效果。 @@ -53,15 +63,17 @@ nmBot 版本号:24.7.1-wine+5154 - 由于网络问题无法登录 nmBot 面板时,提示中新增“重试”按钮。 ### 迷你 App 适配 + - 通过迷你 App 使用 nmBot 面板时支持沉浸 Telegram 迷你 App 标题栏和背景颜色。 - 通过迷你 App 使用 nmBot 面板时,nmBot 面板将自动设置禁用滑动来关闭页面。 - nmBot 面板调试菜单支持显示 Telegram Web App 信息并执行 JavaScript。 - 使用不支持部分功能的 Telegram 版本启动 nmBot 面板时,现在 nmBot 面板支持显示相应提示。 - 在 iOS 或 iPadOS 上通过迷你 App 使用 nmBot 面板时,页面将不再能通过系统手势缩放。 -通过迷你 App 使用 nmBot 面板时,页面将自动展开。 -* 部分功能需要用户的 Telegram App 版本支持。 +- 通过迷你 App 使用 nmBot 面板时,页面将自动展开。 +- 部分功能需要用户的 Telegram App 版本支持。 ### 更多优化 + - nmBot 面板新增“新鲜事儿”显示,供用户了解最近更新的新功能。 - 群组“操作记录”支持记录其他群组管理员对成员进行的权限操作。 - 在消息被识别为骚扰的提示消息中,可以直接封禁用户/解除封禁用户/将用户加入白名单。 @@ -74,6 +86,7 @@ nmBot 版本号:24.7.1-wine+5154 - 优化了“指令列表”页面指令的显示。 ### 问题修复 + - 修复了“频道透视眼”无法获取用户信息时,消息处理流程中断的问题。 - 修复了呼叫群组管理员功能中,部分按钮显示条件和实际效果存在的问题。 - 修复了处理骚扰拦截时,长消息内容的 100 位恰为部分特殊字符时判断时出错的问题。 diff --git a/docs/nmbot-telegram/update-log/2024-08.md b/docs/nmbot-telegram/update-log/2024-08.md index 93ee2fc..81d5943 100644 --- a/docs/nmbot-telegram/update-log/2024-08.md +++ b/docs/nmbot-telegram/update-log/2024-08.md @@ -5,6 +5,7 @@ index: -2408 # nmBot 2024 年 8 月功能更新 ## 新鲜事儿 + ### 置顶对话、高效手势和系统优化——2024 年 8 月功能更新 ![](../img/update-pictures/nmbot-2408.png) @@ -32,11 +33,13 @@ index: -2408 同样,我们也修复了一些已知问题。要了解完整更新内容,您可以[在此](https://nmbotdoc.nmnm.fun/update-log/2024-08/)查看更新日志。 ## 2024 年 9 月 13 日 22:07 + nmBot 版本号:24.8.3-wine+5421 - 修复了特定情况下新成员无法通过应用特定设置的群组的网页验证的问题。 ## 2024 年 8 月 4 日 12:30 + nmBot 版本号:24.8.2-wine+5420 - 优化了“同频气氛组”功能中可能出现错误格式的问题。 @@ -45,24 +48,29 @@ nmBot 版本号:24.8.2-wine+5420 - 修复了消息触发骚扰拦截时,会尝试向发言频道发送提示消息的问题。 ## 2024 年 8 月 3 日 19:10 + nmBot 版本号:24.8.1-wine+5415.1000 - 新增一种骚扰拦截功能实现。应当能提供更高的性能,并优化机器人在服务高峰期的响应速度。 ## 2024 年 8 月 2 日 19:51 + nmBot 版本号:24.8.1-wine+5415 - 优化了骚扰拦截功能的逻辑。 - 修复了特定情况下消息发送者身份识别错误的问题。 ## 2024 年 8 月 1 日 14:25 + ### 便捷操作 + - nmBot 面板支持将对话置顶。 - 手机模式下,支持触屏设备使用手势打开页面切换器、返回上一页或返回首页。 - nmBot 面板“关键词回复”页面支持多选关键词回复删除。 - nmBot 面板的对话列表中的对话、关键词回复列表中的关键词回复支持上下文菜单。 ### 体验优化 + - 过塑功能的返回消息中现在添加了取消按钮。 - 进行网页验证时,页面上现在支持显示验证的剩余时间。 - /white 指令的返回消息中添加了将用户从白名单中移除的说明。 @@ -73,6 +81,7 @@ nmBot 版本号:24.8.1-wine+5415 - 群组未启用“群组管理功能”时,“智能聊天”页面增加了相应的提示。 ### 其他改进 + - 服务器负载高时将暂停新骚扰消息匹配,来保障服务正常运行。 - 处理时间与事件时间相差大于 30 秒的入群事件将不会触发入群验证,以免用户因 nmBot 服务延迟而无法验证。 - /config 指令的返回消息中添加了当前操作群组的名称的显示。 @@ -83,6 +92,7 @@ nmBot 版本号:24.8.1-wine+5415 - 更新了部分数据的缓存机制。 ### 问题修复 + - 删除了代码库中与旧版 nmBot 有关的代码。 - 修复了使用原生入群验证的群组,管理员在 Telegram 客户端手动处理验证后,超时事件和入群验证流程仍能触发的问题。 - 修复了特定情况下用户首次使用 nmBot 面板时 nmBot 面板记录的语言代码不正确的问题。 diff --git a/docs/nmbot-telegram/update-log/2024-09.md b/docs/nmbot-telegram/update-log/2024-09.md index d32f991..c4802b4 100644 --- a/docs/nmbot-telegram/update-log/2024-09.md +++ b/docs/nmbot-telegram/update-log/2024-09.md @@ -5,6 +5,7 @@ index: -2409 # nmBot 2024 年 9 月功能更新 ## 新鲜事儿 + ### 骚扰拦截新举措、频道管理好工具和更多——2024 年 9 月功能更新 ![](../img/update-pictures/nmbot-2409.png) @@ -26,16 +27,19 @@ index: -2409 除了常规优化和修复外,我们还带来了一些让 nmBot 更聪明的改进,不知道你是否能发现呢? ## 2024 年 9 月 27 日 19:10 + nmBot 版本号:24.9.2-wine+5442 - 修复了触发 /banme 指令时,被“饶恕”的用户被误永久禁言,且文本提示错误的问题。 ## 2024 年 9 月 25 日 22:29 + nmBot 版本号:24.9.1-wine+5441.1000 - 修复了启用“限制群成员使用指令”功能时,存在使用频道身份并启用“频道透视眼”的群组管理员,无法正常使用权限设为“仅群组管理员”的指令的问题。 ## 2024 年 9 月 18 日 21:42 + ### 入群验证升级 - 新增入群验证类型“星币验证”。 diff --git a/docs/nmbot-telegram/update-log/2024-10.md b/docs/nmbot-telegram/update-log/2024-10.md index a217e33..67e16eb 100644 --- a/docs/nmbot-telegram/update-log/2024-10.md +++ b/docs/nmbot-telegram/update-log/2024-10.md @@ -5,6 +5,7 @@ index: -2410 # nmBot 2024 年 10 月功能更新 ## 新鲜事儿 + ### 提速提质大优化——2024 年 10 月功能更新 ![](../img/update-pictures/nmbot-2410.png) @@ -24,11 +25,13 @@ index: -2410 除此之外,我们还对各项 nmBot 功能带来了 20 多项优化和 10 多项问题修复,希望能为您带来更好的体验。 ## 2024 年 11 月 2 日 18:38 + nmBot 版本号:24.10.0-wine+5600.3000 - 修复了特定情况下用户可绕过管理员身份使用 /pin 和 /unpin 指令置顶消息的问题。 ## 2024 年 10 月 18 日 16:27 + nmBot 版本号:24.10.0-wine+5600.2000 - 修复了 nmBot 面板宽屏视图下,收窄模式的侧栏中异常显示滚动条的问题。 @@ -36,9 +39,10 @@ nmBot 版本号:24.10.0-wine+5600.2000 - 修复了 nmBot 面板更新引导提示中异常显示调试信息的问题。 ## 2024 年 10 月 17 日 10:30 + ### 面板操作全面提速 -- 通过对 nmBot 面板的加载流程进行优化,加载速度最高可提升至 2 倍*: +- 通过对 nmBot 面板的加载流程进行优化,加载速度最高可提升至 2 倍\*: - 优化了 nmBot 面板本地化字符串的加载逻辑:现在仅加载用户当前所需语言的字符串。 - 优化了 nmBot 面板的图标逻辑:网页资源中现在仅包含页面中使用的图标资源。 - 优化了 nmBot 面板的打包逻辑:网页资源文件和图标文件将单独加载。 @@ -95,4 +99,4 @@ nmBot 版本号:24.10.0-wine+5600.2000 - 修复了 nmartChat 功能发送异常消息的问题。 - 修复了数据中心查询功能的返回消息中,“了解关于此查询的更多信息”链接不起作用的问题。 -\* 实验室数据。 \ No newline at end of file +\* 实验室数据。 diff --git a/docs/nmbot-telegram/update-log/2024-12.md b/docs/nmbot-telegram/update-log/2024-12.md index 7154f86..cea5b1e 100644 --- a/docs/nmbot-telegram/update-log/2024-12.md +++ b/docs/nmbot-telegram/update-log/2024-12.md @@ -5,6 +5,7 @@ index: -2412 # nmBot 2024 年 12 月功能更新 ## 新鲜事儿 + ### 全屏迷你 App 和 nmBot+ 新权益——2024 年 12 月功能更新 ![](../img/update-pictures/nmbot-2412.png) @@ -26,6 +27,7 @@ nmBot+ 推出后的反响远超我们的预期!为了感谢您的支持,本 我们同样在此版本带来了 20 多项优化和修复,请查阅更新日志了解详情。 ## 2024 年 12 月 18 日 16:38 + nmBot 版本号:24.12.2-wine+6006 - 关键词回复保存失败时,现在将显示完整的错误提醒。 @@ -34,6 +36,7 @@ nmBot 版本号:24.12.2-wine+6006 - 提高了关键词回复“AI 智能生成 (Beta)”的可靠性。 ## 2024 年 12 月 17 日 19:30 + nmBot 版本号:24.12.1-wine+6000 - 从旧版 nmBot 中转移的关键词回复不再受关键词回复数量限制的限制。 diff --git a/docs/nmbot-telegram/update-log/2025-01.md b/docs/nmbot-telegram/update-log/2025-01.md index b9b9ecb..ddff79e 100644 --- a/docs/nmbot-telegram/update-log/2025-01.md +++ b/docs/nmbot-telegram/update-log/2025-01.md @@ -5,6 +5,7 @@ index: -2501 # nmBot 2025 年 1 月功能更新 ## 新鲜事儿 + ### 2024 年度报告、配置搜索和更多——2025 年 1 月功能更新 ![](../img/update-pictures/nmbot-2501.png) @@ -34,24 +35,30 @@ nmBot 迎来 2025 年的第一次更新!在这个辞旧迎新的时刻,我 ## 2025 年 1 月 17 日 14:30 ### 2024 年度报告 -- 上线 nmBot 2024 年度报告。您可以在 nmBot 面板“发现” > “2024 年度报告”中回顾您与 nmBot 共同走过的 2024。 + +- 上线 nmBot 2024 年度报告。您可以在 nmBot 面板“发现” > “2024 年度报告”中回顾您与 nmBot 共同走过的 2024。 ### 配置搜索 + - 支持在 nmBot 面板搜索群组配置。 ### 封禁并删除最近消息 + - 新增 /dban, /dmute, /dkick 和 /dwarn 指令,供群组管理员封禁/禁言/踢出/警告用户并删除该用户最近在群组中的消息。 ### 操作名称 + - 操作记录中支持显示完整的更改配置名称。 ### nmBot 面板优化 + - 部分设置项链接按钮的文本现在自动拓展到右侧的空白显示区域。 - 标题栏隐藏的按钮中现可使用新增的返回按钮进行返回。 - 优化了迷你 App 全屏模式下,启用高级页面切换效果时页面操作按钮的显示问题。 - 优化了加载 nmBot 面板管理对话列表的速度。 ### 其他改进 + - 新增 nmBot+ 订阅方案。 - 匹配异常的群组自定义骚扰规则现在将自动禁用。 - 优化了在 iOS 设备打开 nmBot 面板时,输入文本后页面滚动高度异常的问题。 @@ -60,6 +67,7 @@ nmBot 迎来 2025 年的第一次更新!在这个辞旧迎新的时刻,我 - 优化了设置入群验证频道失败时,特定情况下的报错内容。 ### 问题修复 + - 对 nmBot 内容本地化做出了如下调整和修复: - 修复了繁体中文和英文中 nmBot+ 权益中“关键词回复”标题本地化文本缺失的问题。 - 修复了繁体中文中频道帖子编辑器中编辑成功提示本地化文本缺失的问题。 diff --git a/docs/nmbot-telegram/update-log/2025-02.md b/docs/nmbot-telegram/update-log/2025-02.md index 27b7d66..47ef37b 100644 --- a/docs/nmbot-telegram/update-log/2025-02.md +++ b/docs/nmbot-telegram/update-log/2025-02.md @@ -5,6 +5,7 @@ index: -2502 # nmBot 2025 年 2 月功能更新 ## 新鲜事儿 + ### 按消息类型警告和更多优化——2025 年 2 月功能更新 ![](../img/update-pictures/nmbot-2502.png) @@ -57,7 +58,7 @@ index: -2502 - 支持跳转到私信中使用 /config 菜单。 - 进行网页验证时,可以使用 hCaptcha 进行验证。 -- Telegram 星币交易记录中,即日起的 nmBot+ 购买将显示 nmBot+ 图标。 +- Telegram 星币交易记录中,即日起的 nmBot+ 购买将显示 nmBot+ 图标。 - 发起骚扰拦截争议时,不再要求用户提供三张证明活跃度的截图。 - 禁言和封禁等功能的返回消息中的按钮,现在仅发起警告的管理员可用以删除消息。 - 优化了 nmBot 面板“定时任务”设置页面中部分设置项的描述。 diff --git a/docs/nmbot-telegram/update-log/2025-03.md b/docs/nmbot-telegram/update-log/2025-03.md index 8d31bb6..ebc78f4 100644 --- a/docs/nmbot-telegram/update-log/2025-03.md +++ b/docs/nmbot-telegram/update-log/2025-03.md @@ -5,11 +5,11 @@ index: -2503 # nmBot 2025 年 3 月功能更新 ## 新鲜事儿 + ### 同频气氛组与消息模板新升级——2025 年 3 月功能更新 ![](../img/update-pictures/nmbot-2503.png) - 在这个版本中,我们为广受欢迎的两项 nmBot 特色功能带来新升级。 #### 同频气氛组 diff --git a/docs/nmbot-telegram/update-log/2025-04.md b/docs/nmbot-telegram/update-log/2025-04.md index 5bf985a..e9d725b 100644 --- a/docs/nmbot-telegram/update-log/2025-04.md +++ b/docs/nmbot-telegram/update-log/2025-04.md @@ -5,13 +5,14 @@ index: -2504 # nmBot 2025 年 4 月功能更新 ## 新鲜事儿 + ### nmBot 智能上线、群组配置导入与导出——2025 年 4 月功能更新 ![](../img/update-pictures/nmbot-2504.png) 为了庆祝生日月,除了首批 nmBot 智能功能 nmBot 智能聊天 (Beta 版) 外,我们还推出了群组配置导入和导出功能,并对商业关键词回复等功能进行了增强。 -#### nmBot 智能 +#### nmBot 智能 nmBot 智能聊天 (Beta 版) 正式推出!您可以点击加入[等候名单](https://t.me/nmnmfunbot?start=intelligencewaitlist)。 @@ -98,6 +99,7 @@ nmBot 版本号:25.4.1-wine+6181 ### 配置导入和导出 (Beta 版) ⚠️ 以下功能将逐步对所有群组开放。 + - 支持将群组的群组管理功能、群组娱乐功能、关键词回复、锁定模式等部分其他配置导出。 - 支持导入以前导出的 nmBot 配置。 - 支持导出 Rose 机器人的部分配置。 @@ -111,7 +113,8 @@ nmBot 版本号:25.4.1-wine+6181 - 群组操作记录中的部分配置更改中现在支持显示本地化的配置值。 - 调整了 nmBot 面板部分元素的配色。 - nmBot 面板现在将提示使用 Internet Explorer 的用户更换浏览器。 -- +- + ### 问题修复 - 修复了封禁操作的提示消息中可能错误显示链接预览的问题。 diff --git a/docs/nmbot-telegram/update-log/2025-05.md b/docs/nmbot-telegram/update-log/2025-05.md index cd1ee64..1a3fbe9 100644 --- a/docs/nmbot-telegram/update-log/2025-05.md +++ b/docs/nmbot-telegram/update-log/2025-05.md @@ -61,14 +61,14 @@ nmBot 版本号:25.5.0-wine+6643.2000 ### 无障碍优化 - 修复了 nmBot 面板以下元素的文本标签缺失的问题: - - 上下文菜单的关闭按钮。 + - 上下文菜单的关闭按钮。 - 修复了 nmBot 面板以下元素在屏幕阅读器中无法使用的问题: - - “积分”页面积分卡翻转后的操作按钮。 - - “关键词回复”列表的多选指示器。 + - “积分”页面积分卡翻转后的操作按钮。 + - “关键词回复”列表的多选指示器。 - 优化了 nmBot 面板以下元素在屏幕阅读器下的体验: - - 各设置列表中的设置项。 - - nmBot 智能聊天页面的欢迎信息。 - - nmBot 智能聊天页面的聊天消息来源提示。 + - 各设置列表中的设置项。 + - nmBot 智能聊天页面的欢迎信息。 + - nmBot 智能聊天页面的聊天消息来源提示。 - 修复了 nmBot 面板“滚动到最右侧”按钮在未显示时可通过键盘或屏幕阅读器读取的问题。 ### 其他改进和修复 diff --git a/docs/nmbot-telegram/update-log/2025-10.md b/docs/nmbot-telegram/update-log/2025-10.md index 2c0debc..8bff37e 100644 --- a/docs/nmbot-telegram/update-log/2025-10.md +++ b/docs/nmbot-telegram/update-log/2025-10.md @@ -31,12 +31,15 @@ nmBot 版本号:25.10.0-wine+6899.1000 ## 2025 年 10 月 31 日 19:55 ### 关键词回复识别情绪 (Beta 版) + - “关键词回复”现可识别基于身份的仇视言论、侮辱性言论、脏话和粗话、严重过激言论、暴力言论、过激和不雅言论,并执行预设操作。 ### 设备用户界面适配 + - 运行 iOS 26 和 iPadOS 26 的受支持设备在 Telegram 迷你 App 中打开 nmBot 面板时,新的用户界面设计与新系统风格更适配,覆盖顶栏和底栏、页面菜单和输入工具条。 ### 其他更新 + - 为运行部分旧版本浏览器的用户添加了浏览器过时提示。 - 优化了 nmBot 面板部分页面标题区域的样式。 - 调整了 nmBot 面板对话管理页面的按钮列表。 @@ -44,6 +47,7 @@ nmBot 版本号:25.10.0-wine+6899.1000 - 优化了 nmBot 面板频道配置页面部分选项的权限要求提示。 ### 问题修复 + - 修复了“锁定模式”页面列表中“系统模式”的“创建链接预览”功能未在图标列表中显示的问题。 - 修复了特定情况下触发高效手势后,手指移动时页面内容跳动的问题。 - 修复了特定情况下高效手势可在宽屏模式下触发的问题。 @@ -53,4 +57,4 @@ nmBot 版本号:25.10.0-wine+6899.1000 - 修复了特定情况下在 iOS 设备上在 Telegram 迷你 App 中打开 nmBot 面板时,键盘开启时输入工具条位置可能异常的问题。 - 修复了特定情况下退出“关键词回复”和“定时任务”编辑器后,编辑内容未正常清空的问题。 - 修复了使用英文时 nmBot 面板“商业”页面中商业机器人状态部分的文本不完整的问题。 -- 修复了 nmBot 面板侧边栏中“nmBot 智能”选项的字体较其他选项更粗的问题。 \ No newline at end of file +- 修复了 nmBot 面板侧边栏中“nmBot 智能”选项的字体较其他选项更粗的问题。 diff --git a/docs/nmbot-telegram/update-log/2025-12.md b/docs/nmbot-telegram/update-log/2025-12.md index 87b7ca1..6b47ab1 100644 --- a/docs/nmbot-telegram/update-log/2025-12.md +++ b/docs/nmbot-telegram/update-log/2025-12.md @@ -14,7 +14,7 @@ index: -2512 #### 骚扰拦截功能大幅提升 -得益于 nmBot 智能,骚扰拦截功能的识别效果可提升至 5 倍*,同时保证我们一贯的低误封率。 +得益于 nmBot 智能,骚扰拦截功能的识别效果可提升至 5 倍\*,同时保证我们一贯的低误封率。 群组管理员可自定义 nmBot 智能的灵敏度,还可根据消息的骚扰程度设定不同的操作。 diff --git a/docs/nmbot-telegram/update-log/index.md b/docs/nmbot-telegram/update-log/index.md index 85d7be5..4f0b178 100644 --- a/docs/nmbot-telegram/update-log/index.md +++ b/docs/nmbot-telegram/update-log/index.md @@ -1,3 +1,3 @@ # nmBot 更新日志 -您可以在此处查看 nmBot 的更新日志。 \ No newline at end of file +您可以在此处查看 nmBot 的更新日志。 diff --git a/docs/nmbot-telegram/update-log/nmbot-next.md b/docs/nmbot-telegram/update-log/nmbot-next.md index 6da6004..fa0726e 100644 --- a/docs/nmbot-telegram/update-log/nmbot-next.md +++ b/docs/nmbot-telegram/update-log/nmbot-next.md @@ -5,6 +5,7 @@ index: -2406 # nmBot NEXT (2024 年 6 月) 更新日志 ## 2024 年 6 月 21 日 00:22 + nmBot 版本号:24.6.14-wine+5077 - 竞猜活动页面中新增默认选项卡“未开始”,便于快速定位到未开始的比赛。 @@ -12,24 +13,28 @@ nmBot 版本号:24.6.14-wine+5077 - 修复了有多个活动进行时,nmBot 面板“积分”页面中积分活动排版错误的问题。 ## 2024 年 6 月 18 日 13:12 + nmBot 版本号:24.6.13-wine+5072 - 临时解决了系统模式锁定模式运行时未遵循细分权限设置的问题。 - 修复了 nmBot 面板中无法删除锁定模式设置的问题。 ## 2024 年 6 月 17 日 23:59 + nmBot 版本号:24.6.12-wine+5070 - 修复了关键词回复中,一条配置中的非法的正则表达式会干扰其他关键词回复匹配的问题。 - 修复了 nmBot 面板“关键词回复”页面中,关闭的关键词回复未正常在列表中显示的问题。 ## 2024 年 6 月 17 日 00:53 + nmBot 版本号:24.6.11-wine+5068 - 修复了特定情况下禁用独立匹配的关键词回复无法正常触发的问题。 - 修复了消息模板中链接预览“在文本上方显示”选项不起作用的问题。 ## 2024 年 6 月 16 日 23:09 + nmBot 版本号:24.6.10-wine+5066 - 在关键词回复设置中添加了新选项“独立匹配”。 @@ -38,6 +43,7 @@ nmBot 版本号:24.6.10-wine+5066 - 我们为 nmBot NEXT 数据迁移前创建的关键词回复默认禁用该选项,以解决特定设置情况下,关键词回复的逻辑与旧版本 nmBot 不匹配的问题。对新创建的关键词回复,该选项默认启用。 ## 2024 年 6 月 16 日 17:05 + nmBot 版本号:24.6.9-wine+5065 - 修复了设为拷贝消息的消息模板,发送后无法自动删除的问题。 @@ -46,11 +52,13 @@ nmBot 版本号:24.6.9-wine+5065 - 修复了设置操作记录频道时频道 ID 异常显示为 "0" 的问题。 ## 2024 年 6 月 16 日 11:49 + nmBot 版本号:24.6.8-wine+5061 - 修复了无法创建锁定模式的问题。 ## 2024 年 6 月 16 日 11:25 + nmBot 版本号:24.6.7-wine+5060 - 修复了没有封禁用户权限的管理员可使用入群验证功能的管理员操作按钮的问题。 @@ -60,9 +68,11 @@ nmBot 版本号:24.6.7-wine+5060 - 优化了对长消息检测骚扰的性能。 ## 2024 年 6 月 16 日 01:30 + - 我们对数据库进行了修复,解决了在 nmBot NEXT 数据迁移前使用过“同频气氛组”的群组中该功能无法正常使用的问题。 ## 2024 年 6 月 16 日 01:15 + nmBot 版本号:24.6.6-wine+5050 - nmBot 面板中新增了消息模板 ID 和关键词回复 ID 的显示。 @@ -72,6 +82,7 @@ nmBot 版本号:24.6.6-wine+5050 - 修复了一个特定情况下中断消息处理的问题。(注:此修复可能导致部分从旧版 nmBot 迁移的消息模板不再显示。) ## 2024 年 6 月 15 日 18:55 + nmBot 版本号:24.6.5-wine+5030 - 更新了竞猜功能的功能和提示内容。 @@ -89,27 +100,32 @@ nmBot 版本号:24.6.5-wine+5030 - 精简了调试模式下日志的内容。 ## 2024 年 6 月 14 日 23:22 + nmBot 版本号:24.6.4-wine+5011 - 更新了后续功能的所需资源。 ## 2024 年 6 月 14 日 21:43 + nmBot 版本号:24.6.3-wine+5006 - 修复了骚扰拦截功能会识别匿名群组管理员消息的问题。 ## 2024 年 6 月 14 日 20:50 + nmBot 版本号:24.6.2-wine+5005 - 修复了在私信中进行入群验证流程失效的问题。目前我们已对受到此问题影响无法完成入群验证的用户在对应群组中解除封禁,并发送了提示消息。 ## 2024 年 6 月 14 日 20:12 + nmBot 版本号:24.6.1-wine+5004 - 修复了发送 /start 指令后机器人发送异常支付消息的问题。特别说明:该问题系测试 Telegram 星币功能时的测试功能未删除导致。对您造成的不便敬请谅解。 - 修复了骚扰拦截功能会识别频道转发的消息的问题。 ## 2024 年 6 月 14 日 + nmTeam 今日正式推出 nmBot NEXT。 nmBot NEXT 包含数十项新功能,包括: diff --git a/docs/nmbot-telegram/update-log/nmbot-plus.md b/docs/nmbot-telegram/update-log/nmbot-plus.md index 8f97dd3..4aebfe8 100644 --- a/docs/nmbot-telegram/update-log/nmbot-plus.md +++ b/docs/nmbot-telegram/update-log/nmbot-plus.md @@ -30,28 +30,33 @@ nmBot+ 在 nmBot 面板开放购买,您需要在 Telegram 迷你 App 中打开 \* 所有功能以实际使用为准,功能可能会根据实际情况调改。 ## 2024 年 11 月 21 日 11:15 + nmBot 版本号:24.11.5-wine+5911 - 修复了特定情况下“同频气氛组”和“自动回复频道帖子”功能对媒体组消息重复回复多次的问题。 ## 2024 年 11 月 20 日 22:36 + nmBot 版本号:24.11.4-wine+5910 - 修复了入群验证骚扰用户封禁时长设为“使用默认时长”时,设置无法正常生效的问题。 - 修复了选择消息模板时,“其他模板”部分的隐藏其他模板按钮图标未正常显示的问题。 ## 2024 年 11 月 19 日 17:00 + nmBot 版本号:24.11.3-wine+5908 - 为适用于个人账号的 nmBot+ 计划添加了额外说明,以免要为群组购买 nmBot+ 的用户误为个人账号购买 nmBot+。 - 修复了试用 nmBot+ 订阅会出现在可购买列表中的问题。 ## 2024 年 11 月 19 日 14:01 + nmBot 版本号:24.11.2-wine+5906 - 修复了保存商业关键词回复的问题。 ## 2024 年 11 月 17 日 21:32 + nmBot 版本号:24.11.1-wine+5903 - 增加了 /paysupport 指令,以便用户了解如何就支付问题获得支持。 @@ -59,50 +64,62 @@ nmBot 版本号:24.11.1-wine+5903 - 修复了支付处理出现未知错误时提示文本的问题。 ## 2024 年 11 月 17 日 12:00 + ### nmBot+ + - 推出 nmBot+,这款付费计划允许您解锁更多高级 nmBot 功能。 -### nmBot 配置权限管理 +### nmBot 配置权限管理 + - 群组的所有者可以为不同用户设定对 nmBot 设置的管理权限。 - 对于各项功能,权限可设为“默认”“可更改群组信息的管理员”“可添加管理员的管理员”或“仅群主”。 - 群主还可为特定群组管理员针对某项功能添加例外。 ### 自定义骚扰规则 + - 群组管理员可在 nmBot 骚扰消息拦截和入群验证自动拒绝骚扰用户入群功能中补充自己的匹配规则。 -### 群组指令列表设置 +### 群组指令列表设置 + - 群组管理员现在可以自定义在 Telegram App 中聊天时,消息输入框中出现的指令列表。 ### 呼叫群组管理员 + - 支持自定义呼叫关键词:群组管理员可通过正则表达式自定义触发“呼叫群组管理员”功能的消息内容。 - 支持在私信中接收通知:群组成员呼叫 nmBot 时,在私信中向所有群组管理员(适用时包括匿名管理员)发送提示消息。 - 启用在私信中接收通知时,可关闭群组中呼叫消息中对群组管理员的提及。 ### 入群验证 + - 支持自定义入群验证消息:群组管理员可在入群验证消息中添加自定义文本。 - nmBot 面板中支持查看入群验证历史记录。 - - 订阅了 nmBot+ 的群组可查看 15 天内的历史记录,其他群组可查看 15 分钟内的历史记录。 + - 订阅了 nmBot+ 的群组可查看 15 天内的历史记录,其他群组可查看 15 分钟内的历史记录。 - 入群验证可自定义更多验证超时时间和封禁用户时间。 - 入群验证可为被 nmBot 识别或举报为骚扰的用户和被管理员拒绝的用户设置与默认不同的封禁用户时间。 - 入群验证支持永久封禁多次加入群组失败的用户。 - 优化了 nmBot 面板“入群验证”页面设置项的排列。 ### 新成员权限限制 + - 支持限制新加入群组的成员在一段时间内可发送消息的类型。 ### 定时任务 + - “锁定模式”现已更名为“定时任务”。 - 支持在群组中发送定时消息。 ### 关键词回复 + - 支持使用 nmBot AI 自动生成关键词回复。 - 支持通过关键词回复警告用户。 - 支持设置关键词回复随机发送消息的数量。 ### 入群欢迎 + - 支持自定义入群欢迎消息的自动删除时间。 ### 解锁限制 + - nmBot+ 为群组解锁一系列设置权限: - 同频气氛组功能的每日最大可回复消息数增加。 - 关键词回复、定时任务的最大可设置数量增加。 @@ -110,11 +127,13 @@ nmBot 版本号:24.11.1-wine+5903 - 关键词回复、锁定模式、自动回复频道消息功能的最大可选择消息模板数量增加。 ### 其他改进 + - 调整了“限制群成员使用指令”功能的开放逻辑:本次功能更新后使用 nmBot 的群组将无法免费使用“限制群成员使用指令”功能,订阅 nmBot+ 即可使用;本次功能更新前已经使用过 nmBot 的群组,群组成员数量达到 2000 人时即可使用该功能。 - 群组管理员修改部分无权限修改的设置时,新增了用户无权限修改的提示。 - 调整了 nmBot 面板设置项“新功能”标志的样式。 ### 问题修复 + - 修复了“已启用指令限制”提示无法正常显示的问题。 - 修复了 nmBot 面板部分对话框中输入框的边框颜色不正确的问题。 - 修复了在页面上的特定位置打开上下文菜单时位置计算异常的问题。 diff --git a/docs/nmbot-telegram/update-log/previous-log.md b/docs/nmbot-telegram/update-log/previous-log.md index 0ea3f15..615b611 100644 --- a/docs/nmbot-telegram/update-log/previous-log.md +++ b/docs/nmbot-telegram/update-log/previous-log.md @@ -3,37 +3,45 @@ title: 归档的 nmBot 更新日志 description: 此处记录 2023 年 1 月 10 日前的 nmBot 更新日志。2023 年 1 月 10 日后的 nmBot 更新日志将按版本归档。 index: 0 --- + # 归档的 nmBot 更新日志 > 此处记录 2023 年 1 月 10 日前的 nmBot 更新日志。2023 年 1 月 10 日后的 nmBot 更新日志将按版本归档。 ## 2023 年 1 月 9 日 13:50 + - 修复了特定情况下进入 nmBot 面板时基于 URL 的页面导航失效的问题。 - 修复了特定情况下网页验证的验证码模块加载失败的问题。 ## 2022 年 12 月 25 日 23:00 + - 修复了特定情况下 nmartChat 的部分功能与部分指令或链接冲突的问题。 - 修复了调试群组日志的部分输出内容解析错误的问题。 - 处理骚扰举报时支持查看举报文件。 ## 2022 年 12 月 21 日 18:05 + - 在 nmBot 面板搜索群组时支持搜索群组 ID。 - 在 nmBot 面板搜索群组时,若搜索不到结果,将显示相应提示。 - 修复了 nmBot 面板“消息限制与智能识别”设置集内部分元素触发错误的点击事件的问题。 - 修复了 nmBot 面板关键词回复编辑页面中部分 Telegram 链接无法在 nmBot 面板内置浏览器中打开的问题。 ## 2022 年 12 月 19 日 02:50 + - nmBot 面板新增“往期活动”页面和相关提示文本。nmBot 面板已结束的往期活动将归档到“往期活动”页面。 - 移除了“世界杯竞猜王”活动入口在群组列表页面、积分页面的显示,并归档到“往期活动”页面。 ## 2022 年 12 月 19 日 02:07 + - 移除了 nmBot 签到返回消息中与限时活动“世界杯竞猜王”相关的提示。 - 修复了特定情况下 nmBot 入群验证失败消息后错误出现 undefined 字样的问题。 ## 2022 年 12 月 16 日 23:23 + - 优化了活动“世界杯竞猜王”内的部分显示效果。 ## 2022 年 12 月 16 日 18:31 + - 使用 /spam 指令举报用户时支持在空格后输入举报原因。 - /id 功能支持返回群组话题功能的相关参数。 - 管理员举报用户骚扰但群组未启用对应拦截功能时,在返回消息中添加了相关提示。 @@ -41,7 +49,7 @@ index: 0 - 支持通过行内消息发送指令。 - 优化了 nmartChat 对部分文本的识别能力。 - 优化了调试群组的记录信息。 -- 调整了 nmartChat 的部分回复文本。 +- 调整了 nmartChat 的部分回复文本。 - 修复了部分触发 nmartChat 的情形未被正常计入操作间隔的问题。 - 修复了管理员举报用户消息的文件未正常识别的问题。 - 修复了特定情况下,/ts 功能转换的贴纸高度大于 512 的问题。 @@ -53,9 +61,11 @@ index: 0 - nmBot 管理端支持向特定用户推送消息。 ## 2022 年 12 月 13 日 10:55 + - 修复了 nmBot 生成会员卡的底图大小错误的问题,并为受影响用户清空了既有会员卡。受影响用户重新获取会员卡即可。 ## 2022 年 12 月 12 日 16:50 + - 入群验证新增自动封禁骚扰用户功能:nmBot 将验证入群用户的名称和 ID,若识别为骚扰用户,将自动封禁该用户。群组管理员也可以选择解除封禁并加入白名单;加入白名单后,在该群组将不再封禁该用户入群。 - 新增删除消息,封禁用户并举报骚扰功能:群组管理员可回复特定消息或提供用户 ID 发送 /spam 指令,这将封禁该用户,尽可能删除该用户的消息,并将该用户的基本信息和消息内容发送至 nmBot。nmBot 团队确认举报是否属实后,将向群组管理员发送确认信息。 - 新增“智能识别并删除骚扰消息”功能:群组管理员可以自定义启用部分过滤方案;通过群组的实际需求在群组中删除被启用的过滤方案识别的消息,并可设置封禁发送消息的用户。若用户被封禁且启用了封禁提示,群组管理员也可设置将用户加入群组白名单;加入群组白名单的用户将不再被 nmBot 封禁。该功能目前支持识别 @ADGoawayBot 标记有既往史的用户和 nmBot 反骚扰系统认定为骚扰的消息;后续将支持更多类型。 @@ -90,26 +100,32 @@ index: 0 - 删除了 nmBot 的 /about, /site 指令。 ## 2022 年 12 月 6 日 23:15 + - 修复了特定情况下 nmBot 未正常发送入群欢迎消息的问题。 ## 2022 年 11 月 29 日 23:03 + - 限时活动“世界杯竞猜王”现在 nmBot 面板入口页面展示。 - 在 nmBot 签到返回消息中增加了与限时活动“世界杯竞猜王”相关的提示。 - 调整了活动“世界杯竞猜王”中“活动说明”按钮的大小。 ## 2022 年 11 月 29 日 22:00 + - 限时活动“世界杯竞猜王”上线:世界杯期间,在 nmBot 面板参与竞猜赢取积分。活动详情请见公告和 nmBot 面板活动页面。 - 修复了 nmBot 面板中部分积分历史记录文本显示错误的问题。 ## 2022 年 11 月 28 日 20:45 + - 为避免对群组正常聊天造成影响,群组中的 nmBot 签到消息将在 60 秒后自动删除。 - 修复 nmBot 面板的积分相关功能和网页入群验证功能在面板通过 Telegram Web App 打开时无法正常加载的问题。 - 修复 nmBot 面板“关键词回复”页面关键词回复概览中部分文本未正常显示的问题。 ## 2022 年 11 月 26 日 19:23 + - 调整了 /mc, /sign 指令返回文本的排版,以减少对群组消息连贯性的影响。 ## 2022 年 11 月 26 日 19:00 + - nmBot 新增指令 /mc, /mycredit 查询自己的积分。 - nmBot 新增指令 /hey, /sign, /checkin 每日签到获取积分。 - nmBot 面板新增“积分”板块,支持每日签到、查询最近 500 条积分记录、查看积分活动。 @@ -117,9 +133,11 @@ index: 0 - 修复了 nmBot 面板内置浏览器的部分操作文本未本地化的问题。 ## 2022 年 11 月 24 日 17:55 + - 更新了后续功能的所需资源。 ## 2022 年 11 月 19 日 23:00 + - 更新了群组邀请 nmBot 时,nmBot 发送的帮助内容。 - “群友互动”功能新增参数:$date (插入当前日期), $time (插入当前时间), $bot (插入 nmBot 信息), $space (插入空格), $hidefrom ($hf, 隐藏默认位置的互动来源), $hideto ($hidetarget, $ht, 隐藏默认位置的互动目标), $hideexclamation ($he, 隐藏感叹号), $hidele ($hl, 隐藏“了”)。 - 使用“群友互动”功能时,若“了”字前为英文字符或数字,则在“了”字前自动添加一个空格。 @@ -133,15 +151,19 @@ index: 0 - 删除了 nmBot 的部分未使用代码。 ## 2022 年 11 月 19 日 00:56 + - 修正未启用 nmBot 管理功能时“群友互动”功能无法使用的问题。 ## 2022 年 11 月 19 日 00:53 + - 修正“群友互动”功能中含有频道身份时,nmBot 发送的互动消息可能错误显示频道链接预览的问题。 ## 2022 年 11 月 19 日 00:43 + - 修正 nmBot /config 面板中部分按钮排版错误的问题。 ## 2022 年 11 月 19 日 00:03 + - 新增群组娱乐功能“群友互动”:在群组中使用以 / 或 、 开头的指令,以 nmBot 为桥梁与群友进行友好的互动。我们借鉴了其他 Telegram 机器人的相关功能,但 nmBot 支持频道身份参与互动,并支持在频道评论区和启用话题功能的群组使用。“群友互动”功能将默认关闭,并可在 nmBot /config 菜单和 nmBot 面板开启。 - nmBot 在广播通知消息时支持筛选群组。 - 为优化性能,删除了旧版入群验证的相关代码。 @@ -152,9 +174,11 @@ index: 0 - 修复了部分 nmBot 管理员功能无法使用的问题。 ## 2022 年 11 月 17 日 01:53 + - 优化了 nmBot 遇到部分错误时崩溃的问题。 ## 2022 年 11 月 16 日 22:22 + - nmBot 面板新增多语言支持。nmBot 面板将自动匹配浏览器或 Telegram 语言;用户可以在“我的”页面切换语言。 - nmBot 面板新增语言:繁体中文、英语。 - 根据多语言适配需要,调整了 nmBot 面板中部分元素的位置和大小。 @@ -163,18 +187,22 @@ index: 0 - 修正了 nmBot 面板“管理群组”选项组中,“锁定模式”仍被标注为 Beta 版的问题。 ## 2022 年 11 月 14 日 20:10 + - 修复了“入群验证”在特定群组中,因群组名称含特殊字符而无法发送入群验证消息时,发送备份验证仍然失败的问题。 - 修复了 11 月 6 日起“欢乐复读”功能在特定情况下表现错误的问题。 - 关闭了部分 nmBot 面向用户的报错。 ## 2022 年 11 月 10 日 21:33 + - 入群验证失败时,新增“重试”按钮。 - 修复了 reCaptcha JavaScript 加载失败时,入群验证流程卡死的问题。 ## 2022 年 11 月 10 日 21:16 + - 修复了没有在 nmBot 面板注册过管理群组的用户打开 nmBot 面板时,页面内 URL 导航不生效的问题。 ## 2022 年 11 月 10 日 21:00 + - 新增入群验证方式“网页验证 (Beta)”:在 nmBot 面板通过人机身份验证来完成入群验证。 - nmBot 面板支持通过行内按钮在浏览器打开并自动完成登录;在 /panel 指令中也加入了相应按钮和提示。 - nmBot 面板新增对渐进式网络应用程序 (Progressive Web Apps) 的支持:可以在浏览器中将 nmBot 面板添加为应用。 @@ -185,24 +213,31 @@ index: 0 - 修复了双屏模式下 nmBot 面板“管理”和“我的”页面在切换时动画效果不正确的问题。 ## 2022 年 11 月 8 日 15:24 + - 优化了 nmBot 入群验证针对新成员特殊名称导致错误的解决方案。 ## 2022 年 11 月 7 日 20:23 + - 修复了 nmBot 入群验证为被邀请用户提供验证时长错误的问题。 ## 2022 年 11 月 7 日 20:11 + - 修复了 nmBot 入群验证未正确判断用户是否被邀请的问题。 ## 2022 年 11 月 7 日 19:53 + - 修复了使用 /dt 指令设置默认话题后,设置未立即生效的问题。 ## 2022 年 11 月 7 日 19:41 + - 修复了 nmBot 在关闭入群验证的群组提示“群组的入群验证配置存在问题”,并可能错误禁言成员的问题。 ## 2022 年 11 月 7 日 19:31 + - 修复了在 nmBot /config 菜单中无法将入群验证设置为“关闭”的问题。 ## 2022 年 11 月 7 日 19:23 + - 对 nmBot 入群验证功能进行全新升级。 - 重构的入群验证结构与逻辑提升了功能流畅度和稳定性。 - 支持自定义入群验证允许的时间范围(被邀请入群的用户除外)。 @@ -215,24 +250,30 @@ index: 0 - 优化 nmBot 面板“入群验证”设置中关于设置验证频道的帮助文本。 ## 2022 年 11 月 7 日 15:02 + - 同步 nmBot Bot API 依赖版本到最新版。 - 支持在 /id 中显示 entities 信息。 - 修复了在启用话题功能的群组中,若多个话题内同时出现新消息,“欢乐复读”功能表现异常的问题:“欢乐复读”功能将识别不同话题并单独判断是否复读。 ## 2022 年 11 月 7 日 0:29 + - 修复了在启用话题功能的群组中“欢乐复读”功能无法正确复读转发消息的问题。 ## 2022 年 11 月 7 日 0:21 + - 针对启用话题功能的群组支持使用 /dt 指令设置默认话题。更新后,启用话题功能的群组中第一条新消息的话题将被设置为默认话题。 - 修复了 nmBot 入群验证和入群欢迎功能在启用话题功能的群组中无法正常使用的问题。群组设置默认话题后即可正常使用入群验证和入群欢迎。 ## 2022 年 11 月 6 日 23:39 + - 由于入群欢迎在启用了话题功能的群组无法正常工作,我们暂时关闭了上述群组的入群欢迎功能。(入群欢迎功能修复后,我们将恢复原先的入群欢迎设置。) ## 2022 年 11 月 6 日 23:06 + - 由于入群验证在启用了话题功能的群组无法正常工作,我们暂时关闭了上述群组的入群验证功能。(nmBot /config 菜单中显示的可能并非实际入群验证状态。待入群验证功能修复后,我们将恢复原先的入群验证设置。) ## 2022 年 11 月 1 日 22:52 + - 支持在群组中使用 /cw 指令设置频道白名单。白名单频道身份发送的消息将不会被 nmBot 自动删除。 - 在 nmBot /config 菜单打开“自动删除消息”中“频道身份消息”设置项时,新增了关于使用 /cw 指令设置频道白名单的提示。 - 为避免误触,调整了使用 /wc 指令清空入群欢迎的操作方式为输入 /wc -,并在指令返回中添加了相应提示。 @@ -241,10 +282,12 @@ index: 0 - 修复了部分情况下 /id 功能无法正常返回消息的问题。 ## 2022 年 10 月 30 日 19:42 + - “频道透视眼”现作为一项正式功能推出。 - 修复了匿名群组身份可以使用“频道透视眼”并可能导致报错的问题。 ## 2022 年 10 月 29 日 15:30 + - “锁定模式 (Beta)”新增“nmBot 模式”:通过 nmBot 自动删除禁止发送的消息。nmBot 面板已为此模式优化了显示,新增和修改了设置项和说明文本。 - 设置了 nmBot 模式的锁定模式设置时,使用 /config 菜单调整“自动删除消息”设置时,将显示提示文本。 - 在 nmBot 面板“自动删除消息”设置页中新增关于使用锁定模式的警告。 @@ -252,57 +295,71 @@ index: 0 - 修复了 nmBot 面板“自动删除消息”设置页中,“通过 bot 发送的消息”项开关状态显示异常的问题。 ## 2022 年 10 月 29 日 10:55 + - 在“锁定模式”设置中,增加有关 Telegram API 限制的说明。 ## 2022 年 10 月 27 日 22:50 + - 为“锁定模式”列表底端增加了功能说明卡片。 ## 2022 年 10 月 27 日 21:52 + - 修复了 nmBot 面板的警告消息图标显示可能过低的问题。 - 修复了 nmBot 面板“锁定模式”编辑页面中,“发送消息”文本框可以异常调整宽度和高度的问题。 ## 2022 年 10 月 27 日 17:35 + - 为频道透视眼、会员身份鉴定、关键词回复设置、管理功能的部分返回消息添加了删除按钮。 - 在部分提示文本中新增了使用“频道透视眼”功能的提示。 ## 2022 年 10 月 27 日 15:35 + - 修复了 nmBot 面板的警告消息在 webKit 内核上显示过高的问题。(当前警告图标显示仍有问题,我们将尝试在后续更新中修复。) ## 2022 年 10 月 27 日 10:15 + - 修复在 nmBot 面板为群组首次添加关键词回复时,若消息中包含特定文本,有概率出现添加失败的问题。 ## 2022 年 10 月 27 日 02:15 + - 新增“锁定模式 (Beta)”功能定时自动控制群组权限。 - 优化了“频道透视眼 (Beta)”功能的部分提示文本。 - 优化了关键词回复触发“忽略群组管理员”功能时的提示文本。 ## 2022 年 10 月 25 日 17:40 + - 新增“频道透视眼 (Beta)”功能。 - 在部分提示文本中新增了使用“频道透视眼”功能的提示。 ## 2022 年 10 月 24 日 23:00 + - 为优化性能,调整了 nmBot 获取群组关键词回复的频率。 - * 通过 nmBot 面板修改关键词回复时,所作更改将在短时间内应用。 + - 通过 nmBot 面板修改关键词回复时,所作更改将在短时间内应用。 - 通过 nmBot 面板修改群组配置和关键词回复时,将显示所作更改将在短时间内应用的提示。 ## 2022 年 10 月 15 日 14:20 + - 修复了极少数情况下 nmBot 关键词回复功能识别目标群组错误的问题。 ## 2022 年 10 月 15 日 13:14 + - 修复在极少数群组中找不到群组配置时,无法自动创建的问题。 ## 2022 年 10 月 14 日 20:40 + - /admin 呼叫消息新增关闭呼叫群组管理员功能按钮。 - 为优化性能,调整了 nmBot 获取群组配置的频率。 - * 通过 nmBot 面板修改群组配置时,所作更改将在短时间内应用。 + - 通过 nmBot 面板修改群组配置时,所作更改将在短时间内应用。 - 修复极少数情况下 /ban 指令导致报错的问题。 - 删除了 nmBot 面板中过时的帮助文档。 ## 2022 年 10 月 13 日 01:14 + - 优化了 nmBot /pm 指令对频道的称呼。 - 修正了部分情况下,nmBot /pm 指令返回消息中,空格位置和数量可能错误的问题。 ## 2022 年 10 月 13 日 00:36 + - 由于 Telegram 功能变更,调整了 nmBot /pm 功能的部分表现。 - nmBot 将除群组关联频道外的其他频道身份认定为 Telegram 会员。 - nmBot 无法确认群组关联频道的频道主是否为 Telegram 会员,并将此结果及相关帮助信息体现在返回消息中。 @@ -310,14 +367,17 @@ index: 0 - 调整了 /pm 指令对匿名管理员的返回消息内容。 ## 2022 年 10 月 12 日 23:15 + - 优化了 nmartChat API 的返回内容过滤机制。 - 修复了无法在 nmBot /config 菜单面板设置“解除频道置顶”功能的问题。 - 修复了 nmBot 部分行内按钮回调事件的弹窗未正常显示的问题。 ## 2022 年 10 月 12 日 22:51 + - 修复了 nmBot 验证频道设置无法完成的问题。 ## 2022 年 10 月 8 日 22:55 + - 在 nmBot /help 返回消息中,添加了查看 Web 版帮助文档(Beta)的提示。 - 在 nmBot 面板中,添加了查看 Web 版帮助文档(Beta)的人口。 - 查看“没有找到您管理的群组?”帮助时,内容已更改为 Web 版帮助文档(Beta)中的帮助。 @@ -334,12 +394,15 @@ index: 0 - 优化了 nmBot 管理员处理部分 nmBot 错误的流程。 ## 2022 年 9 月 22 日 11:35 + - nmBot 发送的入群欢迎消息将在一段时间后自动删除。 ## 2022 年 9 月 21 日 11:14 + - 修复了未被记录存在管理群组的用户登录 nmBot 面板时页面初始化失败的问题。 ## 2022 年 9 月 19 日 17:30 + - 支持在 /ban 和 /block 时添加参数 d 来删除触发消息。 - 更新了 /ban 和 /block 相关返回消息部分行内按钮的文本。 - 删除了 /ban 和 /block 相关返回消息中部分错误提供的行内按钮。 @@ -349,24 +412,30 @@ index: 0 - 修复了特定情况下 nmBot 对被移出群的判定错误。 ## 2022 年 9 月 18 日 15:00 + - 修复了授予 nmBot 管理员权限,并启用 nmBot 群组管理功能后取消 nmBot 管理员权限后,nmBot 不响应群组消息的问题。 ## 2022 年 9 月 16 日 22:23 + - 优化了 nmBot 入群验证按钮超时的提示文本。 - 修复了“欢乐复读”功能导致群组 nmartChat 贴纸等部分功能不可用的问题。 ## 2022 年 9 月 3 日 16:08 + - 修复了在 nmBot 面板清空入群欢迎后,nmBot 错误发送 [object Object] 的问题。 ## 2022 年 9 月 3 日 15:53 + - 修复了 /wc 指令无法使用的问题。 ## 2022 年 9 月 2 日 15:10 + - 群组封禁 nmBot 时,nmBot 将尝试向封禁者发送调查消息。 - 修复了可以通过 nmBot 获取消息 JSON 参数保存受保护消息内容的问题。通过 /id 获取开启内容保护的群组的消息或消息的 JSON 参数时,回复消息将开启内容保护。 - 删除了 /id 返回消息中的 nmBot 内部参数。 ## 2022 年 8 月 30 日 12:00 + - 除数据库错误外的其它错误,60 秒内仅向同一对话报告一次。 - 更新了 nmBot 错误报告的文本提示,并添加了 nmTeam 支持选项。 - 新增了 banme 和 nmartChat userbot 回应功能的部分提示文本。 @@ -374,14 +443,17 @@ index: 0 - 修复了使用 PagerMaid-Pyro 的用户发送 ",re" 指令复读 nmBot 消息时触发 nmartChat 的问题。 ## 2022 年 8 月 28 日 17:35 + - 入群验证失败或超时的提示遵循入群成员名称屏蔽设置。 - 优化了 nmBot 在特定情况下的稳定性。 ## 2022 年 8 月 26 日 11:23 + - 修复了特定情况下“同频气氛组”功能失效的问题。 - 修正了部分内置冷却时间设置错误的问题。 ## 2022 年 8 月 24 日 18:30 + - 新增自动删除群组内贴纸、图片、视频、视频消息、语音、GIF、来自机器人的消息功能。 - 在 nmBot /config 菜单和 nmBot 面板菜单新增“自动删除消息”子设置集。本次更新中新增的自动删除消息功能和“删除频道消息”“删除入群提示”“删除会员贴纸”功能将被归入该设置集。此外,在 nmBot /config 菜单更改该设置集的设置后,不会自动返回初始页面。 - 在 nmBot 面板关键词回复设置页面,增加了在回复内容中使用 {username} 的相关提示。 @@ -396,6 +468,7 @@ index: 0 - 进行了一些 nmBot 内部优化。 ## 2022 年 8 月 22 日 21:23 + - 设置关键词回复时,支持匹配发送消息的用户或频道。 - 修复了“呼叫群组管理员”功能会提及群组内匿名管理员的问题。 - 新增“呼叫群组管理员”功能没有找到群组管理员时的提示文本。 @@ -403,25 +476,31 @@ index: 0 - 更新了 nmBot 日志的相关功能。 ## 2022 年 8 月 19 日 19:11 + - 修复了 nmBot 面板中“🔒 授予管理员权限来管理更多功能”“允许呼叫管理员”两个按钮的间距异常缺失的问题。 ## 2022 年 8 月 16 日 12:08 + - 修复了特定情况下入群验证消息发送出错时,未能捕捉错误并执行备用操作的问题。 - 修复了 nmBot 部分操作的时间限制可能没有区分不同的频道身份的问题。 - 修复了 nmBot 名片审核管理员无法通过用户设定的名片的问题。我们将在 1 小时内为遇到问题的用户完成审核。 ## 2022 年 8 月 16 日 00:55 + - 优化了 nmBot 入群验证消息发送成功的判定逻辑。 ## 2022 年 8 月 15 日 22:21 + - nmBot 帮助文档新增“支持”帮助项。 - 优化了 nmBot 在入群验证消息发送失败时的备份措施。 - 修复了启用“频道验证”和“删除入群消息”时,入群验证中的名称未被部分屏蔽的问题。 ## 2022 年 8 月 15 日 14:42 + - 针对 nmBot 有极低概率对消息执行一些错误操作的问题进行了内部优化,对造成影响较大的问题进行了针对性修复。由于此问题涉及 nmBot 底层逻辑,我们将在线上持续验证此次修复的有效性,并将在充分测试后,于后续更新中继续进行相应修复,我们对此问题的出现深表歉意。 ## 2022 年 8 月 15 日 00:08 + - 在未开启 nmBot 管理功能的群组的 /config 菜单中新增一个引导。 - 在 /id 的返回消息中新增“关闭”按钮,消息发送者和群组管理员可以点击该按钮来关闭。 - 在群组内发送 /id 时,若提供了参数 p 或 pm,nmBot 会将相关信息发送到私信中。(要使用该功能,请首先与 nmBot 主动私信。) @@ -433,15 +512,19 @@ index: 0 - 修复了 nmBot 管理员广播消息的内部功能无法使用的问题。 ## 2022 年 8 月 14 日 20:55 + - 修复了使用 nmBot 指令封禁频道后,无法通过行内按钮解封的问题。 ## 2022 年 8 月 14 日 20:28 + - 修复了 nmBot 会回复部分频道身份发送的非指令消息的问题。 ## 2022 年 8 月 14 日 14:23 + - 修复了即使授予了 nmBot 置顶消息权限,/config 菜单中“取消频道置顶”设置仍然显示灰色且无法点击的问题。 ## 2022 年 8 月 14 日 14:17 + - 即使群组未授予 nmBot 管理员权限,也可以在 /config 菜单和 nmBot 面板中更改 nmBot 群组娱乐功能和包括“呼叫群组管理员”在内的部分 nmBot 管理功能。 - 优化了 /config 菜单和 nmBot 面板中因权限不足无法设置 banme 功能和自动取消置顶功能时的提示。 - nmBot /config 菜单新增“关闭”按钮。 @@ -451,8 +534,9 @@ index: 0 - 修复了以频道身份触发部分 nmBot 群组管理操作时,nmBot 不响应的问题。 - 修正了 nmBot 部分权限提示的内容错误。 - 批量优化了部分 nmBot 内部功能实现。 - + ## 2022 年 8 月 13 日 18:14 + - 全新设计并实现了 /ban, /unban, /block, /unblock 指令。 - 支持使用 username 来识别频道。nmBot 会自动识别 username,您不需要添加 @ 符号。由于 Telegram API 限制,暂不能通过这种方式指示用户。 - 支持回复 nmBot 的操作反馈来进行进一步操作。 @@ -464,9 +548,11 @@ index: 0 - 更新了 nmBot 帮助文档。 ## 2022 年 8 月 12 日 22:45 + - nmBot 面板文档将自动根据内容匹配页面标题。 ## 2022 年 8 月 12 日 17:23 + - 调整了“欢乐复读”的功能类别。调整后,“欢乐复读”将位于 nmBot 群组娱乐功能设置中,且不再与 nmBot 管理功能绑定。 - 在 /config 菜单和 nmBot 面板中,增加了对部分 nmBot 群组娱乐功能所需 nmBot 管理员权限的提示。 - 为 nmBot /about 指令内容添加了文本描述。 @@ -476,12 +562,15 @@ index: 0 - 调整了 nmBot 日志的分片配置。 ## 2022 年 8 月 9 日 21:21 + - 修正了 banme 娱乐功能的一处文本错误。 ## 2022 年 8 月 9 日 21:15 + - 修复了冷却时间限制识别对象错误的问题。 ## 2022 年 8 月 9 日 21:05 + - nmBot 新增“群组娱乐功能”设置集。在 nmBot /config 菜单和 nmBot 面板可查看和设置群组娱乐功能。 - 新增 nmBot 群组娱乐功能 banme。 - nmBot 关键词回复支持使用正则表达式判断消息的转发来源。 @@ -494,14 +583,17 @@ index: 0 - 修复了如果在创建群组时邀请 nmBot,则会出现数据库未初始化,并导致部分操作无法使用的问题。 ## 2022 年 8 月 7 日 00:10 + - 限制 nmBot 内建数据库告警在短时间内重复向用户和群组发送告警信息。 - 优化了 nmBot 记录数据库告警的机制。 ## 2022 年 8 月 3 日 20:14 + - 修复了 nmBot 频道验证允许他人点击验证按钮验证的漏洞。 - 优化了 nmBot 处理发送欢迎消息时问题的能力。 ## 2022 年 8 月 3 日 15:05 + - 支持设定拒绝新成员加入群组。 - 为部分 nmBot 操作新增关闭按钮。 - 调整了部分 nmBot 操作的冷却时间。 @@ -509,6 +601,7 @@ index: 0 - 优化了 nmBot 名片的相关文本提示。 ## 2022 年 8 月 1 日 21:45 + - nmBot 管理功能新增“删除入群系统消息”功能,可在 /config 菜单和 nmBot 面板中设置。设置后,若同时启用了入群验证,相应消息中的用户昵称将被部分屏蔽。 - nmBot /config 菜单和 nmBot 面板新增“允许呼叫管理员”设置项。关闭后,将不能通过 /admin, @admin 等方式呼叫群组管理员。该选项对启用了 nmBot 管理功能的群组开放设置,且本次更新后默认将打开。 - 为避免 nmBot 相关功能影响群组内正常聊天、对群组管理员和其它成员造成干扰,我们为部分功能增加了冷却时间限制。用户可以在与 nmBot 的私信中发送 /getlimit 来了解最新的限制列表。我们会不断调整相关冷却时间,为大家提供更好的使用体验。 @@ -518,16 +611,19 @@ index: 0 - 修复了关闭 nmBot 管理功能后,部分 nmBot 管理功能仍延续 nmBot 管理功能开启时的设置的问题。 ## 2022 年 7 月 30 日 17:17 + - 修复了 nmBot 面板管理页面群组列表在群组名称较长时,该群组名称显示居中的问题。 - 修复了某些关键词回复可能错误在 nmBot 面板中显示未定义的自动删除时间的问题。 - 优化了 nmBot 面板 Notice 页面的 HTML 标题。 ## 2022 年 7 月 30 日 12:01 + - 修复了“欢乐复读”功能可能未正确识别部分消息的问题。 ## 2022 年 7 月 29 日 23:58 + - 在 nmBot 面板设置关键词回复时,允许将回复内容留空。 - 补充说明:回复内容为空时,显示关闭按钮、自动删除回复消息等部分设置将无效。 + 补充说明:回复内容为空时,显示关闭按钮、自动删除回复消息等部分设置将无效。 - 调低了设置 nmBot 管理功能所需要的管理员权限。调整后,管理员需要拥有封禁成员权限,方可设置 nmBot 管理功能。 - 在 nmBot 面板查看关键词回复列表时,支持显示设置的高级操作。 - nmBot 关键词回复功能支持设置关闭按钮允许操作的用户范围,并新增了相应的提示。 @@ -540,25 +636,31 @@ index: 0 - 修复了 nmBot 面板 oAuth 验证页面文本可被选择的问题。 ## 2022 年 7 月 24 日 17:20 + - 修复了使用 nmBot 面板管理超过一定数量的群组时,nmBot 面板可能无法初始化群组数据的问题。 -* 由于此次更新涉及 nmBot 面板底层,若在使用中遇到问题,请尝试清空您的 webView 缓存。 +- 由于此次更新涉及 nmBot 面板底层,若在使用中遇到问题,请尝试清空您的 webView 缓存。 ## 2022 年 7 月 24 日 16:56 + - 修复了 /mr 指令发送的私信格式化错误的问题。 - + ## 2022 年 7 月 24 日 16:24 + - nmBot /admin 指令也响应含有 @admin 和 @管理员 的消息。 - 优化了 nmBot /admin 指令对频道身份的相关识别。 - 优化了使用频道身份发送 /panel 指令时的相关提示。 - 更新了 Telegram 输入框中提供的 nmBot 功能。 ## 2022 年 7 月 24 日 13:28 + - nmBot 管理员内部管理指令更新。 ## 2022 年 7 月 24 日 11:47 + - 修正了 nmBot 处理过期入群验证时的错误处理逻辑问题。 ## 2022 年 7 月 20 日 16:00 + - 调整 nmBot 入群验证的允许验证时间至 150 秒。 - /panel 指令和 /mr 指令新增通过浏览器打开 nmBot 面板的相关提示。 - 更新了 /start 菜单的内容。 @@ -573,38 +675,49 @@ index: 0 - 更新了 nmBot 帮助文档。 ## 2022 年 7 月 19 日 13:35 + - 修复了 nmBot 欢乐复读功能可能未正确复读贴纸的问题。 ## 2022 年 7 月 18 日 20:03 + - 修复了 nmBot 欢乐复读功能失效的问题。 ## 2022 年 7 月 17 日 21:22 + - 支持通过 nmBot 面板设置入群欢迎。 ## 2022 年 7 月 17 日 20:26 + - 修复了使用 nmBot 的 /pm 指令时,若没有回复消息,将报错并无法使用的问题。 ## 2022 年 7 月 17 日 18:53 + - nmBot 团队分析后台日志后已基本定位问题系默认自动过期有低概率出现问题。为避免问题影响用户使用,已暂时关闭该逻辑,并重新开启定时确认过期入群验证逻辑作为备份。用户可能发现入群验证的提供时间延迟最多 30 秒。 - 修复了一处 nmBot 日志记录逻辑错误的问题。 ## 2022 年 7 月 17 日 12:15 + - 修复了 nmBot 确认过期入群验证时出现错误并重启的问题。 - + ## 2022 年 7 月 17 日 12:05 + - 为方便后续排查和解决问题,nmBot 测试为部分操作记录日志。记录信息不包含用户发送的任何消息内容。 ## 2022 年 7 月 17 日 00:00 -- nmBot 团队监测到 nmBot 入群验证的时间计算可能存在错误,并导致验证时间大幅缩短的问题。现临时关闭了定期确认过期验证的相关功能,nmBot 团队将监测此更改带来的影响,并将在后续通过多种方式持续监测和修复相关问题。 -对于此问题对群组管理造成的不便,nmBot 团队深感抱歉。 + +- nmBot 团队监测到 nmBot 入群验证的时间计算可能存在错误,并导致验证时间大幅缩短的问题。现临时关闭了定期确认过期验证的相关功能,nmBot 团队将监测此更改带来的影响,并将在后续通过多种方式持续监测和修复相关问题。\ + 对于此问题对群组管理造成的不便,nmBot 团队深感抱歉。 ## 2022 年 7 月 15 日 21:12 + - 修复了管理员关闭其他人打开的 nmBot 帮助文档时错误显示错误提示的问题。 ## 2022 年 7 月 14 日 16:18 + - 修复了 nmartChat 在群组错误回复非回复 nmBot 的 userbot 消息的问题。 ## 2022 年 7 月 13 日 23:40 + - 修复了在启用了“删除频道消息”功能的群组使用频道身份发言会意外触发“欢乐复读”功能,并可能导致骚扰的问题。修复后频道身份发言既不会触发,也不会影响“欢乐复读”功能。 - nmBot 帮助文档中新增“欢乐复读”的相关帮助。 - nmBot 帮助文档部分条目中新增指向 nmBot 面板的引导。 @@ -612,51 +725,61 @@ index: 0 - 修复了群组入群验证设置错误时,nmBot 进程意外中止的问题,并优化了相应的提示。 ## 2022 年 7 月 13 日 22:33 -- 新增群组功能“欢乐复读”。 -启用 nmBot 管理功能后,可以在 /config 菜单和 nmBot 面板中启用“欢乐复读”功能。 -“欢乐复读”功能将在群组中连续出现两条相同的文本、贴纸和部分其它类型消息时,复制该消息内容。 + +- 新增群组功能“欢乐复读”。\ + 启用 nmBot 管理功能后,可以在 /config 菜单和 nmBot 面板中启用“欢乐复读”功能。\ + “欢乐复读”功能将在群组中连续出现两条相同的文本、贴纸和部分其它类型消息时,复制该消息内容。 - 若第二条消息带有转发来源,nmBot 将复读并转发第二条消息的消息来源。 - 若第二条消息不带有转发来源,且两条消息中有至少一条回复给某条消息,nmBot 将复读并回复给就近带回复消息的消息的回复消息。 - 若消息内容疑似 Telegram 机器人指令、userbot 指令、与 nmBot 互动的消息,或触发了群组中设置的关键词回复,nmBot 将不会复读。 -- 对 nmBot 的消息使用部分 userbot 指令时,nmBot 将可能随机作出不同反应。(在群组中使用时,该群组需要开启 nmartChat。) -注:该反应内容仅供娱乐,不代表 nmTeam 对任何 userbot 及其制作者的态度。 +- 对 nmBot 的消息使用部分 userbot 指令时,nmBot 将可能随机作出不同反应。(在群组中使用时,该群组需要开启 nmartChat。)\ + 注:该反应内容仅供娱乐,不代表 nmTeam 对任何 userbot 及其制作者的态度。 - 为方便用户使用 userbot 转发 nmBot 的消息,nmBot 将屏蔽部分 userbot 的相关转发指令。 - 解决了 /pm 指令识别频道身份消息的问题。 - 面向 nmBot 管理员的内部优化。 ## 2022 年 7 月 12 日 23:53 + - 修复 nmBot 面板中管理功能的部分按钮文本错误的问题。 ## 2022 年 7 月 12 日 21:57 + - nmartChat 将在请求数据中忽略网页链接。 ## 2022 年 7 月 12 日 17:05 + - nmBot 内部架构更新带来更好的性能和稳定性。 - - 注:由于本次更新涉及较多的 nmBot 内部优化,若在使用中遇到任何问题,还请及时向 nmBot 团队报告。 + - 注:由于本次更新涉及较多的 nmBot 内部优化,若在使用中遇到任何问题,还请及时向 nmBot 团队报告。 - 修正了 nmBot 部分错误提示错误的问题。 - 修正了 nmBot 帮助文档中的一处错误。 ## 2022 年 7 月 9 日 12:24 + - 优化了 nmBot 处理过期入群验证的相关逻辑。 - 修复了 nmBot 处理过期入群验证失败后,不会执行下一个的问题。 ## 2022 年 7 月 8 日 00:13 + - nmBot 关键词回复支持设置自动删除回复消息、显示“关闭”按钮和回复消息的格式化方案。 - 若群主管理员一分钟内第二次触发设置了“忽略群组管理员”的关键词回复,nmBot 将提醒群组管理员此关键词回复已忽略群组管理员。 - 修复了 nmBot 面板骨架加载页面中的按钮可供键盘导航的问题。 - 修复了在 nmBot 面板从“鲁小迅”导入关键词回复时未正确转义 $username 的问题。 ## 2022 年 7 月 7 日 22:10 + - 修复了 nmBot 面板识别群主无法修改 nmBot 管理功能配置的问题。 ## 2022 年 7 月 7 日 20:30 + - 在 nmBot 面板关键词回复添加和编辑关键词回复时,仅在修改内容后退出时显示确认操作对话框。 - 修复了没有更改群组信息权限的管理员无法在 nmBot 面板查看和编辑 nmBot 关键词回复的问题。 ## 2022 年 7 月 7 日 20:10 + - nmBot 关键词回复按钮中支持添加 tg 协议链接。 ## 2022 年 7 月 6 日 16:45 + - nmBot 面板现已作为 nmBot 的正式功能推出。 - nmBot 出现错误后将发送警告消息,必要时将尝试重新启动。 - 优化了 nmBot 的错误处理逻辑,提高了 nmBot 的稳定性。 @@ -664,22 +787,27 @@ index: 0 - 调整 nmBot 判断消息过期的时间为 45 秒。 ## 2022 年 7 月 5 日 11:11 + - 修复了 nmBot 入群欢迎出现错误的问题。 - 提升了 nmBot 的流畅度。 - 优化了 nmBot 的稳定性。 ## 2022 年 7 月 4 日 15:40 + - 修复了管理页面和关键词回复页面的搜索框无法正常工作的问题。 - 修复了在 nmBot 面板添加和编辑关键词回复时,若回复内容中含换行时保存不起效果的问题。 ## 2022 年 7 月 3 日 15:00 + - 支持在 /warn 命令中传入 d 参数来删除源消息。 - 更新了 nmBot 帮助文档。 ## 2022 年 7 月 3 日 14:30 + - 修复了 /warn 命令无法封禁频道的问题。 ## 2022 年 7 月 3 日 14:22 + - 支持使用 /warn 命令警告用户。 - 支持使用 /kick 命令踢出用户。 - 优化了 nmBot 更新时对期间消息的处理方法。 @@ -690,26 +818,33 @@ index: 0 - 此更新将在下一次 nmBot 更新中生效。 ## 2022 年 7 月 3 日 06:00 + - 优化了加载数据时 nmBot 面板 Telegram 登录按钮的提示。 - 解决了 nmBot 面板顶栏的适配问题,并优化了显示效果。 ## 2022 年 7 月 3 日 05:52 + - nmBot 面板在双屏模式下显示顶栏。 ## 2022 年 7 月 3 日 05:45 + - 优化了 nmBot 面板在双屏模式下的显示效果。 ## 2022 年 7 月 3 日 05:35 + - 优化了浏览器对 nmBot 面板的名称、语言和图标显示。 ## 2022 年 7 月 3 日 05:16 + - 支持在浏览器中登录 nmBot 面板。 - 优化了 nmBot 面板识别和应用浏览器颜色主题的能力,并解决了相应的已知问题。 ## 2022 年 6 月 30 日 13:15 + - 修复了 nmBot 面板头像 API 在无法获取用户头像时可能返回错误信息并暴露机器人密钥的漏洞,并更换了新的密钥。暂未监测到此漏洞对机器人运行产生任何影响。同时为避免类似问题出现,nmBot 面板线上版本 API 禁用了报错。 ## 2022 年 6 月 29 日 22:50 + - 支持从“鲁小迅”导入关键词回复。 - nmartChat 将 API 中的特定消息进行文本替换后回复。 - 添加了部分 nmBot 行内消息的说明。 @@ -723,6 +858,7 @@ index: 0 - 修正了 nmBot 修改名片 API 错误返回 Debug 数据的问题。 ## 2022 年 6 月 29 日 01:50 + - 在群组中使用 /pm 指令确认自己与他人的 Telegram 会员身份。 - 支持在 nmBot 面板设置自己的个人信息,并在群组中通过 /me 命令或行内消息发送。 - nmBot 现已响应 Telegram 行内消息。 @@ -731,13 +867,16 @@ index: 0 - 修复了一个可被非法利用以改变当前群组的数据库群组 ID 和其它字段的漏洞。 ## 2022 年 6 月 27 日 23:45 + - 更正了 nmBot 帮助文档中的错别字。 ## 2022 年 6 月 27 日 23:00 + - 修复了无法打开 /config 面板的问题。 ## 2022 年 6 月 27 日 22:50 -- nmBot 面板 \[Beta\] 全新上线,通过更轻松的方式管理群组,设置关键词回复。 + +- nmBot 面板 [Beta] 全新上线,通过更轻松的方式管理群组,设置关键词回复。 - nmBot 将以顿号(、)开头的消息视作以斜杠(/)开头的 nmBot 指令。 - 更新了 nmBot 面板运行所需的指令集,包括 /panel,/mr,/regadmin。 - 使用 /config、/ar、/dr、/sr、/lr 时,nmBot 推荐用户使用 nmBot 面板完成操作。 @@ -748,9 +887,11 @@ index: 0 - 修复了 /admin 指令的定位消息功能在私密群组中无法正常工作的问题。 ## 2022 年 6 月 21 日 20:38 + - 修复了 nmBot 在管理功能开启,“删除会员贴纸”功能未开启时错误回复无管理员权限会员发送的会员贴纸的问题。 ## 2022 年 6 月 21 日 00:45 + - 屏蔽了 nmartChat API 中的部分第三方隐私信息。 - 关于 nmBot » 资讯与交流 中新增 nmBot 频道。 - 新增了 nmBot 在 Telegram 中显示的“它能做什么” GIF。 @@ -761,27 +902,32 @@ index: 0 - 经确认无误,回退了对 nmBot 自动确认过期入群申请的时间的更改。 ## 2022 年 6 月 20 日 13:35 + - 修复了开启仅允许 Telegram 会员加入群组设置时,nmBot 会踢出邀请的机器人的问题。 ## 2022 年 6 月 20 日 13:28 + - 支持仅允许 Telegram 会员加入群组。 - 修复了 nmBot 部分函数调用参数错误的问题。 ## 2022 年 6 月 20 日 12:21 + - 更新了 nmBot 帮助文档。 ## 2022 年 6 月 20 日 12:12 + - 支持设置自动通过 Telegram 会员入群。 - 支持自动删除会员专享贴纸。 - 在消息文本中体现 Telegram 会员身份。 - 修复了数据库中找不到群组时 nmBot 进程意外中止的问题,并优化了相关逻辑。 - 修复了 nmBot 不处理私信的问题。 -- 调整了 nmBot 自动确认过期入群申请的时间,以缓解验证时间错误的问题。 +- 调整了 nmBot 自动确认过期入群申请的时间,以缓解验证时间错误的问题。 - 缓解了部分用户设置的账号名称可能无法被 nmBot 正确解析并可能造成安全性问题的问题。 - 修复了 nmBot 超级回复对象无法在 /lr 和 /sr 结果中正常显示的问题。 - 修复了使用 /sr 命令时 nmBot 崩溃的问题。 ## 2022 年 6 月 14 日 18:45 + - 新增对 nmBot 超级回复对象的支持。 - 支持使用频道验证。 - 新增入群欢迎功能。 @@ -789,6 +935,7 @@ index: 0 - 更新了 nmBot 帮助文档。 ## 2022 年 6 月 12 日 15:30 + - nmartChat 将响应消息内容包含自身名称的消息。 - 修复了未启用 nmBot 管理功能的群组无法打开 nmBot 控制面板的问题。 - 修复了 nmBot 入群验证超时后,原入群验证操作消息不会更新为超时提示的问题。 @@ -796,23 +943,28 @@ index: 0 - 更新了 nmBot 帮助文档。 ## 2022 年 6 月 10 日 23:22 + - 修复了在未启用管理功能的群组中收到频道身份消息时,nmBot 会执行部分操作两次的问题。 - 修复了 nmBack 功能出现卡死的问题。 - 修复了 nmBack 功能不识别负数参数的问题。 ## 2022 年 6 月 10 日 22:32 + - “关键词回复”不再需要启用 nmBot 管理功能。 - 修复了调用 nmBot 指令操作“关键词回复”时,仍会触发关键词回复的问题。 ## 2022 年 6 月 10 日 20:32 + - 修复了 nmartChat 错误转译消息内容中中文的问题。 ## 2022 年 6 月 10 日 16:03 + - 修复了 nmartChat 在消息内容中含部分字符时请求出错的问题。 ## 2022 年 6 月 9 日 14:55 -- nmBot 在收到私信和打开 nmartChat 功能的群组回复的贴纸消息时,将回复贴纸。 -具体回复逻辑如下: + +- nmBot 在收到私信和打开 nmartChat 功能的群组回复的贴纸消息时,将回复贴纸。\ + 具体回复逻辑如下: - nmBot 有 30% 概率回复同一张贴纸。 - 若 nmBot 未回复同一张贴纸,则在设置了群组贴纸包的群组中有 50% 概率随机(无排除)抽取该贴纸包一张贴纸回复。 - 若 nmBot 仍未回复贴纸,则将从收到贴纸的贴纸包中随机(无排除)选择一张贴纸回复。 @@ -820,9 +972,11 @@ index: 0 - 更新了 nmBot 帮助文档。 ## 2022 年 6 月 9 日 23:38 + - nmartChat 不再返回消息中的 {face} 部分。 ## 2022 年 6 月 9 日 23:30 + - 优化了 nmBot 对 nmartChat 聊天消息内容的识别。 - 在私信中触发 nmartChat 时,不再删除消息中的 nm。 - 在群组中回复 nmBot 消息时,不再删除消息中的 nm。 @@ -831,38 +985,49 @@ index: 0 - 不再删除消息内非句首的 nm。 ## 2022 年 6 月 9 日 22:58 + - 修正群组首次设置关键词回复失败且无响应的问题。 ## 2022 年 6 月 9 日 22:50 + - 支持关键词回复功能。 ## 2022 年 6 月 9 日 15:20 + - 数据库修正:修正数据库默认设置新群组的群组管理功能为开的问题。 ## 2022 年 6 月 9 日 00:18 + - 允许通过 /help 参数 p 创建人人能操作的帮助文档。 - 群组内 /help 提示新增“群组管理员可以使用‘关闭’按钮快速删除此消息”和“可以通过 /help p 创建人人能操作的帮助文档”(若未传递此参数)提示。 -- 修复了管理员通过关闭按钮关闭帮助文档时错误提示无权限操作他人打开的帮助文档的问题。 +- 修复了管理员通过关闭按钮关闭帮助文档时错误提示无权限操作他人打开的帮助文档的问题。 ## 2022 年 6 月 8 日 21:35 + - 修正 nmBot 在收到频道和链接频道消息时,若不是群组管理员,可能错误回复消息的问题。 ## 2022 年 6 月 8 日 19:18 + - 在群组中查阅帮助文档时,建议用户在私信中查看。 ## 2022 年 6 月 8 日 18:25 + - 修正 nmBot 在接收入群消息时,若不是群组管理员,将错误发送提示消息的问题。 ## 2022 年 6 月 8 日 18:23 + - 修复 nmBot 聊天功能失效且可能导致进程意外退出的问题。 - 修复 nmBot 在接收入群消息时无法获取自身权限且可能导致进程意外退出的问题。 ## 2022 年 6 月 8 日 17:12 + - 修正用户名称出现错误空格的问题。 - 修复无法打开更新日志的问题。 ## 2022 年 6 月 8 日 16:56 + - 修复一个可能导致 nmBot 无管理员权限时在新成员入群时崩溃的问题。 ## 2022 年 6 月 8 日 16:40 + - nmBot 测试。 diff --git a/docs/nmteam-account/index.md b/docs/nmteam-account/index.md index fc2f931..4f2fcfa 100644 --- a/docs/nmteam-account/index.md +++ b/docs/nmteam-account/index.md @@ -38,10 +38,13 @@ nmTeam 账号是 nmTeam 服务的统一身份认证系统。通过 nmTeam 账号 ## 常见问题 ??? question "如何重置我的密码?" + 您可以在 [nmTeam 账号中心](https://accounts.nmteam.xyz/) 的登录页面点击"忘记密码"链接来重置您的密码。系统会向您的注册邮箱发送密码重置链接。 ??? question "如何更改我的用户名?" + 登录 nmTeam 账号中心后,您可以在"个人资料"页面中修改您的用户名。请注意,用户名更改可能会有一定的限制。 ??? question "如何删除我的账号?" + 如果您希望删除 nmTeam 账号,请联系 nmTeam 支持。请注意,账号删除后将无法恢复,且您将失去对所有关联服务的访问权限。 diff --git a/docs/nmteam-account/issues.md b/docs/nmteam-account/issues.md index 5c1dd2b..eeaaafc 100644 --- a/docs/nmteam-account/issues.md +++ b/docs/nmteam-account/issues.md @@ -9,15 +9,16 @@ index: -1 此页面记录 nmTeam 账号中心的已知问题和已解决的问题。我们会持续更新此页面,以便您了解最新的系统状态。 !!! info "需要帮助?" + 如果您遇到的问题未在此页面列出,或需要进一步的帮助,请[联系 nmTeam 支持](/contact-us/contact-support.md)。 ## 正在进行中 以下问题正在调查或修复中: -| 问题描述 | 状态 | 预计解决时间 | -| :--- | :--- | :--- | -| "通行密钥"功能目前仅支持使用实体密钥。我们正在调查对使用浏览器内置安全密钥的用户不可用的问题。 | :material-progress-wrench: 调查中 | 待定 | +| 问题描述 | 状态 | 预计解决时间 | +| :--------------------------------------------------------------------------------------------- | :-------------------------------- | :----------- | +| "通行密钥"功能目前仅支持使用实体密钥。我们正在调查对使用浏览器内置安全密钥的用户不可用的问题。 | :material-progress-wrench: 调查中 | 待定 | ### 临时解决方案 @@ -30,18 +31,18 @@ index: -1 以下问题已被修复: -| 解决日期 | 问题描述 | -| :--- | :--- | -| 2024 年 9 月 21 日 | 用户可能遇到无法关闭两步验证的问题。 | +| 解决日期 | 问题描述 | +| :----------------- | :-------------------------------------------------------------------- | +| 2024 年 9 月 21 日 | 用户可能遇到无法关闭两步验证的问题。 | | 2024 年 7 月 23 日 | 部分使用 Gmail 邮箱的用户可能无法收到 nmTeam 账号中心发送的验证邮件。 | -| 2024 年 7 月 23 日 | 部分邮箱中含有特定字符的用户可能无法注册 nmTeam 账号。 | +| 2024 年 7 月 23 日 | 部分邮箱中含有特定字符的用户可能无法注册 nmTeam 账号。 | ## 如何报告问题 如果您发现 nmTeam 账号系统的问题,请通过以下方式报告: 1. 通过 [nmTeam 支持](/contact-us/contact-support.md)提交问题报告 -2. 在报告中请包含以下信息: +1. 在报告中请包含以下信息: - 问题的详细描述 - 发生问题的时间 - 您使用的浏览器和操作系统 From 396ef699197c73f9705db846831f9f80cc087e64 Mon Sep 17 00:00:00 2001 From: Alice39s Date: Sun, 9 Aug 2026 17:17:06 +0900 Subject: [PATCH 16/57] ci(actions): add uv, lint, test, and build checks --- .github/workflows/ci.yml | 41 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 .github/workflows/ci.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..16d9cb6 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,41 @@ +# .github/workflows/ci.yml +name: CI + +on: + push: + branches: ["main"] + pull_request: + +jobs: + ci: + runs-on: ubuntu-latest + timeout-minutes: 15 + permissions: {} + steps: + - uses: actions/checkout@v5 + + - name: Install uv + uses: astral-sh/setup-uv@v9 + with: + enable-cache: true + + - name: Sync dependencies (frozen) + run: uv sync --frozen + + - name: Ruff lint + run: uv run ruff check . + + - name: Ruff format check + run: uv run ruff format --check . + + - name: Tests + run: uv run pytest + + - name: Markdown format check + run: uv run mdformat --check docs/ + + - name: Generate documentation + run: uv run python -m nmteam_support + + - name: Build site (strict) + run: uv run mkdocs build --strict From ab6c2e7daefc33617f5191817cb13842e64ff27b Mon Sep 17 00:00:00 2001 From: Alice39s Date: Sun, 9 Aug 2026 17:19:59 +0900 Subject: [PATCH 17/57] chore(tooling): remove legacy dependency workflows --- .gitignore | 8 ++-- README.md | 84 ++++++++++++++++--------------------- dev.bat | 2 +- dev.ps1 | 4 +- dev.sh | 2 +- docs/assets/js/redirects.js | 20 --------- requirements.txt | 6 --- runtime.txt | 1 - 8 files changed, 44 insertions(+), 83 deletions(-) delete mode 100644 docs/assets/js/redirects.js delete mode 100644 requirements.txt delete mode 100644 runtime.txt diff --git a/.gitignore b/.gitignore index 58e672f..4d1c76b 100644 --- a/.gitignore +++ b/.gitignore @@ -81,9 +81,6 @@ target/ profile_default/ ipython_config.py -# pyenv -.python-version - # pipenv # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. # However, in case of collaboration, if having platform-specific dependencies or dependencies @@ -131,4 +128,7 @@ dmypy.json # nmTeam Doc Generator /generated /cache -/mkdocs.yml \ No newline at end of file +/mkdocs.yml + +# uv / ruff +.ruff_cache/ \ No newline at end of file diff --git a/README.md b/README.md index b8a4608..102860f 100644 --- a/README.md +++ b/README.md @@ -1,67 +1,55 @@ # Support -[**nmTeam 支持**](https://support.nmteam.xyz)官方网站。使用 `mkdocs-material` 进行构建。 +[**nmTeam 支持**](https://support.nmteam.xyz)官方网站。使用 `mkdocs-material` 构建,工具链由 `uv` 管理。 -# 快速开始 +## 环境要求 +- Python 3.14+(由 `uv` 按 `.python-version` 自动管理) +- [uv](https://docs.astral.sh/uv/)(`curl -LsSf https://astral.sh/uv/install.sh | sh`) -## 安装依赖 -```bash -# 方式1: 使用管理脚本自动安装 -python manage.py install +## 快速开始 -# 方式2: 手动安装 -pip install -r requirements.txt +### 安装依赖 +```bash +uv sync ``` -## 开发模式 (推荐) +### 开发模式(推荐) ```bash -# 启动开发服务器 (支持热更新) -python manage.py dev - -# 或使用便捷脚本 (Windows) -.\dev.ps1 dev -dev.bat dev - -# 或使用便捷脚本 (Linux/macOS) -./dev.sh dev +uv run python manage.py dev ``` - 开发模式会: -- 自动生成文档结构 -- 启动 MkDocs 开发服务器 (http://127.0.0.1:8000) -- 监听 `docs/` 目录文件变化并自动重新生成 -- 支持热更新,修改后自动刷新页面 +- 自动生成文档结构(`cache/`、`generated/`、`mkdocs.yml`) +- 启动 MkDocs 开发服务器(http://127.0.0.1:8000) +- 监听 `docs/` 目录变化并自动重新生成,浏览器热更新 -## 构建生产版本 +### 构建生产版本 ```bash -# 构建静态站点 -python manage.py build - -# 或使用便捷脚本 -.\dev.ps1 build +uv run python manage.py build ``` +构建结果输出到 `site/` 目录。 -构建过程会: -1. 运行 `generate.py` 生成文档结构 -2. 执行 `mkdocs build` 生成静态站点到 `site/` 目录 - -## 其他命令 +### 其他命令 ```bash -# 清理生成的文件 -python manage.py clean - -# 查看帮助 -python manage.py help +uv run python manage.py clean # 清理 cache/ generated/ site/ +uv run python manage.py install # 等价于 uv sync +uv run python -m nmteam_support # 仅重新生成文档结构 ``` -# 传统部署方式 - -如果你更喜欢传统的分步操作: - -- 安装依赖 `pip install -r requirements.txt` -- 启动本地服务器 `python -m mkdocs serve` -- 构建生成目录 `python generate.py` -- 构建静态界面 `python -m mkdocs build` +## 质量检查 +```bash +uv run ruff check . # lint +uv run ruff format --check . # Python 格式检查 +uv run pytest # 单元测试 +uv run mdformat --check docs/ # Markdown 格式检查 +uv run mkdocs build --strict # 严格模式构建 +``` +以上步骤由 CI(`.github/workflows/ci.yml`)自动执行。 -# 贡献 +## 重定向管理 +```bash +uv run python redirects_manager.py list +uv run python redirects_manager.py add "/old-path/" "/new-path/" +uv run python redirects_manager.py remove "/old-path/" +``` +## 贡献 欢迎您在 GitHub 上提出问题并贡献文档。 diff --git a/dev.bat b/dev.bat index f77841d..75d1599 100644 --- a/dev.bat +++ b/dev.bat @@ -17,4 +17,4 @@ if "%1"=="" ( goto :eof ) -python manage.py %* +uv run python manage.py %* diff --git a/dev.ps1 b/dev.ps1 index 81f116e..4356056 100644 --- a/dev.ps1 +++ b/dev.ps1 @@ -26,10 +26,10 @@ if ($Command -eq "" -or $Command -eq "help") { Write-Host "示例:" -ForegroundColor Yellow Write-Host " .\dev.ps1 dev # 启动开发服务器" -ForegroundColor Gray Write-Host " .\dev.ps1 build # 构建生产版本" -ForegroundColor Gray - Write-Host " .\dev.ps1 clean # 清理临时文件" -ForegroundColor Gray + Write-Host " .\dev.ps1 clean # 清理生成的文件" -ForegroundColor Gray exit 0 } # 执行 Python 脚本 $arguments = @($Command) + $RemainingArgs -& python manage.py $arguments +& uv run python manage.py $arguments diff --git a/dev.sh b/dev.sh index 0d9968c..b0742fe 100644 --- a/dev.sh +++ b/dev.sh @@ -17,4 +17,4 @@ if [ $# -eq 0 ]; then exit 0 fi -python manage.py "$@" +uv run python manage.py "$@" diff --git a/docs/assets/js/redirects.js b/docs/assets/js/redirects.js deleted file mode 100644 index 2b724cd..0000000 --- a/docs/assets/js/redirects.js +++ /dev/null @@ -1,20 +0,0 @@ -// 简单的客户端重定向脚本 -(function () { - // 重定向映射 - const redirects = { - '/nmbot-telegram/about-panel/': '/nmbot-telegram/panel/', - }; - - // 获取当前路径 - const currentPath = window.location.pathname; - - // 检查是否需要重定向 - for (const oldPath in redirects) { - if (currentPath === oldPath || currentPath.startsWith(oldPath)) { - const newPath = redirects[oldPath]; - // 执行重定向 - window.location.replace(newPath); - break; - } - } -})(); diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index cc343c6..0000000 --- a/requirements.txt +++ /dev/null @@ -1,6 +0,0 @@ -mkdocs -mkdocs-material -watchdog -#Add pip package if needed -#mkdocs-bootswatch -#mkdocs-minify-plugin diff --git a/runtime.txt b/runtime.txt deleted file mode 100644 index 548d713..0000000 --- a/runtime.txt +++ /dev/null @@ -1 +0,0 @@ -3.7 \ No newline at end of file From 8426340a2152a2e37f58a41e67f50e68c1d36573 Mon Sep 17 00:00:00 2001 From: Alice39s Date: Sun, 9 Aug 2026 17:23:03 +0900 Subject: [PATCH 18/57] fix(docs): replace broken account documentation links --- docs/nmteam-account/index.md | 8 ++++---- docs/nmteam-account/issues.md | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/nmteam-account/index.md b/docs/nmteam-account/index.md index 4f2fcfa..4eca402 100644 --- a/docs/nmteam-account/index.md +++ b/docs/nmteam-account/index.md @@ -14,20 +14,20 @@ nmTeam 账号是 nmTeam 服务的统一身份认证系统。通过 nmTeam 账号 ## 快速链接 - [nmTeam 账号中心](https://accounts.nmteam.xyz/) -- [注册 nmTeam 账号](register.md) -- [启用双重验证](two_factor_authentication.md) +- [注册 nmTeam 账号](https://accounts.nmteam.xyz/) +- [启用双重验证](https://accounts.nmteam.xyz/) - [已知问题](issues.md) ## 帮助主题 ### 账号注册与登录 -- **[注册 nmTeam 账号](register.md)**:了解如何创建新的 nmTeam 账号。 +- **[注册 nmTeam 账号](https://accounts.nmteam.xyz/)**:了解如何创建新的 nmTeam 账号。 - **登录问题**:如果您在登录时遇到问题,请检查您的用户名和密码是否正确,或尝试重置密码。 ### 账号安全 -- **[启用双重验证](two_factor_authentication.md)**:了解如何通过双重验证来增强账号安全性。 +- **[启用双重验证](https://accounts.nmteam.xyz/)**:了解如何通过双重验证来增强账号安全性。 - **密码管理**:建议定期更新您的密码,并使用强密码来保护您的账号。 ### 问题与支持 diff --git a/docs/nmteam-account/issues.md b/docs/nmteam-account/issues.md index eeaaafc..d37907c 100644 --- a/docs/nmteam-account/issues.md +++ b/docs/nmteam-account/issues.md @@ -25,7 +25,7 @@ index: -1 对于"通行密钥"功能的问题,您可以暂时使用以下替代方案: - 使用支持的实体安全密钥(如 YubiKey) -- 使用[双重验证(2FA)](two_factor_authentication.md)作为替代的安全措施 +- 使用[双重验证(2FA)](https://accounts.nmteam.xyz/)作为替代的安全措施 ## 已解决的问题 From 687cfe2ae7a19b0677779f4413a0e6d5e86d7eb0 Mon Sep 17 00:00:00 2001 From: Alice39s Date: Sun, 9 Aug 2026 17:23:22 +0900 Subject: [PATCH 19/57] ci(actions): run checks on dev branch --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 16d9cb6..c703da8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -3,7 +3,7 @@ name: CI on: push: - branches: ["main"] + branches: ["main", "dev"] pull_request: jobs: From d4e62b30ab43813b368cf327fd9d48c74eab89d4 Mon Sep 17 00:00:00 2001 From: Alice39s Date: Sun, 9 Aug 2026 17:32:01 +0900 Subject: [PATCH 20/57] fix(cli): keep file watcher alive after generation errors --- src/nmteam_support/cli.py | 5 ++++- tests/test_cli.py | 28 ++++++++++++++++++++++++++++ 2 files changed, 32 insertions(+), 1 deletion(-) diff --git a/src/nmteam_support/cli.py b/src/nmteam_support/cli.py index 422e7cf..c14eb25 100644 --- a/src/nmteam_support/cli.py +++ b/src/nmteam_support/cli.py @@ -64,7 +64,10 @@ def _watch_and_regenerate(options: GeneratorOptions, stop: threading.Event) -> N if current != last: last = current print("🔄 检测到变更,重新生成...") - generate(options) + try: + generate(options) + except Exception as error: + print(f"⚠️ 文档生成失败,继续监听: {error}", file=sys.stderr) def _snapshot(options: GeneratorOptions) -> tuple[tuple[int, int], ...]: diff --git a/tests/test_cli.py b/tests/test_cli.py index 3b76b90..efb488f 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -2,6 +2,7 @@ from pathlib import Path +import nmteam_support.cli as cli from nmteam_support.cli import cmd_clean from nmteam_support.generator import GeneratorOptions @@ -26,3 +27,30 @@ def test_cmd_clean_removes_output_dirs(tmp_path): assert not opts.cache_dir.exists() assert not opts.generated_dir.exists() assert not (tmp_path / "site").exists() + + +def test_watcher_continues_after_generation_error(tmp_path, monkeypatch, capsys): + opts = _options(tmp_path) + snapshots = iter([(), ((1, 1),), ((2, 2),)]) + generate_calls = 0 + + class StopAfterTwoChanges: + waits = 0 + + def wait(self, _timeout): + self.waits += 1 + return self.waits > 2 + + def generate_once_then_succeed(_options): + nonlocal generate_calls + generate_calls += 1 + if generate_calls == 1: + raise OSError("temporary failure") + + monkeypatch.setattr(cli, "_snapshot", lambda _options: next(snapshots)) + monkeypatch.setattr(cli, "generate", generate_once_then_succeed) + + cli._watch_and_regenerate(opts, StopAfterTwoChanges()) + + assert generate_calls == 2 + assert "temporary failure" in capsys.readouterr().err From 97b3ba75b513daec061096c2e3442e119bdc3325 Mon Sep 17 00:00:00 2001 From: Alice39s Date: Sun, 9 Aug 2026 17:33:30 +0900 Subject: [PATCH 21/57] fix(ci): pin setup-uv to a published release --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c703da8..4fd3f04 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -15,7 +15,7 @@ jobs: - uses: actions/checkout@v5 - name: Install uv - uses: astral-sh/setup-uv@v9 + uses: astral-sh/setup-uv@v9.0.0 with: enable-cache: true From a2e7c47fa12bfdde62f167e2ffd6305e80a8e4bf Mon Sep 17 00:00:00 2001 From: Alice39s Date: Sun, 9 Aug 2026 17:51:16 +0900 Subject: [PATCH 22/57] fix(tooling): harden deterministic documentation builds --- dev.ps1 | 8 +++++--- pyproject.toml | 5 +++++ src/nmteam_support/index.py | 15 ++++++++++++++- src/nmteam_support/scanner.py | 2 +- src/nmteam_support/template.py | 4 +++- tests/test_index.py | 13 +++++++++++++ tests/test_nav.py | 11 +++++++++++ tests/test_scanner.py | 18 ++++++++++++++++++ 8 files changed, 70 insertions(+), 6 deletions(-) diff --git a/dev.ps1 b/dev.ps1 index 4356056..4e7432c 100644 --- a/dev.ps1 +++ b/dev.ps1 @@ -1,7 +1,4 @@ # nmTeam Documentation Management PowerShell Script -# Enable UTF-8 encoding -[Console]::OutputEncoding = [System.Text.Encoding]::UTF8 -$OutputEncoding = [System.Text.Encoding]::UTF8 param( [Parameter(Position=0)] @@ -10,6 +7,11 @@ param( [string[]]$RemainingArgs ) +# Enable UTF-8 encoding (must come after the param() block, which PowerShell +# requires to be the first executable statement of the script) +[Console]::OutputEncoding = [System.Text.Encoding]::UTF8 +$OutputEncoding = [System.Text.Encoding]::UTF8 + Write-Host "nmTeam Documentation Manager" -ForegroundColor Cyan Write-Host "" diff --git a/pyproject.toml b/pyproject.toml index 1ef3041..0de5750 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -28,6 +28,11 @@ build-backend = "hatchling.build" [tool.hatch.build.targets.wheel] packages = ["src/nmteam_support"] +[[tool.uv.index]] +name = "pypi" +url = "https://pypi.org/simple" +default = true + [tool.pytest.ini_options] testpaths = ["tests"] pythonpath = ["src"] diff --git a/src/nmteam_support/index.py b/src/nmteam_support/index.py index 1091b56..ac77c16 100644 --- a/src/nmteam_support/index.py +++ b/src/nmteam_support/index.py @@ -2,17 +2,30 @@ from __future__ import annotations +import yaml + from nmteam_support.docslist import render_docs_list from nmteam_support.nav import folder_entries, sort_entries from nmteam_support.scanner import ScannedDir +def _yaml_title_scalar(title: str) -> str: + """Render ``title`` as a single valid YAML scalar, quoting only when needed.""" + out = yaml.safe_dump(title, default_flow_style=True, allow_unicode=True) + # PyYAML >= 6.0.3 closes a plain root scalar with an explicit document-end + # marker ("..."); it is not part of the scalar, so drop it. + if out.endswith("\n...\n"): + out = out[: -len("\n...\n")] + "\n" + return out.strip() + + def render_index_page(scan: ScannedDir) -> str: """Render the complete index.md content (frontmatter + body + docsList) for a directory.""" head_lines = [ "---", "automatically_generated: Don't edit this file directly, it's auto generated.", - f"title: {scan.index_meta.title}", + # safe_dump emits a properly quoted/escaped YAML scalar for any title. + f"title: {_yaml_title_scalar(scan.index_meta.title)}", "", "hide:", " - toc", diff --git a/src/nmteam_support/scanner.py b/src/nmteam_support/scanner.py index 60e98d6..f043564 100644 --- a/src/nmteam_support/scanner.py +++ b/src/nmteam_support/scanner.py @@ -44,7 +44,7 @@ def _scan(dir_path: Path, rel_path: str) -> ScannedDir: index_body = "" has_index = False - for name in os.listdir(dir_path): # keep listdir order for stable ties + for name in sorted(os.listdir(dir_path)): # deterministic order, cross-platform entry = dir_path / name child_rel = f"{rel_path}/{name}" if rel_path else name if entry.is_dir(): diff --git a/src/nmteam_support/template.py b/src/nmteam_support/template.py index 7a049d7..95ad7a7 100644 --- a/src/nmteam_support/template.py +++ b/src/nmteam_support/template.py @@ -13,4 +13,6 @@ def render_mkdocs_yml(template: str, nav_yaml: str) -> str: """Replace the nav placeholder block with ``nav_yaml``.""" block = f"{NAV_START}\n{nav_yaml}\n{NAV_END}" if nav_yaml else f"{NAV_START}\n{NAV_END}" - return _NAV_PATTERN.sub(block, template) + # Callable replacement: re.sub otherwise interprets backslash escapes in the + # replacement string (e.g. "\1" -> group reference), mangling literal nav text. + return _NAV_PATTERN.sub(lambda _m: block, template) diff --git a/tests/test_index.py b/tests/test_index.py index 8eecc2e..fbe0db6 100644 --- a/tests/test_index.py +++ b/tests/test_index.py @@ -1,5 +1,7 @@ """Index page generation tests.""" +import yaml + from nmteam_support.index import render_index_page from nmteam_support.scanner import scan_docs @@ -39,3 +41,14 @@ def test_index_page_default_content_without_index_md(tmp_path): page = render_index_page(scan_docs(d)) assert "# docs2\n" in page assert '
' in page # empty list div is still emitted + + +def test_index_page_title_with_yaml_special_chars_round_trips(tmp_path): + """Titles containing colons or hashes must be emitted as valid YAML so a + strict mkdocs build can parse the generated frontmatter.""" + d = tmp_path / "docs" + d.mkdir() + (d / "index.md").write_text("---\ntitle: FAQ: 常见问题 #1\n---\n\n# FAQ\n", encoding="utf-8") + page = render_index_page(scan_docs(d)) + frontmatter = page.split("---", 2)[1] + assert yaml.safe_load(frontmatter)["title"] == "FAQ: 常见问题 #1" diff --git a/tests/test_nav.py b/tests/test_nav.py index 07f38aa..ebdc0ab 100644 --- a/tests/test_nav.py +++ b/tests/test_nav.py @@ -3,6 +3,7 @@ from nmteam_support.models import DocEntry from nmteam_support.nav import build_nav_yaml, sort_entries from nmteam_support.scanner import scan_docs +from nmteam_support.template import render_mkdocs_yml def _entry(path, name, index=0, kind="doc"): @@ -67,3 +68,13 @@ def test_negative_index_doc_precedes_default_index_doc(docs_dir): < nav.index("'contact-us/forum.md'") < nav.index("'contact-us/support.md'") ) + + +def test_render_mkdocs_yml_keeps_backslash_digit_nav(): + """A nav line containing a backslash-digit sequence must be injected literally, + not interpreted as a regex group reference.""" + template = "site_name: Test\nnav:\n# NAV_ARIA_START\n# NAV_ARIA_END\n" + nav = " - Regex \\1 Escape: 'r.md'" + out = render_mkdocs_yml(template, nav) + assert "Regex \\1 Escape" in out + assert "# NAV_ARIA_START\n - Regex \\1 Escape: 'r.md'\n# NAV_ARIA_END" in out diff --git a/tests/test_scanner.py b/tests/test_scanner.py index aa3004b..4368a54 100644 --- a/tests/test_scanner.py +++ b/tests/test_scanner.py @@ -1,5 +1,8 @@ """Directory scanning tests.""" +import os + +from nmteam_support import scanner from nmteam_support.scanner import scan_docs @@ -35,3 +38,18 @@ def test_scan_skips_empty_md_files(docs_dir): (docs_dir / "empty.md").write_text("", encoding="utf-8") root = scan_docs(docs_dir) assert "empty.md" not in [d.name for d in root.docs] + + +def test_scan_sorts_when_listdir_order_is_unordered(monkeypatch, docs_dir): + """Same-index docs keep alphabetical order even when the OS enumerates + entries in a non-alphabetical order, so nav order is stable across platforms.""" + d = docs_dir / "nmbot-telegram" + for name in ["alpha.md", "mike.md", "zeta.md"]: + (d / name).write_text(f"---\ntitle: {name}\n---\n\n# {name}\n", encoding="utf-8") + + real_listdir = os.listdir + monkeypatch.setattr(scanner.os, "listdir", lambda p: sorted(real_listdir(p), reverse=True)) + + root = scan_docs(docs_dir) + nmbot = next(s for s in root.subdirs if s.rel_path == "nmbot-telegram") + assert [doc.name for doc in nmbot.docs] == ["alpha.md", "mcp.md", "mike.md", "zeta.md"] From d193652e7abf8ba18afd16c9dd2146b84023f144 Mon Sep 17 00:00:00 2001 From: Alice39s Date: Sun, 9 Aug 2026 18:50:13 +0900 Subject: [PATCH 23/57] refactor(cli): adopt Typer command routing --- pyproject.toml | 1 + src/nmteam_support/__main__.py | 6 +-- src/nmteam_support/cli.py | 71 +++++++++++++++++++++++----------- tests/test_cli.py | 30 ++++++++++++++ uv.lock | 48 +++++++++++++++++++++++ 5 files changed, 131 insertions(+), 25 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 0de5750..3fe5a4c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,6 +7,7 @@ dependencies = [ "mkdocs==1.6.1", "mkdocs-material==9.7.7", "pyyaml>=6.0.2", + "typer>=0.21.1", ] [project.scripts] diff --git a/src/nmteam_support/__main__.py b/src/nmteam_support/__main__.py index cc83b5d..24d073e 100644 --- a/src/nmteam_support/__main__.py +++ b/src/nmteam_support/__main__.py @@ -1,6 +1,6 @@ -"""Entry point: ``uv run python -m nmteam_support`` regenerates the site.""" +"""Module entry point for the nmTeam Support CLI.""" -from nmteam_support.generator import default_options, generate +from nmteam_support.cli import main if __name__ == "__main__": - generate(default_options()) + main() diff --git a/src/nmteam_support/cli.py b/src/nmteam_support/cli.py index c14eb25..16f9a42 100644 --- a/src/nmteam_support/cli.py +++ b/src/nmteam_support/cli.py @@ -1,16 +1,19 @@ -"""Command-line interface: generate / dev / build / clean / install.""" +"""Cross-platform command-line interface for the documentation toolchain.""" from __future__ import annotations -import argparse import shutil import subprocess import sys import threading from pathlib import Path +import typer + from nmteam_support.generator import GeneratorOptions, default_options, generate +app = typer.Typer(invoke_without_command=True) + def _rm(path: Path) -> None: if path.exists(): @@ -87,23 +90,47 @@ def cmd_install() -> int: return subprocess.call(["uv", "sync"]) -def main(argv: list[str] | None = None) -> int: - parser = argparse.ArgumentParser(prog="nmteam", description="nmTeam Support 文档站工具链") - parser.add_argument( - "command", - nargs="?", - default="generate", - choices=["generate", "dev", "build", "clean", "install"], - ) - args = parser.parse_args(argv) - options = default_options() - if args.command == "generate": - generate(options) - return 0 - if args.command == "dev": - return cmd_dev(options) - if args.command == "build": - return cmd_build(options) - if args.command == "clean": - return cmd_clean(options) - return cmd_install() +def _exit_on_error(code: int) -> None: + if code: + raise typer.Exit(code=code) + + +@app.callback() +def root(context: typer.Context) -> None: + """Manage the nmTeam Support documentation site.""" + if context.invoked_subcommand is None: + typer.echo(context.get_help()) + + +@app.command("generate") +def generate_command() -> None: + """Generate documentation configuration and derived files.""" + generate(default_options()) + + +@app.command("dev") +def dev_command() -> None: + """Serve the site and regenerate derived files after changes.""" + _exit_on_error(cmd_dev(default_options())) + + +@app.command("build") +def build_command() -> None: + """Generate and build the production documentation site.""" + _exit_on_error(cmd_build(default_options())) + + +@app.command("clean") +def clean_command() -> None: + """Remove generated documentation output.""" + _exit_on_error(cmd_clean(default_options())) + + +@app.command("install") +def install_command() -> None: + """Install project dependencies with uv.""" + _exit_on_error(cmd_install()) + + +def main() -> None: + app() diff --git a/tests/test_cli.py b/tests/test_cli.py index efb488f..0bb9f8b 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -2,10 +2,14 @@ from pathlib import Path +from typer.testing import CliRunner + import nmteam_support.cli as cli from nmteam_support.cli import cmd_clean from nmteam_support.generator import GeneratorOptions +runner = CliRunner() + def _options(tmp_path: Path) -> GeneratorOptions: return GeneratorOptions( @@ -29,6 +33,32 @@ def test_cmd_clean_removes_output_dirs(tmp_path): assert not (tmp_path / "site").exists() +def test_cli_without_command_displays_help(): + assert hasattr(cli, "app") + result = runner.invoke(cli.app) + assert result.exit_code == 0 + assert "Usage:" in result.stdout + assert "generate" in result.stdout + + +def test_generate_command_calls_generator(monkeypatch): + calls = [] + monkeypatch.setattr(cli, "generate", lambda options: calls.append(options)) + + result = runner.invoke(cli.app, ["generate"]) + + assert result.exit_code == 0 + assert len(calls) == 1 + + +def test_build_command_propagates_failure(monkeypatch): + monkeypatch.setattr(cli, "cmd_build", lambda options: 17) + + result = runner.invoke(cli.app, ["build"]) + + assert result.exit_code == 17 + + def test_watcher_continues_after_generation_error(tmp_path, monkeypatch, capsys): opts = _options(tmp_path) snapshots = iter([(), ((1, 1),), ((2, 2),)]) diff --git a/uv.lock b/uv.lock index 464a4ef..4fdab5a 100644 --- a/uv.lock +++ b/uv.lock @@ -2,6 +2,15 @@ version = 1 revision = 3 requires-python = ">=3.14" +[[package]] +name = "annotated-doc" +version = "0.0.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5a/8e/38aa427ed5402449e226975b649c5dc73ccadfefeb95e6aecb8f8ea4b6b6/annotated_doc-0.0.5.tar.gz", hash = "sha256:c7e58ce09192557605d8bbd92836d7e1d520ac9580096042c0bfd197efacf1bb", size = 10758, upload-time = "2026-07-28T13:50:58.129Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3e/30/e900b21425a860e195f32e37657aa1f7c7f2b1bfb26f03ca209b90933c06/annotated_doc-0.0.5-py3-none-any.whl", hash = "sha256:117bac03a25ede5df5440e855b32d556049ca169ead221505badf432fed4b101", size = 5302, upload-time = "2026-07-28T13:50:57.239Z" }, +] + [[package]] name = "babel" version = "2.18.0" @@ -355,6 +364,7 @@ dependencies = [ { name = "mkdocs" }, { name = "mkdocs-material" }, { name = "pyyaml" }, + { name = "typer" }, ] [package.dev-dependencies] @@ -371,6 +381,7 @@ requires-dist = [ { name = "mkdocs", specifier = "==1.6.1" }, { name = "mkdocs-material", specifier = "==9.7.7" }, { name = "pyyaml", specifier = ">=6.0.2" }, + { name = "typer", specifier = ">=0.21.1" }, ] [package.metadata.requires-dev] @@ -530,6 +541,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/a0/f4/c67b0b3f1b9245e8d266f0f112c500d50e5b4e83cb6f3b71b6528104182a/requests-2.34.2-py3-none-any.whl", hash = "sha256:2a0d60c172f83ac6ab31e4554906c0f3b3588d37b5cb939b1c061f4907e278e0", size = 73075, upload-time = "2026-05-14T19:25:26.443Z" }, ] +[[package]] +name = "rich" +version = "15.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown-it-py" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c0/8f/0722ca900cc807c13a6a0c696dacf35430f72e0ec571c4275d2371fca3e9/rich-15.0.0.tar.gz", hash = "sha256:edd07a4824c6b40189fb7ac9bc4c52536e9780fbbfbddf6f1e2502c31b068c36", size = 230680, upload-time = "2026-04-12T08:24:00.75Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/82/3b/64d4899d73f91ba49a8c18a8ff3f0ea8f1c1d75481760df8c68ef5235bf5/rich-15.0.0-py3-none-any.whl", hash = "sha256:33bd4ef74232fb73fe9279a257718407f169c09b78a87ad3d296f548e27de0bb", size = 310654, upload-time = "2026-04-12T08:24:02.83Z" }, +] + [[package]] name = "ruamel-yaml" version = "0.19.1" @@ -564,6 +588,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/5b/6c/93e26c22c5f78ff87363e07da49c84955affbeb1098bd1936bf3b3f293bf/ruff-0.16.2-py3-none-win_arm64.whl", hash = "sha256:d614e95cedf38a2053fd351c55b103ba30d017d61688fdbfd40ee0412852a99f", size = 11374065, upload-time = "2026-08-07T13:30:58.775Z" }, ] +[[package]] +name = "shellingham" +version = "1.5.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/58/15/8b3609fd3830ef7b27b655beb4b4e9c62313a4e8da8c676e142cc210d58e/shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de", size = 10310, upload-time = "2023-10-24T04:13:40.426Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686", size = 9755, upload-time = "2023-10-24T04:13:38.866Z" }, +] + [[package]] name = "six" version = "1.17.0" @@ -582,6 +615,21 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/44/6f/7120676b6d73228c96e17f1f794d8ab046fc910d781c8d151120c3f1569e/toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b", size = 16588, upload-time = "2020-11-01T01:40:20.672Z" }, ] +[[package]] +name = "typer" +version = "0.27.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-doc" }, + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "rich" }, + { name = "shellingham" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ae/40/4a3db7990d1f62a53182aa96eaef57aeb2886a27f90a195bc66713565d31/typer-0.27.1.tar.gz", hash = "sha256:a79bef8469a79c45498e7b814ecf8d603cc7644e9acbd9e19cac0334240b18df", size = 203994, upload-time = "2026-08-03T14:41:03.438Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/43/89/9518bc0c3929bee36b3a4a8e3daddd6e03f92f9961c66d4983b837160543/typer-0.27.1-py3-none-any.whl", hash = "sha256:53150287edd11baeb4e4722c8e394fcdf8181c0ae89485cba8d25c778d5edd56", size = 122874, upload-time = "2026-08-03T14:41:04.391Z" }, +] + [[package]] name = "urllib3" version = "2.7.0" From a865062b8eaf2b2288f772575222d7c76e8709a9 Mon Sep 17 00:00:00 2001 From: Alice39s Date: Sun, 9 Aug 2026 18:52:11 +0900 Subject: [PATCH 24/57] feat(cli): integrate redirect management --- redirects_manager.py | 94 --------------------------------- src/nmteam_support/cli.py | 48 +++++++++++++++++ src/nmteam_support/redirects.py | 33 ++++++++++-- tests/test_cli.py | 57 ++++++++++++++++++++ tests/test_redirects.py | 38 +++++++++++++ 5 files changed, 173 insertions(+), 97 deletions(-) delete mode 100644 redirects_manager.py diff --git a/redirects_manager.py b/redirects_manager.py deleted file mode 100644 index 295dc51..0000000 --- a/redirects_manager.py +++ /dev/null @@ -1,94 +0,0 @@ -#!/usr/bin/env python3 -""" -简单的重定向管理工具 -用法: - python redirects_manager.py add "/old-path/" "/new-path/" - python redirects_manager.py remove "/old-path/" - python redirects_manager.py list -""" - -import json -import os -import sys - -REDIRECTS_FILE = "redirects.json" - - -def load_redirects(): - """加载重定向配置""" - if not os.path.exists(REDIRECTS_FILE): - return {"redirects": {}} - - try: - with open(REDIRECTS_FILE, encoding="utf-8") as f: - return json.load(f) - except OSError, ValueError: - return {"redirects": {}} - - -def save_redirects(config): - """保存重定向配置""" - with open(REDIRECTS_FILE, "w", encoding="utf-8") as f: - json.dump(config, f, ensure_ascii=False, indent=2) - - -def add_redirect(old_path, new_path): - """添加重定向""" - config = load_redirects() - config["redirects"][old_path] = new_path - save_redirects(config) - print(f"已添加重定向: {old_path} -> {new_path}") - - -def remove_redirect(old_path): - """删除重定向""" - config = load_redirects() - if old_path in config["redirects"]: - del config["redirects"][old_path] - save_redirects(config) - print(f"已删除重定向: {old_path}") - else: - print(f"未找到重定向: {old_path}") - - -def list_redirects(): - """列出所有重定向""" - config = load_redirects() - if not config["redirects"]: - print("当前没有配置任何重定向") - return - - print("当前重定向规则:") - for old_path, new_path in config["redirects"].items(): - print(f" {old_path} -> {new_path}") - - -def main(): - if len(sys.argv) < 2: - print(__doc__) - return - - command = sys.argv[1] - - if command == "add": - if len(sys.argv) != 4: - print('用法: python redirects_manager.py add "/old-path/" "/new-path/"') - return - add_redirect(sys.argv[2], sys.argv[3]) - - elif command == "remove": - if len(sys.argv) != 3: - print('用法: python redirects_manager.py remove "/old-path/"') - return - remove_redirect(sys.argv[2]) - - elif command == "list": - list_redirects() - - else: - print(f"未知命令: {command}") - print(__doc__) - - -if __name__ == "__main__": - main() diff --git a/src/nmteam_support/cli.py b/src/nmteam_support/cli.py index 16f9a42..149221a 100644 --- a/src/nmteam_support/cli.py +++ b/src/nmteam_support/cli.py @@ -11,8 +11,15 @@ import typer from nmteam_support.generator import GeneratorOptions, default_options, generate +from nmteam_support.redirects import ( + RedirectConfigError, + read_redirects, + write_redirects, +) app = typer.Typer(invoke_without_command=True) +redirects_app = typer.Typer(no_args_is_help=True) +app.add_typer(redirects_app, name="redirects") def _rm(path: Path) -> None: @@ -95,6 +102,15 @@ def _exit_on_error(code: int) -> None: raise typer.Exit(code=code) +def _managed_redirects() -> tuple[Path, dict[str, str]]: + path = default_options().redirects_path + try: + return path, read_redirects(path) + except RedirectConfigError as error: + typer.echo(str(error), err=True) + raise typer.Exit(code=1) from error + + @app.callback() def root(context: typer.Context) -> None: """Manage the nmTeam Support documentation site.""" @@ -132,5 +148,37 @@ def install_command() -> None: _exit_on_error(cmd_install()) +@redirects_app.command("list") +def list_redirects_command() -> None: + """List configured redirects.""" + _, redirects = _managed_redirects() + if not redirects: + typer.echo("当前没有配置任何重定向") + return + for old_path, new_path in redirects.items(): + typer.echo(f"{old_path} -> {new_path}") + + +@redirects_app.command("add") +def add_redirect_command(old_path: str, new_path: str) -> None: + """Add or replace a redirect.""" + path, redirects = _managed_redirects() + redirects[old_path] = new_path + write_redirects(path, redirects) + typer.echo(f"已添加重定向: {old_path} -> {new_path}") + + +@redirects_app.command("remove") +def remove_redirect_command(old_path: str) -> None: + """Remove an existing redirect.""" + path, redirects = _managed_redirects() + if old_path not in redirects: + typer.echo(f"未找到重定向: {old_path}", err=True) + raise typer.Exit(code=1) + del redirects[old_path] + write_redirects(path, redirects) + typer.echo(f"已删除重定向: {old_path}") + + def main() -> None: app() diff --git a/src/nmteam_support/redirects.py b/src/nmteam_support/redirects.py index 85ee12e..0f91f17 100644 --- a/src/nmteam_support/redirects.py +++ b/src/nmteam_support/redirects.py @@ -6,12 +6,39 @@ from pathlib import Path +class RedirectConfigError(ValueError): + """Raised when redirects.json cannot be safely managed.""" + + +def read_redirects(path: Path) -> dict[str, str]: + """Read and validate redirects for management commands.""" + if not path.exists(): + return {} + try: + payload = json.loads(path.read_text(encoding="utf-8")) + redirects = payload["redirects"] + except (json.JSONDecodeError, KeyError, OSError, TypeError) as error: + raise RedirectConfigError(f"无法读取重定向配置: {error}") from error + if not isinstance(redirects, dict) or not all( + isinstance(old, str) and isinstance(new, str) for old, new in redirects.items() + ): + raise RedirectConfigError("redirects 必须是字符串到字符串的映射") + return redirects + + +def write_redirects(path: Path, redirects: dict[str, str]) -> None: + """Write a validated redirect map with stable UTF-8 formatting.""" + payload = json.dumps({"redirects": redirects}, ensure_ascii=False, indent=2) + path.write_text(payload + "\n", encoding="utf-8") + + def load_redirects(path: Path) -> dict[str, str] | None: """Load the redirect map; return None when the file is missing or invalid.""" + if not path.is_file(): + return None try: - with path.open(encoding="utf-8") as f: - return json.load(f)["redirects"] - except json.JSONDecodeError, KeyError, OSError: + return read_redirects(path) + except RedirectConfigError: return None diff --git a/tests/test_cli.py b/tests/test_cli.py index 0bb9f8b..a3f64c9 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -1,5 +1,6 @@ """CLI command tests.""" +import json from pathlib import Path from typer.testing import CliRunner @@ -59,6 +60,62 @@ def test_build_command_propagates_failure(monkeypatch): assert result.exit_code == 17 +def test_redirects_add_persists_quoted_paths(tmp_path, monkeypatch): + monkeypatch.chdir(tmp_path) + + result = runner.invoke(cli.app, ["redirects", "add", "/old path/", "/new path/"]) + + assert result.exit_code == 0 + payload = json.loads((tmp_path / "redirects.json").read_text(encoding="utf-8")) + assert payload == {"redirects": {"/old path/": "/new path/"}} + + +def test_redirects_list_displays_existing_rules(tmp_path, monkeypatch): + monkeypatch.chdir(tmp_path) + (tmp_path / "redirects.json").write_text( + json.dumps({"redirects": {"/old/": "/new/"}}), encoding="utf-8" + ) + + result = runner.invoke(cli.app, ["redirects", "list"]) + + assert result.exit_code == 0 + assert "/old/ -> /new/" in result.stdout + + +def test_redirects_remove_deletes_existing_rule(tmp_path, monkeypatch): + monkeypatch.chdir(tmp_path) + (tmp_path / "redirects.json").write_text( + json.dumps({"redirects": {"/old/": "/new/"}}), encoding="utf-8" + ) + + result = runner.invoke(cli.app, ["redirects", "remove", "/old/"]) + + assert result.exit_code == 0 + payload = json.loads((tmp_path / "redirects.json").read_text(encoding="utf-8")) + assert payload == {"redirects": {}} + + +def test_redirects_remove_reports_missing_rule(tmp_path, monkeypatch): + monkeypatch.chdir(tmp_path) + + result = runner.invoke(cli.app, ["redirects", "remove", "/missing/"]) + + assert result.exit_code == 1 + assert "未找到重定向" in result.stderr + + +def test_redirects_add_preserves_malformed_config(tmp_path, monkeypatch): + monkeypatch.chdir(tmp_path) + path = tmp_path / "redirects.json" + path.write_text("{ invalid", encoding="utf-8") + + result = runner.invoke(cli.app, ["redirects", "add", "/old/", "/new/"]) + + assert result.exit_code == 1 + assert "无法读取重定向配置" in result.stderr + assert path.read_text(encoding="utf-8") == "{ invalid" + + def test_watcher_continues_after_generation_error(tmp_path, monkeypatch, capsys): opts = _options(tmp_path) snapshots = iter([(), ((1, 1),), ((2, 2),)]) diff --git a/tests/test_redirects.py b/tests/test_redirects.py index 5116cff..60aa63a 100644 --- a/tests/test_redirects.py +++ b/tests/test_redirects.py @@ -2,6 +2,9 @@ import json +import pytest + +import nmteam_support.redirects as redirects_module from nmteam_support.redirects import load_redirects, render_redirects_js @@ -37,3 +40,38 @@ def test_load_redirects_missing_key(tmp_path): p = tmp_path / "redirects.json" p.write_text(json.dumps({"foo": 1}), encoding="utf-8") assert load_redirects(p) is None + + +def test_read_redirects_rejects_invalid_json_without_overwriting(tmp_path): + assert hasattr(redirects_module, "read_redirects") + path = tmp_path / "redirects.json" + original = "{ invalid" + path.write_text(original, encoding="utf-8") + + with pytest.raises(redirects_module.RedirectConfigError): + redirects_module.read_redirects(path) + + assert path.read_text(encoding="utf-8") == original + + +def test_read_redirects_missing_file_returns_empty_map(tmp_path): + assert hasattr(redirects_module, "read_redirects") + assert redirects_module.read_redirects(tmp_path / "redirects.json") == {} + + +def test_read_redirects_rejects_non_string_mapping(tmp_path): + assert hasattr(redirects_module, "read_redirects") + path = tmp_path / "redirects.json" + path.write_text(json.dumps({"redirects": {"/old/": 3}}), encoding="utf-8") + + with pytest.raises(redirects_module.RedirectConfigError): + redirects_module.read_redirects(path) + + +def test_write_redirects_round_trips_unicode(tmp_path): + assert hasattr(redirects_module, "write_redirects") + path = tmp_path / "redirects.json" + + redirects_module.write_redirects(path, {"/旧/": "/新/"}) + + assert redirects_module.read_redirects(path) == {"/旧/": "/新/"} From 956f981f3f8e0e931037fbb19725a4a444fa4f15 Mon Sep 17 00:00:00 2001 From: Alice39s Date: Sun, 9 Aug 2026 18:53:44 +0900 Subject: [PATCH 25/57] feat(cli): add unified quality checks --- src/nmteam_support/cli.py | 22 ++++++++++++++++++++ tests/test_cli.py | 44 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 66 insertions(+) diff --git a/src/nmteam_support/cli.py b/src/nmteam_support/cli.py index 149221a..10299d0 100644 --- a/src/nmteam_support/cli.py +++ b/src/nmteam_support/cli.py @@ -21,6 +21,13 @@ redirects_app = typer.Typer(no_args_is_help=True) app.add_typer(redirects_app, name="redirects") +QUALITY_COMMANDS = ( + ["ruff", "check", "."], + ["ruff", "format", "--check", "."], + ["pytest"], + ["mdformat", "--check", "README.md", "docs/"], +) + def _rm(path: Path) -> None: if path.exists(): @@ -97,6 +104,15 @@ def cmd_install() -> int: return subprocess.call(["uv", "sync"]) +def cmd_check(options: GeneratorOptions) -> int: + """Run repository quality checks and a strict documentation build.""" + for command in QUALITY_COMMANDS: + code = subprocess.call(command) + if code: + return code + return cmd_build(options) + + def _exit_on_error(code: int) -> None: if code: raise typer.Exit(code=code) @@ -148,6 +164,12 @@ def install_command() -> None: _exit_on_error(cmd_install()) +@app.command("check") +def check_command() -> None: + """Run linting, tests, formatting checks, and a strict build.""" + _exit_on_error(cmd_check(default_options())) + + @redirects_app.command("list") def list_redirects_command() -> None: """List configured redirects.""" diff --git a/tests/test_cli.py b/tests/test_cli.py index a3f64c9..6a56693 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -3,6 +3,7 @@ import json from pathlib import Path +import pytest from typer.testing import CliRunner import nmteam_support.cli as cli @@ -116,6 +117,49 @@ def test_redirects_add_preserves_malformed_config(tmp_path, monkeypatch): assert path.read_text(encoding="utf-8") == "{ invalid" +def test_check_runs_complete_pipeline_in_order(tmp_path, monkeypatch): + options = _options(tmp_path) + commands = [] + build_options = [] + monkeypatch.setattr(cli.subprocess, "call", lambda command: commands.append(command) or 0) + monkeypatch.setattr(cli, "cmd_build", lambda received: build_options.append(received) or 0) + + assert cli.cmd_check(options) == 0 + assert commands == [ + ["ruff", "check", "."], + ["ruff", "format", "--check", "."], + ["pytest"], + ["mdformat", "--check", "README.md", "docs/"], + ] + assert build_options == [options] + + +def test_check_stops_after_first_failed_command(tmp_path, monkeypatch): + calls = [] + + def run(command): + calls.append(command) + return 9 if command[:2] == ["ruff", "format"] else 0 + + monkeypatch.setattr(cli.subprocess, "call", run) + monkeypatch.setattr( + cli, + "cmd_build", + lambda options: pytest.fail(f"build must not run: {options}"), + ) + + assert cli.cmd_check(_options(tmp_path)) == 9 + assert calls == [["ruff", "check", "."], ["ruff", "format", "--check", "."]] + + +def test_check_command_propagates_failure(monkeypatch): + monkeypatch.setattr(cli, "cmd_check", lambda options: 13) + + result = runner.invoke(cli.app, ["check"]) + + assert result.exit_code == 13 + + def test_watcher_continues_after_generation_error(tmp_path, monkeypatch, capsys): opts = _options(tmp_path) snapshots = iter([(), ((1, 1),), ((2, 2),)]) From 2afeca5b55bcf88a2768ceb80c479238d09a49c3 Mon Sep 17 00:00:00 2001 From: Alice39s Date: Sun, 9 Aug 2026 18:54:53 +0900 Subject: [PATCH 26/57] refactor(scripts): centralize platform launchers --- dev.bat | 20 ----------------- dev.ps1 | 37 ------------------------------- dev.sh | 20 ----------------- manage.py | 16 -------------- scripts/nmteam.bat | 5 +++++ scripts/nmteam.ps1 | 14 ++++++++++++ scripts/nmteam.sh | 6 +++++ tests/test_scripts.py | 51 +++++++++++++++++++++++++++++++++++++++++++ 8 files changed, 76 insertions(+), 93 deletions(-) delete mode 100644 dev.bat delete mode 100644 dev.ps1 delete mode 100644 dev.sh delete mode 100755 manage.py create mode 100644 scripts/nmteam.bat create mode 100644 scripts/nmteam.ps1 create mode 100755 scripts/nmteam.sh create mode 100644 tests/test_scripts.py diff --git a/dev.bat b/dev.bat deleted file mode 100644 index 75d1599..0000000 --- a/dev.bat +++ /dev/null @@ -1,20 +0,0 @@ -@echo off -REM nmTeam Documentation Development Script -REM Windows 批处理脚本 - -echo nmTeam Documentation Manager -echo. - -if "%1"=="" ( - echo 用法: dev.bat [command] - echo. - echo 可用命令: - echo dev 启动开发模式 - echo build 构建静态站点 - echo clean 清理生成文件 - echo install 安装依赖 - echo. - goto :eof -) - -uv run python manage.py %* diff --git a/dev.ps1 b/dev.ps1 deleted file mode 100644 index 4e7432c..0000000 --- a/dev.ps1 +++ /dev/null @@ -1,37 +0,0 @@ -# nmTeam Documentation Management PowerShell Script - -param( - [Parameter(Position=0)] - [string]$Command = "", - [Parameter(ValueFromRemainingArguments=$true)] - [string[]]$RemainingArgs -) - -# Enable UTF-8 encoding (must come after the param() block, which PowerShell -# requires to be the first executable statement of the script) -[Console]::OutputEncoding = [System.Text.Encoding]::UTF8 -$OutputEncoding = [System.Text.Encoding]::UTF8 - -Write-Host "nmTeam Documentation Manager" -ForegroundColor Cyan -Write-Host "" - -if ($Command -eq "" -or $Command -eq "help") { - Write-Host "用法: .\dev.ps1 " -ForegroundColor Yellow - Write-Host "" - Write-Host "可用命令:" -ForegroundColor Green - Write-Host " dev 启动开发模式 (文件监听 + 热更新)" -ForegroundColor White - Write-Host " build 构建静态站点 (生成文档 + 构建)" -ForegroundColor White - Write-Host " clean 清理生成的文件" -ForegroundColor White - Write-Host " install 安装依赖包" -ForegroundColor White - Write-Host " help 显示帮助信息" -ForegroundColor White - Write-Host "" - Write-Host "示例:" -ForegroundColor Yellow - Write-Host " .\dev.ps1 dev # 启动开发服务器" -ForegroundColor Gray - Write-Host " .\dev.ps1 build # 构建生产版本" -ForegroundColor Gray - Write-Host " .\dev.ps1 clean # 清理生成的文件" -ForegroundColor Gray - exit 0 -} - -# 执行 Python 脚本 -$arguments = @($Command) + $RemainingArgs -& uv run python manage.py $arguments diff --git a/dev.sh b/dev.sh deleted file mode 100644 index b0742fe..0000000 --- a/dev.sh +++ /dev/null @@ -1,20 +0,0 @@ -#!/bin/bash -# nmTeam Documentation Development Script -# Unix/Linux shell script - -echo "nmTeam Documentation Manager" -echo - -if [ $# -eq 0 ]; then - echo "用法: ./dev.sh [command]" - echo - echo "可用命令:" - echo " dev 启动开发模式" - echo " build 构建静态站点" - echo " clean 清理生成文件" - echo " install 安装依赖" - echo - exit 0 -fi - -uv run python manage.py "$@" diff --git a/manage.py b/manage.py deleted file mode 100755 index 64cd570..0000000 --- a/manage.py +++ /dev/null @@ -1,16 +0,0 @@ -#!/usr/bin/env python -# manage.py -"""Backward-compatible CLI wrapper (delegates to the nmteam package). - -Kept so ``dev.sh`` / ``dev.ps1`` / ``dev.bat`` and muscle memory for -``python manage.py `` keep working. Run it through uv: - - uv run python manage.py dev -""" - -import sys - -from nmteam_support.cli import main - -if __name__ == "__main__": - sys.exit(main()) diff --git a/scripts/nmteam.bat b/scripts/nmteam.bat new file mode 100644 index 0000000..8f4f9a7 --- /dev/null +++ b/scripts/nmteam.bat @@ -0,0 +1,5 @@ +@echo off +setlocal +cd /d "%~dp0.." +uv run nmteam %* +exit /b %errorlevel% diff --git a/scripts/nmteam.ps1 b/scripts/nmteam.ps1 new file mode 100644 index 0000000..e8d1354 --- /dev/null +++ b/scripts/nmteam.ps1 @@ -0,0 +1,14 @@ +param( + [Parameter(ValueFromRemainingArguments = $true)] + [string[]]$Arguments +) + +$repoRoot = Split-Path -Parent $PSScriptRoot +Push-Location $repoRoot +try { + & uv run nmteam @Arguments + exit $LASTEXITCODE +} +finally { + Pop-Location +} diff --git a/scripts/nmteam.sh b/scripts/nmteam.sh new file mode 100755 index 0000000..b6b17de --- /dev/null +++ b/scripts/nmteam.sh @@ -0,0 +1,6 @@ +#!/usr/bin/env sh +set -eu + +repo_root=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd) +cd "$repo_root" +exec uv run nmteam "$@" diff --git a/tests/test_scripts.py b/tests/test_scripts.py new file mode 100644 index 0000000..ab833d8 --- /dev/null +++ b/tests/test_scripts.py @@ -0,0 +1,51 @@ +"""Cross-platform launcher behavior tests.""" + +import os +import subprocess +from pathlib import Path + + +def test_shell_launcher_forwards_arguments_from_repository_root(tmp_path): + repository_root = Path(__file__).parents[1] + capture_dir = tmp_path / "capture" + capture_dir.mkdir() + executable_dir = tmp_path / "bin" + executable_dir.mkdir() + fake_uv = executable_dir / "uv" + fake_uv.write_text( + "#!/bin/sh\n" + 'printf "%s\\n" "$PWD" > "$CAPTURE_DIR/cwd"\n' + 'printf "%s\\n" "$@" > "$CAPTURE_DIR/args"\n' + "exit 23\n", + encoding="utf-8", + ) + fake_uv.chmod(0o755) + environment = os.environ | { + "CAPTURE_DIR": str(capture_dir), + "PATH": f"{executable_dir}{os.pathsep}{os.environ['PATH']}", + } + + result = subprocess.run( + [ + "sh", + str(repository_root / "scripts" / "nmteam.sh"), + "redirects", + "add", + "/old path/", + "/new path/", + ], + cwd=tmp_path, + env=environment, + check=False, + ) + + assert result.returncode == 23 + assert (capture_dir / "cwd").read_text(encoding="utf-8").strip() == str(repository_root) + assert (capture_dir / "args").read_text(encoding="utf-8").splitlines() == [ + "run", + "nmteam", + "redirects", + "add", + "/old path/", + "/new path/", + ] From 26bdc9e34ad192adb6fd5371cbd6b270cc679c88 Mon Sep 17 00:00:00 2001 From: Alice39s Date: Sun, 9 Aug 2026 18:56:10 +0900 Subject: [PATCH 27/57] fix(cli): ignore local design files in checks --- src/nmteam_support/cli.py | 9 ++++++++- tests/test_cli.py | 9 ++++++++- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/src/nmteam_support/cli.py b/src/nmteam_support/cli.py index 10299d0..3738157 100644 --- a/src/nmteam_support/cli.py +++ b/src/nmteam_support/cli.py @@ -25,7 +25,14 @@ ["ruff", "check", "."], ["ruff", "format", "--check", "."], ["pytest"], - ["mdformat", "--check", "README.md", "docs/"], + [ + "mdformat", + "--check", + "--exclude", + "docs/superpowers/**", + "README.md", + "docs/", + ], ) diff --git a/tests/test_cli.py b/tests/test_cli.py index 6a56693..e774f13 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -129,7 +129,14 @@ def test_check_runs_complete_pipeline_in_order(tmp_path, monkeypatch): ["ruff", "check", "."], ["ruff", "format", "--check", "."], ["pytest"], - ["mdformat", "--check", "README.md", "docs/"], + [ + "mdformat", + "--check", + "--exclude", + "docs/superpowers/**", + "README.md", + "docs/", + ], ] assert build_options == [options] From b3d382846d4dc7c3de97e4ff755660e3910d70c7 Mon Sep 17 00:00:00 2001 From: Alice39s Date: Sun, 9 Aug 2026 18:56:58 +0900 Subject: [PATCH 28/57] docs(tooling): document the cross-platform CLI --- .github/workflows/ci.yml | 19 +-------- .gitignore | 5 ++- README.md | 85 ++++++++++++++++++++++++++++++++-------- 3 files changed, 74 insertions(+), 35 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4fd3f04..8462df2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -22,20 +22,5 @@ jobs: - name: Sync dependencies (frozen) run: uv sync --frozen - - name: Ruff lint - run: uv run ruff check . - - - name: Ruff format check - run: uv run ruff format --check . - - - name: Tests - run: uv run pytest - - - name: Markdown format check - run: uv run mdformat --check docs/ - - - name: Generate documentation - run: uv run python -m nmteam_support - - - name: Build site (strict) - run: uv run mkdocs build --strict + - name: Run quality checks + run: uv run nmteam check diff --git a/.gitignore b/.gitignore index 4d1c76b..0fd4eae 100644 --- a/.gitignore +++ b/.gitignore @@ -131,4 +131,7 @@ dmypy.json /mkdocs.yml # uv / ruff -.ruff_cache/ \ No newline at end of file +.ruff_cache/ + +# Local design and implementation artifacts +/docs/superpowers/ \ No newline at end of file diff --git a/README.md b/README.md index 102860f..61f22c5 100644 --- a/README.md +++ b/README.md @@ -1,55 +1,106 @@ # Support -[**nmTeam 支持**](https://support.nmteam.xyz)官方网站。使用 `mkdocs-material` 构建,工具链由 `uv` 管理。 + +[**nmTeam 支持**](https://support.nmteam.xyz)官方网站。使用 `mkdocs-material` 构建;项目由 `uv` 初始化并管理依赖,通过 Typer 提供统一的跨平台命令行入口。 ## 环境要求 + - Python 3.14+(由 `uv` 按 `.python-version` 自动管理) - [uv](https://docs.astral.sh/uv/)(`curl -LsSf https://astral.sh/uv/install.sh | sh`) ## 快速开始 ### 安装依赖 + ```bash uv sync ``` -### 开发模式(推荐) +也可以使用统一 CLI: + +```bash +uv run nmteam install +``` + +### 开发模式 + ```bash -uv run python manage.py dev +uv run nmteam dev ``` + 开发模式会: + - 自动生成文档结构(`cache/`、`generated/`、`mkdocs.yml`) -- 启动 MkDocs 开发服务器(http://127.0.0.1:8000) +- 启动 MkDocs 开发服务器() - 监听 `docs/` 目录变化并自动重新生成,浏览器热更新 ### 构建生产版本 + ```bash -uv run python manage.py build +uv run nmteam build ``` + 构建结果输出到 `site/` 目录。 ### 其他命令 + ```bash -uv run python manage.py clean # 清理 cache/ generated/ site/ -uv run python manage.py install # 等价于 uv sync -uv run python -m nmteam_support # 仅重新生成文档结构 +uv run nmteam generate # 仅重新生成文档结构 +uv run nmteam clean # 清理 cache/、generated/ 和 site/ +uv run nmteam --help # 显示完整命令帮助 ``` +## 平台启动器 + +直接运行 `uv run nmteam` 是推荐方式。`scripts/` 也提供不包含业务逻辑的薄启动器;它们会自动定位仓库根目录并原样传递参数。 + +=== "Linux / macOS" + + ```bash + scripts/nmteam.sh dev + ``` + +=== "PowerShell" + + ```powershell + .\scripts\nmteam.ps1 dev + ``` + +=== "Windows Batch" + + ```batch + scripts\nmteam.bat dev + ``` + ## 质量检查 + +一条命令运行 Ruff lint、Ruff format check、pytest、mdformat 和 MkDocs strict build: + ```bash -uv run ruff check . # lint -uv run ruff format --check . # Python 格式检查 -uv run pytest # 单元测试 -uv run mdformat --check docs/ # Markdown 格式检查 -uv run mkdocs build --strict # 严格模式构建 +uv run nmteam check ``` -以上步骤由 CI(`.github/workflows/ci.yml`)自动执行。 + +也可以单独运行: + +```bash +uv run ruff check . +uv run ruff format --check . +uv run pytest +uv run mdformat --check README.md docs/ +uv run nmteam build +``` + +以上检查由 CI(`.github/workflows/ci.yml`)自动执行。 ## 重定向管理 + ```bash -uv run python redirects_manager.py list -uv run python redirects_manager.py add "/old-path/" "/new-path/" -uv run python redirects_manager.py remove "/old-path/" +uv run nmteam redirects list +uv run nmteam redirects add "/old-path/" "/new-path/" +uv run nmteam redirects remove "/old-path/" ``` +管理命令不会用空配置覆盖损坏的 `redirects.json`;修复配置后再重试即可。 + ## 贡献 + 欢迎您在 GitHub 上提出问题并贡献文档。 From c88a2997e0908d57268fd222829e8bde04ef2779 Mon Sep 17 00:00:00 2001 From: Alice39s Date: Sun, 9 Aug 2026 19:01:52 +0900 Subject: [PATCH 29/57] ci(actions): verify launchers on all platforms --- .github/workflows/ci.yml | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8462df2..e089a38 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -8,9 +8,15 @@ on: jobs: ci: - runs-on: ubuntu-latest + name: ${{ matrix.os }} + runs-on: ${{ matrix.os }} timeout-minutes: 15 permissions: {} + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, macos-latest, windows-latest] + steps: - uses: actions/checkout@v5 @@ -24,3 +30,17 @@ jobs: - name: Run quality checks run: uv run nmteam check + + - name: Verify POSIX launcher + if: runner.os != 'Windows' + run: scripts/nmteam.sh --help + + - name: Verify PowerShell launcher + if: runner.os == 'Windows' + shell: pwsh + run: .\scripts\nmteam.ps1 --help + + - name: Verify Batch launcher + if: runner.os == 'Windows' + shell: cmd + run: scripts\nmteam.bat --help From 34288d2f45b453c80cd1cc21144b713b4aea49bd Mon Sep 17 00:00:00 2001 From: Alice39s Date: Sun, 9 Aug 2026 19:04:11 +0900 Subject: [PATCH 30/57] test(scripts): compare launcher roots portably --- tests/test_scripts.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/test_scripts.py b/tests/test_scripts.py index ab833d8..76782b9 100644 --- a/tests/test_scripts.py +++ b/tests/test_scripts.py @@ -14,7 +14,8 @@ def test_shell_launcher_forwards_arguments_from_repository_root(tmp_path): fake_uv = executable_dir / "uv" fake_uv.write_text( "#!/bin/sh\n" - 'printf "%s\\n" "$PWD" > "$CAPTURE_DIR/cwd"\n' + "if [ -f pyproject.toml ]; then root_state=root; else root_state=wrong; fi\n" + 'printf "%s\\n" "$root_state" > "$CAPTURE_DIR/cwd"\n' 'printf "%s\\n" "$@" > "$CAPTURE_DIR/args"\n' "exit 23\n", encoding="utf-8", @@ -40,7 +41,7 @@ def test_shell_launcher_forwards_arguments_from_repository_root(tmp_path): ) assert result.returncode == 23 - assert (capture_dir / "cwd").read_text(encoding="utf-8").strip() == str(repository_root) + assert (capture_dir / "cwd").read_text(encoding="utf-8").strip() == "root" assert (capture_dir / "args").read_text(encoding="utf-8").splitlines() == [ "run", "nmteam", From 5b5cfd5bc19b1c1539be756281248cedccb18f32 Mon Sep 17 00:00:00 2001 From: Alice39s Date: Sun, 9 Aug 2026 19:05:51 +0900 Subject: [PATCH 31/57] fix(tooling): pin Markdown line endings --- .gitattributes | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 .gitattributes diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..5cd4978 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +# Keep formatter-sensitive Markdown stable across platforms. +*.md text eol=lf From 27dd874aa49d397174bb10897650bce7c5f067c1 Mon Sep 17 00:00:00 2001 From: Alice39s Date: Sun, 9 Aug 2026 19:33:02 +0900 Subject: [PATCH 32/57] refactor(assets): centralize documentation resources --- {docs/img => assets}/icons/doc.svg | 0 {docs/img => assets}/icons/folder.svg | 0 .../images/nmbot}/add_as_admin_step_1.png | Bin .../images/nmbot}/add_as_admin_step_2.png | Bin .../images/nmbot}/add_to_admin_step_1.png | Bin .../images/nmbot}/add_to_admin_step_2.png | Bin .../images/nmbot}/add_to_admin_step_3.png | Bin .../images/nmbot}/add_to_admin_step_4.png | Bin .../images/nmbot}/add_to_admin_step_5.png | Bin .../img => assets/images/nmbot}/doc cover.png | Bin .../images/nmbot}/doc start intro.png | Bin .../images/nmbot}/doc start panel.png | Bin .../images/nmbot}/doc start telegram.png | Bin .../images/nmbot}/interaction_example.png | Bin .../images/nmbot}/interaction_example2.png | Bin .../images/nmbot}/interaction_example3.png | Bin .../images/nmbot}/interaction_example4.png | Bin .../nmbot}/launch_panel_from_any_chat.png | Bin .../nmbot}/launch_panel_from_chat_corner.png | Bin .../images/nmbot}/mcp/mcp-config-page.png | Bin .../nmbot}/mcp/mcp-create-token-dialog.png | Bin .../images/nmbot}/mcp/mcp-entry.png | Bin .../nmbot}/mcp/mcp-token-created-dialog.png | Bin .../nmbot}/mcp/mcp-vscode-command-palette.png | Bin .../nmbot}/mcp/mcp-vscode-user-config.png | Bin .../nmbot}/nmartchat_panel_enablechat.png | Bin .../images/nmbot}/nmartchat_regadmin.png | Bin .../images/nmbot}/reply-channel_eg.png | Bin .../nmbot}/update-pictures/nmBot plus.png | Bin .../update-pictures/nmbot-2312-christmas.jpg | Bin .../update-pictures/nmbot-2401-winter.jpg | Bin .../nmbot-2402-spring-festival.jpg | Bin .../nmbot}/update-pictures/nmbot-2408.png | Bin .../nmbot}/update-pictures/nmbot-2409.png | Bin .../nmbot}/update-pictures/nmbot-2410.png | Bin .../nmbot}/update-pictures/nmbot-2412.png | Bin .../nmbot}/update-pictures/nmbot-2501.png | Bin .../nmbot}/update-pictures/nmbot-2502.png | Bin .../nmbot}/update-pictures/nmbot-2503.png | Bin .../nmbot}/update-pictures/nmbot-2504.png | Bin .../nmbot}/update-pictures/nmbot-2505.png | Bin .../nmbot}/update-pictures/nmbot-2506.png | Bin .../nmbot}/update-pictures/nmbot-2507.png | Bin .../nmbot}/update-pictures/nmbot-2508.png | Bin .../nmbot}/update-pictures/nmbot-2509.png | Bin .../nmbot}/update-pictures/nmbot-2510.png | Bin .../nmbot}/update-pictures/nmbot-2511.png | Bin .../nmbot}/update-pictures/nmbot-2512.png | Bin .../images/shared}/keyword-reply-edit.png | Bin .../images/shared}/keyword-reply-limit.png | Bin .../shared}/nmteam-support-background.jpg | Bin .../shared}/nmteam_account_register.png | Bin .../img => assets/images/shared}/spam faq.png | Bin {docs => assets/styles}/docsList.css | 4 +- {docs => assets/styles}/plus.css | 0 docs/index.md | 2 +- docs/nmbot-telegram/faq/spam.md | 2 +- docs/nmbot-telegram/group/group-setup.md | 14 +-- docs/nmbot-telegram/group/key-word-reply.md | 4 +- .../group/members-interaction.md | 8 +- docs/nmbot-telegram/group/reply-channel.md | 2 +- .../img/update-pictures/index.md | 8 -- docs/nmbot-telegram/index.md | 8 +- docs/nmbot-telegram/launch-panel.md | 4 +- docs/nmbot-telegram/mcp.md | 12 +-- docs/nmbot-telegram/nmartchat.md | 4 +- docs/nmbot-telegram/update-log/2023-12.md | 2 +- docs/nmbot-telegram/update-log/2024-01.md | 2 +- docs/nmbot-telegram/update-log/2024-02.md | 2 +- docs/nmbot-telegram/update-log/2024-08.md | 2 +- docs/nmbot-telegram/update-log/2024-09.md | 2 +- docs/nmbot-telegram/update-log/2024-10.md | 2 +- docs/nmbot-telegram/update-log/2024-12.md | 2 +- docs/nmbot-telegram/update-log/2025-01.md | 2 +- docs/nmbot-telegram/update-log/2025-02.md | 2 +- docs/nmbot-telegram/update-log/2025-03.md | 2 +- docs/nmbot-telegram/update-log/2025-04.md | 2 +- docs/nmbot-telegram/update-log/2025-05.md | 2 +- docs/nmbot-telegram/update-log/2025-06.md | 2 +- docs/nmbot-telegram/update-log/2025-07.md | 2 +- docs/nmbot-telegram/update-log/2025-08.md | 2 +- docs/nmbot-telegram/update-log/2025-09.md | 2 +- docs/nmbot-telegram/update-log/2025-10.md | 2 +- docs/nmbot-telegram/update-log/2025-11.md | 2 +- docs/nmbot-telegram/update-log/2025-12.md | 2 +- docs/nmbot-telegram/update-log/nmbot-plus.md | 2 +- mkdocs-template.yml | 102 +++++++++--------- src/nmteam_support/cli.py | 2 +- src/nmteam_support/generator.py | 4 + tests/test_cli.py | 11 ++ tests/test_generator.py | 17 +++ 91 files changed, 136 insertions(+), 112 deletions(-) rename {docs/img => assets}/icons/doc.svg (100%) rename {docs/img => assets}/icons/folder.svg (100%) rename {docs/nmbot-telegram/img => assets/images/nmbot}/add_as_admin_step_1.png (100%) rename {docs/nmbot-telegram/img => assets/images/nmbot}/add_as_admin_step_2.png (100%) rename {docs/nmbot-telegram/img => assets/images/nmbot}/add_to_admin_step_1.png (100%) rename {docs/nmbot-telegram/img => assets/images/nmbot}/add_to_admin_step_2.png (100%) rename {docs/nmbot-telegram/img => assets/images/nmbot}/add_to_admin_step_3.png (100%) rename {docs/nmbot-telegram/img => assets/images/nmbot}/add_to_admin_step_4.png (100%) rename {docs/nmbot-telegram/img => assets/images/nmbot}/add_to_admin_step_5.png (100%) rename {docs/nmbot-telegram/img => assets/images/nmbot}/doc cover.png (100%) rename {docs/nmbot-telegram/img => assets/images/nmbot}/doc start intro.png (100%) rename {docs/nmbot-telegram/img => assets/images/nmbot}/doc start panel.png (100%) rename {docs/nmbot-telegram/img => assets/images/nmbot}/doc start telegram.png (100%) rename {docs/nmbot-telegram/img => assets/images/nmbot}/interaction_example.png (100%) rename {docs/nmbot-telegram/img => assets/images/nmbot}/interaction_example2.png (100%) rename {docs/nmbot-telegram/img => assets/images/nmbot}/interaction_example3.png (100%) rename {docs/nmbot-telegram/img => assets/images/nmbot}/interaction_example4.png (100%) rename {docs/nmbot-telegram/img => assets/images/nmbot}/launch_panel_from_any_chat.png (100%) rename {docs/nmbot-telegram/img => assets/images/nmbot}/launch_panel_from_chat_corner.png (100%) rename {docs/nmbot-telegram/img => assets/images/nmbot}/mcp/mcp-config-page.png (100%) rename {docs/nmbot-telegram/img => assets/images/nmbot}/mcp/mcp-create-token-dialog.png (100%) rename {docs/nmbot-telegram/img => assets/images/nmbot}/mcp/mcp-entry.png (100%) rename {docs/nmbot-telegram/img => assets/images/nmbot}/mcp/mcp-token-created-dialog.png (100%) rename {docs/nmbot-telegram/img => assets/images/nmbot}/mcp/mcp-vscode-command-palette.png (100%) rename {docs/nmbot-telegram/img => assets/images/nmbot}/mcp/mcp-vscode-user-config.png (100%) rename {docs/nmbot-telegram/img => assets/images/nmbot}/nmartchat_panel_enablechat.png (100%) rename {docs/nmbot-telegram/img => assets/images/nmbot}/nmartchat_regadmin.png (100%) rename {docs/nmbot-telegram/img => assets/images/nmbot}/reply-channel_eg.png (100%) rename {docs/nmbot-telegram/img => assets/images/nmbot}/update-pictures/nmBot plus.png (100%) rename {docs/nmbot-telegram/img => assets/images/nmbot}/update-pictures/nmbot-2312-christmas.jpg (100%) rename {docs/nmbot-telegram/img => assets/images/nmbot}/update-pictures/nmbot-2401-winter.jpg (100%) rename {docs/nmbot-telegram/img => assets/images/nmbot}/update-pictures/nmbot-2402-spring-festival.jpg (100%) rename {docs/nmbot-telegram/img => assets/images/nmbot}/update-pictures/nmbot-2408.png (100%) rename {docs/nmbot-telegram/img => assets/images/nmbot}/update-pictures/nmbot-2409.png (100%) rename {docs/nmbot-telegram/img => assets/images/nmbot}/update-pictures/nmbot-2410.png (100%) rename {docs/nmbot-telegram/img => assets/images/nmbot}/update-pictures/nmbot-2412.png (100%) rename {docs/nmbot-telegram/img => assets/images/nmbot}/update-pictures/nmbot-2501.png (100%) rename {docs/nmbot-telegram/img => assets/images/nmbot}/update-pictures/nmbot-2502.png (100%) rename {docs/nmbot-telegram/img => assets/images/nmbot}/update-pictures/nmbot-2503.png (100%) rename {docs/nmbot-telegram/img => assets/images/nmbot}/update-pictures/nmbot-2504.png (100%) rename {docs/nmbot-telegram/img => assets/images/nmbot}/update-pictures/nmbot-2505.png (100%) rename {docs/nmbot-telegram/img => assets/images/nmbot}/update-pictures/nmbot-2506.png (100%) rename {docs/nmbot-telegram/img => assets/images/nmbot}/update-pictures/nmbot-2507.png (100%) rename {docs/nmbot-telegram/img => assets/images/nmbot}/update-pictures/nmbot-2508.png (100%) rename {docs/nmbot-telegram/img => assets/images/nmbot}/update-pictures/nmbot-2509.png (100%) rename {docs/nmbot-telegram/img => assets/images/nmbot}/update-pictures/nmbot-2510.png (100%) rename {docs/nmbot-telegram/img => assets/images/nmbot}/update-pictures/nmbot-2511.png (100%) rename {docs/nmbot-telegram/img => assets/images/nmbot}/update-pictures/nmbot-2512.png (100%) rename {docs/img => assets/images/shared}/keyword-reply-edit.png (100%) rename {docs/img => assets/images/shared}/keyword-reply-limit.png (100%) rename {docs/img => assets/images/shared}/nmteam-support-background.jpg (100%) rename {docs/img => assets/images/shared}/nmteam_account_register.png (100%) rename {docs/img => assets/images/shared}/spam faq.png (100%) rename {docs => assets/styles}/docsList.css (90%) rename {docs => assets/styles}/plus.css (100%) delete mode 100644 docs/nmbot-telegram/img/update-pictures/index.md diff --git a/docs/img/icons/doc.svg b/assets/icons/doc.svg similarity index 100% rename from docs/img/icons/doc.svg rename to assets/icons/doc.svg diff --git a/docs/img/icons/folder.svg b/assets/icons/folder.svg similarity index 100% rename from docs/img/icons/folder.svg rename to assets/icons/folder.svg diff --git a/docs/nmbot-telegram/img/add_as_admin_step_1.png b/assets/images/nmbot/add_as_admin_step_1.png similarity index 100% rename from docs/nmbot-telegram/img/add_as_admin_step_1.png rename to assets/images/nmbot/add_as_admin_step_1.png diff --git a/docs/nmbot-telegram/img/add_as_admin_step_2.png b/assets/images/nmbot/add_as_admin_step_2.png similarity index 100% rename from docs/nmbot-telegram/img/add_as_admin_step_2.png rename to assets/images/nmbot/add_as_admin_step_2.png diff --git a/docs/nmbot-telegram/img/add_to_admin_step_1.png b/assets/images/nmbot/add_to_admin_step_1.png similarity index 100% rename from docs/nmbot-telegram/img/add_to_admin_step_1.png rename to assets/images/nmbot/add_to_admin_step_1.png diff --git a/docs/nmbot-telegram/img/add_to_admin_step_2.png b/assets/images/nmbot/add_to_admin_step_2.png similarity index 100% rename from docs/nmbot-telegram/img/add_to_admin_step_2.png rename to assets/images/nmbot/add_to_admin_step_2.png diff --git a/docs/nmbot-telegram/img/add_to_admin_step_3.png b/assets/images/nmbot/add_to_admin_step_3.png similarity index 100% rename from docs/nmbot-telegram/img/add_to_admin_step_3.png rename to assets/images/nmbot/add_to_admin_step_3.png diff --git a/docs/nmbot-telegram/img/add_to_admin_step_4.png b/assets/images/nmbot/add_to_admin_step_4.png similarity index 100% rename from docs/nmbot-telegram/img/add_to_admin_step_4.png rename to assets/images/nmbot/add_to_admin_step_4.png diff --git a/docs/nmbot-telegram/img/add_to_admin_step_5.png b/assets/images/nmbot/add_to_admin_step_5.png similarity index 100% rename from docs/nmbot-telegram/img/add_to_admin_step_5.png rename to assets/images/nmbot/add_to_admin_step_5.png diff --git a/docs/nmbot-telegram/img/doc cover.png b/assets/images/nmbot/doc cover.png similarity index 100% rename from docs/nmbot-telegram/img/doc cover.png rename to assets/images/nmbot/doc cover.png diff --git a/docs/nmbot-telegram/img/doc start intro.png b/assets/images/nmbot/doc start intro.png similarity index 100% rename from docs/nmbot-telegram/img/doc start intro.png rename to assets/images/nmbot/doc start intro.png diff --git a/docs/nmbot-telegram/img/doc start panel.png b/assets/images/nmbot/doc start panel.png similarity index 100% rename from docs/nmbot-telegram/img/doc start panel.png rename to assets/images/nmbot/doc start panel.png diff --git a/docs/nmbot-telegram/img/doc start telegram.png b/assets/images/nmbot/doc start telegram.png similarity index 100% rename from docs/nmbot-telegram/img/doc start telegram.png rename to assets/images/nmbot/doc start telegram.png diff --git a/docs/nmbot-telegram/img/interaction_example.png b/assets/images/nmbot/interaction_example.png similarity index 100% rename from docs/nmbot-telegram/img/interaction_example.png rename to assets/images/nmbot/interaction_example.png diff --git a/docs/nmbot-telegram/img/interaction_example2.png b/assets/images/nmbot/interaction_example2.png similarity index 100% rename from docs/nmbot-telegram/img/interaction_example2.png rename to assets/images/nmbot/interaction_example2.png diff --git a/docs/nmbot-telegram/img/interaction_example3.png b/assets/images/nmbot/interaction_example3.png similarity index 100% rename from docs/nmbot-telegram/img/interaction_example3.png rename to assets/images/nmbot/interaction_example3.png diff --git a/docs/nmbot-telegram/img/interaction_example4.png b/assets/images/nmbot/interaction_example4.png similarity index 100% rename from docs/nmbot-telegram/img/interaction_example4.png rename to assets/images/nmbot/interaction_example4.png diff --git a/docs/nmbot-telegram/img/launch_panel_from_any_chat.png b/assets/images/nmbot/launch_panel_from_any_chat.png similarity index 100% rename from docs/nmbot-telegram/img/launch_panel_from_any_chat.png rename to assets/images/nmbot/launch_panel_from_any_chat.png diff --git a/docs/nmbot-telegram/img/launch_panel_from_chat_corner.png b/assets/images/nmbot/launch_panel_from_chat_corner.png similarity index 100% rename from docs/nmbot-telegram/img/launch_panel_from_chat_corner.png rename to assets/images/nmbot/launch_panel_from_chat_corner.png diff --git a/docs/nmbot-telegram/img/mcp/mcp-config-page.png b/assets/images/nmbot/mcp/mcp-config-page.png similarity index 100% rename from docs/nmbot-telegram/img/mcp/mcp-config-page.png rename to assets/images/nmbot/mcp/mcp-config-page.png diff --git a/docs/nmbot-telegram/img/mcp/mcp-create-token-dialog.png b/assets/images/nmbot/mcp/mcp-create-token-dialog.png similarity index 100% rename from docs/nmbot-telegram/img/mcp/mcp-create-token-dialog.png rename to assets/images/nmbot/mcp/mcp-create-token-dialog.png diff --git a/docs/nmbot-telegram/img/mcp/mcp-entry.png b/assets/images/nmbot/mcp/mcp-entry.png similarity index 100% rename from docs/nmbot-telegram/img/mcp/mcp-entry.png rename to assets/images/nmbot/mcp/mcp-entry.png diff --git a/docs/nmbot-telegram/img/mcp/mcp-token-created-dialog.png b/assets/images/nmbot/mcp/mcp-token-created-dialog.png similarity index 100% rename from docs/nmbot-telegram/img/mcp/mcp-token-created-dialog.png rename to assets/images/nmbot/mcp/mcp-token-created-dialog.png diff --git a/docs/nmbot-telegram/img/mcp/mcp-vscode-command-palette.png b/assets/images/nmbot/mcp/mcp-vscode-command-palette.png similarity index 100% rename from docs/nmbot-telegram/img/mcp/mcp-vscode-command-palette.png rename to assets/images/nmbot/mcp/mcp-vscode-command-palette.png diff --git a/docs/nmbot-telegram/img/mcp/mcp-vscode-user-config.png b/assets/images/nmbot/mcp/mcp-vscode-user-config.png similarity index 100% rename from docs/nmbot-telegram/img/mcp/mcp-vscode-user-config.png rename to assets/images/nmbot/mcp/mcp-vscode-user-config.png diff --git a/docs/nmbot-telegram/img/nmartchat_panel_enablechat.png b/assets/images/nmbot/nmartchat_panel_enablechat.png similarity index 100% rename from docs/nmbot-telegram/img/nmartchat_panel_enablechat.png rename to assets/images/nmbot/nmartchat_panel_enablechat.png diff --git a/docs/nmbot-telegram/img/nmartchat_regadmin.png b/assets/images/nmbot/nmartchat_regadmin.png similarity index 100% rename from docs/nmbot-telegram/img/nmartchat_regadmin.png rename to assets/images/nmbot/nmartchat_regadmin.png diff --git a/docs/nmbot-telegram/img/reply-channel_eg.png b/assets/images/nmbot/reply-channel_eg.png similarity index 100% rename from docs/nmbot-telegram/img/reply-channel_eg.png rename to assets/images/nmbot/reply-channel_eg.png diff --git a/docs/nmbot-telegram/img/update-pictures/nmBot plus.png b/assets/images/nmbot/update-pictures/nmBot plus.png similarity index 100% rename from docs/nmbot-telegram/img/update-pictures/nmBot plus.png rename to assets/images/nmbot/update-pictures/nmBot plus.png diff --git a/docs/nmbot-telegram/img/update-pictures/nmbot-2312-christmas.jpg b/assets/images/nmbot/update-pictures/nmbot-2312-christmas.jpg similarity index 100% rename from docs/nmbot-telegram/img/update-pictures/nmbot-2312-christmas.jpg rename to assets/images/nmbot/update-pictures/nmbot-2312-christmas.jpg diff --git a/docs/nmbot-telegram/img/update-pictures/nmbot-2401-winter.jpg b/assets/images/nmbot/update-pictures/nmbot-2401-winter.jpg similarity index 100% rename from docs/nmbot-telegram/img/update-pictures/nmbot-2401-winter.jpg rename to assets/images/nmbot/update-pictures/nmbot-2401-winter.jpg diff --git a/docs/nmbot-telegram/img/update-pictures/nmbot-2402-spring-festival.jpg b/assets/images/nmbot/update-pictures/nmbot-2402-spring-festival.jpg similarity index 100% rename from docs/nmbot-telegram/img/update-pictures/nmbot-2402-spring-festival.jpg rename to assets/images/nmbot/update-pictures/nmbot-2402-spring-festival.jpg diff --git a/docs/nmbot-telegram/img/update-pictures/nmbot-2408.png b/assets/images/nmbot/update-pictures/nmbot-2408.png similarity index 100% rename from docs/nmbot-telegram/img/update-pictures/nmbot-2408.png rename to assets/images/nmbot/update-pictures/nmbot-2408.png diff --git a/docs/nmbot-telegram/img/update-pictures/nmbot-2409.png b/assets/images/nmbot/update-pictures/nmbot-2409.png similarity index 100% rename from docs/nmbot-telegram/img/update-pictures/nmbot-2409.png rename to assets/images/nmbot/update-pictures/nmbot-2409.png diff --git a/docs/nmbot-telegram/img/update-pictures/nmbot-2410.png b/assets/images/nmbot/update-pictures/nmbot-2410.png similarity index 100% rename from docs/nmbot-telegram/img/update-pictures/nmbot-2410.png rename to assets/images/nmbot/update-pictures/nmbot-2410.png diff --git a/docs/nmbot-telegram/img/update-pictures/nmbot-2412.png b/assets/images/nmbot/update-pictures/nmbot-2412.png similarity index 100% rename from docs/nmbot-telegram/img/update-pictures/nmbot-2412.png rename to assets/images/nmbot/update-pictures/nmbot-2412.png diff --git a/docs/nmbot-telegram/img/update-pictures/nmbot-2501.png b/assets/images/nmbot/update-pictures/nmbot-2501.png similarity index 100% rename from docs/nmbot-telegram/img/update-pictures/nmbot-2501.png rename to assets/images/nmbot/update-pictures/nmbot-2501.png diff --git a/docs/nmbot-telegram/img/update-pictures/nmbot-2502.png b/assets/images/nmbot/update-pictures/nmbot-2502.png similarity index 100% rename from docs/nmbot-telegram/img/update-pictures/nmbot-2502.png rename to assets/images/nmbot/update-pictures/nmbot-2502.png diff --git a/docs/nmbot-telegram/img/update-pictures/nmbot-2503.png b/assets/images/nmbot/update-pictures/nmbot-2503.png similarity index 100% rename from docs/nmbot-telegram/img/update-pictures/nmbot-2503.png rename to assets/images/nmbot/update-pictures/nmbot-2503.png diff --git a/docs/nmbot-telegram/img/update-pictures/nmbot-2504.png b/assets/images/nmbot/update-pictures/nmbot-2504.png similarity index 100% rename from docs/nmbot-telegram/img/update-pictures/nmbot-2504.png rename to assets/images/nmbot/update-pictures/nmbot-2504.png diff --git a/docs/nmbot-telegram/img/update-pictures/nmbot-2505.png b/assets/images/nmbot/update-pictures/nmbot-2505.png similarity index 100% rename from docs/nmbot-telegram/img/update-pictures/nmbot-2505.png rename to assets/images/nmbot/update-pictures/nmbot-2505.png diff --git a/docs/nmbot-telegram/img/update-pictures/nmbot-2506.png b/assets/images/nmbot/update-pictures/nmbot-2506.png similarity index 100% rename from docs/nmbot-telegram/img/update-pictures/nmbot-2506.png rename to assets/images/nmbot/update-pictures/nmbot-2506.png diff --git a/docs/nmbot-telegram/img/update-pictures/nmbot-2507.png b/assets/images/nmbot/update-pictures/nmbot-2507.png similarity index 100% rename from docs/nmbot-telegram/img/update-pictures/nmbot-2507.png rename to assets/images/nmbot/update-pictures/nmbot-2507.png diff --git a/docs/nmbot-telegram/img/update-pictures/nmbot-2508.png b/assets/images/nmbot/update-pictures/nmbot-2508.png similarity index 100% rename from docs/nmbot-telegram/img/update-pictures/nmbot-2508.png rename to assets/images/nmbot/update-pictures/nmbot-2508.png diff --git a/docs/nmbot-telegram/img/update-pictures/nmbot-2509.png b/assets/images/nmbot/update-pictures/nmbot-2509.png similarity index 100% rename from docs/nmbot-telegram/img/update-pictures/nmbot-2509.png rename to assets/images/nmbot/update-pictures/nmbot-2509.png diff --git a/docs/nmbot-telegram/img/update-pictures/nmbot-2510.png b/assets/images/nmbot/update-pictures/nmbot-2510.png similarity index 100% rename from docs/nmbot-telegram/img/update-pictures/nmbot-2510.png rename to assets/images/nmbot/update-pictures/nmbot-2510.png diff --git a/docs/nmbot-telegram/img/update-pictures/nmbot-2511.png b/assets/images/nmbot/update-pictures/nmbot-2511.png similarity index 100% rename from docs/nmbot-telegram/img/update-pictures/nmbot-2511.png rename to assets/images/nmbot/update-pictures/nmbot-2511.png diff --git a/docs/nmbot-telegram/img/update-pictures/nmbot-2512.png b/assets/images/nmbot/update-pictures/nmbot-2512.png similarity index 100% rename from docs/nmbot-telegram/img/update-pictures/nmbot-2512.png rename to assets/images/nmbot/update-pictures/nmbot-2512.png diff --git a/docs/img/keyword-reply-edit.png b/assets/images/shared/keyword-reply-edit.png similarity index 100% rename from docs/img/keyword-reply-edit.png rename to assets/images/shared/keyword-reply-edit.png diff --git a/docs/img/keyword-reply-limit.png b/assets/images/shared/keyword-reply-limit.png similarity index 100% rename from docs/img/keyword-reply-limit.png rename to assets/images/shared/keyword-reply-limit.png diff --git a/docs/img/nmteam-support-background.jpg b/assets/images/shared/nmteam-support-background.jpg similarity index 100% rename from docs/img/nmteam-support-background.jpg rename to assets/images/shared/nmteam-support-background.jpg diff --git a/docs/img/nmteam_account_register.png b/assets/images/shared/nmteam_account_register.png similarity index 100% rename from docs/img/nmteam_account_register.png rename to assets/images/shared/nmteam_account_register.png diff --git a/docs/img/spam faq.png b/assets/images/shared/spam faq.png similarity index 100% rename from docs/img/spam faq.png rename to assets/images/shared/spam faq.png diff --git a/docs/docsList.css b/assets/styles/docsList.css similarity index 90% rename from docs/docsList.css rename to assets/styles/docsList.css index 2b7344d..a1c9c76 100644 --- a/docs/docsList.css +++ b/assets/styles/docsList.css @@ -40,11 +40,11 @@ } .docsList .docsItem .icon.doc { - background-image: url(img/icons/doc.svg); + background-image: url(/assets/icons/doc.svg); } .docsList .docsItem .icon.folder { - background-image: url(img/icons/folder.svg); + background-image: url(/assets/icons/folder.svg); } .docsList .docsItem .text .description { diff --git a/docs/plus.css b/assets/styles/plus.css similarity index 100% rename from docs/plus.css rename to assets/styles/plus.css diff --git a/docs/index.md b/docs/index.md index dfe7c8a..d9b990d 100644 --- a/docs/index.md +++ b/docs/index.md @@ -68,7 +68,7 @@ nmTeam 帮助文档由 nmTeam 成员和社区志愿者共同编辑。您可以 width: 100%; height: 100%; background-color: var(--md-default-bg-color); - background-image: url(../img/nmteam-support-background.jpg); + background-image: url(/assets/images/shared/nmteam-support-background.jpg); background-size: cover; background-position: center; background-repeat: no-repeat; diff --git a/docs/nmbot-telegram/faq/spam.md b/docs/nmbot-telegram/faq/spam.md index c3d4b80..f6c6ed1 100644 --- a/docs/nmbot-telegram/faq/spam.md +++ b/docs/nmbot-telegram/faq/spam.md @@ -13,7 +13,7 @@ description: 了解 nmBot 骚扰拦截功能常见问题。 许多 nmBot 功能和其他机器人的功能,甚至群组管理员和 Telegram 本身都可以删除消息和封禁用户。 因此,您的消息被删除/您被封禁,不一定是 nmBot 的功能导致的。 -![关于不同的封禁情况](../../img/spam%20faq.png) +![关于不同的封禁情况](/assets/images/shared/spam%20faq.png) ## 如何了解我是不是被识别为了骚扰用户而被封禁的? diff --git a/docs/nmbot-telegram/group/group-setup.md b/docs/nmbot-telegram/group/group-setup.md index 46891fd..a2dece1 100644 --- a/docs/nmbot-telegram/group/group-setup.md +++ b/docs/nmbot-telegram/group/group-setup.md @@ -18,23 +18,23 @@ nmBot 管理功能仅在授予 nmBot 必要的管理员权限后可用。 第一步:打开群组信息页面,点击右上角的菜单按钮,然后选择“管理群组”(Manage Group,根据翻译不同可能有所区别,下同)。 -![](../img/add_to_admin_step_1.png) +![](/assets/images/nmbot/add_to_admin_step_1.png) 第二步:选择“管理员”(Admins)选项。 -![](../img/add_to_admin_step_2.png) +![](/assets/images/nmbot/add_to_admin_step_2.png) 第三步:点击“添加管理员”(Add Admin)按钮。 -![](../img/add_to_admin_step_3.png) +![](/assets/images/nmbot/add_to_admin_step_3.png) 第四步:搜索并点击“nmBot”选项。 -![](../img/add_to_admin_step_4.png) +![](/assets/images/nmbot/add_to_admin_step_4.png) 第五步:确保至少选中了“删除消息”(Delete Messages)和“封禁用户”(Ban Users)权限,然后选择保存。 -![](../img/add_to_admin_step_5.png) +![](/assets/images/nmbot/add_to_admin_step_5.png) ## 将 nmBot 添加为管理员 @@ -48,10 +48,10 @@ nmBot 管理功能仅在授予 nmBot 必要的管理员权限后可用。 第一步:打开 nmBot 个人信息页面,在菜单中选择“添加到群组或频道”(Add to Group or Channel,根据翻译不同可能有所区别,下同)。 -![](../img/add_as_admin_step_1.png) +![](/assets/images/nmbot/add_as_admin_step_1.png) 第二步:选择需要添加 nmBot 的群组。 第三步:在弹出的对话框中,确保打开“管理员权限”(Admin Rights)并选中了“删除消息”(Delete Messages)和“封禁用户”(Ban Users)权限,然后选择“添加机器人为管理员”(Add Bot as Admin)。 -![](../img/add_as_admin_step_2.png) +![](/assets/images/nmbot/add_as_admin_step_2.png) diff --git a/docs/nmbot-telegram/group/key-word-reply.md b/docs/nmbot-telegram/group/key-word-reply.md index 9e90626..fcad812 100644 --- a/docs/nmbot-telegram/group/key-word-reply.md +++ b/docs/nmbot-telegram/group/key-word-reply.md @@ -12,7 +12,7 @@ ## 了解关键词回复设置中可用的设置项 -![关键词回复编辑页面](../../img/keyword-reply-edit.png) +![关键词回复编辑页面](/assets/images/shared/keyword-reply-edit.png) 关键词回复包含一系列自定义设置,以满足您的需求。以下是您可以设置的选项: @@ -151,7 +151,7 @@ id:20210614 // 匹配用户 ID 为 20210614 的用户 ### 提示“我们无法完成操作,因为关键词回复数量达到上限。” -![页面提示“我们无法完成操作,因为关键词回复数量达到上限。”](../../img/keyword-reply-limit.png) +![页面提示“我们无法完成操作,因为关键词回复数量达到上限。”](/assets/images/shared/keyword-reply-limit.png) 这意味着您的群组中的关键词回复数量已达到上限。您可以删除一些不再需要的关键词回复,以便添加新的关键词回复。 diff --git a/docs/nmbot-telegram/group/members-interaction.md b/docs/nmbot-telegram/group/members-interaction.md index f0307a8..7d905e1 100644 --- a/docs/nmbot-telegram/group/members-interaction.md +++ b/docs/nmbot-telegram/group/members-interaction.md @@ -6,17 +6,17 @@ ## “群友互动”的触发方式 如果你所在群组的管理员启用了“群友互动”功能,你只需要尝试回复给你想互动的对象,并且随便写点什么,就能立刻看到效果。\ -![](../img/interaction_example.png) +![](/assets/images/nmbot/interaction_example.png) 看,它就这样工作了。\ 当然,你不一定非要对别人出这招,你还可以对自己使用,谁都不回复就可以了——对于社恐人士非常友好! -![](../img/interaction_example2.png) +![](/assets/images/nmbot/interaction_example2.png) 你回复 nmBot 发送的消息时,nmBot 会自动识别到你要回复给群友,所以你不必回复给群友发送的消息,回复 `xx xx 了 xx!` 就可以啦。 ## 让我们来看看高级点的操作 不觉得感叹号前面空空的吗?让我们试试在那里加些什么。\ 只需要打一个空格,并在空格后写点什么,就能把文字加到群友后面了。 -![](../img/interaction_example3.png) +![](/assets/images/nmbot/interaction_example3.png) ……虽然不知道什么路由器那么贵,不过我们已经达到目标了。\ 可是,如果要在前半部分想打一个空格,该怎么办?\ 你先别急,看完下一节,你一定会明白的。 @@ -24,7 +24,7 @@ ## 往消息里再加点料 话不多说,我们来看这个例子: -![](../img/interaction_example4.png) +![](/assets/images/nmbot/interaction_example4.png) ```text /$he的道歉书:由于我买了$space4299$space的路由器,让 非常生气,本人诚挚道歉,但死不悔改。 diff --git a/docs/nmbot-telegram/group/reply-channel.md b/docs/nmbot-telegram/group/reply-channel.md index cbad0eb..10a3a8a 100644 --- a/docs/nmbot-telegram/group/reply-channel.md +++ b/docs/nmbot-telegram/group/reply-channel.md @@ -10,7 +10,7 @@ !!! success "启用“同频气氛组”功能实例" - ![](../img/reply-channel_eg.png) + ![](/assets/images/nmbot/reply-channel_eg.png) ## nmBot 智能同频气氛组 (Beta 版) diff --git a/docs/nmbot-telegram/img/update-pictures/index.md b/docs/nmbot-telegram/img/update-pictures/index.md deleted file mode 100644 index b7b7406..0000000 --- a/docs/nmbot-telegram/img/update-pictures/index.md +++ /dev/null @@ -1,8 +0,0 @@ ---- -title: update-pictures -description: ---- - -# {index_title} - -{index_description} diff --git a/docs/nmbot-telegram/index.md b/docs/nmbot-telegram/index.md index e1cd21e..e574bff 100644 --- a/docs/nmbot-telegram/index.md +++ b/docs/nmbot-telegram/index.md @@ -4,7 +4,7 @@ title: nmBot Telegram 帮助文档 # nmBot Telegram 帮助文档 -![欢迎](./img/doc%20cover.png) +![欢迎](/assets/images/nmbot/doc%20cover.png) 尊敬的用户,您好! @@ -14,13 +14,13 @@ title: nmBot Telegram 帮助文档
-![开始使用 nmBot](./img/doc%20start%20telegram.png){ width="200" } +![开始使用 nmBot](/assets/images/nmbot/doc%20start%20telegram.png){ width="200" } -![开始使用 nmBot](./img/doc%20start%20panel.png){ width="200" } +![开始使用 nmBot](/assets/images/nmbot/doc%20start%20panel.png){ width="200" } -![开始使用 nmBot](./img/doc%20start%20intro.png){ width="200" } +![开始使用 nmBot](/assets/images/nmbot/doc%20start%20intro.png){ width="200" }
diff --git a/docs/nmbot-telegram/launch-panel.md b/docs/nmbot-telegram/launch-panel.md index d77fcf6..5579e8f 100644 --- a/docs/nmbot-telegram/launch-panel.md +++ b/docs/nmbot-telegram/launch-panel.md @@ -4,11 +4,11 @@ 您可以在任意对话中向 nmBot 发送 /panel,然后根据提示启动 nmBot 面板。 -![从任意对话启动 nmBot 面板](img/launch_panel_from_any_chat.png) +![从任意对话启动 nmBot 面板](/assets/images/nmbot/launch_panel_from_any_chat.png) 也可以在与 nmBot 的对话窗口左下角点击按钮打开 nmBot 面板。 -![从与 nmBot 的对话窗口启动 nmBot 面板](img/launch_panel_from_chat_corner.png){ width="200" } +![从与 nmBot 的对话窗口启动 nmBot 面板](/assets/images/nmbot/launch_panel_from_chat_corner.png){ width="200" } ## 在 Web 浏览器中启动 nmBot 面板 diff --git a/docs/nmbot-telegram/mcp.md b/docs/nmbot-telegram/mcp.md index 60c044b..cf7456d 100644 --- a/docs/nmbot-telegram/mcp.md +++ b/docs/nmbot-telegram/mcp.md @@ -29,9 +29,9 @@ description: 创建和管理 nmBot MCP Access Token,并在 VS Code 等外部 您可以在 nmBot 面板中找到 `MCP 配置` 入口。 -![nmBot 面板中的 MCP 配置入口](img/mcp/mcp-entry.png) +![nmBot 面板中的 MCP 配置入口](/assets/images/nmbot/mcp/mcp-entry.png) -![MCP 配置页面](img/mcp/mcp-config-page.png){ width="360" } +![MCP 配置页面](/assets/images/nmbot/mcp/mcp-config-page.png){ width="360" } ## 创建 MCP Access Token @@ -44,13 +44,13 @@ description: 创建和管理 nmBot MCP Access Token,并在 VS Code 等外部 1. 创建成功后,系统会弹出提示框并展示完整 Token。 1. 立即复制该 Token,并保存在安全的位置。 -![创建 MCP Access Token](img/mcp/mcp-create-token-dialog.png){ width="360" } +![创建 MCP Access Token](/assets/images/nmbot/mcp/mcp-create-token-dialog.png){ width="360" } ## 创建后会看到什么 Token 创建成功后,完整 Token 只会展示这一次。返回列表后,通常只能看到 Token 名称、创建时间,以及经过部分隐藏的 Token 内容。 -![Token 创建成功后的提示](img/mcp/mcp-token-created-dialog.png){ width="360" } +![Token 创建成功后的提示](/assets/images/nmbot/mcp/mcp-token-created-dialog.png){ width="360" } ## 在 VS Code 中使用 nmBot MCP @@ -65,7 +65,7 @@ Token 创建成功后,完整 Token 只会展示这一次。返回列表后, 如果这是您个人长期使用的配置,通常更适合放在用户配置中。 -![在 VS Code 中打开 MCP 用户配置](img/mcp/mcp-vscode-command-palette.png) +![在 VS Code 中打开 MCP 用户配置](/assets/images/nmbot/mcp/mcp-vscode-command-palette.png) ### 2. 添加 nmBot MCP 服务器配置 @@ -85,7 +85,7 @@ Token 创建成功后,完整 Token 只会展示这一次。返回列表后, } ``` -![VS Code 中的 MCP 配置示例](img/mcp/mcp-vscode-user-config.png){ width="720" } +![VS Code 中的 MCP 配置示例](/assets/images/nmbot/mcp/mcp-vscode-user-config.png){ width="720" } 请注意: diff --git a/docs/nmbot-telegram/nmartchat.md b/docs/nmbot-telegram/nmartchat.md index c6486de..5859852 100644 --- a/docs/nmbot-telegram/nmartchat.md +++ b/docs/nmbot-telegram/nmartchat.md @@ -26,10 +26,10 @@ nmBot 在收到私信和打开 nmartChat 功能的群组回复的贴纸消息时 !!! faq "没有在面板中找到您的群聊?" 您必须为此群聊的**管理员**, 且需要在群内发送 `/regadmin@nmnmfunbot` 注册您的群聊。 - ![regadmin](img/nmartchat_regadmin.png) + ![regadmin](/assets/images/nmbot/nmartchat_regadmin.png) 3.在群聊设置中开启 nmartChat 功能。\ -![enablechat](img/nmartchat_panel_enablechat.png) +![enablechat](/assets/images/nmbot/nmartchat_panel_enablechat.png) !!! faq "为什么在频道(或在群聊内使用频道身份)发送以 nm 开头的消息,nmBot 不回复我?" diff --git a/docs/nmbot-telegram/update-log/2023-12.md b/docs/nmbot-telegram/update-log/2023-12.md index 3a472f6..d525590 100644 --- a/docs/nmbot-telegram/update-log/2023-12.md +++ b/docs/nmbot-telegram/update-log/2023-12.md @@ -4,7 +4,7 @@ index: -2312 # nmBot 2023 年 12 月功能更新 -![更新宣传图](../img/update-pictures/nmbot-2312-christmas.jpg) +![更新宣传图](/assets/images/nmbot/update-pictures/nmbot-2312-christmas.jpg) 亲爱的用户,您期盼已久的 nmBot 功能更新现已推出。\ 本更新推出新的同频气氛组可选语气“病娇”。本更新还优化了 nmBot 面板的体验,使这款遥遥领先的面板体验更进一步。本更新还包含错误修复和稳定性提升。 diff --git a/docs/nmbot-telegram/update-log/2024-01.md b/docs/nmbot-telegram/update-log/2024-01.md index e20c2d5..65d08e7 100644 --- a/docs/nmbot-telegram/update-log/2024-01.md +++ b/docs/nmbot-telegram/update-log/2024-01.md @@ -4,7 +4,7 @@ index: -2401 # nmBot 2024 年 1 月功能更新 -![更新宣传图](../img/update-pictures/nmbot-2401-winter.jpg) +![更新宣传图](/assets/images/nmbot/update-pictures/nmbot-2401-winter.jpg) 亲爱的用户,您期盼已久的 nmBot 功能更新现已推出。\ 本更新为 nmBot 和 nmBot 面板带来了颇具特色的火星文;本更新还包含错误修复和稳定性提升。 diff --git a/docs/nmbot-telegram/update-log/2024-02.md b/docs/nmbot-telegram/update-log/2024-02.md index 934b30c..884d10d 100644 --- a/docs/nmbot-telegram/update-log/2024-02.md +++ b/docs/nmbot-telegram/update-log/2024-02.md @@ -4,7 +4,7 @@ index: -2402 # nmBot 2024 年 2 月功能更新 -![更新宣传图](../img/update-pictures/nmbot-2402-spring-festival.jpg) +![更新宣传图](/assets/images/nmbot/update-pictures/nmbot-2402-spring-festival.jpg) 亲爱的用户,您期盼已久的 nmBot 功能更新现已推出。\ 本更新推出了“赠品活动追踪”功能,帮助您管理参与的赠品活动;本更新还包含错误修复和稳定性提升。 diff --git a/docs/nmbot-telegram/update-log/2024-08.md b/docs/nmbot-telegram/update-log/2024-08.md index 81d5943..3f9c4e0 100644 --- a/docs/nmbot-telegram/update-log/2024-08.md +++ b/docs/nmbot-telegram/update-log/2024-08.md @@ -8,7 +8,7 @@ index: -2408 ### 置顶对话、高效手势和系统优化——2024 年 8 月功能更新 -![](../img/update-pictures/nmbot-2408.png) +![](/assets/images/nmbot/update-pictures/nmbot-2408.png) 在 8 月更新中,我们致力于系统操作便捷性和稳定性优化,为大家带来了一系列更方便快捷的操作方式。 diff --git a/docs/nmbot-telegram/update-log/2024-09.md b/docs/nmbot-telegram/update-log/2024-09.md index c4802b4..b2bae2b 100644 --- a/docs/nmbot-telegram/update-log/2024-09.md +++ b/docs/nmbot-telegram/update-log/2024-09.md @@ -8,7 +8,7 @@ index: -2409 ### 骚扰拦截新举措、频道管理好工具和更多——2024 年 9 月功能更新 -![](../img/update-pictures/nmbot-2409.png) +![](/assets/images/nmbot/update-pictures/nmbot-2409.png) #### 入群验证拦截进化 diff --git a/docs/nmbot-telegram/update-log/2024-10.md b/docs/nmbot-telegram/update-log/2024-10.md index 67e16eb..0b40c7e 100644 --- a/docs/nmbot-telegram/update-log/2024-10.md +++ b/docs/nmbot-telegram/update-log/2024-10.md @@ -8,7 +8,7 @@ index: -2410 ### 提速提质大优化——2024 年 10 月功能更新 -![](../img/update-pictures/nmbot-2410.png) +![](/assets/images/nmbot/update-pictures/nmbot-2410.png) #### 面板加载大提速 diff --git a/docs/nmbot-telegram/update-log/2024-12.md b/docs/nmbot-telegram/update-log/2024-12.md index cea5b1e..59846c2 100644 --- a/docs/nmbot-telegram/update-log/2024-12.md +++ b/docs/nmbot-telegram/update-log/2024-12.md @@ -8,7 +8,7 @@ index: -2412 ### 全屏迷你 App 和 nmBot+ 新权益——2024 年 12 月功能更新 -![](../img/update-pictures/nmbot-2412.png) +![](/assets/images/nmbot/update-pictures/nmbot-2412.png) 本月我们为免费用户和付费用户双双带来好消息! diff --git a/docs/nmbot-telegram/update-log/2025-01.md b/docs/nmbot-telegram/update-log/2025-01.md index ddff79e..f4a3199 100644 --- a/docs/nmbot-telegram/update-log/2025-01.md +++ b/docs/nmbot-telegram/update-log/2025-01.md @@ -8,7 +8,7 @@ index: -2501 ### 2024 年度报告、配置搜索和更多——2025 年 1 月功能更新 -![](../img/update-pictures/nmbot-2501.png) +![](/assets/images/nmbot/update-pictures/nmbot-2501.png) nmBot 迎来 2025 年的第一次更新!在这个辞旧迎新的时刻,我们邀请您回望 2024 年您和 nmBot 的点点滴滴。得益于分析组件的改进,我们可以在保障隐私的前提下,一同回顾过去一年我们共同书写的不凡篇章。 diff --git a/docs/nmbot-telegram/update-log/2025-02.md b/docs/nmbot-telegram/update-log/2025-02.md index 47ef37b..346be86 100644 --- a/docs/nmbot-telegram/update-log/2025-02.md +++ b/docs/nmbot-telegram/update-log/2025-02.md @@ -8,7 +8,7 @@ index: -2502 ### 按消息类型警告和更多优化——2025 年 2 月功能更新 -![](../img/update-pictures/nmbot-2502.png) +![](/assets/images/nmbot/update-pictures/nmbot-2502.png) 今天我们推出乙巳蛇年的第一次 nmBot 功能更新。 diff --git a/docs/nmbot-telegram/update-log/2025-03.md b/docs/nmbot-telegram/update-log/2025-03.md index ebc78f4..f51d5e5 100644 --- a/docs/nmbot-telegram/update-log/2025-03.md +++ b/docs/nmbot-telegram/update-log/2025-03.md @@ -8,7 +8,7 @@ index: -2503 ### 同频气氛组与消息模板新升级——2025 年 3 月功能更新 -![](../img/update-pictures/nmbot-2503.png) +![](/assets/images/nmbot/update-pictures/nmbot-2503.png) 在这个版本中,我们为广受欢迎的两项 nmBot 特色功能带来新升级。 diff --git a/docs/nmbot-telegram/update-log/2025-04.md b/docs/nmbot-telegram/update-log/2025-04.md index e9d725b..722329f 100644 --- a/docs/nmbot-telegram/update-log/2025-04.md +++ b/docs/nmbot-telegram/update-log/2025-04.md @@ -8,7 +8,7 @@ index: -2504 ### nmBot 智能上线、群组配置导入与导出——2025 年 4 月功能更新 -![](../img/update-pictures/nmbot-2504.png) +![](/assets/images/nmbot/update-pictures/nmbot-2504.png) 为了庆祝生日月,除了首批 nmBot 智能功能 nmBot 智能聊天 (Beta 版) 外,我们还推出了群组配置导入和导出功能,并对商业关键词回复等功能进行了增强。 diff --git a/docs/nmbot-telegram/update-log/2025-05.md b/docs/nmbot-telegram/update-log/2025-05.md index 1a3fbe9..6dd463c 100644 --- a/docs/nmbot-telegram/update-log/2025-05.md +++ b/docs/nmbot-telegram/update-log/2025-05.md @@ -8,7 +8,7 @@ index: -2505 ### 同频气氛组、自动删除外部引用消息、退群消息——2025 年 5 月功能更新 -![](../img/update-pictures/nmbot-2505.png) +![](/assets/images/nmbot/update-pictures/nmbot-2505.png) 本月更新带来了与 nmBot 智能相结合的新版“同频气氛组”(Beta 版)、外部引用消息自动删除和退群消息。 diff --git a/docs/nmbot-telegram/update-log/2025-06.md b/docs/nmbot-telegram/update-log/2025-06.md index acaf591..7fda532 100644 --- a/docs/nmbot-telegram/update-log/2025-06.md +++ b/docs/nmbot-telegram/update-log/2025-06.md @@ -8,7 +8,7 @@ index: -2506 ### 商业定时任务、自动删除群组操作回复消息——2025 年 6 月功能更新 -![](../img/update-pictures/nmbot-2506.png) +![](/assets/images/nmbot/update-pictures/nmbot-2506.png) 在 nmTeam 的生日月,我们为 nmBot 商业功能带来了史上最大的更新。 diff --git a/docs/nmbot-telegram/update-log/2025-07.md b/docs/nmbot-telegram/update-log/2025-07.md index 38cab5c..a1d16ba 100644 --- a/docs/nmbot-telegram/update-log/2025-07.md +++ b/docs/nmbot-telegram/update-log/2025-07.md @@ -8,7 +8,7 @@ index: -2507 ### 焕新 nmBot 面板——2025 年 7 月功能更新 -![](../img/update-pictures/nmbot-2507.png) +![](/assets/images/nmbot/update-pictures/nmbot-2507.png) 2023 年推出的新版 nmBot 面板迎来史上最大的视觉升级!本月我们为 nmBot 面板带来一套全新升级的用户界面。 diff --git a/docs/nmbot-telegram/update-log/2025-08.md b/docs/nmbot-telegram/update-log/2025-08.md index 228859f..545eb62 100644 --- a/docs/nmbot-telegram/update-log/2025-08.md +++ b/docs/nmbot-telegram/update-log/2025-08.md @@ -8,7 +8,7 @@ index: -2508 ### 呼叫管理员优化、骚扰拦截更精准——2025 年 8 月功能更新 -![](../img/update-pictures/nmbot-2508.png) +![](/assets/images/nmbot/update-pictures/nmbot-2508.png) 在 8 月更新中,我们为呼叫群组管理员功能带来了新的通知选项,并为骚扰拦截功能和群组自定义规则添加了全新的识别模式。 diff --git a/docs/nmbot-telegram/update-log/2025-09.md b/docs/nmbot-telegram/update-log/2025-09.md index fe2e829..59b0891 100644 --- a/docs/nmbot-telegram/update-log/2025-09.md +++ b/docs/nmbot-telegram/update-log/2025-09.md @@ -8,7 +8,7 @@ index: -2509 ### 紧急模式、半自动入群验证——2025 年 9 月功能更新 -![](../img/update-pictures/nmbot-2509.png) +![](/assets/images/nmbot/update-pictures/nmbot-2509.png) 本月我们为 nmBot 带来了紧急模式,在应对群组突发状况时特别有用。 diff --git a/docs/nmbot-telegram/update-log/2025-10.md b/docs/nmbot-telegram/update-log/2025-10.md index 8bff37e..bf10c4c 100644 --- a/docs/nmbot-telegram/update-log/2025-10.md +++ b/docs/nmbot-telegram/update-log/2025-10.md @@ -8,7 +8,7 @@ index: -2510 ### 关键词回复识别情绪、适配全新系统 UI——2025 年 10 月功能更新 -![](../img/update-pictures/nmbot-2510.png) +![](/assets/images/nmbot/update-pictures/nmbot-2510.png) 万圣节快乐!本月我们为关键词回复和面板界面带来全新升级。 diff --git a/docs/nmbot-telegram/update-log/2025-11.md b/docs/nmbot-telegram/update-log/2025-11.md index 7fc55b3..3240cb3 100644 --- a/docs/nmbot-telegram/update-log/2025-11.md +++ b/docs/nmbot-telegram/update-log/2025-11.md @@ -8,7 +8,7 @@ index: -2511 ### nmartChat+、频道封禁新升级——2025 年 11 月功能更新 -![](../img/update-pictures/nmbot-2511.png) +![](/assets/images/nmbot/update-pictures/nmbot-2511.png) 本次更新带来 nmartChat+ 和针对频道发言的更多可选项。 diff --git a/docs/nmbot-telegram/update-log/2025-12.md b/docs/nmbot-telegram/update-log/2025-12.md index 6b47ab1..723f9bc 100644 --- a/docs/nmbot-telegram/update-log/2025-12.md +++ b/docs/nmbot-telegram/update-log/2025-12.md @@ -8,7 +8,7 @@ index: -2512 ### 骚扰拦截 2.0、nmBot Copilot——2025 年 12 月功能更新 -![](../img/update-pictures/nmbot-2512.png) +![](/assets/images/nmbot/update-pictures/nmbot-2512.png) 各位新年快乐。这次更新携 nmBot 智能带来了全新升级的骚扰拦截功能和全新升级的 nmBot Copilot。 diff --git a/docs/nmbot-telegram/update-log/nmbot-plus.md b/docs/nmbot-telegram/update-log/nmbot-plus.md index 4aebfe8..c81292e 100644 --- a/docs/nmbot-telegram/update-log/nmbot-plus.md +++ b/docs/nmbot-telegram/update-log/nmbot-plus.md @@ -4,7 +4,7 @@ index: -2411 # nmBot+ (2024 年 11 月) -![nmBot+](../img/update-pictures/nmBot%20plus.png) +![nmBot+](/assets/images/nmbot/update-pictures/nmBot%20plus.png) nmTeam 今日推出 nmBot+,一项为需要更多高级功能的 nmBot 用户和群组量身打造的专属付费方案。 diff --git a/mkdocs-template.yml b/mkdocs-template.yml index a8a4c48..6036ee0 100644 --- a/mkdocs-template.yml +++ b/mkdocs-template.yml @@ -1,52 +1,52 @@ -site_name: nmTeam Support -theme: - name: material - logo: https://websiteres.nmteam.xyz/producticon/nmTeam/logo@128.png - favicon: https://websiteres.nmteam.xyz/producticon/nmTeam/logo@128.png - language: zh - palette: - - media: "(prefers-color-scheme: light)" - scheme: default - primary: yellow - accent: yellow - toggle: - icon: material/weather-sunny - name: 浅色模式 - - media: "(prefers-color-scheme: dark)" - scheme: slate - primary: yellow - accent: yellow - toggle: - icon: material/weather-night - name: 深色模式 - features: - - navigation.tabs - - navigation.sections - - toc.integrate - - navigation.top -extra_css: [docsList.css, plus.css] -extra_javascript: [assets/js/redirects.js] -docs_dir: generated - -extra: - social: - - icon: fontawesome/brands/github - link: https://github.com/nm-Team/Support - name: GitHub - analytics: - provider: google - property: G-KF37QRD9LN -copyright: Copyright 2022-2025 nmTeam. All rights reserved.
This website is licensed under the MIT License. - -markdown_extensions: - - attr_list - - md_in_html - - footnotes - - admonition - - pymdownx.details - - pymdownx.superfences - -nav: -# Contents below are automatically generated by the script. -# NAV_ARIA_START +site_name: nmTeam Support +theme: + name: material + logo: https://websiteres.nmteam.xyz/producticon/nmTeam/logo@128.png + favicon: https://websiteres.nmteam.xyz/producticon/nmTeam/logo@128.png + language: zh + palette: + - media: "(prefers-color-scheme: light)" + scheme: default + primary: yellow + accent: yellow + toggle: + icon: material/weather-sunny + name: 浅色模式 + - media: "(prefers-color-scheme: dark)" + scheme: slate + primary: yellow + accent: yellow + toggle: + icon: material/weather-night + name: 深色模式 + features: + - navigation.tabs + - navigation.sections + - toc.integrate + - navigation.top +extra_css: [assets/styles/docsList.css, assets/styles/plus.css] +extra_javascript: [assets/js/redirects.js] +docs_dir: generated + +extra: + social: + - icon: fontawesome/brands/github + link: https://github.com/nm-Team/Support + name: GitHub + analytics: + provider: google + property: G-KF37QRD9LN +copyright: Copyright 2022-2025 nmTeam. All rights reserved.
This website is licensed under the MIT License. + +markdown_extensions: + - attr_list + - md_in_html + - footnotes + - admonition + - pymdownx.details + - pymdownx.superfences + +nav: +# Contents below are automatically generated by the script. +# NAV_ARIA_START # NAV_ARIA_END \ No newline at end of file diff --git a/src/nmteam_support/cli.py b/src/nmteam_support/cli.py index 3738157..cd798a0 100644 --- a/src/nmteam_support/cli.py +++ b/src/nmteam_support/cli.py @@ -96,7 +96,7 @@ def _watch_and_regenerate(options: GeneratorOptions, stop: threading.Event) -> N def _snapshot(options: GeneratorOptions) -> tuple[tuple[int, int], ...]: stamps: list[tuple[int, int]] = [] - for root in (options.docs_dir, options.template_path): + for root in (options.docs_dir, options.assets_dir, options.template_path): if root.is_file(): stamps.append((root.stat().st_mtime_ns, root.stat().st_size)) elif root.is_dir(): diff --git a/src/nmteam_support/generator.py b/src/nmteam_support/generator.py index a81ce2f..a72fca8 100644 --- a/src/nmteam_support/generator.py +++ b/src/nmteam_support/generator.py @@ -20,6 +20,7 @@ class GeneratorOptions: """All paths the generator touches; tests inject tmp_path-based values.""" docs_dir: Path + assets_dir: Path template_path: Path redirects_path: Path cache_dir: Path @@ -31,6 +32,7 @@ def default_options(root: Path | None = None) -> GeneratorOptions: root = root or Path.cwd() return GeneratorOptions( docs_dir=root / "docs", + assets_dir=root / "assets", template_path=root / "mkdocs-template.yml", redirects_path=root / "redirects.json", cache_dir=root / "cache", @@ -51,6 +53,8 @@ def generate(options: GeneratorOptions) -> None: _write_tree(options.docs_dir, cache_dir, root) _write_indexes(cache_dir, root) + if options.assets_dir.exists(): + shutil.copytree(options.assets_dir, cache_dir / "assets", dirs_exist_ok=True) nav_yaml = build_nav_yaml(root) template = options.template_path.read_text(encoding="UTF-8", errors="ignore") diff --git a/tests/test_cli.py b/tests/test_cli.py index e774f13..d1c994b 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -16,6 +16,7 @@ def _options(tmp_path: Path) -> GeneratorOptions: return GeneratorOptions( docs_dir=tmp_path / "docs", + assets_dir=tmp_path / "assets", template_path=tmp_path / "mkdocs-template.yml", redirects_path=tmp_path / "redirects.json", cache_dir=tmp_path / "cache", @@ -192,3 +193,13 @@ def generate_once_then_succeed(_options): assert generate_calls == 2 assert "temporary failure" in capsys.readouterr().err + + +def test_snapshot_changes_when_asset_changes(tmp_path): + opts = _options(tmp_path) + opts.assets_dir.mkdir() + before = cli._snapshot(opts) + + (opts.assets_dir / "site.css").write_text("body {}\n", encoding="utf-8") + + assert cli._snapshot(opts) != before diff --git a/tests/test_generator.py b/tests/test_generator.py index c716710..88dbc6b 100644 --- a/tests/test_generator.py +++ b/tests/test_generator.py @@ -13,8 +13,11 @@ def _full_options(tmp_path: Path, docs_dir: Path, redirects: str | None = None) ) if redirects is not None: (tmp_path / "redirects.json").write_text(redirects, encoding="utf-8") + assets_dir = tmp_path / "assets" + assets_dir.mkdir(exist_ok=True) return GeneratorOptions( docs_dir=docs_dir, + assets_dir=assets_dir, template_path=tmp_path / "mkdocs-template.yml", redirects_path=tmp_path / "redirects.json", cache_dir=tmp_path / "cache", @@ -33,6 +36,19 @@ def test_generate_writes_all_outputs(tmp_path, docs_dir): assert "nmbot-telegram/mcp.md" in mkdocs_yml +def test_generate_stages_centralized_assets(tmp_path, docs_dir): + options = _full_options(tmp_path, docs_dir) + stylesheet = options.assets_dir / "styles" / "site.css" + stylesheet.parent.mkdir(parents=True) + stylesheet.write_text("body { color: black; }\n", encoding="utf-8") + + generate(options) + + assert (options.generated_dir / "assets" / "styles" / "site.css").read_text( + encoding="utf-8" + ) == stylesheet.read_text(encoding="utf-8") + + def test_generate_injects_contributing_note(tmp_path, docs_dir): options = _full_options(tmp_path, docs_dir) generate(options) @@ -86,6 +102,7 @@ def test_generate_real_docs_tree(tmp_path): pytest.skip("docs/ not present") options = GeneratorOptions( docs_dir=docs, + assets_dir=repo_root / "assets", template_path=repo_root / "mkdocs-template.yml", redirects_path=repo_root / "redirects.json", cache_dir=tmp_path / "cache", From 4ca1e08bbf8387c4400489afc5daca88efd90960 Mon Sep 17 00:00:00 2001 From: Alice39s Date: Sun, 9 Aug 2026 19:39:02 +0900 Subject: [PATCH 33/57] feat(images): add WebP images with compressed fallbacks --- mkdocs-template.yml | 1 + pyproject.toml | 2 + src/nmteam_support/generator.py | 3 +- src/nmteam_support/image_pipeline.py | 56 +++++++++++++++++++++++ src/nmteam_support/markdown_images.py | 62 ++++++++++++++++++++++++++ tests/test_generator.py | 13 ++++++ tests/test_image_pipeline.py | 64 +++++++++++++++++++++++++++ tests/test_markdown_images.py | 39 ++++++++++++++++ uv.lock | 54 ++++++++++++++++++++++ 9 files changed, 293 insertions(+), 1 deletion(-) create mode 100644 src/nmteam_support/image_pipeline.py create mode 100644 src/nmteam_support/markdown_images.py create mode 100644 tests/test_image_pipeline.py create mode 100644 tests/test_markdown_images.py diff --git a/mkdocs-template.yml b/mkdocs-template.yml index 6036ee0..de2acaf 100644 --- a/mkdocs-template.yml +++ b/mkdocs-template.yml @@ -39,6 +39,7 @@ extra: copyright: Copyright 2022-2025 nmTeam. All rights reserved.
This website is licensed under the MIT License. markdown_extensions: + - nmteam_support.markdown_images - attr_list - md_in_html - footnotes diff --git a/pyproject.toml b/pyproject.toml index 3fe5a4c..945c77a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,8 +4,10 @@ version = "0.1.0" description = "nmTeam Support 文档站构建工具链" requires-python = ">=3.14" dependencies = [ + "markdown>=3.10", "mkdocs==1.6.1", "mkdocs-material==9.7.7", + "pillow>=12.1.0", "pyyaml>=6.0.2", "typer>=0.21.1", ] diff --git a/src/nmteam_support/generator.py b/src/nmteam_support/generator.py index a72fca8..2b97525 100644 --- a/src/nmteam_support/generator.py +++ b/src/nmteam_support/generator.py @@ -8,6 +8,7 @@ from nmteam_support.contributing import render_contributing_note, should_hide_contributing_note from nmteam_support.frontmatter import split_frontmatter +from nmteam_support.image_pipeline import stage_assets from nmteam_support.index import render_index_page from nmteam_support.nav import build_nav_yaml from nmteam_support.redirects import load_redirects, render_redirects_js @@ -54,7 +55,7 @@ def generate(options: GeneratorOptions) -> None: _write_tree(options.docs_dir, cache_dir, root) _write_indexes(cache_dir, root) if options.assets_dir.exists(): - shutil.copytree(options.assets_dir, cache_dir / "assets", dirs_exist_ok=True) + stage_assets(options.assets_dir, cache_dir / "assets") nav_yaml = build_nav_yaml(root) template = options.template_path.read_text(encoding="UTF-8", errors="ignore") diff --git a/src/nmteam_support/image_pipeline.py b/src/nmteam_support/image_pipeline.py new file mode 100644 index 0000000..76a288b --- /dev/null +++ b/src/nmteam_support/image_pipeline.py @@ -0,0 +1,56 @@ +"""Documentation asset staging and raster optimization.""" + +from __future__ import annotations + +import shutil +from pathlib import Path + +from PIL import Image + +RASTER_QUALITY = 80 +_RASTER_SUFFIXES = {".jpg", ".jpeg", ".png"} + + +def stage_assets(source_dir: Path, target_dir: Path) -> None: + """Stage assets, emitting optimized fallbacks and WebP raster variants.""" + for source in sorted(source_dir.rglob("*")): + relative = source.relative_to(source_dir) + target = target_dir / relative + if source.is_dir(): + target.mkdir(parents=True, exist_ok=True) + elif source.suffix.lower() in _RASTER_SUFFIXES: + target.parent.mkdir(parents=True, exist_ok=True) + _optimize_raster(source, target) + else: + target.parent.mkdir(parents=True, exist_ok=True) + shutil.copy2(source, target) + + +def _optimize_raster(source: Path, fallback: Path) -> None: + with Image.open(source) as image: + image.load() + webp = fallback.with_suffix(".webp") + image.save(webp, "WEBP", quality=RASTER_QUALITY, method=6) + if fallback.suffix.lower() == ".png": + _quantize_png(image).save(fallback, "PNG", optimize=True) + else: + jpeg = image if image.mode in {"RGB", "L", "CMYK"} else image.convert("RGB") + jpeg.save( + fallback, + "JPEG", + quality=RASTER_QUALITY, + optimize=True, + progressive=True, + ) + + +def _quantize_png(image: Image.Image) -> Image.Image: + if image.mode in {"RGBA", "LA"} or "transparency" in image.info: + return image.convert("RGBA").quantize( + colors=256, + method=Image.Quantize.FASTOCTREE, + ) + return image.convert("RGB").quantize( + colors=256, + method=Image.Quantize.MEDIANCUT, + ) diff --git a/src/nmteam_support/markdown_images.py b/src/nmteam_support/markdown_images.py new file mode 100644 index 0000000..fb31e29 --- /dev/null +++ b/src/nmteam_support/markdown_images.py @@ -0,0 +1,62 @@ +"""WebP-first Markdown image rendering.""" + +from __future__ import annotations + +from pathlib import PurePosixPath +from urllib.parse import urlsplit, urlunsplit +from xml.etree import ElementTree + +from markdown import Markdown +from markdown.extensions import Extension +from markdown.treeprocessors import Treeprocessor + +_RASTER_SUFFIXES = {".jpg", ".jpeg", ".png"} + + +class WebpPictureTreeprocessor(Treeprocessor): + """Wrap local raster images in WebP-first ``picture`` elements.""" + + def run(self, root: ElementTree.Element) -> None: + for parent in list(root.iter()): + for index, image in enumerate(list(parent)): + if image.tag != "img" or not _is_local_raster(image.get("src", "")): + continue + source = ElementTree.Element( + "source", + { + "srcset": _webp_url(image.attrib["src"]), + "type": "image/webp", + }, + ) + picture = ElementTree.Element("picture") + picture.tail = image.tail + image.tail = None + picture.extend((source, image)) + parent[index] = picture + + +class WebpPictureExtension(Extension): + """Register WebP-first rendering after ``attr_list`` processing.""" + + def extendMarkdown(self, md: Markdown) -> None: + md.treeprocessors.register(WebpPictureTreeprocessor(md), "webp_picture", 7) + + +def _is_local_raster(url: str) -> bool: + parsed = urlsplit(url) + return ( + not parsed.scheme + and not parsed.netloc + and PurePosixPath(parsed.path).suffix.lower() in _RASTER_SUFFIXES + ) + + +def _webp_url(url: str) -> str: + parsed = urlsplit(url) + path = str(PurePosixPath(parsed.path).with_suffix(".webp")) + return urlunsplit((parsed.scheme, parsed.netloc, path, parsed.query, parsed.fragment)) + + +def makeExtension(**kwargs) -> WebpPictureExtension: + """Load the extension from MkDocs/Python-Markdown configuration.""" + return WebpPictureExtension(**kwargs) diff --git a/tests/test_generator.py b/tests/test_generator.py index 88dbc6b..8c64b7c 100644 --- a/tests/test_generator.py +++ b/tests/test_generator.py @@ -3,6 +3,7 @@ from pathlib import Path import pytest +from PIL import Image from nmteam_support.generator import GeneratorOptions, generate, render_doc_file @@ -49,6 +50,18 @@ def test_generate_stages_centralized_assets(tmp_path, docs_dir): ) == stylesheet.read_text(encoding="utf-8") +def test_generate_emits_optimized_raster_variants(tmp_path, docs_dir): + options = _full_options(tmp_path, docs_dir) + image_path = options.assets_dir / "images" / "diagram.png" + image_path.parent.mkdir(parents=True) + Image.new("RGB", (16, 16), "red").save(image_path) + + generate(options) + + assert (options.generated_dir / "assets" / "images" / "diagram.png").exists() + assert (options.generated_dir / "assets" / "images" / "diagram.webp").exists() + + def test_generate_injects_contributing_note(tmp_path, docs_dir): options = _full_options(tmp_path, docs_dir) generate(options) diff --git a/tests/test_image_pipeline.py b/tests/test_image_pipeline.py new file mode 100644 index 0000000..528a52b --- /dev/null +++ b/tests/test_image_pipeline.py @@ -0,0 +1,64 @@ +"""Raster asset optimization tests.""" + +from io import BytesIO +from pathlib import Path + +from PIL import Image + +from nmteam_support.image_pipeline import stage_assets + + +def _gradient(mode: str = "RGB") -> Image.Image: + image = Image.new(mode, (32, 32)) + for y in range(32): + for x in range(32): + pixel = (x * 8, y * 8, (x + y) * 4) + image.putpixel((x, y), (*pixel, (x * y) % 256) if mode == "RGBA" else pixel) + return image + + +def test_stage_assets_emits_quality_80_webp_and_jpeg_fallback(tmp_path: Path): + source = tmp_path / "source" + target = tmp_path / "target" + source.mkdir() + image = _gradient() + image.save(source / "photo.jpg", quality=95) + + stage_assets(source, target) + + with Image.open(source / "photo.jpg") as decoded: + expected_jpeg = BytesIO() + decoded.save(expected_jpeg, "JPEG", quality=80, optimize=True, progressive=True) + expected_webp = BytesIO() + decoded.save(expected_webp, "WEBP", quality=80, method=6) + assert (target / "photo.jpg").read_bytes() == expected_jpeg.getvalue() + assert (target / "photo.webp").read_bytes() == expected_webp.getvalue() + + +def test_stage_assets_quantizes_png_fallback_and_preserves_alpha_in_webp(tmp_path: Path): + source = tmp_path / "source" + target = tmp_path / "target" + source.mkdir() + _gradient("RGBA").save(source / "diagram.png") + + stage_assets(source, target) + + with Image.open(target / "diagram.png") as fallback: + assert fallback.mode == "P" + assert fallback.getbands() == ("P",) + with Image.open(target / "diagram.webp") as preferred: + assert preferred.format == "WEBP" + assert "A" in preferred.getbands() + + +def test_stage_assets_copies_non_raster_resources(tmp_path: Path): + source = tmp_path / "source" + target = tmp_path / "target" + (source / "icons").mkdir(parents=True) + (source / "icons" / "doc.svg").write_text("\n", encoding="utf-8") + (source / "site.css").write_text("body {}\n", encoding="utf-8") + + stage_assets(source, target) + + assert (target / "icons" / "doc.svg").read_text(encoding="utf-8") == "\n" + assert (target / "site.css").read_text(encoding="utf-8") == "body {}\n" diff --git a/tests/test_markdown_images.py b/tests/test_markdown_images.py new file mode 100644 index 0000000..7db948a --- /dev/null +++ b/tests/test_markdown_images.py @@ -0,0 +1,39 @@ +"""WebP-first Markdown image rendering tests.""" + +from xml.etree import ElementTree + +from markdown import markdown + +from nmteam_support.markdown_images import WebpPictureExtension + + +def _fragment(source: str) -> ElementTree.Element: + html = markdown(source, extensions=["attr_list", WebpPictureExtension()]) + return ElementTree.fromstring(f"{html}") + + +def test_local_raster_image_renders_webp_before_original_fallback(): + root = _fragment('![配置页面](/assets/images/panel.png){ width="360" }') + + picture = root.find(".//picture") + assert picture is not None + source, fallback = list(picture) + assert source.tag == "source" + assert source.attrib == { + "srcset": "/assets/images/panel.webp", + "type": "image/webp", + } + assert fallback.tag == "img" + assert fallback.attrib["src"] == "/assets/images/panel.png" + assert fallback.attrib["alt"] == "配置页面" + assert fallback.attrib["width"] == "360" + + +def test_external_and_vector_images_remain_plain_images(): + root = _fragment("![远程](https://example.com/image.png)\n\n![图标](/assets/icons/doc.svg)") + + assert root.findall(".//picture") == [] + assert [image.attrib["src"] for image in root.findall(".//img")] == [ + "https://example.com/image.png", + "/assets/icons/doc.svg", + ] diff --git a/uv.lock b/uv.lock index 4fdab5a..3bca790 100644 --- a/uv.lock +++ b/uv.lock @@ -361,8 +361,10 @@ name = "nmteam-support" version = "0.1.0" source = { editable = "." } dependencies = [ + { name = "markdown" }, { name = "mkdocs" }, { name = "mkdocs-material" }, + { name = "pillow" }, { name = "pyyaml" }, { name = "typer" }, ] @@ -378,8 +380,10 @@ dev = [ [package.metadata] requires-dist = [ + { name = "markdown", specifier = ">=3.10" }, { name = "mkdocs", specifier = "==1.6.1" }, { name = "mkdocs-material", specifier = "==9.7.7" }, + { name = "pillow", specifier = ">=12.1.0" }, { name = "pyyaml", specifier = ">=6.0.2" }, { name = "typer", specifier = ">=0.21.1" }, ] @@ -420,6 +424,56 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/f1/d9/7fb5aa316bc299258e68c73ba3bddbc499654a07f151cba08f6153988714/pathspec-1.1.1-py3-none-any.whl", hash = "sha256:a00ce642f577bf7f473932318056212bc4f8bfdf53128c78bbd5af0b9b20b189", size = 57328, upload-time = "2026-04-27T01:46:07.06Z" }, ] +[[package]] +name = "pillow" +version = "12.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1c/3d/bb7fca845737cf9d7dbde16ed1843984665ff2e0a518f5db43e77ec540b9/pillow-12.3.0.tar.gz", hash = "sha256:3b8182a766685eaa002637e28b4ec8d6b18819a0c71f579bf0dbaa5830297cce", size = 47025035, upload-time = "2026-07-01T11:56:38.965Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/dc/01/001f65b68192f0228cc1dbbc8d2530ab5d58b61037ba0587f946fea607cd/pillow-12.3.0-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:9cf95fe4d0f84c82d282745d9bb08ad9f926efa00be4697e767b814ce40d4330", size = 4161736, upload-time = "2026-07-01T11:54:51.156Z" }, + { url = "https://files.pythonhosted.org/packages/1a/d2/0219746d0fd16fc8a84498e79452375be3797d3ce4044596ce565164b84f/pillow-12.3.0-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:8728f216dcdb6e6d555cf971cb34076139ad74b31fc2c14da4fafc741c5f6217", size = 4255435, upload-time = "2026-07-01T11:54:53.414Z" }, + { url = "https://files.pythonhosted.org/packages/c8/02/8d0bc62ef0302318c46ff2a512822d2610e81c7aa46c9b3abe6cbaca5ad0/pillow-12.3.0-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:a45650e8ce7fafffd731db8550230db6b0d306d181a90b67d3e6bca2f1990930", size = 3696262, upload-time = "2026-07-01T11:54:55.739Z" }, + { url = "https://files.pythonhosted.org/packages/85/e2/73c77d218410b14f5f2d565e8a998d5317b7b9c75368d29985139f7a46f0/pillow-12.3.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:ba54cfebe86920a559a7c4d6b9050791c20513650a1952ebe3368c7dc70306f8", size = 5350344, upload-time = "2026-07-01T11:54:57.657Z" }, + { url = "https://files.pythonhosted.org/packages/c7/da/32c752228ae345f489e3a42499d817b6c3996da7e8a3bc7a04fc806b243b/pillow-12.3.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:e158cb00350dc278f3b91551101aa7d12415a66ebf2c91d8d5ac14e56ddd3ad0", size = 4780131, upload-time = "2026-07-01T11:54:59.713Z" }, + { url = "https://files.pythonhosted.org/packages/b1/9d/8b2c807dbef61a5197c047afe99823787eb66f63daf9fb2432f91d6f0462/pillow-12.3.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e9aeb04d6aef139de265b29683e119b638208f88cf73cdd1658aa07221165321", size = 6263757, upload-time = "2026-07-01T11:55:01.778Z" }, + { url = "https://files.pythonhosted.org/packages/5c/44/c85361f65dbe00eea8576ee467c768d25129989efb76e94f205e9ca9bb46/pillow-12.3.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:251bf95b67017e27b13d82f5b326234ca62d70f9cf4c2b9032de2358a3b12c7b", size = 6936962, upload-time = "2026-07-01T11:55:03.93Z" }, + { url = "https://files.pythonhosted.org/packages/18/7e/e483414b35800b86b6f08dbbc7803fb5cd52c4d6f897f47d53ea2c7e6f65/pillow-12.3.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:fe3cca2e4e8a592be0f269a1ca4835c25199d9f3ce815c8491048f785b0a0198", size = 6339171, upload-time = "2026-07-01T11:55:05.989Z" }, + { url = "https://files.pythonhosted.org/packages/f0/f4/68c491844841ede6bed70189546b3ee9731cf9f2cbad396faff5e1ccba45/pillow-12.3.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:23aceaa007d6172b02c277f0cd359c79492bbb14f7072b4ede9fbcaf20648130", size = 7048116, upload-time = "2026-07-01T11:55:08.131Z" }, + { url = "https://files.pythonhosted.org/packages/a3/34/77f3f793fed8efc7d243f21b33c5a3f0d1c97ee70346d3db855587e155ff/pillow-12.3.0-cp314-cp314-win32.whl", hash = "sha256:af8d94b0db561cf68b88a267c5c44b49e134f525d0dc2cb7ed413a66bc23559a", size = 6467209, upload-time = "2026-07-01T11:55:10.408Z" }, + { url = "https://files.pythonhosted.org/packages/f1/e0/492879f69d94f91f60fc8cd05ba03650e9520afebb2fb7aa12777d7c7f38/pillow-12.3.0-cp314-cp314-win_amd64.whl", hash = "sha256:fdafc9cce40277e0f7a0feabce0ee50dd2fa1800f3b38015e51296b5e814048d", size = 7237707, upload-time = "2026-07-01T11:55:12.745Z" }, + { url = "https://files.pythonhosted.org/packages/c9/ac/6b11f2875f1c2ac040d84e1bbf9cf22a88038f901ca1037898b280b38365/pillow-12.3.0-cp314-cp314-win_arm64.whl", hash = "sha256:e91206ee562682b51b98ef4b26a6ef48fd84e15fd4c4bc5ec768eb641d206838", size = 2565995, upload-time = "2026-07-01T11:55:14.736Z" }, + { url = "https://files.pythonhosted.org/packages/52/69/c2208e56af9bfc1913afb24020297a691eb1d4ef688474c8a04913f65e04/pillow-12.3.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:164b31cd1a0490ab6efae01aa5df49da7061be0af1b30e035b6e9a1bfe34ee6e", size = 5352503, upload-time = "2026-07-01T11:55:17.076Z" }, + { url = "https://files.pythonhosted.org/packages/07/70/e5686d753e898a45d778ff1718dba8516ead6ab6b95d85fc8c4b70650cf2/pillow-12.3.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5afb51d599ea772b8365ae807ae557f18bccfe46ab261fd1c2a9ed700fc6eb17", size = 4782956, upload-time = "2026-07-01T11:55:19.448Z" }, + { url = "https://files.pythonhosted.org/packages/d5/37/25c6692f06927ee973ff18c8d9ee98ad0b4d84ee67a09610c2dd1447958e/pillow-12.3.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3edce1d53195db527e0191f84b71d02022de0540bf43a16ed734ed7537b07385", size = 6322855, upload-time = "2026-07-01T11:55:21.613Z" }, + { url = "https://files.pythonhosted.org/packages/cc/91/420637fcb8f1bc11029e403b4538e6694744428d8246118e45719f944556/pillow-12.3.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bf16ba1b4d0b6b7c8e534936632270cf70eb00dbe09005bc345b2677b726855c", size = 6989642, upload-time = "2026-07-01T11:55:24.006Z" }, + { url = "https://files.pythonhosted.org/packages/10/08/b94d7811281ccf0d143a1cf768d1c49e1e54af63e7b708ab2ee3eb87face/pillow-12.3.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:24870b09b224f7ae3c39ed07d10e819d06f8720bc551847b1d623832b5b0e28d", size = 6391281, upload-time = "2026-07-01T11:55:26.252Z" }, + { url = "https://files.pythonhosted.org/packages/d2/87/24233f785f55474dc02ce3e739c5528a77e3a862e9333d1dd7a25cc31f70/pillow-12.3.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:30f2aa603c41533cc25c05acd0da21636e84a315768feb631c937177db558931", size = 7096716, upload-time = "2026-07-01T11:55:28.318Z" }, + { url = "https://files.pythonhosted.org/packages/23/26/fcb2f6e37175b04f53570b59937867e2b80ee1685e744023153028fc14f9/pillow-12.3.0-cp314-cp314t-win32.whl", hash = "sha256:4b0a7fe987b14c31ebda6083f74f22b561fd3739bc0ac51e019622e3d72668c7", size = 6474125, upload-time = "2026-07-01T11:55:30.956Z" }, + { url = "https://files.pythonhosted.org/packages/90/de/3634abee5f1c9e13c56787b7d5517b0ba8d6de51700b95578cf338349c9f/pillow-12.3.0-cp314-cp314t-win_amd64.whl", hash = "sha256:962864dc93511324d51ddbb5b9f8731bf71675b93ca612a07441896f4688fb8c", size = 7242939, upload-time = "2026-07-01T11:55:34.044Z" }, + { url = "https://files.pythonhosted.org/packages/ce/2a/fd13f8eb24de5714a6eb444a3d67e2842c6c576e159a43793adf23051351/pillow-12.3.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0740a512dc522224c77d9aa5a8d70d8b7d73fb91f2c21125d8d025d3b8990e45", size = 2567506, upload-time = "2026-07-01T11:55:35.988Z" }, + { url = "https://files.pythonhosted.org/packages/5d/dc/8fdce34ec725a33c81c6ba122b904d6b9024e50ea9ac7bede62fab54506c/pillow-12.3.0-cp315-cp315-ios_13_0_arm64_iphoneos.whl", hash = "sha256:0feb2e9d6ad6c9e3c06effe9d00f3f1e618a6643273576b016f591e9315a7139", size = 4162063, upload-time = "2026-07-01T11:55:37.941Z" }, + { url = "https://files.pythonhosted.org/packages/76/66/2044b9a63d3b84ff048228dfcb7cd9bf0df983e8470971bf7d4c57b693de/pillow-12.3.0-cp315-cp315-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:9e881fca225083806662a5c43d627d215f258ff43c890f831966c7d7ba9c7402", size = 4255549, upload-time = "2026-07-01T11:55:40.022Z" }, + { url = "https://files.pythonhosted.org/packages/52/7e/1f67e6f4ece6b582ee4b539decbcc9f848dc245a93ed8cd7338bafef72f1/pillow-12.3.0-cp315-cp315-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:4998562bf62a445225f22e07c896bb04b35b1b1f2eb6d760584c9c51d7a5f78c", size = 3696331, upload-time = "2026-07-01T11:55:41.98Z" }, + { url = "https://files.pythonhosted.org/packages/12/40/d306fc2c8e4d45d7f175c77edca7063be7b86fe7fe6e68f4353bf71d808c/pillow-12.3.0-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:dc624f6bc473dacdf7ef7eb8678d0d08edf15cd94fad6ae5c7d6cc67a4e4902f", size = 5350370, upload-time = "2026-07-01T11:55:44.028Z" }, + { url = "https://files.pythonhosted.org/packages/dd/44/668fb1437e8ce420f62d6106eb66e44a5971602a4d794615bdf79315d82d/pillow-12.3.0-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:71d6097b330eea8fd15097780c8e89cb1a8ce7838669f48c5bacd6f663dd4701", size = 4780147, upload-time = "2026-07-01T11:55:46.073Z" }, + { url = "https://files.pythonhosted.org/packages/0c/08/93fa2e70e30a2d81547e481b6ee2bb9522117221fb1e0ce4b5df70967677/pillow-12.3.0-cp315-cp315-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:28ce87c5ab450a9dd970b52e5aca5fe63ed432d18a2eaddd1979a00a1ba24ace", size = 6273659, upload-time = "2026-07-01T11:55:48.264Z" }, + { url = "https://files.pythonhosted.org/packages/f8/6d/043e96ff814fc31a33077e4cba86082167db520c93632afdf2042febbb0c/pillow-12.3.0-cp315-cp315-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6b02afb9b97f65fbca5f31db6a2a3ba21aa93030225f150fa3f249717e938fb4", size = 6947439, upload-time = "2026-07-01T11:55:50.503Z" }, + { url = "https://files.pythonhosted.org/packages/af/92/ba71d2ee2ac0edf3fa33bd9d5ee9ee080da70b1766f3ca3934f9938ddac9/pillow-12.3.0-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:1182d52bc2d5e5d7d0949503aa7e36d12f42205dc287e4883f407b1988820d39", size = 6353577, upload-time = "2026-07-01T11:55:52.697Z" }, + { url = "https://files.pythonhosted.org/packages/0f/ce/e63064e2122923ff687c8ad792d0d736a7b3920a56a46982e81a7fdd25d6/pillow-12.3.0-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:e795b7eb908249c4e43c7c99fac7c2c75dab0c43566e37db472a355f63693d71", size = 7060394, upload-time = "2026-07-01T11:55:55.149Z" }, + { url = "https://files.pythonhosted.org/packages/54/76/a09cc3ccc8d773a7283d34c38bec1708f9e3cc932093cbc4c5e71ac4060b/pillow-12.3.0-cp315-cp315-win32.whl", hash = "sha256:57b3d78c95ba9059768b10e28b813002261d3f3dfc55cc48b0c988f625175827", size = 6467375, upload-time = "2026-07-01T11:55:57.769Z" }, + { url = "https://files.pythonhosted.org/packages/3e/03/1846c49ba3b1d5550392a4bbd06d6fb4578e1cd91a803198b5c90f5f7d53/pillow-12.3.0-cp315-cp315-win_amd64.whl", hash = "sha256:fa4ecea169a355be7a3ade2c783e2ed12f0e40d2c5621cda8b3297faf7fbb9f5", size = 7237048, upload-time = "2026-07-01T11:55:59.975Z" }, + { url = "https://files.pythonhosted.org/packages/fb/bb/89f35dcc79610423f9f195504d7def7f0d1416a711541b42867e25fe3412/pillow-12.3.0-cp315-cp315-win_arm64.whl", hash = "sha256:877c3f311ff35410f690861c4409e7ccbf0cd2f878e50628a28e5a0bb689e658", size = 2566006, upload-time = "2026-07-01T11:56:02.143Z" }, + { url = "https://files.pythonhosted.org/packages/30/88/707027ba09942dfa2c28759b5c222d769290a41c6d20ea60ec250801941f/pillow-12.3.0-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:e9871b1ffbfa9656b60aeee92ed5136a5742696006fa322b29ea3d8da0ecc9cf", size = 5352509, upload-time = "2026-07-01T11:56:04.2Z" }, + { url = "https://files.pythonhosted.org/packages/b0/6d/00352fa25332c2569cd387851f568cc5a4b75a9adbfb37ac4fbce4c02eec/pillow-12.3.0-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:53aa02d20d10c3d814d536aa4e5ac9b84ca0ff5a88377963b085ad6822f93e64", size = 4783167, upload-time = "2026-07-01T11:56:06.631Z" }, + { url = "https://files.pythonhosted.org/packages/13/4f/9e049dfa21af7c22427275720e2490267ba8138120add5c4c574deb69782/pillow-12.3.0-cp315-cp315t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:446c34dcc4324b084a53b705127dc15717b22c5e140ae0a3c38349d4efec071e", size = 6329237, upload-time = "2026-07-01T11:56:08.868Z" }, + { url = "https://files.pythonhosted.org/packages/36/16/cf6eeaae8d0fce8dd390a33437cf68c5d5bd73834a2bc6e2f14efda0ab45/pillow-12.3.0-cp315-cp315t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cf1845d02ad822a369a49f2bb9345b1614744267682e7a03527dc3bf6eea1777", size = 6997047, upload-time = "2026-07-01T11:56:11.379Z" }, + { url = "https://files.pythonhosted.org/packages/1e/69/dbf769bdd55f48bf5733cac28edc6364ffaa072ec9ba336266e4fe66be55/pillow-12.3.0-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:186941b6aef820ad110fb01fb06eb925374dc3a21b17e37ec9a53b250c6fe2d1", size = 6400440, upload-time = "2026-07-01T11:56:13.908Z" }, + { url = "https://files.pythonhosted.org/packages/a0/e1/ffc9cfc2eea0d178da8018e18e959301ad9d6bc9f3edb7181e748a474b97/pillow-12.3.0-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:f13c32a3abd6079a66d9526e18dad9b6d280384d49d7c54040cd57b6424041d9", size = 7105895, upload-time = "2026-07-01T11:56:16.575Z" }, + { url = "https://files.pythonhosted.org/packages/18/f0/a5595c1e8c3ae44b9828cb2f0fa8155e5095ef04d6327b8f61cf44a3df85/pillow-12.3.0-cp315-cp315t-win32.whl", hash = "sha256:1657923d2d45afb66526e5b933e5b3052e6bdea196c90d3abb2424e18c77dae8", size = 6474384, upload-time = "2026-07-01T11:56:18.855Z" }, + { url = "https://files.pythonhosted.org/packages/e4/04/62bcd9f844984c5938d3b05264a61d797a29d3e0812341a8204af70bbdee/pillow-12.3.0-cp315-cp315t-win_amd64.whl", hash = "sha256:8cd2f7bdda092d99c9fc2fb7391354f306d01443d22785d0cbfafa2e2c8bb418", size = 7243537, upload-time = "2026-07-01T11:56:21.214Z" }, + { url = "https://files.pythonhosted.org/packages/3d/68/1f3066acedf37673694a7141381d8f811ae97f30d34413d236abe7d489f1/pillow-12.3.0-cp315-cp315t-win_arm64.whl", hash = "sha256:06ff022112bc9cbf83b60f8e028d94ad87b60621706487e65f673de61610ab59", size = 2567491, upload-time = "2026-07-01T11:56:23.506Z" }, +] + [[package]] name = "platformdirs" version = "4.11.1" From 1083585a1603c2c9ecb032b55141192889aeee7c Mon Sep 17 00:00:00 2001 From: Alice39s Date: Sun, 9 Aug 2026 19:40:25 +0900 Subject: [PATCH 34/57] fix(build): hide Material generator fingerprint --- mkdocs-template.yml | 1 + overrides/main.html | 5 +++++ 2 files changed, 6 insertions(+) create mode 100644 overrides/main.html diff --git a/mkdocs-template.yml b/mkdocs-template.yml index de2acaf..614cf2c 100644 --- a/mkdocs-template.yml +++ b/mkdocs-template.yml @@ -1,6 +1,7 @@ site_name: nmTeam Support theme: name: material + custom_dir: overrides logo: https://websiteres.nmteam.xyz/producticon/nmTeam/logo@128.png favicon: https://websiteres.nmteam.xyz/producticon/nmTeam/logo@128.png language: zh diff --git a/overrides/main.html b/overrides/main.html new file mode 100644 index 0000000..bae7224 --- /dev/null +++ b/overrides/main.html @@ -0,0 +1,5 @@ +{% extends "base.html" %} + +{% block site_meta %} + {{ super() | replace(", mkdocs-material-9.7.7", "") }} +{% endblock %} From b43c92f6d9bfc3f42ab21d6066ac71f6f4ecc435 Mon Sep 17 00:00:00 2001 From: Alice39s Date: Sun, 9 Aug 2026 19:41:48 +0900 Subject: [PATCH 35/57] feat(build): minify generated HTML --- mkdocs-template.yml | 6 ++++++ pyproject.toml | 1 + uv.lock | 37 +++++++++++++++++++++++++++++++++++++ 3 files changed, 44 insertions(+) diff --git a/mkdocs-template.yml b/mkdocs-template.yml index 614cf2c..5147171 100644 --- a/mkdocs-template.yml +++ b/mkdocs-template.yml @@ -1,4 +1,10 @@ site_name: nmTeam Support +plugins: + - search + - minify: + minify_html: true + htmlmin_opts: + remove_comments: true theme: name: material custom_dir: overrides diff --git a/pyproject.toml b/pyproject.toml index 945c77a..06c54e1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,6 +7,7 @@ dependencies = [ "markdown>=3.10", "mkdocs==1.6.1", "mkdocs-material==9.7.7", + "mkdocs-minify-plugin>=0.8.0", "pillow>=12.1.0", "pyyaml>=6.0.2", "typer>=0.21.1", diff --git a/uv.lock b/uv.lock index 3bca790..85bd387 100644 --- a/uv.lock +++ b/uv.lock @@ -98,6 +98,12 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, ] +[[package]] +name = "csscompressor" +version = "0.9.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f1/2a/8c3ac3d8bc94e6de8d7ae270bb5bc437b210bb9d6d9e46630c98f4abd20c/csscompressor-0.9.5.tar.gz", hash = "sha256:afa22badbcf3120a4f392e4d22f9fff485c044a1feda4a950ecc5eba9dd31a05", size = 237808, upload-time = "2017-11-26T21:13:08.238Z" } + [[package]] name = "ghp-import" version = "2.1.0" @@ -110,6 +116,14 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/f7/ec/67fbef5d497f86283db54c22eec6f6140243aae73265799baaaa19cd17fb/ghp_import-2.1.0-py3-none-any.whl", hash = "sha256:8337dd7b50877f163d4c0289bc1f1c7f127550241988d568c1db512c4324a619", size = 11034, upload-time = "2022-05-02T15:47:14.552Z" }, ] +[[package]] +name = "htmlmin2" +version = "0.1.13" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/be/31/a76f4bfa885f93b8167cb4c85cf32b54d1f64384d0b897d45bc6d19b7b45/htmlmin2-0.1.13-py3-none-any.whl", hash = "sha256:75609f2a42e64f7ce57dbff28a39890363bde9e7e5885db633317efbdf8c79a2", size = 34486, upload-time = "2023-03-14T21:28:30.388Z" }, +] + [[package]] name = "idna" version = "3.18" @@ -140,6 +154,12 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" }, ] +[[package]] +name = "jsmin" +version = "3.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5e/73/e01e4c5e11ad0494f4407a3f623ad4d87714909f50b17a06ed121034ff6e/jsmin-3.0.1.tar.gz", hash = "sha256:c0959a121ef94542e807a674142606f7e90214a2b3d1eb17300244bbb5cc2bfc", size = 13925, upload-time = "2022-01-16T20:35:59.13Z" } + [[package]] name = "markdown" version = "3.10.3" @@ -347,6 +367,21 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/5b/54/662a4743aa81d9582ee9339d4ffa3c8fd40a4965e033d77b9da9774d3960/mkdocs_material_extensions-1.3.1-py3-none-any.whl", hash = "sha256:adff8b62700b25cb77b53358dad940f3ef973dd6db797907c49e3c2ef3ab4e31", size = 8728, upload-time = "2023-11-22T19:09:43.465Z" }, ] +[[package]] +name = "mkdocs-minify-plugin" +version = "0.8.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "csscompressor" }, + { name = "htmlmin2" }, + { name = "jsmin" }, + { name = "mkdocs" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/52/67/fe4b77e7a8ae7628392e28b14122588beaf6078b53eb91c7ed000fd158ac/mkdocs-minify-plugin-0.8.0.tar.gz", hash = "sha256:bc11b78b8120d79e817308e2b11539d790d21445eb63df831e393f76e52e753d", size = 8366, upload-time = "2024-01-29T16:11:32.982Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1b/cd/2e8d0d92421916e2ea4ff97f10a544a9bd5588eb747556701c983581df13/mkdocs_minify_plugin-0.8.0-py3-none-any.whl", hash = "sha256:5fba1a3f7bd9a2142c9954a6559a57e946587b21f133165ece30ea145c66aee6", size = 6723, upload-time = "2024-01-29T16:11:31.851Z" }, +] + [[package]] name = "more-itertools" version = "11.1.0" @@ -364,6 +399,7 @@ dependencies = [ { name = "markdown" }, { name = "mkdocs" }, { name = "mkdocs-material" }, + { name = "mkdocs-minify-plugin" }, { name = "pillow" }, { name = "pyyaml" }, { name = "typer" }, @@ -383,6 +419,7 @@ requires-dist = [ { name = "markdown", specifier = ">=3.10" }, { name = "mkdocs", specifier = "==1.6.1" }, { name = "mkdocs-material", specifier = "==9.7.7" }, + { name = "mkdocs-minify-plugin", specifier = ">=0.8.0" }, { name = "pillow", specifier = ">=12.1.0" }, { name = "pyyaml", specifier = ">=6.0.2" }, { name = "typer", specifier = ">=0.21.1" }, From 141db170be64c18264f5657b9482ea005e639a8b Mon Sep 17 00:00:00 2001 From: Alice39s Date: Sun, 9 Aug 2026 19:42:15 +0900 Subject: [PATCH 36/57] docs(build): document the asset pipeline --- README.md | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 61f22c5..9d3329f 100644 --- a/README.md +++ b/README.md @@ -31,7 +31,7 @@ uv run nmteam dev - 自动生成文档结构(`cache/`、`generated/`、`mkdocs.yml`) - 启动 MkDocs 开发服务器() -- 监听 `docs/` 目录变化并自动重新生成,浏览器热更新 +- 监听 `docs/`、`assets/` 和 MkDocs 模板变化并自动重新生成,浏览器热更新 ### 构建生产版本 @@ -41,6 +41,26 @@ uv run nmteam build 构建结果输出到 `site/` 目录。 +最终 HTML 会由 `mkdocs-minify-plugin` 压缩;生成器元标签仅保留 MkDocs +版本,不暴露主题及其版本。 + +## 静态资源 + +仓库内资源统一存放在: + +- `assets/images/`:PNG、JPEG 图片母版 +- `assets/icons/`:SVG 图标 +- `assets/styles/`:站点样式 + +文档使用 `/assets/...` 引用这些资源。生成文档时,每张 PNG 或 JPEG +图片会同时产生: + +- WebP 优先版本:质量 80 +- 原格式 fallback:JPEG 使用质量 80;PNG 使用 256 色有损量化 + +Markdown 中仍使用普通图片语法,构建工具会自动输出 WebP +优先的 `` 元素。外部 URL 不会被下载或镜像。 + ### 其他命令 ```bash From f2d22030c223874e201f3c62784d419cef887abb Mon Sep 17 00:00:00 2001 From: Alice39s Date: Sun, 9 Aug 2026 20:47:46 +0900 Subject: [PATCH 37/57] fix(build): preserve HTML attribute quotes --- mkdocs-template.yml | 2 ++ tests/test_build_config.py | 22 ++++++++++++++++++++++ 2 files changed, 24 insertions(+) create mode 100644 tests/test_build_config.py diff --git a/mkdocs-template.yml b/mkdocs-template.yml index 5147171..a7fff67 100644 --- a/mkdocs-template.yml +++ b/mkdocs-template.yml @@ -5,6 +5,8 @@ plugins: minify_html: true htmlmin_opts: remove_comments: true + remove_optional_attribute_quotes: false + reduce_empty_attributes: false theme: name: material custom_dir: overrides diff --git a/tests/test_build_config.py b/tests/test_build_config.py new file mode 100644 index 0000000..4ffb1b8 --- /dev/null +++ b/tests/test_build_config.py @@ -0,0 +1,22 @@ +"""Production HTML minification configuration tests.""" + +from pathlib import Path + +import htmlmin +import yaml + + +def test_html_minification_preserves_attribute_quotes_and_removes_comments(): + repo_root = Path(__file__).resolve().parents[1] + config = yaml.safe_load((repo_root / "mkdocs-template.yml").read_text(encoding="utf-8")) + minify = next(plugin["minify"] for plugin in config["plugins"] if "minify" in plugin) + source = ( + '
Copyright
' + ) + + output = htmlmin.minify(source, **minify["htmlmin_opts"]) + + assert 'class="md-footer"' in output + assert 'data-empty=""' in output + assert "internal" not in output + assert len(output) < len(source) From 25f5cbbaa1899f6d7f0077973b65900871dacde3 Mon Sep 17 00:00:00 2001 From: Alice39s Date: Sun, 9 Aug 2026 21:07:30 +0900 Subject: [PATCH 38/57] docs: add repository guidelines (AGENTS.md) --- AGENTS.md | 133 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 133 insertions(+) create mode 100644 AGENTS.md diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..11e35cf --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,133 @@ +# Repository Guidelines + +## Project Overview + +nmTeam Support 官方支持文档站(https://support.nmteam.xyz)的构建工具链。纯文档 + Python 工具链仓库:内容为简体中文,覆盖 nmBot(Telegram 机器人)及其 Plus / Intelligence 产品线;工具链负责扫描 `docs/` 与 `assets/`,自动生成 mkdocs 目录结构(nav)、各目录 `index.md`、图片 WebP 优化与重定向脚本,再用 `mkdocs-material` 构建静态站。 + +本项目是 main 分支旧版(单文件 `generate.py`/`manage.py`/`redirects_manager.py` + `runtime.txt` 3.7 + pip)的现代化重构:**uv 管理、Python 3.14+、`src/` 包结构、Typer CLI、pytest 测试、ruff/mdformat 检查、三平台 CI**。 + +## Architecture & Data Flow + +数据流(四输入 → 生成 → 构建): + +``` +docs/ (markdown 源) ─┐ +assets/ (images/icons/styles 母版) ─┤ +mkdocs-template.yml ─┤ generate() → cache/ ─copytree→ generated/ (mkdocs docs_dir) → mkdocs build --strict → site/ +redirects.json ──────┘ (含生成的 assets/js/redirects.js) +``` + +`generate()`(`src/nmteam_support/generator.py`)编排的完整链路: + +1. 清空重建 `cache/` +2. `scanner.scan_docs()` 递归扫描 `docs/` 得到文档树(`SKIP_DIRS`/`INTERNAL_DIRS` 过滤) +3. 写入处理后的页面(非 index.md 注入贡献提示,`contributing.py`) +4. `index.py` 为每目录生成 `index.md`(自动 generated 标记 + docsList 卡片,`docslist.py` 渲染 HTML) +5. `image_pipeline.py` 用 PIL 为 assets 中每张 PNG/JPEG 生成 `.webp` 兄弟文件(质量 80;PNG 另出 256 色有损 fallback) +6. `nav.py` 生成 nav YAML,`template.py` 替换 `mkdocs-template.yml` 的 `# NAV_ARIA_START`/`# NAV_ARIA_END` 标记块写入 `mkdocs.yml` +7. `redirects.py` 读 `redirects.json` 生成 `cache/assets/js/redirects.js` +8. copytree 到 `generated/`,交给 `mkdocs build --strict` + +**图片双层管线**(关键机制):生成期 `image_pipeline.py` 产出同名 `.webp` 兄弟文件;渲染期 `markdown_images.py`(mkdocs 扩展,注册在 mkdocs-template.yml 的 markdown_extensions 中)把本地栅格图 `` 改写为 WebP-first ``,靠同名 `.webp` 约定对接。外部 URL 不下载不镜像。Markdown 中仍写普通图片语法。 + +`mkdocs.yml`、`cache/`、`generated/`、`site/` 均为生成产物(gitignore),**永远不要手改**;配置改 `mkdocs-template.yml`,nav 由脚本生成。 + +## Key Directories + +| 路径 | 用途 | +|---|---| +| `src/nmteam_support/` | 工具链包(15 个模块,见 Important Files) | +| `docs/` | **真实文档源**(唯一需要手工编辑的内容位置) | +| `docs/nmbot-telegram/` | 产品中枢:`panel/`、`plus/`、`legal/`、`group/`、`faq/`、`business/`、`tools/`、`nmbot-intelligence/`、`credit/`、`update-log/`(`YYYY-MM.md` 月度日志)、`mcp/` | +| `docs/contact-us/`、`docs/nmteam-account/` | 其他产品线 | +| `docs/superpowers/` | 本地设计与实现工件(plans/specs),**已 gitignore,勿提交** | +| `assets/images/` | 图片母版:`shared/`(站级共享)、`nmbot/`(含 `mcp/`、`update-pictures/` 子目录);`assets/icons/`(SVG)、`assets/styles/`(CSS) | +| `overrides/` | mkdocs `custom_dir`,`main.html` 覆写 site_meta 移除主题版本号 | +| `scripts/` | 三平台薄启动器(`nmteam.sh` / `nmteam.ps1` / `nmteam.bat`) | +| `tests/` | pytest 测试(15 个文件 + conftest.py) | +| `cache/`、`generated/`、`site/`、`mkdocs.yml` | 生成产物,勿手改勿提交 | + +## Development Commands + +```bash +uv sync # 安装依赖(按 .python-version 3.14 自动管理 Python) +uv run nmteam install # 同上,CLI 封装 +uv run nmteam dev # 生成文档结构 + mkdocs serve http://127.0.0.1:8000, + # 监听 docs/、assets/、mkdocs-template.yml 变化自动重生成 + 热更新 +uv run nmteam build # generate() + mkdocs build --strict → site/ +uv run nmteam generate # 仅重新生成文档结构 +uv run nmteam clean # 清理 cache/ generated/ site/ +uv run nmteam check # 全部质量检查(见下) +uv run nmteam redirects list|add "/old/" "/new/"|remove "/old/" +uv run nmteam --help +``` + +平台启动器(定位仓库根后原样透传参数,无业务逻辑):`scripts/nmteam.sh dev`、`.\scripts\nmteam.ps1 dev`、`scripts\nmteam.bat dev`。 + +**质量检查**(`nmteam check`,CI 同样执行): +```bash +uv run ruff check . +uv run ruff format --check . +uv run pytest +uv run mdformat --check README.md docs/ +uv run nmteam build # 内部为 mkdocs build --strict,严格模式会因警告失败 +``` + +## Code Conventions & Common Patterns + +Python(`src/nmteam_support/`): + +- Python 3.14+(`pyproject.toml` `requires-python = ">=3.14"`,`.python-version` = 3.14)。 +- CLI 用 **Typer**(`cli.py`,命令树 + `QUALITY_COMMANDS` 编排),不用 argparse。 +- 核心模型用 `frozen dataclass`(`models.py`:`PageMetadata`/`DocEntry`);类型标注齐全(`typing`)。 +- ruff 约束:`line-length = 100`(`E501` ignore)、`target py314`、`select = E,F,I,UP,B,SIM,W,C4,RUF`、`isort known-first-party = nmteam_support`、双引号、空格缩进。 +- 错误处理显式:如 `redirects.py` 对损坏的 `redirects.json` 抛 `RedirectConfigError`(绝不静默清空覆盖,管理命令不写坏文件)。 +- 依赖注入轻量:`GeneratorOptions`/`default_options` 传参,函数式模块(scanner/frontmatter/nav/docslist 等),无重 OOP。 + +Markdown 文档(`docs/`): + +- front matter 用 YAML:`title` + `description`(description 供列表卡片);可选 `index: ` 排序;`index.md` 可用 `hide_docs_list: true` 与 `hide: [navigation]`。 +- 常用 `!!! note` / `!!! warning` / `!!! success` / `!!! faq` admonition 与 `??? question` 折叠块;表格(对齐列 `:---`);步骤用有序列表。 +- 付费功能用 `` 行内徽标(样式 `assets/styles/plus.css`,远程 svg);智能功能可用 `nmbot-intelligence-icon`。 +- **图片一律绝对路径引用 `/assets/images/...`**(不要用相对 `./img/`),构建时自动产出 WebP 优先 ``;logo 等品牌资源用外部 URL(websiteres.nmteam.xyz)。 +- 更新日志:`docs/nmbot-telegram/update-log/YYYY-MM.md`,标题 `# nmBot YYYY 年 M 月更新`,`## 日期 时间` + `### 问题修复` / `### 新功能` 列表;配图放 `assets/images/nmbot/update-pictures/nmbot-YYMM.png`。 +- Markdown 由 **mdformat** 统一格式(`.mdformat.toml`:wrap=keep、LF 行尾;插件 mdformat-mkdocs + mdformat-front-matters 保护 admonition 与 front matter,不需要手写 extensions 列表)。 + +## Important Files + +| 文件 | 职责 | +|---|---| +| `src/nmteam_support/cli.py` | Typer 入口 `main`;install/dev/generate/build/clean/check + redirects 子命令 | +| `src/nmteam_support/generator.py` | `generate()` 端到端编排 + `GeneratorOptions`/`default_options` | +| `src/nmteam_support/scanner.py` | 递归扫描 docs/ 树,SKIP_DIRS/INTERNAL_DIRS 过滤 | +| `src/nmteam_support/frontmatter.py` | frontmatter 解析(title/description/index/flag) | +| `src/nmteam_support/index.py` + `docslist.py` | 每目录 index.md 生成 + docsList 卡片 HTML | +| `src/nmteam_support/nav.py` + `template.py` | nav YAML 生成;NAV_ARIA 标记块替换写 mkdocs.yml | +| `src/nmteam_support/contributing.py` | 非 index.md 注入贡献提示 admonition | +| `src/nmteam_support/redirects.py` | redirects.json 管理(损坏保护)+ redirects.js 生成 | +| `src/nmteam_support/image_pipeline.py` + `markdown_images.py` | PIL 生成 .webp 兄弟文件;mkdocs 扩展包 WebP-first `` | +| `src/nmteam_support/models.py` | `PageMetadata`/`DocEntry` frozen dataclass | +| `pyproject.toml` | 包元数据、依赖、入口、pytest/ruff/hatchling 配置 | +| `uv.lock` | 锁定依赖(mkdocs 1.6.1、mkdocs-material 9.7.7、mkdocs-minify-plugin 0.8.0、pillow 12.3.0、typer 0.27.1、pytest 9.1.1、ruff 0.16.2、mdformat 1.0.0 等) | +| `mkdocs-template.yml` | mkdocs 配置模板(material zh 黄色双 palette、minify 插件、custom_dir overrides、`nmteam_support.markdown_images` 扩展、NAV_ARIA 标记) | +| `redirects.json` | 顶层 `redirects` 对象:`{旧路径带斜杠: 新路径}` | +| `.github/workflows/ci.yml` | 三 OS 矩阵 CI(push main/dev + PR):uv sync --frozen → nmteam check → 验证三个启动器 | +| `.mdformat.toml` | mdformat 配置(wrap=keep、LF) | +| `overrides/main.html` | 移除 meta 中 mkdocs-material 版本号 | + +## Runtime/Tooling Preferences + +- **uv** 为唯一包管理器(`uv sync`,lockfile `uv.lock`);Python 由 uv 按 `.python-version`(3.14)自动管理,不手动装。 +- 构建后端 **hatchling**;无 Node/npm 组件。 +- 强制检查:**ruff**(lint + format)+ **mdformat** + **pytest** + **mkdocs strict build**,全部由 `uv run nmteam check` 一键执行,CI 三平台门禁。 +- gitattributes:`*.md` 强制 LF。 +- 许可证 MIT(Copyright 2022 nmTeam)。 + +## Testing & QA + +- **pytest**(dev 组 `pytest>=9.0`),配置在 pyproject.toml:`testpaths = ["tests"]`、`pythonpath = ["src"]`、`addopts = "-ra"`。 +- 测试约定:`tests/` 单层目录,每个模块一个 `test_.py`(test_cli、test_generator、test_scanner、test_frontmatter、test_nav、test_index、test_docslist、test_contributing、test_redirects、test_image_pipeline、test_markdown_images、test_models、test_scripts、test_build_config 等)。 +- 输入构造分级:conftest 的 `docs_dir` fixture(tmp_path 构造最小 docs 树,含 index/hide_docs_list/hide frontmatter)→ `tmp_path` 手写单文件 → 全流程 generate 集成。**不依赖真实 docs/ 内容**。 +- CLI 测试用 `typer.testing.CliRunner` + monkeypatch;启动器测试用 subprocess + 假 uv 脚本。 +- **无覆盖率门槛**(无 pytest-cov、CI 无 coverage 步骤)——新增功能时给模块补 `test_.py` 行为测试即可。 +- CI 在 ubuntu/macos/windows 三平台跑全量 check;提交前本地至少跑 `uv run nmteam check`。 From 94bb7fc5eeaec67c65ebe7d0a45513d085655699 Mon Sep 17 00:00:00 2001 From: Alice39s Date: Sun, 9 Aug 2026 21:24:45 +0900 Subject: [PATCH 39/57] feat(ai): support llms.txt, per-page Markdown and ChatGPT/Claude open --- AGENTS.md | 23 ++++++--- README.md | 14 ++++++ assets/js/ai-tools.js | 85 +++++++++++++++++++++++++++++++++ assets/styles/ai-tools.css | 39 +++++++++++++++ mkdocs-template.yml | 5 +- overrides/partials/actions.html | 32 +++++++++++++ pyproject.toml | 2 +- src/nmteam_support/cli.py | 15 +++++- src/nmteam_support/generator.py | 21 +++++++- src/nmteam_support/llms.py | 74 ++++++++++++++++++++++++++++ src/nmteam_support/template.py | 7 +++ tests/test_generator.py | 30 +++++++++++- tests/test_llms.py | 42 ++++++++++++++++ 13 files changed, 376 insertions(+), 13 deletions(-) create mode 100644 assets/js/ai-tools.js create mode 100644 assets/styles/ai-tools.css create mode 100644 overrides/partials/actions.html create mode 100644 src/nmteam_support/llms.py create mode 100644 tests/test_llms.py diff --git a/AGENTS.md b/AGENTS.md index 11e35cf..6b689a1 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -17,6 +17,10 @@ mkdocs-template.yml ─┤ generate() → cache/ ─copytree→ generated/ (mkdo redirects.json ──────┘ (含生成的 assets/js/redirects.js) ``` +构建后还有一步:`stage_markdown_copies()` 把 `cache/` 下每个 `.md` 复制到 `site/` +同路径(如 `site/nmbot-telegram/mcp.md`),作为每页的 Markdown 版本,供 +`/llms.txt` 链接、View-as-Markdown 按钮与 ChatGPT/Claude 打开。 + `generate()`(`src/nmteam_support/generator.py`)编排的完整链路: 1. 清空重建 `cache/` @@ -26,7 +30,8 @@ redirects.json ──────┘ (含生成的 assets/js/redirects.js 5. `image_pipeline.py` 用 PIL 为 assets 中每张 PNG/JPEG 生成 `.webp` 兄弟文件(质量 80;PNG 另出 256 色有损 fallback) 6. `nav.py` 生成 nav YAML,`template.py` 替换 `mkdocs-template.yml` 的 `# NAV_ARIA_START`/`# NAV_ARIA_END` 标记块写入 `mkdocs.yml` 7. `redirects.py` 读 `redirects.json` 生成 `cache/assets/js/redirects.js` -8. copytree 到 `generated/`,交给 `mkdocs build --strict` +8. `llms.py` 用扫描树 + 模板 `site_url` 生成 `cache/llms.txt`(llmstxt.org 规范:H1 + blockquote + H2 分节链接,链接指向 `.md` 版本;非 md 文件会被 mkdocs 原样拷到 `site/llms.txt`) +9. copytree 到 `generated/`,交给 `mkdocs build --strict` **图片双层管线**(关键机制):生成期 `image_pipeline.py` 产出同名 `.webp` 兄弟文件;渲染期 `markdown_images.py`(mkdocs 扩展,注册在 mkdocs-template.yml 的 markdown_extensions 中)把本地栅格图 `` 改写为 WebP-first ``,靠同名 `.webp` 约定对接。外部 URL 不下载不镜像。Markdown 中仍写普通图片语法。 @@ -36,15 +41,15 @@ redirects.json ──────┘ (含生成的 assets/js/redirects.js | 路径 | 用途 | |---|---| -| `src/nmteam_support/` | 工具链包(15 个模块,见 Important Files) | +| `src/nmteam_support/` | 工具链包(16 个模块,见 Important Files) | | `docs/` | **真实文档源**(唯一需要手工编辑的内容位置) | | `docs/nmbot-telegram/` | 产品中枢:`panel/`、`plus/`、`legal/`、`group/`、`faq/`、`business/`、`tools/`、`nmbot-intelligence/`、`credit/`、`update-log/`(`YYYY-MM.md` 月度日志)、`mcp/` | | `docs/contact-us/`、`docs/nmteam-account/` | 其他产品线 | | `docs/superpowers/` | 本地设计与实现工件(plans/specs),**已 gitignore,勿提交** | -| `assets/images/` | 图片母版:`shared/`(站级共享)、`nmbot/`(含 `mcp/`、`update-pictures/` 子目录);`assets/icons/`(SVG)、`assets/styles/`(CSS) | -| `overrides/` | mkdocs `custom_dir`,`main.html` 覆写 site_meta 移除主题版本号 | +| `assets/images/` | 图片母版:`shared/`(站级共享)、`nmbot/`(含 `mcp/`、`update-pictures/` 子目录);`assets/icons/`(SVG)、`assets/styles/`(CSS)、`assets/js/`(AI 工具脚本 `ai-tools.js`,随构建 stage 到生成站) | +| `overrides/` | mkdocs `custom_dir`:`main.html` 覆写 site_meta 移除主题版本号;`partials/actions.html` 追加 AI 工具按钮组(Markdown / ChatGPT / Claude,毛玻璃样式在 `assets/styles/ai-tools.css`,交互在 `assets/js/ai-tools.js`) | | `scripts/` | 三平台薄启动器(`nmteam.sh` / `nmteam.ps1` / `nmteam.bat`) | -| `tests/` | pytest 测试(15 个文件 + conftest.py) | +| `tests/` | pytest 测试(16 个文件 + conftest.py) | | `cache/`、`generated/`、`site/`、`mkdocs.yml` | 生成产物,勿手改勿提交 | ## Development Commands @@ -54,7 +59,8 @@ uv sync # 安装依赖(按 .python-version 3.14 自 uv run nmteam install # 同上,CLI 封装 uv run nmteam dev # 生成文档结构 + mkdocs serve http://127.0.0.1:8000, # 监听 docs/、assets/、mkdocs-template.yml 变化自动重生成 + 热更新 -uv run nmteam build # generate() + mkdocs build --strict → site/ +uv run nmteam build # generate() + mkdocs build --strict → site/, + # 再 stage 每页 .md 副本到 site/ uv run nmteam generate # 仅重新生成文档结构 uv run nmteam clean # 清理 cache/ generated/ site/ uv run nmteam check # 全部质量检查(见下) @@ -106,6 +112,7 @@ Markdown 文档(`docs/`): | `src/nmteam_support/contributing.py` | 非 index.md 注入贡献提示 admonition | | `src/nmteam_support/redirects.py` | redirects.json 管理(损坏保护)+ redirects.js 生成 | | `src/nmteam_support/image_pipeline.py` + `markdown_images.py` | PIL 生成 .webp 兄弟文件;mkdocs 扩展包 WebP-first `` | +| `src/nmteam_support/llms.py` | `render_llms_txt()` 从扫描树生成 `/llms.txt`(llmstxt.org 规范;链接指向各页 `.md` 版本) | | `src/nmteam_support/models.py` | `PageMetadata`/`DocEntry` frozen dataclass | | `pyproject.toml` | 包元数据、依赖、入口、pytest/ruff/hatchling 配置 | | `uv.lock` | 锁定依赖(mkdocs 1.6.1、mkdocs-material 9.7.7、mkdocs-minify-plugin 0.8.0、pillow 12.3.0、typer 0.27.1、pytest 9.1.1、ruff 0.16.2、mdformat 1.0.0 等) | @@ -114,6 +121,9 @@ Markdown 文档(`docs/`): | `.github/workflows/ci.yml` | 三 OS 矩阵 CI(push main/dev + PR):uv sync --frozen → nmteam check → 验证三个启动器 | | `.mdformat.toml` | mdformat 配置(wrap=keep、LF) | | `overrides/main.html` | 移除 meta 中 mkdocs-material 版本号 | +| `overrides/partials/actions.html` | 覆盖 material actions partial:保留编辑/查看按钮,追加 AI 工具按钮组 | +| `assets/js/ai-tools.js` | 按钮交互:View-as-Markdown 链接(根相对 `.md` 路径)、ChatGPT/Claude 点击后 fetch 页面 `.md` 内容作为提示词打开(fetch 失败降级为仅 URL) | +| `assets/styles/ai-tools.css` | 毛玻璃按钮样式(半透明 + backdrop-filter blur + 柔和阴影,适配明暗主题) | ## Runtime/Tooling Preferences @@ -127,6 +137,7 @@ Markdown 文档(`docs/`): - **pytest**(dev 组 `pytest>=9.0`),配置在 pyproject.toml:`testpaths = ["tests"]`、`pythonpath = ["src"]`、`addopts = "-ra"`。 - 测试约定:`tests/` 单层目录,每个模块一个 `test_.py`(test_cli、test_generator、test_scanner、test_frontmatter、test_nav、test_index、test_docslist、test_contributing、test_redirects、test_image_pipeline、test_markdown_images、test_models、test_scripts、test_build_config 等)。 +- `test_llms.py` 覆盖 llms.txt 生成(标题/摘要/分节/.md 链接/base_url/空树);`test_generator.py` 覆盖 llms.txt 落盘与 `stage_markdown_copies` 镜像 cache。 - 输入构造分级:conftest 的 `docs_dir` fixture(tmp_path 构造最小 docs 树,含 index/hide_docs_list/hide frontmatter)→ `tmp_path` 手写单文件 → 全流程 generate 集成。**不依赖真实 docs/ 内容**。 - CLI 测试用 `typer.testing.CliRunner` + monkeypatch;启动器测试用 subprocess + 假 uv 脚本。 - **无覆盖率门槛**(无 pytest-cov、CI 无 coverage 步骤)——新增功能时给模块补 `test_.py` 行为测试即可。 diff --git a/README.md b/README.md index 9d3329f..a5c36b3 100644 --- a/README.md +++ b/README.md @@ -61,6 +61,20 @@ uv run nmteam build Markdown 中仍使用普通图片语法,构建工具会自动输出 WebP 优先的 `` 元素。外部 URL 不会被下载或镜像。 +## AI 支持 + +站点面向语言模型提供以下能力: + +- `/llms.txt`:按 [llmstxt.org](https://llmstxt.org/) 规范生成的站点索引, + 每个链接指向页面的 Markdown 版本。 +- 每页 Markdown 版本:`nmteam build` 时在每个页面旁生成同路径的 `.md` 文件 + (如 `/nmbot-telegram/mcp.md`)。 +- 页面顶部的 AI 工具按钮:**Markdown**(查看本页 Markdown)、**ChatGPT** / + **Claude**(将本页内容作为上下文在 ChatGPT/Claude 中打开)。 + +注意:`llms.txt` 与 `.md` 版本由 `nmteam build` 输出到 `site/`;开发模式 +(`nmteam dev`)下 ChatGPT/Claude 按钮会退化为仅携带页面链接的提示词。 + ### 其他命令 ```bash diff --git a/assets/js/ai-tools.js b/assets/js/ai-tools.js new file mode 100644 index 0000000..e1ec4cc --- /dev/null +++ b/assets/js/ai-tools.js @@ -0,0 +1,85 @@ +// AI tools: view-as-Markdown, open in ChatGPT / Claude. +// The page's raw Markdown twin lives at the same docs-relative path with a +// ".md" suffix (e.g. /nmbot-telegram/mcp.md); it is staged into site/ by the +// build. When the twin is missing (e.g. `mkdocs serve`), ChatGPT/Claude fall +// back to a prompt that references the page URL instead of its content. +(function () { + "use strict"; + + var ENDPOINTS = { + chatgpt: "https://chatgpt.com/?q=", + claude: "https://claude.ai/new?q=", + }; + // Keep the prompt URL short enough for chat providers to accept. + var MAX_PROMPT = 60000; + + function mdUrl(raw) { + // Root-relative: a relative path would resolve against the page URL, + // e.g. /nmbot-telegram/mcp/ + "nmbot-telegram/mcp.md" -> wrong twin. + // The home page has an empty page.url and its twin is /index.md + // (already carries the ".md" suffix). + if (!raw) { + return "/index.md"; + } + return "/" + raw.replace(/\/+$/, "") + ".md"; + } + + function pageUrl(raw) { + return location.origin + "/" + (raw || ""); + } + + function buildPrompt(content, url) { + if (!content) { + return "请阅读此文档页面并回答我的问题:" + url; + } + var header = "以下是 support.nmteam.xyz 文档页面的内容,请基于此内容回答我的问题:\n\n"; + var body = content; + if (body.length > MAX_PROMPT) { + body = body.slice(0, MAX_PROMPT) + "\n\n…(内容过长已截断)"; + } + return header + body; + } + + function openAi(which, prompt) { + window.open(ENDPOINTS[which] + encodeURIComponent(prompt), "_blank", "noopener"); + } + + function wireFetch(button, which, md, url) { + button.addEventListener("click", function (event) { + event.preventDefault(); + fetch(md) + .then(function (response) { + return response.ok ? response.text() : ""; + }) + .catch(function () { + return ""; + }) + .then(function (content) { + openAi(which, buildPrompt(content, url)); + }); + }); + } + + document.addEventListener("DOMContentLoaded", function () { + var box = document.querySelector(".ai-tools"); + if (!box) { + return; + } + var raw = box.getAttribute("data-md-url") || ""; + var md = mdUrl(raw); + var url = pageUrl(raw); + + var markdownLink = box.querySelector('[data-ai="markdown"]'); + if (markdownLink) { + markdownLink.setAttribute("href", md); + } + var chatgpt = box.querySelector('[data-ai="chatgpt"]'); + if (chatgpt) { + wireFetch(chatgpt, "chatgpt", md, url); + } + var claude = box.querySelector('[data-ai="claude"]'); + if (claude) { + wireFetch(claude, "claude", md, url); + } + }); +})(); diff --git a/assets/styles/ai-tools.css b/assets/styles/ai-tools.css new file mode 100644 index 0000000..6c868fa --- /dev/null +++ b/assets/styles/ai-tools.css @@ -0,0 +1,39 @@ +/* AI tools: view-as-Markdown, open in ChatGPT / Claude. + Glassmorphism style (frosted translucent pill buttons), adapting to the + Material light/dark palettes via theme CSS variables. */ +.ai-tools { + display: inline-flex; + gap: 6px; + margin-inline-start: 8px; + vertical-align: middle; +} + +.ai-tools__link { + display: inline-flex; + align-items: center; + padding: 4px 12px; + font-size: 12px; + line-height: 1.6; + color: var(--md-typeset-color); + background: color-mix(in srgb, var(--md-default-bg-color) 62%, transparent); + -webkit-backdrop-filter: blur(12px) saturate(160%); + backdrop-filter: blur(12px) saturate(160%); + border: 1px solid color-mix(in srgb, var(--md-default-fg-color) 22%, transparent); + border-radius: 999px; + box-shadow: 0 1px 4px color-mix(in srgb, var(--md-default-fg-color) 12%, transparent); + cursor: pointer; + text-decoration: none; + transition: background .2s, color .2s, border-color .2s, box-shadow .2s; +} + +.ai-tools__link:hover { + color: var(--md-primary-fg-color); + background: color-mix(in srgb, var(--md-default-bg-color) 88%, var(--md-primary-fg-color)); + border-color: color-mix(in srgb, var(--md-primary-fg-color) 45%, transparent); + box-shadow: 0 2px 10px color-mix(in srgb, var(--md-primary-fg-color) 25%, transparent); +} + +.ai-tools__link:focus-visible { + outline: 2px solid var(--md-primary-fg-color); + outline-offset: 2px; +} diff --git a/mkdocs-template.yml b/mkdocs-template.yml index a7fff67..6ea2ea3 100644 --- a/mkdocs-template.yml +++ b/mkdocs-template.yml @@ -1,4 +1,5 @@ site_name: nmTeam Support +site_url: https://support.nmteam.xyz plugins: - search - minify: @@ -33,8 +34,8 @@ theme: - navigation.sections - toc.integrate - navigation.top -extra_css: [assets/styles/docsList.css, assets/styles/plus.css] -extra_javascript: [assets/js/redirects.js] +extra_css: [assets/styles/docsList.css, assets/styles/plus.css, assets/styles/ai-tools.css] +extra_javascript: [assets/js/redirects.js, assets/js/ai-tools.js] docs_dir: generated extra: diff --git a/overrides/partials/actions.html b/overrides/partials/actions.html new file mode 100644 index 0000000..0cba32f --- /dev/null +++ b/overrides/partials/actions.html @@ -0,0 +1,32 @@ +{#- + Overrides Material's actions partial: keeps the edit/view source buttons + (unused today, no repo_url is configured) and adds the AI tools button + group (view as Markdown, open in ChatGPT / Claude). +-#} +{% if page.edit_url %} + {% if "content.action.edit" in features %} + + {% set icon = config.theme.icon.edit or "material/file-edit-outline" %} + {% include ".icons/" ~ icon ~ ".svg" %} + + {% endif %} + {% if "content.action.view" in features %} + {% if "/blob/" in page.edit_url %} + {% set part = "blob" %} + {% else %} + {% set part = "edit" %} + {% endif %} + + {% set icon = config.theme.icon.view or "material/file-eye-outline" %} + {% include ".icons/" ~ icon ~ ".svg" %} + + {% endif %} +{% endif %} + +{% if page %} +
+ Markdown + + +
+{% endif %} diff --git a/pyproject.toml b/pyproject.toml index 06c54e1..bad984b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -51,7 +51,7 @@ exclude = ["docs"] [tool.ruff.lint] select = ["E", "F", "I", "UP", "B", "SIM", "W", "C4", "RUF"] ignore = ["E501"] -allowed-confusables = [",", "。", "?", ":", "!"] +allowed-confusables = [",", "。", "?", ":", ";", "!"] [tool.ruff.lint.isort] known-first-party = ["nmteam_support"] diff --git a/src/nmteam_support/cli.py b/src/nmteam_support/cli.py index cd798a0..37d0e1f 100644 --- a/src/nmteam_support/cli.py +++ b/src/nmteam_support/cli.py @@ -10,7 +10,12 @@ import typer -from nmteam_support.generator import GeneratorOptions, default_options, generate +from nmteam_support.generator import ( + GeneratorOptions, + default_options, + generate, + stage_markdown_copies, +) from nmteam_support.redirects import ( RedirectConfigError, read_redirects, @@ -55,7 +60,13 @@ def cmd_clean(options: GeneratorOptions) -> int: def cmd_build(options: GeneratorOptions) -> int: """Regenerate and build the static site into site/.""" generate(options) - return subprocess.call([sys.executable, "-m", "mkdocs", "build", "--strict", "--clean"]) + code = subprocess.call([sys.executable, "-m", "mkdocs", "build", "--strict", "--clean"]) + if code: + return code + site_dir = options.mkdocs_yml_path.parent / "site" + stage_markdown_copies(options.cache_dir, site_dir) + print("Markdown copies staged into site/.") + return 0 def cmd_dev(options: GeneratorOptions) -> int: diff --git a/src/nmteam_support/generator.py b/src/nmteam_support/generator.py index 2b97525..3635f57 100644 --- a/src/nmteam_support/generator.py +++ b/src/nmteam_support/generator.py @@ -10,10 +10,11 @@ from nmteam_support.frontmatter import split_frontmatter from nmteam_support.image_pipeline import stage_assets from nmteam_support.index import render_index_page +from nmteam_support.llms import render_llms_txt from nmteam_support.nav import build_nav_yaml from nmteam_support.redirects import load_redirects, render_redirects_js from nmteam_support.scanner import ScannedDir, scan_docs -from nmteam_support.template import render_mkdocs_yml +from nmteam_support.template import extract_site_url, render_mkdocs_yml @dataclass(frozen=True) @@ -61,6 +62,10 @@ def generate(options: GeneratorOptions) -> None: template = options.template_path.read_text(encoding="UTF-8", errors="ignore") options.mkdocs_yml_path.write_text(render_mkdocs_yml(template, nav_yaml), encoding="UTF-8") + base_url = extract_site_url(template) + (cache_dir / "llms.txt").write_text(render_llms_txt(root, base_url), encoding="UTF-8") + print("llms.txt generated.") + redirects = load_redirects(options.redirects_path) if redirects is None: print("Warning: redirects.json not found. Skipping redirects generation.") @@ -76,6 +81,20 @@ def generate(options: GeneratorOptions) -> None: print("Documentation generated.") +def stage_markdown_copies(cache_dir: Path, site_dir: Path) -> None: + """Copy every staged ``.md`` into ``site/`` so each page has a raw Markdown twin. + + The copies land at the same docs-relative path (e.g. ``nmbot-telegram/mcp.md``), + which is the URL served for ``/nmbot-telegram/mcp.md``. MkDocs copies ``llms.txt`` + verbatim but would re-render any ``.md`` placed inside the docs dir, so the twins + are staged only after the build, directly into the site output. + """ + for source in cache_dir.rglob("*.md"): + target = site_dir / source.relative_to(cache_dir) + target.parent.mkdir(parents=True, exist_ok=True) + shutil.copyfile(source, target) + + def _write_tree(docs_dir: Path, cache_dir: Path, scan: ScannedDir) -> None: for doc in scan.docs: source = docs_dir / doc.path diff --git a/src/nmteam_support/llms.py b/src/nmteam_support/llms.py new file mode 100644 index 0000000..759deb7 --- /dev/null +++ b/src/nmteam_support/llms.py @@ -0,0 +1,74 @@ +"""llms.txt generation from the scanned docs tree.""" + +from __future__ import annotations + +from nmteam_support.models import DocEntry +from nmteam_support.scanner import ScannedDir + +SITE_TITLE = "nmTeam Support" + +# Optional section skipped when a shorter context is needed (llmstxt.org spec). +OPTIONAL_SECTION = "Optional" + + +def render_llms_txt(root: ScannedDir, base_url: str) -> str: + """Render the ``/llms.txt`` file following the llmstxt.org proposal. + + Every link points to the raw Markdown copy staged next to the rendered + page (same docs-relative path), so LLMs can fetch page bodies directly. + """ + lines = [f"# {SITE_TITLE}", ""] + summary = root.index_meta.description or "nmTeam 官方支持文档站。" + lines.append(f"> {summary}") + lines.append("") + lines.append("本文件面向语言模型,提供 nmTeam 支持文档的索引;") + lines.append("每个链接指向对应页面的 Markdown 版本。") + lines.append("") + + sections = _sections(root) + if sections: + for title, entries in sections: + lines.append(f"## {title}") + lines.append("") + for entry in entries: + lines.append(_render_entry(entry, base_url)) + lines.append("") + return "\n".join(lines).rstrip() + "\n" + + +def _render_entry(entry: DocEntry, base_url: str) -> str: + line = f"- [{entry.title}]({base_url}/{entry.path})" + if entry.description: + line += f": {entry.description}" + return line + + +def _sections(root: ScannedDir) -> list[tuple[str, list[DocEntry]]]: + """Collect ``(section title, entries)`` pairs, one per directory with content.""" + sections: list[tuple[str, list[DocEntry]]] = [] + entries = _index_entry(root) + list(root.docs) + if entries: + title = root.index_meta.title or "站点" + sections.append((title, _sorted(entries))) + for sub in root.subdirs: + sections.extend(_sections(sub)) + return sections + + +def _index_entry(scan: ScannedDir) -> list[DocEntry]: + """The directory's own index.md as a DocEntry, when present.""" + if not scan.has_index: + return [] + path = f"{scan.rel_path}/index.md" if scan.rel_path else "index.md" + return [ + DocEntry( + title=scan.index_meta.title, + description=scan.index_meta.description or "目录索引", + path=path, + name="index.md", + ) + ] + + +def _sorted(entries: list[DocEntry]) -> list[DocEntry]: + return sorted(entries, key=lambda entry: (entry.index, entry.path)) diff --git a/src/nmteam_support/template.py b/src/nmteam_support/template.py index 95ad7a7..5f927da 100644 --- a/src/nmteam_support/template.py +++ b/src/nmteam_support/template.py @@ -8,6 +8,7 @@ NAV_END = "# NAV_ARIA_END" _NAV_PATTERN = re.compile(r"# NAV_ARIA_START.*# NAV_ARIA_END", re.S) +_SITE_URL_PATTERN = re.compile(r"^\s*site_url:\s*(\S+)\s*$", re.M) def render_mkdocs_yml(template: str, nav_yaml: str) -> str: @@ -16,3 +17,9 @@ def render_mkdocs_yml(template: str, nav_yaml: str) -> str: # Callable replacement: re.sub otherwise interprets backslash escapes in the # replacement string (e.g. "\1" -> group reference), mangling literal nav text. return _NAV_PATTERN.sub(lambda _m: block, template) + + +def extract_site_url(template: str, fallback: str = "https://support.nmteam.xyz") -> str: + """Read ``site_url:`` from the template, falling back when absent.""" + match = _SITE_URL_PATTERN.search(template) + return match.group(1).strip().rstrip("/") if match else fallback diff --git a/tests/test_generator.py b/tests/test_generator.py index 8c64b7c..8cffe30 100644 --- a/tests/test_generator.py +++ b/tests/test_generator.py @@ -5,7 +5,12 @@ import pytest from PIL import Image -from nmteam_support.generator import GeneratorOptions, generate, render_doc_file +from nmteam_support.generator import ( + GeneratorOptions, + generate, + render_doc_file, + stage_markdown_copies, +) def _full_options(tmp_path: Path, docs_dir: Path, redirects: str | None = None) -> GeneratorOptions: @@ -92,6 +97,29 @@ def test_generate_writes_redirects_before_copy(tmp_path, docs_dir): assert "/a/" in js.read_text(encoding="utf-8") +def test_generate_writes_llms_txt(tmp_path, docs_dir): + options = _full_options(tmp_path, docs_dir) + generate(options) + llms = options.generated_dir / "llms.txt" + assert llms.exists() + content = llms.read_text(encoding="utf-8") + assert content.startswith("# nmTeam Support\n") + assert "nmbot-telegram/mcp.md" in content + assert "https://support.nmteam.xyz/nmbot-telegram/mcp.md" in content + + +def test_stage_markdown_copies_mirrors_cache(tmp_path, docs_dir): + options = _full_options(tmp_path, docs_dir) + generate(options) + site_dir = tmp_path / "site" + stage_markdown_copies(options.cache_dir, site_dir) + twin = site_dir / "nmbot-telegram" / "mcp.md" + assert twin.exists() + cached = options.cache_dir / "nmbot-telegram" / "mcp.md" + assert twin.read_text(encoding="utf-8") == cached.read_text(encoding="utf-8") + assert (site_dir / "index.md").exists() + + def test_generate_skips_superpowers_in_output(tmp_path, docs_dir): internal = docs_dir / "superpowers" internal.mkdir() diff --git a/tests/test_llms.py b/tests/test_llms.py new file mode 100644 index 0000000..9f5fecf --- /dev/null +++ b/tests/test_llms.py @@ -0,0 +1,42 @@ +"""llms.txt generation tests.""" + +from nmteam_support.llms import render_llms_txt +from nmteam_support.scanner import scan_docs + +BASE_URL = "https://support.nmteam.xyz" + + +def test_llms_txt_has_header_and_summary(docs_dir): + content = render_llms_txt(scan_docs(docs_dir), BASE_URL) + assert content.startswith("# nmTeam Support\n") + assert "> 支持中心。" in content + assert content.endswith("\n") + + +def test_llms_txt_sections_and_links(docs_dir): + content = render_llms_txt(scan_docs(docs_dir), BASE_URL) + assert "## nmTeam 支持" in content # root section + assert "## nmBot Telegram" in content + assert "## 联系我们" in content + assert "- [MCP 配置](https://support.nmteam.xyz/nmbot-telegram/mcp.md): 配置 MCP。" in content + assert "- [关于](https://support.nmteam.xyz/about.md): 了解此文档。" in content + assert "- [nmTeam 支持](https://support.nmteam.xyz/index.md): 支持中心。" in content + + +def test_llms_txt_index_entries_use_index_paths(docs_dir): + content = render_llms_txt(scan_docs(docs_dir), BASE_URL) + assert "- [nmBot Telegram](https://support.nmteam.xyz/nmbot-telegram/index.md)" in content + assert "- [联系我们](https://support.nmteam.xyz/contact-us/index.md)" in content + + +def test_llms_txt_links_use_base_url(docs_dir): + content = render_llms_txt(scan_docs(docs_dir), "https://docs.example.org/sub") + assert "https://docs.example.org/sub/nmbot-telegram/mcp.md" in content + assert "support.nmteam.xyz" not in content + + +def test_llms_txt_empty_tree(tmp_path): + (tmp_path / "docs").mkdir() + content = render_llms_txt(scan_docs(tmp_path / "docs"), BASE_URL) + assert content.startswith("# nmTeam Support\n") + assert "## " not in content From 456281348edb7c7ca06409922e8df76e46cbfc19 Mon Sep 17 00:00:00 2001 From: Alice39s Date: Sun, 9 Aug 2026 21:41:22 +0900 Subject: [PATCH 40/57] fix(ai): align AI tool buttons with Material conventions, graceful dev fallback --- assets/js/ai-tools.js | 19 +++++++++++++++++++ assets/styles/ai-tools.css | 7 +++++-- 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/assets/js/ai-tools.js b/assets/js/ai-tools.js index e1ec4cc..a67fc89 100644 --- a/assets/js/ai-tools.js +++ b/assets/js/ai-tools.js @@ -72,6 +72,25 @@ var markdownLink = box.querySelector('[data-ai="markdown"]'); if (markdownLink) { markdownLink.setAttribute("href", md); + // The Markdown twins are only staged into site/ by `nmteam build`; + // under `mkdocs serve` they 404, so explain instead of dead-linking. + markdownLink.addEventListener("click", function (event) { + event.preventDefault(); + fetch(md, { method: "HEAD" }).then(function (response) { + var contentType = response.headers.get("Content-Type") || ""; + // `mkdocs serve` renders .md URLs as HTML pages; only the + // static twins staged by `nmteam build` are served as + // Markdown. Navigate only for the real thing. + if (response.ok && contentType.indexOf("html") === -1) { + location.href = md; + } else { + window.alert( + "Markdown 版本在构建产物中提供。请先运行 `uv run nmteam build`," + + "或改用 ChatGPT / Claude 按钮。" + ); + } + }); + }); } var chatgpt = box.querySelector('[data-ai="chatgpt"]'); if (chatgpt) { diff --git a/assets/styles/ai-tools.css b/assets/styles/ai-tools.css index 6c868fa..8230158 100644 --- a/assets/styles/ai-tools.css +++ b/assets/styles/ai-tools.css @@ -4,8 +4,11 @@ .ai-tools { display: inline-flex; gap: 6px; - margin-inline-start: 8px; - vertical-align: middle; + /* Align with Material's action buttons (edit/view) in the top-right + corner of the content area; without the float the group renders + stacked against the hero banner on the home page. */ + float: right; + margin: 8px 8px 0 0; } .ai-tools__link { From 8d86bc8ca2e6abfaf95e5b88f9eed5c00a0c8b53 Mon Sep 17 00:00:00 2001 From: Alice39s Date: Mon, 10 Aug 2026 07:34:59 +0900 Subject: [PATCH 41/57] fix(ui): restore homepage rendering and add Open menu --- AGENTS.md | 87 ++++++++-------- README.md | 7 +- assets/js/ai-tools.js | 178 +++++++++++++++++++------------- assets/styles/ai-tools.css | 143 ++++++++++++++++++++----- docs/index.md | 11 +- overrides/partials/actions.html | 48 +++++++-- tests/test_homepage.py | 18 ++++ 7 files changed, 335 insertions(+), 157 deletions(-) create mode 100644 tests/test_homepage.py diff --git a/AGENTS.md b/AGENTS.md index 6b689a1..045a5c7 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -24,14 +24,14 @@ redirects.json ──────┘ (含生成的 assets/js/redirects.js `generate()`(`src/nmteam_support/generator.py`)编排的完整链路: 1. 清空重建 `cache/` -2. `scanner.scan_docs()` 递归扫描 `docs/` 得到文档树(`SKIP_DIRS`/`INTERNAL_DIRS` 过滤) -3. 写入处理后的页面(非 index.md 注入贡献提示,`contributing.py`) -4. `index.py` 为每目录生成 `index.md`(自动 generated 标记 + docsList 卡片,`docslist.py` 渲染 HTML) -5. `image_pipeline.py` 用 PIL 为 assets 中每张 PNG/JPEG 生成 `.webp` 兄弟文件(质量 80;PNG 另出 256 色有损 fallback) -6. `nav.py` 生成 nav YAML,`template.py` 替换 `mkdocs-template.yml` 的 `# NAV_ARIA_START`/`# NAV_ARIA_END` 标记块写入 `mkdocs.yml` -7. `redirects.py` 读 `redirects.json` 生成 `cache/assets/js/redirects.js` -8. `llms.py` 用扫描树 + 模板 `site_url` 生成 `cache/llms.txt`(llmstxt.org 规范:H1 + blockquote + H2 分节链接,链接指向 `.md` 版本;非 md 文件会被 mkdocs 原样拷到 `site/llms.txt`) -9. copytree 到 `generated/`,交给 `mkdocs build --strict` +1. `scanner.scan_docs()` 递归扫描 `docs/` 得到文档树(`SKIP_DIRS`/`INTERNAL_DIRS` 过滤) +1. 写入处理后的页面(非 index.md 注入贡献提示,`contributing.py`) +1. `index.py` 为每目录生成 `index.md`(自动 generated 标记 + docsList 卡片,`docslist.py` 渲染 HTML) +1. `image_pipeline.py` 用 PIL 为 assets 中每张 PNG/JPEG 生成 `.webp` 兄弟文件(质量 80;PNG 另出 256 色有损 fallback) +1. `nav.py` 生成 nav YAML,`template.py` 替换 `mkdocs-template.yml` 的 `# NAV_ARIA_START`/`# NAV_ARIA_END` 标记块写入 `mkdocs.yml` +1. `redirects.py` 读 `redirects.json` 生成 `cache/assets/js/redirects.js` +1. `llms.py` 用扫描树 + 模板 `site_url` 生成 `cache/llms.txt`(llmstxt.org 规范:H1 + blockquote + H2 分节链接,链接指向 `.md` 版本;非 md 文件会被 mkdocs 原样拷到 `site/llms.txt`) +1. copytree 到 `generated/`,交给 `mkdocs build --strict` **图片双层管线**(关键机制):生成期 `image_pipeline.py` 产出同名 `.webp` 兄弟文件;渲染期 `markdown_images.py`(mkdocs 扩展,注册在 mkdocs-template.yml 的 markdown_extensions 中)把本地栅格图 `` 改写为 WebP-first ``,靠同名 `.webp` 约定对接。外部 URL 不下载不镜像。Markdown 中仍写普通图片语法。 @@ -39,18 +39,18 @@ redirects.json ──────┘ (含生成的 assets/js/redirects.js ## Key Directories -| 路径 | 用途 | -|---|---| -| `src/nmteam_support/` | 工具链包(16 个模块,见 Important Files) | -| `docs/` | **真实文档源**(唯一需要手工编辑的内容位置) | -| `docs/nmbot-telegram/` | 产品中枢:`panel/`、`plus/`、`legal/`、`group/`、`faq/`、`business/`、`tools/`、`nmbot-intelligence/`、`credit/`、`update-log/`(`YYYY-MM.md` 月度日志)、`mcp/` | -| `docs/contact-us/`、`docs/nmteam-account/` | 其他产品线 | -| `docs/superpowers/` | 本地设计与实现工件(plans/specs),**已 gitignore,勿提交** | -| `assets/images/` | 图片母版:`shared/`(站级共享)、`nmbot/`(含 `mcp/`、`update-pictures/` 子目录);`assets/icons/`(SVG)、`assets/styles/`(CSS)、`assets/js/`(AI 工具脚本 `ai-tools.js`,随构建 stage 到生成站) | -| `overrides/` | mkdocs `custom_dir`:`main.html` 覆写 site_meta 移除主题版本号;`partials/actions.html` 追加 AI 工具按钮组(Markdown / ChatGPT / Claude,毛玻璃样式在 `assets/styles/ai-tools.css`,交互在 `assets/js/ai-tools.js`) | -| `scripts/` | 三平台薄启动器(`nmteam.sh` / `nmteam.ps1` / `nmteam.bat`) | -| `tests/` | pytest 测试(16 个文件 + conftest.py) | -| `cache/`、`generated/`、`site/`、`mkdocs.yml` | 生成产物,勿手改勿提交 | +| 路径 | 用途 | +| --------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `src/nmteam_support/` | 工具链包(16 个模块,见 Important Files) | +| `docs/` | **真实文档源**(唯一需要手工编辑的内容位置) | +| `docs/nmbot-telegram/` | 产品中枢:`panel/`、`plus/`、`legal/`、`group/`、`faq/`、`business/`、`tools/`、`nmbot-intelligence/`、`credit/`、`update-log/`(`YYYY-MM.md` 月度日志)、`mcp/` | +| `docs/contact-us/`、`docs/nmteam-account/` | 其他产品线 | +| `docs/superpowers/` | 本地设计与实现工件(plans/specs),**已 gitignore,勿提交** | +| `assets/images/` | 图片母版:`shared/`(站级共享)、`nmbot/`(含 `mcp/`、`update-pictures/` 子目录);`assets/icons/`(SVG)、`assets/styles/`(CSS)、`assets/js/`(AI 工具脚本 `ai-tools.js`,随构建 stage 到生成站) | +| `overrides/` | mkdocs `custom_dir`:`main.html` 覆写 site_meta 移除主题版本号;`partials/actions.html` 追加 Fumadocs 风格 Open 菜单(GitHub / Markdown / Scira AI / ChatGPT / Claude / Cursor,样式在 `assets/styles/ai-tools.css`,交互在 `assets/js/ai-tools.js`) | +| `scripts/` | 三平台薄启动器(`nmteam.sh` / `nmteam.ps1` / `nmteam.bat`) | +| `tests/` | pytest 测试(17 个文件 + conftest.py) | +| `cache/`、`generated/`、`site/`、`mkdocs.yml` | 生成产物,勿手改勿提交 | ## Development Commands @@ -71,6 +71,7 @@ uv run nmteam --help 平台启动器(定位仓库根后原样透传参数,无业务逻辑):`scripts/nmteam.sh dev`、`.\scripts\nmteam.ps1 dev`、`scripts\nmteam.bat dev`。 **质量检查**(`nmteam check`,CI 同样执行): + ```bash uv run ruff check . uv run ruff format --check . @@ -101,29 +102,29 @@ Markdown 文档(`docs/`): ## Important Files -| 文件 | 职责 | -|---|---| -| `src/nmteam_support/cli.py` | Typer 入口 `main`;install/dev/generate/build/clean/check + redirects 子命令 | -| `src/nmteam_support/generator.py` | `generate()` 端到端编排 + `GeneratorOptions`/`default_options` | -| `src/nmteam_support/scanner.py` | 递归扫描 docs/ 树,SKIP_DIRS/INTERNAL_DIRS 过滤 | -| `src/nmteam_support/frontmatter.py` | frontmatter 解析(title/description/index/flag) | -| `src/nmteam_support/index.py` + `docslist.py` | 每目录 index.md 生成 + docsList 卡片 HTML | -| `src/nmteam_support/nav.py` + `template.py` | nav YAML 生成;NAV_ARIA 标记块替换写 mkdocs.yml | -| `src/nmteam_support/contributing.py` | 非 index.md 注入贡献提示 admonition | -| `src/nmteam_support/redirects.py` | redirects.json 管理(损坏保护)+ redirects.js 生成 | -| `src/nmteam_support/image_pipeline.py` + `markdown_images.py` | PIL 生成 .webp 兄弟文件;mkdocs 扩展包 WebP-first `` | -| `src/nmteam_support/llms.py` | `render_llms_txt()` 从扫描树生成 `/llms.txt`(llmstxt.org 规范;链接指向各页 `.md` 版本) | -| `src/nmteam_support/models.py` | `PageMetadata`/`DocEntry` frozen dataclass | -| `pyproject.toml` | 包元数据、依赖、入口、pytest/ruff/hatchling 配置 | -| `uv.lock` | 锁定依赖(mkdocs 1.6.1、mkdocs-material 9.7.7、mkdocs-minify-plugin 0.8.0、pillow 12.3.0、typer 0.27.1、pytest 9.1.1、ruff 0.16.2、mdformat 1.0.0 等) | -| `mkdocs-template.yml` | mkdocs 配置模板(material zh 黄色双 palette、minify 插件、custom_dir overrides、`nmteam_support.markdown_images` 扩展、NAV_ARIA 标记) | -| `redirects.json` | 顶层 `redirects` 对象:`{旧路径带斜杠: 新路径}` | -| `.github/workflows/ci.yml` | 三 OS 矩阵 CI(push main/dev + PR):uv sync --frozen → nmteam check → 验证三个启动器 | -| `.mdformat.toml` | mdformat 配置(wrap=keep、LF) | -| `overrides/main.html` | 移除 meta 中 mkdocs-material 版本号 | -| `overrides/partials/actions.html` | 覆盖 material actions partial:保留编辑/查看按钮,追加 AI 工具按钮组 | -| `assets/js/ai-tools.js` | 按钮交互:View-as-Markdown 链接(根相对 `.md` 路径)、ChatGPT/Claude 点击后 fetch 页面 `.md` 内容作为提示词打开(fetch 失败降级为仅 URL) | -| `assets/styles/ai-tools.css` | 毛玻璃按钮样式(半透明 + backdrop-filter blur + 柔和阴影,适配明暗主题) | +| 文件 | 职责 | +| ------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ | +| `src/nmteam_support/cli.py` | Typer 入口 `main`;install/dev/generate/build/clean/check + redirects 子命令 | +| `src/nmteam_support/generator.py` | `generate()` 端到端编排 + `GeneratorOptions`/`default_options` | +| `src/nmteam_support/scanner.py` | 递归扫描 docs/ 树,SKIP_DIRS/INTERNAL_DIRS 过滤 | +| `src/nmteam_support/frontmatter.py` | frontmatter 解析(title/description/index/flag) | +| `src/nmteam_support/index.py` + `docslist.py` | 每目录 index.md 生成 + docsList 卡片 HTML | +| `src/nmteam_support/nav.py` + `template.py` | nav YAML 生成;NAV_ARIA 标记块替换写 mkdocs.yml | +| `src/nmteam_support/contributing.py` | 非 index.md 注入贡献提示 admonition | +| `src/nmteam_support/redirects.py` | redirects.json 管理(损坏保护)+ redirects.js 生成 | +| `src/nmteam_support/image_pipeline.py` + `markdown_images.py` | PIL 生成 .webp 兄弟文件;mkdocs 扩展包 WebP-first `` | +| `src/nmteam_support/llms.py` | `render_llms_txt()` 从扫描树生成 `/llms.txt`(llmstxt.org 规范;链接指向各页 `.md` 版本) | +| `src/nmteam_support/models.py` | `PageMetadata`/`DocEntry` frozen dataclass | +| `pyproject.toml` | 包元数据、依赖、入口、pytest/ruff/hatchling 配置 | +| `uv.lock` | 锁定依赖(mkdocs 1.6.1、mkdocs-material 9.7.7、mkdocs-minify-plugin 0.8.0、pillow 12.3.0、typer 0.27.1、pytest 9.1.1、ruff 0.16.2、mdformat 1.0.0 等) | +| `mkdocs-template.yml` | mkdocs 配置模板(material zh 黄色双 palette、minify 插件、custom_dir overrides、`nmteam_support.markdown_images` 扩展、NAV_ARIA 标记) | +| `redirects.json` | 顶层 `redirects` 对象:`{旧路径带斜杠: 新路径}` | +| `.github/workflows/ci.yml` | 三 OS 矩阵 CI(push main/dev + PR):uv sync --frozen → nmteam check → 验证三个启动器 | +| `.mdformat.toml` | mdformat 配置(wrap=keep、LF) | +| `overrides/main.html` | 移除 meta 中 mkdocs-material 版本号 | +| `overrides/partials/actions.html` | 覆盖 material actions partial:保留编辑/查看按钮,追加六项 Open 操作菜单 | +| `assets/js/ai-tools.js` | 菜单交互:开关状态、外部点击/Escape 关闭、View-as-Markdown 开发模式提示,以及 Scira AI / ChatGPT / Claude / Cursor 页面 URL 深链 | +| `assets/styles/ai-tools.css` | Open 触发器与半透明弹层样式,适配明暗主题和窄屏 | ## Runtime/Tooling Preferences diff --git a/README.md b/README.md index a5c36b3..8013520 100644 --- a/README.md +++ b/README.md @@ -69,11 +69,12 @@ Markdown 中仍使用普通图片语法,构建工具会自动输出 WebP 每个链接指向页面的 Markdown 版本。 - 每页 Markdown 版本:`nmteam build` 时在每个页面旁生成同路径的 `.md` 文件 (如 `/nmbot-telegram/mcp.md`)。 -- 页面顶部的 AI 工具按钮:**Markdown**(查看本页 Markdown)、**ChatGPT** / - **Claude**(将本页内容作为上下文在 ChatGPT/Claude 中打开)。 +- 页面顶部的 **Open** 菜单:提供 GitHub 源文件、Markdown 版本,以及 + Scira AI、ChatGPT、Claude、Cursor 六种打开方式。 注意:`llms.txt` 与 `.md` 版本由 `nmteam build` 输出到 `site/`;开发模式 -(`nmteam dev`)下 ChatGPT/Claude 按钮会退化为仅携带页面链接的提示词。 +(`nmteam dev`)下 View as Markdown 会提示先构建,AI 操作仍可通过当前页面 +URL 打开。 ### 其他命令 diff --git a/assets/js/ai-tools.js b/assets/js/ai-tools.js index a67fc89..c16fc84 100644 --- a/assets/js/ai-tools.js +++ b/assets/js/ai-tools.js @@ -1,23 +1,15 @@ -// AI tools: view-as-Markdown, open in ChatGPT / Claude. -// The page's raw Markdown twin lives at the same docs-relative path with a -// ".md" suffix (e.g. /nmbot-telegram/mcp.md); it is staged into site/ by the -// build. When the twin is missing (e.g. `mkdocs serve`), ChatGPT/Claude fall -// back to a prompt that references the page URL instead of its content. +// Fumadocs-style page actions menu. (function () { "use strict"; var ENDPOINTS = { - chatgpt: "https://chatgpt.com/?q=", + scira: "https://scira.ai/?q=", + chatgpt: "https://chatgpt.com/?prompt=", claude: "https://claude.ai/new?q=", + cursor: "https://cursor.com/link/prompt?text=", }; - // Keep the prompt URL short enough for chat providers to accept. - var MAX_PROMPT = 60000; function mdUrl(raw) { - // Root-relative: a relative path would resolve against the page URL, - // e.g. /nmbot-telegram/mcp/ + "nmbot-telegram/mcp.md" -> wrong twin. - // The home page has an empty page.url and its twin is /index.md - // (already carries the ".md" suffix). if (!raw) { return "/index.md"; } @@ -28,77 +20,119 @@ return location.origin + "/" + (raw || ""); } - function buildPrompt(content, url) { - if (!content) { - return "请阅读此文档页面并回答我的问题:" + url; + function providerPrompt(url) { + return "Read " + url + ", I want to ask questions about it."; + } + + function providerUrl(provider, prompt) { + var url = ENDPOINTS[provider] + encodeURIComponent(prompt); + if (provider === "chatgpt") { + return url + "&hints=search"; } - var header = "以下是 support.nmteam.xyz 文档页面的内容,请基于此内容回答我的问题:\n\n"; - var body = content; - if (body.length > MAX_PROMPT) { - body = body.slice(0, MAX_PROMPT) + "\n\n…(内容过长已截断)"; + 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(); } - return header + body; } - function openAi(which, prompt) { - window.open(ENDPOINTS[which] + encodeURIComponent(prompt), "_blank", "noopener"); + function showMarkdownUnavailable() { + window.alert( + "Markdown 版本在构建产物中提供。请先运行 `uv run nmteam build`," + + "或使用其他打开方式。" + ); } - function wireFetch(button, which, md, url) { - button.addEventListener("click", function (event) { + 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(); - fetch(md) - .then(function (response) { - return response.ok ? response.text() : ""; - }) - .catch(function () { - return ""; - }) - .then(function (content) { - openAi(which, buildPrompt(content, url)); - }); + 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(); + }); }); } - document.addEventListener("DOMContentLoaded", function () { - var box = document.querySelector(".ai-tools"); - if (!box) { - return; - } + function wireMenu(box) { + var trigger = box.querySelector(".ai-tools__trigger"); + var menu = box.querySelector(".ai-tools__menu"); var raw = box.getAttribute("data-md-url") || ""; - var md = mdUrl(raw); - var url = pageUrl(raw); - - var markdownLink = box.querySelector('[data-ai="markdown"]'); - if (markdownLink) { - markdownLink.setAttribute("href", md); - // The Markdown twins are only staged into site/ by `nmteam build`; - // under `mkdocs serve` they 404, so explain instead of dead-linking. - markdownLink.addEventListener("click", function (event) { + var markdown = menu.querySelector('[data-ai="markdown"]'); + var prompt = providerPrompt(pageUrl(raw)); + + wireMarkdown(markdown, mdUrl(raw)); + ["scira", "chatgpt", "claude", "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(); - fetch(md, { method: "HEAD" }).then(function (response) { - var contentType = response.headers.get("Content-Type") || ""; - // `mkdocs serve` renders .md URLs as HTML pages; only the - // static twins staged by `nmteam build` are served as - // Markdown. Navigate only for the real thing. - if (response.ok && contentType.indexOf("html") === -1) { - location.href = md; - } else { - window.alert( - "Markdown 版本在构建产物中提供。请先运行 `uv run nmteam build`," + - "或改用 ChatGPT / Claude 按钮。" - ); - } - }); - }); - } - var chatgpt = box.querySelector('[data-ai="chatgpt"]'); - if (chatgpt) { - wireFetch(chatgpt, "chatgpt", md, url); - } - var claude = box.querySelector('[data-ai="claude"]'); - if (claude) { - wireFetch(claude, "claude", md, url); - } + setOpen(box, false, true); + } + }); + } + + document.addEventListener("DOMContentLoaded", function () { + document.querySelectorAll(".ai-tools").forEach(wireMenu); }); })(); diff --git a/assets/styles/ai-tools.css b/assets/styles/ai-tools.css index 8230158..4a90b24 100644 --- a/assets/styles/ai-tools.css +++ b/assets/styles/ai-tools.css @@ -1,42 +1,129 @@ -/* AI tools: view-as-Markdown, open in ChatGPT / Claude. - Glassmorphism style (frosted translucent pill buttons), adapting to the - Material light/dark palettes via theme CSS variables. */ +/* Fumadocs-style page actions menu. */ .ai-tools { - display: inline-flex; - gap: 6px; - /* Align with Material's action buttons (edit/view) in the top-right - corner of the content area; without the float the group renders - stacked against the hero banner on the home page. */ + position: relative; + z-index: 4; float: right; - margin: 8px 8px 0 0; + margin: 0 0 12px 12px; } -.ai-tools__link { +.ai-tools__trigger { display: inline-flex; align-items: center; - padding: 4px 12px; + justify-content: center; + min-height: 30px; + gap: 8px; + padding: 5px 9px; + font: inherit; font-size: 12px; - line-height: 1.6; - color: var(--md-typeset-color); - background: color-mix(in srgb, var(--md-default-bg-color) 62%, transparent); - -webkit-backdrop-filter: blur(12px) saturate(160%); - backdrop-filter: blur(12px) saturate(160%); - border: 1px solid color-mix(in srgb, var(--md-default-fg-color) 22%, transparent); - border-radius: 999px; - box-shadow: 0 1px 4px color-mix(in srgb, var(--md-default-fg-color) 12%, transparent); + font-weight: 600; + line-height: 18px; + color: var(--md-default-fg-color); + background: var(--md-default-bg-color); + border: 1px solid var(--md-default-fg-color--lightest); + border-radius: 6px; cursor: pointer; - text-decoration: none; - transition: background .2s, color .2s, border-color .2s, box-shadow .2s; + transition: background-color 120ms ease, border-color 120ms ease; } -.ai-tools__link:hover { - color: var(--md-primary-fg-color); - background: color-mix(in srgb, var(--md-default-bg-color) 88%, var(--md-primary-fg-color)); - border-color: color-mix(in srgb, var(--md-primary-fg-color) 45%, transparent); - box-shadow: 0 2px 10px color-mix(in srgb, var(--md-primary-fg-color) 25%, transparent); +.ai-tools__trigger:hover, +.ai-tools.is-open .ai-tools__trigger { + background: var(--md-default-fg-color--lightest); + border-color: var(--md-default-fg-color--lighter); } -.ai-tools__link:focus-visible { - outline: 2px solid var(--md-primary-fg-color); +.ai-tools__trigger:focus-visible, +.ai-tools__item:focus-visible { + outline: 2px solid var(--md-accent-fg-color); outline-offset: 2px; } + +.ai-tools__chevron, +.ai-tools__item-icon, +.ai-tools__external { + display: inline-flex; + align-items: center; + justify-content: center; + flex: 0 0 auto; +} + +.ai-tools__chevron { + width: 14px; + height: 14px; + color: var(--md-default-fg-color--light); + transition: transform 140ms ease; +} + +.ai-tools.is-open .ai-tools__chevron { + transform: rotate(180deg); +} + +.ai-tools__menu { + position: absolute; + top: calc(100% + 6px); + right: 0; + display: grid; + width: 216px; + max-width: calc(100vw - 32px); + padding: 8px; + color: var(--md-default-fg-color); + background: color-mix(in srgb, var(--md-default-bg-color) 88%, transparent); + -webkit-backdrop-filter: blur(16px) saturate(135%); + backdrop-filter: blur(16px) saturate(135%); + border: 1px solid var(--md-default-fg-color--lightest); + border-radius: 12px; + box-shadow: 0 16px 40px color-mix(in srgb, #000 22%, transparent); +} + +.ai-tools__menu[hidden] { + display: none; +} + +.md-typeset .ai-tools__item { + display: grid; + grid-template-columns: 18px minmax(0, 1fr) 16px; + align-items: center; + gap: 10px; + min-height: 32px; + padding: 6px 8px; + font-size: 14px; + line-height: 20px; + color: var(--md-default-fg-color); + border-radius: 8px; + text-decoration: none; + transition: color 100ms ease, background-color 100ms ease; +} + +.md-typeset .ai-tools__item:hover { + color: var(--md-default-fg-color); + background: var(--md-default-fg-color--lightest); +} + +.ai-tools__item-icon { + width: 18px; + height: 18px; +} + +.ai-tools__label { + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.ai-tools__external { + width: 16px; + height: 16px; + color: var(--md-default-fg-color--light); +} + +.ai-tools svg { + width: 100%; + height: 100%; + fill: currentcolor; +} + +@media (max-width: 44.984375em) { + .ai-tools { + margin-right: 4px; + } +} diff --git a/docs/index.md b/docs/index.md index d9b990d..7bc6ea2 100644 --- a/docs/index.md +++ b/docs/index.md @@ -12,11 +12,11 @@ hide:

在此获取 nmTeam 旗下产品的支持。

-
+
## 产品和服务 -
+
-
+
## 关于帮助文档 @@ -48,9 +48,12 @@ nmTeam 帮助文档由 nmTeam 成员和社区志愿者共同编辑。您可以 访问 [nmTeam 官网](https://nmteam.xyz)了解 nmTeam 的最新动态和产品信息。 -
+