@router-ai/dsh-plugin-routerai 0.1.0 → 0.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/CHANGELOG.md +9 -0
- package/README.md +16 -1
- package/docs/DETAIL.md +2 -0
- package/docs/DETAIL.ru.md +2 -0
- package/lib/client.js +219 -6
- package/lib/index.js +381 -14
- package/package.json +19 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
# История изменений
|
|
2
2
|
|
|
3
|
+
## 0.2.0
|
|
4
|
+
|
|
5
|
+
- Агент рисует картинки: попросите его нарисовать иллюстрацию, логотип или схему — картинка появится в чате
|
|
6
|
+
и сохранится в папку `routerai-media` проекта. Модель выбирается в настройках, там же рисование можно выключить.
|
|
7
|
+
- Голосовой ввод: RouterAI работает как служба распознавания речи для плагина «Голосовой ввод» DeepSeek Harness.
|
|
8
|
+
Модель распознавания выбирается в настройках.
|
|
9
|
+
- Стоимость картинок входит в стоимость сессии — это точная сумма, которую сообщает RouterAI.
|
|
10
|
+
- Остаток в боковой панели обновляется быстрее после входа через команду `/routerai`.
|
|
11
|
+
|
|
3
12
|
## 0.1.0
|
|
4
13
|
|
|
5
14
|
Первый выпуск.
|
package/README.md
CHANGED
|
@@ -36,8 +36,23 @@ dsh plugin --profile desktop add @router-ai/dsh-plugin-routerai
|
|
|
36
36
|
| Под полем ввода | стоимость текущей сессии, токены, доля входа из кэша |
|
|
37
37
|
| Отказы из-за денег | понятное сообщение и ссылка вместо кода ошибки |
|
|
38
38
|
| Служебная модель | названия сессий и сжатие контекста на недорогой модели |
|
|
39
|
+
| Картинки | агент рисует по просьбе: картинка в чате и файл в папке `routerai-media` проекта |
|
|
40
|
+
| Голосовой ввод | распознавание речи RouterAI для плагина «Голосовой ввод» |
|
|
39
41
|
|
|
40
|
-
Стоимость сессии — расчёт по числу токенов и ценам
|
|
42
|
+
Стоимость сессии — расчёт по числу токенов и ценам каталога; стоимость картинок RouterAI сообщает точно.
|
|
43
|
+
Итоговые списания показывает кабинет RouterAI.
|
|
44
|
+
|
|
45
|
+
### Картинки
|
|
46
|
+
|
|
47
|
+
Попросите агента: «нарисуй логотип для…», «сделай иллюстрацию к…». Модель для картинок выбирается в
|
|
48
|
+
**Настройки → RouterAI → Дополнительно**; там же рисование можно выключить, и тогда агент не будет тратить на него деньги.
|
|
49
|
+
Каждая картинка оплачивается отдельно, цена указана рядом с моделью.
|
|
50
|
+
|
|
51
|
+
### Голосовой ввод
|
|
52
|
+
|
|
53
|
+
Голосовой ввод в DeepSeek Harness — отдельный экспериментальный плагин. Включите его
|
|
54
|
+
(пакет `@deepseek-ai/dsh-experimental-voice-input-bundle`) и выберите службу распознавания **RouterAI**.
|
|
55
|
+
Модель распознавания задаётся в тех же настройках RouterAI.
|
|
41
56
|
|
|
42
57
|
## Команда `/routerai`
|
|
43
58
|
|
package/docs/DETAIL.md
CHANGED
|
@@ -16,4 +16,6 @@ others — behind a single key and bills in rubles. This plugin adds those model
|
|
|
16
16
|
- Balance in the sidebar and the session cost under the composer.
|
|
17
17
|
- Clear messages when the balance runs out or the key hits its spending limit.
|
|
18
18
|
- Savings: session titles and context compaction run on an inexpensive utility model.
|
|
19
|
+
- Images: ask the agent to draw — the result appears in the chat and in the project's `routerai-media` folder.
|
|
20
|
+
- Voice input: RouterAI recognizes speech for the Voice input plugin.
|
|
19
21
|
- The `/routerai` command for the same actions from the chat: `login`, `key`, `logout`, `status`, `models`, `find`.
|
package/docs/DETAIL.ru.md
CHANGED
|
@@ -16,4 +16,6 @@ Qwen и многих других — за одним ключом и прини
|
|
|
16
16
|
- Баланс в боковой панели и стоимость сессии под полем ввода.
|
|
17
17
|
- Понятные сообщения, если закончились деньги или ключ упёрся в лимит.
|
|
18
18
|
- Экономия: названия сессий и сжатие контекста делает недорогая служебная модель.
|
|
19
|
+
- Картинки: попросите агента нарисовать — результат появится в чате и в папке `routerai-media` проекта.
|
|
20
|
+
- Голосовой ввод: RouterAI распознаёт речь для плагина «Голосовой ввод».
|
|
19
21
|
- Команда `/routerai` для тех же действий из чата: `login`, `key`, `logout`, `status`, `models`, `find`.
|
package/lib/client.js
CHANGED
|
@@ -10,6 +10,8 @@ window.__ModuleLoader__.load({
|
|
|
10
10
|
// src/links.ts
|
|
11
11
|
const KEYS_PAGE = "https://routerai.ru/settings/keys";
|
|
12
12
|
const BILLING_PAGE = "https://routerai.ru/settings/billing";
|
|
13
|
+
/** Имя инструмента рисования — по нему же браузерная часть подставляет свою карточку вызова. */
|
|
14
|
+
const DRAW_TOOL = "routerai_draw_image";
|
|
13
15
|
// src/client/logo.ts
|
|
14
16
|
const LOGO = "data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2224%22%20height%3D%2224%22%20fill%3D%22none%22%20viewBox%3D%220%200%2024%2024%22%3E%3Cpath%20fill%3D%22%232d67f6%22%20d%3D%22M8.22%2021.06a4.09%204.09%200%200%200%207.55%200%204.1%204.1%200%200%200%205.34-5.32%204.08%204.08%200%200%200%200-7.53%204.07%204.07%200%200%200-5.34-5.33%204.09%204.09%200%200%200-7.55%200A4.1%204.1%200%200%200%202.9%208.21a4.08%204.08%200%200%200%200%207.53%204.07%204.07%200%200%200%205.33%205.32%22%2F%3E%3Cpath%20fill%3D%22%23fff%22%20d%3D%22M23.25%2011.97a3.7%203.7%200%200%200-2.28-3.42l-.35-.14.14-.35a3.7%203.7%200%200%200-4.84-4.83l-.35.14-.14-.34A3.7%203.7%200%200%200%2012.29.76L12%20.75c-1.54%200-2.87.94-3.43%202.28l-.14.34-.35-.14a3.72%203.72%200%200%200-4.85%204.83l.15.35-.35.14a3.7%203.7%200%200%200-2.28%203.42v.29a3.7%203.7%200%200%200%202.28%203.13l.35.14-.15.35a3.7%203.7%200%200%200%204.85%204.83l.35-.14.14.35a3.71%203.71%200%200%200%206.86%200l.14-.35.35.14a3.72%203.72%200%200%200%204.85-4.83l-.15-.35.35-.14a3.7%203.7%200%200%200%202.28-3.42M12%201.97c1.03%200%201.91.63%202.29%201.53l.61%201.48%201.49-.61a2.48%202.48%200%200%201%203.23%203.23l-.6%201.47%201.47.62c.9.37%201.53%201.26%201.53%202.28v.09a2.5%202.5%200%200%201-1.53%202.2l-1.48.6.61%201.49a2.47%202.47%200%200%201-3.23%203.23l-1.49-.61-.61%201.47a2.48%202.48%200%200%201-4.58%200l-.61-1.47-1.49.6a2.48%202.48%200%200%201-3.23-3.22l.6-1.48-1.47-.62a2.5%202.5%200%200%201-1.53-2.28v-.09c.04-.99.66-1.83%201.53-2.2l1.48-.6-.61-1.48A2.47%202.47%200%200%201%207.6%204.37l1.49.6.61-1.47A2.5%202.5%200%200%201%2012%201.98m0%2015.8q-.36%200-.66.13l-1.6.66.66%201.6a1.73%201.73%200%200%200%203.2%200l.67-1.6-1.61-.66q-.3-.13-.66-.13m-6.93-1.14a1.72%201.72%200%200%200%202.26%202.25l1.6-.65-.66-1.6a1.7%201.7%200%200%200-.93-.93l-1.61-.67zm11.6-.93a1.7%201.7%200%200%200-.94.93l-.66%201.6%201.6.65a1.74%201.74%200%200%200%202.26-2.25l-.66-1.6zM6.18%2011.97q0-.36-.13-.66l-.66-1.6-1.6.67c-.61.25-1.04.84-1.07%201.52v.08c0%20.7.44%201.32%201.07%201.58l1.6.67.66-1.6q.13-.3.13-.66m8.89%200q0-.71.21-1.36a4.5%204.5%200%200%201-1.93-1.92%204.5%204.5%200%200%201-2.72%200A4.4%204.4%200%200%201%208.7%2010.6a4.4%204.4%200%200%201%200%202.72%204.5%204.5%200%200%201%201.93%201.92%204.5%204.5%200%200%201%202.72%200%204.4%204.4%200%200%201%201.93-1.92%204%204%200%200%201-.21-1.36M7.33%205.06A1.74%201.74%200%200%200%205.07%207.3l.66%201.6%201.6-.66a1.7%201.7%200%200%200%20.94-.93l.66-1.6zm11.22.37c-.5-.5-1.25-.63-1.88-.37l-1.6.66.66%201.6a1.7%201.7%200%200%200%20.93.93l1.61.66.66-1.6c.26-.63.13-1.37-.38-1.88M12%202.73c-.72%200-1.34.43-1.6%201.06l-.67%201.6%201.61.66a1.7%201.7%200%200%200%201.32%200l1.6-.67-.66-1.6A1.7%201.7%200%200%200%2012%202.74m-5.06%209.24q0%20.5-.19.94l-.61%201.48%201.48.62a2.5%202.5%200%200%201%201.34%201.33l.61%201.48%201.49-.61a2.5%202.5%200%200%201%201.88%200l1.49.6.61-1.47A2.5%202.5%200%200%201%2016.38%2015l1.48-.62-.61-1.48a2.5%202.5%200%200%201%200-1.88l.6-1.48-1.47-.61a2.5%202.5%200%200%201-1.34-1.34l-.61-1.47-1.49.6a2.5%202.5%200%200%201-1.88%200l-1.49-.6-.61%201.47a2.5%202.5%200%200%201-1.34%201.34l-1.48.61.61%201.48q.19.43.19.94m8.89%200q0%20.76.27%201.41l.15.35-.35.14a3.7%203.7%200%200%200-2%202l-.14.34-.35-.14a3.7%203.7%200%200%200-2.82%200l-.35.14-.14-.34a3.7%203.7%200%200%200-2-2l-.35-.14.15-.35a3.7%203.7%200%200%200%200-2.82l-.15-.34.35-.15a3.7%203.7%200%200%200%202-2l.14-.34.35.15a3.7%203.7%200%200%200%202.82%200l.35-.15.14.35a3.7%203.7%200%200%200%202%202l.35.14-.15.34q-.26.65-.27%201.41m1.98%200q0%20.36.13.66l.66%201.6%201.6-.67a1.7%201.7%200%200%200%201.07-1.52v-.2a1.7%201.7%200%200%200-1.07-1.46l-1.6-.67-.66%201.6q-.12.3-.13.66m6.19%200c0%201.73-.98%203.22-2.42%203.96a4.44%204.44%200%200%201-5.61%205.6%204.46%204.46%200%200%201-7.94%200%204.47%204.47%200%200%201-5.6-5.6A4.5%204.5%200%200%201%200%2012.14v-.17c0-1.72.98-3.22%202.42-3.96a4.44%204.44%200%200%201%205.61-5.6A4.5%204.5%200%200%201%2012%200h.17a4.5%204.5%200%200%201%203.8%202.41%204.47%204.47%200%200%201%205.6%205.6A4.5%204.5%200%200%201%2024%2011.97%22%2F%3E%3Cpath%20fill%3D%22%23fff%22%20d%3D%22M10.44%208.22a4%204%200%200%200%203.12%200%204%204%200%200%200%202.2%202.2%204%204%200%200%200%200%203.1%204%204%200%200%200-2.2%202.2%204%204%200%200%200-3.12%200%204%204%200%200%200-2.2-2.2%204%204%200%200%200%200-3.1%204%204%200%200%200%202.2-2.2%22%2F%3E%3C%2Fsvg%3E";
|
|
15
17
|
// src/client/parts.tsx
|
|
@@ -59,6 +61,7 @@ window.__ModuleLoader__.load({
|
|
|
59
61
|
return payload;
|
|
60
62
|
}
|
|
61
63
|
const IDLE_REFRESH_MS = 45e3;
|
|
64
|
+
const DISCONNECTED_REFRESH_MS = 8e3;
|
|
62
65
|
const WAITING_REFRESH_MS = 1200;
|
|
63
66
|
/** Состояние плагина в этой вкладке. Опрашивает сервер редко, а пока идёт вход — часто. */
|
|
64
67
|
var Remote = class {
|
|
@@ -100,9 +103,10 @@ window.__ModuleLoader__.load({
|
|
|
100
103
|
}
|
|
101
104
|
if (this.stopped) return;
|
|
102
105
|
const waiting = this.view.overview?.signIn.stage === "waiting";
|
|
106
|
+
const pause = waiting ? WAITING_REFRESH_MS : this.view.overview?.connected === false ? DISCONNECTED_REFRESH_MS : IDLE_REFRESH_MS;
|
|
103
107
|
this.timer = setTimeout(() => {
|
|
104
108
|
this.pull(waiting);
|
|
105
|
-
},
|
|
109
|
+
}, pause);
|
|
106
110
|
}
|
|
107
111
|
publish(view) {
|
|
108
112
|
this.view = view;
|
|
@@ -202,6 +206,103 @@ window.__ModuleLoader__.load({
|
|
|
202
206
|
if (tokens >= 1e6) return `${Math.round(tokens / 1e5) / 10}M`;
|
|
203
207
|
return `${Math.round(tokens / 1e3)}K`;
|
|
204
208
|
}
|
|
209
|
+
// src/client/DrawCard.tsx
|
|
210
|
+
/** Описание картинки из аргументов вызова; пока модель их дописывает, JSON может быть неполным. */
|
|
211
|
+
function promptOf(block) {
|
|
212
|
+
for (const raw of [block.argsRaw, block.call?.argsRaw]) {
|
|
213
|
+
if (raw === void 0) continue;
|
|
214
|
+
try {
|
|
215
|
+
const prompt = JSON.parse(raw).prompt;
|
|
216
|
+
if (typeof prompt === "string") return prompt;
|
|
217
|
+
} catch {
|
|
218
|
+
const partial = /"prompt"\s*:\s*"((?:[^"\\]|\\.)*)/.exec(raw)?.[1];
|
|
219
|
+
if (partial !== void 0) return partial.replace(/\\n/g, " ").replace(/\\(.)/g, "$1");
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
return "";
|
|
223
|
+
}
|
|
224
|
+
function Picture({ attachment, loadImage }) {
|
|
225
|
+
const [url, setUrl] = (0, react.useState)(null);
|
|
226
|
+
(0, react.useEffect)(() => {
|
|
227
|
+
let current = true;
|
|
228
|
+
loadImage(attachment).then((loaded) => {
|
|
229
|
+
if (current) setUrl(loaded);
|
|
230
|
+
}, () => {});
|
|
231
|
+
return () => {
|
|
232
|
+
current = false;
|
|
233
|
+
};
|
|
234
|
+
}, [attachment, loadImage]);
|
|
235
|
+
return url === null ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("i", {}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("img", {
|
|
236
|
+
src: url,
|
|
237
|
+
alt: attachment.name ?? "",
|
|
238
|
+
width: attachment.width,
|
|
239
|
+
height: attachment.height,
|
|
240
|
+
style: { height: "auto" }
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
function DrawCard({ t, phase, block, loadImage, openFile }) {
|
|
244
|
+
const prompt = promptOf(block);
|
|
245
|
+
if (phase !== "result") return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
246
|
+
className: "rai-drawn",
|
|
247
|
+
"data-routerai": "draw-card",
|
|
248
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
249
|
+
className: "rai-line",
|
|
250
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: "rai-busy" }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("b", { children: t("drawing") })]
|
|
251
|
+
}), prompt === "" ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
252
|
+
className: "rai-note",
|
|
253
|
+
children: prompt
|
|
254
|
+
})]
|
|
255
|
+
});
|
|
256
|
+
if (block.isError === true) {
|
|
257
|
+
const reason = block.content?.find((item) => item.type === "text")?.text ?? block.error?.reason ?? "";
|
|
258
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
259
|
+
className: "rai-drawn",
|
|
260
|
+
"data-routerai": "draw-card",
|
|
261
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("b", { children: t("drawFailed") }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
262
|
+
className: "rai-alert",
|
|
263
|
+
children: reason
|
|
264
|
+
})]
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
const meta = block.meta ?? {};
|
|
268
|
+
const shown = (block.content ?? []).flatMap((item) => item.type === "image" && item.attachment !== void 0 ? [item.attachment] : []);
|
|
269
|
+
const facts = [meta.model, typeof meta.cost === "number" ? t("drawnCost", { cost: rub(meta.cost) }) : void 0].filter(Boolean).join(" · ");
|
|
270
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
271
|
+
className: "rai-drawn",
|
|
272
|
+
"data-routerai": "draw-card",
|
|
273
|
+
children: [
|
|
274
|
+
shown.length === 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
275
|
+
className: "rai-gallery",
|
|
276
|
+
children: shown.map((attachment) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Picture, {
|
|
277
|
+
attachment,
|
|
278
|
+
loadImage
|
|
279
|
+
}, attachment.attachmentId))
|
|
280
|
+
}),
|
|
281
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
282
|
+
className: "rai-line",
|
|
283
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("b", { children: t("drawn") }), facts === "" ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
284
|
+
className: "rai-faint",
|
|
285
|
+
children: facts
|
|
286
|
+
})]
|
|
287
|
+
}),
|
|
288
|
+
prompt === "" ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
289
|
+
className: "rai-note",
|
|
290
|
+
children: prompt
|
|
291
|
+
}),
|
|
292
|
+
(meta.files ?? []).length === 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
293
|
+
className: "rai-line",
|
|
294
|
+
children: (meta.files ?? []).map((path) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
|
|
295
|
+
variant: "outline",
|
|
296
|
+
size: "sm",
|
|
297
|
+
onClick: () => {
|
|
298
|
+
openFile(path);
|
|
299
|
+
},
|
|
300
|
+
children: t("openFile", { name: path.split(/[\\/]/).pop() ?? path })
|
|
301
|
+
}, path))
|
|
302
|
+
})
|
|
303
|
+
]
|
|
304
|
+
});
|
|
305
|
+
}
|
|
205
306
|
// src/client/SessionCost.tsx
|
|
206
307
|
const REFRESH_MS = 4e3;
|
|
207
308
|
const count = (value) => value.toLocaleString("ru-RU");
|
|
@@ -568,13 +669,14 @@ window.__ModuleLoader__.load({
|
|
|
568
669
|
(0, react.useEffect)(() => {
|
|
569
670
|
setDraft(saved);
|
|
570
671
|
}, [savedKey]);
|
|
571
|
-
const
|
|
572
|
-
const
|
|
672
|
+
const chat = (0, react.useMemo)(() => (catalog ?? []).filter((entry) => entry.makes.includes("text")), [catalog]);
|
|
673
|
+
const bySlug = (0, react.useMemo)(() => new Map(chat.map((entry) => [entry.slug, entry])), [chat]);
|
|
674
|
+
const makers = (0, react.useMemo)(() => [...new Set(chat.map((entry) => entry.maker))].sort((a, b) => a.localeCompare(b)), [chat]);
|
|
573
675
|
const matches = (0, react.useMemo)(() => {
|
|
574
676
|
const needle = search.trim().toLowerCase();
|
|
575
|
-
return
|
|
677
|
+
return chat.filter((entry) => (maker === "" || entry.maker === maker) && (!toolsOnly || entry.tools) && (needle === "" || `${entry.slug} ${entry.maker} ${entry.title}`.toLowerCase().includes(needle)));
|
|
576
678
|
}, [
|
|
577
|
-
|
|
679
|
+
chat,
|
|
578
680
|
search,
|
|
579
681
|
maker,
|
|
580
682
|
toolsOnly
|
|
@@ -816,8 +918,11 @@ window.__ModuleLoader__.load({
|
|
|
816
918
|
function Extras({ t, remote, overview, catalog }) {
|
|
817
919
|
const [threshold, setThreshold] = (0, react.useState)(String(overview.lowBalance));
|
|
818
920
|
const action = useAction();
|
|
819
|
-
const cheapestFirst = (0, react.useMemo)(() => (catalog ?? []).filter((entry) => entry.tools && entry.price !== void 0 && !entry.alias).sort((a, b) => (a.price?.input ?? 0) - (b.price?.input ?? 0)), [catalog]);
|
|
921
|
+
const cheapestFirst = (0, react.useMemo)(() => (catalog ?? []).filter((entry) => entry.makes.includes("text") && entry.tools && entry.price !== void 0 && !entry.alias).sort((a, b) => (a.price?.input ?? 0) - (b.price?.input ?? 0)), [catalog]);
|
|
820
922
|
const current = overview.utilityModel;
|
|
923
|
+
const painters = (0, react.useMemo)(() => (catalog ?? []).filter((entry) => entry.makes.includes("image")).sort((a, b) => (a.perImage ?? 1e400) - (b.perImage ?? 1e400) || a.title.localeCompare(b.title)), [catalog]);
|
|
924
|
+
const listeners = (0, react.useMemo)(() => (catalog ?? []).filter((entry) => entry.makes.includes("transcription")).sort((a, b) => (a.perSecond ?? 1e400) - (b.perSecond ?? 1e400) || a.title.localeCompare(b.title)), [catalog]);
|
|
925
|
+
const known = (list, slug) => slug === null || list.some((entry) => entry.slug === slug);
|
|
821
926
|
const commitThreshold = () => {
|
|
822
927
|
const value = Number(threshold.replace(",", "."));
|
|
823
928
|
if (Number.isFinite(value) && value >= 0) action.run(() => remote.savePreferences({ lowBalance: value }));
|
|
@@ -866,6 +971,79 @@ window.__ModuleLoader__.load({
|
|
|
866
971
|
})
|
|
867
972
|
]
|
|
868
973
|
}),
|
|
974
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
975
|
+
style: {
|
|
976
|
+
display: "grid",
|
|
977
|
+
gap: 6
|
|
978
|
+
},
|
|
979
|
+
children: [
|
|
980
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("imageLabel") }),
|
|
981
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
|
|
982
|
+
className: "rai-choice",
|
|
983
|
+
"data-routerai": "image-model",
|
|
984
|
+
value: overview.imageModel ?? "",
|
|
985
|
+
onChange: (event) => {
|
|
986
|
+
action.run(() => remote.savePreferences({ imageModel: event.target.value === "" ? null : event.target.value }));
|
|
987
|
+
},
|
|
988
|
+
children: [
|
|
989
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
|
|
990
|
+
value: "",
|
|
991
|
+
children: t("imageOff")
|
|
992
|
+
}),
|
|
993
|
+
known(painters, overview.imageModel) ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
|
|
994
|
+
value: overview.imageModel ?? "",
|
|
995
|
+
children: overview.imageModel
|
|
996
|
+
}),
|
|
997
|
+
painters.map((entry) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("option", {
|
|
998
|
+
value: entry.slug,
|
|
999
|
+
children: [
|
|
1000
|
+
entry.maker,
|
|
1001
|
+
" ",
|
|
1002
|
+
entry.title,
|
|
1003
|
+
entry.perImage === void 0 ? "" : ` · ${t("perImage", { price: rub(entry.perImage) })}`
|
|
1004
|
+
]
|
|
1005
|
+
}, entry.slug))
|
|
1006
|
+
]
|
|
1007
|
+
}),
|
|
1008
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1009
|
+
className: "rai-note",
|
|
1010
|
+
children: t("imageHint")
|
|
1011
|
+
})
|
|
1012
|
+
]
|
|
1013
|
+
}),
|
|
1014
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
1015
|
+
style: {
|
|
1016
|
+
display: "grid",
|
|
1017
|
+
gap: 6
|
|
1018
|
+
},
|
|
1019
|
+
children: [
|
|
1020
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("speechLabel") }),
|
|
1021
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
|
|
1022
|
+
className: "rai-choice",
|
|
1023
|
+
"data-routerai": "speech-model",
|
|
1024
|
+
value: overview.speechModel,
|
|
1025
|
+
onChange: (event) => {
|
|
1026
|
+
action.run(() => remote.savePreferences({ speechModel: event.target.value }));
|
|
1027
|
+
},
|
|
1028
|
+
children: [known(listeners, overview.speechModel) ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
|
|
1029
|
+
value: overview.speechModel,
|
|
1030
|
+
children: overview.speechModel
|
|
1031
|
+
}), listeners.map((entry) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("option", {
|
|
1032
|
+
value: entry.slug,
|
|
1033
|
+
children: [
|
|
1034
|
+
entry.maker,
|
|
1035
|
+
" ",
|
|
1036
|
+
entry.title,
|
|
1037
|
+
entry.perSecond === void 0 ? "" : ` · ${t("perMinute", { price: rub(entry.perSecond * 60) })}`
|
|
1038
|
+
]
|
|
1039
|
+
}, entry.slug))]
|
|
1040
|
+
}),
|
|
1041
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1042
|
+
className: "rai-note",
|
|
1043
|
+
children: t("speechHint")
|
|
1044
|
+
})
|
|
1045
|
+
]
|
|
1046
|
+
}),
|
|
869
1047
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
870
1048
|
className: "rai-line",
|
|
871
1049
|
children: [
|
|
@@ -1103,6 +1281,12 @@ label.rai-row{cursor:pointer}
|
|
|
1103
1281
|
.rai-sheet li>span{flex:1;min-width:0}
|
|
1104
1282
|
.rai-banner{display:flex;align-items:center;gap:12px;max-width:540px;padding:12px 14px;border-radius:var(--dsw-radius-md);font:var(--dsw-font-s-14);color:var(--dsw-alias-label-primary);background:var(--dsw-alias-bg-layer-1);border:1px solid var(--dsw-alias-border-l3);box-shadow:0 8px 24px rgba(0,0,0,.14)}
|
|
1105
1283
|
.rai-banner>div{flex:1;min-width:0;display:grid;gap:2px}
|
|
1284
|
+
.rai-drawn{display:grid;gap:8px;padding:10px 12px;border-radius:var(--dsw-radius-md);font:var(--dsw-font-s-14);color:var(--dsw-alias-label-primary);background:var(--dsw-alias-bg-layer-1);border:1px solid var(--dsw-alias-border-l3)}
|
|
1285
|
+
.rai-gallery{display:flex;flex-wrap:wrap;gap:8px}
|
|
1286
|
+
.rai-gallery img,.rai-gallery i{display:block;max-width:min(320px,100%);max-height:320px;border-radius:var(--dsw-radius-sm);background:var(--dsw-alias-bg-layer-2)}
|
|
1287
|
+
.rai-gallery i{width:200px;height:200px}
|
|
1288
|
+
.rai-busy{width:14px;height:14px;flex:none;border-radius:50%;border:2px solid var(--dsw-alias-border-l3);border-top-color:${ACCENT};animation:rai-turn .8s linear infinite}
|
|
1289
|
+
@keyframes rai-turn{to{transform:rotate(1turn)}}
|
|
1106
1290
|
`;
|
|
1107
1291
|
/** Подключить стили. Атрибуты data-plugin* — соглашение harness: по ним он снимает стили выгруженного плагина. */
|
|
1108
1292
|
function mountStyles(owner) {
|
|
@@ -1172,11 +1356,23 @@ label.rai-row{cursor:pointer}
|
|
|
1172
1356
|
utilityHint: "На ней придумываются названия сессий и сжимается длинный контекст. Дешёвая модель экономит деньги и не влияет на ответы.",
|
|
1173
1357
|
utilityOff: "Не использовать — всё делает модель разговора",
|
|
1174
1358
|
lowBalanceLabel: "Предупреждать о балансе ниже",
|
|
1359
|
+
imageLabel: "Модель для картинок",
|
|
1360
|
+
imageHint: "Попросите агента нарисовать что-нибудь — картинка появится в чате и сохранится в папку routerai-media проекта. Каждая картинка оплачивается отдельно.",
|
|
1361
|
+
imageOff: "Не рисовать картинки",
|
|
1362
|
+
perImage: "{price} за картинку",
|
|
1363
|
+
speechLabel: "Модель распознавания речи",
|
|
1364
|
+
speechHint: "Работает с голосовым вводом DeepSeek Harness: включите плагин «Голосовой ввод» и выберите в нём службу RouterAI.",
|
|
1365
|
+
perMinute: "{price} за минуту",
|
|
1175
1366
|
sidebarConnect: "Войти",
|
|
1176
1367
|
sidebarTitle: "RouterAI",
|
|
1177
1368
|
sidebarLow: "Баланс подходит к концу.",
|
|
1178
1369
|
sidebarHint: "Модели и ключ настраиваются в Настройках → RouterAI.",
|
|
1179
1370
|
close: "Закрыть",
|
|
1371
|
+
drawing: "Рисую картинку…",
|
|
1372
|
+
drawn: "Картинка",
|
|
1373
|
+
drawFailed: "Нарисовать не получилось",
|
|
1374
|
+
drawnCost: "стоимость {cost}",
|
|
1375
|
+
openFile: "Открыть {name}",
|
|
1180
1376
|
costTitle: "Стоимость сессии",
|
|
1181
1377
|
costTotal: "Всего",
|
|
1182
1378
|
costLast: "Последний запрос",
|
|
@@ -1251,11 +1447,23 @@ label.rai-row{cursor:pointer}
|
|
|
1251
1447
|
utilityHint: "It names sessions and compacts long context. A cheap model saves money and does not affect answers.",
|
|
1252
1448
|
utilityOff: "Off — the conversation model does everything",
|
|
1253
1449
|
lowBalanceLabel: "Warn when the balance is below",
|
|
1450
|
+
imageLabel: "Image model",
|
|
1451
|
+
imageHint: "Ask the agent to draw something: the image appears in the chat and is saved to the routerai-media folder of the project. Each image is billed separately.",
|
|
1452
|
+
imageOff: "Do not draw images",
|
|
1453
|
+
perImage: "{price} per image",
|
|
1454
|
+
speechLabel: "Speech recognition model",
|
|
1455
|
+
speechHint: "Used by DeepSeek Harness voice input: enable the Voice input plugin and choose the RouterAI service there.",
|
|
1456
|
+
perMinute: "{price} per minute",
|
|
1254
1457
|
sidebarConnect: "Sign in",
|
|
1255
1458
|
sidebarTitle: "RouterAI",
|
|
1256
1459
|
sidebarLow: "The balance is running out.",
|
|
1257
1460
|
sidebarHint: "Models and the key are configured in Settings → RouterAI.",
|
|
1258
1461
|
close: "Close",
|
|
1462
|
+
drawing: "Drawing the image…",
|
|
1463
|
+
drawn: "Image",
|
|
1464
|
+
drawFailed: "Could not draw the image",
|
|
1465
|
+
drawnCost: "cost {cost}",
|
|
1466
|
+
openFile: "Open {name}",
|
|
1259
1467
|
costTitle: "Session cost",
|
|
1260
1468
|
costTotal: "Total",
|
|
1261
1469
|
costLast: "Last request",
|
|
@@ -1338,6 +1546,11 @@ label.rai-row{cursor:pointer}
|
|
|
1338
1546
|
component: MoneyNotice
|
|
1339
1547
|
}
|
|
1340
1548
|
];
|
|
1549
|
+
placements.push({
|
|
1550
|
+
slot: "tool.call.toolview",
|
|
1551
|
+
options: { key: DRAW_TOOL },
|
|
1552
|
+
component: DrawCard
|
|
1553
|
+
});
|
|
1341
1554
|
for (const { slot, options, component } of placements) slots.inject(slot, () => slots.register({
|
|
1342
1555
|
name: slot,
|
|
1343
1556
|
locale: NAMESPACE,
|
package/lib/index.js
CHANGED
|
@@ -9,12 +9,17 @@ import { mkdir, readFile, rename, unlink, writeFile } from "node:fs/promises";
|
|
|
9
9
|
import { spawn } from "node:child_process";
|
|
10
10
|
import { createHash, randomBytes } from "node:crypto";
|
|
11
11
|
import { createServer } from "node:http";
|
|
12
|
+
import { defineTool } from "@deepseek-ai/dsh-tools";
|
|
12
13
|
import { InMemoryCredentialStore, createProvider } from "@earendil-works/pi-ai";
|
|
13
14
|
import { openAICompletionsApi } from "@earendil-works/pi-ai/api/openai-completions.lazy";
|
|
14
15
|
// src/links.ts
|
|
15
16
|
const API_BASE = "https://routerai.ru/api/v1";
|
|
16
17
|
const KEYS_PAGE = "https://routerai.ru/settings/keys";
|
|
17
18
|
const BILLING_PAGE = "https://routerai.ru/settings/billing";
|
|
19
|
+
/** Имя инструмента рисования — по нему же браузерная часть подставляет свою карточку вызова. */
|
|
20
|
+
const DRAW_TOOL = "routerai_draw_image";
|
|
21
|
+
/** Папка в рабочем каталоге проекта, куда сохраняются нарисованные картинки. */
|
|
22
|
+
const MEDIA_FOLDER = "routerai-media";
|
|
18
23
|
/** Идентификатор маршрута в harness: так провайдер называется в пикере и конфигурации. */
|
|
19
24
|
const ROUTE = "routerai";
|
|
20
25
|
// src/failures.ts
|
|
@@ -319,6 +324,24 @@ var CostBook = class {
|
|
|
319
324
|
};
|
|
320
325
|
this.close(sessionId, entry);
|
|
321
326
|
}
|
|
327
|
+
/** Учесть готовую сумму — например, картинку: RouterAI сам сообщает её стоимость. */
|
|
328
|
+
addCharge(sessionId, model, rub) {
|
|
329
|
+
const entry = this.open(sessionId);
|
|
330
|
+
entry.total += rub;
|
|
331
|
+
entry.last = {
|
|
332
|
+
model,
|
|
333
|
+
rub
|
|
334
|
+
};
|
|
335
|
+
const share = entry.perModel[model] ?? {
|
|
336
|
+
calls: 0,
|
|
337
|
+
rub: 0
|
|
338
|
+
};
|
|
339
|
+
entry.perModel[model] = {
|
|
340
|
+
calls: share.calls + 1,
|
|
341
|
+
rub: share.rub + rub
|
|
342
|
+
};
|
|
343
|
+
this.close(sessionId, entry);
|
|
344
|
+
}
|
|
322
345
|
open(sessionId) {
|
|
323
346
|
const existing = this.sessions.get(sessionId);
|
|
324
347
|
return existing === void 0 ? blank() : structuredClone(existing);
|
|
@@ -521,6 +544,8 @@ var Bridge = class {
|
|
|
521
544
|
},
|
|
522
545
|
utilityModel: preferences.current.utilityModel,
|
|
523
546
|
lowBalance: preferences.current.lowBalance,
|
|
547
|
+
imageModel: preferences.current.imageModel,
|
|
548
|
+
speechModel: preferences.current.speechModel,
|
|
524
549
|
problem: this.problem !== null && Date.now() - this.problem.at < PROBLEM_SHOWN_MS ? this.problem : null
|
|
525
550
|
};
|
|
526
551
|
}
|
|
@@ -631,6 +656,12 @@ var Bridge = class {
|
|
|
631
656
|
}
|
|
632
657
|
};
|
|
633
658
|
// src/catalog.ts
|
|
659
|
+
const OUTPUTS = [
|
|
660
|
+
"text",
|
|
661
|
+
"image",
|
|
662
|
+
"transcription"
|
|
663
|
+
];
|
|
664
|
+
const isChatModel = (card) => card.makes.includes("text");
|
|
634
665
|
const SLUG = /^~?[a-z0-9][\w.-]*\/[a-z0-9][\w.:-]*$/i;
|
|
635
666
|
function isModelSlug(value) {
|
|
636
667
|
return typeof value === "string" && value.length <= 160 && SLUG.test(value);
|
|
@@ -666,6 +697,12 @@ function tokenPrice(pricing, units) {
|
|
|
666
697
|
if (cacheWrite !== void 0) price.cacheWrite = cacheWrite;
|
|
667
698
|
return price;
|
|
668
699
|
}
|
|
700
|
+
/** Цена за картинку из `image_pricing`; модели с оплатой по токенам или мегапикселям так не оценить. */
|
|
701
|
+
function firstImagePrice(pricing) {
|
|
702
|
+
if (!Array.isArray(pricing)) return void 0;
|
|
703
|
+
const known = pricing.map(bag).filter((entry) => entry["unit"] === "image").map((entry) => amount(entry["price"])).filter((value) => value !== void 0);
|
|
704
|
+
return known.length === 0 ? void 0 : Math.min(...known);
|
|
705
|
+
}
|
|
669
706
|
/** «Anthropic: Claude Sonnet 5.5» → производитель и название по отдельности. */
|
|
670
707
|
function splitName(name, slug) {
|
|
671
708
|
const colon = name.indexOf(": ");
|
|
@@ -679,13 +716,14 @@ function splitName(name, slug) {
|
|
|
679
716
|
title: name.trim() || slug
|
|
680
717
|
};
|
|
681
718
|
}
|
|
682
|
-
/** Одна запись ответа; undefined для повреждённых записей и
|
|
719
|
+
/** Одна запись ответа; undefined для повреждённых записей и моделей, с которыми плагин не работает (эмбеддинги, видео, озвучка). */
|
|
683
720
|
function readModelCard(entry) {
|
|
684
721
|
const raw = bag(entry);
|
|
685
722
|
if (!isModelSlug(raw["id"])) return void 0;
|
|
686
723
|
const architecture = bag(raw["architecture"]);
|
|
687
|
-
const
|
|
688
|
-
|
|
724
|
+
const declared = strings(architecture["output_modalities"]);
|
|
725
|
+
const makes = declared.length === 0 ? ["text"] : OUTPUTS.filter((output) => declared.includes(output));
|
|
726
|
+
if (makes.length === 0) return void 0;
|
|
689
727
|
const parameters = strings(raw["supported_parameters"]);
|
|
690
728
|
const slug = raw["id"];
|
|
691
729
|
const card = {
|
|
@@ -694,12 +732,19 @@ function readModelCard(entry) {
|
|
|
694
732
|
alias: slug.startsWith("~"),
|
|
695
733
|
tools: parameters.includes("tools"),
|
|
696
734
|
reasoning: parameters.includes("reasoning") || parameters.includes("reasoning_effort"),
|
|
697
|
-
vision: strings(architecture["input_modalities"]).includes("image")
|
|
735
|
+
vision: strings(architecture["input_modalities"]).includes("image"),
|
|
736
|
+
makes
|
|
698
737
|
};
|
|
699
738
|
const window = amount(raw["context_length"]);
|
|
700
739
|
if (window !== void 0 && window > 0) card.window = window;
|
|
701
740
|
const price = tokenPrice(bag(raw["pricing"]), bag(raw["pricing_units"]));
|
|
702
741
|
if (price !== void 0) card.price = price;
|
|
742
|
+
const perImage = firstImagePrice(raw["image_pricing"]);
|
|
743
|
+
if (perImage !== void 0) card.perImage = perImage;
|
|
744
|
+
if (bag(raw["pricing_units"])["seconds"] === "second") {
|
|
745
|
+
const perSecond = amount(bag(raw["pricing"])["seconds"]);
|
|
746
|
+
if (perSecond !== void 0) card.perSecond = perSecond;
|
|
747
|
+
}
|
|
703
748
|
return card;
|
|
704
749
|
}
|
|
705
750
|
/** Ответ `GET /models` целиком. */
|
|
@@ -726,11 +771,11 @@ async function downloadModelList(apiBase, signal) {
|
|
|
726
771
|
}
|
|
727
772
|
function acceptSnapshot(raw) {
|
|
728
773
|
const value = bag(raw);
|
|
729
|
-
if (value["format"] !==
|
|
774
|
+
if (value["format"] !== 2 || typeof value["source"] !== "string" || typeof value["savedAt"] !== "number") return void 0;
|
|
730
775
|
if (!Array.isArray(value["cards"])) return void 0;
|
|
731
|
-
const cards = value["cards"].filter((card) => isModelSlug(bag(card)["slug"]));
|
|
776
|
+
const cards = value["cards"].filter((card) => isModelSlug(bag(card)["slug"]) && Array.isArray(bag(card)["makes"]));
|
|
732
777
|
return {
|
|
733
|
-
format:
|
|
778
|
+
format: 2,
|
|
734
779
|
source: value["source"],
|
|
735
780
|
savedAt: value["savedAt"],
|
|
736
781
|
cards
|
|
@@ -793,7 +838,7 @@ var ModelCatalog = class {
|
|
|
793
838
|
this.lastError = void 0;
|
|
794
839
|
this.adopt(cards, this.clock());
|
|
795
840
|
await this.disk?.write({
|
|
796
|
-
format:
|
|
841
|
+
format: 2,
|
|
797
842
|
source: this.options.apiBase,
|
|
798
843
|
savedAt: this.stamp,
|
|
799
844
|
cards
|
|
@@ -808,6 +853,99 @@ var ModelCatalog = class {
|
|
|
808
853
|
for (const listener of this.listeners) listener();
|
|
809
854
|
}
|
|
810
855
|
};
|
|
856
|
+
// src/media.ts
|
|
857
|
+
var MediaError = class extends Error {
|
|
858
|
+
status;
|
|
859
|
+
constructor(message, status) {
|
|
860
|
+
super(message);
|
|
861
|
+
this.status = status;
|
|
862
|
+
}
|
|
863
|
+
};
|
|
864
|
+
async function post(apiBase, path, apiKey, body, signal, timeoutMs) {
|
|
865
|
+
let response;
|
|
866
|
+
try {
|
|
867
|
+
response = await fetch(`${apiBase}${path}`, {
|
|
868
|
+
method: "POST",
|
|
869
|
+
headers: {
|
|
870
|
+
authorization: `Bearer ${apiKey}`,
|
|
871
|
+
"content-type": "application/json",
|
|
872
|
+
accept: "application/json",
|
|
873
|
+
"X-Title": "DeepSeek Harness"
|
|
874
|
+
},
|
|
875
|
+
body: JSON.stringify(body),
|
|
876
|
+
signal: AbortSignal.any([signal, AbortSignal.timeout(timeoutMs)])
|
|
877
|
+
});
|
|
878
|
+
} catch (error) {
|
|
879
|
+
if (signal.aborted) throw error;
|
|
880
|
+
throw new MediaError(`RouterAI не ответил: ${error instanceof Error ? error.message : String(error)}`);
|
|
881
|
+
}
|
|
882
|
+
const payload = await response.json().catch(() => null);
|
|
883
|
+
if (!response.ok || payload === null) {
|
|
884
|
+
const problem = payload?.["error"];
|
|
885
|
+
const detail = typeof problem === "string" ? problem : problem?.message;
|
|
886
|
+
throw new MediaError(typeof detail === "string" && detail !== "" ? detail : `RouterAI ответил HTTP ${response.status}`, response.status);
|
|
887
|
+
}
|
|
888
|
+
return payload;
|
|
889
|
+
}
|
|
890
|
+
function cost(payload) {
|
|
891
|
+
const value = payload["usage"]?.cost;
|
|
892
|
+
return typeof value === "number" && Number.isFinite(value) ? value : void 0;
|
|
893
|
+
}
|
|
894
|
+
/** Тип картинки по первым байтам: RouterAI указывает его не для всех моделей. */
|
|
895
|
+
function sniffPictureType(bytes) {
|
|
896
|
+
const starts = (...signature) => signature.every((byte, index) => bytes[index] === byte);
|
|
897
|
+
if (starts(137, 80, 78, 71)) return "image/png";
|
|
898
|
+
if (starts(255, 216, 255)) return "image/jpeg";
|
|
899
|
+
if (starts(71, 73, 70, 56)) return "image/gif";
|
|
900
|
+
if (starts(82, 73, 70, 70) && bytes[8] === 87 && bytes[9] === 69 && bytes[10] === 66 && bytes[11] === 80) return "image/webp";
|
|
901
|
+
}
|
|
902
|
+
async function drawPictures(apiBase, apiKey, request, signal) {
|
|
903
|
+
const payload = await post(apiBase, "/images", apiKey, {
|
|
904
|
+
model: request.model,
|
|
905
|
+
prompt: request.prompt,
|
|
906
|
+
...request.aspectRatio === void 0 ? {} : { aspect_ratio: request.aspectRatio },
|
|
907
|
+
...request.count === void 0 ? {} : { n: request.count }
|
|
908
|
+
}, signal, 24e4);
|
|
909
|
+
const pictures = [];
|
|
910
|
+
for (const item of Array.isArray(payload["data"]) ? payload["data"] : []) {
|
|
911
|
+
if (typeof item?.["b64_json"] !== "string") continue;
|
|
912
|
+
const bytes = new Uint8Array(Buffer.from(item["b64_json"], "base64"));
|
|
913
|
+
const type = sniffPictureType(bytes);
|
|
914
|
+
if (type !== void 0) pictures.push({
|
|
915
|
+
bytes,
|
|
916
|
+
type
|
|
917
|
+
});
|
|
918
|
+
}
|
|
919
|
+
if (pictures.length === 0) throw new MediaError("RouterAI не вернул ни одной картинки в поддерживаемом формате (PNG, JPEG, WebP, GIF)");
|
|
920
|
+
const price = cost(payload);
|
|
921
|
+
return price === void 0 ? { pictures } : {
|
|
922
|
+
pictures,
|
|
923
|
+
cost: price
|
|
924
|
+
};
|
|
925
|
+
}
|
|
926
|
+
/** Длительность записи по заголовку WAV; 0, если заголовок не разобрать. */
|
|
927
|
+
function wavSeconds(wav) {
|
|
928
|
+
if (wav.byteLength < 44) return 0;
|
|
929
|
+
const bytesPerSecond = new DataView(wav.buffer, wav.byteOffset, wav.byteLength).getUint32(28, true);
|
|
930
|
+
return bytesPerSecond > 0 ? (wav.byteLength - 44) / bytesPerSecond : 0;
|
|
931
|
+
}
|
|
932
|
+
/** Распознать запись (WAV). Язык — код ISO-639-1; без него RouterAI определяет язык сам. */
|
|
933
|
+
async function recognizeSpeech(apiBase, apiKey, request, signal) {
|
|
934
|
+
const payload = await post(apiBase, "/audio/transcriptions", apiKey, {
|
|
935
|
+
model: request.model,
|
|
936
|
+
input_audio: {
|
|
937
|
+
data: Buffer.from(request.wav).toString("base64"),
|
|
938
|
+
format: "wav"
|
|
939
|
+
},
|
|
940
|
+
...request.language === void 0 ? {} : { language: request.language }
|
|
941
|
+
}, signal, 12e4);
|
|
942
|
+
const heard = { text: typeof payload["text"] === "string" ? payload["text"].trim() : "" };
|
|
943
|
+
const seconds = payload["usage"]?.seconds;
|
|
944
|
+
if (typeof seconds === "number" && seconds > 0) heard.seconds = seconds;
|
|
945
|
+
const price = cost(payload);
|
|
946
|
+
if (price !== void 0) heard.cost = price;
|
|
947
|
+
return heard;
|
|
948
|
+
}
|
|
811
949
|
// src/money.ts
|
|
812
950
|
/** Сколько знаков после запятой показать, чтобы сумма не выглядела нулём и не шумела копейками. */
|
|
813
951
|
function fractionDigits(amount) {
|
|
@@ -835,9 +973,157 @@ function windowLabel(tokens) {
|
|
|
835
973
|
if (tokens >= 1e6) return `${Math.round(tokens / 1e5) / 10}M`;
|
|
836
974
|
return `${Math.round(tokens / 1e3)}K`;
|
|
837
975
|
}
|
|
976
|
+
// src/draw.ts
|
|
977
|
+
const MOST_PICTURES = 4;
|
|
978
|
+
const EXTENSIONS = {
|
|
979
|
+
"image/png": "png",
|
|
980
|
+
"image/jpeg": "jpg",
|
|
981
|
+
"image/webp": "webp",
|
|
982
|
+
"image/gif": "gif"
|
|
983
|
+
};
|
|
984
|
+
/** Имя файла из пожелания модели или из описания картинки: латиница, цифры, дефисы. */
|
|
985
|
+
function fileStem(wish, prompt, now = /* @__PURE__ */ new Date()) {
|
|
986
|
+
const base = (wish ?? "").toLowerCase().replace(/\.[a-z0-9]+$/, "").replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 48);
|
|
987
|
+
if (base !== "") return base;
|
|
988
|
+
const stamp = now.toISOString().replace(/[-:]/g, "").replace("T", "-").slice(0, 15);
|
|
989
|
+
const words = prompt.toLowerCase().match(/[a-z0-9]+/g)?.slice(0, 4).join("-") ?? "";
|
|
990
|
+
return words === "" ? `image-${stamp}` : `${words.slice(0, 40)}-${stamp}`;
|
|
991
|
+
}
|
|
992
|
+
function summary(drawn) {
|
|
993
|
+
const count = Math.max(drawn.files.length, drawn.shown.length);
|
|
994
|
+
const parts = [`Готово: ${count === 1 ? "картинка" : `картинок: ${count}`}, модель ${drawn.model}`];
|
|
995
|
+
if (drawn.cost !== null) parts.push(`стоимость ${rub(drawn.cost)}`);
|
|
996
|
+
if (drawn.files.length > 0) parts.push(`файлы: ${drawn.files.join(", ")}`);
|
|
997
|
+
return `${parts.join("; ")}.`;
|
|
998
|
+
}
|
|
999
|
+
const pictures = (shown) => shown.map((attachment) => ({
|
|
1000
|
+
type: "image",
|
|
1001
|
+
attachment
|
|
1002
|
+
}));
|
|
1003
|
+
function registerDrawTool(ctx, deps) {
|
|
1004
|
+
const tool = defineTool({
|
|
1005
|
+
name: DRAW_TOOL,
|
|
1006
|
+
description: `Нарисовать картинку по текстовому описанию (иллюстрация, логотип, макет, схема). Картинка появится в чате и сохранится в папку ${MEDIA_FOLDER} рабочего каталога. Вызов платный — используй, когда пользователь просит изображение.`,
|
|
1007
|
+
parameters: {
|
|
1008
|
+
prompt: {
|
|
1009
|
+
type: "string",
|
|
1010
|
+
required: true,
|
|
1011
|
+
description: "Подробное описание картинки: что изображено, стиль, цвета, композиция. Надписи на картинке указывай дословно."
|
|
1012
|
+
},
|
|
1013
|
+
aspect_ratio: {
|
|
1014
|
+
type: "string",
|
|
1015
|
+
description: "Соотношение сторон, например 1:1, 16:9, 9:16, 4:3. По умолчанию выбирает модель."
|
|
1016
|
+
},
|
|
1017
|
+
count: {
|
|
1018
|
+
type: "integer",
|
|
1019
|
+
description: `Сколько вариантов нарисовать, от 1 до ${MOST_PICTURES}. По умолчанию 1.`
|
|
1020
|
+
},
|
|
1021
|
+
file_name: {
|
|
1022
|
+
type: "string",
|
|
1023
|
+
description: "Имя файла без расширения, латиницей. По умолчанию составляется из описания."
|
|
1024
|
+
}
|
|
1025
|
+
},
|
|
1026
|
+
output: {
|
|
1027
|
+
schema: {
|
|
1028
|
+
type: "object",
|
|
1029
|
+
additionalProperties: false,
|
|
1030
|
+
properties: {
|
|
1031
|
+
model: {
|
|
1032
|
+
type: "string",
|
|
1033
|
+
required: true
|
|
1034
|
+
},
|
|
1035
|
+
cost: {
|
|
1036
|
+
oneOf: [{ type: "number" }, { type: "null" }],
|
|
1037
|
+
required: true
|
|
1038
|
+
},
|
|
1039
|
+
files: {
|
|
1040
|
+
type: "array",
|
|
1041
|
+
items: { type: "string" },
|
|
1042
|
+
required: true
|
|
1043
|
+
},
|
|
1044
|
+
shown: {
|
|
1045
|
+
type: "array",
|
|
1046
|
+
items: { type: "json" },
|
|
1047
|
+
required: true
|
|
1048
|
+
}
|
|
1049
|
+
}
|
|
1050
|
+
},
|
|
1051
|
+
render: (_args, value) => {
|
|
1052
|
+
const drawn = value;
|
|
1053
|
+
return [{
|
|
1054
|
+
type: "text",
|
|
1055
|
+
text: summary(drawn)
|
|
1056
|
+
}, ...pictures(drawn.shown)];
|
|
1057
|
+
},
|
|
1058
|
+
presentationMeta: (_args, value) => {
|
|
1059
|
+
const drawn = value;
|
|
1060
|
+
return {
|
|
1061
|
+
model: drawn.model,
|
|
1062
|
+
cost: drawn.cost,
|
|
1063
|
+
files: drawn.files
|
|
1064
|
+
};
|
|
1065
|
+
}
|
|
1066
|
+
},
|
|
1067
|
+
timeoutMs: 3e5,
|
|
1068
|
+
isConcurrencySafe: () => true,
|
|
1069
|
+
presentCall: (args) => ({
|
|
1070
|
+
card: "generic",
|
|
1071
|
+
kind: "other",
|
|
1072
|
+
title: `Рисую: ${args.prompt.length > 80 ? `${args.prompt.slice(0, 79)}…` : args.prompt}`
|
|
1073
|
+
}),
|
|
1074
|
+
presentResult: (_args, result) => ({
|
|
1075
|
+
card: "generic",
|
|
1076
|
+
title: "Картинка RouterAI",
|
|
1077
|
+
content: result.content.filter((block) => block.type === "image" || block.type === "text")
|
|
1078
|
+
}),
|
|
1079
|
+
execute: async (args, run) => {
|
|
1080
|
+
const model = deps.imageModel();
|
|
1081
|
+
if (model === null) throw new Error("Рисование картинок выключено в Настройках → RouterAI.");
|
|
1082
|
+
const key = await deps.readKey();
|
|
1083
|
+
if (key === void 0) throw new Error("Нет ключа RouterAI. Войдите: Настройки → RouterAI или команда /routerai login.");
|
|
1084
|
+
const count = Math.min(MOST_PICTURES, Math.max(1, args.count ?? 1));
|
|
1085
|
+
const result = await drawPictures(deps.apiBase, key, {
|
|
1086
|
+
model,
|
|
1087
|
+
prompt: args.prompt,
|
|
1088
|
+
count,
|
|
1089
|
+
...args.aspect_ratio === void 0 ? {} : { aspectRatio: args.aspect_ratio }
|
|
1090
|
+
}, run.signal);
|
|
1091
|
+
const stem = fileStem(args.file_name, args.prompt);
|
|
1092
|
+
const names = result.pictures.map((picture, index) => `${stem}${result.pictures.length > 1 ? `-${index + 1}` : ""}.${EXTENSIONS[picture.type]}`);
|
|
1093
|
+
const files = [];
|
|
1094
|
+
const workingDirectory = ctx.get("workingDirectory");
|
|
1095
|
+
if (workingDirectory !== void 0 && run.agent !== void 0) {
|
|
1096
|
+
const folder = join(await workingDirectory.ensure(run.agent, run.signal), MEDIA_FOLDER);
|
|
1097
|
+
await mkdir(folder, { recursive: true });
|
|
1098
|
+
for (const [index, picture] of result.pictures.entries()) {
|
|
1099
|
+
const path = join(folder, names[index]);
|
|
1100
|
+
await writeFile(path, picture.bytes);
|
|
1101
|
+
files.push(path);
|
|
1102
|
+
}
|
|
1103
|
+
}
|
|
1104
|
+
const shown = [];
|
|
1105
|
+
const attachments = deps.attachments();
|
|
1106
|
+
if (attachments !== void 0) for (const [index, picture] of result.pictures.entries()) shown.push(await attachments.saveImage({
|
|
1107
|
+
data: picture.bytes,
|
|
1108
|
+
mediaType: picture.type,
|
|
1109
|
+
name: names[index]
|
|
1110
|
+
}));
|
|
1111
|
+
if (result.cost !== void 0 && run.agent !== void 0) deps.charged(String(run.agent.id), model, result.cost);
|
|
1112
|
+
return {
|
|
1113
|
+
model,
|
|
1114
|
+
cost: result.cost ?? null,
|
|
1115
|
+
files,
|
|
1116
|
+
shown
|
|
1117
|
+
};
|
|
1118
|
+
}
|
|
1119
|
+
});
|
|
1120
|
+
ctx.effect(() => ctx.tools.register(tool, { when: () => deps.imageModel() !== null }), "routerai: draw tool");
|
|
1121
|
+
}
|
|
838
1122
|
const INITIAL = {
|
|
839
1123
|
utilityModel: "deepseek/deepseek-v4.1-flash",
|
|
840
|
-
lowBalance: 100
|
|
1124
|
+
lowBalance: 100,
|
|
1125
|
+
imageModel: "bytedance-seed/seedream-5-5",
|
|
1126
|
+
speechModel: "openai/whisper-large-v3-turbo"
|
|
841
1127
|
};
|
|
842
1128
|
/** Привести список моделей к каноническому виду или объяснить, что с ним не так. */
|
|
843
1129
|
function cleanModelList(value) {
|
|
@@ -859,6 +1145,8 @@ function fromDisk(raw) {
|
|
|
859
1145
|
} catch {}
|
|
860
1146
|
if (stored["utilityModel"] === null || isModelSlug(stored["utilityModel"])) result.utilityModel = stored["utilityModel"];
|
|
861
1147
|
if (typeof stored["lowBalance"] === "number" && stored["lowBalance"] >= 0) result.lowBalance = stored["lowBalance"];
|
|
1148
|
+
if (stored["imageModel"] === null || isModelSlug(stored["imageModel"])) result.imageModel = stored["imageModel"];
|
|
1149
|
+
if (isModelSlug(stored["speechModel"])) result.speechModel = stored["speechModel"];
|
|
862
1150
|
return result;
|
|
863
1151
|
}
|
|
864
1152
|
var PreferenceStore = class {
|
|
@@ -906,6 +1194,16 @@ var PreferenceStore = class {
|
|
|
906
1194
|
if (typeof threshold !== "number" || !Number.isFinite(threshold) || threshold < 0) throw new Error("Порог баланса: ожидалось неотрицательное число");
|
|
907
1195
|
next.lowBalance = threshold;
|
|
908
1196
|
}
|
|
1197
|
+
if ("imageModel" in patch) {
|
|
1198
|
+
const model = patch["imageModel"];
|
|
1199
|
+
if (model !== null && !isModelSlug(model)) throw new Error("Модель для картинок: ожидался идентификатор vendor/model или null");
|
|
1200
|
+
next.imageModel = model;
|
|
1201
|
+
}
|
|
1202
|
+
if ("speechModel" in patch) {
|
|
1203
|
+
const model = patch["speechModel"];
|
|
1204
|
+
if (!isModelSlug(model)) throw new Error("Модель распознавания речи: ожидался идентификатор vendor/model");
|
|
1205
|
+
next.speechModel = model;
|
|
1206
|
+
}
|
|
909
1207
|
await this.file.write(next);
|
|
910
1208
|
this.value = next;
|
|
911
1209
|
this.notify();
|
|
@@ -976,7 +1274,8 @@ function describeUnknownModel(slug, apiBase) {
|
|
|
976
1274
|
alias: slug.startsWith("~"),
|
|
977
1275
|
tools: true,
|
|
978
1276
|
reasoning: false,
|
|
979
|
-
vision: false
|
|
1277
|
+
vision: false,
|
|
1278
|
+
makes: ["text"]
|
|
980
1279
|
}, apiBase);
|
|
981
1280
|
}
|
|
982
1281
|
/**
|
|
@@ -1103,7 +1402,7 @@ function registerSlashCommand(ctx, deps) {
|
|
|
1103
1402
|
case "find": {
|
|
1104
1403
|
await deps.catalog.sync();
|
|
1105
1404
|
const needle = argument.toLowerCase();
|
|
1106
|
-
const hits = deps.catalog.all().filter((card) => `${card.slug} ${card.maker} ${card.title}`.toLowerCase().includes(needle));
|
|
1405
|
+
const hits = deps.catalog.all().filter(isChatModel).filter((card) => `${card.slug} ${card.maker} ${card.title}`.toLowerCase().includes(needle));
|
|
1107
1406
|
if (hits.length === 0) return bad(`По запросу «${argument}» в каталоге RouterAI ничего нет.`);
|
|
1108
1407
|
const shown = hits.slice(0, 30).map(modelLine);
|
|
1109
1408
|
if (hits.length > shown.length) shown.push(`…и ещё ${hits.length - shown.length}. Уточните запрос.`);
|
|
@@ -1130,6 +1429,55 @@ function registerSlashCommand(ctx, deps) {
|
|
|
1130
1429
|
}
|
|
1131
1430
|
});
|
|
1132
1431
|
}
|
|
1432
|
+
// src/speech.ts
|
|
1433
|
+
const SPEECH_PROVIDER = "routerai";
|
|
1434
|
+
/** Языки, которые предлагает выбор в интерфейсе; `auto` оставляет определение языка модели. */
|
|
1435
|
+
const LANGUAGES = [
|
|
1436
|
+
"auto",
|
|
1437
|
+
"ru",
|
|
1438
|
+
"en",
|
|
1439
|
+
"de",
|
|
1440
|
+
"fr",
|
|
1441
|
+
"es",
|
|
1442
|
+
"it",
|
|
1443
|
+
"pt",
|
|
1444
|
+
"tr",
|
|
1445
|
+
"uk",
|
|
1446
|
+
"kk",
|
|
1447
|
+
"zh",
|
|
1448
|
+
"ja",
|
|
1449
|
+
"ko"
|
|
1450
|
+
];
|
|
1451
|
+
function registerSpeechProvider(ctx, deps) {
|
|
1452
|
+
ctx.effect(() => {
|
|
1453
|
+
const unregister = ctx.speechToText.register({
|
|
1454
|
+
info: {
|
|
1455
|
+
id: SPEECH_PROVIDER,
|
|
1456
|
+
name: "RouterAI",
|
|
1457
|
+
location: "cloud",
|
|
1458
|
+
languages: LANGUAGES
|
|
1459
|
+
},
|
|
1460
|
+
transcribe: async (input, signal) => {
|
|
1461
|
+
const key = await deps.readKey();
|
|
1462
|
+
if (key === void 0) throw new Error("Нет ключа RouterAI. Войдите: Настройки → RouterAI или команда /routerai login.");
|
|
1463
|
+
const started = Date.now();
|
|
1464
|
+
const heard = await recognizeSpeech(deps.apiBase, key, {
|
|
1465
|
+
model: deps.speechModel(),
|
|
1466
|
+
wav: input.audio,
|
|
1467
|
+
...input.language === "auto" || input.language === "" ? {} : { language: input.language }
|
|
1468
|
+
}, signal);
|
|
1469
|
+
return {
|
|
1470
|
+
text: heard.text,
|
|
1471
|
+
audioSeconds: heard.seconds ?? wavSeconds(input.audio),
|
|
1472
|
+
inferenceSeconds: (Date.now() - started) / 1e3
|
|
1473
|
+
};
|
|
1474
|
+
}
|
|
1475
|
+
});
|
|
1476
|
+
return () => {
|
|
1477
|
+
unregister();
|
|
1478
|
+
};
|
|
1479
|
+
}, "routerai: speech provider");
|
|
1480
|
+
}
|
|
1133
1481
|
/** Что видит в пикере новый пользователь. */
|
|
1134
1482
|
const STARTER_MODELS = [
|
|
1135
1483
|
{
|
|
@@ -1165,7 +1513,8 @@ const STARTER_MODELS = [
|
|
|
1165
1513
|
*
|
|
1166
1514
|
* Плагин регистрирует маршрут `routerai`: запросы уходят в OpenAI-совместимый API RouterAI
|
|
1167
1515
|
* через штатный адаптер pi-ai. Сверх этого плагин следит за каталогом моделей и ценами,
|
|
1168
|
-
* помогает получить ключ через браузер, показывает баланс и считает стоимость
|
|
1516
|
+
* помогает получить ключ через браузер, показывает баланс и считает стоимость сессий,
|
|
1517
|
+
* даёт агенту инструмент для рисования картинок и распознаёт речь для голосового ввода.
|
|
1169
1518
|
*
|
|
1170
1519
|
* Настройка из файла (обычно всё делается в Настройках → RouterAI):
|
|
1171
1520
|
*
|
|
@@ -1231,11 +1580,11 @@ function apply(ctx, config) {
|
|
|
1231
1580
|
const models = [];
|
|
1232
1581
|
for (const slug of wanted) {
|
|
1233
1582
|
const card = catalog.find(slug);
|
|
1234
|
-
if (card !== void 0) models.push(describeModel(card, apiBase));
|
|
1583
|
+
if (card !== void 0 && isChatModel(card)) models.push(describeModel(card, apiBase));
|
|
1235
1584
|
else if (catalog.size === 0) models.push(describeUnknownModel(slug, apiBase));
|
|
1236
1585
|
else if (!warnedAbout.has(slug)) {
|
|
1237
1586
|
warnedAbout.add(slug);
|
|
1238
|
-
ctx.logger.warn(`[routerai]
|
|
1587
|
+
ctx.logger.warn(`[routerai] ${slug}: в каталоге RouterAI нет такой чат-модели — в пикер она не попадёт`);
|
|
1239
1588
|
}
|
|
1240
1589
|
}
|
|
1241
1590
|
route = /* @__PURE__ */ new Map([[ROUTE, buildRoute(models, apiBase, credential)]]);
|
|
@@ -1339,6 +1688,24 @@ function apply(ctx, config) {
|
|
|
1339
1688
|
requestBody: "buffered"
|
|
1340
1689
|
}), `routerai: ${endpoint.path}`);
|
|
1341
1690
|
});
|
|
1691
|
+
ctx.inject(["tools"], (scope) => {
|
|
1692
|
+
registerDrawTool(scope, {
|
|
1693
|
+
apiBase,
|
|
1694
|
+
readKey,
|
|
1695
|
+
imageModel: () => preferences.current.imageModel,
|
|
1696
|
+
attachments: () => optional.attachments,
|
|
1697
|
+
charged: (sessionId, model, rub) => {
|
|
1698
|
+
costs.addCharge(sessionId, model, rub);
|
|
1699
|
+
}
|
|
1700
|
+
});
|
|
1701
|
+
});
|
|
1702
|
+
ctx.inject(["speechToText"], (scope) => {
|
|
1703
|
+
registerSpeechProvider(scope, {
|
|
1704
|
+
apiBase,
|
|
1705
|
+
readKey,
|
|
1706
|
+
speechModel: () => preferences.current.speechModel
|
|
1707
|
+
});
|
|
1708
|
+
});
|
|
1342
1709
|
ctx.inject(["commands"], (scope) => {
|
|
1343
1710
|
registerSlashCommand(scope, {
|
|
1344
1711
|
apiBase,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@router-ai/dsh-plugin-routerai",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "RouterAI — провайдер моделей для DeepSeek Harness: сотни LLM по одному ключу с оплатой в рублях",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -52,17 +52,23 @@
|
|
|
52
52
|
"@deepseek-ai/dsh-attachment": "0.2.2-rc.3",
|
|
53
53
|
"@deepseek-ai/dsh-client-connection": "0.2.2-rc.3",
|
|
54
54
|
"@deepseek-ai/dsh-client-locale": "0.2.2-rc.3",
|
|
55
|
+
"@deepseek-ai/dsh-client-ui-chat": "0.2.2-rc.3",
|
|
56
|
+
"@deepseek-ai/dsh-client-ui-conversation": "0.2.2-rc.3",
|
|
55
57
|
"@deepseek-ai/dsh-client-ui-primitives": "0.2.2-rc.3",
|
|
56
58
|
"@deepseek-ai/dsh-client-ui-renderer": "0.2.2-rc.3",
|
|
57
59
|
"@deepseek-ai/dsh-client-ui-settings": "0.2.2-rc.3",
|
|
58
60
|
"@deepseek-ai/dsh-client-ui-sidebar": "0.2.2-rc.3",
|
|
59
61
|
"@deepseek-ai/dsh-client-ui-slots": "0.2.2-rc.3",
|
|
62
|
+
"@deepseek-ai/dsh-client-ui-tool": "0.2.2-rc.3",
|
|
60
63
|
"@deepseek-ai/dsh-commands": "0.2.2-rc.3",
|
|
61
64
|
"@deepseek-ai/dsh-credentials": "0.2.2-rc.3",
|
|
65
|
+
"@deepseek-ai/dsh-experimental-speech-to-text": "0.2.2-rc.3",
|
|
62
66
|
"@deepseek-ai/dsh-fs": "0.2.2-rc.3",
|
|
63
67
|
"@deepseek-ai/dsh-launch-environment": "0.2.2-rc.3",
|
|
64
68
|
"@deepseek-ai/dsh-llm": "0.2.2-rc.3",
|
|
65
69
|
"@deepseek-ai/dsh-llm-pi-ai": "0.2.2-rc.3",
|
|
70
|
+
"@deepseek-ai/dsh-tools": "0.2.2-rc.3",
|
|
71
|
+
"@deepseek-ai/dsh-working-directory": "0.2.2-rc.3",
|
|
66
72
|
"@deepseek-ai/schemastery": "3.19.0",
|
|
67
73
|
"@earendil-works/pi-ai": "1.3.0",
|
|
68
74
|
"@types/node": "24.19.2",
|
|
@@ -78,10 +84,13 @@
|
|
|
78
84
|
"@deepseek-ai/dsh-attachment": ">=0.2.2-rc.1 <0.3.0",
|
|
79
85
|
"@deepseek-ai/dsh-commands": ">=0.2.2-rc.1 <0.3.0",
|
|
80
86
|
"@deepseek-ai/dsh-credentials": ">=0.2.2-rc.1 <0.3.0",
|
|
87
|
+
"@deepseek-ai/dsh-experimental-speech-to-text": ">=0.2.2-rc.1 <0.3.0",
|
|
81
88
|
"@deepseek-ai/dsh-fs": ">=0.2.2-rc.1 <0.3.0",
|
|
82
89
|
"@deepseek-ai/dsh-launch-environment": ">=0.2.2-rc.1 <0.3.0",
|
|
83
90
|
"@deepseek-ai/dsh-llm": ">=0.2.2-rc.1 <0.3.0",
|
|
84
91
|
"@deepseek-ai/dsh-llm-pi-ai": ">=0.2.2-rc.1 <0.3.0",
|
|
92
|
+
"@deepseek-ai/dsh-tools": ">=0.2.2-rc.1 <0.3.0",
|
|
93
|
+
"@deepseek-ai/dsh-working-directory": ">=0.2.2-rc.1 <0.3.0",
|
|
85
94
|
"@deepseek-ai/schemastery": "~3.19.0",
|
|
86
95
|
"@earendil-works/pi-ai": "^1.3.0"
|
|
87
96
|
},
|
|
@@ -97,6 +106,15 @@
|
|
|
97
106
|
},
|
|
98
107
|
"@deepseek-ai/dsh-commands": {
|
|
99
108
|
"optional": true
|
|
109
|
+
},
|
|
110
|
+
"@deepseek-ai/dsh-tools": {
|
|
111
|
+
"optional": true
|
|
112
|
+
},
|
|
113
|
+
"@deepseek-ai/dsh-working-directory": {
|
|
114
|
+
"optional": true
|
|
115
|
+
},
|
|
116
|
+
"@deepseek-ai/dsh-experimental-speech-to-text": {
|
|
117
|
+
"optional": true
|
|
100
118
|
}
|
|
101
119
|
},
|
|
102
120
|
"repository": {
|