hiwork-core 0.0.22 → 0.0.29
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 +20 -4
- package/lib/ai-router-client.js +142 -1
- package/lib/client.js +961 -33
- package/lib/index.js +2185 -353
- package/lib/knowledge-config.js +273 -0
- package/lib/pms-client.js +216 -61
- package/lib/pms-todo-bridge.js +332 -313
- package/lib/types/ai-router-client.d.ts +61 -0
- package/lib/types/client/contracts.d.ts +10 -8
- package/lib/types/client/hero-greeting.d.ts +0 -11
- package/lib/types/client/pms-todo.d.ts +67 -49
- package/lib/types/client/styles.d.ts +28 -0
- package/lib/types/client/types.d.ts +13 -1
- package/lib/types/client/voice/MicButton.d.ts +24 -0
- package/lib/types/client/voice/VoiceOverlay.d.ts +37 -0
- package/lib/types/client/voice/composer.d.ts +98 -0
- package/lib/types/client/voice/entry.d.ts +46 -0
- package/lib/types/client/voice/recording.d.ts +57 -0
- package/lib/types/client/voice/voice-data.d.ts +101 -0
- package/lib/types/client/voice/voice-styles.d.ts +9 -0
- package/lib/types/index.d.ts +3 -1
- package/lib/types/knowledge-config.d.ts +115 -0
- package/lib/types/pms-client.d.ts +99 -34
- package/lib/types/pms-todo-bridge.d.ts +210 -184
- package/lib/types/voice-rpc.d.ts +133 -0
- package/lib/types/web-search.d.ts +123 -0
- package/lib/voice-rpc.js +211 -0
- package/lib/web-search.js +259 -0
- package/package.json +5 -3
package/cordis.patch.yml
CHANGED
|
@@ -15,15 +15,31 @@
|
|
|
15
15
|
- id: llm-deepseek
|
|
16
16
|
disabled: true
|
|
17
17
|
|
|
18
|
-
#
|
|
19
|
-
#
|
|
20
|
-
#
|
|
18
|
+
# 官方 web 搜索(web-search-deepseek)走 DEEPSEEK_API_KEY,企业用户没有该 Key,
|
|
19
|
+
# 调用必然失败,继续停用(设置页那张「Web 搜索」卡片随之消失)。
|
|
20
|
+
# 联网搜索由本插件自带的受管 provider 接管:web_search 工具执行时调网关统一搜索
|
|
21
|
+
# 端点(POST /api/me/web-search),服务商凭据只在网关、客户端对服务商无感知
|
|
22
|
+
# (见 src/web-search.ts 与下面 web / tool-web 两行)。
|
|
21
23
|
- id: web-search-deepseek
|
|
22
24
|
disabled: true
|
|
23
25
|
|
|
26
|
+
# 把 web_search 的后端 pin 到本插件注册的 `hiwork-managed-search`。
|
|
27
|
+
# ⚠️ bundle patch 对 config 是**整行替换**不是合并:基座此行的原值是
|
|
28
|
+
# `{ searchProvider: deepseek-official, fetchProvider: http }`,两个 pin 必须一并重述,
|
|
29
|
+
# 否则 fetch 的 pin 会被顶掉。
|
|
30
|
+
- id: web
|
|
31
|
+
config:
|
|
32
|
+
searchProvider: hiwork-managed-search
|
|
33
|
+
fetchProvider: http
|
|
34
|
+
|
|
35
|
+
# web_search 工具翻回开启(此前 search: false)。⚠️ 这里同样是整行替换:
|
|
36
|
+
# 基座原值 `{ fetch: true, searchTimeoutMs: 60000 }` 一并写全,缺了就回落 schema
|
|
37
|
+
# 缺省(fetch 缺省恰好也是 true,但 searchTimeoutMs 缺省 30s,会悄悄缩水)。
|
|
24
38
|
- id: tool-web
|
|
25
39
|
config:
|
|
26
|
-
|
|
40
|
+
fetch: true
|
|
41
|
+
search: true
|
|
42
|
+
searchTimeoutMs: 60000
|
|
27
43
|
|
|
28
44
|
- insert:
|
|
29
45
|
- id: hiwork-core
|
package/lib/ai-router-client.js
CHANGED
|
@@ -56,6 +56,38 @@ const ME_OFFICE_CONFIG_PATH = '/api/me/office-config';
|
|
|
56
56
|
* 桌面端永远不接触。
|
|
57
57
|
*/
|
|
58
58
|
const ME_OFFICE_RELAY_DOCUMENTS_PATH = '/api/me/office-relay/documents';
|
|
59
|
+
/**
|
|
60
|
+
* 语音识别中继端点(浏览器侧话筒录音 → 网关 → 上游 ASR)。
|
|
61
|
+
*
|
|
62
|
+
* 两条都在网关 `/api/me` 会话组下,鉴权只有 `SessionAuth`(只读 Cookie)——与 MCP / 技能清单
|
|
63
|
+
* 同理,**一个还没被分配虚拟 Key 的账号照样应当能用语音识别**(分能力与分 Key 是两件事),
|
|
64
|
+
* 所以这两个方法都不能复用 `getMeJson`(那条路径把 403 读成「没分配 KEY」,见
|
|
65
|
+
* {@link AiRouterClient.getMyVoiceConfig})。
|
|
66
|
+
*
|
|
67
|
+
* `voice-config` 下发「开关 + 单次录音时长上限」,`voice-transcribe` 搬一次录音(base64);
|
|
68
|
+
* 网关侧再对上游持凭据,桌面端与浏览器都拿不到任何 ASR 服务的地址或密钥。
|
|
69
|
+
*/
|
|
70
|
+
const ME_VOICE_CONFIG_PATH = '/api/me/voice-config';
|
|
71
|
+
const ME_VOICE_TRANSCRIBE_PATH = '/api/me/voice-transcribe';
|
|
72
|
+
/**
|
|
73
|
+
* `voice-transcribe` 的单次请求超时:默认 10s 只够一次普通的 JSON 往返,而这条要
|
|
74
|
+
* 「上传一段录音 + 等上游识别」,公网 + 冷启动时 10s 会稳定误判成网关不可达。
|
|
75
|
+
* 只放宽这一条(见 `AiRouterClient.transcribeVoice`),其余端点仍是构造时的超时。
|
|
76
|
+
*/
|
|
77
|
+
const VOICE_TRANSCRIBE_TIMEOUT_MS = 60_000;
|
|
78
|
+
/** `voice-config` 响应缺字段 / 类型不符时的时长上限兜底(秒)。 */
|
|
79
|
+
const VOICE_CONFIG_FALLBACK_MAX_SECONDS = 60;
|
|
80
|
+
/** 语音识别端点各稳定 code 的本地兜底文案(网关响应体没有可展示的 message 时用)。 */
|
|
81
|
+
const VOICE_ERROR_FALLBACK_MESSAGE = {
|
|
82
|
+
VOICE_DISABLED: '本部署未启用语音识别,请联系管理员',
|
|
83
|
+
VOICE_NOT_CONFIGURED: '本部署尚未配置语音识别服务,请联系管理员',
|
|
84
|
+
VOICE_LIMIT_REACHED: '语音识别请求过于频繁,请稍后再试',
|
|
85
|
+
VOICE_AUDIO_TOO_LARGE: '录音过长或文件过大,请缩短后重试',
|
|
86
|
+
VOICE_BAD_REQUEST: '录音数据不合法,请重新录制',
|
|
87
|
+
VOICE_UPSTREAM: '语音识别服务暂时不可用,请稍后重试',
|
|
88
|
+
};
|
|
89
|
+
/** 语音识别端点未知 code 的兜底文案(不暴露任何上游细节)。 */
|
|
90
|
+
const VOICE_ERROR_DEFAULT_MESSAGE = '语音识别失败,请稍后重试';
|
|
59
91
|
/** 授权请求默认值:桌面端 client_id 与自定义协议回调地址(网关侧精确匹配)。 */
|
|
60
92
|
const DEFAULT_CLIENT_ID = 'hiwork-desktop';
|
|
61
93
|
const DEFAULT_REDIRECT_URI = 'hiwork://auth/callback';
|
|
@@ -513,6 +545,92 @@ export class AiRouterClient {
|
|
|
513
545
|
throw new GatewayError('GATEWAY_UNREACHABLE', `网关返回错误(HTTP ${res.status})`, res.status);
|
|
514
546
|
return await parseMeJson(res);
|
|
515
547
|
}
|
|
548
|
+
/**
|
|
549
|
+
* GET /api/me/voice-config:当前账号的语音识别开关与录音时长上限。
|
|
550
|
+
*
|
|
551
|
+
* 与每日一句的 `getMyDailyQuote` 同款,**这两个方法不在 {@link GatewayClient} 接口上**:
|
|
552
|
+
* 那个接口是编排层的端口,「语音识别」与它无关,加进去只会逼着每处编排层假网关
|
|
553
|
+
* (tests/orchestration.spec.ts 里一堆)都补两个用不到的桩。频道侧改用结构子集
|
|
554
|
+
* `VoiceGatewayPort`(见 `src/voice-rpc.ts`),与 usage / daily 三处一致。
|
|
555
|
+
*
|
|
556
|
+
* 与 {@link getMyMcpAssignments} 同样的理由**不复用 `getMeJson`**:那条路径把 403 映射成
|
|
557
|
+
* `NO_KEY_ASSIGNED`(「尚未被分配虚拟 KEY」),而本端点的鉴权只有会话——一个还没被分配
|
|
558
|
+
* Key 的账号照样应该有语音识别(分能力与分 Key 是两件事)。沿用它的文案会把「没有 Key」
|
|
559
|
+
* 错报成原因。
|
|
560
|
+
*
|
|
561
|
+
* 本端点契约**恒 200**(开关开与关都是 200 + `enabled` 布尔),因此除 401 以外没有任何
|
|
562
|
+
* 需要单独解释的拒绝语义:403 / 404 / 5xx / 反代错误页一律收敛成 `GATEWAY_UNREACHABLE`。
|
|
563
|
+
*
|
|
564
|
+
* 形状收敛(**本层负责**,消费方拿到的永远是一个合法的 {@link GatewayVoiceConfig}):
|
|
565
|
+
* 网关线上是 snake_case `max_audio_seconds`,这里转 camelCase;字段缺失 / 类型不符
|
|
566
|
+
* 按「禁用 + 60 秒」处理——配置读不出来时话筒本就该消失,用一个 undefined 让消费方
|
|
567
|
+
* 去猜是把形状问题推给下游。真正的「读不到就回落」再往上一层(`src/voice-rpc.ts`)。
|
|
568
|
+
*/
|
|
569
|
+
async getMyVoiceConfig(sessionCookie) {
|
|
570
|
+
const res = await this.requestOrUnreachable(ME_VOICE_CONFIG_PATH, { method: 'GET', sessionCookie });
|
|
571
|
+
if (res.status === 401)
|
|
572
|
+
throw new GatewayError('SESSION_EXPIRED', '网关会话已过期,请重新登录', 401);
|
|
573
|
+
if (!res.ok)
|
|
574
|
+
throw new GatewayError('GATEWAY_UNREACHABLE', `网关返回错误(HTTP ${res.status})`, res.status);
|
|
575
|
+
const data = await parseMeJson(res);
|
|
576
|
+
const seconds = data.max_audio_seconds;
|
|
577
|
+
return {
|
|
578
|
+
enabled: data.enabled === true,
|
|
579
|
+
maxDurationSeconds: typeof seconds === 'number' && Number.isFinite(seconds) && seconds > 0
|
|
580
|
+
? seconds
|
|
581
|
+
: VOICE_CONFIG_FALLBACK_MAX_SECONDS,
|
|
582
|
+
};
|
|
583
|
+
}
|
|
584
|
+
/**
|
|
585
|
+
* POST /api/me/voice-transcribe:把一段录音(base64)交给网关转发上游做识别。
|
|
586
|
+
*
|
|
587
|
+
* 状态码映射与服务端 handler 一一对应,**不许合并**:
|
|
588
|
+
* - 401 = 会话过期(唯一一条能让人做点什么——重新登录——的拒绝);
|
|
589
|
+
* - 503 = 网关自己发的「功能不可用」,再按响应体 `code` 细分:
|
|
590
|
+
* `VOICE_NOT_CONFIGURED`(部署没配上游)/ `VOICE_DISABLED`(管理员把开关关了)。
|
|
591
|
+
* 两者给的提示不同(该找管理员配服务 vs 该找管理员开开关)。
|
|
592
|
+
* ⚠️ 响应体没有 `code`(非 JSON / 反代错误页)也归 `VOICE_DISABLED`——这条路由的
|
|
593
|
+
* 503 语义就是「没开」,而**上游故障走 502**(见下),不会伪装成 503;
|
|
594
|
+
* - 429 / 413 / 400 = 配额 / 超长 / 请求体不合法,三种不同的事,分开映射(合并后
|
|
595
|
+
* 客户端只会看到一句「识别失败」,413 的人不知道该录短一点);
|
|
596
|
+
* - 其余(含 502 = 上游出错)→ `VOICE_UPSTREAM`。
|
|
597
|
+
*
|
|
598
|
+
* message 一律取网关响应体里的 `message`(网关侧是冻结的中文文案,会直接展示给用户);
|
|
599
|
+
* 响应体不是合法 JSON / 缺 message 时退回本地兜底文案。**绝不把上游 / 网关原文回显**:
|
|
600
|
+
* 读取响应体走 {@link readVoiceError}(解析失败即空对象),与 `parseMeJson` 不回显原文同源。
|
|
601
|
+
*
|
|
602
|
+
* 超时**单独放宽到 60s**(见 {@link VOICE_TRANSCRIBE_TIMEOUT_MS}):这条要上传录音 +
|
|
603
|
+
* 等上游识别,构造时的 10s 默认值在这里是误报源。
|
|
604
|
+
*/
|
|
605
|
+
async transcribeVoice(sessionCookie, audioBase64) {
|
|
606
|
+
const res = await this.requestOrUnreachable(ME_VOICE_TRANSCRIBE_PATH, {
|
|
607
|
+
method: 'POST',
|
|
608
|
+
sessionCookie,
|
|
609
|
+
body: { audio_base64: audioBase64 },
|
|
610
|
+
timeoutMs: VOICE_TRANSCRIBE_TIMEOUT_MS,
|
|
611
|
+
});
|
|
612
|
+
if (res.status === 401)
|
|
613
|
+
throw new GatewayError('SESSION_EXPIRED', '网关会话已过期,请重新登录', 401);
|
|
614
|
+
if (!res.ok) {
|
|
615
|
+
const body = await readVoiceError(res);
|
|
616
|
+
if (res.status === 503) {
|
|
617
|
+
const code = body.code === 'VOICE_NOT_CONFIGURED' ? 'VOICE_NOT_CONFIGURED' : 'VOICE_DISABLED';
|
|
618
|
+
throw new GatewayError(code, voiceMessage(code, body), 503);
|
|
619
|
+
}
|
|
620
|
+
if (res.status === 429)
|
|
621
|
+
throw new GatewayError('VOICE_LIMIT_REACHED', voiceMessage('VOICE_LIMIT_REACHED', body), 429);
|
|
622
|
+
if (res.status === 413)
|
|
623
|
+
throw new GatewayError('VOICE_AUDIO_TOO_LARGE', voiceMessage('VOICE_AUDIO_TOO_LARGE', body), 413);
|
|
624
|
+
if (res.status === 400)
|
|
625
|
+
throw new GatewayError('VOICE_BAD_REQUEST', voiceMessage('VOICE_BAD_REQUEST', body), 400);
|
|
626
|
+
throw new GatewayError('VOICE_UPSTREAM', voiceMessage('VOICE_UPSTREAM', body), res.status);
|
|
627
|
+
}
|
|
628
|
+
const data = await parseMeJson(res);
|
|
629
|
+
// 缺 `text` / 类型不符按空文本处理(网关契约违约):UI 上「什么都没识别到」与
|
|
630
|
+
// 「识别失败」都是一句话,不值得为它抛一条错误压住已经拿到的结果。**只有字符串
|
|
631
|
+
// 才回显**,绝不把非字符串字段(可能是对象 / 上游原文片段)塞进客户端。
|
|
632
|
+
return { text: typeof data.text === 'string' ? data.text : '' };
|
|
633
|
+
}
|
|
516
634
|
/**
|
|
517
635
|
* /api/me 自助端点的 GET 公共路径:网络异常 → GATEWAY_UNREACHABLE,
|
|
518
636
|
* 401/403/400 映射为稳定 code,响应体非 JSON 时不把原文带进异常。
|
|
@@ -554,7 +672,7 @@ export class AiRouterClient {
|
|
|
554
672
|
const init = {
|
|
555
673
|
method: opts.method,
|
|
556
674
|
headers,
|
|
557
|
-
signal: AbortSignal.timeout(this.timeoutMs),
|
|
675
|
+
signal: AbortSignal.timeout(opts.timeoutMs ?? this.timeoutMs),
|
|
558
676
|
};
|
|
559
677
|
// Uint8Array 作为 BodyInit 需要 ArrayBuffer 视图(TS DOM 类型要求)
|
|
560
678
|
if (opts.rawBody !== undefined) {
|
|
@@ -661,6 +779,29 @@ async function parseMeJson(res) {
|
|
|
661
779
|
}
|
|
662
780
|
return data;
|
|
663
781
|
}
|
|
782
|
+
/**
|
|
783
|
+
* 读语音识别端点的错误响应体:只取字符串形态的 `code` 与 `message`,其余(含解析失败)
|
|
784
|
+
* 一律当作「什么都没给」。**不抛错、不记录、不回显任何原文**——上游 / 网关的响应片段
|
|
785
|
+
* 可能含录音内容或内部地址,它绝不能进异常信息或日志(与 `readJson` 的理由同源)。
|
|
786
|
+
*/
|
|
787
|
+
async function readVoiceError(res) {
|
|
788
|
+
const data = await readJson(res);
|
|
789
|
+
if (typeof data !== 'object' || data === null)
|
|
790
|
+
return {};
|
|
791
|
+
const record = data;
|
|
792
|
+
return {
|
|
793
|
+
...(typeof record.code === 'string' && record.code !== '' ? { code: record.code } : {}),
|
|
794
|
+
...(typeof record.message === 'string' && record.message.trim() !== '' ? { message: record.message.trim() } : {}),
|
|
795
|
+
};
|
|
796
|
+
}
|
|
797
|
+
/**
|
|
798
|
+
* 错误文案:优先用网关响应体里的 `message`(网关侧是**冻结的中文文案**,就是要给用户看的),
|
|
799
|
+
* 没有则用该 code 的本地兜底。网关 message 的消毒(baseURL / HTTP 细节不进 UI)在消费端
|
|
800
|
+
* `/hiwork-voice` 的 Host 半边(`src/voice-rpc.ts` 的 `displayVoiceGatewayMessage`)统一做。
|
|
801
|
+
*/
|
|
802
|
+
function voiceMessage(code, body) {
|
|
803
|
+
return body.message ?? VOICE_ERROR_FALLBACK_MESSAGE[code] ?? VOICE_ERROR_DEFAULT_MESSAGE;
|
|
804
|
+
}
|
|
664
805
|
/** 判定 400 响应体是否为 invalid_grant(只读 error 字段)。 */
|
|
665
806
|
function isInvalidGrant(payload) {
|
|
666
807
|
return typeof payload === 'object' && payload !== null && payload.error === 'invalid_grant';
|