arona-agent 1.0.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/LICENSE +21 -0
- package/README.md +149 -0
- package/assets/blue-archive/arona/spine/arona_spr.atlas.txt +1168 -0
- package/assets/blue-archive/arona/spine/arona_spr.png +0 -0
- package/assets/blue-archive/arona/spine/arona_spr.skel +0 -0
- package/assets/blue-archive/arona/voice.mp3 +0 -0
- package/assets/blue-archive/hoshino/hoshino_spr.atlas.txt +174 -0
- package/assets/blue-archive/hoshino/hoshino_spr.json +15385 -0
- package/assets/blue-archive/hoshino/hoshino_spr.png +0 -0
- package/assets/blue-archive/hoshino/hoshino_spr.skel +0 -0
- package/assets/blue-archive/hoshino/robber/hoshino_robber_spr.atlas.txt +111 -0
- package/assets/blue-archive/hoshino/robber/hoshino_robber_spr.png +0 -0
- package/assets/blue-archive/hoshino/robber/hoshino_robber_spr.skel +0 -0
- package/assets/blue-archive/hoshino/swimsuit/hoshino_swimsuit_spr.atlas.txt +251 -0
- package/assets/blue-archive/hoshino/swimsuit/hoshino_swimsuit_spr.png +0 -0
- package/assets/blue-archive/hoshino/swimsuit/hoshino_swimsuit_spr.skel +0 -0
- package/assets/blue-archive/hoshino/voice.mp3 +0 -0
- package/assets/blue-archive/plana/spine/plana_spr.atlas.txt +1266 -0
- package/assets/blue-archive/plana/spine/plana_spr.png +0 -0
- package/assets/blue-archive/plana/spine/plana_spr.skel +0 -0
- package/assets/blue-archive/plana/voice.mp3 +0 -0
- package/assets/blue-archive/shiroko/ridingsuit/shiroko_ridingsuit_spr.atlas.txt +97 -0
- package/assets/blue-archive/shiroko/ridingsuit/shiroko_ridingsuit_spr.png +0 -0
- package/assets/blue-archive/shiroko/ridingsuit/shiroko_ridingsuit_spr.skel +0 -0
- package/assets/blue-archive/shiroko/robber/shiroko_robber_spr.atlas.txt +174 -0
- package/assets/blue-archive/shiroko/robber/shiroko_robber_spr.png +0 -0
- package/assets/blue-archive/shiroko/robber/shiroko_robber_spr.skel +0 -0
- package/assets/blue-archive/shiroko/shiroko_spr.atlas.txt +167 -0
- package/assets/blue-archive/shiroko/shiroko_spr.json +10702 -0
- package/assets/blue-archive/shiroko/shiroko_spr.png +0 -0
- package/assets/blue-archive/shiroko/shiroko_spr.skel +0 -0
- package/assets/blue-archive/shiroko/voice.mp3 +0 -0
- package/bin/arona.mjs +43 -0
- package/package.json +37 -0
- package/pet/agents.cjs +197 -0
- package/pet/main.cjs +462 -0
- package/pet/preload.cjs +21 -0
- package/pet/renderer/fx.html +30 -0
- package/pet/renderer/fx.js +41 -0
- package/pet/renderer/gallery.html +18 -0
- package/pet/renderer/gallery.js +31 -0
- package/pet/renderer/index.html +18 -0
- package/pet/renderer/renderer.js +382 -0
- package/pet/renderer/spine_layer.js +609 -0
- package/pet/renderer/spinetest.html +24 -0
- package/pet/renderer/spinetest.js +295 -0
- package/pet/renderer/style.css +63 -0
- package/pet/tools/gallery_capture.cjs +267 -0
- package/pet/tools/gen_sway.cjs +170 -0
- package/pet/tools/meshify_region.cjs +105 -0
- package/pet/tools/skel_inspect.cjs +112 -0
- package/pet/tools/skel_to_json.cjs +410 -0
- package/pet/tools/spine_node.cjs +69 -0
- package/pet/tools/visual_test.cjs +262 -0
- package/pet/vendor/spine/spine-canvas.js +8472 -0
- package/pet/vendor/spine/spine-webgl.js +10839 -0
- package/python/__pycache__/_i18n.cpython-313.pyc +0 -0
- package/python/__pycache__/_i18n.cpython-314.pyc +0 -0
- package/python/__pycache__/computer_use.cpython-314.pyc +0 -0
- package/python/__pycache__/hotkey.cpython-313.pyc +0 -0
- package/python/__pycache__/hotkey.cpython-314.pyc +0 -0
- package/python/__pycache__/stt.cpython-313.pyc +0 -0
- package/python/__pycache__/stt.cpython-314.pyc +0 -0
- package/python/__pycache__/tts.cpython-313.pyc +0 -0
- package/python/__pycache__/tts.cpython-314.pyc +0 -0
- package/python/__pycache__/tts_stream.cpython-313.pyc +0 -0
- package/python/__pycache__/tts_stream.cpython-314.pyc +0 -0
- package/python/__pycache__/voice_clone.cpython-314.pyc +0 -0
- package/python/_i18n.py +18 -0
- package/python/computer_use.py +190 -0
- package/python/hotkey.py +196 -0
- package/python/stt.py +240 -0
- package/python/tts_say.py +177 -0
- package/python/voice_clone.py +98 -0
- package/requirements.txt +6 -0
- package/src/agent.ts +791 -0
- package/src/agent_registry.ts +109 -0
- package/src/commands.ts +641 -0
- package/src/config.ts +245 -0
- package/src/in_memory_credentials.ts +61 -0
- package/src/index.ts +78 -0
- package/src/locale.ts +131 -0
- package/src/logo.ts +120 -0
- package/src/mcp.ts +192 -0
- package/src/memory.ts +303 -0
- package/src/pet.ts +346 -0
- package/src/renderer.ts +350 -0
- package/src/repl.ts +902 -0
- package/src/setup.ts +371 -0
- package/src/skills.ts +96 -0
- package/src/slash_menu.ts +371 -0
- package/src/slash_registry.ts +53 -0
- package/src/speaker_context.ts +41 -0
- package/src/text_split.ts +79 -0
- package/src/tools/computer_use.ts +142 -0
- package/src/tools/emotion_tool.ts +55 -0
- package/src/tools/keep_silent_tool.ts +26 -0
- package/src/tools/memory_tool.ts +24 -0
- package/src/tools/skill_tools.ts +100 -0
- package/src/tools/tavily_tools.ts +431 -0
- package/src/tools/voice_tools.ts +28 -0
- package/src/tts_stream.ts +260 -0
- package/src/tui_select.ts +146 -0
- package/src/undo.ts +451 -0
- package/src/utils/python.ts +211 -0
- package/src/utils/spawn.ts +56 -0
- package/src/voice.ts +83 -0
- package/src/voice_cli.ts +198 -0
- package/src/voices.ts +135 -0
|
@@ -0,0 +1,371 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 斜杠命令候选菜单。
|
|
3
|
+
*
|
|
4
|
+
* 参考 opencode-source/packages/tui/src/component/prompt/autocomplete.tsx 的
|
|
5
|
+
* 状态机思路(visible/selected/scrollTop + 循环选择 + 视口滚动),但用原生
|
|
6
|
+
* ANSI 实现,不依赖 SolidJS/OpenTUI 框架。
|
|
7
|
+
*
|
|
8
|
+
* 关键设计:
|
|
9
|
+
* - 菜单画在 readline 提示符下方,带完整边框浮层。
|
|
10
|
+
* - 用「相对光标移动」管理位置(\r\n 下移、\x1b[NA 上移),不依赖 DECSC/DECRC
|
|
11
|
+
* 的绝对位置——后者在菜单导致终端滚动时会失效(保存的坐标被滚动打乱),
|
|
12
|
+
* 从而造成菜单堆积。相对移动天然抗滚动。
|
|
13
|
+
* - 每行用 \r\n 分隔,确保每行回到第 1 列;画完后上移 lineCount 行回到提示行,
|
|
14
|
+
* 再调 rl.prompt(true) 让 readline 重新渲染提示行并校正光标列。
|
|
15
|
+
* - Enter 补全时整体替换 rl.line(旧实现用 rl.write 追加,导致 /re/resume 重复);
|
|
16
|
+
* 若已敲完命令名、正在补参数(首 token = 完整名/别名),则保留原行只关菜单。
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import chalk from "chalk";
|
|
20
|
+
import type { Interface } from "readline";
|
|
21
|
+
import { SLASH_COMMANDS, resolveSlashCommand, type SlashCommandSpec } from "./slash_registry.ts";
|
|
22
|
+
import { t } from "./locale.ts";
|
|
23
|
+
|
|
24
|
+
const MAX_VISIBLE = 8;
|
|
25
|
+
|
|
26
|
+
// ── 显示宽度工具(CJK/全角算 2,控制符算 0)──────────────────────
|
|
27
|
+
function charWidth(code: number): number {
|
|
28
|
+
if (code < 0x20) return 0;
|
|
29
|
+
if (
|
|
30
|
+
code >= 0x1100 &&
|
|
31
|
+
(code <= 0x115f ||
|
|
32
|
+
(code >= 0x2e80 && code <= 0x303e) ||
|
|
33
|
+
(code >= 0x3040 && code <= 0x33bf) ||
|
|
34
|
+
(code >= 0x3400 && code <= 0x4dbf) ||
|
|
35
|
+
(code >= 0x4e00 && code <= 0xa4cf) ||
|
|
36
|
+
(code >= 0xac00 && code <= 0xd7af) ||
|
|
37
|
+
(code >= 0xf900 && code <= 0xfaff) ||
|
|
38
|
+
(code >= 0xfe30 && code <= 0xfe6f) ||
|
|
39
|
+
(code >= 0xff01 && code <= 0xff60) ||
|
|
40
|
+
(code >= 0xffe0 && code <= 0xffe6) ||
|
|
41
|
+
(code >= 0x1f300 && code <= 0x1f9ff))
|
|
42
|
+
) {
|
|
43
|
+
return 2;
|
|
44
|
+
}
|
|
45
|
+
return 1;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function strWidth(s: string): number {
|
|
49
|
+
let w = 0;
|
|
50
|
+
for (const ch of s) w += charWidth(ch.codePointAt(0)!);
|
|
51
|
+
return w;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** 去除 ANSI 颜色/光标转义序列,返回纯可见文本。 */
|
|
55
|
+
function stripAnsi(s: string): string {
|
|
56
|
+
return s.replace(/\x1b(?:\[[0-9;?]*[A-Za-z]|[0-9])/g, "");
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function padRight(s: string, width: number): string {
|
|
60
|
+
const w = strWidth(s);
|
|
61
|
+
return w >= width ? s : s + " ".repeat(width - w);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function truncateToWidth(s: string, width: number): string {
|
|
65
|
+
if (width <= 0) return "";
|
|
66
|
+
if (strWidth(s) <= width) return s;
|
|
67
|
+
let res = "";
|
|
68
|
+
let w = 0;
|
|
69
|
+
for (const ch of s) {
|
|
70
|
+
const cw = charWidth(ch.codePointAt(0)!);
|
|
71
|
+
if (w + cw > width - 1) break;
|
|
72
|
+
res += ch;
|
|
73
|
+
w += cw;
|
|
74
|
+
}
|
|
75
|
+
return res + "…";
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// ── 模糊评分(子序列匹配 + 连续命中加分 + 前缀加权)──────────────
|
|
79
|
+
function fuzzyScore(query: string, target: string): number {
|
|
80
|
+
if (!query) return 0;
|
|
81
|
+
const q = query.toLowerCase();
|
|
82
|
+
const t = target.toLowerCase();
|
|
83
|
+
let qi = 0;
|
|
84
|
+
let score = 0;
|
|
85
|
+
let prevMatch = false;
|
|
86
|
+
for (let ti = 0; ti < t.length && qi < q.length; ti++) {
|
|
87
|
+
if (t[ti] === q[qi]) {
|
|
88
|
+
score += prevMatch ? 5 : 1;
|
|
89
|
+
if (ti === 0) score += 8; // 前缀加权
|
|
90
|
+
qi++;
|
|
91
|
+
prevMatch = true;
|
|
92
|
+
} else {
|
|
93
|
+
prevMatch = false;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return qi === q.length ? score : -1;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function bestScore(query: string, spec: SlashCommandSpec): number {
|
|
100
|
+
const names = [spec.name, ...(spec.aliases ?? [])];
|
|
101
|
+
let best = -1;
|
|
102
|
+
for (const n of names) {
|
|
103
|
+
const s = fuzzyScore(query, n);
|
|
104
|
+
if (s > best) best = s;
|
|
105
|
+
}
|
|
106
|
+
return best;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// ── 菜单类 ───────────────────────────────────────────────────────
|
|
110
|
+
export class SlashMenu {
|
|
111
|
+
private matches: SlashCommandSpec[] = [];
|
|
112
|
+
private selected = 0;
|
|
113
|
+
private scrollTop = 0;
|
|
114
|
+
private visible = false;
|
|
115
|
+
// 当前已画出的菜单行数(含边框与页脚)。用于 close 时定位要清除的区域。
|
|
116
|
+
private drawnLineCount = 0;
|
|
117
|
+
// 「本次填入不执行」信号:confirm() 在选中 needsParams 指令且实际替换了输入行时
|
|
118
|
+
// 置位,由 repl 的 line handler 消费(同一按键的 "line" 事件同步跟随,无泄漏)。
|
|
119
|
+
private pendingNoExec = false;
|
|
120
|
+
|
|
121
|
+
isOpen(): boolean {
|
|
122
|
+
return this.visible;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** 供 repl 的 line handler 读取并清除「本次填入不执行」信号。 */
|
|
126
|
+
consumeNoExecSignal(): boolean {
|
|
127
|
+
const v = this.pendingNoExec;
|
|
128
|
+
this.pendingNoExec = false;
|
|
129
|
+
return v;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* 根据当前 readline 缓冲区决定打开/关闭/更新菜单。用于普通按键后调用
|
|
134
|
+
*(readline 已先更新缓冲区)。
|
|
135
|
+
*/
|
|
136
|
+
refresh(rl: Interface): void {
|
|
137
|
+
const line = (rl as any).line ?? "";
|
|
138
|
+
if (!line.startsWith("/")) {
|
|
139
|
+
if (this.visible) this.close(rl);
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
const query = line.slice(1).split(/\s/)[0]; // 仅对第一个 token 做模糊
|
|
143
|
+
// 守卫:首 token 已是完整指令名/别名(已选定或正在输参数)→ 不弹菜单,
|
|
144
|
+
// 让输入框干净地留着指令。仅输入 "/" 时 query 为空,跳过守卫照常弹全部。
|
|
145
|
+
if (query && resolveSlashCommand(query)) {
|
|
146
|
+
if (this.visible) this.close(rl);
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
if (query === "") {
|
|
150
|
+
this.setMatches(SLASH_COMMANDS.slice(), rl);
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
const scored = SLASH_COMMANDS.map((spec) => ({ spec, s: bestScore(query, spec) }))
|
|
154
|
+
.filter((x) => x.s >= 0)
|
|
155
|
+
.sort((a, b) => b.s - a.s)
|
|
156
|
+
.map((x) => x.spec);
|
|
157
|
+
if (scored.length === 0) {
|
|
158
|
+
if (this.visible) this.close(rl);
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
this.setMatches(scored, rl);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
private setMatches(matches: SlashCommandSpec[], rl: Interface): void {
|
|
165
|
+
this.matches = matches;
|
|
166
|
+
this.selected = 0;
|
|
167
|
+
this.scrollTop = 0;
|
|
168
|
+
if (!this.visible) {
|
|
169
|
+
this.visible = true;
|
|
170
|
+
}
|
|
171
|
+
this.redraw(rl);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/** ↑/↓ 导航(循环)。 */
|
|
175
|
+
move(direction: -1 | 1, rl: Interface): void {
|
|
176
|
+
if (!this.visible || this.matches.length === 0) return;
|
|
177
|
+
let next = this.selected + direction;
|
|
178
|
+
if (next < 0) next = this.matches.length - 1;
|
|
179
|
+
if (next >= this.matches.length) next = 0;
|
|
180
|
+
this.selected = next;
|
|
181
|
+
this.clampScroll();
|
|
182
|
+
this.redraw(rl);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Enter:把选中命令写入输入框(整体替换 rl.line),然后让 readline 的
|
|
187
|
+
* return 处理自然 emit "line" 事件以执行命令。
|
|
188
|
+
*
|
|
189
|
+
* 时序(配合 repl.ts 里的 prependListener,本监听器先于 readline 执行):
|
|
190
|
+
* 1. 本方法:eraseMenu 擦画面 + 重置状态 + 设置 rl.line = "/<name> "
|
|
191
|
+
* 2. readline 的 _onKeypress 处理 return → _onLine → emit "line" 带新值
|
|
192
|
+
* 3. repl 的 line handler 收到补全后的命令名并执行
|
|
193
|
+
*
|
|
194
|
+
* 注意:不能调 rl.prompt(true)——它的 clearScreenDown 会干扰,且会让
|
|
195
|
+
* readline 认为 return 前已重绘,可能跳过 emit "line"。
|
|
196
|
+
*
|
|
197
|
+
* needsParams 指令:填完不执行——置 pendingNoExec
|
|
198
|
+
* 信号,repl 的 line handler 消费后只重绘提示行,用户补参数再 Enter。
|
|
199
|
+
*/
|
|
200
|
+
confirm(rl: Interface): void {
|
|
201
|
+
if (!this.visible) {
|
|
202
|
+
this.pendingNoExec = false;
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
const spec = this.matches[this.selected];
|
|
206
|
+
const filled = this.fillSelected(rl);
|
|
207
|
+
this.pendingNoExec = !!(filled && spec?.needsParams);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* → 键:把选中命令填入输入框,但不触发执行(无后续 return 事件,readline
|
|
212
|
+
* 不会 emit "line")。与 confirm 的区别仅在「是否让执行发生」,填入逻辑共享
|
|
213
|
+
* fillSelected。填完手动 prompt(true) 重绘提示行——此流程没有后续按键来触发
|
|
214
|
+
* 重绘。参数保护同样生效:已在输参数时只关菜单保留原行。
|
|
215
|
+
*/
|
|
216
|
+
complete(rl: Interface): void {
|
|
217
|
+
if (!this.visible) return;
|
|
218
|
+
this.fillSelected(rl);
|
|
219
|
+
(rl as any).prompt(true);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* 擦菜单 + 重置状态 + 按选中项整体替换 rl.line(含参数保护)。返回是否真正
|
|
224
|
+
* 替换了行(false = 首 token 已是完整命令名/别名,参数输入中,保留原行)。
|
|
225
|
+
* 注意先读 spec 再重置 this.matches。
|
|
226
|
+
*/
|
|
227
|
+
private fillSelected(rl: Interface): boolean {
|
|
228
|
+
const spec = this.matches[this.selected];
|
|
229
|
+
// 擦除菜单画面并重置状态
|
|
230
|
+
this.eraseMenu();
|
|
231
|
+
this.visible = false;
|
|
232
|
+
this.matches = [];
|
|
233
|
+
this.selected = 0;
|
|
234
|
+
this.scrollTop = 0;
|
|
235
|
+
this.drawnLineCount = 0;
|
|
236
|
+
if (!spec) return false;
|
|
237
|
+
const currentLine: string = (rl as any).line ?? "";
|
|
238
|
+
const currentToken = currentLine.startsWith("/") ? currentLine.slice(1).split(/\s/)[0] : "";
|
|
239
|
+
const names = [spec.name, ...(spec.aliases ?? [])];
|
|
240
|
+
if (names.includes(currentToken)) return false; // 参数输入中,保留原行
|
|
241
|
+
const replacement = spec.interactive ? `/${spec.name}` : `/${spec.name} `;
|
|
242
|
+
(rl as any).line = replacement;
|
|
243
|
+
(rl as any).cursor = replacement.length;
|
|
244
|
+
return true;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/** Esc / 外部关闭。保留已输入内容。 */
|
|
248
|
+
close(rl: Interface): void {
|
|
249
|
+
if (!this.visible) return;
|
|
250
|
+
this.eraseMenu();
|
|
251
|
+
this.visible = false;
|
|
252
|
+
this.matches = [];
|
|
253
|
+
this.selected = 0;
|
|
254
|
+
this.scrollTop = 0;
|
|
255
|
+
this.drawnLineCount = 0;
|
|
256
|
+
(rl as any).prompt(true);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// ── 渲染 ──────────────────────────────────────────────────────
|
|
260
|
+
|
|
261
|
+
private clampScroll(): void {
|
|
262
|
+
const maxVisible = this.maxVisible();
|
|
263
|
+
if (this.selected < this.scrollTop) this.scrollTop = this.selected;
|
|
264
|
+
else if (this.selected >= this.scrollTop + maxVisible) {
|
|
265
|
+
this.scrollTop = this.selected - maxVisible + 1;
|
|
266
|
+
}
|
|
267
|
+
// 防止 scrollTop 超出末尾窗口
|
|
268
|
+
const lastTop = Math.max(0, this.matches.length - maxVisible);
|
|
269
|
+
if (this.scrollTop > lastTop) this.scrollTop = lastTop;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
private maxVisible(): number {
|
|
273
|
+
const rows = process.stdout.rows ?? 24;
|
|
274
|
+
return Math.max(3, Math.min(MAX_VISIBLE, rows - 6));
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* 擦除已绘制的菜单并回到提示行。调用时光标应在提示行(由上次 redraw 或
|
|
279
|
+
* readline 的按键处理留在那里)。用相对移动:下移 1 行到菜单顶 → 清到屏幕
|
|
280
|
+
* 末尾 → 上移 1 行回提示行。相对移动天然抗终端滚动。
|
|
281
|
+
*/
|
|
282
|
+
private eraseMenu(): void {
|
|
283
|
+
if (this.drawnLineCount === 0) return;
|
|
284
|
+
process.stdout.write("\r\n"); // 回到第 1 列并下移 1 行(到菜单顶)
|
|
285
|
+
process.stdout.write("\x1b[0J"); // 清除从菜单顶到屏幕末尾
|
|
286
|
+
process.stdout.write("\x1b[1A"); // 上移 1 行回提示行
|
|
287
|
+
this.drawnLineCount = 0;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
private redraw(rl: Interface): void {
|
|
291
|
+
if (!this.visible) return;
|
|
292
|
+
const cols = process.stdout.columns ?? 80;
|
|
293
|
+
const maxVisible = this.maxVisible();
|
|
294
|
+
const start = this.scrollTop;
|
|
295
|
+
const end = Math.min(this.matches.length, start + maxVisible);
|
|
296
|
+
const items = this.matches.slice(start, end);
|
|
297
|
+
|
|
298
|
+
// 计算列宽
|
|
299
|
+
const nameCols = items.map((s) => {
|
|
300
|
+
const alias = s.aliases?.length ? ` (${s.aliases.join(", ")})` : "";
|
|
301
|
+
return `/${s.name}${alias}`;
|
|
302
|
+
});
|
|
303
|
+
const maxNameW = Math.max(...nameCols.map(strWidth));
|
|
304
|
+
const descStart = 2 + maxNameW + 2; // marker(2) + name + gap(2)
|
|
305
|
+
let maxDescW = Math.max(...items.map((s) => strWidth(s.description)));
|
|
306
|
+
|
|
307
|
+
const maxInnerW = Math.max(20, cols - 6);
|
|
308
|
+
let innerW = descStart + maxDescW;
|
|
309
|
+
if (innerW > maxInnerW) {
|
|
310
|
+
innerW = maxInnerW;
|
|
311
|
+
maxDescW = Math.max(0, innerW - descStart);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
const lines: string[] = [];
|
|
315
|
+
const border = chalk.cyan;
|
|
316
|
+
// 顶边
|
|
317
|
+
lines.push(border("┌" + "─".repeat(innerW + 2) + "┐"));
|
|
318
|
+
// 各项
|
|
319
|
+
for (let i = 0; i < items.length; i++) {
|
|
320
|
+
const spec = items[i];
|
|
321
|
+
const idx = start + i;
|
|
322
|
+
const nameCol = nameCols[i];
|
|
323
|
+
const marker = idx === this.selected ? "▶ " : " ";
|
|
324
|
+
const gap = " ".repeat(descStart - 2 - strWidth(nameCol));
|
|
325
|
+
let desc = spec.description;
|
|
326
|
+
if (strWidth(desc) > maxDescW) desc = truncateToWidth(desc, maxDescW);
|
|
327
|
+
const content = marker + nameCol + gap + desc;
|
|
328
|
+
const padded = padRight(content, innerW);
|
|
329
|
+
// 选中行整行高亮(含边框),否则边框青色、内容默认色
|
|
330
|
+
const full =
|
|
331
|
+
idx === this.selected
|
|
332
|
+
? chalk.bgCyan.black("┃ " + padded + " ┃")
|
|
333
|
+
: border("┃ ") + padded + border(" ┃");
|
|
334
|
+
lines.push(full);
|
|
335
|
+
}
|
|
336
|
+
// 底边
|
|
337
|
+
lines.push(border("└" + "─".repeat(innerW + 2) + "┘"));
|
|
338
|
+
// 页脚提示
|
|
339
|
+
const moreUp = this.scrollTop > 0;
|
|
340
|
+
const moreDown = this.scrollTop + maxVisible < this.matches.length;
|
|
341
|
+
let footer = t(" ↑/↓ 选择 · Enter 补全 · → 填入 · Esc 取消", " ↑/↓ select · Enter complete · → fill · Esc cancel");
|
|
342
|
+
if (moreUp || moreDown) {
|
|
343
|
+
const ind = `${moreUp ? "↑" : ""}${moreDown ? "↓" : ""}`;
|
|
344
|
+
footer += t(` · ${ind} 可滚动`, ` · ${ind} scrollable`);
|
|
345
|
+
}
|
|
346
|
+
lines.push(chalk.cyan(footer));
|
|
347
|
+
|
|
348
|
+
// ── 绘制(相对移动,抗滚动)──────────────────────────────────
|
|
349
|
+
// 光标当前在提示行。下移 1 行到菜单顶 → 清到屏幕末尾(擦旧菜单)→
|
|
350
|
+
// 画新菜单 → 回到提示行并定位光标到输入位置。
|
|
351
|
+
//
|
|
352
|
+
// 关键:不能调 rl.prompt(true)!readline 的 _refreshLine 会执行
|
|
353
|
+
// clearScreenDown(\x1b[J 清到屏幕末尾),把刚画的菜单整个擦掉。
|
|
354
|
+
// 改为手动把光标定位到「prompt 宽度 + cursor」列,让光标停在输入
|
|
355
|
+
// 末尾,readline 下次按键时会自行重绘提示行。
|
|
356
|
+
process.stdout.write("\r\n"); // 回到第 1 列并下移 1 行(末行会滚动)
|
|
357
|
+
process.stdout.write("\x1b[0J"); // 清除从菜单顶到屏幕末尾(擦旧菜单)
|
|
358
|
+
// 画各行,用 \r\n 分隔确保每行回到第 1 列
|
|
359
|
+
process.stdout.write(lines.join("\r\n"));
|
|
360
|
+
// 光标现在在最后一行末尾;回到提示行第 1 列
|
|
361
|
+
const lineCount = lines.length;
|
|
362
|
+
process.stdout.write("\r\x1b[" + lineCount + "A");
|
|
363
|
+
this.drawnLineCount = lineCount;
|
|
364
|
+
// 光标右移到输入位置:prompt 可见宽度 + cursor。
|
|
365
|
+
// 注意 prompt 可能带 ANSI 颜色转义(如 chalk.cyan("❯ ")),必须先 strip
|
|
366
|
+
// 否则转义里的 [36m 等可见字符会被算进宽度,导致光标右移过多。
|
|
367
|
+
const promptStr = stripAnsi(String((rl as any)._prompt ?? ""));
|
|
368
|
+
const targetCol = strWidth(promptStr) + ((rl as any).cursor ?? 0);
|
|
369
|
+
if (targetCol > 0) process.stdout.write("\x1b[" + targetCol + "C");
|
|
370
|
+
}
|
|
371
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Slash command registry shared by the completion menu and the dispatcher.
|
|
3
|
+
*
|
|
4
|
+
* Each command has a primary name, optional aliases, and a Chinese description
|
|
5
|
+
* shown in the menu. Add new commands here and they'll automatically appear
|
|
6
|
+
* in the / menu AND be dispatchable by /name or /alias.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { t } from "./locale.ts";
|
|
10
|
+
|
|
11
|
+
export interface SlashCommandSpec {
|
|
12
|
+
name: string;
|
|
13
|
+
aliases?: string[];
|
|
14
|
+
description: string;
|
|
15
|
+
// When true, the command takes over the line (e.g. /resume opens an
|
|
16
|
+
// interactive picker that uses raw-mode keypresses). The menu should
|
|
17
|
+
// not auto-append a space after these commands.
|
|
18
|
+
interactive?: boolean;
|
|
19
|
+
// When true, Enter in the menu fills the command into the input line
|
|
20
|
+
// WITHOUT executing it, so the user can type the required argument
|
|
21
|
+
// and press Enter again. Set for every argument-taking command.
|
|
22
|
+
needsParams?: boolean;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export const SLASH_COMMANDS: SlashCommandSpec[] = [
|
|
26
|
+
{ name: "help", aliases: ["?"], description: t("显示命令列表", "Show command list") },
|
|
27
|
+
{ name: "exit", aliases: ["quit", "q"], description: t("退出(仅在有实际对话时保存会话)", "Exit (saves session only after a real conversation)") },
|
|
28
|
+
{ name: "new", aliases: ["clear"], description: t("开始新会话(清空上下文)", "Start a new session (clear context)") },
|
|
29
|
+
{ name: "resume", aliases: ["r"], description: t("上下键选择并恢复一个已保存的会话", "Pick and resume a saved session with arrow keys"), interactive: true },
|
|
30
|
+
{ name: "export", description: t("导出当前会话为 Markdown", "Export current session as Markdown") },
|
|
31
|
+
{ name: "thinking", description: t("开关思考/推理块显示", "Toggle thinking/reasoning block display") },
|
|
32
|
+
{ name: "details", description: t("开关工具执行详情显示", "Toggle tool execution detail display") },
|
|
33
|
+
{ name: "compact", description: t("压缩当前上下文以节省 token", "Compact current context to save tokens") },
|
|
34
|
+
{ name: "tts", description: t("开关文字转语音", "Toggle text-to-speech") },
|
|
35
|
+
{ name: "stt", description: t("开关语音转文字(长按右 Cmd ≥2秒录音)", "Toggle speech-to-text (hold right Cmd ≥2s to record)") },
|
|
36
|
+
{ name: "skill", description: t("调用技能(/skill <名称>)", "Invoke a skill (/skill <name>)") },
|
|
37
|
+
{ name: "mcp", description: t("管理 MCP 服务器和工具", "Manage MCP servers and tools") },
|
|
38
|
+
{ name: "change-agent", description: t("切换主 Agent + 多选子 Agent", "Switch main agent + multi-select sub agents"), interactive: true },
|
|
39
|
+
{ name: "undo", description: t("撤销上一个回合的全部文件改动(本地快照)", "Undo the previous turn's file changes (local snapshot)") },
|
|
40
|
+
{ name: "redo", description: t("重做已撤销的改动", "Redo undone changes") },
|
|
41
|
+
];
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Resolve a typed command (without leading `/`) to its spec. Matches against
|
|
45
|
+
* the primary name and any alias. Returns null if not found.
|
|
46
|
+
*/
|
|
47
|
+
export function resolveSlashCommand(typed: string): SlashCommandSpec | null {
|
|
48
|
+
for (const spec of SLASH_COMMANDS) {
|
|
49
|
+
if (spec.name === typed) return spec;
|
|
50
|
+
if (spec.aliases?.includes(typed)) return spec;
|
|
51
|
+
}
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
// 群聊发言者标注扩展:每次 LLM 调用前(context 事件)触发,给带 speaker 标记的
|
|
2
|
+
// assistant 历史消息文本加上「角色名:」前缀,让模型能区分"哪句话是谁说的"。
|
|
3
|
+
//
|
|
4
|
+
// 为什么需要它:SDK 序列化请求时只取 role + content(pi-ai convertMessages),
|
|
5
|
+
// 项目自定义的 speaker 字段不会发给模型 —— 必须把发言者编码进文本才能让模型区分。
|
|
6
|
+
//
|
|
7
|
+
// 关键性质:只作用于发送边界。扩展返回新数组(不改 state.messages),
|
|
8
|
+
// 因此存储 / 渲染 / TTS / 会话存档全部保持纯文本,前缀零污染。
|
|
9
|
+
// 当前正在生成的 assistant 回复没有 speaker,不会被加前缀(模型知道自己说什么)。
|
|
10
|
+
|
|
11
|
+
import type { ContextEvent, ExtensionAPI, InlineExtension } from "@earendil-works/pi-coding-agent";
|
|
12
|
+
import { getAgentLabel, type AgentId } from "./agent_registry.ts";
|
|
13
|
+
|
|
14
|
+
/** speaker 是项目自定义字段(repl.ts 回填时标记),不在 SDK 的 AgentMessage 类型上。 */
|
|
15
|
+
interface SpeakerMessage {
|
|
16
|
+
role: "assistant";
|
|
17
|
+
speaker?: string;
|
|
18
|
+
content?: Array<{ type: string; text?: string; [k: string]: unknown }>;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export const speakerContextExtension: InlineExtension = {
|
|
22
|
+
name: "arona-speaker-context",
|
|
23
|
+
hidden: true, // 不显示在启动扩展列表
|
|
24
|
+
factory: (pi: ExtensionAPI) => {
|
|
25
|
+
pi.on("context", (event: ContextEvent) => {
|
|
26
|
+
const messages = event.messages.map((m) => {
|
|
27
|
+
const speaker = (m as unknown as SpeakerMessage).speaker;
|
|
28
|
+
if (m.role !== "assistant" || !speaker || !Array.isArray(m.content)) return m;
|
|
29
|
+
const label = getAgentLabel(speaker as AgentId);
|
|
30
|
+
if (!label) return m;
|
|
31
|
+
return {
|
|
32
|
+
...m,
|
|
33
|
+
content: m.content.map((b) =>
|
|
34
|
+
b.type === "text" && b.text?.trim() ? { ...b, text: `${label}:${b.text}` } : b,
|
|
35
|
+
),
|
|
36
|
+
};
|
|
37
|
+
});
|
|
38
|
+
return { messages };
|
|
39
|
+
});
|
|
40
|
+
},
|
|
41
|
+
};
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 流式文本切句工具:renderer 气泡与 TTS 共用同一套算法。
|
|
3
|
+
*
|
|
4
|
+
* 规则:
|
|
5
|
+
* - 只在句子终止标点(中文。!?… / 英文 . ! ? / 换行)处切句,连续终止标点归入同一条。
|
|
6
|
+
* - 英文句点 "." 仅当后跟空白/换行/中文/中文标点/串尾时才视为句末,
|
|
7
|
+
* 避免把小数点/版本号("3.12"、"qwen-audio-3.0")误切。
|
|
8
|
+
* - 逗号族(,、;:,;:)不做句边界,保留在句内交给 TTS 引擎自行处理停顿,
|
|
9
|
+
* 避免把"好好吃饭哦,Sensei"拆成碎句破坏语气。
|
|
10
|
+
* - 连续无标点超过 forceLen 字时,按 forceLen 强切,避免长文本滞留。
|
|
11
|
+
* - 返回本次可输出的完整句子和剩余残段(等待后续 delta 或 isEnd 收尾)。
|
|
12
|
+
*/
|
|
13
|
+
const SENTENCE_END = /[。!?!?\n…]/;
|
|
14
|
+
|
|
15
|
+
/** 判断 buffer[i](英文句点)是否为句末:后跟空白/换行/中文/中文标点/串尾才算,否则视为小数点或缩写。 */
|
|
16
|
+
function isPeriodEnd(buffer: string, i: number): boolean {
|
|
17
|
+
if (buffer[i] !== ".") return false;
|
|
18
|
+
const next = buffer[i + 1];
|
|
19
|
+
if (next === undefined) return true;
|
|
20
|
+
if (/\s/.test(next)) return true;
|
|
21
|
+
if (/[。!?!?…,、;:]/.test(next)) return true;
|
|
22
|
+
if (/[\u4e00-\u9fff]/.test(next)) return true;
|
|
23
|
+
return false;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** CJK 表意文字(含扩展 A 区与兼容区):字数统计中每个字符算 1 字。 */
|
|
27
|
+
const CJK_RE = /[\u4e00-\u9fff\u3400-\u4dbf\uf900-\ufaff]/g;
|
|
28
|
+
|
|
29
|
+
export interface SplitStreamedTextResult {
|
|
30
|
+
sentences: string[];
|
|
31
|
+
rest: string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* 统计"朗读/气泡字数":中文字符每个算 1,英文/数字按空白切分的连续串每个算 1
|
|
36
|
+
* (一个英文单词算一个字),中英文标点与符号不计入。
|
|
37
|
+
*/
|
|
38
|
+
export function countTextUnits(text: string): number {
|
|
39
|
+
// 去掉所有标点符号(仅保留字母、数字、空白与 CJK)
|
|
40
|
+
const noPunct = text.replace(/[^\p{L}\p{N}\s]/gu, "");
|
|
41
|
+
const cjkCount = (noPunct.match(CJK_RE) || []).length;
|
|
42
|
+
const latinPart = noPunct.replace(CJK_RE, " ");
|
|
43
|
+
const words = latinPart.trim().split(/\s+/).filter(Boolean).length;
|
|
44
|
+
return cjkCount + words;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function splitStreamedText(buffer: string, forceLen: number): SplitStreamedTextResult {
|
|
48
|
+
const sentences: string[] = [];
|
|
49
|
+
let current = "";
|
|
50
|
+
|
|
51
|
+
for (let i = 0; i < buffer.length; i++) {
|
|
52
|
+
const ch = buffer[i];
|
|
53
|
+
current += ch;
|
|
54
|
+
|
|
55
|
+
const isEnd = SENTENCE_END.test(ch) || isPeriodEnd(buffer, i);
|
|
56
|
+
|
|
57
|
+
if (isEnd) {
|
|
58
|
+
// 连续终止标点(如“!!”、“…”、句点+空格)与当前句一起输出,避免产生只有标点的碎句
|
|
59
|
+
while (i + 1 < buffer.length) {
|
|
60
|
+
const nx = buffer[i + 1];
|
|
61
|
+
if (SENTENCE_END.test(nx) || nx === "." || nx === " ") {
|
|
62
|
+
current += nx;
|
|
63
|
+
i++;
|
|
64
|
+
} else {
|
|
65
|
+
break;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
const sent = current.trim();
|
|
69
|
+
if (sent) sentences.push(sent);
|
|
70
|
+
current = "";
|
|
71
|
+
} else if (current.length >= forceLen) {
|
|
72
|
+
const sent = current.trim();
|
|
73
|
+
if (sent) sentences.push(sent);
|
|
74
|
+
current = "";
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return { sentences, rest: current };
|
|
79
|
+
}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { Type } from "typebox";
|
|
2
|
+
import { defineTool } from "@earendil-works/pi-coding-agent";
|
|
3
|
+
import { PythonBridge } from "../utils/python.ts";
|
|
4
|
+
import { config } from "../config.ts";
|
|
5
|
+
|
|
6
|
+
let bridge: PythonBridge | null = null;
|
|
7
|
+
|
|
8
|
+
async function getBridge(): Promise<PythonBridge> {
|
|
9
|
+
if (!bridge || !bridge.isRunning) {
|
|
10
|
+
bridge = new PythonBridge("computer_use.py", [], {
|
|
11
|
+
CUA_API_KEY: config.cuaApiKey,
|
|
12
|
+
});
|
|
13
|
+
await bridge.start();
|
|
14
|
+
}
|
|
15
|
+
return bridge;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function imageResult(screenshot: string) {
|
|
19
|
+
return {
|
|
20
|
+
content: [
|
|
21
|
+
{ type: "text" as const, text: `Screenshot captured (${Math.round(screenshot.length / 1024)} KB).` },
|
|
22
|
+
{ type: "image" as const, data: screenshot, mimeType: "image/png" },
|
|
23
|
+
],
|
|
24
|
+
details: {},
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export const computerScreenshot = defineTool({
|
|
29
|
+
name: "computer_screenshot",
|
|
30
|
+
label: "Screenshot",
|
|
31
|
+
description: "Take a screenshot of the current screen. Returns the screenshot as an image.",
|
|
32
|
+
parameters: Type.Object({}),
|
|
33
|
+
execute: async () => {
|
|
34
|
+
const b = await getBridge();
|
|
35
|
+
const result = await b.send({ action: "screenshot" });
|
|
36
|
+
if (result.error) throw new Error(result.error);
|
|
37
|
+
return imageResult(result.screenshot);
|
|
38
|
+
},
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
export const computerClick = defineTool({
|
|
42
|
+
name: "computer_click",
|
|
43
|
+
label: "Click",
|
|
44
|
+
description: "Click at the specified coordinates on the screen. Returns a new screenshot after clicking.",
|
|
45
|
+
parameters: Type.Object({
|
|
46
|
+
x: Type.Number({ description: "X coordinate" }),
|
|
47
|
+
y: Type.Number({ description: "Y coordinate" }),
|
|
48
|
+
button: Type.Optional(Type.Union([Type.Literal("left"), Type.Literal("right"), Type.Literal("double")], { description: "Mouse button (default: left)" })),
|
|
49
|
+
}),
|
|
50
|
+
execute: async (_id, params) => {
|
|
51
|
+
const b = await getBridge();
|
|
52
|
+
const result = await b.send({ action: "click", x: params.x, y: params.y, button: params.button || "left" });
|
|
53
|
+
if (result.error) throw new Error(result.error);
|
|
54
|
+
return imageResult(result.screenshot);
|
|
55
|
+
},
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
export const computerType = defineTool({
|
|
59
|
+
name: "computer_type",
|
|
60
|
+
label: "Type Text",
|
|
61
|
+
description: "Type text at the current cursor position. Returns a new screenshot after typing.",
|
|
62
|
+
parameters: Type.Object({
|
|
63
|
+
text: Type.String({ description: "Text to type" }),
|
|
64
|
+
}),
|
|
65
|
+
execute: async (_id, params) => {
|
|
66
|
+
const b = await getBridge();
|
|
67
|
+
const result = await b.send({ action: "type", text: params.text });
|
|
68
|
+
if (result.error) throw new Error(result.error);
|
|
69
|
+
return imageResult(result.screenshot);
|
|
70
|
+
},
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
export const computerKey = defineTool({
|
|
74
|
+
name: "computer_key",
|
|
75
|
+
label: "Press Keys",
|
|
76
|
+
description: "Press a key combination (e.g. ['ctrl', 'c']). Returns a new screenshot after pressing.",
|
|
77
|
+
parameters: Type.Object({
|
|
78
|
+
keys: Type.Array(Type.String(), { description: "Keys to press, e.g. ['ctrl', 'c'] or ['enter']" }),
|
|
79
|
+
}),
|
|
80
|
+
execute: async (_id, params) => {
|
|
81
|
+
const b = await getBridge();
|
|
82
|
+
const result = await b.send({ action: "key", keys: params.keys });
|
|
83
|
+
if (result.error) throw new Error(result.error);
|
|
84
|
+
return imageResult(result.screenshot);
|
|
85
|
+
},
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
export const computerScroll = defineTool({
|
|
89
|
+
name: "computer_scroll",
|
|
90
|
+
label: "Scroll",
|
|
91
|
+
description: "Scroll at the specified coordinates. Returns a new screenshot after scrolling.",
|
|
92
|
+
parameters: Type.Object({
|
|
93
|
+
x: Type.Number({ description: "X coordinate" }),
|
|
94
|
+
y: Type.Number({ description: "Y coordinate" }),
|
|
95
|
+
direction: Type.Optional(Type.Union([Type.Literal("up"), Type.Literal("down")], { description: "Scroll direction (default: down)" })),
|
|
96
|
+
amount: Type.Optional(Type.Number({ description: "Scroll amount in clicks (default: 3)" })),
|
|
97
|
+
}),
|
|
98
|
+
execute: async (_id, params) => {
|
|
99
|
+
const b = await getBridge();
|
|
100
|
+
const result = await b.send({
|
|
101
|
+
action: "scroll",
|
|
102
|
+
x: params.x,
|
|
103
|
+
y: params.y,
|
|
104
|
+
direction: params.direction || "down",
|
|
105
|
+
amount: params.amount || 3,
|
|
106
|
+
});
|
|
107
|
+
if (result.error) throw new Error(result.error);
|
|
108
|
+
return imageResult(result.screenshot);
|
|
109
|
+
},
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
export const computerMove = defineTool({
|
|
113
|
+
name: "computer_move",
|
|
114
|
+
label: "Move Mouse",
|
|
115
|
+
description: "Move the mouse to the specified coordinates. Returns a new screenshot after moving.",
|
|
116
|
+
parameters: Type.Object({
|
|
117
|
+
x: Type.Number({ description: "X coordinate" }),
|
|
118
|
+
y: Type.Number({ description: "Y coordinate" }),
|
|
119
|
+
}),
|
|
120
|
+
execute: async (_id, params) => {
|
|
121
|
+
const b = await getBridge();
|
|
122
|
+
const result = await b.send({ action: "move", x: params.x, y: params.y });
|
|
123
|
+
if (result.error) throw new Error(result.error);
|
|
124
|
+
return imageResult(result.screenshot);
|
|
125
|
+
},
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
export function stopComputerUse(): void {
|
|
129
|
+
if (bridge) {
|
|
130
|
+
bridge.stop();
|
|
131
|
+
bridge = null;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export const computerUseTools = [
|
|
136
|
+
computerScreenshot,
|
|
137
|
+
computerClick,
|
|
138
|
+
computerType,
|
|
139
|
+
computerKey,
|
|
140
|
+
computerScroll,
|
|
141
|
+
computerMove,
|
|
142
|
+
];
|