openmd-cli 0.1.0__py3-none-any.whl
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.
- openmd/__init__.py +3 -0
- openmd/__main__.py +5 -0
- openmd/adapters/__init__.py +1 -0
- openmd/adapters/base.py +95 -0
- openmd/adapters/bilibili/__init__.py +11 -0
- openmd/adapters/bilibili/adapter.py +104 -0
- openmd/adapters/bilibili/discovery.py +33 -0
- openmd/adapters/douyin/__init__.py +11 -0
- openmd/adapters/douyin/adapter.py +117 -0
- openmd/adapters/douyin/discovery.py +36 -0
- openmd/adapters/douyin/sidecar.py +77 -0
- openmd/adapters/generic_media/__init__.py +14 -0
- openmd/adapters/high_privacy.py +77 -0
- openmd/adapters/local_audio/__init__.py +14 -0
- openmd/adapters/local_audio/adapter.py +169 -0
- openmd/adapters/local_audio/asr.py +58 -0
- openmd/adapters/local_audio/backends/__init__.py +1 -0
- openmd/adapters/local_audio/backends/auto.py +177 -0
- openmd/adapters/local_audio/backends/faster_whisper.py +285 -0
- openmd/adapters/local_audio/backends/funasr_sense_voice.py +14 -0
- openmd/adapters/local_audio/backends/qwen3_asr.py +254 -0
- openmd/adapters/opencli.py +770 -0
- openmd/adapters/pdf/__init__.py +11 -0
- openmd/adapters/pdf/adapter.py +153 -0
- openmd/adapters/pdf/backends/__init__.py +1 -0
- openmd/adapters/pdf/backends/markitdown.py +165 -0
- openmd/adapters/pdf/parser.py +56 -0
- openmd/adapters/qq_group/__init__.py +11 -0
- openmd/adapters/qq_group/adapter.py +289 -0
- openmd/adapters/qq_group/parsers/__init__.py +1 -0
- openmd/adapters/qq_group/parsers/html.py +60 -0
- openmd/adapters/qq_group/parsers/json.py +71 -0
- openmd/adapters/registry.py +149 -0
- openmd/adapters/screenshot/__init__.py +11 -0
- openmd/adapters/screenshot/adapter.py +154 -0
- openmd/adapters/screenshot/backends/__init__.py +1 -0
- openmd/adapters/screenshot/backends/paddle.py +15 -0
- openmd/adapters/screenshot/backends/rapidocr.py +176 -0
- openmd/adapters/screenshot/backends/tesseract.py +15 -0
- openmd/adapters/screenshot/ocr.py +84 -0
- openmd/adapters/wechat_group/__init__.py +11 -0
- openmd/adapters/wechat_group/adapter.py +160 -0
- openmd/adapters/wechat_group/wx_cli.py +268 -0
- openmd/adapters/wechat_moments/__init__.py +11 -0
- openmd/adapters/wechat_moments/adapter.py +167 -0
- openmd/adapters/wechat_moments/wx_cli.py +265 -0
- openmd/adapters/weixin/__init__.py +11 -0
- openmd/adapters/weixin/adapter.py +148 -0
- openmd/adapters/weixin/discovery.py +510 -0
- openmd/adapters/weixin/report.py +278 -0
- openmd/adapters/xiaohongshu.py +158 -0
- openmd/backends/__init__.py +1 -0
- openmd/backends/base.py +204 -0
- openmd/backends/bilibili.py +1480 -0
- openmd/backends/bilibili_wbi.py +277 -0
- openmd/backends/discourse.py +426 -0
- openmd/backends/douyin.py +868 -0
- openmd/backends/douyin_detail.py +124 -0
- openmd/backends/douyin_providers.py +602 -0
- openmd/backends/executor.py +546 -0
- openmd/backends/feishu.py +861 -0
- openmd/backends/generic_media.py +77 -0
- openmd/backends/interactive.py +60 -0
- openmd/backends/ocr.py +41 -0
- openmd/backends/opencli.py +82 -0
- openmd/backends/pdf.py +122 -0
- openmd/backends/registry.py +234 -0
- openmd/backends/screenshot.py +79 -0
- openmd/backends/video_metadata.py +167 -0
- openmd/backends/web_article.py +363 -0
- openmd/backends/wechat_group.py +35 -0
- openmd/backends/wechat_moments.py +35 -0
- openmd/backends/weixin.py +767 -0
- openmd/backends/x.py +455 -0
- openmd/backends/xiaoe.py +940 -0
- openmd/backends/xiaohongshu.py +911 -0
- openmd/backends/xiaohongshu_providers.py +876 -0
- openmd/backends/xiaoyuzhou.py +1017 -0
- openmd/backends/youtube.py +280 -0
- openmd/backends/youtube_discovery.py +1245 -0
- openmd/backends/youtube_transcript.py +213 -0
- openmd/browser_extension/background.js +231 -0
- openmd/browser_extension/icons/icon-128.png +0 -0
- openmd/browser_extension/icons/icon-16.png +0 -0
- openmd/browser_extension/icons/icon-32.png +0 -0
- openmd/browser_extension/icons/icon-48.png +0 -0
- openmd/browser_extension/icons/icon-512.png +0 -0
- openmd/browser_extension/idle.html +6 -0
- openmd/browser_extension/manifest.json +43 -0
- openmd/browser_extension/options.html +42 -0
- openmd/browser_extension/options.js +117 -0
- openmd/browser_extension/popup.css +71 -0
- openmd/browser_extension/popup.html +17 -0
- openmd/browser_extension/popup.js +59 -0
- openmd/browser_extension/vendor/opencli-bridge/LICENSE +190 -0
- openmd/browser_extension/vendor/opencli-bridge/VENDOR.md +31 -0
- openmd/browser_extension/vendor/opencli-bridge/background.js +2742 -0
- openmd/browser_host.py +503 -0
- openmd/browser_setup.py +106 -0
- openmd/cli.py +2012 -0
- openmd/console.py +32 -0
- openmd/core/__init__.py +1 -0
- openmd/core/capabilities.py +162 -0
- openmd/core/content_validation.py +323 -0
- openmd/core/coverage.py +163 -0
- openmd/core/detector.py +247 -0
- openmd/core/doctor.py +104 -0
- openmd/core/execution_policy.py +164 -0
- openmd/core/filename.py +64 -0
- openmd/core/input_validation.py +91 -0
- openmd/core/local_config.py +96 -0
- openmd/core/manifest.py +156 -0
- openmd/core/models.py +168 -0
- openmd/core/opencli_output.py +34 -0
- openmd/core/operation_contract.py +60 -0
- openmd/core/output.py +194 -0
- openmd/core/package_contract.py +335 -0
- openmd/core/package_validator.py +489 -0
- openmd/core/package_writer.py +666 -0
- openmd/core/planner.py +345 -0
- openmd/core/preferences.py +61 -0
- openmd/core/readable.py +129 -0
- openmd/core/receipt.py +285 -0
- openmd/core/renderer.py +185 -0
- openmd/core/runlog.py +208 -0
- openmd/core/secrets.py +313 -0
- openmd/doctor_view.py +68 -0
- openmd/interactive.py +453 -0
- openmd/ocr/__init__.py +1 -0
- openmd/ocr/breakers.py +84 -0
- openmd/ocr/config.py +54 -0
- openmd/ocr/documents.py +522 -0
- openmd/ocr/endpoint.py +72 -0
- openmd/ocr/layout.py +92 -0
- openmd/ocr/layout_filter.py +118 -0
- openmd/ocr/layout_worker.py +89 -0
- openmd/ocr/native_math.py +220 -0
- openmd/ocr/office.py +305 -0
- openmd/ocr/ollama.py +200 -0
- openmd/ocr/otsl.py +51 -0
- openmd/ocr/ovis-prompt.txt +1 -0
- openmd/ocr/pipeline.py +384 -0
- openmd/ocr/settings.json +25 -0
- openmd/ocr/tiling.py +631 -0
- openmd/post_video.py +154 -0
- openmd/probe.py +397 -0
- openmd/routing/__init__.py +1 -0
- openmd/routing/cards/builtin.json +620 -0
- openmd/routing/cards/defuddle.json +70 -0
- openmd/routing/cards/x-tweet-fetcher.json +71 -0
- openmd/routing/cards/xyz-dl.json +60 -0
- openmd/routing/cards/yutto.json +68 -0
- openmd/routing/entry_shim.py +71 -0
- openmd/routing/lifecycle.py +459 -0
- openmd/routing/loader.py +138 -0
- openmd/routing/providers.py +158 -0
- openmd/routing/readers.py +108 -0
- openmd/routing/routes.json +504 -0
- openmd/routing/runner.py +192 -0
- openmd/routing/view.py +40 -0
- openmd/runtime/__init__.py +1 -0
- openmd/runtime/_windows_job.py +234 -0
- openmd/runtime/asr.py +217 -0
- openmd/runtime/asr_worker.py +119 -0
- openmd/runtime/gpu_lock.py +81 -0
- openmd/runtime/gpu_quarantine.py +85 -0
- openmd/runtime/interpreters.py +407 -0
- openmd/runtime/managed_tools.py +204 -0
- openmd/runtime/models.py +202 -0
- openmd/runtime/services.py +175 -0
- openmd/runtime/tempdirs.py +227 -0
- openmd_cli-0.1.0.dist-info/METADATA +168 -0
- openmd_cli-0.1.0.dist-info/RECORD +177 -0
- openmd_cli-0.1.0.dist-info/WHEEL +5 -0
- openmd_cli-0.1.0.dist-info/entry_points.txt +2 -0
- openmd_cli-0.1.0.dist-info/licenses/LICENSE +21 -0
- openmd_cli-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,504 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schema": "openmd.routes/v1",
|
|
3
|
+
"version": "2026-11-10.1",
|
|
4
|
+
"routes": [
|
|
5
|
+
{
|
|
6
|
+
"source": "bilibili",
|
|
7
|
+
"capability": "transcript",
|
|
8
|
+
"chain": [
|
|
9
|
+
"opencli.bilibili.subtitle",
|
|
10
|
+
"bilibili-harvest.core",
|
|
11
|
+
"yt-dlp.bilibili",
|
|
12
|
+
"asr.fallback"
|
|
13
|
+
]
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
"source": "bilibili",
|
|
17
|
+
"capability": "media",
|
|
18
|
+
"chain": [
|
|
19
|
+
"yutto.bilibili.media-asr"
|
|
20
|
+
]
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
"source": "youtube",
|
|
24
|
+
"capability": "transcript",
|
|
25
|
+
"chain": [
|
|
26
|
+
"opencli.youtube.transcript",
|
|
27
|
+
"youtube-transcript-api",
|
|
28
|
+
"yt-dlp.youtube",
|
|
29
|
+
"asr.fallback"
|
|
30
|
+
]
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
"source": "weixin",
|
|
34
|
+
"capability": "content",
|
|
35
|
+
"chain": [
|
|
36
|
+
"opencli.weixin.download",
|
|
37
|
+
"defuddle.web.content",
|
|
38
|
+
"weixin.article.local",
|
|
39
|
+
"opencli.web.read"
|
|
40
|
+
]
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
"source": "wechat_group",
|
|
44
|
+
"capability": "content",
|
|
45
|
+
"chain": [
|
|
46
|
+
"opencli.wx.history"
|
|
47
|
+
]
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
"source": "wechat_moments",
|
|
51
|
+
"capability": "content",
|
|
52
|
+
"chain": [
|
|
53
|
+
"opencli.wx.sns-feed"
|
|
54
|
+
]
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
"source": "qq_group",
|
|
58
|
+
"capability": "content",
|
|
59
|
+
"chain": [
|
|
60
|
+
"qq_group.export"
|
|
61
|
+
]
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
"source": "xiaoyuzhou",
|
|
65
|
+
"capability": "transcript",
|
|
66
|
+
"chain": [
|
|
67
|
+
"opencli.xiaoyuzhou.transcript"
|
|
68
|
+
]
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
"source": "xiaoyuzhou",
|
|
72
|
+
"capability": "media",
|
|
73
|
+
"chain": [
|
|
74
|
+
"xyz-dl.xiaoyuzhou.media-asr"
|
|
75
|
+
]
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
"source": "douyin",
|
|
79
|
+
"capability": "content",
|
|
80
|
+
"chain": [
|
|
81
|
+
"douyin.transcript"
|
|
82
|
+
]
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
"source": "tiktok",
|
|
86
|
+
"capability": "content",
|
|
87
|
+
"chain": [
|
|
88
|
+
"opencli.tiktok.user"
|
|
89
|
+
]
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
"source": "xiaohongshu",
|
|
93
|
+
"capability": "content",
|
|
94
|
+
"chain": [
|
|
95
|
+
"xiaohongshu.composite"
|
|
96
|
+
]
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
"source": "xiaoe",
|
|
100
|
+
"capability": "transcript",
|
|
101
|
+
"chain": [
|
|
102
|
+
"opencli.xiaoe.transcript",
|
|
103
|
+
"opencli.xiaoe.content"
|
|
104
|
+
]
|
|
105
|
+
},
|
|
106
|
+
{
|
|
107
|
+
"source": "x",
|
|
108
|
+
"capability": "content",
|
|
109
|
+
"chain": [
|
|
110
|
+
"x-tweet-fetcher.x.content",
|
|
111
|
+
"opencli.twitter.thread"
|
|
112
|
+
]
|
|
113
|
+
},
|
|
114
|
+
{
|
|
115
|
+
"source": "web_article",
|
|
116
|
+
"capability": "content",
|
|
117
|
+
"chain": [
|
|
118
|
+
"web_article.discourse",
|
|
119
|
+
"defuddle.web.content",
|
|
120
|
+
"web_article.trafilatura",
|
|
121
|
+
"opencli.web.read"
|
|
122
|
+
]
|
|
123
|
+
},
|
|
124
|
+
{
|
|
125
|
+
"source": "feishu",
|
|
126
|
+
"capability": "content",
|
|
127
|
+
"chain": [
|
|
128
|
+
"lark-cli.docs.fetch",
|
|
129
|
+
"lark-cli.drive.export-markdown",
|
|
130
|
+
"lark-cli.markdown.fetch",
|
|
131
|
+
"manual.markdown-export"
|
|
132
|
+
]
|
|
133
|
+
},
|
|
134
|
+
{
|
|
135
|
+
"source": "generic_media",
|
|
136
|
+
"capability": "transcript",
|
|
137
|
+
"chain": [
|
|
138
|
+
"asr.fallback"
|
|
139
|
+
]
|
|
140
|
+
},
|
|
141
|
+
{
|
|
142
|
+
"source": "pdf",
|
|
143
|
+
"capability": "content",
|
|
144
|
+
"chain": [
|
|
145
|
+
"pdf.markitdown",
|
|
146
|
+
"pdf.textlayer",
|
|
147
|
+
"ocr.chain"
|
|
148
|
+
]
|
|
149
|
+
},
|
|
150
|
+
{
|
|
151
|
+
"source": "screenshot",
|
|
152
|
+
"capability": "content",
|
|
153
|
+
"chain": [
|
|
154
|
+
"ocr.chain",
|
|
155
|
+
"ocr.rapidocr",
|
|
156
|
+
"ocr.tesseract"
|
|
157
|
+
]
|
|
158
|
+
}
|
|
159
|
+
],
|
|
160
|
+
"capabilities": [
|
|
161
|
+
{
|
|
162
|
+
"source_id": "bilibili",
|
|
163
|
+
"tier": "A",
|
|
164
|
+
"state": "partial",
|
|
165
|
+
"input_kinds": [
|
|
166
|
+
"single",
|
|
167
|
+
"collection"
|
|
168
|
+
],
|
|
169
|
+
"source_exhausted_kinds": [
|
|
170
|
+
"collection"
|
|
171
|
+
],
|
|
172
|
+
"profiles": [
|
|
173
|
+
"media"
|
|
174
|
+
],
|
|
175
|
+
"route_source": "bilibili",
|
|
176
|
+
"runtime_binding": "route"
|
|
177
|
+
},
|
|
178
|
+
{
|
|
179
|
+
"source_id": "youtube",
|
|
180
|
+
"tier": "A",
|
|
181
|
+
"state": "partial",
|
|
182
|
+
"input_kinds": [
|
|
183
|
+
"single",
|
|
184
|
+
"collection"
|
|
185
|
+
],
|
|
186
|
+
"source_exhausted_kinds": [
|
|
187
|
+
"collection"
|
|
188
|
+
],
|
|
189
|
+
"profiles": [
|
|
190
|
+
"media"
|
|
191
|
+
],
|
|
192
|
+
"route_source": "youtube",
|
|
193
|
+
"runtime_binding": "route"
|
|
194
|
+
},
|
|
195
|
+
{
|
|
196
|
+
"source_id": "weixin",
|
|
197
|
+
"tier": "A",
|
|
198
|
+
"state": "partial",
|
|
199
|
+
"input_kinds": [
|
|
200
|
+
"single",
|
|
201
|
+
"collection"
|
|
202
|
+
],
|
|
203
|
+
"source_exhausted_kinds": [
|
|
204
|
+
"collection"
|
|
205
|
+
],
|
|
206
|
+
"profiles": [
|
|
207
|
+
"article"
|
|
208
|
+
],
|
|
209
|
+
"route_source": "weixin",
|
|
210
|
+
"runtime_binding": "route"
|
|
211
|
+
},
|
|
212
|
+
{
|
|
213
|
+
"source_id": "xiaoyuzhou",
|
|
214
|
+
"tier": "A",
|
|
215
|
+
"state": "partial",
|
|
216
|
+
"input_kinds": [
|
|
217
|
+
"single",
|
|
218
|
+
"collection"
|
|
219
|
+
],
|
|
220
|
+
"source_exhausted_kinds": [
|
|
221
|
+
"collection"
|
|
222
|
+
],
|
|
223
|
+
"profiles": [
|
|
224
|
+
"media"
|
|
225
|
+
],
|
|
226
|
+
"route_source": "xiaoyuzhou",
|
|
227
|
+
"runtime_binding": "route"
|
|
228
|
+
},
|
|
229
|
+
{
|
|
230
|
+
"source_id": "douyin",
|
|
231
|
+
"tier": "A",
|
|
232
|
+
"state": "partial",
|
|
233
|
+
"input_kinds": [
|
|
234
|
+
"single",
|
|
235
|
+
"collection"
|
|
236
|
+
],
|
|
237
|
+
"source_exhausted_kinds": [
|
|
238
|
+
"collection"
|
|
239
|
+
],
|
|
240
|
+
"profiles": [
|
|
241
|
+
"media"
|
|
242
|
+
],
|
|
243
|
+
"route_source": "douyin",
|
|
244
|
+
"runtime_binding": "route"
|
|
245
|
+
},
|
|
246
|
+
{
|
|
247
|
+
"source_id": "tiktok",
|
|
248
|
+
"tier": "C",
|
|
249
|
+
"state": "paused",
|
|
250
|
+
"input_kinds": [
|
|
251
|
+
"single"
|
|
252
|
+
],
|
|
253
|
+
"source_exhausted_kinds": [],
|
|
254
|
+
"profiles": [
|
|
255
|
+
"media"
|
|
256
|
+
],
|
|
257
|
+
"route_source": "tiktok",
|
|
258
|
+
"runtime_binding": null
|
|
259
|
+
},
|
|
260
|
+
{
|
|
261
|
+
"source_id": "xiaohongshu",
|
|
262
|
+
"tier": "A",
|
|
263
|
+
"state": "partial",
|
|
264
|
+
"input_kinds": [
|
|
265
|
+
"single",
|
|
266
|
+
"list"
|
|
267
|
+
],
|
|
268
|
+
"source_exhausted_kinds": [
|
|
269
|
+
"list"
|
|
270
|
+
],
|
|
271
|
+
"profiles": [
|
|
272
|
+
"article",
|
|
273
|
+
"media"
|
|
274
|
+
],
|
|
275
|
+
"route_source": "xiaohongshu",
|
|
276
|
+
"runtime_binding": "route"
|
|
277
|
+
},
|
|
278
|
+
{
|
|
279
|
+
"source_id": "xiaohongshu_creator",
|
|
280
|
+
"tier": "B",
|
|
281
|
+
"state": "blocked",
|
|
282
|
+
"input_kinds": [
|
|
283
|
+
"collection"
|
|
284
|
+
],
|
|
285
|
+
"source_exhausted_kinds": [],
|
|
286
|
+
"profiles": [
|
|
287
|
+
"article",
|
|
288
|
+
"media"
|
|
289
|
+
],
|
|
290
|
+
"route_source": "xiaohongshu",
|
|
291
|
+
"runtime_binding": null
|
|
292
|
+
},
|
|
293
|
+
{
|
|
294
|
+
"source_id": "xiaoe",
|
|
295
|
+
"tier": "A",
|
|
296
|
+
"state": "partial",
|
|
297
|
+
"input_kinds": [
|
|
298
|
+
"single",
|
|
299
|
+
"collection"
|
|
300
|
+
],
|
|
301
|
+
"source_exhausted_kinds": [
|
|
302
|
+
"collection"
|
|
303
|
+
],
|
|
304
|
+
"profiles": [
|
|
305
|
+
"media"
|
|
306
|
+
],
|
|
307
|
+
"route_source": "xiaoe",
|
|
308
|
+
"runtime_binding": "route"
|
|
309
|
+
},
|
|
310
|
+
{
|
|
311
|
+
"source_id": "x",
|
|
312
|
+
"tier": "B",
|
|
313
|
+
"state": "partial",
|
|
314
|
+
"input_kinds": [
|
|
315
|
+
"single",
|
|
316
|
+
"list"
|
|
317
|
+
],
|
|
318
|
+
"source_exhausted_kinds": [],
|
|
319
|
+
"profiles": [
|
|
320
|
+
"article"
|
|
321
|
+
],
|
|
322
|
+
"route_source": "x",
|
|
323
|
+
"runtime_binding": "route"
|
|
324
|
+
},
|
|
325
|
+
{
|
|
326
|
+
"source_id": "web_article",
|
|
327
|
+
"tier": "B",
|
|
328
|
+
"state": "ready",
|
|
329
|
+
"input_kinds": [
|
|
330
|
+
"single",
|
|
331
|
+
"list"
|
|
332
|
+
],
|
|
333
|
+
"source_exhausted_kinds": [],
|
|
334
|
+
"profiles": [
|
|
335
|
+
"article"
|
|
336
|
+
],
|
|
337
|
+
"route_source": "web_article",
|
|
338
|
+
"runtime_binding": "route"
|
|
339
|
+
},
|
|
340
|
+
{
|
|
341
|
+
"source_id": "feishu",
|
|
342
|
+
"tier": "B",
|
|
343
|
+
"state": "partial",
|
|
344
|
+
"input_kinds": [
|
|
345
|
+
"single",
|
|
346
|
+
"list"
|
|
347
|
+
],
|
|
348
|
+
"source_exhausted_kinds": [],
|
|
349
|
+
"profiles": [
|
|
350
|
+
"article",
|
|
351
|
+
"document"
|
|
352
|
+
],
|
|
353
|
+
"route_source": "feishu",
|
|
354
|
+
"runtime_binding": "route"
|
|
355
|
+
},
|
|
356
|
+
{
|
|
357
|
+
"source_id": "generic_media",
|
|
358
|
+
"tier": "B",
|
|
359
|
+
"state": "partial",
|
|
360
|
+
"input_kinds": [
|
|
361
|
+
"single",
|
|
362
|
+
"list"
|
|
363
|
+
],
|
|
364
|
+
"source_exhausted_kinds": [],
|
|
365
|
+
"profiles": [
|
|
366
|
+
"media"
|
|
367
|
+
],
|
|
368
|
+
"route_source": "generic_media",
|
|
369
|
+
"runtime_binding": null
|
|
370
|
+
},
|
|
371
|
+
{
|
|
372
|
+
"source_id": "local_audio",
|
|
373
|
+
"tier": "A",
|
|
374
|
+
"state": "ready",
|
|
375
|
+
"input_kinds": [
|
|
376
|
+
"file",
|
|
377
|
+
"folder"
|
|
378
|
+
],
|
|
379
|
+
"source_exhausted_kinds": [
|
|
380
|
+
"folder"
|
|
381
|
+
],
|
|
382
|
+
"profiles": [
|
|
383
|
+
"media"
|
|
384
|
+
],
|
|
385
|
+
"planned_provider": "asr.auto",
|
|
386
|
+
"route_source": "local_audio",
|
|
387
|
+
"runtime_binding": "asr.auto"
|
|
388
|
+
},
|
|
389
|
+
{
|
|
390
|
+
"source_id": "local_video",
|
|
391
|
+
"tier": "B",
|
|
392
|
+
"state": "partial",
|
|
393
|
+
"input_kinds": [
|
|
394
|
+
"file",
|
|
395
|
+
"folder"
|
|
396
|
+
],
|
|
397
|
+
"source_exhausted_kinds": [
|
|
398
|
+
"folder"
|
|
399
|
+
],
|
|
400
|
+
"profiles": [
|
|
401
|
+
"media"
|
|
402
|
+
],
|
|
403
|
+
"planned_provider": "asr.fallback",
|
|
404
|
+
"route_source": "local_video",
|
|
405
|
+
"runtime_binding": "asr.auto"
|
|
406
|
+
},
|
|
407
|
+
{
|
|
408
|
+
"source_id": "tutorial_video",
|
|
409
|
+
"tier": "C",
|
|
410
|
+
"state": "experimental",
|
|
411
|
+
"input_kinds": [
|
|
412
|
+
"file",
|
|
413
|
+
"single"
|
|
414
|
+
],
|
|
415
|
+
"source_exhausted_kinds": [],
|
|
416
|
+
"profiles": [
|
|
417
|
+
"media"
|
|
418
|
+
],
|
|
419
|
+
"planned_provider": "asr.fallback",
|
|
420
|
+
"route_source": "tutorial_video",
|
|
421
|
+
"runtime_binding": "asr.auto"
|
|
422
|
+
},
|
|
423
|
+
{
|
|
424
|
+
"source_id": "pdf_office",
|
|
425
|
+
"tier": "A",
|
|
426
|
+
"state": "partial",
|
|
427
|
+
"input_kinds": [
|
|
428
|
+
"file",
|
|
429
|
+
"folder"
|
|
430
|
+
],
|
|
431
|
+
"source_exhausted_kinds": [
|
|
432
|
+
"folder"
|
|
433
|
+
],
|
|
434
|
+
"profiles": [
|
|
435
|
+
"document"
|
|
436
|
+
],
|
|
437
|
+
"route_source": "pdf",
|
|
438
|
+
"runtime_binding": "route"
|
|
439
|
+
},
|
|
440
|
+
{
|
|
441
|
+
"source_id": "screenshot",
|
|
442
|
+
"tier": "B",
|
|
443
|
+
"state": "partial",
|
|
444
|
+
"input_kinds": [
|
|
445
|
+
"file",
|
|
446
|
+
"folder"
|
|
447
|
+
],
|
|
448
|
+
"source_exhausted_kinds": [
|
|
449
|
+
"folder"
|
|
450
|
+
],
|
|
451
|
+
"profiles": [
|
|
452
|
+
"document"
|
|
453
|
+
],
|
|
454
|
+
"route_source": "screenshot",
|
|
455
|
+
"runtime_binding": "route"
|
|
456
|
+
},
|
|
457
|
+
{
|
|
458
|
+
"source_id": "wechat_moments",
|
|
459
|
+
"tier": "B",
|
|
460
|
+
"state": "partial",
|
|
461
|
+
"input_kinds": [
|
|
462
|
+
"collection"
|
|
463
|
+
],
|
|
464
|
+
"source_exhausted_kinds": [
|
|
465
|
+
"collection"
|
|
466
|
+
],
|
|
467
|
+
"profiles": [
|
|
468
|
+
"conversation"
|
|
469
|
+
],
|
|
470
|
+
"route_source": "wechat_moments",
|
|
471
|
+
"runtime_binding": null
|
|
472
|
+
},
|
|
473
|
+
{
|
|
474
|
+
"source_id": "wechat_group",
|
|
475
|
+
"tier": "B",
|
|
476
|
+
"state": "partial",
|
|
477
|
+
"input_kinds": [
|
|
478
|
+
"collection"
|
|
479
|
+
],
|
|
480
|
+
"source_exhausted_kinds": [
|
|
481
|
+
"collection"
|
|
482
|
+
],
|
|
483
|
+
"profiles": [
|
|
484
|
+
"conversation"
|
|
485
|
+
],
|
|
486
|
+
"route_source": "wechat_group",
|
|
487
|
+
"runtime_binding": null
|
|
488
|
+
},
|
|
489
|
+
{
|
|
490
|
+
"source_id": "qq_group",
|
|
491
|
+
"tier": "B",
|
|
492
|
+
"state": "paused",
|
|
493
|
+
"input_kinds": [
|
|
494
|
+
"collection"
|
|
495
|
+
],
|
|
496
|
+
"source_exhausted_kinds": [],
|
|
497
|
+
"profiles": [
|
|
498
|
+
"conversation"
|
|
499
|
+
],
|
|
500
|
+
"route_source": "qq_group",
|
|
501
|
+
"runtime_binding": null
|
|
502
|
+
}
|
|
503
|
+
]
|
|
504
|
+
}
|
openmd/routing/runner.py
ADDED
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
"""Safe argv execution and local launcher resolution for tool cards."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import hashlib
|
|
6
|
+
import importlib.util
|
|
7
|
+
import os
|
|
8
|
+
import re
|
|
9
|
+
import shutil
|
|
10
|
+
import subprocess
|
|
11
|
+
import sys
|
|
12
|
+
import time
|
|
13
|
+
from dataclasses import dataclass
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
from typing import Any
|
|
16
|
+
from urllib.parse import urlparse
|
|
17
|
+
|
|
18
|
+
from openmd.core.local_config import load_local_config
|
|
19
|
+
from openmd.core.secrets import sanitize_error_summary
|
|
20
|
+
from openmd.runtime.interpreters import owned_popen, wait_interruptibly, terminate_quietly as _kill_tree
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class ToolError(ValueError):
|
|
24
|
+
def __init__(self, code: str, detail: str = "") -> None:
|
|
25
|
+
self.code = code
|
|
26
|
+
super().__init__(detail or code)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@dataclass(frozen=True)
|
|
30
|
+
class Launcher:
|
|
31
|
+
argv: tuple[str, ...]
|
|
32
|
+
origin: str
|
|
33
|
+
entry_path: Path
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@dataclass(frozen=True)
|
|
37
|
+
class ToolRun:
|
|
38
|
+
exit_code: int
|
|
39
|
+
seconds: float
|
|
40
|
+
stdout: str
|
|
41
|
+
stderr: str
|
|
42
|
+
files: dict[str, dict[str, Any]]
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _executable(value: str) -> str:
|
|
46
|
+
resolved = shutil.which(value) or (str(Path(value).expanduser().resolve()) if Path(value).expanduser().is_file() else None)
|
|
47
|
+
if not resolved:
|
|
48
|
+
raise ToolError("tool_not_installed", "Executable not found; run openmd doctor --profile tools.")
|
|
49
|
+
if Path(resolved).suffix.lower() in {".cmd", ".bat"}:
|
|
50
|
+
raise ToolError("tool_launcher_unsafe", "Batch launchers cannot safely carry URLs; configure a native executable or Node entry.")
|
|
51
|
+
return resolved
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def resolve_launcher(card: dict[str, Any]) -> Launcher:
|
|
55
|
+
tool_id = card["id"]
|
|
56
|
+
config = load_local_config().get("tools", {}).get(tool_id, {})
|
|
57
|
+
if not isinstance(config, dict) or set(config) - {"python", "paths", "root", "node", "script_root"}:
|
|
58
|
+
raise ToolError("tool_location_invalid")
|
|
59
|
+
config = dict(config)
|
|
60
|
+
prefix = "OPENMD_TOOL_" + re.sub(r"[^A-Z0-9]", "_", tool_id.upper())
|
|
61
|
+
origin = "local_config" if config else "which"
|
|
62
|
+
for key in ("python", "root", "node", "script_root"):
|
|
63
|
+
if os.getenv(prefix + "_" + key.upper()):
|
|
64
|
+
config[key] = os.environ[prefix + "_" + key.upper()]
|
|
65
|
+
origin = "env"
|
|
66
|
+
launcher = card.get("launcher", {})
|
|
67
|
+
if card["kind"] == "command" and launcher.get("node_entry"):
|
|
68
|
+
root = config.get("root")
|
|
69
|
+
if root:
|
|
70
|
+
entry = Path(root).expanduser() / launcher["node_entry"]
|
|
71
|
+
else:
|
|
72
|
+
which = next((shutil.which(x) for x in launcher.get("which", []) if shutil.which(x)), None)
|
|
73
|
+
if not which:
|
|
74
|
+
raise ToolError("tool_not_installed")
|
|
75
|
+
entry = Path(which).parent / "node_modules" / launcher["node_entry"]
|
|
76
|
+
if not entry.is_file():
|
|
77
|
+
raise ToolError("tool_not_installed", "Node entry not found; configure tool root and run openmd doctor --profile tools.")
|
|
78
|
+
node = _executable(config.get("node", "node"))
|
|
79
|
+
if launcher.get("node_api"):
|
|
80
|
+
import json
|
|
81
|
+
|
|
82
|
+
# Library-only options are declarative; invoke the unchanged CLI
|
|
83
|
+
# after configuring its shared exported parser. All values are argv.
|
|
84
|
+
api = launcher["node_api"]
|
|
85
|
+
api_path = ((Path(root) if root else entry.parents[2]) / api["module"]).resolve()
|
|
86
|
+
if not api_path.is_file():
|
|
87
|
+
raise ToolError("tool_not_installed")
|
|
88
|
+
code = "const [entry,apiPath,name,options,...args]=process.argv.slice(1);const api=require(apiPath),original=api[name],overrides=JSON.parse(options);api[name]=function(...a){a[a.length-1]={...a[a.length-1],...overrides};return original.apply(this,a)};require(entry).createProgram().parse([process.execPath,entry,...args]);"
|
|
89
|
+
return Launcher((node, "-e", code, str(entry.resolve()), str(api_path), api["export"], json.dumps(api["options"])), origin, entry.resolve())
|
|
90
|
+
return Launcher((node, str(entry.resolve())), origin, entry.resolve())
|
|
91
|
+
if card["kind"] in {"python_entry", "python_script"}:
|
|
92
|
+
if not config:
|
|
93
|
+
raise ToolError("tool_not_installed", "Configure the isolated Python interpreter and dependency paths; direct console scripts cannot enforce this card's TLS and credential boundary.")
|
|
94
|
+
python = _executable(config.get("python", sys.executable))
|
|
95
|
+
paths = config.get("paths", [])
|
|
96
|
+
if not isinstance(paths, list) or not all(isinstance(p, str) and Path(p).is_dir() for p in paths):
|
|
97
|
+
raise ToolError("tool_not_installed", "Isolated Python dependency directory is missing.")
|
|
98
|
+
args = [python, "-I", "-X", "utf8", str(Path(__file__).with_name("entry_shim.py").resolve())]
|
|
99
|
+
for path in paths:
|
|
100
|
+
args.extend(("--path", str(Path(path).resolve())))
|
|
101
|
+
if card["kind"] == "python_script":
|
|
102
|
+
root = config.get("script_root")
|
|
103
|
+
if not isinstance(root, str):
|
|
104
|
+
raise ToolError("tool_not_installed")
|
|
105
|
+
entry = Path(root).expanduser() / launcher["script"]
|
|
106
|
+
if not entry.is_file():
|
|
107
|
+
raise ToolError("tool_not_installed")
|
|
108
|
+
args.extend(("--script", str(entry.resolve())))
|
|
109
|
+
else:
|
|
110
|
+
module = launcher["entry"].split(":", 1)[0]
|
|
111
|
+
relative = Path(*module.split("."))
|
|
112
|
+
entries = [Path(p) / relative for p in paths]
|
|
113
|
+
entry = next((q.with_suffix(".py") for q in entries if q.with_suffix(".py").is_file()), None)
|
|
114
|
+
if entry is None:
|
|
115
|
+
entry = next((q / "__init__.py" for q in entries if (q / "__init__.py").is_file()), None)
|
|
116
|
+
if entry is None and not paths:
|
|
117
|
+
spec = importlib.util.find_spec(module.split(".")[0])
|
|
118
|
+
entry = Path(spec.origin) if spec and spec.origin else None
|
|
119
|
+
if entry is None:
|
|
120
|
+
raise ToolError("tool_not_installed", "Python entry not installed; run openmd doctor --profile tools.")
|
|
121
|
+
args.extend(("--entry", launcher["entry"]))
|
|
122
|
+
return Launcher(tuple([*args, "--"]), origin, entry.resolve())
|
|
123
|
+
raise ToolError("tool_launcher_invalid")
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def validate_url(url: str, capability: dict[str, Any]) -> None:
|
|
127
|
+
parsed = urlparse(url)
|
|
128
|
+
if parsed.scheme.lower() not in {"http", "https"} or not parsed.hostname or parsed.username or parsed.password or any(ord(c) < 32 for c in url):
|
|
129
|
+
raise ToolError("tool_input_invalid", "Tool requires an HTTP(S) source URL without embedded credentials.")
|
|
130
|
+
if capability.get("url_hosts") and parsed.hostname.lower() not in capability["url_hosts"]:
|
|
131
|
+
raise ToolError("tool_input_unsupported")
|
|
132
|
+
if capability.get("url_path_pattern") and not re.fullmatch(capability["url_path_pattern"], parsed.path):
|
|
133
|
+
raise ToolError("tool_input_unsupported", "This card supports individual public items only.")
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def build_argv(launcher: Launcher, capability: dict[str, Any], *, url: str, item_id: str | None, workdir: Path) -> list[str]:
|
|
137
|
+
validate_url(url, capability)
|
|
138
|
+
values = {"url": url, "id": item_id or url.rstrip("/").split("/")[-1], "workdir": str(workdir.resolve())}
|
|
139
|
+
arguments: list[str] = []
|
|
140
|
+
for template in capability["argv"]:
|
|
141
|
+
if any(name not in values for name in re.findall(r"\{([^{}]+)\}", template)):
|
|
142
|
+
raise ToolError("tool_placeholder_invalid")
|
|
143
|
+
argument = template.format_map(values)
|
|
144
|
+
arguments.append(argument)
|
|
145
|
+
return [*launcher.argv, *arguments]
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def isolated_environment(workdir: Path) -> dict[str, str]:
|
|
149
|
+
allowed = {"PATH", "SYSTEMROOT", "WINDIR", "TEMP", "TMP", "LANG", "LC_ALL", "SSL_CERT_FILE", "SSL_CERT_DIR", "REQUESTS_CA_BUNDLE", "CURL_CA_BUNDLE", "HTTP_PROXY", "HTTPS_PROXY", "ALL_PROXY", "NO_PROXY"}
|
|
150
|
+
env = {k: v for k, v in os.environ.items() if k.upper() in allowed}
|
|
151
|
+
home = workdir / ".home"
|
|
152
|
+
home.mkdir(exist_ok=True)
|
|
153
|
+
for key in ("HOME", "USERPROFILE", "APPDATA", "LOCALAPPDATA", "XDG_CONFIG_HOME"):
|
|
154
|
+
env[key] = str(home)
|
|
155
|
+
env["PYTHONIOENCODING"] = "utf-8"
|
|
156
|
+
env["PYTHONUTF8"] = "1"
|
|
157
|
+
return env
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def run_tool(card: dict[str, Any], capability: dict[str, Any], item, workdir: Path) -> ToolRun:
|
|
161
|
+
launcher = resolve_launcher(card)
|
|
162
|
+
argv = build_argv(launcher, capability, url=item.url, item_id=item.id, workdir=workdir)
|
|
163
|
+
stdout_path, stderr_path = workdir / ".stdout", workdir / ".stderr"
|
|
164
|
+
started = time.monotonic()
|
|
165
|
+
try:
|
|
166
|
+
with stdout_path.open("wb") as out, stderr_path.open("wb") as err:
|
|
167
|
+
process = owned_popen(argv, cwd=workdir, env=isolated_environment(workdir), stdin=subprocess.DEVNULL, stdout=out, stderr=err, shell=False, start_new_session=os.name != "nt", creationflags=subprocess.CREATE_NEW_PROCESS_GROUP if os.name == "nt" else 0)
|
|
168
|
+
try:
|
|
169
|
+
wait_interruptibly(process, capability["timeout_s"])
|
|
170
|
+
except subprocess.TimeoutExpired as exc:
|
|
171
|
+
_kill_tree(process)
|
|
172
|
+
raise ToolError("tool_timeout", "Command timed out; its owned process tree was terminated.") from exc
|
|
173
|
+
except BaseException:
|
|
174
|
+
_kill_tree(process)
|
|
175
|
+
raise
|
|
176
|
+
if stdout_path.stat().st_size > 16 * 1024 * 1024 or stderr_path.stat().st_size > 16 * 1024 * 1024:
|
|
177
|
+
raise ToolError("tool_output_too_large")
|
|
178
|
+
stdout = stdout_path.read_text(encoding="utf-8", errors="replace")
|
|
179
|
+
stderr = stderr_path.read_text(encoding="utf-8", errors="replace")
|
|
180
|
+
artifacts: dict[str, dict[str, Any]] = {}
|
|
181
|
+
for path in workdir.rglob("*"):
|
|
182
|
+
if path.is_file() and path.name not in {".stdout", ".stderr", ".openmd-owner.json"} and ".home" not in path.relative_to(workdir).parts:
|
|
183
|
+
if not path.resolve().is_relative_to(workdir.resolve()):
|
|
184
|
+
raise ToolError("tool_artifact_path_invalid")
|
|
185
|
+
digest = hashlib.sha256()
|
|
186
|
+
with path.open("rb") as stream:
|
|
187
|
+
for chunk in iter(lambda: stream.read(1024 * 1024), b""):
|
|
188
|
+
digest.update(chunk)
|
|
189
|
+
artifacts[path.relative_to(workdir).as_posix()] = {"bytes": path.stat().st_size, "sha256": digest.hexdigest()}
|
|
190
|
+
return ToolRun(process.returncode, round(time.monotonic() - started, 3), stdout, stderr, artifacts)
|
|
191
|
+
except OSError as exc:
|
|
192
|
+
raise ToolError("tool_launch_failed", sanitize_error_summary(exc) or "Tool launch failed.") from exc
|