Sfoglia il codice sorgente

完善停止兼容说明与 Node 26 回归测试环境

Samien 1 mese fa
parent
commit
cdad1cb528

+ 1 - 0
docs/runner.md

@@ -49,6 +49,7 @@ macOS App 可直接通过菜单栏“执行节点 → 导入节点配置…”
 - 本机 SQLite 保存命令指纹和执行回执。重复命令复用回执;未完成的历史命令视为结果不确定,禁止自动重放。大响应一小时后压缩为禁止重放的记录。
 - 组队工具只在 Hermes 本机建立回环租约,远端不开放原有回环接口。模型获得的是当轮不透明工具 ID;账号、来源、会话和调用身份由服务端绑定。
 - Gateway RPC 使用固定允许列表;会话操作必须属于自己的通道。HTTP 仅允许必要的只读查询。文件导出要求真实路径位于 `artifactRoots`,留空拒绝导出。
+- 停止任务须通过 `session.active_list` 确认原会话已空闲或退出;Runner 只返回当前通道拥有的会话 ID 与状态。请与控制面一起更新 Runner;旧 Runner 无法确认停止时,系统保留待清理状态并拒绝复用原执行。电脑执行停止超时也保留控制权和持久停止记录,确认停止后才释放。
 - 单个只读结果目前最多 8 MiB;大型产物的分块传输仍待实现。待传命令有数量和总字节限制。
 - 断线后的在途操作可能结果不确定。系统沿用原会话核对,不在另一个节点重放原任务。已完成会话切换节点后带入近期文字记录;历史附件路径需要重新核实。
 - 托管浏览器通过节点能力协商使用会话保留协议。普通 Bot 的持久上下文在正常任务结束或无任务的人工交还后暂停联网,下一轮使用或接管取得新授权后恢复;独立会话授权只保留资源,不继续授予已结束任务的操作权限。默认空闲回收为 30 分钟,状态和截图轮询不会续期。

+ 9 - 2
tests/server/workspaceMessagePatch.test.ts

@@ -13,7 +13,7 @@ import type { WorkspaceConversation, WorkspaceEvent, WorkspaceMessage } from '..
 import type { LocalAuthStore } from '../../src/server/localAuth'
 
 let home: string, store: WorkspaceStore, message: WorkspaceMessage, server: Server
-const auth = { require: () => ({ id: 'owner' }), isUserActive: () => true, pushAuthorizationVersion: () => 1 } as unknown as LocalAuthStore
+const auth = { require: () => ({ id: 'owner' }), currentFromCookieHeader: () => ({ id: 'owner' }), isUserActive: () => true, pushAuthorizationVersion: () => 1 } as unknown as LocalAuthStore
 beforeEach(async () => {
   home = mkdtempSync(join(tmpdir(), 'workspace-patches-'))
   store = new WorkspaceStore(home, { messagePatches: true })
@@ -87,7 +87,14 @@ it('sends a full live baseline then patches, with ordered legacy full frames and
     const response = await fetch(url(format), { headers: { 'Last-Event-ID': String(cursor) }, signal: controller.signal })
     const reader = response.body!.getReader(), decoder = new TextDecoder()
     let text = ''
-    const until = async (needle: string) => { while (!text.includes(needle)) text += decoder.decode((await reader.read()).value); return text }
+    const until = async (needle: string) => {
+      while (!text.includes(needle)) {
+        const chunk = await reader.read()
+        if (chunk.done) throw new Error(`Stream ended before ${needle}`)
+        text += decoder.decode(chunk.value)
+      }
+      return text
+    }
     await until('event: ready')
     return { controller, reader, until }
   }

+ 1 - 1
tests/server/workspaceRoutineTools.test.ts

@@ -82,7 +82,7 @@ it('tells every Bot not to use Hermes cron, and explains its own routine tools w
     members: [{ id: 'bot', name: '助手' }],
     work: { depth: 0, requiredReply: false, replyMode: 'mentioned', messageId: 'message' },
     run: { mentionIds: [] },
-    environment: { open: { computer: false, server: false, vm: false, cloud: false }, tools: { desktopView: false, desktopFile: false, vm: false, cloud: false, plugins: false }, version: 1, capturedAt: 1, execution: { nodeId: 'local', profile: 'dev' }, desktop: { capturedAt: 1, hosts: [] }, virtual: { vm: { status: 'disabled' }, cloud: { status: 'disabled' } }, fileTransferMaxMiB: 25 },
+    environment: { open: { computer: false, server: false, vm: false, cloud: false }, tools: { desktopView: false, desktopFile: false, vm: false, cloud: false, plugins: false }, version: 1, capturedAt: 1, execution: { nodeId: 'local', profile: 'dev' }, runtime: { connected: false }, desktop: { capturedAt: 1, hosts: [] }, virtual: { vm: { status: 'disabled' }, cloud: { status: 'disabled' } }, fileTransferMaxMiB: 25 },
     memory: '', noReply: '[[YAOYAO_NO_REPLY_V1]]', marker: '[yaoyao-run:run:result]', content: '每天提醒我', contentKind: 'user', attachmentRefs: [],
   } as WorkspacePromptInput
   expect(buildWorkspacePrompt(fixture)).toContain('不要调用 Hermes 的 cronjob')

+ 9 - 1
tests/setup.ts

@@ -1,4 +1,12 @@
-import { afterEach } from 'vitest'
+import { afterEach, beforeEach } from 'vitest'
+
+beforeEach(() => {
+  // Node's native storage globals can shadow jsdom's browser storage.
+  const browser = (globalThis as typeof globalThis & { jsdom?: { window: Window } }).jsdom?.window
+  if (!browser) return
+  for (const key of ['localStorage', 'sessionStorage'] as const)
+    Object.defineProperty(globalThis, key, { configurable: true, writable: true, value: browser[key] })
+})
 
 afterEach(() => {
   if (typeof document !== 'undefined') document.body.innerHTML = ''