模块介绍 / 今日规划 / 下班日报 / 团队摘要 生成成功后没有上报,也几乎不写「AI」日志。
流结束时的 usage 用了非阻塞发送,通道一满就被丢掉;很多服务商默认还不在流里带 usage。 现在每次真正打到模型的调用都会: 在本地日志(分类 AI,消息 AI 调用完成)写下 scene / prompt / completion / cached 登录后同步上报到后台日汇总
This commit is contained in:
@@ -160,6 +160,7 @@ const tokenChart = computed(() => {
|
||||
series: [
|
||||
{ name: 'Prompt', type: 'bar', stack: 'tok', barMaxWidth: 16, itemStyle: { color: '#7b73ff', borderRadius: [0, 0, 0, 0] }, data: s.map(p => p.promptTokens || 0) },
|
||||
{ name: 'Completion', type: 'bar', stack: 'tok', barMaxWidth: 16, itemStyle: { color: '#4fd1a1', borderRadius: [3, 3, 0, 0] }, data: s.map(p => p.completionTokens || 0) },
|
||||
{ name: t('adminCached'), type: 'line', smooth: true, showSymbol: false, lineStyle: { width: 2, color: '#23b5d3' }, itemStyle: { color: '#23b5d3' }, data: s.map(p => p.cachedTokens || 0) },
|
||||
{ name: t('adminCalls'), type: 'line', yAxisIndex: 1, smooth: true, showSymbol: false, lineStyle: { width: 2, color: '#f4c84a' }, itemStyle: { color: '#f4c84a' }, data: s.map(p => p.calls || 0) }
|
||||
]
|
||||
}
|
||||
@@ -307,6 +308,7 @@ onUnmounted(() => { offFilesDropped?.() })
|
||||
<div class="stat"><span>{{ t('adminStatTeams') }}</span><b>{{ overview.teamCount }}</b></div>
|
||||
<div class="stat"><span>{{ t('adminStatDAU') }}</span><b>{{ overview.dauToday }}</b></div>
|
||||
<div class="stat"><span>{{ t('adminStatTokens') }}</span><b>{{ (overview.tokenToday?.promptTokens||0)+(overview.tokenToday?.completionTokens||0) }}</b></div>
|
||||
<div class="stat"><span>{{ t('adminStatCached') }}</span><b>{{ overview.tokenToday?.cachedTokens||0 }}</b></div>
|
||||
</div>
|
||||
<div class="chart-grid">
|
||||
<div class="chart-card">
|
||||
@@ -501,7 +503,7 @@ onUnmounted(() => { offFilesDropped?.() })
|
||||
.admin-tabs{display:flex;flex-wrap:wrap;gap:.4rem;margin-bottom:1rem}
|
||||
.admin-tabs button{display:inline-flex;align-items:center;gap:.35rem;padding:.45rem .75rem;border-radius:8px;border:1px solid var(--border);background:transparent;color:inherit;cursor:pointer}
|
||||
.admin-tabs button.on{background:var(--accent, #3b82f6);color:#fff;border-color:transparent}
|
||||
.stat-grid{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:.75rem;margin-bottom:1.25rem}
|
||||
.stat-grid{display:grid;grid-template-columns:repeat(5,minmax(0,1fr));gap:.75rem;margin-bottom:1.25rem}
|
||||
.stat{padding:1rem;border:1px solid var(--border);border-radius:10px;display:flex;flex-direction:column;gap:.35rem}
|
||||
.stat b{font-size:1.6rem}
|
||||
.chart-grid{display:grid;grid-template-columns:1fr 1fr;gap:.75rem}
|
||||
|
||||
@@ -3,13 +3,14 @@ import { computed, nextTick, onMounted, onUnmounted, ref, watch } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { Browser } from '@wailsio/runtime'
|
||||
import { Plus, RefreshCw, Play, Square, Pencil, Trash2, X, Hexagon, Zap, FileCode2, Coffee, Database, Globe, Boxes, Box, Cpu, MemoryStick, HardDrive, Sparkles, Search, ExternalLink, FolderGit2, ScrollText, LoaderCircle, Image, ImageUp, Tags, Settings2, RotateCcw, ChevronRight, Package, Pin } from 'lucide-vue-next'
|
||||
import { Plus, RefreshCw, Play, Square, Pencil, Trash2, X, Hexagon, Zap, FileCode2, Coffee, Database, Globe, Boxes, Box, Cpu, MemoryStick, HardDrive, Sparkles, Search, ExternalLink, FolderGit2, ScrollText, LoaderCircle, Image, ImageUp, Tags, Settings2, RotateCcw, ChevronRight, Package, Pin, Network } from 'lucide-vue-next'
|
||||
import { call, on } from '../api'
|
||||
import { useAppStore } from '../store'
|
||||
import AIScopeDrawer from '../components/AIScopeDrawer.vue'
|
||||
import PackCmdsModal from '../components/PackCmdsModal.vue'
|
||||
import LaunchAppFormModal from '../components/LaunchAppFormModal.vue'
|
||||
import RefreshIntervalPicker from '../components/RefreshIntervalPicker.vue'
|
||||
import NetworkInfoModal from '../components/NetworkInfoModal.vue'
|
||||
import { getPackCmds } from '../packCmds'
|
||||
|
||||
// 启动台:扫描本机监听端口的服务 + 管理保存的应用(启动/停止/资源占用)。
|
||||
@@ -18,6 +19,7 @@ const store = useAppStore()
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const aiOpen = ref(false)
|
||||
const netInfoOpen = ref(false)
|
||||
const entries = ref([])
|
||||
const loading = ref(false)
|
||||
const nameQ = ref('')
|
||||
@@ -510,6 +512,7 @@ watch(() => route.query.pin, async v => {
|
||||
<div class="lp-head-top">
|
||||
<div><h1>{{ t('launchpad') }}</h1><p>{{ t('launchpadSubtitle') }}</p></div>
|
||||
<div class="lp-tools">
|
||||
<button class="btn secondary" @click="netInfoOpen = true"><Network />{{ t('netInfoBtn') }}</button>
|
||||
<button class="btn secondary" @click="aiOpen = true"><Sparkles />{{ t('aiScopeBtn') }}</button>
|
||||
<RefreshIntervalPicker v-model="refreshSec" />
|
||||
<button class="btn secondary" :disabled="loading" @click="load"><RefreshCw :class="{ spinning: loading }" />{{ t('lpRefresh') }}</button>
|
||||
@@ -705,6 +708,7 @@ watch(() => route.query.pin, async v => {
|
||||
</div>
|
||||
</Teleport>
|
||||
<LaunchAppFormModal :initial="editing" :initial-error="editErr" @close="editing = null; editErr = ''" @saved="onFormSaved" />
|
||||
<NetworkInfoModal v-if="netInfoOpen" @close="netInfoOpen = false" />
|
||||
<AIScopeDrawer v-if="aiOpen" kind="launchpad" :title="t('launchpad')" @close="aiOpen = false" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -2,10 +2,11 @@
|
||||
import { computed, onMounted, onUnmounted, ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { Star, Folder, Code2, GitCommitHorizontal, ListTodo, TicketCheck, StickyNote, ArrowRight, Circle, CircleDot, CircleCheck, Play, Check, CalendarDays, Plus, Bell, Save, Pencil, Eye } from 'lucide-vue-next'
|
||||
import { Star, Folder, Code2, GitCommitHorizontal, ListTodo, TicketCheck, StickyNote, ArrowRight, Circle, CircleDot, CircleCheck, Play, Check, CalendarDays, Plus, Bell, Save, Pencil, Eye, Maximize2 } from 'lucide-vue-next'
|
||||
import StatCard from '../components/StatCard.vue'
|
||||
import AIDayPanel from '../components/AIDayPanel.vue'
|
||||
import MarkdownView from '../components/MarkdownView.vue'
|
||||
import NoteModal from '../components/NoteModal.vue'
|
||||
import { call } from '../api'
|
||||
import { useAppStore } from '../store'
|
||||
|
||||
@@ -20,8 +21,10 @@ const noteText = ref('')
|
||||
const noteSavedAt = ref('')
|
||||
const noteDirty = ref(false)
|
||||
const noteSaving = ref(false)
|
||||
// 编辑/预览切换(预览走 Markdown 渲染),选择记忆在本地
|
||||
// 面板内编辑/预览;点击内容区打开模态框查看全文
|
||||
const notePreview = ref(localStorage.getItem('cc-wb-note-preview') === '1')
|
||||
const noteModal = ref(false)
|
||||
const noteModalMode = ref('preview')
|
||||
const messages = ref([])
|
||||
let noteTimer
|
||||
|
||||
@@ -79,13 +82,31 @@ function editNote() {
|
||||
clearTimeout(noteTimer)
|
||||
noteTimer = setTimeout(() => { saveNote(false) }, 800)
|
||||
}
|
||||
// 切到预览前先落盘,避免防抖窗口内的改动丢失
|
||||
async function setNotePreview(on) {
|
||||
if (notePreview.value === on) return
|
||||
if (on && noteDirty.value) await saveNote(false)
|
||||
notePreview.value = on
|
||||
localStorage.setItem('cc-wb-note-preview', on ? '1' : '0')
|
||||
}
|
||||
async function openNoteModal(mode = 'preview') {
|
||||
if (noteDirty.value) await saveNote(false)
|
||||
noteModalMode.value = mode
|
||||
noteModal.value = true
|
||||
}
|
||||
async function onNoteModalChanged() {
|
||||
await loadNotes()
|
||||
if (note.value?.id) {
|
||||
const fresh = notes.value.find(n => n.id === note.value.id)
|
||||
if (fresh) {
|
||||
note.value = fresh
|
||||
noteText.value = fresh.content || ''
|
||||
noteDirty.value = false
|
||||
}
|
||||
} else if (notes.value.length) {
|
||||
note.value = notes.value[0]
|
||||
noteText.value = note.value.content || ''
|
||||
}
|
||||
}
|
||||
async function saveNote(manual = true) {
|
||||
if (noteSaving.value) return
|
||||
noteSaving.value = true
|
||||
@@ -206,17 +227,18 @@ onUnmounted(() => clearTimeout(noteTimer))
|
||||
</section>
|
||||
|
||||
<section class="panel wb-col wb-note-panel">
|
||||
<div class="section-head">
|
||||
<div class="section-head wb-note-head">
|
||||
<h2><StickyNote class="panel-icon" />{{ t('notepad') }}<small>{{ notes.length }}</small></h2>
|
||||
<div class="wb-note-acts">
|
||||
<small v-if="noteDirty" class="wb-note-dirty">{{ t('noteUnsaved') }}</small>
|
||||
<small v-else-if="noteSavedAt" class="wb-note-saved">{{ t('autoSaved') }} {{ noteSavedAt }}</small>
|
||||
<div class="tabs compact wb-note-tabs">
|
||||
<button type="button" :class="{ active: !notePreview }" @click="setNotePreview(false)"><Pencil />{{ t('mdEdit') }}</button>
|
||||
<button type="button" :class="{ active: notePreview }" @click="setNotePreview(true)"><Eye />{{ t('mdPreviewTab') }}</button>
|
||||
<button type="button" :class="{ active: !notePreview }" :title="t('mdEdit')" @click="setNotePreview(false)"><Pencil /></button>
|
||||
<button type="button" :class="{ active: notePreview }" :title="t('mdPreviewTab')" @click="setNotePreview(true)"><Eye /></button>
|
||||
</div>
|
||||
<button type="button" class="btn secondary" :title="t('noteExpand')" @click="openNoteModal(notePreview ? 'preview' : 'edit')"><Maximize2 /></button>
|
||||
<button type="button" class="btn secondary" :title="t('noteNew')" @click="newNote"><Plus /></button>
|
||||
<button type="button" class="btn primary" :disabled="noteSaving || !noteDirty" :title="t('save')" @click="saveNote(true)"><Save />{{ t('save') }}</button>
|
||||
<button type="button" class="btn primary" :disabled="noteSaving || !noteDirty" :title="t('save')" @click="saveNote(true)"><Save /></button>
|
||||
<button type="button" class="btn secondary" @click="router.push('/notes')">{{ t('viewAll') }}<ArrowRight /></button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -235,8 +257,11 @@ onUnmounted(() => clearTimeout(noteTimer))
|
||||
</button>
|
||||
<div v-if="!notes.length" class="wb-note-list-empty">{{ t('noteEmpty') }}</div>
|
||||
</aside>
|
||||
<textarea v-show="!notePreview" v-model="noteText" class="wb-note" :placeholder="t('mdPlaceholder')" @input="editNote" />
|
||||
<MarkdownView v-if="notePreview" class="wb-note-preview md-preview-box" :source="noteText || t('mdEmpty')" />
|
||||
<div class="wb-note-stage" :class="{ preview: notePreview }" @click="notePreview && openNoteModal('preview')">
|
||||
<textarea v-show="!notePreview" v-model="noteText" class="wb-note" :placeholder="t('mdPlaceholder')" @input="editNote" @click.stop />
|
||||
<MarkdownView v-if="notePreview" class="wb-note-preview md-preview-box" :source="noteText || t('mdEmpty')" />
|
||||
<button v-if="notePreview" type="button" class="wb-note-expand" @click.stop="openNoteModal('preview')"><Maximize2 />{{ t('noteExpand') }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -254,4 +279,11 @@ onUnmounted(() => clearTimeout(noteTimer))
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
<NoteModal
|
||||
v-if="noteModal"
|
||||
:note="note"
|
||||
:initial-mode="noteModalMode"
|
||||
@close="noteModal = false; onNoteModalChanged()"
|
||||
@changed="onNoteModalChanged"
|
||||
/>
|
||||
</template>
|
||||
|
||||
Reference in New Issue
Block a user