kingkont 0.20.17 → 0.20.18
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/package.json +1 -1
- package/renderer/generate.js +72 -0
- package/renderer/media.js +22 -0
- package/skill/SKILL.md +102 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "kingkont",
|
|
3
|
-
"version": "0.20.
|
|
3
|
+
"version": "0.20.18",
|
|
4
4
|
"description": "KingKont \u00b7 Chatium \u2014 \u043d\u043e\u0434-\u0440\u0435\u0434\u0430\u043a\u0442\u043e\u0440 \u0441\u0446\u0435\u043d \u0441 AI-\u0433\u0435\u043d\u0435\u0440\u0430\u0446\u0438\u0435\u0439 (\u043a\u0430\u0440\u0442\u0438\u043d\u043a\u0438/\u0432\u0438\u0434\u0435\u043e/\u0433\u043e\u043b\u043e\u0441/SFX/\u043c\u0443\u0437\u044b\u043a\u0430/\u0442\u0435\u043a\u0441\u0442)",
|
|
5
5
|
"main": "main.js",
|
|
6
6
|
"bin": {
|
package/renderer/generate.js
CHANGED
|
@@ -516,6 +516,13 @@ $('textGenSubmit').addEventListener('click', async () => {
|
|
|
516
516
|
// Собираем image-mentions из промпта (видение для модели). Видео/audio в
|
|
517
517
|
// OpenRouter chat не поддерживается — игнорируем.
|
|
518
518
|
const allMediaRefs = (typeof gatherMediaRefs === 'function') ? gatherMediaRefs(rawPrompt) : [];
|
|
519
|
+
// + источник pendingConnectionFrom (anchor-drag → новая text-нода): если
|
|
520
|
+
// юзер протянул из image-ноды в gen-text, картинка автоматически попадает
|
|
521
|
+
// в контекст. Видео-источники для text-gen бесполезны (OpenRouter chat
|
|
522
|
+
// их не съест), но мы оставляем в общем списке — фильтр ниже их уберёт.
|
|
523
|
+
for (const pf of _pendingConnectionFromRef()) {
|
|
524
|
+
if (!allMediaRefs.some(r => r.file === pf.file)) allMediaRefs.push(pf);
|
|
525
|
+
}
|
|
519
526
|
const imageRefs = allMediaRefs.filter(r => r.type === 'image' && r.file);
|
|
520
527
|
// Резолвим mentions: text-ноды → инлайн .md, image-ноды → маркеры [image N].
|
|
521
528
|
const resolvedPrompt = (typeof resolveMentions === 'function') ? resolveMentions(rawPrompt, imageRefs) : rawPrompt;
|
|
@@ -1202,6 +1209,63 @@ function presetPicksForBoard() {
|
|
|
1202
1209
|
if (b?.kind === 'location' && b.name) state.pickedLocName = b.name;
|
|
1203
1210
|
}
|
|
1204
1211
|
|
|
1212
|
+
// Собирает image/video ноды, ИЗ которых есть connection В targetNodeId.
|
|
1213
|
+
// Юзер: «если в ноду 'входит' несколько других нод-картинок или 'нод-видео'
|
|
1214
|
+
// — подавай их тоже как референсы». Когда граф уже выражает связь
|
|
1215
|
+
// (юзер протянул стрелку), не заставляем юзера ещё и писать @name в
|
|
1216
|
+
// промпте — автоматически подмешиваем источники к refs.
|
|
1217
|
+
// Используется в:
|
|
1218
|
+
// • genSubmit (новая нода) — для state.pendingConnectionFrom (anchor-drag)
|
|
1219
|
+
// это уже учитывается отдельно, т.к. там ещё нет node.id; здесь же
|
|
1220
|
+
// полезно когда юзер ставит несколько связей через drag-line ДО
|
|
1221
|
+
// запуска gen-modal'а (редкий кейс).
|
|
1222
|
+
// • regenerateInto (существующая нода) — все incoming-edges с
|
|
1223
|
+
// image/video-источниками. Главный кейс: юзер связал draft-ноду с
|
|
1224
|
+
// несколькими картинками и нажал «↻ Перегенерировать».
|
|
1225
|
+
function gatherIncomingMediaRefs(targetNodeId) {
|
|
1226
|
+
const refs = [];
|
|
1227
|
+
const board = state.currentBoard;
|
|
1228
|
+
if (!board || !targetNodeId) return refs;
|
|
1229
|
+
const conns = board.metadata.connections || [];
|
|
1230
|
+
const nodes = board.metadata.nodes || [];
|
|
1231
|
+
for (const c of conns) {
|
|
1232
|
+
if (c.to !== targetNodeId) continue;
|
|
1233
|
+
const src = nodes.find(n => n.id === c.from);
|
|
1234
|
+
if (!src) continue;
|
|
1235
|
+
if (src.type !== 'image' && src.type !== 'video') continue;
|
|
1236
|
+
if (!src.file) continue; // источник ещё не готов — не блокируем gen, просто пропускаем
|
|
1237
|
+
refs.push({
|
|
1238
|
+
key: `incoming:${src.id}`,
|
|
1239
|
+
name: src.name || src.file.split('/').pop(),
|
|
1240
|
+
type: src.type,
|
|
1241
|
+
file: src.file,
|
|
1242
|
+
boardHandle: board.handle,
|
|
1243
|
+
});
|
|
1244
|
+
}
|
|
1245
|
+
return refs;
|
|
1246
|
+
}
|
|
1247
|
+
|
|
1248
|
+
// Источник pendingConnectionFrom (для NEW ноды, когда node.id ещё не
|
|
1249
|
+
// существует). Возвращает массив с одним рефом или пустой.
|
|
1250
|
+
function _pendingConnectionFromRef() {
|
|
1251
|
+
const refs = [];
|
|
1252
|
+
if (!state.pendingConnectionFrom) return refs;
|
|
1253
|
+
const board = state.currentBoard;
|
|
1254
|
+
if (!board) return refs;
|
|
1255
|
+
const src = (board.metadata.nodes || []).find(n => n.id === state.pendingConnectionFrom);
|
|
1256
|
+
if (!src) return refs;
|
|
1257
|
+
if (src.type !== 'image' && src.type !== 'video') return refs;
|
|
1258
|
+
if (!src.file) return refs;
|
|
1259
|
+
refs.push({
|
|
1260
|
+
key: `incoming:${src.id}`,
|
|
1261
|
+
name: src.name || src.file.split('/').pop(),
|
|
1262
|
+
type: src.type,
|
|
1263
|
+
file: src.file,
|
|
1264
|
+
boardHandle: board.handle,
|
|
1265
|
+
});
|
|
1266
|
+
return refs;
|
|
1267
|
+
}
|
|
1268
|
+
|
|
1205
1269
|
// Собрать рефы из выбранных персонажей и локации (sheet'ы)
|
|
1206
1270
|
function gatherPickedSheetRefs() {
|
|
1207
1271
|
const refs = [];
|
|
@@ -1715,6 +1779,14 @@ $('genSubmit').addEventListener('click', async () => {
|
|
|
1715
1779
|
if (mediaRefs.some(r => r.file === pr.file && r.boardHandle === pr.boardHandle)) continue;
|
|
1716
1780
|
mediaRefs.push(pr);
|
|
1717
1781
|
}
|
|
1782
|
+
// Incoming-edge источник для НОВОЙ ноды (anchor-drag из image/video ноды).
|
|
1783
|
+
// Юзер: «если в ноду 'входит' нода-картинка/видео — подавай её тоже как
|
|
1784
|
+
// референс». Для NEW ноды есть только pendingConnectionFrom (один источник);
|
|
1785
|
+
// существующая нода с множественными incoming обрабатывается в regenerateInto.
|
|
1786
|
+
for (const pf of _pendingConnectionFromRef()) {
|
|
1787
|
+
if (mediaRefs.some(r => r.file === pf.file && r.boardHandle === pf.boardHandle)) continue;
|
|
1788
|
+
mediaRefs.push(pf);
|
|
1789
|
+
}
|
|
1718
1790
|
const missing = mediaRefs.filter(r => !r.file || r.status === 'generating');
|
|
1719
1791
|
if (missing.length) {
|
|
1720
1792
|
alert('Эти ноды ещё не готовы: ' + missing.map(m => '@' + m.name).join(', '));
|
package/renderer/media.js
CHANGED
|
@@ -843,6 +843,17 @@ async function regenerateInto(node, kind, rawPrompt, opts = {}) {
|
|
|
843
843
|
if (refs.some(r => r.file === pr.file && r.boardHandle === pr.boardHandle)) continue;
|
|
844
844
|
refs.push(pr);
|
|
845
845
|
}
|
|
846
|
+
// Incoming-edge refs: для text-нод OpenRouter chat принимает image-входы.
|
|
847
|
+
// Юзер: «если в ноду 'входит' нода-картинка/видео — подавай её как реф».
|
|
848
|
+
// Видео для text-gen бесполезно (модели не глотают video frames в chat
|
|
849
|
+
// API), поэтому фильтруем по image.
|
|
850
|
+
if (typeof gatherIncomingMediaRefs === 'function') {
|
|
851
|
+
for (const ir of gatherIncomingMediaRefs(node.id)) {
|
|
852
|
+
if (ir.type !== 'image') continue;
|
|
853
|
+
if (refs.some(r => r.file === ir.file && r.boardHandle === ir.boardHandle)) continue;
|
|
854
|
+
refs.push(ir);
|
|
855
|
+
}
|
|
856
|
+
}
|
|
846
857
|
const missing = refs.filter(r => !r.file || r.status === 'generating');
|
|
847
858
|
if (missing.length) {
|
|
848
859
|
alert('Эти ноды ещё не готовы: ' + missing.map(m => '@' + (m.name || m.id)).join(', '));
|
|
@@ -875,6 +886,17 @@ async function regenerateInto(node, kind, rawPrompt, opts = {}) {
|
|
|
875
886
|
if (refs.some(r => r.file === pr.file && r.boardHandle === pr.boardHandle)) continue;
|
|
876
887
|
refs.push(pr);
|
|
877
888
|
}
|
|
889
|
+
// Incoming-edge refs (image/video → image/video нода). Юзер: «если в
|
|
890
|
+
// ноду 'входит' несколько других нод-картинок или 'нод-видео' —
|
|
891
|
+
// подавай их тоже как референсы». Граф уже выражает связь, не нужно
|
|
892
|
+
// дублировать через @name в промпте. Берём ВСЕ incoming media-источники
|
|
893
|
+
// — не только pending-anchor, как для NEW ноды.
|
|
894
|
+
if (typeof gatherIncomingMediaRefs === 'function') {
|
|
895
|
+
for (const ir of gatherIncomingMediaRefs(node.id)) {
|
|
896
|
+
if (refs.some(r => r.file === ir.file && r.boardHandle === ir.boardHandle)) continue;
|
|
897
|
+
refs.push(ir);
|
|
898
|
+
}
|
|
899
|
+
}
|
|
878
900
|
const missing = refs.filter(r => !r.file || r.status === 'generating');
|
|
879
901
|
if (missing.length) {
|
|
880
902
|
alert('Эти ноды ещё не готовы: ' + missing.map(m => '@' + (m.name || m.id)).join(', '));
|
package/skill/SKILL.md
CHANGED
|
@@ -395,6 +395,7 @@ open -a "Google Chrome" "http://localhost:17893/"
|
|
|
395
395
|
},
|
|
396
396
|
"status": "generating" | "draft" | "error", // отсутствует если нода готова
|
|
397
397
|
"error": "...",
|
|
398
|
+
"description": "опц. текст-аннотация под image/video нодой", // см. раздел «Описания нод»
|
|
398
399
|
"linkedBoard": { // опц., ссылка на другую доску
|
|
399
400
|
"kind": "episode" | "character" | "location",
|
|
400
401
|
"name": "Сцена 2" // имя целевой доски (handle резолвится в runtime)
|
|
@@ -479,6 +480,66 @@ open -a "Google Chrome" "http://localhost:17893/"
|
|
|
479
480
|
}
|
|
480
481
|
```
|
|
481
482
|
|
|
483
|
+
## Описания нод (image / video)
|
|
484
|
+
|
|
485
|
+
`image` и `video` ноды могут нести **текст-аннотацию** — `node.description`.
|
|
486
|
+
Это полноценная замена «соседней text-ноды»: вместо отдельной ноды юзер
|
|
487
|
+
может прикрепить описание прямо к картинке/видео, и оно «едет» вместе с
|
|
488
|
+
нодой по холсту.
|
|
489
|
+
|
|
490
|
+
**Формат** (поле в ноде):
|
|
491
|
+
```json5
|
|
492
|
+
{
|
|
493
|
+
"type": "image" | "video",
|
|
494
|
+
"file": "images/foo.jpg",
|
|
495
|
+
"x": 100, "y": 100, "width": 280, "height": 280,
|
|
496
|
+
"description": "Лес ночью, луна за облаками, тишина — общий план."
|
|
497
|
+
}
|
|
498
|
+
```
|
|
499
|
+
|
|
500
|
+
Поддерживается только для `type` ∈ `{image, video}`. Для `audio` и `text` —
|
|
501
|
+
игнорируется (у text — сам текст уже в `.md`, у audio — иногда нет визуального
|
|
502
|
+
места под caption).
|
|
503
|
+
|
|
504
|
+
**Где отображается**:
|
|
505
|
+
|
|
506
|
+
Описание рендерится **вне** ноды — как отдельный sibling-блок на холсте,
|
|
507
|
+
плотно под нижней границей ноды. Раньше пробовали инлайн внутри `.node`,
|
|
508
|
+
но `image/video` задают фиксированный aspect-ratio + `paint-containment`
|
|
509
|
+
у `.node` клипит overflow → текст ниже не был виден. Теперь:
|
|
510
|
+
|
|
511
|
+
- Позиция: `left = node.x`, `top = node.y + node.height + 4`
|
|
512
|
+
- Ширина = `node.width` (точно по ширине ноды)
|
|
513
|
+
- Высота = `auto` по контенту (текст любой длины помещается)
|
|
514
|
+
- Движется вместе с нодой при drag, resize, fit-to-media
|
|
515
|
+
|
|
516
|
+
**UI**:
|
|
517
|
+
- ПКМ на image/video ноде → «📝 Добавить описание…» (или «📝 Изменить
|
|
518
|
+
описание…» если уже есть). Открывает большую textarea-модалку (Cmd+Enter
|
|
519
|
+
— сохранить).
|
|
520
|
+
- Если описание пустое — sidecar-элемент удаляется из DOM (нода визуально
|
|
521
|
+
возвращается к «голой» картинке).
|
|
522
|
+
|
|
523
|
+
**Когда использовать описание vs соседнюю text-ноду**:
|
|
524
|
+
|
|
525
|
+
- **Описание** — короткая аннотация, привязанная к этому конкретному кадру:
|
|
526
|
+
«фон облака», «крупный план рук», «свет от свечи». Юзер видит контекст
|
|
527
|
+
картинки не уходя глазами в сторону.
|
|
528
|
+
- **Отдельная text-нода** — самостоятельный смысловой блок (диалог, сценарий,
|
|
529
|
+
заметка), который должен быть отдельной сущностью на холсте (рисуем
|
|
530
|
+
связи, переставляем независимо от кадра).
|
|
531
|
+
|
|
532
|
+
**Через CLI / правку scene.json**:
|
|
533
|
+
|
|
534
|
+
```bash
|
|
535
|
+
# Добавить описание к существующей ноде (правка scene.json напрямую):
|
|
536
|
+
# найди ноду по name или id, добавь поле "description"
|
|
537
|
+
```
|
|
538
|
+
|
|
539
|
+
Прямой CLI-флаг для description пока **не реализован**. Если нужно массово
|
|
540
|
+
проставить описания — правь `scene.json` через `jq` или скриптом, редактор
|
|
541
|
+
подхватит при следующем открытии доски.
|
|
542
|
+
|
|
482
543
|
## Ссылки на доски (link-to-scene)
|
|
483
544
|
|
|
484
545
|
Нода может содержать ссылку на другую сцену/персонажа/локацию того же
|
|
@@ -497,7 +558,15 @@ open -a "Google Chrome" "http://localhost:17893/"
|
|
|
497
558
|
- ПКМ на ноде → «🔗 Сделать ссылку на сцену…» (picker всех досок проекта,
|
|
498
559
|
кроме текущей). Меняется на «🔗 Перейти → <имя>» + «⛓️💥 Убрать ссылку»
|
|
499
560
|
когда ссылка уже есть.
|
|
500
|
-
- На ноде со ссылкой —
|
|
561
|
+
- На ноде со ссылкой — **большая явная кнопка «Перейти → <имя сцены>»**
|
|
562
|
+
внизу ноды (чуть выше нижней границы, абсолютно позиционирована поверх
|
|
563
|
+
контента). Раньше был маленький значок 🔗 в правом-верхнем углу, но юзер
|
|
564
|
+
попросил убрать в пользу явной кнопки — она читабельнее на маленьких
|
|
565
|
+
preview-картинках. Для `drawing`-нод (стрелок) кнопка скрыта — она там
|
|
566
|
+
визуально странная.
|
|
567
|
+
- В share-модалке (расшарить проект) рядом с «Расшарить» есть кнопка
|
|
568
|
+
«↗ Перейти» — открывает share-ссылку в новой вкладке для быстрого
|
|
569
|
+
превью что увидит получатель.
|
|
501
570
|
- Дабл-клик на ноде = переход (перебивает обычное действие dblclick типа
|
|
502
571
|
fullscreen-просмотра).
|
|
503
572
|
- Если целевая доска переименована/удалена — алерт «не найдена».
|
|
@@ -508,6 +577,38 @@ open -a "Google Chrome" "http://localhost:17893/"
|
|
|
508
577
|
структура). Но переименование целевой доски ломает ссылку — нужно
|
|
509
578
|
обновлять вручную в scene.json или пересоздавать через ПКМ.
|
|
510
579
|
|
|
580
|
+
**Через CLI**: прямого `kingkont link-node` пока нет — правь
|
|
581
|
+
`scene.json` напрямую, добавь к ноде поле `linkedBoard`.
|
|
582
|
+
|
|
583
|
+
## Connections как авто-референсы (incoming edges)
|
|
584
|
+
|
|
585
|
+
Если в image/video/text-ноду **«входит» другая нода** (есть `connection`
|
|
586
|
+
где `to === thisNode.id`) — её содержимое **автоматически** идёт в refs
|
|
587
|
+
при генерации/регенерации этой ноды. Юзер не обязан дублировать `@name`
|
|
588
|
+
в промпте — граф уже выражает связь.
|
|
589
|
+
|
|
590
|
+
Правила (применяются при genSubmit и regenerateInto):
|
|
591
|
+
- Источник `type: image` → добавляется как image-реф (всегда).
|
|
592
|
+
- Источник `type: video` → добавляется как video-реф для image/video-нод;
|
|
593
|
+
для text-нод **пропускается** (OpenRouter chat API не глотает video).
|
|
594
|
+
- Источник без `file` (ещё не сгенерирован / draft) → пропускается, чтобы
|
|
595
|
+
не блокировать gen alert'ом «нода ещё не готова».
|
|
596
|
+
- Источник `type: text|audio|drawing|label` → не добавляется в refs
|
|
597
|
+
(для text-источников юзер должен явно писать `[@name]` в промпте,
|
|
598
|
+
это позволяет контролировать ГДЕ инлайнится содержимое).
|
|
599
|
+
|
|
600
|
+
Кейсы где это даёт «бесплатные» референсы:
|
|
601
|
+
- **New нода через anchor-drag**: тянешь линию от image-ноды на пустое
|
|
602
|
+
место → меню → «Сгенерить картинку» → новая нода получает источник как
|
|
603
|
+
ref без писанины в промпте.
|
|
604
|
+
- **Regen существующей ноды с несколькими incoming**: связал draft-image с
|
|
605
|
+
тремя image-источниками (характер + локация + propsheet), нажал
|
|
606
|
+
«↻ Перегенерировать» — все три попадают в refs автоматически.
|
|
607
|
+
|
|
608
|
+
Если нужно НЕ использовать какой-то incoming источник как реф — удали
|
|
609
|
+
саму connection (`scene.json → connections[]`) или временно переименуй
|
|
610
|
+
источник так чтобы он не нашёлся (или unfile его — `delete n.file`).
|
|
611
|
+
|
|
511
612
|
## Файловые операции
|
|
512
613
|
|
|
513
614
|
- **Удаление ноды**: файл переезжает в `<root>/_deleted/<уникальное-имя>`.
|