agentshire 1.0.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 (406) hide show
  1. package/CHANGELOG.md +36 -0
  2. package/LICENSE +21 -0
  3. package/README.md +437 -0
  4. package/README.zh-CN.md +441 -0
  5. package/ROADMAP.md +207 -0
  6. package/THIRD_PARTY_NOTICES.md +49 -0
  7. package/VISION.md +101 -0
  8. package/index.ts +386 -0
  9. package/openclaw.plugin.json +27 -0
  10. package/package.json +100 -0
  11. package/src/bridge/AGENTS.md +136 -0
  12. package/src/bridge/ActivityStream.ts +184 -0
  13. package/src/bridge/CitizenManager.ts +280 -0
  14. package/src/bridge/DirectorBridge.ts +1076 -0
  15. package/src/bridge/EventTranslator.ts +142 -0
  16. package/src/bridge/NpcEventQueue.ts +61 -0
  17. package/src/bridge/ReconnectManager.ts +42 -0
  18. package/src/bridge/RouteManager.ts +270 -0
  19. package/src/bridge/StateTracker.ts +100 -0
  20. package/src/bridge/ToolVfxMapper.ts +94 -0
  21. package/src/bridge/__tests__/ActivityStream.test.ts +156 -0
  22. package/src/bridge/__tests__/CitizenManager.test.ts +174 -0
  23. package/src/bridge/__tests__/EventTranslator.test.ts +292 -0
  24. package/src/bridge/__tests__/NpcEventQueue.test.ts +92 -0
  25. package/src/bridge/__tests__/RouteManager.test.ts +106 -0
  26. package/src/bridge/data/route-config.json +36 -0
  27. package/src/bridge/data/route-config.ts +19 -0
  28. package/src/bridge/implicit-chat.ts +192 -0
  29. package/src/bridge/index.ts +12 -0
  30. package/src/contracts/agent-state.ts +87 -0
  31. package/src/contracts/agui.ts +212 -0
  32. package/src/contracts/chat.ts +67 -0
  33. package/src/contracts/events.ts +170 -0
  34. package/src/contracts/index.ts +71 -0
  35. package/src/contracts/media.ts +77 -0
  36. package/src/contracts/registry.ts +32 -0
  37. package/src/plugin/AGENTS.md +192 -0
  38. package/src/plugin/__tests__/editor-serve-megapack.test.ts +150 -0
  39. package/src/plugin/__tests__/hook-translator.test.ts +197 -0
  40. package/src/plugin/auto-config.ts +131 -0
  41. package/src/plugin/channel.ts +399 -0
  42. package/src/plugin/chat-asset-resolver.ts +75 -0
  43. package/src/plugin/chat-session-watcher.ts +98 -0
  44. package/src/plugin/citizen-agent-manager.ts +178 -0
  45. package/src/plugin/citizen-chat-router.ts +83 -0
  46. package/src/plugin/citizen-workshop-manager.ts +96 -0
  47. package/src/plugin/custom-asset-manager.ts +250 -0
  48. package/src/plugin/editor-serve.ts +1368 -0
  49. package/src/plugin/group-discussion.ts +223 -0
  50. package/src/plugin/hook-translator.ts +202 -0
  51. package/src/plugin/llm-agent-proxy.ts +193 -0
  52. package/src/plugin/outbound-adapter.ts +187 -0
  53. package/src/plugin/paths.ts +33 -0
  54. package/src/plugin/plan-manager.ts +594 -0
  55. package/src/plugin/runtime.ts +9 -0
  56. package/src/plugin/session-history.ts +838 -0
  57. package/src/plugin/session-log-watcher.ts +221 -0
  58. package/src/plugin/soul-prompt-template.ts +290 -0
  59. package/src/plugin/subagent-tracker.ts +205 -0
  60. package/src/plugin/tools.ts +493 -0
  61. package/src/plugin/town-session.ts +40 -0
  62. package/src/plugin/ws-server.ts +680 -0
  63. package/src/town-souls.ts +134 -0
  64. package/town-frontend/dist/assets/CustomAssetStore-oi8aIurt.js +2 -0
  65. package/town-frontend/dist/assets/GLTFLoader-BA5RqSME.js +3804 -0
  66. package/town-frontend/dist/assets/OrbitControls-ZmySp9sQ.js +2 -0
  67. package/town-frontend/dist/assets/SettingsPanel-BO52reJe.js +2 -0
  68. package/town-frontend/dist/assets/SkeletonUtils-BCVmgslc.js +2 -0
  69. package/town-frontend/dist/assets/SkillLearnCard-Dk38iDpy.js +6 -0
  70. package/town-frontend/dist/assets/TopicSetupPanel-DLaLHB_Z.js +2 -0
  71. package/town-frontend/dist/assets/Trap-Bold-CT0JBE39.woff2 +0 -0
  72. package/town-frontend/dist/assets/Trap-SemiBold-R4_-Ld0j.woff2 +0 -0
  73. package/town-frontend/dist/assets/WeatherSystem-Cb3BvHes.js +550 -0
  74. package/town-frontend/dist/assets/avatars/char-female-a.webp +0 -0
  75. package/town-frontend/dist/assets/avatars/char-female-b.webp +0 -0
  76. package/town-frontend/dist/assets/avatars/char-female-c.webp +0 -0
  77. package/town-frontend/dist/assets/avatars/char-female-d.webp +0 -0
  78. package/town-frontend/dist/assets/avatars/char-female-e.webp +0 -0
  79. package/town-frontend/dist/assets/avatars/char-female-f.webp +0 -0
  80. package/town-frontend/dist/assets/avatars/char-male-a.webp +0 -0
  81. package/town-frontend/dist/assets/avatars/char-male-b.webp +0 -0
  82. package/town-frontend/dist/assets/avatars/char-male-c.webp +0 -0
  83. package/town-frontend/dist/assets/avatars/char-male-d.webp +0 -0
  84. package/town-frontend/dist/assets/avatars/char-male-e.webp +0 -0
  85. package/town-frontend/dist/assets/avatars/char-male-f.webp +0 -0
  86. package/town-frontend/dist/assets/avatars/char-pet-beaver.webp +0 -0
  87. package/town-frontend/dist/assets/avatars/char-pet-bee.webp +0 -0
  88. package/town-frontend/dist/assets/avatars/char-pet-bunny.webp +0 -0
  89. package/town-frontend/dist/assets/avatars/char-pet-cat.webp +0 -0
  90. package/town-frontend/dist/assets/avatars/char-pet-caterpillar.webp +0 -0
  91. package/town-frontend/dist/assets/avatars/char-pet-chick.webp +0 -0
  92. package/town-frontend/dist/assets/avatars/char-pet-cow.webp +0 -0
  93. package/town-frontend/dist/assets/avatars/char-pet-crab.webp +0 -0
  94. package/town-frontend/dist/assets/avatars/char-pet-deer.webp +0 -0
  95. package/town-frontend/dist/assets/avatars/char-pet-dog.webp +0 -0
  96. package/town-frontend/dist/assets/avatars/char-pet-elephant.webp +0 -0
  97. package/town-frontend/dist/assets/avatars/char-pet-fish.webp +0 -0
  98. package/town-frontend/dist/assets/avatars/char-pet-fox.webp +0 -0
  99. package/town-frontend/dist/assets/avatars/char-pet-giraffe.webp +0 -0
  100. package/town-frontend/dist/assets/avatars/char-pet-hog.webp +0 -0
  101. package/town-frontend/dist/assets/avatars/char-pet-koala.webp +0 -0
  102. package/town-frontend/dist/assets/avatars/char-pet-lion.webp +0 -0
  103. package/town-frontend/dist/assets/avatars/char-pet-monkey.webp +0 -0
  104. package/town-frontend/dist/assets/avatars/char-pet-panda.webp +0 -0
  105. package/town-frontend/dist/assets/avatars/char-pet-parrot.webp +0 -0
  106. package/town-frontend/dist/assets/avatars/char-pet-penguin.webp +0 -0
  107. package/town-frontend/dist/assets/avatars/char-pet-pig.webp +0 -0
  108. package/town-frontend/dist/assets/avatars/char-pet-polar.webp +0 -0
  109. package/town-frontend/dist/assets/avatars/char-pet-tiger.webp +0 -0
  110. package/town-frontend/dist/assets/character-catalog-DSmLtlNC.js +2 -0
  111. package/town-frontend/dist/assets/citizenEditor-DubGSJOQ.js +296 -0
  112. package/town-frontend/dist/assets/command-parser-BUd15Bmv.js +12 -0
  113. package/town-frontend/dist/assets/editor-B5QO0OtX.js +258 -0
  114. package/town-frontend/dist/assets/editor-Bk8g1NCD.css +1 -0
  115. package/town-frontend/dist/assets/index-BWfrufil.js +2 -0
  116. package/town-frontend/dist/assets/index-faS20RJk.js +6 -0
  117. package/town-frontend/dist/assets/logo-DJI6EtST.png +0 -0
  118. package/town-frontend/dist/assets/logo-title-AdKPZX5E.png +0 -0
  119. package/town-frontend/dist/assets/main-CqsN43aT.js +224 -0
  120. package/town-frontend/dist/assets/main-D7neuy3w.css +1 -0
  121. package/town-frontend/dist/assets/models/buildings/base.bin +0 -0
  122. package/town-frontend/dist/assets/models/buildings/base.gltf +136 -0
  123. package/town-frontend/dist/assets/models/buildings/bench.bin +0 -0
  124. package/town-frontend/dist/assets/models/buildings/bench.gltf +136 -0
  125. package/town-frontend/dist/assets/models/buildings/box_A.bin +0 -0
  126. package/town-frontend/dist/assets/models/buildings/box_A.gltf +136 -0
  127. package/town-frontend/dist/assets/models/buildings/box_B.bin +0 -0
  128. package/town-frontend/dist/assets/models/buildings/box_B.gltf +136 -0
  129. package/town-frontend/dist/assets/models/buildings/building_A.bin +0 -0
  130. package/town-frontend/dist/assets/models/buildings/building_A.gltf +136 -0
  131. package/town-frontend/dist/assets/models/buildings/building_A_withoutBase.bin +0 -0
  132. package/town-frontend/dist/assets/models/buildings/building_A_withoutBase.gltf +136 -0
  133. package/town-frontend/dist/assets/models/buildings/building_B.bin +0 -0
  134. package/town-frontend/dist/assets/models/buildings/building_B.gltf +136 -0
  135. package/town-frontend/dist/assets/models/buildings/building_B_withoutBase.bin +0 -0
  136. package/town-frontend/dist/assets/models/buildings/building_B_withoutBase.gltf +136 -0
  137. package/town-frontend/dist/assets/models/buildings/building_C.bin +0 -0
  138. package/town-frontend/dist/assets/models/buildings/building_C.gltf +136 -0
  139. package/town-frontend/dist/assets/models/buildings/building_C_withoutBase.bin +0 -0
  140. package/town-frontend/dist/assets/models/buildings/building_C_withoutBase.gltf +136 -0
  141. package/town-frontend/dist/assets/models/buildings/building_D.bin +0 -0
  142. package/town-frontend/dist/assets/models/buildings/building_D.gltf +136 -0
  143. package/town-frontend/dist/assets/models/buildings/building_D_withoutBase.bin +0 -0
  144. package/town-frontend/dist/assets/models/buildings/building_D_withoutBase.gltf +136 -0
  145. package/town-frontend/dist/assets/models/buildings/building_E.bin +0 -0
  146. package/town-frontend/dist/assets/models/buildings/building_E.gltf +136 -0
  147. package/town-frontend/dist/assets/models/buildings/building_E_withoutBase.bin +0 -0
  148. package/town-frontend/dist/assets/models/buildings/building_E_withoutBase.gltf +136 -0
  149. package/town-frontend/dist/assets/models/buildings/building_F.bin +0 -0
  150. package/town-frontend/dist/assets/models/buildings/building_F.gltf +136 -0
  151. package/town-frontend/dist/assets/models/buildings/building_F_withoutBase.bin +0 -0
  152. package/town-frontend/dist/assets/models/buildings/building_F_withoutBase.gltf +136 -0
  153. package/town-frontend/dist/assets/models/buildings/building_G.bin +0 -0
  154. package/town-frontend/dist/assets/models/buildings/building_G.gltf +136 -0
  155. package/town-frontend/dist/assets/models/buildings/building_G_withoutBase.bin +0 -0
  156. package/town-frontend/dist/assets/models/buildings/building_G_withoutBase.gltf +136 -0
  157. package/town-frontend/dist/assets/models/buildings/building_H.bin +0 -0
  158. package/town-frontend/dist/assets/models/buildings/building_H.gltf +136 -0
  159. package/town-frontend/dist/assets/models/buildings/building_H_withoutBase.bin +0 -0
  160. package/town-frontend/dist/assets/models/buildings/building_H_withoutBase.gltf +136 -0
  161. package/town-frontend/dist/assets/models/buildings/bush.bin +0 -0
  162. package/town-frontend/dist/assets/models/buildings/bush.gltf +136 -0
  163. package/town-frontend/dist/assets/models/buildings/car_hatchback.bin +0 -0
  164. package/town-frontend/dist/assets/models/buildings/car_hatchback.gltf +442 -0
  165. package/town-frontend/dist/assets/models/buildings/car_police.bin +0 -0
  166. package/town-frontend/dist/assets/models/buildings/car_police.gltf +442 -0
  167. package/town-frontend/dist/assets/models/buildings/car_sedan.bin +0 -0
  168. package/town-frontend/dist/assets/models/buildings/car_sedan.gltf +442 -0
  169. package/town-frontend/dist/assets/models/buildings/car_stationwagon.bin +0 -0
  170. package/town-frontend/dist/assets/models/buildings/car_stationwagon.gltf +442 -0
  171. package/town-frontend/dist/assets/models/buildings/car_taxi.bin +0 -0
  172. package/town-frontend/dist/assets/models/buildings/car_taxi.gltf +442 -0
  173. package/town-frontend/dist/assets/models/buildings/citybits_texture.png +0 -0
  174. package/town-frontend/dist/assets/models/buildings/dumpster.bin +0 -0
  175. package/town-frontend/dist/assets/models/buildings/dumpster.gltf +136 -0
  176. package/town-frontend/dist/assets/models/buildings/firehydrant.bin +0 -0
  177. package/town-frontend/dist/assets/models/buildings/firehydrant.gltf +136 -0
  178. package/town-frontend/dist/assets/models/buildings/road_corner.bin +0 -0
  179. package/town-frontend/dist/assets/models/buildings/road_corner.gltf +136 -0
  180. package/town-frontend/dist/assets/models/buildings/road_corner_curved.bin +0 -0
  181. package/town-frontend/dist/assets/models/buildings/road_corner_curved.gltf +136 -0
  182. package/town-frontend/dist/assets/models/buildings/road_junction.bin +0 -0
  183. package/town-frontend/dist/assets/models/buildings/road_junction.gltf +136 -0
  184. package/town-frontend/dist/assets/models/buildings/road_straight.bin +0 -0
  185. package/town-frontend/dist/assets/models/buildings/road_straight.gltf +136 -0
  186. package/town-frontend/dist/assets/models/buildings/road_straight_crossing.bin +0 -0
  187. package/town-frontend/dist/assets/models/buildings/road_straight_crossing.gltf +136 -0
  188. package/town-frontend/dist/assets/models/buildings/road_tsplit.bin +0 -0
  189. package/town-frontend/dist/assets/models/buildings/road_tsplit.gltf +136 -0
  190. package/town-frontend/dist/assets/models/buildings/streetlight.bin +0 -0
  191. package/town-frontend/dist/assets/models/buildings/streetlight.gltf +136 -0
  192. package/town-frontend/dist/assets/models/buildings/trafficlight_A.bin +0 -0
  193. package/town-frontend/dist/assets/models/buildings/trafficlight_A.gltf +136 -0
  194. package/town-frontend/dist/assets/models/buildings/trafficlight_B.bin +0 -0
  195. package/town-frontend/dist/assets/models/buildings/trafficlight_B.gltf +136 -0
  196. package/town-frontend/dist/assets/models/buildings/trafficlight_C.bin +0 -0
  197. package/town-frontend/dist/assets/models/buildings/trafficlight_C.gltf +136 -0
  198. package/town-frontend/dist/assets/models/buildings/trash_A.bin +0 -0
  199. package/town-frontend/dist/assets/models/buildings/trash_A.gltf +136 -0
  200. package/town-frontend/dist/assets/models/buildings/trash_B.bin +0 -0
  201. package/town-frontend/dist/assets/models/buildings/trash_B.gltf +136 -0
  202. package/town-frontend/dist/assets/models/buildings/watertower.bin +0 -0
  203. package/town-frontend/dist/assets/models/buildings/watertower.gltf +136 -0
  204. package/town-frontend/dist/assets/models/characters/Textures/colormap.png +0 -0
  205. package/town-frontend/dist/assets/models/characters/character-female-a.glb +0 -0
  206. package/town-frontend/dist/assets/models/characters/character-female-b.glb +0 -0
  207. package/town-frontend/dist/assets/models/characters/character-female-c.glb +0 -0
  208. package/town-frontend/dist/assets/models/characters/character-female-d.glb +0 -0
  209. package/town-frontend/dist/assets/models/characters/character-female-e.glb +0 -0
  210. package/town-frontend/dist/assets/models/characters/character-female-f.glb +0 -0
  211. package/town-frontend/dist/assets/models/characters/character-male-a.glb +0 -0
  212. package/town-frontend/dist/assets/models/characters/character-male-b.glb +0 -0
  213. package/town-frontend/dist/assets/models/characters/character-male-c.glb +0 -0
  214. package/town-frontend/dist/assets/models/characters/character-male-d.glb +0 -0
  215. package/town-frontend/dist/assets/models/characters/character-male-e.glb +0 -0
  216. package/town-frontend/dist/assets/models/characters/character-male-f.glb +0 -0
  217. package/town-frontend/dist/assets/models/characters/character-pet-beaver.glb +0 -0
  218. package/town-frontend/dist/assets/models/characters/character-pet-bee.glb +0 -0
  219. package/town-frontend/dist/assets/models/characters/character-pet-bunny.glb +0 -0
  220. package/town-frontend/dist/assets/models/characters/character-pet-cat.glb +0 -0
  221. package/town-frontend/dist/assets/models/characters/character-pet-caterpillar.glb +0 -0
  222. package/town-frontend/dist/assets/models/characters/character-pet-chick.glb +0 -0
  223. package/town-frontend/dist/assets/models/characters/character-pet-cow.glb +0 -0
  224. package/town-frontend/dist/assets/models/characters/character-pet-crab.glb +0 -0
  225. package/town-frontend/dist/assets/models/characters/character-pet-deer.glb +0 -0
  226. package/town-frontend/dist/assets/models/characters/character-pet-dog.glb +0 -0
  227. package/town-frontend/dist/assets/models/characters/character-pet-elephant.glb +0 -0
  228. package/town-frontend/dist/assets/models/characters/character-pet-fish.glb +0 -0
  229. package/town-frontend/dist/assets/models/characters/character-pet-fox.glb +0 -0
  230. package/town-frontend/dist/assets/models/characters/character-pet-giraffe.glb +0 -0
  231. package/town-frontend/dist/assets/models/characters/character-pet-hog.glb +0 -0
  232. package/town-frontend/dist/assets/models/characters/character-pet-koala.glb +0 -0
  233. package/town-frontend/dist/assets/models/characters/character-pet-lion.glb +0 -0
  234. package/town-frontend/dist/assets/models/characters/character-pet-monkey.glb +0 -0
  235. package/town-frontend/dist/assets/models/characters/character-pet-panda.glb +0 -0
  236. package/town-frontend/dist/assets/models/characters/character-pet-parrot.glb +0 -0
  237. package/town-frontend/dist/assets/models/characters/character-pet-penguin.glb +0 -0
  238. package/town-frontend/dist/assets/models/characters/character-pet-pig.glb +0 -0
  239. package/town-frontend/dist/assets/models/characters/character-pet-polar.glb +0 -0
  240. package/town-frontend/dist/assets/models/characters/character-pet-tiger.glb +0 -0
  241. package/town-frontend/dist/assets/models/characters/colormap.png +0 -0
  242. package/town-frontend/dist/assets/models/furniture/armchair.bin +0 -0
  243. package/town-frontend/dist/assets/models/furniture/armchair.gltf +136 -0
  244. package/town-frontend/dist/assets/models/furniture/armchair_pillows.bin +0 -0
  245. package/town-frontend/dist/assets/models/furniture/armchair_pillows.gltf +136 -0
  246. package/town-frontend/dist/assets/models/furniture/bed_double_A.bin +0 -0
  247. package/town-frontend/dist/assets/models/furniture/bed_double_A.gltf +136 -0
  248. package/town-frontend/dist/assets/models/furniture/bed_double_B.bin +0 -0
  249. package/town-frontend/dist/assets/models/furniture/bed_double_B.gltf +136 -0
  250. package/town-frontend/dist/assets/models/furniture/bed_single_A.bin +0 -0
  251. package/town-frontend/dist/assets/models/furniture/bed_single_A.gltf +136 -0
  252. package/town-frontend/dist/assets/models/furniture/bed_single_B.bin +0 -0
  253. package/town-frontend/dist/assets/models/furniture/bed_single_B.gltf +136 -0
  254. package/town-frontend/dist/assets/models/furniture/book_set.bin +0 -0
  255. package/town-frontend/dist/assets/models/furniture/book_set.gltf +136 -0
  256. package/town-frontend/dist/assets/models/furniture/book_single.bin +0 -0
  257. package/town-frontend/dist/assets/models/furniture/book_single.gltf +136 -0
  258. package/town-frontend/dist/assets/models/furniture/cabinet_medium.bin +0 -0
  259. package/town-frontend/dist/assets/models/furniture/cabinet_medium.gltf +136 -0
  260. package/town-frontend/dist/assets/models/furniture/cabinet_medium_decorated.bin +0 -0
  261. package/town-frontend/dist/assets/models/furniture/cabinet_medium_decorated.gltf +136 -0
  262. package/town-frontend/dist/assets/models/furniture/cabinet_small.bin +0 -0
  263. package/town-frontend/dist/assets/models/furniture/cabinet_small.gltf +136 -0
  264. package/town-frontend/dist/assets/models/furniture/cabinet_small_decorated.bin +0 -0
  265. package/town-frontend/dist/assets/models/furniture/cabinet_small_decorated.gltf +136 -0
  266. package/town-frontend/dist/assets/models/furniture/cactus_medium_A.bin +0 -0
  267. package/town-frontend/dist/assets/models/furniture/cactus_medium_A.gltf +136 -0
  268. package/town-frontend/dist/assets/models/furniture/cactus_medium_B.bin +0 -0
  269. package/town-frontend/dist/assets/models/furniture/cactus_medium_B.gltf +136 -0
  270. package/town-frontend/dist/assets/models/furniture/cactus_small_A.bin +0 -0
  271. package/town-frontend/dist/assets/models/furniture/cactus_small_A.gltf +136 -0
  272. package/town-frontend/dist/assets/models/furniture/cactus_small_B.bin +0 -0
  273. package/town-frontend/dist/assets/models/furniture/cactus_small_B.gltf +136 -0
  274. package/town-frontend/dist/assets/models/furniture/chair_A.bin +0 -0
  275. package/town-frontend/dist/assets/models/furniture/chair_A.gltf +136 -0
  276. package/town-frontend/dist/assets/models/furniture/chair_A_wood.bin +0 -0
  277. package/town-frontend/dist/assets/models/furniture/chair_A_wood.gltf +136 -0
  278. package/town-frontend/dist/assets/models/furniture/chair_B.bin +0 -0
  279. package/town-frontend/dist/assets/models/furniture/chair_B.gltf +136 -0
  280. package/town-frontend/dist/assets/models/furniture/chair_B_wood.bin +0 -0
  281. package/town-frontend/dist/assets/models/furniture/chair_B_wood.gltf +136 -0
  282. package/town-frontend/dist/assets/models/furniture/chair_C.bin +0 -0
  283. package/town-frontend/dist/assets/models/furniture/chair_C.gltf +136 -0
  284. package/town-frontend/dist/assets/models/furniture/chair_stool.bin +0 -0
  285. package/town-frontend/dist/assets/models/furniture/chair_stool.gltf +136 -0
  286. package/town-frontend/dist/assets/models/furniture/chair_stool_wood.bin +0 -0
  287. package/town-frontend/dist/assets/models/furniture/chair_stool_wood.gltf +136 -0
  288. package/town-frontend/dist/assets/models/furniture/couch.bin +0 -0
  289. package/town-frontend/dist/assets/models/furniture/couch.gltf +136 -0
  290. package/town-frontend/dist/assets/models/furniture/couch_pillows.bin +0 -0
  291. package/town-frontend/dist/assets/models/furniture/couch_pillows.gltf +136 -0
  292. package/town-frontend/dist/assets/models/furniture/furniturebits_texture.png +0 -0
  293. package/town-frontend/dist/assets/models/furniture/lamp_standing.bin +0 -0
  294. package/town-frontend/dist/assets/models/furniture/lamp_standing.gltf +136 -0
  295. package/town-frontend/dist/assets/models/furniture/lamp_table.bin +0 -0
  296. package/town-frontend/dist/assets/models/furniture/lamp_table.gltf +136 -0
  297. package/town-frontend/dist/assets/models/furniture/pictureframe_large_A.bin +0 -0
  298. package/town-frontend/dist/assets/models/furniture/pictureframe_large_A.gltf +136 -0
  299. package/town-frontend/dist/assets/models/furniture/pictureframe_large_B.bin +0 -0
  300. package/town-frontend/dist/assets/models/furniture/pictureframe_large_B.gltf +136 -0
  301. package/town-frontend/dist/assets/models/furniture/pictureframe_medium.bin +0 -0
  302. package/town-frontend/dist/assets/models/furniture/pictureframe_medium.gltf +136 -0
  303. package/town-frontend/dist/assets/models/furniture/pictureframe_small_A.bin +0 -0
  304. package/town-frontend/dist/assets/models/furniture/pictureframe_small_A.gltf +136 -0
  305. package/town-frontend/dist/assets/models/furniture/pictureframe_small_B.bin +0 -0
  306. package/town-frontend/dist/assets/models/furniture/pictureframe_small_B.gltf +136 -0
  307. package/town-frontend/dist/assets/models/furniture/pictureframe_small_C.bin +0 -0
  308. package/town-frontend/dist/assets/models/furniture/pictureframe_small_C.gltf +136 -0
  309. package/town-frontend/dist/assets/models/furniture/pictureframe_standing_A.bin +0 -0
  310. package/town-frontend/dist/assets/models/furniture/pictureframe_standing_A.gltf +136 -0
  311. package/town-frontend/dist/assets/models/furniture/pictureframe_standing_B.bin +0 -0
  312. package/town-frontend/dist/assets/models/furniture/pictureframe_standing_B.gltf +136 -0
  313. package/town-frontend/dist/assets/models/furniture/pillow_A.bin +0 -0
  314. package/town-frontend/dist/assets/models/furniture/pillow_A.gltf +136 -0
  315. package/town-frontend/dist/assets/models/furniture/pillow_B.bin +0 -0
  316. package/town-frontend/dist/assets/models/furniture/pillow_B.gltf +136 -0
  317. package/town-frontend/dist/assets/models/furniture/rug_oval_A.bin +0 -0
  318. package/town-frontend/dist/assets/models/furniture/rug_oval_A.gltf +136 -0
  319. package/town-frontend/dist/assets/models/furniture/rug_oval_B.bin +0 -0
  320. package/town-frontend/dist/assets/models/furniture/rug_oval_B.gltf +136 -0
  321. package/town-frontend/dist/assets/models/furniture/rug_rectangle_A.bin +0 -0
  322. package/town-frontend/dist/assets/models/furniture/rug_rectangle_A.gltf +136 -0
  323. package/town-frontend/dist/assets/models/furniture/rug_rectangle_B.bin +0 -0
  324. package/town-frontend/dist/assets/models/furniture/rug_rectangle_B.gltf +136 -0
  325. package/town-frontend/dist/assets/models/furniture/rug_rectangle_stripes_A.bin +0 -0
  326. package/town-frontend/dist/assets/models/furniture/rug_rectangle_stripes_A.gltf +136 -0
  327. package/town-frontend/dist/assets/models/furniture/rug_rectangle_stripes_B.bin +0 -0
  328. package/town-frontend/dist/assets/models/furniture/rug_rectangle_stripes_B.gltf +136 -0
  329. package/town-frontend/dist/assets/models/furniture/shelf_A_big.bin +0 -0
  330. package/town-frontend/dist/assets/models/furniture/shelf_A_big.gltf +136 -0
  331. package/town-frontend/dist/assets/models/furniture/shelf_A_small.bin +0 -0
  332. package/town-frontend/dist/assets/models/furniture/shelf_A_small.gltf +136 -0
  333. package/town-frontend/dist/assets/models/furniture/shelf_B_large.bin +0 -0
  334. package/town-frontend/dist/assets/models/furniture/shelf_B_large.gltf +136 -0
  335. package/town-frontend/dist/assets/models/furniture/shelf_B_large_decorated.bin +0 -0
  336. package/town-frontend/dist/assets/models/furniture/shelf_B_large_decorated.gltf +136 -0
  337. package/town-frontend/dist/assets/models/furniture/shelf_B_small.bin +0 -0
  338. package/town-frontend/dist/assets/models/furniture/shelf_B_small.gltf +136 -0
  339. package/town-frontend/dist/assets/models/furniture/shelf_B_small_decorated.bin +0 -0
  340. package/town-frontend/dist/assets/models/furniture/shelf_B_small_decorated.gltf +136 -0
  341. package/town-frontend/dist/assets/models/furniture/table_low.bin +0 -0
  342. package/town-frontend/dist/assets/models/furniture/table_low.gltf +136 -0
  343. package/town-frontend/dist/assets/models/furniture/table_medium.bin +0 -0
  344. package/town-frontend/dist/assets/models/furniture/table_medium.gltf +136 -0
  345. package/town-frontend/dist/assets/models/furniture/table_medium_long.bin +0 -0
  346. package/town-frontend/dist/assets/models/furniture/table_medium_long.gltf +136 -0
  347. package/town-frontend/dist/assets/models/furniture/table_small.bin +0 -0
  348. package/town-frontend/dist/assets/models/furniture/table_small.gltf +136 -0
  349. package/town-frontend/dist/assets/models/props/bench.bin +0 -0
  350. package/town-frontend/dist/assets/models/props/bench.gltf +136 -0
  351. package/town-frontend/dist/assets/models/props/bush.bin +0 -0
  352. package/town-frontend/dist/assets/models/props/bush.gltf +136 -0
  353. package/town-frontend/dist/assets/models/props/capybara.glb +0 -0
  354. package/town-frontend/dist/assets/models/props/car_hatchback.bin +0 -0
  355. package/town-frontend/dist/assets/models/props/car_hatchback.gltf +442 -0
  356. package/town-frontend/dist/assets/models/props/car_sedan.bin +0 -0
  357. package/town-frontend/dist/assets/models/props/car_sedan.gltf +442 -0
  358. package/town-frontend/dist/assets/models/props/car_taxi.bin +0 -0
  359. package/town-frontend/dist/assets/models/props/car_taxi.gltf +442 -0
  360. package/town-frontend/dist/assets/models/props/citybits_texture.png +0 -0
  361. package/town-frontend/dist/assets/models/props/dumpster.bin +0 -0
  362. package/town-frontend/dist/assets/models/props/dumpster.gltf +136 -0
  363. package/town-frontend/dist/assets/models/props/firehydrant.bin +0 -0
  364. package/town-frontend/dist/assets/models/props/firehydrant.gltf +136 -0
  365. package/town-frontend/dist/assets/models/props/streetlight.bin +0 -0
  366. package/town-frontend/dist/assets/models/props/streetlight.gltf +136 -0
  367. package/town-frontend/dist/assets/models/props/trafficlight_A.bin +0 -0
  368. package/town-frontend/dist/assets/models/props/trafficlight_A.gltf +136 -0
  369. package/town-frontend/dist/assets/models/props/trash_A.bin +0 -0
  370. package/town-frontend/dist/assets/models/props/trash_A.gltf +136 -0
  371. package/town-frontend/dist/assets/models/props/watertower.bin +0 -0
  372. package/town-frontend/dist/assets/models/props/watertower.gltf +136 -0
  373. package/town-frontend/dist/assets/models/stage-deco/Flowers_1_D.glb +0 -0
  374. package/town-frontend/dist/assets/models/stage-deco/Flowers_2_B.glb +0 -0
  375. package/town-frontend/dist/assets/models/stage-deco/Grass_A_1.glb +0 -0
  376. package/town-frontend/dist/assets/models/stage-deco/Park_GrassHill_A.glb +0 -0
  377. package/town-frontend/dist/assets/models/stage-deco/Pebles_1_A_2.glb +0 -0
  378. package/town-frontend/dist/assets/music/bgm_day.mp3 +0 -0
  379. package/town-frontend/dist/assets/music/bgm_dusk.mp3 +0 -0
  380. package/town-frontend/dist/assets/music/bgm_night.mp3 +0 -0
  381. package/town-frontend/dist/assets/music/bgm_work.mp3 +0 -0
  382. package/town-frontend/dist/assets/office-whiteboard-idle-CyEwBrq_.webp +0 -0
  383. package/town-frontend/dist/assets/preview-B6hYEQij.js +2 -0
  384. package/town-frontend/dist/assets/town-BKesnERP.js +8888 -0
  385. package/town-frontend/dist/assets/town-BnswKsjF.css +1 -0
  386. package/town-frontend/dist/citizen-editor.html +166 -0
  387. package/town-frontend/dist/editor.html +227 -0
  388. package/town-frontend/dist/index.html +22 -0
  389. package/town-frontend/dist/preview.html +56 -0
  390. package/town-frontend/dist/town.html +165 -0
  391. package/town-frontend/dist/viewer.html +91 -0
  392. package/town-souls/CHEN.md +130 -0
  393. package/town-souls/CHENGZI.md +92 -0
  394. package/town-souls/CITIZEN_tpl.md +16 -0
  395. package/town-souls/DIANDIAN.md +92 -0
  396. package/town-souls/HAITANG.md +94 -0
  397. package/town-souls/QIQI.md +119 -0
  398. package/town-souls/SOUL.md +141 -0
  399. package/town-souls/SOUL_tpl.md +135 -0
  400. package/town-souls/XIAOLIE.md +107 -0
  401. package/town-souls/YAN.md +107 -0
  402. package/town-workspace/IDENTITY.md +5 -0
  403. package/town-workspace/SOUL.md +141 -0
  404. package/town-workspace/project-workflow.md +81 -0
  405. package/town-workspace/town-defaults.json +92 -0
  406. package/town-workspace/town-guide.md +45 -0
@@ -0,0 +1,1368 @@
1
+ /**
2
+ * Shared HTTP request handler for editor workshops (scene + citizen).
3
+ * Used by both the production server (index.ts) and the Vite dev server (vite.config.ts).
4
+ *
5
+ * Returns true if the request was handled, false if it should fall through.
6
+ */
7
+
8
+ import { join, relative } from "node:path";
9
+ import {
10
+ readFileSync,
11
+ writeFileSync,
12
+ existsSync,
13
+ unlinkSync,
14
+ mkdirSync,
15
+ statSync,
16
+ } from "node:fs";
17
+ import { randomUUID } from "node:crypto";
18
+ import { stateDir } from "./paths.js";
19
+
20
+ function getStewardWorkspaceDir(): string {
21
+ try {
22
+ const { getTownRuntime } = require("./runtime.js") as typeof import("./runtime.js");
23
+ const rt = getTownRuntime();
24
+ const cfg = rt.config as any;
25
+ const entry = (cfg?.agents?.list ?? []).find((a: any) => a.id === "town-steward");
26
+ if (entry?.workspace) return entry.workspace;
27
+ } catch {}
28
+ return join(stateDir(), "workspace-town-steward");
29
+ }
30
+
31
+ const MIME_TYPES: Record<string, string> = {
32
+ ".html": "text/html",
33
+ ".js": "application/javascript",
34
+ ".css": "text/css",
35
+ ".json": "application/json",
36
+ ".png": "image/png",
37
+ ".jpg": "image/jpeg",
38
+ ".jpeg": "image/jpeg",
39
+ ".webp": "image/webp",
40
+ ".svg": "image/svg+xml",
41
+ ".gltf": "model/gltf+json",
42
+ ".glb": "model/gltf-binary",
43
+ ".bin": "application/octet-stream",
44
+ ".woff2": "font/woff2",
45
+ };
46
+
47
+ function guessMime(filePath: string): string {
48
+ const ext = filePath.substring(filePath.lastIndexOf("."));
49
+ return MIME_TYPES[ext] ?? "application/octet-stream";
50
+ }
51
+
52
+ function jsonRes(res: any, data: any, status = 200): void {
53
+ res.writeHead(status, {
54
+ "Content-Type": "application/json",
55
+ "Access-Control-Allow-Origin": "*",
56
+ });
57
+ res.end(JSON.stringify(data));
58
+ }
59
+
60
+ function readBody(req: any): Promise<string> {
61
+ return new Promise((resolve) => {
62
+ let b = "";
63
+ req.on("data", (c: Buffer) => {
64
+ b += c.toString();
65
+ });
66
+ req.on("end", () => resolve(b));
67
+ });
68
+ }
69
+
70
+ function serveFile(
71
+ res: any,
72
+ filePath: string,
73
+ baseDir: string,
74
+ cache?: string,
75
+ ): boolean {
76
+ if (
77
+ !filePath.startsWith(baseDir) ||
78
+ !existsSync(filePath) ||
79
+ !statSync(filePath).isFile()
80
+ ) {
81
+ res.writeHead(404);
82
+ res.end("Not Found");
83
+ return true;
84
+ }
85
+ const headers: Record<string, string> = {
86
+ "Content-Type": guessMime(filePath),
87
+ "Access-Control-Allow-Origin": "*",
88
+ };
89
+ if (cache) headers["Cache-Control"] = cache;
90
+ res.writeHead(200, headers);
91
+ res.end(readFileSync(filePath));
92
+ return true;
93
+ }
94
+
95
+ export interface EditorServeDirs {
96
+ pluginDir: string;
97
+ townDataDir: string;
98
+ }
99
+
100
+ function resolveDirs(pluginDir: string): {
101
+ extAssetsDir: string;
102
+ townDataDir: string;
103
+ soulsDir: string;
104
+ defaultSoulsDir: string;
105
+ draftConfigPath: string;
106
+ publishedConfigPath: string;
107
+ avatarsDir: string;
108
+ animDir: string;
109
+ customAssetsDir: string;
110
+ catalogPath: string;
111
+ } {
112
+ const townDataDir = join(pluginDir, "town-data");
113
+ const customAssetsDir = join(townDataDir, "custom-assets");
114
+ return {
115
+ extAssetsDir: join(pluginDir, "assets"),
116
+ townDataDir,
117
+ soulsDir: join(townDataDir, "souls"),
118
+ defaultSoulsDir: join(pluginDir, "town-souls"),
119
+ draftConfigPath: join(townDataDir, "citizen-config-draft.json"),
120
+ publishedConfigPath: join(townDataDir, "citizen-config.json"),
121
+ avatarsDir: join(townDataDir, "avatars"),
122
+ animDir: join(customAssetsDir, "animations"),
123
+ customAssetsDir,
124
+ catalogPath: join(customAssetsDir, "_catalog.json"),
125
+ };
126
+ }
127
+
128
+ function ensureEditorDirs(pluginDir: string): void {
129
+ const d = resolveDirs(pluginDir);
130
+ for (const dir of [d.soulsDir, d.avatarsDir, d.animDir]) {
131
+ if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
132
+ }
133
+ const modelsDir = join(d.customAssetsDir, "models");
134
+ const charsDir = join(d.customAssetsDir, "characters");
135
+ const thumbDir = join(d.customAssetsDir, "thumbnails");
136
+ for (const dir of [modelsDir, charsDir, thumbDir]) {
137
+ if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
138
+ }
139
+ if (!existsSync(d.catalogPath))
140
+ writeFileSync(
141
+ d.catalogPath,
142
+ JSON.stringify({ version: 1, assets: [] }),
143
+ );
144
+ migrateCatalogThumbnails(d.customAssetsDir, d.catalogPath);
145
+ }
146
+
147
+ function migrateCatalogThumbnails(customAssetsDir: string, catalogPath: string): void {
148
+ try {
149
+ const catalog = readCatalog(catalogPath);
150
+ let changed = false;
151
+ const thumbDir = join(customAssetsDir, "thumbnails");
152
+ for (const asset of catalog.assets) {
153
+ if (asset.thumbnailFileName) continue;
154
+ if (!asset.thumbnail || !asset.thumbnail.startsWith("data:")) continue;
155
+ const m = asset.thumbnail.match(/^data:image\/(\w+);base64,(.+)$/);
156
+ if (!m) continue;
157
+ const ext = m[1] === "jpeg" ? "jpg" : m[1];
158
+ const outName = `${asset.id}.${ext}`;
159
+ try {
160
+ writeFileSync(join(thumbDir, outName), Buffer.from(m[2], "base64"));
161
+ asset.thumbnailFileName = outName;
162
+ delete asset.thumbnail;
163
+ changed = true;
164
+ } catch { /* skip this asset */ }
165
+ }
166
+ if (changed) {
167
+ writeCatalog(catalogPath, catalog);
168
+ console.log(`[agentshire] Migrated ${catalog.assets.filter((a: any) => a.thumbnailFileName).length} asset thumbnails to files`);
169
+ }
170
+ } catch {
171
+ /* migration is best-effort */
172
+ }
173
+ }
174
+
175
+ function loadAgentList(): { id: string; name: string }[] {
176
+ try {
177
+ const { getTownRuntime } = require("./runtime.js") as typeof import("./runtime.js");
178
+ const rt = getTownRuntime();
179
+ const cfg = rt.config as any;
180
+ return (cfg?.agents?.list ?? []).map((a: any) => ({
181
+ id: a.id,
182
+ name: a.identity?.name || a.name || a.id,
183
+ }));
184
+ } catch {
185
+ return [];
186
+ }
187
+ }
188
+
189
+ function loadSoulContent(
190
+ personaKey: string,
191
+ soulsDir: string,
192
+ defaultSoulsDir: string,
193
+ ): string | null {
194
+ const userPath = join(soulsDir, `${personaKey}.md`);
195
+ if (existsSync(userPath)) return readFileSync(userPath, "utf-8");
196
+ const defaultPath = join(defaultSoulsDir, `${personaKey}.md`);
197
+ if (existsSync(defaultPath)) return readFileSync(defaultPath, "utf-8");
198
+ return null;
199
+ }
200
+
201
+ function readCatalog(catalogPath: string): any {
202
+ try {
203
+ return JSON.parse(readFileSync(catalogPath, "utf-8"));
204
+ } catch {
205
+ return { version: 1, assets: [] as any[] };
206
+ }
207
+ }
208
+
209
+ function writeCatalog(catalogPath: string, catalog: any): void {
210
+ writeFileSync(catalogPath, JSON.stringify(catalog, null, 2));
211
+ }
212
+
213
+ // ── ext-assets: Characters_1 / Map_1 asset library ──
214
+
215
+ function handleExtAssets(
216
+ res: any,
217
+ urlPath: string,
218
+ extAssetsDir: string,
219
+ ): boolean {
220
+ const relPath = decodeURIComponent(urlPath.slice("/ext-assets/".length));
221
+ const filePath = join(extAssetsDir, relPath);
222
+ return serveFile(res, filePath, extAssetsDir, "public, max-age=86400");
223
+ }
224
+
225
+ // ── citizen-workshop/avatars: user-uploaded avatars ──
226
+
227
+ function handleCitizenAvatars(
228
+ res: any,
229
+ urlPath: string,
230
+ avatarsDir: string,
231
+ ): boolean {
232
+ const fileName = decodeURIComponent(
233
+ urlPath.slice("/citizen-workshop/avatars/".length),
234
+ );
235
+ const filePath = join(avatarsDir, fileName);
236
+ return serveFile(res, filePath, avatarsDir, "public, max-age=3600");
237
+ }
238
+
239
+ // ── custom-assets/animations: custom character anim files ──
240
+
241
+ function handleAnimations(
242
+ res: any,
243
+ urlPath: string,
244
+ animDir: string,
245
+ ): boolean {
246
+ const fileName = decodeURIComponent(
247
+ urlPath.slice("/custom-assets/animations/".length),
248
+ );
249
+ const filePath = join(animDir, fileName);
250
+ return serveFile(res, filePath, animDir, "public, max-age=86400");
251
+ }
252
+
253
+ // ── custom-assets API ──
254
+
255
+ async function handleCustomAssetsApi(
256
+ req: any,
257
+ res: any,
258
+ route: string,
259
+ customAssetsDir: string,
260
+ catalogPath: string,
261
+ ): Promise<boolean> {
262
+ let body: any;
263
+ try {
264
+ body = JSON.parse(await readBody(req));
265
+ } catch {
266
+ jsonRes(res, { error: "Invalid JSON body" }, 400);
267
+ return true;
268
+ }
269
+
270
+ if (route === "list") {
271
+ const catalog = readCatalog(catalogPath);
272
+ const filtered = body.kind
273
+ ? catalog.assets.filter((a: any) => a.kind === body.kind)
274
+ : catalog.assets;
275
+ const assets = filtered.map((a: any) => {
276
+ if (a.thumbnailFileName && (!a.thumbnail || a.thumbnail.startsWith("data:"))) {
277
+ return { ...a, thumbnail: `/custom-assets/thumbnails/${a.thumbnailFileName}` };
278
+ }
279
+ return a;
280
+ });
281
+ jsonRes(res, { assets });
282
+ return true;
283
+ }
284
+
285
+ if (route === "upload") {
286
+ const catalog = readCatalog(catalogPath);
287
+ if (catalog.assets.length >= 20) {
288
+ jsonRes(res, { error: "最多添加 20 个自定义资产" }, 400);
289
+ return true;
290
+ }
291
+ const buf = Buffer.from(body.data, "base64");
292
+ if (buf.length > 30 * 1024 * 1024) {
293
+ jsonRes(res, { error: "文件超过 30MB 限制" }, 400);
294
+ return true;
295
+ }
296
+ const id = randomUUID();
297
+ const fileName = `${id}.glb`;
298
+ const subDir = body.kind === "character" ? "characters" : "models";
299
+ const dir = join(customAssetsDir, subDir);
300
+ if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
301
+ writeFileSync(join(dir, fileName), buf);
302
+ const now = new Date().toISOString();
303
+ let thumbnailFileName: string | undefined;
304
+ const thumbDir = join(customAssetsDir, "thumbnails");
305
+ if (!existsSync(thumbDir)) mkdirSync(thumbDir, { recursive: true });
306
+ if (body.thumbnail && typeof body.thumbnail === "string" && body.thumbnail.startsWith("data:")) {
307
+ const m = body.thumbnail.match(/^data:image\/(\w+);base64,(.+)$/);
308
+ if (m) {
309
+ const ext = m[1] === "jpeg" ? "jpg" : m[1];
310
+ thumbnailFileName = `${id}.${ext}`;
311
+ try { writeFileSync(join(thumbDir, thumbnailFileName), Buffer.from(m[2], "base64")); } catch { thumbnailFileName = undefined; }
312
+ }
313
+ }
314
+ const asset: any = {
315
+ id,
316
+ kind: body.kind ?? "model",
317
+ name: (body.name ?? "").slice(0, 20),
318
+ fileName,
319
+ fileSize: buf.length,
320
+ createdAt: now,
321
+ updatedAt: now,
322
+ cells: body.cells,
323
+ scale: body.scale,
324
+ assetType: body.assetType,
325
+ fixRotationX: body.fixRotationX,
326
+ fixRotationY: body.fixRotationY,
327
+ fixRotationZ: body.fixRotationZ,
328
+ };
329
+ if (thumbnailFileName) {
330
+ asset.thumbnailFileName = thumbnailFileName;
331
+ } else if (body.thumbnail) {
332
+ asset.thumbnail = body.thumbnail;
333
+ }
334
+ catalog.assets.unshift(asset);
335
+ writeCatalog(catalogPath, catalog);
336
+ if (asset.thumbnailFileName) {
337
+ asset.thumbnail = `/custom-assets/thumbnails/${asset.thumbnailFileName}`;
338
+ }
339
+ jsonRes(res, { asset });
340
+ return true;
341
+ }
342
+
343
+ if (route === "update") {
344
+ const catalog = readCatalog(catalogPath);
345
+ const asset = catalog.assets.find((a: any) => a.id === body.id);
346
+ if (!asset) {
347
+ jsonRes(res, { error: "资产不存在" }, 404);
348
+ return true;
349
+ }
350
+ for (const key of [
351
+ "name",
352
+ "cells",
353
+ "scale",
354
+ "assetType",
355
+ "fixRotationX",
356
+ "fixRotationY",
357
+ "fixRotationZ",
358
+ ]) {
359
+ if (body[key] !== undefined)
360
+ (asset as any)[key] =
361
+ key === "name" ? body[key].slice(0, 20) : body[key];
362
+ }
363
+ if (body.thumbnail !== undefined && typeof body.thumbnail === "string" && body.thumbnail.startsWith("data:")) {
364
+ const thumbDir = join(customAssetsDir, "thumbnails");
365
+ if (!existsSync(thumbDir)) mkdirSync(thumbDir, { recursive: true });
366
+ const m = body.thumbnail.match(/^data:image\/(\w+);base64,(.+)$/);
367
+ if (m) {
368
+ const ext = m[1] === "jpeg" ? "jpg" : m[1];
369
+ const outName = `${asset.id}.${ext}`;
370
+ try {
371
+ if (asset.thumbnailFileName) {
372
+ const old = join(thumbDir, asset.thumbnailFileName);
373
+ if (existsSync(old)) unlinkSync(old);
374
+ }
375
+ writeFileSync(join(thumbDir, outName), Buffer.from(m[2], "base64"));
376
+ asset.thumbnailFileName = outName;
377
+ delete asset.thumbnail;
378
+ } catch { asset.thumbnail = body.thumbnail; }
379
+ }
380
+ } else if (body.thumbnail !== undefined) {
381
+ asset.thumbnail = body.thumbnail;
382
+ }
383
+ for (const key of ["animMapping", "detectedClips", "animFileUrls"]) {
384
+ if (body[key] !== undefined) (asset as any)[key] = body[key];
385
+ }
386
+ asset.updatedAt = new Date().toISOString();
387
+ writeCatalog(catalogPath, catalog);
388
+ jsonRes(res, { asset });
389
+ return true;
390
+ }
391
+
392
+ if (route === "delete") {
393
+ const catalog = readCatalog(catalogPath);
394
+ const idx = catalog.assets.findIndex((a: any) => a.id === body.id);
395
+ if (idx < 0) {
396
+ jsonRes(res, { error: "资产不存在" }, 404);
397
+ return true;
398
+ }
399
+ const asset = catalog.assets[idx];
400
+ const subDir = asset.kind === "character" ? "characters" : "models";
401
+ try {
402
+ const fp = join(customAssetsDir, subDir, asset.fileName);
403
+ if (existsSync(fp)) unlinkSync(fp);
404
+ } catch {
405
+ /* gone */
406
+ }
407
+ if (asset.thumbnailFileName) {
408
+ try {
409
+ const tp = join(customAssetsDir, "thumbnails", asset.thumbnailFileName);
410
+ if (existsSync(tp)) unlinkSync(tp);
411
+ } catch { /* gone */ }
412
+ }
413
+ if (asset.animFileUrls && Array.isArray(asset.animFileUrls)) {
414
+ const animDir = join(customAssetsDir, "animations");
415
+ for (const url of asset.animFileUrls) {
416
+ const fn = (url as string).split("/").pop();
417
+ if (fn) {
418
+ try {
419
+ const p = join(animDir, fn);
420
+ if (existsSync(p)) unlinkSync(p);
421
+ } catch {
422
+ /* gone */
423
+ }
424
+ }
425
+ }
426
+ }
427
+ catalog.assets.splice(idx, 1);
428
+ writeCatalog(catalogPath, catalog);
429
+ jsonRes(res, { success: true });
430
+ return true;
431
+ }
432
+
433
+ if (route === "optimize") {
434
+ const catalog = readCatalog(catalogPath);
435
+ const asset = catalog.assets.find((a: any) => a.id === body.id);
436
+ if (!asset) {
437
+ jsonRes(res, { error: "资产不存在" }, 404);
438
+ return true;
439
+ }
440
+ const subDir = asset.kind === "character" ? "characters" : "models";
441
+ const filePath = join(customAssetsDir, subDir, asset.fileName);
442
+ if (!existsSync(filePath)) {
443
+ jsonRes(res, { error: "文件不存在" }, 404);
444
+ return true;
445
+ }
446
+ try {
447
+ const { NodeIO } = await import("@gltf-transform/core");
448
+ const { dedup, flatten, prune, quantize } = await import(
449
+ "@gltf-transform/functions"
450
+ );
451
+ const { ALL_EXTENSIONS } = await import("@gltf-transform/extensions");
452
+ const io = new NodeIO().registerExtensions(ALL_EXTENSIONS);
453
+ const doc = await io.read(filePath);
454
+ await doc.transform(dedup(), flatten(), prune(), quantize());
455
+ const outBuf = await io.writeBinary(doc);
456
+ const originalSize = statSync(filePath).size;
457
+ writeFileSync(filePath, Buffer.from(outBuf));
458
+ const newSize = statSync(filePath).size;
459
+ asset.fileSize = newSize;
460
+ asset.updatedAt = new Date().toISOString();
461
+ writeCatalog(catalogPath, catalog);
462
+ jsonRes(res, {
463
+ success: true,
464
+ originalSize,
465
+ newSize,
466
+ saved: originalSize - newSize,
467
+ ratio: Math.round((1 - newSize / originalSize) * 100),
468
+ });
469
+ } catch (e: any) {
470
+ jsonRes(
471
+ res,
472
+ { error: `优化失败: ${e.message ?? "未知错误"}` },
473
+ 500,
474
+ );
475
+ }
476
+ return true;
477
+ }
478
+
479
+ jsonRes(res, { error: "Unknown API" }, 404);
480
+ return true;
481
+ }
482
+
483
+ // ── citizen-workshop API ──
484
+
485
+ function resolveAssetThumbnailUrl(asset: any): string | undefined {
486
+ if (asset?.thumbnailFileName) return `/custom-assets/thumbnails/${asset.thumbnailFileName}`;
487
+ if (asset?.thumbnail && !asset.thumbnail.startsWith("data:")) return asset.thumbnail;
488
+ return undefined;
489
+ }
490
+
491
+ function resolveAvatarUrlServer(
492
+ avatarUrl: string | undefined,
493
+ avatarId: string,
494
+ catalog: any,
495
+ ): string {
496
+ if (avatarUrl && !avatarUrl.startsWith("data:")) return avatarUrl;
497
+
498
+ if (avatarId.startsWith("custom-")) {
499
+ const assetId = avatarId.replace("custom-", "");
500
+ const asset = (catalog.assets ?? []).find((a: any) => a.id === assetId);
501
+ const thumbUrl = resolveAssetThumbnailUrl(asset);
502
+ if (thumbUrl) return thumbUrl;
503
+ if (asset?.thumbnail) return asset.thumbnail;
504
+ }
505
+
506
+ if (avatarId.startsWith("lib-")) {
507
+ const libId = avatarId.replace("lib-", "");
508
+ return `/ext-assets/Characters_1/thumbnails/lib-${libId}.webp`;
509
+ }
510
+
511
+ return `/assets/avatars/${avatarId}.webp`;
512
+ }
513
+
514
+ function resolveModelUrlServer(
515
+ avatarId: string,
516
+ catalog: any,
517
+ ): string {
518
+ if (avatarId.startsWith("custom-")) {
519
+ const assetId = avatarId.replace("custom-", "");
520
+ const asset = (catalog.assets ?? []).find((a: any) => a.id === assetId);
521
+ if (asset?.fileName) return `/custom-assets/characters/${asset.fileName}`;
522
+ }
523
+
524
+ if (avatarId.startsWith("lib-")) {
525
+ const libId = avatarId.replace("lib-", "");
526
+ return `/ext-assets/Characters_1/gLTF/Characters/Character_${libId}_1_1.glb`;
527
+ }
528
+
529
+ const slug = avatarId.replace("char-", "");
530
+ return `/assets/models/characters/character-${slug}.glb`;
531
+ }
532
+
533
+ const DEFAULT_STEWARD_NAME = "OpenClaw";
534
+ const DEFAULT_STEWARD_BIO = "干练御姐,做事利落,职业经理型,善于引导对话,通过调度居民完成任务";
535
+
536
+ function composeStewardSoul(
537
+ name: string,
538
+ bio: string,
539
+ pluginDir: string,
540
+ ): string {
541
+ const now = new Date().toISOString().split("T")[0];
542
+ const tplPath = join(pluginDir, "town-souls", "SOUL_tpl.md");
543
+ let tplContent = "";
544
+ try {
545
+ tplContent = readFileSync(tplPath, "utf-8");
546
+ } catch {
547
+ console.warn("[citizen-workshop] SOUL_tpl.md not found, using empty template");
548
+ }
549
+ return [
550
+ `# ${name}`,
551
+ "",
552
+ `> 诞生日期: ${now}`,
553
+ "## 人设风格",
554
+ bio,
555
+ "",
556
+ tplContent,
557
+ ].join("\n");
558
+ }
559
+
560
+ function isStewardModified(src: any): boolean {
561
+ return (src.name ?? "") !== DEFAULT_STEWARD_NAME || (src.bio ?? "") !== DEFAULT_STEWARD_BIO;
562
+ }
563
+
564
+ function composeSoulFromConfig(
565
+ name: string,
566
+ bio: string,
567
+ specialty: string,
568
+ customSoul: string,
569
+ pluginDir: string,
570
+ ): string {
571
+ const now = new Date().toISOString().split("T")[0];
572
+ const persona = customSoul || bio;
573
+ const tplPath = join(pluginDir, "town-souls", "CITIZEN_tpl.md");
574
+ if (existsSync(tplPath)) {
575
+ return readFileSync(tplPath, "utf-8")
576
+ .replace(/\{\{name\}\}/g, name)
577
+ .replace(/\{\{date\}\}/g, now)
578
+ .replace(/\{\{specialty\}\}/g, specialty || "通用助手")
579
+ .replace(/\{\{bio\}\}/g, bio)
580
+ .replace(/\{\{persona\}\}/g, persona);
581
+ }
582
+ return `# ${name}\n\n> 诞生日期: ${now}\n> 岗位: ${specialty || "通用助手"}\n> 简介: ${bio}\n\n## 人设核心\n\n你的名字叫${name},你的专业技能是${specialty || "通用助手"}。\n\n${persona}\n\n---\n`;
583
+ }
584
+
585
+ function buildPublishedConfig(
586
+ draft: any,
587
+ soulsDir: string,
588
+ defaultSoulsDir: string,
589
+ catalog: any,
590
+ pluginDir: string,
591
+ ): any {
592
+ const characters: any[] = [];
593
+ const defaultModelTransform = {
594
+ scale: 2.8,
595
+ rotationX: 0,
596
+ rotationY: 0,
597
+ rotationZ: 0,
598
+ offsetX: 0,
599
+ offsetY: 0,
600
+ offsetZ: 0,
601
+ };
602
+
603
+ const resolveEntry = (
604
+ role: "user" | "steward" | "citizen",
605
+ src: any,
606
+ ): any => {
607
+ let persona = src.persona || "";
608
+ const isCustom = !!src.useCustomPersona || !persona;
609
+ let personaFile = "";
610
+
611
+ if (role === "steward") {
612
+ if (isStewardModified(src)) {
613
+ const soulText = composeStewardSoul(src.name ?? "", src.bio ?? "", pluginDir);
614
+ writeFileSync(join(soulsDir, "soul.md"), soulText, "utf-8");
615
+ personaFile = join(soulsDir, "soul.md");
616
+ persona = "soul";
617
+ } else {
618
+ const defaultPath = join(defaultSoulsDir, "SOUL.md");
619
+ personaFile = existsSync(defaultPath) ? defaultPath : "";
620
+ }
621
+ } else if (role === "citizen" && isCustom) {
622
+ const soulText = composeSoulFromConfig(
623
+ src.name ?? "",
624
+ src.bio ?? "",
625
+ src.specialty ?? "",
626
+ src.customSoul ?? "",
627
+ pluginDir,
628
+ );
629
+ const citizenId = src.id ?? `citizen-${Date.now()}`;
630
+ const soulPath = join(soulsDir, `${citizenId}.md`);
631
+ writeFileSync(soulPath, soulText, "utf-8");
632
+ personaFile = soulPath;
633
+ persona = citizenId;
634
+ } else if (role === "citizen" && !isCustom && persona) {
635
+ const userPath = join(soulsDir, `${persona}.md`);
636
+ const defaultPath = join(defaultSoulsDir, `${persona}.md`);
637
+ personaFile = existsSync(userPath) ? userPath : existsSync(defaultPath) ? defaultPath : "";
638
+ }
639
+
640
+ const animData = resolveAnimData(src, catalog);
641
+ return {
642
+ id: src.id ?? role,
643
+ role,
644
+ name: src.name ?? "",
645
+ avatarUrl: resolveAvatarUrlServer(src.avatarUrl, src.avatarId, catalog),
646
+ modelUrl: resolveModelUrlServer(src.avatarId, catalog),
647
+ avatarId: src.avatarId ?? "",
648
+ modelSource: src.modelSource ?? "builtin",
649
+ bio: src.bio ?? "",
650
+ specialty: src.specialty ?? "",
651
+ persona,
652
+ personaFile: personaFile ? relative(pluginDir, personaFile) : "",
653
+ homeId: src.homeId ?? "",
654
+ agentEnabled: role === "citizen" ? !!src.agentEnabled : false,
655
+ modelTransform: resolveModelTransform(src, draft) ?? defaultModelTransform,
656
+ animMapping: animData.animMapping ?? {},
657
+ animFileUrls: animData.animFileUrls ?? [],
658
+ detectedClips: animData.detectedClips ?? [],
659
+ };
660
+ };
661
+
662
+ function resolveModelTransform(srcEntry: any, _draftConfig: any): any {
663
+ if (srcEntry?.modelTransform && typeof srcEntry.modelTransform === "object") return srcEntry.modelTransform;
664
+ return null;
665
+ }
666
+
667
+ function resolveAnimData(src: any, cat: any): any {
668
+ const avatarId = src.avatarId ?? "";
669
+ if (avatarId.startsWith("custom-")) {
670
+ const assetId = avatarId.replace("custom-", "");
671
+ const asset = (cat.assets ?? []).find((a: any) => a.id === assetId);
672
+ if (asset) {
673
+ const mapping = asset.animMapping && Object.values(asset.animMapping).some((v: any) => v)
674
+ ? asset.animMapping
675
+ : src.animMapping;
676
+ return {
677
+ animMapping: mapping ?? {},
678
+ detectedClips: asset.detectedClips ?? [],
679
+ animFileUrls: asset.animFileUrls ?? [],
680
+ };
681
+ }
682
+ }
683
+ return {
684
+ animMapping: src.animMapping ?? {},
685
+ animFileUrls: src.animFileUrls ?? [],
686
+ detectedClips: src.detectedClips ?? [],
687
+ };
688
+ }
689
+
690
+ if (draft.user) characters.push(resolveEntry("user", draft.user));
691
+ if (draft.steward) characters.push(resolveEntry("steward", draft.steward));
692
+ if (Array.isArray(draft.citizens)) {
693
+ for (const c of draft.citizens) {
694
+ characters.push(resolveEntry("citizen", c));
695
+ }
696
+ }
697
+
698
+ return {
699
+ version: 1,
700
+ publishedAt: new Date().toISOString(),
701
+ characters,
702
+ };
703
+ }
704
+
705
+ function syncTownDefaults(published: any, pluginDir: string): void {
706
+ try {
707
+ const chars: any[] = published.characters ?? [];
708
+ const steward = chars.find((c: any) => c.role === "steward");
709
+ const user = chars.find((c: any) => c.role === "user");
710
+ const citizens = chars.filter((c: any) => c.role === "citizen");
711
+
712
+ const townDefaults = {
713
+ townName: "夏尔",
714
+ steward: {
715
+ id: "steward",
716
+ name: steward?.name ?? "shire",
717
+ personaFile: steward?.personaFile ?? "",
718
+ characterKey: steward?.avatarId ?? "char-female-b",
719
+ role: "steward",
720
+ specialty: "管家",
721
+ bio: steward?.bio ?? "",
722
+ },
723
+ user: {
724
+ id: "user",
725
+ name: user?.name ?? "镇长",
726
+ characterKey: user?.avatarId ?? "char-male-c",
727
+ role: "user",
728
+ specialty: "镇长",
729
+ bio: user?.bio || "小镇的主人,负责决策方向",
730
+ },
731
+ citizens: citizens.map((c: any) => ({
732
+ id: c.id,
733
+ name: c.name,
734
+ specialty: c.specialty || "通用",
735
+ role: c.specialty || "通用",
736
+ personaFile: c.personaFile ?? "",
737
+ characterKey: c.avatarId ?? "",
738
+ homeId: c.homeId ?? "",
739
+ bio: c.bio ?? "",
740
+ })),
741
+ };
742
+
743
+ const content = JSON.stringify(townDefaults, null, 2);
744
+
745
+ const frontendPath = join(pluginDir, "town-frontend", "src", "data", "town-defaults.json");
746
+ writeFileSync(frontendPath, content, "utf-8");
747
+
748
+ const stewardWorkspace = getStewardWorkspaceDir();
749
+ if (existsSync(stewardWorkspace)) {
750
+ const resolvePersonaFile = (p: string) =>
751
+ p && !p.startsWith("/") ? join(pluginDir, p) : p;
752
+ const stewardDefaults = {
753
+ ...townDefaults,
754
+ steward: { ...townDefaults.steward, personaFile: resolvePersonaFile(townDefaults.steward.personaFile) },
755
+ citizens: townDefaults.citizens.map((c: any) => ({ ...c, personaFile: resolvePersonaFile(c.personaFile) })),
756
+ };
757
+ writeFileSync(join(stewardWorkspace, "town-defaults.json"), JSON.stringify(stewardDefaults, null, 2), "utf-8");
758
+ }
759
+
760
+ console.log("[citizen-workshop] synced town-defaults.json to frontend + steward workspace");
761
+ } catch (err: any) {
762
+ console.warn("[citizen-workshop] Failed to sync town-defaults.json:", err?.message);
763
+ }
764
+ }
765
+
766
+ function computeChangeset(
767
+ oldConfig: any | null,
768
+ newConfig: any,
769
+ soulsDir: string,
770
+ defaultSoulsDir: string,
771
+ pluginDir: string,
772
+ ): {
773
+ totalCharacters: number;
774
+ agentToCreate: string[];
775
+ agentToDisable: string[];
776
+ agentToUpdateSoul: string[];
777
+ stewardSoulUpdated: boolean;
778
+ changes: Array<{
779
+ action: "create" | "disable" | "update_soul";
780
+ citizenId: string;
781
+ citizenName: string;
782
+ agentId: string;
783
+ soulContent?: string;
784
+ specialty?: string;
785
+ }>;
786
+ } {
787
+ const oldChars: any[] = oldConfig?.characters ?? [];
788
+ const newChars: any[] = newConfig?.characters ?? [];
789
+ const oldMap = new Map(oldChars.map((c: any) => [c.id, c]));
790
+ const newMap = new Map(newChars.map((c: any) => [c.id, c]));
791
+
792
+ const agentToCreate: string[] = [];
793
+ const agentToDisable: string[] = [];
794
+ const agentToUpdateSoul: string[] = [];
795
+ let stewardSoulUpdated = false;
796
+ const changes: Array<{
797
+ action: "create" | "disable" | "update_soul";
798
+ citizenId: string;
799
+ citizenName: string;
800
+ agentId: string;
801
+ soulContent?: string;
802
+ specialty?: string;
803
+ }> = [];
804
+
805
+ function buildAgentId(citizenId: string): string {
806
+ return `citizen-${citizenId.replace(/[^a-zA-Z0-9_-]/g, "-")}`;
807
+ }
808
+
809
+ function resolveSoulFromFile(entry: any): string {
810
+ if (entry.personaFile) {
811
+ const resolved = entry.personaFile.startsWith("/")
812
+ ? entry.personaFile
813
+ : join(pluginDir, entry.personaFile);
814
+ if (existsSync(resolved)) {
815
+ return readFileSync(resolved, "utf-8");
816
+ }
817
+ }
818
+ if (entry.persona) {
819
+ const content = loadSoulContent(entry.persona, soulsDir, defaultSoulsDir);
820
+ if (content) return content;
821
+ }
822
+ return "";
823
+ }
824
+
825
+ // ── Steward change detection ──
826
+ const newSteward = newChars.find((c: any) => c.role === "steward");
827
+ const oldSteward = oldChars.find((c: any) => c.role === "steward");
828
+ if (newSteward) {
829
+ const oldName = oldSteward?.name ?? DEFAULT_STEWARD_NAME;
830
+ const oldBio = oldSteward?.bio ?? DEFAULT_STEWARD_BIO;
831
+ if (newSteward.name !== oldName || newSteward.bio !== oldBio) {
832
+ stewardSoulUpdated = true;
833
+ const stewardSoulContent = resolveSoulFromFile(newSteward);
834
+ if (stewardSoulContent) {
835
+ try {
836
+ const stewardWorkspace = getStewardWorkspaceDir();
837
+ if (existsSync(stewardWorkspace)) {
838
+ writeFileSync(join(stewardWorkspace, "SOUL.md"), stewardSoulContent, "utf-8");
839
+ const identityLines = [
840
+ `# ${newSteward.name}`,
841
+ "",
842
+ `- **Name:** ${newSteward.name}`,
843
+ `- **Role:** 管家`,
844
+ ];
845
+ writeFileSync(join(stewardWorkspace, "IDENTITY.md"), identityLines.join("\n") + "\n", "utf-8");
846
+ console.log("[citizen-workshop] Updated steward SOUL.md + IDENTITY.md in main agent workspace");
847
+ }
848
+ } catch (err: any) {
849
+ console.error("[citizen-workshop] Failed to update steward workspace SOUL.md:", err?.message);
850
+ }
851
+ }
852
+ }
853
+ }
854
+
855
+ // ── Citizen change detection ──
856
+ for (const [id, nc] of newMap) {
857
+ if (nc.role !== "citizen") continue;
858
+ const oc = oldMap.get(id);
859
+ const newEnabled = !!nc.agentEnabled;
860
+ const oldEnabled = !!oc?.agentEnabled;
861
+ const agentId = buildAgentId(id);
862
+
863
+ if (newEnabled && !oldEnabled) {
864
+ agentToCreate.push(nc.name);
865
+ changes.push({ action: "create", citizenId: id, citizenName: nc.name, agentId, soulContent: resolveSoulFromFile(nc), specialty: nc.specialty });
866
+ } else if (newEnabled && oldEnabled && !oc?.agentId) {
867
+ agentToCreate.push(nc.name);
868
+ changes.push({ action: "create", citizenId: id, citizenName: nc.name, agentId, soulContent: resolveSoulFromFile(nc), specialty: nc.specialty });
869
+ } else if (!newEnabled && oldEnabled) {
870
+ agentToDisable.push(nc.name ?? oc?.name ?? id);
871
+ changes.push({ action: "disable", citizenId: id, citizenName: nc.name ?? id, agentId: oc?.agentId ?? agentId });
872
+ } else if (newEnabled && oldEnabled) {
873
+ const soulChanged = nc.persona !== oc?.persona
874
+ || nc.name !== oc?.name
875
+ || nc.bio !== oc?.bio
876
+ || nc.specialty !== oc?.specialty;
877
+ if (soulChanged) {
878
+ agentToUpdateSoul.push(nc.name);
879
+ changes.push({ action: "update_soul", citizenId: id, citizenName: nc.name, agentId: oc?.agentId ?? agentId, soulContent: resolveSoulFromFile(nc), specialty: nc.specialty });
880
+ }
881
+ }
882
+ }
883
+
884
+ for (const [id, oc] of oldMap) {
885
+ if (oc.role !== "citizen" || !oc.agentEnabled) continue;
886
+ if (!newMap.has(id)) {
887
+ agentToDisable.push(oc.name ?? id);
888
+ changes.push({ action: "disable", citizenId: id, citizenName: oc.name ?? id, agentId: oc?.agentId ?? buildAgentId(id) });
889
+ }
890
+ }
891
+
892
+ return { totalCharacters: newChars.length, agentToCreate, agentToDisable, agentToUpdateSoul, stewardSoulUpdated, changes };
893
+ }
894
+
895
+ const SOUL_GEN_TIMEOUT_MS = 90_000;
896
+
897
+ async function generateSoulViaAgent(system: string, user: string): Promise<string> {
898
+ const { getTownRuntime } = require("./runtime.js") as typeof import("./runtime.js");
899
+ const rt = getTownRuntime();
900
+ const cfg = rt.config.loadConfig() as any;
901
+ const sessionKey = `agent:town-steward:soul-gen:${Date.now()}`;
902
+ const message = `【系统指令】\n${system}\n\n【用户输入】\n${user}`;
903
+
904
+ let responseText = "";
905
+ const agentDone = rt.channel.reply.dispatchReplyWithBufferedBlockDispatcher({
906
+ ctx: rt.channel.reply.finalizeInboundContext({
907
+ Body: message,
908
+ RawBody: message,
909
+ CommandBody: message,
910
+ From: "agentshire:user",
911
+ To: "agentshire:steward",
912
+ SessionKey: sessionKey,
913
+ AccountId: "default",
914
+ OriginatingChannel: "agentshire",
915
+ ChatType: "direct",
916
+ SenderId: "user",
917
+ Provider: "agentshire",
918
+ Surface: "agentshire",
919
+ }),
920
+ cfg,
921
+ dispatcherOptions: {
922
+ deliver: async (payload: any) => {
923
+ const text = payload?.text ?? payload?.body;
924
+ if (text) responseText = text;
925
+ },
926
+ },
927
+ });
928
+
929
+ const timeout = new Promise<void>((_, reject) =>
930
+ setTimeout(() => reject(new Error("Soul generation timed out")), SOUL_GEN_TIMEOUT_MS),
931
+ );
932
+
933
+ await Promise.race([agentDone, timeout]);
934
+ return responseText;
935
+ }
936
+
937
+ async function handleCitizenWorkshopApi(
938
+ req: any,
939
+ res: any,
940
+ route: string,
941
+ pluginDir: string,
942
+ ): Promise<boolean> {
943
+ const d = resolveDirs(pluginDir);
944
+ let body: any;
945
+ try {
946
+ body = JSON.parse(await readBody(req));
947
+ } catch {
948
+ jsonRes(res, { error: "Invalid JSON body" }, 400);
949
+ return true;
950
+ }
951
+
952
+ if (route === "load") {
953
+ if (existsSync(d.draftConfigPath)) {
954
+ try {
955
+ const config = JSON.parse(readFileSync(d.draftConfigPath, "utf-8"));
956
+ jsonRes(res, { config });
957
+ } catch {
958
+ jsonRes(res, { config: null });
959
+ }
960
+ } else {
961
+ jsonRes(res, { config: null });
962
+ }
963
+ return true;
964
+ }
965
+
966
+ if (route === "save") {
967
+ writeFileSync(
968
+ d.draftConfigPath,
969
+ JSON.stringify(body.config, null, 2),
970
+ "utf-8",
971
+ );
972
+ if (body.souls && typeof body.souls === "object") {
973
+ for (const [name, content] of Object.entries(body.souls)) {
974
+ writeFileSync(join(d.soulsDir, `${name}.md`), content as string, "utf-8");
975
+ }
976
+ }
977
+ jsonRes(res, { success: true });
978
+ return true;
979
+ }
980
+
981
+ if (route === "publish") {
982
+ const draftPath = d.draftConfigPath;
983
+ if (!existsSync(draftPath)) {
984
+ jsonRes(res, { error: "没有草稿可发布,请先保存" }, 400);
985
+ return true;
986
+ }
987
+ try {
988
+ const draft = JSON.parse(readFileSync(draftPath, "utf-8"));
989
+ if (body.souls && typeof body.souls === "object") {
990
+ for (const [name, content] of Object.entries(body.souls)) {
991
+ writeFileSync(join(d.soulsDir, `${name}.md`), content as string, "utf-8");
992
+ }
993
+ }
994
+ const catalog = readCatalog(d.catalogPath);
995
+ const published = buildPublishedConfig(draft, d.soulsDir, d.defaultSoulsDir, catalog, pluginDir);
996
+ let oldPublished: any = null;
997
+ if (existsSync(d.publishedConfigPath)) {
998
+ try { oldPublished = JSON.parse(readFileSync(d.publishedConfigPath, "utf-8")); } catch {}
999
+ }
1000
+
1001
+ if (oldPublished?.characters) {
1002
+ const oldMap = new Map(oldPublished.characters.map((c: any) => [c.id, c]));
1003
+ for (const entry of published.characters) {
1004
+ const old: any = oldMap.get(entry.id);
1005
+ if (!old) continue;
1006
+ if (old.agentId && !entry.agentId) entry.agentId = old.agentId;
1007
+ if (old.agentStatus && !entry.agentStatus) entry.agentStatus = old.agentStatus;
1008
+ }
1009
+ }
1010
+
1011
+ const changeset = computeChangeset(oldPublished, published, d.soulsDir, d.defaultSoulsDir, pluginDir);
1012
+
1013
+ let agentResults: any[] = [];
1014
+ let agentError: string | null = null;
1015
+
1016
+ console.log(`[citizen-workshop] publish: ${changeset.changes.length} agent changes to process`);
1017
+
1018
+ if (changeset.changes.length > 0) {
1019
+ try {
1020
+ const { applyAgentChanges } = await import("./citizen-agent-manager.js");
1021
+ agentResults = await applyAgentChanges(changeset.changes);
1022
+ for (const r of agentResults) {
1023
+ const entry = published.characters.find((c: any) => c.id === r.citizenId);
1024
+ if (!entry) continue;
1025
+ if (r.success && (r.action === "create" || r.action === "update_soul")) {
1026
+ entry.agentId = r.agentId;
1027
+ entry.agentStatus = "active";
1028
+ } else if (r.success && r.action === "disable") {
1029
+ delete entry.agentId;
1030
+ entry.agentStatus = "stopped";
1031
+ } else if (!r.success) {
1032
+ entry.agentStatus = "error";
1033
+ }
1034
+ }
1035
+ const failed = agentResults.filter((r: any) => !r.success);
1036
+ if (failed.length > 0) {
1037
+ agentError = failed.map((r: any) => `${r.citizenId}: ${r.error}`).join("; ");
1038
+ }
1039
+ } catch (err: any) {
1040
+ agentError = err?.message ?? "Agent 管理模块加载失败";
1041
+ for (const ch of changeset.changes) {
1042
+ if (ch.action === "create" || ch.action === "update_soul") {
1043
+ const entry = published.characters.find((c: any) => c.id === ch.citizenId);
1044
+ if (entry) entry.agentStatus = "error";
1045
+ }
1046
+ }
1047
+ }
1048
+ }
1049
+
1050
+ writeFileSync(
1051
+ d.publishedConfigPath,
1052
+ JSON.stringify(published, null, 2),
1053
+ "utf-8",
1054
+ );
1055
+
1056
+ syncTownDefaults(published, pluginDir);
1057
+
1058
+ const hasAgentChanges = changeset.changes.length > 0;
1059
+ const allSuccess = !agentError;
1060
+
1061
+ if (hasAgentChanges && !allSuccess) {
1062
+ jsonRes(res, {
1063
+ success: false,
1064
+ error: `发布配置已保存,但 Agent 操作失败: ${agentError}`,
1065
+ publishedAt: published.publishedAt,
1066
+ changeset,
1067
+ agentResults: agentResults.length > 0 ? agentResults : undefined,
1068
+ });
1069
+ } else {
1070
+ jsonRes(res, {
1071
+ success: true,
1072
+ publishedAt: published.publishedAt,
1073
+ changeset,
1074
+ agentResults: agentResults.length > 0 ? agentResults : undefined,
1075
+ stewardSoulUpdated: changeset.stewardSoulUpdated || undefined,
1076
+ });
1077
+ }
1078
+ } catch (e: any) {
1079
+ console.error("[citizen-workshop] publish error:", e);
1080
+ jsonRes(res, { error: `发布失败: ${e.message ?? "未知错误"}`, stack: e.stack }, 500);
1081
+ }
1082
+ return true;
1083
+ }
1084
+
1085
+ if (route === "load-published") {
1086
+ if (existsSync(d.publishedConfigPath)) {
1087
+ try {
1088
+ const config = JSON.parse(readFileSync(d.publishedConfigPath, "utf-8"));
1089
+ jsonRes(res, { config });
1090
+ } catch {
1091
+ jsonRes(res, { config: null });
1092
+ }
1093
+ } else {
1094
+ jsonRes(res, { config: null });
1095
+ }
1096
+ return true;
1097
+ }
1098
+
1099
+ if (route === "load-soul") {
1100
+ const content = loadSoulContent(
1101
+ body.name as string,
1102
+ d.soulsDir,
1103
+ d.defaultSoulsDir,
1104
+ );
1105
+ jsonRes(res, { content });
1106
+ return true;
1107
+ }
1108
+
1109
+ if (route === "agents") {
1110
+ jsonRes(res, { agents: loadAgentList() });
1111
+ return true;
1112
+ }
1113
+
1114
+ if (route === "buildings") {
1115
+ try {
1116
+ const mapDraftPath = join(pluginDir, "town-frontend", "town-map.json");
1117
+ let buildings: any[] = [];
1118
+ if (existsSync(mapDraftPath)) {
1119
+ const map = JSON.parse(readFileSync(mapDraftPath, "utf-8"));
1120
+ buildings = (map.buildings ?? [])
1121
+ .filter((b: any) => b.role === "house")
1122
+ .map((b: any) => ({
1123
+ id: b.id,
1124
+ name: b.displayName || b.modelKey || b.id,
1125
+ }));
1126
+ }
1127
+ jsonRes(res, { buildings });
1128
+ } catch {
1129
+ jsonRes(res, { buildings: [] });
1130
+ }
1131
+ return true;
1132
+ }
1133
+
1134
+ if (route === "upload-avatar") {
1135
+ const { fileName, imageData } = body as {
1136
+ fileName: string;
1137
+ imageData: string;
1138
+ };
1139
+ if (!fileName || !imageData) {
1140
+ jsonRes(res, { error: "Missing data" }, 400);
1141
+ return true;
1142
+ }
1143
+ const safeName = fileName.replace(/[^a-zA-Z0-9_\-]/g, "_");
1144
+ const ext = imageData.startsWith("data:image/png") ? ".png" : ".webp";
1145
+ const base64 = imageData.split(",")[1];
1146
+ if (!base64) {
1147
+ jsonRes(res, { error: "Invalid image data" }, 400);
1148
+ return true;
1149
+ }
1150
+ const outName = `${safeName}${ext}`;
1151
+ writeFileSync(join(d.avatarsDir, outName), Buffer.from(base64, "base64"));
1152
+ jsonRes(res, {
1153
+ success: true,
1154
+ url: `/citizen-workshop/avatars/${outName}`,
1155
+ });
1156
+ return true;
1157
+ }
1158
+
1159
+ if (route === "generate-soul") {
1160
+ const { name, bio, industry, specialty } = body as {
1161
+ name: string;
1162
+ bio: string;
1163
+ industry: string;
1164
+ specialty: string;
1165
+ };
1166
+ if (!name || !bio) {
1167
+ jsonRes(res, { error: "Missing name/bio" }, 400);
1168
+ return true;
1169
+ }
1170
+ try {
1171
+ const { chat, isAvailable } = await import("./llm-agent-proxy.js");
1172
+ const { buildPersonaPrompt } = await import("./soul-prompt-template.js");
1173
+ const prompt = buildPersonaPrompt({ name, bio, specialty: specialty || "通用助手" });
1174
+
1175
+ if (isAvailable()) {
1176
+ const result = await chat({
1177
+ system: prompt.system,
1178
+ user: prompt.user,
1179
+ maxTokens: 2000,
1180
+ temperature: 0.8,
1181
+ stop: [],
1182
+ });
1183
+ if (result.text) {
1184
+ jsonRes(res, { content: result.text });
1185
+ return true;
1186
+ }
1187
+ }
1188
+
1189
+ const text = await generateSoulViaAgent(prompt.system, prompt.user);
1190
+ if (text) {
1191
+ jsonRes(res, { content: text });
1192
+ } else {
1193
+ jsonRes(res, { error: "LLM 返回为空" }, 500);
1194
+ }
1195
+ } catch (err: any) {
1196
+ console.error("[citizen-workshop] generate-soul error:", err?.message);
1197
+ jsonRes(res, { error: `生成失败: ${err?.message ?? "未知错误"}` }, 500);
1198
+ }
1199
+ return true;
1200
+ }
1201
+
1202
+ if (route === "upload-anim") {
1203
+ const { data, fileName } = body as { data: string; fileName?: string };
1204
+ if (!data) {
1205
+ jsonRes(res, { error: "Missing data" }, 400);
1206
+ return true;
1207
+ }
1208
+ const id = randomUUID();
1209
+ const safeName = (fileName || "anim").replace(/[^a-zA-Z0-9_\-\.]/g, "_");
1210
+ const outName = `${id}_${safeName}.glb`;
1211
+ writeFileSync(join(d.animDir, outName), Buffer.from(data, "base64"));
1212
+ jsonRes(res, {
1213
+ success: true,
1214
+ url: `/custom-assets/animations/${outName}`,
1215
+ id,
1216
+ });
1217
+ return true;
1218
+ }
1219
+
1220
+ if (route === "delete-anim") {
1221
+ const { url } = body as { url: string };
1222
+ if (!url) {
1223
+ jsonRes(res, { error: "Missing url" }, 400);
1224
+ return true;
1225
+ }
1226
+ const fn = url.split("/").pop();
1227
+ if (fn) {
1228
+ const filePath = join(d.animDir, fn);
1229
+ if (filePath.startsWith(d.animDir) && existsSync(filePath)) {
1230
+ unlinkSync(filePath);
1231
+ }
1232
+ }
1233
+ jsonRes(res, { success: true });
1234
+ return true;
1235
+ }
1236
+
1237
+ jsonRes(res, { error: "Unknown API" }, 404);
1238
+ return true;
1239
+ }
1240
+
1241
+ // ── custom-assets static file serving ──
1242
+
1243
+ function handleCustomAssetsStatic(
1244
+ res: any,
1245
+ urlPath: string,
1246
+ customAssetsDir: string,
1247
+ ): boolean {
1248
+ const relPath = urlPath.slice("/custom-assets/".length);
1249
+ const filePath = join(customAssetsDir, decodeURIComponent(relPath));
1250
+ return serveFile(res, filePath, customAssetsDir);
1251
+ }
1252
+
1253
+ // ── Main entry: handle all editor-related requests ──
1254
+
1255
+ export async function handleEditorRequest(
1256
+ req: any,
1257
+ res: any,
1258
+ pluginDir: string,
1259
+ ): Promise<boolean> {
1260
+ const url = req.url ?? "/";
1261
+ const urlPath =
1262
+ typeof url === "string" && url.startsWith("/")
1263
+ ? url.split("?")[0]
1264
+ : new URL(url, "http://localhost").pathname;
1265
+ const method = req.method ?? "GET";
1266
+
1267
+ const d = resolveDirs(pluginDir);
1268
+
1269
+ // /ext-assets/* → serve Characters_1 / Map_1 from pluginDir/assets/
1270
+ if (urlPath.startsWith("/ext-assets/") && method === "GET") {
1271
+ return handleExtAssets(res, urlPath, d.extAssetsDir);
1272
+ }
1273
+
1274
+ // /assets/models/megapack/gltf/* → serve from pluginDir/assets/Map_1/
1275
+ if (urlPath.startsWith("/assets/models/megapack/gltf/") && method === "GET") {
1276
+ const relPath = decodeURIComponent(urlPath.slice("/assets/models/megapack/gltf/".length));
1277
+ const megapackBase = join(d.extAssetsDir, "Map_1");
1278
+ const filePath = join(megapackBase, relPath);
1279
+ return serveFile(res, filePath, megapackBase, "public, max-age=86400");
1280
+ }
1281
+
1282
+ // /citizen-workshop/avatars/* → serve user-uploaded avatars
1283
+ if (
1284
+ urlPath.startsWith("/citizen-workshop/avatars/") &&
1285
+ method === "GET"
1286
+ ) {
1287
+ return handleCitizenAvatars(res, urlPath, d.avatarsDir);
1288
+ }
1289
+
1290
+ // /custom-assets/animations/* → serve custom anim files
1291
+ if (
1292
+ urlPath.startsWith("/custom-assets/animations/") &&
1293
+ method === "GET"
1294
+ ) {
1295
+ return handleAnimations(res, urlPath, d.animDir);
1296
+ }
1297
+
1298
+ // /custom-assets/_api/* → custom assets CRUD API
1299
+ if (
1300
+ urlPath.startsWith("/custom-assets/_api/") &&
1301
+ method === "POST"
1302
+ ) {
1303
+ const route = urlPath.slice("/custom-assets/_api/".length);
1304
+ return handleCustomAssetsApi(
1305
+ req,
1306
+ res,
1307
+ route,
1308
+ d.customAssetsDir,
1309
+ d.catalogPath,
1310
+ );
1311
+ }
1312
+
1313
+ // /custom-assets/* → static file serving for custom assets
1314
+ if (urlPath.startsWith("/custom-assets/")) {
1315
+ return handleCustomAssetsStatic(res, urlPath, d.customAssetsDir);
1316
+ }
1317
+
1318
+ // /citizen-workshop/_api/media?path=... → serve media files (GET)
1319
+ if (
1320
+ urlPath === "/citizen-workshop/_api/media" &&
1321
+ method === "GET"
1322
+ ) {
1323
+ const urlObj = typeof url === "string" && url.startsWith("/")
1324
+ ? new URL(url, "http://localhost")
1325
+ : new URL(url ?? "/", "http://localhost");
1326
+ const filePath = urlObj.searchParams.get("path") ?? "";
1327
+ if (!filePath || !existsSync(filePath) || !statSync(filePath).isFile()) {
1328
+ res.writeHead(404);
1329
+ res.end("Not Found");
1330
+ return true;
1331
+ }
1332
+ const ext = filePath.substring(filePath.lastIndexOf("."));
1333
+ const mime = MIME_TYPES[ext] ?? "application/octet-stream";
1334
+ res.writeHead(200, {
1335
+ "Content-Type": mime,
1336
+ "Access-Control-Allow-Origin": "*",
1337
+ "Cache-Control": "public, max-age=3600",
1338
+ });
1339
+ res.end(readFileSync(filePath));
1340
+ return true;
1341
+ }
1342
+
1343
+ // /citizen-workshop/_api/* → citizen workshop API
1344
+ if (
1345
+ urlPath.startsWith("/citizen-workshop/_api/") &&
1346
+ method === "POST"
1347
+ ) {
1348
+ const route = urlPath.slice("/citizen-workshop/_api/".length).split("?")[0];
1349
+ return handleCitizenWorkshopApi(req, res, route, pluginDir);
1350
+ }
1351
+
1352
+ // /board/plans → read-only plan snapshot for office whiteboard
1353
+ if (urlPath === "/board/plans" && method === "GET") {
1354
+ const { snapshotPlansForDisplay } = await import("./plan-manager.js");
1355
+ const plans = snapshotPlansForDisplay();
1356
+ res.writeHead(200, {
1357
+ "Content-Type": "application/json",
1358
+ "Access-Control-Allow-Origin": "*",
1359
+ "Cache-Control": "no-cache",
1360
+ });
1361
+ res.end(JSON.stringify({ success: true, plans }));
1362
+ return true;
1363
+ }
1364
+
1365
+ return false;
1366
+ }
1367
+
1368
+ export { ensureEditorDirs, MIME_TYPES };