feat(web): link the online account menu to the hub MCP token page
The signed-in account dropdown gains an "MCP Tokens" row that opens the hub portal's /mcp-tokens page in a new tab, so an online user can mint a per-account MCP token for external clients. It shows only in the hub-served multi-tenant editor (gated on the ?tenant= param); the native desktop never sets the flag — its local MCP is tokenless — and a self-hosted serve-web without a tenant leaves it hidden. New key account.mcpToken across all 15 locales.
This commit is contained in:
parent
3a5f21d672
commit
a92e32a772
|
|
@ -103,12 +103,20 @@ mod tests {
|
|||
pub enum AccountMenuRow {
|
||||
/// Opens the settings modal on the Account tab.
|
||||
Settings,
|
||||
/// Opens the hub portal's MCP-token page in a new tab. Only shown in
|
||||
/// the online/hub-served web editor (gated host-side); native never
|
||||
/// paints it.
|
||||
McpToken,
|
||||
/// Clears `AccountState` back to `Anonymous`.
|
||||
SignOut,
|
||||
}
|
||||
|
||||
impl AccountMenuRow {
|
||||
pub const ALL: [AccountMenuRow; 2] = [AccountMenuRow::Settings, AccountMenuRow::SignOut];
|
||||
pub const ALL: [AccountMenuRow; 3] = [
|
||||
AccountMenuRow::Settings,
|
||||
AccountMenuRow::McpToken,
|
||||
AccountMenuRow::SignOut,
|
||||
];
|
||||
}
|
||||
|
||||
/// Which control in the sign-in modal the cursor is over / has pressed.
|
||||
|
|
|
|||
|
|
@ -283,6 +283,11 @@ pub struct EditorUiState {
|
|||
pub account_menu_open: bool,
|
||||
/// Which account-dropdown row the cursor is over.
|
||||
pub account_menu_hover: Option<crate::account_state::AccountMenuRow>,
|
||||
/// Show the "MCP Tokens" row in the signed-in account dropdown. Set
|
||||
/// true only by the online/hub-served web host (a `?tenant=` page);
|
||||
/// native desktop and self-hosted serve-web leave it false so the row
|
||||
/// — which opens the hub portal's token page — never appears there.
|
||||
pub account_mcp_tokens_entry: bool,
|
||||
/// Sign-in modal (signed-out state) open.
|
||||
pub login_modal_open: bool,
|
||||
/// Which login-modal control the cursor is over.
|
||||
|
|
|
|||
|
|
@ -66,6 +66,7 @@ impl Default for EditorUiState {
|
|||
account: crate::account_state::AccountState::default(),
|
||||
account_menu_open: false,
|
||||
account_menu_hover: None,
|
||||
account_mcp_tokens_entry: false,
|
||||
login_modal_open: false,
|
||||
login_modal_hover: None,
|
||||
login_modal_stub_hint_shown: false,
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@
|
|||
use crate::theme::Theme;
|
||||
use crate::widgets::editor_state_ext::theme_for;
|
||||
use crate::widgets::icons::{draw_icon, Icon};
|
||||
// `Icon::Key` (lucide key.svg) labels the "MCP Tokens" row.
|
||||
use crate::widgets::menu_paint;
|
||||
use crate::widgets::{LayoutBox, LayoutCx, PaintCx, Widget, WidgetId};
|
||||
use crate::{Point2D, Rect, TextLayout};
|
||||
|
|
@ -30,6 +31,10 @@ pub struct AccountMenu<'a> {
|
|||
display_name: String,
|
||||
username: String,
|
||||
hover: Option<AccountMenuRow>,
|
||||
/// Whether the "MCP Tokens" row is shown (online/hub-served web only,
|
||||
/// gated by `EditorUiState::account_mcp_tokens_entry`). When false the
|
||||
/// row is absent from paint AND hit-test so the two stay in sync.
|
||||
show_mcp_tokens: bool,
|
||||
}
|
||||
|
||||
impl<'a> AccountMenu<'a> {
|
||||
|
|
@ -50,6 +55,7 @@ impl<'a> AccountMenu<'a> {
|
|||
display_name,
|
||||
username,
|
||||
hover: ui.account_menu_hover,
|
||||
show_mcp_tokens: ui.account_mcp_tokens_entry,
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -63,8 +69,19 @@ impl<'a> AccountMenu<'a> {
|
|||
}
|
||||
}
|
||||
|
||||
fn action_row_count(&self) -> f32 {
|
||||
if self.show_mcp_tokens {
|
||||
3.0
|
||||
} else {
|
||||
2.0
|
||||
}
|
||||
}
|
||||
|
||||
fn height(&self) -> f32 {
|
||||
HEADER_HEIGHT + (DIVIDER_GAP * 2.0 + 1.0) + ROW_HEIGHT * 2.0 + DIVIDER_GAP
|
||||
HEADER_HEIGHT
|
||||
+ (DIVIDER_GAP * 2.0 + 1.0)
|
||||
+ ROW_HEIGHT * self.action_row_count()
|
||||
+ DIVIDER_GAP
|
||||
}
|
||||
|
||||
pub fn row_at(&self, panel: Rect, point: Point2D) -> Option<AccountMenuRow> {
|
||||
|
|
@ -75,6 +92,12 @@ impl<'a> AccountMenu<'a> {
|
|||
if row_hit(panel.origin.x, y, point) {
|
||||
return Some(AccountMenuRow::Settings);
|
||||
}
|
||||
if self.show_mcp_tokens {
|
||||
y += ROW_HEIGHT;
|
||||
if row_hit(panel.origin.x, y, point) {
|
||||
return Some(AccountMenuRow::McpToken);
|
||||
}
|
||||
}
|
||||
y += ROW_HEIGHT;
|
||||
if row_hit(panel.origin.x, y, point) {
|
||||
return Some(AccountMenuRow::SignOut);
|
||||
|
|
@ -157,6 +180,18 @@ impl<'a> Widget for AccountMenu<'a> {
|
|||
t(self.ui.locale, "account.settings"),
|
||||
self.hover == Some(AccountMenuRow::Settings),
|
||||
);
|
||||
if self.show_mcp_tokens {
|
||||
y += ROW_HEIGHT;
|
||||
paint_action_row(
|
||||
cx,
|
||||
&self.theme,
|
||||
rect.origin.x,
|
||||
y,
|
||||
Icon::Key,
|
||||
t(self.ui.locale, "account.mcpToken"),
|
||||
self.hover == Some(AccountMenuRow::McpToken),
|
||||
);
|
||||
}
|
||||
y += ROW_HEIGHT;
|
||||
paint_action_row(
|
||||
cx,
|
||||
|
|
|
|||
|
|
@ -79,6 +79,9 @@ pub fn close_login_modal(state: &mut EditorState) {
|
|||
pub enum AccountMenuPress {
|
||||
/// Row handled entirely in editor state (Settings).
|
||||
Handled,
|
||||
/// MCP-tokens row — the menu closed; the host opens the hub portal's
|
||||
/// token page (online web only; other hosts never surface this row).
|
||||
OpenMcpTokens,
|
||||
/// Sign-out row — display state is already `Anonymous`; the host
|
||||
/// revokes the device session through its platform transport.
|
||||
SignOut,
|
||||
|
|
@ -123,6 +126,10 @@ pub fn press_account_menu(
|
|||
state.chat.blur_input(now_ms);
|
||||
AccountMenuPress::Handled
|
||||
}
|
||||
Some(AccountMenuRow::McpToken) => {
|
||||
close_account_menu(state);
|
||||
AccountMenuPress::OpenMcpTokens
|
||||
}
|
||||
Some(AccountMenuRow::SignOut) => {
|
||||
close_account_menu(state);
|
||||
state.editor_ui.account = AccountState::Anonymous;
|
||||
|
|
|
|||
|
|
@ -71,6 +71,12 @@ impl WidgetHostNative {
|
|||
self.now_ms,
|
||||
) {
|
||||
AccountMenuPress::Vanished => return,
|
||||
AccountMenuPress::OpenMcpTokens => {
|
||||
// Desktop never emits this: the "MCP Tokens" row is gated
|
||||
// behind `account_mcp_tokens_entry`, which only the online
|
||||
// web host sets. The local MCP is tokenless and there is no
|
||||
// hub portal to open, so there is nothing to do here.
|
||||
}
|
||||
AccountMenuPress::SignOut => {
|
||||
// Revoke the device session (background thread inside
|
||||
// the library; an inert no-op in stub builds).
|
||||
|
|
|
|||
|
|
@ -107,6 +107,13 @@ pub(super) async fn mount_ck(canvas_id: String) -> Result<(), JsValue> {
|
|||
.editor_state_mut()
|
||||
.editor_ui
|
||||
.style_import_file_picker_supported = true;
|
||||
// Online / hub-served mode: a `?tenant=` page URL means the hub is
|
||||
// serving this editor at its own origin, so its `/mcp-tokens`
|
||||
// portal page is reachable. Reveal the "MCP Tokens" row in the
|
||||
// signed-in account dropdown. Self-hosted serve-web (no tenant) and
|
||||
// native desktop leave this false so the row never appears there.
|
||||
b.host.editor_state_mut().editor_ui.account_mcp_tokens_entry =
|
||||
crate::daemon_base::tenant_param().is_some();
|
||||
// First frame paints synchronously so the shell is visible immediately
|
||||
// (no one-frame blank). Subsequent input-driven repaints coalesce
|
||||
// through the rAF installed below.
|
||||
|
|
|
|||
|
|
@ -53,6 +53,11 @@ mod live_sync_glue;
|
|||
mod live_sync_recovery;
|
||||
#[cfg(feature = "canvaskit")]
|
||||
mod web_auth_sync;
|
||||
// Opens the hub portal's per-account MCP-token page in a new tab from the
|
||||
// signed-in account dropdown (online/hub-served web only). Only the
|
||||
// canvaskit widget host calls it, so it shares that gate.
|
||||
#[cfg(feature = "canvaskit")]
|
||||
mod web_mcp_tokens;
|
||||
// Daemon collaboration relay (action drain + projection pull + presence).
|
||||
#[cfg(feature = "canvaskit")]
|
||||
mod collab_sync;
|
||||
|
|
|
|||
35
crates/op-host-web/src/web_mcp_tokens.rs
Normal file
35
crates/op-host-web/src/web_mcp_tokens.rs
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
//! Open the hub portal's per-account MCP-token page from the signed-in
|
||||
//! account dropdown.
|
||||
//!
|
||||
//! The online web editor is served BY the hub at the hub's own origin, so
|
||||
//! the portal's `/mcp-tokens` page is reachable relative to
|
||||
//! `window.location.origin`. This mirrors the sign-in loading popup in
|
||||
//! `web_auth_sync`: a `window.open(url, "_blank")` issued synchronously
|
||||
//! inside the click's user-activation window is not popup-blocked.
|
||||
|
||||
/// Portal path (relative to the hub origin) that lists / generates
|
||||
/// per-account MCP access tokens.
|
||||
const MCP_TOKENS_PATH: &str = "/mcp-tokens";
|
||||
|
||||
/// Open the hub portal's MCP-token page in a new browser tab.
|
||||
///
|
||||
/// The URL is `<origin>/mcp-tokens`, built from the current page origin so
|
||||
/// it always addresses the hub that served this editor. Only reached from
|
||||
/// the online/hub-served host (the row is gated by
|
||||
/// `EditorUiState::account_mcp_tokens_entry`).
|
||||
pub(crate) fn open_mcp_tokens_page() {
|
||||
let Some(window) = web_sys::window() else {
|
||||
return;
|
||||
};
|
||||
// Prefer the explicit `<origin>/mcp-tokens`; fall back to the bare
|
||||
// relative path (the browser resolves it against the origin anyway) if
|
||||
// the origin can't be read.
|
||||
let url = window
|
||||
.location()
|
||||
.origin()
|
||||
.ok()
|
||||
.filter(|origin| !origin.is_empty())
|
||||
.map(|origin| format!("{origin}{MCP_TOKENS_PATH}"))
|
||||
.unwrap_or_else(|| MCP_TOKENS_PATH.to_string());
|
||||
let _ = window.open_with_url_and_target(&url, "_blank");
|
||||
}
|
||||
|
|
@ -76,6 +76,15 @@ impl WidgetHost {
|
|||
self.now_ms,
|
||||
) {
|
||||
AccountMenuPress::Vanished => return,
|
||||
AccountMenuPress::OpenMcpTokens => {
|
||||
// The hub serves this editor at its own origin, so the
|
||||
// portal's per-account MCP-token page lives at
|
||||
// `/mcp-tokens` relative to the current origin. A new tab
|
||||
// opened synchronously inside the click's user-activation
|
||||
// window is not popup-blocked (same pattern as the
|
||||
// sign-in loading popup in `web_auth_sync`).
|
||||
crate::web_mcp_tokens::open_mcp_tokens_page();
|
||||
}
|
||||
AccountMenuPress::SignOut => {
|
||||
self.pending_auth_actions.push(PendingAuthAction::SignOut);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -251,7 +251,7 @@ fn placeholders(value: &str) -> BTreeSet<String> {
|
|||
fn every_locale_has_exactly_the_english_key_set() {
|
||||
let all_tables = tables();
|
||||
let expected = table_keys(all_tables[0].0, all_tables[0].1, all_tables[0].2);
|
||||
assert_eq!(expected.len(), 1655, "update the intentional catalog size");
|
||||
assert_eq!(expected.len(), 1656, "update the intentional catalog size");
|
||||
|
||||
for (name, main, git, lookup) in all_tables {
|
||||
let actual = table_keys(name, main, git);
|
||||
|
|
|
|||
|
|
@ -124,6 +124,7 @@ pub fn lookup(key: &str) -> Option<&'static str> {
|
|||
"sceneTemplate.item.dossierLinenDeck.summary" => "Ein Dossier auf Leinenpapier, vom Deckblatt über Hintergrund, aktuelle Daten, Analyse und Optionsvergleich bis zum Beschluss. Acht Seiten, die sich als eigenständiges Memo lesen, für Entscheidungsvorlagen.",
|
||||
"sceneTemplate.item.ledgerTickDeck.title" => "Kontobuchraster · Wettbewerbsmatrix-Deck",
|
||||
"sceneTemplate.item.ledgerTickDeck.summary" => "Bewertungsmaßstab, Hauptmatrix, Quantilsskalen und die Gegenüberstellung von Lücke und Stärke auf Kontobuchlinien. Sieben Seiten, die einen Wettbewerbsvergleich wie eine prüfbare Rechnung erzählen, für Auswahlentscheidungen und Marktanalysen.",
|
||||
"account.mcpToken" => "MCP-Tokens",
|
||||
_ => return None,
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -164,6 +164,7 @@ pub fn lookup(key: &str) -> Option<&'static str> {
|
|||
"sceneTemplate.item.dossierLinenDeck.summary" => "A linen-paper dossier from cover sheet through background, current data, analysis and option comparison to the resolution — eight pages that read as a standalone memo, for decision reviews.",
|
||||
"sceneTemplate.item.ledgerTickDeck.title" => "Ledger Tick · Competitive Matrix Deck",
|
||||
"sceneTemplate.item.ledgerTickDeck.summary" => "Scoring criteria, the main matrix, quantile scales and a gap-versus-strength read-out on ledger ruling — seven pages that tell a competitive comparison like a balanced account, for vendor selection and market analysis.",
|
||||
"account.mcpToken" => "MCP Tokens",
|
||||
_ => return None,
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -124,6 +124,7 @@ pub fn lookup(key: &str) -> Option<&'static str> {
|
|||
"sceneTemplate.item.dossierLinenDeck.summary" => "Un expediente en papel de lino, de la portada al contexto, los datos actuales, el análisis, la comparación de opciones y la resolución. Ocho páginas que se leen como un memorando autónomo, para comités de decisión.",
|
||||
"sceneTemplate.item.ledgerTickDeck.title" => "Libro mayor · deck de matriz competitiva",
|
||||
"sceneTemplate.item.ledgerTickDeck.summary" => "Criterios de evaluación, matriz principal, escalas por cuantiles y lectura de brechas y fortalezas sobre el rayado de un libro mayor. Siete páginas que cuentan una comparación competitiva como una cuenta verificable, para selección de proveedores y análisis de mercado.",
|
||||
"account.mcpToken" => "Tokens MCP",
|
||||
_ => return None,
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -124,6 +124,7 @@ pub fn lookup(key: &str) -> Option<&'static str> {
|
|||
"sceneTemplate.item.dossierLinenDeck.summary" => "Un dossier sur papier lin, de la page de garde au contexte, aux données actuelles, à l’analyse, à la comparaison des options et à la décision. Huit pages qui se lisent comme une note autonome, pour les comités de décision.",
|
||||
"sceneTemplate.item.ledgerTickDeck.title" => "Registre à colonnes · deck matrice concurrentielle",
|
||||
"sceneTemplate.item.ledgerTickDeck.summary" => "Critères d’évaluation, matrice principale, échelles de quantiles et lecture des écarts et des forces sur une réglure de registre. Sept pages qui racontent une comparaison concurrentielle comme un compte vérifiable, pour le choix de fournisseurs et l’analyse de marché.",
|
||||
"account.mcpToken" => "Jetons MCP",
|
||||
_ => return None,
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -128,6 +128,7 @@ pub fn lookup(key: &str) -> Option<&'static str> {
|
|||
"sceneTemplate.item.dossierLinenDeck.summary" => "लिनन कागज़ की फ़ाइल: आवरण पृष्ठ, पृष्ठभूमि, वर्तमान आँकड़े, विश्लेषण, विकल्पों की तुलना और निर्णय। आठ पृष्ठ अपने आप में पूरा ज्ञापन बनते हैं — निर्णय समीक्षा के लिए।",
|
||||
"sceneTemplate.item.ledgerTickDeck.title" => "बहीखाता रेखा · प्रतिस्पर्धी मैट्रिक्स प्रस्तुति",
|
||||
"sceneTemplate.item.ledgerTickDeck.summary" => "बहीखाते की रेखाओं पर मूल्यांकन मानक, मुख्य मैट्रिक्स, चतुर्थक पैमाने और अंतर बनाम ताकत का मिलान। सात पृष्ठ प्रतिस्पर्धी तुलना को जाँचे जा सकने वाले खाते की तरह कहते हैं — विक्रेता चयन और बाज़ार विश्लेषण के लिए।",
|
||||
"account.mcpToken" => "MCP टोकन",
|
||||
_ => return None,
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -124,6 +124,7 @@ pub fn lookup(key: &str) -> Option<&'static str> {
|
|||
"sceneTemplate.item.dossierLinenDeck.summary" => "Tekstur berkas kertas linen, dari halaman muka, latar, data terkini, analisis, perbandingan opsi, hingga keputusan. Delapan halaman yang terbaca utuh sebagai memo tersendiri, untuk rapat pengambilan keputusan.",
|
||||
"sceneTemplate.item.ledgerTickDeck.title" => "Garis buku besar · deck matriks kompetitor",
|
||||
"sceneTemplate.item.ledgerTickDeck.summary" => "Di atas garis buku besar ada kriteria penilaian, matriks utama, skala kuantil, dan pembacaan selisih berbanding keunggulan. Tujuh halaman menceritakan perbandingan kompetitor seperti pembukuan yang dapat dicocokkan, untuk seleksi vendor dan analisis pasar.",
|
||||
"account.mcpToken" => "Token MCP",
|
||||
_ => return None,
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -140,6 +140,7 @@ pub fn lookup(key: &str) -> Option<&'static str> {
|
|||
"sceneTemplate.item.dossierLinenDeck.summary" => "リネン紙の書類束の質感で、表紙、背景、現状データ、分析、案の比較から決議まで。単体で読み切れるメモとして仕上げた八ページ、意思決定レビュー向け。",
|
||||
"sceneTemplate.item.ledgerTickDeck.title" => "台帳の目盛 · 競合マトリクス デッキ",
|
||||
"sceneTemplate.item.ledgerTickDeck.summary" => "台帳の罫線の上に評価基準、主マトリクス、分位の目盛、差と強みの対照。七ページで競合比較を照合可能な帳簿として語る、製品選定や市場分析向け。",
|
||||
"account.mcpToken" => "MCP トークン",
|
||||
_ => return None,
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -140,6 +140,7 @@ pub fn lookup(key: &str) -> Option<&'static str> {
|
|||
"sceneTemplate.item.dossierLinenDeck.summary" => "리넨 종이 서류철의 질감으로 표지, 배경, 현황 데이터, 분석, 안 비교에서 결의까지. 그 자체로 읽히는 메모로 쓴 여덟 장, 의사결정 검토용.",
|
||||
"sceneTemplate.item.ledgerTickDeck.title" => "장부 눈금 · 경쟁 매트릭스 덱",
|
||||
"sceneTemplate.item.ledgerTickDeck.summary" => "장부 괘선 위의 평가 기준, 주 매트릭스, 분위 눈금, 격차와 강점 대조. 일곱 장으로 경쟁 비교를 대조 가능한 장부처럼 풀어내는, 제품 선정과 시장 분석용.",
|
||||
"account.mcpToken" => "MCP 토큰",
|
||||
_ => return None,
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -124,6 +124,7 @@ pub fn lookup(key: &str) -> Option<&'static str> {
|
|||
"sceneTemplate.item.dossierLinenDeck.summary" => "Um dossiê em papel de linho, da folha de rosto ao contexto, aos dados atuais, à análise, à comparação de opções e à decisão. Oito páginas que se leem como um memorando autônomo, para comitês de decisão.",
|
||||
"sceneTemplate.item.ledgerTickDeck.title" => "Livro-razão · deck de matriz competitiva",
|
||||
"sceneTemplate.item.ledgerTickDeck.summary" => "Critérios de avaliação, matriz principal, escalas por quantis e a leitura de lacunas e forças sobre o pautado de um livro-razão. Sete páginas que contam uma comparação competitiva como uma conta conferível, para seleção de fornecedores e análise de mercado.",
|
||||
"account.mcpToken" => "Tokens MCP",
|
||||
_ => return None,
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -154,6 +154,7 @@ pub fn lookup(key: &str) -> Option<&'static str> {
|
|||
"sceneTemplate.item.dossierLinenDeck.summary" => "Досье на льняной бумаге: титульный лист, предыстория, текущие данные, анализ, сравнение вариантов и решение. Восемь страниц читаются как самостоятельная записка — для разбора решений.",
|
||||
"sceneTemplate.item.ledgerTickDeck.title" => "Разлиновка гроссбуха · презентация конкурентной матрицы",
|
||||
"sceneTemplate.item.ledgerTickDeck.summary" => "Критерии оценки, основная матрица, квантильные шкалы и сопоставление разрывов и сильных сторон на линовке гроссбуха. Семь страниц рассказывают сравнение конкурентов как сверяемый счёт — для выбора поставщика и анализа рынка.",
|
||||
"account.mcpToken" => "Токены MCP",
|
||||
_ => return None,
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -126,6 +126,7 @@ pub fn lookup(key: &str) -> Option<&'static str> {
|
|||
"sceneTemplate.item.dossierLinenDeck.summary" => "ผิวสัมผัสแบบแฟ้มกระดาษลินิน ตั้งแต่หน้าปกเอกสาร ที่มา ข้อมูลปัจจุบัน การวิเคราะห์ การเทียบทางเลือก จนถึงมติ แปดหน้าที่อ่านจบได้ในตัวเองเหมือนบันทึกข้อความ เหมาะกับการพิจารณาตัดสินใจ",
|
||||
"sceneTemplate.item.ledgerTickDeck.title" => "เส้นบัญชี · เด็คเมทริกซ์คู่แข่ง",
|
||||
"sceneTemplate.item.ledgerTickDeck.summary" => "บนเส้นบรรทัดสมุดบัญชี มีเกณฑ์ประเมิน เมทริกซ์หลัก สเกลแบ่งช่วง และการเทียบช่องว่างกับจุดแข็ง เจ็ดหน้าเล่าการเปรียบเทียบคู่แข่งให้เป็นบัญชีที่ตรวจทานได้ เหมาะกับการคัดเลือกและวิเคราะห์ตลาด",
|
||||
"account.mcpToken" => "โทเค็น MCP",
|
||||
_ => return None,
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -124,6 +124,7 @@ pub fn lookup(key: &str) -> Option<&'static str> {
|
|||
"sceneTemplate.item.dossierLinenDeck.summary" => "Keten kâğıt dosya dokusuyla kapak sayfasından arka plana, güncel verilere, çözümlemeye, seçenek karşılaştırmasına ve karara. Tek başına okunabilen bir muhtıra olarak yazılmış sekiz sayfa, karar değerlendirmeleri için.",
|
||||
"sceneTemplate.item.ledgerTickDeck.title" => "Defter çizgisi · rekabet matrisi sunumu",
|
||||
"sceneTemplate.item.ledgerTickDeck.summary" => "Defter çizgileri üzerinde değerlendirme ölçütleri, ana matris, dilim ölçekleri ve fark ile güçlü yön karşılaştırması. Yedi sayfa rekabet karşılaştırmasını denetlenebilir bir hesap gibi anlatır, tedarikçi seçimi ve pazar analizi için.",
|
||||
"account.mcpToken" => "MCP Belirteçleri",
|
||||
_ => return None,
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -136,6 +136,7 @@ pub fn lookup(key: &str) -> Option<&'static str> {
|
|||
"sceneTemplate.item.dossierLinenDeck.summary" => "Chất giấy lanh của một tập hồ sơ, từ trang bìa, bối cảnh, số liệu hiện trạng, phân tích, so sánh phương án đến nghị quyết. Tám trang đọc trọn như một bản ghi nhớ độc lập, hợp cho họp ra quyết định.",
|
||||
"sceneTemplate.item.ledgerTickDeck.title" => "Kẻ ô sổ cái · deck ma trận đối thủ",
|
||||
"sceneTemplate.item.ledgerTickDeck.summary" => "Trên đường kẻ sổ cái là tiêu chí đánh giá, ma trận chính, thang phân vị và đối chiếu khoảng cách với thế mạnh. Bảy trang kể việc so sánh đối thủ như một khoản sổ có thể đối chiếu, hợp cho chọn nhà cung cấp và phân tích thị trường.",
|
||||
"account.mcpToken" => "Token MCP",
|
||||
_ => return None,
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -124,6 +124,7 @@ pub fn lookup(key: &str) -> Option<&'static str> {
|
|||
"sceneTemplate.item.dossierLinenDeck.summary" => "亚麻纸卷宗质感,从文件首页、背景、现状数据到方案对比与决议,八页写成一份能独立读完的备忘录,适合决策评审。",
|
||||
"sceneTemplate.item.ledgerTickDeck.title" => "账簿勾格 · 竞品矩阵档",
|
||||
"sceneTemplate.item.ledgerTickDeck.summary" => "账簿格线上的评估口径、主矩阵、分位刻度与差距优势对照,七页把竞品比较讲成一笔可核对的账,适合选型与市场分析。",
|
||||
"account.mcpToken" => "MCP 令牌",
|
||||
_ => return None,
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -124,6 +124,7 @@ pub fn lookup(key: &str) -> Option<&'static str> {
|
|||
"sceneTemplate.item.dossierLinenDeck.summary" => "亞麻紙卷宗質感,從文件首頁、背景、現況資料到方案比較與決議,八頁寫成一份能獨立讀完的備忘錄,適合決策審查。",
|
||||
"sceneTemplate.item.ledgerTickDeck.title" => "帳簿勾格 · 競品矩陣檔",
|
||||
"sceneTemplate.item.ledgerTickDeck.summary" => "帳簿格線上的評估口徑、主矩陣、分位刻度與差距優勢對照,七頁把競品比較講成一筆可核對的帳,適合選型與市場分析。",
|
||||
"account.mcpToken" => "MCP 權杖",
|
||||
_ => return None,
|
||||
})
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue