feihong-code 0.2.3 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/FUNDING.yml +4 -0
- package/.github/ISSUE_TEMPLATE/bug_report.md +37 -37
- package/.github/ISSUE_TEMPLATE/config.yml +14 -14
- package/.github/ISSUE_TEMPLATE/feature_request.md +28 -28
- package/.github/PULL_REQUEST_TEMPLATE.md +46 -46
- package/.github/SECURITY.md +67 -67
- package/.github/dependabot.yml +16 -0
- package/.github/workflows/ci.yml +1 -1
- package/AGENT-GUIDE.md +382 -237
- package/CHANGELOG.md +392 -0
- package/CODE_OF_CONDUCT.md +56 -56
- package/CONTRIBUTING.md +68 -68
- package/LICENSE +22 -22
- package/README.md +586 -522
- package/README.self-evolve.md +274 -0
- package/dist/agent/code-review.js +2 -0
- package/dist/agent/code-review.js.map +1 -1
- package/dist/agent/code-writer.js +74 -18
- package/dist/agent/code-writer.js.map +1 -1
- package/dist/agent/context-compactor.js +13 -4
- package/dist/agent/context-compactor.js.map +1 -1
- package/dist/agent/experience.js +319 -84
- package/dist/agent/experience.js.map +1 -1
- package/dist/agent/orchestrator.js +233 -71
- package/dist/agent/orchestrator.js.map +1 -1
- package/dist/agent/planner.js +30 -7
- package/dist/agent/planner.js.map +1 -1
- package/dist/agent/prompts.js +9 -0
- package/dist/agent/prompts.js.map +1 -1
- package/dist/agent/quality-gate.js +10 -4
- package/dist/agent/quality-gate.js.map +1 -1
- package/dist/agent/repo-context.js +181 -0
- package/dist/agent/repo-context.js.map +1 -0
- package/dist/agent/repo-reader.js +92 -99
- package/dist/agent/repo-reader.js.map +1 -1
- package/dist/agent/self-heal.js +80 -60
- package/dist/agent/self-heal.js.map +1 -1
- package/dist/agent/self-improver.js +65 -61
- package/dist/agent/self-improver.js.map +1 -1
- package/dist/agent/subagent-summary.js +31 -0
- package/dist/agent/subagent-summary.js.map +1 -0
- package/dist/agent/subagent.js +52 -2
- package/dist/agent/subagent.js.map +1 -1
- package/dist/agent/symbol-index.js +160 -0
- package/dist/agent/symbol-index.js.map +1 -0
- package/dist/agent/team.js +193 -0
- package/dist/agent/team.js.map +1 -0
- package/dist/cli/commands.js +124 -143
- package/dist/cli/commands.js.map +1 -1
- package/dist/cli/index.js +113 -106
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/repl.js +82 -7
- package/dist/cli/repl.js.map +1 -1
- package/dist/cli/run.js +600 -95
- package/dist/cli/run.js.map +1 -1
- package/dist/cli/tui.js +145 -0
- package/dist/cli/tui.js.map +1 -0
- package/dist/cli/version.js +1 -1
- package/dist/enterprise/audit.js +145 -23
- package/dist/enterprise/audit.js.map +1 -1
- package/dist/enterprise/index.js +14 -11
- package/dist/enterprise/index.js.map +1 -1
- package/dist/enterprise/policy.js +22 -10
- package/dist/enterprise/policy.js.map +1 -1
- package/dist/harness/executor.js +127 -0
- package/dist/harness/executor.js.map +1 -0
- package/dist/harness/harness.js +87 -0
- package/dist/harness/harness.js.map +1 -0
- package/dist/harness/index.js +31 -0
- package/dist/harness/index.js.map +1 -0
- package/dist/harness/loader.js +138 -0
- package/dist/harness/loader.js.map +1 -0
- package/dist/harness/reporter.js +34 -0
- package/dist/harness/reporter.js.map +1 -0
- package/dist/harness/types.js +10 -0
- package/dist/harness/types.js.map +1 -0
- package/dist/harness/verifier.js +48 -0
- package/dist/harness/verifier.js.map +1 -0
- package/dist/hello.js +14 -0
- package/dist/hello.js.map +1 -0
- package/dist/memory/auto-summarize.js +208 -0
- package/dist/memory/auto-summarize.js.map +1 -0
- package/dist/memory/index.js +228 -0
- package/dist/memory/index.js.map +1 -0
- package/dist/models/model-router.js +100 -27
- package/dist/models/model-router.js.map +1 -1
- package/dist/models/model.dto.js +25 -3
- package/dist/models/model.dto.js.map +1 -1
- package/dist/models/providers/ollama.provider.js +12 -0
- package/dist/models/providers/ollama.provider.js.map +1 -1
- package/dist/models/providers/openai-compatible.provider.js +14 -1
- package/dist/models/providers/openai-compatible.provider.js.map +1 -1
- package/dist/plugins/plugin-loader.js +179 -0
- package/dist/plugins/plugin-loader.js.map +1 -0
- package/dist/runtime/event-log.js.map +1 -1
- package/dist/runtime/hooks.js +80 -0
- package/dist/runtime/hooks.js.map +1 -0
- package/dist/self-evolve/hook.js +60 -0
- package/dist/self-evolve/hook.js.map +1 -0
- package/dist/self-evolve/hook.ts +80 -0
- package/dist/self-evolve/manager.d.ts +8 -0
- package/dist/self-evolve/manager.js +401 -0
- package/dist/shared/config.js +35 -3
- package/dist/shared/config.js.map +1 -1
- package/dist/shared/errors.js +6 -2
- package/dist/shared/errors.js.map +1 -1
- package/dist/shared/i18n.js +535 -0
- package/dist/shared/i18n.js.map +1 -0
- package/dist/shared/secure-store.js +117 -0
- package/dist/shared/secure-store.js.map +1 -0
- package/dist/skills/grill.js +2 -1
- package/dist/skills/grill.js.map +1 -1
- package/dist/skills/self-heal.js +73 -0
- package/dist/skills/self-heal.js.map +1 -0
- package/dist/skills/skill-loader.js +131 -0
- package/dist/skills/skill-loader.js.map +1 -0
- package/dist/skills/skill-market.js +195 -0
- package/dist/skills/skill-market.js.map +1 -0
- package/dist/tools/analysis/code-analyzer.js +45 -18
- package/dist/tools/analysis/code-analyzer.js.map +1 -1
- package/dist/tools/index.js +5 -0
- package/dist/tools/index.js.map +1 -1
- package/dist/tools/mcp/index.js +84 -0
- package/dist/tools/mcp/index.js.map +1 -0
- package/dist/tools/mcp/mcp-client.js +194 -0
- package/dist/tools/mcp/mcp-client.js.map +1 -0
- package/dist/tools/sandbox.js +126 -0
- package/dist/tools/sandbox.js.map +1 -0
- package/dist/tools/shell/exec.js +72 -3
- package/dist/tools/shell/exec.js.map +1 -1
- package/dist/tools/shell/run-shell.tool.js +37 -7
- package/dist/tools/shell/run-shell.tool.js.map +1 -1
- package/dist/tools/skills/load-skill.tool.js +36 -0
- package/dist/tools/skills/load-skill.tool.js.map +1 -0
- package/dist/tools/tool.interface.js.map +1 -1
- package/dist/tools/tool.registry.js +69 -1
- package/dist/tools/tool.registry.js.map +1 -1
- package/dist/tools/web/web.tool.js +139 -0
- package/dist/tools/web/web.tool.js.map +1 -0
- package/dist/web/auth.js +178 -3
- package/dist/web/auth.js.map +1 -1
- package/dist/web/channels.js +171 -0
- package/dist/web/channels.js.map +1 -0
- package/dist/web/public/css/style.css +1546 -0
- package/dist/web/public/index.html +804 -37
- package/dist/web/public/js/api.js +309 -0
- package/dist/web/public/js/app.js +1472 -0
- package/dist/web/public/js/ui.js +832 -0
- package/dist/web/public/js/utils.js +170 -0
- package/dist/web/server.js +937 -11
- package/dist/web/server.js.map +1 -1
- package/dist/web/task-queue.js +470 -0
- package/dist/web/task-queue.js.map +1 -0
- package/dist/web/web-config.js +143 -0
- package/dist/web/web-config.js.map +1 -0
- package/docs/App/344/275/277/347/224/250/350/257/264/346/230/216/344/271/246.md +299 -0
- package/docs/App/346/212/200/346/234/257/350/257/264/346/230/216/344/271/246.md +554 -0
- package/docs/Deployment_Guide_EN.md +288 -0
- package/docs/SELF-EVOLVE-GUIDE.md +313 -0
- package/docs/Technical_Manual_EN.md +216 -0
- package/docs/User_Manual_EN.md +314 -0
- package/docs/error-codes.md +198 -0
- package/docs/screenshots/cli-demo.png +0 -0
- package/docs/screenshots/feature-comparison.png +0 -0
- package/docs/screenshots/web-console.png +0 -0
- package/docs/self-evolve-implementation.md +165 -0
- package/docs/self-evolve.md +166 -0
- package/docs//344/272/247/345/223/201/345/274/200/345/217/221/346/226/207/346/241/243.md +614 -614
- package/docs//344/274/201/344/270/232/351/203/250/347/275/262/344/270/216/345/220/210/350/247/204.md +267 -267
- package/docs//344/275/277/347/224/250/350/257/264/346/230/216/344/271/246.md +318 -358
- package/docs//345/270/270/350/247/201/351/227/256/351/242/230/344/270/216/346/225/205/351/232/234/346/216/222/346/237/245.md +130 -130
- package/docs//346/212/200/346/234/257/350/257/264/346/230/216/344/271/246.md +216 -303
- package/docs//346/236/266/346/236/204/344/270/216API.md +238 -238
- package/docs//347/224/250/346/210/267/346/211/213/345/206/214.md +196 -196
- package/docs//351/203/250/347/275/262/346/214/207/345/215/227.md +165 -165
- package/docs//351/203/250/347/275/262/350/257/264/346/230/216/344/271/246.md +288 -0
- package/docs//351/205/215/347/275/256/345/217/202/350/200/203.md +127 -127
- package/docs//351/241/265/351/235/242/345/212/237/350/203/275/345/244/215/347/233/230/344/270/216/345/206/222/347/203/237/346/265/213/350/257/225/346/212/245/345/221/212.html +117 -0
- package/package.json +108 -109
- package/tool-schema.json +117 -46
|
@@ -0,0 +1,1472 @@
|
|
|
1
|
+
// i18n 双语字典
|
|
2
|
+
const I18N = {
|
|
3
|
+
zh: {
|
|
4
|
+
'app.title_login': '飞虹 Code 控制台',
|
|
5
|
+
'app.login_hint': '请输入手机号码直接登录',
|
|
6
|
+
'app.brand': '飞虹 Code',
|
|
7
|
+
'login.phone_label': '手机号码',
|
|
8
|
+
'login.btn_login': '手机号登录',
|
|
9
|
+
'nav.chat': '💬 对话',
|
|
10
|
+
'nav.automations': '⚡ 自动化',
|
|
11
|
+
'nav.templates': '📚 模板库',
|
|
12
|
+
'nav.market': '🧩 插件',
|
|
13
|
+
'nav.office': '📎 办公',
|
|
14
|
+
'nav.memory': '🧠 记忆',
|
|
15
|
+
'nav.release': '🚀 发布',
|
|
16
|
+
'sidebar.nav': '导航',
|
|
17
|
+
'sidebar.chat': '对话任务',
|
|
18
|
+
'sidebar.automations': '自动化',
|
|
19
|
+
'sidebar.templates': '模板库',
|
|
20
|
+
'sidebar.market': '插件市场',
|
|
21
|
+
'sidebar.office': '办公助理',
|
|
22
|
+
'sidebar.memory': '记忆系统',
|
|
23
|
+
'sidebar.user': '用户',
|
|
24
|
+
'sidebar.settings': '设置',
|
|
25
|
+
'sidebar.task_list': '任务列表',
|
|
26
|
+
'btn.new_task': '+ 新建任务',
|
|
27
|
+
'empty.no_tasks': '暂无任务',
|
|
28
|
+
'empty.no_skills': '没有找到技能',
|
|
29
|
+
'empty.no_builtin_templates': '无内置模板',
|
|
30
|
+
'empty.no_models': '还没有大模型配置,请在下方添加。',
|
|
31
|
+
'empty.preparing_welcome': '正在为您准备引导任务,请稍候…',
|
|
32
|
+
'task.not_selected': '未选择任务',
|
|
33
|
+
'task.select_hint': '从左侧任务列表选择,或在下方输入指令发起新任务',
|
|
34
|
+
'task.refresh': '刷新当前任务',
|
|
35
|
+
'chat.welcome': '点击「+ 新建任务」开启一个新任务,或直接在下方向当前任务发送消息——所有对话都会归属同一个任务运行。',
|
|
36
|
+
'chat.menu': '+ 菜单',
|
|
37
|
+
'chat.screenshot': '📷 截图',
|
|
38
|
+
'chat.upload_file': '📄 上传文件',
|
|
39
|
+
'chat.upload_image': '🖼 上传图片',
|
|
40
|
+
'chat.direct_mode': '🖥 直接操作电脑',
|
|
41
|
+
'chat.voice': '🎤 语音输入',
|
|
42
|
+
'chat.agent_type': '🤖 选择智能体类型',
|
|
43
|
+
'chat.agent_default': '🤖 通用助手',
|
|
44
|
+
'chat.listening': '● 正在听…',
|
|
45
|
+
'chat.send': '▶ 发送',
|
|
46
|
+
'common.loading': '加载中…',
|
|
47
|
+
'perm.default': '默认权限',
|
|
48
|
+
'call.hint': '通话',
|
|
49
|
+
'call.voice': '📞 语音',
|
|
50
|
+
'call.video': '🎥 视频',
|
|
51
|
+
'call.hangup': '⛔ 挂断',
|
|
52
|
+
'crop.tip': '按住鼠标拖拽选择截图区域 · 松开确认 · ESC 取消',
|
|
53
|
+
'page.automations.sub': '把常用目标存成指令,一键发起任务',
|
|
54
|
+
'btn.new_automation': '+ 新建指令',
|
|
55
|
+
'empty.no_automations': '还没有快捷指令',
|
|
56
|
+
'page.templates.sub': '点击模板即可填充到输入框',
|
|
57
|
+
'btn.save_template': '+ 保存当前为模板',
|
|
58
|
+
'tpl.builtin': '内置模板',
|
|
59
|
+
'tpl.my': '我的模板',
|
|
60
|
+
'empty.no_templates': '暂无自定义模板',
|
|
61
|
+
'page.market.sub': '聚合 ClawHub 与 Agent-Foundry 技能生态',
|
|
62
|
+
'market.search_placeholder': '搜索…',
|
|
63
|
+
'market.source_all': '全部来源',
|
|
64
|
+
'btn.refresh': '刷新',
|
|
65
|
+
'market.installed': '已安装',
|
|
66
|
+
'empty.no_installed': '暂无已安装技能',
|
|
67
|
+
'page.office.sub': '文档处理快捷入口,点击即生成提示词',
|
|
68
|
+
'page.memory.sub': '短期记忆每日自动整理,沉淀长期知识',
|
|
69
|
+
'btn.refresh_mem': '🔄 刷新',
|
|
70
|
+
'btn.summarize': '✨ 立即总结',
|
|
71
|
+
'mem.stat_short_days': '短期记忆天数',
|
|
72
|
+
'mem.stat_long_items': '长期记忆条目',
|
|
73
|
+
'mem.stat_last_summary': '上次总结',
|
|
74
|
+
'mem.stat_next_summary': '下次自动总结',
|
|
75
|
+
'mem.tab_short': '📅 短期记忆',
|
|
76
|
+
'mem.tab_long': '💾 长期记忆',
|
|
77
|
+
'mem.tab_history': '📜 总结历史',
|
|
78
|
+
'btn.view': '查看',
|
|
79
|
+
'mem.select_date': '选择日期查看短期记忆',
|
|
80
|
+
'empty.no_history': '暂无总结记录',
|
|
81
|
+
'footbar.title': '对话任务',
|
|
82
|
+
'footbar.desc': '提交自然语言需求,云端静默执行并回传结果',
|
|
83
|
+
'footbar.model': '大模型',
|
|
84
|
+
'footbar.model_title': '切换当前使用的大模型',
|
|
85
|
+
'footbar.model_settings': '大模型设置',
|
|
86
|
+
'footbar.model_default': '默认(系统路由)',
|
|
87
|
+
'right.detail': '📦 任务详情',
|
|
88
|
+
'right.preview': '👁 产物预览',
|
|
89
|
+
'right.detail_hint': '从顶部选择一个任务查看详情。',
|
|
90
|
+
'right.preview_title': '预览',
|
|
91
|
+
'right.preview_hint': '点击任务或对话中的文件/代码进行预览。',
|
|
92
|
+
'modal.auto_title': '新建快捷指令',
|
|
93
|
+
'modal.auto_hint': '保存后可在「自动化」页一键发起任务。',
|
|
94
|
+
'modal.auto_name': '指令名称',
|
|
95
|
+
'modal.auto_goal': '目标(提交给模型的需求)',
|
|
96
|
+
'modal.perm_title': '🔒 权限处理窗口',
|
|
97
|
+
'modal.perm_hint': '配置工作期间 AI 可读范围及可操作权限。',
|
|
98
|
+
'modal.perm_read_scope': '可读范围',
|
|
99
|
+
'modal.perm_workspace': '仅当前工作区',
|
|
100
|
+
'modal.perm_specified': '指定目录',
|
|
101
|
+
'modal.perm_all': '全部文件',
|
|
102
|
+
'modal.perm_actions': '操作权限',
|
|
103
|
+
'modal.perm_read': '读取文件',
|
|
104
|
+
'modal.perm_write': '写入文件',
|
|
105
|
+
'modal.perm_shell': '执行命令',
|
|
106
|
+
'modal.perm_network': '访问网络',
|
|
107
|
+
'modal.perm_browser': '浏览器控制',
|
|
108
|
+
'modal.agent_title': '🤖 选择智能体类型',
|
|
109
|
+
'modal.agent_hint': '不同智能体会自动调整提示词与执行策略。',
|
|
110
|
+
'modal.folder_title': '📂 选择工作区文件夹',
|
|
111
|
+
'btn.select_folder': '选择此文件夹',
|
|
112
|
+
'modal.model_title': '🧠 大模型设置',
|
|
113
|
+
'modal.model_hint': '配置自定义大模型,可保存多个并在对话中随时切换。勾选「默认」的项将作为对话默认模型。',
|
|
114
|
+
'modal.model_name': '模型名称 *',
|
|
115
|
+
'modal.model_api': 'API 地址',
|
|
116
|
+
'modal.model_key': '密钥(API Key)',
|
|
117
|
+
'modal.model_reasoning': '推理过程运行内容(中间环节)',
|
|
118
|
+
'btn.save_config': '保存配置',
|
|
119
|
+
'btn.clear_form': '清空表单',
|
|
120
|
+
'btn.close': '关闭',
|
|
121
|
+
'modal.new_task_title': '+ 新建对话任务',
|
|
122
|
+
'modal.new_task_hint': '创建一个全新任务,中部对话栏的所有消息都将归属该任务运行(支持多轮连续对话)。',
|
|
123
|
+
'modal.new_task_label': '任务描述',
|
|
124
|
+
'modal.new_task_agent': '智能体类型',
|
|
125
|
+
'modal.new_task_workspace': '工作区',
|
|
126
|
+
'btn.create_task': '创建任务',
|
|
127
|
+
'modal.settings_title': '⚙️ 设置',
|
|
128
|
+
'settings.general': '通用',
|
|
129
|
+
'settings.layout': '栏目配置',
|
|
130
|
+
'settings.perm': '权限处理',
|
|
131
|
+
'settings.perm_desc': '配置工作期间可读范围与操作权限',
|
|
132
|
+
'settings.theme': '外观',
|
|
133
|
+
'settings.theme_desc': '切换浅色 / 深色主题',
|
|
134
|
+
'settings.session': '💬 会话管理',
|
|
135
|
+
'menu.version': '体验版',
|
|
136
|
+
'menu.current': '当前',
|
|
137
|
+
'menu.buddy': 'Buddy 加油站',
|
|
138
|
+
'menu.invite': '去邀约',
|
|
139
|
+
'menu.invite_right': '最高得会员',
|
|
140
|
+
'menu.points': '积分余额',
|
|
141
|
+
'menu.growth': '成长计划',
|
|
142
|
+
'menu.growth_right': '连登有奖',
|
|
143
|
+
'menu.help': '帮助与反馈',
|
|
144
|
+
'menu.update': '检查更新',
|
|
145
|
+
'menu.logout': '退出登录',
|
|
146
|
+
'btn.cancel': '取消',
|
|
147
|
+
'btn.save': '保存',
|
|
148
|
+
'sidebar.release': '产品发布',
|
|
149
|
+
'modal.tpl_title_label': '模板标题',
|
|
150
|
+
'modal.tpl_title_ph': '如:SQL 优化',
|
|
151
|
+
'modal.tpl_category': '分类',
|
|
152
|
+
'modal.tpl_category_ph': '自定义',
|
|
153
|
+
'modal.tpl_goal': '目标内容',
|
|
154
|
+
'settings.session_auto_new': '发送消息前自动新建对话',
|
|
155
|
+
'settings.session_auto_open': '页面打开时自动新建对话',
|
|
156
|
+
'settings.session_clear': '新建对话时清空历史上下文',
|
|
157
|
+
'modal.auto_name_ph': '如:每日构建检查',
|
|
158
|
+
'modal.auto_goal_ph': '运行 npm test 并汇总失败用例…',
|
|
159
|
+
'modal.model_name_ph': '如 GPT-4o / 通义千问 / 自建模型',
|
|
160
|
+
'modal.model_api_ph': 'https://api.openai.com/v1',
|
|
161
|
+
'modal.model_key_ph': 'sk-...',
|
|
162
|
+
'modal.model_reasoning_ph': '配置该模型在中间推理环节的运行内容,例如:先列出需求拆解,再给出实现方案,最后输出代码。',
|
|
163
|
+
'modal.perm_path_ph': '输入允许的目录路径',
|
|
164
|
+
// ---- select 的 title 提示(键名固定为 title.<元素id>)----
|
|
165
|
+
'title.modelSelect': '切换当前使用的大模型',
|
|
166
|
+
// ---- 输入框占位符(键名固定为 ph.<元素id>)----
|
|
167
|
+
'ph.goalInput': '输入指令,消息将归属当前任务持续对话;点上方「+ 新建任务」可开启新任务(创建 → 推理 → 工具验证 → 闭环全程追踪)',
|
|
168
|
+
'ph.marketSearch': '搜索…',
|
|
169
|
+
'ph.autoName': '如:每日构建检查',
|
|
170
|
+
'ph.autoGoal': '运行 npm test 并汇总失败用例…',
|
|
171
|
+
'ph.tplTitle': '如:SQL 优化',
|
|
172
|
+
'ph.tplCategory': '自定义',
|
|
173
|
+
'ph.tplGoal': '粘贴要保存的需求文本…',
|
|
174
|
+
'ph.permReadPath': '输入允许的目录路径',
|
|
175
|
+
'ph.modelName': '如 GPT-4o / 通义千问 / 自建模型',
|
|
176
|
+
'ph.modelApiBase': 'https://api.openai.com/v1',
|
|
177
|
+
'ph.modelApiKey': 'sk-...',
|
|
178
|
+
'ph.modelReasoning': '配置该模型在中间推理环节的运行内容,例如:先列出需求拆解,再给出实现方案,最后输出代码。',
|
|
179
|
+
'ph.ntGoal': '例如:请读取当前工作区 package.json,告诉我其中的 name 与 version 字段',
|
|
180
|
+
'ph.imName': '模型名称 *',
|
|
181
|
+
'ph.imBase': 'API 地址',
|
|
182
|
+
},
|
|
183
|
+
en: {
|
|
184
|
+
'app.title_login': 'Feihong Code Console',
|
|
185
|
+
'app.login_hint': 'Enter phone number to login directly',
|
|
186
|
+
'app.brand': 'Feihong Code',
|
|
187
|
+
'login.phone_label': 'Phone Number',
|
|
188
|
+
'login.btn_login': 'Login',
|
|
189
|
+
'nav.chat': '💬 Chat',
|
|
190
|
+
'nav.automations': '⚡ Automations',
|
|
191
|
+
'nav.templates': '📚 Templates',
|
|
192
|
+
'nav.market': '🧩 Market',
|
|
193
|
+
'nav.office': '📎 Office',
|
|
194
|
+
'nav.release': '🚀 Release',
|
|
195
|
+
'nav.memory': '🧠 Memory',
|
|
196
|
+
'sidebar.nav': 'Nav',
|
|
197
|
+
'sidebar.chat': 'Chat',
|
|
198
|
+
'sidebar.automations': 'Automations',
|
|
199
|
+
'sidebar.templates': 'Templates',
|
|
200
|
+
'sidebar.market': 'Market',
|
|
201
|
+
'sidebar.office': 'Office',
|
|
202
|
+
'sidebar.release': 'Release',
|
|
203
|
+
'sidebar.memory': 'Memory',
|
|
204
|
+
'sidebar.user': 'User',
|
|
205
|
+
'sidebar.settings': 'Settings',
|
|
206
|
+
'sidebar.task_list': 'Tasks',
|
|
207
|
+
'btn.new_task': '+ New Task',
|
|
208
|
+
'empty.no_tasks': 'No tasks',
|
|
209
|
+
'empty.no_skills': 'No skills found',
|
|
210
|
+
'empty.no_builtin_templates': 'No built-in templates',
|
|
211
|
+
'empty.no_models': 'No model configured yet, add one below.',
|
|
212
|
+
'empty.preparing_welcome': 'Preparing welcome tasks for you, please wait…',
|
|
213
|
+
'task.not_selected': 'No task selected',
|
|
214
|
+
'task.select_hint': 'Select from task list or enter commands below',
|
|
215
|
+
'task.refresh': 'Refresh',
|
|
216
|
+
'chat.welcome': 'Click + New Task to start a new task, or send messages to continue the current task.',
|
|
217
|
+
'chat.menu': '+ Menu',
|
|
218
|
+
'chat.screenshot': '📷 Screenshot',
|
|
219
|
+
'chat.upload_file': '📄 Upload File',
|
|
220
|
+
'chat.upload_image': '🖼 Upload Image',
|
|
221
|
+
'chat.direct_mode': '🖥 Direct Control',
|
|
222
|
+
'chat.voice': '🎤 Voice Input',
|
|
223
|
+
'chat.agent_type': '🤖 Agent Type',
|
|
224
|
+
'chat.agent_default': '🤖 Assistant',
|
|
225
|
+
'chat.listening': '● Listening…',
|
|
226
|
+
'chat.send': '▶ Send',
|
|
227
|
+
'common.loading': 'Loading…',
|
|
228
|
+
'perm.default': 'Default Permissions',
|
|
229
|
+
'call.hint': 'Call',
|
|
230
|
+
'call.voice': '📞 Voice',
|
|
231
|
+
'call.video': '🎥 Video',
|
|
232
|
+
'call.hangup': '⛔ Hang Up',
|
|
233
|
+
'crop.tip': 'Drag to select a screenshot area · Release to confirm · ESC to cancel',
|
|
234
|
+
'page.automations.sub': 'Save common goals as shortcuts, launch tasks with one click',
|
|
235
|
+
'btn.new_automation': '+ New Command',
|
|
236
|
+
'empty.no_automations': 'No shortcuts yet',
|
|
237
|
+
'page.templates.sub': 'Click a template to fill the input box',
|
|
238
|
+
'btn.save_template': '+ Save as Template',
|
|
239
|
+
'tpl.builtin': 'Built-in Templates',
|
|
240
|
+
'tpl.my': 'My Templates',
|
|
241
|
+
'empty.no_templates': 'No custom templates',
|
|
242
|
+
'page.market.sub': 'Aggregating ClawHub & Agent-Foundry skill ecosystems',
|
|
243
|
+
'market.search_placeholder': 'Search…',
|
|
244
|
+
'market.source_all': 'All Sources',
|
|
245
|
+
'btn.refresh': 'Refresh',
|
|
246
|
+
'market.installed': 'Installed',
|
|
247
|
+
'empty.no_installed': 'No installed skills',
|
|
248
|
+
'page.office.sub': 'Document processing shortcuts, generate prompts instantly',
|
|
249
|
+
'page.memory.sub': 'Daily auto-summary of short-term memory, accumulate long-term knowledge',
|
|
250
|
+
'btn.refresh_mem': '🔄 Refresh',
|
|
251
|
+
'btn.summarize': '✨ Summarize Now',
|
|
252
|
+
'mem.stat_short_days': 'Short-term Memory Days',
|
|
253
|
+
'mem.stat_long_items': 'Long-term Memory Items',
|
|
254
|
+
'mem.stat_last_summary': 'Last Summary',
|
|
255
|
+
'mem.stat_next_summary': 'Next Summary',
|
|
256
|
+
'mem.tab_short': '📅 Short-term',
|
|
257
|
+
'mem.tab_long': '💾 Long-term',
|
|
258
|
+
'mem.tab_history': '📜 History',
|
|
259
|
+
'btn.view': 'View',
|
|
260
|
+
'mem.select_date': 'Select date to view',
|
|
261
|
+
'empty.no_history': 'No summary records',
|
|
262
|
+
'footbar.title': 'Chat Task',
|
|
263
|
+
'footbar.desc': 'Submit natural language requirements, cloud silent execution and return results',
|
|
264
|
+
'footbar.model': 'Model',
|
|
265
|
+
'footbar.model_title': 'Switch model',
|
|
266
|
+
'footbar.model_settings': 'Model Settings',
|
|
267
|
+
'footbar.model_default': 'Default (System Route)',
|
|
268
|
+
'right.detail': '📦 Task Detail',
|
|
269
|
+
'right.preview': '👁 Preview',
|
|
270
|
+
'right.detail_hint': 'Select a task from the top to view details.',
|
|
271
|
+
'right.preview_title': 'Preview',
|
|
272
|
+
'right.preview_hint': 'Click files/codes in task or chat to preview.',
|
|
273
|
+
'modal.auto_title': 'New Automation',
|
|
274
|
+
'modal.auto_hint': 'Save for one-click task launch on Automations page.',
|
|
275
|
+
'modal.auto_name': 'Command Name',
|
|
276
|
+
'modal.auto_goal': 'Goal (Requirements for model)',
|
|
277
|
+
'modal.perm_title': '🔒 Permission Window',
|
|
278
|
+
'modal.perm_hint': 'Configure AI readable scope and operable permissions.',
|
|
279
|
+
'modal.perm_read_scope': 'Read Scope',
|
|
280
|
+
'modal.perm_workspace': 'Current Workspace Only',
|
|
281
|
+
'modal.perm_specified': 'Specified Directory',
|
|
282
|
+
'modal.perm_all': 'All Files',
|
|
283
|
+
'modal.perm_actions': 'Operation Permissions',
|
|
284
|
+
'modal.perm_read': 'Read Files',
|
|
285
|
+
'modal.perm_write': 'Write Files',
|
|
286
|
+
'modal.perm_shell': 'Execute Commands',
|
|
287
|
+
'modal.perm_network': 'Access Network',
|
|
288
|
+
'modal.perm_browser': 'Browser Control',
|
|
289
|
+
'modal.agent_title': '🤖 Agent Type',
|
|
290
|
+
'modal.agent_hint': 'Different agents auto-adjust prompts and execution strategies.',
|
|
291
|
+
'modal.folder_title': '📂 Select Workspace Folder',
|
|
292
|
+
'btn.select_folder': 'Select This Folder',
|
|
293
|
+
'modal.model_title': '🧠 Model Settings',
|
|
294
|
+
'modal.model_hint': 'Configure custom models, save multiple and switch anytime. Default model will be used by default.',
|
|
295
|
+
'modal.model_name': 'Model Name *',
|
|
296
|
+
'modal.model_api': 'API URL',
|
|
297
|
+
'modal.model_key': 'API Key',
|
|
298
|
+
'modal.model_reasoning': 'Reasoning Content (Optional)',
|
|
299
|
+
'btn.save_config': 'Save Config',
|
|
300
|
+
'btn.clear_form': 'Clear Form',
|
|
301
|
+
'btn.close': 'Close',
|
|
302
|
+
'modal.new_task_title': '+ New Chat Task',
|
|
303
|
+
'modal.new_task_hint': 'Create a brand new task. All messages in the chat will belong to this task (supports multi-turn conversation).',
|
|
304
|
+
'modal.new_task_label': 'Task Description',
|
|
305
|
+
'modal.new_task_agent': 'Agent Type',
|
|
306
|
+
'modal.new_task_workspace': 'Workspace',
|
|
307
|
+
'btn.create_task': 'Create Task',
|
|
308
|
+
'modal.settings_title': '⚙️ Settings',
|
|
309
|
+
'settings.general': 'General',
|
|
310
|
+
'settings.layout': 'Layout Config',
|
|
311
|
+
'settings.perm': 'Permissions',
|
|
312
|
+
'settings.perm_desc': 'Configure readable scope and operation permissions',
|
|
313
|
+
'settings.theme': 'Appearance',
|
|
314
|
+
'settings.theme_desc': 'Switch light/dark theme',
|
|
315
|
+
'settings.session': '💬 Session Management',
|
|
316
|
+
'menu.version': 'Trial Version',
|
|
317
|
+
'menu.current': 'Current',
|
|
318
|
+
'menu.buddy': 'Buddy Station',
|
|
319
|
+
'menu.invite': 'Invite',
|
|
320
|
+
'menu.invite_right': 'Get Premium',
|
|
321
|
+
'menu.points': 'Points Balance',
|
|
322
|
+
'menu.growth': 'Growth Plan',
|
|
323
|
+
'menu.growth_right': 'Streak Bonus',
|
|
324
|
+
'menu.help': 'Help & Feedback',
|
|
325
|
+
'menu.update': 'Check Update',
|
|
326
|
+
'menu.logout': 'Logout',
|
|
327
|
+
'btn.cancel': 'Cancel',
|
|
328
|
+
'btn.save': 'Save',
|
|
329
|
+
'sidebar.release': 'Release',
|
|
330
|
+
'modal.tpl_title_label': 'Template Title',
|
|
331
|
+
'modal.tpl_title_ph': 'e.g. SQL optimization',
|
|
332
|
+
'modal.tpl_category': 'Category',
|
|
333
|
+
'modal.tpl_category_ph': 'Custom',
|
|
334
|
+
'modal.tpl_goal': 'Goal Content',
|
|
335
|
+
'settings.session_auto_new': 'Auto new conversation before sending',
|
|
336
|
+
'settings.session_auto_open': 'Auto new conversation on page open',
|
|
337
|
+
'settings.session_clear': 'Clear history on new conversation',
|
|
338
|
+
'modal.auto_name_ph': 'e.g. Daily build check',
|
|
339
|
+
'modal.auto_goal_ph': 'Run npm test and summarize failures…',
|
|
340
|
+
'modal.model_name_ph': 'e.g. GPT-4o / Qwen / custom model',
|
|
341
|
+
'modal.model_api_ph': 'https://api.openai.com/v1',
|
|
342
|
+
'modal.model_key_ph': 'sk-...',
|
|
343
|
+
'modal.model_reasoning_ph': 'Configure what this model runs in intermediate reasoning steps, e.g. break down requirements first, then propose a solution, then output code.',
|
|
344
|
+
'modal.perm_path_ph': 'Enter allowed directory path',
|
|
345
|
+
// ---- select title hints (key = title.<element id>) ----
|
|
346
|
+
'title.modelSelect': 'Switch the active LLM',
|
|
347
|
+
// ---- input placeholders (key = ph.<element id>) ----
|
|
348
|
+
'ph.goalInput': 'Type an instruction. Messages stay in the current task; click "+ New Task" above to start a new one (create → reason → verify → close the loop).',
|
|
349
|
+
'ph.marketSearch': 'Search…',
|
|
350
|
+
'ph.autoName': 'e.g. Daily build check',
|
|
351
|
+
'ph.autoGoal': 'Run npm test and summarize failing cases…',
|
|
352
|
+
'ph.tplTitle': 'e.g. SQL Optimization',
|
|
353
|
+
'ph.tplCategory': 'Custom',
|
|
354
|
+
'ph.tplGoal': 'Paste the requirement text to save…',
|
|
355
|
+
'ph.permReadPath': 'Enter an allowed directory path',
|
|
356
|
+
'ph.modelName': 'e.g. GPT-4o / Qwen / Self-hosted',
|
|
357
|
+
'ph.modelApiBase': 'https://api.openai.com/v1',
|
|
358
|
+
'ph.modelApiKey': 'sk-...',
|
|
359
|
+
'ph.modelReasoning': 'Define what this model does during intermediate reasoning, e.g. break down requirements first, then propose a plan, then output code.',
|
|
360
|
+
'ph.ntGoal': 'e.g. Read package.json in the current workspace and tell me its name and version fields',
|
|
361
|
+
'ph.imName': 'Model name *',
|
|
362
|
+
'ph.imBase': 'API endpoint',
|
|
363
|
+
}
|
|
364
|
+
};
|
|
365
|
+
|
|
366
|
+
let currentLang = localStorage.getItem('fhcode.lang') || 'zh';
|
|
367
|
+
|
|
368
|
+
|
|
369
|
+
/**
|
|
370
|
+
* 安全地把某个元素的 i18n 文案落地。
|
|
371
|
+
* 关键约束(曾导致「模型无法切换」「侧边栏图标消失」的线上问题):
|
|
372
|
+
* - <select> 绝对不能写 textContent,否则会把所有 <option> 一次性清空;
|
|
373
|
+
* - <input>/<textarea> 只改 placeholder,且必须命中专用键,避免误翻;
|
|
374
|
+
* - 含子元素的容器(图标 + 标签)只改 title / 直接文本节点,绝不整体覆盖。
|
|
375
|
+
*/
|
|
376
|
+
|
|
377
|
+
function switchLang(lang) {
|
|
378
|
+
currentLang = lang;
|
|
379
|
+
localStorage.setItem('fhcode.lang', lang);
|
|
380
|
+
document.querySelectorAll('[data-i18n]').forEach((el) => {
|
|
381
|
+
try { applyI18nToEl(el); } catch (e) { console.warn('[i18n] 应用失败', el, e); }
|
|
382
|
+
});
|
|
383
|
+
document.querySelectorAll('.lang-btn').forEach(btn => {
|
|
384
|
+
btn.classList.toggle('active', btn.getAttribute('data-lang') === lang);
|
|
385
|
+
});
|
|
386
|
+
document.documentElement.lang = lang;
|
|
387
|
+
// 语言切换后重建由 JS 动态渲染的下拉/列表,保证选项文案与选中值不丢失
|
|
388
|
+
try { if (typeof renderModelSelect === 'function') renderModelSelect(); } catch {}
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
document.querySelectorAll('.lang-btn').forEach(btn => {
|
|
392
|
+
btn.addEventListener('click', () => switchLang(btn.getAttribute('data-lang')));
|
|
393
|
+
});
|
|
394
|
+
|
|
395
|
+
// 初始化语言
|
|
396
|
+
switchLang(currentLang);
|
|
397
|
+
|
|
398
|
+
const AGENT_TYPES = {
|
|
399
|
+
general: { label: '通用助手', icon: '🤖' },
|
|
400
|
+
'fix-code': { label: '修复代码', icon: '🐛' },
|
|
401
|
+
'write-code': { label: '编写代码', icon: '✍️' },
|
|
402
|
+
'exec-command': { label: '执行命令', icon: '⚡' },
|
|
403
|
+
};
|
|
404
|
+
|
|
405
|
+
const state = {
|
|
406
|
+
token: '',
|
|
407
|
+
phone: '',
|
|
408
|
+
tasks: [],
|
|
409
|
+
currentTaskId: null,
|
|
410
|
+
submittedTaskIds: new Set(),
|
|
411
|
+
completedTaskIds: new Set(),
|
|
412
|
+
pinnedTasks: new Set(), // 置顶任务ID集合
|
|
413
|
+
market: [],
|
|
414
|
+
installed: new Set(),
|
|
415
|
+
agentType: 'general',
|
|
416
|
+
directMode: false,
|
|
417
|
+
permissions: {
|
|
418
|
+
readScope: 'workspace',
|
|
419
|
+
readPath: '',
|
|
420
|
+
allowRead: true,
|
|
421
|
+
allowWrite: false,
|
|
422
|
+
allowShell: false,
|
|
423
|
+
allowNetwork: true,
|
|
424
|
+
allowBrowser: false,
|
|
425
|
+
},
|
|
426
|
+
workspaceDir: '',
|
|
427
|
+
rightTab: 'detail',
|
|
428
|
+
models: [],
|
|
429
|
+
defaultModelId: null,
|
|
430
|
+
modelId: '',
|
|
431
|
+
// 待发送附件暂存区(截图/上传文件/上传图片,点发送时才上传并发给 AI)
|
|
432
|
+
stagedFiles: [],
|
|
433
|
+
// 会话管理配置
|
|
434
|
+
sessionConfig: {
|
|
435
|
+
autoResetOnNew: false, // 打开时自动新建对话
|
|
436
|
+
autoResetOnOpen: false, // 发送消息时自动重置
|
|
437
|
+
clearHistoryOnNew: true, // 新建时清空历史
|
|
438
|
+
},
|
|
439
|
+
procOpen: false, // 「执行过程」折叠面板是否展开(轮询刷新时保留)
|
|
440
|
+
};
|
|
441
|
+
|
|
442
|
+
(function initSession() {
|
|
443
|
+
const urlToken = new URLSearchParams(location.search).get('token');
|
|
444
|
+
if (urlToken) localStorage.setItem('fhcode.token', urlToken);
|
|
445
|
+
const savedModel = localStorage.getItem('fhcode.model');
|
|
446
|
+
if (savedModel !== null) state.modelId = savedModel;
|
|
447
|
+
state.token = localStorage.getItem('fhcode.token') || '';
|
|
448
|
+
state.phone = localStorage.getItem('fhcode.phone') || '';
|
|
449
|
+
try {
|
|
450
|
+
const p = JSON.parse(localStorage.getItem('fhcode.permissions') || '{}');
|
|
451
|
+
if (p) Object.assign(state.permissions, p);
|
|
452
|
+
} catch {}
|
|
453
|
+
// 恢复主题偏好(脚本位于 body 尾部,尽早生效避免闪烁)
|
|
454
|
+
if (localStorage.getItem('fhcode.theme') === 'dark') {
|
|
455
|
+
document.documentElement.classList.add('dark');
|
|
456
|
+
}
|
|
457
|
+
// 加载会话配置
|
|
458
|
+
try {
|
|
459
|
+
const sc = JSON.parse(localStorage.getItem('fhcode.sessionConfig') || '{}');
|
|
460
|
+
if (sc.autoResetOnNew != null) state.sessionConfig.autoResetOnNew = sc.autoResetOnNew;
|
|
461
|
+
if (sc.autoResetOnOpen != null) state.sessionConfig.autoResetOnOpen = sc.autoResetOnOpen;
|
|
462
|
+
if (sc.clearHistoryOnNew != null) state.sessionConfig.clearHistoryOnNew = sc.clearHistoryOnNew;
|
|
463
|
+
} catch {}
|
|
464
|
+
// autoResetOnOpen: 页面打开时自动新建对话(不自动选中旧任务,显示空白输入区)
|
|
465
|
+
if (state.sessionConfig.autoResetOnOpen && state.token) {
|
|
466
|
+
state.currentTaskId = null;
|
|
467
|
+
const box = document.getElementById('messages');
|
|
468
|
+
if (box) box.innerHTML = '<div class="msg sys">已按设置自动新建对话,输入指令即可发起新任务。</div>';
|
|
469
|
+
}
|
|
470
|
+
})();
|
|
471
|
+
|
|
472
|
+
// 消息气泡通用操作:复制 / 编辑 / 分享 / 创建文档
|
|
473
|
+
function bubbleActions(e, content) {
|
|
474
|
+
e.stopPropagation();
|
|
475
|
+
const menu = document.getElementById('bubbleMenu');
|
|
476
|
+
if (menu) menu.remove();
|
|
477
|
+
const m = document.createElement('div');
|
|
478
|
+
m.id = 'bubbleMenu';
|
|
479
|
+
m.style.cssText = 'position:fixed;z-index:10001;background:var(--card);border:1px solid var(--border);border-radius:8px;box-shadow:0 4px 16px rgba(0,0,0,0.18);padding:4px 0;min-width:140px;font-size:13px;';
|
|
480
|
+
const items = [
|
|
481
|
+
{ label: '📋 复制', action: 'copy' },
|
|
482
|
+
{ label: '✏️ 编辑', action: 'edit' },
|
|
483
|
+
{ label: '🔗 分享链接', action: 'share' },
|
|
484
|
+
{ label: '📄 创建文档', action: 'doc' },
|
|
485
|
+
];
|
|
486
|
+
items.forEach((it) => {
|
|
487
|
+
const el = document.createElement('div');
|
|
488
|
+
el.className = 'dropdown-item';
|
|
489
|
+
el.textContent = it.label;
|
|
490
|
+
el.style.cssText = 'padding:7px 16px;cursor:pointer;';
|
|
491
|
+
el.addEventListener('mouseenter', () => { el.style.background = 'var(--brand-soft)'; });
|
|
492
|
+
el.addEventListener('mouseleave', () => { el.style.background = ''; });
|
|
493
|
+
el.addEventListener('click', () => {
|
|
494
|
+
m.remove();
|
|
495
|
+
handleBubbleAction(it.action, content);
|
|
496
|
+
});
|
|
497
|
+
m.appendChild(el);
|
|
498
|
+
});
|
|
499
|
+
document.body.appendChild(m);
|
|
500
|
+
const rect = e.target.getBoundingClientRect();
|
|
501
|
+
m.style.right = (window.innerWidth - rect.right + 4) + 'px';
|
|
502
|
+
m.style.top = rect.top + 'px';
|
|
503
|
+
}
|
|
504
|
+
async function handleBubbleAction(action, content) {
|
|
505
|
+
if (action === 'copy') {
|
|
506
|
+
try { await navigator.clipboard.writeText(content); toast('已复制到剪贴板'); }
|
|
507
|
+
catch { document.execCommand('copy'); toast('已复制'); }
|
|
508
|
+
} else if (action === 'edit') {
|
|
509
|
+
const edited = prompt('编辑内容:', content);
|
|
510
|
+
if (edited != null && edited.trim()) {
|
|
511
|
+
// 就近更新对应的消息气泡正文(最终回复块 / 对话气泡)
|
|
512
|
+
const box = document.getElementById('messages');
|
|
513
|
+
if (box) {
|
|
514
|
+
const candidates = box.querySelectorAll('.step.final > div:last-child, .msg.assistant, .msg.user');
|
|
515
|
+
for (let i = candidates.length - 1; i >= 0; i--) {
|
|
516
|
+
if ((candidates[i].textContent || '').trim() === (content || '').trim()) {
|
|
517
|
+
candidates[i].textContent = edited;
|
|
518
|
+
break;
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
toast('已更新消息内容(本地预览)');
|
|
523
|
+
}
|
|
524
|
+
} else if (action === 'share') {
|
|
525
|
+
const payload = btoa(unescape(encodeURIComponent(content.slice(0, 2000))));
|
|
526
|
+
const url = location.origin + location.pathname + '#share/' + payload;
|
|
527
|
+
try { await navigator.clipboard.writeText(url); toast('分享链接已复制'); }
|
|
528
|
+
catch { prompt('复制以下链接分享给他人:', url); }
|
|
529
|
+
} else if (action === 'doc') {
|
|
530
|
+
const title = prompt('文档标题(默认: fhcode-回复):', 'fhcode-回复-' + Date.now());
|
|
531
|
+
if (!title) return;
|
|
532
|
+
const blob = new Blob([content], { type: 'text/markdown;charset=utf-8' });
|
|
533
|
+
const a = document.createElement('a');
|
|
534
|
+
a.href = URL.createObjectURL(blob);
|
|
535
|
+
a.download = title.replace(/[^\u4e00-\u9fa5a-zA-Z0-9_\-]/g, '_') + '.md';
|
|
536
|
+
a.click();
|
|
537
|
+
URL.revokeObjectURL(a.href);
|
|
538
|
+
toast('文档已下载');
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
/* ========== 登录 ========== */
|
|
543
|
+
document.getElementById('loginBtn').addEventListener('click', doLogin);
|
|
544
|
+
document.getElementById('loginPhone').addEventListener('keydown', (e) => { if (e.key === 'Enter') doLogin(); });
|
|
545
|
+
|
|
546
|
+
async function afterLogin() {
|
|
547
|
+
updateUserBar();
|
|
548
|
+
// 检查是否是首次登录,如果是则显示欢迎引导
|
|
549
|
+
const isFirstLogin = await checkFirstLogin();
|
|
550
|
+
if (isFirstLogin) {
|
|
551
|
+
// 从 localStorage 恢复 welcomeTasks(登录时已创建)
|
|
552
|
+
const savedWelcomeTasks = localStorage.getItem('fhcode.welcomeTasks');
|
|
553
|
+
if (savedWelcomeTasks) {
|
|
554
|
+
try {
|
|
555
|
+
const tasks = JSON.parse(savedWelcomeTasks);
|
|
556
|
+
if (tasks.length > 0) {
|
|
557
|
+
showWelcomeGuide(tasks);
|
|
558
|
+
return; // 引导流程已启动,跳过常规加载
|
|
559
|
+
}
|
|
560
|
+
} catch {}
|
|
561
|
+
}
|
|
562
|
+
showWelcomeGuide([]);
|
|
563
|
+
}
|
|
564
|
+
await Promise.allSettled([loadTasks(), loadAutomations(), loadTemplates(), loadMarket(), loadOffice(), loadWorkspace(), loadModels(), loadMemoryStats()]);
|
|
565
|
+
startRefresh();
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
async function checkFirstLogin() {
|
|
569
|
+
if (!state.token) return false;
|
|
570
|
+
try {
|
|
571
|
+
const d = await api('/api/auth/me');
|
|
572
|
+
if (d.isFirstLogin === true) {
|
|
573
|
+
// 保存首次登录标记,供后续使用
|
|
574
|
+
localStorage.setItem('fhcode.firstLogin', 'true');
|
|
575
|
+
}
|
|
576
|
+
return d.isFirstLogin === true;
|
|
577
|
+
} catch {
|
|
578
|
+
return false;
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
|
|
583
|
+
|
|
584
|
+
async function runWelcomeTask(taskId) {
|
|
585
|
+
try {
|
|
586
|
+
// 加载任务详情
|
|
587
|
+
const d = await api('/api/tasks/' + taskId);
|
|
588
|
+
state.currentTaskId = d.task.id;
|
|
589
|
+
|
|
590
|
+
// 关闭引导弹窗
|
|
591
|
+
const modal = document.getElementById('welcomeGuideModal');
|
|
592
|
+
if (modal) modal.classList.remove('show');
|
|
593
|
+
|
|
594
|
+
// 显示任务列表
|
|
595
|
+
const taskListSection = document.getElementById('taskListSection');
|
|
596
|
+
if (taskListSection) taskListSection.style.display = 'block';
|
|
597
|
+
|
|
598
|
+
// 加载并刷新任务
|
|
599
|
+
await loadTasks();
|
|
600
|
+
selectTask(taskId);
|
|
601
|
+
toast('已启动引导任务:' + (d.task.goal.slice(0, 20) + '…'));
|
|
602
|
+
} catch (e) {
|
|
603
|
+
toast('启动任务失败:' + e.message);
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
// 运行所有引导任务
|
|
608
|
+
async function runAllWelcomeTasks() {
|
|
609
|
+
const tasks = JSON.parse(localStorage.getItem('fhcode.welcomeTasks') || '[]');
|
|
610
|
+
if (tasks.length === 0) {
|
|
611
|
+
toast('暂无引导任务');
|
|
612
|
+
return;
|
|
613
|
+
}
|
|
614
|
+
// 关闭弹窗
|
|
615
|
+
const modal = document.getElementById('welcomeGuideModal');
|
|
616
|
+
if (modal) modal.classList.remove('show');
|
|
617
|
+
// 显示任务列表
|
|
618
|
+
const taskListSection = document.getElementById('taskListSection');
|
|
619
|
+
if (taskListSection) taskListSection.style.display = 'block';
|
|
620
|
+
// 运行第一个任务
|
|
621
|
+
await runWelcomeTask(tasks[0].taskId);
|
|
622
|
+
toast('已开始执行引导任务,共 ' + tasks.length + ' 个');
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
// 绑定"全部运行"按钮
|
|
626
|
+
document.getElementById('welcomeRunAllBtn')?.addEventListener('click', runAllWelcomeTasks);
|
|
627
|
+
|
|
628
|
+
// 在登录后保存 welcomeTasks 到 localStorage
|
|
629
|
+
async function saveWelcomeTasks(tasks) {
|
|
630
|
+
if (tasks && tasks.length > 0) {
|
|
631
|
+
localStorage.setItem('fhcode.welcomeTasks', JSON.stringify(tasks));
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
|
|
636
|
+
/* ========== 导航切换 ========== */
|
|
637
|
+
|
|
638
|
+
document.querySelectorAll('.topbar-nav-item').forEach((el) => {
|
|
639
|
+
el.addEventListener('click', () => switchView(el.getAttribute('data-nav')));
|
|
640
|
+
});
|
|
641
|
+
document.querySelectorAll('.sidebar-icon[data-nav]').forEach((el) => {
|
|
642
|
+
el.addEventListener('click', () => switchView(el.getAttribute('data-nav')));
|
|
643
|
+
});
|
|
644
|
+
|
|
645
|
+
/* ========== 用户菜单 ========== */
|
|
646
|
+
const userMenuMask = document.getElementById('userMenuMask');
|
|
647
|
+
document.getElementById('topbarUser').addEventListener('click', () => userMenuMask.classList.add('show'));
|
|
648
|
+
document.getElementById('sidebarUser').addEventListener('click', () => userMenuMask.classList.add('show'));
|
|
649
|
+
userMenuMask.addEventListener('click', (e) => { if (e.target === userMenuMask) userMenuMask.classList.remove('show'); });
|
|
650
|
+
document.querySelectorAll('#userMenu .sheet-item').forEach((el) => {
|
|
651
|
+
el.addEventListener('click', () => {
|
|
652
|
+
const action = el.getAttribute('data-action');
|
|
653
|
+
userMenuMask.classList.remove('show');
|
|
654
|
+
switch (action) {
|
|
655
|
+
case 'settings': openModal('settingsModal'); break;
|
|
656
|
+
case 'models': openModels(); break;
|
|
657
|
+
case 'theme': toggleTheme(); break;
|
|
658
|
+
case 'help': openBrowser('https://github.com/wch887292/feihong-code/issues'); break;
|
|
659
|
+
case 'buddy': openBrowser('https://www.klai.top'); break;
|
|
660
|
+
case 'invite': toast('邀请链接已复制(演示)'); break;
|
|
661
|
+
case 'points': toast('积分余额:0(演示)'); break;
|
|
662
|
+
case 'growth': toast('连续登录 1 天(演示)'); break;
|
|
663
|
+
case 'version': toast('当前为体验版'); break;
|
|
664
|
+
case 'update': checkUpdate(); break;
|
|
665
|
+
case 'logout':
|
|
666
|
+
localStorage.removeItem('fhcode.token');
|
|
667
|
+
localStorage.removeItem('fhcode.phone');
|
|
668
|
+
location.reload();
|
|
669
|
+
break;
|
|
670
|
+
}
|
|
671
|
+
});
|
|
672
|
+
});
|
|
673
|
+
|
|
674
|
+
document.getElementById('sidebarSettings').addEventListener('click', () => {
|
|
675
|
+
userMenuMask.classList.remove('show');
|
|
676
|
+
openModal('settingsModal');
|
|
677
|
+
});
|
|
678
|
+
|
|
679
|
+
function openBrowser(url) {
|
|
680
|
+
api('/api/open/browser', 'POST', { url }).then(() => toast('已在系统浏览器打开')).catch((e) => toast('打开失败:' + e.message));
|
|
681
|
+
}
|
|
682
|
+
async function checkUpdate() {
|
|
683
|
+
try {
|
|
684
|
+
const d = await api('/api/health');
|
|
685
|
+
toast('当前版本 ' + d.version + ',已是最新(演示)');
|
|
686
|
+
} catch (e) { toast('检查失败:' + e.message); }
|
|
687
|
+
}
|
|
688
|
+
function toggleTheme() {
|
|
689
|
+
const dark = document.documentElement.classList.toggle('dark');
|
|
690
|
+
localStorage.setItem('fhcode.theme', dark ? 'dark' : 'light');
|
|
691
|
+
toast(dark ? '已切换为深色主题' : '已切换为浅色主题');
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
/* ========== 任务列表与详情 ========== */
|
|
695
|
+
async function selectTask(id) {
|
|
696
|
+
state.currentTaskId = id;
|
|
697
|
+
renderSidebarTaskList();
|
|
698
|
+
renderTaskDetail(id);
|
|
699
|
+
await refreshCurrentThread();
|
|
700
|
+
switchRightTab('detail');
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
// 切换置顶状态
|
|
704
|
+
function togglePinTask(taskId) {
|
|
705
|
+
if (state.pinnedTasks.has(taskId)) {
|
|
706
|
+
state.pinnedTasks.delete(taskId);
|
|
707
|
+
toast('已取消置顶');
|
|
708
|
+
} else {
|
|
709
|
+
state.pinnedTasks.add(taskId);
|
|
710
|
+
toast('已置顶');
|
|
711
|
+
}
|
|
712
|
+
renderSidebarTaskList();
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
// 删除任务
|
|
716
|
+
async function deleteTask(taskId) {
|
|
717
|
+
if (!confirm('确定要删除这个任务吗?')) return;
|
|
718
|
+
try {
|
|
719
|
+
await api('/api/tasks/' + taskId, 'DELETE');
|
|
720
|
+
toast('任务已删除');
|
|
721
|
+
state.tasks = state.tasks.filter((t) => t.id !== taskId);
|
|
722
|
+
// 若删除的是当前任务,清空选中并回退到最新任务或空白态
|
|
723
|
+
if (state.currentTaskId === taskId) {
|
|
724
|
+
state.currentTaskId = null;
|
|
725
|
+
if (state.tasks.length) {
|
|
726
|
+
state.currentTaskId = state.tasks[0].id;
|
|
727
|
+
renderTaskDetail(state.currentTaskId);
|
|
728
|
+
await refreshCurrentThread();
|
|
729
|
+
} else {
|
|
730
|
+
renderTaskThread(null);
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
renderSidebarTaskList();
|
|
734
|
+
} catch (e) {
|
|
735
|
+
toast('删除失败:' + e.message);
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
async function handleDetailOp(task, op, path) {
|
|
739
|
+
if (op === 'folder') {
|
|
740
|
+
try { await api('/api/open/folder', 'POST', { path: path || task.workspaceDir }); toast('已打开资源管理器'); }
|
|
741
|
+
catch (e) { toast('打开失败:' + e.message); }
|
|
742
|
+
} else if (op === 'browser') {
|
|
743
|
+
const url = extractUrl(task.result?.finalAnswer) || 'https://www.klai.top';
|
|
744
|
+
openBrowser(url);
|
|
745
|
+
} else if (op === 'artifacts') {
|
|
746
|
+
await showArtifacts(path || task.workspaceDir);
|
|
747
|
+
} else if (op === 'preview' || op === 'code') {
|
|
748
|
+
const text = task.result?.finalAnswer || '';
|
|
749
|
+
showPreview('任务产物', linkifyArtifacts(text), 'text');
|
|
750
|
+
switchRightTab('preview');
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
/* ========== 任务思维链路渲染 ========== */
|
|
754
|
+
/** 切换思考过程折叠/展开 */
|
|
755
|
+
/** 切换工具调用参数详情显示/隐藏 */
|
|
756
|
+
|
|
757
|
+
/** 群组渲染助手消息为 Cursor 风格的思考过程 + 回复 */
|
|
758
|
+
|
|
759
|
+
// 拉取当前选中任务的完整详情(含思维链路步骤)并渲染
|
|
760
|
+
async function refreshCurrentThread() {
|
|
761
|
+
if (!state.currentTaskId) { renderTaskThread(null); return; }
|
|
762
|
+
try {
|
|
763
|
+
const d = await api('/api/tasks/' + state.currentTaskId);
|
|
764
|
+
const t = d.task;
|
|
765
|
+
const idx = state.tasks.findIndex((x) => x.id === t.id);
|
|
766
|
+
if (idx >= 0) state.tasks[idx] = t; else state.tasks.push(t);
|
|
767
|
+
renderTaskThread(t);
|
|
768
|
+
} catch (e) { console.warn('刷新当前任务线程失败', e); renderTaskThread(null); }
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
|
|
772
|
+
// 将单个任务渲染为对话框:对话流(用户↔助手干净气泡)+ 内部执行过程(默认折叠)
|
|
773
|
+
// 单条执行步骤渲染(思维链路、工具调用、验证结果、自愈/压缩等)
|
|
774
|
+
/* 轻量 Markdown 渲染(用于模型推理内容) */
|
|
775
|
+
/* 工具参数格式化为 key: value 列表 */
|
|
776
|
+
|
|
777
|
+
/* ========== 右侧 Tab ========== */
|
|
778
|
+
document.querySelectorAll('.right-tab').forEach((tab) => {
|
|
779
|
+
tab.addEventListener('click', () => switchRightTab(tab.getAttribute('data-tab')));
|
|
780
|
+
});
|
|
781
|
+
|
|
782
|
+
/* ========== 对话与发送 ========== */
|
|
783
|
+
async function sendTask() {
|
|
784
|
+
const input = document.getElementById('goalInput');
|
|
785
|
+
let goal = input.value.trim();
|
|
786
|
+
// 允许只发送文件(无文本)
|
|
787
|
+
if (!goal && !state.stagedFiles.length) return;
|
|
788
|
+
if (!state.token) { toast('请先登录'); return; }
|
|
789
|
+
if (state.directMode) goal = goal ? ('[直接操作电脑] ' + goal) : '[直接操作电脑](仅附件)';
|
|
790
|
+
const btn = document.getElementById('sendBtn');
|
|
791
|
+
btn.disabled = true;
|
|
792
|
+
try {
|
|
793
|
+
// 上传暂存的附件(仅在点发送时才上传)
|
|
794
|
+
const attachments = await uploadStagedFiles();
|
|
795
|
+
|
|
796
|
+
// autoResetOnNew: 发送消息前自动新建对话,旧任务保留在历史
|
|
797
|
+
if (state.sessionConfig.autoResetOnNew && state.currentTaskId) {
|
|
798
|
+
toast('自动新建对话,旧任务保留在历史');
|
|
799
|
+
state.currentTaskId = null;
|
|
800
|
+
}
|
|
801
|
+
// 已有当前任务 → 多轮续接:消息归属同一任务生命周期
|
|
802
|
+
if (state.currentTaskId) {
|
|
803
|
+
const cur = state.tasks.find((t) => t.id === state.currentTaskId);
|
|
804
|
+
if (cur && (cur.status === 'queued' || cur.status === 'running')) {
|
|
805
|
+
toast('当前任务执行中,请等待完成后再继续对话');
|
|
806
|
+
return;
|
|
807
|
+
}
|
|
808
|
+
const d = await api('/api/tasks/' + state.currentTaskId + '/messages', 'POST', {
|
|
809
|
+
message: goal || '(仅附件)',
|
|
810
|
+
attachments,
|
|
811
|
+
});
|
|
812
|
+
input.value = '';
|
|
813
|
+
clearStagedFiles();
|
|
814
|
+
renderSidebarTaskList();
|
|
815
|
+
renderTaskDetail(d.task.id);
|
|
816
|
+
renderTaskThread(d.task);
|
|
817
|
+
await loadTasks();
|
|
818
|
+
toast(attachments.length ? `已发送(含 ${attachments.length} 个附件),继续当前任务对话` : '已发送,继续当前任务对话');
|
|
819
|
+
return;
|
|
820
|
+
}
|
|
821
|
+
const d = await api('/api/tasks', 'POST', {
|
|
822
|
+
goal: goal || '(仅附件)',
|
|
823
|
+
agentType: state.agentType,
|
|
824
|
+
permissions: state.permissions,
|
|
825
|
+
workspaceDir: state.workspaceDir || undefined,
|
|
826
|
+
modelId: state.modelId || undefined,
|
|
827
|
+
attachments: attachments.length ? attachments : undefined,
|
|
828
|
+
});
|
|
829
|
+
input.value = '';
|
|
830
|
+
clearStagedFiles();
|
|
831
|
+
// 新任务即成为当前任务,中部围绕其单一生命周期可视化追踪
|
|
832
|
+
state.currentTaskId = d.task.id;
|
|
833
|
+
const taskListSection = document.getElementById('taskListSection');
|
|
834
|
+
if (taskListSection) taskListSection.style.display = 'block';
|
|
835
|
+
renderSidebarTaskList();
|
|
836
|
+
renderTaskDetail(d.task.id);
|
|
837
|
+
renderTaskThread({ id: d.task.id, goal: goal || '(仅附件)', status: 'queued', createdAt: new Date().toISOString(), steps: [], conversation: [{ role: 'user', content: goal || '(仅附件)' }] });
|
|
838
|
+
await loadTasks();
|
|
839
|
+
toast(attachments.length ? `任务已提交(含 ${attachments.length} 个附件),开始追踪思维链路` : '任务已提交,开始追踪思维链路');
|
|
840
|
+
} catch (e) { toast('提交失败:' + e.message); }
|
|
841
|
+
finally { btn.disabled = false; }
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
/* ========== 输入区·附件暂存(点发送时统一上传) ========== */
|
|
845
|
+
function stageFile(file) {
|
|
846
|
+
const id = 'sf_' + Date.now() + '_' + Math.random().toString(36).slice(2, 6);
|
|
847
|
+
state.stagedFiles.push({ id, name: file.name, mime: file.mime, dataUrl: file.dataUrl, size: estimateDataUrlSize(file.dataUrl) });
|
|
848
|
+
renderStagedFiles();
|
|
849
|
+
}
|
|
850
|
+
function removeStagedFile(id) {
|
|
851
|
+
state.stagedFiles = state.stagedFiles.filter((f) => f.id !== id);
|
|
852
|
+
renderStagedFiles();
|
|
853
|
+
}
|
|
854
|
+
function clearStagedFiles() {
|
|
855
|
+
state.stagedFiles = [];
|
|
856
|
+
renderStagedFiles();
|
|
857
|
+
}
|
|
858
|
+
async function uploadStagedFiles() {
|
|
859
|
+
if (!state.stagedFiles.length) return [];
|
|
860
|
+
const paths = [];
|
|
861
|
+
for (const f of state.stagedFiles) {
|
|
862
|
+
try {
|
|
863
|
+
const base64 = (f.dataUrl || '').split(',')[1] || '';
|
|
864
|
+
const d = await api('/api/upload', 'POST', { name: f.name, mime: f.mime, dataBase64: base64 });
|
|
865
|
+
paths.push(d.path);
|
|
866
|
+
} catch (e) { toast('上传失败:' + f.name + '(' + e.message + ')'); }
|
|
867
|
+
}
|
|
868
|
+
return paths;
|
|
869
|
+
}
|
|
870
|
+
function appendMsg(role, text, extras = {}) {
|
|
871
|
+
const box = document.getElementById('messages');
|
|
872
|
+
const el = document.createElement('div');
|
|
873
|
+
el.className = 'msg ' + role;
|
|
874
|
+
if (extras.html) el.innerHTML = extras.html;
|
|
875
|
+
else if (extras.file) {
|
|
876
|
+
el.innerHTML = '<span class="file-chip" data-path="' + escapeHtml(extras.file) + '" data-type="' + escapeHtml(extras.mime || 'file') + '">📎 ' + escapeHtml(extras.name) + '</span>';
|
|
877
|
+
} else {
|
|
878
|
+
el.innerHTML = linkifyArtifacts(text);
|
|
879
|
+
}
|
|
880
|
+
box.appendChild(el);
|
|
881
|
+
box.scrollTop = box.scrollHeight;
|
|
882
|
+
bindArtifacts(el);
|
|
883
|
+
}
|
|
884
|
+
document.getElementById('sendBtn').addEventListener('click', sendTask);
|
|
885
|
+
document.getElementById('goalInput').addEventListener('keydown', (e) => { if (e.key === 'Enter' && (e.ctrlKey || e.metaKey)) sendTask(); });
|
|
886
|
+
document.getElementById('tchRefresh').addEventListener('click', () => { refreshCurrentThread(); toast('已刷新当前任务'); });
|
|
887
|
+
document.getElementById('tchStop').addEventListener('click', async () => {
|
|
888
|
+
if (!state.currentTaskId) return;
|
|
889
|
+
try {
|
|
890
|
+
await api('/api/tasks/' + state.currentTaskId + '/stop', 'POST');
|
|
891
|
+
toast('已发送停止指令,任务即将终止');
|
|
892
|
+
await refreshCurrentThread();
|
|
893
|
+
} catch (e) { toast('停止失败:' + e.message); }
|
|
894
|
+
});
|
|
895
|
+
document.getElementById('tchNewTask').addEventListener('click', () => { try { openNewTaskModal(); } catch { toast('新建任务面板不可用'); } });
|
|
896
|
+
document.getElementById('screenshotBtn').addEventListener('click', takeScreenshot);
|
|
897
|
+
|
|
898
|
+
/* ========== 输入框右键菜单(复制/剪切/粘贴/全选) ========== */
|
|
899
|
+
const goalInput = document.getElementById('goalInput');
|
|
900
|
+
goalInput.addEventListener('contextmenu', (e) => {
|
|
901
|
+
e.preventDefault();
|
|
902
|
+
// 移除旧菜单
|
|
903
|
+
const old = document.getElementById('textCtxMenu');
|
|
904
|
+
if (old) old.remove();
|
|
905
|
+
// 记录右键时的选中区间(菜单点击会令文本框失焦,故需先记住)
|
|
906
|
+
const sel = { start: goalInput.selectionStart, end: goalInput.selectionEnd };
|
|
907
|
+
const hasSel = sel.end > sel.start;
|
|
908
|
+
// 创建右键菜单
|
|
909
|
+
const menu = document.createElement('div');
|
|
910
|
+
menu.id = 'textCtxMenu';
|
|
911
|
+
menu.style.cssText = 'position:fixed;z-index:10000;background:var(--card);border:1px solid var(--border);border-radius:8px;box-shadow:0 4px 12px rgba(0,0,0,0.15);padding:4px 0;min-width:160px;';
|
|
912
|
+
const items = [
|
|
913
|
+
{ label: '📋 复制', action: 'copy', disabled: !hasSel },
|
|
914
|
+
{ label: '✂️ 剪切', action: 'cut', disabled: !hasSel },
|
|
915
|
+
{ label: '📌 粘贴', action: 'paste' },
|
|
916
|
+
{ label: '⬛ 全选', action: 'selectall' },
|
|
917
|
+
];
|
|
918
|
+
// 恢复文本框聚焦与选中,供 execCommand 兜底使用
|
|
919
|
+
const restoreSel = () => {
|
|
920
|
+
goalInput.focus();
|
|
921
|
+
goalInput.setSelectionRange(sel.start, sel.end);
|
|
922
|
+
};
|
|
923
|
+
items.forEach((it) => {
|
|
924
|
+
const el = document.createElement('div');
|
|
925
|
+
el.className = 'dropdown-item';
|
|
926
|
+
el.textContent = it.label;
|
|
927
|
+
el.style.cssText = 'padding:6px 16px;cursor:' + (it.disabled ? 'default' : 'pointer') + ';font-size:13px;opacity:' + (it.disabled ? 0.4 : 1) + ';';
|
|
928
|
+
el.addEventListener('mouseenter', () => { if (!it.disabled) el.style.background = 'var(--brand-soft)'; });
|
|
929
|
+
el.addEventListener('mouseleave', () => { el.style.background = ''; });
|
|
930
|
+
el.addEventListener('click', async () => {
|
|
931
|
+
menu.remove();
|
|
932
|
+
if (it.disabled) return;
|
|
933
|
+
if (it.action === 'copy') {
|
|
934
|
+
const selText = goalInput.value.slice(sel.start, sel.end);
|
|
935
|
+
try { await navigator.clipboard.writeText(selText); toast('已复制'); }
|
|
936
|
+
catch { restoreSel(); document.execCommand('copy'); toast('已复制'); }
|
|
937
|
+
} else if (it.action === 'cut') {
|
|
938
|
+
const selText = goalInput.value.slice(sel.start, sel.end);
|
|
939
|
+
try { await navigator.clipboard.writeText(selText); } catch {}
|
|
940
|
+
goalInput.setRangeText('', sel.start, sel.end, 'preserve');
|
|
941
|
+
goalInput.focus();
|
|
942
|
+
goalInput.setSelectionRange(sel.start, sel.start);
|
|
943
|
+
if (selText) toast('已剪切');
|
|
944
|
+
} else if (it.action === 'paste') {
|
|
945
|
+
let text = '';
|
|
946
|
+
try { text = await navigator.clipboard.readText().catch(() => ''); } catch {}
|
|
947
|
+
if (text) {
|
|
948
|
+
goalInput.setRangeText(text, sel.start, sel.end, 'end');
|
|
949
|
+
goalInput.focus();
|
|
950
|
+
} else {
|
|
951
|
+
// 读取失败则回退到系统粘贴(依赖当前选中)
|
|
952
|
+
restoreSel();
|
|
953
|
+
document.execCommand('paste');
|
|
954
|
+
}
|
|
955
|
+
} else if (it.action === 'selectall') {
|
|
956
|
+
goalInput.focus();
|
|
957
|
+
goalInput.select();
|
|
958
|
+
}
|
|
959
|
+
});
|
|
960
|
+
menu.appendChild(el);
|
|
961
|
+
});
|
|
962
|
+
document.body.appendChild(menu);
|
|
963
|
+
menu.style.left = e.clientX + 'px';
|
|
964
|
+
menu.style.top = e.clientY + 'px';
|
|
965
|
+
});
|
|
966
|
+
// 点击其他地方或失焦关闭右键菜单
|
|
967
|
+
document.addEventListener('click', (e) => {
|
|
968
|
+
const menu = document.getElementById('textCtxMenu');
|
|
969
|
+
if (menu && !menu.contains(e.target)) menu.remove();
|
|
970
|
+
});
|
|
971
|
+
document.addEventListener('blur', () => {
|
|
972
|
+
const menu = document.getElementById('textCtxMenu');
|
|
973
|
+
if (menu) menu.remove();
|
|
974
|
+
});
|
|
975
|
+
// 支持在输入框内直接粘贴图片(与「上传图片/截图」共用同一暂存区,发送时一起上传)
|
|
976
|
+
goalInput.addEventListener('paste', (e) => {
|
|
977
|
+
if (!e.clipboardData) return;
|
|
978
|
+
const items = e.clipboardData.items;
|
|
979
|
+
const images = [];
|
|
980
|
+
for (let i = 0; i < items.length; i++) {
|
|
981
|
+
const item = items[i];
|
|
982
|
+
if (item.kind === 'file' && item.type && item.type.startsWith('image/')) {
|
|
983
|
+
const f = item.getAsFile();
|
|
984
|
+
if (f) images.push(f);
|
|
985
|
+
}
|
|
986
|
+
}
|
|
987
|
+
if (!images.length) return; // 非图片粘贴(文本等)交给浏览器默认行为
|
|
988
|
+
e.preventDefault();
|
|
989
|
+
images.forEach((file, idx) => {
|
|
990
|
+
const reader = new FileReader();
|
|
991
|
+
reader.onload = () => {
|
|
992
|
+
const name = file.name || ('paste-image-' + (idx + 1) + '.png');
|
|
993
|
+
stageFile({ name, mime: file.type || 'image/png', dataUrl: String(reader.result) });
|
|
994
|
+
toast('已暂存图片:' + name + '(点发送时一起发给 AI)');
|
|
995
|
+
};
|
|
996
|
+
reader.readAsDataURL(file);
|
|
997
|
+
});
|
|
998
|
+
});
|
|
999
|
+
|
|
1000
|
+
/* ========== 新建对话任务 ========== */
|
|
1001
|
+
async function openNewTaskModal() {
|
|
1002
|
+
// 智能体类型下拉
|
|
1003
|
+
const agentSel = document.getElementById('ntAgentType');
|
|
1004
|
+
agentSel.innerHTML = Object.entries(AGENT_TYPES)
|
|
1005
|
+
.map(([k, v]) => '<option value="' + k + '"' + (state.agentType === k ? ' selected' : '') + '>' + v.icon + ' ' + v.label + '</option>')
|
|
1006
|
+
.join('');
|
|
1007
|
+
// 工作区下拉:默认(沿用当前底部所选)+ 磁盘根目录
|
|
1008
|
+
const wsSel = document.getElementById('ntWorkspaceDir');
|
|
1009
|
+
const cur = state.workspaceDir || '';
|
|
1010
|
+
wsSel.innerHTML = '<option value="">默认(' + (cur || '服务器工作区') + ')</option>';
|
|
1011
|
+
try {
|
|
1012
|
+
const d = await api('/api/drives');
|
|
1013
|
+
(d.drives || []).forEach((dr) => {
|
|
1014
|
+
if (dr !== cur) wsSel.innerHTML += '<option value="' + escapeHtml(dr) + '">' + escapeHtml(dr) + '</option>';
|
|
1015
|
+
});
|
|
1016
|
+
} catch {}
|
|
1017
|
+
document.getElementById('ntGoal').value = '';
|
|
1018
|
+
openModal('newTaskModal');
|
|
1019
|
+
setTimeout(() => document.getElementById('ntGoal').focus(), 60);
|
|
1020
|
+
}
|
|
1021
|
+
async function createNewTask() {
|
|
1022
|
+
const goal = document.getElementById('ntGoal').value.trim();
|
|
1023
|
+
if (!goal) { toast('请填写任务描述'); return; }
|
|
1024
|
+
const btn = document.getElementById('ntCreateBtn');
|
|
1025
|
+
btn.disabled = true;
|
|
1026
|
+
try {
|
|
1027
|
+
const wsVal = document.getElementById('ntWorkspaceDir').value;
|
|
1028
|
+
const payload = {
|
|
1029
|
+
goal,
|
|
1030
|
+
agentType: document.getElementById('ntAgentType').value || state.agentType,
|
|
1031
|
+
permissions: state.permissions,
|
|
1032
|
+
modelId: state.modelId || undefined,
|
|
1033
|
+
};
|
|
1034
|
+
// 工作区:仅在明确选择了目录时覆盖
|
|
1035
|
+
const d = await api('/api/tasks', 'POST', wsVal ? Object.assign(payload, { workspaceDir: wsVal }) : payload);
|
|
1036
|
+
document.getElementById('goalInput').value = '';
|
|
1037
|
+
state.currentTaskId = d.task.id;
|
|
1038
|
+
const taskListSection = document.getElementById('taskListSection');
|
|
1039
|
+
if (taskListSection) taskListSection.style.display = 'block';
|
|
1040
|
+
renderSidebarTaskList();
|
|
1041
|
+
renderTaskDetail(d.task.id);
|
|
1042
|
+
renderTaskThread({ id: d.task.id, goal, status: 'queued', createdAt: new Date().toISOString(), steps: [], conversation: [{ role: 'user', content: goal }] });
|
|
1043
|
+
switchView('chat');
|
|
1044
|
+
closeModal('newTaskModal');
|
|
1045
|
+
await loadTasks();
|
|
1046
|
+
toast('新任务已创建,所有对话将归属该任务');
|
|
1047
|
+
} catch (e) { toast('创建失败:' + e.message); }
|
|
1048
|
+
finally { btn.disabled = false; }
|
|
1049
|
+
}
|
|
1050
|
+
document.getElementById('newTaskBtn')?.addEventListener('click', openNewTaskModal);
|
|
1051
|
+
document.getElementById('tchNewTask')?.addEventListener('click', openNewTaskModal);
|
|
1052
|
+
document.getElementById('ntCreateBtn')?.addEventListener('click', createNewTask);
|
|
1053
|
+
document.getElementById('ntGoal')?.addEventListener('keydown', (e) => { if (e.key === 'Enter' && (e.ctrlKey || e.metaKey)) createNewTask(); });
|
|
1054
|
+
|
|
1055
|
+
/* ========== 产物链接化 ========== */
|
|
1056
|
+
|
|
1057
|
+
/* ========== 中间菜单 ========== */
|
|
1058
|
+
const convMenuWrap = document.getElementById('convMenuWrap');
|
|
1059
|
+
const convMenu = document.getElementById('convMenu');
|
|
1060
|
+
document.getElementById('convMenuBtn').addEventListener('click', (e) => {
|
|
1061
|
+
e.stopPropagation();
|
|
1062
|
+
convMenu.classList.toggle('show');
|
|
1063
|
+
});
|
|
1064
|
+
document.addEventListener('click', (e) => { if (!convMenuWrap.contains(e.target)) convMenu.classList.remove('show'); });
|
|
1065
|
+
convMenu.querySelectorAll('.dropdown-item').forEach((el) => {
|
|
1066
|
+
el.addEventListener('click', () => {
|
|
1067
|
+
convMenu.classList.remove('show');
|
|
1068
|
+
const action = el.getAttribute('data-action');
|
|
1069
|
+
if (action === 'screenshot') takeScreenshot();
|
|
1070
|
+
else if (action === 'upload-file') document.getElementById('fileInput').click();
|
|
1071
|
+
else if (action === 'upload-image') document.getElementById('imageInput').click();
|
|
1072
|
+
else if (action === 'direct-mode') toggleDirectMode();
|
|
1073
|
+
else if (action === 'voice') startVoice();
|
|
1074
|
+
else if (action === 'agent-type') openAgentSelector();
|
|
1075
|
+
});
|
|
1076
|
+
});
|
|
1077
|
+
|
|
1078
|
+
|
|
1079
|
+
async function takeScreenshot() {
|
|
1080
|
+
try {
|
|
1081
|
+
const overlay = document.getElementById('cropOverlay');
|
|
1082
|
+
const selection = document.getElementById('cropSelection');
|
|
1083
|
+
const video = document.getElementById('cropVideo');
|
|
1084
|
+
const stream = await navigator.mediaDevices.getDisplayMedia({ video: true });
|
|
1085
|
+
video.srcObject = stream;
|
|
1086
|
+
await video.play();
|
|
1087
|
+
overlay.style.display = 'block';
|
|
1088
|
+
selection.style.display = 'none';
|
|
1089
|
+
let startX = 0, startY = 0, drawing = false;
|
|
1090
|
+
const onDown = (e) => {
|
|
1091
|
+
drawing = true;
|
|
1092
|
+
startX = e.clientX; startY = e.clientY;
|
|
1093
|
+
selection.style.display = 'block';
|
|
1094
|
+
selection.style.left = startX + 'px';
|
|
1095
|
+
selection.style.top = startY + 'px';
|
|
1096
|
+
selection.style.width = '0px';
|
|
1097
|
+
selection.style.height = '0px';
|
|
1098
|
+
};
|
|
1099
|
+
const onMove = (e) => {
|
|
1100
|
+
if (!drawing) return;
|
|
1101
|
+
const x = Math.min(e.clientX, startX), y = Math.min(e.clientY, startY);
|
|
1102
|
+
const w = Math.abs(e.clientX - startX), h = Math.abs(e.clientY - startY);
|
|
1103
|
+
selection.style.left = x + 'px';
|
|
1104
|
+
selection.style.top = y + 'px';
|
|
1105
|
+
selection.style.width = w + 'px';
|
|
1106
|
+
selection.style.height = h + 'px';
|
|
1107
|
+
};
|
|
1108
|
+
const finish = async (e) => {
|
|
1109
|
+
if (!drawing) return;
|
|
1110
|
+
drawing = false;
|
|
1111
|
+
window.removeEventListener('mousedown', onDown);
|
|
1112
|
+
window.removeEventListener('mousemove', onMove);
|
|
1113
|
+
window.removeEventListener('mouseup', finish);
|
|
1114
|
+
const x = Math.min(e.clientX, startX), y = Math.min(e.clientY, startY);
|
|
1115
|
+
const w = Math.abs(e.clientX - startX), h = Math.abs(e.clientY - startY);
|
|
1116
|
+
overlay.style.display = 'none';
|
|
1117
|
+
if (w < 8 || h < 8) { stream.getTracks().forEach((t) => t.stop()); return; }
|
|
1118
|
+
try {
|
|
1119
|
+
const srcW = video.videoWidth, srcH = video.videoHeight;
|
|
1120
|
+
const scaleX = srcW / window.innerWidth, scaleY = srcH / window.innerHeight;
|
|
1121
|
+
const sx = Math.round(x * scaleX), sy = Math.round(y * scaleY);
|
|
1122
|
+
const sw = Math.round(w * scaleX), sh = Math.round(h * scaleY);
|
|
1123
|
+
const canvas = document.createElement('canvas');
|
|
1124
|
+
canvas.width = sw; canvas.height = sh;
|
|
1125
|
+
canvas.getContext('2d').drawImage(video, sx, sy, sw, sh, 0, 0, sw, sh);
|
|
1126
|
+
stream.getTracks().forEach((t) => t.stop());
|
|
1127
|
+
const dataUrl = canvas.toDataURL('image/png');
|
|
1128
|
+
stageFile({ name: 'screenshot.png', mime: 'image/png', dataUrl });
|
|
1129
|
+
toast('截图已加入暂存,点发送时一起发给 AI');
|
|
1130
|
+
} catch (err) { toast('截图失败:' + err.message); }
|
|
1131
|
+
};
|
|
1132
|
+
const onKey = (e) => {
|
|
1133
|
+
if (e.key === 'Escape') {
|
|
1134
|
+
overlay.style.display = 'none';
|
|
1135
|
+
stream.getTracks().forEach((t) => t.stop());
|
|
1136
|
+
window.removeEventListener('mousedown', onDown);
|
|
1137
|
+
window.removeEventListener('mousemove', onMove);
|
|
1138
|
+
window.removeEventListener('mouseup', finish);
|
|
1139
|
+
window.removeEventListener('keydown', onKey);
|
|
1140
|
+
}
|
|
1141
|
+
};
|
|
1142
|
+
window.addEventListener('mousedown', onDown);
|
|
1143
|
+
window.addEventListener('mousemove', onMove);
|
|
1144
|
+
window.addEventListener('mouseup', finish);
|
|
1145
|
+
window.addEventListener('keydown', onKey);
|
|
1146
|
+
} catch (e) { toast('截图失败:' + e.message); }
|
|
1147
|
+
}
|
|
1148
|
+
|
|
1149
|
+
function handleUpload(input, mimePrefix) {
|
|
1150
|
+
const file = input.files[0];
|
|
1151
|
+
if (!file) return;
|
|
1152
|
+
const reader = new FileReader();
|
|
1153
|
+
reader.onload = () => {
|
|
1154
|
+
stageFile({ name: file.name, mime: file.type || mimePrefix, dataUrl: String(reader.result) });
|
|
1155
|
+
toast('已暂存:' + file.name + '(点发送时一起发给 AI)');
|
|
1156
|
+
};
|
|
1157
|
+
reader.readAsDataURL(file);
|
|
1158
|
+
input.value = '';
|
|
1159
|
+
}
|
|
1160
|
+
document.getElementById('fileInput').addEventListener('change', function() { handleUpload(this, 'application/octet-stream'); });
|
|
1161
|
+
document.getElementById('imageInput').addEventListener('change', function() { handleUpload(this, 'image/png'); });
|
|
1162
|
+
|
|
1163
|
+
/* ========== 大模型配置(三重加密:密钥 RSA 加密传输,服务端 AES 加密落盘) ========== */
|
|
1164
|
+
// 第二重(通信层):用服务器 RSA 公钥加密敏感文本(如模型 API Key)
|
|
1165
|
+
// 持久化会话配置到 localStorage
|
|
1166
|
+
// 切换会话模式(打开/发送时自动重置)
|
|
1167
|
+
document.querySelectorAll('.session-toggle').forEach((el) => {
|
|
1168
|
+
el.addEventListener('change', () => {
|
|
1169
|
+
const key = el.dataset.key;
|
|
1170
|
+
state.sessionConfig[key] = el.checked;
|
|
1171
|
+
persistSessionConfig();
|
|
1172
|
+
toast('会话配置已保存');
|
|
1173
|
+
});
|
|
1174
|
+
});
|
|
1175
|
+
document.getElementById('modelSaveBtn').addEventListener('click', saveModel);
|
|
1176
|
+
document.getElementById('modelResetBtn').addEventListener('click', resetModelForm);
|
|
1177
|
+
document.getElementById('modelSettingsShortcut').addEventListener('click', openModels);
|
|
1178
|
+
document.getElementById('modelSelect').addEventListener('change', (e) => {
|
|
1179
|
+
state.modelId = e.target.value || '';
|
|
1180
|
+
localStorage.setItem('fhcode.model', state.modelId);
|
|
1181
|
+
const m = state.models.find((x) => x.id === state.modelId);
|
|
1182
|
+
toast(m ? ('已切换:' + m.name) : '已切换:系统默认路由');
|
|
1183
|
+
});
|
|
1184
|
+
|
|
1185
|
+
/* ========== 设置面板 ========== */
|
|
1186
|
+
document.querySelectorAll('#settingsModal .settings-cat').forEach((c) => {
|
|
1187
|
+
c.addEventListener('click', () => {
|
|
1188
|
+
const cat = c.getAttribute('data-cat');
|
|
1189
|
+
document.querySelectorAll('#settingsModal .settings-cat').forEach((x) => x.classList.toggle('active', x === c));
|
|
1190
|
+
document.querySelectorAll('#settingsModal .settings-group').forEach((g) => {
|
|
1191
|
+
g.style.display = g.getAttribute('data-group') === cat ? '' : 'none';
|
|
1192
|
+
});
|
|
1193
|
+
});
|
|
1194
|
+
});
|
|
1195
|
+
document.querySelectorAll('#settingsModal .settings-item').forEach((it) => {
|
|
1196
|
+
it.addEventListener('click', () => {
|
|
1197
|
+
const act = it.getAttribute('data-action');
|
|
1198
|
+
if (act === 'perm') { closeModal('settingsModal'); openPermissions(); }
|
|
1199
|
+
else if (act === 'theme') { closeModal('settingsModal'); toggleTheme(); }
|
|
1200
|
+
else if (act === 'model-custom') { toggleModelManage(); }
|
|
1201
|
+
});
|
|
1202
|
+
});
|
|
1203
|
+
async function saveInlineModel() {
|
|
1204
|
+
const name = document.getElementById('imName').value.trim();
|
|
1205
|
+
const apiBase = document.getElementById('imBase').value.trim();
|
|
1206
|
+
const apiKey = document.getElementById('imKey').value;
|
|
1207
|
+
const reasoning = document.getElementById('imReasoning').value;
|
|
1208
|
+
const msg = document.getElementById('imMsg');
|
|
1209
|
+
if (!name) { msg.textContent = '请填写模型名称'; msg.classList.add('err'); return; }
|
|
1210
|
+
const editing = document.getElementById('imSave').dataset.editing;
|
|
1211
|
+
try {
|
|
1212
|
+
const body = { name, apiBase, reasoning };
|
|
1213
|
+
// 三重加密·通信层:密钥 RSA 加密传输
|
|
1214
|
+
if (apiKey) body.apiKeyEnc = await rsaEncryptText(apiKey);
|
|
1215
|
+
if (editing) body.id = editing;
|
|
1216
|
+
const d = await api('/api/models', 'POST', body);
|
|
1217
|
+
msg.textContent = '已保存:' + d.model.name;
|
|
1218
|
+
msg.classList.remove('err');
|
|
1219
|
+
await loadModels();
|
|
1220
|
+
renderModelListInline();
|
|
1221
|
+
resetInlineForm();
|
|
1222
|
+
} catch (e) { msg.textContent = '保存失败:' + e.message; msg.classList.add('err'); }
|
|
1223
|
+
}
|
|
1224
|
+
document.getElementById('imSave').addEventListener('click', saveInlineModel);
|
|
1225
|
+
document.getElementById('imReset').addEventListener('click', resetInlineForm);
|
|
1226
|
+
document.getElementById('modelManageClose')?.addEventListener('click', () => { document.getElementById('modelManage').style.display = 'none'; });
|
|
1227
|
+
|
|
1228
|
+
/* ========== 通话控件 ========== */
|
|
1229
|
+
let callStream = null;
|
|
1230
|
+
async function startCall(kind) {
|
|
1231
|
+
const status = document.getElementById('callStatus');
|
|
1232
|
+
try {
|
|
1233
|
+
const constraints = kind === 'video' ? { audio: true, video: true } : { audio: true };
|
|
1234
|
+
callStream = await navigator.mediaDevices.getUserMedia(constraints);
|
|
1235
|
+
status.textContent = (kind === 'video' ? '视频通话中' : '语音通话中') + '…';
|
|
1236
|
+
document.getElementById('hangupCallBtn').style.display = 'inline-block';
|
|
1237
|
+
toast('已发起' + (kind === 'video' ? '视频' : '语音') + '通话(演示)');
|
|
1238
|
+
} catch (e) {
|
|
1239
|
+
status.textContent = '';
|
|
1240
|
+
toast('无法发起通话:' + e.message);
|
|
1241
|
+
}
|
|
1242
|
+
}
|
|
1243
|
+
function hangupCall() {
|
|
1244
|
+
if (callStream) { callStream.getTracks().forEach((t) => t.stop()); callStream = null; }
|
|
1245
|
+
document.getElementById('callStatus').textContent = '';
|
|
1246
|
+
document.getElementById('hangupCallBtn').style.display = 'none';
|
|
1247
|
+
toast('已挂断');
|
|
1248
|
+
}
|
|
1249
|
+
document.getElementById('voiceCallBtn').addEventListener('click', () => startCall('voice'));
|
|
1250
|
+
document.getElementById('videoCallBtn').addEventListener('click', () => startCall('video'));
|
|
1251
|
+
document.getElementById('hangupCallBtn').addEventListener('click', hangupCall);
|
|
1252
|
+
|
|
1253
|
+
function startVoice() {
|
|
1254
|
+
const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition;
|
|
1255
|
+
if (!SpeechRecognition) { toast('当前浏览器不支持语音输入'); return; }
|
|
1256
|
+
const rec = new SpeechRecognition();
|
|
1257
|
+
rec.lang = 'zh-CN';
|
|
1258
|
+
rec.continuous = false;
|
|
1259
|
+
rec.interimResults = false;
|
|
1260
|
+
const badge = document.getElementById('voiceBadge');
|
|
1261
|
+
badge.style.display = 'inline-flex';
|
|
1262
|
+
rec.onresult = (e) => {
|
|
1263
|
+
const text = e.results[0][0].transcript;
|
|
1264
|
+
const input = document.getElementById('goalInput');
|
|
1265
|
+
input.value = (input.value ? input.value + ' ' : '') + text;
|
|
1266
|
+
};
|
|
1267
|
+
rec.onerror = (e) => { toast('语音识别失败:' + e.error); badge.style.display = 'none'; };
|
|
1268
|
+
rec.onend = () => { badge.style.display = 'none'; };
|
|
1269
|
+
rec.start();
|
|
1270
|
+
}
|
|
1271
|
+
|
|
1272
|
+
/* ========== 智能体选择 ========== */
|
|
1273
|
+
document.getElementById('skillPill').addEventListener('click', openAgentSelector);
|
|
1274
|
+
|
|
1275
|
+
/* ========== 权限窗口 ========== */
|
|
1276
|
+
document.getElementById('permSaveBtn').addEventListener('click', () => {
|
|
1277
|
+
const scope = document.querySelector('input[name="readScope"]:checked')?.value || 'workspace';
|
|
1278
|
+
state.permissions = {
|
|
1279
|
+
readScope: scope,
|
|
1280
|
+
readPath: document.getElementById('permReadPath').value.trim(),
|
|
1281
|
+
allowRead: document.getElementById('permRead').checked,
|
|
1282
|
+
allowWrite: document.getElementById('permWrite').checked,
|
|
1283
|
+
allowShell: document.getElementById('permShell').checked,
|
|
1284
|
+
allowNetwork: document.getElementById('permNetwork').checked,
|
|
1285
|
+
allowBrowser: document.getElementById('permBrowser').checked,
|
|
1286
|
+
};
|
|
1287
|
+
localStorage.setItem('fhcode.permissions', JSON.stringify(state.permissions));
|
|
1288
|
+
closeModal('permModal');
|
|
1289
|
+
toast('权限配置已保存');
|
|
1290
|
+
});
|
|
1291
|
+
|
|
1292
|
+
/* ========== 工作区 ========== */
|
|
1293
|
+
// 工作区跳转功能已移除(旧布局左侧工作区树)
|
|
1294
|
+
|
|
1295
|
+
document.getElementById('workspacePermBadge')?.addEventListener('click', () => openPermissions());
|
|
1296
|
+
document.getElementById('workspacePickBtn')?.addEventListener('click', () => {
|
|
1297
|
+
openFolderPicker();
|
|
1298
|
+
});
|
|
1299
|
+
|
|
1300
|
+
let fpCurrent = '';
|
|
1301
|
+
async function openFolderPicker() {
|
|
1302
|
+
console.log('[folderPicker] 打开文件夹选择器');
|
|
1303
|
+
console.log('[folderPicker] 当前 state.workspaceDir:', state.workspaceDir);
|
|
1304
|
+
console.log('[folderPicker] 当前 state.token:', state.token ? '已登录' : '未登录');
|
|
1305
|
+
|
|
1306
|
+
// 如果 workspaceDir 为空,尝试从 localStorage 恢复或询问用户
|
|
1307
|
+
let initialDir = state.workspaceDir;
|
|
1308
|
+
if (!initialDir) {
|
|
1309
|
+
// 尝试从 localStorage 恢复
|
|
1310
|
+
const savedCwd = localStorage.getItem('fhcode.workspaceDir');
|
|
1311
|
+
if (savedCwd) {
|
|
1312
|
+
initialDir = savedCwd;
|
|
1313
|
+
console.log('[folderPicker] 从 localStorage 恢复工作区:', initialDir);
|
|
1314
|
+
} else {
|
|
1315
|
+
// 使用默认路径
|
|
1316
|
+
initialDir = '.';
|
|
1317
|
+
console.log('[folderPicker] 使用默认路径:', initialDir);
|
|
1318
|
+
}
|
|
1319
|
+
}
|
|
1320
|
+
|
|
1321
|
+
fpCurrent = initialDir || '.';
|
|
1322
|
+
const fpPathEl = document.getElementById('fpPath');
|
|
1323
|
+
if (fpPathEl) fpPathEl.textContent = fpCurrent || '(根目录)';
|
|
1324
|
+
|
|
1325
|
+
try {
|
|
1326
|
+
console.log('[folderPicker] 加载目录列表:', fpCurrent);
|
|
1327
|
+
await loadFolderPicker(fpCurrent);
|
|
1328
|
+
console.log('[folderPicker] 目录加载成功');
|
|
1329
|
+
} catch (e) {
|
|
1330
|
+
console.error('[folderPicker] 加载失败:', e);
|
|
1331
|
+
toast('读取目录失败:' + e.message);
|
|
1332
|
+
return;
|
|
1333
|
+
}
|
|
1334
|
+
openModal('folderPickerModal');
|
|
1335
|
+
console.log('[folderPicker] 模态框已打开');
|
|
1336
|
+
}
|
|
1337
|
+
async function loadFolderPicker(dir) {
|
|
1338
|
+
console.log('[folderPicker] 加载:', dir);
|
|
1339
|
+
const d = await api('/api/workspace/list?path=' + encodeURIComponent(dir));
|
|
1340
|
+
fpCurrent = d.cwd;
|
|
1341
|
+
const fpPathEl = document.getElementById('fpPath');
|
|
1342
|
+
if (fpPathEl) fpPathEl.textContent = fpCurrent;
|
|
1343
|
+
const tree = document.getElementById('fpTree');
|
|
1344
|
+
if (!tree) {
|
|
1345
|
+
console.error('[folderPicker] fpTree 元素不存在');
|
|
1346
|
+
return;
|
|
1347
|
+
}
|
|
1348
|
+
const parentPath = getDirectoryName(d.cwd);
|
|
1349
|
+
// 如果在驱动器根目录,不显示"..",改为显示驱动器列表
|
|
1350
|
+
const isDriveRoot = /^[A-Za-z]:\\$/.test(d.cwd);
|
|
1351
|
+
let upButtonHtml = '';
|
|
1352
|
+
if (!isDriveRoot && parentPath && parentPath !== d.cwd) {
|
|
1353
|
+
upButtonHtml = '<div class="file-item" data-path="' + escapeHtml(parentPath) + '" data-type="up">⬆ ..</div>';
|
|
1354
|
+
} else if (isDriveRoot) {
|
|
1355
|
+
// 动态获取可用驱动器列表
|
|
1356
|
+
const drives = await getDrivesList();
|
|
1357
|
+
upButtonHtml = '<div class="file-item drive-switcher">' +
|
|
1358
|
+
'<span>🖥 切换驱动器</span>' +
|
|
1359
|
+
drives.map(d => '<div class="drive-option" data-drive="' + d + '">' + d + '</div>').join('') +
|
|
1360
|
+
'</div>';
|
|
1361
|
+
}
|
|
1362
|
+
tree.innerHTML = upButtonHtml +
|
|
1363
|
+
d.entries.map((e) => '<div class="file-item ' + (e.type === 'dir' ? 'dir' : '') + '" data-path="' + escapeHtml(e.path) + '" data-type="' + e.type + '">' + (e.type === 'dir' ? '📁' : '📄') + ' ' + escapeHtml(e.name) + '</div>').join('');
|
|
1364
|
+
console.log('[folderPicker] 目录渲染完成, 条目数:', d.entries.length);
|
|
1365
|
+
}
|
|
1366
|
+
// 驱动器列表缓存
|
|
1367
|
+
let _drivesCache = null;
|
|
1368
|
+
|
|
1369
|
+
document.getElementById('fpConfirm')?.addEventListener('click', async () => {
|
|
1370
|
+
console.log('[folderPicker] 点击确认, 当前选择:', fpCurrent);
|
|
1371
|
+
try {
|
|
1372
|
+
console.log('[folderPicker] 提交切换工作区...');
|
|
1373
|
+
const result = await api('/api/workspace', 'POST', { cwd: fpCurrent });
|
|
1374
|
+
console.log('[folderPicker] API 返回:', result);
|
|
1375
|
+
state.workspaceDir = fpCurrent;
|
|
1376
|
+
// 持久化到 localStorage
|
|
1377
|
+
localStorage.setItem('fhcode.workspaceDir', fpCurrent);
|
|
1378
|
+
renderWorkspaceBar();
|
|
1379
|
+
closeModal('folderPickerModal');
|
|
1380
|
+
toast('已切换工作区:' + fpCurrent);
|
|
1381
|
+
} catch (e) {
|
|
1382
|
+
console.error('[folderPicker] 切换失败:', e);
|
|
1383
|
+
toast('设置失败:' + e.message);
|
|
1384
|
+
}
|
|
1385
|
+
});
|
|
1386
|
+
|
|
1387
|
+
/* ========== 自动化、模板、市场、办公助理 ========== */
|
|
1388
|
+
document.getElementById('newAutoBtn')?.addEventListener('click', () => openModal('autoModal'));
|
|
1389
|
+
document.getElementById('autoSaveBtn')?.addEventListener('click', async () => {
|
|
1390
|
+
const name = document.getElementById('autoName').value.trim();
|
|
1391
|
+
const goal = document.getElementById('autoGoal').value.trim();
|
|
1392
|
+
if (!name || !goal) { toast('请填写名称与目标'); return; }
|
|
1393
|
+
try { await api('/api/automations', 'POST', { name, goal }); closeModal('autoModal');
|
|
1394
|
+
const n = document.getElementById('autoName');
|
|
1395
|
+
const g = document.getElementById('autoGoal');
|
|
1396
|
+
if (n) n.value = ''; if (g) g.value = '';
|
|
1397
|
+
await loadAutomations(); toast('已保存'); }
|
|
1398
|
+
catch (e) { toast('保存失败:' + e.message); }
|
|
1399
|
+
});
|
|
1400
|
+
|
|
1401
|
+
document.getElementById('newTplBtn')?.addEventListener('click', () => {
|
|
1402
|
+
const goal = document.getElementById('goalInput')?.value?.trim() || '';
|
|
1403
|
+
const inp = document.getElementById('tplGoal');
|
|
1404
|
+
if (inp) inp.value = goal;
|
|
1405
|
+
openModal('tplModal');
|
|
1406
|
+
});
|
|
1407
|
+
document.getElementById('tplSaveBtn')?.addEventListener('click', async () => {
|
|
1408
|
+
const title = document.getElementById('tplTitle').value.trim();
|
|
1409
|
+
const goal = document.getElementById('tplGoal').value.trim();
|
|
1410
|
+
const category = document.getElementById('tplCategory').value.trim();
|
|
1411
|
+
if (!title || !goal) { toast('请填写标题与内容'); return; }
|
|
1412
|
+
try { await api('/api/templates', 'POST', { title, goal, category }); closeModal('tplModal');
|
|
1413
|
+
const t = document.getElementById('tplTitle');
|
|
1414
|
+
const g = document.getElementById('tplGoal');
|
|
1415
|
+
const c = document.getElementById('tplCategory');
|
|
1416
|
+
if (t) t.value = ''; if (g) g.value = ''; if (c) c.value = '';
|
|
1417
|
+
await loadTemplates(); toast('已保存模板'); }
|
|
1418
|
+
catch (e) { toast('保存失败:' + e.message); }
|
|
1419
|
+
});
|
|
1420
|
+
|
|
1421
|
+
document.getElementById('marketRefresh')?.addEventListener('click', loadMarket);
|
|
1422
|
+
let marketTimer;
|
|
1423
|
+
document.getElementById('marketSearch')?.addEventListener('input', () => { clearTimeout(marketTimer); marketTimer = setTimeout(loadMarket, 400); });
|
|
1424
|
+
document.getElementById('marketSource')?.addEventListener('change', loadMarket);
|
|
1425
|
+
|
|
1426
|
+
|
|
1427
|
+
document.querySelectorAll('[data-close]').forEach((el) => el.addEventListener('click', () => closeModal(el.getAttribute('data-close'))));
|
|
1428
|
+
document.querySelectorAll('.mask').forEach((m) => m.addEventListener('click', (e) => { if (e.target === m) m.classList.remove('show'); }));
|
|
1429
|
+
// 文件夹选择器点击事件(只绑定一次,避免累积)
|
|
1430
|
+
document.getElementById('fpTree')?.addEventListener('click', async (e) => {
|
|
1431
|
+
const driveOption = e.target.closest('.drive-option');
|
|
1432
|
+
if (driveOption) {
|
|
1433
|
+
e.stopPropagation();
|
|
1434
|
+
const drive = driveOption.getAttribute('data-drive');
|
|
1435
|
+
console.log('[folderPicker] 选择驱动器:', drive);
|
|
1436
|
+
try { await loadFolderPicker(drive); } catch (err) { toast('读取失败:' + err.message); }
|
|
1437
|
+
return;
|
|
1438
|
+
}
|
|
1439
|
+
const item = e.target.closest('.file-item');
|
|
1440
|
+
if (!item) return;
|
|
1441
|
+
const type = item.getAttribute('data-type');
|
|
1442
|
+
const path = item.getAttribute('data-path');
|
|
1443
|
+
console.log('[folderPicker] 点击:', type, path);
|
|
1444
|
+
if (type === 'dir' || type === 'up') {
|
|
1445
|
+
try { await loadFolderPicker(path); } catch (e) { toast('读取失败:' + e.message); }
|
|
1446
|
+
} else if (type) {
|
|
1447
|
+
previewFile(path);
|
|
1448
|
+
}
|
|
1449
|
+
});
|
|
1450
|
+
function startRefresh() { setInterval(loadTasks, 5000); }
|
|
1451
|
+
|
|
1452
|
+
/* ========== 记忆系统 ========== */
|
|
1453
|
+
|
|
1454
|
+
|
|
1455
|
+
|
|
1456
|
+
|
|
1457
|
+
|
|
1458
|
+
|
|
1459
|
+
// 当切换到记忆页面时初始化
|
|
1460
|
+
const origSwitchView = switchView;
|
|
1461
|
+
window.switchView = function(nav) {
|
|
1462
|
+
origSwitchView(nav);
|
|
1463
|
+
if (nav === 'memory') {
|
|
1464
|
+
setTimeout(initMemoryUI, 100);
|
|
1465
|
+
}
|
|
1466
|
+
};
|
|
1467
|
+
|
|
1468
|
+
if (state.token && state.phone) {
|
|
1469
|
+
document.getElementById('loginOverlay').style.display = 'none';
|
|
1470
|
+
document.getElementById('appLayout').classList.add('show');
|
|
1471
|
+
afterLogin();
|
|
1472
|
+
}
|