mirror of
https://github.com/zkldi/Tachi.git
synced 2026-10-02 03:48:14 +03:00
Merge branch 'main' of https://github.com/zkldi/Tachi into fix/arcaea/update-ccs
This commit is contained in:
Executable
+19
@@ -0,0 +1,19 @@
|
||||
#!/usr/bin/env bash
|
||||
# MYT gRPC probe — run inside the tachi-server container.
|
||||
#
|
||||
# kubectl exec -it deploy/tachi-server -- sh /app/docker/myt-grpc-probe.sh \
|
||||
# --access-code "YOUR_ACCESS_CODE" --game chunithm
|
||||
#
|
||||
# Or paste the block from the bottom of this file if the script is not on the image yet.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
TACHI_ROOT="${TACHI_ROOT:-/app}"
|
||||
cd "${TACHI_ROOT}/typescript/server"
|
||||
exec bun run src/scripts/myt-grpc-probe.ts -- "$@"
|
||||
|
||||
# --- paste into `kubectl exec ... -- sh` if this file is not on the image yet ---
|
||||
# set -euo pipefail
|
||||
# cd /app/typescript/server
|
||||
# bun run src/scripts/myt-grpc-probe.ts -- --access-code "YOUR_ACCESS_CODE" --game chunithm
|
||||
# bun run src/scripts/myt-grpc-probe.ts -- --profile-api-id "TITLE_API_ID" --game chunithm --max-items 1
|
||||
@@ -48,7 +48,7 @@ TACHI_CDN_SAVE_LOCATION_PRIVATE_BUCKET=tachi-private
|
||||
TACHI_CDN_SAVE_LOCATION_PRIVATE_REGION=us-east-1
|
||||
TACHI_NAME=Tachi Local Dev
|
||||
TACHI_TYPE=omni
|
||||
TACHI_GAME_GROUPS=iidx,museca,sdvx,bms,chunithm,usc,wacca,popn,jubeat,pms,maimai,maimaidx,arcaea,ongeki
|
||||
TACHI_GAME_GROUPS=iidx,museca,sdvx,bms,chunithm,usc,wacca,popn,jubeat,pms,maimai,maimaidx,arcaea,ongeki,ddr
|
||||
TACHI_IMPORT_TYPES=file/eamusement-iidx-csv,file/batch-manual,file/solid-state-squad,file/pli-iidx-csv,ir/direct-manual,ir/barbatos,ir/fervidex,ir/fervidex-static,ir/beatoraja,ir/usc,ir/kshook-sv6c,api/eag-iidx,api/eag-sdvx,api/flo-iidx,api/flo-sdvx,api/min-sdvx
|
||||
|
||||
# TACHI_GITHUB_APP_ID=
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import { GetChartByIdForGame } from "#lib/db-formats/chart";
|
||||
import { log } from "#lib/log/log";
|
||||
import {
|
||||
LEGACY_CHART_ID_LENGTH,
|
||||
} from "#lib/score-import/framework/score-importing/score-id";
|
||||
import { LEGACY_CHART_ID_LENGTH } from "#lib/score-import/framework/score-importing/score-id";
|
||||
import DB from "#services/pg/db";
|
||||
import { GetNextBmsPmsSongLegacyId } from "#utils/db";
|
||||
import { Random20Hex } from "#utils/misc";
|
||||
|
||||
@@ -0,0 +1,327 @@
|
||||
/**
|
||||
* MYT gRPC probe: unary Cards.Lookup vs server-streaming GetPlaylog.
|
||||
*
|
||||
* Run inside the server container (same env as prod):
|
||||
*
|
||||
* cd /app/typescript/server
|
||||
* bun run src/scripts/myt-grpc-probe.ts -- --profile-api-id "<title_api_id>" --game chunithm
|
||||
* bun run src/scripts/myt-grpc-probe.ts -- --access-code "..." --game chunithm
|
||||
*
|
||||
* Env (required): TACHI_MYT_API_HOST, TACHI_MYT_AUTH_TOKEN
|
||||
*
|
||||
* Exit 0 only if every requested phase succeeds.
|
||||
*/
|
||||
import { Cards, LookupRequestSchema } from "#proto/generated/cards/cards_pb";
|
||||
import { ChunithmUser, GetPlaylogRequestSchema } from "#proto/generated/chunithm/user_pb";
|
||||
import {
|
||||
GetPlaylogRequestSchema as MaimaiGetPlaylogRequestSchema,
|
||||
MaimaiUser,
|
||||
} from "#proto/generated/maimai/user_pb";
|
||||
import {
|
||||
GetPlaylogRequestSchema as OngekiGetPlaylogRequestSchema,
|
||||
OngekiUser,
|
||||
} from "#proto/generated/ongeki/user_pb";
|
||||
import { PlaylogRequestSchema, WaccaUser } from "#proto/generated/wacca/user_pb";
|
||||
import { create } from "@bufbuild/protobuf";
|
||||
import { Code, ConnectError, createClient, type Transport } from "@connectrpc/connect";
|
||||
import { createGrpcTransport } from "@connectrpc/connect-node";
|
||||
import { spawnSync } from "node:child_process";
|
||||
import { parseArgs } from "node:util";
|
||||
|
||||
type ProbeGame = "chunithm" | "maimaidx" | "ongeki" | "wacca";
|
||||
|
||||
const MYT_TITLE: Record<ProbeGame, string> = {
|
||||
chunithm: "chunithm",
|
||||
maimaidx: "maimai",
|
||||
ongeki: "ongeki",
|
||||
wacca: "wacca",
|
||||
};
|
||||
|
||||
function log(section: string, message: string, extra?: Record<string, unknown>): void {
|
||||
const prefix = `[myt-probe:${section}]`;
|
||||
if (extra === undefined) {
|
||||
console.log(`${prefix} ${message}`);
|
||||
} else {
|
||||
console.log(`${prefix} ${message}`, JSON.stringify(extra, null, 2));
|
||||
}
|
||||
}
|
||||
|
||||
function fail(message: string): never {
|
||||
console.error(`[myt-probe] ERROR: ${message}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
function requireEnv(name: string): string {
|
||||
const value = process.env[name]?.trim();
|
||||
if (!value) {
|
||||
fail(`Missing env ${name}`);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function createTransport(): Transport {
|
||||
const host = requireEnv("TACHI_MYT_API_HOST");
|
||||
const token = requireEnv("TACHI_MYT_AUTH_TOKEN");
|
||||
|
||||
return createGrpcTransport({
|
||||
baseUrl: `https://${host}`,
|
||||
defaultTimeoutMs: 10 * 60 * 1000,
|
||||
interceptors: [
|
||||
(next) => async (req) => {
|
||||
req.header.set("Authorization", `Bearer ${token}`);
|
||||
return next(req);
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
function connectCodeName(code: Code): string {
|
||||
return Code[code] ?? String(code);
|
||||
}
|
||||
|
||||
function formatConnectError(err: ConnectError): Record<string, unknown> {
|
||||
return {
|
||||
message: err.message,
|
||||
rawMessage: err.rawMessage,
|
||||
code: err.code,
|
||||
codeName: connectCodeName(err.code),
|
||||
metadata: Object.fromEntries(err.metadata.entries()),
|
||||
cause:
|
||||
err.cause instanceof Error
|
||||
? { name: err.cause.name, message: err.cause.message, stack: err.cause.stack }
|
||||
: err.cause,
|
||||
};
|
||||
}
|
||||
|
||||
async function probeLookup(
|
||||
transport: Transport,
|
||||
accessCode: string,
|
||||
game: ProbeGame,
|
||||
): Promise<string> {
|
||||
const client = createClient(Cards, transport);
|
||||
const title = MYT_TITLE[game];
|
||||
const started = performance.now();
|
||||
|
||||
log("lookup", `Cards.Lookup titles=[${title}] ...`);
|
||||
|
||||
try {
|
||||
const response = await client.lookup(
|
||||
create(LookupRequestSchema, { accessCode, titles: [title] }),
|
||||
);
|
||||
|
||||
for (const entry of response.titles) {
|
||||
if (entry.titleKind === title) {
|
||||
const ms = Math.round(performance.now() - started);
|
||||
log("lookup", `OK in ${ms}ms`, {
|
||||
playerApiId: response.playerApiId,
|
||||
titleApiId: entry.titleApiId,
|
||||
titleKind: entry.titleKind,
|
||||
});
|
||||
return entry.titleApiId;
|
||||
}
|
||||
}
|
||||
|
||||
fail(`Lookup succeeded but no title "${title}" in response`);
|
||||
} catch (err) {
|
||||
const ms = Math.round(performance.now() - started);
|
||||
if (err instanceof ConnectError) {
|
||||
log("lookup", `FAILED after ${ms}ms`, formatConnectError(err));
|
||||
} else {
|
||||
log("lookup", `FAILED after ${ms}ms`, { err: String(err) });
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
async function probeGetPlaylog(
|
||||
transport: Transport,
|
||||
profileApiId: string,
|
||||
game: ProbeGame,
|
||||
maxItems: number,
|
||||
): Promise<number> {
|
||||
const started = performance.now();
|
||||
let count = 0;
|
||||
let firstItemMs: number | undefined;
|
||||
|
||||
log("stream", `GetPlaylog (max ${maxItems} items) profileApiId=${profileApiId} ...`);
|
||||
|
||||
const stream = createPlaylogStream(transport, profileApiId, game);
|
||||
|
||||
try {
|
||||
for await (const item of stream) {
|
||||
count++;
|
||||
if (firstItemMs === undefined) {
|
||||
firstItemMs = Math.round(performance.now() - started);
|
||||
log("stream", `First item after ${firstItemMs}ms`, { sample: summarizeItem(item) });
|
||||
}
|
||||
|
||||
if (count >= maxItems) {
|
||||
log("stream", `Stopping after ${count} item(s) (--max-items)`);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
const ms = Math.round(performance.now() - started);
|
||||
log("stream", `OK — received ${count} item(s) in ${ms}ms`, {
|
||||
firstItemMs: firstItemMs ?? null,
|
||||
});
|
||||
return count;
|
||||
} catch (err) {
|
||||
const ms = Math.round(performance.now() - started);
|
||||
if (err instanceof ConnectError) {
|
||||
log(
|
||||
"stream",
|
||||
`FAILED after ${ms}ms (${count} item(s) before error)`,
|
||||
formatConnectError(err),
|
||||
);
|
||||
} else {
|
||||
log("stream", `FAILED after ${ms}ms (${count} item(s) before error)`, {
|
||||
err: String(err),
|
||||
});
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
function summarizeItem(item: unknown): Record<string, unknown> {
|
||||
if (item === null || typeof item !== "object") {
|
||||
return { type: typeof item };
|
||||
}
|
||||
|
||||
const record = item as Record<string, unknown>;
|
||||
const summary: Record<string, unknown> = {};
|
||||
if ("playlogApiId" in record) {
|
||||
summary.playlogApiId = record.playlogApiId;
|
||||
}
|
||||
if ("info" in record && record.info !== undefined) {
|
||||
summary.hasInfo = true;
|
||||
}
|
||||
if ("judge" in record && record.judge !== undefined) {
|
||||
summary.hasJudge = true;
|
||||
}
|
||||
return summary;
|
||||
}
|
||||
|
||||
function createPlaylogStream(
|
||||
transport: Transport,
|
||||
profileApiId: string,
|
||||
game: ProbeGame,
|
||||
): AsyncIterable<unknown> {
|
||||
switch (game) {
|
||||
case "chunithm": {
|
||||
const client = createClient(ChunithmUser, transport);
|
||||
return client.getPlaylog(create(GetPlaylogRequestSchema, { profileApiId }));
|
||||
}
|
||||
case "maimaidx": {
|
||||
const client = createClient(MaimaiUser, transport);
|
||||
return client.getPlaylog(create(MaimaiGetPlaylogRequestSchema, { profileApiId }));
|
||||
}
|
||||
case "ongeki": {
|
||||
const client = createClient(OngekiUser, transport);
|
||||
return client.getPlaylog(create(OngekiGetPlaylogRequestSchema, { profileApiId }));
|
||||
}
|
||||
case "wacca": {
|
||||
const client = createClient(WaccaUser, transport);
|
||||
return client.getPlaylog(create(PlaylogRequestSchema, { apiId: profileApiId }));
|
||||
}
|
||||
default: {
|
||||
const _exhaustive: never = game;
|
||||
return _exhaustive;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function printRuntimeInfo(): void {
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
log("env", `Bun ${Bun.version}`);
|
||||
log("env", `TACHI_MYT_API_HOST=${requireEnv("TACHI_MYT_API_HOST")}`);
|
||||
|
||||
const node = spawnSync("node", ["--version"], { encoding: "utf8" });
|
||||
if (node.status === 0 && node.stdout.trim()) {
|
||||
log("env", `Node ${node.stdout.trim()} (available for manual A/B vs bun)`);
|
||||
} else {
|
||||
log("env", "Node not on PATH — streaming A/B needs a Node binary in the image");
|
||||
}
|
||||
}
|
||||
|
||||
function printHelp(): void {
|
||||
console.log(`
|
||||
myt-grpc-probe — test MYT unary Lookup vs streaming GetPlaylog
|
||||
|
||||
--profile-api-id <id> Title API id (from Lookup or MYT admin)
|
||||
--access-code <code> Card access code (runs Lookup first)
|
||||
--game <name> chunithm | maimaidx | ongeki | wacca (default: chunithm)
|
||||
--max-items <n> Stop stream after N items (default: 5)
|
||||
--lookup-only Skip GetPlaylog
|
||||
--stream-only Skip Lookup (requires --profile-api-id)
|
||||
-h, --help
|
||||
|
||||
Examples (inside container):
|
||||
|
||||
cd /app/typescript/server
|
||||
bun run src/scripts/myt-grpc-probe.ts -- --access-code "YOUR_CODE" --game chunithm
|
||||
bun run src/scripts/myt-grpc-probe.ts -- --profile-api-id "abc123" --max-items 1
|
||||
`);
|
||||
}
|
||||
|
||||
async function main(): Promise<void> {
|
||||
const { values } = parseArgs({
|
||||
options: {
|
||||
"profile-api-id": { type: "string" },
|
||||
"access-code": { type: "string" },
|
||||
game: { type: "string", default: "chunithm" },
|
||||
"max-items": { type: "string", default: "5" },
|
||||
"lookup-only": { type: "boolean", default: false },
|
||||
"stream-only": { type: "boolean", default: false },
|
||||
help: { type: "boolean", short: "h", default: false },
|
||||
},
|
||||
allowPositionals: false,
|
||||
});
|
||||
|
||||
if (values.help) {
|
||||
printHelp();
|
||||
return;
|
||||
}
|
||||
|
||||
const game = values.game as ProbeGame;
|
||||
if (!(game in MYT_TITLE)) {
|
||||
fail(`Unknown --game ${values.game}; use chunithm, maimaidx, ongeki, or wacca`);
|
||||
}
|
||||
|
||||
const maxItems = Number.parseInt(values["max-items"] ?? "5", 10);
|
||||
if (!Number.isFinite(maxItems) || maxItems < 1) {
|
||||
fail("--max-items must be a positive integer");
|
||||
}
|
||||
|
||||
printRuntimeInfo();
|
||||
|
||||
const transport = createTransport();
|
||||
let profileApiId = values["profile-api-id"]?.trim();
|
||||
|
||||
if (!values["stream-only"] && values["access-code"]) {
|
||||
profileApiId = await probeLookup(transport, values["access-code"].trim(), game);
|
||||
if (values["lookup-only"]) {
|
||||
log("done", "Lookup-only mode — exit 0");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (values["lookup-only"]) {
|
||||
if (!values["access-code"]) {
|
||||
fail("--lookup-only requires --access-code");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (!profileApiId) {
|
||||
fail("Need --profile-api-id or --access-code for GetPlaylog");
|
||||
}
|
||||
|
||||
await probeGetPlaylog(transport, profileApiId, game, maxItems);
|
||||
log("done", "All phases OK — exit 0");
|
||||
}
|
||||
|
||||
main().catch(() => {
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -22,7 +22,7 @@ const router: Router = Router({ mergeParams: true });
|
||||
|
||||
router.use(async (req, res, next) => {
|
||||
if (!req[SYMBOL_TACHI_API_AUTH]) {
|
||||
log.info(
|
||||
log.debug(
|
||||
{
|
||||
body: req.body,
|
||||
query: req.query,
|
||||
@@ -43,7 +43,7 @@ router.use(async (req, res, next) => {
|
||||
user = null;
|
||||
}
|
||||
|
||||
log.info(
|
||||
log.debug(
|
||||
{
|
||||
user,
|
||||
body: req.body,
|
||||
|
||||
Reference in New Issue
Block a user