nolo-cli 0.1.23 → 0.1.26
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/README.md +25 -3
- package/agent-runtime/hybridRecordStore.ts +30 -9
- package/agent-runtime/localLoop.ts +12 -4
- package/agent-runtime/localToolPolicy.ts +22 -10
- package/agent-runtime/localWorkspaceTools.ts +140 -374
- package/agent-runtime/runtimeToolPolicy.ts +43 -2
- package/agent-runtime/types.ts +5 -0
- package/agentAliases.ts +1 -1
- package/agentCommandSupport.ts +77 -0
- package/agentInternalCommandEntries.ts +34 -0
- package/agentListCommands.ts +123 -0
- package/agentListHelpers.ts +187 -0
- package/agentMachineCommands.ts +402 -0
- package/agentRecordCommands.ts +141 -0
- package/agentRecordHelpers.ts +395 -0
- package/agentRunCommand.ts +1 -2
- package/agentRuntimeCommands.ts +7 -701
- package/ai/agent/_executeModel.ts +29 -3
- package/ai/agent/buildSystemPrompt.ts +17 -0
- package/ai/agent/cliChatClient.ts +38 -4
- package/ai/agent/cliExecutor.ts +304 -25
- package/ai/agent/createAgentSchema.ts +2 -2
- package/ai/agent/guidedCreation/assistant.ts +82 -0
- package/ai/agent/guidedCreation/capabilities.ts +84 -0
- package/ai/agent/guidedCreation/draft.ts +62 -0
- package/ai/agent/guidedCreation/types.ts +38 -0
- package/ai/agent/machineRunPermissions.ts +21 -3
- package/ai/agent/runtimeGuidance.ts +1 -1
- package/ai/agent/server/fetchPublicAgents.ts +6 -1
- package/ai/agent/streamAgentChatTurn.ts +492 -19
- package/ai/agent/web/useGuidedAgentCreation.ts +217 -0
- package/ai/ai.locale.ts +14 -2
- package/ai/chat/sendOpenAICompletionsRequest.ts +28 -3
- package/ai/chat/sendOpenAIResponseRequest.ts +3 -2
- package/ai/llm/getPricing.ts +14 -0
- package/ai/llm/kimi.ts +3 -0
- package/ai/llm/modelAvatar.ts +1 -0
- package/ai/llm/providers.ts +1 -1
- package/ai/taskRun/taskRunProtocol.ts +24 -10
- package/ai/token/modelUsageQuery.ts +17 -0
- package/ai/tools/agent/agentTools.ts +21 -0
- package/ai/tools/agent/prepareAgentDraftTool.ts +159 -0
- package/ai/tools/checkEnvTool.ts +52 -20
- package/ai/tools/convertMarxistsBookTool.ts +103 -0
- package/ai/tools/{execBashTool.ts → execShellTool.ts} +44 -161
- package/ai/tools/index.ts +24 -14
- package/ai/tools/marxistsOfflineBook.ts +253 -0
- package/ai/tools/modelUsageTools.ts +4 -2
- package/ai/tools/readTool.ts +31 -0
- package/ai/tools/toolApiClient.ts +35 -1
- package/ai/tools/toolNameAliases.ts +13 -0
- package/cliCommandAdapters.ts +28 -0
- package/cliCommandDispatch.ts +66 -0
- package/cliCommandFactories.ts +34 -0
- package/cliCommandTypes.ts +49 -0
- package/cliEnvHelpers.ts +44 -0
- package/client/agentRun.test.ts +123 -19
- package/client/agentRun.ts +40 -1
- package/client/hybridRecordStore.test.ts +42 -0
- package/client/hybridRecordStore.ts +11 -0
- package/client/localRuntimeAdapter.test.ts +189 -21
- package/client/localRuntimeAdapter.ts +84 -8
- package/client/localRuntimeDryRun.test.ts +5 -2
- package/client/taskRunPrompt.ts +1 -0
- package/commandRegistry.ts +11 -61
- package/connector-experimental/capabilities.ts +1 -0
- package/database/server/coreDataProxy.ts +43 -26
- package/index.ts +54 -191
- package/internalCommandEntries.ts +12 -0
- package/machineCommands.ts +301 -32
- package/offlineMarxistsAgentCommand.ts +314 -0
- package/package.json +20 -2
- package/runtimeDoctorCommands.ts +118 -0
- package/scriptCommandEntries.ts +71 -0
- package/systemInternalCommandEntries.ts +71 -0
- package/workflowInternalCommandEntries.ts +11 -0
package/ai/tools/index.ts
CHANGED
|
@@ -22,6 +22,7 @@ import {
|
|
|
22
22
|
createWorkflowFunc,
|
|
23
23
|
} from "./createWorkflowTool";
|
|
24
24
|
import { delayFunctionSchema, delayFunc } from "./delayTool";
|
|
25
|
+
import { canonicalizeToolName } from "./toolNameAliases";
|
|
25
26
|
import {
|
|
26
27
|
completeDialogGoalFunc,
|
|
27
28
|
completeDialogGoalFunctionSchema,
|
|
@@ -160,6 +161,10 @@ import {
|
|
|
160
161
|
fetchWebpageFunctionSchema,
|
|
161
162
|
fetchWebpageFunc,
|
|
162
163
|
} from "./fetchWebpageTool";
|
|
164
|
+
import {
|
|
165
|
+
convertMarxistsBookToOfflineHtmlFunctionSchema,
|
|
166
|
+
convertMarxistsBookToOfflineHtmlFunc,
|
|
167
|
+
} from "./convertMarxistsBookTool";
|
|
163
168
|
import {
|
|
164
169
|
readXPostFunctionSchema,
|
|
165
170
|
readXPostFunc,
|
|
@@ -281,7 +286,7 @@ import {
|
|
|
281
286
|
rememberMemoryFunc,
|
|
282
287
|
rememberMemoryFunctionSchema,
|
|
283
288
|
} from "./rememberMemoryTool";
|
|
284
|
-
import {
|
|
289
|
+
import { execShellFunctionSchema, execShellFunc } from "./execShellTool";
|
|
285
290
|
import { checkEnvFunctionSchema, checkEnvFunc } from "./checkEnvTool";
|
|
286
291
|
|
|
287
292
|
import { agentToolDefinitions } from "./agent/agentTools";
|
|
@@ -1464,6 +1469,23 @@ const baseToolDefinitions: ToolDefinition[] = [
|
|
|
1464
1469
|
},
|
|
1465
1470
|
behavior: "data",
|
|
1466
1471
|
},
|
|
1472
|
+
{
|
|
1473
|
+
id: "convertMarxistsBookToOfflineHtml",
|
|
1474
|
+
schema: convertMarxistsBookToOfflineHtmlFunctionSchema,
|
|
1475
|
+
executor: convertMarxistsBookToOfflineHtmlFunc,
|
|
1476
|
+
description: {
|
|
1477
|
+
name: "convertMarxistsBookToOfflineHtml",
|
|
1478
|
+
description:
|
|
1479
|
+
"将 Marxists.org 中文旧式书籍页面转换成保留原 CSS、背景和排版的离线单 HTML。",
|
|
1480
|
+
category: "网络与智能",
|
|
1481
|
+
},
|
|
1482
|
+
behavior: "data",
|
|
1483
|
+
capability: "web_access",
|
|
1484
|
+
riskLevel: "low",
|
|
1485
|
+
costLevel: "medium",
|
|
1486
|
+
defaultConsent: "auto",
|
|
1487
|
+
cancelable: true,
|
|
1488
|
+
},
|
|
1467
1489
|
{
|
|
1468
1490
|
id: "readXPost",
|
|
1469
1491
|
schema: readXPostFunctionSchema,
|
|
@@ -1837,18 +1859,6 @@ const baseToolDefinitions: ToolDefinition[] = [
|
|
|
1837
1859
|
behavior: "data",
|
|
1838
1860
|
uiGroup: "general",
|
|
1839
1861
|
},
|
|
1840
|
-
{
|
|
1841
|
-
id: "execBash",
|
|
1842
|
-
schema: execBashFunctionSchema,
|
|
1843
|
-
executor: execBashFunc,
|
|
1844
|
-
description: {
|
|
1845
|
-
name: "exec_bash",
|
|
1846
|
-
description:
|
|
1847
|
-
"通过后端 diff-server 的 /api/exec-bash 接口执行一条 bash 命令,仅用于本地开发调试。",
|
|
1848
|
-
category: "网络与智能",
|
|
1849
|
-
},
|
|
1850
|
-
behavior: "action",
|
|
1851
|
-
},
|
|
1852
1862
|
{
|
|
1853
1863
|
id: "execShell",
|
|
1854
1864
|
schema: execShellFunctionSchema,
|
|
@@ -2070,7 +2080,7 @@ export const findToolExecutor = (
|
|
|
2070
2080
|
executor: ToolDefinition["executor"];
|
|
2071
2081
|
canonicalName: string;
|
|
2072
2082
|
} => {
|
|
2073
|
-
const normalizedRawName = normalizeToolName(rawName);
|
|
2083
|
+
const normalizedRawName = normalizeToolName(canonicalizeToolName(rawName));
|
|
2074
2084
|
const canonicalName = Object.keys(toolExecutors).find(
|
|
2075
2085
|
(key) => normalizeToolName(key) === normalizedRawName
|
|
2076
2086
|
);
|
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
export type MarxistsOfflineBookPage = {
|
|
2
|
+
name: string;
|
|
3
|
+
url: string;
|
|
4
|
+
title: string;
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
export type MarxistsOfflineBookResult = {
|
|
8
|
+
title: string;
|
|
9
|
+
startUrl: string;
|
|
10
|
+
indexUrl: string;
|
|
11
|
+
stylesheetUrl: string | null;
|
|
12
|
+
pages: MarxistsOfflineBookPage[];
|
|
13
|
+
html: string;
|
|
14
|
+
validation: {
|
|
15
|
+
pageCount: number;
|
|
16
|
+
hasEmbeddedAssets: boolean;
|
|
17
|
+
hasNetworkUrls: boolean;
|
|
18
|
+
hasUtf8Meta: boolean;
|
|
19
|
+
byteLength: number;
|
|
20
|
+
};
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export type ConvertMarxistsBookArgs = {
|
|
24
|
+
startUrl: string;
|
|
25
|
+
encoding?: string;
|
|
26
|
+
fetchImpl?: typeof fetch;
|
|
27
|
+
maxPages?: number;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const DEFAULT_ENCODING = "gb2312";
|
|
31
|
+
const USER_AGENT =
|
|
32
|
+
"Mozilla/5.0 (compatible; NoloMarxistsOfflineBook/1.0; +https://nolo.chat)";
|
|
33
|
+
|
|
34
|
+
const normalizeBookBaseUrl = (startUrl: string): URL => {
|
|
35
|
+
const parsed = new URL(startUrl);
|
|
36
|
+
if (!/^https?:$/i.test(parsed.protocol)) {
|
|
37
|
+
throw new Error("startUrl must be an http or https URL.");
|
|
38
|
+
}
|
|
39
|
+
return new URL("./", parsed);
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const decodeBytes = (bytes: ArrayBuffer, encoding: string): string => {
|
|
43
|
+
try {
|
|
44
|
+
return new TextDecoder(encoding).decode(bytes);
|
|
45
|
+
} catch {
|
|
46
|
+
return new TextDecoder(DEFAULT_ENCODING).decode(bytes);
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const fetchBytes = async (
|
|
51
|
+
url: string,
|
|
52
|
+
fetchImpl: typeof fetch,
|
|
53
|
+
): Promise<ArrayBuffer> => {
|
|
54
|
+
const response = await fetchImpl(url, {
|
|
55
|
+
headers: {
|
|
56
|
+
"user-agent": USER_AGENT,
|
|
57
|
+
accept: "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
|
|
58
|
+
},
|
|
59
|
+
});
|
|
60
|
+
if (!response.ok) {
|
|
61
|
+
throw new Error(`Failed to fetch ${url}: HTTP ${response.status}`);
|
|
62
|
+
}
|
|
63
|
+
return response.arrayBuffer();
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
const extractTitle = (html: string): string => {
|
|
67
|
+
return (
|
|
68
|
+
html.match(/<title>([\s\S]*?)<\/title>/i)?.[1]
|
|
69
|
+
?.replace(/<[^>]+>/g, " ")
|
|
70
|
+
.replace(/\s+/g, " ")
|
|
71
|
+
.trim() || ""
|
|
72
|
+
);
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
const extractBody = (html: string): string => {
|
|
76
|
+
return html.match(/<body[^>]*>([\s\S]*?)<\/body>/i)?.[1] ?? html;
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
export const discoverMarxistsBookPageNames = (
|
|
80
|
+
indexHtml: string,
|
|
81
|
+
maxPages = 80,
|
|
82
|
+
): string[] => {
|
|
83
|
+
const pageNames = [...indexHtml.matchAll(/href=["']?([^"' >#]+\.htm)(#[^"' >]*)?["']?/gi)]
|
|
84
|
+
.map((match) => match[1])
|
|
85
|
+
.filter((href) => !href.includes("/") && !href.startsWith(".."))
|
|
86
|
+
.map((href) => href.trim())
|
|
87
|
+
.filter(Boolean)
|
|
88
|
+
.sort();
|
|
89
|
+
|
|
90
|
+
const uniqueNames = Array.from(new Set(["index.htm", ...pageNames]));
|
|
91
|
+
if (uniqueNames.length <= 1) {
|
|
92
|
+
throw new Error("No same-directory .htm chapter links were found in index.htm.");
|
|
93
|
+
}
|
|
94
|
+
if (uniqueNames.length > maxPages) {
|
|
95
|
+
throw new Error(
|
|
96
|
+
`Refusing to download ${uniqueNames.length} pages; maxPages is ${maxPages}.`,
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
return uniqueNames;
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
const extractStylesheetHref = (html: string): string | null => {
|
|
103
|
+
return (
|
|
104
|
+
html.match(/<link[^>]+href=["']([^"']+\.css)["'][^>]*>/i)?.[1] ?? null
|
|
105
|
+
);
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
const guessMimeType = (url: string): string => {
|
|
109
|
+
const pathname = new URL(url).pathname.toLowerCase();
|
|
110
|
+
if (pathname.endsWith(".gif")) return "image/gif";
|
|
111
|
+
if (pathname.endsWith(".png")) return "image/png";
|
|
112
|
+
if (pathname.endsWith(".jpg") || pathname.endsWith(".jpeg")) return "image/jpeg";
|
|
113
|
+
if (pathname.endsWith(".webp")) return "image/webp";
|
|
114
|
+
if (pathname.endsWith(".css")) return "text/css";
|
|
115
|
+
return "application/octet-stream";
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
export const inlineCssUrlAssets = async (args: {
|
|
119
|
+
css: string;
|
|
120
|
+
cssUrl: string;
|
|
121
|
+
fetchImpl: typeof fetch;
|
|
122
|
+
}): Promise<string> => {
|
|
123
|
+
const replacements = new Map<string, string>();
|
|
124
|
+
const matches = [...args.css.matchAll(/url\((['"]?)([^)'"]+)\1\)/gi)];
|
|
125
|
+
|
|
126
|
+
for (const match of matches) {
|
|
127
|
+
const rawRef = match[2].trim();
|
|
128
|
+
if (!rawRef || /^(data:|https?:)/i.test(rawRef)) continue;
|
|
129
|
+
const assetUrl = new URL(rawRef, args.cssUrl).toString();
|
|
130
|
+
if (!replacements.has(rawRef)) {
|
|
131
|
+
const bytes = await fetchBytes(assetUrl, args.fetchImpl);
|
|
132
|
+
const base64 = Buffer.from(bytes).toString("base64");
|
|
133
|
+
replacements.set(
|
|
134
|
+
rawRef,
|
|
135
|
+
`data:${guessMimeType(assetUrl)};base64,${base64}`,
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return args.css.replace(/url\((['"]?)([^)'"]+)\1\)/gi, (full, quote, rawRef) => {
|
|
141
|
+
const replacement = replacements.get(String(rawRef).trim());
|
|
142
|
+
return replacement ? `url('${replacement}')` : full;
|
|
143
|
+
});
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
const rewriteLocalChapterLinks = (body: string): string => {
|
|
147
|
+
return body.replace(
|
|
148
|
+
/href=["']([^"']+\.htm)(#[^"']*)?["']/gi,
|
|
149
|
+
(_full, href: string, hash: string | undefined) => {
|
|
150
|
+
const anchor = href.replace(/\.htm$/i, "");
|
|
151
|
+
return `href="#${anchor}${hash ? `-${hash.replace(/^#/, "")}` : ""}"`;
|
|
152
|
+
},
|
|
153
|
+
);
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
const buildSingleHtml = (args: {
|
|
157
|
+
title: string;
|
|
158
|
+
css: string;
|
|
159
|
+
pages: Array<MarxistsOfflineBookPage & { body: string }>;
|
|
160
|
+
}): string => {
|
|
161
|
+
const sections = args.pages.map((page) => {
|
|
162
|
+
const anchor = page.name.replace(/\.htm$/i, "");
|
|
163
|
+
return `<section class="chapter" id="${anchor}"><div class="chapter-source">${page.title}</div>${rewriteLocalChapterLinks(page.body)}</section>`;
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
return `<!doctype html>
|
|
167
|
+
<html lang="zh-CN">
|
|
168
|
+
<head>
|
|
169
|
+
<meta charset="utf-8">
|
|
170
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
171
|
+
<title>${args.title}</title>
|
|
172
|
+
<style>
|
|
173
|
+
${args.css}
|
|
174
|
+
body { margin: 0 10px; }
|
|
175
|
+
.table1 { max-width: 960px; width: min(960px, calc(100vw - 20px)); }
|
|
176
|
+
.chapter { page-break-after: always; break-after: page; }
|
|
177
|
+
.chapter-source { display: none; }
|
|
178
|
+
@media screen and (max-width: 700px) {
|
|
179
|
+
html { margin-left: 8px !important; margin-right: 8px !important; }
|
|
180
|
+
body { margin-left: 8px !important; margin-right: 8px !important; }
|
|
181
|
+
.table1 { width: 100% !important; max-width: 100% !important; }
|
|
182
|
+
table { max-width: 100% !important; }
|
|
183
|
+
}
|
|
184
|
+
@media print {
|
|
185
|
+
.chapter { page-break-after: always; }
|
|
186
|
+
}
|
|
187
|
+
</style>
|
|
188
|
+
</head>
|
|
189
|
+
<body id="top">
|
|
190
|
+
${sections.join("\n")}
|
|
191
|
+
</body>
|
|
192
|
+
</html>`;
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
export async function convertMarxistsBookToOfflineHtml(
|
|
196
|
+
args: ConvertMarxistsBookArgs,
|
|
197
|
+
): Promise<MarxistsOfflineBookResult> {
|
|
198
|
+
const startUrl = args.startUrl?.trim();
|
|
199
|
+
if (!startUrl) throw new Error("startUrl is required.");
|
|
200
|
+
|
|
201
|
+
const fetchImpl = args.fetchImpl ?? fetch;
|
|
202
|
+
const encoding = args.encoding || DEFAULT_ENCODING;
|
|
203
|
+
const bookBaseUrl = normalizeBookBaseUrl(startUrl);
|
|
204
|
+
const indexUrl = new URL("index.htm", bookBaseUrl).toString();
|
|
205
|
+
const indexHtml = decodeBytes(await fetchBytes(indexUrl, fetchImpl), encoding);
|
|
206
|
+
const pageNames = discoverMarxistsBookPageNames(indexHtml, args.maxPages);
|
|
207
|
+
|
|
208
|
+
const pages: Array<MarxistsOfflineBookPage & { body: string }> = [];
|
|
209
|
+
for (const name of pageNames) {
|
|
210
|
+
const pageUrl = new URL(name, bookBaseUrl).toString();
|
|
211
|
+
const html = decodeBytes(await fetchBytes(pageUrl, fetchImpl), encoding);
|
|
212
|
+
pages.push({
|
|
213
|
+
name,
|
|
214
|
+
url: pageUrl,
|
|
215
|
+
title: extractTitle(html),
|
|
216
|
+
body: extractBody(html),
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const stylesheetHref =
|
|
221
|
+
extractStylesheetHref(indexHtml) ||
|
|
222
|
+
pages.map((page) => extractStylesheetHref(page.body)).find(Boolean) ||
|
|
223
|
+
"../../MIA01.css";
|
|
224
|
+
const stylesheetUrl = stylesheetHref
|
|
225
|
+
? new URL(stylesheetHref, indexUrl).toString()
|
|
226
|
+
: null;
|
|
227
|
+
const rawCss = stylesheetUrl
|
|
228
|
+
? decodeBytes(await fetchBytes(stylesheetUrl, fetchImpl), encoding)
|
|
229
|
+
: "";
|
|
230
|
+
const css = stylesheetUrl
|
|
231
|
+
? await inlineCssUrlAssets({ css: rawCss, cssUrl: stylesheetUrl, fetchImpl })
|
|
232
|
+
: rawCss;
|
|
233
|
+
|
|
234
|
+
const title = extractTitle(indexHtml) || pages[0]?.title || "Offline Book";
|
|
235
|
+
const html = buildSingleHtml({ title, css, pages });
|
|
236
|
+
const byteLength = new TextEncoder().encode(html).byteLength;
|
|
237
|
+
|
|
238
|
+
return {
|
|
239
|
+
title,
|
|
240
|
+
startUrl,
|
|
241
|
+
indexUrl,
|
|
242
|
+
stylesheetUrl,
|
|
243
|
+
pages: pages.map(({ body: _body, ...page }) => page),
|
|
244
|
+
html,
|
|
245
|
+
validation: {
|
|
246
|
+
pageCount: pages.length,
|
|
247
|
+
hasEmbeddedAssets: html.includes("data:image/"),
|
|
248
|
+
hasNetworkUrls: /https?:\/\//i.test(html),
|
|
249
|
+
hasUtf8Meta: html.includes('<meta charset="utf-8">'),
|
|
250
|
+
byteLength,
|
|
251
|
+
},
|
|
252
|
+
};
|
|
253
|
+
}
|
|
@@ -52,11 +52,13 @@ export const queryModelUsageFunctionSchema = {
|
|
|
52
52
|
},
|
|
53
53
|
thresholdCredits: {
|
|
54
54
|
type: "number",
|
|
55
|
-
description:
|
|
55
|
+
description:
|
|
56
|
+
"可选阈值,单位 credits;返回 threshold.exceeded 与 0-100 的 threshold.usedPercent。",
|
|
56
57
|
},
|
|
57
58
|
thresholdUsd: {
|
|
58
59
|
type: "number",
|
|
59
|
-
description:
|
|
60
|
+
description:
|
|
61
|
+
"可选阈值,单位 USD;会按 creditsPerUsd 换算,并返回 0-100 的 threshold.usedPercent。",
|
|
60
62
|
},
|
|
61
63
|
},
|
|
62
64
|
},
|
package/ai/tools/readTool.ts
CHANGED
|
@@ -9,6 +9,7 @@ import { isTableMetaKey, rowKey, isAgentKey } from "database/keys";
|
|
|
9
9
|
import { DataType } from "create/types";
|
|
10
10
|
import { TableMeta } from "render/table/types";
|
|
11
11
|
import { fetchAndSerializeTable } from "render/table/utils/tableSerialization";
|
|
12
|
+
import { readFileFunc } from "./readFileTool";
|
|
12
13
|
|
|
13
14
|
// ---- Types ----
|
|
14
15
|
|
|
@@ -24,6 +25,13 @@ export type ReadArgs = {
|
|
|
24
25
|
* - true:使用 readAndWaitAction,等待远程与本地决策后返回“权威”结果
|
|
25
26
|
*/
|
|
26
27
|
waitRemote?: boolean;
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Backward-compatible escape hatch for models that call read with a local
|
|
31
|
+
* file path even when the readFile tool is available.
|
|
32
|
+
*/
|
|
33
|
+
filePath?: string;
|
|
34
|
+
path?: string;
|
|
27
35
|
};
|
|
28
36
|
|
|
29
37
|
// ---- 工具 Schema,供 LLM 调用 ----
|
|
@@ -68,6 +76,29 @@ export async function readFunc(
|
|
|
68
76
|
context?: { parentMessageId?: string; signal?: AbortSignal }
|
|
69
77
|
): Promise<{ rawData: any; displayData?: string }> {
|
|
70
78
|
const { dbKey, waitRemote = false } = args || {};
|
|
79
|
+
const localFilePath =
|
|
80
|
+
typeof args?.filePath === "string"
|
|
81
|
+
? args.filePath
|
|
82
|
+
: typeof args?.path === "string"
|
|
83
|
+
? args.path
|
|
84
|
+
: "";
|
|
85
|
+
|
|
86
|
+
if (!dbKey && localFilePath) {
|
|
87
|
+
const result = await readFileFunc(
|
|
88
|
+
{
|
|
89
|
+
filePath: localFilePath,
|
|
90
|
+
startLine: (args as any).startLine,
|
|
91
|
+
endLine: (args as any).endLine,
|
|
92
|
+
},
|
|
93
|
+
thunkApi,
|
|
94
|
+
context
|
|
95
|
+
);
|
|
96
|
+
return {
|
|
97
|
+
rawData: result,
|
|
98
|
+
displayData:
|
|
99
|
+
typeof result === "string" ? result : JSON.stringify(result, null, 2),
|
|
100
|
+
};
|
|
101
|
+
}
|
|
71
102
|
|
|
72
103
|
if (!dbKey || typeof dbKey !== "string") {
|
|
73
104
|
throw new Error("read 工具需要提供字符串类型的 id(dbKey)。");
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
|
|
15
15
|
import type { RootState } from "app/store";
|
|
16
16
|
import { selectCurrentServer } from "app/settings/settingSlice";
|
|
17
|
+
import { getIsDesktopApp } from "app/utils/env";
|
|
17
18
|
import { selectCurrentToken } from "auth/authSlice";
|
|
18
19
|
import { selectCurrentDialogKey } from "chat/dialog/dialogSlice";
|
|
19
20
|
import { extractCustomId } from "core/prefix";
|
|
@@ -39,6 +40,34 @@ export const resolveToolBaseUrl = (currentServer?: string | null): string => {
|
|
|
39
40
|
return currentServer.replace(/\/+$/, "");
|
|
40
41
|
};
|
|
41
42
|
|
|
43
|
+
const DESKTOP_LOCAL_TOOL_PATHS = new Set([
|
|
44
|
+
"/api/exec-shell",
|
|
45
|
+
"/api/check-env",
|
|
46
|
+
"/api/read-file",
|
|
47
|
+
"/api/write-file",
|
|
48
|
+
"/api/apply-edit",
|
|
49
|
+
"/api/apply-line-edits",
|
|
50
|
+
"/api/code-search",
|
|
51
|
+
"/api/search-repo",
|
|
52
|
+
]);
|
|
53
|
+
|
|
54
|
+
const getWindowOrigin = (): string | null => {
|
|
55
|
+
const _window = (globalThis as any).window;
|
|
56
|
+
const origin = _window?.location?.origin;
|
|
57
|
+
return typeof origin === "string" && origin ? origin.replace(/\/+$/, "") : null;
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
export const resolveToolApiBaseUrl = (
|
|
61
|
+
currentServer?: string | null,
|
|
62
|
+
path?: string
|
|
63
|
+
): string => {
|
|
64
|
+
if (path && DESKTOP_LOCAL_TOOL_PATHS.has(path) && getIsDesktopApp()) {
|
|
65
|
+
const localOrigin = getWindowOrigin();
|
|
66
|
+
if (localOrigin) return localOrigin;
|
|
67
|
+
}
|
|
68
|
+
return resolveToolBaseUrl(currentServer);
|
|
69
|
+
};
|
|
70
|
+
|
|
42
71
|
export const getRequestConfig = (thunkApi: any): RequestConfig => {
|
|
43
72
|
const state = thunkApi.getState() as RootState;
|
|
44
73
|
const currentServer = selectCurrentServer(state);
|
|
@@ -118,6 +147,9 @@ export const buildToolRequestHeaders = (
|
|
|
118
147
|
if (agentKey && typeof agentKey === "string" && agentKey.trim()) {
|
|
119
148
|
headers["X-Nolo-Agent-Key"] = agentKey.trim();
|
|
120
149
|
}
|
|
150
|
+
if (typeof window !== "undefined" && (window as any).__NOLO_DESKTOP__) {
|
|
151
|
+
headers["X-Nolo-Desktop-Tool"] = "1";
|
|
152
|
+
}
|
|
121
153
|
return headers;
|
|
122
154
|
};
|
|
123
155
|
|
|
@@ -152,7 +184,9 @@ export async function callToolApi<T = any>(
|
|
|
152
184
|
): Promise<T> {
|
|
153
185
|
const { currentServer } = getRequestConfig(thunkApi);
|
|
154
186
|
|
|
155
|
-
const
|
|
187
|
+
const baseUrl = resolveToolApiBaseUrl(currentServer, path);
|
|
188
|
+
if (!baseUrl) throw new Error("无法获取工具服务器地址。");
|
|
189
|
+
const url = `${baseUrl}${path}`;
|
|
156
190
|
const headers = buildToolRequestHeaders(thunkApi, options);
|
|
157
191
|
|
|
158
192
|
const response = await fetch(url, {
|
|
@@ -15,6 +15,19 @@ const LEGACY_TOOL_NAME_ALIASES: Record<string, string> = {
|
|
|
15
15
|
read_doc: "readDoc",
|
|
16
16
|
createdoc: "createDoc",
|
|
17
17
|
updateDocTool: "updateDoc",
|
|
18
|
+
readLocal: "readLocalFile",
|
|
19
|
+
read_local: "readLocalFile",
|
|
20
|
+
readLocalTool: "readLocalFile",
|
|
21
|
+
listLocal: "listLocalFiles",
|
|
22
|
+
list_local: "listLocalFiles",
|
|
23
|
+
terminalCommand: "execShell",
|
|
24
|
+
terminal_command: "execShell",
|
|
25
|
+
runCommand: "execShell",
|
|
26
|
+
run_command: "execShell",
|
|
27
|
+
runInBash: "execShell",
|
|
28
|
+
run_in_bash: "execShell",
|
|
29
|
+
executeCommand: "execShell",
|
|
30
|
+
execute_command: "execShell",
|
|
18
31
|
};
|
|
19
32
|
|
|
20
33
|
const normalizeToolName = (name: string): string =>
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { CliCommandHandler, CliRuntimeContext } from "./cliCommandTypes";
|
|
2
|
+
|
|
3
|
+
export function withCliEnv<TDeps extends { env: NodeJS.ProcessEnv }>(
|
|
4
|
+
command: (args: string[], deps: TDeps) => Promise<number>
|
|
5
|
+
): CliCommandHandler {
|
|
6
|
+
return (args: string[], ctx: CliRuntimeContext) =>
|
|
7
|
+
command(args, { env: ctx.env } as TDeps);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function withCliEnvAndScriptDir<TDeps extends { env: NodeJS.ProcessEnv; scriptDir: string }>(
|
|
11
|
+
command: (args: string[], deps: TDeps) => Promise<number>
|
|
12
|
+
): CliCommandHandler {
|
|
13
|
+
return (args: string[], ctx: CliRuntimeContext) =>
|
|
14
|
+
command(args, {
|
|
15
|
+
env: ctx.env,
|
|
16
|
+
scriptDir: ctx.scriptDir,
|
|
17
|
+
} as TDeps);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function withCliEntrypointEnv<TDeps extends { env: NodeJS.ProcessEnv; cliEntrypointPath: string }>(
|
|
21
|
+
command: (args: string[], deps: TDeps) => Promise<number>
|
|
22
|
+
): CliCommandHandler {
|
|
23
|
+
return (args: string[], ctx: CliRuntimeContext) =>
|
|
24
|
+
command(args, {
|
|
25
|
+
env: ctx.env,
|
|
26
|
+
cliEntrypointPath: ctx.entrypointPath,
|
|
27
|
+
} as TDeps);
|
|
28
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { fileURLToPath } from "node:url";
|
|
2
|
+
import type { CliPackageInfo, CliRuntimeContext, CommandEntry } from "./cliCommandTypes";
|
|
3
|
+
|
|
4
|
+
export async function runResolvedCommand(
|
|
5
|
+
command: CommandEntry,
|
|
6
|
+
originalArgs: string[],
|
|
7
|
+
ctx: CliRuntimeContext,
|
|
8
|
+
deps: {
|
|
9
|
+
runScript: (script: string, forwardedArgs: string[], env: NodeJS.ProcessEnv) => Promise<number>;
|
|
10
|
+
}
|
|
11
|
+
) {
|
|
12
|
+
if (command.kind === "internal") {
|
|
13
|
+
if (!command.handler) {
|
|
14
|
+
throw new Error(`Internal command ${command.path.join(" ")} is missing a handler.`);
|
|
15
|
+
}
|
|
16
|
+
return command.handler(originalArgs.slice(command.path.length), ctx);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
if (!command.script) {
|
|
20
|
+
throw new Error(`Script command ${command.path.join(" ")} is missing a script.`);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const forwardedArgs = [...(command.fixedArgs ?? []), ...originalArgs.slice(command.path.length)];
|
|
24
|
+
return deps.runScript(command.script, forwardedArgs, ctx.env);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function readOption(args: string[], flag: string) {
|
|
28
|
+
const index = args.indexOf(flag);
|
|
29
|
+
return index >= 0 ? args[index + 1] : undefined;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function buildDaemonEnv(args: string[], runtimeEnv: NodeJS.ProcessEnv) {
|
|
33
|
+
const serverUrl = readOption(args, "--server-url") || readOption(args, "--server");
|
|
34
|
+
const apiKey = readOption(args, "--api-key") || readOption(args, "--token");
|
|
35
|
+
return {
|
|
36
|
+
...runtimeEnv,
|
|
37
|
+
...(serverUrl ? { NOLO_SERVER: serverUrl } : {}),
|
|
38
|
+
...(apiKey ? { AUTH_TOKEN: apiKey } : {}),
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function looksLikeDaemonShortcut(args: string[]) {
|
|
43
|
+
return args.includes("--server-url") || args.includes("--api-key");
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function createCliRuntimeContext(args: {
|
|
47
|
+
env: NodeJS.ProcessEnv;
|
|
48
|
+
scriptDir: string;
|
|
49
|
+
entrypointPath?: string;
|
|
50
|
+
packageInfo: CliPackageInfo;
|
|
51
|
+
}): CliRuntimeContext {
|
|
52
|
+
return {
|
|
53
|
+
env: args.env,
|
|
54
|
+
scriptDir: args.scriptDir,
|
|
55
|
+
entrypointPath: args.entrypointPath ?? fileURLToPath(import.meta.url),
|
|
56
|
+
packageInfo: args.packageInfo,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function withDaemonShortcutArgs(args: string[], runtimeEnv: NodeJS.ProcessEnv) {
|
|
61
|
+
if (!looksLikeDaemonShortcut(args)) return { commandArgs: args, envOverride: null as NodeJS.ProcessEnv | null };
|
|
62
|
+
return {
|
|
63
|
+
commandArgs: ["daemon", ...args],
|
|
64
|
+
envOverride: buildDaemonEnv(args, runtimeEnv),
|
|
65
|
+
};
|
|
66
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import {
|
|
2
|
+
withCliEntrypointEnv,
|
|
3
|
+
withCliEnv,
|
|
4
|
+
withCliEnvAndScriptDir,
|
|
5
|
+
} from "./cliCommandAdapters";
|
|
6
|
+
import {
|
|
7
|
+
createInternalCommand,
|
|
8
|
+
type CliCommandHandler,
|
|
9
|
+
type CommandEntry,
|
|
10
|
+
} from "./cliCommandTypes";
|
|
11
|
+
|
|
12
|
+
export function createEnvCommand<TDeps extends { env: NodeJS.ProcessEnv }>(
|
|
13
|
+
path: string[],
|
|
14
|
+
description: string,
|
|
15
|
+
command: (args: string[], deps: TDeps) => Promise<number>
|
|
16
|
+
): CommandEntry {
|
|
17
|
+
return createInternalCommand(path, description, withCliEnv(command) as CliCommandHandler);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function createEnvScriptDirCommand<TDeps extends { env: NodeJS.ProcessEnv; scriptDir: string }>(
|
|
21
|
+
path: string[],
|
|
22
|
+
description: string,
|
|
23
|
+
command: (args: string[], deps: TDeps) => Promise<number>
|
|
24
|
+
): CommandEntry {
|
|
25
|
+
return createInternalCommand(path, description, withCliEnvAndScriptDir(command) as CliCommandHandler);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function createEntrypointEnvCommand<TDeps extends { env: NodeJS.ProcessEnv; cliEntrypointPath: string }>(
|
|
29
|
+
path: string[],
|
|
30
|
+
description: string,
|
|
31
|
+
command: (args: string[], deps: TDeps) => Promise<number>
|
|
32
|
+
): CommandEntry {
|
|
33
|
+
return createInternalCommand(path, description, withCliEntrypointEnv(command) as CliCommandHandler);
|
|
34
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import type { readPackageInfo } from "./updateCommands";
|
|
2
|
+
|
|
3
|
+
export type CliPackageInfo = ReturnType<typeof readPackageInfo>;
|
|
4
|
+
|
|
5
|
+
export type CliRuntimeContext = {
|
|
6
|
+
env: NodeJS.ProcessEnv;
|
|
7
|
+
scriptDir: string;
|
|
8
|
+
entrypointPath: string;
|
|
9
|
+
packageInfo: CliPackageInfo;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export type CliCommandHandler = (args: string[], ctx: CliRuntimeContext) => Promise<number>;
|
|
13
|
+
|
|
14
|
+
export type CommandEntry = {
|
|
15
|
+
path: string[];
|
|
16
|
+
description: string;
|
|
17
|
+
kind: "internal" | "script";
|
|
18
|
+
script?: string;
|
|
19
|
+
fixedArgs?: string[];
|
|
20
|
+
handler?: CliCommandHandler;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export function createScriptCommand(
|
|
24
|
+
path: string[],
|
|
25
|
+
script: string,
|
|
26
|
+
description: string,
|
|
27
|
+
fixedArgs?: string[]
|
|
28
|
+
): CommandEntry {
|
|
29
|
+
return {
|
|
30
|
+
path,
|
|
31
|
+
kind: "script",
|
|
32
|
+
script,
|
|
33
|
+
description,
|
|
34
|
+
...(fixedArgs ? { fixedArgs } : {}),
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function createInternalCommand(
|
|
39
|
+
path: string[],
|
|
40
|
+
description: string,
|
|
41
|
+
handler: CliCommandHandler
|
|
42
|
+
): CommandEntry {
|
|
43
|
+
return {
|
|
44
|
+
path,
|
|
45
|
+
kind: "internal",
|
|
46
|
+
description,
|
|
47
|
+
handler,
|
|
48
|
+
};
|
|
49
|
+
}
|