mirror of
https://github.com/zhom/donutbrowser.git
synced 2026-11-14 16:17:18 +01:00
feat: feedback profile picker
This commit is contained in:
1 parent
9cee1188f3
commit
2c66a3d5aa
17 files changed
+254
-3
No files matched your search
@@ -7365,6 +7365,17 @@ test("feedback goes from the More menu to the team, with the logs the person pre
|
||||
}),
|
||||
/FEEDBACK_EMAIL_INVALID/,
|
||||
);
|
||||
assert.match(
|
||||
await app.invokeError("send_feedback", {
|
||||
kind: "bug",
|
||||
message: "Hangs",
|
||||
includeLogs: false,
|
||||
email: null,
|
||||
locale: "en",
|
||||
profileId: "00000000-0000-4000-8000-000000000000",
|
||||
}),
|
||||
/PROFILE_NOT_FOUND/,
|
||||
);
|
||||
assert.equal(capture.received.length, 0);
|
||||
await app.waitFor(
|
||||
async () =>
|
||||
@@ -7459,6 +7470,24 @@ test("feedback goes from the More menu to the team, with the logs the person pre
|
||||
'[data-slot="feedback-email"]',
|
||||
"me@example.com",
|
||||
);
|
||||
// The report may name one profile; only its id leaves the app.
|
||||
assert.equal(
|
||||
await app.execute(
|
||||
`return document.querySelector('#feedback-profile')?.innerText.trim();`,
|
||||
),
|
||||
en.agent.composer.noProfile,
|
||||
);
|
||||
await app.clickSelector("#feedback-profile");
|
||||
await app.clickSelector(
|
||||
`[data-slot="agent-profile-option"][data-profile-id="${profile.id}"]`,
|
||||
);
|
||||
await app.waitFor(
|
||||
async () =>
|
||||
(await app.execute(
|
||||
`return document.querySelector('#feedback-profile')?.innerText.trim();`,
|
||||
)) === "Secret Shop Account",
|
||||
{ description: "the chosen profile on the picker" },
|
||||
);
|
||||
await app.capture("feedback-compose");
|
||||
await app.clickSelector('[data-slot="feedback-send"]');
|
||||
await app.waitFor(
|
||||
@@ -7492,6 +7521,8 @@ test("feedback goes from the More menu to the team, with the logs the person pre
|
||||
assert.equal(sent.body.locale, "en");
|
||||
assert.equal(typeof sent.body.appVersion, "string");
|
||||
assert.ok(sent.body.logs.includes("Donut started"));
|
||||
assert.equal(sent.body.profileId, profile.id);
|
||||
assert.ok(!JSON.stringify(sent.body).includes("Secret Shop Account"));
|
||||
|
||||
await app.clickSelector('[data-slot="feedback-sent"] button');
|
||||
await app.waitFor(
|
||||
@@ -7528,6 +7559,11 @@ test("feedback goes from the More menu to the team, with the logs the person pre
|
||||
assert.equal(await feedbackMessage(app), "Dark tray icon");
|
||||
assert.equal(capture.received.length, 2);
|
||||
assert.equal(capture.received[1].body.kind, "idea");
|
||||
assert.equal(
|
||||
capture.received[1].body.profileId,
|
||||
undefined,
|
||||
"a sent draft does not keep its profile",
|
||||
);
|
||||
assert.equal(
|
||||
capture.received[1].body.logs,
|
||||
undefined,
|
||||
|
||||
@@ -77,6 +77,8 @@ pub struct Feedback {
|
||||
pub logs: Option<String>,
|
||||
pub agent_client: Option<String>,
|
||||
pub context: Option<serde_json::Value>,
|
||||
/// The profile the report is about. Only its id leaves the app.
|
||||
pub profile_id: Option<uuid::Uuid>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
@@ -98,6 +100,8 @@ struct Payload<'a> {
|
||||
agent_client: Option<&'a str>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
context: Option<&'a serde_json::Value>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
profile_id: Option<uuid::Uuid>,
|
||||
}
|
||||
|
||||
impl<'a> Payload<'a> {
|
||||
@@ -118,6 +122,7 @@ impl<'a> Payload<'a> {
|
||||
locale: feedback.locale.as_deref(),
|
||||
agent_client: feedback.agent_client.as_deref(),
|
||||
context: feedback.context.as_ref(),
|
||||
profile_id: feedback.profile_id,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -172,6 +177,32 @@ pub fn validate_email(email: Option<&str>) -> Result<Option<String>, String> {
|
||||
}
|
||||
}
|
||||
|
||||
/// The profile a report is about: blank is none, anything else must be the id
|
||||
/// of an existing profile.
|
||||
pub fn validate_profile_id(
|
||||
profile_id: Option<&str>,
|
||||
exists: impl FnOnce(&uuid::Uuid) -> Result<bool, String>,
|
||||
) -> Result<Option<uuid::Uuid>, String> {
|
||||
let Some(profile_id) = profile_id.map(str::trim).filter(|id| !id.is_empty()) else {
|
||||
return Ok(None);
|
||||
};
|
||||
let not_found = || crate::backend_error("PROFILE_NOT_FOUND");
|
||||
let profile_id = uuid::Uuid::parse_str(profile_id).map_err(|_| not_found())?;
|
||||
if exists(&profile_id)? {
|
||||
Ok(Some(profile_id))
|
||||
} else {
|
||||
Err(not_found())
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether a profile with this id is on this device.
|
||||
pub fn profile_exists(profile_id: &uuid::Uuid) -> Result<bool, String> {
|
||||
crate::profile::ProfileManager::instance()
|
||||
.lookup_profile(profile_id)
|
||||
.map(|profile| profile.is_some())
|
||||
.map_err(|e| crate::backend_error_with_detail("INTERNAL_ERROR", e))
|
||||
}
|
||||
|
||||
/// The log excerpt feedback carries: anonymous facts, then the newest app and
|
||||
/// proxy worker log lines, redacted, at most `MAX_LOG_BYTES`
|
||||
/// (`diagnostics::excerpt`).
|
||||
@@ -342,10 +373,12 @@ pub async fn send_feedback(
|
||||
include_logs: bool,
|
||||
email: Option<String>,
|
||||
locale: Option<String>,
|
||||
profile_id: Option<String>,
|
||||
) -> Result<FeedbackReceipt, String> {
|
||||
let kind = FeedbackKind::parse(&kind).ok_or_else(|| crate::backend_error("FEEDBACK_INVALID"))?;
|
||||
let message = validate_message(&message)?;
|
||||
let email = validate_email(email.as_deref())?;
|
||||
let profile_id = validate_profile_id(profile_id.as_deref(), profile_exists)?;
|
||||
let logs = if include_logs {
|
||||
Some(
|
||||
log_excerpt(
|
||||
@@ -366,6 +399,7 @@ pub async fn send_feedback(
|
||||
logs,
|
||||
agent_client: None,
|
||||
context: None,
|
||||
profile_id,
|
||||
})
|
||||
.await?;
|
||||
tauri::async_runtime::spawn_blocking(crate::settings_manager::note_feedback_sent);
|
||||
@@ -443,6 +477,51 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_profile_is_optional_but_must_exist() {
|
||||
let id = uuid::Uuid::new_v4();
|
||||
let never = |_: &uuid::Uuid| -> Result<bool, String> { panic!("not looked up") };
|
||||
assert_eq!(validate_profile_id(None, never).unwrap(), None);
|
||||
assert_eq!(validate_profile_id(Some(" "), never).unwrap(), None);
|
||||
assert_eq!(
|
||||
validate_profile_id(Some(&format!(" {id} ")), |seen| Ok(*seen == id)).unwrap(),
|
||||
Some(id)
|
||||
);
|
||||
assert_eq!(
|
||||
code_of(&validate_profile_id(Some("not-a-uuid"), never).unwrap_err()),
|
||||
"PROFILE_NOT_FOUND"
|
||||
);
|
||||
assert_eq!(
|
||||
code_of(&validate_profile_id(Some(&id.to_string()), |_| Ok(false)).unwrap_err()),
|
||||
"PROFILE_NOT_FOUND"
|
||||
);
|
||||
let failed = validate_profile_id(Some(&id.to_string()), |_| {
|
||||
Err(crate::backend_error("INTERNAL_ERROR"))
|
||||
});
|
||||
assert_eq!(code_of(&failed.unwrap_err()), "INTERNAL_ERROR");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_payload_names_a_profile_by_id_only() {
|
||||
let id = uuid::Uuid::new_v4();
|
||||
let mut feedback = Feedback {
|
||||
kind: FeedbackKind::Bug,
|
||||
source: FeedbackSource::Person,
|
||||
message: "the proxy check hangs".to_string(),
|
||||
email: None,
|
||||
locale: None,
|
||||
logs: None,
|
||||
agent_client: None,
|
||||
context: None,
|
||||
profile_id: Some(id),
|
||||
};
|
||||
let sent = serde_json::to_value(Payload::of(&feedback)).unwrap();
|
||||
assert_eq!(sent["profileId"], id.to_string());
|
||||
feedback.profile_id = None;
|
||||
let sent = serde_json::to_value(Payload::of(&feedback)).unwrap();
|
||||
assert!(sent.get("profileId").is_none(), "{sent}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_agent_may_send_a_bounded_amount_per_hour() {
|
||||
let mut sends = VecDeque::new();
|
||||
@@ -510,6 +589,7 @@ mod tests {
|
||||
logs: Some("line\n".to_string()),
|
||||
agent_client: None,
|
||||
context: None,
|
||||
profile_id: None,
|
||||
};
|
||||
let body = serde_json::to_vec(&Payload::of(&feedback)).unwrap();
|
||||
let receipt = post(
|
||||
@@ -544,6 +624,7 @@ mod tests {
|
||||
logs: None,
|
||||
agent_client: Some("Claude Code 2.1.0".to_string()),
|
||||
context: Some(serde_json::json!({ "recentCalls": [] })),
|
||||
profile_id: None,
|
||||
};
|
||||
let sent = serde_json::to_value(Payload::of(&feedback)).unwrap();
|
||||
assert_eq!(sent["source"], "agent");
|
||||
@@ -569,6 +650,7 @@ mod tests {
|
||||
logs: Some(String::new()),
|
||||
agent_client: None,
|
||||
context: None,
|
||||
profile_id: None,
|
||||
};
|
||||
let body = serde_json::to_vec(&Payload::of(&feedback)).unwrap();
|
||||
let error = post(format!("{}/api/feedback", server.uri()), body, None)
|
||||
|
||||
@@ -6998,7 +6998,8 @@ impl McpServer {
|
||||
"enum": ["bug", "idea", "praise", "other"],
|
||||
"description": "bug for something broken, idea for something missing, praise for something that worked well (default: other)"
|
||||
},
|
||||
"include_logs": { "type": "boolean", "description": "Attach the recent app logs (default: false)" }
|
||||
"include_logs": { "type": "boolean", "description": "Attach the recent app logs (default: false)" },
|
||||
"profile_id": { "type": "string", "description": "The id of the profile the feedback is about, if any. Only the id is sent, never the profile's name or data" }
|
||||
},
|
||||
"required": ["message"]
|
||||
}),
|
||||
@@ -12814,6 +12815,16 @@ impl McpServer {
|
||||
.get("include_logs")
|
||||
.and_then(serde_json::Value::as_bool)
|
||||
.unwrap_or(false);
|
||||
let profile_id = match arguments.get("profile_id") {
|
||||
None | Some(serde_json::Value::Null) => None,
|
||||
Some(value) => {
|
||||
let id = value
|
||||
.as_str()
|
||||
.ok_or_else(|| Self::invalid_argument("profile_id must be a string"))?;
|
||||
crate::feedback::validate_profile_id(Some(id), crate::feedback::profile_exists)
|
||||
.map_err(|_| Self::invalid_argument("profile_id is not the id of a profile"))?
|
||||
}
|
||||
};
|
||||
|
||||
if !crate::feedback::agent_send_allowed() {
|
||||
return Err(McpError {
|
||||
@@ -12857,6 +12868,7 @@ impl McpServer {
|
||||
agent_client,
|
||||
context: (!recent_calls.is_empty())
|
||||
.then(|| serde_json::json!({ "recentCalls": recent_calls })),
|
||||
profile_id,
|
||||
})
|
||||
.await
|
||||
.map_err(|error| {
|
||||
@@ -17166,6 +17178,11 @@ mod tests {
|
||||
5,
|
||||
serde_json::json!({ "message": "x".repeat(crate::feedback::MAX_MESSAGE_CHARS + 1) }),
|
||||
),
|
||||
(
|
||||
6,
|
||||
serde_json::json!({ "message": "ok", "profile_id": "not-a-uuid" }),
|
||||
),
|
||||
(7, serde_json::json!({ "message": "ok", "profile_id": 7 })),
|
||||
] {
|
||||
let refused = send(&session, id, arguments).await;
|
||||
assert_eq!(
|
||||
|
||||
@@ -3357,6 +3357,7 @@ export default function Home() {
|
||||
setFeedbackDialogOpen(false);
|
||||
}}
|
||||
accountEmail={cloudUser?.email ?? null}
|
||||
profiles={profiles}
|
||||
/>
|
||||
|
||||
<PreLaunchGateDialog
|
||||
|
||||
@@ -190,7 +190,10 @@ export function TaskStatusLabel({
|
||||
);
|
||||
}
|
||||
|
||||
/** Choose any number of profiles, or exactly one, with search. */
|
||||
/**
|
||||
* Choose any number of profiles, or exactly one, with search. `noneLabel`
|
||||
* makes a single choice optional: it names the empty choice and lists it first.
|
||||
*/
|
||||
export function ProfilePicker({
|
||||
id,
|
||||
profiles,
|
||||
@@ -198,6 +201,7 @@ export function ProfilePicker({
|
||||
onChange,
|
||||
disabled,
|
||||
single = false,
|
||||
noneLabel,
|
||||
className,
|
||||
}: {
|
||||
id?: string;
|
||||
@@ -206,6 +210,7 @@ export function ProfilePicker({
|
||||
onChange: (ids: string[]) => void;
|
||||
disabled?: boolean;
|
||||
single?: boolean;
|
||||
noneLabel?: string;
|
||||
className?: string;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
@@ -214,7 +219,8 @@ export function ProfilePicker({
|
||||
const known = profiles.filter((profile) => chosen.has(profile.id));
|
||||
const label =
|
||||
known.length === 0
|
||||
? t(single ? "agent.chat.chooseProfile" : "agent.tasks.chooseProfiles")
|
||||
? (noneLabel ??
|
||||
t(single ? "agent.chat.chooseProfile" : "agent.tasks.chooseProfiles"))
|
||||
: known.length === 1
|
||||
? known[0].name
|
||||
: t("agent.tasks.profileCount", { count: known.length });
|
||||
@@ -258,6 +264,26 @@ export function ProfilePicker({
|
||||
<CommandList className="max-h-64">
|
||||
<CommandEmpty>{t("agent.tasks.noProfileMatch")}</CommandEmpty>
|
||||
<CommandGroup>
|
||||
{single && noneLabel && (
|
||||
<CommandItem
|
||||
value="__none__"
|
||||
data-slot="agent-profile-none"
|
||||
onSelect={() => {
|
||||
onChange([]);
|
||||
setOpen(false);
|
||||
}}
|
||||
className="text-muted-foreground"
|
||||
>
|
||||
<LuCheck
|
||||
className={cn(
|
||||
"size-3.5",
|
||||
known.length === 0 ? "opacity-100" : "opacity-0",
|
||||
)}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
{noneLabel}
|
||||
</CommandItem>
|
||||
)}
|
||||
{!single && (
|
||||
<CommandItem
|
||||
value="__all__"
|
||||
|
||||
@@ -18,6 +18,7 @@ import {
|
||||
LuMessageSquareMore,
|
||||
LuSend,
|
||||
} from "react-icons/lu";
|
||||
import { ProfilePicker } from "@/components/agent-tasks/parts";
|
||||
import { LoadingButton } from "@/components/loading-button";
|
||||
import { AnimatedDisclosureChevron } from "@/components/ui/animated-disclosure";
|
||||
import { AnimatedSwitch } from "@/components/ui/animated-switch";
|
||||
@@ -41,6 +42,7 @@ import { MOTION_SPRING_POSITION } from "@/lib/motion";
|
||||
import { isMacOS } from "@/lib/platform";
|
||||
import { showSuccessToast } from "@/lib/toast-utils";
|
||||
import { cn } from "@/lib/utils";
|
||||
import type { BrowserProfile } from "@/types";
|
||||
import { RippleButton } from "./ui/ripple";
|
||||
|
||||
/** Mirrors `feedback::MAX_MESSAGE_CHARS`. */
|
||||
@@ -76,6 +78,8 @@ interface FeedbackDialogProps {
|
||||
onClose: () => void;
|
||||
/** The signed-in account's email; replies go there. */
|
||||
accountEmail: string | null;
|
||||
/** The profiles the report may be about; only the chosen id is sent. */
|
||||
profiles: BrowserProfile[];
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -88,6 +92,7 @@ export function FeedbackDialog({
|
||||
isOpen,
|
||||
onClose,
|
||||
accountEmail,
|
||||
profiles,
|
||||
}: FeedbackDialogProps) {
|
||||
const { t, i18n } = useTranslation();
|
||||
const animate = useMotionAllowed();
|
||||
@@ -97,6 +102,7 @@ export function FeedbackDialog({
|
||||
const [includeLogs, setIncludeLogs] = useState(true);
|
||||
const [logsChosen, setLogsChosen] = useState(false);
|
||||
const [email, setEmail] = useState("");
|
||||
const [profileId, setProfileId] = useState<string | null>(null);
|
||||
const [previewOpen, setPreviewOpen] = useState(false);
|
||||
const [logs, setLogs] = useState<string | null>(null);
|
||||
const [logsLoading, setLogsLoading] = useState(false);
|
||||
@@ -173,11 +179,18 @@ export function FeedbackDialog({
|
||||
setLogsChosen(false);
|
||||
setPreviewOpen(false);
|
||||
setLogs(null);
|
||||
setProfileId(null);
|
||||
}
|
||||
setError(null);
|
||||
onClose();
|
||||
}, [onClose, sent]);
|
||||
|
||||
useEffect(() => {
|
||||
if (profileId && !profiles.some((profile) => profile.id === profileId)) {
|
||||
setProfileId(null);
|
||||
}
|
||||
}, [profileId, profiles]);
|
||||
|
||||
const canSend = message.trim().length > 0 && !sending;
|
||||
|
||||
const send = useCallback(async () => {
|
||||
@@ -191,6 +204,7 @@ export function FeedbackDialog({
|
||||
includeLogs,
|
||||
email: accountEmail ? null : email.trim() || null,
|
||||
locale: i18n.language,
|
||||
profileId,
|
||||
});
|
||||
setSent({
|
||||
logs: includeLogs,
|
||||
@@ -209,6 +223,7 @@ export function FeedbackDialog({
|
||||
includeLogs,
|
||||
kind,
|
||||
message,
|
||||
profileId,
|
||||
sending,
|
||||
t,
|
||||
]);
|
||||
@@ -437,6 +452,36 @@ export function FeedbackDialog({
|
||||
</div>
|
||||
)}
|
||||
|
||||
{profiles.length > 0 && (
|
||||
<div className="grid gap-1.5">
|
||||
<label
|
||||
htmlFor="feedback-profile"
|
||||
className="flex items-baseline gap-1.5 text-sm font-medium"
|
||||
>
|
||||
{t("feedback.profile.label")}
|
||||
<span className="text-xs font-normal text-muted-foreground">
|
||||
{t("feedback.email.optional")}
|
||||
</span>
|
||||
</label>
|
||||
<div className="flex min-w-0 items-center gap-3">
|
||||
<ProfilePicker
|
||||
id="feedback-profile"
|
||||
single
|
||||
profiles={profiles}
|
||||
selected={profileId ? [profileId] : []}
|
||||
onChange={(ids) => {
|
||||
setProfileId(ids[0] ?? null);
|
||||
}}
|
||||
noneLabel={t("agent.composer.noProfile")}
|
||||
className="shrink-0"
|
||||
/>
|
||||
<p className="min-w-0 text-xs text-muted-foreground">
|
||||
{t("feedback.profile.idOnly")}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{accountEmail ? (
|
||||
<p className="flex min-w-0 items-center gap-1.5 text-xs text-muted-foreground">
|
||||
<LuMail className="size-3.5 shrink-0" aria-hidden="true" />
|
||||
|
||||
@@ -4716,6 +4716,10 @@
|
||||
"prompt": "Nutze die MCP-Tools von Donut Browser, um herauszufinden, warum Donut Browser nicht wie erwartet funktioniert. Folge dem Triage-Leitfaden unter {{url}}: Frag mich, was schiefgelaufen ist, rufe get_diagnostics auf, suche nach einer neueren Version und bekannten Problemen und hilf mir, das Problem zu beheben. Zeig mir den Bericht, bevor du ihn mit send_feedback sendest.",
|
||||
"problem": "Was ich gesehen habe: {{message}}"
|
||||
},
|
||||
"profile": {
|
||||
"label": "Betroffenes Profil",
|
||||
"idOnly": "Nur die Profil-ID wird gesendet."
|
||||
},
|
||||
"email": {
|
||||
"label": "E-Mail für eine Antwort",
|
||||
"optional": "Optional",
|
||||
|
||||
@@ -4716,6 +4716,10 @@
|
||||
"prompt": "Use the Donut Browser MCP tools to find out why Donut Browser does not work as I expect. Follow the triage playbook at {{url}}: ask me what went wrong, call get_diagnostics, look for a newer version and known issues, and help me fix the problem. Show me the report before you send it with send_feedback.",
|
||||
"problem": "What I saw: {{message}}"
|
||||
},
|
||||
"profile": {
|
||||
"label": "Profile this is about",
|
||||
"idOnly": "Only the profile ID is sent."
|
||||
},
|
||||
"email": {
|
||||
"label": "Email for a reply",
|
||||
"optional": "Optional",
|
||||
|
||||
@@ -4769,6 +4769,10 @@
|
||||
"prompt": "Usa las herramientas MCP de Donut Browser para averiguar por qué Donut Browser no funciona como espero. Sigue la guía de diagnóstico en {{url}}: pregúntame qué salió mal, llama a get_diagnostics, busca una versión más nueva y problemas conocidos, y ayúdame a resolver el problema. Muéstrame el informe antes de enviarlo con send_feedback.",
|
||||
"problem": "Lo que vi: {{message}}"
|
||||
},
|
||||
"profile": {
|
||||
"label": "Perfil afectado",
|
||||
"idOnly": "Solo se envía el ID del perfil."
|
||||
},
|
||||
"email": {
|
||||
"label": "Correo para responderte",
|
||||
"optional": "Opcional",
|
||||
|
||||
@@ -4769,6 +4769,10 @@
|
||||
"prompt": "Utilise les outils MCP de Donut Browser pour trouver pourquoi Donut Browser ne fonctionne pas comme prévu. Suis le guide de diagnostic à l'adresse {{url}} : demande-moi ce qui ne va pas, appelle get_diagnostics, cherche une version plus récente et les problèmes connus, et aide-moi à corriger le problème. Montre-moi le rapport avant de l'envoyer avec send_feedback.",
|
||||
"problem": "Ce que j'ai vu : {{message}}"
|
||||
},
|
||||
"profile": {
|
||||
"label": "Profil concerné",
|
||||
"idOnly": "Seul l'identifiant du profil est envoyé."
|
||||
},
|
||||
"email": {
|
||||
"label": "E-mail pour la réponse",
|
||||
"optional": "Facultatif",
|
||||
|
||||
@@ -4716,6 +4716,10 @@
|
||||
"prompt": "Donut Browser の MCP ツールを使って、Donut Browser が期待どおりに動かない原因を調べてください。{{url}} のトリアージ手順に従ってください。何が起きたかを私に聞き、get_diagnostics を呼び出し、新しいバージョンと既知の問題を確認し、問題の解決を手伝ってください。send_feedback で送信する前に、レポートを私に見せてください。",
|
||||
"problem": "私が見たこと: {{message}}"
|
||||
},
|
||||
"profile": {
|
||||
"label": "対象のプロファイル",
|
||||
"idOnly": "送信されるのはプロファイル ID のみです。"
|
||||
},
|
||||
"email": {
|
||||
"label": "返信用メールアドレス",
|
||||
"optional": "任意",
|
||||
|
||||
@@ -4716,6 +4716,10 @@
|
||||
"prompt": "Donut Browser MCP 도구를 사용해 Donut Browser가 예상대로 동작하지 않는 이유를 찾아 주세요. {{url}}의 분류 가이드를 따르세요. 무엇이 잘못됐는지 저에게 묻고, get_diagnostics를 호출하고, 새 버전과 알려진 문제를 확인한 뒤 문제 해결을 도와주세요. send_feedback으로 보내기 전에 보고서를 저에게 보여 주세요.",
|
||||
"problem": "제가 본 내용: {{message}}"
|
||||
},
|
||||
"profile": {
|
||||
"label": "관련 프로필",
|
||||
"idOnly": "프로필 ID만 전송됩니다."
|
||||
},
|
||||
"email": {
|
||||
"label": "답장 받을 이메일",
|
||||
"optional": "선택 사항",
|
||||
|
||||
@@ -4769,6 +4769,10 @@
|
||||
"prompt": "Use as ferramentas MCP do Donut Browser para descobrir por que o Donut Browser não funciona como eu espero. Siga o guia de triagem em {{url}}: pergunte o que deu errado, chame get_diagnostics, procure uma versão mais nova e problemas conhecidos e me ajude a resolver o problema. Mostre o relatório antes de enviá-lo com send_feedback.",
|
||||
"problem": "O que eu vi: {{message}}"
|
||||
},
|
||||
"profile": {
|
||||
"label": "Perfil relacionado",
|
||||
"idOnly": "Apenas o ID do perfil é enviado."
|
||||
},
|
||||
"email": {
|
||||
"label": "E-mail para resposta",
|
||||
"optional": "Opcional",
|
||||
|
||||
@@ -4824,6 +4824,10 @@
|
||||
"prompt": "Используй MCP-инструменты Donut Browser, чтобы выяснить, почему Donut Browser работает не так, как я ожидаю. Следуй руководству по диагностике по адресу {{url}}: спроси меня, что пошло не так, вызови get_diagnostics, проверь, есть ли новая версия и известные проблемы, и помоги мне исправить проблему. Покажи мне отчёт, прежде чем отправить его через send_feedback.",
|
||||
"problem": "Что я увидел: {{message}}"
|
||||
},
|
||||
"profile": {
|
||||
"label": "Профиль, о котором речь",
|
||||
"idOnly": "Отправляется только ID профиля."
|
||||
},
|
||||
"email": {
|
||||
"label": "Почта для ответа",
|
||||
"optional": "Необязательно",
|
||||
|
||||
@@ -4716,6 +4716,10 @@
|
||||
"prompt": "Donut Browser'ın neden beklediğim gibi çalışmadığını bulmak için Donut Browser MCP araçlarını kullan. {{url}} adresindeki sorun giderme kılavuzunu izle: bana neyin yanlış gittiğini sor, get_diagnostics çağır, daha yeni bir sürüm ve bilinen sorunlar ara ve sorunu çözmeme yardım et. Raporu send_feedback ile göndermeden önce bana göster.",
|
||||
"problem": "Gördüğüm: {{message}}"
|
||||
},
|
||||
"profile": {
|
||||
"label": "İlgili profil",
|
||||
"idOnly": "Yalnızca profil kimliği gönderilir."
|
||||
},
|
||||
"email": {
|
||||
"label": "Yanıt için e-posta",
|
||||
"optional": "İsteğe bağlı",
|
||||
|
||||
@@ -4716,6 +4716,10 @@
|
||||
"prompt": "Hãy dùng các công cụ MCP của Donut Browser để tìm hiểu vì sao Donut Browser không hoạt động như tôi mong đợi. Làm theo hướng dẫn phân loại tại {{url}}: hỏi tôi chuyện gì đã xảy ra, gọi get_diagnostics, tìm phiên bản mới hơn và các sự cố đã biết, rồi giúp tôi khắc phục. Cho tôi xem báo cáo trước khi gửi bằng send_feedback.",
|
||||
"problem": "Những gì tôi thấy: {{message}}"
|
||||
},
|
||||
"profile": {
|
||||
"label": "Hồ sơ liên quan",
|
||||
"idOnly": "Chỉ gửi ID của hồ sơ."
|
||||
},
|
||||
"email": {
|
||||
"label": "Email để nhận phản hồi",
|
||||
"optional": "Không bắt buộc",
|
||||
|
||||
@@ -4716,6 +4716,10 @@
|
||||
"prompt": "请使用 Donut Browser 的 MCP 工具,查明 Donut Browser 为什么没有按我预期的方式工作。按照 {{url}} 上的排查手册操作:问我出了什么问题,调用 get_diagnostics,检查是否有新版本和已知问题,并帮我解决问题。用 send_feedback 发送报告之前,先给我看。",
|
||||
"problem": "我看到的情况:{{message}}"
|
||||
},
|
||||
"profile": {
|
||||
"label": "相关配置文件",
|
||||
"idOnly": "只发送配置文件 ID。"
|
||||
},
|
||||
"email": {
|
||||
"label": "用于回复的邮箱",
|
||||
"optional": "可选",
|
||||
|
||||
Reference in new issue
Block a user