@klarkxy/dsh-zhihu 0.1.0

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,23 @@
1
+ //#region src/contracts.d.ts
2
+ declare const ZHIHU_SEARCH_TOOL_NAME = "zhihu_search";
3
+ declare const ZHIHU_GLOBAL_SEARCH_TOOL_NAME = "zhihu_global_search";
4
+ declare const ZHIHU_HOT_LIST_TOOL_NAME = "zhihu_hot_list";
5
+ declare const ZHIHU_ASK_TOOL_NAME = "zhihu_ask";
6
+ declare const ZHIHU_KNOWLEDGE_SEARCH_TOOL_NAME = "zhihu_knowledge_search";
7
+ declare const ZHIHU_RPC_CHANNEL = "/zhihu";
8
+ declare const ZHIHU_CREDENTIAL_REF = "ZHIHU_ACCESS_TOKEN";
9
+ declare const ZHIHU_SEARCH_EVENT = "dsh-editor/zhihu-search";
10
+ type ZhihuRpcResult<T = unknown> = {
11
+ ok: true;
12
+ value: T;
13
+ } | {
14
+ ok: false;
15
+ error: {
16
+ code: string;
17
+ message: string;
18
+ details: Record<string, unknown>;
19
+ };
20
+ };
21
+ //#endregion
22
+ export { ZHIHU_ASK_TOOL_NAME, ZHIHU_CREDENTIAL_REF, ZHIHU_GLOBAL_SEARCH_TOOL_NAME, ZHIHU_HOT_LIST_TOOL_NAME, ZHIHU_KNOWLEDGE_SEARCH_TOOL_NAME, ZHIHU_RPC_CHANNEL, ZHIHU_SEARCH_EVENT, ZHIHU_SEARCH_TOOL_NAME, ZhihuRpcResult };
23
+ //# sourceMappingURL=contracts.d.ts.map
@@ -0,0 +1,13 @@
1
+ //#region src/contracts.ts
2
+ const ZHIHU_SEARCH_TOOL_NAME = "zhihu_search";
3
+ const ZHIHU_GLOBAL_SEARCH_TOOL_NAME = "zhihu_global_search";
4
+ const ZHIHU_HOT_LIST_TOOL_NAME = "zhihu_hot_list";
5
+ const ZHIHU_ASK_TOOL_NAME = "zhihu_ask";
6
+ const ZHIHU_KNOWLEDGE_SEARCH_TOOL_NAME = "zhihu_knowledge_search";
7
+ const ZHIHU_RPC_CHANNEL = "/zhihu";
8
+ const ZHIHU_CREDENTIAL_REF = "ZHIHU_ACCESS_TOKEN";
9
+ const ZHIHU_SEARCH_EVENT = "dsh-editor/zhihu-search";
10
+ //#endregion
11
+ export { ZHIHU_ASK_TOOL_NAME, ZHIHU_CREDENTIAL_REF, ZHIHU_GLOBAL_SEARCH_TOOL_NAME, ZHIHU_HOT_LIST_TOOL_NAME, ZHIHU_KNOWLEDGE_SEARCH_TOOL_NAME, ZHIHU_RPC_CHANNEL, ZHIHU_SEARCH_EVENT, ZHIHU_SEARCH_TOOL_NAME };
12
+
13
+ //# sourceMappingURL=contracts.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"contracts.js","names":[],"sources":["../src/contracts.ts"],"sourcesContent":["export const ZHIHU_SEARCH_TOOL_NAME = 'zhihu_search'\nexport const ZHIHU_GLOBAL_SEARCH_TOOL_NAME = 'zhihu_global_search'\nexport const ZHIHU_HOT_LIST_TOOL_NAME = 'zhihu_hot_list'\nexport const ZHIHU_ASK_TOOL_NAME = 'zhihu_ask'\nexport const ZHIHU_KNOWLEDGE_SEARCH_TOOL_NAME = 'zhihu_knowledge_search'\nexport const ZHIHU_RPC_CHANNEL = '/zhihu'\nexport const ZHIHU_CREDENTIAL_REF = 'ZHIHU_ACCESS_TOKEN'\nexport const ZHIHU_SEARCH_EVENT = 'dsh-editor/zhihu-search'\nexport type ZhihuRpcResult<T = unknown> = { ok: true; value: T } | { ok: false; error: { code: string; message: string; details: Record<string, unknown> } }\n"],"mappings":";AAAA,MAAa,yBAAyB;AACtC,MAAa,gCAAgC;AAC7C,MAAa,2BAA2B;AACxC,MAAa,sBAAsB;AACnC,MAAa,mCAAmC;AAChD,MAAa,oBAAoB;AACjC,MAAa,uBAAuB;AACpC,MAAa,qBAAqB"}
package/lib/index.d.ts ADDED
@@ -0,0 +1,53 @@
1
+ import { ZhihuRpcResult } from "./contracts.js";
2
+ import { ZhihuUsageRecorder } from "./usage.js";
3
+ import { Context } from "@deepseek-ai/cordis";
4
+ //#region src/zhihu-client.d.ts
5
+ type ZhihuSearchFetcher = (input: string, init?: {
6
+ method?: string;
7
+ headers?: Record<string, string>;
8
+ body?: string | FormData;
9
+ signal?: AbortSignal;
10
+ }) => Promise<{
11
+ ok: boolean;
12
+ status: number;
13
+ text(): Promise<string>;
14
+ json(): Promise<unknown>;
15
+ }>;
16
+ type ZhihuSearchResolveOptions = {
17
+ /** Resolve the Access Secret via host-managed credentials (e.g. the settings UI). */
18
+ resolveCredential?: () => Promise<string | undefined>;
19
+ };
20
+ /** Metering payload emitted after each zhihu tool execution. */
21
+ type ZhihuSearchExecuted = {
22
+ ok: boolean;
23
+ results: number;
24
+ };
25
+ type ZhihuClientOptions = ZhihuSearchResolveOptions & {
26
+ fetcher?: ZhihuSearchFetcher;
27
+ /** Allows tests to bypass process env; defaults to process.env. */
28
+ env?: NodeJS.ProcessEnv;
29
+ signal?: AbortSignal;
30
+ /** Override the request timeout in ms. Defaults to {@link ZHIHU_DEFAULT_TIMEOUT_MS}. */
31
+ timeoutMs?: number;
32
+ };
33
+ //#endregion
34
+ //#region src/index.d.ts
35
+ declare const name = "@klarkxy/dsh-zhihu";
36
+ declare const inject: readonly ["connection", "credentials", "storageDomain", "webServer"];
37
+ type ZhihuService = {
38
+ call(endpoint: string, payload: unknown, signal: AbortSignal): Promise<ZhihuRpcResult>;
39
+ run<T>(execute: (signal: AbortSignal) => Promise<T>, signal: AbortSignal): Promise<T>;
40
+ dispose(): Promise<void>;
41
+ usageSummary(days: unknown): Promise<{
42
+ days: unknown[];
43
+ }>;
44
+ toolOptions: ZhihuClientOptions & {
45
+ onExecuted: (event: ZhihuSearchExecuted) => void;
46
+ };
47
+ };
48
+ /** One shared service for direct UI RPC and optional Tool wrappers. */
49
+ declare function createZhihuService(options: ZhihuClientOptions, usage: ZhihuUsageRecorder, notify?: (event: ZhihuSearchExecuted) => void): ZhihuService;
50
+ declare function apply(ctx: Context): Promise<void>;
51
+ //#endregion
52
+ export { ZhihuService, apply, createZhihuService, inject, name };
53
+ //# sourceMappingURL=index.d.ts.map
package/lib/index.js ADDED
@@ -0,0 +1,461 @@
1
+ import { _ as reportExecuted, a as executeZhihuGlobalSearch, c as normalizeRecallScopes, g as parseZhihuEnvelope, h as ZhihuSearchError, i as executeZhihuAsk, n as ZHIHU_ASK_MODELS, o as executeZhihuHotList, p as executeZhihuSearch, s as executeZhihuKnowledgeSearch, t as ZHIHU_ASK_DEFAULT_MODEL, v as zhihuFetchJson } from "./operations.js";
2
+ import { ZHIHU_CREDENTIAL_REF, ZHIHU_RPC_CHANNEL, ZHIHU_SEARCH_EVENT } from "./contracts.js";
3
+ import { createZhihuUsageRecorder, resolveDays, zhihuUsageDomainSpec } from "./usage.js";
4
+ import { credentialRef } from "@deepseek-ai/dsh-credentials";
5
+ /** 设置页 RPC 走 JSON/base64 传输,限额压到 20MB(平台上限 100MB)。 */
6
+ const ZHIHU_KNOWLEDGE_UPLOAD_MAX_BYTES = 20971520;
7
+ const KNOWLEDGE_BASES_PATH = "/api/v1/knowledge/bases";
8
+ const KNOWLEDGE_FILES_PATH = "/api/v1/knowledge/files";
9
+ const ZHIHU_KNOWLEDGE_FILE_EXTENSIONS = [
10
+ ".pdf",
11
+ ".md",
12
+ ".txt",
13
+ ".ppt",
14
+ ".pptx",
15
+ ".xlsx",
16
+ ".xls",
17
+ ".docx",
18
+ ".doc",
19
+ ".webp",
20
+ ".png",
21
+ ".jpg",
22
+ ".mobi",
23
+ ".epub",
24
+ ".csv",
25
+ ".azw3"
26
+ ];
27
+ const KNOWLEDGE_CONTENT_TYPES = {
28
+ ".pdf": "application/pdf",
29
+ ".md": "text/markdown",
30
+ ".txt": "text/plain",
31
+ ".ppt": "application/vnd.ms-powerpoint",
32
+ ".pptx": "application/vnd.openxmlformats-officedocument.presentationml.presentation",
33
+ ".xlsx": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
34
+ ".xls": "application/vnd.ms-excel",
35
+ ".docx": "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
36
+ ".doc": "application/msword",
37
+ ".webp": "image/webp",
38
+ ".png": "image/png",
39
+ ".jpg": "image/jpeg",
40
+ ".mobi": "application/x-mobipocket-ebook",
41
+ ".epub": "application/epub+zip",
42
+ ".csv": "text/csv",
43
+ ".azw3": "application/vnd.amazon.ebook"
44
+ };
45
+ function normalizeBase(raw) {
46
+ return {
47
+ id: typeof raw.KnowledgeBaseID === "string" ? raw.KnowledgeBaseID : "",
48
+ name: typeof raw.Name === "string" && raw.Name.length > 0 ? raw.Name : "(未命名知识库)",
49
+ relation: typeof raw.Relation === "string" ? raw.Relation : "",
50
+ visibility: typeof raw.Visibility === "string" ? raw.Visibility : "",
51
+ isDefault: raw.IsDefault === true,
52
+ contentCount: typeof raw.ContentCount === "number" ? raw.ContentCount : 0
53
+ };
54
+ }
55
+ async function listZhihuKnowledgeBases(options = {}) {
56
+ const body = await zhihuFetchJson(KNOWLEDGE_BASES_PATH, { params: { Scope: "all" } }, "知乎知识库列表", options);
57
+ const data = parseZhihuEnvelope(body);
58
+ return {
59
+ version: 1,
60
+ bases: (Array.isArray(data.Items) ? data.Items : []).filter((item) => !!item && typeof item === "object" && !Array.isArray(item)).map(normalizeBase).filter((base) => base.id.length > 0)
61
+ };
62
+ }
63
+ /** 校验文件名与大小,返回小写扩展名。 */
64
+ function checkKnowledgeFile(fileName, size) {
65
+ const cleaned = fileName.trim();
66
+ if (!cleaned) throw new ZhihuSearchError("BAD_RESPONSE", "知识库文件名不能为空。");
67
+ const base = cleaned.split(/[\\/]/).pop() ?? cleaned;
68
+ for (const ch of base) {
69
+ const code = ch.codePointAt(0) ?? 0;
70
+ if (code < 32 || code === 127) throw new ZhihuSearchError("BAD_RESPONSE", "知识库文件名不能包含控制字符。");
71
+ }
72
+ if (new TextEncoder().encode(base).length > 255) throw new ZhihuSearchError("BAD_RESPONSE", "知识库文件名不能超过 255 个 UTF-8 字节。");
73
+ if (size <= 0) throw new ZhihuSearchError("BAD_RESPONSE", "知识库文件内容不能为空。");
74
+ if (size > 20971520) throw new ZhihuSearchError("BAD_RESPONSE", `知识库文件大小不能超过 ${ZHIHU_KNOWLEDGE_UPLOAD_MAX_BYTES / 1024 / 1024}MB。`);
75
+ const dot = base.lastIndexOf(".");
76
+ const ext = dot >= 0 ? base.slice(dot).toLowerCase() : "";
77
+ if (!ZHIHU_KNOWLEDGE_FILE_EXTENSIONS.includes(ext)) throw new ZhihuSearchError("BAD_RESPONSE", `知识库上传仅支持 ${ZHIHU_KNOWLEDGE_FILE_EXTENSIONS.join("、")}。`);
78
+ return ext;
79
+ }
80
+ async function uploadZhihuKnowledgeFile(input, options = {}) {
81
+ const cleaned = input.fileName.trim().split(/[\\/]/).pop() ?? input.fileName.trim();
82
+ const ext = checkKnowledgeFile(input.fileName, input.data.byteLength);
83
+ const form = new FormData();
84
+ if (input.knowledgeBaseId) form.set("KnowledgeBaseID", input.knowledgeBaseId);
85
+ form.set("File", new Blob([input.data.buffer], { type: KNOWLEDGE_CONTENT_TYPES[ext] ?? "application/octet-stream" }), cleaned);
86
+ const body = await zhihuFetchJson(KNOWLEDGE_FILES_PATH, { formData: form }, "知乎知识库上传", {
87
+ ...options,
88
+ timeoutMs: options.timeoutMs ?? 18e4
89
+ });
90
+ const data = parseZhihuEnvelope(body);
91
+ return {
92
+ version: 1,
93
+ knowledgeBaseId: typeof data.KnowledgeBaseID === "string" ? data.KnowledgeBaseID : "",
94
+ recallContentId: typeof data.RecallContentID === "string" ? data.RecallContentID : "",
95
+ fileName: typeof data.FileName === "string" ? data.FileName : cleaned,
96
+ fileSize: typeof data.FileSize === "number" ? data.FileSize : input.data.byteLength,
97
+ title: typeof data.Title === "string" ? data.Title : "",
98
+ abstract: typeof data.Abstract === "string" ? data.Abstract : "",
99
+ originUrl: typeof data.OriginUrl === "string" ? data.OriginUrl : ""
100
+ };
101
+ }
102
+ //#endregion
103
+ //#region src/host-rpc.ts
104
+ const CHANNEL_PATTERN = /^\/[A-Za-z0-9._~-]+$/;
105
+ const ENDPOINT_SEGMENT = /^[A-Za-z0-9_$.-]+$/;
106
+ const MAX_BODY_BYTES = 67108864;
107
+ /** Standalone copy of manuscript Host RPC registration; this package cannot depend on manuscript. */
108
+ function registerHostRpc(ctx, channel, handler) {
109
+ if (!CHANNEL_PATTERN.test(channel) || channel === "/api") throw new Error(`invalid RPC channel ${JSON.stringify(channel)}`);
110
+ return ctx.webServer.register({
111
+ kind: "prefix",
112
+ path: channel,
113
+ handler: (req, res) => dispatchHostRpc(ctx, channel, handler, req, res)
114
+ });
115
+ }
116
+ function endpointFromRpcPath(channel, pathname) {
117
+ if (!pathname.startsWith(`${channel}/`)) return void 0;
118
+ const endpoint = pathname.slice(channel.length + 1);
119
+ if (endpoint.split("/").some((segment) => segment === "" || segment === "." || segment === ".." || !ENDPOINT_SEGMENT.test(segment))) return;
120
+ return endpoint;
121
+ }
122
+ async function dispatchHostRpc(ctx, channel, handler, req, res) {
123
+ const rejection = ctx.connection.requestRejection?.(req);
124
+ if (rejection !== void 0) {
125
+ res.writeHead(rejection);
126
+ res.end(rejection === 401 ? "unauthorized" : "forbidden");
127
+ return;
128
+ }
129
+ const abort = new AbortController();
130
+ res.on("close", () => {
131
+ if (!res.writableEnded) abort.abort();
132
+ });
133
+ const endpoint = endpointFromRpcPath(channel, new URL(req.url ?? "/", "http://dsh.internal").pathname);
134
+ if (req.method !== "POST" || endpoint === void 0) {
135
+ res.writeHead(404);
136
+ res.end("not found");
137
+ return;
138
+ }
139
+ if (String(req.headers["content-type"] ?? "").split(";", 1)[0]?.trim().toLowerCase() !== "application/json") {
140
+ res.writeHead(415);
141
+ res.end("content type must be application/json");
142
+ return;
143
+ }
144
+ const chunks = [];
145
+ let received = 0;
146
+ for await (const chunk of req) {
147
+ const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
148
+ received += buffer.byteLength;
149
+ if (received > MAX_BODY_BYTES) {
150
+ res.writeHead(413, { connection: "close" });
151
+ res.end();
152
+ req.destroy();
153
+ return;
154
+ }
155
+ chunks.push(buffer);
156
+ }
157
+ let body;
158
+ try {
159
+ body = JSON.parse(Buffer.concat(chunks).toString("utf8") || "null");
160
+ } catch {
161
+ res.writeHead(400);
162
+ res.end("body is not JSON");
163
+ return;
164
+ }
165
+ const record = body && typeof body === "object" && !Array.isArray(body) ? body : {};
166
+ const rpcId = typeof record.rpcId === "string" ? record.rpcId : "invalid-request";
167
+ if (record.type !== "client-request" || record.method !== endpoint) {
168
+ res.writeHead(200, { "content-type": "application/json" });
169
+ res.end(JSON.stringify({
170
+ type: "server-response",
171
+ rpcId,
172
+ result: {
173
+ ok: false,
174
+ error: {
175
+ code: "gateway/bad-request",
176
+ message: record.method !== endpoint ? `method ${JSON.stringify(record.method)} does not match endpoint ${JSON.stringify(endpoint)}` : "invalid client-request message",
177
+ details: { issues: [] }
178
+ }
179
+ }
180
+ }));
181
+ return;
182
+ }
183
+ try {
184
+ const result = await handler(endpoint, record.payload, abort.signal);
185
+ res.writeHead(200, { "content-type": "application/json" });
186
+ res.end(JSON.stringify({
187
+ type: "server-response",
188
+ rpcId,
189
+ result
190
+ }));
191
+ } catch (error) {
192
+ res.writeHead(500);
193
+ res.end(`handler failure: ${String(error)}`);
194
+ }
195
+ }
196
+ //#endregion
197
+ //#region src/web-search-provider.ts
198
+ /**
199
+ * Optional web-search-manager backend. Registers only when this profile already
200
+ * provides `webSearchManager`; Zhihu still works without that plugin.
201
+ * Official model tool remains `web_search`. Dedicated `zhihu_global_search` is unchanged.
202
+ */
203
+ const ZHIHU_WEB_SEARCH_ID = "zhihu-global";
204
+ const ZHIHU_WEB_SEARCH_DESCRIPTOR = {
205
+ id: ZHIHU_WEB_SEARCH_ID,
206
+ label: "知乎全网搜索",
207
+ description: "使用已配置的知乎 Access Secret 检索公开网页。与「知乎资料」共用同一份凭证。",
208
+ credentialRef: ZHIHU_CREDENTIAL_REF,
209
+ credentialShared: true,
210
+ billing: "request",
211
+ signupUrl: "https://developer.zhihu.com"
212
+ };
213
+ function sourceFrom(url, title, snippet) {
214
+ let parsed;
215
+ try {
216
+ parsed = new URL(url);
217
+ } catch {
218
+ return;
219
+ }
220
+ if (!["https:", "http:"].includes(parsed.protocol) || parsed.username || parsed.password) return;
221
+ return {
222
+ url: parsed.href,
223
+ ...title ? { title: title.slice(0, 1e3) } : {},
224
+ ...snippet ? { snippet: snippet.slice(0, 8e3) } : {}
225
+ };
226
+ }
227
+ function isWebSearchManager(value) {
228
+ return !!value && typeof value === "object" && typeof value.registerSearchProvider === "function";
229
+ }
230
+ function createZhihuWebSearchProvider(options) {
231
+ const { apiKey, onExecuted, ...client } = options;
232
+ return {
233
+ id: ZHIHU_WEB_SEARCH_ID,
234
+ available() {
235
+ return Boolean(apiKey?.trim());
236
+ },
237
+ async search(request, signal) {
238
+ if (!apiKey?.trim()) throw new Error("知乎全网搜索未配置 Access Secret。");
239
+ if (!request.query?.trim() || request.query.length > 4e3) throw new Error("查询无效。");
240
+ const count = request.maxResults ?? 5;
241
+ if (!Number.isInteger(count) || count < 1 || count > 20) throw new Error(`结果数须为 1 至 20。`);
242
+ try {
243
+ signal?.throwIfAborted();
244
+ const result = await executeZhihuGlobalSearch(request.query.trim(), count, "all", {
245
+ ...client,
246
+ signal,
247
+ env: {},
248
+ resolveCredential: async () => apiKey
249
+ });
250
+ reportExecuted(onExecuted, {
251
+ ok: true,
252
+ results: result.items.length
253
+ });
254
+ const sources = result.items.flatMap((item) => sourceFrom(item.url, item.title, item.summary) ?? []);
255
+ return {
256
+ sources: sources.slice(0, count),
257
+ truncated: sources.length > count
258
+ };
259
+ } catch (error) {
260
+ reportExecuted(onExecuted, {
261
+ ok: false,
262
+ results: 0
263
+ });
264
+ throw error;
265
+ }
266
+ }
267
+ };
268
+ }
269
+ function registerZhihuGlobalSearchProvider(manager, service) {
270
+ return manager.registerSearchProvider(ZHIHU_WEB_SEARCH_DESCRIPTOR, (options) => {
271
+ const provider = createZhihuWebSearchProvider({
272
+ ...service.toolOptions,
273
+ apiKey: options.apiKey,
274
+ timeoutMs: options.timeoutMs
275
+ });
276
+ return {
277
+ id: provider.id,
278
+ available: () => provider.available(),
279
+ search: (request, signal) => service.run((combined) => provider.search(request, combined), signal ?? new AbortController().signal)
280
+ };
281
+ });
282
+ }
283
+ /** Wait for `webSearchManager` if this profile loaded it; no-op otherwise. */
284
+ function bindZhihuWebSearch(ctx, service) {
285
+ ctx.inject(["webSearchManager"], (inner) => {
286
+ let manager;
287
+ try {
288
+ manager = inner.get("webSearchManager");
289
+ } catch {
290
+ return;
291
+ }
292
+ if (!isWebSearchManager(manager)) return;
293
+ return registerZhihuGlobalSearchProvider(manager, service);
294
+ });
295
+ }
296
+ //#endregion
297
+ //#region src/index.ts
298
+ const name = "@klarkxy/dsh-zhihu";
299
+ const inject = [
300
+ "connection",
301
+ "credentials",
302
+ "storageDomain",
303
+ "webServer"
304
+ ];
305
+ const fail = (code, message) => ({
306
+ ok: false,
307
+ error: {
308
+ code,
309
+ message,
310
+ details: {}
311
+ }
312
+ });
313
+ /** One shared service for direct UI RPC and optional Tool wrappers. */
314
+ function createZhihuService(options, usage, notify = () => {}) {
315
+ let pendingUsage = Promise.resolve();
316
+ const lifetime = new AbortController();
317
+ const active = /* @__PURE__ */ new Set();
318
+ const run = (execute, signal) => {
319
+ const combined = AbortSignal.any([signal, lifetime.signal]);
320
+ const request = Promise.resolve().then(() => {
321
+ combined.throwIfAborted();
322
+ return execute(combined);
323
+ });
324
+ active.add(request);
325
+ return request.finally(() => active.delete(request));
326
+ };
327
+ const record = (event) => {
328
+ pendingUsage = pendingUsage.then(() => usage.record(event)).catch(() => {});
329
+ try {
330
+ notify(event);
331
+ } catch {}
332
+ };
333
+ const service = {
334
+ toolOptions: {
335
+ ...options,
336
+ onExecuted: record
337
+ },
338
+ async usageSummary(days) {
339
+ await pendingUsage;
340
+ return { days: await usage.read(resolveDays(days)) };
341
+ },
342
+ async call(endpoint, payload, signal) {
343
+ if (signal.aborted) return fail("cancelled", "请求已取消");
344
+ const body = payload && typeof payload === "object" && !Array.isArray(payload) ? payload : {};
345
+ const client = {
346
+ ...options,
347
+ signal
348
+ };
349
+ let counted = false;
350
+ try {
351
+ if (endpoint === "usage.summary") {
352
+ await pendingUsage;
353
+ return {
354
+ ok: true,
355
+ value: { days: await usage.read(resolveDays(body.days)) }
356
+ };
357
+ }
358
+ const query = typeof body.query === "string" ? body.query : "";
359
+ if ([
360
+ "search",
361
+ "global.search",
362
+ "ask",
363
+ "knowledge.search"
364
+ ].includes(endpoint) && (!query.trim() || query.length > 4e3)) return fail("bad-request", "请输入不超过 4000 字符的查询");
365
+ let value;
366
+ let results = 0;
367
+ counted = true;
368
+ if (endpoint === "search") {
369
+ const result = await executeZhihuSearch(query, typeof body.count === "number" ? body.count : 5, client);
370
+ value = result;
371
+ results = result.items.length;
372
+ } else if (endpoint === "global.search") {
373
+ const result = await executeZhihuGlobalSearch(query, typeof body.count === "number" ? body.count : 10, body.searchDb === "realtime" || body.searchDb === "static" ? body.searchDb : "all", client);
374
+ value = result;
375
+ results = result.items.length;
376
+ } else if (endpoint === "hot.list") {
377
+ const result = await executeZhihuHotList(typeof body.limit === "number" ? body.limit : 10, client);
378
+ value = result;
379
+ results = result.items.length;
380
+ } else if (endpoint === "ask") {
381
+ const model = ZHIHU_ASK_MODELS.includes(body.model) ? body.model : ZHIHU_ASK_DEFAULT_MODEL;
382
+ value = await executeZhihuAsk(query, model, client);
383
+ results = 1;
384
+ } else if (endpoint === "knowledge.search") {
385
+ const result = await executeZhihuKnowledgeSearch(query, typeof body.limit === "number" ? body.limit : 5, normalizeRecallScopes(body.recallScopes), client);
386
+ value = result;
387
+ results = result.items.length;
388
+ } else if (endpoint === "knowledge.bases") {
389
+ const result = await listZhihuKnowledgeBases(client);
390
+ value = result;
391
+ results = result.bases.length;
392
+ } else if (endpoint === "knowledge.upload") {
393
+ const contentBase64 = typeof body.contentBase64 === "string" ? body.contentBase64 : "";
394
+ if (!contentBase64) throw new Error("缺少文件内容。");
395
+ if (contentBase64.length > Math.ceil(20971520 / 3) * 4 || !/^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/.test(contentBase64)) return fail("bad-request", "文件内容无效或超过 20 MB");
396
+ value = { upload: await uploadZhihuKnowledgeFile({
397
+ fileName: typeof body.fileName === "string" ? body.fileName : "",
398
+ data: new Uint8Array(Buffer.from(contentBase64, "base64")),
399
+ knowledgeBaseId: typeof body.knowledgeBaseId === "string" && body.knowledgeBaseId.trim() ? body.knowledgeBaseId.trim() : void 0
400
+ }, client) };
401
+ results = 1;
402
+ } else {
403
+ counted = false;
404
+ return fail("bad-request", `unknown endpoint ${endpoint}`);
405
+ }
406
+ record({
407
+ ok: true,
408
+ results
409
+ });
410
+ return {
411
+ ok: true,
412
+ value
413
+ };
414
+ } catch (error) {
415
+ if (counted) record({
416
+ ok: false,
417
+ results: 0
418
+ });
419
+ if (signal.aborted) return fail("cancelled", "请求已取消");
420
+ const code = error instanceof ZhihuSearchError ? error.code.toLowerCase().replaceAll("_", "-") : "internal";
421
+ return fail(code, error instanceof Error ? error.message : "知乎请求失败");
422
+ }
423
+ }
424
+ };
425
+ return {
426
+ ...service,
427
+ run,
428
+ async call(endpoint, payload, signal) {
429
+ if (signal.aborted || lifetime.signal.aborted) return fail("cancelled", "请求已取消");
430
+ try {
431
+ return await run((combined) => service.call(endpoint, payload, combined), signal);
432
+ } catch (error) {
433
+ if (signal.aborted || lifetime.signal.aborted) return fail("cancelled", "请求已取消");
434
+ throw error;
435
+ }
436
+ },
437
+ async dispose() {
438
+ lifetime.abort();
439
+ await Promise.allSettled([...active]);
440
+ await pendingUsage;
441
+ }
442
+ };
443
+ }
444
+ async function apply(ctx) {
445
+ const host = ctx;
446
+ const domain = await ctx.storageDomain.open(zhihuUsageDomainSpec);
447
+ const service = createZhihuService({ resolveCredential: async () => (await host.credentials.resolve(credentialRef(ZHIHU_CREDENTIAL_REF)))?.value }, createZhihuUsageRecorder(domain.table("daily")), (event) => {
448
+ ctx.emit.call(ctx, ZHIHU_SEARCH_EVENT, event);
449
+ });
450
+ ctx.effect(() => async () => {
451
+ await service.dispose();
452
+ await domain.close();
453
+ }, "zhihu.usageDomainClose");
454
+ ctx.provide("zhihu", service);
455
+ ctx.effect(() => registerHostRpc(host, ZHIHU_RPC_CHANNEL, service.call));
456
+ bindZhihuWebSearch(ctx, service);
457
+ }
458
+ //#endregion
459
+ export { apply, createZhihuService, inject, name };
460
+
461
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","names":[],"sources":["../src/zhihu-knowledge.ts","../src/host-rpc.ts","../src/web-search-provider.ts","../src/index.ts"],"sourcesContent":["/**\n * 知乎知识库的管理侧能力:列知识库、上传文件。仅供设置页 RPC 使用,\n * 不暴露为模型工具——上传会把文件送入知乎云端,必须是用户在界面上显式发起。\n *\n * 端点(来源:github.com/klarkxy/zhihu-search,src/zhihu_search/upstream/http_client.py):\n * - 列表:GET /api/v1/knowledge/bases?Scope=all|created|subscribed\n * - 上传:POST /api/v1/knowledge/files(multipart: File + 可选 KnowledgeBaseID),同步解析\n */\nimport {\n parseZhihuEnvelope,\n zhihuFetchJson,\n ZhihuSearchError,\n type ZhihuClientOptions,\n} from './zhihu-client.ts'\n\nexport const ZHIHU_KNOWLEDGE_LIST_VERSION = 1\nexport const ZHIHU_KNOWLEDGE_UPLOAD_VERSION = 1\n/** 设置页 RPC 走 JSON/base64 传输,限额压到 20MB(平台上限 100MB)。 */\nexport const ZHIHU_KNOWLEDGE_UPLOAD_MAX_BYTES = 20 * 1024 * 1024\nexport const ZHIHU_KNOWLEDGE_UPLOAD_TIMEOUT_MS = 180_000\n\nconst KNOWLEDGE_BASES_PATH = '/api/v1/knowledge/bases'\nconst KNOWLEDGE_FILES_PATH = '/api/v1/knowledge/files'\n\nexport const ZHIHU_KNOWLEDGE_FILE_EXTENSIONS = [\n '.pdf', '.md', '.txt', '.ppt', '.pptx', '.xlsx', '.xls', '.docx', '.doc',\n '.webp', '.png', '.jpg', '.mobi', '.epub', '.csv', '.azw3',\n] as const\n\nconst KNOWLEDGE_CONTENT_TYPES: Record<string, string> = {\n '.pdf': 'application/pdf',\n '.md': 'text/markdown',\n '.txt': 'text/plain',\n '.ppt': 'application/vnd.ms-powerpoint',\n '.pptx': 'application/vnd.openxmlformats-officedocument.presentationml.presentation',\n '.xlsx': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',\n '.xls': 'application/vnd.ms-excel',\n '.docx': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',\n '.doc': 'application/msword',\n '.webp': 'image/webp',\n '.png': 'image/png',\n '.jpg': 'image/jpeg',\n '.mobi': 'application/x-mobipocket-ebook',\n '.epub': 'application/epub+zip',\n '.csv': 'text/csv',\n '.azw3': 'application/vnd.amazon.ebook',\n}\n\nexport type ZhihuKnowledgeBase = {\n id: string\n name: string\n relation: string\n visibility: string\n isDefault: boolean\n contentCount: number\n}\n\nexport type ZhihuKnowledgeBaseList = {\n version: typeof ZHIHU_KNOWLEDGE_LIST_VERSION\n bases: ZhihuKnowledgeBase[]\n}\n\nfunction normalizeBase(raw: Record<string, unknown>): ZhihuKnowledgeBase {\n return {\n id: typeof raw.KnowledgeBaseID === 'string' ? raw.KnowledgeBaseID : '',\n name: typeof raw.Name === 'string' && raw.Name.length > 0 ? raw.Name : '(未命名知识库)',\n relation: typeof raw.Relation === 'string' ? raw.Relation : '',\n visibility: typeof raw.Visibility === 'string' ? raw.Visibility : '',\n isDefault: raw.IsDefault === true,\n contentCount: typeof raw.ContentCount === 'number' ? raw.ContentCount : 0,\n }\n}\n\nexport async function listZhihuKnowledgeBases(options: ZhihuClientOptions = {}): Promise<ZhihuKnowledgeBaseList> {\n const body = await zhihuFetchJson(KNOWLEDGE_BASES_PATH, {\n params: { Scope: 'all' },\n }, '知乎知识库列表', options)\n const data = parseZhihuEnvelope(body) as { Items?: unknown }\n const items = Array.isArray(data.Items) ? data.Items : []\n return {\n version: ZHIHU_KNOWLEDGE_LIST_VERSION,\n bases: items\n .filter((item): item is Record<string, unknown> => !!item && typeof item === 'object' && !Array.isArray(item))\n .map(normalizeBase)\n .filter((base) => base.id.length > 0),\n }\n}\n\nexport type ZhihuKnowledgeUploadInput = {\n fileName: string\n data: Uint8Array\n /** 指定目标知识库;缺省进默认库。 */\n knowledgeBaseId?: string\n}\n\nexport type ZhihuKnowledgeUpload = {\n version: typeof ZHIHU_KNOWLEDGE_UPLOAD_VERSION\n knowledgeBaseId: string\n recallContentId: string\n fileName: string\n fileSize: number\n title: string\n abstract: string\n originUrl: string\n}\n\n/** 校验文件名与大小,返回小写扩展名。 */\nexport function checkKnowledgeFile(fileName: string, size: number): string {\n const cleaned = fileName.trim()\n if (!cleaned) throw new ZhihuSearchError('BAD_RESPONSE', '知识库文件名不能为空。')\n // 去掉路径成分,只保留文件名\n const base = cleaned.split(/[\\\\/]/).pop() ?? cleaned\n for (const ch of base) {\n const code = ch.codePointAt(0) ?? 0\n if (code < 32 || code === 127) throw new ZhihuSearchError('BAD_RESPONSE', '知识库文件名不能包含控制字符。')\n }\n if (new TextEncoder().encode(base).length > 255) {\n throw new ZhihuSearchError('BAD_RESPONSE', '知识库文件名不能超过 255 个 UTF-8 字节。')\n }\n if (size <= 0) throw new ZhihuSearchError('BAD_RESPONSE', '知识库文件内容不能为空。')\n if (size > ZHIHU_KNOWLEDGE_UPLOAD_MAX_BYTES) {\n throw new ZhihuSearchError('BAD_RESPONSE', `知识库文件大小不能超过 ${ZHIHU_KNOWLEDGE_UPLOAD_MAX_BYTES / 1024 / 1024}MB。`)\n }\n const dot = base.lastIndexOf('.')\n const ext = dot >= 0 ? base.slice(dot).toLowerCase() : ''\n if (!(ZHIHU_KNOWLEDGE_FILE_EXTENSIONS as readonly string[]).includes(ext)) {\n throw new ZhihuSearchError('BAD_RESPONSE', `知识库上传仅支持 ${ZHIHU_KNOWLEDGE_FILE_EXTENSIONS.join('、')}。`)\n }\n return ext\n}\n\nexport async function uploadZhihuKnowledgeFile(\n input: ZhihuKnowledgeUploadInput,\n options: ZhihuClientOptions = {},\n): Promise<ZhihuKnowledgeUpload> {\n const cleaned = (input.fileName.trim().split(/[\\\\/]/).pop() ?? input.fileName.trim())\n const ext = checkKnowledgeFile(input.fileName, input.data.byteLength)\n const form = new FormData()\n if (input.knowledgeBaseId) form.set('KnowledgeBaseID', input.knowledgeBaseId)\n form.set('File', new Blob([input.data.buffer as ArrayBuffer], { type: KNOWLEDGE_CONTENT_TYPES[ext] ?? 'application/octet-stream' }), cleaned)\n const body = await zhihuFetchJson(KNOWLEDGE_FILES_PATH, {\n formData: form,\n }, '知乎知识库上传', { ...options, timeoutMs: options.timeoutMs ?? ZHIHU_KNOWLEDGE_UPLOAD_TIMEOUT_MS })\n const data = parseZhihuEnvelope(body) as Record<string, unknown>\n return {\n version: ZHIHU_KNOWLEDGE_UPLOAD_VERSION,\n knowledgeBaseId: typeof data.KnowledgeBaseID === 'string' ? data.KnowledgeBaseID : '',\n recallContentId: typeof data.RecallContentID === 'string' ? data.RecallContentID : '',\n fileName: typeof data.FileName === 'string' ? data.FileName : cleaned,\n fileSize: typeof data.FileSize === 'number' ? data.FileSize : input.data.byteLength,\n title: typeof data.Title === 'string' ? data.Title : '',\n abstract: typeof data.Abstract === 'string' ? data.Abstract : '',\n originUrl: typeof data.OriginUrl === 'string' ? data.OriginUrl : '',\n }\n}\n","import type { IncomingMessage, ServerResponse } from 'node:http'\n\nconst CHANNEL_PATTERN = /^\\/[A-Za-z0-9._~-]+$/\nconst ENDPOINT_SEGMENT = /^[A-Za-z0-9_$.-]+$/\nconst MAX_BODY_BYTES = 64 * 1024 * 1024\n\nexport type HostRpcHandler = (endpoint: string, payload: unknown, signal: AbortSignal) => Promise<unknown>\n\nexport type HostRpcContext = {\n webServer: {\n register: (route: {\n kind: 'prefix'\n path: string\n handler: (req: IncomingMessage, res: ServerResponse) => void | Promise<void>\n }) => () => void\n }\n connection: {\n requestRejection?: (request: IncomingMessage) => number | undefined\n }\n}\n\n/** Standalone copy of manuscript Host RPC registration; this package cannot depend on manuscript. */\nexport function registerHostRpc(ctx: HostRpcContext, channel: string, handler: HostRpcHandler): () => void {\n if (!CHANNEL_PATTERN.test(channel) || channel === '/api') {\n throw new Error(`invalid RPC channel ${JSON.stringify(channel)}`)\n }\n return ctx.webServer.register({\n kind: 'prefix',\n path: channel,\n handler: (req, res) => dispatchHostRpc(ctx, channel, handler, req, res),\n })\n}\n\nfunction endpointFromRpcPath(channel: string, pathname: string): string | undefined {\n if (!pathname.startsWith(`${channel}/`)) return undefined\n const endpoint = pathname.slice(channel.length + 1)\n if (endpoint.split('/').some((segment) => segment === '' || segment === '.' || segment === '..' || !ENDPOINT_SEGMENT.test(segment))) {\n return undefined\n }\n return endpoint\n}\n\nasync function dispatchHostRpc(\n ctx: HostRpcContext,\n channel: string,\n handler: HostRpcHandler,\n req: IncomingMessage,\n res: ServerResponse,\n): Promise<void> {\n const rejection = ctx.connection.requestRejection?.(req)\n if (rejection !== undefined) {\n res.writeHead(rejection)\n res.end(rejection === 401 ? 'unauthorized' : 'forbidden')\n return\n }\n const abort = new AbortController()\n res.on('close', () => {\n if (!res.writableEnded) abort.abort()\n })\n const url = new URL(req.url ?? '/', 'http://dsh.internal')\n const endpoint = endpointFromRpcPath(channel, url.pathname)\n if (req.method !== 'POST' || endpoint === undefined) {\n res.writeHead(404)\n res.end('not found')\n return\n }\n const contentType = String(req.headers['content-type'] ?? '').split(';', 1)[0]?.trim().toLowerCase()\n if (contentType !== 'application/json') {\n res.writeHead(415)\n res.end('content type must be application/json')\n return\n }\n const chunks: Buffer[] = []\n let received = 0\n for await (const chunk of req) {\n const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)\n received += buffer.byteLength\n if (received > MAX_BODY_BYTES) {\n res.writeHead(413, { connection: 'close' })\n res.end()\n req.destroy()\n return\n }\n chunks.push(buffer)\n }\n let body: unknown\n try {\n body = JSON.parse(Buffer.concat(chunks).toString('utf8') || 'null')\n } catch {\n res.writeHead(400)\n res.end('body is not JSON')\n return\n }\n const record = body && typeof body === 'object' && !Array.isArray(body) ? body as Record<string, unknown> : {}\n const rpcId = typeof record.rpcId === 'string' ? record.rpcId : 'invalid-request'\n if (record.type !== 'client-request' || record.method !== endpoint) {\n res.writeHead(200, { 'content-type': 'application/json' })\n res.end(JSON.stringify({\n type: 'server-response',\n rpcId,\n result: {\n ok: false,\n error: {\n code: 'gateway/bad-request',\n message: record.method !== endpoint\n ? `method ${JSON.stringify(record.method)} does not match endpoint ${JSON.stringify(endpoint)}`\n : 'invalid client-request message',\n details: { issues: [] },\n },\n },\n }))\n return\n }\n try {\n const result = await handler(endpoint, record.payload, abort.signal)\n res.writeHead(200, { 'content-type': 'application/json' })\n res.end(JSON.stringify({ type: 'server-response', rpcId, result }))\n } catch (error) {\n res.writeHead(500)\n res.end(`handler failure: ${String(error)}`)\n }\n}\n","/**\n * Optional web-search-manager backend. Registers only when this profile already\n * provides `webSearchManager`; Zhihu still works without that plugin.\n * Official model tool remains `web_search`. Dedicated `zhihu_global_search` is unchanged.\n */\nimport { ZHIHU_CREDENTIAL_REF } from './contracts.ts'\nimport { reportExecuted, type ZhihuClientOptions, type ZhihuSearchExecuted } from './zhihu-client.ts'\nimport {\n executeZhihuGlobalSearch,\n ZHIHU_GLOBAL_SEARCH_MAX_COUNT,\n} from './operations.ts'\n\ntype ZhihuWebSearchHost = {\n run<T>(execute: (signal: AbortSignal) => Promise<T>, signal: AbortSignal): Promise<T>\n toolOptions: ZhihuClientOptions & { onExecuted: (event: ZhihuSearchExecuted) => void }\n}\n\nexport const ZHIHU_WEB_SEARCH_ID = 'zhihu-global'\n\nexport const ZHIHU_WEB_SEARCH_DESCRIPTOR = {\n id: ZHIHU_WEB_SEARCH_ID,\n label: '知乎全网搜索',\n description: '使用已配置的知乎 Access Secret 检索公开网页。与「知乎资料」共用同一份凭证。',\n credentialRef: ZHIHU_CREDENTIAL_REF,\n credentialShared: true,\n billing: 'request' as const,\n signupUrl: 'https://developer.zhihu.com',\n}\n\nexport type ZhihuWebSearchSource = { url: string; title?: string; snippet?: string }\nexport type ZhihuWebSearchResult = { sources: ZhihuWebSearchSource[]; truncated?: boolean }\nexport type ZhihuWebSearchRequest = { query: string; maxResults?: number }\n\nexport type ZhihuWebSearchProvider = {\n readonly id: string\n available(): boolean\n search(request: ZhihuWebSearchRequest, signal?: AbortSignal): Promise<ZhihuWebSearchResult>\n}\n\nexport type ZhihuWebSearchManager = {\n registerSearchProvider(\n descriptor: typeof ZHIHU_WEB_SEARCH_DESCRIPTOR,\n factory: (options: { apiKey?: string; timeoutMs: number }) => ZhihuWebSearchProvider,\n ): () => void\n}\n\nexport type ZhihuWebSearchProviderOptions = ZhihuClientOptions & {\n apiKey?: string\n onExecuted?: (event: ZhihuSearchExecuted) => void\n}\n\nfunction sourceFrom(url: string, title: string, snippet: string): ZhihuWebSearchSource | undefined {\n let parsed: URL\n try { parsed = new URL(url) } catch { return }\n if (!['https:', 'http:'].includes(parsed.protocol) || parsed.username || parsed.password) return\n return {\n url: parsed.href,\n ...(title ? { title: title.slice(0, 1000) } : {}),\n ...(snippet ? { snippet: snippet.slice(0, 8000) } : {}),\n }\n}\n\nfunction isWebSearchManager(value: unknown): value is ZhihuWebSearchManager {\n return !!value && typeof value === 'object' && typeof (value as ZhihuWebSearchManager).registerSearchProvider === 'function'\n}\n\nexport function createZhihuWebSearchProvider(options: ZhihuWebSearchProviderOptions): ZhihuWebSearchProvider {\n const { apiKey, onExecuted, ...client } = options\n return {\n id: ZHIHU_WEB_SEARCH_ID,\n available() { return Boolean(apiKey?.trim()) },\n async search(request, signal) {\n if (!apiKey?.trim()) throw new Error('知乎全网搜索未配置 Access Secret。')\n if (!request.query?.trim() || request.query.length > 4000) throw new Error('查询无效。')\n const count = request.maxResults ?? 5\n if (!Number.isInteger(count) || count < 1 || count > ZHIHU_GLOBAL_SEARCH_MAX_COUNT) {\n throw new Error(`结果数须为 1 至 ${ZHIHU_GLOBAL_SEARCH_MAX_COUNT}。`)\n }\n try {\n signal?.throwIfAborted()\n const result = await executeZhihuGlobalSearch(request.query.trim(), count, 'all', {\n ...client,\n signal,\n env: {},\n resolveCredential: async () => apiKey,\n })\n reportExecuted(onExecuted, { ok: true, results: result.items.length })\n const sources = result.items.flatMap(item => sourceFrom(item.url, item.title, item.summary) ?? [])\n return { sources: sources.slice(0, count), truncated: sources.length > count }\n } catch (error) {\n reportExecuted(onExecuted, { ok: false, results: 0 })\n throw error\n }\n },\n }\n}\n\nexport function registerZhihuGlobalSearchProvider(\n manager: ZhihuWebSearchManager,\n service: ZhihuWebSearchHost,\n): () => void {\n return manager.registerSearchProvider(ZHIHU_WEB_SEARCH_DESCRIPTOR, options => {\n const provider = createZhihuWebSearchProvider({\n ...service.toolOptions,\n apiKey: options.apiKey,\n timeoutMs: options.timeoutMs,\n })\n return {\n id: provider.id,\n available: () => provider.available(),\n search: (request, signal) => service.run(\n combined => provider.search(request, combined),\n signal ?? new AbortController().signal,\n ),\n }\n })\n}\n\n/** Wait for `webSearchManager` if this profile loaded it; no-op otherwise. */\nexport function bindZhihuWebSearch(\n ctx: { inject(deps: readonly string[], apply: (inner: { get(name: string): unknown }) => (() => void) | void): unknown },\n service: ZhihuWebSearchHost,\n): void {\n ctx.inject(['webSearchManager'], (inner) => {\n let manager: unknown\n try { manager = inner.get('webSearchManager') }\n catch { return }\n if (!isWebSearchManager(manager)) return\n return registerZhihuGlobalSearchProvider(manager, service)\n })\n}\n","import type { Context } from '@deepseek-ai/cordis'\nimport { credentialRef } from '@deepseek-ai/dsh-credentials'\nimport { executeZhihuSearch, ZHIHU_SEARCH_DEFAULT_COUNT } from './search-api.ts'\nimport {\n executeZhihuGlobalSearch, executeZhihuHotList, executeZhihuAsk, executeZhihuKnowledgeSearch,\n normalizeRecallScopes, ZHIHU_ASK_MODELS, ZHIHU_ASK_DEFAULT_MODEL,\n} from './operations.ts'\nimport { listZhihuKnowledgeBases, uploadZhihuKnowledgeFile, ZHIHU_KNOWLEDGE_UPLOAD_MAX_BYTES } from './zhihu-knowledge.ts'\nimport { type ZhihuClientOptions, type ZhihuSearchExecuted, ZhihuSearchError } from './zhihu-client.ts'\nimport { createZhihuUsageRecorder, resolveDays, zhihuUsageDomainSpec, type ZhihuUsageRecorder } from './usage.ts'\nimport { ZHIHU_RPC_CHANNEL, ZHIHU_CREDENTIAL_REF, ZHIHU_SEARCH_EVENT, type ZhihuRpcResult } from './contracts.ts'\nimport { registerHostRpc, type HostRpcContext } from './host-rpc.ts'\nimport { bindZhihuWebSearch } from './web-search-provider.ts'\n\nexport const name = '@klarkxy/dsh-zhihu'\nexport const inject = ['connection', 'credentials', 'storageDomain', 'webServer'] as const\nexport type ZhihuService = {\n call(endpoint: string, payload: unknown, signal: AbortSignal): Promise<ZhihuRpcResult>\n run<T>(execute: (signal: AbortSignal) => Promise<T>, signal: AbortSignal): Promise<T>\n dispose(): Promise<void>\n usageSummary(days: unknown): Promise<{ days: unknown[] }>\n toolOptions: ZhihuClientOptions & { onExecuted: (event: ZhihuSearchExecuted) => void }\n}\n\ntype Host = Context & HostRpcContext & {\n credentials: { resolve: (ref: ReturnType<typeof credentialRef>) => Promise<{ value: string } | undefined> }\n}\nconst fail = (code: string, message: string): ZhihuRpcResult => ({ ok: false, error: { code, message, details: {} } })\n\n/** One shared service for direct UI RPC and optional Tool wrappers. */\nexport function createZhihuService(options: ZhihuClientOptions, usage: ZhihuUsageRecorder, notify: (event: ZhihuSearchExecuted) => void = () => {}): ZhihuService {\n // Serialize read-modify-write counters; concurrent tools must not lose increments.\n let pendingUsage = Promise.resolve()\n const lifetime = new AbortController()\n const active = new Set<Promise<unknown>>()\n const run: ZhihuService['run'] = (execute, signal) => {\n const combined = AbortSignal.any([signal, lifetime.signal])\n const request = Promise.resolve().then(() => { combined.throwIfAborted(); return execute(combined) })\n active.add(request)\n return request.finally(() => active.delete(request))\n }\n const record = (event: ZhihuSearchExecuted) => {\n pendingUsage = pendingUsage.then(() => usage.record(event)).catch(() => {})\n try { notify(event) } catch { /* observer failure does not change a request */ }\n }\n const service = {\n toolOptions: { ...options, onExecuted: record },\n async usageSummary(days: unknown) { await pendingUsage; return { days: await usage.read(resolveDays(days)) } },\n async call(endpoint: string, payload: unknown, signal: AbortSignal): Promise<ZhihuRpcResult> {\n if (signal.aborted) return fail('cancelled', '请求已取消')\n const body = payload && typeof payload === 'object' && !Array.isArray(payload) ? payload as Record<string, unknown> : {}\n const client = { ...options, signal }\n let counted = false\n try {\n if (endpoint === 'usage.summary') { await pendingUsage; return { ok: true, value: { days: await usage.read(resolveDays(body.days)) } } }\n const query = typeof body.query === 'string' ? body.query : ''\n if (['search', 'global.search', 'ask', 'knowledge.search'].includes(endpoint) && (!query.trim() || query.length > 4000)) {\n return fail('bad-request', '请输入不超过 4000 字符的查询')\n }\n let value: unknown\n let results = 0\n counted = true\n if (endpoint === 'search') {\n const result = await executeZhihuSearch(query, typeof body.count === 'number' ? body.count : ZHIHU_SEARCH_DEFAULT_COUNT, client)\n value = result; results = result.items.length\n } else if (endpoint === 'global.search') {\n const result = await executeZhihuGlobalSearch(query, typeof body.count === 'number' ? body.count : 10,\n body.searchDb === 'realtime' || body.searchDb === 'static' ? body.searchDb : 'all', client)\n value = result; results = result.items.length\n } else if (endpoint === 'hot.list') {\n const result = await executeZhihuHotList(typeof body.limit === 'number' ? body.limit : 10, client)\n value = result; results = result.items.length\n } else if (endpoint === 'ask') {\n const model = (ZHIHU_ASK_MODELS as readonly unknown[]).includes(body.model) ? body.model as typeof ZHIHU_ASK_MODELS[number] : ZHIHU_ASK_DEFAULT_MODEL\n value = await executeZhihuAsk(query, model, client); results = 1\n } else if (endpoint === 'knowledge.search') {\n const result = await executeZhihuKnowledgeSearch(query, typeof body.limit === 'number' ? body.limit : 5, normalizeRecallScopes(body.recallScopes), client)\n value = result; results = result.items.length\n } else if (endpoint === 'knowledge.bases') {\n const result = await listZhihuKnowledgeBases(client); value = result; results = result.bases.length\n } else if (endpoint === 'knowledge.upload') {\n const contentBase64 = typeof body.contentBase64 === 'string' ? body.contentBase64 : ''\n if (!contentBase64) throw new Error('缺少文件内容。')\n if (contentBase64.length > Math.ceil(ZHIHU_KNOWLEDGE_UPLOAD_MAX_BYTES / 3) * 4 || !/^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/.test(contentBase64)) {\n return fail('bad-request', '文件内容无效或超过 20 MB')\n }\n const upload = await uploadZhihuKnowledgeFile({\n fileName: typeof body.fileName === 'string' ? body.fileName : '',\n data: new Uint8Array(Buffer.from(contentBase64, 'base64')),\n knowledgeBaseId: typeof body.knowledgeBaseId === 'string' && body.knowledgeBaseId.trim() ? body.knowledgeBaseId.trim() : undefined,\n }, client)\n value = { upload }; results = 1\n } else {\n counted = false\n return fail('bad-request', `unknown endpoint ${endpoint}`)\n }\n record({ ok: true, results })\n return { ok: true, value }\n } catch (error) {\n if (counted) record({ ok: false, results: 0 })\n if (signal.aborted) return fail('cancelled', '请求已取消')\n const code = error instanceof ZhihuSearchError ? error.code.toLowerCase().replaceAll('_', '-') : 'internal'\n return fail(code, error instanceof Error ? error.message : '知乎请求失败')\n }\n },\n }\n return {\n ...service,\n run,\n async call(endpoint, payload, signal) {\n if (signal.aborted || lifetime.signal.aborted) return fail('cancelled', '请求已取消')\n try { return await run(combined => service.call(endpoint, payload, combined), signal) }\n catch (error) {\n if (signal.aborted || lifetime.signal.aborted) return fail('cancelled', '请求已取消')\n throw error\n }\n },\n async dispose() {\n lifetime.abort()\n await Promise.allSettled([...active])\n await pendingUsage\n },\n }\n}\n\nexport async function apply(ctx: Context): Promise<void> {\n const host = ctx as Host\n const domain = await ctx.storageDomain.open(zhihuUsageDomainSpec)\n const service = createZhihuService({\n resolveCredential: async () => (await host.credentials.resolve(credentialRef(ZHIHU_CREDENTIAL_REF)))?.value,\n }, createZhihuUsageRecorder(domain.table('daily')), event => {\n const emit = ctx.emit as unknown as (name: string, event: ZhihuSearchExecuted) => void\n emit.call(ctx, ZHIHU_SEARCH_EVENT, event)\n })\n ctx.effect(() => async () => { await service.dispose(); await domain.close() }, 'zhihu.usageDomainClose')\n ctx.provide('zhihu', service)\n ctx.effect(() => registerHostRpc(host, ZHIHU_RPC_CHANNEL, service.call))\n bindZhihuWebSearch(ctx, service)\n}\n"],"mappings":"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"}