@tnnevol/dsh-zcode 0.0.0-stage → 6.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.
@@ -0,0 +1,244 @@
1
+ import { E as ZCodeEndpointPlan, o as ZCodeConsoleClient } from "./console-client-54wUcPDA.js";
2
+ import { t as ZCodeAuthService } from "./auth-B99K5y4S.js";
3
+ import { g as ZCodeModel, i as ZCodeCatalogService } from "./catalog-CnI_VsFm.js";
4
+ import { Api, Model, Provider } from "@earendil-works/pi-ai";
5
+ import { ReasoningEffortId } from "@deepseek-ai/dsh-llm";
6
+ import { Context } from "@deepseek-ai/cordis";
7
+ import { IncomingMessage } from "node:http";
8
+ import { AttachmentStore } from "@deepseek-ai/dsh-attachment";
9
+ import { ResolvedPiAiProviderProfile } from "@deepseek-ai/dsh-llm-pi-ai";
10
+ //#region src/contracts/preferences.d.ts
11
+ /** 对话输入区用量胶囊的显隐策略。 */
12
+ type ZCodeUsageVisibility = 'auto' | 'always' | 'off';
13
+ /** 持久化的插件偏好。 */
14
+ interface ZCodePreferences {
15
+ /** `auto`:仅选中 ZCode 模型时显示;`always`:常驻;`off`:隐藏并停止轮询。 */
16
+ readonly usageVisibility: ZCodeUsageVisibility;
17
+ /** 目录与额度的自动刷新间隔(分钟);0 表示关闭自动刷新。 */
18
+ readonly refreshMinutes: number;
19
+ /** 在模型选择器里隐藏的模型 id。 */
20
+ readonly hiddenModels: readonly string[];
21
+ /**
22
+ * 推理端点策略:`start-plan` 用账号 JWT 消耗套餐额度,`ultra` 用家族
23
+ * MaaS 令牌消耗按量余额。二者对第三方客户端的可达性不同,故由用户选择。
24
+ */
25
+ readonly endpointPlan: ZCodeEndpointPlan;
26
+ }
27
+ declare const DEFAULT_ZCODE_PREFERENCES: ZCodePreferences;
28
+ /** 合法刷新间隔范围(分钟);下限 1 避免把额度接口打成轮询。 */
29
+ declare const ZCODE_REFRESH_MINUTES: {
30
+ readonly min: 0;
31
+ readonly max: 240;
32
+ };
33
+ /** 规范化任意输入;非法字段回落到默认值,绝不抛给调用方。 */
34
+ declare function normalizePreferences(value: unknown): ZCodePreferences;
35
+ //#endregion
36
+ //#region src/host/preferences.d.ts
37
+ /** 偏好文件名(位于 DSH home 下)。 */
38
+ declare const ZCODE_PREFERENCES_FILENAME = ".dsh-zcode-preferences.json";
39
+ /** 偏好文件路径。 */
40
+ declare function zcodePreferencesPath(dshHome?: string): string;
41
+ /** 文件落盘的偏好存储。 */
42
+ declare class ZCodePreferencesStore {
43
+ readonly filename: string;
44
+ constructor(filename?: string);
45
+ /** 读取偏好;文件缺失或内容不可解析时返回默认值。 */
46
+ read(): Promise<ZCodePreferences>;
47
+ /** 合并写入偏好;返回落盘后的完整值。 */
48
+ write(patch: unknown): Promise<ZCodePreferences>;
49
+ }
50
+ //#endregion
51
+ //#region src/host/provider.d.ts
52
+ /**
53
+ * 把目录条目投影为 pi-ai 模型。
54
+ *
55
+ * @param model - 目录条目。
56
+ * @param baseURL - 覆盖端点(端点策略决定,见 `ZCodeEndpointPlan`)。
57
+ */
58
+ declare function toPiModel(model: ZCodeModel, baseURL?: string): Model<Api>;
59
+ /** 构建 provider。 */
60
+ declare function createZCodeProvider(models: readonly ZCodeModel[], resolveToken: () => Promise<string | undefined>, baseURL?: string): Provider;
61
+ /** 构建供应商 profile。 */
62
+ declare function createZCodeProfile(models: readonly ZCodeModel[], resolveToken: () => Promise<string | undefined>, headers?: Readonly<Record<string, string>>, baseURL?: string): ResolvedPiAiProviderProfile;
63
+ /**
64
+ * 固定显示名的 pi-ai 适配器。
65
+ *
66
+ * registry 在 `registerAdapter` 时立即调用 `providerInfo` 并**永久缓存**结果
67
+ * (`listProviders()` 之后只返回这份快照),而本插件的 profile 快照要等目录
68
+ * 首次刷新才有内容——默认实现那一刻会回落成 provider id,模型选择器的分组名
69
+ * 从此固定成 `dsh-zcode`。这里直接返回契约常量,不依赖任何运行期状态。
70
+ */
71
+ /** 运行时依赖。 */
72
+ interface ZCodeProviderRuntimeOptions {
73
+ /**
74
+ * 按端点策略返回可用凭据;未登录/缺凭据返回 undefined。
75
+ *
76
+ * `start-plan` 用账号 JWT(ZCode 域凭证);`ultra` 用家族 MaaS 令牌。
77
+ * 两种凭据由同一份登录记录提供,但被不同端点接受。
78
+ */
79
+ readonly resolveToken: (plan: ZCodeEndpointPlan) => Promise<string | undefined>;
80
+ /** 提供稳定的设备标识,用于构造路由头。 */
81
+ readonly deviceMid: () => string;
82
+ /** 读取宿主附件服务,供图片输入使用。 */
83
+ readonly resolveAttachments: () => AttachmentStore | undefined;
84
+ readonly logger: Pick<Context['logger'], 'warn' | 'info'>;
85
+ }
86
+ /** 路由状态,供详情页诊断。 */
87
+ interface ZCodeRouteStatus {
88
+ readonly id: string;
89
+ readonly name: string;
90
+ readonly registered: boolean;
91
+ readonly error?: string;
92
+ readonly modelCount: number;
93
+ /** 当前端点策略,便于详情页说明「请求会发到哪里、扣哪种额度」。 */
94
+ readonly endpointPlan?: ZCodeEndpointPlan;
95
+ }
96
+ /**
97
+ * 注册并持有唯一的 `dsh-zcode` 路由。
98
+ *
99
+ * 路由被其他适配器占用时记录原因并保持未注册状态,由详情页把冲突说明展示
100
+ * 给用户,而不是让整个插件失效。
101
+ */
102
+ declare class ZCodeProviderRuntime {
103
+ private readonly options;
104
+ private snapshot;
105
+ /** HTTP 通道:同时是模型元数据的唯一来源。 */
106
+ /** 执行通道:自行收发 Anthropic Messages(满足网关对 system 分块的要求)。 */
107
+ private readonly adapter;
108
+ private registration;
109
+ private registered;
110
+ private error;
111
+ private currentModels;
112
+ private endpointPlan;
113
+ constructor(options: ZCodeProviderRuntimeOptions);
114
+ /** 当前目录模型。 */
115
+ models(): readonly ZCodeModel[];
116
+ /** 路由注册状态。 */
117
+ routeStatus(): readonly ZCodeRouteStatus[];
118
+ /** 在插件 fiber 上注册路由。 */
119
+ register(ctx: Context): void;
120
+ /** 当前端点策略。 */
121
+ plan(): ZCodeEndpointPlan;
122
+ /**
123
+ * 用新目录与端点策略重建 profile;下一次 LLM 操作即可见。
124
+ *
125
+ * 端点策略决定 baseURL 与凭据类型,两者必须一起换:只换端点不换凭据会直接
126
+ * 401(`ultra` 不接受账号 JWT)。
127
+ */
128
+ update(models: readonly ZCodeModel[], plan?: ZCodeEndpointPlan): void;
129
+ }
130
+ //#endregion
131
+ //#region src/contracts/usage.d.ts
132
+ /**
133
+ * 用量契约:Start Plan 的额度窗口。
134
+ *
135
+ * ZCode 的额度按「每个模型 × 周期」表达(日 / 周 / 月 / 一次性),与参考插件的
136
+ * 固定三窗口不同,因此这里保留上游的粒度:一个模型可能同时有日额度和一次性
137
+ * 活动额度,两块都要能显示,不能合并成一个百分比。
138
+ *
139
+ * @module dsh-zcode/usage-contract
140
+ */
141
+ /** 一个模型的额度窗口。 */
142
+ interface ZCodeUsageWindow {
143
+ /** 展示名(如 `GLM-5.3`)。 */
144
+ readonly label: string;
145
+ /** capability 原文(如 `model:glm-5.3`),用于与目录对齐。 */
146
+ readonly capability?: string;
147
+ readonly totalUnits: number;
148
+ readonly usedUnits: number;
149
+ readonly remainingUnits: number;
150
+ /** 已用百分比,0..100;总量为 0 时给 0。 */
151
+ readonly percent: number;
152
+ /** 周期:`daily` / `weekly` / `monthly` / `one_time` 等,上游原样。 */
153
+ readonly period?: string;
154
+ /** 重置或到期时间(ISO 8601)。 */
155
+ readonly resetsAt?: string;
156
+ /** 该窗口是否已耗尽。 */
157
+ readonly exhausted: boolean;
158
+ }
159
+ /** Start Plan 用量快照。 */
160
+ interface ZCodeUsage {
161
+ /** 当前生效的套餐。 */
162
+ readonly plans: readonly {
163
+ readonly planId: string;
164
+ readonly name?: string;
165
+ readonly endsAt?: string;
166
+ }[];
167
+ readonly windows: readonly ZCodeUsageWindow[];
168
+ /** 读取时间(epoch ms)。 */
169
+ readonly fetchedAt: number;
170
+ /** 账号家族。 */
171
+ readonly family?: string;
172
+ }
173
+ /** 剩余百分比。 */
174
+ declare function remainingPercent(window: ZCodeUsageWindow): number;
175
+ /** 距离重置的毫秒数;无效或缺失时间返回 undefined。 */
176
+ declare function resetAfterMs(window: ZCodeUsageWindow, now?: number): number | undefined;
177
+ /**
178
+ * 把上游额度换算成百分比。
179
+ *
180
+ * 上游给的是整数 token 单位,可能为 0(活动额度未发放);此时按 0% 处理而不是
181
+ * 制造 NaN 或 Infinity——界面上「0 / 0」比「NaN%」可读得多。
182
+ */
183
+ declare function usagePercent(used: number, total: number): number;
184
+ //#endregion
185
+ //#region src/host/usage.d.ts
186
+ /** 失败原因。 */
187
+ type ZCodeUsageFailure = 'not-signed-in' | 'request-failed';
188
+ /** 读取结果。 */
189
+ type ZCodeUsageResult = {
190
+ readonly ok: true;
191
+ readonly usage: ZCodeUsage;
192
+ } | {
193
+ readonly ok: false;
194
+ readonly reason: ZCodeUsageFailure;
195
+ readonly message: string;
196
+ };
197
+ /** 额度服务依赖。 */
198
+ interface ZCodeUsageServiceOptions {
199
+ readonly client: ZCodeConsoleClient;
200
+ readonly resolveToken: () => Promise<string | undefined>;
201
+ readonly resolveFamily?: () => Promise<string | undefined>;
202
+ readonly logger?: Pick<Context['logger'], 'warn' | 'info'>;
203
+ /** 缓存时间(毫秒);额度是低频变化数据,读太勤没有意义。 */
204
+ readonly cacheMs?: number;
205
+ readonly now?: () => number;
206
+ }
207
+ /** 额度服务。 */
208
+ declare class ZCodeUsageService {
209
+ private cached;
210
+ private inflight;
211
+ private readonly options;
212
+ constructor(options: ZCodeUsageServiceOptions);
213
+ private get now();
214
+ /** 失效缓存,强制下次读取走网络。 */
215
+ invalidate(): void;
216
+ /** 读取额度;`force` 为 true 时跳过缓存。 */
217
+ read(force?: boolean): Promise<ZCodeUsageResult>;
218
+ private load;
219
+ }
220
+ //#endregion
221
+ //#region src/host/routes.d.ts
222
+ /** 来源不受信任时的错误码(与客户端文案对应)。 */
223
+ declare const ZCODE_ORIGIN_NOT_TRUSTED = "remote-web-origin-not-trusted";
224
+ /** 目录刷新失败的错误码。 */
225
+ declare const ZCODE_CATALOG_UNAVAILABLE = "zcode-catalog-unavailable";
226
+ /** 允许同源页面与 fnOS 网关的回环代理;跨站请求一律拒绝。 */
227
+ declare function trustedRequest(req: IncomingMessage): boolean;
228
+ /** 路由依赖。 */
229
+ interface ZCodeRoutesOptions {
230
+ readonly auth: ZCodeAuthService;
231
+ readonly usage: ZCodeUsageService;
232
+ readonly catalog: ZCodeCatalogService;
233
+ readonly preferences: ZCodePreferencesStore;
234
+ /** 登录状态变化后刷新目录与用量。 */
235
+ readonly refresh: (force: boolean) => Promise<void>;
236
+ /** 偏好被写入后让宿主侧重新调度刷新。 */
237
+ readonly preferencesChanged: () => Promise<void>;
238
+ /** 供应商路由的注册状态,供详情页诊断。 */
239
+ readonly routes: () => readonly ZCodeRouteStatus[];
240
+ }
241
+ /** 注册全部插件路由;返回的清理函数由 `ctx.effect` 管理。 */
242
+ declare function registerZCodeRoutes(ctx: Context, options: ZCodeRoutesOptions): void;
243
+ //#endregion
244
+ export { zcodePreferencesPath as C, ZCodeUsageVisibility as D, ZCodePreferences as E, normalizePreferences as O, ZCodePreferencesStore as S, ZCODE_REFRESH_MINUTES as T, ZCodeRouteStatus as _, trustedRequest as a, toPiModel as b, ZCodeUsageService as c, ZCodeUsageWindow as d, remainingPercent as f, ZCodeProviderRuntimeOptions as g, ZCodeProviderRuntime as h, registerZCodeRoutes as i, ZCodeUsageServiceOptions as l, usagePercent as m, ZCODE_ORIGIN_NOT_TRUSTED as n, ZCodeUsageFailure as o, resetAfterMs as p, ZCodeRoutesOptions as r, ZCodeUsageResult as s, ZCODE_CATALOG_UNAVAILABLE as t, ZCodeUsage as u, createZCodeProfile as v, DEFAULT_ZCODE_PREFERENCES as w, ZCODE_PREFERENCES_FILENAME as x, createZCodeProvider as y };
@@ -0,0 +1,307 @@
1
+ import { d as ZCODE_FAMILIES } from "./identity-DE3rOYuE.js";
2
+ //#region src/contracts/routes.ts
3
+ /**
4
+ * 插件同源 Web 路由:宿主注册、浏览器调用,两端引用同一份常量。
5
+ *
6
+ * 不用 Remote API 命名空间,是因为这里只有低频率、无状态的读写;同源 HTTP
7
+ * 路由的信任边界由宿主侧 `trustedRequest` 把关,与其他账号插件一致。
8
+ *
9
+ * @module dsh-zcode/routes
10
+ */
11
+ /** 本插件 Web 路由的前缀。 */
12
+ const ZCODE_ROUTE_PREFIX = "/plugins/dsh-zcode";
13
+ const ZCODE_ROUTES = {
14
+ /** 账号状态(含进行中的登录挑战)。 */
15
+ status: `${ZCODE_ROUTE_PREFIX}/auth/status`,
16
+ /** 开始授权;响应带授权地址。 */
17
+ login: `${ZCODE_ROUTE_PREFIX}/auth/login`,
18
+ /** 桌面授权码流程的回调地址:浏览器登录后回跳到这里完成换取。 */
19
+ callback: `${ZCODE_ROUTE_PREFIX}/auth/callback`,
20
+ /** 取消进行中的登录,不删除已保存凭据。 */
21
+ cancel: `${ZCODE_ROUTE_PREFIX}/auth/cancel`,
22
+ /** 退出登录并删除凭据。 */
23
+ logout: `${ZCODE_ROUTE_PREFIX}/auth/logout`,
24
+ /** Start Plan 额度。 */
25
+ usage: `${ZCODE_ROUTE_PREFIX}/usage`,
26
+ /** 当前模型目录快照。 */
27
+ catalog: `${ZCODE_ROUTE_PREFIX}/catalog`,
28
+ /** 强制刷新模型目录。 */
29
+ catalogRefresh: `${ZCODE_ROUTE_PREFIX}/catalog/refresh`,
30
+ /** 插件偏好设置。 */
31
+ preferences: `${ZCODE_ROUTE_PREFIX}/preferences`
32
+ };
33
+ //#endregion
34
+ //#region src/host/routes.ts
35
+ /** 请求体上限:只有少量表单字段。 */
36
+ const REQUEST_BODY_LIMIT = 16384;
37
+ /** 来源不受信任时的错误码(与客户端文案对应)。 */
38
+ const ZCODE_ORIGIN_NOT_TRUSTED = "remote-web-origin-not-trusted";
39
+ /** 目录刷新失败的错误码。 */
40
+ const ZCODE_CATALOG_UNAVAILABLE = "zcode-catalog-unavailable";
41
+ function header(req, name) {
42
+ const value = req.headers[name];
43
+ return Array.isArray(value) ? value[0] : value;
44
+ }
45
+ function firstForwarded(value) {
46
+ return value?.split(",")[0]?.trim() || void 0;
47
+ }
48
+ function requestOrigin(req) {
49
+ const host = firstForwarded(header(req, "x-forwarded-host")) ?? header(req, "host");
50
+ if (host === void 0) return void 0;
51
+ const proto = firstForwarded(header(req, "x-forwarded-proto")) ?? (req.socket.encrypted === true ? "https" : "http");
52
+ try {
53
+ return new URL(`${proto}://${host}`).origin;
54
+ } catch {
55
+ return;
56
+ }
57
+ }
58
+ function normalizeOrigin(raw) {
59
+ try {
60
+ return new URL(raw).origin;
61
+ } catch {
62
+ return;
63
+ }
64
+ }
65
+ function localPeer(req) {
66
+ const remote = req.socket.remoteAddress;
67
+ return remote === "127.0.0.1" || remote === "::1" || remote === "::ffff:127.0.0.1";
68
+ }
69
+ /** 允许同源页面与 fnOS 网关的回环代理;跨站请求一律拒绝。 */
70
+ function trustedRequest(req) {
71
+ if (header(req, "sec-fetch-site")?.trim().toLowerCase() === "cross-site") return false;
72
+ const origin = header(req, "origin");
73
+ if (origin !== void 0) return requestOrigin(req) === normalizeOrigin(origin);
74
+ return localPeer(req);
75
+ }
76
+ /** 浏览器整页跳转用的极简结果页(回调路径不在 SPA 内,需要自渲染)。 */
77
+ function html(res, status, body) {
78
+ res.writeHead(status, {
79
+ "content-type": "text/html; charset=utf-8",
80
+ "content-security-policy": "default-src 'none'; style-src 'unsafe-inline'"
81
+ });
82
+ res.end(body);
83
+ }
84
+ /** 回调结果页:一句话结论 + 关闭提示,不引用任何外部资源。 */
85
+ function callbackPage(message, ok) {
86
+ const title = ok ? "ZCode 登录成功" : "ZCode 登录失败";
87
+ return `<!doctype html><html lang="zh-CN"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>${title}</title></head><body style="margin:0;min-height:100vh;display:flex;align-items:center;justify-content:center;font:14px/1.6 -apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;background:#f8f9fa"><div style="padding:32px 40px;border-radius:12px;background:#fff;box-shadow:0 2px 12px rgba(0,0,0,.08);max-width:420px;text-align:center"><h1 style="margin:0 0 12px;font-size:16px;color:${ok ? "#12b886" : "#e03131"}">${title}</h1><p style="margin:0;color:#495057">${message}</p></div></body></html>`;
88
+ }
89
+ function json(res, status, value) {
90
+ res.writeHead(status, {
91
+ "content-type": "application/json; charset=utf-8",
92
+ "cache-control": "no-store",
93
+ "x-content-type-options": "nosniff"
94
+ });
95
+ res.end(JSON.stringify(value));
96
+ }
97
+ function readJsonBody(req) {
98
+ return new Promise((resolve, reject) => {
99
+ let body = "";
100
+ let size = 0;
101
+ let finished = false;
102
+ req.setEncoding("utf8");
103
+ req.on("data", (chunk) => {
104
+ if (finished) return;
105
+ size += Buffer.byteLength(chunk);
106
+ if (size > REQUEST_BODY_LIMIT) {
107
+ finished = true;
108
+ reject(/* @__PURE__ */ new Error("请求体过大"));
109
+ return;
110
+ }
111
+ body += chunk;
112
+ });
113
+ req.on("end", () => {
114
+ if (finished) return;
115
+ finished = true;
116
+ try {
117
+ resolve(body.length === 0 ? {} : JSON.parse(body));
118
+ } catch {
119
+ reject(/* @__PURE__ */ new Error("请求体不是合法 JSON"));
120
+ }
121
+ });
122
+ req.on("error", (error) => {
123
+ if (finished) return;
124
+ finished = true;
125
+ reject(error);
126
+ });
127
+ });
128
+ }
129
+ /** 错误文本脱敏:绝不把 token 回显给浏览器。 */
130
+ function safeMessage(error) {
131
+ return (error instanceof Error ? error.message : String(error)).replace(/\beyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\b/gu, "[redacted token]").replace(/(\b(?:code|token|refresh_token|access_token|apiKey)=)[^&\s]+/giu, "$1[redacted]").slice(0, 1e3);
132
+ }
133
+ /** 从请求体解析账号家族;非法值返回 undefined。 */
134
+ function parseFamily(body) {
135
+ const value = body !== null && typeof body === "object" && !Array.isArray(body) ? body.family : void 0;
136
+ if (typeof value !== "string") return void 0;
137
+ const normalized = value.trim().toLowerCase();
138
+ return ZCODE_FAMILIES.find((family) => family === normalized);
139
+ }
140
+ /** 目录响应的统一形状(列表/强制刷新共用)。 */
141
+ function catalogResponse(catalog, routes, error) {
142
+ return {
143
+ catalog: catalog ?? null,
144
+ routes,
145
+ ...error === void 0 ? {} : { error }
146
+ };
147
+ }
148
+ /** 注册全部插件路由;返回的清理函数由 `ctx.effect` 管理。 */
149
+ function registerZCodeRoutes(ctx, options) {
150
+ ctx.effect(() => {
151
+ const authorize = (req, res) => {
152
+ if (trustedRequest(req)) return true;
153
+ json(res, 403, { error: ZCODE_ORIGIN_NOT_TRUSTED });
154
+ return false;
155
+ };
156
+ const handlers = [
157
+ ctx.webServer.register({
158
+ kind: "exact",
159
+ path: ZCODE_ROUTES.status,
160
+ handler: async (req, res) => {
161
+ if (req.method !== "GET") return json(res, 405, { error: "method not allowed" });
162
+ if (!authorize(req, res)) return;
163
+ try {
164
+ json(res, 200, await options.auth.status());
165
+ } catch (error) {
166
+ json(res, 500, { error: safeMessage(error) });
167
+ }
168
+ }
169
+ }),
170
+ ctx.webServer.register({
171
+ kind: "exact",
172
+ path: ZCODE_ROUTES.login,
173
+ handler: async (req, res) => {
174
+ if (req.method !== "POST") return json(res, 405, { error: "method not allowed" });
175
+ if (!authorize(req, res)) return;
176
+ try {
177
+ const family = parseFamily(await readJsonBody(req)) ?? "bigmodel";
178
+ json(res, 200, {
179
+ status: "signing-in",
180
+ challenge: await options.auth.loginDesktop(family, requestOrigin(req))
181
+ });
182
+ } catch (error) {
183
+ json(res, 500, { error: safeMessage(error) });
184
+ }
185
+ }
186
+ }),
187
+ ctx.webServer.register({
188
+ kind: "exact",
189
+ path: ZCODE_ROUTES.callback,
190
+ handler: async (req, res) => {
191
+ const url = new URL(req.url ?? "/", requestOrigin(req) ?? "http://127.0.0.1");
192
+ const code = url.searchParams.get("code") ?? url.searchParams.get("authCode");
193
+ const state = url.searchParams.get("state");
194
+ const family = parseFamily(url.searchParams.get("family")) ?? "bigmodel";
195
+ if (code === null || state === null) return html(res, 400, callbackPage("授权回调缺少 code 或 state,请回到 DSH 重新登录。", false));
196
+ try {
197
+ await options.auth.completeDesktopLogin({
198
+ code,
199
+ state,
200
+ family
201
+ });
202
+ return html(res, 200, callbackPage("登录成功,可以关闭此页并回到 DSH。", true));
203
+ } catch (error) {
204
+ return html(res, 400, callbackPage(safeMessage(error), false));
205
+ }
206
+ }
207
+ }),
208
+ ctx.webServer.register({
209
+ kind: "exact",
210
+ path: ZCODE_ROUTES.cancel,
211
+ handler: async (req, res) => {
212
+ if (req.method !== "POST") return json(res, 405, { error: "method not allowed" });
213
+ if (!authorize(req, res)) return;
214
+ try {
215
+ options.auth.cancel();
216
+ json(res, 200, await options.auth.status());
217
+ } catch (error) {
218
+ json(res, 500, { error: safeMessage(error) });
219
+ }
220
+ }
221
+ }),
222
+ ctx.webServer.register({
223
+ kind: "exact",
224
+ path: ZCODE_ROUTES.logout,
225
+ handler: async (req, res) => {
226
+ if (req.method !== "POST") return json(res, 405, { error: "method not allowed" });
227
+ if (!authorize(req, res)) return;
228
+ try {
229
+ await options.auth.logout();
230
+ options.usage.invalidate();
231
+ await options.refresh(true).catch(() => void 0);
232
+ json(res, 200, await options.auth.status());
233
+ } catch (error) {
234
+ json(res, 500, { error: safeMessage(error) });
235
+ }
236
+ }
237
+ }),
238
+ ctx.webServer.register({
239
+ kind: "exact",
240
+ path: ZCODE_ROUTES.usage,
241
+ handler: async (req, res) => {
242
+ if (req.method !== "GET") return json(res, 405, { error: "method not allowed" });
243
+ if (!authorize(req, res)) return;
244
+ if ((await options.auth.status().catch(() => ({ status: "signed-out" }))).status !== "signed-in") return json(res, 200, { signedIn: false });
245
+ const result = await options.usage.read();
246
+ json(res, 200, result.ok ? {
247
+ signedIn: true,
248
+ usage: result.usage
249
+ } : {
250
+ signedIn: true,
251
+ error: result.message,
252
+ reason: result.reason
253
+ });
254
+ }
255
+ }),
256
+ ctx.webServer.register({
257
+ kind: "exact",
258
+ path: ZCODE_ROUTES.catalog,
259
+ handler: async (req, res) => {
260
+ if (req.method !== "GET") return json(res, 405, { error: "method not allowed" });
261
+ if (!authorize(req, res)) return;
262
+ try {
263
+ json(res, 200, catalogResponse(options.catalog.snapshot() ?? await options.catalog.refreshIfStale(), options.routes()));
264
+ } catch (error) {
265
+ json(res, 200, catalogResponse(options.catalog.snapshot(), options.routes(), safeMessage(error) || "zcode-catalog-unavailable"));
266
+ }
267
+ }
268
+ }),
269
+ ctx.webServer.register({
270
+ kind: "exact",
271
+ path: ZCODE_ROUTES.catalogRefresh,
272
+ handler: async (req, res) => {
273
+ if (req.method !== "POST") return json(res, 405, { error: "method not allowed" });
274
+ if (!authorize(req, res)) return;
275
+ try {
276
+ await options.refresh(true);
277
+ json(res, 200, catalogResponse(options.catalog.snapshot(), options.routes()));
278
+ } catch (error) {
279
+ json(res, 200, catalogResponse(options.catalog.snapshot(), options.routes(), safeMessage(error) || "zcode-catalog-unavailable"));
280
+ }
281
+ }
282
+ }),
283
+ ctx.webServer.register({
284
+ kind: "exact",
285
+ path: ZCODE_ROUTES.preferences,
286
+ handler: async (req, res) => {
287
+ if (req.method !== "GET" && req.method !== "PUT") return json(res, 405, { error: "method not allowed" });
288
+ if (!authorize(req, res)) return;
289
+ try {
290
+ if (req.method === "GET") return json(res, 200, await options.preferences.read());
291
+ const next = await options.preferences.write(await readJsonBody(req));
292
+ await options.preferencesChanged();
293
+ json(res, 200, next);
294
+ } catch (error) {
295
+ json(res, 400, { error: safeMessage(error) });
296
+ }
297
+ }
298
+ })
299
+ ];
300
+ return async () => {
301
+ for (const dispose of handlers) dispose();
302
+ options.auth.dispose();
303
+ };
304
+ }, "dsh-zcode: Web 路由");
305
+ }
306
+ //#endregion
307
+ export { ZCODE_ROUTES as a, trustedRequest as i, ZCODE_ORIGIN_NOT_TRUSTED as n, ZCODE_ROUTE_PREFIX as o, registerZCodeRoutes as r, ZCODE_CATALOG_UNAVAILABLE as t };