arona-agent 1.2.2 → 1.2.3
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/gui/main.cjs +29 -6
- package/gui/renderer/app.js +289 -39
- package/gui/renderer/index.html +6 -0
- package/gui/renderer/style.css +144 -2
- package/package.json +1 -1
- package/pet/agents.cjs +9 -4
- package/pet/main.cjs +41 -2
- package/pet/renderer/spinetest.js +3 -3
- package/pet/tools/gallery_capture.cjs +1 -1
- package/pet/tools/mouth_capture.cjs +1 -1
- package/pet/tools/visual_test.cjs +1 -1
- package/python/__pycache__/stt.cpython-314.pyc +0 -0
- package/python/__pycache__/tts_say.cpython-314.pyc +0 -0
- package/python/stt.py +24 -3
- package/src/agent.ts +11 -10
- package/src/agent_registry.ts +30 -0
- package/src/coding_agent.ts +6 -5
- package/src/commands.ts +56 -17
- package/src/config.ts +22 -0
- package/src/gui/controller.ts +385 -159
- package/src/gui/index.ts +26 -5
- package/src/gui/protocol.ts +9 -2
- package/src/gui/setup_backend.ts +15 -7
- package/src/index.ts +25 -5
- package/src/memory.ts +105 -12
- package/src/pet.ts +60 -1
- package/src/renderer.ts +27 -3
- package/src/repl.ts +24 -6
- package/src/setup.ts +14 -6
- package/src/speaker_context.ts +67 -1
- package/src/utils/python.ts +11 -6
- package/src/voice.ts +3 -2
- package/src/voices.ts +5 -1
- package/src/workspace.ts +163 -0
package/gui/renderer/style.css
CHANGED
|
@@ -169,6 +169,81 @@ button { font: inherit; color: inherit; }
|
|
|
169
169
|
.session-item .s-time { flex: none; font-size: 12.5px; color: var(--text-3); }
|
|
170
170
|
.session-empty { color: var(--text-3); font-size: 12.5px; padding: 8px; white-space: nowrap; }
|
|
171
171
|
|
|
172
|
+
/* ── 折叠展开过渡(可复用工具类,与 .glass 同级)────────
|
|
173
|
+
用法:外层 <div class="collapse-body [collapsed]"><div class="collapse-inner">内容</div></div>,
|
|
174
|
+
切换 collapsed 类即有 高度收展 + 透明度 过渡;内容高度无需预知(grid 0fr/1fr 技巧),
|
|
175
|
+
初次插入即带 collapsed 不触发动画(过渡只在类切换时发生)。 */
|
|
176
|
+
.collapse-body {
|
|
177
|
+
display: grid;
|
|
178
|
+
grid-template-rows: 1fr;
|
|
179
|
+
opacity: 1;
|
|
180
|
+
transition: grid-template-rows 260ms var(--ease), opacity 200ms var(--ease);
|
|
181
|
+
}
|
|
182
|
+
.collapse-body > .collapse-inner {
|
|
183
|
+
min-height: 0;
|
|
184
|
+
overflow: hidden;
|
|
185
|
+
visibility: visible;
|
|
186
|
+
transition: visibility 0s;
|
|
187
|
+
}
|
|
188
|
+
.collapse-body.collapsed { grid-template-rows: 0fr; opacity: 0; }
|
|
189
|
+
/* 折叠动画播完再隐藏内容(Tab 不可达);展开时立即恢复可见 */
|
|
190
|
+
.collapse-body.collapsed > .collapse-inner { visibility: hidden; transition: visibility 0s 260ms; }
|
|
191
|
+
|
|
192
|
+
/* ── 侧栏工作区分组(工作区嵌套会话)──────── */
|
|
193
|
+
.ws-group { display: flex; flex-direction: column; gap: 2px; }
|
|
194
|
+
.ws-body { flex: none; display: flex; flex-direction: column; gap: 2px; }
|
|
195
|
+
.ws-head {
|
|
196
|
+
display: flex;
|
|
197
|
+
align-items: center;
|
|
198
|
+
gap: 7px;
|
|
199
|
+
height: 32px;
|
|
200
|
+
padding: 0 10px;
|
|
201
|
+
border: none;
|
|
202
|
+
border-radius: 9px;
|
|
203
|
+
background: transparent;
|
|
204
|
+
cursor: pointer;
|
|
205
|
+
width: 100%;
|
|
206
|
+
text-align: left;
|
|
207
|
+
color: var(--text-2);
|
|
208
|
+
font-size: 12.5px;
|
|
209
|
+
font-weight: 600;
|
|
210
|
+
}
|
|
211
|
+
.ws-head:hover { background: var(--hover); color: var(--text); }
|
|
212
|
+
.ws-head .ws-folder { display: inline-flex; flex: none; color: var(--text-3); }
|
|
213
|
+
.ws-head .ws-name { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
214
|
+
.ws-head .ws-count {
|
|
215
|
+
flex: none;
|
|
216
|
+
font-size: 11.5px;
|
|
217
|
+
font-weight: 500;
|
|
218
|
+
color: var(--text-3);
|
|
219
|
+
background: var(--hover);
|
|
220
|
+
border-radius: 8px;
|
|
221
|
+
padding: 1px 7px;
|
|
222
|
+
}
|
|
223
|
+
.ws-head .ws-chev { display: inline-flex; flex: none; color: var(--text-3); transition: transform 180ms var(--ease); }
|
|
224
|
+
.ws-group.collapsed .ws-chev { transform: rotate(-90deg); }
|
|
225
|
+
.ws-group .session-item { padding-left: 31px; } /* 组内嵌套缩进 */
|
|
226
|
+
.ws-more {
|
|
227
|
+
flex: none;
|
|
228
|
+
border: none;
|
|
229
|
+
background: transparent;
|
|
230
|
+
cursor: pointer;
|
|
231
|
+
color: var(--text-3);
|
|
232
|
+
font-size: 12px;
|
|
233
|
+
text-align: left;
|
|
234
|
+
padding: 6px 31px;
|
|
235
|
+
border-radius: 9px;
|
|
236
|
+
width: 100%;
|
|
237
|
+
}
|
|
238
|
+
.ws-more:hover { background: var(--hover); color: var(--text-2); }
|
|
239
|
+
|
|
240
|
+
/* 折叠窄栏:工作区组头只留文件夹图标 */
|
|
241
|
+
body.sb-collapsed .ws-name,
|
|
242
|
+
body.sb-collapsed .ws-count,
|
|
243
|
+
body.sb-collapsed .ws-chev,
|
|
244
|
+
body.sb-collapsed .ws-more { display: none; }
|
|
245
|
+
body.sb-collapsed .ws-head { justify-content: center; padding: 0; }
|
|
246
|
+
|
|
172
247
|
.sb-foot { flex: none; padding-top: 18px; margin-top: 12px; }
|
|
173
248
|
|
|
174
249
|
/* ── 会话多选模式 ─────────────────────────── */
|
|
@@ -627,6 +702,29 @@ body:not(.has-msg) #composer-wrap {
|
|
|
627
702
|
#composer-bar { display: flex; align-items: center; gap: 6px; }
|
|
628
703
|
.cb-spacer { flex: 1; }
|
|
629
704
|
|
|
705
|
+
/* ── 欢迎页工作区选择器(初始页输入框左下角,麦克风左侧;会话开始后隐藏)── */
|
|
706
|
+
.ws-picker {
|
|
707
|
+
display: none;
|
|
708
|
+
align-items: center;
|
|
709
|
+
gap: 7px;
|
|
710
|
+
height: 32px;
|
|
711
|
+
padding: 0 10px 0 9px;
|
|
712
|
+
border: 1px solid var(--border);
|
|
713
|
+
border-radius: 10px;
|
|
714
|
+
background: transparent;
|
|
715
|
+
color: var(--text-2);
|
|
716
|
+
font-size: 12.5px;
|
|
717
|
+
cursor: pointer;
|
|
718
|
+
max-width: 240px;
|
|
719
|
+
}
|
|
720
|
+
body:not(.has-msg) .ws-picker { display: inline-flex; }
|
|
721
|
+
.ws-picker:hover { background: var(--hover); color: var(--text); }
|
|
722
|
+
.ws-picker svg { flex: none; }
|
|
723
|
+
.ws-picker .ws-folder { display: inline-flex; color: var(--text-3); }
|
|
724
|
+
.ws-picker .ws-label { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
725
|
+
.ws-picker .ws-chev { display: inline-flex; color: var(--text-3); transition: transform 200ms var(--ease); }
|
|
726
|
+
.ws-picker.open .ws-chev svg { transform: rotate(180deg); }
|
|
727
|
+
|
|
630
728
|
.icon-btn {
|
|
631
729
|
border: none;
|
|
632
730
|
background: none;
|
|
@@ -683,13 +781,56 @@ body:not(.has-msg) #slash-menu {
|
|
|
683
781
|
.slash-item .desc { color: var(--text-3); font-size: 12px; }
|
|
684
782
|
.slash-item.highlight { background: var(--hover); }
|
|
685
783
|
|
|
784
|
+
/* ── 工作区下拉浮层(欢迎页 composer 上方向弹出)── */
|
|
785
|
+
#ws-menu {
|
|
786
|
+
position: absolute;
|
|
787
|
+
bottom: calc(100% + 8px);
|
|
788
|
+
left: 32px;
|
|
789
|
+
width: min(420px, calc(100% - 64px));
|
|
790
|
+
max-height: 380px;
|
|
791
|
+
overflow-y: auto;
|
|
792
|
+
border-radius: 14px;
|
|
793
|
+
z-index: 30;
|
|
794
|
+
padding: 6px;
|
|
795
|
+
display: flex;
|
|
796
|
+
flex-direction: column;
|
|
797
|
+
gap: 2px;
|
|
798
|
+
}
|
|
799
|
+
body:not(.has-msg) #ws-menu { left: 96px; }
|
|
800
|
+
#ws-menu .ws-all {
|
|
801
|
+
display: flex;
|
|
802
|
+
align-items: center;
|
|
803
|
+
gap: 8px;
|
|
804
|
+
height: 34px;
|
|
805
|
+
padding: 0 10px;
|
|
806
|
+
border: none;
|
|
807
|
+
border-radius: 9px;
|
|
808
|
+
background: transparent;
|
|
809
|
+
cursor: pointer;
|
|
810
|
+
text-align: left;
|
|
811
|
+
width: 100%;
|
|
812
|
+
font-size: 13px;
|
|
813
|
+
font-weight: 600;
|
|
814
|
+
color: var(--text);
|
|
815
|
+
}
|
|
816
|
+
#ws-menu .ws-all:hover { background: var(--hover); }
|
|
817
|
+
#ws-menu .ws-all.current { background: var(--accent-soft); color: var(--accent); }
|
|
818
|
+
#ws-menu .ws-all.current .ws-folder,
|
|
819
|
+
#ws-menu .ws-all.current .ws-count { color: var(--accent); }
|
|
820
|
+
#ws-menu .ws-pick { color: var(--text-2); font-weight: 500; }
|
|
821
|
+
#ws-menu .ws-folder { display: inline-flex; flex: none; color: var(--text-2); }
|
|
822
|
+
#ws-menu .ws-name { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
823
|
+
#ws-menu .ws-count { flex: none; font-size: 11.5px; font-weight: 500; color: var(--text-3); }
|
|
824
|
+
#ws-menu .ws-empty { color: var(--text-3); font-size: 12.5px; padding: 10px; }
|
|
825
|
+
|
|
686
826
|
/* ── macOS 液态玻璃 / 亚克力材质:渐变半透明白面板 + 模糊 + 高光描边;
|
|
687
827
|
新浮层加 .glass 类即可复用(入场动画 glass-in 自带)── */
|
|
688
828
|
.glass,
|
|
689
829
|
#modal,
|
|
690
830
|
#confirm-box,
|
|
691
831
|
#slash-menu,
|
|
692
|
-
#context-menu
|
|
832
|
+
#context-menu,
|
|
833
|
+
#ws-menu {
|
|
693
834
|
background: var(--glass-gradient);
|
|
694
835
|
-webkit-backdrop-filter: var(--glass-blur);
|
|
695
836
|
backdrop-filter: var(--glass-blur);
|
|
@@ -950,6 +1091,7 @@ body:not(.has-msg) #slash-menu {
|
|
|
950
1091
|
#sidebar { transition: none; }
|
|
951
1092
|
.icon-btn.recording { animation: none; }
|
|
952
1093
|
.status-line { animation: none; }
|
|
953
|
-
.glass, #modal, #confirm-box, #slash-menu, #context-menu { animation: none; transition: none; }
|
|
1094
|
+
.glass, #modal, #confirm-box, #slash-menu, #context-menu, #ws-menu { animation: none; transition: none; }
|
|
954
1095
|
.glass-out { animation: none !important; }
|
|
1096
|
+
.collapse-body { transition: none; }
|
|
955
1097
|
}
|
package/package.json
CHANGED
package/pet/agents.cjs
CHANGED
|
@@ -166,8 +166,10 @@ const HOSHINO = {
|
|
|
166
166
|
// 重新生成顺序:skel_to_json → meshify_region → gen_sway(任何前一步都会覆盖后一步产物)
|
|
167
167
|
skelFile: "hoshino_spr.json",
|
|
168
168
|
atlasFile: "hoshino_spr.atlas.txt",
|
|
169
|
-
// bounds 1161×2074
|
|
170
|
-
|
|
169
|
+
// bounds 1161×2074:480×780 窗口下比例偏窄、高度先顶格 → 1.0 恰好满窗高。
|
|
170
|
+
// (1.1 是按旧 320 宽窗口调的:当时宽度先顶格、放大只裁两侧;现窗口加宽后纵向
|
|
171
|
+
// 也被放大 → 脚底/头顶溢出裁切,且调多高都按比例溢出,加高窗口无效)
|
|
172
|
+
extraScale: 1,
|
|
171
173
|
anims: {
|
|
172
174
|
idle: "Idle_01",
|
|
173
175
|
blink: "Eye_Close_01",
|
|
@@ -250,8 +252,11 @@ const KOHARU = {
|
|
|
250
252
|
// .json = skel_to_json 导出 + gen_sway 衣摆形变;重新生成顺序:skel_to_json → gen_sway
|
|
251
253
|
skelFile: "koharu_spr.json",
|
|
252
254
|
atlasFile: "koharu_spr.atlas.txt",
|
|
253
|
-
// bounds 1395×2069
|
|
254
|
-
|
|
255
|
+
// bounds 1395×2069:宽(含光环球)超宽,480×780 下宽度先顶格,1.0 身高仅 ~91%。
|
|
256
|
+
// 纵向不裁的上限 extra = 780/(2069×480/1395) ≈ 1.095,取 1.08 留 idle 摆动余量
|
|
257
|
+
// (身高 ≈98% 满窗;光环球两侧各裁 ~19px,与旧版 1.3 按比例裁两侧一致)。
|
|
258
|
+
// (1.3 是按旧 320 宽窗口调的,现窗口加宽后纵向溢出裁切脚底/头顶)
|
|
259
|
+
extraScale: 1.08,
|
|
255
260
|
anims: {
|
|
256
261
|
idle: "Idle_01",
|
|
257
262
|
blink: "Eye_Close_01",
|
package/pet/main.cjs
CHANGED
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
// 支持多角色同屏:ARONA_AGENT = 主 Agent,ARONA_SUB_AGENTS = 逗号分隔的子 Agent 列表;
|
|
4
4
|
// 每个角色一个 BrowserWindow,共享一个全屏特效窗。
|
|
5
5
|
const { app, BrowserWindow, ipcMain, screen } = require("electron");
|
|
6
|
+
const crypto = require("crypto");
|
|
7
|
+
const http = require("http");
|
|
6
8
|
const path = require("path");
|
|
7
9
|
const fs = require("fs");
|
|
8
10
|
const os = require("os");
|
|
@@ -36,7 +38,7 @@ const POS_FILE = path.join(os.homedir(), ".arona", "pet.json");
|
|
|
36
38
|
const WIN_W = 480; // 窗口 = Spine 角色渲染区本体;480 使宽骨架角色(kei 长发/aris 拖地发)满高显示不裁切。
|
|
37
39
|
// 透明区拦点击的副作用由「动态穿透」兜底:初始 setIgnoreMouseEvents(true,{forward}),
|
|
38
40
|
// renderer 按光标处 alpha 经 pet:clickable 切换(见 pet:clickable IPC)
|
|
39
|
-
const WIN_H =
|
|
41
|
+
const WIN_H = 780;
|
|
40
42
|
const SUB_OFFSET_X = 500; // 子窗口默认横向错开(略大于 WIN_W)
|
|
41
43
|
const SUB_OFFSET_Y = 40;
|
|
42
44
|
|
|
@@ -431,7 +433,7 @@ ipcMain.handle("pet:get-agent-config", (e) => {
|
|
|
431
433
|
return { id: agentId, isMain: agentId === MAIN_AGENT_ID, ...AGENTS[agentId] };
|
|
432
434
|
});
|
|
433
435
|
|
|
434
|
-
// ---- stdin
|
|
436
|
+
// ---- stdin 协议(非 Windows 平台的备用通道)----
|
|
435
437
|
let buffer = "";
|
|
436
438
|
|
|
437
439
|
function sendToAgent(agentId, channel, payload) {
|
|
@@ -517,6 +519,43 @@ process.stdin.on("end", () => {
|
|
|
517
519
|
if (process.platform !== "win32") app.quit();
|
|
518
520
|
});
|
|
519
521
|
|
|
522
|
+
// ---- 本地 HTTP 通道(backend → pet 主方向)----
|
|
523
|
+
// Windows 下 Electron GUI 子系统子进程的 stdin 数据不可达(GUI 白屏坑③同族问题,
|
|
524
|
+
// 症状隐蔽:桌宠静态显示正常,但 set_emotion/tts_level/spawn_agent/quit 全部失效)。
|
|
525
|
+
// 正常路径 = 127.0.0.1 随机端口 + 随机 token 的 HTTP 服务,端口/token 随 hello 行告知
|
|
526
|
+
// backend(防本机其他进程伪造);HTTP 起不来(httpPort=0)时 backend 退回上方 stdin 通道。
|
|
527
|
+
const HTTP_TOKEN = crypto.randomBytes(16).toString("hex");
|
|
528
|
+
let helloSent = false;
|
|
529
|
+
function sendHello(httpPort) {
|
|
530
|
+
if (helloSent) return;
|
|
531
|
+
helloSent = true;
|
|
532
|
+
send({ type: "hello", httpPort: httpPort || 0, token: httpPort ? HTTP_TOKEN : "" });
|
|
533
|
+
}
|
|
534
|
+
const petHttpServer = http.createServer((req, res) => {
|
|
535
|
+
if (req.method !== "POST" || req.headers["x-arona-token"] !== HTTP_TOKEN) {
|
|
536
|
+
res.writeHead(403);
|
|
537
|
+
res.end();
|
|
538
|
+
return;
|
|
539
|
+
}
|
|
540
|
+
let body = "";
|
|
541
|
+
req.on("data", (c) => (body += c));
|
|
542
|
+
req.on("end", () => {
|
|
543
|
+
res.writeHead(204);
|
|
544
|
+
res.end();
|
|
545
|
+
try {
|
|
546
|
+
handleMessage(JSON.parse(body));
|
|
547
|
+
} catch {
|
|
548
|
+
// 非 JSON,忽略
|
|
549
|
+
}
|
|
550
|
+
});
|
|
551
|
+
});
|
|
552
|
+
petHttpServer.on("error", () => {
|
|
553
|
+
sendHello(0); // HTTP 起不来也要发 hello(不带端口,backend 退回 stdin 写入)
|
|
554
|
+
});
|
|
555
|
+
petHttpServer.listen(0, "127.0.0.1", () => {
|
|
556
|
+
sendHello(petHttpServer.address().port);
|
|
557
|
+
});
|
|
558
|
+
|
|
520
559
|
app.whenReady().then(() => {
|
|
521
560
|
if (VERBOSE) {
|
|
522
561
|
// GPU 功能状态:确认 webgl/webgl2 是 hardware/software/disabled(Windows 白屏排查关键)
|
|
@@ -115,7 +115,7 @@ function waitLoad() {
|
|
|
115
115
|
function resize() {
|
|
116
116
|
const dpr = window.devicePixelRatio || 1;
|
|
117
117
|
const w = canvas.clientWidth || 320;
|
|
118
|
-
const h = canvas.clientHeight ||
|
|
118
|
+
const h = canvas.clientHeight || 780;
|
|
119
119
|
canvas.width = Math.round(w * dpr);
|
|
120
120
|
canvas.height = Math.round(h * dpr);
|
|
121
121
|
gl.viewport(0, 0, canvas.width, canvas.height);
|
|
@@ -128,7 +128,7 @@ function fitCamera() {
|
|
|
128
128
|
skeleton.getBounds(offset, size, []);
|
|
129
129
|
const pad = 8;
|
|
130
130
|
const cw = canvas.clientWidth || 320;
|
|
131
|
-
const ch = canvas.clientHeight ||
|
|
131
|
+
const ch = canvas.clientHeight || 780;
|
|
132
132
|
const scale = Math.min(cw / (size.x + pad * 2), ch / (size.y + pad * 2));
|
|
133
133
|
renderer.camera.setViewport(cw / scale, ch / scale);
|
|
134
134
|
renderer.camera.position.set(offset.x + size.x / 2, offset.y + size.y / 2, 0);
|
|
@@ -139,7 +139,7 @@ function fitCamera() {
|
|
|
139
139
|
// 窗口 CSS px → 骨架世界坐标(逆相机变换)
|
|
140
140
|
function windowToSkeleton(x, y) {
|
|
141
141
|
const cw = canvas.clientWidth || 320;
|
|
142
|
-
const ch = canvas.clientHeight ||
|
|
142
|
+
const ch = canvas.clientHeight || 780;
|
|
143
143
|
const p = new spine.webgl.Vector3(x, y, 0);
|
|
144
144
|
renderer.camera.screenToWorld(p, cw, ch);
|
|
145
145
|
return { x: p.x, y: p.y };
|
|
@@ -170,7 +170,7 @@ function zoom(cap, src) {
|
|
|
170
170
|
app.whenReady().then(() => {
|
|
171
171
|
fs.mkdirSync(OUT, { recursive: true });
|
|
172
172
|
win = new BrowserWindow({
|
|
173
|
-
width: 480, height:
|
|
173
|
+
width: 480, height: 780, transparent: true, frame: false, show: true,
|
|
174
174
|
// 窗口被遮挡/后台时 rAF 与 DOM 合成会被节流 → capturePage 拿到过期纹理;关闭节流
|
|
175
175
|
webPreferences: { preload: path.join(PET, "preload.cjs"), contextIsolation: true, nodeIntegration: false, backgroundThrottling: false },
|
|
176
176
|
});
|
|
@@ -78,7 +78,7 @@ function zoom(cap, src) {
|
|
|
78
78
|
app.whenReady().then(() => {
|
|
79
79
|
fs.mkdirSync(OUT, { recursive: true });
|
|
80
80
|
win = new BrowserWindow({
|
|
81
|
-
width: 480, height:
|
|
81
|
+
width: 480, height: 780, transparent: true, frame: false, show: true,
|
|
82
82
|
webPreferences: { preload: path.join(PET, "preload.cjs"), contextIsolation: true, nodeIntegration: false, backgroundThrottling: false },
|
|
83
83
|
});
|
|
84
84
|
win.webContents.on("console-message", (_e, _a, b) => console.log("[R]", b));
|
|
Binary file
|
|
Binary file
|
package/python/stt.py
CHANGED
|
@@ -18,6 +18,7 @@ sentence_end=true 为最终结果,聚合后输出到 stdout。
|
|
|
18
18
|
import json
|
|
19
19
|
import os
|
|
20
20
|
import sys
|
|
21
|
+
import threading
|
|
21
22
|
import uuid
|
|
22
23
|
import asyncio
|
|
23
24
|
import signal
|
|
@@ -31,8 +32,8 @@ MAX_RECORDING_SECONDS = 15
|
|
|
31
32
|
SILENCE_THRESHOLD = 500 # RMS 阈值
|
|
32
33
|
SILENCE_DURATION_MS = 1500 # 静音超过此时长则结束
|
|
33
34
|
|
|
34
|
-
# 优雅停止标志:SIGUSR1/SIGINT(
|
|
35
|
-
#
|
|
35
|
+
# 优雅停止标志:SIGUSR1/SIGINT(POSIX)或 stdin "stop" 行(Windows,见 main)置位后,
|
|
36
|
+
# 录音循环提前跳出,走正常 finish-task 流程把已说内容识别出来(区别于 SIGTERM 直接终止=丢弃)。
|
|
36
37
|
_stop_recording = False
|
|
37
38
|
|
|
38
39
|
|
|
@@ -41,6 +42,24 @@ def _request_stop(*_args):
|
|
|
41
42
|
_stop_recording = True
|
|
42
43
|
|
|
43
44
|
|
|
45
|
+
def _watch_stdin_stop():
|
|
46
|
+
"""监听 stdin 的 "stop" 行置位停止标志。
|
|
47
|
+
|
|
48
|
+
Windows 上进程信号不可捕获(Node proc.kill 全部等效硬杀),Node 侧优雅停止改发
|
|
49
|
+
stdin "stop" 行。EOF 不触发停止——Node 侧可能在识别结束前就关闭了 stdin 管道。
|
|
50
|
+
"""
|
|
51
|
+
try:
|
|
52
|
+
while True:
|
|
53
|
+
line = sys.stdin.readline()
|
|
54
|
+
if not line:
|
|
55
|
+
return
|
|
56
|
+
if line.strip().lower() == "stop":
|
|
57
|
+
_request_stop()
|
|
58
|
+
return
|
|
59
|
+
except Exception:
|
|
60
|
+
return
|
|
61
|
+
|
|
62
|
+
|
|
44
63
|
def build_uri(workspace_id):
|
|
45
64
|
if workspace_id:
|
|
46
65
|
return f"wss://{workspace_id}.cn-beijing.maas.aliyuncs.com/api-ws/v1/inference"
|
|
@@ -242,7 +261,8 @@ def main():
|
|
|
242
261
|
print(t("STT: 未设置 QWEN_STT_API_KEY", "STT: QWEN_STT_API_KEY not set"), file=sys.stderr)
|
|
243
262
|
sys.exit(1)
|
|
244
263
|
|
|
245
|
-
#
|
|
264
|
+
# 优雅停止:提前结束录音但仍识别已上传内容。
|
|
265
|
+
# POSIX:SIGUSR1/SIGINT 信号;Windows:信号不可捕获,Node 侧发 stdin "stop" 行(监视线程)。
|
|
246
266
|
for sig_name in ("SIGUSR1", "SIGINT"):
|
|
247
267
|
sig = getattr(signal, sig_name, None)
|
|
248
268
|
if sig is not None:
|
|
@@ -250,6 +270,7 @@ def main():
|
|
|
250
270
|
signal.signal(sig, _request_stop)
|
|
251
271
|
except (ValueError, OSError):
|
|
252
272
|
pass
|
|
273
|
+
threading.Thread(target=_watch_stdin_stop, daemon=True).start()
|
|
253
274
|
|
|
254
275
|
text = asyncio.run(stt_recognize(api_key, workspace_id, model, audio_format, sample_rate))
|
|
255
276
|
print(text)
|
package/src/agent.ts
CHANGED
|
@@ -25,6 +25,7 @@ import { getMainAgent, getAgentLabel, type SubAgentId, type AgentId } from "./ag
|
|
|
25
25
|
import { speakerContextExtension } from "./speaker_context.ts";
|
|
26
26
|
import { gestureContextExtension } from "./gesture_context.ts";
|
|
27
27
|
import { t, getLang } from "./locale.ts";
|
|
28
|
+
import { currentWorkspace } from "./workspace.ts";
|
|
28
29
|
import { nowStr, reserveTokensFor } from "./prompt_utils.ts";
|
|
29
30
|
|
|
30
31
|
function buildSystemPrompt(memoryContent: string): string {
|
|
@@ -497,7 +498,7 @@ export async function initAgent(): Promise<{
|
|
|
497
498
|
modelRuntime,
|
|
498
499
|
});
|
|
499
500
|
if (cliModel.error) {
|
|
500
|
-
console.warn(`Model resolution warning: ${cliModel.error}`);
|
|
501
|
+
console.warn(t(`模型解析警告:${cliModel.error}`, `Model resolution warning: ${cliModel.error}`));
|
|
501
502
|
}
|
|
502
503
|
|
|
503
504
|
// 3. Load memory
|
|
@@ -507,7 +508,7 @@ export async function initAgent(): Promise<{
|
|
|
507
508
|
|
|
508
509
|
// 4. Create ResourceLoader with custom system prompt
|
|
509
510
|
const loader = new DefaultResourceLoader({
|
|
510
|
-
cwd:
|
|
511
|
+
cwd: currentWorkspace(),
|
|
511
512
|
agentDir: ARONA_DIR,
|
|
512
513
|
systemPromptOverride: () => buildSystemPrompt(memoryContent),
|
|
513
514
|
appendSystemPromptOverride: () => [],
|
|
@@ -546,7 +547,7 @@ export async function initAgent(): Promise<{
|
|
|
546
547
|
// 压缩阈值:按上下文窗口动态推导 reserveTokens(约 85% 水位触发压缩),
|
|
547
548
|
// 避免硬编码 150000 导致小窗口模型(如 DeepSeek 64K/128K)每轮误触发压缩
|
|
548
549
|
// (多一次总结调用 + 前缀每轮变化 → 缓存永不命中)。
|
|
549
|
-
const settingsManager = SettingsManager.create(
|
|
550
|
+
const settingsManager = SettingsManager.create(currentWorkspace(), ARONA_DIR);
|
|
550
551
|
settingsManager.applyOverrides({
|
|
551
552
|
compaction: {
|
|
552
553
|
enabled: true,
|
|
@@ -555,7 +556,7 @@ export async function initAgent(): Promise<{
|
|
|
555
556
|
},
|
|
556
557
|
});
|
|
557
558
|
const { session } = await createAgentSession({
|
|
558
|
-
cwd:
|
|
559
|
+
cwd: currentWorkspace(),
|
|
559
560
|
agentDir: ARONA_DIR,
|
|
560
561
|
model: cliModel.model,
|
|
561
562
|
thinkingLevel: config.thinkingLevel as any,
|
|
@@ -682,7 +683,7 @@ function buildSubSystemPrompt(id: SubAgentId, memoryContent: string): string {
|
|
|
682
683
|
- You are ${getAgentLabel(id)}. Always speak as yourself — do not play, mimic, or mix in the identity or tone of any other character (Arona, Plana, Shiroko, Hoshino, Hanako, Koharu, Kei, Aris).
|
|
683
684
|
- After the main agent finishes replying, each enabled sub-agent takes a turn. Keep your reply SHORT (one or two sentences), natural, in-character, and add nothing but your own spoken line.
|
|
684
685
|
- Do not repeat or summarize the main agent's reply.
|
|
685
|
-
- In the conversation history, assistant messages carry a \`Name:\` prefix showing who said them (e.g. "Arona:", "Shiroko:", etc.); user inputs are Sensei speaking. When you reply, do NOT add any name prefix.
|
|
686
|
+
- In the conversation history, assistant messages carry a \`Name:\` prefix showing who said them (e.g. "Arona:", "Shiroko:", etc.); user inputs are Sensei speaking. When you reply, do NOT add any name prefix — never start your reply with your own name and a colon (e.g. "${getAgentLabel(id)}:"), not even in a script/subtitle style. Output only the words you actually say.
|
|
686
687
|
- Your reply may be read aloud by TTS; keep each sentence under 50 characters/words when possible.
|
|
687
688
|
- If you have nothing to add, call keep_silent instead of writing filler text.
|
|
688
689
|
|
|
@@ -696,7 +697,7 @@ Available tools: change_emotion (set the emotion before speaking), keep_silent (
|
|
|
696
697
|
- 你是${getAgentLabel(id)}。始终以自己的身份发言,禁止扮演或模仿或混用其他角色(阿洛娜、普拉娜、白子、星野、花子、小春、凯伊、爱丽丝)的身份与语气。
|
|
697
698
|
- 主 Agent 回复完毕后,每个启用的子 Agent 依次发言。回复保持简短(一两句),贴角色,只说自己的台词。
|
|
698
699
|
- 不要复读或总结主 Agent 的话。
|
|
699
|
-
- 对话历史中,assistant 消息带「角色名:」前缀标明发言者(如「阿洛娜:」「砂狼白子:」等);用户输入是 Sensei
|
|
700
|
+
- 对话历史中,assistant 消息带「角色名:」前缀标明发言者(如「阿洛娜:」「砂狼白子:」等);用户输入是 Sensei 说的。你发言时不要加名字前缀——绝不要以「自己的名字+冒号」(如「${getAgentLabel(id)}:」)开头,也不要写成剧本/字幕格式,直接说台词本身。
|
|
700
701
|
- 你的回复可能被 TTS 朗读,尽量每句 <50 字。
|
|
701
702
|
- 无话可说就调用 keep_silent,不要写废话。
|
|
702
703
|
|
|
@@ -734,7 +735,7 @@ export async function initSubAgent(
|
|
|
734
735
|
): Promise<{ session: AgentSession; loader: DefaultResourceLoader; customTools: ToolDefinition[] }> {
|
|
735
736
|
const cliModel = resolveCliModel({ cliModel: config.model, modelRuntime });
|
|
736
737
|
if (cliModel.error) {
|
|
737
|
-
console.warn(`Model resolution warning (${agentId}): ${cliModel.error}`);
|
|
738
|
+
console.warn(t(`模型解析警告(${agentId}):${cliModel.error}`, `Model resolution warning (${agentId}): ${cliModel.error}`));
|
|
738
739
|
}
|
|
739
740
|
|
|
740
741
|
const memoryContent = loadMemory();
|
|
@@ -742,7 +743,7 @@ export async function initSubAgent(
|
|
|
742
743
|
// 子 Agent 若再次快照会把基线推进到当前内容,导致主 Agent 首轮 getMemoryDelta() 漏报
|
|
743
744
|
// 启动后、首次输入前发生的 MEMORY.md 变更。
|
|
744
745
|
const loader = new DefaultResourceLoader({
|
|
745
|
-
cwd:
|
|
746
|
+
cwd: currentWorkspace(),
|
|
746
747
|
agentDir: ARONA_DIR,
|
|
747
748
|
// 子 Agent 默认不注入项目文档(CLAUDE.md/AGENTS.md)——SDK 会按 cwd 自动把上下文文件
|
|
748
749
|
// 塞进 system prompt,子 Agent 是纯聊天角色、不需要工作目录约定。需要时由 Agent 主动
|
|
@@ -766,7 +767,7 @@ export async function initSubAgent(
|
|
|
766
767
|
...(config.tavilyApiKey ? premiumTavilyTools : []),
|
|
767
768
|
];
|
|
768
769
|
|
|
769
|
-
const settingsManager = SettingsManager.create(
|
|
770
|
+
const settingsManager = SettingsManager.create(currentWorkspace(), ARONA_DIR);
|
|
770
771
|
settingsManager.applyOverrides({
|
|
771
772
|
compaction: {
|
|
772
773
|
enabled: true,
|
|
@@ -776,7 +777,7 @@ export async function initSubAgent(
|
|
|
776
777
|
});
|
|
777
778
|
|
|
778
779
|
const { session } = await createAgentSession({
|
|
779
|
-
cwd:
|
|
780
|
+
cwd: currentWorkspace(),
|
|
780
781
|
agentDir: ARONA_DIR,
|
|
781
782
|
model: cliModel.model,
|
|
782
783
|
thinkingLevel: config.thinkingLevel as any,
|
package/src/agent_registry.ts
CHANGED
|
@@ -52,6 +52,36 @@ export function getAgentLabel(id: AgentId): string {
|
|
|
52
52
|
}
|
|
53
53
|
}
|
|
54
54
|
|
|
55
|
+
/**
|
|
56
|
+
* 角色名的全部写法(中英文全名/常用短名),供输出侧剥离「名字:」前缀用。
|
|
57
|
+
* 模型偶发模仿历史消息的「角色名:」前缀,且可能用短名(如「星野:」)而非
|
|
58
|
+
* getAgentLabel 的全名,两种都要覆盖。与 locale 无关:中英文都列出。
|
|
59
|
+
*/
|
|
60
|
+
export function getAgentNameVariants(id: AgentId): string[] {
|
|
61
|
+
switch (id) {
|
|
62
|
+
case "arona":
|
|
63
|
+
return ["阿洛娜", "Arona"];
|
|
64
|
+
case "plana":
|
|
65
|
+
return ["普拉娜", "Plana"];
|
|
66
|
+
case "shiroko":
|
|
67
|
+
return ["砂狼白子", "白子", "Shiroko"];
|
|
68
|
+
case "hoshino":
|
|
69
|
+
return ["小鸟游星野", "星野", "Hoshino"];
|
|
70
|
+
case "hanako":
|
|
71
|
+
return ["浦和花子", "花子", "Hanako"];
|
|
72
|
+
case "koharu":
|
|
73
|
+
return ["下江小春", "小春", "Koharu"];
|
|
74
|
+
case "kei":
|
|
75
|
+
return ["天童凯伊", "凯伊", "Kei"];
|
|
76
|
+
case "aris":
|
|
77
|
+
return ["天童爱丽丝", "爱丽丝", "Aris"];
|
|
78
|
+
case "millennium":
|
|
79
|
+
return ["千禧年学员", "Millennium Student"];
|
|
80
|
+
case "justice":
|
|
81
|
+
return ["正义实现部成员", "Justice Task Force Member"];
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
55
85
|
/** 校验 agent id(含类型收窄) */
|
|
56
86
|
export function isValidAgentId(id: string): id is AgentId {
|
|
57
87
|
return (AGENT_IDS as readonly string[]).includes(id);
|
package/src/coding_agent.ts
CHANGED
|
@@ -19,7 +19,8 @@ import { computerUseTools } from "./tools/computer_use.ts";
|
|
|
19
19
|
import { webSearchTool, webExtractTool, premiumTavilyTools } from "./tools/tavily_tools.ts";
|
|
20
20
|
import { type CodingAgentId } from "./agent_registry.ts";
|
|
21
21
|
import { nowStr, reserveTokensFor } from "./prompt_utils.ts";
|
|
22
|
-
import { getLang } from "./locale.ts";
|
|
22
|
+
import { getLang, t } from "./locale.ts";
|
|
23
|
+
import { currentWorkspace } from "./workspace.ts";
|
|
23
24
|
|
|
24
25
|
const CODING_PERSONA_ZH: Record<CodingAgentId, string> = {
|
|
25
26
|
millennium: `你是千年科技学园工程部所属的学生,研究员气质,严谨精确,接受来自什亭之箱方面的委托。
|
|
@@ -140,11 +141,11 @@ export async function initCodingAgent(
|
|
|
140
141
|
): Promise<{ session: AgentSession; loader: DefaultResourceLoader; customTools: ToolDefinition[] }> {
|
|
141
142
|
const cliModel = resolveCliModel({ cliModel: config.model, modelRuntime });
|
|
142
143
|
if (cliModel.error) {
|
|
143
|
-
console.warn(`Model resolution warning (${agentId}): ${cliModel.error}`);
|
|
144
|
+
console.warn(t(`模型解析警告(${agentId}):${cliModel.error}`, `Model resolution warning (${agentId}): ${cliModel.error}`));
|
|
144
145
|
}
|
|
145
146
|
|
|
146
147
|
const loader = new DefaultResourceLoader({
|
|
147
|
-
cwd:
|
|
148
|
+
cwd: currentWorkspace(),
|
|
148
149
|
agentDir: ARONA_DIR,
|
|
149
150
|
// 不设 noContextFiles:与主 Agent 一致走 SDK 默认注入链路(CLAUDE.md/AGENTS.md)
|
|
150
151
|
systemPromptOverride: () => buildCodingSystemPrompt(agentId),
|
|
@@ -162,7 +163,7 @@ export async function initCodingAgent(
|
|
|
162
163
|
...mcpTools,
|
|
163
164
|
];
|
|
164
165
|
|
|
165
|
-
const settingsManager = SettingsManager.create(
|
|
166
|
+
const settingsManager = SettingsManager.create(currentWorkspace(), ARONA_DIR);
|
|
166
167
|
settingsManager.applyOverrides({
|
|
167
168
|
compaction: {
|
|
168
169
|
enabled: true,
|
|
@@ -172,7 +173,7 @@ export async function initCodingAgent(
|
|
|
172
173
|
});
|
|
173
174
|
|
|
174
175
|
const { session } = await createAgentSession({
|
|
175
|
-
cwd:
|
|
176
|
+
cwd: currentWorkspace(),
|
|
176
177
|
agentDir: ARONA_DIR,
|
|
177
178
|
model: cliModel.model,
|
|
178
179
|
thinkingLevel: config.thinkingLevel as any,
|