fix(desktop): refine chat ui and persistence

This commit is contained in:
dd3xp
2026-07-06 10:53:40 -04:00
parent aca66785ba
commit 1bcb41d77a
7 changed files with 1021 additions and 565 deletions
+280 -435
View File
@@ -306,355 +306,8 @@ let bridgeUiOffline = false;
})();
/* ═══════════════ i18n ═══════════════ */
const I18N = {
zh: {
'app.title': 'GenericAgent 桌面版',
'brand.sub': '桌面终端',
'nav.chat': '聊天', 'nav.services': '后台服务', 'nav.channels': '消息通道', 'nav.status': '状态面板',
'nav.collab': '指挥家', 'nav.token': '用量',
'foot.settings': '配置', 'foot.ver': 'GenericAgent · 桌面版',
'chat.startTitle': '开始对话', 'chat.startSub': '直接输入,或点预设功能一键启动',
'preset.butler.t': '指挥家', 'preset.butler.d': '复杂任务自动拆解,只需查看进度和简报',
'preset.plan.t': 'Plan 模式', 'preset.plan.d': '加载 Plan SOP,按探索→规划→执行→验证流程',
'preset.goal.t': 'Goal 模式', 'preset.goal.d': '设定目标,自主完成',
'preset.autonomous.t': '自主行动', 'preset.autonomous.d': '按 SOP 规划/执行任务,产出报告(reflect/autonomous.py 同源)',
'preset.hive.t': 'Hive 协作', 'preset.hive.d': '多 worker 协同攻坚',
'preset.review.t': '深度复核', 'preset.review.d': '挑刺式质量把关',
'preset.findwork.t': '找点事做', 'preset.findwork.d': '分析当前情况,推荐一批让你感兴趣的 TODO',
'preset.mine.t': '我的·周报', 'preset.mine.d': '自定义:抓本周提交并写周报',
'preset.add.t': '自定义', 'preset.add.d': '任意一句话存为功能',
'composer.placeholder': 'GA 能帮你做些什么?',
'search.placeholder': '搜索会话…', 'conv.new': '新对话',
'ctx.pin': '置顶', 'ctx.unpin': '取消置顶', 'ctx.rename': '重命名', 'ctx.del': '删除',
'common.close': '关闭', 'common.more': '更多', 'common.optional': '选填', 'common.save': '保存',
'modal.preset': '预设功能', 'modal.addModel': '添加模型', 'modal.editModel': '编辑模型', 'modal.settings': '配置',
'modal.customPreset': '自定义预设',
'modal.editCustomPreset': '编辑任务',
'customPreset.titlePh': '标题,例如「写周报」',
'customPreset.promptPh': 'Prompt 内容,发送时会作为消息提交',
'customPreset.empty': '标题和 Prompt 不能为空',
'customPreset.removeTitle': '删除',
'customPreset.editTitle': '编辑',
'builtinPreset.restoreBtn': '恢复默认预设',
'set.appearance': '外观', 'set.plainUi': '素色', 'set.fontSize': '聊天字号', 'set.lang': '语言', 'set.model': '模型', 'set.addModel': '添加模型', 'set.features': '功能', 'set.importMykey': '导入已有模型配置(mykey.py', 'set.exportMykey': '导出当前模型配置', 'set.serviceManager': '后台服务管理',
'shortcut.askConfirm': '是否在桌面创建 GenericAgent 快捷方式?',
'appearance.light': '浅色', 'appearance.dark': '深色',
'set.noModels': '暂无模型,点击下方添加',
'lang.zh': '简体中文', 'lang.en': 'English',
'model.name': '备注', 'model.namePh': '会显示在模型列表',
'model.apikey': 'API Key', 'model.apikeyPh': 'sk-...', 'model.apikeyKeep': '留空则保持原 Key 不变',
'model.apibase': 'API 地址', 'model.apibasePh': 'https://.../v1/messages',
'model.protocol': '协议', 'model.protocolPick': '请选择…', 'model.protocolOai': 'OpenAI 兼容 (chat/completions)', 'model.protocolClaude': 'Anthropic (Claude /v1/messages)',
'model.stream': '响应方式', 'model.streamOn': '流式', 'model.streamOff': '非流式',
'model.model': '模型', 'model.modelPh': 'model 参数名',
'model.modelHint': '须与中转站/官方文档中的 model 字段完全一致',
'model.retries': '重试 (次)', 'model.connTimeout': '连接超时 (s)', 'model.readTimeout': '读取超时 (s)',
'model.save': '保存', 'common.cancel': '取消', 'common.confirm': '确认', 'common.edit': '编辑', 'common.delete': '删除',
'pq.title': '快速接入官方模型', 'pq.sub': '填好 API Key 即可使用', 'pq.toggle': '展开 / 收起',
'pq.deepseekDesc': '官方 API · OpenAI 兼容', 'pq.qwenDesc': '通义千问 · 阿里云百炼',
'guide.step1': '点击下方链接,登录后创建并复制 API Key',
'guide.step2': '把 Key 粘贴到下方「API Key」输入框',
'guide.step3': '点击保存,即可在模型列表中选用',
'guide.prefillTip': '已为你预填 API 地址、协议与模型,可按需修改',
'guide.getKey': '获取 {name} 的 API Key', 'guide.copy': '复制链接', 'guide.copied': '链接已复制',
'err.modelSave': '保存失败', 'err.modelSwitch': '切换模型失败', 'err.modelRequired': '请填写模型、API Key 和 API 地址',
'err.modelDelete': '删除失败', 'err.modelDeleteLast': '至少保留一个模型',
'confirm.modelDelete': '确定删除该模型配置?',
'model.aggregation': '渠道组(自动故障转移)', 'model.aggregationShort': '渠道组', 'model.aggregationDesc': '按顺序尝试,失败自动切换到下一个',
'model.emptyMixin': '尚未加入模型',
'model.addToMixin': '加入渠道组', 'model.inMixin': '已在渠道组', 'model.removeFromMixin': '移出渠道组', 'model.alreadyInMixin': '已在渠道组中', 'model.dragReorder': '拖拽调整顺序',
'err.mixinFailed': '操作失败',
'page.services.title': '后台服务', 'page.services.sub': 'IM 消息通道与后台进程,集中查看、启停与日志',
'page.channels.title': '消息通道', 'page.channels.sub': '后台 IM 进程:列表、启停与日志(同 hub.pyw)',
'page.status.title': '状态面板', 'page.status.sub': 'hub.pyw 管理的后台进程/服务,集中查看与启停',
'page.collab.title': '指挥家', 'page.collab.sub': '交代目标,自动拆活与跟进',
'collab.progressTitle': '分工进度',
'collab.progressEmpty': '还没有任务在执行。告诉指挥家你的目标后,这里会显示拆分后的处理进度。',
'collab.placeholder': '请对指挥家描述你想完成的目标',
'collab.guideTitle': '把要完成的事告诉指挥家',
'collab.guideWhen': '适合需要多步处理、要花一些时间才能完成的目标。日常聊天和快问快答,请用左侧「聊天」。',
'collab.guideStep1t': '描述目标',
'collab.guideStep1d': '在聊天框里写下你想做的事,发给指挥家',
'collab.guideStep2t': '自动拆解',
'collab.guideStep2d': '指挥家自动拆解、分配任务,实时监督和调度',
'collab.guideStep3t': '交付摘要',
'collab.guideStep3d': '指挥家根据执行状态,呈上任务简报',
'collab.guideStep4t': '随时调整',
'collab.guideStep4d': '随时补充要求或细节,指挥家都会处理',
'collab.chipProgress': '现在进展如何?',
'collab.chipPause': '先暂停当前任务',
'collab.chipSummary': '总结一下目前的结果',
'collab.showProgressTitle': '查看分工进度',
'collab.statRunning': '进行中',
'collab.statDone': '已完成',
'collab.plusMenu': '更多操作',
'collab.switchMode': '切换模式',
'collab.typing': '指挥家正在处理',
'collab.offline': '无法连接指挥家服务,请确认后端已启动。',
'collab.retry': '重试',
'collab.reconnect': '连接断开,正在重连… 已保留上次任务进度。',
'collab.reconnectIn': '{n} 秒后重试',
'collab.stRunning': '执行中', 'collab.stReported': '已回报', 'collab.stPaused': '已暂停',
'collab.stFailed': '遇到问题', 'collab.stTerminated': '已终止',
'collab.summaryRunning': '正在处理中…', 'collab.summaryWait': '等待回报',
'collab.taskFallback': '任务 {n}',
'collab.timeJust': '刚刚',
'collab.timeSec': '{n} 秒前',
'collab.timeMin': '{n} 分钟前',
'collab.timeHr': '{n} 小时前',
'collab.timeDay': '{n} 天前',
'page.token.title': '用量', 'page.token.sub': '每会话与累计用量及缓存率',
'status.connecting': '正在连接…', 'status.ready': '服务在线', 'status.running': '处理中',
'status.disconnected': '服务离线', 'status.stopped': '已停止', 'status.idle': '待命',
'conv.emptyList': '暂无会话,点「+ 新对话」开始', 'conv.defaultTitle': '新对话',
'err.bridge': '服务未响应', 'err.newSession': '新建会话失败', 'err.poll': '轮询失败', 'err.stop': '停止失败',
'err.interruptTimeout': '等待上一轮停止超时,请稍后再试',
'sys.interruptPrev.hint': '已停止上一轮,正在处理新消息',
'chat.interrupting': '正在停止上一轮…',
'chat.sessionLoading': '正在加载会话…',
'sys.stopRequested': '已请求停止',
'slash.help': '可用命令:\n/new 新会话 /clear 清屏 /stop 停止 /settings 设置',
'slash.unknown': '未知命令',
'upload.hint': '上传文件:选择 / 拖拽 / 粘贴',
'upload.button': '上传文件',
'upload.tooLarge': '文件过大或数量超限', 'upload.empty': '跳过空文件',
'upload.failed': '上传失败',
'err.charLimit': '已达字数上限({n}),发送时将自动截断', 'err.charLimitReached': '已达字数上限({n}', 'err.numMax': '不能超过 {n}',
'file.openFailed': '无法打开文件',
'file.kindGeneric': '文件',
'file.kindDoc': '文档',
'file.kindSheet': '表格',
'file.kindSlide': '幻灯片',
'file.kindCode': '代码',
'file.kindArchive': '压缩包',
'file.kindAudio': '音频',
'file.kindVideo': '视频',
'upload.removeTitle': '移除',
'upload.dropHint': '松开以上传文件',
'lightbox.closeTitle': '关闭',
'fold.thinking': '思考', 'fold.tool': '工具调用', 'fold.toolResult': '工具结果', 'fold.llm': 'LLM Running', 'fold.turn': '第 {n} 轮',
'plan.header': '计划 ({done}/{total})', 'plan.complete': '✓ 计划完成 ({n}/{n})',
'plan.running': '计划执行中', 'plan.completeTitle': '计划完成',
'plan.placeholder': '计划模式已激活', 'plan.waiting': '等待写入 {path} …', 'plan.overflow': '还有 {n} 项',
'plan.current': '当前', 'plan.fold': '折叠', 'plan.collapse': '收起', 'plan.expand': '展开', 'plan.details': '详情',
'plan.capsuleRunning': '运行中', 'plan.capsuleComplete': '已完成',
'timing.elapsed': '已运行 {t}',
'model.auto': '自动选择',
'model.menuLabel': '选择模型',
'chip.plan': 'Plan',
'chip.auto': 'Auto',
'ch.wechat': '微信', 'ch.wecom': '企业微信', 'ch.lark': '飞书', 'ch.dingtalk': '钉钉',
'ch.qq': 'QQ', 'ch.telegram': 'Telegram', 'ch.discord': 'Discord',
'ch.loading': '加载中…', 'ch.empty': '未发现 IM 进程脚本',
'ch.logEmpty': '暂无日志',
'err.channelLoad': '加载失败', 'err.channelStart': '启动失败', 'err.channelStop': '停止失败',
'err.mykeyImport': '导入模型配置失败',
'err.mykeyExport': '导出模型配置失败',
'err.channelNotConfigured': '请先在 mykey.py 中配置该平台',
'sys.channelStarted': '已启动', 'sys.channelStopped': '已停止',
'modal.channelLogs': '进程日志',
'modal.mykeyConfig': 'mykey.py 配置',
'sys.configSaved': '配置已保存',
'sys.mykeyImported': '模型配置已导入',
'sys.mykeyExported': '模型配置已导出',
'st.starting': '启动中…', 'st.stopping': '停止中…', 'st.online': '在线', 'st.offline': '离线', 'st.error': '错误', 'st.running': '运行', 'st.abnormal': '异常',
'act.configure': '配置', 'act.logs': '日志', 'act.restart': '重启', 'act.stop': '停止', 'act.start': '启动', 'act.exit': '退出',
'act.copy': '复制', 'act.copied': '已复制', 'act.copyTex': 'TeX', 'act.send': '发送',
'proc.imbotWechat': 'imbot · 微信', 'proc.imbotDing': 'imbot · 钉钉', 'proc.scheduler': '定时任务调度', 'proc.conductor': '指挥家',
'cm.scheduling': '调度中', 'cm.running': '执行中', 'cm.idleSt': '空闲',
'cm.master': '已派 3 子任务', 'cm.w1': '子任务:抓取数据', 'cm.w2': '子任务:复核结果', 'cm.sub': '等待派单',
'tok.total': '累计', 'tok.cost': '缓存率', 'tok.today': '今日', 'tok.tabAll': '聊天', 'tok.tabConductor': '指挥家', 'tok.condTotal': '指挥家累计', 'tok.condCurrent': '指挥家本次', 'tok.condTip': '指挥家消耗不计入聊天累计', 'tok.condOffline': '指挥家服务离线', 'tok.disclaimer': '不同 API 网站的计费价格可能会有差异,请以实际网站为准。',
'tok.colSession': '会话', 'tok.colIn': '输入', 'tok.colOut': '输出', 'tok.colCacheW': '缓存写入', 'tok.colCache': '缓存读取', 'tok.colCost': '成本',
'tok.from': '从', 'tok.to': '到', 'tok.reset': '重置', 'tok.noData': '暂无记录', 'tok.deleted': '此会话已删除',
'tok.pricingUnknown': '⚠ 此模型计费规则尚未明确,按默认估算',
'tok.priceInput': '输入: $', 'tok.priceOutput': '输出: $',
'tok.priceCacheW': '缓存写入: $', 'tok.priceCacheR': '缓存读取: $',
'presetPrompt.goal': '进入 Goal 模式:读 L3 goal mode SOP,自主达成我接下来描述的目标。',
'presetPrompt.plan': '进入 Plan 模式:先读 memory/plan_sop.md,按其中「探索→规划→执行→验证」流程,等我接下来描述要做的任务。',
'presetPrompt.autonomous': '🤖 进入自主行动模式:阅读 memory/autonomous_operation_sop.md,按 SOP 选取或规划任务,独立执行并产出报告。',
'presetPrompt.hive': '启动 Goal Hive 模式:按 hive SOP 拉起多个 worker 协同完成我接下来的目标。',
'presetPrompt.review': '进入监察者模式:对刚才的产出严格挑刺、逐项复核并报告问题。',
'presetPrompt.findwork': '按照自主行动的规划部分,充分分析我的情况,给我生成一批 TODO,务必让我感兴趣。',
'presetPrompt.mine': '抓取本周的 git 提交并写一份周报。',
'ask.banner': 'GA 等你回答',
'ask.replyHint': '在下方输入框回复',
'ask.placeholderOpen': '在此输入你的回答… (Enter 发送)',
},
en: {
'app.title': 'GenericAgent Desktop',
'brand.sub': 'Desktop terminal',
'nav.chat': 'Chat', 'nav.services': 'Services', 'nav.channels': 'Channels', 'nav.status': 'Status',
'nav.collab': 'Conductor', 'nav.token': 'Usage',
'foot.settings': 'Settings', 'foot.ver': 'GenericAgent · Desktop',
'chat.startTitle': 'Start a conversation', 'chat.startSub': 'Type a message, or pick a preset',
'preset.butler.t': 'Conductor', 'preset.butler.d': 'Auto-decompose complex tasks; just check progress and briefings',
'preset.plan.t': 'Plan mode', 'preset.plan.d': 'Load Plan SOP — explore→plan→execute→verify',
'preset.goal.t': 'Goal mode', 'preset.goal.d': 'Set a goal, run autonomously',
'preset.autonomous.t': 'Autonomous mode', 'preset.autonomous.d': 'Plan/execute tasks per SOP and produce reports (same as reflect/autonomous.py)',
'preset.hive.t': 'Hive', 'preset.hive.d': 'Multi-worker collaboration',
'preset.review.t': 'Deep review', 'preset.review.d': 'Strict quality check',
'preset.findwork.t': 'Find me work', 'preset.findwork.d': 'Analyze my context and suggest a batch of interesting TODOs',
'preset.mine.t': 'My · Weekly', 'preset.mine.d': 'Custom: weekly report from commits',
'preset.add.t': 'Custom', 'preset.add.d': 'Save any prompt as a function',
'composer.placeholder': 'What can GA do for you?',
'search.placeholder': 'Search chats…', 'conv.new': 'New chat',
'ctx.pin': 'Pin', 'ctx.unpin': 'Unpin', 'ctx.rename': 'Rename', 'ctx.del': 'Delete',
'common.close': 'Close', 'common.more': 'More', 'common.optional': 'Optional', 'common.save': 'Save',
'modal.preset': 'Presets', 'modal.addModel': 'Add model', 'modal.editModel': 'Edit model', 'modal.settings': 'Settings',
'modal.customPreset': 'Custom preset',
'modal.editCustomPreset': 'Edit task',
'customPreset.titlePh': 'Title, e.g. "Weekly report"',
'customPreset.promptPh': 'Prompt body — sent as the message when clicked',
'customPreset.empty': 'Title and Prompt cannot be empty',
'customPreset.removeTitle': 'Delete',
'customPreset.editTitle': 'Edit',
'builtinPreset.restoreBtn': 'Restore defaults',
'set.appearance': 'Appearance', 'set.plainUi': 'Plain', 'set.fontSize': 'Chat font size', 'set.lang': 'Language', 'set.model': 'Model', 'set.addModel': 'Add model', 'set.features': 'Features', 'set.importMykey': 'Import model config (mykey.py)', 'set.exportMykey': 'Export current model config', 'set.serviceManager': 'Service manager',
'shortcut.askConfirm': 'Create a desktop shortcut for GenericAgent?',
'appearance.light': 'Light', 'appearance.dark': 'Dark',
'set.noModels': 'No models yet — add one below',
'lang.zh': '简体中文', 'lang.en': 'English',
'model.name': 'Note', 'model.namePh': 'Shown in the model list',
'model.apikey': 'API Key', 'model.apikeyPh': 'sk-...', 'model.apikeyKeep': 'Leave blank to keep the current key',
'model.apibase': 'API base URL', 'model.apibasePh': 'https://.../v1/messages',
'model.protocol': 'Protocol', 'model.protocolPick': 'Select…', 'model.protocolOai': 'OpenAI-compatible (chat/completions)', 'model.protocolClaude': 'Anthropic (Claude /v1/messages)',
'model.stream': 'Response', 'model.streamOn': 'Stream', 'model.streamOff': 'Non-stream',
'model.model': 'Model', 'model.modelPh': 'model parameter name',
'model.modelHint': 'Must match the model field in your provider docs exactly',
'model.retries': 'Retries (×)', 'model.connTimeout': 'Connect (s)', 'model.readTimeout': 'Read (s)',
'model.save': 'Save', 'common.cancel': 'Cancel', 'common.confirm': 'Confirm', 'common.edit': 'Edit', 'common.delete': 'Delete',
'pq.title': 'Quick connect a model', 'pq.sub': 'Add your API key to get started', 'pq.toggle': 'Expand / collapse',
'pq.deepseekDesc': 'Official API · OpenAI-compatible', 'pq.qwenDesc': 'Tongyi Qwen · Aliyun Bailian',
'guide.step1': 'Open the link, sign in, then create & copy your API key',
'guide.step2': 'Paste the key into the “API Key” field below',
'guide.step3': 'Click Save — then pick it from the model list',
'guide.prefillTip': 'API base, protocol and model are pre-filled — edit if needed',
'guide.getKey': 'Get your {name} API key', 'guide.copy': 'Copy link', 'guide.copied': 'Link copied',
'err.modelSave': 'Save failed', 'err.modelSwitch': 'Failed to switch model', 'err.modelRequired': 'Model, API Key and base URL are required',
'err.modelDelete': 'Delete failed', 'err.modelDeleteLast': 'At least one model is required',
'confirm.modelDelete': 'Delete this model profile?',
'model.aggregation': 'Channel group (auto failover)', 'model.aggregationShort': 'Channel group', 'model.aggregationDesc': 'Tries in order, switches to the next on failure',
'model.emptyMixin': 'No models added yet',
'model.addToMixin': 'Add to channel', 'model.inMixin': 'In channel', 'model.removeFromMixin': 'Remove from channel', 'model.alreadyInMixin': 'Already in the channel', 'model.dragReorder': 'Drag to reorder',
'err.mixinFailed': 'Operation failed',
'page.services.title': 'Services', 'page.services.sub': 'IM channels and background processes — view, start/stop, logs',
'page.channels.title': 'Channels', 'page.channels.sub': 'Background IM processes: list, start/stop, logs (hub.pyw style)',
'page.status.title': 'Status', 'page.status.sub': 'Background processes/services managed by hub.pyw',
'page.collab.title': 'Conductor', 'page.collab.sub': 'Describe a goal — split, delegate, and follow up',
'collab.progressTitle': 'Progress',
'collab.progressEmpty': 'No tasks running yet. After you describe a goal to Conductor, split tasks will appear here.',
'collab.placeholder': 'Describe the goal you want to accomplish',
'collab.guideTitle': 'Tell Conductor what you want done',
'collab.guideWhen': 'Best for multi-step goals that take a while. For everyday chat and quick questions, use Chat in the sidebar.',
'collab.guideStep1t': 'Describe your goal',
'collab.guideStep1d': 'Write what you want done in the chat box and send it to Conductor',
'collab.guideStep2t': 'Auto breakdown',
'collab.guideStep2d': 'Conductor breaks down, assigns, monitors, and coordinates',
'collab.guideStep3t': 'Summary',
'collab.guideStep3d': 'Conductor delivers a briefing based on execution status',
'collab.guideStep4t': 'Adjust anytime',
'collab.guideStep4d': 'Add requirements or details anytime — Conductor handles them',
'collab.chipProgress': 'How is it going?',
'collab.chipPause': 'Pause current tasks',
'collab.chipSummary': 'Summarize progress so far',
'collab.showProgressTitle': 'View task progress',
'collab.statRunning': 'Running',
'collab.statDone': 'Done',
'collab.plusMenu': 'More actions',
'collab.switchMode': 'Switch mode',
'collab.typing': 'Conductor is working',
'collab.offline': 'Cannot reach the service. Make sure the backend is running.',
'collab.retry': 'Retry',
'collab.reconnect': 'Disconnected — reconnecting… Your last progress is kept.',
'collab.reconnectIn': 'Retry in {n}s',
'collab.stRunning': 'Running', 'collab.stReported': 'Reported', 'collab.stPaused': 'Paused',
'collab.stFailed': 'Issue', 'collab.stTerminated': 'Ended',
'collab.summaryRunning': 'Working…', 'collab.summaryWait': 'Awaiting report',
'collab.taskFallback': 'Task {n}',
'collab.timeJust': 'just now',
'collab.timeSec': '{n}s ago',
'collab.timeMin': '{n}m ago',
'collab.timeHr': '{n}h ago',
'collab.timeDay': '{n}d ago',
'page.token.title': 'Usage', 'page.token.sub': 'Per-session and total usage & cache rate',
'status.connecting': 'Connecting…', 'status.ready': 'Service online', 'status.running': 'Working…',
'status.disconnected': 'Service offline', 'status.stopped': 'Stopped', 'status.idle': 'Standby',
'conv.emptyList': 'No chats yet — click “+ New chat”', 'conv.defaultTitle': 'New chat',
'err.bridge': 'Service not responding', 'err.newSession': 'Failed to create session', 'err.poll': 'Polling failed', 'err.stop': 'Stop failed',
'err.interruptTimeout': 'Timed out waiting for the previous reply to stop — try again',
'sys.interruptPrev.hint': 'Previous reply stopped — processing new message',
'chat.interrupting': 'Stopping previous reply…',
'chat.sessionLoading': 'Loading conversation…',
'sys.stopRequested': 'Stop requested',
'slash.help': 'Commands:\n/new new chat /clear clear /stop stop /settings settings',
'slash.unknown': 'Unknown command',
'upload.hint': 'Upload file: pick / drag / paste',
'upload.button': 'Upload file',
'upload.tooLarge': 'File too large or limit reached', 'upload.empty': 'Skipped empty file',
'upload.failed': 'Upload failed',
'err.charLimit': 'Character limit reached ({n}), text will be truncated on send', 'err.charLimitReached': 'Character limit reached ({n})', 'err.numMax': 'Cannot exceed {n}',
'file.openFailed': 'Cannot open file',
'file.kindGeneric': 'File',
'file.kindDoc': 'Document',
'file.kindSheet': 'Spreadsheet',
'file.kindSlide': 'Slides',
'file.kindCode': 'Code',
'file.kindArchive': 'Archive',
'file.kindAudio': 'Audio',
'file.kindVideo': 'Video',
'upload.removeTitle': 'Remove',
'upload.dropHint': 'Drop to upload files',
'lightbox.closeTitle': 'Close',
'fold.thinking': 'Thinking', 'fold.tool': 'Tool call', 'fold.toolResult': 'Tool result', 'fold.llm': 'LLM Running', 'fold.turn': 'Turn {n}',
'plan.header': 'Plan ({done}/{total})', 'plan.complete': '✓ Plan complete ({n}/{n})',
'plan.running': 'Running plan', 'plan.completeTitle': 'Plan complete',
'plan.placeholder': 'Plan mode activated', 'plan.waiting': 'waiting for {path} …', 'plan.overflow': '+{n} more',
'plan.current': 'Now', 'plan.fold': 'Fold', 'plan.collapse': 'Collapse', 'plan.expand': 'Expand', 'plan.details': 'Details',
'plan.capsuleRunning': 'Running', 'plan.capsuleComplete': 'Done',
'timing.elapsed': 'Elapsed {t}',
'model.auto': 'Auto',
'model.menuLabel': 'Select model',
'chip.plan': 'Plan',
'chip.auto': 'Auto',
'ch.wechat': 'WeChat', 'ch.wecom': 'WeCom', 'ch.lark': 'Lark', 'ch.dingtalk': 'DingTalk',
'ch.qq': 'QQ', 'ch.telegram': 'Telegram', 'ch.discord': 'Discord',
'ch.loading': 'Loading…', 'ch.empty': 'No IM process scripts found',
'ch.logEmpty': 'No log output yet',
'err.channelLoad': 'Failed to load', 'err.channelStart': 'Start failed', 'err.channelStop': 'Stop failed',
'err.mykeyImport': 'Failed to import model config',
'err.mykeyExport': 'Failed to export model config',
'err.channelNotConfigured': 'Configure this platform in mykey.py first',
'sys.channelStarted': 'Started', 'sys.channelStopped': 'Stopped',
'modal.channelLogs': 'Process logs',
'modal.mykeyConfig': 'mykey.py',
'sys.configSaved': 'Configuration saved',
'sys.mykeyImported': 'Model config imported',
'sys.mykeyExported': 'Model config exported',
'st.starting': 'Starting…', 'st.stopping': 'Stopping…', 'st.online': 'Online', 'st.offline': 'Offline', 'st.error': 'Error', 'st.running': 'Running', 'st.abnormal': 'Error',
'act.configure': 'Configure', 'act.logs': 'Logs', 'act.restart': 'Restart', 'act.stop': 'Stop', 'act.start': 'Start', 'act.exit': 'Exit',
'act.copy': 'Copy', 'act.copied': 'Copied', 'act.copyTex': 'TeX', 'act.send': 'Send',
'proc.imbotWechat': 'imbot · WeChat', 'proc.imbotDing': 'imbot · DingTalk', 'proc.scheduler': 'Scheduler', 'proc.conductor': 'Conductor',
'cm.scheduling': 'Scheduling', 'cm.running': 'Running', 'cm.idleSt': 'Idle',
'cm.master': 'Dispatched 3 subtasks', 'cm.w1': 'Subtask: fetch data', 'cm.w2': 'Subtask: review results', 'cm.sub': 'Waiting for tasks',
'tok.total': 'Total', 'tok.cost': 'Cache rate', 'tok.today': 'Today', 'tok.tabAll': 'Chat', 'tok.tabConductor': 'Conductor', 'tok.condTotal': 'Conductor Total', 'tok.condCurrent': 'Conductor Current', 'tok.condTip': 'Conductor usage is not included in chat totals', 'tok.condOffline': 'Service offline', 'tok.disclaimer': 'Pricing may vary by API provider. Please refer to the actual website.',
'tok.colSession': 'Session', 'tok.colIn': 'Input', 'tok.colOut': 'Output', 'tok.colCacheW': 'Cache write', 'tok.colCache': 'Cache read', 'tok.colCost': 'Cost',
'tok.from': 'From', 'tok.to': 'To', 'tok.reset': 'Reset', 'tok.noData': 'No records', 'tok.deleted': 'Session deleted',
'tok.pricingUnknown': '⚠ Pricing not confirmed, using defaults',
'tok.priceInput': 'Input: $', 'tok.priceOutput': 'Output: $',
'tok.priceCacheW': 'Cache write: $', 'tok.priceCacheR': 'Cache read: $',
'presetPrompt.goal': 'Enter Goal mode: read the L3 goal-mode SOP and autonomously achieve the goal I describe next.',
'presetPrompt.plan': 'Enter Plan mode: first read memory/plan_sop.md, follow its explore→plan→execute→verify flow, and wait for the task I describe next.',
'presetPrompt.autonomous': '🤖 Enter autonomous mode: read memory/autonomous_operation_sop.md, follow the SOP to pick or plan a task, execute independently, and produce a report.',
'presetPrompt.hive': 'Start Goal Hive mode: per the hive SOP, spawn multiple workers to collaboratively achieve the goal I describe next.',
'presetPrompt.review': 'Enter reviewer mode: strictly scrutinize the previous output, review item by item and report issues.',
'presetPrompt.findwork': 'Following the autonomous planning section, analyze my situation thoroughly and generate a batch of TODOs that genuinely interest me.',
'presetPrompt.mine': 'Collect this week\'s git commits and write a weekly report.',
'ask.banner': 'GA is waiting for your answer',
'ask.replyHint': 'Reply in the input below',
'ask.placeholderOpen': 'Type your answer here… (Enter to send)',
},
};
const LANGS = ['zh', 'en'];
const I18N = window.GA_I18N?.dict || { zh: {}, en: {} };
const LANGS = window.GA_I18N?.languages || ['zh', 'en'];
const STORE = { lang: 'ga_lang', theme: 'ga_theme', appearance: 'ga_appearance', plain: 'ga_plain', fontSize: 'ga_font_size' };
const APPEARANCE_IDS = ['light', 'dark'];
const CHAT_FONT_MIN = 10;
@@ -721,7 +374,14 @@ async function bridgeFetch(path, opts = {}) {
if (!res.ok) throw new Error(data.error || data.message || res.statusText);
return data;
}
function t(key) { return (I18N[lang] && I18N[lang][key]) || (I18N.zh[key]) || key; }
function t(key, vars) {
if (window.GA_I18N?.t) return window.GA_I18N.t(lang, key, vars);
let text = (I18N[lang] && I18N[lang][key]) || (I18N.zh && I18N.zh[key]) || key;
if (vars) {
for (const [name, value] of Object.entries(vars)) text = text.replaceAll(`{${name}}`, String(value));
}
return text;
}
window.gaT = t;
document.addEventListener('collab:running-count', e => {
const b = document.getElementById('collab-badge');
@@ -734,6 +394,22 @@ function optionalPh(key) {
const sep = (lang === 'en') ? ', ' : '';
return `${t('common.optional')}${sep}${t(key)}`;
}
function setTooltip(el, text) {
if (!el) return;
const value = String(text || '').trim();
el.removeAttribute('title');
if (el.hasAttribute('data-no-tooltip')) {
delete el.dataset.tooltip;
if (value) el.setAttribute('aria-label', value);
return;
}
if (value) {
el.dataset.tooltip = value;
if (!el.hasAttribute('aria-label')) el.setAttribute('aria-label', value);
} else {
delete el.dataset.tooltip;
}
}
function applyI18n() {
document.documentElement.lang = (lang === 'en') ? 'en' : 'zh-CN';
document.title = t('app.title');
@@ -744,7 +420,8 @@ function applyI18n() {
if (el.isContentEditable) el.setAttribute('data-ph', val); // contenteditable 用 :empty::before 显示
else el.setAttribute('placeholder', val);
});
document.querySelectorAll('[data-i18n-title]').forEach(el => { el.setAttribute('title', t(el.dataset.i18nTitle)); });
document.querySelectorAll('[data-i18n-title]').forEach(el => { setTooltip(el, t(el.dataset.i18nTitle)); });
migrateNativeTooltips();
renderLangList();
// 语言切换后重算激活模型 chip 文案;若当前会话已有渠道组运行态模型,保留运行态而非退回首选项
const _ap = (state.modelProfiles || []).find(p => (p.id ?? 0) === state.llmNo);
@@ -1738,7 +1415,7 @@ const state = {
};
function rt(sess) {
let r = state.runtime.get(sess.id);
if (!r) { r = { polling:false, busy:false, lastId:0, seen:new Set(), draftEl:null, draftSegs:null, draftTurn:0, taskStartedAt:null, taskEndedAt:null, taskTimerId:null, planCompleteAt:null, planLostAt:null, planHoldItems:[], planLastPayload:null, planLastComplete:false, planHideTimer:null, planDismissedComplete:false, planCollapsed:false, planShowAll:false }; state.runtime.set(sess.id, r); }
if (!r) { r = { polling:false, busy:false, lastId:0, seen:new Set(), draftEl:null, draftSegs:null, draftTurn:0, taskStartedAt:null, taskEndedAt:null, taskTimerId:null, planCompleteAt:null, planLostAt:null, planHoldItems:[], planLastPayload:null, planVisiblePayload:null, planLastComplete:false, planHideTimer:null, planDismissedComplete:false, planCollapsed:false, planShowAll:false }; state.runtime.set(sess.id, r); }
return r;
}
const activeSess = () => state.sessions.get(state.activeId) || null;
@@ -1786,7 +1463,7 @@ const MIN_MSG_LOADING_MS = 450;
const HYDRATE_LOADING_TIMEOUT_MS = 10000;
const POLL_MSG_LIMIT = 200;
const PLAN_LOST_GRACE_MS = 1500; // tuiapp_v2._PLAN_LOST_GRACE_SEC
const PLAN_COMPLETE_GRACE_MS = 3000; // tuiapp_v2._PLAN_GRACE_SEC
const PLAN_DISMISS_STORE = 'ga_plan_dismissed_v1';
let _submitInFlight = false;
const runToggle = document.getElementById('run-toggle');
@@ -1861,6 +1538,43 @@ function planTpl(tpl, v) {
return String(tpl || '').replace(/\{(\w+)\}/g, (_, k) => (v[k] != null ? String(v[k]) : `{${k}}`));
}
function planItemsSignature(items) {
return (items || [])
.map(it => `${String(it.status || '')}\u001e${String(it.content || '')}`)
.join('\u001f');
}
function planDismissId(sess, plan) {
if (!sess || !plan) return '';
const total = plan.total ?? (plan.items || []).length;
const done = plan.done ?? (plan.items || []).filter(it => it.status === 'done').length;
return `${sess.id || sess.bridgeSessionId || 'session'}\u001d${done}/${total}\u001d${planItemsSignature(plan.items || [])}`;
}
function readPlanDismissed() {
try {
const raw = localStorage.getItem(PLAN_DISMISS_STORE);
const parsed = raw ? JSON.parse(raw) : {};
return parsed && typeof parsed === 'object' ? parsed : {};
} catch (_) {
return {};
}
}
function isPlanDismissed(sess, plan) {
if (!plan?.complete) return false;
const id = planDismissId(sess, plan);
return !!(id && readPlanDismissed()[id]);
}
function markPlanDismissed(sess, plan) {
const id = planDismissId(sess, plan);
if (!id) return;
const dismissed = readPlanDismissed();
dismissed[id] = Date.now();
try { localStorage.setItem(PLAN_DISMISS_STORE, JSON.stringify(dismissed)); } catch (_) {}
}
let planPollTimer;
function syncPlanPollTimer() {
const on = !!(activeSess()?.bridgeSessionId && state.bridgeReady);
@@ -1881,21 +1595,12 @@ function clearPlanGrace(r) {
r.planCompleteAt = r.planLostAt = null;
r.planHoldItems = [];
r.planLastPayload = null;
r.planVisiblePayload = null;
r.planLastComplete = false;
r.planDismissedComplete = false;
if (r.planHideTimer) { clearTimeout(r.planHideTimer); r.planHideTimer = null; }
}
function schedulePlanCompleteDismiss(sess) {
const r = rt(sess);
if (r.planHideTimer) clearTimeout(r.planHideTimer);
r.planHideTimer = setTimeout(() => {
r.planHideTimer = null;
r.planDismissedComplete = true;
if (isActive(sess)) refreshPlanBar(null);
}, PLAN_COMPLETE_GRACE_MS);
}
/** tuiapp_v2._refresh_planbar:用 runtime 里缓存的 items / placeholder 重绘 */
function refreshPlanBarFromRuntime(sess) {
const r = rt(sess);
@@ -1910,11 +1615,6 @@ function refreshPlanBarFromRuntime(sess) {
refreshPlanBar(null);
return;
}
if (r.planCompleteAt != null && Date.now() - r.planCompleteAt >= PLAN_COMPLETE_GRACE_MS) {
r.planDismissedComplete = true;
refreshPlanBar(null);
return;
}
if (!items.length) {
if (lp?.active && lp.placeholder) {
refreshPlanBar(lp);
@@ -1975,7 +1675,9 @@ function applyPlanPayload(sess, raw) {
const wasComplete = r.planLastComplete;
if (nowComplete && !wasComplete) {
r.planCompleteAt = now;
schedulePlanCompleteDismiss(sess);
r.planDismissedComplete = isPlanDismissed(sess, { ...raw, items: items.length ? items : r.planHoldItems });
} else if (nowComplete) {
r.planDismissedComplete = isPlanDismissed(sess, { ...raw, items: items.length ? items : r.planHoldItems });
} else if (!nowComplete) {
r.planCompleteAt = null;
r.planDismissedComplete = false;
@@ -2053,16 +1755,23 @@ function bindPlanCardUiOnce() {
if (!sess) return;
const r = rt(sess);
const payload = r.planLastPayload;
if (!payload?.active) return;
const visiblePayload = r.planVisiblePayload || payload;
if (e.target.closest('[data-plan-dismiss]')) {
markPlanDismissed(sess, visiblePayload);
r.planDismissedComplete = true;
refreshPlanBar(null);
return;
}
if (!payload?.active && !visiblePayload?.active) return;
if (e.target.closest('[data-plan-expand]')) {
r.planCollapsed = false;
refreshPlanBar(payload);
refreshPlanBar(visiblePayload);
} else if (e.target.closest('[data-plan-collapse]')) {
r.planCollapsed = true;
refreshPlanBar(payload);
refreshPlanBar(visiblePayload);
} else if (e.target.closest('[data-plan-details]')) {
r.planShowAll = !r.planShowAll;
refreshPlanBar(payload);
refreshPlanBar(visiblePayload);
}
});
}
@@ -2074,6 +1783,7 @@ function refreshPlanBar(plan) {
planBarEl.hidden = true;
planBarEl.replaceChildren();
planBarEl.className = 'plan-card';
planBarEl._planRenderSig = '';
return;
}
const sess = activeSess();
@@ -2088,6 +1798,21 @@ function refreshPlanBar(plan) {
plan.complete ? 'plan-card--complete' : '',
plan.placeholder ? 'plan-card--placeholder' : '',
].filter(Boolean).join(' ');
const planItemsSig = planItemsSignature(plan.items || []);
if (sess && plan.complete && isPlanDismissed(sess, plan)) {
r.planDismissedComplete = true;
planBarEl.hidden = true;
planBarEl.replaceChildren();
planBarEl.className = 'plan-card';
planBarEl._planRenderSig = '';
return;
}
r.planVisiblePayload = plan;
const renderSig = JSON.stringify([
lang, mod, stepText, done, total, !!r.planShowAll, plan.pathHint || '', planItemsSig
]);
if (!planBarEl.hidden && planBarEl.className === mod && planBarEl._planRenderSig === renderSig) return;
planBarEl._planRenderSig = renderSig;
planBarEl.hidden = false;
planBarEl.className = mod;
@@ -2100,11 +1825,17 @@ function refreshPlanBar(plan) {
btn.dataset.planExpand = '1';
const dot = document.createElement('span');
dot.className = 'plan-status-dot';
const ico = document.createElement('span');
ico.className = 'plan-capsule-ic';
ico.innerHTML = GA_ICON('listChecks');
const txt = document.createElement('span');
txt.className = 'plan-capsule-text';
if (cap.step) txt.innerHTML = `${escapeHtml(cap.tag)} · <em>${escapeHtml(cap.step)}</em>`;
else txt.textContent = cap.tag;
btn.append(dot, txt);
const chev = document.createElement('span');
chev.className = 'plan-capsule-chevron';
chev.innerHTML = GA_ICON('caretRight');
btn.append(dot, ico, txt, chev);
planBarEl.append(btn);
return;
}
@@ -2128,11 +1859,19 @@ function refreshPlanBar(plan) {
}
const actions = document.createElement('div');
actions.className = 'plan-head-actions';
if (plan.complete) {
const dismissBtn = document.createElement('button');
dismissBtn.type = 'button';
dismissBtn.className = 'plan-btn';
dismissBtn.dataset.planDismiss = '1';
dismissBtn.innerHTML = `${GA_ICON('x', 'plan-btn-ic')}<span>${escapeHtml(t('plan.dismiss'))}</span>`;
actions.append(dismissBtn);
}
const collapseBtn = document.createElement('button');
collapseBtn.type = 'button';
collapseBtn.className = 'plan-btn';
collapseBtn.dataset.planCollapse = '1';
collapseBtn.textContent = t('plan.fold');
collapseBtn.innerHTML = `${GA_ICON('caretDown', 'plan-btn-ic')}<span>${escapeHtml(t('plan.fold'))}</span>`;
actions.append(collapseBtn);
head.append(actions);
frag.append(head);
@@ -2192,7 +1931,7 @@ function refreshPlanBar(plan) {
det.type = 'button';
det.className = 'plan-btn';
det.dataset.planDetails = '1';
det.textContent = r.planShowAll ? t('plan.collapse') : t('plan.details');
det.innerHTML = `${GA_ICON(r.planShowAll ? 'caretDown' : 'caretRight', 'plan-btn-ic')}<span>${escapeHtml(r.planShowAll ? t('plan.collapse') : t('plan.details'))}</span>`;
foot.append(det);
}
if (foot.childNodes.length) frag.append(foot);
@@ -2781,7 +2520,7 @@ function renderSessionList() {
`<span class="ci-dot"></span><div class="ci-main">` +
`<div class="ci-title">${pinSvg}${escapeHtml(displayTitle(sess))}</div>` +
`<div class="ci-meta">${busy ? t('status.running') : t('status.idle')}</div></div>` +
`<button class="ci-more" title="${escapeHtml(t('common.more'))}">${GA_ICON('dotsThreeVertical')}</button>`;
`<button class="ci-more" data-no-tooltip aria-label="${escapeHtml(t('common.more'))}">${GA_ICON('dotsThreeVertical')}</button>`;
convListEl.appendChild(item);
}
}
@@ -2905,6 +2644,26 @@ async function closeSession(id) {
const convMenu = document.getElementById('conv-menu');
let menuTargetId = null;
function hideConvMenu() {
if (!convMenu) return;
convMenu.hidden = true;
menuTargetId = null;
}
function positionConvMenu(anchor) {
if (!convMenu || !anchor) return;
convMenu.hidden = false;
const rect = anchor.getBoundingClientRect();
const pad = 8;
const gap = 4;
const w = convMenu.offsetWidth;
const h = convMenu.offsetHeight;
const left = Math.max(pad, Math.min(window.innerWidth - w - pad, rect.right - w));
const below = rect.bottom + gap;
const above = rect.top - h - gap;
const top = below + h <= window.innerHeight - pad ? below : Math.max(pad, above);
convMenu.style.left = `${left}px`;
convMenu.style.top = `${top}px`;
}
function isSessionRowInteractiveTarget(target) {
const el = target instanceof Element ? target : null;
return !!el?.closest?.(
@@ -2915,7 +2674,18 @@ convListEl.addEventListener('click', (e) => {
const more = e.target.closest('.ci-more');
if (more) {
e.stopPropagation();
menuTargetId = more.closest('.conv-item').dataset.id;
hideTooltip();
const item = more.closest('.conv-item');
const nextTargetId = item?.dataset.id || null;
if (!nextTargetId) {
hideConvMenu();
return;
}
if (!convMenu.hidden && menuTargetId === nextTargetId) {
hideConvMenu();
return;
}
menuTargetId = nextTargetId;
// 根据当前会话置顶状态切菜单文案:置顶 / 取消置顶
const tgt = state.sessions.get(menuTargetId);
const pinSpan = convMenu.querySelector('[data-act="pin"] [data-i18n]');
@@ -2924,10 +2694,7 @@ convListEl.addEventListener('click', (e) => {
pinSpan.setAttribute('data-i18n', k);
pinSpan.textContent = t(k);
}
convMenu.hidden = false;
const rect = more.getBoundingClientRect();
convMenu.style.top = (rect.bottom + 4) + 'px';
convMenu.style.left = (rect.right - convMenu.offsetWidth) + 'px';
positionConvMenu(more);
return;
}
if (isSessionRowInteractiveTarget(e.target)) return;
@@ -2961,7 +2728,7 @@ convMenu.addEventListener('click', (e) => {
patchSession(sess, { pinned: sess.pinned });
renderSessionList();
} else if (sess && act === 'rename') {
convMenu.hidden = true;
hideConvMenu();
const item = convListEl.querySelector(`.conv-item[data-id="${sess.id}"]`);
if (!item) return;
const titleEl = item.querySelector('.ci-title');
@@ -3003,9 +2770,9 @@ convMenu.addEventListener('click', (e) => {
} else if (sess && act === 'del') {
closeSession(sess.id);
}
convMenu.hidden = true;
hideConvMenu();
});
document.addEventListener('click', () => { convMenu.hidden = true; });
document.addEventListener('click', hideConvMenu);
newConvBtn.addEventListener('click', (e) => { e.preventDefault(); newSession(); });
/* ═══════════════ 轮询 + 流式 ═══════════════ */
@@ -3205,9 +2972,19 @@ async function pollSession(sess) {
try {
const result = await fetchSessionPoll(sess);
consecutiveErrors = 0;
const busy = applyPollResult(sess, result);
let busy = applyPollResult(sess, result);
if (busy) await new Promise(z => setTimeout(z, 500));
else {
const hasFinalAssistant = (result.messages || []).some(m => m && m.role === 'assistant');
if (r.draftEl && !hasFinalAssistant) {
await new Promise(z => setTimeout(z, 150));
const finalResult = await fetchSessionPoll(sess);
busy = applyPollResult(sess, finalResult);
if (busy) {
await new Promise(z => setTimeout(z, 500));
continue;
}
}
if (r.draftEl) { r.draftEl.remove(); r.draftEl = null; r.draftSegs = null; r.draftTurn = 0; }
resetTypewriterState(r);
break;
@@ -3364,7 +3141,11 @@ async function sendPrompt(text) {
if (res?.error) throw new Error(res.error.message || res.error);
removeUsedPendingFiles(usedFiles);
const uid = Number(res.userMessageId || res.result?.userMessageId || 0);
if (uid) { r.seen.add(uid); r.lastId = Math.max(r.lastId, uid); }
if (uid) {
userMsg.id = uid;
r.seen.add(uid);
r.lastId = Math.max(r.lastId, uid);
}
planPoll(sess);
pollSession(sess);
return true;
@@ -3392,9 +3173,10 @@ async function submitInput() {
if (_submitInFlight) return;
let text = composerText('chat');
if (!text.trim()) return;
if (text.trim().startsWith('/')) {
const slashName = slashCommandName(text);
if (slashName) {
inputEl.innerHTML = '';
handleSlash(text.trim());
handleSlash(slashName);
return;
}
if (text.length > 20000) {
@@ -3443,8 +3225,103 @@ function showToast(text) {
clearTimeout(_toastTimer);
_toastTimer = setTimeout(() => el.classList.remove('show'), 1800);
}
async function handleSlash(cmd) {
const name = cmd.slice(1).split(/\s+/)[0];
let tooltipEl = null;
let tooltipTarget = null;
let tooltipTimer = null;
function ensureTooltipEl() {
if (!tooltipEl) {
tooltipEl = document.createElement('div');
tooltipEl.className = 'ga-tooltip';
tooltipEl.hidden = true;
document.body.appendChild(tooltipEl);
}
return tooltipEl;
}
function migrateNativeTooltips(root = document) {
const nodes = [];
if (root?.nodeType === Node.ELEMENT_NODE && root.hasAttribute?.('title')) nodes.push(root);
root?.querySelectorAll?.('[title]').forEach(el => nodes.push(el));
nodes.forEach(el => {
const value = el.getAttribute('title') || '';
setTooltip(el, value);
});
}
function tooltipNodeFromEventTarget(target) {
const el = target?.closest?.('[data-tooltip],[title]');
if (el?.hasAttribute('title')) {
const value = el.getAttribute('title') || '';
setTooltip(el, value);
}
return el?.dataset?.tooltip ? el : null;
}
function positionTooltip() {
if (!tooltipEl || !tooltipTarget || tooltipEl.hidden) return;
const r = tooltipTarget.getBoundingClientRect();
const pad = 8;
const tw = tooltipEl.offsetWidth;
const th = tooltipEl.offsetHeight;
const left = Math.max(pad, Math.min(window.innerWidth - tw - pad, r.left + r.width / 2 - tw / 2));
const topAbove = r.top - th - 8;
const top = topAbove >= pad ? topAbove : Math.min(window.innerHeight - th - pad, r.bottom + 8);
tooltipEl.style.left = `${left}px`;
tooltipEl.style.top = `${Math.max(pad, top)}px`;
}
function showTooltip(target) {
const text = target?.dataset?.tooltip || '';
if (!text) return;
tooltipTarget = target;
clearTimeout(tooltipTimer);
tooltipTimer = setTimeout(() => {
const el = ensureTooltipEl();
el.textContent = text;
el.hidden = false;
el.classList.add('show');
positionTooltip();
}, 180);
}
function hideTooltip() {
clearTimeout(tooltipTimer);
tooltipTimer = null;
tooltipTarget = null;
if (tooltipEl) {
tooltipEl.classList.remove('show');
tooltipEl.hidden = true;
}
}
function initCustomTooltips() {
migrateNativeTooltips();
document.addEventListener('pointerover', (e) => {
const target = tooltipNodeFromEventTarget(e.target);
if (target) showTooltip(target);
}, true);
document.addEventListener('pointerout', (e) => {
if (!tooltipTarget) return;
const from = e.target?.closest?.('[data-tooltip]');
if (from === tooltipTarget && !tooltipTarget.contains(e.relatedTarget)) hideTooltip();
}, true);
document.addEventListener('focusin', (e) => {
const target = tooltipNodeFromEventTarget(e.target);
if (target) showTooltip(target);
}, true);
document.addEventListener('focusout', hideTooltip, true);
window.addEventListener('scroll', hideTooltip, true);
window.addEventListener('resize', hideTooltip);
new MutationObserver((records) => {
for (const rec of records) {
if (rec.type === 'attributes' && rec.attributeName === 'title') migrateNativeTooltips(rec.target);
rec.addedNodes?.forEach(node => migrateNativeTooltips(node));
}
}).observe(document.body, { subtree: true, childList: true, attributes: true, attributeFilter: ['title'] });
}
const SLASH_COMMANDS = new Set(['help', 'new', 'clear', 'stop', 'settings']);
function slashCommandName(text) {
const trimmed = String(text || '').trim();
if (!trimmed.startsWith('/')) return '';
const token = trimmed.slice(1).split(/\s+/)[0];
return SLASH_COMMANDS.has(token) ? token : '';
}
async function handleSlash(name) {
switch (name) {
case 'help': showSystem(t('slash.help')); break;
case 'new': await newSession(); break;
@@ -3919,7 +3796,7 @@ function renderModelMenu(menuEl) {
}
function openModelMenu(chipEl, menuEl) {
if (!chipEl || !menuEl) return;
if (typeof convMenu !== 'undefined' && convMenu) convMenu.hidden = true;
if (typeof hideConvMenu === 'function') hideConvMenu();
window.collabComposer?.closeMenu?.();
closeAllModelMenus();
renderModelMenu(menuEl);
@@ -4462,7 +4339,10 @@ window.ga.onBridgeNotification((msg) => {
if (msg && msg.type === 'session-state') {
for (const sess of state.sessions.values()) {
if (sess.bridgeSessionId === msg.sessionId) {
if (msg.status === 'running' || msg.state === 'running') pollSession(sess);
if (['running', 'idle', 'error', 'cancelled'].includes(msg.status) ||
['running', 'idle', 'error', 'cancelled'].includes(msg.state)) {
pollSession(sess);
}
if (msg.state === 'idle' || msg.status === 'idle') tokPollBridge();
renderSessionList();
break;
@@ -4994,49 +4874,13 @@ if (msgArea) {
function uploadRawUrl(path, download) {
return `${BRIDGE_ORIGIN}/upload/raw?path=${encodeURIComponent(path || '')}${download ? '&download=1' : ''}`;
}
function bridgeIsLocal() {
return location.hostname === '127.0.0.1' || location.hostname === 'localhost';
}
async function openUploadFile(path, name) {
// 远程访问:浏览器无法调起 bridge 那台/本机的系统程序,降级为下载到本机
if (!bridgeIsLocal()) {
const a = document.createElement('a');
a.href = uploadRawUrl(path, true);
a.download = name || '';
document.body.appendChild(a); a.click(); a.remove();
return;
}
// 本地:bridge 与你同机,调系统默认程序打开 / 在文件夹显示
const mode = isPreviewableByName(name || path) ? 'open' : 'reveal';
try {
const res = await fetch(`${BRIDGE_ORIGIN}/path/open`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ kind: 'upload', path, mode }),
});
const j = await res.json();
if (!j.ok) throw new Error(j.error || 'open failed');
} catch (e) {
showChanToast(t('file.openFailed'), e.message || String(e), 'err');
}
}
const PREVIEWABLE_EXTS = new Set([
'pdf',
'png', 'jpg', 'jpeg', 'gif', 'webp', 'bmp', 'svg', 'heic', 'tiff',
'txt', 'md', 'log', 'json', 'yaml', 'yml', 'xml', 'csv', 'tsv', 'ini', 'toml', 'env', 'rtf',
'py', 'js', 'ts', 'tsx', 'jsx', 'java', 'c', 'cpp', 'h', 'hpp', 'rs', 'go', 'rb', 'php', 'sh', 'bash', 'zsh', 'fish', 'lua', 'pl', 'r', 'scala', 'kt', 'swift',
'html', 'htm', 'css', 'scss', 'sass', 'less', 'vue', 'svelte', 'sql',
'doc', 'docx', 'pages', 'odt',
'xls', 'xlsx', 'numbers', 'ods',
'ppt', 'pptx', 'key', 'odp',
'mp3', 'wav', 'flac', 'aac', 'ogg', 'm4a',
'mp4', 'mov', 'avi', 'mkv', 'webm', 'wmv',
]);
function isPreviewableByName(name) {
const m = String(name || '').match(/\.([^./\\]+)$/);
if (!m) return false;
return PREVIEWABLE_EXTS.has(m[1].toLowerCase());
const a = document.createElement('a');
a.href = uploadRawUrl(path, true);
a.download = name || '';
document.body.appendChild(a);
a.click();
a.remove();
}
/* ═══════════════ 后台服务页 Tab(消息通道 / 状态面板) ═══════════════ */
@@ -5690,6 +5534,7 @@ initChatFontStepper();
applyChatFontSize(chatFontSize, { persist: false });
syncHljsTheme();
applyI18n();
initCustomTooltips();
updateModelChip();
renderSessionList();
loadCustomPresets();
+19 -21
View File
@@ -2,7 +2,7 @@
<html lang="en">
<head>
<meta charset="UTF-8">
<title>GenericAgent — Setup</title>
<title data-i18n="setup.title">GenericAgent</title>
<style>
:root{--bg:#fff;--text:#0a0a0a;--muted:#6b6b6b;--border:#e8e8e8;--accent:#0a0a0a;--accent-text:#fff;--radius:10px;--font:-apple-system,BlinkMacSystemFont,"SF Pro Text","Helvetica Neue",Helvetica,Arial,sans-serif}
@media(prefers-color-scheme:dark){:root{--bg:#1a1a1a;--text:#ededed;--muted:#a0a0a0;--border:#2a2a2a;--accent:#ededed;--accent-text:#0a0a0a}}
@@ -21,33 +21,31 @@ button:disabled{opacity:.4;cursor:not-allowed}
#status{margin-top:1rem;font-size:.85rem;min-height:1.2em}
.err{color:#dc2626}.ok{color:#16a34a}
</style>
<script src="i18n.js?v=1"></script>
</head>
<body>
<div class="card">
<h1>⚙️ Setup Required</h1>
<p>The backend could not start. Please configure the paths below.</p>
<p>后端启动失败,请配置以下路径。</p>
<h1><span aria-hidden="true">⚙️</span> <span data-i18n="setup.heading"></span></h1>
<p data-i18n="setup.backend_failed"></p>
<label class="field-label">Python interpreter 解释器路径</label>
<label class="field-label" data-i18n="setup.py_label"></label>
<div class="input-row">
<input id="pypath" type="text" placeholder="python / path to interpreter" spellcheck="false">
<input id="pypath" type="text" data-i18n-placeholder="setup.py_placeholder" spellcheck="false">
</div>
<p style="color:var(--muted);font-size:.75rem;margin-top:.3rem">
e.g. <code>C:/Python312/python.exe</code> / <code>~/miniconda3/envs/myenv/bin/python</code>
</p>
<p style="color:var(--muted);font-size:.75rem;margin-top:.3rem" data-i18n="setup.py_hint"></p>
<label class="field-label">Project directory 项目目录</label>
<label class="field-label" data-i18n="setup.project_label"></label>
<div class="input-row">
<input id="projdir" type="text" placeholder="path to GenericAgent folder" spellcheck="false">
<input id="projdir" type="text" data-i18n-placeholder="setup.project_placeholder" spellcheck="false">
</div>
<p style="color:var(--muted);font-size:.75rem;margin-top:.3rem">
The folder containing <code>frontends/desktop_bridge.py</code>
</p>
<p style="color:var(--muted);font-size:.75rem;margin-top:.3rem" data-i18n="setup.project_hint"></p>
<button id="start-btn" onclick="doStart()" style="margin-top:1.2rem;width:100%">Start 启动</button>
<button id="start-btn" onclick="doStart()" style="margin-top:1.2rem;width:100%" data-i18n="setup.start"></button>
<div id="status"></div>
</div>
<script>
const bootT = window.GA_BOOT_I18N?.t || ((key, vars) => vars?.error ? `${key}: ${vars.error}` : key);
window.GA_BOOT_I18N?.apply(document);
const {invoke} = window.__TAURI__.core;
const {getCurrentWindow, Window} = window.__TAURI__.window;
const statusEl = document.getElementById('status');
@@ -64,13 +62,13 @@ invoke('get_config').then(([py, dir]) => {
async function doStart() {
const pythonPath = pyInput.value.trim();
const projectDir = dirInput.value.trim();
if (!pythonPath) { statusEl.className='err'; statusEl.textContent='Please enter Python path / 请输入 Python 路径'; return; }
if (!projectDir) { statusEl.className='err'; statusEl.textContent='Please enter project directory / 请输入项目目录'; return; }
if (!pythonPath) { statusEl.className='err'; statusEl.textContent=bootT('setup.err_py'); return; }
if (!projectDir) { statusEl.className='err'; statusEl.textContent=bootT('setup.err_project'); return; }
btn.disabled = true;
statusEl.className=''; statusEl.textContent='Starting bridge… 正在启动…';
statusEl.className=''; statusEl.textContent=bootT('setup.starting_bridge');
try {
await invoke('start_bridge_with_config', {pythonPath, projectDir});
statusEl.className='ok'; statusEl.textContent='Connected! 已连接,正在打开主窗口…';
statusEl.className='ok'; statusEl.textContent=bootT('setup.connected');
// Show main window and hide setup
const mainWin = await Window.getByLabel('main');
if (mainWin) {
@@ -80,7 +78,7 @@ async function doStart() {
const setupWin = getCurrentWindow();
await setupWin.hide();
} catch(e) {
statusEl.className='err'; statusEl.textContent='Failed: '+e;
statusEl.className='err'; statusEl.textContent=bootT('setup.failed', { error: e });
btn.disabled = false;
}
}
@@ -104,7 +102,7 @@ const _dirInput = document.getElementById('projdir');
el.value = el.value.slice(0, MAX_LEN);
}
if (el.value.length >= MAX_LEN) {
hint.textContent = 'Character limit reached (' + MAX_LEN + ') / 已达字数上限(' + MAX_LEN + '';
hint.textContent = bootT('setup.limit', { n: MAX_LEN });
hint.style.display = 'block';
} else {
hint.style.display = 'none';
+468
View File
@@ -0,0 +1,468 @@
(() => {
const I18N = {
zh: {
'app.title': 'GenericAgent 桌面版',
'brand.sub': '桌面终端',
'nav.chat': '聊天', 'nav.services': '后台服务', 'nav.channels': '消息通道', 'nav.status': '状态面板',
'nav.collab': '指挥家', 'nav.token': '用量',
'foot.settings': '配置', 'foot.ver': 'GenericAgent · 桌面版',
'chat.startTitle': '开始对话', 'chat.startSub': '直接输入,或点预设功能一键启动',
'preset.butler.t': '指挥家', 'preset.butler.d': '复杂任务自动拆解,只需查看进度和简报',
'preset.plan.t': 'Plan 模式', 'preset.plan.d': '加载 Plan SOP,按探索→规划→执行→验证流程',
'preset.goal.t': 'Goal 模式', 'preset.goal.d': '设定目标,自主完成',
'preset.autonomous.t': '自主行动', 'preset.autonomous.d': '按 SOP 规划/执行任务,产出报告(reflect/autonomous.py 同源)',
'preset.hive.t': 'Hive 协作', 'preset.hive.d': '多 worker 协同攻坚',
'preset.review.t': '深度复核', 'preset.review.d': '挑刺式质量把关',
'preset.findwork.t': '找点事做', 'preset.findwork.d': '分析当前情况,推荐一批让你感兴趣的 TODO',
'preset.mine.t': '我的·周报', 'preset.mine.d': '自定义:抓本周提交并写周报',
'preset.add.t': '自定义', 'preset.add.d': '任意一句话存为功能',
'composer.placeholder': 'GA 能帮你做些什么?',
'search.placeholder': '搜索会话…', 'conv.new': '新对话',
'ctx.pin': '置顶', 'ctx.unpin': '取消置顶', 'ctx.rename': '重命名', 'ctx.del': '删除',
'common.close': '关闭', 'common.more': '更多', 'common.optional': '选填', 'common.save': '保存',
'modal.preset': '预设功能', 'modal.addModel': '添加模型', 'modal.editModel': '编辑模型', 'modal.settings': '配置',
'modal.customPreset': '自定义预设',
'modal.editCustomPreset': '编辑任务',
'customPreset.titlePh': '标题,例如「写周报」',
'customPreset.promptPh': 'Prompt 内容,发送时会作为消息提交',
'customPreset.empty': '标题和 Prompt 不能为空',
'customPreset.removeTitle': '删除',
'customPreset.editTitle': '编辑',
'builtinPreset.restoreBtn': '恢复默认预设',
'set.appearance': '外观', 'set.plainUi': '素色', 'set.fontSize': '聊天字号', 'set.lang': '语言', 'set.model': '模型', 'set.addModel': '添加模型', 'set.features': '功能', 'set.importMykey': '导入已有模型配置(mykey.py', 'set.exportMykey': '导出当前模型配置', 'set.serviceManager': '后台服务管理',
'shortcut.askConfirm': '是否在桌面创建 GenericAgent 快捷方式?',
'appearance.light': '浅色', 'appearance.dark': '深色',
'set.noModels': '暂无模型,点击下方添加',
'lang.zh': '简体中文', 'lang.en': 'English',
'model.name': '备注', 'model.namePh': '会显示在模型列表',
'model.apikey': 'API Key', 'model.apikeyPh': 'sk-...', 'model.apikeyKeep': '留空则保持原 Key 不变',
'model.apibase': 'API 地址', 'model.apibasePh': 'https://.../v1/messages',
'model.protocol': '协议', 'model.protocolPick': '请选择…', 'model.protocolOai': 'OpenAI 兼容 (chat/completions)', 'model.protocolClaude': 'Anthropic (Claude /v1/messages)',
'model.stream': '响应方式', 'model.streamOn': '流式', 'model.streamOff': '非流式',
'model.model': '模型', 'model.modelPh': 'model 参数名',
'model.modelHint': '须与中转站/官方文档中的 model 字段完全一致',
'model.retries': '重试 (次)', 'model.connTimeout': '连接超时 (s)', 'model.readTimeout': '读取超时 (s)',
'model.save': '保存', 'common.cancel': '取消', 'common.confirm': '确认', 'common.edit': '编辑', 'common.delete': '删除',
'pq.title': '快速接入官方模型', 'pq.sub': '填好 API Key 即可使用', 'pq.toggle': '展开 / 收起',
'pq.deepseekDesc': '官方 API · OpenAI 兼容', 'pq.qwenDesc': '通义千问 · 阿里云百炼',
'guide.step1': '点击下方链接,登录后创建并复制 API Key',
'guide.step2': '把 Key 粘贴到下方「API Key」输入框',
'guide.step3': '点击保存,即可在模型列表中选用',
'guide.prefillTip': '已为你预填 API 地址、协议与模型,可按需修改',
'guide.getKey': '获取 {name} 的 API Key', 'guide.copy': '复制链接', 'guide.copied': '链接已复制',
'err.modelSave': '保存失败', 'err.modelSwitch': '切换模型失败', 'err.modelRequired': '请填写模型、API Key 和 API 地址',
'err.modelDelete': '删除失败', 'err.modelDeleteLast': '至少保留一个模型',
'confirm.modelDelete': '确定删除该模型配置?',
'model.aggregation': '渠道组(自动故障转移)', 'model.aggregationShort': '渠道组', 'model.aggregationDesc': '按顺序尝试,失败自动切换到下一个',
'model.emptyMixin': '尚未加入模型',
'model.addToMixin': '加入渠道组', 'model.inMixin': '已在渠道组', 'model.removeFromMixin': '移出渠道组', 'model.alreadyInMixin': '已在渠道组中', 'model.dragReorder': '拖拽调整顺序',
'err.mixinFailed': '操作失败',
'page.services.title': '后台服务', 'page.services.sub': 'IM 消息通道与后台进程,集中查看、启停与日志',
'page.channels.title': '消息通道', 'page.channels.sub': '后台 IM 进程:列表、启停与日志(同 hub.pyw)',
'page.status.title': '状态面板', 'page.status.sub': 'hub.pyw 管理的后台进程/服务,集中查看与启停',
'page.collab.title': '指挥家', 'page.collab.sub': '交代目标,自动拆活与跟进',
'collab.progressTitle': '分工进度',
'collab.progressEmpty': '还没有任务在执行。告诉指挥家你的目标后,这里会显示拆分后的处理进度。',
'collab.placeholder': '请对指挥家描述你想完成的目标',
'collab.guideTitle': '把要完成的事告诉指挥家',
'collab.guideWhen': '适合需要多步处理、要花一些时间才能完成的目标。日常聊天和快问快答,请用左侧「聊天」。',
'collab.guideStep1t': '描述目标',
'collab.guideStep1d': '在聊天框里写下你想做的事,发给指挥家',
'collab.guideStep2t': '自动拆解',
'collab.guideStep2d': '指挥家自动拆解、分配任务,实时监督和调度',
'collab.guideStep3t': '交付摘要',
'collab.guideStep3d': '指挥家根据执行状态,呈上任务简报',
'collab.guideStep4t': '随时调整',
'collab.guideStep4d': '随时补充要求或细节,指挥家都会处理',
'collab.chipProgress': '现在进展如何?',
'collab.chipPause': '先暂停当前任务',
'collab.chipSummary': '总结一下目前的结果',
'collab.showProgressTitle': '查看分工进度',
'collab.statRunning': '进行中',
'collab.statDone': '已完成',
'collab.plusMenu': '更多操作',
'collab.switchMode': '切换模式',
'collab.typing': '指挥家正在处理',
'collab.offline': '无法连接指挥家服务,请确认后端已启动。',
'collab.retry': '重试',
'collab.reconnect': '连接断开,正在重连… 已保留上次任务进度。',
'collab.reconnectIn': '{n} 秒后重试',
'collab.stRunning': '执行中', 'collab.stReported': '已回报', 'collab.stPaused': '已暂停',
'collab.stFailed': '遇到问题', 'collab.stTerminated': '已终止',
'collab.summaryRunning': '正在处理中…', 'collab.summaryWait': '等待回报',
'collab.taskFallback': '任务 {n}',
'collab.timeJust': '刚刚',
'collab.timeSec': '{n} 秒前',
'collab.timeMin': '{n} 分钟前',
'collab.timeHr': '{n} 小时前',
'collab.timeDay': '{n} 天前',
'page.token.title': '用量', 'page.token.sub': '每会话与累计用量及缓存率',
'status.connecting': '正在连接…', 'status.ready': '服务在线', 'status.running': '处理中',
'status.disconnected': '服务离线', 'status.stopped': '已停止', 'status.idle': '待命',
'conv.emptyList': '暂无会话,点「+ 新对话」开始', 'conv.defaultTitle': '新对话',
'err.bridge': '服务未响应', 'err.newSession': '新建会话失败', 'err.poll': '轮询失败', 'err.stop': '停止失败',
'err.interruptTimeout': '等待上一轮停止超时,请稍后再试',
'sys.interruptPrev.hint': '已停止上一轮,正在处理新消息',
'chat.interrupting': '正在停止上一轮…',
'chat.sessionLoading': '正在加载会话…',
'sys.stopRequested': '已请求停止',
'slash.help': '可用命令:\n/new 新会话 /clear 清屏 /stop 停止 /settings 设置',
'slash.unknown': '未知命令',
'upload.hint': '上传文件:选择 / 拖拽 / 粘贴',
'upload.button': '上传文件',
'upload.tooLarge': '文件过大或数量超限', 'upload.empty': '跳过空文件',
'upload.failed': '上传失败',
'err.charLimit': '已达字数上限({n}),发送时将自动截断', 'err.charLimitReached': '已达字数上限({n}', 'err.numMax': '不能超过 {n}',
'file.openFailed': '无法打开文件',
'file.kindGeneric': '文件',
'file.kindDoc': '文档',
'file.kindSheet': '表格',
'file.kindSlide': '幻灯片',
'file.kindCode': '代码',
'file.kindArchive': '压缩包',
'file.kindAudio': '音频',
'file.kindVideo': '视频',
'upload.removeTitle': '移除',
'upload.dropHint': '松开以上传文件',
'lightbox.closeTitle': '关闭',
'fold.thinking': '思考', 'fold.tool': '工具调用', 'fold.toolResult': '工具结果', 'fold.llm': 'LLM Running', 'fold.turn': '第 {n} 轮',
'plan.header': '计划 ({done}/{total})', 'plan.complete': '✓ 计划完成 ({n}/{n})',
'plan.running': '计划执行中', 'plan.completeTitle': '计划完成',
'plan.placeholder': '计划模式已激活', 'plan.waiting': '等待写入 {path} …', 'plan.overflow': '还有 {n} 项',
'plan.current': '当前', 'plan.fold': '折叠', 'plan.collapse': '收起', 'plan.expand': '展开', 'plan.details': '详情', 'plan.dismiss': '不再显示',
'plan.capsuleRunning': '运行中', 'plan.capsuleComplete': '已完成',
'timing.elapsed': '已运行 {t}',
'model.auto': '自动选择',
'model.menuLabel': '选择模型',
'chip.plan': 'Plan',
'chip.auto': 'Auto',
'ch.wechat': '微信', 'ch.wecom': '企业微信', 'ch.lark': '飞书', 'ch.dingtalk': '钉钉',
'ch.qq': 'QQ', 'ch.telegram': 'Telegram', 'ch.discord': 'Discord',
'ch.loading': '加载中…', 'ch.empty': '未发现 IM 进程脚本',
'ch.logEmpty': '暂无日志',
'err.channelLoad': '加载失败', 'err.channelStart': '启动失败', 'err.channelStop': '停止失败',
'err.mykeyImport': '导入模型配置失败',
'err.mykeyExport': '导出模型配置失败',
'err.channelNotConfigured': '请先在 mykey.py 中配置该平台',
'sys.channelStarted': '已启动', 'sys.channelStopped': '已停止',
'modal.channelLogs': '进程日志',
'modal.mykeyConfig': 'mykey.py 配置',
'sys.configSaved': '配置已保存',
'sys.mykeyImported': '模型配置已导入',
'sys.mykeyExported': '模型配置已导出',
'st.starting': '启动中…', 'st.stopping': '停止中…', 'st.online': '在线', 'st.offline': '离线', 'st.error': '错误', 'st.running': '运行', 'st.abnormal': '异常',
'act.configure': '配置', 'act.logs': '日志', 'act.restart': '重启', 'act.stop': '停止', 'act.start': '启动', 'act.exit': '退出',
'act.copy': '复制', 'act.copied': '已复制', 'act.copyTex': 'TeX', 'act.send': '发送',
'proc.imbotWechat': 'imbot · 微信', 'proc.imbotDing': 'imbot · 钉钉', 'proc.scheduler': '定时任务调度', 'proc.conductor': '指挥家',
'cm.scheduling': '调度中', 'cm.running': '执行中', 'cm.idleSt': '空闲',
'cm.master': '已派 3 子任务', 'cm.w1': '子任务:抓取数据', 'cm.w2': '子任务:复核结果', 'cm.sub': '等待派单',
'tok.total': '累计', 'tok.cost': '缓存率', 'tok.today': '今日', 'tok.tabAll': '聊天', 'tok.tabConductor': '指挥家', 'tok.condTotal': '指挥家累计', 'tok.condCurrent': '指挥家本次', 'tok.condTip': '指挥家消耗不计入聊天累计', 'tok.condOffline': '指挥家服务离线', 'tok.disclaimer': '不同 API 网站的计费价格可能会有差异,请以实际网站为准。',
'tok.colSession': '会话', 'tok.colIn': '输入', 'tok.colOut': '输出', 'tok.colCacheW': '缓存写入', 'tok.colCache': '缓存读取', 'tok.colCost': '成本',
'tok.from': '从', 'tok.to': '到', 'tok.reset': '重置', 'tok.noData': '暂无记录', 'tok.deleted': '此会话已删除',
'tok.pricingUnknown': '⚠ 此模型计费规则尚未明确,按默认估算',
'tok.priceInput': '输入: $', 'tok.priceOutput': '输出: $',
'tok.priceCacheW': '缓存写入: $', 'tok.priceCacheR': '缓存读取: $',
'presetPrompt.goal': '进入 Goal 模式:读 L3 goal mode SOP,自主达成我接下来描述的目标。',
'presetPrompt.plan': '进入 Plan 模式:先读 memory/plan_sop.md,按其中「探索→规划→执行→验证」流程,等我接下来描述要做的任务。',
'presetPrompt.autonomous': '🤖 进入自主行动模式:阅读 memory/autonomous_operation_sop.md,按 SOP 选取或规划任务,独立执行并产出报告。',
'presetPrompt.hive': '启动 Goal Hive 模式:按 hive SOP 拉起多个 worker 协同完成我接下来的目标。',
'presetPrompt.review': '进入监察者模式:对刚才的产出严格挑刺、逐项复核并报告问题。',
'presetPrompt.findwork': '按照自主行动的规划部分,充分分析我的情况,给我生成一批 TODO,务必让我感兴趣。',
'presetPrompt.mine': '抓取本周的 git 提交并写一份周报。',
'ask.banner': 'GA 等你回答',
'ask.replyHint': '在下方输入框回复',
'ask.placeholderOpen': '在此输入你的回答… (Enter 发送)',
},
en: {
'app.title': 'GenericAgent Desktop',
'brand.sub': 'Desktop terminal',
'nav.chat': 'Chat', 'nav.services': 'Services', 'nav.channels': 'Channels', 'nav.status': 'Status',
'nav.collab': 'Conductor', 'nav.token': 'Usage',
'foot.settings': 'Settings', 'foot.ver': 'GenericAgent · Desktop',
'chat.startTitle': 'Start a conversation', 'chat.startSub': 'Type a message, or pick a preset',
'preset.butler.t': 'Conductor', 'preset.butler.d': 'Auto-decompose complex tasks; just check progress and briefings',
'preset.plan.t': 'Plan mode', 'preset.plan.d': 'Load Plan SOP — explore→plan→execute→verify',
'preset.goal.t': 'Goal mode', 'preset.goal.d': 'Set a goal, run autonomously',
'preset.autonomous.t': 'Autonomous mode', 'preset.autonomous.d': 'Plan/execute tasks per SOP and produce reports (same as reflect/autonomous.py)',
'preset.hive.t': 'Hive', 'preset.hive.d': 'Multi-worker collaboration',
'preset.review.t': 'Deep review', 'preset.review.d': 'Strict quality check',
'preset.findwork.t': 'Find me work', 'preset.findwork.d': 'Analyze my context and suggest a batch of interesting TODOs',
'preset.mine.t': 'My · Weekly', 'preset.mine.d': 'Custom: weekly report from commits',
'preset.add.t': 'Custom', 'preset.add.d': 'Save any prompt as a function',
'composer.placeholder': 'What can GA do for you?',
'search.placeholder': 'Search chats…', 'conv.new': 'New chat',
'ctx.pin': 'Pin', 'ctx.unpin': 'Unpin', 'ctx.rename': 'Rename', 'ctx.del': 'Delete',
'common.close': 'Close', 'common.more': 'More', 'common.optional': 'Optional', 'common.save': 'Save',
'modal.preset': 'Presets', 'modal.addModel': 'Add model', 'modal.editModel': 'Edit model', 'modal.settings': 'Settings',
'modal.customPreset': 'Custom preset',
'modal.editCustomPreset': 'Edit task',
'customPreset.titlePh': 'Title, e.g. "Weekly report"',
'customPreset.promptPh': 'Prompt body — sent as the message when clicked',
'customPreset.empty': 'Title and Prompt cannot be empty',
'customPreset.removeTitle': 'Delete',
'customPreset.editTitle': 'Edit',
'builtinPreset.restoreBtn': 'Restore defaults',
'set.appearance': 'Appearance', 'set.plainUi': 'Plain', 'set.fontSize': 'Chat font size', 'set.lang': 'Language', 'set.model': 'Model', 'set.addModel': 'Add model', 'set.features': 'Features', 'set.importMykey': 'Import model config (mykey.py)', 'set.exportMykey': 'Export current model config', 'set.serviceManager': 'Service manager',
'shortcut.askConfirm': 'Create a desktop shortcut for GenericAgent?',
'appearance.light': 'Light', 'appearance.dark': 'Dark',
'set.noModels': 'No models yet — add one below',
'lang.zh': '简体中文', 'lang.en': 'English',
'model.name': 'Note', 'model.namePh': 'Shown in the model list',
'model.apikey': 'API Key', 'model.apikeyPh': 'sk-...', 'model.apikeyKeep': 'Leave blank to keep the current key',
'model.apibase': 'API base URL', 'model.apibasePh': 'https://.../v1/messages',
'model.protocol': 'Protocol', 'model.protocolPick': 'Select…', 'model.protocolOai': 'OpenAI-compatible (chat/completions)', 'model.protocolClaude': 'Anthropic (Claude /v1/messages)',
'model.stream': 'Response', 'model.streamOn': 'Stream', 'model.streamOff': 'Non-stream',
'model.model': 'Model', 'model.modelPh': 'model parameter name',
'model.modelHint': 'Must match the model field in your provider docs exactly',
'model.retries': 'Retries (×)', 'model.connTimeout': 'Connect (s)', 'model.readTimeout': 'Read (s)',
'model.save': 'Save', 'common.cancel': 'Cancel', 'common.confirm': 'Confirm', 'common.edit': 'Edit', 'common.delete': 'Delete',
'pq.title': 'Quick connect a model', 'pq.sub': 'Add your API key to get started', 'pq.toggle': 'Expand / collapse',
'pq.deepseekDesc': 'Official API · OpenAI-compatible', 'pq.qwenDesc': 'Tongyi Qwen · Aliyun Bailian',
'guide.step1': 'Open the link, sign in, then create & copy your API key',
'guide.step2': 'Paste the key into the “API Key” field below',
'guide.step3': 'Click Save — then pick it from the model list',
'guide.prefillTip': 'API base, protocol and model are pre-filled — edit if needed',
'guide.getKey': 'Get your {name} API key', 'guide.copy': 'Copy link', 'guide.copied': 'Link copied',
'err.modelSave': 'Save failed', 'err.modelSwitch': 'Failed to switch model', 'err.modelRequired': 'Model, API Key and base URL are required',
'err.modelDelete': 'Delete failed', 'err.modelDeleteLast': 'At least one model is required',
'confirm.modelDelete': 'Delete this model profile?',
'model.aggregation': 'Channel group (auto failover)', 'model.aggregationShort': 'Channel group', 'model.aggregationDesc': 'Tries in order, switches to the next on failure',
'model.emptyMixin': 'No models added yet',
'model.addToMixin': 'Add to channel', 'model.inMixin': 'In channel', 'model.removeFromMixin': 'Remove from channel', 'model.alreadyInMixin': 'Already in the channel', 'model.dragReorder': 'Drag to reorder',
'err.mixinFailed': 'Operation failed',
'page.services.title': 'Services', 'page.services.sub': 'IM channels and background processes — view, start/stop, logs',
'page.channels.title': 'Channels', 'page.channels.sub': 'Background IM processes: list, start/stop, logs (hub.pyw style)',
'page.status.title': 'Status', 'page.status.sub': 'Background processes/services managed by hub.pyw',
'page.collab.title': 'Conductor', 'page.collab.sub': 'Describe a goal — split, delegate, and follow up',
'collab.progressTitle': 'Progress',
'collab.progressEmpty': 'No tasks running yet. After you describe a goal to Conductor, split tasks will appear here.',
'collab.placeholder': 'Describe the goal you want to accomplish',
'collab.guideTitle': 'Tell Conductor what you want done',
'collab.guideWhen': 'Best for multi-step goals that take a while. For everyday chat and quick questions, use Chat in the sidebar.',
'collab.guideStep1t': 'Describe your goal',
'collab.guideStep1d': 'Write what you want done in the chat box and send it to Conductor',
'collab.guideStep2t': 'Auto breakdown',
'collab.guideStep2d': 'Conductor breaks down, assigns, monitors, and coordinates',
'collab.guideStep3t': 'Summary',
'collab.guideStep3d': 'Conductor delivers a briefing based on execution status',
'collab.guideStep4t': 'Adjust anytime',
'collab.guideStep4d': 'Add requirements or details anytime — Conductor handles them',
'collab.chipProgress': 'How is it going?',
'collab.chipPause': 'Pause current tasks',
'collab.chipSummary': 'Summarize progress so far',
'collab.showProgressTitle': 'View task progress',
'collab.statRunning': 'Running',
'collab.statDone': 'Done',
'collab.plusMenu': 'More actions',
'collab.switchMode': 'Switch mode',
'collab.typing': 'Conductor is working',
'collab.offline': 'Cannot reach the service. Make sure the backend is running.',
'collab.retry': 'Retry',
'collab.reconnect': 'Disconnected — reconnecting… Your last progress is kept.',
'collab.reconnectIn': 'Retry in {n}s',
'collab.stRunning': 'Running', 'collab.stReported': 'Reported', 'collab.stPaused': 'Paused',
'collab.stFailed': 'Issue', 'collab.stTerminated': 'Ended',
'collab.summaryRunning': 'Working…', 'collab.summaryWait': 'Awaiting report',
'collab.taskFallback': 'Task {n}',
'collab.timeJust': 'just now',
'collab.timeSec': '{n}s ago',
'collab.timeMin': '{n}m ago',
'collab.timeHr': '{n}h ago',
'collab.timeDay': '{n}d ago',
'page.token.title': 'Usage', 'page.token.sub': 'Per-session and total usage & cache rate',
'status.connecting': 'Connecting…', 'status.ready': 'Service online', 'status.running': 'Working…',
'status.disconnected': 'Service offline', 'status.stopped': 'Stopped', 'status.idle': 'Standby',
'conv.emptyList': 'No chats yet — click “+ New chat”', 'conv.defaultTitle': 'New chat',
'err.bridge': 'Service not responding', 'err.newSession': 'Failed to create session', 'err.poll': 'Polling failed', 'err.stop': 'Stop failed',
'err.interruptTimeout': 'Timed out waiting for the previous reply to stop — try again',
'sys.interruptPrev.hint': 'Previous reply stopped — processing new message',
'chat.interrupting': 'Stopping previous reply…',
'chat.sessionLoading': 'Loading conversation…',
'sys.stopRequested': 'Stop requested',
'slash.help': 'Commands:\n/new new chat /clear clear /stop stop /settings settings',
'slash.unknown': 'Unknown command',
'upload.hint': 'Upload file: pick / drag / paste',
'upload.button': 'Upload file',
'upload.tooLarge': 'File too large or limit reached', 'upload.empty': 'Skipped empty file',
'upload.failed': 'Upload failed',
'err.charLimit': 'Character limit reached ({n}), text will be truncated on send', 'err.charLimitReached': 'Character limit reached ({n})', 'err.numMax': 'Cannot exceed {n}',
'file.openFailed': 'Cannot open file',
'file.kindGeneric': 'File',
'file.kindDoc': 'Document',
'file.kindSheet': 'Spreadsheet',
'file.kindSlide': 'Slides',
'file.kindCode': 'Code',
'file.kindArchive': 'Archive',
'file.kindAudio': 'Audio',
'file.kindVideo': 'Video',
'upload.removeTitle': 'Remove',
'upload.dropHint': 'Drop to upload files',
'lightbox.closeTitle': 'Close',
'fold.thinking': 'Thinking', 'fold.tool': 'Tool call', 'fold.toolResult': 'Tool result', 'fold.llm': 'LLM Running', 'fold.turn': 'Turn {n}',
'plan.header': 'Plan ({done}/{total})', 'plan.complete': '✓ Plan complete ({n}/{n})',
'plan.running': 'Running plan', 'plan.completeTitle': 'Plan complete',
'plan.placeholder': 'Plan mode activated', 'plan.waiting': 'waiting for {path} …', 'plan.overflow': '+{n} more',
'plan.current': 'Now', 'plan.fold': 'Fold', 'plan.collapse': 'Collapse', 'plan.expand': 'Expand', 'plan.details': 'Details', 'plan.dismiss': 'Hide',
'plan.capsuleRunning': 'Running', 'plan.capsuleComplete': 'Done',
'timing.elapsed': 'Elapsed {t}',
'model.auto': 'Auto',
'model.menuLabel': 'Select model',
'chip.plan': 'Plan',
'chip.auto': 'Auto',
'ch.wechat': 'WeChat', 'ch.wecom': 'WeCom', 'ch.lark': 'Lark', 'ch.dingtalk': 'DingTalk',
'ch.qq': 'QQ', 'ch.telegram': 'Telegram', 'ch.discord': 'Discord',
'ch.loading': 'Loading…', 'ch.empty': 'No IM process scripts found',
'ch.logEmpty': 'No log output yet',
'err.channelLoad': 'Failed to load', 'err.channelStart': 'Start failed', 'err.channelStop': 'Stop failed',
'err.mykeyImport': 'Failed to import model config',
'err.mykeyExport': 'Failed to export model config',
'err.channelNotConfigured': 'Configure this platform in mykey.py first',
'sys.channelStarted': 'Started', 'sys.channelStopped': 'Stopped',
'modal.channelLogs': 'Process logs',
'modal.mykeyConfig': 'mykey.py',
'sys.configSaved': 'Configuration saved',
'sys.mykeyImported': 'Model config imported',
'sys.mykeyExported': 'Model config exported',
'st.starting': 'Starting…', 'st.stopping': 'Stopping…', 'st.online': 'Online', 'st.offline': 'Offline', 'st.error': 'Error', 'st.running': 'Running', 'st.abnormal': 'Error',
'act.configure': 'Configure', 'act.logs': 'Logs', 'act.restart': 'Restart', 'act.stop': 'Stop', 'act.start': 'Start', 'act.exit': 'Exit',
'act.copy': 'Copy', 'act.copied': 'Copied', 'act.copyTex': 'TeX', 'act.send': 'Send',
'proc.imbotWechat': 'imbot · WeChat', 'proc.imbotDing': 'imbot · DingTalk', 'proc.scheduler': 'Scheduler', 'proc.conductor': 'Conductor',
'cm.scheduling': 'Scheduling', 'cm.running': 'Running', 'cm.idleSt': 'Idle',
'cm.master': 'Dispatched 3 subtasks', 'cm.w1': 'Subtask: fetch data', 'cm.w2': 'Subtask: review results', 'cm.sub': 'Waiting for tasks',
'tok.total': 'Total', 'tok.cost': 'Cache rate', 'tok.today': 'Today', 'tok.tabAll': 'Chat', 'tok.tabConductor': 'Conductor', 'tok.condTotal': 'Conductor Total', 'tok.condCurrent': 'Conductor Current', 'tok.condTip': 'Conductor usage is not included in chat totals', 'tok.condOffline': 'Service offline', 'tok.disclaimer': 'Pricing may vary by API provider. Please refer to the actual website.',
'tok.colSession': 'Session', 'tok.colIn': 'Input', 'tok.colOut': 'Output', 'tok.colCacheW': 'Cache write', 'tok.colCache': 'Cache read', 'tok.colCost': 'Cost',
'tok.from': 'From', 'tok.to': 'To', 'tok.reset': 'Reset', 'tok.noData': 'No records', 'tok.deleted': 'Session deleted',
'tok.pricingUnknown': '⚠ Pricing not confirmed, using defaults',
'tok.priceInput': 'Input: $', 'tok.priceOutput': 'Output: $',
'tok.priceCacheW': 'Cache write: $', 'tok.priceCacheR': 'Cache read: $',
'presetPrompt.goal': 'Enter Goal mode: read the L3 goal-mode SOP and autonomously achieve the goal I describe next.',
'presetPrompt.plan': 'Enter Plan mode: first read memory/plan_sop.md, follow its explore→plan→execute→verify flow, and wait for the task I describe next.',
'presetPrompt.autonomous': '🤖 Enter autonomous mode: read memory/autonomous_operation_sop.md, follow the SOP to pick or plan a task, execute independently, and produce a report.',
'presetPrompt.hive': 'Start Goal Hive mode: per the hive SOP, spawn multiple workers to collaboratively achieve the goal I describe next.',
'presetPrompt.review': 'Enter reviewer mode: strictly scrutinize the previous output, review item by item and report issues.',
'presetPrompt.findwork': 'Following the autonomous planning section, analyze my situation thoroughly and generate a batch of TODOs that genuinely interest me.',
'presetPrompt.mine': 'Collect this week\'s git commits and write a weekly report.',
'ask.banner': 'GA is waiting for your answer',
'ask.replyHint': 'Reply in the input below',
'ask.placeholderOpen': 'Type your answer here… (Enter to send)',
},
};
const BOOT_I18N = {
zh: {
'loading.starting_app': '???? GenericAgent...',
'loading.start': '?????????????...',
'loading.venv': '????????...',
'loading.deps': '??????...',
'loading.done': '??????',
'loading.starting': '??????...',
'setup.title': 'GenericAgent - ??',
'setup.heading': '????',
'setup.backend_failed': '???????????????',
'setup.py_label': 'Python ?????',
'setup.py_placeholder': 'python / ?????',
'setup.py_hint': '?? C:/Python312/python.exe ? ~/miniconda3/envs/myenv/bin/python',
'setup.project_label': '????',
'setup.project_placeholder': 'GenericAgent ?????',
'setup.project_hint': '?? frontends/desktop_bridge.py ????',
'setup.start': '??',
'setup.err_py': '??? Python ??',
'setup.err_project': '???????',
'setup.starting_bridge': '???? bridge...',
'setup.connected': '???????????...',
'setup.failed': '?????{error}',
'setup.limit': '???????{n}?'
},
en: {
'loading.starting_app': 'Starting GenericAgent...',
'loading.start': 'First run: preparing the runtime...',
'loading.venv': 'Creating the runtime environment...',
'loading.deps': 'Installing dependencies...',
'loading.done': 'Dependencies installed',
'loading.starting': 'Starting services...',
'setup.title': 'GenericAgent - Setup',
'setup.heading': 'Setup Required',
'setup.backend_failed': 'The backend could not start. Please configure the paths below.',
'setup.py_label': 'Python interpreter path',
'setup.py_placeholder': 'python / path to interpreter',
'setup.py_hint': 'e.g. C:/Python312/python.exe or ~/miniconda3/envs/myenv/bin/python',
'setup.project_label': 'Project directory',
'setup.project_placeholder': 'path to GenericAgent folder',
'setup.project_hint': 'The folder containing frontends/desktop_bridge.py',
'setup.start': 'Start',
'setup.err_py': 'Please enter Python path',
'setup.err_project': 'Please enter project directory',
'setup.starting_bridge': 'Starting bridge...',
'setup.connected': 'Connected. Opening the main window...',
'setup.failed': 'Failed: {error}',
'setup.limit': 'Character limit reached ({n})'
}
};
for (const [code, messages] of Object.entries(BOOT_I18N)) {
I18N[code] = Object.assign(I18N[code] || {}, messages);
}
const LANGS = Object.freeze(Object.keys(I18N));
function normalizeLang(code) {
return LANGS.includes(code) ? code : 'zh';
}
function browserLang() {
return (navigator.language || '').toLowerCase().startsWith('zh') ? 'zh' : 'en';
}
function bootLang() {
try {
const saved = localStorage.getItem('ga_lang');
if (LANGS.includes(saved)) return saved;
} catch (_) {}
return browserLang();
}
function tFor(lang, key, vars) {
const active = normalizeLang(lang);
let text = (I18N[active] && I18N[active][key]) || (I18N.zh && I18N.zh[key]) || (I18N.en && I18N.en[key]) || key;
if (vars) {
for (const [name, value] of Object.entries(vars)) {
text = text.replaceAll(`{${name}}`, String(value));
}
}
return text;
}
function apply(root, activeLang) {
const lang = normalizeLang(activeLang || bootLang());
const scope = root || document;
if (scope === document) document.documentElement.lang = lang === 'en' ? 'en' : 'zh-CN';
scope.querySelectorAll('[data-i18n]').forEach(el => {
el.textContent = tFor(lang, el.dataset.i18n);
});
scope.querySelectorAll('[data-i18n-ph]').forEach(el => {
el.setAttribute('data-ph', tFor(lang, el.dataset.i18nPh));
});
scope.querySelectorAll('[data-i18n-placeholder]').forEach(el => {
el.setAttribute('placeholder', tFor(lang, el.dataset.i18nPlaceholder));
});
scope.querySelectorAll('[data-i18n-title]').forEach(el => {
el.setAttribute('title', tFor(lang, el.dataset.i18nTitle));
el.setAttribute('aria-label', tFor(lang, el.dataset.i18nTitle));
});
}
window.GA_I18N = {
dict: I18N,
languages: LANGS,
t: tFor,
apply,
bootLang,
browserLang,
};
window.GA_BOOT_I18N = {
get lang() { return bootLang(); },
t(key, vars) { return tFor(bootLang(), key, vars); },
apply(root) { return apply(root || document, bootLang()); },
};
})();
+5 -4
View File
@@ -4,7 +4,7 @@
<meta charset="UTF-8">
<title>GenericAgent</title>
<link rel="stylesheet" href="assets/fonts/fonts.css?v=2">
<link rel="stylesheet" href="styles.css?v=182">
<link rel="stylesheet" href="styles.css?v=186">
<!-- KaTeX: LaTeX 公式渲染 -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/katex@0.16.11/dist/katex.min.css" crossorigin="anonymous">
<!-- highlight.js: 代码高亮 -->
@@ -167,7 +167,7 @@
<div class="input" id="chat-input" contenteditable="true" role="textbox" aria-multiline="true" data-i18n-ph="composer.placeholder"></div>
<div class="composer-bot">
<div class="composer-plus-wrap">
<button type="button" class="ic-btn composer-plus" id="chat-plus-btn" aria-haspopup="menu" aria-expanded="false" data-i18n-title="collab.plusMenu" title="">
<button type="button" class="ic-btn composer-plus" id="chat-plus-btn" aria-haspopup="menu" aria-expanded="false" data-i18n-title="collab.plusMenu" data-no-tooltip title="">
<span data-ga-icon="plus"></span>
</button>
<div class="ga-menu composer-menu" id="chat-menu" hidden role="menu">
@@ -317,7 +317,7 @@
<div class="input" id="cdb-input" contenteditable="true" role="textbox" aria-multiline="true" data-i18n-ph="collab.placeholder"></div>
<div class="composer-bot">
<div class="composer-plus-wrap">
<button type="button" class="ic-btn composer-plus" id="cdb-plus-btn" aria-haspopup="menu" aria-expanded="false" data-i18n-title="collab.plusMenu" title="">
<button type="button" class="ic-btn composer-plus" id="cdb-plus-btn" aria-haspopup="menu" aria-expanded="false" data-i18n-title="collab.plusMenu" data-no-tooltip title="">
<span data-ga-icon="plus"></span>
</button>
<div class="ga-menu composer-menu" id="cdb-menu" hidden role="menu">
@@ -674,6 +674,7 @@
<!-- highlight.js -->
<script src="https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@11.9.0/build/highlight.min.js" crossorigin="anonymous"></script>
<script src="phosphor-icons.js?v=2"></script>
<script src="app.js?v=211"></script>
<script src="i18n.js?v=1"></script>
<script src="app.js?v=216"></script>
</body>
</html>
+9 -15
View File
@@ -17,21 +17,16 @@ html,body{height:100%;margin:0;display:flex;align-items:center;justify-content:c
#ga-bar.indet{width:35%;animation:indet 1.1s ease-in-out infinite}
@keyframes indet{0%{margin-left:-35%}100%{margin-left:100%}}
</style>
<script src="i18n.js?v=1"></script>
<script>
// Loading window shows before the main UI, on a different origin than the app, so it can't
// read the app's saved language; fall back to the system/browser locale.
var GA_ZH = (navigator.language || '').toLowerCase().indexOf('zh') === 0;
var GA_MSG = {
starting_app: GA_ZH ? '正在启动 GenericAgent…' : 'Starting GenericAgent…',
start: GA_ZH ? '首次启动,正在准备运行环境…' : 'First run: preparing the runtime…',
venv: GA_ZH ? '正在创建运行环境…' : 'Creating the runtime environment…',
deps: GA_ZH ? '正在安装依赖…' : 'Installing dependencies…',
done: GA_ZH ? '依赖安装完成' : 'Dependencies installed',
starting: GA_ZH ? '正在启动服务…' : 'Starting services…'
};
const bootT = window.GA_BOOT_I18N?.t || ((key) => key);
window.gaProgress = function(pct, key){
var s = document.getElementById('ga-status');
if (s) s.textContent = GA_MSG[key] || key;
if (s) {
var msgKey = 'loading.' + key;
var msg = bootT(msgKey);
s.textContent = msg === msgKey ? key : msg;
}
var bar = document.querySelector('.bar');
var fill = document.getElementById('ga-bar');
if (bar) bar.classList.add('show');
@@ -41,15 +36,14 @@ window.gaProgress = function(pct, key){
}
};
document.addEventListener('DOMContentLoaded', function(){
var s = document.getElementById('ga-status');
if (s) s.textContent = GA_MSG.starting_app;
window.GA_BOOT_I18N?.apply(document);
});
</script>
</head>
<body>
<div class="card">
<div class="spinner"></div>
<div id="ga-status">正在启动 GenericAgent…</div>
<div id="ga-status" data-i18n="loading.starting_app"></div>
<div class="bar"><div id="ga-bar"></div></div>
</div>
</body>
+220 -85
View File
@@ -92,8 +92,11 @@
--shadow-pop:0 8px 24px #14192829;
--shadow-modal:0 18px 50px #14192847;
--divider-base:rgba(15, 74, 133, 0.22);
--divider-base:rgba(0,0,0,.12);
--divider:var(--divider-base);
--tooltip-bg:var(--card);
--tooltip-fg:var(--txt);
--tooltip-border:var(--line-strong);
--radius:4px; --radius-sm:2px;
/* —— 折叠块 —— */
@@ -124,7 +127,7 @@ html[data-appearance="light"]:not([data-plain="1"])[data-theme]{
--bg:color-mix(in srgb, var(--accent) 7%, var(--bg-base));
--line:color-mix(in srgb, var(--accent) 16%, var(--line-base));
--line-soft:color-mix(in srgb, var(--accent) 10%, var(--line-soft-base));
--divider:color-mix(in srgb, var(--accent) 22%, var(--divider-base));
--divider:var(--divider-base);
}
/* —— 外观:深色 —— */
@@ -174,7 +177,7 @@ html[data-appearance="dark"]{
--fold-tool-bg:#16202b;
--fold-result-border:#3f6647;
--fold-result-bg:#15241a;
--divider-base:#569cd6;
--divider-base:rgba(255,255,255,.10);
--divider:var(--divider-base);
/* 发送键:深色用鲜亮深蓝 */
--ink-green:#2559d8;
@@ -183,7 +186,7 @@ html[data-appearance="dark"]{
html[data-appearance="dark"][data-theme]{
--accent-bg:color-mix(in srgb, var(--accent) 22%, var(--panel));
--accent-soft:color-mix(in srgb, var(--accent) 12%, var(--panel));
--divider:color-mix(in srgb, var(--accent) 18%, var(--divider-base));
--divider:var(--divider-base);
}
html,body{ height:100%; }
@@ -400,6 +403,17 @@ body{ background:var(--bg); color:var(--txt); font-size:13px; overflow:hidden; f
transition:opacity .18s, transform .18s; z-index:9999;
}
.ga-toast.show{ opacity:1; transform:translateX(-50%) translateY(0); }
.ga-tooltip{
position:fixed; z-index:10000; max-width:min(320px, calc(100vw - 16px));
padding:6px 8px; border:1px solid var(--tooltip-border); border-radius:4px;
background:var(--tooltip-bg);
color:var(--tooltip-fg); font:12px/1.35 var(--font-sans);
box-shadow:0 8px 20px #00000026;
pointer-events:none; opacity:0; transform:translateY(2px);
transition:opacity .12s, transform .12s;
white-space:normal; overflow-wrap:anywhere;
}
.ga-tooltip.show{ opacity:1; transform:translateY(0); }
.set-btn{
width:100%; display:flex; align-items:center; gap:8px;
border:1px solid var(--line); background:var(--card); color:var(--txt-2);
@@ -584,7 +598,7 @@ body{ background:var(--bg); color:var(--txt); font-size:13px; overflow:hidden; f
background:var(--field-bg); cursor:pointer; text-align:left;
transition:border-color .15s, background .15s;
}
.pq-btn:hover{ background:var(--card); border-color:var(--accent); }
.pq-btn:hover{ background:var(--line-soft); border-color:var(--line-base); }
.pq-logo{
flex:0 0 auto; width:26px; height:26px; border-radius:7px;
display:flex; align-items:center; justify-content:center;
@@ -594,7 +608,7 @@ body{ background:var(--bg); color:var(--txt); font-size:13px; overflow:hidden; f
.pq-btn-name{ font-size:12px; font-weight:500; color:var(--txt); line-height:1.35; }
.pq-btn-desc{ font-size:10px; color:var(--muted); line-height:1; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
.pq-arrow{ flex:0 0 auto; width:13px; height:13px; color:var(--muted2); transition:transform .15s, color .15s; }
.pq-btn:hover .pq-arrow{ color:var(--accent); transform:translateX(2px); }
.pq-btn:hover .pq-arrow{ color:var(--txt-2); transform:none; }
/* ── 添加模型弹窗内的接入指引横幅 ── */
.model-guide{
@@ -721,9 +735,10 @@ body{ background:var(--bg); color:var(--txt); font-size:13px; overflow:hidden; f
.run-state.busy .dot::before{
content:'';
position:absolute;
left:50%; top:50%;
inset:0;
margin:auto;
width:var(--breathe-core); height:var(--breathe-core);
transform:translate(-50%,-50%);
transform:none;
border-radius:50%;
background:var(--breathe-color);
z-index:2;
@@ -732,9 +747,11 @@ body{ background:var(--bg); color:var(--txt); font-size:13px; overflow:hidden; f
.run-state.busy .dot::after{
content:'';
position:absolute;
left:50%; top:50%;
inset:0;
margin:auto;
width:var(--breathe-core); height:var(--breathe-core);
transform:translate(-50%,-50%);
transform:none;
transform-origin:center;
border-radius:50%;
border:1px solid color-mix(in srgb, var(--breathe-color) 52%, transparent);
z-index:1;
@@ -742,8 +759,8 @@ body{ background:var(--bg); color:var(--txt); font-size:13px; overflow:hidden; f
animation:ga-status-breathe 1.75s ease-out infinite;
}
@keyframes ga-status-breathe{
0%{ transform:translate(-50%,-50%) scale(1); opacity:.55; }
100%{ transform:translate(-50%,-50%) scale(2.35); opacity:0; }
0%{ transform:scale(1); opacity:.55; }
100%{ transform:scale(2.35); opacity:0; }
}
.link{ color:var(--accent); cursor:pointer; }
.muted{ color:var(--muted); }
@@ -1263,10 +1280,6 @@ span.flatpickr-weekday{ color:var(--muted) !important; font-size:11px; }
align-self:end;
}
.composer-inset.is-inline .composer-bot .spacer{ display:none; }
.composer-inset:focus-within{
border-color:var(--accent);
box-shadow:0 0 0 3px color-mix(in srgb, var(--accent) 12%, transparent);
}
.composer-inset .input{
width:100%; min-height:38px; max-height:160px; overflow-y:auto;
border:none; border-radius:0; background:transparent;
@@ -1368,7 +1381,7 @@ span.flatpickr-weekday{ color:var(--muted) !important; font-size:11px; }
margin:0 -3px;
}
.rp-resize:hover, .rp-resize.dragging,
.sb-resize:hover, .sb-resize.dragging{ background:var(--accent); }
.sb-resize:hover, .sb-resize.dragging{ background:var(--line-strong); }
.body.rp-collapsed .rp-resize,
.body.sb-collapsed .sb-resize{ display:none; }
@@ -1496,58 +1509,67 @@ span.flatpickr-weekday{ color:var(--muted) !important; font-size:11px; }
}
.plan-card{
position:absolute;
left:10px;
bottom:100%;
margin-bottom:0;
left:12px;
bottom:calc(100% + 8px);
z-index:13;
width:min(600px, 100%);
width:min(640px, calc(100% - 24px));
min-width:280px;
max-width:100%;
box-sizing:border-box;
font-size:12px;
line-height:1.35;
font-size:13px;
line-height:1.45;
color:var(--txt);
pointer-events:auto;
}
.plan-card[hidden]{ display:none !important; }
.plan-card--expanded{
padding:10px 12px 8px;
background:var(--card);
padding:14px 16px 12px 18px;
background:color-mix(in srgb, var(--card) 94%, var(--accent) 6%);
border:1px solid var(--line);
border-bottom:none;
border-left:3px solid var(--accent);
border-radius:var(--radius-sm) var(--radius-sm) 0 0;
box-shadow:0 -4px 16px #14192812, 0 -1px 0 var(--line) inset;
border-radius:8px;
box-shadow:0 12px 32px #14192818, 0 1px 2px #14192814;
overflow:hidden;
}
.plan-card--expanded::before{
content:"";
position:absolute;
left:0;
top:0;
bottom:0;
width:4px;
background:var(--accent);
}
.plan-card--collapsed{
width:auto;
min-width:0;
max-width:min(520px, 70%);
margin-bottom:0;
}
/* 吸附:卡片底与输入框顶共边,视觉上连成一体 */
/* Keep the composer rounded while the plan card floats above it. */
.composer-slot:has(.plan-card:not([hidden])) .composer-inset{
border-top-left-radius:0;
border-top-right-radius:0;
border-top-left-radius:var(--radius);
border-top-right-radius:var(--radius);
}
.composer-slot:has(.plan-card--collapsed:not([hidden])) .plan-capsule{
border-radius:var(--radius-sm) var(--radius-sm) 0 0;
border-bottom:1px solid var(--line);
box-shadow:0 -4px 12px #14192810;
box-shadow:0 10px 24px #14192814, 0 1px 2px #14192810;
}
.plan-card-head{
display:flex;
align-items:center;
gap:8px;
margin-bottom:6px;
gap:10px;
margin-bottom:10px;
min-height:26px;
}
.plan-status-dot{
flex:0 0 auto;
width:7px; height:7px;
width:8px; height:8px;
border-radius:50%;
background:var(--accent);
box-shadow:0 0 0 4px color-mix(in srgb, var(--accent) 14%, transparent);
}
.plan-card--complete .plan-status-dot{
background:var(--success);
box-shadow:0 0 0 4px var(--success-ring);
}
.plan-card--complete .plan-status-dot{ background:var(--success); }
.plan-card--placeholder .plan-status-dot{
animation:plan-dot-pulse 1.4s ease-in-out infinite;
}
@@ -1557,8 +1579,8 @@ span.flatpickr-weekday{ color:var(--muted) !important; font-size:11px; }
}
.plan-title{
flex:1 1 auto;
font-weight:600;
font-size:12px;
font-weight:700;
font-size:14px;
color:var(--txt-strong);
min-width:0;
white-space:nowrap;
@@ -1567,104 +1589,198 @@ span.flatpickr-weekday{ color:var(--muted) !important; font-size:11px; }
}
.plan-progress{
flex:0 0 auto;
font-weight:600;
display:inline-flex;
align-items:center;
min-height:24px;
padding:0 8px;
border:1px solid color-mix(in srgb, var(--accent) 26%, var(--line));
border-radius:999px;
background:color-mix(in srgb, var(--accent) 9%, var(--card));
font-weight:700;
font-size:12px;
color:var(--accent);
color:var(--txt-strong);
font-family:var(--font-num);
font-variant-numeric:var(--font-num-variant);
}
.plan-card--complete .plan-progress{ color:var(--success); }
.plan-card--complete .plan-progress{
color:var(--success);
border-color:color-mix(in srgb, var(--success) 28%, var(--line));
background:var(--success-bg);
}
.plan-head-actions{
flex:0 0 auto;
display:flex;
align-items:center;
gap:4px;
gap:6px;
}
.plan-btn{
border:0;
display:inline-flex;
align-items:center;
justify-content:center;
gap:5px;
min-height:26px;
border:1px solid transparent;
background:transparent;
padding:2px 6px;
font-size:11px;
font-weight:500;
color:var(--accent);
padding:0 8px;
font-size:12px;
font-weight:600;
color:var(--muted);
cursor:pointer;
border-radius:var(--radius-sm);
border-radius:6px;
font-family:var(--font-sans);
transition:background .15s, border-color .15s, color .15s;
}
.plan-btn:hover{
background:var(--field-bg);
border-color:var(--line);
color:var(--txt-strong);
}
.plan-btn-ic{
width:12px;
height:12px;
flex:0 0 auto;
}
.plan-btn:hover{ background:var(--accent-soft); }
.plan-current{
display:flex;
gap:4px;
gap:6px;
align-items:flex-start;
margin-bottom:8px;
padding-left:15px;
margin:0 0 10px 0;
padding:8px 10px;
border:1px solid var(--line-soft);
border-radius:6px;
background:color-mix(in srgb, var(--field-bg) 82%, var(--accent) 18%);
}
.plan-current-label{
flex:0 0 auto;
color:var(--muted);
font-size:11px;
font-size:12px;
}
.plan-current-text{
flex:1 1 auto;
color:var(--accent);
font-weight:500;
color:var(--txt-strong);
font-weight:600;
word-break:break-word;
}
.plan-card--complete .plan-current-text{ color:var(--success); }
.plan-wait{
color:var(--muted2);
padding:0 0 4px 15px;
font-size:11px;
padding:4px 0 2px;
font-size:12px;
}
.plan-items{
list-style:none;
margin:0;
padding:0 0 4px 15px;
padding:0;
}
.plan-item{
display:flex;
gap:6px;
gap:10px;
align-items:flex-start;
margin-top:3px;
padding:5px 0;
min-height:26px;
}
.plan-item-mark{
flex:0 0 auto;
width:1.1em;
text-align:center;
line-height:1.35;
position:relative;
display:inline-flex;
align-items:center;
justify-content:center;
width:16px;
height:16px;
margin-top:2px;
border:1px solid var(--line-strong);
border-radius:50%;
background:var(--card);
color:var(--muted);
font-size:0;
}
.plan-item-text{
flex:1 1 auto;
word-break:break-word;
color:var(--txt-2);
}
.plan-item--done .plan-item-mark{
color:var(--success);
border-color:color-mix(in srgb, var(--success) 40%, var(--line));
background:var(--success-bg);
}
.plan-item--done .plan-item-mark::after{
content:"";
width:7px;
height:4px;
border-left:2px solid currentColor;
border-bottom:2px solid currentColor;
transform:translateY(-1px) rotate(-45deg);
}
.plan-item--done .plan-item-mark,
.plan-item--done .plan-item-text{
color:var(--muted);
opacity:.75;
opacity:.78;
}
.plan-item--done .plan-item-text{ text-decoration:line-through; }
.plan-item--current .plan-item-mark{ color:var(--accent); }
.plan-item--current .plan-item-text{ color:var(--accent); font-weight:500; }
.plan-item--pending .plan-item-mark,
.plan-item--current .plan-item-mark{
border-color:var(--accent);
background:var(--accent);
box-shadow:0 0 0 4px color-mix(in srgb, var(--accent) 12%, transparent);
}
.plan-item--current .plan-item-mark::after{
content:"";
width:5px;
height:5px;
border-radius:50%;
background:var(--on-accent);
}
.plan-item--current .plan-item-text{
color:var(--txt-strong);
font-weight:700;
}
.plan-item--pending .plan-item-mark::after{
content:"";
width:4px;
height:4px;
border-radius:50%;
background:var(--line-strong);
}
.plan-item--pending .plan-item-text{ color:var(--muted); }
.plan-item--error .plan-item-mark,
.plan-item--error .plan-item-mark{
color:var(--danger);
border-color:color-mix(in srgb, var(--danger) 38%, var(--line));
background:var(--danger-bg);
}
.plan-item--error .plan-item-mark::before,
.plan-item--error .plan-item-mark::after{
content:"";
position:absolute;
width:8px;
height:2px;
border-radius:999px;
background:currentColor;
}
.plan-item--error .plan-item-mark::before{ transform:rotate(45deg); }
.plan-item--error .plan-item-mark::after{ transform:rotate(-45deg); }
.plan-item--error .plan-item-text{ color:var(--danger); }
.plan-item--warn .plan-item-mark,
.plan-item--warn .plan-item-text{ color:#c67600; }
.plan-item--warn .plan-item-mark{
color:#c67600;
border-color:color-mix(in srgb, #c67600 38%, var(--line));
background:color-mix(in srgb, #c67600 12%, var(--card));
font-size:10px;
font-weight:800;
}
.plan-item--warn .plan-item-mark::after{ content:"!"; }
.plan-item--warn .plan-item-text{ color:#9b6100; }
html[data-appearance="dark"] .plan-item--warn .plan-item-mark,
html[data-appearance="dark"] .plan-item--warn .plan-item-text{ color:#f0a020; }
.plan-foot{
display:flex;
justify-content:flex-end;
gap:8px;
padding-top:4px;
padding-top:10px;
border-top:1px solid var(--line-soft);
margin-top:4px;
margin-top:6px;
}
.plan-more-hint{
flex:1 1 auto;
align-self:center;
font-size:11px;
font-size:12px;
color:var(--muted2);
text-align:left;
}
@@ -1673,19 +1789,36 @@ html[data-appearance="dark"] .plan-item--warn .plan-item-text{ color:#f0a020; }
align-items:center;
gap:8px;
max-width:100%;
padding:6px 12px;
min-height:34px;
padding:0 12px 0 10px;
border:1px solid var(--line);
border-left:3px solid var(--accent);
border-radius:999px;
background:var(--card);
background:color-mix(in srgb, var(--card) 94%, var(--accent) 6%);
cursor:pointer;
font-size:12px;
font-weight:500;
font-weight:600;
color:var(--txt);
box-shadow:var(--shadow);
box-shadow:0 10px 24px #14192814, 0 1px 2px #14192810;
font-family:var(--font-sans);
transition:background .15s, border-color .15s, box-shadow .15s;
}
.plan-capsule:hover{
border-color:var(--line-strong);
background:var(--card);
box-shadow:0 12px 28px #1419281f, 0 1px 2px #14192812;
}
.plan-capsule-ic{
width:15px;
height:15px;
flex:0 0 auto;
color:var(--muted);
}
.plan-capsule-chevron{
width:12px;
height:12px;
flex:0 0 auto;
color:var(--muted2);
}
.plan-capsule:hover{ border-color:var(--line-strong); background:var(--field-bg); }
.plan-capsule-text{
min-width:0;
white-space:nowrap;
@@ -1698,7 +1831,9 @@ html[data-appearance="dark"] .plan-item--warn .plan-item-text{ color:#f0a020; }
color:var(--accent);
font-weight:600;
}
.plan-card--complete .plan-capsule{ border-left-color:var(--success); }
.plan-card--complete .plan-capsule{
background:color-mix(in srgb, var(--card) 88%, var(--success) 12%);
}
.plan-card--complete .plan-capsule-text em{ color:var(--success); }
/* ─── chat composer: Plan/Auto toggle ─── */
+20 -5
View File
@@ -70,6 +70,24 @@ def strip_final_info_marker(text: Any) -> str:
return _FINAL_INFO_RE.sub('', str(text or ''))
def normalize_final_turn_segs(full: str, outputs: Any) -> Optional[List[str]]:
if not outputs or not isinstance(outputs, (list, tuple)):
return None
segs = [strip_final_info_marker(s) for s in outputs]
full_text = strip_final_info_marker(full)
if not segs:
return None
joined = "".join(segs)
if full_text.strip() == joined.strip():
return segs
if joined and full_text.startswith(joined):
suffix = full_text[len(joined):]
if suffix.strip():
segs[-1] = segs[-1] + suffix
return segs
return None
for _s in (sys.stdout, sys.stderr):
with contextlib.suppress(Exception):
_s.reconfigure(encoding="utf-8", errors="replace")
@@ -664,9 +682,8 @@ class AgentManager:
_segs[_idx - 1] = str(_outs[-2])
if "done" in item:
full = strip_final_info_marker(item.get("done") or "")
done_outputs = item.get("outputs") # done时=turn_resps.copy()全量轮
done_outputs = normalize_final_turn_segs(full, item.get("outputs")) # done时=turn_resps.copy()全量轮
if done_outputs:
done_outputs = [strip_final_info_marker(s) for s in done_outputs]
with self.lock:
if sess.partial is not None:
sess.partial["content"] = full
@@ -696,12 +713,10 @@ class AgentManager:
with self.lock:
sess.partial = None
full = strip_final_info_marker(full)
if done_outputs:
done_outputs = [strip_final_info_marker(s) for s in done_outputs]
import plan_state
plan_state.sync_plan_path_from_text(sess, full, sess.cwd or self.ga_root)
# 轨道2: 落库时带结构化全量轮(权威turn_segs),前端按轮渲染;content保留兜底
_final_segs = [str(s) for s in done_outputs] if done_outputs else None
_final_segs = normalize_final_turn_segs(full, done_outputs)
if _final_segs:
self.add_message(sess, "assistant", full, turn_segs=_final_segs)
else: