@tyapinalexandr/dsh-account-identity-fallback 0.2.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 +21 -0
- package/README.md +100 -0
- package/client.js +343 -0
- package/cordis.patch.yml +21 -0
- package/icon.svg +8 -0
- package/index.js +264 -0
- package/locale/en.json +6 -0
- package/locale/ru.json +6 -0
- package/package.json +52 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 tyapinalexandr
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
# @tyapinalexandr/dsh-account-identity-fallback
|
|
2
|
+
|
|
3
|
+
Keeps the DeepSeek account name and avatar visible when the Platform profile
|
|
4
|
+
endpoint is unavailable, with an in-app editor for both.
|
|
5
|
+
|
|
6
|
+
## What it does
|
|
7
|
+
|
|
8
|
+
The account UI shows the generic avatar and "Signed in to DeepSeek" when
|
|
9
|
+
`platform.deepseek.com/auth-api/v0/users/current` is blocked (AWS WAF
|
|
10
|
+
challenge / CloudFront 403). The plugin wraps the `deepseekAccount` service: a
|
|
11
|
+
successful profile read is cached, and a failed read is answered from that
|
|
12
|
+
cache or from the identity saved in the plugin settings. It does not bypass bot
|
|
13
|
+
protection; balance, sign-in/out and model calls are untouched.
|
|
14
|
+
|
|
15
|
+
## Install
|
|
16
|
+
|
|
17
|
+
Plugins → Add plugin → `@tyapinalexandr/dsh-account-identity-fallback` →
|
|
18
|
+
Install → Enable now.
|
|
19
|
+
|
|
20
|
+
```sh
|
|
21
|
+
dsh plugin --profile <profile> add @tyapinalexandr/dsh-account-identity-fallback
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
After installing or updating, reload the Harness page once so the browser half
|
|
25
|
+
loads.
|
|
26
|
+
|
|
27
|
+
## Configure
|
|
28
|
+
|
|
29
|
+
Plugins → Installed plugins → the plugin's page: set **Account name** and
|
|
30
|
+
**Avatar** (image URL or a local file up to 512 KB, stored in the profile
|
|
31
|
+
configuration), then **Save**. Saved fields apply immediately without a restart
|
|
32
|
+
and take priority over the cached profile; empty fields use the cached value.
|
|
33
|
+
|
|
34
|
+
The same two fields can be set in the profile's `cordis.patch.yml` (`name`,
|
|
35
|
+
`avatarUrl`).
|
|
36
|
+
|
|
37
|
+
## Cache
|
|
38
|
+
|
|
39
|
+
`$DSH_HOME/cache/account-identity-fallback/profile-cache.json`
|
|
40
|
+
(`~/.dsh/cache/...` by default). Delete it to fall back to the configured
|
|
41
|
+
identity.
|
|
42
|
+
|
|
43
|
+
## Uninstall
|
|
44
|
+
|
|
45
|
+
Plugins → Remove, or:
|
|
46
|
+
|
|
47
|
+
```sh
|
|
48
|
+
dsh plugin --profile <profile> remove @tyapinalexandr/dsh-account-identity-fallback
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
---
|
|
52
|
+
|
|
53
|
+
## Русский
|
|
54
|
+
|
|
55
|
+
Показывает имя и аватар аккаунта DeepSeek, когда платформа не отдаёт профиль,
|
|
56
|
+
и позволяет задать их в интерфейсе DSH.
|
|
57
|
+
|
|
58
|
+
### Что делает
|
|
59
|
+
|
|
60
|
+
Аккаунт показывает стандартную иконку и «Signed in to DeepSeek», когда
|
|
61
|
+
`platform.deepseek.com/auth-api/v0/users/current` блокируется (AWS WAF challenge
|
|
62
|
+
/ CloudFront 403). Плагин оборачивает сервис `deepseekAccount`: успешное чтение
|
|
63
|
+
профиля кешируется, неуспешное — отвечает из кеша или из настроек плагина.
|
|
64
|
+
Антибот-защиту он не обходит; баланс, вход/выход и вызовы моделей не
|
|
65
|
+
затрагиваются.
|
|
66
|
+
|
|
67
|
+
### Установка
|
|
68
|
+
|
|
69
|
+
**Plugins → Add plugin** → `@tyapinalexandr/dsh-account-identity-fallback` →
|
|
70
|
+
**Install** → **Enable now**.
|
|
71
|
+
|
|
72
|
+
```sh
|
|
73
|
+
dsh plugin --profile <профиль> add @tyapinalexandr/dsh-account-identity-fallback
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
После установки или обновления один раз перезагрузите страницу Harness, чтобы
|
|
77
|
+
загрузилась браузерная часть.
|
|
78
|
+
|
|
79
|
+
### Настройка
|
|
80
|
+
|
|
81
|
+
**Plugins → Installed plugins** → страница плагина: задайте **Имя в аккаунте**
|
|
82
|
+
и **Аватар** (ссылка или локальный файл до 512 КБ, сохраняется в конфигурации
|
|
83
|
+
профиля), затем **Сохранить**. Сохранённые поля применяются сразу, без
|
|
84
|
+
перезапуска, и имеют приоритет над кешем; пустые поля используют кеш.
|
|
85
|
+
|
|
86
|
+
Те же два поля можно задать в `cordis.patch.yml` профиля (`name`,
|
|
87
|
+
`avatarUrl`).
|
|
88
|
+
|
|
89
|
+
### Кеш
|
|
90
|
+
|
|
91
|
+
`$DSH_HOME/cache/account-identity-fallback/profile-cache.json` (по умолчанию
|
|
92
|
+
`~/.dsh/cache/...`). Удалите файл, чтобы вернуться к значениям из настроек.
|
|
93
|
+
|
|
94
|
+
### Удаление
|
|
95
|
+
|
|
96
|
+
**Plugins → Remove** или:
|
|
97
|
+
|
|
98
|
+
```sh
|
|
99
|
+
dsh plugin --profile <профиль> remove @tyapinalexandr/dsh-account-identity-fallback
|
|
100
|
+
```
|
package/client.js
ADDED
|
@@ -0,0 +1,343 @@
|
|
|
1
|
+
// Browser half of @tyapinalexandr/dsh-account-identity-fallback.
|
|
2
|
+
//
|
|
3
|
+
// Registers a row configuration page on the Plugins page: Plugins → this
|
|
4
|
+
// bundle → the "account-identity-fallback" row → Configure. The page edits the
|
|
5
|
+
// plugin's volatile identity fields, so a save is applied to the running Host
|
|
6
|
+
// plugin without a restart and the account surfaces refresh.
|
|
7
|
+
window.__ModuleLoader__.load({
|
|
8
|
+
id: "@tyapinalexandr/dsh-account-identity-fallback",
|
|
9
|
+
factory: (require) => {
|
|
10
|
+
var module = { exports: {} };
|
|
11
|
+
var exports = module.exports;
|
|
12
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
13
|
+
const React = require("react");
|
|
14
|
+
const h = React.createElement;
|
|
15
|
+
|
|
16
|
+
const NS = "accountIdentityFallback";
|
|
17
|
+
const PACKAGE = "@tyapinalexandr/dsh-account-identity-fallback";
|
|
18
|
+
const ROW_ID = "account-identity-fallback";
|
|
19
|
+
const FIELD_NAMES = ["name", "avatarUrl"];
|
|
20
|
+
const MAX_AVATAR_BYTES = 512 * 1024;
|
|
21
|
+
|
|
22
|
+
const en = {
|
|
23
|
+
summary: "Set the account name and avatar shown when the Platform profile is unavailable.",
|
|
24
|
+
title: "Account identity fallback",
|
|
25
|
+
loading: "Loading settings…",
|
|
26
|
+
unavailable: "Configuration is not available for this row.",
|
|
27
|
+
readOnly: "This profile does not accept configuration writes.",
|
|
28
|
+
nameLabel: "Account name",
|
|
29
|
+
nameHint: "Shown in the account menu and Settings. Leave empty to use the last known Platform profile.",
|
|
30
|
+
avatarLabel: "Avatar",
|
|
31
|
+
avatarHint: "Paste an image URL, or choose a local image file (up to 512 KB; it is embedded in the profile configuration). Leave empty to use the last known Platform avatar.",
|
|
32
|
+
avatarChoose: "Choose image…",
|
|
33
|
+
replaceImage: "Replace image…",
|
|
34
|
+
useLink: "Use a link instead",
|
|
35
|
+
uploadedImage: "Uploaded image",
|
|
36
|
+
avatarClear: "Remove image",
|
|
37
|
+
save: "Save",
|
|
38
|
+
saving: "Saving…",
|
|
39
|
+
saved: "Saved",
|
|
40
|
+
noChanges: "Nothing to save",
|
|
41
|
+
failed: "Could not save the settings",
|
|
42
|
+
notImage: "Choose an image file",
|
|
43
|
+
tooLarge: "The image is larger than 512 KB",
|
|
44
|
+
readFailed: "Could not read the image file",
|
|
45
|
+
cacheHint: "Empty fields fall back to the last known Platform profile, then to the built-in account label.",
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
const ru = {
|
|
49
|
+
summary: "Имя и аватар аккаунта, когда профиль платформы недоступен.",
|
|
50
|
+
title: "Имя и аватар аккаунта",
|
|
51
|
+
loading: "Загрузка настроек…",
|
|
52
|
+
unavailable: "Настройки для этой строки недоступны.",
|
|
53
|
+
readOnly: "Этот профиль не принимает записи настроек.",
|
|
54
|
+
nameLabel: "Имя в аккаунте",
|
|
55
|
+
nameHint: "Показывается в меню аккаунта и настройках. Пусто — использовать последний известный профиль платформы.",
|
|
56
|
+
avatarLabel: "Аватар",
|
|
57
|
+
avatarHint: "Вставьте ссылку на изображение или выберите файл (до 512 КБ; он сохраняется внутри конфигурации профиля). Пусто — использовать последний известный аватар платформы.",
|
|
58
|
+
avatarChoose: "Выбрать изображение…",
|
|
59
|
+
replaceImage: "Заменить изображение…",
|
|
60
|
+
useLink: "Вместо файла — ссылка",
|
|
61
|
+
uploadedImage: "Загруженное изображение",
|
|
62
|
+
avatarClear: "Убрать изображение",
|
|
63
|
+
save: "Сохранить",
|
|
64
|
+
saving: "Сохранение…",
|
|
65
|
+
saved: "Сохранено",
|
|
66
|
+
noChanges: "Нечего сохранять",
|
|
67
|
+
failed: "Не удалось сохранить настройки",
|
|
68
|
+
notImage: "Выберите файл изображения",
|
|
69
|
+
tooLarge: "Изображение больше 512 КБ",
|
|
70
|
+
readFailed: "Не удалось прочитать файл изображения",
|
|
71
|
+
cacheHint: "Пустые поля берут последний известный профиль платформы, а затем встроенную подпись аккаунта.",
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
const styles = {
|
|
75
|
+
root: { display: "flex", flexDirection: "column", gap: "14px", padding: "4px 0", fontSize: "13px", lineHeight: "20px" },
|
|
76
|
+
grid: { display: "flex", flexDirection: "column", gap: "12px" },
|
|
77
|
+
row: { display: "flex", flexDirection: "column", gap: "4px" },
|
|
78
|
+
label: { color: "var(--dsw-alias-label-primary)", fontWeight: 500 },
|
|
79
|
+
hint: { color: "var(--dsw-alias-label-tertiary)", fontSize: "12px", lineHeight: "18px" },
|
|
80
|
+
input: {
|
|
81
|
+
boxSizing: "border-box", width: "100%", height: "32px", padding: "0 10px",
|
|
82
|
+
color: "var(--dsw-alias-label-primary)", background: "var(--dsw-alias-bg-base)",
|
|
83
|
+
border: "0.5px solid var(--dsw-alias-border-l2)", borderRadius: "var(--dsw-radius-md)", font: "inherit",
|
|
84
|
+
},
|
|
85
|
+
avatarRow: { display: "flex", gap: "10px", alignItems: "center", flexWrap: "wrap" },
|
|
86
|
+
avatar: {
|
|
87
|
+
width: "48px", height: "48px", borderRadius: "50%", objectFit: "cover",
|
|
88
|
+
background: "var(--dsw-alias-bg-skeleton)", flex: "none",
|
|
89
|
+
},
|
|
90
|
+
uploaded: {
|
|
91
|
+
display: "inline-flex", alignItems: "center", gap: "8px", minHeight: "32px", padding: "0 10px",
|
|
92
|
+
color: "var(--dsw-alias-label-secondary)", background: "var(--dsw-alias-bg-skeleton)",
|
|
93
|
+
border: "0.5px solid var(--dsw-alias-border-l2)", borderRadius: "var(--dsw-radius-md)",
|
|
94
|
+
},
|
|
95
|
+
uploadedSize: { color: "var(--dsw-alias-label-tertiary)", fontSize: "12px" },
|
|
96
|
+
actions: { display: "flex", gap: "8px", alignItems: "center", flexWrap: "wrap" },
|
|
97
|
+
button: {
|
|
98
|
+
height: "32px", padding: "0 14px", borderRadius: "var(--dsw-radius-md)",
|
|
99
|
+
border: "0.5px solid var(--dsw-alias-border-l3)", background: "transparent",
|
|
100
|
+
color: "var(--dsw-alias-label-primary)", font: "inherit", cursor: "pointer",
|
|
101
|
+
},
|
|
102
|
+
primary: {
|
|
103
|
+
background: "var(--dsw-alias-button-primary-fill)", borderColor: "transparent",
|
|
104
|
+
color: "var(--dsw-alias-label-primary-foreground)", fontWeight: 500,
|
|
105
|
+
},
|
|
106
|
+
ok: { color: "var(--dsw-alias-label-tertiary)" },
|
|
107
|
+
fail: { color: "var(--dsw-alias-state-error-primary, #e5484d)" },
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
/** Read the form's current identity values into a plain draft object. */
|
|
111
|
+
function draftFrom(snapshot) {
|
|
112
|
+
const value = snapshot && snapshot.value && typeof snapshot.value === "object" ? snapshot.value : {};
|
|
113
|
+
const draft = {};
|
|
114
|
+
for (const field of FIELD_NAMES) {
|
|
115
|
+
draft[field] = typeof value[field] === "string" ? value[field] : "";
|
|
116
|
+
}
|
|
117
|
+
return draft;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** Whether a value is an embedded image URL. */
|
|
121
|
+
function isDataUrl(value) {
|
|
122
|
+
return typeof value === "string" && value.startsWith("data:");
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** Approximate decoded size in KB of a data: URL. */
|
|
126
|
+
function dataUrlSizeKb(value) {
|
|
127
|
+
const comma = value.indexOf(",");
|
|
128
|
+
const payload = comma === -1 ? "" : value.slice(comma + 1);
|
|
129
|
+
const padding = payload.endsWith("==") ? 2 : payload.endsWith("=") ? 1 : 0;
|
|
130
|
+
return Math.max(1, Math.round((payload.length * 3) / 4 - padding) / 1024);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** Render one label + control + hint block. */
|
|
134
|
+
function fieldBlock(label, hint, control) {
|
|
135
|
+
return h("label", { style: styles.row, key: label }, [
|
|
136
|
+
h("span", { style: styles.label, key: "label" }, label),
|
|
137
|
+
control,
|
|
138
|
+
hint ? h("span", { style: styles.hint, key: "hint" }, hint) : null,
|
|
139
|
+
]);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/** Configuration page rendered on the plugin's page in Plugins. */
|
|
143
|
+
function ConfigPage(props) {
|
|
144
|
+
const t = typeof props.t === "function" ? props.t : (key) => en[key] ?? key;
|
|
145
|
+
const form = props.form;
|
|
146
|
+
const [snapshot, setSnapshot] = React.useState(
|
|
147
|
+
() => (form !== undefined && typeof form.getSnapshot === "function" ? form.getSnapshot() : undefined),
|
|
148
|
+
);
|
|
149
|
+
const [draft, setDraft] = React.useState(() => draftFrom(snapshot));
|
|
150
|
+
const [saving, setSaving] = React.useState(false);
|
|
151
|
+
const [notice, setNotice] = React.useState(null);
|
|
152
|
+
const fileRef = React.useRef(null);
|
|
153
|
+
const revision = snapshot ? snapshot.revision : undefined;
|
|
154
|
+
|
|
155
|
+
React.useEffect(() => {
|
|
156
|
+
if (form === undefined || typeof form.getSnapshot !== "function" || typeof form.subscribe !== "function") return undefined;
|
|
157
|
+
const publish = () => setSnapshot(form.getSnapshot());
|
|
158
|
+
publish();
|
|
159
|
+
return form.subscribe(publish);
|
|
160
|
+
}, [form]);
|
|
161
|
+
|
|
162
|
+
React.useEffect(() => {
|
|
163
|
+
setDraft(draftFrom(snapshot));
|
|
164
|
+
setNotice(null);
|
|
165
|
+
}, [revision]);
|
|
166
|
+
|
|
167
|
+
if (props.view === "summary") return h("span", null, t("summary"));
|
|
168
|
+
if (form === undefined || snapshot === undefined) return h("p", { style: styles.hint }, t("unavailable"));
|
|
169
|
+
if (snapshot.status === "loading") return h("p", { style: styles.hint }, t("loading"));
|
|
170
|
+
if (snapshot.status === "unavailable") return h("p", { style: styles.hint }, t("unavailable"));
|
|
171
|
+
const writable = snapshot.writable === true;
|
|
172
|
+
|
|
173
|
+
const setField = (field) => (event) => {
|
|
174
|
+
const value = event.target.value;
|
|
175
|
+
setDraft((previous) => ({ ...previous, [field]: value }));
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
const chooseFile = () => {
|
|
179
|
+
if (fileRef.current !== null) fileRef.current.click();
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
const readFile = (event) => {
|
|
183
|
+
const file = event.target.files && event.target.files[0];
|
|
184
|
+
event.target.value = "";
|
|
185
|
+
if (file === undefined) return;
|
|
186
|
+
if (typeof file.type === "string" && file.type !== "" && !file.type.startsWith("image/")) {
|
|
187
|
+
setNotice({ ok: false, text: t("notImage") });
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
if (file.size > MAX_AVATAR_BYTES) {
|
|
191
|
+
setNotice({ ok: false, text: t("tooLarge") });
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
const reader = new FileReader();
|
|
195
|
+
reader.onload = () => {
|
|
196
|
+
setDraft((previous) => ({ ...previous, avatarUrl: String(reader.result || "") }));
|
|
197
|
+
setNotice(null);
|
|
198
|
+
};
|
|
199
|
+
reader.onerror = () => setNotice({ ok: false, text: t("readFailed") });
|
|
200
|
+
reader.readAsDataURL(file);
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
const save = async () => {
|
|
204
|
+
if (saving || !writable) return;
|
|
205
|
+
setSaving(true);
|
|
206
|
+
setNotice(null);
|
|
207
|
+
try {
|
|
208
|
+
const before = draftFrom(snapshot);
|
|
209
|
+
const ops = [];
|
|
210
|
+
for (const field of FIELD_NAMES) {
|
|
211
|
+
const next = String(draft[field] ?? "").trim();
|
|
212
|
+
if (next === before[field]) continue;
|
|
213
|
+
if (next === "") ops.push({ op: "unset", path: [field] });
|
|
214
|
+
else ops.push({ op: "set", path: [field], value: next });
|
|
215
|
+
}
|
|
216
|
+
if (ops.length === 0) {
|
|
217
|
+
setNotice({ ok: true, text: t("noChanges") });
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
const accepted = await form.mutate(ops, snapshot.revision);
|
|
221
|
+
setNotice({ ok: accepted, text: t(accepted ? "saved" : "failed") });
|
|
222
|
+
} catch (_error) {
|
|
223
|
+
setNotice({ ok: false, text: t("failed") });
|
|
224
|
+
} finally {
|
|
225
|
+
setSaving(false);
|
|
226
|
+
}
|
|
227
|
+
};
|
|
228
|
+
|
|
229
|
+
const avatarPreview = draft.avatarUrl === ""
|
|
230
|
+
? null
|
|
231
|
+
: h("img", {
|
|
232
|
+
key: "preview",
|
|
233
|
+
style: styles.avatar,
|
|
234
|
+
src: draft.avatarUrl,
|
|
235
|
+
alt: t("avatarLabel"),
|
|
236
|
+
referrerPolicy: "no-referrer",
|
|
237
|
+
onError: (event) => { event.currentTarget.style.visibility = "hidden"; },
|
|
238
|
+
onLoad: (event) => { event.currentTarget.style.visibility = "visible"; },
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
return h("div", { style: styles.root, "data-account-identity-fallback": "" }, [
|
|
242
|
+
!writable ? h("p", { style: styles.hint, key: "readonly" }, t("readOnly")) : null,
|
|
243
|
+
h("div", { style: styles.grid, key: "grid" }, [
|
|
244
|
+
fieldBlock(t("nameLabel"), t("nameHint"), h("input", {
|
|
245
|
+
key: "name",
|
|
246
|
+
style: styles.input,
|
|
247
|
+
type: "text",
|
|
248
|
+
value: draft.name,
|
|
249
|
+
disabled: !writable || saving,
|
|
250
|
+
onChange: setField("name"),
|
|
251
|
+
placeholder: t("nameLabel"),
|
|
252
|
+
})),
|
|
253
|
+
fieldBlock(t("avatarLabel"), t("avatarHint"), h("div", { style: styles.avatarRow, key: "avatar" }, [
|
|
254
|
+
avatarPreview,
|
|
255
|
+
isDataUrl(draft.avatarUrl)
|
|
256
|
+
? h("span", { key: "uploaded", style: styles.uploaded }, [
|
|
257
|
+
h("span", { key: "label" }, t("uploadedImage")),
|
|
258
|
+
h("span", { key: "size", style: styles.uploadedSize }, `${dataUrlSizeKb(draft.avatarUrl)} KB`),
|
|
259
|
+
])
|
|
260
|
+
: h("input", {
|
|
261
|
+
key: "avatar-url",
|
|
262
|
+
style: { ...styles.input, flex: "1 1 240px" },
|
|
263
|
+
type: "text",
|
|
264
|
+
value: draft.avatarUrl,
|
|
265
|
+
disabled: !writable || saving,
|
|
266
|
+
onChange: setField("avatarUrl"),
|
|
267
|
+
placeholder: "https://…",
|
|
268
|
+
}),
|
|
269
|
+
h("input", {
|
|
270
|
+
key: "file",
|
|
271
|
+
ref: fileRef,
|
|
272
|
+
type: "file",
|
|
273
|
+
accept: "image/*",
|
|
274
|
+
style: { display: "none" },
|
|
275
|
+
onChange: readFile,
|
|
276
|
+
}),
|
|
277
|
+
h("button", {
|
|
278
|
+
key: "choose",
|
|
279
|
+
type: "button",
|
|
280
|
+
style: styles.button,
|
|
281
|
+
disabled: !writable || saving,
|
|
282
|
+
onClick: chooseFile,
|
|
283
|
+
}, t(isDataUrl(draft.avatarUrl) ? "replaceImage" : "avatarChoose")),
|
|
284
|
+
isDataUrl(draft.avatarUrl) ? h("button", {
|
|
285
|
+
key: "use-link",
|
|
286
|
+
type: "button",
|
|
287
|
+
style: styles.button,
|
|
288
|
+
disabled: !writable || saving,
|
|
289
|
+
onClick: () => setDraft((previous) => ({ ...previous, avatarUrl: "" })),
|
|
290
|
+
}, t("useLink")) : null,
|
|
291
|
+
draft.avatarUrl === "" ? null : h("button", {
|
|
292
|
+
key: "clear",
|
|
293
|
+
type: "button",
|
|
294
|
+
style: styles.button,
|
|
295
|
+
disabled: !writable || saving,
|
|
296
|
+
onClick: () => setDraft((previous) => ({ ...previous, avatarUrl: "" })),
|
|
297
|
+
}, t("avatarClear")),
|
|
298
|
+
])),
|
|
299
|
+
]),
|
|
300
|
+
h("div", { style: styles.actions, key: "actions" }, [
|
|
301
|
+
h("button", {
|
|
302
|
+
key: "save",
|
|
303
|
+
type: "button",
|
|
304
|
+
style: { ...styles.button, ...styles.primary },
|
|
305
|
+
disabled: !writable || saving,
|
|
306
|
+
onClick: save,
|
|
307
|
+
}, t(saving ? "saving" : "save")),
|
|
308
|
+
notice === null ? null : h("span", {
|
|
309
|
+
key: "notice",
|
|
310
|
+
style: notice.ok ? styles.ok : styles.fail,
|
|
311
|
+
role: notice.ok ? "status" : "alert",
|
|
312
|
+
}, notice.text),
|
|
313
|
+
]),
|
|
314
|
+
h("p", { style: styles.hint, key: "cache-hint" }, t("cacheHint")),
|
|
315
|
+
]);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/** Required services: slot registry, locale dictionaries and configuration forms. */
|
|
319
|
+
const inject = ["slots", "locale", "configForms"];
|
|
320
|
+
|
|
321
|
+
/**
|
|
322
|
+
* Register the configuration page on the bundle's own page in Plugins.
|
|
323
|
+
* The page declares `plugins.bundle.config` without a form, so this
|
|
324
|
+
* plugin obtains the settings form for its row itself.
|
|
325
|
+
* @param ctx - browser plugin context.
|
|
326
|
+
*/
|
|
327
|
+
function apply(ctx) {
|
|
328
|
+
ctx.effect(() => ctx.locale.register(NS, { en, ru }), "account-identity-fallback: dictionaries");
|
|
329
|
+
const t = ctx.locale.bind(NS);
|
|
330
|
+
const form = ctx.configForms.get(ROW_ID);
|
|
331
|
+
ctx.effect(() => ctx.configForms.whileServed([ROW_ID], () => ctx.slots.inject("plugins.bundle.config", () => ctx.slots.register({
|
|
332
|
+
name: "plugins.bundle.config",
|
|
333
|
+
key: PACKAGE,
|
|
334
|
+
locale: NS,
|
|
335
|
+
inject: () => ({ t, form }),
|
|
336
|
+
}, ConfigPage))), "account-identity-fallback: configuration page");
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
exports.apply = apply;
|
|
340
|
+
exports.inject = inject;
|
|
341
|
+
return module.exports;
|
|
342
|
+
}
|
|
343
|
+
});
|
package/cordis.patch.yml
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# Bundle layer: insert the fallback plugin row.
|
|
2
|
+
#
|
|
3
|
+
# The identity itself (name / avatar / contact) is per-deployment data, so this
|
|
4
|
+
# layer is intentionally empty. Override it in the profile's own
|
|
5
|
+
# cordis.patch.yml, which is applied after every bundle layer:
|
|
6
|
+
#
|
|
7
|
+
# - id: account-identity-fallback
|
|
8
|
+
# config:
|
|
9
|
+
# name: 'Your display name'
|
|
10
|
+
# avatarUrl: 'https://static.deepseek.com/user-avatar/<avatar-id>'
|
|
11
|
+
# contact: ''
|
|
12
|
+
# id: ''
|
|
13
|
+
#
|
|
14
|
+
- insert:
|
|
15
|
+
- id: account-identity-fallback
|
|
16
|
+
name: '@tyapinalexandr/dsh-account-identity-fallback'
|
|
17
|
+
config:
|
|
18
|
+
name: ''
|
|
19
|
+
contact: ''
|
|
20
|
+
avatarUrl: ''
|
|
21
|
+
id: ''
|
package/icon.svg
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 64 64" role="img" aria-label="Account identity fallback">
|
|
2
|
+
<rect width="64" height="64" rx="14" fill="#1f2833"/>
|
|
3
|
+
<circle cx="32" cy="24" r="9" fill="#e8eef7"/>
|
|
4
|
+
<path d="M32 36c-9.4 0-17 5.4-17 12v2h34v-2c0-6.6-7.6-12-17-12z" fill="#e8eef7"/>
|
|
5
|
+
<path d="M47 17a11 11 0 0 0-15.2 2.6" fill="none" stroke="#4f9dff" stroke-width="4" stroke-linecap="round"/>
|
|
6
|
+
<path d="M51.4 12.6 49 20l-7.4-2.2" fill="none" stroke="#4f9dff" stroke-width="4" stroke-linecap="round" stroke-linejoin="round"/>
|
|
7
|
+
<path d="M52 47a11 11 0 0 1-2.6 15.2" fill="none" stroke="#4f9dff" stroke-width="4" stroke-linecap="round" transform="translate(0 0)"/>
|
|
8
|
+
</svg>
|
package/index.js
ADDED
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Account identity fallback for DeepSeek Harness.
|
|
3
|
+
*
|
|
4
|
+
* The DeepSeek Platform profile read (`platform.deepseek.com/auth-api/v0/users/current`)
|
|
5
|
+
* can be rejected by the Platform's edge protection (AWS WAF challenge / CloudFront 403)
|
|
6
|
+
* even while the stored account credential is still valid. In that state the account UI
|
|
7
|
+
* shows the generic avatar and the "Signed in to DeepSeek" label, because the profile
|
|
8
|
+
* payload (name, contact, picture) never arrives.
|
|
9
|
+
*
|
|
10
|
+
* This Host plugin wraps the `deepseekAccount` service so that:
|
|
11
|
+
* - a successful profile read refreshes a small on-disk profile cache;
|
|
12
|
+
* - a failed (or throwing) profile read is answered from that cache, or from the
|
|
13
|
+
* plugin configuration when no cache exists yet.
|
|
14
|
+
*
|
|
15
|
+
* The identity is editable in the Harness UI (Plugins → this bundle → its row →
|
|
16
|
+
* Configure): `name`, `contact` and `avatarUrl` are volatile config fields, so a
|
|
17
|
+
* save reaches the running plugin in place and the account surfaces refresh.
|
|
18
|
+
*
|
|
19
|
+
* The account UI and Settings then render the last known identity instead of the
|
|
20
|
+
* fallback label. Balance, sign-out, sign-in and model usage are untouched.
|
|
21
|
+
*/
|
|
22
|
+
import { mkdir, readFile, rename, writeFile } from 'node:fs/promises'
|
|
23
|
+
import { homedir } from 'node:os'
|
|
24
|
+
import { dirname, join, resolve } from 'node:path'
|
|
25
|
+
import Schema from '@deepseek-ai/schemastery'
|
|
26
|
+
|
|
27
|
+
export const name = 'account-identity-fallback'
|
|
28
|
+
export const inject = ['deepseekAccount']
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Identity fields exposed by the Plugins configuration page. These are volatile
|
|
32
|
+
* fields: the settings service projects them into a form, and a volatile-only
|
|
33
|
+
* edit is committed into the running plugin without restarting it.
|
|
34
|
+
*/
|
|
35
|
+
export const Config = Schema.object({
|
|
36
|
+
name: Schema.string()
|
|
37
|
+
.default('')
|
|
38
|
+
.volatile()
|
|
39
|
+
.description('Account name shown in the account menu and Settings. Empty falls back to the last known profile.'),
|
|
40
|
+
contact: Schema.string()
|
|
41
|
+
.default('')
|
|
42
|
+
.description('Advanced (YAML only): contact line shown in Settings → Account under the name.'),
|
|
43
|
+
avatarUrl: Schema.string()
|
|
44
|
+
.default('')
|
|
45
|
+
.volatile()
|
|
46
|
+
.description('Avatar image URL (https:) or data: URL. Empty falls back to the last known profile.'),
|
|
47
|
+
id: Schema.string()
|
|
48
|
+
.default('')
|
|
49
|
+
.description('Advanced (YAML only): account id used to prefill the Feedback form.'),
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
const LOG_PREFIX = '[account-identity-fallback]'
|
|
53
|
+
const CACHE_VERSION = 1
|
|
54
|
+
/** Account-service instance marker holding the original profile reader. */
|
|
55
|
+
const ORIGINAL_GET_PROFILE = Symbol.for('@tyapinalexandr/dsh-account-identity-fallback/original-get-profile')
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Resolve the Harness data root the same way the harness does: an explicit
|
|
59
|
+
* `$DSH_HOME` wins, and `~/.dsh` is the default. The cache must live outside
|
|
60
|
+
* the installed package so reinstalling or upgrading the plugin keeps it.
|
|
61
|
+
* @returns absolute DSH home directory.
|
|
62
|
+
*/
|
|
63
|
+
function resolveDshHome() {
|
|
64
|
+
const configured = process.env.DSH_HOME
|
|
65
|
+
if (typeof configured === 'string' && configured.trim() !== '') return resolve(configured.trim())
|
|
66
|
+
return join(homedir(), '.dsh')
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** Last-known profile cache under the DSH data root. */
|
|
70
|
+
const CACHE_FILE = join(resolveDshHome(), 'cache', 'account-identity-fallback', 'profile-cache.json')
|
|
71
|
+
|
|
72
|
+
/** Return a non-empty string, or null. */
|
|
73
|
+
function textOrNull(value) {
|
|
74
|
+
return typeof value === 'string' && value.length > 0 ? value : null
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Project an untrusted profile payload onto the account UI's profile shape.
|
|
79
|
+
* @param value - value received from the account service.
|
|
80
|
+
* @returns sanitized profile, or undefined when nothing usable remains.
|
|
81
|
+
*/
|
|
82
|
+
function projectProfile(value) {
|
|
83
|
+
if (value === null || typeof value !== 'object' || Array.isArray(value)) return undefined
|
|
84
|
+
return {
|
|
85
|
+
id: textOrNull(value.id),
|
|
86
|
+
name: textOrNull(value.name),
|
|
87
|
+
contact: textOrNull(value.contact),
|
|
88
|
+
avatarUrl: textOrNull(value.avatarUrl),
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** Whether a profile carries anything the account UI can display. */
|
|
93
|
+
function hasIdentity(profile) {
|
|
94
|
+
return profile !== undefined
|
|
95
|
+
&& (profile.name !== null || profile.contact !== null || profile.avatarUrl !== null)
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Read one volatile configuration reference. Volatile fields arrive as
|
|
100
|
+
* `Volatile` references whose `get()` returns the value committed by the
|
|
101
|
+
* loader; reading at call time therefore sees a save made in the UI.
|
|
102
|
+
* @param value - volatile reference or plain value.
|
|
103
|
+
* @returns current value.
|
|
104
|
+
*/
|
|
105
|
+
function readVolatile(value) {
|
|
106
|
+
return value !== null && typeof value === 'object' && typeof value.get === 'function' ? value.get() : value
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Build the identity named by the live plugin configuration.
|
|
111
|
+
* @param config - live row configuration.
|
|
112
|
+
* @returns configured profile, or undefined when the row names no identity.
|
|
113
|
+
*/
|
|
114
|
+
function configuredProfile(config) {
|
|
115
|
+
const value = config ?? {}
|
|
116
|
+
const profile = projectProfile({
|
|
117
|
+
id: readVolatile(value.id),
|
|
118
|
+
name: readVolatile(value.name),
|
|
119
|
+
contact: readVolatile(value.contact),
|
|
120
|
+
avatarUrl: readVolatile(value.avatarUrl),
|
|
121
|
+
})
|
|
122
|
+
return hasIdentity(profile) ? profile : undefined
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Merge the configured identity over the last known profile, field by field.
|
|
127
|
+
* Explicit settings win; an empty field keeps the cached value.
|
|
128
|
+
* @param configured - identity named by the configuration, if any.
|
|
129
|
+
* @param cached - last profile read from the Platform, if any.
|
|
130
|
+
* @returns the identity to serve, or undefined when neither source has one.
|
|
131
|
+
*/
|
|
132
|
+
function mergeProfile(configured, cached) {
|
|
133
|
+
if (configured === undefined) return cached
|
|
134
|
+
if (cached === undefined) return configured
|
|
135
|
+
const merged = {
|
|
136
|
+
id: configured.id ?? cached.id,
|
|
137
|
+
name: configured.name ?? cached.name,
|
|
138
|
+
contact: configured.contact ?? cached.contact,
|
|
139
|
+
avatarUrl: configured.avatarUrl ?? cached.avatarUrl,
|
|
140
|
+
}
|
|
141
|
+
return hasIdentity(merged) ? merged : undefined
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** Read the persisted last-known profile; an absent or corrupt cache is not an error. */
|
|
145
|
+
async function readCachedProfile() {
|
|
146
|
+
try {
|
|
147
|
+
const parsed = JSON.parse(await readFile(CACHE_FILE, 'utf8'))
|
|
148
|
+
if (parsed === null || typeof parsed !== 'object' || parsed.version !== CACHE_VERSION) return undefined
|
|
149
|
+
return projectProfile(parsed.profile)
|
|
150
|
+
} catch (error) {
|
|
151
|
+
if (error?.code !== 'ENOENT') console.error(`${LOG_PREFIX} profile cache read failed`, { code: error?.code })
|
|
152
|
+
return undefined
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/** Persist the last successful profile for the next blocked read. */
|
|
157
|
+
async function writeCachedProfile(profile) {
|
|
158
|
+
try {
|
|
159
|
+
await mkdir(dirname(CACHE_FILE), { recursive: true })
|
|
160
|
+
const temp = `${CACHE_FILE}.${process.pid}.tmp`
|
|
161
|
+
await writeFile(temp, JSON.stringify({
|
|
162
|
+
version: CACHE_VERSION,
|
|
163
|
+
updatedAt: new Date().toISOString(),
|
|
164
|
+
profile,
|
|
165
|
+
}), { encoding: 'utf8', mode: 0o600 })
|
|
166
|
+
await rename(temp, CACHE_FILE)
|
|
167
|
+
} catch (error) {
|
|
168
|
+
console.error(`${LOG_PREFIX} profile cache write failed`, { code: error?.code })
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Install the profile-read adapter.
|
|
174
|
+
* @param ctx - Host context owning the account service.
|
|
175
|
+
* @param config - volatile identity configuration; `name`, `contact`, `avatarUrl`, `id`.
|
|
176
|
+
*/
|
|
177
|
+
export function apply(ctx, config) {
|
|
178
|
+
let cached
|
|
179
|
+
const cacheReady = readCachedProfile().then((value) => { cached = value })
|
|
180
|
+
const configuredSignature = () => {
|
|
181
|
+
const profile = configuredProfile(config)
|
|
182
|
+
if (profile === undefined) return ''
|
|
183
|
+
return [profile.name, profile.contact, profile.avatarUrl, profile.id].map((value) => value ?? '').join('\u0000')
|
|
184
|
+
}
|
|
185
|
+
const fallback = () => mergeProfile(configuredProfile(config), cached)
|
|
186
|
+
const nudge = () => {
|
|
187
|
+
try {
|
|
188
|
+
if (typeof account.changed === 'function') account.changed()
|
|
189
|
+
} catch (error) {
|
|
190
|
+
console.error(`${LOG_PREFIX} account change notification failed`, { code: error?.code })
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const account = ctx.deepseekAccount
|
|
195
|
+
if (typeof account?.getProfile !== 'function') {
|
|
196
|
+
console.error(`${LOG_PREFIX} deepseekAccount.getProfile is unavailable; fallback was not installed`)
|
|
197
|
+
return
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// Keep one true original reader on the service instance. A configuration
|
|
201
|
+
// change can remount the plugin before the previous instance is disposed;
|
|
202
|
+
// re-using the marker prevents stacking a patched reader on a patched
|
|
203
|
+
// reader, which would make a fallback reply look like a real Platform read.
|
|
204
|
+
const original = account[ORIGINAL_GET_PROFILE] ?? account.getProfile
|
|
205
|
+
if (account[ORIGINAL_GET_PROFILE] === undefined) {
|
|
206
|
+
try {
|
|
207
|
+
Object.defineProperty(account, ORIGINAL_GET_PROFILE, { value: original, configurable: true })
|
|
208
|
+
} catch (_error) {}
|
|
209
|
+
}
|
|
210
|
+
async function getProfile(client) {
|
|
211
|
+
let result
|
|
212
|
+
try {
|
|
213
|
+
result = await original.call(account, client)
|
|
214
|
+
} catch (error) {
|
|
215
|
+
console.error(`${LOG_PREFIX} profile read threw`, { code: error?.code })
|
|
216
|
+
result = undefined
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
if (result !== null && typeof result === 'object' && result.status === 'ready') {
|
|
220
|
+
const profile = projectProfile(result.value)
|
|
221
|
+
if (profile !== undefined) {
|
|
222
|
+
cached = profile
|
|
223
|
+
void writeCachedProfile(profile)
|
|
224
|
+
}
|
|
225
|
+
return result
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// Only a signed-in-but-unreadable profile is replaced; null keeps sign-out.
|
|
229
|
+
const blocked = result === undefined
|
|
230
|
+
|| (result !== null && typeof result === 'object' && result.status === 'failed')
|
|
231
|
+
if (blocked) {
|
|
232
|
+
await cacheReady
|
|
233
|
+
const profile = fallback()
|
|
234
|
+
if (profile !== undefined) return { status: 'ready', value: profile }
|
|
235
|
+
}
|
|
236
|
+
return result
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
account.getProfile = getProfile
|
|
240
|
+
ctx.effect(() => () => {
|
|
241
|
+
if (account.getProfile === getProfile) account.getProfile = original
|
|
242
|
+
}, 'account-identity-fallback: restore profile read')
|
|
243
|
+
|
|
244
|
+
// A configuration save from the Plugins page commits volatile fields in
|
|
245
|
+
// place and emits `loader/volatile-update`; ask connected account surfaces to
|
|
246
|
+
// read the profile again so the edited identity appears immediately.
|
|
247
|
+
ctx.on('loader/volatile-update', nudge)
|
|
248
|
+
|
|
249
|
+
// The loader event is the primary signal; the signature watch also covers a
|
|
250
|
+
// remount race where the save lands between plugin instances.
|
|
251
|
+
let lastSignature = configuredSignature()
|
|
252
|
+
const watch = setInterval(() => {
|
|
253
|
+
const next = configuredSignature()
|
|
254
|
+
if (next === lastSignature) return
|
|
255
|
+
lastSignature = next
|
|
256
|
+
nudge()
|
|
257
|
+
}, 1000)
|
|
258
|
+
watch.unref?.()
|
|
259
|
+
ctx.effect(() => () => clearInterval(watch), 'account-identity-fallback: configuration watch')
|
|
260
|
+
|
|
261
|
+
// Nudge connected clients once at load so the restored identity is read now
|
|
262
|
+
// instead of waiting for the next Settings visit or app restart.
|
|
263
|
+
setTimeout(nudge, 0)
|
|
264
|
+
}
|
package/locale/en.json
ADDED
package/locale/ru.json
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@tyapinalexandr/dsh-account-identity-fallback",
|
|
3
|
+
"version": "0.2.4",
|
|
4
|
+
"description": "Keeps the DeepSeek account name and avatar visible when the Platform profile endpoint is blocked, with an in-app name/avatar editor",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "index.js",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": "./index.js",
|
|
9
|
+
"./client": "./client.js",
|
|
10
|
+
"./package.json": "./package.json",
|
|
11
|
+
"./locale/*.json": "./locale/*.json"
|
|
12
|
+
},
|
|
13
|
+
"files": [
|
|
14
|
+
"index.js",
|
|
15
|
+
"client.js",
|
|
16
|
+
"cordis.patch.yml",
|
|
17
|
+
"README.md",
|
|
18
|
+
"icon.svg",
|
|
19
|
+
"locale/*.json"
|
|
20
|
+
],
|
|
21
|
+
"icon": "./icon.svg",
|
|
22
|
+
"keywords": [
|
|
23
|
+
"dsh-plugin",
|
|
24
|
+
"deepseek-harness",
|
|
25
|
+
"deepseek",
|
|
26
|
+
"account",
|
|
27
|
+
"profile",
|
|
28
|
+
"fallback"
|
|
29
|
+
],
|
|
30
|
+
"author": "tyapinalexandr",
|
|
31
|
+
"license": "MIT",
|
|
32
|
+
"publishConfig": {
|
|
33
|
+
"access": "public"
|
|
34
|
+
},
|
|
35
|
+
"dsh": {
|
|
36
|
+
"bundle": {
|
|
37
|
+
"patch": "./cordis.patch.yml"
|
|
38
|
+
},
|
|
39
|
+
"client": {
|
|
40
|
+
"platform": "web"
|
|
41
|
+
}
|
|
42
|
+
},
|
|
43
|
+
"peerDependencies": {
|
|
44
|
+
"@deepseek-ai/schemastery": "^3.18.0"
|
|
45
|
+
},
|
|
46
|
+
"devDependencies": {
|
|
47
|
+
"@deepseek-ai/schemastery": "^3.18.0"
|
|
48
|
+
},
|
|
49
|
+
"engines": {
|
|
50
|
+
"dsh": ">=0.2.1-rc.5 <0.3.0"
|
|
51
|
+
}
|
|
52
|
+
}
|