fix:7.22问答调整
This commit is contained in:
parent
69a0992e69
commit
1b788f1a5c
|
|
@ -24,7 +24,7 @@ from app.schemas.chat import (
|
||||||
MessageResponse,
|
MessageResponse,
|
||||||
)
|
)
|
||||||
from app.services.audit_service import write_audit
|
from app.services.audit_service import write_audit
|
||||||
from app.services.document_service import resolve_chat_doc_scope
|
from app.services.document_service import build_unit_chain, resolve_chat_doc_scope
|
||||||
from app.services.user_service import JOB_TITLE_GROUP_ALIASES
|
from app.services.user_service import JOB_TITLE_GROUP_ALIASES
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -178,7 +178,16 @@ def stream_chat(
|
||||||
is_admin = current_user.is_superuser or any(
|
is_admin = current_user.is_superuser or any(
|
||||||
role.code in {"admin", "system_admin"} for role in current_user.roles
|
role.code in {"admin", "system_admin"} for role in current_user.roles
|
||||||
)
|
)
|
||||||
allowed_tags = [] if current_user.is_superuser else [role.code for role in current_user.roles]
|
# ADP 检索层过滤:角色码 + 提问人 unit_chain(整条向上链,支持就近优先/上级回退) + 公开范围标签.
|
||||||
|
# 只传角色码时,其他单位的专属文档也会被 ADP 一并召回喂给 LLM,LLM 会在正文写"依据文档:《XX 单位规定》";
|
||||||
|
# 把整条链一起下发,是为了让本单位无对应制度时,上级单位的制度也在候选内,由 LLM 按"就近优先"选用.
|
||||||
|
# 兄弟单位(不在向上链条上的)始终不进 allowed_tags,不会被召回.超管不过滤.
|
||||||
|
if current_user.is_superuser:
|
||||||
|
allowed_tags: list[str] = []
|
||||||
|
else:
|
||||||
|
allowed_tags = [role.code for role in current_user.roles]
|
||||||
|
allowed_tags.extend(build_unit_chain(current_user))
|
||||||
|
allowed_tags.extend(["所有人可见", "通用", "全体员工"])
|
||||||
|
|
||||||
# role_hint 必须是业务岗位(如"总院中层副职""员工"),用来匹配制度里按职级分组的条款。
|
# role_hint 必须是业务岗位(如"总院中层副职""员工"),用来匹配制度里按职级分组的条款。
|
||||||
# 只取 User.job_title;权限角色(employee/admin/system_admin)和 is_superuser 不参与,
|
# 只取 User.job_title;权限角色(employee/admin/system_admin)和 is_superuser 不参与,
|
||||||
|
|
@ -286,6 +295,21 @@ def stream_chat(
|
||||||
f"【提问人所属单位:{own_unit}】"
|
f"【提问人所属单位:{own_unit}】"
|
||||||
f"当前单位链上均无对应制度,仅可引用公开制度作答,若信息不足请明确说明。"
|
f"当前单位链上均无对应制度,仅可引用公开制度作答,若信息不足请明确说明。"
|
||||||
)
|
)
|
||||||
|
# 硬约束:兜底 ADP 检索层过滤失效 -> 强制 LLM 拒用 unit_chain 之外单位的专属文档.
|
||||||
|
# 允许 unit_chain 内的上级单位文档(就近优先由上面 fell_back/chosen_unit 分支引导).
|
||||||
|
# 优先级最高,凌驾于 FAQ 命中等所有规则.命中 FAQ 但 FAQ 内容显然只覆盖链外单位时,也必须拒答.
|
||||||
|
hint_lines.append(
|
||||||
|
f"【单位隔离硬约束·最高优先级】提问人所属单位:{own_unit}。"
|
||||||
|
f"允许引用的文档必须满足以下之一:"
|
||||||
|
f"(a) 标签值等于{own_unit};"
|
||||||
|
f"(b) 标签值为{own_unit}的上级单位(逐级向上);"
|
||||||
|
f"(c) 标签值为'通用/全体员工/所有人可见'等公开范围。"
|
||||||
|
f"不满足则:"
|
||||||
|
f"(1) 禁止将其内容用于作答,禁止在'依据文档'中列出;"
|
||||||
|
f"(2) 若召回文档全部不满足(即本单位及其上级链上均无对应制度),必须回复原文:"
|
||||||
|
f"'知识库中未找到 {own_unit} 及其上级单位对应的规定,建议咨询本单位财务部门。';"
|
||||||
|
f"(3) 此规则覆盖'FAQ 命中即返回标准答案'的短路,即使命中 FAQ 也须先判定单位适用性。"
|
||||||
|
)
|
||||||
if hint_lines:
|
if hint_lines:
|
||||||
adp_question = (
|
adp_question = (
|
||||||
f"{raw_question}\n\n"
|
f"{raw_question}\n\n"
|
||||||
|
|
|
||||||
|
|
@ -835,6 +835,33 @@ button {
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 共享文档 (ref.source_url 存在) 时, 标题变成蓝色外链, 悬停加下划线 */
|
||||||
|
.bubble-reference-item summary .ref-title-link {
|
||||||
|
color: #1a73e8;
|
||||||
|
text-decoration: none;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.bubble-reference-item summary .ref-title-link:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 展开区域内的链接地址行, 显示完整 URL, 蓝色字, 可换行 */
|
||||||
|
.bubble-reference-item .ref-url-line {
|
||||||
|
display: block;
|
||||||
|
margin: 6px 0;
|
||||||
|
padding: 4px 8px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #1a73e8;
|
||||||
|
background: rgba(26, 115, 232, 0.06);
|
||||||
|
border-radius: 4px;
|
||||||
|
word-break: break-all;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
.bubble-reference-item .ref-url-line:hover {
|
||||||
|
background: rgba(26, 115, 232, 0.16);
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
.bubble-reference-item summary .ref-page {
|
.bubble-reference-item summary .ref-page {
|
||||||
color: var(--muted);
|
color: var(--muted);
|
||||||
margin-left: 2px;
|
margin-left: 2px;
|
||||||
|
|
|
||||||
|
|
@ -308,10 +308,26 @@ watch(messages, scrollToEnd, { deep: true })
|
||||||
>
|
>
|
||||||
<summary>
|
<summary>
|
||||||
<span class="ref-icon">📄</span>
|
<span class="ref-icon">📄</span>
|
||||||
<span class="ref-title">{{ rIdx + 1 }}.{{ ref.source_title || '未知来源' }}</span>
|
<a
|
||||||
|
v-if="ref.source_url"
|
||||||
|
:href="ref.source_url"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
class="ref-title ref-title-link"
|
||||||
|
@click.stop
|
||||||
|
>{{ rIdx + 1 }}.{{ ref.source_title || '未知来源' }}</a>
|
||||||
|
<span v-else class="ref-title">{{ rIdx + 1 }}.{{ ref.source_title || '未知来源' }}</span>
|
||||||
<small v-if="ref.page_no" class="ref-page">第 {{ ref.page_no }} 页</small>
|
<small v-if="ref.page_no" class="ref-page">第 {{ ref.page_no }} 页</small>
|
||||||
<span class="ref-arrow">›</span>
|
<span class="ref-arrow">›</span>
|
||||||
</summary>
|
</summary>
|
||||||
|
<a
|
||||||
|
v-if="ref.source_url"
|
||||||
|
:href="ref.source_url"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
class="ref-url-line"
|
||||||
|
@click.stop
|
||||||
|
>🔗 {{ ref.source_url }}</a>
|
||||||
<div v-if="ref.snippet" class="ref-snippet">{{ ref.snippet.trim() }}</div>
|
<div v-if="ref.snippet" class="ref-snippet">{{ ref.snippet.trim() }}</div>
|
||||||
</details>
|
</details>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
const { request, uploadFile, streamRequest } = require('../../utils/request')
|
const { request, uploadFile, streamRequest } = require('../../utils/request')
|
||||||
|
const { splitMarkdownImages } = require('../../utils/parse-image')
|
||||||
|
|
||||||
const app = getApp()
|
const app = getApp()
|
||||||
|
|
||||||
|
|
@ -88,7 +89,11 @@ Page({
|
||||||
const list = this.data.messages.slice()
|
const list = this.data.messages.slice()
|
||||||
const i = list.findIndex((m) => m.key === key)
|
const i = list.findIndex((m) => m.key === key)
|
||||||
if (i === -1) return
|
if (i === -1) return
|
||||||
list[i] = Object.assign({}, list[i], patch)
|
const merged = Object.assign({}, list[i], patch)
|
||||||
|
if (patch && typeof patch.text === 'string') {
|
||||||
|
merged.contentSegments = splitMarkdownImages(merged.text)
|
||||||
|
}
|
||||||
|
list[i] = merged
|
||||||
this.setData({ messages: list, scrollAnchor: 'anchor-end' })
|
this.setData({ messages: list, scrollAnchor: 'anchor-end' })
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
@ -96,7 +101,11 @@ Page({
|
||||||
const list = this.data.messages.slice()
|
const list = this.data.messages.slice()
|
||||||
const i = list.findIndex((m) => m.key === key)
|
const i = list.findIndex((m) => m.key === key)
|
||||||
if (i === -1) return
|
if (i === -1) return
|
||||||
list[i] = Object.assign({}, list[i], { text: (list[i].text || '') + delta })
|
const newText = (list[i].text || '') + delta
|
||||||
|
list[i] = Object.assign({}, list[i], {
|
||||||
|
text: newText,
|
||||||
|
contentSegments: splitMarkdownImages(newText)
|
||||||
|
})
|
||||||
this.setData({ messages: list, scrollAnchor: 'anchor-end' })
|
this.setData({ messages: list, scrollAnchor: 'anchor-end' })
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
@ -188,10 +197,12 @@ Page({
|
||||||
}
|
}
|
||||||
if (m.answer || (m.references && m.references.length)) {
|
if (m.answer || (m.references && m.references.length)) {
|
||||||
this._msgSeq += 1
|
this._msgSeq += 1
|
||||||
|
const ansText = m.answer || ''
|
||||||
msgs.push({
|
msgs.push({
|
||||||
key: 'k' + this._msgSeq,
|
key: 'k' + this._msgSeq,
|
||||||
role: 'assistant',
|
role: 'assistant',
|
||||||
text: m.answer || '',
|
text: ansText,
|
||||||
|
contentSegments: splitMarkdownImages(ansText),
|
||||||
references: m.references || [],
|
references: m.references || [],
|
||||||
streaming: false
|
streaming: false
|
||||||
})
|
})
|
||||||
|
|
@ -256,6 +267,115 @@ Page({
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// 参考文档带 source_url 时(共享文档)点击复制链接到剪贴板。
|
||||||
|
// 小程序无法直接打开任意外链, 复制后由用户粘贴到浏览器。
|
||||||
|
onRefLinkTap(e) {
|
||||||
|
const url = e.currentTarget.dataset.url
|
||||||
|
if (!url) return
|
||||||
|
wx.setClipboardData({
|
||||||
|
data: url,
|
||||||
|
success: () => wx.showToast({ title: '链接已复制', icon: 'success' }),
|
||||||
|
fail: () => wx.showToast({ title: '复制失败', icon: 'none' })
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
// 长按聊天记录里的图片 (自己发的 or AI 回答里的 markdown 图片) -> 弹菜单
|
||||||
|
onImageLongPress(e) {
|
||||||
|
const src = e.currentTarget.dataset.src
|
||||||
|
if (!src) return
|
||||||
|
wx.showActionSheet({
|
||||||
|
itemList: ['预览大图', '引用此图追问', '保存到相册'],
|
||||||
|
success: (r) => {
|
||||||
|
if (r.tapIndex === 0) wx.previewImage({ current: src, urls: [src] })
|
||||||
|
else if (r.tapIndex === 1) this._quoteImage(src)
|
||||||
|
else if (r.tapIndex === 2) this._saveImageToAlbum(src)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
async _quoteImage(src) {
|
||||||
|
if (this.data.streaming) {
|
||||||
|
wx.showToast({ title: '当前对话生成中', icon: 'none' })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (this.data.pendingImages.length >= 4) {
|
||||||
|
wx.showToast({ title: '最多 4 张图', icon: 'none' })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const localPath = await this._materializeToLocal(src)
|
||||||
|
if (!localPath) return
|
||||||
|
await this._uploadTempImage({ tempFilePath: localPath })
|
||||||
|
},
|
||||||
|
|
||||||
|
async _saveImageToAlbum(src) {
|
||||||
|
const path = await this._materializeToLocal(src)
|
||||||
|
if (!path) return
|
||||||
|
wx.saveImageToPhotosAlbum({
|
||||||
|
filePath: path,
|
||||||
|
success: () => wx.showToast({ title: '已保存到相册' }),
|
||||||
|
fail: (err) => wx.showToast({ title: '保存失败:' + (err.errMsg || ''), icon: 'none' })
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
// 把任意来源 (远端 URL / data URL / 本地临时路径) 归一化成本地文件路径,
|
||||||
|
// 后续能直接喂给 wx.uploadFile / wx.saveImageToPhotosAlbum。失败时返回 null 并已给用户提示。
|
||||||
|
_materializeToLocal(src) {
|
||||||
|
if (!src) return Promise.resolve(null)
|
||||||
|
if (src.indexOf('data:') === 0) return this._dataUrlToTempFile(src)
|
||||||
|
if (/^https?:\/\//.test(src)) return this._downloadRemote(src)
|
||||||
|
return Promise.resolve(src)
|
||||||
|
},
|
||||||
|
|
||||||
|
_dataUrlToTempFile(dataUrl) {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
const m = /^data:([^;]+);base64,(.+)$/.exec(dataUrl)
|
||||||
|
if (!m) {
|
||||||
|
wx.showToast({ title: '图片格式无法识别', icon: 'none' })
|
||||||
|
resolve(null)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const ext = (m[1].split('/')[1] || 'png').replace(/[^a-z0-9]/gi, '') || 'png'
|
||||||
|
const path = `${wx.env.USER_DATA_PATH}/quote_${Date.now()}.${ext}`
|
||||||
|
wx.getFileSystemManager().writeFile({
|
||||||
|
filePath: path,
|
||||||
|
data: m[2],
|
||||||
|
encoding: 'base64',
|
||||||
|
success: () => resolve(path),
|
||||||
|
fail: (err) => {
|
||||||
|
wx.showToast({ title: '写临时文件失败: ' + (err.errMsg || ''), icon: 'none' })
|
||||||
|
resolve(null)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
_downloadRemote(url) {
|
||||||
|
wx.showLoading({ title: '拉取图片...', mask: true })
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
wx.downloadFile({
|
||||||
|
url,
|
||||||
|
success: (r) => {
|
||||||
|
wx.hideLoading()
|
||||||
|
if (r.statusCode === 200) {
|
||||||
|
resolve(r.tempFilePath)
|
||||||
|
} else {
|
||||||
|
wx.showToast({ title: '下载失败 HTTP ' + r.statusCode, icon: 'none' })
|
||||||
|
resolve(null)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
fail: (err) => {
|
||||||
|
wx.hideLoading()
|
||||||
|
wx.showModal({
|
||||||
|
title: '拉图失败',
|
||||||
|
content: (err.errMsg || '') + '\n\n若提示"域名不在合法列表",请在微信公众平台 -> 开发 -> 服务器域名 里把该图片来源域名加进 downloadFile 合法域名。',
|
||||||
|
showCancel: false
|
||||||
|
})
|
||||||
|
resolve(null)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
// ---------------- 语音 ----------------
|
// ---------------- 语音 ----------------
|
||||||
toggleVoiceMode() {
|
toggleVoiceMode() {
|
||||||
this.setData({ voiceMode: !this.data.voiceMode })
|
this.setData({ voiceMode: !this.data.voiceMode })
|
||||||
|
|
|
||||||
|
|
@ -43,20 +43,47 @@
|
||||||
src="{{att.data_url}}"
|
src="{{att.data_url}}"
|
||||||
mode="aspectFill"
|
mode="aspectFill"
|
||||||
bindtap="previewImage"
|
bindtap="previewImage"
|
||||||
|
bindlongpress="onImageLongPress"
|
||||||
data-src="{{att.data_url}}"
|
data-src="{{att.data_url}}"
|
||||||
/>
|
/>
|
||||||
</view>
|
</view>
|
||||||
</block>
|
</block>
|
||||||
<text wx:if="{{item.text}}" class="txt">{{item.text}}</text>
|
<text wx:if="{{item.text}}" class="txt" user-select="{{true}}">{{item.text}}</text>
|
||||||
</block>
|
</block>
|
||||||
<block wx:else>
|
<block wx:else>
|
||||||
<text class="txt">{{item.text || (item.streaming ? '思考中...' : '')}}</text>
|
<block wx:if="{{!item.text && item.streaming}}">
|
||||||
|
<text class="txt">思考中...</text>
|
||||||
|
</block>
|
||||||
|
<block wx:else>
|
||||||
|
<block wx:for="{{item.contentSegments}}" wx:key="idx" wx:for-item="seg" wx:for-index="idx">
|
||||||
|
<text wx:if="{{seg.type === 'text'}}" class="txt" user-select="{{true}}">{{seg.text}}</text>
|
||||||
|
<image
|
||||||
|
wx:elif="{{seg.type === 'img'}}"
|
||||||
|
class="ai-img"
|
||||||
|
src="{{seg.src}}"
|
||||||
|
mode="widthFix"
|
||||||
|
bindtap="previewImage"
|
||||||
|
bindlongpress="onImageLongPress"
|
||||||
|
data-src="{{seg.src}}"
|
||||||
|
/>
|
||||||
|
</block>
|
||||||
|
</block>
|
||||||
<block wx:if="{{item.references && item.references.length}}">
|
<block wx:if="{{item.references && item.references.length}}">
|
||||||
<view class="refs">
|
<view class="refs">
|
||||||
<view class="refs-title">参考来源</view>
|
<view class="refs-title">参考来源</view>
|
||||||
<view class="ref-item" wx:for="{{item.references}}" wx:for-item="ref" wx:key="idx" wx:for-index="idx">
|
<view class="ref-item" wx:for="{{item.references}}" wx:for-item="ref" wx:key="idx" wx:for-index="idx">
|
||||||
<text class="ref-title">[{{idx + 1}}] {{ref.source_title}}</text>
|
<text class="ref-title" user-select="{{true}}">[{{idx + 1}}] {{ref.source_title}}</text>
|
||||||
<text wx:if="{{ref.snippet}}" class="ref-snippet">{{ref.snippet}}</text>
|
<view
|
||||||
|
wx:if="{{ref.source_url}}"
|
||||||
|
class="ref-link"
|
||||||
|
hover-class="ref-link-hover"
|
||||||
|
bindtap="onRefLinkTap"
|
||||||
|
data-url="{{ref.source_url}}"
|
||||||
|
>
|
||||||
|
<text class="ref-link-icon">🔗</text>
|
||||||
|
<text class="ref-link-url" user-select="{{true}}">{{ref.source_url}}</text>
|
||||||
|
</view>
|
||||||
|
<text wx:if="{{ref.snippet}}" class="ref-snippet" user-select="{{true}}">{{ref.snippet}}</text>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</block>
|
</block>
|
||||||
|
|
|
||||||
|
|
@ -186,6 +186,16 @@
|
||||||
background: rgba(0, 0, 0, 0.2);
|
background: rgba(0, 0, 0, 0.2);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* AI 回答里嵌入的 markdown 图片, 宽度撑满气泡, 高度按图片原比例 */
|
||||||
|
.ai-img {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 520rpx;
|
||||||
|
margin: 12rpx 0;
|
||||||
|
border-radius: 12rpx;
|
||||||
|
background: rgba(0, 0, 0, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
.refs {
|
.refs {
|
||||||
margin-top: 20rpx;
|
margin-top: 20rpx;
|
||||||
padding-top: 16rpx;
|
padding-top: 16rpx;
|
||||||
|
|
@ -206,6 +216,30 @@
|
||||||
color: var(--brand);
|
color: var(--brand);
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
}
|
}
|
||||||
|
.ref-link {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 6rpx;
|
||||||
|
align-self: stretch;
|
||||||
|
font-size: 22rpx;
|
||||||
|
margin-top: 4rpx;
|
||||||
|
padding: 4rpx 10rpx;
|
||||||
|
border-radius: 6rpx;
|
||||||
|
background: rgba(26, 115, 232, 0.06);
|
||||||
|
}
|
||||||
|
.ref-link-icon {
|
||||||
|
color: #1a73e8;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
.ref-link-url {
|
||||||
|
color: #1a73e8;
|
||||||
|
word-break: break-all;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
.ref-link-hover {
|
||||||
|
background: rgba(26, 115, 232, 0.16);
|
||||||
|
}
|
||||||
.ref-snippet {
|
.ref-snippet {
|
||||||
font-size: 22rpx;
|
font-size: 22rpx;
|
||||||
color: #666;
|
color: #666;
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,27 @@
|
||||||
|
// 把 AI 回答里的 markdown 图片语法  从纯文本里切出来,
|
||||||
|
// 让 chat.wxml 能把图片用真正的 <image> 组件渲染 (可长按引用),
|
||||||
|
// 其余文本仍走 <text> 保持现状。
|
||||||
|
// 只识别标准 markdown image, 不解析加粗/列表等其他语法。
|
||||||
|
const IMG_RE = /!\[[^\]]*\]\(\s*([^)\s]+)(?:\s+"[^"]*")?\s*\)/g
|
||||||
|
|
||||||
|
function splitMarkdownImages(text) {
|
||||||
|
const src = String(text || '')
|
||||||
|
if (!src) return []
|
||||||
|
const out = []
|
||||||
|
let cursor = 0
|
||||||
|
IMG_RE.lastIndex = 0
|
||||||
|
let m
|
||||||
|
while ((m = IMG_RE.exec(src)) !== null) {
|
||||||
|
if (m.index > cursor) {
|
||||||
|
out.push({ type: 'text', text: src.slice(cursor, m.index) })
|
||||||
|
}
|
||||||
|
out.push({ type: 'img', src: m[1] })
|
||||||
|
cursor = IMG_RE.lastIndex
|
||||||
|
}
|
||||||
|
if (cursor < src.length) {
|
||||||
|
out.push({ type: 'text', text: src.slice(cursor) })
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { splitMarkdownImages }
|
||||||
Loading…
Reference in New Issue