@openafw/openafw 0.5.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/PRIVACY.md +377 -0
- package/README.md +152 -0
- package/dist/backends-Byh5VYtT.js +418 -0
- package/dist/bin/afw.js +45270 -0
- package/dist/bin/tap.js +83 -0
- package/dist/bin/tools.js +432 -0
- package/dist/secrets-9JqUBHyw.js +3 -0
- package/dist/secrets-Bj-gyv53.js +169 -0
- package/package.json +62 -0
- package/ui-dist/assets/index-BY6COSYk.css +1 -0
- package/ui-dist/assets/index-C9yCeZlD.js +20 -0
- package/ui-dist/index.html +13 -0
|
@@ -0,0 +1,418 @@
|
|
|
1
|
+
import { atomicWrite, fileExists, paths } from "./secrets-Bj-gyv53.js";
|
|
2
|
+
import { readFile } from "node:fs/promises";
|
|
3
|
+
|
|
4
|
+
//#region src/core/tool-providers.ts
|
|
5
|
+
const TOOL_PROVIDERS_VERSION = 1;
|
|
6
|
+
/** Seeded default — a keyless DuckDuckGo backend so first-run users
|
|
7
|
+
* have a working `web_search` without any setup. Quality is mediocre
|
|
8
|
+
* vs Brave/Tavily but it costs nothing and never asks for an account. */
|
|
9
|
+
const SEEDED_DDG = {
|
|
10
|
+
id: "ddg",
|
|
11
|
+
label: "DuckDuckGo (built-in)",
|
|
12
|
+
kind: "web_search",
|
|
13
|
+
backend: "duckduckgo",
|
|
14
|
+
origin: "seeded"
|
|
15
|
+
};
|
|
16
|
+
const EMPTY_STORE = {
|
|
17
|
+
version: TOOL_PROVIDERS_VERSION,
|
|
18
|
+
providers: [SEEDED_DDG],
|
|
19
|
+
active: {}
|
|
20
|
+
};
|
|
21
|
+
function isObj(v) {
|
|
22
|
+
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
23
|
+
}
|
|
24
|
+
const KINDS = ["web_search"];
|
|
25
|
+
const BACKENDS = [
|
|
26
|
+
"duckduckgo",
|
|
27
|
+
"brave",
|
|
28
|
+
"searxng",
|
|
29
|
+
"tavily",
|
|
30
|
+
"baidu"
|
|
31
|
+
];
|
|
32
|
+
function normalizeProvider(raw) {
|
|
33
|
+
if (!isObj(raw)) return void 0;
|
|
34
|
+
if (typeof raw.id !== "string" || raw.id === "") return void 0;
|
|
35
|
+
if (!KINDS.includes(raw.kind)) return void 0;
|
|
36
|
+
if (!BACKENDS.includes(raw.backend)) return void 0;
|
|
37
|
+
return {
|
|
38
|
+
id: raw.id,
|
|
39
|
+
label: typeof raw.label === "string" && raw.label !== "" ? raw.label : raw.id,
|
|
40
|
+
kind: raw.kind,
|
|
41
|
+
backend: raw.backend,
|
|
42
|
+
...typeof raw.baseUrl === "string" && raw.baseUrl !== "" ? { baseUrl: raw.baseUrl } : {},
|
|
43
|
+
...typeof raw.authRef === "string" && raw.authRef !== "" ? { authRef: raw.authRef } : {},
|
|
44
|
+
...typeof raw.costPerCall === "number" && raw.costPerCall >= 0 ? { costPerCall: raw.costPerCall } : {},
|
|
45
|
+
origin: raw.origin === "manual" ? "manual" : "seeded"
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
function normalizeToolProviders(raw) {
|
|
49
|
+
if (!isObj(raw)) return {
|
|
50
|
+
...EMPTY_STORE,
|
|
51
|
+
providers: [SEEDED_DDG]
|
|
52
|
+
};
|
|
53
|
+
const providers = Array.isArray(raw.providers) ? raw.providers.map(normalizeProvider).filter((p) => p != null) : [];
|
|
54
|
+
if (!providers.some((p) => p.kind === "web_search")) providers.push(SEEDED_DDG);
|
|
55
|
+
const active = {};
|
|
56
|
+
if (isObj(raw.active)) for (const k of KINDS) {
|
|
57
|
+
const v = raw.active[k];
|
|
58
|
+
if (typeof v === "string" && v !== "") active[k] = v;
|
|
59
|
+
}
|
|
60
|
+
return {
|
|
61
|
+
version: TOOL_PROVIDERS_VERSION,
|
|
62
|
+
providers,
|
|
63
|
+
active
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
async function readToolProviders() {
|
|
67
|
+
if (!await fileExists(paths.toolProviders)) return {
|
|
68
|
+
...EMPTY_STORE,
|
|
69
|
+
providers: [SEEDED_DDG]
|
|
70
|
+
};
|
|
71
|
+
return normalizeToolProviders(JSON.parse(await readFile(paths.toolProviders, "utf8")));
|
|
72
|
+
}
|
|
73
|
+
async function writeToolProviders(store) {
|
|
74
|
+
await atomicWrite(paths.toolProviders, `${JSON.stringify(store, null, 2)}\n`);
|
|
75
|
+
}
|
|
76
|
+
let writeChain = Promise.resolve();
|
|
77
|
+
/** Serialized read-modify-write — see model-registry.ts mutateModelRegistry. */
|
|
78
|
+
function mutateToolProviders(fn) {
|
|
79
|
+
const next = writeChain.then(async () => {
|
|
80
|
+
const store = await readToolProviders();
|
|
81
|
+
const updated = fn(store);
|
|
82
|
+
if (updated) await writeToolProviders(updated);
|
|
83
|
+
return updated ?? store;
|
|
84
|
+
});
|
|
85
|
+
writeChain = next.catch(() => {});
|
|
86
|
+
return next;
|
|
87
|
+
}
|
|
88
|
+
/** Active provider for a kind — explicit `active[kind]` wins, then
|
|
89
|
+
* the first provider of that kind, then undefined (caller falls back
|
|
90
|
+
* to "no backend, return an error to the tool caller"). */
|
|
91
|
+
function activeProviderFor(store, kind) {
|
|
92
|
+
const explicit = store.active[kind];
|
|
93
|
+
if (explicit) {
|
|
94
|
+
const hit = store.providers.find((p) => p.id === explicit && p.kind === kind);
|
|
95
|
+
if (hit) return hit;
|
|
96
|
+
}
|
|
97
|
+
return store.providers.find((p) => p.kind === kind);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
//#endregion
|
|
101
|
+
//#region src/core/web-search/backends.ts
|
|
102
|
+
const DDG_URL = "https://html.duckduckgo.com/html/";
|
|
103
|
+
const DDG_USER_AGENT = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36";
|
|
104
|
+
async function searchDuckDuckGo(opts) {
|
|
105
|
+
const form = new URLSearchParams({ q: opts.query });
|
|
106
|
+
let res;
|
|
107
|
+
try {
|
|
108
|
+
res = await fetch(DDG_URL, {
|
|
109
|
+
method: "POST",
|
|
110
|
+
headers: {
|
|
111
|
+
"content-type": "application/x-www-form-urlencoded",
|
|
112
|
+
"user-agent": DDG_USER_AGENT,
|
|
113
|
+
accept: "text/html"
|
|
114
|
+
},
|
|
115
|
+
body: form.toString()
|
|
116
|
+
});
|
|
117
|
+
} catch (err) {
|
|
118
|
+
return {
|
|
119
|
+
ok: false,
|
|
120
|
+
error: `duckduckgo unreachable: ${err.message}`
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
if (!res.ok) return {
|
|
124
|
+
ok: false,
|
|
125
|
+
error: `duckduckgo HTTP ${res.status}`
|
|
126
|
+
};
|
|
127
|
+
const html = await res.text();
|
|
128
|
+
if (isDuckDuckGoAnomalyPage(html)) return {
|
|
129
|
+
ok: false,
|
|
130
|
+
error: "DuckDuckGo flagged this request as bot traffic and served a challenge page. This often resolves after a few minutes; for sustained use, switch to a key-based backend (Brave Search API) in Control · Tool Providers."
|
|
131
|
+
};
|
|
132
|
+
const results = parseDuckDuckGoHtml(html).slice(0, opts.count ?? 10);
|
|
133
|
+
return {
|
|
134
|
+
ok: true,
|
|
135
|
+
results
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
/** True when DDG returned its bot-challenge page instead of search
|
|
139
|
+
* results. The anomaly form's action URL is the strongest signal. */
|
|
140
|
+
function isDuckDuckGoAnomalyPage(html) {
|
|
141
|
+
return /anomaly\.js/.test(html) || /id="challenge-form"/.test(html);
|
|
142
|
+
}
|
|
143
|
+
/** Parse DDG's HTML results page. Their non-JS endpoint renders each
|
|
144
|
+
* result as a `result__a` anchor + `result__snippet` span. URLs come
|
|
145
|
+
* through a `//duckduckgo.com/l/?uddg=<encoded>` redirector — we
|
|
146
|
+
* decode the inner URL so callers get the real destination. The
|
|
147
|
+
* parser is intentionally a regex scan (no DOM lib) so it works in
|
|
148
|
+
* the daemon's plain node runtime; markup churn risk is the tradeoff. */
|
|
149
|
+
function parseDuckDuckGoHtml(html) {
|
|
150
|
+
const results = [];
|
|
151
|
+
const anchorRe = /<a[^>]+class="[^"]*\bresult__a\b[^"]*"[^>]+href="([^"]+)"[^>]*>([\s\S]*?)<\/a>/g;
|
|
152
|
+
const snippetRe = /<(?:a|span)[^>]+class="[^"]*\bresult__snippet\b[^"]*"[^>]*>([\s\S]*?)<\/(?:a|span)>/g;
|
|
153
|
+
const snippets = [];
|
|
154
|
+
let s;
|
|
155
|
+
while ((s = snippetRe.exec(html)) !== null) snippets.push(stripHtml(s[1] ?? ""));
|
|
156
|
+
let i = 0;
|
|
157
|
+
let m;
|
|
158
|
+
while ((m = anchorRe.exec(html)) !== null) {
|
|
159
|
+
const rawHref = decodeHtmlEntities(m[1] ?? "");
|
|
160
|
+
const url = unwrapDdgRedirect(rawHref);
|
|
161
|
+
const title = stripHtml(m[2] ?? "");
|
|
162
|
+
if (!url || !title) {
|
|
163
|
+
i++;
|
|
164
|
+
continue;
|
|
165
|
+
}
|
|
166
|
+
const snippet = snippets[i];
|
|
167
|
+
results.push(snippet ? {
|
|
168
|
+
title,
|
|
169
|
+
url,
|
|
170
|
+
snippet
|
|
171
|
+
} : {
|
|
172
|
+
title,
|
|
173
|
+
url
|
|
174
|
+
});
|
|
175
|
+
i++;
|
|
176
|
+
}
|
|
177
|
+
return results;
|
|
178
|
+
}
|
|
179
|
+
function unwrapDdgRedirect(href) {
|
|
180
|
+
const m = /[?&]uddg=([^&]+)/.exec(href);
|
|
181
|
+
if (m?.[1]) try {
|
|
182
|
+
return decodeURIComponent(m[1]);
|
|
183
|
+
} catch {
|
|
184
|
+
return "";
|
|
185
|
+
}
|
|
186
|
+
if (href.startsWith("//")) return `https:${href}`;
|
|
187
|
+
if (href.startsWith("http")) return href;
|
|
188
|
+
return "";
|
|
189
|
+
}
|
|
190
|
+
function stripHtml(s) {
|
|
191
|
+
return decodeHtmlEntities(s.replace(/<[^>]+>/g, "")).replace(/\s+/g, " ").trim();
|
|
192
|
+
}
|
|
193
|
+
function decodeHtmlEntities(s) {
|
|
194
|
+
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, "\"").replace(/'/g, "'").replace(/'/g, "'").replace(/ /g, " ");
|
|
195
|
+
}
|
|
196
|
+
const BRAVE_URL = "https://api.search.brave.com/res/v1/web/search";
|
|
197
|
+
async function searchBrave(opts) {
|
|
198
|
+
const url = new URL(BRAVE_URL);
|
|
199
|
+
url.searchParams.set("q", opts.query);
|
|
200
|
+
url.searchParams.set("count", String(opts.count ?? 10));
|
|
201
|
+
if (opts.locale) url.searchParams.set("country", opts.locale);
|
|
202
|
+
let res;
|
|
203
|
+
try {
|
|
204
|
+
res = await fetch(url, { headers: {
|
|
205
|
+
accept: "application/json",
|
|
206
|
+
"x-subscription-token": opts.apiKey
|
|
207
|
+
} });
|
|
208
|
+
} catch (err) {
|
|
209
|
+
return {
|
|
210
|
+
ok: false,
|
|
211
|
+
error: `brave unreachable: ${err.message}`
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
const text = await res.text();
|
|
215
|
+
if (!res.ok) return {
|
|
216
|
+
ok: false,
|
|
217
|
+
error: `brave HTTP ${res.status}: ${text.slice(0, 200)}`
|
|
218
|
+
};
|
|
219
|
+
let json;
|
|
220
|
+
try {
|
|
221
|
+
json = JSON.parse(text);
|
|
222
|
+
} catch {
|
|
223
|
+
return {
|
|
224
|
+
ok: false,
|
|
225
|
+
error: `brave returned non-JSON: ${text.slice(0, 200)}`
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
return {
|
|
229
|
+
ok: true,
|
|
230
|
+
results: parseBraveJson(json).slice(0, opts.count ?? 10)
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
function parseBraveJson(json) {
|
|
234
|
+
if (typeof json !== "object" || json === null) return [];
|
|
235
|
+
const web = json.web;
|
|
236
|
+
if (!web || !Array.isArray(web.results)) return [];
|
|
237
|
+
const out = [];
|
|
238
|
+
for (const r of web.results) {
|
|
239
|
+
if (typeof r !== "object" || r === null) continue;
|
|
240
|
+
const row = r;
|
|
241
|
+
const title = typeof row.title === "string" ? row.title : "";
|
|
242
|
+
const url = typeof row.url === "string" ? row.url : "";
|
|
243
|
+
const description = typeof row.description === "string" ? row.description : "";
|
|
244
|
+
if (!title || !url) continue;
|
|
245
|
+
out.push(description ? {
|
|
246
|
+
title,
|
|
247
|
+
url,
|
|
248
|
+
snippet: description
|
|
249
|
+
} : {
|
|
250
|
+
title,
|
|
251
|
+
url
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
return out;
|
|
255
|
+
}
|
|
256
|
+
const BAIDU_SMART_URL = "https://qianfan.baidubce.com/v2/ai_search/chat/completions";
|
|
257
|
+
const BAIDU_WEB_URL = "https://qianfan.baidubce.com/v2/ai_search/web_search";
|
|
258
|
+
const BAIDU_SMART_MODEL = "ernie-4.5-turbo-32k";
|
|
259
|
+
async function searchBaidu(opts) {
|
|
260
|
+
const smart = await searchBaiduSmart(opts);
|
|
261
|
+
if (smart.ok) return smart;
|
|
262
|
+
const plain = await searchBaiduWeb(opts);
|
|
263
|
+
if (plain.ok) return plain;
|
|
264
|
+
return {
|
|
265
|
+
ok: false,
|
|
266
|
+
error: `baidu smart-search failed (${smart.error}); plain web_search also failed (${plain.error})`
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
/** Smart search via /v2/ai_search/chat/completions. Carries a model
|
|
270
|
+
* parameter (the LLM summary is discarded by afw — the routed
|
|
271
|
+
* model synthesises its own answer — but the references[] field is
|
|
272
|
+
* the same shape we consume from plain web_search). */
|
|
273
|
+
async function searchBaiduSmart(opts) {
|
|
274
|
+
const count = Math.max(1, Math.min(20, opts.count ?? 10));
|
|
275
|
+
const body = {
|
|
276
|
+
messages: [{
|
|
277
|
+
role: "user",
|
|
278
|
+
content: opts.query
|
|
279
|
+
}],
|
|
280
|
+
model: BAIDU_SMART_MODEL,
|
|
281
|
+
search_source: "baidu_search_v2",
|
|
282
|
+
stream: false,
|
|
283
|
+
resource_type_filter: [{
|
|
284
|
+
type: "web",
|
|
285
|
+
top_k: count
|
|
286
|
+
}],
|
|
287
|
+
enable_reasoning: false,
|
|
288
|
+
enable_deep_search: false,
|
|
289
|
+
enable_followup_queries: false,
|
|
290
|
+
search_mode: "required"
|
|
291
|
+
};
|
|
292
|
+
const filter = freshnessToFilter(opts.freshness);
|
|
293
|
+
if (filter) body.search_filter = filter;
|
|
294
|
+
return callBaidu(BAIDU_SMART_URL, opts.apiKey, body, count);
|
|
295
|
+
}
|
|
296
|
+
/** Plain web search via /v2/ai_search/web_search. No model invocation,
|
|
297
|
+
* larger monthly quota — the fallback when smart search refuses. */
|
|
298
|
+
async function searchBaiduWeb(opts) {
|
|
299
|
+
const count = Math.max(1, Math.min(50, opts.count ?? 10));
|
|
300
|
+
const body = {
|
|
301
|
+
messages: [{
|
|
302
|
+
role: "user",
|
|
303
|
+
content: opts.query
|
|
304
|
+
}],
|
|
305
|
+
search_source: "baidu_search_v2",
|
|
306
|
+
resource_type_filter: [{
|
|
307
|
+
type: "web",
|
|
308
|
+
top_k: count
|
|
309
|
+
}]
|
|
310
|
+
};
|
|
311
|
+
const filter = freshnessToFilter(opts.freshness);
|
|
312
|
+
if (filter) body.search_filter = filter;
|
|
313
|
+
return callBaidu(BAIDU_WEB_URL, opts.apiKey, body, count);
|
|
314
|
+
}
|
|
315
|
+
async function callBaidu(url, apiKey, body, count) {
|
|
316
|
+
let res;
|
|
317
|
+
try {
|
|
318
|
+
res = await fetch(url, {
|
|
319
|
+
method: "POST",
|
|
320
|
+
headers: {
|
|
321
|
+
"content-type": "application/json",
|
|
322
|
+
accept: "application/json",
|
|
323
|
+
authorization: `Bearer ${apiKey}`,
|
|
324
|
+
"x-appbuilder-from": "afw"
|
|
325
|
+
},
|
|
326
|
+
body: JSON.stringify(body)
|
|
327
|
+
});
|
|
328
|
+
} catch (err) {
|
|
329
|
+
return {
|
|
330
|
+
ok: false,
|
|
331
|
+
error: `baidu unreachable: ${err.message}`
|
|
332
|
+
};
|
|
333
|
+
}
|
|
334
|
+
const text = await res.text();
|
|
335
|
+
if (!res.ok) return {
|
|
336
|
+
ok: false,
|
|
337
|
+
error: `baidu HTTP ${res.status}: ${text.slice(0, 200)}`
|
|
338
|
+
};
|
|
339
|
+
let json;
|
|
340
|
+
try {
|
|
341
|
+
json = JSON.parse(text);
|
|
342
|
+
} catch {
|
|
343
|
+
return {
|
|
344
|
+
ok: false,
|
|
345
|
+
error: `baidu returned non-JSON: ${text.slice(0, 200)}`
|
|
346
|
+
};
|
|
347
|
+
}
|
|
348
|
+
if (json && typeof json === "object" && "code" in json && json.code) {
|
|
349
|
+
const msg = json.message;
|
|
350
|
+
return {
|
|
351
|
+
ok: false,
|
|
352
|
+
error: `baidu error: ${typeof msg === "string" ? msg : JSON.stringify(msg)}`
|
|
353
|
+
};
|
|
354
|
+
}
|
|
355
|
+
return {
|
|
356
|
+
ok: true,
|
|
357
|
+
results: parseBaiduJson(json).slice(0, count)
|
|
358
|
+
};
|
|
359
|
+
}
|
|
360
|
+
function parseBaiduJson(json) {
|
|
361
|
+
if (typeof json !== "object" || json === null) return [];
|
|
362
|
+
const refs = json.references;
|
|
363
|
+
if (!Array.isArray(refs)) return [];
|
|
364
|
+
const out = [];
|
|
365
|
+
for (const r of refs) {
|
|
366
|
+
if (typeof r !== "object" || r === null) continue;
|
|
367
|
+
const row = r;
|
|
368
|
+
const title = typeof row.title === "string" ? row.title : "";
|
|
369
|
+
const url = typeof row.url === "string" ? row.url : "";
|
|
370
|
+
const snippetField = typeof row.snippet === "string" ? row.snippet : typeof row.content === "string" ? row.content : "";
|
|
371
|
+
if (!title || !url) continue;
|
|
372
|
+
out.push(snippetField ? {
|
|
373
|
+
title,
|
|
374
|
+
url,
|
|
375
|
+
snippet: snippetField
|
|
376
|
+
} : {
|
|
377
|
+
title,
|
|
378
|
+
url
|
|
379
|
+
});
|
|
380
|
+
}
|
|
381
|
+
return out;
|
|
382
|
+
}
|
|
383
|
+
/** Map the Baidu `freshness` shorthand to its `search_filter` JSON.
|
|
384
|
+
* Returns undefined when freshness is absent or malformed (caller
|
|
385
|
+
* omits the filter so Baidu's default time range applies). */
|
|
386
|
+
function freshnessToFilter(freshness) {
|
|
387
|
+
if (!freshness) return void 0;
|
|
388
|
+
const now = new Date();
|
|
389
|
+
const dayShift = (n) => {
|
|
390
|
+
const d = new Date(now);
|
|
391
|
+
d.setUTCDate(d.getUTCDate() - n);
|
|
392
|
+
return d.toISOString().slice(0, 10);
|
|
393
|
+
};
|
|
394
|
+
const tomorrow = (() => {
|
|
395
|
+
const d = new Date(now);
|
|
396
|
+
d.setUTCDate(d.getUTCDate() + 1);
|
|
397
|
+
return d.toISOString().slice(0, 10);
|
|
398
|
+
})();
|
|
399
|
+
let start;
|
|
400
|
+
let end = tomorrow;
|
|
401
|
+
if (freshness === "pd") start = dayShift(1);
|
|
402
|
+
else if (freshness === "pw") start = dayShift(6);
|
|
403
|
+
else if (freshness === "pm") start = dayShift(30);
|
|
404
|
+
else if (freshness === "py") start = dayShift(364);
|
|
405
|
+
else {
|
|
406
|
+
const m = /^(\d{4}-\d{2}-\d{2})to(\d{4}-\d{2}-\d{2})$/.exec(freshness);
|
|
407
|
+
if (!m) return void 0;
|
|
408
|
+
start = m[1];
|
|
409
|
+
end = m[2] ?? end;
|
|
410
|
+
}
|
|
411
|
+
return { range: { page_time: {
|
|
412
|
+
gte: start,
|
|
413
|
+
lt: end
|
|
414
|
+
} } };
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
//#endregion
|
|
418
|
+
export { activeProviderFor, mutateToolProviders, readToolProviders, searchBaidu, searchBrave, searchDuckDuckGo };
|