Jelajahi Sumber

首次安装时随机迁移已有默认 Agent 头像

LSamien 1 bulan lalu
induk
melakukan
40cac88a24
7 mengubah file dengan 43 tambahan dan 10 penghapusan
  1. 1 1
      README.md
  2. 10 0
      docs/releases/v0.3.19.md
  3. 2 2
      package-lock.json
  4. 1 1
      package.json
  5. 3 3
      release.json
  6. 19 0
      src/server/workspaceStore.ts
  7. 7 3
      tests/server/workspace.test.ts

+ 1 - 1
README.md

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

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

@@ -0,0 +1,10 @@
+# v0.3.19
+
+补齐已有 Agent 首次安装头像规则。
+
+- 已有 Agent 如果仍是系统生成的绿色圆形默认头像,会在安装此版本时一次性分配随机形状/造型、颜色和基础表情。
+- 已上传的照片和已自定义的 v2 头像保持原样。
+- 随机结果写入 Agent 记录及对应单聊快照;迁移完成后记录独立标记,重启不会再次变化。
+- 新建 Agent 继续使用 v0.3.18 的创建时随机头像规则。
+
+配套 iOS 继续为 1.3(177),无需插件升级。

+ 2 - 2
package-lock.json

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

+ 1 - 1
package.json

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

+ 3 - 3
release.json

@@ -1,6 +1,6 @@
 {
   "schemaVersion": 1,
-  "releaseVersion": "0.3.18",
-  "webVersion": "0.3.18",
-  "gitTag": "v0.3.18"
+  "releaseVersion": "0.3.19",
+  "webVersion": "0.3.19",
+  "gitTag": "v0.3.19"
 }

+ 19 - 0
src/server/workspaceStore.ts

@@ -120,6 +120,25 @@ export class WorkspaceStore {
       }
       this.db.prepare('INSERT INTO workspace_migrations VALUES(?)').run('avatar-v2')
     })
+    this.atomic(() => {
+      if (this.db.prepare('SELECT id FROM workspace_migrations WHERE id=?').get('avatar-random-default-v1')) return
+      for (const owner of this.owners()) {
+        for (const agent of this.list<Agent>(owner, 'agent')) {
+          const identity = decodeAgentMascotAvatar(agent.avatar)
+          const isSystemDefault = identity && !identity.imageDataURL && identity.shape === 'circle'
+            && identity.color === '#00c875' && identity.expression === 'idle' && !identity.bodyId
+          if (!isSystemDefault) continue
+          const avatar = encodeAgentAvatar(randomAgentIdentity(agent.id, agent.name))
+          this.put(owner, 'agent', agent.id, { ...agent, avatar })
+          for (const conversation of this.list<Conversation>(owner, 'conversation')) {
+            if (conversation.kind === 'direct' && conversation.memberIds[0] === agent.id) {
+              this.put(owner, 'conversation', conversation.id, { ...conversation, avatar })
+            }
+          }
+        }
+      }
+      this.db.prepare('INSERT INTO workspace_migrations VALUES(?)').run('avatar-random-default-v1')
+    })
   }
   get<T>(owner: string, kind: string, id: string): T | undefined {
     const row = this.db

+ 7 - 3
tests/server/workspace.test.ts

@@ -876,17 +876,20 @@ it('marks a stopped administrator as interrupted without sending a false complet
   expect(notifications).toEqual([])
 })
 
-it('migrates old internal avatars and photos once, synchronizing direct snapshots without changing conversations',()=>{
+it('randomizes old default avatars once while preserving photos, custom v2 settings and direct snapshots',()=>{
   const a=store.createAgent(owner,{name:'旧头像',profile:'default',instructions:'保留规则'})
   const photo='data:image/png;base64,aGVsbG8='
   const b=store.createAgent(owner,{name:'照片',profile:'default',avatar:photo})
   store.put(owner,'agent',a.id,{...a,avatar:'yaoyao-mascot:v1:triangle:ff0000:friendly'})
   store.put(owner,'agent',b.id,{...b,avatar:photo})
-  store.db.prepare('DELETE FROM workspace_migrations WHERE id=?').run('avatar-v2')
+  store.db.prepare("DELETE FROM workspace_migrations WHERE id IN ('avatar-v2','avatar-random-default-v1')").run()
+  const random=vi.spyOn(Math,'random').mockReturnValue(0)
   const migrated=new WorkspaceStore(home)
+  random.mockRestore()
   const next=migrated.require<any>(owner,'agent',a.id)
   expect(next.instructions).toBe('保留规则')
-  expect(decodeAgentAvatar(next.avatar)).toMatchObject({shape:'circle',color:'#00c875',expression:'idle'})
+  expect(decodeAgentAvatar(next.avatar)).toMatchObject({avatarMode:'mascot'})
+  expect(next.avatar).not.toBe(encodeAgentAvatar(defaultAgentIdentity(a.id,a.name)))
   const image=migrated.require<any>(owner,'agent',b.id)
   expect(decodeAgentAvatar(image.avatar)).toMatchObject({avatarMode:'image',imageDataURL:photo,imageCrop:'rounded'})
   for(const c of migrated.list<WorkspaceConversation>(owner,'conversation')) expect(c.avatar).toBe(migrated.require<any>(owner,'agent',c.memberIds[0]!).avatar)
@@ -895,6 +898,7 @@ it('migrates old internal avatars and photos once, synchronizing direct snapshot
   migrated.close()
   const reopened=new WorkspaceStore(home)
   expect(reopened.require<any>(owner,'agent',a.id).avatar).toBe(customized)
+  expect(reopened.db.prepare("SELECT count(*) AS count FROM workspace_migrations WHERE id='avatar-random-default-v1'").get()!.count).toBe(1)
   reopened.close()
 })