diff --git a/blog/asset-sources/genoffice-meets-token-station-cover.svg b/blog/asset-sources/genoffice-meets-token-station-cover.svg new file mode 100644 index 0000000..06b3a75 --- /dev/null +++ b/blog/asset-sources/genoffice-meets-token-station-cover.svg @@ -0,0 +1,108 @@ + + genoffice-meets-token-station-cover + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + DOCS · SHEETS · SLIDES · PDF + + + + + Docs + + + + Sheets + + + + Slides + + + + PDF + + + + + + + + + + + GENOFFICE · TOKEN STATION · PRODUCT INTEGRATION + + + Any model. + Pay-as-you-go. + GenOffice, powered by Token Station. + + + + Docs + + + Sheets + + + Slides + + + + + token station + + + + + + + + + ByteFuture + diff --git a/blog/genoffice-meets-token-station-cover.png b/blog/genoffice-meets-token-station-cover.png new file mode 100644 index 0000000..9f66020 Binary files /dev/null and b/blog/genoffice-meets-token-station-cover.png differ diff --git a/blog/genoffice-meets-token-station/demo-docs.mp4 b/blog/genoffice-meets-token-station/demo-docs.mp4 new file mode 100644 index 0000000..d007464 Binary files /dev/null and b/blog/genoffice-meets-token-station/demo-docs.mp4 differ diff --git a/blog/genoffice-meets-token-station/demo-sheets.mp4 b/blog/genoffice-meets-token-station/demo-sheets.mp4 new file mode 100644 index 0000000..b9429bc Binary files /dev/null and b/blog/genoffice-meets-token-station/demo-sheets.mp4 differ diff --git a/blog/genoffice-meets-token-station/demo-slides.mp4 b/blog/genoffice-meets-token-station/demo-slides.mp4 new file mode 100644 index 0000000..1c24066 Binary files /dev/null and b/blog/genoffice-meets-token-station/demo-slides.mp4 differ diff --git a/sitemap.xml b/sitemap.xml index 9a7efca..bb825e5 100644 --- a/sitemap.xml +++ b/sitemap.xml @@ -285,4 +285,24 @@ 2026-07-31 0.6 + + https://bytefuture.ai/blog/genoffice-meets-token-station.html + 2026-08-08 + 0.7 + + + https://bytefuture.ai/blog/genoffice-meets-token-station-zh.html + 2026-08-08 + 0.6 + + + https://bytefuture.ai/blog/genoffice-meets-token-station-ja.html + 2026-08-08 + 0.6 + + + https://bytefuture.ai/blog/genoffice-meets-token-station-ko.html + 2026-08-08 + 0.6 + diff --git a/src/content/writings/en/genoffice-meets-token-station.md b/src/content/writings/en/genoffice-meets-token-station.md new file mode 100644 index 0000000..d7e50e3 --- /dev/null +++ b/src/content/writings/en/genoffice-meets-token-station.md @@ -0,0 +1,175 @@ +--- +slug: "genoffice-meets-token-station" +lang: "en" +title: "GenOffice meets Token Station: any model, pay-as-you-go" +summary: "GenOffice ships with Genspark wired in by default. Point it at Token Station instead and every app in the suite runs on whatever model you choose, billed per request instead of per contract." +category: "tutorial" +date: "2026-08-08" +cta: "https://models.bytefuture.ai/intro.html" +cover: "blog/genoffice-meets-token-station-cover.png" +draft: false +--- + +

GenOffice is an open-source, AI-native office suite built on Electron: a word processor, a spreadsheet editor, a presentation builder, and a PDF viewer, tabbed together under one shell. Five apps share one promise underneath them: file-format fidelity. Open a .docx, .xlsx, or .pptx, make your edits, and everything you didn't touch comes back byte-identical. GenOffice parses the original file, tracks only the blocks you change, and splices narrow patches back into the source XML on save; the rest of the archive is copied through untouched.

+ +

Every app carries the same AI panel: block-level editing with version history in Docs, a tool-calling agent over the live workbook in Sheets, and a constrained layout-scripting agent in Slides that edits presentations through a fixed set of validated primitives rather than free-form code. All three share two packages under the hood: agent-core for the tool-calling loop, and ai-provider for talking to whichever model backend is configured.

+ +

That last part is the hook for this article. ai-provider already speaks plain OpenAI-compatible HTTP. Out of the box, GenOffice points it at Genspark. Point it at Token Station instead, and nothing about the apps changes: only where the tokens come from.

+ +

Why run it on Token Station

+ +

Genspark works the moment you sign in, and that's the right default. But it's one account, one model roster, and a credit balance you refill on Genspark's terms. Token Station changes the shape of that relationship in two ways that matter for a desktop app you run every day.

+ + + + + + +
Genspark (default)Token Station
Account / creditSingle account, single credit poolPay-as-you-go, no monthly or annual contract
ModelsRoster fixed by the integration250+ models across 25+ providers, pick per task
PricingCredits tied to a Genspark planOne key, provider rates, zero markup
+ +

Pay-as-you-go, not a contract. Token Station has no subscription tier. Register free, no card required, and a $1 credit lands in your balance immediately. From there you pay provider rates on the models you actually call, nothing recurring and nothing to cancel. Some models, like NVIDIA NIM, cost nothing at all.

+ +

Freedom to choose the model. A gateway account isn't pinned to one vendor's lineup. Run GenOffice Docs on Claude for long-form editing, switch Sheets to a cheaper model for routine formula work, and point Slides at whichever image-capable model fits the deck, all through the same key and the same OpenAI-style endpoint, with no separate signup per provider.

+ +

Because Token Station speaks the same OpenAI-compatible wire format GenOffice's custom provider slot already expects, wiring it in is a routing change, not a rewrite.

+ +

Setup: patch your GenOffice checkout

+ +

These are the actual changes that route GenOffice's AI traffic to Token Station instead of Genspark. Apply them to your own fork or branch; nothing here depends on a specific GenOffice release.

+ +

1. Install prerequisites and confirm the baseline build runs

+ +

You'll need Node.js and npm on your machine.

+ +
git clone <your-fork-url> genoffice
+cd genoffice
+npm install
+npm run dev
+ +

Confirm the shell launches and the AI panel opens normally. At this point it runs on Genspark by default, with no sign-in prompt unless you actually send a message while logged out.

+ +

2. Add an environment-driven override to the shared provider package

+ +

packages/ai-provider already defines a custom provider: any OpenAI-compatible baseUrl / apiKey / model. Add a small function that fills it in from an environment variable, the same pattern the codebase already uses for Genspark's own key (GSK_API_KEY).

+ +

packages/ai-provider/src/providers.ts

+ +
export const TOKEN_STATION_BASE_URL = 'https://models.bytefuture.ai/v1'
+const TOKEN_STATION_DEFAULT_MODEL = 'anthropic/claude-opus-4-8'
+
+export function applyTokenStationEnvOverride(
+  settings: AiSettings,
+  env: NodeJS.ProcessEnv = process.env,
+): AiSettings {
+  const apiKey = env.TOKEN_STATION_API_KEY
+  if (!apiKey) return settings
+  return {
+    provider: 'custom',
+    providers: {
+      ...settings.providers,
+      custom: {
+        apiKey,
+        model: env.TOKEN_STATION_MODEL || TOKEN_STATION_DEFAULT_MODEL,
+        baseUrl: TOKEN_STATION_BASE_URL,
+      },
+    },
+  }
+}
+ +

Export it from the package's index.ts alongside defaultAiSettings and resolveAiSettings.

+ +

3. Stop each app from forcing Genspark, and apply the override

+ +

Docs, Sheets, and Slides each register an ai:get-settings IPC handler that hard-resets the provider on every read. Remove that line and call the new override instead. Same shape in all three files:

+ +

apps/docs/src/main/docs-main.ts (mirrored in apps/slides/src/main/ai-ipc.ts and apps/sheets/src/main/sheets-main.ts)

+ +
// before
+ipcMain.handle('ai:get-settings', (): AiSettings => {
+  const stored = readJson<Partial<AiSettings> & LegacyAiSettings>(SETTINGS_PATH(), {})
+  const settings = resolveAiSettings(stored, defaultAiSettings())
+  settings.provider = 'genspark'   // ← delete this
+  return settings
+})
+
+// after
+ipcMain.handle('ai:get-settings', (): AiSettings => {
+  const stored = readJson<Partial<AiSettings> & LegacyAiSettings>(SETTINGS_PATH(), {})
+  return applyTokenStationEnvOverride(resolveAiSettings(stored, defaultAiSettings()))
+})
+ +

Sheets' handler looks slightly different in two harmless ways: it takes an IPC channel constant instead of a string literal, and calls a sessionFor(event) check first. The substance is identical: delete the forced-genspark line, call the new override.

+ +

4. Get a key and point the environment at it

+ +

Register at Token Station, grab an API key from the dashboard, then set it as a persistent environment variable and restart your terminal (environment variables only apply to processes launched afterward).

+ +
# Windows (PowerShell)
+[Environment]::SetEnvironmentVariable("TOKEN_STATION_API_KEY", "gw_...", "User")
+
+# macOS / Linux — add to your shell profile
+export TOKEN_STATION_API_KEY=gw_...
+ +

Optional: set TOKEN_STATION_MODEL to any Token Station provider/model id (for example openai/gpt-5.5) to override the default. Relaunch GenOffice: chat, editing, and planning across Docs, Sheets, and Slides now all run on Token Station. Slides' one-shot deck generation is the one feature that needs an additional patch, in step 5.

+ +

5. One more patch for Slides' deck generation

+ +

Slides' generate_deck/regenerate_slide tools originally called a Genspark-only cloud endpoint directly, bypassing the provider system entirely. They need their own patch, in three parts. If you're only routing Docs and Sheets, you can stop at step 4.

+ +

apps/slides/src/renderer/ai/slides-skill.ts: add two optional fields to the DeckAccess interface: a sync aiProvider() getter, and a composePageElements() method that returns a validated element list instead of an HTML marker. Then gate both tools on the active provider instead of a hard Genspark-only check:

+ +
const useCloud = cloudAvailable
+  && (access.aiProvider?.() ?? 'genspark') === 'genspark'
+if (!useCloud) {
+  // fall back to runLocalDeckGeneration() / runLocalRegenerateSlide()
+}
+ +

apps/slides/src/renderer/ai/local-deck-gen.ts (new file): the module that actually does the composing: asks the configured provider for each page's layout as JSON (shapes, text boxes, charts, images), validates it, and builds it with the same add_shape / add_text_box / add_chart / insert_web_image primitives the app's own agent tools already use.

+ +

apps/slides/src/renderer/ai/AiPanel.tsx: wire the two new DeckAccess fields to the same request path generateStyleSkill/planDeckOutline already use. This piece is easy to miss: without it, aiProvider stays undefined and the gate above silently falls back to Genspark.

+ +
aiProvider: () => settingsRef.current.provider,
+composePageElements: async (args) => {
+  const { system, user } = buildPageComposePrompt(args)
+  const r = await runLlmOnce(system, user, undefined, true, args.signal)
+  if (!r.ok || !r.text) return { ok: false, error: r.error ?? tGlobal('aiErrEmptyOutput') }
+  return parsePageElementsJson(r.text, args.canvasW, args.canvasH)
+},
+ +

Known limits of the local path, v1: it's append-only, new pages clone the current last slide, and "replace whole deck" isn't supported yet. Local regenerate_slide also replaces content elements only; background and theme inheritance are left untouched, unlike the cloud version.

+ +

See it running

+ +

Three short demos, one per app, all running on Token Station.

+ +
+ +
Demo 1 · Docs: Project Overview. GenOffice Docs drafting and editing a project overview document with the AI panel, running on Token Station end to end, no Genspark sign-in involved.
+
+ +
+ +
Demo 2 · Sheets: Project Budget. GenOffice Sheets building out a project budget: formulas, formatting, and AI-assisted edits against the live workbook, all routed through Token Station.
+
+ +
+ +
Demo 3 · Slides: Project Deck. GenOffice Slides generating a full presentation deck, the feature that needed the extra patch in step 5, composed page by page through Token Station instead of Genspark's cloud service.
+
+ +

Where to learn more

+ + + +

Sign up at models.bytefuture.ai ($1 in free credit, no card required), export TOKEN_STATION_API_KEY, and relaunch GenOffice. One key, one endpoint, every model your Docs, Sheets, and Slides sessions need.

diff --git a/src/content/writings/ja/genoffice-meets-token-station.md b/src/content/writings/ja/genoffice-meets-token-station.md new file mode 100644 index 0000000..08c7fa4 --- /dev/null +++ b/src/content/writings/ja/genoffice-meets-token-station.md @@ -0,0 +1,175 @@ +--- +slug: "genoffice-meets-token-station" +lang: "ja" +title: "GenOffice と Token Station:どのモデルでも、使った分だけ" +summary: "GenOffice にはデフォルトで Genspark が組み込まれている。接続先を Token Station に変えるだけで、スイート内のすべてのアプリが好きなモデルで動き、契約課金ではなくリクエスト課金になる。" +category: "tutorial" +date: "2026-08-08" +cta: "https://models.bytefuture.ai/intro.html" +cover: "blog/genoffice-meets-token-station-cover.png" +draft: false +--- + +

GenOffice は、Electron 上に構築されたオープンソースの AI ネイティブなオフィススイートだ。ワープロ、表計算エディタ、プレゼンテーション作成ツール、PDF ビューアが、ひとつのシェルの下にタブでまとめられている。5 つのアプリすべてが共有する約束はひとつ、ファイル形式の忠実性だ。.docx.xlsx.pptx を開いて編集しても、触らなかった部分はバイト単位で完全に元のまま返ってくる。GenOffice は元のファイルを解析し、変更されたブロックだけを追跡し、保存時にはその狭い範囲のパッチを元の XML に差し込む。アーカイブの残りの部分はそのままコピーされ、手を加えられることはない。

+ +

すべてのアプリが同じ AI パネルを備えている。Docs ではバージョン履歴付きのブロック単位編集、Sheets ではライブのワークブックに対してツールを呼び出すエージェント、Slides では自由形式のコードではなく固定された検証済みプリミティブの集合だけを使ってプレゼンテーションを編集する、制約付きのレイアウトスクリプトエージェントだ。この 3 つはいずれも、ツール呼び出しループを担う agent-core と、設定されたモデルバックエンドとの通信を担う ai-provider という 2 つのパッケージを裏側で共有している。

+ +

この記事の要点はまさにそこにある。ai-provider はすでに素の OpenAI 互換 HTTP を話す。デフォルトでは、GenOffice はこれを Genspark に向けている。代わりに Token Station に向ければ、アプリ側は何も変わらない。変わるのはトークンの出どころだけだ。

+ +

Token Station で動かす理由

+ +

Genspark はサインインした瞬間から使えて、デフォルトとして正しい選択だ。ただし、それはひとつのアカウント、ひとつの固定モデルラインナップ、そして Genspark の条件でチャージするクレジット残高でもある。Token Station はこの関係の形を 2 つの点で変える。どちらも、毎日使うデスクトップアプリにとって重要な点だ。

+ + + + + + +
Genspark(デフォルト)Token Station
アカウント / クレジット単一アカウント、単一クレジットプール使った分だけの従量課金、月額・年額契約なし
モデル連携先が固定するラインナップ25 以上のプロバイダーにまたがる 250 以上のモデルをタスクごとに選択
価格Genspark のプランに紐づくクレジットひとつの API キー、プロバイダー料金そのまま、上乗せなし
+ +

従量課金であって契約ではない。Token Station にはサブスクリプション階層がない。クレジットカードなしで無料登録でき、登録直後に 1 ドル分のクレジットが付与される。そこから先は、実際に呼び出したモデルに対してプロバイダーの料金をそのまま支払うだけで、定期的な費用も解約すべきものも発生しない。NVIDIA NIM のような一部のモデルはまったくの無料だ。

+ +

モデルを選ぶ自由。ゲートウェイアカウントはひとつのベンダーのラインナップに縛られない。GenOffice の Docs では長文編集に Claude を使い、Sheets では日常的な数式作業に安価なモデルへ切り替え、Slides ではデッキに合った画像対応モデルを選ぶ。すべて同じ API キー、同じ OpenAI 形式のエンドポイントで行え、プロバイダーごとの個別登録は不要だ。

+ +

Token Station は GenOffice のカスタムプロバイダー枠がもともと想定している OpenAI 互換のワイヤーフォーマットをそのまま話すので、組み込みはルーティングの変更であって、書き直しではない。

+ +

セットアップ:手元の GenOffice にパッチを当てる

+ +

以下は、GenOffice の AI トラフィックを Genspark から Token Station へルーティングし直す実際の変更点だ。自分の fork やブランチにそのまま適用できる。特定の GenOffice リリースに依存する内容ではない。

+ +

1. 前提条件をインストールし、ベースのビルドが動くことを確認する

+ +

マシンに Node.js と npm が必要だ。

+ +
git clone <your-fork-url> genoffice
+cd genoffice
+npm install
+npm run dev
+ +

シェルが起動し、AI パネルが正常に開くことを確認する。この時点ではデフォルトで Genspark 上で動いており、ログアウト状態でメッセージを送信しない限りサインインを求められることはない。

+ +

2. 共有 provider パッケージに環境変数ベースの上書きを追加する

+ +

packages/ai-provider にはすでにカスタムプロバイダーが定義されている。任意の OpenAI 互換の baseUrl / apiKey / model だ。環境変数からそれを埋める小さな関数を追加する。これは、Genspark 自身のキー(GSK_API_KEY)に対してコードベースがすでに使っているのと同じパターンだ。

+ +

packages/ai-provider/src/providers.ts

+ +
export const TOKEN_STATION_BASE_URL = 'https://models.bytefuture.ai/v1'
+const TOKEN_STATION_DEFAULT_MODEL = 'anthropic/claude-opus-4-8'
+
+export function applyTokenStationEnvOverride(
+  settings: AiSettings,
+  env: NodeJS.ProcessEnv = process.env,
+): AiSettings {
+  const apiKey = env.TOKEN_STATION_API_KEY
+  if (!apiKey) return settings
+  return {
+    provider: 'custom',
+    providers: {
+      ...settings.providers,
+      custom: {
+        apiKey,
+        model: env.TOKEN_STATION_MODEL || TOKEN_STATION_DEFAULT_MODEL,
+        baseUrl: TOKEN_STATION_BASE_URL,
+      },
+    },
+  }
+}
+ +

これを defaultAiSettingsresolveAiSettings と並べて、パッケージの index.ts からエクスポートする。

+ +

3. 各アプリで Genspark 強制をやめ、上書き関数を呼び出す

+ +

Docs、Sheets、Slides はそれぞれ ai:get-settings という IPC ハンドラーを登録しており、読み込みのたびに provider を Genspark へハードリセットしている。その行を削除し、代わりに新しい上書き関数を呼び出す。3 つのファイルすべてで形はまったく同じだ。

+ +

apps/docs/src/main/docs-main.tsapps/slides/src/main/ai-ipc.tsapps/sheets/src/main/sheets-main.ts でも同様)

+ +
// before
+ipcMain.handle('ai:get-settings', (): AiSettings => {
+  const stored = readJson<Partial<AiSettings> & LegacyAiSettings>(SETTINGS_PATH(), {})
+  const settings = resolveAiSettings(stored, defaultAiSettings())
+  settings.provider = 'genspark'   // ← delete this
+  return settings
+})
+
+// after
+ipcMain.handle('ai:get-settings', (): AiSettings => {
+  const stored = readJson<Partial<AiSettings> & LegacyAiSettings>(SETTINGS_PATH(), {})
+  return applyTokenStationEnvOverride(resolveAiSettings(stored, defaultAiSettings()))
+})
+ +

Sheets のハンドラーは 2 点だけ見た目が異なる。文字列リテラルではなく IPC チャンネル定数を受け取ること、そして最初に sessionFor(event) のチェックを呼ぶことだ。だが本質は同じで、genspark 強制の行を削除し、新しい上書き関数を呼ぶだけでよい。

+ +

4. API キーを取得し、環境変数に設定する

+ +

Token Station に登録し、ダッシュボードから API キーを取得したら、それを永続的な環境変数として設定し、ターミナルを再起動する(環境変数はそれ以降に起動したプロセスにのみ適用される)。

+ +
# Windows (PowerShell)
+[Environment]::SetEnvironmentVariable("TOKEN_STATION_API_KEY", "gw_...", "User")
+
+# macOS / Linux — add to your shell profile
+export TOKEN_STATION_API_KEY=gw_...
+ +

任意設定として、TOKEN_STATION_MODEL に Token Station 上の provider/model 形式の ID(例えば openai/gpt-5.5)を設定すればデフォルトモデルを上書きできる。GenOffice を再起動すれば、Docs、Sheets、Slides でのチャット・編集・プランニングはすべて Token Station 上で動くようになる。Slides のワンショットのデッキ生成だけは追加のパッチが必要な機能で、これはステップ 5 で扱う。

+ +

5. Slides のデッキ生成にはもうひとつパッチが必要

+ +

Slides の generate_deck / regenerate_slide ツールは、もともと provider システムを完全に迂回して Genspark 専用のクラウドエンドポイントを直接呼び出していた。これらには 3 部構成の独自パッチが必要になる。Docs と Sheets だけをルーティングするなら、ステップ 4 で止めてよい。

+ +

apps/slides/src/renderer/ai/slides-skill.tsDeckAccess インターフェースにオプションのフィールドを 2 つ追加する。同期の aiProvider() ゲッターと、HTML マーカーの代わりに検証済みの要素リストを返す composePageElements() メソッドだ。そのうえで、両方のツールの判定を「Genspark 専用のハードコード」から現在の provider ベースの判定に切り替える。

+ +
const useCloud = cloudAvailable
+  && (access.aiProvider?.() ?? 'genspark') === 'genspark'
+if (!useCloud) {
+  // fall back to runLocalDeckGeneration() / runLocalRegenerateSlide()
+}
+ +

apps/slides/src/renderer/ai/local-deck-gen.ts(新規ファイル):実際に構成処理を行うモジュールで、設定された provider に各ページのレイアウトを JSON(図形、テキストボックス、チャート、画像)として要求し、それを検証したうえで、アプリ自身のエージェントツールがすでに使っている add_shape / add_text_box / add_chart / insert_web_image のプリミティブで構築する。

+ +

apps/slides/src/renderer/ai/AiPanel.tsx:新しく追加した 2 つの DeckAccess フィールドを、generateStyleSkill / planDeckOutline がすでに使っているのと同じリクエストパスに接続する。ここは見落としやすい部分だ。接続しないと aiProvider が undefined のままになり、上記の判定が静かに Genspark へフォールバックしてしまう。

+ +
aiProvider: () => settingsRef.current.provider,
+composePageElements: async (args) => {
+  const { system, user } = buildPageComposePrompt(args)
+  const r = await runLlmOnce(system, user, undefined, true, args.signal)
+  if (!r.ok || !r.text) return { ok: false, error: r.error ?? tGlobal('aiErrEmptyOutput') }
+  return parsePageElementsJson(r.text, args.canvasW, args.canvasH)
+},
+ +

ローカルパス v1 の既知の制約:現状は追加専用で、新しいページは常に現在の最後のスライドを複製する。「デッキ全体の置き換え」にはまだ対応していない。ローカル版の regenerate_slide もコンテンツ要素のみを置き換え、背景とテーマの継承には手を加えない点がクラウド版と異なる。

+ +

実際の動作

+ +

アプリごとに 1 本、合計 3 本の短いデモ。すべて Token Station 上で動いている。

+ +
+ +
デモ1・Docs:プロジェクト概要。GenOffice Docs が AI パネルでプロジェクト概要ドキュメントを作成・編集する様子。Genspark へのサインインは一切なく、最初から最後まで Token Station 上で動いている。
+
+ +
+ +
デモ2・Sheets:プロジェクト予算。GenOffice Sheets がプロジェクト予算表を構築する様子。数式、書式設定、そしてライブのワークブックに対する AI 支援編集まで、すべて Token Station 経由でルーティングされている。
+
+ +
+ +
デモ3・Slides:プロジェクトデッキ。GenOffice Slides がプレゼンテーション全体を生成する様子。ステップ 5 の追加パッチが必要になった機能で、Genspark のクラウドサービスの代わりに Token Station を通じてページごとに生成している。
+
+ +

もっと詳しく

+ + + +

models.bytefuture.ai で登録し(1 ドル分の無料クレジット、クレジットカード不要)、TOKEN_STATION_API_KEY をエクスポートしたら、GenOffice を再起動する。ひとつの API キー、ひとつのエンドポイントで、Docs・Sheets・Slides のセッションに必要なすべてのモデルが使える。

diff --git a/src/content/writings/ko/genoffice-meets-token-station.md b/src/content/writings/ko/genoffice-meets-token-station.md new file mode 100644 index 0000000..b7810c0 --- /dev/null +++ b/src/content/writings/ko/genoffice-meets-token-station.md @@ -0,0 +1,175 @@ +--- +slug: "genoffice-meets-token-station" +lang: "ko" +title: "GenOffice와 Token Station의 만남: 어떤 모델이든, 쓴 만큼만" +summary: "GenOffice에는 기본적으로 Genspark가 연결되어 있다. Token Station으로 연결 대상을 바꾸면 스위트 안의 모든 앱이 원하는 모델로 동작하며, 계약 단위가 아니라 요청 단위로 과금된다." +category: "tutorial" +date: "2026-08-08" +cta: "https://models.bytefuture.ai/intro.html" +cover: "blog/genoffice-meets-token-station-cover.png" +draft: false +--- + +

GenOffice는 Electron 위에 구축된 오픈소스 AI 네이티브 오피스 스위트다. 워드프로세서, 스프레드시트 편집기, 프레젠테이션 제작 도구, PDF 뷰어가 하나의 셸 아래 탭으로 묶여 있다. 다섯 개 앱이 공유하는 하나의 약속은 파일 형식 충실도다. .docx, .xlsx, .pptx를 열어 편집해도 손대지 않은 부분은 바이트 단위로 원본과 동일하게 돌아온다. GenOffice는 원본 파일을 파싱하고 변경된 블록만 추적한 뒤, 저장 시 그 좁은 범위의 패치만 원본 XML에 이어 붙인다. 아카이브의 나머지 부분은 그대로 복사될 뿐 손대지 않는다.

+ +

모든 앱은 동일한 AI 패널을 갖추고 있다. Docs에서는 버전 기록이 있는 블록 단위 편집, Sheets에서는 실시간 워크북에 대해 도구를 호출하는 에이전트, Slides에서는 자유 형식 코드가 아니라 고정된 검증 완료 원시 명령(primitive) 집합만으로 프레젠테이션을 편집하는 제한된 레이아웃 스크립팅 에이전트다. 세 앱 모두 내부적으로 두 개의 패키지를 공유한다. 도구 호출 루프를 담당하는 agent-core와, 설정된 모델 백엔드와의 통신을 담당하는 ai-provider다.

+ +

바로 이 지점이 이 글의 핵심이다. ai-provider는 이미 순수한 OpenAI 호환 HTTP를 사용한다. 기본 설정에서 GenOffice는 이를 Genspark로 연결한다. 대신 Token Station으로 연결하면 앱 자체는 아무것도 바뀌지 않는다. 바뀌는 것은 토큰이 어디서 오느냐뿐이다.

+ +

Token Station으로 구동해야 하는 이유

+ +

Genspark는 로그인하는 순간 바로 쓸 수 있고, 그래서 기본값으로 적절하다. 하지만 하나의 계정, 고정된 하나의 모델 목록, 그리고 Genspark의 조건대로 충전하는 크레딧 잔액이라는 한계도 있다. Token Station은 이 관계의 형태를 두 가지 지점에서 바꾼다. 매일 쓰는 데스크톱 앱에서는 둘 다 중요한 차이다.

+ + + + + + +
Genspark(기본값)Token Station
계정 / 크레딧단일 계정, 단일 크레딧 풀쓴 만큼만 지불, 월간·연간 계약 없음
모델연동 방식이 정한 고정 목록25개 이상 제공사, 250개 이상 모델 중 작업별 선택
가격Genspark 요금제에 묶인 크레딧키 하나, 제공사 원가, 마진 없음
+ +

쓴 만큼만 지불, 계약이 아니다. Token Station에는 구독 등급이 없다. 카드 없이 무료로 가입하면 즉시 1달러 크레딧이 지급된다. 이후에는 실제로 호출한 모델에 대해 제공사 가격을 그대로 지불할 뿐, 정기적으로 나가는 비용도 해지할 것도 없다. NVIDIA NIM 같은 일부 모델은 아예 무료다.

+ +

모델을 고를 수 있는 자유. 게이트웨이 계정은 특정 벤더의 라인업에 묶이지 않는다. GenOffice Docs에서는 장문 편집에 Claude를 쓰고, Sheets에서는 일상적인 수식 작업에 더 저렴한 모델로 바꾸고, Slides에서는 덱에 맞는 이미지 지원 모델을 고르면 된다. 모두 같은 키, 같은 OpenAI 방식 엔드포인트로 처리되며 제공사별로 따로 가입할 필요가 없다.

+ +

Token Station은 GenOffice의 커스텀 provider 슬롯이 원래 기대하는 것과 같은 OpenAI 호환 통신 방식을 그대로 사용하므로, 이를 연결하는 작업은 재작성이 아니라 라우팅 변경에 가깝다.

+ +

설정: GenOffice 체크아웃에 패치 적용하기

+ +

아래는 GenOffice의 AI 트래픽을 Genspark에서 Token Station으로 라우팅하도록 바꾸는 실제 변경 사항이다. 자신의 fork나 브랜치에 그대로 적용하면 된다. 특정 GenOffice 릴리스에 종속된 내용은 없다.

+ +

1. 사전 준비물을 설치하고 기본 빌드가 동작하는지 확인한다

+ +

Node.js와 npm이 설치되어 있어야 한다.

+ +
git clone <your-fork-url> genoffice
+cd genoffice
+npm install
+npm run dev
+ +

셸이 실행되고 AI 패널이 정상적으로 열리는지 확인한다. 이 시점에는 기본값인 Genspark로 동작하며, 로그아웃 상태에서 실제로 메시지를 보내지 않는 한 로그인 요청은 뜨지 않는다.

+ +

2. 공유 provider 패키지에 환경 변수 기반 오버라이드 추가

+ +

packages/ai-provider에는 이미 커스텀 provider가 정의되어 있다. 임의의 OpenAI 호환 baseUrl / apiKey / model이다. 환경 변수에서 값을 읽어 이를 채우는 작은 함수를 추가한다. Genspark 자체 키(GSK_API_KEY)에 대해 코드베이스가 이미 쓰고 있는 것과 동일한 패턴이다.

+ +

packages/ai-provider/src/providers.ts

+ +
export const TOKEN_STATION_BASE_URL = 'https://models.bytefuture.ai/v1'
+const TOKEN_STATION_DEFAULT_MODEL = 'anthropic/claude-opus-4-8'
+
+export function applyTokenStationEnvOverride(
+  settings: AiSettings,
+  env: NodeJS.ProcessEnv = process.env,
+): AiSettings {
+  const apiKey = env.TOKEN_STATION_API_KEY
+  if (!apiKey) return settings
+  return {
+    provider: 'custom',
+    providers: {
+      ...settings.providers,
+      custom: {
+        apiKey,
+        model: env.TOKEN_STATION_MODEL || TOKEN_STATION_DEFAULT_MODEL,
+        baseUrl: TOKEN_STATION_BASE_URL,
+      },
+    },
+  }
+}
+ +

이 함수를 defaultAiSettings, resolveAiSettings와 함께 패키지의 index.ts에서 내보낸다.

+ +

3. 각 앱이 Genspark를 강제하지 않도록 하고, 오버라이드를 적용한다

+ +

Docs, Sheets, Slides는 각각 ai:get-settings IPC 핸들러를 등록해 두었는데, 읽을 때마다 provider를 Genspark로 강제 리셋한다. 그 줄을 지우고 새 오버라이드 함수를 호출하도록 바꾼다. 세 파일 모두 형태는 동일하다.

+ +

apps/docs/src/main/docs-main.ts (apps/slides/src/main/ai-ipc.ts, apps/sheets/src/main/sheets-main.ts도 동일)

+ +
// before
+ipcMain.handle('ai:get-settings', (): AiSettings => {
+  const stored = readJson<Partial<AiSettings> & LegacyAiSettings>(SETTINGS_PATH(), {})
+  const settings = resolveAiSettings(stored, defaultAiSettings())
+  settings.provider = 'genspark'   // ← delete this
+  return settings
+})
+
+// after
+ipcMain.handle('ai:get-settings', (): AiSettings => {
+  const stored = readJson<Partial<AiSettings> & LegacyAiSettings>(SETTINGS_PATH(), {})
+  return applyTokenStationEnvOverride(resolveAiSettings(stored, defaultAiSettings()))
+})
+ +

Sheets의 핸들러는 두 가지 사소한 차이가 있다. 문자열 리터럴 대신 IPC 채널 상수를 받는다는 점, 그리고 맨 앞에서 sessionFor(event) 검사를 한 번 먼저 호출한다는 점이다. 본질은 동일하다. genspark 강제 줄을 지우고 새 오버라이드를 호출하면 된다.

+ +

4. 키를 발급받아 환경 변수에 넣는다

+ +

Token Station에 가입하고 대시보드에서 API 키를 받은 뒤, 이를 영구 환경 변수로 설정하고 터미널을 재시작한다(환경 변수는 이후에 실행되는 프로세스에만 적용된다).

+ +
# Windows (PowerShell)
+[Environment]::SetEnvironmentVariable("TOKEN_STATION_API_KEY", "gw_...", "User")
+
+# macOS / Linux — add to your shell profile
+export TOKEN_STATION_API_KEY=gw_...
+ +

선택 사항으로, TOKEN_STATION_MODEL에 Token Station의 provider/model 형식 ID(예: openai/gpt-5.5)를 설정하면 기본 모델을 바꿀 수 있다. GenOffice를 재실행하면 Docs, Sheets, Slides 전반의 채팅, 편집, 계획 기능이 모두 Token Station 위에서 동작한다. Slides의 원샷 덱 생성 기능만 추가 패치가 필요한데, 이는 5단계에서 다룬다.

+ +

5. Slides 덱 생성에는 패치가 하나 더 필요하다

+ +

Slides의 generate_deck/regenerate_slide 도구는 원래 provider 시스템을 완전히 우회해 Genspark 전용 클라우드 엔드포인트를 직접 호출했다. 이 둘에는 세 부분으로 이루어진 별도 패치가 필요하다. Docs와 Sheets만 라우팅할 생각이라면 4단계에서 멈춰도 된다.

+ +

apps/slides/src/renderer/ai/slides-skill.ts: DeckAccess 인터페이스에 선택적 필드 두 개를 추가한다. 동기 방식의 aiProvider() getter와, HTML 마커 대신 검증된 요소 목록을 반환하는 composePageElements() 메서드다. 그런 다음 두 도구의 판단 기준을 "Genspark 전용 하드코딩"에서 현재 provider 기반 판단으로 바꾼다.

+ +
const useCloud = cloudAvailable
+  && (access.aiProvider?.() ?? 'genspark') === 'genspark'
+if (!useCloud) {
+  // fall back to runLocalDeckGeneration() / runLocalRegenerateSlide()
+}
+ +

apps/slides/src/renderer/ai/local-deck-gen.ts(신규 파일): 실제 구성을 담당하는 모듈로, 설정된 provider에 각 페이지의 레이아웃을 JSON(도형, 텍스트 박스, 차트, 이미지)으로 요청하고 검증한 뒤, 앱 자체 에이전트 도구가 이미 쓰고 있는 add_shape / add_text_box / add_chart / insert_web_image 원시 명령으로 페이지를 구성한다.

+ +

apps/slides/src/renderer/ai/AiPanel.tsx: 새로 추가한 두 DeckAccess 필드를 generateStyleSkill/planDeckOutline이 이미 쓰고 있는 것과 같은 요청 경로에 연결한다. 이 부분은 놓치기 쉽다. 연결하지 않으면 aiProvider가 계속 undefined로 남아, 위의 판단 로직이 조용히 Genspark로 되돌아간다.

+ +
aiProvider: () => settingsRef.current.provider,
+composePageElements: async (args) => {
+  const { system, user } = buildPageComposePrompt(args)
+  const r = await runLlmOnce(system, user, undefined, true, args.signal)
+  if (!r.ok || !r.text) return { ok: false, error: r.error ?? tGlobal('aiErrEmptyOutput') }
+  return parsePageElementsJson(r.text, args.canvasW, args.canvasH)
+},
+ +

로컬 경로 v1의 알려진 한계: 현재는 추가 전용이라 새 페이지는 항상 현재 마지막 슬라이드를 복제하며, "덱 전체 교체"는 아직 지원하지 않는다. 로컬 버전의 regenerate_slide 역시 콘텐츠 요소만 교체하며, 배경과 테마 상속은 건드리지 않는다는 점이 클라우드 버전과 다르다.

+ +

실제 동작 확인

+ +

앱마다 하나씩, 총 세 개의 짧은 데모다. 모두 Token Station 위에서 동작한다.

+ +
+ +
데모 1 · Docs: 프로젝트 개요. GenOffice Docs가 AI 패널로 프로젝트 개요 문서를 작성하고 편집하는 모습. Genspark 로그인은 전혀 관여하지 않고 처음부터 끝까지 Token Station 위에서 동작한다.
+
+ +
+ +
데모 2 · Sheets: 프로젝트 예산. GenOffice Sheets가 프로젝트 예산표를 구성하는 모습. 수식, 서식 지정, 실시간 워크북에 대한 AI 지원 편집까지 모두 Token Station을 거쳐 처리된다.
+
+ +
+ +
데모 3 · Slides: 프로젝트 덱. GenOffice Slides가 프레젠테이션 전체를 생성하는 모습. 5단계의 추가 패치가 필요했던 기능으로, Genspark의 클라우드 서비스 대신 Token Station을 통해 페이지 단위로 생성된다.
+
+ +

더 알아보기

+ + + +

models.bytefuture.ai에서 가입하고(카드 없이 1달러 무료 크레딧), TOKEN_STATION_API_KEY를 내보낸 뒤 GenOffice를 재실행한다. 키 하나, 엔드포인트 하나로 Docs, Sheets, Slides 세션에 필요한 모든 모델을 쓸 수 있다.

diff --git a/src/content/writings/zh/genoffice-meets-token-station.md b/src/content/writings/zh/genoffice-meets-token-station.md new file mode 100644 index 0000000..7aebf55 --- /dev/null +++ b/src/content/writings/zh/genoffice-meets-token-station.md @@ -0,0 +1,175 @@ +--- +slug: "genoffice-meets-token-station" +lang: "zh" +title: "GenOffice 携手 Token Station:任选模型,按量付费" +summary: "GenOffice 默认接入 Genspark。把它改接到 Token Station,套件里的每个应用都能运行你选择的任意模型,按请求计费,而不是按合同计费。" +category: "tutorial" +date: "2026-08-08" +cta: "https://models.bytefuture.ai/intro.html" +cover: "blog/genoffice-meets-token-station-cover.png" +draft: false +--- + +

GenOffice 是一款开源、AI 原生的办公套件,基于 Electron 构建:文字处理器、表格编辑器、演示文稿制作工具和 PDF 阅读器,全部整合在同一个外壳下。五个应用共享一个底层承诺:文件格式保真。打开一份 .docx.xlsx.pptx,完成编辑后,凡是你没碰过的内容都会原样返回,逐字节一致。GenOffice 会解析原始文件,只追踪你改动过的区块,保存时把窄幅补丁拼回源 XML;归档中的其余部分则原样复制,不做改动。

+ +

每个应用都带有同样的 AI 面板:Docs 里是带版本历史的区块级编辑,Sheets 里是对实时工作表进行工具调用的智能体,Slides 里则是一个受限的布局脚本智能体,只通过一组固定的、经过校验的操作原语来编辑演示文稿,而不是自由形式的代码。三者底层共用两个包:负责工具调用循环的 agent-core,以及负责对接所配置模型后端的 ai-provider

+ +

这正是本文的切入点:ai-provider 本身就说标准的 OpenAI 兼容 HTTP 协议。开箱即用时,GenOffice 把它指向 Genspark。把它改指向 Token Station,各个应用不会有任何变化:变的只是 token 从哪里来。

+ +

为什么要用 Token Station 来跑

+ +

登录后 Genspark 立刻就能用,这也是它作为默认选项的合理之处。但它是单一账户、单一模型列表,信用额度也要按 Genspark 的条款充值。Token Station 在两个方面改变了这层关系,而这两点对每天都要用的桌面应用来说很关键。

+ + + + + + +
Genspark(默认)Token Station
账户 / 额度单一账户,单一信用池按量付费,没有月度或年度合同
模型由集成方固定模型列表25+ 家供应商、250+ 个模型,按任务挑选
定价额度绑定 Genspark 套餐一把密钥,供应商原价,零加价
+ +

按量付费,不是签合同。Token Station 没有订阅档位。免费注册,无需信用卡,注册后立即到账 1 美元额度。之后你只按实际调用的模型付供应商原价,没有周期性费用,也没有需要取消的东西。像 NVIDIA NIM 这样的部分模型甚至完全免费。

+ +

模型选择的自由。网关账户不会被锁定在某一家供应商的产品线上。GenOffice Docs 用 Claude 做长文编辑,Sheets 用更便宜的模型处理日常公式工作,Slides 则挑选适合当前演示稿的图像模型,全部通过同一把密钥、同一个 OpenAI 风格的接口完成,不需要为每家供应商单独注册。

+ +

因为 Token Station 使用的正是 GenOffice 自定义提供方接口本就预期的 OpenAI 兼容线上协议,接入它只是一次路由变更,不是一次重写。

+ +

配置步骤:给你的 GenOffice 代码打补丁

+ +

下面是把 GenOffice 的 AI 流量从 Genspark 改路由到 Token Station 所需的实际改动。把它们应用到你自己的 fork 或分支上即可;这些改动不依赖任何特定的 GenOffice 版本。

+ +

1. 安装依赖,确认基线版本能正常运行

+ +

你需要在本机安装 Node.js 和 npm。

+ +
git clone <your-fork-url> genoffice
+cd genoffice
+npm install
+npm run dev
+ +

确认外壳能正常启动,AI 面板也能正常打开。此时它默认跑在 Genspark 上,只要不在未登录状态下发送消息,就不会弹出登录提示。

+ +

2. 给共享的 provider 包加上环境变量覆盖逻辑

+ +

packages/ai-provider 里已经定义了一个自定义提供方:任意 OpenAI 兼容的 baseUrl / apiKey / model。我们添加一个小函数,从环境变量里读取这些值来填充它,这与代码库里给 Genspark 自身密钥(GSK_API_KEY)用的模式完全一致。

+ +

packages/ai-provider/src/providers.ts

+ +
export const TOKEN_STATION_BASE_URL = 'https://models.bytefuture.ai/v1'
+const TOKEN_STATION_DEFAULT_MODEL = 'anthropic/claude-opus-4-8'
+
+export function applyTokenStationEnvOverride(
+  settings: AiSettings,
+  env: NodeJS.ProcessEnv = process.env,
+): AiSettings {
+  const apiKey = env.TOKEN_STATION_API_KEY
+  if (!apiKey) return settings
+  return {
+    provider: 'custom',
+    providers: {
+      ...settings.providers,
+      custom: {
+        apiKey,
+        model: env.TOKEN_STATION_MODEL || TOKEN_STATION_DEFAULT_MODEL,
+        baseUrl: TOKEN_STATION_BASE_URL,
+      },
+    },
+  }
+}
+ +

把它和 defaultAiSettingsresolveAiSettings 一起从包的 index.ts 中导出。

+ +

3. 让每个应用不再强制使用 Genspark,改为调用新的覆盖函数

+ +

Docs、Sheets、Slides 各自注册了一个 ai:get-settings IPC 处理函数,每次读取时都会硬性把 provider 重置回 Genspark。删掉那一行,改为调用新的覆盖函数。三个文件里的改法完全一致:

+ +

apps/docs/src/main/docs-main.tsapps/slides/src/main/ai-ipc.tsapps/sheets/src/main/sheets-main.ts 中的改法与此相同)

+ +
// before
+ipcMain.handle('ai:get-settings', (): AiSettings => {
+  const stored = readJson<Partial<AiSettings> & LegacyAiSettings>(SETTINGS_PATH(), {})
+  const settings = resolveAiSettings(stored, defaultAiSettings())
+  settings.provider = 'genspark'   // ← delete this
+  return settings
+})
+
+// after
+ipcMain.handle('ai:get-settings', (): AiSettings => {
+  const stored = readJson<Partial<AiSettings> & LegacyAiSettings>(SETTINGS_PATH(), {})
+  return applyTokenStationEnvOverride(resolveAiSettings(stored, defaultAiSettings()))
+})
+ +

Sheets 的处理函数在两处写法上略有不同:它接收的是一个 IPC 通道常量而不是字符串字面量,而且开头会先调用一次 sessionFor(event) 检查。但改动的实质完全一样:删掉强制 genspark 的那一行,改为调用新的覆盖函数。

+ +

4. 获取密钥,配置到环境变量中

+ +

Token Station 注册,从控制台拿到一个 API 密钥,然后把它设为持久化的环境变量,并重启终端(环境变量只对之后启动的进程生效)。

+ +
# Windows (PowerShell)
+[Environment]::SetEnvironmentVariable("TOKEN_STATION_API_KEY", "gw_...", "User")
+
+# macOS / Linux — add to your shell profile
+export TOKEN_STATION_API_KEY=gw_...
+ +

可选:设置 TOKEN_STATION_MODEL 为 Token Station 上任意 provider/model 形式的 ID(例如 openai/gpt-5.5)来覆盖默认模型。重新启动 GenOffice:Docs、Sheets、Slides 里的聊天、编辑和规划功能现在都跑在 Token Station 上了。Slides 的一键生成整套演示稿功能是唯一还需要额外打补丁的功能,见第 5 步。

+ +

5. Slides 生成演示稿功能还需要一个补丁

+ +

Slides 的 generate_deck/regenerate_slide 工具最初直接调用一个只支持 Genspark 的云端接口,完全绕过了 provider 系统。它们需要单独打三部分补丁。如果你只想接通 Docs 和 Sheets,做到第 4 步就可以停下。

+ +

apps/slides/src/renderer/ai/slides-skill.ts:给 DeckAccess 接口加两个可选字段,一个同步的 aiProvider() getter,以及一个返回校验过的元素列表(而不是 HTML 标记)的 composePageElements() 方法。然后把两个工具的判断逻辑从「硬编码只认 Genspark」改为按当前 provider 判断:

+ +
const useCloud = cloudAvailable
+  && (access.aiProvider?.() ?? 'genspark') === 'genspark'
+if (!useCloud) {
+  // fall back to runLocalDeckGeneration() / runLocalRegenerateSlide()
+}
+ +

apps/slides/src/renderer/ai/local-deck-gen.ts(新文件):这是真正负责组合内容的模块,向所配置的 provider 请求每一页的布局 JSON(形状、文本框、图表、图片),校验之后用应用自身智能体工具已经在用的 add_shape / add_text_box / add_chart / insert_web_image 原语来构建页面。

+ +

apps/slides/src/renderer/ai/AiPanel.tsx:把新增的两个 DeckAccess 字段接到 generateStyleSkill/planDeckOutline 已经在用的同一条请求路径上。这一步很容易漏掉:不接的话 aiProvider 会一直是 undefined,上面的判断逻辑会悄悄退回到 Genspark。

+ +
aiProvider: () => settingsRef.current.provider,
+composePageElements: async (args) => {
+  const { system, user } = buildPageComposePrompt(args)
+  const r = await runLlmOnce(system, user, undefined, true, args.signal)
+  if (!r.ok || !r.text) return { ok: false, error: r.error ?? tGlobal('aiErrEmptyOutput') }
+  return parsePageElementsJson(r.text, args.canvasW, args.canvasH)
+},
+ +

本地路径 v1 版本的已知限制:目前只支持追加,新页面会克隆当前最后一页,还不支持「替换整套演示稿」。本地版的 regenerate_slide 也只替换内容元素,背景和主题继承部分不会被改动,这一点与云端版本不同。

+ +

实际运行效果

+ +

三段简短演示,每个应用一段,全部跑在 Token Station 上。

+ +
+ +
演示一 · Docs:项目概述。GenOffice Docs 用 AI 面板起草并编辑一份项目概述文档,全程跑在 Token Station 上,不涉及 Genspark 登录。
+
+ +
+ +
演示二 · Sheets:项目预算。GenOffice Sheets 搭建一份项目预算表:公式、格式设置、以及针对实时工作表的 AI 辅助编辑,全部通过 Token Station 路由。
+
+ +
+ +
演示三 · Slides:项目演示稿。GenOffice Slides 生成一整套演示文稿,这正是第 5 步额外补丁所支持的功能,通过 Token Station 而不是 Genspark 的云端服务逐页生成。
+
+ +

延伸阅读

+ + + +

前往 models.bytefuture.ai 注册(1 美元免费额度,无需信用卡),导出 TOKEN_STATION_API_KEY,然后重新启动 GenOffice。一把密钥、一个接口,满足你 Docs、Sheets、Slides 会话需要的所有模型。

diff --git a/src/layouts/WritingLayout.astro b/src/layouts/WritingLayout.astro index 8a17fbc..8416c57 100644 --- a/src/layouts/WritingLayout.astro +++ b/src/layouts/WritingLayout.astro @@ -118,6 +118,8 @@ const dateText = new Intl.DateTimeFormat(lang === 'zh' ? 'zh-CN' : lang === 'ja' .prose p,.prose ul,.prose ol { color:#27272a; margin:0 0 24px; } .prose figure { margin:32px 0; max-width:100%; overflow:hidden; } .prose img { display:block; width:100%; max-width:100%; height:auto; object-fit:contain; } + .prose video { display:block; width:100%; max-width:100%; height:auto; border-radius:12px; border:1px solid #e4e4e7; } + .prose figcaption { font-size:14px; color:#71717a; margin-top:10px; text-align:center; } .prose pre { background:#fff; border:1px solid #e4e4e7; border-radius:12px; padding:20px 24px; overflow-x:auto; margin:32px 0; max-width:100%; } .prose pre, .prose code { font-family:'JetBrains Mono', ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; font-size:0.9em; } .code-wrap { position:relative; margin:32px 0; }