jjs_cli 0.1.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/README.md +79 -0
- package/bin/jjs.js +6 -0
- package/package.json +31 -0
- package/src/args.js +156 -0
- package/src/cli.js +355 -0
- package/src/config.js +82 -0
- package/src/mcp-client.js +141 -0
- package/src/tools.js +172 -0
package/README.md
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# jjs_cli
|
|
2
|
+
|
|
3
|
+
Node.js CLI for the `case_storage` Streamable HTTP MCP service. It follows the same install shape as Bailian-style CLIs: install with npm, then use `jjs_cli` or the short command `jjs`.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
After publishing to npm:
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install -g jjs_cli
|
|
11
|
+
jjs_cli --help
|
|
12
|
+
jjs --help
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
From this repository before publishing:
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
npm install -g ./cli/jjs_cli
|
|
19
|
+
jjs_cli --help
|
|
20
|
+
jjs --help
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Requires Node.js `>=22.12.0` and npm.
|
|
24
|
+
|
|
25
|
+
## Auth
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
jjs_cli auth login --url "https://<HOST>/mcp" --api-key "<SERVER_API_KEY>"
|
|
29
|
+
jjs_cli auth status --json
|
|
30
|
+
jjs_cli auth logout
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
The key is sent as `X-Server-API-Key`. CLI output masks the key. Local config is stored under the user config directory as `jjs_cli/config.json`, or a custom path from `CASE_STORAGE_CLI_CONFIG`.
|
|
34
|
+
|
|
35
|
+
Environment overrides:
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
CASE_STORAGE_MCP_URL="https://<HOST>/mcp"
|
|
39
|
+
CASE_STORAGE_SERVER_API_KEY="<SERVER_API_KEY>"
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Commands
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
jjs_cli tools
|
|
46
|
+
jjs_cli schema create_image
|
|
47
|
+
jjs_cli call get_user_info --json
|
|
48
|
+
jjs_cli image create --prompt "产品宣传图" --aspect-ratio 3:4 --image-size 2K
|
|
49
|
+
jjs_cli video seedance --prompt "产品缓慢旋转" --image "https://example.com/ref.jpg"
|
|
50
|
+
jjs_cli task get --task-id "<TASK_ID>" --task-type image
|
|
51
|
+
jjs_cli voice public
|
|
52
|
+
jjs_cli voice synthesize --content "你好" --speaker-id "<SPEAKER_ID>"
|
|
53
|
+
jjs_cli douhot search --keyword "热点" --page 1
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
The installed `jjs` command is equivalent to `jjs_cli`:
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
jjs tools
|
|
60
|
+
jjs image create --prompt "产品宣传图" --json
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Generic calls accept:
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
jjs_cli call create_image \
|
|
67
|
+
--arg prompt="产品宣传图" \
|
|
68
|
+
--json-arg image_urls='["https://example.com/ref.jpg"]' \
|
|
69
|
+
--json
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## Test
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
npm test
|
|
76
|
+
npm pack --dry-run
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
The automated tests use a fake local MCP server and do not require a real backend or key.
|
package/bin/jjs.js
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "jjs_cli",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Node.js CLI for case_storage Streamable HTTP MCP tools.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"jjs_cli": "bin/jjs.js",
|
|
8
|
+
"jjs": "bin/jjs.js"
|
|
9
|
+
},
|
|
10
|
+
"scripts": {
|
|
11
|
+
"test": "node --test"
|
|
12
|
+
},
|
|
13
|
+
"engines": {
|
|
14
|
+
"node": ">=22.12.0"
|
|
15
|
+
},
|
|
16
|
+
"files": [
|
|
17
|
+
"bin/",
|
|
18
|
+
"src/",
|
|
19
|
+
"README.md"
|
|
20
|
+
],
|
|
21
|
+
"keywords": [
|
|
22
|
+
"jjs",
|
|
23
|
+
"mcp",
|
|
24
|
+
"cli"
|
|
25
|
+
],
|
|
26
|
+
"license": "MIT",
|
|
27
|
+
"publishConfig": {
|
|
28
|
+
"access": "public",
|
|
29
|
+
"registry": "https://registry.npmjs.org/"
|
|
30
|
+
}
|
|
31
|
+
}
|
package/src/args.js
ADDED
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
export function parseGlobalOptions(argv) {
|
|
2
|
+
const options = { json: false };
|
|
3
|
+
const rest = [];
|
|
4
|
+
|
|
5
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
6
|
+
const token = argv[i];
|
|
7
|
+
if (token === '--json') {
|
|
8
|
+
options.json = true;
|
|
9
|
+
continue;
|
|
10
|
+
}
|
|
11
|
+
if (token === '--output') {
|
|
12
|
+
const value = requireValue(argv, i, token);
|
|
13
|
+
i += 1;
|
|
14
|
+
options.output = value;
|
|
15
|
+
if (value === 'json') options.json = true;
|
|
16
|
+
continue;
|
|
17
|
+
}
|
|
18
|
+
if (token === '--url') {
|
|
19
|
+
options.url = requireValue(argv, i, token);
|
|
20
|
+
i += 1;
|
|
21
|
+
continue;
|
|
22
|
+
}
|
|
23
|
+
if (token === '--api-key') {
|
|
24
|
+
options.apiKey = requireValue(argv, i, token);
|
|
25
|
+
i += 1;
|
|
26
|
+
continue;
|
|
27
|
+
}
|
|
28
|
+
if (token === '--config') {
|
|
29
|
+
options.configPath = requireValue(argv, i, token);
|
|
30
|
+
i += 1;
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
if (token === '--non-interactive') {
|
|
34
|
+
options.nonInteractive = true;
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
rest.push(token);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return { options, argv: rest };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function parseCallArguments(argv) {
|
|
44
|
+
const out = {};
|
|
45
|
+
|
|
46
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
47
|
+
const token = argv[i];
|
|
48
|
+
if (token === '--args-json') {
|
|
49
|
+
const raw = requireValue(argv, i, token);
|
|
50
|
+
i += 1;
|
|
51
|
+
const parsed = parseJson(raw, '--args-json');
|
|
52
|
+
if (!parsed || Array.isArray(parsed) || typeof parsed !== 'object') {
|
|
53
|
+
throw new Error('--args-json must be a JSON object');
|
|
54
|
+
}
|
|
55
|
+
Object.assign(out, parsed);
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
if (token === '--arg' || token === '--json-arg') {
|
|
59
|
+
const raw = requireValue(argv, i, token);
|
|
60
|
+
i += 1;
|
|
61
|
+
const [key, value] = splitKeyValue(raw, token);
|
|
62
|
+
out[key] = token === '--json-arg' ? parseJson(value, key) : value;
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
throw new Error(`Unknown call argument option: ${token}`);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return out;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function optionValue(argv, name, fallback = undefined) {
|
|
72
|
+
const index = argv.indexOf(name);
|
|
73
|
+
if (index === -1) return fallback;
|
|
74
|
+
return requireValue(argv, index, name);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function optionNumber(argv, name, fallback = undefined) {
|
|
78
|
+
const value = optionValue(argv, name);
|
|
79
|
+
if (value === undefined) return fallback;
|
|
80
|
+
const number = Number(value);
|
|
81
|
+
if (!Number.isFinite(number)) {
|
|
82
|
+
throw new Error(`${name} must be a number`);
|
|
83
|
+
}
|
|
84
|
+
return number;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function optionFlag(argv, name) {
|
|
88
|
+
return argv.includes(name);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function optionBoolean(argv, positive, negative, fallback = undefined) {
|
|
92
|
+
if (argv.includes(positive)) return true;
|
|
93
|
+
if (negative && argv.includes(negative)) return false;
|
|
94
|
+
return fallback;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function optionList(argv, name) {
|
|
98
|
+
const values = [];
|
|
99
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
100
|
+
if (argv[i] === name) {
|
|
101
|
+
values.push(requireValue(argv, i, name));
|
|
102
|
+
i += 1;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
return values;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export function compactObject(input) {
|
|
109
|
+
return Object.fromEntries(Object.entries(input).filter(([, value]) => {
|
|
110
|
+
if (value === undefined || value === null) return false;
|
|
111
|
+
if (Array.isArray(value) && value.length === 0) return false;
|
|
112
|
+
if (value === '') return false;
|
|
113
|
+
return true;
|
|
114
|
+
}));
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export function rejectUnknownOptions(argv, allowed) {
|
|
118
|
+
const allowedSet = new Set(allowed);
|
|
119
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
120
|
+
const token = argv[i];
|
|
121
|
+
if (!token.startsWith('--')) continue;
|
|
122
|
+
if (!allowedSet.has(token)) {
|
|
123
|
+
throw new Error(`Unknown option: ${token}`);
|
|
124
|
+
}
|
|
125
|
+
if (flagTakesValue(token)) i += 1;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function flagTakesValue(token) {
|
|
130
|
+
return !token.startsWith('--no-') && !['--sync', '--auto-translate', '--return-raw', '--cache'].includes(token);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function splitKeyValue(raw, label) {
|
|
134
|
+
const index = raw.indexOf('=');
|
|
135
|
+
if (index <= 0) {
|
|
136
|
+
throw new Error(`${label} expects key=value`);
|
|
137
|
+
}
|
|
138
|
+
return [raw.slice(0, index), raw.slice(index + 1)];
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function parseJson(raw, label) {
|
|
142
|
+
try {
|
|
143
|
+
return JSON.parse(raw);
|
|
144
|
+
} catch (error) {
|
|
145
|
+
throw new Error(`Invalid JSON for ${label}: ${error.message}`);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function requireValue(argv, index, name) {
|
|
150
|
+
const value = argv[index + 1];
|
|
151
|
+
if (value === undefined || value.startsWith('--')) {
|
|
152
|
+
throw new Error(`${name} requires a value`);
|
|
153
|
+
}
|
|
154
|
+
return value;
|
|
155
|
+
}
|
|
156
|
+
|
package/src/cli.js
ADDED
|
@@ -0,0 +1,355 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
compactObject,
|
|
6
|
+
optionBoolean,
|
|
7
|
+
optionFlag,
|
|
8
|
+
optionList,
|
|
9
|
+
optionNumber,
|
|
10
|
+
optionValue,
|
|
11
|
+
parseCallArguments,
|
|
12
|
+
parseGlobalOptions,
|
|
13
|
+
} from './args.js';
|
|
14
|
+
import { configPathFromEnv, deleteConfig, loadConfig, resolveAuth, saveConfig, statusPayload } from './config.js';
|
|
15
|
+
import { McpClient, normalizeMcpUrl, redactText } from './mcp-client.js';
|
|
16
|
+
import { DOUHOT_COMMANDS, TOOL_SPECS, getToolSpec } from './tools.js';
|
|
17
|
+
|
|
18
|
+
export async function main(rawArgv, io = defaultIo()) {
|
|
19
|
+
const { options, argv } = parseGlobalOptions(rawArgv);
|
|
20
|
+
const secrets = [options.apiKey, io.env.CASE_STORAGE_SERVER_API_KEY].filter(Boolean);
|
|
21
|
+
try {
|
|
22
|
+
const code = await dispatch(argv, options, io);
|
|
23
|
+
return code;
|
|
24
|
+
} catch (error) {
|
|
25
|
+
io.stderr.write(`${redactText(error.message || String(error), secrets)}\n`);
|
|
26
|
+
return 1;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async function dispatch(argv, options, io) {
|
|
31
|
+
const command = argv[0];
|
|
32
|
+
if (!command || command === '--help' || command === '-h' || command === 'help') {
|
|
33
|
+
write(io, helpText());
|
|
34
|
+
return 0;
|
|
35
|
+
}
|
|
36
|
+
if (command === '--version' || command === 'version') {
|
|
37
|
+
write(io, 'jjs_cli 0.1.0\n');
|
|
38
|
+
return 0;
|
|
39
|
+
}
|
|
40
|
+
if (command === 'auth') return authCommand(argv.slice(1), options, io);
|
|
41
|
+
if (command === 'tools') return toolsCommand(argv.slice(1), options, io);
|
|
42
|
+
if (command === 'schema') return schemaCommand(argv.slice(1), options, io);
|
|
43
|
+
if (command === 'call') return callCommand(argv.slice(1), options, io);
|
|
44
|
+
if (command === 'user') return userCommand(argv.slice(1), options, io);
|
|
45
|
+
if (command === 'material') return materialCommand(argv.slice(1), options, io);
|
|
46
|
+
if (command === 'image') return imageCommand(argv.slice(1), options, io);
|
|
47
|
+
if (command === 'task') return taskCommand(argv.slice(1), options, io);
|
|
48
|
+
if (command === 'video') return videoCommand(argv.slice(1), options, io);
|
|
49
|
+
if (command === 'digital-human') return digitalHumanCommand(argv.slice(1), options, io);
|
|
50
|
+
if (command === 'voice') return voiceCommand(argv.slice(1), options, io);
|
|
51
|
+
if (command === 'douhot') return douhotCommand(argv.slice(1), options, io);
|
|
52
|
+
throw new Error(`Unknown command: ${command}\nRun: jjs_cli --help`);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async function authCommand(argv, options, io) {
|
|
56
|
+
const subcommand = argv[0] || 'status';
|
|
57
|
+
const configPath = configPathFromEnv(io.env, options);
|
|
58
|
+
const config = loadConfig(configPath);
|
|
59
|
+
|
|
60
|
+
if (subcommand === 'status') {
|
|
61
|
+
const payload = statusPayload({ auth: resolveAuth({ config, env: io.env, flags: options }), configPath });
|
|
62
|
+
return output(payload, options, io, humanAuthStatus(payload));
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (subcommand === 'login') {
|
|
66
|
+
const url = options.url || optionValue(argv, '--url') || config.mcp_url;
|
|
67
|
+
const apiKey = options.apiKey || optionValue(argv, '--api-key') || config.server_api_key;
|
|
68
|
+
if (!url) throw new Error('auth login requires --url <http(s)://host/mcp>');
|
|
69
|
+
if (!apiKey) throw new Error('auth login requires --api-key <SERVER_API_KEY>');
|
|
70
|
+
saveConfig(configPath, {
|
|
71
|
+
mcp_url: normalizeMcpUrl(url),
|
|
72
|
+
server_api_key: apiKey,
|
|
73
|
+
});
|
|
74
|
+
const payload = statusPayload({
|
|
75
|
+
auth: resolveAuth({ config: loadConfig(configPath), env: {}, flags: {} }),
|
|
76
|
+
configPath,
|
|
77
|
+
});
|
|
78
|
+
return output(payload, options, io, `Saved case_storage MCP credentials to ${configPath}\nAPI Key: ${payload.api_key}\n`);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (subcommand === 'logout') {
|
|
82
|
+
deleteConfig(configPath);
|
|
83
|
+
return output({ ok: true, config_path: configPath }, options, io, `Removed ${configPath}\n`);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
throw new Error(`Unknown auth command: ${subcommand}`);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
async function toolsCommand(argv, options, io) {
|
|
90
|
+
if (optionFlag(argv, '--remote')) {
|
|
91
|
+
const result = await clientFromConfig(options, io).listTools();
|
|
92
|
+
return output(result, options, io, `${JSON.stringify(result, null, 2)}\n`);
|
|
93
|
+
}
|
|
94
|
+
const payload = { tools: TOOL_SPECS };
|
|
95
|
+
const human = TOOL_SPECS.map((tool) => `${tool.name.padEnd(38)} ${tool.description}`).join('\n');
|
|
96
|
+
return output(payload, options, io, `${human}\n`);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
async function schemaCommand(argv, options, io) {
|
|
100
|
+
const toolName = argv[0];
|
|
101
|
+
if (!toolName) throw new Error('schema requires a tool name');
|
|
102
|
+
const spec = getToolSpec(toolName);
|
|
103
|
+
return output(spec, options, io, `${JSON.stringify(spec, null, 2)}\n`);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
async function callCommand(argv, options, io) {
|
|
107
|
+
const toolName = argv[0];
|
|
108
|
+
if (!toolName) throw new Error('call requires a tool name');
|
|
109
|
+
getToolSpec(toolName);
|
|
110
|
+
const args = parseCallArguments(argv.slice(1));
|
|
111
|
+
return callAndOutput(toolName, args, options, io);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
async function userCommand(argv, options, io) {
|
|
115
|
+
if (argv[0] !== 'info') throw new Error('Usage: jjs_cli user info');
|
|
116
|
+
return callAndOutput('get_user_info', {}, options, io);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
async function materialCommand(argv, options, io) {
|
|
120
|
+
if (argv[0] !== 'upload') throw new Error('Usage: jjs_cli material upload --file <path>');
|
|
121
|
+
const file = optionValue(argv, '--file');
|
|
122
|
+
if (!file) throw new Error('material upload requires --file <path>');
|
|
123
|
+
const content = fs.readFileSync(file);
|
|
124
|
+
const args = compactObject({
|
|
125
|
+
file_name: optionValue(argv, '--file-name', path.basename(file)),
|
|
126
|
+
mime_type: optionValue(argv, '--mime-type', inferMimeType(file)),
|
|
127
|
+
content_base64: content.toString('base64'),
|
|
128
|
+
title: optionValue(argv, '--title'),
|
|
129
|
+
category: optionValue(argv, '--category'),
|
|
130
|
+
tags: optionValue(argv, '--tags'),
|
|
131
|
+
});
|
|
132
|
+
return callAndOutput('upload_material', args, options, io);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
async function imageCommand(argv, options, io) {
|
|
136
|
+
if (argv[0] !== 'create') throw new Error('Usage: jjs_cli image create --prompt <text>');
|
|
137
|
+
const args = compactObject({
|
|
138
|
+
prompt: optionValue(argv, '--prompt'),
|
|
139
|
+
image_urls: optionList(argv, '--image-url'),
|
|
140
|
+
image_generation_type: optionValue(argv, '--type'),
|
|
141
|
+
model: optionValue(argv, '--model'),
|
|
142
|
+
aspect_ratio: optionValue(argv, '--aspect-ratio'),
|
|
143
|
+
image_size: optionValue(argv, '--image-size'),
|
|
144
|
+
title: optionValue(argv, '--title'),
|
|
145
|
+
task: optionValue(argv, '--task'),
|
|
146
|
+
is_sync: optionFlag(argv, '--sync') || undefined,
|
|
147
|
+
transparent_background: optionBoolean(argv, '--transparent-background', '--no-transparent-background'),
|
|
148
|
+
transparent_key_color: optionValue(argv, '--transparent-key-color'),
|
|
149
|
+
});
|
|
150
|
+
return callAndOutput('create_image', args, options, io);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
async function taskCommand(argv, options, io) {
|
|
154
|
+
if (argv[0] !== 'get') throw new Error('Usage: jjs_cli task get --task-id <id> --task-type <image|video|digital_human>');
|
|
155
|
+
const args = compactObject({
|
|
156
|
+
task_id: optionValue(argv, '--task-id'),
|
|
157
|
+
task_type: optionValue(argv, '--task-type'),
|
|
158
|
+
});
|
|
159
|
+
return callAndOutput('get_task', args, options, io);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
async function videoCommand(argv, options, io) {
|
|
163
|
+
const subcommand = argv[0];
|
|
164
|
+
const toolName = subcommand === 'seedance'
|
|
165
|
+
? 'create_seedance_video'
|
|
166
|
+
: subcommand === 'happyhorse'
|
|
167
|
+
? 'create_happyhorse_video'
|
|
168
|
+
: '';
|
|
169
|
+
if (!toolName) throw new Error('Usage: jjs_cli video <seedance|happyhorse> --prompt <text>');
|
|
170
|
+
const args = compactObject({
|
|
171
|
+
prompt: optionValue(argv, '--prompt'),
|
|
172
|
+
images: optionList(argv, '--image'),
|
|
173
|
+
videos: optionList(argv, '--video'),
|
|
174
|
+
audios: optionList(argv, '--audio'),
|
|
175
|
+
virtual_portrait_uri: optionValue(argv, '--virtual-portrait-uri'),
|
|
176
|
+
virtual_portrait_uris: optionList(argv, '--virtual-portrait-uri'),
|
|
177
|
+
aspect_ratio: optionValue(argv, '--aspect-ratio'),
|
|
178
|
+
quality: optionValue(argv, '--quality'),
|
|
179
|
+
orientation: optionValue(argv, '--orientation'),
|
|
180
|
+
duration: optionNumber(argv, '--duration'),
|
|
181
|
+
seed: optionValue(argv, '--seed'),
|
|
182
|
+
auto_translate: optionFlag(argv, '--auto-translate') || undefined,
|
|
183
|
+
title: optionValue(argv, '--title'),
|
|
184
|
+
});
|
|
185
|
+
return callAndOutput(toolName, args, options, io);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
async function digitalHumanCommand(argv, options, io) {
|
|
189
|
+
if (argv[0] !== 'create') throw new Error('Usage: jjs_cli digital-human create --image-url <url> --audio-url <url>');
|
|
190
|
+
const args = compactObject({
|
|
191
|
+
image_url: optionValue(argv, '--image-url'),
|
|
192
|
+
audio_url: optionValue(argv, '--audio-url'),
|
|
193
|
+
resolution: optionValue(argv, '--resolution'),
|
|
194
|
+
start_time: optionValue(argv, '--start-time'),
|
|
195
|
+
end_time: optionValue(argv, '--end-time'),
|
|
196
|
+
prompt: optionValue(argv, '--prompt'),
|
|
197
|
+
title: optionValue(argv, '--title'),
|
|
198
|
+
});
|
|
199
|
+
return callAndOutput('create_digital_human', args, options, io);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
async function voiceCommand(argv, options, io) {
|
|
203
|
+
const subcommand = argv[0];
|
|
204
|
+
if (subcommand === 'public') return callAndOutput('get_public_voice', {}, options, io);
|
|
205
|
+
if (subcommand === 'custom') return callAndOutput('get_custom_voice', {}, options, io);
|
|
206
|
+
if (subcommand === 'synthesize') {
|
|
207
|
+
const args = compactObject({
|
|
208
|
+
content: optionValue(argv, '--content'),
|
|
209
|
+
speaker_id: optionValue(argv, '--speaker-id'),
|
|
210
|
+
voice_id: optionValue(argv, '--voice-id'),
|
|
211
|
+
group_key: optionValue(argv, '--group-key'),
|
|
212
|
+
model: optionValue(argv, '--model'),
|
|
213
|
+
rate: optionNumber(argv, '--rate'),
|
|
214
|
+
volume: optionNumber(argv, '--volume'),
|
|
215
|
+
audio_format: optionValue(argv, '--audio-format'),
|
|
216
|
+
sample_rate: optionNumber(argv, '--sample-rate'),
|
|
217
|
+
});
|
|
218
|
+
return callAndOutput('create_audio_task', args, options, io);
|
|
219
|
+
}
|
|
220
|
+
if (subcommand === 'clone') {
|
|
221
|
+
const args = compactObject({
|
|
222
|
+
audio_url: optionValue(argv, '--audio-url'),
|
|
223
|
+
prefix: optionValue(argv, '--prefix'),
|
|
224
|
+
language_hints: optionList(argv, '--language-hint'),
|
|
225
|
+
});
|
|
226
|
+
return callAndOutput('clone_voice', args, options, io);
|
|
227
|
+
}
|
|
228
|
+
throw new Error('Usage: jjs_cli voice <public|custom|synthesize|clone>');
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
async function douhotCommand(argv, options, io) {
|
|
232
|
+
const subcommand = argv[0];
|
|
233
|
+
if (subcommand === 'call') return callCommand(argv.slice(1), options, io);
|
|
234
|
+
const toolName = DOUHOT_COMMANDS[subcommand];
|
|
235
|
+
if (!toolName) throw new Error(`Usage: jjs_cli douhot <${Object.keys(DOUHOT_COMMANDS).join('|')}>`);
|
|
236
|
+
if ((subcommand === 'hot-word' || subcommand === 'author') && !optionValue(argv, '--keyword')) {
|
|
237
|
+
throw new Error(`douhot ${subcommand} requires --keyword <text>`);
|
|
238
|
+
}
|
|
239
|
+
const args = compactObject({
|
|
240
|
+
keyword: optionValue(argv, '--keyword'),
|
|
241
|
+
page: optionNumber(argv, '--page'),
|
|
242
|
+
page_size: optionNumber(argv, '--page-size'),
|
|
243
|
+
type: optionValue(argv, '--type'),
|
|
244
|
+
sec_uid: optionValue(argv, '--sec-uid'),
|
|
245
|
+
min_cursor: optionValue(argv, '--min-cursor'),
|
|
246
|
+
max_cursor: optionValue(argv, '--max-cursor'),
|
|
247
|
+
item_id: optionValue(argv, '--item-id'),
|
|
248
|
+
option: optionNumber(argv, '--option'),
|
|
249
|
+
date_window: optionNumber(argv, '--date-window'),
|
|
250
|
+
return_raw: optionBoolean(argv, '--return-raw', '--no-return-raw'),
|
|
251
|
+
cache: optionBoolean(argv, '--cache', '--no-cache'),
|
|
252
|
+
cookie: optionValue(argv, '--cookie'),
|
|
253
|
+
});
|
|
254
|
+
return callAndOutput(toolName, args, options, io);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
async function callAndOutput(toolName, args, options, io) {
|
|
258
|
+
const result = await clientFromConfig(options, io).callTool(toolName, args);
|
|
259
|
+
const code = result?.success === false ? 2 : 0;
|
|
260
|
+
output(result, options, io, `${JSON.stringify(result, null, 2)}\n`);
|
|
261
|
+
return code;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function clientFromConfig(options, io) {
|
|
265
|
+
const configPath = configPathFromEnv(io.env, options);
|
|
266
|
+
const config = loadConfig(configPath);
|
|
267
|
+
const auth = resolveAuth({ config, env: io.env, flags: options });
|
|
268
|
+
return new McpClient({ url: auth.url, apiKey: auth.apiKey });
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function output(payload, options, io, human) {
|
|
272
|
+
if (options.json) {
|
|
273
|
+
write(io, `${JSON.stringify(payload)}\n`);
|
|
274
|
+
} else {
|
|
275
|
+
write(io, human);
|
|
276
|
+
}
|
|
277
|
+
return payload?.success === false ? 2 : 0;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
function write(io, text) {
|
|
281
|
+
io.stdout.write(text);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function humanAuthStatus(payload) {
|
|
285
|
+
if (!payload.authenticated) {
|
|
286
|
+
return `Not authenticated\nConfig: ${payload.config_path}\nRun: jjs_cli auth login --url <MCP_URL> --api-key <SERVER_API_KEY>\n`;
|
|
287
|
+
}
|
|
288
|
+
return `Authenticated\nMCP URL: ${payload.mcp_url}\nAPI Key: ${payload.api_key}\nConfig: ${payload.config_path}\n`;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
function inferMimeType(file) {
|
|
292
|
+
const ext = path.extname(file).toLowerCase();
|
|
293
|
+
if (ext === '.png') return 'image/png';
|
|
294
|
+
if (ext === '.jpg' || ext === '.jpeg') return 'image/jpeg';
|
|
295
|
+
if (ext === '.webp') return 'image/webp';
|
|
296
|
+
throw new Error('Unable to infer MIME type. Pass --mime-type image/png|image/jpeg|image/webp');
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
function defaultIo() {
|
|
300
|
+
return {
|
|
301
|
+
stdout: process.stdout,
|
|
302
|
+
stderr: process.stderr,
|
|
303
|
+
env: process.env,
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
function helpText() {
|
|
308
|
+
return `jjs_cli - Node.js CLI for case_storage MCP tools
|
|
309
|
+
|
|
310
|
+
Install:
|
|
311
|
+
npm install -g jjs_cli
|
|
312
|
+
|
|
313
|
+
Commands after install:
|
|
314
|
+
jjs_cli
|
|
315
|
+
jjs
|
|
316
|
+
|
|
317
|
+
Repo-local install:
|
|
318
|
+
npm install -g ./cli/jjs_cli
|
|
319
|
+
|
|
320
|
+
Global options:
|
|
321
|
+
--url <url> MCP URL, for example http://127.0.0.1:8303/mcp
|
|
322
|
+
--api-key <key> Service API Key sent as X-Server-API-Key
|
|
323
|
+
--json Machine-readable output
|
|
324
|
+
--output json Bailian-style JSON output alias
|
|
325
|
+
--config <path> Override local config path
|
|
326
|
+
|
|
327
|
+
Auth:
|
|
328
|
+
jjs_cli auth login --url <url> --api-key <key>
|
|
329
|
+
jjs_cli auth status --json
|
|
330
|
+
jjs_cli auth logout
|
|
331
|
+
|
|
332
|
+
Introspection:
|
|
333
|
+
jjs_cli tools
|
|
334
|
+
jjs_cli schema <tool>
|
|
335
|
+
jjs_cli call <tool> --arg key=value --json-arg list='["a"]'
|
|
336
|
+
|
|
337
|
+
Friendly commands:
|
|
338
|
+
jjs_cli user info
|
|
339
|
+
jjs_cli material upload --file ref.png
|
|
340
|
+
jjs_cli image create --prompt "产品宣传图" --image-url https://...
|
|
341
|
+
jjs_cli task get --task-id <id> --task-type image
|
|
342
|
+
jjs_cli video seedance --prompt "..." --image https://...
|
|
343
|
+
jjs_cli video happyhorse --prompt "..." --image https://...
|
|
344
|
+
jjs_cli digital-human create --image-url https://... --audio-url https://...
|
|
345
|
+
jjs_cli voice public | custom
|
|
346
|
+
jjs_cli voice synthesize --content "你好" --speaker-id <id>
|
|
347
|
+
jjs_cli voice clone --audio-url https://...
|
|
348
|
+
jjs_cli douhot search --keyword "热点"
|
|
349
|
+
|
|
350
|
+
Environment:
|
|
351
|
+
CASE_STORAGE_MCP_URL
|
|
352
|
+
CASE_STORAGE_SERVER_API_KEY
|
|
353
|
+
CASE_STORAGE_CLI_CONFIG
|
|
354
|
+
`;
|
|
355
|
+
}
|
package/src/config.js
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
|
|
5
|
+
const CONFIG_DIR_NAME = 'jjs_cli';
|
|
6
|
+
|
|
7
|
+
export function defaultConfigPath(runtime = {}) {
|
|
8
|
+
const platform = runtime.platform ?? process.platform;
|
|
9
|
+
const env = runtime.env ?? process.env;
|
|
10
|
+
const homedir = runtime.homedir ?? os.homedir;
|
|
11
|
+
|
|
12
|
+
if (platform === 'win32') {
|
|
13
|
+
const base = env.APPDATA || path.join(homedir(), 'AppData', 'Roaming');
|
|
14
|
+
return path.join(base, CONFIG_DIR_NAME, 'config.json');
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const base = env.XDG_CONFIG_HOME || path.join(homedir(), '.config');
|
|
18
|
+
return path.join(base, CONFIG_DIR_NAME, 'config.json');
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function configPathFromEnv(env = process.env, flags = {}) {
|
|
22
|
+
return flags.configPath || env.CASE_STORAGE_CLI_CONFIG || defaultConfigPath({ env });
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function loadConfig(configPath) {
|
|
26
|
+
if (!fs.existsSync(configPath)) return {};
|
|
27
|
+
const raw = fs.readFileSync(configPath, 'utf8').trim();
|
|
28
|
+
if (!raw) return {};
|
|
29
|
+
return JSON.parse(raw);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function saveConfig(configPath, config) {
|
|
33
|
+
fs.mkdirSync(path.dirname(configPath), { recursive: true });
|
|
34
|
+
fs.writeFileSync(configPath, `${JSON.stringify(config, null, 2)}\n`, { mode: 0o600 });
|
|
35
|
+
try {
|
|
36
|
+
fs.chmodSync(configPath, 0o600);
|
|
37
|
+
} catch {
|
|
38
|
+
// Windows ACLs are handled by the user profile; chmod is best-effort.
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function deleteConfig(configPath) {
|
|
43
|
+
if (fs.existsSync(configPath)) fs.unlinkSync(configPath);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function resolveAuth({ config = {}, env = process.env, flags = {} } = {}) {
|
|
47
|
+
const envUrl = env.CASE_STORAGE_MCP_URL || env.CASE_STORAGE_BASE_URL;
|
|
48
|
+
const envKey = env.CASE_STORAGE_SERVER_API_KEY;
|
|
49
|
+
const configUrl = config.mcp_url || config.url || config.base_url;
|
|
50
|
+
const configKey = config.server_api_key || config.api_key;
|
|
51
|
+
|
|
52
|
+
const url = flags.url || envUrl || configUrl || '';
|
|
53
|
+
const apiKey = flags.apiKey || envKey || configKey || '';
|
|
54
|
+
return {
|
|
55
|
+
url,
|
|
56
|
+
apiKey,
|
|
57
|
+
source: {
|
|
58
|
+
url: flags.url ? 'flag' : envUrl ? 'env' : configUrl ? 'config' : 'missing',
|
|
59
|
+
apiKey: flags.apiKey ? 'flag' : envKey ? 'env' : configKey ? 'config' : 'missing',
|
|
60
|
+
},
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function maskApiKey(apiKey) {
|
|
65
|
+
if (!apiKey) return null;
|
|
66
|
+
const text = String(apiKey);
|
|
67
|
+
if (text.length <= 8) return `${text.slice(0, 2)}***`;
|
|
68
|
+
if (text.startsWith('gva_sk_')) {
|
|
69
|
+
return `gva_***${text.slice(-4)}`;
|
|
70
|
+
}
|
|
71
|
+
return `${text.slice(0, 4)}***${text.slice(-4)}`;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function statusPayload({ auth, configPath }) {
|
|
75
|
+
return {
|
|
76
|
+
authenticated: Boolean(auth.url && auth.apiKey),
|
|
77
|
+
mcp_url: auth.url || null,
|
|
78
|
+
api_key: maskApiKey(auth.apiKey),
|
|
79
|
+
source: auth.source,
|
|
80
|
+
config_path: configPath,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
const DEFAULT_PROTOCOL_VERSION = '2025-06-18';
|
|
2
|
+
|
|
3
|
+
export class McpClient {
|
|
4
|
+
constructor({ url, apiKey, fetchImpl = globalThis.fetch, protocolVersion = DEFAULT_PROTOCOL_VERSION } = {}) {
|
|
5
|
+
if (!url) throw new Error('MCP URL is required. Run: jjs_cli auth login --url <url> --api-key <key>');
|
|
6
|
+
if (!apiKey) throw new Error('Service API Key is required. Run: jjs_cli auth login --url <url> --api-key <key>');
|
|
7
|
+
if (typeof fetchImpl !== 'function') throw new Error('Node.js fetch is unavailable; Node >= 22.12.0 is required');
|
|
8
|
+
this.url = normalizeMcpUrl(url);
|
|
9
|
+
this.apiKey = apiKey;
|
|
10
|
+
this.fetchImpl = fetchImpl;
|
|
11
|
+
this.protocolVersion = protocolVersion;
|
|
12
|
+
this.sessionId = '';
|
|
13
|
+
this.nextId = 1;
|
|
14
|
+
this.initialized = false;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
async initialize() {
|
|
18
|
+
if (this.initialized) return;
|
|
19
|
+
await this.request('initialize', {
|
|
20
|
+
protocolVersion: this.protocolVersion,
|
|
21
|
+
clientInfo: { name: 'jjs_cli', version: '0.1.0' },
|
|
22
|
+
capabilities: {},
|
|
23
|
+
}, { skipInitialize: true });
|
|
24
|
+
this.initialized = true;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async listTools() {
|
|
28
|
+
await this.initialize();
|
|
29
|
+
return this.request('tools/list', {});
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async callTool(name, args = {}) {
|
|
33
|
+
await this.initialize();
|
|
34
|
+
const response = await this.request('tools/call', { name, arguments: args || {} });
|
|
35
|
+
return unwrapToolResult(response);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async request(method, params = {}, options = {}) {
|
|
39
|
+
const id = this.nextId;
|
|
40
|
+
this.nextId += 1;
|
|
41
|
+
const headers = {
|
|
42
|
+
Accept: 'application/json, text/event-stream',
|
|
43
|
+
'Content-Type': 'application/json',
|
|
44
|
+
'X-Server-API-Key': this.apiKey,
|
|
45
|
+
};
|
|
46
|
+
if (this.sessionId) headers['Mcp-Session-Id'] = this.sessionId;
|
|
47
|
+
|
|
48
|
+
const body = JSON.stringify({ jsonrpc: '2.0', id, method, params });
|
|
49
|
+
let response;
|
|
50
|
+
try {
|
|
51
|
+
response = await this.fetchImpl(this.url, { method: 'POST', headers, body });
|
|
52
|
+
} catch (error) {
|
|
53
|
+
throw new Error(redactText(`MCP request failed: ${error.message}`, [this.apiKey]));
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const sessionId = response.headers?.get?.('mcp-session-id');
|
|
57
|
+
if (sessionId) this.sessionId = sessionId;
|
|
58
|
+
|
|
59
|
+
const text = await response.text();
|
|
60
|
+
if (!response.ok) {
|
|
61
|
+
throw new Error(redactText(`MCP HTTP ${response.status}: ${text}`, [this.apiKey]));
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const payload = parseResponseBody(text);
|
|
65
|
+
if (payload.error) {
|
|
66
|
+
const message = payload.error.message || JSON.stringify(payload.error);
|
|
67
|
+
throw new Error(redactText(`MCP JSON-RPC error: ${message}`, [this.apiKey]));
|
|
68
|
+
}
|
|
69
|
+
if (!Object.prototype.hasOwnProperty.call(payload, 'result')) {
|
|
70
|
+
throw new Error('MCP response missing result');
|
|
71
|
+
}
|
|
72
|
+
return payload.result;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function parseResponseBody(text) {
|
|
77
|
+
const raw = String(text || '').trim();
|
|
78
|
+
if (!raw) throw new Error('MCP response body is empty');
|
|
79
|
+
if (!looksLikeSse(raw)) return JSON.parse(raw);
|
|
80
|
+
|
|
81
|
+
const frames = [];
|
|
82
|
+
let current = [];
|
|
83
|
+
for (const line of raw.split(/\r?\n/)) {
|
|
84
|
+
if (line === '') {
|
|
85
|
+
if (current.length > 0) frames.push(current);
|
|
86
|
+
current = [];
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
current.push(line);
|
|
90
|
+
}
|
|
91
|
+
if (current.length > 0) frames.push(current);
|
|
92
|
+
|
|
93
|
+
let lastPayload = null;
|
|
94
|
+
for (const frame of frames) {
|
|
95
|
+
const dataLines = [];
|
|
96
|
+
for (const line of frame) {
|
|
97
|
+
if (line.startsWith(':')) continue;
|
|
98
|
+
if (!line.startsWith('data:')) continue;
|
|
99
|
+
dataLines.push(line.slice(5).trimStart());
|
|
100
|
+
}
|
|
101
|
+
if (dataLines.length === 0) continue;
|
|
102
|
+
const data = dataLines.join('\n').trim();
|
|
103
|
+
if (!data || data === '[DONE]') continue;
|
|
104
|
+
lastPayload = JSON.parse(data);
|
|
105
|
+
}
|
|
106
|
+
if (!lastPayload) throw new Error('MCP SSE response contained no JSON data');
|
|
107
|
+
return lastPayload;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export function redactText(text, secrets = []) {
|
|
111
|
+
let out = String(text ?? '');
|
|
112
|
+
for (const secret of secrets) {
|
|
113
|
+
if (secret) out = out.split(String(secret)).join('[REDACTED]');
|
|
114
|
+
}
|
|
115
|
+
out = out.replace(/gva_sk_[A-Za-z0-9_-]+/g, 'gva_sk_[REDACTED]');
|
|
116
|
+
out = out.replace(/Bearer\s+[A-Za-z0-9._-]+/g, 'Bearer [REDACTED]');
|
|
117
|
+
return out;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export function normalizeMcpUrl(url) {
|
|
121
|
+
const trimmed = String(url || '').trim();
|
|
122
|
+
if (!trimmed) return '';
|
|
123
|
+
return trimmed.endsWith('/mcp') ? trimmed : `${trimmed.replace(/\/+$/, '')}/mcp`;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function looksLikeSse(text) {
|
|
127
|
+
return /^(:|event:|id:|data:)/m.test(text);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function unwrapToolResult(result) {
|
|
131
|
+
if (!result || typeof result !== 'object') return result;
|
|
132
|
+
if (!Array.isArray(result.content)) return result;
|
|
133
|
+
|
|
134
|
+
const textPart = result.content.find((item) => item?.type === 'text' && typeof item.text === 'string');
|
|
135
|
+
if (!textPart) return result;
|
|
136
|
+
try {
|
|
137
|
+
return JSON.parse(textPart.text);
|
|
138
|
+
} catch {
|
|
139
|
+
return { success: true, data: textPart.text, error: null };
|
|
140
|
+
}
|
|
141
|
+
}
|
package/src/tools.js
ADDED
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
const serviceAuth = 'service-api-key';
|
|
2
|
+
|
|
3
|
+
export const TOOL_SPECS = [
|
|
4
|
+
tool('get_user_info', '获取当前 Service API Key 绑定用户的账户、会员和兼容信息。'),
|
|
5
|
+
tool('upload_material', '上传图片素材到 OSS 并注册到当前用户成品库。', [
|
|
6
|
+
arg('file_name', 'string', true),
|
|
7
|
+
arg('mime_type', 'string', true),
|
|
8
|
+
arg('content_base64', 'string', true),
|
|
9
|
+
arg('title'),
|
|
10
|
+
arg('category'),
|
|
11
|
+
arg('tags'),
|
|
12
|
+
]),
|
|
13
|
+
tool('create_image', '创建图片生成或改图任务。', [
|
|
14
|
+
arg('prompt'),
|
|
15
|
+
arg('image_urls', 'string[]'),
|
|
16
|
+
arg('image_generation_type'),
|
|
17
|
+
arg('model'),
|
|
18
|
+
arg('aspect_ratio'),
|
|
19
|
+
arg('image_size'),
|
|
20
|
+
arg('title'),
|
|
21
|
+
arg('task'),
|
|
22
|
+
arg('is_sync', 'boolean'),
|
|
23
|
+
arg('providers', 'string[]'),
|
|
24
|
+
arg('transparent_background', 'boolean'),
|
|
25
|
+
arg('transparent_key_color'),
|
|
26
|
+
]),
|
|
27
|
+
videoTool('create_seedance_video', '创建 Seedance 全能参考视频任务。'),
|
|
28
|
+
videoTool('create_happyhorse_video', '创建 HappyHorse 全能参考视频任务。'),
|
|
29
|
+
tool('create_digital_human', '使用固定模板 101 创建单图数字人视频任务。', [
|
|
30
|
+
arg('image_url', 'string', true),
|
|
31
|
+
arg('audio_url', 'string', true),
|
|
32
|
+
arg('resolution'),
|
|
33
|
+
arg('start_time'),
|
|
34
|
+
arg('end_time'),
|
|
35
|
+
arg('prompt'),
|
|
36
|
+
arg('title'),
|
|
37
|
+
]),
|
|
38
|
+
tool('get_task', '统一查询图片、视频、数字人任务状态和结果 URL。', [
|
|
39
|
+
arg('task_id', 'string', true),
|
|
40
|
+
arg('task_type', 'string', true),
|
|
41
|
+
]),
|
|
42
|
+
tool('get_public_voice', '查询可直接用于语音合成的公共音色列表。'),
|
|
43
|
+
tool('get_custom_voice', '查询当前用户已克隆或定制的音色列表。'),
|
|
44
|
+
tool('clone_voice', '根据公网音频 URL 克隆当前用户的定制音色。', [
|
|
45
|
+
arg('audio_url', 'string', true),
|
|
46
|
+
arg('prefix'),
|
|
47
|
+
arg('language_hints', 'string[]'),
|
|
48
|
+
]),
|
|
49
|
+
tool('create_audio_task', '把文本合成为音频,返回 voice_url。', [
|
|
50
|
+
arg('content', 'string', true),
|
|
51
|
+
arg('speaker_id'),
|
|
52
|
+
arg('voice_id'),
|
|
53
|
+
arg('group_key'),
|
|
54
|
+
arg('model'),
|
|
55
|
+
arg('rate', 'number'),
|
|
56
|
+
arg('volume', 'number'),
|
|
57
|
+
arg('audio_format'),
|
|
58
|
+
arg('sample_rate', 'number'),
|
|
59
|
+
]),
|
|
60
|
+
douhotSearchTool('douhot_search', '抖音热点宝搜索完整降级链路。'),
|
|
61
|
+
douhotSearchTool('douhot_search_video_billboard', '抖音热点宝视频榜搜索。'),
|
|
62
|
+
douhotSearchTool('douhot_search_hot_word', '抖音热点宝热词详情搜索。'),
|
|
63
|
+
douhotSearchTool('douhot_search_challenge_analysis', '抖音热点宝挑战分析搜索。'),
|
|
64
|
+
douhotSearchTool('douhot_search_hot_search', '抖音热点宝热搜榜搜索。'),
|
|
65
|
+
douhotSearchTool('douhot_search_author', '抖音热点宝作者搜索。'),
|
|
66
|
+
douhotCreatorTool('douhot_creator_info', '抖音热点宝创作者信息。', 'info'),
|
|
67
|
+
douhotCreatorTool('douhot_creator_contents', '抖音热点宝创作者作品列表。', 'contents'),
|
|
68
|
+
douhotCreatorTool('douhot_creator_fans_trend_index', '抖音热点宝创作者粉丝趋势概览。', 'info'),
|
|
69
|
+
douhotCreatorTool('douhot_creator_fans_trend_trends', '抖音热点宝创作者粉丝趋势明细。', 'trend'),
|
|
70
|
+
douhotVideoTool('douhot_video_detail', '抖音热点宝视频详情。', 'base'),
|
|
71
|
+
douhotVideoTool('douhot_video_comments', '抖音热点宝视频评论列表。', 'comments'),
|
|
72
|
+
douhotVideoTool('douhot_video_comment_word_cloud', '抖音热点宝视频评论词云。', 'base'),
|
|
73
|
+
douhotVideoTool('douhot_video_trends_index', '抖音热点宝视频趋势概览。', 'base'),
|
|
74
|
+
douhotVideoTool('douhot_video_trends', '抖音热点宝视频趋势明细。', 'trend'),
|
|
75
|
+
];
|
|
76
|
+
|
|
77
|
+
export const friendlyCommandGroups = {
|
|
78
|
+
user: ['get_user_info'],
|
|
79
|
+
media: ['upload_material', 'create_image', 'create_seedance_video', 'create_happyhorse_video', 'create_digital_human'],
|
|
80
|
+
task: ['get_task'],
|
|
81
|
+
voice: ['get_public_voice', 'get_custom_voice', 'clone_voice', 'create_audio_task'],
|
|
82
|
+
douhot: TOOL_SPECS.filter((toolSpec) => toolSpec.name.startsWith('douhot_')).map((toolSpec) => toolSpec.name),
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
export const DOUHOT_COMMANDS = {
|
|
86
|
+
search: 'douhot_search',
|
|
87
|
+
'video-billboard': 'douhot_search_video_billboard',
|
|
88
|
+
'hot-word': 'douhot_search_hot_word',
|
|
89
|
+
'challenge-analysis': 'douhot_search_challenge_analysis',
|
|
90
|
+
'hot-search': 'douhot_search_hot_search',
|
|
91
|
+
author: 'douhot_search_author',
|
|
92
|
+
'creator-info': 'douhot_creator_info',
|
|
93
|
+
'creator-contents': 'douhot_creator_contents',
|
|
94
|
+
'creator-fans-trend-index': 'douhot_creator_fans_trend_index',
|
|
95
|
+
'creator-fans-trend-trends': 'douhot_creator_fans_trend_trends',
|
|
96
|
+
'video-detail': 'douhot_video_detail',
|
|
97
|
+
'video-comments': 'douhot_video_comments',
|
|
98
|
+
'video-comment-word-cloud': 'douhot_video_comment_word_cloud',
|
|
99
|
+
'video-trends-index': 'douhot_video_trends_index',
|
|
100
|
+
'video-trends': 'douhot_video_trends',
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
export function getToolSpec(name) {
|
|
104
|
+
const spec = TOOL_SPECS.find((toolSpec) => toolSpec.name === name);
|
|
105
|
+
if (!spec) throw new Error(`Unknown MCP tool: ${name}`);
|
|
106
|
+
return spec;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function tool(name, description, args = []) {
|
|
110
|
+
return {
|
|
111
|
+
name,
|
|
112
|
+
description,
|
|
113
|
+
auth: serviceAuth,
|
|
114
|
+
arguments: args,
|
|
115
|
+
required: args.filter((item) => item.required).map((item) => item.name),
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function arg(name, type = 'string', required = false) {
|
|
120
|
+
return { name, type, required };
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function videoTool(name, description) {
|
|
124
|
+
return tool(name, description, [
|
|
125
|
+
arg('prompt', 'string', true),
|
|
126
|
+
arg('images', 'string[]'),
|
|
127
|
+
arg('videos', 'string[]'),
|
|
128
|
+
arg('audios', 'string[]'),
|
|
129
|
+
arg('virtual_portrait_uri'),
|
|
130
|
+
arg('virtual_portrait_uris', 'string[]'),
|
|
131
|
+
arg('aspect_ratio'),
|
|
132
|
+
arg('quality'),
|
|
133
|
+
arg('orientation'),
|
|
134
|
+
arg('duration', 'number'),
|
|
135
|
+
arg('seed'),
|
|
136
|
+
arg('auto_translate', 'boolean'),
|
|
137
|
+
arg('title'),
|
|
138
|
+
]);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function douhotCommonArgs(args) {
|
|
142
|
+
return [
|
|
143
|
+
...args,
|
|
144
|
+
arg('return_raw', 'boolean'),
|
|
145
|
+
arg('cache', 'boolean'),
|
|
146
|
+
arg('cookie'),
|
|
147
|
+
];
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function douhotSearchTool(name, description) {
|
|
151
|
+
return tool(name, description, douhotCommonArgs([
|
|
152
|
+
arg('keyword'),
|
|
153
|
+
arg('page', 'number'),
|
|
154
|
+
arg('page_size', 'number'),
|
|
155
|
+
arg('type'),
|
|
156
|
+
]));
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function douhotCreatorTool(name, description, kind) {
|
|
160
|
+
const args = [arg('sec_uid', 'string', true)];
|
|
161
|
+
if (kind === 'contents') args.push(arg('min_cursor'), arg('max_cursor'));
|
|
162
|
+
if (kind === 'trend') args.push(arg('option', 'number'), arg('date_window', 'number'));
|
|
163
|
+
return tool(name, description, douhotCommonArgs(args));
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function douhotVideoTool(name, description, kind) {
|
|
167
|
+
const args = [arg('item_id', 'string', true)];
|
|
168
|
+
if (kind === 'comments') args.push(arg('page', 'number'), arg('page_size', 'number'), arg('keyword'));
|
|
169
|
+
if (kind === 'trend') args.push(arg('option', 'number'), arg('date_window', 'number'));
|
|
170
|
+
return tool(name, description, douhotCommonArgs(args));
|
|
171
|
+
}
|
|
172
|
+
|