@aisa-one/cli 0.3.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (102) hide show
  1. package/CHANGELOG.md +143 -1
  2. package/LICENSE +21 -0
  3. package/README.md +151 -162
  4. package/dist/api.d.ts +10 -0
  5. package/dist/api.js +14 -3
  6. package/dist/catalog.d.ts +1 -18
  7. package/dist/catalog.js +1 -24
  8. package/dist/cli-error.d.ts +17 -0
  9. package/dist/cli-error.js +25 -0
  10. package/dist/cli-guidance.d.ts +14 -0
  11. package/dist/cli-guidance.js +55 -0
  12. package/dist/commands/account.d.ts +17 -0
  13. package/dist/commands/account.js +45 -1
  14. package/dist/commands/api.d.ts +0 -11
  15. package/dist/commands/api.js +6 -175
  16. package/dist/commands/auth.d.ts +2 -1
  17. package/dist/commands/auth.js +43 -9
  18. package/dist/commands/brand-logos.d.ts +6 -0
  19. package/dist/commands/brand-logos.js +21 -0
  20. package/dist/commands/configCmd.js +8 -3
  21. package/dist/commands/connect-shared.d.ts +136 -0
  22. package/dist/commands/connect-shared.js +143 -0
  23. package/dist/commands/connect-t2.d.ts +18 -0
  24. package/dist/commands/connect-t2.js +1599 -0
  25. package/dist/commands/connect-terminal.d.ts +65 -0
  26. package/dist/commands/connect-terminal.js +822 -0
  27. package/dist/commands/connect.d.ts +31 -0
  28. package/dist/commands/connect.js +3181 -0
  29. package/dist/commands/flow.d.ts +685 -0
  30. package/dist/commands/flow.js +697 -0
  31. package/dist/commands/install.d.ts +91 -0
  32. package/dist/commands/install.js +258 -0
  33. package/dist/commands/llm-config.d.ts +103 -0
  34. package/dist/commands/llm-config.js +452 -0
  35. package/dist/commands/manifest.d.ts +68 -0
  36. package/dist/commands/manifest.js +90 -0
  37. package/dist/commands/mcp.d.ts +59 -0
  38. package/dist/commands/mcp.js +122 -44
  39. package/dist/commands/oauth-login.d.ts +33 -0
  40. package/dist/commands/oauth-login.js +333 -0
  41. package/dist/commands/prompt.d.ts +96 -0
  42. package/dist/commands/prompt.js +397 -0
  43. package/dist/commands/serve-results.d.ts +61 -0
  44. package/dist/commands/serve-results.js +245 -0
  45. package/dist/commands/serve-signin.d.ts +28 -0
  46. package/dist/commands/serve-signin.js +90 -0
  47. package/dist/commands/signin-page.d.ts +49 -0
  48. package/dist/commands/signin-page.js +250 -0
  49. package/dist/commands/skills.js +87 -50
  50. package/dist/commands/tool-help.d.ts +69 -0
  51. package/dist/commands/tool-help.js +189 -0
  52. package/dist/commands/tool-input.d.ts +22 -0
  53. package/dist/commands/tool-input.js +220 -0
  54. package/dist/commands/tools.d.ts +12 -0
  55. package/dist/commands/tools.js +307 -0
  56. package/dist/commands/update.d.ts +11 -0
  57. package/dist/commands/update.js +92 -0
  58. package/dist/commands/vscode.d.ts +65 -0
  59. package/dist/commands/vscode.js +173 -0
  60. package/dist/commands/wrappers.d.ts +23 -0
  61. package/dist/commands/wrappers.js +102 -0
  62. package/dist/completion.js +3 -80
  63. package/dist/config.js +76 -2
  64. package/dist/constants.d.ts +51 -38
  65. package/dist/constants.js +53 -109
  66. package/dist/index.js +152 -355
  67. package/dist/json-text.d.ts +12 -0
  68. package/dist/json-text.js +60 -0
  69. package/dist/router.d.ts +39 -0
  70. package/dist/router.js +59 -0
  71. package/dist/skills-registry.js +12 -3
  72. package/dist/utils/banner.d.ts +9 -0
  73. package/dist/utils/banner.js +41 -0
  74. package/dist/utils/browser.d.ts +19 -0
  75. package/dist/utils/browser.js +31 -0
  76. package/dist/utils/exec.d.ts +43 -0
  77. package/dist/utils/exec.js +61 -0
  78. package/dist/utils/http.d.ts +53 -0
  79. package/dist/utils/http.js +124 -0
  80. package/dist/utils/install-method.d.ts +36 -0
  81. package/dist/utils/install-method.js +48 -0
  82. package/dist/utils/journal.d.ts +57 -0
  83. package/dist/utils/journal.js +188 -0
  84. package/dist/utils/key-check.d.ts +20 -0
  85. package/dist/utils/key-check.js +24 -0
  86. package/dist/utils/update-check.d.ts +23 -0
  87. package/dist/utils/update-check.js +116 -0
  88. package/dist/utils/width.d.ts +32 -0
  89. package/dist/utils/width.js +73 -0
  90. package/package.json +8 -4
  91. package/vscode-extension/README.md +6 -0
  92. package/vscode-extension/aisa-vscode.vsix +0 -0
  93. package/dist/commands/finance.d.ts +0 -17
  94. package/dist/commands/finance.js +0 -342
  95. package/dist/commands/run.d.ts +0 -28
  96. package/dist/commands/run.js +0 -182
  97. package/dist/commands/search.d.ts +0 -9
  98. package/dist/commands/search.js +0 -81
  99. package/dist/commands/twitter.d.ts +0 -138
  100. package/dist/commands/twitter.js +0 -374
  101. package/dist/commands/video.d.ts +0 -49
  102. package/dist/commands/video.js +0 -311
@@ -0,0 +1,697 @@
1
+ /**
2
+ * What `aisa connect` asks, in the order it asks it — as data.
3
+ *
4
+ * The page and the terminal are two renderers of this file, not two programs
5
+ * that happen to agree. Before it existed they were the latter: every sentence
6
+ * on the T2 page lived inside a template string (82% of that file is markup),
7
+ * so the terminal carried a hand-written paraphrase of the same choices. Two
8
+ * copies of the same wording drift, and nothing catches it — there is no test
9
+ * that can compare a sentence in HTML against a sentence in a log line.
10
+ *
11
+ * So copy lives here, beside the structure it belongs to. A step that the page
12
+ * renders as a card and the terminal renders as a numbered menu is still one
13
+ * step, with one question and one set of options.
14
+ *
15
+ * ── Language ──────────────────────────────────────────────────────────────
16
+ * Every user-facing string is a `Text`: `{ en, zh }`. This is a product
17
+ * surface, not the machine-readable one — `aisa manifest`, MCP tool
18
+ * descriptions and OpenAPI summaries stay English-only, per the workspace
19
+ * convention, because those are read by agents everywhere. What a person sees
20
+ * while choosing is a different audience.
21
+ *
22
+ * The chosen language is stored by the CLI (see config.ts), not in the
23
+ * browser: the page is served by this process, and a language kept only in
24
+ * localStorage would leave the terminal in English while the page is in
25
+ * Chinese — the exact split this file exists to prevent.
26
+ *
27
+ * ── Scope ─────────────────────────────────────────────────────────────────
28
+ * Step 2 (Your agent) is here now. The remaining steps still render from
29
+ * connect-t2.ts and are being moved one at a time, each behind the byte-exact
30
+ * page snapshots in tests/connect-snapshot.test.ts.
31
+ */
32
+ export const LANGS = ["en", "zh"];
33
+ export function t(text, lang) {
34
+ return text[lang] ?? text.en;
35
+ }
36
+ /**
37
+ * Resolve the language for this run: explicit flag, then what was chosen and
38
+ * remembered, then the OS, then English.
39
+ *
40
+ * The OS check is deliberately loose — `zh_CN.UTF-8`, `zh-Hans`, `zh_TW` all
41
+ * mean a reader who would rather not be handed English.
42
+ */
43
+ export function resolveLang(flag, stored, env = process.env) {
44
+ const known = (v) => LANGS.includes(v) ? v : undefined;
45
+ if (known(flag))
46
+ return known(flag);
47
+ if (known(stored))
48
+ return known(stored);
49
+ const locale = env.LC_ALL || env.LC_MESSAGES || env.LANG || "";
50
+ if (/^zh/i.test(locale))
51
+ return "zh";
52
+ return "en";
53
+ }
54
+ // ── step 2: your agent ──────────────────────────────────────────────────────
55
+ export const STEP_AGENT = {
56
+ id: "agent",
57
+ n: 2,
58
+ title: { en: "Your agent", zh: "你的 agent" },
59
+ sub: { en: "Pick the tool to connect", zh: "选择要连接的工具" },
60
+ question: {
61
+ en: "Which agent should AIsa plug into?",
62
+ zh: "AIsa 要接入哪个 agent?",
63
+ },
64
+ lede: {
65
+ en: "One agent per run, so a problem is always easy to place. Detected tools are ready to connect; a missing one can be installed right here through its official installer.",
66
+ zh: "一次只连一个 agent,出问题时永远知道该看哪里。已检测到的可以直接连接;没装的可以在这里用它官方的安装方式装上。",
67
+ },
68
+ };
69
+ /**
70
+ * Fixed card order, whatever is or is not installed: the terminal agents
71
+ * first, then the editors, then the web targets. Order is part of the flow,
72
+ * not of one renderer — a menu that renumbers itself between the page and the
73
+ * terminal would make "pick 2" mean two different things.
74
+ */
75
+ export const AGENT_ORDER = [
76
+ "claude-code",
77
+ "codex",
78
+ "opencode",
79
+ "vscode",
80
+ "cursor",
81
+ "claude-desktop",
82
+ "claude-ai",
83
+ ];
84
+ export function agentRank(id) {
85
+ const i = AGENT_ORDER.indexOf(id);
86
+ return i === -1 ? 99 : i;
87
+ }
88
+ /** Badges on an agent card. The terminal renders the same three states. */
89
+ export const AGENT_BADGE = {
90
+ detected: { en: "✓ detected", zh: "✓ 已检测到" },
91
+ absent: { en: "not installed", zh: "未安装" },
92
+ web: { en: "web · no install", zh: "网页版 · 无需安装" },
93
+ notFound: { en: "not found", zh: "未找到" },
94
+ soon: { en: "soon", zh: "即将支持" },
95
+ };
96
+ export const AGENT_SIDE_TITLES = {
97
+ how: { en: "How it connects", zh: "它是怎么连上的" },
98
+ have: { en: "Already have it set up?", zh: "已经配置过了怎么办?" },
99
+ };
100
+ /** Offer to install a missing agent, with the exact command that will run. */
101
+ /** Fill {name} placeholders. Keeps punctuation inside the translated string. */
102
+ export function fill(text, lang, vars) {
103
+ return Object.entries(vars).reduce((s, [k, v]) => s.split(`{${k}}`).join(String(v)), t(text, lang));
104
+ }
105
+ export function installOffer(command, lang) {
106
+ return lang === "zh"
107
+ ? `安装<b>并</b>连接 — <code>${command}</code>`
108
+ : `Install <b>and</b> connect it — <code>${command}</code>`;
109
+ }
110
+ /**
111
+ * What connecting each agent actually does, in its own terms.
112
+ *
113
+ * These name the real mechanism — `claude mcp add`, a provider block in
114
+ * config.toml — because the question a reader has at this step is "what are
115
+ * you about to do to my machine", and the honest answer is the command.
116
+ */
117
+ export const AGENT_NOTES = {
118
+ "claude-code": {
119
+ en: "Servers are added with <code>claude mcp add</code> in user scope — the official mechanism, reversible with <code>claude mcp remove</code>. Models, if you choose to, are set through Claude Code's own settings file.",
120
+ zh: "通过 <code>claude mcp add</code> 写入 user scope —— 这是官方机制,用 <code>claude mcp remove</code> 就能撤销。模型(如果你选择切换)走 Claude Code 自己的设置文件。",
121
+ },
122
+ codex: {
123
+ en: "Servers are added with <code>codex mcp add</code>, Codex's own command. Models go in an <code>aisa</code> provider inside <code>~/.codex/config.toml</code> — only keys we wrote are ever touched.",
124
+ zh: "通过 Codex 自己的 <code>codex mcp add</code> 写入。模型写成 <code>~/.codex/config.toml</code> 里的一个 <code>aisa</code> provider —— 只动我们自己写的那些键。",
125
+ },
126
+ opencode: {
127
+ en: "Servers are added with <code>opencode mcp add</code>. Models become an extra <code>aisa</code> provider in <code>opencode.json</code>; pick <code>aisa/…</code> from its model list.",
128
+ zh: "通过 <code>opencode mcp add</code> 写入。模型会成为 <code>opencode.json</code> 里额外的一个 <code>aisa</code> provider,在它的模型列表里选 <code>aisa/…</code> 即可。",
129
+ },
130
+ vscode: {
131
+ en: "Nothing is done by hand. The AIsa servers go into VS Code's <code>mcp.json</code> (with your key, or VS Code signs in to them itself), and a small <b>AIsa extension</b> is installed that asks VS Code to store your key and add the models as a <b>Custom Endpoint</b> group named AIsa — beside Copilot's own, which stay. Inline completions keep using Copilot.",
132
+ zh: "全程无需手动操作。AIsa 的 server 写进 VS Code 的 <code>mcp.json</code>(带上你的 key,或者由 VS Code 自己去登录),同时装一个很小的 <b>AIsa 扩展</b>,由它请 VS Code 保管你的 key 并把模型添加为名为 AIsa 的 <b>Custom Endpoint</b> 分组 —— 与 Copilot 自己的并存,不动它。行内补全仍然用 Copilot。",
133
+ },
134
+ cursor: {
135
+ en: "One click per server: each becomes an <b>Add to Cursor</b> link on the last step. Cursor opens, shows you the exact entry, and writes it into its own MCP settings when you confirm. Models stay as they are — Cursor picks its own.",
136
+ zh: "每个 server 一次点击:最后一步会给出 <b>Add to Cursor</b> 链接。Cursor 会打开、把确切的配置内容给你看,你确认后由它自己写进 MCP 设置。模型保持原样 —— Cursor 用它自己的。",
137
+ },
138
+ "claude-desktop": {
139
+ en: "Claude Desktop's config file only takes MCP servers over stdio, so each AIsa server is written in as a small <code>mcp-remote</code> bridge carrying your key (plus the free <code>aisa-docs</code> server). The bridges live and die with the app — no background service, no open ports. Models cannot be changed: Claude Desktop runs Anthropic's own. Restart the app and the servers appear under its tools menu.",
140
+ zh: "Claude Desktop 的配置文件只接受 stdio 形式的 MCP server,所以每个 AIsa server 都写成一个带你 key 的 <code>mcp-remote</code> 小桥接(外加免费的 <code>aisa-docs</code>)。桥接随 app 启停 —— 没有后台服务,不开端口。模型无法更改:Claude Desktop 跑的是 Anthropic 自家模型。重启 app 后就能在工具菜单里看到这些 server。",
141
+ },
142
+ "claude-ai": {
143
+ en: "Nothing is installed. claude.ai takes remote MCP servers as <b>Connectors</b>: on the last step you get one URL per server with a Copy button, and add each under <b>Settings → Connectors → Add custom connector</b>, then press Connect — claude.ai runs the AIsa sign-in itself. Models cannot be changed: claude.ai runs Anthropic's own.",
144
+ zh: "不安装任何东西。claude.ai 以 <b>Connectors</b> 的形式接受远程 MCP server:最后一步会给出每个 server 的 URL 和复制按钮,在 <b>Settings → Connectors → Add custom connector</b> 里逐个添加,然后点 Connect —— claude.ai 会自己跑 AIsa 的登录流程。模型无法更改:claude.ai 跑的是 Anthropic 自家模型。",
145
+ },
146
+ };
147
+ /**
148
+ * The answer to "will this break what I already have", per agent.
149
+ *
150
+ * Asked on this step rather than later because it is what decides whether
151
+ * someone continues at all.
152
+ */
153
+ export const AGENT_HAVE_NOTES = {
154
+ "claude-code": {
155
+ en: "Nothing of yours is replaced. Your <code>claude</code> keeps its login, models and settings; the next step lets you add AIsa <b>beside</b> it as a separate <b><code>claude-aisa</code></b> command, or switch — your call.",
156
+ zh: "你原有的东西一样都不会被替换。你的 <code>claude</code> 保留登录、模型和设置;下一步你可以选择把 AIsa 装在<b>旁边</b>(一个独立的 <b><code>claude-aisa</code></b> 命令),也可以直接切换 —— 你说了算。",
157
+ },
158
+ codex: {
159
+ en: "Nothing of yours is replaced. Your <code>codex</code> keeps its login and config; the next step lets you add AIsa <b>beside</b> it as an <code>aisa</code> profile and a <b><code>codex-aisa</code></b> command, or switch — your call.",
160
+ zh: "你原有的东西一样都不会被替换。你的 <code>codex</code> 保留登录和配置;下一步你可以选择把 AIsa 装在<b>旁边</b>(一个 <code>aisa</code> profile 加一个 <b><code>codex-aisa</code></b> 命令),也可以直接切换 —— 你说了算。",
161
+ },
162
+ opencode: {
163
+ en: "Nothing of yours is replaced. Your default model stays; the next step can add AIsa as an extra <code>aisa/…</code> provider you pick from the model list, or switch — your call.",
164
+ zh: "你原有的东西一样都不会被替换。默认模型保持不变;下一步可以把 AIsa 添加为额外的 <code>aisa/…</code> provider(在模型列表里选),也可以直接切换 —— 你说了算。",
165
+ },
166
+ "claude-desktop": {
167
+ en: "Nothing of yours is replaced. Anything already in <code>claude_desktop_config.json</code> stays; AIsa's entries are added beside it under <code>aisa-*</code> names and removing them is deleting those entries.",
168
+ zh: "你原有的东西一样都不会被替换。<code>claude_desktop_config.json</code> 里已有的内容保持原样;AIsa 的条目以 <code>aisa-*</code> 命名加在旁边,要移除就是删掉这些条目。",
169
+ },
170
+ "claude-ai": {
171
+ en: "Nothing of yours is touched — the connectors live in your claude.ai account, beside any you already have, and can be removed there in one click.",
172
+ zh: "完全不动你原有的东西 —— connector 存在你的 claude.ai 账户里,与你已有的并存,在那里一键就能移除。",
173
+ },
174
+ vscode: {
175
+ en: "Nothing of yours is replaced. Other MCP servers and model groups in those two files stay; only the <code>aisa-*</code> entries and the AIsa group are ours, and removing them is deleting those entries.",
176
+ zh: "你原有的东西一样都不会被替换。那两个文件里其他的 MCP server 和模型分组保持原样;只有 <code>aisa-*</code> 条目和 AIsa 那个分组是我们写的,要移除就是删掉它们。",
177
+ },
178
+ cursor: {
179
+ en: "Nothing of yours is replaced. Cursor adds each AIsa server beside your existing MCP entries under <code>aisa-*</code> names, and you approve every one inside Cursor first.",
180
+ zh: "你原有的东西一样都不会被替换。Cursor 会把每个 AIsa server 以 <code>aisa-*</code> 命名加在你已有的 MCP 条目旁边,而且每一个都要你在 Cursor 里先确认。",
181
+ },
182
+ };
183
+ // ── the rail: every step's name ─────────────────────────────────────────────
184
+ /**
185
+ * The six steps, and one glyph each.
186
+ *
187
+ * `icon` is for the terminal only. The page has a numbered rail down its left
188
+ * edge — position, colour and a tick already say which step you are on, and a
189
+ * sixth signal would be decoration. A terminal has none of that: the headers
190
+ * scroll past in one column of text, and after two of them "3/6" and "4/6"
191
+ * are the same shape. The glyph is what the eye finds when scrolling back.
192
+ *
193
+ * One per step and never repeated, so it identifies rather than decorates.
194
+ * Deliberately not the marks the journal already uses for outcomes
195
+ * (✅ ⚠️ ❌ ℹ️) — a step heading is not a result.
196
+ */
197
+ export const STEP_TITLES = [
198
+ { n: 1, icon: "👋", title: { en: "Welcome", zh: "开始" },
199
+ sub: { en: "What you are about to get", zh: "你将得到什么" } },
200
+ { n: 2, icon: "🤖", title: STEP_AGENT.title, sub: STEP_AGENT.sub },
201
+ { n: 3, icon: "🧠", title: { en: "Models", zh: "模型" },
202
+ sub: { en: "What it runs on", zh: "它用什么模型跑" } },
203
+ { n: 4, icon: "⚡", title: { en: "Capabilities", zh: "能力" },
204
+ sub: { en: "Live data for your agent", zh: "给 agent 的实时数据" } },
205
+ { n: 5, icon: "📦", title: { en: "Install", zh: "安装" },
206
+ sub: { en: "Sign in and wire it up", zh: "登录并接线" } },
207
+ { n: 6, icon: "🚀", title: { en: "Done", zh: "完成" },
208
+ sub: { en: "Try it now", zh: "现在就试试" } },
209
+ ];
210
+ // ── step 1: welcome ─────────────────────────────────────────────────────────
211
+ export const STEP_WELCOME = {
212
+ h1: {
213
+ en: "One connection. Every major model, live data and skills — inside the agent you already use.",
214
+ zh: "一次连接,把主流大模型、实时数据和技能,装进你已经在用的 agent 里。",
215
+ },
216
+ lede: {
217
+ en: "AIsa is the capability layer for AI agents: one account, one key, and your coding agent can reach the best models and the real world. Setting it up takes about a minute and nothing here is irreversible.",
218
+ zh: "AIsa 是 AI agent 的能力层:一个账户、一把 key,让你的编程 agent 既能用上最好的模型,也能接触真实世界的数据。整个过程大约一分钟,而且没有一步是不可撤销的。",
219
+ },
220
+ tiles: [
221
+ {
222
+ h3: { en: "All the well-known LLM models, one key, lower prices",
223
+ zh: "所有知名大模型,一把 key,更低的价格" },
224
+ p: { en: "Claude, GPT, Gemini, DeepSeek, Kimi, GLM, Qwen, Grok — all behind one endpoint, well below going direct. Switch between them with a single setting, no re-configuring.",
225
+ zh: "Claude、GPT、Gemini、DeepSeek、Kimi、GLM、Qwen、Grok —— 全部在同一个接入点后面,价格明显低于直接购买。改一个设置就能切换,不用重新配置。" },
226
+ },
227
+ {
228
+ h3: { en: "Rich, advanced tools your agent can act on",
229
+ zh: "丰富、先进,而且 agent 真能调用的工具" },
230
+ p: { en: "Market analysis and expansion research, finance data, social media signals, B2B prospecting — a wide network of officially licensed commercial APIs, integrated and refined by AIsa into MCP tools your agent can use out of the box.",
231
+ zh: "市场分析与拓展调研、金融数据、社交媒体信号、B2B 获客 —— 背后是一大批正式授权的商业 API,由 AIsa 集成打磨成开箱即用的 MCP 工具。" },
232
+ },
233
+ {
234
+ h3: { en: "Agent skills, ready to run", zh: "开箱即用的 agent 技能" },
235
+ p: { en: "A public catalogue of skills that teach your agent how to use each capability well — install with <code>aisa skills</code>, no prompt engineering needed.",
236
+ zh: "一个公开的技能目录,教你的 agent 怎么把每项能力用好 —— 用 <code>aisa skills</code> 安装,不需要自己调提示词。" },
237
+ },
238
+ {
239
+ h3: { en: "MCP the friendly way", zh: "让 MCP 变简单" },
240
+ p: { en: "One sign-in in your browser, zero keys to paste, every entry written through your agent's own official command. Remove it all just as easily.",
241
+ zh: "浏览器里登录一次,不用粘贴任何 key,每一条配置都通过 agent 自己的官方命令写入。要移除同样简单。" },
242
+ },
243
+ ],
244
+ };
245
+ // ── step 3: models ──────────────────────────────────────────────────────────
246
+ export const STEP_MODELS = {
247
+ h1: { en: "Every major model, one account — and switching is a one-liner",
248
+ zh: "所有主流模型,一个账户 —— 切换只要改一行" },
249
+ lede: {
250
+ en: "Whatever you decide below, this is what sits behind your AIsa key. No separate accounts, no separate billing, no re-wiring when a better model ships next month.",
251
+ zh: "无论你在下面怎么选,这些都在你的 AIsa key 后面。不用开多个账户、不用分开付费,下个月出了更好的模型也不用重新接线。",
252
+ },
253
+ callout: {
254
+ en: "<b>Switching is the whole point.</b> One key, one endpoint: change the model name and you are on a different lab's best model — Claude today, DeepSeek for the cheap batch job tonight, GPT-5.5 tomorrow. No new sign-ups, no new config, no juggling keys. ",
255
+ zh: "<b>能随时切换才是重点。</b>一把 key、一个接入点:改个模型名字,你就用上了另一家的旗舰模型 —— 今天 Claude,晚上跑批量任务换便宜的 DeepSeek,明天 GPT-5.5。不用重新注册、不用改配置、不用管理一堆 key。 ",
256
+ },
257
+ rerun: {
258
+ en: "Change the default any time — just run <code>aisa connect</code> again.",
259
+ zh: "默认模型随时可改 —— 再跑一次 <b><code>aisa connect</code></b> 就行。",
260
+ },
261
+ h2Prefix: { en: "How should ", zh: "要让 " },
262
+ h2Suffix: { en: " use them?", zh: " 怎么用这些模型?" },
263
+ yourAgent: { en: "your agent", zh: "你的 agent" },
264
+ recommended: { en: "recommended", zh: "推荐" },
265
+ freshSwitch: {
266
+ name: { en: "Run it on AIsa models", zh: "就用 AIsa 的模型跑" },
267
+ brief: { en: "A fresh install has no model backend yet. This writes the agent's own provider settings so it starts on {model} through AIsa — reversible.",
268
+ zh: "全新安装还没有模型后端。这会写入 agent 自己的 provider 设置,让它一上来就通过 AIsa 使用 {model} —— 可撤销。" },
269
+ },
270
+ notNow: {
271
+ name: { en: "Not now", zh: "暂时不用" },
272
+ briefFresh: { en: "Install it without a model. It will not answer a prompt until you configure a provider by hand.",
273
+ zh: "只安装,不配模型。在你手动配好 provider 之前,它无法回答任何问题。" },
274
+ briefDetected: { en: "Leave models exactly as they are; only the MCP tools are added.",
275
+ zh: "模型保持原样,只添加 MCP 工具。" },
276
+ },
277
+ backup: {
278
+ name: { en: "Add AIsa beside it", zh: "把 AIsa 装在旁边" },
279
+ },
280
+ switchIt: {
281
+ name: { en: "Switch it to AIsa", zh: "切换到 AIsa" },
282
+ brief: { en: "Points this agent's model traffic at AIsa ({model}). Writes the agent's own provider settings and nothing else — reversible.",
283
+ zh: "把这个 agent 的模型流量指向 AIsa({model})。只写 agent 自己的 provider 设置,别的都不动 —— 可撤销。" },
284
+ },
285
+ warn: {
286
+ head: { en: "⚠︎ Installing without a model backend", zh: "⚠︎ 安装后将没有模型后端" },
287
+ body: { en: "A fresh install <b>cannot answer a single prompt</b> until you configure a provider by hand. Turn on AIsa models and it leaves here ready to work.",
288
+ zh: "全新安装在你手动配好 provider 之前<b>一个问题都答不了</b>。打开 AIsa 模型,它离开这里时就能直接干活。" },
289
+ fix: { en: "Use AIsa models →", zh: "使用 AIsa 模型 →" },
290
+ },
291
+ };
292
+ /** Why the Models step has nothing to offer a file-configured client. */
293
+ export const FILE_MODEL_NOTE = {
294
+ "claude-desktop": {
295
+ en: "<b>Claude Desktop runs on Anthropic's own models</b>, and AIsa cannot stand in for them — so nothing changes here. You still get every model above through the coding agents on the same AIsa account; Claude Desktop gets the MCP tools.",
296
+ zh: "<b>Claude Desktop 跑的是 Anthropic 自家的模型</b>,AIsa 无法替代它们 —— 所以这一步不做任何改动。上面那些模型你仍然可以在同一个 AIsa 账户下,通过编程 agent 使用;Claude Desktop 这边拿到的是 MCP 工具。",
297
+ },
298
+ "claude-ai": {
299
+ en: "<b>claude.ai runs on Anthropic's own models</b>, and AIsa cannot stand in for them — so nothing changes here. You still get every model above through the coding agents (Claude Code, Codex, opencode) on the same AIsa account; claude.ai gets the MCP connectors.",
300
+ zh: "<b>claude.ai 跑的是 Anthropic 自家的模型</b>,AIsa 无法替代它们 —— 所以这一步不做任何改动。上面那些模型你仍然可以在同一个 AIsa 账户下,通过编程 agent(Claude Code、Codex、opencode)使用;claude.ai 这边拿到的是 MCP connector。",
301
+ },
302
+ cursor: {
303
+ en: "<b>Cursor picks its models inside the app</b>, so nothing changes here — only the MCP servers are added.",
304
+ zh: "<b>Cursor 在它自己的应用里选模型</b>,所以这一步不做改动 —— 只添加 MCP server。",
305
+ },
306
+ };
307
+ /** Backup-mode consent copy, per client — the same contract T1 shows. */
308
+ export const BACKUP_COPY = {
309
+ "claude-code": {
310
+ en: "Installs one small command, <b><code>claude-aisa</code></b>, next to your other tools. Your original <b><code>claude</code></b> keeps its login, models and settings <b>exactly as they are</b>.<br>You can use the newly added <b><code>claude-aisa</code></b> whenever you want AIsa's models at lower prices. Delete that one file to remove it.",
311
+ zh: "在你其他工具旁边装一个很小的命令 <b><code>claude-aisa</code></b>。你原来的 <b><code>claude</code></b> 的登录、模型和设置<b>原封不动</b>。<br>想用 AIsa 的低价模型时,就用新加的 <b><code>claude-aisa</code></b>。删掉那一个文件就等于移除。",
312
+ },
313
+ codex: {
314
+ en: "Adds an <b>aisa profile</b> inside Codex's own config and a <b><code>codex-aisa</code></b> command. Your default Codex is <b>untouched</b>.<br>You can use the newly added <b><code>codex-aisa</code></b> (or <code>codex --profile aisa</code>) whenever you want AIsa's models; it applies to that session only.",
315
+ zh: "在 Codex 自己的配置里加一个 <b>aisa profile</b>,再加一个 <b><code>codex-aisa</code></b> 命令。你默认的 Codex <b>完全不动</b>。<br>想用 AIsa 的模型时,就用新加的 <b><code>codex-aisa</code></b>(或 <code>codex --profile aisa</code>);它只对那一次会话生效。",
316
+ },
317
+ vscode: {
318
+ en: "Adds a <b>Custom Endpoint</b> group named <b>AIsa</b> to VS Code's chat models — Claude, GPT, DeepSeek, Kimi, GLM, Qwen — beside Copilot's own. Your Copilot setup is <b>untouched</b>.<br>A small <b>AIsa extension</b> is installed so VS Code stores your key itself — nothing to paste. Then pick any AIsa model from the chat model picker; inline completions stay on Copilot.",
319
+ zh: "在 VS Code 的聊天模型里加一个名为 <b>AIsa</b> 的 <b>Custom Endpoint</b> 分组 —— Claude、GPT、DeepSeek、Kimi、GLM、Qwen —— 与 Copilot 自己的并存。你的 Copilot 配置<b>完全不动</b>。<br>会装一个很小的 <b>AIsa 扩展</b>,由 VS Code 自己保管你的 key —— 不用粘贴任何东西。之后在聊天模型选择器里挑任意 AIsa 模型即可;行内补全仍然走 Copilot。",
320
+ },
321
+ opencode: {
322
+ en: "Adds AIsa as an <b>extra provider</b> in opencode's config. Your default model is <b>untouched</b>.<br>You can pick the newly added <code>aisa/…</code> models from opencode's model list whenever you want them.",
323
+ zh: "在 opencode 的配置里把 AIsa 添加为<b>额外的 provider</b>。你的默认模型<b>完全不动</b>。<br>想用的时候,在 opencode 的模型列表里选新加的 <code>aisa/…</code> 就行。",
324
+ },
325
+ };
326
+ // ── step 4: capabilities ────────────────────────────────────────────────────
327
+ export const STEP_CAPS = {
328
+ h1: { en: "What should your agent be able to reach?", zh: "你的 agent 需要够得着什么?" },
329
+ ledeTail: {
330
+ en: "Open an area to pick the servers inside it. Everything here is live production data with licensed sources; you can add more later with one more <code>aisa connect</code>.",
331
+ zh: "点开一个领域来挑里面的 server。这里全都是有授权来源的生产环境实时数据;之后再跑一次 <code>aisa connect</code> 就能加更多。",
332
+ },
333
+ pickArea: { en: "Pick an area above", zh: "先在上面选一个领域" },
334
+ selectAll: { en: "Select all in this area", zh: "全选这个领域" },
335
+ /**
336
+ * The counts sentence as one string, because punctuation is not shared
337
+ * between languages: joining "N", "areas", ", " in code produced
338
+ * "2 个领域, 2 个 server, 48 个工具." — Chinese text with ASCII commas and a
339
+ * full stop. Whatever varies goes in as a placeholder instead.
340
+ */
341
+ counts: {
342
+ en: "{areas} areas, {servers} servers, {tools} tools.",
343
+ zh: "{areas} 个领域、{servers} 个 server、{tools} 个工具。",
344
+ },
345
+ /** Category tile meta: "3 servers · 47 tools". */
346
+ tileMeta: {
347
+ en: "{n} {noun} · {tools} tools",
348
+ zh: "{n} 个 {noun} · {tools} 个工具",
349
+ },
350
+ /**
351
+ * A placeholder both renderers can fill. The page used to carry
352
+ * <b id="mModel"></b> here and fill it with script — which left the
353
+ * terminal printing "AIsa()" with nothing inside, because stripping tags is
354
+ * all it can do with markup meant for the other surface.
355
+ */
356
+ serversWord: { en: "servers", zh: "server" },
357
+ serverWord: { en: "server", zh: "server" },
358
+ toolsWord: { en: "tools", zh: "个工具" },
359
+ };
360
+ /** Shown for a file-configured client that has no note of its own. */
361
+ export const FILE_MODEL_FALLBACK = {
362
+ en: "This client picks its own models inside the app, so nothing is changed here — only the MCP servers are added.",
363
+ zh: "这个客户端在它自己的应用里选模型,所以这一步不做改动 —— 只添加 MCP server。",
364
+ };
365
+ export const CATEGORY_BLURB = {
366
+ "Search & Research": {
367
+ en: "Ranked web results with page text already extracted, plus YouTube.",
368
+ zh: "带排序的网页搜索结果,正文已经抽好,另有 YouTube。",
369
+ },
370
+ Finance: {
371
+ en: "US equities, crypto, prediction markets and what X is saying about a ticker.",
372
+ zh: "美股、加密货币、预测市场,以及 X 上关于某只票的讨论。",
373
+ },
374
+ Social: {
375
+ en: "Public X/Twitter, Reddit, Instagram and Pinterest — profiles, posts, engagement.",
376
+ zh: "公开的 X/Twitter、Reddit、Instagram 和 Pinterest —— 资料、帖子、互动数据。",
377
+ },
378
+ Sales: {
379
+ en: "Apollo B2B data — enrich people and companies, find prospects.",
380
+ zh: "Apollo 的 B2B 数据 —— 补全人和公司信息、找潜在客户。",
381
+ },
382
+ "SEO & Search Data": {
383
+ en: "Keywords, rankings, backlinks and on-page checks — and how a brand shows up in AI answers.",
384
+ zh: "关键词、排名、外链、页面检查 —— 还有一个品牌在 AI 回答里的露出情况。",
385
+ },
386
+ Communication: {
387
+ en: "An inbox an agent can actually use — its own address, sending and receiving.",
388
+ zh: "agent 能真正用起来的邮箱 —— 自己的地址,能收能发。",
389
+ },
390
+ Creators: {
391
+ en: "Find creators by audience, topic and reach, across platforms.",
392
+ zh: "按受众、话题和影响力找创作者,跨平台。",
393
+ },
394
+ Search: {
395
+ en: "YouTube search on its own.",
396
+ zh: "单独的 YouTube 搜索。",
397
+ },
398
+ "Market Intelligence": {
399
+ en: "Similarweb data — a site's traffic, engagement and where its visitors come from.",
400
+ zh: "Similarweb 数据 —— 网站流量、互动情况,以及访客从哪来。",
401
+ },
402
+ };
403
+ // ── step 5: install ─────────────────────────────────────────────────────────
404
+ export const STEP_INSTALL = {
405
+ h1: { en: "Ready to connect", zh: "准备接线" },
406
+ lede: {
407
+ en: "Here is everything that happens, in order. It starts on its own; each step reports as it finishes, and your results open on the last step.",
408
+ zh: "下面是将要发生的全部事情,按顺序列出。它会自动开始;每一步完成后都会汇报,结果会在最后一步打开。",
409
+ },
410
+ authKeyed: {
411
+ en: "Your configured AIsa API key is written into each entry — <b>no sign-in needed</b>.",
412
+ zh: "你已配置的 AIsa API key 会写进每一条配置 —— <b>无需登录</b>。",
413
+ },
414
+ authFresh: {
415
+ en: "<b>One sign-in, nothing to paste.</b> Your browser opens the AIsa approval once; it issues a long-lived key for this machine, and every server and model is configured with it.",
416
+ zh: "<b>登录一次,不用粘贴任何东西。</b>浏览器会打开一次 AIsa 授权页;它会为这台机器签发一把长期有效的 key,所有 server 和模型都用它来配置。",
417
+ },
418
+ };
419
+ // ── step 6: done ────────────────────────────────────────────────────────────
420
+ export const STEP_DONE = {
421
+ eyebrow: { en: "Step 6 of 6", zh: "第 6 步 / 共 6 步" },
422
+ h1: { en: "Almost there…", zh: "就快好了…" },
423
+ lede: { en: "The results appear here as soon as the run finishes.", zh: "运行一结束,结果就会显示在这里。" },
424
+ };
425
+ // ── strings the page script needs at runtime ────────────────────────────────
426
+ // Resolved to the current language and injected as a single object, because
427
+ // the browser has no access to this file. Same source, one hop later.
428
+ export const RUNTIME_COPY = {
429
+ nothingSelected: {
430
+ en: "Nothing selected yet — pick at least one server.",
431
+ zh: "还没有选择 —— 至少选一个 server。",
432
+ },
433
+ planBackup: {
434
+ "claude-code": { en: "Install the claude-aisa command", zh: "安装 claude-aisa 命令" },
435
+ codex: { en: "Add the aisa profile and codex-aisa", zh: "添加 aisa profile 和 codex-aisa" },
436
+ vscode: { en: "Add AIsa models to VS Code chat", zh: "把 AIsa 模型加进 VS Code 聊天" },
437
+ other: { en: "Add AIsa as a backup provider", zh: "把 AIsa 添加为备用 provider" },
438
+ },
439
+ planBackupNote: {
440
+ en: "your current setup stays untouched",
441
+ zh: "你现有的配置原封不动",
442
+ },
443
+ ledeFailed: {
444
+ en: "Some steps did not complete — the details are in the list; your results page explains how to retry.",
445
+ zh: "有些步骤没有完成 —— 明细在上面的列表里,结果页会说明怎么重试。",
446
+ },
447
+ ledeAllRan: {
448
+ en: "Everything ran. Your results, balance and try-it-now prompts are on the last step.",
449
+ zh: "全部执行完毕。结果、余额和可以直接试的提示语都在最后一步。",
450
+ },
451
+ backupIntact: { en: "Your usual setup is untouched.", zh: "你平时用的配置原封不动。" },
452
+ backupOpencode: {
453
+ en: "Pick <code>aisa/…</code> from opencode's model list whenever you want AIsa.",
454
+ zh: "想用 AIsa 的时候,在 opencode 的模型列表里选 <code>aisa/…</code> 即可。",
455
+ },
456
+ // Bold, like every other command a reader is meant to type later. The two
457
+ // sit in the same paragraph on the results page and were marked up
458
+ // differently — one command looking more like a command than the other,
459
+ // for no reason a reader could infer.
460
+ backupBin: {
461
+ // Plain <code>, like every other command in this section's prose. The
462
+ // extra weight was added to make two neighbours match; matching the whole
463
+ // page is the better answer — one page with one way of writing a command
464
+ // beats one paragraph with its own.
465
+ en: "Run <code>{bin}</code> whenever you want AIsa's models; delete that one file to remove it.",
466
+ zh: "想用 AIsa 的模型时就跑 <code>{bin}</code>;删掉那一个文件就等于移除。",
467
+ },
468
+ noKeyStored: {
469
+ en: "No key stored here — run <code>aisa login</code>, then copy it from <code>~/.aisa/key</code>.",
470
+ zh: "本机没有存 key —— 先跑 <code>aisa login</code>,再从 <code>~/.aisa/key</code> 复制。",
471
+ },
472
+ cannotStartApp: {
473
+ en: "Could not start {name} — open it from your Applications folder.",
474
+ zh: "无法启动 {name} —— 请从应用程序文件夹里手动打开。",
475
+ },
476
+ // Louder than the prose around it, because when this appears it is the only
477
+ // instruction left: the button that would have done it is gone, and the
478
+ // command is the whole of what the reader has to act on.
479
+ cannotOpenTerminal: {
480
+ en: "Could not open a terminal — run <b><code>{bin}</code></b> in any terminal.",
481
+ zh: "无法打开终端 —— 在任意终端里跑 <b><code>{bin}</code></b>。",
482
+ },
483
+ };
484
+ /** Flatten RUNTIME_COPY to plain strings for the page script. */
485
+ export function runtimeCopy(lang) {
486
+ const walk = (v) => {
487
+ if (v && typeof v === "object" && "en" in v && "zh" in v)
488
+ return t(v, lang);
489
+ if (v && typeof v === "object") {
490
+ return Object.fromEntries(Object.entries(v).map(([k, x]) => [k, walk(x)]));
491
+ }
492
+ return v;
493
+ };
494
+ return walk(RUNTIME_COPY);
495
+ }
496
+ /** What each language calls itself — never translated. */
497
+ export const LANG_LABEL = { en: "EN", zh: "中文" };
498
+ export const NAV = {
499
+ back: { en: "← Back", zh: "← 返回" },
500
+ };
501
+ /** The forward button, worded for the step it leads out of. */
502
+ export const NEXT_WORD = {
503
+ 1: { en: "Let's get started ", zh: "开始吧 " },
504
+ 2: { en: "Continue to models ", zh: "下一步:模型 " },
505
+ 3: { en: "Continue to capabilities ", zh: "下一步:能力 " },
506
+ 4: { en: "Review and connect ", zh: "确认并连接 " },
507
+ 5: { en: "", zh: "" },
508
+ 6: { en: "", zh: "" },
509
+ };
510
+ /** The last gate before anything is written. */
511
+ export const CONFIRM = {
512
+ heading: { en: "Ready to connect", zh: "准备就绪" },
513
+ agent: { en: "Agent", zh: "Agent" },
514
+ models: { en: "Models", zh: "模型" },
515
+ capabilities: { en: "Capabilities", zh: "能力" },
516
+ /**
517
+ * A word, not a keystroke. Everything up to here was reversible browsing;
518
+ * this is the point where files on the machine change, and a prompt you can
519
+ * clear with a stray return is the wrong shape for it.
520
+ */
521
+ /**
522
+ * A picker rather than a typed word.
523
+ *
524
+ * Typing was the first shape, and it fought the arrow-key steps beside it:
525
+ * keys left in the buffer by the picker arrived at readline as empty lines,
526
+ * which re-asked, which read again — the prompt repeated down the screen
527
+ * and tangled with the one after it. One input method for the whole flow.
528
+ *
529
+ * The deliberateness is kept by where the cursor starts: on "go back", so
530
+ * a stray return changes nothing.
531
+ */
532
+ apply: { en: "Apply this — write it to my machine", zh: "开始配置 —— 写入这台机器" },
533
+ goBack: { en: "Go back and change something", zh: "返回修改" },
534
+ ask: { en: "Confirm", zh: "确认" },
535
+ cancelled: { en: "Nothing was written.", zh: "什么都没有写入。" },
536
+ /** n means change something, not give up — the run is still yours. */
537
+ backToEdit: {
538
+ en: "Back to the start — your choices are kept.",
539
+ zh: "回到开头重新选 —— 刚才选的都还在。",
540
+ },
541
+ };
542
+ /** The last question: run something now, or hand the terminal back. */
543
+ export const LAUNCH = {
544
+ question: { en: "What would you like to do now?", zh: "现在想做什么?" },
545
+ exit: { en: "Exit", zh: "退出" },
546
+ /** Says what it does: the page goes with it. Claiming the page survives
547
+ * was true only while the process lingered, which is what made Exit feel
548
+ * broken — it did not exit. */
549
+ exitNote: {
550
+ en: "back to your prompt — closes the results page",
551
+ zh: "回到命令提示符 —— 同时关闭结果页",
552
+ },
553
+ };
554
+ /** Shown on a page a newer run has taken over. */
555
+ /**
556
+ * How a run introduces the two surfaces it is about to use.
557
+ *
558
+ * The console case used to lead with what was missing — "no browser on this
559
+ * machine" — which reads like a fault report for something that is not a
560
+ * fault at all. A server without a desktop is a perfectly ordinary place to
561
+ * run this, and the terminal is a first-class way through the flow, not a
562
+ * consolation. So it is introduced as a mode, by name, and the sentence
563
+ * after it gets on with the work.
564
+ */
565
+ export const SURFACE = {
566
+ pageTitle: { en: "Open this page to choose", zh: "打开这个页面来选择" },
567
+ opening: {
568
+ en: "opening your browser… (or answer here — both stay in step)",
569
+ zh: "正在打开浏览器…(也可以直接在这里回答 —— 两边始终同步)",
570
+ },
571
+ urlOnly: {
572
+ en: "open the URL above, or answer here — both stay in step",
573
+ zh: "打开上面的地址,或者直接在这里回答 —— 两边始终同步",
574
+ },
575
+ consoleTitle: { en: "Console mode", zh: "控制台模式" },
576
+ consoleBody: {
577
+ en: "The browser view is not open here, so we will do this together in the terminal — the same questions, and it is very easy. Off we go.",
578
+ zh: "这里没有开浏览器界面,我们就用命令行一起把它选完 —— 问题都一样,非常简单。这就开始。",
579
+ },
580
+ };
581
+ /**
582
+ * Signing in from a machine whose browser is somewhere else.
583
+ *
584
+ * Clerk offers authorization_code and refresh_token and nothing else — no
585
+ * device grant — so there is no "type this short code" flow to reach for.
586
+ * What there is works, but only if the person is told plainly that the last
587
+ * page is *supposed* to fail: they are being asked to copy an address out of
588
+ * a browser that is showing an error, which nobody does unless told to.
589
+ */
590
+ export const SIGNIN = {
591
+ start: { en: "Signing in to AIsa…", zh: "正在登录 AIsa…" },
592
+ willOpen: { en: "Your browser will open — approve the sign-in there.", zh: "浏览器就要打开了 —— 在那里确认登录。" },
593
+ ifNot: { en: "If it does not open, visit:", zh: "如果没有自动打开,访问:" },
594
+ here: {
595
+ en: "This machine has no browser, so sign in on the one in front of you.",
596
+ zh: "这台机器上没有浏览器,请在你面前那台电脑上登录。",
597
+ },
598
+ step1: { en: "1. Open this address there:", zh: "1. 在那台电脑上打开这个地址:" },
599
+ step2: {
600
+ en: "2. Sign in. A page then shows you a short block of text — press Copy.",
601
+ zh: "2. 登录。之后会出现一个页面,上面有一小段文字 —— 点「复制」。",
602
+ },
603
+ step3: { en: "3. Paste it below.", zh: "3. 粘贴到下面。" },
604
+ prompt: { en: "Paste it here: ", zh: "粘贴到这里: " },
605
+ badPaste: {
606
+ en: "that does not look like the redirect — it should contain code=",
607
+ zh: "这看起来不是那个跳转地址 —— 里面应该有 code=",
608
+ },
609
+ };
610
+ /** What the browser is told when a sign-in lands back on this machine. */
611
+ export const SIGNIN_PAGE = {
612
+ ok: {
613
+ title: { en: "Signed in", zh: "登录成功" },
614
+ body: {
615
+ en: "Your key has been created on the machine you started from. You can close this tab.",
616
+ zh: "key 已经在你发起登录的那台机器上生成好了。可以关掉这个标签页了。",
617
+ },
618
+ },
619
+ failed: {
620
+ title: { en: "Sign-in was not completed", zh: "登录没有完成" },
621
+ body: {
622
+ en: "Nothing was changed. Return to your terminal and try again.",
623
+ zh: "什么都没有改动。回到终端重新试一次。",
624
+ },
625
+ },
626
+ };
627
+ export const SUPERSEDED = {
628
+ title: { en: "A newer setup is running", zh: "已有新的配置在运行" },
629
+ /** The other way a page ends: nobody replaced it, its time ran out. */
630
+ expiringTitle: { en: "This page is about to close", zh: "本页面即将关闭" },
631
+ expiringBody: {
632
+ en: "The run that opened it has finished. Nothing here needs you any more; the page closes in",
633
+ zh: "打开它的那次运行已经结束,这里没有需要你做的事了。本窗口将在",
634
+ },
635
+ body: {
636
+ en: "You started <b>aisa connect</b> again, and this page describes the run before it. It closes in",
637
+ zh: "你又运行了一次 <b>aisa connect</b>,这个页面显示的是上一次的结果。本窗口将在",
638
+ },
639
+ seconds: { en: "seconds", zh: "秒后关闭" },
640
+ closed: {
641
+ en: "This page has closed. The newer run has its own.",
642
+ zh: "本页面已关闭。新的运行有它自己的页面。",
643
+ },
644
+ /**
645
+ * The two lines every ending carries, whatever ended.
646
+ *
647
+ * A surface that is closing has the reader's attention and nothing left to
648
+ * spend it on — which is the one moment these are useful rather than in the
649
+ * way. Both are addresses, not buttons: a button is clicked once and
650
+ * forgotten, while `console.aisa.one` and `aisa connect` are things someone
651
+ * can still type next week. That is the whole point of putting them here.
652
+ */
653
+ console: {
654
+ en: "Usage, spending and top-ups live at",
655
+ zh: "用量、消费和充值都在",
656
+ },
657
+ consoleDomain: "console.aisa.one",
658
+ againBefore: { en: "Run", zh: "随时再跑" },
659
+ againCmd: "aisa connect",
660
+ againAfter: {
661
+ en: "any time — add servers, switch models, or connect another agent.",
662
+ zh: "—— 加 server、换模型、接另一个 agent。",
663
+ },
664
+ };
665
+ /**
666
+ * The second tab, once the sign-in is done.
667
+ *
668
+ * A sign-in started from inside `connect` opens a new tab, and that tab has
669
+ * to end somewhere. Ending it on "you can close this tab" is right for
670
+ * standalone `aisa login`, where closing it is the whole of what is left —
671
+ * but wrong here, where a setup is still running one tab over and the reader
672
+ * has just been sent away from it. This page goes back instead.
673
+ */
674
+ export const RETURN_PAGE = {
675
+ ok: {
676
+ title: { en: "Signed in", zh: "登录成功" },
677
+ body: {
678
+ en: "Taking you back to your setup — it carries on from where it was.",
679
+ zh: "正在带你回到刚才的配置流程 —— 会从原来的位置继续。",
680
+ },
681
+ },
682
+ failed: {
683
+ title: { en: "Sign-in was not completed", zh: "登录没有完成" },
684
+ body: {
685
+ en: "Nothing was changed. Go back to your setup and try the sign-in again.",
686
+ zh: "什么都没有改动。回到配置流程再登录一次。",
687
+ },
688
+ },
689
+ stale: {
690
+ title: { en: "Nothing is waiting for this", zh: "这次回调已经没人在等了" },
691
+ body: {
692
+ en: "This sign-in has already been answered. Back to your setup.",
693
+ zh: "这次登录已经处理过了。回到你的配置流程。",
694
+ },
695
+ },
696
+ link: { en: "Back to your setup", zh: "回到配置流程" },
697
+ };