@senguoyun/dsh-arkme 0.1.89 → 0.1.90

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.
@@ -0,0 +1,43 @@
1
+ # 通知列表刷新与排序
2
+
3
+ 基线:`origin/dev` / `58e579677e931111785d50954755cfc901d85b4a`。本任务在独立 worktree 开发,原 checkout 的录音与频道改动保留。
4
+
5
+ ## 范围与能力边界
6
+
7
+ 用户确认的行为:点击通知只切换内容区;同账号刷新保留已有通知行与内容;通知按最新一条事件的时间参与普通会话排序,保留置顶规则。已读与刷新不生成新的排序时间。
8
+
9
+ 随后确认移除通知标题栏的返回箭头及占位,保留铃铛、标题与全部已读操作;通过会话列表直接切换对话。
10
+
11
+ 跨板块返回统一恢复离开前的会话:补齐通知与 Arko 的 `lastConversationDestination` 记录,复用普通会话、Bot、Harness、Codex 和发给自己的恢复机制。记忆仍由现有 UI controller 持有,仅保存当前运行期间的一个目标,退出登录或账号替换时清除;不增加持久化、网络请求或后台资源。
12
+
13
+ | 能力面 | 范围 | 验证 |
14
+ | --- | --- | --- |
15
+ | UI | 通知缓存展示、账号隔离、目录排序、选中状态与跨板块恢复 | 慢请求、失败、账号切换、目录排序、会话恢复及真实登录态 Web 验收 |
16
+ | Host owner | 复用既有通知查询与已读 API,不改变请求合同或权限 | 无 Host 修改 |
17
+ | Tools / SDK | N/A:仅修复 Browser 展示状态,不新增业务能力、路由或持久化 | 既有入口保持不变 |
18
+ | Client / Harness | 无客户端、原生桥或运行环境修改 | 仅部署独立 Web 预览 |
19
+
20
+ ## 请求与资源边界
21
+
22
+ 入口为已有通知行和通知中心的挂载;缓存 owner 仍为单例通知 store。同账号并发刷新共用现有 in-flight 请求,账号变化取消旧请求并清除旧账号投影;不新增轮询、定时器或后台进程。通知行移动不拥有新的数据请求生命周期。
23
+
24
+ 既有查询上限保持:安排 50 条、世界最近 10 条记录各取最多 50 条互动、AI 来信 20 条。失败保留已有的有界快照,成功结果替换对应快照,不累计历史。目录新增一个合成通知项,继续使用现有分块渲染。
25
+
26
+ 表态仍复用现有未读收件箱,不新增已读表态历史存储;排序取当前通知投影中最新事件的原始时间,不使用点击、已读操作或请求完成时间。
27
+
28
+ ## 验证记录
29
+
30
+ - 基线相关测试:3 文件、56 项通过;新回归用例在修改前复现通知行消失、失败后丢失、固定位置三项问题。
31
+ - 新增 9 项行为测试:慢请求期间保留同一选中行与内容、失败保留、置顶及事件时间排序、真正空数据隐藏、世界与 AI 缓存、世界部分失败、账号切换隔离、并发请求合并、跨目录分块不额外请求。
32
+ - 定向验证:5 文件、69 项通过;增加边界用例并同步旧目录结构断言后,相关 2 文件、29 项通过。
33
+ - `pnpm run typecheck`、`pnpm run build`、`git diff --check` 通过。
34
+ - 全量 `pnpm test --maxWorkers=2`:850 文件、10037 项通过;11 文件、15 项按既有配置跳过。测试使用已安装的 zstd 工具链,未修改测试跳过配置。
35
+ - 移除标题栏返回按钮后重新构建通过;通知目录与入口导航相关 2 文件、11 项测试通过。该追加改动仅删除按钮和未使用样式,未再次复跑全量测试。
36
+ - 会话恢复基线 3 文件、64 项通过;新增切换用例在修复前复现通知和 Arko 往返 6 个板块的 12 项失败。修复后 3 文件、111 项通过,涵盖 42 种会话/板块组合、主动选择新会话、账号清理,以及世界通知入口到产品导航返回的实际组件链路;类型检查和构建通过。
37
+ - 会话恢复最终全量验证:850 文件、10084 项通过;11 文件、15 项按既有配置跳过。
38
+ - 打包版本仍为基线 `0.1.89`;独立临时 Web Profile 安装成功,本地预览使用的客户端构建文件与本次构建一致。
39
+ - 真实登录态 Web:通知显示 8 条,最近事件为 9 月 10 日;目录项排在 9 月 13 日与 9 月 7 日会话之间,进入通知后行保留并显示选中状态。
40
+ - 会话恢复实测:从已读的世界通知进入世界,再点击对话,通知行恢复选中且内容区仍显示 8 条通知;Arko → 世界 → 对话也恢复 Arko。未执行全部已读操作;世界通知跳转沿用既有的世界已查看处理。
41
+ - 更新前已备份旧预览包与配置;原 checkout 的 30 个本地文件逐一校验,与备份完全一致。未修改已安装 Arkme 客户端。
42
+
43
+ 慢请求、失败、账号切换等边界由自动化测试覆盖;实际 Web 检查使用正常网络。临时 Profile 完成安装检查,实际运行验收使用已有独立 Web 预览;两者均不等同于桌面客户端发行验收。
package/lib/client.js CHANGED
@@ -31395,13 +31395,14 @@ window.__ModuleLoader__.load({
31395
31395
  const destination = this.lastConversationDestination;
31396
31396
  this.publish({
31397
31397
  ...rest,
31398
- mode: destination?.kind === "codex" ? "codex" : destination?.kind === "harness" ? "harness" : destination?.kind === "bot" ? "bot" : "source",
31398
+ mode: destination === void 0 || destination.kind === "send_to_self" ? "source" : destination.kind,
31399
31399
  ...destination?.kind === "source" ? { selectedSource: destination.source } : {},
31400
31400
  ...destination?.kind === "bot" ? { selectedBot: destination.bot } : {}
31401
31401
  });
31402
31402
  }
31403
31403
  showNotifications() {
31404
31404
  this.leaveContacts();
31405
+ this.lastConversationDestination = { kind: "notifications" };
31405
31406
  const { selectedSource: _selectedSource, selectedBot: _selectedBot, recordingTarget: _recordingTarget, calendarOpen: _calendarOpen, productMode: _productMode, conversationTarget: _conversationTarget, extensionShareRef: _extensionShareRef, extensionShareAction: _extensionShareAction, extensionDetailId: _extensionDetailId, extensionAuthorFilter: _extensionAuthorFilter, webLoginDialogOpen: _webLoginDialogOpen, ...rest } = this.state;
31406
31407
  this.publish({
31407
31408
  ...rest,
@@ -31433,6 +31434,7 @@ window.__ModuleLoader__.load({
31433
31434
  }
31434
31435
  showArko() {
31435
31436
  this.leaveContacts();
31437
+ this.lastConversationDestination = { kind: "arko" };
31436
31438
  const { selectedSource: _selectedSource, calendarOpen: _calendarOpen, productMode: _productMode, ...rest } = this.state;
31437
31439
  this.publish({
31438
31440
  ...rest,
@@ -162825,6 +162827,7 @@ html body {
162825
162827
  const EMPTY_SNAPSHOT = {
162826
162828
  scope: void 0,
162827
162829
  revision: 0,
162830
+ ready: false,
162828
162831
  loading: false,
162829
162832
  error: void 0,
162830
162833
  arrangementItems: [],
@@ -162936,20 +162939,14 @@ html body {
162936
162939
  this.controller?.abort();
162937
162940
  const controller = new AbortController();
162938
162941
  this.controller = controller;
162942
+ const previous = this.snapshot.scope === scope ? this.snapshot : EMPTY_SNAPSHOT;
162939
162943
  const operation = (async () => {
162940
162944
  this.publish({
162941
- ...this.snapshot,
162945
+ ...previous,
162942
162946
  scope,
162943
162947
  loading: true,
162944
162948
  error: void 0,
162945
162949
  worldError: void 0,
162946
- arrangementItems: [],
162947
- worldItems: [],
162948
- worldUnreadCount: 0,
162949
- worldSeenThroughSequence: 0,
162950
- aiItems: [],
162951
- aiUnreadCount: 0,
162952
- aiLatestUnread: void 0,
162953
162950
  aiError: void 0
162954
162951
  });
162955
162952
  const [arrangementsResult, worldSummaryResult, worldFeedResult, aiUnreadResult, aiListResult] = await Promise.allSettled([
@@ -162971,18 +162968,18 @@ html body {
162971
162968
  }, controller.signal)
162972
162969
  ]);
162973
162970
  if (controller.signal.aborted || this.controller !== controller) return;
162974
- let arrangementItems = [];
162971
+ let arrangementItems = this.snapshot.arrangementItems;
162975
162972
  let error;
162976
162973
  if (arrangementsResult.status === "fulfilled") arrangementItems = Array.isArray(arrangementsResult.value?.items) ? arrangementsResult.value.items : [];
162977
162974
  else error = arrangementsResult.reason instanceof Error ? arrangementsResult.reason.message : tr$1("通知暂时无法加载");
162978
- let worldUnreadCount = 0;
162979
- let worldSeenThroughSequence = 0;
162975
+ let worldUnreadCount = this.snapshot.worldUnreadCount;
162976
+ let worldSeenThroughSequence = this.snapshot.worldSeenThroughSequence;
162980
162977
  let worldError;
162981
162978
  if (worldSummaryResult.status === "fulfilled") {
162982
162979
  worldUnreadCount = Math.max(0, Math.trunc(worldSummaryResult.value.unreadCount));
162983
162980
  worldSeenThroughSequence = Math.max(0, Math.trunc(worldSummaryResult.value.seenThroughSequence));
162984
162981
  } else worldError = worldSummaryResult.reason instanceof Error ? worldSummaryResult.reason.message : tr$1("世界互动暂时无法加载");
162985
- const worldItems = [];
162982
+ let worldItems = [];
162986
162983
  if (worldFeedResult.status === "fulfilled") {
162987
162984
  const feedItems = Array.isArray(worldFeedResult.value?.items) ? worldFeedResult.value.items.slice(0, 10) : [];
162988
162985
  const interactionResults = await Promise.allSettled(feedItems.map((feedItem) => callArkme("world.interactions.list", {
@@ -163001,21 +162998,28 @@ html body {
163001
162998
  });
163002
162999
  });
163003
163000
  worldItems.sort((left, right) => right.atMillis - left.atMillis);
163004
- if (interactionResults.some((result) => result.status === "rejected") && worldError === void 0) worldError = tr$1("部分世界互动暂时无法加载");
163005
- } else if (worldError === void 0) worldError = worldFeedResult.reason instanceof Error ? worldFeedResult.reason.message : tr$1("世界互动暂时无法加载");
163006
- let aiUnreadCount = 0;
163007
- let aiLatestUnread;
163001
+ if (interactionResults.some((result) => result.status === "rejected")) {
163002
+ worldError ??= tr$1("部分世界互动暂时无法加载");
163003
+ if (this.snapshot.worldItems.length > 0) worldItems = this.snapshot.worldItems;
163004
+ }
163005
+ } else {
163006
+ worldItems = this.snapshot.worldItems;
163007
+ worldError ??= worldFeedResult.reason instanceof Error ? worldFeedResult.reason.message : tr$1("世界互动暂时无法加载");
163008
+ }
163009
+ let aiUnreadCount = this.snapshot.aiUnreadCount;
163010
+ let aiLatestUnread = this.snapshot.aiLatestUnread;
163008
163011
  let aiError;
163009
163012
  if (aiUnreadResult.status === "fulfilled") {
163010
163013
  aiUnreadCount = Math.max(0, Math.trunc(aiUnreadResult.value.unreadCount));
163011
163014
  aiLatestUnread = aiUnreadResult.value.latestUnread;
163012
163015
  } else aiError = aiUnreadResult.reason instanceof Error ? aiUnreadResult.reason.message : tr$1("AI 来信暂时无法加载");
163013
- let aiItems = aiListResult.status === "fulfilled" ? (Array.isArray(aiListResult.value?.items) ? aiListResult.value.items : []).map(aiLetterItem).sort((left, right) => right.atMillis - left.atMillis) : [];
163016
+ let aiItems = aiListResult.status === "fulfilled" ? (Array.isArray(aiListResult.value?.items) ? aiListResult.value.items : []).map(aiLetterItem).sort((left, right) => right.atMillis - left.atMillis) : this.snapshot.aiItems;
163014
163017
  if (aiItems.length === 0 && aiLatestUnread !== void 0) aiItems = [aiLetterItem(aiLatestUnread)];
163015
163018
  if (aiListResult.status === "rejected" && aiError === void 0) aiError = aiListResult.reason instanceof Error ? aiListResult.reason.message : tr$1("AI 来信暂时无法加载");
163016
163019
  this.publish({
163017
163020
  ...this.snapshot,
163018
163021
  scope,
163022
+ ready: true,
163019
163023
  loading: false,
163020
163024
  error,
163021
163025
  arrangementItems,
@@ -163091,11 +163095,11 @@ html body {
163091
163095
  function useNotificationItems() {
163092
163096
  const auth = (0, react$1.useSyncExternalStore)(arkmeAuthStore.subscribe, arkmeAuthStore.getSnapshot, arkmeAuthStore.getSnapshot).auth;
163093
163097
  const scope = auth?.status === "authenticated" ? `${auth.environment}:${auth.userId}` : void 0;
163094
- const snapshot = (0, react$1.useSyncExternalStore)(arkmeNotificationStore.subscribe, arkmeNotificationStore.getSnapshot, arkmeNotificationStore.getSnapshot);
163098
+ const storedSnapshot = (0, react$1.useSyncExternalStore)(arkmeNotificationStore.subscribe, arkmeNotificationStore.getSnapshot, arkmeNotificationStore.getSnapshot);
163099
+ const snapshot = storedSnapshot.scope === scope ? storedSnapshot : EMPTY_SNAPSHOT;
163095
163100
  const reactionRevision = (0, react$1.useSyncExternalStore)(reactionNotifications.subscribe, reactionNotifications.getSnapshot, reactionNotifications.getSnapshot);
163096
163101
  (0, react$1.useEffect)(() => {
163097
- if (scope === void 0) return;
163098
- const release = reactionNotifications.acquire(scope);
163102
+ const release = scope === void 0 ? void 0 : reactionNotifications.acquire(scope);
163099
163103
  arkmeNotificationStore.refresh(scope);
163100
163104
  return release;
163101
163105
  }, [scope]);
@@ -163136,7 +163140,7 @@ html body {
163136
163140
  const unread = items.filter((item) => item.unread);
163137
163141
  const latest = items[0];
163138
163142
  return {
163139
- ready: snapshot.scope !== void 0 && !snapshot.loading,
163143
+ ready: snapshot.scope !== void 0 && snapshot.ready,
163140
163144
  hasNotifications: items.length > 0 || snapshot.worldUnreadCount > 0 || snapshot.aiUnreadCount > 0,
163141
163145
  unreadCount: unread.length + snapshot.worldUnreadCount + snapshot.aiUnreadCount,
163142
163146
  preview: latest === void 0 ? tr$1("安排、互动和表态通知") : `${latest.title}:${latest.preview}`,
@@ -163160,17 +163164,6 @@ html body {
163160
163164
  padding: "0 20px",
163161
163165
  borderBottom: `1px solid ${arkmeTheme.borderSoft}`
163162
163166
  },
163163
- back: {
163164
- width: 32,
163165
- height: 32,
163166
- border: 0,
163167
- borderRadius: 8,
163168
- background: "transparent",
163169
- color: arkmeTheme.text,
163170
- font: "inherit",
163171
- fontSize: 22,
163172
- cursor: "pointer"
163173
- },
163174
163167
  title: {
163175
163168
  margin: 0,
163176
163169
  flex: 1,
@@ -163377,15 +163370,6 @@ html body {
163377
163370
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("header", {
163378
163371
  style: styles$33.header,
163379
163372
  children: [
163380
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
163381
- type: "button",
163382
- style: styles$33.back,
163383
- "aria-label": tr$1("返回 Arkme 会话列表"),
163384
- onClick: () => {
163385
- arkmeUi.showConversations();
163386
- },
163387
- children: "‹"
163388
- }),
163389
163373
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
163390
163374
  style: { color: arkmeTheme.accent },
163391
163375
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(NotificationBell, { size: 21 })
@@ -164986,14 +164970,21 @@ html body {
164986
164970
  });
164987
164971
  }
164988
164972
  function ArkmeNotificationsRow({ selected, onClick }) {
164973
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ArkmeNotificationRowContent, {
164974
+ selected,
164975
+ onClick,
164976
+ summary: useArkmeNotificationSummary()
164977
+ });
164978
+ }
164979
+ function ArkmeNotificationRowContent({ selected, onClick, summary }) {
164989
164980
  useArkmeLocale();
164990
- const summary = useArkmeNotificationSummary();
164991
164981
  if (!summary.ready || !summary.hasNotifications) return null;
164992
164982
  const time = timeLabel$3(summary.atMillis);
164993
164983
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
164994
164984
  type: "button",
164995
164985
  role: "treeitem",
164996
164986
  "aria-selected": selected,
164987
+ "data-arkme-directory-row": "notifications",
164997
164988
  "aria-label": summary.unreadCount > 0 ? tr$1("通知,{v0} 条未读", { v0: String(summary.unreadCount) }) : tr$1("通知"),
164998
164989
  style: {
164999
164990
  ...styles$32.chatRow,
@@ -165559,6 +165550,7 @@ html body {
165559
165550
  const recordRevision = (0, react$1.useSyncExternalStore)(arkmeUi.subscribe, arkmeUi.getRecordRevision, arkmeUi.getRecordRevision);
165560
165551
  const chatRevision = (0, react$1.useSyncExternalStore)(arkmeUi.subscribe, arkmeUi.getChatRevision, arkmeUi.getChatRevision);
165561
165552
  const authState = (0, react$1.useSyncExternalStore)(arkmeAuthStore.subscribe, arkmeAuthStore.getSnapshot, arkmeAuthStore.getSnapshot);
165553
+ const notificationSummary = useArkmeNotificationSummary();
165562
165554
  const chatDirectory = (0, react$1.useSyncExternalStore)(arkmeChatDirectory.subscribe, arkmeChatDirectory.getSnapshot, arkmeChatDirectory.getSnapshot);
165563
165555
  const conversationProjection = (0, react$1.useSyncExternalStore)(arkmeChatDirectory.subscribe, arkmeChatDirectory.getConversationSnapshot, arkmeChatDirectory.getConversationSnapshot);
165564
165556
  const notificationActivation = (0, react$1.useSyncExternalStore)(arkmeNotificationActivation.subscribe, arkmeNotificationActivation.getSnapshot, arkmeNotificationActivation.getSnapshot);
@@ -165783,6 +165775,11 @@ html body {
165783
165775
  scope: currentAccountKey,
165784
165776
  enabled: active && directory === "root"
165785
165777
  });
165778
+ const rootDirectoryRows = [...removalFeedback.rows];
165779
+ if (authenticated && notificationSummary.ready && notificationSummary.hasNotifications) {
165780
+ const index = removalFeedback.rows.findIndex((row) => !row.pinned && row.activeAtMillis < notificationSummary.atMillis);
165781
+ rootDirectoryRows.splice(index < 0 ? rootDirectoryRows.length : index, 0, { kind: "notifications" });
165782
+ }
165786
165783
  const directoryContextMenu = (0, react$1.useMemo)(() => {
165787
165784
  if (directoryContextTarget === void 0) return void 0;
165788
165785
  const row = rootConversationRows.find((row) => row.kind === directoryContextTarget.kind && (row.kind === "source" ? arkmeSourceIdentityKey(row.source) : conversationBotVisibilityKey(row.bot)) === directoryContextTarget.key);
@@ -167024,9 +167021,10 @@ html body {
167024
167021
  "aria-label": directory === "send_to_self" ? "发给自己分类" : tr$1("Arkme 会话"),
167025
167022
  children: [
167026
167023
  directory === "root" && authenticated && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ArkmeNotificationPermissionBanner, {}),
167027
- authenticated && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ArkmeNotificationsRow, {
167024
+ directory !== "root" && authenticated && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ArkmeNotificationRowContent, {
167028
167025
  selected: ui.mode === "notifications",
167029
- onClick: showNotifications
167026
+ onClick: showNotifications,
167027
+ summary: notificationSummary
167030
167028
  }),
167031
167029
  directory === "root" && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
167032
167030
  showHarnessEntry && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(DeepSeekHarnessRow, {
@@ -167138,8 +167136,13 @@ html body {
167138
167136
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ArkmeConversationRemovalStyles, {}),
167139
167137
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ArkmeDirectoryWindow, {
167140
167138
  revealKey: unreadJumpTarget?.key,
167141
- activeKey: ui.mode === "source" && ui.selectedSource !== void 0 ? arkmeSourceIdentityKey(ui.selectedSource) : ui.mode === "bot" && ui.selectedBot !== void 0 ? conversationBotVisibilityKey(ui.selectedBot) : void 0,
167142
- children: removalFeedback.rows.map((row) => {
167139
+ activeKey: ui.mode === "notifications" ? "notifications" : ui.mode === "source" && ui.selectedSource !== void 0 ? arkmeSourceIdentityKey(ui.selectedSource) : ui.mode === "bot" && ui.selectedBot !== void 0 ? conversationBotVisibilityKey(ui.selectedBot) : void 0,
167140
+ children: rootDirectoryRows.map((row) => {
167141
+ if (row.kind === "notifications") return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ArkmeNotificationRowContent, {
167142
+ selected: activeDirectoryEntryId === void 0 && ui.mode === "notifications",
167143
+ onClick: showNotifications,
167144
+ summary: notificationSummary
167145
+ }, "notifications");
167143
167146
  if (row.kind === "bot") {
167144
167147
  const { bot } = row;
167145
167148
  const removalPhase = removalFeedback.phases.get(`bot:${conversationBotVisibilityKey(bot)}`);
@@ -213906,7 +213909,7 @@ registerProcessor('arkme-pcm-journal', ArkmePcmJournal);
213906
213909
  const c$3 = e$20;
213907
213910
  //#endregion
213908
213911
  //#region package.json
213909
- var version = "0.1.89";
213912
+ var version = "0.1.90";
213910
213913
  //#endregion
213911
213914
  //#region src/client/arkme-membership.ts
213912
213915
  function membershipLabel(state) {