@jetcrab/pi-desk 1.1.8 → 1.1.10-dev

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 (100) hide show
  1. package/docs/plugins.md +15 -1
  2. package/package.json +7 -7
  3. package/src/client/l2_biz/auth/l2-login-biz.ts +4 -1
  4. package/src/client/l2_biz/settings/hooks/l2-use-auth-settings.ts +18 -1
  5. package/src/client/l2_biz/settings/l2-auth-settings-biz.ts +18 -1
  6. package/src/client/l2_biz/settings/l2-auth-settings.tsx +19 -5
  7. package/src/client/l2_biz/workbench/file-workspace/l2-file-workspace-runtime.ts +144 -23
  8. package/src/client/l2_biz/workbench/file-workspace/l2-file-workspace-view.tsx +9 -5
  9. package/src/client/l2_biz/workbench/file-workspace/l2-project-file-browser.tsx +94 -5
  10. package/src/client/l2_biz/workbench/file-workspace/l2-project-preview-biz.ts +48 -0
  11. package/src/client/l2_biz/workbench/git-workspace/l2-git-workspace-runtime.ts +26 -8
  12. package/src/client/l2_biz/workbench/hooks/l2-workbench-chat-viewport.ts +70 -47
  13. package/src/client/l2_biz/workbench/l2-work-session-columns.tsx +1 -0
  14. package/src/client/l2_biz/workbench/l2-workbench-chat-view.tsx +18 -14
  15. package/src/client/l2_biz/workbench/l2-workbench.tsx +37 -6
  16. package/src/client/l3_modules/conversation/l3-conversation-messages.tsx +28 -9
  17. package/src/client/l3_modules/project-files/l3-project-file-browser.tsx +27 -15
  18. package/src/client/l3_modules/project-files/l3-project-file-runtime.ts +7 -0
  19. package/src/client/l4_foundation/ui/code/l4-code-preferences.ts +1 -1
  20. package/src/common/l2_biz/auth/l2-auth-contract.ts +14 -1
  21. package/src/common/l2_biz/auth/l2-auth-messages.ts +27 -10
  22. package/src/common/l3_modules/app-runtime/l3-app-runtime-contract.ts +4 -1
  23. package/src/common/l3_modules/conversation/l3-conversation-locale-messages.ts +4 -0
  24. package/src/common/l3_modules/project-files/l3-project-files-locale-messages.ts +8 -0
  25. package/src/common/l4_foundation/file/l4-project-preview-contract.ts +25 -0
  26. package/src/server/l1_entry/api/l1-auth-login-route.ts +20 -6
  27. package/src/server/l1_entry/api/l1-auth-settings-routes.ts +1 -1
  28. package/src/server/l1_entry/api/l1-project-route.ts +14 -5
  29. package/src/server/l1_entry/node/l1-server.ts +4 -7
  30. package/src/server/l2_biz/app-runtime/l2-app-runtime.ts +6 -1
  31. package/src/server/l2_biz/auth/l2-auth.ts +52 -2
  32. package/src/server/l2_biz/work-session/l2-work-session-chat-runtime.ts +28 -0
  33. package/src/server/l2_biz/work-session/l2-work-session-manage.ts +8 -0
  34. package/src/server/l3_modules/app-runtime/l3-app-notification-runtime.ts +48 -14
  35. package/src/server/l4_foundation/auth/l4-auth-config.ts +19 -3
  36. package/src/server/l4_foundation/pi/l4-pi-chat-worker.ts +2 -1
  37. package/src/server/l4_foundation/pi/l4-pi-global-runtime.js +3 -13
  38. package/src/server/l4_foundation/pi/l4-pi-session-plugin-runtime.ts +60 -0
  39. package/temp/build/pi-desk/release/.next/BUILD_ID +1 -1
  40. package/temp/build/pi-desk/release/.next/build-manifest.json +3 -3
  41. package/temp/build/pi-desk/release/.next/prerender-manifest.json +3 -3
  42. package/temp/build/pi-desk/release/.next/react-loadable-manifest.json +1 -1
  43. package/temp/build/pi-desk/release/.next/required-server-files.js +3 -3
  44. package/temp/build/pi-desk/release/.next/required-server-files.json +3 -3
  45. package/temp/build/pi-desk/release/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
  46. package/temp/build/pi-desk/release/.next/server/app/_global-error.html +1 -1
  47. package/temp/build/pi-desk/release/.next/server/app/_global-error.rsc +1 -1
  48. package/temp/build/pi-desk/release/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  49. package/temp/build/pi-desk/release/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  50. package/temp/build/pi-desk/release/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  51. package/temp/build/pi-desk/release/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  52. package/temp/build/pi-desk/release/.next/server/app/api/auth/login/route.js +2 -4
  53. package/temp/build/pi-desk/release/.next/server/app/api/auth/logout/route.js +1 -1
  54. package/temp/build/pi-desk/release/.next/server/app/api/auth-settings/get/route.js +1 -1
  55. package/temp/build/pi-desk/release/.next/server/app/api/auth-settings/replace/route.js +1 -1
  56. package/temp/build/pi-desk/release/.next/server/app/api/health/route.js +1 -1
  57. package/temp/build/pi-desk/release/.next/server/app/api/project-files/reveal/route.js +1 -1
  58. package/temp/build/pi-desk/release/.next/server/app/login/page.js +1 -1
  59. package/temp/build/pi-desk/release/.next/server/app/login/page_client-reference-manifest.js +1 -1
  60. package/temp/build/pi-desk/release/.next/server/app/page.js +64 -64
  61. package/temp/build/pi-desk/release/.next/server/app/page_client-reference-manifest.js +1 -1
  62. package/temp/build/pi-desk/release/.next/server/chunks/23054.js +2 -2
  63. package/temp/build/pi-desk/release/.next/server/chunks/25406.js +7 -7
  64. package/temp/build/pi-desk/release/.next/server/chunks/35985.js +3 -3
  65. package/temp/build/pi-desk/release/.next/server/chunks/49900.js +4 -0
  66. package/temp/build/pi-desk/release/.next/server/chunks/5034.js +1 -0
  67. package/temp/build/pi-desk/release/.next/server/chunks/50520.js +1 -1
  68. package/temp/build/pi-desk/release/.next/server/chunks/61211.js +2 -0
  69. package/temp/build/pi-desk/release/.next/server/chunks/6917.js +1 -1
  70. package/temp/build/pi-desk/release/.next/server/chunks/77953.js +1 -0
  71. package/temp/build/pi-desk/release/.next/server/chunks/8584.js +1 -1
  72. package/temp/build/pi-desk/release/.next/server/chunks/92386.js +4 -4
  73. package/temp/build/pi-desk/release/.next/server/chunks/93049.js +1 -0
  74. package/temp/build/pi-desk/release/.next/server/chunks/94296.js +1 -1
  75. package/temp/build/pi-desk/release/.next/server/chunks/98227.js +3 -0
  76. package/temp/build/pi-desk/release/.next/server/instrumentation.js +1 -1
  77. package/temp/build/pi-desk/release/.next/server/middleware-build-manifest.js +1 -1
  78. package/temp/build/pi-desk/release/.next/server/middleware-react-loadable-manifest.js +1 -1
  79. package/temp/build/pi-desk/release/.next/server/pages/500.html +1 -1
  80. package/temp/build/pi-desk/release/.next/server/server-reference-manifest.json +1 -1
  81. package/temp/build/pi-desk/release/.next/static/chunks/45992-304a49aaaebd853e.js +13 -0
  82. package/temp/build/pi-desk/release/.next/static/chunks/{34903-e4205c0becabea76.js → 47340-c1c09c77c1a88514.js} +1 -1
  83. package/temp/build/pi-desk/release/.next/static/chunks/70669.07f3670bfb7d6dd2.js +1 -0
  84. package/temp/build/pi-desk/release/.next/static/chunks/app/{layout-586f6f88483051a7.js → layout-839162b6cfae8347.js} +1 -1
  85. package/temp/build/pi-desk/release/.next/static/chunks/app/login/page-4532a3bbee48e219.js +1 -0
  86. package/temp/build/pi-desk/release/.next/static/chunks/app/page-ca3020d12ae98f64.js +158 -0
  87. package/temp/build/pi-desk/release/.next/static/chunks/{webpack-26261285e181235a.js → webpack-735430d61da52e9d.js} +1 -1
  88. package/temp/build/pi-desk/release/.next/static/css/8a41765636e09e52.css +1 -0
  89. package/temp/build/pi-desk/release/.next/trace-build +1 -1
  90. package/temp/build/pi-desk/release/.next/server/chunks/31442.js +0 -3
  91. package/temp/build/pi-desk/release/.next/server/chunks/37681.js +0 -1
  92. package/temp/build/pi-desk/release/.next/server/chunks/65589.js +0 -4
  93. package/temp/build/pi-desk/release/.next/server/chunks/99672.js +0 -1
  94. package/temp/build/pi-desk/release/.next/static/chunks/45992-a39a014497ec4dd1.js +0 -13
  95. package/temp/build/pi-desk/release/.next/static/chunks/70669.8b4231a8675499e0.js +0 -1
  96. package/temp/build/pi-desk/release/.next/static/chunks/app/login/page-bf893718bb151f3c.js +0 -1
  97. package/temp/build/pi-desk/release/.next/static/chunks/app/page-c92ecc3b7351294d.js +0 -158
  98. package/temp/build/pi-desk/release/.next/static/css/3213ee28336bddc2.css +0 -1
  99. /package/temp/build/pi-desk/release/.next/static/{ZYYO9lnf3gpocPCjcjYmq → d7n5VhA6J-kI3A_-XJ4TQ}/_buildManifest.js +0 -0
  100. /package/temp/build/pi-desk/release/.next/static/{ZYYO9lnf3gpocPCjcjYmq → d7n5VhA6J-kI3A_-XJ4TQ}/_ssgManifest.js +0 -0
@@ -23,8 +23,21 @@ export const L2AuthSettingsGetResponseSchema = z
23
23
  })
24
24
  .strict()
25
25
 
26
+ export const L2AuthNewPasswordSchema = z
27
+ .string()
28
+ .min(12)
29
+ .max(256)
30
+ .refine(
31
+ (password) => password === password.trim() && !/[\p{Cc}\p{Cf}\p{Zl}\p{Zp}]/u.test(password)
32
+ )
33
+ .refine(
34
+ (password) =>
35
+ [/[A-Z]/, /[a-z]/, /[0-9]/, /[\p{P}\p{S}]/u].filter((pattern) => pattern.test(password))
36
+ .length >= 3
37
+ )
38
+
26
39
  export const L2AuthSettingsReplaceRequestSchema = z.union([
27
- L2AuthLoginRequestSchema.extend({ password: z.string().min(8).max(256) }),
40
+ L2AuthLoginRequestSchema.extend({ password: L2AuthNewPasswordSchema }),
28
41
  z.object({ password: z.null() }).strict()
29
42
  ])
30
43
  export const L2AuthSettingsReplaceResponseSchema = z.object({}).strict()
@@ -9,8 +9,12 @@ export const L2_AUTH_MESSAGES = {
9
9
  changePassword: 'Change password',
10
10
  setPassword: 'Set a password',
11
11
  newPassword: 'New password',
12
- passwordHint: 'At least 8 characters',
13
- saveEffect: 'Saving requires all devices to sign in again.',
12
+ passwordHint:
13
+ '12–256 characters; use at least three of: uppercase letters, lowercase letters, numbers and symbols. No leading/trailing spaces, line breaks or invisible control characters.',
14
+ generatePassword: 'Generate password',
15
+ generatePasswordHint: 'Generate a random 16-character password',
16
+ generatePasswordFailed: 'Unable to generate a password. Please try again.',
17
+ saveEffect: 'Keep your password safe. Saving requires all devices to sign in again.',
14
18
  enable: 'Enable sign-in protection',
15
19
  saveAndSignIn: 'Save and sign in again',
16
20
  saving: 'Saving…',
@@ -27,7 +31,7 @@ export const L2_AUTH_MESSAGES = {
27
31
  recoveryDescription:
28
32
  'Delete this file on the device running the service, then refresh the page to restore access.',
29
33
  settingsFailed: 'Unable to load or save sign-in protection settings.',
30
- settingsInvalid: 'Enter a password of at least 8 characters.',
34
+ settingsInvalid: 'The password does not meet the requirements shown below the field.',
31
35
  configUnavailable:
32
36
  'Unable to read sign-in protection settings. Repair the configuration or delete auth.json and set it up again.',
33
37
  crossOriginDenied: 'Cross-site changes to sign-in protection are not allowed.',
@@ -36,8 +40,13 @@ export const L2_AUTH_MESSAGES = {
36
40
  submitting: 'Signing in…',
37
41
  loginFailed: 'Unable to sign in',
38
42
  invalidResponse: 'Invalid response from the sign-in service',
39
- invalidCredentials: 'Incorrect password',
40
- invalidInput: 'Invalid sign-in details',
43
+ invalidCredentials: 'Sign-in failed. Check your sign-in details and try again.',
44
+ invalidInput: 'Sign-in failed. Check your sign-in details and try again.',
45
+ unavailable: 'Sign-in is temporarily unavailable. Please try again later.',
46
+ tooManyAttempts: 'Too many sign-in attempts. Please try again later.',
47
+ loginAlertTitle: 'Unusual sign-in activity',
48
+ loginAlertDescription:
49
+ 'More than 5 consecutive sign-in failures. Check whether these attempts were yours.',
41
50
  invalidClient: 'Invalid client identifier'
42
51
  },
43
52
  'zh-CN': {
@@ -50,8 +59,12 @@ export const L2_AUTH_MESSAGES = {
50
59
  changePassword: '修改密码',
51
60
  setPassword: '设置密码',
52
61
  newPassword: '新密码',
53
- passwordHint: '至少 8 个字符',
54
- saveEffect: '保存后,所有设备都需要重新登录。',
62
+ passwordHint:
63
+ '12–256 个字符,至少包含大写字母、小写字母、数字、符号中的三类;不能有首尾空白、换行或不可见控制字符。',
64
+ generatePassword: '生成密码',
65
+ generatePasswordHint: '随机生成 16 位密码',
66
+ generatePasswordFailed: '生成密码失败,请重试。',
67
+ saveEffect: '请妥善保存密码。保存后,所有设备都需要重新登录。',
55
68
  enable: '启用登录保护',
56
69
  saveAndSignIn: '保存并重新登录',
57
70
  saving: '正在保存…',
@@ -66,7 +79,7 @@ export const L2_AUTH_MESSAGES = {
66
79
  recovery: '忘记密码?',
67
80
  recoveryDescription: '在运行服务的设备上删除下面的文件,然后刷新页面,即可恢复访问。',
68
81
  settingsFailed: '登录保护设置处理失败。',
69
- settingsInvalid: '请填写至少 8 位密码。',
82
+ settingsInvalid: '密码不符合要求,请按输入框下方的规则调整。',
70
83
  configUnavailable: '登录保护配置无法读取,请修复配置或删除 auth.json 后重新设置。',
71
84
  crossOriginDenied: '不允许跨站修改登录保护。',
72
85
  password: '密码',
@@ -74,8 +87,12 @@ export const L2_AUTH_MESSAGES = {
74
87
  submitting: '正在登录',
75
88
  loginFailed: '登录失败',
76
89
  invalidResponse: '登录服务响应无效',
77
- invalidCredentials: '密码不正确',
78
- invalidInput: '登录参数无效',
90
+ invalidCredentials: '登录失败,请检查登录信息后重试。',
91
+ invalidInput: '登录失败,请检查登录信息后重试。',
92
+ unavailable: '登录暂不可用,请稍后重试。',
93
+ tooManyAttempts: '登录尝试过多,请稍后重试。',
94
+ loginAlertTitle: '登录异常提醒',
95
+ loginAlertDescription: '登录已连续失败超过 5 次,请确认是否为本人操作。',
79
96
  invalidClient: '客户端标识无效'
80
97
  }
81
98
  } as const
@@ -9,6 +9,7 @@ import {
9
9
  import { L4TimestampMsSchema } from '@common/l4_foundation/l4-timestamp-contract'
10
10
 
11
11
  export const L3AppNotificationIdSchema = z.string().uuid()
12
+ export const L3AppNotificationKeySchema = z.string().trim().min(1).max(256)
12
13
  export const L3AppNotificationLevelSchema = z.enum(['info', 'success', 'warning', 'error'])
13
14
  export const L3AppNotificationTitleSchema = z.string().trim().min(1).max(120)
14
15
  export const L3AppNotificationDescriptionSchema = z.string().trim().max(320).nullable()
@@ -51,7 +52,8 @@ export const L3AppNotificationChangesSchema = z
51
52
  .object({
52
53
  level: L3AppNotificationLevelSchema.optional(),
53
54
  title: L3AppNotificationTitleSchema.optional(),
54
- description: L3AppNotificationDescriptionSchema.optional()
55
+ description: L3AppNotificationDescriptionSchema.optional(),
56
+ event: L3AppNotificationEventSchema.nullable().optional()
55
57
  })
56
58
  .strict()
57
59
  .refine((changes) => Object.keys(changes).length > 0, {
@@ -173,6 +175,7 @@ export const L3PluginNotificationEventInputSchema = z
173
175
 
174
176
  export const L3PluginNotificationPublishInputSchema = z
175
177
  .object({
178
+ key: L3AppNotificationKeySchema.optional(),
176
179
  level: L3AppNotificationLevelSchema,
177
180
  title: L3AppNotificationTitleSchema,
178
181
  description: z.string().trim().max(320).optional(),
@@ -38,6 +38,8 @@ export const L3_CONVERSATION_LOCALE_MESSAGES = {
38
38
  thinkingLoading: 'Loading thoughts…',
39
39
  thinkingUnavailable: 'Thoughts unavailable',
40
40
  thinkingProcess: 'Reasoning',
41
+ thinkingTruncated: 'Showing the last {{count}} characters',
42
+ copyThinking: 'Copy full reasoning',
41
43
  streamInterrupted: 'The response stream ended before the model completed its answer.',
42
44
  requestTimedOut: 'The model did not complete its response in time.',
43
45
  modelFailed: 'The model request failed without a detailed error.',
@@ -183,6 +185,8 @@ export const L3_CONVERSATION_LOCALE_MESSAGES = {
183
185
  thinkingLoading: '正在加载思考内容…',
184
186
  thinkingUnavailable: '思考内容不可用',
185
187
  thinkingProcess: '思考过程',
188
+ thinkingTruncated: '仅显示最近 {{count}} 个字符',
189
+ copyThinking: '复制完整思考',
186
190
  streamInterrupted: '模型响应流意外中断:连接在回答完成前被关闭。',
187
191
  requestTimedOut: '模型请求超时:在限定时间内未收到完整响应。',
188
192
  modelFailed: '模型请求失败:服务端未提供具体错误信息。',
@@ -13,6 +13,10 @@ export const L3_PROJECT_FILES_LOCALE_MESSAGES = {
13
13
  noChangedFiles: 'No changed files',
14
14
  noChanges: 'No changes',
15
15
  gitBranches: 'Git branches',
16
+ gitBranchReadOnly: 'Git branch (read-only)',
17
+ selectPreviewDirectory: 'Switch preview directory',
18
+ previewDirectories: 'Preview directories',
19
+ sessionDirectory: 'Original folder',
16
20
  unavailable: 'Unavailable',
17
21
  projectFiles: '{{name}} project files',
18
22
  searchFiles: 'Search filenames',
@@ -107,6 +111,10 @@ export const L3_PROJECT_FILES_LOCALE_MESSAGES = {
107
111
  noChangedFiles: '没有变更文件',
108
112
  noChanges: '没有变更',
109
113
  gitBranches: 'Git 分支',
114
+ gitBranchReadOnly: 'Git 分支(只读)',
115
+ selectPreviewDirectory: '切换预览目录',
116
+ previewDirectories: '预览目录',
117
+ sessionDirectory: '原目录',
110
118
  unavailable: '不可用',
111
119
  projectFiles: '{{name}} 项目文件',
112
120
  searchFiles: '搜索文件名',
@@ -0,0 +1,25 @@
1
+ import { z } from 'zod'
2
+
3
+ export const L4_PROJECT_PREVIEW_PLUGIN_KEY = 'project-preview'
4
+ export const L4_PROJECT_PREVIEW_MAX_DIRECTORIES = 32
5
+
6
+ export const L4ProjectPreviewStateSchema = z
7
+ .object({
8
+ directories: z.array(z.string().min(1).max(4096)).max(L4_PROJECT_PREVIEW_MAX_DIRECTORIES),
9
+ defaultDirectory: z.string().min(1).max(4096).nullable()
10
+ })
11
+ .strict()
12
+
13
+ export type L4ProjectPreviewState = z.infer<typeof L4ProjectPreviewStateSchema>
14
+
15
+ export function readL4ProjectPreviewState(value: unknown): L4ProjectPreviewState {
16
+ const result = L4ProjectPreviewStateSchema.safeParse(value)
17
+ return result.success ? result.data : { directories: [], defaultDirectory: null }
18
+ }
19
+
20
+ export function projectPreviewDirectoryKey(directory: string): string {
21
+ const normalized = directory.replaceAll('\\', '/').replace(/\/+$/, '') || '/'
22
+ return /^[a-z]:/i.test(normalized) || normalized.startsWith('//')
23
+ ? normalized.toLowerCase()
24
+ : normalized
25
+ }
@@ -5,7 +5,7 @@ import {
5
5
  L2AuthLoginResponseSchema
6
6
  } from '@common/l2_biz/auth/l2-auth-contract'
7
7
  import { authenticateL2WebLogin } from '@server/l2_biz/auth/l2-auth'
8
- import { L4AuthConfigError } from '@server/l4_foundation/auth/l4-auth-config'
8
+ import { L4AuthBusyError, L4AuthConfigError } from '@server/l4_foundation/auth/l4-auth-config'
9
9
  import {
10
10
  createL4WebAuthCookieHeader,
11
11
  shouldUseL4SecureWebCookie
@@ -25,18 +25,24 @@ export async function POST(request: Request): Promise<Response> {
25
25
  try {
26
26
  payload = await request.json()
27
27
  } catch {
28
- return createL4ApiErrorResponse(400, '请求 JSON 格式无效', { key: 'auth:invalidInput' })
28
+ return createL4ApiErrorResponse(400, '登录失败,请检查登录信息后重试。', {
29
+ key: 'auth:invalidInput'
30
+ })
29
31
  }
30
32
 
31
33
  const input = L2AuthLoginRequestSchema.safeParse(payload)
32
34
  if (!input.success)
33
- return createL4ApiErrorResponse(400, '登录参数无效', { key: 'auth:invalidInput' })
35
+ return createL4ApiErrorResponse(400, '登录失败,请检查登录信息后重试。', {
36
+ key: 'auth:invalidInput'
37
+ })
34
38
 
35
39
  try {
36
40
  const result = await authenticateL2WebLogin(input.data)
37
41
  if (!result) {
38
42
  console.warn('[Pi Desk][AuthLoginApi] 登录凭据无效', { clientId })
39
- return createL4ApiErrorResponse(401, '密码不正确', { key: 'auth:invalidCredentials' })
43
+ return createL4ApiErrorResponse(401, '登录失败,请检查登录信息后重试。', {
44
+ key: 'auth:invalidCredentials'
45
+ })
40
46
  }
41
47
 
42
48
  const response = createL4ApiSuccessResponse(L2AuthLoginResponseSchema.parse({}))
@@ -53,13 +59,21 @@ export async function POST(request: Request): Promise<Response> {
53
59
  console.info('[Pi Desk][AuthLoginApi] 登录成功', { clientId })
54
60
  return response
55
61
  } catch (error) {
62
+ if (error instanceof L4AuthBusyError) {
63
+ const response = createL4ApiErrorResponse(429, error.message, { key: 'auth:tooManyAttempts' })
64
+ response.headers.set('Retry-After', '1')
65
+ response.headers.set('Cache-Control', 'no-store')
66
+ return response
67
+ }
56
68
  console.error('[Pi Desk][AuthLoginApi] 登录处理失败', {
57
69
  clientId,
58
70
  errorName: error instanceof Error ? error.name : 'UnknownError'
59
71
  })
60
72
  if (error instanceof L4AuthConfigError) {
61
- return createL4ApiErrorResponse(503, error.message, { key: 'auth:configUnavailable' })
73
+ return createL4ApiErrorResponse(503, '登录暂不可用,请稍后重试。', {
74
+ key: 'auth:unavailable'
75
+ })
62
76
  }
63
- return createL4ApiErrorResponse(500, '登录失败,请稍后重试', { key: 'auth:loginFailed' })
77
+ return createL4ApiErrorResponse(500, '登录暂不可用,请稍后重试。', { key: 'auth:unavailable' })
64
78
  }
65
79
  }
@@ -65,7 +65,7 @@ async function handle(request: Request, action: 'get' | 'replace'): Promise<Resp
65
65
 
66
66
  const input = L2AuthSettingsReplaceRequestSchema.safeParse(payload)
67
67
  if (!input.success) {
68
- return createL4ApiErrorResponse(400, '请填写至少 8 位密码', {
68
+ return createL4ApiErrorResponse(400, '密码不符合要求,请按输入框下方的规则调整。', {
69
69
  key: 'auth:settingsInvalid'
70
70
  })
71
71
  }
@@ -35,17 +35,26 @@ export function createL1ProjectRoute<I extends contracts.L3ProjectReadContext, O
35
35
  key: 'errors:projectCwdAbsolute'
36
36
  })
37
37
  try {
38
+ let isCurrent: (() => boolean) | undefined
38
39
  if (input.workId) {
39
- const session = await getL2WorkSessionManage().getWorkSession(input.workId)
40
+ const manage = getL2WorkSessionManage()
41
+ const session = await manage.getWorkSession(input.workId)
40
42
  if (!session)
41
43
  return createL4ApiErrorResponse(404, '工作会话不存在', { key: 'errors:sessionMissing' })
42
- if (session.cwd !== input.cwd)
43
- return createL4ApiErrorResponse(409, '工作会话项目已变化', {
44
+ const context = manage.getProjectPreviewContext(session, input.cwd)
45
+ if (!context)
46
+ return createL4ApiErrorResponse(409, '工作会话项目已变化或预览目录未登记', {
44
47
  key: 'errors:projectChanged'
45
48
  })
46
- input.cwd = session.cwd
49
+ input.cwd = context.cwd
50
+ isCurrent = context.isCurrent
47
51
  }
48
- return createL4ApiSuccessResponse(contract.response.parse(await execute(input)))
52
+ const result = await execute(input)
53
+ if (isCurrent && !isCurrent())
54
+ return createL4ApiErrorResponse(409, '工作会话项目已变化', {
55
+ key: 'errors:projectChanged'
56
+ })
57
+ return createL4ApiSuccessResponse(contract.response.parse(result))
49
58
  } catch (error) {
50
59
  console.warn('[Pi Desk][Project] 项目请求失败', {
51
60
  path: contract.path,
@@ -113,12 +113,8 @@ function allowHttpRequest(
113
113
  try {
114
114
  authenticated = authRuntime?.isAuthenticated(request.headers.cookie) ?? false
115
115
  } catch {
116
- endHttpAuthError(
117
- response,
118
- 503,
119
- '登录保护配置无法读取,请修复配置或删除 auth.json 后重新设置',
120
- 'auth:configUnavailable'
121
- )
116
+ console.error('[Pi Desk][NodeServer] 登录保护配置读取失败')
117
+ endHttpAuthError(response, 503, '登录暂不可用,请稍后重试。', 'auth:unavailable')
122
118
  return false
123
119
  }
124
120
  if (pathname === LOGIN_PAGE_PATH) {
@@ -399,7 +395,8 @@ async function startServer(): Promise<void> {
399
395
  try {
400
396
  authenticated = authRuntime.isAuthenticated(request.headers.cookie)
401
397
  } catch {
402
- rejectUpgrade(socket, 503, '登录保护配置无法读取')
398
+ console.error('[Pi Desk][NodeServer] 握手认证配置读取失败')
399
+ rejectUpgrade(socket, 503, '登录暂不可用,请稍后重试。')
403
400
  return
404
401
  }
405
402
  if (!authenticated) {
@@ -28,7 +28,12 @@ export { L3AppRuntimeTargetNotFoundError as L2AppRuntimeTargetNotFoundError }
28
28
  export type L2AppRuntime = L3AppRuntime
29
29
 
30
30
  export interface L2AppNotificationPublisher {
31
- publish(input: { level: L3AppNotificationLevel; title: string; description?: string }): string
31
+ publish(input: {
32
+ key?: string
33
+ level: L3AppNotificationLevel
34
+ title: string
35
+ description?: string
36
+ }): string
32
37
  update(notificationId: string, changes: L3AppNotificationChanges): void
33
38
  delete(notificationId: string): void
34
39
  dispose(): void
@@ -19,12 +19,56 @@ import {
19
19
  type L4AuthCredentials
20
20
  } from '@server/l4_foundation/auth/l4-auth-config'
21
21
  import { appendL4LoginRecord } from '@server/l4_foundation/auth/l4-login-record-store'
22
+ import {
23
+ getL3AppNotificationRuntime,
24
+ type L3AppNotificationPublisher,
25
+ type L3AppNotificationRuntime
26
+ } from '@server/l3_modules/app-runtime/l3-app-notification-runtime'
27
+ import { getL4HostSettingsStore } from '@server/l4_foundation/l4-host-settings-store'
28
+ import { L2_AUTH_MESSAGES } from '@common/l2_biz/auth/l2-auth-messages'
22
29
 
23
30
  export const L2_WEB_SESSION_COOKIE_NAME = 'pi-desk-session'
24
31
  const WEB_SESSION_MAX_AGE_SECONDS = 60 * 60 * 24 * 30
25
32
 
33
+ function recordLoginResult(success: boolean): void {
34
+ const runtime = getL3AppNotificationRuntime()
35
+ const loginAttempts = (globalThis.__piDeskLoginAttempts ??= new WeakMap())
36
+ let attempts = loginAttempts.get(runtime)
37
+ if (!attempts) {
38
+ attempts = { failures: 0, publisher: runtime.createPublisher() }
39
+ loginAttempts.set(runtime, attempts)
40
+ }
41
+ if (success) {
42
+ attempts.failures = 0
43
+ return
44
+ }
45
+ attempts.failures += 1
46
+ if (attempts.failures <= 5) return
47
+ if (attempts.failures === 6) {
48
+ console.warn('[Pi Desk][Auth] 连续登录失败超过阈值,发布登录异常提醒')
49
+ }
50
+ try {
51
+ const locale = getL4HostSettingsStore().getSnapshot().region.locale
52
+ const messages = L2_AUTH_MESSAGES[locale]
53
+ attempts.publisher.publish({
54
+ key: 'auth:login-failures',
55
+ level: 'warning',
56
+ title: messages.loginAlertTitle,
57
+ description: messages.loginAlertDescription
58
+ })
59
+ } catch (error) {
60
+ console.error('[Pi Desk][Auth] 登录异常提醒发布失败', {
61
+ errorName: error instanceof Error ? error.name : 'UnknownError'
62
+ })
63
+ }
64
+ }
65
+
26
66
  declare global {
27
67
  var __piDeskAuthMutation: Promise<void> | undefined
68
+ // 跨 HTTP 路由共享,计数和发布者跟随通知 Runtime 释放,不保存失败请求内容。
69
+ var __piDeskLoginAttempts:
70
+ | WeakMap<L3AppNotificationRuntime, { failures: number; publisher: L3AppNotificationPublisher }>
71
+ | undefined
28
72
  }
29
73
 
30
74
  export interface L2WebAuthLoginResult {
@@ -58,15 +102,20 @@ export async function authenticateL2WebLogin(
58
102
  ): Promise<L2WebAuthLoginResult | null> {
59
103
  const credentials = readL4AuthCredentials()
60
104
  if (!credentials) {
105
+ recordLoginResult(true)
61
106
  return { cookieName: L2_WEB_SESSION_COOKIE_NAME, token: '', maxAgeSeconds: 0 }
62
107
  }
63
108
  const passwordMatches = await verifyL4AuthPassword(input.password, credentials.passwordHash)
64
- if (!passwordMatches) return null
65
- // scrypt 计算期间凭据可能已被修改或删除,不为旧配置签发会话。
109
+ // scrypt 计算期间凭据可能已被修改或删除,不为旧配置签发会话或累计失败次数。
66
110
  if (readL4AuthCredentials()?.sessionSecret !== credentials.sessionSecret) return null
111
+ if (!passwordMatches) {
112
+ recordLoginResult(false)
113
+ return null
114
+ }
67
115
 
68
116
  const loggedInAt = Date.now()
69
117
  appendL4LoginRecord({ loggedInAt })
118
+ recordLoginResult(true)
70
119
  return {
71
120
  cookieName: L2_WEB_SESSION_COOKIE_NAME,
72
121
  token: createL4WebAuthSessionToken(
@@ -101,6 +150,7 @@ export function replaceL2AuthSettings(
101
150
  throw new L2AuthRequiredError()
102
151
  }
103
152
  writeL4AuthConfig(config)
153
+ recordLoginResult(true)
104
154
  console.info('[Pi Desk][Auth] 登录保护已更新', { enabled: config !== null })
105
155
  })
106
156
  globalThis.__piDeskAuthMutation = operation.catch(() => undefined)
@@ -1,5 +1,11 @@
1
1
  import 'server-only'
2
2
 
3
+ import { resolve } from 'node:path'
4
+ import {
5
+ L4_PROJECT_PREVIEW_PLUGIN_KEY,
6
+ projectPreviewDirectoryKey,
7
+ readL4ProjectPreviewState
8
+ } from '@common/l4_foundation/file/l4-project-preview-contract'
3
9
  import {
4
10
  L2ChatModelContextGetRequestSchema,
5
11
  L2ChatModelContextGetResponseSchema,
@@ -274,6 +280,28 @@ export class L2WorkSessionChatRuntime {
274
280
  return record !== undefined && this.matches(record.source, source)
275
281
  }
276
282
 
283
+ getProjectPreviewContext(
284
+ workSession: WorkSession,
285
+ directory: string
286
+ ): { cwd: string; isCurrent: () => boolean } | null {
287
+ const record = this.recordsByWorkId.get(workSession.workId)
288
+ if (!record || record.workSession !== workSession) return null
289
+ const key = projectPreviewDirectoryKey(resolve(directory))
290
+ const originalCwd = key === projectPreviewDirectoryKey(resolve(workSession.cwd))
291
+ const registeredDirectory = (): string | undefined =>
292
+ readL4ProjectPreviewState(
293
+ record.state.runtime.plugins[L4_PROJECT_PREVIEW_PLUGIN_KEY]
294
+ ).directories.find((item) => projectPreviewDirectoryKey(item) === key)
295
+ const cwd = originalCwd ? workSession.cwd : registeredDirectory()
296
+ if (!cwd) return null
297
+ const isCurrent = (): boolean =>
298
+ this.recordsByWorkId.get(workSession.workId) === record &&
299
+ record.acceptingCommands &&
300
+ this.matches(record.source, this.sourceFor(workSession)) &&
301
+ (originalCwd || registeredDirectory() !== undefined)
302
+ return isCurrent() ? { cwd, isCurrent } : null
303
+ }
304
+
277
305
  refreshPluginMessages(): void {
278
306
  for (const record of this.recordsByWorkId.values()) {
279
307
  if (record.state.runtime.presentationMode === 'basic') continue
@@ -477,6 +477,14 @@ export class WorkSessionManage {
477
477
  })
478
478
  }
479
479
 
480
+ getProjectPreviewContext(
481
+ workSession: WorkSession,
482
+ directory: string
483
+ ): { cwd: string; isCurrent: () => boolean } | null {
484
+ if (this.workSessionsById.get(workSession.workId) !== workSession) return null
485
+ return this.chat.getProjectPreviewContext(workSession, directory)
486
+ }
487
+
480
488
  async replaceProjectGitBranch(
481
489
  inputValue: L2WorkSessionGitBranchReplaceRequest
482
490
  ): Promise<L2WorkSessionGitBranchOperationResponse> {
@@ -1,8 +1,10 @@
1
1
  import 'server-only'
2
2
 
3
3
  import { randomUUID } from 'node:crypto'
4
+ import { isDeepStrictEqual } from 'node:util'
4
5
  import {
5
6
  L3AppNotificationChangesSchema,
7
+ L3AppNotificationKeySchema,
6
8
  L3AppNotificationSchema,
7
9
  L3PluginNotificationChangesSchema,
8
10
  L3PluginNotificationPublishInputSchema,
@@ -21,6 +23,7 @@ const L3_PLUGIN_NOTIFICATION_EVENT_MAX_JSON_BYTES = 64 * 1024
21
23
  type L3AppNotificationOwner = string | object
22
24
 
23
25
  export interface L3AppNotificationPublishInput {
26
+ key?: string
24
27
  level: L3AppNotificationLevel
25
28
  title: string
26
29
  description?: string
@@ -49,7 +52,10 @@ export class L3AppNotificationOwnerError extends Error {
49
52
  }
50
53
 
51
54
  export class L3AppNotificationRuntime {
52
- private readonly ownersByNotificationId = new Map<string, L3AppNotificationOwner>()
55
+ private readonly ownersByNotificationId = new Map<
56
+ string,
57
+ { owner: L3AppNotificationOwner; key: string | undefined }
58
+ >()
53
59
  private readonly unsubscribe: () => void
54
60
  private disposed = false
55
61
 
@@ -100,6 +106,7 @@ export class L3AppNotificationRuntime {
100
106
  const pluginName = L3PluginNameSchema.parse(pluginNameInput)
101
107
  const normalized = L3PluginNotificationPublishInputSchema.parse(input)
102
108
  return this.publish(pluginName, {
109
+ key: normalized.key,
103
110
  level: normalized.level,
104
111
  title: normalized.title,
105
112
  ...(normalized.description === undefined ? {} : { description: normalized.description }),
@@ -154,7 +161,42 @@ export class L3AppNotificationRuntime {
154
161
  throw new Error('Plugin notification event data exceeds 64KB')
155
162
  }
156
163
 
164
+ const key = L3AppNotificationKeySchema.optional().parse(input.key)
157
165
  const current = this.runtime.readSnapshot().notifications
166
+ const existing =
167
+ key === undefined
168
+ ? undefined
169
+ : current.find((notification) => {
170
+ const registration = this.ownersByNotificationId.get(notification.notificationId)
171
+ return registration?.owner === owner && registration.key === key
172
+ })
173
+ const notification = L3AppNotificationSchema.parse({
174
+ notificationId: existing?.notificationId ?? randomUUID(),
175
+ level: input.level,
176
+ title: input.title,
177
+ description: input.description ?? null,
178
+ createdAt: existing?.createdAt ?? Date.now(),
179
+ event: input.event ?? null
180
+ })
181
+
182
+ if (existing) {
183
+ const changes = {
184
+ level: notification.level,
185
+ title: notification.title,
186
+ description: notification.description,
187
+ event: notification.event
188
+ }
189
+ if (
190
+ existing.level !== changes.level ||
191
+ existing.title !== changes.title ||
192
+ existing.description !== changes.description ||
193
+ !isDeepStrictEqual(existing.event, changes.event)
194
+ ) {
195
+ this.update(owner, existing.notificationId, changes)
196
+ }
197
+ return existing.notificationId
198
+ }
199
+
158
200
  const oldest = current.length >= L3_APP_NOTIFICATION_MAX_ITEMS ? current[0] : undefined
159
201
  if (oldest) {
160
202
  this.runtime.applyInternal({
@@ -163,15 +205,7 @@ export class L3AppNotificationRuntime {
163
205
  })
164
206
  }
165
207
 
166
- const notification = L3AppNotificationSchema.parse({
167
- notificationId: randomUUID(),
168
- level: input.level,
169
- title: input.title,
170
- description: input.description ?? null,
171
- createdAt: Date.now(),
172
- event: input.event ?? null
173
- })
174
- this.ownersByNotificationId.set(notification.notificationId, owner)
208
+ this.ownersByNotificationId.set(notification.notificationId, { owner, key })
175
209
  try {
176
210
  this.runtime.applyInternal({
177
211
  key: 'notifications',
@@ -215,7 +249,7 @@ export class L3AppNotificationRuntime {
215
249
  this.runtime.readSnapshot().notifications.map((notification) => notification.notificationId)
216
250
  )
217
251
  const notificationIds = [...this.ownersByNotificationId.entries()]
218
- .filter(([, currentOwner]) => currentOwner === owner)
252
+ .filter(([, registration]) => registration.owner === owner)
219
253
  .map(([notificationId]) => notificationId)
220
254
  for (const notificationId of notificationIds) {
221
255
  if (!activeIds.has(notificationId)) {
@@ -230,9 +264,9 @@ export class L3AppNotificationRuntime {
230
264
  }
231
265
 
232
266
  private assertOwner(owner: L3AppNotificationOwner, notificationId: string): void {
233
- const currentOwner = this.ownersByNotificationId.get(notificationId)
234
- if (!currentOwner) throw new L3AppNotificationNotFoundError(notificationId)
235
- if (currentOwner !== owner) throw new L3AppNotificationOwnerError(notificationId)
267
+ const registration = this.ownersByNotificationId.get(notificationId)
268
+ if (!registration) throw new L3AppNotificationNotFoundError(notificationId)
269
+ if (registration.owner !== owner) throw new L3AppNotificationOwnerError(notificationId)
236
270
  }
237
271
 
238
272
  private ensureActive(): void {
@@ -19,6 +19,15 @@ const ReadAuthConfigSchema = AuthConfigSchema.extend({
19
19
 
20
20
  type AuthConfig = z.infer<typeof AuthConfigSchema>
21
21
 
22
+ let activePasswordVerifications = 0
23
+
24
+ export class L4AuthBusyError extends Error {
25
+ constructor() {
26
+ super('登录尝试过多,请稍后重试。')
27
+ this.name = 'L4AuthBusyError'
28
+ }
29
+ }
30
+
22
31
  export interface L4AuthCredentials extends AuthConfig {
23
32
  sessionSecret: string
24
33
  }
@@ -97,9 +106,16 @@ export async function verifyL4AuthPassword(
97
106
  password: string,
98
107
  passwordHash: string
99
108
  ): Promise<boolean> {
100
- const [, salt, expected] = passwordHash.split(':')
101
- const actual = await derivePassword(password, salt)
102
- return timingSafeEqual(actual, Buffer.from(expected, 'hex'))
109
+ if (activePasswordVerifications >= 2) throw new L4AuthBusyError()
110
+ activePasswordVerifications += 1
111
+ try {
112
+ const [, salt, expected] = passwordHash.split(':')
113
+ const actual = await derivePassword(password, salt)
114
+ return timingSafeEqual(actual, Buffer.from(expected, 'hex'))
115
+ } finally {
116
+ // 计算完成才释放,不随 HTTP 连接提前关闭而释放。
117
+ activePasswordVerifications -= 1
118
+ }
103
119
  }
104
120
 
105
121
  export function writeL4AuthConfig(config: AuthConfig | null): void {
@@ -444,7 +444,8 @@ export class L4PiChatWorker {
444
444
  console.warn('[Pi Desk][PiChatWorker] 已忽略非法插件运行态更新', {
445
445
  sessionId: this.sessionManager.getSessionId(),
446
446
  cwd: this.cwd,
447
- errorName: cause instanceof Error ? cause.name : 'UnknownError'
447
+ errorName: cause instanceof Error ? cause.name : 'UnknownError',
448
+ message: cause instanceof Error ? cause.message : String(cause)
448
449
  })
449
450
  }
450
451
  }