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
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 浏览器侧麦克风采集与重采样(改编自 dsh 实验区 client-ui-voice-input 的 audio.ts,
|
|
3
|
+
* 采集/重采样/编码逻辑保持同构,去掉了对其 Remote 面的耦合)。
|
|
4
|
+
*
|
|
5
|
+
* 管线:getUserMedia → MediaRecorder(整段) → decodeAudioData →
|
|
6
|
+
* OfflineAudioContext 重采样 16k 单声道 → PCM16 WAV → base64。
|
|
7
|
+
* 豆包「录音文件识别极速版」吃整段音频,所以这里录完再一次性编码,不做流式。
|
|
8
|
+
*/
|
|
9
|
+
/** 采集失败种类:消息由 UI 层展示(本模块只带 kind)。 */
|
|
10
|
+
export declare class RecordingError extends Error {
|
|
11
|
+
readonly kind: 'unavailable' | 'permission' | 'empty' | 'cancelled' | 'interrupted';
|
|
12
|
+
constructor(kind: 'unavailable' | 'permission' | 'empty' | 'cancelled' | 'interrupted');
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* 把单声道浮点采样编码成 PCM16 WAV(16 kHz、单声道、16 bit、little-endian)。
|
|
16
|
+
* @param samples - 重采样后的 16k 单声道采样。
|
|
17
|
+
*/
|
|
18
|
+
export declare function encodeWave(samples: Float32Array): Uint8Array<ArrayBuffer>;
|
|
19
|
+
/** WAV 字节 → base64(无 data URL 前缀;RPC JSON 载荷用)。 */
|
|
20
|
+
export declare function audioBase64(bytes: Uint8Array): string;
|
|
21
|
+
/**
|
|
22
|
+
* 判断本模块产出的 WAV(16k 单声道 PCM16)是否**接近全静音**。
|
|
23
|
+
*
|
|
24
|
+
* macOS 在 TCC 层拒掉麦克风权限时不报错,而是给一条内容全零的流——录音「成功」、
|
|
25
|
+
* 上传「成功」,服务端只能回「没有识别到有效语音」,用户被误导去查网络/密钥。
|
|
26
|
+
* 停录后就地检查,能把「系统权限被拒」和「真的没说话」在客户端分开。
|
|
27
|
+
* @param threshold - 采样绝对阈值(0~32767),低于它视为静音。
|
|
28
|
+
*/
|
|
29
|
+
export declare function isSilentWav(bytes: Uint8Array, threshold?: number): boolean;
|
|
30
|
+
/** 一次麦克风采集会话;含「授权弹窗还没落定用户就点了取消」的竞态处理。 */
|
|
31
|
+
export declare class Recording {
|
|
32
|
+
private readonly onDispose;
|
|
33
|
+
private stream;
|
|
34
|
+
private recorder;
|
|
35
|
+
private context;
|
|
36
|
+
private analyser;
|
|
37
|
+
private samples;
|
|
38
|
+
private chunks;
|
|
39
|
+
private readonly lifetime;
|
|
40
|
+
private disposal;
|
|
41
|
+
constructor(onDispose: () => void);
|
|
42
|
+
/**
|
|
43
|
+
* 申请麦克风并开始录音。
|
|
44
|
+
* @param onError - 采集途中的失败回调(在异步资源释放完成前触发)。
|
|
45
|
+
*/
|
|
46
|
+
start(onError?: (error: RecordingError) => void): Promise<void>;
|
|
47
|
+
/** 读实时麦克风电平(RMS,0~1 粗略值);不在采集态时恒为 0。 */
|
|
48
|
+
amplitude(): number;
|
|
49
|
+
/**
|
|
50
|
+
* 结束录音并重采样成 WAV。
|
|
51
|
+
* @param maxDurationSeconds - 截掉超出上限的部分(定时器漂移保护)。
|
|
52
|
+
*/
|
|
53
|
+
stop(maxDurationSeconds: number): Promise<Uint8Array<ArrayBuffer>>;
|
|
54
|
+
/** 释放采集资源并作废未完成的授权竞态;重复调用安全。 */
|
|
55
|
+
dispose(): Promise<void>;
|
|
56
|
+
private release;
|
|
57
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 语音输入的客户端取数层:经 `/hiwork-voice` loopback RPC 问**网关中继**两件事——
|
|
3
|
+
* 「这台机器能不能用语音输入、单次最长多久」(`status`)与「把这段 WAV 转成文字」
|
|
4
|
+
* (`transcribe`)。
|
|
5
|
+
*
|
|
6
|
+
* 铁律(对齐 `usage-data.ts` / `daily-data.ts`):
|
|
7
|
+
* - 客户端**不持有**任何豆包凭据,也**不直接 fetch 火山**——录音以 WAV base64 经 RPC 送
|
|
8
|
+
* Host,由 Host 上传识别;网关侧的会话 cookie 同样只活在 Host 半边;
|
|
9
|
+
* - 信封 `{ok:true,value}` / `{ok:false,error:{code,message,details}}`:成功解出 value,
|
|
10
|
+
* 失败抛 {@link VoiceRpcError},**code 原样保留**——浮层文案按 code 映射
|
|
11
|
+
* ({@link voiceErrorMessageOf}),不去猜 message 里的措辞;
|
|
12
|
+
* - 形状不合法(不是信封、字段类型不对)一律抛 `internal`,绝不把未知结构当数据渲染。
|
|
13
|
+
*
|
|
14
|
+
* client bundle 纯度:本文件不 import 任何 `@deepseek-ai/*` 运行时值,也不 import Host 代码
|
|
15
|
+
* (`src/voice-rpc.ts` 的频道/端点/载荷形状在这里按结构重声明,字段名与冻结契约一致)。
|
|
16
|
+
*/
|
|
17
|
+
/** RPC 频道;必须与 Host `src/voice-rpc.ts` 的频道常量一致(由测试锁定)。 */
|
|
18
|
+
export declare const VOICE_CHANNEL = "/hiwork-voice";
|
|
19
|
+
/** 端点白名单;必须与 Host 的 `VOICE_RPC_ENDPOINTS` 一致(由测试锁定)。 */
|
|
20
|
+
export declare const VOICE_ENDPOINTS: readonly ["status", "transcribe"];
|
|
21
|
+
export type VoiceEndpoint = (typeof VOICE_ENDPOINTS)[number];
|
|
22
|
+
/**
|
|
23
|
+
* 单次时长上限的缺省值(秒)。
|
|
24
|
+
*
|
|
25
|
+
* 冻结契约:Host 在 `status` 失败时也回 `{enabled:false, maxDurationSeconds:60}`,
|
|
26
|
+
* 所以这个值只在**形状非法**时兜底——浮层绝不能因为一个坏字段就没有上限。
|
|
27
|
+
*/
|
|
28
|
+
export declare const VOICE_DEFAULT_MAX_DURATION_SECONDS = 60;
|
|
29
|
+
/** 浮层错误文案(**逐字冻结**,见迁移 spec 的 code → 文案映射表)。 */
|
|
30
|
+
export declare const VOICE_ERROR_TEXT_SESSION_EXPIRED = "\u767B\u5F55\u5DF2\u8FC7\u671F\uFF0C\u8BF7\u91CD\u65B0\u767B\u5F55\u540E\u518D\u8BD5";
|
|
31
|
+
export declare const VOICE_ERROR_TEXT_LIMIT_REACHED = "\u4ECA\u5929\u7684\u8BED\u97F3\u8BC6\u522B\u6B21\u6570\u5DF2\u7528\u5B8C\uFF0C\u660E\u5929\u518D\u6765";
|
|
32
|
+
export declare const VOICE_ERROR_TEXT_AUDIO_TOO_LARGE = "\u8FD9\u6BB5\u5F55\u97F3\u592A\u957F\u4E86\uFF0C\u8BF7\u5206\u6BB5\u8BF4\u5B8C";
|
|
33
|
+
export declare const VOICE_ERROR_TEXT_UPSTREAM = "\u8BED\u97F3\u8BC6\u522B\u670D\u52A1\u6682\u65F6\u4E0D\u53EF\u7528\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5";
|
|
34
|
+
/** 其余 code 与**网关不可达**共用的一句(`enabled:false` 也走它,见下方 status 契约)。 */
|
|
35
|
+
export declare const VOICE_ERROR_TEXT_UNREACHABLE = "\u8FDE\u63A5\u8BED\u97F3\u670D\u52A1\u5931\u8D25\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5";
|
|
36
|
+
/** 信封不是信封 / value 形状非法时的哨兵消息(用户可读的兜底在映射层)。 */
|
|
37
|
+
export declare const VOICE_INVALID_RESPONSE_TEXT = "\u8BED\u97F3\u670D\u52A1\u8FD4\u56DE\u4E86\u65E0\u6548\u54CD\u5E94\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5";
|
|
38
|
+
/** 宿主 loopback RPC 调用面(结构兼容 `ctx.connection.rpc`)。 */
|
|
39
|
+
export interface VoiceRpcPort {
|
|
40
|
+
call(channel: string, endpoint: string, payload: unknown, signal?: AbortSignal): Promise<unknown>;
|
|
41
|
+
}
|
|
42
|
+
/** 带稳定 code 的语音 RPC 错误(`message` 由 Host 消毒,可直接展示,但浮层走 code 映射)。 */
|
|
43
|
+
export declare class VoiceRpcError extends Error {
|
|
44
|
+
readonly code: string;
|
|
45
|
+
constructor(code: string, message: string);
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* 解开 DSH Connection 的结果信封。
|
|
49
|
+
* 宿主返回 `{ok:true,value}` / `{ok:false,error:{code,message,details}}`;形状不合法时抛
|
|
50
|
+
* `internal`,绝不把未知结构当数据渲染。
|
|
51
|
+
* @param value - `rpc.call` 的原始返回值。
|
|
52
|
+
* @returns 成功分支的 value。
|
|
53
|
+
*/
|
|
54
|
+
export declare function unwrapVoiceResult<T>(value: unknown): T;
|
|
55
|
+
/** `status` 端点视图:网关中继能不能用 + 单次录音时长上限。 */
|
|
56
|
+
export interface VoiceStatusView {
|
|
57
|
+
/** `false`(含网关不可达)时**不渲染话筒按钮**——见 `voice/entry.ts` 的门禁。 */
|
|
58
|
+
readonly enabled: boolean;
|
|
59
|
+
readonly maxDurationSeconds: number;
|
|
60
|
+
}
|
|
61
|
+
/** `transcribe` 端点视图;`text` 可能是空串(没听清),由调用方决定怎么提示。 */
|
|
62
|
+
export interface VoiceTranscriptView {
|
|
63
|
+
readonly text: string;
|
|
64
|
+
}
|
|
65
|
+
/** 组件消费的最小取数面(便于视图与测试注入假实现)。 */
|
|
66
|
+
export interface VoiceRuntime {
|
|
67
|
+
/** 取网关中继状态;形状非法按 `internal` 抛错(调用方各自降级)。 */
|
|
68
|
+
status(): Promise<VoiceStatusView>;
|
|
69
|
+
/** 送一段 WAV base64 转写;`signal` 取消时透传 AbortError。 */
|
|
70
|
+
transcribe(audioBase64: string, signal?: AbortSignal): Promise<VoiceTranscriptView>;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* 归一 `status` 响应:`enabled` 只认**严格 true**(缺省 / 非布尔一律视为不可用),
|
|
74
|
+
* `maxDurationSeconds` 只认有限正数,否则回落到 {@link VOICE_DEFAULT_MAX_DURATION_SECONDS}。
|
|
75
|
+
* 形状完全不是对象时抛 `internal`。
|
|
76
|
+
* @param value - 已解信封的 value。
|
|
77
|
+
*/
|
|
78
|
+
export declare function parseVoiceStatus(value: unknown): VoiceStatusView;
|
|
79
|
+
/** 归一 `transcribe` 响应:`text` 必填字符串(可为空串),否则 `internal`。 */
|
|
80
|
+
export declare function parseVoiceTranscript(value: unknown): VoiceTranscriptView;
|
|
81
|
+
/**
|
|
82
|
+
* 网关 code → 浮层文案(**逐字冻结**)。
|
|
83
|
+
*
|
|
84
|
+
* `SESSION_EXPIRED`(登录过期)、`VOICE_LIMIT_REACHED`(当日次数用尽)、
|
|
85
|
+
* `VOICE_AUDIO_TOO_LARGE`(单段太长)、`VOICE_UPSTREAM`(上游挂了)各自一句;
|
|
86
|
+
* **其余一切 code 以及网关不可达**共用「连接语音服务失败,请稍后重试」——
|
|
87
|
+
* 用户不需要知道是哪一层断的,只需要知道可以再试。
|
|
88
|
+
* @param error - 任意异常(非 {@link VoiceRpcError} 一律归入兜底文案)。
|
|
89
|
+
*/
|
|
90
|
+
export declare function voiceErrorMessageOf(error: unknown): string;
|
|
91
|
+
/**
|
|
92
|
+
* 这次失败是否值得保留录音并给「重试识别」:只有可能自愈的那几类才值得
|
|
93
|
+
* (登录过期 / 用量上限 / 音频超长 / 参数错 / 已取消——重试必然再失败一次)。
|
|
94
|
+
* @param error - transcribe 抛出的异常。
|
|
95
|
+
*/
|
|
96
|
+
export declare function voiceErrorRetryable(error: unknown): boolean;
|
|
97
|
+
/**
|
|
98
|
+
* 创建语音输入运行时。
|
|
99
|
+
* @param rpc - 宿主 loopback RPC(`ctx.connection.rpc`)。
|
|
100
|
+
*/
|
|
101
|
+
export declare function createVoiceRuntime(rpc: VoiceRpcPort): VoiceRuntime;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/** 注入 <style> 的固定 id,防止重复注入(HMR 后也只剩一份)。 */
|
|
2
|
+
export declare const VOICE_STYLE_ID = "hiwork-core-voice-styles";
|
|
3
|
+
/** 归属标记里的 css 标识,沿用官方客户端预设的 `包名/文件名` 形态。 */
|
|
4
|
+
export declare const VOICE_STYLE_TAG_ID = "hiwork-core/voice.css";
|
|
5
|
+
/**
|
|
6
|
+
* 注入语音输入样式;幂等(同 id 已存在则原样返回,不带标记的旧节点补上标记)。
|
|
7
|
+
* @param doc - 目标文档(测试可传仿真 DOM)。
|
|
8
|
+
*/
|
|
9
|
+
export declare function ensureVoiceStyle(doc: Document): void;
|
package/lib/types/index.d.ts
CHANGED
|
@@ -12,7 +12,9 @@ import { type CredentialsPort, type DefaultModelPort, type AiRouterStatePort } f
|
|
|
12
12
|
export declare const name = "hiwork-core";
|
|
13
13
|
/**
|
|
14
14
|
* 必需服务:`connection` 承载设置页「用量与成本」的 loopback RPC 频道
|
|
15
|
-
* (`/hiwork-usage`,见 `src/usage-rpc.ts
|
|
15
|
+
* (`/hiwork-usage`,见 `src/usage-rpc.ts`);`web` 承载联网搜索 provider 的注册
|
|
16
|
+
* (`ctx.web.registerSearchProvider`,见 `src/web-search.ts`;dsh-base 基座先于本
|
|
17
|
+
* 插件挂载 dsh-web);其余三项是既有登录 / 供给依赖。
|
|
16
18
|
*/
|
|
17
19
|
export declare const inject: string[];
|
|
18
20
|
/** 插件配置 + 运行时状态字段(全部可选;写操作按此 schema 校验)。 */
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/** cordis 服务名(hiwork-knowledge 的 Host 半边按此名 `ctx.get` 探测)。 */
|
|
2
|
+
export declare const HIWORK_KNOWLEDGE_SERVICE = "hiworkKnowledge";
|
|
3
|
+
/** 网关侧端点(跨仓冻结;与 `/api/me/memory-config` 同一条会话鉴权)。 */
|
|
4
|
+
export declare const ME_KNOWLEDGE_CONFIG_PATH = "/api/me/knowledge-config";
|
|
5
|
+
/** 成功结果的缓存 TTL:5 分钟。 */
|
|
6
|
+
export declare const KNOWLEDGE_CONFIG_TTL_MS: number;
|
|
7
|
+
/** 出网超时(与 `AiRouterClient` 的缺省一致)。 */
|
|
8
|
+
export declare const KNOWLEDGE_CONFIG_TIMEOUT_MS = 10000;
|
|
9
|
+
/**
|
|
10
|
+
* 失败原因(稳定枚举,消费方据此决定提示与自愈策略):
|
|
11
|
+
* - `not-configured`:管理员还没开启知识库(网关 503 `KNOWLEDGE_NOT_CONFIGURED`);
|
|
12
|
+
* - `unauthenticated`:没有可用会话(未登录 / 会话已失效);
|
|
13
|
+
* - `unreachable`:超时、网络不通、网关地址未配置(以及防御性的 504);
|
|
14
|
+
* - `error`:其余非 200、响应不符合契约、宿主内部故障等。
|
|
15
|
+
*/
|
|
16
|
+
export type HiworkKnowledgeFailureReason = 'not-configured' | 'unauthenticated' | 'unreachable' | 'error';
|
|
17
|
+
/**
|
|
18
|
+
* 对外视图(camelCase,插件侧唯一接口面):
|
|
19
|
+
* 成功时 `ok: true` 且带 `baseUrl` / `apiKey` / `agentId` / `chatModelId` / `tenantId`
|
|
20
|
+
* (后三个是**永远存在**的字符串,没有就是 `''` —— 插件侧不必再判 undefined);
|
|
21
|
+
* 失败时 `ok: false` 且带 `reason` / `message`,**不带任何半个字段**。
|
|
22
|
+
*/
|
|
23
|
+
export interface HiworkKnowledgeConfigView {
|
|
24
|
+
ok: boolean;
|
|
25
|
+
baseUrl?: string;
|
|
26
|
+
apiKey?: string;
|
|
27
|
+
agentId?: string;
|
|
28
|
+
chatModelId?: string;
|
|
29
|
+
tenantId?: string;
|
|
30
|
+
reason?: HiworkKnowledgeFailureReason;
|
|
31
|
+
message?: string;
|
|
32
|
+
}
|
|
33
|
+
/** 本服务对宿主的可调用面(只读、永不抛异常)。 */
|
|
34
|
+
export interface HiworkKnowledgeService {
|
|
35
|
+
/** 取当前登录员工的知识库配置;force=true 绕过缓存(设置页「重新获取配置」与 401 自愈)。 */
|
|
36
|
+
config(opts?: {
|
|
37
|
+
force?: boolean;
|
|
38
|
+
}): Promise<HiworkKnowledgeConfigView>;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* 网关侧原始响应(**snake_case**,跨仓冻结契约;本文件是唯一读它的地方)。
|
|
42
|
+
* 仅供文档与类型自证——真正的读取走宽容投影 {@link projectKnowledgeConfig}。
|
|
43
|
+
*/
|
|
44
|
+
export interface GatewayKnowledgeConfig {
|
|
45
|
+
base_url: string;
|
|
46
|
+
api_key: string;
|
|
47
|
+
/** 缺失 / 空 = `''`(**不阻止下发**:检索与浏览仍可用,只是问答不可用)。 */
|
|
48
|
+
agent_id?: string;
|
|
49
|
+
chat_model_id?: string;
|
|
50
|
+
tenant_id?: string;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* 传输层结果:**失败是数据而不是异常**(本模块不依赖调用方捕获异常来判失败)。
|
|
54
|
+
* - `kind: 'http'`:拿到了非 200 状态码;
|
|
55
|
+
* - `kind: 'network'`:超时 / 连接失败 / DNS / 请求被中断;
|
|
56
|
+
* - `kind: 'invalid'`:200 但响应体不是可用的 JSON 对象(契约破坏)。
|
|
57
|
+
*/
|
|
58
|
+
export type KnowledgeConfigOutcome = {
|
|
59
|
+
readonly ok: true;
|
|
60
|
+
readonly body: unknown;
|
|
61
|
+
} | {
|
|
62
|
+
readonly ok: false;
|
|
63
|
+
readonly kind: 'http';
|
|
64
|
+
readonly status: number;
|
|
65
|
+
} | {
|
|
66
|
+
readonly ok: false;
|
|
67
|
+
readonly kind: 'network';
|
|
68
|
+
} | {
|
|
69
|
+
readonly ok: false;
|
|
70
|
+
readonly kind: 'invalid';
|
|
71
|
+
};
|
|
72
|
+
/** 网关客户端的 knowledge-config 子集:只声明本模块真正调用的方法(便于单测打桩计数)。 */
|
|
73
|
+
export interface KnowledgeConfigGatewayPort {
|
|
74
|
+
/** 调 `GET /api/me/knowledge-config`,返回 snake_case 原始响应或一个稳定失败原因。 */
|
|
75
|
+
getMyKnowledgeConfig(sessionCookie: string): Promise<KnowledgeConfigOutcome>;
|
|
76
|
+
}
|
|
77
|
+
/** Host 侧取数依赖:会话解析 + 当前 baseURL 下的网关出网端口。 */
|
|
78
|
+
export interface KnowledgeConfigDeps {
|
|
79
|
+
/**
|
|
80
|
+
* 解析网关会话 cookie。未登录 / 会话缺失返回 undefined。
|
|
81
|
+
* **返回值只在本模块内部使用**(缓存判等 + 出网),绝不出现在视图与日志里。
|
|
82
|
+
*/
|
|
83
|
+
resolveSession(): Promise<string | undefined>;
|
|
84
|
+
/** 当前 baseURL 对应的网关出网端口;baseURL 未配置时返回 undefined。 */
|
|
85
|
+
gateway(): KnowledgeConfigGatewayPort | undefined;
|
|
86
|
+
/** Host 日志出口(只记事件与异常类型名,绝不记凭据 / URL / 响应内容)。 */
|
|
87
|
+
log?: ((message: string) => void) | undefined;
|
|
88
|
+
/** 时钟注入(测试用);缺省 `Date.now`。 */
|
|
89
|
+
now?: (() => number) | undefined;
|
|
90
|
+
}
|
|
91
|
+
/** `ctx.provide` 的最小端口(结构兼容 cordis Context,便于单测伪造)。 */
|
|
92
|
+
export interface KnowledgeConfigContext {
|
|
93
|
+
provide(name: string, value?: unknown): unknown;
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* 提供 host 侧服务 `hiworkKnowledge`;返回 disposer(交给 `ctx.effect` 管理生命周期,
|
|
97
|
+
* 插件卸载时服务随 fiber 一并注销)。
|
|
98
|
+
*/
|
|
99
|
+
export declare function provideKnowledgeConfig(ctx: KnowledgeConfigContext, deps: KnowledgeConfigDeps): () => void;
|
|
100
|
+
/**
|
|
101
|
+
* 本端点的出网实现(形状与 `AiRouterClient` 的 `request()` 同款:user-agent 带包版本、
|
|
102
|
+
* 会话 cookie 走 `cookie` 头、超时用 `AbortSignal.timeout`)。
|
|
103
|
+
*
|
|
104
|
+
* **不新造鉴权**:只复用 Host 侧已有的登录会话 cookie(由 `resolveSession` 从凭据库取),
|
|
105
|
+
* 没有任何密钥/令牌在这一层生成或保存。
|
|
106
|
+
*
|
|
107
|
+
* **永不抛异常、绝不回显响应原文**:失败收敛成 {@link KnowledgeConfigOutcome},日志与
|
|
108
|
+
* 异常里都不会出现 URL、状态码或响应体——响应体里带的是知识库凭据。
|
|
109
|
+
*
|
|
110
|
+
* `fetchImpl` / `timeoutMs` 仅用于测试注入。
|
|
111
|
+
*/
|
|
112
|
+
export declare function createKnowledgeConfigGateway(baseURL: string, opts?: {
|
|
113
|
+
timeoutMs?: number;
|
|
114
|
+
fetchImpl?: typeof fetch;
|
|
115
|
+
}): KnowledgeConfigGatewayPort;
|
|
@@ -13,8 +13,9 @@ export declare const PMS_BASE_URL = "https://pms.hivery.cn";
|
|
|
13
13
|
*/
|
|
14
14
|
export declare const PMS_REQUEST_TIMEOUT_MS = 10000;
|
|
15
15
|
/**
|
|
16
|
-
*
|
|
17
|
-
*
|
|
16
|
+
* 令牌的**兜底**有效期:10 小时(= PMS `jwt.expiration=36000000`)。
|
|
17
|
+
* 只在兑换响应缺 `expiresAt`(或它不是一个能解析的时间串)时用;正常路径一律按服务端给的那个
|
|
18
|
+
* ISO 串走(见 {@link PmsExchangeResult.expiresAt})。
|
|
18
19
|
*/
|
|
19
20
|
export declare const PMS_ACCESS_TOKEN_TTL_MS: number;
|
|
20
21
|
/** 本客户端只有三个错误码:凭据失效 / 上游不可用 / 权限不足。其余(输入不合法等)由 Bridge 层判。 */
|
|
@@ -25,7 +26,13 @@ export declare class PmsError extends Error {
|
|
|
25
26
|
readonly status?: number | undefined;
|
|
26
27
|
constructor(code: PmsErrorCode, message: string, status?: number | undefined);
|
|
27
28
|
}
|
|
28
|
-
/**
|
|
29
|
+
/**
|
|
30
|
+
* 未登录 / 凭据失效的文案(与 Bridge 侧 `UNAUTHORIZED` 同口径:引导重新输入,不静默失败)。
|
|
31
|
+
*
|
|
32
|
+
* ⚠️ 换令牌被拒(断言过期 / `aud` 不符 / 工号在 PMS 不存在)也归这个码:那时**该做的是重新登录
|
|
33
|
+
* HiWork 客户端**(断言来自网关会话),不是再输一次 PMS 密码(spec D8)——文案随 Bridge 面的错误
|
|
34
|
+
* 映射一起重写;本层先复用同一个常量,不为一次兑换失败给 `PmsErrorCode` 添新码。
|
|
35
|
+
*/
|
|
29
36
|
export declare const PMS_UNAUTHORIZED_MESSAGE = "\u767B\u5F55\u5DF2\u8FC7\u671F\uFF0C\u8BF7\u91CD\u65B0\u8F93\u5165 PMS \u8D26\u53F7\u5BC6\u7801";
|
|
30
37
|
/** 通用失败文案:不暴露任何 PMS 实现细节(baseURL / 路径 / 状态码 / 响应体)。 */
|
|
31
38
|
export declare const PMS_GENERIC_FAILURE_MESSAGE = "PMS \u6682\u65F6\u4E0D\u53EF\u7528\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5";
|
|
@@ -51,24 +58,20 @@ export declare const PMS_TASK_OPEN_STATUSES: readonly [0, 1, 3];
|
|
|
51
58
|
/** 「我的任务」的默认页大小(spec §3 D7)。50 条足够覆盖一个人的未完成任务,且一次请求就够。 */
|
|
52
59
|
export declare const PMS_TASK_PAGE_SIZE = 50;
|
|
53
60
|
/**
|
|
54
|
-
*
|
|
61
|
+
* 一次「网关断言 → PMS 令牌」兑换的结果(spec §4.1b 的响应投影)。
|
|
55
62
|
*
|
|
56
|
-
* `
|
|
57
|
-
*
|
|
63
|
+
* `token` **绝不出 Host**;`expiresAt` 是服务端给的 **ISO 串**、**原样透传**——mint 端点不属于 PMS 的
|
|
64
|
+
* `Result` 信封,它给的时间串格式由 PMS 侧定,本层不替它归一(把它折成绝对时刻是调用方的事;当年
|
|
65
|
+
* `PmsTokenSet` 用 epoch ms 是因为那个时刻是**我方**自己算的——`now + expiresIn`)。
|
|
66
|
+
* `userId` 是「我的任务」唯一的查询键(`assigneeId`,spec D7),`username` 是 PMS 工号(仅用于展示)。
|
|
67
|
+
* 其余字段(`hint` 等)忽略——演进纪律同 {@link PmsTodoItem}。
|
|
58
68
|
*/
|
|
59
|
-
export interface
|
|
60
|
-
|
|
61
|
-
/**
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
* PMS 的**用户 id**(登录响应 `data.userId`,spec §3 D7 要求登录时捕获)。
|
|
66
|
-
*
|
|
67
|
-
* 它**不是**凭据(是个人数字 id,PMS 侧内部的 `user_id`),但它是「我的任务」唯一的查询键
|
|
68
|
-
* (`assigneeId`),所以必须跟着会话一起存下来。`refresh` 的响应里也可能有,但**不以它为准**
|
|
69
|
-
* ——见 `src/pms-todo-bridge.ts` 的 `authorize`(刷新不改变 userId)。
|
|
70
|
-
*/
|
|
71
|
-
userId?: number;
|
|
69
|
+
export interface PmsExchangeResult {
|
|
70
|
+
token: string;
|
|
71
|
+
/** ISO 串(原样透传;缺失 / 解析不了时本层兜底成 `now + 10 小时` 的 ISO 串)。 */
|
|
72
|
+
expiresAt: string;
|
|
73
|
+
userId: number;
|
|
74
|
+
username: string;
|
|
72
75
|
}
|
|
73
76
|
/**
|
|
74
77
|
* 一条待办(**冻结契约**,spec §4.2 的 `TodoItem`,字段名与 PMS 的 `TodoTaskVO` 一一对应)。
|
|
@@ -207,21 +210,30 @@ export declare class PmsClient {
|
|
|
207
210
|
private readonly log;
|
|
208
211
|
constructor(options?: PmsClientOptions);
|
|
209
212
|
/**
|
|
210
|
-
* `POST
|
|
213
|
+
* `POST {mintUrl}`:用一张网关签发的 **120 秒身份断言**换一把 **10 小时 PMS JWT**
|
|
214
|
+
* (spec `2026-09-21-pms-zero-input-login-design.md` §4.1b)。
|
|
211
215
|
*
|
|
212
|
-
*
|
|
213
|
-
* -
|
|
214
|
-
*
|
|
215
|
-
*
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
*
|
|
216
|
+
* 与其余方法的两处形状差异都是契约给定的,不是本层偷懒:
|
|
217
|
+
* - **`mintUrl` 是绝对地址**,来自网关的连接器分配(`auth.mintUrl`)——它**不拼** {@link PMS_BASE_URL}:
|
|
218
|
+
* 那个常量是「PMS 部署地址」,而兑换端点由网关的分配清单说了算(本层不猜也不改写,见
|
|
219
|
+
* {@link PmsClient.send});
|
|
220
|
+
* - **不解 `Result` 信封**:成功体就是 `{token, expiresAt, userId, username}`,失败体是
|
|
221
|
+
* `{error:"assertion_rejected"}` 这类——两者都不是业务信封。
|
|
222
|
+
*
|
|
223
|
+
* 失败语义:
|
|
224
|
+
* - HTTP 401 / 403(断言过期、`aud` 不符、签名不过、工号在 PMS 不存在)→ `UNAUTHORIZED`:用户该做的
|
|
225
|
+
* 是**重新登录 HiWork 客户端**(断言来自网关会话),不是再输一次 PMS 密码(spec D8);
|
|
226
|
+
* - 其余(网络 / 超时 / 400 / 503 / 非 JSON / 2xx 却形状不符)→ `UPSTREAM_UNAVAILABLE`。
|
|
227
|
+
*
|
|
228
|
+
* **本方法不重试**:断言是一次性的(PMS 侧拿 `jti` 做 2 分钟防重放),要重试必须由调用方**换一张新
|
|
229
|
+
* 断言**再调——重发同一张只会换来第二个 403(spec §6)。
|
|
220
230
|
*
|
|
221
|
-
*
|
|
222
|
-
*
|
|
231
|
+
* @param mintUrl - 网关分配里的 `auth.mintUrl`(绝对 URL)。
|
|
232
|
+
* @param assertion - 网关签发的短命断言(**不进日志、不落盘**,用完即弃)。
|
|
233
|
+
* @param deviceRef - 设备标识串(可空;只进 PMS 侧审计,不参与鉴权)。
|
|
234
|
+
* @returns 令牌与它的元数据(`token` 交给 Host 内存,见 `src/pms-todo-bridge.ts`)。
|
|
223
235
|
*/
|
|
224
|
-
|
|
236
|
+
exchangeAssertion(mintUrl: string, assertion: string, deviceRef: string): Promise<PmsExchangeResult>;
|
|
225
237
|
/**
|
|
226
238
|
* `GET /api/todo/today`:今日待办(⚠️ 口径比名字宽:**今天 + 明天 + 无截止日期**都算,spec §D4)。
|
|
227
239
|
*
|
|
@@ -280,7 +292,25 @@ export declare class PmsClient {
|
|
|
280
292
|
*/
|
|
281
293
|
completeTask(accessToken: string, taskId: number): Promise<PmsTaskStatusResult>;
|
|
282
294
|
/**
|
|
283
|
-
*
|
|
295
|
+
* 发一次请求:拼地址、塞请求头、按 {@link PmsClientOptions.timeoutMs} 超时。**只有传输**。
|
|
296
|
+
*
|
|
297
|
+
* 与 {@link PmsClient.call} 的分工:`call` 负责 `Result` 信封与业务码,本方法不管响应体是什么形状
|
|
298
|
+
* ——换令牌那条路({@link PmsClient.exchangeAssertion})既不解信封也不认业务码,但**超时口径与请求头
|
|
299
|
+
* 必须与其余调用逐字相同**(spec §7:统一 10s、令牌只在有身份时发),所以两条路共用这一处,
|
|
300
|
+
* 而不是各抄一份、将来各自漂移。
|
|
301
|
+
*
|
|
302
|
+
* **重定向自己跟**(`redirect: 'manual'` + {@link redirectTargetOf}):`fetch` 的默认 `follow` 会把
|
|
303
|
+
* 重定向后的请求按 fetch 语义**把 POST 改成 GET**,于是请求落在一个只认 POST 的映射上、拿到一个
|
|
304
|
+
* 「200 + 业务错误信封」,看上去像上游坏了。判据与修法见 {@link PMS_MAX_REDIRECTS}。
|
|
305
|
+
*
|
|
306
|
+
* @param url - 相对路径(拼 {@link PmsClient.baseURL})或**绝对 URL**(原样使用)。
|
|
307
|
+
* @param opts - 方法 / 可选 JSON 体 / 可选 Bearer 令牌。
|
|
308
|
+
* @returns 原始 `Response`:状态码由调用方判定,本方法不把任何状态码当成错误。
|
|
309
|
+
*/
|
|
310
|
+
private send;
|
|
311
|
+
/**
|
|
312
|
+
* 发一次调用并解开 `Result` 信封。**PMS 的业务错误都在这里收敛**,返回值只有 `data`
|
|
313
|
+
* (传输层的失败已在 {@link PmsClient.send} 收敛)。
|
|
284
314
|
*
|
|
285
315
|
* 顺序是刻意的:**先看 HTTP 401,再解信封**——PMS 未登录时回的是 Spring Security 形状
|
|
286
316
|
* (`{path, code:"AUTH_001", message}`),它的 `message` 是给浏览器看跳登录页的,既不是业务文案
|
|
@@ -288,6 +318,41 @@ export declare class PmsClient {
|
|
|
288
318
|
* 误判成「上游故障」而让用户不知道要重新输入密码。
|
|
289
319
|
*/
|
|
290
320
|
private call;
|
|
291
|
-
/** 登录 / 刷新的 `data` → {@link PmsTokenSet}。200 却不给 token 视为契约破坏(上游不可用)。 */
|
|
292
|
-
private tokenSet;
|
|
293
321
|
}
|
|
322
|
+
/**
|
|
323
|
+
* 最多跟随的**重定向跳数**(超过即放弃,把最后那个 3xx 原样交给调用方 → 上游不可用)。
|
|
324
|
+
*
|
|
325
|
+
* 为什么要有这个上限:重定向跟随是本次加固里唯一一处「会替调用方多发一次请求」的地方,
|
|
326
|
+
* 上限定小了(2 跳)足以覆盖真实形态(`http://` 登记 → nginx 302 → `https://` 一跳;
|
|
327
|
+
* 见过的最多是「域名 → 规范域名 → 真实地址」两跳),定大了只会在配错时把断言连打出去。
|
|
328
|
+
*/
|
|
329
|
+
export declare const PMS_MAX_REDIRECTS = 2;
|
|
330
|
+
/** {@link redirectTargetOf} 只看这两个字段,避免把整个 `Response` 绑进签名(便于单测与复用)。 */
|
|
331
|
+
export interface RedirectSource {
|
|
332
|
+
readonly status: number;
|
|
333
|
+
readonly headers: {
|
|
334
|
+
get(name: string): string | null;
|
|
335
|
+
};
|
|
336
|
+
}
|
|
337
|
+
/**
|
|
338
|
+
* 决定这一跳要不要跟、跟去哪。`undefined` = **到此为止**(把当前这份 `Response` 交给调用方)。
|
|
339
|
+
*
|
|
340
|
+
* 背景(生产实测的故障):平台把服务登记成 `http://pms.hivery.cn/mcp/credential-exchange`,
|
|
341
|
+
* nginx 回 302 到 https,而 `fetch` 默认跟随时会按 fetch 语义**把 POST 改成 GET**——于是请求落在
|
|
342
|
+
* 一个只认 `@PostMapping` 的映射上,应用回「200 + `{"code":500}`」信封,桌面端只看到「待办暂时
|
|
343
|
+
* 拉不到」。所以这里手动跟随,并**保持方法与请求体**。
|
|
344
|
+
*
|
|
345
|
+
* 三条闸(都是「宁可不跟」的方向,因为跟着走的那个请求体里装的是**凭据**:mint 的 body 是 120 秒断言):
|
|
346
|
+
* - **状态码**:只有 {@link METHOD_PRESERVING_REDIRECTS} 里的码才跟;
|
|
347
|
+
* - **主机名必须相同**(端口与 scheme 可变,见下):跨主机名跟随等于把断言发给第三方;
|
|
348
|
+
* - **禁止 https → http 降级**(http → https 升级允许):凭据不该因为一次重定向而落到明文上。
|
|
349
|
+
*
|
|
350
|
+
* 端口为什么允许变:真实拓扑里「对外 443 / 内网 8080」是常态,主机名才是「我是不是在跟同一个部署
|
|
351
|
+
* 说话」的边界;scheme 从 http 升到 https(本次的真实形态)跨端口更常见。
|
|
352
|
+
*
|
|
353
|
+
* @param current - 本次请求实际打的地址(绝对 URL)。
|
|
354
|
+
* @param response - 它的响应(只读 `status` 与 `location`)。
|
|
355
|
+
* @param hop - 已经跟随过几跳(从 0 起)。
|
|
356
|
+
* @returns 下一跳的绝对地址;不跟时 `undefined`。
|
|
357
|
+
*/
|
|
358
|
+
export declare function redirectTargetOf(current: string, response: RedirectSource, hop: number): string | undefined;
|