@allbluecn/web-app 0.19.5 → 0.21.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 (235) hide show
  1. package/package.json +3 -3
  2. package/src/components/bug/__tests__/board-subgroups.test.tsx +1 -1
  3. package/src/components/bug/__tests__/board-view.test.tsx +1 -1
  4. package/src/components/bug/__tests__/comment-reactions.test.tsx +1 -1
  5. package/src/components/bug/__tests__/create-bug-dialog.test.tsx +2 -2
  6. package/src/components/bug/__tests__/filters-compat.test.tsx +1 -1
  7. package/src/components/bug/__tests__/fix-request-race.test.tsx +1 -1
  8. package/src/components/bug/__tests__/peek-modes.test.tsx +1 -1
  9. package/src/components/bug/__tests__/use-paged-bug-groups.test.tsx +1 -1
  10. package/src/components/bug/__tests__/view-type.test.tsx +1 -1
  11. package/src/components/bug/bug-list-view.tsx +2 -2
  12. package/src/components/bug/bug-toolbar.tsx +1 -1
  13. package/src/components/bug/create-bug-dialog.tsx +3 -3
  14. package/src/components/bug/detail/__tests__/activity-timeline.test.tsx +1 -1
  15. package/src/components/bug/detail/__tests__/bug-detail-collab.test.tsx +1 -1
  16. package/src/components/bug/detail/__tests__/repro-report-card.test.tsx +1 -1
  17. package/src/components/bug/detail/activity-tabs.tsx +34 -3
  18. package/src/components/bug/detail/activity-timeline.tsx +1 -1
  19. package/src/components/bug/detail/bug-detail-body.tsx +3 -3
  20. package/src/components/bug/detail/comment-input.tsx +1 -1
  21. package/src/components/bug/detail/comment-item.tsx +1 -1
  22. package/src/components/bug/detail/comment-reactions.tsx +1 -1
  23. package/src/components/bug/detail/comment-thread.tsx +1 -1
  24. package/src/components/bug/detail/props-section.tsx +2 -2
  25. package/src/components/bug/detail/repro-report-card.tsx +1 -1
  26. package/src/components/bug/detail/reproduce-steps-view.tsx +1 -1
  27. package/src/components/bug/display/use-paged-bug-groups.ts +2 -2
  28. package/src/components/bug/filters/field-configs.tsx +1 -1
  29. package/src/components/bug/inline-editors/assignee-editor.tsx +1 -1
  30. package/src/components/bug/peek-panel.tsx +2 -2
  31. package/src/components/bug/reproduce-steps-editor.tsx +1 -1
  32. package/src/components/bug/story-bugs-panel.tsx +10 -6
  33. package/src/components/bug/views/board-view.tsx +2 -2
  34. package/src/components/bug/views/bug-list/batch-toolbar.tsx +1 -1
  35. package/src/components/bug/views/bug-list/group.tsx +1 -1
  36. package/src/components/bug/views/bug-list/index.tsx +1 -1
  37. package/src/components/bug/views/bug-list/inline-bug-creator.tsx +1 -1
  38. package/src/components/bug/views/bug-list/row.tsx +2 -2
  39. package/src/components/bug/views/create-view-dialog.tsx +1 -1
  40. package/src/components/design/editor/versions/hooks/use-design-versions.ts +11 -4
  41. package/src/components/design/editor/versions/page-version-panel.tsx +30 -11
  42. package/src/components/harness/code/workspace-context.tsx +5 -1
  43. package/src/components/harness/code/workspace-picker.tsx +19 -6
  44. package/src/components/harness/composer/harness-composer.tsx +25 -3
  45. package/src/components/harness/composer/mention/mention-resolvers.ts +2 -0
  46. package/src/components/harness/composer/mention/mention-suggestion-menu.tsx +6 -3
  47. package/src/components/harness/composer/mention/use-mention-sources.ts +1 -1
  48. package/src/components/iteration/iteration-progress-bar.tsx +2 -2
  49. package/src/components/knowledge/public/legal-compliance-editor.tsx +1 -1
  50. package/src/components/layout/sidebar-03/nav-knowledge.tsx +1 -1
  51. package/src/components/layout/sidebar-03/nav-workspace.tsx +1 -1
  52. package/src/components/project/invite-member-dialog.tsx +2 -2
  53. package/src/components/project/project-member-manager.tsx +0 -3
  54. package/src/components/shared-kit/list-kit/kanban/__tests__/instance-key.test.ts +1 -1
  55. package/src/components/story/__tests__/constants.test.ts +1 -1
  56. package/src/components/story/__tests__/dependency-type-badge.test.ts +1 -1
  57. package/src/components/story/__tests__/peek-panel-patch-sync.test.tsx +1 -1
  58. package/src/components/story/__tests__/story-list-view-peek-sync.test.tsx +1 -1
  59. package/src/components/story/ai/story-ai-panel.tsx +1 -1
  60. package/src/components/story/ai/story-ai-preview-list.tsx +2 -2
  61. package/src/components/story/ai/use-story-ai-model.ts +17 -4
  62. package/src/components/story/columns.tsx +2 -2
  63. package/src/components/story/create-story-dialog.tsx +4 -4
  64. package/src/components/story/detail/__tests__/activity-timeline.test.tsx +1 -1
  65. package/src/components/story/detail/__tests__/associated-attachments-panel.test.tsx +1 -1
  66. package/src/components/story/detail/__tests__/story-collab.test.tsx +1 -1
  67. package/src/components/story/detail/activity-tabs.tsx +1 -1
  68. package/src/components/story/detail/activity-timeline.tsx +36 -17
  69. package/src/components/story/detail/comment-input.tsx +1 -1
  70. package/src/components/story/detail/comment-item.tsx +1 -1
  71. package/src/components/story/detail/comment-thread.tsx +1 -1
  72. package/src/components/story/detail/reaction-bar.tsx +1 -1
  73. package/src/components/story/detail/story-detail-body.tsx +4 -4
  74. package/src/components/story/detail/story-detail-toolbar/__tests__/attachment-cache.test.ts +83 -0
  75. package/src/components/story/detail/story-detail-toolbar/associated-attachments-panel.tsx +50 -9
  76. package/src/components/story/detail/story-detail-toolbar/children.tsx +3 -3
  77. package/src/components/story/detail/story-detail-toolbar/dependency-panel.tsx +9 -8
  78. package/src/components/story/detail/story-detail-toolbar/index.tsx +8 -8
  79. package/src/components/story/detail/story-detail-toolbar/related-test-cases.tsx +2 -2
  80. package/src/components/story/detail/story-properties.tsx +2 -2
  81. package/src/components/story/display/__tests__/flatten-story-groups.test.ts +1 -1
  82. package/src/components/story/display/__tests__/group-fields.test.ts +1 -1
  83. package/src/components/story/display/__tests__/group-resolver.test.ts +2 -2
  84. package/src/components/story/display/__tests__/order-resolver.test.ts +1 -1
  85. package/src/components/story/display/__tests__/use-grouped-stories.test.ts +1 -1
  86. package/src/components/story/display/__tests__/use-paged-story-groups.test.ts +1 -1
  87. package/src/components/story/display/flatten-story-groups.ts +1 -1
  88. package/src/components/story/display/group-fields.ts +2 -2
  89. package/src/components/story/display/group-resolver.tsx +4 -4
  90. package/src/components/story/display/order-resolver.ts +3 -3
  91. package/src/components/story/display/types.ts +5 -4
  92. package/src/components/story/display/use-grouped-stories.ts +5 -5
  93. package/src/components/story/display/use-paged-story-groups.ts +11 -4
  94. package/src/components/story/entity-overlay.ts +24 -0
  95. package/src/components/story/filters/field-configs.tsx +2 -2
  96. package/src/components/story/inline-editors/assignee-editor.tsx +1 -1
  97. package/src/components/story/inline-editors/dependency-type-select.tsx +1 -1
  98. package/src/components/story/inline-editors/parent-story-editor.tsx +2 -2
  99. package/src/components/story/inline-editors/priority-editor.tsx +2 -2
  100. package/src/components/story/inline-editors/state-editor.tsx +2 -2
  101. package/src/components/story/inline-editors/story-select-dialog.tsx +2 -2
  102. package/src/components/story/peek-panel.tsx +6 -5
  103. package/src/components/story/priority-icon.tsx +2 -2
  104. package/src/components/story/relations/__tests__/attachment-manage-dialog.test.tsx +1 -1
  105. package/src/components/story/relations/__tests__/design-select-dialog.test.tsx +1 -1
  106. package/src/components/story/relations/__tests__/knowledge-select-dialog.test.tsx +1 -1
  107. package/src/components/story/relations/__tests__/link-manage-dialog.test.tsx +1 -1
  108. package/src/components/story/relations/attachment-manage-dialog.tsx +4 -4
  109. package/src/components/story/relations/design-select-dialog.tsx +2 -2
  110. package/src/components/story/relations/knowledge-select-dialog.tsx +2 -2
  111. package/src/components/story/relations/link-manage-dialog.tsx +2 -2
  112. package/src/components/story/relations/story-ref-row.tsx +3 -3
  113. package/src/components/story/story-list-table.tsx +1 -1
  114. package/src/components/story/story-list-view.tsx +1 -1
  115. package/src/components/story/story-toolbar.tsx +1 -1
  116. package/src/components/story/transfer-story-popover.tsx +3 -2
  117. package/src/components/story/views/__tests__/calendar-view.test.tsx +1 -1
  118. package/src/components/story/views/__tests__/kanban-drop-subgroup.test.ts +1 -1
  119. package/src/components/story/views/__tests__/kanban-peek-sync.test.tsx +2 -2
  120. package/src/components/story/views/__tests__/kanban-view-membership-sync.test.tsx +1 -1
  121. package/src/components/story/views/__tests__/timeline-view.test.tsx +1 -1
  122. package/src/components/story/views/calendar/__tests__/mappers.test.ts +1 -1
  123. package/src/components/story/views/calendar/mappers.ts +1 -1
  124. package/src/components/story/views/calendar-view.tsx +1 -1
  125. package/src/components/story/views/create-view-dialog.tsx +1 -1
  126. package/src/components/story/views/kanban-view.tsx +2 -2
  127. package/src/components/story/views/story-list/batch-preview-dialog.tsx +2 -2
  128. package/src/components/story/views/story-list/batch-toolbar.tsx +3 -3
  129. package/src/components/story/views/story-list/group.tsx +18 -5
  130. package/src/components/story/views/story-list/index.tsx +4 -2
  131. package/src/components/story/views/story-list/inline-story-creator.tsx +2 -2
  132. package/src/components/story/views/story-list/row.tsx +2 -2
  133. package/src/components/story/views/story-list/use-flat-rows.ts +1 -1
  134. package/src/components/story/views/timeline/__tests__/mappers.test.ts +2 -2
  135. package/src/components/story/views/timeline/mappers.ts +2 -2
  136. package/src/components/story/views/timeline/use-date-commit.ts +1 -1
  137. package/src/components/story/views/timeline-view.tsx +2 -2
  138. package/src/components/testcase/__tests__/case-row.test.tsx +1 -1
  139. package/src/components/testcase/__tests__/create-dialog-scope.test.tsx +1 -1
  140. package/src/components/testcase/__tests__/display-options.test.tsx +1 -1
  141. package/src/components/testcase/__tests__/export-entry.test.tsx +1 -1
  142. package/src/components/testcase/__tests__/list-view.test.tsx +1 -1
  143. package/src/components/testcase/__tests__/step-editor.test.tsx +4 -2
  144. package/src/components/testcase/__tests__/toolbar.test.tsx +1 -1
  145. package/src/components/testcase/create-testcase-dialog.tsx +6 -5
  146. package/src/components/testcase/detail/case-detail-body.tsx +1 -1
  147. package/src/components/testcase/detail/properties.tsx +1 -1
  148. package/src/components/testcase/display/display-dropdown.tsx +2 -2
  149. package/src/components/testcase/display/use-testcase-display-options.ts +1 -1
  150. package/src/components/testcase/filters/field-configs.tsx +1 -1
  151. package/src/components/testcase/generation/draft-card.tsx +2 -2
  152. package/src/components/testcase/inline-editors/assignee-editor.tsx +1 -1
  153. package/src/components/testcase/list/case-row.tsx +1 -1
  154. package/src/components/testcase/list/grouped-list.tsx +1 -1
  155. package/src/components/testcase/list-view.tsx +2 -2
  156. package/src/components/testcase/runs/convert-bug-button.tsx +2 -2
  157. package/src/components/testcase/step-editor.tsx +13 -2
  158. package/src/components/testcase/testcase-toolbar.tsx +2 -2
  159. package/src/components/testcase/views/batch-toolbar.tsx +1 -1
  160. package/src/components/testcase/views/board-view.tsx +2 -2
  161. package/src/lib/changelog/sdk-versions.ts +2 -2
  162. package/src/lib/collections/knowledge/knowledge-bases-collection.ts +2 -1
  163. package/src/lib/collections/query-keys.ts +18 -0
  164. package/src/lib/collections/reference/users-brief-collection.ts +2 -1
  165. package/src/lib/collections/reference/workspaces-collection.ts +2 -1
  166. package/src/lib/story/__tests__/activity-grouper.test.ts +1 -1
  167. package/src/lib/story/activity-grouper.ts +1 -1
  168. package/src/paraglide/messages/_index.d.ts +2 -0
  169. package/src/paraglide/messages/_index.js +88269 -5642
  170. package/src/paraglide/messages/en.js +28255 -0
  171. package/src/paraglide/messages/story_group_loadfailed1.d.ts +17 -0
  172. package/src/paraglide/messages/story_group_loadfailed1.js +30 -0
  173. package/src/paraglide/messages/story_group_retry.d.ts +16 -0
  174. package/src/paraglide/messages/story_group_retry.js +29 -0
  175. package/src/paraglide/messages/zh.js +28253 -0
  176. package/src/routes/_nav/bugs/$bugId/route.lazy.tsx +2 -2
  177. package/src/routes/_nav/projects/$projectId/iterations/$iterationId/route.lazy.tsx +1 -1
  178. package/src/routes/_nav/route.tsx +1 -2
  179. package/src/routes/_nav/stories/$storyId/route.lazy.tsx +2 -2
  180. package/src/routes/_nav/stories/$storyId/route.tsx +1 -1
  181. package/src/routes/api/design/prototype/__tests__/route.test.ts +4 -1
  182. package/src/server/__tests__/task-executor.test.ts +7 -0
  183. package/src/server/ai-gateway/__tests__/ai-output-guard.test.ts +4 -4
  184. package/src/server/ai-gateway/__tests__/tools-authorization.test.ts +25 -0
  185. package/src/server/ai-gateway/tools/server.ts +24 -15
  186. package/src/server/knowledge/reindex-guard.ts +35 -0
  187. package/src/server/lib/__tests__/cron-auth.test.ts +1 -1
  188. package/src/server/serverFns/__tests__/legal-code-canonical.test.ts +169 -0
  189. package/src/server/serverFns/__tests__/story-tree.test.ts +0 -1
  190. package/src/server/serverFns/__tests__/story-types.test.ts +2 -1
  191. package/src/server/serverFns/__tests__/update-bug-guard.test.ts +1 -1
  192. package/src/server/serverFns/bug/__tests__/bug-comments.test.ts +95 -44
  193. package/src/server/serverFns/bug/__tests__/bug-crud.test.ts +46 -18
  194. package/src/server/serverFns/bug/__tests__/bug-groups.test.ts +1 -1
  195. package/src/server/serverFns/bug/bug-activities.ts +2 -2
  196. package/src/server/serverFns/bug/bug-comments.ts +44 -19
  197. package/src/server/serverFns/bug/bug-crud/detail-select.ts +1 -1
  198. package/src/server/serverFns/bug/bug-crud/detail.ts +2 -2
  199. package/src/server/serverFns/bug/bug-crud.ts +142 -110
  200. package/src/server/serverFns/bug/bug-group-query.ts +1 -1
  201. package/src/server/serverFns/bug/bug-groups.ts +2 -2
  202. package/src/server/serverFns/bug/bug-story-utils.ts +1 -1
  203. package/src/server/serverFns/chat/conversations.ts +27 -13
  204. package/src/server/serverFns/harness/github-workspace.ts +32 -25
  205. package/src/server/serverFns/harness/search-mention-candidates.ts +3 -1
  206. package/src/server/serverFns/iteration/iteration-crud.ts +1 -1
  207. package/src/server/serverFns/knowledge/public/legal/create.ts +3 -0
  208. package/src/server/serverFns/knowledge/public/legal/update.ts +8 -8
  209. package/src/server/serverFns/knowledge-index.ts +12 -5
  210. package/src/server/serverFns/project.ts +5 -11
  211. package/src/server/serverFns/story/__tests__/story-dependencies.test.ts +15 -4
  212. package/src/server/serverFns/story/__tests__/story-transfer.test.ts +20 -0
  213. package/src/server/serverFns/story/link-preview.ts +3 -2
  214. package/src/server/serverFns/story/story-children.ts +1 -1
  215. package/src/server/serverFns/story/story-comments.ts +1 -1
  216. package/src/server/serverFns/story/story-crud/bulk.ts +1 -1
  217. package/src/server/serverFns/story/story-crud.ts +1 -1
  218. package/src/server/serverFns/story/story-dependencies.ts +61 -69
  219. package/src/server/serverFns/story/story-groups.ts +1 -1
  220. package/src/server/serverFns/story/story-transfer.ts +8 -20
  221. package/src/server/serverFns/testcase/__tests__/automation.test.ts +21 -0
  222. package/src/server/serverFns/testcase/__tests__/testcase-groups.test.ts +1 -1
  223. package/src/server/serverFns/testcase/automation.ts +24 -1
  224. package/src/server/serverFns/testcase/testcase-board.ts +1 -1
  225. package/src/server/serverFns/testcase/testcase-crud.ts +1 -1
  226. package/src/server/serverFns/testcase/testcase-display-options.ts +1 -1
  227. package/src/server/serverFns/testcase/testcase-filter-views.ts +1 -1
  228. package/src/server/serverFns/testcase/types.ts +1 -1
  229. package/src/server/story/types.ts +7 -3
  230. package/src/shared/bug/__tests__/constants.test.ts +1 -1
  231. package/src/{components → shared}/bug/types.ts +4 -3
  232. package/src/{components → shared}/story/constants.ts +1 -1
  233. package/src/{components → shared}/story/types.ts +1 -7
  234. package/src/types/project.ts +1 -2
  235. /package/src/{components/testcase/display → shared/testcase}/field-configs.ts +0 -0
@@ -15,7 +15,7 @@
15
15
  // You should have received a copy of the GNU Affero General Public License
16
16
  // along with this program. If not, see <https://www.gnu.org/licenses/>.
17
17
 
18
- import React, { useCallback, useEffect, useState } from "react";
18
+ import React, { useCallback, useEffect, useRef, useState } from "react";
19
19
  import { m } from '@/paraglide/messages.js'
20
20
  import { Button } from "@/components/ui/button";
21
21
  import { Input } from "@/components/ui/input";
@@ -27,7 +27,7 @@ import { PageVersionTree, type VersionSnapshot } from "./page-version-tree";
27
27
  import { RestoreVersionDialog } from "./restore-version-dialog";
28
28
  import { Plus } from "lucide-react";
29
29
  import { cn } from "@/lib/utils";
30
- import { type DesignVersionWithSemver as DesignVersion, type DesignVersionWithPages } from "@allbluecn/kernel";
30
+ import { type DesignVersionFull } from "@allbluecn/kernel";
31
31
  import { SUBTABS } from "./constants";
32
32
  import { type PageVersionPanelProps, type PageVersion } from "./types";
33
33
  import { compareDesignVersionDesc } from "./lib/semver";
@@ -120,6 +120,11 @@ export function PageVersionPanel({ designId, pageId }: PageVersionPanelProps) {
120
120
  const [selectedPageVersionNo, setSelectedPageVersionNo] = useState<Map<string, number>>(new Map());
121
121
  const [allPageVersionsByPage, setAllPageVersionsByPage] = useState<Map<string, { version: number; notes: string | null; createdAt: string }[]>>(new Map());
122
122
 
123
+ // S31(审计 A7x-7 ⚪ 勾选风暴治理):版本选项 per-page 缓存——勾选变化只拉
124
+ // 增量新页面,已加载页不再重复请求。失效策略:designId 切换全量失效;发布
125
+ // 成功后由 executePublish 清缓存(新版本产生使已缓存列表失真)。
126
+ const pageVersionsCacheRef = useRef<{ designId: string; map: Map<string, PageVersion[]> }>({ designId: "", map: new Map() });
127
+
123
128
  const [confirmUpdateOpen, setConfirmUpdateOpen] = useState(false);
124
129
  const [confirmSkipOpen, setConfirmSkipOpen] = useState(false);
125
130
  const [skipVersionInfo, setSkipVersionInfo] = useState<{ current: string; previous: string } | null>(null);
@@ -130,8 +135,8 @@ export function PageVersionPanel({ designId, pageId }: PageVersionPanelProps) {
130
135
  selectedPageVersionsObj[pid] = versionNo;
131
136
  }
132
137
  try {
133
- // serverFn 返回 prisma DesignVersion 行(pageVersions 为 Json),与 kernel
134
- // DesignVersionWithSemver 的 pageVersions 类型差异需跨包对齐,此处断言收敛
138
+ // S32:kernel DesignVersionFull 即返回行契约;pageVersions 为 Json 列,
139
+ // JsonValue→结构化形状不可比较(TS 限制),边界断言收敛于此
135
140
  const version = (await createDesignVersionFn({
136
141
  data: {
137
142
  designId,
@@ -142,8 +147,10 @@ export function PageVersionPanel({ designId, pageId }: PageVersionPanelProps) {
142
147
  pageIds: Array.from(selectedPageIds),
143
148
  selectedPageVersions: selectedPageVersionsObj,
144
149
  },
145
- })) as unknown as DesignVersion;
150
+ })) as unknown as DesignVersionFull;
146
151
  upsertVersion(version);
152
+ // S31:发布产生新版本,受影响页面的版本选项缓存失效
153
+ pageVersionsCacheRef.current.map.clear();
147
154
  setNotes("");
148
155
  } catch (err) {
149
156
  console.error(err instanceof Error ? err.message : "unknown");
@@ -188,24 +195,36 @@ export function PageVersionPanel({ designId, pageId }: PageVersionPanelProps) {
188
195
  // Fetch page version options for all leaf pages when publish tab is active
189
196
  useEffect(() => {
190
197
  if (!isPublishTab) return;
198
+ const cache = pageVersionsCacheRef.current;
199
+ if (cache.designId !== designId) {
200
+ cache.designId = designId;
201
+ cache.map = new Map();
202
+ }
191
203
  const allPageIds = getAllPageIds(tree);
192
204
  const leafIds = allPageIds.filter((id) => selectedPageIds.has(id));
193
205
  if (leafIds.length === 0) return;
206
+ // S31:只拉缓存未命中的页面(勾选风暴治理——勾选增量不再全量重拉)
207
+ const uncachedIds = leafIds.filter((id) => !cache.map.has(id));
208
+ if (uncachedIds.length === 0) {
209
+ setAllPageVersionsByPage(new Map(leafIds.map((id) => [id, cache.map.get(id) ?? []])));
210
+ return;
211
+ }
194
212
  let cancelled = false; // 过期响应守卫:快速变更选择时旧批次不得覆盖
195
213
  const fetchVersions = async () => {
196
214
  try {
197
215
  const results = await Promise.all(
198
- leafIds.map(async (pid) => {
216
+ uncachedIds.map(async (pid) => {
199
217
  const versions = await listDesignPageVersionsFn({ data: { pageId: pid, limit: 100 } });
200
218
  return { pageId: pid, versions };
201
219
  })
202
220
  );
203
221
  if (cancelled) return;
204
- const map = new Map<string, PageVersion[]>();
205
222
  for (const r of results) {
206
- if (r) map.set(r.pageId, r.versions.map((v) => ({ ...v, createdAt: v.createdAt.toISOString() })));
223
+ if (r) {
224
+ cache.map.set(r.pageId, r.versions.map((v) => ({ ...v, createdAt: v.createdAt.toISOString() })));
225
+ }
207
226
  }
208
- setAllPageVersionsByPage(map);
227
+ setAllPageVersionsByPage(new Map(leafIds.map((id) => [id, cache.map.get(id) ?? []])));
209
228
  setSelectedPageVersionNo((prev) => {
210
229
  const next = new Map(prev);
211
230
  for (const r of results) {
@@ -223,7 +242,7 @@ export function PageVersionPanel({ designId, pageId }: PageVersionPanelProps) {
223
242
  };
224
243
  fetchVersions();
225
244
  return () => { cancelled = true; };
226
- }, [isPublishTab, selectedPageIds, tree]);
245
+ }, [isPublishTab, selectedPageIds, tree, designId]);
227
246
 
228
247
  return (
229
248
  <div className="flex flex-col h-full">
@@ -398,7 +417,7 @@ export function PageVersionPanel({ designId, pageId }: PageVersionPanelProps) {
398
417
  </div>
399
418
  ) : (
400
419
  <PageVersionTree
401
- versions={versions as unknown as DesignVersionWithPages[]}
420
+ versions={versions}
402
421
  onRestore={async (snapshot: VersionSnapshot) => {
403
422
  let targetCanvas = snapshot.canvas;
404
423
  let targetContent = snapshot.content;
@@ -18,9 +18,13 @@
18
18
 
19
19
  import { createContext, useContext, useState, type ReactNode } from "react"
20
20
 
21
+ // S27:WorkspaceSource 单一权威定义(此前 workspace-picker 另有一份同名异构:
22
+ // local-fsap 丢 dirHandle、缺 local/cloud 变体,经 onChange 流入 context 即残缺)。
23
+ // FSAP 的 dirHandle 仅存客户端(不可序列化),path 为目录展示名。
21
24
  export type WorkspaceSource =
22
25
  | { type: "cloud"; id: string }
23
- | { type: "local-fsap"; dirHandle: FileSystemDirectoryHandle }
26
+ | { type: "local"; path: string } // Tauri 本地路径
27
+ | { type: "local-fsap"; dirHandle: FileSystemDirectoryHandle; path: string }
24
28
  | { type: "github"; owner: string; repo: string; branch: string }
25
29
 
26
30
  interface WorkspaceContextValue {
@@ -26,17 +26,16 @@ import {
26
26
  SelectTrigger,
27
27
  SelectValue,
28
28
  } from "@/components/ui/select"
29
- import { listGithubReposFn, listGithubBranchesFn } from "@/server/serverFns/harness/github-workspace"
29
+ import { listGithubReposFn, listGithubBranchesFn, getGithubConnectionFn } from "@/server/serverFns/harness/github-workspace"
30
30
  import { useQuery } from "@tanstack/react-query"
31
31
  import { m } from "@/paraglide/messages.js"
32
32
  import { isDesktop, getTauriInvoke } from "@/lib/desktop-detect"
33
33
  import { toast } from "@/components/ui/toast"
34
34
  import { hasFileSystemAccess, pickLocalDirectory } from "@/lib/fsap"
35
35
 
36
- export type WorkspaceSource =
37
- | { type: "local"; path: string } // Tauri 路径
38
- | { type: "local-fsap"; path: string } // Chromium FSAP(dirHandle 不进服务端请求)
39
- | { type: "github"; owner: string; repo: string; branch: string }
36
+ // S27:类型从 workspace-context 单一来源导入(删除同名异构副本)
37
+ import type { WorkspaceSource } from "./workspace-context";
38
+
40
39
 
41
40
  interface WorkspacePickerProps {
42
41
  value?: WorkspaceSource
@@ -64,6 +63,17 @@ export function WorkspacePicker({ value, onChange }: WorkspacePickerProps) {
64
63
  enabled: mode === "github",
65
64
  })
66
65
 
66
+ // S27:连接状态显式取自 serverFn(token 不出服务端),未连接给可操作提示而非空列表
67
+ const { data: connection } = useQuery({
68
+ queryKey: ["github-connection"],
69
+ queryFn: async () => {
70
+ const result = await getGithubConnectionFn()
71
+ return result as { connected: boolean; source: "saved" | "session" | null }
72
+ },
73
+ enabled: mode === "github",
74
+ staleTime: 30_000,
75
+ })
76
+
67
77
  const selectedRepo = repos?.repos.find((r) => {
68
78
  if (value?.type !== "github") return false
69
79
  return r.owner === value.owner && r.name === value.repo
@@ -95,7 +105,8 @@ export function WorkspacePicker({ value, onChange }: WorkspacePickerProps) {
95
105
  } else if (hasFileSystemAccess()) {
96
106
  const handle = await pickLocalDirectory()
97
107
  if (handle) {
98
- onChange({ type: "local-fsap", path: handle.name })
108
+ // S27:dirHandle 必须随选入 context(此前仅传 name,文件树拿到 undefined 句柄)
109
+ onChange({ type: "local-fsap", dirHandle: handle, path: handle.name })
99
110
  }
100
111
  } else {
101
112
  // Firefox/Safari 降级:提示上传到云工作区
@@ -167,6 +178,8 @@ export function WorkspacePicker({ value, onChange }: WorkspacePickerProps) {
167
178
  <label className="text-sm font-medium">{m.chat_workspace_selectRepo()}</label>
168
179
  {loadingRepos ? (
169
180
  <Loader2 className="size-4 animate-spin" />
181
+ ) : connection && !connection.connected ? (
182
+ <p className="text-xs text-muted-foreground">{m.chat_workspace_githubNotConnected()}</p>
170
183
  ) : (
171
184
  <Select
172
185
  value={selectedRepo?.full_name}
@@ -48,6 +48,7 @@ export interface MentionCandidate {
48
48
  label: string
49
49
  id: string
50
50
  kbId?: string
51
+ kbName?: string
51
52
  }
52
53
 
53
54
  export interface PrefillMentionEntity {
@@ -241,7 +242,7 @@ export function HarnessComposer({
241
242
  })
242
243
 
243
244
  const handleSend = useCallback(
244
- (text: string) => {
245
+ (text: string, whenBusy: "queue" | "interrupt" = "queue") => {
245
246
  if (!text.trim() || !hasAiKey) return
246
247
 
247
248
  if (mediaRoute === "spec") {
@@ -277,7 +278,7 @@ export function HarnessComposer({
277
278
  const unsentNonImage = attachments.some((a) => !(a.status === "ready" && a.dataUrl))
278
279
 
279
280
  onSend(text, {
280
- whenBusy: "queue",
281
+ whenBusy,
281
282
  knowledgeBaseIds: mergedKbIds.length ? mergedKbIds : undefined,
282
283
  attachments: readyImages.length ? readyImages : undefined,
283
284
  // 透传板块上下文(如果存在),服务端可据此注入系统提示词
@@ -336,9 +337,30 @@ export function HarnessComposer({
336
337
  className="px-3 pt-3"
337
338
  onKeyDownCapture={(event) => {
338
339
  if (event.key !== "Enter" || event.shiftKey || event.nativeEvent.isComposing) return
340
+ // IME 三保险(与 useComposerSubmit 同口径):Safari compositionend 后窗口 + keyCode 229 + 按住不连发
341
+ if (event.keyCode === 229) return
342
+ if (event.repeat) return
339
343
  if (event.target instanceof HTMLElement && !event.target.isContentEditable && event.target.getAttribute("contenteditable") !== "true") return
344
+ // S30(审计 A7x-8 待决策):修饰键进渲染期决策机——busy 时 Ctrl/Cmd+Enter
345
+ // 翻转 queue/steer 偏好提交;steerQueue 分支(空文本打断重排)待上游
346
+ // steer API(composer props 尚无),暂按 blocked 静默
347
+ const decision = decideSubmit({
348
+ text: editorRef.current?.getText() ?? "",
349
+ hasUploadingAttachments: hasUploading,
350
+ isRunning,
351
+ busyPreference,
352
+ modifierKey: event.metaKey || event.ctrlKey,
353
+ composerLocked: false,
354
+ })
355
+ if (decision.action === "blocked" || decision.action === "steerQueue") {
356
+ event.preventDefault()
357
+ return
358
+ }
340
359
  event.preventDefault()
341
- handleSend(editorRef.current?.getText() ?? "")
360
+ handleSend(
361
+ editorRef.current?.getText() ?? "",
362
+ decision.action === "steer" ? "interrupt" : decision.action === "queue" ? "queue" : "queue",
363
+ )
342
364
  }}
343
365
  >
344
366
  <ComposerEditor
@@ -5,6 +5,8 @@ export interface MentionCandidate {
5
5
  label: string
6
6
  id: string
7
7
  kbId?: string
8
+ /** 所属知识库名(kb-doc 专用,mention 菜单分组展示用,S29) */
9
+ kbName?: string
8
10
  }
9
11
 
10
12
  const GROUP_ORDER: MentionKind[] = ["file", "kb-doc", "design", "story", "plugin-tool", "thread"]
@@ -79,9 +79,12 @@ export function MentionSuggestionMenu(props: MentionSuggestionMenuProps) {
79
79
  .map((item) => ({
80
80
  title: item.label,
81
81
  context: { kind: item.kind, id: item.id, label: item.label, kbId: item.kbId },
82
- // 分组用稳定的 kind 语义标签(此前取「标题前 20 字符」,同一前缀才同组,
83
- // 分组语义近似随机且产生大量截断标题头)
84
- group: item.kind === "kb-doc" ? m.harness_mention_kb_doc() : m.harness_mention_prd(),
82
+ // 分组:kb-doc 按所属知识库名(S29),prd 用稳定 kind 语义标签;
83
+ // 无名兜底(存量会话/异常数据)回落 kind 标签
84
+ group:
85
+ item.kind === "kb-doc"
86
+ ? (item.kbName || m.harness_mention_kb_doc())
87
+ : m.harness_mention_prd(),
85
88
  onSelect: handleSelect,
86
89
  }))
87
90
  }
@@ -35,7 +35,7 @@ export function useMentionSources() {
35
35
 
36
36
  return useMemo(() => {
37
37
  const all: MentionCandidate[] = [
38
- ...kbDocs.map((d) => ({ kind: "kb-doc" as const, id: d.id, label: d.label, kbId: d.kbId })),
38
+ ...kbDocs.map((d) => ({ kind: "kb-doc" as const, id: d.id, label: d.label, kbId: d.kbId, kbName: d.kbName })),
39
39
  ...prds.map((p) => ({ kind: "design" as const, id: p.id, label: p.title })),
40
40
  ]
41
41
  // 透传加载态:首载/失败不再被硬编码 false 吞掉,mention 菜单可区分加载中
@@ -14,8 +14,8 @@
14
14
  //
15
15
  // You should have received a copy of the GNU Affero General Public License
16
16
  // along with this program. If not, see <https://www.gnu.org/licenses/>.
17
- import { STATE_COLOR } from "@/components/story/constants";
18
- import type { StoryState } from "@/components/story/constants";
17
+ import { STATE_COLOR } from "@/shared/story/constants";
18
+ import type { StoryState } from "@/shared/story/constants";
19
19
  import { m } from "@/paraglide/messages.js";
20
20
 
21
21
  const ORDER: StoryState[] = ["backlog", "todo", "in_progress", "done", "cancelled"];
@@ -87,7 +87,7 @@ function toDateStr(d: unknown): string {
87
87
  }
88
88
 
89
89
  function mapRecordToProperties(record: LegalOrComplianceDetail, type: EditorType): PropertyValues {
90
- // code 非数据库列(仅参与 canonicalId 计算),detail 接口不返回,历史行为为空串展示
90
+ // legal 的 code 已持久化(S7,detail 整行返回直接带出);compliance 仍无列,空串展示
91
91
  if (type === 'legal') {
92
92
  const r = record as LegalDetail & { code?: string }
93
93
  return {
@@ -70,7 +70,7 @@ import { cn } from "@/lib/utils";
70
70
  import { isParentActive } from "@/lib/sidebar-utils";
71
71
  import { useEffect, useState } from "react";
72
72
  import { useQuery, useQueryClient } from "@tanstack/react-query";
73
- import { KNOWLEDGE_BASES_QUERY_KEY } from "@/lib/collections/knowledge/knowledge-bases-collection";
73
+ import { KNOWLEDGE_BASES_QUERY_KEY } from "@/lib/collections/query-keys";
74
74
  import {
75
75
  listKnowledgeFn,
76
76
  toggleKnowledgePinFn,
@@ -31,7 +31,7 @@ import { cn } from "@/lib/utils";
31
31
  import { isParentActive } from "@/lib/sidebar-utils";
32
32
  import { LucideIconByName } from "@/components/shared-kit/icons/lucide-icon-picker/lucide-icon-by-name";
33
33
  import { CreateWorkspaceDialog } from "@/components/workspace/dialogs/create-workspace-dialog";
34
- import { WORKSPACES_QUERY_KEY } from "@/lib/collections/reference/workspaces-collection";
34
+ import { WORKSPACES_QUERY_KEY } from "@/lib/collections/query-keys";
35
35
  import {
36
36
  listPinnedWorkspacesBriefFn,
37
37
  toggleWorkspacePinFn,
@@ -95,9 +95,9 @@ export function InviteMemberDialog({
95
95
  },
96
96
  });
97
97
 
98
+ // S41 最小披露:项目成员搜索不按 email 过滤(team 管理域不受此限)
98
99
  const filteredMembers = members.filter((m) =>
99
- m.username.toLowerCase().includes(searchQuery.toLowerCase()) ||
100
- m.email?.toLowerCase().includes(searchQuery.toLowerCase())
100
+ m.username.toLowerCase().includes(searchQuery.toLowerCase())
101
101
  );
102
102
 
103
103
  const handleSubmit = async () => {
@@ -157,9 +157,6 @@ export function ProjectMemberManager({
157
157
  </Badge>
158
158
  )}
159
159
  </div>
160
- <p className="truncate text-sm text-muted-foreground">
161
- {member.user?.email}
162
- </p>
163
160
  </div>
164
161
  <span className="hidden text-xs text-muted-foreground sm:block">
165
162
  {new Intl.DateTimeFormat("zh-CN", { year: "numeric", month: "long", day: "numeric" }).format(new Date(member.joinedAt))}
@@ -23,7 +23,7 @@ import {
23
23
  itemKey,
24
24
  storyIdFromInstanceKey,
25
25
  } from "../instance-key";
26
- import type { StoryListItem } from "../../../../story/types";
26
+ import type { StoryListItem } from "@/shared/story/types";
27
27
 
28
28
  function story(id: string): StoryListItem {
29
29
  return { id } as StoryListItem;
@@ -31,7 +31,7 @@ import {
31
31
  toDependencyEdge,
32
32
  type StoryState,
33
33
  type StoryPriority,
34
- } from "../constants";
34
+ } from "@/shared/story/constants";
35
35
 
36
36
  describe("STORY_STATES", () => {
37
37
  it("包含 5 个状态", () => {
@@ -4,7 +4,7 @@ import {
4
4
  dependencyTypeBadgeKey,
5
5
  dependencyTypeOption,
6
6
  toDependencyEdge,
7
- } from "../constants";
7
+ } from "@/shared/story/constants";
8
8
 
9
9
  describe("DEPENDENCY_TYPE_OPTIONS", () => {
10
10
  it("共 6 项,key 唯一,每组各 3 项", () => {
@@ -91,7 +91,7 @@ vi.mock("../detail/story-detail-body", () => ({
91
91
  }));
92
92
 
93
93
  import { StoryPeekPanel } from "../peek-panel";
94
- import type { StoryDetail } from "../types";
94
+ import type { StoryDetail } from "@/shared/story/types";
95
95
 
96
96
  function detail(priority: StoryDetail["priority"]): StoryDetail {
97
97
  return {
@@ -21,7 +21,7 @@
21
21
  import { beforeEach, describe, expect, it, vi } from "vitest";
22
22
  import { act, render, waitFor } from "@testing-library/react";
23
23
  import React from "react";
24
- import type { StoryListItem, UserBrief } from "../types";
24
+ import type { StoryListItem, UserBrief } from "@/shared/story/types";
25
25
 
26
26
  const mocks = vi.hoisted(() => ({
27
27
  onViewChange: undefined as undefined | ((v: string) => void),
@@ -39,7 +39,7 @@ import {
39
39
  import { parseJsonlBuffer, parseFinalBuffer } from "@/lib/ai/story-jsonl"
40
40
  import { StoryAiPreviewList, type PreviewStoryItem } from "./story-ai-preview-list"
41
41
  import { StorySearchPopover } from "@/components/story/search-popover"
42
- import type { UserBrief } from "@/components/story/types"
42
+ import type { UserBrief } from "@/shared/story/types"
43
43
  import { m } from "@/paraglide/messages.js";
44
44
 
45
45
  type AiTab = "create" | "split" | "import"
@@ -26,8 +26,8 @@ import {
26
26
  SelectTrigger,
27
27
  SelectValue,
28
28
  } from "@/components/ui/select"
29
- import { STORY_PRIORITIES } from "@/components/story/constants"
30
- import type { UserBrief } from "@/components/story/types"
29
+ import { STORY_PRIORITIES } from "@/shared/story/constants"
30
+ import type { UserBrief } from "@/shared/story/types"
31
31
  import { m } from "@/paraglide/messages.js";
32
32
 
33
33
  const PRIORITY_LABELS: Record<string, () => string> = {
@@ -22,6 +22,17 @@ import { useCallback, useEffect, useMemo, useState } from "react"
22
22
 
23
23
  const STORAGE_KEY = "story:descriptionAiModel"
24
24
 
25
+ // 浏览器存储访问必须 try/catch:本项目 SSR 的 Node 环境存在注入的 window
26
+ // (opaque origin,localStorage 访问抛 SecurityError),typeof 守卫会穿透
27
+ // (S33,与 lib/ai/use-selected-model readStorage 同口径)
28
+ function readStoredModel(): string {
29
+ try {
30
+ return window.localStorage.getItem(STORAGE_KEY) ?? ""
31
+ } catch {
32
+ return ""
33
+ }
34
+ }
35
+
25
36
  export interface StoryAiModelGroup {
26
37
  label: string
27
38
  items: { value: string; label: string }[]
@@ -57,8 +68,7 @@ export function useStoryAiModel(): {
57
68
  const defaultModel = session?.user?.aiModel ?? "openai/gpt-4o-mini"
58
69
  const [model, setModel] = useState<string>(
59
70
  () =>
60
- (typeof window !== "undefined" && localStorage.getItem(STORAGE_KEY)) ||
61
- "",
71
+ readStoredModel(),
62
72
  )
63
73
 
64
74
  // Fetch enabled models from server
@@ -106,8 +116,11 @@ export function useStoryAiModel(): {
106
116
 
107
117
  // Sync localStorage
108
118
  useEffect(() => {
109
- if (typeof window !== "undefined" && model) {
119
+ if (!model) return
120
+ try {
110
121
  localStorage.setItem(STORAGE_KEY, model)
122
+ } catch {
123
+ // SSR 注入 window / 隐私模式:持久化失败可接受,仅内存态生效
111
124
  }
112
125
  }, [model])
113
126
 
@@ -118,7 +131,7 @@ export function useStoryAiModel(): {
118
131
  const modelValues = useMemo(() => rawModels.map((m) => m.model), [rawModels])
119
132
  useEffect(() => {
120
133
  if (!session || modelValues.length === 0) return
121
- const stored = typeof window !== "undefined" ? localStorage.getItem(STORAGE_KEY) : null
134
+ const stored = readStoredModel()
122
135
  if (stored && modelValues.includes(stored)) {
123
136
  if (model !== stored) setModel(stored)
124
137
  return
@@ -28,8 +28,8 @@ import {
28
28
  import { Badge } from "@/components/ui/badge";
29
29
  import { UserAvatar } from "@/components/shared-kit";
30
30
  import { PriorityIcon } from "./priority-icon";
31
- import { STATE_COLOR, PRIORITY_RANK } from "./constants";
32
- import type { StoryListItem } from "./types";
31
+ import { STATE_COLOR, PRIORITY_RANK } from "@/shared/story/constants";
32
+ import type { StoryListItem } from "@/shared/story/types";
33
33
  import { m } from '@/paraglide/messages.js'
34
34
 
35
35
  const STATE_LABEL_FNS: Record<string, () => string> = {
@@ -38,9 +38,9 @@ import {
38
38
 
39
39
  import { createStoryFn } from "@/server/serverFns/story"
40
40
  import { ensureBuiltinProjectFn } from "@/server/serverFns/project"
41
- import type { StoryPriority, StoryState } from "./constants"
42
- import type { UserBrief } from "./types"
43
- import { serializeStoryDescription } from "./types"
41
+ import type { StoryPriority, StoryState } from "@/shared/story/constants"
42
+ import type { UserBrief } from "@/shared/story/types"
43
+ import { serializeStoryDescription } from "@/shared/story/types"
44
44
  import { StateEditor, PriorityEditor, AssigneeEditor, DateEditor, LabelsEditor } from "./inline-editors"
45
45
  import { createStorySchema } from "./story-create-schema"
46
46
 
@@ -62,7 +62,7 @@ interface CreateStoryDialogProps {
62
62
  initialProjectId?: string
63
63
  onSuccess?: (
64
64
  story: { id: string; name: string; identifier?: string },
65
- activities?: import("./types").ActivityItem[],
65
+ activities?: import("@/shared/story/types").ActivityItem[],
66
66
  ) => void
67
67
  }
68
68
 
@@ -18,7 +18,7 @@
18
18
  import { describe, it, expect, vi } from "vitest";
19
19
  import { render, screen, waitFor } from "@testing-library/react";
20
20
  import { ActivityTimeline } from "../activity-timeline";
21
- import type { ActivityItem, UserBrief } from "../../types";
21
+ import type { ActivityItem, UserBrief } from "@/shared/story/types";
22
22
 
23
23
  vi.mock("@/server/serverFns/project", () => ({
24
24
  listProjectsBriefFn: vi.fn().mockResolvedValue([]),
@@ -46,7 +46,7 @@ vi.mock("@tanstack/react-router", () => ({
46
46
  }));
47
47
 
48
48
  import { AssociatedAttachmentsPanel } from "../story-detail-toolbar/associated-attachments-panel";
49
- import type { RelationsData } from "../../types";
49
+ import type { RelationsData } from "@/shared/story/types";
50
50
 
51
51
  const data = {
52
52
  links: [{ id: "l1", url: "https://a.com", title: "站点A", description: null }],
@@ -193,7 +193,7 @@ const notionLikeReady = import("@/components/tiptap/notion-like");
193
193
 
194
194
  // 被测组件(mock 声明之后 import,保证 mock 生效)
195
195
  import { StoryDetailBody } from "../story-detail-body";
196
- import type { StoryDetail, UserBrief } from "../../types";
196
+ import type { StoryDetail, UserBrief } from "@/shared/story/types";
197
197
  // 真实契约类型(vi.mock 只替换运行时,类型仍取自真实模块 → 替身受契约约束)
198
198
  import type {
199
199
  CollabAwareness,
@@ -18,7 +18,7 @@
18
18
  import { useState } from "react";
19
19
  import { ActivityTimeline } from "./activity-timeline";
20
20
  import { CommentThread } from "./comment-thread";
21
- import type { CommentWithReplies, ActivityItem, UserBrief } from "../types";
21
+ import type { CommentWithReplies, ActivityItem, UserBrief } from "@/shared/story/types";
22
22
  import { m } from "@/paraglide/messages.js";
23
23
 
24
24
  interface ActivityTabsProps {