@allbluecn/web-app 0.23.7 → 0.24.1

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 (68) hide show
  1. package/package.json +2 -2
  2. package/src/components/ai/global-harness-sidebar/__tests__/global-ai-prompts.test.ts +0 -2
  3. package/src/components/harness/ai-chat-history/components/conversation-item.tsx +6 -0
  4. package/src/components/harness/ai-chat-history/types.ts +4 -0
  5. package/src/components/harness/chat-ui.tsx +20 -0
  6. package/src/components/harness/composer/harness-composer.tsx +57 -20
  7. package/src/components/harness/composer/harness-engine-control.tsx +129 -0
  8. package/src/components/harness/harness-workbench-page.tsx +140 -18
  9. package/src/components/layout/sidebar-03/app-sidebar.tsx +2 -9
  10. package/src/config/sidebar-routes.tsx +0 -13
  11. package/src/lib/changelog/sdk-versions.ts +1 -1
  12. package/src/lib/hotkeys/__tests__/registry.test.ts +4 -11
  13. package/src/lib/hotkeys/registry.ts +0 -8
  14. package/src/paraglide/README.md +1 -1
  15. package/src/paraglide/messages/_index.d.ts +12 -2
  16. package/src/paraglide/messages/_index.js +12 -2
  17. package/src/paraglide/messages/{agent_desktop_title.d.ts → harness_engine_cloud.d.ts} +5 -5
  18. package/src/paraglide/messages/harness_engine_cloud.js +29 -0
  19. package/src/paraglide/messages/harness_engine_label.d.ts +16 -0
  20. package/src/paraglide/messages/harness_engine_label.js +29 -0
  21. package/src/paraglide/messages/harness_engine_local.d.ts +16 -0
  22. package/src/paraglide/messages/harness_engine_local.js +29 -0
  23. package/src/paraglide/messages/harness_engine_localbadge1.d.ts +17 -0
  24. package/src/paraglide/messages/harness_engine_localbadge1.js +30 -0
  25. package/src/paraglide/messages/harness_engine_localhint1.d.ts +17 -0
  26. package/src/paraglide/messages/harness_engine_localhint1.js +30 -0
  27. package/src/paraglide/messages/harness_engine_localnoattachments2.d.ts +17 -0
  28. package/src/paraglide/messages/harness_engine_localnoattachments2.js +30 -0
  29. package/src/paraglide/messages/harness_engine_switchlocked1.d.ts +17 -0
  30. package/src/paraglide/messages/harness_engine_switchlocked1.js +30 -0
  31. package/src/paraglide/messages/harness_localworkspace_choose1.d.ts +17 -0
  32. package/src/paraglide/messages/harness_localworkspace_choose1.js +30 -0
  33. package/src/paraglide/messages/harness_localworkspace_dialogdescription2.d.ts +17 -0
  34. package/src/paraglide/messages/harness_localworkspace_dialogdescription2.js +30 -0
  35. package/src/paraglide/messages/harness_localworkspace_dialogtitle2.d.ts +17 -0
  36. package/src/paraglide/messages/harness_localworkspace_dialogtitle2.js +30 -0
  37. package/src/paraglide/messages/harness_localworkspace_manage1.d.ts +17 -0
  38. package/src/paraglide/messages/harness_localworkspace_manage1.js +30 -0
  39. package/src/paraglide/messages/harness_localworkspace_required1.d.ts +17 -0
  40. package/src/paraglide/messages/harness_localworkspace_required1.js +30 -0
  41. package/src/routeTree.gen.ts +0 -23
  42. package/src/routes/_nav/harness/$threadId.lazy.tsx +19 -2
  43. package/src/routes/_nav/harness/$threadId.tsx +8 -2
  44. package/src/server/__tests__/chat-conversations.test.ts +13 -3
  45. package/src/server/__tests__/desktop-bootstrap.test.ts +0 -1
  46. package/src/server/ai-gateway/__tests__/local-agent-adapter.test.ts +110 -0
  47. package/src/server/ai-gateway/__tests__/unified-router-local-agent.test.ts +63 -0
  48. package/src/server/ai-gateway/chat-handler.ts +68 -2
  49. package/src/server/ai-gateway/chat-stream-builder.ts +52 -0
  50. package/src/server/ai-gateway/local-agent-adapter.ts +232 -0
  51. package/src/server/ai-gateway/unified-router.ts +49 -6
  52. package/src/server/serverFns/chat/conversations.ts +122 -1
  53. package/src/components/desktop-agent/__tests__/agent-chat.test.tsx +0 -194
  54. package/src/components/desktop-agent/agent-chat.tsx +0 -222
  55. package/src/lib/desktop-agent/__tests__/connection.test.ts +0 -310
  56. package/src/lib/desktop-agent/__tests__/errors.test.ts +0 -43
  57. package/src/lib/desktop-agent/connection.ts +0 -93
  58. package/src/lib/desktop-agent/errors.ts +0 -21
  59. package/src/paraglide/messages/agent_desktop_title.js +0 -29
  60. package/src/paraglide/messages/common_hotkey_desc_agentabort1.d.ts +0 -17
  61. package/src/paraglide/messages/common_hotkey_desc_agentabort1.js +0 -30
  62. package/src/routes/-tests/agent-gate.test.ts +0 -45
  63. package/src/routes/_nav/agent/route.lazy.tsx +0 -63
  64. package/src/routes/_nav/agent/route.tsx +0 -21
  65. /package/src/components/{desktop-agent → harness/local-agent}/__tests__/agent-tool-card.test.tsx +0 -0
  66. /package/src/components/{desktop-agent → harness/local-agent}/__tests__/agent-workspace-card.test.tsx +0 -0
  67. /package/src/components/{desktop-agent → harness/local-agent}/agent-tool-card.tsx +0 -0
  68. /package/src/components/{desktop-agent → harness/local-agent}/agent-workspace-card.tsx +0 -0
@@ -0,0 +1,30 @@
1
+ /* eslint-disable */
2
+ import { getLocale, experimentalStaticLocale } from '../runtime.js';
3
+
4
+ /** @typedef {import('../runtime.js').LocalizedString} LocalizedString */
5
+
6
+ /** @typedef {{}} Harness_Localworkspace_Dialogdescription2Inputs */
7
+
8
+ const zh_harness_localworkspace_dialogdescription2 = /** @type {(inputs: Harness_Localworkspace_Dialogdescription2Inputs) => LocalizedString} */ () => {
9
+ return /** @type {LocalizedString} */ (`pi 只能读取下方列出的目录;可随时撤销授权。`)
10
+ };
11
+
12
+ const en_harness_localworkspace_dialogdescription2 = /** @type {(inputs: Harness_Localworkspace_Dialogdescription2Inputs) => LocalizedString} */ () => {
13
+ return /** @type {LocalizedString} */ (`pi can only read the folders listed below; revoke access at any time.`)
14
+ };
15
+
16
+ /**
17
+ * | output |
18
+ * | --- |
19
+ * | "pi can only read the folders listed below; revoke access at any time." |
20
+ *
21
+ * @param {Harness_Localworkspace_Dialogdescription2Inputs} inputs
22
+ * @param {{ locale?: "zh" | "en" }} options
23
+ * @returns {LocalizedString}
24
+ */
25
+ const harness_localworkspace_dialogdescription2 = /** @type {((inputs?: Harness_Localworkspace_Dialogdescription2Inputs, options?: { locale?: "zh" | "en" }) => LocalizedString) & import('../runtime.js').MessageMetadata<Harness_Localworkspace_Dialogdescription2Inputs, { locale?: "zh" | "en" }, {}>} */ ((inputs = {}, options = {}) => {
26
+ const locale = experimentalStaticLocale ?? options.locale ?? getLocale()
27
+ if (locale === "en") return en_harness_localworkspace_dialogdescription2(inputs)
28
+ return zh_harness_localworkspace_dialogdescription2(inputs)
29
+ });
30
+ export { harness_localworkspace_dialogdescription2 as "harness_localWorkspace_dialogDescription" }
@@ -0,0 +1,17 @@
1
+ export { harness_localworkspace_dialogtitle2 as "harness_localWorkspace_dialogTitle" };
2
+ export type LocalizedString = import("../runtime.js").LocalizedString;
3
+ export type Harness_Localworkspace_Dialogtitle2Inputs = {};
4
+ /**
5
+ * | output |
6
+ * | --- |
7
+ * | "Local workspace" |
8
+ *
9
+ * @param {Harness_Localworkspace_Dialogtitle2Inputs} inputs
10
+ * @param {{ locale?: "zh" | "en" }} options
11
+ * @returns {LocalizedString}
12
+ */
13
+ declare const harness_localworkspace_dialogtitle2: ((inputs?: Harness_Localworkspace_Dialogtitle2Inputs, options?: {
14
+ locale?: "zh" | "en";
15
+ }) => LocalizedString) & import("../runtime.js").MessageMetadata<Harness_Localworkspace_Dialogtitle2Inputs, {
16
+ locale?: "zh" | "en";
17
+ }, {}>;
@@ -0,0 +1,30 @@
1
+ /* eslint-disable */
2
+ import { getLocale, experimentalStaticLocale } from '../runtime.js';
3
+
4
+ /** @typedef {import('../runtime.js').LocalizedString} LocalizedString */
5
+
6
+ /** @typedef {{}} Harness_Localworkspace_Dialogtitle2Inputs */
7
+
8
+ const zh_harness_localworkspace_dialogtitle2 = /** @type {(inputs: Harness_Localworkspace_Dialogtitle2Inputs) => LocalizedString} */ () => {
9
+ return /** @type {LocalizedString} */ (`本地工作区`)
10
+ };
11
+
12
+ const en_harness_localworkspace_dialogtitle2 = /** @type {(inputs: Harness_Localworkspace_Dialogtitle2Inputs) => LocalizedString} */ () => {
13
+ return /** @type {LocalizedString} */ (`Local workspace`)
14
+ };
15
+
16
+ /**
17
+ * | output |
18
+ * | --- |
19
+ * | "Local workspace" |
20
+ *
21
+ * @param {Harness_Localworkspace_Dialogtitle2Inputs} inputs
22
+ * @param {{ locale?: "zh" | "en" }} options
23
+ * @returns {LocalizedString}
24
+ */
25
+ const harness_localworkspace_dialogtitle2 = /** @type {((inputs?: Harness_Localworkspace_Dialogtitle2Inputs, options?: { locale?: "zh" | "en" }) => LocalizedString) & import('../runtime.js').MessageMetadata<Harness_Localworkspace_Dialogtitle2Inputs, { locale?: "zh" | "en" }, {}>} */ ((inputs = {}, options = {}) => {
26
+ const locale = experimentalStaticLocale ?? options.locale ?? getLocale()
27
+ if (locale === "en") return en_harness_localworkspace_dialogtitle2(inputs)
28
+ return zh_harness_localworkspace_dialogtitle2(inputs)
29
+ });
30
+ export { harness_localworkspace_dialogtitle2 as "harness_localWorkspace_dialogTitle" }
@@ -0,0 +1,17 @@
1
+ export { harness_localworkspace_manage1 as "harness_localWorkspace_manage" };
2
+ export type LocalizedString = import("../runtime.js").LocalizedString;
3
+ export type Harness_Localworkspace_Manage1Inputs = {};
4
+ /**
5
+ * | output |
6
+ * | --- |
7
+ * | "Manage authorized folders" |
8
+ *
9
+ * @param {Harness_Localworkspace_Manage1Inputs} inputs
10
+ * @param {{ locale?: "zh" | "en" }} options
11
+ * @returns {LocalizedString}
12
+ */
13
+ declare const harness_localworkspace_manage1: ((inputs?: Harness_Localworkspace_Manage1Inputs, options?: {
14
+ locale?: "zh" | "en";
15
+ }) => LocalizedString) & import("../runtime.js").MessageMetadata<Harness_Localworkspace_Manage1Inputs, {
16
+ locale?: "zh" | "en";
17
+ }, {}>;
@@ -0,0 +1,30 @@
1
+ /* eslint-disable */
2
+ import { getLocale, experimentalStaticLocale } from '../runtime.js';
3
+
4
+ /** @typedef {import('../runtime.js').LocalizedString} LocalizedString */
5
+
6
+ /** @typedef {{}} Harness_Localworkspace_Manage1Inputs */
7
+
8
+ const zh_harness_localworkspace_manage1 = /** @type {(inputs: Harness_Localworkspace_Manage1Inputs) => LocalizedString} */ () => {
9
+ return /** @type {LocalizedString} */ (`管理授权目录`)
10
+ };
11
+
12
+ const en_harness_localworkspace_manage1 = /** @type {(inputs: Harness_Localworkspace_Manage1Inputs) => LocalizedString} */ () => {
13
+ return /** @type {LocalizedString} */ (`Manage authorized folders`)
14
+ };
15
+
16
+ /**
17
+ * | output |
18
+ * | --- |
19
+ * | "Manage authorized folders" |
20
+ *
21
+ * @param {Harness_Localworkspace_Manage1Inputs} inputs
22
+ * @param {{ locale?: "zh" | "en" }} options
23
+ * @returns {LocalizedString}
24
+ */
25
+ const harness_localworkspace_manage1 = /** @type {((inputs?: Harness_Localworkspace_Manage1Inputs, options?: { locale?: "zh" | "en" }) => LocalizedString) & import('../runtime.js').MessageMetadata<Harness_Localworkspace_Manage1Inputs, { locale?: "zh" | "en" }, {}>} */ ((inputs = {}, options = {}) => {
26
+ const locale = experimentalStaticLocale ?? options.locale ?? getLocale()
27
+ if (locale === "en") return en_harness_localworkspace_manage1(inputs)
28
+ return zh_harness_localworkspace_manage1(inputs)
29
+ });
30
+ export { harness_localworkspace_manage1 as "harness_localWorkspace_manage" }
@@ -0,0 +1,17 @@
1
+ export { harness_localworkspace_required1 as "harness_localWorkspace_required" };
2
+ export type LocalizedString = import("../runtime.js").LocalizedString;
3
+ export type Harness_Localworkspace_Required1Inputs = {};
4
+ /**
5
+ * | output |
6
+ * | --- |
7
+ * | "Choose a local workspace first" |
8
+ *
9
+ * @param {Harness_Localworkspace_Required1Inputs} inputs
10
+ * @param {{ locale?: "zh" | "en" }} options
11
+ * @returns {LocalizedString}
12
+ */
13
+ declare const harness_localworkspace_required1: ((inputs?: Harness_Localworkspace_Required1Inputs, options?: {
14
+ locale?: "zh" | "en";
15
+ }) => LocalizedString) & import("../runtime.js").MessageMetadata<Harness_Localworkspace_Required1Inputs, {
16
+ locale?: "zh" | "en";
17
+ }, {}>;
@@ -0,0 +1,30 @@
1
+ /* eslint-disable */
2
+ import { getLocale, experimentalStaticLocale } from '../runtime.js';
3
+
4
+ /** @typedef {import('../runtime.js').LocalizedString} LocalizedString */
5
+
6
+ /** @typedef {{}} Harness_Localworkspace_Required1Inputs */
7
+
8
+ const zh_harness_localworkspace_required1 = /** @type {(inputs: Harness_Localworkspace_Required1Inputs) => LocalizedString} */ () => {
9
+ return /** @type {LocalizedString} */ (`请先选择本地工作目录`)
10
+ };
11
+
12
+ const en_harness_localworkspace_required1 = /** @type {(inputs: Harness_Localworkspace_Required1Inputs) => LocalizedString} */ () => {
13
+ return /** @type {LocalizedString} */ (`Choose a local workspace first`)
14
+ };
15
+
16
+ /**
17
+ * | output |
18
+ * | --- |
19
+ * | "Choose a local workspace first" |
20
+ *
21
+ * @param {Harness_Localworkspace_Required1Inputs} inputs
22
+ * @param {{ locale?: "zh" | "en" }} options
23
+ * @returns {LocalizedString}
24
+ */
25
+ const harness_localworkspace_required1 = /** @type {((inputs?: Harness_Localworkspace_Required1Inputs, options?: { locale?: "zh" | "en" }) => LocalizedString) & import('../runtime.js').MessageMetadata<Harness_Localworkspace_Required1Inputs, { locale?: "zh" | "en" }, {}>} */ ((inputs = {}, options = {}) => {
26
+ const locale = experimentalStaticLocale ?? options.locale ?? getLocale()
27
+ if (locale === "en") return en_harness_localworkspace_required1(inputs)
28
+ return zh_harness_localworkspace_required1(inputs)
29
+ });
30
+ export { harness_localworkspace_required1 as "harness_localWorkspace_required" }
@@ -23,7 +23,6 @@ import { Route as LoginLoginRouteRouteImport } from './routes/_login/login/route
23
23
  import { Route as LoginRegisterRouteRouteImport } from './routes/_login/register/route'
24
24
  import { Route as LoginResetPasswordRouteRouteImport } from './routes/_login/reset-password/route'
25
25
  import { Route as LoginVerifyEmailRouteRouteImport } from './routes/_login/verify-email/route'
26
- import { Route as NavAgentRouteRouteImport } from './routes/_nav/agent/route'
27
26
  import { Route as NavAppsRouteRouteImport } from './routes/_nav/apps/route'
28
27
  import { Route as NavBugsRouteRouteImport } from './routes/_nav/bugs/route'
29
28
  import { Route as NavChangelogRouteRouteImport } from './routes/_nav/changelog/route'
@@ -252,13 +251,6 @@ const LoginVerifyEmailRouteRoute = LoginVerifyEmailRouteRouteImport.update({
252
251
  } as any).lazy(() =>
253
252
  import('./routes/_login/verify-email/route.lazy').then((d) => d.Route),
254
253
  )
255
- const NavAgentRouteRoute = NavAgentRouteRouteImport.update({
256
- id: '/agent',
257
- path: '/agent',
258
- getParentRoute: () => NavRouteRoute,
259
- } as any).lazy(() =>
260
- import('./routes/_nav/agent/route.lazy').then((d) => d.Route),
261
- )
262
254
  const NavAppsRouteRoute = NavAppsRouteRouteImport.update({
263
255
  id: '/apps',
264
256
  path: '/apps',
@@ -1131,7 +1123,6 @@ export interface FileRoutesByFullPath {
1131
1123
  '/register': typeof LoginRegisterRouteRoute
1132
1124
  '/reset-password': typeof LoginResetPasswordRouteRoute
1133
1125
  '/verify-email': typeof LoginVerifyEmailRouteRoute
1134
- '/agent': typeof NavAgentRouteRoute
1135
1126
  '/apps': typeof NavAppsRouteRouteWithChildren
1136
1127
  '/bugs': typeof NavBugsRouteRouteWithChildren
1137
1128
  '/changelog': typeof NavChangelogRouteRoute
@@ -1271,7 +1262,6 @@ export interface FileRoutesByTo {
1271
1262
  '/register': typeof LoginRegisterRouteRoute
1272
1263
  '/reset-password': typeof LoginResetPasswordRouteRoute
1273
1264
  '/verify-email': typeof LoginVerifyEmailRouteRoute
1274
- '/agent': typeof NavAgentRouteRoute
1275
1265
  '/apps': typeof NavAppsRouteRouteWithChildren
1276
1266
  '/changelog': typeof NavChangelogRouteRoute
1277
1267
  '/dashboard': typeof NavDashboardRouteRoute
@@ -1400,7 +1390,6 @@ export interface FileRoutesById {
1400
1390
  '/_login/register': typeof LoginRegisterRouteRoute
1401
1391
  '/_login/reset-password': typeof LoginResetPasswordRouteRoute
1402
1392
  '/_login/verify-email': typeof LoginVerifyEmailRouteRoute
1403
- '/_nav/agent': typeof NavAgentRouteRoute
1404
1393
  '/_nav/apps': typeof NavAppsRouteRouteWithChildren
1405
1394
  '/_nav/bugs': typeof NavBugsRouteRouteWithChildren
1406
1395
  '/_nav/changelog': typeof NavChangelogRouteRoute
@@ -1542,7 +1531,6 @@ export interface FileRouteTypes {
1542
1531
  | '/register'
1543
1532
  | '/reset-password'
1544
1533
  | '/verify-email'
1545
- | '/agent'
1546
1534
  | '/apps'
1547
1535
  | '/bugs'
1548
1536
  | '/changelog'
@@ -1682,7 +1670,6 @@ export interface FileRouteTypes {
1682
1670
  | '/register'
1683
1671
  | '/reset-password'
1684
1672
  | '/verify-email'
1685
- | '/agent'
1686
1673
  | '/apps'
1687
1674
  | '/changelog'
1688
1675
  | '/dashboard'
@@ -1810,7 +1797,6 @@ export interface FileRouteTypes {
1810
1797
  | '/_login/register'
1811
1798
  | '/_login/reset-password'
1812
1799
  | '/_login/verify-email'
1813
- | '/_nav/agent'
1814
1800
  | '/_nav/apps'
1815
1801
  | '/_nav/bugs'
1816
1802
  | '/_nav/changelog'
@@ -2073,13 +2059,6 @@ declare module '@tanstack/react-router' {
2073
2059
  preLoaderRoute: typeof LoginVerifyEmailRouteRouteImport
2074
2060
  parentRoute: typeof LoginRouteRoute
2075
2061
  }
2076
- '/_nav/agent': {
2077
- id: '/_nav/agent'
2078
- path: '/agent'
2079
- fullPath: '/agent'
2080
- preLoaderRoute: typeof NavAgentRouteRouteImport
2081
- parentRoute: typeof NavRouteRoute
2082
- }
2083
2062
  '/_nav/apps': {
2084
2063
  id: '/_nav/apps'
2085
2064
  path: '/apps'
@@ -3372,7 +3351,6 @@ const NavChatRouteWithChildren =
3372
3351
  NavChatRoute._addFileChildren(NavChatRouteChildren)
3373
3352
 
3374
3353
  interface NavRouteRouteChildren {
3375
- NavAgentRouteRoute: typeof NavAgentRouteRoute
3376
3354
  NavAppsRouteRoute: typeof NavAppsRouteRouteWithChildren
3377
3355
  NavBugsRouteRoute: typeof NavBugsRouteRouteWithChildren
3378
3356
  NavChangelogRouteRoute: typeof NavChangelogRouteRoute
@@ -3394,7 +3372,6 @@ interface NavRouteRouteChildren {
3394
3372
  }
3395
3373
 
3396
3374
  const NavRouteRouteChildren: NavRouteRouteChildren = {
3397
- NavAgentRouteRoute: NavAgentRouteRoute,
3398
3375
  NavAppsRouteRoute: NavAppsRouteRouteWithChildren,
3399
3376
  NavBugsRouteRoute: NavBugsRouteRouteWithChildren,
3400
3377
  NavChangelogRouteRoute: NavChangelogRouteRoute,
@@ -25,9 +25,26 @@ export const Route = createLazyFileRoute('/_nav/harness/$threadId')({
25
25
  // loader 推断类型是 serverFn 的 JSON 序列化形状(role 含 "tool"、parts 为
26
26
  // JsonValue),与 HarnessWorkbenchPage 的 UIMessage 入参不结构兼容,
27
27
  // 断言是承载性的(审计建议的「免断言」会使 tsc 报错)
28
- const messages = Route.useLoaderData() as { messages: UIMessage[] }
28
+ const data = Route.useLoaderData() as {
29
+ messages: UIMessage[]
30
+ conversation: {
31
+ id: string
32
+ title: string | null
33
+ agentId: string | null
34
+ engine: "cloud" | "local"
35
+ localCwd: string | null
36
+ isArchived: boolean
37
+ } | null
38
+ }
29
39
  // key 强制重建:HarnessWorkbenchPage 以 useState(initialThreadId) 持有会话状态,
30
40
  // route param 变化时组件实例被复用,无 key 会残留上一线程的消息
31
- return <HarnessWorkbenchPage key={threadId} initialThreadId={threadId} initialMessages={messages.messages} />
41
+ return (
42
+ <HarnessWorkbenchPage
43
+ key={threadId}
44
+ initialThreadId={threadId}
45
+ initialMessages={data.messages}
46
+ initialConversation={data.conversation}
47
+ />
48
+ )
32
49
  },
33
50
  })
@@ -17,10 +17,16 @@
17
17
 
18
18
  import { createFileRoute } from '@tanstack/react-router'
19
19
  import { listMessagesFn } from '@/server/serverFns/chat/list-messages'
20
+ import { getConversationFn } from '@/server/serverFns/chat/conversations'
20
21
 
21
22
  export const Route = createFileRoute('/_nav/harness/$threadId')({
22
23
  loader: async ({ params }) => {
23
- const messages = await listMessagesFn({ data: { threadId: params.threadId } })
24
- return { messages }
24
+ // 会话元数据(engine/localCwd)与消息并行取:Harness 打开线程时要据此
25
+ // 还原「云端 / 本地工作区」引擎与目录选择,不能等首条消息再猜。
26
+ const [messages, conversation] = await Promise.all([
27
+ listMessagesFn({ data: { threadId: params.threadId } }),
28
+ getConversationFn({ data: { id: params.threadId } }),
29
+ ])
30
+ return { messages, conversation }
25
31
  },
26
32
  })
@@ -142,7 +142,7 @@ describe("createConversationFn", () => {
142
142
 
143
143
  expect(mocks.findFirst).not.toHaveBeenCalled()
144
144
  expect(mocks.create).toHaveBeenCalledWith({
145
- data: { userId, agentId: null },
145
+ data: { userId, agentId: null, engine: "cloud", localCwd: null },
146
146
  include: { agent: { select: { name: true, avatarEmoji: true } } },
147
147
  })
148
148
  expect(result).toMatchObject({
@@ -173,7 +173,7 @@ describe("createConversationFn", () => {
173
173
  },
174
174
  })
175
175
  expect(mocks.create).toHaveBeenCalledWith({
176
- data: { userId, agentId },
176
+ data: { userId, agentId, engine: "cloud", localCwd: null },
177
177
  include: { agent: { select: { name: true, avatarEmoji: true } } },
178
178
  })
179
179
  expect(result).toMatchObject({
@@ -198,7 +198,7 @@ describe("createConversationFn", () => {
198
198
 
199
199
  expect(mocks.findFirst).not.toHaveBeenCalled()
200
200
  expect(mocks.create).toHaveBeenCalledWith({
201
- data: { userId, agentId: null },
201
+ data: { userId, agentId: null, engine: "cloud", localCwd: null },
202
202
  include: { agent: { select: { name: true, avatarEmoji: true } } },
203
203
  })
204
204
  })
@@ -213,6 +213,8 @@ describe("listConversationsFn", () => {
213
213
  updatedAt: now,
214
214
  isArchived: false,
215
215
  agentId: "agent-001",
216
+ engine: "cloud",
217
+ localCwd: null,
216
218
  _count: { messages: 5 },
217
219
  agent: { name: "Researcher", avatarEmoji: "🔬" },
218
220
  interrupts: [{ id: "int-001" }],
@@ -223,6 +225,8 @@ describe("listConversationsFn", () => {
223
225
  updatedAt: new Date("2026-08-16T08:00:00.000Z"),
224
226
  isArchived: false,
225
227
  agentId: null,
228
+ engine: "cloud",
229
+ localCwd: null,
226
230
  _count: { messages: 0 },
227
231
  agent: null,
228
232
  interrupts: [],
@@ -240,6 +244,8 @@ describe("listConversationsFn", () => {
240
244
  title: true,
241
245
  isArchived: true,
242
246
  agentId: true,
247
+ engine: true,
248
+ localCwd: true,
243
249
  updatedAt: true,
244
250
  _count: { select: { messages: true } },
245
251
  agent: { select: { name: true, avatarEmoji: true } },
@@ -254,6 +260,8 @@ describe("listConversationsFn", () => {
254
260
  messageCount: 5,
255
261
  isArchived: false,
256
262
  agentId: "agent-001",
263
+ engine: "cloud",
264
+ localCwd: null,
257
265
  agentName: "Researcher",
258
266
  agentEmoji: "🔬",
259
267
  pendingInteraction: true,
@@ -265,6 +273,8 @@ describe("listConversationsFn", () => {
265
273
  messageCount: 0,
266
274
  isArchived: false,
267
275
  agentId: null,
276
+ engine: "cloud",
277
+ localCwd: null,
268
278
  agentName: null,
269
279
  agentEmoji: null,
270
280
  pendingInteraction: false,
@@ -83,7 +83,6 @@ vi.mock('@/server/entitlements-token', () => ({ generateEntitlementsToken: vi.fn
83
83
  vi.mock('@/server/middlewares/rate-limit', () => ({ checkAuthRateLimit: vi.fn() }))
84
84
 
85
85
  import { prisma } from '@allbluecn/database'
86
- import { ensureBuiltinIndependentProject } from '@/server/builtin-project'
87
86
  import { ensureLocalShadowUser } from '../desktop-bootstrap'
88
87
  import { getSessionFn } from '../serverFns/auth/session'
89
88
 
@@ -0,0 +1,110 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-or-later
2
+ // AllBlue - 理想之海
3
+ // Copyright (C) 2026 AllBlue Contributors
4
+
5
+ import { afterEach, describe, expect, it } from 'vitest'
6
+ import {
7
+ LOCAL_AGENT_GLOBAL_KEY,
8
+ LocalAgentAdapter,
9
+ extractLastUserText,
10
+ } from '../local-agent-adapter'
11
+
12
+ const original = (globalThis as Record<string, unknown>)[LOCAL_AGENT_GLOBAL_KEY]
13
+
14
+ afterEach(() => {
15
+ if (original === undefined) delete (globalThis as Record<string, unknown>)[LOCAL_AGENT_GLOBAL_KEY]
16
+ else (globalThis as Record<string, unknown>)[LOCAL_AGENT_GLOBAL_KEY] = original
17
+ })
18
+
19
+ function installService(run: (input: unknown) => AsyncIterable<unknown>) {
20
+ ;(globalThis as Record<string, unknown>)[LOCAL_AGENT_GLOBAL_KEY] = {
21
+ runLocalAgentPrompt: run,
22
+ }
23
+ }
24
+
25
+ function options(overrides: Record<string, unknown> = {}) {
26
+ return {
27
+ messages: [{ role: 'user', content: '总结这个目录' }],
28
+ threadId: 'thread-1',
29
+ runId: 'run-1',
30
+ modelOptions: { cwd: '/tmp/workspace' },
31
+ ...overrides,
32
+ } as never
33
+ }
34
+
35
+ describe('extractLastUserText', () => {
36
+ it('取最后一条非空 user 文本(字符串 / text parts 两种形态)', () => {
37
+ expect(
38
+ extractLastUserText([
39
+ { role: 'user', content: 'old' },
40
+ { role: 'assistant', content: 'answer' },
41
+ { role: 'user', content: [{ type: 'text', content: 'new' }] },
42
+ ] as never),
43
+ ).toBe('new')
44
+ })
45
+
46
+ it('忽略空 user 文本,回退更早的一条', () => {
47
+ expect(
48
+ extractLastUserText([
49
+ { role: 'user', content: 'first' },
50
+ { role: 'user', content: ' ' },
51
+ ] as never),
52
+ ).toBe('first')
53
+ })
54
+ })
55
+
56
+ describe('LocalAgentAdapter', () => {
57
+ it('未装配桌面运行时 → RUN_ERROR,不抛错', async () => {
58
+ delete (globalThis as Record<string, unknown>)[LOCAL_AGENT_GLOBAL_KEY]
59
+ const events: Array<{ type: string; message?: string }> = []
60
+ for await (const event of new LocalAgentAdapter().chatStream(options())) {
61
+ events.push(event as never)
62
+ }
63
+ expect(events).toHaveLength(1)
64
+ expect(events[0]?.type).toBe('RUN_ERROR')
65
+ expect(events[0]?.message).toContain('本地 Agent 不可用')
66
+ })
67
+
68
+ it('转发 pi 的 AG-UI 事件,并为 RUN_FINISHED 补 finishReason=stop', async () => {
69
+ const received: unknown[] = []
70
+ installService(async function* (input) {
71
+ received.push(input)
72
+ yield { type: 'RUN_STARTED', threadId: 'thread-1', runId: 'run-1' }
73
+ yield { type: 'TEXT_MESSAGE_START', messageId: 'm1', role: 'assistant' }
74
+ yield { type: 'TEXT_MESSAGE_CONTENT', messageId: 'm1', delta: '你好' }
75
+ yield { type: 'TEXT_MESSAGE_END', messageId: 'm1' }
76
+ yield { type: 'RUN_FINISHED', threadId: 'thread-1', runId: 'run-1' }
77
+ })
78
+
79
+ const events: Array<Record<string, unknown>> = []
80
+ for await (const event of new LocalAgentAdapter().chatStream(options())) {
81
+ events.push(event as never)
82
+ }
83
+
84
+ expect(received[0]).toMatchObject({
85
+ cwd: '/tmp/workspace',
86
+ sessionId: 'thread-1',
87
+ threadId: 'thread-1',
88
+ runId: 'run-1',
89
+ })
90
+ expect(events.map((event) => event.type)).toEqual([
91
+ 'RUN_STARTED',
92
+ 'TEXT_MESSAGE_START',
93
+ 'TEXT_MESSAGE_CONTENT',
94
+ 'TEXT_MESSAGE_END',
95
+ 'RUN_FINISHED',
96
+ ])
97
+ expect(events[4]).toMatchObject({ finishReason: 'stop', model: 'local-pi' })
98
+ })
99
+
100
+ it('缺少 cwd → RUN_ERROR', async () => {
101
+ installService(async function* () {})
102
+ const events: Array<{ type: string }> = []
103
+ for await (const event of new LocalAgentAdapter().chatStream(
104
+ options({ modelOptions: {} }),
105
+ )) {
106
+ events.push(event as never)
107
+ }
108
+ expect(events[0]?.type).toBe('RUN_ERROR')
109
+ })
110
+ })
@@ -0,0 +1,63 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-or-later
2
+ // AllBlue - 理想之海
3
+ // Copyright (C) 2026 AllBlue Contributors
4
+
5
+ import { afterEach, describe, expect, it } from 'vitest'
6
+ import { resolveUnifiedAiRoute } from '../unified-router'
7
+ import { LOCAL_AGENT_GLOBAL_KEY } from '../local-agent-adapter'
8
+
9
+ const originalTarget = process.env.DEPLOY_TARGET
10
+ const originalService = (globalThis as Record<string, unknown>)[LOCAL_AGENT_GLOBAL_KEY]
11
+
12
+ afterEach(() => {
13
+ if (originalTarget === undefined) delete process.env.DEPLOY_TARGET
14
+ else process.env.DEPLOY_TARGET = originalTarget
15
+ if (originalService === undefined) delete (globalThis as Record<string, unknown>)[LOCAL_AGENT_GLOBAL_KEY]
16
+ else (globalThis as Record<string, unknown>)[LOCAL_AGENT_GLOBAL_KEY] = originalService
17
+ })
18
+
19
+ const baseRequest = {
20
+ userId: 'local-guest',
21
+ messages: [{ role: 'user' as const, content: '看看这个目录' }],
22
+ threadId: 'thread-local-1',
23
+ engine: 'local' as const,
24
+ localCwd: '/tmp/work',
25
+ }
26
+
27
+ describe('resolveUnifiedAiRoute — 本地 Agent 引擎', () => {
28
+ it('非桌面部署 → 拒绝(云端不能走本地文件)', async () => {
29
+ process.env.DEPLOY_TARGET = 'web'
30
+ await expect(resolveUnifiedAiRoute(baseRequest)).rejects.toThrow(/仅在桌面客户端可用/)
31
+ })
32
+
33
+ it('桌面但未装配运行时 → 明确报错而不是回落 BYOK', async () => {
34
+ process.env.DEPLOY_TARGET = 'desktop'
35
+ delete (globalThis as Record<string, unknown>)[LOCAL_AGENT_GLOBAL_KEY]
36
+ await expect(resolveUnifiedAiRoute(baseRequest)).rejects.toThrow(/运行时未装配/)
37
+ })
38
+
39
+ it('桌面 + 运行时 + cwd → 装配 local 路由(adapter 由声明式 harness 包装)', async () => {
40
+ process.env.DEPLOY_TARGET = 'desktop'
41
+ ;(globalThis as Record<string, unknown>)[LOCAL_AGENT_GLOBAL_KEY] = {
42
+ runLocalAgentPrompt: async function* () {},
43
+ }
44
+ const route = await resolveUnifiedAiRoute(baseRequest)
45
+ expect(route.kind).toBe('local')
46
+ if (route.kind === 'local') {
47
+ // 只需确认可迭代对象已装配;真正的事件映射在 local-agent-adapter 单测覆盖
48
+ expect(typeof route.stream[Symbol.asyncIterator]).toBe('function')
49
+ }
50
+ })
51
+
52
+ it('engine 缺省时不走本地链路(保持云端行为)', async () => {
53
+ process.env.DEPLOY_TARGET = 'desktop'
54
+ ;(globalThis as Record<string, unknown>)[LOCAL_AGENT_GLOBAL_KEY] = {
55
+ runLocalAgentPrompt: async function* () {},
56
+ }
57
+ // 没有 engine=local:会进入 BYOK/hosted 解析(测试环境无 provider → NoAiAvailableError),
58
+ // 关键断言是错误不是「本地 Agent」相关,证明没有误走本地分支
59
+ await expect(
60
+ resolveUnifiedAiRoute({ ...baseRequest, engine: undefined }),
61
+ ).rejects.not.toThrow(/本地 Agent/)
62
+ })
63
+ })