fix: 收紧API JSDoc门禁

This commit is contained in:
sunlei 2026-07-25 11:38:10 +08:00
parent ffd3f503c5
commit de9dd56bb6
36 changed files with 1305 additions and 176 deletions

View File

@ -90,7 +90,7 @@ function getStagedFiles() {
.filter(Boolean);
}
run(getPnpmCommand(), ['run', 'check:jsdoc']);
run(getPnpmCommand(), ['run', 'check:jsdoc', '--', '--staged']);
const files = getStagedFiles().filter((file) => {
if (!existsSync(file)) return false;

View File

@ -1,7 +1,7 @@
#!/usr/bin/env node
import { execFileSync } from 'node:child_process';
import { existsSync, readFileSync, writeFileSync } from 'node:fs';
import { readFileSync, writeFileSync } from 'node:fs';
import path from 'node:path';
import { pathToFileURL } from 'node:url';
@ -17,35 +17,168 @@ const SOURCE_EXTENSIONS = new Set([
]);
const SOURCE_PATHSPECS = ['*.cjs', '*.js', '*.mjs', '*.ts', '*.tsx', '*.vue'];
const CHINESE_CHARACTER_PATTERN = /\p{Script=Han}/u;
const LINE_TERMINATOR_PATTERN = /\r\n|\r|\n/u;
const VUE_SCRIPT_PATTERN = /<script\b([^>]*)>([\s\S]*?)<\/script\s*>/giu;
const TYPEORM_ENTITY_LISTENER_DECORATORS = new Set([
'AfterInsert',
'AfterLoad',
'AfterRecover',
'AfterRemove',
'AfterSoftRemove',
'AfterUpdate',
'BeforeInsert',
'BeforeRecover',
'BeforeRemove',
'BeforeSoftRemove',
'BeforeUpdate',
]);
const CODE_ONLY_TAGS = new Set(['example']);
const STRUCTURAL_TAGS = new Set([
'alias',
'author',
'borrows',
'callback',
'constant',
'constructs',
'copyright',
'default',
'defaultvalue',
'enum',
'exports',
'extends',
'external',
'fires',
'implements',
'kind',
'license',
'link',
'listens',
'member',
'memberof',
'mixes',
'module',
'name',
'namespace',
'requires',
'satisfies',
'since',
'this',
'type',
'typedef',
'version',
]);
const BARE_TYPE_TAGS = new Set([
'exception',
'return',
'returns',
'throws',
'yield',
'yields',
]);
const PARAMETER_TAGS = new Set([
'arg',
'argument',
'param',
'prop',
'property',
'template',
'typeparam',
]);
const BUILTIN_TYPE_NAMES = new Set([
'any',
'bigint',
'boolean',
'never',
'null',
'number',
'object',
'string',
'symbol',
'undefined',
'unknown',
'void',
]);
const ENGLISH_FUNCTION_WORDS = new Set([
'a',
'after',
'an',
'and',
'are',
'as',
'at',
'before',
'by',
'for',
'from',
'if',
'in',
'into',
'is',
'it',
'its',
'of',
'on',
'or',
'otherwise',
'that',
'the',
'these',
'this',
'those',
'to',
'was',
'were',
'when',
'whether',
'with',
]);
const LEADING_DESCRIPTION_VERBS = new Set([
'build',
'check',
'cleanup',
'convert',
'create',
'delete',
'determine',
'ensure',
'fetch',
'format',
'get',
'handle',
'initialize',
'load',
'parse',
'read',
'remove',
'render',
'resolve',
'return',
'save',
'send',
'set',
'transform',
'update',
'use',
'validate',
'write',
]);
const LIFECYCLE_ENTRY_NAMES = new Set([
'activated',
'afterInit',
'afterInsert',
'afterLoad',
'afterQuery',
'afterRecover',
'afterRemove',
'afterSoftRemove',
'afterTransactionCommit',
'afterTransactionRollback',
'afterTransactionStart',
'afterUpdate',
'beforeApplicationShutdown',
'beforeCreate',
'beforeDestroy',
'beforeInsert',
'beforeMount',
'beforeModuleDestroy',
'beforeQuery',
'beforeRecover',
'beforeRemove',
'beforeSoftRemove',
'beforeTransactionCommit',
'beforeTransactionRollback',
'beforeTransactionStart',
'beforeUnmount',
'beforeUpdate',
'componentDidCatch',
'componentDidMount',
'componentDidUpdate',
@ -148,10 +281,408 @@ function getNodeName(node, sourceFile) {
return node.name.text;
}
if (ts.isComputedPropertyName(node.name)) {
const expression = node.name.expression;
if (ts.isStringLiteralLike(expression) || ts.isNumericLiteral(expression)) {
return expression.text;
}
}
return node.name.getText(sourceFile);
}
function classifyOwner(node, sourceFile) {
function getFunctionExpressionRole(node, sourceFile) {
let expression = node;
while (
(ts.isParenthesizedExpression(expression.parent) ||
ts.isAsExpression(expression.parent) ||
ts.isTypeAssertionExpression(expression.parent) ||
ts.isNonNullExpression(expression.parent) ||
ts.isSatisfiesExpression(expression.parent)) &&
expression.parent.expression === expression
) {
expression = expression.parent;
}
const parent = expression.parent;
if (
(ts.isPropertyAssignment(parent) || ts.isPropertyDeclaration(parent)) &&
parent.initializer === expression
) {
return getNodeName(parent, sourceFile);
}
return undefined;
}
function collectTypeOrmListenerBindings(sourceFile) {
const importedNames = new Set();
const namespaceNames = new Set();
for (const statement of sourceFile.statements) {
if (
!ts.isImportDeclaration(statement) ||
!ts.isStringLiteral(statement.moduleSpecifier) ||
statement.moduleSpecifier.text !== 'typeorm' ||
statement.importClause?.isTypeOnly
) {
continue;
}
const namedBindings = statement.importClause?.namedBindings;
if (ts.isNamespaceImport(namedBindings)) {
namespaceNames.add(namedBindings.name.text);
continue;
}
if (!ts.isNamedImports(namedBindings)) continue;
for (const element of namedBindings.elements) {
const importedName = element.propertyName?.text || element.name.text;
if (
!element.isTypeOnly &&
TYPEORM_ENTITY_LISTENER_DECORATORS.has(importedName)
) {
importedNames.add(element.name.text);
}
}
}
return {
importedNames,
namespaceNames,
};
}
function getTypeOrmListenerDecoratorName(node, bindings) {
if (!ts.canHaveDecorators(node)) return undefined;
for (const decorator of ts.getDecorators(node) || []) {
const expression = ts.isCallExpression(decorator.expression)
? decorator.expression.expression
: decorator.expression;
if (
ts.isIdentifier(expression) &&
bindings.importedNames.has(expression.text)
) {
return expression.text;
}
if (
ts.isPropertyAccessExpression(expression) &&
ts.isIdentifier(expression.expression) &&
bindings.namespaceNames.has(expression.expression.text) &&
TYPEORM_ENTITY_LISTENER_DECORATORS.has(expression.name.text)
) {
return expression.name.text;
}
}
return undefined;
}
function maskNonNaturalLanguage(text) {
return text
.replace(/```[\s\S]*?```/gu, (code) => code.replace(/[^\r\n]/gu, '\0'))
.replace(/`[^`\r\n]*`/gu, '\0')
.replace(/\{@(?:code|link|linkcode|linkplain)\s+[^}]+\}/giu, '\0')
.replace(/\b(?:https?|ftp):\/\/[^\s<>{}|\\^`[\]]+/giu, '\0')
.replace(/\b[\w.+-]+@[\w.-]+\.[A-Za-z]{2,}\b/gu, '\0')
.replace(/\bv?\d+(?:\.\d+){1,}\b/gu, '\0')
.replace(/<[^>\r\n]+>/gu, '\0')
.replace(/^[\s*-]+/u, '')
.trim();
}
function splitDescriptionSentences(text) {
return text
.split(/(?:[。!?;]+|[.!?;]+(?=\s|$))/u)
.map((sentence) => sentence.trim())
.filter((sentence) => sentence.replaceAll('\0', '').trim());
}
function isStrongTechnicalWord(word) {
if (/\d/u.test(word)) return true;
return (
word === word.toUpperCase() ||
/[_$]/u.test(word) ||
/[A-Z]/u.test(word.slice(1))
);
}
function isTitleCaseWord(word) {
return /^[A-Z][a-z]+(?:-[A-Z]?[a-z]+)*$/u.test(word);
}
function isLeadingDescriptionVerb(word) {
const lower = word.toLowerCase();
const candidates = new Set([lower]);
if (lower.endsWith('ies')) {
candidates.add(`${lower.slice(0, -3)}y`);
}
if (lower.endsWith('ing')) {
const stem = lower.slice(0, -3);
candidates.add(stem);
candidates.add(`${stem}e`);
if (stem.at(-1) === stem.at(-2)) {
candidates.add(stem.slice(0, -1));
}
}
if (lower.endsWith('ed')) {
const stem = lower.slice(0, -2);
candidates.add(stem);
candidates.add(`${stem}e`);
}
if (lower.endsWith('es')) {
candidates.add(lower.slice(0, -2));
candidates.add(lower.slice(0, -1));
} else if (lower.endsWith('s')) {
candidates.add(lower.slice(0, -1));
}
return [...candidates].some((candidate) =>
LEADING_DESCRIPTION_VERBS.has(candidate),
);
}
function containsEnglishClause(sentence) {
const chineseCount = [...sentence].filter((character) =>
CHINESE_CHARACTER_PATTERN.test(character),
).length;
for (const region of sentence.split(/[\p{Script=Han}\0]+/u)) {
const words = [
...region.matchAll(/[A-Za-z][A-Za-z0-9]*(?:[-'][A-Za-z0-9]+)*/gu),
].map((match) => match[0]);
const ordinaryWords = words.filter((word) => !isStrongTechnicalWord(word));
if (ordinaryWords.length === 0) continue;
if (
ordinaryWords.length >= 2 &&
isLeadingDescriptionVerb(ordinaryWords[0])
) {
return true;
}
const isTitleCaseTechnicalPhrase = words.every(
(word) => isStrongTechnicalWord(word) || isTitleCaseWord(word),
);
if (isTitleCaseTechnicalPhrase) continue;
const hasFunctionWord = ordinaryWords.some((word) =>
ENGLISH_FUNCTION_WORDS.has(word.toLowerCase()),
);
if (
(ordinaryWords.length >= 2 && hasFunctionWord) ||
(ordinaryWords.length >= 4 && ordinaryWords.length > chineseCount)
) {
return true;
}
}
return false;
}
function isBareTypePayload(text) {
const payload = text.trim();
if (!payload || payload.startsWith('-')) return false;
const rootName = payload.match(/^[A-Za-z_$][A-Za-z0-9_$]*/u)?.[0];
if (
!rootName ||
(!BUILTIN_TYPE_NAMES.has(rootName) && !/^[A-Z]/u.test(rootName))
) {
return false;
}
const sourceFile = ts.createSourceFile(
'jsdoc-type.ts',
`type JSDocPayload = ${payload};`,
ts.ScriptTarget.Latest,
true,
ts.ScriptKind.TS,
);
return sourceFile.parseDiagnostics.length === 0;
}
function stripLeadingTypeExpression(text) {
const trimmedText = text.trimStart();
if (!trimmedText.startsWith('{')) return trimmedText;
let depth = 0;
for (let index = 0; index < trimmedText.length; index += 1) {
const character = trimmedText[index];
if (character === '{') {
depth += 1;
} else if (character === '}') {
depth -= 1;
if (depth === 0) {
return trimmedText.slice(index + 1).trimStart();
}
}
}
return trimmedText;
}
function getTagDescription(tagName, rawValue) {
if (CODE_ONLY_TAGS.has(tagName)) return undefined;
if (STRUCTURAL_TAGS.has(tagName) || tagName === 'see') {
const separatorIndex = rawValue.indexOf(' - ');
return separatorIndex === -1
? undefined
: rawValue.slice(separatorIndex + 3).trim() || undefined;
}
let value = stripLeadingTypeExpression(rawValue);
if (PARAMETER_TAGS.has(tagName)) {
value = value
.replace(/^(?:\[[^\]]+\]|[^\s-]+)\s*/u, '')
.replace(/^-\s*/u, '');
} else if (BARE_TYPE_TAGS.has(tagName) && isBareTypePayload(value)) {
return undefined;
}
return value.trim() || undefined;
}
function collectDescriptionUnits(rawComment) {
const units = [];
const bodyLines = rawComment
.replace(/^\/\*\*/u, '')
.replace(/\*\/$/u, '')
.split(/\r\n|\r|\n/u)
.map((line) => line.replace(/^\s*\*\s?/u, '').trimEnd());
let current = {
label: '主说明',
lines: [],
startLineOffset: 0,
tagName: undefined,
};
let insideFence = false;
function flushCurrent() {
const firstContentLine = current.lines.findIndex(
(line) => line.trim() !== '',
);
if (firstContentLine === -1) return;
const rawValue = current.lines.slice(firstContentLine).join('\n').trim();
if (!rawValue) return;
const lineOffset = current.startLineOffset + firstContentLine;
if (!current.tagName) {
units.push({
label: current.label,
lineOffset,
text: rawValue,
});
return;
}
const description = getTagDescription(current.tagName, rawValue);
if (description) {
units.push({
label: current.label,
lineOffset,
text: description,
});
}
}
for (let lineOffset = 0; lineOffset < bodyLines.length; lineOffset += 1) {
const line = bodyLines[lineOffset];
const fenceCount = line.match(/```/gu)?.length || 0;
if (insideFence) {
current.lines.push(line);
if (fenceCount % 2 === 1) insideFence = false;
continue;
}
if (fenceCount > 0) {
current.lines.push(line);
if (fenceCount % 2 === 1) insideFence = true;
continue;
}
if (!current.tagName && !line.trim()) {
flushCurrent();
current = {
label: '主说明',
lines: [],
startLineOffset: lineOffset + 1,
tagName: undefined,
};
continue;
}
const tagMatch = line.match(/^@([A-Za-z][\w-]*)(?:\s(.*))?$/u);
if (tagMatch) {
if (current.tagName === 'example' && /^[A-Z]/u.test(tagMatch[1])) {
continue;
}
flushCurrent();
const tagName = tagMatch[1].toLowerCase();
current = {
label: `@${tagName}`,
lines: CODE_ONLY_TAGS.has(tagName) ? [] : [tagMatch[2] || ''],
startLineOffset: lineOffset,
tagName,
};
continue;
}
if (!CODE_ONLY_TAGS.has(current.tagName)) {
current.lines.push(line);
}
}
flushCurrent();
return units;
}
function findDescriptionIssue(rawComment) {
const units = collectDescriptionUnits(rawComment);
for (const unit of units) {
const maskedText = maskNonNaturalLanguage(unit.text);
const unitHasChinese = CHINESE_CHARACTER_PATTERN.test(maskedText);
const lines = maskedText.split(/\r\n|\r|\n/u);
for (let lineOffset = 0; lineOffset < lines.length; lineOffset += 1) {
const sentences = splitDescriptionSentences(lines[lineOffset]);
for (const sentence of sentences) {
const hasChinese = CHINESE_CHARACTER_PATTERN.test(sentence);
const hasEnglishClause = containsEnglishClause(sentence);
if (!hasChinese) {
if (unitHasChinese && !hasEnglishClause) continue;
return {
label: unit.label,
lineOffset: unit.lineOffset + lineOffset,
reason: `${unit.label} 描述必须使用中文`,
};
}
if (hasEnglishClause) {
return {
label: unit.label,
lineOffset: unit.lineOffset + lineOffset,
reason: `${unit.label} 描述包含完整英文说明,必须改为中文`,
};
}
}
}
}
return undefined;
}
function classifyOwner(node, sourceFile, typeOrmBindings) {
if (!node) {
return {
code: 'invalid-target',
@ -177,12 +708,28 @@ function classifyOwner(node, sourceFile) {
}
const ownerName = getNodeName(node, sourceFile);
if (ownerName === 'setup' || LIFECYCLE_ENTRY_NAMES.has(ownerName)) {
const typeOrmListenerName = getTypeOrmListenerDecoratorName(
node,
typeOrmBindings,
);
const roleName = ts.isFunctionExpression(node)
? getFunctionExpressionRole(node, sourceFile)
: ts.isFunctionDeclaration(node)
? undefined
: ownerName;
const isSetupEntry = roleName === 'setup';
const lifecycleEntryName =
roleName && LIFECYCLE_ENTRY_NAMES.has(roleName) ? roleName : undefined;
if (isSetupEntry || lifecycleEntryName || typeOrmListenerName) {
const entryName = typeOrmListenerName
? `@${typeOrmListenerName}`
: lifecycleEntryName || roleName;
return {
code: 'forbidden-entry',
ownerKind: ts.SyntaxKind[node.kind],
ownerName,
reason: `${ownerName} 是禁止使用 JSDoc 的 setup 或生命周期入口`,
reason: `${entryName} 是禁止使用 JSDoc 的 setup 或生命周期入口`,
};
}
@ -292,6 +839,7 @@ function analyzeScriptBlock(block) {
);
const ownedDocs = collectOwnedJsDocs(sourceFile);
const rawDocs = collectRawJsDocs(sourceFile);
const typeOrmBindings = collectTypeOrmListenerBindings(sourceFile);
const issues = [];
const removals = [];
let retainedCount = 0;
@ -299,9 +847,10 @@ function analyzeScriptBlock(block) {
for (const rawDoc of rawDocs) {
const start = rawDoc.start;
const end = rawDoc.end;
const text = block.sourceText.slice(start, end);
const owner = ownedDocs.get(`${start}:${end}`)?.owner;
const classification = classifyOwner(owner, sourceFile);
const rawComment = block.sourceText.slice(start, end);
const ownedDoc = ownedDocs.get(`${start}:${end}`);
const owner = ownedDoc?.owner;
const classification = classifyOwner(owner, sourceFile, typeOrmBindings);
const position = sourceFile.getLineAndCharacterOfPosition(start);
const common = {
column: position.character + 1,
@ -326,11 +875,14 @@ function analyzeScriptBlock(block) {
continue;
}
if (!CHINESE_CHARACTER_PATTERN.test(text)) {
const descriptionIssue = findDescriptionIssue(rawComment);
if (descriptionIssue) {
issues.push({
...common,
code: 'missing-chinese-description',
reason: 'JSDoc 描述必须整体以中文为主,不能是纯英文或仅标签',
descriptionLineOffset: descriptionIssue.lineOffset,
descriptionUnit: descriptionIssue.label,
reason: descriptionIssue.reason,
});
removals.push({
end: common.end,
@ -385,6 +937,10 @@ export function validateSourceText(filePath, sourceText) {
}
function expandStandaloneRemoval(sourceText, removal) {
const lineTerminator =
sourceText
.slice(removal.start, removal.end)
.match(LINE_TERMINATOR_PATTERN)?.[0] || '';
const lineStart = sourceText.lastIndexOf('\n', removal.start - 1) + 1;
const nextLineBreak = sourceText.indexOf('\n', removal.end);
const lineEnd = nextLineBreak === -1 ? sourceText.length : nextLineBreak + 1;
@ -397,6 +953,7 @@ function expandStandaloneRemoval(sourceText, removal) {
if (before.trim() === '' && after.trim() === '') {
return {
end: lineEnd,
replacement: '',
start: lineStart,
};
}
@ -405,11 +962,15 @@ function expandStandaloneRemoval(sourceText, removal) {
if (horizontalWhitespace) {
return {
end: removal.end,
replacement: lineTerminator,
start: removal.start - horizontalWhitespace.length,
};
}
return removal;
return {
...removal,
replacement: lineTerminator,
};
}
function mergeRemovals(removals) {
@ -420,6 +981,7 @@ function mergeRemovals(removals) {
const previous = merged.at(-1);
if (previous && removal.start <= previous.end) {
previous.end = Math.max(previous.end, removal.end);
previous.replacement ||= removal.replacement;
} else {
merged.push({ ...removal });
}
@ -440,6 +1002,7 @@ export function fixSourceText(filePath, sourceText) {
for (const removal of removals.toReversed()) {
fixedSourceText =
fixedSourceText.slice(0, removal.start) +
removal.replacement +
fixedSourceText.slice(removal.end);
}
@ -458,9 +1021,23 @@ function tokenizeScript(filePath, sourceText, scriptKind) {
true,
scriptKind,
);
return getLeafTokens(sourceFile)
.filter((node) => node.kind !== ts.SyntaxKind.EndOfFileToken)
.map((node) => `${node.kind}:${node.getText(sourceFile)}`);
const tokens = getLeafTokens(sourceFile).filter(
(node) => node.kind !== ts.SyntaxKind.EndOfFileToken,
);
return tokens.map((node, index) => {
const previous = tokens[index - 1];
const hasLineTerminator =
previous &&
LINE_TERMINATOR_PATTERN.test(
sourceText.slice(previous.end, node.getStart(sourceFile)),
);
const tokenText = node
.getText(sourceFile)
.replaceAll('\r\n', '\n')
.replaceAll('\r', '\n');
return `${hasLineTerminator ? 'line-break' : 'same-line'}:${node.kind}:${tokenText}`;
});
}
export function getTokenSignature(filePath, sourceText) {
@ -485,7 +1062,7 @@ function splitNullDelimited(output) {
return output.toString('utf8').split('\0').filter(Boolean);
}
function listCurrentSourceFiles(repoRoot) {
function listWorkingTreeSourceFiles(repoRoot) {
const output = execFileSync(
'git',
[
@ -506,6 +1083,27 @@ function listCurrentSourceFiles(repoRoot) {
return splitNullDelimited(output).filter(isSupportedSourceFile).sort();
}
function listIndexSourceFiles(repoRoot) {
const output = execFileSync(
'git',
['ls-files', '--cached', '-z', '--', ...SOURCE_PATHSPECS],
{
cwd: repoRoot,
encoding: 'buffer',
},
);
return splitNullDelimited(output).filter(isSupportedSourceFile).sort();
}
function readIndexSourceFile(repoRoot, filePath) {
return execFileSync('git', ['show', `:${filePath}`], {
cwd: repoRoot,
encoding: 'utf8',
maxBuffer: 64 * 1024 * 1024,
});
}
function listRefSourceFiles(repoRoot, ref) {
const output = execFileSync(
'git',
@ -539,6 +1137,7 @@ function compareRepositoryTokens(
repoRoot,
ref,
currentFiles,
readCurrentSource,
allowedTokenChanges,
) {
const currentFileSet = new Set(currentFiles);
@ -550,11 +1149,10 @@ function compareRepositoryTokens(
let comparedTokens = 0;
for (const filePath of refFiles) {
const absolutePath = path.resolve(repoRoot, filePath);
if (!currentFileSet.has(filePath) || !existsSync(absolutePath)) {
if (!currentFileSet.has(filePath)) {
mismatches.push({
filePath,
reason: '基线文件在当前工作树中缺失',
reason: '基线文件在当前代码快照中缺失',
});
continue;
}
@ -564,7 +1162,7 @@ function compareRepositoryTokens(
encoding: 'utf8',
maxBuffer: 64 * 1024 * 1024,
});
const afterSource = readFileSync(absolutePath, 'utf8');
const afterSource = readCurrentSource(filePath);
const beforeTokens = getTokenSignature(filePath, beforeSource);
const afterTokens = getTokenSignature(filePath, afterSource);
const difference = findFirstTokenDifference(beforeTokens, afterTokens);
@ -649,8 +1247,18 @@ function runRepositoryCheck({
compareRef,
fix,
repoRoot,
staged,
}) {
const files = listCurrentSourceFiles(repoRoot);
if (fix && staged) {
throw new Error('--fix 不能与 --staged 同时使用');
}
const files = staged
? listIndexSourceFiles(repoRoot)
: listWorkingTreeSourceFiles(repoRoot);
const readCurrentSource = staged
? (filePath) => readIndexSourceFile(repoRoot, filePath)
: (filePath) => readFileSync(path.resolve(repoRoot, filePath), 'utf8');
const issues = [];
const totals = {
forbiddenEntryCount: 0,
@ -663,15 +1271,14 @@ function runRepositoryCheck({
let removedCount = 0;
for (const filePath of files) {
const absolutePath = path.resolve(repoRoot, filePath);
const sourceText = readFileSync(absolutePath, 'utf8');
const sourceText = readCurrentSource(filePath);
if (fix) {
const result = fixSourceText(filePath, sourceText);
addStats(totals, result.stats);
removedCount += result.removedCount;
if (result.sourceText !== sourceText) {
writeFileSync(absolutePath, result.sourceText);
writeFileSync(path.resolve(repoRoot, filePath), result.sourceText);
changedFiles += 1;
}
continue;
@ -683,7 +1290,13 @@ function runRepositoryCheck({
}
const comparison = compareRef
? compareRepositoryTokens(repoRoot, compareRef, files, allowedTokenChanges)
? compareRepositoryTokens(
repoRoot,
compareRef,
files,
readCurrentSource,
allowedTokenChanges,
)
: undefined;
const summary = {
changedFiles,
@ -692,6 +1305,7 @@ function runRepositoryCheck({
issues: issues.length,
mode: fix ? 'fix' : 'check',
removedCount,
sourceMode: staged ? 'index' : 'worktree',
...totals,
};
@ -722,6 +1336,7 @@ function main() {
compareRef: parseOption(args, '--compare-ref'),
fix: args.includes('--fix'),
repoRoot,
staged: args.includes('--staged'),
});
}

View File

@ -1,5 +1,10 @@
import assert from 'node:assert/strict';
import { execFileSync, spawnSync } from 'node:child_process';
import { mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
import { tmpdir } from 'node:os';
import path from 'node:path';
import test from 'node:test';
import { fileURLToPath } from 'node:url';
import {
fixSourceText,
@ -7,12 +12,100 @@ import {
validateSourceText,
} from './check-jsdoc-policy.mjs';
const CHECKER_PATH = fileURLToPath(
new URL('./check-jsdoc-policy.mjs', import.meta.url),
);
const HOOK_CHECK_PATH = fileURLToPath(
new URL('../husky-fast-check.mjs', import.meta.url),
);
function issueCodes(sourceText, filePath = 'fixture.ts') {
return validateSourceText(filePath, sourceText).issues.map(
(issue) => issue.code,
);
}
function createIsolatedGitEnvironment(sourceEnvironment = process.env) {
const environment = Object.fromEntries(
Object.entries(sourceEnvironment).filter(
([name]) => !name.startsWith('GIT_'),
),
);
return {
...environment,
GIT_CONFIG_GLOBAL: '/dev/null',
GIT_CONFIG_NOSYSTEM: '1',
};
}
function executeGit(
repositoryPath,
args,
{ environment = process.env, gitEnvironment = {} } = {},
) {
return execFileSync('git', args, {
cwd: repositoryPath,
encoding: 'utf8',
env: {
...createIsolatedGitEnvironment(environment),
...gitEnvironment,
},
});
}
function createFixtureRepository(sourceEnvironment = process.env) {
const repositoryPath = mkdtempSync(path.join(tmpdir(), 'kt-jsdoc-policy-'));
const validSource = '/** 解析输入。 */\nfunction parseInput() {}\n';
executeGit(repositoryPath, ['init', '--quiet'], {
environment: sourceEnvironment,
});
executeGit(repositoryPath, ['config', 'user.email', 'codex@example.test'], {
environment: sourceEnvironment,
});
executeGit(repositoryPath, ['config', 'user.name', 'Codex Test'], {
environment: sourceEnvironment,
});
writeFileSync(path.join(repositoryPath, 'fixture.ts'), validSource);
writeFileSync(
path.join(repositoryPath, 'deleted.ts'),
'/** 读取输入。 */\nfunction readInput() {}\n',
);
executeGit(repositoryPath, ['add', 'fixture.ts', 'deleted.ts'], {
environment: sourceEnvironment,
});
executeGit(
repositoryPath,
['-c', 'commit.gpgSign=false', 'commit', '--quiet', '-m', 'fixture'],
{ environment: sourceEnvironment },
);
return {
repositoryPath,
validSource,
};
}
function runChecker(
repositoryPath,
args,
{ gitEnvironment = {}, sourceEnvironment = process.env } = {},
) {
return spawnSync(
process.execPath,
[CHECKER_PATH, '--repo-root', repositoryPath, ...args],
{
cwd: repositoryPath,
encoding: 'utf8',
env: {
...createIsolatedGitEnvironment(sourceEnvironment),
...gitEnvironment,
},
},
);
}
test('accepts Chinese JSDoc on explicitly named functions and methods', () => {
const sourceText = `
/** 计算输入值。 */
@ -77,6 +170,404 @@ class Example {
]);
});
test('rejects every imported TypeORM entity listener decorator', () => {
const listenerDecorators = [
'AfterLoad',
'BeforeInsert',
'AfterInsert',
'BeforeUpdate',
'AfterUpdate',
'BeforeRemove',
'AfterRemove',
'BeforeSoftRemove',
'AfterSoftRemove',
'BeforeRecover',
'AfterRecover',
];
const methods = listenerDecorators
.map(
(decoratorName, index) => `
/** 处理实体监听事件。 */
@${decoratorName}()
handleEntityEvent${index}() {}
`,
)
.join('');
const sourceText = `
import { ${listenerDecorators.join(', ')} } from 'typeorm';
class Example {
${methods}
}
`;
assert.deepEqual(
issueCodes(sourceText),
listenerDecorators.map(() => 'forbidden-entry'),
);
});
test('recognizes aliased and namespace TypeORM listeners without rejecting ordinary same-name methods', () => {
const decoratedSource = `
import { BeforeInsert as PersistEntity } from 'typeorm';
import * as TypeOrm from 'typeorm';
class EntityExample {
/** 持久化实体。 */
@PersistEntity()
persist() {}
/** 加载实体。 */
@TypeOrm.AfterLoad()
hydrate() {}
}
`;
const ordinarySource = `
class OrdinaryExample {
/** 执行业务前置处理。 */
beforeInsert() {}
/** 执行业务加载处理。 */
afterLoad() {}
}
`;
assert.deepEqual(issueCodes(decoratedSource), [
'forbidden-entry',
'forbidden-entry',
]);
assert.deepEqual(issueCodes(ordinarySource), []);
});
test('rejects English prose in each main or tag description unit', () => {
const fixtures = [
`
/** 参数 Parse the entire incoming HTTP payload before dispatch. */
function parsePayload() {}
`,
`
/**
* 清理登录运行态
* @param options - Cleanup switches that may revisit provisional rows.
* @returns 清理结果
*/
function cleanupRuntime(options) {}
`,
`
/**
* 格式化计数
* @param value - 原始计数
* @returns Counter text such as \`7890\` or \`12.3万\`.
*/
function formatCounter(value) {}
`,
`
/**
* Parse the entire incoming payload before dispatch.
* @example
* const 参数 = readPayload();
*/
function readPayload() {}
`,
`
/**
* 中文说明
* Asynchronously handle the login process.
*/
function handleLogin() {}
`,
`
/** 中文引子。 Determine whether there is permission from the user role. */
function determinePermission() {}
`,
`
/** 中文the payload is returned by the API. */
function returnPayload() {}
`,
`
/** 中 Returns payload. */
function readShortPayload() {}
`,
`
/** Get machine-readable API runtime health。 */
function getRuntimeHealth() {}
`,
`
/**
* 查询结果
* @returns payload
*/
function readResult() {}
`,
`
/**
* 查询字段
* @param fields record
*/
function readFields(fields) {}
`,
];
for (const fixture of fixtures) {
assert.deepEqual(issueCodes(fixture), ['missing-chinese-description']);
}
const tagIssue = validateSourceText('fixture.ts', fixtures[1]).issues[0];
assert.equal(tagIssue.line, 2);
assert.match(tagIssue.reason, /@param/u);
});
test('allows masked code, URLs, identifiers, and necessary technical terms inside Chinese units', () => {
const fixtures = [
`
/**
* 解析 HTTP payload并把 userId 写入 MySQL DTO
* @param payload - HTTP payload 输入
* @returns MySQL DTO 结果
* @see https://example.test/payload
* @example
* const 参数 = \`HTTP payload\`;
*/
function parsePayload(payload) {}
`,
`
/**
* 解析请求
* HTTP payload
* 并返回 DTO
*/
function parseTechnicalContinuation() {}
`,
`
/** 设置 Dict Decode Cache。 */
function setDictDecodeCache() {}
`,
`
/** 创建 Markdown code fence 到 Argon 代码块 DOM 的 rehype 转换插件。 */
function createMarkdownPlugin() {}
`,
`
/**
* 读取文件
* @returns Blob
*/
function readBlob() {}
`,
`
/**
* 请求文件
* @returns RequestResponse<Blob>
*/
function requestBlob() {}
`,
`
/**
* 查询角色
* @param roles
*/
function readRoles(roles) {}
`,
];
for (const fixture of fixtures) {
assert.deepEqual(issueCodes(fixture), []);
}
});
test('treats inline code and structural tags as neutral while validating custom and marker tag prose', () => {
const validSource = `
/**
* {@code 中文变量}
* @custom 中文扩展说明
* @public
* @author Jane Doe
* @type {Record<string, unknown>}
* @returns RequestResponse<Blob>
*/
function authenticate() {}
`;
const invalidCustomSource = `
/**
* 执行载荷检查
* @custom payload {@code 中文变量}
*/
function checkPayload() {}
`;
const invalidMarkerSource = `
/**
* 执行公开接口检查
* @public Public API access is enabled.
*/
function checkPublicApi() {}
`;
assert.deepEqual(issueCodes(validSource), []);
const customIssue = validateSourceText('fixture.ts', invalidCustomSource)
.issues[0];
assert.equal(customIssue?.code, 'missing-chinese-description');
assert.match(customIssue?.reason || '', /@custom/u);
const markerIssue = validateSourceText('fixture.ts', invalidMarkerSource)
.issues[0];
assert.equal(markerIssue?.code, 'missing-chinese-description');
assert.match(markerIssue?.reason || '', /@public/u);
});
test('validates prose after structural tag payloads', () => {
const validSource = `
/**
* 创建选项类型
* @typedef {object} Options - 选项说明
* @extends {Base} - 基类说明
*/
function createOptions() {}
`;
const invalidSource = `
/**
* 创建选项类型
* @typedef {object} Options - This is English prose.
*/
function createOptions() {}
`;
assert.deepEqual(issueCodes(validSource), []);
const issue = validateSourceText('fixture.ts', invalidSource).issues[0];
assert.equal(issue?.code, 'missing-chinese-description');
assert.match(issue?.reason || '', /@typedef/u);
});
test('does not parse fenced code or example decorators as description tags', () => {
const sourceText = `
/**
* 展示装饰器示例
* \`\`\`ts
* @returns payload
* \`\`\`
* @example
* @Component
* class Demo {}
*/
function showDecoratorExample() {}
`;
assert.deepEqual(issueCodes(sourceText), []);
});
test('does not share a Chinese anchor across main-description paragraphs', () => {
const invalidSource = `
/**
* 中文说明
*
* Cache expires tomorrow.
*/
function readCache() {}
`;
const validSource = `
/**
* 解析载荷
* HTTP payload
* 并返回结果
*/
function readPayload() {}
`;
assert.deepEqual(issueCodes(invalidSource), ['missing-chinese-description']);
assert.deepEqual(issueCodes(validSource), []);
});
test('uses property roles for setup and lifecycle function expressions', () => {
const sourceText = `
const component = {
mounted: /** 组件挂载入口。 */ function mountedHook() {},
setup: /** 组件初始化入口。 */ function namedSetup() {},
['mounted']: /** 计算属性挂载入口。 */ function computedMountedHook() {},
['setup']: /** 计算属性初始化入口。 */ function computedSetup() {},
updated: (/** 括号包裹的更新入口。 */ function updatedHook() {}),
task: /** 执行普通任务。 */ function namedTask() {},
['computedTask']: /** 执行普通计算属性任务。 */ function computedTask() {},
};
/** 读取普通同名函数。 */
function mounted() {}
`;
const issues = validateSourceText('fixture.ts', sourceText).issues;
assert.deepEqual(
issues.map((issue) => issue.code),
[
'forbidden-entry',
'forbidden-entry',
'forbidden-entry',
'forbidden-entry',
'forbidden-entry',
],
);
assert.deepEqual(
issues.map(({ ownerName, reason }) => [ownerName, reason]),
[
['mountedHook', 'mounted 是禁止使用 JSDoc 的 setup 或生命周期入口'],
['namedSetup', 'setup 是禁止使用 JSDoc 的 setup 或生命周期入口'],
[
'computedMountedHook',
'mounted 是禁止使用 JSDoc 的 setup 或生命周期入口',
],
['computedSetup', 'setup 是禁止使用 JSDoc 的 setup 或生命周期入口'],
['updatedHook', 'updated 是禁止使用 JSDoc 的 setup 或生命周期入口'],
],
);
});
test('allows ordinary same-name named functions outside entry roles', () => {
const sourceText = `
/** 执行普通初始化任务。 */
function setup() {}
const setupTask =
/** 执行普通具名函数表达式。 */
function setup() {};
const mountedTask =
/** 执行普通具名函数表达式。 */
function mounted() {};
consume(
/** 执行普通具名回调。 */
function onModuleInit() {},
);
`;
assert.deepEqual(issueCodes(sourceText), []);
});
test('uses class property roles for setup and lifecycle function expressions', () => {
const sourceText = `
class ComponentFields {
setup =
/** 组件初始化类字段入口。 */
function setupField() {};
static mounted =
/** 组件挂载类字段入口。 */
function mountedField() {};
task =
/** 执行普通类字段任务。 */
function setup() {};
}
`;
const issues = validateSourceText('fixture.ts', sourceText).issues;
assert.deepEqual(
issues.map(({ code, ownerName }) => [code, ownerName]),
[
['forbidden-entry', 'setupField'],
['forbidden-entry', 'mountedField'],
],
);
});
test('rejects JSDoc on forbidden declaration and expression targets', () => {
const fixtures = [
`
@ -213,3 +704,167 @@ function calculateResult() {
beforeTokens,
);
});
test('preserves LineTerminator-sensitive return semantics while fixing multiline JSDoc', () => {
const sourceText = `
function readValue() {
return /** English-only
* comment.
*/ 1;
}
`;
const unsafeSourceText = sourceText.replace(
'/** English-only\n * comment.\n */',
'',
);
const beforeTokens = getTokenSignature('fixture.ts', sourceText);
const result = fixSourceText('fixture.ts', sourceText);
const evaluate = (text) => Function(`${text}\nreturn readValue();`)();
assert.notDeepEqual(
getTokenSignature('fixture.ts', unsafeSourceText),
beforeTokens,
);
assert.equal(evaluate(sourceText), undefined);
assert.equal(evaluate(result.sourceText), undefined);
assert.match(result.sourceText, /return[^\S\r\n]*\r?\n[^\S\r\n]*1/u);
assert.deepEqual(
getTokenSignature('fixture.ts', result.sourceText),
beforeTokens,
);
});
test('treats LF and CRLF as the same LineTerminator boundary', () => {
const sourceText = `
function readValue() {
return
\`line one
line two\`;
}
`;
assert.deepEqual(
getTokenSignature('fixture.ts', sourceText.replaceAll('\n', '\r\n')),
getTokenSignature('fixture.ts', sourceText),
);
});
test('staged mode reads additions, modifications, and deletions from the index', () => {
const { repositoryPath, validSource } = createFixtureRepository();
const invalidSource =
'/** Parse the entire incoming payload. */\nfunction parseInput() {}\n';
try {
writeFileSync(path.join(repositoryPath, 'fixture.ts'), invalidSource);
executeGit(repositoryPath, ['add', 'fixture.ts']);
writeFileSync(path.join(repositoryPath, 'fixture.ts'), validSource);
const invalidIndexResult = runChecker(repositoryPath, ['--staged']);
assert.equal(invalidIndexResult.status, 1);
assert.match(invalidIndexResult.stderr, /missing-chinese-description/u);
executeGit(repositoryPath, ['add', 'fixture.ts']);
writeFileSync(path.join(repositoryPath, 'fixture.ts'), invalidSource);
const invalidWorktreeResult = runChecker(repositoryPath, ['--staged']);
assert.equal(invalidWorktreeResult.status, 0);
writeFileSync(path.join(repositoryPath, 'added.ts'), invalidSource);
executeGit(repositoryPath, ['add', 'added.ts']);
writeFileSync(
path.join(repositoryPath, 'added.ts'),
'/** 解析新增输入。 */\nfunction parseAddedInput() {}\n',
);
const invalidAdditionResult = runChecker(repositoryPath, ['--staged']);
assert.equal(invalidAdditionResult.status, 1);
executeGit(repositoryPath, ['add', 'added.ts']);
executeGit(repositoryPath, ['rm', '--quiet', 'deleted.ts']);
writeFileSync(path.join(repositoryPath, 'untracked.ts'), invalidSource);
const deletionAndUntrackedResult = runChecker(repositoryPath, ['--staged']);
assert.equal(deletionAndUntrackedResult.status, 0);
assert.match(deletionAndUntrackedResult.stdout, /"sourceMode": "index"/u);
} finally {
rmSync(repositoryPath, { force: true, recursive: true });
}
});
test('staged mode honors the active GIT_INDEX_FILE used by the commit', () => {
const { repositoryPath, validSource } = createFixtureRepository();
const alternateIndexPath = path.join(repositoryPath, '.git', 'commit-index');
const invalidSource =
'/** Parse the commit index payload. */\nfunction parseInput() {}\n';
const alternateIndexEnvironment = {
GIT_INDEX_FILE: alternateIndexPath,
};
try {
executeGit(repositoryPath, ['read-tree', 'HEAD'], {
gitEnvironment: alternateIndexEnvironment,
});
writeFileSync(path.join(repositoryPath, 'fixture.ts'), invalidSource);
executeGit(repositoryPath, ['add', 'fixture.ts'], {
gitEnvironment: alternateIndexEnvironment,
});
writeFileSync(path.join(repositoryPath, 'fixture.ts'), validSource);
assert.equal(runChecker(repositoryPath, ['--staged']).status, 0);
const commitIndexResult = runChecker(repositoryPath, ['--staged'], {
gitEnvironment: alternateIndexEnvironment,
});
assert.equal(commitIndexResult.status, 1);
assert.match(commitIndexResult.stderr, /missing-chinese-description/u);
} finally {
rmSync(repositoryPath, { force: true, recursive: true });
}
});
test('fixture Git commands isolate hostile inherited GIT variables from the parent repository', () => {
const parentRepositoryPath = executeGit(process.cwd(), [
'rev-parse',
'--show-toplevel',
]).trim();
const parentGitDirectory = executeGit(parentRepositoryPath, [
'rev-parse',
'--absolute-git-dir',
]).trim();
const snapshotParent = () => ({
config: readFileSync(path.join(parentGitDirectory, 'config'), 'utf8'),
head: executeGit(parentRepositoryPath, ['rev-parse', 'HEAD']).trim(),
headReference: readFileSync(path.join(parentGitDirectory, 'HEAD'), 'utf8'),
index: readFileSync(path.join(parentGitDirectory, 'index')).toString(
'base64',
),
status: executeGit(parentRepositoryPath, [
'status',
'--porcelain=v2',
'-z',
]),
});
const before = snapshotParent();
const hostileEnvironment = {
...process.env,
GIT_DIR: parentGitDirectory,
GIT_INDEX_FILE: path.join(parentGitDirectory, 'index'),
GIT_WORK_TREE: parentRepositoryPath,
};
const { repositoryPath } = createFixtureRepository(hostileEnvironment);
try {
assert.deepEqual(snapshotParent(), before);
} finally {
rmSync(repositoryPath, { force: true, recursive: true });
}
});
test('pre-commit invokes the JSDoc checker in staged mode', () => {
const hookSource = readFileSync(HOOK_CHECK_PATH, 'utf8');
assert.match(
hookSource,
/run\(getPnpmCommand\(\), \['run', 'check:jsdoc', '--', '--staged'\]\)/u,
);
});

View File

@ -47,9 +47,6 @@ export class AdminDept {
})
updateTime: KtDateTime;
/**
* Admin
*/
@BeforeInsert()
createId() {
ensureSnowflakeId(this);

View File

@ -99,9 +99,6 @@ export class AdminMenu {
@ManyToMany(() => AdminRole, (role) => role.menus)
roles: AdminRole[];
/**
* Admin
*/
@BeforeInsert()
createId() {
ensureSnowflakeId(this);

View File

@ -74,9 +74,6 @@ export class AdminRole {
@ManyToMany(() => AdminUser, (user) => user.roles)
users: AdminUser[];
/**
* Admin
*/
@BeforeInsert()
createId() {
ensureSnowflakeId(this);

View File

@ -107,9 +107,6 @@ export class AdminUser {
})
dept?: AdminDept | null;
/**
* Admin
*/
@BeforeInsert()
createId() {
ensureSnowflakeId(this);

View File

@ -83,18 +83,12 @@ export class Component {
})
is_deleted: boolean;
/**
* Admin
*/
@AfterLoad()
decodeDictKeys() {
// 查询结果初始化完成后再翻译,避免构造/赋值阶段覆盖派生字段。
decodeDictKeys(this);
}
/**
* Admin
*/
@BeforeInsert()
createId() {
ensureSnowflakeId(this);

View File

@ -72,9 +72,6 @@ export class AdminDict {
})
updateTime: KtDateTime;
/**
* Admin
*/
@BeforeInsert()
createId() {
ensureSnowflakeId(this);

View File

@ -181,9 +181,6 @@ export class AdminNotice {
})
lastSeenAt?: KtDateTime;
/**
* Admin
*/
@BeforeInsert()
createId() {
ensureSnowflakeId(this);

View File

@ -147,9 +147,6 @@ export class BlogArticle {
@ApiPropertyOptional()
excerptText?: string;
/**
*
*/
@BeforeInsert()
createId() {
ensureSnowflakeId(this);

View File

@ -69,9 +69,6 @@ export class BlogTerm {
@ApiPropertyOptional()
parent?: string;
/**
*
*/
@BeforeInsert()
createId() {
ensureSnowflakeId(this);

View File

@ -37,9 +37,6 @@ export class QqbotAccountAbility {
@KtUpdateDateColumn({ name: 'update_time' })
updateTime: KtDateTime;
/**
* QQBot
*/
@BeforeInsert()
createId() {
ensureSnowflakeId(this);

View File

@ -99,9 +99,6 @@ export class QqbotAccount {
@KtUpdateDateColumn({ name: 'update_time' })
updateTime: KtDateTime;
/**
* QQBot
*/
@BeforeInsert()
createId() {
ensureSnowflakeId(this);

View File

@ -63,9 +63,6 @@ export class QqbotCommandLog {
@KtUpdateDateColumn({ name: 'update_time' })
updateTime: KtDateTime;
/**
* QQBot
*/
@BeforeInsert()
createId() {
ensureSnowflakeId(this);

View File

@ -93,9 +93,6 @@ export class QqbotCommand {
@KtUpdateDateColumn({ name: 'update_time' })
updateTime: KtDateTime;
/**
* QQBot
*/
@BeforeInsert()
createId() {
ensureSnowflakeId(this);

View File

@ -26,9 +26,6 @@ export class QqbotConfig {
@KtUpdateDateColumn({ name: 'update_time' })
updateTime: KtDateTime;
/**
* QQBot
*/
@BeforeInsert()
createId() {
ensureSnowflakeId(this);

View File

@ -28,9 +28,6 @@ export class QqbotDedupe {
@KtUpdateDateColumn({ name: 'update_time' })
updateTime: KtDateTime;
/**
* QQBot
*/
@BeforeInsert()
createId() {
ensureSnowflakeId(this);

View File

@ -57,9 +57,6 @@ export class QqbotConversation {
@KtUpdateDateColumn({ name: 'update_time' })
updateTime: KtDateTime;
/**
* QQBot
*/
@BeforeInsert()
createId() {
ensureSnowflakeId(this);

View File

@ -67,9 +67,6 @@ export class QqbotMessage {
@KtUpdateDateColumn({ name: 'update_time' })
updateTime: KtDateTime;
/**
* QQBot
*/
@BeforeInsert()
createId() {
ensureSnowflakeId(this);

View File

@ -42,9 +42,6 @@ export class QqbotAllowlist {
@KtUpdateDateColumn({ name: 'update_time' })
updateTime: KtDateTime;
/**
* QQBot
*/
@BeforeInsert()
createId() {
ensureSnowflakeId(this);

View File

@ -42,9 +42,6 @@ export class QqbotBlocklist {
@KtUpdateDateColumn({ name: 'update_time' })
updateTime: KtDateTime;
/**
* QQBot
*/
@BeforeInsert()
createId() {
ensureSnowflakeId(this);

View File

@ -60,9 +60,6 @@ export class QqbotRule {
@KtUpdateDateColumn({ name: 'update_time' })
updateTime: KtDateTime;
/**
* QQBot
*/
@BeforeInsert()
createId() {
ensureSnowflakeId(this);

View File

@ -55,9 +55,6 @@ export class QqbotSendLog {
@KtUpdateDateColumn({ name: 'update_time' })
updateTime: KtDateTime;
/**
* QQBot
*/
@BeforeInsert()
createId() {
ensureSnowflakeId(this);

View File

@ -2195,11 +2195,6 @@ export class QqbotNapcatLoginService {
}
}
/**
* NapCat
* @param container - container 使 `id`
* @param options - Cleanup switches; create-login cleanup may revisit already-deleted provisional rows after async startup races.
*/
private async cleanupRuntimeContainer(
container: QqbotNapcatRuntime,
options: { includeDeletedCreateContainer?: boolean } = {},

View File

@ -15,11 +15,6 @@ export type NapcatDockerDeviceOptions = {
runFlags: string[];
};
/**
* NapCat
* @param identity - Persisted device identity row that supplies stable directory, hostname, machine-id, and MAC values for Docker.
* @returns Docker option bundle used by remote create scripts.
*/
export function toNapcatDockerDeviceOptions(
identity: Pick<
NapcatDeviceIdentity,

View File

@ -55,9 +55,6 @@ export class NapcatAccountBinding {
@KtUpdateDateColumn({ name: 'update_time' })
updateTime: KtDateTime;
/**
* NapCat
*/
@BeforeInsert()
createId() {
ensureSnowflakeId(this);

View File

@ -80,9 +80,6 @@ export class NapcatContainer {
@KtUpdateDateColumn({ name: 'update_time' })
updateTime: KtDateTime;
/**
* NapCat
*/
@BeforeInsert()
createId() {
ensureSnowflakeId(this);

View File

@ -65,9 +65,6 @@ export class NapcatDeviceIdentity {
@KtUpdateDateColumn({ name: 'update_time' })
updateTime: KtDateTime;
/**
* NapCat
*/
@BeforeInsert()
createId() {
ensureSnowflakeId(this);

View File

@ -54,9 +54,6 @@ export class NapcatLoginChallengeEntity {
@KtUpdateDateColumn({ name: 'update_time' })
updateTime: KtDateTime;
/**
* NapCat
*/
@BeforeInsert()
createId() {
ensureSnowflakeId(this);

View File

@ -63,9 +63,6 @@ export class NapcatLoginSession {
@KtUpdateDateColumn({ name: 'update_time' })
updateTime: KtDateTime;
/**
* NapCat
*/
@BeforeInsert()
createId() {
ensureSnowflakeId(this);

View File

@ -37,9 +37,6 @@ export class NapcatRuntimeCleanup {
@KtUpdateDateColumn({ name: 'update_time' })
updateTime: KtDateTime;
/**
* NapCat
*/
@BeforeInsert()
createId() {
ensureSnowflakeId(this);

View File

@ -65,9 +65,6 @@ export class QqbotPlugin {
@KtUpdateDateColumn({ name: 'update_time' })
updateTime: KtDateTime;
/**
* QQBot
*/
@BeforeInsert()
createId() {
ensureSnowflakeId(this);
@ -95,9 +92,6 @@ export class QqbotPluginVersion {
@KtCreateDateColumn({ name: 'create_time' })
createTime: KtDateTime;
/**
* QQBot
*/
@BeforeInsert()
createId() {
ensureSnowflakeId(this);
@ -131,9 +125,6 @@ export class QqbotPluginInstallation {
@KtUpdateDateColumn({ name: 'update_time' })
updateTime: KtDateTime;
/**
* QQBot
*/
@BeforeInsert()
createId() {
ensureSnowflakeId(this);
@ -166,9 +157,6 @@ export class QqbotPluginOperation {
@KtCreateDateColumn({ name: 'create_time' })
createTime: KtDateTime;
/**
* QQBot
*/
@BeforeInsert()
createId() {
ensureSnowflakeId(this);
@ -198,9 +186,6 @@ export class QqbotPluginEventHandler {
@KtCreateDateColumn({ name: 'create_time' })
createTime: KtDateTime;
/**
* QQBot
*/
@BeforeInsert()
createId() {
ensureSnowflakeId(this);
@ -227,9 +212,6 @@ export class QqbotPluginAccountBinding {
@KtCreateDateColumn({ name: 'create_time' })
createTime: KtDateTime;
/**
* QQBot
*/
@BeforeInsert()
createId() {
ensureSnowflakeId(this);
@ -257,9 +239,6 @@ export class QqbotPluginConfig {
@KtUpdateDateColumn({ name: 'update_time' })
updateTime: KtDateTime;
/**
* QQBot
*/
@BeforeInsert()
createId() {
ensureSnowflakeId(this);
@ -287,9 +266,6 @@ export class QqbotPluginAsset {
@KtCreateDateColumn({ name: 'create_time' })
createTime: KtDateTime;
/**
* QQBot
*/
@BeforeInsert()
createId() {
ensureSnowflakeId(this);
@ -325,9 +301,6 @@ export class QqbotPluginRuntimeEvent {
@KtCreateDateColumn({ name: 'create_time' })
createTime: KtDateTime;
/**
* QQBot
*/
@BeforeInsert()
createId() {
ensureSnowflakeId(this);
@ -402,9 +375,6 @@ export class QqbotPluginTask {
@KtUpdateDateColumn({ name: 'update_time' })
updateTime: KtDateTime;
/**
* QQBot
*/
@BeforeInsert()
createId() {
ensureSnowflakeId(this);
@ -458,9 +428,6 @@ export class QqbotPluginTaskRun {
@KtCreateDateColumn({ name: 'create_time' })
createTime: KtDateTime;
/**
* QQBot
*/
@BeforeInsert()
createId() {
ensureSnowflakeId(this);

View File

@ -43,11 +43,6 @@ function buildCanonicalBilibiliVideoUrl(video: BilibiliVideoInfo) {
return `https://www.bilibili.com/video/${videoId}`;
}
/**
* Formats Bilibili stat counters using compact Chinese units.
* @param value - Raw counter from the Bilibili video API response.
* @returns Counter text such as `7890` or `12.3万`.
*/
function formatBilibiliStat(value: number) {
const normalized = Math.max(0, Math.floor(value || 0));
if (normalized < 10000) return `${normalized}`;

View File

@ -248,11 +248,6 @@ export class FflogsClient {
return (value: string) => resolved.get(value) || null;
}
/**
* Known World
* @param value - FF14 server, region, data-center, or path token parsed from FFLogs command text.
* @returns Legacy host resolution when available, otherwise package-owned dictionary fallback for generic workers.
*/
async resolveKnownWorld(value: string) {
if (this.host.resolveKnownWorld) {
return this.host.resolveKnownWorld(value);

View File

@ -8,10 +8,6 @@ import type { RuntimeHealthReport } from './runtime-health.types';
export class RuntimeHealthController {
constructor(private readonly runtimeHealthService: RuntimeHealthService) {}
/**
* Get machine-readable API runtime health
* @returns
*/
@Get('runtime')
@ApiOperation({ summary: 'Get machine-readable API runtime health' })
getRuntimeHealth(): RuntimeHealthReport {