hiwork-core 0.0.2 → 0.0.6
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 +16 -7
- package/cordis.patch.yml +21 -0
- package/lib/ai-router-client.js +258 -2
- package/lib/browser-login.js +241 -0
- package/lib/client-policy.js +82 -0
- package/lib/client.js +1062 -1002
- package/lib/daily-rpc.js +161 -0
- package/lib/default-model.js +39 -0
- package/lib/index.js +1158 -71
- package/lib/orchestration.js +45 -75
- package/lib/provision-session.js +85 -0
- package/lib/provision.js +74 -24
- package/lib/types/ai-router-client.d.ts +255 -3
- package/lib/types/browser-login.d.ts +99 -0
- package/lib/types/client/account-cache.d.ts +46 -0
- package/lib/types/client/controller.d.ts +68 -0
- package/lib/types/client/daily-card.d.ts +29 -20
- package/lib/types/client/daily-data.d.ts +116 -0
- package/lib/types/client/index.d.ts +1 -1
- package/lib/types/client/login-gate.d.ts +9 -0
- package/lib/types/client/session-cost.d.ts +119 -0
- package/lib/types/client/styles.d.ts +12 -0
- package/lib/types/client/types.d.ts +1 -1
- package/lib/types/client/usage-data.d.ts +276 -0
- package/lib/types/client/usage-section.d.ts +18 -0
- package/lib/types/client-policy.d.ts +51 -0
- package/lib/types/daily-rpc.d.ts +110 -0
- package/lib/types/default-model.d.ts +37 -0
- package/lib/types/index.d.ts +113 -0
- package/lib/types/orchestration.d.ts +28 -55
- package/lib/types/provision-session.d.ts +106 -0
- package/lib/types/provision.d.ts +48 -9
- package/lib/types/types.d.ts +12 -1
- package/lib/types/usage-rpc.d.ts +196 -0
- package/lib/usage-rpc.js +386 -0
- package/package.json +6 -2
package/lib/daily-rpc.js
ADDED
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `/hiwork-daily` loopback RPC 的 Host 实现(新建任务 hero 页「每日一句」的唯一取数口)。
|
|
3
|
+
*
|
|
4
|
+
* 设计要点(与 `src/usage-rpc.ts` 完全同构):
|
|
5
|
+
* - 只注册一个 channel,endpoint 白名单在 {@link DAILY_RPC_ENDPOINTS},未知 endpoint
|
|
6
|
+
* 直接 `bad-request`;
|
|
7
|
+
* - payload 严格校验:`today` 只接受空对象,`random` 只接受 `{ excludeId?: 正整数 }`,
|
|
8
|
+
* 任何其它字段 / 类型不符一律 `bad-request`;
|
|
9
|
+
* - **网关会话 cookie 绝不离开 Host**:调用方只拿到端点原始响应,cookie 只在
|
|
10
|
+
* `resolveSession()` 内部出现;
|
|
11
|
+
* - 网关 `GatewayError` 的稳定 code 原样透传,但 message 先经
|
|
12
|
+
* {@link displayDailyGatewayMessage} 消毒——baseURL / HTTP 状态码这类实现细节不进 UI;
|
|
13
|
+
* - 其余异常收敛为通用失败(code `internal`),Host 只记日志、不把堆栈或原始消息
|
|
14
|
+
* 交给客户端。
|
|
15
|
+
*
|
|
16
|
+
* 消毒逻辑是**同构复制**而非复用 usage-rpc 的导出:后者的兜底文案是用量域的
|
|
17
|
+
* (「用量数据暂时无法获取…」),复用会让每日一句的失败吐出错误域的文案;且本仓库的
|
|
18
|
+
* 频道模块本就各自自带一份同实现(usage-rpc 与 hiwork-automation 的 rpc.ts 亦然),
|
|
19
|
+
* 保持模块自包含。
|
|
20
|
+
*
|
|
21
|
+
* 本模块不 import cordis 运行时,只依赖结构化的 {@link DailyRpcContext} 端口,
|
|
22
|
+
* 便于单测直接伪造。
|
|
23
|
+
*/
|
|
24
|
+
import { GatewayError, } from "./ai-router-client.js";
|
|
25
|
+
/** RPC 频道;必须与 client 半边 `src/client/daily-data.ts` 的 `DAILY_CHANNEL` 一致(由测试锁定)。 */
|
|
26
|
+
export const DAILY_RPC_CHANNEL = '/hiwork-daily';
|
|
27
|
+
/** 端点白名单:当日条目 + 换一句(随机)。 */
|
|
28
|
+
export const DAILY_RPC_ENDPOINTS = ['today', 'random'];
|
|
29
|
+
/** 请求侧错误(只有 `bad-request`:未知端点 / 非法 payload)。 */
|
|
30
|
+
export class DailyRequestError extends Error {
|
|
31
|
+
code;
|
|
32
|
+
constructor(code, message) {
|
|
33
|
+
super(message);
|
|
34
|
+
this.code = code;
|
|
35
|
+
this.name = 'DailyRequestError';
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
/** 各稳定错误码的用户可见文案(网关 message 含实现细节时用它兜底)。 */
|
|
39
|
+
const DAILY_MESSAGE_FALLBACK = {
|
|
40
|
+
SESSION_EXPIRED: '登录状态已过期,请重新登录',
|
|
41
|
+
GATEWAY_UNREACHABLE: '网关暂时不可用,请稍后重试',
|
|
42
|
+
RATE_LIMITED: '请求过于频繁,请稍后再试',
|
|
43
|
+
NOT_FOUND: '网关未找到对应的数据',
|
|
44
|
+
AUTH_FAILED: '登录状态异常,请重新登录',
|
|
45
|
+
};
|
|
46
|
+
/** 通用失败文案:不暴露任何网关细节。 */
|
|
47
|
+
export const DAILY_GENERIC_FAILURE_MESSAGE = '每日一句暂时无法获取,请稍后重试';
|
|
48
|
+
/** 未登录 / 会话失效的统一文案(与用量频道的 `SESSION_EXPIRED` 口径一致)。 */
|
|
49
|
+
export const DAILY_SESSION_EXPIRED_MESSAGE = '登录状态已过期,请重新登录';
|
|
50
|
+
/**
|
|
51
|
+
* 网关 message 消毒(与 `usage-rpc.ts` 的 `displayGatewayMessage` 同实现):
|
|
52
|
+
* 含 baseURL / HTTP 状态码 / URL 的原始消息一律换成稳定文案,其余(网关自己写好的
|
|
53
|
+
* 中文业务文案)原样保留。code 不受影响,始终原样透传。
|
|
54
|
+
*/
|
|
55
|
+
export function displayDailyGatewayMessage(code, message) {
|
|
56
|
+
const text = typeof message === 'string' ? message.trim() : '';
|
|
57
|
+
if (text === '')
|
|
58
|
+
return DAILY_MESSAGE_FALLBACK[code] ?? DAILY_GENERIC_FAILURE_MESSAGE;
|
|
59
|
+
const leaksImplementationDetail = /https?:\/\//i.test(text)
|
|
60
|
+
|| /\bHTTP\s*\d{3}\b/i.test(text)
|
|
61
|
+
|| /\bbaseURL\b/i.test(text);
|
|
62
|
+
if (leaksImplementationDetail)
|
|
63
|
+
return DAILY_MESSAGE_FALLBACK[code] ?? DAILY_GENERIC_FAILURE_MESSAGE;
|
|
64
|
+
return text;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* payload 校验:`today` 只放行空对象,`random` 只放行 `{ excludeId?: 正整数 }`;
|
|
68
|
+
* 其余字段 / 类型不符一律 `bad-request`。
|
|
69
|
+
*/
|
|
70
|
+
export function parseDailyPayload(endpoint, raw) {
|
|
71
|
+
if (raw === undefined || raw === null)
|
|
72
|
+
return {};
|
|
73
|
+
if (typeof raw !== 'object' || Array.isArray(raw)) {
|
|
74
|
+
throw new DailyRequestError('bad-request', '每日一句请求参数不合法。');
|
|
75
|
+
}
|
|
76
|
+
const record = raw;
|
|
77
|
+
const keys = Object.keys(record);
|
|
78
|
+
if (endpoint === 'today') {
|
|
79
|
+
// today 无任何入参:出现任何字段都是客户端实现缺陷
|
|
80
|
+
if (keys.length > 0)
|
|
81
|
+
throw new DailyRequestError('bad-request', `每日一句请求包含未知字段 '${keys[0]}'。`);
|
|
82
|
+
return {};
|
|
83
|
+
}
|
|
84
|
+
for (const key of keys) {
|
|
85
|
+
if (key !== 'excludeId')
|
|
86
|
+
throw new DailyRequestError('bad-request', `每日一句请求包含未知字段 '${key}'。`);
|
|
87
|
+
}
|
|
88
|
+
const payload = {};
|
|
89
|
+
const rawExcludeId = record['excludeId'];
|
|
90
|
+
if (rawExcludeId !== undefined) {
|
|
91
|
+
if (typeof rawExcludeId !== 'number' || !Number.isInteger(rawExcludeId) || rawExcludeId <= 0) {
|
|
92
|
+
throw new DailyRequestError('bad-request', '每日一句请求的 excludeId 必须是正整数。');
|
|
93
|
+
}
|
|
94
|
+
payload.excludeId = rawExcludeId;
|
|
95
|
+
}
|
|
96
|
+
return payload;
|
|
97
|
+
}
|
|
98
|
+
/** 是否为白名单端点。 */
|
|
99
|
+
export function isDailyRpcEndpoint(endpoint) {
|
|
100
|
+
return DAILY_RPC_ENDPOINTS.includes(endpoint);
|
|
101
|
+
}
|
|
102
|
+
/** 取消判定(用函数包一层,避免 TS 把属性窄化固化到 `await` 之后)。 */
|
|
103
|
+
function isAborted(signal) {
|
|
104
|
+
return signal?.aborted === true;
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* 处理一次端点调用:返回 DSH Connection 的结果信封,**永不抛异常**。
|
|
108
|
+
* 导出以便单测直接驱动(无需伪造 `ctx.connection`)。
|
|
109
|
+
*/
|
|
110
|
+
export async function handleDailyEndpoint(endpoint, rawPayload, deps, signal) {
|
|
111
|
+
try {
|
|
112
|
+
if (isAborted(signal))
|
|
113
|
+
return failure('cancelled', '每日一句请求已取消。');
|
|
114
|
+
if (!isDailyRpcEndpoint(endpoint)) {
|
|
115
|
+
throw new DailyRequestError('bad-request', `未知的每日一句端点 '${endpoint}'`);
|
|
116
|
+
}
|
|
117
|
+
const payload = parseDailyPayload(endpoint, rawPayload);
|
|
118
|
+
const sessionCookie = await deps.resolveSession();
|
|
119
|
+
if (sessionCookie === undefined || sessionCookie === '') {
|
|
120
|
+
return failure('SESSION_EXPIRED', DAILY_SESSION_EXPIRED_MESSAGE);
|
|
121
|
+
}
|
|
122
|
+
const gateway = deps.gateway();
|
|
123
|
+
if (gateway === undefined) {
|
|
124
|
+
return failure('GATEWAY_UNREACHABLE', '网关地址未配置,请先在「网关模型」里完成登录');
|
|
125
|
+
}
|
|
126
|
+
if (isAborted(signal))
|
|
127
|
+
return failure('cancelled', '每日一句请求已取消。');
|
|
128
|
+
const value = await callEndpoint(gateway, sessionCookie, endpoint, payload);
|
|
129
|
+
return { ok: true, value };
|
|
130
|
+
}
|
|
131
|
+
catch (error) {
|
|
132
|
+
if (error instanceof DailyRequestError)
|
|
133
|
+
return failure(error.code, error.message);
|
|
134
|
+
if (error instanceof GatewayError) {
|
|
135
|
+
// code 原样透传(稳定契约),message 消毒后再给客户端。
|
|
136
|
+
return failure(error.code, displayDailyGatewayMessage(error.code, error.message));
|
|
137
|
+
}
|
|
138
|
+
deps.log?.(`hiwork-core: 每日一句 RPC '${endpoint}' 失败(${error instanceof Error ? error.name : typeof error})`);
|
|
139
|
+
return failure('internal', DAILY_GENERIC_FAILURE_MESSAGE);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
/** 单端点转发:返回网关原始响应,不做任何字段重包装(形状校验由客户端取数层负责)。 */
|
|
143
|
+
async function callEndpoint(gateway, sessionCookie, endpoint, payload) {
|
|
144
|
+
switch (endpoint) {
|
|
145
|
+
case 'today':
|
|
146
|
+
return await gateway.getMyDailyQuote(sessionCookie);
|
|
147
|
+
case 'random':
|
|
148
|
+
return await gateway.getMyDailyQuoteRandom(sessionCookie, payload.excludeId);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
function failure(code, message) {
|
|
152
|
+
return { ok: false, error: { code, message, details: {} } };
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* 注册 `/hiwork-daily` 频道,返回异步 disposer(交给 `ctx.effect` 管理生命周期)。
|
|
156
|
+
*/
|
|
157
|
+
export function registerDailyRpc(ctx, deps) {
|
|
158
|
+
const handler = (endpoint, payload, signal) => handleDailyEndpoint(endpoint, payload, deps, signal);
|
|
159
|
+
const remove = ctx.connection.rpc.handle(DAILY_RPC_CHANNEL, handler, { authority: 'loopback' });
|
|
160
|
+
return async () => { await remove(); };
|
|
161
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 默认模型校正。
|
|
3
|
+
*
|
|
4
|
+
* `agent-default-model` 是 dsh 的默认模型选择(provider + model),dsh-base 的组合默认值
|
|
5
|
+
* 指向官方 DeepSeek 提供方(`deepseek-official/deepseek-v4-flash`)。本产品停用了官方提供方、
|
|
6
|
+
* 只供给网关线路,所以全新安装或提供方/模型变动后,这个默认值可能指向一个不再注册的
|
|
7
|
+
* provider 或已下架的 model —— 新会话第一次发请求会 UNKNOWN_MODEL,用户必须先去模型选择器
|
|
8
|
+
* 手动选一次。供给成功后在这里校正它:当前选择仍可路由就不动,否则回落到兜底线路的第一个模型。
|
|
9
|
+
*
|
|
10
|
+
* 不写 composition 配置、只写用户层,所以用户自己选过的模型只要仍然可用就不会被覆盖。
|
|
11
|
+
*/
|
|
12
|
+
import { pickFallbackDefaultModel } from "./provision.js";
|
|
13
|
+
/** dsh-agent-default-model 的 settings 命名空间。 */
|
|
14
|
+
export const DEFAULT_MODEL_NS = 'agent-default-model';
|
|
15
|
+
/**
|
|
16
|
+
* 供给成功后校正默认模型。
|
|
17
|
+
* @param routeModels - 本次供给出的各线路模型归属。
|
|
18
|
+
* @param ports - settings / llm 端口。
|
|
19
|
+
* @returns 实际写入的新选择;无需变更时 undefined。
|
|
20
|
+
*/
|
|
21
|
+
export async function ensureDefaultModel(routeModels, ports) {
|
|
22
|
+
if (!ports.available())
|
|
23
|
+
return undefined;
|
|
24
|
+
const current = ports.read();
|
|
25
|
+
if (current !== undefined && await routable(current, routeModels, ports))
|
|
26
|
+
return undefined;
|
|
27
|
+
const next = pickFallbackDefaultModel(routeModels);
|
|
28
|
+
if (next === undefined)
|
|
29
|
+
return undefined;
|
|
30
|
+
await ports.write(next);
|
|
31
|
+
return next;
|
|
32
|
+
}
|
|
33
|
+
/** 当前选择是否仍可路由:本插件供给的线路内查表,其它提供方问 llm 注册表。 */
|
|
34
|
+
async function routable(selection, routeModels, ports) {
|
|
35
|
+
const models = routeModels[selection.provider];
|
|
36
|
+
if (models !== undefined)
|
|
37
|
+
return models.some((model) => model.id === selection.model);
|
|
38
|
+
return ports.isRoutableElsewhere(selection);
|
|
39
|
+
}
|