From 582a470fcc79bb0070cf0e98ee330e99d89a4277 Mon Sep 17 00:00:00 2001 From: z0o0ey Date: Wed, 12 Aug 2026 13:59:59 +0800 Subject: [PATCH 1/2] fix --- content/_index.md | 2 +- content/_index.zh.md | 2 +- content/blog/modelscope-taas/index.md | 14 +++++++------- content/blog/modelscope-taas/index.zh.md | 14 +++++++------- content/docs/guide/taas.md | 5 +++-- content/docs/guide/taas.zh.md | 5 +++-- 6 files changed, 22 insertions(+), 20 deletions(-) diff --git a/content/_index.md b/content/_index.md index 948a628..7403b0d 100644 --- a/content/_index.md +++ b/content/_index.md @@ -138,7 +138,7 @@ sections: # Use ModelScope's official TaaS endpoint — free, no local GPU needed base_url = 'https://www.modelscope.cn/twinkle' api_key = os.environ.get('MODELSCOPE_TOKEN') - base_model = 'Qwen/Qwen3.6-27B' + base_model = 'Qwen/Qwen3.8-27B' # Prepare data locally dataset = Dataset(dataset_meta=DatasetMeta('ms://swift/self-cognition')) diff --git a/content/_index.zh.md b/content/_index.zh.md index aa92f4b..7732706 100644 --- a/content/_index.zh.md +++ b/content/_index.zh.md @@ -138,7 +138,7 @@ sections: # 使用魔搭社区官方 TaaS 端点 — 免费,无需本地 GPU base_url = 'https://www.modelscope.cn/twinkle' api_key = os.environ.get('MODELSCOPE_TOKEN') - base_model = 'Qwen/Qwen3.6-27B' + base_model = 'Qwen/Qwen3.8-27B' # 本地准备数据 dataset = Dataset(dataset_meta=DatasetMeta('ms://swift/self-cognition')) diff --git a/content/blog/modelscope-taas/index.md b/content/blog/modelscope-taas/index.md index eadf5f3..3c3a1f3 100644 --- a/content/blog/modelscope-taas/index.md +++ b/content/blog/modelscope-taas/index.md @@ -18,7 +18,7 @@ We're excited to announce that **Twinkle Training-as-a-Service (TaaS)** is now a Training-as-a-Service lets you fine-tune large language models through a simple API, without managing infrastructure. The model runs on ModelScope's backend servers; you just send data and receive trained adapters. -Currently available model: **[Qwen/Qwen3.6-27B](https://www.modelscope.cn/models/Qwen/Qwen3.6-27B)** +Currently available model: **[Qwen/Qwen3.8-27B](https://www.modelscope.cn/models/Qwen/Qwen3.8-27B)** ## Getting Started @@ -44,8 +44,8 @@ from twinkle.dataset import Dataset, DatasetMeta from twinkle.preprocessor import SelfCognitionProcessor from twinkle.server.common import input_feature_to_datum -base_model = 'Qwen/Qwen3.6-27B' -base_url = 'http://www.modelscope.cn/twinkle' +base_model = 'Qwen/Qwen3.8-27B' +base_url = 'https://www.modelscope.cn/twinkle' api_key = os.environ.get('MODELSCOPE_TOKEN') # Load and preprocess dataset @@ -101,8 +101,8 @@ from twinkle import init_tinker_client init_tinker_client() from tinker import ServiceClient -base_model = 'Qwen/Qwen3.6-27B' -base_url = 'http://www.modelscope.cn/twinkle' +base_model = 'Qwen/Qwen3.8-27B' +base_url = 'https://www.modelscope.cn/twinkle' service_client = ServiceClient( base_url=base_url, @@ -111,7 +111,7 @@ service_client = ServiceClient( # Load your trained LoRA sampling_client = service_client.create_sampling_client( - model_path='twinkle://xxx-Qwen_Qwen3.6-27B-xxx/weights/twinkle-lora-1', + model_path='twinkle://xxx-Qwen_Qwen3.8-27B-xxx/weights/twinkle-lora-1', base_model=base_model ) @@ -160,7 +160,7 @@ The free tier has some limitations: |-----------|-------| | Max Rank | 32 | | modules_to_save | Not supported | -| Multimodal | Text-only (Qwen3.6-27B) | +| Multimodal | Text-only (Qwen3.8-27B) | ## What You Can Customize diff --git a/content/blog/modelscope-taas/index.zh.md b/content/blog/modelscope-taas/index.zh.md index b02eb72..cd2f13e 100644 --- a/content/blog/modelscope-taas/index.zh.md +++ b/content/blog/modelscope-taas/index.zh.md @@ -18,7 +18,7 @@ categories: 训练即服务(Training-as-a-Service)让你通过简单的 API 微调大语言模型,无需管理基础设施。模型运行在魔搭的后台服务器上;你只需发送数据,即可获得训练好的适配器。 -当前可用模型:**[Qwen/Qwen3.6-27B](https://www.modelscope.cn/models/Qwen/Qwen3.6-27B)** +当前可用模型:**[Qwen/Qwen3.8-27B](https://www.modelscope.cn/models/Qwen/Qwen3.8-27B)** ## 快速开始 @@ -44,8 +44,8 @@ from twinkle.dataset import Dataset, DatasetMeta from twinkle.preprocessor import SelfCognitionProcessor from twinkle.server.common import input_feature_to_datum -base_model = 'Qwen/Qwen3.6-27B' -base_url = 'http://www.modelscope.cn/twinkle' +base_model = 'Qwen/Qwen3.8-27B' +base_url = 'https://www.modelscope.cn/twinkle' api_key = os.environ.get('MODELSCOPE_TOKEN') # 加载并预处理数据集 @@ -101,8 +101,8 @@ from twinkle import init_tinker_client init_tinker_client() from tinker import ServiceClient -base_model = 'Qwen/Qwen3.6-27B' -base_url = 'http://www.modelscope.cn/twinkle' +base_model = 'Qwen/Qwen3.8-27B' +base_url = 'https://www.modelscope.cn/twinkle' service_client = ServiceClient( base_url=base_url, @@ -111,7 +111,7 @@ service_client = ServiceClient( # 加载训练好的 LoRA sampling_client = service_client.create_sampling_client( - model_path='twinkle://xxx-Qwen_Qwen3.6-27B-xxx/weights/twinkle-lora-1', + model_path='twinkle://xxx-Qwen_Qwen3.8-27B-xxx/weights/twinkle-lora-1', base_model=base_model ) @@ -160,7 +160,7 @@ for i, seq in enumerate(result.sequences): |------|------| | 最大 Rank | 32 | | modules_to_save | 不支持 | -| 多模态 | 仅支持文本(Qwen3.6-27B) | +| 多模态 | 仅支持文本(Qwen3.8-27B) | ## 可自定义的内容 diff --git a/content/docs/guide/taas.md b/content/docs/guide/taas.md index b8b3260..ae0000f 100644 --- a/content/docs/guide/taas.md +++ b/content/docs/guide/taas.md @@ -23,8 +23,8 @@ from twinkle.dataset import Dataset, DatasetMeta from twinkle.preprocessor import SelfCognitionProcessor from twinkle.server.common import input_feature_to_datum -# The base model (currently Qwen3.6-27B) -base_model = 'Qwen/Qwen3.6-27B' +# The base model (currently Qwen3.8-27B) +base_model = 'Qwen/Qwen3.8-27B' # Prepare dataset dataset = Dataset(dataset_meta=DatasetMeta( @@ -167,6 +167,7 @@ client = init_twinkle_client( | Model | Size | HuggingFace ID | Megatron | |:------|:-----|:---------------|:---------| +| Qwen3.8 | 27B | Qwen/Qwen3.8-* | Yes | | Qwen3.6 | 4B-35B-A3B | Qwen/Qwen3.6-* | Yes | | Qwen3.5 | 2B-27B | Qwen/Qwen3.5-* | Yes | | Qwen3 | 0.6B-32B | Qwen/Qwen3-* | Yes | diff --git a/content/docs/guide/taas.zh.md b/content/docs/guide/taas.zh.md index eba32ae..dad3b7b 100644 --- a/content/docs/guide/taas.zh.md +++ b/content/docs/guide/taas.zh.md @@ -23,8 +23,8 @@ from twinkle.dataset import Dataset, DatasetMeta from twinkle.preprocessor import SelfCognitionProcessor from twinkle.server.common import input_feature_to_datum -# 基座模型(当前为 Qwen3.6-27B) -base_model = 'Qwen/Qwen3.6-27B' +# 基座模型(当前为 Qwen3.8-27B) +base_model = 'Qwen/Qwen3.8-27B' # 准备数据集 dataset = Dataset(dataset_meta=DatasetMeta( @@ -167,6 +167,7 @@ client = init_twinkle_client( | 模型 | 规模 | HuggingFace ID | Megatron | |:-----|:-----|:---------------|:---------| +| Qwen3.8 | 27B | Qwen/Qwen3.8-* | 支持 | | Qwen3.6 | 4B-35B-A3B | Qwen/Qwen3.6-* | 支持 | | Qwen3.5 | 2B-27B | Qwen/Qwen3.5-* | 支持 | | Qwen3 | 0.6B-32B | Qwen/Qwen3-* | 支持 | From 71464a35cb689375436dcb39da7fe879ff82f9cf Mon Sep 17 00:00:00 2001 From: z0o0ey Date: Fri, 14 Aug 2026 15:24:37 +0800 Subject: [PATCH 2/2] fix --- scripts/sync-docs.py | 131 ++++++++++++++++++++++++++++++------------- 1 file changed, 91 insertions(+), 40 deletions(-) diff --git a/scripts/sync-docs.py b/scripts/sync-docs.py index 0071dd8..b2a4da0 100644 --- a/scripts/sync-docs.py +++ b/scripts/sync-docs.py @@ -13,9 +13,11 @@ """ import os +import posixpath import re import shutil from pathlib import Path +from urllib.parse import unquote # --------------------------------------------------------------------------- # Path setup @@ -127,13 +129,21 @@ def _init_docs_paths(): def slugify(name: str) -> str: """Convert a directory/file name to a Hugo-friendly slug.""" - s = name.lower().strip() + s = unquote(name).lower().strip() # "Usage%20Guide" -> "usage guide" s = re.sub(r"[_ ]+", "-", s) # spaces / underscores -> hyphens s = re.sub(r"[^a-z0-9\-.]", "", s) # strip non-ascii s = re.sub(r"-{2,}", "-", s) # collapse double hyphens return s.strip("-") +def slugify_path_segment(name: str) -> str: + """Slugify one path segment of a link, mapping Chinese section directory + names to their English slug (Chinese names would otherwise slugify to ""). + """ + decoded = unquote(name) + return slugify(ZH_SECTION_TITLES.get(decoded, decoded)) + + def extract_title_from_md(content: str) -> str: """Return the first H1 heading from Markdown content, or ''.""" for line in content.splitlines(): @@ -164,6 +174,12 @@ def strip_first_heading(content: str) -> str: # Link rewriting # --------------------------------------------------------------------------- +# Source .md file (resolved path, EN and ZH) -> output page path relative to +# OUT_DIR without extension, e.g. "components/agentic/envs". +# Filled in before any file is written so links can point at pages that are +# synced later. +PAGE_REGISTRY: dict[Path, str] = {} + # Mapping: broken relative path pattern -> corrected path # Supports both exact matches and regex-based rewrites LINK_REWRITES = { @@ -185,46 +201,60 @@ def strip_first_heading(content: str) -> str: } -def rewrite_links(content: str) -> str: +def rewrite_links(content: str, src_file: Path | None = None, + page_path: str | None = None) -> str: """Rewrite broken internal links to correct Hugo paths. - + Handles: 1. Exact path rewrites from LINK_REWRITES mapping - 2. Case-insensitive .md references (slugify them) + 2. References to synced .md files, resolved through PAGE_REGISTRY so the + exact output URL is known (works for English and Chinese sources) + 3. Fallback slugification for .md references outside the toctree + + ``src_file`` is the source file being converted and ``page_path`` its output + page path relative to OUT_DIR. Every synced file is a Hugo leaf page served + from a virtual directory (page.md -> /page/), so relative links resolve from + that virtual directory and need one extra "../" to reach a sibling. """ def replace_link(match): full_match = match.group(0) text = match.group(1) target = match.group(2) - + # Check exact rewrites (including URL-decoded version) - from urllib.parse import unquote decoded = unquote(target) if target in LINK_REWRITES: return f"[{text}]({LINK_REWRITES[target]})" if decoded in LINK_REWRITES: return f"[{text}]({LINK_REWRITES[decoded]})" - - # Auto-slugify .md references: "Dir/File.md" -> "dir/file/" - # NOTE: All synced files are Hugo leaf pages, which get a virtual - # directory (e.g. page.md -> URL /page/). Sibling references need - # "../" to escape the virtual directory first. - if target.endswith(".md") and not target.startswith(("http://", "https://")): - parts = target.split("/") - filename = parts[-1][:-3] # strip .md - slug = slugify(filename) - if len(parts) == 1: - # Bare sibling: "File.md" -> "../file/" - return f"[{text}](../{slug}/)" - # Reconstruct with relative prefix preserved - prefix = "/".join(p if p in (".", "..") else slugify(p) for p in parts[:-1]) - # "./File.md" -> prefix is ".", should become "../file/" - if prefix == ".": - return f"[{text}](../{slug}/)" - return f"[{text}]({prefix}/{slug}/)" - - return full_match - + + if target.startswith(("http://", "https://", "mailto:", "#", "/")): + return full_match + + path, _, anchor = decoded.partition("#") + if not path.endswith(".md"): + return full_match + suffix = f"#{anchor}" if anchor else "" + + # Preferred: the target is a synced page, so its output URL is known + if src_file is not None and page_path is not None: + resolved = (src_file.parent / path).resolve() + dest = PAGE_REGISTRY.get(resolved) + if dest: + rel = posixpath.relpath(dest, page_path) + return f"[{text}]({rel}/{suffix})" + + # Fallback: slugify the reference. "Dir/File.md" -> "../dir/file/" + parts = path.split("/") + slug = slugify(parts[-1][:-3]) # strip .md + rel_parts = [".."] # escape the virtual directory first + for part in parts[:-1]: + if part == ".": + continue + rel_parts.append(part if part == ".." else slugify_path_segment(part)) + rel = "/".join(p for p in rel_parts if p) + return f"[{text}]({rel}/{slug}/{suffix})" + return re.sub(r'\[([^\]]*)\]\(([^)]+)\)', replace_link, content) def parse_toctree(rst_path: Path) -> tuple[str, list[str]]: @@ -329,8 +359,22 @@ def write_index(out_dir: Path, title: str, weight: int, (out_dir / "_index.zh.md").write_text("\n".join(lines_zh), encoding="utf-8") +def queue_md_file(src_en: Path, src_zh: Path | None, + dest_dir: Path, weight: int, plan: list): + """Register one page's output path and queue it for writing. + + Registration must happen for every page before the first file is written, + otherwise links to pages processed later cannot be resolved. + """ + page_path = (dest_dir.relative_to(OUT_DIR) / slugify(src_en.stem)).as_posix() + PAGE_REGISTRY[src_en.resolve()] = page_path + if src_zh: + PAGE_REGISTRY[src_zh.resolve()] = page_path + plan.append((src_en, src_zh, dest_dir, weight, page_path)) + + def sync_md_file(src_en: Path, src_zh: Path | None, - dest_dir: Path, weight: int): + dest_dir: Path, weight: int, page_path: str): """Copy one English .md (and its Chinese counterpart) into dest_dir.""" dest_dir.mkdir(parents=True, exist_ok=True) @@ -342,14 +386,14 @@ def sync_md_file(src_en: Path, src_zh: Path | None, slug = slugify(src_en.stem) + ".md" # Check title overrides (key = relative path under OUT_DIR without .md) - override_key = str(dest_dir.relative_to(OUT_DIR) / slugify(src_en.stem)) + override_key = page_path if override_key in TITLE_OVERRIDES: title = TITLE_OVERRIDES[override_key][0] # Strip the first heading since Hugo uses front-matter title body = strip_first_heading(content_en) # Rewrite broken links - body = rewrite_links(body) + body = rewrite_links(body, src_en, page_path) out_en = add_front_matter(body, title, weight) (dest_dir / slug).write_text(out_en, encoding="utf-8") @@ -362,8 +406,8 @@ def sync_md_file(src_en: Path, src_zh: Path | None, if override_key in TITLE_OVERRIDES: title_zh = TITLE_OVERRIDES[override_key][1] body_zh = strip_first_heading(content_zh) - # Rewrite broken links - body_zh = rewrite_links(body_zh) + # Rewrite broken links (relative to the Chinese source location) + body_zh = rewrite_links(body_zh, src_zh, page_path) out_zh = add_front_matter(body_zh, title_zh, weight) zh_slug = slugify(src_en.stem) + ".zh.md" (dest_dir / zh_slug).write_text(out_zh, encoding="utf-8") @@ -444,7 +488,7 @@ def _recurse_section_map(en_index: Path, zh_index: Path, def process_section(en_section_dir: Path, out_base: Path, - section_weight: int, en_zh_map: dict): + section_weight: int, en_zh_map: dict, plan: list): """Recursively process an English section directory.""" index_rst = en_section_dir / "index.rst" if not index_rst.exists(): @@ -466,17 +510,17 @@ def process_section(en_section_dir: Path, out_base: Path, if entry.endswith("index.rst"): # Sub-section sub_dir = entry_path.parent - process_section(sub_dir, out_dir, sub_weight, en_zh_map) + process_section(sub_dir, out_dir, sub_weight, en_zh_map, plan) elif entry.endswith(".md"): if entry_path.exists(): zh_file = en_zh_map.get(entry_path) - sync_md_file(entry_path, zh_file, out_dir, sub_weight) + queue_md_file(entry_path, zh_file, out_dir, sub_weight, plan) sub_weight += 1 def process_top_level_section(section_name: str, en_base: Path, out_base: Path, section_weight: int, - en_zh_map: dict): + en_zh_map: dict, plan: list): """Process a top-level section (Usage Guide or Components).""" en_dir = en_base / section_name if not en_dir.exists(): @@ -504,11 +548,11 @@ def process_top_level_section(section_name: str, en_base: Path, if entry.endswith("index.rst"): # It's a sub-section directory sub_dir = entry_path.parent - process_section(sub_dir, out_dir, sub_weight, en_zh_map) + process_section(sub_dir, out_dir, sub_weight, en_zh_map, plan) elif entry.endswith(".md"): if entry_path.exists(): zh_file = en_zh_map.get(entry_path) - sync_md_file(entry_path, zh_file, out_dir, sub_weight) + queue_md_file(entry_path, zh_file, out_dir, sub_weight, plan) sub_weight += 1 @@ -532,15 +576,22 @@ def main(): shutil.rmtree(target) print(f" [CLEAN] {target}") + # Phase 1: walk the toctrees, write section indexes and register every page + plan: list = [] + # Process Usage Guide (weight=10, after getting-started at weight=1) process_top_level_section("Usage Guide", DOCS_EN, OUT_DIR, section_weight=10, - en_zh_map=en_zh_map) + en_zh_map=en_zh_map, plan=plan) # Process Components (weight=20) process_top_level_section("Components", DOCS_EN, OUT_DIR, section_weight=20, - en_zh_map=en_zh_map) + en_zh_map=en_zh_map, plan=plan) + + # Phase 2: write the pages, now that every link target is known + for src_en, src_zh, dest_dir, weight, page_path in plan: + sync_md_file(src_en, src_zh, dest_dir, weight, page_path) # Count output files total = sum(1 for _ in OUT_DIR.rglob("*.md")