@model-go/docs-core 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +77 -0
- package/assets/images/agents/cc-switch/claude-add.webp +0 -0
- package/assets/images/agents/cc-switch/config-cn.webp +0 -0
- package/assets/images/agents/cc-switch/config-global.webp +0 -0
- package/assets/images/agents/cc-switch/overview.webp +0 -0
- package/assets/images/agents/cc-switch/provider-cn.webp +0 -0
- package/assets/images/agents/cc-switch/provider-global.webp +0 -0
- package/assets/images/agents/cc-switch/use-cn.webp +0 -0
- package/assets/images/agents/cc-switch/use-global.webp +0 -0
- package/assets/images/agents/cherry-studio/add-provider.webp +0 -0
- package/assets/images/agents/cherry-studio/custom-provider-global.webp +0 -0
- package/assets/images/agents/cherry-studio/custom-provider.webp +0 -0
- package/assets/images/agents/cherry-studio/fetch-models-global.webp +0 -0
- package/assets/images/agents/cherry-studio/fetch-models.webp +0 -0
- package/assets/images/agents/cherry-studio/model-list-global.webp +0 -0
- package/assets/images/agents/cherry-studio/model-list.webp +0 -0
- package/assets/images/agents/cherry-studio/model-picker.webp +0 -0
- package/assets/images/agents/cline/cline-chat.webp +0 -0
- package/assets/images/agents/cline/cline-config-cn.webp +0 -0
- package/assets/images/agents/cline/cline-config-global.webp +0 -0
- package/assets/images/agents/cline/cline-market.webp +0 -0
- package/assets/images/agents/codex/codex-chat.webp +0 -0
- package/assets/images/agents/codex/codex-skip.webp +0 -0
- package/assets/images/agents/codex/codex-start.webp +0 -0
- package/assets/images/agents/cursor/add-model.webp +0 -0
- package/assets/images/agents/cursor/chat.webp +0 -0
- package/assets/images/agents/cursor/config-cn.webp +0 -0
- package/assets/images/agents/cursor/config-global.webp +0 -0
- package/assets/images/agents/cursor/install.webp +0 -0
- package/assets/images/agents/cursor/model-list.webp +0 -0
- package/assets/images/agents/cursor/settings.webp +0 -0
- package/assets/images/agents/deepseek-harness/install.webp +0 -0
- package/assets/images/agents/deepseek-harness/model-picker.webp +0 -0
- package/assets/images/agents/deepseek-harness/provider-config.webp +0 -0
- package/assets/images/agents/deepseek-harness/session.webp +0 -0
- package/assets/images/agents/deepseek-harness/settings.webp +0 -0
- package/assets/images/agents/kilo-code/jetbrains-byok.webp +0 -0
- package/assets/images/agents/kilo-code/jetbrains-config-cn.webp +0 -0
- package/assets/images/agents/kilo-code/jetbrains-config-global.webp +0 -0
- package/assets/images/agents/kilo-code/jetbrains-market.webp +0 -0
- package/assets/images/agents/kilo-code/jetbrains-panel.webp +0 -0
- package/assets/images/agents/kilo-code/jetbrains-start.webp +0 -0
- package/assets/images/agents/kilo-code/vscode-byok.webp +0 -0
- package/assets/images/agents/kilo-code/vscode-config-cn.webp +0 -0
- package/assets/images/agents/kilo-code/vscode-config-global.webp +0 -0
- package/assets/images/agents/kilo-code/vscode-market.webp +0 -0
- package/assets/images/agents/kilo-code/vscode-panel.webp +0 -0
- package/assets/images/agents/kilo-code/vscode-start.webp +0 -0
- package/assets/images/agents/opencode/chat.webp +0 -0
- package/assets/images/agents/opencode/model-search.webp +0 -0
- package/assets/images/agents/opencode/provider-filter.webp +0 -0
- package/assets/images/agents/opencode/provider-name.webp +0 -0
- package/assets/images/agents/zcode/chat.webp +0 -0
- package/assets/images/agents/zcode/install.webp +0 -0
- package/assets/images/agents/zcode/provider-fields.webp +0 -0
- package/assets/images/agents/zcode/provider-saved-global.webp +0 -0
- package/assets/images/agents/zcode/provider-saved.webp +0 -0
- package/assets/images/agents/zcode/setup.webp +0 -0
- package/dist/code-highlight.d.ts +1 -0
- package/dist/code-highlight.js +50 -0
- package/dist/content.generated.d.ts +2 -0
- package/dist/content.generated.js +4 -0
- package/dist/index.d.ts +67 -0
- package/dist/index.js +625 -0
- package/package.json +39 -0
- package/styles/docs-content.css +928 -0
- package/styles/docs-responsive.css +113 -0
- package/styles/docs-tokens.css +52 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,625 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @model-go/docs-core —— 文档内容与渲染的单一真源(SSOT)
|
|
3
|
+
*
|
|
4
|
+
* 内容(content/**)与渲染器(本文件)同源发布,消费方只负责调用:
|
|
5
|
+
* - docs.modelgo.com(modelgo-doc-web):linkBase = '',assetBase = ''
|
|
6
|
+
* - modelgo.ai/docs(modelgo-global-web):linkBase = '/docs' | '/zh/docs',assetBase = '/docs'
|
|
7
|
+
*
|
|
8
|
+
* 本文件是从 modelgo-doc-web 的 .vitepress/theme/composables/docs.ts 抽出的纯渲染部分:
|
|
9
|
+
* 不含任何浏览器 API(location / navigator / localStorage / sessionStorage),
|
|
10
|
+
* 可在 Node 服务端直接执行,因此官网能把它渲染成爬虫可抓的服务端 HTML。
|
|
11
|
+
*/
|
|
12
|
+
import { renderCodeBlock } from './code-highlight.js';
|
|
13
|
+
import { DOCS_PAGES, DOCS_METAS, DOCS_CHANGELOG } from './content.generated.js';
|
|
14
|
+
// ── 导航装配 ────────────────────────────────────────────────────────
|
|
15
|
+
function navItemId(item) {
|
|
16
|
+
return item.slug || item.path || item.title;
|
|
17
|
+
}
|
|
18
|
+
function adaptSectionChildren(children) {
|
|
19
|
+
const pages = [];
|
|
20
|
+
const subsections = [];
|
|
21
|
+
for (const child of children) {
|
|
22
|
+
if (child.type === 'page') {
|
|
23
|
+
pages.push({
|
|
24
|
+
id: navItemId(child),
|
|
25
|
+
title: child.title,
|
|
26
|
+
slug: child.slug,
|
|
27
|
+
description: child.description,
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
else if (child.type === 'section') {
|
|
31
|
+
const { pages: childPages, subsections: childSubs } = adaptSectionChildren(child.children ?? []);
|
|
32
|
+
if (childPages.length > 0 || childSubs.length > 0) {
|
|
33
|
+
subsections.push({
|
|
34
|
+
id: navItemId(child),
|
|
35
|
+
title: child.title,
|
|
36
|
+
pages: childPages,
|
|
37
|
+
children: childSubs.length > 0 ? childSubs : undefined,
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return { pages, subsections };
|
|
43
|
+
}
|
|
44
|
+
function adaptNav(nav) {
|
|
45
|
+
const sections = [];
|
|
46
|
+
for (const item of nav.items) {
|
|
47
|
+
if (item.type === 'page') {
|
|
48
|
+
sections.push({
|
|
49
|
+
id: navItemId(item),
|
|
50
|
+
title: null,
|
|
51
|
+
pages: [{
|
|
52
|
+
id: navItemId(item),
|
|
53
|
+
title: item.title,
|
|
54
|
+
slug: item.slug,
|
|
55
|
+
description: item.description,
|
|
56
|
+
}],
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
else if (item.type === 'section') {
|
|
60
|
+
const { pages, subsections } = adaptSectionChildren(item.children ?? []);
|
|
61
|
+
if (pages.length > 0 || subsections.length > 0) {
|
|
62
|
+
sections.push({
|
|
63
|
+
id: navItemId(item),
|
|
64
|
+
title: item.title,
|
|
65
|
+
pages,
|
|
66
|
+
children: subsections.length > 0 ? subsections : undefined,
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return { title: nav.title, description: nav.description, sections };
|
|
72
|
+
}
|
|
73
|
+
const PAGES = new Map(Object.entries(DOCS_PAGES));
|
|
74
|
+
const METAS = new Map(Object.entries(DOCS_METAS));
|
|
75
|
+
// 站点目录映射:cn → china, global → international
|
|
76
|
+
function getDocsContentDirectory(profile) {
|
|
77
|
+
return profile === 'cn' ? 'china' : 'international';
|
|
78
|
+
}
|
|
79
|
+
// ── HTML 转义 ─────────────────────────────────────────────────────
|
|
80
|
+
function escapeHtml(value) {
|
|
81
|
+
return value
|
|
82
|
+
.replaceAll('&', '&')
|
|
83
|
+
.replaceAll('<', '<')
|
|
84
|
+
.replaceAll('>', '>')
|
|
85
|
+
.replaceAll('"', '"')
|
|
86
|
+
.replaceAll("'", ''');
|
|
87
|
+
}
|
|
88
|
+
// 把「当前页所在目录」与 markdown 里写的相对路径(如 ../models/text/generation、errors)
|
|
89
|
+
// 解析成站内 slug(如 models/text/generation、quickstart/errors)——slug 规则与 meta.json
|
|
90
|
+
// 里手写的 slug 字段、以及 getDocsPage 的查找 key 完全一致,都是「相对 lang 目录的文件路径,无扩展名」。
|
|
91
|
+
function resolveRelativeSlug(currentSlug, relative) {
|
|
92
|
+
const source = relative.startsWith('/') ? relative : dirOf(currentSlug) + relative;
|
|
93
|
+
const stack = source.split('/');
|
|
94
|
+
const resolved = [];
|
|
95
|
+
for (const seg of stack) {
|
|
96
|
+
if (seg === '' || seg === '.')
|
|
97
|
+
continue;
|
|
98
|
+
if (seg === '..')
|
|
99
|
+
resolved.pop();
|
|
100
|
+
else
|
|
101
|
+
resolved.push(seg);
|
|
102
|
+
}
|
|
103
|
+
return resolved.join('/');
|
|
104
|
+
}
|
|
105
|
+
const EXTERNAL_LINK_RE = /^([a-z][a-z0-9+.-]*:|\/\/)/i;
|
|
106
|
+
// 渲染单个 [label](target) 为 <a>:
|
|
107
|
+
// - 外链 / mailto / 协议相对:新标签页打开
|
|
108
|
+
// - #anchor:页内锚点,标记 data-doc-link 交给统一路由同步 URL 与滚动位置
|
|
109
|
+
// - 站内相对路径:解析成 slug,若确实存在对应页面,标记 data-doc-link 交给
|
|
110
|
+
// DocsApp.vue 的点击委托做 SPA 内跳转;
|
|
111
|
+
// 若解析不到页面(如手滑打错路径),退化成纯文本,不渲染成一个点了没反应的死链接。
|
|
112
|
+
function renderLink(label, target, ctx) {
|
|
113
|
+
if (/^(?:javascript|data|vbscript):/i.test(target))
|
|
114
|
+
return label;
|
|
115
|
+
if (EXTERNAL_LINK_RE.test(target) || target.startsWith('mailto:')) {
|
|
116
|
+
return `<a href="${target}" target="_blank" rel="noreferrer">${label}</a>`;
|
|
117
|
+
}
|
|
118
|
+
if (target.startsWith('#')) {
|
|
119
|
+
return `<a href="${target}" data-doc-link>${label}</a>`;
|
|
120
|
+
}
|
|
121
|
+
const [path, hash] = target.split('#', 2);
|
|
122
|
+
const slug = resolveRelativeSlug(ctx.currentSlug, path.replace(/\.md$/, ''));
|
|
123
|
+
const exists = PAGES.has(`${ctx.siteDir}/${ctx.lang}/${slug}.md`);
|
|
124
|
+
if (!exists)
|
|
125
|
+
return label;
|
|
126
|
+
const href = `${ctx.linkBase}/${slug}${hash ? `#${hash}` : ''}`;
|
|
127
|
+
return `<a href="${href}" class="docs-inline-link" data-doc-link>${label}</a>`;
|
|
128
|
+
}
|
|
129
|
+
function inlineMarkdown(value, ctx) {
|
|
130
|
+
return escapeHtml(value)
|
|
131
|
+
.replace(/!\[([^\]]*)\]\(([^)\s]+)\)/g, (_all, alt, target) => {
|
|
132
|
+
// Images are rendered from local public assets or explicit http(s) URLs only;
|
|
133
|
+
// reject executable/data schemes before placing the escaped URL in markup.
|
|
134
|
+
if (!/^(?:https?:|\/|\.\.?\/)/i.test(target))
|
|
135
|
+
return alt;
|
|
136
|
+
// 站内图片走 assetBase,避免挂到官网后 /images/... 落到官网根路径
|
|
137
|
+
const src = target.startsWith('/') ? `${ctx.assetBase}${target}` : target;
|
|
138
|
+
return `<figure class="docs-image"><img src="${src}" alt="${alt}" loading="lazy" decoding="async"></figure>`;
|
|
139
|
+
})
|
|
140
|
+
.replace(/`([^`]+)`/g, '<code>$1</code>')
|
|
141
|
+
.replace(/\*\*([^*]+)\*\*/g, '<strong>$1</strong>')
|
|
142
|
+
.replace(/(?<!!)\[([^\]]+)\]\(([^)\s]+)\)/g, (_all, label, target) => renderLink(label, target, ctx));
|
|
143
|
+
}
|
|
144
|
+
function stripFrontmatter(content) {
|
|
145
|
+
return content.replace(/^---\r?\n[\s\S]*?\r?\n---\r?\n?/, '');
|
|
146
|
+
}
|
|
147
|
+
// ── Markdown 块级渲染 ───────────────────────────────────────────────
|
|
148
|
+
function renderMarkdown(content, ctx) {
|
|
149
|
+
const stripped = stripFrontmatter(content);
|
|
150
|
+
const lines = stripped.replace(/\r\n/g, '\n').split('\n');
|
|
151
|
+
let firstH1Skipped = false;
|
|
152
|
+
const html = [];
|
|
153
|
+
const toc = [];
|
|
154
|
+
const paragraph = [];
|
|
155
|
+
const tableRows = [];
|
|
156
|
+
const codeLines = [];
|
|
157
|
+
let listOpen = false;
|
|
158
|
+
let listType = null;
|
|
159
|
+
let codeOpen = false;
|
|
160
|
+
let codeLang = '';
|
|
161
|
+
let customBlock = null;
|
|
162
|
+
const closeParagraph = () => {
|
|
163
|
+
if (!paragraph.length)
|
|
164
|
+
return;
|
|
165
|
+
html.push(`<p>${inlineMarkdown(paragraph.join(' '), ctx)}</p>`);
|
|
166
|
+
paragraph.length = 0;
|
|
167
|
+
};
|
|
168
|
+
const closeList = () => {
|
|
169
|
+
if (!listOpen)
|
|
170
|
+
return;
|
|
171
|
+
html.push(`</${listType ?? 'ul'}>`);
|
|
172
|
+
listOpen = false;
|
|
173
|
+
listType = null;
|
|
174
|
+
};
|
|
175
|
+
const closeTable = () => {
|
|
176
|
+
if (!tableRows.length)
|
|
177
|
+
return;
|
|
178
|
+
const rows = tableRows
|
|
179
|
+
.map((row) => row
|
|
180
|
+
.replace(/^\|/, '')
|
|
181
|
+
.replace(/\|$/, '')
|
|
182
|
+
.split('|')
|
|
183
|
+
.map((cell) => cell.trim()))
|
|
184
|
+
.filter((cells) => !cells.every((cell) => /^:?-{3,}:?$/.test(cell)));
|
|
185
|
+
const [head, ...body] = rows;
|
|
186
|
+
if (head) {
|
|
187
|
+
html.push('<table>');
|
|
188
|
+
html.push(`<thead><tr>${head.map((cell) => `<th>${inlineMarkdown(cell, ctx)}</th>`).join('')}</tr></thead>`);
|
|
189
|
+
if (body.length) {
|
|
190
|
+
html.push('<tbody>');
|
|
191
|
+
body.forEach((row) => {
|
|
192
|
+
html.push(`<tr>${row.map((cell) => `<td>${inlineMarkdown(cell, ctx)}</td>`).join('')}</tr>`);
|
|
193
|
+
});
|
|
194
|
+
html.push('</tbody>');
|
|
195
|
+
}
|
|
196
|
+
html.push('</table>');
|
|
197
|
+
}
|
|
198
|
+
tableRows.length = 0;
|
|
199
|
+
};
|
|
200
|
+
const closeCode = () => {
|
|
201
|
+
html.push(renderCodeBlock(codeLines.join('\n'), codeLang));
|
|
202
|
+
codeLines.length = 0;
|
|
203
|
+
codeOpen = false;
|
|
204
|
+
codeLang = '';
|
|
205
|
+
};
|
|
206
|
+
const closeCustomBlock = () => {
|
|
207
|
+
if (!customBlock)
|
|
208
|
+
return;
|
|
209
|
+
const block = customBlock;
|
|
210
|
+
if (block.type === 'agent-tools') {
|
|
211
|
+
const rows = block.lines
|
|
212
|
+
.map((line) => line.trim())
|
|
213
|
+
.filter((line) => line.startsWith('|') && line.endsWith('|'))
|
|
214
|
+
.map((line) => line.replace(/^\|/, '').replace(/\|$/, '').split('|').map((cell) => cell.trim()))
|
|
215
|
+
.filter((cells) => cells.length === 3 && !cells.every((cell) => /^:?-{3,}:?$/.test(cell)));
|
|
216
|
+
const [head, ...body] = rows;
|
|
217
|
+
if (head) {
|
|
218
|
+
html.push('<table class="docs-agent-tools-table">');
|
|
219
|
+
html.push(`<thead><tr>${head.map((cell) => `<th>${inlineMarkdown(cell, ctx)}</th>`).join('')}</tr></thead>`);
|
|
220
|
+
if (body.length) {
|
|
221
|
+
html.push('<tbody>');
|
|
222
|
+
body.forEach(([tool, status, manualDocs]) => {
|
|
223
|
+
const statusKind = /^(支持|Supported)$/i.test(status) ? 'supported' : 'unavailable';
|
|
224
|
+
html.push('<tr>' +
|
|
225
|
+
`<td>${inlineMarkdown(tool, ctx)}</td>` +
|
|
226
|
+
`<td><span class="docs-agent-status-tag docs-agent-status-tag--${statusKind}">${inlineMarkdown(status, ctx)}</span></td>` +
|
|
227
|
+
`<td>${inlineMarkdown(manualDocs, ctx)}</td>` +
|
|
228
|
+
'</tr>');
|
|
229
|
+
});
|
|
230
|
+
html.push('</tbody>');
|
|
231
|
+
}
|
|
232
|
+
html.push('</table>');
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
else if (block.type === 'endpoints') {
|
|
236
|
+
const rows = block.lines
|
|
237
|
+
.map((line) => line.trim())
|
|
238
|
+
.filter(Boolean)
|
|
239
|
+
.map((line) => {
|
|
240
|
+
const separator = line.indexOf('|');
|
|
241
|
+
if (separator < 0)
|
|
242
|
+
return null;
|
|
243
|
+
return {
|
|
244
|
+
label: line.slice(0, separator).trim(),
|
|
245
|
+
value: line.slice(separator + 1).trim(),
|
|
246
|
+
};
|
|
247
|
+
})
|
|
248
|
+
.filter((row) => row !== null);
|
|
249
|
+
html.push('<div class="docs-endpoint-grid">');
|
|
250
|
+
rows.forEach(({ label, value }) => {
|
|
251
|
+
html.push('<div class="docs-endpoint-card">' +
|
|
252
|
+
`<span class="docs-endpoint-protocol">${inlineMarkdown(label, ctx)}</span>` +
|
|
253
|
+
`<code>${escapeHtml(value)}</code>` +
|
|
254
|
+
`<button class="code-copy-btn" type="button" title="Copy" aria-label="Copy ${escapeHtml(label)} Base URL">` +
|
|
255
|
+
'<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="13" height="13" rx="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>' +
|
|
256
|
+
'</button></div>');
|
|
257
|
+
});
|
|
258
|
+
html.push('</div>');
|
|
259
|
+
}
|
|
260
|
+
else if (block.type === 'clients') {
|
|
261
|
+
const clients = block.lines
|
|
262
|
+
.map((line) => line.trim())
|
|
263
|
+
.filter(Boolean);
|
|
264
|
+
const columns = Array.from({ length: 3 }, () => []);
|
|
265
|
+
clients.forEach((client, index) => columns[index % columns.length].push(client));
|
|
266
|
+
html.push('<div class="docs-client-grid">');
|
|
267
|
+
columns.forEach((column) => {
|
|
268
|
+
html.push('<div class="docs-client-column">');
|
|
269
|
+
column.forEach((client) => html.push(`<span class="docs-client-item">${escapeHtml(client)}</span>`));
|
|
270
|
+
html.push('</div>');
|
|
271
|
+
});
|
|
272
|
+
html.push('</div>');
|
|
273
|
+
}
|
|
274
|
+
else if (block.type === 'tools') {
|
|
275
|
+
const tools = block.lines
|
|
276
|
+
.map((line) => line.trim())
|
|
277
|
+
.filter(Boolean)
|
|
278
|
+
.map((line) => {
|
|
279
|
+
const [packageName, title, description, command, linkLabel, linkTarget] = line
|
|
280
|
+
.split('|')
|
|
281
|
+
.map((part) => part.trim());
|
|
282
|
+
if (!packageName || !title || !description || !command || !linkLabel || !linkTarget)
|
|
283
|
+
return null;
|
|
284
|
+
return { packageName, title, description, command, linkLabel, linkTarget };
|
|
285
|
+
})
|
|
286
|
+
.filter((tool) => tool !== null);
|
|
287
|
+
html.push('<div class="docs-tool-overview">');
|
|
288
|
+
tools.forEach(({ packageName, title, description, command, linkLabel, linkTarget }) => {
|
|
289
|
+
const sectionTarget = packageName === '@model-go/config' ? 'connect-ai-clients' : 'install-agent-tools';
|
|
290
|
+
html.push(`<section class="docs-tool-card" role="link" tabindex="0" data-doc-heading="${sectionTarget}" aria-label="${escapeHtml(title)}">` +
|
|
291
|
+
`<span class="docs-tool-package">${escapeHtml(packageName)}</span>` +
|
|
292
|
+
`<h3>${inlineMarkdown(title, ctx)}</h3>` +
|
|
293
|
+
`<p>${inlineMarkdown(description, ctx)}</p>` +
|
|
294
|
+
`<div class="docs-tool-command">${renderCodeBlock(command, 'bash')}</div>` +
|
|
295
|
+
`<div class="docs-tool-link">${renderLink(`${linkLabel} ↗`, linkTarget, ctx)}</div>` +
|
|
296
|
+
'</section>');
|
|
297
|
+
});
|
|
298
|
+
html.push('</div>');
|
|
299
|
+
}
|
|
300
|
+
else {
|
|
301
|
+
const items = block.lines
|
|
302
|
+
.map((line) => line.trim().match(/^[-*]\s+(.+)$/)?.[1])
|
|
303
|
+
.filter((item) => Boolean(item));
|
|
304
|
+
html.push('<aside class="docs-note">');
|
|
305
|
+
html.push('<div class="docs-note-title">' +
|
|
306
|
+
'<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="9"/><path d="M12 11v5"/><path d="M12 8h.01"/></svg>' +
|
|
307
|
+
`<span>${inlineMarkdown(block.title || 'Note', ctx)}</span></div>`);
|
|
308
|
+
if (items.length) {
|
|
309
|
+
html.push('<ul>');
|
|
310
|
+
items.forEach((item) => html.push(`<li>${inlineMarkdown(item, ctx)}</li>`));
|
|
311
|
+
html.push('</ul>');
|
|
312
|
+
}
|
|
313
|
+
html.push('</aside>');
|
|
314
|
+
}
|
|
315
|
+
customBlock = null;
|
|
316
|
+
};
|
|
317
|
+
lines.forEach((line) => {
|
|
318
|
+
const trimmed = line.trim();
|
|
319
|
+
if (customBlock) {
|
|
320
|
+
if (trimmed === ':::')
|
|
321
|
+
closeCustomBlock();
|
|
322
|
+
else
|
|
323
|
+
customBlock.lines.push(line);
|
|
324
|
+
return;
|
|
325
|
+
}
|
|
326
|
+
const customFence = trimmed.match(/^:::(agent-tools|clients|endpoints|note|tools)(?:\s+(.+))?$/);
|
|
327
|
+
if (customFence) {
|
|
328
|
+
closeParagraph();
|
|
329
|
+
closeList();
|
|
330
|
+
closeTable();
|
|
331
|
+
customBlock = {
|
|
332
|
+
type: customFence[1],
|
|
333
|
+
title: customFence[2]?.trim() ?? '',
|
|
334
|
+
lines: [],
|
|
335
|
+
};
|
|
336
|
+
return;
|
|
337
|
+
}
|
|
338
|
+
const codeFence = line.match(/^```(\w+)?\s*$/);
|
|
339
|
+
if (codeFence) {
|
|
340
|
+
closeParagraph();
|
|
341
|
+
closeList();
|
|
342
|
+
closeTable();
|
|
343
|
+
if (codeOpen) {
|
|
344
|
+
closeCode();
|
|
345
|
+
}
|
|
346
|
+
else {
|
|
347
|
+
codeOpen = true;
|
|
348
|
+
codeLang = codeFence[1] ?? '';
|
|
349
|
+
}
|
|
350
|
+
return;
|
|
351
|
+
}
|
|
352
|
+
if (codeOpen) {
|
|
353
|
+
codeLines.push(line);
|
|
354
|
+
return;
|
|
355
|
+
}
|
|
356
|
+
if (!trimmed) {
|
|
357
|
+
closeParagraph();
|
|
358
|
+
closeList();
|
|
359
|
+
closeTable();
|
|
360
|
+
return;
|
|
361
|
+
}
|
|
362
|
+
if (trimmed.startsWith('|') && trimmed.endsWith('|')) {
|
|
363
|
+
closeParagraph();
|
|
364
|
+
closeList();
|
|
365
|
+
tableRows.push(trimmed);
|
|
366
|
+
return;
|
|
367
|
+
}
|
|
368
|
+
const heading = trimmed.match(/^(#{1,6})\s+(.+)$/);
|
|
369
|
+
if (heading) {
|
|
370
|
+
closeParagraph();
|
|
371
|
+
closeList();
|
|
372
|
+
closeTable();
|
|
373
|
+
const level = heading[1].length;
|
|
374
|
+
const rawTitle = heading[2].trim();
|
|
375
|
+
const explicitAnchor = rawTitle.match(/\s+\{#([A-Za-z][\w-]*)\}$/);
|
|
376
|
+
const title = explicitAnchor ? rawTitle.slice(0, explicitAnchor.index).trim() : rawTitle;
|
|
377
|
+
const id = explicitAnchor?.[1] ?? `heading-${toc.length + 1}`;
|
|
378
|
+
toc.push({ id, title, level });
|
|
379
|
+
if (level === 1 && !firstH1Skipped) {
|
|
380
|
+
firstH1Skipped = true;
|
|
381
|
+
return;
|
|
382
|
+
}
|
|
383
|
+
html.push(`<h${level} id="${id}">${inlineMarkdown(title, ctx)}</h${level}>`);
|
|
384
|
+
return;
|
|
385
|
+
}
|
|
386
|
+
const unorderedItem = trimmed.match(/^[-*]\s+(.+)$/);
|
|
387
|
+
const orderedItem = trimmed.match(/^\d+[.)]\s+(.+)$/);
|
|
388
|
+
const listItem = unorderedItem ?? orderedItem;
|
|
389
|
+
if (listItem) {
|
|
390
|
+
closeParagraph();
|
|
391
|
+
closeTable();
|
|
392
|
+
const nextListType = orderedItem ? 'ol' : 'ul';
|
|
393
|
+
if (listOpen && listType !== nextListType)
|
|
394
|
+
closeList();
|
|
395
|
+
if (!listOpen) {
|
|
396
|
+
listType = nextListType;
|
|
397
|
+
html.push(`<${listType}>`);
|
|
398
|
+
listOpen = true;
|
|
399
|
+
}
|
|
400
|
+
html.push(`<li>${inlineMarkdown(listItem[1], ctx)}</li>`);
|
|
401
|
+
return;
|
|
402
|
+
}
|
|
403
|
+
const quote = trimmed.match(/^>\s+(.+)$/);
|
|
404
|
+
if (quote) {
|
|
405
|
+
closeParagraph();
|
|
406
|
+
closeList();
|
|
407
|
+
closeTable();
|
|
408
|
+
html.push(`<blockquote><p>${inlineMarkdown(quote[1], ctx)}</p></blockquote>`);
|
|
409
|
+
return;
|
|
410
|
+
}
|
|
411
|
+
const imageOnly = trimmed.match(/^!\[[^\]]*\]\(([^)\s]+)\)$/);
|
|
412
|
+
if (imageOnly) {
|
|
413
|
+
closeParagraph();
|
|
414
|
+
closeList();
|
|
415
|
+
closeTable();
|
|
416
|
+
html.push(inlineMarkdown(trimmed, ctx));
|
|
417
|
+
return;
|
|
418
|
+
}
|
|
419
|
+
closeTable();
|
|
420
|
+
paragraph.push(trimmed);
|
|
421
|
+
});
|
|
422
|
+
closeParagraph();
|
|
423
|
+
closeList();
|
|
424
|
+
closeTable();
|
|
425
|
+
closeCustomBlock();
|
|
426
|
+
if (codeOpen)
|
|
427
|
+
closeCode();
|
|
428
|
+
return { html: html.join('\n'), toc };
|
|
429
|
+
}
|
|
430
|
+
// ── 站点目录 ────────────────────────────────────────────────────────
|
|
431
|
+
// 站点目录映射:cn → china, global → international
|
|
432
|
+
function getDocsDir(profile) {
|
|
433
|
+
return getDocsContentDirectory(profile);
|
|
434
|
+
}
|
|
435
|
+
// ── 站点变量 ────────────────────────────────────────────────────────
|
|
436
|
+
const SITE_VARS = {
|
|
437
|
+
cn: {
|
|
438
|
+
platform_name: 'ModelGo',
|
|
439
|
+
site_endpoint: 'https://modelgo.com',
|
|
440
|
+
console_endpoint: 'https://console.modelgo.com',
|
|
441
|
+
api_endpoint: 'https://api.modelgo.com',
|
|
442
|
+
region_flag: 'domestic',
|
|
443
|
+
},
|
|
444
|
+
global: {
|
|
445
|
+
platform_name: 'ModelGo',
|
|
446
|
+
site_endpoint: 'https://modelgo.ai',
|
|
447
|
+
console_endpoint: 'https://console.modelgo.ai',
|
|
448
|
+
api_endpoint: 'https://api.modelgo.ai',
|
|
449
|
+
region_flag: 'global',
|
|
450
|
+
},
|
|
451
|
+
};
|
|
452
|
+
function substituteVars(text, profile) {
|
|
453
|
+
const vars = SITE_VARS[profile];
|
|
454
|
+
return text.replace(/\{\{\s*(\w+)\s*\}\}/g, (whole, key) => key in vars ? vars[key] : whole);
|
|
455
|
+
}
|
|
456
|
+
// ── 站点 API 地址 ───────────────────────────────────────────────────
|
|
457
|
+
/**
|
|
458
|
+
* 站点的模型调用网关地址(正文里的 `{{ api_endpoint }}` 用的同一个值)。
|
|
459
|
+
*
|
|
460
|
+
* 首页 hero 的「Base URL」卡片过去写死成 `/api/v1`——相对路径对用户毫无意义(他们要的是
|
|
461
|
+
* 能直接粘进 SDK 的完整地址),而且国内/国际两站显示同一个值。这里导出真源给它复用,
|
|
462
|
+
* 避免再各写一份(需求方 shared/modelgo_prd#376「文档内也需要更新」)。
|
|
463
|
+
*/
|
|
464
|
+
export function siteApiEndpoint(profile) {
|
|
465
|
+
return SITE_VARS[profile].api_endpoint;
|
|
466
|
+
}
|
|
467
|
+
// ── 路径工具 ────────────────────────────────────────────────────────
|
|
468
|
+
function dirOf(path) {
|
|
469
|
+
const i = path.lastIndexOf('/');
|
|
470
|
+
return i >= 0 ? path.slice(0, i + 1) : '';
|
|
471
|
+
}
|
|
472
|
+
function normalizeContentPath(path) {
|
|
473
|
+
const parts = [];
|
|
474
|
+
path.split('/').forEach((part) => {
|
|
475
|
+
if (!part || part === '.')
|
|
476
|
+
return;
|
|
477
|
+
if (part === '..')
|
|
478
|
+
parts.pop();
|
|
479
|
+
else
|
|
480
|
+
parts.push(part);
|
|
481
|
+
});
|
|
482
|
+
return parts.join('/');
|
|
483
|
+
}
|
|
484
|
+
// ── 导航解析 ────────────────────────────────────────────────────────
|
|
485
|
+
function resolveNavItem(item, dir, lang, profile) {
|
|
486
|
+
const siteDir = getDocsDir(profile);
|
|
487
|
+
const relPath = normalizeContentPath(dir + item.file);
|
|
488
|
+
if (item.type === 'page') {
|
|
489
|
+
const raw = PAGES.get(`${siteDir}/${lang}/${relPath}`);
|
|
490
|
+
if (!raw)
|
|
491
|
+
return null;
|
|
492
|
+
const { meta } = parseFrontmatter(raw);
|
|
493
|
+
return { type: 'page', slug: item.slug, title: meta.title || item.slug, description: meta.description };
|
|
494
|
+
}
|
|
495
|
+
const meta = METAS.get(`${siteDir}/${lang}/${relPath}`);
|
|
496
|
+
if (!meta)
|
|
497
|
+
return null;
|
|
498
|
+
const children = (meta.items ?? [])
|
|
499
|
+
.map((child) => resolveNavItem(child, dirOf(relPath), lang, profile))
|
|
500
|
+
.filter((child) => child !== null);
|
|
501
|
+
return { type: 'section', slug: item.slug, title: meta.title, children };
|
|
502
|
+
}
|
|
503
|
+
export async function getDocsNav(lang, profile) {
|
|
504
|
+
const siteDir = getDocsDir(profile);
|
|
505
|
+
const top = METAS.get(`${siteDir}/${lang}/meta.json`);
|
|
506
|
+
if (!top)
|
|
507
|
+
throw new Error(`文档导航缺失:${siteDir}/${lang}/meta.json`);
|
|
508
|
+
const items = (top.items ?? [])
|
|
509
|
+
.map((item) => resolveNavItem(item, '', lang, profile))
|
|
510
|
+
.filter((item) => item !== null);
|
|
511
|
+
return adaptNav({ lang, title: top.title, description: top.description ?? '', items });
|
|
512
|
+
}
|
|
513
|
+
// ── frontmatter ─────────────────────────────────────────────────
|
|
514
|
+
function parseFrontmatter(content) {
|
|
515
|
+
const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?/);
|
|
516
|
+
if (!match)
|
|
517
|
+
return { meta: {}, body: content };
|
|
518
|
+
const meta = {};
|
|
519
|
+
match[1].split('\n').forEach((line) => {
|
|
520
|
+
const i = line.indexOf(':');
|
|
521
|
+
if (i > 0)
|
|
522
|
+
meta[line.slice(0, i).trim()] = line.slice(i + 1).trim();
|
|
523
|
+
});
|
|
524
|
+
return { meta, body: content.slice(match[0].length) };
|
|
525
|
+
}
|
|
526
|
+
export async function getDocsPage(slug, lang, profile, render) {
|
|
527
|
+
const siteDir = getDocsDir(profile);
|
|
528
|
+
const clean = slug.replace(/^\/+/, '');
|
|
529
|
+
const raw = PAGES.get(`${siteDir}/${lang}/${clean}.md`);
|
|
530
|
+
if (!raw)
|
|
531
|
+
throw new Error(`文档页面不存在:${siteDir}/${lang}/${clean}`);
|
|
532
|
+
const { meta, body } = parseFrontmatter(substituteVars(raw, profile));
|
|
533
|
+
const rendered = renderMarkdown(body, {
|
|
534
|
+
siteDir,
|
|
535
|
+
lang,
|
|
536
|
+
currentSlug: clean,
|
|
537
|
+
linkBase: render?.linkBase ?? '',
|
|
538
|
+
assetBase: render?.assetBase ?? '',
|
|
539
|
+
});
|
|
540
|
+
return {
|
|
541
|
+
slug: clean,
|
|
542
|
+
title: meta.title || rendered.toc[0]?.title || clean,
|
|
543
|
+
html: rendered.html,
|
|
544
|
+
markdown: body,
|
|
545
|
+
toc: rendered.toc,
|
|
546
|
+
};
|
|
547
|
+
}
|
|
548
|
+
// 极简 markdown → 纯文本(用于全文检索与摘要,无外部依赖)
|
|
549
|
+
function toPlainText(markdown) {
|
|
550
|
+
return markdown
|
|
551
|
+
.replace(/^:::[^\n]*$/gm, ' ') // 自定义提示卡 / 地址卡片边界
|
|
552
|
+
.replace(/```[\s\S]*?```/g, ' ') // 代码块
|
|
553
|
+
.replace(/`([^`]+)`/g, '$1') // 行内 code
|
|
554
|
+
.replace(/!\[[^\]]*\]\([^)]*\)/g, ' ') // 图片
|
|
555
|
+
.replace(/\[([^\]]+)\]\([^)]*\)/g, '$1') // 链接保留文字
|
|
556
|
+
.replace(/^\s*[#>|]+/gm, ' ') // 标题 / 引用 / 表格分隔符
|
|
557
|
+
.replace(/^\s*[-*]\s+/gm, ' ') // 列表符号
|
|
558
|
+
.replace(/[*_~]/g, '') // 强调符号
|
|
559
|
+
.replace(/\s+/g, ' ')
|
|
560
|
+
.trim();
|
|
561
|
+
}
|
|
562
|
+
// 对已打包进内存的本地内容做客户端全文检索(标题命中优先)
|
|
563
|
+
export function searchDocs(query, lang, profile, limit = 20) {
|
|
564
|
+
const q = query.trim().toLowerCase();
|
|
565
|
+
if (!q)
|
|
566
|
+
return [];
|
|
567
|
+
const siteDir = getDocsDir(profile);
|
|
568
|
+
const prefix = `${siteDir}/${lang}/`;
|
|
569
|
+
const hits = [];
|
|
570
|
+
for (const [key, raw] of PAGES) {
|
|
571
|
+
if (!key.startsWith(prefix) || !key.endsWith('.md'))
|
|
572
|
+
continue;
|
|
573
|
+
const slug = key.slice(prefix.length, -3); // 去 `${siteDir}/${lang}/` 前缀与 `.md` 后缀 = slug
|
|
574
|
+
const { meta, body } = parseFrontmatter(substituteVars(raw, profile));
|
|
575
|
+
const title = meta.title || slug;
|
|
576
|
+
const text = toPlainText(body);
|
|
577
|
+
const titleHit = title.toLowerCase().includes(q);
|
|
578
|
+
const bodyIdx = text.toLowerCase().indexOf(q);
|
|
579
|
+
if (!titleHit && bodyIdx === -1)
|
|
580
|
+
continue;
|
|
581
|
+
let excerpt;
|
|
582
|
+
if (bodyIdx !== -1) {
|
|
583
|
+
const start = Math.max(0, bodyIdx - 40);
|
|
584
|
+
excerpt = `${start > 0 ? '…' : ''}${text.slice(start, bodyIdx + q.length + 80).trim()}…`;
|
|
585
|
+
}
|
|
586
|
+
else {
|
|
587
|
+
excerpt = `${text.slice(0, 120).trim()}…`;
|
|
588
|
+
}
|
|
589
|
+
hits.push({ slug, title, excerpt, score: titleHit ? 0 : 1 });
|
|
590
|
+
}
|
|
591
|
+
return hits
|
|
592
|
+
.sort((a, b) => a.score - b.score || a.title.localeCompare(b.title))
|
|
593
|
+
.slice(0, limit)
|
|
594
|
+
.map((r) => ({ slug: r.slug, title: r.title, excerpt: r.excerpt }));
|
|
595
|
+
}
|
|
596
|
+
/**
|
|
597
|
+
* 某站点 + 语言下的全部文档 slug(相对 <profile>/<lang>/ 的文件路径,无扩展名)。
|
|
598
|
+
* 官网构建期用它生成静态路由(getStaticPaths)。
|
|
599
|
+
*/
|
|
600
|
+
export function getDocSlugs(lang, profile) {
|
|
601
|
+
const siteDir = getDocsContentDirectory(profile);
|
|
602
|
+
const prefix = `${siteDir}/${lang}/`;
|
|
603
|
+
const slugs = [];
|
|
604
|
+
for (const key of PAGES.keys()) {
|
|
605
|
+
if (!key.startsWith(prefix) || !key.endsWith('.md'))
|
|
606
|
+
continue;
|
|
607
|
+
slugs.push(key.slice(prefix.length, -3));
|
|
608
|
+
}
|
|
609
|
+
return slugs.sort();
|
|
610
|
+
}
|
|
611
|
+
/**
|
|
612
|
+
* 更新日志(仓库根 changelog.md):不分市场/语言,只有一份。
|
|
613
|
+
* 它原来走 VitePress 自己的页面渲染(客户端再渲染),并入官网后同样要吃服务端 HTML。
|
|
614
|
+
*/
|
|
615
|
+
export function getChangelog(profile, render) {
|
|
616
|
+
const { meta, body } = parseFrontmatter(substituteVars(DOCS_CHANGELOG, profile));
|
|
617
|
+
const rendered = renderMarkdown(body, {
|
|
618
|
+
siteDir: getDocsContentDirectory(profile),
|
|
619
|
+
lang: 'en',
|
|
620
|
+
currentSlug: 'changelog',
|
|
621
|
+
linkBase: render?.linkBase ?? '',
|
|
622
|
+
assetBase: render?.assetBase ?? '',
|
|
623
|
+
});
|
|
624
|
+
return { title: meta.title || 'Changelog', html: rendered.html, toc: rendered.toc };
|
|
625
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@model-go/docs-core",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "ModelGo 文档内容与渲染器的单一真源(SSOT):内容 + 纯渲染器,供文档站与官网共用",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"import": "./dist/index.js"
|
|
12
|
+
},
|
|
13
|
+
"./content": {
|
|
14
|
+
"types": "./dist/content.generated.d.ts",
|
|
15
|
+
"import": "./dist/content.generated.js"
|
|
16
|
+
},
|
|
17
|
+
"./styles/*": "./styles/*",
|
|
18
|
+
"./assets/*": "./assets/*"
|
|
19
|
+
},
|
|
20
|
+
"files": [
|
|
21
|
+
"dist",
|
|
22
|
+
"styles",
|
|
23
|
+
"assets",
|
|
24
|
+
"README.md"
|
|
25
|
+
],
|
|
26
|
+
"publishConfig": {
|
|
27
|
+
"access": "public"
|
|
28
|
+
},
|
|
29
|
+
"devDependencies": {
|
|
30
|
+
"typescript": "^5.9.3"
|
|
31
|
+
},
|
|
32
|
+
"scripts": {
|
|
33
|
+
"build": "tsc -p tsconfig.json && node scripts/build-content.mjs",
|
|
34
|
+
"sync:assets": "node scripts/sync-assets.mjs",
|
|
35
|
+
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
36
|
+
"gate:prepublish": "node scripts/prepublish-gate.mjs",
|
|
37
|
+
"gate:readme": "node scripts/readme-gate.mjs"
|
|
38
|
+
}
|
|
39
|
+
}
|