work-agent 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +234 -0
- package/app/(admin)/approvals/page.tsx +16 -0
- package/app/(admin)/audit/page.tsx +18 -0
- package/app/(admin)/layout.tsx +47 -0
- package/app/(admin)/scheduled-tasks/page.tsx +17 -0
- package/app/(admin)/settings/page.tsx +46 -0
- package/app/(admin)/skills/[name]/page.tsx +378 -0
- package/app/(admin)/skills/page.tsx +406 -0
- package/app/(admin)/statistics/page.tsx +416 -0
- package/app/(admin)/tickets/[id]/page.tsx +348 -0
- package/app/(admin)/tickets/new/page.tsx +309 -0
- package/app/(admin)/tickets/page.tsx +27 -0
- package/app/api/audit/route.ts +30 -0
- package/app/api/auth/feishu/callback/route.ts +72 -0
- package/app/api/auth/feishu/login/route.ts +17 -0
- package/app/api/auth/feishu/sso/route.ts +78 -0
- package/app/api/auth/login/route.ts +85 -0
- package/app/api/auth/oauth/route.ts +168 -0
- package/app/api/config/providers/route.ts +105 -0
- package/app/api/config/route.ts +115 -0
- package/app/api/config/status/route.ts +56 -0
- package/app/api/config/test/route.ts +212 -0
- package/app/api/documents/[id]/route.ts +88 -0
- package/app/api/documents/route.ts +53 -0
- package/app/api/health/route.ts +32 -0
- package/app/api/knowledge/[id]/route.ts +152 -0
- package/app/api/knowledge/from-session/route.ts +27 -0
- package/app/api/knowledge/route.ts +100 -0
- package/app/api/market/knowledge/[id]/route.ts +92 -0
- package/app/api/market/knowledge/route.ts +130 -0
- package/app/api/marketplace/skills/[id]/approve/route.ts +68 -0
- package/app/api/marketplace/skills/[id]/certify/route.ts +54 -0
- package/app/api/marketplace/skills/[id]/install/route.ts +180 -0
- package/app/api/marketplace/skills/[id]/promote-to-system/route.ts +219 -0
- package/app/api/marketplace/skills/[id]/rate/route.ts +90 -0
- package/app/api/marketplace/skills/[id]/ratings/route.ts +55 -0
- package/app/api/marketplace/skills/[id]/reject/route.ts +68 -0
- package/app/api/marketplace/skills/[id]/route.ts +177 -0
- package/app/api/marketplace/skills/route.ts +235 -0
- package/app/api/memory/route.ts +40 -0
- package/app/api/my/files/[id]/route.ts +52 -0
- package/app/api/my/files/route.ts +230 -0
- package/app/api/my/knowledge/route.ts +36 -0
- package/app/api/pi-chat/route.ts +443 -0
- package/app/api/recommend/route.ts +38 -0
- package/app/api/scheduled-tasks/[id]/execute/route.ts +132 -0
- package/app/api/scheduled-tasks/[id]/route.ts +165 -0
- package/app/api/scheduled-tasks/[id]/toggle/route.ts +53 -0
- package/app/api/scheduled-tasks/route.ts +101 -0
- package/app/api/sessions/[id]/messages/route.ts +212 -0
- package/app/api/sessions/route.ts +101 -0
- package/app/api/share/file/[id]/route.ts +37 -0
- package/app/api/skills/[name]/execute/route.ts +121 -0
- package/app/api/skills/[name]/route.ts +167 -0
- package/app/api/skills/create/route.ts +65 -0
- package/app/api/skills/generate/route.ts +405 -0
- package/app/api/skills/installed/route.ts +151 -0
- package/app/api/skills/route.ts +174 -0
- package/app/api/skills/translate/route.ts +40 -0
- package/app/api/skills/user/[name]/route.ts +159 -0
- package/app/api/skills/user/route.ts +90 -0
- package/app/api/statistics/route.ts +94 -0
- package/app/api/task-executions/[id]/route.ts +34 -0
- package/app/api/task-executions/route.ts +29 -0
- package/app/api/tickets/[id]/approve/route.ts +129 -0
- package/app/api/tickets/[id]/execute/route.ts +201 -0
- package/app/api/tickets/[id]/route.ts +127 -0
- package/app/api/tickets/route.ts +103 -0
- package/app/api/user/skills/route.ts +175 -0
- package/app/api/users/route.ts +80 -0
- package/app/chat/page.tsx +5 -0
- package/app/globals.css +84 -0
- package/app/h5/layout.tsx +5 -0
- package/app/h5/mobile-approvals-page.tsx +167 -0
- package/app/h5/mobile-chat-page.tsx +951 -0
- package/app/h5/mobile-profile-page.tsx +147 -0
- package/app/h5/mobile-tickets-page.tsx +121 -0
- package/app/h5/page.tsx +23 -0
- package/app/h5/ticket-action-buttons.tsx +80 -0
- package/app/layout.tsx +26 -0
- package/app/login/page.tsx +318 -0
- package/app/market/knowledge/[id]/page.tsx +77 -0
- package/app/market/knowledge/page.tsx +358 -0
- package/app/market/layout.tsx +29 -0
- package/app/market/page.tsx +18 -0
- package/app/market/skills/page.tsx +397 -0
- package/app/my/files/page.tsx +511 -0
- package/app/my/knowledge/[id]/page.tsx +271 -0
- package/app/my/knowledge/new/page.tsx +234 -0
- package/app/my/knowledge/page.tsx +248 -0
- package/app/my/layout.tsx +32 -0
- package/app/my/memory/page.tsx +164 -0
- package/app/my/page.tsx +18 -0
- package/app/my/scheduled-tasks/[id]/edit/page.tsx +290 -0
- package/app/my/scheduled-tasks/[id]/executions/page.tsx +275 -0
- package/app/my/scheduled-tasks/[id]/page.tsx +284 -0
- package/app/my/scheduled-tasks/new/page.tsx +230 -0
- package/app/my/scheduled-tasks/page.tsx +27 -0
- package/app/my/skills/[name]/page.tsx +320 -0
- package/app/my/skills/new/page.tsx +394 -0
- package/app/my/skills/page.tsx +303 -0
- package/app/page.tsx +2288 -0
- package/app/share/[sessionId]/page.tsx +226 -0
- package/app/share/file/[id]/page.tsx +140 -0
- package/bin/README.md +63 -0
- package/bin/generate-api-system +300 -0
- package/bin/postinstall.js +95 -0
- package/bin/work-agent.js +173 -0
- package/components/ai-elements/agent.tsx +142 -0
- package/components/ai-elements/artifact.tsx +149 -0
- package/components/ai-elements/attachments.tsx +427 -0
- package/components/ai-elements/audio-player.tsx +232 -0
- package/components/ai-elements/canvas.tsx +26 -0
- package/components/ai-elements/chain-of-thought.tsx +223 -0
- package/components/ai-elements/checkpoint.tsx +72 -0
- package/components/ai-elements/code-block.tsx +555 -0
- package/components/ai-elements/commit.tsx +449 -0
- package/components/ai-elements/confirmation.tsx +173 -0
- package/components/ai-elements/connection.tsx +28 -0
- package/components/ai-elements/context.tsx +410 -0
- package/components/ai-elements/controls.tsx +19 -0
- package/components/ai-elements/conversation.tsx +167 -0
- package/components/ai-elements/edge.tsx +144 -0
- package/components/ai-elements/environment-variables.tsx +325 -0
- package/components/ai-elements/file-tree.tsx +298 -0
- package/components/ai-elements/image.tsx +25 -0
- package/components/ai-elements/inline-citation.tsx +294 -0
- package/components/ai-elements/jsx-preview.tsx +250 -0
- package/components/ai-elements/message.tsx +367 -0
- package/components/ai-elements/mic-selector.tsx +372 -0
- package/components/ai-elements/model-selector.tsx +214 -0
- package/components/ai-elements/node.tsx +72 -0
- package/components/ai-elements/open-in-chat.tsx +367 -0
- package/components/ai-elements/package-info.tsx +235 -0
- package/components/ai-elements/panel.tsx +16 -0
- package/components/ai-elements/persona.tsx +280 -0
- package/components/ai-elements/plan.tsx +144 -0
- package/components/ai-elements/prompt-input.tsx +1341 -0
- package/components/ai-elements/queue.tsx +275 -0
- package/components/ai-elements/reasoning.tsx +355 -0
- package/components/ai-elements/sandbox.tsx +133 -0
- package/components/ai-elements/schema-display.tsx +473 -0
- package/components/ai-elements/shimmer.tsx +78 -0
- package/components/ai-elements/snippet.tsx +141 -0
- package/components/ai-elements/sources.tsx +78 -0
- package/components/ai-elements/speech-input.tsx +324 -0
- package/components/ai-elements/stack-trace.tsx +531 -0
- package/components/ai-elements/suggestion.tsx +58 -0
- package/components/ai-elements/task.tsx +88 -0
- package/components/ai-elements/terminal.tsx +277 -0
- package/components/ai-elements/test-results.tsx +497 -0
- package/components/ai-elements/tool.tsx +174 -0
- package/components/ai-elements/toolbar.tsx +17 -0
- package/components/ai-elements/transcription.tsx +126 -0
- package/components/ai-elements/voice-selector.tsx +525 -0
- package/components/ai-elements/web-preview.tsx +282 -0
- package/components/audit-log-list.tsx +114 -0
- package/components/chat/EmptyPreviewState.tsx +12 -0
- package/components/chat/KnowledgePickerDialog.tsx +464 -0
- package/components/chat/KnowledgePreview.tsx +70 -0
- package/components/chat/KnowledgePreviewPanel.tsx +86 -0
- package/components/chat/MentionInput.tsx +309 -0
- package/components/chat/OrganizeDialog.tsx +258 -0
- package/components/chat/RecommendationBanner.tsx +94 -0
- package/components/chat/SaveToKnowledgeDialog.tsx +193 -0
- package/components/chat/SkillSelector.tsx +305 -0
- package/components/chat/SkillSwitcher.tsx +163 -0
- package/components/client-layout.tsx +15 -0
- package/components/knowledge/KnowledgeMetadataPanel.tsx +293 -0
- package/components/layout-wrapper.tsx +18 -0
- package/components/mobile-layout.tsx +62 -0
- package/components/scheduled-task-list.tsx +356 -0
- package/components/setup-guide.tsx +484 -0
- package/components/sub-nav.tsx +54 -0
- package/components/ticket-detail-content.tsx +383 -0
- package/components/ticket-list.tsx +366 -0
- package/components/top-nav.tsx +132 -0
- package/components/ui/accordion.tsx +58 -0
- package/components/ui/alert.tsx +59 -0
- package/components/ui/avatar.tsx +50 -0
- package/components/ui/badge.tsx +36 -0
- package/components/ui/button-group.tsx +83 -0
- package/components/ui/button.tsx +57 -0
- package/components/ui/card.tsx +91 -0
- package/components/ui/carousel.tsx +262 -0
- package/components/ui/collapsible.tsx +11 -0
- package/components/ui/command.tsx +153 -0
- package/components/ui/dialog.tsx +122 -0
- package/components/ui/dropdown-menu.tsx +200 -0
- package/components/ui/hover-card.tsx +29 -0
- package/components/ui/input-group.tsx +170 -0
- package/components/ui/input.tsx +22 -0
- package/components/ui/label.tsx +26 -0
- package/components/ui/popover.tsx +31 -0
- package/components/ui/progress.tsx +28 -0
- package/components/ui/scroll-area.tsx +48 -0
- package/components/ui/select.tsx +174 -0
- package/components/ui/separator.tsx +31 -0
- package/components/ui/spinner.tsx +16 -0
- package/components/ui/switch.tsx +29 -0
- package/components/ui/table.tsx +120 -0
- package/components/ui/tabs.tsx +55 -0
- package/components/ui/textarea.tsx +22 -0
- package/components/ui/tooltip.tsx +30 -0
- package/components/welcome-guide.tsx +182 -0
- package/components.json +24 -0
- package/lib/command-parser.ts +331 -0
- package/lib/dangerous-commands.ts +672 -0
- package/lib/db.ts +2250 -0
- package/lib/feishu-auth.ts +135 -0
- package/lib/file-storage.ts +306 -0
- package/lib/file-tool.ts +583 -0
- package/lib/knowledge-tool.ts +152 -0
- package/lib/knowledge-types.ts +66 -0
- package/lib/market-client.ts +313 -0
- package/lib/market-db.ts +736 -0
- package/lib/market-types.ts +51 -0
- package/lib/memory-tool.ts +211 -0
- package/lib/memory.ts +197 -0
- package/lib/pi-config.ts +436 -0
- package/lib/pi-session.ts +799 -0
- package/lib/pinyin.ts +13 -0
- package/lib/recommendation.ts +227 -0
- package/lib/risk-estimator.ts +350 -0
- package/lib/scheduled-task-tool.ts +184 -0
- package/lib/scheduler-init.ts +43 -0
- package/lib/scheduler.ts +416 -0
- package/lib/secure-bash-tool.ts +413 -0
- package/lib/skill-engine.ts +396 -0
- package/lib/skill-generator.ts +269 -0
- package/lib/skill-loader.ts +234 -0
- package/lib/skill-tool.ts +188 -0
- package/lib/skill-types.ts +82 -0
- package/lib/skills-init.ts +58 -0
- package/lib/ticket-tool.ts +246 -0
- package/lib/user-skill-types.ts +30 -0
- package/lib/user-skills.ts +362 -0
- package/lib/utils.ts +6 -0
- package/lib/workflow.ts +154 -0
- package/lib/zip-tool.ts +191 -0
- package/next.config.js +8 -0
- package/package.json +106 -0
- package/public/.gitkeep +1 -0
- package/public/icon.svg +1 -0
- package/tsconfig.json +42 -0
package/lib/db.ts
ADDED
|
@@ -0,0 +1,2250 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 数据库层 - 使用 better-sqlite3 (原生 Node.js SQLite 绑定)
|
|
3
|
+
* 比 sql.js 更稳定,性能更好,不会出现 Segmentation fault
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import Database from 'better-sqlite3';
|
|
7
|
+
import { mkdirSync, existsSync, rmSync } from 'fs';
|
|
8
|
+
import { join } from 'path';
|
|
9
|
+
|
|
10
|
+
// Initialize skills directories
|
|
11
|
+
import './skills-init';
|
|
12
|
+
|
|
13
|
+
// 数据库文件路径(存储在项目根目录的 data 文件夹中)
|
|
14
|
+
const DB_DIR = join(process.cwd(), 'data');
|
|
15
|
+
const DB_PATH = join(DB_DIR, 'assistant.db');
|
|
16
|
+
|
|
17
|
+
// 确保数据目录存在
|
|
18
|
+
if (!existsSync(DB_DIR)) {
|
|
19
|
+
try {
|
|
20
|
+
mkdirSync(DB_DIR, { recursive: true });
|
|
21
|
+
} catch (error) {
|
|
22
|
+
// 目录可能已创建,忽略错误
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// 使用 globalThis 来存储数据库实例
|
|
27
|
+
declare global {
|
|
28
|
+
var _assistantDb: Database.Database | null | undefined;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* 获取数据库实例(单例模式)
|
|
33
|
+
*/
|
|
34
|
+
export function getDb(): Database.Database {
|
|
35
|
+
if (!globalThis._assistantDb) {
|
|
36
|
+
console.log('[db] Opening database at:', DB_PATH);
|
|
37
|
+
globalThis._assistantDb = new Database(DB_PATH);
|
|
38
|
+
// 启用 WAL 模式以提高并发性能
|
|
39
|
+
globalThis._assistantDb.pragma('journal_mode = WAL');
|
|
40
|
+
// 启用外键约束
|
|
41
|
+
globalThis._assistantDb.pragma('foreign_keys = ON');
|
|
42
|
+
// 创建表结构
|
|
43
|
+
createSchema(globalThis._assistantDb);
|
|
44
|
+
// 运行迁移
|
|
45
|
+
runMigrations(globalThis._assistantDb);
|
|
46
|
+
// 修复旧数据库缺少的列
|
|
47
|
+
fixUserSkillsTable(globalThis._assistantDb);
|
|
48
|
+
console.log('[db] Database initialized successfully');
|
|
49
|
+
}
|
|
50
|
+
return globalThis._assistantDb;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* 创建数据库表结构
|
|
55
|
+
*/
|
|
56
|
+
function createSchema(db: Database.Database): void {
|
|
57
|
+
db.exec(`
|
|
58
|
+
-- 工单表
|
|
59
|
+
CREATE TABLE IF NOT EXISTS tickets (
|
|
60
|
+
id TEXT PRIMARY KEY,
|
|
61
|
+
type TEXT NOT NULL,
|
|
62
|
+
title TEXT NOT NULL,
|
|
63
|
+
description TEXT,
|
|
64
|
+
status TEXT NOT NULL,
|
|
65
|
+
priority TEXT NOT NULL,
|
|
66
|
+
command TEXT,
|
|
67
|
+
command_type TEXT,
|
|
68
|
+
skill_name TEXT,
|
|
69
|
+
script_content TEXT,
|
|
70
|
+
script_path TEXT,
|
|
71
|
+
output TEXT,
|
|
72
|
+
error TEXT,
|
|
73
|
+
risk_level TEXT NOT NULL DEFAULT 'medium',
|
|
74
|
+
affected_resources TEXT,
|
|
75
|
+
approvals TEXT NOT NULL,
|
|
76
|
+
created_by TEXT NOT NULL,
|
|
77
|
+
created_at TEXT NOT NULL,
|
|
78
|
+
updated_at TEXT NOT NULL,
|
|
79
|
+
ai_session_id TEXT,
|
|
80
|
+
ai_generated INTEGER DEFAULT 0
|
|
81
|
+
);
|
|
82
|
+
|
|
83
|
+
-- 审计日志表
|
|
84
|
+
CREATE TABLE IF NOT EXISTS audit_logs (
|
|
85
|
+
id TEXT PRIMARY KEY,
|
|
86
|
+
timestamp TEXT NOT NULL,
|
|
87
|
+
action TEXT NOT NULL,
|
|
88
|
+
user_id TEXT NOT NULL,
|
|
89
|
+
ticket_id TEXT,
|
|
90
|
+
session_id TEXT,
|
|
91
|
+
details TEXT,
|
|
92
|
+
status TEXT NOT NULL
|
|
93
|
+
);
|
|
94
|
+
|
|
95
|
+
-- 文档表
|
|
96
|
+
CREATE TABLE IF NOT EXISTS documents (
|
|
97
|
+
id TEXT PRIMARY KEY,
|
|
98
|
+
title TEXT NOT NULL,
|
|
99
|
+
content TEXT,
|
|
100
|
+
category TEXT,
|
|
101
|
+
tags TEXT,
|
|
102
|
+
author TEXT NOT NULL,
|
|
103
|
+
created_at TEXT NOT NULL,
|
|
104
|
+
updated_at TEXT NOT NULL
|
|
105
|
+
);
|
|
106
|
+
|
|
107
|
+
-- 定时任务表
|
|
108
|
+
CREATE TABLE IF NOT EXISTS scheduled_tasks (
|
|
109
|
+
id TEXT PRIMARY KEY,
|
|
110
|
+
title TEXT NOT NULL,
|
|
111
|
+
description TEXT,
|
|
112
|
+
command TEXT NOT NULL,
|
|
113
|
+
status TEXT NOT NULL,
|
|
114
|
+
schedule_type TEXT NOT NULL,
|
|
115
|
+
schedule_expression TEXT NOT NULL,
|
|
116
|
+
next_run_at TEXT,
|
|
117
|
+
last_run_at TEXT,
|
|
118
|
+
created_by TEXT NOT NULL,
|
|
119
|
+
created_at TEXT NOT NULL,
|
|
120
|
+
updated_at TEXT NOT NULL
|
|
121
|
+
);
|
|
122
|
+
|
|
123
|
+
-- 任务执行历史表
|
|
124
|
+
CREATE TABLE IF NOT EXISTS task_executions (
|
|
125
|
+
id TEXT PRIMARY KEY,
|
|
126
|
+
task_id TEXT NOT NULL,
|
|
127
|
+
status TEXT NOT NULL,
|
|
128
|
+
started_at TEXT,
|
|
129
|
+
completed_at TEXT,
|
|
130
|
+
ticket_id TEXT,
|
|
131
|
+
error_message TEXT,
|
|
132
|
+
created_at TEXT NOT NULL
|
|
133
|
+
);
|
|
134
|
+
|
|
135
|
+
-- 索引
|
|
136
|
+
CREATE INDEX IF NOT EXISTS idx_tickets_status ON tickets(status);
|
|
137
|
+
CREATE INDEX IF NOT EXISTS idx_audit_logs_ticket_id ON audit_logs(ticket_id);
|
|
138
|
+
CREATE INDEX IF NOT EXISTS idx_audit_logs_timestamp ON audit_logs(timestamp);
|
|
139
|
+
CREATE INDEX IF NOT EXISTS idx_documents_category ON documents(category);
|
|
140
|
+
CREATE INDEX IF NOT EXISTS idx_documents_author ON documents(author);
|
|
141
|
+
CREATE INDEX IF NOT EXISTS idx_scheduled_tasks_status ON scheduled_tasks(status);
|
|
142
|
+
CREATE INDEX IF NOT EXISTS idx_scheduled_tasks_next_run_at ON scheduled_tasks(next_run_at);
|
|
143
|
+
CREATE INDEX IF NOT EXISTS idx_task_executions_task_id ON task_executions(task_id);
|
|
144
|
+
CREATE INDEX IF NOT EXISTS idx_task_executions_status ON task_executions(status);
|
|
145
|
+
|
|
146
|
+
-- 用户表
|
|
147
|
+
CREATE TABLE IF NOT EXISTS users (
|
|
148
|
+
id TEXT PRIMARY KEY,
|
|
149
|
+
username TEXT NOT NULL UNIQUE,
|
|
150
|
+
role TEXT DEFAULT 'guest',
|
|
151
|
+
created_at TEXT NOT NULL,
|
|
152
|
+
updated_at TEXT NOT NULL,
|
|
153
|
+
last_login_at TEXT,
|
|
154
|
+
settings TEXT
|
|
155
|
+
);
|
|
156
|
+
|
|
157
|
+
-- 内置Skill配置表
|
|
158
|
+
CREATE TABLE IF NOT EXISTS builtin_skills (
|
|
159
|
+
id TEXT PRIMARY KEY,
|
|
160
|
+
skill_name TEXT NOT NULL UNIQUE,
|
|
161
|
+
enabled INTEGER DEFAULT 1,
|
|
162
|
+
config TEXT,
|
|
163
|
+
created_at TEXT NOT NULL
|
|
164
|
+
);
|
|
165
|
+
|
|
166
|
+
-- 用户已安装的Skill表
|
|
167
|
+
CREATE TABLE IF NOT EXISTS user_skills (
|
|
168
|
+
id TEXT PRIMARY KEY,
|
|
169
|
+
user_id TEXT NOT NULL,
|
|
170
|
+
skill_name TEXT NOT NULL,
|
|
171
|
+
dir_name TEXT,
|
|
172
|
+
source TEXT NOT NULL,
|
|
173
|
+
skill_data TEXT,
|
|
174
|
+
skill_id TEXT,
|
|
175
|
+
use_count INTEGER DEFAULT 0,
|
|
176
|
+
installed_at TEXT NOT NULL,
|
|
177
|
+
last_used_at TEXT,
|
|
178
|
+
UNIQUE(user_id, skill_name)
|
|
179
|
+
);
|
|
180
|
+
|
|
181
|
+
-- Skill使用统计表
|
|
182
|
+
CREATE TABLE IF NOT EXISTS skill_usage (
|
|
183
|
+
id TEXT PRIMARY KEY,
|
|
184
|
+
skill_name TEXT NOT NULL,
|
|
185
|
+
session_id TEXT NOT NULL,
|
|
186
|
+
user_id TEXT NOT NULL,
|
|
187
|
+
context TEXT,
|
|
188
|
+
result TEXT,
|
|
189
|
+
success INTEGER,
|
|
190
|
+
called_at TEXT NOT NULL
|
|
191
|
+
);
|
|
192
|
+
|
|
193
|
+
-- 新增索引
|
|
194
|
+
CREATE INDEX IF NOT EXISTS idx_users_username ON users(username);
|
|
195
|
+
CREATE INDEX IF NOT EXISTS idx_user_skills_user_id ON user_skills(user_id);
|
|
196
|
+
CREATE INDEX IF NOT EXISTS idx_skill_usage_skill_name ON skill_usage(skill_name);
|
|
197
|
+
CREATE INDEX IF NOT EXISTS idx_skill_usage_session_id ON skill_usage(session_id);
|
|
198
|
+
|
|
199
|
+
-- 会话列表表(用于快速查询会话)
|
|
200
|
+
CREATE TABLE IF NOT EXISTS sessions (
|
|
201
|
+
session_id TEXT PRIMARY KEY,
|
|
202
|
+
user_id TEXT,
|
|
203
|
+
source TEXT,
|
|
204
|
+
source_user_id TEXT,
|
|
205
|
+
created_at TEXT NOT NULL,
|
|
206
|
+
last_accessed_at TEXT NOT NULL,
|
|
207
|
+
message_count INTEGER DEFAULT 0,
|
|
208
|
+
recent_summary TEXT
|
|
209
|
+
);
|
|
210
|
+
|
|
211
|
+
CREATE INDEX IF NOT EXISTS idx_sessions_user_id ON sessions(user_id);
|
|
212
|
+
`);
|
|
213
|
+
|
|
214
|
+
// Try to create index on source column - may fail if column doesn't exist yet (old DB)
|
|
215
|
+
try {
|
|
216
|
+
db.exec(`CREATE INDEX IF NOT EXISTS idx_sessions_source ON sessions(source)`);
|
|
217
|
+
} catch (error: any) {
|
|
218
|
+
if (!error.message.includes('no such column')) {
|
|
219
|
+
console.warn('[db] Warning creating idx_sessions_source:', error.message);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* 获取数据库版本
|
|
226
|
+
*/
|
|
227
|
+
function getUserVersion(db: Database.Database): number {
|
|
228
|
+
const result = db.pragma('user_version', { simple: true });
|
|
229
|
+
return result as number;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* 设置数据库版本
|
|
234
|
+
*/
|
|
235
|
+
function setUserVersion(db: Database.Database, version: number): void {
|
|
236
|
+
db.pragma(`user_version = ${version}`);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* 创建技能市场相关表
|
|
241
|
+
*/
|
|
242
|
+
function createSkillMarketSchema(db: Database.Database): void {
|
|
243
|
+
db.exec(`
|
|
244
|
+
-- 技能市场表
|
|
245
|
+
CREATE TABLE IF NOT EXISTS skill_market (
|
|
246
|
+
id TEXT PRIMARY KEY,
|
|
247
|
+
name TEXT NOT NULL UNIQUE,
|
|
248
|
+
display_name TEXT NOT NULL,
|
|
249
|
+
description TEXT,
|
|
250
|
+
version TEXT NOT NULL,
|
|
251
|
+
author TEXT NOT NULL,
|
|
252
|
+
author_name TEXT,
|
|
253
|
+
status TEXT NOT NULL DEFAULT 'pending',
|
|
254
|
+
is_official INTEGER DEFAULT 0,
|
|
255
|
+
rating_avg REAL DEFAULT 0,
|
|
256
|
+
rating_count INTEGER DEFAULT 0,
|
|
257
|
+
download_count INTEGER DEFAULT 0,
|
|
258
|
+
file_path TEXT,
|
|
259
|
+
reviewed_by TEXT,
|
|
260
|
+
reviewed_at TEXT,
|
|
261
|
+
review_comment TEXT,
|
|
262
|
+
created_at TEXT NOT NULL,
|
|
263
|
+
updated_at TEXT NOT NULL
|
|
264
|
+
);
|
|
265
|
+
|
|
266
|
+
-- 技能评分表
|
|
267
|
+
CREATE TABLE IF NOT EXISTS skill_ratings (
|
|
268
|
+
id TEXT PRIMARY KEY,
|
|
269
|
+
skill_id TEXT NOT NULL,
|
|
270
|
+
user_id TEXT NOT NULL,
|
|
271
|
+
rating INTEGER NOT NULL,
|
|
272
|
+
comment TEXT,
|
|
273
|
+
created_at TEXT NOT NULL,
|
|
274
|
+
UNIQUE(skill_id, user_id),
|
|
275
|
+
FOREIGN KEY (skill_id) REFERENCES skill_market(id) ON DELETE CASCADE
|
|
276
|
+
);
|
|
277
|
+
|
|
278
|
+
-- 创建索引
|
|
279
|
+
CREATE INDEX IF NOT EXISTS idx_skill_market_status ON skill_market(status);
|
|
280
|
+
CREATE INDEX IF NOT EXISTS idx_skill_market_author ON skill_market(author);
|
|
281
|
+
CREATE INDEX IF NOT EXISTS idx_skill_ratings_skill_id ON skill_ratings(skill_id);
|
|
282
|
+
`);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* 运行数据库迁移(添加新字段到现有表)
|
|
287
|
+
*/
|
|
288
|
+
function runMigrations(db: Database.Database): void {
|
|
289
|
+
// 版本控制迁移
|
|
290
|
+
const currentVersion = getUserVersion(db);
|
|
291
|
+
const targetVersion = 2; // 新版本号
|
|
292
|
+
|
|
293
|
+
if (currentVersion < targetVersion) {
|
|
294
|
+
console.log('[db] Running migration to version', targetVersion);
|
|
295
|
+
createSkillMarketSchema(db);
|
|
296
|
+
setUserVersion(db, targetVersion);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// 知识库功能 - 添加新字段(向后兼容的 ALTER TABLE 迁移)
|
|
300
|
+
const migrations = [
|
|
301
|
+
{ sql: 'ALTER TABLE documents ADD COLUMN type TEXT', name: 'type' },
|
|
302
|
+
{ sql: 'ALTER TABLE documents ADD COLUMN source_type TEXT', name: 'source_type' },
|
|
303
|
+
{ sql: 'ALTER TABLE documents ADD COLUMN session_id TEXT', name: 'session_id' },
|
|
304
|
+
{ sql: 'ALTER TABLE documents ADD COLUMN is_active INTEGER DEFAULT 1', name: 'is_active' },
|
|
305
|
+
// 市场相关字段
|
|
306
|
+
{ sql: 'ALTER TABLE documents ADD COLUMN market_status TEXT', name: 'market_status' },
|
|
307
|
+
{ sql: 'ALTER TABLE documents ADD COLUMN market_downloads INTEGER DEFAULT 0', name: 'market_downloads' },
|
|
308
|
+
{ sql: 'ALTER TABLE documents ADD COLUMN market_rating REAL DEFAULT 0', name: 'market_rating' },
|
|
309
|
+
{ sql: 'ALTER TABLE documents ADD COLUMN market_rating_count INTEGER DEFAULT 0', name: 'market_rating_count' },
|
|
310
|
+
// 用户角色
|
|
311
|
+
{ sql: "ALTER TABLE users ADD COLUMN role TEXT DEFAULT 'guest'", name: 'role' },
|
|
312
|
+
// 审计日志会话ID
|
|
313
|
+
{ sql: 'ALTER TABLE audit_logs ADD COLUMN session_id TEXT', name: 'audit_session_id' },
|
|
314
|
+
// user_skills 新字段
|
|
315
|
+
{ sql: 'ALTER TABLE user_skills ADD COLUMN dir_name TEXT', name: 'user_skills_dir_name' },
|
|
316
|
+
{ sql: 'ALTER TABLE user_skills ADD COLUMN skill_id TEXT', name: 'user_skills_skill_id' },
|
|
317
|
+
{ sql: "ALTER TABLE user_skills ADD COLUMN source TEXT DEFAULT 'market'", name: 'user_skills_source' },
|
|
318
|
+
// sessions 表标题字段
|
|
319
|
+
{ sql: 'ALTER TABLE sessions ADD COLUMN title TEXT', name: 'sessions_title' },
|
|
320
|
+
// 飞书用户信息
|
|
321
|
+
{ sql: 'ALTER TABLE users ADD COLUMN feishu_union_id TEXT', name: 'feishu_union_id' },
|
|
322
|
+
{ sql: 'ALTER TABLE users ADD COLUMN feishu_open_id TEXT', name: 'feishu_open_id' },
|
|
323
|
+
// 用户显示信息(新字段)
|
|
324
|
+
{ sql: 'ALTER TABLE users ADD COLUMN display_name TEXT', name: 'display_name' },
|
|
325
|
+
{ sql: 'ALTER TABLE users ADD COLUMN avatar TEXT', name: 'avatar' },
|
|
326
|
+
// 会话来源字段
|
|
327
|
+
{ sql: 'ALTER TABLE sessions ADD COLUMN source TEXT', name: 'sessions_source' },
|
|
328
|
+
{ sql: 'ALTER TABLE sessions ADD COLUMN source_user_id TEXT', name: 'sessions_source_user_id' },
|
|
329
|
+
// 文件管理字段
|
|
330
|
+
{ sql: 'ALTER TABLE documents ADD COLUMN file_metadata TEXT', name: 'file_metadata' },
|
|
331
|
+
{ sql: 'ALTER TABLE documents ADD COLUMN is_public INTEGER DEFAULT 0', name: 'is_public' },
|
|
332
|
+
];
|
|
333
|
+
|
|
334
|
+
for (const migration of migrations) {
|
|
335
|
+
try {
|
|
336
|
+
db.exec(migration.sql);
|
|
337
|
+
console.log(`[db] Migration: Added ${migration.name}`);
|
|
338
|
+
} catch (error: any) {
|
|
339
|
+
// 只忽略 "duplicate column" 错误
|
|
340
|
+
if (!error.message.includes('duplicate column')) {
|
|
341
|
+
console.warn(`[db] Migration warning for ${migration.name}:`, error.message);
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
// 存量数据迁移:将 dir_name 设为与 skill_name 相同(兼容旧数据)
|
|
347
|
+
try {
|
|
348
|
+
const updateStmt = db.prepare('UPDATE user_skills SET dir_name = skill_name WHERE dir_name IS NULL');
|
|
349
|
+
const result = updateStmt.run();
|
|
350
|
+
if (result.changes > 0) {
|
|
351
|
+
console.log(`[db] Migration: Updated ${result.changes} rows to set dir_name = skill_name`);
|
|
352
|
+
}
|
|
353
|
+
} catch (error: any) {
|
|
354
|
+
console.warn('[db] Migration warning for dir_name migration:', error.message);
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
// 迁移旧飞书字段数据到新字段
|
|
358
|
+
try {
|
|
359
|
+
const migrateStmt = db.prepare(`
|
|
360
|
+
UPDATE users
|
|
361
|
+
SET display_name = feishu_name,
|
|
362
|
+
avatar = feishu_avatar
|
|
363
|
+
WHERE feishu_name IS NOT NULL OR feishu_avatar IS NOT NULL
|
|
364
|
+
`);
|
|
365
|
+
const migrateResult = migrateStmt.run();
|
|
366
|
+
if (migrateResult.changes > 0) {
|
|
367
|
+
console.log(`[db] Migration: Migrated ${migrateResult.changes} rows of feishu user info`);
|
|
368
|
+
}
|
|
369
|
+
} catch (error: any) {
|
|
370
|
+
console.warn('[db] Migration warning for feishu data migration:', error.message);
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
// 删除旧字段(如果存在)
|
|
374
|
+
try {
|
|
375
|
+
db.exec('ALTER TABLE users DROP COLUMN feishu_avatar');
|
|
376
|
+
console.log('[db] Migration: Dropped column feishu_avatar');
|
|
377
|
+
} catch (error: any) {
|
|
378
|
+
if (!error.message.includes('no such column')) {
|
|
379
|
+
console.warn('[db] Migration warning for dropping feishu_avatar:', error.message);
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
try {
|
|
383
|
+
db.exec('ALTER TABLE users DROP COLUMN feishu_name');
|
|
384
|
+
console.log('[db] Migration: Dropped column feishu_name');
|
|
385
|
+
} catch (error: any) {
|
|
386
|
+
if (!error.message.includes('no such column')) {
|
|
387
|
+
console.warn('[db] Migration warning for dropping feishu_name:', error.message);
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
// 修复 user_skills 表缺少 source 列的问题(兼容旧数据库)
|
|
393
|
+
function fixUserSkillsTable(db: Database.Database): void {
|
|
394
|
+
try {
|
|
395
|
+
// 检查 source 列是否存在
|
|
396
|
+
const result = db.prepare("PRAGMA table_info(user_skills)").all() as any[];
|
|
397
|
+
const hasSourceColumn = result.some((col: any) => col.name === 'source');
|
|
398
|
+
|
|
399
|
+
if (!hasSourceColumn) {
|
|
400
|
+
db.exec("ALTER TABLE user_skills ADD COLUMN source TEXT NOT NULL DEFAULT 'market'");
|
|
401
|
+
console.log('[db] Fixed: Added source column to user_skills table');
|
|
402
|
+
}
|
|
403
|
+
} catch (error: any) {
|
|
404
|
+
console.warn('[db] Fix warning for user_skills source column:', error.message);
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
// ==================== 类型定义 ====================
|
|
409
|
+
|
|
410
|
+
export type TicketType = 'bash-execute' | 'skill-script';
|
|
411
|
+
|
|
412
|
+
export type TicketStatus = 'draft' | 'pending' | 'approved' | 'rejected' | 'executing' | 'completed' | 'failed' | 'deleted';
|
|
413
|
+
|
|
414
|
+
export type RiskLevel = 'low' | 'medium' | 'high' | 'critical';
|
|
415
|
+
|
|
416
|
+
export interface Ticket {
|
|
417
|
+
id: string;
|
|
418
|
+
type: TicketType;
|
|
419
|
+
title: string;
|
|
420
|
+
description: string;
|
|
421
|
+
status: TicketStatus;
|
|
422
|
+
priority: RiskLevel;
|
|
423
|
+
|
|
424
|
+
// Command info (for bash-execute)
|
|
425
|
+
command?: string;
|
|
426
|
+
commandType?: string;
|
|
427
|
+
|
|
428
|
+
// Skill info (for skill-script)
|
|
429
|
+
skillName?: string;
|
|
430
|
+
scriptContent?: string;
|
|
431
|
+
scriptPath?: string;
|
|
432
|
+
|
|
433
|
+
// Execution result
|
|
434
|
+
output?: string;
|
|
435
|
+
error?: string;
|
|
436
|
+
|
|
437
|
+
// Risk assessment
|
|
438
|
+
riskLevel: RiskLevel;
|
|
439
|
+
affectedResources?: string[];
|
|
440
|
+
|
|
441
|
+
approvals: Approval[];
|
|
442
|
+
createdBy: string;
|
|
443
|
+
createdAt: Date;
|
|
444
|
+
updatedAt: Date;
|
|
445
|
+
|
|
446
|
+
// AI 相关字段
|
|
447
|
+
aiSessionId?: string;
|
|
448
|
+
aiGenerated?: boolean;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
export interface Approval {
|
|
452
|
+
id: string;
|
|
453
|
+
approver: string;
|
|
454
|
+
decision: 'approved' | 'rejected' | 'pending';
|
|
455
|
+
comment?: string;
|
|
456
|
+
timestamp: Date;
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
export interface AuditLog {
|
|
460
|
+
id: string;
|
|
461
|
+
timestamp: Date;
|
|
462
|
+
action: string;
|
|
463
|
+
userId: string;
|
|
464
|
+
ticketId?: string;
|
|
465
|
+
sessionId?: string;
|
|
466
|
+
details?: unknown;
|
|
467
|
+
status: 'success' | 'failure';
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
// 知识类型
|
|
471
|
+
export type KnowledgeType =
|
|
472
|
+
| 'documentation' // 文档型知识
|
|
473
|
+
| 'troubleshooting' // 问题排查
|
|
474
|
+
| 'best-practice' // 最佳实践
|
|
475
|
+
| 'conversation-summary'; // 会话总结
|
|
476
|
+
|
|
477
|
+
// 知识来源
|
|
478
|
+
export type KnowledgeSourceType =
|
|
479
|
+
| 'manual' // 手动创建
|
|
480
|
+
| 'ai-generated' // AI生成
|
|
481
|
+
| 'conversation-summary'; // 会话总结
|
|
482
|
+
|
|
483
|
+
export interface FileMetadata {
|
|
484
|
+
size: number;
|
|
485
|
+
mimeType: string;
|
|
486
|
+
md5?: string;
|
|
487
|
+
storageKey: string;
|
|
488
|
+
storageType: 'local' | 's3' | 'minio';
|
|
489
|
+
originalName?: string;
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
export interface Document {
|
|
493
|
+
id: string;
|
|
494
|
+
title: string;
|
|
495
|
+
content: string;
|
|
496
|
+
category?: string;
|
|
497
|
+
tags?: string[];
|
|
498
|
+
author: string;
|
|
499
|
+
createdAt: Date;
|
|
500
|
+
updatedAt: Date;
|
|
501
|
+
// 新增字段 - 知识库功能
|
|
502
|
+
type?: KnowledgeType | 'file';
|
|
503
|
+
sourceType?: KnowledgeSourceType;
|
|
504
|
+
sessionId?: string;
|
|
505
|
+
isActive?: boolean;
|
|
506
|
+
// 市场功能
|
|
507
|
+
marketStatus?: 'published' | 'draft' | 'archived';
|
|
508
|
+
marketDownloads?: number;
|
|
509
|
+
marketRating?: number;
|
|
510
|
+
marketRatingCount?: number;
|
|
511
|
+
// 文件管理功能
|
|
512
|
+
fileMetadata?: FileMetadata;
|
|
513
|
+
isPublic?: boolean;
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
export interface ScheduledTask {
|
|
517
|
+
id: string;
|
|
518
|
+
title: string;
|
|
519
|
+
description?: string;
|
|
520
|
+
command: string; // 要执行的命令
|
|
521
|
+
status: 'active' | 'paused' | 'disabled';
|
|
522
|
+
scheduleType: 'cron' | 'interval' | 'once';
|
|
523
|
+
scheduleExpression: string;
|
|
524
|
+
nextRunAt?: Date;
|
|
525
|
+
lastRunAt?: Date;
|
|
526
|
+
createdBy: string;
|
|
527
|
+
createdAt: Date;
|
|
528
|
+
updatedAt: Date;
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
export interface TaskExecution {
|
|
532
|
+
id: string;
|
|
533
|
+
taskId: string;
|
|
534
|
+
status: 'pending' | 'running' | 'completed' | 'failed';
|
|
535
|
+
startedAt?: Date;
|
|
536
|
+
completedAt?: Date;
|
|
537
|
+
ticketId?: string;
|
|
538
|
+
errorMessage?: string;
|
|
539
|
+
createdAt: Date;
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
// ==================== 新增类型定义 ====================
|
|
543
|
+
|
|
544
|
+
export interface User {
|
|
545
|
+
id: string;
|
|
546
|
+
username: string;
|
|
547
|
+
role: 'admin' | 'guest';
|
|
548
|
+
createdAt: Date;
|
|
549
|
+
updatedAt: Date;
|
|
550
|
+
lastLoginAt?: Date;
|
|
551
|
+
settings?: Record<string, unknown>;
|
|
552
|
+
feishuUnionId?: string;
|
|
553
|
+
feishuOpenId?: string;
|
|
554
|
+
displayName?: string;
|
|
555
|
+
avatar?: string;
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
export interface BuiltinSkill {
|
|
559
|
+
id: string;
|
|
560
|
+
skillName: string;
|
|
561
|
+
enabled: boolean;
|
|
562
|
+
config?: Record<string, unknown>;
|
|
563
|
+
createdAt: Date;
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
export interface UserSkill {
|
|
567
|
+
id: string;
|
|
568
|
+
userId: string;
|
|
569
|
+
skillName: string;
|
|
570
|
+
source: 'market' | 'builtin' | 'personal';
|
|
571
|
+
skillData?: Record<string, unknown>;
|
|
572
|
+
useCount: number;
|
|
573
|
+
installedAt: Date;
|
|
574
|
+
lastUsedAt?: Date;
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
export interface SkillUsage {
|
|
578
|
+
id: string;
|
|
579
|
+
skillName: string;
|
|
580
|
+
sessionId: string;
|
|
581
|
+
userId: string;
|
|
582
|
+
context?: string;
|
|
583
|
+
result?: string;
|
|
584
|
+
success?: boolean;
|
|
585
|
+
calledAt: Date;
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
// ==================== 工单操作 ====================
|
|
589
|
+
|
|
590
|
+
export function createTicket(ticket: Omit<Ticket, 'id' | 'createdAt' | 'updatedAt'>): Ticket {
|
|
591
|
+
const db = getDb();
|
|
592
|
+
const id = `${Date.now()}-${Math.random().toString(36).substring(2, 15)}`;
|
|
593
|
+
const now = new Date();
|
|
594
|
+
const newTicket: Ticket = {
|
|
595
|
+
...ticket,
|
|
596
|
+
id,
|
|
597
|
+
createdAt: now,
|
|
598
|
+
updatedAt: now,
|
|
599
|
+
};
|
|
600
|
+
|
|
601
|
+
const stmt = db.prepare(`
|
|
602
|
+
INSERT INTO tickets (
|
|
603
|
+
id, type, title, description, status, priority, command,
|
|
604
|
+
command_type, skill_name, script_content, script_path, output, error,
|
|
605
|
+
risk_level, affected_resources, approvals, created_by, created_at, updated_at,
|
|
606
|
+
ai_session_id, ai_generated
|
|
607
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
608
|
+
`);
|
|
609
|
+
|
|
610
|
+
stmt.run(
|
|
611
|
+
newTicket.id,
|
|
612
|
+
newTicket.type,
|
|
613
|
+
newTicket.title,
|
|
614
|
+
newTicket.description,
|
|
615
|
+
newTicket.status,
|
|
616
|
+
newTicket.priority,
|
|
617
|
+
newTicket.command ?? null,
|
|
618
|
+
newTicket.commandType ?? null,
|
|
619
|
+
newTicket.skillName ?? null,
|
|
620
|
+
newTicket.scriptContent ?? null,
|
|
621
|
+
newTicket.scriptPath ?? null,
|
|
622
|
+
newTicket.output ?? null,
|
|
623
|
+
newTicket.error ?? null,
|
|
624
|
+
newTicket.riskLevel,
|
|
625
|
+
newTicket.affectedResources ? JSON.stringify(newTicket.affectedResources) : null,
|
|
626
|
+
JSON.stringify(newTicket.approvals),
|
|
627
|
+
newTicket.createdBy,
|
|
628
|
+
now.toISOString(),
|
|
629
|
+
now.toISOString(),
|
|
630
|
+
newTicket.aiSessionId ?? null,
|
|
631
|
+
newTicket.aiGenerated ? 1 : 0,
|
|
632
|
+
);
|
|
633
|
+
|
|
634
|
+
return newTicket;
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
export function getTicket(id: string): Ticket | null {
|
|
638
|
+
const db = getDb();
|
|
639
|
+
const stmt = db.prepare('SELECT * FROM tickets WHERE id = ?');
|
|
640
|
+
const row = stmt.get(id) as any;
|
|
641
|
+
|
|
642
|
+
if (!row) return null;
|
|
643
|
+
|
|
644
|
+
return parseTicketRow(row);
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
export function getTickets(filters?: {
|
|
648
|
+
status?: string;
|
|
649
|
+
type?: string;
|
|
650
|
+
createdBy?: string;
|
|
651
|
+
aiSessionId?: string;
|
|
652
|
+
limit?: number;
|
|
653
|
+
}): Ticket[] {
|
|
654
|
+
const db = getDb();
|
|
655
|
+
|
|
656
|
+
let query = 'SELECT * FROM tickets WHERE 1=1';
|
|
657
|
+
const params: any[] = [];
|
|
658
|
+
|
|
659
|
+
if (filters?.status) {
|
|
660
|
+
query += ' AND status = ?';
|
|
661
|
+
params.push(filters.status);
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
if (filters?.type) {
|
|
665
|
+
query += ' AND type = ?';
|
|
666
|
+
params.push(filters.type);
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
if (filters?.createdBy) {
|
|
670
|
+
query += ' AND created_by = ?';
|
|
671
|
+
params.push(filters.createdBy);
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
if (filters?.aiSessionId) {
|
|
675
|
+
query += ' AND ai_session_id = ?';
|
|
676
|
+
params.push(filters.aiSessionId);
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
query += ' ORDER BY created_at DESC';
|
|
680
|
+
|
|
681
|
+
if (filters?.limit) {
|
|
682
|
+
query += ' LIMIT ?';
|
|
683
|
+
params.push(filters.limit);
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
const stmt = db.prepare(query);
|
|
687
|
+
const rows = stmt.all(...params) as any[];
|
|
688
|
+
|
|
689
|
+
return rows.map(row => parseTicketRow(row));
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
export function updateTicket(id: string, updates: Partial<Omit<Ticket, 'id' | 'createdAt' | 'createdBy'>>): Ticket | null {
|
|
693
|
+
const db = getDb();
|
|
694
|
+
const current = getTicket(id);
|
|
695
|
+
if (!current) return null;
|
|
696
|
+
|
|
697
|
+
const updated = {
|
|
698
|
+
...current,
|
|
699
|
+
...updates,
|
|
700
|
+
updatedAt: new Date(),
|
|
701
|
+
};
|
|
702
|
+
|
|
703
|
+
const stmt = db.prepare(`
|
|
704
|
+
UPDATE tickets SET
|
|
705
|
+
type = ?, title = ?, description = ?, status = ?, priority = ?,
|
|
706
|
+
command = ?, command_type = ?, skill_name = ?,
|
|
707
|
+
script_content = ?, script_path = ?, output = ?, error = ?,
|
|
708
|
+
risk_level = ?, affected_resources = ?,
|
|
709
|
+
approvals = ?, updated_at = ?, ai_session_id = ?, ai_generated = ?
|
|
710
|
+
WHERE id = ?
|
|
711
|
+
`);
|
|
712
|
+
|
|
713
|
+
stmt.run(
|
|
714
|
+
updated.type,
|
|
715
|
+
updated.title,
|
|
716
|
+
updated.description,
|
|
717
|
+
updated.status,
|
|
718
|
+
updated.priority,
|
|
719
|
+
updated.command ?? null,
|
|
720
|
+
updated.commandType ?? null,
|
|
721
|
+
updated.skillName ?? null,
|
|
722
|
+
updated.scriptContent ?? null,
|
|
723
|
+
updated.scriptPath ?? null,
|
|
724
|
+
updated.output ?? null,
|
|
725
|
+
updated.error ?? null,
|
|
726
|
+
updated.riskLevel,
|
|
727
|
+
updated.affectedResources ? JSON.stringify(updated.affectedResources) : null,
|
|
728
|
+
JSON.stringify(updated.approvals),
|
|
729
|
+
updated.updatedAt.toISOString(),
|
|
730
|
+
updated.aiSessionId ?? null,
|
|
731
|
+
updated.aiGenerated ? 1 : 0,
|
|
732
|
+
id,
|
|
733
|
+
);
|
|
734
|
+
|
|
735
|
+
return updated;
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
export function deleteTicket(id: string): boolean {
|
|
739
|
+
const db = getDb();
|
|
740
|
+
const current = getTicket(id);
|
|
741
|
+
if (!current) return false;
|
|
742
|
+
|
|
743
|
+
const stmt = db.prepare('DELETE FROM tickets WHERE id = ?');
|
|
744
|
+
stmt.run(id);
|
|
745
|
+
|
|
746
|
+
return true;
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
export function addApproval(ticketId: string, approval: Omit<Approval, 'id' | 'timestamp'>): Approval | null {
|
|
750
|
+
const ticket = getTicket(ticketId);
|
|
751
|
+
if (!ticket) return null;
|
|
752
|
+
|
|
753
|
+
const newApproval: Approval = {
|
|
754
|
+
...approval,
|
|
755
|
+
id: `${Date.now()}-${Math.random().toString(36).substring(2, 15)}`,
|
|
756
|
+
timestamp: new Date(),
|
|
757
|
+
};
|
|
758
|
+
|
|
759
|
+
const approvals = [...ticket.approvals, newApproval];
|
|
760
|
+
updateTicket(ticketId, { approvals });
|
|
761
|
+
|
|
762
|
+
return newApproval;
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
// ==================== 审计日志操作 ====================
|
|
766
|
+
|
|
767
|
+
export function createAuditLog(log: Omit<AuditLog, 'id' | 'timestamp'>): AuditLog {
|
|
768
|
+
const db = getDb();
|
|
769
|
+
const id = `${Date.now()}-${Math.random().toString(36).substring(2, 15)}`;
|
|
770
|
+
const now = new Date();
|
|
771
|
+
const newLog: AuditLog = {
|
|
772
|
+
...log,
|
|
773
|
+
id,
|
|
774
|
+
timestamp: now,
|
|
775
|
+
};
|
|
776
|
+
|
|
777
|
+
const stmt = db.prepare(`
|
|
778
|
+
INSERT INTO audit_logs (id, timestamp, action, user_id, ticket_id, session_id, details, status)
|
|
779
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
780
|
+
`);
|
|
781
|
+
|
|
782
|
+
stmt.run(
|
|
783
|
+
newLog.id,
|
|
784
|
+
now.toISOString(),
|
|
785
|
+
newLog.action,
|
|
786
|
+
newLog.userId,
|
|
787
|
+
newLog.ticketId ?? null,
|
|
788
|
+
newLog.sessionId ?? null,
|
|
789
|
+
newLog.details ? JSON.stringify(newLog.details) : null,
|
|
790
|
+
newLog.status,
|
|
791
|
+
);
|
|
792
|
+
|
|
793
|
+
return newLog;
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
export function getAuditLogs(filters?: {
|
|
797
|
+
userId?: string;
|
|
798
|
+
ticketId?: string;
|
|
799
|
+
sessionId?: string;
|
|
800
|
+
limit?: number;
|
|
801
|
+
}): AuditLog[] {
|
|
802
|
+
const db = getDb();
|
|
803
|
+
|
|
804
|
+
let query = 'SELECT * FROM audit_logs WHERE 1=1';
|
|
805
|
+
const params: any[] = [];
|
|
806
|
+
|
|
807
|
+
if (filters?.userId) {
|
|
808
|
+
query += ' AND user_id = ?';
|
|
809
|
+
params.push(filters.userId);
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
if (filters?.ticketId) {
|
|
813
|
+
query += ' AND ticket_id = ?';
|
|
814
|
+
params.push(filters.ticketId);
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
if (filters?.sessionId) {
|
|
818
|
+
query += ' AND session_id = ?';
|
|
819
|
+
params.push(filters.sessionId);
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
query += ' ORDER BY timestamp DESC';
|
|
823
|
+
|
|
824
|
+
if (filters?.limit) {
|
|
825
|
+
query += ' LIMIT ?';
|
|
826
|
+
params.push(filters.limit);
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
const stmt = db.prepare(query);
|
|
830
|
+
const rows = stmt.all(...params) as any[];
|
|
831
|
+
|
|
832
|
+
return rows.map(row => ({
|
|
833
|
+
id: row.id,
|
|
834
|
+
timestamp: new Date(row.timestamp),
|
|
835
|
+
action: row.action,
|
|
836
|
+
userId: row.user_id,
|
|
837
|
+
ticketId: row.ticket_id ?? undefined,
|
|
838
|
+
sessionId: row.session_id ?? undefined,
|
|
839
|
+
details: row.details ? JSON.parse(row.details) : undefined,
|
|
840
|
+
status: row.status as 'success' | 'failure',
|
|
841
|
+
}));
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
// ==================== 工具函数 ====================
|
|
845
|
+
|
|
846
|
+
// Safe JSON parse helper
|
|
847
|
+
function safeJsonParse<T>(value: unknown, fallback: T): T {
|
|
848
|
+
if (value === null || value === undefined) return fallback;
|
|
849
|
+
if (typeof value === 'string') {
|
|
850
|
+
try {
|
|
851
|
+
return JSON.parse(value);
|
|
852
|
+
} catch {
|
|
853
|
+
return fallback;
|
|
854
|
+
}
|
|
855
|
+
}
|
|
856
|
+
return value as T;
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
function parseTicketRow(row: any): Ticket {
|
|
860
|
+
return {
|
|
861
|
+
id: row.id,
|
|
862
|
+
type: row.type as TicketType,
|
|
863
|
+
title: row.title,
|
|
864
|
+
description: safeJsonParse<string>(row.description, ''),
|
|
865
|
+
status: row.status as TicketStatus,
|
|
866
|
+
priority: row.priority as RiskLevel,
|
|
867
|
+
command: row.command ?? undefined,
|
|
868
|
+
commandType: row.command_type ?? undefined,
|
|
869
|
+
skillName: row.skill_name ?? undefined,
|
|
870
|
+
scriptContent: row.script_content ?? undefined,
|
|
871
|
+
scriptPath: row.script_path ?? undefined,
|
|
872
|
+
output: row.output ?? undefined,
|
|
873
|
+
error: row.error ?? undefined,
|
|
874
|
+
riskLevel: row.risk_level as RiskLevel,
|
|
875
|
+
affectedResources: safeJsonParse<string[] | undefined>(row.affected_resources, undefined),
|
|
876
|
+
approvals: safeJsonParse<Approval[]>(row.approvals, []),
|
|
877
|
+
createdBy: row.created_by,
|
|
878
|
+
createdAt: new Date(row.created_at),
|
|
879
|
+
updatedAt: new Date(row.updated_at),
|
|
880
|
+
aiSessionId: row.ai_session_id ?? undefined,
|
|
881
|
+
aiGenerated: row.ai_generated === 1,
|
|
882
|
+
};
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
// ==================== 文档操作 ====================
|
|
886
|
+
|
|
887
|
+
export function createDocument(document: Omit<Document, 'id' | 'createdAt' | 'updatedAt'>): Document {
|
|
888
|
+
const db = getDb();
|
|
889
|
+
const id = `${Date.now()}-${Math.random().toString(36).substring(2, 15)}`;
|
|
890
|
+
const now = new Date();
|
|
891
|
+
const newDocument: Document = {
|
|
892
|
+
...document,
|
|
893
|
+
id,
|
|
894
|
+
createdAt: now,
|
|
895
|
+
updatedAt: now,
|
|
896
|
+
};
|
|
897
|
+
|
|
898
|
+
const stmt = db.prepare(`
|
|
899
|
+
INSERT INTO documents (id, title, content, category, tags, author, created_at, updated_at, type, source_type, session_id, is_active, file_metadata, is_public)
|
|
900
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
901
|
+
`);
|
|
902
|
+
|
|
903
|
+
stmt.run(
|
|
904
|
+
newDocument.id,
|
|
905
|
+
newDocument.title,
|
|
906
|
+
newDocument.content,
|
|
907
|
+
newDocument.category ?? null,
|
|
908
|
+
newDocument.tags ? JSON.stringify(newDocument.tags) : null,
|
|
909
|
+
newDocument.author,
|
|
910
|
+
now.toISOString(),
|
|
911
|
+
now.toISOString(),
|
|
912
|
+
newDocument.type ?? null,
|
|
913
|
+
newDocument.sourceType ?? null,
|
|
914
|
+
newDocument.sessionId ?? null,
|
|
915
|
+
newDocument.isActive !== false ? 1 : 0,
|
|
916
|
+
newDocument.fileMetadata ? JSON.stringify(newDocument.fileMetadata) : null,
|
|
917
|
+
newDocument.isPublic ? 1 : 0,
|
|
918
|
+
);
|
|
919
|
+
|
|
920
|
+
return newDocument;
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
export function getDocument(id: string): Document | null {
|
|
924
|
+
const db = getDb();
|
|
925
|
+
const stmt = db.prepare('SELECT * FROM documents WHERE id = ?');
|
|
926
|
+
const row = stmt.get(id) as any;
|
|
927
|
+
|
|
928
|
+
if (!row) return null;
|
|
929
|
+
|
|
930
|
+
return parseDocumentRowWithKnowledge(row);
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
export function getDocuments(filters?: {
|
|
934
|
+
category?: string;
|
|
935
|
+
author?: string;
|
|
936
|
+
limit?: number;
|
|
937
|
+
offset?: number;
|
|
938
|
+
isActive?: boolean;
|
|
939
|
+
type?: string;
|
|
940
|
+
search?: string;
|
|
941
|
+
}): Document[] | { documents: Document[]; total: number } {
|
|
942
|
+
const db = getDb();
|
|
943
|
+
|
|
944
|
+
let whereClause = 'WHERE 1=1';
|
|
945
|
+
const params: any[] = [];
|
|
946
|
+
|
|
947
|
+
if (filters?.category) {
|
|
948
|
+
whereClause += ' AND category = ?';
|
|
949
|
+
params.push(filters.category);
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
if (filters?.author) {
|
|
953
|
+
whereClause += ' AND author = ?';
|
|
954
|
+
params.push(filters.author);
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
if (filters?.isActive !== undefined) {
|
|
958
|
+
whereClause += ' AND is_active = ?';
|
|
959
|
+
params.push(filters.isActive ? 1 : 0);
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
if (filters?.type) {
|
|
963
|
+
whereClause += ' AND type = ?';
|
|
964
|
+
params.push(filters.type);
|
|
965
|
+
}
|
|
966
|
+
|
|
967
|
+
if (filters?.search) {
|
|
968
|
+
whereClause += ' AND (title LIKE ? OR content LIKE ?)';
|
|
969
|
+
const searchTerm = `%${filters.search}%`;
|
|
970
|
+
params.push(searchTerm, searchTerm);
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
// 如果请求了分页参数,返回分页结果
|
|
974
|
+
if (filters?.offset !== undefined || filters?.search) {
|
|
975
|
+
// 获取总数
|
|
976
|
+
const countStmt = db.prepare(`SELECT COUNT(*) as count FROM documents ${whereClause}`);
|
|
977
|
+
const countResult = countStmt.get(...params) as { count: number };
|
|
978
|
+
const total = countResult.count;
|
|
979
|
+
|
|
980
|
+
// 获取分页数据
|
|
981
|
+
let query = `SELECT * FROM documents ${whereClause} ORDER BY updated_at DESC`;
|
|
982
|
+
|
|
983
|
+
if (filters?.limit) {
|
|
984
|
+
query += ' LIMIT ?';
|
|
985
|
+
params.push(filters.limit);
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
if (filters?.offset) {
|
|
989
|
+
query += ' OFFSET ?';
|
|
990
|
+
params.push(filters.offset);
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
const stmt = db.prepare(query);
|
|
994
|
+
const rows = stmt.all(...params) as any[];
|
|
995
|
+
|
|
996
|
+
return {
|
|
997
|
+
documents: rows.map(row => parseDocumentRowWithKnowledge(row)),
|
|
998
|
+
total,
|
|
999
|
+
};
|
|
1000
|
+
}
|
|
1001
|
+
|
|
1002
|
+
// 原有逻辑,返回数组
|
|
1003
|
+
let query = `SELECT * FROM documents ${whereClause} ORDER BY updated_at DESC`;
|
|
1004
|
+
|
|
1005
|
+
if (filters?.limit) {
|
|
1006
|
+
query += ' LIMIT ?';
|
|
1007
|
+
params.push(filters.limit);
|
|
1008
|
+
}
|
|
1009
|
+
|
|
1010
|
+
const stmt = db.prepare(query);
|
|
1011
|
+
const rows = stmt.all(...params) as any[];
|
|
1012
|
+
|
|
1013
|
+
return rows.map(row => parseDocumentRowWithKnowledge(row));
|
|
1014
|
+
}
|
|
1015
|
+
|
|
1016
|
+
export function updateDocument(id: string, updates: Partial<Omit<Document, 'id' | 'createdAt' | 'author'>>): Document | null {
|
|
1017
|
+
const db = getDb();
|
|
1018
|
+
const current = getDocument(id);
|
|
1019
|
+
if (!current) return null;
|
|
1020
|
+
|
|
1021
|
+
const updated = {
|
|
1022
|
+
...current,
|
|
1023
|
+
...updates,
|
|
1024
|
+
updatedAt: new Date(),
|
|
1025
|
+
};
|
|
1026
|
+
|
|
1027
|
+
const stmt = db.prepare(`
|
|
1028
|
+
UPDATE documents SET
|
|
1029
|
+
title = ?, content = ?, category = ?, tags = ?, updated_at = ?,
|
|
1030
|
+
type = ?, source_type = ?, session_id = ?, is_active = ?,
|
|
1031
|
+
market_status = ?, market_downloads = ?, market_rating = ?, market_rating_count = ?,
|
|
1032
|
+
file_metadata = ?, is_public = ?
|
|
1033
|
+
WHERE id = ?
|
|
1034
|
+
`);
|
|
1035
|
+
|
|
1036
|
+
stmt.run(
|
|
1037
|
+
updated.title,
|
|
1038
|
+
updated.content,
|
|
1039
|
+
updated.category ?? null,
|
|
1040
|
+
updated.tags ? JSON.stringify(updated.tags) : null,
|
|
1041
|
+
updated.updatedAt.toISOString(),
|
|
1042
|
+
updated.type ?? null,
|
|
1043
|
+
updated.sourceType ?? null,
|
|
1044
|
+
updated.sessionId ?? null,
|
|
1045
|
+
updated.isActive !== false ? 1 : 0,
|
|
1046
|
+
updated.marketStatus ?? null,
|
|
1047
|
+
updated.marketDownloads ?? 0,
|
|
1048
|
+
updated.marketRating ?? 0,
|
|
1049
|
+
updated.marketRatingCount ?? 0,
|
|
1050
|
+
updated.fileMetadata ? JSON.stringify(updated.fileMetadata) : null,
|
|
1051
|
+
updated.isPublic ? 1 : 0,
|
|
1052
|
+
id,
|
|
1053
|
+
);
|
|
1054
|
+
|
|
1055
|
+
return updated;
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
export function deleteDocument(id: string): boolean {
|
|
1059
|
+
const db = getDb();
|
|
1060
|
+
const current = getDocument(id);
|
|
1061
|
+
if (!current) return false;
|
|
1062
|
+
|
|
1063
|
+
const stmt = db.prepare('DELETE FROM documents WHERE id = ?');
|
|
1064
|
+
stmt.run(id);
|
|
1065
|
+
|
|
1066
|
+
return true;
|
|
1067
|
+
}
|
|
1068
|
+
|
|
1069
|
+
// ==================== 知识库操作 ====================
|
|
1070
|
+
|
|
1071
|
+
/**
|
|
1072
|
+
* 获取所有激活的知识条目(用于注入AI会话)
|
|
1073
|
+
*/
|
|
1074
|
+
export function getActiveKnowledge(): Document[] {
|
|
1075
|
+
const db = getDb();
|
|
1076
|
+
|
|
1077
|
+
try {
|
|
1078
|
+
const stmt = db.prepare(`
|
|
1079
|
+
SELECT * FROM documents
|
|
1080
|
+
WHERE is_active = 1 OR is_active IS NULL
|
|
1081
|
+
ORDER BY updated_at DESC
|
|
1082
|
+
`);
|
|
1083
|
+
const rows = stmt.all() as any[];
|
|
1084
|
+
|
|
1085
|
+
return rows.map(row => parseDocumentRowWithKnowledge(row));
|
|
1086
|
+
} catch {
|
|
1087
|
+
// 如果字段不存在,返回所有文档(兼容旧数据库)
|
|
1088
|
+
return getDocuments({}) as Document[];
|
|
1089
|
+
}
|
|
1090
|
+
}
|
|
1091
|
+
|
|
1092
|
+
/**
|
|
1093
|
+
* 根据关键词搜索知识条目
|
|
1094
|
+
*/
|
|
1095
|
+
export function searchKnowledge(keywords: string, limit: number = 5): Document[] {
|
|
1096
|
+
const db = getDb();
|
|
1097
|
+
|
|
1098
|
+
if (!keywords.trim()) {
|
|
1099
|
+
return [];
|
|
1100
|
+
}
|
|
1101
|
+
|
|
1102
|
+
const searchPattern = `%${keywords}%`;
|
|
1103
|
+
|
|
1104
|
+
try {
|
|
1105
|
+
const stmt = db.prepare(`
|
|
1106
|
+
SELECT * FROM documents
|
|
1107
|
+
WHERE (title LIKE ? OR content LIKE ?)
|
|
1108
|
+
AND (is_active = 1 OR is_active IS NULL)
|
|
1109
|
+
ORDER BY updated_at DESC
|
|
1110
|
+
LIMIT ?
|
|
1111
|
+
`);
|
|
1112
|
+
const rows = stmt.all(searchPattern, searchPattern, limit) as any[];
|
|
1113
|
+
|
|
1114
|
+
return rows.map(row => parseDocumentRowWithKnowledge(row));
|
|
1115
|
+
} catch {
|
|
1116
|
+
// 如果字段不存在,返回空数组
|
|
1117
|
+
return [];
|
|
1118
|
+
}
|
|
1119
|
+
}
|
|
1120
|
+
|
|
1121
|
+
/**
|
|
1122
|
+
* 从会话创建知识条目
|
|
1123
|
+
*/
|
|
1124
|
+
export function createKnowledgeFromSession(
|
|
1125
|
+
sessionId: string,
|
|
1126
|
+
summary: string,
|
|
1127
|
+
title?: string
|
|
1128
|
+
): Document {
|
|
1129
|
+
const knowledgeTitle = title || `会话总结 - ${new Date().toLocaleDateString('zh-CN')}`;
|
|
1130
|
+
|
|
1131
|
+
return createDocument({
|
|
1132
|
+
title: knowledgeTitle,
|
|
1133
|
+
content: summary,
|
|
1134
|
+
category: '会话总结',
|
|
1135
|
+
author: 'ai-assistant',
|
|
1136
|
+
type: 'conversation-summary',
|
|
1137
|
+
sourceType: 'conversation-summary',
|
|
1138
|
+
sessionId,
|
|
1139
|
+
isActive: true,
|
|
1140
|
+
});
|
|
1141
|
+
}
|
|
1142
|
+
|
|
1143
|
+
/**
|
|
1144
|
+
* 更新知识条目的激活状态
|
|
1145
|
+
*/
|
|
1146
|
+
export function updateKnowledgeActive(id: string, isActive: boolean): Document | null {
|
|
1147
|
+
const db = getDb();
|
|
1148
|
+
const current = getDocument(id);
|
|
1149
|
+
if (!current) return null;
|
|
1150
|
+
|
|
1151
|
+
const stmt = db.prepare('UPDATE documents SET is_active = ?, updated_at = ? WHERE id = ?');
|
|
1152
|
+
stmt.run(isActive ? 1 : 0, new Date().toISOString(), id);
|
|
1153
|
+
|
|
1154
|
+
return { ...current, isActive, updatedAt: new Date() };
|
|
1155
|
+
}
|
|
1156
|
+
|
|
1157
|
+
/**
|
|
1158
|
+
* 解析包含知识库字段的文档行
|
|
1159
|
+
*/
|
|
1160
|
+
function parseDocumentRowWithKnowledge(row: any): Document {
|
|
1161
|
+
return {
|
|
1162
|
+
id: row.id,
|
|
1163
|
+
title: row.title,
|
|
1164
|
+
content: row.content ?? '',
|
|
1165
|
+
category: row.category ?? undefined,
|
|
1166
|
+
tags: row.tags ? JSON.parse(row.tags) : undefined,
|
|
1167
|
+
author: row.author,
|
|
1168
|
+
createdAt: new Date(row.created_at),
|
|
1169
|
+
updatedAt: new Date(row.updated_at),
|
|
1170
|
+
// 新字段 - 可能不存在(兼容旧数据)
|
|
1171
|
+
type: row.type as KnowledgeType | undefined,
|
|
1172
|
+
sourceType: row.source_type as KnowledgeSourceType | undefined,
|
|
1173
|
+
sessionId: row.session_id ?? undefined,
|
|
1174
|
+
isActive: row.is_active === 1 || row.is_active === undefined,
|
|
1175
|
+
// 市场字段
|
|
1176
|
+
marketStatus: row.market_status ?? undefined,
|
|
1177
|
+
marketDownloads: row.market_downloads ?? 0,
|
|
1178
|
+
marketRating: row.market_rating ?? 0,
|
|
1179
|
+
marketRatingCount: row.market_rating_count ?? 0,
|
|
1180
|
+
// 文件管理字段
|
|
1181
|
+
fileMetadata: row.file_metadata ? safeJsonParse<FileMetadata | undefined>(row.file_metadata, undefined) : undefined,
|
|
1182
|
+
isPublic: row.is_public === 1,
|
|
1183
|
+
};
|
|
1184
|
+
}
|
|
1185
|
+
|
|
1186
|
+
// ==================== 定时任务操作 ====================
|
|
1187
|
+
|
|
1188
|
+
export function createScheduledTask(task: Omit<ScheduledTask, 'id' | 'createdAt' | 'updatedAt'>): ScheduledTask {
|
|
1189
|
+
const db = getDb();
|
|
1190
|
+
const id = `${Date.now()}-${Math.random().toString(36).substring(2, 15)}`;
|
|
1191
|
+
const now = new Date();
|
|
1192
|
+
const newTask: ScheduledTask = {
|
|
1193
|
+
...task,
|
|
1194
|
+
id,
|
|
1195
|
+
createdAt: now,
|
|
1196
|
+
updatedAt: now,
|
|
1197
|
+
};
|
|
1198
|
+
|
|
1199
|
+
const stmt = db.prepare(`
|
|
1200
|
+
INSERT INTO scheduled_tasks (
|
|
1201
|
+
id, title, description, command, status, schedule_type, schedule_expression,
|
|
1202
|
+
next_run_at, last_run_at, created_by, created_at, updated_at
|
|
1203
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
1204
|
+
`);
|
|
1205
|
+
|
|
1206
|
+
stmt.run(
|
|
1207
|
+
newTask.id,
|
|
1208
|
+
newTask.title,
|
|
1209
|
+
newTask.description ?? null,
|
|
1210
|
+
newTask.command,
|
|
1211
|
+
newTask.status,
|
|
1212
|
+
newTask.scheduleType,
|
|
1213
|
+
newTask.scheduleExpression,
|
|
1214
|
+
newTask.nextRunAt?.toISOString() ?? null,
|
|
1215
|
+
newTask.lastRunAt?.toISOString() ?? null,
|
|
1216
|
+
newTask.createdBy,
|
|
1217
|
+
now.toISOString(),
|
|
1218
|
+
now.toISOString(),
|
|
1219
|
+
);
|
|
1220
|
+
|
|
1221
|
+
return newTask;
|
|
1222
|
+
}
|
|
1223
|
+
|
|
1224
|
+
export function getScheduledTask(id: string): ScheduledTask | null {
|
|
1225
|
+
const db = getDb();
|
|
1226
|
+
const stmt = db.prepare('SELECT * FROM scheduled_tasks WHERE id = ?');
|
|
1227
|
+
const row = stmt.get(id) as any;
|
|
1228
|
+
|
|
1229
|
+
if (!row) return null;
|
|
1230
|
+
|
|
1231
|
+
return parseScheduledTaskRow(row);
|
|
1232
|
+
}
|
|
1233
|
+
|
|
1234
|
+
export function getScheduledTasks(filters?: {
|
|
1235
|
+
status?: string;
|
|
1236
|
+
type?: string;
|
|
1237
|
+
createdBy?: string;
|
|
1238
|
+
limit?: number;
|
|
1239
|
+
}): ScheduledTask[] {
|
|
1240
|
+
const db = getDb();
|
|
1241
|
+
|
|
1242
|
+
let query = 'SELECT * FROM scheduled_tasks WHERE 1=1';
|
|
1243
|
+
const params: any[] = [];
|
|
1244
|
+
|
|
1245
|
+
if (filters?.status) {
|
|
1246
|
+
query += ' AND status = ?';
|
|
1247
|
+
params.push(filters.status);
|
|
1248
|
+
}
|
|
1249
|
+
|
|
1250
|
+
if (filters?.type) {
|
|
1251
|
+
query += ' AND type = ?';
|
|
1252
|
+
params.push(filters.type);
|
|
1253
|
+
}
|
|
1254
|
+
|
|
1255
|
+
if (filters?.createdBy) {
|
|
1256
|
+
query += ' AND created_by = ?';
|
|
1257
|
+
params.push(filters.createdBy);
|
|
1258
|
+
}
|
|
1259
|
+
|
|
1260
|
+
query += ' ORDER BY created_at DESC';
|
|
1261
|
+
|
|
1262
|
+
if (filters?.limit) {
|
|
1263
|
+
query += ' LIMIT ?';
|
|
1264
|
+
params.push(filters.limit);
|
|
1265
|
+
}
|
|
1266
|
+
|
|
1267
|
+
const stmt = db.prepare(query);
|
|
1268
|
+
const rows = stmt.all(...params) as any[];
|
|
1269
|
+
|
|
1270
|
+
return rows.map(row => parseScheduledTaskRow(row));
|
|
1271
|
+
}
|
|
1272
|
+
|
|
1273
|
+
export function updateScheduledTask(id: string, updates: Partial<Omit<ScheduledTask, 'id' | 'createdAt' | 'createdBy'>>): ScheduledTask | null {
|
|
1274
|
+
const db = getDb();
|
|
1275
|
+
const current = getScheduledTask(id);
|
|
1276
|
+
if (!current) return null;
|
|
1277
|
+
|
|
1278
|
+
const updated = {
|
|
1279
|
+
...current,
|
|
1280
|
+
...updates,
|
|
1281
|
+
updatedAt: new Date(),
|
|
1282
|
+
};
|
|
1283
|
+
|
|
1284
|
+
const stmt = db.prepare(`
|
|
1285
|
+
UPDATE scheduled_tasks SET
|
|
1286
|
+
title = ?, description = ?, command = ?, status = ?, schedule_type = ?,
|
|
1287
|
+
schedule_expression = ?, next_run_at = ?, last_run_at = ?, updated_at = ?
|
|
1288
|
+
WHERE id = ?
|
|
1289
|
+
`);
|
|
1290
|
+
|
|
1291
|
+
stmt.run(
|
|
1292
|
+
updated.title,
|
|
1293
|
+
updated.description ?? null,
|
|
1294
|
+
updated.command,
|
|
1295
|
+
updated.status,
|
|
1296
|
+
updated.scheduleType,
|
|
1297
|
+
updated.scheduleExpression,
|
|
1298
|
+
updated.nextRunAt?.toISOString() ?? null,
|
|
1299
|
+
updated.lastRunAt?.toISOString() ?? null,
|
|
1300
|
+
updated.updatedAt.toISOString(),
|
|
1301
|
+
id,
|
|
1302
|
+
);
|
|
1303
|
+
|
|
1304
|
+
return updated;
|
|
1305
|
+
}
|
|
1306
|
+
|
|
1307
|
+
export function deleteScheduledTask(id: string): boolean {
|
|
1308
|
+
const db = getDb();
|
|
1309
|
+
const current = getScheduledTask(id);
|
|
1310
|
+
if (!current) return false;
|
|
1311
|
+
|
|
1312
|
+
const stmt = db.prepare('DELETE FROM scheduled_tasks WHERE id = ?');
|
|
1313
|
+
stmt.run(id);
|
|
1314
|
+
|
|
1315
|
+
return true;
|
|
1316
|
+
}
|
|
1317
|
+
|
|
1318
|
+
export function getScheduledTasksDueForExecution(): ScheduledTask[] {
|
|
1319
|
+
const db = getDb();
|
|
1320
|
+
const now = new Date().toISOString();
|
|
1321
|
+
|
|
1322
|
+
const stmt = db.prepare(`
|
|
1323
|
+
SELECT * FROM scheduled_tasks
|
|
1324
|
+
WHERE status = 'active'
|
|
1325
|
+
AND next_run_at IS NOT NULL
|
|
1326
|
+
AND next_run_at <= ?
|
|
1327
|
+
ORDER BY next_run_at ASC
|
|
1328
|
+
`);
|
|
1329
|
+
|
|
1330
|
+
const rows = stmt.all(now) as any[];
|
|
1331
|
+
|
|
1332
|
+
return rows.map(row => parseScheduledTaskRow(row));
|
|
1333
|
+
}
|
|
1334
|
+
|
|
1335
|
+
function parseScheduledTaskRow(row: any): ScheduledTask {
|
|
1336
|
+
return {
|
|
1337
|
+
id: row.id,
|
|
1338
|
+
title: row.title,
|
|
1339
|
+
description: row.description ?? undefined,
|
|
1340
|
+
command: row.command,
|
|
1341
|
+
status: row.status as ScheduledTask['status'],
|
|
1342
|
+
scheduleType: row.schedule_type as ScheduledTask['scheduleType'],
|
|
1343
|
+
scheduleExpression: row.schedule_expression,
|
|
1344
|
+
nextRunAt: row.next_run_at ? new Date(row.next_run_at) : undefined,
|
|
1345
|
+
lastRunAt: row.last_run_at ? new Date(row.last_run_at) : undefined,
|
|
1346
|
+
createdBy: row.created_by,
|
|
1347
|
+
createdAt: new Date(row.created_at),
|
|
1348
|
+
updatedAt: new Date(row.updated_at),
|
|
1349
|
+
};
|
|
1350
|
+
}
|
|
1351
|
+
|
|
1352
|
+
// ==================== 任务执行历史操作 ====================
|
|
1353
|
+
|
|
1354
|
+
export function createTaskExecution(execution: Omit<TaskExecution, 'id' | 'createdAt'>): TaskExecution {
|
|
1355
|
+
const db = getDb();
|
|
1356
|
+
const id = `${Date.now()}-${Math.random().toString(36).substring(2, 15)}`;
|
|
1357
|
+
const now = new Date();
|
|
1358
|
+
const newExecution: TaskExecution = {
|
|
1359
|
+
...execution,
|
|
1360
|
+
id,
|
|
1361
|
+
createdAt: now,
|
|
1362
|
+
};
|
|
1363
|
+
|
|
1364
|
+
const stmt = db.prepare(`
|
|
1365
|
+
INSERT INTO task_executions (
|
|
1366
|
+
id, task_id, status, started_at, completed_at, ticket_id, error_message, created_at
|
|
1367
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
1368
|
+
`);
|
|
1369
|
+
|
|
1370
|
+
stmt.run(
|
|
1371
|
+
newExecution.id,
|
|
1372
|
+
newExecution.taskId,
|
|
1373
|
+
newExecution.status,
|
|
1374
|
+
newExecution.startedAt?.toISOString() ?? null,
|
|
1375
|
+
newExecution.completedAt?.toISOString() ?? null,
|
|
1376
|
+
newExecution.ticketId ?? null,
|
|
1377
|
+
newExecution.errorMessage ?? null,
|
|
1378
|
+
now.toISOString(),
|
|
1379
|
+
);
|
|
1380
|
+
|
|
1381
|
+
return newExecution;
|
|
1382
|
+
}
|
|
1383
|
+
|
|
1384
|
+
export function getTaskExecution(id: string): TaskExecution | null {
|
|
1385
|
+
const db = getDb();
|
|
1386
|
+
const stmt = db.prepare('SELECT * FROM task_executions WHERE id = ?');
|
|
1387
|
+
const row = stmt.get(id) as any;
|
|
1388
|
+
|
|
1389
|
+
if (!row) return null;
|
|
1390
|
+
|
|
1391
|
+
return parseTaskExecutionRow(row);
|
|
1392
|
+
}
|
|
1393
|
+
|
|
1394
|
+
export function getTaskExecutions(filters?: {
|
|
1395
|
+
taskId?: string;
|
|
1396
|
+
status?: string;
|
|
1397
|
+
limit?: number;
|
|
1398
|
+
}): TaskExecution[] {
|
|
1399
|
+
const db = getDb();
|
|
1400
|
+
|
|
1401
|
+
let query = 'SELECT * FROM task_executions WHERE 1=1';
|
|
1402
|
+
const params: any[] = [];
|
|
1403
|
+
|
|
1404
|
+
if (filters?.taskId) {
|
|
1405
|
+
query += ' AND task_id = ?';
|
|
1406
|
+
params.push(filters.taskId);
|
|
1407
|
+
}
|
|
1408
|
+
|
|
1409
|
+
if (filters?.status) {
|
|
1410
|
+
query += ' AND status = ?';
|
|
1411
|
+
params.push(filters.status);
|
|
1412
|
+
}
|
|
1413
|
+
|
|
1414
|
+
query += ' ORDER BY created_at DESC';
|
|
1415
|
+
|
|
1416
|
+
if (filters?.limit) {
|
|
1417
|
+
query += ' LIMIT ?';
|
|
1418
|
+
params.push(filters.limit);
|
|
1419
|
+
}
|
|
1420
|
+
|
|
1421
|
+
const stmt = db.prepare(query);
|
|
1422
|
+
const rows = stmt.all(...params) as any[];
|
|
1423
|
+
|
|
1424
|
+
return rows.map(row => parseTaskExecutionRow(row));
|
|
1425
|
+
}
|
|
1426
|
+
|
|
1427
|
+
export function updateTaskExecution(id: string, updates: Partial<Omit<TaskExecution, 'id' | 'taskId' | 'createdAt'>>): TaskExecution | null {
|
|
1428
|
+
const db = getDb();
|
|
1429
|
+
const current = getTaskExecution(id);
|
|
1430
|
+
if (!current) return null;
|
|
1431
|
+
|
|
1432
|
+
const updated = {
|
|
1433
|
+
...current,
|
|
1434
|
+
...updates,
|
|
1435
|
+
};
|
|
1436
|
+
|
|
1437
|
+
const stmt = db.prepare(`
|
|
1438
|
+
UPDATE task_executions SET
|
|
1439
|
+
status = ?, started_at = ?, completed_at = ?, ticket_id = ?, error_message = ?
|
|
1440
|
+
WHERE id = ?
|
|
1441
|
+
`);
|
|
1442
|
+
|
|
1443
|
+
stmt.run(
|
|
1444
|
+
updated.status,
|
|
1445
|
+
updated.startedAt?.toISOString() ?? null,
|
|
1446
|
+
updated.completedAt?.toISOString() ?? null,
|
|
1447
|
+
updated.ticketId ?? null,
|
|
1448
|
+
updated.errorMessage ?? null,
|
|
1449
|
+
id,
|
|
1450
|
+
);
|
|
1451
|
+
|
|
1452
|
+
return updated;
|
|
1453
|
+
}
|
|
1454
|
+
|
|
1455
|
+
function parseTaskExecutionRow(row: any): TaskExecution {
|
|
1456
|
+
return {
|
|
1457
|
+
id: row.id,
|
|
1458
|
+
taskId: row.task_id,
|
|
1459
|
+
status: row.status as TaskExecution['status'],
|
|
1460
|
+
startedAt: row.started_at ? new Date(row.started_at) : undefined,
|
|
1461
|
+
completedAt: row.completed_at ? new Date(row.completed_at) : undefined,
|
|
1462
|
+
ticketId: row.ticket_id ?? undefined,
|
|
1463
|
+
errorMessage: row.error_message ?? undefined,
|
|
1464
|
+
createdAt: new Date(row.created_at),
|
|
1465
|
+
};
|
|
1466
|
+
}
|
|
1467
|
+
|
|
1468
|
+
// ==================== 用户相关函数 ====================
|
|
1469
|
+
|
|
1470
|
+
export function createUser(username: string, role: 'admin' | 'guest' = 'guest'): User {
|
|
1471
|
+
const db = getDb();
|
|
1472
|
+
const id = `user-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`;
|
|
1473
|
+
const now = new Date().toISOString();
|
|
1474
|
+
|
|
1475
|
+
const stmt = db.prepare(`
|
|
1476
|
+
INSERT INTO users (id, username, role, created_at, updated_at, last_login_at)
|
|
1477
|
+
VALUES (?, ?, ?, ?, ?, ?)
|
|
1478
|
+
`);
|
|
1479
|
+
stmt.run(id, username, role, now, now, now);
|
|
1480
|
+
|
|
1481
|
+
return {
|
|
1482
|
+
id,
|
|
1483
|
+
username,
|
|
1484
|
+
role,
|
|
1485
|
+
createdAt: new Date(now),
|
|
1486
|
+
updatedAt: new Date(now),
|
|
1487
|
+
lastLoginAt: new Date(now),
|
|
1488
|
+
};
|
|
1489
|
+
}
|
|
1490
|
+
|
|
1491
|
+
export function getUserByUsername(username: string): User | null {
|
|
1492
|
+
const db = getDb();
|
|
1493
|
+
const stmt = db.prepare('SELECT * FROM users WHERE username = ?');
|
|
1494
|
+
const row = stmt.get(username) as any;
|
|
1495
|
+
if (!row) return null;
|
|
1496
|
+
return {
|
|
1497
|
+
id: row.id,
|
|
1498
|
+
username: row.username,
|
|
1499
|
+
role: (row.role || 'guest') as 'admin' | 'guest',
|
|
1500
|
+
createdAt: new Date(row.created_at),
|
|
1501
|
+
updatedAt: new Date(row.updated_at),
|
|
1502
|
+
lastLoginAt: row.last_login_at ? new Date(row.last_login_at) : undefined,
|
|
1503
|
+
settings: row.settings ? JSON.parse(row.settings) : undefined,
|
|
1504
|
+
feishuUnionId: row.feishu_union_id || undefined,
|
|
1505
|
+
feishuOpenId: row.feishu_open_id || undefined,
|
|
1506
|
+
displayName: row.display_name || undefined,
|
|
1507
|
+
avatar: row.avatar || undefined,
|
|
1508
|
+
};
|
|
1509
|
+
}
|
|
1510
|
+
|
|
1511
|
+
export function getUserById(id: string): User | null {
|
|
1512
|
+
const db = getDb();
|
|
1513
|
+
const stmt = db.prepare('SELECT * FROM users WHERE id = ?');
|
|
1514
|
+
const row = stmt.get(id) as any;
|
|
1515
|
+
if (!row) return null;
|
|
1516
|
+
return {
|
|
1517
|
+
id: row.id,
|
|
1518
|
+
username: row.username,
|
|
1519
|
+
role: (row.role || 'guest') as 'admin' | 'guest',
|
|
1520
|
+
createdAt: new Date(row.created_at),
|
|
1521
|
+
updatedAt: new Date(row.updated_at),
|
|
1522
|
+
lastLoginAt: row.last_login_at ? new Date(row.last_login_at) : undefined,
|
|
1523
|
+
settings: row.settings ? JSON.parse(row.settings) : undefined,
|
|
1524
|
+
feishuUnionId: row.feishu_union_id || undefined,
|
|
1525
|
+
feishuOpenId: row.feishu_open_id || undefined,
|
|
1526
|
+
displayName: row.display_name || undefined,
|
|
1527
|
+
avatar: row.avatar || undefined,
|
|
1528
|
+
};
|
|
1529
|
+
}
|
|
1530
|
+
|
|
1531
|
+
export function updateUserLogin(id: string): void {
|
|
1532
|
+
const db = getDb();
|
|
1533
|
+
const now = new Date().toISOString();
|
|
1534
|
+
const stmt = db.prepare('UPDATE users SET last_login_at = ?, updated_at = ? WHERE id = ?');
|
|
1535
|
+
stmt.run(now, now, id);
|
|
1536
|
+
}
|
|
1537
|
+
|
|
1538
|
+
export function updateUserRole(id: string, role: 'admin' | 'guest'): void {
|
|
1539
|
+
const db = getDb();
|
|
1540
|
+
const now = new Date().toISOString();
|
|
1541
|
+
const stmt = db.prepare('UPDATE users SET role = ?, updated_at = ? WHERE id = ?');
|
|
1542
|
+
stmt.run(role, now, id);
|
|
1543
|
+
}
|
|
1544
|
+
|
|
1545
|
+
export function getUserByFeishuUnionId(unionId: string): User | null {
|
|
1546
|
+
const db = getDb();
|
|
1547
|
+
const stmt = db.prepare('SELECT * FROM users WHERE feishu_union_id = ?');
|
|
1548
|
+
const row = stmt.get(unionId) as any;
|
|
1549
|
+
if (!row) return null;
|
|
1550
|
+
return {
|
|
1551
|
+
id: row.id,
|
|
1552
|
+
username: row.username,
|
|
1553
|
+
role: (row.role || 'guest') as 'admin' | 'guest',
|
|
1554
|
+
createdAt: new Date(row.created_at),
|
|
1555
|
+
updatedAt: new Date(row.updated_at),
|
|
1556
|
+
lastLoginAt: row.last_login_at ? new Date(row.last_login_at) : undefined,
|
|
1557
|
+
settings: row.settings ? JSON.parse(row.settings) : undefined,
|
|
1558
|
+
feishuUnionId: row.feishu_union_id || undefined,
|
|
1559
|
+
feishuOpenId: row.feishu_open_id || undefined,
|
|
1560
|
+
displayName: row.display_name || undefined,
|
|
1561
|
+
avatar: row.avatar || undefined,
|
|
1562
|
+
};
|
|
1563
|
+
}
|
|
1564
|
+
|
|
1565
|
+
export function updateUserFeishuInfo(
|
|
1566
|
+
id: string,
|
|
1567
|
+
feishuInfo: { unionId?: string; openId?: string; avatar?: string; displayName?: string }
|
|
1568
|
+
): void {
|
|
1569
|
+
const db = getDb();
|
|
1570
|
+
const now = new Date().toISOString();
|
|
1571
|
+
const updates: string[] = ['updated_at = ?'];
|
|
1572
|
+
const params: any[] = [now];
|
|
1573
|
+
|
|
1574
|
+
if (feishuInfo.unionId !== undefined) {
|
|
1575
|
+
updates.push('feishu_union_id = ?');
|
|
1576
|
+
params.push(feishuInfo.unionId);
|
|
1577
|
+
}
|
|
1578
|
+
if (feishuInfo.openId !== undefined) {
|
|
1579
|
+
updates.push('feishu_open_id = ?');
|
|
1580
|
+
params.push(feishuInfo.openId);
|
|
1581
|
+
}
|
|
1582
|
+
if (feishuInfo.avatar !== undefined) {
|
|
1583
|
+
updates.push('avatar = ?');
|
|
1584
|
+
params.push(feishuInfo.avatar);
|
|
1585
|
+
}
|
|
1586
|
+
if (feishuInfo.displayName !== undefined) {
|
|
1587
|
+
updates.push('display_name = ?');
|
|
1588
|
+
params.push(feishuInfo.displayName);
|
|
1589
|
+
}
|
|
1590
|
+
|
|
1591
|
+
params.push(id);
|
|
1592
|
+
const stmt = db.prepare(`UPDATE users SET ${updates.join(', ')} WHERE id = ?`);
|
|
1593
|
+
stmt.run(...params);
|
|
1594
|
+
}
|
|
1595
|
+
|
|
1596
|
+
// ==================== 内置Skill相关函数 ====================
|
|
1597
|
+
|
|
1598
|
+
export function getBuiltinSkills(): BuiltinSkill[] {
|
|
1599
|
+
const db = getDb();
|
|
1600
|
+
const stmt = db.prepare('SELECT * FROM builtin_skills ORDER BY skill_name');
|
|
1601
|
+
const rows = stmt.all() as any[];
|
|
1602
|
+
return rows.map(row => ({
|
|
1603
|
+
id: row.id,
|
|
1604
|
+
skillName: row.skill_name,
|
|
1605
|
+
enabled: !!row.enabled,
|
|
1606
|
+
config: row.config ? JSON.parse(row.config) : undefined,
|
|
1607
|
+
createdAt: new Date(row.created_at),
|
|
1608
|
+
}));
|
|
1609
|
+
}
|
|
1610
|
+
|
|
1611
|
+
export function setBuiltinSkill(skillName: string, enabled: boolean, config?: Record<string, unknown>): void {
|
|
1612
|
+
const db = getDb();
|
|
1613
|
+
const now = new Date().toISOString();
|
|
1614
|
+
const id = `builtin-${skillName}`;
|
|
1615
|
+
|
|
1616
|
+
const stmt = db.prepare(`
|
|
1617
|
+
INSERT INTO builtin_skills (id, skill_name, enabled, config, created_at)
|
|
1618
|
+
VALUES (?, ?, ?, ?, ?)
|
|
1619
|
+
ON CONFLICT(id) DO UPDATE SET enabled = ?, config = ?
|
|
1620
|
+
`);
|
|
1621
|
+
stmt.run(id, skillName, enabled ? 1 : 0, config ? JSON.stringify(config) : null, now, enabled ? 1 : 0, config ? JSON.stringify(config) : null);
|
|
1622
|
+
}
|
|
1623
|
+
|
|
1624
|
+
// ==================== 用户Skill相关函数 ====================
|
|
1625
|
+
|
|
1626
|
+
export function getUserSkills(userId: string): UserSkill[] {
|
|
1627
|
+
const db = getDb();
|
|
1628
|
+
const stmt = db.prepare('SELECT * FROM user_skills WHERE user_id = ? ORDER BY use_count DESC');
|
|
1629
|
+
const rows = stmt.all(userId) as any[];
|
|
1630
|
+
return rows.map(row => ({
|
|
1631
|
+
id: row.id,
|
|
1632
|
+
userId: row.user_id,
|
|
1633
|
+
skillName: row.skill_name,
|
|
1634
|
+
source: row.source as UserSkill['source'],
|
|
1635
|
+
skillData: row.skill_data ? JSON.parse(row.skill_data) : undefined,
|
|
1636
|
+
useCount: row.use_count,
|
|
1637
|
+
installedAt: new Date(row.installed_at),
|
|
1638
|
+
lastUsedAt: row.last_used_at ? new Date(row.last_used_at) : undefined,
|
|
1639
|
+
}));
|
|
1640
|
+
}
|
|
1641
|
+
|
|
1642
|
+
export function installUserSkill(userId: string, skillName: string, source: UserSkill['source'], skillData?: Record<string, unknown>): UserSkill {
|
|
1643
|
+
const db = getDb();
|
|
1644
|
+
const id = `us-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`;
|
|
1645
|
+
const now = new Date().toISOString();
|
|
1646
|
+
|
|
1647
|
+
const existing = db.prepare('SELECT id FROM user_skills WHERE user_id = ? AND skill_name = ?').get(userId, skillName) as { id: string } | undefined;
|
|
1648
|
+
|
|
1649
|
+
if (existing) {
|
|
1650
|
+
db.prepare(`
|
|
1651
|
+
UPDATE user_skills SET source = ?, skill_data = ? WHERE user_id = ? AND skill_name = ?
|
|
1652
|
+
`).run(source, skillData ? JSON.stringify(skillData) : null, userId, skillName);
|
|
1653
|
+
return {
|
|
1654
|
+
id: existing.id as string,
|
|
1655
|
+
userId,
|
|
1656
|
+
skillName,
|
|
1657
|
+
source,
|
|
1658
|
+
skillData,
|
|
1659
|
+
useCount: 0,
|
|
1660
|
+
installedAt: new Date(now),
|
|
1661
|
+
};
|
|
1662
|
+
}
|
|
1663
|
+
|
|
1664
|
+
const stmt = db.prepare(`
|
|
1665
|
+
INSERT INTO user_skills (id, user_id, skill_name, source, skill_data, use_count, installed_at)
|
|
1666
|
+
VALUES (?, ?, ?, ?, ?, 0, ?)
|
|
1667
|
+
`);
|
|
1668
|
+
stmt.run(id, userId, skillName, source, skillData ? JSON.stringify(skillData) : null, now);
|
|
1669
|
+
|
|
1670
|
+
return {
|
|
1671
|
+
id,
|
|
1672
|
+
userId,
|
|
1673
|
+
skillName,
|
|
1674
|
+
source,
|
|
1675
|
+
skillData,
|
|
1676
|
+
useCount: 0,
|
|
1677
|
+
installedAt: new Date(now),
|
|
1678
|
+
};
|
|
1679
|
+
}
|
|
1680
|
+
|
|
1681
|
+
export function uninstallUserSkill(userId: string, skillName: string): void {
|
|
1682
|
+
const db = getDb();
|
|
1683
|
+
|
|
1684
|
+
const row = db.prepare(
|
|
1685
|
+
'SELECT dir_name FROM user_skills WHERE user_id = ? AND skill_name = ?'
|
|
1686
|
+
).get(userId, skillName) as { dir_name: string } | undefined;
|
|
1687
|
+
|
|
1688
|
+
const dirName = row?.dir_name || skillName;
|
|
1689
|
+
const skillDir = join(process.cwd(), 'data', 'skills', 'users', userId, dirName);
|
|
1690
|
+
|
|
1691
|
+
if (existsSync(skillDir)) {
|
|
1692
|
+
try {
|
|
1693
|
+
rmSync(skillDir, { recursive: true });
|
|
1694
|
+
} catch (error) {
|
|
1695
|
+
console.error(`Failed to delete skill directory ${skillDir}:`, error);
|
|
1696
|
+
}
|
|
1697
|
+
}
|
|
1698
|
+
|
|
1699
|
+
const stmt = db.prepare('DELETE FROM user_skills WHERE user_id = ? AND skill_name = ?');
|
|
1700
|
+
stmt.run(userId, skillName);
|
|
1701
|
+
}
|
|
1702
|
+
|
|
1703
|
+
export function incrementUserSkillUseCount(userId: string, skillName: string): void {
|
|
1704
|
+
const db = getDb();
|
|
1705
|
+
const now = new Date().toISOString();
|
|
1706
|
+
const stmt = db.prepare(`
|
|
1707
|
+
UPDATE user_skills SET use_count = use_count + 1, last_used_at = ?
|
|
1708
|
+
WHERE user_id = ? AND skill_name = ?
|
|
1709
|
+
`);
|
|
1710
|
+
stmt.run(now, userId, skillName);
|
|
1711
|
+
}
|
|
1712
|
+
|
|
1713
|
+
// ==================== Skill目录名映射函数 ====================
|
|
1714
|
+
|
|
1715
|
+
export function findDirNameBySkillName(userId: string, skillName: string): string | null {
|
|
1716
|
+
const db = getDb();
|
|
1717
|
+
const row = db.prepare(
|
|
1718
|
+
'SELECT dir_name FROM user_skills WHERE user_id = ? AND skill_name = ?'
|
|
1719
|
+
).get(userId, skillName) as { dir_name: string } | undefined;
|
|
1720
|
+
return row?.dir_name || null;
|
|
1721
|
+
}
|
|
1722
|
+
|
|
1723
|
+
export function findSkillNameByDirName(userId: string, dirName: string): string | null {
|
|
1724
|
+
const db = getDb();
|
|
1725
|
+
const row = db.prepare(
|
|
1726
|
+
'SELECT skill_name FROM user_skills WHERE user_id = ? AND dir_name = ?'
|
|
1727
|
+
).get(userId, dirName) as { skill_name: string } | undefined;
|
|
1728
|
+
return row?.skill_name || null;
|
|
1729
|
+
}
|
|
1730
|
+
|
|
1731
|
+
// ==================== Skill使用统计相关函数 ====================
|
|
1732
|
+
|
|
1733
|
+
export function recordSkillUsage(usage: Omit<SkillUsage, 'id' | 'calledAt'>): SkillUsage {
|
|
1734
|
+
const db = getDb();
|
|
1735
|
+
const id = `su-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`;
|
|
1736
|
+
const now = new Date().toISOString();
|
|
1737
|
+
|
|
1738
|
+
const stmt = db.prepare(`
|
|
1739
|
+
INSERT INTO skill_usage (id, skill_name, session_id, user_id, context, result, success, called_at)
|
|
1740
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
1741
|
+
`);
|
|
1742
|
+
stmt.run(id, usage.skillName, usage.sessionId, usage.userId, usage.context, usage.result, usage.success ? 1 : 0, now);
|
|
1743
|
+
|
|
1744
|
+
return {
|
|
1745
|
+
...usage,
|
|
1746
|
+
id,
|
|
1747
|
+
calledAt: new Date(now),
|
|
1748
|
+
};
|
|
1749
|
+
}
|
|
1750
|
+
|
|
1751
|
+
export function getSkillUsageStats(skillName: string): { total: number; success: number; fail: number } {
|
|
1752
|
+
const db = getDb();
|
|
1753
|
+
const stmt = db.prepare(`
|
|
1754
|
+
SELECT
|
|
1755
|
+
COUNT(*) as total,
|
|
1756
|
+
SUM(CASE WHEN success = 1 THEN 1 ELSE 0 END) as success_count,
|
|
1757
|
+
SUM(CASE WHEN success = 0 THEN 1 ELSE 0 END) as fail_count
|
|
1758
|
+
FROM skill_usage WHERE skill_name = ?
|
|
1759
|
+
`);
|
|
1760
|
+
const row = stmt.get(skillName) as any;
|
|
1761
|
+
return {
|
|
1762
|
+
total: row?.total || 0,
|
|
1763
|
+
success: row?.success_count || 0,
|
|
1764
|
+
fail: row?.fail_count || 0,
|
|
1765
|
+
};
|
|
1766
|
+
}
|
|
1767
|
+
|
|
1768
|
+
// ==================== 市场知识相关函数 ====================
|
|
1769
|
+
|
|
1770
|
+
export function getMarketKnowledge(filters?: { category?: string; search?: string; limit?: number; offset?: number }): Document[] | { documents: Document[]; total: number } {
|
|
1771
|
+
const db = getDb();
|
|
1772
|
+
let whereClause = 'WHERE market_status = ?';
|
|
1773
|
+
const params: any[] = ['published'];
|
|
1774
|
+
|
|
1775
|
+
if (filters?.category) {
|
|
1776
|
+
whereClause += ' AND category = ?';
|
|
1777
|
+
params.push(filters.category);
|
|
1778
|
+
}
|
|
1779
|
+
|
|
1780
|
+
if (filters?.search) {
|
|
1781
|
+
whereClause += ' AND (title LIKE ? OR content LIKE ?)';
|
|
1782
|
+
const searchTerm = `%${filters.search}%`;
|
|
1783
|
+
params.push(searchTerm, searchTerm);
|
|
1784
|
+
}
|
|
1785
|
+
|
|
1786
|
+
// 如果有分页参数,返回分页结果
|
|
1787
|
+
if (filters?.offset !== undefined) {
|
|
1788
|
+
// 获取总数
|
|
1789
|
+
const countStmt = db.prepare(`SELECT COUNT(*) as count FROM documents ${whereClause}`);
|
|
1790
|
+
const countResult = countStmt.get(...params) as { count: number };
|
|
1791
|
+
const total = countResult.count;
|
|
1792
|
+
|
|
1793
|
+
// 获取分页数据
|
|
1794
|
+
let query = `SELECT * FROM documents ${whereClause} ORDER BY market_downloads DESC, market_rating DESC`;
|
|
1795
|
+
|
|
1796
|
+
if (filters?.limit) {
|
|
1797
|
+
query += ' LIMIT ?';
|
|
1798
|
+
params.push(filters.limit);
|
|
1799
|
+
}
|
|
1800
|
+
|
|
1801
|
+
if (filters?.offset) {
|
|
1802
|
+
query += ' OFFSET ?';
|
|
1803
|
+
params.push(filters.offset);
|
|
1804
|
+
}
|
|
1805
|
+
|
|
1806
|
+
const stmt = db.prepare(query);
|
|
1807
|
+
const rows = stmt.all(...params) as any[];
|
|
1808
|
+
|
|
1809
|
+
return {
|
|
1810
|
+
documents: rows.map(row => parseDocumentRowWithKnowledge(row)),
|
|
1811
|
+
total,
|
|
1812
|
+
};
|
|
1813
|
+
}
|
|
1814
|
+
|
|
1815
|
+
// 原有逻辑,返回数组
|
|
1816
|
+
let query = `SELECT * FROM documents ${whereClause} ORDER BY market_downloads DESC, market_rating DESC`;
|
|
1817
|
+
|
|
1818
|
+
if (filters?.limit) {
|
|
1819
|
+
query += ' LIMIT ?';
|
|
1820
|
+
params.push(filters.limit);
|
|
1821
|
+
}
|
|
1822
|
+
|
|
1823
|
+
const stmt = db.prepare(query);
|
|
1824
|
+
const rows = stmt.all(...params) as any[];
|
|
1825
|
+
return rows.map(row => parseDocumentRowWithKnowledge(row));
|
|
1826
|
+
}
|
|
1827
|
+
|
|
1828
|
+
export function publishKnowledgeToMarket(id: string, author: string): Document | null {
|
|
1829
|
+
const db = getDb();
|
|
1830
|
+
const now = new Date().toISOString();
|
|
1831
|
+
const stmt = db.prepare(`
|
|
1832
|
+
UPDATE documents SET market_status = ?, updated_at = ? WHERE id = ?
|
|
1833
|
+
`);
|
|
1834
|
+
stmt.run('published', now, id);
|
|
1835
|
+
|
|
1836
|
+
const getStmt = db.prepare('SELECT * FROM documents WHERE id = ?');
|
|
1837
|
+
const row = getStmt.get(id) as any;
|
|
1838
|
+
if (!row) return null;
|
|
1839
|
+
return parseDocumentRowWithKnowledge(row);
|
|
1840
|
+
}
|
|
1841
|
+
|
|
1842
|
+
// ==================== 技能市场相关类型 ====================
|
|
1843
|
+
|
|
1844
|
+
export interface SkillMarket {
|
|
1845
|
+
id: string;
|
|
1846
|
+
name: string;
|
|
1847
|
+
displayName: string;
|
|
1848
|
+
description?: string;
|
|
1849
|
+
version: string;
|
|
1850
|
+
author: string;
|
|
1851
|
+
authorName?: string;
|
|
1852
|
+
status: 'pending' | 'approved' | 'rejected' | 'archived';
|
|
1853
|
+
isOfficial: boolean;
|
|
1854
|
+
ratingAvg: number;
|
|
1855
|
+
ratingCount: number;
|
|
1856
|
+
downloadCount: number;
|
|
1857
|
+
filePath?: string;
|
|
1858
|
+
reviewedBy?: string;
|
|
1859
|
+
reviewedAt?: string;
|
|
1860
|
+
reviewComment?: string;
|
|
1861
|
+
createdAt: string;
|
|
1862
|
+
updatedAt: string;
|
|
1863
|
+
}
|
|
1864
|
+
|
|
1865
|
+
export interface SkillRating {
|
|
1866
|
+
id: string;
|
|
1867
|
+
skillId: string;
|
|
1868
|
+
userId: string;
|
|
1869
|
+
rating: number;
|
|
1870
|
+
comment?: string;
|
|
1871
|
+
createdAt: string;
|
|
1872
|
+
}
|
|
1873
|
+
|
|
1874
|
+
export interface UserInstalledSkill {
|
|
1875
|
+
id: string;
|
|
1876
|
+
userId: string;
|
|
1877
|
+
skillId: string;
|
|
1878
|
+
installedAt: string;
|
|
1879
|
+
}
|
|
1880
|
+
|
|
1881
|
+
// ==================== 会话列表相关类型 ====================
|
|
1882
|
+
|
|
1883
|
+
export interface Session {
|
|
1884
|
+
sessionId: string;
|
|
1885
|
+
userId?: string;
|
|
1886
|
+
source?: 'web' | 'feishu_bot' | 'api';
|
|
1887
|
+
sourceUserId?: string;
|
|
1888
|
+
createdAt: Date;
|
|
1889
|
+
lastAccessedAt: Date;
|
|
1890
|
+
messageCount: number;
|
|
1891
|
+
recentSummary?: string;
|
|
1892
|
+
title?: string;
|
|
1893
|
+
}
|
|
1894
|
+
|
|
1895
|
+
// ==================== 会话列表操作 ====================
|
|
1896
|
+
|
|
1897
|
+
export function saveSession(session: Omit<Session, 'createdAt' | 'lastAccessedAt'> & { createdAt: string | Date; lastAccessedAt: string | Date }): Session {
|
|
1898
|
+
const db = getDb();
|
|
1899
|
+
const now = new Date();
|
|
1900
|
+
const createdAt = session.createdAt instanceof Date ? session.createdAt.toISOString() : session.createdAt;
|
|
1901
|
+
const lastAccessedAt = session.lastAccessedAt instanceof Date ? session.lastAccessedAt.toISOString() : session.lastAccessedAt;
|
|
1902
|
+
|
|
1903
|
+
const stmt = db.prepare(`
|
|
1904
|
+
INSERT INTO sessions (session_id, user_id, source, source_user_id, created_at, last_accessed_at, message_count, recent_summary)
|
|
1905
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
1906
|
+
ON CONFLICT(session_id) DO UPDATE SET
|
|
1907
|
+
user_id = excluded.user_id,
|
|
1908
|
+
source = excluded.source,
|
|
1909
|
+
source_user_id = excluded.source_user_id,
|
|
1910
|
+
last_accessed_at = excluded.last_accessed_at,
|
|
1911
|
+
message_count = excluded.message_count,
|
|
1912
|
+
recent_summary = excluded.recent_summary
|
|
1913
|
+
`);
|
|
1914
|
+
stmt.run(
|
|
1915
|
+
session.sessionId,
|
|
1916
|
+
session.userId ?? null,
|
|
1917
|
+
session.source ?? null,
|
|
1918
|
+
session.sourceUserId ?? null,
|
|
1919
|
+
createdAt,
|
|
1920
|
+
lastAccessedAt,
|
|
1921
|
+
session.messageCount,
|
|
1922
|
+
session.recentSummary ?? null
|
|
1923
|
+
);
|
|
1924
|
+
|
|
1925
|
+
return {
|
|
1926
|
+
sessionId: session.sessionId,
|
|
1927
|
+
userId: session.userId,
|
|
1928
|
+
source: session.source,
|
|
1929
|
+
sourceUserId: session.sourceUserId,
|
|
1930
|
+
createdAt: new Date(createdAt),
|
|
1931
|
+
lastAccessedAt: new Date(lastAccessedAt),
|
|
1932
|
+
messageCount: session.messageCount,
|
|
1933
|
+
recentSummary: session.recentSummary,
|
|
1934
|
+
};
|
|
1935
|
+
}
|
|
1936
|
+
|
|
1937
|
+
export function getSession(sessionId: string): Session | null {
|
|
1938
|
+
const db = getDb();
|
|
1939
|
+
const stmt = db.prepare('SELECT * FROM sessions WHERE session_id = ?');
|
|
1940
|
+
const row = stmt.get(sessionId) as any;
|
|
1941
|
+
|
|
1942
|
+
if (!row) return null;
|
|
1943
|
+
|
|
1944
|
+
return {
|
|
1945
|
+
sessionId: row.session_id,
|
|
1946
|
+
userId: row.user_id ?? undefined,
|
|
1947
|
+
source: row.source ?? undefined,
|
|
1948
|
+
sourceUserId: row.source_user_id ?? undefined,
|
|
1949
|
+
createdAt: new Date(row.created_at),
|
|
1950
|
+
lastAccessedAt: new Date(row.last_accessed_at),
|
|
1951
|
+
messageCount: row.message_count,
|
|
1952
|
+
recentSummary: row.recent_summary ?? undefined,
|
|
1953
|
+
title: row.title ?? undefined,
|
|
1954
|
+
};
|
|
1955
|
+
}
|
|
1956
|
+
|
|
1957
|
+
export function getAllSessions(userId?: string): Session[] {
|
|
1958
|
+
const db = getDb();
|
|
1959
|
+
|
|
1960
|
+
let stmt;
|
|
1961
|
+
let rows: any[];
|
|
1962
|
+
|
|
1963
|
+
if (userId) {
|
|
1964
|
+
stmt = db.prepare('SELECT * FROM sessions WHERE user_id = ? ORDER BY last_accessed_at DESC');
|
|
1965
|
+
rows = stmt.all(userId);
|
|
1966
|
+
} else {
|
|
1967
|
+
stmt = db.prepare('SELECT * FROM sessions ORDER BY last_accessed_at DESC');
|
|
1968
|
+
rows = stmt.all();
|
|
1969
|
+
}
|
|
1970
|
+
|
|
1971
|
+
return rows.map(row => ({
|
|
1972
|
+
sessionId: row.session_id,
|
|
1973
|
+
userId: row.user_id ?? undefined,
|
|
1974
|
+
createdAt: new Date(row.created_at),
|
|
1975
|
+
lastAccessedAt: new Date(row.last_accessed_at),
|
|
1976
|
+
messageCount: row.message_count,
|
|
1977
|
+
recentSummary: row.recent_summary ?? undefined,
|
|
1978
|
+
title: row.title ?? undefined,
|
|
1979
|
+
}));
|
|
1980
|
+
}
|
|
1981
|
+
|
|
1982
|
+
export function deleteSession(sessionId: string): boolean {
|
|
1983
|
+
const db = getDb();
|
|
1984
|
+
const stmt = db.prepare('DELETE FROM sessions WHERE session_id = ?');
|
|
1985
|
+
const result = stmt.run(sessionId);
|
|
1986
|
+
return result.changes > 0;
|
|
1987
|
+
}
|
|
1988
|
+
|
|
1989
|
+
export function updateSessionLastAccessed(sessionId: string): void {
|
|
1990
|
+
const db = getDb();
|
|
1991
|
+
const now = new Date().toISOString();
|
|
1992
|
+
const stmt = db.prepare('UPDATE sessions SET last_accessed_at = ? WHERE session_id = ?');
|
|
1993
|
+
stmt.run(now, sessionId);
|
|
1994
|
+
}
|
|
1995
|
+
|
|
1996
|
+
export function updateSessionTitle(sessionId: string, title: string): void {
|
|
1997
|
+
const db = getDb();
|
|
1998
|
+
const stmt = db.prepare('UPDATE sessions SET title = ? WHERE session_id = ?');
|
|
1999
|
+
stmt.run(title, sessionId);
|
|
2000
|
+
}
|
|
2001
|
+
|
|
2002
|
+
// ==================== 统计分析相关函数 ====================
|
|
2003
|
+
|
|
2004
|
+
export interface SessionStats {
|
|
2005
|
+
totalSessions: number;
|
|
2006
|
+
activeUsers: number;
|
|
2007
|
+
totalMessages: number;
|
|
2008
|
+
totalSkillCalls: number;
|
|
2009
|
+
aiGeneratedTickets: number;
|
|
2010
|
+
dateRange: { start: string; end: string };
|
|
2011
|
+
}
|
|
2012
|
+
|
|
2013
|
+
export interface TrendData {
|
|
2014
|
+
date: string;
|
|
2015
|
+
sessionCount: number;
|
|
2016
|
+
userCount: number;
|
|
2017
|
+
}
|
|
2018
|
+
|
|
2019
|
+
export interface UserStats {
|
|
2020
|
+
userId: string;
|
|
2021
|
+
sessionCount: number;
|
|
2022
|
+
messageCount: number;
|
|
2023
|
+
skillCallCount: number;
|
|
2024
|
+
lastActiveAt: string;
|
|
2025
|
+
}
|
|
2026
|
+
|
|
2027
|
+
export interface SkillStats {
|
|
2028
|
+
skillName: string;
|
|
2029
|
+
callCount: number;
|
|
2030
|
+
successCount: number;
|
|
2031
|
+
failCount: number;
|
|
2032
|
+
successRate: number;
|
|
2033
|
+
lastCalledAt: string;
|
|
2034
|
+
}
|
|
2035
|
+
|
|
2036
|
+
export interface KnowledgeStats {
|
|
2037
|
+
documentId: string;
|
|
2038
|
+
title: string;
|
|
2039
|
+
category: string;
|
|
2040
|
+
引用次数: number;
|
|
2041
|
+
}
|
|
2042
|
+
|
|
2043
|
+
export function getSessionStats(period: '7d' | '30d' | 'all'): SessionStats {
|
|
2044
|
+
const db = getDb();
|
|
2045
|
+
const now = new Date();
|
|
2046
|
+
let startDate: Date;
|
|
2047
|
+
let dateClause = '';
|
|
2048
|
+
|
|
2049
|
+
if (period === '7d') {
|
|
2050
|
+
startDate = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000);
|
|
2051
|
+
} else if (period === '30d') {
|
|
2052
|
+
startDate = new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000);
|
|
2053
|
+
} else {
|
|
2054
|
+
startDate = new Date(0);
|
|
2055
|
+
}
|
|
2056
|
+
|
|
2057
|
+
if (period !== 'all') {
|
|
2058
|
+
dateClause = `WHERE created_at >= '${startDate.toISOString()}'`;
|
|
2059
|
+
}
|
|
2060
|
+
|
|
2061
|
+
// 总会话数
|
|
2062
|
+
const sessionStmt = db.prepare(`SELECT COUNT(*) as total FROM sessions ${dateClause}`);
|
|
2063
|
+
const totalSessions = (sessionStmt.get() as any)?.total || 0;
|
|
2064
|
+
|
|
2065
|
+
// 活跃用户数(指定时间段内有会话的用户)
|
|
2066
|
+
const activeUserStmt = db.prepare(`
|
|
2067
|
+
SELECT COUNT(DISTINCT user_id) as total
|
|
2068
|
+
FROM sessions
|
|
2069
|
+
WHERE user_id IS NOT NULL AND created_at >= ?
|
|
2070
|
+
`);
|
|
2071
|
+
const activeUsers = (activeUserStmt.get(startDate.toISOString()) as any)?.total || 0;
|
|
2072
|
+
|
|
2073
|
+
// 总会话消息数
|
|
2074
|
+
const messageStmt = db.prepare(`SELECT COALESCE(SUM(message_count), 0) as total FROM sessions ${dateClause}`);
|
|
2075
|
+
const totalMessages = (messageStmt.get() as any)?.total || 0;
|
|
2076
|
+
|
|
2077
|
+
// 技能调用总次数
|
|
2078
|
+
let skillDateClause = period !== 'all' ? `WHERE called_at >= '${startDate.toISOString()}'` : '';
|
|
2079
|
+
const skillStmt = db.prepare(`SELECT COUNT(*) as total FROM skill_usage ${skillDateClause}`);
|
|
2080
|
+
const totalSkillCalls = (skillStmt.get() as any)?.total || 0;
|
|
2081
|
+
|
|
2082
|
+
// AI生成的工单数
|
|
2083
|
+
let ticketDateClause = period !== 'all' ? `AND created_at >= '${startDate.toISOString()}'` : '';
|
|
2084
|
+
const ticketStmt = db.prepare(`SELECT COUNT(*) as total FROM tickets WHERE ai_generated = 1 ${ticketDateClause}`);
|
|
2085
|
+
const aiGeneratedTickets = (ticketStmt.get() as any)?.total || 0;
|
|
2086
|
+
|
|
2087
|
+
return {
|
|
2088
|
+
totalSessions,
|
|
2089
|
+
activeUsers,
|
|
2090
|
+
totalMessages,
|
|
2091
|
+
totalSkillCalls,
|
|
2092
|
+
aiGeneratedTickets,
|
|
2093
|
+
dateRange: {
|
|
2094
|
+
start: startDate.toISOString(),
|
|
2095
|
+
end: now.toISOString(),
|
|
2096
|
+
},
|
|
2097
|
+
};
|
|
2098
|
+
}
|
|
2099
|
+
|
|
2100
|
+
export function getSessionTrends(period: '7d' | '30d' | 'all'): TrendData[] {
|
|
2101
|
+
const db = getDb();
|
|
2102
|
+
const now = new Date();
|
|
2103
|
+
let days = period === '7d' ? 7 : period === '30d' ? 30 : 90;
|
|
2104
|
+
let startDate = new Date(now.getTime() - days * 24 * 60 * 60 * 1000);
|
|
2105
|
+
|
|
2106
|
+
const stmt = db.prepare(`
|
|
2107
|
+
SELECT
|
|
2108
|
+
DATE(created_at) as date,
|
|
2109
|
+
COUNT(*) as sessionCount,
|
|
2110
|
+
COUNT(DISTINCT user_id) as userCount
|
|
2111
|
+
FROM sessions
|
|
2112
|
+
WHERE created_at >= ?
|
|
2113
|
+
GROUP BY DATE(created_at)
|
|
2114
|
+
ORDER BY date ASC
|
|
2115
|
+
`);
|
|
2116
|
+
|
|
2117
|
+
const rows = stmt.all(startDate.toISOString()) as any[];
|
|
2118
|
+
|
|
2119
|
+
// 补齐没有数据的日期
|
|
2120
|
+
const result: TrendData[] = [];
|
|
2121
|
+
const dataMap = new Map(rows.map(r => [r.date, r]));
|
|
2122
|
+
|
|
2123
|
+
for (let i = 0; i < days; i++) {
|
|
2124
|
+
const d = new Date(startDate.getTime() + i * 24 * 60 * 60 * 1000);
|
|
2125
|
+
const dateStr = d.toISOString().split('T')[0];
|
|
2126
|
+
const existing = dataMap.get(dateStr);
|
|
2127
|
+
result.push({
|
|
2128
|
+
date: dateStr,
|
|
2129
|
+
sessionCount: existing?.sessioncount || 0,
|
|
2130
|
+
userCount: existing?.usercount || 0,
|
|
2131
|
+
});
|
|
2132
|
+
}
|
|
2133
|
+
|
|
2134
|
+
return result;
|
|
2135
|
+
}
|
|
2136
|
+
|
|
2137
|
+
export function getUserStats(period: '7d' | '30d' | 'all'): UserStats[] {
|
|
2138
|
+
const db = getDb();
|
|
2139
|
+
const now = new Date();
|
|
2140
|
+
let startDate: Date;
|
|
2141
|
+
|
|
2142
|
+
if (period === '7d') {
|
|
2143
|
+
startDate = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000);
|
|
2144
|
+
} else if (period === '30d') {
|
|
2145
|
+
startDate = new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000);
|
|
2146
|
+
} else {
|
|
2147
|
+
startDate = new Date(0);
|
|
2148
|
+
}
|
|
2149
|
+
|
|
2150
|
+
const stmt = db.prepare(`
|
|
2151
|
+
SELECT
|
|
2152
|
+
s.user_id,
|
|
2153
|
+
COUNT(s.session_id) as sessionCount,
|
|
2154
|
+
COALESCE(SUM(s.message_count), 0) as messageCount,
|
|
2155
|
+
COALESCE((SELECT COUNT(*) FROM skill_usage su WHERE su.user_id = s.user_id AND su.called_at >= ?), 0) as skillCallCount,
|
|
2156
|
+
MAX(s.last_accessed_at) as lastActiveAt
|
|
2157
|
+
FROM sessions s
|
|
2158
|
+
WHERE s.user_id IS NOT NULL AND s.created_at >= ?
|
|
2159
|
+
GROUP BY s.user_id
|
|
2160
|
+
ORDER BY sessionCount DESC
|
|
2161
|
+
LIMIT 50
|
|
2162
|
+
`);
|
|
2163
|
+
|
|
2164
|
+
const rows = stmt.all(startDate.toISOString(), startDate.toISOString()) as any[];
|
|
2165
|
+
|
|
2166
|
+
return rows.map(row => ({
|
|
2167
|
+
userId: row.user_id,
|
|
2168
|
+
sessionCount: row.sessioncount || 0,
|
|
2169
|
+
messageCount: row.messagecount || 0,
|
|
2170
|
+
skillCallCount: row.skillcallcount || 0,
|
|
2171
|
+
lastActiveAt: row.lastactiveat || '',
|
|
2172
|
+
}));
|
|
2173
|
+
}
|
|
2174
|
+
|
|
2175
|
+
export function getSkillStats(period: '7d' | '30d' | 'all'): SkillStats[] {
|
|
2176
|
+
const db = getDb();
|
|
2177
|
+
const now = new Date();
|
|
2178
|
+
let startDate: Date;
|
|
2179
|
+
|
|
2180
|
+
if (period === '7d') {
|
|
2181
|
+
startDate = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000);
|
|
2182
|
+
} else if (period === '30d') {
|
|
2183
|
+
startDate = new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000);
|
|
2184
|
+
} else {
|
|
2185
|
+
startDate = new Date(0);
|
|
2186
|
+
}
|
|
2187
|
+
|
|
2188
|
+
const stmt = db.prepare(`
|
|
2189
|
+
SELECT
|
|
2190
|
+
skill_name,
|
|
2191
|
+
COUNT(*) as callCount,
|
|
2192
|
+
SUM(CASE WHEN success = 1 THEN 1 ELSE 0 END) as successCount,
|
|
2193
|
+
SUM(CASE WHEN success = 0 THEN 1 ELSE 0 END) as failCount,
|
|
2194
|
+
MAX(called_at) as lastCalledAt
|
|
2195
|
+
FROM skill_usage
|
|
2196
|
+
WHERE called_at >= ?
|
|
2197
|
+
GROUP BY skill_name
|
|
2198
|
+
ORDER BY callCount DESC
|
|
2199
|
+
LIMIT 20
|
|
2200
|
+
`);
|
|
2201
|
+
|
|
2202
|
+
const rows = stmt.all(startDate.toISOString()) as any[];
|
|
2203
|
+
|
|
2204
|
+
return rows.map(row => ({
|
|
2205
|
+
skillName: row.skill_name,
|
|
2206
|
+
callCount: row.callcount || 0,
|
|
2207
|
+
successCount: row.successcount || 0,
|
|
2208
|
+
failCount: row.failcount || 0,
|
|
2209
|
+
successRate: row.callcount > 0 ? Math.round((row.successcount / row.callcount) * 100) : 0,
|
|
2210
|
+
lastCalledAt: row.lastcalledat || '',
|
|
2211
|
+
}));
|
|
2212
|
+
}
|
|
2213
|
+
|
|
2214
|
+
export function getKnowledgeStats(period: '7d' | '30d' | 'all'): KnowledgeStats[] {
|
|
2215
|
+
const db = getDb();
|
|
2216
|
+
const now = new Date();
|
|
2217
|
+
let startDate: Date;
|
|
2218
|
+
|
|
2219
|
+
if (period === '7d') {
|
|
2220
|
+
startDate = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000);
|
|
2221
|
+
} else if (period === '30d') {
|
|
2222
|
+
startDate = new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000);
|
|
2223
|
+
} else {
|
|
2224
|
+
startDate = new Date(0);
|
|
2225
|
+
}
|
|
2226
|
+
|
|
2227
|
+
const stmt = db.prepare(`
|
|
2228
|
+
SELECT
|
|
2229
|
+
d.id,
|
|
2230
|
+
d.title,
|
|
2231
|
+
d.category,
|
|
2232
|
+
(SELECT COUNT(*) FROM audit_logs al
|
|
2233
|
+
WHERE al.details LIKE '%' || d.id || '%'
|
|
2234
|
+
AND al.action = 'knowledge-used'
|
|
2235
|
+
AND al.timestamp >= ?) as 引用次数
|
|
2236
|
+
FROM documents d
|
|
2237
|
+
WHERE d.is_active = 1
|
|
2238
|
+
ORDER BY 引用次数 DESC
|
|
2239
|
+
LIMIT 20
|
|
2240
|
+
`);
|
|
2241
|
+
|
|
2242
|
+
const rows = stmt.all(startDate.toISOString()) as any[];
|
|
2243
|
+
|
|
2244
|
+
return rows.map(row => ({
|
|
2245
|
+
documentId: row.id,
|
|
2246
|
+
title: row.title || '未命名',
|
|
2247
|
+
category: row.category || '未分类',
|
|
2248
|
+
引用次数: row.引用次数 || 0,
|
|
2249
|
+
})).filter(k => k.引用次数 > 0);
|
|
2250
|
+
}
|