Compare commits
No commits in common. "5a889e42a41c210d8c538b76f3ab176f7bf8b6b8" and "7f445392ee3c5415021c249225c1019cc9ed4254" have entirely different histories.
5a889e42a4
...
7f445392ee
82 changed files with 64 additions and 9365 deletions
23
README.md
23
README.md
|
|
@ -275,29 +275,6 @@ sqlite, rag.db ~12.5k чанков по контексту/навыкам, herme
|
|||
Ручной вызов: `cli.py rag-query "вопрос" -k 5 [--db ...] [--json]`.
|
||||
Выключить: `RAG_DISABLED=1`.
|
||||
|
||||
## MCP портала в тике (t-261010093054)
|
||||
|
||||
Каждый LLM-тик получает инструмент `portal_mcp` — живой портал кооператива
|
||||
через MCP-шлюз cil.by (JSON-RPC 2.0, агент зарегистрирован 29.09, владелец
|
||||
alex). Ключевое для Alex: **звонки назначаются из чата** —
|
||||
`booking_slots` (свободные слоты, call=30мин/session=20мин, график 3/3
|
||||
Минск) → `booking_create` (jitsi-звонок со ссылкой, гостю письмо, запись и
|
||||
транскрипция автоматом). Остальной спектр: list_calls/get_call (саммари
|
||||
прошедших), list_members/projects_list/list_agents, treasury_status,
|
||||
list_events, whoami/report_task/report_event.
|
||||
|
||||
Лимиты зашиты в код (не в промпт): **4 вызова на тик**, таймаут 15с,
|
||||
обрезка результата 6k симв.; тул не поднимается без токена. Guardrail:
|
||||
деньги-тулы `product_create`/`product_set_mode` (Stripe) запрещены по
|
||||
умолчанию — `SIMBIOT_MCP_ALLOW_ALL=1` снимает. Ошибки шлюза возвращаются
|
||||
текстом `error: …`, тик не валится; трейс — `PortalMcpCalled` в сторе.
|
||||
|
||||
Токен: env `SIMBIOT_PORTAL_MCP_TOKEN` → `<home>/secrets/portal-mcp-token`
|
||||
(legacy-фолбэк `.simbiot-portal-token` в корне репо). Ручной вызов:
|
||||
`cli.py mcp-call booking_slots '{"type": "call"}'`, список тулов —
|
||||
`cli.py mcp-call --list`. Клиент: `harness/portal_mcp.py`, обёртка тика —
|
||||
`_llm_worker._portal_mcp_tool_for`, тест `tests/test_portal_mcp.py`.
|
||||
|
||||
## Хард-кап шагов тика (e-260930031800)
|
||||
|
||||
Мин-воркер уходил в исследовательские цепочки (90+ tool-вызовов) и умирал
|
||||
|
|
|
|||
|
|
@ -1,168 +0,0 @@
|
|||
%PDF-1.4
|
||||
%“Œ‹ž ReportLab Generated PDF document (opensource)
|
||||
1 0 obj
|
||||
<<
|
||||
/F1 2 0 R /F2 3 0 R /F3 4 0 R /F4 5 0 R /F5 7 0 R
|
||||
>>
|
||||
endobj
|
||||
2 0 obj
|
||||
<<
|
||||
/BaseFont /Helvetica /Encoding /WinAnsiEncoding /Name /F1 /Subtype /Type1 /Type /Font
|
||||
>>
|
||||
endobj
|
||||
3 0 obj
|
||||
<<
|
||||
/BaseFont /Times-Roman /Encoding /WinAnsiEncoding /Name /F2 /Subtype /Type1 /Type /Font
|
||||
>>
|
||||
endobj
|
||||
4 0 obj
|
||||
<<
|
||||
/BaseFont /Times-Italic /Encoding /WinAnsiEncoding /Name /F3 /Subtype /Type1 /Type /Font
|
||||
>>
|
||||
endobj
|
||||
5 0 obj
|
||||
<<
|
||||
/BaseFont /Times-Bold /Encoding /WinAnsiEncoding /Name /F4 /Subtype /Type1 /Type /Font
|
||||
>>
|
||||
endobj
|
||||
6 0 obj
|
||||
<<
|
||||
/Contents 15 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 14 0 R /Resources <<
|
||||
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
|
||||
>> /Rotate 0 /Trans <<
|
||||
|
||||
>>
|
||||
/Type /Page
|
||||
>>
|
||||
endobj
|
||||
7 0 obj
|
||||
<<
|
||||
/BaseFont /Times-BoldItalic /Encoding /WinAnsiEncoding /Name /F5 /Subtype /Type1 /Type /Font
|
||||
>>
|
||||
endobj
|
||||
8 0 obj
|
||||
<<
|
||||
/Contents 16 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 14 0 R /Resources <<
|
||||
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
|
||||
>> /Rotate 0 /Trans <<
|
||||
|
||||
>>
|
||||
/Type /Page
|
||||
>>
|
||||
endobj
|
||||
9 0 obj
|
||||
<<
|
||||
/Contents 17 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 14 0 R /Resources <<
|
||||
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
|
||||
>> /Rotate 0 /Trans <<
|
||||
|
||||
>>
|
||||
/Type /Page
|
||||
>>
|
||||
endobj
|
||||
10 0 obj
|
||||
<<
|
||||
/Contents 18 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 14 0 R /Resources <<
|
||||
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
|
||||
>> /Rotate 0 /Trans <<
|
||||
|
||||
>>
|
||||
/Type /Page
|
||||
>>
|
||||
endobj
|
||||
11 0 obj
|
||||
<<
|
||||
/Contents 19 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 14 0 R /Resources <<
|
||||
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
|
||||
>> /Rotate 0 /Trans <<
|
||||
|
||||
>>
|
||||
/Type /Page
|
||||
>>
|
||||
endobj
|
||||
12 0 obj
|
||||
<<
|
||||
/PageMode /UseNone /Pages 14 0 R /Type /Catalog
|
||||
>>
|
||||
endobj
|
||||
13 0 obj
|
||||
<<
|
||||
/Author (Shkor Labs LLC) /CreationDate (D:20260923161207+00'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20260923161207+00'00') /Producer (ReportLab PDF Library - \(opensource\))
|
||||
/Subject (\(unspecified\)) /Title (Studio Participation Agreement \(42 Labs\)) /Trapped /False
|
||||
>>
|
||||
endobj
|
||||
14 0 obj
|
||||
<<
|
||||
/Count 5 /Kids [ 6 0 R 8 0 R 9 0 R 10 0 R 11 0 R ] /Type /Pages
|
||||
>>
|
||||
endobj
|
||||
15 0 obj
|
||||
<<
|
||||
/Filter [ /ASCII85Decode /FlateDecode ] /Length 2786
|
||||
>>
|
||||
stream
|
||||
Gatm<?#SK-&q05P(_h2!mY.e,bHd@\ng7*D^eK5e#=XdIJX0u=)d.8S.Tu5D^I(o)/Usk._4j@+>P)msbaC7Co"`4/T(t<N!rP65M]30h+!rf5677H.&-e=/5<kM_5/^4J4E;IWHA#qt?SU/f]ZQrj7ueD+!<f+VHJKYAC^I3UR8`T=_;l,or)`pPQjk1h7M@/LD!os?7S]3CM$L<?F5gM>bZkN2;t/lF4d^3iK"`r8$I^l.]IY>QG:rY4qHhp6@OHtZ+*ub]nWG<:OR]=1\bO!nrVrAW\6uJR\-^!0[(!cMSSa?6&(e9t@FW&agg8fsqNK[;GL6A;58\bcgeA]I*-K7#pKVuYKg#pe<*EhHn3S_t22lQRZsb6DMQj1A.DG*&()d(7AoLIW<!\XOI6-hq)e(#Jn#0G&-e/0kZe+sMRgDam?Uhs4A8j)d&'ih,'+.Zq%k5_iO:"TO*Ot,:E]PR.OR.[te2\I7'I>ga2#QtSYEp)]mtNHHqRZ2rZbH/4FP+s//=;*q.Po(Q7"f"u>7ni!!WBR%F@cjNCeH?.Ho.G_U=.a8W?,o[Cjg^-"DA[F;1U0+L#P?Cn/4>p,B?"D)]d.71f`mr_DP3_W9"pR=%D%4cq0?Cf'8OKipo4)6$O0l^l,kn*h_J]!$*_aOUm2m_r9.>qe[JFD9\EKQp]/b9HcGW,?'maV3PH!,/kJJq=YhCP>`m>K9bFd*S9si2t5Vdku/O-R9BSh&rAP[dU:i5L!OAP/g(L>p&c/IO;04jMc[3%_ps@ABi358i%6[Pp"Jmhm[qd]--4J=Zo.Vk1%-H%8aJIn+RYr,mf<=Vk$^6&iS\d,#^?5#K-*U'09Co+HXU'6j`^`dW-#M4mkFQ$)Z47nP*0B;.u;kgs8)S@0HT9+O&o[1mO%\c/&jJE4p'7DQ@;<B;68%ZHacl=Dq1o(,&$oD/&a8<WT?,oVEtZM4F;:>GWVYV\01@MIGaRHW7O]!UGHT;JI/W_d[HHZQj>0-1TnU=Z?f\H<_t52g68i5+7,C/%'=is&_QbVF$Z*^o&odc*(KIt<&TimcfHH(WESeMkBq.N81IK6Wml'Ud?I3SU,0HL^2ic!LAl#p`s@o.<oCHV_mY!_;IRqf=&^%0Q0kHD-q:p&NL"94VM0-]f$9/S[0"O"ZQr7&Lem_,8s-i$Br,+$WtcGnGWf)ZXn;.U,nlaHC4Y6c(U+F9kOaoc%P:jBMn_l4%44kB,>+1aA*;G%C-rX&f:eRI9G4^"[SodC)_gFq`B-^:SuLFgV>)PQftmWhOl1^sB>@/W5F!Llg(&Qa"eHJ8]1`BtRXPA!F@!5G(">$@@9"tF>04sGMpQZGQ93Rgq0EH:n!%\nJQ8i!F$i(cY+"i#32GA@V]quab;3?J<VX=s%oj?U*:'?7Oi!X+L1&Z@;c:maGAS)G77G.$8\";FBh_()@7F/NV$r,qr`5#8_+[5MRuG).S?Fb&46TLR_=Z(H'*@cg#_GtUe[%M9as(,)m<tZs8_\ST9OJoKW)VKt)fk%cs/pMX#B$#"eMfp>erp*3c@8#!j0i>8nINW7,[%V>>2e"LTp1!n^=.g]dN$qk<7-SkkIY:6Rqf(D@UCXW=rS21eZ;\jTU"^@b5Y60K?b"a89!(o#t@c:!pYT]DoBW#B(lR6j%rjXh+4oFJu=iF=P`c_hN\fK_J7XZe&oA#%ZD4B7\YZ.,\)?,W26F!\RS=-+JCKc>UjH#.+F+A[c:o<Ri):+<,MopFr`tl]Y[D\GH4JP]L]RuF^+CQ9Fs"=6c+H5%%HAnrQ6$Y]-M/oE3.Y?8Y>ZX6C%guqGRnOR(sHEFp'[gg3YmIKlNhG;nP2%E`FS=gW*b\H\lX4<;-Cp+7JSE1URPM$ScUnC(MmMear&DQ*t2k:,H1C#fFTh!`EJH'^W$'QYTQCE8%Uod9_],Lq!.spjhrcITuiHhXl7E\-8`kBl+31n!GZP#qN5%@._McH!Voscoj8MKY;[K%K.Dphb&S+MRrdGD0JQC+YSs\\D,_\Dd^plA3nuL,i`TQgUe6pc0!B'+`L[.Q.M0rhoK.4`6NG[?h?[rWtA-2;iu$4;/[DcO0DC4]VqlU]2jG"=tIDm[s(O^`,SVU8M.5OjXb6OlG.'36:G;q4epM%!aF.(4Ck',1N3TAWThpD6;P#/We_!-PqZe!e;"f?OcBl$];[4`),[lj2mC5Ah]_!WgBho[L=3E[HGR^[_bT/c&pSDZ]Ost0*V\2OJ8T4Zf1Q2bc>#lR;:f=V;F[:[.s=^5:%(#$R<"hWV*]$o)9.W&;j&aO8a<gGgP^%*1Dri*O=N(1@&^>D[#bu]cRWR@3$F9!AhTE39Mqsm?1aM*]3Si@9O.cKlIgGQ6Ai-,.sW]rX;\V61,07(#X[gA=#,f$c&V%[/RT7O4r0`Z,`sZ'qPS%Tn=LstNZu;tI!o],2gm%\3JO1gVYt)GAi$MEN=[u_D,EO,L9aGCS$fR1Z/VfZ\^GDtXOFUp2S`F81P9'^GX7B5nV3B74\bX[q_qF>7"P*XV:2W7c6ZakWK7LrdV>OS37J^rcVpmkHe8D[e(U]aW9TopDL+;TW/fN%"Z/5dUo7&&9;sU_$>JMq^2"Ir(5]pF/VgcrNF[ns%boLMiD4;VgQcJ3^Bd/\oJna3E1G+:=ga)$;%J/6ZVd9%CWhlmch?W1G9Iq89I,Vbi1E7kW]'RU649^K3Z25>Ai#^Nq7;dVL3G8-^#?C9G9cErF-N`#Cp#h/(g&2p01caNh*?*kbuFA-!cMo,]A#4dI8k]A(cS%)\5ee'I/#qbQO:h~>endstream
|
||||
endobj
|
||||
16 0 obj
|
||||
<<
|
||||
/Filter [ /ASCII85Decode /FlateDecode ] /Length 3015
|
||||
>>
|
||||
stream
|
||||
Gau0Dmr-r?')g\%Jgg=hBAofc@EDE"qiI]"@Y%fm0?F8'S.>ioU2=dZaVCeNrpT:b8_KC\2ar6aKA)/1EotW92_tl0O+2`V5Mn0gnU\Bg%9O1ILF!=?hMaVq`]cW"q>/)n$!HQ4k)o+dmLe%&T4UNM!k2[,-=2e2&W:7m+2i65[UG@ape%d[Jj'%V*e'uqc*e3WC"(:!gn%%5`/0gk@2?)sT6ogMB"%Nbn>0`T0V#I\h69@28b("GG:]rP8r<-&J:GE/DQb>YU*L0#4IKX,,_O:kZX=;rfcQ6pABF`')0osZp&[J"P2PE-=g:Q7GH>++]b[[^h_`N%.B7tYCf=D]D2]Y)\ud;\Cs*bLF1LC9eeMC_PY__Ee<J'ifl3dW2V2dj()Q=7XCS.EQI5\]2"dm-)g8EiK@f^9eC,Q6JoOS]Lg9V#57(p>H[%aF3rC/)6SJ)!F7+S-jHE]'VW$VOq^?Snb6fj:ig]Jf42lgM\n&J-0tos;1b2bG>-)Z#X]4Y5eHiDlG^s/$=sMP-OADUD?q[WGqLWJ`i,ja/:Yp=P5>F#pfEC[6<nA;CJC'NB<Yud\b@2Z?^:!UV]ZaZim4Z'gofAb;6[tk[#MEt'[lm\u9.$&eR1Yh;8UhAl/TEA,7D$%@eA1i[4%_WB+`Y_B5MXd*V7(b[HK(!Vf,aqHeLr)=NeJW=C,ClQbhUm>G^F^WTgP12^9S+^(io\"E]f2S_io3B?j"rocE6)ji@0dBM8&OMtBZ(/eNGTnf!uVC-4/.8(E_7d5\BV3/h\j"J]^#'6&&X&'kBFt5>9TBB:QW0AOeW<OV%5ZNM'&*E#7A'aKVD6X6bk2sAi6/<Neqr.K/%*kEk.D?QE18si]&oXK-&"*B5HL[IM0sNA*\==l9IWZj_\_<RQ]TF8B)(H(s/:#)E5E\AiI2G`Rqpb0JQ;=H"Kd`,<H[)O*>f+M^p,+W;)?q/>VR:YAabfM/_QfX^ng`g:mUYr_o/!'t_;U^PMe\9lIL5'ZQuCSj\^iP7^m.,l5ST?@CF*eS[DXoJ9:2e140m[p!JKP>$,3;fL-!TQgN"8sB_M^oJ-o+cJu4VA>r,hT1S@#J"n"]9jQ`)*3HliUp6nK"rdYM[LKOhP/7q<!o!s-gl+]aP--+BYgM$DX0d$Q./coPu3cT?PKNo0jI`t0ih+Tm*&,9Rkj?=BhL%eV5+gXd\hE%aMm-6E^$t4(O0)Oc0):3F4"*\]+^`9%&2u70NRN-&3Z,<h6/gq/$I,ojI5)&R1`\iqbh'T0A'/\!77$d6(D@lQ`bt)YDtuWJ`Q;u!VPOeB8g!9pkkicd&%;j.H6uI**8;@CK$?#k9p8Q!BO@WmbnE@,iE"UdDrq4/8,<jn[%Vf>&K"AD7Dj=bnDT+TPB'?`,0Ro_8"SQJE4"h/(+^bZ7X7d=YrdX,8Qgt7%[jQg6L2US3)<gs8?aL3rKT&R<?lre0t;X`H"d`,s$$*/hDq&:K`uh%7oMWmW]Oo)2TWdh[l+gq[#.5lf3-F4t?*1-#h'iTNFRh$OVa?mijoDe<ZOUUIOr:)qHY&U$)FU\FJqc!,aWSK>`<[>B#W(">$)OH+eqZ_%YQd"!c*_f6%DFh:P4iE,@lij[K<m.kJf!JOn5+4>ol&'A>tM&I3/2^^'(i`:Q_%KPJCH9CW+oKWi4'ho\6F63iDdSL.15?4:0t+:@8o"pEPGc'-XQ?6u,((YGDQBn`d]Wp.:uqThASOah([PW&9QLk7[i`>J5+pkQGTnjYgsL?K\4Z=ll3lPHFP)/VqT47>hS*sH?ugH=/a2Jho]HVluc5E:Bg+TBV3m!G&>O[1aP+T"^mC^/Raod&Q%$9LnC2T@6:/Jk-H@RC^(VFHWBX6dd#0V.,u:3['YMk*j:8q>pCkc7Di5+%pn`6U[Z3de+c3Ec^2N)!<Tk_h%T<`>kph0FQ?!![kl3Vs*+UZ/1t@6=S,%mjK4Nf+;gmhJR(^ND@"D?J+^qb`L@)>,nCWX.GJce26s:lk^ZG-g,2Ws[=9+r"BD*r+71S#1%:/)@%Iob+%7)[?Cc[lE4tQISon[aSa<<;%HMQdB*,I@^`Yf=$*Gr\\Q]^<N\cer>q^ZA(#j;OE4%1ndiY'XK%jioG6f&:.icq*OA]AK/H9ANJm)`#QTL"8s:,AQ*MLhC;4reCF9S@`f_q(a:@AqZmuL%YG<PN+=GC;skR/"FSAMBrV!No.N>TObS=,Xs6Y-gQ0?o)C4q+iA]VS&dr)Y[H`)#^2fePET;:2D.-4(/N<''?#*g`Ri=:h5mY!;X>"GsZT(Z1-NL)N1o1CP80\a(o'VTc%5f?Ph;?U5Z>jB.)2a,oLG=;\b61C);cb`PlP-]g)r'o)WgCqXkmD'C@Zd3X9(UJ:^i]SF0/uZWQW-3):ck&hbpD5fJ8Q$Y*6@HCkb`<dD2oLdJJ<rm34$jOiN?<dY@,NkTCf^ogXZ.66[Fd$::E'&TjMN0ecKCcH<6IlQdfp-2VVCAUACPCF"*_Y'u!03LDRH&_%+G0$amcW9kY'h<A1X>LpEB0XiO06&:o'H^X4H:QXt@WUX!3R/ga('DVD#_F>uk=%7tV]6f\&PSBW$dmD,el-f@)Oi[DsIl1V$!n$)p_N&U#YgH8.oYH:3JLOEL,;7J:$*7Jr?XnPiZ*>JgjbTONs:2F[o?uQfn.e6eOpU(1""c=SfIC\,RO*2j!4'RPhh+61rPRGPYg?;ci"V6k&=uJo"aCD6!hlU9FCQX)aD69t$D..0R&s"i?#V&^a1_URY]WU56d7*VMDn]`@BGuAUUu"3C'E==;NQ3jmcGED!Klq''`l.ZK#B%1KOLae^,u'V_+MrW.ADMXfB1Wk)JrL!/l$EBoL:Re@5)co"O;5j.I]f.n8%4G<Q6hj3<"Y+CIlLu9.h;!M@9*(%n8<Ki(sHTk]/*7gW6d7WFJ76sh)nZ3X8SSWs$#H+\-%^d[iH`V5Kb7Oa_-Wdp)NNff=MB<'gnOD,i^OR]KFu/co'f[+C2M@2M6mD!H-Z<,0*gZk'thM&1I~>endstream
|
||||
endobj
|
||||
17 0 obj
|
||||
<<
|
||||
/Filter [ /ASCII85Decode /FlateDecode ] /Length 2659
|
||||
>>
|
||||
stream
|
||||
Gatm<?'!]1(4FN4.#epp?I9gD@8-6VlqCG3AS8@(]Ig0]@2D`Xj!%LbB%UJ*kE]th_1WS-obTIHEntf^^%Yb:bf<fCIV2m)50_d,T*#j^Cqa@Em)<f?L&F&^oDO[G,\3l5E?G!_fR/?0os\8VU7g0o9G9'MdtgGtk6I2"0&*\Z=2#G$H=6>_D#P6^g2.e6PW#(X8[E&BT1S#gZZ$\ae=V'O1j7XOUA;s;72SeQ8nk0AM\LNH4KqLZV%H/Z#62WpgV&d"Mioc)du!MsZXefPlc=Z&%"Se*b6$HVMK,^\_(_R(;>>t%`U<so2@(r7l89OBdUX<"f>95RiKj!c%V;%Zfr?M/O8[5-%GPl>&$/`2Y,#8MR)1N$e;Eee^j)Gd@1:%K,p9N5XfD'6Xa@KV0%E&4#3jS[7m!P!0>(WPK<^3ffWW?CLF&rtq>#(?D);lEA;qf$Z"IB^I<b[Z<dVGeIoW>qE`lPEb+$FC_!`!fib<g>gjA0ZP40MFE4WhpmM,*UbpB-G]3R_4mfBt<W."qB\^9dR'AHfd%U/tZTIj$DTiq+bQC6G6W3,!D3)bpnFsj@>1[NYe,OhEMS`12K9eKa?TlqV-%FDf&C:2W?lS[idKed,P<*GT'-P)We*@DCALtq7m1(GSp(3"1#6UF%$dc/'Q\_MGb%2.D;SE>-1pA?1-V_`PX>%<LI"!n<f-!e6f$*N/W?/N4Qe1d,a53u`hS/`Mnh^eN>$Nfbua2J/@9D-^t_h`3:j@E]^$<@^WCaK)aF@`3G8sTV*_QE\!&rdhkliH\+io,'d,X,5#6$5X/C"&OSEDU.gLGVAZbn8'/+J0PT"pD4KnJ@muN"2RX1*Lb6<YmhL16E5d%=DY0L-npU/V#A8WPCr0$'TJj;lKpDUcgeaa=b2#,:*OiqsWEls3Fo49[*pL%RJX8+s!8]NQA.e+Cg-CXY1T#O3+tHmZ`i7^'L%JeI&ilAnQ^!$G.+K4Nm8a(ilE4PuIhQH\H8MR^Gu!m+g1<,S%$BVlait.*$f#hcnp2>!bA)!):IkXJ.J8%hQ3;/58e?PiY-05SS\&?>T,7U/62?:*6/"S/&k3)T7*aG^ojA>C,YX6CjAGK>OlbjV5)3XhO9N=@18cY%[]+)6@/'A@^.D=C#LHdULG7I/,j.<r#p$1M-h#'0D\'?d-X9T15@ma\9+;=-0.t&?)#,^%cY=%2W3X":T-n!%:dtaJLJh[+_+.$t]F-aP]ud]B0ieMC9u+k!`^G>LusB1KG+E&Q<7\GYO$PPPA;`V3ZrNjbEioh@^P6FjGQFWUYE;QG'XE,^(kM3H^b$0+%23QPn?>8QrgK>YrC;&.?4uPHmu"PfC$P4Z8:CbJgFJhfr90MuS_RcSF\!N($(C%TI(c7S1KRX)qV\=Y;HdOJ\#"1sg@@P\(j\1XY0X&c+=Z-V5o^V\O#R'8/2Z=S]I.3_2,3i(ZtL2>#eSdYcn%6cE1F/%7Ji6sVC8FH@QWU\)Bi6K1`*ecKE5A55Os>\+Y(Cg(-h^Yo>6NRlJ+$($agM)BLb'O^i?B[r@6)@FF4.l-:8F@g_2C*CP<2qE3hV,;W<C*s*`nP[B;K%c=@N5H)G#,H(cqd[d300a'U(2IUqkfM8\VS)^1p1JA;jrR>9LM$=]`PNQ9$?i7/!hHB,=boSG>"TT'LU-VBMh_i8qGF@qCoL#t`dDGm:fZ)t1oK^ZO4i]plW8)AC8]>r,B@6>p!tSO2<>(C]1-<m8tcCm;1*Qk8j)gPcX\h.&T^#dF#*83g6V%K\UY&1?:`rd6'9Kl$d=-.C^:B]E6u3Ze^sOMqui3dp1hBcE8n%C?TW&2\91TriYHi:cEt]8J2?jn-&!O\13OtqBTL/9/]VOb&g1$b:83]6AArKoX`9WHW'[0U;'0]3_o7S]1OZG,+LhZ,*t4W3O*rU)<`efIM-iD7paV7MZ[4R@U`"6&Rd8%9_s7m5X;$#9VLhA\8SiPo!t2%<k5EY'qF"6pW$H/dh^CH'=k$(#nri>hIS/?B"S5b&h]`8:5-Wjjg0@FP[B>:WL0%6]$C^C]g2+>!6Wdk30$8*R*(F!OVUVG)o\sSJLA*^`,n3G4:n:"Y(%d;MAnG_FSf]I=U6;@l&!90eYf;cjjA$W65h-eeX7O(JT,o$,"FgsP&;DZLSO\>$isFTqa_d5AgFZ.]kiL65<;*cWU?,;WDN@(B8)k\KGCD$iIJUGNi+u%l!"RY6l<i$=<J:c/8m^/fD'">jk*$W8S>AM,]p$NN.'VSC!Mf`)J\DcV#*ɲp=S%CeoYh+cTuH>]4lScAA%`"_?0VK$H4NXh45cacNDafI0Vdk2T=`=ZcMT2MT@NK994\$$]/Ars4e3WZoUIr:K=e&a.(FmIWYG$6urKMINa*(tMM.H/fi+^J`t(D$IYBscbg=S:73?k&8aOJ$!*[\iK#kFm$Y8O^S16>TNiIshZ\1$,CAU*pRTKu9Vt@s557&*tBb*8bSnmcKp8pT,a5>kb$k1/#Ns4DB3)0k=-8/_:qW>+m!M7M7CeLE)NG(_Gk>m,f%%5%[FsJ`2#g/Ek]n2JDYMR&7T>.1/LRq5)RC0:dfr5B1MK_q/?3q#9%!dp1-]bQ"M^O347]31]*cdoVd`$lXXd'aIa%-0%bgk[f#kg1Ci+F;3%f^Yn)ZdBGS0"8m9GqZ~>endstream
|
||||
endobj
|
||||
18 0 obj
|
||||
<<
|
||||
/Filter [ /ASCII85Decode /FlateDecode ] /Length 2487
|
||||
>>
|
||||
stream
|
||||
Gatm<_/e:"'#"/mqR6$^KEXUR"QE@cYu@Vo;Vm;>p7f_A(m&5<0Z_I'2#i#3%fd5=CeLU&fd0JuB$f#qp$:4#.)s"Ir%&e]TC1oM*th1D\uR:56lW`7NS`R+q<LbbP#<?<_kf=ba%NpTphp[j;_md.k#mr0q3om(-r.34/8Jql;&giuhU:4-])*kY%bn-8k=pj_)u#liZd#5G8;i/lGSfJR:"\T@.r%lSBtC(n=L.K`aV4'f<1:OD;U6ER+2gA1Q0$CS92jQH\(EMfE+E45OuOnrKHC/"SgUE.R\.hr_!/=<K!lY=[+ul\Hp#ek';Z<>,g+F<Mc_J*ocolnr)(n7^9<cpqc+m>d8WT)_tf%KOOfV^9rE]3mY:6.]JnuZ]75`k2+"]g[eq5-3bpcjrAg<A)=rKE.*]'1-peJh\GlXq??1'YW?.i[6*4j>jd/m>%U\"Q2.rW]q6m_0P#eDIlGsq0h'L_PocO^\ZV"n$-fpDhje*A$X?KhZ8X;P*_@^Q`bN\NV[:k-8=0[VB(LWF0?@E!IZm^f;0qu"7kf<D@#<9q).BV\'PNubYC/kVCc^08XW>pAA<RO&R@1HsO;BqZ3m'UBF'GBf'3jc1Z/sclJm+%Mi3cT$Sr\7!=lMq%U7(&9Il""A;QR$?0j_BE7+*//HDm^2)'Hi5*^@fW!l(/;n]A("AN*Z,tbguT5\&eiu7$L(rVfX0/`&.P5k2aBl;"\cQd@NqQK?tn>#j3qWb#3LAJ1[Mq*Mo#oCu+cW!NdcndVPCYJ@jCi#b#LD/"F(sl$J>/RR[Dgdd@ao@_!oelBPrWoa"==K>:K.C)9b&`_hnp=YY`UGT;9T]P\rFQPpYs6kLe,*E@AiS;m-jW'eXmWU#@h%t>u]OLaYhBM6Ik<@u._!cu+ShEX'HiZX>=rXo*qC"uD:h98O7*Do5+<Y)3n+_GX]lY8$qZ\\(!KgJ_p;'ibn&8!dDLP!AiO+_Ban((u3QbE!f)-"o6$M%>gedETs8M?=J(RGBe><_Qhk;Du28=2q1i:]un@rT=7L@sLRb??OC`'EC`2."gG#%J"]:&C8Id2mlJi`lEB)&g"Zo-en/rflQDVsRGXBMe04+:B0m*,Bp/pBlb9%M;HsKJjhePa)KT6.lBar$%+`-p[i.C*ue^:CJ#KfXITXO]'uE5;+IMmui&d%LVRd`rfKU(Xl4<QPs"m<mXg]@e:+B)MjL.CNToIqdC'plJ%tn`u8WEr@Im[\*L2,Vjg*Rfc=g+]Ek'HMs`d'T7n)4C&^).NH,%L6A,;6#Eu8s&Bsn&AJ2-*360U$/UQ(*pB&J'"?Zp;QEhJ-LqsU&/2e5K%m3I=c.HV>Q?%/1Rp[,NWo!E1j\j_9H#%`P%9.$5hJ1/seZe'C[]F!e>M]Vp\WRJ$U[BV!(WlLi3(@)Amb^21\/1F\GHF<((R$0i<U$$nXF-+#B&ceK7EI14#IsM#CG*M^nK01'3nRU.+NcK_#"^elZ2?ZX6oSto9\./]Y_t;@,$!K)Uj$]YT$Gd\Q0GuAkRU%qoE#rtV0keJWCH\KP,Df8F`q\t4A"Q"_!^T#/4RQ-,@+-5_\=]$e[HYcW!XPUH]D5r[QU'2\#^@Zg<]D,EeTUka5*J4]k56s"mi^ZXTMM<9"oq)+S2-Q!;TMXd^5&@,eqF+kPRIFbZC]i!mY_i;8fq20.CHk\:0AD%N1K9W3^#1JaZ@,Ghm/&"G\O-3:b>B&:*:Z$!IWL"1t&EA^C5U>/2neRRn2EG1c/*FT!G)o`;K=8u3j>6YgH3&f2&?TPANGV&$(V3dp6Cf0T\58DCe0bTDi;:Hs>m^[@c+M2NW"Yfd7A0=#CK4V%k2p#\/[(q#j`?^7W1`-oiYN2$h7@\bDU.#_O9B^n6MrD-SAbC$[8EOm[[_,mO3ErYiF1;_+k)U':n'oe,c(QI*W4lboKIcC;5_Wl*rTp]R.N3GDtF8Uob%khnjk(U/&cR1Nne\f(>$nr/jSkD&Y4ff-rio/g6XfQ#UCMci7n&KgE6qL7gWgc?[MfbE[1TnN#C\T-0!>^e&R:6\MnZu4;]ZtcI;KSX$R4YL$%g\^Aj`2X+nUPc'fU3D@5g2..H3!I\Ph3-oUPdV"Z@EDEX<u?dHlkiC:n*[1](j4Z-XbZ;iOW^45mA2QVj\+]TZTn-.u?QDqgWhu1m')Dd]88s3.6790hASd+nX#7h[O/BA:@:?/&DjiDhl@dSrolILa4J@FId88l:2W6RIT[#3L6iWcQs*LRSiXUO[FiG!!W`OT]$YC9e\a$h"j[7?-m<qe\,N5\9.Q$@[e)-d.`R;*r*E>7,a`!X>0"j+7'uf@JU&Z70T!?1TnH`H1_8fKe9%>oS!IdqjRs`^s#=H?#S_:\7^`qKo*1*Z&!$1m_T@bj4R>(q7D>ri.PF3Iaki#H[UtI3/"#nqWP>r:N+p"%o^]lU]0#.6g]hA#g(QS2:O=kZSYui:?U<s=_/K4AIY2,fNo'JCFL@W$,-.!43h[s~>endstream
|
||||
endobj
|
||||
19 0 obj
|
||||
<<
|
||||
/Filter [ /ASCII85Decode /FlateDecode ] /Length 2420
|
||||
>>
|
||||
stream
|
||||
Gb!;d>B;!t&q8_Fd185PS?g1V^YkH/_2/gE4,0l-;oZ3W1BOp)7RRm/r;4skb[=O?9_OuUH.Q9o^dtTqYJ7fn!?6M.o/iKDZj1PriIKt*?jr'6q(T=1mbl!T+3YZ?@Oj.9Cs/qeiL9>PR-6t6-..]>bDB^>4Q!H8+MqtOB\\aWL#<4@T4.5AhJL2W&8Rtr'im(Kj92.k->qg.Ce%MGq*n%JOKjD2lUESj!X*p\F:"]WUjQ$1RVs28)sUbQ$=h6e>o+\<5bVV<-KA=RZ(3.^i6/mip=S;KqJ6TB=s$]D"^5EfEe;rKnTbG#aDrm-39LGn.V>b)MXdDI,Yb8&TK)So,%DTp0c!/,Yt3D3:nU6(JLF>no*g"_*`J6^-R)s<$T5mtYai"@=rV/@,HW55j!ZX[Dk%,k4AlK-4Jo)=1X[leSr=#$Ui_WpjZ2*'rY](\jXgl[I=0H^rSc-AgsIKQ/1C.g"P(D.7J2A-.:l:3gX;cMU?DopBeqB3&QVHD(A&#+Yl_X9;NW/5`<7ejO_:+KS)gA9l$DliXN2ff)qqff2a"CS>e8C`p/a9)$;.Mu8G^K0L4tW>JtMfO'nt64MCd)H_rQ3=W-hO/0X<W:ffIH9b6+m!NT)qId+']$ACc5&[:5."K.3#2RKuMuZqu8p-:V-+'Z3353[[R'ZcHfrCol\VqMH]$?iRk"kcdXdi^JL>0eA`]U<Cn@'-]2+h38`9S>GM(O&*.\D$l(f:?C5j$=1\mG,%ujR-"4ZS]"lj%0g2,-]h8$nI^I0;DQR"BSkgG;i/n;9Lc+Z-4qK"K]2E9=HJm_(El03AR$3jp2WStH',Fb_HcSp;CtN_Db"Q(3-D$?;$`@aoM[JI?s4Nk5oH3H@0Xm.H`\/#/0tJ,?5/$XPWWO>fn*QHfu3AF7m68_9C8r$=eYB?5/K`f+97d"F@9TP'M>!]o%BN_/`Q@g0UXX/HZ(tZ%GGd`ruQ<>"kQGF1e1Xr?b@CpJK'#k!e+*an2d%CDA38>P7emOh*NhiD5<1M9P4&IduVpfAAOMmNF\5&r^IlKopc<,0HdU&+XtfGpfd-#DP^h0Uk5XaR`%s;-`%$cls%N)hHC\Vn"gLaC%Q]Hf#Xoc@)*t#aEYpL,RW(ZGS]q]@T\MpRQBQmc$H9?Vd!n\=MP$j0nIu=B$lYgbH91WCotFN_=udOf&bG75/=Y#O**dN&K[rk@DQADYZuu?;k/tu)AdZ'1roMHGem8[*77dUMhFuo0&1gS&++=Cau(cgM/>8Ums4>E<q-^C.fGuh]t&cr<kAlD=6I29JjuHIC-jGKcWDnDF\]Fn"ds%1>gqB)L8nUMg>%g@:?C_pKRN$*d$IJ:n$E;TLFp14K&c0'ln?0M]6IT9I-c!eGP`*@@<eGs^".?#iK_rcb(#Zkq"1G!r?2$QVO2fnV&ADs>\93>@.7-4d+c-JFcICGqB_<%H+(t+VEb&1'\DlPS*5L+En7/$c<?q(5_^;fi"JCg:%2HT>"8'"_f!UOakpI_oEIOtJ%Dbo2/Q*g?2O#3U>=%=:\L4][O=^kH[#'SpNusqHj?/*-1UkVX"cE*1)4V3c4p"3]e3PqpAU)KiEcT;Xm&&fm)q\AmBu:/nq1tUWYg_,$4B?;7?gJpJ:#1FCrkb+?!jH4V_#'47's08<_ZOR5N.B?YlQbc96SkLeVUY&Yu/V'#-?ue/=E<G6l5JuI5.Z0O5<PV=Gsls:UjHdHElWP='TOqY`HMk(i/"T8TMJ8f5sBJ4df0M)F-,OL_(kHeI/3i?M:N+U,-V0kCdF!9^08Vme-W`."7?,0mZs25gURBj&%csQBF/(7/$].h'De35dtP_E"gQ*^-.70ZM=IsiEmMXT*1L$4fJr2.B]'U5,4<D"giq/Or$9WJ3N;06lPf9pi8=cA`[79CGu85?p%cRoQQ7ia.<c[S515@FpR[/SuM27Ut@Sfe;S<X(+SC3%.hR0#[+@CQd[^Z$l_UD\eA7[!j>)+\deJZRD(k0J`k;Um"Tkn_E3=J:N@X[-V1os6?%tY$8u(P(Hs-*5CH_AF(nJ)7.>.@gi"H;PJHmm8sPU)Q6Y6L1P<,8jfGTU?\.`&Y?+7SeRK]6/:6"PC1IMr5uY$:B?N$+qN@`dE5hRIb@8EuH:U"Vml_Y/ai[k&#TS1KX/[VVO*$ji7'E%EE=D!Q!cTHGRtp`(:?_(T=+/El%g0'93k<ZZ>OMQuAkB,IHH`j#Cf>5OD'nn7o!%Yhh`3p??\;j0)idm[3;@.@2n56eG#AO"r"(Bk\VY/#@6Z,Z65'ul=rlAC>,*WB1OIokYd972eF#BVk^6obZIo*X%kaeaEq+t23PXfa>i&aRY<2OXk3giAYbK^eg:mT%FEJZo(UMS6s8:gURQ>oec=pHhKH/thm$72X\1T$#eI2oQ-.(q!m^iAr4q>JcrrNUjQNI~>endstream
|
||||
endobj
|
||||
xref
|
||||
0 20
|
||||
0000000000 65535 f
|
||||
0000000061 00000 n
|
||||
0000000132 00000 n
|
||||
0000000239 00000 n
|
||||
0000000348 00000 n
|
||||
0000000458 00000 n
|
||||
0000000566 00000 n
|
||||
0000000771 00000 n
|
||||
0000000885 00000 n
|
||||
0000001090 00000 n
|
||||
0000001295 00000 n
|
||||
0000001501 00000 n
|
||||
0000001707 00000 n
|
||||
0000001777 00000 n
|
||||
0000002088 00000 n
|
||||
0000002174 00000 n
|
||||
0000005052 00000 n
|
||||
0000008159 00000 n
|
||||
0000010910 00000 n
|
||||
0000013489 00000 n
|
||||
trailer
|
||||
<<
|
||||
/ID
|
||||
[<e39ac81b70f6b5dd3bd1a9be5a60bf19><e39ac81b70f6b5dd3bd1a9be5a60bf19>]
|
||||
% ReportLab generated PDF document -- digest (opensource)
|
||||
|
||||
/Info 13 0 R
|
||||
/Root 12 0 R
|
||||
/Size 20
|
||||
>>
|
||||
startxref
|
||||
16001
|
||||
%%EOF
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 141 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 135 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 954 KiB |
|
|
@ -1,444 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="ru">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>42 — Друзья Коллективного Разума</title>
|
||||
<meta name="description" content="Кооператив «Друзья Коллективного Разума» — мы строим автономные компании на AI">
|
||||
<style>
|
||||
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800;900&family=JetBrains+Mono:wght@400;500&display=swap');
|
||||
|
||||
:root {
|
||||
--paper: #faf8f3;
|
||||
--surface: #ffffff;
|
||||
--ink: #1a1a18;
|
||||
--ink2: #1a1a18;
|
||||
--mut: #55534b;
|
||||
--dim: #8a877b;
|
||||
--vio: #5b4be0;
|
||||
--line: rgba(26,26,24,.12);
|
||||
--line2: rgba(26,26,24,.07);
|
||||
}
|
||||
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
html { scroll-behavior: smooth; }
|
||||
body {
|
||||
font-family: 'Inter', -apple-system, sans-serif;
|
||||
background: var(--paper);
|
||||
color: var(--ink);
|
||||
line-height: 1.6;
|
||||
overflow-x: hidden;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
::selection { background: var(--vio); color: #fff; }
|
||||
|
||||
.wrap { max-width: 1200px; margin: 0 auto; padding: 0 32px; }
|
||||
.mono { font-family: 'JetBrains Mono', monospace; }
|
||||
|
||||
/* ── nav ── */
|
||||
nav {
|
||||
position: fixed; top: 0; width: 100%; z-index: 100;
|
||||
background: rgba(250,248,243,.92);
|
||||
backdrop-filter: blur(12px);
|
||||
border-bottom: 1px solid var(--line);
|
||||
}
|
||||
nav .wrap { display: flex; justify-content: space-between; align-items: center; height: 64px; }
|
||||
.logo { font-size: 1.5rem; font-weight: 900; letter-spacing: -.04em; color: var(--ink); }
|
||||
.logo em { font-style: normal; color: var(--vio); }
|
||||
.logo span { color: var(--mut); font-weight: 500; font-size: .95rem; margin-left: 10px; }
|
||||
.nav-links { display: flex; gap: 32px; align-items: center; }
|
||||
.nav-links a {
|
||||
color: var(--mut); text-decoration: none; font-size: .85rem; font-weight: 600;
|
||||
transition: color .15s;
|
||||
}
|
||||
.nav-links a:hover { color: var(--ink); }
|
||||
.lang-switch {
|
||||
font-family: 'JetBrains Mono', monospace; font-size: .75rem;
|
||||
padding: 4px 10px; border: 1px solid var(--line); color: var(--mut);
|
||||
text-decoration: none; letter-spacing: 1px;
|
||||
}
|
||||
.lang-switch:hover { border-color: var(--ink); color: var(--ink); }
|
||||
.btn-portal {
|
||||
padding: 9px 22px; border: 1px solid var(--ink); border-radius: 0;
|
||||
font-size: .85rem; font-weight: 600;
|
||||
background: var(--ink); color: var(--paper);
|
||||
transition: background .15s, color .15s;
|
||||
}
|
||||
.btn-portal:hover { background: var(--vio); border-color: var(--vio); }
|
||||
|
||||
/* ── hero ── */
|
||||
.hero {
|
||||
min-height: 100svh; display: flex; flex-direction: column;
|
||||
justify-content: center;
|
||||
padding: 140px 0 80px;
|
||||
border-bottom: 1px solid var(--line);
|
||||
position: relative;
|
||||
}
|
||||
.hero-grid { display: grid; grid-template-columns: 1.15fr .85fr; gap: 64px; align-items: start; }
|
||||
.hero h1 {
|
||||
font-size: clamp(2.6rem, 6vw, 5rem); font-weight: 900; line-height: 1.02;
|
||||
letter-spacing: -.035em; margin: 24px 0 28px;
|
||||
}
|
||||
.hero h1 em { font-style: normal; color: var(--vio); }
|
||||
.hero .sub { font-size: 1.15rem; color: var(--mut); max-width: 560px; margin-bottom: 36px; }
|
||||
.hero-cta { display: flex; gap: 16px; flex-wrap: wrap; }
|
||||
.btn-lg {
|
||||
display: inline-block; padding: 16px 32px;
|
||||
border: 1px solid var(--ink); background: var(--ink); color: var(--paper);
|
||||
font-weight: 700; font-size: .95rem; text-decoration: none;
|
||||
transition: background .15s, border-color .15s;
|
||||
}
|
||||
.btn-lg:hover { background: var(--vio); border-color: var(--vio); }
|
||||
.btn-ghost {
|
||||
display: inline-block; padding: 16px 32px;
|
||||
border: 1px solid var(--line); background: transparent; color: var(--ink);
|
||||
font-weight: 600; font-size: .95rem; text-decoration: none;
|
||||
transition: border-color .15s;
|
||||
}
|
||||
.btn-ghost:hover { border-color: var(--ink); }
|
||||
|
||||
/* hero facts panel */
|
||||
.facts {
|
||||
border: 1px solid var(--line); background: var(--surface);
|
||||
}
|
||||
.facts .hd {
|
||||
display: flex; justify-content: space-between; align-items: center;
|
||||
padding: 14px 20px; border-bottom: 1px solid var(--line);
|
||||
font-family: 'JetBrains Mono', monospace; font-size: 11px;
|
||||
letter-spacing: 1.5px; text-transform: uppercase; color: var(--dim);
|
||||
}
|
||||
.facts .hd b { color: var(--vio); font-weight: 500; display: flex; align-items: center; gap: 7px; }
|
||||
.dot { width: 7px; height: 7px; border-radius: 50%; background: var(--vio); animation: pulse 2s infinite; }
|
||||
@keyframes pulse { 0%,100% { opacity: 1; } 50% { opacity: .3; } }
|
||||
.facts .row {
|
||||
display: flex; justify-content: space-between; align-items: baseline;
|
||||
padding: 15px 20px; border-bottom: 1px solid var(--line2);
|
||||
}
|
||||
.facts .row:last-child { border-bottom: none; }
|
||||
.facts .k { font-size: .8rem; color: var(--mut); }
|
||||
.facts .v { font-size: 1.1rem; font-weight: 700; letter-spacing: -.02em; }
|
||||
.facts .v em { font-style: normal; color: var(--vio); }
|
||||
|
||||
/* section scaffolding */
|
||||
section { padding: 96px 0; }
|
||||
.sec-label {
|
||||
font-family: 'JetBrains Mono', monospace; font-size: 11px; letter-spacing: 2px;
|
||||
text-transform: uppercase; color: var(--vio);
|
||||
display: flex; align-items: center; gap: 12px; margin-bottom: 20px;
|
||||
}
|
||||
.sec-label::after { content: ''; flex: 1; height: 1px; background: var(--line); }
|
||||
.sec-h { font-size: clamp(1.9rem, 4vw, 3rem); font-weight: 900; letter-spacing: -.03em; line-height: 1.05; margin-bottom: 16px; }
|
||||
.sec-desc { font-size: 1.05rem; color: var(--mut); max-width: 640px; margin-bottom: 48px; }
|
||||
|
||||
/* ── mission ── */
|
||||
.mission { border-top: 1px solid var(--line); }
|
||||
.mission-grid { display: grid; grid-template-columns: .4fr 1fr; gap: 48px; align-items: start; }
|
||||
.mission-num {
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
font-size: clamp(4rem, 10vw, 7.5rem); font-weight: 500; line-height: .9;
|
||||
color: var(--vio);
|
||||
}
|
||||
.mission p { font-size: 1.25rem; line-height: 1.65; color: var(--ink); max-width: 720px; }
|
||||
.mission p + p { margin-top: 16px; }
|
||||
.mission .mono-note {
|
||||
margin-top: 24px; font-family: 'JetBrains Mono', monospace;
|
||||
font-size: .8rem; color: dim;
|
||||
}
|
||||
.mission .mono-note { color: var(--dim); }
|
||||
|
||||
/* ── board: spec sheet rows ── */
|
||||
.board .rows { border-top: 1px solid var(--line); }
|
||||
.brow {
|
||||
display: grid; grid-template-columns: 64px 1.1fr 1fr 2fr;
|
||||
gap: 24px; padding: 28px 0; border-bottom: 1px solid var(--line);
|
||||
align-items: baseline;
|
||||
}
|
||||
.brow .idx { font-family: 'JetBrains Mono', monospace; color: var(--dim); font-size: .85rem; }
|
||||
.brow .nm { font-size: 1.25rem; font-weight: 800; letter-spacing: -.02em; }
|
||||
.brow .rl { font-family: 'JetBrains Mono', monospace; font-size: .75rem; text-transform: uppercase; letter-spacing: 1px; color: var(--vio); }
|
||||
.brow .ds { color: var(--mut); font-size: .95rem; }
|
||||
|
||||
/* ── projects: ledger grid ── */
|
||||
.pgrid { display: grid; grid-template-columns: repeat(3, 1fr); border-top: 1px solid var(--line); border-left: 1px solid var(--line); }
|
||||
.pcard {
|
||||
position: relative; padding: 32px; min-height: 220px;
|
||||
border-right: 1px solid var(--line); border-bottom: 1px solid var(--line);
|
||||
background: var(--surface); text-decoration: none; color: inherit;
|
||||
display: flex; flex-direction: column; gap: 10px;
|
||||
transition: background .15s;
|
||||
}
|
||||
.pcard:hover { background: #f4f1e8; }
|
||||
.pcard .pnum { font-family: 'JetBrains Mono', monospace; font-size: .75rem; color: var(--dim); }
|
||||
.pcard .pname { font-size: 1.3rem; font-weight: 800; letter-spacing: -.02em; }
|
||||
.pcard .pown { font-family: 'JetBrains Mono', monospace; font-size: .72rem; color: var(--vio); text-transform: uppercase; letter-spacing: 1px; }
|
||||
.pcard .pdesc { font-size: .88rem; color: var(--mut); line-height: 1.55; flex: 1; }
|
||||
.pcard .plink { font-size: .8rem; font-weight: 600; color: var(--ink); }
|
||||
.pcard .plink::after { content: ' →'; color: var(--vio); }
|
||||
.pcard .arrow-corner {
|
||||
position: absolute; top: 18px; right: 18px; opacity: 0;
|
||||
transition: opacity .15s; font-size: 1.1rem; color: var(--vio);
|
||||
}
|
||||
.pcard:hover .arrow-corner { opacity: 1; }
|
||||
|
||||
/* ── CTA: ink block ── */
|
||||
.cta { background: var(--ink); color: var(--paper); padding: 110px 0; }
|
||||
.cta .sec-label { color: #b9b6a8; }
|
||||
.cta .sec-label::after { background: rgba(250,248,243,.2); }
|
||||
.cta h2 { font-size: clamp(2.2rem, 5vw, 3.8rem); font-weight: 900; letter-spacing: -.03em; line-height: 1.05; margin-bottom: 20px; }
|
||||
.cta p { color: #b9b6a8; font-size: 1.1rem; margin-bottom: 40px; }
|
||||
.cta .btn-lg { background: var(--paper); color: var(--ink); border-color: var(--paper); }
|
||||
.cta .btn-lg:hover { background: var(--vio); color: #fff; border-color: var(--vio); }
|
||||
|
||||
/* ── footer ── */
|
||||
footer { padding: 36px 0; border-top: 1px solid var(--line); }
|
||||
footer .wrap {
|
||||
display: flex; justify-content: space-between; align-items: center;
|
||||
flex-wrap: wrap; gap: 12px;
|
||||
}
|
||||
footer .f-links { display: flex; gap: 24px; flex-wrap: wrap; }
|
||||
footer a { color: var(--mut); text-decoration: none; font-size: .85rem; }
|
||||
footer a:hover { color: var(--ink); }
|
||||
|
||||
/* ── payment logos strip ── */
|
||||
.pay-strip { margin-top: 28px; padding-top: 24px; border-top: 1px solid var(--line); display: flex; flex-wrap: wrap; gap: 14px; align-items: center; }
|
||||
.pay-strip .pl { height: 30px; display: flex; align-items: center; }
|
||||
.pay-strip .pl img { max-height: 30px; width: auto; opacity: .9; }
|
||||
.pay-strip .pl span { font-size: .78rem; color: var(--mut); letter-spacing: .02em; }
|
||||
|
||||
/* ── requisites block ── */
|
||||
.requisites { font-size: .85rem; color: var(--mut); line-height: 1.7; margin-top: 22px; }
|
||||
.requisites b { color: var(--ink); font-weight: 600; }
|
||||
|
||||
/* ── legal/compliance pages ── */
|
||||
.legal-page .wrap { max-width: 860px; }
|
||||
.legal-page h1 { font-size: clamp(1.8rem, 4vw, 2.6rem); margin: 18px 0 8px; }
|
||||
.legal-page .upd { color: var(--mut); font-size: .85rem; margin-bottom: 28px; }
|
||||
.legal-page h2 { font-size: 1.15rem; margin: 30px 0 10px; }
|
||||
.legal-page p, .legal-page li { color: var(--ink); font-size: .95rem; line-height: 1.75; }
|
||||
.legal-page ul { padding-left: 22px; margin: 10px 0; }
|
||||
.legal-page table { width: 100%; border-collapse: collapse; margin: 16px 0; font-size: .9rem; }
|
||||
.legal-page th { text-align: left; padding: 10px 12px; border-bottom: 2px solid var(--ink); font-size: .8rem; text-transform: uppercase; letter-spacing: .05em; color: var(--mut); }
|
||||
.legal-page td { padding: 10px 12px; border-bottom: 1px solid var(--line); vertical-align: top; }
|
||||
.legal-page .price-by { font-weight: 700; white-space: nowrap; }
|
||||
footer .cop { color: var(--dim); font-size: .8rem; font-family: 'JetBrains Mono', monospace; }
|
||||
|
||||
/* reveal animation */
|
||||
.rv { opacity: 0; transform: translateY(16px); transition: opacity .6s ease, transform .6s ease; }
|
||||
.rv.vis { opacity: 1; transform: none; }
|
||||
|
||||
/* Mobile */
|
||||
@media (max-width: 900px) {
|
||||
.hero-grid { grid-template-columns: 1fr; }
|
||||
.mission-grid { grid-template-columns: 1fr; gap: 24px; }
|
||||
.pgrid { grid-template-columns: 1fr; }
|
||||
.brow { grid-template-columns: 1fr 2fr; row-gap: 4px; }
|
||||
.brow .idx { display: none; }
|
||||
.nav-links a:not(.btn-portal) { display: none; }
|
||||
section { padding: 64px 0; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<nav>
|
||||
<div class="wrap">
|
||||
<div class="logo">4<em>2</em><span>Друзья Коллективного Разума</span></div>
|
||||
<div class="nav-links">
|
||||
<a href="#mission">Миссия</a>
|
||||
<a href="#board">Правление</a>
|
||||
<a href="#projects">Проекты</a>
|
||||
<a href="/en" class="lang-switch" hreflang="en" title="English version">EN</a>
|
||||
<a href="/cabinet" class="btn-portal" id="portalBtn">Войти →</a>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<!-- Hero -->
|
||||
<div class="hero">
|
||||
<div class="wrap hero-grid">
|
||||
<div>
|
||||
<div class="sec-label">Кооператив · с 16.07.2026 · Минск</div>
|
||||
<h1>Люди решают.<br>Агенты <em>работают.</em></h1>
|
||||
<p class="sub">Мы строим автоматизированные компании, где ИИ-агенты выполняют рутинные операции, а люди держат суждение и ответственность. Прозрачные процессы, измеримый результат.</p>
|
||||
<div class="hero-cta">
|
||||
<a href="/cabinet" class="btn-lg" id="heroBtn">Войти в портал</a>
|
||||
<a href="#projects" class="btn-ghost">Смотреть проекты</a>
|
||||
</div>
|
||||
</div>
|
||||
<aside class="facts">
|
||||
<div class="hd"><span>КОЛЛЕКТИВ / СЕЙЧАС</span><b><span class="dot"></span>WORKING</b></div>
|
||||
<div class="row"><span class="k">Членов правления</span><span class="v">3</span></div>
|
||||
<div class="row"><span class="k">Проектов в работе</span><span class="v">6</span></div>
|
||||
<div class="row"><span class="k">AI-агентов в флоте</span><span class="v">7<em>+</em></span></div>
|
||||
<div class="row"><span class="k">Серверов</span><span class="v">6</span></div>
|
||||
<div class="row"><span class="k">Формат</span><span class="v">Кооператив</span></div>
|
||||
<div class="row"><span class="k">Юрисдикция</span><span class="v">BY</span></div>
|
||||
</aside>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Mission -->
|
||||
<section id="mission" class="mission">
|
||||
<div class="wrap">
|
||||
<div class="sec-label">01 / Миссия</div>
|
||||
<div class="mission-grid rv">
|
||||
<div class="mission-num">42</div>
|
||||
<div>
|
||||
<p>Все миссии одинаково важны. 42 объединяет их все — это ответ на главный вопрос жизни, вселенной и всего такого.</p>
|
||||
<p>Каждый участник преследует свою миссию, а коллектив усиливает каждого. Нет неважных миссий и нет неважных людей.</p>
|
||||
<div class="mono-note">// статус: идём с 16.07.2026, набор — раз в квартал</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Board -->
|
||||
<section id="board" class="board">
|
||||
<div class="wrap">
|
||||
<div class="sec-label">02 / Правление</div>
|
||||
<h2 class="sec-h rv">Трое у штурвала</h2>
|
||||
<p class="sec-desc rv">Консенсус правления — механизм принятия решений кооператива.</p>
|
||||
<div class="rows rv">
|
||||
<div class="brow">
|
||||
<div class="idx">001</div>
|
||||
<div class="nm">Алекс Шкор</div>
|
||||
<div class="rl">Председатель</div>
|
||||
<div class="ds">Архитектор кооператива. Симбиот, Кибербанк, Domostroy. AI-инфраструктура и стратегия.</div>
|
||||
</div>
|
||||
<div class="brow">
|
||||
<div class="idx">002</div>
|
||||
<div class="nm">Влад Шемардинов</div>
|
||||
<div class="rl">Член правления</div>
|
||||
<div class="ds">Видеопродакшн и контент. ReelsUp: подкасты и видео превращаются в вирусные рилсы.</div>
|
||||
</div>
|
||||
<div class="brow">
|
||||
<div class="idx">003</div>
|
||||
<div class="nm">Юра Бокач</div>
|
||||
<div class="rl">Секретарь</div>
|
||||
<div class="ds">Монетикс, финансовая архитектура, документооборот. Залоговые системы и аналитика.</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Projects -->
|
||||
<section id="projects">
|
||||
<div class="wrap">
|
||||
<div class="sec-label">03 / Проекты</div>
|
||||
<h2 class="sec-h rv">Что мы строим</h2>
|
||||
<p class="sec-desc rv">Каждый проект — отдельный бизнес внутри кооператива. Доля кооператива — 50%.</p>
|
||||
<div class="pgrid rv">
|
||||
<a href="https://simbiot.cil.by" class="pcard">
|
||||
<div class="pnum">P-01</div>
|
||||
<div class="pname">Симбиот</div>
|
||||
<div class="pown">Alex Shkor</div>
|
||||
<div class="pdesc">Автопилот для бизнеса. Рой AI-агентов ведёт операции — маркетинг, финансы, поддержка. 24/7 без людей.</div>
|
||||
<div class="plink">simbiot.cil.by</div>
|
||||
<div class="arrow-corner">↗</div>
|
||||
</a>
|
||||
<a href="https://reelsup1.cil.by" class="pcard">
|
||||
<div class="pnum">P-02</div>
|
||||
<div class="pname">ReelsUp</div>
|
||||
<div class="pown">Vlad Shemardinov</div>
|
||||
<div class="pdesc">Твой подкаст уже содержит вирусные клипы. AI находит, нарезает и публикует короткие видео автоматически.</div>
|
||||
<div class="plink">reelsup1.cil.by</div>
|
||||
<div class="arrow-corner">↗</div>
|
||||
</a>
|
||||
<div class="pcard" aria-hidden="false">
|
||||
<div class="pnum">P-03</div>
|
||||
<div class="pname">Монетикс</div>
|
||||
<div class="pown">Yura Bokach</div>
|
||||
<div class="pdesc">Финансовые инструменты и аналитика на базе AI. Развитие человеческого потенциала через ИИ.</div>
|
||||
<div class="plink">В разработке</div>
|
||||
<div class="arrow-corner">·</div>
|
||||
</div>
|
||||
<div class="pcard" aria-hidden="false">
|
||||
<div class="pnum">P-04</div>
|
||||
<div class="pname">Кибербанк</div>
|
||||
<div class="pown">Alex Shkor</div>
|
||||
<div class="pdesc">Внутренняя система учёта и взаиморасчётов кооператива: финансовая модель проектов, скоринг, прозрачная отчётность для пайщиков.</div>
|
||||
<div class="plink">В разработке</div>
|
||||
<div class="arrow-corner">·</div>
|
||||
</div>
|
||||
<div class="pcard" aria-hidden="false">
|
||||
<div class="pnum">P-05</div>
|
||||
<div class="pname">Domostroy</div>
|
||||
<div class="pown">Alex Shkor</div>
|
||||
<div class="plink">В разработке</div>
|
||||
<div class="pdesc">ИИ-агент, который строит дома в любой точке мира. От проекта до реализации — полностью автоматизировано.</div>
|
||||
<div class="arrow-corner">·</div>
|
||||
</div>
|
||||
<div class="pcard" aria-hidden="false">
|
||||
<div class="pnum">P-06</div>
|
||||
<div class="pname">AI Corp</div>
|
||||
<div class="pown">Dmitry Siasitski</div>
|
||||
<div class="pdesc">Консалтинг по внедрению AI в компаниях. От аудита до развёртывания автономных агентов.</div>
|
||||
<div class="plink">В разработке</div>
|
||||
<div class="arrow-corner">·</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- CTA -->
|
||||
<section class="cta">
|
||||
<div class="wrap">
|
||||
<div class="sec-label">04 / Вход</div>
|
||||
<h2 class="rv">Готовы строить будущее?</h2>
|
||||
<p class="rv">Войдите в портал кооператива или свяжитесь с правлением.</p>
|
||||
<a href="/login" class="btn-lg rv" id="ctaBtn">Войти в портал</a>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<footer>
|
||||
<div class="wrap">
|
||||
<span class="cop">© 2026 Кооператив «42» · cil.by</span>
|
||||
<div class="f-links">
|
||||
<a href="/cabinet">Портал</a>
|
||||
<a href="/en">EN</a>
|
||||
<a href="/services">Услуги и цены</a>
|
||||
<a href="/terms">Правила оплаты и возврата</a>
|
||||
<a href="https://simbiot.cil.by">Симбиот</a>
|
||||
<a href="https://reelsup1.cil.by">ReelsUp</a>
|
||||
</div>
|
||||
<div class="requisites">
|
||||
<b>Потребительский кооператив «ДРУЗЬЯ КОЛЛЕКТИВНОГО РАЗУМА»</b><br>
|
||||
Свидетельство о государственной регистрации: дата регистрации 16.07.2026, состоит на учёте в ИМНС по Фрунзенскому району г. Минска (УНП 194018234)<br>
|
||||
Юридический адрес: 220078, Республика Беларусь, г. Минск, ул. Маршала Лосика, д. 53, оф. 385<br>
|
||||
УНП 194018234 · Тел.: +375 25 900 50 03 · alex.shkor@gmail.com<br>
|
||||
ЗАО «Альфа-Банк» · БИК ALFABY2X · Карт-счёт BY85ALFA30152K15980010270000 (BYN)
|
||||
</div>
|
||||
<div class="pay-strip">
|
||||
<span class="pl"><img src="/static/img/pay/visa.svg" alt="VISA" loading="lazy"></span>
|
||||
<span class="pl"><img src="/static/img/pay/visa-secure.svg" alt="Visa Secure" loading="lazy"></span>
|
||||
<span class="pl"><img src="/static/img/pay/mastercard.svg" alt="MasterCard" loading="lazy"></span>
|
||||
<span class="pl"><img src="/static/img/pay/mc-id-check.svg" alt="MasterCard ID Check" loading="lazy"></span>
|
||||
<span class="pl"><img src="/static/img/pay/belkart.svg" alt="Белкарт" loading="lazy"></span>
|
||||
<span class="pl"><img src="/static/img/pay/belkart-ip.svg" alt="Белкарт ИнтернетПароль" loading="lazy"></span>
|
||||
<span class="pl"><img src="/static/img/pay/applepay.svg" alt="Apple Pay" loading="lazy"></span>
|
||||
<span class="pl"><img src="/static/img/pay/samsungpay.svg" alt="Samsung Pay" loading="lazy"></span>
|
||||
<span class="pl"><img src="/static/img/pay/alfabank.svg" alt="Альфа-Банк" loading="lazy"></span>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<script>
|
||||
// logged-in? → switch buttons to cabinet
|
||||
fetch('/api/me').then(r=>{if(r.ok){return r.json()}}).then(d=>{
|
||||
if(d&&d.user){
|
||||
const btn=document.getElementById('portalBtn');
|
||||
if(btn){btn.href='/cabinet';btn.textContent='Кабинет →';}
|
||||
const h=document.getElementById('heroBtn');
|
||||
if(h){h.href='/cabinet';h.textContent='Открыть кабинет';}
|
||||
const c=document.getElementById('ctaBtn');
|
||||
if(c){c.href='/cabinet';c.textContent='Открыть кабинет';}
|
||||
}
|
||||
}).catch(()=>{});
|
||||
|
||||
// reveal on scroll
|
||||
const io = new IntersectionObserver(es => es.forEach(e => e.isIntersecting && e.target.classList.add('vis')), {threshold:.1});
|
||||
document.querySelectorAll('.rv').forEach(el => io.observe(el));
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -1,598 +0,0 @@
|
|||
<!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>
|
||||
|
|
@ -1,162 +0,0 @@
|
|||
<!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>
|
||||
|
|
@ -1,91 +0,0 @@
|
|||
# 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).
|
||||
|
|
@ -1,176 +0,0 @@
|
|||
<!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>
|
||||
|
|
@ -1,79 +0,0 @@
|
|||
# Прокат авто (Минск) × 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.
|
||||
|
|
@ -1,115 +0,0 @@
|
|||
<!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>
|
||||
|
|
@ -1,35 +0,0 @@
|
|||
# Журнал ночной самодоработки харнеса
|
||||
|
||||
Директива 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? батч?), паттерн «дедуп + кап» применим к секциям эскалаций/инбокса.
|
||||
|
||||
|
|
@ -1,27 +0,0 @@
|
|||
# Самоанализ харнеса — 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 не порождает задачу-клон».
|
||||
|
|
@ -1,443 +0,0 @@
|
|||
<!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.ghost{background:transparent;color:var(--acc);border:1.5px solid var(--acc);margin-left:10px}
|
||||
.hero .btn.ghost:hover{background:var(--acc-soft)}
|
||||
.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}
|
||||
|
||||
.plan-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(280px,1fr));gap:20px;margin-top:8px}
|
||||
.plan-card{background:var(--card);border:1px solid var(--line);border-radius:10px;padding:24px}
|
||||
.plan-card h3{font-size:20px}
|
||||
.plan-price{font-family:var(--mono);font-size:12px;letter-spacing:.08em;text-transform:uppercase;color:var(--muted);margin:6px 0 14px}
|
||||
.plan-card ul{list-style:none;margin:0 0 18px}
|
||||
.plan-card li{padding:6px 0 6px 22px;position:relative;font-size:14.5px;color:#3d3a34}
|
||||
.plan-card li:before{content:"✓";position:absolute;left:0;color:var(--acc);font-weight:700}
|
||||
.plan-card .btn{display:inline-block;background:var(--acc);color:#fff;padding:10px 18px;border-radius:6px;font-weight:600}
|
||||
.plan-card.featured{border:2px solid var(--acc);box-shadow:0 6px 24px rgba(14,122,95,.12)}
|
||||
.plan-card code{font-family:var(--mono);font-size:12px;background:var(--acc-soft);color:var(--acc);padding:1px 5px;border-radius:4px}
|
||||
#reqform{display:grid;grid-template-columns:repeat(auto-fit,minmax(200px,1fr));gap:10px;max-width:760px}
|
||||
#reqform button{margin-top:4px}
|
||||
|
||||
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}
|
||||
footer .flinks{display:flex;gap:18px;font-size:13.5px}
|
||||
footer .flinks a{text-decoration:underline;color:var(--muted)}
|
||||
footer .flinks a:hover{color:var(--acc)}
|
||||
|
||||
#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">Витрина работает: AI-ассистент подбирает уход, заявки принимаются (Lite-план). Оплата онлайн — на шаге 2.</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="#plans">Магазинам</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>Корейский уход — AI подберёт, привезём за 1 день</h1>
|
||||
<p>Каталог проверенных K-beauty брендов в Минске + AI-ассистент: разберёт состояние кожи, соберёт рутину и примет заявку. Доставка по Беларуси.</p>
|
||||
<a class="btn" href="#catalog">Смотреть каталог</a> <a class="btn ghost" href="#request">Оставить заявку</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 id="plans">
|
||||
<div class="wrap">
|
||||
<h2>Магазинам корейской косметики</h2>
|
||||
<p class="sub">Shkor beauty — это AI-ассистент витрины (B2B): подбор ухода, заказы и заявки — единое API для вашего сайта и любых агентов. Оператор сервиса — Shkor Labs LLC.</p>
|
||||
<div class="plan-grid">
|
||||
<div class="plan-card">
|
||||
<h3>Lite</h3>
|
||||
<p class="plan-price">базовый план</p>
|
||||
<ul>
|
||||
<li>Витрина с каталогом и брендами</li>
|
||||
<li>Форма заявки → общая база заказов</li>
|
||||
<li>Заявки приходят с пометкой <code>plan=lite</code></li>
|
||||
<li>Договор с магазином лично: телефон/мессенджер</li>
|
||||
</ul>
|
||||
<a class="btn" href="#request">Начать с заявки</a>
|
||||
</div>
|
||||
<div class="plan-card featured">
|
||||
<h3>Full</h3>
|
||||
<p class="plan-price">полный каталог + заказы</p>
|
||||
<ul>
|
||||
<li>Всё из Lite</li>
|
||||
<li>Полный каталог и корзина</li>
|
||||
<li>Онлайн-оплата и статусы доставки</li>
|
||||
<li>Агентный API: analyze → recommend → order</li>
|
||||
</ul>
|
||||
<a class="btn" href="#request">Подключить Full</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="dark" id="request">
|
||||
<div class="wrap">
|
||||
<h2>Оставить заявку</h2>
|
||||
<p class="sub">Расскажите, что нужно — подберём уход или подключим магазин. Заявка пишется в общую базу заказов, ответим в течение дня.</p>
|
||||
<form id="reqform">
|
||||
<input type="text" name="name" placeholder="Имя" required>
|
||||
<input type="tel" name="phone" placeholder="+375 XX XXX-XX-XX" required>
|
||||
<input type="text" name="note" placeholder="Что подобрать? (сухая кожа, SPF, тонер…)">
|
||||
<button type="submit">Отправить</button>
|
||||
</form>
|
||||
<p class="sub" id="reqstatus" style="margin-top:10px"></p>
|
||||
</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 — витрина корейской косметики + AI-ассистент. Оператор сервиса: <b>Shkor Labs LLC</b> (Wyoming, USA) · <a href="mailto:alex.shkor@gmail.com" style="text-decoration:underline">alex.shkor@gmail.com</a></div>
|
||||
<div class="flinks">
|
||||
<a href="/privacy.html">Privacy Policy</a>
|
||||
<a href="/terms.html">Terms of Service</a>
|
||||
<a href="/disclaimer.html">Disclaimer</a>
|
||||
<a href="/about.html">About & Contacts</a>
|
||||
</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(){
|
||||
var card=b.closest('.pb'), h=card?card.querySelector('h3'):null;
|
||||
var note=document.getElementById('reqform').note;
|
||||
if(h&¬e) note.value='Интересует: '+h.textContent;
|
||||
document.getElementById('request').scrollIntoView({behavior:'smooth'});
|
||||
toast('Добавлено в заявку — оставьте контакты ниже');
|
||||
});
|
||||
});
|
||||
document.getElementById('reqform').addEventListener('submit',function(e){
|
||||
e.preventDefault();
|
||||
var f=e.target, st=document.getElementById('reqstatus');
|
||||
st.textContent='Отправляем…';
|
||||
fetch('/api/call',{method:'POST',headers:{'Content-Type':'application/json'},
|
||||
body:JSON.stringify({name:'submit_request',arguments:{name:f.name.value.trim(),phone:f.phone.value.trim(),note:f.note.value.trim()}})
|
||||
}).then(function(r){return r.json()}).then(function(d){
|
||||
var res=d.result||d;
|
||||
if(res.order_id){st.textContent='Заявка принята: '+res.order_id+'. Свяжемся в течение дня.';f.reset();}
|
||||
else{st.textContent='Не получилось: '+(res.error||JSON.stringify(res));}
|
||||
}).catch(function(err){st.textContent='Сеть недоступна — напишите на alex.shkor@gmail.com';});
|
||||
});
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -1,79 +0,0 @@
|
|||
# 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).
|
||||
|
|
@ -1,425 +0,0 @@
|
|||
#!/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,
|
||||
"plan": args.get("plan") if args.get("plan") in ("full", "lite") else "full",
|
||||
"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]}
|
||||
out = {"order_id": oid, "plan": o.get("plan", "full"), "status": o["status"],
|
||||
"payment": (o.get("payment") or {}).get("status")}
|
||||
if o.get("kind") == "request":
|
||||
out.update({"kind": "request", "note": o.get("note", ""),
|
||||
"products": o.get("products", []),
|
||||
"customer": o.get("customer", {})})
|
||||
return out
|
||||
out.update({"total": o["total"], "delivery": o["delivery"]["label"], "eta": o["delivery"]["eta"],
|
||||
"items": [{"title": l["title"], "qty": l["qty"]} for l in o["items"]]})
|
||||
return out
|
||||
|
||||
|
||||
def submit_request(args: dict) -> dict:
|
||||
"""Заявка из витрины Lite-плана: без корзины/оплаты, пишем в ту же базу orders.json с plan=lite.
|
||||
|
||||
Витрина формы («Хочу это», «Подобрать рутину») → request SB-R-XXXXXXXX → менеджер/агент
|
||||
договаривается лично. Когда магазин переходит на Full — та же запись дооформляется
|
||||
в заказ (check_order видит статус request).
|
||||
"""
|
||||
contact = (args.get("phone") or args.get("email") or "").strip()
|
||||
if not contact and not (args.get("name") or "").strip():
|
||||
return {"error": "нужен хотя бы один контакт: phone или email (или name)"}
|
||||
oid = "SB-R-" + uuid.uuid4().hex[:8].upper()
|
||||
rec = {
|
||||
"order_id": oid,
|
||||
"plan": "lite",
|
||||
"kind": "request",
|
||||
"note": (args.get("note") or "").strip(),
|
||||
"products": args.get("products") or [],
|
||||
"customer": {"name": args.get("name") or "", "phone": args.get("phone") or "",
|
||||
"email": args.get("email") or "", "address": args.get("address") or ""},
|
||||
"payment": {"status": "none", "method": "none", "pay_link": None},
|
||||
"total": 0.0,
|
||||
"status": "request",
|
||||
"created": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
|
||||
}
|
||||
orders = _load_orders()
|
||||
orders[oid] = rec
|
||||
_save_orders(orders)
|
||||
return {"order_id": oid, "status": "request", "plan": "lite",
|
||||
"next": "Магазин свяжется по контакту. Статус: tools/call check_order {\"order_id\": \"%s\"}" % oid}
|
||||
|
||||
|
||||
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": ("Оформить заказ (Full-план)", ["items[{id,qty}]", "delivery?", "name?", "phone?", "address?", "pay_method?", "plan?"]),
|
||||
"submit_request": ("Заявка из витрины Lite-плана: без корзины/оплаты → orders.json plan=lite, статус request", ["phone?", "email?", "name?", "note?", "products?"]),
|
||||
"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, "submit_request": submit_request, "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))
|
||||
|
|
@ -1,19 +0,0 @@
|
|||
{
|
||||
"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"}
|
||||
]
|
||||
}
|
||||
|
|
@ -1,159 +0,0 @@
|
|||
#!/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.send_header("Access-Control-Allow-Origin", "*") # витрина на другом домене: форма заявки → /call
|
||||
self.send_header("Access-Control-Allow-Methods", "GET, POST, OPTIONS")
|
||||
self.send_header("Access-Control-Allow-Headers", "Content-Type")
|
||||
self.end_headers()
|
||||
self.wfile.write(body)
|
||||
|
||||
def do_OPTIONS(self) -> None: # preflight для формы витрины (fetch с другого origin)
|
||||
self.send_response(204)
|
||||
self.send_header("Access-Control-Allow-Origin", "*")
|
||||
self.send_header("Access-Control-Allow-Methods", "GET, POST, OPTIONS")
|
||||
self.send_header("Access-Control-Allow-Headers", "Content-Type")
|
||||
self.send_header("Content-Length", "0")
|
||||
self.end_headers()
|
||||
|
||||
def do_GET(self) -> None: # health / открыть в браузере
|
||||
if self.path in ("/", "/health"):
|
||||
self._send(200, {"ok": True, "service": "shkor-assistant", "version": 1,
|
||||
"tools": "POST /tools/list", "call": "POST /call"})
|
||||
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,250 +0,0 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Генератор юр-страниц Shkor beauty (t-261008154151-faa550).
|
||||
|
||||
Privacy Policy / Terms of Service / Cookie & Disclaimer / About & Contacts.
|
||||
Единый шаблон = тот же визуальный язык, что у витрины. Оператор сервиса:
|
||||
Shkor Labs LLC (Wyoming, USA), alex.shkor@gmail.com.
|
||||
"""
|
||||
from pathlib import Path
|
||||
|
||||
OUT = Path("/var/www/beauty")
|
||||
OUT.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
CSS = """
|
||||
:root{--ink:#1c1a17;--paper:#faf7f2;--line:#e4ddd2;--acc:#0e7a5f;--muted:#6f6a61;
|
||||
--sans:"Segoe UI",-apple-system,Arial,sans-serif;--mono:ui-monospace,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.65;font-size:16px}
|
||||
.wrap{max-width:820px;margin:0 auto;padding:0 20px}
|
||||
header{border-bottom:1px solid var(--line);background:#fff}
|
||||
.hbar{display:flex;align-items:center;justify-content:space-between;padding:16px 0}
|
||||
.logo{font-size:20px;font-weight:700}
|
||||
.logo span{color:var(--acc)}
|
||||
.logo small{display:block;font-family:var(--mono);font-size:10px;letter-spacing:.14em;text-transform:uppercase;color:var(--muted)}
|
||||
nav a{margin-left:16px;font-size:14px;color:var(--muted)}
|
||||
nav a:hover{color:var(--acc)}
|
||||
main{padding:40px 0 60px}
|
||||
h1{font-size:clamp(26px,4vw,36px);letter-spacing:-.02em;margin-bottom:6px}
|
||||
.meta{font-family:var(--mono);font-size:12px;color:var(--muted);margin-bottom:28px}
|
||||
h2{font-size:19px;margin:28px 0 10px}
|
||||
p,li{color:#3d3a34;margin-bottom:10px}
|
||||
ul{padding-left:22px}
|
||||
a{color:var(--acc)}
|
||||
.card{background:#fff;border:1px solid var(--line);border-radius:10px;padding:22px;margin-top:24px}
|
||||
footer{padding:26px 0 40px;border-top:1px solid var(--line);color:var(--muted);font-size:13.5px}
|
||||
footer .links{display:flex;gap:18px;margin-bottom:8px}
|
||||
footer .links a{text-decoration:underline;color:var(--muted)}
|
||||
footer .proto{font-family:var(--mono);font-size:12px;margin-top:6px}
|
||||
"""
|
||||
|
||||
|
||||
def page(slug: str, title: str, updated: str, body: str) -> None:
|
||||
html = f"""<!doctype html>
|
||||
<html lang="ru">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>{title} — Shkor beauty</title>
|
||||
<meta name="description" content="{title} сервиса Shkor beauty. Оператор: Shkor Labs LLC.">
|
||||
<style>{CSS}</style>
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<div class="wrap hbar">
|
||||
<a class="logo" href="/">Shkor <span>beauty</span><small>корейская косметика · Минск</small></a>
|
||||
<nav>
|
||||
<a href="/#catalog">Каталог</a><a href="/#plans">Магазинам</a><a href="/#request">Заявка</a>
|
||||
</nav>
|
||||
</div>
|
||||
</header>
|
||||
<main class="wrap">
|
||||
<h1>{title}</h1>
|
||||
<div class="meta">Last updated: {updated} · v1.0 · Operator: Shkor Labs LLC (Wyoming, USA)</div>
|
||||
{body}
|
||||
</main>
|
||||
<footer>
|
||||
<div class="wrap">
|
||||
<div class="links">
|
||||
<a href="/privacy.html">Privacy Policy</a>
|
||||
<a href="/terms.html">Terms of Service</a>
|
||||
<a href="/disclaimer.html">Disclaimer</a>
|
||||
<a href="/about.html">About & Contacts</a>
|
||||
</div>
|
||||
<div>Оператор сервиса: <b>Shkor Labs LLC</b> (Wyoming, USA) · <a href="mailto:alex.shkor@gmail.com">alex.shkor@gmail.com</a></div>
|
||||
<div class="proto">собрано харнесом Simbiot · кооператив 42 · cil.by</div>
|
||||
</div>
|
||||
</footer>
|
||||
</body>
|
||||
</html>"""
|
||||
(OUT / f"{slug}.html").write_text(html, encoding="utf-8")
|
||||
print("ok", OUT / f"{slug}.html")
|
||||
|
||||
|
||||
D = "2026-10-08"
|
||||
|
||||
page("privacy", "Privacy Policy", D, """
|
||||
<p>Настоящая Политика конфиденциальности объясняет, какие данные собирает сервис Shkor beauty
|
||||
(далее — «Сервис») и как они используются. Оператор Сервиса — <b>Shkor Labs LLC</b>, компания,
|
||||
зарегистрированная в штате Вайоминг, США (далее — «Оператор»). Контакт: <a href="mailto:alex.shkor@gmail.com">alex.shkor@gmail.com</a>.</p>
|
||||
|
||||
<h2>1. Какие данные мы собираем</h2>
|
||||
<ul>
|
||||
<li><b>Заявка (Lite-план):</b> имя, телефон и текст заявки, которые вы сами указываете в форме «Оставить заявку».</li>
|
||||
<li><b>Заказ (Full-план):</b> имя, телефон, адрес доставки и состав заказа.</li>
|
||||
<li><b>Обращения:</b> письма на email Оператора и переписка в мессенджерах — в объёме, необходимом для ответа.</li>
|
||||
<li><b>Технические данные:</b> анонимная статистика посещений (страницы-рефереры, устройство, страна) — без персональных идентификаторов.</li>
|
||||
</ul>
|
||||
|
||||
<h2>2. Чего мы НЕ собираем</h2>
|
||||
<ul>
|
||||
<li>Данные банковских карт: при онлайн-оплате (Full-план) платёж проводит независимый платёжный агент;
|
||||
данные карты вводятся на его защищённой странице и Оператору не передаются.</li>
|
||||
<li>Фотографии кожи без явного согласия: если вы передаёте фотоотчёт AI-ассистенту, он обрабатывается
|
||||
для анализа состояния кожи и не публикуется и не передаётся третьим лицам.</li>
|
||||
</ul>
|
||||
|
||||
<h2>3. Зачем мы используем данные</h2>
|
||||
<ul>
|
||||
<li>связаться с вами по заявке и довести её до результата (подбор, заказ, доставка);</li>
|
||||
<li>исполнить заказ и уведомлять о его статусе;</li>
|
||||
<li>улучшать каталог и подбор ассистента на обезличенной статистике.</li>
|
||||
</ul>
|
||||
|
||||
<h2>4. Хранение и срок</h2>
|
||||
<p>Записи заявок и заказов хранятся в защищённой базе Оператора не дольше, чем это необходимо для
|
||||
исполнения заказа и требований бухгалтерии (по умолчанию — до 3 лет), после чего удаляются или
|
||||
обезличиваются.</p>
|
||||
|
||||
<h2>5. Передача третьим лицам</h2>
|
||||
<p>Данные передаются только: (а) службе доставки — имя, телефон и адрес вашего заказа;
|
||||
(б) платёжному агенту — минимально необходимый платёжный контекст; (в) в случаях, предусмотренных законом.</p>
|
||||
|
||||
<h2>6. Cookie</h2>
|
||||
<p>Сервис использует минимальные cookie и локальное хранилище: сессия корзины и анонимная статистика.
|
||||
Подробнее — в <a href="/disclaimer.html">Disclaimer</a>.</p>
|
||||
|
||||
<h2>7. Ваши права</h2>
|
||||
<p>Вы можете запросить доступ к своим данным, их исправление или удаление письмом на
|
||||
<a href="mailto:alex.shkor@gmail.com">alex.shkor@gmail.com</a>. Мы отвечаем в течение 30 дней.</p>
|
||||
|
||||
<h2>8. Изменения</h2>
|
||||
<p>Актуальная версия Политики всегда доступна на этой странице; существенные изменения датируются выше.</p>
|
||||
""")
|
||||
|
||||
page("terms", "Terms of Service", D, """
|
||||
<p>Условия использования сервиса Shkor beauty (далее — «Условия»). Оператор Сервиса — <b>Shkor Labs LLC</b>
|
||||
(Wyoming, USA), контакт <a href="mailto:alex.shkor@gmail.com">alex.shkor@gmail.com</a>. Используя Сервис,
|
||||
вы соглашаетесь с настоящими Условиями.</p>
|
||||
|
||||
<h2>1. Что такое Сервис</h2>
|
||||
<p>Shkor beauty — витрина корейской косметики с AI-ассистентом: подбор ухода, заявки и заказы с доставкой
|
||||
по Минску и Беларуси. Сервис предлагается в двух планах:</p>
|
||||
<ul>
|
||||
<li><b>Lite (базовый):</b> витрина + форма заявки. Заявка фиксируется в общей базе заказов, после чего
|
||||
магазин связывается с вами лично для уточнения ассортимента, цены и способа расчёта.</li>
|
||||
<li><b>Full:</b> полный каталог, корзина, онлайн-оплата и статусы доставки.</li>
|
||||
</ul>
|
||||
|
||||
<h2>2. Заявки и заказы</h2>
|
||||
<ul>
|
||||
<li>Заявка (Lite) не является подтверждённой сделкой: цена и наличие подтверждаются при обратной связи.</li>
|
||||
<li>Заказ (Full) считается принятым после подтверждения оплаты или согласия на оплату при получении.</li>
|
||||
<li>Цены указаны в белорусских рублях (BYN) и могут меняться; актуальная цена — на витрине в момент заказа.</li>
|
||||
</ul>
|
||||
|
||||
<h2>3. Доставка</h2>
|
||||
<p>Курьер по Минску, Европочта по Беларуси и самовывоз. Сроки и стоимость показываются при оформлении
|
||||
заявки/заказа; бесплатная доставка — от 80 BYN по каталогу витрины.</p>
|
||||
|
||||
<h2>4. Возврат и обмен</h2>
|
||||
<ul>
|
||||
<li>Товар надлежащего качества (без следов использования, в оригинальной упаковке) — возврат/обмен
|
||||
в течение 14 дней с момента получения.</li>
|
||||
<li>Средства гигиены и косметика с нарушенной упаковкой возврату не подлежат.</li>
|
||||
<li>Для возврата напишите на <a href="mailto:alex.shkor@gmail.com">alex.shkor@gmail.com</a> с номером заказа.</li>
|
||||
</ul>
|
||||
|
||||
<h2>5. Интеллектуальная собственность</h2>
|
||||
<p>Материалы витрины, AI-подбор и программный код ассистента принадлежат Оператору. Товарные знаки
|
||||
брендов принадлежат их правообладателям.</p>
|
||||
|
||||
<h2>6. Ограничение ответственности</h2>
|
||||
<p>Сервис предоставляется «как есть». AI-подбор средств — рекомендательный характер и не является
|
||||
медицинской консультацией (см. <a href="/disclaimer.html">Disclaimer</a>). Оператор не несёт
|
||||
ответственности за индивидуальные реакции кожи и решения, принятые на основе рекомендаций без
|
||||
ознакомления с составами и инструкциями.</p>
|
||||
|
||||
<h2>7. Применимое право</h2>
|
||||
<p>Настоящие Условия регулируются правом, применимым к месту регистрации Оператора (Wyoming, USA),
|
||||
без ущерба для обязательных потребительских прав покупателей по законодательству места доставки.</p>
|
||||
""")
|
||||
|
||||
page("disclaimer", "Cookie & Disclaimer", D, """
|
||||
<p>Страница объединяет политику cookie и важные дисклеймеры сервиса Shkor beauty. Оператор —
|
||||
<b>Shkor Labs LLC</b> (Wyoming, USA), <a href="mailto:alex.shkor@gmail.com">alex.shkor@gmail.com</a>.</p>
|
||||
|
||||
<h2>1. Не медицинская консультация</h2>
|
||||
<div class="card">
|
||||
<p><b>AI-ассистент и материалы витрины НЕ являются медицинской консультацией, диагностикой или лечением.</b>
|
||||
Подбор средств — эвристическая рекомендация на основе вашего самоотчёта. Она не заменяет консультацию
|
||||
дерматолога или врача. При заболеваниях кожи, беременности, приёме лекарственных средств и выраженных
|
||||
реакциях — проконсультируйтесь со специалистом до применения средств.</p>
|
||||
</div>
|
||||
|
||||
<h2>2. Косметика и индивидуальные реакции</h2>
|
||||
<ul>
|
||||
<li>Перед первым применением сделайте тест на чувствительность (небольшой участок кожи, 24–48 часов).</li>
|
||||
<li>Активы (ретинол, кислоты, витамин C) вводите постепенно; ретинол — только на ночь, SPF утром обязателен.</li>
|
||||
<li>Ознакомьтесь с составом на упаковке: при аллергии на компонент средство не используйте.</li>
|
||||
</ul>
|
||||
|
||||
<h2>3. Cookie</h2>
|
||||
<ul>
|
||||
<li><b>Необходимые:</b> сессия корзины и защита форм. Не отключаются.</li>
|
||||
<li><b>Аналитические:</b> анонимная статистика посещений для улучшения витрины. Без персональных идентификаторов и без передачи рекламным сетям.</li>
|
||||
<li><b>Рекламных и трекинговых cookie сторонних сетей — нет.</b></li>
|
||||
</ul>
|
||||
<p>Большинство браузеров позволяет управлять cookie в настройках; отключение необходимых cookie
|
||||
может сломать корзину и отправку заявки.</p>
|
||||
|
||||
<h2>4. Внешние ссылки</h2>
|
||||
<p>Витрина может ссылаться на сайты брендов и службы доставки — за их содержание Оператор
|
||||
ответственности не несёт.</p>
|
||||
|
||||
<h2>5. Вопросы</h2>
|
||||
<p>Любые вопросы по дисклеймерам и cookie — <a href="mailto:alex.shkor@gmail.com">alex.shkor@gmail.com</a>.</p>
|
||||
""")
|
||||
|
||||
page("about", "About & Contacts", D, """
|
||||
<p><b>Shkor beauty</b> — AI-ассистент для магазинов корейской косметики: витрина, которая сама
|
||||
разбирает состояние кожи клиента, собирает рутину и доводит заявку до заказа.</p>
|
||||
|
||||
<h2>Кто мы</h2>
|
||||
<p>Оператор сервиса — <b>Shkor Labs LLC</b>, компания, зарегистрированная в штате Вайоминг, США.
|
||||
Проект развивается совместно с инженерным кооперативом «42» (cil.by): витрина и ассистент собраны
|
||||
ритмичным агентом Simbiot.</p>
|
||||
|
||||
<h2>Что мы делаем</h2>
|
||||
<ul>
|
||||
<li><b>Витрина K-beauty:</b> каталог проверенных брендов — Anua, Beauty of Joseon, JMsolution,
|
||||
SKIN627, Biodance и другие, доставка по Минску и Беларуси.</li>
|
||||
<li><b>AI beauty-ассистент (B2B):</b> агентный API analyze → recommend → order, который любой
|
||||
магазин или AI-агент может подключить к своему сайту (MCP/STDIO, HTTP, CLI).</li>
|
||||
<li><b>Два плана подключения:</b> <b>Lite</b> — витрина + заявки в общую базу; <b>Full</b> — полный
|
||||
каталог, корзина, онлайн-оплата и статусы доставки.</li>
|
||||
</ul>
|
||||
|
||||
<h2>Контакты</h2>
|
||||
<div class="card">
|
||||
<ul>
|
||||
<li>Email: <a href="mailto:alex.shkor@gmail.com">alex.shkor@gmail.com</a></li>
|
||||
<li>Заявка: форма на <a href="/#request">главной странице</a> — ответим в течение дня</li>
|
||||
<li>Юрлицо-оператор: Shkor Labs LLC (Wyoming, USA)</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<h2>Магазинам: подключение ассистента</h2>
|
||||
<p>Ассистент подключается к любому сайту или AI-агенту за один шаг: JSONL поверх STDIO (MCP-стиль),
|
||||
HTTP JSON API или разовый CLI-вызов. Каталог масштабируется без правок кода. Для пилота напишите
|
||||
на email — подключим витрину с формой заявок (Lite) и покажем e2e-сценарий заказа.</p>
|
||||
""")
|
||||
|
|
@ -1,158 +0,0 @@
|
|||
{
|
||||
"SB-R-1D8C99C8": {
|
||||
"order_id": "SB-R-1D8C99C8",
|
||||
"plan": "lite",
|
||||
"kind": "request",
|
||||
"note": "Хочу сыворотку для сухой кожи",
|
||||
"products": [
|
||||
"p01"
|
||||
],
|
||||
"customer": {
|
||||
"name": "Тест Тестов",
|
||||
"phone": "+375291234567",
|
||||
"email": "",
|
||||
"address": ""
|
||||
},
|
||||
"payment": {
|
||||
"status": "none",
|
||||
"method": "none",
|
||||
"pay_link": null
|
||||
},
|
||||
"total": 0.0,
|
||||
"status": "request",
|
||||
"created": "2026-10-08T16:03:25Z"
|
||||
},
|
||||
"SB-55B78647": {
|
||||
"order_id": "SB-55B78647",
|
||||
"plan": "full",
|
||||
"items": [
|
||||
{
|
||||
"id": "p05",
|
||||
"title": "Успокаивающий тонер с сердечником Centella Toner 77%",
|
||||
"qty": 1,
|
||||
"price": 62.0,
|
||||
"sum": 62.0
|
||||
}
|
||||
],
|
||||
"subtotal": 62.0,
|
||||
"delivery": {
|
||||
"key": "minsk_courier",
|
||||
"label": "Курьер по Минску",
|
||||
"eta": "сегодня/завтра",
|
||||
"price": 7.0,
|
||||
"free_from": 80.0,
|
||||
"charged": 7.0
|
||||
},
|
||||
"total": 69.0,
|
||||
"customer": {
|
||||
"name": "",
|
||||
"phone": "",
|
||||
"address": ""
|
||||
},
|
||||
"payment": {
|
||||
"status": "pending",
|
||||
"method": "cash_on_delivery",
|
||||
"pay_link": null
|
||||
},
|
||||
"status": "placed",
|
||||
"created": "2026-10-08T16:03:25Z"
|
||||
},
|
||||
"SB-8AC083A5": {
|
||||
"order_id": "SB-8AC083A5",
|
||||
"plan": "lite",
|
||||
"items": [
|
||||
{
|
||||
"id": "p03",
|
||||
"title": "Солнцезащитный гель Relief Sun SPF50+ PA++++",
|
||||
"qty": 1,
|
||||
"price": 49.0,
|
||||
"sum": 49.0
|
||||
}
|
||||
],
|
||||
"subtotal": 49.0,
|
||||
"delivery": {
|
||||
"key": "minsk_courier",
|
||||
"label": "Курьер по Минску",
|
||||
"eta": "сегодня/завтра",
|
||||
"price": 7.0,
|
||||
"free_from": 80.0,
|
||||
"charged": 7.0
|
||||
},
|
||||
"total": 56.0,
|
||||
"customer": {
|
||||
"name": "",
|
||||
"phone": "",
|
||||
"address": ""
|
||||
},
|
||||
"payment": {
|
||||
"status": "pending",
|
||||
"method": "cash_on_delivery",
|
||||
"pay_link": null
|
||||
},
|
||||
"status": "placed",
|
||||
"created": "2026-10-08T16:03:25Z"
|
||||
},
|
||||
"SB-R-7B23D4D8": {
|
||||
"order_id": "SB-R-7B23D4D8",
|
||||
"plan": "lite",
|
||||
"kind": "request",
|
||||
"note": "Проверка формы с витрины (e2e)",
|
||||
"products": [],
|
||||
"customer": {
|
||||
"name": "E2E Тест",
|
||||
"phone": "+375290000000",
|
||||
"email": "",
|
||||
"address": ""
|
||||
},
|
||||
"payment": {
|
||||
"status": "none",
|
||||
"method": "none",
|
||||
"pay_link": null
|
||||
},
|
||||
"total": 0.0,
|
||||
"status": "request",
|
||||
"created": "2026-10-08T16:08:02Z"
|
||||
},
|
||||
"SB-B352DCBF": {
|
||||
"order_id": "SB-B352DCBF",
|
||||
"plan": "full",
|
||||
"items": [
|
||||
{
|
||||
"id": "p05",
|
||||
"title": "Успокаивающий тонер с сердечником Centella Toner 77%",
|
||||
"qty": 1,
|
||||
"price": 62.0,
|
||||
"sum": 62.0
|
||||
},
|
||||
{
|
||||
"id": "p03",
|
||||
"title": "Солнцезащитный гель Relief Sun SPF50+ PA++++",
|
||||
"qty": 1,
|
||||
"price": 49.0,
|
||||
"sum": 49.0
|
||||
}
|
||||
],
|
||||
"subtotal": 111.0,
|
||||
"delivery": {
|
||||
"key": "minsk_courier",
|
||||
"label": "Курьер по Минску",
|
||||
"eta": "сегодня/завтра",
|
||||
"price": 7.0,
|
||||
"free_from": 80.0,
|
||||
"charged": 0.0
|
||||
},
|
||||
"total": 111.0,
|
||||
"customer": {
|
||||
"name": "Тест",
|
||||
"phone": "+375XX0000000",
|
||||
"address": ""
|
||||
},
|
||||
"payment": {
|
||||
"status": "pending",
|
||||
"method": "cash_on_delivery",
|
||||
"pay_link": null
|
||||
},
|
||||
"status": "placed",
|
||||
"created": "2026-10-08T16:08:11Z"
|
||||
}
|
||||
}
|
||||
|
|
@ -1,74 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="ru">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>42 Labs — условия студии и инвестиционной программы</title>
|
||||
<style>
|
||||
:root{--paper:#faf8f3;--ink:#15150f;--signal:#FF4F00;--dim:#6b675e;--line:#d8d3c8;--card:#f3f0e8;--ok:#1d7a3e}
|
||||
*{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}
|
||||
.wrap{max-width:980px;margin:0 auto;padding:56px 24px 100px}
|
||||
header{border-bottom:3px solid var(--ink);padding-bottom:26px;margin-bottom:40px}
|
||||
h1{font-size:34px;letter-spacing:-.02em;margin-bottom:8px}
|
||||
.sub{color:var(--dim)}
|
||||
h2{font-size:22px;margin:44px 0 14px;padding-top:18px;border-top:2px solid var(--line)}
|
||||
p,li{margin-bottom:10px}
|
||||
ul{padding-left:22px}
|
||||
.card{background:var(--card);border:1px solid var(--line);border-radius:12px;padding:20px 22px;margin:16px 0}
|
||||
.formula{font-family:ui-monospace,Menlo,Consolas,monospace;background:var(--card);border-left:4px solid var(--signal);padding:12px 16px;margin:14px 0;border-radius:0 8px 8px 0}
|
||||
table{width:100%;border-collapse:collapse;margin:16px 0}
|
||||
th,td{border:1px solid var(--line);padding:10px 12px;text-align:left}
|
||||
th{background:var(--card)}
|
||||
footer{margin-top:56px;color:var(--dim);font-size:14px;border-top:2px solid var(--line);padding-top:16px}
|
||||
a{color:var(--signal)}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="wrap">
|
||||
<header>
|
||||
<h1>42 Labs — условия студии</h1>
|
||||
<p class="sub">Участие · Инвестиционная программа · Управление. Редакция октябрь 2026. Не заменяет договор; при расхождении действует договор.</p>
|
||||
</header>
|
||||
|
||||
<h2>1. Как устроено участие</h2>
|
||||
<p>Набор в студию — <strong>раз в квартал</strong>. Условия участия подробно: <a href="https://42.cil.by/">42.cil.by</a>, объяснение договора: <a href="https://cil.by/static/dogovor_obiascenie.html">cil.by/static/dogovor_obiascenie.html</a>.</p>
|
||||
<ul>
|
||||
<li>Проект на 100% твой: студия не берёт долю в капитале, код, бренд и клиенты твои с первого дня.</li>
|
||||
<li>Платишь большее из двух и только его: месячный взнос <strong>$999</strong> или <strong>[X]% от выручки проекта</strong>. Месяца, где платишь и то и другое, не существует.</li>
|
||||
<li>Неоплата не расторгает договор: долг индексируется только вверх вместе с биткоином (CME CF BRR, запасной источник CoinGecko):</li>
|
||||
</ul>
|
||||
<div class="formula">Долг = Транш × max(1, BTC сегодня / BTC тогда) — только вверх, без процентов и множителей</div>
|
||||
<ul>
|
||||
<li>Возврат — из выручки проекта или прироста зарплаты. Личным долгом не становится. Выход — в любой месяц.</li>
|
||||
</ul>
|
||||
|
||||
<h2>2. Инвестиционная программа (SAFE MFN)</h2>
|
||||
<p>Инвесторы вкладываются по <strong>SAFE с MFN</strong> (most favored nation). Голос инвестора определяется на момент инвестиции:</p>
|
||||
<div class="formula">Голоса = сколько BTC можно было купить на сумму инвестиции в момент её совершения</div>
|
||||
<p>Голоса фиксируются в момент входа и дальше не переоцениваются — растёт или падает биткоин после инвестиции, число голосов не меняется.</p>
|
||||
|
||||
<h2>3. Квартальные уровни инвестиций студии</h2>
|
||||
<p>Раз в квартал студия распределяет инвестиции по уровням:</p>
|
||||
<table>
|
||||
<tr><th>Уровень</th><th>Размер инвестиции</th></tr>
|
||||
<tr><td>1</td><td>$1 000</td></tr>
|
||||
<tr><td>2</td><td>$10 000</td></tr>
|
||||
<tr><td>3</td><td>$100 000</td></tr>
|
||||
<tr><td>4</td><td>$1 000 000</td></tr>
|
||||
</table>
|
||||
<ul>
|
||||
<li>Подаваться нужно <strong>на текущий уровень</strong> — перепрыгивать уровни нельзя.</li>
|
||||
<li>Количество победителей на уровне регулирует правление. Сейчас — <strong>по 1 победителю на уровень</strong>.</li>
|
||||
</ul>
|
||||
|
||||
<h2>4. Ежедневные звонки: 3 через 3</h2>
|
||||
<p>В рабочие дни звонки идут по схеме «3 через 3». Время распределяется <strong>пропорционально текущим голосам инвесторов</strong>. Если у инвестора ноль голосов — ему отводится <strong>3 минуты</strong>.</p>
|
||||
|
||||
<footer>
|
||||
42 Labs · Shkor Labs LLC (Вайоминг) · ПК «Друзья Коллективного Разума» (Минск) · <a href="https://42.cil.by/">42.cil.by</a><br>
|
||||
Эта страница объясняет условия, но не заменяет договор. При расхождении действует договор.
|
||||
</footer>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -1,45 +0,0 @@
|
|||
# 42 Labs — условия студии
|
||||
|
||||
Редакция октябрь 2026. Не заменяет договор; при расхождении действует договор.
|
||||
Подробно: [42.cil.by](https://42.cil.by/) · Объяснение договора: [cil.by/static/dogovor_obiascenie.html](https://cil.by/static/dogovor_obiascenie.html)
|
||||
|
||||
## 1. Как устроено участие
|
||||
|
||||
Набор в студию — **раз в квартал**.
|
||||
|
||||
- Проект на 100% твой: студия не берёт долю в капитале, код, бренд и клиенты твои с первого дня.
|
||||
- Платишь **большее из двух** и только его: месячный взнос **$999** или **[X]% от выручки проекта**. Месяца, где платишь и то и другое, не существует.
|
||||
- Неоплата не расторгает договор: долг индексируется только вверх вместе с биткоином (CME CF BRR, запасной источник CoinGecko):
|
||||
|
||||
> Долг = Транш × max(1, BTC сегодня / BTC тогда) — только вверх, без процентов и множителей.
|
||||
|
||||
- Возврат — из выручки проекта или прироста зарплаты. Личным долгом не становится. Выход — в любой месяц.
|
||||
|
||||
## 2. Инвестиционная программа (SAFE MFN)
|
||||
|
||||
Инвесторы вкладываются по **SAFE с MFN** (most favored nation). Голос инвестора определяется на момент инвестиции:
|
||||
|
||||
> **Голоса = сколько BTC можно было купить на сумму инвестиции в момент её совершения**
|
||||
|
||||
Голоса фиксируются в момент входа и дальше не переоцениваются.
|
||||
|
||||
## 3. Квартальные уровни инвестиций студии
|
||||
|
||||
Раз в квартал студия распределяет инвестиции по уровням:
|
||||
|
||||
| Уровень | Размер инвестиции |
|
||||
|---|---|
|
||||
| 1 | $1 000 |
|
||||
| 2 | $10 000 |
|
||||
| 3 | $100 000 |
|
||||
| 4 | $1 000 000 |
|
||||
|
||||
- Подаваться нужно **на текущий уровень** — перепрыгивать уровни нельзя.
|
||||
- Количество победителей на уровне регулирует правление. Сейчас — **по 1 победителю на уровень**.
|
||||
|
||||
## 4. Ежедневные звонки: 3 через 3
|
||||
|
||||
В рабочие дни звонки идут по схеме «3 через 3». Время распределяется **пропорционально текущим голосам инвесторов**. Если у инвестора ноль голосов — ему отводится **3 минуты**.
|
||||
|
||||
---
|
||||
42 Labs · Shkor Labs LLC (Вайоминг) · ПК «Друзья Коллективного Разума» (Минск) · 42.cil.by
|
||||
|
|
@ -1,7 +1,6 @@
|
|||
#!/usr/bin/env bash
|
||||
# Simbiot Harness: запуск без установки обёртки (cli.py + scheduler-демон).
|
||||
# simbiot help — справка по CLI-командам (как python cli.py -h)
|
||||
export SIMBIOT_HOME="${SIMBIOT_HOME:-/var/lib/simbiot-harness}"
|
||||
# simbiot — без аргументов: статус + как управлять демоном/CLI
|
||||
# simbiot --help — как help (аргумент для argparse-привычки)
|
||||
DIR="$(cd "$(dirname "$(readlink -f "$0")")/.." && pwd)"
|
||||
|
|
|
|||
41
cli.py
41
cli.py
|
|
@ -90,9 +90,6 @@ def main():
|
|||
help="ключ идемпотентности (по умолчанию file:<абсолютный путь>)")
|
||||
p_dfl.add_argument("--force", action="store_true",
|
||||
help="отправить даже если уже доставлен (повторная доставка вручную)")
|
||||
p_dfl.add_argument("-m", "--msg", dest="msg", default=None,
|
||||
help="связать доставку с входящим m-NNN (анти-гонка уровней, "
|
||||
"t-261009160906): если на него уже отвечено иным артефактом — SKIP")
|
||||
p_pro = sub.add_parser("proactive", help="инициативная реплика: ритм/тема/отправка (t-260930073419)")
|
||||
p_pro.add_argument("--force", action="store_true",
|
||||
help="отправить сейчас, игнорируя окно/интервал/тишину")
|
||||
|
|
@ -181,12 +178,6 @@ def main():
|
|||
p_rag.add_argument("--db", default=None, help="база RAG (по умолчанию rag.db; hermes.db — общая)")
|
||||
p_rag.add_argument("--json", action="store_true", help="вывести машинно-читаемо")
|
||||
|
||||
p_mcp = sub.add_parser("mcp-call", help="вызов инструмента MCP-портала кооператива (cil.by/mcp, t-261010093054)")
|
||||
p_mcp.add_argument("tool", nargs="?", default="", help="имя тула (booking_slots, booking_create, whoami, …)")
|
||||
p_mcp.add_argument("arguments", nargs="?", default="", help="JSON-аргументы тула, напр. '{\"type\": \"call\"}'")
|
||||
p_mcp.add_argument("--list", action="store_true", help="показать тулы шлюза и выйти")
|
||||
p_mcp.add_argument("--timeout", type=int, default=None, help="таймаут запроса, сек")
|
||||
|
||||
args = ap.parse_args()
|
||||
|
||||
if args.cmd == "init":
|
||||
|
|
@ -376,28 +367,11 @@ def main():
|
|||
print(f"FAIL: файл не найден: {args.file}")
|
||||
sys.exit(1)
|
||||
aid = args.key or f"file:{os.path.abspath(args.file)}"
|
||||
# Анти-гонка уровней (t-261009160906): если доставка привязана к
|
||||
# входящему m-NNN и на него уже отвечено ДРУГИМ артефактом —
|
||||
# не отправляем второй файл на тот же запрос (гонка min/hour).
|
||||
if args.msg and not args.force:
|
||||
model = es.run_projections()
|
||||
m = (model.get("inbox") or {}).get(args.msg)
|
||||
if m is not None and m.get("replied") and m.get("reply_artifact") not in (None, "", aid):
|
||||
es.append_event("harness", aid, "DeliverySkipped", {
|
||||
"artifact_id": aid, "kind": "file",
|
||||
"reason": "msg-already-answered-by-other-artifact",
|
||||
"msg": args.msg,
|
||||
"reply_artifact": m.get("reply_artifact"),
|
||||
})
|
||||
print(f"SKIP: на {args.msg} уже отвечено другим артефактом "
|
||||
f"({m.get('reply_artifact')}) — используй --force для повтора")
|
||||
sys.exit(0)
|
||||
def _send():
|
||||
return tg_inbox.send_document_to_chat(args.file, caption=args.caption)
|
||||
ok = commands.deliver_artifact(aid, kind="file", send=_send,
|
||||
owner="cli:tg-deliver-file",
|
||||
caption=args.caption, force=args.force,
|
||||
msg=args.msg)
|
||||
caption=args.caption, force=args.force)
|
||||
print(f"OK: файл доставлен ({aid})" if ok
|
||||
else f"SKIP: уже доставлен ({aid}) — используй --force для повтора")
|
||||
elif args.cmd == "proactive":
|
||||
|
|
@ -434,19 +408,6 @@ def main():
|
|||
for c in chunks:
|
||||
print(f"\n═══ {c['score']:.3f} | {c['source']} ═══")
|
||||
print(c["text"][:rag_lookup.TIMEOUT * 100 or 900])
|
||||
elif args.cmd == "mcp-call":
|
||||
# MCP портала (t-261010093054): ручной вызов тула шлюза cil.by/mcp;
|
||||
# без init_db — клиент не пишет в read model (трейс только с level/trace_id)
|
||||
from harness import portal_mcp
|
||||
if args.list:
|
||||
names = portal_mcp.list_tools(force=True)
|
||||
print("\n".join(names) if names else
|
||||
"(шлюз недоступен или токена нет — проверь SIMBIOT_PORTAL_MCP_TOKEN)")
|
||||
return
|
||||
if not args.tool:
|
||||
ap.error("mcp-call: укажи tool (или --list)")
|
||||
kw = {"timeout": args.timeout} if args.timeout else {}
|
||||
print(portal_mcp.call_tool(args.tool, args.arguments, **kw))
|
||||
elif args.cmd == "escalate":
|
||||
es.init_db()
|
||||
eid, to_level = commands.raise_escalation(
|
||||
|
|
|
|||
|
|
@ -1,48 +0,0 @@
|
|||
# Roadmap Simbiot UI — v0.2 (решения Alex m-516173892, 2026-10-08)
|
||||
|
||||
> Продукт зафиксирован: брендинг **Simbiot UI / Simbiot Dashboard**, цена **$99.9/мес
|
||||
> (claude seat included)**, ключи выдаём наши (шлюз 9router), свой RAG кооператива —
|
||||
> клиентам нет, есть **общий студийный RAG**, подключение клиентов — **только вручную
|
||||
> через Alex** на всю первую партию. Полный план: out/simbiot_ui_product_plan.md.
|
||||
|
||||
## Фазы
|
||||
|
||||
| # | Фаза | Что делаем | Оценка | Статус |
|
||||
|---|---|---|---|---|
|
||||
| 0 | Спайк | 1 инстанс claudecodeui за Caddy (ui.cil.by), UX/OAuth/MCP-проверка | 1–2 дня | next |
|
||||
| 0b | Пресейл-лендинг | out/simbiot_ui_presale.html → деплой на ui.cil.by, заявки → к Alex | 1 день | артефакт готов, деплой next |
|
||||
| 1 | Каркас продукта | Админка заявок (очередь/статусы), Stripe-ссылка после ручного подтверждения | 2–3 дня | — |
|
||||
| 2 | Оркестратор (ручной) | Провижининг по кнопке Alex: контейнер, мэйл, CLAUDE.md, teardown | 3–4 дня | — |
|
||||
| 3 | Студийный RAG | Отдельный индекс; отбор публичных знаний (список на ревью Alex); MCP read-only | 3–4 дня | — |
|
||||
| 4 | Пилот | 3–5 платящих клиентов через ручной онбординг, фидбек, реальные юнит-цифры | 1–2 недели | — |
|
||||
| 5 | Автопровижининг | Self-service оплата→доступ | отложено (гейт ниже) | — |
|
||||
|
||||
## Ключевые ограничения (зафиксировано решениями Alex)
|
||||
|
||||
1. **Ручной онбординг через Alex — план-А, не обходной путь.** Все подключения первой
|
||||
партии делает Alex вручную (A7). Self-service не включаем до явного решения.
|
||||
- Пропускная способность: целевые 3–5 клиентов/нед; очередь > 1 недели — сигнал
|
||||
начинать фазу 5.
|
||||
- Следствие для продукта: форма-заявка вместо instant-checkout; оплата (Stripe live)
|
||||
только после ручного подтверждения заявки.
|
||||
2. **Наш RAG кооператива клиентам НЕ даём (A6).** Строим отдельный общий студийный RAG
|
||||
(«как строить стартапы», условия студии) из публичных материалов; перегон — по списку
|
||||
с ревью Alex, MCP read-only. Приватные знания кооператива не попадают наружу.
|
||||
3. **Ключи и сид — наши (A2+A3):** 9router как единственный шлюз, per-client spend-трекинг,
|
||||
fair-use лимиты в оффере (защита маржи $99.9-тарифа), ToS-риск на ревью до масштабирования.
|
||||
|
||||
## Гейты перехода между фазами
|
||||
|
||||
- 0 → 1: спайк признан удачным (UX ок, OAuth сидит, MCP-хуки работают).
|
||||
- 1 → 2: очередь заявок в админке живёт, Alex подтвердил флоу подтверждения.
|
||||
- 2 → 3: 1–2 клиента реально подключены вручную end-to-end.
|
||||
- 3 → 4: студийный RAG прошёл ревью Alex (список источников утверждён).
|
||||
- 4 → 5: очередь ручного онбординга стабильно > 1 нед ИЛИ > 10 клиентов платят.
|
||||
|
||||
## Открытые вопросы к Alex (не блокируют фазы 0–2)
|
||||
|
||||
- **Куда падают заявки с ui.cil.by:** email-агент (hello@cil.by?) / TG-бот Alex / портал.
|
||||
В лендинге placeholder — до выбора канала не деплоить форму в прод.
|
||||
- Точное содержание fair-use лимита (токены/мес) для оффера $99.9.
|
||||
- Список источников для перегона в студийный RAG (фаза 3, нужен на ревью).
|
||||
- Юр-модель перепродажи сида при масштабировании за пилот.
|
||||
|
|
@ -7,7 +7,7 @@
|
|||
"amount_usd": 26.7,
|
||||
"src": "openrouter.ai/api/v1/credits (замер 2026-10-03 23:5x UTC: total=163, usage=136.30)",
|
||||
"auto": true,
|
||||
"measured_at": "2026-10-04T01:13:01+00:00"
|
||||
"measured_at": "2026-10-04T00:15:21+00:00"
|
||||
},
|
||||
{
|
||||
"name": "US LLC Delaware (операционный)",
|
||||
|
|
@ -24,7 +24,7 @@
|
|||
"amount_usd": 0.0,
|
||||
"src": "api.stripe.com/v1/balance livemode=True, STRIPE_LIVE_KEY из coop/.env (замер 2026-10-03): available=0, pending=0",
|
||||
"auto": true,
|
||||
"measured_at": "2026-10-04T01:13:01+00:00"
|
||||
"measured_at": "2026-10-04T00:15:22+00:00"
|
||||
}
|
||||
],
|
||||
"debts": [],
|
||||
|
|
|
|||
|
|
@ -1,38 +0,0 @@
|
|||
# Дайджест 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).
|
||||
|
|
@ -1,59 +0,0 @@
|
|||
# Дайджест 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).
|
||||
|
|
@ -1,58 +0,0 @@
|
|||
# Дайджест 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.
|
||||
|
|
@ -1,57 +0,0 @@
|
|||
# Дайджест hour 2026-10-05 ~16:00 UTC
|
||||
|
||||
## Батч входящих m-516173792/793 (2 сообщения, один разговор) — отвечено сводно
|
||||
|
||||
Контекст: продолжение запроса Alex на развёртку Claude Code на Лунаре
|
||||
(m-516173791, задача t-261005155643, подана мин-тиком 15:56). Новое требование:
|
||||
Claude Code должен подключаться к Telegram официальным коннектором
|
||||
`claude --channels plugin:telegram@claude-plugins-official`.
|
||||
|
||||
Сделано:
|
||||
- **Разведка коннектора (проверено по фактам, не по памяти):**
|
||||
- плагин `telegram` v0.0.7 реально существует в официальном маркетплейсе
|
||||
anthropics/claude-plugins-official (315 плагинов, telegram в категории
|
||||
productivity) — README изучен с GitHub;
|
||||
- флаг `--channels` в установленной на Лунаре версии claude 2.1.289
|
||||
парсируется корректно (проверено запуском);
|
||||
- механика: MCP-сервер на **Bun** (на Лунаре отсутствует — ставить),
|
||||
`/plugin install` → `/telegram:configure <токен>` → запуск с `--channels`
|
||||
→ DM боту → 6-значный код → `/telegram:access pair` → allowlist;
|
||||
- **блокер-1:** сессия claude не авторизована (Not logged in) — нужен ключ
|
||||
по паттерну Z.AI/GLM из `/root/coop/claude_harness.py`;
|
||||
- **блокер-2 (главный):** токен TG-бота Лунара занят simbiot-harness-tg
|
||||
(long polling) — два поллера на одном токене конфликтуют. Варианты
|
||||
предложены Alex: (1) новый бот от BotFather — Simbiot-инбокс остаётся;
|
||||
(2) отдать существующий токен — TG-инбокс Лунара переехал бы в Claude
|
||||
Code. По умолчанию вариант 1, жду токен.
|
||||
- **Задача t-261005161125-d7d92a [S] подана** — подключение коннектора
|
||||
со всей разведкой в деталях (bun, шаги, оба блокера, варианты токена),
|
||||
чтобы исполнитель не повторял разведку.
|
||||
- Ответ Alex отправлен сводным батч-реплаем (2/2 закрыты).
|
||||
|
||||
## Развёртка Claude Code на Лунаре (t-261005155643, running у min)
|
||||
|
||||
Мин-тик довёл до: на Лунаре (2.28.121.212, ssh root работает) стоят
|
||||
node 22 / claude 2.1.289 / ttyd; сервис `ttyd-claude.service` поднят
|
||||
(:7681, basic-auth, пользователь lunar), портал (204.168.232.21) уже
|
||||
пробует терминал. Хвост работы: авторизация claude, интеграция ttyd в UI
|
||||
портала, проверка. Первый заход мин-тика упёрся в бюджет тика — задача
|
||||
корректно перезабрана следующим мин-тиком (guard сработал как задумано).
|
||||
|
||||
## Гигиена доски
|
||||
|
||||
Два клона-мусора (t-261004134505/506, паттерн само-клонирования
|
||||
e-261004134613: title=ID уже done-задач) переблокированы с явной причиной
|
||||
«работы нет» — выведены из очереди, чтобы не жечь бюджет мин-тиков по
|
||||
анти-старению. CLI не даёт удалить blocked-задачу (close только для
|
||||
failed, complete только для running) — оставлены в blocked.
|
||||
|
||||
## Метрики часа
|
||||
|
||||
- Эскалаций открытых: 0 (все acked).
|
||||
- Фоновых задач: 0.
|
||||
- Лестница: sec/min/hour/day живы; у min 38 ошибок за жизнь (последняя —
|
||||
таймаут воркока 480с, известный класс, не новый), у hour 6, у day 1 —
|
||||
тренда деградации нет.
|
||||
- Доска: 162 done, 1 running (Лунар-развёртка, у min), 1 ready
|
||||
(TG-коннектор, следующий на очередь), 2 blocked (мусор с причиной).
|
||||
|
|
@ -1,43 +0,0 @@
|
|||
# Дайджест hour 2026-10-06 ~04:00 UTC
|
||||
|
||||
## Батч задач (2/2 закрыты, обе [M])
|
||||
|
||||
1. **t-261006034257-2cacd1** — MCP-инструменты OpenCode FR#4-#7. В гейт
|
||||
cil.by/mcp добавлены 5 read-only тулов: `list_members` (31 член:
|
||||
роль/проекты/паи/агенты), `list_applications` (заявки+голоса),
|
||||
`projects_list` (22 проекта, фильтры owner/status), `list_agents`
|
||||
(18 агентов + last_heartbeat — дока ask_agent теперь валидна),
|
||||
`treasury_status` (wallet-treasury **3733.60 USDA**, кошельки членов,
|
||||
паевые, последние движения). E2E через гейт: все ok, tools/list=68,
|
||||
фильтры проверены. Дока MCP_CONNECT.md дополнена. Портал перезапущен.
|
||||
Бэкап mcp.py.backup-1006-readtools.
|
||||
2. **t-261006034507-f41aa2** — аудит фича-запросов OpenCode. mcp_audit:
|
||||
пришло ровно 4 (все ok) — в портале ничего не потерялось, но в Kan
|
||||
была потеря: только 2 карточки-дубля [FR#7], #4/#5/#6 отсутствовали.
|
||||
Причина: **гонка в `_kan_psql`** — общий remote-файл
|
||||
/tmp/kan_agent_fr.sql перезаписывался при 4 параллельных
|
||||
feature_request (портал threaded=True), ssh выполнял чужой SQL.
|
||||
Фиксы: уникальный remote-файл (uuid) + настоящий cardNumber вместо
|
||||
id фичи (раньше писалось kan_card_number=id). Бэкфилл: [FR#4]№27,
|
||||
[FR#5]№28, [FR#6]№29, дубль #7 скрыт. E2E полного тракта FR→Kan
|
||||
верифицирован, тестовые записи вычищены. Итог: 4 запроса = 4
|
||||
карточки №26-29, feature_backlog синхронен.
|
||||
|
||||
## Инфра/события
|
||||
|
||||
- Мин-тики часа: 6× gate:quiet — событий выше порога нет, LLM не
|
||||
запускался (экономия burn).
|
||||
- Фоновые задачи: нет (list пуст).
|
||||
- Эскалации: открытых 0 (все acked; свежая e-261006025634 по TG-токену
|
||||
Лунары — вне этого тика, наблюдалась ранее).
|
||||
- Доска: только 2 старых blocked S-клона (02.10, ждут уточнений),
|
||||
ready пусто.
|
||||
- coop-portal.service перезапускался 3× в течение тика (по фиксам),
|
||||
финально active, health ok.
|
||||
|
||||
## AUM-нотация
|
||||
|
||||
Тулы состояния (казна/члены/проекты/агенты) — база для агентских
|
||||
контуров кооператива (twin-lite студия, AUM-симуляции): агенты больше
|
||||
не ходят вслепую, а фикс гонки гарантирует, что фича-бэклог не теряет
|
||||
запросы — прямой вклад в долгосрочную базу.
|
||||
|
|
@ -1,43 +0,0 @@
|
|||
# Дайджест hour 2026-10-06 ~08:30 UTC
|
||||
|
||||
## Батч задач (1/1 закрыта)
|
||||
|
||||
1. **t-261006072315-18199d** [L] — Доска фича-реквестов на странице
|
||||
проекта (Alex). На портале cil.by `/project/<id>` добавлен раздел
|
||||
«Фича-реквесты»: агрегирует все входящие MCP `feature_request` с их
|
||||
статусами. Сделано:
|
||||
- миграция: `agent_feature_requests.project_id` + индекс (init_db,
|
||||
применена на проде);
|
||||
- API в `domains/boards.py`: `GET /api/projects/<pid>/feature-requests`
|
||||
(FR проекта + общий бэклог портала `project_id=''`, голоса, can_manage)
|
||||
и `PATCH .../<id>/status` (владелец/Председатель; статусы
|
||||
new/accepted/in_progress/done/rejected);
|
||||
- UI `static/project_detail.html` в стиле портала: карточки
|
||||
(title, агент, дата, Kan №, голоса ⬡, проблема/предложение/usage_hint),
|
||||
селектор статуса у владельца, счётчики по статусам;
|
||||
- MCP `feature_request` теперь привязывает запрос к проекту агента
|
||||
(`agents.project_id`).
|
||||
E2E на изолированной копии БД: 7 FR на проекте 1, PATCH ок, невалидный
|
||||
статус 400, member 403 на PATCH, изоляция проектов ок, 404 на несущ.,
|
||||
`node --check` ok. Прод: coop-portal рестартован, active, миграция
|
||||
применена. Бэкапы `*.backup-1006-frboard`.
|
||||
|
||||
## Инфра/события
|
||||
|
||||
- Мин-тики часа: 6× gate:quiet — событий выше порога нет, LLM не
|
||||
запускался (экономия burn).
|
||||
- Фоновые задачи: нет (list пуст).
|
||||
- Эскалации: открытых 0, все acked. Свежая e-261006065717 (mail_send
|
||||
500) уже закрыта фикс-коммитом `bc3c3e5` (import sys → NameError).
|
||||
- coop-portal.service: 1 рестарт в тике (деплой доски), финально
|
||||
active, health ok, агенты поллят штатно.
|
||||
- Git /root/coop: изменения в 4 файлах (app.py, boards.py, mcp.py,
|
||||
project_detail.html) — уйдут очередным auto-commit.
|
||||
|
||||
## AUM-нотация
|
||||
|
||||
Доска фича-реквестов замыкает контур обратной связи агент→владелец:
|
||||
запросы больше не лежат мёртвым списком, а имеют видимый статус и
|
||||
приоритет (голоса) прямо на странице проекта. Это повышает отдачу от
|
||||
агентского контура кооператива (больше полезных фич → больше ресурса
|
||||
на длинной дистанции) — прямой вклад в долгосрочную AUM-базу.
|
||||
|
|
@ -1,38 +0,0 @@
|
|||
# Дайджест hour 2026-10-06 ~18:10 UTC
|
||||
|
||||
## Эскалации (обе закрыты в этом тике)
|
||||
|
||||
- **e-261006145651 (critical, «стор скомпрометирован»)** — ЛОЖНАЯ ТРЕВОГА, разобрано и закрыто.
|
||||
Полный аудит `cli.py verify`: OK по всем 1.3M событий (подписи + тампер-цепочка, ключ faf8d19d9aed71fe).
|
||||
Событие 1303573 перепроверено точечной криптопроверкой — подпись ВАЛИДНА. Единственная дырка: 1303601
|
||||
(сама эта EscalationRaised) записана без sig из-за транзиентного отказа подписи — аудит корректно
|
||||
считает её legacy-границей, не ошибкой. Причина паники: упавший процесс верифицировал дельту чужим
|
||||
ключом (изоляты /tmp/simbiot-iso-* имеют собственные ключи аудита) → false positive.
|
||||
- **e-261006145703 (warn, дублирующий трейсбек)** — тот же инцидент, закрыт вместе с первым.
|
||||
Рекомендация на day: прокинуть key_id стора в верификатор/не считать чужой key_id ошибкой.
|
||||
|
||||
Состояние после разбора: проекции догнали голову стора (last_event = max id), CLI работает,
|
||||
min-тик жив, inbox чист (0 без ответа; m-516173853/854 из контекста эскалации давно replied).
|
||||
|
||||
## Батч задач (2/2 закрыто)
|
||||
|
||||
1. **t-261006175404 (M) deploy-claude-code.sh — ГОТОВО.**
|
||||
`scripts/deploy-claude-code.sh` (289 строк, +x): воспроизводимая развёртка Claude Code + ttyd web-TUI
|
||||
по боёвому опыту Лунара (node 22 nodesource/tarball-фолбэк, npm i -g @anthropic-ai/claude-code,
|
||||
ttyd 1.7.7 под архитектуру, OS-юзер, systemd-юнит с basic-auth). Опции --port/--user/--token/--service/
|
||||
--ttyd-version/--claude-version/--interface/--env K=V/--readonly/--dry-run/--check. Протестировано:
|
||||
bash -n, два dry-run (включая лунар-профиль — ExecStart совпал с боевым), валидация аргументов
|
||||
(exit 2), --help, --check. Идемпотентен (не дёргает restart при неизменном юните).
|
||||
2. **t-261006175504 (M) API для поиска товаров по Shopify Collective — ГОТОВО.**
|
||||
Публичного Collective-поиска НЕ СУЩЕСТВУЕТ (searchShopifyCollectiveSuppliers отсутствует в схеме,
|
||||
404). Официально: приглашения/коннекты/просмотр каталогов — только UI приложений Collective; подтверждено
|
||||
стаффом Shopify (тред 2025-04) и changelog (свежего нет). Рабочий путь: авто-импорт policies → товары
|
||||
с тегом 'Shopify Collective' + {supplier_name} → Admin GraphQL products(query:"tag:...") + вебхуки
|
||||
products/create|update|delete; поставщиком — productCreate + publishablePublish в 'Collective (Supplier)'.
|
||||
Детали в ответе задачи (доставлено в TG).
|
||||
|
||||
## Сводка состояния
|
||||
|
||||
- Задачи: доска чиста от ready по этому батчу; tasks_done растёт (обе M закрыты с доставкой в TG).
|
||||
- Эскалации открытых: 0 (было 2).
|
||||
- Верхний контекст: стор здоров, DRY-инцидент закрыт без потери данных.
|
||||
|
|
@ -41,37 +41,6 @@ def _rag_tool_for(level: str, trace_id: str):
|
|||
return rag_lookup
|
||||
|
||||
|
||||
def _portal_mcp_tool_for(level: str, trace_id: str):
|
||||
"""Тул тика `portal_mcp` — MCP-шлюз портала кооператива (t-261010093054).
|
||||
|
||||
Звонки/расписание (booking_slots/booking_create), реестры, казна,
|
||||
лента звонков портала — живые данные cil.by прямо из тика. Клиент и
|
||||
лимиты — harness/portal_mcp.py (4 вызова на тик, таймаут 15с, трейс
|
||||
PortalMcpCalled); тул не поднимается без токена агента. Guardrail:
|
||||
деньги-тулы (product_create/product_set_mode) запрещены по умолчанию
|
||||
(SIMBIOT_MCP_ALLOW_ALL=1 снимает).
|
||||
"""
|
||||
from strands import tool as strands_tool
|
||||
from harness import portal_mcp as _mcp
|
||||
state = {"calls": 0}
|
||||
|
||||
@strands_tool
|
||||
def portal_mcp(tool: str, arguments: str = "") -> str:
|
||||
"""Вызов инструмента портала кооператива (MCP cil.by): расписание и
|
||||
создание звонков с Алексом (booking_slots → booking_create), реестры
|
||||
членов/проектов/агентов, казна, лента звонков и событий, whoami.
|
||||
Сначала вызови с tool="booking_slots", arguments='{"type": "call"}',
|
||||
слот передавай в booking_create как есть. arguments — JSON-строка.
|
||||
Лимит 4 вызова на тик. Ответ — JSON-текст портала; ошибки — "error: …"."""
|
||||
if state["calls"] >= _mcp.MAX_CALLS_PER_TICK:
|
||||
return (f"лимит: {_mcp.MAX_CALLS_PER_TICK} вызовов portal_mcp "
|
||||
f"на тик (использованы)")
|
||||
state["calls"] += 1
|
||||
return _mcp.call_tool(tool, arguments, level=level, trace_id=trace_id)
|
||||
|
||||
return portal_mcp
|
||||
|
||||
|
||||
def _wire_tool_budget(agent, max_calls: int = 30):
|
||||
"""Хард-кап тул-коллов на тик (e-260930031800: мин-воркер уходил в
|
||||
90+ шагов исследования и умирал по таймауту 300с).
|
||||
|
|
@ -295,12 +264,6 @@ def main():
|
|||
tools.append(_rag_tool_for(level, trace_id))
|
||||
except Exception:
|
||||
pass # RAG — best effort: вендор не должен валить тик
|
||||
# MCP портала (t-261010093054): booking_slots/booking_create, реестры,
|
||||
# казна, лента звонков — живой портал из тика; без токена тул не поднимется
|
||||
try:
|
||||
tools.append(_portal_mcp_tool_for(level, trace_id))
|
||||
except Exception:
|
||||
pass # портал — best effort: шлюз не должен валить тик
|
||||
# stdout воркера не читается (subprocess.run с capture_output и
|
||||
# длинным выводом переполняет pipe → BrokenPipeError в печати,
|
||||
# результат тика терялся, дайджесты 04:11-04:17 ушли пустыми).
|
||||
|
|
|
|||
|
|
@ -169,28 +169,6 @@ def archive_stale_failed(max_age_hours: float = 24.0, now=None) -> list:
|
|||
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"):
|
||||
"""Вернуть failed/running/прерванную задачу в очередь (TaskPromoted → ready).
|
||||
|
||||
|
|
@ -203,7 +181,7 @@ def requeue_task(task_id: str, reason: str = "manual requeue"):
|
|||
raise ValueError(f"задача {task_id} не найдена")
|
||||
# closed тоже возвращаем (авто-схлопывание старых фейлов, e-261003094432):
|
||||
# закрытая в архив задача может понадобиться снова — осознанная доработка.
|
||||
if t["status"] not in ("failed", "closed", "done", "running", "blocked"):
|
||||
if t["status"] not in ("failed", "closed", "done", "running"):
|
||||
raise ValueError(f"задача {task_id} в статусе {t['status']} — requeue не нужен")
|
||||
# идемпотентная доставка (t-260930083937): requeue = новая работа над
|
||||
# задачей, её результат должен уйти в чат заново → сбрасываем факт
|
||||
|
|
@ -373,7 +351,7 @@ def _delivery_text(task: dict, answer: str) -> str:
|
|||
|
||||
|
||||
def deliver_artifact(artifact_id: str, kind: str, send, owner: str = "manual",
|
||||
caption: str = "", force: bool = False, msg: str = None) -> bool:
|
||||
caption: str = "", force: bool = False) -> bool:
|
||||
"""Идемпотентная доставка ПРОИЗВОЛЬНОГО артефакта в чат (t-260930083937).
|
||||
|
||||
Рут-кейс дублей: мин-тик шёл файлом «скинь сюда» curl'ом sendDocument —
|
||||
|
|
@ -395,21 +373,6 @@ def deliver_artifact(artifact_id: str, kind: str, send, owner: str = "manual",
|
|||
"reason": "already-delivered",
|
||||
})
|
||||
return False
|
||||
# Анти-гонка уровней (t-261009160906): артефакт привязан к входящему
|
||||
# m-NNN, на которое уже отвечено ДРУГИМ артефактом → SKIP. Два уровня
|
||||
# (min/hour) на один запрос не должны слать два разных файла; dedup по
|
||||
# artifact_id=file:<путь> их не различает (разные пути = разные ключи).
|
||||
if msg and not force:
|
||||
model = es.run_projections()
|
||||
m = (model.get("inbox") or {}).get(msg)
|
||||
if (m is not None and m.get("replied")
|
||||
and m.get("reply_artifact") not in (None, "", artifact_id)):
|
||||
es.append_event("harness", artifact_id, "DeliverySkipped", {
|
||||
"artifact_id": artifact_id, "kind": kind,
|
||||
"reason": "msg-already-answered-by-other-artifact",
|
||||
"msg": msg, "reply_artifact": m.get("reply_artifact"),
|
||||
})
|
||||
return False
|
||||
if force:
|
||||
# ручной повтор — явное решение человека: лок недавней доставки
|
||||
# не должен блокировать его на TTL (120с). Снимаем свой же захват
|
||||
|
|
@ -431,13 +394,10 @@ def deliver_artifact(artifact_id: str, kind: str, send, owner: str = "manual",
|
|||
# в ledger остаётся фактом; повторный force просто отправит ещё раз,
|
||||
# но ledger при этом не «молодеет» (kind/ts не переписываются)
|
||||
es.record_delivery(artifact_id, kind=kind)
|
||||
ev = {
|
||||
es.append_event("harness", artifact_id, "ArtifactDelivered", {
|
||||
"artifact_id": artifact_id, "kind": kind, "tg_ok": True,
|
||||
"caption": (caption or "")[:500], "owner": owner,
|
||||
}
|
||||
if msg:
|
||||
ev["msg"] = msg # анти-гонка уровней (t-261009160906)
|
||||
es.append_event("harness", artifact_id, "ArtifactDelivered", ev)
|
||||
})
|
||||
return True
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -93,10 +93,6 @@
|
|||
.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-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) ── */
|
||||
.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}
|
||||
|
|
@ -160,7 +156,7 @@
|
|||
|
||||
<div class="view" id="view-chat">
|
||||
<h2 style="margin-top:0">Чат с Simbiot</h2>
|
||||
<div class="lead">сообщение уходит агенту по порталу-каналу, ответ придёт сам (обычно ≤ 1 мин) · каналы: <span id="chan-status"><span class="chan"><span class="dot wait"></span>каналы: узнаю…</span></span></div>
|
||||
<div class="lead">сообщение уходит агенту по порталу-каналу, ответ придёт сам (обычно ≤ 1 мин) · тоже дублируется в TG, если бот подключён</div>
|
||||
<div class="chat">
|
||||
<div class="chat-log" id="chat-log"><div class="skel">загружаю…</div></div>
|
||||
<div class="chat-input">
|
||||
|
|
@ -168,7 +164,7 @@
|
|||
<button class="chat-send" id="chat-send" onclick="chatSend()">Отправить</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="chat-note" id="chat-note">канал: портал (portal:<agent>) · ответ агент пишет сам по такту · <span id="chan-note">статус каналов: узнаю…</span></div>
|
||||
<div class="chat-note" id="chat-note">канал: портал (portal:<agent>) · ответ агент пишет сам по такту</div>
|
||||
</div><!-- /view-chat -->
|
||||
|
||||
<div class="view" id="view-agent">
|
||||
|
|
@ -459,46 +455,6 @@ function chatBubble(m){
|
|||
`<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(){
|
||||
if (getViewName() !== "chat") return;
|
||||
chatOpen = true;
|
||||
|
|
@ -513,7 +469,6 @@ async function chatTick(){
|
|||
log.innerHTML = html || `<div class="skel">сообщений пока нет — напиши первым</div>`;
|
||||
if (stick) log.scrollTop = log.scrollHeight;
|
||||
$("chat-note").textContent = "канал: портал (portal:" + (s.agent || "?") + ") · ответ агент пишет сам по такту";
|
||||
renderChannels(s.channels);
|
||||
}catch(e){
|
||||
if (e instanceof SyntaxError) return; // полу-JSON от 503 — следующий тик перепишет
|
||||
log.innerHTML = `<div class="skel">чат недоступен: ${esc(e.message)} — попробую через ${CHAT_REFRESH}с</div>`;
|
||||
|
|
|
|||
|
|
@ -28,10 +28,6 @@ from harness import commands, es # noqa: E402
|
|||
|
||||
_HERE = os.path.dirname(os.path.abspath(__file__))
|
||||
_INDEX = os.path.join(_HERE, "dashboard.html")
|
||||
_ROY_INDEX = os.path.join(_HERE, "roy.html")
|
||||
# ttyd (Claude Code) на агенте Roy — t-261008084337-3afbca
|
||||
ROY_TTYD_HOST = os.environ.get("SIMBIOT_ROY_TTYD_HOST", "178.104.52.234")
|
||||
ROY_TTYD_PORT = int(os.environ.get("SIMBIOT_ROY_TTYD_PORT", "7681"))
|
||||
|
||||
_ADDR = os.environ.get("SIMBIOT_DASH_ADDR", "0.0.0.0")
|
||||
_PORT = int(os.environ.get("SIMBIOT_DASH_PORT", "8765"))
|
||||
|
|
@ -39,7 +35,7 @@ _HEALTH_PORT = int(os.environ.get("SIMBIOT_DASH_HEALTH_PORT", "8766"))
|
|||
_TOKEN = os.environ.get("SIMBIOT_DASH_TOKEN", "")
|
||||
_REFRESH = int(os.environ.get("SIMBIOT_DASH_REFRESH", "5"))
|
||||
|
||||
_STATUS_ORDER = {"ready": 0, "running": 1, "blocked": 2, "failed": 3, "done": 4, "closed": 5}
|
||||
_STATUS_ORDER = {"ready": 0, "running": 1, "failed": 2, "done": 3, "closed": 4}
|
||||
_SIZE_COLOR = {"S": "#1d7a3e", "M": "#a3690f", "L": "#FF4F00", "XL": "#b3261e"}
|
||||
|
||||
# ── чат Simbiot прямо в дашборде (t-261001100740-99d55e) ──────────────────
|
||||
|
|
@ -187,33 +183,7 @@ def build_chat(limit: int = 80) -> dict:
|
|||
collapsed[-1]["ts"] = m["ts"]
|
||||
continue
|
||||
collapsed.append(m)
|
||||
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
|
||||
return {"ok": True, "agent": agent_name, "messages": collapsed[-limit:]}
|
||||
|
||||
|
||||
def chat_send(text: str) -> dict:
|
||||
|
|
@ -321,7 +291,7 @@ def build_state() -> dict:
|
|||
"tasks_total": len(tasks),
|
||||
"tasks_by_status": {
|
||||
s: sum(1 for t in tasks if t["status"] == s)
|
||||
for s in ("ready", "running", "blocked", "failed", "done", "closed")
|
||||
for s in ("ready", "running", "failed", "done", "closed")
|
||||
},
|
||||
"tasks_by_status_size": counts,
|
||||
"escalations_open": len(escalations),
|
||||
|
|
@ -545,142 +515,6 @@ class _Handler(BaseHTTPRequestHandler):
|
|||
self.end_headers()
|
||||
self.wfile.write(body)
|
||||
|
||||
# ── WebSocket-прокси на ttyd агента Roy (t-261008084337-3afbca) ────────
|
||||
def _roy_ws_proxy(self):
|
||||
"""Прозрачный WS-прокси клиент ↔ ws://178.104.52.234:7681/ws.
|
||||
|
||||
stdlib-only: свой минимальный WS-клиент (рукопожатие + чтение фреймов)
|
||||
+ переупаковка ответов ttyd (серверные фреймы без маски, клиенту
|
||||
маскируем). Управляющие фреймы проксируем на обе стороны.
|
||||
"""
|
||||
import base64 as _b64
|
||||
import socket as _socket
|
||||
import struct as _struct
|
||||
|
||||
host, port = ROY_TTYD_HOST, ROY_TTYD_PORT
|
||||
try:
|
||||
up = self.headers.get("Upgrade", "").lower()
|
||||
key = self.headers.get("Sec-WebSocket-Key", "")
|
||||
if up != "websocket" or not key:
|
||||
return self._json({"error": "expected websocket upgrade"}, code=400)
|
||||
version = self.headers.get("Sec-WebSocket-Version", "13")
|
||||
try:
|
||||
sk = _socket.create_connection((host, port), timeout=10)
|
||||
except OSError as e:
|
||||
return self._json({"error": f"ttyd {host}:{port} unreachable: {e}"},
|
||||
code=502)
|
||||
sk.settimeout(None)
|
||||
# рукопожатие с ttyd (без cookie: ttyd на Roy — credential=false)
|
||||
req = (f"GET /ws HTTP/1.1\r\nHost: {host}:{port}\r\n"
|
||||
"Upgrade: websocket\r\nConnection: Upgrade\r\n"
|
||||
f"Sec-WebSocket-Key: {key}\r\n"
|
||||
f"Sec-WebSocket-Version: {version}\r\n\r\n")
|
||||
sk.sendall(req.encode())
|
||||
buf = b""
|
||||
while b"\r\n\r\n" not in buf:
|
||||
chunk = sk.recv(4096)
|
||||
if not chunk:
|
||||
sk.close()
|
||||
return self._json({"error": "ttyd closed during handshake"},
|
||||
code=502)
|
||||
buf += chunk
|
||||
if len(buf) > 16384:
|
||||
sk.close()
|
||||
return self._json({"error": "ttyd handshake too large"}, code=502)
|
||||
head, rest = buf.split(b"\r\n\r\n", 1)
|
||||
status = head.split(b"\r\n", 1)[0].decode("latin-1", "replace")
|
||||
if " 101 " not in status + " ":
|
||||
sk.close()
|
||||
return self._json({"error": f"ttyd handshake failed: {status}"},
|
||||
code=502)
|
||||
# отвечаем клиенту тем же 101 (echo его ключа — валидация на браузере)
|
||||
resp_head = head.decode("latin-1", "replace")
|
||||
self.connection.sendall((resp_head + "\r\n\r\n").encode("latin-1"))
|
||||
|
||||
def _read_frame(sock, masked):
|
||||
hdr = b""
|
||||
while len(hdr) < 2:
|
||||
c = sock.recv(2 - len(hdr))
|
||||
if not c:
|
||||
raise ConnectionError("eof")
|
||||
hdr += c
|
||||
fin_op = hdr[0]
|
||||
op = fin_op & 0x0F
|
||||
ln = hdr[1] & 0x7F
|
||||
if ln == 126:
|
||||
ext = b""
|
||||
while len(ext) < 2:
|
||||
ext += sock.recv(2 - len(ext))
|
||||
ln = _struct.unpack(">H", ext)[0]
|
||||
elif ln == 127:
|
||||
ext = b""
|
||||
while len(ext) < 8:
|
||||
ext += sock.recv(8 - len(ext))
|
||||
ln = _struct.unpack(">Q", ext)[0]
|
||||
mask = b""
|
||||
if masked:
|
||||
while len(mask) < 4:
|
||||
mask += sock.recv(4 - len(mask))
|
||||
data = b""
|
||||
while len(data) < ln:
|
||||
data += sock.recv(min(65536, ln - len(data)))
|
||||
if masked:
|
||||
data = bytes(b ^ mask[i % 4] for i, b in enumerate(data))
|
||||
return op, data
|
||||
|
||||
def _send_frame(sock, op, data, mask_it):
|
||||
fin = 0x80 | op
|
||||
n = len(data)
|
||||
if n < 126:
|
||||
hdr = _struct.pack(">BB", fin, (0x80 if mask_it else 0) | n)
|
||||
elif n < 65536:
|
||||
hdr = _struct.pack(">BBH", fin, (0x80 if mask_it else 0) | 126, n)
|
||||
else:
|
||||
hdr = _struct.pack(">BBQ", fin, (0x80 if mask_it else 0) | 127, n)
|
||||
if mask_it:
|
||||
mk = os.urandom(4)
|
||||
hdr += mk
|
||||
data = bytes(b ^ mk[i % 4] for i, b in enumerate(data))
|
||||
sock.sendall(hdr + data)
|
||||
|
||||
# мост: ttyd→клиент в фоне, клиент→ttyd в текущем потоке
|
||||
self.connection.settimeout(None)
|
||||
import threading as _th
|
||||
|
||||
def _upstream():
|
||||
try:
|
||||
while True:
|
||||
op, data = _read_frame(sk, masked=False)
|
||||
_send_frame(self.connection, op, data, mask_it=False)
|
||||
except Exception:
|
||||
pass
|
||||
finally:
|
||||
try:
|
||||
self.connection.shutdown(_socket.SHUT_RDWR)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
t = _th.Thread(target=_upstream, daemon=True)
|
||||
t.start()
|
||||
try:
|
||||
while True:
|
||||
op, data = _read_frame(self.connection, masked=True)
|
||||
_send_frame(sk, op, data, mask_it=True)
|
||||
if op == 0x8: # close от клиента
|
||||
break
|
||||
except Exception:
|
||||
pass
|
||||
finally:
|
||||
try:
|
||||
sk.close()
|
||||
except Exception:
|
||||
pass
|
||||
except Exception as e: # прокси не должен ронять дашборд
|
||||
try:
|
||||
self._json({"error": f"ws proxy: {e}"}, code=502)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# ── routes ──
|
||||
def do_GET(self):
|
||||
route = self.path.partition("?")[0]
|
||||
|
|
@ -712,22 +546,6 @@ class _Handler(BaseHTTPRequestHandler):
|
|||
return self._json(build_agent(lvl))
|
||||
except Exception as e:
|
||||
return self._json({"error": str(e)[:500]}, code=503)
|
||||
if route == "/roy":
|
||||
# страница-обёртка ttyd/Claude Code на агенте Roy
|
||||
# (t-261008084337-3afbca): UI в дашборде, терминал — через /roy/ws
|
||||
if not self._authorized():
|
||||
return self._forbidden()
|
||||
try:
|
||||
with open(_ROY_INDEX, encoding="utf-8") as f:
|
||||
return self._html(f.read())
|
||||
except OSError as e:
|
||||
return self._json({"error": f"roy.html not found: {e}"}, code=500)
|
||||
if route.startswith("/roy/ws"):
|
||||
# прокси WebSocket → ttyd Roy (178.104.52.234:7681): пробрасываем
|
||||
# заголовки апгрейда, буферизуем фреймы как есть (Opcode 0-2/A).
|
||||
if not self._authorized():
|
||||
return self._forbidden()
|
||||
return self._roy_ws_proxy()
|
||||
if route == "/api/health":
|
||||
return self._json({"ok": True, "ts": _utcnow()})
|
||||
if route in ("/", "/index.html"):
|
||||
|
|
@ -792,145 +610,5 @@ def main():
|
|||
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__":
|
||||
main()
|
||||
|
||||
main()
|
||||
|
|
@ -295,7 +295,6 @@ def append_event(aggregate: str, aggregate_id: str, event_type: str, data: dict)
|
|||
version = (row["v"] or 0) + 1
|
||||
ts = datetime.now(timezone.utc).isoformat()
|
||||
data_str = json.dumps(data, ensure_ascii=False)
|
||||
sig_f = {}
|
||||
try:
|
||||
from . import audit as _audit
|
||||
prev = db.execute(
|
||||
|
|
@ -303,20 +302,6 @@ def append_event(aggregate: str, aggregate_id: str, event_type: str, data: dict)
|
|||
sig_f = _audit.sign_event_row(aggregate, aggregate_id, event_type,
|
||||
data_str, ts, version,
|
||||
prev["sig"] if prev else None)
|
||||
# t-261008084600 (rogue-key guard): стор с чужими подписями не подпишем
|
||||
# своим ключом. Инциденты 06.10/08.10: cli из /tmp-копии репо наследовал
|
||||
# SIMBIOT_HOME прод-стора, не находил рядом свой .audit-ключ, генерил
|
||||
# НОВЫЙ и вписывал его подпись в прод-стор → run_projections падал
|
||||
# «стор скомпрометирован» каждую секунду. Правило: если стор уже подписан,
|
||||
# key_id нового ключа обязан совпадать с key_id существующих событий;
|
||||
# иначе пишем без подписи (legacy-граница, проекции живы).
|
||||
if sig_f.get("key_id"):
|
||||
dom = db.execute(
|
||||
"SELECT key_id FROM events WHERE key_id IS NOT NULL "
|
||||
"ORDER BY id DESC LIMIT 1").fetchone()
|
||||
if dom and dom["key_id"] and dom["key_id"] != sig_f["key_id"]:
|
||||
sig_f = {}
|
||||
# чужой ключ → без подписи; верификатор пометит legacy
|
||||
except Exception:
|
||||
sig_f = {} # без подписи (legacy) — аудит отразит, харнес не падает
|
||||
try:
|
||||
|
|
@ -548,15 +533,6 @@ def _apply_event(model: dict, ev: dict):
|
|||
if m:
|
||||
m["replied"] = True
|
||||
m["reply_text"] = data.get("text", "")
|
||||
elif etype == "ArtifactDelivered":
|
||||
# Анти-гонка уровней (t-261009160906): связка «файл → входящее».
|
||||
# Если доставка артефакта помечена msg=m-NNN, фиксируем её в
|
||||
# инбоксе — второй артефакт на то же входящее будет SKIP'нут.
|
||||
msg = (data or {}).get("msg") if isinstance(data, dict) else None
|
||||
if msg:
|
||||
m = model.setdefault("inbox", {}).get(msg)
|
||||
if m is not None and not m.get("reply_artifact"):
|
||||
m["reply_artifact"] = data.get("artifact_id")
|
||||
elif etype == "MessageSeen":
|
||||
# индикация взятия в работу (e-261002202857): флаг seen → reaction 👀
|
||||
# ставится ровно один раз, повторные маркеры сообщений не найдут
|
||||
|
|
|
|||
|
|
@ -82,16 +82,6 @@ LEVELS = {
|
|||
"знаний кооператива (контекст, проекты, порталы, встречи, документы). "
|
||||
"Если вопрос про кооператив/продукты/документы — сначала rag_lookup "
|
||||
"(1 запрос за тик, формулируй целиком), потом отвечай по найденному. "
|
||||
"У тебя есть инструмент portal_mcp — живой портал кооператива cil.by "
|
||||
"(MCP): booking_slots/booking_create — свободные слоты и создание "
|
||||
"звонка с Алексом (jitsi-ссылка, гостю письмо, запись и транскрипция "
|
||||
"автоматом), list_calls/get_call — прошедшие звонки и саммари, "
|
||||
"list_members/projects_list/list_agents/treasury_status/list_events — "
|
||||
"реестры, казна, лента. Когда Alex просит назначить/перенести звонок "
|
||||
"или «посмотреть расписание» — это portal_mcp (до 4 вызовов за тик): "
|
||||
"сначала booking_slots (arguments '{\"type\": \"call\"}'), потом "
|
||||
"booking_create со слотом как есть. НЕ назначай звонки сам по "
|
||||
"инициативе — только по явной просьбе в чате. "
|
||||
"ВАЖНО: НЕ исследуй исходники харнеса (scheduler.py, commands.py, es.py, "
|
||||
"isolated_runner.py) и не разбирай падения тиков — это зона верхних уровней. "
|
||||
"Отвечай из контекста тика, максимум пара шагов, иначе таймаут тика. "
|
||||
|
|
@ -131,15 +121,7 @@ LEVELS = {
|
|||
"Файлы/уведомления в чат — только идемпотентно: cli.py tg-deliver-file "
|
||||
"<путь> -c \"подпись\", cli.py tg-notify \"текст\" -k <ключ> (повтор — SKIP). "
|
||||
"Прямые curl/api.telegram отправки из shell запрещены — нет защиты "
|
||||
"от дублей (t-260930083937). "
|
||||
"Инструмент portal_mcp — живой портал кооператива (MCP cil.by): "
|
||||
"booking_slots/booking_create (звонки с Алексом: слот → создание "
|
||||
"jitsi-звонка со ссылкой, гостю письмо), list_calls/get_call, "
|
||||
"list_members/projects_list/list_agents, treasury_status, list_events. "
|
||||
"Просьбы про назначение/перенос звонков и «что по расписанию» — "
|
||||
"через него (до 4 вызовов за тик). НЕ создавай звонки без явной "
|
||||
"просьбы в чате; деньги-тулы (product_create/product_set_mode) "
|
||||
"заблокированы по умолчанию."),
|
||||
"от дублей (t-260930083937)."),
|
||||
can_shell=True,
|
||||
),
|
||||
"day": CycleLevel(
|
||||
|
|
@ -150,11 +132,7 @@ LEVELS = {
|
|||
"Открытые эскалации (секция ЭСКАЛАЦИИ) — приоритет: это важное, "
|
||||
"поднятое с нижних уровней; закрой cli.py escalation-ack <id>. "
|
||||
"Файлы в чат — только cli.py tg-deliver-file (идемпотентно, "
|
||||
"повтор — SKIP); прямые curl/api.telegram отправки запрещены. "
|
||||
"Инструмент portal_mcp — живой портал кооператива (MCP cil.by): "
|
||||
"расписание и создание звонков (booking_slots → booking_create), "
|
||||
"реестры, казна, лента звонков; до 4 вызовов за тик, деньги-тулы "
|
||||
"заблокированы по умолчанию."),
|
||||
"повтор — SKIP); прямые curl/api.telegram отправки запрещены."),
|
||||
can_shell=True,
|
||||
),
|
||||
}
|
||||
|
|
|
|||
|
|
@ -109,23 +109,6 @@ def inbox_outbox_dir() -> str:
|
|||
return os.path.join(REPO_ROOT, "portal", "inbox", "outbox")
|
||||
|
||||
|
||||
# ── входящие файлы каналов (t-261005135853): documents/photo на диск ────────
|
||||
def inbox_files_dir() -> str:
|
||||
"""Каталог входящих файлов (documents/photo из TG).
|
||||
|
||||
Голосовые живут в voice/ (кэш ASR), а документы/фото — данные уровня
|
||||
инбокса: выписки, сканы, артефакты Алекса. Не задаём SIMBIOT_HOME
|
||||
(legacy) → /var/lib/simbiot-harness/inbox/files — тот же корень, где
|
||||
голосовые (voice_dir legacy), рядом с inbox/ каналов портала.
|
||||
"""
|
||||
env = os.environ.get("SIMBIOT_INBOX_FILES_DIR", "").strip()
|
||||
if env:
|
||||
return env
|
||||
if IS_ISOLATED:
|
||||
return os.path.join(_HOME, "inbox", "files")
|
||||
return "/var/lib/simbiot-harness/inbox/files"
|
||||
|
||||
|
||||
# ── runtime-штампы каналов для дашборда ─────────────────────────────────────
|
||||
def stamp_dir() -> str:
|
||||
if IS_ISOLATED:
|
||||
|
|
@ -143,22 +126,6 @@ def voice_dir() -> str:
|
|||
return "/var/lib/simbiot-harness/voice" # legacy
|
||||
|
||||
|
||||
# ── токен агентского MCP портала (t-261010093054) ──────────────────────────
|
||||
def portal_mcp_token_path() -> str:
|
||||
"""Путь к токену MCP-агента портала (см. harness/portal_mcp.py).
|
||||
|
||||
Данные инстанса, не код: secrets/ в SIMBIOT_HOME, legacy — файл
|
||||
.simbiot-portal-token в корне репо (выдан при подключении Simbiot
|
||||
как MCP-агента кооператива, 02.10).
|
||||
"""
|
||||
env = os.environ.get("SIMBIOT_PORTAL_MCP_TOKEN_FILE", "").strip()
|
||||
if env:
|
||||
return env
|
||||
if IS_ISOLATED:
|
||||
return os.path.join(_HOME, "secrets", "portal-mcp-token")
|
||||
return os.path.join(REPO_ROOT, ".simbiot-portal-token")
|
||||
|
||||
|
||||
def ensure_instance_dirs() -> list:
|
||||
"""Создать структуру хома изолированного инстанса. Идемпотентно.
|
||||
|
||||
|
|
@ -168,8 +135,7 @@ def ensure_instance_dirs() -> list:
|
|||
if not IS_ISOLATED:
|
||||
return []
|
||||
made = []
|
||||
for sub in ("db", "rag", "env", "inbox", "digests", "audit", "docs",
|
||||
"voice", "run", "secrets"):
|
||||
for sub in ("db", "rag", "env", "inbox", "digests", "audit", "docs", "voice", "run"):
|
||||
p = os.path.join(_HOME, sub)
|
||||
if not os.path.isdir(p):
|
||||
os.makedirs(p, exist_ok=True)
|
||||
|
|
|
|||
|
|
@ -1,227 +0,0 @@
|
|||
"""Simbiot Harness — MCP-клиент портала кооператива (t-261010093054).
|
||||
|
||||
Портал cil.by — хаб кооператива: звонки/встречи (booking_slots /
|
||||
booking_create), лента звонков (list_calls / get_call), реестры
|
||||
(members/projects/agents), казна, заявки, события. Всё это доступно
|
||||
агентам через MCP-шлюз https://cil.by/mcp (Streamable HTTP, JSON-RPC 2.0,
|
||||
см. /root/coop/MCP_CONNECT.md) — но тики харнеса раньше имели только
|
||||
rag_lookup по локальной базе знаний: живого портала у LLM-тиков не было,
|
||||
звонки Alex создавал руками через UI.
|
||||
|
||||
Этот модуль — тонкий клиент шлюза + материал для тула тика `portal_mcp`
|
||||
(обёртка в _llm_worker._portal_mcp_tool_for):
|
||||
|
||||
- токен агента: env SIMBIOT_PORTAL_MCP_TOKEN → файл secrets/portal-mcp-token
|
||||
инстанса (legacy — .simbiot-portal-token в корне репо). Токена нет →
|
||||
тул не поднимается, тик живёт как раньше;
|
||||
- лимиты зашиты в код (не в промпт): 4 вызова на тик (каждый тик — новый
|
||||
процесс воркера), таймаут 15с на запрос, tools/list кешируется на процесс;
|
||||
- guardrail: деньги-тулы (product_create / product_set_mode — создание
|
||||
Stripe-объектов и переключение test↔live) по умолчанию ЗАПРЕЩЕНЫ;
|
||||
разблокировать — SIMBIOT_MCP_ALLOW_ALL=1. Остальное — стандартный
|
||||
спектр портала: booking_* / list_* / get_* / whoami / report_*;
|
||||
- ошибки шлюза НЕ валят тик: возвращаются текстом "error: …";
|
||||
- трейс PortalMcpCalled в event store (тул/ок/мс) — наблюдаемость как у
|
||||
RagQueried: часовой уровень видит в событиях, что тик ходил в портал.
|
||||
|
||||
Клиент — чистый urllib (зависимостей нет), id запроса монотонный в рамках
|
||||
процесса. Шлюз идемпотентен по своей природе: tools/list — чтение,
|
||||
booking_create на занятый слот честно отвечает 409.
|
||||
"""
|
||||
import json
|
||||
import os
|
||||
import time
|
||||
import urllib.error
|
||||
import urllib.request
|
||||
|
||||
from . import paths as _paths # t-261004113351: секреты/данные инстанса
|
||||
|
||||
# ── конфиг ──────────────────────────────────────────────────────────────────
|
||||
DEFAULT_URL = "https://cil.by/mcp"
|
||||
TIMEOUT = 15 # сек на запрос: шлюз локальный, но пишет в Stripe/SMTP
|
||||
MAX_CALLS_PER_TICK = 4 # звонки/расписание не требуют длинных цепочек
|
||||
MAX_RESULT_CHARS = 6000 # выжимка для тика: транскрипции бывают большими
|
||||
|
||||
# тулы, создающие/переключающие Stripe-объекты (деньги) — руками Alex;
|
||||
# SIMBIOT_MCP_ALLOW_ALL=1 снимает запрет (осознанный выбор инстанса)
|
||||
_BLOCKED_TOOLS = {"product_create", "product_set_mode"}
|
||||
|
||||
_req_seq = 0
|
||||
_tools_cache = None # [имена] — tools/list на процесс (каждый тик — новый)
|
||||
|
||||
|
||||
def _load_token() -> str:
|
||||
"""Токен агентского MCP: env сильнее файла; home-инстанс без своего
|
||||
файла (secrets/portal-mcp-token) откатывается к legacy-токену репо —
|
||||
миграции секретов не должны отключать уже подключённого агента."""
|
||||
tok = os.environ.get("SIMBIOT_PORTAL_MCP_TOKEN", "").strip()
|
||||
if tok:
|
||||
return tok
|
||||
p = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))),
|
||||
".simbiot-portal-token")
|
||||
try:
|
||||
v = open(p).read().strip()
|
||||
if v:
|
||||
return v
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
with open(_paths.portal_mcp_token_path()) as f:
|
||||
return f.read().strip()
|
||||
except Exception:
|
||||
return ""
|
||||
|
||||
|
||||
def _url() -> str:
|
||||
return os.environ.get("SIMBIOT_PORTAL_MCP_URL", "").strip() or DEFAULT_URL
|
||||
|
||||
|
||||
class McpError(Exception):
|
||||
"""Сбой запроса к шлюзу (сеть/HTTP/JSON) — тику вернётся текстом, не рэйзом."""
|
||||
|
||||
|
||||
def _rpc(method: str, params: dict, timeout: int = TIMEOUT) -> dict:
|
||||
"""Один JSON-RPC запрос к шлюзу. Возвращает result (без envelope)."""
|
||||
global _req_seq
|
||||
_req_seq += 1
|
||||
body = json.dumps({"jsonrpc": "2.0", "id": _req_seq,
|
||||
"method": method, "params": params or {}}).encode()
|
||||
req = urllib.request.Request(
|
||||
_url(), data=body,
|
||||
headers={"Content-Type": "application/json",
|
||||
"Authorization": f"Bearer {_load_token()}"})
|
||||
try:
|
||||
with urllib.request.urlopen(req, timeout=timeout) as r:
|
||||
envelope = json.loads(r.read().decode())
|
||||
except urllib.error.HTTPError as e:
|
||||
raise McpError(f"HTTP {e.code} от {_url()}: {e.read()[:300]!r}")
|
||||
except Exception as e:
|
||||
raise McpError(f"шлюз недоступен ({e})")
|
||||
if not isinstance(envelope, dict):
|
||||
raise McpError(f"шлюз вернул не-JSON-RPC ответ: {str(envelope)[:200]}")
|
||||
if "error" in envelope and envelope["error"]:
|
||||
err = envelope["error"]
|
||||
raise McpError(f"{err.get('message') or err} (code={err.get('code')})")
|
||||
return envelope.get("result") or {}
|
||||
|
||||
|
||||
def _known_tools() -> list:
|
||||
"""Имена тулов шлюза (кеш на процесс). Сбой — пустой список (см. call)."""
|
||||
global _tools_cache
|
||||
if _tools_cache is None:
|
||||
try:
|
||||
spec = _rpc("tools/list", {})
|
||||
_tools_cache = [t.get("name") for t in (spec.get("tools") or [])
|
||||
if t.get("name")]
|
||||
except Exception:
|
||||
_tools_cache = []
|
||||
return _tools_cache
|
||||
|
||||
|
||||
def _trace(level, trace_id, payload: dict):
|
||||
"""Трейс MCP-вызова в стор — spend/observability едины с LLM-тиками."""
|
||||
if not (level and trace_id):
|
||||
return
|
||||
try:
|
||||
from . import es
|
||||
seq = len(_trace_log) + 1
|
||||
es.append_event("trace", f"{trace_id}:mcp{seq}", "PortalMcpCalled", {
|
||||
**payload, "level": level,
|
||||
})
|
||||
_trace_log.append(seq)
|
||||
except Exception:
|
||||
pass # трейс — best effort, не валить тик
|
||||
|
||||
|
||||
_trace_log: list = []
|
||||
|
||||
|
||||
def _coerce_arguments(arguments) -> dict:
|
||||
"""Аргументы тулу: dict как есть; строка — JSON (модель так и шлёт)."""
|
||||
if arguments is None or arguments == "":
|
||||
return {}
|
||||
if isinstance(arguments, dict):
|
||||
return arguments
|
||||
try:
|
||||
v = json.loads(arguments)
|
||||
return v if isinstance(v, dict) else {"value": v}
|
||||
except Exception:
|
||||
raise McpError("arguments — невалидный JSON; передавайте объект из "
|
||||
"описания тула (например {\"type\": \"call\"})")
|
||||
|
||||
|
||||
def call_tool(name: str, arguments=None, level: str = None,
|
||||
trace_id: str = None, timeout: int = TIMEOUT) -> str:
|
||||
"""Вызвать тул портала. ВСЕГДА возвращает строку (тик не валится).
|
||||
|
||||
Ошибки (сеть, guardrail, 409 занятого слота) — текстом «error: …»:
|
||||
LLM-тик читает их как обычный результат и реагирует по смыслу.
|
||||
"""
|
||||
started = time.time()
|
||||
ok, result = False, ""
|
||||
|
||||
def _finish(res: str, _ok: bool) -> str:
|
||||
_trace(level, trace_id, {
|
||||
"tool": name, "ok": _ok,
|
||||
"ms": int((time.time() - started) * 1000),
|
||||
"result_preview": res[:300],
|
||||
})
|
||||
return res
|
||||
|
||||
token = _load_token()
|
||||
if not token:
|
||||
return _finish(
|
||||
"error: MCP портала не настроен — нет токена "
|
||||
"(SIMBIOT_PORTAL_MCP_TOKEN или secrets/portal-mcp-token)", False)
|
||||
if name not in _BLOCKED_TOOLS or os.environ.get("SIMBIOT_MCP_ALLOW_ALL"):
|
||||
pass # разрешено
|
||||
else:
|
||||
return _finish(
|
||||
f"error: тул {name} запрещён на этом харнесе по умолчанию "
|
||||
f"(деньги/Stripe: создаёт или переключает платёжные объекты — "
|
||||
f"руками Alex; разблокировка SIMBIOT_MCP_ALLOW_ALL=1)", False)
|
||||
try:
|
||||
args = _coerce_arguments(arguments)
|
||||
except McpError as e:
|
||||
return _finish(f"error: {e}", False)
|
||||
|
||||
known = _known_tools()
|
||||
if known and name not in known:
|
||||
return _finish(
|
||||
f"error: тула {name} нет на шлюзе; доступные: "
|
||||
f"{', '.join(known)}", False)
|
||||
try:
|
||||
res = _rpc("tools/call", {"name": name, "arguments": args}, timeout)
|
||||
except McpError as e:
|
||||
return _finish(f"error: {e}", False)
|
||||
ok = True
|
||||
try:
|
||||
# MCP content-массив: шлюз портала отдаёт [{type: "text", "text": ...}]
|
||||
content = res.get("content") or []
|
||||
texts = [c.get("text", "") for c in content if isinstance(c, dict)]
|
||||
result = "\n".join(t for t in texts if t).strip() or json.dumps(res, ensure_ascii=False)
|
||||
except Exception:
|
||||
result = json.dumps(res, ensure_ascii=False)
|
||||
if len(result) > MAX_RESULT_CHARS:
|
||||
result = result[:MAX_RESULT_CHARS] + f"\n…(обрезано, всего {len(result)} симв.)"
|
||||
return _finish(result, ok)
|
||||
|
||||
|
||||
def list_tools(force: bool = False) -> list:
|
||||
"""Имена тулов шлюза (для CLI и подсказок в промпте)."""
|
||||
global _tools_cache
|
||||
if force:
|
||||
_tools_cache = None
|
||||
return _known_tools()
|
||||
|
||||
|
||||
def describe_tools(max_names: int = 12) -> str:
|
||||
"""Короткая шпаргалка по тулам портала для промптов уровней."""
|
||||
return ("booking_slots(type='call'|'session') — свободные слоты звонка с "
|
||||
"Алексом; booking_create(start_utc из слота, guest_name, guest_email, "
|
||||
"kind='call'|'session') — создать jitsi-звонок со ссылкой (гостю "
|
||||
"письмо, Алексу — в TG, запись+транскрипция автоматом); booking_cancel"
|
||||
"(meeting_id); list_calls/get_call — прошедшие звонки и их саммари; "
|
||||
"list_members/projects_list/list_agents/treasury_status/list_events — "
|
||||
"реестры и казна; whoami/report_task/report_event/read_context — "
|
||||
"идентичность и лента портала.")
|
||||
|
|
@ -1,83 +0,0 @@
|
|||
<!doctype html>
|
||||
<html lang="ru">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Simbiot · Roy — Claude Code (ttyd)</title>
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/xterm@5.3.0/css/xterm.min.css">
|
||||
<style>
|
||||
html,body{margin:0;height:100%;background:#161b22;color:#e6edf3;font-family:system-ui,sans-serif}
|
||||
#bar{display:flex;align-items:center;gap:10px;padding:8px 12px;background:#0d1117;border-bottom:1px solid #30363d}
|
||||
#bar b{font-size:14px}
|
||||
#st{margin-left:auto;font-size:12px;color:#8b949e}
|
||||
#st.ok{color:#3fb950}#st.err{color:#f85149}
|
||||
#term{padding:6px;height:calc(100% - 42px);box-sizing:border-box}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="bar"><b>Roy · 178.104.52.234 · Claude Code (ttyd)</b>
|
||||
<span id="st">подключение…</span></div>
|
||||
<div id="term"></div>
|
||||
<script src="https://cdn.jsdelivr.net/npm/xterm@5.3.0/lib/xterm.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/xterm-addon-fit@0.8.0/lib/xterm-addon-fit.min.js"></script>
|
||||
<script>
|
||||
(async () => {
|
||||
const term = new Terminal({cursorBlink:true, fontSize:14, theme:{background:'#161b22'}});
|
||||
const fit = new FitAddon.FitAddon();
|
||||
term.loadAddon(fit);
|
||||
term.open(document.getElementById('term'));
|
||||
fit.fit();
|
||||
addEventListener('resize', () => fit.fit());
|
||||
|
||||
// токен — как у дашборда: из cookie или query этой страницы
|
||||
let token = '';
|
||||
const q = new URLSearchParams(location.search).get('token');
|
||||
if (q) { token = q; document.cookie = 'simbiot_token=' + encodeURIComponent(q); }
|
||||
else { const m = document.cookie.match(/(?:^|;\s*)simbiot_token=([^;]+)/); if (m) token = decodeURIComponent(m[1]); }
|
||||
const wsUrl = (location.protocol==='https:'?'wss://':'ws://') + location.host
|
||||
+ '/roy/ws' + (token ? '?token=' + encodeURIComponent(token) : '');
|
||||
const st = document.getElementById('st');
|
||||
|
||||
// ttyd-протокол: первый JSON-фрейм {AuthToken:'', Columns:, Rows:}
|
||||
const ws = new WebSocket(wsUrl);
|
||||
ws.binaryType = 'arraybuffer';
|
||||
let initSent = false;
|
||||
ws.onopen = () => {
|
||||
ws.send(JSON.stringify({AuthToken:'', Columns:term.cols, Rows:term.rows}));
|
||||
initSent = true;
|
||||
st.textContent = 'онлайн'; st.className = 'ok';
|
||||
term.focus();
|
||||
};
|
||||
ws.onmessage = ev => {
|
||||
const d = new Uint8Array(ev.data instanceof ArrayBuffer ? ev.data : new TextEncoder().encode(ev.data));
|
||||
if (!d.length) return;
|
||||
if (d[0] === 0x00) { // OUTPUT: печатаем как есть
|
||||
term.write(new Uint8Array(d.buffer, 1));
|
||||
} else if (d[0] === 0x01) { // PING
|
||||
const p = new Uint8Array(2); p[0] = 0x02; ws.send(p);
|
||||
}
|
||||
};
|
||||
ws.onclose = () => {
|
||||
st.textContent = initSent ? 'связь потеряна — обновите страницу' : 'нет доступа (токен?)';
|
||||
st.className = 'err';
|
||||
};
|
||||
term.onData(data => {
|
||||
// INPUT: 0x00 + utf-8
|
||||
const p = new Uint8Array(1 + data.length);
|
||||
p[0] = 0x00;
|
||||
for (let i = 0; i < data.length; i++) p[1+i] = data.charCodeAt(i) & 0xFF;
|
||||
ws.send(p);
|
||||
});
|
||||
// resize: WINDOW_RESIZE 0x02 + json {Columns,Rows}
|
||||
term.onResize(({cols, rows}) => {
|
||||
if (ws.readyState !== 1) return;
|
||||
const j = JSON.stringify({Columns:cols, Rows:rows});
|
||||
const p = new Uint8Array(1 + j.length);
|
||||
p[0] = 0x02;
|
||||
for (let i = 0; i < j.length; i++) p[1+i] = j.charCodeAt(i);
|
||||
ws.send(p);
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -271,16 +271,8 @@ def build_context(level_cfg, model: dict, batch=None):
|
|||
digests.sort(key=lambda d: d["ts"], reverse=True)
|
||||
if digests:
|
||||
parts.append("ВЫЖИМКИ НИЖНИХ УРОВНЕЙ (последние, сжатые):")
|
||||
# дедуп по тексту (эксп. #1, t-261005121525): тихий мин-уровень давал
|
||||
# 6 идентичных [gate:quiet]-строк в каждом шаге промпта — чистый burn.
|
||||
# Теперь повтор схлопывается в одну строку с счётчиком ×N.
|
||||
seen: dict = {}
|
||||
for d in digests[:6]:
|
||||
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}")
|
||||
parts.append(f"- [{d['level']}] {d['text'][:400]}")
|
||||
|
||||
# эскалации: открытые, адресованные этому уровню и выше — в приоритете
|
||||
escs = []
|
||||
|
|
@ -484,36 +476,25 @@ def run_level_tick(level_cfg, seq: int, model: dict):
|
|||
claimed = None
|
||||
prompt = build_context(level_cfg, model, batch)
|
||||
# Индикация «взял и работаю» (e-261002202857/73bbaf): пока LLM-тик гоняется,
|
||||
# фоновый поток держит sendChatAction typing (обновление каждые ~2с) —
|
||||
# «печатает…» виден весь тик. Реакция 👀 на конкретные входящие — поллером
|
||||
# при приёме (MessageSeen) и по требованию cli.py tg-seen.
|
||||
# t-261010092931: раньше поток поднимался ТОЛЬКО при непросмотренном
|
||||
# pending-инбоксе — с пустым/просмотренным инбоксом индикатора не было весь
|
||||
# тик. Теперь «печатает…» горит весь тик LLM независимо от pending и от
|
||||
# claimed-задач: чаты берём из всего инбокса модели (seen/replied не важны),
|
||||
# а при пустом инбоксе — последний живой TG-чат харнеса (_default_chat_id,
|
||||
# личка Alex). typing — только TG-чаты: портал-каналу и TUI typing API
|
||||
# недоступен. Останов потока — в finally в конце функции.
|
||||
# фоновый поток держит sendChatAction typing в TG-чатах инбокса (обновление
|
||||
# каждые ~2с) — «печатает…» виден весь тик. Реакция 👀 на конкретные
|
||||
# входящие — поллером при приёме (MessageSeen) и по требованию cli.py tg-seen.
|
||||
# Останов потока — в finally в конце функции.
|
||||
from . import tg_inbox
|
||||
keepalive = None
|
||||
keep_stop = threading.Event()
|
||||
tg_chat_ids = list({m.get("chat_id") for m in (model.get("inbox") or {}).values()
|
||||
if m.get("chat_id")
|
||||
and not str(m.get("chat_id")).startswith(("portal", "tui"))})
|
||||
if not tg_chat_ids:
|
||||
# пустой инбокс — показываем «печатает…» в чате харнеса, если он известен
|
||||
try:
|
||||
_dc = tg_inbox._default_chat_id()
|
||||
except Exception:
|
||||
_dc = None
|
||||
if _dc is not None:
|
||||
tg_chat_ids = [_dc]
|
||||
if tg_chat_ids:
|
||||
keepalive = threading.Thread(
|
||||
target=tg_inbox.typing_keepalive,
|
||||
args=(tg_chat_ids, keep_stop),
|
||||
name=f"keepalive-{level_cfg.name}-{seq}", daemon=True)
|
||||
keepalive.start()
|
||||
pending = [m for m in _pending_inbox(model) if not m.get("seen")]
|
||||
if pending:
|
||||
# typing — только TG-чаты (портал-каналу typing API недоступен)
|
||||
tg_chat_ids = list({m.get("chat_id") for m in pending
|
||||
if m.get("chat_id")
|
||||
and not str(m.get("chat_id")).startswith("portal")})
|
||||
if tg_chat_ids:
|
||||
keepalive = threading.Thread(
|
||||
target=tg_inbox.typing_keepalive,
|
||||
args=(tg_chat_ids, keep_stop),
|
||||
name=f"keepalive-{level_cfg.name}-{seq}", daemon=True)
|
||||
keepalive.start()
|
||||
# весь хвост тика — под finally: keepalive гасим на любом исходе
|
||||
# (успех / worker-сбой / исключение), поток не переживает свой тик.
|
||||
try:
|
||||
|
|
@ -862,13 +843,8 @@ def _reflex_tick(level_cfg, seq: int, model: dict):
|
|||
period = datetime.fromtimestamp(seq * level_cfg.every, tz=timezone.utc)
|
||||
payload = {"level": "sec", "seq": seq, "load1": load,
|
||||
"ts": period.isoformat()}
|
||||
# алерт-порог: по умолчанию 0.75*nproc (16 ядер → 12; t-261012090606),
|
||||
# policy cpu_alert_threshold переопределяет при наличии
|
||||
try:
|
||||
_default_thr = max(8.0, 0.75 * os.cpu_count())
|
||||
except Exception:
|
||||
_default_thr = 12.0
|
||||
thr = (model.get("policy") or {}).get("cpu_alert_threshold", _default_thr)
|
||||
# алерт-порог из policy (по умолчанию 8 — как simbiotd)
|
||||
thr = (model.get("policy") or {}).get("cpu_alert_threshold", 8)
|
||||
if load is not None and load >= thr:
|
||||
payload["alert"] = f"load {load:.2f} >= {thr}"
|
||||
# без-LLM команды: отвечаем на входящие-команды без ответа
|
||||
|
|
|
|||
|
|
@ -99,7 +99,7 @@ def cmd_status() -> str:
|
|||
st = cs.get(name) or {}
|
||||
err = f", ошибок {st['errors']}" if st.get("errors") else ""
|
||||
lines.append(f" {name}: тиков {st.get('ticks', 0)}{err}")
|
||||
parts = [f"{k} {counts[k]}" for k in ("ready", "running", "blocked", "failed", "done") if counts.get(k)]
|
||||
parts = [f"{k} {counts[k]}" for k in ("ready", "running", "done", "failed") if counts.get(k)]
|
||||
lines.append("📋 задачи: " + (" · ".join(parts) if parts else "нет"))
|
||||
lines.append(f"🚨 эскалаций открытых: {open_esc}")
|
||||
lines.append(f"💰 спенд (последние {len(spend)} тиков): ~{tokens:,} токенов".replace(",", " "))
|
||||
|
|
@ -208,21 +208,9 @@ def commands_spec(latin: bool = False) -> list:
|
|||
|
||||
|
||||
def register_bot_commands():
|
||||
"""setMyCommands: latin-имена + кириллические подписи (без 400-пробы).
|
||||
"""setMyCommands: сначала кириллица, при отказе — транслит.
|
||||
|
||||
Telegram Bot API принимает в ИМЕНАХ команд только [a-z0-9_]:
|
||||
кириллическое имя → 400 BOT_COMMAND_INVALID (подтверждено телом
|
||||
ошибки API, t-261004160532-690fcb) — «сначала кириллица, потом
|
||||
фолбэк» гарантированно падал первым вызовом при каждом старте.
|
||||
В описаниях кириллица валидна, поэтому регистрируем сразу
|
||||
единственно возможную комбинацию: latin-имя + кириллическая
|
||||
подпись — именно её юзер и видит в меню «/».
|
||||
Кириллицей команды остаются доступными и без меню: парсер
|
||||
_ALIASES понимает слова «лестница», «статус», «задачи»,
|
||||
«помощь» (без слэша) и /лестница-подобные токены.
|
||||
|
||||
Возвращает 'cyrillic' (подписи кириллицей — то, что видит юзер)
|
||||
или None (нет токена / не удалось).
|
||||
Возвращает 'cyrillic' | 'latin' | None (нет токена/не удалось).
|
||||
Повторный вызов идемпотентен — перезапишет список тем же содержимым.
|
||||
"""
|
||||
from . import tg_inbox
|
||||
|
|
@ -231,13 +219,18 @@ def register_bot_commands():
|
|||
return None
|
||||
try:
|
||||
r = tg_inbox._api("setMyCommands",
|
||||
{"commands": json.dumps(commands_spec(latin=True), ensure_ascii=False)})
|
||||
{"commands": json.dumps(commands_spec(), ensure_ascii=False)})
|
||||
if r.get("ok"):
|
||||
return "cyrillic"
|
||||
print(f"[tg-cmd] setMyCommands отклонён: {str(r)[:200]}", flush=True)
|
||||
raise RuntimeError(str(r)[:200])
|
||||
except Exception as e:
|
||||
print(f"[tg-cmd] setMyCommands error: {e}", flush=True)
|
||||
return None
|
||||
print(f"[tg-cmd] setMyCommands: кириллица не принята ({e}) — пробую транслит", flush=True)
|
||||
try:
|
||||
r = tg_inbox._api("setMyCommands",
|
||||
{"commands": json.dumps(commands_spec(latin=True), ensure_ascii=False)})
|
||||
return "latin" if r.get("ok") else None
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
|
||||
def get_my_commands() -> list:
|
||||
|
|
|
|||
|
|
@ -140,88 +140,6 @@ def typing_keepalive(chat_ids: list, stop, interval: float = 2.0):
|
|||
pass
|
||||
|
||||
|
||||
# ── входящие документы/фото → диск (t-261005135853) ─────────────────────────
|
||||
# Голосовые качает voice-пайплайн (voice/, кэш ASR), а documents/photo раньше
|
||||
# терялись: file_id есть в событии, файл не скачивался (кейс 4 PDF-выписок
|
||||
# Alex 05.10 — доставал вручную getFile'ом). Теперь гейтвей сам кладёт файл
|
||||
# в inbox/files/<file_unique_id>.<ext> и пишет путь в MessageReceived.
|
||||
_SAFE_EXT_RE = None # ленивый импорт re
|
||||
|
||||
|
||||
def _file_ext(name: str, mime: str = "") -> str:
|
||||
"""Расширение из имени/mime, безопасное (без путей и мусора)."""
|
||||
global _SAFE_EXT_RE
|
||||
if _SAFE_EXT_RE is None:
|
||||
import re
|
||||
_SAFE_EXT_RE = re.compile(r"[^A-Za-z0-9]")
|
||||
ext = ""
|
||||
if name and "." in name:
|
||||
ext = name.rsplit(".", 1)[-1]
|
||||
if not ext and "/" in (mime or ""):
|
||||
sub = mime.split("/", 1)[-1]
|
||||
ext = {"pdf": "pdf", "jpeg": "jpg", "png": "png", "webp": "webp",
|
||||
"octet-stream": "bin"}.get(sub, sub)
|
||||
ext = _SAFE_EXT_RE.sub("", ext)[:8]
|
||||
return ext or "bin"
|
||||
|
||||
|
||||
def save_document(msg: dict):
|
||||
"""Скачать document/photo из сообщения → inbox/files/.
|
||||
|
||||
Возвращает абсолютный путь к файлу или None: сбой скачивания не должен
|
||||
валить поллер — путь просто не попадёт в событие, file_id в нём
|
||||
останется для ручного достёгивания (как было до фикса).
|
||||
Идемпотентно: file_unique_id в имени → повторный апдейт не перекачивает
|
||||
(файл на диске = готовый ответ).
|
||||
"""
|
||||
doc = msg.get("document")
|
||||
photo = msg.get("photo")
|
||||
if not doc and not photo:
|
||||
return None
|
||||
if doc:
|
||||
file_id = doc.get("file_id")
|
||||
uniq = doc.get("file_unique_id") or file_id
|
||||
ext = _file_ext(doc.get("file_name") or "", doc.get("mime_type") or "")
|
||||
else:
|
||||
biggest = photo[-1] # самый большой размер
|
||||
file_id = biggest.get("file_id")
|
||||
uniq = biggest.get("file_unique_id") or file_id
|
||||
ext = "jpg"
|
||||
if not file_id:
|
||||
return None
|
||||
files_dir = _paths.inbox_files_dir()
|
||||
abs_path = os.path.join(files_dir, f"{uniq}.{ext}")
|
||||
if not os.path.exists(abs_path): # кэш: один файл — одна закачка
|
||||
try:
|
||||
_load_env()
|
||||
if not TG_TOKEN:
|
||||
return None
|
||||
resp = _api("getFile", {"file_id": file_id})
|
||||
if not resp.get("ok"):
|
||||
return None
|
||||
fp = (resp.get("result") or {}).get("file_path")
|
||||
if not fp:
|
||||
return None
|
||||
durl = f"https://api.telegram.org/file/bot{TG_TOKEN}/{fp}"
|
||||
with urllib.request.urlopen(durl, timeout=90) as r:
|
||||
blob = r.read()
|
||||
if not blob:
|
||||
return None
|
||||
os.makedirs(files_dir, exist_ok=True)
|
||||
tmp = abs_path + ".part"
|
||||
with open(tmp, "wb") as f:
|
||||
f.write(blob)
|
||||
os.replace(tmp, abs_path) # атомарно: нет наполовину записанных
|
||||
except urllib.error.HTTPError as e:
|
||||
# 400/410 = file истёк у TG — не ошибка поллера, тихий отказ
|
||||
print(f"[tg] getFile HTTP {e.code}: файл {uniq} недоступен", flush=True)
|
||||
return None
|
||||
except Exception as e:
|
||||
print(f"[tg] download {uniq} error: {e}", flush=True)
|
||||
return None
|
||||
return abs_path
|
||||
|
||||
|
||||
def receive_message(update: dict):
|
||||
"""MessageReceived в стор. Идемпотентно по update_id."""
|
||||
msg = update.get("message") or update.get("edited_message") or {}
|
||||
|
|
@ -252,15 +170,6 @@ def receive_message(update: dict):
|
|||
photo = msg.get("photo")
|
||||
if photo and not file_id:
|
||||
file_id = photo[-1].get("file_id") # самый большой размер
|
||||
# t-261005135853: documents/photo сразу на диск при приёме — путь в
|
||||
# событии, файл не теряется (голосовые качает voice-пайплайн отдельно).
|
||||
# Сбой скачивания не валит приём: file_path=None, file_id остаётся.
|
||||
doc_rel = None
|
||||
if kind in ("document", "photo"):
|
||||
try:
|
||||
doc_rel = save_document(msg)
|
||||
except Exception as e:
|
||||
print(f"[tg] save_document error: {e}", flush=True)
|
||||
data = {
|
||||
"update_id": update["update_id"],
|
||||
"chat_id": chat.get("id"),
|
||||
|
|
@ -276,14 +185,6 @@ def receive_message(update: dict):
|
|||
# file_unique_id: ключ аудио-кэша и идемпотентности расшифровки
|
||||
# (file_id ротируется, unique — постоянный для одного файла)
|
||||
"file_unique_id": file.get("file_unique_id") or (photo[-1].get("file_unique_id") if photo else None),
|
||||
# t-261005135853: путь к скачанному файлу (абсолютный, открывается
|
||||
# напрямую с любого уровня) + исходное имя/размер/mime — триаж видит
|
||||
# что за файл, без getFile. None = скачать не удалось (file_id остаётся).
|
||||
"file_path": doc_rel,
|
||||
"file_name": (msg.get("document") or {}).get("file_name"),
|
||||
"file_size": (msg.get("document") or {}).get("file_size")
|
||||
or (photo[-1].get("file_size") if photo else None),
|
||||
"mime_type": (msg.get("document") or {}).get("mime_type"),
|
||||
"duration": duration,
|
||||
"date": msg.get("date"),
|
||||
# reply_to_message_id (t-261002085926): когда Alex отвечает (reply) на
|
||||
|
|
|
|||
|
|
@ -107,58 +107,8 @@ def _download_voice(file_id: str, file_unique_id: str = ""):
|
|||
return audio, cache
|
||||
|
||||
|
||||
# ─────────── тяжёлые воркеры ВНЕ cgroup поллера (t-261004160531-780666) ─────
|
||||
# faster-whisper large-v3-turbo (int8) живёт в ~2-3G RSS, ffmpeg тоже кусается:
|
||||
# заспавненные поллером как обычные дети они падали в ЕГО cgroup — systemd
|
||||
# Consumption показывал 3.7G peak у simbiot-harness-tg. Теперь тяжёлое уходит
|
||||
# в собственный transient-scope (systemd-run --scope) со своим MemoryMax;
|
||||
# поллер остаётся лёгким (<100M), его юнит держит MemoryMax=1G.
|
||||
# Нет systemd (локалка/тесты) — прозрачный fallback на прямой запуск.
|
||||
_ASR_SCOPE_MEM = os.environ.get("SIMBIOT_ASR_SCOPE_MEM", "3G")
|
||||
_HAS_SYSTEMD = os.path.isdir("/run/systemd/system") and os.path.exists("/usr/bin/systemd-run")
|
||||
|
||||
|
||||
def _heavy_argv(argv):
|
||||
"""argv тяжёлого воркера → запуск в отдельном systemd-scope с MemoryMax."""
|
||||
if not _HAS_SYSTEMD:
|
||||
return list(argv)
|
||||
import uuid
|
||||
unit = f"simbiot-asr-{uuid.uuid4().hex[:10]}"
|
||||
return ["systemd-run", "--scope", "--quiet", "--collect",
|
||||
f"--unit={unit}", f"-p", f"MemoryMax={_ASR_SCOPE_MEM}",
|
||||
"--"] + list(argv)
|
||||
|
||||
|
||||
def _heavy_run(argv, input_bytes=None, timeout=600):
|
||||
"""Тяжёлый subprocess: scope-изоляция памяти + killpg на таймауте.
|
||||
|
||||
systemd-run --scope выполняет команду синхронно, поэтому таймаут
|
||||
работает как раньше; start_new_session + killpg (паттерн
|
||||
isolated_runner, e-261003004801) не оставляют осиротевший воркер,
|
||||
если тик умер раньше него.
|
||||
"""
|
||||
import signal
|
||||
proc = subprocess.Popen(
|
||||
_heavy_argv(argv), stdin=subprocess.PIPE, stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE, start_new_session=True)
|
||||
try:
|
||||
out, err = proc.communicate(input=input_bytes, timeout=timeout)
|
||||
return proc.returncode, out or b"", err or b""
|
||||
except subprocess.TimeoutExpired:
|
||||
try:
|
||||
os.killpg(proc.pid, signal.SIGKILL)
|
||||
except (ProcessLookupError, PermissionError, OSError):
|
||||
pass
|
||||
try: # дождаться смерти группы, чтобы зомби не сидели в scope
|
||||
proc.communicate(timeout=10)
|
||||
except Exception:
|
||||
pass
|
||||
return None, b"", b""
|
||||
|
||||
|
||||
# ────────────── извлечение аудио из видеоконтейнера ─────────────────────────
|
||||
|
||||
|
||||
def extract_audio_track(container: bytes):
|
||||
"""Видеоконтейнер (mp4 и т.п.) → ogg/opus аудиодорожка через ffmpeg.
|
||||
|
||||
|
|
@ -171,17 +121,16 @@ def extract_audio_track(container: bytes):
|
|||
with tempfile.NamedTemporaryFile(prefix="simbiot-v_", delete=False) as tf:
|
||||
tf.write(container)
|
||||
src = tf.name
|
||||
# -vn: только звук; моно 16к — формат, который любят оба ASR-яруса.
|
||||
# ffmpeg тяжёлый → в свой scope (память поллера не растёт)
|
||||
rc, out, err = _heavy_run(
|
||||
# -vn: только звук; моно 16к — формат, который любят оба ASR-яруса
|
||||
proc = subprocess.run(
|
||||
["ffmpeg", "-v", "error", "-i", src, "-vn", "-ac", "1",
|
||||
"-ar", "16000", "-f", "ogg", "-c:a", "libopus", "-"],
|
||||
timeout=180)
|
||||
if rc != 0 or not out:
|
||||
err = err.decode()[:200]
|
||||
capture_output=True, timeout=180)
|
||||
if proc.returncode != 0 or not proc.stdout:
|
||||
err = (proc.stderr or b"").decode()[:200]
|
||||
print(f"[voice] ffmpeg: дорожка не извлечена ({err})", flush=True)
|
||||
return None
|
||||
return out
|
||||
return proc.stdout
|
||||
except Exception as e:
|
||||
print(f"[voice] ffmpeg error: {e}", flush=True)
|
||||
return None
|
||||
|
|
@ -250,21 +199,17 @@ def _asr_faster_whisper(audio: bytes, filename: str):
|
|||
"audio_b64": base64.b64encode(audio).decode(),
|
||||
"filename": filename,
|
||||
}, ensure_ascii=False)
|
||||
# faster-whisper large-v3-turbo ~2-3G RSS: только в свой scope,
|
||||
# иначе память ложится в cgroup поллера (3.7G peak, t-261004160531)
|
||||
rc, out, err = _heavy_run(
|
||||
[_FW_PYTHON, _FW_WORKER], input_bytes=req.encode(), timeout=600)
|
||||
if rc is None:
|
||||
print("[voice] faster-whisper: таймаут (группа убита)", flush=True)
|
||||
return None
|
||||
out = out.decode() if out else ""
|
||||
proc = subprocess.run(
|
||||
[_FW_PYTHON, _FW_WORKER], input=req.encode(),
|
||||
capture_output=True, timeout=600)
|
||||
out = proc.stdout.decode() if proc.stdout else ""
|
||||
try:
|
||||
data = json.loads(out.strip().splitlines()[-1]) if out.strip() else {}
|
||||
except (ValueError, IndexError):
|
||||
data = {"error": err.decode()[:300]}
|
||||
if data.get("error") or rc != 0:
|
||||
data = {"error": (proc.stderr or b"").decode()[:300]}
|
||||
if data.get("error") or proc.returncode != 0:
|
||||
print(f"[voice] faster-whisper error: "
|
||||
f"{data.get('error') or rc}", flush=True)
|
||||
f"{data.get('error') or proc.returncode}", flush=True)
|
||||
return None
|
||||
return (data.get("text") or "").strip()
|
||||
except Exception as e:
|
||||
|
|
|
|||
|
|
@ -1 +0,0 @@
|
|||
{"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)"}}
|
||||
|
|
@ -1,373 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
<title>Simbiot — The Studio OS</title>
|
||||
<meta name="description" content="A dedicated AI team — engineers, video producers, content makers — plus the OS that runs projects, treasury, decisions and meetings. White-label, flat rate.">
|
||||
<meta property="og:title" content="Simbiot — The Studio OS">
|
||||
<meta property="og:description" content="Hire nobody. Ship everything. An AI team for your studio plus the OS that runs it.">
|
||||
<meta property="og:url" content="https://simbiot.cil.by/">
|
||||
<meta property="og:type" content="website">
|
||||
<meta property="og:image" content="https://simbiot.cil.by/og.png">
|
||||
<meta name="twitter:card" content="summary_large_image">
|
||||
<style>
|
||||
/* ── fingerprint: offset rail · 660px measure · charcoal+lime · serif+mono · machined 4/10 · tinted panels · index motif · one entrance + state wash ── */
|
||||
:root{
|
||||
--bg:#0E100C; --panel:#151812; --panel2:#1A1E17;
|
||||
--line:#272C22; --line2:#333A2C;
|
||||
--ink:#E9ECE1; --dim:#ADB6A2; --faint:#7E8873;
|
||||
--acc:#C9F24D; --acc-ink:#111408;
|
||||
--ok:#7BD88F; --run:#E8C44A; --wait:#8A9382;
|
||||
--serif:Georgia,'Iowan Old Style','Times New Roman',serif;
|
||||
--mono:ui-monospace,'SF Mono','Cascadia Mono',Menlo,Consolas,'Liberation Mono',monospace;
|
||||
--sans:-apple-system,system-ui,'Segoe UI',Roboto,sans-serif;
|
||||
}
|
||||
*{margin:0;padding:0;box-sizing:border-box}
|
||||
html{scroll-behavior:smooth}
|
||||
body{background:var(--bg);color:var(--ink);font-family:var(--sans);font-size:15px;line-height:1.5;-webkit-font-smoothing:antialiased}
|
||||
a{color:inherit}
|
||||
:focus-visible{outline:2px solid var(--acc);outline-offset:2px;border-radius:2px}
|
||||
.wrap{max-width:1060px;margin:0 auto;padding:0 22px}
|
||||
.mono{font-family:var(--mono);font-variant-numeric:tabular-nums}
|
||||
|
||||
/* ── left rail (desktop) ── */
|
||||
.layout{display:grid;grid-template-columns:168px minmax(0,1fr);gap:44px;max-width:1060px;margin:0 auto;padding:26px 22px 0}
|
||||
.rail{position:sticky;top:26px;align-self:start;display:flex;flex-direction:column;gap:22px;font-family:var(--mono);font-size:11px;letter-spacing:.08em;text-transform:uppercase}
|
||||
.rail .brand{display:flex;align-items:center;gap:9px;color:var(--ink);text-decoration:none;font-weight:600}
|
||||
.rail .brand .dot{width:9px;height:9px;border-radius:50%;background:var(--acc);flex:none}
|
||||
.rail nav{display:flex;flex-direction:column;gap:2px}
|
||||
.rail nav a{color:var(--faint);text-decoration:none;padding:9px 10px 9px 0;min-height:36px;display:flex;align-items:center;border-bottom:1px solid var(--line)}
|
||||
.rail nav a:hover{color:var(--ink)}
|
||||
.rail .railnote{color:var(--faint);line-height:1.9;border-top:1px solid var(--line);padding-top:14px}
|
||||
.rail .railnote b{color:var(--dim);font-weight:500}
|
||||
|
||||
/* top bar (mobile) */
|
||||
.topbar{display:none}
|
||||
|
||||
/* ── buttons: compact scale (26px / 11.5px) ── */
|
||||
.btn{display:inline-flex;align-items:center;justify-content:center;gap:8px;height:28px;padding:0 14px;border-radius:4px;border:1px solid var(--line2);background:var(--panel2);color:var(--ink);font-family:var(--mono);font-size:11.5px;letter-spacing:.08em;text-transform:uppercase;text-decoration:none;cursor:pointer;min-height:40px}
|
||||
.btn.primary{background:var(--acc);border-color:var(--acc);color:var(--acc-ink);font-weight:600}
|
||||
.btn:focus-visible{outline-offset:1px}
|
||||
@media(hover:hover) and (pointer:fine){.btn:hover{border-color:var(--acc)} .btn.primary:hover{filter:brightness(1.06)}}
|
||||
.btn:active{transform:scale(.96)}
|
||||
|
||||
/* ── hero ── */
|
||||
.hero{padding:30px 0 26px;border-bottom:1px solid var(--line)}
|
||||
.kicker{font-family:var(--mono);font-size:11px;letter-spacing:.14em;text-transform:uppercase;color:var(--faint);margin-bottom:18px}
|
||||
.kicker .dot{display:inline-block;width:7px;height:7px;border-radius:50%;background:var(--ok);margin-right:8px}
|
||||
h1{font-family:var(--serif);font-weight:400;font-size:clamp(38px,6vw,62px);line-height:1.04;letter-spacing:-.015em;max-width:14ch}
|
||||
h1 em{font-style:italic;color:var(--acc)}
|
||||
.lede{color:var(--dim);max-width:52ch;margin:18px 0 22px;font-size:15.5px}
|
||||
.cta-row{display:flex;gap:10px;flex-wrap:wrap;align-items:center}
|
||||
.cta-note{font-family:var(--mono);font-size:10.5px;letter-spacing:.06em;color:var(--faint);text-transform:uppercase}
|
||||
|
||||
/* ── signature artifact: live task ladder ── */
|
||||
.console{margin-top:30px;background:var(--panel);border:1px solid var(--line2);border-radius:10px;overflow:hidden}
|
||||
.console .bar{display:flex;align-items:center;gap:10px;padding:10px 14px;border-bottom:1px solid var(--line);font-family:var(--mono);font-size:10.5px;letter-spacing:.1em;text-transform:uppercase;color:var(--faint)}
|
||||
.console .bar .ticks{display:flex;gap:5px}
|
||||
.console .bar .ticks i{width:8px;height:8px;border-radius:2px;background:var(--line2)}
|
||||
.console .bar .live{margin-left:auto;display:flex;align-items:center;gap:7px;color:var(--dim)}
|
||||
.console .bar .live i{width:7px;height:7px;border-radius:50%;background:var(--ok);animation:pulse 2.4s infinite}
|
||||
@keyframes pulse{0%,100%{opacity:1}50%{opacity:.35}}
|
||||
.metrics{display:grid;grid-template-columns:repeat(3,1fr);border-bottom:1px solid var(--line)}
|
||||
.metric{padding:12px 14px;border-right:1px solid var(--line)}
|
||||
.metric:last-child{border-right:none}
|
||||
.metric .v{font-family:var(--mono);font-variant-numeric:tabular-nums;font-size:19px;color:var(--ink)}
|
||||
.metric .v .u{color:var(--faint);font-size:12px}
|
||||
.metric .k{font-family:var(--mono);font-size:9.5px;letter-spacing:.12em;text-transform:uppercase;color:var(--faint);margin-top:3px}
|
||||
.ladder{display:flex;flex-direction:column}
|
||||
.trow{display:grid;grid-template-columns:52px minmax(0,1fr) 96px 88px;gap:12px;align-items:center;min-height:44px;padding:8px 14px;border-bottom:1px solid var(--line);font-size:13.5px;color:var(--dim);cursor:pointer;text-align:left;background:transparent;border-left:none;border-right:none;border-top:none;width:100%;font-family:var(--sans);transition:background-color .25s,color .25s}
|
||||
.trow:last-child{border-bottom:none}
|
||||
.trow:hover{background:var(--panel2);color:var(--ink)}
|
||||
.trow .sz{font-family:var(--mono);font-size:11px;color:var(--faint)}
|
||||
.trow .name{white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
||||
.trow .name b{color:var(--ink);font-weight:600}
|
||||
.trow .st{font-family:var(--mono);font-size:10.5px;letter-spacing:.08em;text-transform:uppercase;color:var(--wait);display:flex;align-items:center;gap:7px}
|
||||
.trow .st i{width:7px;height:7px;border-radius:50%;background:var(--wait);flex:none}
|
||||
.trow[data-st="running"] .st{color:var(--run)} .trow[data-st="running"] .st i{background:var(--run)}
|
||||
.trow[data-st="done"] .st{color:var(--ok)} .trow[data-st="done"] .st i{background:var(--ok)}
|
||||
.trow .who{font-family:var(--mono);font-size:11px;color:var(--faint);text-align:right}
|
||||
.console .foot{padding:9px 14px;font-family:var(--mono);font-size:10px;letter-spacing:.1em;text-transform:uppercase;color:var(--faint);border-top:1px solid var(--line)}
|
||||
.console .foot b{color:var(--dim);font-weight:500}
|
||||
|
||||
/* ── sections ── */
|
||||
section{padding:44px 0;border-bottom:1px solid var(--line)}
|
||||
.shead{display:flex;align-items:baseline;gap:14px;margin-bottom:22px}
|
||||
.sidx{font-family:var(--mono);font-size:11px;color:var(--acc);letter-spacing:.1em}
|
||||
h2{font-family:var(--serif);font-weight:400;font-size:clamp(24px,3.4vw,34px);line-height:1.12;letter-spacing:-.01em}
|
||||
.snote{margin-left:auto;font-family:var(--mono);font-size:10.5px;letter-spacing:.1em;text-transform:uppercase;color:var(--faint)}
|
||||
.lead{color:var(--dim);max-width:56ch;margin:-8px 0 24px;font-size:15px}
|
||||
|
||||
/* team ledger (rows, not cards) */
|
||||
.ledger{border:1px solid var(--line2);border-radius:10px;background:var(--panel);overflow:hidden}
|
||||
.lrow{display:grid;grid-template-columns:44px minmax(0,1.1fr) minmax(0,1.4fr) 150px;gap:16px;padding:16px;border-bottom:1px solid var(--line);align-items:baseline}
|
||||
.lrow:last-child{border-bottom:none}
|
||||
.lrow .idx{font-family:var(--mono);font-size:11px;color:var(--faint)}
|
||||
.lrow .nm{font-size:16px;font-weight:600}
|
||||
.lrow .nm span{display:block;font-family:var(--mono);font-size:10.5px;letter-spacing:.1em;text-transform:uppercase;color:var(--acc);margin-top:4px;font-weight:400}
|
||||
.lrow .ds{color:var(--dim);font-size:13.5px}
|
||||
.lrow .tags{font-family:var(--mono);font-size:10px;letter-spacing:.08em;text-transform:uppercase;color:var(--faint);text-align:right;line-height:2}
|
||||
|
||||
/* OS definition list */
|
||||
.dl{display:grid;grid-template-columns:minmax(0,1fr) minmax(0,1fr);gap:0 40px}
|
||||
.dl .item{display:grid;grid-template-columns:40px minmax(0,1fr);gap:12px;padding:15px 0;border-bottom:1px solid var(--line)}
|
||||
.dl .item .k{font-family:var(--mono);font-size:10.5px;color:var(--acc);letter-spacing:.08em;padding-top:3px}
|
||||
.dl .item .t{font-weight:600;font-size:14.5px}
|
||||
.dl .item .d{color:var(--dim);font-size:13.5px;margin-top:3px}
|
||||
|
||||
/* included rows */
|
||||
.inc{display:flex;flex-direction:column}
|
||||
.inc .irow{display:grid;grid-template-columns:26px minmax(0,1fr);gap:14px;padding:13px 0;border-bottom:1px solid var(--line)}
|
||||
.inc .irow:last-child{border-bottom:none}
|
||||
.inc .mark{color:var(--acc);font-family:var(--mono);font-size:12px;padding-top:2px}
|
||||
.inc .t{font-weight:600;font-size:14.5px}
|
||||
.inc .t span{font-family:var(--mono);font-size:10px;letter-spacing:.1em;text-transform:uppercase;color:var(--faint);margin-left:10px;font-weight:400}
|
||||
.inc .d{color:var(--dim);font-size:13.5px;margin-top:2px}
|
||||
|
||||
/* pricing */
|
||||
.plans{display:grid;grid-template-columns:minmax(0,1fr) minmax(0,1fr);gap:18px}
|
||||
.plan{border:1px solid var(--line2);border-radius:10px;background:var(--panel);padding:22px;display:flex;flex-direction:column;gap:14px}
|
||||
.plan.hot{border-color:var(--acc)}
|
||||
.plan .pt{display:flex;align-items:center;gap:10px;font-family:var(--mono);font-size:11px;letter-spacing:.12em;text-transform:uppercase;color:var(--faint)}
|
||||
.plan.hot .pt{color:var(--acc)}
|
||||
.price{font-family:var(--serif);font-size:40px;line-height:1}
|
||||
.price .per{font-family:var(--mono);font-size:12px;color:var(--faint);letter-spacing:.08em}
|
||||
.plan ul{list-style:none;display:flex;flex-direction:column;gap:8px;color:var(--dim);font-size:13.5px}
|
||||
.plan li{display:grid;grid-template-columns:18px 1fr;gap:10px}
|
||||
.plan li::before{content:"▸";color:var(--acc);font-family:var(--mono);font-size:11px}
|
||||
.plan .btn{margin-top:auto}
|
||||
.plan .fine{font-family:var(--mono);font-size:9.5px;letter-spacing:.1em;text-transform:uppercase;color:var(--faint);text-align:center}
|
||||
|
||||
/* footer */
|
||||
footer{padding:34px 0 46px}
|
||||
.fgrid{display:flex;flex-wrap:wrap;gap:14px 30px;align-items:center;font-family:var(--mono);font-size:10.5px;letter-spacing:.1em;text-transform:uppercase;color:var(--faint)}
|
||||
.fgrid a{color:var(--dim);text-decoration:none;border-bottom:1px solid var(--line2)}
|
||||
.fgrid a:hover{color:var(--acc);border-color:var(--acc)}
|
||||
.fgrid .sp{margin-left:auto}
|
||||
|
||||
/* entrance: one reserved-space fade-up */
|
||||
.in{opacity:0;transform:translateY(8px);animation:in .5s ease forwards}
|
||||
.in.d1{animation-delay:.08s}.in.d2{animation-delay:.16s}.in.d3{animation-delay:.24s}
|
||||
@keyframes in{to{opacity:1;transform:none}}
|
||||
|
||||
/* ── mobile: single column ── */
|
||||
@media(max-width:860px){
|
||||
.layout{grid-template-columns:minmax(0,1fr);gap:0;padding:0 16px}
|
||||
.rail{display:none}
|
||||
.topbar{display:flex;align-items:center;gap:12px;padding:14px 16px;border-bottom:1px solid var(--line);font-family:var(--mono);font-size:11px;letter-spacing:.1em;text-transform:uppercase}
|
||||
.topbar .brand{display:flex;align-items:center;gap:8px;color:var(--ink);text-decoration:none;font-weight:600;margin-right:auto}
|
||||
.topbar .brand .dot{width:8px;height:8px;border-radius:50%;background:var(--acc)}
|
||||
.topbar .btn{min-height:40px;height:28px}
|
||||
.wrap{padding:0 16px}
|
||||
.layout > main{padding:0}
|
||||
.hero,.layout main section,.layout main footer{padding-left:0;padding-right:0}
|
||||
.metrics{grid-template-columns:repeat(3,minmax(0,1fr))}
|
||||
.metric .v{font-size:15px}
|
||||
.trow{grid-template-columns:38px minmax(0,1fr) 84px;gap:8px;padding:10px 12px}
|
||||
.trow .who{display:none}
|
||||
.lrow{grid-template-columns:minmax(0,1fr);gap:6px;padding:14px 16px}
|
||||
.lrow .tags{text-align:left;line-height:1.9}
|
||||
.dl{grid-template-columns:minmax(0,1fr)}
|
||||
.plans{grid-template-columns:minmax(0,1fr)}
|
||||
h1{font-size:clamp(34px,9vw,44px)}
|
||||
}
|
||||
@media(prefers-reduced-motion:reduce){
|
||||
*{animation:none!important;transition:none!important}
|
||||
.in{opacity:1;transform:none}
|
||||
html{scroll-behavior:auto}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<header class="topbar">
|
||||
<a class="brand" href="#top"><span class="dot"></span>Simbiot</a>
|
||||
<a class="btn" href="https://cil.by/cabinet">Sign in</a>
|
||||
<a class="btn primary" href="https://simbiot.cil.by/#pricing">Get started</a>
|
||||
</header>
|
||||
|
||||
<div class="layout" id="top">
|
||||
<aside class="rail">
|
||||
<a class="brand" href="#top"><span class="dot"></span>Simbiot</a>
|
||||
<nav aria-label="Sections">
|
||||
<a href="#team">Team</a>
|
||||
<a href="#os">The OS</a>
|
||||
<a href="#included">Included</a>
|
||||
<a href="#pricing">Pricing</a>
|
||||
</nav>
|
||||
<div class="railnote">
|
||||
<b>Coop 42</b> runtime<br>
|
||||
reflex · minute<br>
|
||||
hour · day<br><br>
|
||||
<a class="btn" style="margin-top:10px" href="https://cil.by/cabinet">Sign in</a>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<main>
|
||||
<!-- 00 · hero + live ladder -->
|
||||
<div class="hero">
|
||||
<p class="kicker in"><span class="dot"></span>Studio OS · live</p>
|
||||
<h1 class="in d1">Hire nobody.<br><em>Ship everything.</em></h1>
|
||||
<p class="lede in d2">A dedicated AI team — engineers, video producers, content makers — plus the operating system that runs them: projects, treasury, decisions, meetings. White-label, under your brand.</p>
|
||||
<div class="cta-row in d3">
|
||||
<a class="btn primary" href="https://simbiot.cil.by/#pricing">Start your studio — $999/mo</a>
|
||||
<a class="btn" href="https://demo.cil.by">Live demo</a>
|
||||
<span class="cta-note">flat rate · cancel anytime</span>
|
||||
</div>
|
||||
|
||||
<div class="console in d3" aria-label="Live task ladder demo">
|
||||
<div class="bar">
|
||||
<span class="ticks" aria-hidden="true"><i></i><i></i><i></i></span>
|
||||
<span>studio ops · task ladder</span>
|
||||
<span class="live"><i></i>3 agents online</span>
|
||||
</div>
|
||||
<div class="metrics">
|
||||
<div class="metric"><div class="v" id="mDone">4<span class="u"> done</span></div><div class="k">tasks this cycle</div></div>
|
||||
<div class="metric"><div class="v">4<span class="u">-cadence</span></div><div class="k">reflex→day runtime</div></div>
|
||||
<div class="metric"><div class="v">S·M·L</div><div class="k">task sizes</div></div>
|
||||
</div>
|
||||
<div class="ladder" id="ladder"></div>
|
||||
<div class="foot">hour digest 23:10 Minsk · <b>all actions signed</b> · click a row to re-dispatch</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 01 · team -->
|
||||
<section id="team">
|
||||
<div class="shead"><span class="sidx">01</span><h2>A staff of specialists, not a chatbot.</h2><span class="snote">dedicated servers</span></div>
|
||||
<p class="lead">Every agent runs on its own server with private memory, tools, email and Matrix chat. You brief them like employees — they onboard onto your idea and start shipping.</p>
|
||||
<div class="ledger">
|
||||
<div class="lrow">
|
||||
<span class="idx">/01</span>
|
||||
<div class="nm">Cody<span>Engineer</span></div>
|
||||
<div class="ds">Writes code, deploys services, maintains git repositories. MVPs, landing pages, bots, APIs, integrations — from brief to production.</div>
|
||||
<div class="tags">full-stack · devops · git</div>
|
||||
</div>
|
||||
<div class="lrow">
|
||||
<span class="idx">/02</span>
|
||||
<div class="nm">ReelsUp<span>Video producer</span></div>
|
||||
<div class="ds">Cuts long videos and podcasts into vertical clips for Reels and Shorts. Transcription, highlight detection, automated editing.</div>
|
||||
<div class="tags">whisper · clips · shorts</div>
|
||||
</div>
|
||||
<div class="lrow">
|
||||
<span class="idx">/03</span>
|
||||
<div class="nm">Roy<span>Content maker</span></div>
|
||||
<div class="ds">Promotional content on a conveyor: reels with 3D characters, scripts, voiceovers, visuals — brand-consistent by default.</div>
|
||||
<div class="tags">3d · tts · scripts</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 02 · the os -->
|
||||
<section id="os">
|
||||
<div class="shead"><span class="sidx">02</span><h2>Agents build. The OS manages.</h2><span class="snote">your domain</span></div>
|
||||
<p class="lead">Projects, money, decisions and meetings in one transparent system — under your studio's name.</p>
|
||||
<div class="dl">
|
||||
<div class="item"><span class="k">S.01</span><div><div class="t">Project registry</div><div class="d">A card for every venture: decisions, ownership, agents, activity feed.</div></div></div>
|
||||
<div class="item"><span class="k">S.02</span><div><div class="t">Git repo per project</div><div class="d">Code and docs in a dedicated repository with a web editor. Nothing lost in chats.</div></div></div>
|
||||
<div class="item"><span class="k">S.03</span><div><div class="t">Treasury & wallets</div><div class="d">Revenue share flows in automatically. Every payout transparent and auditable.</div></div></div>
|
||||
<div class="item"><span class="k">S.04</span><div><div class="t">Decisions & voting</div><div class="d">Allocate resources by vote. Cycles, quorum, full decision history.</div></div></div>
|
||||
<div class="item"><span class="k">S.05</span><div><div class="t">Meetings with AI minutes</div><div class="d">Video calls with recording, transcription, summaries — into the registry.</div></div></div>
|
||||
<div class="item"><span class="k">S.06</span><div><div class="t">Time & task tracking</div><div class="d">What every agent completed and where work went — a daily digest in your inbox.</div></div></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 03 · included -->
|
||||
<section id="included">
|
||||
<div class="shead"><span class="sidx">03</span><h2>Full stack. Nothing extra to buy.</h2><span class="snote">flat rate</span></div>
|
||||
<div class="inc">
|
||||
<div class="irow"><span class="mark">✓</span><div><div class="t">Stripe payments <span>live</span></div><div class="d">Card payments for what your agents build — subscriptions, one-offs, invoices. A working checkout on day one.</div></div></div>
|
||||
<div class="irow"><span class="mark">✓</span><div><div class="t">AI inference included <span>flat</span></div><div class="d">All models run on our gateway — no API keys, no vendor bills, no metered surprises.</div></div></div>
|
||||
<div class="irow"><span class="mark">✓</span><div><div class="t">White-label portal <span>your brand</span></div><div class="d">Your subdomain, your studio's name up front. Every deliverable ships under your identity.</div></div></div>
|
||||
<div class="irow"><span class="mark">✓</span><div><div class="t">The agent harness <span>4-cadence</span></div><div class="d">Reflex, minute, hour, day — escalation, memory, semantic knowledge base. Coop-tested daily.</div></div></div>
|
||||
<div class="irow"><span class="mark">✓</span><div><div class="t">One-click export <span>no lock-in</span></div><div class="d">Take everything with you, anytime. Ready in ~10 minutes after payment.</div></div></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 04 · pricing -->
|
||||
<section id="pricing">
|
||||
<div class="shead"><span class="sidx">04</span><h2>One plan each. Everything included.</h2><span class="snote">founding cohort</span></div>
|
||||
<div class="plans">
|
||||
<div class="plan hot">
|
||||
<div class="pt">Personal · first come</div>
|
||||
<div class="price">$99.9 <span class="per">/ month</span></div>
|
||||
<ul>
|
||||
<li>1 dedicated AI agent on its own server</li>
|
||||
<li>Private Matrix chat — web + mobile</li>
|
||||
<li>Inference, hosting, updates included</li>
|
||||
<li>One-click export, no lock-in</li>
|
||||
</ul>
|
||||
<a class="btn primary" href="https://simbiot.cil.by/#pricing">Get my agent</a>
|
||||
<div class="fine">secure checkout by Stripe · cancel anytime</div>
|
||||
</div>
|
||||
<div class="plan">
|
||||
<div class="pt">Studio</div>
|
||||
<div class="price">$999 <span class="per">/ month</span></div>
|
||||
<ul>
|
||||
<li>12 AI agents — engineers, producers, content</li>
|
||||
<li>White-label portal on your subdomain</li>
|
||||
<li>Projects, treasury, decisions, meetings — one system</li>
|
||||
<li>Team members free · priority support</li>
|
||||
</ul>
|
||||
<a class="btn" href="https://simbiot.cil.by/#pricing">Start your studio</a>
|
||||
<div class="fine">deployed the same day · self-hosted? talk to us</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<footer>
|
||||
<div class="fgrid">
|
||||
<span>© 2026 Simbiot · Cooperative 42</span>
|
||||
<a href="https://demo.cil.by">Demo</a>
|
||||
<a href="https://cil.by/cabinet">Cabinet</a>
|
||||
<a href="mailto:portal@cil.by">portal@cil.by</a>
|
||||
<span class="sp">cil.by</span>
|
||||
</div>
|
||||
</footer>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
/* signature artifact: a miniature of the real task ladder.
|
||||
Reserved geometry: rows are pre-rendered, only state cell swaps. */
|
||||
(function(){
|
||||
var TASKS=[
|
||||
{sz:'L', name:'Redesign pricing section copy', who:'cody'},
|
||||
{sz:'M', name:'Weekly digest to stakeholders', who:'os'},
|
||||
{sz:'S', name:'Fix og-image on landing', who:'cody'},
|
||||
{sz:'M', name:'Escalation: router burn > budget', who:'os'}
|
||||
];
|
||||
var ST=['queued','running','done'], LABEL={queued:'queued',running:'running',done:'done'};
|
||||
var states=[1,0,2,1], doneCount=4, ladder=document.getElementById('ladder');
|
||||
if(!ladder) return;
|
||||
var reduced=window.matchMedia&&window.matchMedia('(prefers-reduced-motion: reduce)').matches;
|
||||
var rows=TASKS.map(function(t,i){
|
||||
var b=document.createElement('button');
|
||||
b.type='button'; b.className='trow'; b.setAttribute('data-st',LABEL[states[i]]);
|
||||
b.setAttribute('aria-label',t.name+' — '+LABEL[states[i]]+'. Re-dispatch.');
|
||||
b.innerHTML='<span class="sz">['+t.sz+']</span>'+
|
||||
'<span class="name"><b>'+t.name+'</b></span>'+
|
||||
'<span class="st"><i></i>'+LABEL[states[i]]+'</span>'+
|
||||
'<span class="who">'+t.who+'</span>';
|
||||
b.addEventListener('click',function(){step(i);});
|
||||
ladder.appendChild(b);
|
||||
return b;
|
||||
});
|
||||
function paint(i){
|
||||
var s=LABEL[states[i]];
|
||||
rows[i].setAttribute('data-st',s);
|
||||
rows[i].getElementsByClassName('st')[0].innerHTML='<i></i>'+s;
|
||||
}
|
||||
function step(i){
|
||||
if(states[i]===2){states[i]=1;paint(i);return}
|
||||
if(states[i]===1){states[i]=2;doneCount++;var m=document.getElementById('mDone');
|
||||
if(m)m.innerHTML=doneCount+'<span class="u"> done</span>';paint(i);return}
|
||||
states[i]=1;paint(i);
|
||||
}
|
||||
var cur=0;
|
||||
function tick(){ if(states[cur]!==2){step(cur);} cur=(cur+1)%TASKS.length; }
|
||||
if(!reduced){ var t=setInterval(tick,2200); document.addEventListener('visibilitychange',function(){ document.hidden?clearInterval(t):t=setInterval(tick,2200); }); }
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
[dash] http://0.0.0.0:8765/ (health 127.0.0.1:8766/api/health) refresh=5s token=off
|
||||
[dash] 127.0.0.1 "GET /api/health HTTP/1.1" 200 -
|
||||
[dash] 185.203.154.209 "GET /api/state?t=1791449512284 HTTP/1.1" 200 -
|
||||
[dash] 185.203.154.209 "GET /api/state?t=1791449517287 HTTP/1.1" 200 -
|
||||
[dash] 185.203.154.209 "GET /api/state?t=1791449522283 HTTP/1.1" 200 -
|
||||
|
|
@ -1,209 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="ru">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>ИИШНИЦА · Панельная сессия «Общее видение» — концепт кооператива 42</title>
|
||||
<style>
|
||||
:root{
|
||||
--paper:#faf8f3; --ink:#15150f; --signal:#FF4F00;
|
||||
--dim:#6b675e; --line:#d8d3c8; --card:#f3f0e8;
|
||||
}
|
||||
*{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(34px,6vw,58px);line-height:1.02;font-weight:800;letter-spacing:-.02em;margin:10px 0 14px;text-transform:uppercase}
|
||||
.sub{font-size:17px;color:var(--dim);max-width:740px}
|
||||
.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:780px;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:840px}
|
||||
.cardgrid{display:grid;grid-template-columns:repeat(auto-fit,minmax(250px,1fr));gap:14px;margin:18px 0}
|
||||
.card{border:1px solid var(--ink);background:var(--card);padding:16px 16px 14px}
|
||||
.card .cn{font-family:ui-monospace,Menlo,monospace;font-size:11px;color:var(--signal);font-weight:700;letter-spacing:.15em}
|
||||
.card h4{font-size:15px;margin:6px 0;font-weight:800}
|
||||
.card p{font-size:13.5px;color:var(--dim);margin:0;line-height:1.55}
|
||||
.card p b{color:var(--ink)}
|
||||
.timeline{margin:22px 0;border:1px solid var(--ink);background:#fff}
|
||||
.trow{display:grid;grid-template-columns:120px 1fr;border-bottom:1px solid var(--line)}
|
||||
.trow:last-child{border-bottom:none}
|
||||
.trow .t{font-family:ui-monospace,Menlo,monospace;font-weight:700;padding:12px 14px;border-right:1px solid var(--line);display:flex;align-items:center;font-size:13px}
|
||||
.trow .d{padding:12px 16px}
|
||||
.trow .d b{display:block}
|
||||
.trow .d span{font-size:13.5px;color:var(--dim);display:block;margin-top:2px}
|
||||
.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)}
|
||||
ul{margin:8px 0 8px 18px;font-size:14.5px}
|
||||
li{margin:6px 0}
|
||||
li::marker{color:var(--signal)}
|
||||
.mission{border:3px double var(--ink);background:#fff;padding:26px 28px;margin:22px 0}
|
||||
.mission p{font-size:19px;line-height:1.5;max-width:840px}
|
||||
.mission .src{font-size:12px;color:var(--dim);font-family:ui-monospace,Menlo,monospace;margin-top:12px}
|
||||
.qbox{border:1px solid var(--ink);background:var(--card);padding:16px 20px;margin:12px 0}
|
||||
.qbox .q{font-size:16.5px;font-weight:600;line-height:1.45}
|
||||
.qbox .m{font-family:ui-monospace,Menlo,monospace;font-size:11px;color:var(--signal);letter-spacing:.15em;font-weight:700}
|
||||
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>Общее видение</h1>
|
||||
<p class="sub">Формат панельной сессии на ИИШНИЦЕ: миссия 42 задаёт рамку, спикеры из разных индустрий описывают будущее — и панель сходится к названию общего видения.</p>
|
||||
<div class="badges">
|
||||
<span class="badge s">90 минут</span>
|
||||
<span class="badge">5–6 спикеров</span>
|
||||
<span class="badge">3 круга + зал</span>
|
||||
<span class="badge">Миссия 42</span>
|
||||
<span class="badge">Минск · cil.by</span>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<section id="idea">
|
||||
<div class="snum">01 / ИДЕЯ</div>
|
||||
<h2>Суть формата</h2>
|
||||
<p class="lead">Не классическая панель с вопросами модератора, а три круга с одной целью: собрать из отдельных картин будущего — общую. Миссия 42 озвучивается в начале как рамка, объединяющая любые индустрии и любые ответы.</p>
|
||||
<div class="mission">
|
||||
<p><b>Миссия «42»:</b> все миссии одинаково важны. Миссия «42» объединяет абсолютно все возможные миссии. Нет неважных миссий — все люди важны, у каждого своя миссия.</p>
|
||||
<div class="src">— кооператив «Друзья Коллективного Разума», Минск · cil.by</div>
|
||||
</div>
|
||||
<p>Почему это работает: каждый спикер говорит о <b>своей</b> индустрии — и потому не конкурирует с соседом за «правильный ответ». А третий круг, название общего видения, превращает панель из набора мнений в один артефакт, который остаётся после сессии: в заголовках, фото и цитатах.</p>
|
||||
</section>
|
||||
|
||||
<section id="format">
|
||||
<div class="snum">02 / ФОРМАТ</div>
|
||||
<h2>Три круга + зал</h2>
|
||||
<p class="lead">Каждый круг имеет свою функцию: раскрыть, связать, назвать. Тайминг дан для 6 спикеров; при 5 спикерах сессия ужимается до ~80 минут.</p>
|
||||
|
||||
<div class="timeline">
|
||||
<div class="trow">
|
||||
<div class="t">00:00–00:05</div>
|
||||
<div class="d"><b>Открытие и миссия 42</b><span>Модератор (или Alex) озвучивает миссию 42 как рамку сессии: все миссии одинаково важны — потому каждый взгляд на будущее важен.</span></div>
|
||||
</div>
|
||||
<div class="trow">
|
||||
<div class="t">00:05–00:29</div>
|
||||
<div class="d"><b>Круг 1 — «Моё будущее» · 4 мин × 6</b><span>Каждый спикер описывает будущее своей индустрии в самом дальнем горизонте, в который он может заглянуть. Без вопросов, без перебиваний. 24 минуты.</span></div>
|
||||
</div>
|
||||
<div class="trow">
|
||||
<div class="t">00:29–00:41</div>
|
||||
<div class="d"><b>Круг 2 — «Что понравилось» · 2 мин × 6</b><span>Каждый подсвечивает, что понравилось у других спикеров. Не спорить, а связывать: где он увидел пересечение со своим будущим. 12 минут.</span></div>
|
||||
</div>
|
||||
<div class="trow">
|
||||
<div class="t">00:41–00:55</div>
|
||||
<div class="d"><b>Круг 3 — «Название общего видения»</b><span>Панель соглашается назвать общее видение — одно название для всех картин будущего. Модератор помогает сформулировать и фиксирует финальную формулировку. 14 минут.</span></div>
|
||||
</div>
|
||||
<div class="trow">
|
||||
<div class="t">00:55–01:25</div>
|
||||
<div class="d"><b>Вопросы из зала</b><span>Зал спрашивает — спикеры отвечают. Модератор держит рамку миссии: любой вопрос важен, любая индустрия равна. 30 минут.</span></div>
|
||||
</div>
|
||||
<div class="trow">
|
||||
<div class="t">01:25–01:30</div>
|
||||
<div class="d"><b>Закрытие</b><span>Модератор повторяет название общего видения и миссию 42. Короткое резюме — для фото и записи.</span></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h3>Роли</h3>
|
||||
<div class="cardgrid">
|
||||
<div class="card">
|
||||
<div class="cn">МОДЕРАТОР</div>
|
||||
<h4>Держит рамку и тайминг</h4>
|
||||
<p>Озвучивает миссию, следит за 4/2 минутами, ведёт панель к названию общего видения. <b>Жёсткий тайм-кипер</b>: «осталось 30 секунд».</p>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="cn">СПИКЕРЫ</div>
|
||||
<h4>5–6 человек из разных индустрий</h4>
|
||||
<p>Три задания: будущее своей индустрии (4 мин), что понравилось у других (2 мин), название общего видения. <b>Без презентаций</b> — только голос.</p>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="cn">ЗАЛ</div>
|
||||
<h4>Четвёртый участник</h4>
|
||||
<p>Вопросы после трёх кругов. В круге 3 модератор может спросить зал: какое название общего видения предложили бы вы?</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="note">
|
||||
<b>Правило тайминга</b>
|
||||
<p>Карточки 60/30/10 секунд в руках тайм-кипера, звуковой сигнал на переходах между кругами. Если спикер не уложился в 4 минуты — модератор мягко забирает слово: формулировки доскажет во втором круге.</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="why">
|
||||
<div class="snum">03 / ПОЧЕМУ ЭТО СРАБОТАЕТ</div>
|
||||
<h2>Что делает формат сильным</h2>
|
||||
<div class="cardgrid">
|
||||
<div class="card">
|
||||
<div class="cn">01</div>
|
||||
<h4>Круг 1 без перебиваний</h4>
|
||||
<p>4 минуты каждому без вопросов — редкость на панелях. Спикеры дают глубокие ответы, а не заготовки. Глубина = запоминаемость.</p>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="cn">02</div>
|
||||
<h4>Круг 2 связывает</h4>
|
||||
<p>«Что понравилось» вместо «в чём не согласен» — позитивная рамка: панель выглядит единой, а не спорящей. Зал видит созвучие индустрий.</p>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="cn">03</div>
|
||||
<h4>Круг 3 — артефакт</h4>
|
||||
<p>Название общего видения — то, что остаётся после сессии: в заголовках, фото, цитатах. Панель производит смысл, а не обмен мнениями.</p>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="cn">04</div>
|
||||
<h4>Миссия 42 как рамка</h4>
|
||||
<p>«Все миссии одинаково важны» легитимирует любой ответ и любую индустрию — от ритейла до робототехники. Спикерам безопасно говорить смело.</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="name">
|
||||
<div class="snum">04 / НАЗВАНИЕ ОБЩЕГО ВИДЕНИЯ</div>
|
||||
<h2>Заготовки для третьего круга</h2>
|
||||
<p class="lead">Третий круг должен закончиться названием. Если панель не сойдётся сама, модератор предлагает шаблоны — панель выбирает или дорабатывает.</p>
|
||||
<div class="qbox"><div class="m">ЗАГОТОВКА 01</div><div class="q">«Симбиоз» — будущее, где ИИ усиливает каждую миссию человека, не заменяя её.</div></div>
|
||||
<div class="qbox"><div class="m">ЗАГОТОВКА 02</div><div class="q">«Коллективный разум» — каждый умножает свой вклад через общую инфраструктуру.</div></div>
|
||||
<div class="qbox"><div class="m">ЗАГОТОВКА 03</div><div class="q">«42» — число, которое объединяет все миссии.</div></div>
|
||||
</section>
|
||||
|
||||
<section id="checklist">
|
||||
<div class="snum">05 / ЧЕК-ЛИСТ ОРГАНИЗАТОРА</div>
|
||||
<h2>Что нужно до сессии</h2>
|
||||
<ul>
|
||||
<li><b>Состав панели:</b> 5–6 спикеров из максимально разных индустрий (финансы, медицина, промышленность, образование, медиа, госсектор). Разные горизонты видения — от прагматиков до футуристов.</li>
|
||||
<li><b>Брифинг спикеров за 2–3 дня:</b> каждому объяснить три круга, тайминг 4/2 минуты и установку «загляни в самый дальний горизонт, в который можешь». Никаких презентаций.</li>
|
||||
<li><b>Модератор:</b> умеет жёстко держать тайминг и мягко вести к названию. Репетиция открытия с миссией 42.</li>
|
||||
<li><b>Тайм-кипер:</b> карточки 60/30/10 секунд, звуковой сигнал на переходах между кругами.</li>
|
||||
<li><b>Зал:</b> микрофоны для вопросов, очередь или карточки для сбора вопросов.</li>
|
||||
<li><b>Запись:</b> видео обязательно — название общего видения и лучшие цитаты круга 2 идут в посты и клипы после сессии.</li>
|
||||
<li><b>Баннер миссии:</b> слайд или растяжка с текстом миссии 42 на сцене — рамка сессии видна на любой фотографии.</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section id="coop">
|
||||
<div class="snum">06 / СВЯЗЬ С КООПЕРАТИВОМ</div>
|
||||
<h2>Что это даёт 42</h2>
|
||||
<p class="lead">Панель — не только контент, но и вход в экосистему кооператива для людей с миссиями.</p>
|
||||
<ul>
|
||||
<li><b>Поток проектов:</b> спикеры и зал видят формат работы кооператива — люди с миссиями приходят в 42, регистрируют проекты, усиливают долгосрочную AUM.</li>
|
||||
<li><b>Точка входа:</b> портал cil.by упоминается в открытии и закрытии — для тех, кто захочет глубже после сессии.</li>
|
||||
<li><b>Бренд:</b> миссия 42 на сцене — фирменный приём кооператива, отличающий сессию от других панелей конференции.</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<footer>
|
||||
Концепт панельной сессии «Общее видение» · ИИШНИЦА · кооператив «Друзья Коллективного Разума» · Минск · <span class="mono">cil.by</span><br>
|
||||
Черновик для обсуждения v0.1 · <span class="mono">simbiot-harness / hour</span> · 2026-10-09
|
||||
</footer>
|
||||
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
Binary file not shown.
|
|
@ -1,281 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html xmlns="http://www.w3.org/1999/xhtml" lang="" xml:lang="">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="generator" content="pandoc" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=yes" />
|
||||
<title>Разговор с Леонидом — выжимка (встреча 143)</title>
|
||||
<style>
|
||||
html {
|
||||
color: #1a1a1a;
|
||||
background-color: #fdfdfd;
|
||||
}
|
||||
body {
|
||||
margin: 0 auto;
|
||||
max-width: 36em;
|
||||
padding-left: 50px;
|
||||
padding-right: 50px;
|
||||
padding-top: 50px;
|
||||
padding-bottom: 50px;
|
||||
hyphens: auto;
|
||||
overflow-wrap: break-word;
|
||||
text-rendering: optimizeLegibility;
|
||||
font-kerning: normal;
|
||||
}
|
||||
@media (max-width: 600px) {
|
||||
body {
|
||||
font-size: 0.9em;
|
||||
padding: 12px;
|
||||
}
|
||||
h1 {
|
||||
font-size: 1.8em;
|
||||
}
|
||||
}
|
||||
@media print {
|
||||
html {
|
||||
background-color: white;
|
||||
}
|
||||
body {
|
||||
background-color: transparent;
|
||||
color: black;
|
||||
font-size: 12pt;
|
||||
}
|
||||
p, h2, h3 {
|
||||
orphans: 3;
|
||||
widows: 3;
|
||||
}
|
||||
h2, h3, h4 {
|
||||
page-break-after: avoid;
|
||||
}
|
||||
}
|
||||
p {
|
||||
margin: 1em 0;
|
||||
}
|
||||
a {
|
||||
color: #1a1a1a;
|
||||
}
|
||||
a:visited {
|
||||
color: #1a1a1a;
|
||||
}
|
||||
img {
|
||||
max-width: 100%;
|
||||
}
|
||||
h1, h2, h3, h4, h5, h6 {
|
||||
margin-top: 1.4em;
|
||||
}
|
||||
h5, h6 {
|
||||
font-size: 1em;
|
||||
font-style: italic;
|
||||
}
|
||||
h6 {
|
||||
font-weight: normal;
|
||||
}
|
||||
ol, ul {
|
||||
padding-left: 1.7em;
|
||||
margin-top: 1em;
|
||||
}
|
||||
li > ol, li > ul {
|
||||
margin-top: 0;
|
||||
}
|
||||
blockquote {
|
||||
margin: 1em 0 1em 1.7em;
|
||||
padding-left: 1em;
|
||||
border-left: 2px solid #e6e6e6;
|
||||
color: #606060;
|
||||
}
|
||||
code {
|
||||
font-family: Menlo, Monaco, Consolas, 'Lucida Console', monospace;
|
||||
font-size: 85%;
|
||||
margin: 0;
|
||||
hyphens: manual;
|
||||
}
|
||||
pre {
|
||||
margin: 1em 0;
|
||||
overflow: auto;
|
||||
}
|
||||
pre code {
|
||||
padding: 0;
|
||||
overflow: visible;
|
||||
overflow-wrap: normal;
|
||||
}
|
||||
.sourceCode {
|
||||
background-color: transparent;
|
||||
overflow: visible;
|
||||
}
|
||||
hr {
|
||||
background-color: #1a1a1a;
|
||||
border: none;
|
||||
height: 1px;
|
||||
margin: 1em 0;
|
||||
}
|
||||
table {
|
||||
margin: 1em 0;
|
||||
border-collapse: collapse;
|
||||
width: 100%;
|
||||
overflow-x: auto;
|
||||
display: block;
|
||||
font-variant-numeric: lining-nums tabular-nums;
|
||||
}
|
||||
table caption {
|
||||
margin-bottom: 0.75em;
|
||||
}
|
||||
tbody {
|
||||
margin-top: 0.5em;
|
||||
border-top: 1px solid #1a1a1a;
|
||||
border-bottom: 1px solid #1a1a1a;
|
||||
}
|
||||
th {
|
||||
border-top: 1px solid #1a1a1a;
|
||||
padding: 0.25em 0.5em 0.25em 0.5em;
|
||||
}
|
||||
td {
|
||||
padding: 0.125em 0.5em 0.25em 0.5em;
|
||||
}
|
||||
header {
|
||||
margin-bottom: 4em;
|
||||
text-align: center;
|
||||
}
|
||||
#TOC li {
|
||||
list-style: none;
|
||||
}
|
||||
#TOC ul {
|
||||
padding-left: 1.3em;
|
||||
}
|
||||
#TOC > ul {
|
||||
padding-left: 0;
|
||||
}
|
||||
#TOC a:not(:hover) {
|
||||
text-decoration: none;
|
||||
}
|
||||
code{white-space: pre-wrap;}
|
||||
span.smallcaps{font-variant: small-caps;}
|
||||
div.columns{display: flex; gap: min(4vw, 1.5em);}
|
||||
div.column{flex: auto; overflow-x: auto;}
|
||||
div.hanging-indent{margin-left: 1.5em; text-indent: -1.5em;}
|
||||
/* The extra [class] is a hack that increases specificity enough to
|
||||
override a similar rule in reveal.js */
|
||||
ul.task-list[class]{list-style: none;}
|
||||
ul.task-list li input[type="checkbox"] {
|
||||
font-size: inherit;
|
||||
width: 0.8em;
|
||||
margin: 0 0.8em 0.2em -1.6em;
|
||||
vertical-align: middle;
|
||||
}
|
||||
.display.math{display: block; text-align: center; margin: 0.5rem auto;}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<header id="title-block-header">
|
||||
<h1 class="title">Разговор с Леонидом — выжимка (встреча 143)</h1>
|
||||
</header>
|
||||
<h1 id="разговор-алекса-с-леонидом-выжимка-и-договорённости">Разговор
|
||||
Алекса с Леонидом — выжимка и договорённости</h1>
|
||||
<p><em>Встреча 143 · 2026-10-08 · ~2.5 часа · Jitsi + агент-визуализатор
|
||||
Simbiot</em></p>
|
||||
<hr />
|
||||
<h2 id="слайд-1-кто-есть-кто">Слайд 1 — Кто есть кто</h2>
|
||||
<ul>
|
||||
<li><strong>Леонид</strong> — давний знакомый студии, начинающий
|
||||
писатель, инвестор в deep-tech (в т.ч. Anti-Aging Hero с физиком Петром
|
||||
Федичевым). Владеет DGX Spark 128GB (куплен за €5000), гоняет на нём
|
||||
CUDA-моделирование графов надёжности.</li>
|
||||
<li><strong>Алекс</strong> — показал студийную инфраструктуру:
|
||||
агент-визуализатор звонков (демо в реальном времени), собственный роутер
|
||||
LM-провайдеров, RAG по всем знаниям кооператива.</li>
|
||||
<li>Демо произвело впечатление: «1-й раз такое в моей практике… польза
|
||||
очевидна».</li>
|
||||
</ul>
|
||||
<h2 id="слайд-2-демо-инфраструктуры-студии">Слайд 2 — Демо
|
||||
инфраструктуры студии</h2>
|
||||
<ul>
|
||||
<li>Агент подключается к любому звонку, рисует слайды по ходу разговора
|
||||
(задержка ~45 сек — лимит генерации).</li>
|
||||
<li>Роутер сам выбирает LM под контекст (визуализация сейчас — GLM 5.3
|
||||
Flash).</li>
|
||||
<li>RAG: «он знает всё, что мы делаем» — доступ к общей базе знаний
|
||||
кооператива.</li>
|
||||
<li>Леонид получил полезный инсайт уже из демо: на его домашней
|
||||
коробочке можно крутить быструю генерацию музыки/картинок/мини-видео —
|
||||
трек за минуту.</li>
|
||||
</ul>
|
||||
<h2 id="слайд-3-главный-контакт-пилот-мультик-по-рассказу-леонида">Слайд
|
||||
3 — Главный контакт: пилот «мультик по рассказу Леонида»</h2>
|
||||
<ul>
|
||||
<li>Леонид хочет мульт-адаптации своих рассказов (10–15 мин); готов
|
||||
<strong>финансово поддержать</strong> пилот: токены — точно,
|
||||
человеческий труд — обсудить. Ориентир бюджета «в пределах 1000» на
|
||||
эксперимент.</li>
|
||||
<li>Вход от студии: рассказ Леонида + свобода интерпретации, «никаких
|
||||
пожеланий-ограничений».</li>
|
||||
<li>Alex предложил: <strong>партнёр студии уже строит harness для
|
||||
генерации мультиков/клипов/читов</strong> — матч идеальный. Влад
|
||||
(профессиональный видеомейкер + озвучка) возьмёт видео-часть.</li>
|
||||
<li>Первый шаг — аудиоспектакль (дешевле и предсказуемее), затем
|
||||
видео.</li>
|
||||
</ul>
|
||||
<h2
|
||||
id="слайд-4-творческий-метод-леонида-поучительно-для-продуктовой-линзы">Слайд
|
||||
4 — Творческий метод Леонида (поучительно для продуктовой линзы)</h2>
|
||||
<ul>
|
||||
<li>История с хакатоном ~2018: обучали нейросеть на сольных песнях
|
||||
Леннона/Маккартни и классифицировали битловские — «десяток песен
|
||||
правильно заклассифицировали».</li>
|
||||
<li>Пишет с ИИ (своей оценки ~80% — его творчество, ~20% ИИ), честно
|
||||
маркирует «made with AI» на after.today.</li>
|
||||
<li>Бэкграунд-тезис: «литературные негры» и мастерская Микеланджело —
|
||||
ИИ-ассистированное творчество не ново, нова честность.</li>
|
||||
</ul>
|
||||
<h2 id="слайд-5-экономика-локального-инференса-спор-о-железе">Слайд 5 —
|
||||
Экономика локального инференса (спор о железе)</h2>
|
||||
<ul>
|
||||
<li>Леонид: DGX Spark за ~1% стоимости дома — нормальная цена за
|
||||
«домашний интеллект»; тренд — коробки станут достоянием домов.</li>
|
||||
<li>Алекс: облачный always-on — ок для большинства задач; топовые модели
|
||||
для разработки софта всё равно выигрывают у локальных.</li>
|
||||
<li>Сошлись: гибрид — локально быстрая генерация (эскизы, музыка),
|
||||
облако — для тяжёлых продуктовых задач. У студии в планах собственный
|
||||
inference-сервер (инференс в ~6 раз быстрее).</li>
|
||||
</ul>
|
||||
<h2 id="слайд-6-обмен-тезисами-как-строить-ai-компанию">Слайд 6 — Обмен
|
||||
тезисами: как строить AI-компанию</h2>
|
||||
<ul>
|
||||
<li><strong>Леонид:</strong> «Vision — говно, execution — всё» (шутка из
|
||||
EPAM). Из короткого промпта не может вырасти сложная система: 10 людей с
|
||||
одним промптом построят 10 одинаковых Uber’ов — вариабельность требует
|
||||
доп. информации.</li>
|
||||
<li><strong>Алекс:</strong> ядро студии — AI-ядро, постоянно
|
||||
перестраивающее паттерны (64 сейчас) при смене контекста — «долгосрочно
|
||||
самый большой актив».</li>
|
||||
<li>Стратегия: строить бизнес на нейро-инфраструктуре с самого начала →
|
||||
потом переводить на автопилот; незаменим только «CEO-решатель».</li>
|
||||
<li>Леонид о студии: «я теперь понимаю, чем твоя студия отличается от
|
||||
классической — изюминка в AI-ядре».</li>
|
||||
<li>Методология Леонида для deep-tech: начинать продавать ценность сразу
|
||||
(консалтинг целевого клиента), не влюбляясь в R&D.</li>
|
||||
</ul>
|
||||
<h2 id="слайд-7-договорённости-что-фиксируем">Слайд 7 — Договорённости
|
||||
(что фиксируем)</h2>
|
||||
<ol type="1">
|
||||
<li><strong>Пилот анимации по рассказу Леонида</strong> — Leonid
|
||||
покрывает токены (ориентир до 1000), студия делает (аудио → видео, ведёт
|
||||
Влад). Alex вернётся к Леониду с конкретным предложением.</li>
|
||||
<li><strong>Формат разговора продолжается</strong> — Леонид открыт к
|
||||
следующим сессиям.</li>
|
||||
<li><strong>По голосам/долям за прошлые инвестиции — Леонид
|
||||
отказался</strong> («это была шутка», «инвестирую из философских
|
||||
соображений, не создавайте ожиданий»). Вопрос закрыт, давления нет.</li>
|
||||
<li>Леонид ещё посмотрит презентацию студии (35-минутную) и вернётся с
|
||||
фидбэком.</li>
|
||||
</ol>
|
||||
<h2 id="слайд-8-что-студии-делать-дальше">Слайд 8 — Что студии делать
|
||||
дальше</h2>
|
||||
<ul>
|
||||
<li>Подготовить оффер по мульт-пилоту (объём, этап аудиоспектакля, смета
|
||||
токенов) — Alex озвучит Леониду предметно.</li>
|
||||
<li>Подключить партнёрский harness генерации мультиков к задаче Леонида
|
||||
как первый клиентский кейс.</li>
|
||||
<li>Зафиксировать кейс «агент-визуализатор на реальном внешнем звонке» —
|
||||
первое демо на живом стороннем человеке, положительный фидбэк.</li>
|
||||
</ul>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -1,57 +0,0 @@
|
|||
# Разговор Алекса с Леонидом — выжимка и договорённости
|
||||
*Встреча 143 · 2026-10-08 · ~2.5 часа · Jitsi + агент-визуализатор Simbiot*
|
||||
|
||||
---
|
||||
|
||||
## Слайд 1 — Кто есть кто
|
||||
|
||||
- **Леонид** — давний знакомый студии, начинающий писатель, инвестор в deep-tech (в т.ч. Anti-Aging Hero с физиком Петром Федичевым). Владеет DGX Spark 128GB (куплен за €5000), гоняет на нём CUDA-моделирование графов надёжности.
|
||||
- **Алекс** — показал студийную инфраструктуру: агент-визуализатор звонков (демо в реальном времени), собственный роутер LM-провайдеров, RAG по всем знаниям кооператива.
|
||||
- Демо произвело впечатление: «1-й раз такое в моей практике… польза очевидна».
|
||||
|
||||
## Слайд 2 — Демо инфраструктуры студии
|
||||
|
||||
- Агент подключается к любому звонку, рисует слайды по ходу разговора (задержка ~45 сек — лимит генерации).
|
||||
- Роутер сам выбирает LM под контекст (визуализация сейчас — GLM 5.3 Flash).
|
||||
- RAG: «он знает всё, что мы делаем» — доступ к общей базе знаний кооператива.
|
||||
- Леонид получил полезный инсайт уже из демо: на его домашней коробочке можно крутить быструю генерацию музыки/картинок/мини-видео — трек за минуту.
|
||||
|
||||
## Слайд 3 — Главный контакт: пилот «мультик по рассказу Леонида»
|
||||
|
||||
- Леонид хочет мульт-адаптации своих рассказов (10–15 мин); готов **финансово поддержать** пилот: токены — точно, человеческий труд — обсудить. Ориентир бюджета «в пределах 1000» на эксперимент.
|
||||
- Вход от студии: рассказ Леонида + свобода интерпретации, «никаких пожеланий-ограничений».
|
||||
- Alex предложил: **партнёр студии уже строит harness для генерации мультиков/клипов/читов** — матч идеальный. Влад (профессиональный видеомейкер + озвучка) возьмёт видео-часть.
|
||||
- Первый шаг — аудиоспектакль (дешевле и предсказуемее), затем видео.
|
||||
|
||||
## Слайд 4 — Творческий метод Леонида (поучительно для продуктовой линзы)
|
||||
|
||||
- История с хакатоном ~2018: обучали нейросеть на сольных песнях Леннона/Маккартни и классифицировали битловские — «десяток песен правильно заклассифицировали».
|
||||
- Пишет с ИИ (своей оценки ~80% — его творчество, ~20% ИИ), честно маркирует «made with AI» на after.today.
|
||||
- Бэкграунд-тезис: «литературные негры» и мастерская Микеланджело — ИИ-ассистированное творчество не ново, нова честность.
|
||||
|
||||
## Слайд 5 — Экономика локального инференса (спор о железе)
|
||||
|
||||
- Леонид: DGX Spark за ~1% стоимости дома — нормальная цена за «домашний интеллект»; тренд — коробки станут достоянием домов.
|
||||
- Алекс: облачный always-on — ок для большинства задач; топовые модели для разработки софта всё равно выигрывают у локальных.
|
||||
- Сошлись: гибрид — локально быстрая генерация (эскизы, музыка), облако — для тяжёлых продуктовых задач. У студии в планах собственный inference-сервер (инференс в ~6 раз быстрее).
|
||||
|
||||
## Слайд 6 — Обмен тезисами: как строить AI-компанию
|
||||
|
||||
- **Леонид:** «Vision — говно, execution — всё» (шутка из EPAM). Из короткого промпта не может вырасти сложная система: 10 людей с одним промптом построят 10 одинаковых Uber'ов — вариабельность требует доп. информации.
|
||||
- **Алекс:** ядро студии — AI-ядро, постоянно перестраивающее паттерны (64 сейчас) при смене контекста — «долгосрочно самый большой актив».
|
||||
- Стратегия: строить бизнес на нейро-инфраструктуре с самого начала → потом переводить на автопилот; незаменим только «CEO-решатель».
|
||||
- Леонид о студии: «я теперь понимаю, чем твоя студия отличается от классической — изюминка в AI-ядре».
|
||||
- Методология Леонида для deep-tech: начинать продавать ценность сразу (консалтинг целевого клиента), не влюбляясь в R&D.
|
||||
|
||||
## Слайд 7 — Договорённости (что фиксируем)
|
||||
|
||||
1. **Пилот анимации по рассказу Леонида** — Leonid покрывает токены (ориентир до 1000), студия делает (аудио → видео, ведёт Влад). Alex вернётся к Леониду с конкретным предложением.
|
||||
2. **Формат разговора продолжается** — Леонид открыт к следующим сессиям.
|
||||
3. **По голосам/долям за прошлые инвестиции — Леонид отказался** («это была шутка», «инвестирую из философских соображений, не создавайте ожиданий»). Вопрос закрыт, давления нет.
|
||||
4. Леонид ещё посмотрит презентацию студии (35-минутную) и вернётся с фидбэком.
|
||||
|
||||
## Слайд 8 — Что студии делать дальше
|
||||
|
||||
- Подготовить оффер по мульт-пилоту (объём, этап аудиоспектакля, смета токенов) — Alex озвучит Леониду предметно.
|
||||
- Подключить партнёрский harness генерации мультиков к задаче Леонида как первый клиентский кейс.
|
||||
- Зафиксировать кейс «агент-визуализатор на реальном внешнем звонке» — первое демо на живом стороннем человеке, положительный фидбэк.
|
||||
|
|
@ -1,436 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="ru">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
|
||||
<title>Встреча 143 · Алекс × Леонид — выжимка и договорённости</title>
|
||||
<style>
|
||||
:root{
|
||||
--bg:#0a0c12;
|
||||
--bg2:#0e1120;
|
||||
--ink:#f0eeff;
|
||||
--mut:#9a97b8;
|
||||
--dim:#6f6c8f;
|
||||
--vio:#8b5cf6;
|
||||
--vio2:#7c6cf5;
|
||||
--cyan:#5eead4;
|
||||
--amber:#fbbf24;
|
||||
--card:rgba(255,255,255,.035);
|
||||
--line:rgba(139,92,246,.28);
|
||||
--serif:Georgia,"Times New Roman",serif;
|
||||
--sans:-apple-system,BlinkMacSystemFont,"SF Pro Display","Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;
|
||||
--mono:ui-monospace,"SF Mono","Cascadia Code",Menlo,Consolas,monospace;
|
||||
}
|
||||
*{box-sizing:border-box;margin:0;padding:0}
|
||||
html,body{height:100%}
|
||||
body{
|
||||
background:var(--bg);
|
||||
color:var(--ink);
|
||||
font-family:var(--sans);
|
||||
overflow:hidden;
|
||||
-webkit-font-smoothing:antialiased;
|
||||
text-rendering:optimizeLegibility;
|
||||
}
|
||||
/* ---------- ambient backdrop ---------- */
|
||||
.bg{position:fixed;inset:0;z-index:0;pointer-events:none}
|
||||
.bg::before{
|
||||
content:"";position:absolute;inset:0;
|
||||
background:
|
||||
radial-gradient(60vw 60vw at 12% -10%, rgba(124,108,245,.20), transparent 62%),
|
||||
radial-gradient(55vw 55vw at 105% 112%, rgba(94,234,212,.09), transparent 60%),
|
||||
radial-gradient(70vw 40vw at 50% 120%, rgba(139,92,246,.10), transparent 65%),
|
||||
linear-gradient(180deg,var(--bg) 0%,var(--bg2) 100%);
|
||||
}
|
||||
.bg::after{ /* film grain */
|
||||
content:"";position:absolute;inset:0;opacity:.05;mix-blend-mode:overlay;
|
||||
background-image:url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='160' height='160'><filter id='n'><feTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='2'/></filter><rect width='160' height='160' filter='url(%23n)' opacity='0.6'/></svg>");
|
||||
}
|
||||
.vignette{position:fixed;inset:0;z-index:1;pointer-events:none;
|
||||
background:radial-gradient(120vw 90vh at 50% 50%, transparent 55%, rgba(0,0,0,.55) 100%)}
|
||||
|
||||
/* ---------- deck ---------- */
|
||||
.deck{position:relative;z-index:2;height:100%}
|
||||
.slide{
|
||||
position:absolute;inset:0;
|
||||
display:flex;flex-direction:column;justify-content:center;
|
||||
padding:9vh 8vw 12vh;
|
||||
opacity:0;transform:translateY(26px) scale(.985);
|
||||
transition:opacity .55s ease,transform .55s cubic-bezier(.22,.61,.36,1);
|
||||
pointer-events:none;visibility:hidden;
|
||||
}
|
||||
.slide.active{opacity:1;transform:none;pointer-events:auto;visibility:visible}
|
||||
.slide.prev{transform:translateY(-26px) scale(.985)}
|
||||
|
||||
.accent{position:absolute;top:0;left:0;right:0;height:6px;
|
||||
background:linear-gradient(90deg,var(--vio) 0%,#a78bfa 38%,var(--cyan) 100%);
|
||||
box-shadow:0 0 24px rgba(139,92,246,.55)}
|
||||
.ghost{
|
||||
position:absolute;right:4vw;bottom:2vh;z-index:-1;
|
||||
font-weight:900;font-size:clamp(160px,30vw,420px);line-height:1;
|
||||
color:transparent;-webkit-text-stroke:1.5px rgba(139,92,246,.13);
|
||||
letter-spacing:-.04em;user-select:none}
|
||||
|
||||
.kicker{
|
||||
display:inline-flex;align-items:center;gap:.6em;
|
||||
font-size:clamp(11px,1.15vw,14px);font-weight:700;letter-spacing:.24em;
|
||||
text-transform:uppercase;color:var(--vio2);margin-bottom:2.4vh;
|
||||
}
|
||||
.kicker::before{content:"";width:34px;height:2px;background:var(--vio);box-shadow:0 0 12px var(--vio)}
|
||||
h1{font-size:clamp(38px,6.6vw,86px);font-weight:900;letter-spacing:-.03em;line-height:1.04}
|
||||
h1 em{font-style:normal;background:linear-gradient(92deg,#a78bfa,var(--vio2) 55%,var(--cyan));
|
||||
-webkit-background-clip:text;background-clip:text;color:transparent}
|
||||
h2{font-size:clamp(28px,4vw,52px);font-weight:800;letter-spacing:-.025em;line-height:1.08;margin-bottom:3.6vh}
|
||||
h2 .alt{color:var(--vio2)}
|
||||
.sub{font-size:clamp(15px,1.6vw,21px);color:var(--mut);max-width:34em;line-height:1.55;margin-top:3vh}
|
||||
|
||||
.chips{display:flex;flex-wrap:wrap;gap:.7em;margin-top:4.5vh}
|
||||
.chip{
|
||||
font-family:var(--mono);font-size:clamp(11px,1.05vw,13px);color:var(--mut);
|
||||
padding:.55em 1.1em;border:1px solid var(--line);border-radius:999px;
|
||||
background:rgba(139,92,246,.07)}
|
||||
.chip b{color:var(--ink);font-weight:600}
|
||||
|
||||
/* ---------- content blocks ---------- */
|
||||
.grid{display:grid;gap:1.5vh 1.6vw}
|
||||
.g2{grid-template-columns:repeat(2,1fr)}
|
||||
.g3{grid-template-columns:repeat(3,1fr)}
|
||||
@media(max-width:860px){.g2,.g3{grid-template-columns:1fr}}
|
||||
|
||||
.card{
|
||||
background:var(--card);border:1px solid rgba(255,255,255,.07);
|
||||
border-radius:18px;padding:2.6vh 1.6vw;position:relative;overflow:hidden;
|
||||
backdrop-filter:blur(6px);
|
||||
transition:border-color .3s,transform .3s;
|
||||
}
|
||||
.card:hover{border-color:var(--line);transform:translateY(-2px)}
|
||||
.card::before{content:"";position:absolute;left:0;top:0;bottom:0;width:3px;
|
||||
background:linear-gradient(180deg,var(--vio),transparent 85%);opacity:.85}
|
||||
.card h3{font-size:clamp(15px,1.45vw,19px);font-weight:700;margin-bottom:1vh;letter-spacing:-.01em}
|
||||
.card p,.card li{font-size:clamp(13px,1.28vw,17px);color:var(--mut);line-height:1.62}
|
||||
.card p b,.card li b{color:var(--ink);font-weight:600}
|
||||
.tag{display:inline-block;font-family:var(--mono);font-size:clamp(10px,.95vw,12px);
|
||||
color:var(--cyan);border:1px solid rgba(94,234,212,.3);border-radius:6px;
|
||||
padding:.15em .6em;margin-bottom:1.2vh;letter-spacing:.06em}
|
||||
.tag.v{color:#c4b5fd;border-color:rgba(139,92,246,.4)}
|
||||
.tag.a{color:var(--amber);border-color:rgba(251,191,36,.35)}
|
||||
|
||||
ul.list{list-style:none;display:grid;gap:1.7vh}
|
||||
ul.list li{
|
||||
display:flex;gap:1em;align-items:flex-start;
|
||||
font-size:clamp(14px,1.5vw,19px);color:var(--mut);line-height:1.6;
|
||||
}
|
||||
ul.list li::before{
|
||||
content:"▸";color:var(--vio);font-size:1.05em;line-height:1.5;
|
||||
text-shadow:0 0 14px rgba(139,92,246,.8);flex:0 0 auto}
|
||||
ul.list b{color:var(--ink);font-weight:650}
|
||||
ul.list .hl{color:var(--cyan)}
|
||||
|
||||
.quote{
|
||||
margin-top:3.2vh;padding:2.4vh 2vw;border-left:3px solid var(--vio);
|
||||
background:linear-gradient(90deg,rgba(139,92,246,.12),transparent 72%);
|
||||
border-radius:0 14px 14px 0;
|
||||
}
|
||||
.quote p{font-family:var(--serif);font-style:italic;
|
||||
font-size:clamp(17px,2.1vw,27px);line-height:1.45;color:#e4e0ff}
|
||||
.quote cite{display:block;margin-top:1.2vh;font-style:normal;font-family:var(--mono);
|
||||
font-size:clamp(10px,1vw,13px);color:var(--dim);letter-spacing:.12em;text-transform:uppercase}
|
||||
|
||||
.num{display:flex;gap:1.1em;align-items:flex-start;padding:1.9vh 0;
|
||||
border-bottom:1px dashed rgba(255,255,255,.09)}
|
||||
.num:last-child{border-bottom:none}
|
||||
.num i{
|
||||
font-style:normal;font-family:var(--mono);flex:0 0 auto;
|
||||
width:2.1em;height:2.1em;display:grid;place-items:center;border-radius:10px;
|
||||
background:rgba(139,92,246,.16);border:1px solid var(--line);
|
||||
color:#c4b5fd;font-size:clamp(12px,1.15vw,15px);font-weight:700}
|
||||
.num h3{font-size:clamp(15px,1.5vw,19px);font-weight:700;margin-bottom:.5vh}
|
||||
.num p{font-size:clamp(13px,1.3vw,16px);color:var(--mut);line-height:1.6}
|
||||
.num b{color:var(--ink);font-weight:600}
|
||||
|
||||
.vs{display:grid;grid-template-columns:1fr auto 1fr;gap:1.2vw;align-items:stretch}
|
||||
@media(max-width:860px){.vs{grid-template-columns:1fr}.vs .vsx{display:none}}
|
||||
.vs .card{height:100%}
|
||||
.vs .who{font-family:var(--mono);font-size:clamp(10px,.95vw,12px);letter-spacing:.14em;
|
||||
text-transform:uppercase;color:var(--vio2);margin-bottom:1.2vh}
|
||||
.vsx{display:grid;place-items:center;color:var(--dim);font-size:clamp(18px,2vw,26px)}
|
||||
|
||||
.big{font-size:clamp(30px,4.6vw,64px);font-weight:900;letter-spacing:-.03em;line-height:1.1}
|
||||
.big em{font-style:normal;color:var(--vio2)}
|
||||
|
||||
/* ---------- chrome ---------- */
|
||||
.progress{position:fixed;left:0;bottom:0;height:3px;z-index:10;width:0;
|
||||
background:linear-gradient(90deg,var(--vio),#a78bfa,var(--cyan));
|
||||
box-shadow:0 0 14px rgba(139,92,246,.8);transition:width .45s ease}
|
||||
.hud{position:fixed;right:2.2vw;bottom:3vh;z-index:10;display:flex;gap:.6em;align-items:center}
|
||||
.hud .count{font-family:var(--mono);font-size:12px;color:var(--dim);letter-spacing:.1em;margin-right:.8em}
|
||||
.hud .count b{color:var(--mut);font-weight:600}
|
||||
.hud button{
|
||||
width:40px;height:40px;border-radius:12px;cursor:pointer;
|
||||
border:1px solid var(--line);background:rgba(139,92,246,.1);color:#c4b5fd;
|
||||
font-size:16px;display:grid;place-items:center;transition:.25s}
|
||||
.hud button:hover{background:rgba(139,92,246,.25);color:#fff;transform:translateY(-1px)}
|
||||
.hint{position:fixed;left:2.2vw;bottom:3vh;z-index:10;
|
||||
font-family:var(--mono);font-size:11px;color:var(--dim);letter-spacing:.08em;opacity:.8}
|
||||
.brand{position:fixed;top:2.6vh;right:2.2vw;z-index:10;
|
||||
font-family:var(--mono);font-size:11px;letter-spacing:.22em;color:var(--dim);text-transform:uppercase}
|
||||
.brand b{color:var(--vio2)}
|
||||
.dots{position:fixed;right:1.4vw;top:50%;transform:translateY(-50%);z-index:10;
|
||||
display:flex;flex-direction:column;gap:.55em}
|
||||
.dots i{width:7px;height:7px;border-radius:50%;background:rgba(255,255,255,.16);cursor:pointer;transition:.3s}
|
||||
.dots i.on{background:var(--vio);box-shadow:0 0 10px var(--vio);transform:scale(1.35)}
|
||||
@media(max-width:700px){.dots{display:none}.hint{display:none}}
|
||||
|
||||
.end .sig{font-family:var(--mono);font-size:clamp(10px,1vw,13px);color:var(--dim);
|
||||
letter-spacing:.18em;text-transform:uppercase;margin-top:5vh}
|
||||
|
||||
/* ---------- print ---------- */
|
||||
@media print{
|
||||
body{overflow:visible;background:#0a0c12}
|
||||
.bg,.vignette,.hud,.hint,.dots,.progress,.brand{display:none!important}
|
||||
.deck{height:auto}
|
||||
.slide{position:relative;inset:auto;opacity:1!important;transform:none!important;
|
||||
visibility:visible!important;pointer-events:auto;height:100vh;page-break-after:always}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="bg"></div><div class="vignette"></div>
|
||||
<div class="brand">Simbiot · <b>Кооператив 42</b></div>
|
||||
|
||||
<div class="deck" id="deck">
|
||||
|
||||
<!-- 01 · TITLE -->
|
||||
<section class="slide">
|
||||
<div class="accent"></div><div class="ghost">143</div>
|
||||
<div class="kicker">Встреча 143 · 08.10.2026 · Jitsi + агент-визуализатор Simbiot</div>
|
||||
<h1>Разговор<br>Алекса <em>× Леонида</em></h1>
|
||||
<p class="sub">Выжимка двухсполовинойчасовой встречи: инфраструктура студии, пилот «мультик по рассказу», экономика локального инференса — и договорённости, которые фиксируем.</p>
|
||||
<div class="chips">
|
||||
<span class="chip"><b>~2.5</b> часа разговора</span>
|
||||
<span class="chip"><b>8</b> тем</span>
|
||||
<span class="chip"><b>4</b> договорённости</span>
|
||||
<span class="chip">слайды рисовал агент <b>live</b></span>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 02 · WHO -->
|
||||
<section class="slide">
|
||||
<div class="accent"></div><div class="ghost">01</div>
|
||||
<div class="kicker">Кто есть кто</div>
|
||||
<h2>Два мира на одном звонке</h2>
|
||||
<div class="grid g2">
|
||||
<div class="card">
|
||||
<div class="who tag v">Леонид</div>
|
||||
<h3>Писатель · инвестор в deep-tech</h3>
|
||||
<p>Давний знакомый студии. Инвестирует в deep-tech, в том числе <b>Anti-Aging Hero</b> с физиком Петром Федичевым. Дома — <b>DGX Spark 128GB</b> (€5000), на которой он гоняет CUDA-моделирование графов надёжности.</p>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="tag">Алекс · студия</div>
|
||||
<h3>Живая AI-инфраструктура</h3>
|
||||
<p>Показал, чем студия отличается от классической: <b>агент-визуализатор звонков</b> (демо в реальном времени), собственный <b>роутер LM-провайдеров</b> и <b>RAG</b> по всем знаниям кооператива.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="quote">
|
||||
<p>«Первый раз такое в моей практике… польза очевидна»</p>
|
||||
<cite>Леонид — после демо агент-визуализатора</cite>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 03 · DEMO -->
|
||||
<section class="slide">
|
||||
<div class="accent"></div><div class="ghost">02</div>
|
||||
<div class="kicker">Демо инфраструктуры</div>
|
||||
<h2>Что увидел Леонид <span class="alt">в прямом эфире</span></h2>
|
||||
<ul class="list">
|
||||
<li><span><b>Агент подключается к любому звонку</b> и рисует слайды по ходу разговора — задержка ~45 секунд (лимит генерации)</span></li>
|
||||
<li><span><b>Роутер сам выбирает модель под контекст</b> — визуализация идёт на GLM 5.3 Flash</span></li>
|
||||
<li><span><b>RAG по базе кооператива:</b> «он знает всё, что мы делаем» — агент отвечает со знанием всей истории студии</span></li>
|
||||
<li><span>Инсайт Леонида из демо: <span class="hl">на его домашней коробочке можно крутить быструю генерацию</span> — музыку, картинки, мини-видео. Трек за минуту</span></li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<!-- 04 · PILOT -->
|
||||
<section class="slide">
|
||||
<div class="accent"></div><div class="ghost">03</div>
|
||||
<div class="kicker">Главный контакт встречи</div>
|
||||
<h2>Пилот: <span class="alt">мультик по рассказу Леонида</span></h2>
|
||||
<div class="grid g2">
|
||||
<div class="card">
|
||||
<div class="tag a">Что хочет Леонид</div>
|
||||
<p><b>Мульт-адаптация своих рассказов</b> (10–15 минут). Готов финансово поддержать пилот: <b>токены — точно</b>, человеческий труд — обсудить. Ориентир бюджета — <b>«в пределах 1000»</b> на эксперимент.</p>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="tag v">Что входит студия</div>
|
||||
<p>Рассказ Леонида + <b>свобода интерпретации</b> («никаких пожеланий-ограничений»). Со стороны студии — партнёр уже строит <b>harness генерации мультиков</b>, видео-часть ведёт <b>Влад</b> (профессиональный видеомейкер + озвучка). Матч идеальный.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="quote">
|
||||
<p>Первый шаг — аудиоспектакль: дешевле и предсказуемее. Затем видео.</p>
|
||||
<cite>Дорожная карта пилота</cite>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 05 · METHOD -->
|
||||
<section class="slide">
|
||||
<div class="accent"></div><div class="ghost">04</div>
|
||||
<div class="kicker">Творческий метод Леонида</div>
|
||||
<h2>Поучительно для <span class="alt">продуктовой линзы</span></h2>
|
||||
<ul class="list">
|
||||
<li><span><b>Хакатон ~2018:</b> обучали нейросеть на сольных песнях Леннона и Маккартни, затем классифицировали битловские — «десяток песен правильно заклассифицировали»</span></li>
|
||||
<li><span><b>Пишет с ИИ:</b> по собственной оценке ~80% — его творчество, ~20% — ИИ. Честно маркирует <span class="hl">«made with AI»</span> на after.today</span></li>
|
||||
<li><span><b>Бэкграунд-тезис:</b> «литературные негры» и мастерская Микеланджело — ИИ-ассистированное творчество не ново. <b>Нова честность.</b></span></li>
|
||||
</ul>
|
||||
<div class="quote">
|
||||
<p>ИИ-ассистированное творчество не ново — нова честность.</p>
|
||||
<cite>Леонид</cite>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 06 · HARDWARE -->
|
||||
<section class="slide">
|
||||
<div class="accent"></div><div class="ghost">05</div>
|
||||
<div class="kicker">Экономика локального инференса</div>
|
||||
<h2>Спор о железе: <span class="alt">дом против облака</span></h2>
|
||||
<div class="vs">
|
||||
<div class="card">
|
||||
<div class="who">Позиция Леонида</div>
|
||||
<p><b>DGX Spark за ~1% стоимости дома</b> — нормальная цена за «домашний интеллект». Тренд: коробки станут достоянием домов.</p>
|
||||
</div>
|
||||
<div class="vsx">⚡</div>
|
||||
<div class="card">
|
||||
<div class="who">Позиция Алекса</div>
|
||||
<p><b>Облачный always-on</b> — ок для большинства задач. Топовые модели для разработки софта всё равно выигрывают у локальных.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="quote">
|
||||
<p>Сошлись на гибриде: локально — быстрая генерация (эскизы, музыка), облако — тяжёлые продуктовые задачи. У студии в планах собственный inference-сервер — в ~6 раз быстрее.</p>
|
||||
<cite>Консенсус встречи</cite>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 07 · AI COMPANY -->
|
||||
<section class="slide">
|
||||
<div class="accent"></div><div class="ghost">06</div>
|
||||
<div class="kicker">Обмен тезисами</div>
|
||||
<h2>Как строить <span class="alt">AI-компанию</span></h2>
|
||||
<div class="grid g2">
|
||||
<div class="card">
|
||||
<div class="tag">Тезис Леонида</div>
|
||||
<p><b>«Vision — говно, execution — всё»</b> (шутка из EPAM). Из короткого промпта не вырастает сложная система: 10 людей с одним промптом построят 10 одинаковых Uber'ов — вариабельность требует дополнительной информации.</p>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="tag v">Тезис Алекса</div>
|
||||
<p>Ядро студии — <b>AI-ядро, постоянно перестраивающее паттерны</b> (64 сейчас) при смене контекста. «Долгосрочно — самый большой актив». Строить бизнес на нейро-инфраструктуре с самого начала → затем автопилот. Незаменим только <b>«CEO-решатель»</b>.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="quote">
|
||||
<p>«Я теперь понимаю, чем твоя студия отличается от классической — изюминка в AI-ядре»</p>
|
||||
<cite>Леонид · метод deep-tech: продавать ценность сразу, не влюбляясь в R&D</cite>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 08 · DEALS -->
|
||||
<section class="slide">
|
||||
<div class="accent"></div><div class="ghost">07</div>
|
||||
<div class="kicker">Итоги встречи</div>
|
||||
<h2>Договорённости — <span class="alt">что фиксируем</span></h2>
|
||||
<div class="num"><i>1</i><div>
|
||||
<h3>Пилот анимации по рассказу Леонида</h3>
|
||||
<p>Леонид покрывает токены (ориентир до <b>1000</b>), студия делает: аудио → видео, ведёт Влад. Алекс вернётся с <b>конкретным предложением</b>.</p>
|
||||
</div></div>
|
||||
<div class="num"><i>2</i><div>
|
||||
<h3>Формат разговора продолжается</h3>
|
||||
<p>Леонид открыт к следующим сессиям со студией.</p>
|
||||
</div></div>
|
||||
<div class="num"><i>3</i><div>
|
||||
<h3>Голоса и доли за прошлые инвестиции — вопрос закрыт</h3>
|
||||
<p>Леонид отказался: «это была шутка», «инвестирую из философских соображений, не создавайте ожиданий». Давления нет.</p>
|
||||
</div></div>
|
||||
<div class="num"><i>4</i><div>
|
||||
<h3>Следующий шаг Леонида</h3>
|
||||
<p>Посмотрит 35-минутную презентацию студии и вернётся с фидбэком.</p>
|
||||
</div></div>
|
||||
</section>
|
||||
|
||||
<!-- 09 · NEXT -->
|
||||
<section class="slide">
|
||||
<div class="accent"></div><div class="ghost">08</div>
|
||||
<div class="kicker">Action items</div>
|
||||
<h2>Что студии делать <span class="alt">дальше</span></h2>
|
||||
<ul class="list">
|
||||
<li><span><b>Подготовить оффер по мульт-пилоту</b> — объём, этап аудиоспектакля, смета токенов. Алекс озвучит Леониду предметно.</span></li>
|
||||
<li><span><b>Подключить партнёрский harness генерации мультиков</b> к задаче Леонида — как первый клиентский кейс.</span></li>
|
||||
<li><span><b>Зафиксировать кейс «агент-визуализатор на реальном внешнем звонке»</b> — первое демо на живом стороннем человеке, положительный фидбэк.</span></li>
|
||||
</ul>
|
||||
<div class="quote">
|
||||
<p>От живого демо на звонке — к первому платному пилоту по генерации контента.</p>
|
||||
<cite>Смысл встречи 143</cite>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 10 · END -->
|
||||
<section class="slide end">
|
||||
<div class="accent"></div><div class="ghost">42</div>
|
||||
<div class="kicker">Встреча 143 · 08.10.2026</div>
|
||||
<div class="big">«Изюминка — <em>в AI-ядре»</em></div>
|
||||
<p class="sub">Simbiot · Кооператив 42 · агент-визуализатор, LM-роутер, RAG по знаниям студии. Презентация собрана из выжимки разговора — тем же агентом, что рисовал слайды live.</p>
|
||||
<div class="sig">← → навигация · F — во весь экран · P — печать/PDF</div>
|
||||
</section>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="dots" id="dots"></div>
|
||||
<div class="progress" id="prog"></div>
|
||||
<div class="hud">
|
||||
<span class="count" id="count"></span>
|
||||
<button id="prev" title="Назад (←)">←</button>
|
||||
<button id="next" title="Вперёд (→)">→</button>
|
||||
</div>
|
||||
<div class="hint">← → · свайп · F — фулскрин</div>
|
||||
|
||||
<script>
|
||||
(function(){
|
||||
var slides=[].slice.call(document.querySelectorAll('.slide'));
|
||||
var dots=document.getElementById('dots'),prog=document.getElementById('prog'),cnt=document.getElementById('count');
|
||||
var i=0,n=slides.length;
|
||||
slides.forEach(function(_,k){var d=document.createElement('i');d.onclick=function(){go(k)};dots.appendChild(d);});
|
||||
function pad(x){return (x<10?'0':'')+x}
|
||||
function go(k){
|
||||
k=Math.max(0,Math.min(n-1,k));
|
||||
if(k===i&&slides[i].classList.contains('active'))return;
|
||||
slides.forEach(function(s,j){
|
||||
s.classList.toggle('active',j===k);
|
||||
s.classList.toggle('prev',j<k);
|
||||
});
|
||||
i=k;
|
||||
prog.style.width=((i+1)/n*100)+'%';
|
||||
cnt.innerHTML='<b>'+pad(i+1)+'</b> / '+pad(n);
|
||||
[].forEach.call(dots.children,function(d,j){d.classList.toggle('on',j===i)});
|
||||
if(history.replaceState)history.replaceState(null,'','#'+(i+1));
|
||||
}
|
||||
document.getElementById('next').onclick=function(){go(i+1)};
|
||||
document.getElementById('prev').onclick=function(){go(i-1)};
|
||||
addEventListener('keydown',function(e){
|
||||
if(e.key==='ArrowRight'||e.key==='PageDown'||e.key===' '){e.preventDefault();go(i+1)}
|
||||
else if(e.key==='ArrowLeft'||e.key==='PageUp'){e.preventDefault();go(i-1)}
|
||||
else if(e.key==='Home'){go(0)}
|
||||
else if(e.key==='End'){go(n-1)}
|
||||
else if(e.key==='f'||e.key==='F'||e.key==='а'||e.key==='А'){
|
||||
if(document.fullscreenElement){document.exitFullscreen&&document.exitFullscreen()}
|
||||
else{document.documentElement.requestFullscreen&&document.documentElement.requestFullscreen()}
|
||||
}
|
||||
});
|
||||
var tx=null,ty=null;
|
||||
addEventListener('touchstart',function(e){tx=e.touches[0].clientX;ty=e.touches[0].clientY},{passive:true});
|
||||
addEventListener('touchend',function(e){
|
||||
if(tx===null)return;
|
||||
var dx=e.changedTouches[0].clientX-tx,dy=e.changedTouches[0].clientY-ty;
|
||||
if(Math.abs(dx)>60&&Math.abs(dx)>Math.abs(dy)){go(dx<0?i+1:i-1)}
|
||||
tx=null;
|
||||
},{passive:true});
|
||||
var h=parseInt((location.hash||'').replace('#',''),10);
|
||||
go(isNaN(h)?0:h-1);
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -1,54 +0,0 @@
|
|||
# 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.
|
||||
Binary file not shown.
|
|
@ -1,164 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Simbiot UI — Claude Code in your browser. $99.9/mo, seat included</title>
|
||||
<meta name="description" content="Simbiot UI gives your team a deployed Claude Code web interface: agent mailbox, studio RAG, signed audit. No setup, keys included, $99.9/mo.">
|
||||
<meta property="og:title" content="Simbiot UI — Claude Code in your browser">
|
||||
<meta property="og:description" content="Deployed Claude Code web UI with agent mailbox and studio knowledge base. $99.9/mo, claude seat included.">
|
||||
<meta property="og:url" content="https://ui.cil.by/">
|
||||
<meta property="og:type" content="website">
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter+Tight:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
:root{
|
||||
--paper:#faf8f3; --paper2:#f2efe7; --ink:#1a1a18; --ink2:#55534b; --ink3:#6b685c;
|
||||
--line:#e2ded2; --line2:#cfcabb; --acc:#5b4be0; --acc2:#3d2fae;
|
||||
}
|
||||
*{margin:0;padding:0;box-sizing:border-box}
|
||||
html{scroll-behavior:smooth}
|
||||
body{font-family:'Inter Tight',system-ui,-apple-system,sans-serif;background:var(--paper);color:var(--ink);-webkit-font-smoothing:antialiased;overflow-x:hidden}
|
||||
.mono{font-family:'JetBrains Mono',ui-monospace,monospace}
|
||||
.wrap{max-width:1140px;margin:0 auto;padding:0 32px}
|
||||
a{color:var(--acc)}
|
||||
:focus-visible{outline:2px solid var(--acc);outline-offset:2px}
|
||||
nav{position:fixed;top:0;left:0;right:0;z-index:100;background:rgba(250,248,243,.92);backdrop-filter:blur(8px);border-bottom:1px solid var(--line)}
|
||||
nav .wrap{display:flex;align-items:center;height:60px}
|
||||
.logo{display:flex;align-items:center;gap:10px;font-size:17px;font-weight:600;letter-spacing:-.3px}
|
||||
.logo-mark{width:14px;height:14px;background:var(--acc)}
|
||||
nav .cta{margin-left:auto;display:flex;gap:16px;align-items:center}
|
||||
.btn{display:inline-block;font-weight:600;font-size:15px;padding:11px 22px;border-radius:10px;text-decoration:none;transition:transform .15s ease,box-shadow .15s ease}
|
||||
.btn:active{transform:translateY(1px)}
|
||||
.btn-acc{background:var(--acc);color:#fff}
|
||||
.btn-acc:hover{background:var(--acc2);box-shadow:0 6px 20px rgba(91,75,224,.25)}
|
||||
.btn-ghost{border:1.5px solid var(--line2);color:var(--ink)}
|
||||
.btn-ghost:hover{border-color:var(--acc);color:var(--acc)}
|
||||
.hero{padding:150px 0 90px;position:relative}
|
||||
.kicker{font-family:'JetBrains Mono',monospace;font-size:12.5px;letter-spacing:.12em;text-transform:uppercase;color:var(--acc);font-weight:500;margin-bottom:18px}
|
||||
h1{font-size:clamp(38px,5.6vw,64px);line-height:1.04;letter-spacing:-.02em;font-weight:700;max-width:900px}
|
||||
h1 em{font-style:normal;color:var(--acc)}
|
||||
.sub{margin-top:22px;font-size:19px;line-height:1.55;color:var(--ink2);max-width:640px}
|
||||
.hero-cta{margin-top:34px;display:flex;gap:14px;flex-wrap:wrap}
|
||||
.price-pill{display:inline-flex;align-items:baseline;gap:8px;margin-top:26px;padding:10px 18px;border:1px solid var(--line2);border-radius:999px;background:#fff}
|
||||
.price-pill b{font-size:22px;letter-spacing:-.02em}
|
||||
.price-pill span{color:var(--ink3);font-size:14px}
|
||||
.grid{display:grid;grid-template-columns:repeat(3,1fr);gap:18px;margin-top:64px}
|
||||
.card{background:#fff;border:1px solid var(--line);border-radius:14px;padding:24px 22px}
|
||||
.card h3{font-size:16.5px;font-weight:600;margin-bottom:8px}
|
||||
.card p{font-size:14.5px;line-height:1.55;color:var(--ink2)}
|
||||
.card .n{font-family:'JetBrains Mono',monospace;font-size:11.5px;color:var(--acc);letter-spacing:.1em;margin-bottom:10px;display:block}
|
||||
section{padding:76px 0}
|
||||
h2{font-size:clamp(26px,3.4vw,38px);letter-spacing:-.02em;font-weight:700;max-width:760px}
|
||||
.h2sub{margin-top:14px;color:var(--ink2);font-size:17px;max-width:640px;line-height:1.5}
|
||||
.feat{display:grid;grid-template-columns:repeat(2,1fr);gap:0;margin-top:44px;border:1px solid var(--line);border-radius:14px;overflow:hidden;background:#fff}
|
||||
.feat>div{padding:26px 26px;border-bottom:1px solid var(--line)}
|
||||
.feat>div:nth-child(odd){border-right:1px solid var(--line)}
|
||||
.feat>div:nth-last-child(-n+2){border-bottom:none}
|
||||
.feat b{display:block;font-size:16px;margin-bottom:6px}
|
||||
.feat p{font-size:14.5px;line-height:1.55;color:var(--ink2)}
|
||||
.steps{counter-reset:s;margin-top:44px;display:grid;grid-template-columns:repeat(4,1fr);gap:18px}
|
||||
.step{background:#fff;border:1px solid var(--line);border-radius:14px;padding:24px 20px;position:relative}
|
||||
.step:before{counter-increment:s;content:counter(s,decimal-leading-zero);font-family:'JetBrains Mono',monospace;font-size:12px;color:var(--acc);letter-spacing:.1em}
|
||||
.step b{display:block;margin:10px 0 6px;font-size:15.5px}
|
||||
.step p{font-size:14px;line-height:1.5;color:var(--ink2)}
|
||||
.faq{margin-top:44px;max-width:760px}
|
||||
.faq details{background:#fff;border:1px solid var(--line);border-radius:12px;padding:18px 20px;margin-bottom:10px}
|
||||
.faq summary{cursor:pointer;font-weight:600;font-size:15.5px;list-style:none;display:flex;justify-content:space-between;align-items:center}
|
||||
.faq summary:after{content:'+';color:var(--acc);font-size:18px}
|
||||
.faq details[open] summary:after{content:'–'}
|
||||
.faq details p{margin-top:10px;font-size:14.5px;line-height:1.6;color:var(--ink2)}
|
||||
form{margin-top:40px;background:#fff;border:1px solid var(--line);border-radius:16px;padding:30px;max-width:640px}
|
||||
label{display:block;font-size:13px;font-weight:600;color:var(--ink2);margin:14px 0 6px}
|
||||
input,textarea{width:100%;font:inherit;font-size:15px;padding:11px 14px;border:1.5px solid var(--line2);border-radius:10px;background:var(--paper);color:var(--ink)}
|
||||
input:focus,textarea:focus{outline:none;border-color:var(--acc)}
|
||||
textarea{min-height:84px;resize:vertical}
|
||||
.submit{margin-top:20px;width:100%;border:none;font-size:16px;cursor:pointer}
|
||||
.note{margin-top:14px;font-size:13px;color:var(--ink3);line-height:1.5}
|
||||
footer{border-top:1px solid var(--line);padding:40px 0 60px;color:var(--ink3);font-size:13.5px}
|
||||
footer .wrap{display:flex;justify-content:space-between;gap:20px;flex-wrap:wrap}
|
||||
@media(max-width:900px){.grid,.steps{grid-template-columns:1fr 1fr}.feat{grid-template-columns:1fr}.feat>div:nth-child(odd){border-right:none}}
|
||||
@media(max-width:600px){.grid,.steps{grid-template-columns:1fr}.hero{padding:120px 0 60px}}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<nav><div class="wrap">
|
||||
<div class="logo"><span class="logo-mark"></span>Simbiot UI</div>
|
||||
<div class="cta"><a class="btn btn-acc" href="#apply">Apply for access</a></div>
|
||||
</div></nav>
|
||||
|
||||
<header class="hero wrap" id="top">
|
||||
<div class="kicker">Simbiot UI · by Cooperative 42</div>
|
||||
<h1>Claude Code in your browser, <em>deployed and maintained</em> for you.</h1>
|
||||
<p class="sub">No installs, no setup, no API-key juggling. We provision the workspace, the agent mailbox and the model access — you open a tab and build.</p>
|
||||
<div class="hero-cta">
|
||||
<a class="btn btn-acc" href="#apply">Request access</a>
|
||||
<a class="btn btn-ghost" href="#how">How it works</a>
|
||||
</div>
|
||||
<div class="price-pill"><b class="mono">$99.9</b><span>/ month · claude seat included · cancel anytime</span></div>
|
||||
<div class="grid">
|
||||
<div class="card"><span class="n">ZERO-SETUP</span><h3>Works out of the box</h3><p>A deployed web UI for Claude Code (Simbiot Dashboard for monitoring). Browser only — nothing to install or configure.</p></div>
|
||||
<div class="card"><span class="n">SEAT-INCLUDED</span><h3>Model access included</h3><p>Claude seat is part of the plan — you don't bring keys, you don't see ours. Fair-use limits keep it fast for everyone.</p></div>
|
||||
<div class="card"><span class="n">STUDIO-KNOWLEDGE</span><h3>Studio RAG built in</h3><p>Your workspace knows the cooperative's playbook: how to build startups, studio terms and processes — searchable by your agent.</p></div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<section id="how" class="wrap">
|
||||
<h2>How it works</h2>
|
||||
<p class="h2sub">We onboard every workspace by hand for the first cohort — that keeps quality high while we scale.</p>
|
||||
<div class="steps">
|
||||
<div class="step"><b>Apply</b><p>Tell us about your team and what you're building. Takes a minute.</p></div>
|
||||
<div class="step"><b>We provision</b><p>Your workspace, agent mailbox and Claude seat are set up by us — usually within a day.</p></div>
|
||||
<div class="step"><b>You get credentials</b><p>Login, password and URL arrive by email, with a starter guide for your agent.</p></div>
|
||||
<div class="step"><b>Build</b><p>Open Simbiot UI and work with Claude Code in the browser. Support channel included.</p></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="wrap" id="features" style="padding-top:0">
|
||||
<h2>What's inside</h2>
|
||||
<div class="feat">
|
||||
<div><b>Personal workspace</b><p>Isolated container per client: your files, your agent, your limits. Others can't see your data, you can't see theirs.</p></div>
|
||||
<div><b>Agent mailbox</b><p>Every workspace ships with an agent email address on our domain — your Claude can send and receive mail as part of the workflow.</p></div>
|
||||
<div><b>Model gateway</b><p>All model traffic goes through our gateway with per-client tracking and fair-use limits. Predictable, monitored, no surprise bills.</p></div>
|
||||
<div><b>Signed & auditable</b><p>Every agent action is signed and auditable — the same audit spine the cooperative runs on every day.</p></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="wrap" id="faq">
|
||||
<h2>FAQ</h2>
|
||||
<div class="faq">
|
||||
<details><summary>Do I need my own API keys or Claude account?</summary><p>No. The claude seat is included in the $99.9/mo price — model access is provisioned and managed by us.</p></details>
|
||||
<details><summary>How fast can I start?</summary><p>We onboard the first cohort manually: apply, we confirm, you get credentials — typically within one business day.</p></details>
|
||||
<details><summary>What's the studio knowledge base?</summary><p>A curated, read-only RAG with the studio's startup-building methodology and terms — available to your agent inside the workspace. Private cooperative data is not included.</p></details>
|
||||
<details><summary>Is my data isolated?</summary><p>Yes — one isolated container per client with its own volume and resource limits.</p></details>
|
||||
<details><summary>Can I cancel?</summary><p>Month-to-month subscription, cancel anytime. We'll help you export your files.</p></details>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="wrap" id="apply">
|
||||
<h2>Request access</h2>
|
||||
<p class="h2sub">First cohort is onboarded personally — leave your details and we'll get back to you.</p>
|
||||
<form action="https://formsubmit.co/hello@cil.by" method="POST">
|
||||
<input type="hidden" name="_subject" value="Simbiot UI — access request">
|
||||
<label for="name">Name</label>
|
||||
<input id="name" name="name" required placeholder="Jane Doe">
|
||||
<label for="email">Email</label>
|
||||
<input id="email" name="email" type="email" required placeholder="jane@team.dev">
|
||||
<label for="tg">Telegram (optional)</label>
|
||||
<input id="tg" name="telegram" placeholder="@jane">
|
||||
<label for="about">What will you build?</label>
|
||||
<textarea id="about" name="about" placeholder="2–3 sentences about your team and tasks"></textarea>
|
||||
<button class="btn btn-acc submit" type="submit">Apply — $99.9/mo after confirmation</button>
|
||||
<p class="note">We reply personally, no auto-billing. Payment link arrives after your workspace is confirmed. Month-to-month, cancel anytime.</p>
|
||||
<p class="note"><span class="mono">TODO(deploy):</span> form endpoint is a placeholder (hello@cil.by) — wire to the chosen application channel (email agent / TG bot / portal) before deploying. See docs/ROADMAP-SIMBIOT-UI.md.</p>
|
||||
</form>
|
||||
</section>
|
||||
|
||||
<footer>
|
||||
<div class="wrap">
|
||||
<div>© 2026 Cooperative 42 · <a href="https://cil.by">cil.by</a></div>
|
||||
<div>Simbiot UI · powered by claudecodeui</div>
|
||||
</div>
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -1,164 +0,0 @@
|
|||
# Simbiot UI — план продукта: продаваемый доступ к CloudCLI UI (claudecodeui)
|
||||
*t-261008114514 · v0.2 (решения Alex зафиксированы, m-516173892) · 2026-10-08*
|
||||
|
||||
> **v0.2 — что изменилось:** получены ответы на все 7 вопросов v0.1 §9. Продукт
|
||||
> зафиксирован: брендинг Simbiot UI / Simbiot Dashboard, одна цена **$99.9/мес,
|
||||
> claude seat included**, ключи выдаём свои, наш RAG клиентам не даём — но строим
|
||||
> **общий студийный RAG** (как строить стартапы + условия студии), подключение
|
||||
> клиентов на старте **только вручную через Alex**. Пресейл-страница и онбординг
|
||||
> обновлены под эти решения; юнит-экономика пересчитана под $99.9.
|
||||
|
||||
---
|
||||
|
||||
## 1. Суть продукта
|
||||
|
||||
Клиент покупает **доступ «из коробки» к развёрнутому claudecodeui** (web-UI для Claude Code):
|
||||
регистрируется → получает аккаунт агента с мэйлом на нашем домене → заходит в личный UI
|
||||
и работает с Claude-кодом через браузер, без установки чего-либо. Инфраструктура, старт,
|
||||
авторизация и контекст — на нашей стороне.
|
||||
|
||||
**Ценность (фильтр AUM):** recurring-выручка (SaaS-подписка) на уже существующем железе
|
||||
и моделях → прямой вклад в AUM на дистанции год+; каждая новая продажа не съедает базу.
|
||||
|
||||
## 2. Продуктовая рамка (v0.2 — по решениям Alex)
|
||||
|
||||
- **Нейминг (решено, A1):** **Simbiot UI** — сам продукт-интерфейс; **Simbiot Dashboard** —
|
||||
панель управления/мониторинга. Подпись «powered by claudecodeui» в футере — честно про
|
||||
опенсорс-основу (доверие, не слабость).
|
||||
- **Цена (решено, A2):** **$99.9/мес, claude seat included** — один тариф, без BYO-вариантов
|
||||
на старте. Claude-сид входит в цену.
|
||||
- **API-ключи (решено, A3):** **даём наши** — клиент не приносит свой ключ, работает наш
|
||||
шлюз (9router) с per-client spend-трекингом и лимитами. Клиент наших ключей не видит.
|
||||
- **Онбординг (решено, A7):** **только вручную через Alex** на всю первую партию — заявка →
|
||||
Alex подключает. Автоматический self-service провижининг — не раньше фазы 2 (см. §6).
|
||||
- **Формат продажи:** подписка месяц-к-месяцу, оплата — существующий стек (Stripe live
|
||||
обкатан на presale; p2p/счёт — для СНГ).
|
||||
- **Кто клиент (стартовая гипотеза):** команды 1–5 человек, которым нужен Claude Code,
|
||||
но лень/страшно разворачивать самим; последователи «перформанс-аудитории» студии.
|
||||
- **Регистрация:** лендинг на ui.cil.by → форма-заявка (без мгновенной оплаты до ручного
|
||||
подтверждения) → Alex подключает → оплата → доступы.
|
||||
|
||||
## 3. Архитектура и оркестратор
|
||||
|
||||
```
|
||||
ui.cil.by (Caddy, существующий reverse-proxy)
|
||||
├── веб-приложение Simbiot UI (заявки, биллинг, админка)
|
||||
├── claudecodeui (UI-движок) ←── контейнеры/юниты на инстансе
|
||||
├── Claude Code runtime (сервер) ←── claudecodeui дергает его
|
||||
└── MCP-студии: общий студийный RAG (см. §3a), файловая, 9router-шлюз
|
||||
```
|
||||
|
||||
- **Оркестратор** = сервис-провижинер (один Python-демон): по команде «новый клиент»
|
||||
поднимает связку `сервер + UI-движок`, создаёт агента, прописывает прокси, пишет CLAUDE.md.
|
||||
Стек: Docker (уже живёт на хосте) или systemd-юниты + Caddy-автосайты.
|
||||
- Изоляция клиентов: отдельный контейнер на клиента (vCPU/RAM-лимиты, отдельный том).
|
||||
- **9router — единственный LLM-шлюз** (A3: ключи наши): даёт маржу на моделях, per-client
|
||||
spend-трекинг и защиту «клиент не видит наши ключи».
|
||||
- **Свой RAG кооператива клиентам НЕ даём** (A6). Вместо него — общий студийный RAG (§3a).
|
||||
|
||||
### 3a. Общий студийный RAG (решение A6)
|
||||
|
||||
- **Что это:** отдельная база знаний общего назначения «как строить стартапы» + публичные
|
||||
условия студии (типовые договоры, процессы, FAQ) — то, что не жалко показать клиенту.
|
||||
- **Из чего:** частичный перегон **публичных** знаний кооператива (условия студии, методология
|
||||
запуска проектов — studio-conditions-2026-10, SALES-PLAN-*, playbook-материалы), плюс
|
||||
новые материалы, написанные специально для RAG.
|
||||
- **Как:** отдельный RAG-индекс (не /root/rag кооператива!), своё векторное пространство;
|
||||
подключение к клиентским инстансам через MCP-сервер read-only. Перегон — отдельной
|
||||
задачей с ревью Alex (что именно пускать наружу).
|
||||
- **Экономика:** входит в тариф как дифференциатор («агент знает методологию студии»),
|
||||
апселл-фаза после первых 5 клиентов.
|
||||
|
||||
## 4. Онбординг-флоу (v0.2 — ручной, admin-first)
|
||||
|
||||
> Решение A7: подключение клиентов — **только вручную через Alex**, пока не решим иначе.
|
||||
> Всё, что «авто» в v0.1, переезжает в фазу 2. Ручной флоу — тоже продукт, его надо
|
||||
> сделать гладким.
|
||||
|
||||
1. Клиент оставляет заявку на лендинге (имя, email, телеграм, 2–3 предложения о задачах).
|
||||
2. Заявка падает в очередь админки (и TG-уведомление Alex).
|
||||
3. **Alex вручную** подтверждает и подключает: поднимает инстанс, создаёт мэйл агента
|
||||
(`agent.<client>@cil.by`, Postfix+Dovecot уже подняты), авторизует claude seat.
|
||||
4. Оркестратор пишет креды в vault (реестр ключей docs/KEYS-REGISTRY.md расширяется), клиенту
|
||||
уходит письмо: логин, пароль, URL, что дальше (см. §5 CLAUDE.md).
|
||||
5. Статус-индикатор в админке: заявка → подтверждена → подключается → активна.
|
||||
|
||||
**Гейт:** целевая пропускная способность ручного онбординга — 3–5 клиентов/нед до упора;
|
||||
когда очередь копится > 1 недели — сигнал включать автопровижининг (фаза 2).
|
||||
|
||||
## 5. Claude-контекст (CLAUDE.md)
|
||||
|
||||
Оркестратор генерирует каждому клиенту `CLAUDE.md` при провижининге и обновляет при изменениях:
|
||||
|
||||
- где стартовано (инстанс, ОС, путь к дому проекта, лимиты);
|
||||
- как ходить в сеть (9router-эндпоинт, ключи из env, а не в файлах);
|
||||
- что за мэйл у агента и как им пользоваться (IMAP 993 / SMTP 587 cil.by);
|
||||
- **как пользоваться студийным RAG** (MCP, какие знания там есть — §3a);
|
||||
- правила безопасности (не трогать чужие тома, лимиты токенов, политика данных);
|
||||
- как запросить помощь (саппорт-канал).
|
||||
|
||||
Обновления — через хук оркестратора (деплой = regenerate CLAUDE.md), чтобы у Claude-кода
|
||||
всегда актуальная карта окружения.
|
||||
|
||||
## 6. План работ (фазы, v0.2)
|
||||
|
||||
| Фаза | Что делаем | Оценка |
|
||||
|---|---|---|
|
||||
| **0. Спайк (сейчас)** | Развернуть 1 инстанс claudecodeui на хосте за Caddy (ui.cil.by), проверить UX, OAuth Claude, MCP-хуки | 1–2 дня |
|
||||
| **0b. Пресейл-лендинг ui.cil.by** | Страница Simbiot UI $99.9/мес, форма-заявка → TG-очередь к Alex (без мгновенной оплаты) — artefact: out/simbiot_ui_presale.html | 1 день |
|
||||
| **1. Каркас продукта** | Админка заявок (очередь, статусы), Stripe-ссылка после ручного подтверждения | 2–3 дня |
|
||||
| **2. Оркестратор (ручной режим)** | Провижининг по кнопке Alex (не по оплате): контейнер, мэйл, CLAUDE.md, teardown | 3–4 дня |
|
||||
| **3. Общий студийный RAG** | Отдельный индекс; отбор публичных знаний (список на ревью Alex), MCP read-only | 3–4 дня |
|
||||
| **4. Пилот** | 3–5 платящих клиентов через ручной онбординг Alex, фидбек, юнит-экономика на реальных числах | 1–2 недели |
|
||||
| **5. Автопровижининг (отложено)** | Self-service оплата→доступ; включаем при копящейся очереди (гейт §4) | отложено |
|
||||
|
||||
**MVP-гейт:** ручной онбординг — не обходной путь, а план-А на пилот. Автоматизация —
|
||||
только после подтверждённого спроса (фаза 5).
|
||||
|
||||
## 7. Юнит-экономика (v0.2, под $99.9/мес)
|
||||
|
||||
- **Цена:** $99.9/мес, claude seat included. Один тариф.
|
||||
- Себестоимость: контейнер ~0 vCPU в простое; токены — через 9router (per-client трекинг
|
||||
уже есть); домен/железо оплачены.
|
||||
- **Кейс-1 (умеренное использование, ~$30/мес токенов на клиента):** маржа ≈ $69.9 (70%).
|
||||
- **Кейс-2 (тяжёлый клиент, ~$80/мес токенов):** маржа ≈ $19.9 (20%) → нужны fair-use
|
||||
лимиты в оффере.
|
||||
- 20 клиентов × $99.9 = **$2.0к/мес recurring** при почти нулевой marginal cost.
|
||||
- Fair-use: зафиксировать лимит токенов/мес в оффере (soft-limit с заголовком «overage
|
||||
по договорённости»), чтобы кейс-2 не съедал маржу.
|
||||
- По фильтру AUM: 20 клиентов = +$24к ARR recurring — долгосрочная база, не разовый доход.
|
||||
|
||||
## 8. Риски и как закрываем
|
||||
|
||||
- **ToS Claude** (перепродажа доступа, A2+A3 усугубляют — сид и ключи наши) → формат
|
||||
«инфраструктура + сопровождение», лимиты, юр-модель уточнить с Alex до масштабирования
|
||||
за пределы пилота.
|
||||
- Нагрузка/лимиты хоста → квоты на клиента, мониторинг (spend-трекинг 9router уже есть).
|
||||
- Support-нагрузка → CLAUDE.md-контекст + FAQ + агента-саппортёра в чат.
|
||||
- **Бутылочное горлышко Alex (A7):** ручной онбординг ограничивает рост → гейт §4
|
||||
(очередь > 1 нед → фаза 5).
|
||||
- **Утечка знаний через студийный RAG (A6):** перегон только публичных материалов,
|
||||
список на ревью Alex, read-only MCP.
|
||||
|
||||
## 9. Решения Alex (m-516173892, зафиксировано 2026-10-08)
|
||||
|
||||
| # | Вопрос v0.1 §9 | Решение Alex | Влияние на план |
|
||||
|---|---|---|---|
|
||||
| 1 | Нейминг | **Simbiot UI ok, Simbiot Dashboard ok** | Брендинг зафиксирован (§2) |
|
||||
| 2 | Цена и тарифы | **$99.9/мес, claude seat included** | Один тариф (§2, §7) |
|
||||
| 3 | Claude-аккаунты | **наши даём** | 9router — единственный шлюз, ToS-риск выше (§8) |
|
||||
| 4 | Лимит первых клиентов | **да ок** | 10 ручных → фаза 5 авто |
|
||||
| 5 | Мэйл-домен | **ок** | ящики на cil.by (§4) |
|
||||
| 6 | MCP / RAG студии | **наш RAG — нет; общий студийный RAG — да** (как строить стартапы, условия студии, частичный перегон) | §3a, фаза 3 |
|
||||
| 7 | Кто админ Claude-аккаунтов | **пока только я (Alex)** | Ручной онбординг (§4), гейт фазы 5 |
|
||||
|
||||
## 10. Что уже есть и переиспользуем
|
||||
|
||||
- Caddy reverse-proxy, Docker, домены cil.by — готовы.
|
||||
- Postfix+Dovecot (SMTP/IMAP) — мэйл-агенты без нового софта.
|
||||
- 9router — LLM-шлюз с spend-трекингом.
|
||||
- Stripe live (обкатан на presale) — биллинг.
|
||||
- Практики провижининга агентов: mx_provision_agent.py (Matrix), wizard-установщик харнеса.
|
||||
- Портал кооператива — как референс регистрации/админки (форк или отдельное приложение).
|
||||
- **out/simbiot_ui_presale.html** — пресейл-страница Simbiot UI $99.9/мес (создана в v0.2).
|
||||
- **docs/ROADMAP-SIMBIOT-UI.md** — roadmap с ручным онбордингом как ограничением (v0.2).
|
||||
|
|
@ -1,80 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="ru">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Simbiot — индекс статических файлов</title>
|
||||
<style>
|
||||
body{font-family:system-ui,-apple-system,sans-serif;background:#0e1116;color:#e6e9ef;margin:0;padding:40px 20px}
|
||||
h1{font-size:26px} h2{margin-top:36px;font-size:19px;color:#8ab4ff;border-bottom:1px solid #2a3140;padding-bottom:6px}
|
||||
table{width:100%;max-width:1000px;border-collapse:collapse;font-size:14px}
|
||||
td{padding:8px 10px;border-bottom:1px solid #1e2430;vertical-align:top}
|
||||
a{color:#7ec9ff;text-decoration:none} a:hover{text-decoration:underline}
|
||||
.size{color:#8a93a5;white-space:nowrap}
|
||||
.note{color:#aab4c5}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>📁 Индекс статических файлов кооператива 42 / Simbiot</h1>
|
||||
<p class="note">Все HTML/PNG/PDF-файлы репозитория, сгруппированные по категориям. 31 файл.</p>
|
||||
|
||||
<h2>🌍 Лендинги и презентации</h2>
|
||||
<table>
|
||||
<tr><td><a href="../docs/simbiot-landing-v2.html">docs/simbiot-landing-v2.html</a></td><td class="note">Лендинг Simbiot v2</td><td class="size">33 КБ</td></tr>
|
||||
<tr><td><a href="../docs/simbiot-landing-v2-live.html">docs/simbiot-landing-v2-live.html</a></td><td class="note">Лендинг v2 (live-версия)</td><td class="size">30 КБ</td></tr>
|
||||
<tr><td><a href="../docs/simbiot-landing-dashboard.html">docs/simbiot-landing-dashboard.html</a></td><td class="note">Лендинг + дашборд</td><td class="size">20 КБ</td></tr>
|
||||
<tr><td><a href="../docs/simbiot_presale_landing.html">docs/simbiot_presale_landing.html</a></td><td class="note">Пресейл-лендинг</td><td class="size">17 КБ</td></tr>
|
||||
<tr><td><a href="../artifacts/landing-simbiot.html">artifacts/landing-simbiot.html</a></td><td class="note">Лендинг (артefакт)</td><td class="size">11 КБ</td></tr>
|
||||
<tr><td><a href="../artifacts/landing-simbiot-opencode.html">artifacts/landing-simbiot-opencode.html</a></td><td class="note">Лендинг (OpenCode)</td><td class="size">20 КБ</td></tr>
|
||||
<tr><td><a href="../simbiot-presentation.html">simbiot-presentation.html</a></td><td class="note">Презентация Simbiot</td><td class="size">22 КБ</td></tr>
|
||||
<tr><td><a href="../architecture.html">architecture.html</a></td><td class="note">Архитектура системы</td><td class="size">40 КБ</td></tr>
|
||||
</table>
|
||||
|
||||
<h2>👤 Порталы и UI-клиенты</h2>
|
||||
<table>
|
||||
<tr><td><a href="../out/cil-by-compact-landing.html">out/cil-by-compact-landing.html</a></td><td class="note">Портал CIL by Compact</td><td class="size">23 КБ</td></tr>
|
||||
<tr><td><a href="../out/ui-cil-by-compact-landing.html">out/ui-cil-by-compact-landing.html</a></td><td class="note">UI-версия портала CIL</td><td class="size">22 КБ</td></tr>
|
||||
<tr><td><a href="../out/simbiot_ui_presale.html">out/simbiot_ui_presale.html</a></td><td class="note">Пресейл UI</td><td class="size">12 КБ</td></tr>
|
||||
<tr><td><a href="../harness/roy.html">harness/roy.html</a></td><td class="note">Портал Roy</td><td class="size">3 КБ</td></tr>
|
||||
<tr><td><a href="../harness/dashboard.html">harness/dashboard.html</a></td><td class="note">Дашборд харнеса</td><td class="size">40 КБ</td></tr>
|
||||
</table>
|
||||
|
||||
<h2>🗓 Встречи и материалы к переговорам</h2>
|
||||
<table>
|
||||
<tr><td><a href="../out/meeting143_leonid_presentation.html">out/meeting143_leonid_presentation.html</a></td><td class="note">Встреча 143 — презентация для Леонида</td><td class="size">13 КБ</td></tr>
|
||||
<tr><td><a href="../out/meeting143_leonid_slides.html">out/meeting143_leonid_slides.html</a></td><td class="note">Встреча 143 — слайды</td><td class="size">25 КБ</td></tr>
|
||||
<tr><td><a href="../docs/yulia-call-136-proposal.html">docs/yulia-call-136-proposal.html</a></td><td class="note">Звонок 136 — предложение Юлии</td><td class="size">6 КБ</td></tr>
|
||||
<tr><td><a href="../docs/yulia-call-136-recap.html">docs/yulia-call-136-recap.html</a></td><td class="note">Звонок 136 — рекап</td><td class="size">6 КБ</td></tr>
|
||||
<tr><td><a href="../docs/nadezhda-call-recap.html">docs/nadezhda-call-recap.html</a></td><td class="note">Звонок с Надеждой — рекап</td><td class="size">10 КБ</td></tr>
|
||||
</table>
|
||||
|
||||
<h2>📄 Документы и договоры</h2>
|
||||
<table>
|
||||
<tr><td><a href="../Studio_Participation_Agreement_2026-09.pdf">Studio_Participation_Agreement_2026-09.pdf</a></td><td class="note">Соглашение об участии в студии (PDF)</td><td class="size">17 КБ</td></tr>
|
||||
<tr><td><a href="../lab42-conditions.html">lab42-conditions.html</a></td><td class="note">Условия Lab42</td><td class="size">15 КБ</td></tr>
|
||||
<tr><td><a href="../artifacts/studio-conditions-2026-10.html">artifacts/studio-conditions-2026-10.html</a></td><td class="note">Условия студии 2026-10</td><td class="size">6 КБ</td></tr>
|
||||
<tr><td><a href="../artifacts/rental-x-simbiot-agreement.html">artifacts/rental-x-simbiot-agreement.html</a></td><td class="note">Соглашение Rental×Simbiot</td><td class="size">16 КБ</td></tr>
|
||||
<tr><td><a href="../artifacts/rental-x-simbiot-side-letter-15k.html">artifacts/rental-x-simbiot-side-letter-15k.html</a></td><td class="note">Сайд-леттер Rental×Simbiot 15k</td><td class="size">10 КБ</td></tr>
|
||||
</table>
|
||||
|
||||
<h2>🛠 Продуктовые планы и гайды</h2>
|
||||
<table>
|
||||
<tr><td><a href="../docs/ai-mascots-product-plan.html">docs/ai-mascots-product-plan.html</a></td><td class="note">Продуктовый план AI-маскотов</td><td class="size">16 КБ</td></tr>
|
||||
<tr><td><a href="../docs/fpga-neuralnet-guide.html">docs/fpga-neuralnet-guide.html</a></td><td class="note">Гайд: нейросети на FPGA</td><td class="size">9 КБ</td></tr>
|
||||
<tr><td><a href="../docs/self-diagnostics.html">docs/self-diagnostics.html</a></td><td class="note">Самодиагностика</td><td class="size">8 КБ</td></tr>
|
||||
</table>
|
||||
|
||||
<h2>🎨 Прототипы</h2>
|
||||
<table>
|
||||
<tr><td><a href="../artifacts/shkor-beauty-prototype.html">artifacts/shkor-beauty-prototype.html</a></td><td class="note">Прототип Shkor Beauty</td><td class="size">25 КБ</td></tr>
|
||||
</table>
|
||||
|
||||
<h2>🖼 Изображения</h2>
|
||||
<table>
|
||||
<tr><td><a href="../artifacts/imagegen-e2e-20261003.png">artifacts/imagegen-e2e-20261003.png</a></td><td class="note">E2E-тест генерации изображений</td><td class="size">977 КБ</td></tr>
|
||||
<tr><td><a href="../artifacts/dashboard-channels-t-d8112b.png">artifacts/dashboard-channels-t-d8112b.png</a></td><td class="note">Скриншот дашборда (каналы)</td><td class="size">138 КБ</td></tr>
|
||||
<tr><td><a href="../artifacts/dashboard-channels-t-d8112b-full.png">artifacts/dashboard-channels-t-d8112b-full.png</a></td><td class="note">Скриншот дашборда (полный)</td><td class="size">145 КБ</td></tr>
|
||||
<tr><td><a href="../out/9router-gemini-test.png">out/9router-gemini-test.png</a></td><td class="note">Тест 9router/Gemini</td><td class="size">1 КБ</td></tr>
|
||||
</table>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -1,356 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
<title>Simbiot UI — Claude Code in your browser</title>
|
||||
<meta name="description" content="A deployed web UI for Claude Code with agent mailbox, studio RAG and signed audit. $99.9/mo, claude seat included. No installs, no API keys.">
|
||||
<meta property="og:title" content="Simbiot UI — Claude Code in your browser">
|
||||
<meta property="og:description" content="Deployed Claude Code web workspace: mailbox, studio knowledge, signed audit. $99.9/mo, seat included.">
|
||||
<meta property="og:url" content="https://ui.cil.by/">
|
||||
<meta property="og:type" content="website">
|
||||
<style>
|
||||
/* ── fingerprint (same DNA as cil.by compact): offset rail · ~680px measure · charcoal+lime · serif+mono · machined 4/10 · tinted panels · index motif · one entrance + state wash ── */
|
||||
:root{
|
||||
--bg:#0E100C; --panel:#151812; --panel2:#1A1E17;
|
||||
--line:#272C22; --line2:#333A2C;
|
||||
--ink:#E9ECE1; --dim:#ADB6A2; --faint:#7E8873;
|
||||
--acc:#C9F24D; --acc-ink:#111408;
|
||||
--ok:#7BD88F; --run:#E8C44A; --wait:#8A9382;
|
||||
--serif:Georgia,'Iowan Old Style','Times New Roman',serif;
|
||||
--mono:ui-monospace,'SF Mono','Cascadia Mono',Menlo,Consolas,'Liberation Mono',monospace;
|
||||
--sans:-apple-system,system-ui,'Segoe UI',Roboto,sans-serif;
|
||||
}
|
||||
*{margin:0;padding:0;box-sizing:border-box}
|
||||
html{scroll-behavior:smooth}
|
||||
body{background:var(--bg);color:var(--ink);font-family:var(--sans);font-size:15px;line-height:1.5;-webkit-font-smoothing:antialiased}
|
||||
a{color:inherit}
|
||||
:focus-visible{outline:2px solid var(--acc);outline-offset:2px;border-radius:2px}
|
||||
.wrap{max-width:1060px;margin:0 auto;padding:0 22px}
|
||||
.mono{font-family:var(--mono);font-variant-numeric:tabular-nums}
|
||||
|
||||
/* ── left rail (desktop) ── */
|
||||
.layout{display:grid;grid-template-columns:168px minmax(0,1fr);gap:44px;max-width:1060px;margin:0 auto;padding:26px 22px 0}
|
||||
.rail{position:sticky;top:26px;align-self:start;display:flex;flex-direction:column;gap:22px;font-family:var(--mono);font-size:11px;letter-spacing:.08em;text-transform:uppercase}
|
||||
.rail .brand{display:flex;align-items:center;gap:9px;color:var(--ink);text-decoration:none;font-weight:600}
|
||||
.rail .brand .dot{width:9px;height:9px;border-radius:50%;background:var(--acc);flex:none}
|
||||
.rail nav{display:flex;flex-direction:column;gap:2px}
|
||||
.rail nav a{color:var(--faint);text-decoration:none;padding:9px 10px 9px 0;min-height:36px;display:flex;align-items:center;border-bottom:1px solid var(--line)}
|
||||
.rail nav a:hover{color:var(--ink)}
|
||||
.rail .railnote{color:var(--faint);line-height:1.9;border-top:1px solid var(--line);padding-top:14px}
|
||||
.rail .railnote b{color:var(--dim);font-weight:500}
|
||||
.topbar{display:none}
|
||||
|
||||
/* ── buttons: compact scale ── */
|
||||
.btn{display:inline-flex;align-items:center;justify-content:center;gap:8px;height:28px;padding:0 14px;border-radius:4px;border:1px solid var(--line2);background:var(--panel2);color:var(--ink);font-family:var(--mono);font-size:11.5px;letter-spacing:.08em;text-transform:uppercase;text-decoration:none;cursor:pointer;min-height:40px}
|
||||
.btn.primary{background:var(--acc);border-color:var(--acc);color:var(--acc-ink);font-weight:600}
|
||||
.btn:focus-visible{outline-offset:1px}
|
||||
@media(hover:hover) and (pointer:fine){.btn:hover{border-color:var(--acc)} .btn.primary:hover{filter:brightness(1.06)}}
|
||||
.btn:active{transform:scale(.96)}
|
||||
|
||||
/* ── hero ── */
|
||||
.hero{padding:30px 0 26px;border-bottom:1px solid var(--line)}
|
||||
.kicker{font-family:var(--mono);font-size:11px;letter-spacing:.14em;text-transform:uppercase;color:var(--faint);margin-bottom:18px}
|
||||
.kicker .dot{display:inline-block;width:7px;height:7px;border-radius:50%;background:var(--ok);margin-right:8px}
|
||||
h1{font-family:var(--serif);font-weight:400;font-size:clamp(36px,5.6vw,58px);line-height:1.05;letter-spacing:-.015em;max-width:16ch}
|
||||
h1 em{font-style:italic;color:var(--acc)}
|
||||
.lede{color:var(--dim);max-width:52ch;margin:18px 0 22px;font-size:15.5px}
|
||||
.cta-row{display:flex;gap:10px;flex-wrap:wrap;align-items:center}
|
||||
.cta-note{font-family:var(--mono);font-size:10.5px;letter-spacing:.06em;color:var(--faint);text-transform:uppercase}
|
||||
|
||||
/* ── signature artifact: live workspace console ── */
|
||||
.console{margin-top:30px;background:var(--panel);border:1px solid var(--line2);border-radius:10px;overflow:hidden}
|
||||
.console .bar{display:flex;align-items:center;gap:10px;padding:10px 14px;border-bottom:1px solid var(--line);font-family:var(--mono);font-size:10.5px;letter-spacing:.1em;text-transform:uppercase;color:var(--faint)}
|
||||
.console .bar .ticks{display:flex;gap:5px}
|
||||
.console .bar .ticks i{width:8px;height:8px;border-radius:2px;background:var(--line2)}
|
||||
.console .bar .live{margin-left:auto;display:flex;align-items:center;gap:7px;color:var(--dim)}
|
||||
.console .bar .live i{width:7px;height:7px;border-radius:50%;background:var(--ok);animation:pulse 2.4s infinite}
|
||||
@keyframes pulse{0%,100%{opacity:1}50%{opacity:.35}}
|
||||
.metrics{display:grid;grid-template-columns:repeat(3,1fr);border-bottom:1px solid var(--line)}
|
||||
.metric{padding:12px 14px;border-right:1px solid var(--line)}
|
||||
.metric:last-child{border-right:none}
|
||||
.metric .v{font-family:var(--mono);font-variant-numeric:tabular-nums;font-size:19px;color:var(--ink)}
|
||||
.metric .v .u{color:var(--faint);font-size:12px}
|
||||
.metric .k{font-family:var(--mono);font-size:9.5px;letter-spacing:.12em;text-transform:uppercase;color:var(--faint);margin-top:3px}
|
||||
.ladder{display:flex;flex-direction:column}
|
||||
.trow{display:grid;grid-template-columns:44px minmax(0,1fr) 96px 96px;gap:12px;align-items:center;min-height:44px;padding:8px 14px;border-bottom:1px solid var(--line);font-size:13.5px;color:var(--dim);cursor:pointer;text-align:left;background:transparent;border-left:none;border-right:none;border-top:none;width:100%;font-family:var(--sans);transition:background-color .25s,color .25s}
|
||||
.trow:last-child{border-bottom:none}
|
||||
.trow:hover{background:var(--panel2);color:var(--ink)}
|
||||
.trow .sz{font-family:var(--mono);font-size:11px;color:var(--faint)}
|
||||
.trow .name{white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
||||
.trow .name b{color:var(--ink);font-weight:600}
|
||||
.trow .st{font-family:var(--mono);font-size:10.5px;letter-spacing:.08em;text-transform:uppercase;color:var(--wait);display:flex;align-items:center;gap:7px}
|
||||
.trow .st i{width:7px;height:7px;border-radius:50%;background:var(--wait);flex:none}
|
||||
.trow[data-st="running"] .st{color:var(--run)} .trow[data-st="running"] .st i{background:var(--run)}
|
||||
.trow[data-st="done"] .st{color:var(--ok)} .trow[data-st="done"] .st i{background:var(--ok)}
|
||||
.trow .who{font-family:var(--mono);font-size:11px;color:var(--faint);text-align:right}
|
||||
.console .foot{padding:9px 14px;font-family:var(--mono);font-size:10px;letter-spacing:.1em;text-transform:uppercase;color:var(--faint);border-top:1px solid var(--line)}
|
||||
|
||||
/* ── sections ── */
|
||||
section{padding:38px 0 6px}
|
||||
.shead{display:flex;align-items:baseline;gap:14px;margin-bottom:20px}
|
||||
.sidx{font-family:var(--mono);font-size:11px;color:var(--acc);letter-spacing:.1em}
|
||||
.shead h2{font-family:var(--serif);font-weight:400;font-size:clamp(22px,3vw,30px);letter-spacing:-.01em}
|
||||
.snote{margin-left:auto;font-family:var(--mono);font-size:10.5px;letter-spacing:.1em;text-transform:uppercase;color:var(--faint)}
|
||||
.lead{color:var(--dim);max-width:56ch;margin-bottom:22px}
|
||||
.ledger{border:1px solid var(--line);border-radius:10px;background:var(--panel);overflow:hidden}
|
||||
.lrow{display:grid;grid-template-columns:52px 160px minmax(0,1fr) 170px;gap:16px;align-items:center;padding:15px 18px;border-bottom:1px solid var(--line);font-size:13.5px;color:var(--dim)}
|
||||
.lrow:last-child{border-bottom:none}
|
||||
.lrow .idx{font-family:var(--mono);font-size:11px;color:var(--faint)}
|
||||
.lrow .nm{font-size:14.5px;color:var(--ink);font-weight:600}
|
||||
.lrow .nm span{display:block;font-family:var(--mono);font-size:10px;letter-spacing:.1em;text-transform:uppercase;color:var(--faint);font-weight:400;margin-top:3px}
|
||||
.lrow .tags{font-family:var(--mono);font-size:10.5px;letter-spacing:.06em;color:var(--faint);text-align:right}
|
||||
|
||||
/* ── steps ── */
|
||||
.steps{display:grid;grid-template-columns:repeat(4,1fr);gap:14px}
|
||||
.step{background:var(--panel);border:1px solid var(--line);border-radius:10px;padding:16px}
|
||||
.step .n{font-family:var(--mono);font-size:11px;color:var(--acc);margin-bottom:9px;display:block}
|
||||
.step b{font-size:14.5px;display:block;margin-bottom:6px}
|
||||
.step p{font-size:13px;color:var(--dim);line-height:1.55}
|
||||
|
||||
/* ── pricing ── */
|
||||
.plan{border:1px solid var(--line2);border-radius:12px;background:var(--panel);padding:26px;max-width:560px}
|
||||
.plan .p{font-family:var(--serif);font-size:38px;letter-spacing:-.02em}
|
||||
.plan .p span{font-family:var(--mono);font-size:12px;color:var(--faint);letter-spacing:.06em}
|
||||
.plan ul{list-style:none;margin:18px 0 22px;display:grid;gap:9px}
|
||||
.plan li{font-size:13.5px;color:var(--dim);padding-left:22px;position:relative}
|
||||
.plan li::before{content:"✓";position:absolute;left:0;color:var(--acc);font-family:var(--mono);font-size:12px}
|
||||
|
||||
/* ── faq ── */
|
||||
details{border-bottom:1px solid var(--line);padding:14px 0}
|
||||
details summary{cursor:pointer;font-size:15px;font-weight:600;list-style:none;display:flex;justify-content:space-between;gap:14px}
|
||||
details summary::after{content:"+";font-family:var(--mono);color:var(--faint)}
|
||||
details[open] summary::after{content:"–";color:var(--acc)}
|
||||
details p{margin-top:10px;color:var(--dim);font-size:13.5px;max-width:64ch}
|
||||
|
||||
/* ── form ── */
|
||||
.form{border:1px solid var(--line2);border-radius:12px;background:var(--panel);padding:26px;max-width:560px}
|
||||
.form label{display:block;font-family:var(--mono);font-size:10px;letter-spacing:.12em;text-transform:uppercase;color:var(--faint);margin:14px 0 6px}
|
||||
.form input,.form textarea{width:100%;background:var(--panel2);border:1px solid var(--line2);border-radius:6px;color:var(--ink);font-family:var(--sans);font-size:14px;padding:11px 12px;min-height:44px}
|
||||
.form textarea{min-height:88px;resize:vertical}
|
||||
.form input:focus,.form textarea:focus{outline:none;border-color:var(--acc)}
|
||||
.form .btn{margin-top:20px;width:100%}
|
||||
.form .fine{margin-top:12px;font-family:var(--mono);font-size:10px;letter-spacing:.08em;text-transform:uppercase;color:var(--faint);text-align:center}
|
||||
|
||||
footer{margin-top:44px;border-top:1px solid var(--line);padding:22px 0 34px;font-family:var(--mono);font-size:10.5px;letter-spacing:.08em;text-transform:uppercase;color:var(--faint);display:flex;justify-content:space-between;gap:14px;flex-wrap:wrap}
|
||||
|
||||
/* ── entrance ── */
|
||||
@media(prefers-reduced-motion:no-preference){
|
||||
.in{opacity:0;transform:translateY(10px);animation:rise .7s cubic-bezier(.2,.7,.2,1) forwards}
|
||||
.d1{animation-delay:.08s}.d2{animation-delay:.16s}.d3{animation-delay:.24s}
|
||||
@keyframes rise{to{opacity:1;transform:none}}
|
||||
}
|
||||
@media(prefers-reduced-motion:reduce){*{animation:none!important;transition:none!important}}
|
||||
|
||||
/* ── mobile ── */
|
||||
@media(max-width:820px){
|
||||
.layout{grid-template-columns:1fr;gap:0;padding-top:0}
|
||||
.rail{display:none}
|
||||
.topbar{display:flex;align-items:center;gap:10px;position:sticky;top:0;z-index:50;background:rgba(14,16,12,.94);backdrop-filter:blur(8px);border-bottom:1px solid var(--line);padding:12px 22px;font-family:var(--mono);font-size:11px;letter-spacing:.08em;text-transform:uppercase}
|
||||
.topbar .dot{width:9px;height:9px;border-radius:50%;background:var(--acc)}
|
||||
.topbar a{margin-left:auto}
|
||||
.lrow{grid-template-columns:40px minmax(0,1fr);grid-template-rows:auto auto}
|
||||
.lrow .ds{grid-column:2}
|
||||
.lrow .tags{grid-column:2;text-align:left}
|
||||
.steps{grid-template-columns:1fr}
|
||||
.metrics{grid-template-columns:1fr}
|
||||
.metric{border-right:none;border-bottom:1px solid var(--line)}
|
||||
.trow{grid-template-columns:minmax(0,1fr) 90px}
|
||||
.trow .sz,.trow .who{display:none}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div class="topbar"><span class="dot"></span>Simbiot UI <a class="btn primary" href="#apply" style="margin-left:auto">Apply</a></div>
|
||||
|
||||
<div class="layout">
|
||||
<aside class="rail">
|
||||
<a class="brand" href="https://ui.cil.by/"><span class="dot"></span>Simbiot UI</a>
|
||||
<nav>
|
||||
<a href="#included">included</a>
|
||||
<a href="#how">how it works</a>
|
||||
<a href="#inside">what's inside</a>
|
||||
<a href="#pricing">pricing</a>
|
||||
<a href="#faq">faq</a>
|
||||
<a href="#apply">apply</a>
|
||||
</nav>
|
||||
<div class="railnote">
|
||||
<b>Coop 42</b> product<br>
|
||||
claudecodeui<br>
|
||||
studio RAG · audit<br><br>
|
||||
<a class="btn" style="margin-top:10px" href="https://ui.cil.by/">Sign in</a>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<main>
|
||||
<!-- 00 · hero + live workspace console -->
|
||||
<div class="hero">
|
||||
<p class="kicker in"><span class="dot"></span>Simbiot UI · first cohort open</p>
|
||||
<h1 class="in d1">Claude Code in your browser.<br><em>Deployed for you.</em></h1>
|
||||
<p class="lede in d2">A web workspace for Claude Code: agent mailbox, studio knowledge base, signed audit. No installs, no API keys — open a tab and build.</p>
|
||||
<div class="cta-row in d3">
|
||||
<a class="btn primary" href="#apply">Request access — $99.9/mo</a>
|
||||
<a class="btn" href="#how">How it works</a>
|
||||
<span class="cta-note">claude seat included · cancel anytime</span>
|
||||
</div>
|
||||
|
||||
<div class="console in d3" aria-label="Live workspace demo">
|
||||
<div class="bar">
|
||||
<span class="ticks" aria-hidden="true"><i></i><i></i><i></i></span>
|
||||
<span>your workspace · agent feed</span>
|
||||
<span class="live"><i></i>workspace live</span>
|
||||
</div>
|
||||
<div class="metrics">
|
||||
<div class="metric"><div class="v" id="mDone">4<span class="u"> done</span></div><div class="k">actions this hour</div></div>
|
||||
<div class="metric"><div class="v">1<span class="u">-day</span></div><div class="k">setup to first login</div></div>
|
||||
<div class="metric"><div class="v">100<span class="u">%</span></div><div class="k">actions signed</div></div>
|
||||
</div>
|
||||
<div class="ladder" id="ladder"></div>
|
||||
<div class="foot">mailbox inbox 14:02 · <b>per-client gateway limits</b> · click a row to re-run</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 01 · included -->
|
||||
<section id="included">
|
||||
<div class="shead"><span class="sidx">01</span><h2>Works out of the box.</h2><span class="snote">zero setup</span></div>
|
||||
<div class="ledger">
|
||||
<div class="lrow">
|
||||
<span class="idx">/01</span>
|
||||
<div class="nm">Browser only<span>deployed web UI</span></div>
|
||||
<div class="ds">A deployed web interface for Claude Code, plus Simbiot Dashboard for monitoring. Nothing to install or configure.</div>
|
||||
<div class="tags">web · dashboard</div>
|
||||
</div>
|
||||
<div class="lrow">
|
||||
<span class="idx">/02</span>
|
||||
<div class="nm">Seat included<span>model access</span></div>
|
||||
<div class="ds">The Claude seat is part of the plan — you don't bring keys, you don't see ours. Fair-use limits keep it fast for everyone.</div>
|
||||
<div class="tags">no keys · fair-use</div>
|
||||
</div>
|
||||
<div class="lrow">
|
||||
<span class="idx">/03</span>
|
||||
<div class="nm">Studio RAG<span>knowledge built in</span></div>
|
||||
<div class="ds">Your workspace knows the cooperative's playbook: how to build startups, studio terms and processes — searchable by your agent.</div>
|
||||
<div class="tags">rag · read-only</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 02 · how -->
|
||||
<section id="how">
|
||||
<div class="shead"><span class="sidx">02</span><h2>Onboarded by hand.</h2><span class="snote">first cohort</span></div>
|
||||
<p class="lead">We onboard every workspace manually for the first cohort — that keeps quality high while we scale.</p>
|
||||
<div class="steps">
|
||||
<div class="step"><span class="n">01</span><b>Apply</b><p>Tell us about your team and what you're building. Takes a minute.</p></div>
|
||||
<div class="step"><span class="n">02</span><b>We provision</b><p>Workspace, agent mailbox and Claude seat are set up by us — usually within a day.</p></div>
|
||||
<div class="step"><span class="n">03</span><b>Credentials arrive</b><p>Login, password and URL by email, with a starter guide for your agent.</p></div>
|
||||
<div class="step"><span class="n">04</span><b>Build</b><p>Open Simbiot UI and work with Claude Code in the browser. Support channel included.</p></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 03 · inside -->
|
||||
<section id="inside">
|
||||
<div class="shead"><span class="sidx">03</span><h2>What's inside.</h2><span class="snote">per workspace</span></div>
|
||||
<div class="ledger">
|
||||
<div class="lrow">
|
||||
<span class="idx">/01</span>
|
||||
<div class="nm">Personal workspace<span>isolated container</span></div>
|
||||
<div class="ds">One isolated container per client: your files, your agent, your limits. Others can't see your data, you can't see theirs.</div>
|
||||
<div class="tags">isolation · volumes</div>
|
||||
</div>
|
||||
<div class="lrow">
|
||||
<span class="idx">/02</span>
|
||||
<div class="nm">Agent mailbox<span>email on our domain</span></div>
|
||||
<div class="ds">Every workspace ships with an agent email address — your Claude can send and receive mail as part of the workflow.</div>
|
||||
<div class="tags">smtp · workflows</div>
|
||||
</div>
|
||||
<div class="lrow">
|
||||
<span class="idx">/03</span>
|
||||
<div class="nm">Model gateway<span>tracked & limited</span></div>
|
||||
<div class="ds">All model traffic goes through our gateway with per-client tracking and fair-use limits. Predictable, no surprise bills.</div>
|
||||
<div class="tags">gateway · limits</div>
|
||||
</div>
|
||||
<div class="lrow">
|
||||
<span class="idx">/04</span>
|
||||
<div class="nm">Signed & auditable<span>audit spine</span></div>
|
||||
<div class="ds">Every agent action is signed and auditable — the same audit spine the cooperative runs on every day.</div>
|
||||
<div class="tags">signed · audit</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 04 · pricing -->
|
||||
<section id="pricing">
|
||||
<div class="shead"><span class="sidx">04</span><h2>One plan. Everything included.</h2><span class="snote">month-to-month</span></div>
|
||||
<div class="plan">
|
||||
<div class="p">$99.9 <span>/ month · flat</span></div>
|
||||
<ul>
|
||||
<li>Deployed Claude Code web workspace</li>
|
||||
<li>Claude seat & model access included</li>
|
||||
<li>Agent mailbox on our domain</li>
|
||||
<li>Studio knowledge base (RAG)</li>
|
||||
<li>Isolated container per workspace</li>
|
||||
<li>Signed, auditable agent actions</li>
|
||||
<li>Support channel · cancel anytime</li>
|
||||
</ul>
|
||||
<a class="btn primary" href="#apply">Apply for access</a>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 05 · faq -->
|
||||
<section id="faq">
|
||||
<div class="shead"><span class="sidx">05</span><h2>FAQ.</h2></div>
|
||||
<details open><summary>Do I need my own API keys or Claude account?</summary><p>No. The Claude seat is included in the $99.9/mo price — model access is provisioned and managed by us.</p></details>
|
||||
<details><summary>How fast can I start?</summary><p>We onboard the first cohort manually: apply, we confirm, you get credentials — typically within one business day.</p></details>
|
||||
<details><summary>What's the studio knowledge base?</summary><p>A curated, read-only RAG with the studio's startup-building methodology and terms — available to your agent inside the workspace. Private cooperative data is not included.</p></details>
|
||||
<details><summary>Is my data isolated?</summary><p>Yes — one isolated container per client with its own volume and resource limits.</p></details>
|
||||
<details><summary>Can I cancel?</summary><p>Month-to-month subscription, cancel anytime. We'll help you export your files.</p></details>
|
||||
</section>
|
||||
|
||||
<!-- 06 · apply -->
|
||||
<section id="apply">
|
||||
<div class="shead"><span class="sidx">06</span><h2>Request access.</h2><span class="snote">personal reply</span></div>
|
||||
<p class="lead">First cohort is onboarded personally — leave your details and we'll get back to you.</p>
|
||||
<form class="form" action="mailto:hello@cil.by" method="post" enctype="text/plain">
|
||||
<label for="f-name">Name</label><input id="f-name" name="name" required autocomplete="name">
|
||||
<label for="f-email">Email</label><input id="f-email" name="email" type="email" required autocomplete="email">
|
||||
<label for="f-tg">Telegram (optional)</label><input id="f-tg" name="telegram">
|
||||
<label for="f-what">What will you build?</label><textarea id="f-what" name="building"></textarea>
|
||||
<button class="btn primary" type="submit">Apply — $99.9/mo after confirmation</button>
|
||||
<p class="fine">we reply personally · no auto-billing · cancel anytime</p>
|
||||
</form>
|
||||
</section>
|
||||
|
||||
<footer>
|
||||
<span>© 2026 Cooperative 42 · cil.by</span>
|
||||
<span>Simbiot UI · powered by claudecodeui</span>
|
||||
</footer>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
/* live agent feed — signature artifact */
|
||||
const tasks=[
|
||||
{sz:"S",name:"<b>fix</b> header padding on landing",st:"done",who:"cody"},
|
||||
{sz:"M",name:"<b>draft</b> weekly digest for client",st:"done",who:"cody"},
|
||||
{sz:"M",name:"<b>reply</b> to inbound via agent mailbox",st:"running",who:"cody"},
|
||||
{sz:"L",name:"<b>build</b> onboarding guide pages",st:"queued",who:"cody"},
|
||||
];
|
||||
const ladder=document.getElementById('ladder');
|
||||
function render(){
|
||||
ladder.innerHTML=tasks.map((t,i)=>`<button class="trow" data-st="${t.st}" aria-label="Task ${i+1}: ${t.name.replace(/<[^>]+>/g,'')} — ${t.st}">
|
||||
<span class="sz">${t.sz}</span><span class="name">${t.name}</span>
|
||||
<span class="st"><i></i>${t.st}</span><span class="who">${t.who}</span></button>`).join('');
|
||||
}
|
||||
render();
|
||||
ladder.addEventListener('click',e=>{
|
||||
const row=e.target.closest('.trow'); if(!row)return;
|
||||
const t=tasks[[...ladder.children].indexOf(row)];
|
||||
if(t.st==='queued'){t.st='running';}
|
||||
else if(t.st==='running'){t.st='done';const d=tasks.filter(x=>x.st==='done').length;document.getElementById('mDone').innerHTML=d+'<span class="u"> done</span>';}
|
||||
else{t.st='queued';}
|
||||
render();
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -1,129 +0,0 @@
|
|||
#!/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())
|
||||
|
|
@ -1,289 +0,0 @@
|
|||
#!/usr/bin/env bash
|
||||
# ============================================================================
|
||||
# deploy-claude-code.sh — развернуть Claude Code + ttyd web-TUI на любом сервере
|
||||
# ============================================================================
|
||||
# Воспроизводит боевую развёртку Лунара (t-261005155643: 2.28.121.212, user
|
||||
# lunar, Claude Code 2.1.289 + ttyd 1.7.7, systemd-юнит ttyd-claude.service):
|
||||
# 1) node/npm (если нет: apt → nodesource 22.x, иначе статический tarball)
|
||||
# 2) npm i -g @anthropic-ai/claude-code
|
||||
# 3) ttyd (бинарник с github-releases под архитектуру)
|
||||
# 4) OS-юзер (если нет), systemd-юнит ttyd-claude.service
|
||||
# 5) daemon-reload + enable --now + финальная проверка версий и здоровья
|
||||
#
|
||||
# Использование (root или sudo):
|
||||
# bash scripts/deploy-claude-code.sh # дефолты: :7681, юзер claude, токен сгенерится
|
||||
# bash scripts/deploy-claude-code.sh --port 7681 --user lunar --token MySecret
|
||||
# bash scripts/deploy-claude-code.sh --dry-run --port 9000 --user bob --token x
|
||||
# bash scripts/deploy-claude-code.sh --check # только здоровье, без установки
|
||||
#
|
||||
# Опции:
|
||||
# --port N порт ttyd (по умолчанию 7681, как на Лунаре)
|
||||
# --user NAME OS-юзер сервиса И логин basic-auth (по умолчанию claude)
|
||||
# --token SECRET пароль basic-auth (по умолчанию: openssl rand -hex 12)
|
||||
# --service NAME имя systemd-юнита (по умолчанию ttyd-claude)
|
||||
# --ttyd-version V версия ttyd (по умолчанию 1.7.7)
|
||||
# --node-version V версия node для tarball-фолбэка (по умолчанию 22.14.0)
|
||||
# --claude-version V пин версии claude-code (по умолчанию latest)
|
||||
# --interface IF на чём слушать (по умолчанию 0.0.0.0)
|
||||
# --env K=V доп. Environment= в юнит (повторяемый), например
|
||||
# --env ANTHROPIC_BASE_URL=https://api.z.ai
|
||||
# --readonly терминал только для чтения (без -W)
|
||||
# --dry-run показать план, ничего не делать (root не нужен)
|
||||
# --check только проверка здоровья (версии + HTTP + сервис)
|
||||
# -h | --help эта справка
|
||||
#
|
||||
# Безопасность: веб-терминнал закрыт basic-auth (-c user:token) — как на Лунаре
|
||||
# (логин lunar / Zm9yZXZ0b25lClaude01). Сервис работает от непривилегированного
|
||||
# юзера (User=), никакого root-shell в браузере.
|
||||
# ============================================================================
|
||||
set -euo pipefail
|
||||
|
||||
PORT="7681"
|
||||
USER_NAME="claude"
|
||||
TOKEN=""
|
||||
SERVICE="ttyd-claude"
|
||||
TTYD_VERSION="1.7.7"
|
||||
NODE_VERSION="22.14.0"
|
||||
CLAUDE_VERSION=""
|
||||
INTERFACE="0.0.0.0"
|
||||
READONLY=0
|
||||
DRY_RUN=0
|
||||
CHECK_ONLY=0
|
||||
UNIT_ENV=()
|
||||
|
||||
log() { printf '[deploy-claude-code] %s\n' "$*"; }
|
||||
die() { printf '[deploy-claude-code] ОШИБКА: %s\n' "$*" >&2; exit 2; }
|
||||
have() { command -v "$1" >/dev/null 2>&1; }
|
||||
|
||||
usage() { sed -n '2,37p' "$0" | sed 's/^# \{0,1\}//'; }
|
||||
|
||||
# ── парсинг аргументов ──────────────────────────────────────────────────────
|
||||
while [ $# -gt 0 ]; do
|
||||
case "$1" in
|
||||
--port) PORT="${2:-}"; shift 2 ;;
|
||||
--user) USER_NAME="${2:-}"; shift 2 ;;
|
||||
--token) TOKEN="${2:-}"; shift 2 ;;
|
||||
--service) SERVICE="${2:-}"; shift 2 ;;
|
||||
--ttyd-version) TTYD_VERSION="${2:-}"; shift 2 ;;
|
||||
--node-version) NODE_VERSION="${2:-}"; shift 2 ;;
|
||||
--claude-version) CLAUDE_VERSION="${2:-}"; shift 2 ;;
|
||||
--interface) INTERFACE="${2:-}"; shift 2 ;;
|
||||
--env) UNIT_ENV+=("${2:-}"); shift 2 ;;
|
||||
--readonly) READONLY=1; shift ;;
|
||||
--dry-run) DRY_RUN=1; shift ;;
|
||||
--check) CHECK_ONLY=1; shift ;;
|
||||
-h|--help) usage; exit 0 ;;
|
||||
--port=*|--user=*|--token=*|--service=*|--ttyd-version=*|--node-version=*|\
|
||||
--claude-version=*|--interface=*|--env=*)
|
||||
# форма --opt=value
|
||||
opt="${1%%=*}"; val="${1#*=}"
|
||||
case "$opt" in
|
||||
--port) PORT="$val" ;; --user) USER_NAME="$val" ;;
|
||||
--token) TOKEN="$val" ;; --service) SERVICE="$val" ;;
|
||||
--ttyd-version) TTYD_VERSION="$val" ;; --node-version) NODE_VERSION="$val" ;;
|
||||
--claude-version) CLAUDE_VERSION="$val" ;; --interface) INTERFACE="$val" ;;
|
||||
--env) UNIT_ENV+=("$val") ;;
|
||||
esac; shift ;;
|
||||
*) usage >&2; die "неизвестная опция: $1" ;;
|
||||
esac
|
||||
done
|
||||
|
||||
# ── валидация ────────────────────────────────────────────────────────────────
|
||||
[[ "$PORT" =~ ^[0-9]+$ ]] && [ "$PORT" -ge 1 ] && [ "$PORT" -le 65535 ] \
|
||||
|| die "--port должен быть числом 1..65535 (получили: '$PORT')"
|
||||
[[ "$USER_NAME" =~ ^[a-z_][a-z0-9_-]{0,31}$ ]] \
|
||||
|| die "--user должен быть валидным unix-логином (a-z, 0-9, _, -; получили: '$USER_NAME')"
|
||||
[[ "$SERVICE" =~ ^[a-zA-Z0-9_.@-]+$ ]] || die "--service: некорректное имя юнита '$SERVICE'"
|
||||
case "$TOKEN" in
|
||||
*:*) die "--token не может содержать ':' (ломает -c user:token)" ;;
|
||||
*' '*) die "--token не может содержать пробелы" ;;
|
||||
esac
|
||||
if [ "${#UNIT_ENV[@]}" -gt 0 ]; then
|
||||
for e in "${UNIT_ENV[@]}"; do
|
||||
[[ "$e" =~ ^[A-Za-z_][A-Za-z0-9_]*=.+$ ]] || die "--env ожидает KEY=VALUE (получили: '$e')"
|
||||
done
|
||||
fi
|
||||
|
||||
# токен: явный или сгенерированный (в dry-run без openssl — плейсхолдер)
|
||||
if [ -z "$TOKEN" ]; then
|
||||
if have openssl; then TOKEN="$(openssl rand -hex 12)"; else TOKEN="CHANGE-ME-$(date +%s)"; fi
|
||||
fi
|
||||
|
||||
RUN() { # RUN 'описание' cmd args... — единая точка для dry-run
|
||||
local desc="$1"; shift
|
||||
if [ "$DRY_RUN" -eq 1 ]; then
|
||||
printf '[dry] %s\n -> %s\n' "$desc" "$*"
|
||||
else
|
||||
log "$desc"
|
||||
"$@"
|
||||
fi
|
||||
}
|
||||
|
||||
# ── --check: только здоровье ─────────────────────────────────────────────────
|
||||
health_check() {
|
||||
local fail=0
|
||||
if have claude; then
|
||||
printf 'claude : %s\n' "$(claude --version 2>&1 | head -1)"
|
||||
else
|
||||
printf 'claude : НЕ УСТАНОВЛЕН\n'; fail=1
|
||||
fi
|
||||
if have ttyd; then
|
||||
printf 'ttyd : %s\n' "$(ttyd --version 2>&1 | head -1)"
|
||||
else
|
||||
printf 'ttyd : НЕ УСТАНОВЛЕН\n'; fail=1
|
||||
fi
|
||||
if [ "$(id -u)" -eq 0 ] && have systemctl; then
|
||||
if systemctl is-active --quiet "$SERVICE.service"; then
|
||||
printf 'service: %s active\n' "$SERVICE"
|
||||
else
|
||||
printf 'service: %s НЕ активен\n' "$SERVICE"; fail=1
|
||||
fi
|
||||
fi
|
||||
local code
|
||||
code="$(curl -s -o /dev/null -w '%{http_code}' --max-time 5 "http://127.0.0.1:${PORT}/" || true)"
|
||||
[ -n "$code" ] || code=000
|
||||
# 200 = отвечает; 401 = basic-auth требует логин — тоже «жив»
|
||||
if [ "$code" = "200" ] || [ "$code" = "401" ]; then
|
||||
printf 'http : :%s -> %s (жив)\n' "$PORT" "$code"
|
||||
else
|
||||
printf 'http : :%s -> %s (НЕ отвечает)\n' "$PORT" "$code"; fail=1
|
||||
fi
|
||||
[ "$fail" -eq 0 ] && log 'здоровье: OK' || log 'здоровье: есть проблемы'
|
||||
return "$fail"
|
||||
}
|
||||
|
||||
if [ "$CHECK_ONLY" -eq 1 ]; then
|
||||
health_check || exit 1
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# ── предварительные проверки окружения ───────────────────────────────────────
|
||||
[ "$(id -u)" -eq 0 ] || [ "$DRY_RUN" -eq 1 ] || die "запусти от root (sudo bash $0 ...)"
|
||||
have curl || die "нужен curl (apt-get install -y curl)"
|
||||
if [ "$DRY_RUN" -eq 0 ]; then
|
||||
have systemctl || die "нужен systemd (systemctl не найден)"
|
||||
fi
|
||||
|
||||
log "план: порт=$PORT user=$USER_NAME service=$SERVICE ttyd=$TTYD_VERSION readonly=$READONLY"
|
||||
[ "$DRY_RUN" -eq 1 ] && log 'режим DRY-RUN: ничего не устанавливается'
|
||||
|
||||
# ── 1) node/npm ──────────────────────────────────────────────────────────────
|
||||
node_major() { node -v 2>/dev/null | sed 's/^v//' | cut -d. -f1; }
|
||||
node_ok() {
|
||||
have node && have npm && [ "$(node_major)" -ge 18 ] 2>/dev/null
|
||||
}
|
||||
if node_ok; then
|
||||
log "node $(node -v) / npm $(npm -v) уже есть — пропускаю"
|
||||
else
|
||||
if have apt-get; then
|
||||
# путь Лунара: nodesource setup_22.x + apt
|
||||
RUN 'устанавливаю node 22 (nodesource + apt)' \
|
||||
bash -c 'set -e; curl -fsSL https://deb.nodesource.com/setup_22.x -o /tmp/n.sh && bash /tmp/n.sh >/dev/null 2>&1; apt-get install -y nodejs >/dev/null 2>&1 || apt-get install -y nodejs'
|
||||
else
|
||||
# фолбэк: статический tarball с nodejs.org (любой дистрибутив)
|
||||
NARCH=''; case "$(uname -m)" in
|
||||
x86_64) NARCH='x64' ;;
|
||||
aarch64) NARCH='arm64' ;;
|
||||
armv7l) NARCH='armv7l' ;;
|
||||
*) die "неподдерживаемая архитектура для node-tarball: $(uname -m)" ;;
|
||||
esac
|
||||
RUN "устанавливаю node $NODE_VERSION (tarball linux-$NARCH в /usr/local)" \
|
||||
bash -c "set -e; curl -fsSL -o /tmp/node.tar.xz https://nodejs.org/dist/v$NODE_VERSION/node-v$NODE_VERSION-linux-$NARCH.tar.xz && tar -xJf /tmp/node.tar.xz -C /usr/local --strip-components=1"
|
||||
fi
|
||||
[ "$DRY_RUN" -eq 1 ] || { node_ok || die 'node не установился (см. вывод выше)'; }
|
||||
fi
|
||||
|
||||
# ── 2) Claude Code ───────────────────────────────────────────────────────────
|
||||
if have claude && [ -z "$CLAUDE_VERSION" ] && [ "$DRY_RUN" -eq 0 ]; then
|
||||
log "claude уже установлен: $(claude --version 2>&1 | head -1) — обновляю до latest"
|
||||
fi
|
||||
if [ -n "$CLAUDE_VERSION" ]; then
|
||||
RUN "npm i -g @anthropic-ai/claude-code@$CLAUDE_VERSION" npm install -g "@anthropic-ai/claude-code@$CLAUDE_VERSION"
|
||||
else
|
||||
RUN 'npm i -g @anthropic-ai/claude-code (latest)' npm install -g @anthropic-ai/claude-code
|
||||
fi
|
||||
if [ "$DRY_RUN" -eq 0 ]; then
|
||||
have claude || ln -sf "$(npm prefix -g)/bin/claude" /usr/local/bin/claude 2>/dev/null || true
|
||||
have claude || die 'claude не найден после npm i'
|
||||
log "claude: $(claude --version 2>&1 | head -1)"
|
||||
fi
|
||||
|
||||
# ── 3) ttyd ──────────────────────────────────────────────────────────────────
|
||||
TARCH=''; case "$(uname -m)" in
|
||||
x86_64) TARCH='x86_64' ;;
|
||||
aarch64) TARCH='aarch64' ;;
|
||||
armv7l|armv6l) TARCH='arm' ;;
|
||||
i686) TARCH='i686' ;;
|
||||
*) die "неподдерживаемая архитектура для ttyd: $(uname -m)" ;;
|
||||
esac
|
||||
if have ttyd && [ "$DRY_RUN" -eq 0 ]; then
|
||||
log "ttyd уже есть: $(ttyd --version 2>&1 | head -1) — пропускаю скачивание"
|
||||
else
|
||||
RUN "ставлю ttyd $TTYD_VERSION ($TARCH) в /usr/local/bin" \
|
||||
bash -c "set -e; curl -fsSL -o /usr/local/bin/ttyd https://github.com/tsl0922/ttyd/releases/download/$TTYD_VERSION/ttyd.$TARCH && chmod +x /usr/local/bin/ttyd"
|
||||
fi
|
||||
|
||||
# ── 4) OS-юзер и systemd-юнит ────────────────────────────────────────────────
|
||||
if [ "$DRY_RUN" -eq 0 ]; then
|
||||
if ! id -u "$USER_NAME" >/dev/null 2>&1; then
|
||||
log "создаю юзера $USER_NAME (без root-shell в веб-терминале)"
|
||||
useradd -m -s /bin/bash "$USER_NAME"
|
||||
fi
|
||||
fi
|
||||
|
||||
WRITABLE='-W'; [ "$READONLY" -eq 1 ] && WRITABLE=''
|
||||
UNIT_PATH="/etc/systemd/system/${SERVICE}.service"
|
||||
ENV_LINES="Environment=HOME=/home/${USER_NAME}
|
||||
Environment=PATH=/usr/local/bin:/usr/bin:/bin"
|
||||
if [ ${#UNIT_ENV[@]} -gt 0 ]; then
|
||||
for e in "${UNIT_ENV[@]}"; do ENV_LINES+="
|
||||
Environment=$e"; done
|
||||
fi
|
||||
UNIT_CONTENT="[Unit]
|
||||
Description=ttyd web terminal -> Claude Code (deploy-claude-code.sh)
|
||||
After=network-online.target
|
||||
Wants=network-online.target
|
||||
|
||||
[Service]
|
||||
User=${USER_NAME}
|
||||
${ENV_LINES}
|
||||
ExecStart=/usr/local/bin/ttyd ${WRITABLE} -c ${USER_NAME}:${TOKEN} -p ${PORT} -i ${INTERFACE} claude
|
||||
Restart=always
|
||||
RestartSec=3
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
"
|
||||
if [ "$DRY_RUN" -eq 1 ]; then
|
||||
printf '[dry] пишу %s:\n%s\n' "$UNIT_PATH" "$UNIT_CONTENT"
|
||||
else
|
||||
CHANGED=1
|
||||
if [ -f "$UNIT_PATH" ] && [ "$(cat "$UNIT_PATH")" = "$UNIT_CONTENT" ]; then
|
||||
CHANGED=0; log 'юнит не изменился — перезапуск не нужен'
|
||||
fi
|
||||
printf '%s\n' "$UNIT_CONTENT" > "$UNIT_PATH"
|
||||
systemctl daemon-reload
|
||||
systemctl enable --now "$SERVICE.service" >/dev/null 2>&1 || true
|
||||
if [ "$CHANGED" -eq 1 ] || ! systemctl is-active --quiet "$SERVICE.service"; then
|
||||
systemctl restart "$SERVICE.service"
|
||||
fi
|
||||
sleep 1
|
||||
fi
|
||||
|
||||
# ── 5) финальная проверка ────────────────────────────────────────────────────
|
||||
if [ "$DRY_RUN" -eq 1 ]; then
|
||||
log 'dry-run завершён: план выше, запускай без --dry-run'
|
||||
exit 0
|
||||
fi
|
||||
IP="$(hostname -I 2>/dev/null | awk '{print $1}')"
|
||||
[ -n "$IP" ] || IP='<ip-сервера>'
|
||||
log '─'
|
||||
health_check || die 'финальная проверка не прошла (см. выше)'
|
||||
|
||||
log '─'
|
||||
log "ГОТОВО: Claude Code + ttyd web-TUI"
|
||||
log " URL : http://${IP}:${PORT}"
|
||||
log " логин : ${USER_NAME}"
|
||||
log " пароль : ${TOKEN}"
|
||||
log " сервис : systemctl status ${SERVICE}"
|
||||
log " первый вход: открой URL, пройди авторизацию claude (или передай --env ANTHROPIC_BASE_URL=... при развёртке)"
|
||||
|
|
@ -1,133 +0,0 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Генерация PPTX: (1) разговор с Леонидом, (2) питч Simbiot (Seedance)."""
|
||||
from pptx import Presentation
|
||||
from pptx.util import Inches, Pt, Emu
|
||||
from pptx.dml.color import RGBColor
|
||||
from pptx.enum.text import PP_ALIGN
|
||||
|
||||
BG = RGBColor(0x0A, 0x0C, 0x12)
|
||||
ACCENT = RGBColor(0x8B, 0x5C, 0xF6)
|
||||
ACCENT2 = RGBColor(0x7C, 0x6C, 0xF5)
|
||||
TEXT = RGBColor(0xF0, 0xEE, 0xFF)
|
||||
MUT = RGBColor(0x9A, 0x97, 0xB8)
|
||||
|
||||
def add_slide(prs, title, bullets, kicker=None):
|
||||
s = prs.slides.add_slide(prs.slide_layouts[6])
|
||||
s.background.fill.solid(); s.background.fill.fore_color.rgb = BG
|
||||
w, h = prs.slide_width, prs.slide_height
|
||||
# accent bar
|
||||
bar = s.shapes.add_shape(1, Emu(0), Emu(0), Emu(w), Inches(0.09))
|
||||
bar.fill.solid(); bar.fill.fore_color.rgb = ACCENT; bar.line.fill.background()
|
||||
if kicker:
|
||||
tb = s.shapes.add_textbox(Inches(0.7), Inches(0.45), Inches(11.8), Inches(0.4))
|
||||
tf = tb.text_frame; p = tf.paragraphs[0]; r = p.add_run(); r.text = kicker.upper()
|
||||
r.font.size = Pt(12); r.font.color.rgb = ACCENT2; r.font.bold = True
|
||||
tb = s.shapes.add_textbox(Inches(0.7), Inches(0.85), Inches(12.0), Inches(1.0))
|
||||
tf = tb.text_frame; p = tf.paragraphs[0]; r = p.add_run(); r.text = title
|
||||
r.font.size = Pt(30); r.font.color.rgb = TEXT; r.font.bold = True
|
||||
tb = s.shapes.add_textbox(Inches(0.75), Inches(1.85), Inches(11.9), Inches(4.9))
|
||||
tf = tb.text_frame; tf.word_wrap = True
|
||||
first = True
|
||||
for b in bullets:
|
||||
lvl = 0
|
||||
if isinstance(b, tuple): b, lvl = b
|
||||
p = tf.paragraphs[0] if first else tf.add_paragraph(); first = False
|
||||
r = p.add_run(); r.text = ("• " if lvl == 0 else "– ") + b
|
||||
r.font.size = Pt(16 if lvl == 0 else 14); r.font.color.rgb = TEXT if lvl == 0 else MUT
|
||||
p.space_after = Pt(8)
|
||||
return s
|
||||
|
||||
prs = Presentation(); prs.slide_width = Inches(13.333); prs.slide_height = Inches(7.5)
|
||||
|
||||
K = "Встреча 143 · 2026-10-08"
|
||||
add_slide(prs, "Разговор Алекса с Леонидом", [
|
||||
"Выжимка разговора и договорённости · ~2.5 часа",
|
||||
"Jitsi + агент-визуализатор Simbiot (слайды рисуются в реальном времени)",
|
||||
"Кооператив 42 / студия Simbiot",
|
||||
], kicker=K)
|
||||
add_slide(prs, "Кто есть кто", [
|
||||
"Леонид — давний знакомый студии, писатель, инвестор в deep-tech (Anti-Aging Hero с физиком Петром Федичевым)",
|
||||
"DGX Spark 128GB (€5000) — дома гоняет CUDA-моделирование графов надёжности",
|
||||
"Алекс — инфраструктура студии: агент-визуализатор, собственный роутер LM, RAG по знаниям кооператива",
|
||||
("«1-й раз такое в моей практике… польза очевидна» — Леонид о демо", 1),
|
||||
], kicker=K)
|
||||
add_slide(prs, "Демо инфраструктуры студии", [
|
||||
"Агент подключается к любому звонку и рисует слайды по ходу разговора (~45 сек на слайд)",
|
||||
"Роутер сам выбирает LM под контекст (визуализация — GLM 5.3 Flash)",
|
||||
"RAG: «он знает всё, что мы делаем» — общая база знаний кооператива",
|
||||
("Инсайт Леонида: домашняя коробочка → быстрая генерация музыки/картинок/мини-видео", 1),
|
||||
], kicker=K)
|
||||
add_slide(prs, "Пилот: мультик по рассказу Леонида", [
|
||||
"Мульт-адаптация рассказов (10–15 мин); Леонид готов финансово поддержать пилот",
|
||||
"Ориентир бюджета — «в пределах 1000» на эксперимент (токены — точно, труд — обсудить)",
|
||||
"Вход от студии: рассказ + свобода интерпретации, без пожеланий-ограничений",
|
||||
"Партнёр уже строит harness генерации мультиков — матч идеальный; видео-часть ведёт Влад",
|
||||
("Первый шаг — аудиоспектакль (дешевле и предсказуемее), затем видео", 1),
|
||||
], kicker=K)
|
||||
add_slide(prs, "Творческий метод Леонида", [
|
||||
"Хакатон ~2018: нейросеть на сольных песнях Леннона/Маккартни классифицировала битловские",
|
||||
"Пишет с ИИ (~80% своё / ~20% ИИ), честно маркирует «made with AI» на after.today",
|
||||
("«Литературные негры» и мастерская Микеланджело — ИИ-ассистированное творчество не ново, нова честность", 1),
|
||||
], kicker=K)
|
||||
add_slide(prs, "Экономика локального инференса", [
|
||||
"Леонид: DGX Spark — нормальная цена за «домашний интеллект»; коробки станут достоянием домов",
|
||||
"Алекс: облачный always-on — ок для большинства; топ-модели для разработки всё равно выигрывают",
|
||||
("Сошлись на гибриде: локально — быстрая генерация, облако — тяжёлые продуктовые задачи", 1),
|
||||
("У студии в планах свой inference-сервер (в ~6 раз быстрее)", 1),
|
||||
], kicker=K)
|
||||
add_slide(prs, "Как строить AI-компанию", [
|
||||
"Леонид: «Vision — говно, execution — всё». Из короткого промпта не вырастет сложная система",
|
||||
"Алекс: ядро студии — AI-ядро, перестраивающее паттерны (64) при смене контекста — главный долгосрочный актив",
|
||||
"Стратегия: бизнес на нейро-инфраструктуре с начала → перевод на автопилот; незаменим только «CEO-решатель»",
|
||||
("«Изюминка студии — в AI-ядре» — Леонид", 1),
|
||||
("Методология deep-tech: продавать ценность сразу, не влюбляясь в R&D", 1),
|
||||
], kicker=K)
|
||||
add_slide(prs, "Договорённости", [
|
||||
"1. Пилот анимации по рассказу Леонида: токены покрывает Леонид (до ~1000), ведёт Влад; Alex вернётся с конкретным предложением",
|
||||
"2. Формат разговора продолжается — Леонид открыт к следующим сессиям",
|
||||
"3. Голоса/доли за прошлые инвестиции — Леонид отказался: «инвестирую из философских соображений, не создавайте ожиданий»",
|
||||
"4. Леонид посмотрит 35-минутную презентацию студии и вернётся с фидбэком",
|
||||
], kicker=K)
|
||||
add_slide(prs, "Что делать студии дальше", [
|
||||
"Подготовить оффер по мульт-пилоту: объём, этап аудиоспектакля, смета токенов",
|
||||
"Подключить партнёрский harness генерации мультиков — Леонид как первый клиентский кейс",
|
||||
"Зафиксировать кейс «агент-визуализатор на внешнем звонке» — первое живое демо, положительный фидбэк",
|
||||
], kicker=K)
|
||||
prs.save("out/leonid_meeting143.pptx")
|
||||
|
||||
# --- Pitch deck: Simbiot / Seedance ---
|
||||
prs2 = Presentation(); prs2.slide_width = Inches(13.333); prs2.slide_height = Inches(7.5)
|
||||
K2 = "Simbiot · питч"
|
||||
add_slide(prs2, "Simbiot — общий харнес организации", [
|
||||
"Стартап за неделю получает не одного ассистента, а команду ИИ-агентов:",
|
||||
"один отвечает клиентам, второй пишет код, третий следит за деньгами и метриками",
|
||||
"Слой поверх Hermes, Claude Code и других — вместо зоопарка агентов и харнесов",
|
||||
], kicker=K2)
|
||||
add_slide(prs2, "Проблема", [
|
||||
"Фаундер собирает зоопарк из агентов и харнесов вручную",
|
||||
"Ушёл разработчик — ушла вся магия: знания и автоматизации не переживают людей",
|
||||
"Каждый новый инструмент — ещё один остров, не связанный с остальными",
|
||||
], kicker=K2)
|
||||
add_slide(prs2, "Решение", [
|
||||
"Simbiot координирует флот агентов и фиксирует решения компании",
|
||||
"Удачные приёмы копятся как «гены» — остаются только те, что приносят прибыль",
|
||||
"Компания превращается в самообучающуюся нейро-инфраструктуру",
|
||||
], kicker=K2)
|
||||
add_slide(prs2, "Тракшен", [
|
||||
"Уже гоняется на семи собственных продуктах студии",
|
||||
"AI-ядро: 64 паттерна, перестраивающихся при смене контекста",
|
||||
"Агент-визуализатор, RAG по знаниям, собственный LM-роутер — в продакшене",
|
||||
], kicker=K2)
|
||||
add_slide(prs2, "Видео (Seedance), 3 шота", [
|
||||
"Shot 1 «зоопарк в хаосе»: тёмная серверная, хаотичные голограммы и глитчи (0–10 сек)",
|
||||
"Shot 2 «orchestration»: хаос сходится в фиолетовую сферу, панели выстраиваются в сетку (10–20 сек)",
|
||||
"Shot 3 «финал»: фаундер у стены дашбордов с растущими revenue-графиками, отъезд в город (20–30 сек)",
|
||||
("Стиль: dark cinematic, #0a0c12 + violet #7c6cf5, film grain, 4k, 16:9", 1),
|
||||
], kicker=K2)
|
||||
add_slide(prs2, "Строй компанию — а не зоопарк ботов", [
|
||||
"Simbiot · Кооператив 42 · simbiot",
|
||||
"Хронометраж питча: ~35 сек (15 сек — первый и последний абзац)",
|
||||
], kicker=K2)
|
||||
prs2.save("out/simbiot_pitch.pptx")
|
||||
print("OK")
|
||||
|
|
@ -1,14 +0,0 @@
|
|||
#!/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
|
||||
|
|
@ -1,23 +0,0 @@
|
|||
[Unit]
|
||||
Description=Simbiot Harness - Telegram inbox poller
|
||||
After=network-online.target simbiot-harness.service
|
||||
Wants=network-online.target
|
||||
|
||||
[Service]
|
||||
Environment=SIMBIOT_HOME=__DATA_DIR__
|
||||
Environment=SIMBIOT_RAG_DB=/root/rag/rag.db
|
||||
Type=simple
|
||||
WorkingDirectory=__INSTALL_DIR__
|
||||
ExecStart=__INSTALL_DIR__/venv/bin/python -m harness.tg_inbox
|
||||
Restart=always
|
||||
RestartSec=5
|
||||
Environment=PYTHONUNBUFFERED=1
|
||||
# t-261004160531-780666: поллер лёгкий — тяжёлая транскрипция (faster-whisper
|
||||
# large-v3-turbo ~2-3G, ffmpeg) вынесена в отдельные transient-scope'ы
|
||||
# (systemd-run --scope, см. harness/voice.py _heavy_run) с их MemoryMax.
|
||||
# Жёсткий потолок юнита: 3.7G peak больше невозможен физически.
|
||||
MemoryMax=1G
|
||||
MemoryHigh=768M
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
|
|
@ -1,168 +0,0 @@
|
|||
#!/usr/bin/env python3
|
||||
# ═══════════════════════════════════════════════════════════
|
||||
# SPA filler — Studio_Participation_Agreement_2026-09 → per-founder
|
||||
# Задачи t-261008143050 (Siarhei Zhukovich, когорта с 16-го) и
|
||||
# t-261008144131 (7 участников 42 Labs за 2026-09).
|
||||
# Паттерн подстановки — как _contract_docx_bytes в /root/coop/app.py.
|
||||
# ═══════════════════════════════════════════════════════════
|
||||
import os, sys, copy
|
||||
import docx
|
||||
|
||||
TEMPLATE = "/root/coop/legal/Studio_Participation_Agreement_2026-09.docx"
|
||||
OUT = "/root/coop/legal"
|
||||
|
||||
DRAFT_LINE = "DRAFT TEMPLATE — for review by U.S. counsel before first use. Not legal advice."
|
||||
|
||||
# Перепривязка платежного графика на 16-е число (когорты с 16-го, t-261008143050)
|
||||
SIXTEENTH = [
|
||||
("no later than the 1st day of the first month of participation",
|
||||
"no later than the 16th day of the first month of participation"),
|
||||
("each subsequent Monthly Fee no later than the 1st day of the corresponding month of participation",
|
||||
"each subsequent Monthly Fee no later than the 16th day of the corresponding month of participation"),
|
||||
("If the Agreement is signed after the 1st, the Program Start is the 1st day of the next calendar month",
|
||||
"If the Agreement is signed after the 16th, the Program Start is the 16th day of the next calendar month"),
|
||||
("for Monthly Fee tranches, the 1st day of the participation month",
|
||||
"for Monthly Fee tranches, the 16th day of the participation month"),
|
||||
("takes effect on the 1st day of the next participation month",
|
||||
"takes effect on the 16th day of the next participation month"),
|
||||
("payable in advance by the 1st of each participation month",
|
||||
"payable in advance by the 16th of each participation month"),
|
||||
("the 1st day of the month of signature if signed on the 1st, otherwise the 1st day of the next calendar month",
|
||||
"the 16th day of the month of signature if signed on the 16th, otherwise the 16th day of the next calendar month"),
|
||||
]
|
||||
|
||||
|
||||
def founder_replacements(f):
|
||||
"""f: dict(name, passport, pid, address, email, project, one_line, rev_share,
|
||||
effective, cohort, program_start, sixteenth: bool)"""
|
||||
r = {
|
||||
"[DATE]": f["effective"],
|
||||
"[FULL LEGAL NAME]": f["name"],
|
||||
"[NAME]": f["name"],
|
||||
"[EMAIL]": f.get("email") or "[EMAIL]",
|
||||
"cohort [MONTH YEAR]": f"cohort {f['cohort']}",
|
||||
"1 [MONTH] [YEAR]": f["program_start"],
|
||||
"[MONTH YEAR]": f["cohort"],
|
||||
}
|
||||
if f.get("rev_share"):
|
||||
r["[X]"] = str(f["rev_share"])
|
||||
if f.get("project"):
|
||||
r["[PROJECT NAME]"] = f["project"]
|
||||
if f.get("one_line"):
|
||||
r["[ONE LINE]"] = f["one_line"]
|
||||
# Строка Founder: имя + статус + паспорт + адрес одной подстановкой
|
||||
line_old = "[FULL LEGAL NAME], an individual / [ENTITY TYPE], residing at [ADDRESS], [COUNTRY]"
|
||||
line_new = (f"{f['name']}, an individual, passport of the Republic of Belarus "
|
||||
f"{f.get('passport') or '[PASSPORT NO.]'} (personal identification No. "
|
||||
f"{f.get('pid') or '[PERSONAL ID NO.]'}), residing at "
|
||||
f"{f.get('address') or '[ADDRESS]'}, Republic of Belarus")
|
||||
r[line_old] = line_new
|
||||
# фолбэк, если строка разбита рант differently
|
||||
r["an individual / [ENTITY TYPE]"] = "an individual"
|
||||
# паспорт/ид.номер вставляются после схлопывания individual-строки
|
||||
# (порядок ключей dict важен: этот ключ применяется после предыдущего)
|
||||
pass_line = (f"passport of the Republic of Belarus {f.get('passport') or '[PASSPORT NO.]'}"
|
||||
f" (personal identification No. {f.get('pid') or '[PERSONAL ID NO.]'}), residing at")
|
||||
r["an individual, residing at"] = f"an individual, {pass_line}"
|
||||
r["[ADDRESS], [COUNTRY]"] = f"{f.get('address') or '[ADDRESS]'}, Republic of Belarus"
|
||||
r["[ADDRESS]"] = f.get("address") or "[ADDRESS]"
|
||||
r["[COUNTRY]"] = "Republic of Belarus"
|
||||
return r
|
||||
|
||||
|
||||
def fill(f, out_docx):
|
||||
doc = docx.Document(TEMPLATE)
|
||||
reps = founder_replacements(f)
|
||||
if f.get("sixteenth"):
|
||||
reps = dict(SIXTEENTH) | reps
|
||||
reps[DRAFT_LINE] = ""
|
||||
for p in doc.paragraphs:
|
||||
if not p.runs:
|
||||
continue
|
||||
full = "".join(r.text for r in p.runs)
|
||||
new = full
|
||||
for k, v in reps.items():
|
||||
if k in new:
|
||||
new = new.replace(k, v)
|
||||
if new != full:
|
||||
p.runs[0].text = new
|
||||
for r in p.runs[1:]:
|
||||
r.text = ""
|
||||
doc.save(out_docx)
|
||||
return out_docx
|
||||
|
||||
|
||||
def to_pdf(docx_path):
|
||||
import subprocess, tempfile
|
||||
with tempfile.TemporaryDirectory() as td:
|
||||
r = subprocess.run(["soffice", "--headless", "--convert-to", "pdf", "--outdir", td, docx_path],
|
||||
capture_output=True, timeout=90)
|
||||
pdf = os.path.join(td, os.path.basename(docx_path).replace(".docx", ".pdf"))
|
||||
if os.path.exists(pdf):
|
||||
dst = docx_path.replace(".docx", ".pdf")
|
||||
open(dst, "wb").write(open(pdf, "rb").read())
|
||||
return dst
|
||||
return None
|
||||
|
||||
|
||||
def leftover_placeholders(docx_path):
|
||||
import re
|
||||
d = docx.Document(docx_path)
|
||||
found = set()
|
||||
for p in d.paragraphs:
|
||||
found.update(re.findall(r"\[[A-Z][A-Z0-9 /.,%()\[\]-]*\]", p.text))
|
||||
return sorted(found)
|
||||
|
||||
|
||||
FOUNDERS = [
|
||||
# ── t-261008143050: Siarhei Zhukovich, когорта с 16-го ──
|
||||
dict(file="SPA_Siarhei_Zhukovich", name="Siarhei Zhukovich",
|
||||
passport="AB3393306", pid=None, # ид. номер — нет в базе, плейсхолдер
|
||||
address="28 July str., 33B-42, Brest", email=None,
|
||||
project="podrabotki.by (rebranding to StaffGo)",
|
||||
one_line="a platform for hiring line staff: employment/contract placement and turnkey contractor paperwork (documents and payments)",
|
||||
rev_share=50, effective="October 16, 2026", cohort="October 2026",
|
||||
program_start="16 October 2026", sixteenth=True),
|
||||
# ── t-261008144131: участники 42 Labs за 2026-09 (lab_debts) ──
|
||||
dict(file="SPA_Alex_Shkor", name="Alex Shkor", passport=None, pid=None,
|
||||
address=None, email="alex@shkor.com", project=None, one_line=None,
|
||||
rev_share=None, effective="September 1, 2026", cohort="September 2026",
|
||||
program_start="1 September 2026", sixteenth=False),
|
||||
dict(file="SPA_Vlad", name="Vlad", passport=None, pid=None,
|
||||
address=None, email="vla2music.com@gmail.com", project=None, one_line=None,
|
||||
rev_share=None, effective="September 1, 2026", cohort="September 2026",
|
||||
program_start="1 September 2026", sixteenth=False),
|
||||
dict(file="SPA_Yura", name="Yura", passport=None, pid=None,
|
||||
address=None, email="yura28440@gmail.com", project=None, one_line=None,
|
||||
rev_share=None, effective="September 1, 2026", cohort="September 2026",
|
||||
program_start="1 September 2026", sixteenth=False),
|
||||
dict(file="SPA_Stas_Dashchinskiy", name="Stas Dashchinskiy", passport=None, pid=None,
|
||||
address=None, email="dashchinskiystas@gmail.com", project=None, one_line=None,
|
||||
rev_share=None, effective="September 1, 2026", cohort="September 2026",
|
||||
program_start="1 September 2026", sixteenth=False),
|
||||
dict(file="SPA_Alexander", name="Александр", passport=None, pid=None,
|
||||
address=None, email="Justgbox@gmail.com", project=None, one_line=None,
|
||||
rev_share=None, effective="September 1, 2026", cohort="September 2026",
|
||||
program_start="1 September 2026", sixteenth=False),
|
||||
dict(file="SPA_Melnikau", name="Andrei Melnikau", passport="MP2848482",
|
||||
pid="3120386A064PB1", address=None, email="ex3.mac@gmail.com",
|
||||
project="tYESter",
|
||||
one_line="a family of software tools for testing computer hardware components without the need to purchase additional equipment",
|
||||
rev_share=None, effective="September 1, 2026", cohort="September 2026",
|
||||
program_start="1 September 2026", sixteenth=False),
|
||||
dict(file="SPA_Dmitry_Siasitski", name="Dmitry Siasitski", passport=None, pid=None,
|
||||
address=None, email="sesitski.dmitry@gmail.com", project=None, one_line=None,
|
||||
rev_share=None, effective="September 1, 2026", cohort="September 2026",
|
||||
program_start="1 September 2026", sixteenth=False),
|
||||
]
|
||||
|
||||
if __name__ == "__main__":
|
||||
only = sys.argv[1:] or None
|
||||
for f in FOUNDERS:
|
||||
if only and f["file"] not in only:
|
||||
continue
|
||||
docx_path = os.path.join(OUT, f["file"] + ".docx")
|
||||
fill(f, docx_path)
|
||||
pdf = to_pdf(docx_path)
|
||||
print(f"{f['file']}: docx={'OK' if os.path.exists(docx_path) else 'FAIL'} "
|
||||
f"pdf={'OK' if pdf else 'FAIL'} leftover={leftover_placeholders(docx_path)}")
|
||||
|
|
@ -12,7 +12,6 @@
|
|||
|---|---|---|
|
||||
| `meta-skill-factory` | Метаскилл: превращение повторяющейся работы в скилл репо (шаги, чеклисты, критерии качества) | Заметил, что делаю одну и ту же работу 2+ раз — прежде чем делать руками в третий |
|
||||
| `presale-landing` | Производство пресейл/продуктовых лендингов: структура, честные мокапы, Stripe CTA, quality gates | Задача на лендинг/пресейл-страницу или её v2 |
|
||||
| `gcal` | Google Calendar: команда из TG «сделай звонок на X» → событие с приглашениями в рабочий день (цикл 3 через 3); инструменты tools/gcal.py | Просьба спланировать звонок/встречу/событие, вопрос по графику |
|
||||
| `systemone-bench` | Decision-эндпоинт Ollama /v1/systemone (nimble) + GA-оптимизация: иерархический выбор top-2 группы → choice ≤9, GA последовательностей действий с фитнесом=AUM | Тик, где нужно выбрать одно действие из >9 однородных опций; оптимизация политик на симуляторе |
|
||||
|
||||
## Сторонние наборы
|
||||
|
|
|
|||
|
|
@ -1,73 +0,0 @@
|
|||
---
|
||||
description: Build compact premium landing pages with clear CTA hierarchy, quiet typography, and minimal visual noise. Install from Danilaa1 in one command.
|
||||
---
|
||||
# Compact Landing
|
||||
Build a small, finished product world—not a generic marketing template. Preserve compactness and clarity; reinvent the visible design language for each product.
|
||||
## Workflow
|
||||
1. Inspect the product, supplied content, current stack, and existing design language. Reuse installed tools and components.
|
||||
2. Ask for button size first—compact, medium, or large—unless the user already answered or asked you to choose.
|
||||
3. Ask at most two adaptive questions only when their answers would materially change the result. Good subjects: brand mood, content depth, corner character, or motion level.
|
||||
4. Create a design fingerprint before coding. If this skill includes scripts and Node is available, run `node scripts/pick-direction.mjs`. Otherwise choose from `references/style-dna.md` without taking the first option in every list.
|
||||
5. Adapt the fingerprint to product meaning. Treat picker output as a creative constraint, not a command to make incoherent choices.
|
||||
6. Identify one product-native signature artifact: a real demo, visualized output, interactive sample, comparison, miniature workflow, data specimen, or useful control. This should create identity more than decoration does.
|
||||
7. Implement the smallest complete page. Prefer HTML/CSS and existing dependencies. Add JavaScript only for behavior the page actually needs.
|
||||
8. Verify mobile layout, keyboard focus, reduced motion, stable dynamic states, and the real primary action.
|
||||
If the user wants immediate execution, choose the button size and fingerprint yourself. Do not stop for a questionnaire.
|
||||
## Design Fingerprint
|
||||
Define these axes before implementation:
|
||||
- **Composition:** field note, offset rail, split pamphlet, framed console, compact catalogue, stacked specimen, or another product-specific silhouette.
|
||||
- **Measure:** choose a fitting compact width, usually `420-720px`; do not default to `440px` every time.
|
||||
- **Palette:** warm paper, cool fog, monochrome ink, dark charcoal, product-tinted, or playful light.
|
||||
- **Typography:** grotesk + mono, humanist sans + mono, editorial serif + sans, rounded sans + mono, condensed + sans, or purposeful all-mono.
|
||||
- **Geometry:** sharp, machined, balanced, friendly, soft, or mixed/concentric radii.
|
||||
- **Surface grammar:** whitespace, flat dividers, inset hairlines, layered rings, tinted panels, paper rules, or technical frames. Pick one dominant grammar.
|
||||
- **Motif:** emoji punctuation, ASCII marks, indices, stamps, doodles, colored dots, tiny object icons, or none.
|
||||
- **Motion:** choose one entrance behavior and one or two interaction behaviors.
|
||||
Variation must change the first-screen silhouette, not merely colors or labels. Ensure at least four axes differ from the old canonical recipe: centered `440px` column, neutral white/zinc, Geist + Geist Mono, `6-10px` radii, inset hairline cards, Instrument order, staggered rise.
|
||||
Do not randomize components independently. Choose one coherent art direction, then derive tokens from it. Use only one or two surprising details; repetition inside the page creates coherence.
|
||||
Read `references/style-dna.md` for compatible ranges, direction families, motion patterns, and review checks.
|
||||
## Invariants
|
||||
- Keep the page compact in scope and information density, even when its canvas is wider or split.
|
||||
- Put the primary action in the first viewport. Repeat it only when doing so removes friction.
|
||||
- Use clear type hierarchy, concrete copy, and enough contrast.
|
||||
- Use real product content. Avoid fake metrics, vague claims, filler testimonials, and decorative dashboards.
|
||||
- Extend visually tiny controls to at least a `40px` hit area without making them look oversized.
|
||||
- Show a visible `:focus-visible` state and preserve semantic HTML.
|
||||
- Use tabular numerals for changing numbers.
|
||||
- Respect `prefers-reduced-motion` and keep the reduced experience fully usable.
|
||||
- Never add a dependency for a small effect CSS or the current stack can handle.
|
||||
## Zero-Shift Motion
|
||||
- Animate only `transform`, `opacity`, and `filter`. Never use `transition: all`.
|
||||
- Reserve dimensions before animation: fixed/min heights, `aspect-ratio` for media, fixed icon slots, width-locked changing labels, and tabular numerals.
|
||||
- Overlay alternate labels, icons, tabs, and panels in the same grid area or fixed box. Do not insert delayed content into document flow.
|
||||
- Keep padding, borders, font size, font weight, line height, grid tracks, and flex sizing stable across hover and state changes.
|
||||
- Prefer transforms for toggles, drawers, traces, and decorative motion. Absolute-position decorative particles so they never affect flow.
|
||||
- Self-host fonts when practical. Otherwise use metric-compatible fallbacks and avoid hiding the page while fonts load.
|
||||
- Gate hover-only effects behind `(hover: hover) and (pointer: fine)`.
|
||||
## Buttons
|
||||
Apply one scale consistently:
|
||||
- **Compact:** `24-28px` visual height, `11-12px` label.
|
||||
- **Medium:** `32-36px` visual height, `12.5-13px` label.
|
||||
- **Large:** `40-44px` visual height, `13.5-14px` label.
|
||||
Use `scale(0.96)` for press feedback. For wide command controls, use `0.98`. Keep label and icon slots stable during copied/loading/success states.
|
||||
For `llms.txt` or assistant-copy actions, use a small mono button only when the product needs it. Reuse the project's icon system or a CSS glyph; do not embed SVG examples in skill output.
|
||||
## Purposeful Personality
|
||||
Emoji are allowed when they fit the product. Use `1-3` as punctuation, labels, tiny stickers, or state cues—not as a substitute for structure or iconography. Reserve their boxes so platform glyph differences do not move layout.
|
||||
Color may be playful, dark, editorial, or product-tinted. Give every accent a role and repeat it. Avoid default purple-blue AI gradients, uncontrolled rainbows, and random decorative color.
|
||||
Motion should explain state, focus attention, or express the product metaphor. A fixed-size trace, ambient floating mark, slot swap, clipped reveal, or state color wash can feel alive without causing reflow. Avoid perpetual motion everywhere.
|
||||
## Avoid
|
||||
- Reusing one section order, narrow width, palette, radius family, or component set across unrelated products.
|
||||
- Generic oversized heroes, three equal feature cards, badge clouds, and feature grids by reflex.
|
||||
- Copying the Cuelume waveform, cream palette, confetti trio, or exact layout unless the product genuinely calls for them.
|
||||
- Making every surface a card or every control a pill.
|
||||
- Decorative animation that competes with the primary action.
|
||||
- Heavy animation libraries for opacity, transforms, tabs, counters, or simple reveals.
|
||||
## Final Check
|
||||
- Fingerprint chosen before code; at least four high-salience axes differ from the canonical recipe.
|
||||
- Product-native signature artifact is visible and useful.
|
||||
- First viewport has a distinct silhouette and clear primary action.
|
||||
- Radius, palette, type, surface, motif, and motion form one coherent direction.
|
||||
- Mobile becomes a clean single column without horizontal overflow.
|
||||
- Dynamic content, media, icons, labels, and panels have reserved geometry.
|
||||
- Keyboard, focus, contrast, touch targets, and reduced motion work.
|
||||
- Build and the primary interaction pass using the project's existing verification path.
|
||||
|
|
@ -1,66 +0,0 @@
|
|||
---
|
||||
name: gcal
|
||||
description: Use when Alex asks in TG to schedule a call/meeting ("сделай звонок на X с Владой", "запланируй созвон", "поставь встречу") — creates a Google Calendar event with guest invites in a workday slot (Alex's «3 через 3» cycle), or checks the schedule. Powered by tools/gcal.py.
|
||||
metadata:
|
||||
origin: task t-261008155436-a658e9, messages m-516173943/m-516173944
|
||||
requires: secrets/gcal-service-account.json (service account key; until provided — dry-run/planning only)
|
||||
---
|
||||
|
||||
# gcal: команда из TG → событие в календаре Alex
|
||||
|
||||
Одна команда из TG должна решаться одним прогоном этого скилла:
|
||||
интеншн → валидный слот (рабочий день!) → событие с приглашением → ответ в чат.
|
||||
|
||||
## Шаг 1 — разобрать команду
|
||||
|
||||
Из сообщения извлечь:
|
||||
- **что**: тема (по умолчанию «Звонок: <тема/имя гостя>»);
|
||||
- **когда**: день (сегодня/завтра/дата) и время; длительность (по умолчанию 30 мин);
|
||||
- **кого**: email гостей. Известные контакты: Влада — videoprodshema@gmail.com.
|
||||
Email НЕ известен → спросить у Alex ДО создания (угадывать адрес нельзя);
|
||||
- **время**: Alex в Минске (UTC+3). Время из TG трактовать как минское,
|
||||
в инструмент передавать UTC.
|
||||
|
||||
## Шаг 2 — валидация слота (строго до создания)
|
||||
|
||||
```
|
||||
./venv/bin/python tools/gcal.py next-workdays # ближайшие рабочие
|
||||
./venv/bin/python tools/gcal.py create ... --dry-run # что попадёт в Google
|
||||
```
|
||||
|
||||
Guard'ы (встроены в create, не обходить):
|
||||
1. **Только рабочий день** — цикл «3 через 3» (docs/alex-schedule.md).
|
||||
Якорь цикла: env GCAL_WORK_ANCHOR (по умолчанию 2026-10-02). Сдвиг цикла
|
||||
Alex сообщил в TG → обновить docs/alex-schedule.md и якорь.
|
||||
2. **Не позже 2ч до начала** (GCAL_MIN_LEAD_MIN=120) — анти-паттерн рекордера
|
||||
(t-261008155708: встречи, созданные <2 мин до старта, не заходятся).
|
||||
|
||||
Выходной/поздний слот → НЕ создавать, ответить Alex с ближайшими рабочими
|
||||
днями и попросить подтвердить слот.
|
||||
|
||||
## Шаг 3 — создать событие
|
||||
|
||||
```
|
||||
./venv/bin/python tools/gcal.py create \
|
||||
--title "Звонок: Влада" --day YYYY-MM-DD --time HH:MM \
|
||||
--duration 30 --guests "videoprodshema@gmail.com" --timezone Europe/Minsk
|
||||
```
|
||||
|
||||
`sendUpdates=all` — приглашения уходят гостям автоматически.
|
||||
Ответ в чат: ссылка на событие (htmlLink), слот, гости.
|
||||
|
||||
## Ключ (один раз, если не настроен)
|
||||
|
||||
`tools/gcal.py check` печатает готовность и шаги:
|
||||
Google Cloud → Calendar API → service account → JSON-ключ → дать
|
||||
client_email право «Вносить изменения» на календаре Alex → положить в
|
||||
`secrets/gcal-service-account.json` (0600). Ключа нет → работать dry-run'ом
|
||||
и попросить Alex прислать ключ (это может сделать только владелец аккаунта).
|
||||
|
||||
## Критерии качества
|
||||
|
||||
- [ ] Слот — рабочий день цикла; иначе отказ с предложением ближайших рабочих
|
||||
- [ ] До начала ≥2ч (или осознанный --force по явной просьбе Alex)
|
||||
- [ ] Гости получили приглашения (sendUpdates=all), email не угадан
|
||||
- [ ] В ответе Alex — время в его зоне (Минск) и ссылка на событие
|
||||
- [ ] `check` зелёный; ключ лежит в secrets/ с правами 0600
|
||||
|
|
@ -1,111 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""t-261009160906: анти-гонка уровней min/hour на входящих TG (hermetic).
|
||||
|
||||
Кейс 09.10: Alex попросил концепт (m-516174013). Мин-тик ответил и доставил
|
||||
свой файл (16:01). Часовой тик, не видя ответа мин-уровня, сгенерировал СВОЙ
|
||||
файл и доставил вторым (16:06) — Alex получил два разных файла на один запрос.
|
||||
Dedup по artifact_id=file:<путь> не ловит: два разных файла = два разных ключа.
|
||||
|
||||
Фикс: связка «артефакт → входящее» (-m m-NNN у tg-deliver-file, msg= у
|
||||
deliver_artifact, reply_artifact в read model). Если на входящее уже отвечено
|
||||
иным артефактом — SKIP с событием. force обходит (ручная повторная доставка).
|
||||
|
||||
Запуск: ./venv/bin/python tests/test_msg_artifact_race.py
|
||||
"""
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
# ── изоляция: своя БД в /tmp (prod не трогаем) ──
|
||||
_DST = "/tmp/simbiot-test-msg-artifact-race.db"
|
||||
for p in (_DST, _DST + "-wal", _DST + "-shm"):
|
||||
if os.path.exists(p):
|
||||
os.unlink(p)
|
||||
os.environ["SIMBIOT_DB"] = _DST
|
||||
sys.path.insert(0, "/root/simbiot-harness")
|
||||
|
||||
from harness import es, commands
|
||||
|
||||
fails = []
|
||||
|
||||
|
||||
def check(name, cond, extra=""):
|
||||
print(("PASS" if cond else "FAIL"), name, ("| " + str(extra)) if not cond else "")
|
||||
if not cond:
|
||||
fails.append(name)
|
||||
|
||||
|
||||
es.init_db()
|
||||
|
||||
# ── подготовка: входящее m-990001, на которое мин-уровень уже ответил ──
|
||||
es.append_event("inbox", "m-990001", "MessageReceived", {
|
||||
"update_id": 990001, "chat_id": 42, "message_id": 1, "user_id": 1,
|
||||
"text": "сделай концепт в html", "kind": "text", "date": 1791560000,
|
||||
})
|
||||
es.append_event("inbox", "m-990001", "ReplySent", {
|
||||
"to_chat": 42, "text": "готово, отправил файлом", "tg_ok": True, "channel": "tg",
|
||||
})
|
||||
|
||||
# файлы-двойники гонки: мин-уровень и часовой
|
||||
_F1 = "/tmp/simbiot-test-race-min.html"
|
||||
_F2 = "/tmp/simbiot-test-race-hour.html"
|
||||
for f in (_F1, _F2):
|
||||
with open(f, "w") as fh:
|
||||
fh.write("<html><body>r</body></html>")
|
||||
|
||||
sends = []
|
||||
|
||||
# 1) мин-уровень доставил свой файл с привязкой к сообщению
|
||||
ok1 = commands.deliver_artifact(f"file:{_F1}", kind="file", send=lambda: (sends.append(_F1) or True),
|
||||
owner="test:min", caption="мин", msg="m-990001")
|
||||
check("min-level file delivered", ok1 and sends == [_F1], sends)
|
||||
|
||||
model = es.run_projections()
|
||||
m = model["inbox"]["m-990001"]
|
||||
check("read model binds reply_artifact", m.get("reply_artifact") == f"file:{_F1}", m.get("reply_artifact"))
|
||||
|
||||
# 2) часовой уровень пытается доставить СВОЙ файл на то же входящее → SKIP
|
||||
ok2 = commands.deliver_artifact(f"file:{_F2}", kind="file", send=lambda: (sends.append(_F2) or True),
|
||||
owner="test:hour", caption="час", msg="m-990001")
|
||||
check("hour-level duplicate SKIPped", ok2 is False and sends == [_F1], sends)
|
||||
|
||||
db = es._conn()
|
||||
skip = list(db.execute(
|
||||
"SELECT data FROM events WHERE event_type='DeliverySkipped' AND data LIKE '%msg-already%'"))
|
||||
check("DeliverySkipped event with reason", len(skip) == 1, len(skip))
|
||||
|
||||
# 3) force обходит защиту (ручная повторная доставка — явное решение человека)
|
||||
ok3 = commands.deliver_artifact(f"file:{_F2}", kind="file", send=lambda: (sends.append(_F2) or True),
|
||||
owner="test:hour", caption="час", force=True, msg="m-990001")
|
||||
check("force bypasses guard", ok3 and sends == [_F1, _F2], sends)
|
||||
|
||||
# 4) доставки БЕЗ привязки к сообщению не задеваются (обратная совместимость)
|
||||
ok4 = commands.deliver_artifact("file:/tmp/simbiot-test-race-unbound.html", kind="file",
|
||||
send=lambda: (sends.append("unbound") or True),
|
||||
owner="test", caption="x")
|
||||
check("unrelated delivery unaffected", ok4 and sends[-1] == "unbound", sends)
|
||||
|
||||
# 5) второе входящее без артефакта: доставка с привязкой проходит свободно
|
||||
es.append_event("inbox", "m-990002", "MessageReceived", {
|
||||
"update_id": 990002, "chat_id": 42, "message_id": 2, "user_id": 1,
|
||||
"text": "ещё один запрос", "kind": "text", "date": 1791560100,
|
||||
})
|
||||
es.append_event("inbox", "m-990002", "ReplySent", {
|
||||
"to_chat": 42, "text": "ок", "tg_ok": True, "channel": "tg",
|
||||
})
|
||||
ok5 = commands.deliver_artifact("file:/tmp/simbiot-test-race-second.html", kind="file",
|
||||
send=lambda: (sends.append("second") or True),
|
||||
owner="test:hour", caption="y", msg="m-990002")
|
||||
check("fresh msg accepts its artifact", ok5 and sends[-1] == "second", sends)
|
||||
|
||||
# 6) CLI: tg-deliver-file -m m-990001 с чужим артефактом → SKIP, выход 0
|
||||
r = subprocess.run(
|
||||
["./venv/bin/python", "cli.py", "tg-deliver-file", _F2, "-m", "m-990001",
|
||||
"-c", "dup", "-k", "file:" + _F2],
|
||||
capture_output=True, text=True, cwd="/root/simbiot-harness")
|
||||
check("CLI SKIP on raced msg", r.returncode == 0 and "SKIP" in r.stdout,
|
||||
r.stdout.strip()[:120] + r.stderr.strip()[-200:])
|
||||
|
||||
print("=" * 40)
|
||||
print("FAILS:", len(fails), fails if fails else "— все проверки прошли")
|
||||
sys.exit(1 if fails else 0)
|
||||
|
|
@ -1,159 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""t-261004160532: служебный текст inbox не порождает задачу-клон.
|
||||
|
||||
Паттерн само-клонирования чинился дважды:
|
||||
e-261003125707 — 'list' из чата → мин-агент звал `cli.py task list`,
|
||||
создавался клон «list»; фикс: 'list'/'tasks'/'task list' = доска;
|
||||
e-261004134613 — агент звал `cli.py task t-...` для ЧТЕНИЯ задачи,
|
||||
создавался клон с title=ID; фикс: title=ID → показать / exit 2.
|
||||
|
||||
Регресс проверяет весь путь: парсер команд ('list' — команда, а не речь),
|
||||
CLI-гварды (list/ID не создают задач), и инбокс-уровень (служебные тексты
|
||||
распознаются как команды → answered, LLM их не плодит).
|
||||
|
||||
Запуск: ./venv/bin/python tests/test_no_clone_inbox.py | pytest tests/test_no_clone_inbox.py
|
||||
"""
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
# ── изоляция: свежая БД в /tmp, прод не нужен ──
|
||||
_DST = "/tmp/simbiot-test-no-clone.db"
|
||||
for p in (_DST, _DST + "-wal", _DST + "-shm"):
|
||||
if os.path.exists(p):
|
||||
os.unlink(p)
|
||||
sys.path.insert(0, "/root/simbiot-harness")
|
||||
os.environ["SIMBIOT_DB"] = _DST
|
||||
|
||||
from harness import es, tg_commands, tg_inbox
|
||||
|
||||
fails = []
|
||||
_REPO = "/root/simbiot-harness"
|
||||
_PY = os.path.join(_REPO, "venv", "bin", "python")
|
||||
|
||||
|
||||
def check(name, cond, extra=""):
|
||||
print(("PASS" if cond else "FAIL"), name, ("| " + extra) if not cond else "", flush=True)
|
||||
if not cond:
|
||||
fails.append(name)
|
||||
raise AssertionError(f"{name} {extra}")
|
||||
|
||||
|
||||
es.init_db()
|
||||
|
||||
|
||||
def _cli(*args):
|
||||
"""cli.py в изолированной БД (env наследуется)."""
|
||||
r = subprocess.run([_PY, os.path.join(_REPO, "cli.py")] + list(args),
|
||||
capture_output=True, text=True, timeout=60,
|
||||
cwd=_REPO)
|
||||
return r.returncode, (r.stdout or "") + (r.stderr or "")
|
||||
|
||||
|
||||
def _task_count():
|
||||
return len((es.run_projections().get("tasks") or {}))
|
||||
|
||||
|
||||
# ── 1. парсер: служебные слова — команды, а не речь для LLM ──────────────────
|
||||
|
||||
def test_parse_service_words():
|
||||
P = tg_commands.parse
|
||||
check("parse 'list' → команда задачи (e-261001124837)",
|
||||
P("list") == "задачи", f"{P('list')!r}")
|
||||
check("parse 'tasks' → команда", P("tasks") == "задачи")
|
||||
check("parse 'LIST' регистронезависимо", P("LIST") == "задачи")
|
||||
check("parse '/задачи' → команда", P("/задачи") == "задачи")
|
||||
check("parse 'list please' — НЕ команда (речь, но не служебный текст)",
|
||||
P("list please") is None)
|
||||
# ID задачи в тексте — тоже служебный шум, не команда и не «задача»
|
||||
check("parse 't-261004160531-ed25ed' — не команда", P("t-261004160531-ed25ed") is None)
|
||||
|
||||
|
||||
# ── 2. CLI-гварды: list / ID не создают задач ────────────────────────────────
|
||||
|
||||
def test_cli_list_is_board_not_clone():
|
||||
"""`cli.py task list` → доска; число задач не растёт (e-261003125707)."""
|
||||
from harness import commands
|
||||
commands.submit_task("реальная работа", "", minutes=5)
|
||||
before = _task_count()
|
||||
check("задача-незабудка создана", before >= 1)
|
||||
for variant in ("list", "tasks", "task list"):
|
||||
rc, out = _cli("task", variant)
|
||||
check(f"cli task '{variant}': rc=0", rc == 0, f"rc={rc} {out[:120]}")
|
||||
check(f"cli task '{variant}': показывает доску",
|
||||
"реальная работа" in out, out[:150])
|
||||
after = _task_count()
|
||||
check("cli task list/tasks/task list: клонов НЕТ (счёт задач тот же)",
|
||||
after == before, f"было {before}, стало {after}")
|
||||
|
||||
|
||||
def test_cli_task_id_reads_not_clones():
|
||||
"""`cli.py task <id>` → чтение; несуществующий → exit 2 без клона."""
|
||||
from harness import commands
|
||||
real_id, _ = commands.submit_task("целевая задача для чтения", "", minutes=5)
|
||||
before = _task_count()
|
||||
rc, out = _cli("task", real_id)
|
||||
check("cli task <существующий id>: rc=0", rc == 0, f"rc={rc}")
|
||||
check("cli task <id>: показывает задачу", "целевая задача для чтения" in out, out[:200])
|
||||
fake = "t-269999999999-deadbe"
|
||||
rc2, out2 = _cli("task", fake)
|
||||
check("cli task <несуществующий id>: exit 2", rc2 == 2, f"rc={rc2}")
|
||||
check("cli task <несуществующий id>: отказ в создании клона",
|
||||
"не найдена" in out2 and "запрещено" in out2, out2[:200])
|
||||
after = _task_count()
|
||||
check("cli task <id>: клонов НЕТ (e-261004134613)",
|
||||
after == before, f"было {before}, стало {after}")
|
||||
|
||||
|
||||
# ── 3. инбокс-уровень: служебный текст распознан и отвечен без клона ────────
|
||||
|
||||
def test_inbox_service_text_no_task():
|
||||
"""'list' и ID задачи в inbox → ответ командой, задач не прибавилось."""
|
||||
before = _task_count()
|
||||
# в реальном стриме их кладёт поллер; кладём так же, как он
|
||||
tg_inbox.receive_message({"update_id": 991001, "message": {
|
||||
"message_id": 991001 % 100000, "date": 1780000100,
|
||||
"chat": {"id": 42}, "from": {"id": 1, "username": "alex", "first_name": "Alex"},
|
||||
"text": "list"}})
|
||||
tg_inbox.receive_message({"update_id": 991002, "message": {
|
||||
"message_id": 991002 % 100000, "date": 1780000101,
|
||||
"chat": {"id": 42}, "from": {"id": 1, "username": "alex", "first_name": "Alex"},
|
||||
"text": "t-261004160531-ed25ed что там по срокам?"}})
|
||||
model = es.run_projections()
|
||||
inbox = model.get("inbox") or {}
|
||||
check("инбокс: оба служебных текста приняты",
|
||||
"m-991001" in inbox and "m-991002" in inbox)
|
||||
check("инбокс: 'list' распознан как команда ('задачи')",
|
||||
tg_commands.parse(inbox["m-991001"]["text"]) == "задачи")
|
||||
check("инбокс: ID в фразе — не команда (речь, уходит LLM)",
|
||||
tg_commands.parse(inbox["m-991002"]["text"]) is None)
|
||||
# minute-level фильтр: команды не показываются мин-LLM как «сырые» входящие
|
||||
from harness.scheduler import build_context, LEVELS
|
||||
fake = {"inbox": dict(inbox), "tasks": dict(model.get("tasks") or {}),
|
||||
"escalations": {}, "policy": {}}
|
||||
ctx = build_context(LEVELS["min"], fake)
|
||||
check("build_context(min): 'list' не показан как открытое входящее",
|
||||
"m-991001" not in ctx or "list" not in _line(ctx, "m-991001"))
|
||||
check("после обработки служебных текстов задач-клонов НЕТ",
|
||||
_task_count() == before, f"было {before}, стало {_task_count()}")
|
||||
|
||||
|
||||
def _line(ctx, marker):
|
||||
for ln in ctx.splitlines():
|
||||
if marker in ln:
|
||||
return ln
|
||||
return ""
|
||||
|
||||
|
||||
# ────────────────────────────── plain-run режим ──────────────────────────────
|
||||
|
||||
if __name__ == "__main__":
|
||||
tests = [v for k, v in sorted(globals().items()) if k.startswith("test_") and callable(v)]
|
||||
for t in tests:
|
||||
try:
|
||||
t()
|
||||
except AssertionError:
|
||||
pass
|
||||
print(f"\n{'='*60}")
|
||||
print(f"{'ALL OK' if not fails else 'FAILED: ' + ', '.join(fails)} ({len(tests)} тестов)")
|
||||
sys.exit(1 if fails else 0)
|
||||
|
|
@ -1,175 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""t-261010093054-f67dac: MCP портала кооператива как инструмент харнеса, hermetic.
|
||||
|
||||
Запуск: ./venv/bin/python tests/test_portal_mcp.py
|
||||
|
||||
Сеть НЕ трогается: harness.portal_mcp._rpc подменяется фейком, шлюз
|
||||
отвечает заготовками. Проверяет:
|
||||
- call_tool: happy-path — text из content[] портала;
|
||||
- guardrail: product_create/product_set_mode запрещены (деньги/Stripe),
|
||||
SIMBIOT_MCP_ALLOW_ALL=1 снимает запрет;
|
||||
- неизвестный тул → error: со списком доступных (из tools/list);
|
||||
- arguments: dict как есть, JSON-строка парсится, мусор — error:;
|
||||
- нет токена → error:, не рэйз;
|
||||
- обрезка длинного результата до MAX_RESULT_CHARS;
|
||||
- HTTP/JSON-сбой шлюза → error:, не рэйз (тик не валится);
|
||||
- трейс PortalMcpCalled в event store (level/trace_id, tool, ok);
|
||||
- тул тика _portal_mcp_tool_for: лимит 4 вызова, дальше отказ текстом;
|
||||
- paths.portal_mcp_token_path: env → secrets/ инстанса → legacy.
|
||||
"""
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import threading # noqa: F401 (паритет импортов с другими тестами)
|
||||
|
||||
_DST = "/tmp/simbiot-test-portal-mcp.db"
|
||||
for p in (_DST, _DST + "-wal", _DST + "-shm"):
|
||||
if os.path.exists(p):
|
||||
os.unlink(p)
|
||||
os.environ["SIMBIOT_DB"] = _DST
|
||||
# токен для основного потока проверок (env сильнее файла)
|
||||
os.environ["SIMBIOT_PORTAL_MCP_TOKEN"] = "agt_test_token"
|
||||
|
||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
from harness import es, portal_mcp
|
||||
from harness import paths as _paths
|
||||
|
||||
fails = []
|
||||
|
||||
|
||||
def check(name, cond, extra=""):
|
||||
print(("PASS" if cond else "FAIL"), name, ("| " + extra) if not cond else "")
|
||||
if not cond:
|
||||
fails.append(name)
|
||||
|
||||
|
||||
es.init_db()
|
||||
|
||||
# ── фейк шлюза: tools/list + tools/call с заготовками ────────────────────────
|
||||
SPEC = {"tools": [{"name": n} for n in
|
||||
("whoami", "booking_slots", "booking_create", "booking_cancel",
|
||||
"list_calls", "product_create", "product_set_mode")]}
|
||||
rpc_calls = []
|
||||
|
||||
|
||||
def _fake_rpc(method, params, timeout=15):
|
||||
rpc_calls.append((method, params))
|
||||
if method == "tools/list":
|
||||
return SPEC
|
||||
if method == "tools/call":
|
||||
name = params.get("name")
|
||||
if name == "whoami":
|
||||
return {"content": [{"type": "text",
|
||||
"text": json.dumps({"agent": "Simbiot"})}]}
|
||||
if name == "booking_slots":
|
||||
return {"content": [{"type": "text",
|
||||
"text": json.dumps({"ok": True, "slots": [{"utc": "x"}]})}]}
|
||||
raise AssertionError(f"неожиданный тул в тесте: {name}")
|
||||
raise AssertionError(f"неожиданный метод: {method}")
|
||||
|
||||
|
||||
portal_mcp._rpc = _fake_rpc
|
||||
portal_mcp._tools_cache = None
|
||||
|
||||
# ── 1. happy-path: text из content[] ─────────────────────────────────────────
|
||||
r = portal_mcp.call_tool("whoami", None, level="min", trace_id="t-test-mcp")
|
||||
check("call_tool: text из content[]", json.loads(r)["agent"] == "Simbiot", r[:120])
|
||||
check("call_tool: arguments None → {}", rpc_calls[-1][1]["arguments"] == {})
|
||||
|
||||
# ── 2. трейс PortalMcpCalled в сторе ────────────────────────────────────────
|
||||
model = es.run_projections()
|
||||
tevs = [e for e in model.get("traces", {}).values()
|
||||
if isinstance(e, dict) and e.get("tool") == "whoami"] if "traces" in model else []
|
||||
check("trace: PortalMcpCalled записан (best effort)", True) # наличие — не контракт тика
|
||||
# прямой контракт: событие в сторе по aggregate_id
|
||||
raw = [a for a in es.list_events() if a[2] == "PortalMcpCalled"] if hasattr(es, "list_events") else []
|
||||
if raw:
|
||||
check("trace: событие в сторе с level", any("min" in str(a[3]) for a in raw), str(raw[:1]))
|
||||
else:
|
||||
# API стора без list_events — проверяем через проекцию не падая
|
||||
check("trace: событие в сторе с level", True)
|
||||
|
||||
# ── 3. arguments: dict / JSON-строка / мусор ─────────────────────────────────
|
||||
r = portal_mcp.call_tool("booking_slots", {"type": "call"})
|
||||
check("arguments: dict как есть", json.loads(r)["ok"] is True)
|
||||
r = portal_mcp.call_tool("booking_slots", '{"type": "call"}')
|
||||
check("arguments: JSON-строка парсится", json.loads(r)["ok"] is True)
|
||||
r = portal_mcp.call_tool("booking_slots", "не json")
|
||||
check("arguments: мусор → error:", r.startswith("error:"), r[:100])
|
||||
|
||||
# ── 4. guardrail: деньги-тулы запрещены по умолчанию ─────────────────────────
|
||||
for name in ("product_create", "product_set_mode"):
|
||||
r = portal_mcp.call_tool(name, {})
|
||||
check(f"guardrail: {name} запрещён", r.startswith("error:") and "запрещён" in r, r[:120])
|
||||
os.environ["SIMBIOT_MCP_ALLOW_ALL"] = "1"
|
||||
portal_mcp._tools_cache = None # сбросить кеш имён (фейк знает тул)
|
||||
try:
|
||||
# фейк не умеет product_create — подменим ответ, проверяем только проход guardrail
|
||||
portal_mcp._rpc = lambda m, p, timeout=15: (SPEC if m == "tools/list" else
|
||||
{"content": [{"text": '{"ok": true}'}]})
|
||||
r = portal_mcp.call_tool("product_create", {})
|
||||
check("guardrail: ALLOW_ALL пропускает", not r.startswith("error:"), r[:120])
|
||||
finally:
|
||||
del os.environ["SIMBIOT_MCP_ALLOW_ALL"]
|
||||
portal_mcp._rpc = _fake_rpc
|
||||
portal_mcp._tools_cache = None
|
||||
|
||||
# ── 5. неизвестный тул → error со списком ───────────────────────────────────
|
||||
r = portal_mcp.call_tool("nope", {})
|
||||
check("неизвестный тул → error со списком доступных",
|
||||
r.startswith("error:") and "whoami" in r, r[:150])
|
||||
|
||||
# ── 6. сбой шлюза → error:, не рэйз ──────────────────────────────────────────
|
||||
def _boom(method, params, timeout=15):
|
||||
raise portal_mcp.McpError("HTTP 500 от шлюза")
|
||||
portal_mcp._rpc = _boom
|
||||
portal_mcp._tools_cache = None
|
||||
r = portal_mcp.call_tool("whoami", {})
|
||||
check("шлюз упал → error: (тик не валится)", r.startswith("error: HTTP 500"), r[:120])
|
||||
|
||||
# ── 7. нет токена → error:, не рэйз ─────────────────────────────────────────
|
||||
os.environ.pop("SIMBIOT_PORTAL_MCP_TOKEN", None)
|
||||
saved = portal_mcp._load_token
|
||||
portal_mcp._load_token = lambda: "" # и файла нет
|
||||
portal_mcp._tools_cache = None
|
||||
try:
|
||||
r = portal_mcp.call_tool("whoami", {})
|
||||
check("нет токена → error: (тул не поднимается)",
|
||||
r.startswith("error:") and "токен" in r, r[:150])
|
||||
finally:
|
||||
portal_mcp._load_token = saved
|
||||
os.environ["SIMBIOT_PORTAL_MCP_TOKEN"] = "agt_test_token"
|
||||
portal_mcp._rpc = _fake_rpc
|
||||
|
||||
# ── 8. обрезка длинного результата ───────────────────────────────────────────
|
||||
portal_mcp._rpc = lambda m, p, timeout=15: (SPEC if m == "tools/list" else
|
||||
{"content": [{"text": "x" * 20000}]})
|
||||
portal_mcp._tools_cache = None
|
||||
r = portal_mcp.call_tool("whoami", {})
|
||||
check("обрезка результата",
|
||||
len(r) <= portal_mcp.MAX_RESULT_CHARS + 100 and "обрезано" in r,
|
||||
f"len={len(r)}")
|
||||
portal_mcp._rpc = _fake_rpc
|
||||
portal_mcp._tools_cache = None
|
||||
|
||||
# ── 9. тул тика: лимит 4 вызова ──────────────────────────────────────────────
|
||||
from harness import _llm_worker
|
||||
tool = _llm_worker._portal_mcp_tool_for("hour", "t-test-mcp-tool")
|
||||
outs = [tool(tool_name, "{}") for tool_name in
|
||||
("whoami", "whoami", "whoami", "whoami", "whoami")]
|
||||
check("тул тика: первые 4 прошли", all(not o.startswith("error:") for o in outs[:4]),
|
||||
str([o[:40] for o in outs[:4]]))
|
||||
check("тул тика: 5-й — лимит", outs[4].startswith("лимит"), outs[4][:100])
|
||||
|
||||
# ── 10. путь токена: env сильнее схемы, изолят → secrets/ ────────────────────
|
||||
os.environ["SIMBIOT_PORTAL_MCP_TOKEN_FILE"] = "/tmp/my-token"
|
||||
check("paths: env TOKEN_FILE бьёт схему",
|
||||
_paths.portal_mcp_token_path() == "/tmp/my-token")
|
||||
del os.environ["SIMBIOT_PORTAL_MCP_TOKEN_FILE"]
|
||||
check("paths: изолят → secrets/portal-mcp-token",
|
||||
_paths.portal_mcp_token_path().endswith("secrets/portal-mcp-token"),
|
||||
_paths.portal_mcp_token_path())
|
||||
|
||||
print()
|
||||
print("FAILURES:", fails if fails else "нет — все проверки пройдены")
|
||||
sys.exit(1 if fails else 0)
|
||||
|
|
@ -9,9 +9,8 @@
|
|||
(нет ни реакции, ни второго события), TG-сбой — False без события;
|
||||
- receive_message сохраняет message_id (нативный, не хэш-фолбэк);
|
||||
- typing_keepalive: держит sendChatAction typing и останавливается по Event;
|
||||
- run_level_tick: поднимает keepalive по TG-чатам инбокса ВЕСЬ тик, без
|
||||
привязки к pending/seen (t-261010092931), исключает портал-чаты, при
|
||||
пустом инбоксе — фолбэк на чат харнеса; гасит поток в finally (любой исход).
|
||||
- run_level_tick: поднимает keepalive по TG-чатам неотвеченного инбокса,
|
||||
исключает портал-чаты, гасит поток в finally (на любом исходе возврата).
|
||||
"""
|
||||
import json
|
||||
import os
|
||||
|
|
@ -174,9 +173,7 @@ try:
|
|||
check("tick: worker упал → finally всё равно гасит keepalive",
|
||||
ka_seen.get("stop") is not None and ka_seen.get("stop").is_set())
|
||||
|
||||
# t-261010092931: всё seen → «печатает…» всё равно горит весь тик
|
||||
# (индикатор больше не зависит от pending: Alex видит «печатает…»
|
||||
# всё время, пока LLM-тик работает)
|
||||
# все сообщения уже seen → keepalive не поднимается вовсе
|
||||
ka_seen.clear()
|
||||
model2 = dict(model)
|
||||
model2["inbox"] = {
|
||||
|
|
@ -185,50 +182,7 @@ try:
|
|||
}
|
||||
_ir_module.run_llm_tick = staticmethod(_FakeRunner.run_llm_tick)
|
||||
run_level_tick(LEVELS["hour"], 4244, model2)
|
||||
check("tick: всё seen → keepalive всё равно поднят по TG-чату",
|
||||
ka_seen.get("ids") == [42], str(ka_seen))
|
||||
check("tick: всё seen → finally погасил keepalive",
|
||||
ka_seen.get("stop") is not None and ka_seen.get("stop").is_set())
|
||||
|
||||
# t-261010092931: пустой инбокс → фолбэк на последний живой TG-чат
|
||||
# (_default_chat_id, личка Alex): индикатор горит и без входящих
|
||||
ka_seen.clear()
|
||||
model3 = dict(model)
|
||||
model3["inbox"] = {}
|
||||
_orig_default_chat = tg_inbox._default_chat_id
|
||||
tg_inbox._default_chat_id = lambda: 42
|
||||
try:
|
||||
_ir_module.run_llm_tick = staticmethod(_FakeRunner.run_llm_tick)
|
||||
run_level_tick(LEVELS["hour"], 4245, model3)
|
||||
finally:
|
||||
tg_inbox._default_chat_id = _orig_default_chat
|
||||
check("tick: пустой инбокс → keepalive по чату харнеса (фолбэк)",
|
||||
ka_seen.get("ids") == [42], str(ka_seen))
|
||||
check("tick: пустой инбокс → finally погасил keepalive",
|
||||
ka_seen.get("stop") is not None and ka_seen.get("stop").is_set())
|
||||
|
||||
# инбокс без TG-чатов (только портал/tui) → тоже фолбэк на чат харнеса
|
||||
ka_seen.clear()
|
||||
model4 = dict(model)
|
||||
model4["inbox"] = {"m-2": model["inbox"]["m-2"]}
|
||||
tg_inbox._default_chat_id = lambda: 42
|
||||
try:
|
||||
_ir_module.run_llm_tick = staticmethod(_FakeRunner.run_llm_tick)
|
||||
run_level_tick(LEVELS["hour"], 4246, model4)
|
||||
finally:
|
||||
tg_inbox._default_chat_id = _orig_default_chat
|
||||
check("tick: только портал-чаты → keepalive по фолбэку, портал исключён",
|
||||
ka_seen.get("ids") == [42], str(ka_seen))
|
||||
|
||||
# фолбэк недоступен (нет известного чата) → keepalive не поднимается
|
||||
ka_seen.clear()
|
||||
tg_inbox._default_chat_id = lambda: None
|
||||
try:
|
||||
_ir_module.run_llm_tick = staticmethod(_FakeRunner.run_llm_tick)
|
||||
run_level_tick(LEVELS["hour"], 4247, model3)
|
||||
finally:
|
||||
tg_inbox._default_chat_id = _orig_default_chat
|
||||
check("tick: чат неизвестен → keepalive не поднимается", ka_seen == {}, str(ka_seen))
|
||||
check("tick: всё seen → keepalive не поднимается", ka_seen == {}, str(ka_seen))
|
||||
finally:
|
||||
scheduler.es = _orig_es
|
||||
tg_inbox.typing_keepalive = _orig_tg_typing_backup
|
||||
|
|
|
|||
|
|
@ -1,174 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""t-261005135853: входящие documents/photo → на диск, путь в событии.
|
||||
|
||||
Покрытие:
|
||||
1. unit: _file_ext — имя/mime/мусор/пусто;
|
||||
2. e2e: MessageReceived(document) → getFile/download (моки) → файл в
|
||||
inbox/files/<unique>.pdf, file_path (абсолютный) + file_name + size
|
||||
в проекции inbox;
|
||||
3. e2e: photo (3 размера) → .jpg самого большого размера;
|
||||
4. идемпотентность: повторный апдейт не перекачивает (счётчик getFile);
|
||||
5. сбой getFile (ok=False / HTTP 400) → file_path=None, входящее живо
|
||||
(file_id остаётся для ручного достёгивания).
|
||||
|
||||
Запуск: ./venv/bin/python tests/test_tg_documents.py | pytest tests/test_tg_documents.py
|
||||
Telegram-сеть мокнута (нет реальных file_id), стор — свой в /tmp.
|
||||
"""
|
||||
import io
|
||||
import os
|
||||
import sys
|
||||
import urllib.error
|
||||
|
||||
# ── изоляция: свой стор и свой каталог файлов, прод не трогаем ──
|
||||
_DST = "/tmp/simbiot-test-tg-docs.db"
|
||||
_FILES = "/tmp/simbiot-test-tg-docs-files"
|
||||
for p in (_DST, _DST + "-wal", _DST + "-shm"):
|
||||
if os.path.exists(p):
|
||||
os.unlink(p)
|
||||
os.environ["SIMBIOT_DB"] = _DST
|
||||
os.environ["SIMBIOT_INBOX_FILES_DIR"] = _FILES
|
||||
sys.path.insert(0, "/root/simbiot-harness")
|
||||
|
||||
from harness import es, tg_inbox
|
||||
|
||||
fails = []
|
||||
_calls = {"getFile": 0}
|
||||
|
||||
|
||||
def check(name, cond, extra=""):
|
||||
print(("PASS" if cond else "FAIL"), name, (f"| {extra}" if not cond else ""), flush=True)
|
||||
if not cond:
|
||||
fails.append(name)
|
||||
|
||||
|
||||
# ── моки Telegram: getFile → file_path, urlopen → байты файла ──
|
||||
class _Resp(io.BytesIO):
|
||||
def __enter__(self):
|
||||
return self
|
||||
|
||||
def __exit__(self, *a):
|
||||
return False
|
||||
|
||||
|
||||
def _fake_api(method, params, timeout=35):
|
||||
if method == "getFile":
|
||||
_calls["getFile"] += 1
|
||||
return {"ok": True, "result": {
|
||||
"file_id": params["file_id"],
|
||||
"file_path": f"documents/{params['file_id']}.pdf"}}
|
||||
return {"ok": True, "result": {}}
|
||||
|
||||
|
||||
def _fake_urlopen(url, timeout=60):
|
||||
if "getFile-fail" in url or "expired" in url:
|
||||
raise urllib.error.HTTPError(url, 400, "Bad Request", {}, io.BytesIO(b""))
|
||||
return _Resp(b"%PDF-1.4 fake-test-blob")
|
||||
|
||||
|
||||
tg_inbox._api = _fake_api
|
||||
tg_inbox._load_env = lambda: setattr(tg_inbox, "TG_TOKEN", "TEST:TOKEN") # токен мока
|
||||
_real_urlopen = urllib.request.urlopen
|
||||
urllib.request.urlopen = lambda url, timeout=60: _fake_urlopen(url, timeout)
|
||||
|
||||
es.init_db()
|
||||
|
||||
|
||||
# ────────────────────────────── 1. _file_ext ────────────────────────────────
|
||||
|
||||
def test_file_ext():
|
||||
check("ext из имени", tg_inbox._file_ext("Alfa-Bank_выписка.PDF") == "PDF"
|
||||
or tg_inbox._file_ext("Alfa-Bank_выписка.PDF").lower() == "pdf",
|
||||
tg_inbox._file_ext("Alfa-Bank_выписка.PDF"))
|
||||
check("ext из mime", tg_inbox._file_ext("", "image/jpeg") == "jpg",
|
||||
tg_inbox._file_ext("", "image/jpeg"))
|
||||
check("мусор вырезан", tg_inbox._file_ext("a.tar.gz/../../x", "") in ("gz", "x", "bin"),
|
||||
tg_inbox._file_ext("a.tar.gz/../../x"))
|
||||
check("пусто → bin", tg_inbox._file_ext("", "") == "bin")
|
||||
|
||||
|
||||
# ─────────────────────── 2. document → файл + путь в событии ────────────────
|
||||
|
||||
def test_document_e2e():
|
||||
upd = {"update_id": 990001, "message": {
|
||||
"message_id": 111, "date": 1791200000,
|
||||
"from": {"id": 1, "first_name": "A"},
|
||||
"chat": {"id": 1},
|
||||
"document": {"file_id": "file-AAA", "file_unique_id": "AgADdoc1",
|
||||
"file_name": "Alfa_Statement.pdf",
|
||||
"mime_type": "application/pdf", "file_size": 123456},
|
||||
"caption": "выписка октябрь",
|
||||
}}
|
||||
mid = tg_inbox.receive_message(upd)
|
||||
p = f"{_FILES}/AgADdoc1.pdf"
|
||||
check("файл в inbox/files", os.path.exists(p), p)
|
||||
check("содержимое скачано", os.path.exists(p) and open(p, "rb").read() == b"%PDF-1.4 fake-test-blob")
|
||||
m = es.run_projections()["inbox"][mid]
|
||||
check("file_path в событии (абс)", m.get("file_path") == p, m.get("file_path"))
|
||||
check("путь открывается напрямую", os.path.exists(m.get("file_path") or ""))
|
||||
check("file_name в событии", m.get("file_name") == "Alfa_Statement.pdf")
|
||||
check("file_size в событии", m.get("file_size") == 123456)
|
||||
check("kind=document", m.get("kind") == "document")
|
||||
check("caption в text", m.get("text") == "выписка октябрь")
|
||||
return upd
|
||||
|
||||
|
||||
# ───────────────────────────── 3. photo → .jpg ───────────────────────────────
|
||||
|
||||
def test_photo_e2e():
|
||||
upd = {"update_id": 990002, "message": {
|
||||
"message_id": 112, "date": 1791200001,
|
||||
"from": {"id": 1, "first_name": "A"},
|
||||
"chat": {"id": 1},
|
||||
"photo": [
|
||||
{"file_id": "ph-sm", "file_unique_id": "UPsm", "file_size": 100, "width": 90, "height": 90},
|
||||
{"file_id": "ph-md", "file_unique_id": "UPmd", "file_size": 900, "width": 320, "height": 320},
|
||||
{"file_id": "ph-lg", "file_unique_id": "UPlg", "file_size": 9900, "width": 1280, "height": 1280},
|
||||
],
|
||||
}}
|
||||
mid = tg_inbox.receive_message(upd)
|
||||
p = f"{_FILES}/UPlg.jpg"
|
||||
check("фото: самый большой размер сохранён", os.path.exists(p), p)
|
||||
m = es.run_projections()["inbox"][mid]
|
||||
check("photo file_path", m.get("file_path") == p, m.get("file_path"))
|
||||
check("photo kind", m.get("kind") == "photo")
|
||||
check("photo file_size", m.get("file_size") == 9900)
|
||||
return upd
|
||||
|
||||
|
||||
# ─────────────────── 4. идемпотентность: повтор не качает ────────────────────
|
||||
|
||||
def test_no_redownload(upd):
|
||||
before = _calls["getFile"]
|
||||
mtime = os.path.getmtime(f"{_FILES}/AgADdoc1.pdf")
|
||||
tg_inbox.receive_message(upd) # тот же update_id → тот же файл
|
||||
check("getFile не рос при повторе", _calls["getFile"] == before,
|
||||
f"{before} → {_calls['getFile']}")
|
||||
check("файл не переписан", os.path.getmtime(f"{_FILES}/AgADdoc1.pdf") == mtime)
|
||||
|
||||
|
||||
# ──────────────────── 5. сбой скачивания не валит приём ─────────────────────
|
||||
|
||||
def test_download_failed():
|
||||
upd = {"update_id": 990003, "message": {
|
||||
"message_id": 113, "date": 1791200002,
|
||||
"from": {"id": 1, "first_name": "A"},
|
||||
"chat": {"id": 1},
|
||||
"document": {"file_id": "getFile-fail-expired", "file_unique_id": "ADdead",
|
||||
"file_name": "old.pdf", "mime_type": "application/pdf"},
|
||||
}}
|
||||
mid = tg_inbox.receive_message(upd)
|
||||
m = es.run_projections()["inbox"][mid]
|
||||
check("file_path=None при сбое", m.get("file_path") is None, m.get("file_path"))
|
||||
check("file_id сохранился (ручной путь)", m.get("file_id") == "getFile-fail-expired")
|
||||
check("входящее живо (не упало)", bool(mid))
|
||||
check("мусора .part не осталось", not any(f.endswith(".part") for f in os.listdir(_FILES)))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
test_file_ext()
|
||||
upd = test_document_e2e()
|
||||
test_photo_e2e()
|
||||
test_no_redownload(upd)
|
||||
test_download_failed()
|
||||
print(f"\n{'ALL PASS' if not fails else 'FAILURES: ' + ', '.join(fails)}")
|
||||
sys.exit(1 if fails else 0)
|
||||
|
|
@ -1,229 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""t-261004160531: видео→текст в voice-пайплайне (регресс t-261004152714).
|
||||
|
||||
Покрытие:
|
||||
1. unit: extract_audio_track — реальный ffmpeg на живых фикстурах
|
||||
(mp4 с дорожкой → ogg/opus байты; mp4 без дорожки и мусор → None);
|
||||
2. unit: роутинг kind=video ≤60с → сразу faster-whisper turbo
|
||||
(webservice-ярус не зовётся, движок = large-v3-turbo);
|
||||
3. unit: длинное видео (>60с) → обычная лестница (webservice base первым);
|
||||
4. unit: контейнер без дорожки → MessageTranscribed(audio_track_failed);
|
||||
5. e2e на фикстуре: MessageReceived(kind=video) → download (мок) →
|
||||
ffmpeg-извлечение → ASR (мок) → транскрипт в проекции inbox;
|
||||
идемпотентность повтора (второй вызов — из read model, без хождений).
|
||||
|
||||
Запуск: ./venv/bin/python tests/test_voice_video.py | pytest tests/test_voice_video.py
|
||||
ASR-слои мокнуты (нет сети/моделей), ffmpeg — настоящий (он на хосте).
|
||||
"""
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
# ── изоляция: своя БД в /tmp, прод-стор не трогаем ──
|
||||
_DST = "/tmp/simbiot-test-voice-video.db"
|
||||
for p in (_DST, _DST + "-wal", _DST + "-shm"):
|
||||
if os.path.exists(p):
|
||||
os.unlink(p)
|
||||
os.environ["SIMBIOT_DB"] = _DST
|
||||
sys.path.insert(0, "/root/simbiot-harness")
|
||||
|
||||
from harness import es, tg_inbox
|
||||
import harness.voice as voice
|
||||
|
||||
fails = []
|
||||
|
||||
|
||||
def check(name, cond, extra=""):
|
||||
print(("PASS" if cond else "FAIL"), name, ("| " + extra) if not cond else "", flush=True)
|
||||
if not cond:
|
||||
fails.append(name)
|
||||
raise AssertionError(f"{name} {extra}")
|
||||
|
||||
|
||||
# ── фикстуры: генерируем ffmpeg'ом при запуске (детерминировано, без бинарников в репо) ──
|
||||
_TMP = "/tmp/simbiot-test-voice-video"
|
||||
os.makedirs(_TMP, exist_ok=True)
|
||||
WITH_AUDIO = os.path.join(_TMP, "with_audio.mp4") # testsrc + sine 440Hz
|
||||
NO_AUDIO = os.path.join(_TMP, "no_audio.mp4") # только видео
|
||||
|
||||
|
||||
def _ff(args):
|
||||
r = subprocess.run(["ffmpeg", "-y", "-v", "error"] + args,
|
||||
capture_output=True, timeout=120)
|
||||
if r.returncode != 0:
|
||||
raise RuntimeError((r.stderr or b"").decode()[:300])
|
||||
|
||||
|
||||
_ff(["-f", "lavfi", "-i", "testsrc=duration=2:size=128x96:rate=10",
|
||||
"-f", "lavfi", "-i", "sine=frequency=440:duration=2",
|
||||
"-c:v", "libx264", "-preset", "ultrafast", "-c:a", "aac",
|
||||
"-shortest", WITH_AUDIO])
|
||||
_ff(["-f", "lavfi", "-i", "testsrc=duration=2:size=128x96:rate=10",
|
||||
"-c:v", "libx264", "-preset", "ultrafast", "-an", NO_AUDIO])
|
||||
|
||||
with open(WITH_AUDIO, "rb") as f:
|
||||
VIDEO_WITH_AUDIO = f.read()
|
||||
with open(NO_AUDIO, "rb") as f:
|
||||
VIDEO_NO_AUDIO = f.read()
|
||||
|
||||
es.init_db()
|
||||
|
||||
|
||||
# ────────────────────────── 1. extract_audio_track ──────────────────────────
|
||||
|
||||
def test_extract_audio_track_from_container():
|
||||
"""mp4 с дорожкой → ogg/opus байты (заголовок OggS + OpusHead, звук есть)."""
|
||||
track = voice.extract_audio_track(VIDEO_WITH_AUDIO)
|
||||
check("контейнер с дорожкой → байты", bool(track), "None/пусто")
|
||||
assert track[:4] == b"OggS", f"не ogg: {track[:8]!r}"
|
||||
assert b"OpusHead" in track[:64], "не opus-кодек"
|
||||
check("ogg/opus формат дорожки", True)
|
||||
|
||||
|
||||
def test_extract_audio_track_no_track():
|
||||
"""mp4 без аудио → None (ffmpeg ничего не вынул)."""
|
||||
r = voice.extract_audio_track(VIDEO_NO_AUDIO)
|
||||
check("контейнер без дорожки → None", r is None, f"получили {type(r)}")
|
||||
r = voice.extract_audio_track(b"not a video at all")
|
||||
check("мусор на входе → None (без исключения)", r is None, f"получили {type(r)}")
|
||||
|
||||
|
||||
# ─────────────────────── 2-4. роутинг kind=video ────────────────────────────
|
||||
|
||||
def _video_msg(mid, duration):
|
||||
"""MessageReceived c video-вложением в Telegram-формате."""
|
||||
return {"update_id": int(mid.split("-")[1]), "message": {
|
||||
"message_id": int(mid.split("-")[1]) % 100000, "date": 1780000000,
|
||||
"chat": {"id": 42}, "from": {"id": 1, "username": "alex", "first_name": "Alex"},
|
||||
"video": {"file_id": f"fid-{mid}", "file_unique_id": f"fuid-{mid}",
|
||||
"duration": duration}}}
|
||||
|
||||
|
||||
class _Rec:
|
||||
"""Записываем вызовы ASR-слоёв и подменяем их ответами."""
|
||||
|
||||
def __init__(self, fw_text=None, ws_text=None):
|
||||
self.calls = {"fw": [], "ws": [], "dl": []}
|
||||
self._orig = {}
|
||||
self._patch("_download_voice",
|
||||
lambda fid, fuid="": (self.calls["dl"].append(fid) or VIDEO_WITH_AUDIO, None))
|
||||
self._patch("_asr_faster_whisper",
|
||||
lambda audio, fn: (self.calls["fw"].append(audio) or fw_text))
|
||||
self._patch("_asr_webservice",
|
||||
lambda audio, fn: (self.calls["ws"].append(audio) or ws_text))
|
||||
|
||||
def _patch(self, name, fn):
|
||||
self._orig[name] = getattr(voice, name)
|
||||
setattr(voice, name, fn)
|
||||
|
||||
def restore(self):
|
||||
for k, v in self._orig.items():
|
||||
setattr(voice, k, v)
|
||||
self._orig = {}
|
||||
|
||||
|
||||
def test_short_video_routes_to_turbo():
|
||||
"""kind=video ≤60с: сразу faster-whisper turbo, webservice не зовётся."""
|
||||
tg_inbox.receive_message(_video_msg("m-910001", 5))
|
||||
rec = _Rec(fw_text="привет из видео")
|
||||
# детерминированный маркер: сам ogg-заголовок между запусками ffmpeg
|
||||
# не байт-стабилен — проверяем передачу ровно того, что вернул extract
|
||||
marker = b"OggS-EXTRACTED-TRACK"
|
||||
orig_extract = voice.extract_audio_track
|
||||
voice.extract_audio_track = lambda c: (rec.calls.setdefault("ex", []).append(1) or marker)
|
||||
try:
|
||||
text, engine = voice.transcribe_message(
|
||||
"m-910001", "fid-m-910001", "fuid-m-910001", duration=5, kind="video")
|
||||
finally:
|
||||
voice.extract_audio_track = orig_extract
|
||||
rec.restore()
|
||||
check("транскрипт получен", text == "привет из видео", f"text={text!r}")
|
||||
check("движок = large-v3-turbo", engine == "faster-whisper-large-v3-turbo",
|
||||
f"engine={engine!r}")
|
||||
check("turbo-ярус вызван ровно 1 раз", len(rec.calls["fw"]) == 1,
|
||||
f"{len(rec.calls['fw'])}")
|
||||
check("webservice-ярус пропущен", len(rec.calls["ws"]) == 0,
|
||||
f"{len(rec.calls['ws'])}")
|
||||
check("в ASR ушла ИЗВЛЕЧЁННАЯ дорожка (не контейнер)",
|
||||
rec.calls["fw"] and rec.calls["fw"][0] == marker, "байты != extract()")
|
||||
check("extract вызван для видео", len(rec.calls.get("ex", [])) == 1)
|
||||
m = es.run_projections()["inbox"]["m-910001"]
|
||||
check("проекция: transcript в read model", m["transcript"] == "привет из видео")
|
||||
check("проекция: text = транскрипт", m["text"] == "привет из видео")
|
||||
check("проекция: без transcribe_reason", not m.get("transcribe_reason"))
|
||||
|
||||
|
||||
def test_long_video_uses_tier_ladder():
|
||||
"""kind=video >60с: сначала webservice base (обычная лестница)."""
|
||||
tg_inbox.receive_message(_video_msg("m-910002", 120))
|
||||
rec = _Rec(ws_text="длинное видео base")
|
||||
try:
|
||||
text, engine = voice.transcribe_message(
|
||||
"m-910002", "fid-m-910002", "fuid-m-910002", duration=120, kind="video")
|
||||
finally:
|
||||
rec.restore()
|
||||
check("транскрипт из base-яруса", text == "длинное видео base", f"text={text!r}")
|
||||
check("движок = whisper-asr-base", engine == "whisper-asr-base", f"engine={engine!r}")
|
||||
check("webservice вызван", len(rec.calls["ws"]) == 1, f"{len(rec.calls['ws'])}")
|
||||
check("turbo не понадобился", len(rec.calls["fw"]) == 0, f"{len(rec.calls['fw'])}")
|
||||
|
||||
|
||||
def test_video_without_track_marks_failure():
|
||||
"""Дорожки нет → MessageTranscribed(reason=audio_track_failed), движок None."""
|
||||
tg_inbox.receive_message(_video_msg("m-910003", 7))
|
||||
rec = _Rec(fw_text="не должен вызываться")
|
||||
rec._patch("_download_voice",
|
||||
lambda fid, fuid="": (self_dl := rec.calls["dl"].append(fid) or VIDEO_NO_AUDIO, None))
|
||||
try:
|
||||
text, engine = voice.transcribe_message(
|
||||
"m-910003", "fid-m-910003", "fuid-m-910003", duration=7, kind="video")
|
||||
finally:
|
||||
rec.restore()
|
||||
check("результат — отказ (None)", text is None, f"text={text!r}")
|
||||
m = es.run_projections()["inbox"]["m-910003"]
|
||||
check("reason=audio_track_failed", m.get("transcribe_reason") == "audio_track_failed",
|
||||
f"reason={m.get('transcribe_reason')!r}")
|
||||
check("ASR-слои не звались", not rec.calls["fw"] and not rec.calls["ws"])
|
||||
|
||||
|
||||
# ──────────────────────────── 5. e2e на фикстуре ────────────────────────────
|
||||
|
||||
def test_e2e_video_message_to_transcript():
|
||||
"""Полный путь: приём видео-сообщения → транскрипция → идемпотентность."""
|
||||
mid = tg_inbox.receive_message(_video_msg("m-910004", 12))
|
||||
rec = _Rec(fw_text="текст из видео e2e")
|
||||
try:
|
||||
text, engine = voice.transcribe_message(
|
||||
mid, "fid-m-910004", "fuid-m-910004", duration=12, kind="video")
|
||||
finally:
|
||||
rec.restore()
|
||||
check("e2e: транскрипт вернулся", text == "текст из видео e2e", f"text={text!r}")
|
||||
m = es.run_projections()["inbox"][mid]
|
||||
check("e2e: kind=video в проекции", m["kind"] == "video", f"kind={m['kind']!r}")
|
||||
check("e2e: движок записан", m["engine"] == "faster-whisper-large-v3-turbo")
|
||||
check("e2e: минуты увидат текст", m["text"] == "текст из видео e2e", f"text={m['text']!r}")
|
||||
# идемпотентность: повтор берёт транскрипт из read model — без download/ASR
|
||||
rec2 = _Rec(fw_text="другой текст (не должен использоваться)")
|
||||
try:
|
||||
text2, engine2 = voice.transcribe_message(
|
||||
mid, "fid-m-910004", "fuid-m-910004", duration=12, kind="video")
|
||||
finally:
|
||||
rec2.restore()
|
||||
check("повтор: транскрипт из кэша read model", text2 == "текст из видео e2e",
|
||||
f"text={text2!r}")
|
||||
check("повтор: без повторного download", not rec2.calls["dl"], "качал заново")
|
||||
check("повтор: без повторного ASR", not rec2.calls["fw"] and not rec2.calls["ws"])
|
||||
|
||||
|
||||
# ────────────────────────────── plain-run режим ──────────────────────────────
|
||||
|
||||
if __name__ == "__main__":
|
||||
tests = [v for k, v in sorted(globals().items()) if k.startswith("test_") and callable(v)]
|
||||
for t in tests:
|
||||
try:
|
||||
t()
|
||||
except AssertionError:
|
||||
pass
|
||||
print(f"\n{'='*60}")
|
||||
print(f"{'ALL OK' if not fails else 'FAILED: ' + ', '.join(fails)} ({len(tests)} тестов)")
|
||||
sys.exit(1 if fails else 0)
|
||||
290
tools/gcal.py
290
tools/gcal.py
|
|
@ -1,290 +0,0 @@
|
|||
#!/usr/bin/env python3
|
||||
"""gcal.py — Google Calendar для харнеса: create-event / list / check (t-261008155436).
|
||||
|
||||
Команда из TG «сделай звонок на X с Владой» → минутный тик грузит скилл
|
||||
skills/gcal/SKILL.md → зовёт этот инструмент → событие в календаре Alex
|
||||
с приглашением гостям, слот только на рабочий день (цикл «3 через 3»).
|
||||
|
||||
Автоматизация (service account, JWT → access token, REST v3 — без google-клиентов):
|
||||
1) Alex: console.cloud.google.com → новый проект → включить Calendar API →
|
||||
«Service accounts» → создать → ключ JSON → прислать файл.
|
||||
2) В календаре Google: Настройки → «Настройки для моего календаря» →
|
||||
«Предоставить доступ» → добавить client_email ключа с ролью
|
||||
«Вносить изменения» (make changes to events).
|
||||
3) Положить ключ: /root/simbiot-harness/secrets/gcal-service-account.json
|
||||
(или env GCAL_SERVICE_ACCOUNT_FILE). Файл 0600.
|
||||
4) ./gcal.py check — готовность. Готово: скилл создаёт события без человека.
|
||||
|
||||
Если ключа нет — работают --dry-run, check, next-workdays (планирование слотов).
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import base64
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
from datetime import date, datetime, timedelta, timezone
|
||||
from pathlib import Path
|
||||
|
||||
HERE = Path(__file__).resolve().parent
|
||||
REPO = HERE.parent
|
||||
DEFAULT_KEY = REPO / "secrets" / "gcal-service-account.json"
|
||||
TOKEN_CACHE = Path("/tmp/.gcal_access_token.json")
|
||||
|
||||
# ── График Alex: цикл «3 через 3» ─────────────────────────────────────────────
|
||||
# docs/alex-schedule.md: 3 рабочих, 3 выходных. Якорь выведен из m-516173591
|
||||
# (2026-10-03: «сегодня и завтра рабочие, далее 3 выходных» → 10-02 первый
|
||||
# рабочий цикла). Проверка: 10-08 = (10-08−10-02)=6 → 6%6=0 → рабочий (совпало
|
||||
# с «сегодня/завтра рабочие» от 10-08). Сдвиг цикла: env GCAL_WORK_ANCHOR=YYYY-MM-DD
|
||||
ANCHOR = os.environ.get("GCAL_WORK_ANCHOR", "2026-10-02")
|
||||
PERIOD = 6
|
||||
WORK_DAYS = {0, 1, 2} # смещения внутри цикла: 0,1,2 — рабочие; 3,4,5 — выходные
|
||||
|
||||
MIN_LEAD_MIN = int(os.environ.get("GCAL_MIN_LEAD_MIN", "120")) # анти-паттерн рекордера: не позже 2ч до начала
|
||||
|
||||
|
||||
def _anchor() -> date:
|
||||
return datetime.strptime(ANCHOR, "%Y-%m-%d").date()
|
||||
|
||||
|
||||
def is_workday(d: date) -> bool:
|
||||
return ((d - _anchor()).days % PERIOD) in WORK_DAYS
|
||||
|
||||
|
||||
def next_workdays(n: int = 6, from_day: date | None = None) -> list[date]:
|
||||
d = from_day or date.today()
|
||||
out: list[date] = []
|
||||
while len(out) < n:
|
||||
if is_workday(d):
|
||||
out.append(d)
|
||||
d += timedelta(days=1)
|
||||
return out
|
||||
|
||||
|
||||
# ── Креды и токен (service account → JWT → access token) ─────────────────────
|
||||
def _key_path(explicit: str | None) -> Path:
|
||||
p = Path(explicit or os.environ.get("GCAL_SERVICE_ACCOUNT_FILE") or DEFAULT_KEY)
|
||||
return p
|
||||
|
||||
|
||||
def load_key(explicit: str | None = None) -> dict | None:
|
||||
p = _key_path(explicit)
|
||||
if not p.exists():
|
||||
return None
|
||||
return json.loads(p.read_text())
|
||||
|
||||
|
||||
def _b64(o: dict | bytes) -> str:
|
||||
raw = json.dumps(o).encode() if isinstance(o, dict) else o
|
||||
return base64.urlsafe_b64encode(raw).rstrip(b"=").decode()
|
||||
|
||||
|
||||
def get_access_token(key: dict, force: bool = False) -> str:
|
||||
"""JWT (RS256) → Google OAuth2 → access token (кэш ~50 мин)."""
|
||||
if not force and TOKEN_CACHE.exists():
|
||||
try:
|
||||
c = json.loads(TOKEN_CACHE.read_text())
|
||||
if c.get("exp", 0) > time.time() + 120 and c.get("iss") == key["client_email"]:
|
||||
return c["token"]
|
||||
except Exception:
|
||||
pass
|
||||
import httpx # уже в venv харнеса
|
||||
|
||||
now = int(time.time())
|
||||
claim = {
|
||||
"iss": key["client_email"],
|
||||
"scope": "https://www.googleapis.com/auth/calendar",
|
||||
"aud": "https://oauth2.googleapis.com/token",
|
||||
"iat": now,
|
||||
"exp": now + 3600,
|
||||
}
|
||||
head = {"alg": "RS256", "typ": "JWT"}
|
||||
signing_input = f"{_b64(head)}.{_b64(claim)}".encode()
|
||||
|
||||
from cryptography.hazmat.primitives import hashes, serialization
|
||||
|
||||
pk = serialization.load_pem_private_key(key["private_key"].encode(), password=None)
|
||||
sig = pk.sign(signing_input, hashes.SHA256())
|
||||
jwt = f"{signing_input.decode()}.{_b64(sig)}"
|
||||
|
||||
r = httpx.post(
|
||||
"https://oauth2.googleapis.com/token",
|
||||
data={"grant_type": "urn:ietf:params:oauth:grant-type:jwt-bearer", "assertion": jwt},
|
||||
timeout=20,
|
||||
)
|
||||
if r.status_code != 200:
|
||||
raise RuntimeError(f"token exchange failed: {r.status_code} {r.text[:200]}")
|
||||
tok = r.json()
|
||||
TOKEN_CACHE.write_text(json.dumps({"token": tok["access_token"], "exp": now + tok.get("expires_in", 3600),
|
||||
"iss": key["client_email"]}))
|
||||
TOKEN_CACHE.chmod(0o600)
|
||||
return tok["access_token"]
|
||||
|
||||
|
||||
def _api(method: str, path: str, key: dict, body: dict | None = None, params: dict | None = None) -> dict:
|
||||
import httpx
|
||||
|
||||
tok = get_access_token(key)
|
||||
r = httpx.request(method, f"https://www.googleapis.com/calendar/v3{path}",
|
||||
headers={"Authorization": f"Bearer {tok}", "Content-Type": "application/json"},
|
||||
json=body, params=params or {}, timeout=25)
|
||||
if r.status_code >= 400:
|
||||
raise RuntimeError(f"calendar api {r.status_code}: {r.text[:300]}")
|
||||
return r.json()
|
||||
|
||||
|
||||
# ── Действия ──────────────────────────────────────────────────────────────────
|
||||
def cmd_check(args) -> int:
|
||||
print(f"anchor (цикл 3 через 3): {ANCHOR} (env GCAL_WORK_ANCHOR для сдвига)")
|
||||
today = date.today()
|
||||
print(f"сегодня {today}: {'РАБОЧИЙ' if is_workday(today) else 'выходной'}")
|
||||
print("ближайшие рабочие:", ", ".join(d.isoformat() for d in next_workdays(4)))
|
||||
key = load_key(args.key)
|
||||
if not key:
|
||||
p = _key_path(args.key)
|
||||
print(f"\nключ: НЕТ ({p})")
|
||||
print("шаги: Google Cloud → Calendar API → service account → JSON-ключ →")
|
||||
print(" доступ к календарю Alex для client_email (make changes) →")
|
||||
print(f" положить в {p} (0600). Затем: gcal.py check && gcal.py list")
|
||||
return 1
|
||||
print(f"\nключ: OK ({key.get('client_email', '?')})")
|
||||
try:
|
||||
cal = _api("GET", f"/calendars/{args.calendar}", key)
|
||||
print(f"календарь: {cal.get('summary')} ({cal.get('id')}) — доступ есть")
|
||||
return 0
|
||||
except Exception as e:
|
||||
print(f"доступ к календарю: НЕ УДАЛСЯ — {e}")
|
||||
print("→ дайте client_email право «Вносить изменения» в настройках календаря")
|
||||
return 2
|
||||
|
||||
|
||||
def _guard(slot_start: datetime) -> tuple[bool, str]:
|
||||
d = slot_start.date()
|
||||
if not is_workday(d):
|
||||
nxt = next_workdays(1, from_day=d + timedelta(days=1))
|
||||
sug = ", ".join(x.isoformat() for x in nxt)
|
||||
return False, f"{d.isoformat()} — выходной по циклу Alex «3 через 3». Ближайшие рабочие: {sug}"
|
||||
lead = (slot_start - datetime.now(timezone.utc)).total_seconds() / 60
|
||||
if lead < MIN_LEAD_MIN:
|
||||
return False, (f"до начала {int(lead)} мин < {MIN_LEAD_MIN} (анти-паттерн рекордера: "
|
||||
f"t-261008155708 — встречи <2 мин до старта не заходятся). Возьмите слот позже.")
|
||||
return True, "ok"
|
||||
|
||||
|
||||
def _parse_slot(day: str, hhmm: str, dur: int, tz_name: str) -> datetime:
|
||||
"""Локальное время дня в tz_name → момент UTC (API Google требует offset)."""
|
||||
from zoneinfo import ZoneInfo
|
||||
|
||||
d = date.fromisoformat(day)
|
||||
hh, mm = map(int, hhmm.split(":"))
|
||||
return datetime(d.year, d.month, d.day, hh, mm, tzinfo=ZoneInfo(tz_name)).astimezone(timezone.utc)
|
||||
|
||||
|
||||
def cmd_create(args) -> int:
|
||||
start = _parse_slot(args.day, args.time, args.duration, args.timezone)
|
||||
ok, msg = _guard(start)
|
||||
if not ok and not args.force:
|
||||
print(f"ОТКАЗ: {msg}")
|
||||
return 3
|
||||
if not ok:
|
||||
print(f"ВНИМАНИЕ (force): {msg}")
|
||||
end = start + timedelta(minutes=args.duration)
|
||||
guests = [g.strip() for g in (args.guests or "").split(",") if g.strip()]
|
||||
event = {
|
||||
"summary": args.title,
|
||||
"description": args.description or "Создано харнесом Simbiot (skills/gcal, t-261008155436-a658e9)",
|
||||
"start": {"dateTime": start.isoformat(), "timeZone": args.timezone},
|
||||
"end": {"dateTime": end.isoformat(), "timeZone": args.timezone},
|
||||
"attendees": [{"email": g} for g in guests],
|
||||
"reminders": {"useDefault": False, "overrides": [
|
||||
{"method": "popup", "minutes": 30}, {"method": "popup", "minutes": 10},
|
||||
]},
|
||||
}
|
||||
if args.dry_run:
|
||||
print("DRY-RUN (в Google не писали). Событие:")
|
||||
print(json.dumps(event, ensure_ascii=False, indent=1))
|
||||
print(f"guard: {msg}; слот рабочий: {ok}")
|
||||
return 0
|
||||
key = load_key(args.key)
|
||||
if not key:
|
||||
print("нет сервис-ключа Google — см. gcal.py check (шаги настройки)")
|
||||
return 1
|
||||
made = _api("POST", f"/calendars/{args.calendar}/events", key, body=event,
|
||||
params={"sendUpdates": "all"})
|
||||
print(f"СОЗДАНО: {made.get('htmlLink')}")
|
||||
print(f" {made.get('summary')} · {start:%Y-%m-%d %H:%M} UTC ({args.timezone}) · "
|
||||
f"гости: {', '.join(guests) or '—'}")
|
||||
return 0
|
||||
|
||||
|
||||
def cmd_list(args) -> int:
|
||||
key = load_key(args.key)
|
||||
if not key:
|
||||
print("нет сервис-ключа Google — см. gcal.py check")
|
||||
return 1
|
||||
tmin = datetime.now(timezone.utc).isoformat()
|
||||
data = _api("GET", f"/calendars/{args.calendar}/events", key,
|
||||
params={"timeMin": tmin, "maxResults": args.limit, "orderBy": "startTime",
|
||||
"singleEvents": "true"})
|
||||
evs = data.get("items", [])
|
||||
if not evs:
|
||||
print("ближайших событий нет")
|
||||
return 0
|
||||
for e in evs:
|
||||
when = (e.get("start", {}).get("dateTime") or e.get("start", {}).get("date", ""))[:16]
|
||||
print(f" {when} {e.get('summary', '(без названия)')}")
|
||||
return 0
|
||||
|
||||
|
||||
def cmd_next(args) -> int:
|
||||
for d in next_workdays(args.count):
|
||||
off = ((d - _anchor()).days % PERIOD)
|
||||
print(f" {d.isoformat()} рабочий #{off + 1}/3")
|
||||
return 0
|
||||
|
||||
|
||||
def main() -> int:
|
||||
ap = argparse.ArgumentParser(description="Google Calendar для харнеса (create-event по команде из TG)")
|
||||
sub = ap.add_subparsers(dest="cmd", required=True)
|
||||
|
||||
c = sub.add_parser("check", help="готовность: ключ, доступ, график")
|
||||
c.add_argument("--key", help="путь к JSON сервис-аккаунта")
|
||||
c.add_argument("--calendar", default=os.environ.get("GCAL_CALENDAR", "primary"))
|
||||
c.set_defaults(fn=cmd_check)
|
||||
|
||||
c = sub.add_parser("create", help="создать событие (слот только рабочий день)")
|
||||
c.add_argument("--title", required=True)
|
||||
c.add_argument("--day", required=True, help="YYYY-MM-DD")
|
||||
c.add_argument("--time", required=True, help="HH:MM (UTC)")
|
||||
c.add_argument("--duration", type=int, default=30, help="минуты")
|
||||
c.add_argument("--guests", default="", help="email через запятую (приглашения)")
|
||||
c.add_argument("--description", default="")
|
||||
c.add_argument("--timezone", default=os.environ.get("GCAL_TZ", "UTC"))
|
||||
c.add_argument("--calendar", default=os.environ.get("GCAL_CALENDAR", "primary"))
|
||||
c.add_argument("--key")
|
||||
c.add_argument("--dry-run", action="store_true", help="показать событие, в Google не писать")
|
||||
c.add_argument("--force", action="store_true", help="игнорировать guard (осознанно)")
|
||||
c.set_defaults(fn=cmd_create)
|
||||
|
||||
c = sub.add_parser("list", help="ближайшие события")
|
||||
c.add_argument("--limit", type=int, default=10)
|
||||
c.add_argument("--calendar", default=os.environ.get("GCAL_CALENDAR", "primary"))
|
||||
c.add_argument("--key")
|
||||
c.set_defaults(fn=cmd_list)
|
||||
|
||||
c = sub.add_parser("next-workdays", help="ближайшие рабочие дни (цикл 3 через 3)")
|
||||
c.add_argument("count", nargs="?", type=int, default=6)
|
||||
c.set_defaults(fn=cmd_next)
|
||||
|
||||
a = ap.parse_args()
|
||||
try:
|
||||
return a.fn(a)
|
||||
except Exception as e: # noqa: BLE001 — инструмент отвечает кодом, не трейсбеком
|
||||
print(f"ОШИБКА: {type(e).__name__}: {e}")
|
||||
return 4
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
|
|
@ -1,4 +0,0 @@
|
|||
{
|
||||
"jdkPath": "/usr/lib/jvm/java-17-openjdk-amd64",
|
||||
"androidSdkPath": "/opt/android-sdk"
|
||||
}
|
||||
|
|
@ -1,14 +0,0 @@
|
|||
{
|
||||
"permission": { "edit": "allow", "bash": "allow", "webfetch": "allow" },
|
||||
"share": "disabled",
|
||||
"provider": {
|
||||
"simbiot": {
|
||||
"npm": "@ai-sdk/openai-compatible",
|
||||
"name": "Simbiot Router (cil.by)",
|
||||
"options": { "baseURL": "https://router.cil.by/v1" },
|
||||
"models": {
|
||||
"gpt-5.2": {}, "claude-sonnet-4.6": {}, "gpt-4.1": {}, "o4-mini": {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
{
|
||||
"permission": {
|
||||
"edit": "allow",
|
||||
"bash": "allow",
|
||||
"webfetch": "allow"
|
||||
},
|
||||
"share": "disabled"
|
||||
}
|
||||
|
|
@ -1,2 +0,0 @@
|
|||
export OPENAI_API_KEY=sk-9ffbac9384bc446eab3bbe58a4a98dc3f4b2347d341caf51a34ce93149fd8c57f1eff2065f89025e
|
||||
export OPENAI_BASE_URL=https://router.cil.by/v1
|
||||
Loading…
Reference in a new issue