Исправлена сборка плагина и удалён нерабочий код

- Исправлен маппинг статусов Logseq → PPDB (теперь отправляет TODO/DOING/DONE вместо new/in_progress/completed)
- Удалён нерабочий код setupConfig (использовал несуществующий API)
- Исправлена сборка: теперь создаётся index.html + assets/ вместо одного index.js
- Исправлено чтение свойств блоков (проверка source-id/sourceId/source_id)
- Добавлен скрипт deploy.sh для деплоя в ~/.logseq/plugins/
This commit is contained in:
2026-01-16 00:27:24 +03:00
parent 1b9da0ded4
commit d59ad3c677
9 changed files with 1176 additions and 309 deletions

47
CLAUDE.md Normal file
View File

@@ -0,0 +1,47 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Описание проекта
TODO Everywhere — Logseq плагин для синхронизации задач из внешних источников (PPDB, Gitea) в Logseq.
## Команды сборки
```bash
npm install # Установка зависимостей
npm run build # Сборка для продакшена (dist/index.js)
npm run dev # Режим разработки с автопересборкой
```
## Архитектура
Плагин состоит из одного файла `src/index.ts`, который:
1. **Настройка config.edn** (`setupConfig`) — при первой установке автоматически добавляет Datalog-запросы для PPDB и Gitea в конфиг Logseq
2. **Синхронизация PPDB** (`syncPPDBTasks`) — получает задачи через `/api/logseq/tasks` и создаёт страницу `PPDB - TODO`
3. **Синхронизация Gitea** (`syncGiteaTasks`) — получает issues через Gitea API и создаёт страницы `Gitea - {name} - TODO`
4. **Запись в страницу** (`writeToPage`) — конвертирует данные в формат Logseq блоков с TODO/DOING/DONE статусами
### Ключевые интерфейсы
- `PPDBTask` — структура задачи из PPDB API (включает `logseq_priority` и `logseq_status`)
- `GiteaIssue` — структура issue из Gitea API
- `GiteaSource` — настройки подключения к Gitea репозиторию
### Конвертация данных
- Приоритеты: critical/high → [#A], normal → [#B], low → [#C]
- Статусы: new → TODO, in_progress → DOING, completed/rejected → DONE
- Gitea labels определяют приоритет и тип (bug/feature)
## Настройки плагина
Настройки хранятся через `logseq.settings` и включают:
- `ppdbUrl`, `ppdbApiKey` — подключение к PPDB
- `giteaSources` — JSON массив Gitea репозиториев
- `configVersion` — версия конфига для отслеживания миграций
## Связанный API
PPDB endpoint `/api/logseq/tasks` (в проекте ppdb-site) требует `LOGSEQ_API_KEY` в переменных окружения.

17
deploy.sh Executable file
View File

@@ -0,0 +1,17 @@
#!/bin/bash
# Скрипт деплоя плагина в Logseq
DEST="$HOME/.logseq/plugins/logseq-todo-everywhere"
# Удаляем старую версию
rm -rf "$DEST"
# Создаём папки
mkdir -p "$DEST/dist"
# Копируем файлы
cp package.json "$DEST/"
cp icon.svg "$DEST/"
cp -r dist/* "$DEST/dist/"
echo "Плагин скопирован в $DEST"

259
dist/assets/index-jnn8zDEO.js vendored Normal file

File diff suppressed because one or more lines are too long

11
dist/index.html vendored Normal file
View File

@@ -0,0 +1,11 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>TODO Everywhere</title>
<script type="module" crossorigin src="./assets/index-jnn8zDEO.js"></script>
</head>
<body>
<div id="app"></div>
</body>
</html>

42
dist/index.js vendored
View File

@@ -1,42 +0,0 @@
(function(){"use strict";const w=`{:title "PPDB"
:query [:find (pull ?h [:db/id :block/uuid :block/content :block/marker :block/priority :block/created-at :block/updated-at {:block/page [:block/journal-day]}])
:where
[?p :block/original-name "PPDB - TODO"]
[?h :block/marker ?marker]
[(contains? #{"NOW" "DOING" "TODO"} ?marker)]
[?h :block/page ?p]
[?p :block/journal? false]]
:result-transform (fn [result]
(sort-by (fn [h]
[(get h :block/priority "Z")
(or (get h :block/created-at) 999999999999999)]) result))
:collapsed? true}`,P=`{:title "Gitea"
:query [:find (pull ?h [:db/id :block/uuid :block/content :block/marker :block/priority :block/created-at :block/updated-at {:block/page [:block/journal-day :block/original-name]}])
:where
[?p :block/original-name ?name]
[(clojure.string/starts-with? ?name "Gitea -")]
[?h :block/marker ?marker]
[(contains? #{"NOW" "DOING" "TODO"} ?marker)]
[?h :block/page ?p]
[?p :block/journal? false]]
:result-transform (fn [result]
(sort-by (fn [h]
[(get h :block/priority "Z")
(or (get h :block/created-at) 999999999999999)]) result))
:collapsed? true}`,O=[{key:"ppdbEnabled",type:"boolean",default:!1,title:"PPDB - Включить синхронизацию",description:"Синхронизировать задачи из PPDB"},{key:"ppdbUrl",type:"string",default:"https://ppdb.linux-gaming.ru",title:"PPDB - URL сервера",description:"Базовый URL PPDB API"},{key:"ppdbApiKey",type:"string",default:"",title:"PPDB - API ключ",description:"Ключ для доступа к API"},{key:"ppdbPageName",type:"string",default:"PPDB - TODO",title:"PPDB - Имя страницы",description:"Название страницы для задач PPDB"},{key:"ppdbCategories",type:"string",default:"",title:"PPDB - Категории (через запятую)",description:"Фильтр по категориям: ppdb, linux-gaming, portproton, portprotonqt. Пусто = все"},{key:"giteaEnabled",type:"boolean",default:!1,title:"Gitea - Включить синхронизацию",description:"Синхронизировать issues из Gitea"},{key:"giteaSources",type:"string",default:"[]",title:"Gitea - Источники (JSON)",description:'JSON массив источников: [{"name":"MyRepo","url":"https://gitea.example.com","token":"xxx","owner":"user","repo":"repo","enabled":true}]'},{key:"syncOnStartup",type:"boolean",default:!0,title:"Синхронизация при запуске",description:"Автоматически синхронизировать при запуске Logseq"},{key:"syncInterval",type:"number",default:0,title:"Интервал синхронизации (минуты)",description:"0 = только вручную"},{key:"configVersion",type:"number",default:0,title:"Версия конфигурации (не изменять)",description:"Внутренняя переменная для отслеживания настройки config.edn"}],f=1;async function $(){try{const n=await logseq.App.getCurrentGraph();if(!n?.path)return console.error("Cannot get graph path"),!1;const s=`${n.path}/logseq/config.edn`,t=await window.logseq.FileStorage.getItem(s);if(!t)return console.error("Cannot read config.edn"),!1;let a=!1,e=t;if(!t.includes(':title "PPDB"')){const o=h(e);o>0&&(e=e.slice(0,o)+`
`+w+e.slice(o),a=!0)}if(!e.includes(':title "Gitea"')){const o=h(e);o>0&&(e=e.slice(0,o)+`
`+P+e.slice(o),a=!0)}return e.includes('"PPDB - TODO"')||(e=e.replace(/\(not \[\(contains\? #\{([^}]*)"РЕД СОФТ - TODO"([^}]*)\} \?name\)\]\)/g,'(not [(contains? #{$1"РЕД СОФТ - TODO" "PPDB - TODO"$2} ?name)])'),a=!0),e.includes('clojure.string/starts-with? ?name "Gitea -"')||(e=e.replace(/(\(not \[\(contains\? #\{[^}]*"PPDB - TODO"[^}]*\} \?name\)\]\))(\s*)(\[?\?h :block\/marker)/g,`$1
(not [(clojure.string/starts-with? ?name "Gitea -")])$2$3`),a=!0),e.includes('"PPDB - TODO"')||(e=e.replace(/:favorites \[([^\]]*)"РЕД СОФТ - TODO"([^\]]*)\]/,':favorites [$1"РЕД СОФТ - TODO" "PPDB - TODO"$2]'),a=!0),a?(await window.logseq.FileStorage.setItem(s,e),console.log("config.edn updated successfully"),!0):!1}catch(n){return console.error("Error setting up config:",n),!1}}function h(n){const s=[/:title "Остальные дела"/,/:title "Остальные"/];for(const a of s){const e=n.match(a);if(e&&e.index){let o=e.index;for(;o>0&&n[o]!=="{";)o--;return o}}const t=n.search(/\]\s*\}\s*;;\s*Add custom commands/);return t>0?t:-1}async function D(){logseq.useSettingsSchema(O),(logseq.settings?.configVersion||0)<f&&(logseq.UI.showMsg("TODO Everywhere: настройка config.edn...","info"),await $()&&(logseq.updateSettings({configVersion:f}),logseq.UI.showMsg("config.edn обновлён. Перезапустите Logseq для применения изменений.","success"))),logseq.Editor.registerSlashCommand("sync-tasks",async()=>{await p()}),logseq.Editor.registerSlashCommand("sync-ppdb",async()=>{await y()}),logseq.Editor.registerSlashCommand("sync-gitea",async()=>{await m()}),logseq.App.registerUIItem("toolbar",{key:"todo-everywhere-sync",template:`
<a class="button" data-on-click="syncAll" title="Sync External Tasks">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M21.5 2v6h-6M2.5 22v-6h6M2 11.5a10 10 0 0 1 18.8-4.3M22 12.5a10 10 0 0 1-18.8 4.2"/>
</svg>
</a>
`}),logseq.provideModel({async syncAll(){await p()}}),logseq.settings?.syncOnStartup&&setTimeout(()=>p(),3e3);const s=logseq.settings?.syncInterval;s>0&&setInterval(()=>p(),s*60*1e3),console.log("TODO Everywhere plugin loaded")}async function p(){logseq.UI.showMsg("Синхронизация задач...","info");try{const n=[];if(logseq.settings?.ppdbEnabled){const s=await y();n.push(s)}if(logseq.settings?.giteaEnabled){const s=await m();n.push(s)}n.length===0?logseq.UI.showMsg("Нет включенных источников","warning"):logseq.UI.showMsg(n.join(`
`),"success")}catch(n){console.error("Sync error:",n),logseq.UI.showMsg(`Ошибка синхронизации: ${n}`,"error")}}async function y(){const n=logseq.settings?.ppdbUrl,s=logseq.settings?.ppdbApiKey,t=logseq.settings?.ppdbPageName||"PPDB - TODO",a=(logseq.settings?.ppdbCategories||"").split(",").map(u=>u.trim()).filter(Boolean);if(!n||!s)throw new Error("PPDB: не настроены URL или API ключ");let e=`${n}/api/logseq/tasks?api_key=${s}`;a.length===1&&(e+=`&category=${a[0]}`);const o=await fetch(e);if(!o.ok)throw new Error(`PPDB API error: ${o.status}`);let l=(await o.json()).tasks;a.length>1&&(l=l.filter(u=>a.includes(u.category)));const i=B(l);return await b(t,i),`PPDB: ${l.length} задач`}async function m(){const n=logseq.settings?.giteaSources||"[]";let s;try{s=JSON.parse(n)}catch{throw new Error("Gitea: неверный формат JSON источников")}const t=s.filter(e=>e.enabled);if(t.length===0)return"Gitea: нет активных источников";const a=[];for(const e of t)try{const o=await q(e),r=`Gitea - ${e.name} - TODO`,l=T(o,e);await b(r,l),a.push(`${e.name}: ${o.length}`)}catch(o){console.error(`Gitea ${e.name} error:`,o),a.push(`${e.name}: ошибка`)}return`Gitea: ${a.join(", ")}`}async function q(n){const s=`${n.url}/api/v1/repos/${n.owner}/${n.repo}/issues?state=open&type=issues`,t=await fetch(s,{headers:{Authorization:`token ${n.token}`,"Content-Type":"application/json"}});if(!t.ok)throw new Error(`Gitea API error: ${t.status}`);return await t.json()}function B(n){const s=[];s.push("source:: PPDB"),s.push(`synced:: ${new Date().toISOString()}`),s.push("");const t={TODO:[],DOING:[],DONE:[]};for(const e of n){const o=e.logseq_status;t[o]&&t[o].push(e)}const a={A:0,B:1,C:2};for(const e of Object.keys(t))t[e].sort((o,r)=>(a[o.logseq_priority]||1)-(a[r.logseq_priority]||1));for(const e of[...t.DOING,...t.TODO,...t.DONE]){const o=e.logseq_priority?`[#${e.logseq_priority}] `:"",r=e.task_type?`#${e.task_type} `:"",l=`#${e.category} `;let i=`- ${e.logseq_status} ${o}${r}${l}${e.title}`;if(s.push(i),s.push(` id:: ppdb-${e.id}`),e.url&&s.push(` url:: ${e.url}`),e.description){const u=e.description.split(`
`).filter(c=>c.trim());for(const c of u)s.push(` - ${c}`)}e.admin_comment&&s.push(` - **Комментарий:** ${e.admin_comment}`),e.assigned_to_username&&s.push(` assignee:: ${e.assigned_to_username}`),s.push(` author:: ${e.author_username}`),s.push(` created:: ${e.created_at.split("T")[0]}`)}return s.join(`
`)}function T(n,s){const t=[];t.push("source:: Gitea"),t.push(`repo:: ${s.owner}/${s.repo}`),t.push(`synced:: ${new Date().toISOString()}`),t.push("");const a=r=>{const l=r.labels.map(i=>i.name.toLowerCase());return l.some(i=>i.includes("critical")||i.includes("urgent")||i.includes("p0"))||l.some(i=>i.includes("high")||i.includes("important")||i.includes("p1"))?"A":l.some(i=>i.includes("low")||i.includes("minor")||i.includes("p3"))?"C":"B"},e=r=>{const l=r.labels.map(i=>i.name.toLowerCase());return l.some(i=>i.includes("bug")||i.includes("fix"))?"bug":l.some(i=>i.includes("feature")||i.includes("enhancement"))?"feature":""},o={A:0,B:1,C:2};n.sort((r,l)=>(o[a(r)]||1)-(o[a(l)]||1));for(const r of n){const l=a(r),i=e(r),u=i?`#${i} `:"",c=r.labels.map(g=>`#${g.name.replace(/\s+/g,"-")}`).join(" ");let k=`- TODO [#${l}] ${u}#${r.number} ${r.title}`;if(c&&(k+=` ${c}`),t.push(k),t.push(` id:: gitea-${s.name}-${r.number}`),t.push(` url:: ${r.html_url}`),r.assignee&&t.push(` assignee:: ${r.assignee.login}`),r.milestone&&t.push(` milestone:: ${r.milestone.title}`),t.push(` created:: ${r.created_at.split("T")[0]}`),r.body){const g=r.body.split(`
`).filter(d=>d.trim()).slice(0,5);for(const d of g)t.push(` - ${d.substring(0,200)}`);r.body.split(`
`).length>5&&t.push(" - ...")}}return t.join(`
`)}async function b(n,s){let t=await logseq.Editor.getPage(n);if(t||(t=await logseq.Editor.createPage(n,{},{redirect:!1})),!t)throw new Error(`Не удалось создать страницу ${n}`);const a=await logseq.Editor.getPageBlocksTree(n);for(const o of a)await logseq.Editor.removeBlock(o.uuid);const e=s.split(`
`);if(e.length>0){const o=await logseq.Editor.insertBlock(t.uuid,e[0],{sibling:!1});if(o){let r=o.uuid;for(let l=1;l<e.length;l++){const i=e[l];if(!i.trim())continue;if(i.search(/\S/)<=0||i.startsWith("-")){const c=await logseq.Editor.insertBlock(r,i.replace(/^-\s*/,""),{sibling:!0});c&&(r=c.uuid)}else{const c=i.trim();c.includes("::")?await logseq.Editor.upsertBlockProperty(r,c.split("::")[0].trim(),c.split("::")[1].trim()):c.startsWith("-")&&await logseq.Editor.insertBlock(r,c.replace(/^-\s*/,""),{sibling:!1})}}}}}logseq.ready(D).catch(console.error)})();

11
index.html Normal file
View File

@@ -0,0 +1,11 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>TODO Everywhere</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="./src/index.ts"></script>
</body>
</html>

View File

@@ -2,7 +2,7 @@
"name": "logseq-todo-everywhere", "name": "logseq-todo-everywhere",
"version": "1.0.0", "version": "1.0.0",
"description": "Sync tasks from external sources (PPDB, Gitea) to Logseq", "description": "Sync tasks from external sources (PPDB, Gitea) to Logseq",
"main": "dist/index.js", "main": "dist/index.html",
"scripts": { "scripts": {
"dev": "npx vite build --watch", "dev": "npx vite build --watch",
"build": "npx vite build" "build": "npx vite build"

File diff suppressed because it is too large Load Diff

View File

@@ -5,20 +5,6 @@ export default defineConfig({
plugins: [logseqPlugin()], plugins: [logseqPlugin()],
build: { build: {
target: 'esnext', target: 'esnext',
minify: 'esbuild', minify: 'esbuild'
lib: {
entry: 'src/index.ts',
formats: ['iife'],
name: 'TodoEverywhere',
fileName: () => 'index.js'
},
rollupOptions: {
external: ['@logseq/libs'],
output: {
globals: {
'@logseq/libs': 'logseq'
}
}
}
} }
}) })