media-analysis-mcp 2.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.
- package/LICENSE +21 -0
- package/README.md +191 -0
- package/dist/bailian.js +320 -0
- package/dist/bailian.js.map +1 -0
- package/dist/bytes.js +20 -0
- package/dist/bytes.js.map +1 -0
- package/dist/cli.js +56 -0
- package/dist/cli.js.map +1 -0
- package/dist/config-lookup.js +271 -0
- package/dist/config-lookup.js.map +1 -0
- package/dist/config.js +212 -0
- package/dist/config.js.map +1 -0
- package/dist/doctor.js +206 -0
- package/dist/doctor.js.map +1 -0
- package/dist/errors.js +387 -0
- package/dist/errors.js.map +1 -0
- package/dist/index.js +58 -0
- package/dist/index.js.map +1 -0
- package/dist/media.js +585 -0
- package/dist/media.js.map +1 -0
- package/dist/mpeg-audio.js +226 -0
- package/dist/mpeg-audio.js.map +1 -0
- package/dist/provider-error.js +73 -0
- package/dist/provider-error.js.map +1 -0
- package/dist/sanitize.js +103 -0
- package/dist/sanitize.js.map +1 -0
- package/dist/server.js +290 -0
- package/dist/server.js.map +1 -0
- package/dist/sse.js +327 -0
- package/dist/sse.js.map +1 -0
- package/dist/upload-cache.js +146 -0
- package/dist/upload-cache.js.map +1 -0
- package/dist/upload.js +368 -0
- package/dist/upload.js.map +1 -0
- package/dist/version.js +10 -0
- package/dist/version.js.map +1 -0
- package/package.json +73 -0
- package/scripts/prepare.mjs +43 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 sommio
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
# Media Analysis MCP
|
|
2
|
+
|
|
3
|
+
[](https://github.com/JaylanJerry/media-analysis-mcp/actions/workflows/ci.yml)
|
|
4
|
+
[](https://www.npmjs.com/package/media-analysis-mcp)
|
|
5
|
+
[](LICENSE)
|
|
6
|
+
[](https://nodejs.org)
|
|
7
|
+
|
|
8
|
+
> **Rename release candidate:** Media Analysis MCP uses the new package/CLI `media-analysis-mcp` and Host key `media_analysis_mcp`. Version 2.0.0 is prepared but not published yet. The previous `analyze-video-mcp@1.0.0` remains available; see [migration](#name-migration) and [release status](tasks/release-2.0.0.md).
|
|
9
|
+
|
|
10
|
+
MCP media gateway for local agents: submit an MP4/MOV video, MP3 audio, or public HTTPS video URL, and ask the media model a question. The Agent decides what to ask and how to present the answer.
|
|
11
|
+
|
|
12
|
+
本地 Agent 的媒体分析入口:上传视频或音频,由 Agent 向媒体模型提问。服务端不补写分析提纲、不强制证据 JSON、不自动发起纠错分析。
|
|
13
|
+
|
|
14
|
+
## Requirements
|
|
15
|
+
|
|
16
|
+
- **Node.js 24.x**. Other Node major versions are not formally supported.
|
|
17
|
+
- An Alibaba Cloud Bailian API key (`DASHSCOPE_API_KEY`). Requests can incur provider charges.
|
|
18
|
+
- An MCP client such as Cursor, Claude Code, Claude Desktop, Codex or VS Code.
|
|
19
|
+
|
|
20
|
+
## Getting started
|
|
21
|
+
|
|
22
|
+
1. Create a key in the [Bailian console](https://bailian.console.aliyun.com/).
|
|
23
|
+
2. Add the config below to your MCP client. Set `MEDIA_ALLOWED_ROOTS` to an existing folder containing media you permit the Agent to upload.
|
|
24
|
+
3. Restart or reload MCP servers. In Codex, start a new chat after changing the server configuration.
|
|
25
|
+
4. Explicitly ask for MCP analysis of a small MP4, MOV or MP3 inside the allowed folder, and state what you want to know.
|
|
26
|
+
|
|
27
|
+
Do not commit a config containing a real key. Local media is uploaded in full to Bailian's Beijing temporary storage on a cache miss (objects expire after about 48 hours).
|
|
28
|
+
|
|
29
|
+
### Standard config
|
|
30
|
+
|
|
31
|
+
```json
|
|
32
|
+
{
|
|
33
|
+
"mcpServers": {
|
|
34
|
+
"media_analysis_mcp": {
|
|
35
|
+
"command": "npx",
|
|
36
|
+
"args": ["-y", "--prefer-offline", "media-analysis-mcp@2.0.0"],
|
|
37
|
+
"env": {
|
|
38
|
+
"DASHSCOPE_API_KEY": "YOUR_DASHSCOPE_API_KEY",
|
|
39
|
+
"QWEN_MODEL": "qwen3.8-omni-flash",
|
|
40
|
+
"MEDIA_ALLOWED_ROOTS": "C:\\Users\\用户名\\Videos"
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Names are separate: product/MCP server name `Media Analysis MCP`; package/CLI name `media-analysis-mcp`; example Host config key `media_analysis_mcp`; the only Tool is `analyze_media`. The Host key may be renamed.
|
|
48
|
+
|
|
49
|
+
Templates: [`Cursor`](examples/mcp.cursor.json), [`Claude Code/Desktop`](examples/mcp.claude-code.json), [`Codex`](examples/mcp.codex.toml).
|
|
50
|
+
|
|
51
|
+
### Cursor and Claude Desktop
|
|
52
|
+
|
|
53
|
+
Use the standard config in Cursor's `~/.cursor/mcp.json`, or Claude Desktop's `claude_desktop_config.json`.
|
|
54
|
+
|
|
55
|
+
On Windows, if the host cannot find `npx`, use `"command": "cmd"` and `"args": ["/c", "npx", "-y", "--prefer-offline", "media-analysis-mcp@2.0.0"]`.
|
|
56
|
+
|
|
57
|
+
[](https://cursor.com/en/install-mcp?name=media_analysis_mcp&config=eyJjb21tYW5kIjoibnB4IiwiYXJncyI6WyIteSIsIi0tcHJlZmVyLW9mZmxpbmUiLCJtZWRpYS1hbmFseXNpcy1tY3BAMi4wLjAiXSwiZW52Ijp7IkRBU0hTQ09QRV9BUElfS0VZIjoiWU9VUl9EQVNIU0NPUEVfQVBJX0tFWSIsIlFXRU5fTU9ERUwiOiJxd2VuMy44LW9tbmktZmxhc2giLCJNRURJQV9BTExPV0VEX1JPT1RTIjoiQzpcXFVzZXJzXFznlKjmiLflkI1cXFZpZGVvcyJ9fQ==)
|
|
58
|
+
|
|
59
|
+
### Claude Code
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
claude mcp add --env DASHSCOPE_API_KEY=YOUR_DASHSCOPE_API_KEY --env QWEN_MODEL=qwen3.8-omni-flash --env MEDIA_ALLOWED_ROOTS="C:\Users\用户名\Videos" --transport stdio media_analysis_mcp -- npx -y --prefer-offline media-analysis-mcp@2.0.0
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
On native Windows, add `cmd /c` before `npx` if required by the host.
|
|
66
|
+
|
|
67
|
+
### Codex
|
|
68
|
+
|
|
69
|
+
Use [`examples/mcp.codex.toml`](examples/mcp.codex.toml). Set `startup_timeout_sec = 120` and `tool_timeout_sec = 1200`; put the Key and allowed roots in the MCP `env` table because Codex may not inherit user/system environment variables. Start a new chat after installation.
|
|
70
|
+
|
|
71
|
+
```powershell
|
|
72
|
+
codex mcp list
|
|
73
|
+
codex mcp get media_analysis_mcp
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
### VS Code
|
|
77
|
+
|
|
78
|
+
Use the standard server entry under `servers` instead of `mcpServers` in `.vscode/mcp.json` or the user MCP configuration.
|
|
79
|
+
|
|
80
|
+
## Tool
|
|
81
|
+
|
|
82
|
+
```text
|
|
83
|
+
analyze_media(media, prompt)
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
| Field | Required | Description |
|
|
87
|
+
| -------- | -------- | --------------------------------------------------------------------------------------- |
|
|
88
|
+
| `media` | yes | Absolute local MP4/MOV/MP3 path, or public HTTPS video URL without embedded credentials |
|
|
89
|
+
| `prompt` | yes | The Agent's question; 1–8000 characters after trimming surrounding whitespace |
|
|
90
|
+
|
|
91
|
+
The prompt's internal whitespace and wording are retained. The server adds only a fixed protocol note requesting text and honest uncertainty. It does not prescribe a timeline, shot list, critique or output structure. Empty and oversized questions are rejected.
|
|
92
|
+
|
|
93
|
+
```text
|
|
94
|
+
请用 MCP 分析 C:\Videos\clip.mov,描述画面和内嵌声音;无法确认时明确说明。
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
Results contain one text answer plus `structuredContent.answer`, `media`, `request`, optional `usage`, and `limitations`. Media facts are populated only when established locally. A matching answer does not prove the model's internal modality path or the accuracy of its timestamps.
|
|
98
|
+
|
|
99
|
+
The Agent should read only one copy of the answer. For long results, save the tool result and read it in chunks if the host display truncates it; do not call paid analysis again merely because display output was truncated.
|
|
100
|
+
|
|
101
|
+
There is no evidence correction request. Before any answer text arrives, transient failures may receive one bounded retry (configurable). Known content-inspection rejections are not automatically retried. Only one analysis is active per process.
|
|
102
|
+
|
|
103
|
+
Internal upload URLs, credential-shaped tokens and local absolute paths are redacted. The exact local input path is hidden in the answer, while public HTTP(S) links are preserved. Generic redaction of other paths has declared limits; see [`SECURITY.md`](docs/SECURITY.md).
|
|
104
|
+
|
|
105
|
+
## Environment
|
|
106
|
+
|
|
107
|
+
| Variable | Purpose |
|
|
108
|
+
| ---------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
109
|
+
| `DASHSCOPE_API_KEY` | Required Bailian Key. Resolved from explicit config, MCP/process env, user config, then supported Windows user env lookup. Never printed. |
|
|
110
|
+
| `MEDIA_ALLOWED_ROOTS` | Existing absolute allowed folders; separate with the platform delimiter (`;` on Windows). Unset means all local files are refused. |
|
|
111
|
+
| `MEDIA_ALLOW_ANY_LOCAL_FILE` | Default `off`. `on` permits any absolute local media path the Agent names, including files outside allowed roots. Enable only if you accept mistaken or prompt-induced uploads. |
|
|
112
|
+
| `MEDIA_MAX_LOCAL_MEDIA_MB` | Local size limit; default and maximum 1024 MiB. |
|
|
113
|
+
| `QWEN_MODEL` | Model id; default `qwen3.8-omni-flash`. Video/audio support varies by model. `qwen3.5-omni-plus` remains selectable. Not a Tool field. |
|
|
114
|
+
| `QWEN_CONFIG_FILE` | Optional config file path (same format as `--config`); the server does not implicitly load the cwd `.env`. |
|
|
115
|
+
| `QWEN_MCP_SERVER_NAME` | MCP server name; does not change Tool name. |
|
|
116
|
+
| `QWEN_UPLOAD_CACHE` | Default on; `off` disables temporary-upload reuse. |
|
|
117
|
+
| `DASHSCOPE_BASE_URL` | Default Beijing OpenAI-compatible endpoint. Key region must match. |
|
|
118
|
+
| `DASHSCOPE_UPLOAD_URL` | Default Beijing temporary upload endpoint. |
|
|
119
|
+
|
|
120
|
+
Use `media-analysis-mcp --doctor --json` to inspect configuration, variable sources and obsolete names without printing the Key. A missing Key permits MCP initialization but analysis returns `CONFIG_MISSING`.
|
|
121
|
+
|
|
122
|
+
## Limits
|
|
123
|
+
|
|
124
|
+
- Local inputs: **MP4/MOV** (ISO BMFF) with H.264/H.265 video and AAC audio; **MP3** with real MPEG Layer III frames. Renaming an MP4 to `.mp3` does not convert it and is refused.
|
|
125
|
+
- An audio-only MP4 has been refused by the provider through the video path. Export genuine MP3 for standalone audio. The server never transcodes, extracts frames, decodes audio or installs FFmpeg.
|
|
126
|
+
- Known local durations greater than **3600 seconds** are refused; exactly 3600 is allowed. Unknown duration is allowed. MP3 duration is reported only from declared Xing/Info/VBRI frame counts, with no bitrate estimate.
|
|
127
|
+
- HTTPS inputs are fetched by the provider; the server does not download or probe their content/duration. Remote `.mp3` URLs are refused.
|
|
128
|
+
- Media understanding is sampled, not frame-accurate or a verified transcript. Use a 5–30 second excerpt for precise timing questions; shot counts, dialogue and timestamps still require independent checking.
|
|
129
|
+
- Cache entries include file identity, a size/head/tail fingerprint, model, upload endpoint and credential fingerprint, and expire after about 47 hours. Changing only the middle while preserving identity/head/tail can reuse stale media; this is an accepted limitation. Disable cache if that matters.
|
|
130
|
+
- Images, downloads from video sites, automatic conversion, batch analysis, cross-call conversation memory and deterministic loudness/edit checks are outside scope.
|
|
131
|
+
|
|
132
|
+
## Migrating from 0.6.1
|
|
133
|
+
|
|
134
|
+
This is a breaking release. Existing configs do not silently gain MP3 access.
|
|
135
|
+
|
|
136
|
+
| Published 0.6.1 | 1.0.0 |
|
|
137
|
+
| ------------------------------------ | ------------------------------------------------ |
|
|
138
|
+
| `analyze_video(video, question?)` | `analyze_media(media, prompt)`; prompt required |
|
|
139
|
+
| Optional question and server outline | Agent supplies the question; no business outline |
|
|
140
|
+
| Evidence JSON and correction request | Plain answer with media facts and limitations |
|
|
141
|
+
| Local MP4/MOV | Local MP4/MOV/MP3 |
|
|
142
|
+
| `QWEN_ALLOWED_ROOTS` | `MEDIA_ALLOWED_ROOTS` |
|
|
143
|
+
| `QWEN_ALLOW_ANY_LOCAL_VIDEO` | `MEDIA_ALLOW_ANY_LOCAL_FILE` (off by default) |
|
|
144
|
+
| `QWEN_MAX_LOCAL_VIDEO_MB` | `MEDIA_MAX_LOCAL_MEDIA_MB` |
|
|
145
|
+
| `QWEN_AUDIO_SILENCE_CHECK` | Removed |
|
|
146
|
+
| Node `>=22` declaration | Formal support only for Node 24.x |
|
|
147
|
+
|
|
148
|
+
Old authorization variables grant nothing. Reload the MCP server after changing the version and variables. Full contract: [`API_CONTRACT.md`](docs/API_CONTRACT.md).
|
|
149
|
+
|
|
150
|
+
## Development and release
|
|
151
|
+
|
|
152
|
+
```powershell
|
|
153
|
+
npm install
|
|
154
|
+
npm run typecheck
|
|
155
|
+
npm run lint
|
|
156
|
+
npm run format:check
|
|
157
|
+
npm test
|
|
158
|
+
npm run coverage
|
|
159
|
+
npm run build
|
|
160
|
+
npm run test:pack-install
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
Default tests are mocked and free. Live tests require explicit authorization and a real Key; they are not part of the default suite.
|
|
164
|
+
|
|
165
|
+
After remote Node 24 CI passes, pushing an authorized version tag matching `package.json` and `src/version.ts` runs [`release.yml`](.github/workflows/release.yml): package/install probes, npm Trusted Publishing (OIDC), then GitHub Release. No `NPM_TOKEN` is used. See [ADR 0014](docs/decisions/0014-npm-trusted-publishing.md).
|
|
166
|
+
|
|
167
|
+
GitHub fallback:
|
|
168
|
+
|
|
169
|
+
```text
|
|
170
|
+
npx -y --allow-git=all github:JaylanJerry/media-analysis-mcp#v2.0.0
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
npm 12 requires `--allow-git=all`. Use the `v2.0.0` fallback only after its release is confirmed.
|
|
174
|
+
|
|
175
|
+
## Name migration
|
|
176
|
+
|
|
177
|
+
The product and MCP server are **Media Analysis MCP**. Version 2.0.0 renames the npm package and CLI to `media-analysis-mcp`, the example Host key to `media_analysis_mcp`, and the repository to `JaylanJerry/media-analysis-mcp`. `analyze_media(media, prompt)` and the media protocol remain unchanged.
|
|
178
|
+
|
|
179
|
+
Existing `analyze-video-mcp@1.0.0` installations are not automatically upgraded to a different package. Replace the command/package and Host key using the examples above, remove the duplicate old Host entry, and restart the Host. This repository does not edit your installed Host configuration or global packages automatically.
|
|
180
|
+
|
|
181
|
+
The default user config path becomes `~/.media-analysis-mcp/config.env`; the default upload cache directory becomes `media-analysis-mcp`. Move your own config file or use `--config` explicitly. Old implicit paths are not read, and the old upload cache is not automatically migrated; the first analysis after migration may upload again and incur normal provider charges. `DASHSCOPE_*`, `QWEN_MODEL`, `QWEN_UPLOAD_CACHE`, and `MEDIA_*` environment variables keep their existing meanings.
|
|
182
|
+
|
|
183
|
+
Historical tags, published versions, ADRs, and archived reports keep their original names. See [ADR 0026](docs/decisions/0026-media-analysis-mcp-rename.md).
|
|
184
|
+
|
|
185
|
+
## Repository maintenance
|
|
186
|
+
|
|
187
|
+
Start with [DEVELOPMENT_HANDOFF.md](DEVELOPMENT_HANDOFF.md), the [current documentation index](docs/README.md) and [maintenance status](tasks/README.md). Historical specifications and completed work are indexed under [docs/archive](docs/archive/README.md) and [tasks/archive](tasks/archive/README.md). Archived plans describe their original dates and are not current implementation instructions.
|
|
188
|
+
|
|
189
|
+
## License
|
|
190
|
+
|
|
191
|
+
MIT. Specialized fork of [`sommio/qwen-omni-mcp`](https://github.com/sommio/qwen-omni-mcp).
|
package/dist/bailian.js
ADDED
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
import { MediaError } from "./errors.js";
|
|
2
|
+
import { mapProviderError, safeProviderRequestId } from "./provider-error.js";
|
|
3
|
+
import { SseParser } from "./sse.js";
|
|
4
|
+
/**
|
|
5
|
+
* The only server-side wording added to a request. It is protocol-level — ask for
|
|
6
|
+
* text, and do not fabricate — and deliberately carries no analysis outline: the
|
|
7
|
+
* Agent owns the question, and the server must not expand or replace it.
|
|
8
|
+
*/
|
|
9
|
+
export const PROTOCOL_NOTE = "只输出文本回答。只写你实际看到或听到的内容;看不清、听不清或无法判断的地方请直接说明,不要编造或补全没有观察到的内容。";
|
|
10
|
+
export function contentBlock(media) {
|
|
11
|
+
if (media.format === "mp3") {
|
|
12
|
+
return { type: "input_audio", input_audio: { data: media.url, format: "mp3" } };
|
|
13
|
+
}
|
|
14
|
+
return { type: "video_url", video_url: { url: media.url } };
|
|
15
|
+
}
|
|
16
|
+
const MAX_RETRY_AFTER_MS = 30_000;
|
|
17
|
+
function chatCompletionsUrl(cfg) {
|
|
18
|
+
const base = cfg.baseUrl.replace(/\/+$/, "");
|
|
19
|
+
return `${base}/chat/completions`;
|
|
20
|
+
}
|
|
21
|
+
export function buildMediaPayload(cfg, media, request) {
|
|
22
|
+
return {
|
|
23
|
+
model: cfg.model,
|
|
24
|
+
messages: [
|
|
25
|
+
{
|
|
26
|
+
role: "system",
|
|
27
|
+
content: [{ type: "text", text: PROTOCOL_NOTE }],
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
role: "user",
|
|
31
|
+
content: [contentBlock(media), { type: "text", text: request.prompt }],
|
|
32
|
+
},
|
|
33
|
+
],
|
|
34
|
+
modalities: ["text"],
|
|
35
|
+
stream: true,
|
|
36
|
+
stream_options: { include_usage: true },
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
function analysisHeaders(cfg, media) {
|
|
40
|
+
const headers = {
|
|
41
|
+
"Content-Type": "application/json",
|
|
42
|
+
Authorization: `Bearer ${cfg.apiKey}`,
|
|
43
|
+
Accept: "text/event-stream",
|
|
44
|
+
};
|
|
45
|
+
if (media.requiresOssResolve || media.url.startsWith("oss://")) {
|
|
46
|
+
headers["X-DashScope-OssResourceResolve"] = "enable";
|
|
47
|
+
}
|
|
48
|
+
return headers;
|
|
49
|
+
}
|
|
50
|
+
function errorForStatus(status, requestId) {
|
|
51
|
+
if (status === 401 || status === 403) {
|
|
52
|
+
return new MediaError({
|
|
53
|
+
code: "PROVIDER_UNAUTHORIZED",
|
|
54
|
+
stage: "analyzing",
|
|
55
|
+
httpStatus: status,
|
|
56
|
+
...(requestId === undefined ? {} : { requestId }),
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
if (status === 429) {
|
|
60
|
+
return new MediaError({
|
|
61
|
+
code: "PROVIDER_RATE_LIMITED",
|
|
62
|
+
stage: "analyzing",
|
|
63
|
+
httpStatus: status,
|
|
64
|
+
...(requestId === undefined ? {} : { requestId }),
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
if (status === 502 || status === 503) {
|
|
68
|
+
return new MediaError({
|
|
69
|
+
code: "PROVIDER_UNAVAILABLE",
|
|
70
|
+
stage: "analyzing",
|
|
71
|
+
httpStatus: status,
|
|
72
|
+
...(requestId === undefined ? {} : { requestId }),
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
return new MediaError({
|
|
76
|
+
code: "MEDIA_ANALYSIS_FAILED",
|
|
77
|
+
stage: "analyzing",
|
|
78
|
+
httpStatus: status,
|
|
79
|
+
retryable: false,
|
|
80
|
+
...(requestId === undefined ? {} : { requestId }),
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
/** Error bodies can be arbitrary; read only enough to identify a provider error. */
|
|
84
|
+
async function readErrorBody(body) {
|
|
85
|
+
if (body === null)
|
|
86
|
+
return undefined;
|
|
87
|
+
const reader = body.getReader();
|
|
88
|
+
const chunks = [];
|
|
89
|
+
let bytes = 0;
|
|
90
|
+
try {
|
|
91
|
+
for (;;) {
|
|
92
|
+
const part = await reader.read();
|
|
93
|
+
if (part.done)
|
|
94
|
+
break;
|
|
95
|
+
bytes += part.value.byteLength;
|
|
96
|
+
if (bytes > 64 * 1024) {
|
|
97
|
+
await reader.cancel().catch(() => undefined);
|
|
98
|
+
return undefined;
|
|
99
|
+
}
|
|
100
|
+
chunks.push(Buffer.from(part.value));
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
catch {
|
|
104
|
+
return undefined;
|
|
105
|
+
}
|
|
106
|
+
finally {
|
|
107
|
+
reader.releaseLock();
|
|
108
|
+
}
|
|
109
|
+
try {
|
|
110
|
+
return JSON.parse(Buffer.concat(chunks).toString("utf8"));
|
|
111
|
+
}
|
|
112
|
+
catch {
|
|
113
|
+
return undefined;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
function canAutoRetry(err, sawText, retriesLeft) {
|
|
117
|
+
if (sawText || retriesLeft <= 0) {
|
|
118
|
+
return false;
|
|
119
|
+
}
|
|
120
|
+
return err.code === "PROVIDER_RATE_LIMITED" || err.code === "PROVIDER_UNAVAILABLE";
|
|
121
|
+
}
|
|
122
|
+
async function sleep(ms, signal) {
|
|
123
|
+
if (ms <= 0) {
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
await new Promise((resolve, reject) => {
|
|
127
|
+
const timer = setTimeout(() => {
|
|
128
|
+
signal.removeEventListener("abort", onAbort);
|
|
129
|
+
resolve();
|
|
130
|
+
}, ms);
|
|
131
|
+
const onAbort = () => {
|
|
132
|
+
clearTimeout(timer);
|
|
133
|
+
reject(new MediaError({ code: "PROVIDER_TIMEOUT", stage: "analyzing" }));
|
|
134
|
+
};
|
|
135
|
+
if (signal.aborted) {
|
|
136
|
+
onAbort();
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
function retryWaitMs(res) {
|
|
143
|
+
if (res === undefined) {
|
|
144
|
+
return 250 + Math.floor(Math.random() * 250);
|
|
145
|
+
}
|
|
146
|
+
const raw = res.headers.get("Retry-After");
|
|
147
|
+
if (raw === null || raw.trim() === "") {
|
|
148
|
+
return 250 + Math.floor(Math.random() * 250);
|
|
149
|
+
}
|
|
150
|
+
const seconds = Number(raw);
|
|
151
|
+
if (!Number.isFinite(seconds) || seconds < 0) {
|
|
152
|
+
return 250 + Math.floor(Math.random() * 250);
|
|
153
|
+
}
|
|
154
|
+
return Math.min(seconds * 1000, MAX_RETRY_AFTER_MS);
|
|
155
|
+
}
|
|
156
|
+
async function* bodyChunks(body) {
|
|
157
|
+
const reader = body.getReader();
|
|
158
|
+
try {
|
|
159
|
+
for (;;) {
|
|
160
|
+
const read = await reader.read();
|
|
161
|
+
if (read.done) {
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
yield read.value;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
finally {
|
|
168
|
+
reader.releaseLock();
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
async function analyzeMediaOnce(cfg, media, request, signal) {
|
|
172
|
+
let res;
|
|
173
|
+
try {
|
|
174
|
+
res = await fetch(chatCompletionsUrl(cfg), {
|
|
175
|
+
method: "POST",
|
|
176
|
+
headers: analysisHeaders(cfg, media),
|
|
177
|
+
body: JSON.stringify(buildMediaPayload(cfg, media, request)),
|
|
178
|
+
signal,
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
catch (err) {
|
|
182
|
+
if (err instanceof Error && err.name === "AbortError") {
|
|
183
|
+
return {
|
|
184
|
+
ok: false,
|
|
185
|
+
error: new MediaError({ code: "PROVIDER_TIMEOUT", stage: "analyzing" }),
|
|
186
|
+
retryAfterMs: 0,
|
|
187
|
+
sawText: false,
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
return {
|
|
191
|
+
ok: false,
|
|
192
|
+
error: new MediaError({ code: "PROVIDER_UNAVAILABLE", stage: "analyzing" }),
|
|
193
|
+
retryAfterMs: retryWaitMs(undefined),
|
|
194
|
+
sawText: false,
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
if (!res.ok) {
|
|
198
|
+
const wait = retryWaitMs(res);
|
|
199
|
+
const requestId = safeProviderRequestId(res.headers.get("x-request-id")) ??
|
|
200
|
+
safeProviderRequestId(res.headers.get("x-dashscope-request-id"));
|
|
201
|
+
const providerError = mapProviderError(await readErrorBody(res.body), {
|
|
202
|
+
...(requestId === undefined ? {} : { requestId }),
|
|
203
|
+
httpStatus: res.status,
|
|
204
|
+
});
|
|
205
|
+
const error = providerError?.code === "PROVIDER_CONTENT_REJECTED" ||
|
|
206
|
+
providerError?.code === "MEDIA_MODEL_UNSUPPORTED"
|
|
207
|
+
? providerError
|
|
208
|
+
: errorForStatus(res.status, requestId);
|
|
209
|
+
return { ok: false, error, retryAfterMs: wait, sawText: false };
|
|
210
|
+
}
|
|
211
|
+
if (res.body === null) {
|
|
212
|
+
return {
|
|
213
|
+
ok: false,
|
|
214
|
+
error: new MediaError({ code: "PROVIDER_RESPONSE_INVALID", stage: "analyzing" }),
|
|
215
|
+
retryAfterMs: 0,
|
|
216
|
+
sawText: false,
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
const requestId = safeProviderRequestId(res.headers.get("x-request-id")) ??
|
|
220
|
+
safeProviderRequestId(res.headers.get("x-dashscope-request-id"));
|
|
221
|
+
const parser = new SseParser(requestId);
|
|
222
|
+
try {
|
|
223
|
+
for await (const chunk of bodyChunks(res.body)) {
|
|
224
|
+
parser.push(chunk);
|
|
225
|
+
}
|
|
226
|
+
const aggregated = parser.finish();
|
|
227
|
+
if (aggregated.finishReason === "length") {
|
|
228
|
+
throw new MediaError({
|
|
229
|
+
code: "PROVIDER_RESPONSE_INVALID",
|
|
230
|
+
stage: "analyzing",
|
|
231
|
+
...(aggregated.requestId === undefined ? {} : { requestId: aggregated.requestId }),
|
|
232
|
+
diagnostic: {
|
|
233
|
+
parse_reason: "truncated",
|
|
234
|
+
received_sse_events: aggregated.receivedEvents,
|
|
235
|
+
...(aggregated.usage?.prompt_tokens === undefined
|
|
236
|
+
? {}
|
|
237
|
+
: { prompt_tokens: aggregated.usage.prompt_tokens }),
|
|
238
|
+
...(aggregated.usage?.completion_tokens === undefined
|
|
239
|
+
? {}
|
|
240
|
+
: { completion_tokens: aggregated.usage.completion_tokens }),
|
|
241
|
+
...(aggregated.usage?.total_tokens === undefined
|
|
242
|
+
? {}
|
|
243
|
+
: { total_tokens: aggregated.usage.total_tokens }),
|
|
244
|
+
},
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
return {
|
|
248
|
+
ok: true,
|
|
249
|
+
result: {
|
|
250
|
+
answer: aggregated.text,
|
|
251
|
+
requestId: aggregated.requestId,
|
|
252
|
+
receivedEvents: aggregated.receivedEvents,
|
|
253
|
+
finishReason: aggregated.finishReason,
|
|
254
|
+
usage: aggregated.usage,
|
|
255
|
+
},
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
catch (err) {
|
|
259
|
+
const error = err instanceof MediaError
|
|
260
|
+
? err
|
|
261
|
+
: new MediaError({
|
|
262
|
+
code: "PROVIDER_RESPONSE_INVALID",
|
|
263
|
+
stage: "analyzing",
|
|
264
|
+
diagnostic: { received_sse_events: parser.eventCount },
|
|
265
|
+
});
|
|
266
|
+
return { ok: false, error, retryAfterMs: 0, sawText: parser.sawText };
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
export async function analyzeMedia(cfg, media, request, external) {
|
|
270
|
+
const controller = new AbortController();
|
|
271
|
+
const abortFromExternal = () => {
|
|
272
|
+
controller.abort();
|
|
273
|
+
};
|
|
274
|
+
if (external?.aborted) {
|
|
275
|
+
throw new MediaError({ code: "MEDIA_ANALYSIS_CANCELLED", stage: "aborted" });
|
|
276
|
+
}
|
|
277
|
+
external?.addEventListener("abort", abortFromExternal, { once: true });
|
|
278
|
+
const timer = setTimeout(() => {
|
|
279
|
+
controller.abort();
|
|
280
|
+
}, cfg.analysisTimeoutMs);
|
|
281
|
+
let retriesLeft = cfg.analysisRetries;
|
|
282
|
+
try {
|
|
283
|
+
for (;;) {
|
|
284
|
+
const attempt = await analyzeMediaOnce(cfg, media, request, controller.signal);
|
|
285
|
+
if (attempt.ok) {
|
|
286
|
+
return attempt.result;
|
|
287
|
+
}
|
|
288
|
+
if (external?.aborted) {
|
|
289
|
+
throw new MediaError({ code: "MEDIA_ANALYSIS_CANCELLED", stage: "aborted" });
|
|
290
|
+
}
|
|
291
|
+
if (!canAutoRetry(attempt.error, attempt.sawText, retriesLeft)) {
|
|
292
|
+
throw attempt.error;
|
|
293
|
+
}
|
|
294
|
+
retriesLeft -= 1;
|
|
295
|
+
try {
|
|
296
|
+
await sleep(attempt.retryAfterMs, controller.signal);
|
|
297
|
+
}
|
|
298
|
+
catch (err) {
|
|
299
|
+
// The backoff wait is a cancellation point: an abort here is the caller
|
|
300
|
+
// cancelling, not the provider timing out.
|
|
301
|
+
if (external?.aborted) {
|
|
302
|
+
throw new MediaError({ code: "MEDIA_ANALYSIS_CANCELLED", stage: "aborted" });
|
|
303
|
+
}
|
|
304
|
+
throw err;
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
finally {
|
|
309
|
+
clearTimeout(timer);
|
|
310
|
+
external?.removeEventListener("abort", abortFromExternal);
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
export function createMediaAnalyzer(cfg) {
|
|
314
|
+
return {
|
|
315
|
+
analyze(media, request, signal) {
|
|
316
|
+
return analyzeMedia(cfg, media, request, signal);
|
|
317
|
+
},
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
//# sourceMappingURL=bailian.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"bailian.js","sourceRoot":"","sources":["../src/bailian.ts"],"names":[],"mappings":"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"}
|
package/dist/bytes.js
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Positioned read that refuses to run past the probe budget, so a malformed or
|
|
3
|
+
* hostile header can never turn a bounded probe into a full-file scan.
|
|
4
|
+
*/
|
|
5
|
+
export async function readBounded(reader, position, length, budget, limitBytes) {
|
|
6
|
+
if (length <= 0 ||
|
|
7
|
+
!Number.isSafeInteger(position) ||
|
|
8
|
+
position < 0 ||
|
|
9
|
+
budget.bytesRead + length > limitBytes) {
|
|
10
|
+
return undefined;
|
|
11
|
+
}
|
|
12
|
+
const buffer = Buffer.alloc(length);
|
|
13
|
+
const { bytesRead } = await reader.read(buffer, 0, length, position);
|
|
14
|
+
budget.bytesRead += bytesRead;
|
|
15
|
+
if (bytesRead < length) {
|
|
16
|
+
return undefined;
|
|
17
|
+
}
|
|
18
|
+
return buffer;
|
|
19
|
+
}
|
|
20
|
+
//# sourceMappingURL=bytes.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"bytes.js","sourceRoot":"","sources":["../src/bytes.ts"],"names":[],"mappings":"AAcA;;;GAGG;AACH,MAAM,CAAC,KAAK,UAAU,WAAW,CAC/B,MAAwB,EACxB,QAAgB,EAChB,MAAc,EACd,MAAkB,EAClB,UAAkB;IAElB,IACE,MAAM,IAAI,CAAC;QACX,CAAC,MAAM,CAAC,aAAa,CAAC,QAAQ,CAAC;QAC/B,QAAQ,GAAG,CAAC;QACZ,MAAM,CAAC,SAAS,GAAG,MAAM,GAAG,UAAU,EACtC,CAAC;QACD,OAAO,SAAS,CAAC;IACnB,CAAC;IACD,MAAM,MAAM,GAAG,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC;IACpC,MAAM,EAAE,SAAS,EAAE,GAAG,MAAM,MAAM,CAAC,IAAI,CAAC,MAAM,EAAE,CAAC,EAAE,MAAM,EAAE,QAAQ,CAAC,CAAC;IACrE,MAAM,CAAC,SAAS,IAAI,SAAS,CAAC;IAC9B,IAAI,SAAS,GAAG,MAAM,EAAE,CAAC;QACvB,OAAO,SAAS,CAAC;IACnB,CAAC;IACD,OAAO,MAAM,CAAC;AAChB,CAAC"}
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { resolve } from "node:path";
|
|
2
|
+
import { setCliConfigPath } from "./config-lookup.js";
|
|
3
|
+
import { ConfigError } from "./errors.js";
|
|
4
|
+
export function parseCliArgs(argv) {
|
|
5
|
+
let version = false;
|
|
6
|
+
let doctor = false;
|
|
7
|
+
let json = false;
|
|
8
|
+
let configPath;
|
|
9
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
10
|
+
const arg = argv[i];
|
|
11
|
+
if (arg === undefined) {
|
|
12
|
+
continue;
|
|
13
|
+
}
|
|
14
|
+
if (arg === "--version" || arg === "-v") {
|
|
15
|
+
version = true;
|
|
16
|
+
continue;
|
|
17
|
+
}
|
|
18
|
+
if (arg === "--doctor") {
|
|
19
|
+
doctor = true;
|
|
20
|
+
continue;
|
|
21
|
+
}
|
|
22
|
+
if (arg === "--json") {
|
|
23
|
+
json = true;
|
|
24
|
+
continue;
|
|
25
|
+
}
|
|
26
|
+
if (arg === "--config") {
|
|
27
|
+
const next = argv[i + 1];
|
|
28
|
+
if (next === undefined || next.startsWith("-")) {
|
|
29
|
+
throw new ConfigError("--config requires a file path", {
|
|
30
|
+
suggestion: "请提供 --config 指向的 env 文件路径",
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
configPath = next;
|
|
34
|
+
i += 1;
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
if (arg.startsWith("--config=")) {
|
|
38
|
+
const value = arg.slice("--config=".length);
|
|
39
|
+
if (value.trim() === "") {
|
|
40
|
+
throw new ConfigError("--config requires a file path", {
|
|
41
|
+
suggestion: "请提供 --config 指向的 env 文件路径",
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
configPath = value;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return { version, doctor, json, configPath };
|
|
48
|
+
}
|
|
49
|
+
export function applyCliArgs(argv) {
|
|
50
|
+
const args = parseCliArgs(argv);
|
|
51
|
+
if (args.configPath !== undefined) {
|
|
52
|
+
setCliConfigPath(resolve(args.configPath));
|
|
53
|
+
}
|
|
54
|
+
return args;
|
|
55
|
+
}
|
|
56
|
+
//# sourceMappingURL=cli.js.map
|
package/dist/cli.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACpC,OAAO,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;AACtD,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAS1C,MAAM,UAAU,YAAY,CAAC,IAAc;IACzC,IAAI,OAAO,GAAG,KAAK,CAAC;IACpB,IAAI,MAAM,GAAG,KAAK,CAAC;IACnB,IAAI,IAAI,GAAG,KAAK,CAAC;IACjB,IAAI,UAA8B,CAAC;IACnC,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,IAAI,CAAC,MAAM,EAAE,CAAC,IAAI,CAAC,EAAE,CAAC;QACxC,MAAM,GAAG,GAAG,IAAI,CAAC,CAAC,CAAC,CAAC;QACpB,IAAI,GAAG,KAAK,SAAS,EAAE,CAAC;YACtB,SAAS;QACX,CAAC;QACD,IAAI,GAAG,KAAK,WAAW,IAAI,GAAG,KAAK,IAAI,EAAE,CAAC;YACxC,OAAO,GAAG,IAAI,CAAC;YACf,SAAS;QACX,CAAC;QACD,IAAI,GAAG,KAAK,UAAU,EAAE,CAAC;YACvB,MAAM,GAAG,IAAI,CAAC;YACd,SAAS;QACX,CAAC;QACD,IAAI,GAAG,KAAK,QAAQ,EAAE,CAAC;YACrB,IAAI,GAAG,IAAI,CAAC;YACZ,SAAS;QACX,CAAC;QACD,IAAI,GAAG,KAAK,UAAU,EAAE,CAAC;YACvB,MAAM,IAAI,GAAG,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC;YACzB,IAAI,IAAI,KAAK,SAAS,IAAI,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,EAAE,CAAC;gBAC/C,MAAM,IAAI,WAAW,CAAC,+BAA+B,EAAE;oBACrD,UAAU,EAAE,2BAA2B;iBACxC,CAAC,CAAC;YACL,CAAC;YACD,UAAU,GAAG,IAAI,CAAC;YAClB,CAAC,IAAI,CAAC,CAAC;YACP,SAAS;QACX,CAAC;QACD,IAAI,GAAG,CAAC,UAAU,CAAC,WAAW,CAAC,EAAE,CAAC;YAChC,MAAM,KAAK,GAAG,GAAG,CAAC,KAAK,CAAC,WAAW,CAAC,MAAM,CAAC,CAAC;YAC5C,IAAI,KAAK,CAAC,IAAI,EAAE,KAAK,EAAE,EAAE,CAAC;gBACxB,MAAM,IAAI,WAAW,CAAC,+BAA+B,EAAE;oBACrD,UAAU,EAAE,2BAA2B;iBACxC,CAAC,CAAC;YACL,CAAC;YACD,UAAU,GAAG,KAAK,CAAC;QACrB,CAAC;IACH,CAAC;IACD,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,UAAU,EAAE,CAAC;AAC/C,CAAC;AAED,MAAM,UAAU,YAAY,CAAC,IAAc;IACzC,MAAM,IAAI,GAAG,YAAY,CAAC,IAAI,CAAC,CAAC;IAChC,IAAI,IAAI,CAAC,UAAU,KAAK,SAAS,EAAE,CAAC;QAClC,gBAAgB,CAAC,OAAO,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC,CAAC;IAC7C,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC"}
|