@lemoncat7/dsh-partner 2.23.55 → 2.24.0-rc.9

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 (194) hide show
  1. package/README.md +5 -0
  2. package/docs/compatibility-0.2.2-rc.1.md +7 -0
  3. package/docs/releases/2.24.0-rc.2.md +9 -0
  4. package/docs/releases/2.24.0-rc.3.md +6 -0
  5. package/docs/releases/2.24.0-rc.4.md +11 -0
  6. package/docs/releases/2.24.0-rc.5.md +11 -0
  7. package/docs/releases/2.24.0-rc.6.md +12 -0
  8. package/docs/releases/2.24.0-rc.9.md +5 -0
  9. package/lib/agent-runtime.d.ts +1 -0
  10. package/lib/agent-runtime.d.ts.map +1 -1
  11. package/lib/agent-runtime.js +54 -5
  12. package/lib/agent-runtime.js.map +1 -1
  13. package/lib/api.d.ts +1 -0
  14. package/lib/api.d.ts.map +1 -1
  15. package/lib/api.js +68 -1
  16. package/lib/api.js.map +1 -1
  17. package/lib/channels/activity-defaults.d.ts.map +1 -1
  18. package/lib/channels/activity-defaults.js +2 -0
  19. package/lib/channels/activity-defaults.js.map +1 -1
  20. package/lib/channels/bilibili/activity-provider.d.ts +3 -0
  21. package/lib/channels/bilibili/activity-provider.d.ts.map +1 -0
  22. package/lib/channels/bilibili/activity-provider.js +8 -0
  23. package/lib/channels/bilibili/activity-provider.js.map +1 -0
  24. package/lib/channels/bilibili/catalog.d.ts +23 -0
  25. package/lib/channels/bilibili/catalog.d.ts.map +1 -0
  26. package/lib/channels/bilibili/catalog.js +45 -0
  27. package/lib/channels/bilibili/catalog.js.map +1 -0
  28. package/lib/channels/bilibili/media.d.ts +9 -0
  29. package/lib/channels/bilibili/media.d.ts.map +1 -0
  30. package/lib/channels/bilibili/media.js +67 -0
  31. package/lib/channels/bilibili/media.js.map +1 -0
  32. package/lib/channels/bilibili/service.d.ts +37 -0
  33. package/lib/channels/bilibili/service.d.ts.map +1 -0
  34. package/lib/channels/bilibili/service.js +138 -0
  35. package/lib/channels/bilibili/service.js.map +1 -0
  36. package/lib/channels/bilibili/tools.d.ts +6 -0
  37. package/lib/channels/bilibili/tools.d.ts.map +1 -0
  38. package/lib/channels/bilibili/tools.js +48 -0
  39. package/lib/channels/bilibili/tools.js.map +1 -0
  40. package/lib/channels/direct/message-format.js +298 -206
  41. package/lib/channels/direct/message-format.js.map +4 -4
  42. package/lib/channels/manager.d.ts.map +1 -1
  43. package/lib/channels/manager.js +8 -2
  44. package/lib/channels/manager.js.map +1 -1
  45. package/lib/channels/qq/authority.d.ts +1 -1
  46. package/lib/channels/qq/authority.d.ts.map +1 -1
  47. package/lib/channels/qq/authority.js +2 -2
  48. package/lib/channels/qq/authority.js.map +1 -1
  49. package/lib/channels/weixin/connect.d.ts +3 -1
  50. package/lib/channels/weixin/connect.d.ts.map +1 -1
  51. package/lib/character/affection-tool.js +2 -2
  52. package/lib/character/affection-tool.js.map +1 -1
  53. package/lib/character/affection.d.ts +6 -42
  54. package/lib/character/affection.d.ts.map +1 -1
  55. package/lib/character/affection.js +4 -13
  56. package/lib/character/affection.js.map +1 -1
  57. package/lib/character/conversation-behavior.js +2 -2
  58. package/lib/character/conversation-behavior.js.map +1 -1
  59. package/lib/character/habit-tool.d.ts +4 -0
  60. package/lib/character/habit-tool.d.ts.map +1 -0
  61. package/lib/character/habit-tool.js +31 -0
  62. package/lib/character/habit-tool.js.map +1 -0
  63. package/lib/character/habits.d.ts +38 -0
  64. package/lib/character/habits.d.ts.map +1 -0
  65. package/lib/character/habits.js +92 -0
  66. package/lib/character/habits.js.map +1 -0
  67. package/lib/character/idle-contact.d.ts +2 -0
  68. package/lib/character/idle-contact.d.ts.map +1 -1
  69. package/lib/character/idle-contact.js +5 -1
  70. package/lib/character/idle-contact.js.map +1 -1
  71. package/lib/character/interaction-review.d.ts +10 -0
  72. package/lib/character/interaction-review.d.ts.map +1 -1
  73. package/lib/character/interaction-review.js +51 -31
  74. package/lib/character/interaction-review.js.map +1 -1
  75. package/lib/chat/live-speech.d.ts +5 -2
  76. package/lib/chat/live-speech.d.ts.map +1 -1
  77. package/lib/chat/live-speech.js +37 -9
  78. package/lib/chat/live-speech.js.map +1 -1
  79. package/lib/chat/persona-delivery.d.ts +29 -0
  80. package/lib/chat/persona-delivery.d.ts.map +1 -0
  81. package/lib/chat/persona-delivery.js +129 -0
  82. package/lib/chat/persona-delivery.js.map +1 -0
  83. package/lib/chat/reply-continuity.d.ts.map +1 -1
  84. package/lib/chat/reply-continuity.js +12 -0
  85. package/lib/chat/reply-continuity.js.map +1 -1
  86. package/lib/chat/tools.d.ts.map +1 -1
  87. package/lib/chat/tools.js +22 -10
  88. package/lib/chat/tools.js.map +1 -1
  89. package/lib/chat/work-expression.d.ts +36 -0
  90. package/lib/chat/work-expression.d.ts.map +1 -1
  91. package/lib/chat/work-expression.js +80 -17
  92. package/lib/chat/work-expression.js.map +1 -1
  93. package/lib/client-api.d.ts +10 -1
  94. package/lib/client-api.d.ts.map +1 -1
  95. package/lib/client-api.js.map +1 -1
  96. package/lib/client.d.ts.map +1 -1
  97. package/lib/client.js +4186 -3842
  98. package/lib/client.js.map +4 -4
  99. package/lib/collaboration/composition.d.ts.map +1 -1
  100. package/lib/collaboration/composition.js +2 -0
  101. package/lib/collaboration/composition.js.map +1 -1
  102. package/lib/collaboration/tool-discovery.d.ts.map +1 -1
  103. package/lib/collaboration/tool-discovery.js +5 -4
  104. package/lib/collaboration/tool-discovery.js.map +1 -1
  105. package/lib/companions/identity-tool.d.ts +1 -1
  106. package/lib/companions/identity-tool.d.ts.map +1 -1
  107. package/lib/companions/identity-tool.js +3 -4
  108. package/lib/companions/identity-tool.js.map +1 -1
  109. package/lib/companions/identity.js +4 -4
  110. package/lib/companions/identity.js.map +1 -1
  111. package/lib/companions/persona-refinement.d.ts.map +1 -1
  112. package/lib/companions/persona-refinement.js +1 -45
  113. package/lib/companions/persona-refinement.js.map +1 -1
  114. package/lib/compat/native-presentation.d.ts +4 -0
  115. package/lib/compat/native-presentation.d.ts.map +1 -0
  116. package/lib/compat/native-presentation.js +40 -0
  117. package/lib/compat/native-presentation.js.map +1 -0
  118. package/lib/compat/ptc-events.d.ts +2 -0
  119. package/lib/compat/ptc-events.d.ts.map +1 -0
  120. package/lib/compat/ptc-events.js +2 -0
  121. package/lib/compat/ptc-events.js.map +1 -0
  122. package/lib/compat/tool-dispatch.d.ts +4 -0
  123. package/lib/compat/tool-dispatch.d.ts.map +1 -0
  124. package/lib/compat/tool-dispatch.js +14 -0
  125. package/lib/compat/tool-dispatch.js.map +1 -0
  126. package/lib/concern-tool.d.ts.map +1 -1
  127. package/lib/concern-tool.js +5 -3
  128. package/lib/concern-tool.js.map +1 -1
  129. package/lib/domain.d.ts +10 -1
  130. package/lib/domain.d.ts.map +1 -1
  131. package/lib/domain.js.map +1 -1
  132. package/lib/execution/operation-tool-access.d.ts.map +1 -1
  133. package/lib/execution/operation-tool-access.js +2 -6
  134. package/lib/execution/operation-tool-access.js.map +1 -1
  135. package/lib/execution/service.d.ts +2 -0
  136. package/lib/execution/service.d.ts.map +1 -1
  137. package/lib/execution/service.js +7 -1
  138. package/lib/execution/service.js.map +1 -1
  139. package/lib/execution/task-preamble.js +2 -2
  140. package/lib/execution/task-preamble.js.map +1 -1
  141. package/lib/execution/work-progress-card.d.ts +10 -0
  142. package/lib/execution/work-progress-card.d.ts.map +1 -0
  143. package/lib/execution/work-progress-card.js +45 -0
  144. package/lib/execution/work-progress-card.js.map +1 -0
  145. package/lib/execution/work-progress-state.d.ts +32 -0
  146. package/lib/execution/work-progress-state.d.ts.map +1 -0
  147. package/lib/execution/work-progress-state.js +19 -0
  148. package/lib/execution/work-progress-state.js.map +1 -0
  149. package/lib/execution/work-progress.d.ts +16 -0
  150. package/lib/execution/work-progress.d.ts.map +1 -0
  151. package/lib/execution/work-progress.js +76 -0
  152. package/lib/execution/work-progress.js.map +1 -0
  153. package/lib/execution/work-tool.d.ts.map +1 -1
  154. package/lib/execution/work-tool.js +62 -51
  155. package/lib/execution/work-tool.js.map +1 -1
  156. package/lib/index.d.ts.map +1 -1
  157. package/lib/index.js +5 -2
  158. package/lib/index.js.map +1 -1
  159. package/lib/memory-store.d.ts +1 -1
  160. package/lib/observation-loop.d.ts.map +1 -1
  161. package/lib/observation-loop.js +17 -3
  162. package/lib/observation-loop.js.map +1 -1
  163. package/lib/store.d.ts.map +1 -1
  164. package/lib/store.js +9 -0
  165. package/lib/store.js.map +1 -1
  166. package/lib/ui/affection-panel.js +1 -1
  167. package/lib/ui/affection-panel.js.map +1 -1
  168. package/lib/ui/bilibili-settings.d.ts +12 -0
  169. package/lib/ui/bilibili-settings.d.ts.map +1 -0
  170. package/lib/ui/bilibili-settings.js +77 -0
  171. package/lib/ui/bilibili-settings.js.map +1 -0
  172. package/lib/ui/channels-panel.d.ts.map +1 -1
  173. package/lib/ui/channels-panel.js +4 -3
  174. package/lib/ui/channels-panel.js.map +1 -1
  175. package/lib/ui/companion-vitals.js +1 -1
  176. package/lib/ui/companion-vitals.js.map +1 -1
  177. package/lib/ui/habits-panel.d.ts +4 -0
  178. package/lib/ui/habits-panel.d.ts.map +1 -0
  179. package/lib/ui/habits-panel.js +30 -0
  180. package/lib/ui/habits-panel.js.map +1 -0
  181. package/lib/ui/identity-editor.js +1 -8
  182. package/lib/ui/identity-editor.js.map +1 -1
  183. package/lib/ui/idle-contact-editor.d.ts.map +1 -1
  184. package/lib/ui/idle-contact-editor.js +2 -1
  185. package/lib/ui/idle-contact-editor.js.map +1 -1
  186. package/lib/ui/push-draft.d.ts +2 -2
  187. package/lib/ui/workspace-navigation.d.ts +16 -16
  188. package/package.json +62 -48
  189. package/resources/bilibili/Dockerfile +7 -0
  190. package/resources/bilibili/media.py +245 -0
  191. package/resources/bilibili/requirements.txt +3 -0
  192. package/resources/bilibili/server.py +300 -0
  193. package/resources/bilibili/test_bridge.py +115 -0
  194. package/resources/bilibili/test_media.py +110 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lemoncat7/dsh-partner",
3
- "version": "2.23.55",
3
+ "version": "2.24.0-rc.9",
4
4
  "description": "Long-lived AI companions with QQ, WeChat, Matrix and Mattermost channels for DeepSeek Harness",
5
5
  "keywords": [
6
6
  "deepseek-harness",
@@ -31,6 +31,7 @@
31
31
  }
32
32
  },
33
33
  "files": [
34
+ "docs/releases/2.24.0-rc.9.md",
34
35
  "docs/releases/2.23.54.md",
35
36
  "docs/releases/2.23.53.md",
36
37
  "docs/releases/2.23.52.md",
@@ -199,7 +200,16 @@
199
200
  "docs/releases/2.23.21.md",
200
201
  "docs/releases/2.23.22.md",
201
202
  "docs/releases/2.23.23.md",
202
- "docs/releases/2.23.24.md"
203
+ "docs/releases/2.23.24.md",
204
+ "resources/bilibili/*.py",
205
+ "resources/bilibili/requirements.txt",
206
+ "resources/bilibili/Dockerfile",
207
+ "docs/releases/2.24.0-rc.2.md",
208
+ "docs/releases/2.24.0-rc.3.md",
209
+ "docs/releases/2.24.0-rc.4.md",
210
+ "docs/releases/2.24.0-rc.5.md",
211
+ "docs/releases/2.24.0-rc.6.md",
212
+ "docs/compatibility-0.2.2-rc.1.md"
203
213
  ],
204
214
  "dsh": {
205
215
  "bundle": {
@@ -219,8 +229,8 @@
219
229
  }
220
230
  },
221
231
  "engines": {
222
- "node": "^22.19.0 || >=24.0.0",
223
- "dsh": "^0.2.1-rc.2"
232
+ "node": ">=24.9.0",
233
+ "dsh": "=0.2.2-rc.1"
224
234
  },
225
235
  "scripts": {
226
236
  "clean": "node -e \"require('node:fs').rmSync('lib', { recursive: true, force: true })\"",
@@ -231,7 +241,7 @@
231
241
  "pack:verify": "node scripts/check-package.mjs"
232
242
  },
233
243
  "dependencies": {
234
- "@deepseek-ai/schemastery": "~3.18.5",
244
+ "@deepseek-ai/schemastery": "~3.19.0",
235
245
  "@modelcontextprotocol/sdk": "^1.30.0",
236
246
  "mdast-util-from-markdown": "2.0.3",
237
247
  "mdast-util-gfm": "3.1.0",
@@ -239,50 +249,52 @@
239
249
  "micromark-extension-gfm": "3.0.0"
240
250
  },
241
251
  "peerDependencies": {
242
- "@deepseek-ai/cordis": "^4.0.5",
243
- "@deepseek-ai/dsh-agent": "^0.2.1-rc.2",
244
- "@deepseek-ai/dsh-agent-default-model": "^0.2.1-rc.2",
245
- "@deepseek-ai/dsh-agent-preset-registry": "^0.2.1-rc.2",
246
- "@deepseek-ai/dsh-attachment": "^0.2.1-rc.2",
247
- "@deepseek-ai/dsh-credentials": "^0.2.1-rc.2",
248
- "@deepseek-ai/dsh-llm": "^0.2.1-rc.2",
249
- "@deepseek-ai/dsh-scope": "^0.2.1-rc.2",
250
- "@deepseek-ai/dsh-session": "^0.2.1-rc.2",
251
- "@deepseek-ai/dsh-settings": "^0.2.1-rc.2",
252
- "@deepseek-ai/dsh-system-prompt": "^0.2.1-rc.2",
253
- "@deepseek-ai/dsh-tools": "^0.2.1-rc.2",
254
- "@deepseek-ai/dsh-user-questions": "^0.2.1-rc.2",
255
- "@deepseek-ai/dsh-workspace": "^0.2.1-rc.2"
252
+ "@deepseek-ai/cordis": "^4.1.0",
253
+ "@deepseek-ai/dsh-agent": "^0.2.2-rc.1",
254
+ "@deepseek-ai/dsh-agent-default-model": "^0.2.2-rc.1",
255
+ "@deepseek-ai/dsh-agent-preset-registry": "^0.2.2-rc.1",
256
+ "@deepseek-ai/dsh-attachment": "^0.2.2-rc.1",
257
+ "@deepseek-ai/dsh-credentials": "^0.2.2-rc.1",
258
+ "@deepseek-ai/dsh-llm": "^0.2.2-rc.1",
259
+ "@deepseek-ai/dsh-scope": "^0.2.2-rc.1",
260
+ "@deepseek-ai/dsh-session": "^0.2.2-rc.1",
261
+ "@deepseek-ai/dsh-settings": "^0.2.2-rc.1",
262
+ "@deepseek-ai/dsh-system-prompt": "^0.2.2-rc.1",
263
+ "@deepseek-ai/dsh-tools": "^0.2.2-rc.1",
264
+ "@deepseek-ai/dsh-user-questions": "^0.2.2-rc.1",
265
+ "@deepseek-ai/dsh-workspace": "^0.2.2-rc.1",
266
+ "@deepseek-ai/dsh-tool-executor-native": "^0.2.2-rc.1",
267
+ "@deepseek-ai/dsh-tool-executor-ptc": "^0.2.2-rc.1"
256
268
  },
257
269
  "devDependencies": {
258
- "@deepseek-ai/cordis": "~4.0.5",
259
- "@deepseek-ai/dsh-agent": "0.2.1-rc.2",
260
- "@deepseek-ai/dsh-agent-default-model": "0.2.1-rc.2",
261
- "@deepseek-ai/dsh-agent-preset-registry": "0.2.1-rc.2",
262
- "@deepseek-ai/dsh-api-remotes": "0.2.1-rc.2",
263
- "@deepseek-ai/dsh-api-session-controller": "0.2.1-rc.2",
264
- "@deepseek-ai/dsh-attachment": "0.2.1-rc.2",
265
- "@deepseek-ai/dsh-client-ui-conversation": "0.2.1-rc.2",
266
- "@deepseek-ai/dsh-client-ui-layout": "0.2.1-rc.2",
267
- "@deepseek-ai/dsh-client-ui-primitives": "0.2.1-rc.2",
268
- "@deepseek-ai/dsh-client-ui-renderer": "0.2.1-rc.2",
269
- "@deepseek-ai/dsh-client-ui-sidebar": "0.2.1-rc.2",
270
- "@deepseek-ai/dsh-client-ui-slots": "0.2.1-rc.2",
271
- "@deepseek-ai/dsh-client-ui-theme": "0.2.1-rc.2",
272
- "@deepseek-ai/dsh-client-ui-workspace": "0.2.1-rc.2",
273
- "@deepseek-ai/dsh-credentials": "0.2.1-rc.2",
274
- "@deepseek-ai/dsh-llm": "0.2.1-rc.2",
275
- "@deepseek-ai/dsh-ptc-runtime": "0.2.1-rc.2",
276
- "@deepseek-ai/dsh-sandbox": "0.2.1-rc.2",
277
- "@deepseek-ai/dsh-sandbox-policy": "0.2.1-rc.2",
278
- "@deepseek-ai/dsh-scope": "0.2.1-rc.2",
279
- "@deepseek-ai/dsh-session": "0.2.1-rc.2",
280
- "@deepseek-ai/dsh-settings": "0.2.1-rc.2",
281
- "@deepseek-ai/dsh-system-prompt": "0.2.1-rc.2",
282
- "@deepseek-ai/dsh-tools": "0.2.1-rc.2",
283
- "@deepseek-ai/dsh-user-approval": "0.2.1-rc.2",
284
- "@deepseek-ai/dsh-user-questions": "0.2.1-rc.2",
285
- "@deepseek-ai/dsh-workspace": "0.2.1-rc.2",
270
+ "@deepseek-ai/cordis": "~4.1.0",
271
+ "@deepseek-ai/dsh-agent": "0.2.2-rc.1",
272
+ "@deepseek-ai/dsh-agent-default-model": "0.2.2-rc.1",
273
+ "@deepseek-ai/dsh-agent-preset-registry": "0.2.2-rc.1",
274
+ "@deepseek-ai/dsh-api-remotes": "0.2.2-rc.1",
275
+ "@deepseek-ai/dsh-api-session-controller": "0.2.2-rc.1",
276
+ "@deepseek-ai/dsh-attachment": "0.2.2-rc.1",
277
+ "@deepseek-ai/dsh-client-ui-conversation": "0.2.2-rc.1",
278
+ "@deepseek-ai/dsh-client-ui-layout": "0.2.2-rc.1",
279
+ "@deepseek-ai/dsh-client-ui-primitives": "0.2.2-rc.1",
280
+ "@deepseek-ai/dsh-client-ui-renderer": "0.2.2-rc.1",
281
+ "@deepseek-ai/dsh-client-ui-sidebar": "0.2.2-rc.1",
282
+ "@deepseek-ai/dsh-client-ui-slots": "0.2.2-rc.1",
283
+ "@deepseek-ai/dsh-client-ui-theme": "0.2.2-rc.1",
284
+ "@deepseek-ai/dsh-client-ui-workspace": "0.2.2-rc.1",
285
+ "@deepseek-ai/dsh-credentials": "0.2.2-rc.1",
286
+ "@deepseek-ai/dsh-llm": "0.2.2-rc.1",
287
+ "@deepseek-ai/dsh-ptc-runtime": "0.2.2-rc.1",
288
+ "@deepseek-ai/dsh-sandbox": "0.2.2-rc.1",
289
+ "@deepseek-ai/dsh-sandbox-policy": "0.2.2-rc.1",
290
+ "@deepseek-ai/dsh-scope": "0.2.2-rc.1",
291
+ "@deepseek-ai/dsh-session": "0.2.2-rc.1",
292
+ "@deepseek-ai/dsh-settings": "0.2.2-rc.1",
293
+ "@deepseek-ai/dsh-system-prompt": "0.2.2-rc.1",
294
+ "@deepseek-ai/dsh-tools": "0.2.2-rc.1",
295
+ "@deepseek-ai/dsh-user-approval": "0.2.2-rc.1",
296
+ "@deepseek-ai/dsh-user-questions": "0.2.2-rc.1",
297
+ "@deepseek-ai/dsh-workspace": "0.2.2-rc.1",
286
298
  "@dimforge/rapier3d-compat": "0.20.0",
287
299
  "@hugeicons/core-free-icons": "^4.3.5",
288
300
  "@hugeicons/react": "^1.1.10",
@@ -295,7 +307,9 @@
295
307
  "react-dom": "18.3.1",
296
308
  "three": "0.185.1",
297
309
  "typescript": "^5.9.3",
298
- "ws": "^8.22.0"
310
+ "ws": "^8.22.0",
311
+ "@deepseek-ai/dsh-tool-executor-native": "0.2.2-rc.1",
312
+ "@deepseek-ai/dsh-tool-executor-ptc": "0.2.2-rc.1"
299
313
  },
300
314
  "publishConfig": {
301
315
  "access": "public"
@@ -0,0 +1,7 @@
1
+ FROM python:3.11-slim
2
+ WORKDIR /app
3
+ COPY requirements.txt .
4
+ RUN pip install --no-cache-dir -r requirements.txt
5
+ COPY --chmod=644 server.py media.py .
6
+ USER 1000:1000
7
+ CMD ["python", "server.py"]
@@ -0,0 +1,245 @@
1
+ """Staged video submission. Playback APIs only return links; no downloads."""
2
+ import asyncio, json, os, re, shutil, time, uuid
3
+ from pathlib import Path
4
+ from urllib.parse import urlparse
5
+ from aiohttp import ClientSession, ClientTimeout, ClientError, web
6
+ from bilibili_api import video, video_uploader, dynamic
7
+ from bilibili_api.utils.picture import Picture
8
+ from bilibili_api.utils.network import Api, get_client, BiliAPIFile
9
+
10
+ HEADERS={'User-Agent':'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36','Referer':'https://www.bilibili.com/'}
11
+ MAX_FILE=512*1024*1024
12
+ try:
13
+ from imageio_ffmpeg import get_ffmpeg_exe
14
+ FFMPEG=get_ffmpeg_exe()
15
+ except ImportError:
16
+ FFMPEG='ffmpeg'
17
+
18
+ def media_url(url):
19
+ p=urlparse(url)
20
+ if p.scheme!='https' or p.username or p.password or p.port not in (None,443) or not any((p.hostname or '').endswith('.'+h) for h in ('bilivideo.com','bilivideo.cn','hdslb.com')):
21
+ raise ValueError('视频来源不受支持')
22
+ return url
23
+
24
+ class LocalPicture(Picture):
25
+ def __init__(self,path):
26
+ from PIL import Image
27
+ self.path=str(path)
28
+ with Image.open(path) as im:
29
+ if im.format not in ('PNG','JPEG','WEBP') or im.width*im.height>25000000:raise ValueError('图片格式或尺寸无效')
30
+ self.width,self.height=im.size;self.imageType={'PNG':'png','JPEG':'jpg','WEBP':'webp'}[im.format];self.mime=im.get_format_mimetype();im.verify()
31
+ self.content=Path(path).read_bytes();self.size=len(self.content)//1024;self.url='file://'+str(path)
32
+ def _to_biliapifile(self):
33
+ return BiliAPIFile(path=self.path,mime_type=self.mime)
34
+
35
+ class UploadRejected(ValueError):
36
+ def __init__(self, phase, status, payload=None):
37
+ payload=payload if isinstance(payload,dict) else {}
38
+ code=payload.get('code');code=code if isinstance(code,int) else None
39
+ limited=code==601 or status==429
40
+ self.details={'phase':phase,'httpStatus':status,'code':code,'rateLimited':limited}
41
+ # Fixed descriptions prevent secrets or attacker-controlled upstream text leaking.
42
+ message='平台提示上传过快,请稍作休息后再继续' if limited else '平台拒绝上传请求,请核实登录状态和创作中心提示'
43
+ super().__init__(f'{phase}失败:HTTP {status}'+(f',业务码 {code}' if code is not None else '')+';'+message)
44
+
45
+ def checked_response(response,phase):
46
+ try:data=response.json()
47
+ except Exception:data={}
48
+ if response.code>=400 or data.get('OK')==0 or isinstance(data.get('code'),int) and data['code']!=0:
49
+ raise UploadRejected(phase,response.code,data)
50
+ return data
51
+
52
+ class UploadOnly(video_uploader.VideoUploader):
53
+ async def _preupload(self,page):
54
+ # SDK 17.4.2 discards the body on HTTP errors. Keep bounded diagnostic fields.
55
+ client=get_client()
56
+ resp=await client.request(method='GET',url='https://member.bilibili.com/preupload',
57
+ params={'profile':'ugcfx/bup','name':os.path.basename(page.path),'size':page.get_size(),'r':self.line['os'],'ssl':'0','version':'2.14.0','build':'2100400','upcdn':self.line['upcdn'],'probe_version':self.line['probe_version']},
58
+ cookies=await self.credential.get_buvid_cookies(),headers=HEADERS)
59
+ data=checked_response(resp,'预上传')
60
+ if data.get('OK')!=1 or not all(k in data for k in ('auth','endpoint','upos_uri','chunk_size','biz_id')):raise ValueError('预上传返回缺少上传配置,未上传素材')
61
+ data=self._switch_upload_endpoint(data,self.line)
62
+ url=media_url(self._get_upload_url(data))
63
+ resp=await client.request(method='POST',url=url,headers={**HEADERS,'x-upos-auth':data['auth']},params={'uploads':'','output':'json','profile':'ugcfx/bup','filesize':page.get_size(),'partsize':data['chunk_size'],'biz_id':data['biz_id']})
64
+ result=checked_response(resp,'初始化分片')
65
+ if result.get('OK')!=1 or not result.get('upload_id'):raise ValueError('分片初始化未返回任务编号')
66
+ data['upload_id']=result['upload_id'];return data
67
+ async def _submit(self, videos, cover_url=''):
68
+ return {'videos':videos,'cover':cover_url}
69
+
70
+ class Media:
71
+ def __init__(self, root, credential):
72
+ self.root=root/'media';self.root.mkdir(exist_ok=True);self.credential=credential
73
+ self.tasks={};self.limit=asyncio.Semaphore(2)
74
+ for path in self.root.glob('*/*/job.json'):
75
+ try:
76
+ j=json.loads(path.read_text())
77
+ if j['status'] in ('queued','running','submitting'):
78
+ j.update(status='unknown' if j['status']=='submitting' else 'interrupted',message='服务重启,任务未自动重试;若已提交投稿请在创作中心核实');self.save(path.parent,j)
79
+ except (ValueError,KeyError,OSError):pass
80
+ def directory(self,channel,jid):
81
+ if not re.fullmatch(r'bilibili-[A-Za-z0-9-]{1,80}',channel) or not re.fullmatch(r'[a-f0-9]{32}',jid):raise ValueError('媒体任务 ID 无效')
82
+ return self.root/channel/jid
83
+ def save(self,d,j):
84
+ d.mkdir(parents=True,exist_ok=True);p=d/'job.tmp';p.write_text(json.dumps(j));os.chmod(p,0o600);os.replace(p,d/'job.json')
85
+ def load(self,channel,jid):
86
+ d=self.directory(channel,jid)
87
+ if not (d/'job.json').exists():raise ValueError('媒体任务不存在或不属于本渠道')
88
+ return d,json.loads((d/'job.json').read_text())
89
+ def cleanup(self):
90
+ for p in self.root.glob('*/*/job.json'):
91
+ if time.time()-p.stat().st_mtime>7*86400 and p.parent.name not in self.tasks:shutil.rmtree(p.parent,ignore_errors=True)
92
+ size=sum(p.stat().st_size for p in self.root.glob('*/*/*') if p.is_file())
93
+ if size>2*1024**3:raise ValueError('视频暂存空间已满,请清理旧任务后重试')
94
+ def public(self,j):
95
+ return {k:j[k] for k in ('id','operation','status','createdAt','title','message','bvid','page','size','mimeType','published','result','uid','phase','error','retryAt','dynamic_id') if k in j}
96
+ def cooldown(self,channel):
97
+ # Channel validation also prevents arbitrary filesystem reads.
98
+ self.directory(channel,'0'*32)
99
+ p=self.root/channel/'cooldown.json'
100
+ if p.exists():
101
+ data=json.loads(p.read_text())
102
+ if data.get('retryAt',0)>time.time():return data
103
+ return {}
104
+ def record_failure(self,channel,j,e):
105
+ details=getattr(e,'details',None)
106
+ j.update(status='failed',published=False,message=str(e)[:250] if isinstance(e,ValueError) else '媒体操作失败('+type(e).__name__+')',phase=j.get('phase','上传素材'))
107
+ if details:
108
+ j['error']=details;j['phase']=details['phase']
109
+ if details.get('rateLimited'):
110
+ j['retryAt']=time.time()+900
111
+ p=self.root/channel/'cooldown.json';tmp=p.with_suffix('.tmp');tmp.write_text(json.dumps({'retryAt':j['retryAt'],'reason':'平台上传限流;十五分钟为本地保护间隔,不保证届时平台已解除限制'}));os.chmod(tmp,0o600);os.replace(tmp,p)
112
+ async def stage(self,request):
113
+ channel=request.match_info['channel'];jid=uuid.uuid4().hex;d=self.directory(channel,jid);self.cleanup()
114
+ d.mkdir(parents=True,exist_ok=True)
115
+ total=0;names=set();images=request.path.startswith('/stage-images/')
116
+ try:
117
+ reader=await request.multipart()
118
+ while (field:=await reader.next()) is not None:
119
+ if (not re.fullmatch(r'image[0-8]',field.name or '') if images else field.name not in ('video','cover')) or field.name in names:raise ValueError('只接受按顺序编号的1–9张图片' if images else '只接受一个视频和一个封面')
120
+ names.add(field.name);size=0
121
+ with (d/field.name).open('wb') as out:
122
+ while chunk:=await field.read_chunk(65536):
123
+ size+=len(chunk);total+=len(chunk)
124
+ if size>(MAX_FILE if field.name=='video' else 8*1024**2):raise ValueError('每张图片上限 8 MB' if images else '视频上限 512 MB,封面上限 8 MB')
125
+ out.write(chunk)
126
+ if not size:raise ValueError('媒体文件为空')
127
+ if images:
128
+ if not names or names!={f'image{i}' for i in range(len(names))}:raise ValueError('漫画图片须为连续的 1–9 张')
129
+ from PIL import Image
130
+ for name in names:
131
+ with Image.open(d/name) as im:
132
+ if im.format not in ('PNG','JPEG','WEBP') or im.width*im.height>25000000:raise ValueError('漫画图片须为 PNG/JPEG/WebP,最多2500万像素')
133
+ im.verify()
134
+ elif names!={'video','cover'}:raise ValueError('投稿需要视频与封面文件')
135
+ j={'id':jid,'operation':'stage','status':'staged','createdAt':time.time(),'size':total,'imageCount':len(names) if images else 0};self.save(d,j)
136
+ return web.json_response({'stageId':jid})
137
+ except BaseException:
138
+ shutil.rmtree(d,ignore_errors=True);raise
139
+ async def probe(self,path):
140
+ proc=await asyncio.create_subprocess_exec(FFMPEG,'-nostdin','-v','error','-protocol_whitelist','file,pipe','-format_whitelist','mov,matroska,webm,avi,flv','-i',str(path),'-map','0:v:0','-frames:v','1','-f','null','-',stdout=asyncio.subprocess.DEVNULL,stderr=asyncio.subprocess.DEVNULL)
141
+ try:await asyncio.wait_for(proc.wait(),20)
142
+ except BaseException:proc.kill();await proc.wait();raise
143
+ if proc.returncode:raise ValueError('文件不是可读取的视频')
144
+ async def start(self,channel,op,p):
145
+ if op not in ('upload_draft','image_dynamic'):raise ValueError('不支持的上传操作')
146
+ cooling=self.cooldown(channel) if op=='upload_draft' else {}
147
+ if cooling:raise ValueError('B站上传限流冷却中,请勿立即重试;可用 upload_health 查询建议重试时间')
148
+ self.cleanup()
149
+ if any(x[0]==channel and not x[1].done() for x in self.tasks.values()):raise ValueError('本渠道已有媒体任务,请等待完成')
150
+ jid=p.get('stageId')
151
+ d=self.directory(channel,jid)
152
+ cred=self.credential(channel)
153
+ _,old=self.load(channel,jid)
154
+ if old['status']!='staged':raise ValueError('素材已使用,请查询原任务')
155
+ if op=='image_dynamic' and (not 1<=old.get('imageCount',0)<=9 or len(p.get('text',''))>2000):raise ValueError('漫画图片暂存无效或文字过长')
156
+ if op=='upload_draft':
157
+ _,old=self.load(channel,jid)
158
+ if old['status']!='staged':raise ValueError('素材已使用,请查询原任务')
159
+ await self.probe(d/'video')
160
+ from PIL import Image
161
+ with Image.open(d/'cover') as im:
162
+ if im.format not in ('PNG','JPEG','WEBP') or im.width*im.height>25000000:raise ValueError('封面需为 PNG/JPEG/WebP 图片,最多 2500 万像素')
163
+ if not str(p.get('title','')).strip() or not p.get('tags') or not isinstance(p.get('tid'),int):raise ValueError('缺少标题、标签或分区')
164
+ if p.get('copyright')==2 and not p.get('source'):raise ValueError('转载投稿需要来源')
165
+ j={'id':jid,'operation':op,'status':'queued','createdAt':time.time(),'published':False}
166
+ j.update(uid=str(cred.dedeuserid))
167
+ if op=='upload_draft':j.update(title=p['title'])
168
+ else:j.update(imageCount=old['imageCount'])
169
+ self.save(d,j)
170
+ task=asyncio.create_task(self.run(channel,d,j,p,cred) if op=='upload_draft' else self.run_images(channel,d,j,p,cred));self.tasks[jid]=(channel,task)
171
+ task.add_done_callback(lambda _:self.tasks.pop(jid,None))
172
+ return self.public(j)
173
+ async def run(self,channel,d,j,p,cred):
174
+ try:
175
+ async with self.limit:
176
+ j['status']='running';j['phase']='上传素材';self.save(d,j)
177
+ if not await cred.check_valid():raise ValueError('上传前登录失效,请重新登录')
178
+ meta={'title':p['title'],'desc':p.get('description',''),'tid':p['tid'],'tag':','.join(p['tags']),'copyright':p.get('copyright',1),'source':p.get('source',''),'no_reprint':1,'desc_format_id':0}
179
+ uploader=UploadOnly([video_uploader.VideoUploaderPage(str(d/'video'),p['title'])],meta,cred,cover=LocalPicture(d/'cover'))
180
+ uploaded=await asyncio.wait_for(uploader.start(),1800)
181
+ if not uploaded or not uploaded.get('videos'):raise ValueError('素材上传未完成')
182
+ j.update(status='draft',meta=meta,uploaded=uploaded,message='素材已上传,待投稿草稿保存在伙伴服务中;尚未提交 B 站审核,不是创作中心云草稿。')
183
+ except asyncio.CancelledError:
184
+ j.update(status='interrupted',message='任务已中断,未自动重试或投稿');raise
185
+ except Exception as e:
186
+ self.record_failure(channel,j,e)
187
+ finally:
188
+ for name in ('video','cover'):(d/name).unlink(missing_ok=True)
189
+ self.save(d,j)
190
+ async def run_images(self,channel,d,j,p,cred):
191
+ submitting=False
192
+ try:
193
+ async with self.limit:
194
+ j.update(status='running',phase='上传漫画图片');self.save(d,j)
195
+ if not await cred.check_valid():raise ValueError('登录失效,请重新扫码')
196
+ pics=[]
197
+ for index in range(j['imageCount']):
198
+ picture=LocalPicture(d/f'image{index}')
199
+ uploaded=await asyncio.wait_for(dynamic.upload_image(picture,cred),90)
200
+ image_url=uploaded.get('image_url','')
201
+ if image_url.startswith('http://'):image_url='https://'+image_url[7:]
202
+ media_url(image_url)
203
+ pics.append({'img_src':image_url,'img_width':picture.width,'img_height':picture.height})
204
+ # Same request shape as bilibili-api send_dynamic; upload first so a
205
+ # failure before this boundary is known not to have published anything.
206
+ info=dynamic.BuildDynamic.empty().add_plain_text(p.get('text',''))
207
+ data={'dyn_req':{'content':{'contents':await info.get_contents(credential=cred)},'scene':2,'meta':{'app_meta':{'from':'create.dynamic.web','mobi_app':'web'}},'pics':pics,'attach_card':None}}
208
+ if str(self.credential(channel).dedeuserid)!=str(cred.dedeuserid):raise ValueError('登录账号已变化,图片动态未发布')
209
+ j.update(status='submitting',phase='发布图片动态');self.save(d,j);submitting=True
210
+ result=await asyncio.wait_for(Api(**dynamic.API['send']['instant'],credential=cred,json_body=True).update_data(**data).update_params(csrf=cred.bili_jct).result,35)
211
+ did=str(result.get('dyn_id_str') or result.get('dynamic_id_str') or result.get('dynamic_id') or '')
212
+ if not did.isdigit() or int(did)<=0:raise ValueError('动态返回缺少编号,请核实账号动态,不要重复发布')
213
+ j.update(status='submitted',published=True,dynamic_id=did,result={'dynamic_id':did,'url':'https://t.bilibili.com/'+did},message='图片动态已提交,实际可见状态以B站为准')
214
+ except BaseException as e:
215
+ if submitting:j.update(status='unknown',message='图片动态提交结果不明,请查看账号动态,禁止自动重发')
216
+ elif isinstance(e,asyncio.CancelledError):j.update(status='interrupted',message='图片上传已中断,尚未发布')
217
+ else:self.record_failure(channel,j,e)
218
+ if isinstance(e,asyncio.CancelledError):raise
219
+ finally:
220
+ for file in d.glob('image[0-8]'):file.unlink(missing_ok=True)
221
+ self.save(d,j)
222
+
223
+ async def publish(self,channel,p):
224
+ d,j=self.load(channel,p['draft_id']);cred=self.credential(channel)
225
+ if j.get('uid')!=str(cred.dedeuserid):raise ValueError('草稿不属于当前登录账号')
226
+ if j['status']!='draft':raise ValueError('草稿未就绪或已提交,请查询原任务,不重复投稿')
227
+ if time.time()-j['createdAt']>7*86400:raise ValueError('草稿已过期,请重新上传')
228
+ j['status']='submitting';self.save(d,j)
229
+ try:
230
+ meta={**j['meta'],**j['uploaded'],'csrf':cred.bili_jct}
231
+ r=await Api(url='https://member.bilibili.com/x/vu/web/add/v3',method='POST',verify=True,credential=cred,no_csrf=True,json_body=True).update_params(csrf=cred.bili_jct,t=time.time()*1000).update_data(**meta).result
232
+ if not r or not r.get('bvid'):raise ValueError('投稿返回缺少 BV 号,请在创作中心核实')
233
+ j.update(status='submitted',published=True,result={'bvid':r['bvid'],'aid':str(r.get('aid',''))},message='投稿已提交,实际公开状态以 B 站审核为准')
234
+ except BaseException:
235
+ j.update(status='unknown',message='投稿结果不明,请在创作中心核实,不自动重发');raise
236
+ finally:self.save(d,j)
237
+ return self.public(j)
238
+ async def cancel(self,channel):
239
+ tasks=[t for owner,t in self.tasks.values() if owner==channel]
240
+ for t in tasks:t.cancel()
241
+ await asyncio.gather(*tasks,return_exceptions=True)
242
+ async def close(self,app):
243
+ tasks=[t for _,t in self.tasks.values()]
244
+ for t in tasks:t.cancel()
245
+ await asyncio.gather(*tasks,return_exceptions=True)
@@ -0,0 +1,3 @@
1
+ bilibili-api-python==17.4.2
2
+ aiohttp==3.14.5
3
+ imageio-ffmpeg==0.6.0