hiwork-core 0.0.30 → 0.0.35
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/cordis.patch.yml +15 -1
- package/lib/ai-router-client.js +394 -2
- package/lib/client-policy.js +2 -2
- package/lib/client.js +2413 -439
- package/lib/image-rpc.js +306 -0
- package/lib/imagegen.js +424 -0
- package/lib/index.js +19212 -7694
- package/lib/knowledge-config.js +91 -1
- package/lib/provision-session.js +10 -2
- package/lib/tool-names.js +10 -0
- package/lib/tools.js +205 -0
- package/lib/types/ai-router-client.d.ts +255 -2
- package/lib/types/client/about-section.d.ts +27 -7
- package/lib/types/client/controller.d.ts +17 -9
- package/lib/types/client/feature-center.d.ts +46 -1
- package/lib/types/client/feature-registry.d.ts +36 -3
- package/lib/types/client/image-cards.d.ts +218 -0
- package/lib/types/client/image-styles.d.ts +9 -0
- package/lib/types/client/images-data.d.ts +127 -0
- package/lib/types/client/images-page.d.ts +97 -0
- package/lib/types/client/index.d.ts +0 -1
- package/lib/types/client/login-gate.d.ts +2 -2
- package/lib/types/client/logo.d.ts +7 -2
- package/lib/types/client/menu-preferences.d.ts +163 -0
- package/lib/types/client/menu-section.d.ts +5 -0
- package/lib/types/client/menu-store.d.ts +101 -0
- package/lib/types/client/styles.d.ts +16 -0
- package/lib/types/client/types.d.ts +9 -0
- package/lib/types/image-rpc.d.ts +142 -0
- package/lib/types/imagegen.d.ts +241 -0
- package/lib/types/index.d.ts +70 -8
- package/lib/types/knowledge-config.d.ts +50 -2
- package/lib/types/tool-names.d.ts +11 -0
- package/lib/types/tools.d.ts +80 -0
- package/lib/types/user-agent.d.ts +31 -0
- package/lib/user-agent.js +54 -0
- package/lib/web-search.js +2 -2
- package/package.json +34 -24
package/lib/knowledge-config.js
CHANGED
|
@@ -50,8 +50,18 @@ import { PKG_VERSION } from "./version.js";
|
|
|
50
50
|
export const HIWORK_KNOWLEDGE_SERVICE = 'hiworkKnowledge';
|
|
51
51
|
/** 网关侧端点(跨仓冻结;与 `/api/me/memory-config` 同一条会话鉴权)。 */
|
|
52
52
|
export const ME_KNOWLEDGE_CONFIG_PATH = '/api/me/knowledge-config';
|
|
53
|
-
/** 成功结果的缓存 TTL:5
|
|
53
|
+
/** 成功结果的缓存 TTL:5 分钟(与网关签发的令牌寿命同量级)。 */
|
|
54
54
|
export const KNOWLEDGE_CONFIG_TTL_MS = 5 * 60 * 1000;
|
|
55
|
+
/**
|
|
56
|
+
* **令牌**缓存的提前量:令牌寿命与缓存 TTL 都是 5 分钟时,缓存到期时刻恒晚于令牌 `exp`
|
|
57
|
+
* (响应到达 = 签发 + 一次往返),那段时间里命中的令牌**必然已过期** → 上游 401 → 插件
|
|
58
|
+
* 强制刷新一次再重试。留 30 秒余量把这条无谓的 401 往返消掉。
|
|
59
|
+
*
|
|
60
|
+
* (配置缓存的 TTL 不需要余量:`base_url`/`api_key` 没有自带寿命。)
|
|
61
|
+
*/
|
|
62
|
+
export const KNOWLEDGE_TOKEN_CACHE_MARGIN_MS = 30_000;
|
|
63
|
+
/** 令牌缓存的 TTL:比令牌寿命短一点(见上面的理由)。 */
|
|
64
|
+
export const KNOWLEDGE_TOKEN_CACHE_TTL_MS = KNOWLEDGE_CONFIG_TTL_MS - KNOWLEDGE_TOKEN_CACHE_MARGIN_MS;
|
|
55
65
|
/** 出网超时(与 `AiRouterClient` 的缺省一致)。 */
|
|
56
66
|
export const KNOWLEDGE_CONFIG_TIMEOUT_MS = 10_000;
|
|
57
67
|
/**
|
|
@@ -113,8 +123,34 @@ function projectKnowledgeConfig(body) {
|
|
|
113
123
|
agentId: textOrUndefined(body['agent_id'] ?? body['agentId']) ?? '',
|
|
114
124
|
chatModelId: textOrUndefined(body['chat_model_id'] ?? body['chatModelId']) ?? '',
|
|
115
125
|
tenantId: textOrUndefined(body['tenant_id'] ?? body['tenantId']) ?? '',
|
|
126
|
+
knowledgeBases: projectKnowledgeBases(body['knowledge_bases'] ?? body['knowledgeBases']),
|
|
116
127
|
};
|
|
117
128
|
}
|
|
129
|
+
/**
|
|
130
|
+
* 投影展示范围(设计 §3.1:去空 ID、按 ID 去重、名称为空允许、保持网关顺序)。
|
|
131
|
+
*
|
|
132
|
+
* 归一在**本模块**再做一遍(网关也做了):网关与 core 可能版本交错,而这里的输出会直接影响
|
|
133
|
+
* 「哪些库算在范围内」——空白 ID 或重复项混进来会让一张展示表与 ID 列表对不上。
|
|
134
|
+
*/
|
|
135
|
+
function projectKnowledgeBases(value) {
|
|
136
|
+
if (!Array.isArray(value))
|
|
137
|
+
return EMPTY_KNOWLEDGE_BASES;
|
|
138
|
+
const seen = new Set();
|
|
139
|
+
const bases = [];
|
|
140
|
+
for (const item of value) {
|
|
141
|
+
if (item === null || typeof item !== 'object')
|
|
142
|
+
continue;
|
|
143
|
+
const record = item;
|
|
144
|
+
const id = textOrUndefined(record['id']);
|
|
145
|
+
if (id === undefined || seen.has(id))
|
|
146
|
+
continue;
|
|
147
|
+
seen.add(id);
|
|
148
|
+
bases.push({ id, name: textOrUndefined(record['name']) ?? '' });
|
|
149
|
+
}
|
|
150
|
+
return bases.length === 0 ? EMPTY_KNOWLEDGE_BASES : bases;
|
|
151
|
+
}
|
|
152
|
+
/** 冻结的空展示范围(同一引用,避免每次投影都分配)。 */
|
|
153
|
+
const EMPTY_KNOWLEDGE_BASES = Object.freeze([]);
|
|
118
154
|
/** 异常类型名(日志只记类型,不记消息,避免把凭据/URL 带进日志)。 */
|
|
119
155
|
function errorName(error) {
|
|
120
156
|
return error instanceof Error ? error.name : typeof error;
|
|
@@ -156,9 +192,63 @@ function reasonOfStatus(status) {
|
|
|
156
192
|
class KnowledgeConfigProvider {
|
|
157
193
|
deps;
|
|
158
194
|
cached;
|
|
195
|
+
tokenCached;
|
|
159
196
|
constructor(deps) {
|
|
160
197
|
this.deps = deps;
|
|
161
198
|
}
|
|
199
|
+
async token(opts) {
|
|
200
|
+
const force = opts?.force === true;
|
|
201
|
+
let sessionCookie;
|
|
202
|
+
try {
|
|
203
|
+
sessionCookie = await this.deps.resolveSession();
|
|
204
|
+
}
|
|
205
|
+
catch (error) {
|
|
206
|
+
this.deps.log?.(`hiwork-core: 知识库令牌获取失败(${errorName(error)})`);
|
|
207
|
+
return failure('error');
|
|
208
|
+
}
|
|
209
|
+
if (!sessionCookie)
|
|
210
|
+
return failure('unauthenticated');
|
|
211
|
+
const hit = this.tokenCached;
|
|
212
|
+
if (!force && hit?.sessionCookie === sessionCookie && this.now() - hit.at < KNOWLEDGE_TOKEN_CACHE_TTL_MS) {
|
|
213
|
+
return hit.view;
|
|
214
|
+
}
|
|
215
|
+
const gateway = this.deps.gateway();
|
|
216
|
+
if (!gateway)
|
|
217
|
+
return failure('unreachable');
|
|
218
|
+
try {
|
|
219
|
+
if (!gateway.getMyKnowledgeToken)
|
|
220
|
+
return failure('error');
|
|
221
|
+
const raw = await gateway.getMyKnowledgeToken(sessionCookie);
|
|
222
|
+
const envelope = raw;
|
|
223
|
+
const body = (envelope?.['data'] && typeof envelope['data'] === 'object' ? envelope['data'] : envelope);
|
|
224
|
+
const token = textOrUndefined(body?.['token'] ?? body?.['knowledge_token']);
|
|
225
|
+
const audience = textOrUndefined(body?.['audience'] ?? body?.['aud']);
|
|
226
|
+
const ids = body?.['knowledge_base_ids'] ?? body?.['knowledgeBaseIds'];
|
|
227
|
+
if (!token || audience !== 'hiwork-knowledge' || !Array.isArray(ids) || !ids.every(id => typeof id === 'string')) {
|
|
228
|
+
this.deps.log?.('hiwork-core: 网关返回的 knowledge-token 形状无效');
|
|
229
|
+
return failure('error');
|
|
230
|
+
}
|
|
231
|
+
const knowledgeBaseIds = [...new Set(ids.map(id => id.trim()).filter(Boolean))];
|
|
232
|
+
const rawBases = body?.['knowledge_bases'] ?? body?.['knowledgeBases'];
|
|
233
|
+
const knowledgeBases = Array.isArray(rawBases) ? rawBases.flatMap(item => {
|
|
234
|
+
if (item === null || typeof item !== 'object')
|
|
235
|
+
return [];
|
|
236
|
+
const obj = item;
|
|
237
|
+
const id = textOrUndefined(obj['id']);
|
|
238
|
+
return id ? [{ id, name: textOrUndefined(obj['name']) ?? '' }] : [];
|
|
239
|
+
}).filter(item => knowledgeBaseIds.includes(item.id)) : [];
|
|
240
|
+
const view = { ok: true, token, audience, knowledgeBaseIds, knowledgeBases };
|
|
241
|
+
this.tokenCached = { sessionCookie, view, at: this.now() };
|
|
242
|
+
return view;
|
|
243
|
+
}
|
|
244
|
+
catch (error) {
|
|
245
|
+
const mapped = reasonOfErrorCode(error);
|
|
246
|
+
if (mapped)
|
|
247
|
+
return failure(mapped);
|
|
248
|
+
this.deps.log?.(`hiwork-core: 知识库令牌获取失败(${errorName(error)})`);
|
|
249
|
+
return failure('error');
|
|
250
|
+
}
|
|
251
|
+
}
|
|
162
252
|
async config(opts) {
|
|
163
253
|
const force = opts?.force === true;
|
|
164
254
|
let sessionCookie;
|
package/lib/provision-session.js
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* (浏览器授权登录)各自直接 import,**它自身不 import 这两者**,因此不存在循环依赖
|
|
7
7
|
* (打包产物里也不会出现模块级 TDZ 风险)。
|
|
8
8
|
*/
|
|
9
|
-
import { GatewayError } from "./ai-router-client.js";
|
|
9
|
+
import { GatewayError, isImageErrorCode } from "./ai-router-client.js";
|
|
10
10
|
import { API_KEY_REF, buildPiAiProviderProfiles, computeModelIds, computeRouteModels, } from "./provision.js";
|
|
11
11
|
/**
|
|
12
12
|
* 归一化 budgets:过滤空模型名与已下架模型(model_status === 'inactive',已不可调用,
|
|
@@ -38,8 +38,16 @@ export function computeKeys(keys) {
|
|
|
38
38
|
}
|
|
39
39
|
/** 任意抛错 → 稳定 LoginError;GatewayError 原样,其余归为 GATEWAY_UNREACHABLE。 */
|
|
40
40
|
export function toLoginError(error, fallback) {
|
|
41
|
-
|
|
41
|
+
// `GatewayError` 的 code 取值域同时含**生图**链路的码(`ImageErrorCode`)。本函数只服务
|
|
42
|
+
// 登录与供给编排,生图的码走到这里属编程错误:收口成 GATEWAY_UNREACHABLE 而不是把它
|
|
43
|
+
// 当成登录码返回(client 半边按 code 分支,未知码会走兜底文案)。
|
|
44
|
+
// 判别式走 isImageErrorCode 而不是 `as` 断言:收窄由类型系统证明(见该函数注释),
|
|
45
|
+
// 将来新增不以 IMAGE_ 开头的生图码会在编译期报错而不是静默放行。
|
|
46
|
+
if (error instanceof GatewayError) {
|
|
47
|
+
if (isImageErrorCode(error.code))
|
|
48
|
+
return { code: 'GATEWAY_UNREACHABLE', message: fallback };
|
|
42
49
|
return { code: error.code, message: error.message };
|
|
50
|
+
}
|
|
43
51
|
return { code: 'GATEWAY_UNREACHABLE', message: fallback };
|
|
44
52
|
}
|
|
45
53
|
/**
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 生图工具名的**单一事实源**。
|
|
3
|
+
*
|
|
4
|
+
* Web 半边(`src/client/ImageCards.tsx` 的 `tool.call.toolview` keyed 座位)与 Host 注册
|
|
5
|
+
* 都从这里读同一个常量:keyed 座位**按线上工具名认领**,名字拼错的失败方式是**静默**的
|
|
6
|
+
* ——卡片永不渲染、通用工具行照常出现,没有任何测试会因为它变红(knowledge 已踩过)。
|
|
7
|
+
* 所以这里既导出元组,也导出它的元素类型(`Record<ImageToolName, …>` 这种写法能在
|
|
8
|
+
* 编译期挡住「新增工具忘了补卡片」)。
|
|
9
|
+
*/
|
|
10
|
+
export const IMAGE_TOOL_NAMES = ['image_generate'];
|
package/lib/tools.js
ADDED
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Agent 侧生图工具 `image_generate`(唯一一个会**写工作区**的 HiWork 工具)。
|
|
3
|
+
*
|
|
4
|
+
* 形状照 `hiwork-knowledge/src/tools.ts` 的 `registerKnowledgeTools(service, agent)`:
|
|
5
|
+
* `defineTool` + 只在**挂载它的根 Agent** 上生效 + `disposer` 交给 `ctx.effect` 管生命周期。
|
|
6
|
+
*
|
|
7
|
+
* 规则:
|
|
8
|
+
* - `exec.agent` 不是本 Agent、或 `exec.signal.aborted` → 统一返回
|
|
9
|
+
* `{ ok: false, code: 'cancelled' }`,**不碰网关**(不产生任何费用);
|
|
10
|
+
* - 工作区根从**本次执行**的会话上下文取(`exec.agent.session.header.cwd`,与设计 §6.2
|
|
11
|
+
* 一致),取不到回落默认工作区(`src/imagegen.ts`);
|
|
12
|
+
* - **失败一律 `{ok:false, code, message}`**,不给模型留「猜为什么失败」的空间;
|
|
13
|
+
* `code` 直接透传网关的 `IMAGE_*`(卡片按 code 分文案,设计 §6.6);
|
|
14
|
+
* - 返回给模型的 `text` 里**只有路径、尺寸、模型、耗时与一句可执行提示**——绝不含
|
|
15
|
+
* base64 或任何凭据;
|
|
16
|
+
* - 结果同时是**卡片的数据源**:`output.render` 把一个 JSON 串作为工具结果正文
|
|
17
|
+
* (与 knowledge 的 `JSON_OUTPUT` 同一形态),Web 卡片从 `content[0].text` 里解析它
|
|
18
|
+
* ——设计 §6.3 要求「结构化结果给卡片(JSON 形态,与 GenerateOutcome 同形)」,
|
|
19
|
+
* 模型可见的那段话就在同一个 JSON 的 `text` 字段里(见 `formatImageGenerateText`)。
|
|
20
|
+
*/
|
|
21
|
+
import { defineTool, } from '@deepseek-ai/dsh-tools';
|
|
22
|
+
import { IMAGE_SIZES } from "./imagegen.js";
|
|
23
|
+
import { IMAGE_TOOL_NAMES } from "./tool-names.js";
|
|
24
|
+
export { IMAGE_TOOL_NAMES };
|
|
25
|
+
/**
|
|
26
|
+
* 生图工具的单次预算(**工具层护栏**,不是业务判据)。
|
|
27
|
+
*
|
|
28
|
+
* 它必须**大于**客户端真正会等的时间,否则工具会先掐断客户端请求,把一次本可成功的
|
|
29
|
+
* 生图变成工具超时。客户端等待上限 = 网关预算 + 15s(见 `imageGenerateTimeoutMs`),
|
|
30
|
+
* 而网关预算的合法上界是 600s、读不到配置时走 900s 兜底 ⇒ 工具预算取 960s(16 分钟),
|
|
31
|
+
* 比两者都高。真正的超时语义由网关的 504 / `IMAGE_TIMEOUT` 表达。
|
|
32
|
+
*
|
|
33
|
+
* 实测背景:原值 330s 是按「网关默认 180s」估的,而网关上界 600s、实测单张已到 415s
|
|
34
|
+
* —— 按那条估算会在合法的慢生图上误报。
|
|
35
|
+
*/
|
|
36
|
+
export const IMAGE_TOOL_TIMEOUT_MS = 960_000;
|
|
37
|
+
/** 工具的模型可见描述(设计 §6.3 逐字冻结)。 */
|
|
38
|
+
export const IMAGE_GENERATE_DESCRIPTION = '生成一张图片并保存到工作区。用户要求画图、配图、生成插画/海报/图标/示意图时使用。'
|
|
39
|
+
+ 'prompt 要写清主体、风格、构图与用途;用户只给一句话时也要把它扩写成可执行的画面描述。'
|
|
40
|
+
+ '一次调用生成一张,多张就多次调用。不支持修改已有图片(没有图生图)。';
|
|
41
|
+
/**
|
|
42
|
+
* 结果投影:**JSON 串**给模型(knowledge 同款)。卡片半边从 `content[0].text` 解析同一份
|
|
43
|
+
* 结构化结果(平台没有第二个能给到 keyed 卡片的通道)。
|
|
44
|
+
*/
|
|
45
|
+
const JSON_OUTPUT = {
|
|
46
|
+
schema: { type: 'json' },
|
|
47
|
+
render: (_args, value) => [{ type: 'text', text: JSON.stringify(value) }],
|
|
48
|
+
};
|
|
49
|
+
function asRecord(value) {
|
|
50
|
+
if (typeof value !== 'object' || value === null || Array.isArray(value))
|
|
51
|
+
return {};
|
|
52
|
+
return value;
|
|
53
|
+
}
|
|
54
|
+
/** JSON 安全化(工具结果要能无损序列化;值本身全是标量/数组)。 */
|
|
55
|
+
function toToolJson(value) {
|
|
56
|
+
return JSON.parse(JSON.stringify(value));
|
|
57
|
+
}
|
|
58
|
+
function presentCall(title, kind, rawInput) {
|
|
59
|
+
return { card: 'generic', title, kind, ...(rawInput === undefined ? {} : { rawInput }) };
|
|
60
|
+
}
|
|
61
|
+
function isExecutingAgent(exec, agent) {
|
|
62
|
+
if (exec.agent === agent)
|
|
63
|
+
return true;
|
|
64
|
+
const value = asRecord(exec.agent);
|
|
65
|
+
return value['id'] === agent.id;
|
|
66
|
+
}
|
|
67
|
+
/** 秒级耗时(一位小数):卡片与模型都只看这个量级,`41230ms` 那种精度没有意义。 */
|
|
68
|
+
function formatSeconds(ms) {
|
|
69
|
+
if (!Number.isFinite(ms) || ms <= 0)
|
|
70
|
+
return '';
|
|
71
|
+
return `${(ms / 1000).toFixed(1)}s`;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* 每个错误码对应**一句可执行的话**(设计 §6.3:「失败时的可执行提示」)。
|
|
75
|
+
* 不给模型留下「猜为什么失败」的空间,也不让它「再试一次」去撞一堵不可能推倒的墙。
|
|
76
|
+
*/
|
|
77
|
+
const IMAGE_FAILURE_HINT = {
|
|
78
|
+
IMAGE_DISABLED: '管理员还没开启图片生成。请把这句话告诉用户,不要重复调用本工具。',
|
|
79
|
+
IMAGE_NOT_CONFIGURED: '本部署的生图链路还没配置好(模型、映射或额度)。请告诉用户联系管理员,不要重复调用本工具。',
|
|
80
|
+
IMAGE_BUDGET_EXCEEDED: '这个月的生图额度可能已用完,或该模型还没配置好。请告诉用户联系管理员,不要重复调用本工具。',
|
|
81
|
+
IMAGE_BAD_REQUEST: '提示词没能通过校验(可能过长或含有不被接受的参数)。改写成更具体的画面描述后再试一次。',
|
|
82
|
+
IMAGE_TOO_LARGE: '生成的图片超过了体积上限。换小一点的尺寸(例如 1024x1024)再试一次。',
|
|
83
|
+
IMAGE_TIMEOUT: '这次生成超时了(复杂画面会更慢)。可以再试一次,或把画面描述简化一些。',
|
|
84
|
+
IMAGE_UPSTREAM_ERROR: '生图服务这次没能出图。稍后重试;连续失败就把这句话告诉用户。',
|
|
85
|
+
GATEWAY_UNREACHABLE: '网关当前不可用。稍后重试,并把这句话告诉用户。',
|
|
86
|
+
SESSION_EXPIRED: '登录状态已过期。请提示用户重新登录后再试。',
|
|
87
|
+
'write-failed': '图片**已经生成**(上游已出图、已计费),只是没能保存到工作区。'
|
|
88
|
+
+ '请把这一点告诉用户:图片可以在「我的图片」页里看到并手动保存,不要当作生成失败重试。',
|
|
89
|
+
'path-outside-workspace': '保存位置越出了工作区,已拒绝写入。请把这句话告诉用户。',
|
|
90
|
+
cancelled: '本次调用已取消。',
|
|
91
|
+
};
|
|
92
|
+
/** 模型可见的那段话(成功:路径 + 尺寸/模型/耗时 + 落盘说明;失败:原因 + 可执行提示)。 */
|
|
93
|
+
export function formatImageGenerateText(outcome) {
|
|
94
|
+
if (outcome.ok) {
|
|
95
|
+
const lines = outcome.files.map((file) => `<path>${file.path}</path>`);
|
|
96
|
+
const meta = [
|
|
97
|
+
`尺寸 ${outcome.size === '' ? '未知' : outcome.size}`,
|
|
98
|
+
`模型 ${outcome.model === '' ? '未知' : outcome.model}`,
|
|
99
|
+
];
|
|
100
|
+
const seconds = formatSeconds(outcome.generationMs);
|
|
101
|
+
if (seconds !== '')
|
|
102
|
+
meta.push(`耗时 ${seconds}`);
|
|
103
|
+
lines.push(meta.join(' · '));
|
|
104
|
+
if (outcome.files.length > 0)
|
|
105
|
+
lines.push(`已保存到工作区:${outcome.files[0].path}`);
|
|
106
|
+
else
|
|
107
|
+
lines.push('已生成,但没有可用的落盘路径。');
|
|
108
|
+
return lines.join('\n');
|
|
109
|
+
}
|
|
110
|
+
const detail = outcome.code === undefined ? '' : `(code: ${outcome.code})`;
|
|
111
|
+
const hint = IMAGE_FAILURE_HINT[outcome.code] ?? '稍后重试;连续失败就把它告诉用户。';
|
|
112
|
+
return `生成失败:${outcome.message}${detail}\n${hint}`;
|
|
113
|
+
}
|
|
114
|
+
/** `GenerateOutcome` → 工具结果(卡片与模型共用同一份 JSON)。 */
|
|
115
|
+
export function toImageToolValue(outcome) {
|
|
116
|
+
const base = {
|
|
117
|
+
ok: outcome.ok,
|
|
118
|
+
text: formatImageGenerateText(outcome),
|
|
119
|
+
files: outcome.files.map((file) => ({
|
|
120
|
+
path: file.path,
|
|
121
|
+
mimeType: file.mimeType,
|
|
122
|
+
byteSize: file.byteSize,
|
|
123
|
+
...(file.revisedPrompt === undefined ? {} : { revisedPrompt: file.revisedPrompt }),
|
|
124
|
+
})),
|
|
125
|
+
};
|
|
126
|
+
if (outcome.ok) {
|
|
127
|
+
return { ...base, prompt: outcome.prompt, model: outcome.model, size: outcome.size, generationMs: outcome.generationMs };
|
|
128
|
+
}
|
|
129
|
+
return {
|
|
130
|
+
...base,
|
|
131
|
+
...(outcome.prompt === undefined ? {} : { prompt: outcome.prompt }),
|
|
132
|
+
...(outcome.model === undefined ? {} : { model: outcome.model }),
|
|
133
|
+
...(outcome.size === undefined ? {} : { size: outcome.size }),
|
|
134
|
+
...(outcome.generationMs === undefined ? {} : { generationMs: outcome.generationMs }),
|
|
135
|
+
code: outcome.code,
|
|
136
|
+
message: outcome.message,
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
/** 会话工作区根(`exec.agent.session.header.cwd`);取不到返回 undefined。 */
|
|
140
|
+
function sessionWorkspaceRoot(exec) {
|
|
141
|
+
const agent = asRecord(exec.agent);
|
|
142
|
+
const session = asRecord(agent['session']);
|
|
143
|
+
const header = asRecord(session['header']);
|
|
144
|
+
const cwd = header['cwd'];
|
|
145
|
+
return typeof cwd === 'string' && cwd.trim() !== '' ? cwd : undefined;
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* 给一个根 Agent 注册生图工具。
|
|
149
|
+
* @param service - Host 侧生图服务(生产是 `src/imagegen.ts` 的 `generateImageFile`)。
|
|
150
|
+
* @param agent - 目标根 Agent。
|
|
151
|
+
* @returns 注销器(注销全部已注册工具)。
|
|
152
|
+
*/
|
|
153
|
+
export function registerImageTools(service, agent) {
|
|
154
|
+
const disposers = [];
|
|
155
|
+
const register = (definition) => {
|
|
156
|
+
disposers.push(agent.ctx.tools.register(definition));
|
|
157
|
+
};
|
|
158
|
+
const guard = (exec) => isExecutingAgent(exec, agent) && !exec.signal.aborted;
|
|
159
|
+
try {
|
|
160
|
+
register(defineTool({
|
|
161
|
+
name: IMAGE_TOOL_NAMES[0],
|
|
162
|
+
description: IMAGE_GENERATE_DESCRIPTION,
|
|
163
|
+
parameters: {
|
|
164
|
+
prompt: {
|
|
165
|
+
type: 'string',
|
|
166
|
+
required: true,
|
|
167
|
+
description: '画面描述:主体、风格、构图、用途。用户只说一句时也要扩写成可执行的画面描述。',
|
|
168
|
+
},
|
|
169
|
+
// 三值是**跨仓冻结契约**(与网关 `image.default_size` 的枚举集合一致):
|
|
170
|
+
// `defineTool` 在注册期就把 schema 冻死,没法让枚举跟着 `image-config` 走,
|
|
171
|
+
// 所以它必须写在这里,并由 tests/imagegen.spec.ts 逐项比对。
|
|
172
|
+
size: {
|
|
173
|
+
type: 'string',
|
|
174
|
+
enum: IMAGE_SIZES,
|
|
175
|
+
description: '画布尺寸;省略时用网关配置的默认尺寸(通常是 1024x1024)。',
|
|
176
|
+
},
|
|
177
|
+
},
|
|
178
|
+
output: JSON_OUTPUT,
|
|
179
|
+
timeoutMs: IMAGE_TOOL_TIMEOUT_MS,
|
|
180
|
+
async execute(args, exec) {
|
|
181
|
+
if (!guard(exec))
|
|
182
|
+
return toToolJson({ ok: false, code: 'cancelled', message: '本次调用已取消。', text: '', files: [] });
|
|
183
|
+
const workspaceRoot = sessionWorkspaceRoot(exec);
|
|
184
|
+
const outcome = await service.generate({
|
|
185
|
+
prompt: args.prompt,
|
|
186
|
+
...(args.size === undefined ? {} : { size: args.size }),
|
|
187
|
+
...(workspaceRoot === undefined ? {} : { workspaceRoot }),
|
|
188
|
+
});
|
|
189
|
+
return toToolJson(toImageToolValue(outcome));
|
|
190
|
+
},
|
|
191
|
+
// 卡片标题在**执行中**就显示提示词:生图要几十秒,这段时间里用户必须知道它在画什么。
|
|
192
|
+
presentCall: args => presentCall('生成图片', 'edit', args.prompt),
|
|
193
|
+
}));
|
|
194
|
+
}
|
|
195
|
+
catch (error) {
|
|
196
|
+
// 注册中途失败也必须把已注册的工具摘掉,否则会留下半个工具面。
|
|
197
|
+
for (const dispose of disposers.splice(0).reverse())
|
|
198
|
+
dispose();
|
|
199
|
+
throw error;
|
|
200
|
+
}
|
|
201
|
+
return () => {
|
|
202
|
+
for (const dispose of disposers.splice(0).reverse())
|
|
203
|
+
dispose();
|
|
204
|
+
};
|
|
205
|
+
}
|
|
@@ -1,9 +1,92 @@
|
|
|
1
1
|
import type { DeviceSession, LoginErrorCode } from './types.ts';
|
|
2
|
+
/**
|
|
3
|
+
* 生图的单次请求超时(**按网关下发的预算算**,不是常量)。
|
|
4
|
+
*
|
|
5
|
+
* 生图是「数十秒 ~ 分钟级」的调用,默认 10s 会**稳定误判**成网关不可达(语音那条的
|
|
6
|
+
* 同一理由,只是量级更大)。真正该等多久由网关的 `image.timeout_seconds` 决定,
|
|
7
|
+
* 而这个值**现在随 `image-config` 下发**(见 {@link GatewayImageConfig.timeoutSeconds})。
|
|
8
|
+
*
|
|
9
|
+
* 为什么不写常量(实测踩过):原实现把网关闭口硬编码成 180s,而网关实际被管理员
|
|
10
|
+
* 设为 600s —— 于是每次真实生图(实测 213s / 415s)都被客户端判超时,网关却还在画。
|
|
11
|
+
* 与 `model` 同理:参数只在网关,客户端跟随下发。
|
|
12
|
+
*
|
|
13
|
+
* `list` / `fetch` 是普通 JSON / 二进制往返,仍用构造时的默认超时。
|
|
14
|
+
*/
|
|
15
|
+
export declare const IMAGE_GATEWAY_TIMEOUT_MARGIN_MS = 15000;
|
|
16
|
+
/**
|
|
17
|
+
* 读不到 `timeout_seconds` 时的兜底等待上限(仅此情形用)。
|
|
18
|
+
*
|
|
19
|
+
* 取 900s:网关侧 `image.timeout_seconds` 的合法区间是 30~600(`intRange(30, 600)`),
|
|
20
|
+
* 兜底必须**大于它的上界**,否则配置读不出来时又会对合法调用误判超时。
|
|
21
|
+
* 配置正常时**不用**这个值——用「网关预算 + 15s」,这样网关自己 504 之前客户端
|
|
22
|
+
* 不会先放弃(否则用户看到的是「网络错误」而不是「超时」)。
|
|
23
|
+
*/
|
|
24
|
+
export declare const IMAGE_GENERATE_TIMEOUT_FALLBACK_MS = 900000;
|
|
25
|
+
/**
|
|
26
|
+
* 按网关下发的单次预算算客户端等待上限。
|
|
27
|
+
*
|
|
28
|
+
* 传 `undefined` / 非法值(配置没读到、字段缺失、0 或负数)→ 兜底 900s;
|
|
29
|
+
* 否则 = 预算 + {@link IMAGE_GATEWAY_TIMEOUT_MARGIN_MS}。余量是留给中转与网络的:
|
|
30
|
+
* 它必须**大于**网关自己的超时,才能让「网关判超时(504 → IMAGE_TIMEOUT)」先于
|
|
31
|
+
* 「客户端放弃(GATEWAY_UNREACHABLE)」发生——两者对用户的含义完全不同(一个重试、
|
|
32
|
+
* 一个找运维)。
|
|
33
|
+
*/
|
|
34
|
+
export declare function imageGenerateTimeoutMs(gatewayTimeoutSeconds?: number): number;
|
|
35
|
+
/**
|
|
36
|
+
* 生图链路的稳定错误码(网关 `me_image_handler.go` 的**冻结契约**,客户端按 code 分文案)。
|
|
37
|
+
*
|
|
38
|
+
* 前七条 = `POST /api/me/image-generate` 的七档(设计 §5.2 的表);后两条是「我的图片」
|
|
39
|
+
* 自助路径(`GET /api/me/images/:id/file`)的两种拒绝——它们不在那张表上(那张表只描述
|
|
40
|
+
* 生图端点),但同样需要可展示的分档,否则画廊页会把「别人的图」说成「服务不可用」。
|
|
41
|
+
*
|
|
42
|
+
* ⚠️ 这九个 code 的**归宿**是 `src/client/types.ts` 的 `LoginErrorCode`(那里是所有稳定
|
|
43
|
+
* 码的单一事实源);本文件因为文件边界不碰 client 半边,改为在此声明并用
|
|
44
|
+
* {@link GatewayErrorCode} 并入 `GatewayError` 的取值域。两处合并后类型完全等价。
|
|
45
|
+
*/
|
|
46
|
+
export type ImageErrorCode =
|
|
47
|
+
/** 400:坏 JSON / prompt 空或超长 / size、quality 不在枚举内。 */
|
|
48
|
+
'IMAGE_BAD_REQUEST'
|
|
49
|
+
/** 402:该 Key 对该模型的预算预检失败(复用既有 402 语义,不改文案归属)。 */
|
|
50
|
+
| 'IMAGE_BUDGET_EXCEEDED'
|
|
51
|
+
/** 413:单张图超过上限(20MiB)。 */
|
|
52
|
+
| 'IMAGE_TOO_LARGE'
|
|
53
|
+
/** 503:管理员还没开启图片生成(`image.enabled=false`)。 */
|
|
54
|
+
| 'IMAGE_DISABLED'
|
|
55
|
+
/** 503:模型名为空 / 该 Key 取不到 / loopback 被网关自身拒绝(无映射、模型 inactive)。 */
|
|
56
|
+
| 'IMAGE_NOT_CONFIGURED'
|
|
57
|
+
/** 502:上游不可达 / 超时 / 凭据被拒 / 响应里没有图片数据。 */
|
|
58
|
+
| 'IMAGE_UPSTREAM_ERROR'
|
|
59
|
+
/** 504:超过 `image.timeout_seconds`(与上游故障分开:一个重试、一个找运维)。 */
|
|
60
|
+
| 'IMAGE_TIMEOUT'
|
|
61
|
+
/** 404:这张图不存在(已被清理,或 id 不在该账号名下)。 */
|
|
62
|
+
| 'IMAGE_NOT_FOUND'
|
|
63
|
+
/** 403:这张图不属于当前账号(网关按「该用户的 Key 列表」判定归属)。 */
|
|
64
|
+
| 'IMAGE_FORBIDDEN'
|
|
65
|
+
/** 画廊页 `save`:指定的 `workspaceId` 不在工作区注册表里(不落别处)。 */
|
|
66
|
+
| 'IMAGE_WORKSPACE_NOT_FOUND'
|
|
67
|
+
/** 画廊页 `save`:图取回来了但没能写进工作区(越界 / 写盘失败 / 空内容)。 */
|
|
68
|
+
| 'IMAGE_SAVE_FAILED';
|
|
69
|
+
/** `GatewayError.code` 的取值域:登录/刷新链路的码 + 生图链路的码。 */
|
|
70
|
+
export type GatewayErrorCode = LoginErrorCode | ImageErrorCode;
|
|
71
|
+
/**
|
|
72
|
+
* 生图链路码的**判别式**:`ImageErrorCode` 的每个成员都以 `IMAGE_` 开头,而 `LoginErrorCode`
|
|
73
|
+
* 里没有任何这样的码。用模板字面量做 `Extract`/`Exclude`,`isImageErrorCode` 判真之后
|
|
74
|
+
* TS 能把 `GatewayErrorCode` 收窄成**恰好** `LoginErrorCode` —— 于是消费方(如
|
|
75
|
+
* `toLoginError`)不需要 `as` 断言就能拿到合法类型,也不用手维护一份码表。
|
|
76
|
+
*
|
|
77
|
+
* 哪天有人新增一个不以 `IMAGE_` 开头的生图码,`ImageErrorCode` 与这个判别式就会失配:
|
|
78
|
+
* 那种情况下 `Exclude` 收窄不掉它,`toLoginError` 的返回类型会在**编译期**报错
|
|
79
|
+
* —— 这正是我们要的(比运行时静默返回未知码好)。
|
|
80
|
+
*/
|
|
81
|
+
export type ImagePrefixedCode = Extract<GatewayErrorCode, `IMAGE_${string}`>;
|
|
82
|
+
export type NonImageErrorCode = Exclude<GatewayErrorCode, `IMAGE_${string}`>;
|
|
83
|
+
/** 该码是否属于生图链路(见 {@link ImagePrefixedCode})。 */
|
|
84
|
+
export declare function isImageErrorCode(code: GatewayErrorCode): code is ImagePrefixedCode;
|
|
2
85
|
/** 网关错误:携带稳定 code,供编排层映射。 */
|
|
3
86
|
export declare class GatewayError extends Error {
|
|
4
|
-
readonly code:
|
|
87
|
+
readonly code: GatewayErrorCode;
|
|
5
88
|
readonly status?: number | undefined;
|
|
6
|
-
constructor(code:
|
|
89
|
+
constructor(code: GatewayErrorCode, message: string, status?: number | undefined);
|
|
7
90
|
}
|
|
8
91
|
/** 登录响应中的用户信息。display_name 为网关自 LDAP 同步的中文名(可能缺省)。 */
|
|
9
92
|
export interface GatewayUser {
|
|
@@ -253,6 +336,15 @@ export interface GatewayMcpAssignmentsView {
|
|
|
253
336
|
* `jwtSecret` 是**共享的**一把密钥(Document Server 社区版只有一把),终端拿到它只为
|
|
254
337
|
* 签编辑器 config / 验保存回调;它只应在 host 侧使用,绝不进浏览器或日志。
|
|
255
338
|
*/
|
|
339
|
+
export interface GatewayKnowledgeToken {
|
|
340
|
+
token: string;
|
|
341
|
+
audience: string;
|
|
342
|
+
knowledge_base_ids: string[];
|
|
343
|
+
knowledge_bases?: Array<{
|
|
344
|
+
id: string;
|
|
345
|
+
name: string;
|
|
346
|
+
}>;
|
|
347
|
+
}
|
|
256
348
|
export interface GatewayOfficeConfig {
|
|
257
349
|
documentServerUrl: string;
|
|
258
350
|
jwtSecret: string;
|
|
@@ -303,6 +395,88 @@ export interface GatewayVoiceConfig {
|
|
|
303
395
|
export interface GatewayVoiceTranscription {
|
|
304
396
|
text: string;
|
|
305
397
|
}
|
|
398
|
+
/**
|
|
399
|
+
* GET /api/me/image-config 响应。
|
|
400
|
+
*
|
|
401
|
+
* 与语音配置同款:网关线上是 snake_case,**在这一层就转成 camelCase**(`default_size`
|
|
402
|
+
* → `defaultSize`),Host 频道、浏览器半边与测试都只认一种写法。**本文件是唯一的转换点**
|
|
403
|
+
* (设计 §6.1 的硬约束),下游不许再转一次。
|
|
404
|
+
*
|
|
405
|
+
* 端点契约**恒 200**:开关关 / 模型没配都体现成 `enabled:false`,不在这条端点上分错误档。
|
|
406
|
+
*/
|
|
407
|
+
export interface GatewayImageConfig {
|
|
408
|
+
/** 本部署 + 本账号是否可用生图(管理员开关 + 模型名非空)。 */
|
|
409
|
+
enabled: boolean;
|
|
410
|
+
/** 当前生效的 Responses 模型名(客户端**仅用于展示**,不参与任何客户端逻辑)。 */
|
|
411
|
+
model: string;
|
|
412
|
+
/** 未指定尺寸时的默认值(客户端仍可覆盖)。 */
|
|
413
|
+
defaultSize: string;
|
|
414
|
+
/** 允许的尺寸集合(冻结三值)。 */
|
|
415
|
+
sizes: string[];
|
|
416
|
+
/** 生效的质量档(**不下发给员工选**,由管理员在设置里定)。 */
|
|
417
|
+
quality: string;
|
|
418
|
+
/**
|
|
419
|
+
* 网关侧单次生图预算(秒),用于算客户端的等待上限(见
|
|
420
|
+
* {@link imageGenerateTimeoutMs})。读不到时为 `undefined` → 走兜底。
|
|
421
|
+
*
|
|
422
|
+
* 原设计写着「客户端不需要超时值」——**那是错的**,且直接导致了线上故障:
|
|
423
|
+
* 客户端不知道这个数就只能硬编码,与网关实际值脱钩。
|
|
424
|
+
*/
|
|
425
|
+
timeoutSeconds?: number;
|
|
426
|
+
}
|
|
427
|
+
/** POST /api/me/image-generate 里的一张图(`data_base64` 无 data URL 前缀)。 */
|
|
428
|
+
export interface GatewayGeneratedImage {
|
|
429
|
+
/** 裸 base64(Host 据此解码写盘)。**绝不进日志、绝不进 RPC 响应以外的任何地方。** */
|
|
430
|
+
dataBase64: string;
|
|
431
|
+
mimeType: string;
|
|
432
|
+
/** 容器格式(png / jpeg / webp)。 */
|
|
433
|
+
format: string;
|
|
434
|
+
byteSize: number;
|
|
435
|
+
/** 上游改写的提示词,可能为空串。 */
|
|
436
|
+
revisedPrompt?: string;
|
|
437
|
+
}
|
|
438
|
+
/** POST /api/me/image-generate 响应(恒回 1 张图)。 */
|
|
439
|
+
export interface GatewayImageGenerateResult {
|
|
440
|
+
images: GatewayGeneratedImage[];
|
|
441
|
+
/** 本次实际用的模型与尺寸(回显,便于卡片显示)。 */
|
|
442
|
+
model: string;
|
|
443
|
+
size: string;
|
|
444
|
+
generationMs: number;
|
|
445
|
+
requestId: string;
|
|
446
|
+
}
|
|
447
|
+
/**
|
|
448
|
+
* GET /api/me/images 的一行(桌面端画廊页的冻结契约 `ImageRow`,设计 §6.5)。
|
|
449
|
+
*
|
|
450
|
+
* **不透传 `file_url`**:浏览器拿不到会话 cookie,也拿不到别人的图;看单张一律经
|
|
451
|
+
* Host 的 `fetch` 端点现取。网关的 `page_size` / `revised_prompt` / `mime_type` /
|
|
452
|
+
* `byte_size` / `generation_ms` / `created_at` 在这里一次性转成 camelCase。
|
|
453
|
+
*/
|
|
454
|
+
export interface GatewayImageRow {
|
|
455
|
+
id: number;
|
|
456
|
+
prompt: string;
|
|
457
|
+
revisedPrompt?: string;
|
|
458
|
+
model: string;
|
|
459
|
+
/** 画布尺寸(如 `1024x1024`);与 `format`(容器格式)、`mimeType` 是三个侧面,别混。 */
|
|
460
|
+
size: string;
|
|
461
|
+
format: string;
|
|
462
|
+
mimeType: string;
|
|
463
|
+
byteSize: number;
|
|
464
|
+
generationMs: number;
|
|
465
|
+
createdAt: string;
|
|
466
|
+
}
|
|
467
|
+
/** GET /api/me/images 响应(分页元数据也是 camelCase)。 */
|
|
468
|
+
export interface GatewayImagesView {
|
|
469
|
+
items: GatewayImageRow[];
|
|
470
|
+
total: number;
|
|
471
|
+
page: number;
|
|
472
|
+
pageSize: number;
|
|
473
|
+
}
|
|
474
|
+
/** GET /api/me/images/:id/file 的结果:图片本体(base64)+ 类型 + 字节数。 */
|
|
475
|
+
export interface GatewayImageFile {
|
|
476
|
+
dataBase64: string;
|
|
477
|
+
mimeType: string;
|
|
478
|
+
byteSize: number;
|
|
479
|
+
}
|
|
306
480
|
/**
|
|
307
481
|
* POST /api/me/mcp-assertions 响应:一张 **120 秒**的身份断言(RS256 JWT)+
|
|
308
482
|
* 去哪换令牌。断言本身不是凭据(换不成令牌就是废纸),所以它可以出现在这里;
|
|
@@ -446,6 +620,8 @@ export interface GatewayClient {
|
|
|
446
620
|
estimateCost(sessionCookie: string, items: GatewayCostEstimateItem[]): Promise<GatewayCostEstimateResponse>;
|
|
447
621
|
/** 管理员下发给当前账号的 MCP 清单(响应里没有令牌)。 */
|
|
448
622
|
getMyMcpAssignments(sessionCookie: string): Promise<GatewayMcpAssignmentsView>;
|
|
623
|
+
/** 为当前会话签发知识库短期访问令牌。 */
|
|
624
|
+
getMyKnowledgeToken?(sessionCookie: string): Promise<GatewayKnowledgeToken>;
|
|
449
625
|
/** 本部署 OnlyOffice 集成配置(地址 + 共享 JWT 密钥 + 中继开关;部署未配置时 503)。 */
|
|
450
626
|
getMyOfficeConfig(sessionCookie: string): Promise<GatewayOfficeConfig>;
|
|
451
627
|
/** 上传文档到中继(原始字节 + 文件名头),拿回 DS 侧签名 URL。 */
|
|
@@ -561,6 +737,7 @@ export declare class AiRouterClient implements GatewayClient {
|
|
|
561
737
|
* 「部署未配置」的稳定信号,单独映射——消费方(hiwork-office)据此提示联系管理员
|
|
562
738
|
* 并回退本地手工配置,而不是报一句含糊的「网关不可达」。
|
|
563
739
|
*/
|
|
740
|
+
getMyKnowledgeToken(sessionCookie: string): Promise<GatewayKnowledgeToken>;
|
|
564
741
|
getMyOfficeConfig(sessionCookie: string): Promise<GatewayOfficeConfig>;
|
|
565
742
|
/**
|
|
566
743
|
* POST /api/me/mcp-assertions:为某条分配签一张 120 秒断言。
|
|
@@ -688,6 +865,82 @@ export declare class AiRouterClient implements GatewayClient {
|
|
|
688
865
|
* 等上游识别,构造时的 10s 默认值在这里是误报源。
|
|
689
866
|
*/
|
|
690
867
|
transcribeVoice(sessionCookie: string, audioBase64: string): Promise<GatewayVoiceTranscription>;
|
|
868
|
+
/**
|
|
869
|
+
* GET /api/me/image-config:当前账号的生图开关、模型名、默认尺寸与可选尺寸。
|
|
870
|
+
*
|
|
871
|
+
* 与 {@link getMyVoiceConfig} 同款,**不复用 `getMeJson`**:那条路径把 403 映射成
|
|
872
|
+
* `NO_KEY_ASSIGNED`(「尚未被分配虚拟 KEY」),而本端点的鉴权只有会话——分能力与分 Key
|
|
873
|
+
* 是两件事。契约**恒 200**(开关开与关都是 200 + `enabled` 布尔),所以除 401 以外没有
|
|
874
|
+
* 任何需要单独解释的拒绝语义:403 / 404 / 5xx / 反代错误页一律 `GATEWAY_UNREACHABLE`。
|
|
875
|
+
*
|
|
876
|
+
* 形状收敛(**本层负责**,消费方拿到的永远是一个合法的 {@link GatewayImageConfig}):
|
|
877
|
+
* `default_size` → `defaultSize`;字段缺失 / 类型不符按「禁用 + 默认尺寸 + 三尺寸枚举」
|
|
878
|
+
* 处理——配置读不出来时生图本就该消失,让消费方去猜 undefined 是把形状问题推给下游。
|
|
879
|
+
* 真正的「读不到就回落默认尺寸」在 `src/imagegen.ts`。
|
|
880
|
+
*/
|
|
881
|
+
getMyImageConfig(sessionCookie: string): Promise<GatewayImageConfig>;
|
|
882
|
+
/**
|
|
883
|
+
* POST /api/me/image-generate:把一句提示词交给网关**用该用户自己的虚拟 Key**代发生图。
|
|
884
|
+
*
|
|
885
|
+
* 状态码映射与服务端 handler 一一对应(设计 §5.2 的冻结表),**不许合并**:
|
|
886
|
+
* - 401 = 会话过期(唯一一条能让人重新登录来解决的拒绝);
|
|
887
|
+
* - 400 = 坏 JSON / prompt 空或超长 / size 不在枚举内 → `IMAGE_BAD_REQUEST`;
|
|
888
|
+
* - 402 = 该 Key 对该模型的预算预检失败 → `IMAGE_BUDGET_EXCEEDED`(复用的既有 402
|
|
889
|
+
* 语义;网关侧有四条不同成因共用这个状态码,所以卡片文案写成「两种成因都覆盖」——
|
|
890
|
+
* 桌面端**不按中文文案分流**,那是脆契约);
|
|
891
|
+
* - 413 = 单张图超限 → `IMAGE_TOO_LARGE`;
|
|
892
|
+
* - 503 = 网关自己发的「功能不可用」,按响应体 `code` 细分 `IMAGE_NOT_CONFIGURED`
|
|
893
|
+
* (链路没配好)/ `IMAGE_DISABLED`(管理员关了开关)。响应体没有 `code`
|
|
894
|
+
* (非 JSON / 反代错误页)归 `IMAGE_DISABLED`——这条路由的 503 语义就是「没开」,
|
|
895
|
+
* 而**上游故障走 502**(见下),不会伪装成 503;
|
|
896
|
+
* - 504 = 超过网关预算 → `IMAGE_TIMEOUT`(与上游故障分开:一个重试、一个找运维);
|
|
897
|
+
* - 其余(含 502 = 上游出错)→ `IMAGE_UPSTREAM_ERROR`。
|
|
898
|
+
*
|
|
899
|
+
* `key_id` 走 **query**(缺省 = 该用户首个 Key),与 `POST /api/me/cost-estimate`
|
|
900
|
+
* 同一口径;桌面端目前不传它。
|
|
901
|
+
*
|
|
902
|
+
* 超时**单独放宽**:这条要等上游画完一张图,构造时的 10s 默认值在这里是稳定的误报源。
|
|
903
|
+
* 等待上限由调用方按网关下发的预算传进来(见 {@link imageGenerateTimeoutMs});
|
|
904
|
+
* 不传则走兜底——**不要**在这里写死一个数,那正是上一版故障的来源。
|
|
905
|
+
* `size` / `quality` 缺省时由网关落设置默认值。
|
|
906
|
+
*
|
|
907
|
+
* **响应体里的 base64 不进日志**:本方法只把它交给调用方(`src/imagegen.ts` 写盘),
|
|
908
|
+
* 任何日志都只记尺寸 / 耗时 / 字节数。
|
|
909
|
+
*/
|
|
910
|
+
generateImage(sessionCookie: string, req: {
|
|
911
|
+
prompt: string;
|
|
912
|
+
size?: string;
|
|
913
|
+
quality?: string;
|
|
914
|
+
}, keyId?: string, timeoutMs?: number): Promise<GatewayImageGenerateResult>;
|
|
915
|
+
/**
|
|
916
|
+
* GET /api/me/images:当前账号的生图历史分页列表(**既有**员工自助端点,管理台「我的
|
|
917
|
+
* 图片」也在用;桌面端画廊页只是第二个消费方)。
|
|
918
|
+
*
|
|
919
|
+
* 与技能/语音那几条同理**不复用 `getMeJson`**(403 是「没分配 KEY」,与这里无关)。
|
|
920
|
+
* 这条路径上只有 401 有独立语义,其余一律 `GATEWAY_UNREACHABLE`。
|
|
921
|
+
*
|
|
922
|
+
* 形状收敛在本层一次做完:`page_size` → `pageSize`,行的 snake_case 字段 → camelCase,
|
|
923
|
+
* 并**丢掉 `file_url`**(浏览器拿不到会话 cookie,看单张一律走 {@link fetchMyImage})。
|
|
924
|
+
* 分页缺省由网关决定(它按自己的缺省值翻页),这里原样回显它给的值。
|
|
925
|
+
*/
|
|
926
|
+
getMyImages(sessionCookie: string, req?: {
|
|
927
|
+
page?: number;
|
|
928
|
+
pageSize?: number;
|
|
929
|
+
}): Promise<GatewayImagesView>;
|
|
930
|
+
/**
|
|
931
|
+
* GET /api/me/images/:id/file:取一张图片的本体(网关侧代理流式下发,无预签名直链)。
|
|
932
|
+
*
|
|
933
|
+
* 与「我的图片」列表同款**不复用 `getMeJson`**。这条路径上的拒绝是有语义的、必须分开:
|
|
934
|
+
* - 404 = 这张图不存在(已被清理)→ `IMAGE_NOT_FOUND`;
|
|
935
|
+
* - 403 = 这张图不在当前账号名下(网关按「该用户的 Key 列表」判定)→ `IMAGE_FORBIDDEN`;
|
|
936
|
+
* - 401 = 会话过期。
|
|
937
|
+
*
|
|
938
|
+
* **体积闸**:超过 20MiB 直接拒(`IMAGE_TOO_LARGE`),**先按 `content-length` 预判**,
|
|
939
|
+
* 读回来再复核一次——绝不把几十 MB 的 base64 塞进 loopback RPC 信封。
|
|
940
|
+
* (20MiB 这个值与 `src/imagegen.ts` 的 `MAX_IMAGE_BYTES` 是同一个跨层常量:那边是
|
|
941
|
+
* 业务上限的单一事实源,这边是 HTTP 读取侧的防线,两处都写死以免任一侧被悄悄放宽。)
|
|
942
|
+
*/
|
|
943
|
+
fetchMyImage(sessionCookie: string, id: number): Promise<GatewayImageFile>;
|
|
691
944
|
/**
|
|
692
945
|
* /api/me 自助端点的 GET 公共路径:网络异常 → GATEWAY_UNREACHABLE,
|
|
693
946
|
* 401/403/400 映射为稳定 code,响应体非 JSON 时不把原文带进异常。
|