فهرست منبع

发布 v0.3.32:统一 Web 和移动端聊天工作目录

LSamien 1 ماه پیش
والد
کامیت
9c8fb14038
8فایلهای تغییر یافته به همراه35 افزوده شده و 8 حذف شده
  1. 1 1
      README.md
  2. 10 0
      docs/releases/v0.3.32.md
  3. 2 2
      package-lock.json
  4. 1 1
      package.json
  5. 3 3
      release.json
  6. 5 0
      tests/e2e/workspace-chat.spec.ts
  7. 2 0
      tests/fixtures/fake-hermes.mjs
  8. 11 1
      tests/fixtures/workspace-server.ts

+ 1 - 1
README.md

@@ -1,6 +1,6 @@
 # 夭夭 Web
 
-当前发布版本:**v0.3.31**,配套 iOS **1.3(188)**。详见 [发布说明](docs/releases/v0.3.31.md)。
+当前发布版本:**v0.3.32**,配套 iOS **1.3(188)**。详见 [发布说明](docs/releases/v0.3.32.md)。
 
 夭夭 Web 为网页、iOS 和安卓提供统一的 Agent、群聊、文件库及语音配置服务。Hermes 通过标准 9119 HTTP/WebSocket 接口执行任务。
 

+ 10 - 0
docs/releases/v0.3.32.md

@@ -0,0 +1,10 @@
+# v0.3.32
+
+- 统一 Web 和移动端聊天的工作目录,使用所选基础 Hermes Profile 的 `terminal.cwd`,包括普通对话、Bot 单聊和群聊。
+- 新会话创建时传入配置目录;旧会话在下一次空闲发送或上传附件前纠正目录,不再优先沿用历史路径。
+- 配置变更在后续空闲执行时生效。正在执行的回复不切换目录;目录已一致时不重建终端。
+- 配置读取失败或 Hermes 无法切换到该目录时停止提交,避免静默在错误目录执行。远端 Agent 使用目标服务自己的配置和授权身份。
+
+本次修复由 Web 服务端统一执行。升级 Web 后,已有移动端即可使用同一规则,无需迁移历史文件或修改全局配置。原生 Hermes Bot 入口仍使用其自身工作区管理。
+
+`terminal.cwd` 的 `.`、`~` 和相对路径由实际执行的 Hermes 主机解析;需要固定目录时请填写该主机上的绝对路径。详见[聊天工作目录规则](../chat-working-directory.md)。

+ 2 - 2
package-lock.json

@@ -1,12 +1,12 @@
 {
   "name": "hermes-yaoyao",
-  "version": "0.3.31",
+  "version": "0.3.32",
   "lockfileVersion": 3,
   "requires": true,
   "packages": {
     "": {
       "name": "hermes-yaoyao",
-      "version": "0.3.31",
+      "version": "0.3.32",
       "license": "BSL-1.1",
       "dependencies": {
         "@koa/bodyparser": "^5.0.0",

+ 1 - 1
package.json

@@ -1,6 +1,6 @@
 {
   "name": "hermes-yaoyao",
-  "version": "0.3.31",
+  "version": "0.3.32",
   "private": true,
   "license": "BSL-1.1",
   "description": "夭夭 Web:直连 Hermes 9119 的聊天、团队、文件库与产物工作台",

+ 3 - 3
release.json

@@ -1,6 +1,6 @@
 {
   "schemaVersion": 1,
-  "releaseVersion": "0.3.31",
-  "webVersion": "0.3.31",
-  "gitTag": "v0.3.31"
+  "releaseVersion": "0.3.32",
+  "webVersion": "0.3.32",
+  "gitTag": "v0.3.32"
 }

+ 5 - 0
tests/e2e/workspace-chat.spec.ts

@@ -62,6 +62,11 @@ test('an open Web chat list receives cross-client sessions and real title events
   const sessionId = decodeURIComponent(new URL(pageB.url()).pathname.split('/').at(-1) || '')
   expect(sessionId).not.toMatch(/^draft-/)
   await expect(pageB.locator('.message--assistant')).toContainText('会话独立保存')
+  const cwdCalls = await (await page.request.get('http://127.0.0.1:19120/__calls')).json()
+  const created = cwdCalls.calls.filter((call: { method: string }) => call.method === 'session.create').at(-1)
+  expect(created.params.cwd).toBe('/tmp/hermes-fixture/default')
+  const aligned = cwdCalls.calls.filter((call: { method: string }) => call.method === 'session.cwd.set').at(-1)
+  expect(aligned.params.cwd).toBe('/tmp/hermes-fixture/default')
 
   const sessionRow = page.locator(`.desktop-sidebar .sidebar-item[data-sidebar-id="${sessionId}"]`)
   await expect(sessionRow).toContainText(initialTitle)

+ 2 - 0
tests/fixtures/fake-hermes.mjs

@@ -199,6 +199,7 @@ const server = createServer(async (request, response) => {
   }
   if (url.pathname === '/auth/logout' && request.method === 'POST') return json(response, 200, { ok: true }, { 'Set-Cookie': 'fake_session=; Path=/; Max-Age=0; HttpOnly; SameSite=Lax' })
   if (!authenticated(request)) return json(response, 401, { detail: 'Unauthorized' })
+  if (url.pathname === '/api/config') return json(response, 200, { terminal: { cwd: '/tmp/hermes-fixture' } })
   if (url.pathname === '/api/files/download' && request.method === 'GET') {
     const path = url.searchParams.get('path')
     if (![...profileMediaPaths, ...userProfileMediaPaths].includes(path)) return json(response, 404, { detail: 'File not found' })
@@ -478,6 +479,7 @@ server.on('upgrade', (request, socket, head) => {
       const requestFrame = JSON.parse(raw.toString())
       rpcRequests.push(requestFrame)
       const respond = result => client.send(JSON.stringify({ jsonrpc: '2.0', id: requestFrame.id, result }))
+      if (requestFrame.method === 'session.cwd.set') return respond({ cwd: requestFrame.params.cwd })
       if (requestFrame.method === 'session.resume') return respond({ session_id: 'runtime-demo', stored_session_id: requestFrame.params.session_id, fast: requestFrame.params.session_id === 'session-yaoer', messages: [] })
       if (requestFrame.method === 'session.create') return respond({ session_id: 'runtime-new', stored_session_id: 'session-new', fast: requestFrame.params.fast === true })
       if (requestFrame.method === 'session.usage') return respond({ context_used: 12500, context_max: 114688, total: 12500, input: 9000, output: 3500 })

+ 11 - 1
tests/fixtures/workspace-server.ts

@@ -22,6 +22,7 @@ const sessions = new Map<
     title: string
     messages: Array<{ role: string; content: string; id: string; timestamp: number }>
     running: boolean
+    cwd?: string
   }
 >()
 const latestRuntimeBySession = new Map<string, string>()
@@ -55,6 +56,10 @@ const upstream = createServer((req, res) => {
   const url = new URL(req.url || '/', `http://127.0.0.1:${upstreamPort}`)
   res.setHeader('content-type', 'application/json')
   const send = (v: unknown) => res.end(JSON.stringify(v))
+  if (url.pathname === '/api/config') {
+    send({ terminal: { cwd: `/tmp/hermes-fixture/${url.searchParams.get('profile') || 'default'}` } })
+    return
+  }
   if (url.pathname === '/__release' && req.method === 'POST') {
     const held = heldReplies.splice(0)
     for (const complete of held) complete()
@@ -218,11 +223,16 @@ wss.on('connection', (socket) => {
         stored_session_id: stored.id,
         session_key: stored.id,
         running: stored.running,
-        info: { profile_name: stored.profile },
+        info: { profile_name: stored.profile, cwd: stored.cwd },
       })
       return
     }
     const stored = sessions.get(runtimes.get(f.params.session_id) ?? '')
+    if (f.method === 'session.cwd.set' && stored) {
+      stored.cwd = String(f.params.cwd)
+      respond({ cwd: stored.cwd })
+      return
+    }
     if (f.method === 'prompt.submit' && stored) {
       stored.running = true
       const requestedTitle = /^\[cross-client-title:([^\]\r\n]+)\]/