Unverified 提交 beeaef57 authored 作者: Will Chen's avatar Will Chen 提交者: GitHub

Replace ChatLogsData with comprehensive SessionDebugBundle schema (#2488)

## Summary - Replaces the old `ChatLogsDataSchema` with a new `SessionDebugBundleSchema` (schema version 1) that captures all non-sensitive data needed for debugging chat sessions - Each message now includes AI SDK JSON (with base64 images stripped), model name, token usage, timestamps, commit hashes, request ID, and approval state - Adds non-sensitive user settings snapshot, app metadata, custom provider/model definitions, and MCP server configurations to the debug bundle - Updates HelpDialog to use the new schema and adds `Session Schema: v2.0` marker to GitHub issue templates ## Test plan - [ ] Verify `npm run ts` passes (type-check) - [ ] Verify `npm run lint` passes - [ ] Verify `npm test` passes (all 661 unit tests) - [ ] Manual: Open Help dialog → Upload Chat Session → verify the review modal shows messages, codebase, logs, and system info correctly - [ ] Manual: Complete upload and verify the GitHub issue template includes `Session Schema: v2.0` - [ ] Manual: Download uploaded JSON and verify it contains: `schemaVersion`, `system`, `settings`, `app`, `chat` (with `aiMessagesJson` per message), `providers`, `mcpServers`, `codebase`, `logs` - [ ] Manual: Verify no API keys, OAuth tokens, or MCP env vars appear in uploaded data 🤖 Generated with [Claude Code](https://claude.com/claude-code) <!-- devin-review-badge-begin --> --- <a href="https://app.devin.ai/review/dyad-sh/dyad/pull/2488"> <picture> <source media="(prefers-color-scheme: dark)" srcset="https://static.devin.ai/assets/gh-open-in-devin-review-dark.svg?v=1"> <img src="https://static.devin.ai/assets/gh-open-in-devin-review-light.svg?v=1" alt="Open with Devin"> </picture> </a> <!-- devin-review-badge-end --> <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Medium Risk** > Expands what gets captured and uploaded for support sessions (settings/app metadata/providers/MCP and richer message data), so any sanitization or schema mismatch could leak data or break the support upload flow. > > **Overview** > Switches chat-session uploads from `ChatLogsData` to a new versioned `SessionDebugBundle` (`SESSION_DEBUG_SCHEMA_VERSION = 2`) and updates the IPC contract/exports accordingly. > > The main-process handler now assembles a comprehensive bundle (system/runtime info, sanitized settings snapshot, app metadata, full chat messages with stripped image/file blobs, custom provider/model summaries, MCP server configs without env/header secrets, codebase snapshot, and last ~1000 log lines), and `HelpDialog` is updated to fetch/review/upload this bundle and annotate created GitHub issues with `Session Schema: v2.0` (plus adds a settings section to the bug report template). > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 7b94199939aa6963787e8fe69716e20cd6570b7d. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup> <!-- /CURSOR_SUMMARY --> --------- Co-authored-by: 's avatarClaude Opus 4.5 <noreply@anthropic.com>
上级 fd917781
...@@ -21,7 +21,7 @@ import { ipc } from "@/ipc/types"; ...@@ -21,7 +21,7 @@ import { ipc } from "@/ipc/types";
import { useState, useEffect } from "react"; import { useState, useEffect } from "react";
import { useAtomValue } from "jotai"; import { useAtomValue } from "jotai";
import { selectedChatIdAtom } from "@/atoms/chatAtoms"; import { selectedChatIdAtom } from "@/atoms/chatAtoms";
import { ChatLogsData } from "@/ipc/types"; import { SessionDebugBundle } from "@/ipc/types";
import { showError } from "@/lib/toast"; import { showError } from "@/lib/toast";
import { HelpBotDialog } from "./HelpBotDialog"; import { HelpBotDialog } from "./HelpBotDialog";
import { useSettings } from "@/hooks/useSettings"; import { useSettings } from "@/hooks/useSettings";
...@@ -37,7 +37,9 @@ export function HelpDialog({ isOpen, onClose }: HelpDialogProps) { ...@@ -37,7 +37,9 @@ export function HelpDialog({ isOpen, onClose }: HelpDialogProps) {
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
const [isUploading, setIsUploading] = useState(false); const [isUploading, setIsUploading] = useState(false);
const [reviewMode, setReviewMode] = useState(false); const [reviewMode, setReviewMode] = useState(false);
const [chatLogsData, setChatLogsData] = useState<ChatLogsData | null>(null); const [debugBundle, setDebugBundle] = useState<SessionDebugBundle | null>(
null,
);
const [uploadComplete, setUploadComplete] = useState(false); const [uploadComplete, setUploadComplete] = useState(false);
const [sessionId, setSessionId] = useState(""); const [sessionId, setSessionId] = useState("");
const [isHelpBotOpen, setIsHelpBotOpen] = useState(false); const [isHelpBotOpen, setIsHelpBotOpen] = useState(false);
...@@ -52,7 +54,7 @@ export function HelpDialog({ isOpen, onClose }: HelpDialogProps) { ...@@ -52,7 +54,7 @@ export function HelpDialog({ isOpen, onClose }: HelpDialogProps) {
setIsLoading(false); setIsLoading(false);
setIsUploading(false); setIsUploading(false);
setReviewMode(false); setReviewMode(false);
setChatLogsData(null); setDebugBundle(null);
setUploadComplete(false); setUploadComplete(false);
setSessionId(""); setSessionId("");
}; };
...@@ -76,6 +78,20 @@ export function HelpDialog({ isOpen, onClose }: HelpDialogProps) { ...@@ -76,6 +78,20 @@ export function HelpDialog({ isOpen, onClose }: HelpDialogProps) {
const debugInfo = await ipc.system.getSystemDebugInfo(); const debugInfo = await ipc.system.getSystemDebugInfo();
// Create a formatted issue body with the debug info // Create a formatted issue body with the debug info
const settingsLines = settings
? [
`- Selected Model: ${settings.selectedModel?.provider}:${settings.selectedModel?.name}`,
`- Chat Mode: ${settings.selectedChatMode ?? "default"}`,
`- Auto Approve Changes: ${settings.autoApproveChanges ?? "n/a"}`,
`- Dyad Pro Enabled: ${settings.enableDyadPro ?? "n/a"}`,
`- Thinking Budget: ${settings.thinkingBudget ?? "n/a"}`,
`- Runtime Mode: ${settings.runtimeMode2 ?? "n/a"}`,
`- Release Channel: ${settings.releaseChannel ?? "n/a"}`,
`- Auto Fix Problems: ${settings.enableAutoFixProblems ?? "n/a"}`,
`- Native Git: ${settings.enableNativeGit ?? "n/a"}`,
].join("\n")
: "Settings not available";
const issueBody = ` const issueBody = `
<!-- Please fill in all fields in English --> <!-- Please fill in all fields in English -->
...@@ -96,6 +112,9 @@ export function HelpDialog({ isOpen, onClose }: HelpDialogProps) { ...@@ -96,6 +112,9 @@ export function HelpDialog({ isOpen, onClose }: HelpDialogProps) {
- Telemetry ID: ${debugInfo.telemetryId || "n/a"} - Telemetry ID: ${debugInfo.telemetryId || "n/a"}
- Model: ${debugInfo.selectedLanguageModel || "n/a"} - Model: ${debugInfo.selectedLanguageModel || "n/a"}
## Settings
${settingsLines}
## Logs ## Logs
\`\`\` \`\`\`
${debugInfo.logs.slice(-3_500) || "No logs available"} ${debugInfo.logs.slice(-3_500) || "No logs available"}
...@@ -131,10 +150,10 @@ ${debugInfo.logs.slice(-3_500) || "No logs available"} ...@@ -131,10 +150,10 @@ ${debugInfo.logs.slice(-3_500) || "No logs available"}
setIsUploading(true); setIsUploading(true);
try { try {
// Get chat logs (includes debug info, chat data, and codebase) // Get chat logs (includes debug info, chat data, and codebase)
const chatLogs = await ipc.misc.getChatLogs(selectedChatId); const debugBundle = await ipc.misc.getSessionDebugBundle(selectedChatId);
// Store data for review and switch to review mode // Store data for review and switch to review mode
setChatLogsData(chatLogs); setDebugBundle(debugBundle);
setReviewMode(true); setReviewMode(true);
} catch (error) { } catch (error) {
console.error("Failed to upload chat session:", error); console.error("Failed to upload chat session:", error);
...@@ -147,17 +166,10 @@ ${debugInfo.logs.slice(-3_500) || "No logs available"} ...@@ -147,17 +166,10 @@ ${debugInfo.logs.slice(-3_500) || "No logs available"}
}; };
const handleSubmitChatLogs = async () => { const handleSubmitChatLogs = async () => {
if (!chatLogsData) return; if (!debugBundle) return;
setIsUploading(true); setIsUploading(true);
try { try {
// Prepare data for upload
const chatLogsJson = {
systemInfo: chatLogsData.debugInfo,
chat: chatLogsData.chat,
codebaseSnippet: chatLogsData.codebase,
};
// Get signed URL // Get signed URL
const response = await fetch( const response = await fetch(
"https://upload-logs.dyad.sh/generate-upload-url", "https://upload-logs.dyad.sh/generate-upload-url",
...@@ -180,10 +192,11 @@ ${debugInfo.logs.slice(-3_500) || "No logs available"} ...@@ -180,10 +192,11 @@ ${debugInfo.logs.slice(-3_500) || "No logs available"}
const { uploadUrl, filename } = await response.json(); const { uploadUrl, filename } = await response.json();
// Upload the full debug bundle directly
await ipc.system.uploadToSignedUrl({ await ipc.system.uploadToSignedUrl({
url: uploadUrl, url: uploadUrl,
contentType: "application/json", contentType: "application/json",
data: chatLogsJson, data: debugBundle,
}); });
// Extract session ID (filename without extension) // Extract session ID (filename without extension)
...@@ -201,7 +214,7 @@ ${debugInfo.logs.slice(-3_500) || "No logs available"} ...@@ -201,7 +214,7 @@ ${debugInfo.logs.slice(-3_500) || "No logs available"}
const handleCancelReview = () => { const handleCancelReview = () => {
setReviewMode(false); setReviewMode(false);
setChatLogsData(null); setDebugBundle(null);
}; };
const handleOpenGitHubIssue = () => { const handleOpenGitHubIssue = () => {
...@@ -210,6 +223,7 @@ ${debugInfo.logs.slice(-3_500) || "No logs available"} ...@@ -210,6 +223,7 @@ ${debugInfo.logs.slice(-3_500) || "No logs available"}
<!-- Please fill in all fields in English --> <!-- Please fill in all fields in English -->
Session ID: ${sessionId} Session ID: ${sessionId}
Session Schema: v2.0
Pro User ID: ${userBudget?.redactedUserId || "n/a"} Pro User ID: ${userBudget?.redactedUserId || "n/a"}
## Issue Description (required) ## Issue Description (required)
...@@ -276,7 +290,7 @@ Pro User ID: ${userBudget?.redactedUserId || "n/a"} ...@@ -276,7 +290,7 @@ Pro User ID: ${userBudget?.redactedUserId || "n/a"}
); );
} }
if (reviewMode && chatLogsData) { if (reviewMode && debugBundle) {
return ( return (
<Dialog open={isOpen} onOpenChange={handleClose}> <Dialog open={isOpen} onOpenChange={handleClose}>
<DialogContent className="max-w-4xl max-h-[80vh] overflow-hidden flex flex-col"> <DialogContent className="max-w-4xl max-h-[80vh] overflow-hidden flex flex-col">
...@@ -302,7 +316,7 @@ Pro User ID: ${userBudget?.redactedUserId || "n/a"} ...@@ -302,7 +316,7 @@ Pro User ID: ${userBudget?.redactedUserId || "n/a"}
<div className="border rounded-md p-3"> <div className="border rounded-md p-3">
<h3 className="font-medium mb-2">Chat Messages</h3> <h3 className="font-medium mb-2">Chat Messages</h3>
<div className="text-sm bg-slate-50 dark:bg-slate-900 rounded p-2 max-h-40 overflow-y-auto"> <div className="text-sm bg-slate-50 dark:bg-slate-900 rounded p-2 max-h-40 overflow-y-auto">
{chatLogsData.chat.messages.map((msg) => ( {debugBundle.chat.messages.map((msg) => (
<div key={msg.id} className="mb-2"> <div key={msg.id} className="mb-2">
<span className="font-semibold"> <span className="font-semibold">
{msg.role === "user" ? "You" : "Assistant"}:{" "} {msg.role === "user" ? "You" : "Assistant"}:{" "}
...@@ -316,29 +330,63 @@ Pro User ID: ${userBudget?.redactedUserId || "n/a"} ...@@ -316,29 +330,63 @@ Pro User ID: ${userBudget?.redactedUserId || "n/a"}
<div className="border rounded-md p-3"> <div className="border rounded-md p-3">
<h3 className="font-medium mb-2">Codebase Snapshot</h3> <h3 className="font-medium mb-2">Codebase Snapshot</h3>
<div className="text-sm bg-slate-50 dark:bg-slate-900 rounded p-2 max-h-40 overflow-y-auto font-mono"> <div className="text-sm bg-slate-50 dark:bg-slate-900 rounded p-2 max-h-40 overflow-y-auto font-mono">
{chatLogsData.codebase} {debugBundle.codebase}
</div> </div>
</div> </div>
<div className="border rounded-md p-3"> <div className="border rounded-md p-3">
<h3 className="font-medium mb-2">Logs</h3> <h3 className="font-medium mb-2">Logs</h3>
<div className="text-sm bg-slate-50 dark:bg-slate-900 rounded p-2 max-h-40 overflow-y-auto font-mono"> <div className="text-sm bg-slate-50 dark:bg-slate-900 rounded p-2 max-h-40 overflow-y-auto font-mono">
{chatLogsData.debugInfo.logs} {debugBundle.logs}
</div> </div>
</div> </div>
<div className="border rounded-md p-3"> <div className="border rounded-md p-3">
<h3 className="font-medium mb-2">System Information</h3> <h3 className="font-medium mb-2">System Information</h3>
<div className="text-sm bg-slate-50 dark:bg-slate-900 rounded p-2 max-h-32 overflow-y-auto"> <div className="text-sm bg-slate-50 dark:bg-slate-900 rounded p-2 max-h-32 overflow-y-auto">
<p>Dyad Version: {chatLogsData.debugInfo.dyadVersion}</p> <p>Dyad Version: {debugBundle.system.dyadVersion}</p>
<p>Platform: {chatLogsData.debugInfo.platform}</p> <p>Platform: {debugBundle.system.platform}</p>
<p>Architecture: {chatLogsData.debugInfo.architecture}</p> <p>Architecture: {debugBundle.system.architecture}</p>
<p> <p>
Node Version:{" "} Node Version:{" "}
{chatLogsData.debugInfo.nodeVersion || "Not available"} {debugBundle.system.nodeVersion || "Not available"}
</p> </p>
</div> </div>
</div> </div>
<details className="border rounded-md p-3">
<summary className="font-medium cursor-pointer">Settings</summary>
<div className="text-sm bg-slate-50 dark:bg-slate-900 rounded p-2 max-h-40 overflow-y-auto mt-2 font-mono whitespace-pre-wrap">
{JSON.stringify(debugBundle.settings, null, 2)}
</div>
</details>
<details className="border rounded-md p-3">
<summary className="font-medium cursor-pointer">
App Metadata
</summary>
<div className="text-sm bg-slate-50 dark:bg-slate-900 rounded p-2 max-h-40 overflow-y-auto mt-2 font-mono whitespace-pre-wrap">
{JSON.stringify(debugBundle.app, null, 2)}
</div>
</details>
<details className="border rounded-md p-3">
<summary className="font-medium cursor-pointer">
Custom Providers & Models
</summary>
<div className="text-sm bg-slate-50 dark:bg-slate-900 rounded p-2 max-h-40 overflow-y-auto mt-2 font-mono whitespace-pre-wrap">
{JSON.stringify(debugBundle.providers, null, 2)}
</div>
</details>
<details className="border rounded-md p-3">
<summary className="font-medium cursor-pointer">
MCP Servers
</summary>
<div className="text-sm bg-slate-50 dark:bg-slate-900 rounded p-2 max-h-40 overflow-y-auto mt-2 font-mono whitespace-pre-wrap">
{JSON.stringify(debugBundle.mcpServers, null, 2)}
</div>
</details>
</div> </div>
<div className="flex justify-between mt-4 pt-2 sticky bottom-0 bg-background"> <div className="flex justify-between mt-4 pt-2 sticky bottom-0 bg-background">
......
...@@ -3,8 +3,11 @@ import { platform, arch } from "os"; ...@@ -3,8 +3,11 @@ import { platform, arch } from "os";
import { readSettings } from "../../main/settings"; import { readSettings } from "../../main/settings";
import { createTypedHandler } from "./base"; import { createTypedHandler } from "./base";
import { systemContracts } from "../types/system"; import { systemContracts } from "../types/system";
import { miscContracts } from "../types/misc"; import { miscContracts, SESSION_DEBUG_SCHEMA_VERSION } from "../types/misc";
import type { SystemDebugInfo } from "../types/system"; import type { SystemDebugInfo } from "../types/system";
import type { SessionDebugBundle } from "../types/misc";
import type { UserSettings } from "@/lib/schemas";
import type { AiMessagesJsonV6 } from "../../db/schema";
import log from "electron-log"; import log from "electron-log";
import path from "path"; import path from "path";
...@@ -12,10 +15,15 @@ import fs from "fs"; ...@@ -12,10 +15,15 @@ import fs from "fs";
import { runShellCommand } from "../utils/runShellCommand"; import { runShellCommand } from "../utils/runShellCommand";
import { extractCodebase } from "../../utils/codebase"; import { extractCodebase } from "../../utils/codebase";
import { db } from "../../db"; import { db } from "../../db";
import { chats, apps } from "../../db/schema"; import {
chats,
apps,
language_model_providers,
language_models,
mcpServers,
} from "../../db/schema";
import { eq } from "drizzle-orm"; import { eq } from "drizzle-orm";
import { getDyadAppPath } from "../../paths/paths"; import { getDyadAppPath } from "../../paths/paths";
import { LargeLanguageModel } from "@/lib/schemas";
import { validateChatContext } from "../utils/context_paths_utils"; import { validateChatContext } from "../utils/context_paths_utils";
// Shared function to get system debug info // Shared function to get system debug info
...@@ -117,6 +125,140 @@ async function getSystemDebugInfo({ ...@@ -117,6 +125,140 @@ async function getSystemDebugInfo({
}; };
} }
function serializeModelForDebug(model: {
provider: string;
name: string;
customModelId?: number;
}): string {
return `${model.provider}:${model.name} | customId: ${model.customModelId}`;
}
/**
* Extracts non-sensitive settings for the debug bundle.
* All Secret fields (API keys, OAuth tokens) are excluded.
* Provider setup status is derived as boolean flags only.
*/
function sanitizeSettingsForDebug(settings: UserSettings) {
// Build provider setup status: { providerName: hasApiKey }
const providerSetupStatus: Record<string, boolean> = {};
if (settings.providerSettings) {
for (const [provider, providerSetting] of Object.entries(
settings.providerSettings,
)) {
providerSetupStatus[provider] = !!providerSetting?.apiKey?.value;
}
}
return {
selectedModel: {
name: settings.selectedModel.name,
provider: settings.selectedModel.provider,
customModelId: settings.selectedModel.customModelId,
},
selectedChatMode: settings.selectedChatMode ?? null,
defaultChatMode: settings.defaultChatMode ?? null,
autoApproveChanges: settings.autoApproveChanges ?? null,
enableDyadPro: settings.enableDyadPro ?? null,
thinkingBudget: settings.thinkingBudget ?? null,
maxChatTurnsInContext: settings.maxChatTurnsInContext ?? null,
enableAutoFixProblems: settings.enableAutoFixProblems ?? null,
enableNativeGit: settings.enableNativeGit ?? null,
enableAutoUpdate: settings.enableAutoUpdate,
releaseChannel: settings.releaseChannel,
runtimeMode2: settings.runtimeMode2 ?? null,
zoomLevel: settings.zoomLevel ?? null,
previewDeviceMode: settings.previewDeviceMode ?? null,
enableProLazyEditsMode: settings.enableProLazyEditsMode ?? null,
proLazyEditsMode: settings.proLazyEditsMode ?? null,
enableProSmartFilesContextMode:
settings.enableProSmartFilesContextMode ?? null,
enableProWebSearch: settings.enableProWebSearch ?? null,
proSmartContextOption: settings.proSmartContextOption ?? null,
enableSupabaseWriteSqlMigration:
settings.enableSupabaseWriteSqlMigration ?? null,
agentToolConsents: settings.agentToolConsents ?? null,
experiments: settings.experiments
? Object.fromEntries(
Object.entries(settings.experiments).filter(
([, v]) => typeof v === "boolean",
),
)
: null,
customNodePath: settings.customNodePath ?? null,
providerSetupStatus,
};
}
/**
* Strips base64 image data from AI SDK messages JSON.
* Replaces image content with "[stripped]" and adds _strippedByteLength metadata.
* This keeps the bundle size manageable while preserving message structure.
*
* Works on the raw JSON representation to avoid tight coupling with AI SDK types.
*/
function stripImagesFromAiMessagesJson(json: AiMessagesJsonV6 | null): unknown {
if (!json || !json.messages) return json;
// Work on raw JSON to avoid AI SDK type constraints when modifying content
const raw = JSON.parse(JSON.stringify(json));
for (const msg of raw.messages) {
if (!Array.isArray(msg.content)) continue;
for (let i = 0; i < msg.content.length; i++) {
const part = msg.content[i];
if (
part.type === "image" &&
typeof part.image === "string" &&
part.image.length > 200
) {
msg.content[i] = {
...part,
_strippedByteLength: part.image.length,
image: "[stripped]",
};
} else if (
part.type === "file" &&
typeof part.data === "string" &&
part.data.length > 200
) {
msg.content[i] = {
...part,
_strippedByteLength: part.data.length,
data: "[stripped]",
};
}
}
}
return raw;
}
/**
* Reads application logs from the electron-log file.
*/
function readAppLogs(linesOfLogs: number, level: "warn" | "info"): string {
try {
const logPath = log.transports.file.getFile().path;
if (!fs.existsSync(logPath)) return "";
const logContent = fs.readFileSync(logPath, "utf8");
const logLines = logContent.split("\n").filter((line) => {
if (level === "info") return true;
const logLevelRegex = /\[.*?\] \[(\w+)\]/;
const match = line.match(logLevelRegex);
if (!match) return true;
const logLevel = match[1];
if (level === "warn") {
return logLevel === "warn" || logLevel === "error";
}
return true;
});
return logLines.slice(-linesOfLogs).join("\n");
} catch (err) {
console.error("Failed to read log file:", err);
return `Error reading logs: ${err}`;
}
}
export function registerDebugHandlers() { export function registerDebugHandlers() {
createTypedHandler(systemContracts.getSystemDebugInfo, async () => { createTypedHandler(systemContracts.getSystemDebugInfo, async () => {
console.log("IPC: get-system-debug-info called"); console.log("IPC: get-system-debug-info called");
...@@ -126,18 +268,40 @@ export function registerDebugHandlers() { ...@@ -126,18 +268,40 @@ export function registerDebugHandlers() {
}); });
}); });
createTypedHandler(miscContracts.getChatLogs, async (_, chatId) => { createTypedHandler(miscContracts.getSessionDebugBundle, async (_, chatId) => {
console.log(`IPC: get-chat-logs called for chat ${chatId}`); console.log(`IPC: get-session-debug-bundle called for chat ${chatId}`);
try { try {
// We can retrieve a lot more lines here because we're not limited by the const settings = readSettings();
// GitHub issue URL length limit.
const debugInfo = await getSystemDebugInfo({ // Get Dyad version
linesOfLogs: 1_000, const packageJsonPath = path.resolve(
level: "info", __dirname,
}); "..",
"..",
"package.json",
);
let dyadVersion = "unknown";
try {
const packageJson = JSON.parse(
fs.readFileSync(packageJsonPath, "utf8"),
);
dyadVersion = packageJson.version;
} catch (err) {
console.error("Failed to read package.json:", err);
}
// Get chat data from database // Get runtime info in parallel
const [nodeVersion, pnpmVersion, nodePathResult] = await Promise.all([
runShellCommand("node --version").catch(() => null),
runShellCommand("pnpm --version").catch(() => null),
(platform() === "win32"
? runShellCommand("where.exe node")
: runShellCommand("which node")
).catch(() => null),
]);
// Get chat with full messages from database
const chatRecord = await db.query.chats.findFirst({ const chatRecord = await db.query.chats.findFirst({
where: eq(chats.id, chatId), where: eq(chats.id, chatId),
with: { with: {
...@@ -151,18 +315,6 @@ export function registerDebugHandlers() { ...@@ -151,18 +315,6 @@ export function registerDebugHandlers() {
throw new Error(`Chat with ID ${chatId} not found`); throw new Error(`Chat with ID ${chatId} not found`);
} }
// Format the chat to match the Chat interface
const chat = {
id: chatRecord.id,
title: chatRecord.title || "Untitled Chat",
messages: chatRecord.messages.map((msg) => ({
id: msg.id,
role: msg.role,
content: msg.content,
approvalState: msg.approvalState,
})),
};
// Get app data from database // Get app data from database
const app = await db.query.apps.findFirst({ const app = await db.query.apps.findFirst({
where: eq(apps.id, chatRecord.appId), where: eq(apps.id, chatRecord.appId),
...@@ -172,22 +324,123 @@ export function registerDebugHandlers() { ...@@ -172,22 +324,123 @@ export function registerDebugHandlers() {
throw new Error(`App with ID ${chatRecord.appId} not found`); throw new Error(`App with ID ${chatRecord.appId} not found`);
} }
// Extract codebase // Query custom providers, custom models, and MCP servers in parallel
const appPath = getDyadAppPath(app.path); const [customProviders, customModels, mcpServerRecords, codebase] =
const codebase = ( await Promise.all([
await extractCodebase({ db.select().from(language_model_providers),
appPath, db.select().from(language_models),
db.select().from(mcpServers),
extractCodebase({
appPath: getDyadAppPath(app.path),
chatContext: validateChatContext(app.chatContext), chatContext: validateChatContext(app.chatContext),
}) }).then((result) => result.formattedOutput),
).formattedOutput; ]);
// Read logs
const logs = readAppLogs(1_000, "info");
// Build the bundle
const bundle: SessionDebugBundle = {
schemaVersion: SESSION_DEBUG_SCHEMA_VERSION,
exportedAt: new Date().toISOString(),
system: {
dyadVersion,
platform: process.platform,
architecture: arch(),
nodeVersion,
pnpmVersion,
nodePath: nodePathResult,
electronVersion: process.versions.electron ?? "unknown",
telemetryId:
settings.telemetryConsent === "opted_out"
? null
: settings.telemetryUserId || "unknown",
},
settings: sanitizeSettingsForDebug(settings),
app: {
id: app.id,
name: app.name,
path: app.path,
createdAt: app.createdAt.toISOString(),
updatedAt: app.updatedAt.toISOString(),
githubOrg: app.githubOrg,
githubRepo: app.githubRepo,
githubBranch: app.githubBranch,
supabaseProjectId: app.supabaseProjectId,
supabaseOrganizationSlug: app.supabaseOrganizationSlug,
neonProjectId: app.neonProjectId,
vercelProjectId: app.vercelProjectId,
vercelProjectName: app.vercelProjectName,
vercelDeploymentUrl: app.vercelDeploymentUrl,
installCommand: app.installCommand,
startCommand: app.startCommand,
chatContext: app.chatContext ?? null,
themeId: app.themeId,
},
chat: {
id: chatRecord.id,
appId: chatRecord.appId,
title: chatRecord.title,
initialCommitHash: chatRecord.initialCommitHash,
createdAt: chatRecord.createdAt.toISOString(),
messages: chatRecord.messages.map((msg) => ({
id: msg.id,
role: msg.role,
content: msg.content,
createdAt: msg.createdAt.toISOString(),
aiMessagesJson: stripImagesFromAiMessagesJson(
msg.aiMessagesJson ?? null,
),
model: msg.model ?? null,
totalTokens: msg.maxTokensUsed ?? null,
approvalState: msg.approvalState ?? null,
sourceCommitHash: msg.sourceCommitHash ?? null,
commitHash: msg.commitHash ?? null,
requestId: msg.requestId ?? null,
usingFreeAgentModeQuota: msg.usingFreeAgentModeQuota ?? null,
})),
},
providers: {
customProviders: customProviders.map((p) => ({
id: p.id,
name: p.name,
hasApiBaseUrl: !!p.api_base_url,
envVarName: p.env_var_name,
})),
customModels: customModels.map((m) => ({
id: m.id,
displayName: m.displayName,
apiName: m.apiName,
builtinProviderId: m.builtinProviderId,
customProviderId: m.customProviderId,
maxOutputTokens: m.max_output_tokens,
contextWindow: m.context_window,
})),
},
mcpServers: mcpServerRecords.map((s) => ({
id: s.id,
name: s.name,
transport: s.transport,
command: s.command,
args: s.args,
url: s.url,
enabled: s.enabled,
// envJson and headersJson intentionally excluded (may contain secrets)
})),
return {
debugInfo,
chat,
codebase, codebase,
logs,
}; };
return bundle;
} catch (error) { } catch (error) {
console.error(`Error in get-chat-logs:`, error); console.error(`Error in get-session-debug-bundle:`, error);
throw error; throw error;
} }
}); });
...@@ -208,7 +461,3 @@ export function registerDebugHandlers() { ...@@ -208,7 +461,3 @@ export function registerDebugHandlers() {
clipboard.writeImage(image); clipboard.writeImage(image);
}); });
} }
function serializeModelForDebug(model: LargeLanguageModel): string {
return `${model.provider}:${model.name} | customId: ${model.customModelId}`;
}
...@@ -280,7 +280,12 @@ export type { ...@@ -280,7 +280,12 @@ export type {
export type { SecurityReviewResult } from "./security"; export type { SecurityReviewResult } from "./security";
// Misc types // Misc types
export type { ChatLogsData, DeepLinkData, AppOutput, EnvVar } from "./misc"; export type {
SessionDebugBundle,
DeepLinkData,
AppOutput,
EnvVar,
} from "./misc";
// Free agent quota types // Free agent quota types
export type { FreeAgentQuotaStatus } from "./free_agent_quota"; export type { FreeAgentQuotaStatus } from "./free_agent_quota";
......
...@@ -41,39 +41,241 @@ export const SetAppEnvVarsParamsSchema = z.object({ ...@@ -41,39 +41,241 @@ export const SetAppEnvVarsParamsSchema = z.object({
}); });
// ============================================================================= // =============================================================================
// Chat Logs Schemas // Session Debug Bundle Schemas
// ============================================================================= // =============================================================================
export const ChatLogsDataSchema = z.object({ /**
debugInfo: z.object({ * Schema version for the session debug bundle format.
nodeVersion: z.string().nullable(), * Bump this when making breaking changes to the schema.
pnpmVersion: z.string().nullable(), */
nodePath: z.string().nullable(), export const SESSION_DEBUG_SCHEMA_VERSION = 2;
telemetryId: z.string(),
telemetryConsent: z.string(), // -- System info --
telemetryUrl: z.string(),
const DebugSystemInfoSchema = z.object({
/** Dyad application version (from package.json) */
dyadVersion: z.string(), dyadVersion: z.string(),
/** OS platform: "darwin", "win32", "linux" */
platform: z.string(), platform: z.string(),
/** CPU architecture: "x64", "arm64" */
architecture: z.string(), architecture: z.string(),
logs: z.string(), /** Node.js version, or null if not found */
selectedLanguageModel: z.string(), nodeVersion: z.string().nullable(),
/** pnpm version, or null if not found */
pnpmVersion: z.string().nullable(),
/** Resolved path to the node binary, or null */
nodePath: z.string().nullable(),
/** Electron version */
electronVersion: z.string(),
/** Telemetry ID for cross-referencing server-side logs. Null if user opted out. */
telemetryId: z.string().nullable(),
});
// -- Non-sensitive settings snapshot --
const DebugSettingsSchema = z.object({
/** Currently selected language model */
selectedModel: z.object({
name: z.string(),
provider: z.string(),
customModelId: z.number().optional(),
}), }),
chat: z.object({ /** Active chat mode for the session */
selectedChatMode: z.string().nullable(),
/** Default chat mode preference */
defaultChatMode: z.string().nullable(),
/** Whether changes are auto-approved without review */
autoApproveChanges: z.boolean().nullable(),
/** Whether Dyad Pro is enabled */
enableDyadPro: z.boolean().nullable(),
/** Thinking budget level: "low" | "medium" | "high" */
thinkingBudget: z.string().nullable(),
/** Max chat turns kept in context window */
maxChatTurnsInContext: z.number().nullable(),
/** Whether auto-fix problems is enabled */
enableAutoFixProblems: z.boolean().nullable(),
/** Whether native git is enabled */
enableNativeGit: z.boolean().nullable(),
/** Whether auto-update is enabled */
enableAutoUpdate: z.boolean(),
/** Release channel: "stable" | "beta" */
releaseChannel: z.string(),
/** Runtime mode: "host" | "docker" */
runtimeMode2: z.string().nullable(),
/** UI zoom level */
zoomLevel: z.string().nullable(),
/** Preview device mode: "desktop" | "tablet" | "mobile" */
previewDeviceMode: z.string().nullable(),
/** Whether turbo edits mode is enabled */
enableProLazyEditsMode: z.boolean().nullable(),
/** Turbo edits mode variant: "off" | "v1" | "v2" */
proLazyEditsMode: z.string().nullable(),
/** Whether smart files context mode is enabled (Pro) */
enableProSmartFilesContextMode: z.boolean().nullable(),
/** Whether web search is enabled (Pro) */
enableProWebSearch: z.boolean().nullable(),
/** Smart context option: "balanced" | "conservative" | "deep" */
proSmartContextOption: z.string().nullable(),
/** Whether Supabase write SQL migration is enabled */
enableSupabaseWriteSqlMigration: z.boolean().nullable(),
/** Agent tool consent settings per tool */
agentToolConsents: z.record(z.string(), z.string()).nullable(),
/** Experiment flags */
experiments: z.record(z.string(), z.boolean()).nullable(),
/** Custom node path override */
customNodePath: z.string().nullable(),
/** Map of provider ID -> whether configured (has API key). No secrets. */
providerSetupStatus: z.record(z.string(), z.boolean()),
});
// -- App metadata --
const DebugAppInfoSchema = z.object({
id: z.number(), id: z.number(),
title: z.string(), name: z.string(),
messages: z.array( /** Relative app path (not full filesystem path) */
z.object({ path: z.string(),
createdAt: z.string(),
updatedAt: z.string(),
// Integration identifiers (non-secret)
githubOrg: z.string().nullable(),
githubRepo: z.string().nullable(),
githubBranch: z.string().nullable(),
supabaseProjectId: z.string().nullable(),
supabaseOrganizationSlug: z.string().nullable(),
neonProjectId: z.string().nullable(),
vercelProjectId: z.string().nullable(),
vercelProjectName: z.string().nullable(),
vercelDeploymentUrl: z.string().nullable(),
// Dev commands
installCommand: z.string().nullable(),
startCommand: z.string().nullable(),
// Chat context configuration
chatContext: z.any().nullable(),
// Theme
themeId: z.string().nullable(),
});
// -- Message with full debug detail --
const DebugMessageSchema = z.object({
id: z.number(), id: z.number(),
role: z.string(), role: z.enum(["user", "assistant"]),
/** Human-readable message text */
content: z.string(), content: z.string(),
approvalState: z.string().nullable().optional(), /** ISO 8601 timestamp */
createdAt: z.string(),
/**
* Full AI SDK structured message data (tool calls, image refs, multi-turn state).
* Base64 image data is stripped and replaced with:
* { type: "image", image: "[stripped]", mediaType: "...", _strippedByteLength: N }
*/
aiMessagesJson: z.any().nullable(),
/** Model name used to generate this response (assistant messages only) */
model: z.string().nullable(),
/** Total tokens used for this response (assistant messages only) */
totalTokens: z.number().nullable(),
/** Approval state: "approved" | "rejected" | null */
approvalState: z.enum(["approved", "rejected"]).nullable(),
/** Git commit hash of codebase when this message was created */
sourceCommitHash: z.string().nullable(),
/** Git commit hash of codebase after changes from this message were applied */
commitHash: z.string().nullable(),
/** Pro request UUID for billing/tracking */
requestId: z.string().nullable(),
/** Whether this message used the free agent mode quota */
usingFreeAgentModeQuota: z.boolean().nullable(),
});
// -- Chat with messages --
const DebugChatSchema = z.object({
id: z.number(),
appId: z.number(),
title: z.string().nullable(),
/** Git commit hash at start of this chat */
initialCommitHash: z.string().nullable(),
/** ISO 8601 timestamp */
createdAt: z.string(),
messages: z.array(DebugMessageSchema),
});
// -- Provider / model configuration (no secrets) --
const DebugProvidersSchema = z.object({
/** Custom provider definitions from language_model_providers table */
customProviders: z.array(
z.object({
id: z.string(),
name: z.string(),
hasApiBaseUrl: z.boolean(),
envVarName: z.string().nullable(),
}), }),
), ),
/** Custom model definitions from language_models table */
customModels: z.array(
z.object({
id: z.number(),
displayName: z.string(),
apiName: z.string(),
builtinProviderId: z.string().nullable(),
customProviderId: z.string().nullable(),
maxOutputTokens: z.number().nullable(),
contextWindow: z.number().nullable(),
}), }),
),
});
// -- MCP server configuration (no env/header secrets) --
const DebugMcpServerSchema = z.object({
id: z.number(),
name: z.string(),
transport: z.string(),
command: z.string().nullable(),
args: z.array(z.string()).nullable(),
url: z.string().nullable(),
enabled: z.boolean(),
// NOTE: envJson and headersJson are intentionally EXCLUDED (may contain secrets)
});
// -- Top-level bundle --
/**
* Complete session debug bundle for upload.
*
* Contains all non-sensitive data needed to debug a chat session:
* system info, user settings, app config, full chat messages with
* AI SDK JSON, provider/model setup, MCP servers, codebase snapshot,
* and application logs.
*
* Sensitive data (API keys, OAuth tokens, MCP env vars) is stripped.
* Base64 image data in AI SDK messages is replaced with placeholders.
*/
export const SessionDebugBundleSchema = z.object({
/** Schema version number. Bump on breaking changes. */
schemaVersion: z.number(),
/** ISO 8601 timestamp of when this bundle was exported */
exportedAt: z.string(),
/** Runtime environment info */
system: DebugSystemInfoSchema,
/** Non-sensitive user settings snapshot */
settings: DebugSettingsSchema,
/** App configuration and integration metadata */
app: DebugAppInfoSchema,
/** Chat with full message history including AI SDK JSON */
chat: DebugChatSchema,
/** Custom provider and model definitions (no secrets) */
providers: DebugProvidersSchema,
/** MCP server configurations (no env/header secrets) */
mcpServers: z.array(DebugMcpServerSchema),
/** Formatted codebase snapshot */
codebase: z.string(), codebase: z.string(),
/** Application logs (last 1000 lines) */
logs: z.string(),
}); });
export type ChatLogsData = z.infer<typeof ChatLogsDataSchema>; export type SessionDebugBundle = z.infer<typeof SessionDebugBundleSchema>;
// ============================================================================= // =============================================================================
// Deep Link Schemas // Deep Link Schemas
...@@ -133,11 +335,11 @@ export const miscContracts = { ...@@ -133,11 +335,11 @@ export const miscContracts = {
output: z.void(), output: z.void(),
}), }),
// Chat logs // Session debug bundle
getChatLogs: defineContract({ getSessionDebugBundle: defineContract({
channel: "get-chat-logs", channel: "get-session-debug-bundle",
input: z.number(), // chatId input: z.number(), // chatId
output: ChatLogsDataSchema, output: SessionDebugBundleSchema,
}), }),
// Console logs // Console logs
......
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论