@bolloon/bolloon-agent 0.5.5 → 0.6.0
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/dist/agents/parse-tool-call.js +6 -1
- package/dist/agents/pi-sdk-session-factory.js +36 -3
- package/dist/agents/pi-sdk.js +349 -199
- package/dist/agents/run-context.js +20 -0
- package/dist/agents/runner-resolver.js +2 -1
- package/dist/electron-build/agents/parse-tool-call.js +6 -1
- package/dist/electron-build/agents/parse-tool-call.js.map +1 -1
- package/dist/electron-build/agents/pi-sdk-session-factory.js +36 -3
- package/dist/electron-build/agents/pi-sdk-session-factory.js.map +1 -1
- package/dist/electron-build/agents/pi-sdk-types.js.map +1 -1
- package/dist/electron-build/agents/pi-sdk.js +349 -199
- package/dist/electron-build/agents/pi-sdk.js.map +1 -1
- package/dist/electron-build/agents/run-context.js +24 -0
- package/dist/electron-build/agents/run-context.js.map +1 -0
- package/dist/electron-build/agents/runner-resolver.js +2 -1
- package/dist/electron-build/agents/runner-resolver.js.map +1 -1
- package/dist/electron-build/kernel/channel-actor.js +314 -0
- package/dist/electron-build/kernel/channel-actor.js.map +1 -0
- package/dist/index.js +24 -19
- package/dist/kernel/channel-actor.js +297 -0
- package/dist/kernel/control.js +95 -0
- package/dist/kernel/gate-scan.js +1036 -0
- package/dist/kernel/model-runtime.js +472 -0
- package/dist/kernel/plan-channel-actor.js +298 -0
- package/dist/kernel/plan-constraint.js +165 -0
- package/dist/kernel/plan-deletion.js +32 -0
- package/dist/kernel/plan-modelruntime.js +60 -0
- package/dist/kernel/plan-runcontext.js +163 -0
- package/dist/kernel/plan.js +153 -0
- package/dist/kernel/roster.js +242 -0
- package/dist/web/bolloon-web.json +3 -3
- package/dist/web/client.js +3 -2
- package/dist/web/routes-tasks.js +5 -1
- package/dist/web/server.js +58 -18
- package/dist/web/sw.js +1 -1
- package/dist/web/ui/message-renderer.js +473 -29
- package/dist/web/ui/step-timeline.js +13 -12
- package/package.json +1 -1
- package/scripts/build-web.ts +74 -1
- package/dist/constraint-runtime/_archive_helper.d.ts +0 -1
- package/dist/constraint-runtime/_archive_helper.js +0 -9
- package/dist/constraint-runtime/assistant/index.d.ts +0 -11
- package/dist/constraint-runtime/assistant/index.js +0 -12
- package/dist/constraint-runtime/bootstrap/index.d.ts +0 -11
- package/dist/constraint-runtime/bootstrap/index.js +0 -12
- package/dist/constraint-runtime/bridge/index.d.ts +0 -11
- package/dist/constraint-runtime/bridge/index.js +0 -12
- package/dist/constraint-runtime/buddy/index.d.ts +0 -11
- package/dist/constraint-runtime/buddy/index.js +0 -12
- package/dist/constraint-runtime/cli/index.d.ts +0 -11
- package/dist/constraint-runtime/cli/index.js +0 -12
- package/dist/constraint-runtime/components/index.d.ts +0 -11
- package/dist/constraint-runtime/components/index.js +0 -12
- package/dist/constraint-runtime/constants/index.d.ts +0 -11
- package/dist/constraint-runtime/constants/index.js +0 -12
- package/dist/constraint-runtime/coordinator/index.d.ts +0 -11
- package/dist/constraint-runtime/coordinator/index.js +0 -12
- package/dist/constraint-runtime/cost_hook.d.ts +0 -2
- package/dist/constraint-runtime/cost_hook.js +0 -4
- package/dist/constraint-runtime/entrypoints/index.d.ts +0 -11
- package/dist/constraint-runtime/entrypoints/index.js +0 -12
- package/dist/constraint-runtime/execution_registry.d.ts +0 -20
- package/dist/constraint-runtime/execution_registry.js +0 -44
- package/dist/constraint-runtime/hooks/index.d.ts +0 -11
- package/dist/constraint-runtime/hooks/index.js +0 -12
- package/dist/constraint-runtime/ink.d.ts +0 -1
- package/dist/constraint-runtime/ink.js +0 -4
- package/dist/constraint-runtime/keybindings/index.d.ts +0 -11
- package/dist/constraint-runtime/keybindings/index.js +0 -12
- package/dist/constraint-runtime/memdir/index.d.ts +0 -11
- package/dist/constraint-runtime/memdir/index.js +0 -12
- package/dist/constraint-runtime/migrations/index.d.ts +0 -11
- package/dist/constraint-runtime/migrations/index.js +0 -12
- package/dist/constraint-runtime/moreright/index.d.ts +0 -11
- package/dist/constraint-runtime/moreright/index.js +0 -12
- package/dist/constraint-runtime/native_ts/index.d.ts +0 -11
- package/dist/constraint-runtime/native_ts/index.js +0 -12
- package/dist/constraint-runtime/output_styles/index.d.ts +0 -11
- package/dist/constraint-runtime/output_styles/index.js +0 -12
- package/dist/constraint-runtime/plugins/index.d.ts +0 -11
- package/dist/constraint-runtime/plugins/index.js +0 -12
- package/dist/constraint-runtime/port_manifest.d.ts +0 -7
- package/dist/constraint-runtime/port_manifest.js +0 -11
- package/dist/constraint-runtime/query.d.ts +0 -6
- package/dist/constraint-runtime/query.js +0 -1
- package/dist/constraint-runtime/remote/index.d.ts +0 -11
- package/dist/constraint-runtime/remote/index.js +0 -12
- package/dist/constraint-runtime/schemas/index.d.ts +0 -11
- package/dist/constraint-runtime/schemas/index.js +0 -12
- package/dist/constraint-runtime/screens/index.d.ts +0 -11
- package/dist/constraint-runtime/screens/index.js +0 -12
- package/dist/constraint-runtime/server/index.d.ts +0 -11
- package/dist/constraint-runtime/server/index.js +0 -12
- package/dist/constraint-runtime/services/index.d.ts +0 -11
- package/dist/constraint-runtime/services/index.js +0 -12
- package/dist/constraint-runtime/state/index.d.ts +0 -11
- package/dist/constraint-runtime/state/index.js +0 -12
- package/dist/constraint-runtime/system_init.d.ts +0 -1
- package/dist/constraint-runtime/system_init.js +0 -20
- package/dist/constraint-runtime/types/index.d.ts +0 -11
- package/dist/constraint-runtime/types/index.js +0 -12
- package/dist/constraint-runtime/upstream_proxy/index.d.ts +0 -11
- package/dist/constraint-runtime/upstream_proxy/index.js +0 -12
- package/dist/constraint-runtime/utils/index.d.ts +0 -11
- package/dist/constraint-runtime/utils/index.js +0 -12
- package/dist/constraint-runtime/vim/index.d.ts +0 -11
- package/dist/constraint-runtime/vim/index.js +0 -12
- package/dist/constraint-runtime/voice/index.d.ts +0 -11
- package/dist/constraint-runtime/voice/index.js +0 -12
|
@@ -1,16 +1,460 @@
|
|
|
1
|
+
// src/web/ui/message-renderer.ts
|
|
1
2
|
import {
|
|
2
3
|
mountStepTimeline,
|
|
3
4
|
pushStepToTimeline,
|
|
4
5
|
migrateStepTimeline,
|
|
5
6
|
getStepTimeline
|
|
6
7
|
} from "./step-timeline.js";
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
const
|
|
8
|
+
|
|
9
|
+
// src/agents/parse-tool-call.ts
|
|
10
|
+
var SHELL_KEYWORDS = ["git", "npx", "npm", "tsx", "tsc", "vitest", "node", "mkdir", "touch", "ls", "echo", "cat", "head", "tail", "wc", "pwd", "date"];
|
|
11
|
+
function defaultResolveAlias(name, tools) {
|
|
12
|
+
if (tools.has(name)) return name;
|
|
13
|
+
const lower = name.toLowerCase();
|
|
14
|
+
const aliasMap = {
|
|
15
|
+
read: "read_file",
|
|
16
|
+
edit: "edit_file",
|
|
17
|
+
write: "write_file",
|
|
18
|
+
rm: "delete_file",
|
|
19
|
+
mv: "move_file",
|
|
20
|
+
bash: "shell_exec",
|
|
21
|
+
shell: "shell_exec",
|
|
22
|
+
sh: "shell_exec",
|
|
23
|
+
cat: "read_file",
|
|
24
|
+
test: "vitest_run",
|
|
25
|
+
vitest: "vitest_run",
|
|
26
|
+
typecheck: "tsc_check",
|
|
27
|
+
tsc: "tsc_check",
|
|
28
|
+
log: "git_log",
|
|
29
|
+
show: "git_show",
|
|
30
|
+
diff: "git_diff",
|
|
31
|
+
commit: "git_commit",
|
|
32
|
+
push: "git_push",
|
|
33
|
+
branch: "git_branch",
|
|
34
|
+
checkout: "git_branch",
|
|
35
|
+
stash: "git_stash",
|
|
36
|
+
todo_write: "create_task",
|
|
37
|
+
todowrite: "create_task",
|
|
38
|
+
task: "create_task"
|
|
39
|
+
};
|
|
40
|
+
const aliased = aliasMap[lower];
|
|
41
|
+
if (aliased && tools.has(aliased)) return aliased;
|
|
42
|
+
if (tools.has(lower)) return lower;
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
function autoSplitCommand(args) {
|
|
46
|
+
if (typeof args.command === "string" && args.command.includes(" ") && !args.args) {
|
|
47
|
+
const parts = args.command.split(/\s+/);
|
|
48
|
+
args.command = parts[0];
|
|
49
|
+
args.args = parts.slice(1).join(" ");
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
function resolve(ctx, name) {
|
|
53
|
+
if (ctx.resolveAlias) return ctx.resolveAlias(name);
|
|
54
|
+
return defaultResolveAlias(name, ctx.tools);
|
|
55
|
+
}
|
|
56
|
+
var _parseEnv = typeof process !== "undefined" ? process.env : void 0;
|
|
57
|
+
var PARSE_DIAG_ON = !!_parseEnv && (_parseEnv.BOLLOON_PARSE_DIAG === "1" || _parseEnv.BOLLOON_VERBOSE === "1");
|
|
58
|
+
function parseToolCall(content, ctx) {
|
|
59
|
+
if (!content) return null;
|
|
60
|
+
const strippedContent = content.replace(/<think[\s\S]*?<\/think/g, "");
|
|
61
|
+
if (PARSE_DIAG_ON) try {
|
|
62
|
+
const result = /* @__PURE__ */ (function _diagProbe() {
|
|
63
|
+
return null;
|
|
64
|
+
})();
|
|
65
|
+
void result;
|
|
66
|
+
const probe = (function _doParse() {
|
|
67
|
+
const m1 = strippedContent.match(/<invoke\s+name=["']([\w]+)["']/);
|
|
68
|
+
if (m1) {
|
|
69
|
+
return { name: m1[1], args: { __probe_invoketag: "1" } };
|
|
70
|
+
}
|
|
71
|
+
const m2 = strippedContent.match(/<function_calls>[\s\S]*?<invoke\s+name=["']([\w]+)["']/);
|
|
72
|
+
if (m2) {
|
|
73
|
+
return { name: m2[1], args: { __probe_function_calls: "1" } };
|
|
74
|
+
}
|
|
75
|
+
const m3 = strippedContent.match(/\{[\s\S]*?"name"\s*:\s*["']([\w]+)["']/);
|
|
76
|
+
if (m3) {
|
|
77
|
+
return { name: m3[1], args: { __probe_json_name: "1" } };
|
|
78
|
+
}
|
|
79
|
+
return null;
|
|
80
|
+
})();
|
|
81
|
+
console.warn(
|
|
82
|
+
"[parseToolCall diag] rawLen=" + content.length + " strippedLen=" + strippedContent.length + " probeName=" + (probe?.name ?? "null") + " rawHead=" + JSON.stringify(content.slice(0, 500))
|
|
83
|
+
);
|
|
84
|
+
} catch (diagErr) {
|
|
85
|
+
console.warn("[parseToolCall diag] diag-self-failed:", String(diagErr));
|
|
86
|
+
}
|
|
87
|
+
const jsonPatterns = [
|
|
88
|
+
// markdown json code block + OpenAI 块, 同时匹配 arguments/input 字段
|
|
89
|
+
/(?:```(?:json|json5)?\s*\n?)?\{[\s\S]*?"name"\s*:\s*["'](\w+)["']\s*,\s*["']?(?:arguments|input)["']?\s*:\s*(\{[\s\S]*?\})\s*\}/
|
|
90
|
+
];
|
|
91
|
+
for (const p of jsonPatterns) {
|
|
92
|
+
const m = content.match(p);
|
|
93
|
+
if (m) {
|
|
94
|
+
const name = m[1];
|
|
95
|
+
let args = {};
|
|
96
|
+
try {
|
|
97
|
+
const parsed = JSON.parse(m[2]);
|
|
98
|
+
if (parsed && typeof parsed === "object") {
|
|
99
|
+
args = Object.fromEntries(Object.entries(parsed).map(([k, v]) => [k, String(v)]));
|
|
100
|
+
}
|
|
101
|
+
} catch {
|
|
102
|
+
}
|
|
103
|
+
autoSplitCommand(args);
|
|
104
|
+
const resolved = resolve(ctx, name);
|
|
105
|
+
if (resolved) {
|
|
106
|
+
return { name: resolved, args };
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
const toolsCallMatch = content.match(/<tools:call\s+name=["'](\w+)["']>([\s\S]*?)<\/tools:call>/);
|
|
111
|
+
if (toolsCallMatch) {
|
|
112
|
+
const name = toolsCallMatch[1];
|
|
113
|
+
const inner = toolsCallMatch[2];
|
|
114
|
+
const args = {};
|
|
115
|
+
const argTags = inner.matchAll(/<tools:call\s+name=["'](\w+)["']>([\s\S]*?)<\/tools:call>/g);
|
|
116
|
+
for (const m of argTags) {
|
|
117
|
+
args[m[1]] = m[2].trim();
|
|
118
|
+
}
|
|
119
|
+
const resolved = resolve(ctx, name);
|
|
120
|
+
if (resolved) {
|
|
121
|
+
return { name: resolved, args };
|
|
122
|
+
}
|
|
123
|
+
if (args.command) {
|
|
124
|
+
const cmdFirst = args.command.split(/\s+/)[0];
|
|
125
|
+
if (SHELL_KEYWORDS.includes(cmdFirst)) {
|
|
126
|
+
return { name: "shell_exec", args };
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
const patterns = [
|
|
131
|
+
// minimax/Hermes 自闭合 XML 格式 <invoke name="X">...</invoke>
|
|
132
|
+
new RegExp(`<invoke\\s+name=["']([\\w]+)["']>([\\s\\S]*?)</invoke>`),
|
|
133
|
+
// <function_calls> 包裹
|
|
134
|
+
new RegExp(`<function_calls>[\\s\\S]*?<invoke\\s+name=["']([\\w]+)["']>([\\s\\S]*?)</invoke>[\\s\\S]*?</function_calls>`),
|
|
135
|
+
// <function_calls><tool name="X"><param name="Y">value</param></tool></function_calls>
|
|
136
|
+
new RegExp(`<function_calls>[\\s\\S]*?<tool\\s+name=["']([\\w]+)["']>([\\s\\S]*?)</tool>[\\s\\S]*?</function_calls>`),
|
|
137
|
+
/调用工具[::]\s*(\w+)\s*\(([^)]*)\)/,
|
|
138
|
+
/使用工具[::]\s*(\w+)\s*\(([^)]*)\)/,
|
|
139
|
+
/tool[_\w]*[::]\s*(\w+)\s*\(([^)]*)\)/i,
|
|
140
|
+
/(\w+)\s*\(\s*([^)]*)\s*\)/,
|
|
141
|
+
// 对象字面量格式 {tool => "get_identity", args => {...}}
|
|
142
|
+
/\{\s*tool\s*=>\s*["'](\w+)["']\s*(?:,\s*args\s*=>\s*(\{[\s\S]*?\}))?\s*\}/,
|
|
143
|
+
// tool => "get_identity" (无 args 包裹)
|
|
144
|
+
/\btool\s*=>\s*["'](\w+)["']/,
|
|
145
|
+
// [TOOL_CALL] 块内 JSON 形式 {"name": "x", "args": {...}}
|
|
146
|
+
/\[TOOL_CALL\][\s\S]*?\{\s*"name"\s*:\s*"(\w+)"\s*,\s*"args"\s*:\s*(\{[\s\S]*?\})/i,
|
|
147
|
+
// "tool_name {json_args}" 形式 (单行, 无 name 字段)
|
|
148
|
+
/(?:^|\n)(\w+)\s+(\{[\s\S]*?\})(?=\n|$)/,
|
|
149
|
+
// XML 格式 <tool_name>...<arg>value</arg>...</tool_name>
|
|
150
|
+
/<(\w+)>([\s\S]*?)<\/\1>/
|
|
151
|
+
];
|
|
152
|
+
for (const pattern of patterns) {
|
|
153
|
+
const match = strippedContent.match(pattern);
|
|
154
|
+
if (!match) continue;
|
|
155
|
+
const name = match[1];
|
|
156
|
+
let args = {};
|
|
157
|
+
const rawArgs = match[2] || "";
|
|
158
|
+
if (rawArgs && rawArgs.trim().startsWith("{")) {
|
|
159
|
+
try {
|
|
160
|
+
const parsed = JSON.parse(rawArgs);
|
|
161
|
+
if (parsed && typeof parsed === "object") {
|
|
162
|
+
args = Object.fromEntries(Object.entries(parsed).map(([k, v]) => [k, String(v)]));
|
|
163
|
+
}
|
|
164
|
+
} catch {
|
|
165
|
+
const argPairs = rawArgs.split(",").map((s) => s.trim()).filter(Boolean);
|
|
166
|
+
for (const pair of argPairs) {
|
|
167
|
+
const [key, ...valueParts] = pair.split(":").map((s) => s.trim().replace(/['"]/g, ""));
|
|
168
|
+
if (key) args[key] = valueParts.join(":") || "";
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
} else if (rawArgs && /<[\w]/.test(rawArgs) && /<\/\w+>/.test(rawArgs)) {
|
|
172
|
+
const paramRe = /<parameter\s+name=["'](\w+)["']>([\s\S]*?)<\/parameter>/g;
|
|
173
|
+
const paramReShort = /<param\s+name=["'](\w+)["']>([\s\S]*?)<\/param>/g;
|
|
174
|
+
let pMatch;
|
|
175
|
+
while ((pMatch = paramRe.exec(rawArgs)) !== null) {
|
|
176
|
+
const argName = pMatch[1];
|
|
177
|
+
const argValue = pMatch[2].trim();
|
|
178
|
+
if (argName && argValue) {
|
|
179
|
+
args[argName] = argValue;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
if (Object.keys(args).length === 0) {
|
|
183
|
+
let sMatch;
|
|
184
|
+
paramReShort.lastIndex = 0;
|
|
185
|
+
while ((sMatch = paramReShort.exec(rawArgs)) !== null) {
|
|
186
|
+
const argName = sMatch[1];
|
|
187
|
+
const argValue = sMatch[2].trim().replace(/^["']|["']$/g, "");
|
|
188
|
+
if (argName && argValue) {
|
|
189
|
+
args[argName] = argValue;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
if (Object.keys(args).length === 0) {
|
|
194
|
+
const xmlArgPattern = /<(\w+)>([\s\S]*?)<\/\1>/g;
|
|
195
|
+
let xmlMatch;
|
|
196
|
+
while ((xmlMatch = xmlArgPattern.exec(rawArgs)) !== null) {
|
|
197
|
+
const argName = xmlMatch[1];
|
|
198
|
+
const argValue = xmlMatch[2].trim();
|
|
199
|
+
if (argName && argValue) {
|
|
200
|
+
args[argName] = argValue;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
} else if (rawArgs) {
|
|
205
|
+
const argPairs = rawArgs.split(",").map((s) => s.trim()).filter(Boolean);
|
|
206
|
+
for (const pair of argPairs) {
|
|
207
|
+
const [key, ...valueParts] = pair.split(":").map((s) => s.trim().replace(/['"]/g, ""));
|
|
208
|
+
if (key) args[key] = valueParts.join(":") || "";
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
const resolved = ctx.tools.has(name) ? name : resolve(ctx, name);
|
|
212
|
+
if (resolved) {
|
|
213
|
+
autoSplitCommand(args);
|
|
214
|
+
return { name: resolved, args };
|
|
215
|
+
}
|
|
216
|
+
if (rawArgs && /<\w+>[\s\S]*<\/\w+>/.test(strippedContent)) {
|
|
217
|
+
break;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
const xmlTagMatch = strippedContent.match(/<(\w+)>([\s\S]*?)<\/\1>/);
|
|
221
|
+
if (xmlTagMatch) {
|
|
222
|
+
const outerTag = xmlTagMatch[1];
|
|
223
|
+
const inner = xmlTagMatch[2];
|
|
224
|
+
if (!resolve(ctx, outerTag)) {
|
|
225
|
+
const cmdMatch = inner.match(/<command>([\s\S]*?)<\/command>/);
|
|
226
|
+
if (cmdMatch) {
|
|
227
|
+
const cmd = cmdMatch[1].trim();
|
|
228
|
+
const cmdFirst = cmd.split(/\s+/)[0];
|
|
229
|
+
if (SHELL_KEYWORDS.includes(cmdFirst)) {
|
|
230
|
+
const args = {};
|
|
231
|
+
const remaining = cmd.slice(cmdFirst.length).trim();
|
|
232
|
+
if (remaining) {
|
|
233
|
+
args.command = cmdFirst;
|
|
234
|
+
args.args = remaining;
|
|
235
|
+
} else {
|
|
236
|
+
args.command = cmd;
|
|
237
|
+
}
|
|
238
|
+
const argsM = inner.match(/<args>([\s\S]*?)<\/args>/);
|
|
239
|
+
if (argsM && argsM[1].trim()) {
|
|
240
|
+
args.args = argsM[1].trim();
|
|
241
|
+
}
|
|
242
|
+
return { name: "shell_exec", args };
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
return null;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// src/agents/chat-segmenter.ts
|
|
251
|
+
function segmentChatReply(reply, ctx) {
|
|
252
|
+
if (!reply) return [];
|
|
253
|
+
const segments = [];
|
|
254
|
+
let remaining = reply;
|
|
255
|
+
if (!remaining.startsWith("<think>")) {
|
|
256
|
+
remaining = extractLeadingThinking(remaining, segments);
|
|
257
|
+
}
|
|
258
|
+
remaining = extractAndPush("think", remaining, segments, /<think>([\s\S]*?)<\/think>/g);
|
|
259
|
+
remaining = extractAndPush("env_details", remaining, segments, /<environment_details>([\s\S]*?)<\/environment_details>/g);
|
|
260
|
+
remaining = stripToolCallMarkers(remaining, segments, ctx);
|
|
261
|
+
if (remaining.trim()) {
|
|
262
|
+
remaining = filterFillerText(remaining);
|
|
263
|
+
}
|
|
264
|
+
const finalIdx = remaining.indexOf("<final gen>");
|
|
265
|
+
let beforeFinalText = remaining;
|
|
266
|
+
if (finalIdx >= 0) {
|
|
267
|
+
beforeFinalText = remaining.substring(0, finalIdx);
|
|
268
|
+
}
|
|
269
|
+
const textContent = beforeFinalText.trim();
|
|
270
|
+
if (textContent) {
|
|
271
|
+
segments.push({ type: "text", content: textContent });
|
|
272
|
+
}
|
|
273
|
+
if (finalIdx >= 0) {
|
|
274
|
+
const afterFinal = remaining.substring(finalIdx + "<final gen>".length).trim();
|
|
275
|
+
if (afterFinal) {
|
|
276
|
+
segments.push({ type: "final", content: afterFinal });
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
return segments;
|
|
280
|
+
}
|
|
281
|
+
function extractAndPush(type, text, out, re) {
|
|
282
|
+
const matches = [];
|
|
283
|
+
const localRe = new RegExp(re.source, re.flags);
|
|
284
|
+
let m;
|
|
285
|
+
while ((m = localRe.exec(text)) !== null) {
|
|
286
|
+
const content = m[1] ? m[1].trim() : "";
|
|
287
|
+
matches.push({
|
|
288
|
+
start: m.index,
|
|
289
|
+
end: m.index + m[0].length,
|
|
290
|
+
content
|
|
291
|
+
});
|
|
292
|
+
if (m[0].length === 0) localRe.lastIndex++;
|
|
293
|
+
}
|
|
294
|
+
if (matches.length === 0) return text;
|
|
295
|
+
for (const m2 of matches) {
|
|
296
|
+
if (m2.content) out.push({ type, content: m2.content });
|
|
297
|
+
}
|
|
298
|
+
let next = "";
|
|
299
|
+
let cursor = 0;
|
|
300
|
+
for (const m2 of matches) {
|
|
301
|
+
next += text.substring(cursor, m2.start);
|
|
302
|
+
cursor = m2.end;
|
|
303
|
+
}
|
|
304
|
+
next += text.substring(cursor);
|
|
305
|
+
return next;
|
|
306
|
+
}
|
|
307
|
+
function extractLeadingThinking(text, out) {
|
|
308
|
+
const firstLineEnd = text.indexOf("\n");
|
|
309
|
+
const firstLine = firstLineEnd === -1 ? text : text.substring(0, firstLineEnd);
|
|
310
|
+
const rest = firstLineEnd === -1 ? "" : text.substring(firstLineEnd + 1);
|
|
311
|
+
const thinkingStartRe = /^(让我|我先|我应该|先来|先|接下来|好的[,,]?\s*我|让我先|先看看|让我看看|思考|考虑)/;
|
|
312
|
+
const enThinkingStartRe = /^(Let me|I'll|I will|First,|Next,|Now,|So,|Alright[,.]\s+(?:let me|I'll|I will))/i;
|
|
313
|
+
if (firstLine.trim().length === 0) {
|
|
314
|
+
return text;
|
|
315
|
+
}
|
|
316
|
+
if (firstLine.length <= 120 && (thinkingStartRe.test(firstLine) || enThinkingStartRe.test(firstLine))) {
|
|
317
|
+
out.push({ type: "think", content: firstLine.trim() });
|
|
318
|
+
return rest;
|
|
319
|
+
}
|
|
320
|
+
if (firstLine.length <= 80 && (thinkingStartRe.test(firstLine) || enThinkingStartRe.test(firstLine))) {
|
|
321
|
+
out.push({ type: "think", content: firstLine.trim() });
|
|
322
|
+
return rest;
|
|
323
|
+
}
|
|
324
|
+
return text;
|
|
325
|
+
}
|
|
326
|
+
function filterFillerText(text) {
|
|
327
|
+
const lines = text.split("\n");
|
|
328
|
+
const filtered = [];
|
|
329
|
+
for (const line of lines) {
|
|
330
|
+
const t = line.trim();
|
|
331
|
+
if (!t) continue;
|
|
332
|
+
if (/^(好|好了|好的|完成|完成了|任务完成|可以|可以了|答完了|说完了|就这样|完了|done|ok|OK|Okay|okay|alright|fine|let me check|我来)\.?$/i.test(t)) {
|
|
333
|
+
continue;
|
|
334
|
+
}
|
|
335
|
+
filtered.push(line);
|
|
336
|
+
}
|
|
337
|
+
return filtered.join("\n").trim();
|
|
338
|
+
}
|
|
339
|
+
function stripToolCallMarkers(text, out, ctx) {
|
|
340
|
+
let result = text.replace(/\[TOOL_CALL\][\s\S]*?\[\/TOOL_CALL\]/g, (m) => {
|
|
341
|
+
const parsed = parseToolCall(m, { tools: ctx.knownToolNames });
|
|
342
|
+
if (parsed) out.push({ type: "tool_call", tool: { name: parsed.name, args: parsed.args } });
|
|
343
|
+
return "";
|
|
344
|
+
});
|
|
345
|
+
result = result.replace(/<tool_call>[\s\S]*?<\/tool_call>/gi, (m) => {
|
|
346
|
+
const parsed = parseToolCall(m, { tools: ctx.knownToolNames });
|
|
347
|
+
if (parsed) out.push({ type: "tool_call", tool: { name: parsed.name, args: parsed.args } });
|
|
348
|
+
return "";
|
|
349
|
+
});
|
|
350
|
+
result = result.replace(/<invoke\s+name=["']([\w]+)["']>([\s\S]*?)<\/invoke>/g, (_m, name, inner) => {
|
|
351
|
+
if (ctx.knownToolNames.has(name)) {
|
|
352
|
+
const args = extractSimpleArgs(inner);
|
|
353
|
+
out.push({ type: "tool_call", tool: { name, args } });
|
|
354
|
+
}
|
|
355
|
+
return "";
|
|
356
|
+
});
|
|
357
|
+
result = result.replace(/<function_calls>[\s\S]*?<\/function_calls>/g, (m) => {
|
|
358
|
+
const parsed = parseToolCall(m, { tools: ctx.knownToolNames });
|
|
359
|
+
if (parsed) out.push({ type: "tool_call", tool: { name: parsed.name, args: parsed.args } });
|
|
360
|
+
return "";
|
|
361
|
+
});
|
|
362
|
+
result = result.replace(
|
|
363
|
+
/\{\s*"name"\s*:\s*"([\w]+)"\s*,\s*"(?:arguments|input|args|params)"\s*:\s*(\{[\s\S]*?\})\s*\}/g,
|
|
364
|
+
(_m, name, argsJson) => {
|
|
365
|
+
if (!ctx.knownToolNames.has(name)) return "";
|
|
366
|
+
let args = {};
|
|
367
|
+
try {
|
|
368
|
+
const parsed = JSON.parse(argsJson);
|
|
369
|
+
if (parsed && typeof parsed === "object") {
|
|
370
|
+
args = Object.fromEntries(Object.entries(parsed).map(([k, v]) => [k, String(v)]));
|
|
371
|
+
}
|
|
372
|
+
} catch {
|
|
373
|
+
}
|
|
374
|
+
out.push({ type: "tool_call", tool: { name, args } });
|
|
375
|
+
return "";
|
|
376
|
+
}
|
|
377
|
+
);
|
|
378
|
+
result = result.replace(/\{\s*tool\s*=>\s*["']([\w]+)["']\s*(?:,\s*args\s*=>\s*(\{[\s\S]*?\}))?\s*\}/g, (_m, name, argsJson) => {
|
|
379
|
+
if (!ctx.knownToolNames.has(name)) return "";
|
|
380
|
+
let args = {};
|
|
381
|
+
if (argsJson) {
|
|
382
|
+
try {
|
|
383
|
+
const parsed = JSON.parse(argsJson);
|
|
384
|
+
if (parsed && typeof parsed === "object") {
|
|
385
|
+
args = Object.fromEntries(Object.entries(parsed).map(([k, v]) => [k, String(v)]));
|
|
386
|
+
}
|
|
387
|
+
} catch {
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
out.push({ type: "tool_call", tool: { name, args } });
|
|
391
|
+
return "";
|
|
392
|
+
});
|
|
393
|
+
result = result.replace(/\[Function[^\]]*\]\s*/g, "");
|
|
394
|
+
result = stripUnclosedToolCallTags(result);
|
|
395
|
+
return result;
|
|
396
|
+
}
|
|
397
|
+
function stripUnclosedToolCallTags(text) {
|
|
398
|
+
const startTags = [
|
|
399
|
+
{ tag: "<tool_call>", closeTag: "</tool_call>", openRe: /<tool_call>/g },
|
|
400
|
+
{ tag: "<invoke ", closeTag: "</invoke>", openRe: /<invoke\s+name=["']([\w]+)["']>/g },
|
|
401
|
+
{ tag: "<function_calls>", closeTag: "</function_calls>", openRe: /<function_calls>/g },
|
|
402
|
+
{ tag: "[TOOL_CALL]", closeTag: "[/TOOL_CALL]", openRe: /\[TOOL_CALL\]/g }
|
|
403
|
+
];
|
|
404
|
+
let result = text;
|
|
405
|
+
for (const { tag, closeTag, openRe } of startTags) {
|
|
406
|
+
let m;
|
|
407
|
+
while ((m = openRe.exec(result)) !== null) {
|
|
408
|
+
const tail = result.substring(m.index);
|
|
409
|
+
const closeIdx = tail.indexOf(closeTag);
|
|
410
|
+
if (closeIdx === -1) {
|
|
411
|
+
const nextBlank = result.indexOf("\n\n", m.index);
|
|
412
|
+
const endIdx = nextBlank === -1 ? result.length : nextBlank;
|
|
413
|
+
result = result.substring(0, m.index) + result.substring(endIdx);
|
|
414
|
+
break;
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
return result;
|
|
419
|
+
}
|
|
420
|
+
function extractSimpleArgs(inner) {
|
|
421
|
+
const args = {};
|
|
422
|
+
const paramRe = /<parameter\s+name=["'](\w+)["']>([\s\S]*?)<\/parameter>/g;
|
|
423
|
+
let m;
|
|
424
|
+
while ((m = paramRe.exec(inner)) !== null) {
|
|
425
|
+
args[m[1]] = m[2].trim();
|
|
426
|
+
}
|
|
427
|
+
if (Object.keys(args).length === 0) {
|
|
428
|
+
const shortRe = /<param\s+name=["'](\w+)["']>([\s\S]*?)<\/param>/g;
|
|
429
|
+
while ((m = shortRe.exec(inner)) !== null) {
|
|
430
|
+
args[m[1]] = m[2].trim().replace(/^["']|["']$/g, "");
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
if (Object.keys(args).length === 0) {
|
|
434
|
+
const cmdM = inner.match(/<command>([\s\S]*?)<\/command>/);
|
|
435
|
+
const argsM = inner.match(/<args>([\s\S]*?)<\/args>/);
|
|
436
|
+
if (cmdM) {
|
|
437
|
+
const cmd = cmdM[1].trim();
|
|
438
|
+
if (cmd.includes(" ") && !argsM) {
|
|
439
|
+
const parts = cmd.split(/\s+/);
|
|
440
|
+
args.command = parts[0];
|
|
441
|
+
args.args = parts.slice(1).join(" ");
|
|
442
|
+
} else {
|
|
443
|
+
args.command = cmd;
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
if (argsM) args.args = argsM[1].trim();
|
|
447
|
+
}
|
|
448
|
+
return args;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
// src/web/ui/message-renderer.ts
|
|
452
|
+
var streamingMessageEl = null;
|
|
453
|
+
var streamingTextNode = null;
|
|
454
|
+
var streamingText = "";
|
|
455
|
+
var lastUserCommand = "";
|
|
456
|
+
var lastAiContent = "";
|
|
457
|
+
var stepEventBuffer = /* @__PURE__ */ new Map();
|
|
14
458
|
function hasStreamingText() {
|
|
15
459
|
return streamingText.length > 0;
|
|
16
460
|
}
|
|
@@ -37,7 +481,7 @@ function injectRecoveredText(partialText, ctx = { messagesEl: null, messagesCont
|
|
|
37
481
|
ctx
|
|
38
482
|
);
|
|
39
483
|
}
|
|
40
|
-
|
|
484
|
+
var scrollToBottomTimer = null;
|
|
41
485
|
function scheduleScrollToBottom(container) {
|
|
42
486
|
if (!container) return;
|
|
43
487
|
if (scrollToBottomTimer) return;
|
|
@@ -76,7 +520,7 @@ function addMessage(content, type, save = true, container, usedJudgmentIds = [],
|
|
|
76
520
|
if (save) {
|
|
77
521
|
const lastContent = type === "user" ? lastUserCommand : lastAiContent;
|
|
78
522
|
if (lastContent && content === lastContent) {
|
|
79
|
-
console.log(`[addMessage]
|
|
523
|
+
console.log(`[addMessage] 跳过重复的 ${type} 消息`);
|
|
80
524
|
return;
|
|
81
525
|
}
|
|
82
526
|
if (type === "user") lastUserCommand = content;
|
|
@@ -150,7 +594,7 @@ function addMessage(content, type, save = true, container, usedJudgmentIds = [],
|
|
|
150
594
|
if (type === "ai" && Array.isArray(usedJudgmentIds) && usedJudgmentIds.length > 0) {
|
|
151
595
|
const link = document.createElement("a");
|
|
152
596
|
link.className = "used-judgments-link";
|
|
153
|
-
link.textContent =
|
|
597
|
+
link.textContent = `📎 参考 ${usedJudgmentIds.length} 条原则`;
|
|
154
598
|
link.onclick = (e) => {
|
|
155
599
|
e.preventDefault();
|
|
156
600
|
if (typeof ctx.openJudgmentsModalWithFilter === "function") {
|
|
@@ -174,17 +618,17 @@ function buildThinkContainer(thinkContent) {
|
|
|
174
618
|
container.className = "think-container";
|
|
175
619
|
const toggle = document.createElement("div");
|
|
176
620
|
toggle.className = "think-toggle";
|
|
177
|
-
toggle.innerHTML = '
|
|
621
|
+
toggle.innerHTML = '💭 思考过程 <span class="think-arrow">▸</span>';
|
|
178
622
|
toggle.onclick = function() {
|
|
179
623
|
const details = container.querySelector(".think-content");
|
|
180
624
|
const arrow = toggle.querySelector(".think-arrow");
|
|
181
625
|
if (!details || !arrow) return;
|
|
182
626
|
if (details.style.display === "none") {
|
|
183
627
|
details.style.display = "block";
|
|
184
|
-
arrow.textContent = "
|
|
628
|
+
arrow.textContent = "▾";
|
|
185
629
|
} else {
|
|
186
630
|
details.style.display = "none";
|
|
187
|
-
arrow.textContent = "
|
|
631
|
+
arrow.textContent = "▸";
|
|
188
632
|
}
|
|
189
633
|
};
|
|
190
634
|
const content = document.createElement("div");
|
|
@@ -200,17 +644,17 @@ function buildEnvContainer(envDetails) {
|
|
|
200
644
|
container.className = "env-container";
|
|
201
645
|
const toggle = document.createElement("div");
|
|
202
646
|
toggle.className = "env-toggle";
|
|
203
|
-
toggle.innerHTML = '
|
|
647
|
+
toggle.innerHTML = '⚙️ 环境信息 <span class="env-arrow">▸</span>';
|
|
204
648
|
toggle.onclick = function() {
|
|
205
649
|
const details = container.querySelector(".environment-details");
|
|
206
650
|
const arrow = toggle.querySelector(".env-arrow");
|
|
207
651
|
if (!details || !arrow) return;
|
|
208
652
|
if (details.style.display === "none") {
|
|
209
653
|
details.style.display = "block";
|
|
210
|
-
arrow.textContent = "
|
|
654
|
+
arrow.textContent = "▾";
|
|
211
655
|
} else {
|
|
212
656
|
details.style.display = "none";
|
|
213
|
-
arrow.textContent = "
|
|
657
|
+
arrow.textContent = "▸";
|
|
214
658
|
}
|
|
215
659
|
};
|
|
216
660
|
const content = document.createElement("div");
|
|
@@ -266,30 +710,30 @@ function buildMessageActions(div, rawContent, ctx) {
|
|
|
266
710
|
actions.className = "message-actions";
|
|
267
711
|
const copyBtn = document.createElement("button");
|
|
268
712
|
copyBtn.className = "action-btn copy-btn";
|
|
269
|
-
copyBtn.innerHTML = copyIcon() + "
|
|
270
|
-
copyBtn.title = "
|
|
713
|
+
copyBtn.innerHTML = copyIcon() + " 复制";
|
|
714
|
+
copyBtn.title = "复制消息";
|
|
271
715
|
copyBtn.onclick = () => {
|
|
272
716
|
navigator.clipboard.writeText(rawContent).then(() => {
|
|
273
|
-
copyBtn.innerHTML = checkIcon() + "
|
|
717
|
+
copyBtn.innerHTML = checkIcon() + " 已复制";
|
|
274
718
|
setTimeout(() => {
|
|
275
|
-
copyBtn.innerHTML = copyIcon() + "
|
|
719
|
+
copyBtn.innerHTML = copyIcon() + " 复制";
|
|
276
720
|
}, 2e3);
|
|
277
721
|
});
|
|
278
722
|
};
|
|
279
723
|
actions.appendChild(copyBtn);
|
|
280
724
|
const saveJudgmentBtn = document.createElement("button");
|
|
281
725
|
saveJudgmentBtn.className = "action-btn save-as-judgment";
|
|
282
|
-
saveJudgmentBtn.title = "AI
|
|
726
|
+
saveJudgmentBtn.title = "AI 蒸馏为 30-80 字判断力 + 自动演化对齐";
|
|
283
727
|
saveJudgmentBtn.setAttribute("data-decision", rawContent.substring(0, 800));
|
|
284
728
|
if (ctx.currentChannelId) saveJudgmentBtn.setAttribute("data-channel-id", ctx.currentChannelId);
|
|
285
|
-
saveJudgmentBtn.innerHTML = shieldIcon() + "
|
|
729
|
+
saveJudgmentBtn.innerHTML = shieldIcon() + " 蒸馏为判断";
|
|
286
730
|
actions.appendChild(saveJudgmentBtn);
|
|
287
731
|
const regenBtn = document.createElement("button");
|
|
288
732
|
regenBtn.className = "action-btn regenerate-btn";
|
|
289
|
-
regenBtn.innerHTML = refreshIcon(false) + "
|
|
290
|
-
regenBtn.title = "
|
|
733
|
+
regenBtn.innerHTML = refreshIcon(false) + " 重新回答";
|
|
734
|
+
regenBtn.title = "重新生成回复";
|
|
291
735
|
regenBtn.onclick = () => {
|
|
292
|
-
regenBtn.innerHTML = refreshIcon(true) + "
|
|
736
|
+
regenBtn.innerHTML = refreshIcon(true) + " 生成中...";
|
|
293
737
|
regenBtn.disabled = true;
|
|
294
738
|
const messages = div.parentElement?.querySelectorAll(".message") || [];
|
|
295
739
|
let lastUserMsg = "";
|
|
@@ -310,10 +754,10 @@ function buildMessageActions(div, rawContent, ctx) {
|
|
|
310
754
|
}).then((res) => {
|
|
311
755
|
if (!res.ok) throw new Error("regenerate failed");
|
|
312
756
|
}).catch((err) => {
|
|
313
|
-
console.error("
|
|
314
|
-
regenBtn.innerHTML = refreshIcon(false) + "
|
|
757
|
+
console.error("重新生成失败:", err);
|
|
758
|
+
regenBtn.innerHTML = refreshIcon(false) + " 失败";
|
|
315
759
|
setTimeout(() => {
|
|
316
|
-
regenBtn.innerHTML = refreshIcon(false) + "
|
|
760
|
+
regenBtn.innerHTML = refreshIcon(false) + " 重新回答";
|
|
317
761
|
regenBtn.disabled = false;
|
|
318
762
|
}, 2e3);
|
|
319
763
|
});
|
|
@@ -441,7 +885,7 @@ function seedDedupState(lastType, lastContent) {
|
|
|
441
885
|
if (lastType === "user") lastUserCommand = lastContent || "";
|
|
442
886
|
else if (lastType === "ai") lastAiContent = lastContent || "";
|
|
443
887
|
}
|
|
444
|
-
|
|
888
|
+
var MessageRenderer = {
|
|
445
889
|
addMessage,
|
|
446
890
|
handleStreamTokenEvent,
|
|
447
891
|
finalizeTimelineAsMessage,
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
// src/web/ui/step-timeline.ts
|
|
2
|
+
var MAX_VISIBLE_NODES = 20;
|
|
3
|
+
var STORAGE_PREFIX = "bolloon.stepTimeline.expanded.";
|
|
4
|
+
var stateMap = /* @__PURE__ */ new WeakMap();
|
|
4
5
|
function readExpanded(channelId) {
|
|
5
6
|
if (!channelId) return false;
|
|
6
7
|
try {
|
|
@@ -17,17 +18,17 @@ function writeExpanded(channelId, expanded) {
|
|
|
17
18
|
}
|
|
18
19
|
}
|
|
19
20
|
function computeTitle(steps) {
|
|
20
|
-
if (steps.length === 0) return "
|
|
21
|
+
if (steps.length === 0) return "执行步骤";
|
|
21
22
|
const activeIdx = steps.findIndex((s) => s.status === "active");
|
|
22
|
-
if (activeIdx >= 0) return
|
|
23
|
+
if (activeIdx >= 0) return `● 执行中 · ${steps[activeIdx].name}`;
|
|
23
24
|
const lastErrorIdx = (() => {
|
|
24
25
|
for (let i = steps.length - 1; i >= 0; i--) {
|
|
25
26
|
if (steps[i].status === "error") return i;
|
|
26
27
|
}
|
|
27
28
|
return -1;
|
|
28
29
|
})();
|
|
29
|
-
if (lastErrorIdx >= 0) return
|
|
30
|
-
return
|
|
30
|
+
if (lastErrorIdx >= 0) return `✗ 失败 · ${steps[lastErrorIdx].name}`;
|
|
31
|
+
return `✓ 已完成 · ${steps.length} 步`;
|
|
31
32
|
}
|
|
32
33
|
function render(timelineEl) {
|
|
33
34
|
const state = stateMap.get(timelineEl);
|
|
@@ -53,7 +54,7 @@ function render(timelineEl) {
|
|
|
53
54
|
dotsEl.appendChild(dot);
|
|
54
55
|
}
|
|
55
56
|
dot.setAttribute("data-status", step.status);
|
|
56
|
-
dot.setAttribute("title", `${step.name}
|
|
57
|
+
dot.setAttribute("title", `${step.name} · ${step.status}`);
|
|
57
58
|
}
|
|
58
59
|
while (dotsEl.children.length > steps.length) {
|
|
59
60
|
dotsEl.removeChild(dotsEl.lastChild);
|
|
@@ -71,7 +72,7 @@ function render(timelineEl) {
|
|
|
71
72
|
moreBtn = document.createElement("li");
|
|
72
73
|
moreBtn.className = "step-timeline-more";
|
|
73
74
|
moreBtn.setAttribute("role", "button");
|
|
74
|
-
moreBtn.textContent = `+ ${hiddenCount}
|
|
75
|
+
moreBtn.textContent = `+ ${hiddenCount} 更多`;
|
|
75
76
|
moreBtn.onclick = () => {
|
|
76
77
|
state.showAll = true;
|
|
77
78
|
render(timelineEl);
|
|
@@ -82,7 +83,7 @@ function render(timelineEl) {
|
|
|
82
83
|
listEl.appendChild(moreBtn);
|
|
83
84
|
}
|
|
84
85
|
} else {
|
|
85
|
-
moreBtn.textContent = `+ ${hiddenCount}
|
|
86
|
+
moreBtn.textContent = `+ ${hiddenCount} 更多`;
|
|
86
87
|
}
|
|
87
88
|
htmlIdx++;
|
|
88
89
|
}
|
|
@@ -177,14 +178,14 @@ function createEmptyStepTimeline(channelId = null) {
|
|
|
177
178
|
titleSpan.className = "step-timeline-title";
|
|
178
179
|
const titleContent = document.createElement("span");
|
|
179
180
|
titleContent.setAttribute("data-current-tool", "");
|
|
180
|
-
titleContent.textContent = "
|
|
181
|
+
titleContent.textContent = "执行步骤";
|
|
181
182
|
titleSpan.appendChild(titleContent);
|
|
182
183
|
const dots = document.createElement("div");
|
|
183
184
|
dots.className = "step-timeline-dots";
|
|
184
185
|
dots.setAttribute("data-dots", "");
|
|
185
186
|
const arrow = document.createElement("span");
|
|
186
187
|
arrow.className = "step-timeline-arrow";
|
|
187
|
-
arrow.textContent = "
|
|
188
|
+
arrow.textContent = "▾";
|
|
188
189
|
summary.appendChild(titleSpan);
|
|
189
190
|
summary.appendChild(dots);
|
|
190
191
|
summary.appendChild(arrow);
|