forked from b2894lxlx/tuanwei-admin
fix(ai): 写作入口采集上游主控字段,用户诉求不再被默认值覆盖(issue 13)
聊天页「写作」模式没有表单,只按「键:值」解析输入,普通自然语言解析为空后一律回落 doc_type=新闻稿 / Number=800。上游「AI内容创作与宣传助手」的 prompt 把这两项当主控指令, 实测模型会主动忽略用户原话里的冲突要求,思维链原文: I will ignore the conflicting "300字推文" instruction inside the bullet points because the main task says "文稿类型为:新闻稿" and "篇幅要求(字数限制):800" 这就是用户报的「写作模块听不懂要求」——不是模型听不懂,是把「新闻稿/800字」当硬指令发给了它。 - 新增 src/views/oa/chatgpt/writing-params.ts:从自然语言抽取文体/字数/主题/时间/地点/ 人员/活动类型,保留既有「键:值」入参,界面显式选择优先于自动识别;纯函数零依赖, 可直接用 node 运行测试 - 文体识别对齐上游真实存在的 4 个模板(新闻稿/活动总结/微信推文/通知公告);认不出的文体 (发言稿、短视频脚本等)原样下发交给模型发挥,强行映射成新闻稿会更糟 - 用户没提字数时按文体给常规篇幅,而不是不下发——上游对空字数的处理会在正文里留一行 「(未设定有效字数限制)」 - 写作模式输入框上方新增「文体 / 字数」两组按钮,复用既有 ratio-panel 样式 验证:解析用例 8/8(.scratch/ai-assistant/probe/test-writing-params.ts);同一句输入做端到端 对照,修复前得 800 字新闻稿,修复后得 281 字微信推文(引用块 + ## 模块小标题 + 【互动时间】)。
This commit is contained in:
@@ -213,6 +213,32 @@
|
||||
<span>{{ assistantTitle }}</span>
|
||||
</div>
|
||||
<div class="input-shell">
|
||||
<div v-if="activeChatMode === 'writing'" class="ratio-panel">
|
||||
<span class="ratio-label">文体</span>
|
||||
<button
|
||||
v-for="item in WRITING_DOC_TYPE_OPTIONS"
|
||||
:key="item.value"
|
||||
class="ratio-option"
|
||||
type="button"
|
||||
:class="{ 'is-active': writingDocType === item.value }"
|
||||
:disabled="loading"
|
||||
@click="writingDocType = item.value"
|
||||
>
|
||||
{{ item.label }}
|
||||
</button>
|
||||
<span class="ratio-label">字数</span>
|
||||
<button
|
||||
v-for="item in WRITING_WORD_COUNT_OPTIONS"
|
||||
:key="item.value"
|
||||
class="ratio-option"
|
||||
type="button"
|
||||
:class="{ 'is-active': writingWordCount === item.value }"
|
||||
:disabled="loading"
|
||||
@click="writingWordCount = item.value"
|
||||
>
|
||||
{{ item.label }}
|
||||
</button>
|
||||
</div>
|
||||
<div v-if="activeChatMode === 'image'" class="ratio-panel">
|
||||
<span class="ratio-label">图片比例</span>
|
||||
<button
|
||||
@@ -316,6 +342,12 @@ import { listAiChatList, removeAiChatList } from "@/api/ai/aiChatList";
|
||||
import type { AiChatList } from "@/api/ai/aiChatList/model";
|
||||
import { sendMessage, stopMessage } from "@/api/oa/chatgpt";
|
||||
import { manuscriptGen } from "@/api/ai/manuscriptGen";
|
||||
import {
|
||||
parseWritingRequest,
|
||||
WRITING_DOC_TYPE_OPTIONS,
|
||||
WRITING_WORD_COUNT_OPTIONS
|
||||
} from "./writing-params";
|
||||
import type { WritingOverrides } from "./writing-params";
|
||||
import { creativeAssistant } from "@/api/ai/creativeAssistant";
|
||||
import { API_BASE_URL } from "@/config/setting";
|
||||
import type { Menu } from "@/api/system/menu/model";
|
||||
@@ -360,6 +392,9 @@ const stopping = ref(false);
|
||||
const content = ref("");
|
||||
const activeChatMode = ref<ChatMode>("chat");
|
||||
const imageRatio = ref("1:1");
|
||||
// 写作模式的显式选择:默认 auto,即由 writing-params 从用户输入里自动识别
|
||||
const writingDocType = ref("auto");
|
||||
const writingWordCount = ref("auto");
|
||||
const wsConnected = ref(false);
|
||||
const deletingChatId = ref<number>();
|
||||
const messages = ref<ChatMessageItem[]>([]);
|
||||
@@ -413,7 +448,7 @@ const imageRatioOptions = [
|
||||
|
||||
const inputPlaceholder = computed(() => {
|
||||
if (activeChatMode.value === "writing") {
|
||||
return "请输入写作需求,例如新闻稿、推文文案、小论文、发言稿,也可以补充标题、受众、字数和风格";
|
||||
return "直接描述需求即可,例如:帮我写一篇300字关于迎新晚会的推文,风格活泼,面向大一新生(也可在下方直接指定文体和字数)";
|
||||
}
|
||||
if (activeChatMode.value === "image") {
|
||||
return "请输入图片生成需求,例如校园活动海报、公众号头图、志愿服务插画,并选择图片比例";
|
||||
@@ -960,32 +995,19 @@ const pickString = (...values: unknown[]) => {
|
||||
return "";
|
||||
};
|
||||
|
||||
const parseKeyValueInput = (value: string) => {
|
||||
const result: Record<string, string> = {};
|
||||
value
|
||||
.split(/\n+/)
|
||||
.map((item) => item.trim())
|
||||
.filter(Boolean)
|
||||
.forEach((line) => {
|
||||
const match = line.match(/^([^::]+)[::]\s*(.*)$/);
|
||||
if (match) {
|
||||
result[match[1].trim()] = match[2].trim();
|
||||
}
|
||||
});
|
||||
return result;
|
||||
};
|
||||
|
||||
const buildWritingPayload = (query: string) => {
|
||||
const fields = parseKeyValueInput(query);
|
||||
// 界面上显式选了文体/字数就用显式值,否则由 writing-params 从自然语言里识别。
|
||||
// 过去这里没有表单,一律回落 doc_type=新闻稿 / Number=800,而上游提示词把这两项
|
||||
// 当作主控指令,会直接压掉用户原话里的诉求(实测见 .scratch/ai-assistant/probe/)。
|
||||
const overrides: WritingOverrides = {};
|
||||
if (writingDocType.value !== "auto") {
|
||||
overrides.docType = writingDocType.value;
|
||||
}
|
||||
if (writingWordCount.value !== "auto") {
|
||||
overrides.number = Number(writingWordCount.value);
|
||||
}
|
||||
return {
|
||||
docType: fields["稿件类型"] || fields["文体"] || fields["类型"] || "新闻稿",
|
||||
eventType: fields["活动类型"] || fields["场景"] || "校园内容",
|
||||
theme: fields["稿件标题"] || fields["标题"] || fields["主题"] || query,
|
||||
time: fields["活动时间"] || fields["时间"] || "",
|
||||
location: fields["活动地点"] || fields["地点"] || "",
|
||||
participants: fields["参与人员"] || fields["受众"] || fields["人员"] || "",
|
||||
highlights: fields["亮点内容"] || fields["要点"] || fields["亮点"] || fields["内容"] || query,
|
||||
number: Number(fields["建议篇幅"] || fields["字数"] || 800),
|
||||
...parseWritingRequest(query, overrides),
|
||||
files: []
|
||||
};
|
||||
};
|
||||
|
||||
@@ -0,0 +1,365 @@
|
||||
/**
|
||||
* 「AI 写作助手」入参解析:把用户自然语言诉求抽取成上游 Dify 真正读取的字段。
|
||||
*
|
||||
* 背景(实测记录见 .scratch/ai-assistant/probe/):
|
||||
* 上游应用「AI内容创作与宣传助手」是按**结构化表单**设计的,它的 user prompt 模板是
|
||||
*
|
||||
* 本次需要生成的文稿类型为:{doc_type}
|
||||
* 篇幅要求(字数限制):{Number}
|
||||
* 以下是本次活动的核心要素,请严格据此生成稿件:
|
||||
* - 活动主题:{theme}
|
||||
* - 活动类型:{event_type}
|
||||
* - 举办时间:{time} / 举办地点:{location} / 参与人员:{participants}
|
||||
* - 核心亮点与具体要求:{highlights}
|
||||
*
|
||||
* 其中 doc_type 与 Number 是**主控指令**,模型会显式服从、甚至忽略用户原话里的冲突要求。
|
||||
* 实测思维链原文:
|
||||
* I will ignore the conflicting "300字推文" instruction inside the bullet points
|
||||
* because the main task says "文稿类型为:新闻稿" and "篇幅要求(字数限制):800"
|
||||
*
|
||||
* 而聊天页的「写作」入口过去没有表单,只按「键:值」解析,解析不到就一律回落
|
||||
* doc_type=新闻稿 / Number=800,用户自然语言里的真实诉求(推文、300 字)因此被覆盖,
|
||||
* 表现为「写作模块听不懂要求」。
|
||||
*
|
||||
* 本模块做三件事:
|
||||
* 1. 保留既有的「键:值」入参(向后兼容,老用法不受影响);
|
||||
* 2. 从自然语言里抽取文体、字数、主题、时间、地点、人员、活动类型;
|
||||
* 3. 允许用户在界面上显式指定文体 / 字数,显式选择优先于自动识别。
|
||||
*
|
||||
* 纯函数、零依赖,可直接用 node 运行测试。
|
||||
*/
|
||||
|
||||
export interface WritingOverrides {
|
||||
/** 用户显式选择的文体;不传或传 "auto" 表示自动识别 */
|
||||
docType?: string;
|
||||
/** 用户显式选择的字数;不传表示自动识别 */
|
||||
number?: number;
|
||||
}
|
||||
|
||||
export interface WritingRequest {
|
||||
docType: string;
|
||||
eventType: string;
|
||||
theme: string;
|
||||
time: string;
|
||||
location: string;
|
||||
participants: string;
|
||||
highlights: string;
|
||||
number?: number;
|
||||
}
|
||||
|
||||
/** 上游 Dify 提示词里真正定义了 Markdown 结构与文风的 4 种文体 */
|
||||
export const WRITING_DOC_TYPE_OPTIONS = [
|
||||
{ label: '自动识别', value: 'auto' },
|
||||
{ label: '新闻稿', value: '新闻稿' },
|
||||
{ label: '活动总结', value: '活动总结' },
|
||||
{ label: '微信推文', value: '微信推文' },
|
||||
{ label: '通知公告', value: '通知公告' }
|
||||
];
|
||||
|
||||
export const WRITING_WORD_COUNT_OPTIONS = [
|
||||
{ label: '自动识别', value: 'auto' },
|
||||
{ label: '300字', value: '300' },
|
||||
{ label: '500字', value: '500' },
|
||||
{ label: '800字', value: '800' },
|
||||
{ label: '1000字', value: '1000' },
|
||||
{ label: '1500字', value: '1500' }
|
||||
];
|
||||
|
||||
const DEFAULT_DOC_TYPE = '新闻稿';
|
||||
const DEFAULT_EVENT_TYPE = '校园内容';
|
||||
|
||||
/**
|
||||
* 用户没提字数时的兜底:按文体给一个常规篇幅。
|
||||
*
|
||||
* 之所以不干脆不下发 Number —— 上游提示词对空字数的处理是「按文体常规篇幅撰写,
|
||||
* 并在文末用一行注明(未设定有效字数限制)」,那行尾注会直接出现在用户看到的正文里。
|
||||
*/
|
||||
const DEFAULT_WORD_COUNT_BY_DOC_TYPE: Record<string, number> = {
|
||||
新闻稿: 800,
|
||||
活动总结: 1000,
|
||||
微信推文: 600,
|
||||
通知公告: 500
|
||||
};
|
||||
|
||||
const FIELD_ALIASES: Record<string, string[]> = {
|
||||
docType: ['稿件类型', '文稿类型', '文体', '类型'],
|
||||
eventType: ['活动类型', '场景'],
|
||||
theme: ['稿件标题', '标题', '主题', '活动主题'],
|
||||
time: ['活动时间', '举办时间', '时间'],
|
||||
location: ['活动地点', '举办地点', '地点'],
|
||||
participants: ['参与人员', '参加人员', '受众', '人员'],
|
||||
highlights: ['亮点内容', '要点', '亮点', '具体要求', '要求', '内容'],
|
||||
number: ['建议篇幅', '字数', '篇幅']
|
||||
};
|
||||
|
||||
/** 活动类型关键词(与 gxmu/manuscriptGen 表单页的选项保持一致的措辞) */
|
||||
const EVENT_TYPE_KEYWORDS = [
|
||||
'志愿服务',
|
||||
'社会实践',
|
||||
'文体活动',
|
||||
'会议论坛',
|
||||
'品牌项目',
|
||||
'校园活动',
|
||||
'团日活动',
|
||||
'迎新',
|
||||
'讲座',
|
||||
'培训',
|
||||
'比赛',
|
||||
'表彰',
|
||||
'招聘',
|
||||
'宣讲'
|
||||
];
|
||||
|
||||
/** 既有的「键:值」解析:按行取 `键:值`,普通自然语言(无冒号)解析为空对象 */
|
||||
export const parseKeyValueInput = (value: string) => {
|
||||
const result: Record<string, string> = {};
|
||||
value
|
||||
.split(/\n+/)
|
||||
.map((item) => item.trim())
|
||||
.filter(Boolean)
|
||||
.forEach((line) => {
|
||||
const match = line.match(/^([^::]+)[::]\s*(.*)$/);
|
||||
if (match) {
|
||||
result[match[1].trim()] = match[2].trim();
|
||||
}
|
||||
});
|
||||
return result;
|
||||
};
|
||||
|
||||
const pickField = (fields: Record<string, string>, aliases: string[]) => {
|
||||
for (const alias of aliases) {
|
||||
const value = fields[alias];
|
||||
if (value) {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
return '';
|
||||
};
|
||||
|
||||
/** 上游没有内置模板、但用户会说的其它文体;命中就原样交给模型发挥 */
|
||||
const EXTRA_DOC_TYPE_WORDS = [
|
||||
'发言稿',
|
||||
'演讲稿',
|
||||
'致辞',
|
||||
'主持词',
|
||||
'短视频脚本',
|
||||
'视频脚本',
|
||||
'脚本',
|
||||
'策划书',
|
||||
'活动方案',
|
||||
'简报',
|
||||
'心得体会'
|
||||
];
|
||||
|
||||
/**
|
||||
* 从一段文本里识别文体关键词,认不出来返回空串。
|
||||
*
|
||||
* 只认「明确的文体词」,不做任何兜底 —— 它会被喂进用户的**整句话**,
|
||||
* 若像 {@link normalizeDocType} 那样原样返回,整句话就会变成一个非法的文稿类型。
|
||||
*/
|
||||
export const inferDocType = (text: string) => {
|
||||
if (/推文|公众号|微信|朋友圈|新媒体/.test(text)) {
|
||||
return '微信推文';
|
||||
}
|
||||
if (/总结|复盘|综述/.test(text)) {
|
||||
return '活动总结';
|
||||
}
|
||||
if (/通知|公告|告示|通告|启事/.test(text)) {
|
||||
return '通知公告';
|
||||
}
|
||||
if (/新闻|通讯|通稿|报道|简讯|宣传稿|消息稿/.test(text)) {
|
||||
return '新闻稿';
|
||||
}
|
||||
for (const word of EXTRA_DOC_TYPE_WORDS) {
|
||||
if (text.includes(word)) {
|
||||
return word;
|
||||
}
|
||||
}
|
||||
return '';
|
||||
};
|
||||
|
||||
/**
|
||||
* 把用户给出的文体归一化到上游定义过的 4 种。
|
||||
*
|
||||
* 认不出来的类型(如「发言稿」「短视频脚本」)**原样返回**,交给模型自行发挥 ——
|
||||
* 强行映射成「新闻稿」会生成一篇完全不是发言稿的文章,比不映射更糟。
|
||||
* 注意:入参应当是「文体名」(键值或界面选择),不要传用户整句话。
|
||||
*/
|
||||
export const normalizeDocType = (raw: string) => {
|
||||
const value = (raw || '').trim();
|
||||
if (!value) {
|
||||
return '';
|
||||
}
|
||||
return inferDocType(value) || value;
|
||||
};
|
||||
|
||||
/**
|
||||
* 从文本里抽取目标字数。
|
||||
*
|
||||
* 覆盖「300字」「300字左右」「不少于500字」「800-1000字」等写法;
|
||||
* 区间取上界(用户给区间时通常不希望写少)。50~10000 之外视为无效。
|
||||
*/
|
||||
export const parseWordCount = (text: string): number | undefined => {
|
||||
const valid = (raw: string | undefined) => {
|
||||
if (!raw) {
|
||||
return undefined;
|
||||
}
|
||||
const value = Number(raw);
|
||||
return value >= 50 && value <= 10000 ? value : undefined;
|
||||
};
|
||||
// 「键:值」入参里的字数常常就是一个裸数字(字数:500)
|
||||
const bare = text.trim().match(/^(\d{2,5})\s*(?:字|字符)?$/);
|
||||
if (bare) {
|
||||
return valid(bare[1]);
|
||||
}
|
||||
const range = text.match(
|
||||
/(\d{2,5})\s*[-~~—-至到]\s*(\d{2,5})\s*(?:字|字符)/
|
||||
);
|
||||
if (range) {
|
||||
return valid(range[2]);
|
||||
}
|
||||
const single = text.match(/(\d{2,5})\s*(?:字|字符)/);
|
||||
if (single) {
|
||||
return valid(single[1]);
|
||||
}
|
||||
const target = text.match(
|
||||
/(?:不少于|不低于|至少|不超过|大约|约|控制在|限制在|需要|要)\s*(\d{2,5})\s*(?:字|字符)?/
|
||||
);
|
||||
return valid(target?.[1]);
|
||||
};
|
||||
|
||||
/** 从「关于X的」「以X为主题」「写一篇X总结」等说法里抽取活动主题 */
|
||||
export const parseTheme = (query: string) => {
|
||||
const patterns = [
|
||||
/关于[「“"'《]?([^,。;!?\n]{2,40}?)[」”"'》]?的/,
|
||||
/以[「“"'《]?([^,。;!?\n]{2,40}?)[」”"'》]?(?:为主题|为题|为标题)/,
|
||||
/(?:写|撰写|生成|创作|拟写|草拟|做)(?:一篇|一份|一段|一个|一则|条)?[「“"'《]?([^,。;!?\n]{2,40}?)[」”"'》]?(?:上的|中的|里的|的|之)?(?:新闻稿|通讯稿|通稿|报道|推文|总结|通知|公告|文案|稿件|宣传稿|发言稿|脚本|简讯)/,
|
||||
/([^,。;!?\n]{2,30}?)(?:活动)?(?:总结|新闻稿|推文|通知|公告|报道|简讯)/
|
||||
];
|
||||
for (const pattern of patterns) {
|
||||
const match = query.match(pattern);
|
||||
const value = match?.[1]?.trim();
|
||||
if (value) {
|
||||
return tidyTheme(value);
|
||||
}
|
||||
}
|
||||
return '';
|
||||
};
|
||||
|
||||
/**
|
||||
* 主题清洗:把抽到的短语里混进来的时间、地点、指令动词和助词去掉。
|
||||
* 例如「2026年3月5日在学校礼堂举办的学习雷锋志愿服务活动」→「学习雷锋志愿服务活动」。
|
||||
*/
|
||||
const tidyTheme = (value: string) => {
|
||||
const cleaned = value
|
||||
.replace(/^\s*(?:\d{4}\s*年)?\s*\d{1,2}\s*月\s*\d{1,2}\s*[日号]\s*/, '')
|
||||
.replace(
|
||||
/^\s*(?:在|于)[^,。;!?\n]{2,20}?(?:举办|举行|开展|召开|进行)的?/,
|
||||
''
|
||||
)
|
||||
.replace(/^\s*(?:开展|举办|举行|组织|进行|召开)/, '')
|
||||
.replace(/(?:上的|中的|里的|的)$/, '')
|
||||
.replace(/[,。;!?、\s]+$/, '')
|
||||
.trim();
|
||||
return cleaned || value.trim();
|
||||
};
|
||||
|
||||
/** 兜底主题:清洗掉指令性词汇与字数要求后截断 */
|
||||
const fallbackTheme = (query: string) => {
|
||||
const cleaned = query
|
||||
.replace(/[\r\n]+/g, ' ')
|
||||
.replace(
|
||||
/(?:请|帮我|麻烦|辛苦|需要)?\s*(?:写|撰写|生成|创作|拟写|草拟|出一?)(?:一篇|一份|一段|一个|条)?/g,
|
||||
''
|
||||
)
|
||||
.replace(/(?:关于|围绕|以)/g, '')
|
||||
.replace(
|
||||
/\d{2,5}\s*(?:[-~~—-至到]\s*\d{2,5})?\s*(?:字|字符)(?:左右|以内|以下|以上)?/g,
|
||||
''
|
||||
)
|
||||
.replace(/[,。;!?、,.;!?]/g, ' ')
|
||||
.replace(/\s+/g, ' ')
|
||||
.trim();
|
||||
return (cleaned || query.trim()).slice(0, 30);
|
||||
};
|
||||
|
||||
const parseTime = (query: string) => {
|
||||
const match = query.match(/(\d{4}\s*年)?\s*\d{1,2}\s*月\s*\d{1,2}\s*[日号]/);
|
||||
return match ? match[0].replace(/\s+/g, '') : '';
|
||||
};
|
||||
|
||||
const parseLocation = (query: string) => {
|
||||
const match = query.match(
|
||||
/(?:在|于)\s*([^,。;!?\n]{2,20}?)\s*(?:举办|举行|开展|召开|进行)/
|
||||
);
|
||||
return match?.[1]?.trim() || '';
|
||||
};
|
||||
|
||||
const parseParticipants = (query: string) => {
|
||||
const match = query.match(/面向\s*([^,。;!?\n]{2,20})/);
|
||||
return match?.[1]?.trim() || '';
|
||||
};
|
||||
|
||||
const parseEventType = (query: string) => {
|
||||
for (const keyword of EVENT_TYPE_KEYWORDS) {
|
||||
if (query.includes(keyword)) {
|
||||
return keyword;
|
||||
}
|
||||
}
|
||||
return '';
|
||||
};
|
||||
|
||||
/**
|
||||
* 把用户输入解析成上游工作流的入参。
|
||||
*
|
||||
* @param query 用户在写作框里输入的内容(自然语言或「键:值」)
|
||||
* @param overrides 界面上的显式选择,优先于自动识别
|
||||
*/
|
||||
export const parseWritingRequest = (
|
||||
query: string,
|
||||
overrides: WritingOverrides = {}
|
||||
): WritingRequest => {
|
||||
const fields = parseKeyValueInput(query);
|
||||
|
||||
const explicitDocType =
|
||||
overrides.docType && overrides.docType !== 'auto' ? overrides.docType : '';
|
||||
const keyedDocType = normalizeDocType(
|
||||
pickField(fields, FIELD_ALIASES.docType)
|
||||
);
|
||||
const inferredDocType = inferDocType(query) || DEFAULT_DOC_TYPE;
|
||||
const docType = explicitDocType || keyedDocType || inferredDocType;
|
||||
|
||||
const explicitNumber =
|
||||
typeof overrides.number === 'number' && overrides.number > 0
|
||||
? overrides.number
|
||||
: undefined;
|
||||
const keyedNumber = parseWordCount(pickField(fields, FIELD_ALIASES.number));
|
||||
const inferredNumber = parseWordCount(query);
|
||||
const number =
|
||||
explicitNumber ??
|
||||
keyedNumber ??
|
||||
inferredNumber ??
|
||||
DEFAULT_WORD_COUNT_BY_DOC_TYPE[docType] ??
|
||||
800;
|
||||
|
||||
const theme =
|
||||
pickField(fields, FIELD_ALIASES.theme) ||
|
||||
parseTheme(query) ||
|
||||
fallbackTheme(query);
|
||||
|
||||
return {
|
||||
docType,
|
||||
eventType:
|
||||
pickField(fields, FIELD_ALIASES.eventType) ||
|
||||
parseEventType(query) ||
|
||||
DEFAULT_EVENT_TYPE,
|
||||
theme,
|
||||
time: pickField(fields, FIELD_ALIASES.time) || parseTime(query),
|
||||
location: pickField(fields, FIELD_ALIASES.location) || parseLocation(query),
|
||||
participants:
|
||||
pickField(fields, FIELD_ALIASES.participants) || parseParticipants(query),
|
||||
// 「核心亮点与具体要求」保留用户原话:这里放原话比放抽取结果更贴合上游用途
|
||||
highlights: pickField(fields, FIELD_ALIASES.highlights) || query,
|
||||
number
|
||||
};
|
||||
};
|
||||
Reference in New Issue
Block a user