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
@@ -0,0 +1,175 @@
1
+ import { NextRequest, NextResponse } from 'next/server';
2
+ import {
3
+ getUserSkills,
4
+ installUserSkill,
5
+ uninstallUserSkill,
6
+ getBuiltinSkills,
7
+ getUserByUsername,
8
+ } from '@/lib/db';
9
+ import {
10
+ getMarketSkillByName,
11
+ incrementDownloadCount,
12
+ } from '@/lib/market-db';
13
+ import { getAvailableSkills } from '@/lib/skill-engine';
14
+ import { listUserSkills, syncUserSkills } from '@/lib/user-skills';
15
+
16
+ // 获取用户已安装的Skill列表
17
+ export async function GET(request: NextRequest) {
18
+ try {
19
+ const userId = request.nextUrl.searchParams.get('userId');
20
+ const username = request.nextUrl.searchParams.get('username');
21
+
22
+ if (!userId && !username) {
23
+ return NextResponse.json(
24
+ { error: '缺少userId或username参数' },
25
+ { status: 400 }
26
+ );
27
+ }
28
+
29
+ let targetUserId = userId;
30
+ if (!targetUserId && username) {
31
+ const user = getUserByUsername(username);
32
+ if (!user) {
33
+ return NextResponse.json({ skills: [] });
34
+ }
35
+ targetUserId = user.id;
36
+ }
37
+
38
+ // 同步用户技能(文件系统 <-> 数据库)
39
+ syncUserSkills(targetUserId!);
40
+
41
+ // 获取内置技能配置
42
+ const builtinSkills = getBuiltinSkills();
43
+ const enabledBuiltin = builtinSkills.filter(s => s.enabled);
44
+ const builtinEnabledSet = new Set(enabledBuiltin.map(s => s.skillName));
45
+
46
+ // 获取系统技能信息
47
+ const systemSkills = getAvailableSkills();
48
+ const systemSkillMap = new Map(systemSkills.map(s => [s.name, s]));
49
+
50
+ // 从数据库获取已安装技能(用于统计信息)
51
+ const dbSkills = getUserSkills(targetUserId!);
52
+ const dbSkillMap = new Map(dbSkills.map(s => [s.skillName, s]));
53
+
54
+ // 构建返回列表
55
+ const result: any[] = [];
56
+ const addedSkills = new Set<string>();
57
+
58
+ // 1. 从用户私有目录读取技能(通过 create_skill 工具创建的)
59
+ const userPrivateSkills = listUserSkills(targetUserId!);
60
+ for (const skill of userPrivateSkills) {
61
+ if (!addedSkills.has(skill.name)) {
62
+ const dbSkill = dbSkillMap.get(skill.name);
63
+ result.push({
64
+ skillName: skill.name,
65
+ displayName: skill.displayName || skill.name,
66
+ description: skill.description,
67
+ version: skill.version,
68
+ author: skill.author,
69
+ source: 'personal',
70
+ isBuiltin: builtinEnabledSet.has(skill.name),
71
+ installedAt: skill.createdAt || dbSkill?.installedAt || new Date().toISOString(),
72
+ useCount: dbSkill?.useCount ?? 1,
73
+ });
74
+ addedSkills.add(skill.name);
75
+ }
76
+ }
77
+
78
+ // 2. 从数据库读取用户已安装的技能(从市场安装的,但目录中不存在)
79
+ for (const skill of dbSkills) {
80
+ if (!addedSkills.has(skill.skillName)) {
81
+ const isBuiltin = builtinEnabledSet.has(skill.skillName);
82
+ result.push({
83
+ ...skill,
84
+ isBuiltin,
85
+ });
86
+ addedSkills.add(skill.skillName);
87
+ }
88
+ }
89
+
90
+ // 3. 添加已启用的内置技能
91
+ for (const builtin of enabledBuiltin) {
92
+ if (!addedSkills.has(builtin.skillName)) {
93
+ const sysSkill = systemSkillMap.get(builtin.skillName);
94
+ if (sysSkill) {
95
+ result.push({
96
+ skillName: sysSkill.name,
97
+ displayName: sysSkill.displayName || sysSkill.name,
98
+ description: sysSkill.description,
99
+ version: sysSkill.version,
100
+ source: 'builtin',
101
+ isBuiltin: true,
102
+ isInstalled: false,
103
+ installedAt: new Date().toISOString(),
104
+ useCount: 0,
105
+ });
106
+ addedSkills.add(builtin.skillName);
107
+ }
108
+ }
109
+ }
110
+
111
+ return NextResponse.json({ skills: result });
112
+ } catch (error) {
113
+ console.error('Get user skills failed:', error);
114
+ return NextResponse.json(
115
+ { error: '获取Skill列表失败' },
116
+ { status: 500 }
117
+ );
118
+ }
119
+ }
120
+
121
+ // 安装/卸载Skill
122
+ export async function POST(request: NextRequest) {
123
+ try {
124
+ const body = await request.json();
125
+ const { userId, action, skillName, source, skillData } = body;
126
+
127
+ if (!userId || !action) {
128
+ return NextResponse.json(
129
+ { error: '缺少必要参数' },
130
+ { status: 400 }
131
+ );
132
+ }
133
+
134
+ if (action === 'install') {
135
+ if (!skillName) {
136
+ return NextResponse.json(
137
+ { error: '缺少skillName参数' },
138
+ { status: 400 }
139
+ );
140
+ }
141
+
142
+ // 如果是从市场安装,增加下载计数
143
+ if (source === 'market') {
144
+ const marketSkill = getMarketSkillByName(skillName);
145
+ if (marketSkill) {
146
+ incrementDownloadCount(marketSkill.id);
147
+ }
148
+ }
149
+
150
+ const userSkill = installUserSkill(userId, skillName, source || 'market', skillData);
151
+ return NextResponse.json({ skill: userSkill });
152
+ } else if (action === 'uninstall') {
153
+ if (!skillName) {
154
+ return NextResponse.json(
155
+ { error: '缺少skillName参数' },
156
+ { status: 400 }
157
+ );
158
+ }
159
+
160
+ uninstallUserSkill(userId, skillName);
161
+ return NextResponse.json({ success: true });
162
+ } else {
163
+ return NextResponse.json(
164
+ { error: '无效的action' },
165
+ { status: 400 }
166
+ );
167
+ }
168
+ } catch (error) {
169
+ console.error('Skill operation failed:', error);
170
+ return NextResponse.json(
171
+ { error: '操作失败' },
172
+ { status: 500 }
173
+ );
174
+ }
175
+ }
@@ -0,0 +1,80 @@
1
+ import { NextRequest, NextResponse } from 'next/server';
2
+ import { createUser, getUserByUsername, updateUserLogin } from '@/lib/db';
3
+
4
+ export async function POST(request: NextRequest) {
5
+ try {
6
+ const body = await request.json();
7
+ const { username } = body;
8
+
9
+ if (!username || typeof username !== 'string' || username.trim().length === 0) {
10
+ return NextResponse.json(
11
+ { error: '用户名不能为空' },
12
+ { status: 400 }
13
+ );
14
+ }
15
+
16
+ const trimmedUsername = username.trim().toLowerCase();
17
+
18
+ // 查找用户,不存在则创建
19
+ let user = getUserByUsername(trimmedUsername);
20
+ if (!user) {
21
+ user = createUser(trimmedUsername);
22
+ } else {
23
+ // 更新最后登录时间
24
+ updateUserLogin(user.id);
25
+ }
26
+
27
+ return NextResponse.json({
28
+ user: {
29
+ id: user.id,
30
+ username: user.username,
31
+ role: user.role,
32
+ createdAt: user.createdAt.toISOString(),
33
+ }
34
+ });
35
+ } catch (error) {
36
+ console.error('Login failed:', error);
37
+ return NextResponse.json(
38
+ { error: '登录失败' },
39
+ { status: 500 }
40
+ );
41
+ }
42
+ }
43
+
44
+ export async function GET(request: NextRequest) {
45
+ try {
46
+ const userId = request.nextUrl.searchParams.get('userId');
47
+ if (!userId) {
48
+ return NextResponse.json(
49
+ { error: '缺少userId参数' },
50
+ { status: 400 }
51
+ );
52
+ }
53
+
54
+ const { getUserById } = await import('@/lib/db');
55
+ const user = getUserById(userId);
56
+
57
+ if (!user) {
58
+ return NextResponse.json(
59
+ { error: '用户不存在' },
60
+ { status: 404 }
61
+ );
62
+ }
63
+
64
+ return NextResponse.json({
65
+ user: {
66
+ id: user.id,
67
+ username: user.username,
68
+ role: user.role,
69
+ createdAt: user.createdAt.toISOString(),
70
+ lastLoginAt: user.lastLoginAt?.toISOString(),
71
+ }
72
+ });
73
+ } catch (error) {
74
+ console.error('Get user failed:', error);
75
+ return NextResponse.json(
76
+ { error: '获取用户信息失败' },
77
+ { status: 500 }
78
+ );
79
+ }
80
+ }
@@ -0,0 +1,5 @@
1
+ import { redirect } from 'next/navigation';
2
+
3
+ export default function ChatPage() {
4
+ redirect('/');
5
+ }
@@ -0,0 +1,84 @@
1
+ @tailwind base;
2
+ @tailwind components;
3
+ @tailwind utilities;
4
+
5
+ @source "highlight.js/styles/github-dark.css";
6
+
7
+ /* Fix streamdown code block button issues */
8
+ @layer components {
9
+ /* Remove sticky positioning from code block button container */
10
+ [data-streamdown="code-block"] > [data-streamdown="code-block-header"] + div {
11
+ position: static !important;
12
+ top: auto !important;
13
+ margin-top: 0 !important;
14
+ pointer-events: auto !important;
15
+ }
16
+
17
+ /* Reverse button order to put Copy first, Download second */
18
+ [data-streamdown="code-block-actions"] {
19
+ flex-direction: row-reverse !important;
20
+ }
21
+ }
22
+
23
+ @layer base {
24
+ :root {
25
+ --background: 0 0% 100%;
26
+ --foreground: 222.2 84% 4.9%;
27
+ --card: 0 0% 100%;
28
+ --card-foreground: 222.2 84% 4.9%;
29
+ --popover: 0 0% 100%;
30
+ --popover-foreground: 222.2 84% 4.9%;
31
+ --primary: 221.2 83.2% 53.3%;
32
+ --primary-foreground: 210 40% 98%;
33
+ --secondary: 210 40% 96.1%;
34
+ --secondary-foreground: 222.2 47.4% 11.2%;
35
+ --muted: 210 40% 96.1%;
36
+ --muted-foreground: 215.4 16.3% 46.9%;
37
+ --accent: 210 40% 96.1%;
38
+ --accent-foreground: 222.2 47.4% 11.2%;
39
+ --destructive: 0 84.2% 60.2%;
40
+ --destructive-foreground: 210 40% 98%;
41
+ --border: 214.3 31.8% 91.4%;
42
+ --input: 214.3 31.8% 91.4%;
43
+ --ring: 221.2 83.2% 53.3%;
44
+ --radius: 0.5rem;
45
+ }
46
+
47
+ /* 移动端安全区域 */
48
+ .pb-safe {
49
+ padding-bottom: env(safe-area-inset-bottom, 0);
50
+ }
51
+
52
+ .pt-safe {
53
+ padding-top: env(safe-area-inset-top, 0);
54
+ }
55
+
56
+ .dark {
57
+ --background: 222.2 84% 4.9%;
58
+ --foreground: 210 40% 98%;
59
+ --card: 222.2 84% 4.9%;
60
+ --card-foreground: 210 40% 98%;
61
+ --popover: 222.2 84% 4.9%;
62
+ --popover-foreground: 210 40% 98%;
63
+ --primary: 217.2 91.2% 59.8%;
64
+ --primary-foreground: 222.2 47.4% 11.2%;
65
+ --secondary: 217.2 32.6% 17.5%;
66
+ --secondary-foreground: 210 40% 98%;
67
+ --muted: 217.2 32.6% 17.5%;
68
+ --muted-foreground: 215 20.2% 65.1%;
69
+ --accent: 217.2 32.6% 17.5%;
70
+ --accent-foreground: 210 40% 98%;
71
+ --destructive: 0 62.8% 30.6%;
72
+ --destructive-foreground: 210 40% 98%;
73
+ --border: 217.2 32.6% 17.5%;
74
+ --input: 217.2 32.6% 17.5%;
75
+ --ring: 224.3 76.3% 48%;
76
+ }
77
+
78
+ * {
79
+ @apply border-border;
80
+ }
81
+ body {
82
+ @apply bg-background text-foreground;
83
+ }
84
+ }
@@ -0,0 +1,5 @@
1
+ import MobilePage from './page';
2
+
3
+ export default function MobileLayout() {
4
+ return <MobilePage />;
5
+ }
@@ -0,0 +1,167 @@
1
+ 'use client';
2
+
3
+ import { useState, useEffect } from 'react';
4
+ import { CheckCircle } from 'lucide-react';
5
+ import { Button } from '@/components/ui/button';
6
+
7
+ interface Ticket {
8
+ id: string;
9
+ title: string;
10
+ type: string;
11
+ status: string;
12
+ riskLevel: string;
13
+ description?: string;
14
+ command?: string;
15
+ createdAt: string;
16
+ }
17
+
18
+ export default function MobileApprovalsPage() {
19
+ const [tickets, setTickets] = useState<Ticket[]>([]);
20
+ const [loading, setLoading] = useState(true);
21
+ const [selectedTicket, setSelectedTicket] = useState<Ticket | null>(null);
22
+ const [actionLoading, setActionLoading] = useState(false);
23
+
24
+ useEffect(() => {
25
+ async function load() {
26
+ try {
27
+ const response = await fetch('/api/tickets?status=pending');
28
+ const data = await response.json();
29
+ setTickets(data.tickets || []);
30
+ } catch (error) {
31
+ console.error('Failed to load approvals:', error);
32
+ } finally {
33
+ setLoading(false);
34
+ }
35
+ }
36
+ load();
37
+ }, []);
38
+
39
+ const handleApprove = async (ticketId: string) => {
40
+ setActionLoading(true);
41
+ try {
42
+ await fetch(`/api/tickets/${ticketId}/approve`, {
43
+ method: 'POST',
44
+ headers: { 'Content-Type': 'application/json' },
45
+ body: JSON.stringify({ action: 'approve', approver: 'user', comment: '' }),
46
+ });
47
+ setTickets(prev => prev.filter(t => t.id !== ticketId));
48
+ setSelectedTicket(null);
49
+ } catch (error) {
50
+ console.error('Failed to approve:', error);
51
+ } finally {
52
+ setActionLoading(false);
53
+ }
54
+ };
55
+
56
+ const handleReject = async (ticketId: string) => {
57
+ setActionLoading(true);
58
+ try {
59
+ await fetch(`/api/tickets/${ticketId}/approve`, {
60
+ method: 'POST',
61
+ headers: { 'Content-Type': 'application/json' },
62
+ body: JSON.stringify({ action: 'reject', approver: 'user', comment: '' }),
63
+ });
64
+ setTickets(prev => prev.filter(t => t.id !== ticketId));
65
+ setSelectedTicket(null);
66
+ } catch (error) {
67
+ console.error('Failed to reject:', error);
68
+ } finally {
69
+ setActionLoading(false);
70
+ }
71
+ };
72
+
73
+ const getRiskBadge = (risk: string) => {
74
+ const config: Record<string, { label: string; className: string }> = {
75
+ low: { label: '低', className: 'bg-green-100 text-green-800' },
76
+ medium: { label: '中', className: 'bg-yellow-100 text-yellow-800' },
77
+ high: { label: '高', className: 'bg-orange-100 text-orange-800' },
78
+ critical: { label: '极高', className: 'bg-red-100 text-red-800' },
79
+ };
80
+ const { label, className } = config[risk] || config.medium;
81
+ return <span className={`px-1.5 py-0.5 rounded text-xs ${className}`}>{label}</span>;
82
+ };
83
+
84
+ if (loading) {
85
+ return <div className="flex-1 flex items-center justify-center">加载中...</div>;
86
+ }
87
+
88
+ if (selectedTicket) {
89
+ return (
90
+ <div className="flex-1 overflow-y-auto p-4">
91
+ <div className="flex items-center gap-2 mb-4">
92
+ <Button variant="ghost" size="sm" onClick={() => setSelectedTicket(null)} className="-ml-2">
93
+ ← 返回
94
+ </Button>
95
+ </div>
96
+
97
+ <h3 className="font-semibold text-lg mb-3">{selectedTicket.title}</h3>
98
+
99
+ <div className="space-y-3 mb-4">
100
+ <div className="flex items-center gap-2">
101
+ {getRiskBadge(selectedTicket.riskLevel)}
102
+ <span className="text-sm text-muted-foreground">
103
+ {selectedTicket.type === 'bash-execute' ? '命令执行' : 'Skill 脚本'}
104
+ </span>
105
+ </div>
106
+
107
+ {selectedTicket.description && (
108
+ <p className="text-sm text-muted-foreground bg-muted/30 p-2 rounded">
109
+ {selectedTicket.description}
110
+ </p>
111
+ )}
112
+
113
+ {selectedTicket.command && (
114
+ <div>
115
+ <p className="text-xs text-muted-foreground mb-1">执行命令:</p>
116
+ <pre className="text-xs bg-muted p-2 rounded overflow-x-auto">
117
+ {selectedTicket.command}
118
+ </pre>
119
+ </div>
120
+ )}
121
+
122
+ <div className="text-xs text-muted-foreground">
123
+ 创建时间: {new Date(selectedTicket.createdAt).toLocaleString('zh-CN')}
124
+ </div>
125
+ </div>
126
+
127
+ <div className="flex gap-2">
128
+ <Button onClick={() => handleApprove(selectedTicket.id)} disabled={actionLoading} className="flex-1 bg-green-600 hover:bg-green-700">
129
+ ✓ 批准
130
+ </Button>
131
+ <Button variant="destructive" onClick={() => handleReject(selectedTicket.id)} disabled={actionLoading} className="flex-1">
132
+ ✗ 拒绝
133
+ </Button>
134
+ </div>
135
+ </div>
136
+ );
137
+ }
138
+
139
+ return (
140
+ <div className="flex-1 overflow-y-auto p-3">
141
+ {tickets.length === 0 ? (
142
+ <div className="text-center text-muted-foreground py-8">
143
+ <CheckCircle className="h-12 w-12 mx-auto mb-2 opacity-50" />
144
+ <p>暂无待审核工单</p>
145
+ </div>
146
+ ) : (
147
+ <div className="space-y-2">
148
+ {tickets.map((ticket) => (
149
+ <div
150
+ key={ticket.id}
151
+ onClick={() => setSelectedTicket(ticket)}
152
+ className="p-3 bg-muted/30 hover:bg-muted rounded-lg cursor-pointer"
153
+ >
154
+ <div className="flex items-start justify-between mb-1">
155
+ <span className="font-medium text-sm flex-1">{ticket.title}</span>
156
+ {getRiskBadge(ticket.riskLevel)}
157
+ </div>
158
+ <div className="text-xs text-muted-foreground">
159
+ {ticket.type === 'bash-execute' ? '命令执行' : 'Skill 脚本'} · {new Date(ticket.createdAt).toLocaleDateString('zh-CN')}
160
+ </div>
161
+ </div>
162
+ ))}
163
+ </div>
164
+ )}
165
+ </div>
166
+ );
167
+ }