Merge branch 'main' into main
This commit is contained in:
8 files changed
+433
-434
No files matched your search
@@ -82,7 +82,7 @@ Just install and start writing. All features work out of the box.
|
||||
|
||||
### Requirements
|
||||
- **Node.js** 18+
|
||||
- **npm** 9+
|
||||
- **npm** 9+ or **pnpm** 8+
|
||||
|
||||
### Installation
|
||||
|
||||
@@ -95,6 +95,9 @@ cd author
|
||||
|
||||
# Install dependencies
|
||||
npm install
|
||||
# Or use pnpm (no phantom dependency issues)
|
||||
# pnpm install
|
||||
# pnpm approve-builds # Required by pnpm to activate native packages
|
||||
|
||||
# Configure environment variables (optional)
|
||||
cp .env.example .env.local
|
||||
@@ -133,7 +136,7 @@ Download the latest installer from the [Releases](https://github.com/YuanShiJiLo
|
||||
|
||||
```bash
|
||||
git pull origin main
|
||||
npm install
|
||||
npm install # or: pnpm install && pnpm approve-builds
|
||||
npm run dev
|
||||
```
|
||||
|
||||
|
||||
+5
-2
@@ -83,7 +83,7 @@
|
||||
|
||||
### 环境要求
|
||||
- **Node.js** 18+
|
||||
- **npm** 9+
|
||||
- **npm** 9+ 或 **pnpm** 8+
|
||||
|
||||
### 安装
|
||||
|
||||
@@ -96,6 +96,9 @@ cd author
|
||||
|
||||
# 安装依赖
|
||||
npm install
|
||||
# 或使用 pnpm(无幽灵依赖问题)
|
||||
# pnpm install
|
||||
# pnpm approve-builds # pnpm 需要手动激活原生构建包
|
||||
|
||||
# 配置环境变量(可选)
|
||||
cp .env.example .env.local
|
||||
@@ -136,7 +139,7 @@ npm start
|
||||
|
||||
```bash
|
||||
git pull origin main
|
||||
npm install
|
||||
npm install # 或: pnpm install && pnpm approve-builds
|
||||
npm run dev
|
||||
```
|
||||
|
||||
|
||||
+352
-343
@@ -13,10 +13,11 @@ import { useAppStore } from '../store/useAppStore';
|
||||
import ChatMarkdown from './ChatMarkdown';
|
||||
import { useI18n } from '../lib/useI18n';
|
||||
|
||||
// 解析消息中的 [SETTINGS_ACTION] 块
|
||||
// 解析消息中的 [SETTINGS_ACTION] 块(支持可选的代码围栏包裹)
|
||||
function parseSettingsActions(content) {
|
||||
if (!content) return { parts: [content || ''], actions: [] };
|
||||
const regex = /\[SETTINGS_ACTION\]\s*([\s\S]*?)\s*\[\/SETTINGS_ACTION\]/g;
|
||||
// 匹配带或不带 ``` 包裹的 [SETTINGS_ACTION] 块
|
||||
const regex = /(?:```[^\n]*\n)?\[SETTINGS_ACTION\]\s*([\s\S]*?)\s*\[\/SETTINGS_ACTION\](?:\n```)?/g;
|
||||
const parts = [];
|
||||
const actions = [];
|
||||
let lastIndex = 0;
|
||||
@@ -1006,7 +1007,7 @@ export default function AiSidebar({ onInsertText }) {
|
||||
e.stopPropagation();
|
||||
// 提取正文:去掉系统块、markdown标记、编辑点评
|
||||
let text = (msg.content || '')
|
||||
.replace(/\[SETTINGS_ACTION\][\s\S]*?\[\\?\/SETTINGS_ACTION\]/g, '')
|
||||
.replace(/(?:```[^\n]*\n)?\[SETTINGS_ACTION\][\s\S]*?\[\\?\/SETTINGS_ACTION\](?:\n```)?/g, '')
|
||||
.replace(/^#{1,6}\s+/gm, '') // 去掉标题 #
|
||||
.replace(/\*\*(.+?)\*\*/g, '$1') // **粗体** → 粗体
|
||||
.replace(/\*(.+?)\*/g, '$1') // *斜体* → 斜体
|
||||
@@ -1025,7 +1026,8 @@ export default function AiSidebar({ onInsertText }) {
|
||||
}}
|
||||
>{t('aiSidebar.copy')}</button>
|
||||
</div>
|
||||
)}
|
||||
)
|
||||
}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
@@ -1058,368 +1060,375 @@ export default function AiSidebar({ onInsertText }) {
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
)
|
||||
}
|
||||
|
||||
{/* ==================== 📋 存档 Tab ==================== */}
|
||||
{activeTab === 'archive' && (
|
||||
<div className="ai-sidebar-body">
|
||||
<div className="archive-search-bar">
|
||||
<input
|
||||
className="archive-search-input"
|
||||
placeholder={t('aiSidebar.searchArchive')}
|
||||
value={archiveSearch}
|
||||
onChange={e => setArchiveSearch(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="archive-list">
|
||||
{filteredArchive.length === 0 && (
|
||||
<div className="chat-empty">
|
||||
<div>{t('aiSidebar.emptyArchiveIcon')}</div>
|
||||
<div>{t('aiSidebar.emptyArchiveTitle')}</div>
|
||||
<div className="chat-empty-hint">{t('aiSidebar.emptyArchiveHint')}</div>
|
||||
</div>
|
||||
)}
|
||||
{[...filteredArchive].reverse().map(item => (
|
||||
<div
|
||||
key={item.id}
|
||||
className={`archive-item ${item.status}`}
|
||||
onClick={() => setExpandedArchive(expandedArchive === item.id ? null : item.id)}
|
||||
>
|
||||
<div className="archive-item-header">
|
||||
<span className={`archive-status ${item.status}`}>
|
||||
{t(`aiSidebar.statuses.${item.status}`) || item.status}
|
||||
</span>
|
||||
<span className="archive-mode">{t(`aiSidebar.modes.${item.mode}`) || item.mode}</span>
|
||||
<span className="archive-time">
|
||||
{new Date(item.timestamp).toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' })}
|
||||
</span>
|
||||
</div>
|
||||
<div className="archive-preview">
|
||||
{item.text?.slice(0, 60)}…
|
||||
</div>
|
||||
{expandedArchive === item.id && (
|
||||
<div className="archive-expanded">
|
||||
<pre className="archive-full-text">{item.text}</pre>
|
||||
<div className="archive-actions">
|
||||
<button className="btn-mini" onClick={(e) => { e.stopPropagation(); onInsertText?.(item.text); }}>
|
||||
{t('aiSidebar.insertEditor')}
|
||||
</button>
|
||||
<button className="btn-mini" onClick={(e) => { e.stopPropagation(); navigator.clipboard.writeText(item.text); }}>
|
||||
{t('aiSidebar.copy')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* ==================== 📚 参考 Tab ==================== */}
|
||||
{activeTab === 'reference' && (
|
||||
<div className="ai-sidebar-body">
|
||||
{/* Token 预算进度条 */}
|
||||
<div className="context-budget-bar">
|
||||
<div className="context-budget-label">
|
||||
<span>{t('aiSidebar.tokenUsage')}</span>
|
||||
<span className={isOverBudget ? 'context-over-budget' : ''}>
|
||||
{totalSelectedTokens.toLocaleString()} / {(INPUT_TOKEN_BUDGET / 1000).toFixed(0)}k
|
||||
</span>
|
||||
</div>
|
||||
<div className="context-budget-track">
|
||||
<div
|
||||
className={`context-budget-fill ${isOverBudget ? 'over' : ''}`}
|
||||
style={{ width: `${Math.min(100, budgetPercent)}%` }}
|
||||
{
|
||||
activeTab === 'archive' && (
|
||||
<div className="ai-sidebar-body">
|
||||
<div className="archive-search-bar">
|
||||
<input
|
||||
className="archive-search-input"
|
||||
placeholder={t('aiSidebar.searchArchive')}
|
||||
value={archiveSearch}
|
||||
onChange={e => setArchiveSearch(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 搜索框 */}
|
||||
<div className="context-search-bar">
|
||||
<input
|
||||
className="context-search-input"
|
||||
placeholder={t('aiSidebar.searchContext')}
|
||||
value={contextSearch}
|
||||
onChange={e => setContextSearch(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 分组列表 */}
|
||||
<div className="context-groups">
|
||||
{Object.entries(groupedItems).length === 0 && (
|
||||
<div className="chat-empty">
|
||||
<div>{t('aiSidebar.emptyContextIcon')}</div>
|
||||
<div>{t('aiSidebar.emptyContextTitle')}</div>
|
||||
<div className="chat-empty-hint">
|
||||
{contextSearch ? t('aiSidebar.emptyContextHint1') : t('aiSidebar.emptyContextHint2')}
|
||||
<div className="archive-list">
|
||||
{filteredArchive.length === 0 && (
|
||||
<div className="chat-empty">
|
||||
<div>{t('aiSidebar.emptyArchiveIcon')}</div>
|
||||
<div>{t('aiSidebar.emptyArchiveTitle')}</div>
|
||||
<div className="chat-empty-hint">{t('aiSidebar.emptyArchiveHint')}</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{Object.entries(groupedItems).map(([groupName, items]) => {
|
||||
const isCollapsed = collapsedGroups.has(groupName);
|
||||
const checkedCount = items.filter(it => contextSelection?.has(it.id)).length;
|
||||
const groupTokens = items
|
||||
.filter(it => contextSelection?.has(it.id))
|
||||
.reduce((sum, it) => sum + it.tokens, 0);
|
||||
const allGroupChecked = checkedCount === items.length;
|
||||
|
||||
return (
|
||||
<div key={groupName} className="context-group">
|
||||
<div
|
||||
className="context-group-header"
|
||||
onClick={() => toggleCollapse(groupName)}
|
||||
>
|
||||
<span className="context-collapse-icon">
|
||||
{isCollapsed ? '▶' : '▼'}
|
||||
)}
|
||||
{[...filteredArchive].reverse().map(item => (
|
||||
<div
|
||||
key={item.id}
|
||||
className={`archive-item ${item.status}`}
|
||||
onClick={() => setExpandedArchive(expandedArchive === item.id ? null : item.id)}
|
||||
>
|
||||
<div className="archive-item-header">
|
||||
<span className={`archive-status ${item.status}`}>
|
||||
{t(`aiSidebar.statuses.${item.status}`) || item.status}
|
||||
</span>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={allGroupChecked && items.length > 0}
|
||||
ref={el => {
|
||||
if (el) el.indeterminate = checkedCount > 0 && checkedCount < items.length;
|
||||
}}
|
||||
onChange={(e) => {
|
||||
e.stopPropagation();
|
||||
toggleGroup(groupName);
|
||||
}}
|
||||
onClick={e => e.stopPropagation()}
|
||||
className="context-group-check"
|
||||
/>
|
||||
<span className="context-group-name">
|
||||
{groupName} ({checkedCount}/{items.length})
|
||||
</span>
|
||||
<span className="context-group-tokens">
|
||||
{groupTokens > 0 ? `${groupTokens.toLocaleString()}t` : '—'}
|
||||
<span className="archive-mode">{t(`aiSidebar.modes.${item.mode}`) || item.mode}</span>
|
||||
<span className="archive-time">
|
||||
{new Date(item.timestamp).toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' })}
|
||||
</span>
|
||||
</div>
|
||||
{!isCollapsed && (
|
||||
<div className="context-group-items">
|
||||
{items.map(item => (
|
||||
<label key={item.id} className="context-item">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={contextSelection?.has(item.id) || false}
|
||||
onChange={() => toggleContextItem(item.id)}
|
||||
className="context-item-check"
|
||||
/>
|
||||
<span className="context-item-name" title={item.name}>
|
||||
{item.name}
|
||||
</span>
|
||||
<span className="context-item-tokens">
|
||||
{item.tokens > 0 ? `${item.tokens.toLocaleString()}t` : '—'}
|
||||
</span>
|
||||
</label>
|
||||
))}
|
||||
<div className="archive-preview">
|
||||
{item.text?.slice(0, 60)}…
|
||||
</div>
|
||||
{expandedArchive === item.id && (
|
||||
<div className="archive-expanded">
|
||||
<pre className="archive-full-text">{item.text}</pre>
|
||||
<div className="archive-actions">
|
||||
<button className="btn-mini" onClick={(e) => { e.stopPropagation(); onInsertText?.(item.text); }}>
|
||||
{t('aiSidebar.insertEditor')}
|
||||
</button>
|
||||
<button className="btn-mini" onClick={(e) => { e.stopPropagation(); navigator.clipboard.writeText(item.text); }}>
|
||||
{t('aiSidebar.copy')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
{/* 批量操作 */}
|
||||
<div className="context-actions">
|
||||
<button className="btn-mini" onClick={selectAll}>{t('aiSidebar.selectAll')}</button>
|
||||
<button className="btn-mini" onClick={selectNone}>{t('aiSidebar.selectNone')}</button>
|
||||
<button className="btn-mini" onClick={resetSelection}>{t('aiSidebar.reset')}</button>
|
||||
<button className="btn-mini" onClick={onOpenSettings}>{t('aiSidebar.settings')}</button>
|
||||
{/* ==================== 📚 参考 Tab ==================== */}
|
||||
{
|
||||
activeTab === 'reference' && (
|
||||
<div className="ai-sidebar-body">
|
||||
{/* Token 预算进度条 */}
|
||||
<div className="context-budget-bar">
|
||||
<div className="context-budget-label">
|
||||
<span>{t('aiSidebar.tokenUsage')}</span>
|
||||
<span className={isOverBudget ? 'context-over-budget' : ''}>
|
||||
{totalSelectedTokens.toLocaleString()} / {(INPUT_TOKEN_BUDGET / 1000).toFixed(0)}k
|
||||
</span>
|
||||
</div>
|
||||
<div className="context-budget-track">
|
||||
<div
|
||||
className={`context-budget-fill ${isOverBudget ? 'over' : ''}`}
|
||||
style={{ width: `${Math.min(100, budgetPercent)}%` }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 搜索框 */}
|
||||
<div className="context-search-bar">
|
||||
<input
|
||||
className="context-search-input"
|
||||
placeholder={t('aiSidebar.searchContext')}
|
||||
value={contextSearch}
|
||||
onChange={e => setContextSearch(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 分组列表 */}
|
||||
<div className="context-groups">
|
||||
{Object.entries(groupedItems).length === 0 && (
|
||||
<div className="chat-empty">
|
||||
<div>{t('aiSidebar.emptyContextIcon')}</div>
|
||||
<div>{t('aiSidebar.emptyContextTitle')}</div>
|
||||
<div className="chat-empty-hint">
|
||||
{contextSearch ? t('aiSidebar.emptyContextHint1') : t('aiSidebar.emptyContextHint2')}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{Object.entries(groupedItems).map(([groupName, items]) => {
|
||||
const isCollapsed = collapsedGroups.has(groupName);
|
||||
const checkedCount = items.filter(it => contextSelection?.has(it.id)).length;
|
||||
const groupTokens = items
|
||||
.filter(it => contextSelection?.has(it.id))
|
||||
.reduce((sum, it) => sum + it.tokens, 0);
|
||||
const allGroupChecked = checkedCount === items.length;
|
||||
|
||||
return (
|
||||
<div key={groupName} className="context-group">
|
||||
<div
|
||||
className="context-group-header"
|
||||
onClick={() => toggleCollapse(groupName)}
|
||||
>
|
||||
<span className="context-collapse-icon">
|
||||
{isCollapsed ? '▶' : '▼'}
|
||||
</span>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={allGroupChecked && items.length > 0}
|
||||
ref={el => {
|
||||
if (el) el.indeterminate = checkedCount > 0 && checkedCount < items.length;
|
||||
}}
|
||||
onChange={(e) => {
|
||||
e.stopPropagation();
|
||||
toggleGroup(groupName);
|
||||
}}
|
||||
onClick={e => e.stopPropagation()}
|
||||
className="context-group-check"
|
||||
/>
|
||||
<span className="context-group-name">
|
||||
{groupName} ({checkedCount}/{items.length})
|
||||
</span>
|
||||
<span className="context-group-tokens">
|
||||
{groupTokens > 0 ? `${groupTokens.toLocaleString()}t` : '—'}
|
||||
</span>
|
||||
</div>
|
||||
{!isCollapsed && (
|
||||
<div className="context-group-items">
|
||||
{items.map(item => (
|
||||
<label key={item.id} className="context-item">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={contextSelection?.has(item.id) || false}
|
||||
onChange={() => toggleContextItem(item.id)}
|
||||
className="context-item-check"
|
||||
/>
|
||||
<span className="context-item-name" title={item.name}>
|
||||
{item.name}
|
||||
</span>
|
||||
<span className="context-item-tokens">
|
||||
{item.tokens > 0 ? `${item.tokens.toLocaleString()}t` : '—'}
|
||||
</span>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* 批量操作 */}
|
||||
<div className="context-actions">
|
||||
<button className="btn-mini" onClick={selectAll}>{t('aiSidebar.selectAll')}</button>
|
||||
<button className="btn-mini" onClick={selectNone}>{t('aiSidebar.selectNone')}</button>
|
||||
<button className="btn-mini" onClick={resetSelection}>{t('aiSidebar.reset')}</button>
|
||||
<button className="btn-mini" onClick={onOpenSettings}>{t('aiSidebar.settings')}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
)
|
||||
}
|
||||
|
||||
{/* ==================== 📊 统计 Tab ==================== */}
|
||||
{activeTab === 'stats' && (
|
||||
<div className="ai-sidebar-body">
|
||||
<div className="token-stats-panel">
|
||||
{tokenStats.totalRequests === 0 ? (
|
||||
<div className="chat-empty">
|
||||
<div>📊</div>
|
||||
<div>{t('aiSidebar.statsNoData')}</div>
|
||||
<div className="chat-empty-hint">{t('aiSidebar.statsNoDataHint')}</div>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{/* 汇总卡片 */}
|
||||
<div className="stats-grid">
|
||||
<div className="stats-card">
|
||||
<div className="stats-card-value">{tokenStats.totalTokens.toLocaleString()}</div>
|
||||
<div className="stats-card-label">{t('aiSidebar.statsTotalTokens')}</div>
|
||||
</div>
|
||||
<div className="stats-card">
|
||||
<div className="stats-card-value">{tokenStats.totalPromptTokens.toLocaleString()}</div>
|
||||
<div className="stats-card-label">{t('aiSidebar.statsTotalInput')}</div>
|
||||
</div>
|
||||
<div className="stats-card">
|
||||
<div className="stats-card-value">{tokenStats.totalCompletionTokens.toLocaleString()}</div>
|
||||
<div className="stats-card-label">{t('aiSidebar.statsTotalOutput')}</div>
|
||||
</div>
|
||||
<div className="stats-card">
|
||||
<div className="stats-card-value">{tokenStats.totalRequests}</div>
|
||||
<div className="stats-card-label">{t('aiSidebar.statsTotalRequests')}</div>
|
||||
</div>
|
||||
<div className="stats-card">
|
||||
<div className="stats-card-value">{tokenStats.trackedDays}</div>
|
||||
<div className="stats-card-label">{t('aiSidebar.statsTrackedDays')}</div>
|
||||
</div>
|
||||
{
|
||||
activeTab === 'stats' && (
|
||||
<div className="ai-sidebar-body">
|
||||
<div className="token-stats-panel">
|
||||
{tokenStats.totalRequests === 0 ? (
|
||||
<div className="chat-empty">
|
||||
<div>📊</div>
|
||||
<div>{t('aiSidebar.statsNoData')}</div>
|
||||
<div className="chat-empty-hint">{t('aiSidebar.statsNoDataHint')}</div>
|
||||
</div>
|
||||
|
||||
{/* 消耗速率 */}
|
||||
<div className="stats-section">
|
||||
<div className="stats-section-title">{t('aiSidebar.statsRates')}</div>
|
||||
<div className="stats-section-hint">{t('aiSidebar.statsRatesHint')}</div>
|
||||
<table className="projection-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{t('aiSidebar.statsRateMetric')}</th>
|
||||
<th>{t('aiSidebar.statsRateDesc')}</th>
|
||||
<th>{t('aiSidebar.statsRateValue')}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{[
|
||||
['TPS', tokenStats.rates.tps, t('aiSidebar.statsRateTPS')],
|
||||
['TPM', tokenStats.rates.tpm, t('aiSidebar.statsRateTPM')],
|
||||
['TPH', tokenStats.rates.tph, t('aiSidebar.statsRateTPH')],
|
||||
['TPD', tokenStats.rates.tpd, t('aiSidebar.statsRateTPD')],
|
||||
['RPM', tokenStats.rates.rpm, t('aiSidebar.statsRateRPM')],
|
||||
['RPH', tokenStats.rates.rph, t('aiSidebar.statsRateRPH')],
|
||||
['RPD', tokenStats.rates.rpd, t('aiSidebar.statsRateRPD')],
|
||||
].map(([key, value, label]) => (
|
||||
<tr key={key} title={label}>
|
||||
<td><strong>{key}</strong></td>
|
||||
<td className="stats-rate-desc">{label}</td>
|
||||
<td>{value < 1 ? value.toFixed(2) : value < 10 ? value.toFixed(1) : Math.round(value).toLocaleString()}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{/* 近期请求速度 */}
|
||||
{tokenStats.recentSpeeds.length > 0 && (
|
||||
<div className="stats-section">
|
||||
<div className="stats-section-title">{t('aiSidebar.statsRecentSpeeds')}</div>
|
||||
<div className="speed-chart">
|
||||
{(() => {
|
||||
const maxSpeed = Math.max(...tokenStats.recentSpeeds.map(s => s.speed));
|
||||
return tokenStats.recentSpeeds.map((s, i) => (
|
||||
<div key={i} className="speed-bar-wrapper" title={`${s.speed.toFixed(1)} tokens/s · ${s.tokens} tokens`}>
|
||||
<div
|
||||
className="speed-bar"
|
||||
style={{ height: `${Math.max(4, (s.speed / maxSpeed) * 100)}%` }}
|
||||
/>
|
||||
<span className="speed-bar-label">{s.speed.toFixed(0)}</span>
|
||||
</div>
|
||||
));
|
||||
})()}
|
||||
) : (
|
||||
<>
|
||||
{/* 汇总卡片 */}
|
||||
<div className="stats-grid">
|
||||
<div className="stats-card">
|
||||
<div className="stats-card-value">{tokenStats.totalTokens.toLocaleString()}</div>
|
||||
<div className="stats-card-label">{t('aiSidebar.statsTotalTokens')}</div>
|
||||
</div>
|
||||
<div className="stats-card">
|
||||
<div className="stats-card-value">{tokenStats.totalPromptTokens.toLocaleString()}</div>
|
||||
<div className="stats-card-label">{t('aiSidebar.statsTotalInput')}</div>
|
||||
</div>
|
||||
<div className="stats-card">
|
||||
<div className="stats-card-value">{tokenStats.totalCompletionTokens.toLocaleString()}</div>
|
||||
<div className="stats-card-label">{t('aiSidebar.statsTotalOutput')}</div>
|
||||
</div>
|
||||
<div className="stats-card">
|
||||
<div className="stats-card-value">{tokenStats.totalRequests}</div>
|
||||
<div className="stats-card-label">{t('aiSidebar.statsTotalRequests')}</div>
|
||||
</div>
|
||||
<div className="stats-card">
|
||||
<div className="stats-card-value">{tokenStats.trackedDays}</div>
|
||||
<div className="stats-card-label">{t('aiSidebar.statsTrackedDays')}</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 消耗预估 */}
|
||||
<div className="stats-section">
|
||||
<div className="stats-section-title">{t('aiSidebar.statsProjections')}</div>
|
||||
<div className="stats-section-hint">{t('aiSidebar.statsProjectionsHint')}</div>
|
||||
<table className="projection-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{t('aiSidebar.statsPeriod')}</th>
|
||||
<th>{t('aiSidebar.statsTokens')}</th>
|
||||
<th>{t('aiSidebar.statsRequests')}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{[
|
||||
['statsPeriodDay', tokenStats.projections.perDay],
|
||||
['statsPeriodWeek', tokenStats.projections.perWeek],
|
||||
['statsPeriodMonth', tokenStats.projections.perMonth],
|
||||
['statsPeriodQuarter', tokenStats.projections.perQuarter],
|
||||
['statsPeriodYear', tokenStats.projections.perYear],
|
||||
].map(([key, data]) => (
|
||||
<tr key={key}>
|
||||
<td>{t(`aiSidebar.${key}`)}</td>
|
||||
<td>{data.tokens.toLocaleString()}</td>
|
||||
<td>{data.requests}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{/* 渠道/模型分类统计 */}
|
||||
{tokenStats.modelBreakdown.length > 0 && (
|
||||
{/* 消耗速率 */}
|
||||
<div className="stats-section">
|
||||
<div className="stats-section-title">{t('aiSidebar.statsModelBreakdown')}</div>
|
||||
<div className="stats-section-hint">{t('aiSidebar.statsModelBreakdownHint')}</div>
|
||||
{tokenStats.modelBreakdown.map((m, idx) => {
|
||||
const bgGradient = getProviderColor(m.provider, m.model);
|
||||
return (
|
||||
<div key={idx} className="model-info-card">
|
||||
<div className="model-info-header">
|
||||
<div className="model-info-title">
|
||||
<span className="model-info-badge" style={{ background: bgGradient }}>
|
||||
<ProviderLogo provider={m.provider} model={m.model} className="provider-logo-svg" />
|
||||
{m.provider}
|
||||
</span>
|
||||
<span className="model-info-name" title={m.model}>{m.model}</span>
|
||||
</div>
|
||||
<div className="model-info-percent">
|
||||
{Math.round(m.tokenPercent)}<span>%</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="model-info-bar-track">
|
||||
<div className="model-info-bar-fill" style={{ width: `${m.tokenPercent}%`, background: bgGradient }} />
|
||||
</div>
|
||||
|
||||
<div className="model-info-stats">
|
||||
<div className="info-stat-group">
|
||||
<span className="info-stat-value">{m.tokens.toLocaleString()}</span>
|
||||
<span className="info-stat-label">Tokens</span>
|
||||
</div>
|
||||
<div className="info-stat-group">
|
||||
<span className="info-stat-value">{m.requests}</span>
|
||||
<span className="info-stat-label">{t('aiSidebar.statsRequests')}</span>
|
||||
</div>
|
||||
<div className="info-stat-group" title={`${t('aiSidebar.statsTotalInput')}: ${m.promptTokens} / ${t('aiSidebar.statsTotalOutput')}: ${m.completionTokens}`}>
|
||||
<span className="info-stat-value">
|
||||
{m.promptTokens > 1000 ? (m.promptTokens / 1000).toFixed(1) + 'k' : m.promptTokens} / {m.completionTokens > 1000 ? (m.completionTokens / 1000).toFixed(1) + 'k' : m.completionTokens}
|
||||
</span>
|
||||
<span className="info-stat-label">In / Out</span>
|
||||
</div>
|
||||
{m.avgSpeed > 0 && (
|
||||
<div className="info-stat-group">
|
||||
<span className="info-stat-value">{m.avgSpeed.toFixed(1)}</span>
|
||||
<span className="info-stat-label">t/s</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
<div className="stats-section-title">{t('aiSidebar.statsRates')}</div>
|
||||
<div className="stats-section-hint">{t('aiSidebar.statsRatesHint')}</div>
|
||||
<table className="projection-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{t('aiSidebar.statsRateMetric')}</th>
|
||||
<th>{t('aiSidebar.statsRateDesc')}</th>
|
||||
<th>{t('aiSidebar.statsRateValue')}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{[
|
||||
['TPS', tokenStats.rates.tps, t('aiSidebar.statsRateTPS')],
|
||||
['TPM', tokenStats.rates.tpm, t('aiSidebar.statsRateTPM')],
|
||||
['TPH', tokenStats.rates.tph, t('aiSidebar.statsRateTPH')],
|
||||
['TPD', tokenStats.rates.tpd, t('aiSidebar.statsRateTPD')],
|
||||
['RPM', tokenStats.rates.rpm, t('aiSidebar.statsRateRPM')],
|
||||
['RPH', tokenStats.rates.rph, t('aiSidebar.statsRateRPH')],
|
||||
['RPD', tokenStats.rates.rpd, t('aiSidebar.statsRateRPD')],
|
||||
].map(([key, value, label]) => (
|
||||
<tr key={key} title={label}>
|
||||
<td><strong>{key}</strong></td>
|
||||
<td className="stats-rate-desc">{label}</td>
|
||||
<td>{value < 1 ? value.toFixed(2) : value < 10 ? value.toFixed(1) : Math.round(value).toLocaleString()}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 清空按钮 */}
|
||||
<div className="stats-actions">
|
||||
<button
|
||||
className="btn-mini danger"
|
||||
onClick={() => {
|
||||
if (confirm(t('aiSidebar.statsClearConfirm'))) {
|
||||
clearTokenStats();
|
||||
setStatsVersion(v => v + 1);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{t('aiSidebar.statsClearBtn')}
|
||||
</button>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{/* 近期请求速度 */}
|
||||
{tokenStats.recentSpeeds.length > 0 && (
|
||||
<div className="stats-section">
|
||||
<div className="stats-section-title">{t('aiSidebar.statsRecentSpeeds')}</div>
|
||||
<div className="speed-chart">
|
||||
{(() => {
|
||||
const maxSpeed = Math.max(...tokenStats.recentSpeeds.map(s => s.speed));
|
||||
return tokenStats.recentSpeeds.map((s, i) => (
|
||||
<div key={i} className="speed-bar-wrapper" title={`${s.speed.toFixed(1)} tokens/s · ${s.tokens} tokens`}>
|
||||
<div
|
||||
className="speed-bar"
|
||||
style={{ height: `${Math.max(4, (s.speed / maxSpeed) * 100)}%` }}
|
||||
/>
|
||||
<span className="speed-bar-label">{s.speed.toFixed(0)}</span>
|
||||
</div>
|
||||
));
|
||||
})()}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 消耗预估 */}
|
||||
<div className="stats-section">
|
||||
<div className="stats-section-title">{t('aiSidebar.statsProjections')}</div>
|
||||
<div className="stats-section-hint">{t('aiSidebar.statsProjectionsHint')}</div>
|
||||
<table className="projection-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{t('aiSidebar.statsPeriod')}</th>
|
||||
<th>{t('aiSidebar.statsTokens')}</th>
|
||||
<th>{t('aiSidebar.statsRequests')}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{[
|
||||
['statsPeriodDay', tokenStats.projections.perDay],
|
||||
['statsPeriodWeek', tokenStats.projections.perWeek],
|
||||
['statsPeriodMonth', tokenStats.projections.perMonth],
|
||||
['statsPeriodQuarter', tokenStats.projections.perQuarter],
|
||||
['statsPeriodYear', tokenStats.projections.perYear],
|
||||
].map(([key, data]) => (
|
||||
<tr key={key}>
|
||||
<td>{t(`aiSidebar.${key}`)}</td>
|
||||
<td>{data.tokens.toLocaleString()}</td>
|
||||
<td>{data.requests}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{/* 渠道/模型分类统计 */}
|
||||
{tokenStats.modelBreakdown.length > 0 && (
|
||||
<div className="stats-section">
|
||||
<div className="stats-section-title">{t('aiSidebar.statsModelBreakdown')}</div>
|
||||
<div className="stats-section-hint">{t('aiSidebar.statsModelBreakdownHint')}</div>
|
||||
{tokenStats.modelBreakdown.map((m, idx) => {
|
||||
const bgGradient = getProviderColor(m.provider, m.model);
|
||||
return (
|
||||
<div key={idx} className="model-info-card">
|
||||
<div className="model-info-header">
|
||||
<div className="model-info-title">
|
||||
<span className="model-info-badge" style={{ background: bgGradient }}>
|
||||
<ProviderLogo provider={m.provider} model={m.model} className="provider-logo-svg" />
|
||||
{m.provider}
|
||||
</span>
|
||||
<span className="model-info-name" title={m.model}>{m.model}</span>
|
||||
</div>
|
||||
<div className="model-info-percent">
|
||||
{Math.round(m.tokenPercent)}<span>%</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="model-info-bar-track">
|
||||
<div className="model-info-bar-fill" style={{ width: `${m.tokenPercent}%`, background: bgGradient }} />
|
||||
</div>
|
||||
|
||||
<div className="model-info-stats">
|
||||
<div className="info-stat-group">
|
||||
<span className="info-stat-value">{m.tokens.toLocaleString()}</span>
|
||||
<span className="info-stat-label">Tokens</span>
|
||||
</div>
|
||||
<div className="info-stat-group">
|
||||
<span className="info-stat-value">{m.requests}</span>
|
||||
<span className="info-stat-label">{t('aiSidebar.statsRequests')}</span>
|
||||
</div>
|
||||
<div className="info-stat-group" title={`${t('aiSidebar.statsTotalInput')}: ${m.promptTokens} / ${t('aiSidebar.statsTotalOutput')}: ${m.completionTokens}`}>
|
||||
<span className="info-stat-value">
|
||||
{m.promptTokens > 1000 ? (m.promptTokens / 1000).toFixed(1) + 'k' : m.promptTokens} / {m.completionTokens > 1000 ? (m.completionTokens / 1000).toFixed(1) + 'k' : m.completionTokens}
|
||||
</span>
|
||||
<span className="info-stat-label">In / Out</span>
|
||||
</div>
|
||||
{m.avgSpeed > 0 && (
|
||||
<div className="info-stat-group">
|
||||
<span className="info-stat-value">{m.avgSpeed.toFixed(1)}</span>
|
||||
<span className="info-stat-label">t/s</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 清空按钮 */}
|
||||
<div className="stats-actions">
|
||||
<button
|
||||
className="btn-mini danger"
|
||||
onClick={() => {
|
||||
if (confirm(t('aiSidebar.statsClearConfirm'))) {
|
||||
clearTokenStats();
|
||||
setStatsVersion(v => v + 1);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{t('aiSidebar.statsClearBtn')}
|
||||
</button>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</div >
|
||||
);
|
||||
}
|
||||
+46
-72
@@ -134,7 +134,7 @@ export async function getContextItems(activeChapterId) {
|
||||
id: `chapter-${ch.id}`,
|
||||
group: '📑 章节',
|
||||
name: `第${i + 1}章 ${ch.title}${i > currentIndex ? '(后续)' : ''}`,
|
||||
tokens: estimateTokens(text ? text.slice(0, 1500) : ''),
|
||||
tokens: estimateTokens(text),
|
||||
category: 'chapter',
|
||||
enabled: i < currentIndex, // 前文章节默认启用,后续章节默认不启用
|
||||
});
|
||||
@@ -180,65 +180,60 @@ export async function buildContext(activeChapterId, selectedText, selectedIds =
|
||||
// 获取所有有效的设定条目
|
||||
const allValidItemNodes = nodes.filter(n => n.type === 'item' && n.enabled !== false);
|
||||
|
||||
// 如果有 selectedIds,提取手动勾选的设定
|
||||
const manualItemNodes = [];
|
||||
const unselectedItemNodes = [];
|
||||
let finalItemNodes;
|
||||
|
||||
for (const n of allValidItemNodes) {
|
||||
if (selectedIds && selectedIds.has(`setting-${n.id}`)) {
|
||||
manualItemNodes.push(n);
|
||||
} else if (!selectedIds) {
|
||||
// selectedIds 为 null — 兼容旧逻辑,全部走 RAG
|
||||
unselectedItemNodes.push(n);
|
||||
} else {
|
||||
unselectedItemNodes.push(n);
|
||||
}
|
||||
}
|
||||
if (!selectedIds) {
|
||||
// selectedIds 为 null — 用户没有手动勾选任何参考
|
||||
// 包含所有启用的设定条目,再用 RAG 补充排序(如果可用)
|
||||
finalItemNodes = [...allValidItemNodes];
|
||||
} else {
|
||||
// 用户手动勾选了参考条目
|
||||
const manualItemNodes = [];
|
||||
const unselectedItemNodes = [];
|
||||
|
||||
// --- RAG 自动检索(仅当开启嵌入功能时) ---
|
||||
let autoRetrievedNodes = [];
|
||||
const queryText = (selectedText || '').trim();
|
||||
if (settings.apiConfig?.useCustomEmbed && queryText && unselectedItemNodes.length > 0) {
|
||||
try {
|
||||
// 获取 Query 的 Embedding
|
||||
// 此处的文本可以是光标前的一小段文字,为了简便使用 selectedText
|
||||
// TODO: 这里如果是空文本,可能需要补当前章节最近的内容
|
||||
let ragSourceText = queryText;
|
||||
if (ragSourceText.length < 50 && currentChapter) {
|
||||
// 文本太短,用当前章节末尾补充
|
||||
const stripChapText = stripHtml(currentChapter.content || '').slice(-200);
|
||||
ragSourceText = ragSourceText + '\n' + stripChapText;
|
||||
for (const n of allValidItemNodes) {
|
||||
if (selectedIds.has(`setting-${n.id}`)) {
|
||||
manualItemNodes.push(n);
|
||||
} else {
|
||||
unselectedItemNodes.push(n);
|
||||
}
|
||||
|
||||
const queryVector = await getEmbedding(ragSourceText, settings.apiConfig);
|
||||
if (queryVector) {
|
||||
// 计算相似度
|
||||
const scoredNodes = unselectedItemNodes.map(n => {
|
||||
if (!n.embedding) return { node: n, score: -1 };
|
||||
return { node: n, score: cosineSimilarity(queryVector, n.embedding) };
|
||||
}).filter(x => x.score > 0.3); // 设置一个基础阈值 0.3
|
||||
|
||||
// 按分数降序
|
||||
scoredNodes.sort((a, b) => b.score - a.score);
|
||||
|
||||
// 取 Top-K (比如最多取 5 个)
|
||||
autoRetrievedNodes = scoredNodes.slice(0, 5).map(x => x.node);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('RAG Retrieval failed:', e);
|
||||
}
|
||||
}
|
||||
|
||||
// 合并手动与自动检索的节点
|
||||
const itemNodes = [...manualItemNodes, ...autoRetrievedNodes];
|
||||
// 去重
|
||||
const finalItemNodes = Array.from(new Set(itemNodes));
|
||||
// --- RAG 自动检索(仅当有手动勾选时,对未勾选项做 RAG 补充) ---
|
||||
let autoRetrievedNodes = [];
|
||||
const queryText = (selectedText || '').trim();
|
||||
if (settings.apiConfig?.useCustomEmbed && queryText && unselectedItemNodes.length > 0) {
|
||||
try {
|
||||
let ragSourceText = queryText;
|
||||
if (ragSourceText.length < 50 && currentChapter) {
|
||||
const stripChapText = stripHtml(currentChapter.content || '').slice(-200);
|
||||
ragSourceText = ragSourceText + '\n' + stripChapText;
|
||||
}
|
||||
|
||||
const queryVector = await getEmbedding(ragSourceText, settings.apiConfig);
|
||||
if (queryVector) {
|
||||
const scoredNodes = unselectedItemNodes.map(n => {
|
||||
if (!n.embedding) return { node: n, score: -1 };
|
||||
return { node: n, score: cosineSimilarity(queryVector, n.embedding) };
|
||||
}).filter(x => x.score > 0.3);
|
||||
|
||||
scoredNodes.sort((a, b) => b.score - a.score);
|
||||
autoRetrievedNodes = scoredNodes.slice(0, 5).map(x => x.node);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('RAG Retrieval failed:', e);
|
||||
}
|
||||
}
|
||||
|
||||
// 合并手动与自动检索的节点,去重
|
||||
finalItemNodes = Array.from(new Set([...manualItemNodes, ...autoRetrievedNodes]));
|
||||
}
|
||||
|
||||
const writingMode = getWritingMode();
|
||||
|
||||
// 先构建各模块的原始文本
|
||||
const rawModules = {
|
||||
bookInfo: (selectedIds && selectedIds.has('bookinfo')) ? buildBookInfoContext(settings.bookInfo) : '',
|
||||
bookInfo: (!selectedIds || selectedIds.has('bookinfo')) ? buildBookInfoContext(settings.bookInfo) : '',
|
||||
characters: buildCharactersContext(finalItemNodes.filter(n => n.category === 'character')),
|
||||
locations: buildLocationsContext(finalItemNodes.filter(n => n.category === 'location'), nodes),
|
||||
worldbuilding: buildWorldContext(finalItemNodes.filter(n => n.category === 'world'), nodes),
|
||||
@@ -596,17 +591,6 @@ function buildPreviousContext(chapters, currentIndex) {
|
||||
return prevChapters.map((ch, i) => {
|
||||
const text = stripHtml(ch.content || '');
|
||||
if (!text) return `第${i + 1}章「${ch.title}」:(空)`;
|
||||
|
||||
if (i === currentIndex - 1) {
|
||||
// 前一章:保留更多内容
|
||||
const excerpt = text.length > 1500 ? '...' + text.slice(-1500) : text;
|
||||
return `第${i + 1}章「${ch.title}」(前一章完整回顾):\n${excerpt}`;
|
||||
}
|
||||
|
||||
// 远距章节:只保留简要摘要
|
||||
if (text.length > 300) {
|
||||
return `第${i + 1}章「${ch.title}」:\n${text.slice(0, 150)}…(中间省略)…${text.slice(-100)}`;
|
||||
}
|
||||
return `第${i + 1}章「${ch.title}」:\n${text}`;
|
||||
}).join('\n\n');
|
||||
}
|
||||
@@ -620,15 +604,6 @@ function buildPreviousContextFiltered(chapters, currentIndex, selectedIds) {
|
||||
const i = chapters.indexOf(ch);
|
||||
const text = stripHtml(ch.content || '');
|
||||
if (!text) return `第${i + 1}章「${ch.title}」:(空)`;
|
||||
|
||||
if (i === currentIndex - 1) {
|
||||
const excerpt = text.length > 1500 ? '...' + text.slice(-1500) : text;
|
||||
return `第${i + 1}章「${ch.title}」(前一章完整回顾):\n${excerpt}`;
|
||||
}
|
||||
|
||||
if (text.length > 300) {
|
||||
return `第${i + 1}章「${ch.title}」:\n${text.slice(0, 150)}…(中间省略)…${text.slice(-100)}`;
|
||||
}
|
||||
return `第${i + 1}章「${ch.title}」:\n${text}`;
|
||||
}).join('\n\n');
|
||||
}
|
||||
@@ -699,11 +674,9 @@ function getModeInstruction(mode) {
|
||||
【设定集管理能力】
|
||||
当用户要求你创建、修改或删除设定集条目时,你可以在回复中嵌入操作指令块。格式如下:
|
||||
|
||||
\`\`\`
|
||||
[SETTINGS_ACTION]
|
||||
{"action":"add","category":"character","name":"角色姓名","content":{"role":"主角","personality":"...","background":"..."}}
|
||||
[/SETTINGS_ACTION]
|
||||
\`\`\`
|
||||
|
||||
可用的 action: "add"(新增)、"update"(更新,需提供 nodeId)、"delete"(删除,需提供 nodeId)
|
||||
|
||||
@@ -720,6 +693,7 @@ function getModeInstruction(mode) {
|
||||
- 每个操作块只包含一个 JSON 对象
|
||||
- 如果需要多个操作,使用多个 [SETTINGS_ACTION] 块
|
||||
- 操作块前后必须有正常的文字说明
|
||||
- 不要用代码围栏(\`\`\`)包裹操作块,直接使用 [SETTINGS_ACTION] 标签
|
||||
- update 示例:{"action":"update","nodeId":"具体id","name":"新名称","content":{...}}
|
||||
- delete 示例:{"action":"delete","nodeId":"具体id"}
|
||||
- 在正文中已有角色/设定出现时,如果用户要求,可以从正文分析内容并自动创建设定`;
|
||||
|
||||
+11
-2
@@ -499,8 +499,12 @@ export async function addSettingsNode({ name, type, category, parentId, icon, co
|
||||
if (node.type === 'item') {
|
||||
const { apiConfig } = getProjectSettings();
|
||||
if (apiConfig.useCustomEmbed) {
|
||||
const textToEmbed = extractTextForEmbedding(node);
|
||||
node.embedding = await getEmbedding(textToEmbed, apiConfig);
|
||||
try {
|
||||
const textToEmbed = extractTextForEmbedding(node);
|
||||
node.embedding = await getEmbedding(textToEmbed, apiConfig);
|
||||
} catch (e) {
|
||||
console.warn('[Settings] Embedding failed for new node, will retry later:', e.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -594,6 +598,7 @@ export async function moveSettingsNode(id, newParentId) {
|
||||
}
|
||||
|
||||
// 重新计算所有条目的 embedding (Async)
|
||||
// 每次请求间隔 500ms 以避免超出 TPM 限制
|
||||
export async function rebuildAllEmbeddings(onProgress) {
|
||||
const nodes = await getSettingsNodes();
|
||||
const { apiConfig } = getProjectSettings();
|
||||
@@ -616,6 +621,10 @@ export async function rebuildAllEmbeddings(onProgress) {
|
||||
}
|
||||
done++;
|
||||
onProgress?.(done, items.length, failed);
|
||||
// 请求间隔:避免超出 TPM / RPM 限制
|
||||
if (done < items.length) {
|
||||
await new Promise(r => setTimeout(r, 700));
|
||||
}
|
||||
}
|
||||
|
||||
await saveSettingsNodes(nodes);
|
||||
|
||||
+2
-2
@@ -2,8 +2,8 @@
|
||||
const nextConfig = {
|
||||
output: 'standalone',
|
||||
devIndicators: false,
|
||||
// pdf-parse 仅在服务端 API 路由中使用,将其标记为外部包避免打包解析
|
||||
serverExternalPackages: ['pdf-parse'],
|
||||
// pdf-parse / word-extractor 仅在服务端 API 路由中使用,标记为外部包避免打包解析
|
||||
serverExternalPackages: ['pdf-parse', 'word-extractor'],
|
||||
};
|
||||
|
||||
export default nextConfig;
|
||||
Generated
+10
-9
@@ -1,13 +1,14 @@
|
||||
{
|
||||
"name": "author-app",
|
||||
"version": "1.1.4",
|
||||
"version": "1.1.10",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "author-app",
|
||||
"version": "1.1.4",
|
||||
"version": "1.1.10",
|
||||
"dependencies": {
|
||||
"@tiptap/core": "^3.20.0",
|
||||
"@tiptap/extension-character-count": "^3.19.0",
|
||||
"@tiptap/extension-color": "^3.19.0",
|
||||
"@tiptap/extension-font-family": "^3.19.0",
|
||||
@@ -2300,16 +2301,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@tiptap/core": {
|
||||
"version": "3.19.0",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/core/-/core-3.19.0.tgz",
|
||||
"integrity": "sha512-bpqELwPW+DG8gWiD8iiFtSl4vIBooG5uVJod92Qxn3rA9nFatyXRr4kNbMJmOZ66ezUvmCjXVe/5/G4i5cyzKA==",
|
||||
"version": "3.20.0",
|
||||
"resolved": "https://registry.npmmirror.com/@tiptap/core/-/core-3.20.0.tgz",
|
||||
"integrity": "sha512-aC9aROgia/SpJqhsXFiX9TsligL8d+oeoI8W3u00WI45s0VfsqjgeKQLDLF7Tu7hC+7F02teC84SAHuup003VQ==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ueberdosis"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@tiptap/pm": "^3.19.0"
|
||||
"@tiptap/pm": "^3.20.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tiptap/extension-blockquote": {
|
||||
@@ -2786,9 +2787,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@tiptap/pm": {
|
||||
"version": "3.19.0",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/pm/-/pm-3.19.0.tgz",
|
||||
"integrity": "sha512-789zcnM4a8OWzvbD2DL31d0wbSm9BVeO/R7PLQwLIGysDI3qzrcclyZ8yhqOEVuvPitRRwYLq+mY14jz7kY4cw==",
|
||||
"version": "3.20.0",
|
||||
"resolved": "https://registry.npmmirror.com/@tiptap/pm/-/pm-3.20.0.tgz",
|
||||
"integrity": "sha512-jn+2KnQZn+b+VXr8EFOJKsnjVNaA4diAEr6FOazupMt8W8ro1hfpYtZ25JL87Kao/WbMze55sd8M8BDXLUKu1A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"prosemirror-changeset": "^2.3.0",
|
||||
|
||||
+2
-2
@@ -13,7 +13,7 @@
|
||||
"electron:start": "npx electron ."
|
||||
},
|
||||
"dependencies": {
|
||||
"@tiptap/core": "^3.19.0",
|
||||
"@tiptap/core": "^3.20.0",
|
||||
"@tiptap/extension-character-count": "^3.19.0",
|
||||
"@tiptap/extension-color": "^3.19.0",
|
||||
"@tiptap/extension-font-family": "^3.19.0",
|
||||
@@ -108,4 +108,4 @@
|
||||
"createStartMenuShortcut": true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user