dsh-multi2api 0.1.4
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/LICENSE +23 -0
- package/README.md +69 -0
- package/bin/multi2api-darwin-amd64 +0 -0
- package/bin/multi2api-darwin-arm64 +0 -0
- package/bin/multi2api-linux-amd64 +0 -0
- package/bin/multi2api-linux-arm64 +0 -0
- package/bin/multi2api-win32-x64.exe +0 -0
- package/cordis.patch.yml +3 -0
- package/go-patch/multi2api-plugin-changes.patch +417 -0
- package/icon.svg +6 -0
- package/lib/client.js +960 -0
- package/lib/index.js +1678 -0
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +62 -0
package/lib/client.js
ADDED
|
@@ -0,0 +1,960 @@
|
|
|
1
|
+
// dsh-multi2api —— Web UI 管理面板(浏览器侧)。
|
|
2
|
+
//
|
|
3
|
+
// 说明:这里只依赖 react(从宿主模块表取),不加载任何其它 Harness 客户端包;
|
|
4
|
+
// 控件与样式都按宿主的做法自己写,颜色只用 --dsw-alias-* 主题变量,跟随深浅色。
|
|
5
|
+
window.__ModuleLoader__.load({
|
|
6
|
+
id: "dsh-multi2api",
|
|
7
|
+
factory: function (require) {
|
|
8
|
+
const React = require("react");
|
|
9
|
+
|
|
10
|
+
const ROUTE_PREFIX = "/plugins/dsh-multi2api";
|
|
11
|
+
const REFRESH_MS = 15000;
|
|
12
|
+
|
|
13
|
+
const en = {
|
|
14
|
+
tabLabel: "Multi2API Accounts",
|
|
15
|
+
title: "Multi2API Accounts",
|
|
16
|
+
intro:
|
|
17
|
+
"Each account becomes its own model in DSH, so sessions never cross accounts. Add an account, or switch a session to a different account model.",
|
|
18
|
+
running: "Running",
|
|
19
|
+
stopped: "Not running",
|
|
20
|
+
port: "Port",
|
|
21
|
+
dataDir: "Data folder",
|
|
22
|
+
restart: "Restart service",
|
|
23
|
+
restarting: "Restarting…",
|
|
24
|
+
refresh: "Refresh",
|
|
25
|
+
autoCheckin: "Auto check-in",
|
|
26
|
+
autoCheckinOnHint: "On: every DSH start signs in all accounts once.",
|
|
27
|
+
autoCheckinOffHint: "Off: no automatic sign-in.",
|
|
28
|
+
accounts: "Accounts",
|
|
29
|
+
accountCardUnnamed: "Unnamed account",
|
|
30
|
+
credits: "Credits",
|
|
31
|
+
cooling: "Cooling",
|
|
32
|
+
disabled: "Disabled",
|
|
33
|
+
checkedIn: "Checked in today",
|
|
34
|
+
notCheckedIn: "Not checked in today",
|
|
35
|
+
checkIn: "Check in",
|
|
36
|
+
deleteAccount: "Delete",
|
|
37
|
+
confirmDelete: "Delete this account? The service will restart.",
|
|
38
|
+
model: "Model",
|
|
39
|
+
login: "Add account",
|
|
40
|
+
loginHint: "Scan or open the link below in a browser to sign in. This panel will finish automatically.",
|
|
41
|
+
openLogin: "Open sign-in page",
|
|
42
|
+
waitingLogin: "Waiting for sign-in…",
|
|
43
|
+
loginDone: "Account added.",
|
|
44
|
+
cancel: "Cancel",
|
|
45
|
+
importAccount: "Import account files",
|
|
46
|
+
importHint:
|
|
47
|
+
"Pick one or several credential JSON files exported from another machine, or paste the JSON below. Each file adds one account.",
|
|
48
|
+
pickFiles: "Choose files…",
|
|
49
|
+
pickHint: "You can select several JSON files at once.",
|
|
50
|
+
orPaste: "Or paste JSON",
|
|
51
|
+
importing: "Importing…",
|
|
52
|
+
import: "Import",
|
|
53
|
+
imported: "Imported",
|
|
54
|
+
importPartial: "Some files were skipped",
|
|
55
|
+
failed: "Failed",
|
|
56
|
+
logs: "Service log",
|
|
57
|
+
showLogs: "Show log",
|
|
58
|
+
hideLogs: "Hide log",
|
|
59
|
+
loadFailed: "Could not read status",
|
|
60
|
+
saveFailed: "Operation failed",
|
|
61
|
+
autoModel: "Auto (highest-credit healthy account)",
|
|
62
|
+
empty: "No accounts yet. Use “Add account” to sign in.",
|
|
63
|
+
noCredits: "—",
|
|
64
|
+
catalog: "Upstream models & credit rate",
|
|
65
|
+
catalogHint:
|
|
66
|
+
"Read straight from the upstream account, so this is the live list. The credit rate is how many credits one call costs.",
|
|
67
|
+
catalogFailed: "Could not read upstream models",
|
|
68
|
+
refreshModels: "Re-read upstream models",
|
|
69
|
+
refreshingModels: "Reading…",
|
|
70
|
+
colModel: "Model",
|
|
71
|
+
colRate: "Credit rate",
|
|
72
|
+
colContext: "Context",
|
|
73
|
+
colCapability: "Ability",
|
|
74
|
+
capReasoning: "reasoning",
|
|
75
|
+
capImages: "images",
|
|
76
|
+
capTools: "tools",
|
|
77
|
+
noCatalog: "No upstream model list yet. Try “Re-read upstream models”.",
|
|
78
|
+
accountAlias: "Alias",
|
|
79
|
+
modelCount: "Models",
|
|
80
|
+
colEnable: "On",
|
|
81
|
+
enableHint:
|
|
82
|
+
"Tick a model to add it to DSH’s model list. Unticked models stay hidden, so the chat model picker stays short.",
|
|
83
|
+
accountEnable: "Enable",
|
|
84
|
+
accountEnableHint:
|
|
85
|
+
"Turn an account off and it stops showing up in the model list. New accounts are on by default.",
|
|
86
|
+
enabledCount: "Enabled",
|
|
87
|
+
selectionSaved: "Saved",
|
|
88
|
+
modelHidden: "Off",
|
|
89
|
+
modelEnable: "Turn this model on",
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
const zh = {
|
|
93
|
+
tabLabel: "Multi2API 账号池",
|
|
94
|
+
title: "Multi2API 账号池",
|
|
95
|
+
intro:
|
|
96
|
+
"每个账号在 DSH 里就是一个独立模型,会话互不干扰。加号用“添加账号”,换号时把这个会话切到对应账号的模型即可。",
|
|
97
|
+
running: "运行中",
|
|
98
|
+
stopped: "未运行",
|
|
99
|
+
port: "端口",
|
|
100
|
+
dataDir: "数据目录",
|
|
101
|
+
restart: "重启服务",
|
|
102
|
+
restarting: "重启中…",
|
|
103
|
+
refresh: "刷新",
|
|
104
|
+
autoCheckin: "自动签到",
|
|
105
|
+
autoCheckinOnHint: "已开:每次启动 DSH 会给所有账号签到一次。",
|
|
106
|
+
autoCheckinOffHint: "已关:不自动签到。",
|
|
107
|
+
accounts: "账号",
|
|
108
|
+
accountCardUnnamed: "未命名账号",
|
|
109
|
+
credits: "积分",
|
|
110
|
+
cooling: "冷却中",
|
|
111
|
+
disabled: "已停用",
|
|
112
|
+
checkedIn: "今天已签到",
|
|
113
|
+
notCheckedIn: "今天未签到",
|
|
114
|
+
checkIn: "签到",
|
|
115
|
+
deleteAccount: "删除",
|
|
116
|
+
confirmDelete: "确定删除这个账号吗?服务会重启。",
|
|
117
|
+
model: "模型",
|
|
118
|
+
login: "添加账号",
|
|
119
|
+
loginHint: "用浏览器打开下面的登录链接完成登录,本面板会自动完成后续步骤。",
|
|
120
|
+
openLogin: "打开登录页",
|
|
121
|
+
waitingLogin: "等待登录中…",
|
|
122
|
+
loginDone: "账号已添加。",
|
|
123
|
+
cancel: "取消",
|
|
124
|
+
importAccount: "导入账号文件",
|
|
125
|
+
importHint:
|
|
126
|
+
"选一个或多个从别的机器导出的凭证 JSON 文件,每个文件添加一个账号;也可以直接把内容粘在下面。",
|
|
127
|
+
pickFiles: "选择文件…",
|
|
128
|
+
pickHint: "可以一次选多个 JSON 文件。",
|
|
129
|
+
orPaste: "或粘贴内容",
|
|
130
|
+
importing: "导入中…",
|
|
131
|
+
import: "导入",
|
|
132
|
+
imported: "已导入",
|
|
133
|
+
importPartial: "部分文件被跳过",
|
|
134
|
+
failed: "失败",
|
|
135
|
+
logs: "服务日志",
|
|
136
|
+
showLogs: "查看日志",
|
|
137
|
+
hideLogs: "收起日志",
|
|
138
|
+
loadFailed: "读取状态失败",
|
|
139
|
+
saveFailed: "操作失败",
|
|
140
|
+
autoModel: "自动(积分最高的健康账号)",
|
|
141
|
+
empty: "还没有账号。点“添加账号”登录。",
|
|
142
|
+
noCredits: "—",
|
|
143
|
+
catalog: "上游模型与积分倍率",
|
|
144
|
+
catalogHint: "直接从上游账号读取,所以这里是实时列表。倍率就是每次调用扣多少积分。",
|
|
145
|
+
catalogFailed: "读取上游模型失败",
|
|
146
|
+
refreshModels: "重新读取上游模型",
|
|
147
|
+
refreshingModels: "读取中…",
|
|
148
|
+
colModel: "模型",
|
|
149
|
+
colRate: "积分倍率",
|
|
150
|
+
colContext: "上下文",
|
|
151
|
+
colCapability: "能力",
|
|
152
|
+
capReasoning: "推理",
|
|
153
|
+
capImages: "图片",
|
|
154
|
+
capTools: "工具",
|
|
155
|
+
noCatalog: "还没有上游模型列表,点“重新读取上游模型”试试。",
|
|
156
|
+
accountAlias: "别名",
|
|
157
|
+
modelCount: "模型数",
|
|
158
|
+
colEnable: "启用",
|
|
159
|
+
enableHint: "勾上的模型才会出现在 DSH 的模型列表里,没勾的就不显示,聊天界面的选择框才不会被塞满。",
|
|
160
|
+
accountEnable: "启用",
|
|
161
|
+
accountEnableHint: "关掉的账号不会出现在模型列表里。新加的账号默认是开的。",
|
|
162
|
+
enabledCount: "已启用",
|
|
163
|
+
selectionSaved: "已保存",
|
|
164
|
+
modelHidden: "未启用",
|
|
165
|
+
modelEnable: "启用这个模型",
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
const namespace = "settings.multi2api";
|
|
169
|
+
|
|
170
|
+
async function request(path, init) {
|
|
171
|
+
const response = await fetch(`${ROUTE_PREFIX}${path}`, {
|
|
172
|
+
headers: { accept: "application/json" },
|
|
173
|
+
credentials: "same-origin",
|
|
174
|
+
...init,
|
|
175
|
+
});
|
|
176
|
+
const body = await response.json().catch(() => undefined);
|
|
177
|
+
if (!response.ok) throw new Error(body?.error ?? `HTTP ${response.status}`);
|
|
178
|
+
return body;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function post(path, payload) {
|
|
182
|
+
return request(path, {
|
|
183
|
+
method: "POST",
|
|
184
|
+
headers: { "content-type": "application/json", accept: "application/json" },
|
|
185
|
+
body: JSON.stringify(payload ?? {}),
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function formatCredits(value) {
|
|
190
|
+
const number = Number(value);
|
|
191
|
+
if (!Number.isFinite(number)) return "—";
|
|
192
|
+
return number.toLocaleString();
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// 上游给的倍率是文案,形如 "x0.11 credits" / "x0.05" / "x0.00 credits",
|
|
196
|
+
// 直接原样显示最不容易出错;只把空值换成占位符。
|
|
197
|
+
function formatRate(value) {
|
|
198
|
+
const text = String(value ?? "").trim();
|
|
199
|
+
return text.length > 0 ? text : "—";
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function formatContext(value) {
|
|
203
|
+
const size = Number(value);
|
|
204
|
+
if (!Number.isFinite(size) || size <= 0) return "—";
|
|
205
|
+
if (size >= 1000) return `${Math.round(size / 1000)}K`;
|
|
206
|
+
return String(size);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function timeText(value) {
|
|
210
|
+
if (!value) return "";
|
|
211
|
+
const date = new Date(value);
|
|
212
|
+
if (Number.isNaN(date.getTime()) || date.getFullYear() < 2000) return "";
|
|
213
|
+
return date.toLocaleString();
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function Button(props) {
|
|
217
|
+
const { variant = "secondary", disabled, children, onClick } = props;
|
|
218
|
+
const background =
|
|
219
|
+
variant === "primary"
|
|
220
|
+
? "var(--dsw-alias-brand-primary)"
|
|
221
|
+
: variant === "danger"
|
|
222
|
+
? "transparent"
|
|
223
|
+
: "var(--dsw-alias-bg-layer-2)";
|
|
224
|
+
const color =
|
|
225
|
+
variant === "primary"
|
|
226
|
+
? "var(--dsw-alias-bg-base)"
|
|
227
|
+
: variant === "danger"
|
|
228
|
+
? "var(--dsw-alias-state-error-primary)"
|
|
229
|
+
: "var(--dsw-alias-label-primary)";
|
|
230
|
+
return React.createElement(
|
|
231
|
+
"button",
|
|
232
|
+
{
|
|
233
|
+
type: "button",
|
|
234
|
+
className: "m2a-btn",
|
|
235
|
+
disabled: disabled === true,
|
|
236
|
+
onClick,
|
|
237
|
+
style: {
|
|
238
|
+
background,
|
|
239
|
+
color,
|
|
240
|
+
border:
|
|
241
|
+
variant === "primary"
|
|
242
|
+
? "1px solid transparent"
|
|
243
|
+
: variant === "danger"
|
|
244
|
+
? "1px solid var(--dsw-alias-border-l2)"
|
|
245
|
+
: "1px solid var(--dsw-alias-border-l1)",
|
|
246
|
+
},
|
|
247
|
+
},
|
|
248
|
+
children,
|
|
249
|
+
);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function Badge(props) {
|
|
253
|
+
const tone = props.tone ?? "idle";
|
|
254
|
+
const color =
|
|
255
|
+
tone === "ok"
|
|
256
|
+
? "var(--dsw-alias-state-success-primary)"
|
|
257
|
+
: tone === "warn"
|
|
258
|
+
? "var(--dsw-alias-state-warn-primary)"
|
|
259
|
+
: tone === "error"
|
|
260
|
+
? "var(--dsw-alias-state-error-primary)"
|
|
261
|
+
: "var(--dsw-alias-label-tertiary)";
|
|
262
|
+
return React.createElement(
|
|
263
|
+
"span",
|
|
264
|
+
{
|
|
265
|
+
className: "m2a-badge",
|
|
266
|
+
style: { color, borderColor: "var(--dsw-alias-border-l1)" },
|
|
267
|
+
},
|
|
268
|
+
props.children,
|
|
269
|
+
);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// 开关控件:一个圆角轨道 + 一个圆点,纯 CSS,不依赖 UI 库。
|
|
273
|
+
function Switch(props) {
|
|
274
|
+
const checked = props.checked === true;
|
|
275
|
+
const disabled = props.disabled === true;
|
|
276
|
+
return React.createElement(
|
|
277
|
+
"label",
|
|
278
|
+
{
|
|
279
|
+
className: "m2a-switch",
|
|
280
|
+
title: props.title ?? "",
|
|
281
|
+
style: { opacity: disabled ? 0.55 : 1, cursor: disabled ? "default" : "pointer" },
|
|
282
|
+
},
|
|
283
|
+
React.createElement("input", {
|
|
284
|
+
type: "checkbox",
|
|
285
|
+
checked,
|
|
286
|
+
disabled,
|
|
287
|
+
onChange: (event) => {
|
|
288
|
+
if (disabled) return;
|
|
289
|
+
props.onChange?.(event.target.checked === true);
|
|
290
|
+
},
|
|
291
|
+
style: { position: "absolute", opacity: 0, width: 0, height: 0 },
|
|
292
|
+
}),
|
|
293
|
+
React.createElement("span", {
|
|
294
|
+
className: "m2a-switch-track",
|
|
295
|
+
style: {
|
|
296
|
+
background: checked ? "var(--dsw-alias-brand-primary)" : "var(--dsw-alias-bg-layer-2)",
|
|
297
|
+
borderColor: "var(--dsw-alias-border-l1)",
|
|
298
|
+
},
|
|
299
|
+
}, React.createElement("span", { className: "m2a-switch-knob", style: { left: checked ? 15 : 2 } })),
|
|
300
|
+
React.createElement("span", { className: "m2a-switch-label" }, props.label),
|
|
301
|
+
);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
function Row(props) {
|
|
305
|
+
return React.createElement(
|
|
306
|
+
"div",
|
|
307
|
+
{ className: "m2a-row" },
|
|
308
|
+
React.createElement("span", { className: "m2a-key" }, props.label),
|
|
309
|
+
React.createElement("span", { className: "m2a-value" }, props.children),
|
|
310
|
+
);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
function Multi2ApiPanel(props) {
|
|
314
|
+
const t = props.t;
|
|
315
|
+
const [status, setStatus] = React.useState(undefined);
|
|
316
|
+
const [error, setError] = React.useState("");
|
|
317
|
+
const [busy, setBusy] = React.useState("");
|
|
318
|
+
const [notice, setNotice] = React.useState("");
|
|
319
|
+
const [login, setLogin] = React.useState(undefined);
|
|
320
|
+
const [importOpen, setImportOpen] = React.useState(false);
|
|
321
|
+
const [importText, setImportText] = React.useState("");
|
|
322
|
+
const [pasteOpen, setPasteOpen] = React.useState(false);
|
|
323
|
+
const [autoCheckin, setAutoCheckin] = React.useState(true);
|
|
324
|
+
const fileInputRef = React.useRef(null);
|
|
325
|
+
const [logsOpen, setLogsOpen] = React.useState(false);
|
|
326
|
+
const [logs, setLogs] = React.useState("");
|
|
327
|
+
const mounted = React.useRef(true);
|
|
328
|
+
|
|
329
|
+
const load = React.useCallback(async () => {
|
|
330
|
+
try {
|
|
331
|
+
const body = await request("/status", {});
|
|
332
|
+
if (mounted.current) {
|
|
333
|
+
setStatus(body);
|
|
334
|
+
setError("");
|
|
335
|
+
}
|
|
336
|
+
// 开关状态存在插件数据目录里,单独读一次。
|
|
337
|
+
const settings = await request("/settings", {});
|
|
338
|
+
if (mounted.current && typeof settings?.autoCheckin === "boolean") {
|
|
339
|
+
setAutoCheckin(settings.autoCheckin);
|
|
340
|
+
}
|
|
341
|
+
} catch (loadError) {
|
|
342
|
+
if (mounted.current) setError(String(loadError.message ?? loadError));
|
|
343
|
+
}
|
|
344
|
+
}, []);
|
|
345
|
+
|
|
346
|
+
React.useEffect(() => {
|
|
347
|
+
mounted.current = true;
|
|
348
|
+
void load();
|
|
349
|
+
const timer = window.setInterval(() => {
|
|
350
|
+
void load();
|
|
351
|
+
}, REFRESH_MS);
|
|
352
|
+
return () => {
|
|
353
|
+
mounted.current = false;
|
|
354
|
+
window.clearInterval(timer);
|
|
355
|
+
};
|
|
356
|
+
}, [load]);
|
|
357
|
+
|
|
358
|
+
// 登录轮询:拿到 state 后每 2 秒问一次。
|
|
359
|
+
React.useEffect(() => {
|
|
360
|
+
if (login === undefined || login.state === undefined) return undefined;
|
|
361
|
+
let stopped = false;
|
|
362
|
+
const tick = async () => {
|
|
363
|
+
try {
|
|
364
|
+
const body = await post("/login/poll", { state: login.state });
|
|
365
|
+
if (stopped) return;
|
|
366
|
+
if (body?.done === true) {
|
|
367
|
+
if (body.ok === true) {
|
|
368
|
+
setLogin(undefined);
|
|
369
|
+
setNotice(t("loginDone"));
|
|
370
|
+
await load();
|
|
371
|
+
} else {
|
|
372
|
+
setLogin(undefined);
|
|
373
|
+
setError(String(body?.error ?? t("saveFailed")));
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
} catch (pollError) {
|
|
377
|
+
if (!stopped) setError(String(pollError.message ?? pollError));
|
|
378
|
+
}
|
|
379
|
+
};
|
|
380
|
+
const timer = window.setInterval(() => {
|
|
381
|
+
void tick();
|
|
382
|
+
}, 2000);
|
|
383
|
+
return () => {
|
|
384
|
+
stopped = true;
|
|
385
|
+
window.clearInterval(timer);
|
|
386
|
+
};
|
|
387
|
+
}, [login, load, t]);
|
|
388
|
+
|
|
389
|
+
const run = async (name, action) => {
|
|
390
|
+
setBusy(name);
|
|
391
|
+
setError("");
|
|
392
|
+
setNotice("");
|
|
393
|
+
try {
|
|
394
|
+
await action();
|
|
395
|
+
} catch (runError) {
|
|
396
|
+
setError(String(runError.message ?? runError));
|
|
397
|
+
} finally {
|
|
398
|
+
if (mounted.current) setBusy("");
|
|
399
|
+
}
|
|
400
|
+
};
|
|
401
|
+
|
|
402
|
+
const accounts = Array.isArray(status?.accounts) ? status.accounts : [];
|
|
403
|
+
const running = status?.running === true;
|
|
404
|
+
const catalog = Array.isArray(status?.catalog) ? status.catalog : [];
|
|
405
|
+
const modelRows = Array.isArray(status?.models) ? status.models : [];
|
|
406
|
+
|
|
407
|
+
// 勾/取消勾一个模型:后端会重算模型列表并换掉 pi-ai 快照。
|
|
408
|
+
const toggleModel = async (id, enabled) => {
|
|
409
|
+
await run(`model:${id}`, async () => {
|
|
410
|
+
await post("/selection/save", { kind: "model", id, enabled });
|
|
411
|
+
setNotice(t("selectionSaved"));
|
|
412
|
+
await load();
|
|
413
|
+
});
|
|
414
|
+
};
|
|
415
|
+
|
|
416
|
+
// 开关一个账号:关掉的号不会注册成模型。
|
|
417
|
+
const toggleAccount = async (uid, enabled) => {
|
|
418
|
+
await run(`account:${uid}`, async () => {
|
|
419
|
+
await post("/selection/save", { kind: "account", uid, enabled });
|
|
420
|
+
setNotice(t("selectionSaved"));
|
|
421
|
+
await load();
|
|
422
|
+
});
|
|
423
|
+
};
|
|
424
|
+
|
|
425
|
+
return React.createElement(
|
|
426
|
+
"div",
|
|
427
|
+
{ className: "m2a-root" },
|
|
428
|
+
React.createElement(
|
|
429
|
+
"div",
|
|
430
|
+
{ className: "m2a-head" },
|
|
431
|
+
React.createElement("h2", { className: "m2a-title" }, t("title")),
|
|
432
|
+
React.createElement("p", { className: "m2a-intro" }, t("intro")),
|
|
433
|
+
),
|
|
434
|
+
React.createElement(
|
|
435
|
+
"div",
|
|
436
|
+
{ className: "m2a-panel" },
|
|
437
|
+
React.createElement(
|
|
438
|
+
"div",
|
|
439
|
+
{ className: "m2a-toolbar" },
|
|
440
|
+
React.createElement(Badge, { tone: running ? "ok" : "error" }, running ? t("running") : t("stopped")),
|
|
441
|
+
running
|
|
442
|
+
? React.createElement(Badge, null, `${t("port")} ${status.port}`)
|
|
443
|
+
: null,
|
|
444
|
+
React.createElement("span", { className: "m2a-spacer" }),
|
|
445
|
+
React.createElement(
|
|
446
|
+
Button,
|
|
447
|
+
{
|
|
448
|
+
disabled: busy !== "",
|
|
449
|
+
onClick: () => {
|
|
450
|
+
void run("refresh", load);
|
|
451
|
+
},
|
|
452
|
+
},
|
|
453
|
+
t("refresh"),
|
|
454
|
+
),
|
|
455
|
+
React.createElement(
|
|
456
|
+
Button,
|
|
457
|
+
{
|
|
458
|
+
disabled: busy !== "" || running !== true,
|
|
459
|
+
onClick: () => {
|
|
460
|
+
void run("refreshModels", async () => {
|
|
461
|
+
const body = await post("/refresh-models", {});
|
|
462
|
+
if (body?.ok !== true) throw new Error(body?.error ?? t("saveFailed"));
|
|
463
|
+
setNotice(`${t("catalog")}: ${body.catalog?.length ?? 0}`);
|
|
464
|
+
await load();
|
|
465
|
+
});
|
|
466
|
+
},
|
|
467
|
+
},
|
|
468
|
+
busy === "refreshModels" ? t("refreshingModels") : t("refreshModels"),
|
|
469
|
+
),
|
|
470
|
+
React.createElement(
|
|
471
|
+
Switch,
|
|
472
|
+
{
|
|
473
|
+
checked: autoCheckin === true,
|
|
474
|
+
disabled: busy !== "",
|
|
475
|
+
label: t("autoCheckin"),
|
|
476
|
+
title: autoCheckin === true ? t("autoCheckinOnHint") : t("autoCheckinOffHint"),
|
|
477
|
+
onChange: (next) => {
|
|
478
|
+
void run("autoCheckin", async () => {
|
|
479
|
+
const body = await post("/settings/save", { autoCheckin: next });
|
|
480
|
+
if (body?.ok !== true) throw new Error(body?.error ?? t("saveFailed"));
|
|
481
|
+
setAutoCheckin(body.autoCheckin === true);
|
|
482
|
+
});
|
|
483
|
+
},
|
|
484
|
+
},
|
|
485
|
+
),
|
|
486
|
+
React.createElement(
|
|
487
|
+
Button,
|
|
488
|
+
{
|
|
489
|
+
disabled: busy !== "",
|
|
490
|
+
onClick: () => {
|
|
491
|
+
void run("restart", async () => {
|
|
492
|
+
const body = await post("/restart", {});
|
|
493
|
+
if (body?.ok !== true) throw new Error(body?.error ?? t("saveFailed"));
|
|
494
|
+
await load();
|
|
495
|
+
});
|
|
496
|
+
},
|
|
497
|
+
},
|
|
498
|
+
busy === "restart" ? t("restarting") : t("restart"),
|
|
499
|
+
),
|
|
500
|
+
React.createElement(
|
|
501
|
+
Button,
|
|
502
|
+
{
|
|
503
|
+
variant: "primary",
|
|
504
|
+
disabled: busy !== "",
|
|
505
|
+
onClick: () => {
|
|
506
|
+
void run("login", async () => {
|
|
507
|
+
const body = await post("/login/start", {});
|
|
508
|
+
if (body?.ok !== true) throw new Error(body?.error ?? t("saveFailed"));
|
|
509
|
+
setLogin({ state: body.state, authUrl: body.authUrl });
|
|
510
|
+
window.open(body.authUrl, "_blank", "noopener,noreferrer");
|
|
511
|
+
});
|
|
512
|
+
},
|
|
513
|
+
},
|
|
514
|
+
t("login"),
|
|
515
|
+
),
|
|
516
|
+
),
|
|
517
|
+
React.createElement(
|
|
518
|
+
"div",
|
|
519
|
+
{ className: "m2a-meta" },
|
|
520
|
+
React.createElement(Row, { label: t("dataDir") }, status?.dataDir ?? "—"),
|
|
521
|
+
status?.error ? React.createElement(Row, { label: t("saveFailed") }, status.error) : null,
|
|
522
|
+
status?.statusError ? React.createElement(Row, { label: t("loadFailed") }, status.statusError) : null,
|
|
523
|
+
),
|
|
524
|
+
error ? React.createElement("p", { className: "m2a-error" }, error) : null,
|
|
525
|
+
notice ? React.createElement("p", { className: "m2a-notice" }, notice) : null,
|
|
526
|
+
|
|
527
|
+
login !== undefined
|
|
528
|
+
? React.createElement(
|
|
529
|
+
"div",
|
|
530
|
+
{ className: "m2a-login" },
|
|
531
|
+
React.createElement("p", { className: "m2a-hint" }, t("loginHint")),
|
|
532
|
+
React.createElement(
|
|
533
|
+
"a",
|
|
534
|
+
{
|
|
535
|
+
className: "m2a-link",
|
|
536
|
+
href: login.authUrl,
|
|
537
|
+
target: "_blank",
|
|
538
|
+
rel: "noopener noreferrer",
|
|
539
|
+
},
|
|
540
|
+
t("openLogin"),
|
|
541
|
+
),
|
|
542
|
+
React.createElement("p", { className: "m2a-hint" }, t("waitingLogin")),
|
|
543
|
+
React.createElement(
|
|
544
|
+
Button,
|
|
545
|
+
{
|
|
546
|
+
onClick: () => {
|
|
547
|
+
setLogin(undefined);
|
|
548
|
+
},
|
|
549
|
+
},
|
|
550
|
+
t("cancel"),
|
|
551
|
+
),
|
|
552
|
+
)
|
|
553
|
+
: null,
|
|
554
|
+
|
|
555
|
+
React.createElement(
|
|
556
|
+
"div",
|
|
557
|
+
{ className: "m2a-section" },
|
|
558
|
+
React.createElement("h3", { className: "m2a-subtitle" }, `${t("accounts")} (${accounts.length})`),
|
|
559
|
+
accounts.length === 0
|
|
560
|
+
? React.createElement("p", { className: "m2a-empty" }, t("empty"))
|
|
561
|
+
: React.createElement(
|
|
562
|
+
"div",
|
|
563
|
+
{ className: "m2a-grid" },
|
|
564
|
+
accounts.map((account) =>
|
|
565
|
+
React.createElement(
|
|
566
|
+
"div",
|
|
567
|
+
{ className: "m2a-card", key: account.uid },
|
|
568
|
+
React.createElement(
|
|
569
|
+
"div",
|
|
570
|
+
{ className: "m2a-card-head" },
|
|
571
|
+
React.createElement(
|
|
572
|
+
"span",
|
|
573
|
+
{ className: "m2a-card-name" },
|
|
574
|
+
account.displayName || t("accountCardUnnamed"),
|
|
575
|
+
),
|
|
576
|
+
account.disabled === true ? React.createElement(Badge, { tone: "error" }, t("disabled")) : null,
|
|
577
|
+
account.cooling === true ? React.createElement(Badge, { tone: "warn" }, t("cooling")) : null,
|
|
578
|
+
React.createElement(
|
|
579
|
+
Badge,
|
|
580
|
+
{ tone: account.checkedInToday === true ? "ok" : "idle" },
|
|
581
|
+
account.checkedInToday === true ? t("checkedIn") : t("notCheckedIn"),
|
|
582
|
+
),
|
|
583
|
+
),
|
|
584
|
+
React.createElement(Row, { label: t("credits") }, formatCredits(account.credits)),
|
|
585
|
+
account.alias
|
|
586
|
+
? React.createElement(Row, { label: t("accountAlias") }, account.alias)
|
|
587
|
+
: null,
|
|
588
|
+
account.alias
|
|
589
|
+
? React.createElement(
|
|
590
|
+
Row,
|
|
591
|
+
{ label: t("modelCount") },
|
|
592
|
+
String(modelRows.filter((row) => row.alias === account.alias).length),
|
|
593
|
+
)
|
|
594
|
+
: null,
|
|
595
|
+
account.cooling === true && timeText(account.until)
|
|
596
|
+
? React.createElement(Row, { label: t("cooling") }, `${timeText(account.until)} ${account.reason ?? ""}`)
|
|
597
|
+
: null,
|
|
598
|
+
React.createElement(
|
|
599
|
+
"div",
|
|
600
|
+
{ className: "m2a-card-actions" },
|
|
601
|
+
React.createElement(Switch, {
|
|
602
|
+
checked: account.enabled !== false,
|
|
603
|
+
disabled: busy !== "",
|
|
604
|
+
label: t("accountEnable"),
|
|
605
|
+
title: t("accountEnableHint"),
|
|
606
|
+
onChange: (next) => {
|
|
607
|
+
void toggleAccount(account.uid, next);
|
|
608
|
+
},
|
|
609
|
+
}),
|
|
610
|
+
React.createElement(
|
|
611
|
+
Button,
|
|
612
|
+
{
|
|
613
|
+
variant: "danger",
|
|
614
|
+
disabled: busy !== "",
|
|
615
|
+
onClick: () => {
|
|
616
|
+
if (!window.confirm(t("confirmDelete"))) return;
|
|
617
|
+
void run(`delete:${account.uid}`, async () => {
|
|
618
|
+
const body = await post("/accounts/delete", { uid: account.uid });
|
|
619
|
+
if (body?.ok !== true) throw new Error(body?.error ?? t("saveFailed"));
|
|
620
|
+
await load();
|
|
621
|
+
});
|
|
622
|
+
},
|
|
623
|
+
},
|
|
624
|
+
t("deleteAccount"),
|
|
625
|
+
),
|
|
626
|
+
),
|
|
627
|
+
),
|
|
628
|
+
),
|
|
629
|
+
),
|
|
630
|
+
),
|
|
631
|
+
|
|
632
|
+
// 上游模型清单 + 积分倍率,从上游账号实时读取。
|
|
633
|
+
React.createElement(
|
|
634
|
+
"div",
|
|
635
|
+
{ className: "m2a-section" },
|
|
636
|
+
React.createElement(
|
|
637
|
+
"div",
|
|
638
|
+
{ className: "m2a-toolbar" },
|
|
639
|
+
React.createElement(
|
|
640
|
+
"h3",
|
|
641
|
+
{ className: "m2a-subtitle" },
|
|
642
|
+
`${t("catalog")} (${t("enabledCount")} ${catalog.filter((item) => item.enabled === true).length}/${catalog.length})`,
|
|
643
|
+
),
|
|
644
|
+
React.createElement("span", { className: "m2a-spacer" }),
|
|
645
|
+
),
|
|
646
|
+
React.createElement("p", { className: "m2a-hint" }, t("catalogHint")),
|
|
647
|
+
React.createElement("p", { className: "m2a-hint" }, t("enableHint")),
|
|
648
|
+
status?.catalogError
|
|
649
|
+
? React.createElement(
|
|
650
|
+
"p",
|
|
651
|
+
{ className: "m2a-error" },
|
|
652
|
+
`${t("catalogFailed")}: ${status.catalogError}`,
|
|
653
|
+
)
|
|
654
|
+
: null,
|
|
655
|
+
catalog.length === 0
|
|
656
|
+
? React.createElement("p", { className: "m2a-empty" }, t("noCatalog"))
|
|
657
|
+
: React.createElement(
|
|
658
|
+
"table",
|
|
659
|
+
{ className: "m2a-table" },
|
|
660
|
+
React.createElement(
|
|
661
|
+
"thead",
|
|
662
|
+
null,
|
|
663
|
+
React.createElement(
|
|
664
|
+
"tr",
|
|
665
|
+
null,
|
|
666
|
+
React.createElement("th", { className: "m2a-check-col" }, t("colEnable")),
|
|
667
|
+
React.createElement("th", null, t("colModel")),
|
|
668
|
+
React.createElement("th", null, t("colRate")),
|
|
669
|
+
React.createElement("th", null, t("colContext")),
|
|
670
|
+
React.createElement("th", null, t("colCapability")),
|
|
671
|
+
),
|
|
672
|
+
),
|
|
673
|
+
React.createElement(
|
|
674
|
+
"tbody",
|
|
675
|
+
null,
|
|
676
|
+
catalog.map((item) =>
|
|
677
|
+
React.createElement(
|
|
678
|
+
"tr",
|
|
679
|
+
{ key: item.id, className: item.enabled === true ? "" : "m2a-row-off" },
|
|
680
|
+
React.createElement(
|
|
681
|
+
"td",
|
|
682
|
+
{ className: "m2a-check-col" },
|
|
683
|
+
React.createElement("input", {
|
|
684
|
+
type: "checkbox",
|
|
685
|
+
className: "m2a-check",
|
|
686
|
+
checked: item.enabled === true,
|
|
687
|
+
disabled: busy !== "",
|
|
688
|
+
title: item.enabled === true ? t("colEnable") : t("modelEnable"),
|
|
689
|
+
onChange: (event) => {
|
|
690
|
+
void toggleModel(item.id, event.target.checked);
|
|
691
|
+
},
|
|
692
|
+
}),
|
|
693
|
+
),
|
|
694
|
+
React.createElement("td", null, item.name || item.id),
|
|
695
|
+
React.createElement("td", { className: "m2a-num" }, formatRate(item.credits)),
|
|
696
|
+
React.createElement("td", { className: "m2a-num" }, formatContext(item.contextWindow)),
|
|
697
|
+
React.createElement(
|
|
698
|
+
"td",
|
|
699
|
+
null,
|
|
700
|
+
[
|
|
701
|
+
item.reasoning === true ? t("capReasoning") : "",
|
|
702
|
+
item.images === true ? t("capImages") : "",
|
|
703
|
+
item.toolCall === true ? t("capTools") : "",
|
|
704
|
+
]
|
|
705
|
+
.filter((text) => text.length > 0)
|
|
706
|
+
.join(" · ") || "—",
|
|
707
|
+
),
|
|
708
|
+
),
|
|
709
|
+
),
|
|
710
|
+
),
|
|
711
|
+
),
|
|
712
|
+
),
|
|
713
|
+
|
|
714
|
+
React.createElement(
|
|
715
|
+
"div",
|
|
716
|
+
{ className: "m2a-section" },
|
|
717
|
+
React.createElement(
|
|
718
|
+
"div",
|
|
719
|
+
{ className: "m2a-toolbar" },
|
|
720
|
+
React.createElement("h3", { className: "m2a-subtitle" }, t("importAccount")),
|
|
721
|
+
React.createElement("span", { className: "m2a-spacer" }),
|
|
722
|
+
React.createElement(
|
|
723
|
+
Button,
|
|
724
|
+
{
|
|
725
|
+
onClick: () => {
|
|
726
|
+
setImportOpen(!importOpen);
|
|
727
|
+
},
|
|
728
|
+
},
|
|
729
|
+
importOpen ? t("cancel") : t("import"),
|
|
730
|
+
),
|
|
731
|
+
),
|
|
732
|
+
importOpen
|
|
733
|
+
? React.createElement(
|
|
734
|
+
"div",
|
|
735
|
+
{ className: "m2a-import" },
|
|
736
|
+
React.createElement("p", { className: "m2a-hint" }, t("importHint")),
|
|
737
|
+
// 选择文件:浏览器只允许用户主动点选,所以用一个隐藏的 file input,
|
|
738
|
+
// 用按钮去触发它。multiple 允许一次选多个文件。
|
|
739
|
+
React.createElement("input", {
|
|
740
|
+
ref: fileInputRef,
|
|
741
|
+
type: "file",
|
|
742
|
+
accept: ".json,application/json",
|
|
743
|
+
multiple: true,
|
|
744
|
+
style: { display: "none" },
|
|
745
|
+
onChange: (event) => {
|
|
746
|
+
const files = Array.from(event.target.files ?? []);
|
|
747
|
+
event.target.value = "";
|
|
748
|
+
if (files.length === 0) return;
|
|
749
|
+
void run("import", async () => {
|
|
750
|
+
const docs = [];
|
|
751
|
+
for (const file of files) {
|
|
752
|
+
try {
|
|
753
|
+
docs.push(JSON.parse(await file.text()));
|
|
754
|
+
} catch {
|
|
755
|
+
throw new Error(`${file.name}: JSON 格式不对`);
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
const body = await post("/accounts/import", { docs });
|
|
759
|
+
if (body?.ok !== true) {
|
|
760
|
+
const reason = body?.failed?.[0]?.error ?? body?.error ?? t("saveFailed");
|
|
761
|
+
throw new Error(reason);
|
|
762
|
+
}
|
|
763
|
+
const count = body.imported?.length ?? 0;
|
|
764
|
+
const skipped = body.failed?.length ?? 0;
|
|
765
|
+
setNotice(
|
|
766
|
+
skipped > 0
|
|
767
|
+
? `${t("imported")}: ${count} · ${t("importPartial")}: ${skipped}`
|
|
768
|
+
: `${t("imported")}: ${count}`,
|
|
769
|
+
);
|
|
770
|
+
await load();
|
|
771
|
+
});
|
|
772
|
+
},
|
|
773
|
+
}),
|
|
774
|
+
React.createElement(
|
|
775
|
+
Button,
|
|
776
|
+
{
|
|
777
|
+
variant: "primary",
|
|
778
|
+
disabled: busy !== "",
|
|
779
|
+
onClick: () => {
|
|
780
|
+
fileInputRef.current?.click();
|
|
781
|
+
},
|
|
782
|
+
},
|
|
783
|
+
busy === "import" ? t("importing") : t("pickFiles"),
|
|
784
|
+
),
|
|
785
|
+
React.createElement("span", { className: "m2a-hint" }, t("pickHint")),
|
|
786
|
+
React.createElement(
|
|
787
|
+
"div",
|
|
788
|
+
{ className: "m2a-paste-toggle" },
|
|
789
|
+
React.createElement(
|
|
790
|
+
Button,
|
|
791
|
+
{
|
|
792
|
+
onClick: () => {
|
|
793
|
+
setPasteOpen(!pasteOpen);
|
|
794
|
+
},
|
|
795
|
+
},
|
|
796
|
+
pasteOpen ? t("cancel") : t("orPaste"),
|
|
797
|
+
),
|
|
798
|
+
),
|
|
799
|
+
pasteOpen
|
|
800
|
+
? React.createElement(
|
|
801
|
+
"div",
|
|
802
|
+
{ className: "m2a-paste-body" },
|
|
803
|
+
React.createElement("textarea", {
|
|
804
|
+
className: "m2a-textarea",
|
|
805
|
+
rows: 6,
|
|
806
|
+
value: importText,
|
|
807
|
+
spellCheck: false,
|
|
808
|
+
onChange: (event) => {
|
|
809
|
+
setImportText(event.target.value);
|
|
810
|
+
},
|
|
811
|
+
}),
|
|
812
|
+
React.createElement(
|
|
813
|
+
Button,
|
|
814
|
+
{
|
|
815
|
+
variant: "primary",
|
|
816
|
+
disabled: busy !== "" || importText.trim().length === 0,
|
|
817
|
+
onClick: () => {
|
|
818
|
+
void run("import", async () => {
|
|
819
|
+
let doc;
|
|
820
|
+
try {
|
|
821
|
+
doc = JSON.parse(importText);
|
|
822
|
+
} catch {
|
|
823
|
+
throw new Error("JSON 格式不对");
|
|
824
|
+
}
|
|
825
|
+
const body = await post("/accounts/import", { doc });
|
|
826
|
+
if (body?.ok !== true) {
|
|
827
|
+
const reason =
|
|
828
|
+
body?.failed?.[0]?.error ?? body?.error ?? t("saveFailed");
|
|
829
|
+
throw new Error(reason);
|
|
830
|
+
}
|
|
831
|
+
setImportText("");
|
|
832
|
+
setPasteOpen(false);
|
|
833
|
+
setNotice(`${t("imported")}: ${body.imported?.length ?? 0}`);
|
|
834
|
+
await load();
|
|
835
|
+
});
|
|
836
|
+
},
|
|
837
|
+
},
|
|
838
|
+
t("import"),
|
|
839
|
+
),
|
|
840
|
+
)
|
|
841
|
+
: null,
|
|
842
|
+
)
|
|
843
|
+
: null,
|
|
844
|
+
),
|
|
845
|
+
|
|
846
|
+
React.createElement(
|
|
847
|
+
"div",
|
|
848
|
+
{ className: "m2a-section" },
|
|
849
|
+
React.createElement(
|
|
850
|
+
"div",
|
|
851
|
+
{ className: "m2a-toolbar" },
|
|
852
|
+
React.createElement("h3", { className: "m2a-subtitle" }, t("logs")),
|
|
853
|
+
React.createElement("span", { className: "m2a-spacer" }),
|
|
854
|
+
React.createElement(
|
|
855
|
+
Button,
|
|
856
|
+
{
|
|
857
|
+
onClick: () => {
|
|
858
|
+
const next = !logsOpen;
|
|
859
|
+
setLogsOpen(next);
|
|
860
|
+
if (next) {
|
|
861
|
+
void run("logs", async () => {
|
|
862
|
+
const body = await request("/logs?lines=300", {});
|
|
863
|
+
setLogs(String(body?.text ?? ""));
|
|
864
|
+
});
|
|
865
|
+
}
|
|
866
|
+
},
|
|
867
|
+
},
|
|
868
|
+
logsOpen ? t("hideLogs") : t("showLogs"),
|
|
869
|
+
),
|
|
870
|
+
),
|
|
871
|
+
logsOpen ? React.createElement("pre", { className: "m2a-logs" }, logs) : null,
|
|
872
|
+
),
|
|
873
|
+
),
|
|
874
|
+
);
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
const styleText = [
|
|
878
|
+
".m2a-root{max-width:760px;display:flex;flex-direction:column;gap:12px}",
|
|
879
|
+
".m2a-title{margin:0;font-size:18px;font-weight:600;color:var(--dsw-alias-label-primary)}",
|
|
880
|
+
".m2a-intro{margin:0;font-size:13px;color:var(--dsw-alias-label-tertiary)}",
|
|
881
|
+
".m2a-panel{display:flex;flex-direction:column;gap:10px}",
|
|
882
|
+
".m2a-toolbar{display:flex;align-items:center;gap:8px;flex-wrap:wrap}",
|
|
883
|
+
".m2a-spacer{flex:1}",
|
|
884
|
+
".m2a-btn{border-radius:6px;padding:5px 10px;font-size:13px;cursor:pointer;line-height:1.4}",
|
|
885
|
+
".m2a-btn:disabled{opacity:.5;cursor:default}",
|
|
886
|
+
".m2a-switch{position:relative;display:inline-flex;align-items:center;gap:6px;font-size:13px;user-select:none}",
|
|
887
|
+
".m2a-switch-track{position:relative;display:inline-block;width:32px;height:18px;border:1px solid;border-radius:999px;transition:background .15s ease}",
|
|
888
|
+
".m2a-switch-knob{position:absolute;top:2px;width:12px;height:12px;border-radius:50%;background:var(--dsw-alias-bg-base);transition:left .15s ease}",
|
|
889
|
+
".m2a-switch-label{white-space:nowrap}",
|
|
890
|
+
".m2a-badge{font-size:12px;border:1px solid var(--dsw-alias-border-l1);border-radius:999px;padding:1px 8px;white-space:nowrap}",
|
|
891
|
+
".m2a-meta{display:flex;flex-direction:column;gap:4px;background:var(--dsw-alias-bg-layer-1);border:1px solid var(--dsw-alias-border-l1);border-radius:8px;padding:8px 10px}",
|
|
892
|
+
".m2a-row{display:flex;gap:8px;font-size:13px}",
|
|
893
|
+
".m2a-key{color:var(--dsw-alias-label-tertiary);min-width:64px}",
|
|
894
|
+
".m2a-value{color:var(--dsw-alias-label-primary);word-break:break-all}",
|
|
895
|
+
".m2a-error{color:var(--dsw-alias-state-error-primary);font-size:13px;margin:0}",
|
|
896
|
+
".m2a-notice{color:var(--dsw-alias-state-success-primary);font-size:13px;margin:0}",
|
|
897
|
+
".m2a-hint{color:var(--dsw-alias-label-tertiary);font-size:13px;margin:0}",
|
|
898
|
+
".m2a-login{display:flex;flex-direction:column;gap:8px;background:var(--dsw-alias-bg-layer-1);border:1px solid var(--dsw-alias-border-l1);border-radius:8px;padding:10px}",
|
|
899
|
+
".m2a-link{color:var(--dsw-alias-brand-primary);font-size:13px}",
|
|
900
|
+
".m2a-section{display:flex;flex-direction:column;gap:8px;border-top:1px solid var(--dsw-alias-border-l1);padding-top:10px}",
|
|
901
|
+
".m2a-subtitle{margin:0;font-size:14px;font-weight:600;color:var(--dsw-alias-label-primary)}",
|
|
902
|
+
".m2a-empty{color:var(--dsw-alias-label-tertiary);font-size:13px;margin:0}",
|
|
903
|
+
".m2a-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(300px,1fr));gap:10px}",
|
|
904
|
+
".m2a-card{display:flex;flex-direction:column;gap:6px;background:var(--dsw-alias-bg-layer-1);border:1px solid var(--dsw-alias-border-l1);border-radius:8px;padding:10px}",
|
|
905
|
+
".m2a-card-head{display:flex;align-items:center;gap:6px;flex-wrap:wrap}",
|
|
906
|
+
".m2a-card-name{font-weight:600;font-size:14px;color:var(--dsw-alias-label-primary)}",
|
|
907
|
+
".m2a-card-actions{display:flex;gap:8px;align-items:center;margin-top:2px}",
|
|
908
|
+
".m2a-import{display:flex;flex-direction:column;gap:8px;align-items:flex-start}",
|
|
909
|
+
".m2a-table{width:100%;border-collapse:collapse;font-size:13px}",
|
|
910
|
+
".m2a-table th{text-align:left;font-weight:600;color:var(--dsw-alias-label-tertiary);border-bottom:1px solid var(--dsw-alias-border-l1);padding:4px 8px 4px 0}",
|
|
911
|
+
".m2a-table td{color:var(--dsw-alias-label-primary);border-bottom:1px solid var(--dsw-alias-border-l1);padding:4px 8px 4px 0}",
|
|
912
|
+
".m2a-check-col{width:44px}",
|
|
913
|
+
".m2a-check{width:15px;height:15px;cursor:pointer;accent-color:var(--dsw-alias-brand-primary,rgb(56,132,255))}",
|
|
914
|
+
".m2a-check:disabled{cursor:default;opacity:.5}",
|
|
915
|
+
".m2a-row-off td{color:var(--dsw-alias-label-tertiary)}",
|
|
916
|
+
".m2a-num{font-family:monospace;white-space:nowrap}",
|
|
917
|
+
".m2a-paste-toggle{margin-top:2px}",
|
|
918
|
+
".m2a-paste-body{display:flex;flex-direction:column;gap:8px;align-items:flex-start;width:100%}",
|
|
919
|
+
".m2a-textarea{width:100%;font-family:monospace;font-size:12px;background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-primary);border:1px solid var(--dsw-alias-border-l1);border-radius:6px;padding:8px;resize:vertical}",
|
|
920
|
+
".m2a-logs{max-height:320px;overflow:auto;font-size:12px;font-family:monospace;background:var(--dsw-alias-bg-layer-1);border:1px solid var(--dsw-alias-border-l1);border-radius:8px;padding:8px;color:var(--dsw-alias-label-secondary);white-space:pre-wrap}",
|
|
921
|
+
].join("\n");
|
|
922
|
+
|
|
923
|
+
let styleInjected = false;
|
|
924
|
+
function ensureStyle() {
|
|
925
|
+
if (styleInjected) return;
|
|
926
|
+
styleInjected = true;
|
|
927
|
+
const element = document.createElement("style");
|
|
928
|
+
element.setAttribute("data-plugin", "dsh-multi2api");
|
|
929
|
+
element.textContent = styleText;
|
|
930
|
+
document.head.appendChild(element);
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
return {
|
|
934
|
+
name: "dsh-multi2api",
|
|
935
|
+
inject: ["slots", "locale"],
|
|
936
|
+
apply(ctx) {
|
|
937
|
+
try {
|
|
938
|
+
ensureStyle();
|
|
939
|
+
ctx.effect(() => ctx.locale.register(namespace, { zh, en }), "dsh-multi2api: 面板文案");
|
|
940
|
+
const t = ctx.locale.bind(namespace);
|
|
941
|
+
ctx.slots.inject("settings.plugins.tab", () =>
|
|
942
|
+
ctx.slots.register(
|
|
943
|
+
{
|
|
944
|
+
name: "settings.plugins.tab",
|
|
945
|
+
id: "multi2api",
|
|
946
|
+
order: 31,
|
|
947
|
+
label: () => t("tabLabel"),
|
|
948
|
+
locale: namespace,
|
|
949
|
+
inject: () => ({ t }),
|
|
950
|
+
},
|
|
951
|
+
Multi2ApiPanel,
|
|
952
|
+
),
|
|
953
|
+
);
|
|
954
|
+
} catch (error) {
|
|
955
|
+
console.error("[dsh-multi2api] 面板注册失败", error);
|
|
956
|
+
}
|
|
957
|
+
},
|
|
958
|
+
};
|
|
959
|
+
},
|
|
960
|
+
});
|