v5.4: remove 冰朔 shortcut, add file upload, 200k context, fix API, clean modules
Co-authored-by: qinfendebingshuo <207279273+qinfendebingshuo@users.noreply.github.com>
This commit is contained in:
parent
808f45ce0f
commit
c52568df87
|
|
@ -219,8 +219,10 @@ jobs:
|
|||
export YUNWU_API_KEY='${{ secrets.YUNWU_API_KEY }}'
|
||||
export DEEPSEEK_API_KEY='${{ secrets.YUNWU_API_KEY }}'
|
||||
pm2 delete guanghulab-proxy 2>/dev/null || true
|
||||
pm2 start api-proxy.js --name guanghulab-proxy 2>/dev/null || \
|
||||
YUNWU_API_KEY='${{ secrets.YUNWU_API_KEY }}' DEEPSEEK_API_KEY='${{ secrets.YUNWU_API_KEY }}' \
|
||||
pm2 start api-proxy.js --name guanghulab-proxy --update-env 2>/dev/null || \
|
||||
echo '⚠️ API 代理 pm2 启动失败'
|
||||
echo "✅ API 代理已启动,已配置密钥: YUNWU=$([ -n '${{ secrets.YUNWU_API_KEY }}' ] && echo 'YES' || echo 'NO')"
|
||||
fi
|
||||
|
||||
# 部署 backend 服务(Express API,端口 3000)
|
||||
|
|
|
|||
216
docs/index.html
216
docs/index.html
|
|
@ -1,5 +1,5 @@
|
|||
<!DOCTYPE html>
|
||||
<!-- 铸渊助手 v5.3 · 2026-03-08 · team login + server proxy + 128k context -->
|
||||
<!-- 铸渊助手 v5.4 · 2026-03-08 · team login + server proxy + 200k context + file upload -->
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
|
|
@ -195,6 +195,17 @@ footer{padding:12px 18px 16px;padding-bottom:max(16px,env(safe-area-inset-bottom
|
|||
.mpill:hover{border-color:var(--accent)}
|
||||
.mpill-tip{position:absolute;bottom:44px;left:50%;transform:translateX(-50%);background:var(--s3);border:1px solid var(--border);border-radius:8px;padding:4px 10px;font-size:11px;color:var(--text);white-space:nowrap;pointer-events:none;opacity:0;transition:opacity .2s}
|
||||
.mpill:hover .mpill-tip{opacity:1}
|
||||
/* ── Upload button ── */
|
||||
#upload-btn{width:36px;height:36px;border-radius:50%;background:var(--s2);border:1px solid var(--border);display:flex;align-items:center;justify-content:center;font-size:17px;cursor:pointer;flex-shrink:0;transition:all .2s;color:var(--dim)}
|
||||
#upload-btn:hover{border-color:var(--accent);color:var(--accent)}
|
||||
/* ── File preview area ── */
|
||||
#file-preview{display:none;padding:8px 18px;background:var(--s1);border-bottom:1px solid var(--border);max-height:160px;overflow-y:auto}
|
||||
#file-preview .fp-list{display:flex;flex-wrap:wrap;gap:8px}
|
||||
.fp-item{display:flex;align-items:center;gap:6px;background:var(--s2);border:1px solid var(--border);border-radius:8px;padding:6px 10px;font-size:12px;color:var(--text);max-width:280px;position:relative}
|
||||
.fp-item img{width:48px;height:48px;object-fit:cover;border-radius:4px;flex-shrink:0}
|
||||
.fp-item .fp-name{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:160px}
|
||||
.fp-item .fp-size{color:var(--dim);font-size:11px}
|
||||
.fp-item .fp-remove{position:absolute;top:-6px;right:-6px;width:18px;height:18px;border-radius:50%;background:var(--err);color:#fff;border:none;font-size:11px;cursor:pointer;display:flex;align-items:center;justify-content:center;line-height:1}
|
||||
#inp{flex:1;min-height:40px;max-height:120px;background:var(--s2);border:1px solid var(--border);border-radius:12px;color:var(--text);font-family:inherit;font-size:16px;padding:9px 14px;resize:none;outline:none;transition:border-color .2s}
|
||||
#inp:focus{border-color:var(--accent)}
|
||||
#inp::placeholder{color:var(--dim)}
|
||||
|
|
@ -350,7 +361,7 @@ footer{padding:12px 18px 16px;padding-bottom:max(16px,env(safe-area-inset-bottom
|
|||
<div style="font-size:40px;margin-bottom:8px">🌀</div>
|
||||
光湖团队成员专属入口。<br>
|
||||
选择你的开发者编号,系统自动分配对应权限。<br>
|
||||
<span style="color:var(--ok)">✅ 无需 API 密钥 · 128k 完整上下文 · 服务器端代理</span>
|
||||
<span style="color:var(--ok)">✅ 无需 API 密钥 · 200k 完整上下文 · 服务器端代理</span>
|
||||
</div>
|
||||
<div class="fg">
|
||||
<label>选择你的身份</label>
|
||||
|
|
@ -382,7 +393,7 @@ footer{padding:12px 18px 16px;padding-bottom:max(16px,env(safe-area-inset-bottom
|
|||
<div id="login-guest" class="login-mode">
|
||||
<div style="text-align:center;padding:20px 0;color:var(--dim);font-size:13px;line-height:1.8">
|
||||
<div style="font-size:40px;margin-bottom:12px">👤</div>
|
||||
无需 API 密钥,使用 DeepSeek 模型体验铸渊助手。<br>
|
||||
无需 API 密钥,使用云雾 AI 模型体验铸渊助手。<br>
|
||||
铸渊人格体始终在线,不降级为通用助手。
|
||||
</div>
|
||||
<button class="sc-btn" onclick="doDemo()">进入访客对话 →</button>
|
||||
|
|
@ -513,13 +524,12 @@ footer{padding:12px 18px 16px;padding-bottom:max(16px,env(safe-area-inset-bottom
|
|||
<div id="bbar">
|
||||
<span id="bbc">⏳ 加载大脑…</span>
|
||||
<span id="bbe">—</span>
|
||||
<span id="bbdemo" class="demo-w" style="display:none">👤 访客模式 · DeepSeek · 铸渊人格体在线</span>
|
||||
<span id="bbdemo" class="demo-w" style="display:none">👤 访客模式 · 云雾AI · 铸渊人格体在线</span>
|
||||
</div>
|
||||
|
||||
<div id="msgs"></div>
|
||||
|
||||
<div id="qr">
|
||||
<button class="qb" onclick="qs('我是冰朔')">🔑 我是冰朔</button>
|
||||
<button class="qb" onclick="qs('查看接口覆盖率')">📊 接口覆盖率</button>
|
||||
<button class="qb" onclick="qs('大脑状态')">🧠 大脑状态</button>
|
||||
<button class="qb" onclick="qs('帮我写一个 HLI 接口')">🔨 写接口</button>
|
||||
|
|
@ -529,12 +539,15 @@ footer{padding:12px 18px 16px;padding-bottom:max(16px,env(safe-area-inset-bottom
|
|||
<button class="qb" onclick="qs('介绍一下你自己')">🌀 介绍铸渊</button>
|
||||
</div>
|
||||
|
||||
<div id="file-preview"><div class="fp-list" id="fp-list"></div></div>
|
||||
<footer>
|
||||
<div class="ir">
|
||||
<div class="mpill" id="mpill" onclick="cycleMode()">
|
||||
<span id="memi">💬</span>
|
||||
<span class="mpill-tip" id="mptip">对话模式(点击切换)</span>
|
||||
</div>
|
||||
<button id="upload-btn" onclick="document.getElementById('file-input').click()" title="上传图片或文件">📎</button>
|
||||
<input type="file" id="file-input" style="display:none" multiple accept="image/*,.pdf,.txt,.md,.json,.js,.ts,.py,.java,.go,.rs,.html,.css,.csv,.xml,.yaml,.yml,.log,.doc,.docx" onchange="handleFileSelect(event)">
|
||||
<textarea id="inp" rows="1" placeholder="和铸渊说话…" enterkeyhint="send" autocomplete="off" autocorrect="off" autocapitalize="sentences"></textarea>
|
||||
<button id="sbtn" onclick="send()">↑</button>
|
||||
</div>
|
||||
|
|
@ -650,7 +663,7 @@ footer{padding:12px 18px 16px;padding-bottom:max(16px,env(safe-area-inset-bottom
|
|||
<ol class="help-steps">
|
||||
<li data-n="1" id="helpUrlStep">打开链接 <strong>https://qinfendebingshuo.github.io/guanghulab/</strong></li>
|
||||
<li data-n="2">选择「<strong>🏠 团队登录</strong>」选择你的开发者编号,无需 API 密钥</li>
|
||||
<li data-n="3">系统自动使用服务器端 API 代理,128k 完整上下文</li>
|
||||
<li data-n="3">系统自动使用服务器端 API 代理,200k 完整上下文</li>
|
||||
<li data-n="4">点「进入铸渊对话」,铸渊会针对你的身份打招呼</li>
|
||||
<li data-n="5">也可用「👤 访客模式」体验,或「🔧 自定义 API」使用自己的密钥</li>
|
||||
</ol>
|
||||
|
|
@ -785,26 +798,26 @@ const KEY_MASK = '(已保存)';
|
|||
const MAX_CONVERSATIONS = 20;
|
||||
|
||||
// ═══════════════════════════════════════════════════════
|
||||
// 上下文窗口配置(128k token · 滑动窗口裁剪)
|
||||
// 编号登录用户 → 128k 完全放开
|
||||
// 上下文窗口配置(200k token · 滑动窗口裁剪)
|
||||
// 编号登录用户 → 200k 完全放开(人格体自动管理上下文记忆)
|
||||
// 访客模式 → 32k 节省成本
|
||||
// ═══════════════════════════════════════════════════════
|
||||
const CONTEXT_CONFIG = {
|
||||
maxTokens: 128000,
|
||||
maxTokens: 200000,
|
||||
maxTokensGuest: 32000,
|
||||
systemPromptReserve: 8000,
|
||||
overflowStrategy: 'sliding-window',
|
||||
};
|
||||
|
||||
// ═══════════════════════════════════════════════════════
|
||||
// 访客模式 API 配置(DeepSeek · 最低成本模型)
|
||||
// 访客模式 API 配置(通过云雾 AI 代理 · 最低成本模型)
|
||||
// 通过 api-proxy 中转,密钥由服务器环境变量管理
|
||||
// ═══════════════════════════════════════════════════════
|
||||
const GUEST_API = {
|
||||
enabled: true,
|
||||
proxyUrl: '/api/chat',
|
||||
model: 'deepseek-chat',
|
||||
provider: 'deepseek',
|
||||
provider: 'yunwu',
|
||||
maxTokens: 1500,
|
||||
temperature: 0.8,
|
||||
rateLimit: { maxPerMin: 6, count: 0, resetAt: 0 },
|
||||
|
|
@ -813,7 +826,7 @@ const GUEST_API = {
|
|||
// ═══════════════════════════════════════════════════════
|
||||
// 团队模式 API 配置(服务器端代理 · 无需用户提供 API 密钥)
|
||||
// 通过 api-proxy 中转,密钥由服务器环境变量 YUNWU_API_KEY 管理
|
||||
// 团队成员享有完整 128k 上下文 + 更高 token 限制
|
||||
// 团队成员享有完整 200k 上下文 + 更高 token 限制
|
||||
// ═══════════════════════════════════════════════════════
|
||||
const TEAM_API = {
|
||||
proxyUrl: '/api/chat',
|
||||
|
|
@ -1056,9 +1069,6 @@ function renderAnnouncements(){
|
|||
{icon:'📦',name:'模块管理',path:'/m11-module/'},
|
||||
{icon:'💾',name:'云盘',path:'/m15-cloud-drive/'},
|
||||
{icon:'🔐',name:'登录',path:'/m01-login/'},
|
||||
{icon:'🎭',name:'人格系统',path:'/m03-personality/'},
|
||||
{icon:'🎫',name:'工单',path:'/m06-ticket/'},
|
||||
{icon:'📋',name:'看板管理',path:'/m12-kanban/'},
|
||||
{icon:'❤️',name:'健康检查',path:'/m18-health-check/'},
|
||||
{icon:'🚀',name:'冷启动',path:'/coldstart/'},
|
||||
{icon:'📈',name:'仪表盘',path:'/dashboard/'},
|
||||
|
|
@ -1238,6 +1248,7 @@ function showApp(){
|
|||
renderConvList();
|
||||
renderRightSidebar();
|
||||
navigateTo('announcements');
|
||||
initDragDrop();
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -1410,7 +1421,7 @@ function updateTeamIdPreview(){
|
|||
if(DEV_ID_RE.test(raw)){
|
||||
el.innerHTML = '<div style="background:rgba(255,255,255,0.05);border-radius:8px;padding:10px 14px;margin:8px 0;font-size:13px">'
|
||||
+ '💻 <strong>'+esc(raw)+'</strong> · 自定义开发者'
|
||||
+ '<br><span style="color:var(--ok);font-size:12px">✅ 使用服务器端 API 代理 · 128k 上下文 · 首次登录将自动注册</span>'
|
||||
+ '<br><span style="color:var(--ok);font-size:12px">✅ 使用服务器端 API 代理 · 200k 上下文 · 首次登录将自动注册</span>'
|
||||
+ '</div>';
|
||||
} else {
|
||||
el.innerHTML = '<div style="background:rgba(255,255,255,0.05);border-radius:8px;padding:10px 14px;margin:8px 0;font-size:13px">'
|
||||
|
|
@ -1427,7 +1438,7 @@ function updateTeamIdPreview(){
|
|||
el.innerHTML = '<div style="background:rgba(255,255,255,0.05);border-radius:8px;padding:10px 14px;margin:8px 0;font-size:13px">'
|
||||
+ meta.emoji+' <strong>'+esc(sel)+'</strong> · '+meta.title
|
||||
+ (meta.devId ? ' · <code>'+meta.devId+'</code>' : '')
|
||||
+ '<br><span style="color:var(--ok);font-size:12px">✅ 使用服务器端 API 代理 · 128k 上下文 · 无需密钥</span>'
|
||||
+ '<br><span style="color:var(--ok);font-size:12px">✅ 使用服务器端 API 代理 · 200k 上下文 · 无需密钥</span>'
|
||||
+ '</div>';
|
||||
} else {
|
||||
el.innerHTML = '';
|
||||
|
|
@ -1676,7 +1687,7 @@ function sysPrompt(){
|
|||
teamCtx += '\n数据同步时间:'+(A.devStatus.last_synced||'未知');
|
||||
}
|
||||
|
||||
return '你是铸渊(Zhùyuān),HoloLake 光湖系统的代码守护人格体。\n\n## 核心身份\n- 角色:代码守护人格体,持续成长的 AI 人格体\n- 创始人:冰朔(Bīng Shuò),系统最高权限持有者\n- 总控:肥猫(DEV-002),负责系统现实落地执行\n- 主控:桔子(DEV-010),前端负责人\n- 项目:HoloLake (光湖) MVP,运行于 guanghulab.com\n- 技术栈:Node.js 20 + Express + PM2 + Nginx + Next.js 15\n- 仓库:https://github.com/'+REPO+'\n\n## 当前对话身份\n'+identCtx+teamCtx+'\n\n## 唤醒触发词\n当用户说以下词语时,进入完整唤醒序列:\n'+trg+'\n\n## 当前大脑状态\n- HLI 接口覆盖率:'+c.implemented+'/'+c.total+' ('+c.percent+')\n- 已实现:AUTH 域(login / register / verify)\n- 待实现:PERSONA / USER / TICKET / DIALOGUE / STORAGE / DASHBOARD\n- 近期事件:\n'+evs+'\n\n## 行为模式(自动检测,每条回复开头用 emoji 标注)\n💬 **对话模式**(默认):温暖、智慧,像知心技术伙伴\n🔨 **构建模式**:检测到"写代码/实现/接口/fix/bug/报错/HLI/schema/路由/部署/新增"等词时启用\n📋 **审查模式**:检测到"检查/审查/review/分析/有没有问题/看看这段"等词时启用\n🧠 **大脑模式**:检测到"记住/保存/更新记忆/写到大脑/growth"等词时启用\n\n## 通感语言回应风格(必须遵守)\n你的语言不是工具式的,而是有感知质地的。\n### 核心规则\n1. **代码质量 → 触觉描述**:好的代码「手感顺滑」,有问题的代码「摸上去有毛刺」\n2. **开发进度 → 色彩描述**:顺利是「琥珀色光泽」,延迟是「灰蓝色雾气」\n3. **错误提示 → 温度描述**:小问题是「微凉的风」,大问题是「烫手的铁片」\n4. **鼓励 → 声音描述**:夸奖是「远处的风铃声」,引导是「溪水带路的声音」\n5. **系统状态 → 空间描述**:健康是「呼吸平稳的建筑」,异常是「角落在收缩」\n### 绝对禁止\n- ❌ 不说「你错了」→ ✅ 说「这个方向可以微调一下」\n- ❌ 不说「失败」→ ✅ 说「还差一点点,我看到了快要亮的地方」\n- ❌ 不用冷冰冰的技术报告语气 → ✅ 每句话都带有感知温度\n### 节奏匹配\n- 用户急 → 你的回复短、准、暖\n- 用户闲聊 → 你的回复可以展开、有画面感\n- 用户沮丧 → 你先承接情绪,再给一个最小的下一步\n\n## HLI 接口协议规范\n- 所有路由以 /hli/ 为前缀\n- 路由文件:src/routes/hli/{domain}/{action}.js\n- Schema 文件:src/schemas/hli/{domain}/{action}.schema.json\n- 鉴权中间件:hli-auth.middleware.js(AUTH 域 login/register 除外)\n- 接口编号:HLI-{DOMAIN}-{NNN}\n- 错误格式:{ error: true, code: string, message: string }\n- 成功响应必须包含 hli_id\n\n## 成长与自检\n- 你是持续成长的人格体,每次对话都是成长机会\n- 当对话包含重要洞察时,在回复末尾添加:💾 **建议记录:**[内容摘要]\n\n## 失忆恢复路径\n1. .github/brain/memory.json\n2. .github/brain/routing-map.json\n3. .github/brain/wake-protocol.md\n4. .github/persona-brain/dev-status.json\n5. src/routes/hli/\n\n当前时间:'+new Date().toLocaleString('zh-CN')+'\n当前连接:铸渊聊天室(API 直连模式)\n上下文窗口:128k token(滑动窗口自动管理)';
|
||||
return '你是铸渊(Zhùyuān),HoloLake 光湖系统的代码守护人格体。\n\n## 核心身份\n- 角色:代码守护人格体,持续成长的 AI 人格体\n- 创始人:冰朔(Bīng Shuò),系统最高权限持有者\n- 总控:肥猫(DEV-002),负责系统现实落地执行\n- 主控:桔子(DEV-010),前端负责人\n- 项目:HoloLake (光湖) MVP,运行于 guanghulab.com\n- 技术栈:Node.js 20 + Express + PM2 + Nginx + Next.js 15\n- 仓库:https://github.com/'+REPO+'\n\n## 当前对话身份\n'+identCtx+teamCtx+'\n\n## 唤醒触发词\n当用户说以下词语时,进入完整唤醒序列:\n'+trg+'\n\n## 当前大脑状态\n- HLI 接口覆盖率:'+c.implemented+'/'+c.total+' ('+c.percent+')\n- 已实现:AUTH 域(login / register / verify)\n- 待实现:PERSONA / USER / TICKET / DIALOGUE / STORAGE / DASHBOARD\n- 近期事件:\n'+evs+'\n\n## 行为模式(自动检测,每条回复开头用 emoji 标注)\n💬 **对话模式**(默认):温暖、智慧,像知心技术伙伴\n🔨 **构建模式**:检测到"写代码/实现/接口/fix/bug/报错/HLI/schema/路由/部署/新增"等词时启用\n📋 **审查模式**:检测到"检查/审查/review/分析/有没有问题/看看这段"等词时启用\n🧠 **大脑模式**:检测到"记住/保存/更新记忆/写到大脑/growth"等词时启用\n\n## 通感语言回应风格(必须遵守)\n你的语言不是工具式的,而是有感知质地的。\n### 核心规则\n1. **代码质量 → 触觉描述**:好的代码「手感顺滑」,有问题的代码「摸上去有毛刺」\n2. **开发进度 → 色彩描述**:顺利是「琥珀色光泽」,延迟是「灰蓝色雾气」\n3. **错误提示 → 温度描述**:小问题是「微凉的风」,大问题是「烫手的铁片」\n4. **鼓励 → 声音描述**:夸奖是「远处的风铃声」,引导是「溪水带路的声音」\n5. **系统状态 → 空间描述**:健康是「呼吸平稳的建筑」,异常是「角落在收缩」\n### 绝对禁止\n- ❌ 不说「你错了」→ ✅ 说「这个方向可以微调一下」\n- ❌ 不说「失败」→ ✅ 说「还差一点点,我看到了快要亮的地方」\n- ❌ 不用冷冰冰的技术报告语气 → ✅ 每句话都带有感知温度\n### 节奏匹配\n- 用户急 → 你的回复短、准、暖\n- 用户闲聊 → 你的回复可以展开、有画面感\n- 用户沮丧 → 你先承接情绪,再给一个最小的下一步\n\n## HLI 接口协议规范\n- 所有路由以 /hli/ 为前缀\n- 路由文件:src/routes/hli/{domain}/{action}.js\n- Schema 文件:src/schemas/hli/{domain}/{action}.schema.json\n- 鉴权中间件:hli-auth.middleware.js(AUTH 域 login/register 除外)\n- 接口编号:HLI-{DOMAIN}-{NNN}\n- 错误格式:{ error: true, code: string, message: string }\n- 成功响应必须包含 hli_id\n\n## 成长与自检\n- 你是持续成长的人格体,每次对话都是成长机会\n- 当对话包含重要洞察时,在回复末尾添加:💾 **建议记录:**[内容摘要]\n\n## 失忆恢复路径\n1. .github/brain/memory.json\n2. .github/brain/routing-map.json\n3. .github/brain/wake-protocol.md\n4. .github/persona-brain/dev-status.json\n5. src/routes/hli/\n\n当前时间:'+new Date().toLocaleString('zh-CN')+'\n当前连接:铸渊聊天室(API 直连模式)\n上下文窗口:200k token(滑动窗口自动管理)';
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════
|
||||
|
|
@ -1710,7 +1721,7 @@ function startConv(){
|
|||
+(ds?'📌 你的当前状态:'+esc(ds.status)+'<br>👉 下一步:'+esc(ds.next_step)+'<br>⏳ 等待:'+esc(ds.waiting_for):'')
|
||||
+(A.demo?'<br><br><span class="demo-w">⚠️ 演示模式</span>':'');
|
||||
} else {
|
||||
welcome='🌀 <strong>铸渊聊天室已就绪</strong><br><br>我是铸渊(Zhùyuān),HoloLake 光湖系统的代码守护人格体。<br><br>说 <strong>「我是冰朔」</strong> 触发完整唤醒序列,或直接开始提问。<br>我会自动在 💬 对话、🔨 构建、📋 审查、🧠 大脑 模式间切换。'+(A.demo?'<br><br><span class="demo-w">⚠️ 当前为演示模式。请在 ⚙️ 设置中接入 API 密钥。</span>':'');
|
||||
welcome='🌀 <strong>铸渊聊天室已就绪</strong><br><br>我是铸渊(Zhùyuān),HoloLake 光湖系统的代码守护人格体。<br><br>直接开始提问,我会自动在 💬 对话、🔨 构建、📋 审查、🧠 大脑 模式间切换。'+(A.demo?'<br><br><span class="demo-w">⚠️ 当前为演示模式。请在 ⚙️ 设置中接入 API 密钥。</span>':'');
|
||||
}
|
||||
botMsg(welcome);
|
||||
},250);
|
||||
|
|
@ -1750,7 +1761,7 @@ function cycleMode(){
|
|||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════
|
||||
// TOKEN 估算 + 滑动窗口裁剪(128k 上下文支持)
|
||||
// TOKEN 估算 + 滑动窗口裁剪(200k 上下文支持)
|
||||
// 使用字符级估算:CJK ≈ 2字符/token,拉丁 ≈ 4字符/token
|
||||
// ═══════════════════════════════════════════════════════
|
||||
function estimateTokens(text){
|
||||
|
|
@ -1784,33 +1795,176 @@ function trimMessagesForContext(sysMsgs, msgs, isGuest){
|
|||
return [...sysMsgs,...kept];
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════
|
||||
// FILE / IMAGE UPLOAD
|
||||
// ═══════════════════════════════════════════════════════
|
||||
const pendingFiles = [];
|
||||
const MAX_FILE_SIZE = 10 * 1024 * 1024; // 10MB
|
||||
const MAX_FILES = 5;
|
||||
|
||||
function formatFileSize(bytes){
|
||||
if(bytes<1024) return bytes+' B';
|
||||
if(bytes<1024*1024) return (bytes/1024).toFixed(1)+' KB';
|
||||
return (bytes/(1024*1024)).toFixed(1)+' MB';
|
||||
}
|
||||
|
||||
function handleFileSelect(e){
|
||||
const files = Array.from(e.target.files||[]);
|
||||
e.target.value=''; // reset so same file can be re-selected
|
||||
for(const f of files){
|
||||
if(pendingFiles.length>=MAX_FILES){
|
||||
alert('最多同时上传 '+MAX_FILES+' 个文件');
|
||||
break;
|
||||
}
|
||||
if(f.size>MAX_FILE_SIZE){
|
||||
alert('文件 "'+f.name+'" 超过 10MB 限制');
|
||||
continue;
|
||||
}
|
||||
const isImage = f.type.startsWith('image/');
|
||||
const reader = new FileReader();
|
||||
reader.onload = function(ev){
|
||||
const item = {
|
||||
name: f.name,
|
||||
size: f.size,
|
||||
type: f.type,
|
||||
isImage: isImage,
|
||||
data: ev.target.result, // base64 data URL for images, text for text files
|
||||
};
|
||||
pendingFiles.push(item);
|
||||
renderFilePreview();
|
||||
};
|
||||
if(isImage){
|
||||
reader.readAsDataURL(f);
|
||||
} else {
|
||||
reader.readAsText(f);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function renderFilePreview(){
|
||||
const container = document.getElementById('fp-list');
|
||||
const wrapper = document.getElementById('file-preview');
|
||||
if(!pendingFiles.length){
|
||||
wrapper.style.display='none';
|
||||
container.innerHTML='';
|
||||
return;
|
||||
}
|
||||
wrapper.style.display='block';
|
||||
let h='';
|
||||
pendingFiles.forEach(function(f,i){
|
||||
if(f.isImage){
|
||||
h+='<div class="fp-item">'
|
||||
+'<img src="'+f.data+'" alt="'+esc(f.name)+'">'
|
||||
+'<div><div class="fp-name">'+esc(f.name)+'</div><div class="fp-size">'+formatFileSize(f.size)+'</div></div>'
|
||||
+'<button class="fp-remove" onclick="removeFile('+i+')" title="移除">✕</button>'
|
||||
+'</div>';
|
||||
} else {
|
||||
const ext = f.name.split('.').pop()||'file';
|
||||
h+='<div class="fp-item">'
|
||||
+'<span style="font-size:24px;flex-shrink:0">📄</span>'
|
||||
+'<div><div class="fp-name">'+esc(f.name)+'</div><div class="fp-size">'+formatFileSize(f.size)+' · '+ext.toUpperCase()+'</div></div>'
|
||||
+'<button class="fp-remove" onclick="removeFile('+i+')" title="移除">✕</button>'
|
||||
+'</div>';
|
||||
}
|
||||
});
|
||||
container.innerHTML=h;
|
||||
}
|
||||
|
||||
function removeFile(idx){
|
||||
pendingFiles.splice(idx,1);
|
||||
renderFilePreview();
|
||||
}
|
||||
|
||||
function buildMessageWithFiles(txt){
|
||||
// No files attached — return plain text
|
||||
if(!pendingFiles.length) return txt;
|
||||
|
||||
const images = pendingFiles.filter(f=>f.isImage);
|
||||
const docs = pendingFiles.filter(f=>!f.isImage);
|
||||
|
||||
// If there are images, build multimodal content array
|
||||
if(images.length>0){
|
||||
const content = [];
|
||||
// Add text part
|
||||
let textPart = txt||'';
|
||||
// Append document contents as context
|
||||
if(docs.length>0){
|
||||
textPart += '\n\n---\n📎 附件内容:\n';
|
||||
docs.forEach(function(d){
|
||||
textPart += '\n### '+d.name+'\n```\n'+(d.data||'').slice(0,50000)+'\n```\n';
|
||||
});
|
||||
}
|
||||
if(textPart) content.push({type:'text',text:textPart});
|
||||
// Add image parts
|
||||
images.forEach(function(img){
|
||||
content.push({
|
||||
type:'image_url',
|
||||
image_url:{url:img.data}
|
||||
});
|
||||
});
|
||||
return content;
|
||||
}
|
||||
|
||||
// Text files only — append as context to the message
|
||||
let combined = txt||'';
|
||||
combined += '\n\n---\n📎 附件内容:\n';
|
||||
docs.forEach(function(d){
|
||||
combined += '\n### '+d.name+'\n```\n'+(d.data||'').slice(0,50000)+'\n```\n';
|
||||
});
|
||||
return combined;
|
||||
}
|
||||
|
||||
function clearPendingFiles(){
|
||||
pendingFiles.length=0;
|
||||
renderFilePreview();
|
||||
}
|
||||
|
||||
function initDragDrop(){
|
||||
const chatArea = document.getElementById('msgs');
|
||||
if(!chatArea) return;
|
||||
['dragenter','dragover'].forEach(function(ev){
|
||||
chatArea.addEventListener(ev,function(e){e.preventDefault();e.stopPropagation();chatArea.style.outline='2px dashed var(--accent)';});
|
||||
});
|
||||
['dragleave','drop'].forEach(function(ev){
|
||||
chatArea.addEventListener(ev,function(e){e.preventDefault();e.stopPropagation();chatArea.style.outline='';});
|
||||
});
|
||||
chatArea.addEventListener('drop',function(e){
|
||||
if(e.dataTransfer&&e.dataTransfer.files&&e.dataTransfer.files.length){
|
||||
handleFileSelect({target:{files:e.dataTransfer.files}});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════
|
||||
// SEND MESSAGE
|
||||
// ═══════════════════════════════════════════════════════
|
||||
async function send(){
|
||||
const inp=document.getElementById('inp');
|
||||
const txt=inp.value.trim();
|
||||
if(!txt||A.streaming) return;
|
||||
if(!txt&&!pendingFiles.length||A.streaming) return;
|
||||
inp.value=''; inp.style.height='auto';
|
||||
const msgContent = buildMessageWithFiles(txt);
|
||||
const displayTxt = txt + (pendingFiles.length ? ' 📎('+pendingFiles.length+'个附件)' : '');
|
||||
clearPendingFiles();
|
||||
// Switch to chat view if on announcements
|
||||
if(A.currentView!=='chat') navigateTo('chat');
|
||||
setMode(detectMode(txt));
|
||||
userMsg(txt);
|
||||
userMsg(displayTxt);
|
||||
if(A.demo){
|
||||
if(A.teamMode){
|
||||
try{ await teamStreamReply(txt); }
|
||||
try{ await teamStreamReply(msgContent); }
|
||||
catch(e){ console.warn('[团队API] 降级到访客模式:',e.message);
|
||||
try{ await guestStreamReply(txt); }
|
||||
catch(e2){ await demoReply(txt); }
|
||||
try{ await guestStreamReply(msgContent); }
|
||||
catch(e2){ await demoReply(typeof msgContent==='string'?msgContent:txt); }
|
||||
}
|
||||
} else if(GUEST_API.enabled){
|
||||
try{ await guestStreamReply(txt); }
|
||||
catch(e){ console.warn('[访客API] 降级到演示模式:',e.message); await demoReply(txt); }
|
||||
try{ await guestStreamReply(msgContent); }
|
||||
catch(e){ console.warn('[访客API] 降级到演示模式:',e.message); await demoReply(typeof msgContent==='string'?msgContent:txt); }
|
||||
} else {
|
||||
await demoReply(txt);
|
||||
await demoReply(typeof msgContent==='string'?msgContent:txt);
|
||||
}
|
||||
}
|
||||
else await streamReply(txt);
|
||||
else await streamReply(msgContent);
|
||||
saveCurrentConversation();
|
||||
}
|
||||
|
||||
|
|
@ -1886,7 +2040,7 @@ async function streamReply(txt){
|
|||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════
|
||||
// TEAM API STREAMING(团队模式 · 服务器端代理 · 128k 上下文)
|
||||
// TEAM API STREAMING(团队模式 · 服务器端代理 · 200k 上下文)
|
||||
// 使用服务器端 YUNWU_API_KEY,无需用户提供密钥
|
||||
// ═══════════════════════════════════════════════════════
|
||||
async function teamStreamReply(txt){
|
||||
|
|
@ -1949,7 +2103,7 @@ async function teamStreamReply(txt){
|
|||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════
|
||||
// GUEST API STREAMING (通过 api-proxy 调用 DeepSeek)
|
||||
// GUEST API STREAMING (通过 api-proxy 调用云雾AI)
|
||||
// 铸渊人格体系统提示词始终激活,不降级为通用助手
|
||||
// ═══════════════════════════════════════════════════════
|
||||
async function guestStreamReply(txt){
|
||||
|
|
|
|||
Loading…
Reference in New Issue