@lingjingai/scriptctl 0.63.1 → 0.63.3
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 +1 -1
- package/changes/0.63.2.md +4 -0
- package/changes/0.63.3.md +5 -0
- package/dist/bin.js +93 -153
- package/dist/bin.js.map +3 -3
- package/dist/llm/router.d.ts +3 -5
- package/dist/llm/router.js +79 -171
- package/dist/llm/router.js.map +1 -1
- package/dist/review-agent.js +1 -1
- package/dist/review-agent.js.map +2 -2
- package/package.json +1 -1
- package/skills/scriptctl/SKILL.md +6 -2
- package/skills/scriptctl/references/ingest-workflow.md +4 -3
package/README.md
CHANGED
|
@@ -136,7 +136,7 @@ MIT
|
|
|
136
136
|
|
|
137
137
|
### 指定视频理解服务
|
|
138
138
|
|
|
139
|
-
|
|
139
|
+
Gemini 文本、结构化工具和视频请求在当前路由重试耗尽或被审核拦截后,由已配置的豆包兜底。视频的默认路由与显式指定的 Gemini 均保留所选主模型;视频上传失败时豆包读取同一份本地文件。用户取消不触发兜底。豆包使用 `ARK_API_KEY`,`ARK_MODEL` 可指定已开通的视频理解端点,默认 `ep-20260721150136-98dj9`。直接选择豆包时只调用豆包:
|
|
140
140
|
|
|
141
141
|
```bash
|
|
142
142
|
# 豆包:默认读取 ARK_API_KEY;模型也可通过 ARK_MODEL 设置
|
package/dist/bin.js
CHANGED
|
@@ -7794,10 +7794,6 @@ var OpenAiCompatibleModel = class {
|
|
|
7794
7794
|
};
|
|
7795
7795
|
|
|
7796
7796
|
// src/llm/router.ts
|
|
7797
|
-
var VIDEO_FALLBACK_MODELS = ["gemini-3.7-flash"];
|
|
7798
|
-
var PROXY_GEMINI_MODELS = [];
|
|
7799
|
-
var PROXY_GEMINI_API_KEY_ENVS = ["GEMINI_PROXY_API_KEY", "REDRAW_GEMINI_PROXY_API_KEY"];
|
|
7800
|
-
var PROXY_GEMINI_BASE_URL_ENVS = ["GEMINI_PROXY_BASE_URL", "REDRAW_GEMINI_PROXY_BASE_URL"];
|
|
7801
7797
|
var DOUBAO_FALLBACK = {
|
|
7802
7798
|
model: "ep-20260721150136-98dj9",
|
|
7803
7799
|
modelEnv: "ARK_MODEL",
|
|
@@ -7838,24 +7834,30 @@ function credentialError(provider) {
|
|
|
7838
7834
|
});
|
|
7839
7835
|
}
|
|
7840
7836
|
function routedFailure(error, route) {
|
|
7841
|
-
|
|
7842
|
-
|
|
7843
|
-
|
|
7844
|
-
|
|
7845
|
-
|
|
7846
|
-
|
|
7847
|
-
|
|
7848
|
-
|
|
7849
|
-
|
|
7850
|
-
|
|
7851
|
-
|
|
7852
|
-
|
|
7853
|
-
|
|
7854
|
-
|
|
7855
|
-
|
|
7856
|
-
|
|
7857
|
-
|
|
7858
|
-
);
|
|
7837
|
+
return createOperationalFailure(classificationOf(error.failure), {
|
|
7838
|
+
...contextOf(error.failure),
|
|
7839
|
+
provider: route.provider,
|
|
7840
|
+
model: route.model,
|
|
7841
|
+
retryable: error.failure.retryable,
|
|
7842
|
+
message: error.message
|
|
7843
|
+
});
|
|
7844
|
+
}
|
|
7845
|
+
function fallbackFailure(primary, error, route) {
|
|
7846
|
+
const failures = [primary, routedFailure(error, route)];
|
|
7847
|
+
const aggregate = aggregateFailures(failures, "Primary model and Doubao fallback both failed.");
|
|
7848
|
+
return new LlmTransportError(error.code, aggregate.message, {
|
|
7849
|
+
retryable: aggregate.retryable,
|
|
7850
|
+
classification: classificationOf(aggregate),
|
|
7851
|
+
context: contextOf(aggregate),
|
|
7852
|
+
modelFailures: failures,
|
|
7853
|
+
details: failures.map(formatFailure)
|
|
7854
|
+
});
|
|
7855
|
+
}
|
|
7856
|
+
function isGeminiRoute(route) {
|
|
7857
|
+
return route.protocol === "google-gemini" || /(^|\/)gemini-/i.test(route.model);
|
|
7858
|
+
}
|
|
7859
|
+
function canFallback(error, signal) {
|
|
7860
|
+
return !signal?.aborted && error instanceof LlmTransportError && error.code !== "cancelled";
|
|
7859
7861
|
}
|
|
7860
7862
|
var ModelRouter = class {
|
|
7861
7863
|
constructor(config, options = {}) {
|
|
@@ -8027,52 +8029,6 @@ var ModelRouter = class {
|
|
|
8027
8029
|
throw error;
|
|
8028
8030
|
}
|
|
8029
8031
|
}
|
|
8030
|
-
videoRoute(route, model) {
|
|
8031
|
-
const modelConfig = model === route.info.model ? route.modelConfig : { ...route.modelConfig, model };
|
|
8032
|
-
return {
|
|
8033
|
-
...route.info,
|
|
8034
|
-
model,
|
|
8035
|
-
fingerprint: routeFingerprint(route.info.task, route.providerConfig, modelConfig)
|
|
8036
|
-
};
|
|
8037
|
-
}
|
|
8038
|
-
resolvedVideoRoute(route, model) {
|
|
8039
|
-
const modelConfig = model === route.info.model ? route.modelConfig : { ...route.modelConfig, model };
|
|
8040
|
-
return {
|
|
8041
|
-
info: this.videoRoute(route, model),
|
|
8042
|
-
providerConfig: route.providerConfig,
|
|
8043
|
-
modelConfig
|
|
8044
|
-
};
|
|
8045
|
-
}
|
|
8046
|
-
proxyVideoRoutes(task) {
|
|
8047
|
-
if (this.options.videoProvider) return [];
|
|
8048
|
-
const apiKeyEnv = PROXY_GEMINI_API_KEY_ENVS.find((name) => (process.env[name] ?? "").trim());
|
|
8049
|
-
const baseUrlEnv = PROXY_GEMINI_BASE_URL_ENVS.find((name) => (process.env[name] ?? "").trim());
|
|
8050
|
-
if (!apiKeyEnv || !baseUrlEnv) return [];
|
|
8051
|
-
const baseUrl = normalizeProviderBaseUrl(process.env[baseUrlEnv], baseUrlEnv);
|
|
8052
|
-
return PROXY_GEMINI_MODELS.map((model, index) => {
|
|
8053
|
-
const providerConfig = {
|
|
8054
|
-
protocol: "google-gemini",
|
|
8055
|
-
baseUrl,
|
|
8056
|
-
apiKeyEnv,
|
|
8057
|
-
timeoutMs: 9e4,
|
|
8058
|
-
maxAttempts: index < 2 ? 2 : 1
|
|
8059
|
-
};
|
|
8060
|
-
const modelConfig = { provider: "gemini-proxy", model };
|
|
8061
|
-
return {
|
|
8062
|
-
info: {
|
|
8063
|
-
task,
|
|
8064
|
-
profile: "gemini-proxy",
|
|
8065
|
-
provider: "gemini-proxy",
|
|
8066
|
-
protocol: "google-gemini",
|
|
8067
|
-
baseUrl,
|
|
8068
|
-
model,
|
|
8069
|
-
fingerprint: routeFingerprint(task, providerConfig, modelConfig)
|
|
8070
|
-
},
|
|
8071
|
-
providerConfig,
|
|
8072
|
-
modelConfig
|
|
8073
|
-
};
|
|
8074
|
-
});
|
|
8075
|
-
}
|
|
8076
8032
|
resolveDoubao(task, explicit) {
|
|
8077
8033
|
const overrides = explicit ? this.options.videoProvider : void 0;
|
|
8078
8034
|
const modelName = explicit ? (overrides?.model ?? process.env["ARK_MODEL"] ?? "").trim() : this.doubaoModel();
|
|
@@ -8114,9 +8070,7 @@ var ModelRouter = class {
|
|
|
8114
8070
|
this.adapters.set(key, adapter);
|
|
8115
8071
|
return adapter;
|
|
8116
8072
|
}
|
|
8117
|
-
|
|
8118
|
-
// that shares the provider/credential but targets a fallback model.
|
|
8119
|
-
videoAdapter(route, model) {
|
|
8073
|
+
videoAdapter(route) {
|
|
8120
8074
|
if (route.providerConfig.protocol !== "google-gemini") {
|
|
8121
8075
|
throw configRuntimeError(
|
|
8122
8076
|
"Video tasks require a google-gemini provider.",
|
|
@@ -8124,14 +8078,13 @@ var ModelRouter = class {
|
|
|
8124
8078
|
[`provider: ${route.info.provider}`, `protocol: ${route.providerConfig.protocol}`]
|
|
8125
8079
|
);
|
|
8126
8080
|
}
|
|
8127
|
-
const modelName =
|
|
8081
|
+
const modelName = route.info.model;
|
|
8128
8082
|
const key = `${route.info.provider} ${route.providerConfig.baseUrl ?? "official"} ${modelName}`;
|
|
8129
8083
|
const cached2 = this.videoAdapters.get(key);
|
|
8130
8084
|
if (cached2) return cached2;
|
|
8131
8085
|
const apiKey = this.videoCredential(route);
|
|
8132
8086
|
if (!apiKey) throw credentialError(route.info.provider);
|
|
8133
|
-
const
|
|
8134
|
-
const adapter = new GeminiVideoModel(route.providerConfig, modelConfig, apiKey);
|
|
8087
|
+
const adapter = new GeminiVideoModel(route.providerConfig, route.modelConfig, apiKey);
|
|
8135
8088
|
this.videoAdapters.set(key, adapter);
|
|
8136
8089
|
return adapter;
|
|
8137
8090
|
}
|
|
@@ -8146,8 +8099,8 @@ var ModelRouter = class {
|
|
|
8146
8099
|
if (spec2.mode !== "video") throw configRuntimeError("The task does not support video generation.", ["video task"], [`task: ${task}`]);
|
|
8147
8100
|
const route = this.resolve(task);
|
|
8148
8101
|
if (this.options.videoProvider?.provider === "doubao") {
|
|
8149
|
-
const
|
|
8150
|
-
if (!
|
|
8102
|
+
const adapter2 = this.doubaoAdapter(route);
|
|
8103
|
+
if (!adapter2) throw credentialError("doubao");
|
|
8151
8104
|
return {
|
|
8152
8105
|
prepare: async (localPath, mimeType, mode, signal) => {
|
|
8153
8106
|
if (mode !== "file-api") {
|
|
@@ -8162,7 +8115,7 @@ var ModelRouter = class {
|
|
|
8162
8115
|
}
|
|
8163
8116
|
request2.signal?.throwIfAborted();
|
|
8164
8117
|
const { path: localPath, mimeType: localMime } = request2.media;
|
|
8165
|
-
return this.tracked(route.info, () =>
|
|
8118
|
+
return this.tracked(route.info, () => adapter2.run({
|
|
8166
8119
|
localPath,
|
|
8167
8120
|
localMime,
|
|
8168
8121
|
user: request2.user,
|
|
@@ -8184,88 +8137,68 @@ var ModelRouter = class {
|
|
|
8184
8137
|
[`provider: ${route.info.provider}`, `protocol: ${route.providerConfig.protocol}`]
|
|
8185
8138
|
);
|
|
8186
8139
|
}
|
|
8187
|
-
const
|
|
8188
|
-
const proxyRoutes = this.proxyVideoRoutes(task);
|
|
8189
|
-
if (this.options.videoProvider || proxyRoutes.length === 0) void this.videoAdapter(route);
|
|
8140
|
+
const adapter = this.videoAdapter(route);
|
|
8190
8141
|
return {
|
|
8191
8142
|
prepare: async (localPath, mimeType, mode, signal) => {
|
|
8192
|
-
const
|
|
8143
|
+
const started = Date.now();
|
|
8193
8144
|
try {
|
|
8194
|
-
return await
|
|
8145
|
+
return await adapter.prepare(localPath, mimeType, mode, signal);
|
|
8195
8146
|
} catch (error) {
|
|
8196
|
-
if (signal
|
|
8197
|
-
|
|
8198
|
-
|
|
8199
|
-
|
|
8200
|
-
|
|
8147
|
+
if (!canFallback(error, signal) || !this.doubaoAdapter()) throw error;
|
|
8148
|
+
this.recordEvent(
|
|
8149
|
+
route.info,
|
|
8150
|
+
"failed",
|
|
8151
|
+
new Date(started).toISOString(),
|
|
8152
|
+
(/* @__PURE__ */ new Date()).toISOString(),
|
|
8153
|
+
Date.now() - started,
|
|
8154
|
+
error.usage,
|
|
8155
|
+
error.attempts,
|
|
8156
|
+
error.code
|
|
8157
|
+
);
|
|
8158
|
+
return { kind: "local-fallback", path: localPath, mimeType, primaryFailure: routedFailure(error, route.info) };
|
|
8201
8159
|
}
|
|
8202
8160
|
},
|
|
8203
|
-
release: (media, signal) => media.kind === "
|
|
8161
|
+
release: (media, signal) => media.kind === "local-fallback" ? Promise.resolve() : adapter.release(media, signal),
|
|
8204
8162
|
generate: async (request2) => {
|
|
8205
|
-
|
|
8206
|
-
|
|
8207
|
-
|
|
8208
|
-
for (const [index, attemptRoute] of geminiChain.entries()) {
|
|
8209
|
-
try {
|
|
8210
|
-
const response = await this.tracked(
|
|
8211
|
-
attemptRoute.info,
|
|
8212
|
-
() => this.videoAdapter(attemptRoute).generate({ ...request2, maxOutputTokens: spec2.maxOutputTokens })
|
|
8213
|
-
);
|
|
8214
|
-
if (index > 0) process.stderr.write(`\u21AA\uFE0F \u5DF2\u5207\u6362\u5230\u5907\u7528\u6A21\u578B ${attemptRoute.info.model}
|
|
8215
|
-
`);
|
|
8216
|
-
return response;
|
|
8217
|
-
} catch (error) {
|
|
8218
|
-
if (request2.signal?.aborted) throw error;
|
|
8219
|
-
const failure = routedFailure(error, attemptRoute.info);
|
|
8220
|
-
failures.push(failure);
|
|
8221
|
-
const detail = formatFailure(failure);
|
|
8222
|
-
if (index < geminiChain.length - 1) {
|
|
8223
|
-
process.stderr.write(`\u21AA\uFE0F ${attemptRoute.info.model} \u5F53\u524D\u4E0D\u53EF\u7528\uFF0C\u5207\u6362\u4E0B\u4E00\u7EBF\u8DEF (${detail})
|
|
8224
|
-
`);
|
|
8225
|
-
}
|
|
8226
|
-
}
|
|
8163
|
+
request2.signal?.throwIfAborted();
|
|
8164
|
+
if (request2.media.kind === "local-fallback") {
|
|
8165
|
+
return this.generateDoubaoVideo(task, request2, request2.media.primaryFailure);
|
|
8227
8166
|
}
|
|
8228
|
-
|
|
8229
|
-
|
|
8230
|
-
|
|
8231
|
-
|
|
8232
|
-
|
|
8233
|
-
process.stderr.write(`\u21AA\uFE0F Gemini \u5168\u90E8\u5931\u8D25,\u56DE\u9000\u5230\u8C46\u5305 ${this.doubaoModel()}
|
|
8234
|
-
`);
|
|
8235
|
-
const response = await this.tracked(attemptRoute, () => doubao.run({
|
|
8236
|
-
localPath,
|
|
8237
|
-
localMime: request2.media.kind === "local-fallback" ? request2.media.mimeType : request2.localMime ?? "video/mp4",
|
|
8238
|
-
user: request2.user,
|
|
8239
|
-
fps: request2.fps,
|
|
8240
|
-
maxOutputTokens: spec2.maxOutputTokens,
|
|
8241
|
-
...request2.system ? { system: request2.system } : {},
|
|
8242
|
-
...request2.responseJsonSchema ? { responseJsonSchema: request2.responseJsonSchema } : {},
|
|
8243
|
-
...request2.signal ? { signal: request2.signal } : {}
|
|
8244
|
-
}));
|
|
8245
|
-
return response;
|
|
8246
|
-
} catch (error) {
|
|
8247
|
-
if (request2.signal?.aborted) throw error;
|
|
8248
|
-
failures.push(routedFailure(error, attemptRoute));
|
|
8249
|
-
}
|
|
8250
|
-
} else if (!doubao) {
|
|
8251
|
-
if (!this.options.videoProvider) {
|
|
8252
|
-
skipped.push("doubao: skipped (ARK_API_KEY not set)");
|
|
8253
|
-
}
|
|
8254
|
-
} else {
|
|
8255
|
-
skipped.push("doubao: skipped (no local file to re-upload)");
|
|
8167
|
+
try {
|
|
8168
|
+
return await this.tracked(route.info, () => adapter.generate({ ...request2, maxOutputTokens: spec2.maxOutputTokens }));
|
|
8169
|
+
} catch (error) {
|
|
8170
|
+
if (!canFallback(error, request2.signal) || !this.doubaoAdapter() || !request2.localPath) throw error;
|
|
8171
|
+
return this.generateDoubaoVideo(task, request2, routedFailure(error, route.info));
|
|
8256
8172
|
}
|
|
8257
|
-
const aggregate = aggregateFailures(failures, "All configured video model attempts failed.");
|
|
8258
|
-
throw new LlmTransportError("request_failed", aggregate.message, {
|
|
8259
|
-
retryable: aggregate.retryable,
|
|
8260
|
-
classification: classificationOf(aggregate),
|
|
8261
|
-
context: contextOf(aggregate),
|
|
8262
|
-
modelFailures: failures,
|
|
8263
|
-
details: [...failures.map(formatFailure), ...skipped]
|
|
8264
|
-
});
|
|
8265
8173
|
}
|
|
8266
8174
|
};
|
|
8267
8175
|
}
|
|
8268
|
-
|
|
8176
|
+
async generateDoubaoVideo(task, request2, primary) {
|
|
8177
|
+
const adapter = this.doubaoAdapter();
|
|
8178
|
+
if (!adapter) throw credentialError("doubao");
|
|
8179
|
+
const localPath = request2.media.kind === "local-fallback" ? request2.media.path : request2.localPath;
|
|
8180
|
+
if (!localPath) throw configRuntimeError("Doubao video fallback requires the local video.", ["localPath"], ["localPath: missing"]);
|
|
8181
|
+
const localMime = request2.localMime ?? (request2.media.kind === "file-api" ? request2.media.file.mimeType : request2.media.mimeType);
|
|
8182
|
+
const route = this.doubaoRoute(task);
|
|
8183
|
+
process.stderr.write(`\u21AA\uFE0F ${primary.model} \u8BF7\u6C42\u5931\u8D25\uFF0C\u56DE\u9000\u5230\u8C46\u5305 ${route.model} (${formatFailure(primary)})
|
|
8184
|
+
`);
|
|
8185
|
+
try {
|
|
8186
|
+
return await this.tracked(route, () => adapter.run({
|
|
8187
|
+
localPath,
|
|
8188
|
+
localMime,
|
|
8189
|
+
user: request2.user,
|
|
8190
|
+
fps: request2.fps,
|
|
8191
|
+
maxOutputTokens: LLM_TASK_REGISTRY[task].maxOutputTokens,
|
|
8192
|
+
...request2.system ? { system: request2.system } : {},
|
|
8193
|
+
...request2.responseJsonSchema ? { responseJsonSchema: request2.responseJsonSchema } : {},
|
|
8194
|
+
...request2.signal ? { signal: request2.signal } : {}
|
|
8195
|
+
}));
|
|
8196
|
+
} catch (error) {
|
|
8197
|
+
if (!canFallback(error, request2.signal)) throw error;
|
|
8198
|
+
throw fallbackFailure(primary, error, route);
|
|
8199
|
+
}
|
|
8200
|
+
}
|
|
8201
|
+
// Resolve the default fallback or explicitly selected Doubao video.
|
|
8269
8202
|
// Overridable in tests to inject a fake Ark client.
|
|
8270
8203
|
doubaoAdapter(route) {
|
|
8271
8204
|
return this.arkAdapter(route);
|
|
@@ -8289,29 +8222,36 @@ var ModelRouter = class {
|
|
|
8289
8222
|
doubaoModel() {
|
|
8290
8223
|
return (process.env[DOUBAO_FALLBACK.modelEnv] ?? "").trim() || DOUBAO_FALLBACK.model;
|
|
8291
8224
|
}
|
|
8292
|
-
async
|
|
8225
|
+
async withTextFallback(task, signal, generate) {
|
|
8293
8226
|
const route = this.resolve(task);
|
|
8294
8227
|
const adapter = this.adapter(route);
|
|
8295
8228
|
try {
|
|
8296
8229
|
return await this.tracked(route.info, () => generate(adapter));
|
|
8297
8230
|
} catch (error) {
|
|
8298
|
-
if (signal
|
|
8231
|
+
if (!canFallback(error, signal) || !isGeminiRoute(route.info) && error.code !== "content_filtered") throw error;
|
|
8299
8232
|
const doubao = this.doubaoTextAdapter();
|
|
8300
8233
|
if (!doubao) throw error;
|
|
8301
|
-
|
|
8234
|
+
const primary = routedFailure(error, route.info);
|
|
8235
|
+
const fallback = this.doubaoRoute(task);
|
|
8236
|
+
process.stderr.write(`\u21AA\uFE0F ${route.info.model} \u8BF7\u6C42\u5931\u8D25\uFF0C\u56DE\u9000\u5230\u8C46\u5305 ${fallback.model} (${formatFailure(primary)})
|
|
8302
8237
|
`);
|
|
8303
|
-
|
|
8238
|
+
try {
|
|
8239
|
+
return await this.tracked(fallback, () => generate(doubao));
|
|
8240
|
+
} catch (fallbackError) {
|
|
8241
|
+
if (!canFallback(fallbackError, signal)) throw fallbackError;
|
|
8242
|
+
throw fallbackFailure(primary, fallbackError, fallback);
|
|
8243
|
+
}
|
|
8304
8244
|
}
|
|
8305
8245
|
}
|
|
8306
8246
|
async generateText(task, request2) {
|
|
8307
8247
|
const spec2 = LLM_TASK_REGISTRY[task];
|
|
8308
8248
|
if (spec2.mode !== "text") throw configRuntimeError("The task does not support text generation.", ["text task"], [`task: ${task}`]);
|
|
8309
|
-
return this.
|
|
8249
|
+
return this.withTextFallback(task, request2.signal, (adapter) => adapter.generateText({ ...request2, maxOutputTokens: spec2.maxOutputTokens }));
|
|
8310
8250
|
}
|
|
8311
8251
|
async generateTool(task, request2) {
|
|
8312
8252
|
const spec2 = LLM_TASK_REGISTRY[task];
|
|
8313
8253
|
if (spec2.mode !== "tool") throw configRuntimeError("The task does not support tool generation.", ["tool task"], [`task: ${task}`]);
|
|
8314
|
-
return this.
|
|
8254
|
+
return this.withTextFallback(task, request2.signal, (adapter) => adapter.generateTool({ ...request2, maxOutputTokens: spec2.maxOutputTokens }));
|
|
8315
8255
|
}
|
|
8316
8256
|
};
|
|
8317
8257
|
function createLlmExecutor(explicitConfigPath, options) {
|