@allbluecn/web-app 0.21.0 → 0.22.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 (74) hide show
  1. package/package.json +3 -3
  2. package/src/components/billing/__tests__/use-resource.test.tsx +2 -0
  3. package/src/components/billing/subscription-provider.tsx +2 -0
  4. package/src/components/bug/create-bug-dialog.tsx +1 -1
  5. package/src/components/bug/detail/props-section.tsx +1 -0
  6. package/src/components/bug/inline-editors/labels-editor.tsx +4 -1
  7. package/src/components/bug/views/bug-list/batch-toolbar.tsx +5 -2
  8. package/src/components/bug/views/bug-list/inline-bug-creator.tsx +1 -1
  9. package/src/components/bug/views/bug-list/row.tsx +1 -1
  10. package/src/components/design/editor/dashboard/design-dashboard.tsx +21 -7
  11. package/src/components/design/editor/layout/design-navigation.tsx +22 -12
  12. package/src/components/harness/dock/dock-panel-container.tsx +17 -4
  13. package/src/components/harness/harness-workbench-page.tsx +14 -1
  14. package/src/components/harness/todo-panel.tsx +2 -5
  15. package/src/components/harness/use-todo-projection.ts +3 -3
  16. package/src/components/story/ai/story-ai-panel.tsx +2 -2
  17. package/src/components/story/create-story-dialog.tsx +1 -1
  18. package/src/components/story/detail/story-collab-gate.tsx +191 -0
  19. package/src/components/story/detail/story-detail-body.tsx +5 -169
  20. package/src/components/story/detail/story-properties.tsx +1 -0
  21. package/src/components/story/filters/field-configs.tsx +6 -0
  22. package/src/components/story/inline-editors/__tests__/labels-editor.test.tsx +2 -1
  23. package/src/components/story/inline-editors/labels-editor.tsx +14 -10
  24. package/src/components/story/peek-panel.tsx +1 -1
  25. package/src/components/story/relations/story-ref-row.tsx +1 -1
  26. package/src/components/story/views/story-list/batch-toolbar.tsx +1 -1
  27. package/src/components/story/views/story-list/inline-story-creator.tsx +1 -0
  28. package/src/components/story/views/story-list/row.tsx +1 -1
  29. package/src/components/testcase/runs/convert-bug-button.tsx +1 -1
  30. package/src/design-engine/document/serialize-read.ts +30 -1
  31. package/src/design-engine/engine/app.ts +18 -1
  32. package/src/design-engine/engine/engine-base.ts +2 -0
  33. package/src/lib/changelog/sdk-versions.ts +4 -4
  34. package/src/lib/collections/reference/labels-collection.ts +1 -1
  35. package/src/paraglide/messages/_index.d.ts +4 -0
  36. package/src/paraglide/messages/bug_list_loadmorefailed2.d.ts +17 -0
  37. package/src/paraglide/messages/bug_list_loadmorefailed2.js +30 -0
  38. package/src/paraglide/messages/bug_storypanel_partialfailed2.d.ts +19 -0
  39. package/src/paraglide/messages/bug_storypanel_partialfailed2.js +30 -0
  40. package/src/paraglide/messages/chat_workspace_githubnotconnected2.d.ts +17 -0
  41. package/src/paraglide/messages/chat_workspace_githubnotconnected2.js +30 -0
  42. package/src/paraglide/messages/dashboard_activityfeed_ai1.d.ts +17 -0
  43. package/src/paraglide/messages/dashboard_activityfeed_ai1.js +30 -0
  44. package/src/routes/_nav/bugs/$bugId/route.tsx +1 -1
  45. package/src/routes/_nav/bugs/index.tsx +1 -1
  46. package/src/routes/_nav/dashboard/-components/activity-feed.tsx +23 -4
  47. package/src/routes/_nav/dashboard/route.lazy.tsx +6 -6
  48. package/src/routes/_nav/dashboard/route.tsx +11 -4
  49. package/src/routes/_nav/projects/$projectId/settings/route.lazy.tsx +35 -1
  50. package/src/routes/_nav/stories/$storyId/route.tsx +1 -1
  51. package/src/routes/_nav/stories/index.tsx +1 -1
  52. package/src/routes/api/billing-webhook.ts +1 -1
  53. package/src/server/__tests__/resources.agents.test.ts +2 -0
  54. package/src/server/__tests__/resources.license.test.ts +2 -0
  55. package/src/server/__tests__/resources.team-seats.test.ts +2 -0
  56. package/src/server/__tests__/resources.test.ts +2 -0
  57. package/src/server/ai-gateway/__tests__/pgvector-memory-adapter.test.ts +16 -0
  58. package/src/server/ai-gateway/memory/pgvector-adapter.ts +14 -0
  59. package/src/server/ai-gateway/storage/__tests__/blob-store.test.ts +4 -0
  60. package/src/server/ai-gateway/storage/db-blob-store.ts +4 -0
  61. package/src/server/billing-email.ts +18 -5
  62. package/src/server/bug/bug-crud-helpers.ts +107 -0
  63. package/src/server/knowledge/knowledge-helpers.ts +40 -0
  64. package/src/server/resources.ts +43 -2
  65. package/src/server/serverFns/__tests__/story-crud-labels-users.test.ts +27 -14
  66. package/src/server/serverFns/activity.ts +61 -0
  67. package/src/server/serverFns/anyhub/install.ts +3 -0
  68. package/src/server/serverFns/bug/bug-crud.ts +10 -83
  69. package/src/server/serverFns/knowledge.ts +2 -17
  70. package/src/server/serverFns/story/__tests__/kb-scope.test.ts +2 -2
  71. package/src/server/serverFns/story/kb-scope.ts +6 -6
  72. package/src/server/serverFns/story/story-crud/labels.ts +20 -7
  73. package/src/server/story/story-crud/label-upsert.ts +47 -9
  74. package/src/start.ts +15 -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, { lazy, Suspense, useCallback, useEffect, useMemo, useRef, useState, type LazyExoticComponent, type ComponentType, type ReactElement, type ReactNode } from "react";
18
+ import React, { lazy, Suspense, useCallback, useEffect, useMemo, useRef, useState, type LazyExoticComponent, type ComponentType, type ReactElement } from "react";
19
19
  import {
20
20
  ChevronsDownUp,
21
21
  ChevronsUpDown,
@@ -35,9 +35,8 @@ import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip
35
35
  import { updateStoryFn, listStoryReactionsFn, toggleStoryReactionFn } from "@/server/serverFns/story";
36
36
  import { formatTimestamp } from "@/lib/datetime";
37
37
  import { toast } from "@/components/ui/toast";
38
- import type { StoryDetail, UserBrief, CommentWithReplies, ActivityItem, ReactionGroup, RelationsData } from "@/shared/story/types";
38
+ import type { StoryDetail, ReactionGroup } from "@/shared/story/types";
39
39
  import { parseStoryDescription, serializeStoryDescription } from "@/shared/story/types";
40
- import type { StoryState } from "@/shared/story/constants";
41
40
  import type { StoryPatch } from "../display/group-fields";
42
41
  import { ActivityTabs } from "./activity-tabs";
43
42
  import { EmojiPicker } from "@/components/shared-kit/comments/emoji-picker";
@@ -48,20 +47,16 @@ import { LucideIconByName } from "@/components/shared-kit/icons/lucide-icon-pick
48
47
  import type { Editor } from "@tiptap/react";
49
48
  import { StoryProperties } from "./story-properties";
50
49
  import { StoryDetailToolbar } from "./story-detail-toolbar";
50
+ import { NON_COLLAB_STATE, PATCHABLE_FIELDS, StoryCollabGate } from "./story-collab-gate";
51
+ import type { StoryCollabRuntimeState, StoryDetailBodyProps } from "./story-collab-gate";
52
+ export type { StoryCollabRuntimeState, StoryDetailBodyProps };
51
53
  import { m } from "@/paraglide/messages.js";
52
54
  import { useDebouncer } from "@/lib/pacer";
53
55
 
54
56
  import type { NotionDocEditorProps } from "@/components/tiptap/notion-like";
55
57
  import { errorMessage } from "@/lib/toast-error";
56
- import { buildRoomName } from "@allbluecn/collab-core/rooms";
57
- import { getUserColor } from "@/lib/collab/user-color";
58
- import { useCollabDoc } from "@/lib/collab/use-collab-doc";
59
- import type { CollabProviderHandle } from "@/lib/collab/provider";
60
58
  import { useCollabFields } from "@/lib/collab/bindings/fields";
61
- import { useSelfProfileBroadcast } from "@/lib/collab/use-self-profile-broadcast";
62
- import type { NotionEditorCollabExtensions } from "@/components/tiptap/notion-like/editor-extensions";
63
59
  import { FieldEditingPill } from "@/components/collab/field-editing-pill";
64
- import { useSession } from "@/components/shared-kit/auth/auth-client";
65
60
  const NotionDocEditor = lazy(() => import("@/components/tiptap/notion-like").then(m => ({ default: m.NotionDocEditor }))) as LazyExoticComponent<ComponentType<NotionDocEditorProps>>;
66
61
 
67
62
  /**
@@ -93,165 +88,6 @@ const STORY_COLLAB_FIELDS = [
93
88
  ] as const;
94
89
 
95
90
  /** 协同运行时状态(StoryCollabGate render-prop 产物;结构对齐任务 15 口径) */
96
- interface StoryCollabRuntimeState {
97
- /** 协同配置(null = 会话未就绪/协同不可用,编辑器暂不挂载协同扩展) */
98
- config: NotionEditorCollabExtensions | null;
99
- /** 会话是否就绪(协同配置稳定后再挂编辑器,防 caret user 半截装配) */
100
- ready: boolean;
101
- /** provider offline(降级单机模式,提示条 + 旧保存链路) */
102
- offline: boolean;
103
- /** provider connecting(建连窗口,轻提示条;窗口内 Y 是唯一写手) */
104
- connecting: boolean;
105
- /** 协同写手活跃(connecting/synced 均算——Y 是唯一内容源,DB 链路短路) */
106
- collabActive: boolean;
107
- /** room 连接句柄(字段 binding 数据源;null = 非协同/noop) */
108
- handle: CollabProviderHandle | null;
109
- /** 字段 editing 标记署名(session brief;null 走 binding 内 profile/clientID 降级) */
110
- identity: { userId: string; name: string } | null;
111
- }
112
-
113
- /** 非协同分支的协同状态常量(稳定引用,行为与既有单机链路逐字一致) */
114
- const NON_COLLAB_STATE: StoryCollabRuntimeState = {
115
- config: null,
116
- ready: true,
117
- offline: false,
118
- connecting: false,
119
- collabActive: false,
120
- handle: null,
121
- identity: null,
122
- };
123
-
124
- interface StoryCollabGateProps {
125
- storyId: string;
126
- snapshot: string | null;
127
- children: (state: StoryCollabRuntimeState) => ReactNode;
128
- }
129
-
130
- /**
131
- * 协同运行时装配(条件挂载于 collab 分支,非协同路径不产生任何协同 hook
132
- * 观察/连接——结构对齐任务 15 CollabDocGate):会话 brief(caret 用户 + 字段
133
- * editing 署名)+ entity room 建连/快照预水合/状态订阅。
134
- */
135
- function StoryCollabGate({ storyId, snapshot, children }: StoryCollabGateProps) {
136
- const { data: session, isPending } = useSession();
137
- const roomName = useMemo(
138
- () => buildRoomName({ kind: "entity-story", storyId }),
139
- [storyId],
140
- );
141
- const { handle, status } = useCollabDoc({ roomName, snapshot });
142
- // 自身 profile 广播:字段 binding 的 identity 缺省路径(会话 pending 期间
143
- // 创建 binding)读 awareness profile 兜底署名——无广播则 editing 标记降级
144
- // 为 client:{clientID}(远端 pill 无名)。Story 详情无 presence 头像组,
145
- // 广播在本 gate 自持(hook 自 collab-presence-header 提取复用)。
146
- useSelfProfileBroadcast(handle);
147
-
148
- const user = session?.user?.id
149
- ? {
150
- name: session.user.name || session.user.id,
151
- color: getUserColor(session.user.id),
152
- }
153
- : null;
154
-
155
- const config: NotionEditorCollabExtensions | null =
156
- handle && user
157
- ? {
158
- ydoc: handle.doc,
159
- // noop/awareness 缺失时不挂 Caret(扩展工厂按 provider 判定)
160
- provider: handle.awareness ? handle : null,
161
- user,
162
- }
163
- : null;
164
-
165
- // I1 钉住(同任务 15):扩展集在编辑器创建时冻结,config 一旦非空就沿用
166
- // 首个非空值——防中途回落 null 时 stale 内容经仍在位的 Collaboration 插件
167
- // 写进 Y 并同步给协作者(协同内容静默回滚)。
168
- const pinnedConfigRef = useRef<NotionEditorCollabExtensions | null>(null);
169
- if (pinnedConfigRef.current === null && config !== null) {
170
- pinnedConfigRef.current = config;
171
- }
172
- const effectiveConfig = pinnedConfigRef.current;
173
-
174
- // 会话就绪但拿不到 user(_nav beforeLoad 守卫下不应发生)时不进协同态:
175
- // collabActive=false 让 DB 保存链路兜底,防「编辑器非 Y 绑定 + DB 短路」双失能
176
- const sessionUsable = isPending || user != null;
177
-
178
- return (
179
- <>
180
- {children({
181
- config: effectiveConfig,
182
- ready: !isPending,
183
- offline: status === "offline",
184
- connecting: status === "connecting",
185
- collabActive: sessionUsable && status !== null && status !== "offline",
186
- handle,
187
- identity:
188
- session?.user?.id
189
- ? { userId: session.user.id, name: session.user.name || session.user.id }
190
- : null,
191
- })}
192
- </>
193
- );
194
- }
195
-
196
- /** 详情面板可回写视图(列表/看板乐观更新)的字段集合——与 StoryPatch 对齐。
197
- * projectId 不在其中(项目归属变更走专门的转移流程,不在此通道)。 */
198
- const PATCHABLE_FIELDS = new Set<keyof StoryPatch>([
199
- "name",
200
- "state",
201
- "priority",
202
- "assigneeId",
203
- "startDate",
204
- "targetDate",
205
- ]);
206
-
207
- export interface StoryDetailBodyProps {
208
- story: StoryDetail;
209
- users: UserBrief[];
210
- comments: CommentWithReplies[];
211
- activities: ActivityItem[];
212
- currentUserId: string | undefined;
213
- layout?: "split" | "stack";
214
- identifier?: string;
215
- /** 所属项目(图标 + 名称),用于在需求 ID 前展示项目归属 */
216
- project?: { name: string; icon: string } | null;
217
- noScroll?: boolean;
218
- /** 所有标签列表 */
219
- labels?: { id: string; name: string; color: string }[];
220
- /** 所有需求列表(用于父需求选择) */
221
- stories?: { id: string; identifier: string; name: string; state: StoryState; projectId?: string }[];
222
- /** 所有项目列表(用于父需求选择) */
223
- projects?: { id: string; name: string; icon: string; displayId?: string | null }[];
224
- /** 迭代列表(用于迭代选择器) */
225
- iterations?: import("@/server/serverFns/iteration/schemas").IterationSummary[];
226
- /** 关联数据(从路由 loader 预加载) */
227
- relationsData?: RelationsData;
228
- onRelationsUpdate?: (next: RelationsData | ((prev: RelationsData) => RelationsData)) => void;
229
- onAppendActivities?: (items: ActivityItem[]) => void;
230
- onAppendComment?: (comment: CommentWithReplies) => void;
231
- onRemoveComment?: (commentId: string) => void;
232
- onStoryUpdate?: (s: StoryDetail) => void;
233
- /** 字段落库成功后的补丁回调(PeekPanel 回写列表/看板做乐观更新) */
234
- onStoryPatch?: (storyId: string, patch: StoryPatch) => void;
235
- onNavigateInPanel?: (targetId: string, targetIdentifier?: string, relationKind?: "child" | "dependency") => void;
236
- onOpenEntity?: (entity: import("@/components/story/entity-overlay").StoryEntityOverlay) => void;
237
- /** 用例/BUG 行的面板内叠加打开(peek 传入;缺省整页跳转) */
238
- /** 内嵌面板导航面包屑(在面板内跳转时替代单层 identifier 显示) */
239
- breadcrumbItems?: Array<{ identifier?: string; projectId?: string; label: string; onClick?: () => void }>;
240
- /** 编辑器实例就绪回调(透传 NotionDocEditor.onEditorReady;测试观察点) */
241
- onEditorReady?: (editor: Editor) => void;
242
- /**
243
- * 协同模式(Yjs 多人协作任务 20,规格 §7.1 Story 行):true 时 description
244
- * 接 Collaboration(Y fragment 'default')+ 字段接 useCollabFields,全部
245
- * 保存通道(8s 防抖 / 失焦 / 卸载 / meta 字段 / 标签 / 父需求)短路——
246
- * Y 是唯一写手;offline 降级恢复 DB 链路。缺省 false = 现有单机链路逐字保留。
247
- */
248
- collab?: boolean;
249
- /** SSR 协同快照(loader 并行取 getCollabSnapshotFn;collab=true 时必传,null = 空文档) */
250
- collabSnapshot?: string | null;
251
- }
252
-
253
- /** memo:顶栏滚动标题等轻量 state 翻转时,详情巨树(面板/行列表)必须整体跳过
254
- * 重渲染,否则越阈一次卡几百毫秒、标题延迟出现。调用方回调需 useCallback 稳定。 */
255
91
  export const StoryDetailBody = React.memo(function StoryDetailBody(props: StoryDetailBodyProps) {
256
92
  const { collab, collabSnapshot, ...bodyProps } = props;
257
93
  // 协同双模式:collab=true 时 gate 组件持有 useSession/useCollabDoc(条件
@@ -105,6 +105,7 @@ export function StoryProperties({
105
105
  {labels && (
106
106
  <PropertyItem icon={<Tag className="size-3.5" />} label={m.story_properties_labels()}>
107
107
  <LabelsEditor
108
+ projectId={story.projectId ?? ""}
108
109
  labelIds={story.labels.map((l) => l.id)}
109
110
  labels={labels}
110
111
  onChange={onLabelsChange}
@@ -69,6 +69,9 @@ const TEXT_OPERATORS: FilterOperator[] = ["contains", "not_contains", "is_empty"
69
69
  // ---------------------------------------------------------------------------
70
70
 
71
71
  const compactBtn = "h-6 rounded-sm bg-muted px-2 text-xs font-normal";
72
+ // S24:filter 值编辑器无项目上下文,新建标签在服务端被拒(标签必挂项目);
73
+ // 此处仅作为 LabelsEditor 的必填占位(勾选既有标签不受影响)
74
+ const FILTER_EDITOR_PROJECT_ID = "";
72
75
 
73
76
  function renderValueEditor({ property, operator, value, onChange, multi, selected, toggleMultiValue, users, labels }: FilterValueEditorContext<FilterKey>): ReactNode {
74
77
  switch (property) {
@@ -112,6 +115,9 @@ function renderValueEditor({ property, operator, value, onChange, multi, selecte
112
115
  case "labelId":
113
116
  return (
114
117
  <LabelsEditor
118
+ // S24:filter 值编辑器无项目上下文——仅勾选既有标签,新建标签走
119
+ // 需求详情/看板行内编辑器(projectId 透传自 story/bug 归属)
120
+ projectId={FILTER_EDITOR_PROJECT_ID}
115
121
  labelIds={selected}
116
122
  labels={labels ?? []}
117
123
  onChange={(ids) => onChange(ids.length ? ids.join(",") : "")}
@@ -52,6 +52,7 @@ const LABELS = [
52
52
  ];
53
53
 
54
54
  const DEFAULT_PROPS: React.ComponentProps<typeof LabelsEditor> = {
55
+ projectId: "p-test",
55
56
  labelIds: [],
56
57
  labels: LABELS,
57
58
  onChange: vi.fn(),
@@ -124,7 +125,7 @@ describe("LabelsEditor AI 入口", () => {
124
125
 
125
126
  fireEvent.click(withText(document.querySelector("[data-slot=popover-content]")!, "应用"));
126
127
  await waitFor(() => {
127
- expect(mocks.createLabelFn).toHaveBeenCalledWith({ data: { name: "登录" } });
128
+ expect(mocks.createLabelFn).toHaveBeenCalledWith({ data: { name: "登录", projectId: "p-test" } });
128
129
  expect(onChange).toHaveBeenCalledWith(["l1", "l-new"]);
129
130
  });
130
131
  });
@@ -30,6 +30,8 @@ import { m } from "@/paraglide/messages.js";
30
30
  import { toastError } from "@/lib/toast-error";
31
31
 
32
32
  interface LabelsEditorProps {
33
+ /** S24:标签归属项目(创建必挂;列表=全局存量 ∪ 本项目)。 */
34
+ projectId: string;
33
35
  labelIds: string[];
34
36
  labels: { id: string; name: string; color: string }[];
35
37
  onChange: (labelIds: string[]) => void;
@@ -59,16 +61,18 @@ type AiPanel =
59
61
  // 挂载即拉全量清单会在打开面板时形成请求突发、挤占 dev SSR 并发致其他
60
62
  // RPC 被丢(Failed to fetch)——同一批并发挂载共享同一个在途请求
61
63
  let labelsInFlight: Promise<LabelLike[]> | null = null;
62
- function fetchLabelsShared(): Promise<LabelLike[]> {
63
- if (!labelsInFlight) {
64
- labelsInFlight = listLabelsFn().finally(() => {
65
- labelsInFlight = null;
66
- });
67
- }
64
+ let labelsInFlightKey = "";
65
+ function fetchLabelsShared(projectId: string): Promise<LabelLike[]> {
66
+ // 同 key 复用在途请求;projectId 维度隔离缓存(不同项目标签集不同)
67
+ if (labelsInFlight && labelsInFlightKey === projectId) return labelsInFlight;
68
+ labelsInFlightKey = projectId;
69
+ labelsInFlight = listLabelsFn({ data: { projectId } }).finally(() => {
70
+ labelsInFlight = null;
71
+ });
68
72
  return labelsInFlight;
69
73
  }
70
74
 
71
- export function LabelsEditor({ labelIds, labels, onChange, mode = "multi", className, variant = "chip", aiContext, onLabelCreated, maxVisibleLabels = 5 }: LabelsEditorProps) {
75
+ export function LabelsEditor({ projectId, labelIds, labels, onChange, mode = "multi", className, variant = "chip", aiContext, onLabelCreated, maxVisibleLabels = 5 }: LabelsEditorProps) {
72
76
  const [open, setOpen] = useState(false);
73
77
  const { data: session } = useSession();
74
78
  const hasAiKey = session?.user?.hasAiKey ?? false;
@@ -93,7 +97,7 @@ export function LabelsEditor({ labelIds, labels, onChange, mode = "multi", class
93
97
 
94
98
  useEffect(() => {
95
99
  let cancelled = false;
96
- fetchLabelsShared()
100
+ fetchLabelsShared(projectId)
97
101
  .then((rows: LabelLike[]) => {
98
102
  if (!cancelled) setFetchedLabels(rows);
99
103
  })
@@ -182,7 +186,7 @@ export function LabelsEditor({ labelIds, labels, onChange, mode = "multi", class
182
186
  if (ai.phase !== "done") return;
183
187
  const newIds: string[] = [];
184
188
  for (const name of selectedNew) {
185
- const created = await createLabelFn({ data: { name } });
189
+ const created = await createLabelFn({ data: { name, projectId } });
186
190
  newIds.push(created.id);
187
191
  setLocalLabels((prev) =>
188
192
  prev.some((l) => l.id === created.id) ? prev : [...prev, created],
@@ -209,7 +213,7 @@ export function LabelsEditor({ labelIds, labels, onChange, mode = "multi", class
209
213
  }
210
214
  setCreating(true);
211
215
  try {
212
- const created = await createLabelFn({ data: { name } });
216
+ const created = await createLabelFn({ data: { name, projectId } });
213
217
  setLocalLabels((prev) => [...prev, created]);
214
218
  onLabelCreated?.(created);
215
219
  toggle(created.id);
@@ -238,7 +238,7 @@ export function StoryPeekPanel({
238
238
  const restP = Promise.all([
239
239
  listCommentsFn({ data: { storyId: effectiveStoryId } }),
240
240
  listActivitiesFn({ data: { storyId: effectiveStoryId, take: 50 } }),
241
- listLabelsFn(),
241
+ listLabelsFn({ data: {} }), // S24:prefetch 阶段无 story.projectId,全量标签;创建走详情态编辑器
242
242
  listChildStoriesFn({ data: { parentId: effectiveStoryId } }),
243
243
  getDependenciesFn({ data: { storyId: effectiveStoryId } }),
244
244
  getLinksFn({ data: { storyId: effectiveStoryId } }),
@@ -244,7 +244,7 @@ export function StoryRefRow({
244
244
  <DateEditor value={storyState.targetDate} onChange={(v) => updateField({ targetDate: v })} label={m.story_children_dueDate()} icon={CalendarCheck} overdue={overdue} />
245
245
  )}
246
246
  {properties.includes("labels") && (
247
- <LabelsEditor labelIds={currentLabelIds} labels={labels} onChange={(v) => updateField({ labelIds: v })} variant="button" aiContext={{ name: () => storyState.name }} />
247
+ <LabelsEditor projectId={storyState.projectId ?? ""} labelIds={currentLabelIds} labels={labels} onChange={(v) => updateField({ labelIds: v })} variant="button" aiContext={{ name: () => storyState.name }} />
248
248
  )}
249
249
  <DropdownMenu>
250
250
  <DropdownMenuTrigger render={<Button variant="ghost" size="icon" className="size-7 p-0 ml-auto border border-border/60 rounded hover:bg-muted" />}>
@@ -140,7 +140,7 @@ export function BatchToolbar({
140
140
  <AssigneeEditor assignee={null} users={users} onChange={(v) => onBulkUpdate({ assigneeId: v })} variant="button" />
141
141
  <DateEditor value={null} onChange={(v) => onBulkUpdate({ startDate: v })} label={m.story_batchToolbar_startDate()} icon={CalendarPlus2} />
142
142
  <DateEditor value={null} onChange={(v) => onBulkUpdate({ targetDate: v })} label={m.story_batchToolbar_dueDate()} icon={CalendarCheck} />
143
- <LabelsEditor labelIds={[]} labels={labels} onChange={(v) => onBulkUpdate({ labelIds: v })} variant="button" />
143
+ <LabelsEditor projectId="" labelIds={[]} labels={labels} onChange={(v) => onBulkUpdate({ labelIds: v })} variant="button" />
144
144
  </div>
145
145
  </BatchToolbarShell>
146
146
 
@@ -94,6 +94,7 @@ export const InlineStoryCreator = memo(function InlineStoryCreator({ name, setNa
94
94
  <DateEditor value={targetDate} onChange={setTargetDate} label={m.story_inlineCreator_dueDate()} icon={CalendarCheck} />
95
95
  {/* 标签选择纳入提交(此前 onChange 为 no-op,用户勾选被静默丢弃,审计 A7x 🟡) */}
96
96
  <LabelsEditor
97
+ projectId=""
97
98
  labelIds={labelIds}
98
99
  labels={labels || []}
99
100
  onChange={(v) => setLabelIds?.(v)}
@@ -127,7 +127,7 @@ export const StoryListRow = memo(function StoryListRow({ story, group, indent, i
127
127
  <DateEditor value={story.targetDate} onChange={setTargetDate} label={m.story_row_dueDate()} icon={CalendarCheck} overdue={overdue} />
128
128
  )}
129
129
  {properties.includes("labels") && (
130
- <LabelsEditor labelIds={currentLabelIds} labels={labels} onChange={setLabels} variant="button" aiContext={{ name: () => story.name }} />
130
+ <LabelsEditor projectId={story.projectId ?? ""} labelIds={currentLabelIds} labels={labels} onChange={setLabels} variant="button" aiContext={{ name: () => story.name }} />
131
131
  )}
132
132
  </div>
133
133
  );
@@ -111,7 +111,7 @@ export function ConvertBugButton({ runCase, execution, runProjectId, onConverted
111
111
  const [users, projects, labels] = await Promise.all([
112
112
  listUsersBriefFn(),
113
113
  listProjectsBriefFn(),
114
- listLabelsFn(),
114
+ listLabelsFn({ data: {} }), // 转换前置无归属项目,全量标签
115
115
  ]);
116
116
  setRoster({ users, projects, labels });
117
117
  setDialogOpen(true);
@@ -23,6 +23,33 @@ export type DocSceneResult = {
23
23
  nodeIndex: Map<string, ILeaf>
24
24
  }
25
25
 
26
+ // S8(裁决:serialize-read style 通道白名单化):读侧只收白名单键,其余
27
+ // leafer 原生键丢弃——持久入口(存档/协作文档)不再能把任意字段注入 style
28
+ // 通道并在写侧 applyProjectedKeys 落到 leaf。黑名单保留为双闸(新内部键
29
+ // 漏出白名单时不致误入 style)。键清单 = 现网 style 通道实际消费面:
30
+ // paints canonical(fill/stroke 家族)+ 文本投影(fontSize 等 5 键,inferType
31
+ // 与文本工厂消费)+ 效果回显占位(shadow/innerShadow/blur,STYLE_EXCLUDE 已
32
+ // 排除故白名单含它也不进——防御白名单误扩)+ 通用样式(opacity/blendMode/
33
+ // rotation 占位/可见性/圆角)。
34
+ const STYLE_ALLOW = new Set([
35
+ // paints canonical(style/paints.ts 结构化键)
36
+ "fill", "stroke", "strokeWidth", "strokeAlign", "strokeJoin", "strokeCap",
37
+ "dashPattern", "strokeStyles",
38
+ // 文本投影(inferType 判 text + 文本工厂重建;对齐键经写侧 text 通道落
39
+ // leaf 后回读顶层)
40
+ "fontSize", "fontWeight", "lineHeight", "letterSpacing", "fontFamily",
41
+ "textAlign", "verticalAlign",
42
+ // 通用样式(写侧 PROJECTED_KEYS 读回落点)+ 翻转规范化数据源
43
+ // (下方 scaleX/scaleY 负值坐标规范化从此读取)
44
+ "opacity", "blendMode", "rotation", "scaleX", "scaleY",
45
+ // 圆角(rect/ellipse corners 投影通道)
46
+ "cornerRadius",
47
+ // 图形原生参数(SHAPE_PROP_KEYS 全集:style 通道是 polygon/star/line/箭头
48
+ // 工厂全量重建的数据源,serialize-read 下方 style→props 恢复亦从此读)
49
+ "innerRadius", "startAngle", "endAngle", "sides", "corners", "curve",
50
+ "closed", "points", "windingRule", "startArrow", "endArrow",
51
+ ])
52
+
26
53
  const STYLE_EXCLUDE = new Set([
27
54
  "id",
28
55
  "children",
@@ -135,7 +162,9 @@ function descToNode(
135
162
  const id = rawId && !AUTO_ID_RE.test(rawId) ? rawId : idGen()
136
163
  const style: Record<string, unknown> = {}
137
164
  for (const [key, value] of Object.entries(desc)) {
138
- if (STYLE_EXCLUDE.has(key) || value === undefined) continue
165
+ // S8:白名单主闸 + 黑名单副闸(双层;黑名单含效果键——即便未来误扩白名单
166
+ // 也不致复活「删除效果后陈旧副本」缺陷)
167
+ if (STYLE_EXCLUDE.has(key) || !STYLE_ALLOW.has(key) || value === undefined) continue
139
168
  style[key] = value
140
169
  }
141
170
  const props: Record<string, unknown> = {}
@@ -198,7 +198,17 @@ export class LeaferEngine extends ActionsEngine {
198
198
  this.refreshCornerDragDecor()
199
199
  this.notifySelectionChange()
200
200
  }
201
- const docHandler = (): void => this.syncDocFromTree()
201
+ // S9(审计 A91 ⚪ 性能债,裁决落地):拖拽期间 editor MOVE/SCALE/ROTATE/
202
+ // SKEW 每次触发都全量 leaferToDoc(大文档每帧 O(N) 序列化)——拖拽中只标
203
+ // dirty,drag END(app DragEvent,含 re-parent 落位)统一读回一次。
204
+ // 非拖拽路径(键盘移动/对齐命令,不经 DragEvent)保持即时读回不变。
205
+ const docHandler = (): void => {
206
+ if (this.preDragDoc !== null) {
207
+ this.dragReadbackDirty = true
208
+ return
209
+ }
210
+ this.syncDocFromTree()
211
+ }
202
212
  this.eventIds = [
203
213
  editor.on_(events.SELECT, selectionHandler),
204
214
  editor.on_(events.MOVE, docHandler),
@@ -224,6 +234,13 @@ export class LeaferEngine extends ActionsEngine {
224
234
  const dragEndHandler = (): void => {
225
235
  const before = this.preDragDoc
226
236
  this.preDragDoc = null
237
+ // S9:拖拽期间的 MOVE/SCALE 被抑制为 dirty 标记,松手统一读回一次
238
+ // (re-parent 落位前读回,notifyDragChange(false) 与 history.push 消费
239
+ // 的 currentDoc 即含最终位姿)
240
+ if (this.dragReadbackDirty) {
241
+ this.dragReadbackDirty = false
242
+ this.syncDocFromTree()
243
+ }
227
244
  // 引用比较(原 JSON.stringify 全量比较在 10 万节点下冻结 6.1s,实测):
228
245
  // currentDoc 为不可变更新,拖拽期间任何读回/patch 都产生新引用,
229
246
  // 无变更时引用保持——语义等价且 O(1)
@@ -59,6 +59,8 @@ export class LeaferEngineBase {
59
59
  protected snap: SnapInstance | null = null
60
60
  // 拖拽开始时的文档快照,DragEvent.END 时入撤销栈
61
61
  protected preDragDoc: SceneDocument | null = null
62
+ // S9:拖拽期间 editor 几何事件抑制读回的 dirty 标记(drag END 统一读回)
63
+ protected dragReadbackDirty = false
62
64
  protected tool: ToolId = "select"
63
65
  protected docListeners = new Set<(doc: SceneDocument) => void>()
64
66
  protected selectionListeners = new Set<(ids: string[]) => void>()
@@ -96,15 +96,15 @@ export const SDK_GROUPS: SdkGroup[] = [
96
96
  "items": [
97
97
  {
98
98
  "name": "@allbluecn/web-app",
99
- "version": "v0.21.0",
100
- "lastUpdate": "Oct 28, 2026",
99
+ "version": "v0.22.0",
100
+ "lastUpdate": "Oct 29, 2026",
101
101
  "source": "apps/web/package.json",
102
102
  "docsUrl": "https://github.com/allbluecn/allblue"
103
103
  },
104
104
  {
105
105
  "name": "@allbluecn/kernel",
106
- "version": "v0.11.0",
107
- "lastUpdate": "Oct 28, 2026",
106
+ "version": "v0.12.0",
107
+ "lastUpdate": "Oct 29, 2026",
108
108
  "source": "packages/kernel/package.json",
109
109
  "docsUrl": "https://github.com/allbluecn/allblue"
110
110
  }
@@ -24,7 +24,7 @@ export const labelsCollection = createCollection(
24
24
  queryCollectionOptions({
25
25
  id: 'labels',
26
26
  queryKey: ['labels'],
27
- queryFn: () => listLabelsFn(),
27
+ queryFn: () => listLabelsFn({ data: {} }),
28
28
  getKey: (item: { id: string }) => item.id,
29
29
  queryClient: getQueryClient(),
30
30
  })
@@ -623,6 +623,7 @@ export * from "./bug_list_empty_title.js";
623
623
  export * from "./bug_list_first_occur.js";
624
624
  export * from "./bug_list_group_by.js";
625
625
  export * from "./bug_list_last_occur.js";
626
+ export * from "./bug_list_loadmorefailed2.js";
626
627
  export * from "./bug_list_load_more.js";
627
628
  export * from "./bug_list_loading.js";
628
629
  export * from "./bug_list_loading_data.js";
@@ -841,6 +842,7 @@ export * from "./bug_storypanel_order_lastoccur2.js";
841
842
  export * from "./bug_storypanel_order_severity1.js";
842
843
  export * from "./bug_storypanel_order_state1.js";
843
844
  export * from "./bug_storypanel_order_updated1.js";
845
+ export * from "./bug_storypanel_partialfailed2.js";
844
846
  export * from "./bug_storypanel_selectedcount2.js";
845
847
  export * from "./bug_storypanel_setseverity2.js";
846
848
  export * from "./bug_storypanel_setstate2.js";
@@ -1603,6 +1605,7 @@ export * from "./chat_workspace_connectgithub1.js";
1603
1605
  export * from "./chat_workspace_fsapunavailable1.js";
1604
1606
  export * from "./chat_workspace_github.js";
1605
1607
  export * from "./chat_workspace_githubconnect1.js";
1608
+ export * from "./chat_workspace_githubnotconnected2.js";
1606
1609
  export * from "./chat_workspace_githubnotwired2.js";
1607
1610
  export * from "./chat_workspace_local.js";
1608
1611
  export * from "./chat_workspace_localhint1.js";
@@ -1934,6 +1937,7 @@ export * from "./common_translator_translating.js";
1934
1937
  export * from "./composer_attach_button.js";
1935
1938
  export * from "./composer_attach_imagesonly1.js";
1936
1939
  export * from "./composer_attach_remove.js";
1940
+ export * from "./dashboard_activityfeed_ai1.js";
1937
1941
  export * from "./dashboard_activityfeed_empty1.js";
1938
1942
  export * from "./dashboard_activityfeed_title1.js";
1939
1943
  export * from "./dashboard_askai_overview1.js";
@@ -0,0 +1,17 @@
1
+ export { bug_list_loadmorefailed2 as "bug_list_loadMoreFailed" };
2
+ export type LocalizedString = import("../runtime.js").LocalizedString;
3
+ export type Bug_List_Loadmorefailed2Inputs = {};
4
+ /**
5
+ * | output |
6
+ * | --- |
7
+ * | "Failed to load more" |
8
+ *
9
+ * @param {Bug_List_Loadmorefailed2Inputs} inputs
10
+ * @param {{ locale?: "zh" | "en" }} options
11
+ * @returns {LocalizedString}
12
+ */
13
+ declare const bug_list_loadmorefailed2: ((inputs?: Bug_List_Loadmorefailed2Inputs, options?: {
14
+ locale?: "zh" | "en";
15
+ }) => LocalizedString) & import("../runtime.js").MessageMetadata<Bug_List_Loadmorefailed2Inputs, {
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 {{}} Bug_List_Loadmorefailed2Inputs */
7
+
8
+ const zh_bug_list_loadmorefailed2 = /** @type {(inputs: Bug_List_Loadmorefailed2Inputs) => LocalizedString} */ () => {
9
+ return /** @type {LocalizedString} */ (`加载更多失败`)
10
+ };
11
+
12
+ const en_bug_list_loadmorefailed2 = /** @type {(inputs: Bug_List_Loadmorefailed2Inputs) => LocalizedString} */ () => {
13
+ return /** @type {LocalizedString} */ (`Failed to load more`)
14
+ };
15
+
16
+ /**
17
+ * | output |
18
+ * | --- |
19
+ * | "Failed to load more" |
20
+ *
21
+ * @param {Bug_List_Loadmorefailed2Inputs} inputs
22
+ * @param {{ locale?: "zh" | "en" }} options
23
+ * @returns {LocalizedString}
24
+ */
25
+ const bug_list_loadmorefailed2 = /** @type {((inputs?: Bug_List_Loadmorefailed2Inputs, options?: { locale?: "zh" | "en" }) => LocalizedString) & import('../runtime.js').MessageMetadata<Bug_List_Loadmorefailed2Inputs, { locale?: "zh" | "en" }, {}>} */ ((inputs = {}, options = {}) => {
26
+ const locale = experimentalStaticLocale ?? options.locale ?? getLocale()
27
+ if (locale === "en") return en_bug_list_loadmorefailed2(inputs)
28
+ return zh_bug_list_loadmorefailed2(inputs)
29
+ });
30
+ export { bug_list_loadmorefailed2 as "bug_list_loadMoreFailed" }
@@ -0,0 +1,19 @@
1
+ export { bug_storypanel_partialfailed2 as "bug_storyPanel_partialFailed" };
2
+ export type LocalizedString = import("../runtime.js").LocalizedString;
3
+ export type Bug_Storypanel_Partialfailed2Inputs = {
4
+ count: NonNullable<unknown>;
5
+ };
6
+ /**
7
+ * | output |
8
+ * | --- |
9
+ * | "{count} items failed, please retry" |
10
+ *
11
+ * @param {Bug_Storypanel_Partialfailed2Inputs} inputs
12
+ * @param {{ locale?: "zh" | "en" }} options
13
+ * @returns {LocalizedString}
14
+ */
15
+ declare const bug_storypanel_partialfailed2: ((inputs: Bug_Storypanel_Partialfailed2Inputs, options?: {
16
+ locale?: "zh" | "en";
17
+ }) => LocalizedString) & import("../runtime.js").MessageMetadata<Bug_Storypanel_Partialfailed2Inputs, {
18
+ locale?: "zh" | "en";
19
+ }, {}>;
@@ -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 {{ count: NonNullable<unknown> }} Bug_Storypanel_Partialfailed2Inputs */
7
+
8
+ const zh_bug_storypanel_partialfailed2 = /** @type {(inputs: Bug_Storypanel_Partialfailed2Inputs) => LocalizedString} */ (i) => {
9
+ return /** @type {LocalizedString} */ (`另有 ${i?.count} 条处理失败,请重试`)
10
+ };
11
+
12
+ const en_bug_storypanel_partialfailed2 = /** @type {(inputs: Bug_Storypanel_Partialfailed2Inputs) => LocalizedString} */ (i) => {
13
+ return /** @type {LocalizedString} */ (`${i?.count} items failed, please retry`)
14
+ };
15
+
16
+ /**
17
+ * | output |
18
+ * | --- |
19
+ * | "{count} items failed, please retry" |
20
+ *
21
+ * @param {Bug_Storypanel_Partialfailed2Inputs} inputs
22
+ * @param {{ locale?: "zh" | "en" }} options
23
+ * @returns {LocalizedString}
24
+ */
25
+ const bug_storypanel_partialfailed2 = /** @type {((inputs: Bug_Storypanel_Partialfailed2Inputs, options?: { locale?: "zh" | "en" }) => LocalizedString) & import('../runtime.js').MessageMetadata<Bug_Storypanel_Partialfailed2Inputs, { locale?: "zh" | "en" }, {}>} */ ((inputs, options = {}) => {
26
+ const locale = experimentalStaticLocale ?? options.locale ?? getLocale()
27
+ if (locale === "en") return en_bug_storypanel_partialfailed2(inputs)
28
+ return zh_bug_storypanel_partialfailed2(inputs)
29
+ });
30
+ export { bug_storypanel_partialfailed2 as "bug_storyPanel_partialFailed" }