模块介绍 / 今日规划 / 下班日报 / 团队摘要 生成成功后没有上报,也几乎不写「AI」日志。

流结束时的 usage 用了非阻塞发送,通道一满就被丢掉;很多服务商默认还不在流里带 usage。
现在每次真正打到模型的调用都会:

在本地日志(分类 AI,消息 AI 调用完成)写下 scene / prompt / completion / cached
登录后同步上报到后台日汇总
This commit is contained in:
李琦
2026-08-17 14:00:47 +08:00
parent 83e69317d4
commit e71ce451ff
23 changed files with 1363 additions and 93 deletions

View File

@@ -2,7 +2,7 @@
import { computed, onMounted, onUnmounted, ref, watch } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { useI18n } from 'vue-i18n'
import { LayoutDashboard, ScrollText, Settings, X, Database, SlidersHorizontal, Home, ListTodo, TicketCheck, CalendarDays, CalendarCheck2, Sparkles, UserRound, ClipboardList, Wrench, Rocket, ChevronDown, Bell, StickyNote, ListFilter, Palette, Bot, Users, NotebookPen, CloudUpload, Power, Image, Activity, Package, Shield } from 'lucide-vue-next'
import { LayoutDashboard, ScrollText, Settings, X, Database, SlidersHorizontal, Home, ListTodo, TicketCheck, CalendarDays, CalendarCheck2, Sparkles, UserRound, ClipboardList, Wrench, Rocket, ChevronDown, Bell, StickyNote, ListFilter, Palette, Bot, Users, NotebookPen, CloudUpload, Power, Image, Activity, Package, Shield, Plus, Pin, PinOff } from 'lucide-vue-next'
import { useAppStore } from './store'
import DatabaseSetup from './components/DatabaseSetup.vue'
import BrowserBlocked from './components/BrowserBlocked.vue'
@@ -107,14 +107,74 @@ const navGroups = [
{ to: '/settings?tab=database', icon: Database, label: 'tabDatabase', match: r => r.path === '/settings' && settingsTab(r) === 'database' }
] }
]
// ---- 概览快捷入口:把其它分区菜单钉到「概览」二级导航 ----
const SHORTCUT_KEY = 'cc-nav-shortcuts'
const loadShortcuts = () => {
try {
const raw = JSON.parse(localStorage.getItem(SHORTCUT_KEY) || '[]')
return Array.isArray(raw) ? raw.filter(x => typeof x === 'string') : []
} catch { return [] }
}
const navShortcuts = ref(loadShortcuts())
const shortcutPicker = ref(false)
function saveShortcuts() {
localStorage.setItem(SHORTCUT_KEY, JSON.stringify(navShortcuts.value))
}
const catalogByTo = computed(() => {
const map = new Map()
for (const g of navGroups) {
for (const it of g.items) map.set(it.to, { ...it, groupId: g.id, groupLabel: g.label })
}
return map
})
// 可钉到概览的项:非概览内置、且当前用户可见
const pinnableItems = computed(() => {
const overviewTos = new Set(navGroups[0].items.map(it => it.to))
const out = []
for (const g of navGroups) {
if (g.id === 'overview') continue
for (const it of g.items) {
if (overviewTos.has(it.to)) continue
if (it.adminOnly && store.syncStatus.userId !== 1) continue
out.push({ ...it, groupId: g.id, groupLabel: g.label })
}
}
return out
})
function addShortcut(to) {
if (navShortcuts.value.includes(to)) return
navShortcuts.value = [...navShortcuts.value, to]
saveShortcuts()
shortcutPicker.value = false
}
function removeShortcut(to) {
navShortcuts.value = navShortcuts.value.filter(x => x !== to)
saveShortcuts()
}
function toggleShortcut(to) {
if (navShortcuts.value.includes(to)) removeShortcut(to)
else addShortcut(to)
}
// adminOnly 项只对云端管理员id=1展示
const visibleItems = g => g.items.filter(it => !it.adminOnly || store.syncStatus.userId === 1)
const visibleItems = g => {
let items = g.items.filter(it => !it.adminOnly || store.syncStatus.userId === 1)
if (g.id === 'overview') {
const extras = navShortcuts.value
.map(to => catalogByTo.value.get(to))
.filter(it => it && (!it.adminOnly || store.syncStatus.userId === 1))
.map(it => ({ ...it, shortcut: true }))
items = [...items, ...extras]
}
return items
}
const itemActive = it => it.match ? it.match(route) : route.path === it.to.split('?')[0]
// 导航徽标badge 显示数字dot 只显示小圆点;一级 rail 在组内任一非零时亮点
const badgeVal = it => it.badge ? (store.badges[it.badge] || 0) : 0
const badgeText = it => { const n = badgeVal(it); return n > 99 ? '99+' : String(n) }
const dotVal = it => it.dot ? !!store.badges[it.dot] : false
const groupDot = g => g.items.some(it => badgeVal(it) > 0 || dotVal(it))
const groupDot = g => visibleItems(g).some(it => badgeVal(it) > 0 || dotVal(it))
const childActive = c => route.path === '/settings' && String(route.query.tab || '') === c.tab
const groupOfRoute = () => navGroups.find(g => g.items.some(it => itemActive(it)))?.id
const activeGroupId = ref(groupOfRoute() || 'overview')
@@ -126,6 +186,7 @@ watch(() => route.fullPath, () => {
const g = groupOfRoute()
if (g) activeGroupId.value = g
railFlyout.value = null
if (g !== 'overview') shortcutPicker.value = false
for (const grp of navGroups) {
for (const it of grp.items) if (it.children && itemActive(it)) expandedParents.value[it.label] = true
}
@@ -263,16 +324,29 @@ watch(activeTask, task => {
<div class="sub-title">{{ t(activeGroup.label) }}</div>
<nav class="sub-nav" :aria-label="t(activeGroup.label)">
<template v-for="it in visibleItems(activeGroup)" :key="it.to">
<RouterLink :to="it.to" :class="{ active: itemActive(it) }">
<RouterLink :to="it.to" :class="{ active: itemActive(it), shortcut: it.shortcut }">
<component :is="it.icon" /><span>{{ t(it.label) }}</span>
<em v-if="badgeVal(it)" class="nav-badge">{{ badgeText(it) }}</em>
<i v-else-if="dotVal(it)" class="nav-dot" aria-hidden="true" />
<button
v-if="it.shortcut"
type="button"
class="nav-unpin"
:title="t('navUnpinShortcut')"
@click.prevent.stop="removeShortcut(it.to)"
><PinOff /></button>
<button v-if="it.children" type="button" class="sub-caret" :class="{ open: expandedParents[it.label] }" :aria-label="t(it.label)" @click="toggleParent(it, $event)"><ChevronDown /></button>
</RouterLink>
<div v-if="it.children && expandedParents[it.label]" class="sub-children">
<RouterLink v-for="c in it.children" :key="c.to" :to="c.to" :class="{ active: childActive(c) }">{{ t(c.label) }}</RouterLink>
</div>
</template>
<button
v-if="activeGroup.id === 'overview'"
type="button"
class="nav-add-shortcut"
@click="shortcutPicker = !shortcutPicker"
><Plus />{{ t('navAddShortcut') }}</button>
</nav>
<div class="sidebar-bottom">
<span class="version"><i />v{{ appVersion }}</span>
@@ -348,6 +422,33 @@ watch(activeTask, task => {
</section>
</div>
</Teleport>
<Teleport to="body">
<div v-if="shortcutPicker" class="overlay" @click.self="shortcutPicker = false">
<section class="modal nav-shortcut-modal" @click.stop>
<header class="modal-head">
<h2><Pin />{{ t('navShortcutPicker') }}</h2>
<button type="button" class="nm-close" :title="t('close')" @click="shortcutPicker = false"><X /></button>
</header>
<p class="nav-shortcut-hint">{{ t('navShortcutHint') }}</p>
<div class="nav-shortcut-list">
<button
v-for="it in pinnableItems"
:key="it.to"
type="button"
class="nav-shortcut-item"
:class="{ on: navShortcuts.includes(it.to) }"
@click="toggleShortcut(it.to)"
>
<component :is="it.icon" class="nav-shortcut-ico" />
<span class="nav-shortcut-label">{{ t(it.label) }}</span>
<small class="nav-shortcut-group">{{ t(it.groupLabel) }}</small>
<Pin v-if="navShortcuts.includes(it.to)" class="nav-shortcut-mark on" />
<Plus v-else class="nav-shortcut-mark" />
</button>
</div>
</section>
</div>
</Teleport>
<main><RouterView /></main>
<div v-if="visibleTask && !useFullscreenLoading" class="taskbar">
<div><b>{{ activeTaskProject || visibleTask.stage }}</b><span>{{ t(visibleTask.messageKey || 'task.start', visibleTask.params || {}) }}</span></div>