feat: Поиск - with it on the assistant searches DuckDuckGo itself and answers from what it found; without it, it cannot. «Думал N сек.»

This commit is contained in:
redtea committed 2026-10-16 10:32:33 +03:00
1 parent fa62f2c5b6
commit 9bf30b281e
8 files changed
+921 -7

No files matched your search

+8
View File
@@ -363,6 +363,10 @@ const aiAgent = `
Режим агента: ты сам ведёшь задачу шаг за шагом. В каждом ответе - коротко, что делаешь, и ровно одно действие. Получив результат, решай следующий шаг. Режим агента: ты сам ведёшь задачу шаг за шагом. В каждом ответе - коротко, что делаешь, и ровно одно действие. Получив результат, решай следующий шаг.
Когда задача решена (или решить нельзя) - ответь итогом без блоков действий.` Когда задача решена (или решить нельзя) - ответь итогом без блоков действий.`
const aiSearch = `
Поиск в интернете включён: блок ` + "```search\nзапрос\n```" + ` - панель найдёт его в DuckDuckGo и пришлёт результаты (заголовок, адрес, отрывок) следующим сообщением.
Ищи активно: свежие данные, версии, документация, тексты ошибок, всё, в чём не уверен, - сначала поиск, потом ответ со ссылками на найденное. Одно действие за ответ. Ссылки не выдумывай.`
const aiThink = ` const aiThink = `
Режим размышления: начни ответ строкой ::думаю:: и подумай - разбери задачу, варианты и риски; затем строка ::ответ:: и сам ответ.` Режим размышления: начни ответ строкой ::думаю:: и подумай - разбери задачу, варианты и риски; затем строка ::ответ:: и сам ответ.`
@@ -524,6 +528,7 @@ func (s *Server) handleAIChat(w http.ResponseWriter, r *http.Request) {
Context string `json:"context"` Context string `json:"context"`
Agent bool `json:"agent"` Agent bool `json:"agent"`
Think bool `json:"think"` Think bool `json:"think"`
Search bool `json:"search"`
Chat int64 `json:"chat"` Chat int64 `json:"chat"`
} }
if err := json.NewDecoder(http.MaxBytesReader(w, r.Body, 1<<20)).Decode(&in); err != nil || (in.Message == "" && in.Result == "") { if err := json.NewDecoder(http.MaxBytesReader(w, r.Body, 1<<20)).Decode(&in); err != nil || (in.Message == "" && in.Result == "") {
@@ -546,6 +551,9 @@ func (s *Server) handleAIChat(w http.ResponseWriter, r *http.Request) {
if in.Agent { if in.Agent {
system += aiAgent system += aiAgent
} }
if in.Search {
system += aiSearch
}
if in.Think { if in.Think {
system += aiThink system += aiThink
// Models heed the last words most: the reminder goes there too (not // Models heed the last words most: the reminder goes there too (not
+128
View File
@@ -0,0 +1,128 @@
package server
import (
"context"
"encoding/json"
"errors"
"html"
"io"
"net/http"
"net/url"
"regexp"
"strings"
"time"
)
// Web search for the assistant, when the user turned «Поиск» on: DuckDuckGo's
// plain pages, no key and nothing to install. The light page first, the
// HTML one if that fails; DuckDuckGo turns away too many requests from one
// address, and then the assistant is told so.
type searchResult struct {
Title string `json:"title"`
URL string `json:"url"`
Snippet string `json:"snippet"`
}
var (
reLiteLink = regexp.MustCompile(`(?s)<a[^>]+href="([^"]+)"[^>]*class='result-link'[^>]*>(.*?)</a>`)
reLiteSnippet = regexp.MustCompile(`(?s)<td class='result-snippet'>(.*?)</td>`)
reHTMLLink = regexp.MustCompile(`(?s)<a[^>]+class="result__a"[^>]+href="([^"]+)"[^>]*>(.*?)</a>`)
reHTMLSnippet = regexp.MustCompile(`(?s)<a[^>]+class="result__snippet"[^>]*>(.*?)</a>`)
reTags = regexp.MustCompile(`<[^>]+>`)
)
// text is a bit of a results page as plain text.
func text(s string) string {
return strings.Join(strings.Fields(html.UnescapeString(reTags.ReplaceAllString(s, ""))), " ")
}
// realURL takes DuckDuckGo's redirect (/l/?uddg=…) back to the address.
func realURL(h string) string {
h = html.UnescapeString(h)
if strings.HasPrefix(h, "//") {
h = "https:" + h
}
if u, err := url.Parse(h); err == nil {
if t := u.Query().Get("uddg"); t != "" {
return t
}
}
return h
}
// parseDDG reads the results of the light or the HTML page.
func parseDDG(page string) []searchResult {
links, snips := reLiteLink.FindAllStringSubmatch(page, -1), reLiteSnippet.FindAllStringSubmatch(page, -1)
if len(links) == 0 {
links, snips = reHTMLLink.FindAllStringSubmatch(page, -1), reHTMLSnippet.FindAllStringSubmatch(page, -1)
}
var out []searchResult
for i, l := range links {
r := searchResult{Title: text(l[2]), URL: realURL(l[1])}
if i < len(snips) {
r.Snippet = text(snips[i][1])
}
// Ads lead back through DuckDuckGo itself: left out.
if strings.Contains(r.URL, "duckduckgo.com/y.js") || r.Title == "" {
continue
}
out = append(out, r)
}
return out
}
var searchClient = &http.Client{Timeout: 15 * time.Second}
// webSearch asks DuckDuckGo for q: up to n results.
func webSearch(ctx context.Context, q string, n int) ([]searchResult, error) {
var last error
for _, base := range []string{"https://lite.duckduckgo.com/lite/?q=", "https://html.duckduckgo.com/html/?q="} {
req, _ := http.NewRequestWithContext(ctx, http.MethodGet, base+url.QueryEscape(q), nil)
req.Header.Set("User-Agent", "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0 Safari/537.36")
req.Header.Set("Accept-Language", "ru,en;q=0.8")
resp, err := searchClient.Do(req)
if err != nil {
last = err
continue
}
b, _ := io.ReadAll(io.LimitReader(resp.Body, 2<<20))
resp.Body.Close()
rs := parseDDG(string(b))
if resp.StatusCode == http.StatusOK && len(rs) > 0 {
if len(rs) > n {
rs = rs[:n]
}
return rs, nil
}
if resp.StatusCode == http.StatusOK {
last = errors.New("ничего не нашлось")
} else {
last = errors.New("поисковик не ответил (" + resp.Status + "), возможно, временно ограничил запросы")
}
}
return nil, last
}
// POST /api/panel/ai/search - {q}: the results, for a user with the
// assistant on (the «Поиск» switch is the page's: it lets the model ask).
func (s *Server) handleAISearch(w http.ResponseWriter, r *http.Request) {
a := s.currentAdmin(r)
if on, _ := s.db.GetConfig(aiUserKey(a.ID)); on != "1" {
writeJSON(w, map[string]any{"ok": false, "error": "ассистент выключен в профиле"})
return
}
var in struct {
Q string `json:"q"`
}
if err := json.NewDecoder(http.MaxBytesReader(w, r.Body, 4<<10)).Decode(&in); err != nil || strings.TrimSpace(in.Q) == "" {
writeJSON(w, map[string]any{"ok": false, "error": "пустой запрос"})
return
}
rs, err := webSearch(r.Context(), strings.TrimSpace(in.Q), 6)
if err != nil {
writeJSON(w, map[string]any{"ok": false, "error": err.Error()})
return
}
writeJSON(w, map[string]any{"ok": true, "data": rs})
}
+25
View File
@@ -0,0 +1,25 @@
package server
import (
"os"
"strings"
"testing"
)
// The results of DuckDuckGo's light page: titles, real addresses (not its
// redirects), snippets without tags.
func TestParseDDGLite(t *testing.T) {
b, err := os.ReadFile("testdata/ddg_lite.html")
if err != nil {
t.Fatal(err)
}
rs := parseDDG(string(b))
if len(rs) < 5 {
t.Fatalf("results: %d", len(rs))
}
r := rs[0]
if r.URL != "https://go.dev/doc/database/cancel-operations" || !strings.Contains(r.Title, "Canceling in-progress operations") ||
!strings.Contains(r.Snippet, "Learn how to use context.Context") || strings.Contains(r.Snippet, "<b>") {
t.Fatalf("first: %+v", r)
}
}
+1
View File
@@ -264,6 +264,7 @@ func (s *Server) Handler() http.Handler {
mux.Handle("GET /api/panel/ai/chats/{cid}", s.requireAdmin(s.handleAIHistory)) mux.Handle("GET /api/panel/ai/chats/{cid}", s.requireAdmin(s.handleAIHistory))
mux.Handle("POST /api/panel/ai/chats/{cid}/delete", s.requireAdmin(s.handleAIChatDelete)) mux.Handle("POST /api/panel/ai/chats/{cid}/delete", s.requireAdmin(s.handleAIChatDelete))
mux.Handle("POST /api/panel/ai/note", s.requireAdmin(s.handleAINote)) mux.Handle("POST /api/panel/ai/note", s.requireAdmin(s.handleAINote))
mux.Handle("POST /api/panel/ai/search", s.requireAdmin(s.handleAISearch))
mux.Handle("POST /settings/ai", s.adminsOnly(s.handleSetAI)) mux.Handle("POST /settings/ai", s.adminsOnly(s.handleSetAI))
mux.Handle("POST /settings/db", s.adminsOnly(s.handleSetDB)) mux.Handle("POST /settings/db", s.adminsOnly(s.handleSetDB))
mux.Handle("POST /settings/db/restart", s.adminsOnly(s.handleRestart)) mux.Handle("POST /settings/db/restart", s.adminsOnly(s.handleRestart))
+732
View File
@@ -0,0 +1,732 @@
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="content-type" content="text/html; charset=UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=3.0, user-scalable=1;">
<meta name="referrer" content="origin">
<meta name="HandheldFriendly" content="true" />
<meta name="robots" content="noindex, nofollow" />
<title>golang context cancel at DuckDuckGo</title>
<link title="DuckDuckGo (Lite)" type="application/opensearchdescription+xml" rel="search" href="//duckduckgo.com/opensearch_lite_v2.xml">
<link href="//duckduckgo.com/favicon.ico" rel="shortcut icon" />
<link rel="icon" href="//duckduckgo.com/favicon.ico" type="image/x-icon" />
<link id="icon60" rel="apple-touch-icon" href="//duckduckgo.com/assets/icons/meta/DDG-iOS-icon_60x60.png?v=2"/>
<link id="icon76" rel="apple-touch-icon" sizes="76x76" href="//duckduckgo.com/assets/icons/meta/DDG-iOS-icon_76x76.png?v=2"/>
<link id="icon120" rel="apple-touch-icon" sizes="120x120" href="//duckduckgo.com/assets/icons/meta/DDG-iOS-icon_120x120.png?v=2"/>
<link id="icon152" rel="apple-touch-icon" sizes="152x152" href="//duckduckgo.com/assets/icons/meta/DDG-iOS-icon_152x152.png?v=2"/>
<link rel="image_src" href="//duckduckgo.com/assets/icons/meta/DDG-icon_256x256.png">
<link rel="stylesheet" media="handheld, all" href="//duckduckgo.com/dist/lr.48ddfe4eadf6a534e93f.css" type="text/css"/>
</head>
<body>
<p class='extra'>&nbsp;</p>
<div class="header">
DuckDuckGo
</div>
<p class='extra'>&nbsp;</p>
<form action="/lite/" method="post">
<input class='query' type="text" size="40" name="q" value="golang context cancel">
<input class='submit' type="submit" value="Search">
<div class="filters">
<select class="submit" name="kl">
<option value="" >All Regions</option>
<option value="ar-es" >Argentina</option>
<option value="au-en" >Australia</option>
<option value="at-de" >Austria</option>
<option value="be-fr" >Belgium (fr)</option>
<option value="be-nl" >Belgium (nl)</option>
<option value="br-pt" >Brazil</option>
<option value="bg-bg" >Bulgaria</option>
<option value="ca-en" >Canada (en)</option>
<option value="ca-fr" >Canada (fr)</option>
<option value="ct-ca" >Catalonia</option>
<option value="cl-es" >Chile</option>
<option value="cn-zh" >China</option>
<option value="co-es" >Colombia</option>
<option value="hr-hr" >Croatia</option>
<option value="cz-cs" >Czech Republic</option>
<option value="dk-da" >Denmark</option>
<option value="ee-et" >Estonia</option>
<option value="fi-fi" >Finland</option>
<option value="fr-fr" >France</option>
<option value="de-de" >Germany</option>
<option value="gr-el" >Greece</option>
<option value="hk-tzh" >Hong Kong</option>
<option value="hu-hu" >Hungary</option>
<option value="is-is" >Iceland</option>
<option value="in-en" >India (en)</option>
<option value="id-en" >Indonesia (en)</option>
<option value="ie-en" >Ireland</option>
<option value="il-en" >Israel (en)</option>
<option value="it-it" >Italy</option>
<option value="jp-jp" >Japan</option>
<option value="kr-kr" >Korea</option>
<option value="lv-lv" >Latvia</option>
<option value="lt-lt" >Lithuania</option>
<option value="my-en" >Malaysia (en)</option>
<option value="mx-es" >Mexico</option>
<option value="nl-nl" >Netherlands</option>
<option value="nz-en" >New Zealand</option>
<option value="no-no" >Norway</option>
<option value="pk-en" >Pakistan (en)</option>
<option value="pe-es" >Peru</option>
<option value="ph-en" >Philippines (en)</option>
<option value="pl-pl" >Poland</option>
<option value="pt-pt" >Portugal</option>
<option value="ro-ro" >Romania</option>
<option value="ru-ru" >Russia</option>
<option value="xa-ar" >Saudi Arabia</option>
<option value="sg-en" >Singapore</option>
<option value="sk-sk" >Slovakia</option>
<option value="sl-sl" >Slovenia</option>
<option value="za-en" >South Africa</option>
<option value="es-ca" >Spain (ca)</option>
<option value="es-es" >Spain (es)</option>
<option value="se-sv" >Sweden</option>
<option value="ch-de" >Switzerland (de)</option>
<option value="ch-fr" >Switzerland (fr)</option>
<option value="tw-tzh" >Taiwan</option>
<option value="th-en" >Thailand (en)</option>
<option value="tr-tr" >Turkey</option>
<option value="us-en" >US (English)</option>
<option value="us-es" >US (Spanish)</option>
<option value="ua-uk" >Ukraine</option>
<option value="uk-en" >United Kingdom</option>
<option value="vn-en" >Vietnam (en)</option>
</select>
<select class="submit" name="df">
<option value="" selected>Any Time</option>
<option value="d" >Past Day</option>
<option value="w" >Past Week</option>
<option value="m" >Past Month</option>
<option value="y" >Past Year</option>
</select>
</div>
</form>
<p class="extra">&nbsp;</p>
<table border="0">
<tr>
<td>
</td>
<td>
<!-- Next Page Button Sub-template -->
<form class="next_form" action="/lite/" method="post">
<input type="submit" class='navbutton' value="Next Page &gt;">
<input type="hidden" name="q" value="golang context cancel">
<input type="hidden" name="s" value="10">
<input type="hidden" name="nextParams" value="">
<input type="hidden" name="v" value="l">
<input type="hidden" name="o" value="json">
<input type="hidden" name="dc" value="11">
<input type="hidden" name="api" value="d.js">
<input type="hidden" name="vqd" value="4-288446547072343849888557955747865435943">
&nbsp;&nbsp;&nbsp;&nbsp;
<input name="kl" value="wt-wt" type="hidden">
</form>
</td>
</tr>
</table>
<p class='extra'>&nbsp;</p>
<table border="0">
</table>
<table border="0">
<!-- Web results are present -->
<tr>
<td valign="top">
1.&nbsp;
</td>
<td>
<a rel="nofollow" href="//duckduckgo.com/l/?uddg=https%3A%2F%2Fgo.dev%2Fdoc%2Fdatabase%2Fcancel%2Doperations&amp;rut=114b1aa13c9158c8c2b077ccd5f890f8a940277c1fc938e7bdcaad636c444a06" class='result-link'>Canceling in-progress operations - The Go Programming Language</a>
</td>
</tr>
<!-- Only show abstract separately if there's a click URL (not EOF) -->
<tr>
<td>&nbsp;&nbsp;&nbsp;</td>
<td class='result-snippet'>
Learn how to use <b>context</b>.<b>Context</b> to manage and <b>cancel</b> database operations in Go. See examples of how to set a timeout, deadline, and propagate cancellation requests with <b>context</b>.
</td>
</tr>
<tr>
<td>&nbsp;&nbsp;&nbsp;</td>
<td>
<span class='link-text'>go.dev/doc/database/cancel-operations</span>
</td>
</tr>
<tr>
<td>&nbsp;</td>
<td>&nbsp;</td>
</tr>
<tr>
<td valign="top">
2.&nbsp;
</td>
<td>
<a rel="nofollow" href="//duckduckgo.com/l/?uddg=https%3A%2F%2Fgolangtutorial.com%2Fgolang%2Dcontext%2F&amp;rut=c8e3cbb5601e47ba9c55f0ad6b52790e09acc1c0d3caf33c676b68ee65a1f419" class='result-link'>Go Context Tutorial: Cancellation and Deadlines</a>
</td>
</tr>
<!-- Only show abstract separately if there's a click URL (not EOF) -->
<tr>
<td>&nbsp;&nbsp;&nbsp;</td>
<td class='result-snippet'>
Learn Go <b>context</b> with practical cancellation, timeout, HTTP request, and goroutine examples.
</td>
</tr>
<tr>
<td>&nbsp;&nbsp;&nbsp;</td>
<td>
<span class='link-text'>golangtutorial.com/golang-context/</span>
&nbsp;&nbsp;&nbsp;
<span class='timestamp'>2026-07-29T00:00:00.0000000</span>
</td>
</tr>
<tr>
<td>&nbsp;</td>
<td>&nbsp;</td>
</tr>
<tr>
<td valign="top">
3.&nbsp;
</td>
<td>
<a rel="nofollow" href="//duckduckgo.com/l/?uddg=https%3A%2F%2Fstackoverflow.com%2Fquestions%2F52799280%2Fcontext%2Dconfusion%2Dregarding%2Dcancellation&amp;rut=0fa1c305fa28cede62f85c49d0501954cac2eba160d5c48f155a1008367ae78c" class='result-link'>go - Context confusion regarding cancellation - Stack Overflow</a>
</td>
</tr>
<!-- Only show abstract separately if there's a click URL (not EOF) -->
<tr>
<td>&nbsp;&nbsp;&nbsp;</td>
<td class='result-snippet'>
Additional info #1: calling <b>cancel</b> () right after <b>context</b> initialized As per <b>context</b> documentation regarding the <b>cancel</b> () function: Canceling this <b>context</b> releases resources associated with it, so code should call <b>cancel</b> as soon as the operations running in this <b>Context</b> complete. It&#x27;s good to always call <b>cancel</b> () function right after the <b>context</b> declaration. doesn&#x27;t matter whether it&#x27;s also ...
</td>
</tr>
<tr>
<td>&nbsp;&nbsp;&nbsp;</td>
<td>
<span class='link-text'>stackoverflow.com/questions/52799280/context-confusion-regarding-cancellation</span>
</td>
</tr>
<tr>
<td>&nbsp;</td>
<td>&nbsp;</td>
</tr>
<tr>
<td valign="top">
4.&nbsp;
</td>
<td>
<a rel="nofollow" href="//duckduckgo.com/l/?uddg=https%3A%2F%2Fpkg.go.dev%2Fcontext&amp;rut=925dd34b1fc94c2b0cb26f6e920e10aa3131c3cfc14163fd7c01b17554611b77" class='result-link'>context package - context - Go Packages</a>
</td>
</tr>
<!-- Only show abstract separately if there's a click URL (not EOF) -->
<tr>
<td>&nbsp;&nbsp;&nbsp;</td>
<td class='result-snippet'>
Package <b>context</b> defines the <b>Context</b> type, which carries deadlines, cancellation signals, and other request-scoped values across API boundaries and between processes.
</td>
</tr>
<tr>
<td>&nbsp;&nbsp;&nbsp;</td>
<td>
<span class='link-text'>pkg.go.dev/context</span>
&nbsp;&nbsp;&nbsp;
<span class='timestamp'>2026-10-08T00:00:00.0000000</span>
</td>
</tr>
<tr>
<td>&nbsp;</td>
<td>&nbsp;</td>
</tr>
<tr>
<td valign="top">
5.&nbsp;
</td>
<td>
<a rel="nofollow" href="//duckduckgo.com/l/?uddg=https%3A%2F%2Fstackoverflow.com%2Fquestions%2F70042646%2Fbest%2Dpractices%2Don%2Dgo%2Dcontext%2Dcancelation%2Dfunctions&amp;rut=f3a199db4b71e24ff5a3b0d4cbf7241ee53a03312bc2e2bcadf6d9cb20aaa474" class='result-link'>Best practices on go context cancelation functions</a>
</td>
</tr>
<!-- Only show abstract separately if there's a click URL (not EOF) -->
<tr>
<td>&nbsp;&nbsp;&nbsp;</td>
<td class='result-snippet'>
So, is passing this cancellation function actually an issue? Are there any best practices related to the use of the <b>context</b> package and their <b>cancel</b> function?
</td>
</tr>
<tr>
<td>&nbsp;&nbsp;&nbsp;</td>
<td>
<span class='link-text'>stackoverflow.com/questions/70042646/best-practices-on-go-context-cancelation-functions</span>
</td>
</tr>
<tr>
<td>&nbsp;</td>
<td>&nbsp;</td>
</tr>
<tr>
<td valign="top">
6.&nbsp;
</td>
<td>
<a rel="nofollow" href="//duckduckgo.com/l/?uddg=https%3A%2F%2Fgolangbot.com%2Fcontext%2Dtimeout%2Dcancellation%2F&amp;rut=dc6314fa589ab9eac3160a9b8df6a7ca4604e480917f0b99cf558d081c78ab02" class='result-link'>Golang Context - Cancellation, Timeout and Propagation | golangbot.com</a>
</td>
</tr>
<!-- Only show abstract separately if there's a click URL (not EOF) -->
<tr>
<td>&nbsp;&nbsp;&nbsp;</td>
<td class='result-snippet'>
Learn how to <b>cancel</b> long running tasks in Go using <b>context</b> cancellation and timeout. This tutorial also teaches how to terminate outstanding web requests which were cancelled by the client.
</td>
</tr>
<tr>
<td>&nbsp;&nbsp;&nbsp;</td>
<td>
<span class='link-text'>golangbot.com/context-timeout-cancellation/</span>
&nbsp;&nbsp;&nbsp;
<span class='timestamp'>2026-07-08T00:00:00.0000000</span>
</td>
</tr>
<tr>
<td>&nbsp;</td>
<td>&nbsp;</td>
</tr>
<tr>
<td valign="top">
7.&nbsp;
</td>
<td>
<a rel="nofollow" href="//duckduckgo.com/l/?uddg=https%3A%2F%2Fgo.dev%2Fblog%2Fcontext&amp;rut=6d324e0f7c589513830578b05d2a0a5ccedf509c8d4181ac590b3a229c8b9057" class='result-link'>Go Concurrency Patterns: Context - The Go Programming Language</a>
</td>
</tr>
<!-- Only show abstract separately if there's a click URL (not EOF) -->
<tr>
<td>&nbsp;&nbsp;&nbsp;</td>
<td class='result-snippet'>
Learn how to use the <b>context</b> package to pass request-scoped values, cancellation signals, and deadlines across API boundaries in Go servers. See a complete example of an HTTP server that handles Google Web Search requests with <b>context</b>.
</td>
</tr>
<tr>
<td>&nbsp;&nbsp;&nbsp;</td>
<td>
<span class='link-text'>go.dev/blog/context</span>
</td>
</tr>
<tr>
<td>&nbsp;</td>
<td>&nbsp;</td>
</tr>
<tr>
<td valign="top">
8.&nbsp;
</td>
<td>
<a rel="nofollow" href="//duckduckgo.com/l/?uddg=https%3A%2F%2Fwww.sohamkamani.com%2Fgolang%2Fcontext%2F&amp;rut=7274980ce55c2a5b88f9299c12e1228a7ecfffbb5fabbab8a837cbad990a1a87" class='result-link'>How to Use Context in Golang (Deadlines, Cancellation, and Passing Values)</a>
</td>
</tr>
<!-- Only show abstract separately if there's a click URL (not EOF) -->
<tr>
<td>&nbsp;&nbsp;&nbsp;</td>
<td class='result-snippet'>
In this post, we&#x27;ll learn about Go&#x27;s <b>context</b> package, and more specifically, how we can use it to improve our applications performance. The main purpose of using <b>Context</b> is to manage long-running processes (like an HTTP request, a database call, or a network call) in a way that doesn&#x27;t waste resources. If used correctly, the <b>context</b> package can help you: <b>Cancel</b> long running processes ...
</td>
</tr>
<tr>
<td>&nbsp;&nbsp;&nbsp;</td>
<td>
<span class='link-text'>www.sohamkamani.com/golang/context/</span>
</td>
</tr>
<tr>
<td>&nbsp;</td>
<td>&nbsp;</td>
</tr>
<tr>
<td valign="top">
9.&nbsp;
</td>
<td>
<a rel="nofollow" href="//duckduckgo.com/l/?uddg=https%3A%2F%2Fgolangdoc.github.io%2Fpkg%2F1.12%2Fcontext%2Findex.html&amp;rut=4baaf27a174aa7e8bbcc0cfe1985ad916fe3ff014eb45fc06dd68178fc84e261" class='result-link'>context - The Go Programming Language</a>
</td>
</tr>
<!-- Only show abstract separately if there's a click URL (not EOF) -->
<tr>
<td>&nbsp;&nbsp;&nbsp;</td>
<td class='result-snippet'>
The returned <b>context&#x27;s</b> Done channel is closed when the deadline expires, when the returned <b>cancel</b> function is called, or when the parent <b>context&#x27;s</b> Done channel is closed, whichever happens first.
</td>
</tr>
<tr>
<td>&nbsp;&nbsp;&nbsp;</td>
<td>
<span class='link-text'>golangdoc.github.io/pkg/1.12/context/index.html</span>
</td>
</tr>
<tr>
<td>&nbsp;</td>
<td>&nbsp;</td>
</tr>
<tr>
<td valign="top">
10.&nbsp;
</td>
<td>
<a rel="nofollow" href="//duckduckgo.com/l/?uddg=https%3A%2F%2Fwww.golinuxcloud.com%2Fgolang%2Dcontext%2F&amp;rut=efdde246db353602fc52ef95d8d09a146270aeb46114aa061f320d037adea574" class='result-link'>Golang context — WithCancel, WithTimeout, WithDeadline, Background</a>
</td>
</tr>
<!-- Only show abstract separately if there's a click URL (not EOF) -->
<tr>
<td>&nbsp;&nbsp;&nbsp;</td>
<td class='result-snippet'>
The <b>golang</b> <b>context</b> package ties cancellation, timeouts, and optional request values to a <b>Context</b> value you pass through your call stack. Use Background (or TODO temporarily) as the root, WithTimeout / WithDeadline for time bounds, WithCancel for explicit shutdown, and WithValue only for small, request-scoped metadata—always defer <b>cancel</b> ...
</td>
</tr>
<tr>
<td>&nbsp;&nbsp;&nbsp;</td>
<td>
<span class='link-text'>www.golinuxcloud.com/golang-context/</span>
</td>
</tr>
<tr>
<td>&nbsp;</td>
<td>&nbsp;</td>
</tr>
<tr>
<td colspan=2>
<!-- Next Page Button Sub-template -->
<form class="next_form" action="/lite/" method="post">
<input type="submit" class='navbutton' value="Next Page &gt;">
<input type="hidden" name="q" value="golang context cancel">
<input type="hidden" name="s" value="10">
<input type="hidden" name="nextParams" value="">
<input type="hidden" name="v" value="l">
<input type="hidden" name="o" value="json">
<input type="hidden" name="dc" value="11">
<input type="hidden" name="api" value="d.js">
<input type="hidden" name="vqd" value="4-288446547072343849888557955747865435943">
&nbsp;&nbsp;&nbsp;&nbsp;
<input name="kl" value="wt-wt" type="hidden">
</form>
</td>
</tr>
</table>
<p class='extra'>&nbsp;</p>
<form action="/lite/" method="post">
<input class='query' type="text" size="40" name="q" value="golang context cancel" >
<input class='submit' type="submit" value="Search">
<input name="kl" value="wt-wt" type="hidden">
</form>
<p class='extra'>&nbsp;</p>
<img src="//duckduckgo.com/t/sl_l"/>
<div id='end-spacer'>&nbsp;</div>
</body>
</html>
+20 -6
View File
@@ -28,6 +28,8 @@
'<div class="ai-pills">' + '<div class="ai-pills">' +
'<button type="button" class="ai-pill" data-mode="agent" aria-pressed="false" title="Сам выполняет шаги один за другим, спрашивая перед каждой командой и записью файла">' + '<button type="button" class="ai-pill" data-mode="agent" aria-pressed="false" title="Сам выполняет шаги один за другим, спрашивая перед каждой командой и записью файла">' +
'<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 8V4H8"/><rect x="4" y="8" width="16" height="12" rx="2"/><path d="M2 14h2M20 14h2M9 13v2M15 13v2"/></svg>Агент</button>' + '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 8V4H8"/><rect x="4" y="8" width="16" height="12" rx="2"/><path d="M2 14h2M20 14h2M9 13v2M15 13v2"/></svg>Агент</button>' +
'<button type="button" class="ai-pill" data-mode="search" aria-pressed="false" title="Поиск в интернете (DuckDuckGo): ассистент ищет сам, когда нужно">' +
'<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="9"/><path d="M3 12h18M12 3a14 14 0 0 1 0 18M12 3a14 14 0 0 0 0 18"/></svg>Поиск</button>' +
'<button type="button" class="ai-pill" data-mode="think" aria-pressed="false" title="Сначала обдумывает, потом отвечает: дольше, но точнее">' + '<button type="button" class="ai-pill" data-mode="think" aria-pressed="false" title="Сначала обдумывает, потом отвечает: дольше, но точнее">' +
'<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9 18h6M10 22h4"/><path d="M12 2a7 7 0 0 0-4 12.7c.6.5 1 1.3 1 2.3h6c0-1 .4-1.8 1-2.3A7 7 0 0 0 12 2z"/></svg>Думать</button>' + '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9 18h6M10 22h4"/><path d="M12 2a7 7 0 0 0-4 12.7c.6.5 1 1.3 1 2.3h6c0-1 .4-1.8 1-2.3A7 7 0 0 0 12 2z"/></svg>Думать</button>' +
'</div>' + '</div>' +
@@ -314,6 +316,14 @@
const b64 = text => { const u = new TextEncoder().encode(text); let s = ''; for (let i = 0; i < u.length; i++) s += String.fromCharCode(u[i]); return btoa(s); }; const b64 = text => { const u = new TextEncoder().encode(text); let s = ''; for (let i = 0; i < u.length; i++) s += String.fromCharCode(u[i]); return btoa(s); };
// perform does one action with the user's rights and tells what came of it. // perform does one action with the user's rights and tells what came of it.
async function perform(a, signal) { async function perform(a, signal) {
// A web search: through the server, only with «Поиск» on.
if (a.kind === 'search') {
if (!searchOn()) return 'Ошибка: поиск выключен - включите плашку «Поиск»';
const r = await fetch('/api/panel/ai/search', {method: 'POST', headers: {'Content-Type': 'application/json'}, credentials: 'same-origin',
body: JSON.stringify({q: a.body})}).then(x => x.json()).catch(() => ({ok: false, error: 'нет связи'}));
if (!r.ok) return 'Ошибка поиска «' + a.body + '»: ' + r.error;
return 'Результаты поиска «' + a.body + '»:\n' + r.data.map((x, i) => (i + 1) + '. ' + x.title + ' - ' + x.url + (x.snippet ? '\n ' + x.snippet : '')).join('\n');
}
await loadMachines(); await loadMachines();
const m = machineOf(a.machine); const m = machineOf(a.machine);
if (!m) return 'Ошибка: нет машины «' + (a.machine || '?') + '». Машины: ' + machines.map(x => x.name).join(', '); if (!m) return 'Ошибка: нет машины «' + (a.machine || '?') + '». Машины: ' + machines.map(x => x.name).join(', ');
@@ -355,11 +365,12 @@
// ---------- messages ---------- // ---------- messages ----------
// stripThink: the answer without its thinking - what is acted on and kept. // stripThink: the answer without its thinking - what is acted on and kept.
const stripThink = t => t.replace(/^\s*(?:<think>|::думаю::)[\s\S]*?((?:<\/think>|::ответ::)\s*|$)/, ''); const stripThink = t => t.replace(/^\s*(?:<think>|::думаю::)[\s\S]*?((?:<\/think>|::ответ::)\s*|$)/, '');
const KIND = {run: 'команда', read: 'чтение', ls: 'папка', write: 'запись'}; const KIND = {run: 'команда', read: 'чтение', ls: 'папка', write: 'запись', search: 'поиск'};
const IC = d => '<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round">' + d + '</svg>'; const IC = d => '<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round">' + d + '</svg>';
const ICONS = {run: IC('<path d="m5 7 5 5-5 5M12 17h7"/>'), read: IC('<path d="M14 3H7a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V8z"/><path d="M14 3v5h5"/>'), const ICONS = {run: IC('<path d="m5 7 5 5-5 5M12 17h7"/>'), read: IC('<path d="M14 3H7a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V8z"/><path d="M14 3v5h5"/>'),
ls: IC('<path d="M3 7a2 2 0 0 1 2-2h4l2 2.5h8a2 2 0 0 1 2 2V17a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/>'), write: IC('<path d="M12 20h9"/><path d="M16.5 3.5a2.1 2.1 0 0 1 3 3L7 19l-4 1 1-4z"/>')}; ls: IC('<path d="M3 7a2 2 0 0 1 2-2h4l2 2.5h8a2 2 0 0 1 2 2V17a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/>'), write: IC('<path d="M12 20h9"/><path d="M16.5 3.5a2.1 2.1 0 0 1 3 3L7 19l-4 1 1-4z"/>'),
const LABEL = {run: 'Выполнить', read: 'Прочитать', ls: 'Открыть', write: 'Записать'}; search: IC('<circle cx="12" cy="12" r="9"/><path d="M3 12h18M12 3a14 14 0 0 1 0 18M12 3a14 14 0 0 0 0 18"/>')};
const LABEL = {run: 'Выполнить', read: 'Прочитать', ls: 'Открыть', write: 'Записать', search: 'Найти'};
// render: text with ``` blocks; action blocks get their button. // render: text with ``` blocks; action blocks get their button.
// render draws an answer: its thinking, folded, above; the text with // render draws an answer: its thinking, folded, above; the text with
// Markdown; ``` blocks apart, action blocks with their button. The // Markdown; ``` blocks apart, action blocks with their button. The
@@ -384,7 +395,7 @@
think.classList.toggle('going', going); think.classList.toggle('going', going);
// How long: timed here as it streams, or kept with the answer. // How long: timed here as it streams, or kept with the answer.
const ms = el._thinkMs || (el._t1 - el._t0); const ms = el._thinkMs || (el._t1 - el._t0);
think.querySelector('.ai-think-h .lbl').textContent = going ? 'Думаю…' : 'Думал ' + Math.max(1, Math.round(ms / 1000)) + ' с'; think.querySelector('.ai-think-h .lbl').textContent = going ? 'Думаю…' : 'Думал ' + Math.max(1, Math.round(ms / 1000)) + ' сек.';
think.querySelector('.ai-think-in').innerHTML = WSUtil.md(th[1].trim()); think.querySelector('.ai-think-in').innerHTML = WSUtil.md(th[1].trim());
text = text.slice(th[0].length); text = text.slice(th[0].length);
} }
@@ -459,7 +470,7 @@
await loadMachines(); await loadMachines();
const r = await fetch('/api/panel/ai/chat', { const r = await fetch('/api/panel/ai/chat', {
method: 'POST', headers: {'Content-Type': 'application/json'}, credentials: 'same-origin', signal: ac.signal, method: 'POST', headers: {'Content-Type': 'application/json'}, credentials: 'same-origin', signal: ac.signal,
body: JSON.stringify({message: text || '', result: res || '', context: context(), agent: agentOn(), think: thinkOn(), chat: chatId}) body: JSON.stringify({message: text || '', result: res || '', context: context(), agent: agentOn(), think: thinkOn(), search: searchOn(), chat: chatId})
}); });
// A new chat gets its id (and its name) from the answer. // A new chat gets its id (and its name) from the answer.
const cid = Number(r.headers.get('X-Chat-Id')) || 0; const cid = Number(r.headers.get('X-Chat-Id')) || 0;
@@ -489,6 +500,7 @@
const pill = k => q('.ai-pill[data-mode="' + k + '"]'); const pill = k => q('.ai-pill[data-mode="' + k + '"]');
const agentOn = () => pill('agent').getAttribute('aria-pressed') === 'true'; const agentOn = () => pill('agent').getAttribute('aria-pressed') === 'true';
const thinkOn = () => pill('think').getAttribute('aria-pressed') === 'true'; const thinkOn = () => pill('think').getAttribute('aria-pressed') === 'true';
const searchOn = () => pill('search').getAttribute('aria-pressed') === 'true';
// The modes are kept in this browser. // The modes are kept in this browser.
box.querySelectorAll('.ai-pill').forEach(b => { box.querySelectorAll('.ai-pill').forEach(b => {
try { b.setAttribute('aria-pressed', String(localStorage.getItem('lpra.ai.' + b.dataset.mode) === '1')); } catch (e) { /* off */ } try { b.setAttribute('aria-pressed', String(localStorage.getItem('lpra.ai.' + b.dataset.mode) === '1')); } catch (e) { /* off */ }
@@ -503,9 +515,11 @@
// and its result sent back - until an answer asks for nothing. // and its result sent back - until an answer asks for nothing.
async function talkTurn(text) { async function talkTurn(text) {
let turn = await ask(text); let turn = await ask(text);
for (let step = 0; agentOn() && ac && !ac.signal.aborted && step < 15; step++) { for (let step = 0; ac && !ac.signal.aborted && step < 15; step++) {
const a = WSUtil.parseActions(turn.answer)[0]; const a = WSUtil.parseActions(turn.answer)[0];
if (!a) break; if (!a) break;
// Without «Агент» only searches go on by themselves, with «Поиск» on.
if (!agentOn() && !(a.kind === 'search' && searchOn())) break;
const b = turn.el.querySelector('.ai-act'); const b = turn.el.querySelector('.ai-act');
if (needsYes(a) && !await UI.confirm(describe(a), {ok: a.kind === 'run' ? 'Выполнить' : 'Записать'})) { if (needsYes(a) && !await UI.confirm(describe(a), {ok: a.kind === 'run' ? 'Выполнить' : 'Записать'})) {
if (b) { b.disabled = true; b.textContent = 'Отклонено'; } if (b) { b.disabled = true; b.textContent = 'Отклонено'; }
+2 -1
View File
@@ -48,10 +48,11 @@
// closed ``` blocks headed run [machine], read|ls [machine:]path or // closed ``` blocks headed run [machine], read|ls [machine:]path or
// write [machine:]path (its body the file's text). // write [machine:]path (its body the file's text).
function parseActions(text) { function parseActions(text) {
const out = [], re = /```(run|read|ls|write)([^\n]*)\n([\s\S]*?)```/g; const out = [], re = /```(run|read|ls|write|search)([^\n]*)\n([\s\S]*?)```/g;
for (let m; (m = re.exec(text));) { for (let m; (m = re.exec(text));) {
const kind = m[1], head = m[2].trim(), body = m[3].replace(/\n$/, ''); const kind = m[1], head = m[2].trim(), body = m[3].replace(/\n$/, '');
if (kind === 'run') { out.push({kind, machine: head, path: '', body}); continue; } if (kind === 'run') { out.push({kind, machine: head, path: '', body}); continue; }
if (kind === 'search') { out.push({kind, machine: '', path: '', body: (head + '\n' + body).trim()}); continue; }
const c = head.indexOf(':'); const c = head.indexOf(':');
const machine = c > 0 && !head.startsWith('/') ? head.slice(0, c) : ''; const machine = c > 0 && !head.startsWith('/') ? head.slice(0, c) : '';
out.push({kind, machine, path: machine ? head.slice(c + 1) : head, body: kind === 'write' ? body : ''}); out.push({kind, machine, path: machine ? head.slice(c + 1) : head, body: kind === 'write' ? body : ''});
@@ -75,3 +75,8 @@ test('md: the common marks, everything else escaped', () => {
// Inside code nothing is marked up. // Inside code nothing is marked up.
assert.strictEqual(U.md('`**a**`'), '<p><code>**a**</code></p>'); assert.strictEqual(U.md('`**a**`'), '<p><code>**a**</code></p>');
}); });
test('parseActions: search blocks', () => {
assert.deepStrictEqual(U.parseActions('Ищу:\n```search\nnginx 1.27 changelog\n```'),
[{kind: 'search', machine: '', path: '', body: 'nginx 1.27 changelog'}]);
});