style: 代码格式优化

This commit is contained in:
digua
2026-04-06 21:04:05 +08:00
committed by digua
parent bfc96a723d
commit 3189de3a6e
34 changed files with 218 additions and 261 deletions

View File

@@ -37,13 +37,13 @@ Response example:
## General Information
| Item | Description |
|------|-------------|
| Base URL | `http://127.0.0.1:5200` |
| API Prefix | `/api/v1` |
| Authentication | Bearer Token |
| Data Format | JSON |
| Bind Address | `127.0.0.1` (localhost only) |
| Item | Description |
| -------------- | ---------------------------- |
| Base URL | `http://127.0.0.1:5200` |
| API Prefix | `/api/v1` |
| Authentication | Bearer Token |
| Data Format | JSON |
| Bind Address | `127.0.0.1` (localhost only) |
### Authentication
@@ -88,29 +88,29 @@ The Token can be viewed and regenerated in Settings → ChatLab API.
### System
| Method | Path | Description |
|--------|------|-------------|
| GET | `/api/v1/status` | Service status |
| GET | `/api/v1/schema` | ChatLab Format JSON Schema |
| Method | Path | Description |
| ------ | ---------------- | -------------------------- |
| GET | `/api/v1/status` | Service status |
| GET | `/api/v1/schema` | ChatLab Format JSON Schema |
### Data Query (Export)
| Method | Path | Description |
|--------|------|-------------|
| GET | `/api/v1/sessions` | List all sessions |
| GET | `/api/v1/sessions/:id` | Get single session details |
| GET | `/api/v1/sessions/:id/messages` | Query messages (paginated) |
| GET | `/api/v1/sessions/:id/members` | Get member list |
| GET | `/api/v1/sessions/:id/stats/overview` | Get overview statistics |
| POST | `/api/v1/sessions/:id/sql` | Execute custom SQL (read-only) |
| GET | `/api/v1/sessions/:id/export` | Export ChatLab Format JSON |
| Method | Path | Description |
| ------ | ------------------------------------- | ------------------------------ |
| GET | `/api/v1/sessions` | List all sessions |
| GET | `/api/v1/sessions/:id` | Get single session details |
| GET | `/api/v1/sessions/:id/messages` | Query messages (paginated) |
| GET | `/api/v1/sessions/:id/members` | Get member list |
| GET | `/api/v1/sessions/:id/stats/overview` | Get overview statistics |
| POST | `/api/v1/sessions/:id/sql` | Execute custom SQL (read-only) |
| GET | `/api/v1/sessions/:id/export` | Export ChatLab Format JSON |
### Data Import
| Method | Path | Description |
|--------|------|-------------|
| POST | `/api/v1/import` | Import chat records (new session) |
| POST | `/api/v1/sessions/:id/import` | Incremental import to existing session |
| Method | Path | Description |
| ------ | ----------------------------- | -------------------------------------- |
| POST | `/api/v1/import` | Import chat records (new session) |
| POST | `/api/v1/sessions/:id/import` | Incremental import to existing session |
---
@@ -122,12 +122,12 @@ Get the running status of the API service.
**Response:**
| Field | Type | Description |
|-------|------|-------------|
| `name` | string | Service name (`ChatLab API`) |
| `version` | string | ChatLab application version |
| `uptime` | number | Service uptime in seconds |
| `sessionCount` | number | Total number of sessions |
| Field | Type | Description |
| -------------- | ------ | ---------------------------- |
| `name` | string | Service name (`ChatLab API`) |
| `version` | string | ChatLab application version |
| `uptime` | number | Service uptime in seconds |
| `sessionCount` | number | Total number of sessions |
---
@@ -167,9 +167,9 @@ Get detailed information for a single session.
**Path parameters:**
| Parameter | Type | Description |
|-----------|------|-------------|
| `id` | string | Session ID |
| Parameter | Type | Description |
| --------- | ------ | ----------- |
| `id` | string | Session ID |
---
@@ -179,15 +179,15 @@ Query messages from a specific session with pagination and filtering support.
**Query parameters:**
| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `page` | number | 1 | Page number |
| `limit` | number | 100 | Items per page (max 1000) |
| `startTime` | number | - | Start timestamp (Unix seconds) |
| `endTime` | number | - | End timestamp (Unix seconds) |
| `keyword` | string | - | Keyword search |
| `senderId` | string | - | Filter by sender's platformId |
| `type` | number | - | Filter by message type |
| Parameter | Type | Default | Description |
| ----------- | ------ | ------- | ------------------------------ |
| `page` | number | 1 | Page number |
| `limit` | number | 100 | Items per page (max 1000) |
| `startTime` | number | - | Start timestamp (Unix seconds) |
| `endTime` | number | - | End timestamp (Unix seconds) |
| `keyword` | string | - | Keyword search |
| `senderId` | string | - | Filter by sender's platformId |
| `type` | number | - | Filter by message type |
**Request example:**
@@ -262,7 +262,7 @@ Get overview statistics for a specific session.
```
| Field | Description |
|-------|-------------|
| --- | --- |
| `messageCount` | Total message count |
| `memberCount` | Total member count |
| `timeRange` | Earliest/latest message timestamps (Unix seconds) |
@@ -339,7 +339,7 @@ Import chat records into ChatLab, **creating a new session**.
#### Supported Content-Types
| Content-Type | Format | Use Case | Body Limit |
|------|------|------|------|
| --- | --- | --- | --- |
| `application/json` | ChatLab Format JSON | Small to medium data (quick testing, script integration) | **50MB** |
| `application/x-ndjson` | ChatLab JSONL format | Large-scale data (production integration) | **Unlimited** |
@@ -409,9 +409,9 @@ The unique key for each message is `timestamp + senderPlatformId + contentLength
**Path parameters:**
| Parameter | Type | Description |
|-----------|------|-------------|
| `id` | string | Target session ID |
| Parameter | Type | Description |
| --------- | ------ | ----------------- |
| `id` | string | Target session ID |
Content-Type and request body format are the same as `POST /api/v1/import`.
@@ -432,30 +432,30 @@ Content-Type and request body format are the same as `POST /api/v1/import`.
## Concurrency & Limits
| Limit | Value | Description |
|-------|-------|-------------|
| JSON body size | 50MB | `application/json` mode |
| JSONL body size | Unlimited | `application/x-ndjson` streaming mode |
| Export message limit | 100,000 | `/export` endpoint |
| Max page size | 1,000 | `/messages` endpoint |
| Import concurrency | 1 | Only one import operation allowed at a time |
| Limit | Value | Description |
| -------------------- | --------- | ------------------------------------------- |
| JSON body size | 50MB | `application/json` mode |
| JSONL body size | Unlimited | `application/x-ndjson` streaming mode |
| Export message limit | 100,000 | `/export` endpoint |
| Max page size | 1,000 | `/messages` endpoint |
| Import concurrency | 1 | Only one import operation allowed at a time |
---
## Error Codes
| Error Code | HTTP Status | Description |
|------------|-------------|-------------|
| `UNAUTHORIZED` | 401 | Invalid or missing token |
| `SESSION_NOT_FOUND` | 404 | Session not found |
| `INVALID_FORMAT` | 400 | Request body does not conform to ChatLab Format |
| `SQL_READONLY_VIOLATION` | 400 | SQL is not a SELECT statement |
| `SQL_EXECUTION_ERROR` | 400 | SQL execution error |
| `EXPORT_TOO_LARGE` | 400 | Message count exceeds export limit (100K) |
| `BODY_TOO_LARGE` | 413 | Request body exceeds 50MB (JSON mode only) |
| `IMPORT_IN_PROGRESS` | 409 | Another import is already in progress |
| `IMPORT_FAILED` | 500 | Import failed |
| `SERVER_ERROR` | 500 | Internal server error |
| Error Code | HTTP Status | Description |
| ------------------------ | ----------- | ----------------------------------------------- |
| `UNAUTHORIZED` | 401 | Invalid or missing token |
| `SESSION_NOT_FOUND` | 404 | Session not found |
| `INVALID_FORMAT` | 400 | Request body does not conform to ChatLab Format |
| `SQL_READONLY_VIOLATION` | 400 | SQL is not a SELECT statement |
| `SQL_EXECUTION_ERROR` | 400 | SQL execution error |
| `EXPORT_TOO_LARGE` | 400 | Message count exceeds export limit (100K) |
| `BODY_TOO_LARGE` | 413 | Request body exceeds 50MB (JSON mode only) |
| `IMPORT_IN_PROGRESS` | 409 | Another import is already in progress |
| `IMPORT_FAILED` | 500 | Import failed |
| `SERVER_ERROR` | 500 | Internal server error |
---
@@ -494,6 +494,6 @@ Configure external data source URLs in the Settings page. ChatLab will automatic
## Version History
| Version | Description |
|---------|-------------|
| v1 | Initial release — session query, message search, SQL, export, import (JSON + JSONL), Pull scheduler |
| Version | Description |
| ------- | --------------------------------------------------------------------------------------------------- |
| v1 | Initial release — session query, message search, SQL, export, import (JSON + JSONL), Pull scheduler |

View File

@@ -10,10 +10,10 @@ presetQuestions:
- Analyze the active hours of the chat
---
You are a professional yet friendly chat record analysis assistant.
Your job is to help users understand and analyze their chat history data. You can occasionally use light humor to keep the conversation engaging, but never at the expense of accuracy.
You are a professional yet friendly chat record analysis assistant. Your job is to help users understand and analyze their chat history data. You can occasionally use light humor to keep the conversation engaging, but never at the expense of accuracy.
## Response Guidelines
1. Base your answers on data returned by tools — never fabricate information
2. If the data is insufficient to answer a question, say so clearly
3. Keep responses concise and use Markdown formatting

View File

@@ -10,10 +10,10 @@ presetQuestions:
- チャットの活発な時間帯を分析して
---
あなたはプロフェッショナルでありながら親しみやすいチャット記録分析アシスタントです。
ユーザーのチャット履歴データを理解し分析する手助けをすることが主な役割です。適度にユーモアを交えても構いませんが、分析の正確性を最優先にしてください。
あなたはプロフェッショナルでありながら親しみやすいチャット記録分析アシスタントです。ユーザーのチャット履歴データを理解し分析する手助けをすることが主な役割です。適度にユーモアを交えても構いませんが、分析の正確性を最優先にしてください。
## 回答ガイドライン
1. ツールから返されたデータに基づいて回答し、情報を捏造しないこと
2. データが不十分な場合はその旨を明確に伝えること
3. 簡潔で分かりやすい回答を心がけ、Markdown形式を使用すること

View File

@@ -189,4 +189,3 @@ export interface AssistantSaveResult {
success: boolean
error?: string
}

View File

@@ -2,13 +2,7 @@
* 技能系统模块入口
*/
export type {
SkillDef,
SkillSummary,
BuiltinSkillInfo,
SkillInitResult,
SkillSaveResult,
} from './types'
export type { SkillDef, SkillSummary, BuiltinSkillInfo, SkillInitResult, SkillSaveResult } from './types'
export {
initSkillManager,

View File

@@ -16,13 +16,7 @@ import { createHash } from 'crypto'
import { getAiDataDir, ensureDir } from '../../paths'
import { aiLogger } from '../logger'
import { parseSkillFile } from './parser'
import type {
SkillDef,
SkillSummary,
SkillInitResult,
SkillSaveResult,
BuiltinSkillInfo,
} from './types'
import type { SkillDef, SkillSummary, SkillInitResult, SkillSaveResult, BuiltinSkillInfo } from './types'
// ==================== 内置技能模板 ====================
// 云端市场上线后,本地不再内置技能模板,全部从云端获取
@@ -271,10 +265,7 @@ const MAX_SKILL_MENU_ITEMS = 15
* 构建 AI 自选技能菜单文本
* 只包含与当前 chatType + 助手工具权限兼容的技能
*/
export function getSkillMenu(
chatType: 'group' | 'private',
allowedTools?: string[]
): string | null {
export function getSkillMenu(chatType: 'group' | 'private', allowedTools?: string[]): string | null {
ensureInitialized()
const compatible = Array.from(cachedSkills.values()).filter((skill) => {

View File

@@ -40,7 +40,11 @@ export function createTool(context: ToolContext): AgentTool<typeof schema> {
}
const texts = rows.map((r) => r.content)
const segLocale: SupportedLocale = locale?.startsWith('ja') ? 'ja-JP' : locale?.startsWith('zh') ? 'zh-CN' : 'en-US'
const segLocale: SupportedLocale = locale?.startsWith('ja')
? 'ja-JP'
: locale?.startsWith('zh')
? 'zh-CN'
: 'en-US'
const segOptions: BatchSegmentOptions = {
minCount: 2,
topN,

View File

@@ -37,7 +37,9 @@ export function createTool(context: ToolContext): AgentTool<typeof schema> {
`
const rows = await workerManager.pluginQuery<MsgRow>(sessionId, sql, { days })
if (!rows || rows.length < 2) {
const text = isZh ? '该时间范围内消息不足,无法分析响应时间' : 'Not enough messages in this time range to analyze response time'
const text = isZh
? '该时间范围内消息不足,无法分析响应时间'
: 'Not enough messages in this time range to analyze response time'
return { content: [{ type: 'text', text }], details: null }
}

View File

@@ -15,8 +15,7 @@ const SQL_TOOL_DEFS: CustomSqlToolDef[] = [
// ==================== 通用分析 ====================
{
name: 'message_type_breakdown',
description:
'按消息类型统计近 N 天的消息分布(文本、图片、语音、表情等各有多少条)。适用于了解沟通方式偏好。',
description: '按消息类型统计近 N 天的消息分布(文本、图片、语音、表情等各有多少条)。适用于了解沟通方式偏好。',
parameters: {
type: 'object',
properties: {
@@ -183,8 +182,7 @@ const SQL_TOOL_DEFS: CustomSqlToolDef[] = [
},
{
name: 'conversation_initiator_stats',
description:
'统计每个成员发起会话(作为会话首条消息的发送者)的次数,找出谁最常开启话题。需要已生成会话索引。',
description: '统计每个成员发起会话(作为会话首条消息的发送者)的次数,找出谁最常开启话题。需要已生成会话索引。',
parameters: {
type: 'object',
properties: {
@@ -204,8 +202,7 @@ const SQL_TOOL_DEFS: CustomSqlToolDef[] = [
},
{
name: 'activity_heatmap',
description:
'返回 星期×小时 的消息数矩阵适合生成活跃度热力图。weekday: 0=周日, 1=周一, ..., 6=周六。',
description: '返回 星期×小时 的消息数矩阵适合生成活跃度热力图。weekday: 0=周日, 1=周一, ..., 6=周六。',
parameters: {
type: 'object',
properties: {

View File

@@ -64,7 +64,9 @@ export function generateId(): string {
return `ds_${crypto.randomBytes(6).toString('hex')}`
}
export function addDataSource(partial: Omit<DataSource, 'id' | 'createdAt' | 'lastPullAt' | 'lastStatus' | 'lastError' | 'lastNewMessages'>): DataSource {
export function addDataSource(
partial: Omit<DataSource, 'id' | 'createdAt' | 'lastPullAt' | 'lastStatus' | 'lastError' | 'lastNewMessages'>
): DataSource {
const sources = loadDataSources()
const ds: DataSource = {
...partial,

View File

@@ -23,7 +23,9 @@ function getTempFilePath(ext: string): string {
function cleanupTempFile(filePath: string): void {
try {
if (fs.existsSync(filePath)) fs.unlinkSync(filePath)
} catch { /* ignore */ }
} catch {
/* ignore */
}
}
function notifySessionListChanged(): void {
@@ -33,7 +35,9 @@ function notifySessionListChanged(): void {
for (const win of wins) {
win.webContents.send('api:importCompleted')
}
} catch { /* ignore */ }
} catch {
/* ignore */
}
}
function notifyPullResult(dsId: string, status: 'success' | 'error', detail: string): void {
@@ -43,7 +47,9 @@ function notifyPullResult(dsId: string, status: 'success' | 'error', detail: str
for (const win of wins) {
win.webContents.send('api:pullResult', { dsId, status, detail })
}
} catch { /* ignore */ }
} catch {
/* ignore */
}
}
/**
@@ -51,9 +57,7 @@ function notifyPullResult(dsId: string, status: 'success' | 'error', detail: str
*/
async function fetchToTempFile(ds: DataSource): Promise<string> {
return new Promise<string>((resolve, reject) => {
const url = ds.url.includes('?')
? `${ds.url}&since=${ds.lastPullAt}`
: `${ds.url}?since=${ds.lastPullAt}`
const url = ds.url.includes('?') ? `${ds.url}&since=${ds.lastPullAt}` : `${ds.url}?since=${ds.lastPullAt}`
const request = net.request(url)
@@ -140,7 +144,9 @@ async function executePull(ds: DataSource): Promise<void> {
if (result.success) {
try {
await worker.generateIncrementalSessions(ds.targetSessionId)
} catch { /* ignore */ }
} catch {
/* ignore */
}
}
} else {
result = await worker.streamImport(tempFile)

View File

@@ -62,11 +62,7 @@ export function getImportingStatus(): boolean {
return isImporting
}
async function handleImport(
request: FastifyRequest,
reply: FastifyReply,
sessionId?: string
): Promise<void> {
async function handleImport(request: FastifyRequest, reply: FastifyReply, sessionId?: string): Promise<void> {
if (isImporting) {
const err = importInProgress()
reply.code(err.statusCode).send(errorResponse(err))
@@ -174,12 +170,9 @@ async function handleImport(
export function registerImportRoutes(server: FastifyInstance): void {
// JSONL mode: skip fastify's default body parsing, use request.raw stream directly
server.addContentTypeParser(
'application/x-ndjson',
(_request, _payload, done) => {
done(null, undefined)
}
)
server.addContentTypeParser('application/x-ndjson', (_request, _payload, done) => {
done(null, undefined)
})
// POST /api/v1/import — Import to new session
server.post('/api/v1/import', async (request, reply) => {
@@ -187,10 +180,7 @@ export function registerImportRoutes(server: FastifyInstance): void {
})
// POST /api/v1/sessions/:id/import — Incremental import to existing session
server.post<{ Params: { id: string } }>(
'/api/v1/sessions/:id/import',
async (request, reply) => {
await handleImport(request, reply, request.params.id)
}
)
server.post<{ Params: { id: string } }>('/api/v1/sessions/:id/import', async (request, reply) => {
await handleImport(request, reply, request.params.id)
})
}

View File

@@ -57,24 +57,15 @@ export function registerSessionRoutes(server: FastifyInstance): void {
const keywords = keyword ? [keyword] : []
const senderIdNum = senderId ? parseInt(senderId, 10) : undefined
const result = await worker.searchMessages(
id,
keywords,
hasFilter ? filter : undefined,
limit,
offset,
senderIdNum
)
const result = await worker.searchMessages(id, keywords, hasFilter ? filter : undefined, limit, offset, senderIdNum)
return successResponse(
{
messages: result.messages,
total: result.total,
page,
limit,
totalPages: Math.ceil(result.total / limit),
}
)
return successResponse({
messages: result.messages,
total: result.total,
page,
limit,
totalPages: Math.ceil(result.total / limit),
})
})
// GET /api/v1/sessions/:id/members — Member list
@@ -117,33 +108,30 @@ export function registerSessionRoutes(server: FastifyInstance): void {
})
// POST /api/v1/sessions/:id/sql — Execute SQL (read-only)
server.post<{ Params: { id: string }; Body: { sql: string } }>(
'/api/v1/sessions/:id/sql',
async (request, reply) => {
const { id } = request.params
await ensureSession(id)
server.post<{ Params: { id: string }; Body: { sql: string } }>('/api/v1/sessions/:id/sql', async (request, reply) => {
const { id } = request.params
await ensureSession(id)
const { sql } = request.body || {}
if (!sql || typeof sql !== 'string') {
const err = sqlExecutionError('Missing sql parameter')
return reply.code(err.statusCode).send(errorResponse(err))
}
try {
const result = await worker.executeRawSQL(id, sql)
return successResponse(result)
} catch (err: any) {
const message = err.message || 'SQL execution error'
if (message.includes('SELECT') || message.includes('只读') || message.includes('readonly')) {
const apiErr = new ApiError('SQL_READONLY_VIOLATION' as any, message)
apiErr.statusCode = 400
return reply.code(400).send(errorResponse(apiErr))
}
const apiErr = sqlExecutionError(message)
return reply.code(apiErr.statusCode).send(errorResponse(apiErr))
}
const { sql } = request.body || {}
if (!sql || typeof sql !== 'string') {
const err = sqlExecutionError('Missing sql parameter')
return reply.code(err.statusCode).send(errorResponse(err))
}
)
try {
const result = await worker.executeRawSQL(id, sql)
return successResponse(result)
} catch (err: any) {
const message = err.message || 'SQL execution error'
if (message.includes('SELECT') || message.includes('只读') || message.includes('readonly')) {
const apiErr = new ApiError('SQL_READONLY_VIOLATION' as any, message)
apiErr.statusCode = 400
return reply.code(400).send(errorResponse(apiErr))
}
const apiErr = sqlExecutionError(message)
return reply.code(apiErr.statusCode).send(errorResponse(apiErr))
}
})
// GET /api/v1/sessions/:id/export — Export ChatLab Format JSON
server.get<{ Params: { id: string } }>('/api/v1/sessions/:id/export', async (request, reply) => {

View File

@@ -46,7 +46,10 @@ export function registerSystemRoutes(server: FastifyInstance): void {
required: ['name', 'platform', 'type'],
properties: {
name: { type: 'string' },
platform: { type: 'string', enum: ['qq', 'wechat', 'telegram', 'discord', 'line', 'whatsapp', 'instagram', 'unknown'] },
platform: {
type: 'string',
enum: ['qq', 'wechat', 'telegram', 'discord', 'line', 'whatsapp', 'instagram', 'unknown'],
},
type: { type: 'string', enum: ['group', 'private'] },
groupId: { type: 'string' },
},

View File

@@ -144,9 +144,7 @@ const migrations: Migration[] = [
descriptionKey: 'database.migrationV4Desc',
userMessageKey: 'database.migrationV4Message',
up: (db) => {
const hasTable = db
.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='message_fts'")
.get()
const hasTable = db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='message_fts'").get()
if (hasTable) return
db.exec(`
@@ -161,9 +159,7 @@ const migrations: Migration[] = [
const insertFts = db.prepare('INSERT INTO message_fts(rowid, content) VALUES (?, ?)')
const countRow = db
.prepare(
"SELECT COUNT(*) as total FROM message WHERE type = 0 AND content IS NOT NULL AND content != ''"
)
.prepare("SELECT COUNT(*) as total FROM message WHERE type = 0 AND content IS NOT NULL AND content != ''")
.get() as { total: number }
let offset = 0

View File

@@ -111,11 +111,7 @@ export interface OverviewCache {
/**
* 从数据库计算概览统计并写入缓存
*/
export function computeAndSetOverviewCache(
db: Database.Database,
sessionId: string,
cacheDir: string
): OverviewCache {
export function computeAndSetOverviewCache(db: Database.Database, sessionId: string, cacheDir: string): OverviewCache {
const msgStats = db.prepare('SELECT MIN(ts) as first_ts, MAX(ts) as last_ts FROM message').get() as {
first_ts: number | null
last_ts: number | null
@@ -169,11 +165,7 @@ export interface MembersCache {
/**
* 从数据库计算成员统计并写入缓存
*/
export function computeAndSetMembersCache(
db: Database.Database,
sessionId: string,
cacheDir: string
): MembersCache {
export function computeAndSetMembersCache(db: Database.Database, sessionId: string, cacheDir: string): MembersCache {
const rows = db
.prepare(
`SELECT msg.sender_id, COUNT(*) as count,

View File

@@ -6,19 +6,8 @@ import { ipcMain } from 'electron'
import type { IpcContext } from './types'
import * as apiServer from '../api'
import { loadConfig, regenerateToken, updateConfig } from '../api/config'
import {
loadDataSources,
addDataSource,
updateDataSource,
deleteDataSource,
type DataSource,
} from '../api/dataSource'
import {
initScheduler,
stopAllTimers,
reloadTimer,
triggerPull,
} from '../api/pullScheduler'
import { loadDataSources, addDataSource, updateDataSource, deleteDataSource, type DataSource } from '../api/dataSource'
import { initScheduler, stopAllTimers, reloadTimer, triggerPull } from '../api/pullScheduler'
export function registerApiHandlers(_ctx: IpcContext): void {
// ==================== API Server Management ====================
@@ -63,10 +52,7 @@ export function registerApiHandlers(_ctx: IpcContext): void {
'api:addDataSource',
(
_event,
partial: Omit<
DataSource,
'id' | 'createdAt' | 'lastPullAt' | 'lastStatus' | 'lastError' | 'lastNewMessages'
>
partial: Omit<DataSource, 'id' | 'createdAt' | 'lastPullAt' | 'lastStatus' | 'lastError' | 'lastNewMessages'>
) => {
const ds = addDataSource(partial)
if (ds.enabled) {

View File

@@ -394,9 +394,10 @@ export function getTimeRange(sessionId: string): { start: number; end: number }
const db = openDatabase(sessionId)
if (!db) return null
const row = db
.prepare('SELECT MIN(ts) as start, MAX(ts) as end FROM message')
.get() as { start: number | null; end: number | null }
const row = db.prepare('SELECT MIN(ts) as start, MAX(ts) as end FROM message').get() as {
start: number | null
end: number | null
}
if (row.start === null || row.end === null) return null

View File

@@ -43,9 +43,9 @@ export function hasFtsIndex(sessionId: string): boolean {
const db = openDatabase(sessionId)
if (!db) return false
try {
const row = db
.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='message_fts'")
.get() as { name: string } | undefined
const row = db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='message_fts'").get() as
| { name: string }
| undefined
return !!row
} catch {
return false
@@ -72,9 +72,9 @@ export function buildFtsIndex(sessionId: string): { indexed: number } {
const insertFts = db.prepare('INSERT INTO message_fts(rowid, content) VALUES (?, ?)')
const countRow = db.prepare(
"SELECT COUNT(*) as total FROM message WHERE type = 0 AND content IS NOT NULL AND content != ''"
).get() as { total: number }
const countRow = db
.prepare("SELECT COUNT(*) as total FROM message WHERE type = 0 AND content IS NOT NULL AND content != ''")
.get() as { total: number }
const total = countRow.total
let indexed = 0
@@ -121,9 +121,7 @@ export function rebuildFtsIndex(sessionId: string): { indexed: number } {
if (!db) return { indexed: 0 }
try {
const hasTable = db
.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='message_fts'")
.get()
const hasTable = db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='message_fts'").get()
if (hasTable) {
db.exec('DROP TABLE message_fts')
@@ -142,17 +140,12 @@ export function rebuildFtsIndex(sessionId: string): { indexed: number } {
* 批量写入 FTS 条目
* 用于增量导入时同步写入
*/
export function insertFtsEntries(
sessionId: string,
entries: Array<{ id: number; content: string | null }>
): void {
export function insertFtsEntries(sessionId: string, entries: Array<{ id: number; content: string | null }>): void {
const db = openWritableDb(sessionId)
if (!db) return
try {
const hasTable = db
.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='message_fts'")
.get()
const hasTable = db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='message_fts'").get()
if (!hasTable) {
db.close()
return
@@ -194,14 +187,12 @@ export function searchByFts(
if (!matchQuery) return { rowids: [], total: 0 }
try {
const countRow = db
.prepare('SELECT COUNT(*) as total FROM message_fts WHERE content MATCH ?')
.get(matchQuery) as { total: number }
const countRow = db.prepare('SELECT COUNT(*) as total FROM message_fts WHERE content MATCH ?').get(matchQuery) as {
total: number
}
const rows = db
.prepare(
`SELECT rowid FROM message_fts WHERE content MATCH ? ORDER BY rank LIMIT ? OFFSET ?`
)
.prepare(`SELECT rowid FROM message_fts WHERE content MATCH ? ORDER BY rank LIMIT ? OFFSET ?`)
.all(matchQuery, limit, offset) as Array<{ rowid: number }>
return {

View File

@@ -345,9 +345,7 @@ function searchMessagesWithFts(
LIMIT ? OFFSET ?
`
const rows = db
.prepare(sql)
.all(matchQuery, ...timeParams, ...senderParams, limit, offset) as DbMessageRow[]
const rows = db.prepare(sql).all(matchQuery, ...timeParams, ...senderParams, limit, offset) as DbMessageRow[]
return {
messages: rows.map(sanitizeMessageRow),

View File

@@ -68,7 +68,9 @@ export const apiServerApi = {
return ipcRenderer.invoke('api:getDataSources')
},
addDataSource: (partial: Omit<DataSource, 'id' | 'createdAt' | 'lastPullAt' | 'lastStatus' | 'lastError' | 'lastNewMessages'>): Promise<DataSource> => {
addDataSource: (
partial: Omit<DataSource, 'id' | 'createdAt' | 'lastPullAt' | 'lastStatus' | 'lastError' | 'lastNewMessages'>
): Promise<DataSource> => {
return ipcRenderer.invoke('api:addDataSource', partial)
},

View File

@@ -916,7 +916,9 @@ interface ApiServerApi {
regenerateToken: () => Promise<ApiServerConfig>
onStartupError: (callback: (data: { error: string }) => void) => () => void
getDataSources: () => Promise<DataSource[]>
addDataSource: (partial: Omit<DataSource, 'id' | 'createdAt' | 'lastPullAt' | 'lastStatus' | 'lastError' | 'lastNewMessages'>) => Promise<DataSource>
addDataSource: (
partial: Omit<DataSource, 'id' | 'createdAt' | 'lastPullAt' | 'lastStatus' | 'lastError' | 'lastNewMessages'>
) => Promise<DataSource>
updateDataSource: (id: string, updates: Partial<DataSource>) => Promise<DataSource | null>
deleteDataSource: (id: string) => Promise<boolean>
triggerPull: (id: string) => Promise<{ success: boolean; error?: string }>

View File

@@ -121,10 +121,12 @@ description: Use when 用户希望根据一句自然语言需求创建新的 Cha
## 工具选择规则
工具分为两类:
- **核心工具core**:始终启用,无需在 `allowedBuiltinTools` 中列出。包括get_chat_overview, search_messages, get_recent_messages, get_message_context, search_sessions, get_session_messages, get_members
- **分析工具analysis**:需在 `allowedBuiltinTools` 中显式列出才会启用
`allowedBuiltinTools` 仅用于控制分析工具,核心工具始终可用:
- 若角色不需要分析工具,可省略 `allowedBuiltinTools`(默认仅核心工具可用)
- 若角色需要特定分析能力,列出所需的分析工具名称
- 仅在角色明显聚焦时选择对应的分析工具,例如:

View File

@@ -467,17 +467,11 @@ function closeModal() {
{{ t('ai.assistant.config.analysisTools') }}
</h3>
<div v-if="!readonly" class="flex gap-2">
<button
class="text-[10px] text-primary-500 hover:text-primary-600"
@click="selectAllAnalysisTools"
>
<button class="text-[10px] text-primary-500 hover:text-primary-600" @click="selectAllAnalysisTools">
{{ t('ai.assistant.config.selectAll') }}
</button>
<span class="text-[10px] text-gray-300 dark:text-gray-600">|</span>
<button
class="text-[10px] text-primary-500 hover:text-primary-600"
@click="clearAllAnalysisTools"
>
<button class="text-[10px] text-primary-500 hover:text-primary-600" @click="clearAllAnalysisTools">
{{ t('ai.assistant.config.deselectAll') }}
</button>
</div>

View File

@@ -394,7 +394,9 @@ function fillInput(content: string) {
})
}
function handleSelectMention(member: Pick<MentionCandidate, 'memberId' | 'platformId' | 'displayName' | 'insertName' | 'aliases'>) {
function handleSelectMention(
member: Pick<MentionCandidate, 'memberId' | 'platformId' | 'displayName' | 'insertName' | 'aliases'>
) {
if (props.disabled || !mentionRange.value) return
const prefix = inputValue.value.slice(0, mentionRange.value.start)

View File

@@ -285,9 +285,7 @@ function handleRetry() {
</button>
<template v-else>
<span
class="px-3 py-1.5 text-xs font-medium text-gray-400 dark:text-gray-500"
>
<span class="px-3 py-1.5 text-xs font-medium text-gray-400 dark:text-gray-500">
{{ t('ai.skill.market.imported') }}
</span>
<button
@@ -302,7 +300,10 @@ function handleRetry() {
</div>
</div>
<div v-if="!cloudLoading && !cloudError && sortedCatalog.length === 0" class="py-12 text-center text-sm text-gray-400">
<div
v-if="!cloudLoading && !cloudError && sortedCatalog.length === 0"
class="py-12 text-center text-sm text-gray-400"
>
{{ t('ai.skill.market.noCatalog') }}
</div>
</div>

View File

@@ -76,7 +76,7 @@ function initChart() {
chartInstance.setOption({ backgroundColor: 'transparent', ...props.option })
}
// 更新图表
// 更新图表
function updateChart() {
if (!chartInstance) {
initChart()

View File

@@ -194,7 +194,10 @@ onMounted(() => {
</script>
<template>
<div class="main-content" :class="props.showHeader ? 'max-w-5xl p-6' : 'flex h-full max-w-none flex-col overflow-hidden p-4'">
<div
class="main-content"
:class="props.showHeader ? 'max-w-5xl p-6' : 'flex h-full max-w-none flex-col overflow-hidden p-4'"
>
<!-- 页面标题 -->
<div v-if="props.showHeader" class="mb-6">
<div class="flex items-center gap-3">

View File

@@ -72,7 +72,11 @@ const viewTimeFilter = computed(() => ({
:session-id="props.sessionId"
:time-filter="viewTimeFilter"
/>
<ClusterView v-else-if="activeSubTab === 'cluster'" :session-id="props.sessionId" :time-filter="viewTimeFilter" />
<ClusterView
v-else-if="activeSubTab === 'cluster'"
:session-id="props.sessionId"
:time-filter="viewTimeFilter"
/>
<RankingView
v-else-if="activeSubTab === 'ranking'"
:session-id="props.sessionId"

View File

@@ -20,7 +20,9 @@ const isOpen = ref(false)
<UModal v-if="props.sessionId" v-model:open="isOpen" :ui="{ content: 'max-w-6xl h-[85vh]' }">
<template #content>
<div class="flex h-full flex-col overflow-hidden bg-white dark:bg-gray-900">
<div class="flex flex-none items-center justify-between border-b border-gray-200 px-5 py-3 dark:border-gray-700">
<div
class="flex flex-none items-center justify-between border-b border-gray-200 px-5 py-3 dark:border-gray-700"
>
<h2 class="text-lg font-semibold text-gray-900 dark:text-white">
{{ t('analysis.subTabs.member.nicknameHistory') }}
</h2>

View File

@@ -120,7 +120,6 @@ const otherMemberAvatar = computed(() => {
return null
})
</script>
<template>
@@ -260,19 +259,18 @@ const otherMemberAvatar = computed(() => {
<UModal v-if="currentSessionId" v-model:open="showMemberManagementModal" :ui="{ content: 'max-w-6xl h-[85vh]' }">
<template #content>
<div class="flex h-full flex-col overflow-hidden bg-white dark:bg-gray-900">
<div class="flex flex-none items-center justify-between border-b border-gray-200 px-5 py-3 dark:border-gray-700">
<div
class="flex flex-none items-center justify-between border-b border-gray-200 px-5 py-3 dark:border-gray-700"
>
<div>
<h2 class="text-lg font-semibold text-gray-900 dark:text-white">{{ t('analysis.tooltip.memberManagement') }}</h2>
<h2 class="text-lg font-semibold text-gray-900 dark:text-white">
{{ t('analysis.tooltip.memberManagement') }}
</h2>
<p class="text-sm text-gray-500 dark:text-gray-400">
{{ t('members.private.description', { count: session?.memberCount ?? 0 }) }}
</p>
</div>
<UButton
variant="ghost"
icon="i-heroicons-x-mark"
size="sm"
@click="showMemberManagementModal = false"
/>
<UButton variant="ghost" icon="i-heroicons-x-mark" size="sm" @click="showMemberManagementModal = false" />
</div>
<div class="flex-1 overflow-auto">
<MemberManagementPanel :session-id="currentSessionId" :show-header="false" />

View File

@@ -126,9 +126,14 @@ function formatTime(ts: number): string {
<template>
<div class="space-y-6">
<!-- Beta 提示 -->
<div class="rounded-lg border border-amber-200/60 bg-amber-50 px-4 py-3 dark:border-amber-500/20 dark:bg-amber-500/10">
<div
class="rounded-lg border border-amber-200/60 bg-amber-50 px-4 py-3 dark:border-amber-500/20 dark:bg-amber-500/10"
>
<div class="flex items-start gap-3">
<UIcon name="i-heroicons-exclamation-triangle" class="mt-0.5 h-5 w-5 shrink-0 text-amber-500 dark:text-amber-400" />
<UIcon
name="i-heroicons-exclamation-triangle"
class="mt-0.5 h-5 w-5 shrink-0 text-amber-500 dark:text-amber-400"
/>
<p class="text-sm font-medium text-amber-800 dark:text-amber-300">
{{ t('settings.api.betaWarning') }}
</p>

View File

@@ -126,7 +126,9 @@ export const useApiServerStore = defineStore('apiServer', () => {
}
}
async function addDataSource(partial: Omit<DataSource, 'id' | 'createdAt' | 'lastPullAt' | 'lastStatus' | 'lastError' | 'lastNewMessages'>) {
async function addDataSource(
partial: Omit<DataSource, 'id' | 'createdAt' | 'lastPullAt' | 'lastStatus' | 'lastError' | 'lastNewMessages'>
) {
try {
const ds = await window.apiServerApi.addDataSource(partial)
dataSources.value.push(ds)