feat: referentiel projets dynamique + validation nommage by design
Durcit la convention de nommage des projets (dérive constatée : 'Sliding Automation', 'code_versioning'... au lieu des formes canoniques). - trilium_api.py : projets_canoniques() lit le référentiel = valeurs du label projet sur les notes de type=projet (source unique, pas de constante en dur). Note-projet CodeVersioning créée (manquait). - mcp_server.py : _valider_projet() branché dans les 6 tools de création (add_decision/history/backlog, new_conversation, create_entite, add_skill). Refuse un projet non canonique (suggestion si faute) ou inconnu (renvoi au processus de création de projet). Ne verrouille pas si référentiel illisible. - lint_audit.py : VAL-nommage aligné sur le référentiel (attrape casse, espace ET snake_case ; l'ancien 'contient un espace' ratait code_versioning). - Données : 79 notes ré-étiquetées vers les 3 formes canoniques. Quality by design : l'erreur de nommage devient impossible à l'écriture, le Lint n'est plus que le filet de sécurité.
This commit is contained in:
@@ -0,0 +1,161 @@
|
||||
import re
|
||||
import sys
|
||||
from typing import Optional
|
||||
|
||||
import requests
|
||||
from loguru import logger
|
||||
|
||||
from .version import __version__
|
||||
|
||||
|
||||
class WEBAPI:
|
||||
__version__ = __version__
|
||||
|
||||
def __init__(self, server_url: str, sid: Optional[str] = None, _csrf: Optional[str] = None,
|
||||
csrf_token: Optional[str] = None):
|
||||
if sys.version_info < (3, 9):
|
||||
print(
|
||||
(
|
||||
f'You are using Python {sys.version_info.major}.{sys.version_info.minor}'
|
||||
', 3.9+ is required.'
|
||||
),
|
||||
file=sys.stderr,
|
||||
)
|
||||
|
||||
self.server_url = server_url
|
||||
self.sid: str = sid
|
||||
self._csrf = _csrf
|
||||
self.csrf_token = csrf_token
|
||||
|
||||
def get_cookie(self) -> dict:
|
||||
return {
|
||||
'_csrf': self._csrf,
|
||||
'trilium.sid': self.sid,
|
||||
'trilium-device': 'desktop',
|
||||
# Trilium next uses this cookie
|
||||
'trilium-csrf': self.csrf_token,
|
||||
}
|
||||
|
||||
def get_headers(self) -> dict:
|
||||
return {
|
||||
'x-csrf-token': self.csrf_token,
|
||||
}
|
||||
|
||||
def refresh_csrf_token(self) -> str:
|
||||
# Classic Trilium 0.63.7, the csrfToken is from `/` endpoint
|
||||
url = f'{self.server_url}/'
|
||||
res = requests.get(url, cookies=self.get_cookie())
|
||||
csrf_token_match = re.search(r"csrfToken:\s*'([^']+)'", res.text)
|
||||
|
||||
if csrf_token_match:
|
||||
csrf_token = csrf_token_match.group(1)
|
||||
logger.info(f"Extracted csrfToken: {csrf_token}")
|
||||
self.csrf_token = csrf_token
|
||||
return csrf_token
|
||||
else:
|
||||
logger.info("csrfToken not found.")
|
||||
# Trilium next, the csrfToken is from `/bootstrap` endpoint
|
||||
logger.info("Trying to extract csrfToken from /bootstrap endpoint...")
|
||||
url = f'{self.server_url}/bootstrap'
|
||||
res = requests.get(url, cookies=self.get_cookie())
|
||||
csrf_token = res.json()['csrfToken']
|
||||
logger.info(f"Extracted csrfToken: {csrf_token}")
|
||||
self.csrf_token = csrf_token
|
||||
return csrf_token
|
||||
|
||||
def login(self, password: str) -> Optional[str]:
|
||||
"""
|
||||
mimic web login
|
||||
"""
|
||||
url = f'{self.server_url}/login'
|
||||
|
||||
data = {'password': password}
|
||||
|
||||
# login process is in 2-step
|
||||
# 1. 302 set-cookie trilium.sid
|
||||
# 2. 200 set-cookie _csrf
|
||||
# single requests will not work, need to use session
|
||||
session = requests.Session()
|
||||
res = session.post(url, data=data, allow_redirects=True)
|
||||
for cookie in session.cookies:
|
||||
logger.info(f"{cookie.name}: {cookie.value}")
|
||||
|
||||
if res.status_code == 200:
|
||||
self.sid = session.cookies.get('trilium.sid')
|
||||
self._csrf = session.cookies.get('_csrf')
|
||||
self.refresh_csrf_token()
|
||||
return self.sid
|
||||
else:
|
||||
logger.info(res.text)
|
||||
return ''
|
||||
|
||||
def logout(self, sid: Optional[str] = None) -> bool:
|
||||
"""
|
||||
mimic web logout
|
||||
"""
|
||||
|
||||
if not sid:
|
||||
sid = self.sid
|
||||
|
||||
if not sid:
|
||||
return False
|
||||
|
||||
url = f'{self.server_url}/logout'
|
||||
res = requests.post(url, headers=self.get_headers(), cookies=self.get_cookie())
|
||||
|
||||
if res.status_code == 200:
|
||||
logger.info('logout successfully')
|
||||
return True
|
||||
return False
|
||||
|
||||
def get_note_content(self, note_id):
|
||||
url = f'{self.server_url}/api/notes/{note_id}/blob'
|
||||
res = requests.get(url, cookies=self.get_cookie())
|
||||
return res.json()['content']
|
||||
|
||||
def update_note_content(self, note_id, content):
|
||||
url = f'{self.server_url}/api/notes/{note_id}/data'
|
||||
data = {'content': content}
|
||||
res = requests.put(url, headers=self.get_headers(), cookies=self.get_cookie(), json=data)
|
||||
if res.status_code == 204:
|
||||
return True
|
||||
return False
|
||||
|
||||
def share_note(self, note_id: str):
|
||||
url = f'{self.server_url}/api/notes/{note_id}/clone-to-note/_share'
|
||||
|
||||
res = requests.put(url, headers=self.get_headers(), cookies=self.get_cookie())
|
||||
logger.info(res.json())
|
||||
if res.json():
|
||||
return True
|
||||
return False
|
||||
|
||||
def cancel_share_note(self, note_id: str):
|
||||
url = f'{self.server_url}/api/branches/_share_{note_id}?taskId=no-progress-reporting'
|
||||
|
||||
res = requests.delete(url, headers=self.get_headers(), cookies=self.get_cookie())
|
||||
logger.info(res.json())
|
||||
|
||||
if res.status_code == 200: # DELETE 请求通常返回 200 表示成功
|
||||
return True
|
||||
return False
|
||||
|
||||
def enter_protected_session(self, password: str):
|
||||
url = f'{self.server_url}/api/login/protected'
|
||||
data = {'password': password}
|
||||
res = requests.post(url, headers=self.get_headers(), cookies=self.get_cookie(), json=data)
|
||||
try:
|
||||
if res.json()['success'] == True:
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
except:
|
||||
return False
|
||||
|
||||
def leave_protected_session(self):
|
||||
url = f'{self.server_url}/api/logout/protected'
|
||||
res = requests.post(url, headers=self.get_headers(), cookies=self.get_cookie())
|
||||
if res.status_code == 204:
|
||||
logger.info('leave protected session successfully')
|
||||
return True
|
||||
return False
|
||||
Reference in New Issue
Block a user