diff --git a/CLAUDE.md b/CLAUDE.md index 55b8b3920..f1a14766a 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -343,6 +343,29 @@ Loopback requests bypass daemon token auth. Non-loopback requests require `X-ACE The frontend has pure helpers under [web/src/lib/](web/src/lib) with Node-based tests. Prefer adding data-shaping logic there rather than embedding it directly in components. +### 消息通道(QQ / 微信 / 飞书 / 钉钉 / Telegram / Discord / LINE,openspec add-desktop-im-channels) + +用户文档 [docs/channels.md](docs/channels.md),接口 [docs/daemon-api.md](docs/daemon-api.md) 第 19 节。分三层,方向只能向下:`src/adapters/im/<平台>/` 是平台传输层(QQ 官方机器人 v2 网关、微信 iLink 长轮询、飞书长连接、钉钉 Stream、Telegram 长轮询、Discord 网关、LINE webhook),不认识会话;`src/host/channels/core/` 是核心(存储、访问与配对、命令、出站投影、会话路由、平台运行时、宿主);`src/apps/daemon/im_channels.cpp` 装配(worker.cpp 已接近 1000 行上限,装配与 `/rc` 共用的 `rc_session_catalog` 都拆在外面)。WhatsApp(`src/host/channels/*.cpp` 其余文件)不经过这套核心。 + +- **平台清单只有一张表** `core/platforms.cpp`(`PlatformSpec`:凭据字段与是否密钥 / 可手填、账号字段、群成员身份是否按群隔离、换机器人是否清空机主与授权、机主绑定方式 Scan / Link / Code)。运行时、宿主、Web 接口、快照的 `credentials_public`(非密钥原样、密钥只给尾号)都按这张表走;认识各平台传输层类型的只有 `core/platform_adapters.cpp`(创建传输层、联网校验凭据、扫码流程),测试改地址走 `HostEndpoints`。加平台 = 加一行描述 + 一个 adapters 分支 + `Address::valid` 放行平台名。 +- **机主绑定三种**:QQ / 微信扫码时带出扫码人;Telegram 用 `/start <码>` 链接;飞书、钉钉、Discord、LINE 在连上之后由 `owner-link` 发 6 位一次性绑定码(`AccessControl::issue_owner_pin`,10 分钟、一次有效、码存在期间错 10 次作废),私聊发送这串数字即成为机主,这条消息不进会话。 +- **新代码过 R15 所有权检查(CI `check_ownership --strict --final`):** 不写裸 `std::thread`,线程用 `JoiningThread(&Class::method, this)`;存起来的回调捕获 `lifetime_.ref(*this)`,`LifetimeToken` 声明为最后一个成员、析构函数体先 `stop()`;同步回调用具名捕获,不要 `[&]` / `[this]`。未跟踪的新文件不在 lint 范围里,本地要用临时索引把它们加进去再跑。 +- **锁:** `PlatformRuntime` 的 `ops_mu_` 串行化开关 / 改凭据 / 接管 / 停止,`process_mu_` 让入站处理与断开互斥,`mu_` 只护状态字段;调 `transport->stop()` 时不能持 `mu_`(传输线程的 `on_status` 要拿它)。传输层在自己的锁外回调 `on_status`,快照在本对象锁外读 `transport->status()`,两边不会互锁。 +- **只给本机:** 路由用 `is_trusted_local_client_address`,远程 Web 代理转发进来的 `127.0.0.2` 也是 403;`WebServer::broadcast_local_event` 只推给 `WsConnState::trusted_local` 的连接 —— `channels_bind` 带扫码二维码内容。凭据只回尾号,二维码内容与密钥不写日志。 +- **会话列表 `channel_bound:{platform}`** 由 `sessions_for_workspace` 统一标注,侧栏显示与 `/rc` 相同的电脑图标;`/rc` 的换绑动画等专属行为仍只看 `remote_control_bound`,别把两者合并。 +- **连接后由工作线程为已有绑定恢复出站投影**(`Conversations::restore`),否则重启后在 Desktop 里输入的回复不会发到 IM。QQ 换 AppID 时清空机主与授权名单(用户 openid 按机器人隔离),并丢弃旧机器人下的绑定。 +- **QQ:** 全进程单调 `msg_seq`;被动回复额度按入站 msg_id 记账,被拒先降级主动消息,再被拒才暂存(只在内存,下一条入站时先补发);Markdown 被拒后改纯文本且不再尝试;超过直传上限的文件改发文字说明。扫码协议见 `qq_bind.hpp` 文件头(腾讯 connector 无开源许可,只参照协议)。 +- **Telegram:** update offset 存 `state.json` 游标,重启不重复处理;409 分 webhook(等用户在页面确认移除)与另一个轮询方(60 秒后重试)两种。 +- **Telegram 在 getMe 成功后就置已连接**,不要改回等第一次长轮询返回 —— 没有新消息时真实平台要等满 25 秒,设置页会一直停在“连接中”。 +- **设置页只给简要状态,细节写日志**(2026-10-05 验收反馈,仿 WorkBuddy「远程通道」):两列卡片、单色聊天图标、「连接 / 取消连接」按钮;没有凭据或没有机主时「连接」打开三步向导(创建机器人 → 连接 → 绑定机主),配好的直接连。失败原因、隐私模式、暂存待补发、绑定与收发计数由 `core/state_log.cpp` 在每次 `publish_state` 比较前后快照写进 daemon 日志(`[channels/<平台>]` 前缀,凭据尾号也不写);入站判定原因(`AccessResult::reason`)、IM 命令、QQ 扫码各阶段与发送回退在各自位置记日志。前端 `channelsSettings.test.js` 有守卫断言页面不渲染这些细节,别加回页面。`ChannelHost::publish_all` 也走 `publish_state`,否则绑定变化不进日志。 +- **微信(iLink)**:凭据全部来自扫码(`weixin_login`,二维码编码 `qrcode_img_content` 而不是 32 位令牌),设置页不能手填,校验只查齐全不联网。轮询游标与每个联系人的 `context_token` 存在 store 的 `values`(`weixin_cursor` / `weixin_context_tokens`,换机器人随 `clear_transport_state` 清掉);主动发送(Desktop 里输入的回复)也要带 context_token。-14 = 登录失效 → Failed 且停止重试,设置页点「连接」直接回到扫码。只有私聊。CDN 媒体 AES-128-ECB,上传结果在响应头 `x-encrypted-param`(`im::HttpResponse::headers` 键已小写)。 +- **钉钉 Stream**:校验 = 取 access token + 试注册一次 Stream(票据丢弃),能分出凭据错 / 应用未发布或没开 Stream 模式。每条入站要 ACK;回复优先用消息带来的 sessionWebhook,过期后走机器人 OpenAPI(要检查 errcode,HTTP 200 也可能失败)。同一应用每条消息只投给随机一个连接,靠核心的按账号文件锁保证只有一个 ACECode 在连。 +- **飞书长连接**:protobuf 帧(`feishu_frame`),每个事件帧都要 ACK,分片按 sum/seq 重组;飞书要求长连接**在线时**才能保存「使用长连接接收事件」,所以机器人未发布 / 未就绪只在 `extra.bot_warning` 里提示、不判校验失败,连接步骤的清单告诉用户回后台保存订阅、加 `im.message.receive_v1` 并发布。`domain`=lark 走 open.larksuite.com,测试经 `HostEndpoints::feishu_base` 覆盖。握手失败的响应头(`Handshake-Status` 等)`WebSocketClient` 拿不到,403 / 连接数超限目前按可重试处理。飞书、钉钉都按集群模式把事件随机分给同一应用的某个连接,同一应用接在别的工具上会被分走消息。 +- **LINE**:唯一要公网回调的平台。回调端口是 `core/line_webhook_server`(独立 Crow 应用,只监听 127.0.0.1,只有 webhook / health / media 三条路由,**绝不复用 daemon 主端口** —— 本机请求在主端口免 token,经隧道进来的也是本机地址);端口第一次由系统分配后存 cursor `line_listen_port` 沿用。公网地址留空 → 起 cloudflared 快速隧道(找不到时 `extra.cloudflared_missing`,设置页提示 winget 安装),每次连接用 PUT 重新登记 webhook;控制台的 Use webhook 只能手动开,没开时停在 Retrying(`extra.webhook_active=false`,设置页给出该做什么)。回复令牌 50 秒内免费用 reply,否则 push(计月额度,额度用完暂存到下一条消息再补发);机器人不能发文件,图片走回调端口的临时链接。 +- **Discord**:校验时 `/applications/@me` 报 Message Content Intent 关着就直接拒绝保存(否则网关 4014);保存时带回 `bot_id` / `application_id`(设置页邀请链接用)。优先 Resume,Identify 每 24 小时有上限,计数经 `on_identify_ledger` 存两个 cursor 跨重启保留;4004 / 4010–4014 停止重试。服务器频道与线程算群,@ 或回复机器人才处理;不 ping @everyone。 +- **Telegram 群隐私模式开启时 @机器人 的消息照常送达**(BotFather 默认即开启,正好符合“群里只处理 @”);关闭后机器人收到群里全部消息,仍只处理 @ 它的。早期 spec 把这条写反了,已更正。 +- 回归:`tests/daemon/im_channels_e2e_test.cpp`(生产装配 + 真实 SessionRegistry / 脚本化模型 + 真实传输层对假平台,覆盖验收清单里不依赖真实账号的部分)、`tests/im/`、`tests/network/websocket_client_test.cpp`、`tests/platform/aes_gcm_test.cpp`、`tests/channels/core/`(核心用内存版 SessionClient 与假传输层,宿主测试含两个宿主争同一把锁;`state_log_test.cpp` 守着日志内容与“不写凭据”)、`tests/web/channels_handler_test.cpp`、`web_server_smoke_test.cpp` 的 `ChannelRoutesRejectNonLocalClients` 等三条,前端 `channelsSettings.test.js` 与 `desktopNotify.test.js`。各平台假服务在 `tests/test_support/im/fake_*_server.hpp`(端到端测试只在用到的用例里创建微信 / 飞书 / 钉钉 / Discord / LINE 假服务,传输层参数经 `ImChannelDeps::tune_services` 调整),所有测试都不连真实平台。 + ## Desktop Shell The desktop shell runs a webview against workspace-local daemon processes. It does not change daemon internals; each daemon still serves one current working directory. Workspace switching currently uses whole-page navigation so browser origin follows the active loopback port. @@ -581,6 +604,10 @@ SidePanel 折叠 UI:`ChatView` 把 `SidePanel` 包到 ` {stats.length > 0 && (
+ {task.lastTool && } {stats.join(' · ')}
)} diff --git a/web/src/components/ToolBlock.jsx b/web/src/components/ToolBlock.jsx index f6272fcde..3dfc4861d 100644 --- a/web/src/components/ToolBlock.jsx +++ b/web/src/components/ToolBlock.jsx @@ -406,7 +406,7 @@ export const ToolBlock = memo(function ToolBlock({ entry, onReviewToggle, sessio : 'submit'} > } + icon={} label={completedSummary.verb || title || tool || 'AskUserQuestion'} detail={completedSummary.object || ''} trailing={liveElapsed > 0 ? {formatElapsed(liveElapsed)} : null} @@ -438,7 +438,7 @@ export const ToolBlock = memo(function ToolBlock({ entry, onReviewToggle, sessio className="ace-tool-activity min-w-0" > } + icon={} label={completedSummary.verb || title || tool || '工具完成'} detail={bashCommand || completedSummary.object || ''} trailing={( @@ -498,6 +498,8 @@ export const ToolBlock = memo(function ToolBlock({ entry, onReviewToggle, sessio > } spinnerStatic={!liveProgress} label={genericSummary.verb || title || displayOverride || tool || '正在执行工具'} detail={bashCommand || genericSummary.object || ''} diff --git a/web/src/i18n/sourceCatalog.generated.js b/web/src/i18n/sourceCatalog.generated.js index 367ad002e..abeb8b32a 100644 --- a/web/src/i18n/sourceCatalog.generated.js +++ b/web/src/i18n/sourceCatalog.generated.js @@ -23,10 +23,12 @@ export const sourceCatalogs = { "s_01bf3b34f431074f": "此页面不符合 App Transport Security 的安全连接要求。", "s_01ca13e5a1ebdc8d": "勾选的条目固定显示在侧栏,其余条目收在「扩展」菜单里;拖动行可调整顺序。", "s_01e1429e4860a2d5": "每周代码变更总结", + "s_01ebb09c9bad848f": "飞书(中国)", "s_020754edb04dde8d": "macOS 系统通知权限", "s_0215ff0b3cce53f1": "反馈内容", "s_0220aafc260ee131": "启用 hooks 功能后,已配置的钩子会显示在此处", "s_025486bcb86499e3": "请选择摘要模型", + "s_027be81bc401b0cd": "连接失败,原因已写入日志。确认凭据无误后重试,或回到上一步更换。", "s_029d7b3375ad16aa": "写入规则文件", "s_02b411ce6d16f108": "动态上下文", "s_02bcdbc5a1453cb0": "复制相对路径", @@ -45,8 +47,10 @@ export const sourceCatalogs = { "s_051836569928a9f9": "编辑", "s_054b73a972372bdb": "未找到可用终端", "s_05564f10bcba6ae8": "同一专家可属于多个 Tag", + "s_057dc352ed0a0a29": "更换机器人", "s_0580fcee37e02012": "删除失败:", "s_05a531a083e68807": "复制 {{p0}} 验证码 {{p1}}", + "s_05abefa30a624a8a": "派发任务", "s_05df17e0e401d645": "让 Agent 帮你重构、修 bug、加测试", "s_05efef8465e0c666": "git 响应超时(仓库可能很大)", "s_05fdf30411d98d32": "沙箱", @@ -67,6 +71,7 @@ export const sourceCatalogs = { "s_07918dbd4c70b182": "缩小预览", "s_07a10608cd7a1e0c": "关闭后沙箱内命令无法联网(macOS / Linux 真隔离)", "s_07b0e6eadae125e1": "目标版本", + "s_07bc10c7295f8dae": "机主与授权名单", "s_07bf8b28ae3680e0": "专家组件类型", "s_07deea1ebeb8c9f8": "读取失败 (HTTP {{p0}})", "s_07ee23a0924d3d0b": "由于你中断了当前响应,队列已暂停", @@ -82,6 +87,7 @@ export const sourceCatalogs = { "s_08af65aa6bfa1a95": "手写的规则文件只读展示,请直接编辑文件;解析失败的文件整体不生效。", "s_08ba7ec0955d1d28": "正在启动浏览器", "s_08d43bdc4dcb6253": "可更新", + "s_08e5a3f74d4c6894": "用手机 QQ 扫一扫,按提示创建并授权机器人", "s_08e9d9ef7d382947": "仅拦截写入", "s_08f10fb5ebba9e65": "使用深色背景,关闭后使用浅色背景", "s_0970eb5b921e82c5": "{{p0}} 行", @@ -106,6 +112,7 @@ export const sourceCatalogs = { "s_0acd4d885ae0c69a": "源文件夹", "s_0ad947db2d9ee20a": "daemon 退出时运行被中断", "s_0ade55ea5a89bf7d": "写文件失败", + "s_0adea32a9fd1dcbe": "扫码成功,你已是机主", "s_0ae64c633fbe0f42": "检查过期依赖、安全风险和可控范围内的技术债。", "s_0ae7312510f4b495": "收起详情", "s_0af42a6e2915abaa": "删除记忆失败", @@ -131,6 +138,7 @@ export const sourceCatalogs = { "s_0c8320ac03732e4c": "需要访问令牌", "s_0c869dac8a4aa550": "请输入 bash.exe 完整路径", "s_0cc2ca1cf8347d79": "任务完成通知已关闭", + "s_0ced3f40670a5ac1": "Lark(海外)", "s_0cf8b14f4651f4de": "上周", "s_0d0cbac2eee54113": "重命名", "s_0d0e1a86b3aa7877": "用户", @@ -142,12 +150,14 @@ export const sourceCatalogs = { "s_0d94ad38844747ce": "切换失败:", "s_0d966a3d69ee069c": "关闭后所有命令直接在当前用户权限下执行,自动模式的未知命令改为逐条确认", "s_0de50760cb250e32": "移除模型 {{p0}}", + "s_0df737cb62bb5175": "群", "s_0e0163080d2e4ad4": "无法获取文件路径", "s_0e0c2d7597ef508d": "已缺失", "s_0e1a7040390fb03e": "新建项目", "s_0e700e0f522f37ff": "比较基线:{{p0}}(仅切换查看对象,不会 checkout)", "s_0e993148f8f5d3d2": "Grok 登录已失效,请重新连接", "s_0eab58baf452449c": "请输入完整的绝对路径,例如 C:\\Users 或 /home", + "s_0eb01c40432b05a9": "连接中断,正在自动重试", "s_0eca7afbf4f68601": "搜索技能名称或描述...", "s_0ecd009b61757eae": "创建独立 worktree,不包含未提交的修改。", "s_0f415f8b2bbbb37a": "读取运行记录失败:", @@ -161,12 +171,14 @@ export const sourceCatalogs = { "s_10e80ac177155143": "消息不能为空", "s_111448a2e1a82306": "还有 {{p0}} 个字段", "s_11152b370ff3e7ca": "超高画质模型", + "s_1117493dfd5708d1": "在钉钉里找到这个机器人,私聊它发送下面的 6 位数字。", "s_1123245d1bed08b2": "规则", "s_11448fff91a477dc": "已生成", "s_1159250ae5748529": "无效的关闭窗口行为", "s_117b19fd23b61edb": "按命令前缀决定放行、询问或禁止。前缀按空格分词,git status|diff 表示第二个词可以是任一个。「放行(沙箱外)」会跳过沙箱,只给确实需要的命令;解释器、shell、rm、sudo 这类前缀不能放行。", "s_117c113c3b45cf89": "排队", "s_1198cb9da295145d": "新建会话默认使用此模式,当前会话会同步切换", + "s_11a4dc9fc810be2f": "机器人的消息接收模式选「Stream 模式」,在「权限管理」开通企业内机器人发送消息权限,然后发布应用。", "s_11a6ac8794688be8": "命中询问规则", "s_11b999fc4610de85": "联网", "s_11cb87897cfb7e7b": "{{p0}}能力", @@ -186,6 +198,7 @@ export const sourceCatalogs = { "s_132411ba41da7354": "推理", "s_132a029e79ec9872": "与智能体共享", "s_13357fcd2d59d851": "命令前缀不能为空", + "s_13439ef69aeff4f2": "在「凭证与基础信息」复制 App ID 与 App Secret,填到下面。", "s_134a1a3e2b52b04f": "收起整个右侧面板", "s_134fdd6479c8036b": "发送 (Enter)", "s_13670a878cf6f01d": "ACECode 升级", @@ -200,14 +213,17 @@ export const sourceCatalogs = { "s_13f6a415ac664be2": "从当前 Provider 探测真实模型列表", "s_140b07fe3f9f7d48": "需要下载", "s_1417bae3d17d6405": "收起图像", + "s_142cf02f83d4e7ab": "撰写回复", "s_14357c0d6ca270df": "请检查地址是否正确,然后重试。", "s_14381a6ef00a1253": "定时测试与问题修复", + "s_143dab9db74c3131": "连接完成", "s_145219e726b87790": "隐藏", "s_145eb48a326b5d27": "工具重写", "s_148d195e21b05db5": "配置", "s_14cbad1d857ee3e6": "连接已断开,停止状态尚未确认", "s_14d1e349b14010a9": "取消下载", "s_14e7acfc08afa9e9": "浏览器组件已停止", + "s_14ee02a96d9bca6d": "已连接 · {{p0}}", "s_150dd0cecdc803d0": "当前工作区", "s_150f3c28d9d036af": "已清除标题", "s_155c2f5d0a3b2394": "笔记本", @@ -236,24 +252,32 @@ export const sourceCatalogs = { "s_1832990660bb60c2": "函数", "s_183b5f823fdb5522": "添加工作区失败:", "s_18435419f154847b": "命令前缀", + "s_1843acb8c6ea0f47": "私信机器人或在服务器频道里 @它 下达指令,结果回到频道。", "s_184568b57e5e6d3f": "正在生成测试图片…", "s_1847a84db6d5fc1a": "权限模式切换失败:", "s_18565a5455a0a875": "加载沙箱设置失败", + "s_18578640315b5b33": "更新待办", "s_1882a447d5f76d5f": "反馈内容不能超过 10000 字", + "s_188397c445996182": "已由另一个 ACECode 连接", "s_18850bf66a4ba4c6": "模型申请临时加宽沙盒权限", + "s_1903b018a3b98d7d": "{{p0}} 机器人", "s_19086bec77d5e5e5": "已自动兼容系统命名规则", + "s_190eedb7bfaa8609": "追加任务", "s_191041794ad98158": "(所有匹配)", "s_1910636fddbffe3e": "读取应用控件并操作鼠标和键盘", "s_19107d868966ff2c": "会话标题", "s_194b5dba3c63f537": "推理设置", "s_1964dafada0807b5": "Git 分支", "s_19af3dcd414d81df": "{{p0}} 个附件", + "s_19bf536853e2e78a": "协作", "s_19bfb1631d25a204": "正在保存主题…", "s_19cfa7d09d8dd287": "派遣中…", "s_1a0bf11a28779888": "选择后填入真实聊天框,不会自动发送", "s_1a16ea81ee6a29e4": "网站无法完成跳转,请稍后重试。", + "s_1a474c3207ed49f4": "待命", "s_1a4c6ebd7bbdbaa8": "还没有填写能力介绍。", "s_1a52bda1e037f2c3": "执行、编程或处理任务时使用", + "s_1a6c41c6ee173bab": "已绑定到 {{p0}}", "s_1a7224bf1e427ee4": "分叉失败:无 session_id", "s_1a7e48179574fffa": "收起变更摘要", "s_1a7eef17e199fa07": "收起命令详情", @@ -265,6 +289,7 @@ export const sourceCatalogs = { "s_1ae7a34c1452785e": "Markdown 所见即所得编辑器", "s_1afed6a81a2512d2": "选择模型", "s_1b14bd33bab15a6d": "{{p0}} 继承全局", + "s_1b442420620b3bb4": "现在可以在飞书里私聊机器人了。把机器人拉进群后 @ 它,第一次需要在设置页批准。", "s_1b479e1420fb0342": "放行目录", "s_1b4bc8cfe06cb405": "标记为已读", "s_1b58f84ec946fe67": "职业 / 定位", @@ -312,6 +337,7 @@ export const sourceCatalogs = { "s_1f4b8e02fe0eb7ab": "可选择多个模型,确认后一次性回填到新增模型表单。", "s_1f6c6927e129a5f5": "请升级并重启 ACECode daemon 后再试。", "s_1f79c9f362dc9d4b": "外观设置保存失败,已恢复上次配置:", + "s_1f9d55088d4f8ec0": "机主可以切换到任意会话;其他人只能在自己创建的会话之间切换。", "s_1fca6787d35baa53": "进入 Plan", "s_1fcd48a731ea363a": "原生选择器返回无效结果", "s_1fe3eb68a68b187d": "放行并记住", @@ -334,6 +360,7 @@ export const sourceCatalogs = { "s_21482b7f3029698f": "设为主理人", "s_21493a0f78021d9b": "选择当前文件夹", "s_2156709a76aa43e6": "输入消息…", + "s_2157f1235d2a58ee": "群 {{p0}} 的成员 {{p1}}", "s_21a08761b9914419": "一行一条。~ 表示家目录,:workspace_roots 表示当前工作区根,:tmpdir 表示沙箱临时目录,其余必须是绝对路径。黑名单支持 **/.env 这类通配。", "s_21a9727afd60df42": "已开启蜂群模式(星型)", "s_21cab4f2e52fbfef": "未命名对话", @@ -342,6 +369,7 @@ export const sourceCatalogs = { "s_2217610d8e605cdc": "打开全局 Skill 目录", "s_222c9c161b8907d6": "当前会话已多次压缩,建议在新会话中接续当前工作。", "s_222eac7780eaef4d": "模型配置已检查", + "s_225a51a3b48fddf4": "在 Discord 里私信这个机器人(需要和它在同一个服务器),发送下面的 6 位数字。", "s_227ea7277bfc00e9": "加载 MCP 失败:", "s_228d8b711ccf3ab3": "提交反馈", "s_228f61d57a0a4568": "已明确选择", @@ -362,9 +390,11 @@ export const sourceCatalogs = { "s_23f176008ae4f3bd": "原生文件数据无效", "s_246e46c2a4189cb5": "重命名失败:", "s_248e8e2921bcc92c": "会话 conversation messages auto collapse fold expand tools", + "s_251dee09f7adbd2e": "请先填写凭据", "s_2529b19bc41c0e9e": "请求格式错误", "s_2539cf900d5b8d37": "正在安装更新", "s_255c8d56e8bb1295": "无匹配结果", + "s_2560c96f89c6fea0": "在 LINE Official Account Manager 建立官方账号,并在「设定 > Messaging API」启用 Messaging API。", "s_257bbcc44c839db4": "修改时间", "s_257c21aae4198b7e": "设置与再次查看", "s_25c76c88d05b627c": "原生文件系统桥接不可用", @@ -374,18 +404,23 @@ export const sourceCatalogs = { "s_25ec0421b03bc4f3": "拖动调整右侧栏宽度", "s_261dba7261ed8c05": "描述不能为空", "s_2623156dcd71bf2d": "星号", + "s_264333fb5458a573": "准备调用工具", "s_2657124b1481b789": "项目提供", "s_267c4a7a86dd1849": "全部类型", "s_26ab0eee67e8c707": "已调用工具", "s_26c8aefe6fbfa744": "沙箱、文件与命令策略,以及每一次判定的审计记录。", "s_26da3cee0bb25151": "等待用户回复 AskUserQuestion", "s_26df939b25bc7b02": "排队消息内容", + "s_270dd2ebfcbec271": "在 LINE 里把官方账号加为好友,私聊它发送下面的 6 位数字。", "s_2719a8ea304b8cbf": "从", "s_2719f7ce2ace6db7": "无法打开 macOS 通知设置", "s_2721b6c32975b55d": "远程 Web 连接", + "s_27389885c97c4ab1": "钉钉", + "s_273f40d0251a8261": "打开钉钉开放平台", "s_2745ccb884fcf6ab": "意见反馈", "s_27586f180db1c812": "已重命名", "s_2812f6c1263c96a9": "已删除“{{p0}}”", + "s_281b0924bb9c5a16": "用微信扫一扫并在手机上确认;扫码的微信号就是机主,也是唯一能使用它的人。", "s_28200a87c40ced80": "导入并应用", "s_28321cb9b77f137c": "从这里分叉", "s_2834cf755087f50d": "摘要生成", @@ -425,6 +460,7 @@ export const sourceCatalogs = { "s_2c7dfb7f0b25589e": "当前对话内容查找", "s_2c843e836a4d4422": "保存关闭窗口设置失败:", "s_2c8855fc8ab8f5fb": "未发现可用的非本机网络地址,请检查网卡连接。", + "s_2c8904ba4a192db8": "创建机器人", "s_2ca1a16a870f47d3": "未找到相关设置", "s_2cc0b5d70839d137": "Mermaid 源码", "s_2cd0f3be8738a86c": "取消", @@ -478,6 +514,8 @@ export const sourceCatalogs = { "s_31ebdbfa17b81a02": "模型已保存", "s_32058ae99d61249c": "无法获取迁移进度:{{p0}}", "s_321e4914f68c1db0": "未知升级错误", + "s_3221a042ea3677a8": "重新生成", + "s_323bf944ea7c06e1": "在飞书私聊或群里 @机器人 下达指令,结果回到聊天窗口。", "s_3264ce720a9e420d": "主题正在下载,请稍候", "s_326d1d537e72f82d": "正在启动或停止反向代理", "s_327f00da9fc2ec36": "隐藏详情面板", @@ -504,15 +542,18 @@ export const sourceCatalogs = { "s_34b3cac2a496d263": "模型目录", "s_34c3c3b2bd45600d": "展开全部 diff", "s_34f7336d39831fca": "全部关闭", + "s_350a7736d0d4cb03": "绑定机主", "s_3548b099890e187f": "模型用量明细", "s_3568d317c8161ad4": "这是一个内嵌浏览器。", "s_357211096f78cdb8": "设为默认", "s_3590fba381ad57f0": "选中文本操作", "s_35c52c32418a2678": "将删除全部 {{p0}} 条记录,无法恢复。需要留档请先导出。", "s_35cda8a7052dec20": "迁移失败", + "s_3615b021b8af5e82": "收尾", "s_3620d3d986f14584": "关闭模型弹窗", "s_3625f9fa2f9c629a": "拒绝执行命令", "s_364970d8168ca5fe": "发送中…", + "s_3687f953cfe85fb8": "取消连接", "s_369078855fa122f7": "只读工具调用", "s_36f33adaf0942634": "确认", "s_3703e47860f64a12": "文件不存在", @@ -522,13 +563,16 @@ export const sourceCatalogs = { "s_375f5091e213f094": "路径都是 daemon 所在机器上的路径", "s_377e4422342c71f7": "问题反馈已上传", "s_3785cba81d184679": "当前工作区记忆", + "s_378eaca774bb9551": "扫码登录", "s_37979829426b0cdb": "工作区", + "s_37a41b2e983b06ee": "使用技能", "s_37c427d9f5dc817d": "默认模型已设为 ", "s_37d36b087f49c6d2": "选择专家组件", "s_37e4e42c314102da": "图像", "s_381117baedb79e1a": "选择 Provider 和模型后,只显示当前连接真正支持的配置。", "s_381796352dc0301d": "配置已保存但未能生效,请重试", "s_3819d5e4aa5dba68": "钢笔", + "s_38744132b501a1eb": "提问", "s_38c043e08502e710": "个性化", "s_38cd88ed8b3ddbf1": "复制完整输出", "s_38d456bbdb81317c": "按 Tag 筛选", @@ -536,6 +580,7 @@ export const sourceCatalogs = { "s_390fd9bc7d9d7a2a": "审计中心", "s_392d0dceb45998d3": "极高", "s_39592d688f12a5a6": "已存在同名条目", + "s_3969d8203ae05775": "扫码连接二维码", "s_39932f24fe11a6ba": "文件", "s_39a9046f9c8c993d": "最近使用", "s_39b068ea492a4f9a": "模型能力标签无效", @@ -577,10 +622,12 @@ export const sourceCatalogs = { "s_3db7b06b5f6de0e0": "工作目录", "s_3dcff5522afda275": "提示词缓存命中率", "s_3dd1245c155aba60": "模型配置不可用,已保留当前连接", + "s_3dd922527fba482d": "把机器人拉进群后 @ 它,第一次需要在设置页批准。", "s_3de42d7c68d442e5": "单次", "s_3df739dcd8a069eb": "最近会话记录", "s_3e04f83df97fa6f2": "最早更新", "s_3e48e68ed87a9add": "点击空白处或按 / 直接输入路径", + "s_3e6221be3922f850": "用微信扫一扫,并在手机上确认", "s_3e65bba57b173f30": "粘贴的文本太长,超出 /{{p0}} 的上限({{p1}} 字节)。去掉命令可作为普通消息发送,或缩短内容", "s_3e7d75b94f3a1f26": "加载工具重写配置失败", "s_3ea0e6e41518ea24": "空目录", @@ -632,6 +679,7 @@ export const sourceCatalogs = { "s_42e1b73173a3b1d4": "确认专家切换状态失败:{{p0}}", "s_432a6c3328243eee": "无法连接 xAI 设备授权服务", "s_4334aa1fdaa5a8c1": "已从会话中提炼、尚未整合进记忆的观察", + "s_433779053c77eea8": "机器人 {{p0}}", "s_434203a723b4614e": "已更新", "s_4342d572b0ee2718": "主会话仍可继续输入", "s_434c7efecbad5725": "请选择执行时间", @@ -641,11 +689,13 @@ export const sourceCatalogs = { "s_4382c23e9e50479f": "Agent 已完成计划,请求批准后退出 Plan 模式。", "s_43911903e1cf7e63": "已复制预览内容", "s_43cad95bdcdcc30d": "升级失败", + "s_43ce458cae49a1ad": "思考中", "s_43d7d2b51d75fa42": "创建项目", "s_43db2741a745b179": "启用服务器", "s_43f3dc12a6077835": "正在启动…", "s_43f4a4d322d58a5b": "已编辑 {{p0}}", "s_43fc32e85fb21c53": "进行中的目标", + "s_43fcef33a825715a": "把 Token 粘贴到下面;验证通过后用邀请链接把机器人拉进你的服务器。", "s_43fd0fc1e9ae1edb": "名称冲突已处理", "s_4430768f253a5d6d": "开启工具重写,并对之后的模型请求生效", "s_443cb29217829285": "查看擅长领域后派遣到新任务,或维护自己的专家和专家团。", @@ -681,6 +731,7 @@ export const sourceCatalogs = { "s_475ac58abf66032b": "读取根目录失败:{{p0}}", "s_476c022a773e1f82": "请关闭其他 ACECode 窗口后再迁移", "s_478211dd029aa604": "网站的安全证书无效或与当前地址不匹配。", + "s_47b50c43e14ca6ba": "扫码成功", "s_47c99fb0ddf2cffa": "{{p0}} – {{p1}} 累计使用了 {{p2}} 个 Token", "s_47f22728ee1600ed": "状态消息:", "s_481acc2c1413db38": "图像生成", @@ -691,6 +742,7 @@ export const sourceCatalogs = { "s_4871613a44be8d7a": "默认打开文件和文件夹的位置", "s_487422ef0c4e25b6": "首页标题", "s_48807d75530c1b03": "找不到这条排队消息", + "s_4888c0a26e905c1f": "用户 {{p0}}", "s_48d75d4b7e92d9f2": "切换到项目", "s_48f78f1abeca0c47": "粘贴内容较大({{p0}}),将分 {{p1}} 段上传", "s_490a88b8ae9d4a2e": "该模型已不存在,请刷新", @@ -706,12 +758,14 @@ export const sourceCatalogs = { "s_4a6d8d490c54d438": "允许一次", "s_4a83accbad2c11e1": "开场白", "s_4a8d6841b4023edf": "确认导入", + "s_4aa95aa725909efe": "把机器人拉进你的服务器", "s_4ab32793197b28db": "所选模型已不可用,定时任务已停用", "s_4ac9db43b40c292d": "当前后台服务不支持电脑操控,请更新 ACECode 并重启后重试", "s_4ad732f1d78f6725": "后台进程状态", "s_4ad8aa6461e64127": "上一题 (Shift+Tab)", "s_4af8f4c6dd2e233e": "大脑", "s_4afad488b461b58f": "侧边聊天记录", + "s_4b19c2f91cd3f418": "消息通道只能在运行 ACECode 的这台电脑上设置", "s_4b427656a253ca1e": "请完全退出并重新启动 ACECode", "s_4b47dbae97ba938a": "提示词", "s_4b50c9fa24dcafa3": "未配置模型", @@ -733,6 +787,7 @@ export const sourceCatalogs = { "s_4cc7b00050cb1c21": "行", "s_4cca8a4406aea952": "需要确认", "s_4ce87e7fe2b6e95d": "文件或文件夹", + "s_4ceae06adf5b9199": "撤销后立即停止处理其消息,并中止其会话正在进行的回合。", "s_4d0680f9efaef147": "未读", "s_4d2b19065afe4b07": "LSP 已开启:{{p0}}", "s_4d3f6a081ea86097": "彻底删除失败:", @@ -755,7 +810,9 @@ export const sourceCatalogs = { "s_4ef515abfdb8690d": "安装包", "s_4f01af9093fc9e59": "无法连接到此网站", "s_4f021f59121c6827": "等待执行", + "s_4f1f6ea2b75de587": "消息通道", "s_4f2bf16cc26825e8": "取消自定义答案", + "s_4f3bac187963a04a": "管理 {{p0}}", "s_4f5b0e563a452c80": "刷新技能列表", "s_4f61317e9307ad3e": "访问被拒", "s_4fca07787f3ef1ec": "移除这个待发送附件?", @@ -763,6 +820,7 @@ export const sourceCatalogs = { "s_4fe5ca6370a901ef": "ACECode 本地工具分类", "s_4fed9b0a87376127": "主题工坊", "s_5002a3c1c2822955": "原生与受管", + "s_500b6710a6f009cc": "在「版本管理与发布」创建版本并发布,可用范围要包含你自己。", "s_5019872ac7610542": "无法安全打开此网站", "s_5026a63b58cdf048": "执行中", "s_50563579e4119e75": "其它规则文件", @@ -798,6 +856,7 @@ export const sourceCatalogs = { "s_52b06532811be3e4": "与另一条规则重复", "s_52b44d4d2c562541": "连接 xAI", "s_52d25a9e30ba94f1": "关于", + "s_52ebdbe6cf99a77c": "还没有授权任何人。", "s_52f79fed15ec2723": "当前模型", "s_52fb62f878dcdbef": "主题包校验失败,请重新下载", "s_5305d3b2b3f1648f": "爆米花", @@ -828,9 +887,11 @@ export const sourceCatalogs = { "s_55db57105231bd95": "工作空间不可用", "s_55ef676879f7cca3": "diff 过大,请在终端查看", "s_561817f0252936a9": "放行", + "s_562834f547655a37": "搜索代码", "s_5653d97cdf569cb9": "系统防火墙、路由器或云安全组可能仍需放行代理端口。公网访问仍建议使用可信 VPN,并在上游配置 HTTPS。", "s_56627c94a9decee6": "最大输出 Token", "s_566e5f182a63185d": "正在压缩主题资源…", + "s_5670da45d1d6e526": "App ID(cli_ 开头)", "s_567146d66825e213": "开启自动换行", "s_56964754c2e1ab57": "反向代理已监听 {{p0}}:{{p1}}", "s_569af1a47dbdf053": "最新", @@ -852,6 +913,7 @@ export const sourceCatalogs = { "s_57e4fcb2dad50b5c": "远程 Web 模式已开启", "s_57eb46dacc97defb": "在任务列表每一行右侧显示最近活动时间,关闭后仍可在悬停卡片里查看", "s_5804b9cca84304ff": "清空侧边聊天", + "s_58210afb340c2c5e": "链接 10 分钟内有效,只能用一次。", "s_5835048333dad1d6": "选择自定义答案", "s_584f28c1077647a9": "正在取消...", "s_5868e5f7632072d9": "拒绝 {{p0}}", @@ -905,8 +967,10 @@ export const sourceCatalogs = { "s_5d7fd21f1e3ca6b4": "复制消息", "s_5d817dbb5da09259": "导入主题返回了无效结果,请刷新后重试", "s_5d937ed111134879": "模型能力", + "s_5ddcb16ab6fcb808": "查看成员", "s_5ddffd77ea7fedae": "正在加载已保存模型…", "s_5e56bec8550f499d": "请选择文件夹", + "s_5e78c941de4a98ed": "连接 {{p0}}", "s_5e80c1e561d45b11": "选择权限模式", "s_5e8e7af57abbcbcf": "删除中…", "s_5eaefa4a3950e8a3": "请选择新路径", @@ -917,11 +981,14 @@ export const sourceCatalogs = { "s_5f53727059cdc69a": "输入你的答案", "s_5f5a4c5fb8ab71d6": "输入框当前不可用", "s_5f5b6195dfd24aa1": "等待交互内容同步", + "s_5f76b2bf82dd2c5d": "版本", "s_5f7780324a4b55f1": "会话续接建议", "s_5fba64a2b5976bd4": "上传失败,点击重试", "s_5fcad9600a50f834": "关闭右侧标签页", "s_5fe46d0c2faeacf7": "选择模型连接", "s_605a534da98e42ec": "Token 活动视图", + "s_60b83f21d9d5d703": "群 {{p0}}", + "s_610a30de692b91b0": "请把你的公网地址转发到本机 http://127.0.0.1:{{p0}},连通后会自动继续。", "s_610e4e820d6881ea": "彻底删除会话“{{p0}}”?此操作不可撤销。", "s_6135d4159e892541": "移除", "s_613de904cc604e3b": "{{p0}}本地工具 {{p1}}", @@ -934,7 +1001,9 @@ export const sourceCatalogs = { "s_624b5799ebd036b0": "无工作区", "s_6258a2c6f9c89143": "复制全文", "s_62731d95bc805803": "正在导出…", + "s_6278c2f645ab3982": "公网地址", "s_629bb52ce0e5270f": "已将 {{p0}} 添加到聊天", + "s_62b72265c96e5e34": "正在重试", "s_62c850eabc4ba70e": "部分搜索数据加载失败:", "s_6309a3bb5ba4c714": "保存失败", "s_63448df0ea0c5be2": "插话中断", @@ -950,7 +1019,9 @@ export const sourceCatalogs = { "s_641778357a5e8a41": "查看配置 Schema", "s_643a1b94e9498780": "已信任钩子", "s_6492494fbe805292": "描述灵感,生成专属主题", + "s_64ded447c5bdd6ea": "验证并继续", "s_64e9f3e72b149b27": "此页面意外退出,请重新加载。", + "s_64fd6a54381b531c": "获取二维码失败,请重试", "s_64fdd111ae7c2ead": "工作空间正在迁移,请等待完成并重启", "s_651ec477427c76eb": "添加能力或上下文", "s_65273e466af9881c": "来自后台任务:{{p0}}", @@ -961,6 +1032,7 @@ export const sourceCatalogs = { "s_65b3f17534a8c0ab": "加载记忆失败", "s_663a5905150ea46d": "请检查系统代理设置后重新加载页面。", "s_664c216a6c75054f": "/expert-manager 帮我创建一个 XXX 专家,擅长 XXXXX。我的经验是:[请补充你的行业背景、相关经验]。", + "s_66687ec68c240db9": "在「Messaging API」页打开 Use webhook;在官方账号的回应设定里关闭自动回应讯息与加入好友的欢迎讯息。", "s_6679f618e868b532": "斜杠命令", "s_667b75d63a19b6b8": "上传失败:", "s_66809c2ae51ddf06": "移除 Tag {{p0}}", @@ -1012,7 +1084,9 @@ export const sourceCatalogs = { "s_6aa3b1bd38fe141b": "名字不能为空", "s_6abdf8409678b516": "在右侧审查面板中查看", "s_6ad55eb177526999": "放大预览", + "s_6add2714c40f1d45": "私聊", "s_6ae40b532ea0f924": "已转至新会话继续。", + "s_6aecec7bb2ae1d74": "公网地址(可选,留空则自动建立隧道)", "s_6b01fce4dbeaeeb9": "更改", "s_6bbaf705e8c64c2b": "目录内容", "s_6bea77acefb364ac": "标准", @@ -1029,6 +1103,7 @@ export const sourceCatalogs = { "s_6cec8d70466c7b55": "请在 Microsoft Store 的“库”中获取 ACECode 更新。", "s_6cf2531ab1119b67": "授权", "s_6d0b032bc4d31ea2": "复制标题", + "s_6d0d116c95898867": "机主", "s_6d330244eecae355": "连接 daemon 失败:", "s_6d3d916f1807a4d2": "继续添加 Tag", "s_6d5503cc0bd22117": "选择探测到的模型", @@ -1043,6 +1118,7 @@ export const sourceCatalogs = { "s_6dc0d06dd58b2cb6": "内置默认禁止名单", "s_6dd39868b7d587c7": "当前后台服务不支持安全中心,请更新 ACECode 并重启后重试", "s_6e1ba4bf187327e9": "终端程序路径", + "s_6e382d9250f8dc10": "手动填写", "s_6e392a597af4abab": "memory summary model 记忆摘要模型", "s_6e55e0f207bfd2e0": "运行记录", "s_6e6aba42eba94da0": "正在与智能体共享", @@ -1052,6 +1128,7 @@ export const sourceCatalogs = { "s_6f4b75ff28679ce4": "扁平列表", "s_6f5994e138557f90": "% 已用", "s_6f87682bc02532c6": "每个工作区可拥有独立的 Skill,仅在该工作区的会话中生效。", + "s_6f98bb98abfcd0c9": "打开飞书开放平台,创建「企业自建应用」,在「添加应用能力」里添加「机器人」。", "s_6f9c11640850a30c": "清空审计日志失败", "s_6fb9dd3555d1b3b0": "确认迁移", "s_6fc38ef3602fe789": "上下文", @@ -1077,12 +1154,16 @@ export const sourceCatalogs = { "s_71dcce0699b98a09": "专家组件", "s_71dfe4a817b8801d": "请先登录 Copilot", "s_71e95b289aad1f6a": "已跳过", + "s_71ea09dd1e9905d5": "电脑上登录了 Telegram 也可以直接点链接:", "s_71f09042e226bdf9": "高画质模型", "s_71fa1b09e415447e": "把身份、擅长领域、开场白和运行时能力配置在同一个专家中。", "s_720182db8f88cf7d": "正在读取运行时能力…", + "s_722dd20ced9e6d55": "用你的 {{p0}} 私聊机器人一句,下面会出现请求,批准后你就是机主。", "s_725831ec12394243": "取消归档失败:", + "s_72ccb1274a093efb": "填写 QQ 开放平台里机器人的 AppID 与 AppSecret,保存前会先验证。", "s_72dde8dfef88d37d": "确认添加", "s_72ec61d7855c86a1": "请求超时(秒)", + "s_737c725c1321d260": "已保存 {{p0}},留空则不修改", "s_73952aa91a9366b5": "已切换至 ", "s_73ae35f541bc8124": "复制附件地址", "s_73b629753f6d5fe5": "已复制全部 diff", @@ -1091,6 +1172,7 @@ export const sourceCatalogs = { "s_73e5df48000d6a22": "选择任务位置", "s_73fab2d02ab591cb": "模型检测失败{{p0}}:{{p1}}{{p2}}", "s_7409a608060f5804": "未指定", + "s_741a285fa14f8523": "整理上下文", "s_742025b1a0eccc8c": "类型无效", "s_743342489c23fe24": "刷新记忆列表", "s_744a61203be24c0b": "每 {{p0}} {{p1}}", @@ -1129,6 +1211,7 @@ export const sourceCatalogs = { "s_76e350aa9b3cf01a": "正在读取默认位置…", "s_76ebf39ee090026f": "无人值守下不能批越权", "s_76f677430db630af": "仅 ACECode Desktop 支持内嵌浏览器", + "s_7715155115f58274": "返回扫码", "s_77322b7c285eb8bd": "主题已删除", "s_7734b5d136de7bdb": "自动重启失败:", "s_77423bdd32d35ce3": "编辑刚刚发出的内容", @@ -1148,11 +1231,13 @@ export const sourceCatalogs = { "s_78e5c1be3c1a2347": "壶铃", "s_78ea07a31cd96f65": "从此版本开始记录新的 token 用量,历史会话不会回填", "s_79167440a990362e": "会话已导出:{{p0}}", + "s_79209170acff4896": "等待授权", "s_79383e9981843085": "读取 Web 核心信息", "s_7948c26c9630a0cc": "资深开发工程师", "s_79612d07708f27c8": "平台状态", "s_7967bb84220ae3d1": "该工作区没有技能;放到项目的 .acecode/skills 目录即可被发现", "s_797644874d78c30a": "剪贴板不可用", + "s_79ee5900e8dc266b": "{{p0}} 配对请求", "s_79f326be4409d51f": "项目", "s_7a0b79e584676550": "ACECode 全局项目目录", "s_7a437376c304672c": "在当前回合的下一次模型调用前加入这条消息", @@ -1161,6 +1246,7 @@ export const sourceCatalogs = { "s_7a8a11ead50742a2": "添加", "s_7a9b22bb6b0a159e": "Grok Coding Plan 已退出", "s_7aa6d0ef5feb745c": "彻底删除", + "s_7ace720715576424": "绑定码", "s_7acf2ee49b4924ef": "位置:{{p0}}", "s_7ad6bbf3f8508928": "忽略建议", "s_7adc856731f31518": "尚未运行", @@ -1168,6 +1254,7 @@ export const sourceCatalogs = { "s_7afb72a4b5b21193": "添加工作区", "s_7b00c3d69a0c48a5": "个模型", "s_7b10b3966368f243": "没有匹配的", + "s_7b113358cc1fe5fd": "新联系人", "s_7b1347c4ded0097a": "侧边聊天", "s_7b187349d641590c": "导出会话失败:", "s_7b1c1c5b441d279b": "请检查网站地址和网络连接,然后重试。", @@ -1195,6 +1282,7 @@ export const sourceCatalogs = { "s_7d09ee28674f2d4b": "跳过原因:", "s_7d1070ff5c5b7bdf": "[粘贴的文本: {{p0}}]", "s_7d166479398a3bce": "正在提交插话…", + "s_7d20222788427d6b": "在「权限管理」开通:im:message、im:message:send_as_bot、im:message.p2p_msg:readonly、im:message.group_at_msg:readonly、im:resource。", "s_7d3a4b81e7018779": "会话历史已更新,请重试", "s_7d4ca7f48effd602": "大", "s_7d6667c86ebec2fa": "已达成目标", @@ -1207,11 +1295,13 @@ export const sourceCatalogs = { "s_7df5c456c765e4c3": "禁用", "s_7e1df49bc3432fb2": "找不到该预设对应的 Provider 描述", "s_7e2ffe585c643f17": "准断网", + "s_7e4419d1de251094": "飞书", "s_7e8586f653066a9c": "删除模型预设", "s_7e963d9ae21a7ad5": "摘要模型", "s_7ea852a1f549b6ca": "请输入定时任务名称", "s_7ecf1cde648680d2": "已选", "s_7ecff345fb5ca29e": "Copilot 授权校验失败", + "s_7ed80e44eaf9c51c": "写入文件", "s_7edb086ee0d4c6c8": "任务完成通知已打开,但 macOS 系统权限已拒绝", "s_7ee8b2d0ae7ade97": "移除附件", "s_7efe433909425026": "自定义请求头格式不正确", @@ -1222,6 +1312,7 @@ export const sourceCatalogs = { "s_7f64e6cc3a5dccc7": "调整后台任务面板宽度", "s_7f68ebad19ba6bcd": "检查更新", "s_7f6c661eb101f549": "连接 GitHub", + "s_7f92b2869303b795": "在微信里私聊机器人下达指令,结果回到聊天窗口。", "s_7fd7c6c1c60155c2": "没有可批注的选中文本", "s_7fe9cf36941a738f": "最大化预览面板", "s_80160e448f307c23": "仅显示预览", @@ -1239,18 +1330,23 @@ export const sourceCatalogs = { "s_8143449ded6af56c": "展开项目", "s_81508b76150acb3d": "留空从 PATH 查找", "s_817af552ff22fdff": "加载失败:", + "s_81b430b696435180": "微信", "s_81c9f5ef56c3acfd": "summary title generation local model 摘要模型 会话标题", "s_81ee43758501ff89": "例如 128000", "s_81f19964ae735b1a": "高级功能", "s_81fd16a12933fe19": "编辑{{p0}}", + "s_8227ca04cabbb5e8": "现在可以在微信里给机器人发消息了。微信机器人只支持私聊。", "s_822fb37dba29381a": "选择文件", "s_82395d423099a07a": "已归档", + "s_823b4ba6e569146a": "在 LINE Developers 控制台的频道「Basic settings」复制 Channel ID 与 Channel secret,填到下面。", "s_825a80f75113a9d3": "提示词缓存命中:{{p0}} / {{p1}} 输入 token({{p2}}%),缓存写入 {{p3}}", + "s_82601c48d5d64d71": "修改代码", "s_8268968d4a8759fc": "当前模型不支持只读旁路聊天,请切换模型后重试。", "s_82808f881bf7755f": "团队成员 · {{p0}} 人", "s_82a043c05e8a9446": "专家名称", "s_82c4aa13b7ca109e": "最大化", "s_82ec71d0714dc013": "正在连接 Windows WebView2。", + "s_8301f8304e5dd793": "获取二维码失败,可以重试,或改为手动填写", "s_836c3d5d6bc88f78": "写/执行操作前确认", "s_837415ff50de9e5c": "匹配:", "s_837acaec000d9f38": "已保存。{{p0}} 处疑似密钥已替换为 [REDACTED],上面显示的是保存后的内容。", @@ -1276,17 +1372,20 @@ export const sourceCatalogs = { "s_8526267fcc158a52": "显示更早的消息", "s_85501294b3c40740": "内容超过 50 万字符,直接编辑会明显卡顿;可复制出去修改后用剪贴板内容替换", "s_8569e005ca1f98f1": "已打开技能目录", + "s_856dba844b90b484": "打开 LINE Official Account Manager", "s_8585b2747da94d76": "页面重定向失败", "s_85c016964ce0bedf": "目录不存在", "s_85c5c1f5567b4d67": "批注", "s_85e2f5601c1a0258": "无人值守", "s_8609635074718e14": "已引用到聊天", "s_86353011351e5d0b": "opencode 会话导入失败:", + "s_8652e90f49cd7463": "调用 MCP", "s_86542c6b657c93ab": "会话:{{p0}}", "s_86624f5719c93e79": "显示更早的 {{p0}} 条消息", "s_867698fb7b0c730c": "项目名称", "s_867ea0e1328728d9": "错误:", "s_8696c49ac55b8c71": "超高", + "s_86a515d230592181": "扫码后会自动创建机器人并完成配置,扫码人成为机主。", "s_86a63f23a076b11e": "外观", "s_86a82374a35763d1": "{{p0}} 个问题待回答", "s_86a88f06d31bd3b6": "正在读取 macOS 系统通知权限", @@ -1312,6 +1411,7 @@ export const sourceCatalogs = { "s_88d72ece7cf76737": "搜索结果", "s_8905355415cd0bd0": "没有可引用的选中文本", "s_89140d4ebd886f12": "插件已启用", + "s_891836aa11f5c4d9": "在 Telegram 私聊或群里 @机器人 下达指令,结果回到聊天窗口。", "s_89330587b799cc59": "暂时无法下载", "s_8949d6721460a7f8": "聊天输入区的模型选择器使用这里的预设。", "s_8968ac45a626bd2a": "会话菜单", @@ -1323,6 +1423,7 @@ export const sourceCatalogs = { "s_8996cf52b5aacf6a": "归档失败:", "s_89c4766afc85125f": "连接失败", "s_89d3e8892ae8fd51": "更多专家", + "s_89db648162e7cea1": "连接中断,正在自动重试…", "s_89e0ebd9d750f2c1": "当前后台服务不支持图像生成,请更新 ACECode 并重启后重试", "s_8a32f1c8f96d5905": "沙箱后端不可用:{{p0}}", "s_8a687fb42860ccd0": "思考深度:{{p0}}", @@ -1341,15 +1442,18 @@ export const sourceCatalogs = { "s_8b6ff498515bcc2f": "时间", "s_8b7cbd4cede975c1": "导入主题", "s_8bcc3e177f5f3b10": "不保存", + "s_8be0ea36a9bba286": "正在连接…", "s_8c1ae715559b78bd": "无法获取升级进度", "s_8c5c96ae7d403826": "已放行指定目录", "s_8c803ec4ccd237fb": "此网站暂时无法正确响应,请稍后重试。", "s_8ca30ef95071173d": "正在分叉到新会话", "s_8cb443ab83797881": "图表", "s_8cb45d256976500e": "静默允许", + "s_8cbe697b157364a5": "批准", "s_8cd177ccd9a3ce2a": "文件:{{p0}}", "s_8ce03a48ca299d89": "默认打开目标", "s_8cf0105d35ee8db8": "思考", + "s_8d0d535863719b97": "dingtalk 钉钉 stream client id client secret 机器人 im channel 消息通道 连接 取消连接", "s_8d2773df13e2ebed": "宫格里还没有会话", "s_8d2c69445d32a5b4": "当前不可用", "s_8d2e9a70cf226d91": "搜索已保存模型", @@ -1375,11 +1479,14 @@ export const sourceCatalogs = { "s_8e6cd5a3fba2a97c": "后台运行设置失败:", "s_8e7847be62b68c2b": "上一级", "s_8e7aa97f7fda6bc8": "重置记忆失败", + "s_8e86f9b1d54f2c51": "复制链接", "s_8e94c67730c3984e": "尚未授权", "s_8eb6432c43427be3": "已知安全命令", "s_8ee6730e12551245": "配置写盘失败", "s_8effeb945cdcfbd8": "钩子代码", + "s_8f1094e99bf0e30a": "这个机器人设置了 webhook,ACECode 收不到消息。移除后由 ACECode 接管,原来接收 webhook 的服务会停止收到消息。", "s_8f407bb9c116f41d": "间隔数值", + "s_8f43a2aeaac85a91": "回到飞书开放平台的「事件与回调」,订阅方式选「使用长连接接收事件」并保存(这里连上之后才能保存)。", "s_8f46cb4df9c2e6ca": "仅支持 PNG、JPEG、GIF、WebP、BMP 或 ICO", "s_8f68def4890f0915": "最近使用的专家组件", "s_8f6f8d979c981ced": "已复制", @@ -1396,13 +1503,16 @@ export const sourceCatalogs = { "s_90ccdf079052559f": "删除模型 {{p0}}", "s_90dae385f5c1bd68": "旁路聊天连接已中断,请重试。", "s_90fd1d973ec921f7": "电脑操控目前仅支持 Windows", + "s_9118ed423edf0b11": "在「凭证与基础信息」复制 Client ID 与 Client Secret,填到下面。", "s_912be01af6735ac1": "默认权限模式已设为 ", + "s_913216fda599e7d3": "{{p0}} 分钟后过期", "s_913cd56da217a9a2": "指针样式", "s_9147f07072527b70": "启用模型推理", "s_914b7a5f9d45d7b9": "粘贴的文本正在上传,完成后请再发送", "s_9154825fa11a80a2": "复用连接的 API 地址和 Key,图像模型在下方单独指定。", "s_915b988cbfed5442": "远程 Web 模式", "s_916a15661d20131c": "已暂停的目标", + "s_916afcd85322d732": "更换", "s_917b1c1f18d0276b": "保存中...", "s_92172b32227b0aae": "删除后无法恢复;记忆摘要之后也不会再自动生成这条记忆。", "s_926a50b98ece2667": "撤销", @@ -1419,6 +1529,7 @@ export const sourceCatalogs = { "s_9344597efd4950cc": "执行失败", "s_9344fdfb51e2e192": "擅长领域", "s_935717bb48835029": "没有操作或等待下一项任务时使用", + "s_93828ba27fd646b2": "在钉钉私聊或群里 @机器人 下达指令,结果回到聊天窗口。", "s_93a8734cce300531": "搜索模型", "s_93ac0efe6818ad18": "引用已有专家,指定一位主理人;成员保留各自能力范围。", "s_93b93a43e96a3556": "目录名:", @@ -1465,6 +1576,7 @@ export const sourceCatalogs = { "s_981a6df8cf54be60": "PNG 图片", "s_9833401820de5e2b": "受管 Provider 在这里完成登录,密钥和端点不会进入模型弹窗。", "s_9842842bc84fd474": "电脑操控支持 Windows 和 macOS 14 及以上版本", + "s_98532681701285de": "现在可以在 LINE 里私聊机器人了。把机器人邀请进群后 @ 它,第一次需要在设置页批准。", "s_985511d8c078002c": "正在回答…", "s_985575198233e251": "导出主题失败", "s_986c56322e8bb553": "请选择关闭窗口操作", @@ -1472,13 +1584,17 @@ export const sourceCatalogs = { "s_9884e90a773101b9": "全局记忆", "s_988c12f1398e3d9d": "AI 执行的命令运行在隔离沙箱里,文件、命令、网络策略按下面的规则生效。", "s_98cbc356f0e61a08": "新增一个 Provider 配置后即可在这里管理。", + "s_98fc221b10e87c21": "创建应用", "s_991bb7cfe5a81550": "保存修改", "s_992b4416c9d23e96": "正在读取模型…", "s_9966d6b375133f89": "路径必须是绝对路径:{{p0}}", + "s_99731fe916c6b3bf": "现在可以在钉钉里私聊机器人了。把机器人拉进群后 @ 它,第一次需要在设置页批准。", + "s_99845832ed7f16f6": "稍后再说", "s_998568217a58f2fc": "无匹配的全局技能", "s_999bf43be3657ea3": "非交互嵌入默认放行", "s_99a4e1e59743908f": "扩展", "s_99aea2f9131ad6da": "技能", + "s_99b8595e331d53a0": "在 QQ 私聊或群里 @机器人 下达指令,结果回到聊天窗口。", "s_99c32108b160659e": "请选择不超过 16 MB 的完整主题 ZIP", "s_9a1537c4f4d32cef": "未知命令且沙箱不可用", "s_9a548610ec6b027f": "已复制代码", @@ -1500,7 +1616,9 @@ export const sourceCatalogs = { "s_9d028438fbd04725": "请先填写 API 地址和 Key,或选择可用的模型连接", "s_9d0f70c4cee1d679": "本次会话放行", "s_9d1a1b1c006d85f0": "文件编码不受支持,只能只读预览", + "s_9d8692671d208b74": "群成员", "s_9d8fb8378821c838": "近 365 天内累计", + "s_9dbb88d5e18dcea1": "移除 webhook 并连接", "s_9dc0825fba5422e4": "加载中...", "s_9dc11d465f7d22c2": "迁移任务已不存在,请重新发起迁移", "s_9dcb7682ca6c6910": "{{p0}} 个 LSP", @@ -1509,6 +1627,7 @@ export const sourceCatalogs = { "s_9e3fc11213b90ed0": "在资源管理器中显示", "s_9e4a69b0e32825ba": "选择任务范围", "s_9e66271fb2e5fc90": "技能与协作", + "s_9e6c825ec268ab7b": "把它回复的 Token 粘贴到下面。", "s_9e73b26eb0a378c9": "没有可读取的内容", "s_9e87fbcaed97c244": "升级已取消,当前安装未修改", "s_9e928f35eb8008a4": "暂时无法获取主题下载大小,请重试", @@ -1531,9 +1650,11 @@ export const sourceCatalogs = { "s_9fc9368cc2c409a3": "暂无已配置插件", "s_9fd56f7aae1ca61d": "显示隐藏项目", "s_9fef8ee282a01ab9": "本地模型目录格式不完整,请更新 ACECode 或刷新目录。", + "s_9ff545329109ea65": "机主:{{p0}}", "s_a00235e8c624f16d": "扩展 / 专家组件", "s_a011d73ec679db1a": "启动升级失败:", "s_a071de661efe95fb": "删除这条记忆?", + "s_a0a0e5f6072c8213": "weixin wechat 微信 扫码 ilink 机器人 im channel 消息通道 连接 取消连接", "s_a0dd1a666530c173": "默认权限模式已更新,当前会话同步失败:", "s_a0f73bcccc33cb7b": "关闭预览面板会关闭当前预览区域的全部 {{p0}} 个标签页。是否继续?", "s_a1315bcfc62a19f8": "原生桥返回了无效结果", @@ -1541,8 +1662,10 @@ export const sourceCatalogs = { "s_a153567fbff7a035": "工作空间依赖项", "s_a184d57717116620": "已有旁路提问正在回答,请稍候", "s_a1a9da9ee6ed0401": "未知原因", + "s_a1dfbbbaa9f2fef9": "机器人设置了 webhook,需要处理", "s_a203f86cf6a6a841": "MCP 服务器", "s_a22774c08bc9187e": "加入可写", + "s_a253535fd2df2437": "接下来 10 分钟内第一个私聊机器人的人成为机主,现在就用你的 {{p0}} 私聊它一句。", "s_a25f7afbdeabbf5b": "天平", "s_a26e8186b190f6a1": "已添加到会话", "s_a27718d789f9cd53": "请先连接 {{p0}}", @@ -1558,18 +1681,22 @@ export const sourceCatalogs = { "s_a3030bf8f16dc63c": "保存", "s_a3446a9f995b97ae": "新路径不能位于当前工作空间内", "s_a37778f17c5f3ee5": "已取消", + "s_a379593b5b7590f0": "重新扫码", "s_a37fe6cce130db08": "允许多进程启动", "s_a383b3a7e26ba13e": "由 Microsoft Store 管理更新", "s_a398cdc4c87f6bb1": "当前 daemon 不支持外观配置持久化", "s_a3a33d4898a73327": "项目配置与全局配置叠加,同名服务器以项目配置为准。", + "s_a3c4db63188defb7": "在同一页打开「Message Content Intent」。", "s_a46b5607cf04e274": "请选择决策", "s_a48c70216c141e4a": "我们该在 {{p0}} 中做什么?", "s_a49f5287fc9ef2d0": "展开目录", "s_a4c3313deb180960": "三", + "s_a4d14414860b025b": "机器人", "s_a4dbd208ac604795": "迁移失败:{{p0}}", "s_a4e47879fbd494b2": "探测模型", "s_a500bd5efd41f72a": "当前问题", "s_a53a4e7bcadab26f": "取消目标", + "s_a5574109f0208e89": "连接", "s_a55e054ba8777aed": "我了解这些记忆会被永久删除,且无法撤销", "s_a567bdaa11367f26": "中", "s_a56e7d2220e99dc0": "运行中的会话正在使用该模型", @@ -1579,6 +1706,7 @@ export const sourceCatalogs = { "s_a5c89f1001ff103d": "任务已终止:{{p0}}", "s_a60959c3478597ba": "导出 PNG 图片", "s_a621d610084a268b": "Windows WebView2 暂时无法启动,请重新启动 ACECode 后再试。", + "s_a627f4f2dbd7c83b": "feishu lark 飞书 app id app secret 长连接 机器人 im channel 消息通道 连接 取消连接", "s_a63280253f17d440": "会话", "s_a655bcaa91a25426": "取消派遣:{{p0}}", "s_a67fa4bcb68094b4": "没有符合筛选条件的专家组件", @@ -1587,6 +1715,7 @@ export const sourceCatalogs = { "s_a6dfb80406396953": "当前平台不拦截读取,此清单不生效(仅 macOS / Linux)", "s_a6e1beaceb5bbafa": "暂无任务或项目", "s_a7310a28aa28faf4": "取消升级失败:", + "s_a73a5fef875e86b2": "没有找到 cloudflared。先运行 winget install --id Cloudflare.cloudflared 安装后重试,或回到上一步填写自己的公网地址。", "s_a74010990fc78e7d": "本地导入", "s_a76952e53abfc0d9": "刷新模型", "s_a77ecf353bbece4f": "升级失败:", @@ -1611,6 +1740,7 @@ export const sourceCatalogs = { "s_a9a52fa6f6089c4d": "无法保存,请重新打开后再试", "s_a9c7ba0a90821f96": "退出连接", "s_a9f9acf6c0a4da5e": "已删除 {{p0}} 个会话", + "s_a9fe4ee81f686689": "连接失败,详情见日志", "s_aa06c367f0d85408": "更新中", "s_aa1b765fb519fa08": "认证失败:", "s_aa1e899dd35771b0": "橙色", @@ -1629,6 +1759,7 @@ export const sourceCatalogs = { "s_ab48bf2aff24b769": "远程 Web 代理启动或停止超时", "s_ab61b9d1e06f9811": "新建终端(默认 shell)", "s_ab732547ef013de7": "允许后,这条命令将在沙盒内执行。", + "s_ab7a910f5dec5480": "还差几步", "s_ab9084a640fbb864": "全不选", "s_abb6edd1093c5cf2": "任务已终止", "s_abe29e9399d0301c": "加载更多失败", @@ -1638,6 +1769,7 @@ export const sourceCatalogs = { "s_ac3f6e014eef361b": "切换开发者工具", "s_ac4901c4979510a2": "{{p0}}(不可用)", "s_ac6062580b3feb00": "后台任务建议", + "s_ac7246e3c8488218": "{{p0}} 个请求待批准", "s_ac7646d3e0e17e38": "待导入主题预览", "s_ac77953a1e064ed6": "发送失败", "s_ac8e7ffd9e3458ab": "本平台没有可用沙盒,未知命令需要确认", @@ -1648,6 +1780,7 @@ export const sourceCatalogs = { "s_ad24308da105ecab": "无法连接 Grok 模型服务", "s_ad403ac8e643bda1": "主题已删除,部分缓存待清理", "s_ad4c46728555aeb6": "找不到此网站", + "s_ad5aaf33d6ce9b8a": "正在获取二维码…", "s_ae115efd1b2c2524": "任务已中止", "s_ae13d96782b88e57": "探索代码库", "s_ae30451fc65b75de": "没有匹配的 Provider", @@ -1679,6 +1812,7 @@ export const sourceCatalogs = { "s_b0e60c5b81438dff": "主理人", "s_b0fda488e4430202": "文件过大,无法在详情中编辑", "s_b135c840f4ab004d": "请先选择工作空间", + "s_b1806dfd868502c0": "现在可以在 QQ 里私聊机器人了。把机器人拉进群后 @ 它,第一次需要在设置页批准。未经个人认证的机器人只能由你本人使用,也只能加入你担任群主的群。", "s_b193e624476c776d": "未能获取模型回复,请检查配置后重试", "s_b19b09a6ed90c4be": "{{p0}}当前生效头像", "s_b1a521adee6fcd78": "旁路聊天响应格式错误,请重试。", @@ -1689,12 +1823,14 @@ export const sourceCatalogs = { "s_b2749206c4e01861": "旁路提问失败", "s_b2888f5f93782bb7": "加载会话失败:", "s_b28e2113fe3a14e6": "全部时间", + "s_b28f2b46e181e999": "打开 Discord 开发者后台,新建应用,在「Bot」页点「Reset Token」并复制。", "s_b291a71042529666": "包含 {{p0}} 条本地估算记录,总量可能与服务商账单不同", "s_b2d2f7bccbeeffa1": "正在确认…", "s_b31640e590dd972a": "有尚未保存的更改", "s_b33181ed8c62a396": "删除主题", "s_b3594eb35263f1eb": "批注 {{p0}},原文已变化", "s_b387dca9c27793f0": "取消导出失败,请重试", + "s_b3bc6035b32575a6": "discord bot token 机器人 服务器 im channel 消息通道 连接 取消连接", "s_b3dd7d131a1959f4": "直接用自然语言交代任务,或输入 / 查看可用命令。发送后 ACECode 会在所选范围内工作。", "s_b40cae1f1f4446f0": "关闭窗口时您希望执行什么操作?退出应用将停止所有正在运行的任务和定时任务。", "s_b40f2c82dca202be": "当前后端不隔离网络。", @@ -1729,6 +1865,7 @@ export const sourceCatalogs = { "s_b76fe0327e7aefc8": "取消置顶失败:", "s_b79ce792c1a1287a": "正在校验并安装…", "s_b7f4557b8b746a5e": "请输入完整的文件夹路径", + "s_b806e39b8e3c4403": "用手机相机扫码(Telegram 自带的扫码扫不了这个),在 Telegram 里点「开始」就成为机主。", "s_b817c99bb4f24dd5": "展开列表面板", "s_b8199c9199128706": "计划与任务", "s_b858b96d7e9a464e": "在文件树中定位", @@ -1741,8 +1878,10 @@ export const sourceCatalogs = { "s_b91472f6f25b1041": "输入消息或 / 命令…", "s_b91b8a344a77fca6": "目录路径为空", "s_b9238eb0c9178901": "主题操作失败,请重试", + "s_b925ae46007cd1f0": "机主绑定二维码", "s_b92b399d2820c415": "清除已保存密钥", "s_b92d86f4f5570817": "关闭其他", + "s_b93afa1b4934b706": "查阅网页", "s_b967309734beff88": "记忆摘要", "s_b97ad406809572e1": "启用推理", "s_b97c0e55e836dee2": "所选摘要模型不可用,请重新选择模型", @@ -1764,6 +1903,7 @@ export const sourceCatalogs = { "s_bad0ab59c33beb1d": "每天 {{p0}}", "s_baeb44eebe946b43": "只能包含字母、数字、下划线和连字符,最多 64 个字符", "s_bb3e66754a5e03e3": "Agent 请求执行以下操作:", + "s_bb6d995724f49a72": "管理", "s_bb7cc334f9434dab": "关闭后,本次填写的内容和能力选择不会保存。", "s_bb7ed30127ddfd49": "导出任务返回了无效结果,请重试", "s_bb7ef73495dd8a74": "结果", @@ -1794,10 +1934,12 @@ export const sourceCatalogs = { "s_be235f67fed22f01": "取消归档", "s_be385b84994016d0": "浏览器组件意外停止,请重试加载此页面。", "s_be3e095ce6b95549": "名字以 ( 开头是系统保留", + "s_be7bbdf1213e0555": "QQ 机器人", "s_be98c07b13ac0281": "静态", "s_bee1bfab91f18284": "无人值守自动放行", "s_bf1617e297b85f92": "已从桌面工作区列表移除", "s_bf24e735d802644f": "一行一个;显示在卡片与详情中", + "s_bf2a78216b764bb1": "qq bot 机器人 扫码 appid appsecret im channel 消息通道 连接 取消连接 owner approve revoke 机主 授权 审批 撤销", "s_bf39e015be237ba0": "项目已创建,但返回的工作区信息不完整", "s_bf3a8846479b7fb7": "输入或粘贴绝对路径,回车跳转", "s_bf3e55705e1de1e8": "任务完成通知设置失败:", @@ -1817,7 +1959,9 @@ export const sourceCatalogs = { "s_c02c246a6d181676": "无法读取粘贴的文本", "s_c02fdcf208b3e472": "删除规则", "s_c03b9deb55408bc4": "页面进程已停止", + "s_c043eb82f9332c31": "正在打开连接…", "s_c044ecbe7fe9c678": "仅显示主题色箭头", + "s_c051216ed509001e": "在 Telegram 里搜索 @BotFather(认准蓝色认证标记),发送 /newbot。", "s_c05f7cd6ed3587a2": "飞机", "s_c0866a5a020be7a3": "彻底删除会话", "s_c087c3d4b210ee82": "删除主题返回了无效结果,请刷新后重试", @@ -1836,6 +1980,7 @@ export const sourceCatalogs = { "s_c27916570d04cf33": "当前工作区未在后台服务中登记", "s_c2a497830ef74215": "最新版本", "s_c2b732b196ccc7f3": "会话已彻底删除", + "s_c2b794434132af6e": "重新获取二维码", "s_c2e410cd40759fb5": "向 ACECode 描述任务,或输入 / 命令...", "s_c2f0625f0f3d2c18": "展开工具详情", "s_c304f59bf05cfa14": "桌面启动", @@ -1846,8 +1991,10 @@ export const sourceCatalogs = { "s_c39d1e51a3941a89": "正在保存主题包…", "s_c3a73c5eecea42a3": "专家", "s_c3b8f744ed2c55bf": "JSON 格式错误:", + "s_c3e1b26c451e8221": "添加事件「接收消息 v2.0」(im.message.receive_v1)。", "s_c3e5589604083870": "暂无插件", "s_c3f989b790daab98": "后台服务暂不可用,请稍后重试", + "s_c43f153cb1be5ca5": "等成员回报", "s_c4ae8fa5a16b5a75": "跳过所有工具权限确认", "s_c4c5b649cbf25fbb": "记忆与上下文", "s_c52c7fbed621e8ff": "批注 {{p0}}", @@ -1908,12 +2055,14 @@ export const sourceCatalogs = { "s_ca6343cddb75f289": "生成测试图片失败,请检查连接和模型名称", "s_ca66c2da6f5bf825": "了解更多", "s_ca74d439e7712aa5": "daemon 未运行,已错过且不会补跑", + "s_ca78ab9bac8e9f5b": "在飞书里搜索这个机器人,私聊它发送下面的 6 位数字。", "s_ca949d1eb3377a79": "ACE 标记", "s_cadd75ef8275de01": "已查看 {{p0}} 张图像", "s_caecb5d342cc889a": "恢复目标", "s_cafc8a4f6acda2cb": "Tag 可多选;同一专家会出现在每个已选 Tag 下", "s_cb2fcb9099668d50": "暂无回答", "s_cb31eb7a0c2aa250": "编辑模型 · {{p0}}", + "s_cb51a062db592988": "这个机器人正由另一个 ACECode 连接,它退出后这里会自动接管。", "s_cb5d682bac3d1a2d": "重置", "s_cb62ebd689ee8f20": "当前", "s_cb65690b6ee3a1d4": "无法获取 GitHub 验证码", @@ -1921,13 +2070,17 @@ export const sourceCatalogs = { "s_cbb5a32c96c3cdee": "添加所选模型", "s_cbd7dc0ea6976fd0": "关闭窗口时", "s_cc0477362c195cf3": "暂无可复用的 OpenAI 兼容连接,请选择单独配置。", + "s_cc574bc5da5d9ac3": "绑定码 10 分钟内有效,只能用一次;发送后这里会自动完成。", "s_cc66c3f576fd5a49": "完全访问权限", "s_cc91b14c8a02a155": "将 {{p0}} 设为默认模型", "s_cce894450069ba5b": "未知来源", "s_cd0832ff9b505231": "主题包已交给浏览器下载", + "s_cd0ce16ee4fd90de": "复制失败,请手动选中复制", + "s_cd1db6a7d69cb947": "在 QQ、微信、飞书、钉钉、Telegram、Discord、LINE 里远程使用 ACECode。凭据只保存在这台电脑上;关闭 Desktop 后是否继续在线,取决于 设置 > 常规 里的“退出 ACECode 后继续运行后台进程”。", "s_cd32a8c1d823d060": "{{p0}} · 原文已变化", "s_cd3b78eb00f7cd12": "已配置的钩子将显示在此处", "s_cd62bee012d94183": "页面组件意外停止,请重新加载。", + "s_cd68324b409f9962": "LINE 只能通过公网地址回调。公网地址留空时,ACECode 会用 cloudflared 自动建立临时隧道(先安装:winget install --id Cloudflare.cloudflared),每次连接都会自动更新 LINE 的回调地址。", "s_cd7395dfd00b67f2": "只放行写入 {{p0}}", "s_cda33af428d74680": "命中沙箱内放行规则", "s_cdae8daae433af05": "原生选择器不可用", @@ -1950,8 +2103,10 @@ export const sourceCatalogs = { "s_cf479a0f90887a09": "暂时无法获取主题信息,请检查网络后重试", "s_cf4880f621b9d7bd": "我的专家", "s_cf4b1ead545e9bfc": "通配符只能用在黑名单里:{{p0}}", + "s_cf5c7eb43df2410f": "一个群", "s_cf6506cb7fc47b9c": "粘贴的文本 · 待上传", "s_cf6783b5388c5684": "放行(沙箱内)", + "s_cf8ab6767bf0526f": "二维码过期后已自动换新,请扫描新的二维码", "s_cf911266fb127d62": "展开项目栏", "s_cf9430e214bde1a8": "当前目录无法创建 worktree", "s_cfbb88c4ba804b90": "哑铃", @@ -1962,6 +2117,7 @@ export const sourceCatalogs = { "s_d04fcbda737fc0c6": "加载中", "s_d059018d896d6cde": "删除 {{p0}} 行", "s_d06e875dde456038": "还没有被拦截的路径", + "s_d0916de8c1b603bd": "已批准 {{p0}}", "s_d0ca08d604551151": "队列已暂停", "s_d0f680208ed8796c": "git 超时,点击重试", "s_d0fb5bbca8184bdd": "跳转到当前问题:{{p0}}", @@ -1981,6 +2137,8 @@ export const sourceCatalogs = { "s_d2fc539871184000": "插话已提交,正在打断当前回合", "s_d3136b6fa93050df": "来自 {{p0}} 个会话", "s_d31c72ba37a59575": "暂无已归档会话", + "s_d321ee860efcbb5d": "使用已保存的机器人,或者换一个。", + "s_d33995ad669c80cf": "创建频道", "s_d33e249955c846e3": "正在安全停止升级,请稍候。", "s_d35ab580f555345e": "停止请求发送失败,连接恢复后请重试", "s_d381ac2d678d3f8e": "点击窗口右上角关闭按钮时执行的操作", @@ -2004,6 +2162,7 @@ export const sourceCatalogs = { "s_d538a7ab8d9826bb": "启动导入失败", "s_d53d32f1a1211371": "自定义请求头 JSON", "s_d57bc0acdb5f23ab": "加载 diff 失败", + "s_d593d21fca298b8f": "微信机器人", "s_d5d126f6c79d9b11": "所有工作区", "s_d5d360963aa38a66": "打开会话失败:", "s_d5e75e3452a056c7": "插话提交失败:", @@ -2021,6 +2180,7 @@ export const sourceCatalogs = { "s_d69766c856685234": "新手指引", "s_d6cd7e5f2a8a82cf": "这条记忆已不存在,请刷新列表", "s_d6d20f05ed0f96e0": "单次执行时间", + "s_d6e882143a3c3b83": "扫码创建机器人", "s_d6f766f2adf53227": "排队中", "s_d6feded758aed3eb": "取消升级", "s_d73e140df277dffe": "模型已更新", @@ -2028,6 +2188,7 @@ export const sourceCatalogs = { "s_d74679ad02055214": "钩子操作失败:", "s_d74f204139fdd6ab": "搜索专家名称、Tag 或擅长领域", "s_d7503697e8e7850b": "重试主题工坊", + "s_d750c976bfff1be2": "打开飞书开放平台", "s_d77674f90811d7af": "配置范围", "s_d7875850553752e5": "切换专家失败:{{p0}}", "s_d7b2165028a50b95": "自动编辑和运行沙盒内命令,危险操作与越界请求需确认", @@ -2048,6 +2209,7 @@ export const sourceCatalogs = { "s_d94f9bafff59c06b": "已启用钩子", "s_d954c8e3f3042985": "危险命令", "s_d9686d510cd34aff": "通知会静默进入通知中心", + "s_d9894ab44928d667": "按提示起名字,用户名要以 bot 结尾。", "s_d9b7af1d4322eb34": "折叠全部 diff", "s_d9c35f6c3c4deeaa": "(空目录)", "s_d9f02c7caa8a847d": "保存自定义指令失败:", @@ -2074,9 +2236,11 @@ export const sourceCatalogs = { "s_db8630211fe3934a": "原生设置不可用", "s_db95142124934467": "音乐", "s_dbaa6ba5056580a7": "不在当前可选专家范围内", + "s_dbcac8ae7d65ae39": "二维码已过期", "s_dbcb25260e3df31a": "每一次命令执行、文件修改、沙箱拦截与规则变更的判定记录;最多保留 {{p0}} 条。", "s_dbd379b39f0d3537": "粉色", "s_dc2ba467fc7ac962": "描述", + "s_dc3cabe68bcc3046": "打开钉钉开放平台,创建「企业内部应用」,在「应用能力」里添加「机器人」。", "s_dc47f62bd3f882f0": "组建专家团", "s_dc4d662d071eea52": "创建于", "s_dc55764fa54e2a48": "MCP 配置范围", @@ -2092,6 +2256,7 @@ export const sourceCatalogs = { "s_dd42f689b17393c2": "正在加载更早的消息…", "s_dd7a9008edbca109": "已取消 {{p0}} 个会话的归档,{{p1}} 个失败", "s_dda206707b809157": "连接已中断", + "s_ddcf6e77b0ee1000": "准备", "s_ddde08946298195d": "还原", "s_ddfe2c796c7eb71b": "手动添加", "s_de03ee139fe2fd9f": "已找到", @@ -2103,6 +2268,7 @@ export const sourceCatalogs = { "s_de32e20193ad1042": "知道了", "s_de8184da1ef88d03": "已配置", "s_deb6051740064edd": "使用记忆", + "s_dec96df98f052d22": "打开 LINE Developers", "s_deeceedbed503b78": "每日代码健康巡检", "s_df359a6a54eab051": "通过配置和已启用的插件管理生命周期钩子。", "s_df3d58c7d84b85f2": "设置", @@ -2118,13 +2284,17 @@ export const sourceCatalogs = { "s_e058a2b5c4f81526": "查看全文", "s_e05f7e88f89c5f88": ",下载完成后自动应用。", "s_e07802f10db9b6b8": "找到适合当前工作的专家", + "s_e0959e358684c224": "{{p0}}(留空则不修改)", "s_e09b1c37f79f440d": "放行(沙箱外)", "s_e0b05fd1040bf6ce": "删除主题失败,请重试", "s_e0b1051909eb179e": "任务与工作区", "s_e0b6ce5e09cd4386": "服务器配置", "s_e0c8243464aa770c": "0 / {{p0}} 步", "s_e0d995508b32cdbb": "{{p0}}% 已用", + "s_e0e9b9269cbd7678": "打开 Discord 开发者后台", "s_e0ec651f0dae7cf3": "引用", + "s_e0f62acd5d3cebdd": "通道数据异常,详情见日志", + "s_e0fecdf371158416": "已取消扫码,原有配置未改动", "s_e122303044304acc": "使用此名称保存", "s_e17df82098f51602": "已将控制台日志添加到聊天", "s_e19c3b9e266c69c9": "浏览器", @@ -2139,6 +2309,7 @@ export const sourceCatalogs = { "s_e246195c1a39636e": "下一轮", "s_e2469247115761e9": "默认画质", "s_e26740162fbcb387": "将排队消息插入当前回合", + "s_e2780ee23078ea51": "{{p0}}{{p1}}请求使用 ACECode,到设置 > 消息通道处理", "s_e2a76ef1f12e147f": "终端", "s_e2f12da7faa68217": "已下载", "s_e2fda204edef967e": "JSON 必须是对象", @@ -2194,12 +2365,14 @@ export const sourceCatalogs = { "s_e806fbbe8ec6e82d": "插件", "s_e833097b603cb745": "{{p0}} 个上下文", "s_e84e54a6b6dbbbb9": "读写都拒绝:{{p0}}", + "s_e8ac23cf743aa1f8": "或者批准私聊请求,第一个被批准的人成为机主", "s_e8dc871a7fdee23b": "总计", "s_e8e08b0f61dd4ddb": "正在取消…", "s_e90589613fe54246": "显示隐藏", "s_e948d90880bb3dba": "JSON 无效,无法切换", "s_e951cbd07b5633c1": "移除本地项目", "s_e95f2ba203c9af41": "请等待正在运行的任务完成后再迁移", + "s_e99a6717b6917534": "处理", "s_e99b5859dcf96dc4": "向上查看更早的问题", "s_e9a1310b7638d211": "下一轮派遣{{p0}}:{{p1}}", "s_e9b00d7f92151b2d": "派遣 {{p0}}", @@ -2223,16 +2396,19 @@ export const sourceCatalogs = { "s_eb4c406398df4d73": "暂无工作区用量", "s_eb557068ab4833dd": "无法连接 Copilot 模型服务", "s_eb55f0980fa90bac": "等待你回答", + "s_eb92486a46895884": "现在可以私信机器人,或在服务器频道里 @ 它。第一次在频道里使用需要在设置页批准。", "s_eb93045599c0e25c": "进入 Plan 模式", "s_ebb240a1930e8bfb": "搜索图标", "s_ebb63196c7d3c318": "复制图片失败:", "s_ec7a11c7061bc164": "正在迁移工作空间…", "s_ec869826ec67f839": "Agent Browser 的部分浏览器数据未能复制,重启后可能需要重新登录", "s_ec9bbc8139d8bdd1": "专", + "s_ecdf5ea03d83b916": "line messaging api channel secret webhook cloudflared 机器人 im channel 消息通道 连接 取消连接", "s_ece45f7651c8c206": "Agent Browser 桌面桥不可用", "s_ed100f18801746c9": "命令规则已保存", "s_ed147f03761ab80e": "将元素添加到聊天", "s_ed150cce119ea114": "已记住 Git Bash 路径", + "s_ed16bd1db4b68498": "等待回答", "s_ed1790d489e671a9": "ACECode 本地工具继承全局", "s_ed21620162d871e5": "加载会话记录…", "s_ed2f18de3cdc1c5f": "关于 ACECode", @@ -2241,12 +2417,15 @@ export const sourceCatalogs = { "s_eda73a5daf8f734f": "当前问题:{{p0}}", "s_edb18f09d0ff4c83": "编辑模型 {{p0}}", "s_edbd4f79f1b7919a": "... +{{p0}} 行已折叠", + "s_ede83cc65dad0522": "使用已保存的机器人,或者重新扫码换一个。", "s_edecf0ae6e5144f9": "发送", "s_ee00e57c07195431": "当前运行模式不支持自动重启,请完全退出后重新启动 ACECode", "s_ee1db622be4f2443": "关闭 {{p0}}", + "s_ee1e5738be96819a": "已连接 {{p0}}", "s_ee32f25f70508f9c": "清除搜索", "s_ee34c74d32e7548a": "最近创建", "s_ee529160390df1c4": "端点模式", + "s_ee9ebe523d4205f8": "连接中…", "s_eea1694c235b5ddf": "每天", "s_eeccc53745f048de": "查看{{p0}}“{{p1}}”", "s_eeef861b87676f3f": "地球", @@ -2267,12 +2446,14 @@ export const sourceCatalogs = { "s_f00ef1be4493739b": "每次询问", "s_f0352e9758fbff2d": "禁止执行命令", "s_f035aab05364d2c5": "选择「{{p0}}」", + "s_f03b1d0aa80f24ea": "在 LINE 私聊或群里 @机器人 下达指令,结果回到聊天窗口。", "s_f04360cb3c9187dc": "{{p0}} 个文件有未保存的修改。是否保存后继续?", "s_f07ba1ef037a8e44": "已有定时任务", "s_f07bda17207c1e3e": "只读文件系统", "s_f0935665814a3f25": "复制连接失败:{{p0}}", "s_f098218a3032a625": "已拒绝", "s_f09e3585d4a4e27e": "模型连接", + "s_f0b4569c602a10a0": "使用已保存的机器人", "s_f121ab742cefcb8e": "需要关注", "s_f1929d7304960203": "已禁用钩子", "s_f1aa0173729f7228": "移除引用上下文", @@ -2280,6 +2461,7 @@ export const sourceCatalogs = { "s_f1b112d6acd841b1": "学士帽", "s_f1bdeeb9fb4cab81": "主题资源校验失败,无法导出", "s_f1e0dfb858e7fcfe": "钩子拒绝", + "s_f21f25112af89630": "回调地址已登记。请在 LINE Developers 控制台的 Messaging API 页打开 Use webhook,打开后这里会自动变为已连接。", "s_f27627514a2cb542": "专家组件加载失败", "s_f28461bb49c85647": "已完成", "s_f2902ada282332d2": "未知命令,沙箱内执行", @@ -2287,9 +2469,11 @@ export const sourceCatalogs = { "s_f2b4237b35f0b3b2": "查看会话", "s_f2bdc88464c51c2e": "显示 API Key", "s_f2d964d3dc5862d1": "超时:", + "s_f2df9f7ce435d9d6": "通道数据文件异常,详情见日志。", "s_f2f491398295b1b9": "莲花", "s_f323fc507d585565": "最小化到托盘", "s_f335420fcda22e54": "该文件格式暂不支持预览", + "s_f33c31866a8c335e": "已拒绝 {{p0}}", "s_f36f1a90b64f69e5": "Node.js 工具", "s_f37b8e7538b116da": "展开{{p0}}", "s_f39f410e0b493a67": "跳过指引", @@ -2331,6 +2515,7 @@ export const sourceCatalogs = { "s_f628f6c3e43734b2": "清除筛选后查看全部可用组件。", "s_f69273dbbebfb3a1": "格式化", "s_f699d16db6c37a35": "当前平台不能真正断网:关闭网络访问时只注入离线环境(代理指向无效端口、ssh 桩),不走代理的程序仍能联网。", + "s_f6dba35520d09e76": "telegram bot token botfather 机器人 机主 im channel 消息通道 连接 取消连接 owner approve revoke 授权 审批 撤销", "s_f6dc1b1641600dd0": "正在导入…", "s_f70f928df89406a7": "立即升级", "s_f72684a97677d48a": "对话问题轨道", @@ -2346,6 +2531,7 @@ export const sourceCatalogs = { "s_f8209b2310bc54a8": "打开快捷菜单", "s_f82c68c72b8f9aca": "下一题", "s_f84c7d6122a7753f": "用量记录", + "s_f8567a884d2336b5": "撤销 {{p0}} 的授权?", "s_f896a064e0be9e4e": "解析失败,整个文件未生效:{{p0}}", "s_f8b6ebf2a9e19bbd": "已复制图片", "s_f8c104adf2f77e87": "请填写专家名称", @@ -2409,6 +2595,7 @@ export const sourceCatalogs = { "s_fcea4baf388f169a": "运行核心测试,定位并修复可可靠复现的问题。", "s_fcf8b4bff0df782d": "打开文件夹", "s_fcffc107c99acbba": "已注册工作区", + "s_fd351bc95e9337e4": "发消息", "s_fd4b9e3b6c685bae": "继续编辑", "s_fd5fd3c4567938c7": "询问", "s_fd6529f1e944f8c7": "与当前会话共享文件;有任务运行时会排队。", @@ -2429,6 +2616,7 @@ export const sourceCatalogs = { "s_ff199a8f48c79fd8": "审计日志已导出", "s_ff20131bcd4725f2": "还没有规则。确认框里选「以后都允许」也会自动写到这里。", "s_ff509c9ba052a21c": "保存中…", + "s_ff62ea25d5d6b3a6": "叫停成员", "s_ff63d45ac706d173": "在新会话中继续", "s_ff857f2b6ac947ac": "最近整合", "s_ff8b0d734b03b1a4": "自定义模型", @@ -2458,10 +2646,12 @@ export const sourceCatalogs = { "s_01bf3b34f431074f": "This page does not meet App Transport Security's secure connection requirements.", "s_01ca13e5a1ebdc8d": "Checked items stay pinned in the sidebar; the rest live in the Extensions menu. Drag a row to reorder.", "s_01e1429e4860a2d5": "Weekly code change summary", + "s_01ebb09c9bad848f": "Feishu (China)", "s_020754edb04dde8d": "macOS system notification permissions", "s_0215ff0b3cce53f1": "Feedback content", "s_0220aafc260ee131": "After enabling the hooks function, the configured hooks will be displayed here", "s_025486bcb86499e3": "Please select a summary model", + "s_027be81bc401b0cd": "Connection failed; the reason is in the log. Check the credentials and retry, or go back to change them.", "s_029d7b3375ad16aa": "Rule written to file", "s_02b411ce6d16f108": "Dynamic context", "s_02bcdbc5a1453cb0": "copy relative path", @@ -2480,8 +2670,10 @@ export const sourceCatalogs = { "s_051836569928a9f9": "edit", "s_054b73a972372bdb": "No usable terminal found", "s_05564f10bcba6ae8": "The same expert can belong to multiple Tags", + "s_057dc352ed0a0a29": "Change bot", "s_0580fcee37e02012": "Delete failed:", "s_05a531a083e68807": "Copy {{p0}} verification code {{p1}}", + "s_05abefa30a624a8a": "Dispatching a task", "s_05df17e0e401d645": "Ask Agent to refactor, fix bugs, and add tests", "s_05efef8465e0c666": "git response timeout (the warehouse may be large)", "s_05fdf30411d98d32": "Sandbox", @@ -2502,6 +2694,7 @@ export const sourceCatalogs = { "s_07918dbd4c70b182": "Zoom out preview", "s_07a10608cd7a1e0c": "When off, sandboxed commands cannot reach the network (fully enforced on macOS / Linux)", "s_07b0e6eadae125e1": "target version", + "s_07bc10c7295f8dae": "Owner and approved contacts", "s_07bf8b28ae3680e0": "Expert component type", "s_07deea1ebeb8c9f8": "Read failed (HTTP {{p0}})", "s_07ee23a0924d3d0b": "Queue paused because you interrupted the current response", @@ -2517,6 +2710,7 @@ export const sourceCatalogs = { "s_08af65aa6bfa1a95": "Hand-written rules files are read-only here; edit the files directly. A file that fails to parse is ignored as a whole.", "s_08ba7ec0955d1d28": "Starting Browser", "s_08d43bdc4dcb6253": "Update available", + "s_08e5a3f74d4c6894": "Scan with QQ on your phone and follow the prompts to create and authorize the bot", "s_08e9d9ef7d382947": "writes only", "s_08f10fb5ebba9e65": "Use dark backgrounds; turn it off to use light backgrounds", "s_0970eb5b921e82c5": "{{p0}} line", @@ -2541,6 +2735,7 @@ export const sourceCatalogs = { "s_0acd4d885ae0c69a": "Source folders", "s_0ad947db2d9ee20a": "Operation is interrupted when daemon exits", "s_0ade55ea5a89bf7d": "Failed to write file", + "s_0adea32a9fd1dcbe": "Scanned. You are now the owner", "s_0ae64c633fbe0f42": "Check for out-of-date dependencies, security risks, and manageable technical debt.", "s_0ae7312510f4b495": "Collapse details", "s_0af42a6e2915abaa": "Could not delete memory", @@ -2566,6 +2761,7 @@ export const sourceCatalogs = { "s_0c8320ac03732e4c": "Access token required", "s_0c869dac8a4aa550": "Please enter the full path to bash.exe", "s_0cc2ca1cf8347d79": "Task completion notifications disabled", + "s_0ced3f40670a5ac1": "Lark (international)", "s_0cf8b14f4651f4de": "last week", "s_0d0cbac2eee54113": "Rename", "s_0d0e1a86b3aa7877": "User", @@ -2577,12 +2773,14 @@ export const sourceCatalogs = { "s_0d94ad38844747ce": "Switch failed:", "s_0d966a3d69ee069c": "When off, every command runs with your user privileges and unknown commands in auto mode ask for confirmation one by one", "s_0de50760cb250e32": "Remove model {{p0}}", + "s_0df737cb62bb5175": "Group", "s_0e0163080d2e4ad4": "Unable to get file path", "s_0e0c2d7597ef508d": "Missing", "s_0e1a7040390fb03e": "New project", "s_0e700e0f522f37ff": "Comparison baseline:{{p0}}(only switches the viewing object, not checkout)", "s_0e993148f8f5d3d2": "The Grok sign-in is invalid. Connect again.", "s_0eab58baf452449c": "Enter a full absolute path, such as C:\\Users or /home", + "s_0eb01c40432b05a9": "Connection lost, retrying automatically", "s_0eca7afbf4f68601": "Search for skill name or description...", "s_0ecd009b61757eae": "Create a separate worktree. Uncommitted changes are not included.", "s_0f415f8b2bbbb37a": "Failed to read running records:", @@ -2596,12 +2794,14 @@ export const sourceCatalogs = { "s_10e80ac177155143": "Message cannot be empty", "s_111448a2e1a82306": "There are {{p0}} fields", "s_11152b370ff3e7ca": "Ultra quality model", + "s_1117493dfd5708d1": "Find the robot in DingTalk and send it the 6-digit code below in a private chat.", "s_1123245d1bed08b2": "Rule", "s_11448fff91a477dc": "Created", "s_1159250ae5748529": "Invalid close-window behavior", "s_117b19fd23b61edb": "Allow, prompt or forbid commands by prefix. Prefixes are split on spaces; git status|diff means the second word may be either. \"Allow (outside sandbox)\" skips the sandbox, so reserve it for commands that truly need it; interpreters, shells, rm and sudo cannot be allowed.", "s_117c113c3b45cf89": "queue", "s_1198cb9da295145d": "New sessions use this mode by default, and the current session will be switched synchronously.", + "s_11a4dc9fc810be2f": "Set the robot's message mode to Stream, grant the internal robot send-message permission under Permissions, then publish the app.", "s_11a6ac8794688be8": "Matched a prompt rule", "s_11b999fc4610de85": "networking", "s_11cb87897cfb7e7b": "{{p0}} capability", @@ -2621,6 +2821,7 @@ export const sourceCatalogs = { "s_132411ba41da7354": "reasoning", "s_132a029e79ec9872": "Share with agent", "s_13357fcd2d59d851": "The command prefix must not be empty", + "s_13439ef69aeff4f2": "Copy the App ID and App Secret from Credentials & Basic Info and enter them below.", "s_134a1a3e2b52b04f": "Collapse the entire right panel", "s_134fdd6479c8036b": "Send (Enter)", "s_13670a878cf6f01d": "ACECode upgrade", @@ -2635,14 +2836,17 @@ export const sourceCatalogs = { "s_13f6a415ac664be2": "Detect the actual model list from this provider", "s_140b07fe3f9f7d48": "Download size:", "s_1417bae3d17d6405": "Collapse images", + "s_142cf02f83d4e7ab": "Writing reply", "s_14357c0d6ca270df": "Check that the address is correct, then try again.", "s_14381a6ef00a1253": "Regular testing and problem fixing", + "s_143dab9db74c3131": "All set", "s_145219e726b87790": "Hidden", "s_145eb48a326b5d27": "Tool rewrite", "s_148d195e21b05db5": "Configuration", "s_14cbad1d857ee3e6": "Disconnected. The stop has not been confirmed", "s_14d1e349b14010a9": "Cancel download", "s_14e7acfc08afa9e9": "Browser component stopped", + "s_14ee02a96d9bca6d": "Connected · {{p0}}", "s_150dd0cecdc803d0": "Current workspace", "s_150f3c28d9d036af": "Title cleared", "s_155c2f5d0a3b2394": "Notebook", @@ -2671,24 +2875,32 @@ export const sourceCatalogs = { "s_1832990660bb60c2": "function", "s_183b5f823fdb5522": "Failed to add workspace:", "s_18435419f154847b": "Command prefix", + "s_1843acb8c6ea0f47": "DM the bot or @mention it in a server channel to give instructions. Results come back to the channel.", "s_184568b57e5e6d3f": "Generating test image…", "s_1847a84db6d5fc1a": "Permission mode switch failed:", "s_18565a5455a0a875": "Could not load sandbox settings", + "s_18578640315b5b33": "Updating todos", "s_1882a447d5f76d5f": "Feedback cannot exceed 10,000 characters", + "s_188397c445996182": "Connected by another ACECode", "s_18850bf66a4ba4c6": "The model requests temporary extra sandbox permissions", + "s_1903b018a3b98d7d": "{{p0}} bot", "s_19086bec77d5e5e5": "Automatically compatible with system naming rules", + "s_190eedb7bfaa8609": "Adding a follow-up task", "s_191041794ad98158": "(all matches)", "s_1910636fddbffe3e": "Read application controls and operate the mouse and keyboard", "s_19107d868966ff2c": "Session title", "s_194b5dba3c63f537": "Reasoning settings", "s_1964dafada0807b5": "Git branch", "s_19af3dcd414d81df": "{{p0}} attachments", + "s_19bf536853e2e78a": "Collaborating", "s_19bfb1631d25a204": "Saving theme…", "s_19cfa7d09d8dd287": "Dispatching…", "s_1a0bf11a28779888": "Fill in the real chat box after selection, it will not be sent automatically", "s_1a16ea81ee6a29e4": "The site could not complete the redirect. Try again later.", + "s_1a474c3207ed49f4": "Standing by", "s_1a4c6ebd7bbdbaa8": "No competency introduction has been filled in yet.", "s_1a52bda1e037f2c3": "Used while executing, coding, or handling a task", + "s_1a6c41c6ee173bab": "Bound to {{p0}}", "s_1a7224bf1e427ee4": "Fork failed: No session_id", "s_1a7e48179574fffa": "Collapse change summary", "s_1a7eef17e199fa07": "Collapse command details", @@ -2700,6 +2912,7 @@ export const sourceCatalogs = { "s_1ae7a34c1452785e": "Markdown WYSIWYG editor", "s_1afed6a81a2512d2": "Select model", "s_1b14bd33bab15a6d": "Inherit global settings for {{p0}}", + "s_1b442420620b3bb4": "You can now message the bot privately in Feishu. Add it to a group and @mention it there; the first time needs approval in Settings.", "s_1b479e1420fb0342": "Directory allowed", "s_1b4bc8cfe06cb405": "Mark as read", "s_1b58f84ec946fe67": "Career/Position", @@ -2747,6 +2960,7 @@ export const sourceCatalogs = { "s_1f4b8e02fe0eb7ab": "You can select multiple models and fill in the new model form at once after confirmation.", "s_1f6c6927e129a5f5": "Please upgrade and restart the ACECode daemon and try again.", "s_1f79c9f362dc9d4b": "Could not save appearance settings; restored the last saved settings:", + "s_1f9d55088d4f8ec0": "The owner can switch to any session; others can only switch between sessions they created.", "s_1fca6787d35baa53": "Enter Plan", "s_1fcd48a731ea363a": "Native selector returns invalid results", "s_1fe3eb68a68b187d": "Allowed and remembered", @@ -2769,6 +2983,7 @@ export const sourceCatalogs = { "s_21482b7f3029698f": "Make lead", "s_21493a0f78021d9b": "Select current folder", "s_2156709a76aa43e6": "Enter message…", + "s_2157f1235d2a58ee": "Member {{p1}} of group {{p0}}", "s_21a08761b9914419": "One entry per line. ~ is the home directory,:workspace_roots the current workspace root,:tmpdir the sandbox temp directory; everything else must be an absolute path. The deny list accepts wildcards such as **/.env.", "s_21a9727afd60df42": "Swarm mode (Star) enabled", "s_21cab4f2e52fbfef": "Unnamed conversation", @@ -2777,6 +2992,7 @@ export const sourceCatalogs = { "s_2217610d8e605cdc": "Open the global Skill Catalog", "s_222c9c161b8907d6": "This conversation has been compacted several times. Continue the current work in a new conversation.", "s_222eac7780eaef4d": "Model configuration checked", + "s_225a51a3b48fddf4": "DM the bot in Discord (you need to share a server with it) and send the 6-digit code below.", "s_227ea7277bfc00e9": "Failed to load MCP:", "s_228d8b711ccf3ab3": "Submit feedback", "s_228f61d57a0a4568": "Explicitly selected", @@ -2797,9 +3013,11 @@ export const sourceCatalogs = { "s_23f176008ae4f3bd": "The native file data is invalid", "s_246e46c2a4189cb5": "Rename failed:", "s_248e8e2921bcc92c": "conversation messages auto collapse fold expand tools", + "s_251dee09f7adbd2e": "Fill in the credentials first", "s_2529b19bc41c0e9e": "Request format error", "s_2539cf900d5b8d37": "Installing updates", "s_255c8d56e8bb1295": "No matching results", + "s_2560c96f89c6fea0": "Create an official account in LINE Official Account Manager and enable the Messaging API under Settings > Messaging API.", "s_257bbcc44c839db4": "Modified", "s_257c21aae4198b7e": "Setup and review", "s_25c76c88d05b627c": "Native filesystem bridge unavailable", @@ -2809,18 +3027,23 @@ export const sourceCatalogs = { "s_25ec0421b03bc4f3": "Drag to adjust the width of the right column", "s_261dba7261ed8c05": "Description cannot be empty", "s_2623156dcd71bf2d": "Asterisk", + "s_264333fb5458a573": "Preparing a tool call", "s_2657124b1481b789": "Provided by project", "s_267c4a7a86dd1849": "All types", "s_26ab0eee67e8c707": "Tool called", "s_26c8aefe6fbfa744": "Sandbox, file and command policies, plus an audit record of every decision.", "s_26da3cee0bb25151": "Wait for user to reply to AskUserQuestion", "s_26df939b25bc7b02": "Queued message", + "s_270dd2ebfcbec271": "Add the official account as a friend in LINE and send it the 6-digit code below.", "s_2719a8ea304b8cbf": "from", "s_2719f7ce2ace6db7": "Can't open macOS notification settings", "s_2721b6c32975b55d": "Remote Web connection", + "s_27389885c97c4ab1": "DingTalk", + "s_273f40d0251a8261": "Open DingTalk Open Platform", "s_2745ccb884fcf6ab": "Feedback", "s_27586f180db1c812": "renamed", "s_2812f6c1263c96a9": "Deleted “{{p0}}”", + "s_281b0924bb9c5a16": "Scan with WeChat and confirm on your phone. The WeChat account that scans becomes the owner and is the only one who can use the bot.", "s_28200a87c40ced80": "Import and apply", "s_28321cb9b77f137c": "Branch from here", "s_2834cf755087f50d": "Summary generation", @@ -2860,6 +3083,7 @@ export const sourceCatalogs = { "s_2c7dfb7f0b25589e": "Search current conversation content", "s_2c843e836a4d4422": "Failed to save close-window settings:", "s_2c8855fc8ab8f5fb": "No usable non-local network address was found. Check your network adapter.", + "s_2c8904ba4a192db8": "Create bot", "s_2ca1a16a870f47d3": "No matching settings", "s_2cc0b5d70839d137": "Mermaid source", "s_2cd0f3be8738a86c": "Cancel", @@ -2913,6 +3137,8 @@ export const sourceCatalogs = { "s_31ebdbfa17b81a02": "Model saved", "s_32058ae99d61249c": "Could not get migration progress: {{p0}}", "s_321e4914f68c1db0": "Unknown upgrade error", + "s_3221a042ea3677a8": "Regenerate", + "s_323bf944ea7c06e1": "Give instructions in a Feishu private chat or by @mentioning the bot in a group. Results come back to the chat.", "s_3264ce720a9e420d": "The theme is downloading. Please wait.", "s_326d1d537e72f82d": "Starting or stopping reverse proxy", "s_327f00da9fc2ec36": "Hide details panel", @@ -2939,15 +3165,18 @@ export const sourceCatalogs = { "s_34b3cac2a496d263": "Model catalog", "s_34c3c3b2bd45600d": "Expand all diff", "s_34f7336d39831fca": "Close all", + "s_350a7736d0d4cb03": "Set owner", "s_3548b099890e187f": "Model usage details", "s_3568d317c8161ad4": "This is an embedded browser.", "s_357211096f78cdb8": "Set as default", "s_3590fba381ad57f0": "Selected text actions", "s_35c52c32418a2678": "All {{p0}} entries will be deleted and cannot be recovered. Export first if you need a copy.", "s_35cda8a7052dec20": "Migration failed", + "s_3615b021b8af5e82": "Wrapping up", "s_3620d3d986f14584": "Close model dialog", "s_3625f9fa2f9c629a": "Command denied", "s_364970d8168ca5fe": "Sending…", + "s_3687f953cfe85fb8": "Disconnect", "s_369078855fa122f7": "Read-only tool calls", "s_36f33adaf0942634": "Confirm", "s_3703e47860f64a12": "File does not exist", @@ -2957,13 +3186,16 @@ export const sourceCatalogs = { "s_375f5091e213f094": "Paths are on the machine running the daemon", "s_377e4422342c71f7": "Problem feedback has been uploaded", "s_3785cba81d184679": "Current workspace memory", + "s_378eaca774bb9551": "Scan to sign in", "s_37979829426b0cdb": "Workspace", + "s_37a41b2e983b06ee": "Using a skill", "s_37c427d9f5dc817d": "The default model has been set to", "s_37d36b087f49c6d2": "Select expert component", "s_37e4e42c314102da": "image", "s_381117baedb79e1a": "After choosing a provider and model, only settings supported by that connection are shown.", "s_381796352dc0301d": "Settings were saved but could not be applied. Please try again", "s_3819d5e4aa5dba68": "Pen", + "s_38744132b501a1eb": "Question", "s_38c043e08502e710": "Personalization", "s_38cd88ed8b3ddbf1": "Copy full output", "s_38d456bbdb81317c": "Filter by Tag", @@ -2971,6 +3203,7 @@ export const sourceCatalogs = { "s_390fd9bc7d9d7a2a": "Audit Center", "s_392d0dceb45998d3": "Extra high", "s_39592d688f12a5a6": "An entry with the same name already exists", + "s_3969d8203ae05775": "QR code for connecting", "s_39932f24fe11a6ba": "document", "s_39a9046f9c8c993d": "Recently used", "s_39b068ea492a4f9a": "Model capability label is invalid", @@ -3012,10 +3245,12 @@ export const sourceCatalogs = { "s_3db7b06b5f6de0e0": "Working directory", "s_3dcff5522afda275": "Prompt cache hit rate", "s_3dd1245c155aba60": "The model configuration is unavailable; the current connection was retained", + "s_3dd922527fba482d": "Add the bot to a group and @mention it there; the first time needs approval in Settings.", "s_3de42d7c68d442e5": "Single", "s_3df739dcd8a069eb": "Recent conversation history", "s_3e04f83df97fa6f2": "Oldest updated", "s_3e48e68ed87a9add": "Click the empty area or press / to type a path", + "s_3e6221be3922f850": "Scan with WeChat and confirm on your phone", "s_3e65bba57b173f30": "The pasted text is too long for the /{{p0}} limit ({{p1}} bytes). Remove the command to send it as a normal message, or shorten it", "s_3e7d75b94f3a1f26": "Failed to load tool rewrite settings", "s_3ea0e6e41518ea24": "Empty directory", @@ -3067,6 +3302,7 @@ export const sourceCatalogs = { "s_42e1b73173a3b1d4": "Could not confirm the expert switch status: {{p0}}", "s_432a6c3328243eee": "Could not reach the xAI device authorization service", "s_4334aa1fdaa5a8c1": "Observations extracted from sessions that have not been consolidated into memories yet", + "s_433779053c77eea8": "Bot {{p0}}", "s_434203a723b4614e": "updated", "s_4342d572b0ee2718": "The main session can still continue typing", "s_434c7efecbad5725": "Please select execution time", @@ -3076,11 +3312,13 @@ export const sourceCatalogs = { "s_4382c23e9e50479f": "The Agent has completed the plan and exits Plan mode after requesting approval.", "s_43911903e1cf7e63": "Preview content copied", "s_43cad95bdcdcc30d": "Upgrade failed", + "s_43ce458cae49a1ad": "Thinking", "s_43d7d2b51d75fa42": "Create project", "s_43db2741a745b179": "Enable server", "s_43f3dc12a6077835": "Starting...", "s_43f4a4d322d58a5b": "Edited {{p0}}", "s_43fc32e85fb21c53": "Active goal", + "s_43fcef33a825715a": "Paste the token below. Once it is verified, use the invite link to add the bot to your server.", "s_43fd0fc1e9ae1edb": "Name conflict resolved", "s_4430768f253a5d6d": "Enable tool rewrite for subsequent model requests", "s_443cb29217829285": "Review specialties and dispatch to a new task, or manage your own experts and expert teams.", @@ -3116,6 +3354,7 @@ export const sourceCatalogs = { "s_475ac58abf66032b": "Could not load the root folders: {{p0}}", "s_476c022a773e1f82": "Close other ACECode windows before migrating", "s_478211dd029aa604": "The site security certificate is invalid or does not match this address.", + "s_47b50c43e14ca6ba": "Scanned", "s_47c99fb0ddf2cffa": "{{p0}} – {{p1}}: {{p2}} tokens used in total", "s_47f22728ee1600ed": "Status message:", "s_481acc2c1413db38": "Image generation", @@ -3126,6 +3365,7 @@ export const sourceCatalogs = { "s_4871613a44be8d7a": "Where to open files and folders by default", "s_487422ef0c4e25b6": "Home title", "s_48807d75530c1b03": "This queued message cannot be found", + "s_4888c0a26e905c1f": "User {{p0}}", "s_48d75d4b7e92d9f2": "Switch to project", "s_48f78f1abeca0c47": "The pasted content is large ({{p0}}) and will be uploaded in {{p1}} parts", "s_490a88b8ae9d4a2e": "This model no longer exists, please refresh", @@ -3141,12 +3381,14 @@ export const sourceCatalogs = { "s_4a6d8d490c54d438": "allowed once", "s_4a83accbad2c11e1": "opening remarks", "s_4a8d6841b4023edf": "Confirm import", + "s_4aa95aa725909efe": "Add the bot to your server", "s_4ab32793197b28db": "The selected model is unavailable; the scheduled task was disabled", "s_4ac9db43b40c292d": "The current backend does not support Computer Use. Update ACECode, restart, and try again.", "s_4ad732f1d78f6725": "Background process", "s_4ad8aa6461e64127": "Previous (Shift+Tab)", "s_4af8f4c6dd2e233e": "Brain", "s_4afad488b461b58f": "Side chat history", + "s_4b19c2f91cd3f418": "Messaging channels can only be configured on the computer running ACECode", "s_4b427656a253ca1e": "Fully quit and restart ACECode", "s_4b47dbae97ba938a": "prompt word", "s_4b50c9fa24dcafa3": "No model configured", @@ -3168,6 +3410,7 @@ export const sourceCatalogs = { "s_4cc7b00050cb1c21": "OK", "s_4cca8a4406aea952": "Confirmation required", "s_4ce87e7fe2b6e95d": "File or folder", + "s_4ceae06adf5b9199": "Their messages stop being processed immediately, and any running turn in their sessions is stopped.", "s_4d0680f9efaef147": "unread", "s_4d2b19065afe4b07": "LSP is on:{{p0}}", "s_4d3f6a081ea86097": "Complete deletion failed:", @@ -3190,7 +3433,9 @@ export const sourceCatalogs = { "s_4ef515abfdb8690d": "Installation package", "s_4f01af9093fc9e59": "Cannot connect to this site", "s_4f021f59121c6827": "Waiting for execution", + "s_4f1f6ea2b75de587": "Messaging channels", "s_4f2bf16cc26825e8": "Unselect custom answer", + "s_4f3bac187963a04a": "Manage {{p0}}", "s_4f5b0e563a452c80": "Refresh skill list", "s_4f61317e9307ad3e": "Access denied", "s_4fca07787f3ef1ec": "Remove this pending attachment?", @@ -3198,6 +3443,7 @@ export const sourceCatalogs = { "s_4fe5ca6370a901ef": "ACECode local tool categories", "s_4fed9b0a87376127": "Theme workshop", "s_5002a3c1c2822955": "Native and managed", + "s_500b6710a6f009cc": "Create and publish a version under Version Management & Release, with availability including yourself.", "s_5019872ac7610542": "Cannot open this site securely", "s_5026a63b58cdf048": "Executing", "s_50563579e4119e75": "Other rules files", @@ -3233,6 +3479,7 @@ export const sourceCatalogs = { "s_52b06532811be3e4": "Duplicates another rule", "s_52b44d4d2c562541": "Connect xAI", "s_52d25a9e30ba94f1": "About", + "s_52ebdbe6cf99a77c": "No one is approved yet.", "s_52f79fed15ec2723": "Current model", "s_52fb62f878dcdbef": "Theme verification failed. Download it again.", "s_5305d3b2b3f1648f": "Popcorn", @@ -3263,9 +3510,11 @@ export const sourceCatalogs = { "s_55db57105231bd95": "Workspace is unavailable", "s_55ef676879f7cca3": "The diff is too large, please check it in the terminal", "s_561817f0252936a9": "Allowed", + "s_562834f547655a37": "Searching code", "s_5653d97cdf569cb9": "Your system firewall, router, or cloud security group may still need to allow the proxy port. For public Internet access, continue to use a trusted VPN and configure HTTPS upstream.", "s_56627c94a9decee6": "Maximum output tokens", "s_566e5f182a63185d": "Compressing theme files…", + "s_5670da45d1d6e526": "App ID (starts with cli_)", "s_567146d66825e213": "Enable automatic line wrapping", "s_56964754c2e1ab57": "Reverse proxy listening on {{p0}}:{{p1}}", "s_569af1a47dbdf053": "up to date", @@ -3287,6 +3536,7 @@ export const sourceCatalogs = { "s_57e4fcb2dad50b5c": "Remote Web mode enabled", "s_57eb46dacc97defb": "Show the last active time on the right of each task row; when off, it is still available in the hover card", "s_5804b9cca84304ff": "Clear side chat", + "s_58210afb340c2c5e": "The link is valid for 10 minutes and works once.", "s_5835048333dad1d6": "Select custom answer", "s_584f28c1077647a9": "Cancelling...", "s_5868e5f7632072d9": "Denied {{p0}}", @@ -3340,8 +3590,10 @@ export const sourceCatalogs = { "s_5d7fd21f1e3ca6b4": "Copy message", "s_5d817dbb5da09259": "The theme import returned an invalid result. Refresh and try again", "s_5d937ed111134879": "Model capabilities", + "s_5ddcb16ab6fcb808": "Checking members", "s_5ddffd77ea7fedae": "Loading saved models…", "s_5e56bec8550f499d": "Choose a directory", + "s_5e78c941de4a98ed": "Connect {{p0}}", "s_5e80c1e561d45b11": "Select permission mode", "s_5e8e7af57abbcbcf": "Deleting…", "s_5eaefa4a3950e8a3": "Choose a new path", @@ -3352,11 +3604,14 @@ export const sourceCatalogs = { "s_5f53727059cdc69a": "Enter your answer", "s_5f5a4c5fb8ab71d6": "The input box is currently unavailable", "s_5f5b6195dfd24aa1": "Waiting for interactive content to be synchronized", + "s_5f76b2bf82dd2c5d": "Region", "s_5f7780324a4b55f1": "Conversation continuation suggestion", "s_5fba64a2b5976bd4": "Upload failed, click to retry", "s_5fcad9600a50f834": "Close the tab on the right", "s_5fe46d0c2faeacf7": "Choose a model connection", "s_605a534da98e42ec": "Token activity view", + "s_60b83f21d9d5d703": "Group {{p0}}", + "s_610a30de692b91b0": "Forward your public address to http://127.0.0.1:{{p0}} on this computer. Setup continues automatically once it is reachable.", "s_610e4e820d6881ea": "Completely delete session \"{{p0}}\"? This action cannot be undone.", "s_6135d4159e892541": "Remove", "s_613de904cc604e3b": "{{p0}} local tool {{p1}}", @@ -3369,7 +3624,9 @@ export const sourceCatalogs = { "s_624b5799ebd036b0": "No workspace", "s_6258a2c6f9c89143": "Copy all", "s_62731d95bc805803": "Exporting…", + "s_6278c2f645ab3982": "Public address", "s_629bb52ce0e5270f": "Added {{p0}} to chat", + "s_62b72265c96e5e34": "Retrying", "s_62c850eabc4ba70e": "Partial search data failed to load:", "s_6309a3bb5ba4c714": "Save failed", "s_63448df0ea0c5be2": "Interjection interrupted", @@ -3385,7 +3642,9 @@ export const sourceCatalogs = { "s_641778357a5e8a41": "View configuration schema", "s_643a1b94e9498780": "Trusted hook", "s_6492494fbe805292": "Describe your idea, create your theme", + "s_64ded447c5bdd6ea": "Verify and continue", "s_64e9f3e72b149b27": "This page exited unexpectedly. Reload it.", + "s_64fd6a54381b531c": "Couldn't get a QR code. Try again", "s_64fdd111ae7c2ead": "Workspace migration is in progress. Wait for it to finish, then restart.", "s_651ec477427c76eb": "Add capability or context", "s_65273e466af9881c": "From background task:{{p0}}", @@ -3396,6 +3655,7 @@ export const sourceCatalogs = { "s_65b3f17534a8c0ab": "Could not load memories", "s_663a5905150ea46d": "Check your system proxy settings, then reload the page.", "s_664c216a6c75054f": "/expert-manager Help me create an XXX expert specializing in XXXXX. My experience is: [add your industry background and relevant experience].", + "s_66687ec68c240db9": "Turn on Use webhook on the Messaging API page, and turn off auto-reply and greeting messages in the official account's response settings.", "s_6679f618e868b532": "slash command", "s_667b75d63a19b6b8": "Upload failed:", "s_66809c2ae51ddf06": "Remove Tag {{p0}}", @@ -3447,7 +3707,9 @@ export const sourceCatalogs = { "s_6aa3b1bd38fe141b": "name cannot be empty", "s_6abdf8409678b516": "View in the review panel on the right", "s_6ad55eb177526999": "Zoom in preview", + "s_6add2714c40f1d45": "Private chat", "s_6ae40b532ea0f924": "Work has continued in a new conversation.", + "s_6aecec7bb2ae1d74": "Public address (optional; leave empty for an automatic tunnel)", "s_6b01fce4dbeaeeb9": "Change", "s_6bbaf705e8c64c2b": "Folder contents", "s_6bea77acefb364ac": "Standard", @@ -3464,6 +3726,7 @@ export const sourceCatalogs = { "s_6cec8d70466c7b55": "Get ACECode updates from Library in Microsoft Store.", "s_6cf2531ab1119b67": "Authorize", "s_6d0b032bc4d31ea2": "Copy title", + "s_6d0d116c95898867": "Owner", "s_6d330244eecae355": "Failed to connect to daemon:", "s_6d3d916f1807a4d2": "Continue to add tags", "s_6d5503cc0bd22117": "Select detected model", @@ -3478,6 +3741,7 @@ export const sourceCatalogs = { "s_6dc0d06dd58b2cb6": "Built-in default deny list", "s_6dd39868b7d587c7": "The current background service does not support the Security Center. Update ACECode, restart and try again", "s_6e1ba4bf187327e9": "Terminal program path", + "s_6e382d9250f8dc10": "Enter manually", "s_6e392a597af4abab": "memory summary model", "s_6e55e0f207bfd2e0": "Run history", "s_6e6aba42eba94da0": "Shared with agent", @@ -3487,6 +3751,7 @@ export const sourceCatalogs = { "s_6f4b75ff28679ce4": "Flat list", "s_6f5994e138557f90": "% used", "s_6f87682bc02532c6": "Each workspace can have an independent Skill, which only takes effect in the session of that workspace.", + "s_6f98bb98abfcd0c9": "Open the Feishu Open Platform, create a custom app, and add the Bot capability under Add features.", "s_6f9c11640850a30c": "Could not clear the audit log", "s_6fb9dd3555d1b3b0": "Confirm migration", "s_6fc38ef3602fe789": "Context", @@ -3512,12 +3777,16 @@ export const sourceCatalogs = { "s_71dcce0699b98a09": "Expert components", "s_71dfe4a817b8801d": "Please log in to Copilot first", "s_71e95b289aad1f6a": "skipped", + "s_71ea09dd1e9905d5": "If Telegram is signed in on this computer, you can also click the link:", "s_71f09042e226bdf9": "High quality model", "s_71fa1b09e415447e": "Configure the identity, areas of expertise, opening remarks, and runtime capabilities in the same expert.", "s_720182db8f88cf7d": "Reading runtime capabilities...", + "s_722dd20ced9e6d55": "Send the bot a private message from your {{p0}}. A request appears below; approve it to become the owner.", "s_725831ec12394243": "Unarchiving failed:", + "s_72ccb1274a093efb": "Enter the bot's AppID and AppSecret from QQ Open Platform. They are verified before saving.", "s_72dde8dfef88d37d": "Confirm to add", "s_72ec61d7855c86a1": "Request timeout (seconds)", + "s_737c725c1321d260": "Saved {{p0}}. Leave blank to keep it", "s_73952aa91a9366b5": "Switched to", "s_73ae35f541bc8124": "Copy attachment address", "s_73b629753f6d5fe5": "All diffs copied", @@ -3526,6 +3795,7 @@ export const sourceCatalogs = { "s_73e5df48000d6a22": "Choose task location", "s_73fab2d02ab591cb": "Model test failed {{p0}}: {{p1}}{{p2}}", "s_7409a608060f5804": "not specified", + "s_741a285fa14f8523": "Compacting context", "s_742025b1a0eccc8c": "Invalid type", "s_743342489c23fe24": "Refresh memories", "s_744a61203be24c0b": "per {{p0}}{{p1}}", @@ -3564,6 +3834,7 @@ export const sourceCatalogs = { "s_76e350aa9b3cf01a": "Reading default location...", "s_76ebf39ee090026f": "Escalation cannot be approved unattended", "s_76f677430db630af": "The embedded Browser is available only in ACECode Desktop", + "s_7715155115f58274": "Back to QR code", "s_77322b7c285eb8bd": "Theme deleted", "s_7734b5d136de7bdb": "Automatic restart failed:", "s_77423bdd32d35ce3": "Edit the message you just sent", @@ -3583,11 +3854,13 @@ export const sourceCatalogs = { "s_78e5c1be3c1a2347": "Kettlebell", "s_78ea07a31cd96f65": "Starting from this version, new token usage will be recorded, and historical sessions will not be backfilled.", "s_79167440a990362e": "Session exported:{{p0}}", + "s_79209170acff4896": "Waiting for approval", "s_79383e9981843085": "Read web core information", "s_7948c26c9630a0cc": "Senior development engineer", "s_79612d07708f27c8": "Platform status", "s_7967bb84220ae3d1": "There are no skills in this workspace; they can be found by placing them in the project's.acecode/skills directory.", "s_797644874d78c30a": "Clipboard unavailable", + "s_79ee5900e8dc266b": "{{p0}} pairing request", "s_79f326be4409d51f": "Project", "s_7a0b79e584676550": "ACECode global project directory", "s_7a437376c304672c": "Add this message before the next model call in the current round", @@ -3596,6 +3869,7 @@ export const sourceCatalogs = { "s_7a8a11ead50742a2": "Add to", "s_7a9b22bb6b0a159e": "Grok Coding Plan disconnected", "s_7aa6d0ef5feb745c": "Delete completely", + "s_7ace720715576424": "Binding code", "s_7acf2ee49b4924ef": "Location: {{p0}}", "s_7ad6bbf3f8508928": "Dismiss suggestion", "s_7adc856731f31518": "Not run yet", @@ -3603,6 +3877,7 @@ export const sourceCatalogs = { "s_7afb72a4b5b21193": "Add workspace", "s_7b00c3d69a0c48a5": "models", "s_7b10b3966368f243": "no match", + "s_7b113358cc1fe5fd": "A new contact", "s_7b1347c4ded0097a": "Side chat", "s_7b187349d641590c": "Export session failed:", "s_7b1c1c5b441d279b": "Check the site address and your network connection, then try again.", @@ -3630,6 +3905,7 @@ export const sourceCatalogs = { "s_7d09ee28674f2d4b": "Reason for skipping:", "s_7d1070ff5c5b7bdf": "[Pasted text: {{p0}}]", "s_7d166479398a3bce": "Submitting interjection…", + "s_7d20222788427d6b": "Under Permissions, enable: im:message, im:message:send_as_bot, im:message.p2p_msg:readonly, im:message.group_at_msg:readonly, im:resource.", "s_7d3a4b81e7018779": "Conversation history changed. Try again", "s_7d4ca7f48effd602": "big", "s_7d6667c86ebec2fa": "Goal completed", @@ -3642,11 +3918,13 @@ export const sourceCatalogs = { "s_7df5c456c765e4c3": "Disable", "s_7e1df49bc3432fb2": "The provider definition for this profile was not found", "s_7e2ffe585c643f17": "best-effort offline", + "s_7e4419d1de251094": "Feishu", "s_7e8586f653066a9c": "Delete model profile", "s_7e963d9ae21a7ad5": "Summary model", "s_7ea852a1f549b6ca": "Enter a scheduled task name", "s_7ecf1cde648680d2": "Selected", "s_7ecff345fb5ca29e": "Copilot authorization verification failed", + "s_7ed80e44eaf9c51c": "Writing a file", "s_7edb086ee0d4c6c8": "Task completion notifications are enabled, but macOS permission was denied", "s_7ee8b2d0ae7ade97": "Remove attachment", "s_7efe433909425026": "Custom request headers are invalid", @@ -3657,6 +3935,7 @@ export const sourceCatalogs = { "s_7f64e6cc3a5dccc7": "Resize background task panel", "s_7f68ebad19ba6bcd": "Check for updates", "s_7f6c661eb101f549": "Connect GitHub", + "s_7f92b2869303b795": "Message the bot privately in WeChat to give instructions. Results come back to the chat.", "s_7fd7c6c1c60155c2": "No selected text to annotate", "s_7fe9cf36941a738f": "Maximize preview panel", "s_80160e448f307c23": "Show preview only", @@ -3674,18 +3953,23 @@ export const sourceCatalogs = { "s_8143449ded6af56c": "Expand project", "s_81508b76150acb3d": "Leave empty to search PATH", "s_817af552ff22fdff": "Loading failed:", + "s_81b430b696435180": "WeChat", "s_81c9f5ef56c3acfd": "summary title generation local model summary model session title", "s_81ee43758501ff89": "For example, 128000", "s_81f19964ae735b1a": "Advanced features", "s_81fd16a12933fe19": "Edit {{p0}}", + "s_8227ca04cabbb5e8": "You can now message the bot in WeChat. WeChat bots only support private chats.", "s_822fb37dba29381a": "Select file", "s_82395d423099a07a": "Archived", + "s_823b4ba6e569146a": "Copy the Channel ID and Channel secret from the channel's Basic settings in the LINE Developers console and enter them below.", "s_825a80f75113a9d3": "Prompt cache hits: {{p0}} of {{p1}} input tokens ({{p2}}%), {{p3}} written to cache", + "s_82601c48d5d64d71": "Editing code", "s_8268968d4a8759fc": "This model does not support tool-free side chat. Switch models and try again.", "s_82808f881bf7755f": "Team Members ·{{p0}} people", "s_82a043c05e8a9446": "Expert name", "s_82c4aa13b7ca109e": "maximize", "s_82ec71d0714dc013": "Connecting to Windows WebView2.", + "s_8301f8304e5dd793": "Couldn't get a QR code. Retry, or enter the credentials manually", "s_836c3d5d6bc88f78": "Confirm before writing/executing operations", "s_837415ff50de9e5c": "match:", "s_837acaec000d9f38": "Saved. {{p0}} suspected secret(s) were replaced with [REDACTED]; the saved content is shown above.", @@ -3711,17 +3995,20 @@ export const sourceCatalogs = { "s_8526267fcc158a52": "Show earlier messages", "s_85501294b3c40740": "The content exceeds 500,000 characters and editing it here would be slow; copy it out, edit it, then replace it with the clipboard content", "s_8569e005ca1f98f1": "Skill catalog opened", + "s_856dba844b90b484": "Open LINE Official Account Manager", "s_8585b2747da94d76": "Page redirect failed", "s_85c016964ce0bedf": "Directory not found", "s_85c5c1f5567b4d67": "Annotate", "s_85e2f5601c1a0258": "Unattended", "s_8609635074718e14": "Referenced to chat", "s_86353011351e5d0b": "opencode session import failed:", + "s_8652e90f49cd7463": "Calling MCP", "s_86542c6b657c93ab": "Session: {{p0}}", "s_86624f5719c93e79": "Show {{p0}} earlier messages", "s_867698fb7b0c730c": "Project name", "s_867ea0e1328728d9": "mistake:", "s_8696c49ac55b8c71": "Ultra", + "s_86a515d230592181": "Scanning creates and configures the bot automatically. Whoever scans becomes the owner.", "s_86a63f23a076b11e": "Appearance", "s_86a82374a35763d1": "{{p0}} questions pending", "s_86a88f06d31bd3b6": "Reading macOS system notification permissions", @@ -3747,6 +4034,7 @@ export const sourceCatalogs = { "s_88d72ece7cf76737": "Search results", "s_8905355415cd0bd0": "No selected text to quote", "s_89140d4ebd886f12": "Plugin enabled", + "s_891836aa11f5c4d9": "Give instructions in a Telegram private chat or by @mentioning the bot in a group. Results come back to the chat.", "s_89330587b799cc59": "Download unavailable", "s_8949d6721460a7f8": "The model picker in the chat composer uses these profiles.", "s_8968ac45a626bd2a": "session menu", @@ -3758,6 +4046,7 @@ export const sourceCatalogs = { "s_8996cf52b5aacf6a": "Archiving failed:", "s_89c4766afc85125f": "Connection failed", "s_89d3e8892ae8fd51": "More experts", + "s_89db648162e7cea1": "Connection lost, retrying automatically…", "s_89e0ebd9d750f2c1": "The current backend does not support image generation. Update ACECode, restart, and try again.", "s_8a32f1c8f96d5905": "Sandbox backend unavailable: {{p0}}", "s_8a687fb42860ccd0": "Reasoning depth: {{p0}}", @@ -3776,15 +4065,18 @@ export const sourceCatalogs = { "s_8b6ff498515bcc2f": "Timing", "s_8b7cbd4cede975c1": "Import theme", "s_8bcc3e177f5f3b10": "Don't save", + "s_8be0ea36a9bba286": "Connecting…", "s_8c1ae715559b78bd": "Unable to get upgrade progress", "s_8c5c96ae7d403826": "Allowed for the selected directory", "s_8c803ec4ccd237fb": "The site cannot respond correctly right now. Try again later.", "s_8ca30ef95071173d": "Forking to a new session", "s_8cb443ab83797881": "Chart", "s_8cb45d256976500e": "silently allow", + "s_8cbe697b157364a5": "Approve", "s_8cd177ccd9a3ce2a": "File: {{p0}}", "s_8ce03a48ca299d89": "Default open target", "s_8cf0105d35ee8db8": "think", + "s_8d0d535863719b97": "dingtalk stream client id client secret robot im channel messaging connect disconnect", "s_8d2773df13e2ebed": "There are no conversations yet in Gonggri", "s_8d2c69445d32a5b4": "Currently unavailable", "s_8d2e9a70cf226d91": "Search saved models", @@ -3810,11 +4102,14 @@ export const sourceCatalogs = { "s_8e6cd5a3fba2a97c": "Background run setup failed:", "s_8e7847be62b68c2b": "Up one level", "s_8e7aa97f7fda6bc8": "Could not reset memory", + "s_8e86f9b1d54f2c51": "Copy link", "s_8e94c67730c3984e": "Not authorized yet", "s_8eb6432c43427be3": "Known safe command", "s_8ee6730e12551245": "Configuration disk writing failed", "s_8effeb945cdcfbd8": "Hook code", + "s_8f1094e99bf0e30a": "This bot has a webhook set, so ACECode can't receive messages. After it is removed, ACECode takes over and the service that received the webhook stops getting messages.", "s_8f407bb9c116f41d": "interval value", + "s_8f43a2aeaac85a91": "Back in the Feishu Open Platform, under Events & Callbacks choose \"Receive events through persistent connection\" and save (saving only works once this connection is up).", "s_8f46cb4df9c2e6ca": "Only PNG, JPEG, GIF, WebP, BMP, or ICO is supported", "s_8f68def4890f0915": "Recently used expert components", "s_8f6f8d979c981ced": "Copied", @@ -3831,13 +4126,16 @@ export const sourceCatalogs = { "s_90ccdf079052559f": "Delete model {{p0}}", "s_90dae385f5c1bd68": "The side chat connection was interrupted. Try again.", "s_90fd1d973ec921f7": "Computer Use is currently available only on Windows", + "s_9118ed423edf0b11": "Copy the Client ID and Client Secret from Credentials & Basic Info and enter them below.", "s_912be01af6735ac1": "The default permission mode is set to", + "s_913216fda599e7d3": "Expires in {{p0}} min", "s_913cd56da217a9a2": "Pointer style", "s_9147f07072527b70": "Enable model reasoning", "s_914b7a5f9d45d7b9": "The pasted text is uploading; send again when it finishes", "s_9154825fa11a80a2": "Reuse the connection’s API URL and key. Choose image models separately below.", "s_915b988cbfed5442": "Remote Web mode", "s_916a15661d20131c": "Paused goal", + "s_916afcd85322d732": "Change", "s_917b1c1f18d0276b": "Saving...", "s_92172b32227b0aae": "This cannot be undone, and memory summarization will not recreate this memory.", "s_926a50b98ece2667": "Cancel", @@ -3854,6 +4152,7 @@ export const sourceCatalogs = { "s_9344597efd4950cc": "Execution failed", "s_9344fdfb51e2e192": "Areas of expertise", "s_935717bb48835029": "Used while inactive or waiting for the next task", + "s_93828ba27fd646b2": "Give instructions in a DingTalk private chat or by @mentioning the bot in a group. Results come back to the chat.", "s_93a8734cce300531": "Search models", "s_93ac0efe6818ad18": "Citing existing experts, designate a leader; members retain their respective areas of competence.", "s_93b93a43e96a3556": "Directory name:", @@ -3900,6 +4199,7 @@ export const sourceCatalogs = { "s_981a6df8cf54be60": "PNG image", "s_9833401820de5e2b": "Sign in to managed providers here. Their credentials and endpoints never enter the model dialog.", "s_9842842bc84fd474": "Computer Use supports Windows and macOS 14 or later", + "s_98532681701285de": "You can now message the bot privately in LINE. Invite it to a group and @mention it there; the first time needs approval in Settings.", "s_985511d8c078002c": "Answering…", "s_985575198233e251": "Theme export failed", "s_986c56322e8bb553": "Choose a close-window action", @@ -3907,13 +4207,17 @@ export const sourceCatalogs = { "s_9884e90a773101b9": "Global memory", "s_988c12f1398e3d9d": "Commands run by the AI execute inside an isolated sandbox; file, command and network policies follow the rules below.", "s_98cbc356f0e61a08": "Add a provider configuration to manage it here.", + "s_98fc221b10e87c21": "Create app", "s_991bb7cfe5a81550": "Save changes", "s_992b4416c9d23e96": "Reading models…", "s_9966d6b375133f89": "Paths must be absolute: {{p0}}", + "s_99731fe916c6b3bf": "You can now message the robot privately in DingTalk. Add it to a group and @mention it there; the first time needs approval in Settings.", + "s_99845832ed7f16f6": "Later", "s_998568217a58f2fc": "No matching global skills", "s_999bf43be3657ea3": "Allowed by default in a non-interactive embedding", "s_99a4e1e59743908f": "Extensions", "s_99aea2f9131ad6da": "Skills", + "s_99b8595e331d53a0": "Give instructions in a QQ private chat or by @mentioning the bot in a group. Results come back to the chat.", "s_99c32108b160659e": "Choose a complete theme ZIP up to 16 MB", "s_9a1537c4f4d32cef": "Unknown command and the sandbox is unavailable", "s_9a548610ec6b027f": "Code copied", @@ -3935,7 +4239,9 @@ export const sourceCatalogs = { "s_9d028438fbd04725": "Enter an API URL and key, or choose an available model connection", "s_9d0f70c4cee1d679": "Allowed for this session", "s_9d1a1b1c006d85f0": "Unsupported file encoding; read-only preview only", + "s_9d8692671d208b74": "Group member", "s_9d8fb8378821c838": "Cumulative within the last 365 days", + "s_9dbb88d5e18dcea1": "Remove webhook and connect", "s_9dc0825fba5422e4": "Loading...", "s_9dc11d465f7d22c2": "The migration task no longer exists. Start the migration again.", "s_9dcb7682ca6c6910": "{{p0}} LSPs", @@ -3944,6 +4250,7 @@ export const sourceCatalogs = { "s_9e3fc11213b90ed0": "Show in explorer", "s_9e4a69b0e32825ba": "Select task scope", "s_9e66271fb2e5fc90": "Skills and collaboration", + "s_9e6c825ec268ab7b": "Paste the token it replies with below.", "s_9e73b26eb0a378c9": "Nothing to read", "s_9e87fbcaed97c244": "Update cancelled. The current installation was not changed", "s_9e928f35eb8008a4": "Could not load the download size. Try again.", @@ -3966,9 +4273,11 @@ export const sourceCatalogs = { "s_9fc9368cc2c409a3": "No plugins configured", "s_9fd56f7aae1ca61d": "Show hidden items", "s_9fef8ee282a01ab9": "The local model catalog is incomplete. Update ACECode or refresh the catalog.", + "s_9ff545329109ea65": "Owner: {{p0}}", "s_a00235e8c624f16d": "Extension/expert components", "s_a011d73ec679db1a": "Failed to start upgrade:", "s_a071de661efe95fb": "Delete this memory?", + "s_a0a0e5f6072c8213": "weixin wechat qr ilink bot im channel messaging connect disconnect", "s_a0dd1a666530c173": "The default permission mode has been updated and the current session synchronization failed:", "s_a0f73bcccc33cb7b": "Closing the preview panel will close all {{p0}} tabs in the current preview area. Continue?", "s_a1315bcfc62a19f8": "Native bridge returned invalid results", @@ -3976,8 +4285,10 @@ export const sourceCatalogs = { "s_a153567fbff7a035": "workspace dependencies", "s_a184d57717116620": "There is already a bypass question being answered, please wait.", "s_a1a9da9ee6ed0401": "unknown reason", + "s_a1dfbbbaa9f2fef9": "The bot has a webhook set; action needed", "s_a203f86cf6a6a841": "MCP servers", "s_a22774c08bc9187e": "Allow writes", + "s_a253535fd2df2437": "The first person to message the bot privately in the next 10 minutes becomes the owner. Send it a message from your {{p0}} now.", "s_a25f7afbdeabbf5b": "Scale", "s_a26e8186b190f6a1": "Added to session", "s_a27718d789f9cd53": "Connect {{p0}} first", @@ -3993,18 +4304,22 @@ export const sourceCatalogs = { "s_a3030bf8f16dc63c": "Save", "s_a3446a9f995b97ae": "The new path cannot be inside the current workspace", "s_a37778f17c5f3ee5": "Cancelled", + "s_a379593b5b7590f0": "Scan again", "s_a37fe6cce130db08": "Allow multiple instances", "s_a383b3a7e26ba13e": "Updates managed by Microsoft Store", "s_a398cdc4c87f6bb1": "The current daemon does not support persistent appearance settings", "s_a3a33d4898a73327": "Project servers are combined with global servers. Project definitions override matching names.", + "s_a3c4db63188defb7": "On the same page, turn on Message Content Intent.", "s_a46b5607cf04e274": "Choose a decision", "s_a48c70216c141e4a": "What should we do in {{p0}}?", "s_a49f5287fc9ef2d0": "Expand directory", "s_a4c3313deb180960": "three", + "s_a4d14414860b025b": "the bot", "s_a4dbd208ac604795": "Migration failed: {{p0}}", "s_a4e47879fbd494b2": "Detect models", "s_a500bd5efd41f72a": "Current issues", "s_a53a4e7bcadab26f": "Cancel goal", + "s_a5574109f0208e89": "Connect", "s_a55e054ba8777aed": "I understand these memories will be permanently deleted and this cannot be undone", "s_a567bdaa11367f26": "Medium", "s_a56e7d2220e99dc0": "A running session is using this model", @@ -4014,6 +4329,7 @@ export const sourceCatalogs = { "s_a5c89f1001ff103d": "Task terminated:{{p0}}", "s_a60959c3478597ba": "Export PNG image", "s_a621d610084a268b": "Windows WebView2 could not start. Restart ACECode and try again.", + "s_a627f4f2dbd7c83b": "feishu lark app id app secret persistent connection bot im channel messaging connect disconnect", "s_a63280253f17d440": "session", "s_a655bcaa91a25426": "Cancel dispatch: {{p0}}", "s_a67fa4bcb68094b4": "There are no expert components matching the filter criteria", @@ -4022,6 +4338,7 @@ export const sourceCatalogs = { "s_a6dfb80406396953": "The current platform does not block reads, so this list has no effect (macOS / Linux only)", "s_a6e1beaceb5bbafa": "No tasks or projects yet", "s_a7310a28aa28faf4": "Failed to cancel update:", + "s_a73a5fef875e86b2": "cloudflared was not found. Install it with winget install --id Cloudflare.cloudflared and try again, or go back and enter your own public address.", "s_a74010990fc78e7d": "Import locally", "s_a76952e53abfc0d9": "Refresh model", "s_a77ecf353bbece4f": "Upgrade failed:", @@ -4046,6 +4363,7 @@ export const sourceCatalogs = { "s_a9a52fa6f6089c4d": "Could not save. Reopen this form and try again.", "s_a9c7ba0a90821f96": "Disconnect", "s_a9f9acf6c0a4da5e": "{{p0}} conversations deleted", + "s_a9fe4ee81f686689": "Connection failed. See the log for details", "s_aa06c367f0d85408": "Updating", "s_aa1b765fb519fa08": "Authentication failed:", "s_aa1e899dd35771b0": "Orange", @@ -4064,6 +4382,7 @@ export const sourceCatalogs = { "s_ab48bf2aff24b769": "Timed out while starting or stopping the remote Web proxy", "s_ab61b9d1e06f9811": "Create a new terminal (default shell)", "s_ab732547ef013de7": "If approved, this command will run inside the sandbox.", + "s_ab7a910f5dec5480": "A few more steps", "s_ab9084a640fbb864": "Deselect all", "s_abb6edd1093c5cf2": "Task terminated", "s_abe29e9399d0301c": "Failed to load more", @@ -4073,6 +4392,7 @@ export const sourceCatalogs = { "s_ac3f6e014eef361b": "Toggle developer tools", "s_ac4901c4979510a2": "{{p0}} (unavailable)", "s_ac6062580b3feb00": "Suggested task", + "s_ac7246e3c8488218": "{{p0}} request(s) awaiting approval", "s_ac7646d3e0e17e38": "Theme import preview", "s_ac77953a1e064ed6": "Sending failed", "s_ac8e7ffd9e3458ab": "No sandbox is available; unknown commands require approval", @@ -4083,6 +4403,7 @@ export const sourceCatalogs = { "s_ad24308da105ecab": "Could not reach the Grok model service", "s_ad403ac8e643bda1": "Theme deleted; some cached files still need cleanup", "s_ad4c46728555aeb6": "Site not found", + "s_ad5aaf33d6ce9b8a": "Getting a QR code…", "s_ae115efd1b2c2524": "Task aborted", "s_ae13d96782b88e57": "Explore the codebase", "s_ae30451fc65b75de": "No matching providers", @@ -4114,6 +4435,7 @@ export const sourceCatalogs = { "s_b0e60c5b81438dff": "Lead", "s_b0fda488e4430202": "The file is too large to edit in details", "s_b135c840f4ab004d": "Please select a workspace first", + "s_b1806dfd868502c0": "You can now message the bot privately in QQ. To use it in a group, add the bot and @mention it; the first time needs approval in Settings. A bot without personal verification can only be used by you and can only join groups you own.", "s_b193e624476c776d": "Could not get a model reply. Check the settings and retry", "s_b19b09a6ed90c4be": "Effective {{p0}} avatar", "s_b1a521adee6fcd78": "The side chat response was invalid. Try again.", @@ -4124,12 +4446,14 @@ export const sourceCatalogs = { "s_b2749206c4e01861": "Side chat failed", "s_b2888f5f93782bb7": "Failed to load session:", "s_b28e2113fe3a14e6": "All time", + "s_b28f2b46e181e999": "Open the Discord Developer Portal, create an application, and on the Bot page click Reset Token and copy it.", "s_b291a71042529666": "Contains {{p0}} local estimate records, the total may be different from the service provider bill", "s_b2d2f7bccbeeffa1": "Confirming…", "s_b31640e590dd972a": "There are unsaved changes", "s_b33181ed8c62a396": "Delete theme", "s_b3594eb35263f1eb": "Annotation {{p0}}, source text changed", "s_b387dca9c27793f0": "Could not cancel the export. Try again.", + "s_b3bc6035b32575a6": "discord bot token server im channel messaging connect disconnect", "s_b3dd7d131a1959f4": "Tell tasks directly in natural language, or enter / view available commands. After sending ACECode will work within the selected range.", "s_b40cae1f1f4446f0": "What should happen when you close the window? Exiting the app stops all running tasks and scheduled tasks.", "s_b40f2c82dca202be": "The current backend does not isolate the network.", @@ -4164,6 +4488,7 @@ export const sourceCatalogs = { "s_b76fe0327e7aefc8": "Failed to unpin:", "s_b79ce792c1a1287a": "Verifying and installing…", "s_b7f4557b8b746a5e": "Enter an absolute directory path", + "s_b806e39b8e3c4403": "Scan it with your phone's camera (Telegram's own scanner can't read it), then tap Start in Telegram to become the owner.", "s_b817c99bb4f24dd5": "Expand list panel", "s_b8199c9199128706": "Planning and tasks", "s_b858b96d7e9a464e": "Locate in file tree", @@ -4176,8 +4501,10 @@ export const sourceCatalogs = { "s_b91472f6f25b1041": "Enter message or /command…", "s_b91b8a344a77fca6": "Directory path is empty", "s_b9238eb0c9178901": "The theme action failed. Try again.", + "s_b925ae46007cd1f0": "Owner binding QR code", "s_b92b399d2820c415": "Clear saved key", "s_b92d86f4f5570817": "Close other", + "s_b93afa1b4934b706": "Browsing the web", "s_b967309734beff88": "Memory summarization", "s_b97ad406809572e1": "Enable reasoning", "s_b97c0e55e836dee2": "The selected summary model is unavailable. Please select another model", @@ -4199,6 +4526,7 @@ export const sourceCatalogs = { "s_bad0ab59c33beb1d": "Every day {{p0}}", "s_baeb44eebe946b43": "Only letters, digits, underscores and hyphens, up to 64 characters", "s_bb3e66754a5e03e3": "The Agent requests the following actions:", + "s_bb6d995724f49a72": "Manage", "s_bb7cc334f9434dab": "After closing, the content and ability selection filled in this time will not be saved.", "s_bb7ed30127ddfd49": "The export returned an invalid result. Try again.", "s_bb7ef73495dd8a74": "Result", @@ -4229,10 +4557,12 @@ export const sourceCatalogs = { "s_be235f67fed22f01": "Unarchive", "s_be385b84994016d0": "The browser component stopped unexpectedly. Try loading this page again.", "s_be3e095ce6b95549": "Names starting with ( are reserved by the system", + "s_be7bbdf1213e0555": "QQ bot", "s_be98c07b13ac0281": "static", "s_bee1bfab91f18284": "Auto-approved while unattended", "s_bf1617e297b85f92": "Removed from desktop workspace list", "s_bf24e735d802644f": "One per line; shown in cards and details", + "s_bf2a78216b764bb1": "qq bot qr appid appsecret im channel messaging connect disconnect owner approve revoke", "s_bf39e015be237ba0": "The project was created, but incomplete workspace information was returned", "s_bf3a8846479b7fb7": "Type or paste an absolute path, then press Enter", "s_bf3e55705e1de1e8": "Failed to update task completion notifications:", @@ -4252,7 +4582,9 @@ export const sourceCatalogs = { "s_c02c246a6d181676": "Could not read the pasted text", "s_c02fdcf208b3e472": "Delete rule", "s_c03b9deb55408bc4": "Page process stopped", + "s_c043eb82f9332c31": "Opening the connection…", "s_c044ecbe7fe9c678": "Theme-colored arrow only", + "s_c051216ed509001e": "In Telegram, search for @BotFather (look for the blue verified badge) and send /newbot.", "s_c05f7cd6ed3587a2": "Plane", "s_c0866a5a020be7a3": "Delete conversation completely", "s_c087c3d4b210ee82": "Theme deletion returned an invalid result. Refresh and try again.", @@ -4271,6 +4603,7 @@ export const sourceCatalogs = { "s_c27916570d04cf33": "The current workspace is not registered with the background service", "s_c2a497830ef74215": "latest version", "s_c2b732b196ccc7f3": "Conversation has been completely deleted", + "s_c2b794434132af6e": "Get a new QR code", "s_c2e410cd40759fb5": "Describe a task to ACECode, or type / for commands…", "s_c2f0625f0f3d2c18": "Expand tool details", "s_c304f59bf05cfa14": "Desktop startup", @@ -4281,8 +4614,10 @@ export const sourceCatalogs = { "s_c39d1e51a3941a89": "Saving theme package…", "s_c3a73c5eecea42a3": "Expert", "s_c3b8f744ed2c55bf": "JSON format error:", + "s_c3e1b26c451e8221": "Add the event \"Message received v2.0\" (im.message.receive_v1).", "s_c3e5589604083870": "No plugins", "s_c3f989b790daab98": "The backend is temporarily unavailable. Try again later.", + "s_c43f153cb1be5ca5": "Waiting for members", "s_c4ae8fa5a16b5a75": "Skip all tool permission confirmations", "s_c4c5b649cbf25fbb": "Memory and context", "s_c52c7fbed621e8ff": "Annotation {{p0}}", @@ -4343,12 +4678,14 @@ export const sourceCatalogs = { "s_ca6343cddb75f289": "Could not generate a test image. Check the connection and model name.", "s_ca66c2da6f5bf825": "learn more", "s_ca74d439e7712aa5": "The daemon is not running, it has been missed and will not be rerun.", + "s_ca78ab9bac8e9f5b": "Find the bot in Feishu and send it the 6-digit code below in a private chat.", "s_ca949d1eb3377a79": "ACE badge", "s_cadd75ef8275de01": "Viewed {{p0}} images", "s_caecb5d342cc889a": "Resume goal", "s_cafc8a4f6acda2cb": "Tags can be selected multiple times; the same expert will appear under each selected Tag", "s_cb2fcb9099668d50": "No answer yet", "s_cb31eb7a0c2aa250": "Edit model · {{p0}}", + "s_cb51a062db592988": "Another ACECode is connected to this bot. This one takes over automatically after it exits.", "s_cb5d682bac3d1a2d": "Reset", "s_cb62ebd689ee8f20": "current", "s_cb65690b6ee3a1d4": "Unable to get GitHub verification code", @@ -4356,13 +4693,17 @@ export const sourceCatalogs = { "s_cbb5a32c96c3cdee": "Add selected model", "s_cbd7dc0ea6976fd0": "When closing the window", "s_cc0477362c195cf3": "No reusable OpenAI-compatible connections. Choose Configure separately.", + "s_cc574bc5da5d9ac3": "The code is valid for 10 minutes and works once. This step completes by itself after you send it.", "s_cc66c3f576fd5a49": "Full access", "s_cc91b14c8a02a155": "Set {{p0}} as the default model", "s_cce894450069ba5b": "unknown source", "s_cd0832ff9b505231": "Theme package sent to browser downloads", + "s_cd0ce16ee4fd90de": "Copy failed. Select the text and copy it manually", + "s_cd1db6a7d69cb947": "Use ACECode remotely from QQ, WeChat, Feishu, DingTalk, Telegram, Discord and LINE. Credentials stay on this computer. Whether channels stay online after Desktop closes depends on \"Continue running the background process after exiting ACECode\" in Settings > General.", "s_cd32a8c1d823d060": "{{p0}} · Source text changed", "s_cd3b78eb00f7cd12": "Configured hooks will appear here", "s_cd62bee012d94183": "The page component stopped unexpectedly. Reload it.", + "s_cd68324b409f9962": "LINE can only deliver messages to a public address. If you leave the public address empty, ACECode opens a temporary cloudflared tunnel (install it first: winget install --id Cloudflare.cloudflared) and updates LINE's webhook URL on every connect.", "s_cd7395dfd00b67f2": "Only allow writes to {{p0}}", "s_cda33af428d74680": "Matched a sandboxed allow rule", "s_cdae8daae433af05": "Native selector is not available", @@ -4385,8 +4726,10 @@ export const sourceCatalogs = { "s_cf479a0f90887a09": "Could not load theme information. Check your connection and try again.", "s_cf4880f621b9d7bd": "my expert", "s_cf4b1ead545e9bfc": "Wildcards are only allowed in the deny list: {{p0}}", + "s_cf5c7eb43df2410f": "A group", "s_cf6506cb7fc47b9c": "Pasted text · Waiting to upload", "s_cf6783b5388c5684": "Allow (inside sandbox)", + "s_cf8ab6767bf0526f": "The QR code expired and was replaced. Scan the new one", "s_cf911266fb127d62": "Expand sidebar", "s_cf9430e214bde1a8": "A worktree cannot be created from this directory", "s_cfbb88c4ba804b90": "Dumbbell", @@ -4397,6 +4740,7 @@ export const sourceCatalogs = { "s_d04fcbda737fc0c6": "Loading", "s_d059018d896d6cde": "Delete the {{p0}} line", "s_d06e875dde456038": "No blocked paths yet", + "s_d0916de8c1b603bd": "Approved {{p0}}", "s_d0ca08d604551151": "Queue paused", "s_d0f680208ed8796c": "git times out, click to try again", "s_d0fb5bbca8184bdd": "Jump to current question:{{p0}}", @@ -4416,6 +4760,8 @@ export const sourceCatalogs = { "s_d2fc539871184000": "Interjection submitted; interrupting the active turn", "s_d3136b6fa93050df": "From {{p0}} session(s)", "s_d31c72ba37a59575": "No archived conversations yet", + "s_d321ee860efcbb5d": "Use the saved bot, or switch to another one.", + "s_d33995ad669c80cf": "Create channel", "s_d33e249955c846e3": "Stopping the update safely. Please wait.", "s_d35ab580f555345e": "Could not send the stop request. Try again after reconnecting", "s_d381ac2d678d3f8e": "What to do when the close button in the top-right corner is clicked", @@ -4439,6 +4785,7 @@ export const sourceCatalogs = { "s_d538a7ab8d9826bb": "Failed to start import", "s_d53d32f1a1211371": "Custom request headers JSON", "s_d57bc0acdb5f23ab": "Loading diff failed", + "s_d593d21fca298b8f": "WeChat bot", "s_d5d126f6c79d9b11": "All workspaces", "s_d5d360963aa38a66": "Failed to open session:", "s_d5e75e3452a056c7": "Failed to submit interjection:", @@ -4456,6 +4803,7 @@ export const sourceCatalogs = { "s_d69766c856685234": "Beginner's Guide", "s_d6cd7e5f2a8a82cf": "This memory no longer exists. Refresh the list.", "s_d6d20f05ed0f96e0": "Single execution time", + "s_d6e882143a3c3b83": "Scan to create bot", "s_d6f766f2adf53227": "Queuing", "s_d6feded758aed3eb": "Cancel update", "s_d73e140df277dffe": "Model updated", @@ -4463,6 +4811,7 @@ export const sourceCatalogs = { "s_d74679ad02055214": "Hook operation failed:", "s_d74f204139fdd6ab": "Search for expert names, tags or areas of expertise", "s_d7503697e8e7850b": "Retry workshop", + "s_d750c976bfff1be2": "Open Feishu Open Platform", "s_d77674f90811d7af": "Configuration scope", "s_d7875850553752e5": "Failed to switch expert: {{p0}}", "s_d7b2165028a50b95": "Automatically edit and run sandboxed commands; confirm dangerous operations and requests outside the sandbox", @@ -4483,6 +4832,7 @@ export const sourceCatalogs = { "s_d94f9bafff59c06b": "Hooks enabled", "s_d954c8e3f3042985": "Dangerous command", "s_d9686d510cd34aff": "Notifications will enter the notification center silently", + "s_d9894ab44928d667": "Follow the prompts to name it. The username must end in bot.", "s_d9b7af1d4322eb34": "Collapse all diff", "s_d9c35f6c3c4deeaa": "(empty directory)", "s_d9f02c7caa8a847d": "Failed to save custom directive:", @@ -4509,9 +4859,11 @@ export const sourceCatalogs = { "s_db8630211fe3934a": "Native settings are not available", "s_db95142124934467": "Music", "s_dbaa6ba5056580a7": "Outside the currently selectable expert set", + "s_dbcac8ae7d65ae39": "The QR code expired", "s_dbcb25260e3df31a": "A decision record for every command run, file change, sandbox block and rule change; the newest {{p0}} entries are kept.", "s_dbd379b39f0d3537": "Pink", "s_dc2ba467fc7ac962": "Description", + "s_dc3cabe68bcc3046": "Open the DingTalk Open Platform, create an internal app, and add the Robot capability.", "s_dc47f62bd3f882f0": "Build expert team", "s_dc4d662d071eea52": "Created", "s_dc55764fa54e2a48": "MCP configuration scope", @@ -4527,6 +4879,7 @@ export const sourceCatalogs = { "s_dd42f689b17393c2": "Loading earlier messages…", "s_dd7a9008edbca109": "Unarchived {{p0}} sessions; {{p1}} failed", "s_dda206707b809157": "Connection interrupted", + "s_ddcf6e77b0ee1000": "Preparing to", "s_ddde08946298195d": "reduction", "s_ddfe2c796c7eb71b": "Add manually", "s_de03ee139fe2fd9f": "Found", @@ -4538,6 +4891,7 @@ export const sourceCatalogs = { "s_de32e20193ad1042": "Got it", "s_de8184da1ef88d03": "Configured", "s_deb6051740064edd": "Use memory", + "s_dec96df98f052d22": "Open LINE Developers", "s_deeceedbed503b78": "Daily code health inspection", "s_df359a6a54eab051": "Manage lifecycle hooks via configuration and enabled plugins.", "s_df3d58c7d84b85f2": "Settings", @@ -4553,13 +4907,17 @@ export const sourceCatalogs = { "s_e058a2b5c4f81526": "View full text", "s_e05f7e88f89c5f88": ". The theme will be applied after download.", "s_e07802f10db9b6b8": "Find the right expert for your current job", + "s_e0959e358684c224": "{{p0}} (leave empty to keep)", "s_e09b1c37f79f440d": "Allow (outside sandbox)", "s_e0b05fd1040bf6ce": "Could not delete the theme. Try again.", "s_e0b1051909eb179e": "Tasks and workspaces", "s_e0b6ce5e09cd4386": "Server configuration", "s_e0c8243464aa770c": "0 /{{p0}} steps", "s_e0d995508b32cdbb": "{{p0}}% used", + "s_e0e9b9269cbd7678": "Open the Discord Developer Portal", "s_e0ec651f0dae7cf3": "Quote", + "s_e0f62acd5d3cebdd": "Channel data error. See the log for details", + "s_e0fecdf371158416": "QR setup canceled. The existing configuration is unchanged", "s_e122303044304acc": "Save with this name", "s_e17df82098f51602": "Added console logs to chat", "s_e19c3b9e266c69c9": "Browser", @@ -4574,6 +4932,7 @@ export const sourceCatalogs = { "s_e246195c1a39636e": "next round", "s_e2469247115761e9": "Default quality", "s_e26740162fbcb387": "Interject queued message into the active turn", + "s_e2780ee23078ea51": "{{p0}}{{p1}} wants to use ACECode. Review it in Settings > Messaging channels", "s_e2a76ef1f12e147f": "Terminal", "s_e2f12da7faa68217": "Downloaded", "s_e2fda204edef967e": "JSON must be an object", @@ -4629,12 +4988,14 @@ export const sourceCatalogs = { "s_e806fbbe8ec6e82d": "Plugins", "s_e833097b603cb745": "{{p0}} contexts", "s_e84e54a6b6dbbbb9": "Reads and writes are denied for: {{p0}}", + "s_e8ac23cf743aa1f8": "Or approve a private chat request. The first person approved becomes the owner", "s_e8dc871a7fdee23b": "total", "s_e8e08b0f61dd4ddb": "Cancelling…", "s_e90589613fe54246": "Show hidden", "s_e948d90880bb3dba": "JSON is invalid and cannot be switched", "s_e951cbd07b5633c1": "Remove local project", "s_e95f2ba203c9af41": "Wait for running tasks to finish before migrating.", + "s_e99a6717b6917534": "Fix", "s_e99b5859dcf96dc4": "Scroll up to see older questions", "s_e9a1310b7638d211": "Next round dispatch {{p0}}:{{p1}}", "s_e9b00d7f92151b2d": "Dispatch {{p0}}", @@ -4658,16 +5019,19 @@ export const sourceCatalogs = { "s_eb4c406398df4d73": "No workspace usage yet", "s_eb557068ab4833dd": "Unable to connect to Copilot model service", "s_eb55f0980fa90bac": "waiting for your answer", + "s_eb92486a46895884": "You can now DM the bot or @mention it in a server channel. Using it in a channel the first time needs approval in Settings.", "s_eb93045599c0e25c": "Enter Plan mode", "s_ebb240a1930e8bfb": "Search icons", "s_ebb63196c7d3c318": "Failed to copy image:", "s_ec7a11c7061bc164": "Migrating workspace…", "s_ec869826ec67f839": "Some Agent Browser data could not be copied. You may need to sign in again after restarting.", "s_ec9bbc8139d8bdd1": "Specialize", + "s_ecdf5ea03d83b916": "line messaging api channel secret webhook cloudflared bot im channel messaging connect disconnect", "s_ece45f7651c8c206": "The Agent Browser Desktop bridge is unavailable", "s_ed100f18801746c9": "Command rules saved", "s_ed147f03761ab80e": "Add element to chat", "s_ed150cce119ea114": "Git Bash path memorized", + "s_ed16bd1db4b68498": "Waiting for an answer", "s_ed1790d489e671a9": "Inherit global ACECode local tools", "s_ed21620162d871e5": "Loading session records…", "s_ed2f18de3cdc1c5f": "About ACECode", @@ -4676,12 +5040,15 @@ export const sourceCatalogs = { "s_eda73a5daf8f734f": "Current issue:{{p0}}", "s_edb18f09d0ff4c83": "Edit model {{p0}}", "s_edbd4f79f1b7919a": "... +{{p0}} lines collapsed", + "s_ede83cc65dad0522": "Use the saved bot, or scan again to switch to another one.", "s_edecf0ae6e5144f9": "send", "s_ee00e57c07195431": "The current running mode does not support automatic restart. Please exit completely and then restart ACECode.", "s_ee1db622be4f2443": "Close {{p0}}", + "s_ee1e5738be96819a": "Connected {{p0}}", "s_ee32f25f70508f9c": "Clear search", "s_ee34c74d32e7548a": "Recently created", "s_ee529160390df1c4": "Endpoint mode", + "s_ee9ebe523d4205f8": "Connecting…", "s_eea1694c235b5ddf": "every day", "s_eeccc53745f048de": "View {{p0}}\"{{p1}}\"", "s_eeef861b87676f3f": "Globe", @@ -4702,12 +5069,14 @@ export const sourceCatalogs = { "s_f00ef1be4493739b": "Ask every time", "s_f0352e9758fbff2d": "Command forbidden", "s_f035aab05364d2c5": "Select \"{{p0}}\"", + "s_f03b1d0aa80f24ea": "Give instructions in a LINE private chat or by @mentioning the bot in a group. Results come back to the chat.", "s_f04360cb3c9187dc": "{{p0}} file(s) have unsaved changes. Save before continuing?", "s_f07ba1ef037a8e44": "Existing scheduled task", "s_f07bda17207c1e3e": "Read-only file system", "s_f0935665814a3f25": "Failed to copy connection: {{p0}}", "s_f098218a3032a625": "Rejected", "s_f09e3585d4a4e27e": "Model connections", + "s_f0b4569c602a10a0": "Use the saved bot", "s_f121ab742cefcb8e": "Needs attention", "s_f1929d7304960203": "Hook disabled", "s_f1aa0173729f7228": "Remove reference context", @@ -4715,6 +5084,7 @@ export const sourceCatalogs = { "s_f1b112d6acd841b1": "Graduation cap", "s_f1bdeeb9fb4cab81": "Theme verification failed; the theme cannot be exported", "s_f1e0dfb858e7fcfe": "Denied by hook", + "s_f21f25112af89630": "The webhook URL is registered. Turn on Use webhook on the Messaging API page of the LINE Developers console; this changes to connected automatically once it is on.", "s_f27627514a2cb542": "Expert component failed to load", "s_f28461bb49c85647": "Completed", "s_f2902ada282332d2": "Unknown command, ran inside the sandbox", @@ -4722,9 +5092,11 @@ export const sourceCatalogs = { "s_f2b4237b35f0b3b2": "View session", "s_f2bdc88464c51c2e": "Show API key", "s_f2d964d3dc5862d1": "time out:", + "s_f2df9f7ce435d9d6": "The channel data file has a problem. See the log for details.", "s_f2f491398295b1b9": "Lotus", "s_f323fc507d585565": "Minimize to tray", "s_f335420fcda22e54": "This file format does not currently support preview", + "s_f33c31866a8c335e": "Declined {{p0}}", "s_f36f1a90b64f69e5": "Node.js Tools", "s_f37b8e7538b116da": "Expand {{p0}}", "s_f39f410e0b493a67": "skip guide", @@ -4766,6 +5138,7 @@ export const sourceCatalogs = { "s_f628f6c3e43734b2": "Clear filters to view all available components.", "s_f69273dbbebfb3a1": "format", "s_f699d16db6c37a35": "This platform cannot fully cut the network: turning network access off only injects an offline environment (proxy pointed at a dead port, ssh stubs); programs that ignore the proxy can still connect.", + "s_f6dba35520d09e76": "telegram bot token botfather owner im channel messaging connect disconnect approve revoke", "s_f6dc1b1641600dd0": "Importing…", "s_f70f928df89406a7": "Upgrade now", "s_f72684a97677d48a": "Conversation Question Track", @@ -4781,6 +5154,7 @@ export const sourceCatalogs = { "s_f8209b2310bc54a8": "Open context menu", "s_f82c68c72b8f9aca": "Next question", "s_f84c7d6122a7753f": "Usage record", + "s_f8567a884d2336b5": "Revoke access for {{p0}}?", "s_f896a064e0be9e4e": "Parse failed, the whole file is ignored: {{p0}}", "s_f8b6ebf2a9e19bbd": "Image copied", "s_f8c104adf2f77e87": "Please fill in the name of the expert", @@ -4844,6 +5218,7 @@ export const sourceCatalogs = { "s_fcea4baf388f169a": "Run core tests to locate and fix reliably reproducible issues.", "s_fcf8b4bff0df782d": "Open folder", "s_fcffc107c99acbba": "Registered workspaces", + "s_fd351bc95e9337e4": "Sending a message", "s_fd4b9e3b6c685bae": "Continue editing", "s_fd5fd3c4567938c7": "Ask", "s_fd6529f1e944f8c7": "Share files with this conversation. The task will queue while other work is running.", @@ -4864,6 +5239,7 @@ export const sourceCatalogs = { "s_ff199a8f48c79fd8": "Audit log exported", "s_ff20131bcd4725f2": "No rules yet. Choosing \"Always allow\" in a confirmation dialog also writes here automatically.", "s_ff509c9ba052a21c": "Saving…", + "s_ff62ea25d5d6b3a6": "Stopping a member", "s_ff63d45ac706d173": "Continue in a new conversation", "s_ff857f2b6ac947ac": "Last consolidation", "s_ff8b0d734b03b1a4": "Custom models", diff --git a/web/src/lib/api.js b/web/src/lib/api.js index a25f97521..a01ada64f 100644 --- a/web/src/lib/api.js +++ b/web/src/lib/api.js @@ -616,6 +616,10 @@ export function createApi(base = null) { ); }, refreshModelCatalog: () => request('POST', '/api/models/catalog/refresh', {}, base), + getDesktopOffice: ({sessionId = '', workspaceHash = ''} = {}, {signal} = {}) => { + const query = new URLSearchParams({session: sessionId, workspace: workspaceHash}); + return request('GET', `/api/desktop-office?${query}`, undefined, base, {signal}); + }, getSessionModel: (sid, workspaceHash = '') => { const qs = workspaceHash ? `?workspace=${encodeURIComponent(workspaceHash)}` : ''; return request('GET', `/api/sessions/${encodeURIComponent(sid)}/model${qs}`, undefined, base); @@ -709,6 +713,27 @@ export function createApi(base = null) { exportAudit: (query = '') => request('GET', '/api/security/audit/export' + query, undefined, base), clearAudit: () => request('DELETE', '/api/security/audit', undefined, base), setConnectors: (cfg) => request('PUT', '/api/config/connectors', cfg, base), + // 消息通道(Settings > 集成 > 消息通道,openspec add-desktop-im-channels)。只接受本机直连。 + getChannels: () => request('GET', '/api/channels', undefined, base), + setChannelEnabled: (platform, enabled) => + request('POST', `/api/channels/${encodeURIComponent(platform)}/enabled`, { enabled }, base), + // 保存前联网校验(QQ 换取令牌 / Telegram getMe),给足时间。 + setChannelCredentials: (platform, credentials) => + request('PUT', `/api/channels/${encodeURIComponent(platform)}/credentials`, credentials, base, + { timeoutMs: 45000 }), + startChannelBind: (platform) => + request('POST', `/api/channels/${encodeURIComponent(platform)}/bind`, {}, base), + cancelChannelBind: (platform) => + request('DELETE', `/api/channels/${encodeURIComponent(platform)}/bind`, undefined, base), + createChannelOwnerLink: (platform) => + request('POST', `/api/channels/${encodeURIComponent(platform)}/owner-link`, {}, base), + removeTelegramWebhook: () => request('POST', '/api/channels/telegram/remove-webhook', {}, base), + decideChannelRequest: (platform, id, approve) => + request('POST', `/api/channels/${encodeURIComponent(platform)}/requests/${encodeURIComponent(id)}/` + + (approve ? 'approve' : 'reject'), {}, base), + revokeChannelAccess: (platform, principal) => + request('DELETE', `/api/channels/${encodeURIComponent(platform)}/access/${encodeURIComponent(principal)}`, + undefined, base), getUpgradeConfig: () => request('GET', '/api/config/upgrade', undefined, base), setUpgradeConfig: (cfg) => request('PUT', '/api/config/upgrade', cfg, base), getToolchainConfig: () => request('GET', '/api/config/toolchains', undefined, base), diff --git a/web/src/lib/archivedSessions.js b/web/src/lib/archivedSessions.js index 76db4121b..3e067a896 100644 --- a/web/src/lib/archivedSessions.js +++ b/web/src/lib/archivedSessions.js @@ -24,6 +24,7 @@ export function groupArchivedSessions(items, { query = '', workspaceKey = '', sortOrder = 'newest', + workspaceOrder = [], } = {}) { const search = String(query).trim().toLowerCase(); const rows = (Array.isArray(items) ? items : []) @@ -34,19 +35,29 @@ export function groupArchivedSessions(items, { return [sessionDisplayTitle(item, item?.name || ''), workspace.name, workspace.path] .some((value) => value.toLowerCase().includes(search)); }); - rows.sort((a, b) => { - if (a.time === null) return b.time === null ? 0 : 1; - if (b.time === null) return -1; - return sortOrder === 'oldest' ? a.time - b.time : b.time - a.time; - }); const groups = new Map(); - for (const { item, workspace } of rows) { + for (const row of rows) { + const { workspace } = row; if (!groups.has(workspace.key)) { groups.set(workspace.key, { ...workspace, items: [] }); } - groups.get(workspace.key).items.push(item); + groups.get(workspace.key).items.push(row); } - return [...groups.values()]; + const order = Array.isArray(workspaceOrder) ? workspaceOrder : []; + const ranks = new Map(order.map((key, index) => [key, index])); + // The sidebar places no-workspace tasks before workspace folders. + ranks.set('__no_workspace__', -1); + return [...groups.values()] + .sort((a, b) => (ranks.get(a.key) ?? order.length) - (ranks.get(b.key) ?? order.length) + || a.key.localeCompare(b.key)) + .map((group) => ({ + ...group, + items: group.items.sort((a, b) => { + if (a.time === null) return b.time === null ? 0 : 1; + if (b.time === null) return -1; + return sortOrder === 'oldest' ? a.time - b.time : b.time - a.time; + }).map(({ item }) => item), + })); } export function archivedSessionTarget(item = {}) { diff --git a/web/src/lib/archivedSessions.test.js b/web/src/lib/archivedSessions.test.js index c15ff00d9..d169391a4 100644 --- a/web/src/lib/archivedSessions.test.js +++ b/web/src/lib/archivedSessions.test.js @@ -110,12 +110,12 @@ const archive = [ test('archive groups keep same-name workspaces and no-workspace tasks separate', () => { const groups = groupArchivedSessions(archive); - assert.deepEqual(groups.map(({ key }) => key), ['two', 'one', '__no_workspace__']); + assert.deepEqual(groups.map(({ key }) => key), ['__no_workspace__', 'one', 'two']); assert.deepEqual(groups[1].items.map(({ id }) => id), ['middle', 'same']); assert.deepEqual(groups.map(({ items }) => items.length), [1, 2, 1]); - assert.equal(groups[0].name, groups[1].name); - assert.notEqual(groups[0].path, groups[1].path); - assert.equal(groups[2].name, '无工作区'); + assert.equal(groups[1].name, groups[2].name); + assert.notEqual(groups[1].path, groups[2].path); + assert.equal(groups[0].name, '无工作区'); }); test('archive workspace labels support paths, legacy local rows, and explicit no-workspace flags', () => { @@ -127,7 +127,7 @@ test('archive workspace labels support paths, legacy local rows, and explicit no key: '__no_workspace__', name: '无工作区', path: '', }); assert.deepEqual(groupArchivedSessions([{ id: 'local' }, { id: 'task', no_workspace: true }]) - .map(({ key }) => key), ['__local__', '__no_workspace__']); + .map(({ key }) => key), ['__no_workspace__', '__local__']); }); test('archive search combines trimmed case-insensitive titles, workspace names, paths, and workspace filter', () => { @@ -155,13 +155,51 @@ test('archive sorting uses creation fallback, keeps undated rows last, and leave ]); const newest = groupArchivedSessions(items); const oldest = groupArchivedSessions(items, { sortOrder: 'oldest' }); - assert.deepEqual(newest.map(({ key }) => key), ['two', 'one', '__no_workspace__']); - assert.deepEqual(oldest.map(({ key }) => key), ['one', '__no_workspace__', 'two']); + assert.deepEqual(newest.map(({ key }) => key), ['__no_workspace__', 'one', 'two']); + assert.deepEqual(oldest.map(({ key }) => key), ['__no_workspace__', 'one', 'two']); assert.deepEqual(newest[1].items.map(({ id }) => id), ['middle', 'fallback', 'same', 'unknown']); - assert.deepEqual(oldest[0].items.map(({ id }) => id), ['same', 'fallback', 'middle', 'unknown']); + assert.deepEqual(oldest[1].items.map(({ id }) => id), ['same', 'fallback', 'middle', 'unknown']); assert.equal(items[0].id, 'unknown'); }); +test('sidebar workspace order is independent of session times and sort direction', () => { + for (const workspaceOrder of [Object.freeze(['one', 'two']), Object.freeze(['two', 'one'])]) { + for (const sortOrder of ['newest', 'oldest']) { + const groups = groupArchivedSessions([...archive].reverse(), { workspaceOrder, sortOrder }); + assert.deepEqual(groups.map(({ key }) => key), ['__no_workspace__', ...workspaceOrder]); + assert.deepEqual(groups.find(({ key }) => key === 'one').items.map(({ id }) => id), + sortOrder === 'oldest' ? ['same', 'middle'] : ['middle', 'same']); + } + } +}); + +test('search and archive removal preserve workspace order without empty groups', () => { + const options = { workspaceOrder: ['two', 'empty', 'one'] }; + assert.deepEqual(groupArchivedSessions(archive, { ...options, query: 'draft', sortOrder: 'oldest' }) + .map(({ key }) => key), ['__no_workspace__', 'two', 'one']); + assert.deepEqual(groupArchivedSessions(archive, { ...options, query: 'project' }) + .map(({ key }) => key), ['two', 'one']); + const remaining = removeArchivedSessionsByKey(archive, [archivedSessionKey(archive[2])]); + assert.deepEqual(groupArchivedSessions(remaining, options).map(({ key }) => key), + ['__no_workspace__', 'two', 'one']); + assert.deepEqual(groupArchivedSessions(remaining, { ...options, workspaceKey: 'one' })[0].items, + [archive[0]]); + assert.deepEqual(groupArchivedSessions(remaining, { ...options, workspaceKey: 'empty' }), []); +}); + +test('unlisted and local archive groups have stable order after listed workspaces', () => { + const items = [{ id: 'local', updated_at: '2030-01-01' }, ...archive]; + for (const sortOrder of ['newest', 'oldest']) { + assert.deepEqual(groupArchivedSessions(items, { workspaceOrder: ['two'], sortOrder }) + .map(({ key }) => key), ['__no_workspace__', 'two', '__local__', 'one']); + const reversed = [...items].reverse(); + assert.deepEqual(groupArchivedSessions(reversed, { workspaceOrder: ['two'], sortOrder }) + .map(({ key }) => key), ['__no_workspace__', 'two', '__local__', 'one']); + assert.deepEqual(groupArchivedSessions(reversed, { workspaceOrder: null, sortOrder }) + .map(({ key }) => key), ['__no_workspace__', '__local__', 'one', 'two']); + } +}); + test('filtered selection excludes hidden sessions and preserves visible selection through sorting and failures', () => { const selected = toggleAllArchivedSessionSelection(archive, new Set()); const filtered = groupArchivedSessions(archive, { workspaceKey: 'one' }).flatMap(({ items }) => items); diff --git a/web/src/lib/channelsSettings.js b/web/src/lib/channelsSettings.js new file mode 100644 index 000000000..c8b261682 --- /dev/null +++ b/web/src/lib/channelsSettings.js @@ -0,0 +1,673 @@ +// 消息通道设置页(设置 > 集成 > 消息通道,openspec add-desktop-im-channels)的纯逻辑: +// 快照规整、卡片(连接 / 取消连接 + 一行简要状态)、分步连接向导各步的视图、扫码流程视图、 +// WS 事件合并、二维码路径、会话列表的 channel_bound 判定。组件 components/ChannelsSettings.jsx +// 只负责渲染与请求。页面刻意只给简要状态:原因、隐私模式、暂存待补发、收发计数等细节由 daemon +// 写进日志(src/host/channels/core/state_log.cpp),不要再加回页面。 +import qrcode from 'qrcode-generator'; + +// 与 daemon 的平台描述表(src/host/channels/core/platforms.cpp)同序。 +export const CHANNEL_PLATFORMS = ['qq', 'weixin', 'feishu', 'dingtalk', 'telegram', 'discord', 'line']; + +// 有扫码绑定流程的平台。 +export const SCAN_PLATFORMS = ['qq', 'weixin']; + +function text(value) { + return value == null ? '' : String(value); +} + +function object(value) { + return value && typeof value === 'object' && !Array.isArray(value) ? value : {}; +} + +function array(value) { + return Array.isArray(value) ? value : []; +} + +function count(value) { + const n = Number(value); + return Number.isFinite(n) && n > 0 ? Math.floor(n) : 0; +} + +// 平台短名(侧栏提示“已绑定到 …”、请求通知用)。写成函数:文案在调用时求值,跟随界面语言。 +export function channelPlatformTitle(platform) { + switch (platform) { + case 'qq': return 'QQ'; + case 'weixin': return '微信'; + case 'feishu': return '飞书'; + case 'dingtalk': return '钉钉'; + case 'telegram': return 'Telegram'; + case 'discord': return 'Discord'; + case 'line': return 'LINE'; + default: return text(platform); + } +} + +export function isChannelPlatform(platform) { + return CHANNEL_PLATFORMS.includes(platform); +} + +export function emptyChannelPlatform(platform) { + return { + platform, + configured: false, + enabled: false, + state: 'unconfigured', + detail: '', + retryStopped: false, + account: '', + displayName: '', + extra: {}, + credentialHint: '', + credentialsPublic: {}, + appId: '', + owner: '', + ownerWindow: false, + hostedByPid: 0, + contacts: [], + pending: [], + bindings: [], + }; +} + +export function normalizeChannelPlatform(raw) { + const source = object(raw); + const platform = text(source.platform); + const base = emptyChannelPlatform(platform); + return { + ...base, + configured: source.configured === true, + enabled: source.enabled === true, + state: text(source.state) || base.state, + detail: text(source.detail), + retryStopped: source.retry_stopped === true, + account: text(source.account), + displayName: text(source.display_name), + extra: object(source.extra), + credentialHint: text(source.credential_hint), + credentialsPublic: object(source.credentials_public), + appId: text(source.app_id), + owner: text(source.owner), + ownerWindow: source.owner_window === true, + hostedByPid: count(source.hosted_by_pid), + contacts: array(source.contacts).map((item) => { + const contact = object(item); + return { + principal: text(contact.principal), + kind: text(contact.kind), + name: text(contact.name), + owner: contact.owner === true, + }; + }).filter((contact) => contact.principal), + pending: array(source.pending).map((item) => { + const request = object(item); + return { + id: text(request.id), + kind: text(request.kind), + name: text(request.name), + principal: text(request.principal), + label: text(request.label), + expiresInS: count(request.expires_in_s), + }; + }).filter((request) => request.id), + bindings: array(source.bindings).map((item) => { + const binding = object(item); + return { + key: text(binding.key), + label: text(binding.label), + chat: text(binding.chat), + sender: text(binding.sender), + sessionId: text(binding.session_id), + noWorkspace: binding.no_workspace !== false, + cwd: text(binding.cwd), + workspaceHash: text(binding.workspace_hash), + sent: count(binding.sent), + failed: count(binding.failed), + dropped: count(binding.dropped), + }; + }).filter((binding) => binding.key && binding.sessionId), + }; +} + +// 扫码绑定流程的状态(QQ、微信)。 +export function normalizeBind(raw) { + const source = object(raw); + return { + platform: text(source.platform), + phase: text(source.phase) || 'idle', + qrUrl: text(source.qr_url), + refreshes: count(source.refreshes), + error: text(source.error), + account: text(source.account), + ownerBound: source.owner_bound === true, + }; +} + +export function normalizeChannelsSnapshot(raw) { + const source = object(raw); + const platforms = {}; + for (const platform of CHANNEL_PLATFORMS) platforms[platform] = emptyChannelPlatform(platform); + for (const item of array(source.platforms)) { + const normalized = normalizeChannelPlatform(item); + if (isChannelPlatform(normalized.platform)) platforms[normalized.platform] = normalized; + } + const rawBinds = object(source.binds); + const binds = {}; + for (const platform of SCAN_PLATFORMS) binds[platform] = normalizeBind({ platform, ...object(rawBinds[platform]) }); + return { platforms, binds }; +} + +// WS 事件合并:channels_state 替换单个平台快照,channels_bind 更新该平台的扫码状态; +// 无关事件原样返回同一个对象(调用方可据此跳过重渲染)。 +export function applyChannelsEvent(state, envelope) { + const message = object(envelope); + const payload = object(message.payload); + if (message.type === 'channels_state') { + const platform = normalizeChannelPlatform(payload); + if (!isChannelPlatform(platform.platform)) return state; + return { ...state, platforms: { ...state.platforms, [platform.platform]: platform } }; + } + if (message.type === 'channels_bind' && SCAN_PLATFORMS.includes(payload.platform)) { + return { ...state, binds: { ...object(state.binds), [payload.platform]: normalizeBind(payload) } }; + } + return state; +} + +// 平台卡片的固定内容。图标统一用单色图标,默认是聊天图标(用户拍板:不用彩色品牌 logo)。 +// 写成函数:文案在渲染时求值,切换界面语言后跟着变。 +export function channelCards() { + const card = (platform, title, description) => ({ platform, title, icon: 'chat', description }); + return [ + card('qq', 'QQ 机器人', '在 QQ 私聊或群里 @机器人 下达指令,结果回到聊天窗口。'), + card('weixin', '微信', '在微信里私聊机器人下达指令,结果回到聊天窗口。'), + card('feishu', '飞书', '在飞书私聊或群里 @机器人 下达指令,结果回到聊天窗口。'), + card('dingtalk', '钉钉', '在钉钉私聊或群里 @机器人 下达指令,结果回到聊天窗口。'), + card('telegram', 'Telegram', '在 Telegram 私聊或群里 @机器人 下达指令,结果回到聊天窗口。'), + card('discord', 'Discord', '私信机器人或在服务器频道里 @它 下达指令,结果回到频道。'), + card('line', 'LINE', '在 LINE 私聊或群里 @机器人 下达指令,结果回到聊天窗口。'), + ]; +} + +const DISCORD_PERMISSIONS = '274878008320'; // 查看频道、发消息、读历史、附件、在子区发消息 + +export function discordInviteUrl(applicationId) { + const id = text(applicationId); + return id ? `https://discord.com/oauth2/authorize?client_id=${id}&scope=bot&permissions=${DISCORD_PERMISSIONS}` : ''; +} + +// 每个平台的连接向导:三步的名称、第一步(扫码或填写凭据)的说明与字段、连上后要回平台后台 +// 完成的事、机主怎么产生(scan:QQ 扫码人 / scanner:微信扫码人 / link:Telegram 链接 / +// code:6 位绑定码)、完成时的提示。 +export function platformSetup(platform) { + const field = (key, label, extra = {}) => ({ key, label, placeholder: label, secret: false, ...extra }); + switch (platform) { + case 'qq': + return { + steps: ['扫码创建机器人', '连接', '绑定机主'], + credential: { + kind: 'scan', + scanHint: '扫码后会自动创建机器人并完成配置,扫码人成为机主。', + manualIntro: '填写 QQ 开放平台里机器人的 AppID 与 AppSecret,保存前会先验证。', + fields: [field('app_id', 'AppID'), field('app_secret', 'AppSecret', { secret: true })], + }, + connectChecklist: [], + owner: 'scan', + doneHint: '现在可以在 QQ 里私聊机器人了。把机器人拉进群后 @ 它,第一次需要在设置页批准。未经个人认证的机器人只能由你本人使用,也只能加入你担任群主的群。', + }; + case 'weixin': + return { + steps: ['扫码登录', '连接', '完成'], + credential: { + kind: 'scan', + scanHint: '用微信扫一扫并在手机上确认;扫码的微信号就是机主,也是唯一能使用它的人。', + manualIntro: '', + fields: [], + }, + connectChecklist: [], + owner: 'scanner', + doneHint: '现在可以在微信里给机器人发消息了。微信机器人只支持私聊。', + }; + case 'feishu': + return { + steps: ['创建应用', '连接', '绑定机主'], + credential: { + kind: 'form', + instructions: [ + '打开飞书开放平台,创建「企业自建应用」,在「添加应用能力」里添加「机器人」。', + '在「权限管理」开通:im:message、im:message:send_as_bot、im:message.p2p_msg:readonly、im:message.group_at_msg:readonly、im:resource。', + '在「凭证与基础信息」复制 App ID 与 App Secret,填到下面。', + ], + links: [{ label: '打开飞书开放平台', href: 'https://open.feishu.cn/app' }], + fields: [ + field('app_id', 'App ID', { placeholder: 'App ID(cli_ 开头)' }), + field('app_secret', 'App Secret', { secret: true }), + field('domain', '版本', { + kind: 'select', + options: [{ value: 'feishu', label: '飞书(中国)' }, { value: 'lark', label: 'Lark(海外)' }], + defaultValue: 'feishu', + }), + ], + }, + connectChecklist: [ + '回到飞书开放平台的「事件与回调」,订阅方式选「使用长连接接收事件」并保存(这里连上之后才能保存)。', + '添加事件「接收消息 v2.0」(im.message.receive_v1)。', + '在「版本管理与发布」创建版本并发布,可用范围要包含你自己。', + ], + owner: 'code', + ownerHint: '在飞书里搜索这个机器人,私聊它发送下面的 6 位数字。', + doneHint: '现在可以在飞书里私聊机器人了。把机器人拉进群后 @ 它,第一次需要在设置页批准。', + }; + case 'dingtalk': + return { + steps: ['创建应用', '连接', '绑定机主'], + credential: { + kind: 'form', + instructions: [ + '打开钉钉开放平台,创建「企业内部应用」,在「应用能力」里添加「机器人」。', + '机器人的消息接收模式选「Stream 模式」,在「权限管理」开通企业内机器人发送消息权限,然后发布应用。', + '在「凭证与基础信息」复制 Client ID 与 Client Secret,填到下面。', + ], + links: [{ label: '打开钉钉开放平台', href: 'https://open-dev.dingtalk.com/' }], + fields: [field('client_id', 'Client ID'), field('client_secret', 'Client Secret', { secret: true })], + }, + connectChecklist: [], + owner: 'code', + ownerHint: '在钉钉里找到这个机器人,私聊它发送下面的 6 位数字。', + doneHint: '现在可以在钉钉里私聊机器人了。把机器人拉进群后 @ 它,第一次需要在设置页批准。', + }; + case 'telegram': + return { + steps: ['创建机器人', '连接', '绑定机主'], + credential: { + kind: 'form', + instructions: [ + '在 Telegram 里搜索 @BotFather(认准蓝色认证标记),发送 /newbot。', + '按提示起名字,用户名要以 bot 结尾。', + '把它回复的 Token 粘贴到下面。', + ], + links: [], + fields: [field('token', 'Bot Token', { secret: true })], + }, + connectChecklist: [], + owner: 'link', + doneHint: '把机器人拉进群后 @ 它,第一次需要在设置页批准。', + }; + case 'discord': + return { + steps: ['创建机器人', '连接', '绑定机主'], + credential: { + kind: 'form', + instructions: [ + '打开 Discord 开发者后台,新建应用,在「Bot」页点「Reset Token」并复制。', + '在同一页打开「Message Content Intent」。', + '把 Token 粘贴到下面;验证通过后用邀请链接把机器人拉进你的服务器。', + ], + links: [{ label: '打开 Discord 开发者后台', href: 'https://discord.com/developers/applications' }], + fields: [field('token', 'Bot Token', { secret: true })], + }, + connectChecklist: [], + owner: 'code', + ownerHint: '在 Discord 里私信这个机器人(需要和它在同一个服务器),发送下面的 6 位数字。', + doneHint: '现在可以私信机器人,或在服务器频道里 @ 它。第一次在频道里使用需要在设置页批准。', + }; + case 'line': + return { + steps: ['创建频道', '连接', '绑定机主'], + credential: { + kind: 'form', + instructions: [ + '在 LINE Official Account Manager 建立官方账号,并在「设定 > Messaging API」启用 Messaging API。', + '在 LINE Developers 控制台的频道「Basic settings」复制 Channel ID 与 Channel secret,填到下面。', + '在「Messaging API」页打开 Use webhook;在官方账号的回应设定里关闭自动回应讯息与加入好友的欢迎讯息。', + ], + note: 'LINE 只能通过公网地址回调。公网地址留空时,ACECode 会用 cloudflared 自动建立临时隧道(先安装:winget install --id Cloudflare.cloudflared),每次连接都会自动更新 LINE 的回调地址。', + links: [ + { label: '打开 LINE Developers', href: 'https://developers.line.biz/console/' }, + { label: '打开 LINE Official Account Manager', href: 'https://manager.line.biz/' }, + ], + fields: [ + field('channel_id', 'Channel ID'), + field('channel_secret', 'Channel secret', { secret: true }), + field('public_url', '公网地址', { placeholder: '公网地址(可选,留空则自动建立隧道)', clearable: true }), + ], + }, + connectChecklist: [], + owner: 'code', + ownerHint: '在 LINE 里把官方账号加为好友,私聊它发送下面的 6 位数字。', + doneHint: '现在可以在 LINE 里私聊机器人了。把机器人邀请进群后 @ 它,第一次需要在设置页批准。', + }; + default: + return { + steps: ['配置', '连接', '绑定机主'], + credential: { kind: 'form', instructions: [], links: [], fields: [] }, + connectChecklist: [], + owner: 'code', + ownerHint: '', + doneHint: '', + }; + } +} + +export function wizardSteps(platform) { + return platformSetup(platform).steps; +} + +// 已保存的机器人在向导与管理对话框里的一行摘要(只用非密钥字段与密钥尾号)。 +export function savedBotSummary(platformKey, platform) { + const item = platform || emptyChannelPlatform(platformKey); + const fields = object(item.credentialsPublic); + const value = (key) => text(fields[key]); + switch (platformKey) { + case 'qq': + return { title: `AppID ${item.appId || value('app_id') || '未知'}`, desc: `AppSecret ${item.credentialHint || '已保存'}` }; + case 'weixin': + return { title: item.displayName || '微信机器人', desc: value('bot_id') ? `机器人 ${value('bot_id')}` : '' }; + case 'feishu': + return { title: item.displayName || `App ID ${value('app_id')}`, desc: `App Secret ${value('app_secret') || '已保存'}` }; + case 'dingtalk': + return { title: item.displayName || `Client ID ${value('client_id')}`, desc: `Client Secret ${value('client_secret') || '已保存'}` }; + case 'line': + return { title: item.displayName || `Channel ${value('channel_id')}`, desc: `Channel secret ${value('channel_secret') || '已保存'}` }; + default: + return { title: item.displayName || `${channelPlatformTitle(platformKey)} 机器人`, desc: `Token ${item.credentialHint || '已保存'}` }; + } +} + +// 卡片:按钮(连接 / 取消连接)与一行简要状态,tone 取 ok / warn / danger / mute。 +// 失败只说“详情见日志”,并给出 action:'fix' 让卡片显示「处理」入口(打开向导的连接步)。 +export function channelCardView(platform, busy = false) { + const item = platform || emptyChannelPlatform(''); + const button = item.enabled + ? { action: 'disconnect', label: '取消连接', disabled: Boolean(busy) } + : { action: 'connect', label: '连接', disabled: Boolean(busy) || item.state === 'error' }; + return { button, status: cardStatus(item) }; +} + +function cardStatus(item) { + if (item.state === 'error') return { tone: 'danger', text: '通道数据异常,详情见日志' }; + if (!item.enabled) return null; + switch (item.state) { + case 'connected': + return { tone: 'ok', text: item.displayName ? `已连接 · ${item.displayName}` : '已连接' }; + case 'connecting': + return { tone: 'warn', text: '连接中…' }; + case 'retrying': + return { tone: 'warn', text: '连接中断,正在自动重试' }; + case 'failed': + return telegramWebhookBlocked(item) + ? { tone: 'warn', text: '机器人设置了 webhook,需要处理', action: 'fix' } + : { tone: 'danger', text: '连接失败,详情见日志', action: 'fix' }; + case 'standby': + return { tone: 'mute', text: '已由另一个 ACECode 连接' }; + default: + return { tone: 'mute', text: '未连接' }; + } +} + +// 点「连接」:还没配好(没有凭据,或还没有机主)时打开分步向导,已经配好的直接连接。 +export function connectNeedsWizard(platform) { + const item = platform || emptyChannelPlatform(''); + return !item.configured || !item.owner || scanLoginExpired(item); +} + +// 微信的登录凭据会在腾讯侧失效:平台停止重试后只能重新扫码,重连旧凭据没有意义。 +function scanLoginExpired(item) { + return item.platform === 'weixin' && item.state === 'failed' && item.retryStopped; +} + +// 向导从哪一步开始:没有凭据(或微信登录已失效)从第 1 步,有凭据从第 2 步(连接)。 +export function initialWizardStep(platform) { + if (!platform?.configured || scanLoginExpired(platform)) return 0; + return 1; +} + +// 向导第 2 步(连接)的视图:phase 决定能做什么,text 是给用户看的一句话。 +export function connectStepView(platform) { + const item = platform || emptyChannelPlatform(''); + if (item.state === 'error') return { phase: 'failed', tone: 'danger', text: '通道数据文件异常,详情见日志。' }; + if (item.state === 'connected') { + return { phase: 'connected', tone: 'ok', text: item.displayName ? `已连接 ${item.displayName}` : '已连接' }; + } + if (!item.enabled) return { phase: 'idle', tone: 'mute', text: '正在打开连接…' }; + const lineHint = lineConnectHint(item); + if (lineHint) return lineHint; + switch (item.state) { + case 'failed': + if (telegramWebhookBlocked(item)) { + return { + phase: 'webhook', + tone: 'warn', + text: '这个机器人设置了 webhook,ACECode 收不到消息。移除后由 ACECode 接管,原来接收 webhook 的服务会停止收到消息。', + }; + } + return { phase: 'failed', tone: 'danger', text: '连接失败,原因已写入日志。确认凭据无误后重试,或回到上一步更换。' }; + case 'retrying': + return { phase: 'connecting', tone: 'warn', text: '连接中断,正在自动重试…' }; + case 'standby': + return { phase: 'standby', tone: 'mute', text: '这个机器人正由另一个 ACECode 连接,它退出后这里会自动接管。' }; + default: + return { phase: 'connecting', tone: 'mute', text: '正在连接…' }; + } +} + +// LINE 连接里只有用户能做的几步:装 cloudflared、在控制台打开 Use webhook、把自己的公网地址 +// 转发到本机回调端口。其余失败原因照常只写日志。 +function lineConnectHint(item) { + if (item.platform !== 'line') return null; + const extra = item.extra || {}; + if (item.state === 'failed' && extra.cloudflared_missing === true) { + return { + phase: 'failed', + tone: 'danger', + text: '没有找到 cloudflared。先运行 winget install --id Cloudflare.cloudflared 安装后重试,或回到上一步填写自己的公网地址。', + }; + } + if (item.state !== 'retrying') return null; + if (extra.webhook_active === false) { + return { + phase: 'connecting', + tone: 'warn', + text: '回调地址已登记。请在 LINE Developers 控制台的 Messaging API 页打开 Use webhook,打开后这里会自动变为已连接。', + }; + } + const port = Number(extra.listen_port) || 0; + if (extra.tunnel === 'custom' && port > 0 && !text(extra.webhook_url)) { + return { + phase: 'connecting', + tone: 'warn', + text: `请把你的公网地址转发到本机 http://127.0.0.1:${port},连通后会自动继续。`, + }; + } + return null; +} + +export function ownerTitle(platform) { + const owner = text(platform?.owner); + if (!owner) return ''; + const contact = array(platform?.contacts).find((item) => item.principal === owner); + return contact?.name || principalLabel(owner); +} + +// 向导第 3 步(绑定机主)。Telegram 用一次性链接;飞书、钉钉、Discord、LINE 用 6 位绑定码; +// QQ 扫码时通常已带出机主,没带出时靠 10 分钟窗口,窗口过了就批准对方的私聊请求 +// (批准第一个私聊请求即成为机主);微信的扫码人就是机主。 +export function ownerStepView(platform, platformKey) { + const item = platform || emptyChannelPlatform(platformKey); + if (item.owner) return { done: true, mode: 'done', text: `机主:${ownerTitle(item)}` }; + const owner = platformSetup(platformKey).owner; + if (owner === 'link') return { done: false, mode: 'link', text: '' }; + if (owner === 'code') return { done: false, mode: 'code', text: platformSetup(platformKey).ownerHint }; + const title = channelPlatformTitle(platformKey); + if (item.ownerWindow) { + return { done: false, mode: 'window', text: `接下来 10 分钟内第一个私聊机器人的人成为机主,现在就用你的 ${title} 私聊它一句。` }; + } + return { done: false, mode: 'request', text: `用你的 ${title} 私聊机器人一句,下面会出现请求,批准后你就是机主。` }; +} + +// 扫码流程视图(QQ、微信)。 +export function bindView(bind, platformKey = 'qq') { + const state = bind || normalizeBind(null); + const active = state.phase === 'starting' || state.phase === 'waiting'; + switch (state.phase) { + case 'starting': + return { active, showQr: false, tone: 'mute', message: '正在获取二维码…' }; + case 'waiting': + return { + active, + showQr: Boolean(state.qrUrl), + tone: 'mute', + message: state.refreshes > 0 + ? '二维码过期后已自动换新,请扫描新的二维码' + : platformKey === 'weixin' + ? '用微信扫一扫,并在手机上确认' + : '用手机 QQ 扫一扫,按提示创建并授权机器人', + }; + case 'completed': + return { active, showQr: false, tone: 'ok', message: state.ownerBound ? '扫码成功,你已是机主' : '扫码成功' }; + case 'failed': + return { + active, + showQr: false, + tone: 'danger', + message: state.error || (platformKey === 'weixin' ? '获取二维码失败,请重试' : '获取二维码失败,可以重试,或改为手动填写'), + }; + case 'cancelled': + return { active, showQr: false, tone: 'mute', message: '已取消扫码,原有配置未改动' }; + case 'timed_out': + return { active, showQr: false, tone: 'danger', message: '二维码已过期' }; + default: + return { active: false, showQr: false, tone: 'mute', message: '' }; + } +} + +export function principalLabel(principal) { + const value = text(principal); + if (value.startsWith('member:')) { + const rest = value.slice('member:'.length); + const split = rest.indexOf(':'); + if (split > 0) return `群 ${rest.slice(0, split)} 的成员 ${rest.slice(split + 1)}`; + } + if (value.startsWith('group:')) return `群 ${value.slice('group:'.length)}`; + if (value.startsWith('user:')) return `用户 ${value.slice('user:'.length)}`; + return value; +} + +export function principalKindLabel(kind) { + if (kind === 'group') return '群'; + if (kind === 'member') return '群成员'; + return '私聊'; +} + +export function contactRows(platform) { + return array(platform?.contacts).map((contact) => ({ + ...contact, + title: contact.name || principalLabel(contact.principal), + subtitle: contact.name ? principalLabel(contact.principal) : '', + kindLabel: contact.owner ? '机主' : principalKindLabel(contact.kind), + })); +} + +export function pendingRows(platform) { + return array(platform?.pending).map((request) => { + const minutes = Math.max(1, Math.ceil(request.expiresInS / 60)); + return { + ...request, + title: request.name || principalLabel(request.principal), + subtitle: [principalKindLabel(request.kind), request.label, `${minutes} 分钟后过期`] + .filter(Boolean).join(' · '), + }; + }); +} + +export function telegramWebhookBlocked(platform) { + return object(platform?.extra).webhook === true; +} + +// 平台第一步要填写的字段(QQ 是扫码之外的手动填写)。 +export function credentialFields(platform) { + return platformSetup(platform).credential.fields; +} + +// 保存凭据的请求体:去两端空白,空字段省略(表示沿用已保存的值);标了 clearable 的可选字段 +// 留空表示清除(如 LINE 公网地址改回自动隧道)。没有任何内容时返回 null。 +export function credentialsPayload(platform, draft) { + const source = object(draft); + const body = {}; + let filled = false; + for (const field of credentialFields(platform)) { + const value = text(source[field.key] ?? field.defaultValue).trim(); + if (value) { + body[field.key] = value; + if (field.kind !== 'select') filled = true; + } else if (field.clearable) { + body[field.key] = ''; + } + } + return filled || Object.keys(body).some((key) => body[key] === '') ? body : null; +} + +// 输入框占位:已保存的密钥给尾号并提示留空不改,已保存的普通字段给原值,否则给字段名。 +export function credentialPlaceholder(platform, field) { + const saved = text(object(platform?.credentialsPublic)[field.key]); + if (saved) return field.secret ? `已保存 ${saved},留空则不修改` : `${saved}(留空则不修改)`; + return field.placeholder || field.label; +} + +// 二维码:返回带 4 格静区的边长与 SVG 路径(按行合并相邻深色模块)。 +export function qrSvgModel(content, errorCorrection = 'M') { + const value = text(content); + if (!value) return null; + const code = qrcode(0, errorCorrection); + code.addData(value); + code.make(); + const modules = code.getModuleCount(); + const quiet = 4; + const parts = []; + for (let row = 0; row < modules; row += 1) { + let col = 0; + while (col < modules) { + if (!code.isDark(row, col)) { + col += 1; + continue; + } + let end = col; + while (end < modules && code.isDark(row, end)) end += 1; + parts.push(`M${col + quiet} ${row + quiet}h${end - col}v1h-${end - col}z`); + col = end; + } + } + return { size: modules + quiet * 2, modules, path: parts.join('') }; +} + +// 会话列表:被 IM 会话绑定的会话带 channel_bound:{platform};返回平台名或空串。 +export function channelBoundPlatform(session) { + const bound = object(session?.channel_bound ?? session?.channelBound); + const platform = text(bound.platform); + return isChannelPlatform(platform) ? platform : ''; +} + +export function channelBoundLabel(platform) { + return platform ? `已绑定到 ${channelPlatformTitle(platform)}` : ''; +} + +// 所有平台绑定的会话 id 集合(排序后的字符串,便于比较是否变化)。 +export function channelBindingSignature(state) { + const ids = []; + for (const platform of CHANNEL_PLATFORMS) { + for (const binding of array(state?.platforms?.[platform]?.bindings)) ids.push(`${platform}:${binding.sessionId}`); + } + return ids.sort().join('|'); +} + +// 单个 channels_state 事件里的绑定签名;侧栏据此判断要不要立即刷新。 +export function channelStateBindingSignature(payload) { + const platform = normalizeChannelPlatform(payload); + if (!isChannelPlatform(platform.platform)) return null; + return platform.bindings.map((binding) => binding.sessionId).sort().join('|'); +} + +// 接口错误的展示文案:优先用后端给的中文 message(不含凭据);远程客户端统一提示本机设置。 +export function channelErrorText(error) { + const body = object(error?.body); + if (error?.code === 'LOCAL_ONLY' || body.error === 'LOCAL_ONLY') return '消息通道只能在运行 ACECode 的这台电脑上设置'; + return text(body.message) || text(error?.message) || '操作失败'; +} diff --git a/web/src/lib/channelsSettings.test.js b/web/src/lib/channelsSettings.test.js new file mode 100644 index 000000000..054f69951 --- /dev/null +++ b/web/src/lib/channelsSettings.test.js @@ -0,0 +1,387 @@ +// lib/channelsSettings 单元测试:消息通道设置页的快照规整、卡片(连接 / 取消连接 + 简要状态)、 +// 分步连接向导各步的视图、扫码视图、WS 事件合并、二维码路径与侧栏 channel_bound 判定。 + +import assert from 'node:assert/strict'; +import { readFileSync } from 'node:fs'; +import qrcode from 'qrcode-generator'; +import { + applyChannelsEvent, + channelBindingSignature, + channelBoundLabel, + channelBoundPlatform, + channelCardView, + channelCards, + channelErrorText, + channelStateBindingSignature, + connectNeedsWizard, + connectStepView, + contactRows, + credentialFields, + credentialPlaceholder, + credentialsPayload, + discordInviteUrl, + initialWizardStep, + normalizeChannelsSnapshot, + ownerStepView, + pendingRows, + principalLabel, + bindView, + platformSetup, + savedBotSummary, + qrSvgModel, + telegramWebhookBlocked, + wizardSteps, +} from './channelsSettings.js'; + +function run(name, fn) { + try { + fn(); + console.log(`[pass] ${name}`); + } catch (error) { + console.error(`[fail] ${name}`); + throw error; + } +} + +const TELEGRAM = { + platform: 'telegram', + configured: true, + enabled: true, + state: 'connected', + display_name: '@ace_bot', + credential_hint: '****wxyz', + extra: { username: 'ace_bot', privacy_mode: true }, + owner: 'user:42', + contacts: [{ principal: 'user:42', kind: 'user', name: '我', owner: true }], + pending: [{ id: 'r1', kind: 'user', name: 'Alice', principal: 'user:7', label: 'Telegram 私聊', expires_in_s: 61 }], + bindings: [{ + key: '["telegram","1","private","42"]', + label: 'Telegram 私聊', + session_id: 's-1', + no_workspace: false, + cwd: 'C:/work/项目A', + sent: 3, + failed: 1, + dropped: 0, + }], +}; + +// 场景:接口返回只有 Telegram 一个平台,QQ 缺失;字段是 snake_case。 +// 期望:两个平台都有默认快照;字段转成 camelCase,数组项缺 id 的被丢弃。 +run('normalizeChannelsSnapshot 补齐缺失平台并规整字段', () => { + const state = normalizeChannelsSnapshot({ + platforms: [{ ...TELEGRAM, pending: [...TELEGRAM.pending, { name: '缺 id' }] }], + binds: { qq: { platform: 'qq', phase: 'waiting', qr_url: 'https://q.qq.com/x', refreshes: 2 } }, + }); + assert.equal(state.platforms.qq.state, 'unconfigured'); + assert.equal(state.platforms.qq.configured, false); + assert.equal(state.platforms.telegram.displayName, '@ace_bot'); + assert.equal(state.platforms.telegram.pending.length, 1); + assert.equal(state.platforms.telegram.bindings[0].sessionId, 's-1'); + assert.equal(state.binds.qq.qrUrl, 'https://q.qq.com/x'); + assert.equal(state.binds.qq.refreshes, 2); + assert.equal(state.binds.weixin.phase, 'idle'); + assert.equal(state.platforms.discord.state, 'unconfigured'); +}); + +// 场景:WS 推来 channels_state(替换单个平台)、channels_bind(扫码进度)与无关事件。 +// 期望:只改对应部分;无关事件返回同一个对象,组件可跳过重渲染。 +run('applyChannelsEvent 只合并消息通道事件', () => { + const state = normalizeChannelsSnapshot({ platforms: [TELEGRAM] }); + const next = applyChannelsEvent(state, { + type: 'channels_state', + payload: { ...TELEGRAM, state: 'retrying', detail: '网络中断' }, + }); + assert.equal(next.platforms.telegram.state, 'retrying'); + assert.equal(next.platforms.qq, state.platforms.qq); + const bind = applyChannelsEvent(next, { type: 'channels_bind', payload: { platform: 'qq', phase: 'failed', error: '门户不可用' } }); + assert.equal(bind.binds.qq.phase, 'failed'); + const weixin = applyChannelsEvent(bind, { type: 'channels_bind', payload: { platform: 'weixin', phase: 'waiting', qr_url: 'https://liteapp.weixin.qq.com/q/x' } }); + assert.equal(weixin.binds.weixin.qrUrl, 'https://liteapp.weixin.qq.com/q/x'); + assert.equal(weixin.binds.qq, bind.binds.qq); + assert.equal(applyChannelsEvent(weixin, { type: 'channels_bind', payload: { platform: 'telegram', phase: 'waiting' } }), weixin); + assert.equal(applyChannelsEvent(bind, { type: 'token', payload: {} }), bind); + assert.equal(applyChannelsEvent(bind, { type: 'channels_state', payload: { platform: 'whatsapp' } }), bind); +}); + +// 场景:设置页的平台卡片。 +// 期望:七个平台按国内(QQ、微信、飞书、钉钉)再海外(Telegram、Discord、LINE)排列;图标统一是 +// 单色聊天图标(用户拍板:不用彩色品牌 logo);向导都是三步,第一步按平台区分。 +run('channelCards 与向导步骤', () => { + assert.deepEqual(channelCards().map((card) => card.platform), + ['qq', 'weixin', 'feishu', 'dingtalk', 'telegram', 'discord', 'line']); + assert.ok(channelCards().every((card) => card.icon === 'chat')); + assert.deepEqual(wizardSteps('qq'), ['扫码创建机器人', '连接', '绑定机主']); + assert.deepEqual(wizardSteps('weixin'), ['扫码登录', '连接', '完成']); + assert.deepEqual(wizardSteps('telegram'), ['创建机器人', '连接', '绑定机主']); + assert.deepEqual(wizardSteps('line'), ['创建频道', '连接', '绑定机主']); +}); + +// 场景:各平台的接入方式。 +// 期望:QQ、微信扫码(只有 QQ 能改手填);飞书、钉钉、Discord、LINE 填凭据并用 6 位绑定码定机主; +// Telegram 用一次性链接;飞书连上后还要回后台保存长连接订阅并发布版本,所以有连接后清单。 +run('platformSetup 区分接入方式与机主绑定', () => { + assert.equal(platformSetup('qq').credential.kind, 'scan'); + assert.equal(platformSetup('qq').credential.fields.length, 2); + assert.equal(platformSetup('weixin').credential.kind, 'scan'); + assert.equal(platformSetup('weixin').credential.fields.length, 0); + assert.equal(platformSetup('weixin').owner, 'scanner'); + for (const platform of ['feishu', 'dingtalk', 'discord', 'line']) { + assert.equal(platformSetup(platform).credential.kind, 'form', platform); + assert.equal(platformSetup(platform).owner, 'code', platform); + } + assert.equal(platformSetup('telegram').owner, 'link'); + assert.ok(platformSetup('feishu').connectChecklist.some((item) => item.includes('使用长连接接收事件'))); + assert.deepEqual(credentialFields('dingtalk').map((field) => field.key), ['client_id', 'client_secret']); + assert.deepEqual(credentialFields('line').map((field) => field.key), ['channel_id', 'channel_secret', 'public_url']); +}); + +// 场景:Discord 校验通过后拿到 application id,向导要给出邀请链接;已保存机器人的摘要。 +// 期望:邀请链接带 bot 作用域与发消息所需权限;没有 id 时不给链接;摘要只用非密钥字段与尾号。 +run('discordInviteUrl 与已保存机器人摘要', () => { + assert.equal(discordInviteUrl('123'), 'https://discord.com/oauth2/authorize?client_id=123&scope=bot&permissions=274878008320'); + assert.equal(discordInviteUrl(''), ''); + assert.deepEqual(savedBotSummary('feishu', { credentialsPublic: { app_id: 'cli_1', app_secret: '****abcd' } }), + { title: 'App ID cli_1', desc: 'App Secret ****abcd' }); + assert.deepEqual(savedBotSummary('discord', { displayName: 'AceBot', credentialHint: '****wxyz', credentialsPublic: {} }), + { title: 'AceBot', desc: 'Token ****wxyz' }); +}); + +// 场景:卡片在各种状态下的按钮与状态行。 +// 期望:没开时按钮是「连接」且不显示状态;开着时按钮是「取消连接」;已连接显示机器人名; +// 失败只说“详情见日志”并带「处理」入口(原因写进 daemon 日志,不在页面展开);webhook 单独提示; +// 数据文件异常时「连接」不可点;请求进行中按钮一律禁用。 +run('channelCardView 按钮与简要状态', () => { + const state = normalizeChannelsSnapshot({ platforms: [TELEGRAM] }).platforms.telegram; + assert.deepEqual(channelCardView(state), { + button: { action: 'disconnect', label: '取消连接', disabled: false }, + status: { tone: 'ok', text: '已连接 · @ace_bot' }, + }); + const off = { ...state, enabled: false, state: 'disabled' }; + assert.deepEqual(channelCardView(off), { button: { action: 'connect', label: '连接', disabled: false }, status: null }); + const failed = channelCardView({ ...state, state: 'failed', retryStopped: true, detail: 'token 无效' }); + assert.deepEqual(failed.status, { tone: 'danger', text: '连接失败,详情见日志', action: 'fix' }); + assert.equal(failed.status.text.includes('token'), false); + assert.equal(channelCardView({ ...state, state: 'failed', extra: { webhook: true } }).status.text, + '机器人设置了 webhook,需要处理'); + assert.equal(channelCardView({ ...state, state: 'retrying', detail: '网络中断' }).status.text, '连接中断,正在自动重试'); + assert.equal(channelCardView({ ...state, state: 'standby', hostedByPid: 4321 }).status.text, '已由另一个 ACECode 连接'); + assert.equal(channelCardView({ ...off, state: 'error' }).button.disabled, true); + assert.equal(channelCardView(off, true).button.disabled, true); + assert.equal(channelCardView(state, true).button.disabled, true); +}); + +// 场景:点「连接」时平台的配置程度不同。 +// 期望:没有凭据或还没有机主 -> 打开向导(没有凭据从第 1 步,有凭据从第 2 步); +// 凭据与机主都齐 -> 直接连接,不弹向导。 +run('connectNeedsWizard 与 initialWizardStep', () => { + const blank = normalizeChannelsSnapshot(null).platforms.telegram; + const configured = { ...blank, configured: true }; + const ready = { ...configured, owner: 'user:42' }; + assert.equal(connectNeedsWizard(blank), true); + assert.equal(connectNeedsWizard(configured), true); + assert.equal(connectNeedsWizard(ready), false); + assert.equal(initialWizardStep(blank), 0); + assert.equal(initialWizardStep(configured), 1); +}); + +// 场景:LINE 连接停在需要用户动手的几步:没装 cloudflared、控制台没开 Use webhook、 +// 自己的公网地址还没转发到本机回调端口;以及普通的重试。 +// 期望:前三种给出具体该做什么(含端口号);普通重试仍是通用文案;其它平台不受这些字段影响。 +run('LINE 连接步骤的可操作提示', () => { + const base = { ...normalizeChannelsSnapshot(null).platforms.line, configured: true, enabled: true }; + const missing = connectStepView({ ...base, state: 'failed', extra: { cloudflared_missing: true } }); + assert.equal(missing.phase, 'failed'); + assert.ok(missing.text.includes('winget install --id Cloudflare.cloudflared')); + const webhookOff = connectStepView({ ...base, state: 'retrying', extra: { webhook_active: false } }); + assert.equal(webhookOff.tone, 'warn'); + assert.ok(webhookOff.text.includes('Use webhook')); + const custom = connectStepView({ ...base, state: 'retrying', extra: { tunnel: 'custom', listen_port: 41234 } }); + assert.ok(custom.text.includes('http://127.0.0.1:41234')); + const plain = connectStepView({ ...base, state: 'retrying', extra: { tunnel: 'cloudflare' } }); + assert.equal(plain.text, '连接中断,正在自动重试…'); + const telegram = { ...normalizeChannelsSnapshot(null).platforms.telegram, configured: true, enabled: true }; + assert.equal(connectStepView({ ...telegram, state: 'retrying', extra: { webhook_active: false } }).text, + '连接中断,正在自动重试…'); +}); + +// 场景:微信登录在腾讯侧失效,传输层报失败并停止重试;另有一次普通的微信连接失败。 +// 期望:登录失效时点「连接」打开向导并从扫码(第 1 步)开始;普通失败仍直接重连; +// 其他平台停止重试不受影响。 +run('微信登录失效时连接回到扫码', () => { + const weixin = normalizeChannelsSnapshot(null).platforms.weixin; + const ready = { ...weixin, configured: true, owner: 'user:o@im.wechat' }; + const expired = { ...ready, state: 'failed', retryStopped: true }; + assert.equal(connectNeedsWizard(expired), true); + assert.equal(initialWizardStep(expired), 0); + assert.equal(connectNeedsWizard({ ...ready, state: 'failed', retryStopped: false }), false); + const telegram = normalizeChannelsSnapshot(null).platforms.telegram; + const stopped = { ...telegram, configured: true, owner: 'user:42', state: 'failed', retryStopped: true }; + assert.equal(connectNeedsWizard(stopped), false); +}); + +// 场景:向导第 2 步(连接)期间平台状态的变化。 +// 期望:还没打开 -> 正在打开;连接中 / 重试中 -> 继续等;已连接 -> 可进入下一步; +// webhook 阻塞 -> 说明后果并允许移除;其他失败 -> 只说原因已写入日志,可重试;待命 -> 说明会自动接管。 +run('connectStepView 映射连接阶段', () => { + const base = { ...normalizeChannelsSnapshot(null).platforms.telegram, configured: true }; + assert.equal(connectStepView(base).phase, 'idle'); + assert.equal(connectStepView({ ...base, enabled: true, state: 'connecting' }).phase, 'connecting'); + assert.equal(connectStepView({ ...base, enabled: true, state: 'retrying' }).phase, 'connecting'); + assert.deepEqual(connectStepView({ ...base, enabled: true, state: 'connected', displayName: '@ace_bot' }), + { phase: 'connected', tone: 'ok', text: '已连接 @ace_bot' }); + const webhook = connectStepView({ ...base, enabled: true, state: 'failed', extra: { webhook: true } }); + assert.equal(webhook.phase, 'webhook'); + assert.match(webhook.text, /原来接收 webhook 的服务会停止收到消息/); + const failed = connectStepView({ ...base, enabled: true, state: 'failed', detail: 'token 无效' }); + assert.equal(failed.phase, 'failed'); + assert.match(failed.text, /原因已写入日志/); + assert.equal(connectStepView({ ...base, enabled: true, state: 'standby' }).phase, 'standby'); +}); + +// 场景:向导第 3 步(绑定机主)。 +// 期望:已有机主 -> 完成并显示机主名;Telegram 没有机主 -> 一次性链接;QQ 扫码后的 10 分钟窗口内 -> +// 提示去私聊;窗口已过 -> 提示私聊后批准请求(批准第一个私聊请求即成为机主)。 +run('ownerStepView 按平台给出绑定方式', () => { + const telegram = normalizeChannelsSnapshot({ platforms: [TELEGRAM] }).platforms.telegram; + assert.deepEqual(ownerStepView(telegram, 'telegram'), { done: true, mode: 'done', text: '机主:我' }); + assert.equal(ownerStepView({ ...telegram, owner: '' }, 'telegram').mode, 'link'); + const qq = { ...normalizeChannelsSnapshot(null).platforms.qq, configured: true }; + assert.equal(ownerStepView({ ...qq, ownerWindow: true }, 'qq').mode, 'window'); + assert.equal(ownerStepView(qq, 'qq').mode, 'request'); + assert.equal(ownerStepView({ ...qq, owner: 'user:9' }, 'qq').text, '机主:用户 9'); + const discord = { ...normalizeChannelsSnapshot(null).platforms.discord, configured: true }; + assert.equal(ownerStepView(discord, 'discord').mode, 'code'); + assert.match(ownerStepView(discord, 'discord').text, /6 位数字/); +}); + +// 场景:扫码流程各阶段。 +// 期望:进行中阶段 active=true;等待阶段有二维码才显示;过期刷新后提示扫新码; +// 完成时区分扫码人是否已成为机主;失败附原因并提示可改为手动填写。 +run('bindView 映射扫码阶段', () => { + assert.equal(bindView({ phase: 'starting' }).active, true); + const waiting = bindView({ phase: 'waiting', qrUrl: 'u', refreshes: 0 }); + assert.equal(waiting.showQr, true); + assert.equal(waiting.message, '用手机 QQ 扫一扫,按提示创建并授权机器人'); + assert.match(bindView({ phase: 'waiting', qrUrl: 'u', refreshes: 1 }).message, /自动换新/); + assert.equal(bindView({ phase: 'completed', ownerBound: true }).message, '扫码成功,你已是机主'); + assert.equal(bindView({ phase: 'completed', ownerBound: false }).message, '扫码成功'); + assert.match(bindView({ phase: 'failed', error: '' }).message, /手动填写/); + assert.equal(bindView({ phase: 'failed', error: '' }, 'weixin').message, '获取二维码失败,请重试'); + assert.equal(bindView({ phase: 'waiting', qrUrl: 'u' }, 'weixin').message, '用微信扫一扫,并在手机上确认'); + assert.equal(bindView({ phase: 'failed', error: '门户不可用' }).message, '门户不可用'); + assert.equal(bindView({ phase: 'idle' }).active, false); +}); + +// 场景:授权名单与待批准请求的展示行。 +// 期望:身份转成中文标签;机主标“机主”;请求显示过期分钟数。 +run('列表行规整', () => { + const state = normalizeChannelsSnapshot({ platforms: [TELEGRAM] }).platforms.telegram; + assert.equal(principalLabel('member:G1:M1'), '群 G1 的成员 M1'); + assert.equal(principalLabel('group:-100'), '群 -100'); + assert.equal(principalLabel('user:7'), '用户 7'); + assert.equal(contactRows(state)[0].kindLabel, '机主'); + assert.equal(pendingRows(state)[0].subtitle, '私聊 · Telegram 私聊 · 2 分钟后过期'); +}); + +// 场景:Telegram 机器人被配置了 webhook,或者没有。 +// 期望:只有 extra.webhook === true 才判定为被阻塞。 +run('telegramWebhookBlocked', () => { + assert.equal(telegramWebhookBlocked({ extra: { webhook: true } }), true); + assert.equal(telegramWebhookBlocked({ extra: {} }), false); +}); + +// 场景:有人把细节加回设置页(2026-10-05 用户验收反馈:设置界面太复杂,细节打印到日志)。 +// 期望:页面组件不渲染原因 / 隐私模式 / 暂存数 / 绑定计数这些细节;卡片两列;不用开关,用连接按钮。 +run('消息通道设置页保持简洁,细节只进日志', () => { + const page = readFileSync(new URL('../components/ChannelsSettings.jsx', import.meta.url), 'utf8'); + assert.match(page, /grid grid-cols-2/); + assert.doesNotMatch(page, /platform\.detail|privacy_mode|\.held\b|bindingRows|binding\.stats/); + assert.doesNotMatch(page, / { + assert.deepEqual(credentialsPayload('qq', { app_id: ' 1020 ', app_secret: '' }), { app_id: '1020' }); + assert.deepEqual(credentialsPayload('telegram', { token: ' 1:abc ' }), { token: '1:abc' }); + assert.equal(credentialsPayload('telegram', { token: ' ' }), null); + // 飞书的版本选择默认飞书;只选了版本、没填任何凭据不算有内容。 + assert.deepEqual(credentialsPayload('feishu', { app_id: 'cli_1', app_secret: 's' }), + { app_id: 'cli_1', app_secret: 's', domain: 'feishu' }); + assert.equal(credentialsPayload('feishu', {}), null); + // LINE 公网地址留空表示清除(改回自动隧道),所以即使别的都没填也要提交。 + assert.deepEqual(credentialsPayload('line', { public_url: ' ' }), { public_url: '' }); + assert.deepEqual(credentialsPayload('line', { channel_id: '1', channel_secret: 's', public_url: 'https://x.example' }), + { channel_id: '1', channel_secret: 's', public_url: 'https://x.example' }); + const token = credentialFields('telegram')[0]; + assert.equal(credentialPlaceholder({ credentialsPublic: { token: '****wxyz' } }, token), '已保存 ****wxyz,留空则不修改'); + assert.equal(credentialPlaceholder({ credentialsPublic: {} }, token), 'Bot Token'); + const appId = credentialFields('feishu')[0]; + assert.equal(credentialPlaceholder({ credentialsPublic: { app_id: 'cli_9' } }, appId), 'cli_9(留空则不修改)'); +}); + +// 场景:把扫码链接画成二维码。 +// 期望:边长 = 模块数 + 两侧各 4 格静区;路径里的深色格数与库给出的深色模块数一致;空内容返回 null。 +run('qrSvgModel 生成带静区的二维码路径', () => { + const content = 'https://q.qq.com/qqbot/openclaw/connect.html?task_id=abc&source=acecode&_wv=2'; + const model = qrSvgModel(content); + assert.equal(model.size, model.modules + 8); + const code = qrcode(0, 'M'); + code.addData(content); + code.make(); + let dark = 0; + for (let r = 0; r < code.getModuleCount(); r += 1) + for (let c = 0; c < code.getModuleCount(); c += 1) if (code.isDark(r, c)) dark += 1; + const covered = [...model.path.matchAll(/h(\d+)v1/g)].reduce((sum, match) => sum + Number(match[1]), 0); + assert.equal(covered, dark); + assert.equal(qrSvgModel(''), null); +}); + +// 场景:会话列表行带 channel_bound,或只有 /rc 绑定,或平台名未知。 +// 期望:只认 QQ / Telegram;悬停提示写明平台;/rc 绑定不影响这个判定。 +run('channelBoundPlatform 只认消息通道平台', () => { + assert.equal(channelBoundPlatform({ channel_bound: { platform: 'qq' } }), 'qq'); + assert.equal(channelBoundPlatform({ channelBound: { platform: 'telegram' } }), 'telegram'); + assert.equal(channelBoundPlatform({ remote_control_bound: true }), ''); + assert.equal(channelBoundPlatform({ channel_bound: { platform: 'whatsapp' } }), ''); + assert.equal(channelBoundLabel('qq'), '已绑定到 QQ'); + assert.equal(channelBoundLabel(''), ''); +}); + +// 场景:绑定从会话 A 移到会话 B,或只是发送计数变化。 +// 期望:签名只在绑定的会话集合变化时改变,侧栏据此决定是否立即刷新。 +run('绑定签名只随会话集合变化', () => { + const state = normalizeChannelsSnapshot({ platforms: [TELEGRAM] }); + const moved = applyChannelsEvent(state, { + type: 'channels_state', + payload: { ...TELEGRAM, bindings: [{ ...TELEGRAM.bindings[0], session_id: 's-2' }] }, + }); + const counted = applyChannelsEvent(state, { + type: 'channels_state', + payload: { ...TELEGRAM, bindings: [{ ...TELEGRAM.bindings[0], sent: 9 }] }, + }); + assert.notEqual(channelBindingSignature(moved), channelBindingSignature(state)); + assert.equal(channelBindingSignature(counted), channelBindingSignature(state)); + assert.equal(channelStateBindingSignature(TELEGRAM), 's-1'); + assert.equal(channelStateBindingSignature({ platform: 'whatsapp' }), null); +}); + +// 场景:接口返回带中文 message 的 400、远程客户端的 403、网络异常。 +// 期望:分别展示后端文案、本机设置提示、异常自身的消息。 +run('channelErrorText 选择展示文案', () => { + assert.equal(channelErrorText({ code: 'CHANNEL_ERROR', body: { error: 'CHANNEL_ERROR', message: 'Token 无效' } }), 'Token 无效'); + assert.equal(channelErrorText({ code: 'LOCAL_ONLY', body: { error: 'LOCAL_ONLY' } }), '消息通道只能在运行 ACECode 的这台电脑上设置'); + assert.equal(channelErrorText(new Error('Failed to fetch')), 'Failed to fetch'); + assert.equal(channelErrorText(null), '操作失败'); +}); + +// 场景:侧栏渲染被消息通道绑定的会话。 +// 期望:与 /rc 共用电脑图标并带平台提示;/rc 的换绑动画类名仍只由 remoteControlBound 决定。 +run('Sidebar 用 channelBoundPlatform 显示通道绑定图标', () => { + const sidebar = readFileSync(new URL('../components/Sidebar.jsx', import.meta.url), 'utf8'); + assert.match(sidebar, /channelBoundPlatform\(s\)/); + assert.match(sidebar, /data-channel-session-icon=\{channelPlatform\}/); + assert.match(sidebar, /remoteControlBound && 'is-remote-control-bound'/); + assert.doesNotMatch(sidebar, /channelPlatform && 'is-remote-control-bound'/); +}); diff --git a/web/src/lib/chatScrollFollowArchitecture.test.js b/web/src/lib/chatScrollFollowArchitecture.test.js index c53decebb..b9ceefd36 100644 --- a/web/src/lib/chatScrollFollowArchitecture.test.js +++ b/web/src/lib/chatScrollFollowArchitecture.test.js @@ -101,10 +101,11 @@ run('tail button reuses session status without changing visibility or navigation assert.match(button, /: /); }); -run('tail progress dots animate left to right and respect reduced motion', () => { +run('tail progress dots keep a lower-amplitude live signal under reduced motion', () => { const styles = source('styles/globals.css'); assert.match(styles, /\.ace-chat-tail-progress > span\s*\{[^}]*background: currentColor;[^}]*animation: ace-chat-tail-dot-hop 1\.2s ease-in-out infinite;/); - assert.match(styles, /\.ace-chat-tail-progress > span:nth-child\(2\)\s*\{ animation-delay: \.2s;/); - assert.match(styles, /\.ace-chat-tail-progress > span:nth-child\(3\)\s*\{ animation-delay: \.4s;/); - assert.match(styles, /@media \(prefers-reduced-motion: reduce\)\s*\{\s*\.ace-chat-tail-progress > span\s*\{ animation: none;/); + assert.match(styles, /\.ace-chat-tail-progress > span:nth-child\(2\)\s*\{ animation-delay: \.4s;/); + assert.match(styles, /\.ace-chat-tail-progress > span:nth-child\(3\)\s*\{ animation-delay: \.8s;/); + assert.match(styles, /\.ace-chat-tail-progress\s*\{ --ace-chat-tail-dot-lift: -1\.5px;/); + assert.doesNotMatch(styles, /\.ace-chat-tail-progress > span\s*\{ animation: none;/); }); diff --git a/web/src/lib/composerDomSelection.js b/web/src/lib/composerDomSelection.js new file mode 100644 index 000000000..4c686e959 --- /dev/null +++ b/web/src/lib/composerDomSelection.js @@ -0,0 +1,51 @@ +import { Range } from 'slate'; + +function containsNode(root, node) { + if (!root || !node) return false; + if (root === node) return true; + try { + return typeof root.contains === 'function' && root.contains(node); + } catch { + return false; + } +} + +// Slate copies the browser caret into editor.selection only from its throttled +// selectionchange listener, and skips that listener while it believes a drag, +// a selection write or a composition is in progress. If one of those flags is +// left behind, Slate keeps an old selection while the user sees the caret +// elsewhere. Chrome compositions and Backspace/Delete then apply to the old +// Slate selection, so IME text lands at the previous position (often the end). +// +// Returns the Slate range of the live DOM selection when it must replace +// editor.selection before the editor consumes input, otherwise null. +export function composerDomSelectionToAdopt({ + editor, + editableElement, + toSlateRange, + renderPending = false, +} = {}) { + if (!editor || !editableElement || typeof toSlateRange !== 'function') return null; + // Unrendered operations mean the DOM still shows the previous document or + // selection. Slate's own sync also waits; mapping now would undo the change. + if (renderPending || (editor.operations?.length || 0) > 0) return null; + const documentRef = editableElement.ownerDocument; + if (!documentRef || documentRef.activeElement !== editableElement) return null; + const domSelection = typeof documentRef.getSelection === 'function' + ? documentRef.getSelection() + : documentRef.defaultView?.getSelection?.(); + if (!domSelection || !(domSelection.rangeCount > 0)) return null; + if (!containsNode(editableElement, domSelection.anchorNode) + || !containsNode(editableElement, domSelection.focusNode)) { + return null; + } + let range = null; + try { + range = toSlateRange(domSelection); + } catch { + return null; + } + if (!range?.anchor || !range?.focus) return null; + if (editor.selection && Range.equals(range, editor.selection)) return null; + return range; +} diff --git a/web/src/lib/composerDomSelection.test.js b/web/src/lib/composerDomSelection.test.js new file mode 100644 index 000000000..9441a7212 --- /dev/null +++ b/web/src/lib/composerDomSelection.test.js @@ -0,0 +1,178 @@ +// 本文件覆盖「输入框把浏览器光标同步进 Slate 选区」这一修复(用户反馈:光标在行首, +// 用中文输入法打的字却出现在行末,切换会话后恢复)。 +// +// 根因:Slate 只在 selectionchange 里把 DOM 光标写回 editor.selection,并且在它认为 +// 「正在拖拽 / 正在写选区 / 正在合成」时整段跳过。拖拽标记一旦残留(dragstart 之后没有 +// dragend/drop),之后的点击、Home 等都不再更新 editor.selection;而 Chrome 的输入法 +// 合成结束时,Slate 是把文字插到 editor.selection(过期的行末),不是用户看到的光标处。 +// 修复:在 compositionstart / keydown 消费选区之前,用 composerDomSelectionToAdopt 判断 +// 是否需要采纳 DOM 选区。 +// +// 场景: +// 1. 回归:Slate 选区停在行末、DOM 光标在行首 → 返回行首的 Slate range +// 2. 两边一致 → 不重复 select +// 3. Slate 还有未渲染的改动(渲染中 / 操作未 flush)→ 不用旧 DOM 覆盖新选区 +// 4. 编辑器没有焦点、DOM 选区不在编辑器里、没有选区 → 不采纳 +// 5. DOM 选区映射失败(返回 null 或抛错)→ 不采纳 +// 6. editor.selection 为空时也采纳 DOM 光标 +// 7. 生产接线:compositionstart 先采纳 DOM 光标,再标记合成状态并调用父组件回调 +// 8. 生产接线:渲染哨兵与 editor.onChange 包装共同维护「未渲染」标记 +import assert from 'node:assert/strict'; +import fs from 'node:fs'; +import vm from 'node:vm'; +import { parseSync, traverse } from '@babel/core'; +import { composerDomSelectionToAdopt } from './composerDomSelection.js'; + +function run(name, fn) { + try { + fn(); + console.log(`[pass] ${name}`); + } catch (error) { + console.error(`[fail] ${name}`); + throw error; + } +} + +const point = (offset) => ({ path: [0, 0], offset }); +const caret = (offset) => ({ anchor: point(offset), focus: point(offset) }); + +// 鸭子类型的假 DOM:editable 只认识自己和 inner 文本节点;document 的 activeElement / +// getSelection 可按场景替换。 +function fixture({ + editorSelection = caret(4), + domRange = caret(0), + focused = true, + selectionInside = true, + rangeCount = 1, + operations = [], + renderPending = false, +} = {}) { + const inner = { nodeType: 3 }; + const outside = { nodeType: 3 }; + const editable = { + contains: (node) => node === editable || node === inner, + }; + const node = selectionInside ? inner : outside; + const domSelection = { rangeCount, anchorNode: node, focusNode: node }; + editable.ownerDocument = { + activeElement: focused ? editable : { tagName: 'BUTTON' }, + getSelection: () => domSelection, + }; + const calls = []; + return { + calls, + args: { + editor: { selection: editorSelection, operations }, + editableElement: editable, + renderPending, + toSlateRange: (selection) => { + calls.push(selection); + return typeof domRange === 'function' ? domRange() : domRange; + }, + }, + }; +} + +// 场景 1(回归):用户先打了「帮我」,editor.selection 在行末 offset 4;之后点击 / Home +// 把 DOM 光标移到行首,但 Slate 的同步被残留的拖拽标记跳过。 +// 期望:返回 DOM 光标对应的行首 range,调用方据此 select。 +// 修复前:输入法提交的文字插到行末,用户看到光标在行首、字却出现在末尾。 +run('stale Slate selection adopts the caret the user actually sees', () => { + const test = fixture(); + assert.deepEqual(composerDomSelectionToAdopt(test.args), caret(0)); + assert.equal(test.calls.length, 1, 'DOM 选区只映射一次'); +}); + +// 场景 2:正常情况下 Slate 已同步,两边相同。 +// 期望:返回 null,不产生多余的 set_selection(否则每次按键都触发一次渲染)。 +run('an already synchronized selection is left untouched', () => { + const test = fixture({ editorSelection: caret(2), domRange: caret(2) }); + assert.equal(composerDomSelectionToAdopt(test.args), null); +}); + +// 场景 3:程序刚 select 了新位置(例如方向键跨过标签),onChange 已发出但 React 还没把它 +// 写进 DOM(renderPending),或者操作还没 flush(operations 非空)。此时 DOM 仍是旧光标。 +// 期望:不采纳,也不去映射 DOM(NODE 映射表此时也是旧的)。 +// 若误采纳:刚做完的程序化选区会被旧 DOM 光标撤销。 +run('pending renders or unflushed operations keep the programmatic selection', () => { + for (const options of [{ renderPending: true }, { operations: [{ type: 'set_selection' }] }]) { + const test = fixture(options); + assert.equal(composerDomSelectionToAdopt(test.args), null); + assert.equal(test.calls.length, 0, '未渲染时不应读取 DOM 选区'); + } +}); + +// 场景 4:焦点在别的控件(按钮、终端)、DOM 选区落在编辑器之外、或者根本没有选区。 +// 期望:全部返回 null —— 不能把别处的选区搬进输入框。 +run('selections outside a focused editor are ignored', () => { + for (const options of [{ focused: false }, { selectionInside: false }, { rangeCount: 0 }]) { + const test = fixture(options); + assert.equal(composerDomSelectionToAdopt(test.args), null, JSON.stringify(options)); + assert.equal(test.calls.length, 0); + } + assert.equal(composerDomSelectionToAdopt({}), null, '缺少参数时安全返回'); +}); + +// 场景 5:DOM 正在被 React 替换等情况下,toSlateRange 映射不出来(null)或直接抛错。 +// 期望:返回 null,保留 Slate 原选区,输入不会因为异常中断。 +run('unmappable DOM selections fall back to the existing Slate selection', () => { + assert.equal(composerDomSelectionToAdopt(fixture({ domRange: null }).args), null); + assert.equal(composerDomSelectionToAdopt(fixture({ domRange: () => { throw new Error('Cannot resolve a Slate point'); } }).args), null); +}); + +// 场景 6:Slate 选区为空(刚挂载、被 deselect)但编辑器已获得焦点且 DOM 有光标。 +// 期望:直接采纳 DOM 光标。 +run('a missing Slate selection adopts the DOM caret', () => { + const test = fixture({ editorSelection: null, domRange: caret(3) }); + assert.deepEqual(composerDomSelectionToAdopt(test.args), caret(3)); +}); + +const composerSource = fs.readFileSync(new URL('../components/RichComposer.jsx', import.meta.url), 'utf8'); +const ast = parseSync(composerSource, { configFile: false, babelrc: false, parserOpts: { plugins: ['jsx'] } }); +const callbacks = new Map(); +traverse(ast, { + VariableDeclarator({ node }) { + if (node.id.name === 'handleCompositionStart' || node.id.name === 'adoptDomSelection') { + const callback = node.init.arguments[0]; + callbacks.set(node.id.name, composerSource.slice(callback.start, callback.end)); + } + }, +}); + +// 场景 7:生产代码的 handleCompositionStart。 +// 触发:compositionstart 到达(Slate 的 onCompositionStart 先调我们的回调,再决定是否删除 +// 选中片段)。 +// 期望:先采纳 DOM 光标,再置 active、清 settling,最后把父组件回调的返回值原样交还 Slate。 +// 修复前:没有采纳这一步,合成结束时文字落在过期的 editor.selection。 +run('composition start adopts the DOM caret before composition state and parent callbacks', () => { + assert.ok(callbacks.has('handleCompositionStart'), '缺少生产回调 handleCompositionStart'); + const order = []; + const compositionStateRef = { current: { active: false, settling: true } }; + const context = vm.createContext({ + compositionStateRef, + clearCompositionSettleTimer: () => order.push('clear'), + adoptDomSelection: () => { + order.push(`adopt:${compositionStateRef.current.active}`); + return true; + }, + onCompositionStart: () => { + order.push(`parent:${compositionStateRef.current.active}`); + return 'parent-result'; + }, + }); + const handleCompositionStart = vm.runInContext(`(${callbacks.get('handleCompositionStart')})`, context); + assert.equal(handleCompositionStart({}), 'parent-result'); + assert.deepEqual(order, ['clear', 'adopt:false', 'parent:true']); + assert.equal(compositionStateRef.current.settling, false); +}); + +// 场景 8:「未渲染」标记的两端都必须接上:editor.onChange 包装置位,与 Editable 同一次提交里 +// 的渲染哨兵清除;adoptDomSelection 把它传给判定函数。缺任一端,修复要么失效要么会用旧 +// DOM 覆盖新选区。 +run('render sentinel and onChange wrapper maintain the pending-render flag', () => { + assert.match(composerSource, /instance\.onChange = \(options\) => \{[\s\S]*?renderPendingRef\.current = true;[\s\S]*?onChange\(options\);/); + assert.match(composerSource, /spellCheck\s*\/>\s*\s*<\/Slate>/); + assert.match(composerSource, /function ComposerRenderSentinel\(\{ onRendered \}\) \{\s*useSlate\(\);\s*useLayoutEffect\(\(\) => \{\s*onRendered\(\);/); + assert.match(callbacks.get('adoptDomSelection') || '', /renderPending: renderPendingRef\.current/); + assert.match(callbacks.get('adoptDomSelection') || '', /exactMatch: false/); +}); diff --git a/web/src/lib/desktopNotify.js b/web/src/lib/desktopNotify.js index defbeaae6..29c9ad195 100644 --- a/web/src/lib/desktopNotify.js +++ b/web/src/lib/desktopNotify.js @@ -5,17 +5,19 @@ // 不存在 → 所有 notify 调用 no-op,这与 search palette 跨 workspace bridge 的 // 降级模式一致。 // -// 唯一弹窗规则: +// 弹窗规则: // - master enabled 开启 -// - 事件是主任务 completion +// - 事件是主任务 completion,或消息通道的新配对请求(channel) // - 整个 ACECode 窗口失焦 // permission / question 一律留在页面内处理;窗口只要有焦点,无论当前打开 -// 哪个会话、workspace 或页面都不发 native completion 通知。旧的 per-type -// 和 suppress_when_focused 配置字段仅保留后端兼容,不再参与运行时判定。 +// 哪个会话、workspace 或页面都不发 native 通知(配对请求改由页面内 toast 提示)。 +// 旧的 per-type 和 suppress_when_focused 配置字段仅保留后端兼容,不再参与运行时判定。 // // payload 构造抽到 buildNotificationPayload — 长文本截断 80 字 + 省略号, // 空文本回退到默认占位,纯函数,可测。 +import { channelPlatformTitle } from './channelsSettings.js'; + const NOTIFICATION_BODY_LIMIT = 80; // WebView2 / 壳内 focus 有时会短暂落到 native 标题栏,document.hasFocus() @@ -122,12 +124,36 @@ export function shouldNotifySessionCompletion({ export function shouldSuppress(payload, hasFocus, cfg) { if (!notificationsEnabled(cfg)) return true; - // payload.id 形如 "question-..." / "completion-...",取首段判类型。 + // payload.id 形如 "question-..." / "completion-..." / "channel-...",取首段判类型。 const type = String(payload?.id || '').split('-')[0]; - if (type !== 'completion') return true; + if (type !== 'completion' && type !== 'channel') return true; return hasFocus === true; } +// 消息通道(QQ、微信、飞书、钉钉、Telegram、Discord、LINE)的新配对请求(WS channels_request)。 +// 不关联会话,点击通知只把窗口拉到前台。 +export function buildChannelRequestPayload(request = {}) { + const platform = String(request.platform || ''); + const title = `${channelPlatformTitle(platform)} 配对请求`; + const who = String(request.name || '').trim() || (request.kind === 'group' ? '一个群' : '新联系人'); + const where = String(request.label || '').trim(); + return { + id: `channel-${platform || 'unknown'}-${String(request.id || '')}-${Date.now()}`, + workspace_hash: '', + session_id: '', + title, + body: truncateForNotification(`${who}${where ? `(${where})` : ''}请求使用 ACECode,到设置 > 消息通道处理`), + }; +} + +export function maybeNotifyChannelRequest(request, { hasFocus, cfg } = {}) { + if (!bridgeAvailable()) return false; + const payload = buildChannelRequestPayload(request); + const focused = typeof hasFocus === 'boolean' ? hasFocus : isHostWindowFocused(); + if (shouldSuppress(payload, focused, cfg)) return false; + return notify(payload); +} + function bridgeAvailable() { return typeof window !== 'undefined' && typeof window.aceDesktop_notify === 'function'; @@ -155,10 +181,10 @@ export function focusSession(workspaceHash, sessionId) { return false; } try { - window.aceDesktop_focusSession(JSON.stringify({ + window.aceDesktop_focusSession({ workspace_hash: workspaceHash || '', session_id: sessionId, - })); + }); return true; } catch { return false; diff --git a/web/src/lib/desktopNotify.test.js b/web/src/lib/desktopNotify.test.js index d26f96be5..7d74b160a 100644 --- a/web/src/lib/desktopNotify.test.js +++ b/web/src/lib/desktopNotify.test.js @@ -14,6 +14,9 @@ import { shouldSuppress, truncateForNotification, maybeNotify, + focusSession, + maybeNotifyChannelRequest, + buildChannelRequestPayload, noteHostWindowFocus, isHostWindowFocused, } from './desktopNotify.js'; @@ -286,3 +289,51 @@ run('maybeNotify 桥可用 + 窗口聚焦 → 不投递完成通知', () => { assert.equal(called, 0); global.window = prev; }); + +// 消息通道(QQ / Telegram)的新配对请求:窗口失焦时弹系统通知,聚焦时由页面内 toast 提示; +// 总开关关闭时一律不弹。 +run('buildChannelRequestPayload 写明平台、来源与处理入口', () => { + const payload = buildChannelRequestPayload({ platform: 'qq', id: 'r1', kind: 'user', name: 'Alice', label: 'QQ 私聊' }); + assert.equal(payload.title, 'QQ 配对请求'); + assert.equal(payload.body, 'Alice(QQ 私聊)请求使用 ACECode,到设置 > 消息通道处理'); + assert.equal(payload.session_id, ''); + assert.ok(payload.id.startsWith('channel-qq-r1-')); + const group = buildChannelRequestPayload({ platform: 'telegram', id: 'r2', kind: 'group', name: '' }); + assert.equal(group.title, 'Telegram 配对请求'); + assert.ok(group.body.startsWith('一个群请求使用')); +}); + +run('shouldSuppress: 配对请求只在窗口失焦且总开关开启时弹', () => { + const payload = buildChannelRequestPayload({ platform: 'telegram', id: 'r1', name: 'Bob' }); + assert.equal(shouldSuppress(payload, false, null), false); + assert.equal(shouldSuppress(payload, true, null), true); + assert.equal(shouldSuppress(payload, false, { enabled: false }), true); +}); + +run('maybeNotifyChannelRequest 失焦时经桥投递,聚焦时不投递', () => { + const prev = global.window; + const captured = []; + global.window = { aceDesktop_notify: (json) => { captured.push(JSON.parse(json)); } }; + assert.equal(maybeNotifyChannelRequest({ platform: 'qq', id: 'r9', name: 'Carol' }, { hasFocus: false, cfg: null }), true); + assert.equal(maybeNotifyChannelRequest({ platform: 'qq', id: 'r9', name: 'Carol' }, { hasFocus: true, cfg: null }), false); + assert.equal(captured.length, 1); + assert.equal(captured[0].title, 'QQ 配对请求'); + global.window = prev; +}); + +run('focusSession passes a structured native argument and supports no-workspace sessions', () => { + const previous = globalThis.window; + let request; + globalThis.window = {aceDesktop_focusSession: value => {request = value;}}; + try { + assert.equal(focusSession('', 'office-root'), true); + // Native receives the JSON-encoded argument array, whose first item must + // be an object rather than a second, nested JSON string. + const nativeArgs = JSON.parse(JSON.stringify([request])); + assert.deepEqual(nativeArgs, [{workspace_hash:'', session_id:'office-root'}]); + assert.equal(focusSession('workspace', ''), false); + } finally { + if (previous === undefined) delete globalThis.window; + else globalThis.window = previous; + } +}); diff --git a/web/src/lib/desktopOffice.test.js b/web/src/lib/desktopOffice.test.js new file mode 100644 index 000000000..8ed976f29 --- /dev/null +++ b/web/src/lib/desktopOffice.test.js @@ -0,0 +1,197 @@ +import assert from 'node:assert/strict'; +import { recentOffices, officeActor, projectDesktopOffice, stableOfficeSeed, fitTail, speechTail } from './desktopOfficeState.js'; +import { createDesktopOfficeController, OFFICE_LIVE_PUBLISH_MS } from './desktopOfficeController.js'; +import { toolActivityVerb, toolIconName, toolIconSvg } from './toolIcons.js'; +import { interfaceIconSvg } from './interfaceIcons.js'; + +function test(name, fn) {fn(); console.log(`[pass] desktop office: ${name}`);} +async function check(name, fn) {await fn(); console.log(`[pass] desktop office: ${name}`);} +const session = (id, overrides = {}) => ({id, active:true, last_user_message_at:'2026-10-06T10:00:00Z', ...overrides}); +const snapshot = (id, overrides = {}) => ({selected:session(id),offices:[session(id)],agents:[session(id)],complete:true,...overrides}); + +test('recent five depend only on human send time across workspaces', () => { + const rows = Array.from({length:7}, (_,i) => session(String(i),{workspace_hash:`w${i%2}`,last_user_message_at:`2026-10-06T10:00:0${i}Z`,updated_at:`2026-10-06T20:00:0${9-i}Z`})); + rows.push(session('archived',{archived:true,last_user_message_at:'2099-01-01'})); + rows.push(session('child',{parent_session_id:'6',last_user_message_at:'2099-01-01'})); + rows.push(session('empty',{last_user_message_at:''})); + assert.deepEqual(recentOffices(rows).map(s=>s.id),['6','5','4','3','2']); +}); + +test('main sleeps on completion and wakes; workers leave on completion', () => { + assert.equal(officeActor(session('root',{last_turn_outcome:'completed'}),'root').state,'sleep'); + assert.equal(officeActor(session('root',{busy:true,last_turn_outcome:'completed'}),'root').state,'think'); + assert.equal(officeActor(session('child',{last_turn_outcome:'completed'}),'root').present,false); + assert.equal(officeActor(session('child',{busy:true,last_turn_outcome:'completed'}),'root').present,true); + assert.equal(officeActor(session('root'),'root').state,'idle'); +}); + +test('waiting, retry, cancellation and errors are distinct', () => { + for(const [phase,expected] of [['permission_waiting','permission'],['question_waiting','question'],['compacting','compact'],['model_retry','retry']]) { + assert.equal(officeActor(session('root',{busy:true,activity:{phase}}),'root').state,expected); + } + assert.equal(officeActor(session('root',{last_turn_outcome:'aborted'}),'root').state,'stopped'); + assert.equal(officeActor(session('root',{last_turn_outcome:'error'}),'root').state,'error'); +}); + +test('failed mesh workers with internal input stay visible for the current root turn', () => { + const child=session('mesh',{last_user_message_at:'',updated_at:'2026-10-06T10:01:00Z',last_turn_outcome:'error'}); + assert.equal(projectDesktopOffice(snapshot('root',{agents:[session('root'),child]})).agents.length,2); + const newer=session('root',{last_user_message_at:'2026-10-06T10:02:00Z'}); + assert.equal(projectDesktopOffice({...snapshot('root'),selected:newer,agents:[newer,child]}).agents.length,1); +}); + +test('paper thickness follows current context rather than lifetime totals', () => { + const base=session('root',{context_window:1000,token_usage:{has_data:true,prompt_tokens:900},session_token_usage:{total_tokens:9000000}}); + const full=officeActor(base,'root'); + assert.equal(full.contextRatio,.9); + const compacted=officeActor({...base,token_usage:{has_data:true,prompt_tokens:150}},'root'); + assert.equal(compacted.contextRatio,.15); + assert.equal(officeActor(session('root'),'root').contextKnown,false); +}); + +test('stable identity and nested mesh children belong to one office with overflow', () => { + const agents=[session('root'),...Array.from({length:9},(_,i)=>session(`child${i}`,{parent_session_id:'root',agent_path:`/root/a/child${i}`,busy:true}))]; + const view=projectDesktopOffice(snapshot('root',{agents})); + assert.equal(view.agents.length,10);assert.equal(view.overflow,2); + assert.equal(view.agents[0].name,'Maestro'); + assert.equal(stableOfficeSeed('root'),stableOfficeSeed('root')); + assert.notEqual(stableOfficeSeed('root'),stableOfficeSeed('another')); + assert.equal(view.agents.find(a=>a.id==='child0').seed,officeActor(agents[1],'root').seed); +}); + +test('tool SVG is exactly the shared chat icon source', () => { + for(const tool of ['file_read','file_write','file_edit','apply_patch','grep','glob','web_search','bash','spawn_subagent','agent_send_message','AskUserQuestion','TodoWrite','task_complete','vision_analyze','skill_view','mcp__server__tool','unknown']) { + const icon=toolIconName(tool); + assert.equal(toolIconSvg(tool),interfaceIconSvg(icon,14)); + assert.match(toolIconSvg(tool),/^ { + const actor=officeActor(session('root',{busy:true,activity:{phase:'responding',text:'第一段\n\n## 小标题\n**最后**几个字是这些'}}),'root'); + assert.equal(actor.state,'type');assert.equal(actor.label,'撰写回复'); + assert.ok(actor.detail.endsWith('最后几个字是这些'),actor.detail); + assert.ok(!/[#*]/.test(actor.detail),actor.detail); + assert.equal(actor.tool,''); +}); + +// 工具执行:动词 + 调用预览(长路径保留文件名一端);等成员回报单独一种状态。 +test('tool work names the verb and keeps the end of long previews', () => { + const read=officeActor(session('root',{busy:true,activity:{phase:'tool_running',tool:'file_read',detail:'src/apps/desktop/some/very/deep/folder/desktop_pet_mac.mm'}}),'root'); + assert.equal(read.state,'work');assert.equal(read.label,'读取');assert.equal(read.icon,'OpenFile'); + assert.ok(read.detail.startsWith('…')&&read.detail.endsWith('desktop_pet_mac.mm'),read.detail); + assert.equal(officeActor(session('root',{busy:true,activity:{tool:'wait_subagent'}}),'root').state,'wait'); + assert.equal(officeActor(session('root',{busy:true,activity:{tool:'agent_wait'}}),'root').label,'等成员回报'); + const planning=officeActor(session('root',{busy:true,activity:{phase:'tool_planning',tool:'file_write'}}),'root'); + assert.equal(planning.state,'type');assert.equal(planning.label,'准备写入文件');assert.equal(planning.icon,'Save'); + assert.equal(toolActivityVerb('bash'),'运行命令');assert.equal(toolActivityVerb('mcp__x__y'),'调用 MCP'); +}); + +// 「适合日常工作」模式的具体进度文案优先;引擎默认的笼统文案不进气泡。 +test('concrete progress titles win over generic engine labels', () => { + assert.equal(officeActor(session('root',{busy:true,activity:{phase:'model_waiting',label:'正在等待模型响应'}}),'root').label,'思考中'); + assert.equal(officeActor(session('root',{busy:true,activity:{phase:'tool_running',tool:'bash',label:'正在调用工具 bash'}}),'root').label,'运行命令'); + assert.equal(officeActor(session('root',{busy:true,activity:{phase:'tool_running',tool:'grep',label:'正在搜索代码并读取'}}),'root').label,'搜索代码并读取'); + assert.equal(officeActor(session('root',{busy:true,activity:{phase:'reasoning',label:'正在推理',text:'check the config'}}),'root').detail,'check the config'); +}); + +// 气泡宽度按半角单位计:汉字算 2,超出时保留末尾并以省略号开头。 +test('bubble text keeps the tail within its width budget', () => { + assert.equal(fitTail('abcdef',4),'…cdef'); + assert.equal(fitTail('一二三四五',6),'…三四五'); + assert.equal(fitTail(' short ',28),'short'); + assert.equal(speechTail('see [docs](http://x.test) `code`'),'see docs code'); +}); + +// 子会话路径:/root/a/b 的上级是 /root/a,主会话没有上级。 +test('mesh workers know their dispatching parent', () => { + assert.equal(officeActor(session('c',{agent_path:'/root/a/b'}),'root').parentPath,'/root/a'); + assert.equal(officeActor(session('root'),'root').parentPath,''); +}); + +function fixture(fetchSnapshot) { + const publications=[],retained=new Set(),released=[],timers=new Map(),delays=new Map();let timerId=0; + const controller=createDesktopOfficeController({fetchSnapshot,publish:value=>publications.push(value), + retainSession:id=>retained.add(id),releaseSession:id=>{retained.delete(id);released.push(id);}, + schedule:(fn,delay)=>{timers.set(++timerId,fn);delays.set(timerId,delay);return timerId;},cancel:id=>timers.delete(id)}); + // 只触发指定延迟的计时器(实时文字节流 vs 快照轮询),其余保持挂起。 + const fire=delay=>{for(const [id,fn] of [...timers]) if(delays.get(id)===delay){timers.delete(id);fn();}}; + return {controller,publications,retained,released,timers,fire}; +} +const deferred=()=>{let resolve,reject;const promise=new Promise((a,b)=>{resolve=a;reject=b;});return {promise,resolve,reject};}; + +await check('manual choice turns follow off and keeps office fixed', async () => { + const requests=[];const f=fixture(async ref=>{requests.push(ref.sessionId);return snapshot(ref.sessionId||'recent');}); + f.controller.setActive({sessionId:'a'});await f.controller.refresh(); + assert.equal(f.controller.state.follow,true);assert.equal(f.controller.state.selected.sessionId,'a'); + f.controller.select({session_id:'b'});await f.controller.refresh(); + f.controller.setActive({sessionId:'c'});await f.controller.refresh(); + assert.equal(f.controller.state.selected.sessionId,'b');assert.equal(f.controller.state.follow,false); + f.controller.setFollow(true);await f.controller.refresh(); + assert.equal(f.controller.state.selected.sessionId,'c');f.controller.dispose(); + assert.equal(f.retained.size,0);assert.equal(f.timers.size,0); +}); + +await check('late responses cannot replace the new office or leak subscriptions', async () => { + const a=deferred(),b=deferred();const f=fixture(ref=>ref.sessionId==='a'?a.promise:b.promise); + f.controller.setActive({sessionId:'a'});const old=f.controller.refresh(); + f.controller.select({sessionId:'b'});const next=f.controller.refresh(); + b.resolve(snapshot('b'));await next;a.resolve(snapshot('a'));await old; + assert.equal(f.controller.state.selected.sessionId,'b');assert.deepEqual([...f.retained],['b']); + f.controller.dispose();assert.equal(f.retained.size,0); +}); + +await check('disposing with an in-flight response prevents further publication', async () => { + const pending=deferred();const f=fixture(()=>pending.promise); + const request=f.controller.refresh();f.controller.dispose();const count=f.publications.length; + pending.resolve(snapshot('late'));await request;assert.equal(f.publications.length,count); + assert.equal(f.retained.size,0); +}); + +await check('offline is explicit and reconnect recovers from a fresh snapshot', async () => { + let fail=false;const f=fixture(async()=>{if(fail)throw new Error('offline');return snapshot('root');}); + await f.controller.refresh();fail=true;await f.controller.refresh();assert.equal(f.controller.state.connected,false); + fail=false;const releases=f.released.length;f.controller.reconnected();assert.equal(f.released.length,releases);await f.controller.refresh();assert.equal(f.controller.state.connected,true); + assert.equal(f.controller.state.selected.sessionId,'root');f.controller.dispose(); +}); + +await check('completion releases a child while the selected office stays subscribed', async () => { + let done=false;const f=fixture(async()=>snapshot('root',{agents:[session('root'),session('child',{busy:!done,last_turn_outcome:done?'completed':''})]})); + await f.controller.refresh();assert.equal(f.retained.has('child'),true); + done=true;await f.controller.refresh();assert.equal(f.retained.has('child'),false); + assert.equal(f.controller.state.agents.some(a=>a.id==='child'),false); + assert.equal(f.retained.has('root'),true);f.controller.dispose(); +}); + +// 流式 token 走 WebSocket 本地叠加:不发额外快照请求,按节流间隔发布; +// 只认办公室里的成员;快照追上(seq 不再落后)后叠加层让位给服务端状态。 +await check('streamed tokens update the speaker between snapshots without extra requests', async () => { + let requests=0,serverActivity={seq:5,busy:true,phase:'model_waiting'}; + const f=fixture(async()=>{requests++;return snapshot('root',{agents:[session('root',{busy:true,activity:serverActivity})]});}); + await f.controller.refresh();requests=0;const published=f.publications.length; + f.controller.onEvent({type:'token',session_id:'root',seq:6,payload:{text:'你好'}}); + f.controller.onEvent({type:'token',session_id:'root',seq:7,payload:{text:'世界'}}); + f.controller.onEvent({type:'token',session_id:'outsider',seq:8,payload:{text:'别人'}}); + assert.equal(f.publications.length,published,'节流期间不逐 token 发布'); + f.fire(OFFICE_LIVE_PUBLISH_MS); + assert.equal(f.publications.length,published+1); + let root=f.publications.at(-1).agents[0]; + assert.equal(root.state,'type');assert.equal(root.detail,'你好世界');assert.equal(requests,0); + // 服务端快照已包含到 seq 7 的正文:以服务端为准,本地叠加被丢弃。 + serverActivity={seq:7,busy:true,phase:'responding',text:'你好世界'}; + await f.controller.refresh(); + root=f.controller.state.agents[0];assert.equal(root.detail,'你好世界'); + // 叠加层被清掉后,服务端进入工具阶段不会再被旧的正文覆盖。 + serverActivity={seq:9,busy:true,phase:'tool_running',tool:'bash',detail:'npm test'}; + await f.controller.refresh(); + root=f.controller.state.agents[0];assert.equal(root.state,'work');assert.equal(root.detail,'npm test'); + // 快照之后又来的 token 接在服务端尾巴后面(同一段正文)。 + serverActivity={seq:10,busy:true,phase:'responding',text:'前半'}; + await f.controller.refresh(); + f.controller.onEvent({type:'token',session_id:'root',seq:11,payload:{text:'后半'}}); + assert.equal(f.controller.state.agents[0].detail,'前半后半'); + f.controller.dispose();assert.equal(f.timers.size,0); +}); diff --git a/web/src/lib/desktopOfficeController.js b/web/src/lib/desktopOfficeController.js new file mode 100644 index 000000000..037a8c8c4 --- /dev/null +++ b/web/src/lib/desktopOfficeController.js @@ -0,0 +1,136 @@ +import { officeSessionRef, projectDesktopOffice } from './desktopOfficeState.js'; + +// Streaming text reaches the office a few times per second, not per token. +export const OFFICE_LIVE_PUBLISH_MS = 150; +const LIVE_TEXT_CHARS = 400; + +// One App-owned controller; every asynchronous response belongs to a selection +// generation. Inject IO, timers and publishing so lifecycle races are testable. +export function createDesktopOfficeController({ + fetchSnapshot, fetchModel, publish, retainSession = () => {}, releaseSession = () => {}, + schedule = setTimeout, cancel = clearTimeout, initial = {}, +} = {}) { + let follow = initial.follow !== false, active = {}, selected = initial.selected || {}; + let generation = 0, disposed = false, timer = null, timerDelay = Infinity, request = null; + let snapshot = {}, connected = true, queued = false, liveTimer = null; + const retained = new Set(); + // session id -> {phase, seq, fromSeq, text}: token/reasoning events newer than the snapshot. + const live = new Map(); + const project = () => projectDesktopOffice(snapshot, {follow, connected, live}); + const emit = () => !disposed && publish(project()); + const emitLive = () => { + if (disposed || liveTimer !== null) return; + liveTimer = schedule(() => {liveTimer = null; emit();}, OFFICE_LIVE_PUBLISH_MS); + }; + const tracked = id => (snapshot.agents || []).some(session => session.id === id); + const pruneLive = () => { + for (const [id, entry] of live) { + const session = (snapshot.agents || []).find(item => item.id === id); + if (!session || entry.seq <= (Number(session.activity?.seq) || 0)) live.delete(id); + } + }; + const syncRetained = () => { + const wanted = new Set(); + for (const session of [...(snapshot.offices || []), ...(snapshot.agents || [])]) { + if (session.active && (session.id === snapshot.selected?.id || session.busy || session.status === 'running')) wanted.add(session.id); + } + for (const id of retained) if (!wanted.has(id)) {releaseSession(id); retained.delete(id);} + for (const id of wanted) if (!retained.has(id)) {retainSession(id); retained.add(id);} + }; + function later(delay) { + if (disposed) return; + if (timer !== null && timerDelay <= delay) return; + if (timer !== null) cancel(timer); + timerDelay = delay; + timer = schedule(() => {timer = null; timerDelay = Infinity; void refresh();}, delay); + } + async function refresh() { + if (disposed) return; + if (request) {queued = true; return;} + const version = generation, controller = new AbortController(); + request = controller; + try { + const wanted = follow && active.sessionId ? active : selected; + const next = await fetchSnapshot(wanted, {signal: controller.signal}); + if (disposed || version !== generation || controller.signal.aborted) return; + // Dormant sessions need their model window; never use lifetime token totals. + if (next.selected && !next.selected.context_window && fetchModel) { + try { + const model = await fetchModel(next.selected.id, next.selected.workspace_hash || ''); + if (disposed || version !== generation || controller.signal.aborted) return; + const limit = Number(model?.context_window ?? model?.contextWindow) || 0; + next.selected.context_window = limit; + for (const agent of next.agents || []) if (agent.id === next.selected.id) agent.context_window = limit; + } catch { /* Unknown context remains unknown until a later snapshot. */ } + } + snapshot = next; connected = true; + if (next.selected) selected = officeSessionRef(next.selected); + pruneLive(); syncRetained(); emit(); + } catch (error) { + if (disposed || version !== generation || controller.signal.aborted) return; + if (error?.status === 404) { + selected = {}; snapshot = {...snapshot, selected:null, agents:[], offices:error.body?.offices || snapshot.offices || []}; + if (follow) active = {}; + live.clear(); syncRetained(); + } else connected = false; + emit(); + } finally { + if (request === controller) request = null; + if (!disposed && version === generation) { + const delay = queued ? 100 : snapshot.complete === false ? 500 : 3000; + queued = false; later(delay); + } + } + } + function change() { + generation++; + request?.abort(); request = null; queued = false; + snapshot = {...snapshot, selected:null, agents:[]}; + live.clear(); syncRetained(); emit(); later(0); + } + function streamed(message) { + const id = String(message.session_id || ''); + if (!id || !tracked(id)) return; + const phase = message.type === 'token' ? 'responding' : 'reasoning'; + const seq = Number(message.seq) || 0, piece = String(message.payload?.text || ''); + const prev = live.get(id); + live.set(id, prev && prev.phase === phase + ? {...prev, seq: Math.max(seq, prev.seq), text: (prev.text + piece).slice(-LIVE_TEXT_CHARS)} + : {phase, seq, fromSeq: seq, text: piece.slice(-LIVE_TEXT_CHARS)}); + emitLive(); + } + return { + start() {later(0);}, + refresh, + setActive(value) { + const next = officeSessionRef(value); + if (next.sessionId === active.sessionId && next.workspaceHash === active.workspaceHash) return; + active = next; + if (follow && next.sessionId) change(); + }, + select(value) {selected = officeSessionRef(value); follow = false; change();}, + setFollow(value) {follow = !!value; change();}, + onEvent(message) { + if (message.type === 'token' || message.type === 'reasoning') {streamed(message); return;} + if (message.type === 'tool_update') return; + if (request) queued = true; + else later(100); + }, + disconnected() {connected = false; emit(); later(500);}, + reconnected() { + generation++; request?.abort(); request = null; queued = false; + // Preserve retained subscriptions while recovering: releasing them from + // an onopen callback would close/reopen an office-only connection forever. + later(0); + }, + get state() {return project();}, + dispose() { + disposed = true; generation++; request?.abort(); request = null; + if (timer !== null) cancel(timer); + if (liveTimer !== null) cancel(liveTimer); + timer = null; liveTimer = null; live.clear(); + for (const id of retained) releaseSession(id); + retained.clear(); + }, + }; +} diff --git a/web/src/lib/desktopOfficeState.js b/web/src/lib/desktopOfficeState.js new file mode 100644 index 000000000..a6f076806 --- /dev/null +++ b/web/src/lib/desktopOfficeState.js @@ -0,0 +1,131 @@ +import { normalizeTokenBudget } from './tokenBudget.js'; +import { toolActivityVerb, toolIconName, toolIconSvg } from './toolIcons.js'; + +export const OFFICE_LIMIT = 5; +export const OFFICE_WORKER_SEATS = 7; +const idOf = value => String(value?.id || value?.sessionId || value?.session_id || ''); +export const officeSessionRef = value => ({ + sessionId: idOf(value), + workspaceHash: String(value?.workspaceHash || value?.workspace_hash || ''), +}); +export function stableOfficeSeed(value) { + let hash = 2166136261; + for (const c of String(value)) hash = Math.imul(hash ^ c.codePointAt(0), 16777619); + return hash >>> 0; +} +export function recentOffices(sessions = []) { + const unique = new Map(); + for (const session of sessions) { + if (!idOf(session) || session.archived || session.parent_session_id || !session.last_user_message_at) continue; + unique.set(idOf(session), session); + } + return [...unique.values()].sort((a,b) => + String(b.last_user_message_at).localeCompare(String(a.last_user_message_at)) || idOf(a).localeCompare(idOf(b))) + .slice(0, OFFICE_LIMIT); +} + +// Bubble text is measured in half-width units: CJK and full-width glyphs count twice. +const charUnits = ch => (/[ᄀ-ᅟ⺀-꓏가-힣豈-﫿︰-﹏＀-⦆¢-₩]/.test(ch) + || ch.codePointAt(0) > 0xffff ? 2 : 1); + +// Keep the end of a line (latest words of a reply, file name of a path). +export function fitTail(text, units = 28) { + const chars = [...String(text || '').replace(/\s+/g, ' ').trim()]; + let width = 0, start = chars.length; + while (start > 0 && width + charUnits(chars[start - 1]) <= units) width += charUnits(chars[--start]); + return start > 0 ? '…' + chars.slice(start).join('') : chars.join(''); +} + +// Last visible words of streamed Markdown, without the formatting punctuation. +export function speechTail(text, units = 28) { + const plain = String(text || '') + .replace(/```[\w-]*/g, ' ') + .replace(/[`*#]+/g, '') + .replace(/^\s*>\s?/gm, '') + .replace(/\[([^\]]*)\]\([^)]*\)/g, '$1'); + return fitTail(plain, units); +} + +const GENERIC_LABEL = /^正在(?:等待模型响应|推理|准备工具调用|准备调用\s|调用工具\s)/; +const WAIT_TOOLS = new Set(['wait_subagent', 'agent_wait']); + +export function officeActor(session, rootId) { + const id = idOf(session), root = id === rootId, a = session.activity || {}; + const busy = session.busy === true || session.status === 'running' || a.busy === true; + const outcome = String(a.outcome || session.last_turn_outcome || ''); + const phase = String(a.phase || ''), tool = String(a.tool || ''); + const raw = String(a.label || ''); + // Concrete progress titles (work mode) are richer than generic engine labels. + const concrete = raw && !GENERIC_LABEL.test(raw) ? raw.replace(/^正在/, '') : ''; + let state = 'idle', label = '待命', detail = ''; + if (busy) { + if (phase === 'permission_waiting') { + state = 'permission'; label = '等待授权'; detail = tool ? toolActivityVerb(tool) : ''; + } else if (phase === 'question_waiting') { state = 'question'; label = '等待回答'; } + else if (phase === 'compacting' || phase === 'context_repair') { state = 'compact'; label = '整理上下文'; } + else if (phase === 'model_retry') { state = 'retry'; label = '正在重试'; } + else if (phase === 'responding') { state = 'type'; label = '撰写回复'; detail = speechTail(a.text); } + else if (phase === 'tool_planning') { + state = 'type'; label = tool ? '准备' + toolActivityVerb(tool) : '准备调用工具'; + } else if (tool || phase === 'tool_running') { + state = WAIT_TOOLS.has(tool.toLowerCase()) ? 'wait' : 'work'; + label = concrete || (tool ? toolActivityVerb(tool) : '调用工具'); + detail = fitTail(a.detail, 30); + } else if (phase === 'reasoning') { state = 'think'; label = concrete || '思考中'; detail = speechTail(a.text, 24); } + else { state = 'think'; label = concrete || '思考中'; } + } else if (outcome === 'completed') { state = root ? 'sleep' : 'done'; label = root ? 'zzz' : '已完成'; } + else if (outcome === 'error' || outcome === 'errored') { state = 'error'; label = '执行失败'; } + else if (outcome === 'aborted' || outcome === 'interrupted') { state = 'stopped'; label = '已停止'; } + const context = normalizeTokenBudget({usage: session.token_usage, contextWindow: session.context_window}); + const shownTool = state === 'work' || state === 'wait' || state === 'permission' || phase === 'tool_planning' ? tool : ''; + const path = String(session.agent_path || (root ? '/root' : '')); + return { + id, root, name: root ? 'Maestro' : String(session.title || session.agent_path?.split('/').pop() || session.summary || 'Agent'), + path, parentPath: path.includes('/', 1) ? path.slice(0, path.lastIndexOf('/')) : '', + state, label, detail, tool: shownTool, icon: shownTool ? toolIconName(shownTool) : '', + iconSvg: shownTool ? toolIconSvg(shownTool) : '', busy, + seed: stableOfficeSeed(id), contextKnown: context.known, + contextRatio: context.usedRatio, contextTokens: context.usedTokens, contextLimit: context.limitTokens, + compactId: String(a.compact_id || ''), seq: Number(a.seq) || 0, + transfers: Array.isArray(a.transfers) ? a.transfers : [], + // Completed workers leave immediately. Failed/stopped workers remain visible + // for their current parent turn, so they cannot masquerade as success. + present: root || busy || state === 'error' || state === 'stopped', + lastActivityAt: String(session.updated_at || session.last_user_message_at || ''), + }; +} + +// Streaming tokens arrive over the WebSocket faster than snapshots. The overlay +// is newer than the snapshot only while its event sequence is ahead of it. +export function withLiveActivity(session, entry) { + const a = session.activity || {}; + const seq = Number(a.seq) || 0; + if (!entry || !(entry.seq > seq)) return session; + const continues = a.phase === entry.phase && entry.fromSeq > seq; + return {...session, busy: true, activity: {...a, busy: true, phase: entry.phase, + text: (continues ? String(a.text || '') : '') + entry.text, tool: '', detail: '', seq: entry.seq}}; +} + +export function projectDesktopOffice(snapshot = {}, { follow = true, connected = true, live = null } = {}) { + const selected = snapshot.selected || null; + const rootId = idOf(selected); + const rootTime = String(selected?.last_user_message_at || ''); + const sessions = (snapshot.agents || []).map(session => withLiveActivity(session, live?.get(idOf(session)))); + const actors = sessions.map(session => officeActor(session, rootId)); + const agents = actors.filter(actor => actor.present && (actor.root || actor.busy || !rootTime || actor.lastActivityAt >= rootTime)); + agents.sort((a,b) => Number(b.root) - Number(a.root) || a.id.localeCompare(b.id)); + return { + version: 1, follow, connected, complete: snapshot.complete !== false, + offices: recentOffices(snapshot.offices).map(session => ({ + ...officeSessionRef(session), title: String(session.title || session.summary || '未命名会话'), + busy: session.busy === true || session.status === 'running', + lastUserMessageAt: session.last_user_message_at, + workspaceName: String(session.workspace_name || ''), + })), + selected: selected ? { ...officeSessionRef(selected), title: String(selected.title || selected.summary || '未命名会话') } : null, + seed: stableOfficeSeed(rootId || 'empty-office'), + agents, + completed: actors.filter(actor => !actor.root && actor.state === 'done').map(actor => actor.id), + overflow: Math.max(0, agents.length - OFFICE_WORKER_SEATS - 1), + }; +} diff --git a/web/src/lib/richComposerKeyboard.test.js b/web/src/lib/richComposerKeyboard.test.js index 1a47a2075..e07cd6b83 100644 --- a/web/src/lib/richComposerKeyboard.test.js +++ b/web/src/lib/richComposerKeyboard.test.js @@ -52,6 +52,9 @@ function fixture({ kind = 'path', key = 'Backspace', selected = false } = {}) { parentCalls: 0, submissions: 0, removed: [], + adoptCalls: 0, + // 模拟用户看到的 DOM 光标;为 null 表示 DOM 与 Slate 一致、无需采纳。 + domCaret: null, }; const context = vm.createContext({ ...composerModel, ...contentModel, ...composerSelection, Editor, Range, Transforms, HistoryEditor, COMPOSER_CLIPBOARD_TYPE: 'application/x-acecode-composer-content' }); const editor = withHistory(loadFunction('withComposerInlineTags', context)(createEditor())); @@ -75,6 +78,12 @@ function fixture({ kind = 'path', key = 'Backspace', selected = false } = {}) { onKeyDown: () => { state.parentCalls += 1; }, onSubmit: () => { state.submissions += 1; }, onRemoveAttachment: (attachmentKey) => state.removed.push(attachmentKey), + adoptDomSelection: () => { + state.adoptCalls += 1; + if (!state.domCaret) return false; + Transforms.select(editor, state.domCaret); + return true; + }, deleteAdjacentTag: loadFunction('deleteAdjacentTag', context), removeAttachmentReference: loadFunction('removeAttachmentReference', context), deleteSelectedPlainText: loadFunction('deleteSelectedPlainText', context), @@ -103,6 +112,8 @@ function assertImeOwnsKey(test) { assert.deepEqual(test.state.removed, [], 'IME key must not remove attachments'); assert.equal(test.state.parentCalls, 0, 'IME key must not invoke parent history navigation'); assert.equal(test.state.submissions, 0, 'IME key must not submit'); + // 合成期间 DOM 里有未提交的拼音,此时的 DOM 选区不能写回 Slate(由 compositionstart 负责)。 + assert.equal(test.state.adoptCalls, 0, 'IME key must not adopt the DOM selection'); assert.equal(handled, true, 'Slate keyboard fallthrough must be skipped'); assert.equal(test.event.defaultPrevented, false, 'native IME default must remain available'); assert.equal(test.event.propagationStopped, false); @@ -358,6 +369,25 @@ for (const directory of ['@src/', '@"my docs/"']) run('entering a directory keep assert.equal(changed.parts.some((part) => part.type === 'path'), false); }); +// 场景:Slate 的 selectionchange 同步被卡住(例如拖拽结束事件丢失),editor.selection 停在 +// 行首(标签之前),用户看到的光标却在 "@main.js " 末尾。按普通 Backspace。 +// 期望:handleKeyDown 先采纳 DOM 光标,父组件 onKeyDown 读到的是采纳后的选区,退格按用户 +// 看到的位置删除前面的标签。 +// 修复前:退格在过期的行首选区上执行,标签不删(同一根因下输入法文字也会落到过期位置)。 +run('ordinary keys adopt the visible DOM caret before parent and deletion handlers read the selection', () => { + const test = fixture({ kind: 'path', key: 'Backspace' }); + const visibleCaret = structuredClone(test.editor.selection); + Transforms.select(test.editor, Editor.start(test.editor, [])); + test.state.domCaret = visibleCaret; + let parentSelection = null; + test.context.onKeyDown = () => { parentSelection = structuredClone(test.editor.selection); }; + test.handleKeyDown(test.event); + assert.equal(test.state.adoptCalls, 1); + assert.deepEqual(parentSelection, visibleCaret); + assert.equal(test.event.defaultPrevented, true); + assert.equal(composerModel.composerTextFromDocument(test.editor.children), ''); +}); + run('queue editor lets Enter reach Slate when submitOnEnter is disabled', () => { const test = fixture({ key: 'Enter' }); test.context.submitOnEnter = false; diff --git a/web/src/lib/runTests.js b/web/src/lib/runTests.js index 41556c201..51391e034 100644 --- a/web/src/lib/runTests.js +++ b/web/src/lib/runTests.js @@ -1,3 +1,4 @@ +import './desktopOffice.test.js'; import './questionPicker.test.js'; import './turnCancellation.test.js'; import './modelReasoningSync.test.js'; @@ -97,6 +98,7 @@ import './composerLifecycle.test.js'; import './composerDraftCallbacks.test.js'; import './richComposerKeyboard.test.js'; import './richComposerFocus.test.js'; +import './composerDomSelection.test.js'; import './slateComposerArchitecture.test.js'; import './chatFileDropFeedbackArchitecture.test.js'; import './consoleDropPaths.test.js'; @@ -237,6 +239,7 @@ import './fileTypeIcons.test.js'; import './builtinCommandRouting.test.js'; import './slashCommandWorkspace.test.js'; import './desktopNotify.test.js'; +import './channelsSettings.test.js'; import './subagentCompletionNotificationArchitecture.test.js'; import './desktopNotificationMonitor.test.js'; import './desktopNotificationAuthorization.test.js'; diff --git a/web/src/lib/settingsNavigation.js b/web/src/lib/settingsNavigation.js index a5b9a59a9..19541f7c0 100644 --- a/web/src/lib/settingsNavigation.js +++ b/web/src/lib/settingsNavigation.js @@ -17,6 +17,7 @@ export const SETTINGS_NAV_GROUPS = [ { key: 'skills', label: '技能', icon: 'lightbulb' }, { key: 'mcp', label: 'MCP 服务器', icon: 'mcp' }, { key: 'connectors', label: '插件', icon: 'extension' }, + { key: 'channels', label: '消息通道', icon: 'chat' }, ], }, { diff --git a/web/src/lib/settingsNavigation.test.js b/web/src/lib/settingsNavigation.test.js index 057d2b29c..633681646 100644 --- a/web/src/lib/settingsNavigation.test.js +++ b/web/src/lib/settingsNavigation.test.js @@ -32,7 +32,7 @@ test('settings navigation uses the confirmed Codex-style groups', () => { { key: 'integrations', label: '集成', - items: ['技能', 'MCP 服务器', '插件'], + items: ['技能', 'MCP 服务器', '插件', '消息通道'], }, { key: 'coding', @@ -64,6 +64,7 @@ test('flattened settings routes remain unique and complete', () => { 'skills', 'mcp', 'connectors', + 'channels', 'models', 'tools', 'hooks', diff --git a/web/src/lib/settingsSearch.js b/web/src/lib/settingsSearch.js index ffa2a5946..45f365cc7 100644 --- a/web/src/lib/settingsSearch.js +++ b/web/src/lib/settingsSearch.js @@ -33,6 +33,13 @@ export function settingsSearchEntries(developerModeUnlocked = false) { ['personalization', '摘要模型', 'memory summary model 记忆摘要模型'], ['skills', '工作区 Skill 目录', 'workspace skill directory'], ['mcp', '服务器配置', 'mcp server config json'], + ['channels', 'QQ 机器人', 'qq bot 机器人 扫码 appid appsecret im channel 消息通道 连接 取消连接 owner approve revoke 机主 授权 审批 撤销'], + ['channels', '微信', 'weixin wechat 微信 扫码 ilink 机器人 im channel 消息通道 连接 取消连接'], + ['channels', '飞书', 'feishu lark 飞书 app id app secret 长连接 机器人 im channel 消息通道 连接 取消连接'], + ['channels', '钉钉', 'dingtalk 钉钉 stream client id client secret 机器人 im channel 消息通道 连接 取消连接'], + ['channels', 'Telegram', 'telegram bot token botfather 机器人 机主 im channel 消息通道 连接 取消连接 owner approve revoke 授权 审批 撤销'], + ['channels', 'Discord', 'discord bot token 机器人 服务器 im channel 消息通道 连接 取消连接'], + ['channels', 'LINE', 'line messaging api channel secret webhook cloudflared 机器人 im channel 消息通道 连接 取消连接'], ['tools', '内置工具', 'builtin tools'], ['tools', 'Agent 浏览器', 'agent browser'], ['tools', '电脑操控(实验性)', 'computer use experimental windows macos accessibility screen recording desktop mouse keyboard screenshot pointer cursor style theme color ace plain 指针 样式 主题色'], diff --git a/web/src/lib/toolIcons.js b/web/src/lib/toolIcons.js new file mode 100644 index 000000000..728563e6f --- /dev/null +++ b/web/src/lib/toolIcons.js @@ -0,0 +1,46 @@ +import { interfaceIconSvg } from './interfaceIcons.js'; + +const GROUPS = [ + ['OpenFile', ['file_read', 'read_file', 'read', 'list_dir', 'ls', 'read_resource']], + ['Save', ['file_write', 'write_file', 'write']], + ['Edit', ['file_edit', 'edit_file', 'edit', 'apply_patch', 'multiedit']], + ['Search', ['grep', 'file_search', 'glob', 'search_files', 'find_files']], + ['BrowserGlobe', ['web_search', 'web_fetch', 'fetch', 'browse', 'open_url']], + ['TerminalReadWrite', ['bash', 'shell', 'exec_command', 'execute_command', 'write_stdin']], + ['Embedding', ['spawn_subagent', 'wait_subagent', 'agent_spawn', 'agent_wait', 'agent_list', 'agent_send_message', 'agent_followup_task', 'agent_interrupt']], + ['StatusHelp', ['askuserquestion', 'ask_user_question']], + ['List', ['todowrite', 'todo_write', 'todo_read']], + ['StatusOK', ['task_complete']], + ['Eye', ['vision_analyze', 'show_image', 'view_image']], + ['Extension', ['skill', 'skill_view', 'skills_list']], +]; +const BY_TOOL = new Map(GROUPS.flatMap(([icon, names]) => names.map(name => [name, icon]))); +const LEGACY = new Map([['R','OpenFile'],['W','Save'],['E','Edit'],['S','Search'],['$','TerminalReadWrite'],['*','Tool'],['D','StatusOK'],['!','StatusWarning']]); + +// One mapping and SVG source for chat, background tasks and the native office. +export function toolIconName(tool = '', summaryIcon = '') { + const name = String(tool).toLowerCase().replace(/^functions\./, ''); + if (name.startsWith('mcp__') || name.startsWith('mcp:')) return 'MCP'; + return BY_TOOL.get(name) || LEGACY.get(summaryIcon) || 'Tool'; +} + +export function toolIconSvg(tool = '', summaryIcon = '') { + return interfaceIconSvg(toolIconName(tool, summaryIcon), 14); +} + +// Short present-tense verbs for the desktop office speech bubbles. +const VERB_BY_ICON = new Map([ + ['OpenFile', '读取'], ['Save', '写入文件'], ['Edit', '修改代码'], ['Search', '搜索代码'], + ['BrowserGlobe', '查阅网页'], ['TerminalReadWrite', '运行命令'], ['Embedding', '协作'], + ['StatusHelp', '提问'], ['List', '更新待办'], ['StatusOK', '收尾'], ['Eye', '看图'], + ['Extension', '使用技能'], ['MCP', '调用 MCP'], ['Tool', '调用工具'], +]); +const VERB_BY_TOOL = new Map([ + ['spawn_subagent', '派发任务'], ['agent_spawn', '派发任务'], ['wait_subagent', '等成员回报'], + ['agent_wait', '等成员回报'], ['agent_send_message', '发消息'], ['agent_followup_task', '追加任务'], + ['agent_interrupt', '叫停成员'], ['agent_list', '查看成员'], +]); +export function toolActivityVerb(tool = '') { + const name = String(tool).toLowerCase().replace(/^functions\./, ''); + return VERB_BY_TOOL.get(name) || VERB_BY_ICON.get(toolIconName(tool)) || '调用工具'; +} diff --git a/web/src/lib/useDesktopOffice.js b/web/src/lib/useDesktopOffice.js new file mode 100644 index 000000000..b135d8968 --- /dev/null +++ b/web/src/lib/useDesktopOffice.js @@ -0,0 +1,56 @@ +import { useEffect, useRef } from 'react'; +import { api } from './api.js'; +import { connection } from './connection.js'; +import { focusSession } from './desktopNotify.js'; +import { createDesktopOfficeController } from './desktopOfficeController.js'; + +export function useDesktopOffice(activeRef) { + const current = useRef(activeRef); + current.current = activeRef; + const controllerRef = useRef(null); + useEffect(() => { + if (typeof window.aceDesktop_updateOffice !== 'function') return undefined; + let disposed = false, controller = null; + const onMessage = event => controller?.onEvent(event.detail || {}); + const onOpen = () => controller?.reconnected(); + const onDisconnect = () => controller?.disconnected(); + const onAction = event => { + const action = event.detail || {}; + if (action.type === 'select') controller?.select(action); + else if (action.type === 'follow') controller?.setFollow(action.follow); + else if (action.type === 'open') focusSession(action.workspace_hash, action.session_id); + }; + const start = async () => { + let initial = {}; + try { + const value = await window.aceDesktop_getOfficeState?.(); + initial = typeof value === 'string' ? JSON.parse(value) : value || {}; + } catch {} + if (disposed) return; + controller = createDesktopOfficeController({ + initial, fetchSnapshot: api.getDesktopOffice, fetchModel: api.getSessionModel, + publish: state => { + try {Promise.resolve(window.aceDesktop_updateOffice(JSON.stringify(state))).catch(() => {});} catch {} + }, + retainSession: id => connection.retainSession(id), + releaseSession: id => connection.releaseSession(id), + }); + controllerRef.current = controller; + controller.setActive(current.current); + controller.start(); + }; + connection.addEventListener('message', onMessage); + connection.addEventListener('open', onOpen); + connection.addEventListener('disconnect', onDisconnect); + window.addEventListener('ace-desktop-office-action', onAction); + void start(); + return () => { + disposed = true; controller?.dispose(); controllerRef.current = null; + connection.removeEventListener('message', onMessage); + connection.removeEventListener('open', onOpen); + connection.removeEventListener('disconnect', onDisconnect); + window.removeEventListener('ace-desktop-office-action', onAction); + }; + }, []); + useEffect(() => {controllerRef.current?.setActive(activeRef);}, [activeRef]); +} diff --git a/web/src/styles/globals.css b/web/src/styles/globals.css index 85e40bcc8..3652c5234 100644 --- a/web/src/styles/globals.css +++ b/web/src/styles/globals.css @@ -4159,6 +4159,7 @@ a.ace-thread-link:focus-visible { } .ace-chat-tail-progress { + --ace-chat-tail-dot-lift: -3px; display: inline-flex; align-items: center; justify-content: center; @@ -4173,14 +4174,15 @@ a.ace-thread-link:focus-visible { background: currentColor; animation: ace-chat-tail-dot-hop 1.2s ease-in-out infinite; } -.ace-chat-tail-progress > span:nth-child(2) { animation-delay: .2s; } -.ace-chat-tail-progress > span:nth-child(3) { animation-delay: .4s; } +.ace-chat-tail-progress > span:nth-child(2) { animation-delay: .4s; } +.ace-chat-tail-progress > span:nth-child(3) { animation-delay: .8s; } @keyframes ace-chat-tail-dot-hop { - 0%, 16.667%, 100% { transform: translateY(0); } - 8.333% { transform: translateY(-3px); } + 0%, 33.333%, 100% { transform: translateY(0); } + 16.667% { transform: translateY(var(--ace-chat-tail-dot-lift)); } } @media (prefers-reduced-motion: reduce) { - .ace-chat-tail-progress > span { animation: none; } + /* Keep the live-work signal visible without the full bounce amplitude. */ + .ace-chat-tail-progress { --ace-chat-tail-dot-lift: -1.5px; } } /* hover actions on chat messages (复制 / 分叉) */