opencode-go-usage-tui 1.0.1 → 1.2.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.
- package/README.md +24 -1
- package/README_EN.md +117 -0
- package/build.tui.mjs +5 -1
- package/dist/tui.js +406 -114
- package/package.json +3 -2
- package/src/i18n.ts +108 -0
- package/src/index.tsx +210 -74
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "opencode-go-usage-tui",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"description": "OpenCode TUI plugin displaying OpenCode Go usage in the sidebar",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -15,7 +15,8 @@
|
|
|
15
15
|
"dist",
|
|
16
16
|
"src",
|
|
17
17
|
"build.tui.mjs",
|
|
18
|
-
"README.md"
|
|
18
|
+
"README.md",
|
|
19
|
+
"README_EN.md"
|
|
19
20
|
],
|
|
20
21
|
"scripts": {
|
|
21
22
|
"build": "bun run build.tui.mjs",
|
package/src/i18n.ts
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
export type LangCode = "zh" | "en"
|
|
2
|
+
|
|
3
|
+
const ZH_T = {
|
|
4
|
+
panelTitle: "Go 用量",
|
|
5
|
+
rowRolling: "5小时用量",
|
|
6
|
+
rowWeekly: "每周用量",
|
|
7
|
+
rowMonthly: "每月用量",
|
|
8
|
+
weeklyShort: "周",
|
|
9
|
+
updatedAt: "最近更新",
|
|
10
|
+
notConfigured: "未配置账号",
|
|
11
|
+
notConfiguredHint: "输入 /go-config 设置",
|
|
12
|
+
loading: "加载中...",
|
|
13
|
+
queryFailed: "查询失败",
|
|
14
|
+
cookieExpired: "cookie 已过期",
|
|
15
|
+
reconfigureHint: "输入 /go-config 重新配置",
|
|
16
|
+
resetDone: "已重置",
|
|
17
|
+
dayHour: "{d}天{h}小时",
|
|
18
|
+
hourMin: "{h}小时{m}分钟",
|
|
19
|
+
minute: "{m}分钟",
|
|
20
|
+
cmdTitle: "Go Usage: Configure",
|
|
21
|
+
cmdDesc: "设置 OpenCode Go workspace ID 和 auth cookie",
|
|
22
|
+
wsPromptTitle: "输入 workspace_id",
|
|
23
|
+
wsPromptDesc: "在 opencode.ai 控制台 URL 中获取(wrk_ 开头)",
|
|
24
|
+
wsPlaceholder: "wrk_xxxxxxxxxxxx",
|
|
25
|
+
cookiePromptTitle: "输入 auth cookie",
|
|
26
|
+
cookiePromptDesc: "登录 opencode.ai 后,浏览器 F12 → Application → Cookies → opencode.ai → 复制 auth 的值",
|
|
27
|
+
cookiePlaceholder: "Fe26.2*...",
|
|
28
|
+
configSaved: "Go 用量配置已保存",
|
|
29
|
+
langTitle: "显示语言",
|
|
30
|
+
langCmdTitle: "Go Usage: Language",
|
|
31
|
+
langCmdDesc: "切换显示语言 | Switch display language",
|
|
32
|
+
langSwitchedZh: "已切换为中文",
|
|
33
|
+
langSwitchedEn: "Switched to English",
|
|
34
|
+
onboardLangTitle: "Welcome to Go Usage / 欢迎使用 Go 用量",
|
|
35
|
+
onboardLangDesc: "Choose display language / 选择显示语言",
|
|
36
|
+
onboardConfigTitle: "Welcome to Go Usage / 欢迎使用 Go 用量",
|
|
37
|
+
onboardConfigDesc: "First-time setup / 首次使用需配置账号",
|
|
38
|
+
setupCanceled: "已取消配置,输入 /go-config 可重新设置",
|
|
39
|
+
} as const
|
|
40
|
+
|
|
41
|
+
export type Translation = { [K in keyof typeof ZH_T]: string }
|
|
42
|
+
|
|
43
|
+
const EN_T: Translation = {
|
|
44
|
+
panelTitle: "Go Usage",
|
|
45
|
+
rowRolling: "5h usage",
|
|
46
|
+
rowWeekly: "Weekly",
|
|
47
|
+
rowMonthly: "Monthly",
|
|
48
|
+
weeklyShort: "Wk",
|
|
49
|
+
updatedAt: "Updated",
|
|
50
|
+
notConfigured: "Not configured",
|
|
51
|
+
notConfiguredHint: "Run /go-config to set up",
|
|
52
|
+
loading: "Loading...",
|
|
53
|
+
queryFailed: "Query failed",
|
|
54
|
+
cookieExpired: "Cookie expired",
|
|
55
|
+
reconfigureHint: "Run /go-config to reconfigure",
|
|
56
|
+
resetDone: "Reset",
|
|
57
|
+
dayHour: "{d}d {h}h",
|
|
58
|
+
hourMin: "{h}h {m}m",
|
|
59
|
+
minute: "{m}m",
|
|
60
|
+
cmdTitle: "Go Usage: Configure",
|
|
61
|
+
cmdDesc: "Set OpenCode Go workspace ID and auth cookie",
|
|
62
|
+
wsPromptTitle: "Enter workspace_id",
|
|
63
|
+
wsPromptDesc: "Get it from the opencode.ai console URL (starts with wrk_)",
|
|
64
|
+
wsPlaceholder: "wrk_xxxxxxxxxxxx",
|
|
65
|
+
cookiePromptTitle: "Enter auth cookie",
|
|
66
|
+
cookiePromptDesc: "After logging into opencode.ai, press F12 → Application → Cookies → opencode.ai → copy the auth value",
|
|
67
|
+
cookiePlaceholder: "Fe26.2*...",
|
|
68
|
+
configSaved: "Go usage config saved",
|
|
69
|
+
langTitle: "Display language",
|
|
70
|
+
langCmdTitle: "Go Usage: Language",
|
|
71
|
+
langCmdDesc: "切换显示语言 | Switch display language",
|
|
72
|
+
langSwitchedZh: "已切换为中文",
|
|
73
|
+
langSwitchedEn: "Switched to English",
|
|
74
|
+
onboardLangTitle: "Welcome to Go Usage / 欢迎使用 Go 用量",
|
|
75
|
+
onboardLangDesc: "Choose display language / 选择显示语言",
|
|
76
|
+
onboardConfigTitle: "Welcome to Go Usage / 欢迎使用 Go 用量",
|
|
77
|
+
onboardConfigDesc: "First-time setup / 首次使用需配置账号",
|
|
78
|
+
setupCanceled: "Setup canceled, run /go-config to reconfigure",
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export const LANGS: Record<LangCode, Translation> = { zh: ZH_T, en: EN_T }
|
|
82
|
+
|
|
83
|
+
export const LANG_META: { code: LangCode; label: string }[] = [
|
|
84
|
+
{ code: "zh", label: "中文" },
|
|
85
|
+
{ code: "en", label: "English" },
|
|
86
|
+
]
|
|
87
|
+
|
|
88
|
+
export function applyParams(tpl: string, params?: Record<string, string | number>): string {
|
|
89
|
+
if (!params) return tpl
|
|
90
|
+
return tpl.replace(/\{(\w+)\}/g, (m, k: string) =>
|
|
91
|
+
k in params ? String(params[k]) : m,
|
|
92
|
+
)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function createT(getCode: () => LangCode) {
|
|
96
|
+
return (key: keyof Translation, params?: Record<string, string | number>): string =>
|
|
97
|
+
applyParams(LANGS[getCode()][key], params)
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export function detectLang(): LangCode {
|
|
101
|
+
try {
|
|
102
|
+
const loc = Intl.DateTimeFormat().resolvedOptions().locale.toLowerCase()
|
|
103
|
+
if (loc.startsWith("zh")) return "zh"
|
|
104
|
+
return "en"
|
|
105
|
+
} catch {
|
|
106
|
+
return "en"
|
|
107
|
+
}
|
|
108
|
+
}
|
package/src/index.tsx
CHANGED
|
@@ -10,45 +10,119 @@ import type {
|
|
|
10
10
|
} from "@opencode-ai/plugin/tui"
|
|
11
11
|
import { createSignal, createEffect, onMount, onCleanup, Show } from "solid-js"
|
|
12
12
|
import type { JSX } from "@opentui/solid"
|
|
13
|
-
import { readFileSync, writeFileSync, mkdirSync } from "node:fs"
|
|
13
|
+
import { readFileSync, writeFileSync, mkdirSync, renameSync, existsSync } from "node:fs"
|
|
14
14
|
import { join, dirname } from "node:path"
|
|
15
|
+
import { createCipheriv, createDecipheriv, randomBytes } from "node:crypto"
|
|
16
|
+
import { createT, detectLang, LANG_META } from "./i18n"
|
|
17
|
+
import type { LangCode } from "./i18n"
|
|
15
18
|
|
|
16
19
|
declare const process: { env: Record<string, string | undefined> } | undefined
|
|
17
20
|
|
|
21
|
+
const VERSION: string = __PLUGIN_VERSION__
|
|
18
22
|
const CHECK_INTERVAL = Number(process?.env?.OPENCODE_GO_CHECK_INTERVAL ?? 60000)
|
|
19
23
|
const WARN_THRESHOLD = Number(process?.env?.OPENCODE_GO_WARN_THRESHOLD ?? 0.8)
|
|
20
24
|
const CONFIG_DIR = process?.env?.OPENCODE_CONFIG_DIR
|
|
21
25
|
|| (process?.env?.USERPROFILE ? `${process.env.USERPROFILE}\\.config\\opencode` : "")
|
|
22
26
|
|| process?.env?.HOME + "/.config/opencode"
|
|
23
27
|
const CONFIG_FILE = join(CONFIG_DIR, "go-usage-config.json")
|
|
28
|
+
const COOKIE_ENC_FILE = join(CONFIG_DIR, "go-auth-cookie.enc")
|
|
29
|
+
const KEY_FILE = join(CONFIG_DIR, ".encryption-key")
|
|
30
|
+
const COOKIE_PLAIN_FILE = `${CONFIG_DIR}\\go-auth-cookie.txt`
|
|
31
|
+
const KV_PREFIX = "go-usage"
|
|
32
|
+
|
|
33
|
+
// ── AES-256-GCM 加密存储 ────────────────────────────────
|
|
34
|
+
// cookie 以密文存 go-auth-cookie.enc,密钥存 .encryption-key。
|
|
35
|
+
// 注意:Windows 无 Unix 权限位,mode 0o600 仅对 macOS/Linux 生效;
|
|
36
|
+
// Windows 上密钥与密文同目录,防护重点是"被动泄露"(备份/云同步/日志),
|
|
37
|
+
// 无法抵御能读取该目录的本地恶意程序。
|
|
38
|
+
function loadEncryptionKey(): Buffer {
|
|
39
|
+
try {
|
|
40
|
+
const hex = readFileSync(KEY_FILE, "utf8").trim()
|
|
41
|
+
if (hex) return Buffer.from(hex, "hex")
|
|
42
|
+
} catch { /* 无密钥则新建 */ }
|
|
43
|
+
const key = randomBytes(32)
|
|
44
|
+
mkdirSync(dirname(KEY_FILE), { recursive: true })
|
|
45
|
+
writeFileSync(KEY_FILE, key.toString("hex"), { encoding: "utf8", mode: 0o600 })
|
|
46
|
+
return key
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function encryptText(text: string): string {
|
|
50
|
+
const key = loadEncryptionKey()
|
|
51
|
+
const iv = randomBytes(16)
|
|
52
|
+
const cipher = createCipheriv("aes-256-gcm", key, iv)
|
|
53
|
+
const data = cipher.update(text, "utf8", "hex") + cipher.final("hex")
|
|
54
|
+
return JSON.stringify({ v: 1, iv: iv.toString("hex"), data, tag: cipher.getAuthTag().toString("hex") })
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function decryptText(encJson: string): string {
|
|
58
|
+
const { iv, data, tag } = JSON.parse(encJson)
|
|
59
|
+
const key = loadEncryptionKey()
|
|
60
|
+
const decipher = createDecipheriv("aes-256-gcm", key, Buffer.from(iv, "hex"))
|
|
61
|
+
decipher.setAuthTag(Buffer.from(tag, "hex"))
|
|
62
|
+
return decipher.update(data, "hex", "utf8") + decipher.final("utf8")
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function readJsonFile<T>(file: string): T | null {
|
|
66
|
+
try { return JSON.parse(readFileSync(file, "utf8")) as T } catch { return null }
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
interface ConfigFile { workspace_id?: string; cookie?: string; auth_cookie?: string; cookie_file?: string }
|
|
24
70
|
|
|
25
71
|
function loadConfig(): { workspaceId: string; authCookie: string } {
|
|
26
72
|
const env = process?.env ?? {}
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
73
|
+
const fileCfg = readJsonFile<ConfigFile>(CONFIG_FILE) ?? {}
|
|
74
|
+
|
|
75
|
+
// 1. 环境变量优先
|
|
76
|
+
if (env.OPENCODE_GO_AUTH_COOKIE) {
|
|
77
|
+
return { workspaceId: env.OPENCODE_GO_WORKSPACE_ID || fileCfg.workspace_id || "", authCookie: env.OPENCODE_GO_AUTH_COOKIE }
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// 2. 加密存储
|
|
33
81
|
try {
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
82
|
+
if (existsSync(COOKIE_ENC_FILE)) {
|
|
83
|
+
const cookie = decryptText(readFileSync(COOKIE_ENC_FILE, "utf8"))
|
|
84
|
+
if (cookie) {
|
|
85
|
+
return { workspaceId: env.OPENCODE_GO_WORKSPACE_ID || fileCfg.workspace_id || "", authCookie: cookie }
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
} catch { /* 密文损坏时回退到明文迁移 */ }
|
|
89
|
+
|
|
90
|
+
// 3. 兼容旧明文(config.json 内嵌 cookie / auth_cookie / 独立 txt 文件)——自动迁移
|
|
91
|
+
const legacyCookie = fileCfg.auth_cookie || fileCfg.cookie || (() => {
|
|
92
|
+
try { return readFileSync(COOKIE_PLAIN_FILE, "utf8").trim() } catch { return "" }
|
|
93
|
+
})()
|
|
94
|
+
if (legacyCookie) {
|
|
95
|
+
const enc = encryptText(legacyCookie)
|
|
96
|
+
mkdirSync(CONFIG_DIR, { recursive: true })
|
|
97
|
+
writeFileSync(COOKIE_ENC_FILE, enc, "utf8")
|
|
98
|
+
// 清理旧明文
|
|
99
|
+
const cleanCfg: ConfigFile = { ...fileCfg }
|
|
100
|
+
delete cleanCfg.cookie
|
|
101
|
+
delete cleanCfg.auth_cookie
|
|
102
|
+
try { writeFileSync(CONFIG_FILE, JSON.stringify(cleanCfg, null, 2), "utf8") } catch { /* 忽略 */ }
|
|
103
|
+
try { if (existsSync(COOKIE_PLAIN_FILE)) renameSync(COOKIE_PLAIN_FILE, COOKIE_PLAIN_FILE + ".bak") } catch { /* 忽略 */ }
|
|
104
|
+
return { workspaceId: env.OPENCODE_GO_WORKSPACE_ID || fileCfg.workspace_id || "", authCookie: legacyCookie }
|
|
42
105
|
}
|
|
106
|
+
|
|
107
|
+
return { workspaceId: env.OPENCODE_GO_WORKSPACE_ID || fileCfg.workspace_id || "", authCookie: "" }
|
|
43
108
|
}
|
|
44
109
|
|
|
45
110
|
function saveConfig(patch: { workspace_id?: string; cookie?: string }): void {
|
|
46
|
-
|
|
47
|
-
try {
|
|
48
|
-
fileCfg = JSON.parse(readFileSync(CONFIG_FILE, "utf8"))
|
|
49
|
-
} catch { fileCfg = {} }
|
|
111
|
+
const fileCfg = readJsonFile<ConfigFile>(CONFIG_FILE) ?? {}
|
|
50
112
|
if (patch.workspace_id !== undefined) fileCfg.workspace_id = patch.workspace_id
|
|
51
|
-
if (patch.cookie !== undefined)
|
|
113
|
+
if (patch.cookie !== undefined) {
|
|
114
|
+
// cookie 改为加密存储,不再写明文到 config.json
|
|
115
|
+
if (patch.cookie) {
|
|
116
|
+
mkdirSync(CONFIG_DIR, { recursive: true })
|
|
117
|
+
writeFileSync(COOKIE_ENC_FILE, encryptText(patch.cookie), "utf8")
|
|
118
|
+
delete fileCfg.cookie
|
|
119
|
+
delete fileCfg.auth_cookie
|
|
120
|
+
// 清理可能存在的旧明文 txt
|
|
121
|
+
try { if (existsSync(COOKIE_PLAIN_FILE)) renameSync(COOKIE_PLAIN_FILE, COOKIE_PLAIN_FILE + ".bak") } catch { /* 忽略 */ }
|
|
122
|
+
} else {
|
|
123
|
+
try { if (existsSync(COOKIE_ENC_FILE)) renameSync(COOKIE_ENC_FILE, COOKIE_ENC_FILE + ".bak") } catch { /* 忽略 */ }
|
|
124
|
+
}
|
|
125
|
+
}
|
|
52
126
|
try {
|
|
53
127
|
mkdirSync(dirname(CONFIG_FILE), { recursive: true })
|
|
54
128
|
writeFileSync(CONFIG_FILE, JSON.stringify(fileCfg, null, 2), "utf8")
|
|
@@ -58,6 +132,17 @@ function saveConfig(patch: { workspace_id?: string; cookie?: string }): void {
|
|
|
58
132
|
// 模块级共享信号:/go-config 保存配置后递增,面板监听它立即刷新
|
|
59
133
|
const [configTick, setConfigTick] = createSignal(0)
|
|
60
134
|
|
|
135
|
+
// 模块级共享语言信号:/go-lang 或首次引导切换后,面板和命令实时响应
|
|
136
|
+
const [langCode, setLangCode] = createSignal<LangCode>(detectLang())
|
|
137
|
+
const t = createT(() => langCode())
|
|
138
|
+
|
|
139
|
+
function kvTryGet(api: TuiPluginApi, key: string): string | undefined {
|
|
140
|
+
try { return api.kv.get<string>(key) } catch { return undefined }
|
|
141
|
+
}
|
|
142
|
+
function kvTrySet(api: TuiPluginApi, key: string, val: unknown): void {
|
|
143
|
+
try { api.kv.set(key, val) } catch { /* 忽略 */ }
|
|
144
|
+
}
|
|
145
|
+
|
|
61
146
|
interface Usage {
|
|
62
147
|
rollingUsage: { usagePercent: number; resetInSec: number }
|
|
63
148
|
weeklyUsage: { usagePercent: number; resetInSec: number }
|
|
@@ -107,13 +192,13 @@ async function fetchUsage(workspaceId: string, authCookie: string): Promise<Usag
|
|
|
107
192
|
}
|
|
108
193
|
|
|
109
194
|
function formatReset(sec: number): string {
|
|
110
|
-
if (sec <= 0) return "
|
|
195
|
+
if (sec <= 0) return t("resetDone")
|
|
111
196
|
const d = Math.floor(sec / 86400)
|
|
112
197
|
const h = Math.floor((sec % 86400) / 3600)
|
|
113
198
|
const m = Math.floor((sec % 3600) / 60)
|
|
114
|
-
if (d > 0) return
|
|
115
|
-
if (h > 0) return
|
|
116
|
-
return
|
|
199
|
+
if (d > 0) return t("dayHour", { d, h })
|
|
200
|
+
if (h > 0) return t("hourMin", { h, m })
|
|
201
|
+
return t("minute", { m })
|
|
117
202
|
}
|
|
118
203
|
|
|
119
204
|
function progressBar(percent: number, width: number): string {
|
|
@@ -175,8 +260,8 @@ function GoUsagePanel(props: { theme: TuiThemeCurrent; api: TuiPluginApi }): JSX
|
|
|
175
260
|
}
|
|
176
261
|
setConfigured(true)
|
|
177
262
|
const u = await fetchUsage(cfg.workspaceId, cfg.authCookie)
|
|
178
|
-
if (u === null) { setError("
|
|
179
|
-
if (u.error === "cookie_expired") { setError("
|
|
263
|
+
if (u === null) { setError(t("queryFailed")); return }
|
|
264
|
+
if (u.error === "cookie_expired") { setError(t("cookieExpired")); return }
|
|
180
265
|
if (u.error === "not_configured") { setConfigured(false); return }
|
|
181
266
|
setUsage(u)
|
|
182
267
|
setLastUpdated(new Date().toLocaleTimeString())
|
|
@@ -196,10 +281,10 @@ function GoUsagePanel(props: { theme: TuiThemeCurrent; api: TuiPluginApi }): JSX
|
|
|
196
281
|
|
|
197
282
|
const [pal, setPal] = createSignal<Record<string, string>>({ ...FALLBACK })
|
|
198
283
|
createEffect(() => {
|
|
199
|
-
const
|
|
284
|
+
const th = props.theme as any
|
|
200
285
|
const p: Record<string, string> = { ...FALLBACK }
|
|
201
286
|
for (const k of Object.keys(FALLBACK)) {
|
|
202
|
-
const h = hex(
|
|
287
|
+
const h = hex(th?.[k])
|
|
203
288
|
if (h) p[k] = h
|
|
204
289
|
}
|
|
205
290
|
setPal(p)
|
|
@@ -250,10 +335,11 @@ function GoUsagePanel(props: { theme: TuiThemeCurrent; api: TuiPluginApi }): JSX
|
|
|
250
335
|
>
|
|
251
336
|
<text onMouseUp={() => setOpen(o => !o)}>
|
|
252
337
|
<span style={{ fg: colors().muted }}>{open() ? "\u25bc " : "\u25b6 "}</span>
|
|
253
|
-
<span style={{ fg: colors().primary }}><b>
|
|
338
|
+
<span style={{ fg: colors().primary }}><b>{t("panelTitle")}</b></span>
|
|
339
|
+
<span style={{ fg: colors().muted }}> v{VERSION}</span>
|
|
254
340
|
<Show when={!open() && usage()}>
|
|
255
341
|
<span style={{ fg: colorFor(usage()!.weeklyUsage.usagePercent) }}>
|
|
256
|
-
{" ".repeat(2)}
|
|
342
|
+
{" ".repeat(2)}{t("weeklyShort")} {usage()!.weeklyUsage.usagePercent}%
|
|
257
343
|
</span>
|
|
258
344
|
</Show>
|
|
259
345
|
</text>
|
|
@@ -262,25 +348,25 @@ function GoUsagePanel(props: { theme: TuiThemeCurrent; api: TuiPluginApi }): JSX
|
|
|
262
348
|
<text fg={colors().muted}>{sep()}</text>
|
|
263
349
|
|
|
264
350
|
<Show when={!configured()}>
|
|
265
|
-
<text fg={colors().warning}
|
|
266
|
-
<text fg={colors().muted}
|
|
351
|
+
<text fg={colors().warning}>{t("notConfigured")}</text>
|
|
352
|
+
<text fg={colors().muted}>{t("notConfiguredHint")}</text>
|
|
267
353
|
</Show>
|
|
268
354
|
|
|
269
355
|
<Show when={configured() && error()} fallback={
|
|
270
356
|
<Show when={configured() && usage()} fallback={
|
|
271
|
-
<Show when={configured()}><text fg={colors().muted}
|
|
357
|
+
<Show when={configured()}><text fg={colors().muted}>{t("loading")}</text></Show>
|
|
272
358
|
}>
|
|
273
|
-
{renderRow("
|
|
274
|
-
{renderRow("
|
|
275
|
-
{renderRow("
|
|
359
|
+
{renderRow(t("rowRolling"), usage()!.rollingUsage)}
|
|
360
|
+
{renderRow(t("rowWeekly"), usage()!.weeklyUsage)}
|
|
361
|
+
{renderRow(t("rowMonthly"), usage()!.monthlyUsage)}
|
|
276
362
|
<text>
|
|
277
|
-
<span style={{ fg: colors().muted }}
|
|
363
|
+
<span style={{ fg: colors().muted }}>{t("updatedAt")} </span>
|
|
278
364
|
<span style={{ fg: colors().muted }}>{lastUpdated()}</span>
|
|
279
365
|
</text>
|
|
280
366
|
</Show>
|
|
281
367
|
}>
|
|
282
368
|
<text fg={colors().error}>{error()}</text>
|
|
283
|
-
<text fg={colors().muted}
|
|
369
|
+
<text fg={colors().muted}>{t("reconfigureHint")}</text>
|
|
284
370
|
</Show>
|
|
285
371
|
</Show>
|
|
286
372
|
</box>
|
|
@@ -298,50 +384,100 @@ function createSidebarSlot(api: TuiPluginApi): TuiSlotPlugin {
|
|
|
298
384
|
}
|
|
299
385
|
}
|
|
300
386
|
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
387
|
+
function runConfigDialog(api: TuiPluginApi, dialog: any): void {
|
|
388
|
+
dialog?.replace(() => (
|
|
389
|
+
<api.ui.DialogPrompt
|
|
390
|
+
title={t("wsPromptTitle")}
|
|
391
|
+
description={() => (
|
|
392
|
+
<text>{t("wsPromptDesc")}</text>
|
|
393
|
+
)}
|
|
394
|
+
placeholder={t("wsPlaceholder")}
|
|
395
|
+
onConfirm={(value) => {
|
|
396
|
+
const wsId = value.trim()
|
|
397
|
+
if (!wsId) { dialog?.clear(); return }
|
|
398
|
+
saveConfig({ workspace_id: wsId })
|
|
311
399
|
dialog?.replace(() => (
|
|
312
400
|
<api.ui.DialogPrompt
|
|
313
|
-
title="
|
|
401
|
+
title={t("cookiePromptTitle")}
|
|
314
402
|
description={() => (
|
|
315
|
-
<text
|
|
403
|
+
<text>{t("cookiePromptDesc")}</text>
|
|
316
404
|
)}
|
|
317
|
-
placeholder="
|
|
318
|
-
onConfirm={(
|
|
319
|
-
const
|
|
320
|
-
if (!
|
|
321
|
-
saveConfig({
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
description={() => (
|
|
326
|
-
<text>登录 opencode.ai 后,浏览器 F12 → Application → Cookies → opencode.ai → 复制 auth 的值</text>
|
|
327
|
-
)}
|
|
328
|
-
placeholder="Fe26.2*..."
|
|
329
|
-
onConfirm={(val) => {
|
|
330
|
-
const cookie = val.trim()
|
|
331
|
-
if (!cookie) { dialog?.clear(); return }
|
|
332
|
-
saveConfig({ cookie })
|
|
333
|
-
setConfigTick((v) => v + 1)
|
|
334
|
-
api.ui.toast({ variant: "success", message: "Go 用量配置已保存" })
|
|
335
|
-
dialog?.clear()
|
|
336
|
-
}}
|
|
337
|
-
onCancel={() => dialog?.clear()}
|
|
338
|
-
/>
|
|
339
|
-
))
|
|
405
|
+
placeholder={t("cookiePlaceholder")}
|
|
406
|
+
onConfirm={(val) => {
|
|
407
|
+
const cookie = val.trim()
|
|
408
|
+
if (!cookie) { dialog?.clear(); return }
|
|
409
|
+
saveConfig({ cookie })
|
|
410
|
+
setConfigTick((v) => v + 1)
|
|
411
|
+
api.ui.toast({ variant: "success", message: t("configSaved") })
|
|
412
|
+
dialog?.clear()
|
|
340
413
|
}}
|
|
341
414
|
onCancel={() => dialog?.clear()}
|
|
342
415
|
/>
|
|
343
416
|
))
|
|
344
|
-
}
|
|
417
|
+
}}
|
|
418
|
+
onCancel={() => dialog?.clear()}
|
|
419
|
+
/>
|
|
420
|
+
))
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
function runLangDialog(api: TuiPluginApi, dialog: any): void {
|
|
424
|
+
dialog?.replace(() => (
|
|
425
|
+
<api.ui.DialogSelect
|
|
426
|
+
title={t("langTitle")}
|
|
427
|
+
options={LANG_META.map((m) => ({
|
|
428
|
+
title: `${m.label}${langCode() === m.code ? " \u2713" : ""}`,
|
|
429
|
+
value: m.code,
|
|
430
|
+
}))}
|
|
431
|
+
onSelect={(opt) => {
|
|
432
|
+
const code = opt.value as LangCode
|
|
433
|
+
setLangCode(code)
|
|
434
|
+
kvTrySet(api, `${KV_PREFIX}.lang`, code)
|
|
435
|
+
api.ui.toast({ message: code === "zh" ? t("langSwitchedZh") : t("langSwitchedEn") })
|
|
436
|
+
// 语言选择后:若无配置则继续引导配置账号
|
|
437
|
+
const cfg = loadConfig()
|
|
438
|
+
if (!cfg.workspaceId || !cfg.authCookie) {
|
|
439
|
+
runConfigDialog(api, dialog)
|
|
440
|
+
} else {
|
|
441
|
+
dialog?.clear()
|
|
442
|
+
}
|
|
443
|
+
}}
|
|
444
|
+
onCancel={() => dialog?.clear()}
|
|
445
|
+
/>
|
|
446
|
+
))
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
const tui: TuiPlugin = async (api: TuiPluginApi) => {
|
|
450
|
+
api.slots.register(createSidebarSlot(api))
|
|
451
|
+
|
|
452
|
+
// 恢复已保存的语言;若无则按系统检测
|
|
453
|
+
const savedLang = kvTryGet(api, `${KV_PREFIX}.lang`) as LangCode | undefined
|
|
454
|
+
if (savedLang === "zh" || savedLang === "en") setLangCode(savedLang)
|
|
455
|
+
else setLangCode(detectLang())
|
|
456
|
+
|
|
457
|
+
// 首次启动引导:已引导过则跳过(用 kv 标记)
|
|
458
|
+
const onboarded = kvTryGet(api, `${KV_PREFIX}.onboarded`)
|
|
459
|
+
if (!onboarded) {
|
|
460
|
+
kvTrySet(api, `${KV_PREFIX}.onboarded`, "1")
|
|
461
|
+
setTimeout(() => {
|
|
462
|
+
// 通过 trigger 触发语言选择命令,命令 onSelect 会拿到 dialog 栈并弹窗
|
|
463
|
+
try { api.command.trigger("go-usage.lang") } catch { /* 命令未就绪时忽略 */ }
|
|
464
|
+
}, 1500)
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
api.command?.register(() => [
|
|
468
|
+
{
|
|
469
|
+
title: t("cmdTitle"),
|
|
470
|
+
value: "go-usage.config",
|
|
471
|
+
description: t("cmdDesc"),
|
|
472
|
+
slash: { name: "go-config" },
|
|
473
|
+
onSelect: (dialog) => runConfigDialog(api, dialog),
|
|
474
|
+
},
|
|
475
|
+
{
|
|
476
|
+
title: t("langCmdTitle"),
|
|
477
|
+
value: "go-usage.lang",
|
|
478
|
+
description: t("langCmdDesc"),
|
|
479
|
+
slash: { name: "go-lang" },
|
|
480
|
+
onSelect: (dialog) => runLangDialog(api, dialog),
|
|
345
481
|
},
|
|
346
482
|
])
|
|
347
483
|
}
|