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.
Files changed (245) hide show
  1. package/README.md +234 -0
  2. package/app/(admin)/approvals/page.tsx +16 -0
  3. package/app/(admin)/audit/page.tsx +18 -0
  4. package/app/(admin)/layout.tsx +47 -0
  5. package/app/(admin)/scheduled-tasks/page.tsx +17 -0
  6. package/app/(admin)/settings/page.tsx +46 -0
  7. package/app/(admin)/skills/[name]/page.tsx +378 -0
  8. package/app/(admin)/skills/page.tsx +406 -0
  9. package/app/(admin)/statistics/page.tsx +416 -0
  10. package/app/(admin)/tickets/[id]/page.tsx +348 -0
  11. package/app/(admin)/tickets/new/page.tsx +309 -0
  12. package/app/(admin)/tickets/page.tsx +27 -0
  13. package/app/api/audit/route.ts +30 -0
  14. package/app/api/auth/feishu/callback/route.ts +72 -0
  15. package/app/api/auth/feishu/login/route.ts +17 -0
  16. package/app/api/auth/feishu/sso/route.ts +78 -0
  17. package/app/api/auth/login/route.ts +85 -0
  18. package/app/api/auth/oauth/route.ts +168 -0
  19. package/app/api/config/providers/route.ts +105 -0
  20. package/app/api/config/route.ts +115 -0
  21. package/app/api/config/status/route.ts +56 -0
  22. package/app/api/config/test/route.ts +212 -0
  23. package/app/api/documents/[id]/route.ts +88 -0
  24. package/app/api/documents/route.ts +53 -0
  25. package/app/api/health/route.ts +32 -0
  26. package/app/api/knowledge/[id]/route.ts +152 -0
  27. package/app/api/knowledge/from-session/route.ts +27 -0
  28. package/app/api/knowledge/route.ts +100 -0
  29. package/app/api/market/knowledge/[id]/route.ts +92 -0
  30. package/app/api/market/knowledge/route.ts +130 -0
  31. package/app/api/marketplace/skills/[id]/approve/route.ts +68 -0
  32. package/app/api/marketplace/skills/[id]/certify/route.ts +54 -0
  33. package/app/api/marketplace/skills/[id]/install/route.ts +180 -0
  34. package/app/api/marketplace/skills/[id]/promote-to-system/route.ts +219 -0
  35. package/app/api/marketplace/skills/[id]/rate/route.ts +90 -0
  36. package/app/api/marketplace/skills/[id]/ratings/route.ts +55 -0
  37. package/app/api/marketplace/skills/[id]/reject/route.ts +68 -0
  38. package/app/api/marketplace/skills/[id]/route.ts +177 -0
  39. package/app/api/marketplace/skills/route.ts +235 -0
  40. package/app/api/memory/route.ts +40 -0
  41. package/app/api/my/files/[id]/route.ts +52 -0
  42. package/app/api/my/files/route.ts +230 -0
  43. package/app/api/my/knowledge/route.ts +36 -0
  44. package/app/api/pi-chat/route.ts +443 -0
  45. package/app/api/recommend/route.ts +38 -0
  46. package/app/api/scheduled-tasks/[id]/execute/route.ts +132 -0
  47. package/app/api/scheduled-tasks/[id]/route.ts +165 -0
  48. package/app/api/scheduled-tasks/[id]/toggle/route.ts +53 -0
  49. package/app/api/scheduled-tasks/route.ts +101 -0
  50. package/app/api/sessions/[id]/messages/route.ts +212 -0
  51. package/app/api/sessions/route.ts +101 -0
  52. package/app/api/share/file/[id]/route.ts +37 -0
  53. package/app/api/skills/[name]/execute/route.ts +121 -0
  54. package/app/api/skills/[name]/route.ts +167 -0
  55. package/app/api/skills/create/route.ts +65 -0
  56. package/app/api/skills/generate/route.ts +405 -0
  57. package/app/api/skills/installed/route.ts +151 -0
  58. package/app/api/skills/route.ts +174 -0
  59. package/app/api/skills/translate/route.ts +40 -0
  60. package/app/api/skills/user/[name]/route.ts +159 -0
  61. package/app/api/skills/user/route.ts +90 -0
  62. package/app/api/statistics/route.ts +94 -0
  63. package/app/api/task-executions/[id]/route.ts +34 -0
  64. package/app/api/task-executions/route.ts +29 -0
  65. package/app/api/tickets/[id]/approve/route.ts +129 -0
  66. package/app/api/tickets/[id]/execute/route.ts +201 -0
  67. package/app/api/tickets/[id]/route.ts +127 -0
  68. package/app/api/tickets/route.ts +103 -0
  69. package/app/api/user/skills/route.ts +175 -0
  70. package/app/api/users/route.ts +80 -0
  71. package/app/chat/page.tsx +5 -0
  72. package/app/globals.css +84 -0
  73. package/app/h5/layout.tsx +5 -0
  74. package/app/h5/mobile-approvals-page.tsx +167 -0
  75. package/app/h5/mobile-chat-page.tsx +951 -0
  76. package/app/h5/mobile-profile-page.tsx +147 -0
  77. package/app/h5/mobile-tickets-page.tsx +121 -0
  78. package/app/h5/page.tsx +23 -0
  79. package/app/h5/ticket-action-buttons.tsx +80 -0
  80. package/app/layout.tsx +26 -0
  81. package/app/login/page.tsx +318 -0
  82. package/app/market/knowledge/[id]/page.tsx +77 -0
  83. package/app/market/knowledge/page.tsx +358 -0
  84. package/app/market/layout.tsx +29 -0
  85. package/app/market/page.tsx +18 -0
  86. package/app/market/skills/page.tsx +397 -0
  87. package/app/my/files/page.tsx +511 -0
  88. package/app/my/knowledge/[id]/page.tsx +271 -0
  89. package/app/my/knowledge/new/page.tsx +234 -0
  90. package/app/my/knowledge/page.tsx +248 -0
  91. package/app/my/layout.tsx +32 -0
  92. package/app/my/memory/page.tsx +164 -0
  93. package/app/my/page.tsx +18 -0
  94. package/app/my/scheduled-tasks/[id]/edit/page.tsx +290 -0
  95. package/app/my/scheduled-tasks/[id]/executions/page.tsx +275 -0
  96. package/app/my/scheduled-tasks/[id]/page.tsx +284 -0
  97. package/app/my/scheduled-tasks/new/page.tsx +230 -0
  98. package/app/my/scheduled-tasks/page.tsx +27 -0
  99. package/app/my/skills/[name]/page.tsx +320 -0
  100. package/app/my/skills/new/page.tsx +394 -0
  101. package/app/my/skills/page.tsx +303 -0
  102. package/app/page.tsx +2288 -0
  103. package/app/share/[sessionId]/page.tsx +226 -0
  104. package/app/share/file/[id]/page.tsx +140 -0
  105. package/bin/README.md +63 -0
  106. package/bin/generate-api-system +300 -0
  107. package/bin/postinstall.js +95 -0
  108. package/bin/work-agent.js +173 -0
  109. package/components/ai-elements/agent.tsx +142 -0
  110. package/components/ai-elements/artifact.tsx +149 -0
  111. package/components/ai-elements/attachments.tsx +427 -0
  112. package/components/ai-elements/audio-player.tsx +232 -0
  113. package/components/ai-elements/canvas.tsx +26 -0
  114. package/components/ai-elements/chain-of-thought.tsx +223 -0
  115. package/components/ai-elements/checkpoint.tsx +72 -0
  116. package/components/ai-elements/code-block.tsx +555 -0
  117. package/components/ai-elements/commit.tsx +449 -0
  118. package/components/ai-elements/confirmation.tsx +173 -0
  119. package/components/ai-elements/connection.tsx +28 -0
  120. package/components/ai-elements/context.tsx +410 -0
  121. package/components/ai-elements/controls.tsx +19 -0
  122. package/components/ai-elements/conversation.tsx +167 -0
  123. package/components/ai-elements/edge.tsx +144 -0
  124. package/components/ai-elements/environment-variables.tsx +325 -0
  125. package/components/ai-elements/file-tree.tsx +298 -0
  126. package/components/ai-elements/image.tsx +25 -0
  127. package/components/ai-elements/inline-citation.tsx +294 -0
  128. package/components/ai-elements/jsx-preview.tsx +250 -0
  129. package/components/ai-elements/message.tsx +367 -0
  130. package/components/ai-elements/mic-selector.tsx +372 -0
  131. package/components/ai-elements/model-selector.tsx +214 -0
  132. package/components/ai-elements/node.tsx +72 -0
  133. package/components/ai-elements/open-in-chat.tsx +367 -0
  134. package/components/ai-elements/package-info.tsx +235 -0
  135. package/components/ai-elements/panel.tsx +16 -0
  136. package/components/ai-elements/persona.tsx +280 -0
  137. package/components/ai-elements/plan.tsx +144 -0
  138. package/components/ai-elements/prompt-input.tsx +1341 -0
  139. package/components/ai-elements/queue.tsx +275 -0
  140. package/components/ai-elements/reasoning.tsx +355 -0
  141. package/components/ai-elements/sandbox.tsx +133 -0
  142. package/components/ai-elements/schema-display.tsx +473 -0
  143. package/components/ai-elements/shimmer.tsx +78 -0
  144. package/components/ai-elements/snippet.tsx +141 -0
  145. package/components/ai-elements/sources.tsx +78 -0
  146. package/components/ai-elements/speech-input.tsx +324 -0
  147. package/components/ai-elements/stack-trace.tsx +531 -0
  148. package/components/ai-elements/suggestion.tsx +58 -0
  149. package/components/ai-elements/task.tsx +88 -0
  150. package/components/ai-elements/terminal.tsx +277 -0
  151. package/components/ai-elements/test-results.tsx +497 -0
  152. package/components/ai-elements/tool.tsx +174 -0
  153. package/components/ai-elements/toolbar.tsx +17 -0
  154. package/components/ai-elements/transcription.tsx +126 -0
  155. package/components/ai-elements/voice-selector.tsx +525 -0
  156. package/components/ai-elements/web-preview.tsx +282 -0
  157. package/components/audit-log-list.tsx +114 -0
  158. package/components/chat/EmptyPreviewState.tsx +12 -0
  159. package/components/chat/KnowledgePickerDialog.tsx +464 -0
  160. package/components/chat/KnowledgePreview.tsx +70 -0
  161. package/components/chat/KnowledgePreviewPanel.tsx +86 -0
  162. package/components/chat/MentionInput.tsx +309 -0
  163. package/components/chat/OrganizeDialog.tsx +258 -0
  164. package/components/chat/RecommendationBanner.tsx +94 -0
  165. package/components/chat/SaveToKnowledgeDialog.tsx +193 -0
  166. package/components/chat/SkillSelector.tsx +305 -0
  167. package/components/chat/SkillSwitcher.tsx +163 -0
  168. package/components/client-layout.tsx +15 -0
  169. package/components/knowledge/KnowledgeMetadataPanel.tsx +293 -0
  170. package/components/layout-wrapper.tsx +18 -0
  171. package/components/mobile-layout.tsx +62 -0
  172. package/components/scheduled-task-list.tsx +356 -0
  173. package/components/setup-guide.tsx +484 -0
  174. package/components/sub-nav.tsx +54 -0
  175. package/components/ticket-detail-content.tsx +383 -0
  176. package/components/ticket-list.tsx +366 -0
  177. package/components/top-nav.tsx +132 -0
  178. package/components/ui/accordion.tsx +58 -0
  179. package/components/ui/alert.tsx +59 -0
  180. package/components/ui/avatar.tsx +50 -0
  181. package/components/ui/badge.tsx +36 -0
  182. package/components/ui/button-group.tsx +83 -0
  183. package/components/ui/button.tsx +57 -0
  184. package/components/ui/card.tsx +91 -0
  185. package/components/ui/carousel.tsx +262 -0
  186. package/components/ui/collapsible.tsx +11 -0
  187. package/components/ui/command.tsx +153 -0
  188. package/components/ui/dialog.tsx +122 -0
  189. package/components/ui/dropdown-menu.tsx +200 -0
  190. package/components/ui/hover-card.tsx +29 -0
  191. package/components/ui/input-group.tsx +170 -0
  192. package/components/ui/input.tsx +22 -0
  193. package/components/ui/label.tsx +26 -0
  194. package/components/ui/popover.tsx +31 -0
  195. package/components/ui/progress.tsx +28 -0
  196. package/components/ui/scroll-area.tsx +48 -0
  197. package/components/ui/select.tsx +174 -0
  198. package/components/ui/separator.tsx +31 -0
  199. package/components/ui/spinner.tsx +16 -0
  200. package/components/ui/switch.tsx +29 -0
  201. package/components/ui/table.tsx +120 -0
  202. package/components/ui/tabs.tsx +55 -0
  203. package/components/ui/textarea.tsx +22 -0
  204. package/components/ui/tooltip.tsx +30 -0
  205. package/components/welcome-guide.tsx +182 -0
  206. package/components.json +24 -0
  207. package/lib/command-parser.ts +331 -0
  208. package/lib/dangerous-commands.ts +672 -0
  209. package/lib/db.ts +2250 -0
  210. package/lib/feishu-auth.ts +135 -0
  211. package/lib/file-storage.ts +306 -0
  212. package/lib/file-tool.ts +583 -0
  213. package/lib/knowledge-tool.ts +152 -0
  214. package/lib/knowledge-types.ts +66 -0
  215. package/lib/market-client.ts +313 -0
  216. package/lib/market-db.ts +736 -0
  217. package/lib/market-types.ts +51 -0
  218. package/lib/memory-tool.ts +211 -0
  219. package/lib/memory.ts +197 -0
  220. package/lib/pi-config.ts +436 -0
  221. package/lib/pi-session.ts +799 -0
  222. package/lib/pinyin.ts +13 -0
  223. package/lib/recommendation.ts +227 -0
  224. package/lib/risk-estimator.ts +350 -0
  225. package/lib/scheduled-task-tool.ts +184 -0
  226. package/lib/scheduler-init.ts +43 -0
  227. package/lib/scheduler.ts +416 -0
  228. package/lib/secure-bash-tool.ts +413 -0
  229. package/lib/skill-engine.ts +396 -0
  230. package/lib/skill-generator.ts +269 -0
  231. package/lib/skill-loader.ts +234 -0
  232. package/lib/skill-tool.ts +188 -0
  233. package/lib/skill-types.ts +82 -0
  234. package/lib/skills-init.ts +58 -0
  235. package/lib/ticket-tool.ts +246 -0
  236. package/lib/user-skill-types.ts +30 -0
  237. package/lib/user-skills.ts +362 -0
  238. package/lib/utils.ts +6 -0
  239. package/lib/workflow.ts +154 -0
  240. package/lib/zip-tool.ts +191 -0
  241. package/next.config.js +8 -0
  242. package/package.json +106 -0
  243. package/public/.gitkeep +1 -0
  244. package/public/icon.svg +1 -0
  245. 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
+ }