添加插件文档和代码更新,支持可配置的文件标题来源,增强导出功能
This commit is contained in:
96
.github/copilot-instructions.md
vendored
96
.github/copilot-instructions.md
vendored
@@ -39,6 +39,14 @@ README 文件应包含以下内容:
|
||||
- 故障排除指南 / Troubleshooting guide
|
||||
- 版本和作者信息 / Version and author information
|
||||
|
||||
### 官方文档 (Official Documentation)
|
||||
|
||||
如果插件被合并到主仓库,还需更新 `docs/` 目录下的相关文档:
|
||||
- `docs/plugins/{type}/plugin-name.md`
|
||||
- `docs/plugins/{type}/plugin-name.zh.md`
|
||||
|
||||
其中 `{type}` 对应插件类型(如 `actions`, `filters`, `pipes` 等)。
|
||||
|
||||
---
|
||||
|
||||
## 📝 文档字符串规范 (Docstring Standard)
|
||||
@@ -370,6 +378,94 @@ from open_webui.models.users import Users
|
||||
|
||||
---
|
||||
|
||||
## 📄 文件导出与命名规范 (File Export and Naming)
|
||||
|
||||
对于涉及文件导出的插件(通常是 Action),必须提供灵活的标题生成策略。
|
||||
|
||||
### Valves 配置 (Valves Configuration)
|
||||
|
||||
应包含 `TITLE_SOURCE` 选项:
|
||||
|
||||
```python
|
||||
class Valves(BaseModel):
|
||||
TITLE_SOURCE: str = Field(
|
||||
default="chat_title",
|
||||
description="Title Source: 'chat_title', 'ai_generated', 'markdown_title'",
|
||||
)
|
||||
```
|
||||
|
||||
### 标题获取逻辑 (Title Retrieval Logic)
|
||||
|
||||
1. **chat_title**: 尝试从 `body` 获取,若失败且有 `chat_id`,则从数据库获取 (`Chats.get_chat_by_id`)。
|
||||
2. **markdown_title**: 从 Markdown 内容提取第一个 H1 或 H2。
|
||||
3. **ai_generated**: 使用轻量级 Prompt 让 AI 生成简短标题。
|
||||
|
||||
### 优先级与回退 (Priority and Fallback)
|
||||
|
||||
代码应根据 `TITLE_SOURCE` 优先尝试指定方法,若失败则按以下顺序回退:
|
||||
`chat_title` -> `markdown_title` -> `user_name + date`
|
||||
|
||||
```python
|
||||
# 核心逻辑示例
|
||||
if self.valves.TITLE_SOURCE == "chat_title":
|
||||
title = chat_title
|
||||
elif self.valves.TITLE_SOURCE == "markdown_title":
|
||||
title = self.extract_title(content)
|
||||
elif self.valves.TITLE_SOURCE == "ai_generated":
|
||||
title = await self.generate_title_using_ai(...)
|
||||
```
|
||||
|
||||
### AI 标题生成实现 (AI Title Generation Implementation)
|
||||
|
||||
如果支持 `ai_generated` 选项,应实现类似以下的方法:
|
||||
|
||||
```python
|
||||
async def generate_title_using_ai(
|
||||
self,
|
||||
body: dict,
|
||||
content: str,
|
||||
user_id: str,
|
||||
request: Any
|
||||
) -> str:
|
||||
"""Generates a short title using the current LLM model."""
|
||||
if not request:
|
||||
return ""
|
||||
|
||||
try:
|
||||
# 获取当前用户和模型
|
||||
user_obj = Users.get_user_by_id(user_id)
|
||||
model = body.get("model")
|
||||
|
||||
# 构造请求
|
||||
payload = {
|
||||
"model": model,
|
||||
"messages": [
|
||||
{
|
||||
"role": "system",
|
||||
"content": "You are a helpful assistant. Generate a short, concise title (max 10 words) for the following text. Do not use quotes. Only output the title."
|
||||
},
|
||||
{
|
||||
"role": "user",
|
||||
"content": content[:2000] # 限制上下文长度以节省 Token
|
||||
}
|
||||
],
|
||||
"stream": False,
|
||||
}
|
||||
|
||||
# 调用 OpenWebUI 内部生成接口
|
||||
response = await generate_chat_completion(request, payload, user_obj)
|
||||
|
||||
if response and "choices" in response:
|
||||
return response["choices"][0]["message"]["content"].strip()
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error generating title: {e}")
|
||||
|
||||
return ""
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ✅ 开发检查清单 (Development Checklist)
|
||||
|
||||
开发新插件时,请确保完成以下检查:
|
||||
|
||||
@@ -17,7 +17,17 @@ The Export to Word plugin converts chat messages from Markdown to a polished Wor
|
||||
- :material-format-bold: **Rich Markdown Support**: Headings, bold/italic, lists, tables
|
||||
- :material-code-tags: **Syntax Highlighting**: Pygments-powered code blocks
|
||||
- :material-format-quote-close: **Styled Blockquotes**: Left-border gray quote styling
|
||||
- :material-file-document-outline: **Smart Filenames**: Prefers chat title → Markdown title → user/date
|
||||
- :material-file-document-outline: **Smart Filenames**: Configurable title source (Chat Title, AI Generated, or Markdown Title)
|
||||
|
||||
---
|
||||
|
||||
## Configuration
|
||||
|
||||
You can configure the following settings via the **Valves** button in the plugin settings:
|
||||
|
||||
| Valve | Description | Default |
|
||||
| :------------- | :------------------------------------------------------------------------------------------ | :----------- |
|
||||
| `TITLE_SOURCE` | Source for document title/filename. Options: `chat_title`, `ai_generated`, `markdown_title` | `chat_title` |
|
||||
|
||||
---
|
||||
|
||||
@@ -39,21 +49,21 @@ The Export to Word plugin converts chat messages from Markdown to a polished Wor
|
||||
|
||||
## Supported Markdown
|
||||
|
||||
| Syntax | Word Result |
|
||||
| :---------------------------------- | :-------------------------------- |
|
||||
| `# Heading 1` to `###### Heading 6` | Heading levels 1-6 |
|
||||
| `**bold**` / `__bold__` | Bold text |
|
||||
| `*italic*` / `_italic_` | Italic text |
|
||||
| `***bold italic***` | Bold + Italic |
|
||||
| `` `inline code` `` | Monospace with gray background |
|
||||
| <code>``` code block ```</code> | Syntax-highlighted code block |
|
||||
| `> blockquote` | Left-bordered gray italic text |
|
||||
| `[link](url)` | Blue underlined link |
|
||||
| `~~strikethrough~~` | Strikethrough |
|
||||
| `- item` / `* item` | Bullet list |
|
||||
| `1. item` | Numbered list |
|
||||
| Markdown tables | Grid table |
|
||||
| `---` / `***` | Horizontal rule |
|
||||
| Syntax | Word Result |
|
||||
| :---------------------------------- | :----------------------------- |
|
||||
| `# Heading 1` to `###### Heading 6` | Heading levels 1-6 |
|
||||
| `**bold**` / `__bold__` | Bold text |
|
||||
| `*italic*` / `_italic_` | Italic text |
|
||||
| `***bold italic***` | Bold + Italic |
|
||||
| `` `inline code` `` | Monospace with gray background |
|
||||
| <code>``` code block ```</code> | Syntax-highlighted code block |
|
||||
| `> blockquote` | Left-bordered gray italic text |
|
||||
| `[link](url)` | Blue underlined link |
|
||||
| `~~strikethrough~~` | Strikethrough |
|
||||
| `- item` / `* item` | Bullet list |
|
||||
| `1. item` | Numbered list |
|
||||
| Markdown tables | Grid table |
|
||||
| `---` / `***` | Horizontal rule |
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -17,7 +17,17 @@ Export to Word 插件会把聊天消息从 Markdown 转成精致的 Word 文档
|
||||
- :material-format-bold: **丰富 Markdown 支持**:标题、粗斜体、列表、表格
|
||||
- :material-code-tags: **语法高亮**:Pygments 驱动的代码块上色
|
||||
- :material-format-quote-close: **引用样式**:左侧边框的灰色斜体引用
|
||||
- :material-file-document-outline: **智能文件名**:优先对话标题 → Markdown 标题 → 用户/日期
|
||||
- :material-file-document-outline: **智能文件名**:可配置标题来源(对话标题、AI 生成或 Markdown 标题)
|
||||
|
||||
---
|
||||
|
||||
## 配置
|
||||
|
||||
您可以通过插件设置中的 **Valves** 按钮配置以下选项:
|
||||
|
||||
| Valve | 说明 | 默认值 |
|
||||
| :------------- | :--------------------------------------------------------------------------------------------------------------- | :----------- |
|
||||
| `TITLE_SOURCE` | 文档标题/文件名的来源。选项:`chat_title` (对话标题), `ai_generated` (AI 生成), `markdown_title` (Markdown 标题) | `chat_title` |
|
||||
|
||||
---
|
||||
|
||||
@@ -39,21 +49,21 @@ Export to Word 插件会把聊天消息从 Markdown 转成精致的 Word 文档
|
||||
|
||||
## 支持的 Markdown
|
||||
|
||||
| 语法 | Word 效果 |
|
||||
| :---------------------------------- | :-------------------------------- |
|
||||
| `# 标题1` 到 `###### 标题6` | 标题级别 1-6 |
|
||||
| `**粗体**` / `__粗体__` | 粗体文本 |
|
||||
| `*斜体*` / `_斜体_` | 斜体文本 |
|
||||
| `***粗斜体***` | 粗体 + 斜体 |
|
||||
| `` `行内代码` `` | 等宽字体 + 灰色背景 |
|
||||
| <code>``` 代码块 ```</code> | 语法高亮代码块 |
|
||||
| `> 引用文本` | 左侧边框的灰色斜体 |
|
||||
| `[链接](url)` | 蓝色下划线链接 |
|
||||
| `~~删除线~~` | 删除线 |
|
||||
| `- 项目` / `* 项目` | 无序列表 |
|
||||
| `1. 项目` | 有序列表 |
|
||||
| Markdown 表格 | 带边框表格 |
|
||||
| `---` / `***` | 水平分割线 |
|
||||
| 语法 | Word 效果 |
|
||||
| :-------------------------- | :------------------ |
|
||||
| `# 标题1` 到 `###### 标题6` | 标题级别 1-6 |
|
||||
| `**粗体**` / `__粗体__` | 粗体文本 |
|
||||
| `*斜体*` / `_斜体_` | 斜体文本 |
|
||||
| `***粗斜体***` | 粗体 + 斜体 |
|
||||
| `` `行内代码` `` | 等宽字体 + 灰色背景 |
|
||||
| <code>``` 代码块 ```</code> | 语法高亮代码块 |
|
||||
| `> 引用文本` | 左侧边框的灰色斜体 |
|
||||
| `[链接](url)` | 蓝色下划线链接 |
|
||||
| `~~删除线~~` | 删除线 |
|
||||
| `- 项目` / `* 项目` | 无序列表 |
|
||||
| `1. 项目` | 有序列表 |
|
||||
| Markdown 表格 | 带边框表格 |
|
||||
| `---` / `***` | 水平分割线 |
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -9,7 +9,16 @@ Export current conversation from Markdown to Word (.docx) with **syntax highligh
|
||||
- **Syntax Highlighting**: Code blocks are highlighted with Pygments (supports 500+ languages).
|
||||
- **Blockquote Support**: Markdown blockquotes are rendered with left border and gray styling.
|
||||
- **Multi-language Support**: Properly handles both Chinese and English text without garbled characters.
|
||||
- **Smarter Filenames**: Prefers chat title (from body or chat_id lookup) → first Markdown h1/h2 → user + date.
|
||||
- **Smarter Filenames**: Configurable title source (Chat Title, AI Generated, or Markdown Title).
|
||||
|
||||
## Configuration
|
||||
|
||||
You can configure the following settings via the **Valves** button in the plugin settings:
|
||||
|
||||
- **TITLE_SOURCE**: Choose how the document title/filename is generated.
|
||||
- `chat_title`: Use the conversation title (default).
|
||||
- `ai_generated`: Use AI to generate a short title based on the content.
|
||||
- `markdown_title`: Extract the first h1/h2 heading from the Markdown content.
|
||||
|
||||
## Supported Markdown Syntax
|
||||
|
||||
|
||||
@@ -9,7 +9,16 @@
|
||||
- **代码语法高亮**:使用 Pygments 库为代码块添加语法高亮(支持 500+ 种语言)。
|
||||
- **引用块支持**:Markdown 引用块会渲染为带左侧边框的灰色斜体样式。
|
||||
- **多语言支持**:正确处理中文和英文文本,无乱码问题。
|
||||
- **更智能的文件名**:优先使用对话标题(来自请求体或基于 chat_id 查询),其次 Markdown 一级/二级标题,最后用户+日期。
|
||||
- **更智能的文件名**:可配置标题来源(对话标题、AI 生成或 Markdown 标题)。
|
||||
|
||||
## 配置 (Configuration)
|
||||
|
||||
您可以通过插件设置中的 **Valves** 按钮配置以下选项:
|
||||
|
||||
- **TITLE_SOURCE**:选择文档标题/文件名的生成方式。
|
||||
- `chat_title`:使用对话标题(默认)。
|
||||
- `ai_generated`:使用 AI 根据内容生成简短标题。
|
||||
- `markdown_title`:从 Markdown 内容中提取第一个一级或二级标题。
|
||||
|
||||
## 支持的 Markdown 语法
|
||||
|
||||
|
||||
@@ -25,6 +25,9 @@ from docx.enum.style import WD_STYLE_TYPE
|
||||
from docx.oxml.ns import qn
|
||||
from docx.oxml import OxmlElement
|
||||
from open_webui.models.chats import Chats
|
||||
from open_webui.models.users import Users
|
||||
from open_webui.utils.chat import generate_chat_completion
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
# Pygments for syntax highlighting
|
||||
try:
|
||||
@@ -45,8 +48,14 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Action:
|
||||
class Valves(BaseModel):
|
||||
TITLE_SOURCE: str = Field(
|
||||
default="chat_title",
|
||||
description="Title Source: 'chat_title' (Chat Title), 'ai_generated' (AI Generated), 'markdown_title' (Markdown Title)",
|
||||
)
|
||||
|
||||
def __init__(self):
|
||||
pass
|
||||
self.valves = self.Valves()
|
||||
|
||||
async def _send_notification(self, emitter: Callable, type: str, content: str):
|
||||
await emitter(
|
||||
@@ -60,6 +69,7 @@ class Action:
|
||||
__event_emitter__=None,
|
||||
__event_call__: Optional[Callable[[Any], Awaitable[None]]] = None,
|
||||
__metadata__: Optional[dict] = None,
|
||||
__request__: Optional[Any] = None,
|
||||
):
|
||||
logger.info(f"action:{__name__}")
|
||||
|
||||
@@ -101,26 +111,54 @@ class Action:
|
||||
)
|
||||
return
|
||||
|
||||
# Generate filename (prefer chat title; fetch via chat_id if missing; then markdown title; then fallback)
|
||||
# Generate filename
|
||||
title = ""
|
||||
chat_id = self.extract_chat_id(body, __metadata__)
|
||||
chat_title = self.extract_chat_title(body)
|
||||
if not chat_title and chat_id:
|
||||
|
||||
# Fetch chat_title directly via chat_id as it's usually missing in body
|
||||
chat_title = ""
|
||||
if chat_id:
|
||||
chat_title = await self.fetch_chat_title(chat_id, user_id)
|
||||
title = self.extract_title(message_content)
|
||||
|
||||
if (
|
||||
self.valves.TITLE_SOURCE == "chat_title"
|
||||
or not self.valves.TITLE_SOURCE
|
||||
):
|
||||
title = chat_title
|
||||
elif self.valves.TITLE_SOURCE == "markdown_title":
|
||||
title = self.extract_title(message_content)
|
||||
elif self.valves.TITLE_SOURCE == "ai_generated":
|
||||
title = await self.generate_title_using_ai(
|
||||
body, message_content, user_id, __request__
|
||||
)
|
||||
|
||||
# Fallback logic
|
||||
if not title:
|
||||
if self.valves.TITLE_SOURCE != "chat_title" and chat_title:
|
||||
title = chat_title
|
||||
elif self.valves.TITLE_SOURCE != "markdown_title":
|
||||
extracted = self.extract_title(message_content)
|
||||
if extracted:
|
||||
title = extracted
|
||||
|
||||
current_datetime = datetime.datetime.now()
|
||||
formatted_date = current_datetime.strftime("%Y%m%d")
|
||||
|
||||
if chat_title:
|
||||
filename = f"{self.clean_filename(chat_title)}.docx"
|
||||
elif title:
|
||||
if title:
|
||||
filename = f"{self.clean_filename(title)}.docx"
|
||||
else:
|
||||
filename = f"{user_name}_{formatted_date}.docx"
|
||||
|
||||
top_heading = ""
|
||||
if chat_title:
|
||||
top_heading = chat_title
|
||||
elif title:
|
||||
top_heading = title
|
||||
|
||||
# Create Word document; if no h1 exists, inject chat title as h1
|
||||
has_h1 = bool(re.search(r"^#\s+.+$", message_content, re.MULTILINE))
|
||||
doc = self.markdown_to_docx(
|
||||
message_content, top_heading=chat_title, has_h1=has_h1
|
||||
message_content, top_heading=top_heading, has_h1=has_h1
|
||||
)
|
||||
|
||||
# Save to memory
|
||||
@@ -194,6 +232,36 @@ class Action:
|
||||
f"Error exporting Word document: {str(e)}",
|
||||
)
|
||||
|
||||
async def generate_title_using_ai(
|
||||
self, body: dict, content: str, user_id: str, request: Any
|
||||
) -> str:
|
||||
if not request:
|
||||
return ""
|
||||
|
||||
try:
|
||||
user_obj = Users.get_user_by_id(user_id)
|
||||
model = body.get("model")
|
||||
|
||||
payload = {
|
||||
"model": model,
|
||||
"messages": [
|
||||
{
|
||||
"role": "system",
|
||||
"content": "You are a helpful assistant. Generate a short, concise title (max 10 words) for the following text. Do not use quotes. Only output the title.",
|
||||
},
|
||||
{"role": "user", "content": content[:2000]}, # Limit content length
|
||||
],
|
||||
"stream": False,
|
||||
}
|
||||
|
||||
response = await generate_chat_completion(request, payload, user_obj)
|
||||
if response and "choices" in response:
|
||||
return response["choices"][0]["message"]["content"].strip()
|
||||
except Exception as e:
|
||||
logger.error(f"Error generating title: {e}")
|
||||
|
||||
return ""
|
||||
|
||||
def extract_title(self, content: str) -> str:
|
||||
"""Extract title from Markdown h1/h2 only"""
|
||||
lines = content.split("\n")
|
||||
|
||||
@@ -25,6 +25,9 @@ from docx.enum.style import WD_STYLE_TYPE
|
||||
from docx.oxml.ns import qn
|
||||
from docx.oxml import OxmlElement
|
||||
from open_webui.models.chats import Chats
|
||||
from open_webui.models.users import Users
|
||||
from open_webui.utils.chat import generate_chat_completion
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
# Pygments for syntax highlighting
|
||||
try:
|
||||
@@ -45,8 +48,14 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Action:
|
||||
class Valves(BaseModel):
|
||||
TITLE_SOURCE: str = Field(
|
||||
default="chat_title",
|
||||
description="标题来源: 'chat_title' (对话标题), 'ai_generated' (AI 生成), 'markdown_title' (Markdown 标题)",
|
||||
)
|
||||
|
||||
def __init__(self):
|
||||
pass
|
||||
self.valves = self.Valves()
|
||||
|
||||
async def _send_notification(self, emitter: Callable, type: str, content: str):
|
||||
await emitter(
|
||||
@@ -60,6 +69,7 @@ class Action:
|
||||
__event_emitter__=None,
|
||||
__event_call__: Optional[Callable[[Any], Awaitable[None]]] = None,
|
||||
__metadata__: Optional[dict] = None,
|
||||
__request__: Optional[Any] = None,
|
||||
):
|
||||
logger.info(f"action:{__name__}")
|
||||
|
||||
@@ -98,26 +108,54 @@ class Action:
|
||||
)
|
||||
return
|
||||
|
||||
# 生成文件名(优先对话标题;若缺失则通过 chat_id 查询;再到 Markdown 标题;最后用户+日期)
|
||||
# 生成文件名
|
||||
title = ""
|
||||
chat_id = self.extract_chat_id(body, __metadata__)
|
||||
chat_title = self.extract_chat_title(body)
|
||||
if not chat_title and chat_id:
|
||||
|
||||
# 直接通过 chat_id 获取标题,因为 body 中通常不包含标题
|
||||
chat_title = ""
|
||||
if chat_id:
|
||||
chat_title = await self.fetch_chat_title(chat_id, user_id)
|
||||
title = self.extract_title(message_content)
|
||||
|
||||
# 根据配置决定文件名使用的标题
|
||||
if (
|
||||
self.valves.TITLE_SOURCE == "chat_title"
|
||||
or not self.valves.TITLE_SOURCE
|
||||
):
|
||||
title = chat_title
|
||||
elif self.valves.TITLE_SOURCE == "markdown_title":
|
||||
title = self.extract_title(message_content)
|
||||
elif self.valves.TITLE_SOURCE == "ai_generated":
|
||||
title = await self.generate_title_using_ai(
|
||||
body, message_content, user_id, __request__
|
||||
)
|
||||
|
||||
current_datetime = datetime.datetime.now()
|
||||
formatted_date = current_datetime.strftime("%Y%m%d")
|
||||
|
||||
if chat_title:
|
||||
filename = f"{self.clean_filename(chat_title)}.docx"
|
||||
elif title:
|
||||
if title:
|
||||
filename = f"{self.clean_filename(title)}.docx"
|
||||
else:
|
||||
filename = f"{user_name}_{formatted_date}.docx"
|
||||
|
||||
# 创建 Word 文档;若正文无一级标题,使用对话标题作为一级标题
|
||||
# 如果选择了 chat_title 且获取到了,则作为 top_heading
|
||||
# 如果选择了其他方式,title 就是文件名,也可以作为 top_heading
|
||||
|
||||
# 保持原有逻辑:top_heading 主要是为了在文档开头补充标题
|
||||
# 这里我们尽量使用 chat_title 作为 top_heading,如果它存在的话,因为它通常是对话的主题
|
||||
# 即使文件名是 AI 生成的,文档内的标题用 chat_title 也是合理的
|
||||
# 但如果用户选择了 markdown_title,可能不希望插入 chat_title
|
||||
|
||||
top_heading = ""
|
||||
if chat_title:
|
||||
top_heading = chat_title
|
||||
elif title:
|
||||
top_heading = title
|
||||
|
||||
has_h1 = bool(re.search(r"^#\s+.+$", message_content, re.MULTILINE))
|
||||
doc = self.markdown_to_docx(
|
||||
message_content, top_heading=chat_title, has_h1=has_h1
|
||||
message_content, top_heading=top_heading, has_h1=has_h1
|
||||
)
|
||||
|
||||
# 保存到内存
|
||||
@@ -189,6 +227,36 @@ class Action:
|
||||
__event_emitter__, "error", f"导出 Word 文档时出错: {str(e)}"
|
||||
)
|
||||
|
||||
async def generate_title_using_ai(
|
||||
self, body: dict, content: str, user_id: str, request: Any
|
||||
) -> str:
|
||||
if not request:
|
||||
return ""
|
||||
|
||||
try:
|
||||
user_obj = Users.get_user_by_id(user_id)
|
||||
model = body.get("model")
|
||||
|
||||
payload = {
|
||||
"model": model,
|
||||
"messages": [
|
||||
{
|
||||
"role": "system",
|
||||
"content": "You are a helpful assistant. Generate a short, concise title (max 10 words) for the following text. Do not use quotes. Only output the title.",
|
||||
},
|
||||
{"role": "user", "content": content[:2000]}, # Limit content length
|
||||
],
|
||||
"stream": False,
|
||||
}
|
||||
|
||||
response = await generate_chat_completion(request, payload, user_obj)
|
||||
if response and "choices" in response:
|
||||
return response["choices"][0]["message"]["content"].strip()
|
||||
except Exception as e:
|
||||
logger.error(f"Error generating title: {e}")
|
||||
|
||||
return ""
|
||||
|
||||
def extract_title(self, content: str) -> str:
|
||||
"""从 Markdown 内容提取一级/二级标题"""
|
||||
lines = content.split("\n")
|
||||
|
||||
Reference in New Issue
Block a user