chore: 纳入项目全部源码进根仓库统一管理

删除 YouleNexus 内嵌 .git,统一由根仓库唯一管理。提交:
- projects/ 旧 H5 模板 Game_Surface_3 与各子游戏源码
- cocoscreator_projects/YouleNexus 工程 + cocos-creator-mcp 扩展源码
- CLAUDE.md、.mcp.json 等配置
library/temp/node_modules/dist 等缓存由 .gitignore 排除。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-28 11:55:13 +08:00
co-authored by Claude Opus 4.8
parent f5673c33c8
commit 7ab748919a
7317 changed files with 2810014 additions and 0 deletions
File diff suppressed because one or more lines are too long
@@ -0,0 +1,91 @@
/**
* McpConsoleCapture — ゲームプレビュー時のconsole.log/warn/errorをMCPサーバーに送信する。
*
* MCPサーバーの POST /log エンドポイントにバッチ送信する。
* MCPサーバー未起動時は黙って無視(本番影響なし)。
*
* Usage:
* import { initMcpConsoleCapture } from "./McpConsoleCapture";
* initMcpConsoleCapture(); // or initMcpConsoleCapture({ mcpBaseUrl: "http://127.0.0.1:3001" });
*/
export interface McpConsoleCaptureConfig {
/** MCPサーバーのベースURL (default: "http://127.0.0.1:3000") */
mcpBaseUrl?: string;
/** バッチ送信間隔ms (default: 500) */
flushInterval?: number;
/** 1回の送信最大件数 (default: 50) */
maxBatchSize?: number;
}
interface LogEntry {
timestamp: string;
level: "log" | "warn" | "error";
message: string;
}
let _buffer: LogEntry[] = [];
let _timer: any = null;
let _initialized = false;
let _config: Required<McpConsoleCaptureConfig>;
function formatArgs(args: any[]): string {
return args.map(a => {
if (typeof a === "string") return a;
if (a instanceof Error) return `${a.message}\n${a.stack || ""}`;
try { return JSON.stringify(a); } catch { return String(a); }
}).join(" ");
}
function flush(): void {
if (_buffer.length === 0) return;
const entries = _buffer.splice(0, _config.maxBatchSize);
fetch(`${_config.mcpBaseUrl}/log`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(entries),
}).catch(() => {});
}
/** ゲーム起動時に1回呼ぶ。console.log/warn/errorをフックしてMCPに送信開始 */
export function initMcpConsoleCapture(config?: McpConsoleCaptureConfig): void {
if (_initialized) return;
_initialized = true;
_config = {
mcpBaseUrl: config?.mcpBaseUrl ?? "http://127.0.0.1:3000",
flushInterval: config?.flushInterval ?? 500,
maxBatchSize: config?.maxBatchSize ?? 50,
};
const originalLog = console.log;
const originalWarn = console.warn;
const originalError = console.error;
function hook(level: LogEntry["level"], original: (...args: any[]) => void) {
return function (...args: any[]) {
original.apply(console, args);
_buffer.push({
timestamp: new Date().toISOString(),
level,
message: formatArgs(args),
});
};
}
console.log = hook("log", originalLog);
console.warn = hook("warn", originalWarn);
console.error = hook("error", originalError);
_timer = setInterval(flush, _config.flushInterval);
console.log("[McpConsoleCapture] initialized");
}
/** 停止 */
export function stopMcpConsoleCapture(): void {
if (_timer) {
clearInterval(_timer);
_timer = null;
}
flush();
_initialized = false;
}
File diff suppressed because one or more lines are too long
@@ -0,0 +1,315 @@
/**
* McpDebugClient — MCPサーバーとCocosCreatorプレビューの双方向通信
*
* MCPサーバーのコマンドキューをポーリングし、コマンドを実行して結果を返す。
* MCPサーバー未起動時は黙って無視(本番影響なし)。
*
* ビルトインコマンド:
* - screenshot: RenderTexture経由でゲーム画面キャプチャ
* - click: ノード名指定でクリックイベント発火
*
* カスタムコマンドを追加可能(プロジェクト固有の機能):
* initMcpDebugClient({
* customCommands: {
* state: () => ({ success: true, data: myDb.dump() }),
* navigate: async (args) => { await myRouter.go(args.page); return { success: true }; },
* },
* });
*/
import { director, Node, Button, Camera, RenderTexture, gfx } from "cc";
export interface McpDebugClientConfig {
/** MCPサーバーのベースURL (default: "http://127.0.0.1:3000") */
mcpBaseUrl?: string;
/** ポーリング間隔ms (default: 500) */
pollInterval?: number;
/** プロジェクト固有のコマンドハンドラー */
customCommands?: Record<string, (args: any) => any | Promise<any>>;
}
interface McpCommand {
id: string;
type: string;
args?: any;
}
let _polling = false;
let _timer: any = null;
let _config: Required<Pick<McpDebugClientConfig, "mcpBaseUrl" | "pollInterval">> & { customCommands: Record<string, (args: any) => any | Promise<any>> };
async function pollCommand(): Promise<void> {
if (_polling) return;
_polling = true;
try {
const res = await fetch(`${_config.mcpBaseUrl}/game/command`);
if (!res.ok) return;
const cmd: McpCommand | null = await res.json();
if (!cmd) return;
console.log(`[McpDebugClient] command: ${cmd.type}`, cmd.args || "");
const result = await executeCommand(cmd);
await fetch(`${_config.mcpBaseUrl}/game/result`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(result),
}).catch(() => {});
} catch {
// MCP server not running
} finally {
_polling = false;
}
}
async function executeCommand(cmd: McpCommand): Promise<any> {
try {
// ビルトインコマンド
switch (cmd.type) {
case "screenshot":
return { id: cmd.id, ...takeScreenshot() };
case "click":
return { id: cmd.id, ...clickNode(cmd.args?.name) };
case "record_start":
return { id: cmd.id, ...startRecording(cmd.args) };
case "record_stop":
return { id: cmd.id, ...(await stopRecording()) };
}
// カスタムコマンド
const handler = _config.customCommands[cmd.type];
if (handler) {
const result = await handler(cmd.args);
return { id: cmd.id, ...result };
}
return { id: cmd.id, success: false, error: `Unknown command: ${cmd.type}` };
} catch (e: any) {
return { id: cmd.id, success: false, error: e.message || String(e) };
}
}
// ─── ビルトインコマンド ───
function takeScreenshot(): { success: boolean; data?: any; error?: string } {
try {
const scene = director.getScene();
if (!scene) return { success: false, error: "No active scene" };
let camera: Camera | null = null;
const findCamera = (node: Node) => {
const cam = node.getComponent(Camera);
if (cam && cam.enabled) { camera = cam; return; }
for (const child of node.children) {
if (camera) return;
findCamera(child);
}
};
findCamera(scene);
if (!camera) return { success: false, error: "No camera found" };
const cam = camera as Camera;
const width = Math.floor(cam.camera.width);
const height = Math.floor(cam.camera.height);
const rt = new RenderTexture();
rt.reset({ width, height });
const prevTarget = cam.targetTexture;
cam.targetTexture = rt;
director.root!.frameMove(0);
cam.targetTexture = prevTarget;
const region = new gfx.BufferTextureCopy();
region.texOffset.x = 0;
region.texOffset.y = 0;
region.texExtent.width = width;
region.texExtent.height = height;
const buffer = new Uint8Array(width * height * 4);
const gfxTex = rt.getGFXTexture()!;
director.root!.device.copyTextureToBuffers(gfxTex, [buffer], [region]);
const cvs = document.createElement("canvas");
cvs.width = width;
cvs.height = height;
const ctx = cvs.getContext("2d")!;
const imageData = ctx.createImageData(width, height);
for (let y = 0; y < height; y++) {
const srcRow = (height - 1 - y) * width * 4;
const dstRow = y * width * 4;
for (let x = 0; x < width * 4; x++) {
imageData.data[dstRow + x] = buffer[srcRow + x];
}
}
ctx.putImageData(imageData, 0, 0);
const dataUrl = cvs.toDataURL("image/png");
rt.destroy();
return { success: true, data: { dataUrl, width, height } };
} catch (e: any) {
return { success: false, error: e.message };
}
}
function clickNode(name?: string): { success: boolean; error?: string } {
if (!name) return { success: false, error: "name argument required" };
const scene = director.getScene();
if (!scene) return { success: false, error: "No active scene" };
const found = findNodeByName(scene, name);
if (!found) return { success: false, error: `Node '${name}' not found` };
found.emit(Button.EventType.CLICK, found);
return { success: true };
}
// ─── 録画(MediaRecorder経由) ───
let _mediaRecorder: MediaRecorder | null = null;
let _recordChunks: Blob[] = [];
let _recordStream: MediaStream | null = null;
let _recordId: string | null = null;
const QUALITY_PRESETS: Record<string, number> = {
low: 0.15,
medium: 0.25,
high: 0.40,
ultra: 0.60,
};
function startRecording(args?: { fps?: number; videoBitsPerSecond?: number; quality?: string; coefficient?: number; format?: "webm" | "mp4" }): { success: boolean; error?: string; data?: any } {
// 前回の録画状態が残っていたら強制クリア
if (_mediaRecorder) {
try { if (_mediaRecorder.state !== "inactive") _mediaRecorder.stop(); } catch {}
_recordStream?.getTracks().forEach(t => { try { t.stop(); } catch {} });
_mediaRecorder = null;
_recordStream = null;
_recordChunks = [];
_recordId = null;
}
try {
// GameView canvas取得
const canvas = document.getElementById("GameCanvas") as HTMLCanvasElement
|| document.querySelector("canvas") as HTMLCanvasElement;
if (!canvas) return { success: false, error: "canvas not found" };
const fps = args?.fps ?? 30;
const quality = args?.quality ?? "medium";
const coef = args?.coefficient ?? QUALITY_PRESETS[quality] ?? QUALITY_PRESETS.medium;
const autoBps = Math.round(canvas.width * canvas.height * fps * coef);
const bps = args?.videoBitsPerSecond ?? autoBps;
const format = args?.format ?? "mp4";
_recordStream = canvas.captureStream(fps);
_recordChunks = [];
_recordId = `rec_${new Date().toISOString().replace(/[:.]/g, "-").replace("T", "_").substring(0, 19)}`;
// 指定フォーマット優先、非対応ならwebmにfallback
const mp4Candidates = [
"video/mp4;codecs=h264",
"video/mp4;codecs=avc1.42E01E",
"video/mp4",
];
const webmCandidates = [
"video/webm;codecs=vp9",
"video/webm;codecs=vp8",
"video/webm",
];
const candidates = format === "mp4"
? [...mp4Candidates, ...webmCandidates]
: webmCandidates;
let mimeType = "";
for (const c of candidates) {
if (typeof MediaRecorder !== "undefined" && MediaRecorder.isTypeSupported(c)) {
mimeType = c;
break;
}
}
if (!mimeType) return { success: false, error: "No supported MediaRecorder mimeType" };
_mediaRecorder = new MediaRecorder(_recordStream, { mimeType, videoBitsPerSecond: bps });
_mediaRecorder.ondataavailable = (e: BlobEvent) => {
if (e.data.size > 0) _recordChunks.push(e.data);
};
_mediaRecorder.start();
return { success: true, data: { id: _recordId, mimeType, fps, videoBitsPerSecond: bps, quality, canvasWidth: canvas.width, canvasHeight: canvas.height } };
} catch (e: any) {
_mediaRecorder = null;
_recordStream = null;
return { success: false, error: e.message || String(e) };
}
}
async function stopRecording(): Promise<{ success: boolean; error?: string; data?: any }> {
if (!_mediaRecorder || !_recordId) return { success: false, error: "not recording" };
const id = _recordId;
const mimeType = _mediaRecorder.mimeType;
return new Promise((resolve) => {
_mediaRecorder!.onstop = async () => {
try {
const blob = new Blob(_recordChunks, { type: mimeType });
_recordStream?.getTracks().forEach(t => t.stop());
_mediaRecorder = null;
_recordStream = null;
_recordChunks = [];
_recordId = null;
// Blob → base64 → POST /game/recording
const base64 = await blobToBase64(blob);
const uploadRes = await fetch(`${_config.mcpBaseUrl}/game/recording`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ id, base64, mimeType }),
});
const uploadData = await uploadRes.json();
resolve({ success: true, data: { id, size: blob.size, ...uploadData } });
} catch (e: any) {
resolve({ success: false, error: e.message || String(e) });
}
};
_mediaRecorder!.stop();
});
}
function blobToBase64(blob: Blob): Promise<string> {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onloadend = () => {
const result = reader.result as string;
// "data:video/webm;base64,XXX" → XXX だけ抽出
const commaIdx = result.indexOf(",");
resolve(commaIdx >= 0 ? result.substring(commaIdx + 1) : result);
};
reader.onerror = reject;
reader.readAsDataURL(blob);
});
}
function findNodeByName(root: Node, name: string): Node | null {
if (root.name === name) return root;
for (const child of root.children) {
const found = findNodeByName(child, name);
if (found) return found;
}
return null;
}
// ─── 初期化・停止 ───
export function initMcpDebugClient(config?: McpDebugClientConfig): void {
if (_timer) return;
_config = {
mcpBaseUrl: config?.mcpBaseUrl ?? "http://127.0.0.1:3000",
pollInterval: config?.pollInterval ?? 500,
customCommands: config?.customCommands ?? {},
};
_timer = setInterval(pollCommand, _config.pollInterval);
console.log("[McpDebugClient] initialized");
}
export function stopMcpDebugClient(): void {
if (_timer) {
clearInterval(_timer);
_timer = null;
}
}
@@ -0,0 +1,171 @@
#!/usr/bin/env node
/**
* cocos-creator-mcp stdio bridge
*
* stdin で line-delimited JSON-RPC を読み取り、HTTP MCP エンドポイントに転送する。
* レスポンスは stdout に 1 行ずつ書き出す。
*
* Claude Code VSCode 拡張の HTTP MCP OAuth 強制バグ (#26917 等) を回避するための
* 代替トランスポート。ユーザーは .mcp.json で stdio 型として設定する:
*
* {
* "mcpServers": {
* "cocos-creator-mcp": {
* "command": "node",
* "args": [
* "C:/path/to/cocos-creator-mcp/client/stdio-bridge.js"
* ]
* }
* }
* }
*
* Optional 環境変数:
* COCOS_MCP_URL - MCP サーバーの URL (default: http://127.0.0.1:3000/mcp)
*/
"use strict";
const http = require("http");
const { URL } = require("url");
const readline = require("readline");
const DEFAULT_URL = "http://127.0.0.1:3000/mcp";
const serverUrl = new URL(process.env.COCOS_MCP_URL || DEFAULT_URL);
let sessionId = null;
function log(...args) {
// デバッグログは stderr に出す(stdout は JSON-RPC 応答専用)
console.error("[stdio-bridge]", ...args);
}
function postJsonRpc(body) {
return new Promise((resolve, reject) => {
const headers = {
"Content-Type": "application/json",
"Accept": "application/json",
"Content-Length": Buffer.byteLength(body),
};
if (sessionId) headers["Mcp-Session-Id"] = sessionId;
const req = http.request(
{
hostname: serverUrl.hostname,
port: serverUrl.port || 80,
path: serverUrl.pathname,
method: "POST",
headers,
},
(res) => {
const chunks = [];
res.on("data", (c) => chunks.push(c));
res.on("end", () => {
const responseSession = res.headers["mcp-session-id"];
if (responseSession && !sessionId) {
sessionId = responseSession;
log("session established:", sessionId);
}
const text = Buffer.concat(chunks).toString("utf8");
resolve({ status: res.statusCode || 0, body: text });
});
}
);
req.on("error", (err) => reject(err));
req.write(body);
req.end();
});
}
function writeResponse(obj) {
process.stdout.write(JSON.stringify(obj) + "\n");
}
async function handleLine(line) {
const trimmed = line.trim();
if (!trimmed) return;
let request;
try {
request = JSON.parse(trimmed);
} catch (e) {
writeResponse({
jsonrpc: "2.0",
id: null,
error: { code: -32700, message: "Parse error", data: e.message },
});
return;
}
try {
const { status, body } = await postJsonRpc(trimmed);
// Notifications (no id) may yield empty body
if (!body) {
if (request.id !== undefined) {
writeResponse({
jsonrpc: "2.0",
id: request.id,
error: { code: -32603, message: "Empty response from server", data: { status } },
});
}
return;
}
// Pass through server response (single JSON object per line)
try {
const parsed = JSON.parse(body);
writeResponse(parsed);
} catch {
// サーバーが SSE 等で返した場合のフォールバック
process.stdout.write(body + (body.endsWith("\n") ? "" : "\n"));
}
} catch (err) {
writeResponse({
jsonrpc: "2.0",
id: request.id !== undefined ? request.id : null,
error: {
code: -32603,
message: "HTTP bridge error",
data: err && err.message ? err.message : String(err),
},
});
}
}
let inflight = 0;
let stdinClosed = false;
function maybeExit() {
if (stdinClosed && inflight === 0) {
log("stdin closed, no inflight, exiting");
process.exit(0);
}
}
function main() {
log(`starting, target=${serverUrl.href}`);
const rl = readline.createInterface({
input: process.stdin,
terminal: false,
});
rl.on("line", (line) => {
inflight++;
handleLine(line)
.catch((e) => log("handler error:", e))
.finally(() => {
inflight--;
maybeExit();
});
});
rl.on("close", () => {
stdinClosed = true;
maybeExit();
});
process.on("SIGINT", () => process.exit(0));
process.on("SIGTERM", () => process.exit(0));
}
main();