统一 Rust 与 TypeScript 格式化门禁

纳入 AGC Cargo workspace 的统一 rustfmt 检查与格式化入口

完成项目 TypeScript/Prettier 与 Rust 全量格式化

修复 Pingora expected executable 门禁的空白敏感误报

同步开发运维文档与 AGC skill pack 格式化忽略规则
This commit is contained in:
2026-09-01 16:26:20 +08:00
parent ae79d50b14
commit 071faa482c
1051 changed files with 28412 additions and 19834 deletions
@@ -120,25 +120,15 @@
"visual_novel_work_profile": []
},
"profileIds": {
"puzzle": [
"profile-001",
"profile-002",
"profile-003"
],
"customWorld": [
"profile-081"
],
"puzzle": ["profile-001", "profile-002", "profile-003"],
"customWorld": ["profile-081"],
"match3d": [],
"squareHole": [],
"bigFish": [],
"visualNovel": []
},
"workIds": {
"puzzle": [
"work-001",
"work-002",
"work-003"
],
"puzzle": ["work-001", "work-002", "work-003"],
"customWorld": [],
"match3d": [],
"squareHole": [],
+3 -13
View File
@@ -120,25 +120,15 @@
"visual_novel_work_profile": []
},
"profileIds": {
"puzzle": [
"profile-001",
"profile-002",
"profile-003"
],
"customWorld": [
"profile-081"
],
"puzzle": ["profile-001", "profile-002", "profile-003"],
"customWorld": ["profile-081"],
"match3d": [],
"squareHole": [],
"bigFish": [],
"visualNovel": []
},
"workIds": {
"puzzle": [
"work-001",
"work-002",
"work-003"
],
"puzzle": ["work-001", "work-002", "work-003"],
"customWorld": [],
"match3d": [],
"squareHole": [],
+83 -25
View File
@@ -30,10 +30,18 @@ const TABLE_OUTPUT_ORDER = [
'visual_novel_work_profile',
];
const WORK_TYPES = ['puzzle', 'customWorld', 'match3d', 'squareHole', 'bigFish', 'visualNovel'];
const WORK_TYPES = [
'puzzle',
'customWorld',
'match3d',
'squareHole',
'bigFish',
'visualNovel',
];
const SHORT_TEXT_LIMIT = 120;
const LONG_TEXT_LIMIT = 500;
const SENSITIVE_PATTERN = /(token|secret|password|passwd|phone|wallet|credential|authorization|auth[_-]?key|api[_-]?key)/giu;
const SENSITIVE_PATTERN =
/(token|secret|password|passwd|phone|wallet|credential|authorization|auth[_-]?key|api[_-]?key)/giu;
class StableMapper {
constructor(prefix) {
@@ -98,11 +106,13 @@ function redactSensitiveText(value) {
function sanitizeCoverImageSrc(value) {
const unwrapped = unwrapSpacetimeOption(value);
if (unwrapped === undefined || unwrapped === null || unwrapped === '') return unwrapped;
if (unwrapped === undefined || unwrapped === null || unwrapped === '')
return unwrapped;
const text = String(unwrapped);
if (text.startsWith('data:image/')) return '[redacted-data-image]';
let withoutQuery = text.split('?')[0].split('#')[0];
if (withoutQuery.length > 180) withoutQuery = `${withoutQuery.slice(0, 180)}…`;
if (withoutQuery.length > 180)
withoutQuery = `${withoutQuery.slice(0, 180)}…`;
return withoutQuery;
}
@@ -113,9 +123,15 @@ function sanitizeLargeJson(value) {
return truncateText(redactSensitiveText(unwrapped), LONG_TEXT_LIMIT);
}
try {
return truncateText(redactSensitiveText(JSON.stringify(unwrapped)), LONG_TEXT_LIMIT);
return truncateText(
redactSensitiveText(JSON.stringify(unwrapped)),
LONG_TEXT_LIMIT,
);
} catch {
return truncateText(redactSensitiveText(String(unwrapped)), LONG_TEXT_LIMIT);
return truncateText(
redactSensitiveText(String(unwrapped)),
LONG_TEXT_LIMIT,
);
}
}
@@ -128,27 +144,42 @@ function firstDefined(row, keys) {
function sanitizeShortField(row, sanitized, key) {
if (row[key] !== undefined) {
sanitized[key] = truncateText(unwrapSpacetimeOption(row[key]), SHORT_TEXT_LIMIT);
sanitized[key] = truncateText(
unwrapSpacetimeOption(row[key]),
SHORT_TEXT_LIMIT,
);
}
}
function sanitizeWorkRow(row, ctx) {
const sanitized = {};
const profileId = unwrapSpacetimeOption(firstDefined(row, ['profile_id', 'profileId']));
const workId = unwrapSpacetimeOption(firstDefined(row, ['work_id', 'workId']));
const profileId = unwrapSpacetimeOption(
firstDefined(row, ['profile_id', 'profileId']),
);
const workId = unwrapSpacetimeOption(
firstDefined(row, ['work_id', 'workId']),
);
if (profileId !== undefined) sanitized.profile_id = ctx.profile.map(profileId);
if (profileId !== undefined)
sanitized.profile_id = ctx.profile.map(profileId);
if (workId !== undefined) sanitized.work_id = ctx.work.map(workId);
if (row.owner_user_id !== undefined) {
sanitized.owner_user_id = ctx.user.map(unwrapSpacetimeOption(row.owner_user_id));
sanitized.owner_user_id = ctx.user.map(
unwrapSpacetimeOption(row.owner_user_id),
);
}
if (row.user_id !== undefined) sanitized.user_id = ctx.user.map(unwrapSpacetimeOption(row.user_id));
if (row.user_id !== undefined)
sanitized.user_id = ctx.user.map(unwrapSpacetimeOption(row.user_id));
if (row.author_display_name !== undefined) {
sanitized.author_display_name = ctx.author.map(unwrapSpacetimeOption(row.author_display_name));
sanitized.author_display_name = ctx.author.map(
unwrapSpacetimeOption(row.author_display_name),
);
}
if (row.public_work_code !== undefined) {
sanitized.public_work_code = ctx.publicWorkCode.map(unwrapSpacetimeOption(row.public_work_code));
sanitized.public_work_code = ctx.publicWorkCode.map(
unwrapSpacetimeOption(row.public_work_code),
);
}
if (row.author_public_user_code !== undefined) {
sanitized.author_public_user_code = ctx.authorCode.map(
@@ -156,9 +187,12 @@ function sanitizeWorkRow(row, ctx) {
);
}
if (row.cover_asset_id !== undefined) {
sanitized.cover_asset_id = ctx.coverAsset.map(unwrapSpacetimeOption(row.cover_asset_id));
sanitized.cover_asset_id = ctx.coverAsset.map(
unwrapSpacetimeOption(row.cover_asset_id),
);
}
if (row.cover_image_src !== undefined) sanitized.cover_image_src = sanitizeCoverImageSrc(row.cover_image_src);
if (row.cover_image_src !== undefined)
sanitized.cover_image_src = sanitizeCoverImageSrc(row.cover_image_src);
for (const key of [
'title',
@@ -174,7 +208,12 @@ function sanitizeWorkRow(row, ctx) {
sanitizeShortField(row, sanitized, key);
}
for (const key of ['levels_json', 'profile_payload_json', 'anchor_pack_json', 'theme_tags_json']) {
for (const key of [
'levels_json',
'profile_payload_json',
'anchor_pack_json',
'theme_tags_json',
]) {
if (row[key] !== undefined) sanitized[key] = sanitizeLargeJson(row[key]);
}
@@ -196,7 +235,8 @@ function sanitizeWorkRow(row, ctx) {
'tags',
];
for (const key of passthroughKeys) {
if (row[key] !== undefined) sanitized[key] = unwrapSpacetimeOption(row[key]);
if (row[key] !== undefined)
sanitized[key] = unwrapSpacetimeOption(row[key]);
}
return sanitized;
@@ -211,8 +251,14 @@ function normalizeWork(tableName, row) {
ownerUserId: row.owner_user_id,
publicWorkCode: row.public_work_code,
title: row.title ?? row.work_title ?? row.level_name ?? row.world_name,
subtitle: row.subtitle ?? row.summary_text ?? row.summary ?? row.work_description ?? row.description,
publicationStatus: row.publicationStatus ?? row.publication_status ?? row.status,
subtitle:
row.subtitle ??
row.summary_text ??
row.summary ??
row.work_description ??
row.description,
publicationStatus:
row.publicationStatus ?? row.publication_status ?? row.status,
playCount: row.playCount ?? row.play_count ?? 0,
likeCount: row.likeCount ?? row.like_count ?? 0,
remixCount: row.remixCount ?? row.remix_count ?? 0,
@@ -299,7 +345,11 @@ function createSampleOutput(output, maxRowsPerTable = 3) {
]),
);
const normalizedWorks = output.normalizedWorks
.filter((work) => allowedWorkIds.has(work.workId) || allowedProfileIds.has(work.profileId))
.filter(
(work) =>
allowedWorkIds.has(work.workId) ||
allowedProfileIds.has(work.profileId),
)
.slice(0, maxRowsPerTable * 6);
return {
@@ -318,7 +368,8 @@ function parseArgs(argv) {
const arg = argv[index];
if (arg === '--input' || arg === '--output' || arg === '--sample-output') {
const value = argv[index + 1];
if (!value || value.startsWith('--')) throw new Error(`${arg} requires a value`);
if (!value || value.startsWith('--'))
throw new Error(`${arg} requires a value`);
args[arg.slice(2)] = value;
index += 1;
} else if (arg === '--help' || arg === '-h') {
@@ -345,12 +396,18 @@ export async function runCli(argv = process.argv.slice(2)) {
const raw = await readFile(args.input, 'utf8');
const migration = JSON.parse(raw);
const output = extractWorksListData(migration, { source: basename(args.input) });
const output = extractWorksListData(migration, {
source: basename(args.input),
});
await writeFile(args.output, `${JSON.stringify(output, null, 2)}\n`, 'utf8');
if (args['sample-output']) {
const sample = createSampleOutput(output);
await writeFile(args['sample-output'], `${JSON.stringify(sample, null, 2)}\n`, 'utf8');
await writeFile(
args['sample-output'],
`${JSON.stringify(sample, null, 2)}\n`,
'utf8',
);
}
console.log(
@@ -361,7 +418,8 @@ export async function runCli(argv = process.argv.slice(2)) {
}
}
const isDirectRun = process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1];
const isDirectRun =
process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1];
if (isDirectRun) {
runCli().catch((error) => {
console.error(error instanceof Error ? error.message : String(error));
@@ -10,7 +10,9 @@ import { describe, expect, it } from 'vitest';
import { extractWorksListData } from './extract-works-list-data.mjs';
const execFileAsync = promisify(execFile);
const scriptPath = fileURLToPath(new URL('./extract-works-list-data.mjs', import.meta.url));
const scriptPath = fileURLToPath(
new URL('./extract-works-list-data.mjs', import.meta.url),
);
const fixtureMigration = {
schema_version: 7,
@@ -32,8 +34,13 @@ const fixtureMigration = {
play_count: 42,
like_count: 7,
cover_asset_id: { some: 'asset-secret-cover' },
cover_image_src: { some: 'https://cdn.example.test/cover.png?token=***&sig=abc' },
levels_json: JSON.stringify({ secret: 'level-token-value', data: 'x'.repeat(2000) }),
cover_image_src: {
some: 'https://cdn.example.test/cover.png?token=***&sig=abc',
},
levels_json: JSON.stringify({
secret: 'level-token-value',
data: 'x'.repeat(2000),
}),
theme_tags_json: JSON.stringify(['化学家', '实验室']),
remix_count: 2,
updated_at: '2026-05-01T00:00:00Z',
@@ -88,7 +95,12 @@ const fixtureMigration = {
},
{
name: 'refresh_session',
rows: [{ token: 'refresh-token-secret', source_session_id: 'session-secret-789' }],
rows: [
{
token: 'refresh-token-secret',
source_session_id: 'session-secret-789',
},
],
},
{
name: 'profile_wallet_ledger',
@@ -108,7 +120,9 @@ async function withTempDir(fn) {
describe('extractWorksListData', () => {
it('只保留作品 profile 白名单表,禁用的行为/敏感表不会出现在输出 JSON 字符串中', () => {
const output = extractWorksListData(fixtureMigration, { source: 'fixture.local.json' });
const output = extractWorksListData(fixtureMigration, {
source: 'fixture.local.json',
});
const serialized = JSON.stringify(output);
expect(Object.keys(output.tables).sort()).toEqual([
@@ -125,7 +139,9 @@ describe('extractWorksListData', () => {
});
it('不会输出 owner/user/session/auth/token/phone/wallet 等敏感原值,owner 稳定映射', () => {
const output = extractWorksListData(fixtureMigration, { source: 'fixture.local.json' });
const output = extractWorksListData(fixtureMigration, {
source: 'fixture.local.json',
});
const serialized = JSON.stringify(output);
for (const secret of [
@@ -143,12 +159,16 @@ describe('extractWorksListData', () => {
expect(output.tables.puzzle_work_profile[0].owner_user_id).toBe('user-001');
expect(output.tables.puzzle_work_profile[1].owner_user_id).toBe('user-001');
expect(output.tables.puzzle_work_profile[0].author_display_name).toBe('author-001');
expect(output.tables.puzzle_work_profile[0].author_display_name).toBe(
'author-001',
);
expect(serialized).not.toContain('level-token-value');
});
it('puzzle 数据生成 profileIds/workIds 和 normalizedWorks,并保留列表展示字段', () => {
const output = extractWorksListData(fixtureMigration, { source: 'fixture.local.json' });
const output = extractWorksListData(fixtureMigration, {
source: 'fixture.local.json',
});
expect(output.source).toBe('fixture.local.json');
expect(output.generatedAt).toEqual(expect.any(String));
@@ -168,8 +188,12 @@ describe('extractWorksListData', () => {
}),
]),
);
expect(output.tables.puzzle_work_profile[0].cover_image_src).toBe('https://cdn.example.test/cover.png');
expect(output.tables.puzzle_work_profile[0].theme_tags_json).toBe('["化学家","实验室"]');
expect(output.tables.puzzle_work_profile[0].cover_image_src).toBe(
'https://cdn.example.test/cover.png',
);
expect(output.tables.puzzle_work_profile[0].theme_tags_json).toBe(
'["化学家","实验室"]',
);
});
it('data image、URL token 和绝对输入路径不会泄露到输出', async () => {
@@ -186,8 +210,13 @@ describe('extractWorksListData', () => {
{
profile_id: 'profile-real',
work_id: 'work-real',
cover_image_src: { some: 'data:image/png;base64,SECRET_IMAGE_BYTES' },
levels_json: JSON.stringify({ token: 'SECRET_TOKEN_VALUE', title: 'safe' }),
cover_image_src: {
some: 'data:image/png;base64,SECRET_IMAGE_BYTES',
},
levels_json: JSON.stringify({
token: 'SECRET_TOKEN_VALUE',
title: 'safe',
}),
},
],
},
@@ -196,7 +225,13 @@ describe('extractWorksListData', () => {
'utf8',
);
await execFileAsync(process.execPath, [scriptPath, '--input', input, '--output', output]);
await execFileAsync(process.execPath, [
scriptPath,
'--input',
input,
'--output',
output,
]);
const extracted = JSON.parse(await readFile(output, 'utf8'));
const serialized = JSON.stringify(extracted);
@@ -204,7 +239,9 @@ describe('extractWorksListData', () => {
expect(serialized).not.toContain(dir);
expect(serialized).not.toContain('SECRET_IMAGE_BYTES');
expect(serialized).not.toContain('SECRET_TOKEN_VALUE');
expect(extracted.tables.puzzle_work_profile[0].cover_image_src).toBe('[redacted-data-image]');
expect(extracted.tables.puzzle_work_profile[0].cover_image_src).toBe(
'[redacted-data-image]',
);
});
});
@@ -223,11 +260,21 @@ describe('extractWorksListData', () => {
}));
await writeFile(
input,
JSON.stringify({ tables: [{ name: 'puzzle_work_profile', rows: manyRows }] }),
JSON.stringify({
tables: [{ name: 'puzzle_work_profile', rows: manyRows }],
}),
'utf8',
);
await execFileAsync(process.execPath, [scriptPath, '--input', input, '--output', output, '--sample-output', sampleOutput]);
await execFileAsync(process.execPath, [
scriptPath,
'--input',
input,
'--output',
output,
'--sample-output',
sampleOutput,
]);
const sample = JSON.parse(await readFile(sampleOutput, 'utf8'));
const serialized = JSON.stringify(sample);
@@ -239,7 +286,9 @@ describe('extractWorksListData', () => {
});
it('CLI 参数缺失时退出非 0 并输出清晰错误', async () => {
await expect(execFileAsync(process.execPath, [scriptPath, '--input', 'missing.json'])).rejects.toMatchObject({
await expect(
execFileAsync(process.execPath, [scriptPath, '--input', 'missing.json']),
).rejects.toMatchObject({
code: 1,
stderr: expect.stringContaining('--output'),
});
+61 -16
View File
@@ -7,7 +7,10 @@ import { Rate, Trend } from 'k6/metrics';
// k6 resolves open() paths relative to this script file, not the shell cwd.
const DEFAULT_WORKS_DATA = 'data/works-list.local.json';
const WORKS_DATA = __ENV.WORKS_DATA || DEFAULT_WORKS_DATA;
const BASE_URL = (__ENV.BASE_URL || 'http://127.0.0.1:8787').replace(/\/+$/u, '');
const BASE_URL = (__ENV.BASE_URL || 'http://127.0.0.1:8787').replace(
/\/+$/u,
'',
);
const AUTH_TOKEN = __ENV.AUTH_TOKEN || '';
const SCENARIO = __ENV.SCENARIO || 'smoke';
const REQUEST_TIMEOUT = __ENV.REQUEST_TIMEOUT || '30s';
@@ -20,8 +23,12 @@ const worksDetailShapeErrorRate = new Rate('works_detail_shape_error_rate');
const worksListDuration = new Trend('works_list_duration');
const worksDetailDuration = new Trend('works_detail_duration');
const data = new SharedArray('works-list-data', () => [JSON.parse(open(WORKS_DATA))])[0];
const normalizedWorks = Array.isArray(data.normalizedWorks) ? data.normalizedWorks : [];
const data = new SharedArray('works-list-data', () => [
JSON.parse(open(WORKS_DATA)),
])[0];
const normalizedWorks = Array.isArray(data.normalizedWorks)
? data.normalizedWorks
: [];
const scenarioOptions = {
smoke: {
@@ -61,9 +68,18 @@ const scenarioOptions = {
maxVUs: Number(__ENV.MAX_VUS || 200),
timeUnit: '1s',
stages: [
{ target: Number(__ENV.PEAK_RPS || 25), duration: __ENV.RAMP_UP || '30s' },
{ target: Number(__ENV.PEAK_RPS || 25), duration: __ENV.HOLD || '2m' },
{ target: Number(__ENV.END_RPS || 5), duration: __ENV.RAMP_DOWN || '30s' },
{
target: Number(__ENV.PEAK_RPS || 25),
duration: __ENV.RAMP_UP || '30s',
},
{
target: Number(__ENV.PEAK_RPS || 25),
duration: __ENV.HOLD || '2m',
},
{
target: Number(__ENV.END_RPS || 5),
duration: __ENV.RAMP_DOWN || '30s',
},
],
},
},
@@ -153,10 +169,19 @@ function hasListItemShape(payload, keys) {
const item = collection[0];
const hasId = Boolean(
item &&
(item.profileId || item.profile_id || item.workId || item.work_id || item.publicWorkCode),
(item.profileId ||
item.profile_id ||
item.workId ||
item.work_id ||
item.publicWorkCode),
);
const hasTitle = Boolean(
item && (item.title || item.workTitle || item.work_title || item.levelName || item.worldName),
item &&
(item.title ||
item.workTitle ||
item.work_title ||
item.levelName ||
item.worldName),
);
return hasId && hasTitle;
}
@@ -167,7 +192,9 @@ function randomItem(items) {
}
function listEndpoints() {
return AUTH_TOKEN ? PUBLIC_ENDPOINTS.concat(AUTH_ENDPOINTS) : PUBLIC_ENDPOINTS;
return AUTH_TOKEN
? PUBLIC_ENDPOINTS.concat(AUTH_ENDPOINTS)
: PUBLIC_ENDPOINTS;
}
function detailEndpointFor(work) {
@@ -191,15 +218,22 @@ function detailEndpointFor(work) {
function performListRequest(endpoint) {
const url = buildUrl(endpoint.path);
const response = http.request(endpoint.method, url, null, requestParams(endpoint.name));
const response = http.request(
endpoint.method,
url,
null,
requestParams(endpoint.name),
);
worksListDuration.add(response.timings.duration, { endpoint: endpoint.name });
const json = parseJson(response);
const payload = unwrapPayload(json);
const ok = check(response, {
[`${endpoint.name} status is 200`]: (res) => res.status === 200,
[`${endpoint.name} returns json object`]: () => Boolean(payload),
[`${endpoint.name} has collection`]: () => hasCollection(payload, endpoint.expectCollectionKeys),
[`${endpoint.name} list item shape`]: () => hasListItemShape(payload, endpoint.expectCollectionKeys),
[`${endpoint.name} has collection`]: () =>
hasCollection(payload, endpoint.expectCollectionKeys),
[`${endpoint.name} list item shape`]: () =>
hasListItemShape(payload, endpoint.expectCollectionKeys),
});
worksListShapeErrorRate.add(!ok, { endpoint: endpoint.name });
}
@@ -208,8 +242,13 @@ function performDetailRequest() {
const endpoint = detailEndpointFor(randomItem(normalizedWorks));
if (!endpoint) return;
const response = http.get(buildUrl(endpoint.path), requestParams(endpoint.name));
worksDetailDuration.add(response.timings.duration, { endpoint: endpoint.name });
const response = http.get(
buildUrl(endpoint.path),
requestParams(endpoint.name),
);
worksDetailDuration.add(response.timings.duration, {
endpoint: endpoint.name,
});
const json = parseJson(response);
const payload = unwrapPayload(json);
const ok = check(response, {
@@ -224,10 +263,16 @@ export default function () {
for (const endpoint of listEndpoints()) {
performListRequest(endpoint);
}
if (normalizedWorks.length && DETAIL_RATIO > 0 && Math.random() < DETAIL_RATIO) {
if (
normalizedWorks.length &&
DETAIL_RATIO > 0 &&
Math.random() < DETAIL_RATIO
) {
performDetailRequest();
}
const jitter = SLEEP_MIN_SECONDS + Math.random() * Math.max(0, SLEEP_MAX_SECONDS - SLEEP_MIN_SECONDS);
const jitter =
SLEEP_MIN_SECONDS +
Math.random() * Math.max(0, SLEEP_MAX_SECONDS - SLEEP_MIN_SECONDS);
sleep(jitter);
}