tensorgrid-ui 1.6.1 → 2.0.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/LICENSE +15 -0
- package/brand/tensorgrid.ico +0 -0
- package/env/snapshot.json +916 -0
- package/install.ps1 +457 -0
- package/lib/cli.mjs +67 -0
- package/lib/client.js +134 -104
- package/lib/http-runtime.js +58 -23
- package/lib/index.js +62 -2
- package/lib/review-log.js +296 -35
- package/lib/review-settings.js +205 -144
- package/lib/review.js +36 -26
- package/lib/reviewers.js +73 -16
- package/package.json +12 -2
- package/preset/agent.cordis.yml +304 -0
- package/preset/preset.yml +5 -0
- package/profile/cordis.patch.yml +65 -0
- package/profile/package.json +14 -0
- package/tools/round-ico-corners.mjs +113 -0
package/lib/client.js
CHANGED
|
@@ -180,22 +180,27 @@ window.__ModuleLoader__.load({
|
|
|
180
180
|
'review.effortName': 'effort',
|
|
181
181
|
'review.other': 'another…',
|
|
182
182
|
'review.modelName': 'model name',
|
|
183
|
-
'review.custom': 'Your own set of reviewers — no mode matches it.',
|
|
184
|
-
'review.details': 'Detailed settings',
|
|
185
183
|
'review.fromList': 'back to the list',
|
|
186
|
-
'review.ours': 'ours',
|
|
187
184
|
'review.objectFalse': 'I disagree — this is real',
|
|
188
185
|
'review.accuracy': 'How often each reviewer turned out to be right',
|
|
189
186
|
'review.notChecked': 'not verified yet',
|
|
190
187
|
'review.byAgent': 'checked by the assistant',
|
|
191
|
-
'review.humanEvidence': '
|
|
188
|
+
'review.humanEvidence': 'The user disagreed with the assistant\'s verdict, having looked at the finding.',
|
|
192
189
|
'review.lastReview': 'Last review',
|
|
193
190
|
'review.objectReal': 'I disagree — this is wrong',
|
|
194
191
|
'review.of': 'of',
|
|
195
192
|
'review.byHuman': 'your call',
|
|
193
|
+
'review.needsProduct': 'Works only if that product is installed separately',
|
|
194
|
+
'review.add': 'Add a reviewer:',
|
|
195
|
+
'review.participates': 'Take part in reviews',
|
|
196
|
+
'review.noneActive': 'Every reviewer is switched off — a review will find nothing.',
|
|
197
|
+
'review.remove': 'Remove this reviewer',
|
|
198
|
+
'review.bySubscription': 'subscription',
|
|
199
|
+
'review.limit': 'That is as many reviewers as one review can hold.',
|
|
200
|
+
'review.byApiKey': 'paid per token',
|
|
196
201
|
'review.nav': 'Reviewers',
|
|
197
202
|
'review.title': 'Second opinion',
|
|
198
|
-
'review.hint': 'Independent reviewers examine finished work and report what is wrong.
|
|
203
|
+
'review.hint': 'Independent reviewers examine finished work and report what is wrong. Choose how many and which ones: different models find different things.',
|
|
199
204
|
'review.model': 'Model',
|
|
200
205
|
'review.effort': 'Effort',
|
|
201
206
|
'review.asConfigured': 'as configured',
|
|
@@ -251,22 +256,27 @@ window.__ModuleLoader__.load({
|
|
|
251
256
|
'review.effortName': '强度',
|
|
252
257
|
'review.other': '其他…',
|
|
253
258
|
'review.modelName': '模型名称',
|
|
254
|
-
'review.custom': '自定义审阅者组合——没有匹配的模式。',
|
|
255
|
-
'review.details': '详细设置',
|
|
256
259
|
'review.fromList': '返回列表',
|
|
257
|
-
'review.ours': '我们的',
|
|
258
260
|
'review.objectFalse': '我不同意——这是真的',
|
|
259
261
|
'review.accuracy': '各审阅者的正确率',
|
|
260
262
|
'review.notChecked': '尚未核实',
|
|
261
263
|
'review.byAgent': '由助手核实',
|
|
262
|
-
'review.humanEvidence': '
|
|
264
|
+
'review.humanEvidence': '用户查看该发现后,不同意助手的判定。',
|
|
263
265
|
'review.lastReview': '上次审阅',
|
|
264
266
|
'review.objectReal': '我不同意——这是错的',
|
|
265
267
|
'review.of': '共',
|
|
266
268
|
'review.byHuman': '你的判断',
|
|
269
|
+
'review.needsProduct': '仅在单独安装该产品后可用',
|
|
270
|
+
'review.add': '添加审阅者:',
|
|
271
|
+
'review.participates': '参与审阅',
|
|
272
|
+
'review.noneActive': '所有审阅者都已关闭——审阅将一无所获。',
|
|
273
|
+
'review.remove': '移除该审阅者',
|
|
274
|
+
'review.bySubscription': '订阅',
|
|
275
|
+
'review.limit': '一次审阅最多只能有这么多审阅者。',
|
|
276
|
+
'review.byApiKey': '按量计费',
|
|
267
277
|
'review.nav': '审阅者',
|
|
268
278
|
'review.title': '第二意见',
|
|
269
|
-
'review.hint': '
|
|
279
|
+
'review.hint': '独立审阅者检查已完成的工作并报告问题。自行决定数量与人选:不同模型发现的问题不同。',
|
|
270
280
|
'review.model': '模型',
|
|
271
281
|
'review.effort': '推理强度',
|
|
272
282
|
'review.asConfigured': '按产品设置',
|
|
@@ -1619,22 +1629,27 @@ window.__ModuleLoader__.load({
|
|
|
1619
1629
|
'review.effortName': 'усилие',
|
|
1620
1630
|
'review.other': 'другая…',
|
|
1621
1631
|
'review.modelName': 'имя модели',
|
|
1622
|
-
'review.custom': 'Свой набор ревизоров — ни один режим ему не соответствует.',
|
|
1623
|
-
'review.details': 'Подробные настройки',
|
|
1624
1632
|
'review.fromList': 'вернуться к списку',
|
|
1625
|
-
'review.ours': 'наш',
|
|
1626
1633
|
'review.objectFalse': 'Не согласен — это настоящая',
|
|
1627
1634
|
'review.accuracy': 'Как часто каждый ревизор оказывался прав',
|
|
1628
1635
|
'review.notChecked': 'ещё не проверено',
|
|
1629
1636
|
'review.byAgent': 'проверил помощник',
|
|
1630
|
-
'review.humanEvidence': '
|
|
1637
|
+
'review.humanEvidence': 'Пользователь не согласился с приговором помощника, посмотрев находку.',
|
|
1631
1638
|
'review.lastReview': 'Последний обзор',
|
|
1632
1639
|
'review.objectReal': 'Не согласен — это ложная',
|
|
1633
1640
|
'review.of': 'из',
|
|
1634
1641
|
'review.byHuman': 'ваше решение',
|
|
1642
|
+
'review.needsProduct': 'Работает, только если этот продукт установлен отдельно',
|
|
1643
|
+
'review.add': 'Добавить ревизора:',
|
|
1644
|
+
'review.participates': 'Участвует в обзорах',
|
|
1645
|
+
'review.noneActive': 'Все ревизоры выключены — обзор ничего не найдёт.',
|
|
1646
|
+
'review.remove': 'Убрать этого ревизора',
|
|
1647
|
+
'review.bySubscription': 'по подписке',
|
|
1648
|
+
'review.limit': 'Больше ревизоров в один обзор не поместится.',
|
|
1649
|
+
'review.byApiKey': 'по токенам',
|
|
1635
1650
|
'review.nav': 'Ревизоры',
|
|
1636
1651
|
'review.title': 'Второе мнение',
|
|
1637
|
-
'review.hint': 'Независимые ревизоры смотрят готовую работу и сообщают, что в ней не так.
|
|
1652
|
+
'review.hint': 'Независимые ревизоры смотрят готовую работу и сообщают, что в ней не так. Сколько их и какие — выбираете вы: разные модели находят разное.',
|
|
1638
1653
|
'review.model': 'Модель',
|
|
1639
1654
|
'review.effort': 'Усилие',
|
|
1640
1655
|
'review.asConfigured': 'как настроено',
|
|
@@ -1788,7 +1803,7 @@ window.__ModuleLoader__.load({
|
|
|
1788
1803
|
// поэтому строка остаётся согласованной при любой палитре.
|
|
1789
1804
|
'.dsx-setting{display:flex;align-items:center;justify-content:space-between;gap:16px;width:100%;}',
|
|
1790
1805
|
'.dsx-setting__title{color:var(--dsw-alias-label-primary);font-size:13px;font-weight:500;}',
|
|
1791
|
-
'.dsx-rev__panel{margin-top:14px;padding-top:12px;border-top:1px solid var(--dsw-alias-border-l1)}.dsx-rev__panel-head{display:flex;align-items:center;gap:8px;margin-bottom:8px}.dsx-rev__count{padding:1px 7px;border-radius:99px;background:var(--dsw-alias-bg-layer-3);color:var(--dsw-alias-label-tertiary);font-size:11px}.dsx-rev__findings{display:flex;flex-direction:column;gap:8px}.dsx-rev__finding{padding:9px 11px;border-radius:8px;border:1px solid var(--dsw-alias-border-l1);background:var(--dsw-alias-bg-layer-2)}.dsx-rev__finding--false{opacity:.55}.dsx-rev__finding-head{display:flex;align-items:baseline;gap:7px;flex-wrap:wrap}.dsx-rev__finding-title{font-weight:600;color:var(--dsw-alias-label-primary);font-size:13px;flex:1}.dsx-rev__sev{padding:1px 6px;border-radius:4px;font-size:10px;text-transform:uppercase;letter-spacing:.03em}.dsx-rev__sev--crit{background:#5c1f22;color:#ffb4b4}.dsx-rev__sev--high{background:#5a3f16;color:#f0cd8a}.dsx-rev__sev--unknown{background:#3a3a44;color:#c9c9d4}.dsx-rev__sev--low{background:var(--dsw-alias-bg-layer-3);color:var(--dsw-alias-label-tertiary)}.dsx-rev__who{font-size:11px;color:var(--dsw-alias-label-tertiary)}.dsx-rev__where{margin-top:4px;font-family:ui-monospace,monospace;font-size:11px;color:var(--dsw-alias-label-tertiary)}.dsx-rev__what{margin-top:4px;font-size:12px;color:var(--dsw-alias-label-secondary);line-height:1.45}.dsx-rev__verdict{display:flex;align-items:flex-start;gap:6px;margin-top:7px;padding-top:7px;border-top:1px dashed var(--dsw-alias-border-l1);font-size:11px;color:var(--dsw-alias-label-secondary)}.dsx-rev__verdict-mark{font-weight:700}.dsx-rev__verdict-text{flex:1;line-height:1.4}.dsx-rev__by{color:var(--dsw-alias-label-tertiary);white-space:nowrap}.dsx-rev__object{margin-top:5px}.dsx-rev__object-btn{border:0;background:none;padding:0;color:var(--dsw-alias-label-tertiary);font-size:11px;cursor:pointer;text-decoration:underline dotted}.dsx-rev__object-btn:hover{color:var(--dsw-alias-label-primary)}.dsx-rev__stats{margin-top:12px;padding-top:10px;border-top:1px solid var(--dsw-alias-border-l1)}.dsx-rev__stat{display:flex;justify-content:space-between;gap:10px;margin-top:4px;font-size:12px}.dsx-rev__stat-name{color:var(--dsw-alias-label-secondary)}.dsx-rev__stat-rate{color:var(--dsw-alias-label-primary);font-variant-numeric:tabular-nums}.dsx-
|
|
1806
|
+
'.dsx-rev__panel{margin-top:14px;padding-top:12px;border-top:1px solid var(--dsw-alias-border-l1)}.dsx-rev__panel-head{display:flex;align-items:center;gap:8px;margin-bottom:8px}.dsx-rev__count{padding:1px 7px;border-radius:99px;background:var(--dsw-alias-bg-layer-3);color:var(--dsw-alias-label-tertiary);font-size:11px}.dsx-rev__findings{display:flex;flex-direction:column;gap:8px}.dsx-rev__finding{padding:9px 11px;border-radius:8px;border:1px solid var(--dsw-alias-border-l1);background:var(--dsw-alias-bg-layer-2)}.dsx-rev__finding--false{opacity:.55}.dsx-rev__finding-head{display:flex;align-items:baseline;gap:7px;flex-wrap:wrap}.dsx-rev__finding-title{font-weight:600;color:var(--dsw-alias-label-primary);font-size:13px;flex:1}.dsx-rev__sev{padding:1px 6px;border-radius:4px;font-size:10px;text-transform:uppercase;letter-spacing:.03em}.dsx-rev__sev--crit{background:#5c1f22;color:#ffb4b4}.dsx-rev__sev--high{background:#5a3f16;color:#f0cd8a}.dsx-rev__sev--unknown{background:#3a3a44;color:#c9c9d4}.dsx-rev__sev--low{background:var(--dsw-alias-bg-layer-3);color:var(--dsw-alias-label-tertiary)}.dsx-rev__who{font-size:11px;color:var(--dsw-alias-label-tertiary)}.dsx-rev__where{margin-top:4px;font-family:ui-monospace,monospace;font-size:11px;color:var(--dsw-alias-label-tertiary)}.dsx-rev__what{margin-top:4px;font-size:12px;color:var(--dsw-alias-label-secondary);line-height:1.45}.dsx-rev__verdict{display:flex;align-items:flex-start;gap:6px;margin-top:7px;padding-top:7px;border-top:1px dashed var(--dsw-alias-border-l1);font-size:11px;color:var(--dsw-alias-label-secondary)}.dsx-rev__verdict-mark{font-weight:700}.dsx-rev__verdict-text{flex:1;line-height:1.4}.dsx-rev__by{color:var(--dsw-alias-label-tertiary);white-space:nowrap}.dsx-rev__object{margin-top:5px}.dsx-rev__object-btn{border:0;background:none;padding:0;color:var(--dsw-alias-label-tertiary);font-size:11px;cursor:pointer;text-decoration:underline dotted}.dsx-rev__object-btn:hover{color:var(--dsw-alias-label-primary)}.dsx-rev__stats{margin-top:12px;padding-top:10px;border-top:1px solid var(--dsw-alias-border-l1)}.dsx-rev__stat{display:flex;justify-content:space-between;gap:10px;margin-top:4px;font-size:12px}.dsx-rev__stat-name{color:var(--dsw-alias-label-secondary)}.dsx-rev__stat-rate{color:var(--dsw-alias-label-primary);font-variant-numeric:tabular-nums}.dsx-rev__tag{margin-left:6px;padding:1px 6px;border-radius:99px;background:var(--dsw-alias-bg-layer-3);color:var(--dsw-alias-label-tertiary);font-size:10px}.dsx-rev__custom{display:inline-flex;align-items:center;gap:4px}.dsx-rev__input{padding:3px 6px;border-radius:6px;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-primary);font-size:12px;width:170px}.dsx-rev__back{border:0;background:none;color:var(--dsw-alias-label-tertiary);cursor:pointer;font-size:14px;line-height:1;padding:0 2px}.dsx-rev__remove{margin-left:auto;border:0;background:none;color:var(--dsw-alias-label-tertiary);font-size:16px;line-height:1;padding:0 4px;cursor:pointer}.dsx-rev__remove:hover{color:#ffb4b4}.dsx-rev__add{display:flex;flex-wrap:wrap;align-items:center;gap:6px;margin-top:12px}.dsx-rev__add-label{color:var(--dsw-alias-label-secondary);font-size:12px;margin-right:2px}.dsx-rev__add-btn{padding:4px 11px;border:1px dashed var(--dsw-alias-border-l2);border-radius:99px;background:transparent;color:var(--dsw-alias-label-secondary);font-size:12px;cursor:pointer;font-family:inherit}.dsx-rev__add-btn:hover:not(:disabled){border-style:solid;color:var(--dsw-alias-label-primary)}.dsx-rev__add-btn:disabled{opacity:.4;cursor:default}.dsx-rev__list{display:flex;flex-direction:column;gap:8px}.dsx-rev__card{padding:10px 12px;border-radius:8px;border:1px solid var(--dsw-alias-border-l1);background:var(--dsw-alias-bg-layer-2);opacity:.6;transition:opacity .15s ease,border-color .15s ease}.dsx-rev__card--on{opacity:1;border-color:var(--dsw-alias-border-l2)}.dsx-rev__head{display:flex;align-items:center;gap:8px;cursor:pointer}.dsx-rev__name{font-weight:600;color:var(--dsw-alias-label-primary)}.dsx-rev__fields{display:flex;flex-wrap:wrap;gap:12px;margin-top:8px;padding-left:22px}.dsx-rev__field{display:flex;align-items:center;gap:6px;color:var(--dsw-alias-label-secondary);font-size:12px}.dsx-rev__select{padding:3px 6px;border-radius:6px;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-primary);font-size:12px}.dsx-subs{display:flex;flex-direction:column;gap:14px;padding:4px 0}.dsx-subs__head{display:flex;flex-direction:column;gap:4px}.dsx-subs__title{font-weight:600;font-size:15px;color:var(--dsw-alias-label-primary)}.dsx-subs__note{padding-top:6px;border-top:1px solid var(--dsw-alias-border-l1);color:var(--dsw-alias-label-tertiary);font-size:12px;line-height:1.5}.dsx-setting--stack{flex-direction:column;align-items:stretch;gap:10px}.dsx-auth__list{display:flex;flex-direction:column;gap:6px}.dsx-auth__row{display:flex;align-items:center;justify-content:space-between;gap:12px}.dsx-auth__name{color:var(--dsw-alias-label-primary)}.dsx-auth__badge{margin-left:8px;padding:1px 6px;border-radius:999px;font-size:11px;color:var(--dsw-alias-state-success-primary);border:1px solid var(--dsw-alias-border-l2)}.dsx-auth__panel{display:flex;flex-direction:column;gap:8px;padding:10px;border-radius:8px;background:var(--dsw-alias-bg-layer-2);border:1px solid var(--dsw-alias-border-l1)}.dsx-auth__notice,.dsx-auth__prompt{display:flex;flex-direction:column;gap:6px;color:var(--dsw-alias-label-secondary)}.dsx-auth__link{color:var(--dsw-alias-brand-primary);word-break:break-all}.dsx-auth__code{font-family:ui-monospace,monospace;letter-spacing:.08em;color:var(--dsw-alias-label-primary)}.dsx-auth__input{flex:1;min-width:0;padding:4px 8px;border-radius:6px;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-primary)}.dsx-setting__hint--strong{color:var(--dsw-alias-label-primary)}.dsx-setting__hint--warn{color:var(--dsw-alias-state-warn-primary)}.dsx-setting__hint{color:var(--dsw-alias-label-secondary);font-size:12px;margin-top:2px;}',
|
|
1792
1807
|
'.dsx-setting__control{display:inline-flex;padding:2px;gap:2px;border:1px solid var(--dsw-alias-border-l1);border-radius:8px;background:var(--dsw-alias-bg-layer-2);flex:none;}',
|
|
1793
1808
|
'.dsx-seg{appearance:none;border:0;cursor:pointer;padding:4px 10px;border-radius:6px;font-size:12px;line-height:18px;background:transparent;color:var(--dsw-alias-label-secondary);transition:background .16s ease,color .16s ease;}',
|
|
1794
1809
|
'.dsx-seg:hover{color:var(--dsw-alias-label-primary);}',
|
|
@@ -2346,7 +2361,7 @@ window.__ModuleLoader__.load({
|
|
|
2346
2361
|
disabled: disabled,
|
|
2347
2362
|
title: t('review.fromList'),
|
|
2348
2363
|
onClick: () => { setCustom(false); onPick(null) },
|
|
2349
|
-
children: '
|
|
2364
|
+
children: '\u00d7',
|
|
2350
2365
|
}),
|
|
2351
2366
|
],
|
|
2352
2367
|
})
|
|
@@ -2367,7 +2382,6 @@ window.__ModuleLoader__.load({
|
|
|
2367
2382
|
],
|
|
2368
2383
|
})
|
|
2369
2384
|
}
|
|
2370
|
-
|
|
2371
2385
|
/** Класс важности: имена уровней приходят с Host по-русски. */
|
|
2372
2386
|
function severityClass(severity) {
|
|
2373
2387
|
if (severity === 'критично') return 'crit'
|
|
@@ -2385,16 +2399,19 @@ window.__ModuleLoader__.load({
|
|
|
2385
2399
|
}
|
|
2386
2400
|
// ── Ревизоры ─────────────────────────────────────────────────────────
|
|
2387
2401
|
//
|
|
2388
|
-
//
|
|
2402
|
+
// Свободный список, а не готовые режимы.
|
|
2403
|
+
//
|
|
2404
|
+
// Режимы «быстрый, обычный, глубокий» отвечали за человека на вопрос, на
|
|
2405
|
+
// который он вполне способен ответить сам, и при этом не давали позвать,
|
|
2406
|
+
// скажем, три разные модели одного семейства. Теперь: сколько угодно
|
|
2407
|
+
// записей, у каждой свой вид, модель и усилие.
|
|
2389
2408
|
//
|
|
2390
|
-
//
|
|
2391
|
-
//
|
|
2392
|
-
//
|
|
2393
|
-
// не должен её выпрашивать.
|
|
2409
|
+
// Рядом с каждой моделью показано, чем она оплачена. Это не украшение:
|
|
2410
|
+
// подписка безлимитна, а ключ API считается по токенам, и человек,
|
|
2411
|
+
// собирающий состав, должен видеть разницу ДО того, как получит счёт.
|
|
2394
2412
|
function ReviewRow({ t }) {
|
|
2395
2413
|
const [state, setState] = React.useState(null)
|
|
2396
2414
|
const [busy, setBusy] = React.useState(false)
|
|
2397
|
-
const [expanded, setExpanded] = React.useState(false)
|
|
2398
2415
|
|
|
2399
2416
|
const translate = React.useRef(t)
|
|
2400
2417
|
translate.current = t
|
|
@@ -2443,40 +2460,41 @@ window.__ModuleLoader__.load({
|
|
|
2443
2460
|
React.useEffect(() => { refresh() }, [refresh])
|
|
2444
2461
|
|
|
2445
2462
|
const catalogue = state !== null && Array.isArray(state.catalogue) ? state.catalogue : []
|
|
2446
|
-
const modes = state !== null && Array.isArray(state.modes) ? state.modes : []
|
|
2447
|
-
const settings = state !== null && state.settings ? state.settings : null
|
|
2448
2463
|
const journal = state !== null && state.journal ? state.journal : null
|
|
2449
|
-
|
|
2450
|
-
|
|
2451
|
-
|
|
2452
|
-
|
|
2453
|
-
|
|
2454
|
-
|
|
2455
|
-
|
|
2456
|
-
|
|
2457
|
-
|
|
2458
|
-
save({ ...
|
|
2464
|
+
const reviewers = state !== null && state.settings && Array.isArray(state.settings.reviewers)
|
|
2465
|
+
? state.settings.reviewers
|
|
2466
|
+
: []
|
|
2467
|
+
|
|
2468
|
+
// Список уходит ЦЕЛИКОМ: удаление записи иначе было бы невыразимо —
|
|
2469
|
+
// слияние на стороне Host всегда возвращало бы удалённое обратно.
|
|
2470
|
+
const save = React.useCallback((next) => { refresh({ reviewers: next }) }, [refresh])
|
|
2471
|
+
|
|
2472
|
+
const patch = (id, changes) =>
|
|
2473
|
+
save(reviewers.map((entry) => (entry.id === id ? { ...entry, ...changes } : entry)))
|
|
2474
|
+
|
|
2475
|
+
const remove = (id) => save(reviewers.filter((entry) => entry.id !== id))
|
|
2476
|
+
|
|
2477
|
+
const add = (kind) => {
|
|
2478
|
+
const spec = catalogue.find((entry) => entry.kind === kind)
|
|
2479
|
+
save([...reviewers, {
|
|
2480
|
+
kind,
|
|
2481
|
+
// Умолчание берётся из справочника: пустая модель означала бы
|
|
2482
|
+
// умолчание маршрута, то есть копию уже имеющегося ревизора.
|
|
2483
|
+
provider: spec && spec.internal ? spec.defaultProvider : null,
|
|
2484
|
+
model: spec ? spec.defaultModel : null,
|
|
2485
|
+
effort: null,
|
|
2486
|
+
enabled: true,
|
|
2487
|
+
}])
|
|
2459
2488
|
}
|
|
2460
2489
|
|
|
2461
|
-
const
|
|
2462
|
-
if (
|
|
2463
|
-
|
|
2464
|
-
|
|
2465
|
-
save({ ...settings, choices })
|
|
2490
|
+
const billingLabel = (kind) => {
|
|
2491
|
+
if (kind === 'subscription') return t('review.bySubscription')
|
|
2492
|
+
if (kind === 'api-key') return t('review.byApiKey')
|
|
2493
|
+
return null
|
|
2466
2494
|
}
|
|
2467
2495
|
|
|
2468
|
-
|
|
2469
|
-
|
|
2470
|
-
// называется иначе, чем первая часть имени.
|
|
2471
|
-
const pickModel = (kind, entry) => {
|
|
2472
|
-
if (settings === null) return
|
|
2473
|
-
const previous = settings.choices[kind] || { provider: null, model: null, effort: null }
|
|
2474
|
-
const choices = {
|
|
2475
|
-
...settings.choices,
|
|
2476
|
-
[kind]: { ...previous, provider: entry === null ? null : entry.provider, model: entry === null ? null : entry.id },
|
|
2477
|
-
}
|
|
2478
|
-
save({ ...settings, choices })
|
|
2479
|
-
}
|
|
2496
|
+
const atLimit = reviewers.length >= 8
|
|
2497
|
+
const activeCount = reviewers.filter((entry) => entry.enabled).length
|
|
2480
2498
|
|
|
2481
2499
|
return jsxs('div', {
|
|
2482
2500
|
className: 'dsx-subs',
|
|
@@ -2492,53 +2510,37 @@ window.__ModuleLoader__.load({
|
|
|
2492
2510
|
? jsx('div', { className: 'dsx-setting__hint dsx-setting__hint--warn', children: state.problem })
|
|
2493
2511
|
: null,
|
|
2494
2512
|
|
|
2495
|
-
|
|
2496
|
-
modes.length === 0 ? null : jsx('div', {
|
|
2497
|
-
className: 'dsx-rev__modes',
|
|
2498
|
-
children: modes.map((mode) => jsxs('button', {
|
|
2499
|
-
type: 'button',
|
|
2500
|
-
className: 'dsx-rev__mode' + (settings !== null && settings.mode === mode.id ? ' dsx-rev__mode--on' : ''),
|
|
2501
|
-
disabled: busy,
|
|
2502
|
-
onClick: () => save({ mode: mode.id, applyMode: true }),
|
|
2503
|
-
children: [
|
|
2504
|
-
jsx('span', { className: 'dsx-rev__mode-name', children: mode.label }),
|
|
2505
|
-
jsx('span', { className: 'dsx-rev__mode-hint', children: mode.hint }),
|
|
2506
|
-
],
|
|
2507
|
-
}, mode.id)),
|
|
2508
|
-
}),
|
|
2509
|
-
settings !== null && settings.mode === null
|
|
2510
|
-
? jsx('div', { className: 'dsx-setting__hint', children: t('review.custom') })
|
|
2511
|
-
: null,
|
|
2512
|
-
|
|
2513
|
-
// Подробные настройки
|
|
2514
|
-
jsx('button', {
|
|
2515
|
-
type: 'button',
|
|
2516
|
-
className: 'dsx-rev__toggle',
|
|
2517
|
-
onClick: () => setExpanded(!expanded),
|
|
2518
|
-
children: (expanded ? '▾ ' : '▸ ') + t('review.details'),
|
|
2519
|
-
}),
|
|
2520
|
-
|
|
2521
|
-
!expanded ? null : jsx('div', {
|
|
2513
|
+
jsx('div', {
|
|
2522
2514
|
className: 'dsx-rev__list',
|
|
2523
|
-
children:
|
|
2524
|
-
const
|
|
2525
|
-
|
|
2526
|
-
|
|
2527
|
-
const
|
|
2528
|
-
const choice = stored || { provider: null, model: null, effort: null }
|
|
2529
|
-
const models = Array.isArray(entry.models) ? entry.models : []
|
|
2515
|
+
children: reviewers.map((entry) => {
|
|
2516
|
+
const spec = catalogue.find((item) => item.kind === entry.kind)
|
|
2517
|
+
const models = spec && Array.isArray(spec.models) ? spec.models : []
|
|
2518
|
+
const chosen = models.find((m) => m.id === entry.model)
|
|
2519
|
+
const billing = billingLabel(chosen ? chosen.billing : null)
|
|
2530
2520
|
|
|
2531
2521
|
return jsxs('div', {
|
|
2532
|
-
className: 'dsx-rev__card' + (
|
|
2522
|
+
className: 'dsx-rev__card' + (entry.enabled ? ' dsx-rev__card--on' : ''),
|
|
2533
2523
|
children: [
|
|
2534
|
-
jsxs('
|
|
2524
|
+
jsxs('div', {
|
|
2535
2525
|
className: 'dsx-rev__head',
|
|
2536
2526
|
children: [
|
|
2537
|
-
jsx('input', {
|
|
2538
|
-
|
|
2539
|
-
|
|
2540
|
-
|
|
2541
|
-
:
|
|
2527
|
+
jsx('input', {
|
|
2528
|
+
type: 'checkbox',
|
|
2529
|
+
checked: entry.enabled,
|
|
2530
|
+
disabled: busy,
|
|
2531
|
+
title: t('review.participates'),
|
|
2532
|
+
onChange: () => patch(entry.id, { enabled: !entry.enabled }),
|
|
2533
|
+
}),
|
|
2534
|
+
jsx('span', { className: 'dsx-rev__name', children: spec ? spec.label : entry.kind }),
|
|
2535
|
+
billing === null ? null : jsx('span', { className: 'dsx-rev__tag', children: billing }),
|
|
2536
|
+
jsx('button', {
|
|
2537
|
+
type: 'button',
|
|
2538
|
+
className: 'dsx-rev__remove',
|
|
2539
|
+
disabled: busy,
|
|
2540
|
+
title: t('review.remove'),
|
|
2541
|
+
onClick: () => remove(entry.id),
|
|
2542
|
+
children: '\u00d7',
|
|
2543
|
+
}),
|
|
2542
2544
|
],
|
|
2543
2545
|
}),
|
|
2544
2546
|
jsxs('div', {
|
|
@@ -2550,16 +2552,24 @@ window.__ModuleLoader__.load({
|
|
|
2550
2552
|
jsx('span', { children: t('review.model') }),
|
|
2551
2553
|
jsx(ValuePicker, {
|
|
2552
2554
|
t: t,
|
|
2553
|
-
value:
|
|
2555
|
+
value: entry.model,
|
|
2554
2556
|
disabled: busy,
|
|
2555
2557
|
placeholder: t('review.modelName'),
|
|
2556
2558
|
options: models.map((m) => ({
|
|
2557
2559
|
value: m.id,
|
|
2558
|
-
|
|
2560
|
+
// Провайдер и способ оплаты стоят в самом
|
|
2561
|
+
// названии: у OpenRouter сотни моделей, и без
|
|
2562
|
+
// этого выбрать осмысленно нельзя.
|
|
2563
|
+
label: (m.provider === null ? '' : m.provider + ' \u00b7 ') + m.id
|
|
2564
|
+
+ (m.billing === 'subscription' ? ' \u2014 ' + t('review.bySubscription') : '')
|
|
2565
|
+
+ (m.billing === 'api-key' ? ' \u2014 ' + t('review.byApiKey') : ''),
|
|
2559
2566
|
})),
|
|
2560
2567
|
onPick: (value) => {
|
|
2561
2568
|
const found = models.find((m) => m.id === value)
|
|
2562
|
-
|
|
2569
|
+
patch(entry.id, {
|
|
2570
|
+
provider: value === null ? null : (found ? found.provider : entry.provider),
|
|
2571
|
+
model: value,
|
|
2572
|
+
})
|
|
2563
2573
|
},
|
|
2564
2574
|
}),
|
|
2565
2575
|
],
|
|
@@ -2570,21 +2580,44 @@ window.__ModuleLoader__.load({
|
|
|
2570
2580
|
jsx('span', { children: t('review.effort') }),
|
|
2571
2581
|
jsx(ValuePicker, {
|
|
2572
2582
|
t: t,
|
|
2573
|
-
value:
|
|
2583
|
+
value: entry.effort,
|
|
2574
2584
|
disabled: busy,
|
|
2575
2585
|
placeholder: t('review.effortName'),
|
|
2576
|
-
options: (
|
|
2577
|
-
onPick: (value) =>
|
|
2586
|
+
options: (spec && spec.efforts ? spec.efforts : []).map((e) => ({ value: e, label: e })),
|
|
2587
|
+
onPick: (value) => patch(entry.id, { effort: value }),
|
|
2578
2588
|
}),
|
|
2579
2589
|
],
|
|
2580
2590
|
}),
|
|
2581
2591
|
],
|
|
2582
2592
|
}),
|
|
2583
2593
|
],
|
|
2584
|
-
}, entry.
|
|
2594
|
+
}, entry.id)
|
|
2585
2595
|
}),
|
|
2586
2596
|
}),
|
|
2587
2597
|
|
|
2598
|
+
// Добавление: по кнопке на каждый вид ревизора. Внешние продукты
|
|
2599
|
+
// помечены — они работают, только если установлены отдельно.
|
|
2600
|
+
jsxs('div', {
|
|
2601
|
+
className: 'dsx-rev__add',
|
|
2602
|
+
children: [
|
|
2603
|
+
jsx('span', { className: 'dsx-rev__add-label', children: t('review.add') }),
|
|
2604
|
+
...catalogue.map((spec) => jsx('button', {
|
|
2605
|
+
type: 'button',
|
|
2606
|
+
className: 'dsx-rev__add-btn',
|
|
2607
|
+
disabled: busy || atLimit,
|
|
2608
|
+
title: spec.optional ? t('review.needsProduct') : '',
|
|
2609
|
+
onClick: () => add(spec.kind),
|
|
2610
|
+
children: spec.label + (spec.optional ? ' \u2217' : ''),
|
|
2611
|
+
}, spec.kind)),
|
|
2612
|
+
],
|
|
2613
|
+
}),
|
|
2614
|
+
atLimit ? jsx('div', { className: 'dsx-setting__hint', children: t('review.limit') }) : null,
|
|
2615
|
+
|
|
2616
|
+
reviewers.length === 0
|
|
2617
|
+
? jsx('div', { className: 'dsx-setting__hint dsx-setting__hint--warn', children: t('review.none') })
|
|
2618
|
+
: activeCount === 0
|
|
2619
|
+
? jsx('div', { className: 'dsx-setting__hint dsx-setting__hint--warn', children: t('review.noneActive') })
|
|
2620
|
+
: null,
|
|
2588
2621
|
// ── Панель находок ──────────────────────────────────────────
|
|
2589
2622
|
//
|
|
2590
2623
|
// Показывает последний обзор: что нашли ревизоры и что из этого
|
|
@@ -2680,9 +2713,6 @@ window.__ModuleLoader__.load({
|
|
|
2680
2713
|
}),
|
|
2681
2714
|
],
|
|
2682
2715
|
}),
|
|
2683
|
-
settings !== null && settings.enabled.length === 0
|
|
2684
|
-
? jsx('div', { className: 'dsx-setting__hint dsx-setting__hint--warn', children: t('review.none') })
|
|
2685
|
-
: null,
|
|
2686
2716
|
jsx('div', { className: 'dsx-subs__note', children: t('review.note') }),
|
|
2687
2717
|
],
|
|
2688
2718
|
})
|
package/lib/http-runtime.js
CHANGED
|
@@ -225,7 +225,7 @@ export async function handleUpdate(req, res, state, loadModule) {
|
|
|
225
225
|
* @param llm - служба моделей; может отсутствовать в профиле без неё.
|
|
226
226
|
* @returns пары «провайдер и имя модели», пригодные для показа.
|
|
227
227
|
*/
|
|
228
|
-
async function listRouteModels(llm) {
|
|
228
|
+
async function listRouteModels(llm, credentials) {
|
|
229
229
|
if (llm === undefined || llm === null || typeof llm.listProviders !== 'function') return []
|
|
230
230
|
|
|
231
231
|
const providers = (() => {
|
|
@@ -236,15 +236,42 @@ async function listRouteModels(llm) {
|
|
|
236
236
|
}
|
|
237
237
|
})()
|
|
238
238
|
|
|
239
|
+
// ── ЧЕМ ОПЛАЧЕН КАЖДЫЙ ПРОВАЙДЕР ────────────────────────────────────
|
|
240
|
+
//
|
|
241
|
+
// Это не украшение: подписка безлимитна, а ключ API считается по токенам.
|
|
242
|
+
// Человек, собирающий состав ревизоров, должен видеть разницу ДО того,
|
|
243
|
+
// как получит счёт, — особенно когда ревизоров несколько и каждый читает
|
|
244
|
+
// весь код.
|
|
245
|
+
//
|
|
246
|
+
// Признак берётся из хранилища учётных данных: запись вида `grant` — это
|
|
247
|
+
// вход по подписке, `api-key` — сохранённый ключ. Провайдер, который
|
|
248
|
+
// работает, но записи не имеет, настроен извне: у OpenRouter ключ лежит
|
|
249
|
+
// переменной окружения, и по записям он выглядел бы ненастроенным, хотя
|
|
250
|
+
// отвечает. Проверено на живых данных.
|
|
251
|
+
const billing = new Map()
|
|
252
|
+
if (credentials !== undefined && credentials !== null) {
|
|
253
|
+
try {
|
|
254
|
+
for (const record of await credentials.listRecords()) {
|
|
255
|
+
const key = String(record.key ?? '')
|
|
256
|
+
const slash = key.lastIndexOf('/')
|
|
257
|
+
const name = slash >= 0 ? key.slice(slash + 1) : key
|
|
258
|
+
billing.set(name, record.kind === 'grant' ? 'subscription' : 'api-key')
|
|
259
|
+
}
|
|
260
|
+
} catch {
|
|
261
|
+
// Хранилище недоступно — покажем модели без отметки, это лучше отказа.
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
239
265
|
const collected = []
|
|
240
266
|
for (const info of providers) {
|
|
241
267
|
const name = typeof info === 'string' ? info : info?.name ?? info?.provider
|
|
242
268
|
if (typeof name !== 'string' || name === '') continue
|
|
269
|
+
const kind = billing.get(name) ?? 'external'
|
|
243
270
|
try {
|
|
244
271
|
const models = await llm.listModels(name)
|
|
245
272
|
for (const model of models ?? []) {
|
|
246
273
|
const id = typeof model === 'string' ? model : model?.id ?? model?.name
|
|
247
|
-
if (typeof id === 'string' && id !== '') collected.push({ provider: name, id })
|
|
274
|
+
if (typeof id === 'string' && id !== '') collected.push({ provider: name, id, billing: kind })
|
|
248
275
|
}
|
|
249
276
|
} catch {
|
|
250
277
|
// Провайдер без списка или с истёкшим ключом просто не добавит своих.
|
|
@@ -275,31 +302,36 @@ export async function handleReview(req, res, loadModule, services) {
|
|
|
275
302
|
// Для внешних продуктов так не выйдет, и это не лень: Claude Code и
|
|
276
303
|
// Codex понимают только СВОИ имена моделей, а спросить у них список
|
|
277
304
|
// нечем. Там короткий перечень плюс возможность вписать своё.
|
|
278
|
-
const routeModels = await listRouteModels(services?.llm)
|
|
305
|
+
const routeModels = await listRouteModels(services?.llm, services?.credentials)
|
|
279
306
|
|
|
280
307
|
const catalogue = Object.entries(REVIEWERS).map(([kind, spec]) => ({
|
|
281
308
|
kind,
|
|
282
309
|
label: spec.label,
|
|
283
310
|
internal: spec.kind === 'internal',
|
|
284
|
-
|
|
311
|
+
optional: spec.optional === true,
|
|
312
|
+
models: spec.kind === 'internal' ? routeModels : spec.models.map((id) => ({ provider: null, id, billing: 'external' })),
|
|
285
313
|
efforts: spec.efforts,
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
const modes = Object.entries(settings.MODES).map(([id, spec]) => ({
|
|
289
|
-
id,
|
|
290
|
-
label: spec.label,
|
|
291
|
-
hint: spec.hint,
|
|
292
|
-
enabled: spec.enabled,
|
|
314
|
+
defaultProvider: spec.defaultProvider ?? null,
|
|
315
|
+
defaultModel: spec.defaultModel ?? null,
|
|
293
316
|
}))
|
|
294
317
|
|
|
295
318
|
// Журнал отдаётся вместе с настройками: панель находок и настройки —
|
|
296
319
|
// один раздел, и второй запрос за тем же экраном был бы лишней задержкой.
|
|
297
320
|
const log = await loadModule('./review-log.js')
|
|
298
321
|
const journal = await log.read()
|
|
299
|
-
|
|
322
|
+
// Повреждённый журнал не должен уносить с собой весь раздел: состав
|
|
323
|
+
// ревизоров, режимы и выбор моделей к нему не относятся и обязаны
|
|
324
|
+
// остаться доступными. Пользователю показывается причина, а не ошибка
|
|
325
|
+
// инструмента вместо настроек.
|
|
326
|
+
const digest = {
|
|
327
|
+
latest: log.latest(journal),
|
|
328
|
+
accuracy: log.accuracy(journal),
|
|
329
|
+
runs: journal.runs.length,
|
|
330
|
+
...(journal.intact === false ? { problem: journal.problem ?? 'журнал обзоров повреждён' } : {}),
|
|
331
|
+
}
|
|
300
332
|
|
|
301
333
|
if (req.method === 'GET') {
|
|
302
|
-
reply(res, 200, { catalogue,
|
|
334
|
+
reply(res, 200, { catalogue, settings: await settings.readSettings(null), journal: digest })
|
|
303
335
|
return
|
|
304
336
|
}
|
|
305
337
|
|
|
@@ -328,25 +360,28 @@ export async function handleReview(req, res, loadModule, services) {
|
|
|
328
360
|
// обратно уже не отменяется: следующий обзор не должен молча стереть
|
|
329
361
|
// ровно то несогласие, ради которого панель и сделана.
|
|
330
362
|
if (typeof body.findingId === 'string' && typeof body.verdict === 'string') {
|
|
331
|
-
await log.recordVerdict(
|
|
363
|
+
const outcome = await log.recordVerdict(
|
|
364
|
+
body.findingId,
|
|
365
|
+
body.verdict,
|
|
366
|
+
typeof body.evidence === 'string' ? body.evidence : '',
|
|
367
|
+
'human',
|
|
368
|
+
)
|
|
332
369
|
const after = await log.read()
|
|
333
370
|
reply(res, 200, {
|
|
334
371
|
catalogue,
|
|
335
|
-
modes,
|
|
336
372
|
settings: await settings.readSettings(null),
|
|
337
373
|
journal: { latest: log.latest(after), accuracy: log.accuracy(after), runs: after.runs.length },
|
|
374
|
+
// Отклонённое возражение должно быть ВИДНО: иначе человек считает,
|
|
375
|
+
// что высказался, а в панели ничего не изменилось.
|
|
376
|
+
...(outcome.applied === false ? { problem: outcome.problem ?? 'возражение не принято' } : {}),
|
|
338
377
|
})
|
|
339
378
|
return
|
|
340
379
|
}
|
|
341
380
|
|
|
342
|
-
//
|
|
343
|
-
//
|
|
344
|
-
//
|
|
345
|
-
|
|
346
|
-
? settings.applyMode(await settings.readSettings(null), body.mode)
|
|
347
|
-
: body
|
|
348
|
-
|
|
349
|
-
reply(res, 200, { catalogue, modes, settings: await settings.writeSettings(null, next), journal: digest })
|
|
381
|
+
// Список приходит целиком и целиком же замещает сохранённый. Для списка
|
|
382
|
+
// это единственно верно: слияние возвращало бы удалённую запись обратно,
|
|
383
|
+
// то есть удалить ревизора стало бы нельзя.
|
|
384
|
+
reply(res, 200, { catalogue, settings: await settings.writeSettings(null, body), journal: digest })
|
|
350
385
|
} catch (error) {
|
|
351
386
|
reply(res, 500, { problem: String(error?.message ?? error) })
|
|
352
387
|
}
|