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
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import { DEFAULT_MODEL } from "ai/llm/providers";
|
|
2
|
+
import {
|
|
3
|
+
GUIDED_AGENT_CAPABILITIES,
|
|
4
|
+
mapCapabilityIdsToToolIds,
|
|
5
|
+
} from "ai/agent/guidedCreation/capabilities";
|
|
6
|
+
import type {
|
|
7
|
+
GuidedAgentCapabilityId,
|
|
8
|
+
GuidedAgentDraft,
|
|
9
|
+
GuidedAgentReferenceChoice,
|
|
10
|
+
} from "ai/agent/guidedCreation/types";
|
|
11
|
+
|
|
12
|
+
export type PrepareAgentDraftToolArgs = Partial<GuidedAgentDraft>;
|
|
13
|
+
|
|
14
|
+
const capabilityIds = new Set(Object.keys(GUIDED_AGENT_CAPABILITIES));
|
|
15
|
+
|
|
16
|
+
const trim = (value: unknown) =>
|
|
17
|
+
typeof value === "string" ? value.trim() : "";
|
|
18
|
+
|
|
19
|
+
const stringArray = (value: unknown): string[] =>
|
|
20
|
+
Array.isArray(value)
|
|
21
|
+
? value.map((item) => trim(item)).filter(Boolean)
|
|
22
|
+
: [];
|
|
23
|
+
|
|
24
|
+
const guidedCapabilityArray = (value: unknown): GuidedAgentCapabilityId[] =>
|
|
25
|
+
stringArray(value).filter((id): id is GuidedAgentCapabilityId =>
|
|
26
|
+
capabilityIds.has(id)
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
const referencesArray = (value: unknown): GuidedAgentReferenceChoice[] =>
|
|
30
|
+
Array.isArray(value)
|
|
31
|
+
? value
|
|
32
|
+
.filter((item): item is Record<string, unknown> =>
|
|
33
|
+
!!item && typeof item === "object" && !Array.isArray(item)
|
|
34
|
+
)
|
|
35
|
+
.map((item) => ({
|
|
36
|
+
dbKey: trim(item.dbKey),
|
|
37
|
+
title: trim(item.title),
|
|
38
|
+
type: item.type === "instruction" ? "instruction" : "knowledge",
|
|
39
|
+
selected: item.selected === true,
|
|
40
|
+
...(trim(item.reason) ? { reason: trim(item.reason) } : {}),
|
|
41
|
+
}))
|
|
42
|
+
.filter((item) => item.dbKey)
|
|
43
|
+
: [];
|
|
44
|
+
|
|
45
|
+
const buildPrompt = (args: PrepareAgentDraftToolArgs) => {
|
|
46
|
+
const prompt = trim(args.prompt);
|
|
47
|
+
if (prompt) return prompt;
|
|
48
|
+
const name = trim(args.name) || "用户的专属 AI";
|
|
49
|
+
const summary = trim(args.promptSummary) || trim(args.introduction);
|
|
50
|
+
return [
|
|
51
|
+
`你是${name}。`,
|
|
52
|
+
summary || "你需要理解用户目标,并给出清晰、可执行、有边界的帮助。",
|
|
53
|
+
"如果信息不足,先提出必要澄清问题;不要编造用户没有确认的资料来源。",
|
|
54
|
+
].join("\n");
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
export const buildPreparedAgentDraft = (
|
|
58
|
+
args: PrepareAgentDraftToolArgs
|
|
59
|
+
): GuidedAgentDraft => {
|
|
60
|
+
const selectedCapabilities = guidedCapabilityArray(args.capabilityIds);
|
|
61
|
+
const providedToolIds = stringArray(args.toolIds);
|
|
62
|
+
const mappedToolIds = mapCapabilityIdsToToolIds(selectedCapabilities);
|
|
63
|
+
return {
|
|
64
|
+
name: trim(args.name),
|
|
65
|
+
introduction: trim(args.introduction),
|
|
66
|
+
prompt: buildPrompt(args),
|
|
67
|
+
promptSummary: trim(args.promptSummary),
|
|
68
|
+
provider: trim(args.provider) || DEFAULT_MODEL.provider,
|
|
69
|
+
model: trim(args.model) || DEFAULT_MODEL.name,
|
|
70
|
+
isPublic: args.isPublic === true,
|
|
71
|
+
capabilityIds: selectedCapabilities,
|
|
72
|
+
toolIds: Array.from(new Set([...providedToolIds, ...mappedToolIds])),
|
|
73
|
+
references: referencesArray(args.references),
|
|
74
|
+
tags: stringArray(args.tags),
|
|
75
|
+
unresolved: stringArray(args.unresolved),
|
|
76
|
+
};
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
export const prepareAgentDraftToolFunctionSchema = {
|
|
80
|
+
name: "prepareAgentDraft",
|
|
81
|
+
description:
|
|
82
|
+
"整理一个可预览的 Agent 创建草稿。只生成草稿,不创建真实 Agent 记录。",
|
|
83
|
+
parameters: {
|
|
84
|
+
type: "object",
|
|
85
|
+
properties: {
|
|
86
|
+
name: { type: "string", description: "建议的 Agent 名称。" },
|
|
87
|
+
introduction: { type: "string", description: "面向用户展示的简介。" },
|
|
88
|
+
prompt: { type: "string", description: "完整系统 prompt 草稿。" },
|
|
89
|
+
promptSummary: { type: "string", description: "prompt 的简短摘要。" },
|
|
90
|
+
provider: { type: "string", description: "建议 provider。" },
|
|
91
|
+
model: { type: "string", description: "建议模型名称。" },
|
|
92
|
+
isPublic: { type: "boolean", description: "是否建议公开。" },
|
|
93
|
+
capabilityIds: {
|
|
94
|
+
type: "array",
|
|
95
|
+
items: {
|
|
96
|
+
type: "string",
|
|
97
|
+
enum: Object.keys(GUIDED_AGENT_CAPABILITIES),
|
|
98
|
+
},
|
|
99
|
+
description: "已确认或高度确定的能力标签。",
|
|
100
|
+
},
|
|
101
|
+
toolIds: {
|
|
102
|
+
type: "array",
|
|
103
|
+
items: { type: "string" },
|
|
104
|
+
description: "可选的底层工具名;通常由 capabilityIds 推导。",
|
|
105
|
+
},
|
|
106
|
+
references: {
|
|
107
|
+
type: "array",
|
|
108
|
+
items: {
|
|
109
|
+
type: "object",
|
|
110
|
+
properties: {
|
|
111
|
+
dbKey: { type: "string" },
|
|
112
|
+
title: { type: "string" },
|
|
113
|
+
type: { type: "string", enum: ["knowledge", "instruction"] },
|
|
114
|
+
selected: { type: "boolean" },
|
|
115
|
+
reason: { type: "string" },
|
|
116
|
+
},
|
|
117
|
+
required: ["dbKey"],
|
|
118
|
+
},
|
|
119
|
+
description: "建议挂载的知识引用,默认等待用户确认。",
|
|
120
|
+
},
|
|
121
|
+
tags: {
|
|
122
|
+
type: "array",
|
|
123
|
+
items: { type: "string" },
|
|
124
|
+
},
|
|
125
|
+
unresolved: {
|
|
126
|
+
type: "array",
|
|
127
|
+
items: { type: "string" },
|
|
128
|
+
description: "仍需确认的字段或问题。",
|
|
129
|
+
},
|
|
130
|
+
},
|
|
131
|
+
required: ["name", "introduction", "promptSummary"],
|
|
132
|
+
},
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
export async function prepareAgentDraftToolFunc(
|
|
136
|
+
args: PrepareAgentDraftToolArgs
|
|
137
|
+
): Promise<{
|
|
138
|
+
rawData: { draft: GuidedAgentDraft; createUrl: string };
|
|
139
|
+
displayData: string;
|
|
140
|
+
}> {
|
|
141
|
+
const draft = buildPreparedAgentDraft(args);
|
|
142
|
+
const missing = draft.unresolved.length
|
|
143
|
+
? `\n- 待确认: ${draft.unresolved.join(", ")}`
|
|
144
|
+
: "";
|
|
145
|
+
return {
|
|
146
|
+
rawData: {
|
|
147
|
+
draft,
|
|
148
|
+
createUrl: "/create/agent",
|
|
149
|
+
},
|
|
150
|
+
displayData: [
|
|
151
|
+
`Agent 草稿: ${draft.name || "未命名 AI"}`,
|
|
152
|
+
draft.promptSummary ? `- 摘要: ${draft.promptSummary}` : "",
|
|
153
|
+
draft.capabilityIds.length
|
|
154
|
+
? `- 能力: ${draft.capabilityIds.join(", ")}`
|
|
155
|
+
: "",
|
|
156
|
+
missing,
|
|
157
|
+
].filter(Boolean).join("\n"),
|
|
158
|
+
};
|
|
159
|
+
}
|
package/ai/tools/checkEnvTool.ts
CHANGED
|
@@ -3,39 +3,60 @@
|
|
|
3
3
|
|
|
4
4
|
import { callToolApi } from "./toolApiClient";
|
|
5
5
|
|
|
6
|
-
export type CheckEnvArgs = {
|
|
7
|
-
check?: "build" | "context";
|
|
8
|
-
|
|
6
|
+
export type CheckEnvArgs = {
|
|
7
|
+
check?: "build" | "context";
|
|
8
|
+
key?: string;
|
|
9
|
+
};
|
|
9
10
|
|
|
10
11
|
export const checkEnvFunctionSchema = {
|
|
11
12
|
name: "checkEnv",
|
|
12
13
|
description: [
|
|
13
|
-
"执行环境检查并返回结构化结果。",
|
|
14
|
-
"check='context' 可查看当前平台、工作目录与可用 shell;check='build' 可快速验证是否可编译。",
|
|
14
|
+
"执行环境检查并返回结构化结果。",
|
|
15
|
+
"check='context' 可查看当前平台、工作目录与可用 shell;check='build' 可快速验证是否可编译。",
|
|
16
|
+
"key 可检查某个本地工具,例如 key='gemini' 会探测本机 Gemini CLI 与 npm 最新版本。",
|
|
15
17
|
].join("\n"),
|
|
16
18
|
parameters: {
|
|
17
19
|
type: "object",
|
|
18
20
|
properties: {
|
|
19
|
-
check: {
|
|
20
|
-
type: "string",
|
|
21
|
-
enum: ["build", "context"],
|
|
22
|
-
description: "检查项:context 返回当前运行环境;build 执行构建检查。",
|
|
23
|
-
},
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
21
|
+
check: {
|
|
22
|
+
type: "string",
|
|
23
|
+
enum: ["build", "context"],
|
|
24
|
+
description: "检查项:context 返回当前运行环境;build 执行构建检查。",
|
|
25
|
+
},
|
|
26
|
+
key: {
|
|
27
|
+
type: "string",
|
|
28
|
+
enum: ["gemini", "node", "npm", "bun", "python", "git", "shell"],
|
|
29
|
+
description: "可选:探测某个本地工具,不执行 build。常用值如 gemini、node、npm、bun、python、git、shell。",
|
|
30
|
+
},
|
|
31
|
+
},
|
|
32
|
+
},
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const formatProbeOutput = (probe: any): string => {
|
|
36
|
+
const status = probe?.ok ? "OK" : "FAIL";
|
|
37
|
+
const lines = [`- [${status}] ${probe?.label ?? "probe"}`];
|
|
38
|
+
const stdout = typeof probe?.stdout === "string" ? probe.stdout.trim() : "";
|
|
39
|
+
const stderr = typeof probe?.stderr === "string" ? probe.stderr.trim() : "";
|
|
40
|
+
const error = typeof probe?.error === "string" ? probe.error.trim() : "";
|
|
41
|
+
if (stdout) lines.push(` stdout: ${stdout}`);
|
|
42
|
+
if (stderr) lines.push(` stderr: ${stderr}`);
|
|
43
|
+
if (error) lines.push(` error: ${error}`);
|
|
44
|
+
return lines.join("\n");
|
|
45
|
+
};
|
|
27
46
|
|
|
28
47
|
export async function checkEnvFunc(
|
|
29
48
|
args: CheckEnvArgs,
|
|
30
49
|
thunkApi: any,
|
|
31
50
|
context?: { agentKey?: string }
|
|
32
51
|
): Promise<{ rawData: any; displayData: string }> {
|
|
33
|
-
const
|
|
34
|
-
|
|
52
|
+
const key = typeof args?.key === "string" ? args.key.trim().toLowerCase() : "";
|
|
53
|
+
const check = args?.check === "context" ? "context" : "build";
|
|
54
|
+
const payload = key ? { key } : { check };
|
|
55
|
+
|
|
35
56
|
const result = await callToolApi<any>(
|
|
36
57
|
thunkApi,
|
|
37
58
|
"/api/check-env",
|
|
38
|
-
|
|
59
|
+
payload,
|
|
39
60
|
{
|
|
40
61
|
withAuth: true,
|
|
41
62
|
agentKey: context?.agentKey,
|
|
@@ -45,7 +66,18 @@ export async function checkEnvFunc(
|
|
|
45
66
|
const ok = !!result?.ok;
|
|
46
67
|
const exitCode = typeof result?.exitCode === "number" ? result.exitCode : -1;
|
|
47
68
|
|
|
48
|
-
if (check === "
|
|
69
|
+
if (result?.check === "tool") {
|
|
70
|
+
const probes = Array.isArray(result?.probes) ? result.probes : [];
|
|
71
|
+
return {
|
|
72
|
+
rawData: result,
|
|
73
|
+
displayData: [
|
|
74
|
+
`本地工具检查: ${result?.key ?? key}`,
|
|
75
|
+
...probes.map(formatProbeOutput),
|
|
76
|
+
].join("\n"),
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (check === "context") {
|
|
49
81
|
const displayParts = [
|
|
50
82
|
`平台: ${result?.platform ?? "unknown"}`,
|
|
51
83
|
`cwd: ${result?.cwd ?? "unknown"}`,
|
|
@@ -59,9 +91,9 @@ export async function checkEnvFunc(
|
|
|
59
91
|
rawData: result,
|
|
60
92
|
displayData: `🧭 当前环境\n${displayParts.join("\n")}`,
|
|
61
93
|
};
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
return {
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return {
|
|
65
97
|
rawData: result,
|
|
66
98
|
displayData: ok
|
|
67
99
|
? `✅ 环境检查通过: ${check}`
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { convertMarxistsBookToOfflineHtml } from "./marxistsOfflineBook";
|
|
2
|
+
|
|
3
|
+
export const convertMarxistsBookToOfflineHtmlFunctionSchema = {
|
|
4
|
+
name: "convertMarxistsBookToOfflineHtml",
|
|
5
|
+
description:
|
|
6
|
+
"将 Marxists.org 中文旧式书籍页面转换成离线单 HTML。会从章节 URL 推导目录页,下载同目录章节、CSS 和背景图,保留原始排版并内嵌资源,适合 iPad/移动设备离线阅读。",
|
|
7
|
+
parameters: {
|
|
8
|
+
type: "object",
|
|
9
|
+
properties: {
|
|
10
|
+
startUrl: {
|
|
11
|
+
type: "string",
|
|
12
|
+
description:
|
|
13
|
+
"任意一个 Marxists.org 中文书籍章节 URL,例如 https://www.marxists.org/chinese/fromm/1973/01.htm。",
|
|
14
|
+
},
|
|
15
|
+
encoding: {
|
|
16
|
+
type: "string",
|
|
17
|
+
description: "源站 HTML/CSS 编码,老中文页面通常是 gb2312。默认 gb2312。",
|
|
18
|
+
default: "gb2312",
|
|
19
|
+
},
|
|
20
|
+
outputMode: {
|
|
21
|
+
type: "string",
|
|
22
|
+
enum: ["summary", "html"],
|
|
23
|
+
description:
|
|
24
|
+
"summary 只返回元数据和文件引用;html 同时返回完整 HTML 字符串。需要直接拿到文件内容时用 html。",
|
|
25
|
+
default: "summary",
|
|
26
|
+
},
|
|
27
|
+
singleFileName: {
|
|
28
|
+
type: "string",
|
|
29
|
+
description:
|
|
30
|
+
"生成文件建议名称,默认根据书名或 URL 生成;server-side agent 可用它作为保存文件名。",
|
|
31
|
+
},
|
|
32
|
+
maxPages: {
|
|
33
|
+
type: "number",
|
|
34
|
+
description: "最多允许抓取的同目录章节页数量,默认 80,防止误抓整个站点。",
|
|
35
|
+
default: 80,
|
|
36
|
+
},
|
|
37
|
+
saveAsFile: {
|
|
38
|
+
type: "boolean",
|
|
39
|
+
description:
|
|
40
|
+
"server-side agent 可设为 true,将 HTML 保存为 Nolo 文件并返回下载 URL;如果只需要 HTML 字符串,可设为 false。",
|
|
41
|
+
default: true,
|
|
42
|
+
},
|
|
43
|
+
},
|
|
44
|
+
required: ["startUrl"],
|
|
45
|
+
},
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
export type ConvertMarxistsBookToolArgs = {
|
|
49
|
+
startUrl: string;
|
|
50
|
+
encoding?: string;
|
|
51
|
+
outputMode?: "summary" | "html";
|
|
52
|
+
singleFileName?: string;
|
|
53
|
+
maxPages?: number;
|
|
54
|
+
saveAsFile?: boolean;
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
export async function convertMarxistsBookToOfflineHtmlFunc(
|
|
58
|
+
args: ConvertMarxistsBookToolArgs,
|
|
59
|
+
): Promise<{ rawData: Record<string, unknown>; displayData: string }> {
|
|
60
|
+
const result = await convertMarxistsBookToOfflineHtml({
|
|
61
|
+
startUrl: args.startUrl,
|
|
62
|
+
encoding: args.encoding,
|
|
63
|
+
maxPages: args.maxPages,
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
const rawData: Record<string, unknown> = {
|
|
67
|
+
title: result.title,
|
|
68
|
+
startUrl: result.startUrl,
|
|
69
|
+
indexUrl: result.indexUrl,
|
|
70
|
+
stylesheetUrl: result.stylesheetUrl,
|
|
71
|
+
pages: result.pages,
|
|
72
|
+
validation: result.validation,
|
|
73
|
+
fileName: args.singleFileName,
|
|
74
|
+
};
|
|
75
|
+
if (args.outputMode === "html") {
|
|
76
|
+
rawData.html = result.html;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return {
|
|
80
|
+
rawData,
|
|
81
|
+
displayData: formatMarxistsOfflineBookDisplayData({
|
|
82
|
+
...rawData,
|
|
83
|
+
htmlIncluded: args.outputMode === "html",
|
|
84
|
+
}),
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function formatMarxistsOfflineBookDisplayData(data: Record<string, unknown>): string {
|
|
89
|
+
const validation = (data.validation ?? {}) as any;
|
|
90
|
+
const pages = Array.isArray(data.pages) ? data.pages : [];
|
|
91
|
+
return [
|
|
92
|
+
"✅ 已生成离线 HTML",
|
|
93
|
+
`书名:${data.title || "未知"}`,
|
|
94
|
+
`章节数:${validation.pageCount ?? pages.length}`,
|
|
95
|
+
`大小:${validation.byteLength ?? "未知"} bytes`,
|
|
96
|
+
`资源内嵌:${validation.hasEmbeddedAssets ? "是" : "否"}`,
|
|
97
|
+
`残留网络 URL:${validation.hasNetworkUrls ? "是" : "否"}`,
|
|
98
|
+
data.fileUrl ? `文件:${data.fileUrl}` : undefined,
|
|
99
|
+
data.htmlIncluded ? "已在 rawData.html 返回完整 HTML。" : undefined,
|
|
100
|
+
]
|
|
101
|
+
.filter(Boolean)
|
|
102
|
+
.join("\n");
|
|
103
|
+
}
|
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
import { DEFAULT_LOCAL_API_ORIGIN } from "core/localOrigins";
|
|
2
|
-
import { callToolApi } from "./toolApiClient";
|
|
1
|
+
import { DEFAULT_LOCAL_API_ORIGIN } from "core/localOrigins";
|
|
2
|
+
import { callToolApi } from "./toolApiClient";
|
|
3
|
+
import { ToolResultError } from "./toolResultError";
|
|
3
4
|
|
|
4
5
|
type ExecShellKind = "auto" | "bash" | "powershell";
|
|
5
6
|
|
|
6
|
-
type
|
|
7
|
+
type ExecShellRequestArgs = {
|
|
7
8
|
command?: string;
|
|
8
9
|
cwd?: string;
|
|
9
10
|
interactive?: boolean;
|
|
@@ -14,58 +15,7 @@ type ExecBashRequestArgs = {
|
|
|
14
15
|
close?: boolean;
|
|
15
16
|
};
|
|
16
17
|
|
|
17
|
-
export const
|
|
18
|
-
name: "execBash",
|
|
19
|
-
description:
|
|
20
|
-
"兼容旧 agent 的 shell 执行工具。底层会调用后端 shell 执行接口;在 Windows 上可能自动回退到 PowerShell。环境不明确时,先调用 checkEnv({check:'context'}) 判断当前平台与可用 shell。支持交互式 session:先用 interactive=true 启动,再通过 sessionId + input 继续写入 stdin,或用 sessionId 单独轮询输出。仅用于本地开发环境。对 rm 等危险命令默认拦截,需显式传 unsafe: true 才会执行。不要用它执行 rg/grep 代码搜索;代码搜索请改用 codeSearch。",
|
|
21
|
-
parameters: {
|
|
22
|
-
type: "object",
|
|
23
|
-
properties: {
|
|
24
|
-
command: {
|
|
25
|
-
type: "string",
|
|
26
|
-
description:
|
|
27
|
-
"要执行的 shell 命令,例如: 'git status --short', 'bun test --watch=false'。",
|
|
28
|
-
},
|
|
29
|
-
cwd: {
|
|
30
|
-
type: "string",
|
|
31
|
-
description:
|
|
32
|
-
"可选:命令执行的工作目录(相对于 diff-server 所在机器),默认使用后端的 process.cwd()。",
|
|
33
|
-
},
|
|
34
|
-
unsafe: {
|
|
35
|
-
type: "boolean",
|
|
36
|
-
description:
|
|
37
|
-
"可选:是否允许执行被判定为危险的命令(例如 rm -rf)。默认 false;当为 false 且命令被识别为危险时,会直接拦截而不真正执行。",
|
|
38
|
-
},
|
|
39
|
-
interactive: {
|
|
40
|
-
type: "boolean",
|
|
41
|
-
description:
|
|
42
|
-
"可选:是否以交互式 session 启动命令。为 true 时接口会立即返回 sessionId,后续可继续写 stdin 或轮询输出。",
|
|
43
|
-
},
|
|
44
|
-
pty: {
|
|
45
|
-
type: "boolean",
|
|
46
|
-
description:
|
|
47
|
-
"可选:是否为交互式 session 分配伪终端(PTY)。适合 python -i、bash、node 等真正依赖终端语义的 REPL。",
|
|
48
|
-
},
|
|
49
|
-
sessionId: {
|
|
50
|
-
type: "string",
|
|
51
|
-
description:
|
|
52
|
-
"可选:已存在的交互式 sessionId。传入后可继续写入 input、查询最新输出或关闭该 session。",
|
|
53
|
-
},
|
|
54
|
-
input: {
|
|
55
|
-
type: "string",
|
|
56
|
-
description:
|
|
57
|
-
"可选:要写入 session stdin 的文本。通常需要自己补换行,例如 'y\\n' 或 'exit\\n'。",
|
|
58
|
-
},
|
|
59
|
-
close: {
|
|
60
|
-
type: "boolean",
|
|
61
|
-
description:
|
|
62
|
-
"可选:是否关闭指定 session。通常与 sessionId 搭配使用。",
|
|
63
|
-
},
|
|
64
|
-
},
|
|
65
|
-
},
|
|
66
|
-
};
|
|
67
|
-
|
|
68
|
-
export const execShellFunctionSchema = {
|
|
18
|
+
export const execShellFunctionSchema = {
|
|
69
19
|
name: "execShell",
|
|
70
20
|
description:
|
|
71
21
|
"跨平台 shell 执行工具。会在 Windows 上优先使用 PowerShell,在 Linux/macOS 上使用 bash。环境不明确时,先调用 checkEnv({check:'context'}) 判断当前平台与可用 shell。支持交互式 session:先用 interactive=true 启动,再通过 sessionId + input 继续写入 stdin,或用 sessionId 单独轮询输出。仅用于本地开发环境。对 rm 等危险命令默认拦截,需显式传 unsafe: true 才会执行。不要用它执行 rg/grep 代码搜索;代码搜索请改用 codeSearch。",
|
|
@@ -143,9 +93,9 @@ function isDangerousCommand(rawCommand: string): boolean {
|
|
|
143
93
|
}
|
|
144
94
|
|
|
145
95
|
async function requestExec(
|
|
146
|
-
args:
|
|
96
|
+
args: ExecShellRequestArgs,
|
|
147
97
|
thunkApi?: any,
|
|
148
|
-
endpoint = "/api/exec-
|
|
98
|
+
endpoint = "/api/exec-shell",
|
|
149
99
|
context?: { agentKey?: string },
|
|
150
100
|
): Promise<any> {
|
|
151
101
|
const payload = Object.fromEntries(
|
|
@@ -163,12 +113,16 @@ async function requestExec(
|
|
|
163
113
|
(typeof process !== "undefined" &&
|
|
164
114
|
(process as any).env?.DIFF_SERVER_ORIGIN) ||
|
|
165
115
|
(typeof window !== "undefined" ? window.location.origin : DEFAULT_LOCAL_API_ORIGIN);
|
|
166
|
-
const url = `${baseUrl}${endpoint}`;
|
|
167
|
-
const
|
|
168
|
-
|
|
169
|
-
headers
|
|
170
|
-
|
|
171
|
-
|
|
116
|
+
const url = `${baseUrl}${endpoint}`;
|
|
117
|
+
const headers: Record<string, string> = { "Content-Type": "application/json" };
|
|
118
|
+
if (typeof window !== "undefined" && (window as any).__NOLO_DESKTOP__) {
|
|
119
|
+
headers["X-Nolo-Desktop-Tool"] = "1";
|
|
120
|
+
}
|
|
121
|
+
const res = await fetch(url, {
|
|
122
|
+
method: "POST",
|
|
123
|
+
headers,
|
|
124
|
+
body: JSON.stringify(payload),
|
|
125
|
+
});
|
|
172
126
|
|
|
173
127
|
const text = await res.text();
|
|
174
128
|
try {
|
|
@@ -178,7 +132,7 @@ async function requestExec(
|
|
|
178
132
|
}
|
|
179
133
|
}
|
|
180
134
|
|
|
181
|
-
function formatExecResult(prefixLabel: string, args: any, json: any, trimmedCommand: string, sessionId?: string) {
|
|
135
|
+
function formatExecResult(prefixLabel: string, args: any, json: any, trimmedCommand: string, sessionId?: string) {
|
|
182
136
|
const prefix = json?.ok === false || json?.error ? `[${prefixLabel} 失败]` : `[${prefixLabel} 成功]`;
|
|
183
137
|
const stdout = json.stdout ?? "";
|
|
184
138
|
const stderr = json.stderr ?? "";
|
|
@@ -219,10 +173,20 @@ function formatExecResult(prefixLabel: string, args: any, json: any, trimmedComm
|
|
|
219
173
|
...json,
|
|
220
174
|
},
|
|
221
175
|
displayData: displayParts.join("\n"),
|
|
222
|
-
};
|
|
223
|
-
}
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function throwExecToolError(toolName: "execShell", message: string, rawData?: any): never {
|
|
180
|
+
const displayData = `${toolName} 调用失败:${message}`;
|
|
181
|
+
throw new ToolResultError(displayData, {
|
|
182
|
+
code: "EXEC_TOOL_FAILED",
|
|
183
|
+
rawData: rawData ?? { error: displayData },
|
|
184
|
+
displayData,
|
|
185
|
+
retryable: true,
|
|
186
|
+
});
|
|
187
|
+
}
|
|
224
188
|
|
|
225
|
-
function buildBlockedResponse(toolName: "
|
|
189
|
+
function buildBlockedResponse(toolName: "execShell", trimmedCommand: string, cwd?: string, shell?: ExecShellKind) {
|
|
226
190
|
const lines: string[] = [];
|
|
227
191
|
lines.push(`[${toolName} 拦截] 检测到潜在危险命令,默认不执行。`);
|
|
228
192
|
lines.push(`command: ${trimmedCommand}`);
|
|
@@ -247,57 +211,7 @@ function buildBlockedResponse(toolName: "execBash" | "execShell", trimmedCommand
|
|
|
247
211
|
};
|
|
248
212
|
}
|
|
249
213
|
|
|
250
|
-
export const
|
|
251
|
-
thunkApi: any,
|
|
252
|
-
args: { command: string; cwd?: string; pty?: boolean }
|
|
253
|
-
) =>
|
|
254
|
-
requestExec(
|
|
255
|
-
{
|
|
256
|
-
command: args.command,
|
|
257
|
-
cwd: args.cwd,
|
|
258
|
-
interactive: true,
|
|
259
|
-
pty: args.pty,
|
|
260
|
-
},
|
|
261
|
-
thunkApi,
|
|
262
|
-
);
|
|
263
|
-
|
|
264
|
-
export const writeExecBashSession = (
|
|
265
|
-
thunkApi: any,
|
|
266
|
-
args: { sessionId: string; input: string }
|
|
267
|
-
) =>
|
|
268
|
-
requestExec(
|
|
269
|
-
{
|
|
270
|
-
sessionId: args.sessionId,
|
|
271
|
-
input: args.input,
|
|
272
|
-
},
|
|
273
|
-
thunkApi,
|
|
274
|
-
);
|
|
275
|
-
|
|
276
|
-
export const readExecBashSession = (
|
|
277
|
-
thunkApi: any,
|
|
278
|
-
args: { sessionId: string }
|
|
279
|
-
) =>
|
|
280
|
-
requestExec(
|
|
281
|
-
{
|
|
282
|
-
sessionId: args.sessionId,
|
|
283
|
-
},
|
|
284
|
-
thunkApi,
|
|
285
|
-
);
|
|
286
|
-
|
|
287
|
-
export const closeExecBashSession = (
|
|
288
|
-
thunkApi: any,
|
|
289
|
-
args: { sessionId: string; input?: string }
|
|
290
|
-
) =>
|
|
291
|
-
requestExec(
|
|
292
|
-
{
|
|
293
|
-
sessionId: args.sessionId,
|
|
294
|
-
input: args.input,
|
|
295
|
-
close: true,
|
|
296
|
-
},
|
|
297
|
-
thunkApi,
|
|
298
|
-
);
|
|
299
|
-
|
|
300
|
-
export const startExecShellSession = (
|
|
214
|
+
export const startExecShellSession = (
|
|
301
215
|
thunkApi: any,
|
|
302
216
|
args: { command: string; cwd?: string; pty?: boolean; shell?: ExecShellKind }
|
|
303
217
|
) =>
|
|
@@ -310,43 +224,8 @@ export const startExecShellSession = (
|
|
|
310
224
|
shell: args.shell,
|
|
311
225
|
},
|
|
312
226
|
thunkApi,
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
export async function execBashFunc(args: any, thunkApi?: any, context?: { agentKey?: string }): Promise<{
|
|
317
|
-
rawData: any;
|
|
318
|
-
displayData: string;
|
|
319
|
-
}> {
|
|
320
|
-
const { command, cwd, unsafe, interactive, pty, sessionId, input, close } = args || {};
|
|
321
|
-
const isSessionFollowup = typeof sessionId === "string" && sessionId.trim().length > 0;
|
|
322
|
-
|
|
323
|
-
if (!isSessionFollowup && (!command || typeof command !== "string")) {
|
|
324
|
-
const msg = "execBash 需要提供 command,或提供有效的 sessionId 来继续已有交互式 session。";
|
|
325
|
-
return { rawData: { error: msg }, displayData: msg };
|
|
326
|
-
}
|
|
327
|
-
|
|
328
|
-
const trimmedCommand = typeof command === "string" ? command.trim() : "";
|
|
329
|
-
if (!isSessionFollowup && !unsafe && isDangerousCommand(trimmedCommand)) {
|
|
330
|
-
return buildBlockedResponse("execBash", trimmedCommand, cwd);
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
try {
|
|
334
|
-
const json = await requestExec({
|
|
335
|
-
...(trimmedCommand ? { command: trimmedCommand } : {}),
|
|
336
|
-
...(cwd ? { cwd } : {}),
|
|
337
|
-
...(interactive ? { interactive: true } : {}),
|
|
338
|
-
...(pty ? { pty: true } : {}),
|
|
339
|
-
...(isSessionFollowup ? { sessionId } : {}),
|
|
340
|
-
...(typeof input === "string" ? { input } : {}),
|
|
341
|
-
...(close ? { close: true } : {}),
|
|
342
|
-
}, thunkApi, "/api/exec-bash", context);
|
|
343
|
-
return formatExecResult("execBash", { cwd }, json, trimmedCommand, sessionId);
|
|
344
|
-
} catch (error: any) {
|
|
345
|
-
const msg = `execBash 调用失败:${error?.message || String(error)}`;
|
|
346
|
-
return { rawData: { error: msg }, displayData: msg };
|
|
347
|
-
}
|
|
348
|
-
}
|
|
349
|
-
|
|
227
|
+
);
|
|
228
|
+
|
|
350
229
|
export async function execShellFunc(args: any, thunkApi?: any, context?: { agentKey?: string }): Promise<{
|
|
351
230
|
rawData: any;
|
|
352
231
|
displayData: string;
|
|
@@ -375,9 +254,13 @@ export async function execShellFunc(args: any, thunkApi?: any, context?: { agent
|
|
|
375
254
|
...(typeof input === "string" ? { input } : {}),
|
|
376
255
|
...(close ? { close: true } : {}),
|
|
377
256
|
}, thunkApi, "/api/exec-shell", context);
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
}
|
|
257
|
+
if (json?.ok === false || json?.error) {
|
|
258
|
+
const message = typeof json?.error === "string" ? json.error : JSON.stringify(json?.error ?? json);
|
|
259
|
+
throwExecToolError("execShell", message, { command: trimmedCommand || null, ...json });
|
|
260
|
+
}
|
|
261
|
+
return formatExecResult("execShell", { cwd, shell }, json, trimmedCommand, sessionId);
|
|
262
|
+
} catch (error: any) {
|
|
263
|
+
if (error instanceof ToolResultError) throw error;
|
|
264
|
+
throwExecToolError("execShell", error?.message || String(error));
|
|
265
|
+
}
|
|
266
|
+
}
|