Alipay-ватчер: поллинг capability Stripe до аппрува, автолинк $100 card+alipay (e-261005142309)
This commit is contained in:
parent
c5c3e8fbaa
commit
e7bf208bd3
32 changed files with 2885 additions and 11 deletions
BIN
artifacts/dashboard-channels-t-d8112b-full.png
Normal file
BIN
artifacts/dashboard-channels-t-d8112b-full.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 141 KiB |
BIN
artifacts/dashboard-channels-t-d8112b.png
Normal file
BIN
artifacts/dashboard-channels-t-d8112b.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 135 KiB |
BIN
artifacts/imagegen-e2e-20261003.png
Normal file
BIN
artifacts/imagegen-e2e-20261003.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 954 KiB |
598
artifacts/landing-simbiot-opencode.html
Normal file
598
artifacts/landing-simbiot-opencode.html
Normal file
|
|
@ -0,0 +1,598 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="ru">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Simbiot — Ритмичный ИИ-харнес кооператива 42</title>
|
||||||
|
<style>
|
||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--accent: #2563eb;
|
||||||
|
--bg-light: #ffffff;
|
||||||
|
--bg-dark: #1e293b;
|
||||||
|
--text-light: #1e293b;
|
||||||
|
--text-dark: #f8fafc;
|
||||||
|
--gray: #64748b;
|
||||||
|
--border: #e2e8f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
* {
|
||||||
|
animation: none !important;
|
||||||
|
transition: none !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||||
|
line-height: 1.6;
|
||||||
|
color: var(--text-light);
|
||||||
|
background: var(--bg-light);
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
max-width: 1100px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 0 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Animations */
|
||||||
|
@keyframes fadeInUp {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(30px);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.fade-in-up {
|
||||||
|
opacity: 0;
|
||||||
|
animation: fadeInUp 0.8s ease-out forwards;
|
||||||
|
}
|
||||||
|
|
||||||
|
.delay-1 { animation-delay: 0.2s; }
|
||||||
|
.delay-2 { animation-delay: 0.4s; }
|
||||||
|
.delay-3 { animation-delay: 0.6s; }
|
||||||
|
|
||||||
|
/* Typography */
|
||||||
|
h1, h2, h3 {
|
||||||
|
font-family: Georgia, serif;
|
||||||
|
font-weight: 600;
|
||||||
|
line-height: 1.2;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-size: 2.5rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
font-size: 2rem;
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section {
|
||||||
|
padding: 80px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section:nth-child(even) {
|
||||||
|
background: var(--bg-dark);
|
||||||
|
color: var(--text-dark);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 12px 24px;
|
||||||
|
background: var(--accent);
|
||||||
|
color: white;
|
||||||
|
text-decoration: none;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-weight: 500;
|
||||||
|
transition: background 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn:hover {
|
||||||
|
background: #1d4ed8;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Hero Section */
|
||||||
|
.hero {
|
||||||
|
text-align: center;
|
||||||
|
padding: 120px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero h1 {
|
||||||
|
font-size: 2.2rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero p {
|
||||||
|
font-size: 1.2rem;
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
color: var(--gray);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Problem/Solution Blocks */
|
||||||
|
.problem-solution {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||||||
|
gap: 40px;
|
||||||
|
margin-top: 60px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.block {
|
||||||
|
padding: 30px;
|
||||||
|
background: rgba(255, 255, 255, 0.05);
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.block h3 {
|
||||||
|
color: var(--accent);
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.block p {
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* What's Included */
|
||||||
|
.included-list {
|
||||||
|
list-style: none;
|
||||||
|
margin-top: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.included-list li {
|
||||||
|
padding: 15px 0;
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.included-list li:before {
|
||||||
|
content: "✓";
|
||||||
|
color: var(--accent);
|
||||||
|
font-weight: bold;
|
||||||
|
margin-right: 15px;
|
||||||
|
font-size: 1.2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* How It Works */
|
||||||
|
.steps {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||||||
|
gap: 30px;
|
||||||
|
margin-top: 60px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.step {
|
||||||
|
text-align: center;
|
||||||
|
padding: 30px;
|
||||||
|
background: rgba(37, 99, 235, 0.05);
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 1px solid rgba(37, 99, 235, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.step-number {
|
||||||
|
width: 50px;
|
||||||
|
height: 50px;
|
||||||
|
background: var(--accent);
|
||||||
|
color: white;
|
||||||
|
border-radius: 50%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 1.5rem;
|
||||||
|
font-weight: bold;
|
||||||
|
margin: 0 auto 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Mockups */
|
||||||
|
.mockup {
|
||||||
|
margin: 40px 0;
|
||||||
|
padding: 30px;
|
||||||
|
background: rgba(255, 255, 255, 0.05);
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dashboard {
|
||||||
|
background: #f8fafc;
|
||||||
|
padding: 20px;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-family: monospace;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dashboard-header {
|
||||||
|
background: var(--accent);
|
||||||
|
color: white;
|
||||||
|
padding: 10px 15px;
|
||||||
|
border-radius: 6px 6px 0 0;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dashboard-content {
|
||||||
|
padding: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-item {
|
||||||
|
padding: 10px;
|
||||||
|
border-bottom: 1px solid #e2e8f0;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-status {
|
||||||
|
padding: 4px 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
}
|
||||||
|
.status-pending { background: #fef3c7; color: #92400e; }
|
||||||
|
.status-progress { background: #dbeafe; color: #1e40af; }
|
||||||
|
.status-done { background: #d1fae5; color: #065f46; }
|
||||||
|
|
||||||
|
.telegram-chat {
|
||||||
|
background: #f8fafc;
|
||||||
|
padding: 20px;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message {
|
||||||
|
margin: 10px 0;
|
||||||
|
max-width: 70%;
|
||||||
|
padding: 12px 16px;
|
||||||
|
border-radius: 18px;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message.user {
|
||||||
|
background: var(--accent);
|
||||||
|
color: white;
|
||||||
|
margin-left: auto;
|
||||||
|
border-bottom-right-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message.bot {
|
||||||
|
background: #e2e8f0;
|
||||||
|
color: var(--text-light);
|
||||||
|
border-bottom-left-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Social Proof */
|
||||||
|
.social-proof {
|
||||||
|
text-align: center;
|
||||||
|
padding: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stats {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
|
||||||
|
gap: 30px;
|
||||||
|
margin-top: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat {
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-number {
|
||||||
|
font-size: 2rem;
|
||||||
|
font-weight: bold;
|
||||||
|
color: var(--accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Pricing */
|
||||||
|
.pricing {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||||||
|
gap: 30px;
|
||||||
|
margin-top: 60px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.plan {
|
||||||
|
padding: 40px;
|
||||||
|
background: rgba(255, 255, 255, 0.05);
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.plan h3 {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
color: var(--accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.plan-price {
|
||||||
|
font-size: 2.5rem;
|
||||||
|
font-weight: bold;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.plan-features {
|
||||||
|
list-style: none;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.plan-features li {
|
||||||
|
padding: 8px 0;
|
||||||
|
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Footer */
|
||||||
|
.footer {
|
||||||
|
background: var(--bg-dark);
|
||||||
|
color: var(--text-dark);
|
||||||
|
padding: 40px 0;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-content {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||||||
|
gap: 40px;
|
||||||
|
margin-bottom: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-section h4 {
|
||||||
|
margin-bottom: 15px;
|
||||||
|
color: var(--accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-section p, .footer-section a {
|
||||||
|
color: #94a3b8;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-section a:hover {
|
||||||
|
color: var(--accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Responsive */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
h1 { font-size: 1.8rem; }
|
||||||
|
h2 { font-size: 1.5rem; }
|
||||||
|
.section { padding: 60px 0; }
|
||||||
|
.hero { padding: 80px 0; }
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<!-- Hero Section -->
|
||||||
|
<section class="hero section">
|
||||||
|
<div class="container fade-in-up">
|
||||||
|
<h1>Харнес превращает решения в исполненные задачи</h1>
|
||||||
|
<p>Для кооперативов, которые хотят ≥ +1% к AUM в горизонте год+</p>
|
||||||
|
<a href="#pricing" class="btn">Начать использовать Simbiot</a>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- What This Is -->
|
||||||
|
<section class="section">
|
||||||
|
<div class="container">
|
||||||
|
<h2>Что это</h2>
|
||||||
|
<div class="problem-solution">
|
||||||
|
<div class="block fade-in-up delay-1">
|
||||||
|
<h3>Проблема</h3>
|
||||||
|
<p>Решения из звонков и чатов теряются, не доходят до исполнителей, важное тонет в рутине</p>
|
||||||
|
</div>
|
||||||
|
<div class="block fade-in-up delay-2">
|
||||||
|
<h3>Механизм</h3>
|
||||||
|
<p>Харнес автоматически извлекает решения из диалогов, создает задачи с четкими сроками и ответственными</p>
|
||||||
|
</div>
|
||||||
|
<div class="block fade-in-up delay-3">
|
||||||
|
<h3>Результат</h3>
|
||||||
|
<p>Важное всегда на виду, эскалации важного поднимаются наверх, AUM растет системно</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- What's Included -->
|
||||||
|
<section class="section">
|
||||||
|
<div class="container">
|
||||||
|
<h2>Что включено</h2>
|
||||||
|
<ul class="included-list">
|
||||||
|
<li>Автоматический парсинг решений из Telegram-чатов и звонков</li>
|
||||||
|
<li>Система задач с тактами (минута/час/день) для агентов</li>
|
||||||
|
<li>Дашборд контроля исполнения в реальном времени</li>
|
||||||
|
<li>Эскалация важных задач без потери контекста</li>
|
||||||
|
<li>Интеграция с существующими системами кооператива</li>
|
||||||
|
<li>Аналитика эффективности решений и их исполнения</li>
|
||||||
|
<li>Поддержка 7 продуктов кооператива 42</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- How It Works -->
|
||||||
|
<section class="section">
|
||||||
|
<div class="container">
|
||||||
|
<h2>Как это работает</h2>
|
||||||
|
<div class="steps">
|
||||||
|
<div class="step fade-in-up">
|
||||||
|
<div class="step-number">1</div>
|
||||||
|
<h3>Обсуждение</h3>
|
||||||
|
<p>Вы обсуждаете решения в Telegram или звонках</p>
|
||||||
|
</div>
|
||||||
|
<div class="step fade-in-up delay-1">
|
||||||
|
<div class="step-number">2</div>
|
||||||
|
<h3>Авто-создание задач</h3>
|
||||||
|
<p>Simbiot превращает решения в задачи с ответственными</p>
|
||||||
|
</div>
|
||||||
|
<div class="step fade-in-up delay-2">
|
||||||
|
<div class="step-number">3</div>
|
||||||
|
<h3>Контроль исполнения</h3>
|
||||||
|
<p>Агенты выполняют по тактам, важное эскалируется</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Product Visuals -->
|
||||||
|
<section class="section">
|
||||||
|
<div class="container">
|
||||||
|
<h2>Визуалы продукта</h2>
|
||||||
|
|
||||||
|
<div class="mockup fade-in-up">
|
||||||
|
<h3>Дашборд задач</h3>
|
||||||
|
<div class="dashboard">
|
||||||
|
<div class="dashboard-header">Панель управления задачами</div>
|
||||||
|
<div class="dashboard-content">
|
||||||
|
<div class="task-item">
|
||||||
|
<span>Анализ Q3 отчетов</span>
|
||||||
|
<span class="task-status status-progress">В работе</span>
|
||||||
|
</div>
|
||||||
|
<div class="task-item">
|
||||||
|
<span>Подготовить презентацию для инвесторов</span>
|
||||||
|
<span class="task-status status-pending">Ожидание</span>
|
||||||
|
</div>
|
||||||
|
<div class="task-item">
|
||||||
|
<span>Обновить CRM систему</span>
|
||||||
|
<span class="task-status status-done">Готово</span>
|
||||||
|
</div>
|
||||||
|
<div class="task-item">
|
||||||
|
<span>Проверить compliance требования</span>
|
||||||
|
<span class="task-status status-pending">Ожидание</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mockup fade-in-up delay-1">
|
||||||
|
<h3>Telegram-диалог</h3>
|
||||||
|
<div class="telegram-chat">
|
||||||
|
<div class="message bot">
|
||||||
|
<strong>Simbiot:</strong> Обнаружено решение: "Запланировать встречу с командой по стратегии Q4"
|
||||||
|
</div>
|
||||||
|
<div class="message user">
|
||||||
|
<strong>Вы:</strong> Подтверждаю, создай задачу
|
||||||
|
</div>
|
||||||
|
<div class="message bot">
|
||||||
|
<strong>Simbiot:</strong> Задача создана: Встреча по стратегии Q4, ответственный: Вы, срок: 25.10.2024
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Social Proof -->
|
||||||
|
<section class="section">
|
||||||
|
<div class="container">
|
||||||
|
<h2>Соцдоказательство</h2>
|
||||||
|
<div class="social-proof">
|
||||||
|
<p>Кооператив 42 «Друзья Коллективного Разума»</p>
|
||||||
|
<p>Юрлицо с 2026 года, Минск</p>
|
||||||
|
<p>7 продуктов под управлением, сайт cil.by, Telegram t.me/cil_by</p>
|
||||||
|
|
||||||
|
<div class="stats">
|
||||||
|
<div class="stat">
|
||||||
|
<div class="stat-number">2026</div>
|
||||||
|
<div>Год основания</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat">
|
||||||
|
<div class="stat-number">7</div>
|
||||||
|
<div>Активных продуктов</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat">
|
||||||
|
<div class="stat-number">≥+1%</div>
|
||||||
|
<div>Рост AUM в год+</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Pricing -->
|
||||||
|
<section class="section" id="pricing">
|
||||||
|
<div class="container">
|
||||||
|
<h2>Тарифные планы</h2>
|
||||||
|
<div class="pricing">
|
||||||
|
<div class="plan fade-in-up">
|
||||||
|
<h3>Старт</h3>
|
||||||
|
<div class="plan-price">₽15к/мес</div>
|
||||||
|
<ul class="plan-features">
|
||||||
|
<li>До 5 пользователей</li>
|
||||||
|
<li>1 Telegram-чат</li>
|
||||||
|
<li>Базовый дашборд</li>
|
||||||
|
<li>Стандартная поддержка</li>
|
||||||
|
</ul>
|
||||||
|
<a href="#" class="btn">Начать</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="plan fade-in-up delay-1">
|
||||||
|
<h3>Про</h3>
|
||||||
|
<div class="plan-price">₽35к/мес</div>
|
||||||
|
<ul class="plan-features">
|
||||||
|
<li>До 20 пользователей</li>
|
||||||
|
<li>5 Telegram-чатов</li>
|
||||||
|
<li>Продвинутый дашборд</li>
|
||||||
|
<li>Приоритетная поддержка</li>
|
||||||
|
<li>API интеграции</li>
|
||||||
|
</ul>
|
||||||
|
<a href="#" class="btn">Выбрать</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="plan fade-in-up delay-2">
|
||||||
|
<h3>Enterprise</h3>
|
||||||
|
<div class="plan-price">По запросу</div>
|
||||||
|
<ul class="plan-features">
|
||||||
|
<li>Неограниченно пользователей</li>
|
||||||
|
<li>Неограниченно чатов</li>
|
||||||
|
<li>Полный функционал</li>
|
||||||
|
<li>Персональный менеджер</li>
|
||||||
|
<li>Кастомные интеграции</li>
|
||||||
|
</ul>
|
||||||
|
<a href="#" class="btn">Контакты</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Final CTA -->
|
||||||
|
<section class="hero section">
|
||||||
|
<div class="container fade-in-up">
|
||||||
|
<h1>Харнес превращает решения в исполненные задачи</h1>
|
||||||
|
<p>Для кооперативов, которые хотят ≥ +1% к AUM в горизонте год+</p>
|
||||||
|
<a href="#pricing" class="btn">Начать использовать Simbiot</a>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Footer -->
|
||||||
|
<footer class="footer">
|
||||||
|
<div class="container">
|
||||||
|
<div class="footer-content">
|
||||||
|
<div class="footer-section">
|
||||||
|
<h4>Кто делает</h4>
|
||||||
|
<p>Кооператив 42 «Друзья Коллективного Разума»</p>
|
||||||
|
<p>Разработка Simbiot</p>
|
||||||
|
</div>
|
||||||
|
<div class="footer-section">
|
||||||
|
<h4>Контакты</h4>
|
||||||
|
<p>Сайт: cil.by</p>
|
||||||
|
<p>Telegram: t.me/cil_by</p>
|
||||||
|
<p>Email: info@cil.by</p>
|
||||||
|
</div>
|
||||||
|
<div class="footer-section">
|
||||||
|
<h4>Юр. информация</h4>
|
||||||
|
<p>УНП: 123456789</p>
|
||||||
|
<p>Адрес: г. Минск, ул. Примерная, 42</p>
|
||||||
|
<p>2026-2026 © Все права защищены</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
162
artifacts/landing-simbiot.html
Normal file
162
artifacts/landing-simbiot.html
Normal file
|
|
@ -0,0 +1,162 @@
|
||||||
|
<!doctype html>
|
||||||
|
<html lang="ru">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<title>Simbiot — общий харнес кооператива 42</title>
|
||||||
|
<meta name="description" content="Simbiot — ритмичный ИИ-харнес кооператива 42: решения со звонков превращаются в исполненные задачи, агенты живут тактами.">
|
||||||
|
<style>
|
||||||
|
:root{
|
||||||
|
--ink:#101418; --paper:#f4f0e6; --tape:#e0d9c8;
|
||||||
|
--acc:#0e7a5f; --acc2:#c2571b; --line:#101418;
|
||||||
|
--mono:"JetBrains Mono","SF Mono",ui-monospace,Menlo,Consolas,monospace;
|
||||||
|
--serif:"Iowan Old Style","Palatino Linotype",Palatino,Georgia,serif;
|
||||||
|
}
|
||||||
|
*{box-sizing:border-box;margin:0;padding:0}
|
||||||
|
html{scroll-behavior:smooth}
|
||||||
|
body{background:var(--paper);color:var(--ink);font-family:var(--serif);line-height:1.55}
|
||||||
|
.mono{font-family:var(--mono)}
|
||||||
|
a{color:inherit}
|
||||||
|
|
||||||
|
/* ---------- шапка-таблица ---------- */
|
||||||
|
header{border-bottom:2px solid var(--line);padding:18px clamp(16px,4vw,48px);display:flex;justify-content:space-between;align-items:baseline;gap:16px;flex-wrap:wrap}
|
||||||
|
.brand{font-family:var(--mono);font-weight:700;font-size:18px;letter-spacing:.04em}
|
||||||
|
.brand i{color:var(--acc2);font-style:normal}
|
||||||
|
nav{font-family:var(--mono);font-size:13px;display:flex;gap:20px}
|
||||||
|
nav a{text-decoration:none;border-bottom:1px dotted var(--ink)}
|
||||||
|
nav a:hover{color:var(--acc2)}
|
||||||
|
|
||||||
|
/* ---------- hero: «осциллограмма» тактов ---------- */
|
||||||
|
.hero{position:relative;overflow:hidden;padding:clamp(56px,9vw,110px) clamp(16px,4vw,48px) 40px;border-bottom:2px solid var(--line)}
|
||||||
|
.hero h1{font-weight:400;font-size:clamp(38px,6.5vw,84px);line-height:1.04;letter-spacing:-.015em;max-width:14ch}
|
||||||
|
.hero h1 b{font-weight:600}
|
||||||
|
.hero p.lede{margin-top:26px;max-width:52ch;font-size:clamp(17px,1.6vw,21px)}
|
||||||
|
.tick-strip{margin-top:48px;display:flex;align-items:flex-end;gap:3px;height:64px}
|
||||||
|
.tick-strip span{flex:1;background:var(--ink);min-width:2px;animation:pulse 2.4s ease-in-out infinite}
|
||||||
|
@keyframes pulse{0%,100%{transform:scaleY(.35);opacity:.35}50%{transform:scaleY(1);opacity:1}}
|
||||||
|
.tick-strip span:nth-child(odd){background:var(--acc)}
|
||||||
|
.tick-label{font-family:var(--mono);font-size:12px;margin-top:10px;color:#555}
|
||||||
|
.cta{margin-top:40px;display:flex;gap:14px;flex-wrap:wrap}
|
||||||
|
.btn{font-family:var(--mono);font-size:15px;text-decoration:none;padding:14px 26px;border:2px solid var(--line)}
|
||||||
|
.btn.solid{background:var(--acc);color:#fff;border-color:var(--acc)}
|
||||||
|
.btn:hover{transform:translate(-2px,-2px);box-shadow:3px 3px 0 var(--line)}
|
||||||
|
.btn.solid:hover{box-shadow:3px 3px 0 var(--acc2)}
|
||||||
|
|
||||||
|
/* ---------- секции ---------- */
|
||||||
|
section{padding:clamp(44px,6vw,80px) clamp(16px,4vw,48px);border-bottom:1px solid #c9c2b0}
|
||||||
|
h2{font-weight:400;font-size:clamp(26px,3.4vw,40px);letter-spacing:-.01em;margin-bottom:28px}
|
||||||
|
h2 em{font-style:italic;color:var(--acc2)}
|
||||||
|
|
||||||
|
.cards{display:grid;grid-template-columns:repeat(auto-fit,minmax(250px,1fr));gap:0;border:2px solid var(--line)}
|
||||||
|
.card{padding:26px 24px 30px;border-right:1px solid var(--line);position:relative}
|
||||||
|
.card:last-child{border-right:none}
|
||||||
|
.card .n{font-family:var(--mono);font-size:12px;color:var(--acc2);display:block;margin-bottom:14px}
|
||||||
|
.card h3{font-weight:600;font-size:19px;margin-bottom:10px}
|
||||||
|
.card p{font-size:15.5px;color:#33301f}
|
||||||
|
|
||||||
|
/* поток решения */
|
||||||
|
.flow{font-family:var(--mono);font-size:14px;display:flex;flex-wrap:wrap;gap:10px 0;align-items:center}
|
||||||
|
.flow .step{border:1.5px solid var(--ink);padding:10px 16px;background:#fff8}
|
||||||
|
.flow .step b{color:var(--acc)}
|
||||||
|
.flow .arw{padding:0 12px;color:var(--acc2)}
|
||||||
|
|
||||||
|
.numbers{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:32px;font-family:var(--mono)}
|
||||||
|
.numbers .v{font-size:clamp(30px,4vw,46px);font-weight:700}
|
||||||
|
.numbers .v.a{color:var(--acc)} .numbers .v.b{color:var(--acc2)}
|
||||||
|
.numbers .c{font-size:13px;color:#555;margin-top:6px;max-width:24ch}
|
||||||
|
|
||||||
|
blockquote{border-left:4px solid var(--acc);padding:6px 0 6px 22px;font-size:clamp(19px,2.2vw,26px);font-style:italic;max-width:46ch}
|
||||||
|
|
||||||
|
footer{padding:36px clamp(16px,4vw,48px);font-family:var(--mono);font-size:13px;display:flex;justify-content:space-between;gap:16px;flex-wrap:wrap;background:var(--ink);color:var(--paper)}
|
||||||
|
footer a{color:#8fd6bd}
|
||||||
|
@media (prefers-reduced-motion:reduce){.tick-strip span{animation:none;transform:scaleY(.6);opacity:.7}.btn:hover{transform:none;box-shadow:none}}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<header>
|
||||||
|
<div class="brand">simbiot<i>·42</i></div>
|
||||||
|
<nav>
|
||||||
|
<a href="#what">что это</a>
|
||||||
|
<a href="#how">как работает</a>
|
||||||
|
<a href="#numbers">цифры</a>
|
||||||
|
<a href="#join">участие</a>
|
||||||
|
</nav>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<section class="hero" style="border:none;padding-bottom:56px">
|
||||||
|
<h1>Компания, которая <b>думает тактами</b>, а не совещаниями</h1>
|
||||||
|
<p class="lede">Simbiot — ритмичный ИИ-харнес кооператива 42. Решение, зафиксированное на звонке, к утру превращается в выполненную задачу с проверяемым результатом. Агенты живут тактами, копят память и отчитываются — как люди, только без выходных.</p>
|
||||||
|
<div class="tick-strip" aria-hidden="true">
|
||||||
|
<span style="animation-delay:0s"></span><span style="animation-delay:.2s"></span><span style="animation-delay:.4s"></span><span style="animation-delay:.6s"></span><span style="animation-delay:.8s"></span><span style="animation-delay:1s"></span><span style="animation-delay:1.2s"></span><span style="animation-delay:1.4s"></span><span style="animation-delay:1.6s"></span><span style="animation-delay:1.8s"></span><span style="animation-delay:2s"></span><span style="animation-delay:2.2s"></span>
|
||||||
|
</div>
|
||||||
|
<div class="tick-label mono">каждая полоска — один такт агента · минутный пульс → часовой → дневной</div>
|
||||||
|
<div class="cta">
|
||||||
|
<a class="btn solid" href="https://cil.by">Зайти в портал</a>
|
||||||
|
<a class="btn" href="#how">Как это работает ↓</a>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="what">
|
||||||
|
<h2>Не чат-бот. <em>Операционная система</em> студии</h2>
|
||||||
|
<div class="cards">
|
||||||
|
<div class="card">
|
||||||
|
<span class="n">01 / симбиоз</span>
|
||||||
|
<h3>Общий харнес, а не личный</h3>
|
||||||
|
<p>Разработчики строят личные харнесы и уносят их с собой. Simbiot — харнес всей организации: нулевая точка воздействия, он умеет переписывать сам себя.</p>
|
||||||
|
</div>
|
||||||
|
<div class="card">
|
||||||
|
<span class="n">02 / координация</span>
|
||||||
|
<h3>Интегрирует открытое</h3>
|
||||||
|
<p>Координатор харнесов: встраивает открытые системы (Hermes, OpenClaw) и свои скиллы в один контур исполнения.</p>
|
||||||
|
</div>
|
||||||
|
<div class="card">
|
||||||
|
<span class="n">03 / память</span>
|
||||||
|
<h3>CQRS + Event Sourcing</h3>
|
||||||
|
<p>Каждое решение — событие. История не теряется, любой такт можно переиграть. База знаний живая: протоколы встреч, договоры, проекты.</p>
|
||||||
|
</div>
|
||||||
|
<div class="card">
|
||||||
|
<span class="n">04 / эскалация</span>
|
||||||
|
<h3>Уровни, а не один мозг</h3>
|
||||||
|
<p>Минутный пульс отвечает в чате, часовой и дневной — берут сложное. Что не по зубам уровню, уходит наверх с контекстом.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="how" style="background:#fff6; ">
|
||||||
|
<h2>От голоса — <em>к результату</em></h2>
|
||||||
|
<div class="flow">
|
||||||
|
<span class="step">звонок → <b>протокол решений</b></span><span class="arw">→</span>
|
||||||
|
<span class="step">задача на доске с <b>критерием готовности</b></span><span class="arw">→</span>
|
||||||
|
<span class="step">агент исполняет <b>такт за тактом</b></span><span class="arw">→</span>
|
||||||
|
<span class="step">результат в чат, <b>метрика AUM</b> растёт</span>
|
||||||
|
</div>
|
||||||
|
<blockquote style="margin-top:44px">Фильтр любого решения: добавляет ли оно +1% к AUM в горизонте год и больше?</blockquote>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="numbers">
|
||||||
|
<h2>Цифры, без глянца</h2>
|
||||||
|
<div class="numbers">
|
||||||
|
<div><div class="v a">7</div><div class="c">продуктов в студии, живые деньги — уже у двух</div></div>
|
||||||
|
<div><div class="v b">42</div><div class="c">кооператив «Друзья Коллективного Разума» — юрлицо живое с 2026</div></div>
|
||||||
|
<div><div class="v">24/7</div><div class="c">пульс: минутный, часовой и дневной уровни агентов</div></div>
|
||||||
|
<div><div class="v a">100%</div><div class="c">уходя, участник забирает свой проект — база не разваливается</div></div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="join">
|
||||||
|
<h2>Встроиться в <em>симбиоз</em></h2>
|
||||||
|
<p style="max-width:56ch;font-size:18px">Мы берём проекты в программу 42 Labs: юрлицо и платёжный контур, студийная ОС с первого дня, плата из выручки — когда проект заработал. Или просто напишите боту в Telegram и посмотрите, как отвечает минутный пульс.</p>
|
||||||
|
<div class="cta">
|
||||||
|
<a class="btn solid" href="https://cil.by">Подать заявку на портале</a>
|
||||||
|
<a class="btn" href="https://t.me/cil_by">Написать в Telegram</a>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<footer>
|
||||||
|
<div>кооператив 42 · «Друзья Коллективного Разума» · Минск</div>
|
||||||
|
<div><a href="https://cil.by">cil.by</a> · собрано харнесом Simbiot</div>
|
||||||
|
</footer>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
91
artifacts/mirofish-analysis-2026-10-04.md
Normal file
91
artifacts/mirofish-analysis-2026-10-04.md
Normal file
|
|
@ -0,0 +1,91 @@
|
||||||
|
# MiroFish (github 666ghj/MiroFish): разбор и применимость — 2026-10-04
|
||||||
|
|
||||||
|
Задача: t-261004062405-fa9926. Метод: клон репо (depth 1), разбор кода backend
|
||||||
|
(58 py-файлов, ~17.2k строк) + README. Версия зависимостей: camel-oasis==0.2.5,
|
||||||
|
zep-cloud==3.25.0, flask>=3.0, openai>=1.0.
|
||||||
|
|
||||||
|
## Вердикт
|
||||||
|
|
||||||
|
**Как движок — НЕ берём. Как источник паттернов — да, 3-5 идей.**
|
||||||
|
Причины против движка: (1) стоимость — README сам пишет «High consumption,
|
||||||
|
попробуйте сначала <40 раундов»; (2) жёсткая привязка к Zep Cloud (внешний
|
||||||
|
SaaS-граф памяти, счёт отдельно от LLM) и OASIS/camel-ai; (3) китайский
|
||||||
|
дефолт-стек (qwen-plus через dashscope) — у нас 9router; (4) это продукт-платформа
|
||||||
|
(Flask 5001 + React 3000 + Docker), а не библиотека — интеграция = second brain.
|
||||||
|
|
||||||
|
## Архитектура (проверено по коду)
|
||||||
|
|
||||||
|
Пайплайн: **Graph Building → Environment Setup → Simulation → Report → Interaction**
|
||||||
|
|
||||||
|
1. **Graph Building** (`graph_builder.py` 879 строк, `ontology_generator.py` 732):
|
||||||
|
seed-материалы (новости/политики/финанс.сигналы, PDF/текст) → извлечение
|
||||||
|
сущностей и связей → GraphRAG-граф в Zep (онтология генерится LLM).
|
||||||
|
2. **Environment Setup** (`oasis_profile_generator.py` 1248): из сущностей Zep
|
||||||
|
LLM-ом генерятся персоны агентов — индивидуальные и групповые
|
||||||
|
(`generate_profile_from_entity` → `_generate_profile_with_llm`, JSON-схема,
|
||||||
|
username, bio, persona-текст) → конфиг агентов инжектится в OASIS.
|
||||||
|
3. **Simulation** (`simulation_runner.py` 2033, `run_parallel_simulation.py` 1699,
|
||||||
|
twitter/reddit скрипты): движок **OASIS (camel-ai)** — двухплатформенная
|
||||||
|
параллельная симуляция соцсети (Twitter+Reddit). Раунды = часы×60/минут_на_раунд,
|
||||||
|
есть max_rounds-обрезка (контроль стоимости). Каждый агент за раунд —
|
||||||
|
действие через `LLMAction` (≥1 LLM-вызов на активного агента за раунд).
|
||||||
|
Память: каждое действие → episode-текст → динамическое обновление Zep-графа
|
||||||
|
(`zep_graph_memory_updater.py` 791: описатели create_post/like/repost/
|
||||||
|
comment/follow/search).
|
||||||
|
4. **Report** (`report_agent.py` 2619 — самый большой модуль): ReportAgent с 4
|
||||||
|
инструментами — `insight_forge`, `panorama_search`, `quick_search`,
|
||||||
|
`interview_agents` (интервьюирует агентов мира post-factum); протокол
|
||||||
|
`<tool_call>{...}</tool_call>`, трекер «не все инструменты использованы»
|
||||||
|
дожимает агент до полного отчёта.
|
||||||
|
5. **Interaction**: чат с любым агентом мира + ReportAgent (фронт).
|
||||||
|
|
||||||
|
Память = **Zep Cloud** (knowledge-graph + temporal): не векторная база, а граф
|
||||||
|
сущностей с би-темпоралью; free-тариф «на простое использование».
|
||||||
|
|
||||||
|
## Оценка стоимости прогона
|
||||||
|
|
||||||
|
Формула расхода: ~ `N_агентов × N_раундов` LLM-вызовов (действия) + граф/онтология
|
||||||
|
+ персоны + отчёт (десятки вызовов) + инференс Zep (граф-обновления, отдельный счёт).
|
||||||
|
|
||||||
|
- Заявленный дефолт: qwen-plus. README: «<40 раундов на старте».
|
||||||
|
- Пример: 100 агентов × 40 раундов ≈ 4–5k вызовов ≈ 15–25M входящих токенов
|
||||||
|
(контекст платформы в каждом) → **~$10–25 на qwen-plus за прогон**;
|
||||||
|
на премиум-модели класса GPT-4o — **×10–30 (сотни $)**.
|
||||||
|
- Осмысленный прогнозный прогон (500+ агентов, 100+ раундов) — **сотни $ за
|
||||||
|
прогон** даже на дешёвых моделях + Zep Cloud сверх.
|
||||||
|
- Контекст: наш burn 9router = 2291.81 у.е./7д — один-два «взрослых» прогона
|
||||||
|
MiroFish съели бы заметный кусок недельного бюджета без прямой выручки.
|
||||||
|
|
||||||
|
## Что берём (идеи заимствования)
|
||||||
|
|
||||||
|
1. **Персона-из-сущности** (`_generate_profile_with_llm`): LLM-генерация
|
||||||
|
структурированной персоны агента из записи графа знаний (JSON-схема, инди/групп).
|
||||||
|
Куда: v2 aum-pilot — вместо placeholder-модели продуктивности, агентные симуляции
|
||||||
|
решений кооператива (роли: Alex/Vlad/Yura/клиент) с сгенерированными персонами.
|
||||||
|
2. **ReportAgent-паттерн**: отчётёр = агент с узким набором инструментов
|
||||||
|
(сводка/поиск/дип-поиск/интервью) + трекер обязательного использования всех
|
||||||
|
инструментов. Куда: наш «отчёт-адвокат» в aum-pilot и часовые дайджесты —
|
||||||
|
интервью опрошенных агентов симуляции вместо голой статистики.
|
||||||
|
3. **Episode→граф памяти**: действие → компактный episode-текст → обновление
|
||||||
|
графа по ходу симуляции (не в конце). Куда: память ритмичных агентов харнеса
|
||||||
|
— но на нашем RAG (без Zep: episode → embedding → граф связей) — паттерн
|
||||||
|
повторяем, сервис не покупаем.
|
||||||
|
4. **Предпродажная симуляция спроса**: OASIS даёт готовую соц-динамику
|
||||||
|
(лайк/репост/коммент/фоллов). Идея под AUM: перед запуском лендинга/прайса —
|
||||||
|
дешёвый прогон (20–40 раундов, flash-модель, 50–100 агентов-«клиентов» из
|
||||||
|
персон сегмента) → прогноз реакции и возражений. Это прямой кандидат в
|
||||||
|
наши симуляции поведения пользователей.
|
||||||
|
5. **Автогенерация конфига из NL-запроса** (`simulation_config_generator.py`):
|
||||||
|
«что хочешь узнать» естественным языком → параметры прогона + max_rounds
|
||||||
|
(жёсткий потолок стоимости). Куда: постановка задачи → конфиг прогона
|
||||||
|
aum-pilot; потолок раундов — в наши длинные симуляции.
|
||||||
|
6. **IPC/прогресс долгих прогонов** (`simulation_ipc.py` + progress %):
|
||||||
|
отдельный процесс, структурированный прогресс, чистая обрезка. У нас уже
|
||||||
|
есть _llm_worker — позаимствовать схему прогресса/тримминга.
|
||||||
|
|
||||||
|
## Чего не берём
|
||||||
|
|
||||||
|
- Zep Cloud (внешний SaaS, счёт отдельно, данные наружу) — свой RAG/graph.
|
||||||
|
- OASIS целиком (тяжёлый camel-ai стек) — только если сделаем п.4; тогда
|
||||||
|
изолированный Docker-стенд, flash-модель через 9router, budget-cap на прогон.
|
||||||
|
- Их фронт/платформу — у нас свои каналы (портал, MCP, TG).
|
||||||
176
artifacts/rental-x-simbiot-agreement.html
Normal file
176
artifacts/rental-x-simbiot-agreement.html
Normal file
|
|
@ -0,0 +1,176 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="ru">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Прокат авто × Simbiot — партнёрское соглашение v1</title>
|
||||||
|
<style>
|
||||||
|
:root{
|
||||||
|
--paper:#faf8f3; --ink:#15150f; --signal:#FF4F00;
|
||||||
|
--dim:#6b675e; --line:#d8d3c8; --card:#f3f0e8; --ok:#1d7a3e; --warn:#a3690f;
|
||||||
|
}
|
||||||
|
*{margin:0;padding:0;box-sizing:border-box}
|
||||||
|
html{scroll-behavior:smooth}
|
||||||
|
body{background:var(--paper);color:var(--ink);font:16px/1.62 -apple-system,"Segoe UI",Roboto,Arial,sans-serif;-webkit-font-smoothing:antialiased}
|
||||||
|
.mono,code{font-family:ui-monospace,"JetBrains Mono","SF Mono",Menlo,Consolas,monospace}
|
||||||
|
.wrap{max-width:980px;margin:0 auto;padding:56px 24px 100px}
|
||||||
|
header{border-bottom:3px solid var(--ink);padding-bottom:26px;margin-bottom:36px}
|
||||||
|
.kicker{font-family:ui-monospace,Menlo,monospace;font-size:12px;letter-spacing:.22em;color:var(--signal);font-weight:700;text-transform:uppercase}
|
||||||
|
h1{font-size:clamp(32px,5.5vw,52px);line-height:1.05;font-weight:800;letter-spacing:-.02em;margin:10px 0 14px;text-transform:uppercase}
|
||||||
|
.sub{font-size:17px;color:var(--dim);max-width:720px}
|
||||||
|
.badges{display:flex;flex-wrap:wrap;gap:8px;margin-top:18px}
|
||||||
|
.badge{border:1px solid var(--ink);padding:5px 12px;font-family:ui-monospace,Menlo,monospace;font-size:12px;letter-spacing:.08em;text-transform:uppercase;background:var(--paper)}
|
||||||
|
.badge.s{background:var(--ink);color:var(--paper)}
|
||||||
|
section{margin:52px 0 0}
|
||||||
|
.snum{font-family:ui-monospace,Menlo,monospace;font-size:12px;color:var(--signal);letter-spacing:.2em;font-weight:700}
|
||||||
|
h2{font-size:26px;font-weight:800;letter-spacing:-.01em;margin:4px 0 6px;text-transform:uppercase}
|
||||||
|
.lead{color:var(--dim);max-width:760px;margin-bottom:20px}
|
||||||
|
h3{font-size:14px;text-transform:uppercase;letter-spacing:.06em;margin:24px 0 10px;border-left:3px solid var(--signal);padding-left:10px}
|
||||||
|
p{margin:10px 0;max-width:820px}
|
||||||
|
.clause{border:1px solid var(--line);background:#fff;padding:14px 18px;margin:12px 0;max-width:860px}
|
||||||
|
.clause .cn{font-family:ui-monospace,Menlo,monospace;font-size:11px;color:var(--signal);font-weight:700;letter-spacing:.15em}
|
||||||
|
.clause h4{font-size:15px;margin:4px 0 6px;font-weight:800}
|
||||||
|
.clause p{font-size:14px;color:var(--ink);margin:4px 0}
|
||||||
|
.note{border:1px solid var(--ink);border-left:6px solid var(--signal);background:#fff;padding:14px 18px;margin:20px 0;max-width:860px}
|
||||||
|
.note b{font-family:ui-monospace,Menlo,monospace;font-size:12px;letter-spacing:.12em;text-transform:uppercase;color:var(--signal)}
|
||||||
|
.note.ok{border-left-color:var(--ok)} .note.ok b{color:var(--ok)}
|
||||||
|
table{width:100%;border-collapse:collapse;margin:18px 0;max-width:900px;background:#fff;border:1px solid var(--ink)}
|
||||||
|
th{font-family:ui-monospace,Menlo,monospace;font-size:11px;letter-spacing:.12em;text-transform:uppercase;text-align:left;padding:10px 12px;background:var(--ink);color:var(--paper)}
|
||||||
|
td{padding:10px 12px;border-top:1px solid var(--line);font-size:14px;vertical-align:top}
|
||||||
|
td .mono{font-size:12.5px;color:var(--signal);font-weight:700}
|
||||||
|
ul{margin:8px 0 8px 18px;font-size:14.5px}
|
||||||
|
li{margin:6px 0}
|
||||||
|
li::marker{color:var(--signal)}
|
||||||
|
.grid2{display:grid;grid-template-columns:1fr 1fr;gap:14px;margin:18px 0}
|
||||||
|
@media(max-width:760px){.grid2{grid-template-columns:1fr}}
|
||||||
|
.sign{border:3px double var(--ink);background:#fff;padding:22px;margin:26px 0;display:grid;grid-template-columns:1fr 1fr;gap:20px}
|
||||||
|
@media(max-width:760px){.sign{grid-template-columns:1fr}}
|
||||||
|
.sig .org{font-family:ui-monospace,Menlo,monospace;font-size:11px;letter-spacing:.12em;text-transform:uppercase;color:var(--dim);margin-bottom:14px}
|
||||||
|
.sig .line{border-bottom:1px solid var(--ink);height:38px;margin-bottom:8px}
|
||||||
|
.sig .field{font-size:12.5px;color:var(--dim)}
|
||||||
|
footer{margin-top:60px;border-top:1px solid var(--line);padding-top:18px;color:var(--dim);font-size:12.5px}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="wrap">
|
||||||
|
|
||||||
|
<header>
|
||||||
|
<div class="kicker">Потребительский кооператив «Друзья Коллективного Разума» · Минск</div>
|
||||||
|
<h1>Соглашение о партнёрском вознаграждении № 1</h1>
|
||||||
|
<div class="sub">Прокат авто (Минск) × Simbiot. Договор-черновик на согласование: платформа как операционный и маркетинговый слой партнёра, вознаграждение — 10% от заработка на атрибутированных клиентах.</div>
|
||||||
|
<div class="badges">
|
||||||
|
<span class="badge s">Черновик v1 · 04.10.2026</span>
|
||||||
|
<span class="badge">10% от нетто-выручки</span>
|
||||||
|
<span class="badge">Ставка фиксирована 12 мес</span>
|
||||||
|
<span class="badge">Атрибуция — 24 мес</span>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<div class="snum">00 / СТОРОНЫ</div>
|
||||||
|
<h2>Стороны соглашения</h2>
|
||||||
|
<div class="grid2">
|
||||||
|
<div class="clause"><div class="cn">ОПЕРАТОР ПЛАТФОРМЫ</div><h4>Кооператив</h4><p>[полное наименование кооператива 42], г. Минск — оператор платформы Simbiot (агентские, маркетинговые и операционные AI-сервисы).</p></div>
|
||||||
|
<div class="clause"><div class="cn">ПАРТНЁР</div><h4>Прокатная компания</h4><p>[наименование юрлица/ИП], г. Минск — владелец автопарка, передаёт Platform привлечение клиентов и операционное управление частью бизнеса.</p></div>
|
||||||
|
</div>
|
||||||
|
<div class="note ok"><b>Разделение документов ·</b> Этот документ — только про партнёрское вознаграждение и рост выручки. Исторический долг Партнёра (~15 000 USD) вынесен в отдельный Side Letter № 1 — подписывается параллельно, на условия этого соглашения не влияет.</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<div class="snum">01 / ПРЕДМЕТ</div>
|
||||||
|
<h2>Предмет соглашения</h2>
|
||||||
|
<div class="clause"><div class="cn">1.1</div><p>Оператор предоставляет Партнёру доступ к Платформе Simbiot (агентские, маркетинговые и операционные AI-сервисы) для привлечения клиентов и управления автопарком Партнёра.</p></div>
|
||||||
|
<div class="clause"><div class="cn">1.2</div><p>За это Партнёр уплачивает Оператору <b>партнёрское вознаграждение в размере 10%</b> от заработка по каждому <b>Атрибутированному клиенту</b> (раздел 02).</p></div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<div class="snum">02 / АТРИБУЦИЯ</div>
|
||||||
|
<h2>Кто считается атрибутированным клиентом</h2>
|
||||||
|
<p class="lead">Три категории. Фиксация — записью в Реестре атрибуций (Приложение № 1): ведёт Оператор, Партнёру — доступ на чтение.</p>
|
||||||
|
<table>
|
||||||
|
<tr><th style="width:80px">Категория</th><th>Определение</th></tr>
|
||||||
|
<tr><td><span class="mono">(а)</span></td><td><b>Привлечённая машина</b> — собственник, передавший автомобиль в управление/арендную схему через Платформу.</td></tr>
|
||||||
|
<tr><td><span class="mono">(б)</span></td><td><b>Клиент на аренду</b> — арендатор, пришедший по лиду/воронке/агентству Платформы (метка в CRM/UTM/реестр).</td></tr>
|
||||||
|
<tr><td><span class="mono">(в)</span></td><td><b>Агентство</b> — клиент кооператива, обслуженный через Платформу (корпоративный, трансферный, подписочный и т.п.).</td></tr>
|
||||||
|
</table>
|
||||||
|
<div class="clause"><div class="cn">2.1</div><p>Спорная атрибуция: приоритет у более ранней фиксации лида; неразрешённые споры — на ежемесячной сверке.</p></div>
|
||||||
|
<div class="clause"><div class="cn">2.2</div><p>Клиент считается атрибутированным на <b>24 месяца</b> с первой фиксации (включая повторные аренды и продления).</p></div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<div class="snum">03 / ДЕНЬГИ</div>
|
||||||
|
<h2>Вознаграждение и порядок расчётов</h2>
|
||||||
|
<div class="clause"><div class="cn">3.1</div><p>База расчёта: <b>нетто-выручка</b> по атрибутированному клиенту (полученные платежи за вычетом возвратов, депозитов, комиссий платёжных систем и налогов).</p></div>
|
||||||
|
<div class="clause"><div class="cn">3.2</div><p>Вознаграждение: <b>10% от базы</b>. Ставка фиксируется на 12 месяцев.</p></div>
|
||||||
|
<div class="clause"><div class="cn">3.3</div><p>Расчётный период — календарный месяц. Партнёр до 10-го числа следующего месяца направляет подписанный акт + выгрузку; оплата в течение 5 банковских дней, BYN по курсу НБ РБ на дату оплаты (либо USDT — по выбору сторон).</p></div>
|
||||||
|
<div class="clause"><div class="cn">3.4</div><p>Просрочка >15 дней: пеня 0,1%/день; >45 дней — право Оператора приостановить доступ к Платформе.</p></div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<div class="snum">04 / РОЛЬ ПЛАТФОРМЫ</div>
|
||||||
|
<h2>Что делает Simbiot</h2>
|
||||||
|
<p class="lead">Ровно тот функционал, на котором строится «заработок на атрибутированном клиенте»:</p>
|
||||||
|
<ul>
|
||||||
|
<li><b>Воронки и лид-скоринг</b> — обработка входящих заявок на аренду, приоритизация лидов;</li>
|
||||||
|
<li><b>Агент-диспетчер 24/7</b> — аренда, продления, подписка без участия человека в ночи и выходные;</li>
|
||||||
|
<li><b>Скоринг таксистов</b> — аренда с выкупом: отсев рискованных драйверов;</li>
|
||||||
|
<li><b>Онбординг владельцев машин</b> — приём машин в паркоменеджмент (категория «а»);</li>
|
||||||
|
<li><b>Контент-PR</b> — первое в РБ полностью AI-агентское продвижение проката: почти бесплатный охват + входящие заявки на франшизу/white label.</li>
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<div class="snum">05 / РОСТ ВЫРУЧКИ</div>
|
||||||
|
<h2>Рычаги роста парка</h2>
|
||||||
|
<p class="lead">Ориентир: полное развёртывание рычагов 1–4 даёт расчётно <b>+60–100% выручки за 6–12 мес.</b>, из них ~+20% — в первые 8 недель (монетизация простоя). CAPEX — только ребаланс парка. Оценки сверить с фактическими данными парка (машин, ставки, vacancy) — выгрузка от Партнёра до запуска.</p>
|
||||||
|
<table>
|
||||||
|
<tr><th>#</th><th>Рычаг</th><th>Эффект</th></tr>
|
||||||
|
<tr><td><span class="mono">1</span></td><td><b>Подписка на простой</b> — простаивающие машины сдаются по подписке (такси, доставки, каршеринг-операторы)</td><td>быстрые деньги: монетизация vacancy, ~+20% за 8 недель</td></tr>
|
||||||
|
<tr><td><span class="mono">2</span></td><td><b>Аренда с выкупом</b> — пилот 2–3 эконом-машины, скоринг водителей</td><td>длинные деньги: выкупные платежи + продажа машины в конце</td></tr>
|
||||||
|
<tr><td><span class="mono">3</span></td><td><b>Привлечённые машины</b> — приём авто собственников в управление</td><td>рост парка без CAPEX</td></tr>
|
||||||
|
<tr><td><span class="mono">4</span></td><td><b>Ребаланс парка</b> — перераспределение под спрос (класс, сезон)</td><td>повышение ставки/дня и снижение vacancy</td></tr>
|
||||||
|
<tr><td><span class="mono">5</span></td><td><b>Воронка и агент-диспетчер</b></td><td>конверсия лидов, снижение стоимости обслуживания</td></tr>
|
||||||
|
<tr><td><span class="mono">6</span></td><td><b>PR «полностью AI-based агентство»</b></td><td>отложенный: лиды + заявки на франшизу/white label</td></tr>
|
||||||
|
</table>
|
||||||
|
<h3>Последовательность внедрения</h3>
|
||||||
|
<ul>
|
||||||
|
<li><b>Недели 1–2:</b> подписка на простой (1) + запуск PR-волны (6) — быстрые деньги и охват;</li>
|
||||||
|
<li><b>Месяц 1–2:</b> пилот аренды с выкупом на 2–3 эконом-машинах (2) + приём первых привлечённых машин (3);</li>
|
||||||
|
<li><b>Месяц 2–3:</b> ребаланс парка (4), масштабирование подписки и привлечённого парка.</li>
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<div class="snum">06 / ПОРЯДОК ПОДПИСАНИЯ</div>
|
||||||
|
<h2>Как подписываем</h2>
|
||||||
|
<div class="clause"><div class="cn">6.1</div><p>Стороны согласовывают реквизиты и Приложение № 1 (Реестр атрибуций, стартовый состав).</p></div>
|
||||||
|
<div class="clause"><div class="cn">6.2</div><p>Подписывается настоящее Соглашение и параллельно — <b>Side Letter № 1</b> (урегулирование исторической задолженности, отдельный документ).</p></div>
|
||||||
|
<div class="clause"><div class="cn">6.3</div><p>Юридическая проверка обеих документов до подписания обязательна.</p></div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<div class="snum">07 / РЕКВИЗИТЫ</div>
|
||||||
|
<h2>Подписи</h2>
|
||||||
|
<div class="sign">
|
||||||
|
<div class="sig">
|
||||||
|
<div class="org">Оператор Платформы · Кооператив</div>
|
||||||
|
<div class="line"></div>
|
||||||
|
<div class="field">Председатель правления · подпись</div>
|
||||||
|
<div class="field">Alex Shkor · «___» _______ 2026 г.</div>
|
||||||
|
</div>
|
||||||
|
<div class="sig">
|
||||||
|
<div class="org">Партнёр · [наименование прокатной компании]</div>
|
||||||
|
<div class="line"></div>
|
||||||
|
<div class="field">[должность, ФИО] · подпись</div>
|
||||||
|
<div class="field">М.П. · «___» _______ 2026 г.</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<footer>
|
||||||
|
Черновик для обсуждения, не оферта. Юридическая проверка перед подписанием обязательна. Основа: встреча 04.10.2026 (meeting-6feceb4c). Исторический долг (~15 000 USD) — отдельный Side Letter № 1.
|
||||||
|
</footer>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
79
artifacts/rental-x-simbiot-deal-2026-10-04.md
Normal file
79
artifacts/rental-x-simbiot-deal-2026-10-04.md
Normal file
|
|
@ -0,0 +1,79 @@
|
||||||
|
# Прокат авто (Минск) × Simbiot — договор-черновик и план роста выручки
|
||||||
|
|
||||||
|
**Дата:** 04.10.2026 · **Основа:** встреча meeting-6feceb4c (04.10.2026), фиксация договорённостей
|
||||||
|
**Owner:** Alex · **Статус:** черновик на согласование (юридическая проверка перед подписанием обязательна)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Часть I. Договор-черновик
|
||||||
|
|
||||||
|
> СОГЛАШЕНИЕ О ПАРТНЁРСКОМ ВОЗНАГРАЖДЕНИИ № 1
|
||||||
|
> г. Минск, «04» октября 2026 г.
|
||||||
|
|
||||||
|
**Стороны:**
|
||||||
|
- **Кооператив** — [полное наименование кооператива 42], далее «Оператор Платформы» (Simbiot);
|
||||||
|
- **Партнёр** — [наименование юрлица/ИП прокатной компании], г. Минск, далее «Партнёр».
|
||||||
|
|
||||||
|
### 1. Предмет соглашения
|
||||||
|
1.1. Оператор предоставляет Партнёру доступ к Платформе Simbiot (агентские, маркетинговые и операционные AI-сервисы) для привлечения клиентов и управления автопарком Партнёра.
|
||||||
|
1.2. За это Партнёр уплачивает Оператору **партнёрское вознаграждение в размере 10%** от заработка по каждому **Атрибутированному клиенту** (п. 2).
|
||||||
|
|
||||||
|
### 2. Атрибуция. Атрибутированным считается клиент:
|
||||||
|
- **(а) Привлечённая машина** — собственник, передавший автомобиль в управление/арендную схему через Платформу;
|
||||||
|
- **(б) Клиент на аренду** — арендатор, пришедший по лиду/воронке/агентству Платформы (метка в CRM/UTM/реестр);
|
||||||
|
- **(в) Агентство** — клиент кооператива, обслуженный через Платформу (корпоративный, трансферный, подписочный и т.п.).
|
||||||
|
2.1. Факт атрибуции фиксируется записью в **Реестре атрибуций** (Приложение №1, ведёт Оператор, доступ Партнёру — чтение). Спорная атрибуция: приоритет у более ранней фиксации лида; неразрешённые споры — на ежемесячной сверке.
|
||||||
|
2.2. Клиент считается атрибутированным на **24 месяца** с первой фиксации (включая повторные аренды и продления).
|
||||||
|
|
||||||
|
### 3. Вознаграждение и порядок расчётов
|
||||||
|
3.1. База расчёта: **нетто-выручка** по атрибутированному клиенту (полученные платежи за вычетом возвратов, депозитов, комиссий платёжных систем и налогов).
|
||||||
|
3.2. Вознаграждение: **10% от базы**. Ставка фиксируется на 12 месяцев.
|
||||||
|
3.3. Расчётный период — календарный месяц. Партнёр до 10-го числа следующего месяца направляет подписанный акт + выгрузку; оплата в течение 5 банковских дней, BYN по курсу НБ РБ на дату оплаты (либо USDT — по выбору сторон).
|
||||||
|
3.4. Просрочка >15 дней: пеня 0,1%/день; >45 дней — право Оператора приостановить доступ к Платформе.
|
||||||
|
|
||||||
|
### 4. Конвертация долга в инвестицию
|
||||||
|
4.1. Стороны признают задолженность Партнёра перед Оператором за работы за период ~3 месяца, с инфляционной индексацией, **эквивалент ~15 000 USD, выраженный в BTC по курсу на дату образования долга** (курс-привязка: сумма фиксируется в BTC по курсу BTC/USD на [дату образования долга], рублёвая/долларовая переоценка — по курсу на дату расчётов).
|
||||||
|
4.2. По подписании настоящего Соглашения долг **полностью конвертируется в инвестиционный вклад Партнёра в Проект «Интеграция Simbiot в прокатный бизнес»** (юнит: развитие Партнёра как витринного кейса платформы) на условиях Приложения №2 (Инвестиционное соглашение: доля/profit-share, reporting, выход).
|
||||||
|
4.3. С момента подписания Приложения №2 требование по долгу считается урегулированным (новация).
|
||||||
|
|
||||||
|
### 5. ИП и конфиденциальность
|
||||||
|
5.1. Все права на Платформу Simbiot, модели, данные обучения и CRM принадлежат Оператору. Данные о клиентах Партнёра — совместная коммерческая тайна; Оператор вправе использовать **обезличенную** статистику в маркетинге.
|
||||||
|
5.2. Кейс Партнёра («AI-агентство в прокате») Оператор вправе публиковать по согласованию формулировок.
|
||||||
|
|
||||||
|
### 6. Срок, расторжение
|
||||||
|
6.1. Срок: 12 месяцев с даты подписания, автопролонгация на 12 мес., если ни одна сторона не уведомила за 30 дней.
|
||||||
|
6.2. Расторжение не отменяет вознаграждение за атрибутированных клиентов в текущем периоде и 24-месячный хвост п. 2.2.
|
||||||
|
|
||||||
|
### 7. Прочее
|
||||||
|
Форс-мажор, применимое право РБ, арбитраж по месту Оператора, обмен сканами = оригинал, ЭЦП/eDocument допустимы. Реквизиты — Приложение №3.
|
||||||
|
|
||||||
|
**Открытые вопросы к Alex (до подписания):**
|
||||||
|
1. База 10% — валовая или нетто-выручка (п. 3.1)?
|
||||||
|
2. Хвост атрибуции 24 мес. — устраивает?
|
||||||
|
3. Параметры Приложения №2: доля Партнёра в проекте / profit-share % и срок.
|
||||||
|
4. Точная дата образования долга для фиксации BTC-курса.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Часть II. Анализ рычагов роста выручки (приоритизировано)
|
||||||
|
|
||||||
|
**Исходная проблема парка:** перекос в премиум (CLA45, X7, Audi, BMW2) при нишевом спросе → премиум-активы простаивают, простой = 0 выручки при ненулевых издержках (страхование, стоянка, амортизация).
|
||||||
|
|
||||||
|
| # | Рычаг | Усилие | Эффект (оценка) | Почему сработает |
|
||||||
|
|---|-------|--------|------------------|------------------|
|
||||||
|
| **1** | **Подписка на простой, ~2990/мес** | S | **+15–25% к выручке парка за 1–2 мес.**, почти без CAPEX | «Любая несданная тачка окупается за 3 дня подписки»: 2990/30 ≈ 100/день. Каждые 3 простых дня сегодня = потерянный месячный платёж. Простой превращается из 0 в выручку. ЦА: экспаты/долгосрочники/малый бизнес |
|
||||||
|
| **2** | **Аренда с выкупом под таксистов (~30%/год)** | M | **×1.6 выручки с машины за 2 года**, vacancy → ~0 | Эконом-авто ~25k: клиент-таксист платит ~30% годовых сверх стоимости 24 мес. → возврат ~40k с одной машины, предсказуемый денежный поток, износ компенсирован ставкой |
|
||||||
|
| **3** | **Привлечённые машины (кейс Vestgroup: 0 своих)** | M | **рост парка ×2–3 без CAPEX**, комиссия ~25–35% с каждой | Asset-light: владельцы отдают машины в управление, платформа монетизирует их же воронкой. Снимает потолок роста, ограниченный собственным капиталом |
|
||||||
|
| **4** | **Ребаланс парка: прем → эконом** | M (капитал) | **+10–15% выручки + высвобождение замороженного капитала** | Непроседающий премиум продаётся (вернуть капитал), вместо него 2–3 эконом — стабильный спрос, ниже износ и риск |
|
||||||
|
| **5** | **Дешёвый трафик / новые сегменты** | S–M | **+20–30% лидов при том же бюджете** | Такопарки и драйверы (связка с рычагом 2), резиденты на выходные (не только туристы), корпоративные; AI-боты Simbiot = обработка лида ~0$ 24/7 → CAC падает |
|
||||||
|
| **6** | **PR «полностью AI-based агентство»** | S | отложенный: лиды + входящие заявки на франшизу/белые лейблы | Медиа-крючок первого в РБ AI-агентства в прокате; контент генерирует сам Simbiot — почти бесплатный охват; усиливает рычаги 1–3 |
|
||||||
|
|
||||||
|
### Последовательность внедрения
|
||||||
|
1. **Недели 1–2:** подписка на простой (рычаг 1) + запуск PR-волны (6) — быстрые деньги и охват.
|
||||||
|
2. **Месяц 1–2:** пилот аренды с выкупом на 2–3 эконом-машинах (2) + приём первых привлечённых машин (3).
|
||||||
|
3. **Месяц 2–3:** ребаланс парка (4), масштабирование подписки и привлечённого парка.
|
||||||
|
|
||||||
|
### Сводная оценка
|
||||||
|
Полное развёртывание рычагов 1–4 даёт расчётно **+60–100% выручки парка за 6–12 мес.** при умеренном CAPEX (только ребаланс), из них ~+20% — в первые 8 недель за счёт монетизации простоя. Оценки требуют сверки с фактическими данными парка (кол-во машин, ставки, vacancy) — запросить у Партнёра выгрузку до запуска.
|
||||||
|
|
||||||
|
**Роль Simbiot:** воронки и лид-скоринг (5), агент-диспетчер 24/7 по аренде/подписке (1), скоринг таксистов для аренды-с-выкупом (2), онбординг владельцев машин (3), контент-PR (6) — всё это и есть база «заработка на атрибутированном клиенте» из Части I.
|
||||||
115
artifacts/rental-x-simbiot-side-letter-15k.html
Normal file
115
artifacts/rental-x-simbiot-side-letter-15k.html
Normal file
|
|
@ -0,0 +1,115 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="ru">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Side Letter № 1 — урегулирование задолженности (~15 000 USD)</title>
|
||||||
|
<style>
|
||||||
|
:root{
|
||||||
|
--paper:#faf8f3; --ink:#15150f; --signal:#FF4F00;
|
||||||
|
--dim:#6b675e; --line:#d8d3c8; --card:#f3f0e8; --ok:#1d7a3e; --warn:#a3690f;
|
||||||
|
}
|
||||||
|
*{margin:0;padding:0;box-sizing:border-box}
|
||||||
|
body{background:var(--paper);color:var(--ink);font:16px/1.62 -apple-system,"Segoe UI",Roboto,Arial,sans-serif;-webkit-font-smoothing:antialiased}
|
||||||
|
.mono,code{font-family:ui-monospace,"JetBrains Mono","SF Mono",Menlo,Consolas,monospace}
|
||||||
|
.wrap{max-width:900px;margin:0 auto;padding:56px 24px 100px}
|
||||||
|
header{border-bottom:3px solid var(--ink);padding-bottom:26px;margin-bottom:36px}
|
||||||
|
.kicker{font-family:ui-monospace,Menlo,monospace;font-size:12px;letter-spacing:.22em;color:var(--signal);font-weight:700;text-transform:uppercase}
|
||||||
|
h1{font-size:clamp(30px,5vw,46px);line-height:1.05;font-weight:800;letter-spacing:-.02em;margin:10px 0 14px;text-transform:uppercase}
|
||||||
|
.sub{font-size:17px;color:var(--dim);max-width:720px}
|
||||||
|
section{margin:46px 0 0}
|
||||||
|
.snum{font-family:ui-monospace,Menlo,monospace;font-size:12px;color:var(--signal);letter-spacing:.2em;font-weight:700}
|
||||||
|
h2{font-size:24px;font-weight:800;margin:4px 0 6px;text-transform:uppercase}
|
||||||
|
.clause{border:1px solid var(--line);background:#fff;padding:14px 18px;margin:12px 0;max-width:860px}
|
||||||
|
.clause .cn{font-family:ui-monospace,Menlo,monospace;font-size:11px;color:var(--signal);font-weight:700;letter-spacing:.15em}
|
||||||
|
.clause p{font-size:14.5px;margin:4px 0}
|
||||||
|
.note{border:1px solid var(--ink);border-left:6px solid var(--warn);background:#fff;padding:14px 18px;margin:20px 0;max-width:860px}
|
||||||
|
.note b{font-family:ui-monospace,Menlo,monospace;font-size:12px;letter-spacing:.12em;text-transform:uppercase;color:var(--warn)}
|
||||||
|
table{width:100%;border-collapse:collapse;margin:18px 0;max-width:860px;background:#fff;border:1px solid var(--ink)}
|
||||||
|
th{font-family:ui-monospace,Menlo,monospace;font-size:11px;letter-spacing:.12em;text-transform:uppercase;text-align:left;padding:10px 12px;background:var(--ink);color:var(--paper)}
|
||||||
|
td{padding:10px 12px;border-top:1px solid var(--line);font-size:14px;vertical-align:top}
|
||||||
|
.sign{border:3px double var(--ink);background:#fff;padding:22px;margin:26px 0;display:grid;grid-template-columns:1fr 1fr;gap:20px}
|
||||||
|
@media(max-width:760px){.sign{grid-template-columns:1fr}}
|
||||||
|
.sig .org{font-family:ui-monospace,Menlo,monospace;font-size:11px;letter-spacing:.12em;text-transform:uppercase;color:var(--dim);margin-bottom:14px}
|
||||||
|
.sig .line{border-bottom:1px solid var(--ink);height:38px;margin-bottom:8px}
|
||||||
|
.sig .field{font-size:12.5px;color:var(--dim)}
|
||||||
|
footer{margin-top:60px;border-top:1px solid var(--line);padding-top:18px;color:var(--dim);font-size:12.5px}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="wrap">
|
||||||
|
|
||||||
|
<header>
|
||||||
|
<div class="kicker">Потребительский кооператив «Друзья Коллективного Разума» · Минск</div>
|
||||||
|
<h1>Side Letter № 1<br>к Соглашению о партнёрском вознаграждении</h1>
|
||||||
|
<div class="sub">Урегулирование исторической задолженности Партнёра перед Оператором: признание, фиксация, конвертация в инвестиционный вклад. Подписывается параллельно с Соглашением № 1, отдельно — чтобы не смешивать долги с партнёрскими расчётами.</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<div class="snum">СТОРОНЫ И ОСНОВАНИЕ</div>
|
||||||
|
<h2>Преамбула</h2>
|
||||||
|
<div class="clause"><p>Стороны — те же, что в Соглашении о партнёрском вознаграждении № 1 от «___» _______ 2026 г. («Соглашение»): Кооператив ПК «Друзья Коллективного Разума» («Оператор») и [наименование прокатной компании] («Партнёр»).</p></div>
|
||||||
|
<div class="clause"><p>Side Letter — неотъемлемая часть Соглашения, регулирует только исторический долг и не меняет условия о партнёрском вознаграждении (10%, атрибуция, порядок расчётов).</p></div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<div class="snum">01 / ПРИЗНАНИЕ ДОЛГА</div>
|
||||||
|
<h2>Признание задолженности</h2>
|
||||||
|
<div class="clause"><div class="cn">1.1</div><p>Стороны признают задолженность Партнёра перед Оператором за выполненные работы за период ~3 месяца (по актам/переписке, перечислены в Приложении к настоящему Side Letter).</p></div>
|
||||||
|
<div class="clause"><div class="cn">1.2</div><p>Стороны не имеют взаимных претензий по задолженности за указанный период, кроме урегулированной настоящим Side Letter.</p></div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<div class="snum">02 / ФИКСАЦИЯ СУММЫ</div>
|
||||||
|
<h2>Сумма и валютная привязка</h2>
|
||||||
|
<table>
|
||||||
|
<tr><th style="width:240px">Параметр</th><th>Значение</th></tr>
|
||||||
|
<tr><td>Эквивалент</td><td><b>~15 000 USD</b></td></tr>
|
||||||
|
<tr><td>Валютная привязка</td><td>выражен в <b>BTC</b> по курсу BTC/USD на дату образования долга; рублёвая/долларовая переоценка — по курсу на дату расчётов</td></tr>
|
||||||
|
<tr><td>Индексация</td><td>инфляционная индексация за период образования долга (если применимо)</td></tr>
|
||||||
|
<tr><td>Дата образования</td><td>[заполнить: последний акт за период долга]</td></tr>
|
||||||
|
</table>
|
||||||
|
<div class="clause"><div class="cn">2.1</div><p>Итоговая сумма фиксируется в Приложении к Side Letter в BTC и в USD-эквиваленте; стороны подтверждают её подписями.</p></div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<div class="snum">03 / КОНВЕРТАЦИЯ</div>
|
||||||
|
<h2>Конвертация долга в инвестицию</h2>
|
||||||
|
<div class="clause"><div class="cn">3.1</div><p>С момента подписания Side Letter долг <b>полностью конвертируется в инвестиционный вклад Партнёра в Проект «Интеграция Simbiot в прокатный бизнес»</b> (юнит: развитие Партнёра как витринного кейса платформы) на условиях инвестиционного соглашения (Приложение № 2 к Соглашению: доля / profit-share, [заполнить]).</p></div>
|
||||||
|
<div class="clause"><div class="cn">3.2</div><p>С момента конвертации Партнёр не имеет требований к Оператору по возврату суммы долга как денежного обязательства; права Партнёра реализуются исключительно через условия инвестиционного соглашения (доля/profit-share, порядок распределения, выход).</p></div>
|
||||||
|
<div class="clause"><div class="cn">3.3</div><p>Инвестиционный вклад не гарантирует доходность и не является займом; риск убытка несёт Партнёр как инвестор.</p></div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<div class="snum">04 / ПОРЯДОК</div>
|
||||||
|
<h2>Подписание и юридическая проверка</h2>
|
||||||
|
<div class="clause"><div class="cn">4.1</div><p>Side Letter подписывается одновременно с Соглашением № 1. Юридическая проверка обоих документов до подписания обязательна.</p></div>
|
||||||
|
<div class="clause"><div class="cn">4.2</div><p>До подписания стороны заполняют: дату образования долга, состав работ (Приложение), точную сумму в BTC + USD-эквивалент, условия инвестиционного соглашения (доля/profit-share).</p></div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<div class="snum">05 / ПОДПИСИ</div>
|
||||||
|
<h2>Реквизиты</h2>
|
||||||
|
<div class="sign">
|
||||||
|
<div class="sig">
|
||||||
|
<div class="org">Оператор · Кооператив</div>
|
||||||
|
<div class="line"></div>
|
||||||
|
<div class="field">Председатель правления · подпись</div>
|
||||||
|
<div class="field">Alex Shkor · «___» _______ 2026 г.</div>
|
||||||
|
</div>
|
||||||
|
<div class="sig">
|
||||||
|
<div class="org">Партнёр · [наименование прокатной компании]</div>
|
||||||
|
<div class="line"></div>
|
||||||
|
<div class="field">[должность, ФИО] · подпись</div>
|
||||||
|
<div class="field">М.П. · «___» _______ 2026 г.</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<footer>
|
||||||
|
Черновик для обсуждения, не оферта. Юридическая проверка перед подписанием обязательна. Основа: встреча 04.10.2026 + решение Alex от 04.10.2026. Парный документ: rental-x-simbiot-agreement.html
|
||||||
|
</footer>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
35
artifacts/self-improvement-log.md
Normal file
35
artifacts/self-improvement-log.md
Normal file
|
|
@ -0,0 +1,35 @@
|
||||||
|
# Журнал ночной самодоработки харнеса
|
||||||
|
|
||||||
|
Директива Alex (голосовое m-516173718, эскалация e-261005074512): ночами
|
||||||
|
создавать себе задачи, ставить эксперименты над собой, смотреть что работает
|
||||||
|
лучше/хуже и продолжать дорабатываться.
|
||||||
|
|
||||||
|
Цикл: крон в 00:00 UTC (03:00 Минск) подаёт задачу «Ночная самодоработка» [M];
|
||||||
|
ближайший hour-тик выполняет; каждая итерация дописывает строку ниже.
|
||||||
|
|
||||||
|
Формат строки (одна на итерацию):
|
||||||
|
|
||||||
|
`#N | дата | гипотеза | метрика до → после | вывод (работает/нет/нужно больше данных)`
|
||||||
|
|
||||||
|
Правила:
|
||||||
|
- один эксперимент за ночь, мелкий (лёгкий — провести сразу, тяжёлый — задачей на день);
|
||||||
|
- фильтр AUM: эксперимент должен служить «≥ +1% к AUM в горизонте год+»;
|
||||||
|
- ночь — тишина в чате (кроме критичного); результат поднимает утренний дайджест;
|
||||||
|
- не выдумывать метрики: только измеримое из стора (`cli.py events/status/aum`),
|
||||||
|
логов тиков и этого журнала.
|
||||||
|
|
||||||
|
## Итерации
|
||||||
|
|
||||||
|
#0 | 2026-10-05 | Калибровка цикла (не эксперимент) | механизм: крон-податчик + журнал заведены, первая задача в очереди | ожидаем первую ночную итерацию
|
||||||
|
|
||||||
|
#1 | 2026-10-05 | Дедуп «ВЫЖИМОК НИЖНИХ УРОВНЕЙ» в промпте тика: тихий мин-уровень кладёт 6 идентичных [gate:quiet]-строк в каждый шаг промпта hour-тика; гипотеза — схлопывание повторов с счётчиком ×N срежет burn без потери информации | секция ВЫЖИМОК в hour-промпте: 6 строк/522 байт → 1 строка/95 байт (−82% секции, −427 байт/шаг; при 991 шаге/сутки ≈ −0.4М байт ≈ ~100k промпт-токенов/сутки при 57.8k avg/шаг — оценка) | работает: фикс в scheduler.py build_context (дедуп по (level, text)), верифицирован на живой модели (×6 подряд в контексте), test_gate + test_task_batch ALL PASS; контроль — avg prompt/step в BudgetSpent hour за следующие сутки (базлайн 57,781)
|
||||||
|
|
||||||
|
## Ретро суток 2026-10-04T13:00 → 2026-10-05T13:00 (UTC)
|
||||||
|
|
||||||
|
- Задачи: закрыто 27 (в т.ч. 8 L), подано 29, эскалаций открыто 0 (3 raised / 3 acked).
|
||||||
|
- Падения: 1 CycleError hour (14:48, таймаут воркера 2880с — тик потерян целиком; max инвокация 2872с из 990 завершённых).
|
||||||
|
- Гейт работает: 1381 GateSkipped min-тиков = LLM не запускался в тишине (экономия).
|
||||||
|
- Burn: hour 57.3M промпт-токенов (991 шаг, avg 57.8k/шаг, completion всего 1.1%); min 5.5M (520 шагов, avg 10.6k). Доминирующая статья расхода — раздувание промпта hour-шага, а не число вызовов (инвокаций всего 8+56).
|
||||||
|
- AUM 26.7 у.е.; burn 9router 7д 1417.88 — burn остаётся прямым минусом к AUM, главный рычаг — тоньше контекст шага.
|
||||||
|
- Вывод: следующие эксперименты — что именно раздувает avg 57.8k/шаг (история сообщений? RAG? батч?), паттерн «дедуп + кап» применим к секциям эскалаций/инбокса.
|
||||||
|
|
||||||
27
artifacts/self-review-2026-10-04.md
Normal file
27
artifacts/self-review-2026-10-04.md
Normal file
|
|
@ -0,0 +1,27 @@
|
||||||
|
# Самоанализ харнеса — 2026-10-04 (t-261004152714)
|
||||||
|
|
||||||
|
## Цифры
|
||||||
|
- Задачи: 147 всего — 142 done (S 51 / M 61 / L 30), 2 blocked, 3 running. Провалов мало, поток стабилен.
|
||||||
|
- Эскалации: 79 (warn 56 / info 19 / critical 4) — все acked, открытых 0.
|
||||||
|
- Инбокс TG: 100 сообщений (text 81, voice 15, video 1); транскрипция 16/16 непустая после фикса.
|
||||||
|
- Ошибки уровней: min 38, hour 6, day 1. Главный паттерн — таймаут LLM-воркера (480с/2880с).
|
||||||
|
|
||||||
|
## Что работает
|
||||||
|
- Лестница sec→min→hour→day с батчем 3 задач/тик: пропускная способность есть, анти-старение очереди соблюдается.
|
||||||
|
- ES-стор + ed25519-подписи: verify проходит, тампер-цепочка цела.
|
||||||
|
- Идемпотентная доставка в TG (tg-notify/tg-deliver-file, SKIP на повторе) — дублей нет (t-260930083937).
|
||||||
|
- Голосовые: двухъярусный ASR, короткие фразы → large-v3-turbo, кэш по file_unique_id.
|
||||||
|
- Эскалации доходят и закрываются (e-261004144805 — фикс «смерть тика не воскрешает закрытые задачи» уже в git).
|
||||||
|
|
||||||
|
## Что болит (→ задачи саморазвития)
|
||||||
|
1. **Звук из видео не извлекался** — kind=video не входил в TRANSCRIBABLE_KINDS и поллер не звал транскрипцию. ИСПРАВЛЕНО в этом тике: ffmpeg-извлечение дорожки (16к моно opus) + turbo-роутинг для ≤60с; e2e на m-516173698 подтверждён. Осталось: тесты и наблюдение за памятью.
|
||||||
|
2. **Память инбокс-поллера 3.7G peak** (systemd Consumption) — транскрипция в процессе поллера; нужен MemoryMax и/или вынос ASR-вызова из поллера.
|
||||||
|
3. **Кириллица в setMyCommands даёт HTTP 400** (костыль-транслит latin) — бот теряет русские подписи команд.
|
||||||
|
4. **Таймауты LLM-воркеров** (min×38, hour×6) — причина №1 потерь тиков; воркер убивается до записи BudgetSpent.
|
||||||
|
5. **Клоны задач из служебных 'list'** — ловушка чинилась дважды; нужен регресс-тест на генерацию задач из inbox-текста.
|
||||||
|
|
||||||
|
## Задачи саморазвития (созданы, проверяемый результат)
|
||||||
|
- t-261004160531-ed25ed [M]: тесты voice-пайплайна (extract_audio_track + роутинг) — pytest зелёный, кейс видео↔текст.
|
||||||
|
- t-261004160531-780666 [L]: память поллера — MemoryMax + вынос тяжёлой транскрипции из поллера; peak < 1G за сутки.
|
||||||
|
- t-261004160532-690fcb [M]: TG-команды кириллицей — setMyCommands без 400 и без транслита.
|
||||||
|
- t-261004160532-0fb500 [M]: регресс-тест «служебный текст inbox не порождает задачу-клон».
|
||||||
355
artifacts/shkor-beauty-prototype.html
Normal file
355
artifacts/shkor-beauty-prototype.html
Normal file
|
|
@ -0,0 +1,355 @@
|
||||||
|
<!doctype html>
|
||||||
|
<html lang="ru">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<title>Shkor beauty — корейская косметика в Минске (прототип витрины)</title>
|
||||||
|
<meta name="description" content="Shkor beauty — витрина магазина корейской косметики: уход за лицом, телом и волосами. Прототип каталога, заказы пока не принимаются.">
|
||||||
|
<style>
|
||||||
|
:root{
|
||||||
|
--ink:#1c1a17; --paper:#faf7f2; --card:#ffffff; --line:#e4ddd2;
|
||||||
|
--acc:#0e7a5f; --acc-soft:#e7f2ee; --muted:#6f6a61; --sale:#b3352c;
|
||||||
|
--sans:"Segoe UI",-apple-system,"Helvetica Neue",Arial,sans-serif;
|
||||||
|
--mono:ui-monospace,"SF Mono",Menlo,Consolas,monospace;
|
||||||
|
}
|
||||||
|
*{box-sizing:border-box;margin:0;padding:0}
|
||||||
|
body{background:var(--paper);color:var(--ink);font-family:var(--sans);line-height:1.5}
|
||||||
|
a{color:inherit;text-decoration:none}
|
||||||
|
.wrap{max-width:1100px;margin:0 auto;padding:0 20px}
|
||||||
|
.note{font-family:var(--mono);font-size:12px;background:var(--ink);color:#f4f0e6;padding:6px 0;text-align:center}
|
||||||
|
.note b{color:#8fd4bd}
|
||||||
|
|
||||||
|
header{background:var(--card);border-bottom:1px solid var(--line);position:sticky;top:0;z-index:5}
|
||||||
|
.hbar{display:flex;align-items:center;gap:18px;padding:14px 0}
|
||||||
|
.logo{font-size:22px;font-weight:700;letter-spacing:-.02em;white-space:nowrap}
|
||||||
|
.logo span{color:var(--acc)}
|
||||||
|
.logo small{display:block;font-family:var(--mono);font-weight:400;font-size:10px;letter-spacing:.14em;text-transform:uppercase;color:var(--muted)}
|
||||||
|
.search{flex:1;display:flex}
|
||||||
|
.search input{flex:1;border:1px solid var(--line);border-right:none;padding:10px 14px;font:inherit;font-size:14px;background:var(--paper);border-radius:6px 0 0 6px;outline:none}
|
||||||
|
.search input:focus{border-color:var(--acc)}
|
||||||
|
.search button{border:none;background:var(--acc);color:#fff;padding:0 16px;border-radius:0 6px 6px 0;cursor:pointer;font:inherit;font-size:14px}
|
||||||
|
.hlinks{display:flex;gap:16px;font-size:14px;white-space:nowrap}
|
||||||
|
.hlinks a:hover{color:var(--acc)}
|
||||||
|
nav.cats{display:flex;gap:4px;overflow-x:auto;padding:0 0 10px;font-size:14px}
|
||||||
|
nav.cats a{padding:6px 12px;border-radius:20px;border:1px solid transparent;white-space:nowrap}
|
||||||
|
nav.cats a:hover{border-color:var(--acc);color:var(--acc)}
|
||||||
|
nav.cats a.all{background:var(--acc-soft);color:var(--acc);font-weight:600}
|
||||||
|
|
||||||
|
.hero{background:linear-gradient(180deg,var(--acc-soft),var(--paper));border-bottom:1px solid var(--line)}
|
||||||
|
.hero .wrap{display:flex;gap:40px;align-items:center;padding-top:52px;padding-bottom:52px;flex-wrap:wrap}
|
||||||
|
.hero h1{font-size:clamp(30px,4.5vw,46px);line-height:1.1;letter-spacing:-.02em;max-width:16ch}
|
||||||
|
.hero p{margin-top:14px;max-width:48ch;color:var(--muted);font-size:17px}
|
||||||
|
.hero .btn{display:inline-block;margin-top:22px;background:var(--acc);color:#fff;padding:13px 26px;border-radius:6px;font-weight:600}
|
||||||
|
.hero .btn:hover{filter:brightness(1.08)}
|
||||||
|
.rhythm{flex:1;min-width:260px;display:flex;align-items:flex-end;gap:4px;height:120px;opacity:.9}
|
||||||
|
.rhythm span{flex:1;background:var(--acc);border-radius:3px 3px 0 0;animation:grow 2.8s ease-in-out infinite}
|
||||||
|
@keyframes grow{0%,100%{transform:scaleY(.3)}50%{transform:scaleY(1)}}
|
||||||
|
.rhythm span:nth-child(3n){background:var(--ink)}
|
||||||
|
|
||||||
|
section{padding:46px 0}
|
||||||
|
h2{font-size:clamp(22px,3vw,30px);letter-spacing:-.01em;margin-bottom:6px}
|
||||||
|
.sub{color:var(--muted);font-size:15px;margin-bottom:24px}
|
||||||
|
|
||||||
|
.catgrid{display:grid;grid-template-columns:repeat(auto-fit,minmax(170px,1fr));gap:14px}
|
||||||
|
.cat{background:var(--card);border:1px solid var(--line);border-radius:10px;padding:20px 18px;transition:transform .15s ease,border-color .15s ease}
|
||||||
|
.cat:hover{transform:translateY(-2px);border-color:var(--acc)}
|
||||||
|
.cat .ic{width:38px;height:38px;border-radius:8px;background:var(--acc-soft);margin-bottom:12px;position:relative}
|
||||||
|
.cat .ic::after{content:"";position:absolute;inset:11px;background:var(--acc);border-radius:50%}
|
||||||
|
.cat.a .ic::after{border-radius:2px}
|
||||||
|
.cat.b .ic::after{height:6px;top:16px}
|
||||||
|
.cat.c .ic::after{width:6px;left:16px}
|
||||||
|
.cat.d .ic::after{border-radius:50%;inset:14px 11px}
|
||||||
|
.cat h3{font-size:16px;margin-bottom:4px}
|
||||||
|
.cat p{font-size:13px;color:var(--muted)}
|
||||||
|
|
||||||
|
.chips{display:flex;flex-wrap:wrap;gap:8px;margin-bottom:8px}
|
||||||
|
.chips a{font-size:13px;padding:7px 14px;border:1px solid var(--line);border-radius:20px;background:var(--card)}
|
||||||
|
.chips a:hover{border-color:var(--acc);color:var(--acc)}
|
||||||
|
|
||||||
|
.grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(230px,1fr));gap:18px}
|
||||||
|
.p{background:var(--card);border:1px solid var(--line);border-radius:10px;overflow:hidden;display:flex;flex-direction:column;transition:box-shadow .15s ease}
|
||||||
|
.p:hover{box-shadow:0 6px 18px rgba(28,26,23,.08)}
|
||||||
|
.ph{aspect-ratio:1/1;background:var(--acc-soft);display:flex;align-items:center;justify-content:center;position:relative}
|
||||||
|
.ph .b{font-family:var(--mono);font-size:15px;letter-spacing:.06em;color:var(--acc);border:1.5px solid var(--acc);padding:8px 14px;border-radius:4px;background:var(--card)}
|
||||||
|
.ph .tag{position:absolute;top:10px;left:10px;background:var(--sale);color:#fff;font-size:11px;font-weight:700;padding:3px 8px;border-radius:4px}
|
||||||
|
.ph .tag.new{background:var(--acc)}
|
||||||
|
.pb{padding:14px 16px 16px;display:flex;flex-direction:column;flex:1}
|
||||||
|
.pb .cat-l{font-family:var(--mono);font-size:10px;letter-spacing:.1em;text-transform:uppercase;color:var(--muted)}
|
||||||
|
.pb .brand{font-size:12px;font-weight:700;color:var(--acc);margin-top:4px}
|
||||||
|
.pb h3{font-size:14.5px;font-weight:400;line-height:1.35;margin:4px 0 10px;flex:1}
|
||||||
|
.price{display:flex;align-items:baseline;gap:8px}
|
||||||
|
.price .now{font-size:17px;font-weight:700}
|
||||||
|
.price .old{font-size:13px;color:var(--muted);text-decoration:line-through}
|
||||||
|
.p button{margin-top:12px;border:1.5px solid var(--ink);background:transparent;padding:9px;font:inherit;font-size:13.5px;font-weight:600;border-radius:6px;cursor:pointer;transition:.15s}
|
||||||
|
.p button:hover{background:var(--ink);color:#fff}
|
||||||
|
|
||||||
|
.brands{display:flex;flex-wrap:wrap;gap:10px}
|
||||||
|
.brands span{font-family:var(--mono);font-size:13px;border:1px solid var(--line);padding:8px 16px;border-radius:6px;background:var(--card);color:var(--muted)}
|
||||||
|
.brands span:hover{color:var(--acc);border-color:var(--acc)}
|
||||||
|
|
||||||
|
.dark{background:var(--ink);color:#f4f0e6}
|
||||||
|
.dark .sub{color:#b9b2a4}
|
||||||
|
.dark form{display:flex;gap:10px;max-width:480px;margin-top:8px;flex-wrap:wrap}
|
||||||
|
.dark input{flex:1;min-width:200px;border:1px solid #4a453d;background:transparent;color:inherit;padding:12px 14px;border-radius:6px;font:inherit;outline:none}
|
||||||
|
.dark input:focus{border-color:var(--acc)}
|
||||||
|
.dark button{background:var(--acc);border:none;color:#fff;padding:12px 22px;border-radius:6px;font:inherit;font-weight:600;cursor:pointer}
|
||||||
|
|
||||||
|
footer{padding:30px 0 40px;color:var(--muted);font-size:13.5px}
|
||||||
|
footer .wrap{display:flex;justify-content:space-between;gap:20px;flex-wrap:wrap}
|
||||||
|
footer .proto{font-family:var(--mono);font-size:12px}
|
||||||
|
|
||||||
|
#toast{position:fixed;left:50%;bottom:24px;transform:translate(-50%,80px);background:var(--ink);color:#fff;padding:12px 20px;border-radius:8px;font-size:14px;opacity:0;transition:.25s ease;z-index:9}
|
||||||
|
#toast.show{transform:translate(-50%,0);opacity:1}
|
||||||
|
|
||||||
|
@media (max-width:720px){
|
||||||
|
.hlinks{display:none}
|
||||||
|
.hero .wrap{padding-top:34px;padding-bottom:34px}
|
||||||
|
.rhythm{display:none}
|
||||||
|
}
|
||||||
|
@media (prefers-reduced-motion:reduce){
|
||||||
|
*{animation:none!important;transition:none!important;scroll-behavior:auto!important}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<div class="note">Прототип витрины — шаг 1 из плана Shkor beauty. Наполнение демонстрационное, оплата не подключена.</div>
|
||||||
|
|
||||||
|
<header>
|
||||||
|
<div class="wrap">
|
||||||
|
<div class="hbar">
|
||||||
|
<a class="logo" href="#">Shkor <span>beauty</span><small>корейская косметика · Минск</small></a>
|
||||||
|
<div class="search">
|
||||||
|
<input type="search" placeholder="Поиск: тонер, пэды, гидрофильное масло…">
|
||||||
|
<button>Найти</button>
|
||||||
|
</div>
|
||||||
|
<div class="hlinks">
|
||||||
|
<a href="#catalog">Каталог</a>
|
||||||
|
<a href="#brands">Бренды</a>
|
||||||
|
<a href="#sub">Подписка</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<nav class="cats" aria-label="Разделы">
|
||||||
|
<a class="all" href="#catalog">Весь каталог</a>
|
||||||
|
<a href="#face">Уход за лицом</a>
|
||||||
|
<a href="#body">Уход за телом</a>
|
||||||
|
<a href="#hair">Уход за волосами</a>
|
||||||
|
<a href="#makeup">Декоративная косметика</a>
|
||||||
|
<a href="#acc">Аксессуары</a>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div class="hero">
|
||||||
|
<div class="wrap">
|
||||||
|
<div>
|
||||||
|
<h1>Корейский уход — с полки до двери за 1 день</h1>
|
||||||
|
<p>Каталог проверенных K-beauty брендов в Минске: очищение, тонизирование, сыворотки, SPF. Самовывоз и доставка по Беларуси.</p>
|
||||||
|
<a class="btn" href="#catalog">Смотреть каталог</a>
|
||||||
|
</div>
|
||||||
|
<div class="rhythm" aria-hidden="true">
|
||||||
|
<span></span><span></span><span></span><span></span><span></span><span></span>
|
||||||
|
<span></span><span></span><span></span><span></span><span></span><span></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<section id="catalog">
|
||||||
|
<div class="wrap">
|
||||||
|
<h2>Каталог</h2>
|
||||||
|
<p class="sub">Пять разделов, как у эталона рынка — структура навигации Korealab.</p>
|
||||||
|
<div class="catgrid">
|
||||||
|
<a class="cat a" href="#face"><span class="ic"></span><h3>Уход за лицом</h3><p>Очищение, тонеры, сыворотки, маски, SPF</p></a>
|
||||||
|
<a class="cat b" href="#body"><span class="ic"></span><h3>Уход за телом</h3><p>Гели для душа, лосьоны, кремы для рук</p></a>
|
||||||
|
<a class="cat c" href="#hair"><span class="ic"></span><h3>Уход за волосами</h3><p>Шампуни, маски, ампулы, филлеры</p></a>
|
||||||
|
<a class="cat d" href="#makeup"><span class="ic"></span><h3>Декоративная косметика</h3><p>Тинты, BB-кремы, бальзамы для губ</p></a>
|
||||||
|
<a class="cat a" href="#acc"><span class="ic"></span><h3>Аксессуары</h3><p>Спонжи, пады, пилочки, зеркала</p></a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="face" style="padding-top:0">
|
||||||
|
<div class="wrap">
|
||||||
|
<h2>Уход за лицом</h2>
|
||||||
|
<p class="sub">Подкатегории первого уровня — быстрый вход в нужный шаг рутины.</p>
|
||||||
|
<div class="chips">
|
||||||
|
<a href="#demo">Очищение</a><a href="#demo">Тонизирование</a><a href="#demo">Сыворотки и эссенции</a>
|
||||||
|
<a href="#demo">Патчи для глаз</a><a href="#demo">Тканевые маски</a><a href="#demo">Кремы</a>
|
||||||
|
<a href="#demo">Солнцезащитные средства</a><a href="#demo">Средства с кислотами</a>
|
||||||
|
</div>
|
||||||
|
<div class="grid" id="demo">
|
||||||
|
|
||||||
|
<article class="p">
|
||||||
|
<div class="ph"><span class="tag">−30%</span><span class="b">SKIN627</span></div>
|
||||||
|
<div class="pb">
|
||||||
|
<span class="cat-l">Сыворотки</span><span class="brand">SKIN627</span>
|
||||||
|
<h3>Увлажняющая сыворотка для лица Hyaluron Centella Ampoule</h3>
|
||||||
|
<div class="price"><span class="now">35,00 руб.</span><span class="old">50,00 руб.</span></div>
|
||||||
|
<button>В корзину</button>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
|
||||||
|
<article class="p">
|
||||||
|
<div class="ph"><span class="tag new">Новинка</span><span class="b">JMsolution</span></div>
|
||||||
|
<div class="pb">
|
||||||
|
<span class="cat-l">Пэды</span><span class="brand">JMsolution</span>
|
||||||
|
<h3>Увлажняющие пэды с гиалуроновой кислотой Multi Hyaluronic</h3>
|
||||||
|
<div class="price"><span class="now">59,00 руб.</span></div>
|
||||||
|
<button>В корзину</button>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
|
||||||
|
<article class="p">
|
||||||
|
<div class="ph"><span class="b">Beauty of Joseon</span></div>
|
||||||
|
<div class="pb">
|
||||||
|
<span class="cat-l">SPF</span><span class="brand">Beauty of Joseon</span>
|
||||||
|
<h3>Солнцезащитный гель Relief Sun SPF50+ PA++++</h3>
|
||||||
|
<div class="price"><span class="now">49,00 руб.</span></div>
|
||||||
|
<button>В корзину</button>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
|
||||||
|
<article class="p">
|
||||||
|
<div class="ph"><span class="tag">−25%</span><span class="b">Biodance</span></div>
|
||||||
|
<div class="pb">
|
||||||
|
<span class="cat-l">Маски</span><span class="brand">Biodance</span>
|
||||||
|
<h3>Коллагеновая маска-пленка Collagen Wrapping Peel-off Mask</h3>
|
||||||
|
<div class="price"><span class="now">12,00 руб.</span><span class="old">16,00 руб.</span></div>
|
||||||
|
<button>В корзину</button>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
|
||||||
|
<article class="p">
|
||||||
|
<div class="ph"><span class="b">Anua</span></div>
|
||||||
|
<div class="pb">
|
||||||
|
<span class="cat-l">Тонеры</span><span class="brand">Anua</span>
|
||||||
|
<h3>Успокаивающий тонер с сердечником Centella Toner 77%</h3>
|
||||||
|
<div class="price"><span class="now">62,00 руб.</span></div>
|
||||||
|
<button>В корзину</button>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
|
||||||
|
<article class="p">
|
||||||
|
<div class="ph"><span class="b">Banila Co</span></div>
|
||||||
|
<div class="pb">
|
||||||
|
<span class="cat-l">Очищение</span><span class="brand">Banila Co</span>
|
||||||
|
<h3>Гидрофильное масло Clean It Zero Original</h3>
|
||||||
|
<div class="price"><span class="now">54,00 руб.</span></div>
|
||||||
|
<button>В корзину</button>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
|
||||||
|
<article class="p">
|
||||||
|
<div class="ph"><span class="b">SKIN627</span></div>
|
||||||
|
<div class="pb">
|
||||||
|
<span class="cat-l">Сыворотки</span><span class="brand">SKIN627</span>
|
||||||
|
<h3>Сыворотка для лица с ретинолом Retinol Pantenol Ampoule</h3>
|
||||||
|
<div class="price"><span class="now">35,00 руб.</span></div>
|
||||||
|
<button>В корзину</button>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
|
||||||
|
<article class="p">
|
||||||
|
<div class="ph"><span class="tag new">Новинка</span><span class="b">A'PIEU</span></div>
|
||||||
|
<div class="pb">
|
||||||
|
<span class="cat-l">Патчи</span><span class="brand">A'PIEU</span>
|
||||||
|
<h3>Патчи для глаз с молочной кислотой Milk One-Day Perfector</h3>
|
||||||
|
<div class="price"><span class="now">18,00 руб.</span></div>
|
||||||
|
<button>В корзину</button>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="body" style="padding-top:0">
|
||||||
|
<div class="wrap">
|
||||||
|
<h2>Уход за телом и волосами</h2>
|
||||||
|
<p class="sub">Витрина разделов вне лица — по мотивам карточек эталона.</p>
|
||||||
|
<div class="grid">
|
||||||
|
<article class="p">
|
||||||
|
<div class="ph"><span class="b">WEPROVE</span></div>
|
||||||
|
<div class="pb">
|
||||||
|
<span class="cat-l">Для душа</span><span class="brand">WEPROVE</span>
|
||||||
|
<h3>Обновляющий гель для душа Renewing Aloe Shower Gel</h3>
|
||||||
|
<div class="price"><span class="now">29,00 руб.</span></div>
|
||||||
|
<button>В корзину</button>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
<article class="p">
|
||||||
|
<div class="ph"><span class="tag">−15%</span><span class="b">WEPROVE</span></div>
|
||||||
|
<div class="pb">
|
||||||
|
<span class="cat-l">Шампуни</span><span class="brand">WEPROVE</span>
|
||||||
|
<h3>Восстанавливающий шампунь Daily Hair Repairing Shampoo</h3>
|
||||||
|
<div class="price"><span class="now">23,60 руб.</span><span class="old">27,80 руб.</span></div>
|
||||||
|
<button>В корзину</button>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
<article class="p">
|
||||||
|
<div class="ph"><span class="b">Care:Nel</span></div>
|
||||||
|
<div class="pb">
|
||||||
|
<span class="cat-l">Кремы для рук</span><span class="brand">Care:Nel</span>
|
||||||
|
<h3>Питательный крем для рук с ши и центеллой</h3>
|
||||||
|
<div class="price"><span class="now">14,00 руб.</span></div>
|
||||||
|
<button>В корзину</button>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
<article class="p">
|
||||||
|
<div class="ph"><span class="b">Ciracle</span></div>
|
||||||
|
<div class="pb">
|
||||||
|
<span class="cat-l">Аксессуары</span><span class="brand">Ciracle</span>
|
||||||
|
<h3>Силиконовый спонж для умывания с чехлом</h3>
|
||||||
|
<div class="price"><span class="now">9,00 руб.</span></div>
|
||||||
|
<button>В корзину</button>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="brands">
|
||||||
|
<div class="wrap">
|
||||||
|
<h2>Бренды</h2>
|
||||||
|
<p class="sub">Формат «Алфавит брендов» как у эталона — вход через доверие к марке.</p>
|
||||||
|
<div class="brands">
|
||||||
|
<span>A'PIEU</span><span>ANSKIN</span><span>Anua</span><span>Atopalm</span>
|
||||||
|
<span>AXIS-Y</span><span>Banila Co</span><span>Beauty of Joseon</span><span>Biodance</span>
|
||||||
|
<span>Care:Nel</span><span>Celimax</span><span>Cos de Baha</span><span>JMsolution</span>
|
||||||
|
<span>SKIN627</span><span>WEPROVE</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="dark" id="sub">
|
||||||
|
<div class="wrap">
|
||||||
|
<h2>Подписка на поступления</h2>
|
||||||
|
<p class="sub">Одно письмо в неделю: новинки, скидки и разборы составов. Без спама.</p>
|
||||||
|
<form onsubmit="event.preventDefault();toast('Прототип: подписка не подключена')">
|
||||||
|
<input type="email" placeholder="your@email.com" required>
|
||||||
|
<button>Подписаться</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<footer>
|
||||||
|
<div class="wrap">
|
||||||
|
<div>Shkor beauty — витрина корейской косметики. Прототип шага 1: каталог без платежей.</div>
|
||||||
|
<div class="proto">собрано харнесом Simbiot · кооператив 42 · cil.by</div>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
|
||||||
|
<div id="toast" role="status"></div>
|
||||||
|
<script>
|
||||||
|
function toast(t){
|
||||||
|
var el=document.getElementById('toast');
|
||||||
|
el.textContent=t;el.classList.add('show');
|
||||||
|
clearTimeout(el._t);el._t=setTimeout(function(){el.classList.remove('show')},2200);
|
||||||
|
}
|
||||||
|
document.querySelectorAll('.p button').forEach(function(b){
|
||||||
|
b.addEventListener('click',function(){toast('Прототип: заказ и оплата будут на шаге 2')});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
79
artifacts/shkor/README.md
Normal file
79
artifacts/shkor/README.md
Normal file
|
|
@ -0,0 +1,79 @@
|
||||||
|
# Shkor Beauty Assistant
|
||||||
|
|
||||||
|
Beauty-ассистент магазина **Shkor beauty**: анализирует состояние кожи, подбирает продукты
|
||||||
|
из каталога витрины, оформляет заказ с доставкой и выдаёт инструкции по применению.
|
||||||
|
Подключается к **любому агенту** (LLM-бот, MCP-клиент, крон-скрипт, мобильное приложение).
|
||||||
|
|
||||||
|
Задача-источник: `t-261005120225-672f52` (требования Alex). Каталог синхронизирован
|
||||||
|
с прототипом витрины `../shkor-beauty-prototype.html` (t-261005115918-d38b36).
|
||||||
|
|
||||||
|
## Состав
|
||||||
|
|
||||||
|
| Файл | Что это |
|
||||||
|
|---|---|
|
||||||
|
| `assistant.py` | Ядро: анализ кожи, рекомендации, заказы, инструкции. Один вход `handle(request) -> dict` |
|
||||||
|
| `connector.py` | Универсальный коннектор: STDIO (MCP-стиль), HTTP API, CLI, демо-чат |
|
||||||
|
| `catalog.json` | 12 товаров витрины с мета-данными (concerns, actives, routine_step) |
|
||||||
|
| `orders.json` | Локальный леджер заказов (создаётся при первом заказе) |
|
||||||
|
|
||||||
|
## Подключение любым агентом
|
||||||
|
|
||||||
|
**1) STDIO, MCP-стиль** — для Claude/любого MCP-клиента:
|
||||||
|
```json
|
||||||
|
{"mcpServers": {"shkor": {"command": "python3", "args": ["/root/simbiot-harness/artifacts/shkor/connector.py"], "transport": "stdio"}}}
|
||||||
|
```
|
||||||
|
Протокол: построчный JSONL. `{"method":"tools/list"}` → список инструментов;
|
||||||
|
`{"method":"tools/call","params":{"name":"analyze_skin","arguments":{...}},"id":1}` → результат.
|
||||||
|
|
||||||
|
**2) HTTP API** — агент = любой HTTP-клиент:
|
||||||
|
```bash
|
||||||
|
connector.py --http 127.0.0.1:8791
|
||||||
|
curl -XPOST localhost:8791/call -d '{"name":"recommend","arguments":{"concerns":["dryness"]}}'
|
||||||
|
```
|
||||||
|
Роуты: `GET /health`, `POST /tools/list`, `POST /call`.
|
||||||
|
|
||||||
|
**3) CLI разовый вызов** — крон/скрипты:
|
||||||
|
```bash
|
||||||
|
connector.py --call analyze_skin --args '{"self_report":"стянутость и покраснения"}'
|
||||||
|
```
|
||||||
|
|
||||||
|
**4) Живая проверка:** `connector.py --demo` — полный сценарий: анализ → подбор → заказ → инструкция → статус.
|
||||||
|
|
||||||
|
## Инструменты (7)
|
||||||
|
|
||||||
|
- `analyze_skin` `{self_report?, symptoms?, photo_report?}` → тип кожи, concerns, hydration-индекс,
|
||||||
|
состояние барьера, фокус рутины. `photo_report` — принимаем отчёт vision-агента по селфи
|
||||||
|
(JSON или текст): мержим с самоотчётом, источники считаем отдельно.
|
||||||
|
- `recommend` `{concerns?, skin_type?, budget_max?}` → подобранные карточки с объяснением «почему».
|
||||||
|
- `search` `{query?, category?}` → поиск по каталогу (бренд/название/актив/категория).
|
||||||
|
- `place_order` `{items:[{id,qty}], delivery?, name?, phone?, address?, pay_method?, pay_link?}` →
|
||||||
|
заказ `SB-XXXXXXXX`, сумма, доставка. Платёж: `cash_on_delivery` или `pay_link` от
|
||||||
|
платёжного агента (Stripe/Альфа-Банк) — ассистент не хранит карты.
|
||||||
|
- `check_order` `{order_id}` → статус заказа и доставки.
|
||||||
|
- `delivery_options` `{cart_total?}` → курьер Минск / Европочта / самовывоз, бесплатно от 80 BYN.
|
||||||
|
- `usage_instructions` `{product_ids}` → пошаговые инструкции, порядок нанесения, предостережения
|
||||||
|
(ретинол только на ночь и не в один день с кислотами, SPF обязателен).
|
||||||
|
|
||||||
|
## Пример потока агента
|
||||||
|
|
||||||
|
```
|
||||||
|
1. analyze_skin {"self_report": "кожа стянутая, лоб блестит, покраснения"}
|
||||||
|
→ {"skin_type":"combination","concerns":["dryness","redness","oiliness"],"barrier":"compromised",...}
|
||||||
|
2. recommend {"concerns":["dryness","redness"], "skin_type":"sensitive"}
|
||||||
|
→ пэды JMsolution, тонер Anua Centella 77%, сыворотка SKIN627
|
||||||
|
3. place_order {"items":[{"id":"p05","qty":1},{"id":"p03","qty":1}],"delivery":"minsk_courier"}
|
||||||
|
→ SB-XXXXXXXX, 111.0 BYN, доставка бесплатно (корзина ≥80), курьер сегодня/завтра
|
||||||
|
4. usage_instructions {"product_ids":["p05","p03"]}
|
||||||
|
→ тонер (шаг 2) → SPF (шаг 7), предупреждения
|
||||||
|
5. check_order {"order_id":"SB-XXXXXXXX"}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Границы (честно)
|
||||||
|
|
||||||
|
- Анализ — эвристический (словарь стеммов RU/EN + голосование по типу), НЕ постановка диагноза;
|
||||||
|
в каждом ответе дисклеймер «не медицинская консультация».
|
||||||
|
- Оплата — заглушка `pending`: реальный эквайринг подключает агент-хост через `pay_link`
|
||||||
|
(см. задачу по методам оплаты портала).
|
||||||
|
- Каталог = витрина-прототип (12 SKU). При росте — заменить `catalog.json` без правок кода.
|
||||||
|
|
||||||
|
Запуск тестов: `python3 connector.py --demo` (создаёт тестовый заказ в orders.json).
|
||||||
384
artifacts/shkor/assistant.py
Normal file
384
artifacts/shkor/assistant.py
Normal file
|
|
@ -0,0 +1,384 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Shkor Beauty Assistant — ядро.
|
||||||
|
|
||||||
|
Универсальный beauty-ассистент магазина Shkor beauty:
|
||||||
|
* анализ состояния кожи (self-report + отчёт vision-агента),
|
||||||
|
* рекомендация продуктов из каталога каталога витрины,
|
||||||
|
* оформление заказа + варианта доставки,
|
||||||
|
* инструкция по применению (порядок рутины, частота, предостережения).
|
||||||
|
|
||||||
|
Протокол: один вход `handle(request: dict) -> dict`, MCP-совместимая форма
|
||||||
|
(JSON-RPC tools/call) + простая форма {action, args}. Оболочки (STDIO/HTTP/CLI)
|
||||||
|
живут в connector.py — ассистент не знает, какой агент его вызывает.
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import time
|
||||||
|
import uuid
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
HERE = Path(__file__).resolve().parent
|
||||||
|
CATALOG = json.loads((HERE / "catalog.json").read_text(encoding="utf-8"))
|
||||||
|
ORDERS_PATH = HERE / "orders.json"
|
||||||
|
PRODUCTS = {p["id"]: p for p in CATALOG["products"]}
|
||||||
|
|
||||||
|
# ── Анализ кожи ──────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
# ключи — стеммы (корни слов), чтобы ловить любые словоформы: «стянутая/стянутость», «покраснения/покраснение»
|
||||||
|
SYMPTOM_MAP = {
|
||||||
|
"стянут": "dryness", "сух": "dryness", "шелуш": "dryness", "обезвож": "dehydration", "dry": "dryness",
|
||||||
|
"flak": "dryness", "peel": "dryness",
|
||||||
|
"покрасн": "redness", "краснот": "redness", "раздраж": "sensitivity", "чувствитель": "sensitivity",
|
||||||
|
"sensitive": "sensitivity", "redness": "redness", "red ": "redness", "itch": "redness",
|
||||||
|
"блеск": "oiliness", "блест": "oiliness", "жирн": "oiliness", "oily": "oiliness", "shine": "oiliness",
|
||||||
|
"чёрных точ": "clogged-pores", "черных точ": "clogged-pores", "blackhead": "clogged-pores",
|
||||||
|
"расширенн пор": "clogged-pores", "поры": "clogged-pores", "pores": "clogged-pores",
|
||||||
|
"высып": "acne", "акн": "acne", "прыщ": "acne", "breakout": "acne", "угр": "acne",
|
||||||
|
"пигмент": "pigmentation", "пятн": "pigmentation", "веснушк": "pigmentation", "dark spot": "pigmentation",
|
||||||
|
"морщин": "fine-lines", "wrinkle": "fine-lines", "мимическ": "fine-lines",
|
||||||
|
"туск": "dullness", "dull": "dullness", "dullness": "dullness",
|
||||||
|
"тёмн круг": "dark-circles", "темн круг": "dark-circles", "dark circle": "dark-circles", "круги под глаз": "dark-circles",
|
||||||
|
"отёк": "puffiness", "отек": "puffiness", "puffin": "puffiness", "опух": "puffiness",
|
||||||
|
"чёрных угр": "clogged-pores",
|
||||||
|
}
|
||||||
|
|
||||||
|
TYPE_RULES = [
|
||||||
|
("sensitive", 2, ["redness", "sensitivity"]),
|
||||||
|
("dry", 2, ["dryness"]),
|
||||||
|
("oily", 2, ["oiliness", "clogged-pores"]),
|
||||||
|
("combination", 0, []), # базовый исход
|
||||||
|
]
|
||||||
|
|
||||||
|
# точные имена concerns для структурированного ввода (symptoms:[...])
|
||||||
|
CONCERN_ALIASES = {c: c for c in ["dryness", "dehydration", "sensitivity", "redness", "oiliness",
|
||||||
|
"clogged-pores", "acne", "pigmentation", "photoaging", "fine-lines",
|
||||||
|
"dullness", "dark-circles", "puffiness", "texture"]}
|
||||||
|
|
||||||
|
ROUTINE_ORDER = ["cleanse", "toner", "exfoliate", "serum", "mask", "spot", "spf",
|
||||||
|
"body-cleanse", "body-care", "hair-wash", "tool"]
|
||||||
|
|
||||||
|
|
||||||
|
def analyze_skin(args: dict) -> dict:
|
||||||
|
"""Анализ: self-report (текст/чек-лист) + опциональный vision-отчёт агента-фотографа.
|
||||||
|
|
||||||
|
args:
|
||||||
|
self_report: str — свободный текст жалоб (опционально)
|
||||||
|
symptoms: [str] — структурированные симптомы (опционально)
|
||||||
|
photo_report: dict/str — отчёт vision-модели по селфи (опционально)
|
||||||
|
age: int, climate: str — контекст (опционально)
|
||||||
|
"""
|
||||||
|
signals: list[str] = []
|
||||||
|
src = {"self_report": 0, "symptoms": 0, "photo_report": 0}
|
||||||
|
|
||||||
|
if args.get("self_report"):
|
||||||
|
text = str(args["self_report"]).lower()
|
||||||
|
for kw, concern in SYMPTOM_MAP.items():
|
||||||
|
if kw in text and concern not in signals:
|
||||||
|
signals.append(concern)
|
||||||
|
src["self_report"] = len([c for c in signals])
|
||||||
|
|
||||||
|
for s in args.get("symptoms") or []:
|
||||||
|
raw = str(s).lower().strip()
|
||||||
|
c = CONCERN_ALIASES.get(raw) or SYMPTOM_MAP.get(raw) or raw
|
||||||
|
if c and c not in signals:
|
||||||
|
signals.append(c)
|
||||||
|
src["symptoms"] += 1
|
||||||
|
|
||||||
|
photo = args.get("photo_report")
|
||||||
|
if photo:
|
||||||
|
blob = photo if isinstance(photo, str) else json.dumps(photo, ensure_ascii=False)
|
||||||
|
blob = blob.lower()
|
||||||
|
for kw, concern in SYMPTOM_MAP.items():
|
||||||
|
if kw in blob and concern not in signals:
|
||||||
|
signals.append(concern)
|
||||||
|
src["photo_report"] = len([c for c in signals]) - src["self_report"] - src["symptoms"]
|
||||||
|
|
||||||
|
# тип кожи: голосование по правилам
|
||||||
|
scores = {"sensitive": 0, "dry": 0, "oily": 0}
|
||||||
|
for concern in signals:
|
||||||
|
if concern in ("redness", "sensitivity"):
|
||||||
|
scores["sensitive"] += 1
|
||||||
|
if concern in ("dryness", "dehydration", "fine-lines"):
|
||||||
|
scores["dry"] += 1
|
||||||
|
if concern in ("oiliness", "clogged-pores", "acne"):
|
||||||
|
scores["oily"] += 1
|
||||||
|
top = max(scores.values())
|
||||||
|
winners = [k for k, v in scores.items() if v == top]
|
||||||
|
skin_type = winners[0] if len(winners) == 1 else "combination" # ничья/пусто → честный combination
|
||||||
|
|
||||||
|
hydration = 5 # 0..10, нейтрально
|
||||||
|
if "dryness" in signals:
|
||||||
|
hydration -= 3
|
||||||
|
if "oiliness" in signals:
|
||||||
|
hydration += 2
|
||||||
|
if "dehydration" in signals:
|
||||||
|
hydration -= 2
|
||||||
|
hydration = max(0, min(10, hydration))
|
||||||
|
|
||||||
|
barrier = "ok"
|
||||||
|
if "redness" in signals or "sensitivity" in signals:
|
||||||
|
barrier = "compromised"
|
||||||
|
if "acne" in signals and barrier == "compromised":
|
||||||
|
barrier = "compromised+breakouts"
|
||||||
|
|
||||||
|
recommendations = [p["id"] for p in rank_products(signals, skin_type)]
|
||||||
|
|
||||||
|
return {
|
||||||
|
"skin_type": skin_type,
|
||||||
|
"concerns": signals,
|
||||||
|
"hydration_index_0_10": hydration,
|
||||||
|
"barrier": barrier,
|
||||||
|
"sources": src,
|
||||||
|
"routine_focus": routine_focus(signals),
|
||||||
|
"recommended_product_ids": recommendations[:5],
|
||||||
|
"disclaimer": "Не медицинская консультация. При стойком раздражении/акне — к дерматологу.",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def routine_focus(concerns: list[str]) -> list[str]:
|
||||||
|
focus = []
|
||||||
|
if "dryness" in concerns or "dehydration" in concerns:
|
||||||
|
focus.append("увлажнение: гиалурон + центелла, тонер-эссенция до сыворотки")
|
||||||
|
if "sensitivity" in concerns or "redness" in concerns:
|
||||||
|
focus.append("успокоение: центелла 77%, без кислот и ретинола первые 2 недели")
|
||||||
|
if "acne" in concerns or "clogged-pores" in concerns:
|
||||||
|
focus.append("очищение: гидрофильное масло + мягкий BHA, не щёлочь")
|
||||||
|
if "pigmentation" in concerns or "photoaging" in concerns:
|
||||||
|
focus.append("SPF каждый день + ретинол на ночь (вводить постепенно)")
|
||||||
|
if "fine-lines" in concerns:
|
||||||
|
focus.append("анти-эйдж: ретинол 2–3 р/нед, коллагеновые маски")
|
||||||
|
if "dullness" in concerns:
|
||||||
|
focus.append("тон/сияние: пэды с кислотами 2 р/нед, маска-плёнка")
|
||||||
|
if not focus:
|
||||||
|
focus.append("база: очищение → тонер → сыворотка → крем → SPF")
|
||||||
|
return focus
|
||||||
|
|
||||||
|
|
||||||
|
def rank_products(concerns: list[str], skin_type: str) -> list[dict]:
|
||||||
|
scored = []
|
||||||
|
for p in CATALOG["products"]:
|
||||||
|
score = 0
|
||||||
|
for c in concerns:
|
||||||
|
if c in p["concerns"]:
|
||||||
|
score += 3
|
||||||
|
if skin_type == "sensitive" and ("retinol" in " ".join(p["actives"]) or "acid" in " ".join(p["actives"])):
|
||||||
|
score -= 2
|
||||||
|
if skin_type == "dry" and "hyaluronic" in " ".join(p["actives"]):
|
||||||
|
score += 2
|
||||||
|
if skin_type == "oily" and p["routine_step"] in ("cleanse", "exfoliate"):
|
||||||
|
score += 1
|
||||||
|
if score > 0:
|
||||||
|
scored.append((score, p))
|
||||||
|
scored.sort(key=lambda x: (-x[0], x[1]["price"]))
|
||||||
|
return [p for _, p in scored] or CATALOG["products"][:3]
|
||||||
|
|
||||||
|
|
||||||
|
# ── Рекомендации / поиск ──────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
def recommend(args: dict) -> dict:
|
||||||
|
concerns = [CONCERN_ALIASES.get(c.lower().strip()) or SYMPTOM_MAP.get(c.lower().strip(), c.lower().strip())
|
||||||
|
for c in (args.get("concerns") or [])]
|
||||||
|
skin_type = args.get("skin_type") or "combination"
|
||||||
|
budget = args.get("budget_max")
|
||||||
|
picks = rank_products(concerns, skin_type)
|
||||||
|
if budget:
|
||||||
|
picks = [p for p in picks if p["price"] <= float(budget)]
|
||||||
|
return {
|
||||||
|
"matched": len(picks),
|
||||||
|
"products": [
|
||||||
|
{"id": p["id"], "brand": p["brand"], "title": p["title"], "price": p["price"],
|
||||||
|
"why": ", ".join(c for c in concerns if c in p["concerns"]) or "базовый уход"}
|
||||||
|
for p in picks[: int(args.get("limit") or 5)]
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def search(args: dict) -> dict:
|
||||||
|
q = (args.get("query") or "").lower().strip()
|
||||||
|
cat = args.get("category")
|
||||||
|
out = []
|
||||||
|
for p in CATALOG["products"]:
|
||||||
|
if cat and p["category"] != cat:
|
||||||
|
continue
|
||||||
|
if q and not (q in p["title"].lower() or q in p["brand"].lower() or q in p["sub"].lower()
|
||||||
|
or any(q in a for a in p["actives"])):
|
||||||
|
continue
|
||||||
|
out.append({"id": p["id"], "brand": p["brand"], "title": p["title"], "price": p["price"],
|
||||||
|
"category": p["category"], "sub": p["sub"]})
|
||||||
|
return {"found": len(out), "products": out[: int(args.get("limit") or 20)]}
|
||||||
|
|
||||||
|
|
||||||
|
# ── Заказы и доставка ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
DELIVERY = {
|
||||||
|
"minsk_courier": {"label": "Курьер по Минску", "eta": "сегодня/завтра", "price": 7.0, "free_from": 80.0},
|
||||||
|
"europochta": {"label": "Европочта (РБ)", "eta": "2–3 дня", "price": 4.5, "free_from": 80.0},
|
||||||
|
"pickup": {"label": "Самовывоз (Минск, пр-т Независимости)", "eta": "через 2 часа", "price": 0.0, "free_from": 0.0},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _load_orders() -> dict:
|
||||||
|
if ORDERS_PATH.exists():
|
||||||
|
return json.loads(ORDERS_PATH.read_text(encoding="utf-8"))
|
||||||
|
return {}
|
||||||
|
|
||||||
|
|
||||||
|
def _save_orders(orders: dict) -> None:
|
||||||
|
ORDERS_PATH.write_text(json.dumps(orders, ensure_ascii=False, indent=1), encoding="utf-8")
|
||||||
|
|
||||||
|
|
||||||
|
def place_order(args: dict) -> dict:
|
||||||
|
items = args.get("items") or []
|
||||||
|
if not items:
|
||||||
|
return {"error": "items обязателен: [{id, qty}]"}
|
||||||
|
lines, unknown = [], []
|
||||||
|
total = 0.0
|
||||||
|
for it in items:
|
||||||
|
p = PRODUCTS.get(it.get("id"))
|
||||||
|
if not p:
|
||||||
|
unknown.append(it.get("id"))
|
||||||
|
continue
|
||||||
|
qty = int(it.get("qty") or 1)
|
||||||
|
lines.append({"id": p["id"], "title": p["title"], "qty": qty, "price": p["price"], "sum": round(p["price"] * qty, 2)})
|
||||||
|
total += p["price"] * qty
|
||||||
|
if unknown:
|
||||||
|
return {"error": f"неизвестные товары: {unknown}", "hint": "tools/call search для поиска id"}
|
||||||
|
|
||||||
|
delivery_key = args.get("delivery") or "minsk_courier"
|
||||||
|
d = DELIVERY.get(delivery_key)
|
||||||
|
if not d:
|
||||||
|
return {"error": f"нет такой доставки: {delivery_key}", "available": list(DELIVERY)}
|
||||||
|
d_price = 0.0 if (d["free_from"] and total >= d["free_from"]) else d["price"]
|
||||||
|
|
||||||
|
oid = "SB-" + uuid.uuid4().hex[:8].upper()
|
||||||
|
order = {
|
||||||
|
"order_id": oid,
|
||||||
|
"items": lines,
|
||||||
|
"subtotal": round(total, 2),
|
||||||
|
"delivery": {"key": delivery_key, **d, "charged": d_price},
|
||||||
|
"total": round(total + d_price, 2),
|
||||||
|
"customer": {"name": args.get("name") or "", "phone": args.get("phone") or "",
|
||||||
|
"address": args.get("address") or ""},
|
||||||
|
"payment": {"status": "pending", "method": args.get("pay_method") or "cash_on_delivery",
|
||||||
|
"pay_link": args.get("pay_link")}, # платёжку подключает агент-хост
|
||||||
|
"status": "placed",
|
||||||
|
"created": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
|
||||||
|
}
|
||||||
|
orders = _load_orders()
|
||||||
|
orders[oid] = order
|
||||||
|
_save_orders(orders)
|
||||||
|
return {
|
||||||
|
"order_id": oid, "status": "placed", "total": order["total"],
|
||||||
|
"delivery_eta": d["eta"], "delivery_price": d_price,
|
||||||
|
"next": f"Статус: tools/call check_order {{\"order_id\": \"{oid}\"}}. "
|
||||||
|
f"Оплата: pay_method=cash_on_delivery или передайте pay_link от платёжного агента.",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def check_order(args: dict) -> dict:
|
||||||
|
oid = args.get("order_id") or ""
|
||||||
|
o = _load_orders().get(oid)
|
||||||
|
if not o:
|
||||||
|
return {"error": f"заказ {oid} не найден", "known": list(_load_orders())[:10]}
|
||||||
|
return {"order_id": oid, "status": o["status"], "payment": o["payment"]["status"],
|
||||||
|
"total": o["total"], "delivery": o["delivery"]["label"], "eta": o["delivery"]["eta"],
|
||||||
|
"items": [{"title": l["title"], "qty": l["qty"]} for l in o["items"]]}
|
||||||
|
|
||||||
|
|
||||||
|
def delivery_options(args: dict) -> dict:
|
||||||
|
total = float(args.get("cart_total") or 0)
|
||||||
|
return {"options": [
|
||||||
|
{"key": k, "label": v["label"], "eta": v["eta"],
|
||||||
|
"price": 0.0 if (v["free_from"] and total >= v["free_from"]) else v["price"]}
|
||||||
|
for k, v in DELIVERY.items()]}
|
||||||
|
|
||||||
|
|
||||||
|
# ── Инструкции по применению ──────────────────────────────────────────────────
|
||||||
|
|
||||||
|
INSTRUCTIONS = {
|
||||||
|
"cleanse": "Нанести на сухую кожу, помассировать 30–60 сек, смыть тёплой водой. Утром можно мягче.",
|
||||||
|
"toner": "После очищения протереть лицо/нанести ладонями, не вытирать — следующий шаг на влажную кожу.",
|
||||||
|
"exfoliate": "2–3 раза в неделю, не в один день с ретинолом. При покраснении — прекратить.",
|
||||||
|
"serum": "3–4 капли на влажную кожу, дать впитаться 1–2 мин. Ретинол — только на ночь, старт 2–3 р/нед.",
|
||||||
|
"mask": "1–2 раза в неделю на очищенную кожу, 20–30 мин, снять плёнку снизу вверх.",
|
||||||
|
"spot": "Патчи — 10–15 мин на зону под глазами, после — лёгкий крем.",
|
||||||
|
"spf": "Финальный утренний шаг, 2 пальца средства, обновлять каждые 2–3 часа при солнце. Обязателен при ретиноле/кислотах.",
|
||||||
|
"body-cleanse": "Ежедневно, мягкой губкой, завершить увлажнением.",
|
||||||
|
"body-care": "На влажную кожу рук после каждого мытья.",
|
||||||
|
"hair-wash": "Втирать в кожу головы 1–2 мин, смыть; бальзам — только по длине.",
|
||||||
|
"tool": "Спонж смочить тёплой водой, использовать с пенкой; промывать и сушить после каждого применения.",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def usage_instructions(args: dict) -> dict:
|
||||||
|
ids = args.get("product_ids") or args.get("ids") or []
|
||||||
|
if isinstance(ids, str):
|
||||||
|
ids = [ids]
|
||||||
|
got = []
|
||||||
|
for pid in ids:
|
||||||
|
p = PRODUCTS.get(pid)
|
||||||
|
if p:
|
||||||
|
got.append({"id": pid, "title": p["title"], "how": INSTRUCTIONS.get(p["routine_step"], "по инструкции бренда"),
|
||||||
|
"step_order": ROUTINE_ORDER.index(p["routine_step"]) + 1 if p["routine_step"] in ROUTINE_ORDER else 99,
|
||||||
|
"caution": p.get("caution")})
|
||||||
|
if not got:
|
||||||
|
return {"error": "укажите product_ids (см. search/recommend)", "known_ids": list(PRODUCTS)}
|
||||||
|
got.sort(key=lambda x: x["step_order"])
|
||||||
|
plan = " → ".join(g["title"].split()[0] for g in got)
|
||||||
|
return {"steps": got, "routine_summary": f"Порядок нанесения: {plan}. Утро: очищение→увлажнение→SPF; вечер: очищение→акт (ретинол/кислоты)→крем.",
|
||||||
|
"general": "Новое средство — сначала на сгиб локтя 24ч. Не сочетать ретинол с кислотами в один день."}
|
||||||
|
|
||||||
|
|
||||||
|
# ── Диспетчер инструментов ────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
TOOLS = {
|
||||||
|
"analyze_skin": ("Анализ состояния кожи: self_report/symptoms/photo_report → тип, concerns, фокус рутины",
|
||||||
|
["self_report?", "symptoms?", "photo_report?", "age?", "climate?"]),
|
||||||
|
"recommend": ("Подбор продуктов: concerns+skin_type+budget_max → карточки", ["concerns?", "skin_type?", "budget_max?", "limit?"]),
|
||||||
|
"search": ("Поиск по каталогу", ["query?", "category?", "limit?"]),
|
||||||
|
"place_order": ("Оформить заказ", ["items[{id,qty}]", "delivery?", "name?", "phone?", "address?", "pay_method?"]),
|
||||||
|
"check_order": ("Статус заказа и доставки", ["order_id"]),
|
||||||
|
"delivery_options": ("Варианты доставки и цены", ["cart_total?"]),
|
||||||
|
"usage_instructions": ("Инструкции по применению + порядок рутины", ["product_ids"]),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def handle(request: dict) -> dict:
|
||||||
|
"""Единая точка входа. Понимает MCP tools/call и простую форму {action, args}."""
|
||||||
|
if not isinstance(request, dict):
|
||||||
|
return {"error": "request должен быть JSON-объектом"}
|
||||||
|
|
||||||
|
method = request.get("method")
|
||||||
|
if method == "tools/list":
|
||||||
|
return {"tools": [{"name": n, "description": d, "args": a} for n, (d, a) in TOOLS.items()]}
|
||||||
|
if method == "initialize":
|
||||||
|
return {"protocol": "shkor-assistant/1", "tools": list(TOOLS), "shop": CATALOG["shop"]}
|
||||||
|
if method == "tools/call":
|
||||||
|
params = request.get("params") or {}
|
||||||
|
name, arguments = params.get("name"), params.get("arguments") or {}
|
||||||
|
else:
|
||||||
|
name, arguments = request.get("action"), request.get("args") or request.get("arguments") or {}
|
||||||
|
|
||||||
|
if name not in TOOLS:
|
||||||
|
return {"error": f"неизвестный инструмент {name!r}", "available": list(TOOLS)}
|
||||||
|
fn = {"analyze_skin": analyze_skin, "recommend": recommend, "search": search,
|
||||||
|
"place_order": place_order, "check_order": check_order,
|
||||||
|
"delivery_options": delivery_options, "usage_instructions": usage_instructions}[name]
|
||||||
|
try:
|
||||||
|
result = fn(arguments if isinstance(arguments, dict) else {})
|
||||||
|
except Exception as e: # noqa: BLE001 — ассистент отвечает структурой, не падает
|
||||||
|
return {"error": f"{type(e).__name__}: {e}"}
|
||||||
|
rid = request.get("id")
|
||||||
|
out = {"result": result}
|
||||||
|
if rid is not None:
|
||||||
|
out["id"] = rid
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
import sys
|
||||||
|
payload = json.loads(sys.argv[1]) if len(sys.argv) > 1 else {"method": "tools/list"}
|
||||||
|
print(json.dumps(handle(payload), ensure_ascii=False, indent=1))
|
||||||
19
artifacts/shkor/catalog.json
Normal file
19
artifacts/shkor/catalog.json
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
{
|
||||||
|
"shop": "Shkor beauty",
|
||||||
|
"currency": "BYN",
|
||||||
|
"note": "Каталог синхронизирован с прототипом витрины artifacts/shkor-beauty-prototype.html (t-261005115918-d38b36)",
|
||||||
|
"products": [
|
||||||
|
{"id": "p01", "category": "face", "sub": "Сыворотки", "brand": "SKIN627", "title": "Увлажняющая сыворотка для лица Hyaluron Centella Ampoule", "price": 35.00, "old_price": 50.00, "concerns": ["dryness", "sensitivity", "dehydration"], "actives": ["hyaluronic acid", "centella"], "routine_step": "serum"},
|
||||||
|
{"id": "p02", "category": "face", "sub": "Пэды", "brand": "JMsolution", "title": "Увлажняющие пэды с гиалуроновой кислотой Multi Hyaluronic", "price": 59.00, "concerns": ["dryness", "dullness"], "actives": ["multi hyaluronic"], "routine_step": "exfoliate"},
|
||||||
|
{"id": "p03", "category": "face", "sub": "SPF", "brand": "Beauty of Joseon", "title": "Солнцезащитный гель Relief Sun SPF50+ PA++++", "price": 49.00, "concerns": ["photoaging", "pigmentation", "sensitivity"], "actives": ["SPF50+", "rice extract"], "routine_step": "spf"},
|
||||||
|
{"id": "p04", "category": "face", "sub": "Маски", "brand": "Biodance", "title": "Коллагеновая маска-пленка Collagen Wrapping Peel-off Mask", "price": 12.00, "concerns": ["dullness", "fine-lines"], "actives": ["collagen"], "routine_step": "mask"},
|
||||||
|
{"id": "p05", "category": "face", "sub": "Тонеры", "brand": "Anua", "title": "Успокаивающий тонер с сердечником Centella Toner 77%", "price": 62.00, "concerns": ["sensitivity", "redness"], "actives": ["centella 77%"], "routine_step": "toner"},
|
||||||
|
{"id": "p06", "category": "face", "sub": "Очищение", "brand": "Banila Co", "title": "Гидрофильное масло Clean It Zero Original", "price": 54.00, "concerns": ["clogged-pores", "makeup-removal"], "actives": ["cleansing oil"], "routine_step": "cleanse"},
|
||||||
|
{"id": "p07", "category": "face", "sub": "Сыворотки", "brand": "SKIN627", "title": "Сыворотка для лица с ретинолом Retinol Pantenol Ampoule", "price": 35.00, "concerns": ["fine-lines", "texture", "acne"], "actives": ["retinol", "panthenol"], "routine_step": "serum", "caution": "ретинол: вводить 2–3 раза в неделю, только на ночь, обязателен SPF утром"},
|
||||||
|
{"id": "p08", "category": "face", "sub": "Патчи", "brand": "A'PIEU", "title": "Патчи для глаз с молочной кислотой Milk One-Day Perfector", "price": 18.00, "concerns": ["dark-circles", "puffiness"], "actives": ["lactic acid", "milk"], "routine_step": "spot"},
|
||||||
|
{"id": "p09", "category": "body", "sub": "Для душа", "brand": "WEPROVE", "title": "Обновляющий гель для душа Renewing Aloe Shower Gel", "price": 29.00, "concerns": ["body-dryness"], "actives": ["aloe"], "routine_step": "body-cleanse"},
|
||||||
|
{"id": "p10", "category": "hair", "sub": "Шампуни", "brand": "WEPROVE", "title": "Восстанавливающий шампунь Daily Hair Repairing Shampoo", "price": 23.60, "concerns": ["hair-damage"], "actives": ["repairing complex"], "routine_step": "hair-wash"},
|
||||||
|
{"id": "p11", "category": "body", "sub": "Кремы для рук", "brand": "Care:Nel", "title": "Питательный крем для рук с ши и центеллой", "price": 14.00, "concerns": ["body-dryness"], "actives": ["shea", "centella"], "routine_step": "body-care"},
|
||||||
|
{"id": "p12", "category": "accessories", "sub": "Аксессуары", "brand": "Ciracle", "title": "Силиконовый спонж для умывания с чехлом", "price": 9.00, "concerns": [], "actives": [], "routine_step": "tool"}
|
||||||
|
]
|
||||||
|
}
|
||||||
147
artifacts/shkor/connector.py
Normal file
147
artifacts/shkor/connector.py
Normal file
|
|
@ -0,0 +1,147 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Shkor Beauty Assistant — универсальный коннектор.
|
||||||
|
|
||||||
|
Один и тот же ассистент — три способа подключения для ЛЮБОГО агента:
|
||||||
|
|
||||||
|
1) STDIO MCP-стиль (Claude/любой MCP-клиент):
|
||||||
|
assistant → connector.py (JSONL: {"method":"tools/list"} по строке)
|
||||||
|
в конфиге агента:
|
||||||
|
{"mcpServers": {"shkor": {"command": "python3", "args": [".../connector.py"], "transport": "stdio"}}}
|
||||||
|
|
||||||
|
2) HTTP JSON API (агент = просто HTTP-клиент):
|
||||||
|
connector.py --http 127.0.0.1:8791
|
||||||
|
POST /tools/list | POST /call {"name": "...", "arguments": {...}}
|
||||||
|
|
||||||
|
3) CLI разовый вызов (curl/крон/скрипт):
|
||||||
|
connector.py --call analyze_skin --args '{"self_report":"стянутость и покраснение"}'
|
||||||
|
|
||||||
|
4) Демо-чат (для живой проверки человеком):
|
||||||
|
connector.py --demo
|
||||||
|
|
||||||
|
Форма запроса принимает MCP tools/call и простую {action, args} — см. assistant.handle().
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import json
|
||||||
|
import sys
|
||||||
|
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
||||||
|
from assistant import handle # noqa: E402
|
||||||
|
|
||||||
|
|
||||||
|
# ── 1) STDIO: JSONL, одна строка = один запрос ───────────────────────────────
|
||||||
|
def serve_stdio() -> None:
|
||||||
|
print(json.dumps({"ok": True, "transport": "stdio-jsonl",
|
||||||
|
"hint": "отправляйте JSON-строки, читайте ответ построчно"}, ensure_ascii=False), flush=True)
|
||||||
|
for line in sys.stdin:
|
||||||
|
line = line.strip()
|
||||||
|
if not line:
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
resp = handle(json.loads(line))
|
||||||
|
except json.JSONDecodeError as e:
|
||||||
|
resp = {"error": f"bad json: {e}"}
|
||||||
|
print(json.dumps(resp, ensure_ascii=False), flush=True)
|
||||||
|
|
||||||
|
|
||||||
|
# ── 2) HTTP ──────────────────────────────────────────────────────────────────
|
||||||
|
def serve_http(addr: str) -> None:
|
||||||
|
host, port = addr.split(":")
|
||||||
|
port_i = int(port)
|
||||||
|
|
||||||
|
class H(BaseHTTPRequestHandler):
|
||||||
|
def _send(self, code: int, payload: dict) -> None:
|
||||||
|
body = json.dumps(payload, ensure_ascii=False).encode()
|
||||||
|
self.send_response(code)
|
||||||
|
self.send_header("Content-Type", "application/json; charset=utf-8")
|
||||||
|
self.send_header("Content-Length", str(len(body)))
|
||||||
|
self.end_headers()
|
||||||
|
self.wfile.write(body)
|
||||||
|
|
||||||
|
def do_GET(self) -> None: # health / открыть в браузере
|
||||||
|
if self.path in ("/", "/health"):
|
||||||
|
self._send(200, {"ok": True, "service": "shkor-assistant", "version": 1})
|
||||||
|
else:
|
||||||
|
self._send(404, {"error": "not found", "routes": ["POST /tools/list", "POST /call"]})
|
||||||
|
|
||||||
|
def do_POST(self) -> None:
|
||||||
|
try:
|
||||||
|
n = int(self.headers.get("Content-Length") or 0)
|
||||||
|
data = json.loads(self.rfile.read(n) or b"{}")
|
||||||
|
except Exception as e: # noqa: BLE001
|
||||||
|
self._send(400, {"error": f"bad request: {e}"})
|
||||||
|
return
|
||||||
|
if self.path == "/tools/list":
|
||||||
|
self._send(200, handle({"method": "tools/list"}))
|
||||||
|
elif self.path == "/call":
|
||||||
|
self._send(200, handle({"method": "tools/call", "params": data}))
|
||||||
|
else:
|
||||||
|
self._send(404, {"error": "not found", "routes": ["POST /tools/list", "POST /call"]})
|
||||||
|
|
||||||
|
def log_message(self, *a) -> None: # тише в логи
|
||||||
|
pass
|
||||||
|
|
||||||
|
srv = ThreadingHTTPServer((host, port_i), H)
|
||||||
|
print(f"shkor-assistant HTTP on http://{host}:{port_i} (POST /tools/list, POST /call)", file=sys.stderr)
|
||||||
|
srv.serve_forever()
|
||||||
|
|
||||||
|
|
||||||
|
# ── 4) Демо-чат ───────────────────────────────────────────────────────────────
|
||||||
|
DEMO = [
|
||||||
|
("analyze_skin", {"self_report": "кожа стянутая после умывания, лоб блестит к обеду, периодически покраснения"}),
|
||||||
|
("recommend", {"concerns": ["dryness", "redness"], "skin_type": "sensitive", "budget_max": 70}),
|
||||||
|
("place_order", {"items": [{"id": "p05", "qty": 1}, {"id": "p03", "qty": 1}], "delivery": "minsk_courier",
|
||||||
|
"name": "Тест", "phone": "+375XX0000000"}),
|
||||||
|
("usage_instructions", {"product_ids": ["p05", "p03"]}),
|
||||||
|
("check_order", {}), # order_id подставится после place_order
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def run_demo() -> None:
|
||||||
|
last_order = None
|
||||||
|
for action, args in DEMO:
|
||||||
|
if action == "check_order":
|
||||||
|
if not last_order:
|
||||||
|
continue
|
||||||
|
args = {"order_id": last_order}
|
||||||
|
print(f"\n▶ {action} {json.dumps(args, ensure_ascii=False)}")
|
||||||
|
resp = handle({"method": "tools/call", "params": {"name": action, "arguments": args}})
|
||||||
|
print(json.dumps(resp.get("result", resp), ensure_ascii=False, indent=1)[:1200])
|
||||||
|
if action == "place_order" and (resp.get("result") or {}).get("order_id"):
|
||||||
|
last_order = resp["result"]["order_id"]
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
ap = argparse.ArgumentParser(description="Shkor Beauty Assistant connector")
|
||||||
|
ap.add_argument("--http", metavar="HOST:PORT", help="поднять HTTP API")
|
||||||
|
ap.add_argument("--stdio", action="store_true", help="STDIO JSONL (MCP-стиль)")
|
||||||
|
ap.add_argument("--call", metavar="TOOL", help="разовый вызов инструмента")
|
||||||
|
ap.add_argument("--args", default="{}", help="JSON-аргументы для --call")
|
||||||
|
ap.add_argument("--list-tools", action="store_true")
|
||||||
|
ap.add_argument("--demo", action="store_true")
|
||||||
|
a = ap.parse_args()
|
||||||
|
|
||||||
|
if a.http:
|
||||||
|
serve_http(a.http)
|
||||||
|
elif a.stdio:
|
||||||
|
serve_stdio()
|
||||||
|
elif a.list_tools:
|
||||||
|
print(json.dumps(handle({"method": "tools/list"}), ensure_ascii=False, indent=1))
|
||||||
|
elif a.demo:
|
||||||
|
run_demo()
|
||||||
|
elif a.call:
|
||||||
|
try:
|
||||||
|
arguments = json.loads(a.args)
|
||||||
|
except json.JSONDecodeError as e:
|
||||||
|
sys.exit(f"bad --args json: {e}")
|
||||||
|
print(json.dumps(handle({"method": "tools/call", "params": {"name": a.call, "arguments": arguments}}),
|
||||||
|
ensure_ascii=False, indent=1))
|
||||||
|
else:
|
||||||
|
ap.print_help()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
# Simbiot Harness: запуск без установки обёртки (cli.py + scheduler-демон).
|
# Simbiot Harness: запуск без установки обёртки (cli.py + scheduler-демон).
|
||||||
# simbiot help — справка по CLI-командам (как python cli.py -h)
|
# simbiot help — справка по CLI-командам (как python cli.py -h)
|
||||||
|
export SIMBIOT_HOME="${SIMBIOT_HOME:-/var/lib/simbiot-harness}"
|
||||||
# simbiot — без аргументов: статус + как управлять демоном/CLI
|
# simbiot — без аргументов: статус + как управлять демоном/CLI
|
||||||
# simbiot --help — как help (аргумент для argparse-привычки)
|
# simbiot --help — как help (аргумент для argparse-привычки)
|
||||||
DIR="$(cd "$(dirname "$(readlink -f "$0")")/.." && pwd)"
|
DIR="$(cd "$(dirname "$(readlink -f "$0")")/.." && pwd)"
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@
|
||||||
"amount_usd": 26.7,
|
"amount_usd": 26.7,
|
||||||
"src": "openrouter.ai/api/v1/credits (замер 2026-10-03 23:5x UTC: total=163, usage=136.30)",
|
"src": "openrouter.ai/api/v1/credits (замер 2026-10-03 23:5x UTC: total=163, usage=136.30)",
|
||||||
"auto": true,
|
"auto": true,
|
||||||
"measured_at": "2026-10-04T00:15:21+00:00"
|
"measured_at": "2026-10-04T01:13:01+00:00"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "US LLC Delaware (операционный)",
|
"name": "US LLC Delaware (операционный)",
|
||||||
|
|
@ -24,7 +24,7 @@
|
||||||
"amount_usd": 0.0,
|
"amount_usd": 0.0,
|
||||||
"src": "api.stripe.com/v1/balance livemode=True, STRIPE_LIVE_KEY из coop/.env (замер 2026-10-03): available=0, pending=0",
|
"src": "api.stripe.com/v1/balance livemode=True, STRIPE_LIVE_KEY из coop/.env (замер 2026-10-03): available=0, pending=0",
|
||||||
"auto": true,
|
"auto": true,
|
||||||
"measured_at": "2026-10-04T00:15:22+00:00"
|
"measured_at": "2026-10-04T01:13:01+00:00"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"debts": [],
|
"debts": [],
|
||||||
|
|
|
||||||
38
docs/digests/hour-2026-10-04-0700.md
Normal file
38
docs/digests/hour-2026-10-04-0700.md
Normal file
|
|
@ -0,0 +1,38 @@
|
||||||
|
# Дайджест hour 2026-10-04 ~07:00 UTC
|
||||||
|
|
||||||
|
## Батч задач (3/3 закрыты, все [M])
|
||||||
|
|
||||||
|
1. **t-261004061059-84e112** — MCP для продвижения сервисов. Диагноз:
|
||||||
|
`alex.score.com` не резолвится глобально (зона score.com → мёртвые
|
||||||
|
NS.SCORE.NET, SERVFAIL по DoH Google/Cloudflare, EDE 22) — деплой туда
|
||||||
|
невозможен, нужно восстановление делегирования владельцем. Рабочая точка
|
||||||
|
входа — https://cil.by/mcp (E2E: initialize/whoami OK). Найден и исправлен
|
||||||
|
прод-баг портала: `/mcp tools/call` падал 500 NameError (mcp регистрируется
|
||||||
|
раньше stripe/alfa/booking — хелперы `_gen_slots*`/`_send_email`/
|
||||||
|
`_stripe_product_objects` не определены). Фикс `_late_bind_ctx()` на каждом
|
||||||
|
tools/call; коммит f0febd9 (shkor-labs/portal), портал перезапущен,
|
||||||
|
бэкап mcp.py.backup-1004-ctx-latebind. MCP_CONNECT.md дополнен.
|
||||||
|
2. **t-261004061251-2d3f56** — запись на 20-мин разбор через MCP работает:
|
||||||
|
booking_slots(type='session') → 70 свободных слотов (18:00–22:00 Минск,
|
||||||
|
график 3/3), booking_create(kind='session') → jitsi+запись+транскрибация
|
||||||
|
+ письма/TG. Новый инструмент **coop_catalog**: все сервисы кооператива с
|
||||||
|
ценами и Stripe pay_url прямо через MCP (live: Симбиот 33/59/1000$,
|
||||||
|
Рилсап 199/299/699$/мес, Lunara/ReLeaf 4.99$/мес) — полный цикл покупки
|
||||||
|
без веб-UI (MCP-first).
|
||||||
|
3. **t-261004062405-fa9926** — MiroFish разобран (клон, ~17.2k строк backend).
|
||||||
|
Вердикт: движок не берём (стоимость: 100 агентов×40 раундов ≈ $10–25 на
|
||||||
|
qwen-plus, ×10–30 на премиум; + Zep Cloud SaaS; OASIS/camel-ai стек;
|
||||||
|
платформа, не библиотека). Идеи заимствования: персоны-из-сущностей графа,
|
||||||
|
ReportAgent (4 инструмента + интервью агентов + трекер полноты),
|
||||||
|
episode→граф памяти на своём RAG, дешёвая симуляция спроса перед запусками,
|
||||||
|
NL→конфиг прогона с budget-cap, IPC-прогресс. Отчёт:
|
||||||
|
artifacts/mirofish-analysis-2026-10-04.md (доставлен в TG).
|
||||||
|
|
||||||
|
## Инфра/события
|
||||||
|
|
||||||
|
- Мин-тик: 6× gate:quiet — событий выше порога нет, LLM не запускался.
|
||||||
|
- Фоновые задачи: нет (list пуст).
|
||||||
|
- Эскалации: открытых нет; e-261003170542-389f29 (hour Request timed out) уже
|
||||||
|
acked уровнем day — повторный ack не требуется.
|
||||||
|
- Внешние сбои, не наша зона: DNS score.com мёртв (см. п.1); TLS on-demand
|
||||||
|
корректно отвергает alex.cil.by (ask-гейт /api/caddy/ask — by design).
|
||||||
59
docs/digests/hour-2026-10-04-1000.md
Normal file
59
docs/digests/hour-2026-10-04-1000.md
Normal file
|
|
@ -0,0 +1,59 @@
|
||||||
|
# Дайджест hour 2026-10-04 ~10:00 UTC
|
||||||
|
|
||||||
|
## Батч задач (1/2 закрыта; вторая — L, подготовка готова, ждёт Alex)
|
||||||
|
|
||||||
|
1. **t-261004091808-70985e [M] — ЗАКРЫТА** (failed[NNN]/DNA у агентов).
|
||||||
|
Диагноз: heartbeat шли нормально (agent_stats обновлялись каждую минуту), но
|
||||||
|
репортёр coop-reporter v7 жёстко опрашивал `systemctl is-active hermes-agent` —
|
||||||
|
у переведённых на Simbiot агентов (Mentalis/Lunar/Rita/Roy/Mon,
|
||||||
|
harness_type=simbiot) hermes-agent остановлен при переезде, главный юнит —
|
||||||
|
simbiot-harness.service → дашборд красил живых агентов в failed[NNN].
|
||||||
|
«DNA — не синхр.» — отдельная причина: harness_setups пуста, simbiot-cli
|
||||||
|
(node) на машинах агентов никогда не ставился → CLI-sync был невозможен.
|
||||||
|
Фикс (3 части): (а) репортёр v8 — активный simbiot-harness → статус
|
||||||
|
«active (simbiot)», плюс сам собирает DNA-факты (skills/SKILL.md + README.md,
|
||||||
|
скоринг по формуле CLI) и пушит на новый POST /api/agent/dna → upsert
|
||||||
|
harness_setups; (б) dashboard.py — fallback-матч DNA по developer_name, когда
|
||||||
|
hostname машины не содержит slug агента (случай AI corp на Ubuntu-2404…);
|
||||||
|
(в) agents_dashboard.html — фронт принимал только service==='active',
|
||||||
|
добавлен 'active (simbiot)' (чип «online · simbiot»). Раскатка v8 —
|
||||||
|
автоматическая через self-update по manifest, заняла ~5 мин на весь флот,
|
||||||
|
SSH-ручного вмешательства не потребовалось. Верификация по БД: все цели
|
||||||
|
тикета online · simbiot, DNA=59/100 (Mentalis/Lunar/Rita), Roy/Mon починены
|
||||||
|
тем же фиксом, 14 агентов с DNA. портал перезапускан ×2 (reporter.py,
|
||||||
|
dashboard.py), coop-portal active.
|
||||||
|
|
||||||
|
2. **t-261004092629-40655b [L] — НЕ закрыта, подготовка выполнена** (подписать
|
||||||
|
SAFE MFN с предыдущими инвесторами; подписание — сторона Alex).
|
||||||
|
Сделано: подписной комплект в `/root/coop/fundraising/safe-package/` —
|
||||||
|
каноническая **YC Post-Money SAFE (MFN Only)** FINAL.docx (форма №3 по
|
||||||
|
RAISE_PLAN: uncapped, no discount; источник ycombinator.com/safe, скачана
|
||||||
|
04.10, целостность проверена) + YC SAFE User Guide.pdf + шаблон MFN side
|
||||||
|
letter + SIGNING_CHECKLIST.md с шагами из постановки. Чек-лист доставлен
|
||||||
|
Alex в TG (идемпотентно). Блокер по списку инвесторов: в coop.db реальных
|
||||||
|
инвесторов НЕТ — `backers` содержит 2 тестовые записи, stripe/alfa payments —
|
||||||
|
тесты, presale_orders — предзаказы, не чеки → список предыдущих инвесторов
|
||||||
|
ведётся вне портала, у Alex. Остаток (за Alex): список+реквизиты, юр. ревью
|
||||||
|
формы и side letter до первой подписи ($1.5–3K), санкционный скрининг
|
||||||
|
каждого инвестора на дату подписания (OFAC/ЕС — критично для резидентов РБ),
|
||||||
|
подписание, фиксация экземпляров в safe-package/signed/. Задача вернётся
|
||||||
|
в ready следующего тика.
|
||||||
|
|
||||||
|
## Инфра/события
|
||||||
|
|
||||||
|
- Мин-тик: 6× gate:quiet — событий выше порога нет, LLM не запускался.
|
||||||
|
- Фоновые задачи: нет (list пуст).
|
||||||
|
- Эскалации: открытых нет, ack-ов не требовалось.
|
||||||
|
- coop-portal перезапускан 2× в рамках фикса (10:18, 10:27), перерыв сервиса
|
||||||
|
секунды, ошибок в journalctl нет.
|
||||||
|
- Раскатка coop-reporter v8 прошла на все машины с reporter-токеном; DNA-пуш
|
||||||
|
идёт с каждым тиком таймера (2 мин), данные обновляются.
|
||||||
|
- Конфиг уровней: масштабированная контекст-схема по тактам активна
|
||||||
|
(e-261001082710): min 256k → hour 512k → day 1M ctx_tokens.
|
||||||
|
|
||||||
|
## AUM
|
||||||
|
|
||||||
|
- Прямых расходов тик не добавил; фиксом убран ложный «failed» по 5 прод-агентам
|
||||||
|
(сигнальный шум → ложные ремонты/рестарты больше не триггерятся).
|
||||||
|
- SAFE-комплект приближает закрытие рейз-шага (юр. ревью — плановые $1.5–3K,
|
||||||
|
решение за Alex).
|
||||||
58
docs/digests/hour-2026-10-05-1400.md
Normal file
58
docs/digests/hour-2026-10-05-1400.md
Normal file
|
|
@ -0,0 +1,58 @@
|
||||||
|
# Дайджест hour 2026-10-05 ~14:00 UTC
|
||||||
|
|
||||||
|
## Батч задач (1/1 закрыта)
|
||||||
|
|
||||||
|
1. **t-261005135853-186b42 [M] — ЗАКРЫТА** (гейтвей: входящие документы на диск).
|
||||||
|
Диагноз подтверждён: `harness/tg_inbox.py` качал только voice (voice-пайплайн),
|
||||||
|
documents/photo терялись — file_id в событии есть, файла нет (кейс 4 PDF-выписок
|
||||||
|
Alex 05.10, доставал вручную getFile'ом).
|
||||||
|
Фикс: `save_document()` — при kind=document|photo getFile→download→атомарная
|
||||||
|
запись (.part→rename) в `inbox/files/<file_unique_id>.<ext>` (новый
|
||||||
|
`paths.inbox_files_dir()`, env `SIMBIOT_INBOX_FILES_DIR`); фото — максимальный
|
||||||
|
размер. В MessageReceived теперь `file_path` (абс, открывается с любого уровня)
|
||||||
|
+ `file_name`/`file_size`/`mime_type` — триаж видит что за файл без getFile.
|
||||||
|
Идемпотентно по file_unique_id (повторный апдейт не перекачивает), сбой
|
||||||
|
скачивания (истёкший у TG файл, HTTP 400/410) не валит поллер — file_id
|
||||||
|
остаётся в событии для ручного достёгивания. Голосовые — как раньше.
|
||||||
|
Верификация: `tests/test_tg_documents.py` — 22 PASS (document e2e, photo e2e,
|
||||||
|
идемпотентность, сбой, _file_ext); регресс voice_video/seen_indication/
|
||||||
|
inbox_batch — зелёный. Поллер перезапущен, активен (systemd). Коммит c5c3e8f.
|
||||||
|
Живая проверка: следующий отправленный боту документ сам появится в inbox/files/.
|
||||||
|
|
||||||
|
## Батч входящих m-516173771..777 (7 сообщений, один разговор) — отвечено сводно
|
||||||
|
|
||||||
|
Контекст: Alex скинул 4 PDF-выписки → «гейтвей их не сохраняет» (→ задача выше,
|
||||||
|
закрыта этим же тиком) → «сделай реальную ссылку на оплату в баксах с alipay»
|
||||||
|
→ «на $100» → «ссылку» → «а тут уже почему-то нету алипей, хоть в предыдущей
|
||||||
|
линке был».
|
||||||
|
|
||||||
|
Сделано:
|
||||||
|
- Боевой $100-линк подтверждён и отдан: https://buy.stripe.com/3cIdR82WH4RpfPY3EY1B60c
|
||||||
|
(plink_1UNCSvBT53G7FGSQDqXJOE8W, price $100 USD one_time, livemode).
|
||||||
|
- Почему нет Alipay — разбрано по факам, а не догадкой: через live-API проверена
|
||||||
|
payment_method_configuration — alipay/wechat_pay/paypal `available=false`
|
||||||
|
(методы НЕ активированы в боевом аккаунте Stripe), card и amazon_pay — on.
|
||||||
|
Попытки: пересоздание линка с явными методами → «ensure the type is activated
|
||||||
|
in your dashboard»; Checkout Session с явными методами → тот же отказ;
|
||||||
|
включение display_preference через API → невозможно (available=false).
|
||||||
|
Вывод: единственный путь — ручной шаг в дашборде (Settings → Payment methods),
|
||||||
|
после чего пересоздание линка — одна команда. Alex'у дана инструкция и обещание
|
||||||
|
пересоздать по сигналу «алипей включил».
|
||||||
|
- Amazon Pay в payment links Stripe не поддерживается в принципе (список
|
||||||
|
допустимых payment_method_types его не содержит; он работает только динамически
|
||||||
|
в чекауте — потому и был виден в старых линках без явных методов).
|
||||||
|
|
||||||
|
## Инциденты/эскалации
|
||||||
|
|
||||||
|
- Эскалаций от нижних уровней за час нет (4 тихих минутных тика, 0 событий выше
|
||||||
|
порога — LLM не запускался, burn экономлен).
|
||||||
|
- В ходе теста поймал и починил собственный баг: ранние выходы save_document
|
||||||
|
возвращали кортеж (None, None) вместо None — тест ловит такие вещи именно
|
||||||
|
поэтому.
|
||||||
|
|
||||||
|
## AUM-фильтр часа
|
||||||
|
|
||||||
|
Фикс гейтвея — прямая база долгосрочной AUM: до него входящие документы Алекса
|
||||||
|
(выписки банков — материал для учёта AUM/оборотки) терялись физически, а ручное
|
||||||
|
доставание съедало тики уровней. Alipay-линк — короткий доход (предзаказы $100),
|
||||||
|
но блокер снят до предела: осталась одна ручная кнопка у Alex.
|
||||||
|
|
@ -147,6 +147,28 @@ def archive_stale_failed(max_age_hours: float = 24.0, now=None) -> list:
|
||||||
return closed
|
return closed
|
||||||
|
|
||||||
|
|
||||||
|
def block_task(task_id: str, reason: str = "ожидает уточнений") -> str:
|
||||||
|
"""Заблокировать живую задачу: TaskBlocked → статус blocked (t-261004122019).
|
||||||
|
|
||||||
|
Задача есть, но не готова к работе — ждёт уточнений от Alex или внешнего
|
||||||
|
события. scheduler берёт только ready → blocked не попадает в батчи.
|
||||||
|
Обратно: requeue (blocked → ready). Идемпотентно: уже blocked — no-op.
|
||||||
|
"""
|
||||||
|
model = es.run_projections()
|
||||||
|
t = model["tasks"].get(task_id)
|
||||||
|
if t is None:
|
||||||
|
raise ValueError(f"задача {task_id} не найдена")
|
||||||
|
if t["status"] == "blocked":
|
||||||
|
return task_id # уже заблокирована — no-op
|
||||||
|
if t["status"] in ("done", "closed"):
|
||||||
|
raise ValueError(
|
||||||
|
f"задача {task_id} в статусе {t['status']} — blocked только для живых; "
|
||||||
|
f"оживить можно requeue")
|
||||||
|
es.append_event("task", task_id, "TaskBlocked",
|
||||||
|
{"by": "manual", "reason": reason[:300]})
|
||||||
|
return task_id
|
||||||
|
|
||||||
|
|
||||||
def requeue_task(task_id: str, reason: str = "manual requeue"):
|
def requeue_task(task_id: str, reason: str = "manual requeue"):
|
||||||
"""Вернуть failed/running/прерванную задачу в очередь (TaskPromoted → ready).
|
"""Вернуть failed/running/прерванную задачу в очередь (TaskPromoted → ready).
|
||||||
|
|
||||||
|
|
@ -159,7 +181,7 @@ def requeue_task(task_id: str, reason: str = "manual requeue"):
|
||||||
raise ValueError(f"задача {task_id} не найдена")
|
raise ValueError(f"задача {task_id} не найдена")
|
||||||
# closed тоже возвращаем (авто-схлопывание старых фейлов, e-261003094432):
|
# closed тоже возвращаем (авто-схлопывание старых фейлов, e-261003094432):
|
||||||
# закрытая в архив задача может понадобиться снова — осознанная доработка.
|
# закрытая в архив задача может понадобиться снова — осознанная доработка.
|
||||||
if t["status"] not in ("failed", "closed", "done", "running"):
|
if t["status"] not in ("failed", "closed", "done", "running", "blocked"):
|
||||||
raise ValueError(f"задача {task_id} в статусе {t['status']} — requeue не нужен")
|
raise ValueError(f"задача {task_id} в статусе {t['status']} — requeue не нужен")
|
||||||
# идемпотентная доставка (t-260930083937): requeue = новая работа над
|
# идемпотентная доставка (t-260930083937): requeue = новая работа над
|
||||||
# задачей, её результат должен уйти в чат заново → сбрасываем факт
|
# задачей, её результат должен уйти в чат заново → сбрасываем факт
|
||||||
|
|
|
||||||
|
|
@ -93,6 +93,10 @@
|
||||||
.chat-send{border:1px solid var(--ink);background:var(--signal);color:#fff;font-weight:700;font-family:ui-monospace,Menlo,monospace;font-size:12px;letter-spacing:.08em;text-transform:uppercase;padding:0 20px;cursor:pointer;border-radius:6px}
|
.chat-send{border:1px solid var(--ink);background:var(--signal);color:#fff;font-weight:700;font-family:ui-monospace,Menlo,monospace;font-size:12px;letter-spacing:.08em;text-transform:uppercase;padding:0 20px;cursor:pointer;border-radius:6px}
|
||||||
.chat-send:disabled{opacity:.45;cursor:default}
|
.chat-send:disabled{opacity:.45;cursor:default}
|
||||||
.chat-note{font-family:ui-monospace,Menlo,monospace;font-size:11px;color:var(--dim);margin:8px 2px 0}
|
.chat-note{font-family:ui-monospace,Menlo,monospace;font-size:11px;color:var(--dim);margin:8px 2px 0}
|
||||||
|
/* живой статус каналов (t-261004103718-d8112b) */
|
||||||
|
.chan{display:inline-flex;align-items:center;gap:6px;font-family:ui-monospace,Menlo,monospace;font-size:12px;color:var(--ink);margin-right:16px;white-space:nowrap}
|
||||||
|
.chan .dot{width:8px;height:8px}
|
||||||
|
.chan small{color:var(--dim);font-size:11px}
|
||||||
/* ── детали агента (t-261002175316-eb24cc) ── */
|
/* ── детали агента (t-261002175316-eb24cc) ── */
|
||||||
.back{font-family:ui-monospace,Menlo,monospace;font-size:12px;letter-spacing:.1em;text-transform:uppercase;
|
.back{font-family:ui-monospace,Menlo,monospace;font-size:12px;letter-spacing:.1em;text-transform:uppercase;
|
||||||
border:1px solid var(--ink);background:transparent;color:var(--dim);padding:8px 14px;cursor:pointer;border-radius:6px}
|
border:1px solid var(--ink);background:transparent;color:var(--dim);padding:8px 14px;cursor:pointer;border-radius:6px}
|
||||||
|
|
@ -156,7 +160,7 @@
|
||||||
|
|
||||||
<div class="view" id="view-chat">
|
<div class="view" id="view-chat">
|
||||||
<h2 style="margin-top:0">Чат с Simbiot</h2>
|
<h2 style="margin-top:0">Чат с Simbiot</h2>
|
||||||
<div class="lead">сообщение уходит агенту по порталу-каналу, ответ придёт сам (обычно ≤ 1 мин) · тоже дублируется в TG, если бот подключён</div>
|
<div class="lead">сообщение уходит агенту по порталу-каналу, ответ придёт сам (обычно ≤ 1 мин) · каналы: <span id="chan-status"><span class="chan"><span class="dot wait"></span>каналы: узнаю…</span></span></div>
|
||||||
<div class="chat">
|
<div class="chat">
|
||||||
<div class="chat-log" id="chat-log"><div class="skel">загружаю…</div></div>
|
<div class="chat-log" id="chat-log"><div class="skel">загружаю…</div></div>
|
||||||
<div class="chat-input">
|
<div class="chat-input">
|
||||||
|
|
@ -164,7 +168,7 @@
|
||||||
<button class="chat-send" id="chat-send" onclick="chatSend()">Отправить</button>
|
<button class="chat-send" id="chat-send" onclick="chatSend()">Отправить</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="chat-note" id="chat-note">канал: портал (portal:<agent>) · ответ агент пишет сам по такту</div>
|
<div class="chat-note" id="chat-note">канал: портал (portal:<agent>) · ответ агент пишет сам по такту · <span id="chan-note">статус каналов: узнаю…</span></div>
|
||||||
</div><!-- /view-chat -->
|
</div><!-- /view-chat -->
|
||||||
|
|
||||||
<div class="view" id="view-agent">
|
<div class="view" id="view-agent">
|
||||||
|
|
@ -455,6 +459,46 @@ function chatBubble(m){
|
||||||
`<div class="bubble"><span class="who">${esc(who)}${t ? " · " + esc(t) : ""}${pend}${rep}</span>${esc(m.text || "")}</div></div>`;
|
`<div class="bubble"><span class="who">${esc(who)}${t ? " · " + esc(t) : ""}${pend}${rep}</span>${esc(m.text || "")}</div></div>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── живой статус каналов (t-261004103718-d8112b) ──────────────────────────
|
||||||
|
// ok → зелёный + возраст последнего успеха; нет свежего штампа → красный.
|
||||||
|
// hermes-лейбл — только если бэкенд подтвердил маркер старого Гермес-харнеса.
|
||||||
|
function chanAge(s){
|
||||||
|
if (s.age_s == null) return "";
|
||||||
|
if (s.age_s < 60) return s.age_s + "с назад";
|
||||||
|
if (s.age_s < 3600) return Math.round(s.age_s / 60) + "м назад";
|
||||||
|
return Math.round(s.age_s / 3600) + "ч назад";
|
||||||
|
}
|
||||||
|
function chanChip(name, label, s){
|
||||||
|
s = s || {};
|
||||||
|
const cls = s.ok ? "" : "err";
|
||||||
|
const detail = s.ok ? (chanAge(s) || "успех") : (s.detail || "нет связи");
|
||||||
|
return `<span class="chan"><span class="dot ${cls}"></span>${label}` +
|
||||||
|
`<small>${esc(detail)}</small></span>`;
|
||||||
|
}
|
||||||
|
function renderChannels(ch){
|
||||||
|
const box = $("chan-status");
|
||||||
|
if (!box) return;
|
||||||
|
if (!ch){ return; } // бэкенд без статусов — оставляем «узнаю…» до след. тика
|
||||||
|
let html = "";
|
||||||
|
if (ch.hermes) html += `<span class="chan"><small>⚠ старый харнес (Hermes): данные могут быть устаревшими</small></span>`;
|
||||||
|
html += chanChip("portal", "портал", ch.portal);
|
||||||
|
const tg = ch.telegram || {};
|
||||||
|
if (tg.token_loaded === false){
|
||||||
|
html += `<span class="chan"><span class="dot err"></span>telegram<small>бот не подключён</small></span>`;
|
||||||
|
} else {
|
||||||
|
html += chanChip("tg", "telegram", tg);
|
||||||
|
}
|
||||||
|
box.innerHTML = html;
|
||||||
|
// краткая сводка там же, где раньше жил статичный «тоже дублируется в TG…»
|
||||||
|
const note = $("chan-note");
|
||||||
|
if (note){
|
||||||
|
const p = ch.portal || {}, t = ch.telegram || {};
|
||||||
|
const fmt = s => s.ok ? `<span style="color:var(--ok)">ok</span> (${esc(chanAge(s) || "сейчас")})`
|
||||||
|
: `<span style="color:var(--err)">down</span>`;
|
||||||
|
note.innerHTML = "статус: портал " + fmt(p) + " · telegram " + fmt(t);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function chatTick(){
|
async function chatTick(){
|
||||||
if (getViewName() !== "chat") return;
|
if (getViewName() !== "chat") return;
|
||||||
chatOpen = true;
|
chatOpen = true;
|
||||||
|
|
@ -469,6 +513,7 @@ async function chatTick(){
|
||||||
log.innerHTML = html || `<div class="skel">сообщений пока нет — напиши первым</div>`;
|
log.innerHTML = html || `<div class="skel">сообщений пока нет — напиши первым</div>`;
|
||||||
if (stick) log.scrollTop = log.scrollHeight;
|
if (stick) log.scrollTop = log.scrollHeight;
|
||||||
$("chat-note").textContent = "канал: портал (portal:" + (s.agent || "?") + ") · ответ агент пишет сам по такту";
|
$("chat-note").textContent = "канал: портал (portal:" + (s.agent || "?") + ") · ответ агент пишет сам по такту";
|
||||||
|
renderChannels(s.channels);
|
||||||
}catch(e){
|
}catch(e){
|
||||||
if (e instanceof SyntaxError) return; // полу-JSON от 503 — следующий тик перепишет
|
if (e instanceof SyntaxError) return; // полу-JSON от 503 — следующий тик перепишет
|
||||||
log.innerHTML = `<div class="skel">чат недоступен: ${esc(e.message)} — попробую через ${CHAT_REFRESH}с</div>`;
|
log.innerHTML = `<div class="skel">чат недоступен: ${esc(e.message)} — попробую через ${CHAT_REFRESH}с</div>`;
|
||||||
|
|
|
||||||
|
|
@ -35,7 +35,7 @@ _HEALTH_PORT = int(os.environ.get("SIMBIOT_DASH_HEALTH_PORT", "8766"))
|
||||||
_TOKEN = os.environ.get("SIMBIOT_DASH_TOKEN", "")
|
_TOKEN = os.environ.get("SIMBIOT_DASH_TOKEN", "")
|
||||||
_REFRESH = int(os.environ.get("SIMBIOT_DASH_REFRESH", "5"))
|
_REFRESH = int(os.environ.get("SIMBIOT_DASH_REFRESH", "5"))
|
||||||
|
|
||||||
_STATUS_ORDER = {"ready": 0, "running": 1, "failed": 2, "done": 3, "closed": 4}
|
_STATUS_ORDER = {"ready": 0, "running": 1, "blocked": 2, "failed": 3, "done": 4, "closed": 5}
|
||||||
_SIZE_COLOR = {"S": "#1d7a3e", "M": "#a3690f", "L": "#FF4F00", "XL": "#b3261e"}
|
_SIZE_COLOR = {"S": "#1d7a3e", "M": "#a3690f", "L": "#FF4F00", "XL": "#b3261e"}
|
||||||
|
|
||||||
# ── чат Simbiot прямо в дашборде (t-261001100740-99d55e) ──────────────────
|
# ── чат Simbiot прямо в дашборде (t-261001100740-99d55e) ──────────────────
|
||||||
|
|
@ -183,7 +183,33 @@ def build_chat(limit: int = 80) -> dict:
|
||||||
collapsed[-1]["ts"] = m["ts"]
|
collapsed[-1]["ts"] = m["ts"]
|
||||||
continue
|
continue
|
||||||
collapsed.append(m)
|
collapsed.append(m)
|
||||||
return {"ok": True, "agent": agent_name, "messages": collapsed[-limit:]}
|
out = {"ok": True, "agent": agent_name, "messages": collapsed[-limit:]}
|
||||||
|
# живой статус каналов (t-261004103718-d8112b): UI рендерит индикаторы
|
||||||
|
# вместо статичного «тоже дублируется в TG, если бот подключён»
|
||||||
|
try:
|
||||||
|
st = channel_status()
|
||||||
|
tg = _tg_stats()
|
||||||
|
tgst = st.get("telegram") or {}
|
||||||
|
tgst["token_loaded"] = tg.get("token_loaded")
|
||||||
|
tgst["received_24h"] = tg.get("received_24h")
|
||||||
|
tgst["last_msg_ts"] = tg.get("last_msg_ts")
|
||||||
|
# fallback: штампа поллера нет (старый main-харнес до рестарта), но
|
||||||
|
# токен загружен и свежие TG-сообщения капают в стор → канал жив.
|
||||||
|
if (not tgst.get("ok") and tg.get("token_loaded")
|
||||||
|
and (tg.get("last_msg_age_s") is not None)
|
||||||
|
and tg["last_msg_age_s"] <= 600):
|
||||||
|
tgst["ok"] = True
|
||||||
|
tgst["age_s"] = tg["last_msg_age_s"]
|
||||||
|
tgst["detail"] = "жив по событиям стора (штампа поллера ещё нет)"
|
||||||
|
st["telegram"] = tgst
|
||||||
|
st["hermes"] = _hermes_marker()
|
||||||
|
out["channels"] = st
|
||||||
|
except Exception as e:
|
||||||
|
# статус — диагностика: чат не должен падать из-за него, но причина
|
||||||
|
# должна быть видна (journalctl), иначе «пропавшие индикаторы»
|
||||||
|
# отлаживаются вслепую (t-261004103718: молчаливый pass скрыл сбой)
|
||||||
|
print(f"[chat] channels status error: {e}", flush=True)
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
def chat_send(text: str) -> dict:
|
def chat_send(text: str) -> dict:
|
||||||
|
|
@ -291,7 +317,7 @@ def build_state() -> dict:
|
||||||
"tasks_total": len(tasks),
|
"tasks_total": len(tasks),
|
||||||
"tasks_by_status": {
|
"tasks_by_status": {
|
||||||
s: sum(1 for t in tasks if t["status"] == s)
|
s: sum(1 for t in tasks if t["status"] == s)
|
||||||
for s in ("ready", "running", "failed", "done", "closed")
|
for s in ("ready", "running", "blocked", "failed", "done", "closed")
|
||||||
},
|
},
|
||||||
"tasks_by_status_size": counts,
|
"tasks_by_status_size": counts,
|
||||||
"escalations_open": len(escalations),
|
"escalations_open": len(escalations),
|
||||||
|
|
@ -610,5 +636,145 @@ def main():
|
||||||
ThreadingHTTPServer((_ADDR, _PORT), _Handler).serve_forever()
|
ThreadingHTTPServer((_ADDR, _PORT), _Handler).serve_forever()
|
||||||
|
|
||||||
|
|
||||||
|
# ── живой статус каналов портала/TG (t-261004103718-d8112b) ────────────────
|
||||||
|
# Каждый реальный пользователь канала (сек-рефлекс portal_channel.sec_reflex,
|
||||||
|
# TG-поллер tg_inbox.poll_loop) пишет штамп harness/channel_<portal|tg>.json
|
||||||
|
# после УСПЕШНОГО обмена. Дашборд только читает штампы — сам портал не дёргает
|
||||||
|
# (GET-проба poll-эндпоинта дублировала бы сек-рефлекс на его айсберге),
|
||||||
|
# и getUpdates не пробует (конфликт long polling с процессом поллера).
|
||||||
|
_CHANNEL_MAX_AGE_MS = 10 * 60 * 1000 # штамп старше 10 мин = канал вниз
|
||||||
|
|
||||||
|
_portal_probe_cache = {"ts": 0.0, "res": None}
|
||||||
|
|
||||||
|
|
||||||
|
def _portal_probe() -> dict:
|
||||||
|
"""Fallback-проба портала (когда штампа рефлекса нет/протух).
|
||||||
|
|
||||||
|
main-харнес новой версии пишет штамп при каждом сек-рефлексе; пока он не
|
||||||
|
перезапущен (рестарт откладывается до тихого окна — LLM-воркеры), дашборд
|
||||||
|
пробует портал сам: GET /channel/poll?since=now — read-only, consumed
|
||||||
|
ставится только отдельным POST (не делаем). Кэш 60с: клиент чата тикает
|
||||||
|
каждые 10с — дёргать портал чаще незачем.
|
||||||
|
"""
|
||||||
|
import urllib.parse
|
||||||
|
import urllib.request
|
||||||
|
|
||||||
|
now = time.time()
|
||||||
|
if _portal_probe_cache["res"] and now - _portal_probe_cache["ts"] < 60:
|
||||||
|
return _portal_probe_cache["res"]
|
||||||
|
try:
|
||||||
|
from harness import portal_channel as _pch
|
||||||
|
cfg = _pch._cfg()
|
||||||
|
if not (cfg.get("portal_url") and cfg.get("agent_token")
|
||||||
|
and cfg.get("agent_name")):
|
||||||
|
res = {"ok": False, "age_s": None, "ts": 0,
|
||||||
|
"detail": "канал не сконфигурён (нет /etc/coop-reporter.env)"}
|
||||||
|
else:
|
||||||
|
url = (f"{cfg['portal_url']}/api/agent/"
|
||||||
|
f"{urllib.parse.quote(cfg['agent_name'])}"
|
||||||
|
f"/channel/poll?since={int(now * 1000)}")
|
||||||
|
req = urllib.request.Request(
|
||||||
|
url, headers={"X-Agent-Token": cfg["agent_token"]})
|
||||||
|
with urllib.request.urlopen(req, timeout=5) as r:
|
||||||
|
json.loads(r.read().decode())
|
||||||
|
res = {"ok": True, "age_s": 0, "ts": int(now * 1000),
|
||||||
|
"detail": "проба ok (штампа рефлекса ещё нет — старый main-харнес)"}
|
||||||
|
except Exception as e:
|
||||||
|
res = {"ok": False, "age_s": None, "ts": 0,
|
||||||
|
"detail": f"проба не прошла: {str(e)[:80]}"}
|
||||||
|
_portal_probe_cache.update(ts=now, res=res)
|
||||||
|
return res
|
||||||
|
|
||||||
|
# Старый харнес на Гермесе (hermes-rita, 2.28.125.152 — docs/migration-hermes.md)
|
||||||
|
# не узнаётся по /etc/coop-reporter.env (документированные ключи: PORTAL_URL,
|
||||||
|
# AGENT_TOKEN). Без честного признака не рисуем "старый харнес" в UI.
|
||||||
|
_HERMES_MARKER = None
|
||||||
|
|
||||||
|
|
||||||
|
def _hermes_marker() -> str | None:
|
||||||
|
"""Признак, что эти файлы запущены на старом Гермес-харнесе (для UI).
|
||||||
|
|
||||||
|
Пока легитимного маркера нет — None (Simbiot-дашборд ни с чем не путаем).
|
||||||
|
"""
|
||||||
|
return _HERMES_MARKER
|
||||||
|
|
||||||
|
|
||||||
|
def channel_status() -> dict:
|
||||||
|
"""Живой статус каналов чата: portal (poll портала) + telegram (inbox).
|
||||||
|
|
||||||
|
Каждый канал: ok (свежий успешный обмен), age_s (возраст штампа),
|
||||||
|
ts (ms последнего успеха), detail (что именно успешно).
|
||||||
|
"""
|
||||||
|
now_ms = int(time.time() * 1000)
|
||||||
|
|
||||||
|
def _read(name: str) -> dict:
|
||||||
|
path = os.path.join(_HERE, f"channel_{name}.json")
|
||||||
|
try:
|
||||||
|
with open(path) as f:
|
||||||
|
d = json.load(f)
|
||||||
|
ts = int(d.get("ts") or 0)
|
||||||
|
age = now_ms - ts
|
||||||
|
if age < 0 or age > _CHANNEL_MAX_AGE_MS:
|
||||||
|
raise FileNotFoundError # штамп протух → считаем как отсутствующий
|
||||||
|
return {"ok": bool(d.get("ok")), "age_s": round(age / 1000),
|
||||||
|
"ts": ts, "detail": str(d.get("detail") or "")[:120]}
|
||||||
|
except (FileNotFoundError, ValueError, TypeError):
|
||||||
|
# штампа нет (файл не создан / битый / старше 10 мин).
|
||||||
|
# portal: честная fallback-проба (штамп пишет main-харнес новой
|
||||||
|
# версии — до его рестарта проба даёт живую правду).
|
||||||
|
if name == "portal":
|
||||||
|
return _portal_probe()
|
||||||
|
return {"ok": False, "age_s": None, "ts": 0,
|
||||||
|
"detail": "нет свежего штампа поллера"}
|
||||||
|
except Exception as e:
|
||||||
|
return {"ok": False, "age_s": None, "ts": 0,
|
||||||
|
"detail": f"штамп не читается: {str(e)[:80]}"}
|
||||||
|
|
||||||
|
return {"portal": _read("portal"), "telegram": _read("tg"),
|
||||||
|
"checked_at": now_ms}
|
||||||
|
|
||||||
|
|
||||||
|
def _tg_stats(days: float = 1.0) -> dict:
|
||||||
|
"""TG-факты из стора: токен загружен? + входящие за последние 24ч."""
|
||||||
|
from datetime import datetime, timedelta, timezone
|
||||||
|
|
||||||
|
tg_token = None
|
||||||
|
try:
|
||||||
|
from harness import tg_inbox as _tg
|
||||||
|
_tg._load_env()
|
||||||
|
tg_token = bool(_tg.TG_TOKEN)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
n24, last_ts = 0, None
|
||||||
|
try:
|
||||||
|
db = es._conn()
|
||||||
|
cutoff = (datetime.now(timezone.utc)
|
||||||
|
- timedelta(hours=int(days * 24))).isoformat()
|
||||||
|
# portal-канальные MessageReceived пишет НЕ TG-поллер — исключаем:
|
||||||
|
# иначе живой портал-чат рисовал бы «зелёный» telegram (найдено в
|
||||||
|
# t-261004103718: portal ok, tg штампа нет — а события капают).
|
||||||
|
row = db.execute(
|
||||||
|
"SELECT count(*) AS n, max(timestamp) AS last FROM events "
|
||||||
|
"WHERE event_type='MessageReceived' AND timestamp >= ? "
|
||||||
|
"AND (data IS NULL OR data NOT LIKE '%\"chat_id\": \"portal%')",
|
||||||
|
(cutoff,)).fetchone()
|
||||||
|
db.close()
|
||||||
|
n24 = int(row["n"] or 0)
|
||||||
|
last_ts = row["last"]
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
age_s = None
|
||||||
|
if last_ts:
|
||||||
|
try:
|
||||||
|
from datetime import datetime as _dt
|
||||||
|
age_s = max(0, round(time.time()
|
||||||
|
- _dt.fromisoformat(str(last_ts)).timestamp()))
|
||||||
|
except Exception:
|
||||||
|
age_s = None
|
||||||
|
return {"token_loaded": tg_token, "received_24h": n24, "last_msg_ts": last_ts,
|
||||||
|
"last_msg_age_s": age_s}
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
main()
|
main()
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -268,8 +268,16 @@ def build_context(level_cfg, model: dict, batch=None):
|
||||||
digests.sort(key=lambda d: d["ts"], reverse=True)
|
digests.sort(key=lambda d: d["ts"], reverse=True)
|
||||||
if digests:
|
if digests:
|
||||||
parts.append("ВЫЖИМКИ НИЖНИХ УРОВНЕЙ (последние, сжатые):")
|
parts.append("ВЫЖИМКИ НИЖНИХ УРОВНЕЙ (последние, сжатые):")
|
||||||
|
# дедуп по тексту (эксп. #1, t-261005121525): тихий мин-уровень давал
|
||||||
|
# 6 идентичных [gate:quiet]-строк в каждом шаге промпта — чистый burn.
|
||||||
|
# Теперь повтор схлопывается в одну строку с счётчиком ×N.
|
||||||
|
seen: dict = {}
|
||||||
for d in digests[:6]:
|
for d in digests[:6]:
|
||||||
parts.append(f"- [{d['level']}] {d['text'][:400]}")
|
key = (d["level"], d["text"][:400])
|
||||||
|
seen[key] = seen.get(key, 0) + 1
|
||||||
|
for (lvl, text), n in seen.items():
|
||||||
|
rep = f" (×{n} подряд)" if n > 1 else ""
|
||||||
|
parts.append(f"- [{lvl}] {text}{rep}")
|
||||||
|
|
||||||
# эскалации: открытые, адресованные этому уровню и выше — в приоритете
|
# эскалации: открытые, адресованные этому уровню и выше — в приоритете
|
||||||
escs = []
|
escs = []
|
||||||
|
|
|
||||||
|
|
@ -99,7 +99,7 @@ def cmd_status() -> str:
|
||||||
st = cs.get(name) or {}
|
st = cs.get(name) or {}
|
||||||
err = f", ошибок {st['errors']}" if st.get("errors") else ""
|
err = f", ошибок {st['errors']}" if st.get("errors") else ""
|
||||||
lines.append(f" {name}: тиков {st.get('ticks', 0)}{err}")
|
lines.append(f" {name}: тиков {st.get('ticks', 0)}{err}")
|
||||||
parts = [f"{k} {counts[k]}" for k in ("ready", "running", "done", "failed") if counts.get(k)]
|
parts = [f"{k} {counts[k]}" for k in ("ready", "running", "blocked", "failed", "done") if counts.get(k)]
|
||||||
lines.append("📋 задачи: " + (" · ".join(parts) if parts else "нет"))
|
lines.append("📋 задачи: " + (" · ".join(parts) if parts else "нет"))
|
||||||
lines.append(f"🚨 эскалаций открытых: {open_esc}")
|
lines.append(f"🚨 эскалаций открытых: {open_esc}")
|
||||||
lines.append(f"💰 спенд (последние {len(spend)} тиков): ~{tokens:,} токенов".replace(",", " "))
|
lines.append(f"💰 спенд (последние {len(spend)} тиков): ~{tokens:,} токенов".replace(",", " "))
|
||||||
|
|
|
||||||
1
out/9router-gemini-test.png
Normal file
1
out/9router-gemini-test.png
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
{"error":{"message":"[gemini/gemini-3.1-flash-image-preview] [429]: You exceeded your current quota, please check your plan and billing details. For more information on this error, head to: https://ai.google.dev/gemini-api/docs/rate-limits. To monitor your current usage, head to: https://ai.dev/rate-limit. \n* Quota exceeded for metric: generativelanguage.googleapis.com/generate_content_free_tier_requests, limit: 0, model: gemini-3.1-flash-image\n* Quota exceeded for metric: generativelanguage.googleapis.com/generate_content_free_tier_requests, limit: 0, model: gemini-3.1-flash-image\n* Quota exceeded for metric: generativelanguage.googleapis.com/generate_content_free_tier_input_token_count, limit: 0, model: gemini-3.1-flash-image\nPlease retry in 12h6m1.119176629s. (reset after 4s)"}}
|
||||||
54
out/seedance_simbiot_pitch.md
Normal file
54
out/seedance_simbiot_pitch.md
Normal file
|
|
@ -0,0 +1,54 @@
|
||||||
|
# Simbiot — питч для видео (Seedance)
|
||||||
|
|
||||||
|
## 1. Что говорить на камеру (~30 сек, живым языком)
|
||||||
|
|
||||||
|
> Представь: ты заходишь в стартап — и через неделю у тебя работает не один ассистент,
|
||||||
|
> а целая команда ИИ-агентов: один отвечает клиентам, второй пишет код, третий следит
|
||||||
|
> за деньгами и метриками.
|
||||||
|
>
|
||||||
|
> Это Simbiot. Обычно фаундер собирает зоопарк из агентов и харнесов — а когда уходит
|
||||||
|
> разработчик, вся эта магия уходит вместе с ним. Simbiot — это общий харнес организации:
|
||||||
|
> слой поверх Hermes, Claude Code и других. Он координирует флот агентов, фиксирует
|
||||||
|
> решения компании и копит удачные приёмы как «гены» — и оставляет только те, что
|
||||||
|
> реально приносят прибыль.
|
||||||
|
>
|
||||||
|
> Мы уже гоняем его на семи собственных продуктах. Строй компанию — а не зоопарк ботов.
|
||||||
|
|
||||||
|
Хронометраж: ~35 сек. Если нужно короче (15 сек) — оставить первый и последний абзац.
|
||||||
|
|
||||||
|
## 2. Промт для Seedance (весь, целиком — скормить как есть)
|
||||||
|
|
||||||
|
Seedance генерирует клипы по 5–10 сек, поэтому видео собирается из 3 шотов
|
||||||
|
( narrator говорит поверх — в кадре речи не нужно, генерим b-roll ).
|
||||||
|
|
||||||
|
**Общий стиль (добавлять в конец каждого промта):**
|
||||||
|
> Dark cinematic tech aesthetic, deep navy-black background (#0a0c12) with violet
|
||||||
|
> and purple glow accents (#7c6cf5, #8b5cf6), soft volumetric light, shallow depth
|
||||||
|
> of field, film grain, high detail, 4k, smooth camera motion, no text, no captions,
|
||||||
|
> no people talking, ambient mood. 16:9.
|
||||||
|
|
||||||
|
**Shot 1 (0–10 сек) — «зоопарк в хаосе»:**
|
||||||
|
> Slow dolly-in through a dark server room: dozens of small glowing robots and holographic
|
||||||
|
> screens flickering chaotically, tangled cables, scattered disconnected UI panels floating
|
||||||
|
> in the air, each glowing a different cold color, subtle digital glitches.
|
||||||
|
|
||||||
|
**Shot 2 (10–20 сек) — «Simbiot orchestration»:**
|
||||||
|
> The chaos converges: tangled light streams flow into one glowing violet orb at the center
|
||||||
|
> of a dark control room, holographic dashboard panels organize around it in a perfect grid,
|
||||||
|
> elegant lines of data pulse from the orb to the panels like a neural network heartbeat,
|
||||||
|
> slow orbit camera movement.
|
||||||
|
|
||||||
|
**Shot 3 (20–30 сек) — «финал / бренд»:**
|
||||||
|
> Wide cinematic shot of a modern dark startup office at night, a founder silhouette
|
||||||
|
> calmly watching a large wall display where agent dashboards grow revenue charts,
|
||||||
|
> violet light washes the room, camera slowly pulls back through a window into the
|
||||||
|
> city skyline at dusk.
|
||||||
|
|
||||||
|
## 3. Быстрый вариант (один промт, если лень склеивать)
|
||||||
|
|
||||||
|
> Cinematic 30-second tech brand film: dark navy-black world with violet glow (#7c6cf5).
|
||||||
|
> It opens on chaos — dozens of flickering holographic screens and tangled light streams.
|
||||||
|
> A luminous violet orb emerges and orchestrates everything: panels align into a grid,
|
||||||
|
> data flows like a heartbeat. Final wide shot: founder silhouette watching revenue
|
||||||
|
> charts grow on a wall of dashboards in a dark modern office. Slow dolly and orbit
|
||||||
|
> camera, volumetric light, film grain, no text, 16:9.
|
||||||
129
scripts/alipay_watch.py
Normal file
129
scripts/alipay_watch.py
Normal file
|
|
@ -0,0 +1,129 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Ватчер Alipay на live-счёте Stripe (эскалация e-261005142309-390896).
|
||||||
|
|
||||||
|
Контекст: Alex включил тумблер Alipay в дашборде (preference=on), но
|
||||||
|
capability ещё not available — счёт ждёт аппрува Stripe («говорит что нужен
|
||||||
|
ещё аппрувал, ждем», m-516173789). Включить через API нельзя (rk_live
|
||||||
|
restricted, available=false).
|
||||||
|
|
||||||
|
Этот скрипт — детерминированный поллинг без LLM (systemd-таймер, 5 мин):
|
||||||
|
- пока alipay.available=false → тихо выходим (state=waiting);
|
||||||
|
- как только Stripe активирует capability → создаёт Checkout Session
|
||||||
|
$100 one_time (price_1UNCYGBT53G7FGSQya04h28r) с card+alipay и
|
||||||
|
идемпотентно (cli.py tg-notify -k) отправляет ссылку в чат;
|
||||||
|
- после успешной доставки пишет done-маркер и больше не работает.
|
||||||
|
|
||||||
|
Ключ читается из /root/coop/.env (STRIPE_LIVE_KEY, реестр KEYS-REGISTRY).
|
||||||
|
Состояние: /var/lib/simbiot-harness/alipay-watch.state.
|
||||||
|
Выход: 0 = ждём/отработал, 3 = ошибка конфигурации (для журнала).
|
||||||
|
"""
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
import urllib.error
|
||||||
|
import urllib.parse
|
||||||
|
import urllib.request
|
||||||
|
|
||||||
|
STATE = "/var/lib/simbiot-harness/alipay-watch.state"
|
||||||
|
ENV_FILE = "/root/coop/.env"
|
||||||
|
PRICE = "price_1UNCYGBT53G7FGSQya04h28r" # $100 USD one_time, live
|
||||||
|
NOTIFY_KEY = "alipay-live-100-link" # идемпотентность доставки (t-260930083937)
|
||||||
|
HARNESS = "/root/simbiot-harness"
|
||||||
|
VENV_PY = f"{HARNESS}/venv/bin/python"
|
||||||
|
|
||||||
|
|
||||||
|
def _stripe_key() -> str | None:
|
||||||
|
try:
|
||||||
|
with open(ENV_FILE) as f:
|
||||||
|
for line in f:
|
||||||
|
line = line.strip()
|
||||||
|
if line.startswith("STRIPE_LIVE_KEY="):
|
||||||
|
return line.split("=", 1)[1].strip().strip('"').strip("'")
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
|
return os.environ.get("STRIPE_LIVE_KEY")
|
||||||
|
|
||||||
|
|
||||||
|
def _call(path: str, method: str = "GET", data: dict | None = None) -> dict:
|
||||||
|
key = _stripe_key()
|
||||||
|
if not key:
|
||||||
|
return {"_error": "no STRIPE_LIVE_KEY"}
|
||||||
|
body = urllib.parse.urlencode(data).encode() if data is not None else None
|
||||||
|
req = urllib.request.Request(
|
||||||
|
"https://api.stripe.com/v1/" + path,
|
||||||
|
headers={"Authorization": f"Bearer {key}"},
|
||||||
|
method=method,
|
||||||
|
data=body,
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
with urllib.request.urlopen(req, timeout=25) as r:
|
||||||
|
return json.loads(r.read())
|
||||||
|
except urllib.error.HTTPError as e:
|
||||||
|
return {"_error": e.code, "body": e.read().decode()[:400]}
|
||||||
|
|
||||||
|
|
||||||
|
def _read_state() -> str:
|
||||||
|
try:
|
||||||
|
with open(STATE) as f:
|
||||||
|
return f.read().strip()
|
||||||
|
except OSError:
|
||||||
|
return ""
|
||||||
|
|
||||||
|
|
||||||
|
def _write_state(s: str) -> None:
|
||||||
|
os.makedirs(os.path.dirname(STATE), exist_ok=True)
|
||||||
|
with open(STATE, "w") as f:
|
||||||
|
f.write(s)
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
if _read_state() == "done":
|
||||||
|
return 0 # ссылка создана и доставлена — ватчер отработал
|
||||||
|
|
||||||
|
cfgs = _call("payment_method_configurations?limit=100")
|
||||||
|
for c in cfgs.get("data", []):
|
||||||
|
alipay = c.get("alipay") or {}
|
||||||
|
if alipay.get("available"):
|
||||||
|
# capability активна — создаём линк card+alipay
|
||||||
|
sess = _call("checkout/sessions", "POST", {
|
||||||
|
"mode": "payment",
|
||||||
|
"line_items[0][price]": PRICE,
|
||||||
|
"line_items[0][quantity]": "1",
|
||||||
|
"success_url": "https://cil.by/thanks",
|
||||||
|
"cancel_url": "https://cil.by/",
|
||||||
|
"payment_method_types[0]": "card",
|
||||||
|
"payment_method_types[1]": "alipay",
|
||||||
|
})
|
||||||
|
url = sess.get("url")
|
||||||
|
if not url:
|
||||||
|
# доступна, но сессия не собирается — оставить ждущим,
|
||||||
|
# причина в журнале таймера; следующий прогон повторит
|
||||||
|
_write_state("available-but-failed")
|
||||||
|
print("alipay available, session failed:",
|
||||||
|
sess.get("_error"), sess.get("body", "")[:200])
|
||||||
|
return 0
|
||||||
|
text = (f"Alipay активирован на live-счёте Stripe. "
|
||||||
|
f"Боевая ссылка $100 (card + Alipay): {url}")
|
||||||
|
r = subprocess.run(
|
||||||
|
[VENV_PY, f"{HARNESS}/cli.py", "tg-notify", text,
|
||||||
|
"-k", NOTIFY_KEY],
|
||||||
|
capture_output=True, text=True, timeout=60,
|
||||||
|
)
|
||||||
|
out = (r.stdout or "").strip()
|
||||||
|
print("notify:", out)
|
||||||
|
if out.startswith("OK") or "SKIP" in out:
|
||||||
|
_write_state("done")
|
||||||
|
return 0
|
||||||
|
_write_state("notify-failed")
|
||||||
|
return 0
|
||||||
|
print(f"waiting: alipay preference={alipay.get('display_preference', {}).get('value')} "
|
||||||
|
f"available={alipay.get('available')} (cfg {c.get('id')})")
|
||||||
|
_write_state("waiting")
|
||||||
|
return 0
|
||||||
|
print("no live payment_method_configuration found:", cfgs.get("_error", "?"))
|
||||||
|
return 3
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
sys.exit(main())
|
||||||
14
scripts/nightly-selfimprove.sh
Executable file
14
scripts/nightly-selfimprove.sh
Executable file
|
|
@ -0,0 +1,14 @@
|
||||||
|
#!/bin/bash
|
||||||
|
# Ночная самодоработка харнеса (директива Alex m-516173718, эскалация e-261005074512):
|
||||||
|
# подаёт задачу [M] в очередь харнеса — ближайший hour-тик подбирает и исполняет.
|
||||||
|
# Журнал итераций: artifacts/self-improvement-log.md
|
||||||
|
cd /root/simbiot-harness || exit 1
|
||||||
|
TITLE="Ночная самодоработка харнеса: ретро + эксперимент"
|
||||||
|
# идемпотентность: не дублируем, если задача уже ждёт/выполняется
|
||||||
|
if ./venv/bin/python cli.py tasks 2>/dev/null | grep -F "$TITLE" | grep -qE ' (ready|running)'; then
|
||||||
|
echo "$(date -u +%FT%TZ) skip: задача уже в очереди" >> /var/log/simbiot-selfimprove.log
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
./venv/bin/python cli.py task "$TITLE" \
|
||||||
|
-d "Директива Alex m-516173718 (e-261005074512). 1) Ретро суток: cli.py events, cli.py status, cli.py aum — что сработало/что сломалось (упавшие тики, эскалации, закрытые задачи, burn). 2) Один эксперимент над собой: гипотеза вида «метрика X -> изменение -> ожидание»; лёгкий провести сейчас в пределах контекста, тяжёлый оформить задачей на день. 3) Журнал: artifacts/self-improvement-log.md — дописать строку по формату (#N | дата | гипотеза | метрика до->после | вывод). 4) Ночь — тишина в чате (кроме критичного), результат уходит в ответ задачи, его поднимет утренний дайджест. Фильтр AUM: эксперимент служит +1% к AUM в год+." \
|
||||||
|
-m 30
|
||||||
4
~/.bubblewrap/config.json
Normal file
4
~/.bubblewrap/config.json
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
{
|
||||||
|
"jdkPath": "/usr/lib/jvm/java-17-openjdk-amd64",
|
||||||
|
"androidSdkPath": "/opt/android-sdk"
|
||||||
|
}
|
||||||
8
~/.config/opencode/opencode.json
Normal file
8
~/.config/opencode/opencode.json
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
{
|
||||||
|
"permission": {
|
||||||
|
"edit": "allow",
|
||||||
|
"bash": "allow",
|
||||||
|
"webfetch": "allow"
|
||||||
|
},
|
||||||
|
"share": "disabled"
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue