feat: 完成20260315用户管理3全部5个功能
1. 链接人和事:补充CKB_OPEN_API_KEY/ACCOUNT配置,新增fix-ckb批量创建获客计划API 2. 规则配置:打通DB规则与ruleEngine,新增/api/miniprogram/user-rules接口, ruleEngine改为从API动态加载规则并按enabled状态执行 3. 获客计划:修复获客数统计中personId/token不匹配导致永远为0的bug, 管理端新增"修复CKB密钥"按钮 4. 支付问题:修复钱包充值和代付分享中openId缺失导致400错误, 添加getOpenId()兜底逻辑 5. 朋友圈分享:shareToMoments改为复制文章前200字+省略号+手指箭头emoji Made-with: Cursor
This commit is contained in:
@@ -100,8 +100,10 @@ function parseBlockToSegments(block) {
|
||||
|
||||
/**
|
||||
* 从 HTML 中解析出 lines(纯文本行)和 segments(含富文本片段)
|
||||
* @param {string} html
|
||||
* @param {object} [config] - { persons: [], linkTags: [] },用于对 text 段自动匹配 @人名 / #标签
|
||||
*/
|
||||
function parseHtmlToSegments(html) {
|
||||
function parseHtmlToSegments(html, config) {
|
||||
const lines = []
|
||||
const segments = []
|
||||
|
||||
@@ -125,7 +127,7 @@ function parseHtmlToSegments(html) {
|
||||
for (const block of blocks) {
|
||||
if (!block.trim()) continue
|
||||
|
||||
const blockSegs = parseBlockToSegments(block)
|
||||
let blockSegs = parseBlockToSegments(block)
|
||||
if (!blockSegs.length) continue
|
||||
|
||||
// 纯图片行独立成段
|
||||
@@ -135,6 +137,20 @@ function parseHtmlToSegments(html) {
|
||||
continue
|
||||
}
|
||||
|
||||
// 对 text 段再跑一遍 @人名 / #标签 自动匹配(处理未用 TipTap 插入而是手打的 @xxx)
|
||||
if (config && (config.persons?.length || config.linkTags?.length)) {
|
||||
const expanded = []
|
||||
for (const seg of blockSegs) {
|
||||
if (seg.type === 'text' && seg.text) {
|
||||
const sub = matchLineToSegments(seg.text, config)
|
||||
expanded.push(...sub)
|
||||
} else {
|
||||
expanded.push(seg)
|
||||
}
|
||||
}
|
||||
blockSegs = expanded
|
||||
}
|
||||
|
||||
// 行纯文本用于 lines(previewParagraphs 降级展示)
|
||||
const lineText = decodeEntities(block.replace(/<[^>]+>/g, '')).trim()
|
||||
lines.push(lineText)
|
||||
@@ -163,27 +179,108 @@ function stripMarkdownFormatting(text) {
|
||||
return s
|
||||
}
|
||||
|
||||
/**
|
||||
* 对一行纯文本进行 @人名 / #标签 自动匹配,返回 segments 数组
|
||||
* config: { persons: [{personId, name, aliases}], linkTags: [{tagId, label, type, pagePath, mpKey, url, aliases}] }
|
||||
*/
|
||||
function matchLineToSegments(line, config) {
|
||||
if (!config || (!config.persons?.length && !config.linkTags?.length)) {
|
||||
return [{ type: 'text', text: line }]
|
||||
}
|
||||
const normalize = s => (s || '').trim().toLowerCase()
|
||||
const personMap = {}
|
||||
const tagMap = {}
|
||||
for (const p of (config.persons || [])) {
|
||||
const keys = [p.name, ...(p.aliases ? p.aliases.split(',') : [])].map(normalize).filter(Boolean)
|
||||
for (const k of keys) { if (!personMap[k]) personMap[k] = p }
|
||||
}
|
||||
for (const t of (config.linkTags || [])) {
|
||||
const keys = [t.label, ...(t.aliases ? t.aliases.split(',') : [])].map(normalize).filter(Boolean)
|
||||
for (const k of keys) { if (!tagMap[k]) tagMap[k] = t }
|
||||
}
|
||||
const esc = n => n.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
||||
const personNames = Object.keys(personMap).sort((a, b) => b.length - a.length).map(esc)
|
||||
const tagLabels = Object.keys(tagMap).sort((a, b) => b.length - a.length).map(esc)
|
||||
if (!personNames.length && !tagLabels.length) return [{ type: 'text', text: line }]
|
||||
|
||||
const parts = []
|
||||
if (personNames.length) parts.push('[@@](' + personNames.join('|') + ')')
|
||||
if (tagLabels.length) parts.push('[##](' + tagLabels.join('|') + ')')
|
||||
const pattern = new RegExp(parts.join('|'), 'gi')
|
||||
|
||||
const segs = []
|
||||
let lastEnd = 0
|
||||
let m
|
||||
while ((m = pattern.exec(line)) !== null) {
|
||||
if (m.index > lastEnd) {
|
||||
segs.push({ type: 'text', text: line.slice(lastEnd, m.index) })
|
||||
}
|
||||
const full = m[0]
|
||||
const prefix = full[0]
|
||||
const body = full.slice(1)
|
||||
if (prefix === '@' || prefix === '@') {
|
||||
const person = personMap[normalize(body)]
|
||||
if (person) {
|
||||
segs.push({ type: 'mention', userId: person.personId || '', nickname: person.name || body })
|
||||
} else {
|
||||
segs.push({ type: 'text', text: full })
|
||||
}
|
||||
} else {
|
||||
const tag = tagMap[normalize(body)]
|
||||
if (tag) {
|
||||
segs.push({
|
||||
type: 'linkTag',
|
||||
label: tag.label || body,
|
||||
url: tag.url || '',
|
||||
tagType: tag.type || 'url',
|
||||
pagePath: tag.pagePath || '',
|
||||
tagId: tag.tagId || '',
|
||||
appId: tag.appId || '',
|
||||
mpKey: tag.mpKey || ''
|
||||
})
|
||||
} else {
|
||||
segs.push({ type: 'text', text: full })
|
||||
}
|
||||
}
|
||||
lastEnd = m.index + full.length
|
||||
}
|
||||
if (lastEnd < line.length) {
|
||||
segs.push({ type: 'text', text: line.slice(lastEnd) })
|
||||
}
|
||||
return segs.length ? segs : [{ type: 'text', text: line }]
|
||||
}
|
||||
|
||||
/** 纯文本/Markdown 按行解析 */
|
||||
function parsePlainTextToSegments(text) {
|
||||
function parsePlainTextToSegments(text, config) {
|
||||
const cleaned = stripMarkdownFormatting(text)
|
||||
const lines = cleaned.split('\n').map(l => l.trim()).filter(l => l.length > 0)
|
||||
const segments = lines.map(line => [{ type: 'text', text: line }])
|
||||
const segments = lines.map(line => matchLineToSegments(line, config))
|
||||
return { lines, segments }
|
||||
}
|
||||
|
||||
/** 清理残留的 Markdown 图片引用文本(如 "image.png" ) */
|
||||
function stripOrphanImageRefs(text) {
|
||||
if (!text) return text
|
||||
text = text.replace(/[^\s]*\.(?:png|jpg|jpeg|gif|webp|svg|bmp)!\[[^\]]*\]\([^)]*\)/gi, '')
|
||||
text = text.replace(/!\[[^\]]*\]\([^)]*\)/g, '')
|
||||
return text
|
||||
}
|
||||
|
||||
/**
|
||||
* 将原始内容解析为 contentSegments(用于阅读页展示)
|
||||
* @param {string} rawContent
|
||||
* @param {object} [config] - { persons: [], linkTags: [] }
|
||||
* @returns {{ lines: string[], segments: Array<Array<segment>> }}
|
||||
*/
|
||||
function parseContent(rawContent) {
|
||||
function parseContent(rawContent, config) {
|
||||
if (!rawContent || typeof rawContent !== 'string') {
|
||||
return { lines: [], segments: [] }
|
||||
}
|
||||
if (isHtmlContent(rawContent)) {
|
||||
return parseHtmlToSegments(rawContent)
|
||||
let content = stripOrphanImageRefs(rawContent)
|
||||
if (isHtmlContent(content)) {
|
||||
return parseHtmlToSegments(content, config)
|
||||
}
|
||||
return parsePlainTextToSegments(rawContent)
|
||||
return parsePlainTextToSegments(content, config)
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
|
||||
Reference in New Issue
Block a user