@lemoncat7/dsh-knowledge 0.6.0-alpha.7 → 0.6.0-alpha.9
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 +12 -7
- package/cordis.patch.yml +1 -0
- package/docs/architecture.md +8 -5
- package/lib/api.d.ts +15 -1
- package/lib/api.d.ts.map +1 -1
- package/lib/api.js +57 -6
- package/lib/api.js.map +1 -1
- package/lib/client.js +1 -1
- package/lib/client.js.map +2 -2
- package/lib/config.d.ts.map +1 -1
- package/lib/config.js +2 -5
- package/lib/config.js.map +1 -1
- package/lib/connection.d.ts +1 -1
- package/lib/connection.d.ts.map +1 -1
- package/lib/connection.js +2 -2
- package/lib/connection.js.map +1 -1
- package/lib/control.d.ts +2 -2
- package/lib/control.d.ts.map +1 -1
- package/lib/control.js +8 -4
- package/lib/control.js.map +1 -1
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +98 -40
- package/lib/index.js.map +1 -1
- package/lib/local-provider.d.ts +1 -0
- package/lib/local-provider.d.ts.map +1 -1
- package/lib/local-provider.js +9 -0
- package/lib/local-provider.js.map +1 -1
- package/lib/management-proxy.d.ts +4 -0
- package/lib/management-proxy.d.ts.map +1 -0
- package/lib/management-proxy.js +133 -0
- package/lib/management-proxy.js.map +1 -0
- package/lib/service-settings.d.ts +7 -0
- package/lib/service-settings.d.ts.map +1 -0
- package/lib/service-settings.js +39 -0
- package/lib/service-settings.js.map +1 -0
- package/lib/web.d.ts +1 -1
- package/lib/web.d.ts.map +1 -1
- package/lib/web.js +2 -1
- package/lib/web.js.map +1 -1
- package/package.json +1 -1
- package/web/app.js +68 -7
- package/web/index.html +1 -0
- package/web/styles.css +10 -0
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
const MAX_REQUEST_BYTES = 1_048_576;
|
|
2
|
+
const MAX_RESPONSE_BYTES = 10_485_760;
|
|
3
|
+
export function registerRemoteManagementProxy(ctx, prefix, current) {
|
|
4
|
+
const webServer = ctx.webServer ?? ctx.get('webServer');
|
|
5
|
+
if (webServer === undefined)
|
|
6
|
+
throw new Error('remote knowledge management requires the DSH webServer service');
|
|
7
|
+
return webServer.register({
|
|
8
|
+
kind: 'prefix',
|
|
9
|
+
path: prefix,
|
|
10
|
+
handler: async (req, res) => {
|
|
11
|
+
try {
|
|
12
|
+
await dispatch(prefix, current(), req, res);
|
|
13
|
+
}
|
|
14
|
+
catch (error) {
|
|
15
|
+
sendError(res, error);
|
|
16
|
+
}
|
|
17
|
+
},
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
async function dispatch(prefix, settings, req, res) {
|
|
21
|
+
assertManagementRequest(req);
|
|
22
|
+
if (settings.backend !== 'remote' || settings.remoteUrl === undefined || settings.remoteToken === undefined) {
|
|
23
|
+
throw httpError(409, '当前没有可用的远程知识库连接。');
|
|
24
|
+
}
|
|
25
|
+
const incoming = new URL(req.url ?? '/', 'http://knowledge.local');
|
|
26
|
+
const relative = incoming.pathname.slice(prefix.length).replace(/^\/+/, '');
|
|
27
|
+
const method = req.method ?? 'GET';
|
|
28
|
+
if (relative === 'service') {
|
|
29
|
+
if (method !== 'GET')
|
|
30
|
+
throw httpError(409, '请在中央 DSH 的知识库管理台中修改远程 API 状态。');
|
|
31
|
+
return sendJson(res, 200, {
|
|
32
|
+
publicApiEnabled: true,
|
|
33
|
+
publicApiPrefix: settings.remoteUrl,
|
|
34
|
+
remote: true,
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
if (!['GET', 'POST', 'PUT', 'PATCH', 'DELETE'].includes(method))
|
|
38
|
+
throw httpError(405, '不支持此远程管理请求方法。');
|
|
39
|
+
const base = settings.remoteUrl.endsWith('/') ? settings.remoteUrl : `${settings.remoteUrl}/`;
|
|
40
|
+
const target = new URL(`${relative}${incoming.search}`, base);
|
|
41
|
+
const body = method === 'GET' ? undefined : await readBody(req);
|
|
42
|
+
const controller = new AbortController();
|
|
43
|
+
const abort = () => { controller.abort(); };
|
|
44
|
+
req.once('aborted', abort);
|
|
45
|
+
try {
|
|
46
|
+
const response = await fetch(target, {
|
|
47
|
+
method,
|
|
48
|
+
headers: {
|
|
49
|
+
accept: 'application/json',
|
|
50
|
+
authorization: `Bearer ${settings.remoteToken}`,
|
|
51
|
+
...body === undefined ? {} : { 'content-type': 'application/json' },
|
|
52
|
+
},
|
|
53
|
+
...body === undefined ? {} : { body },
|
|
54
|
+
signal: controller.signal,
|
|
55
|
+
redirect: 'manual',
|
|
56
|
+
});
|
|
57
|
+
const declared = Number(response.headers.get('content-length') ?? 0);
|
|
58
|
+
if (Number.isFinite(declared) && declared > MAX_RESPONSE_BYTES)
|
|
59
|
+
throw httpError(502, '中央知识库响应过大。');
|
|
60
|
+
const payload = Buffer.from(await response.arrayBuffer());
|
|
61
|
+
if (payload.byteLength > MAX_RESPONSE_BYTES)
|
|
62
|
+
throw httpError(502, '中央知识库响应过大。');
|
|
63
|
+
res.writeHead(response.status, {
|
|
64
|
+
'content-type': response.headers.get('content-type') ?? 'application/json; charset=utf-8',
|
|
65
|
+
'content-length': payload.byteLength,
|
|
66
|
+
'cache-control': 'no-store',
|
|
67
|
+
'x-content-type-options': 'nosniff',
|
|
68
|
+
});
|
|
69
|
+
res.end(payload);
|
|
70
|
+
}
|
|
71
|
+
finally {
|
|
72
|
+
req.off('aborted', abort);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
function assertManagementRequest(req) {
|
|
76
|
+
if (req.headers['x-dsh-knowledge-client'] !== 'management-web')
|
|
77
|
+
throw httpError(401, '缺少知识库管理客户端标识。');
|
|
78
|
+
const fetchSite = req.headers['sec-fetch-site'];
|
|
79
|
+
if (fetchSite !== undefined && fetchSite !== 'same-origin' && fetchSite !== 'none')
|
|
80
|
+
throw httpError(403, '已拒绝跨站知识库管理请求。');
|
|
81
|
+
const origin = req.headers.origin;
|
|
82
|
+
if (origin === undefined)
|
|
83
|
+
return;
|
|
84
|
+
let originHost;
|
|
85
|
+
try {
|
|
86
|
+
originHost = new URL(origin).host;
|
|
87
|
+
}
|
|
88
|
+
catch {
|
|
89
|
+
throw httpError(403, '知识库管理请求来源无效。');
|
|
90
|
+
}
|
|
91
|
+
const expectedHost = firstHeader(req.headers['x-forwarded-host']) ?? req.headers.host;
|
|
92
|
+
if (expectedHost === undefined || originHost.toLowerCase() !== expectedHost.toLowerCase())
|
|
93
|
+
throw httpError(403, '已拒绝跨域知识库管理请求。');
|
|
94
|
+
}
|
|
95
|
+
async function readBody(req) {
|
|
96
|
+
const declared = Number(req.headers['content-length'] ?? 0);
|
|
97
|
+
if (Number.isFinite(declared) && declared > MAX_REQUEST_BYTES)
|
|
98
|
+
throw httpError(413, '远程管理请求内容过大。');
|
|
99
|
+
const chunks = [];
|
|
100
|
+
let size = 0;
|
|
101
|
+
for await (const chunk of req) {
|
|
102
|
+
const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
103
|
+
size += buffer.length;
|
|
104
|
+
if (size > MAX_REQUEST_BYTES)
|
|
105
|
+
throw httpError(413, '远程管理请求内容过大。');
|
|
106
|
+
chunks.push(buffer);
|
|
107
|
+
}
|
|
108
|
+
return size === 0 ? undefined : Buffer.concat(chunks).toString('utf8');
|
|
109
|
+
}
|
|
110
|
+
function firstHeader(value) {
|
|
111
|
+
const raw = Array.isArray(value) ? value[0] : value;
|
|
112
|
+
return raw?.split(',')[0]?.trim();
|
|
113
|
+
}
|
|
114
|
+
function sendError(res, error) {
|
|
115
|
+
const status = error !== null && typeof error === 'object' && typeof error.status === 'number'
|
|
116
|
+
? error.status : 502;
|
|
117
|
+
const message = status >= 500 ? '无法连接中央知识库,请检查网络和服务状态。' : error instanceof Error ? error.message : '远程管理请求失败。';
|
|
118
|
+
sendJson(res, status, { error: message, code: `HTTP_${status}` });
|
|
119
|
+
}
|
|
120
|
+
function sendJson(res, status, body) {
|
|
121
|
+
const payload = Buffer.from(JSON.stringify(body));
|
|
122
|
+
res.writeHead(status, {
|
|
123
|
+
'content-type': 'application/json; charset=utf-8',
|
|
124
|
+
'content-length': payload.byteLength,
|
|
125
|
+
'cache-control': 'no-store',
|
|
126
|
+
'x-content-type-options': 'nosniff',
|
|
127
|
+
});
|
|
128
|
+
res.end(payload);
|
|
129
|
+
}
|
|
130
|
+
function httpError(status, message) {
|
|
131
|
+
return Object.assign(new Error(message), { status });
|
|
132
|
+
}
|
|
133
|
+
//# sourceMappingURL=management-proxy.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"management-proxy.js","sourceRoot":"","sources":["../src/management-proxy.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export interface KnowledgeServiceSettings {
|
|
2
|
+
publicApiEnabled: boolean;
|
|
3
|
+
}
|
|
4
|
+
export declare function serviceSettingsPath(connectionPath: string | undefined): string | undefined;
|
|
5
|
+
export declare function loadServiceSettings(path: string | undefined): KnowledgeServiceSettings | undefined;
|
|
6
|
+
export declare function storeServiceSettings(path: string, settings: KnowledgeServiceSettings): Promise<void>;
|
|
7
|
+
//# sourceMappingURL=service-settings.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"service-settings.d.ts","sourceRoot":"","sources":["../src/service-settings.ts"],"names":[],"mappings":"AAKA,MAAM,WAAW,wBAAwB;IACvC,gBAAgB,EAAE,OAAO,CAAA;CAC1B;AAED,wBAAgB,mBAAmB,CAAC,cAAc,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,GAAG,SAAS,CAE1F;AAED,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,MAAM,GAAG,SAAS,GAAG,wBAAwB,GAAG,SAAS,CAalG;AAED,wBAAsB,oBAAoB,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,wBAAwB,GAAG,OAAO,CAAC,IAAI,CAAC,CAS1G"}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { readFileSync } from 'node:fs';
|
|
3
|
+
import { mkdir, rename, rm, writeFile } from 'node:fs/promises';
|
|
4
|
+
import { dirname } from 'node:path';
|
|
5
|
+
export function serviceSettingsPath(connectionPath) {
|
|
6
|
+
return connectionPath === undefined ? undefined : `${connectionPath}.service.json`;
|
|
7
|
+
}
|
|
8
|
+
export function loadServiceSettings(path) {
|
|
9
|
+
if (path === undefined)
|
|
10
|
+
return undefined;
|
|
11
|
+
let value;
|
|
12
|
+
try {
|
|
13
|
+
value = JSON.parse(readFileSync(path, 'utf8'));
|
|
14
|
+
}
|
|
15
|
+
catch (error) {
|
|
16
|
+
if (error.code === 'ENOENT')
|
|
17
|
+
return undefined;
|
|
18
|
+
throw new Error(`failed to read knowledge service settings: ${error instanceof Error ? error.message : String(error)}`);
|
|
19
|
+
}
|
|
20
|
+
if (value === null || typeof value !== 'object' || Array.isArray(value)) {
|
|
21
|
+
throw new Error('stored knowledge service settings must be a JSON object');
|
|
22
|
+
}
|
|
23
|
+
const enabled = value.publicApiEnabled;
|
|
24
|
+
if (typeof enabled !== 'boolean')
|
|
25
|
+
throw new Error('stored public API setting must be a boolean');
|
|
26
|
+
return { publicApiEnabled: enabled };
|
|
27
|
+
}
|
|
28
|
+
export async function storeServiceSettings(path, settings) {
|
|
29
|
+
await mkdir(dirname(path), { recursive: true, mode: 0o700 });
|
|
30
|
+
const temporary = `${path}.${randomUUID()}.tmp`;
|
|
31
|
+
try {
|
|
32
|
+
await writeFile(temporary, `${JSON.stringify(settings, null, 2)}\n`, { encoding: 'utf8', mode: 0o600, flag: 'wx' });
|
|
33
|
+
await rename(temporary, path);
|
|
34
|
+
}
|
|
35
|
+
finally {
|
|
36
|
+
await rm(temporary, { force: true }).catch(() => { });
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
//# sourceMappingURL=service-settings.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"service-settings.js","sourceRoot":"","sources":["../src/service-settings.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAA;AACxC,OAAO,EAAE,YAAY,EAAE,MAAM,SAAS,CAAA;AACtC,OAAO,EAAE,KAAK,EAAE,MAAM,EAAE,EAAE,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAA;AAC/D,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAA;AAMnC,MAAM,UAAU,mBAAmB,CAAC,cAAkC;IACpE,OAAO,cAAc,KAAK,SAAS,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,GAAG,cAAc,eAAe,CAAA;AACpF,CAAC;AAED,MAAM,UAAU,mBAAmB,CAAC,IAAwB;IAC1D,IAAI,IAAI,KAAK,SAAS;QAAE,OAAO,SAAS,CAAA;IACxC,IAAI,KAAc,CAAA;IAClB,IAAI,CAAC;QAAC,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,IAAI,EAAE,MAAM,CAAC,CAAY,CAAA;IAAC,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QAC/E,IAAK,KAA+B,CAAC,IAAI,KAAK,QAAQ;YAAE,OAAO,SAAS,CAAA;QACxE,MAAM,IAAI,KAAK,CAAC,8CAA8C,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC,CAAA;IACzH,CAAC;IACD,IAAI,KAAK,KAAK,IAAI,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC;QACxE,MAAM,IAAI,KAAK,CAAC,yDAAyD,CAAC,CAAA;IAC5E,CAAC;IACD,MAAM,OAAO,GAAI,KAAwC,CAAC,gBAAgB,CAAA;IAC1E,IAAI,OAAO,OAAO,KAAK,SAAS;QAAE,MAAM,IAAI,KAAK,CAAC,6CAA6C,CAAC,CAAA;IAChG,OAAO,EAAE,gBAAgB,EAAE,OAAO,EAAE,CAAA;AACtC,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,oBAAoB,CAAC,IAAY,EAAE,QAAkC;IACzF,MAAM,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,CAAA;IAC5D,MAAM,SAAS,GAAG,GAAG,IAAI,IAAI,UAAU,EAAE,MAAM,CAAA;IAC/C,IAAI,CAAC;QACH,MAAM,SAAS,CAAC,SAAS,EAAE,GAAG,IAAI,CAAC,SAAS,CAAC,QAAQ,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,EAAE,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC,CAAA;QACnH,MAAM,MAAM,CAAC,SAAS,EAAE,IAAI,CAAC,CAAA;IAC/B,CAAC;YAAS,CAAC;QACT,MAAM,EAAE,CAAC,SAAS,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE,GAAE,CAAC,CAAC,CAAA;IACtD,CAAC;AACH,CAAC"}
|
package/lib/web.d.ts
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
import type { RuntimeContextLike } from './runtime.js';
|
|
2
|
-
export declare function registerKnowledgeWeb(ctx: RuntimeContextLike, webPath: string, apiPrefix: string): () => void;
|
|
2
|
+
export declare function registerKnowledgeWeb(ctx: RuntimeContextLike, webPath: string, apiPrefix: string, authMode?: 'bearer' | 'same-origin'): () => void;
|
|
3
3
|
//# sourceMappingURL=web.d.ts.map
|
package/lib/web.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"web.d.ts","sourceRoot":"","sources":["../src/web.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,cAAc,CAAA;AAiBtD,wBAAgB,oBAAoB,CAClC,GAAG,EAAE,kBAAkB,EACvB,OAAO,EAAE,MAAM,EACf,SAAS,EAAE,MAAM,
|
|
1
|
+
{"version":3,"file":"web.d.ts","sourceRoot":"","sources":["../src/web.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,cAAc,CAAA;AAiBtD,wBAAgB,oBAAoB,CAClC,GAAG,EAAE,kBAAkB,EACvB,OAAO,EAAE,MAAM,EACf,SAAS,EAAE,MAAM,EACjB,QAAQ,GAAE,QAAQ,GAAG,aAAwB,GAC5C,MAAM,IAAI,CAaZ"}
|
package/lib/web.js
CHANGED
|
@@ -9,12 +9,13 @@ const ASSET_VERSION = createHash('sha256')
|
|
|
9
9
|
.digest('hex')
|
|
10
10
|
.slice(0, 12);
|
|
11
11
|
const INDEX_TEMPLATE = readFileSync(new URL('../web/index.html', import.meta.url), 'utf8');
|
|
12
|
-
export function registerKnowledgeWeb(ctx, webPath, apiPrefix) {
|
|
12
|
+
export function registerKnowledgeWeb(ctx, webPath, apiPrefix, authMode = 'bearer') {
|
|
13
13
|
const webServer = ctx.webServer ?? ctx.get('webServer');
|
|
14
14
|
if (webServer === undefined)
|
|
15
15
|
throw new Error('exposeWeb requires the DSH webServer service');
|
|
16
16
|
const index = Buffer.from(INDEX_TEMPLATE
|
|
17
17
|
.replaceAll('__DSH_KNOWLEDGE_API_PREFIX__', escapeHtmlAttribute(apiPrefix))
|
|
18
|
+
.replaceAll('__DSH_KNOWLEDGE_AUTH_MODE__', escapeHtmlAttribute(authMode))
|
|
18
19
|
.replaceAll('__DSH_KNOWLEDGE_WEB_PATH__', escapeHtmlAttribute(webPath))
|
|
19
20
|
.replaceAll('__DSH_KNOWLEDGE_ASSET_VERSION__', ASSET_VERSION));
|
|
20
21
|
return webServer.register({
|
package/lib/web.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"web.js","sourceRoot":"","sources":["../src/web.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,SAAS,CAAA;AACtC,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAA;AASxC,MAAM,aAAa,GAAG,IAAI,GAAG,CAAgB;IAC3C,CAAC,QAAQ,EAAE,SAAS,CAAC,eAAe,EAAE,gCAAgC,CAAC,CAAC;IACxE,CAAC,YAAY,EAAE,SAAS,CAAC,mBAAmB,EAAE,yBAAyB,CAAC,CAAC;CAC1E,CAAC,CAAA;AACF,MAAM,aAAa,GAAG,UAAU,CAAC,QAAQ,CAAC;KACvC,MAAM,CAAC,CAAC,GAAG,aAAa,CAAC,MAAM,EAAE,CAAC,CAAC,GAAG,CAAC,KAAK,CAAC,EAAE,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;KACrE,MAAM,CAAC,KAAK,CAAC;KACb,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,CAAA;AACf,MAAM,cAAc,GAAG,YAAY,CAAC,IAAI,GAAG,CAAC,mBAAmB,EAAE,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC,CAAA;AAE1F,MAAM,UAAU,oBAAoB,CAClC,GAAuB,EACvB,OAAe,EACf,SAAiB;
|
|
1
|
+
{"version":3,"file":"web.js","sourceRoot":"","sources":["../src/web.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
package/web/app.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
const API_BASE = document.querySelector('meta[name="dsh-knowledge-api"]')?.content || '/knowledge-api/v1'
|
|
2
|
+
const AUTH_MODE = document.querySelector('meta[name="dsh-knowledge-auth-mode"]')?.content || 'bearer'
|
|
2
3
|
const TOKEN_KEY = 'dsh-knowledge.session-token'
|
|
3
4
|
const TYPES = ['preference', 'fact', 'decision', 'procedure', 'lesson']
|
|
4
5
|
const TYPE_LABELS = { preference: '偏好', fact: '事实', decision: '决策', procedure: '流程', lesson: '经验' }
|
|
@@ -51,6 +52,7 @@ const state = {
|
|
|
51
52
|
candidates: [],
|
|
52
53
|
candidateStatus: 'pending',
|
|
53
54
|
tokens: [],
|
|
55
|
+
service: { publicApiEnabled: false, publicApiPrefix: '/knowledge-api/v1', remote: false },
|
|
54
56
|
loading: false,
|
|
55
57
|
error: '',
|
|
56
58
|
}
|
|
@@ -132,6 +134,7 @@ function showToast(message, kind = '') {
|
|
|
132
134
|
|
|
133
135
|
async function api(path, options = {}) {
|
|
134
136
|
const headers = { accept: 'application/json', ...(options.body === undefined ? {} : { 'content-type': 'application/json' }) }
|
|
137
|
+
if (AUTH_MODE === 'same-origin') headers['x-dsh-knowledge-client'] = 'management-web'
|
|
135
138
|
if (state.token) headers.authorization = `Bearer ${state.token}`
|
|
136
139
|
const response = await fetch(`${API_BASE}/${path.replace(/^\/+/, '')}`, {
|
|
137
140
|
method: options.method || 'GET',
|
|
@@ -153,6 +156,12 @@ async function api(path, options = {}) {
|
|
|
153
156
|
}
|
|
154
157
|
|
|
155
158
|
async function boot() {
|
|
159
|
+
if (AUTH_MODE === 'same-origin') {
|
|
160
|
+
state.token = ''
|
|
161
|
+
try { state.service = await api('service') } catch {}
|
|
162
|
+
await navigate('overview')
|
|
163
|
+
return
|
|
164
|
+
}
|
|
156
165
|
if (!state.token) {
|
|
157
166
|
renderLogin()
|
|
158
167
|
return
|
|
@@ -213,7 +222,8 @@ function renderLogin(message = '') {
|
|
|
213
222
|
function signOut() {
|
|
214
223
|
sessionStorage.removeItem(TOKEN_KEY)
|
|
215
224
|
Object.assign(state, { token: '', stats: null, overview: null, knowledgeBases: [], mounts: [], resolvedMounts: [], entries: [], documents: [], candidates: [], tokens: [] })
|
|
216
|
-
|
|
225
|
+
if (AUTH_MODE === 'same-origin') void boot()
|
|
226
|
+
else renderLogin()
|
|
217
227
|
}
|
|
218
228
|
|
|
219
229
|
async function navigate(view) {
|
|
@@ -229,7 +239,7 @@ async function navigate(view) {
|
|
|
229
239
|
if (view === 'candidates') await loadCandidates()
|
|
230
240
|
if (view === 'tokens') await loadTokens()
|
|
231
241
|
} catch (error) {
|
|
232
|
-
if (error.status === 401) return signOut()
|
|
242
|
+
if (error.status === 401 && AUTH_MODE === 'bearer') return signOut()
|
|
233
243
|
state.error = friendlyError(error)
|
|
234
244
|
} finally {
|
|
235
245
|
state.loading = false
|
|
@@ -329,7 +339,9 @@ async function loadCandidates() {
|
|
|
329
339
|
}
|
|
330
340
|
|
|
331
341
|
async function loadTokens() {
|
|
332
|
-
|
|
342
|
+
const [tokens, service] = await Promise.all([api('tokens'), api('service')])
|
|
343
|
+
state.tokens = tokens
|
|
344
|
+
state.service = service
|
|
333
345
|
if (!state.stats) await refreshStats()
|
|
334
346
|
}
|
|
335
347
|
|
|
@@ -435,7 +447,7 @@ function renderSidebar() {
|
|
|
435
447
|
const pending = state.stats?.candidates.pending
|
|
436
448
|
const navItems = [
|
|
437
449
|
['overview', '概览', '◫'], ['bases', '知识库', '▦'], ['entries', '文档', '◇'], ['candidates', '审核', '✓'], ['tokens', '访问管理', '⌁'],
|
|
438
|
-
]
|
|
450
|
+
].filter(([id]) => id !== 'tokens' || !state.service.remote)
|
|
439
451
|
return element('aside', { class: 'sidebar', 'aria-label': '知识库导航' },
|
|
440
452
|
element('div', { class: 'brand' },
|
|
441
453
|
element('div', { class: 'brand-mark', 'aria-hidden': 'true' }, 'K'),
|
|
@@ -447,8 +459,8 @@ function renderSidebar() {
|
|
|
447
459
|
}, element('span', { class: 'nav-icon', 'aria-hidden': 'true' }, icon), label,
|
|
448
460
|
id === 'candidates' && pending ? element('span', { class: 'nav-count', 'aria-label': `${pending} 条待审核` }, pending) : null))),
|
|
449
461
|
element('div', { class: 'sidebar-footer' },
|
|
450
|
-
element('div', { class: 'connection' }, element('span', { class: 'status-dot', 'aria-hidden': 'true' }), '
|
|
451
|
-
actionButton('退出当前会话', signOut, 'ghost small'),
|
|
462
|
+
element('div', { class: 'connection' }, element('span', { class: 'status-dot', 'aria-hidden': 'true' }), state.service.remote ? '中央知识库已连接' : '本地知识库已连接'),
|
|
463
|
+
AUTH_MODE === 'bearer' ? actionButton('退出当前会话', signOut, 'ghost small') : null,
|
|
452
464
|
),
|
|
453
465
|
)
|
|
454
466
|
}
|
|
@@ -1145,7 +1157,25 @@ function renderCandidateCard(candidate) {
|
|
|
1145
1157
|
}
|
|
1146
1158
|
|
|
1147
1159
|
function renderTokens() {
|
|
1160
|
+
const apiAddress = new URL(state.service.publicApiPrefix, location.origin).href.replace(/\/$/, '')
|
|
1148
1161
|
return element('section', { 'aria-labelledby': 'tokens-heading' },
|
|
1162
|
+
element('div', { class: 'api-access-card' },
|
|
1163
|
+
element('div', { class: 'api-access-heading' },
|
|
1164
|
+
element('div', {}, element('strong', {}, '远程知识库 API'), element('p', {}, state.service.publicApiEnabled
|
|
1165
|
+
? '其他 DSH 客户端可以使用下面的地址和客户端令牌连接。'
|
|
1166
|
+
: '当前仅能在本机管理;开启后其他 DSH 客户端才能连接。')),
|
|
1167
|
+
badge(state.service.publicApiEnabled ? '已开放' : '未开放', state.service.publicApiEnabled ? 'success' : 'warning'),
|
|
1168
|
+
),
|
|
1169
|
+
element('label', { class: 'api-address-label', for: 'knowledge-public-api-address' }, '客户端填写的服务器地址'),
|
|
1170
|
+
element('div', { class: 'api-address-row' },
|
|
1171
|
+
Object.assign(element('input', { id: 'knowledge-public-api-address', class: 'input', readonly: true, value: apiAddress }), { value: apiAddress }),
|
|
1172
|
+
actionButton('复制地址', () => copyText(apiAddress, 'API 地址已复制。'), 'small'),
|
|
1173
|
+
),
|
|
1174
|
+
element('div', { class: 'api-access-actions' },
|
|
1175
|
+
element('small', {}, state.service.publicApiEnabled ? '关闭后已有客户端会立即无法连接,本地管理不受影响。' : '开启远程 API 后,请为每台客户端创建独立令牌。'),
|
|
1176
|
+
actionButton(state.service.publicApiEnabled ? '关闭远程 API' : '开启远程 API', togglePublicApi, state.service.publicApiEnabled ? 'danger small' : 'primary small'),
|
|
1177
|
+
),
|
|
1178
|
+
),
|
|
1149
1179
|
element('div', { class: 'section-heading' },
|
|
1150
1180
|
element('div', {}, element('h2', { id: 'tokens-heading' }, '客户端访问令牌'), element('p', {}, '给其他 DSH 客户端分配最小必要权限;原始令牌只显示一次。')),
|
|
1151
1181
|
actionButton('+ 创建令牌', openTokenCreator, 'primary'),
|
|
@@ -1164,10 +1194,32 @@ function renderTokenRow(token) {
|
|
|
1164
1194
|
element('div', { class: 'permissions' }, token.permissions.map(permission => badge(permission))),
|
|
1165
1195
|
element('small', {}, `创建于 ${formatDate(token.createdAt)}${token.lastUsedAt ? ` · 最近使用 ${formatDate(token.lastUsedAt)}` : ' · 尚未使用'}`),
|
|
1166
1196
|
),
|
|
1167
|
-
revoked
|
|
1197
|
+
revoked
|
|
1198
|
+
? actionButton('永久删除', () => confirmDeleteToken(token), 'danger small')
|
|
1199
|
+
: actionButton('撤销', () => confirmRevokeToken(token), 'danger small'),
|
|
1168
1200
|
)
|
|
1169
1201
|
}
|
|
1170
1202
|
|
|
1203
|
+
async function copyText(value, successMessage) {
|
|
1204
|
+
try { await navigator.clipboard.writeText(value); showToast(successMessage) } catch { showToast('复制失败,请手动选择内容。', 'error') }
|
|
1205
|
+
}
|
|
1206
|
+
|
|
1207
|
+
function togglePublicApi() {
|
|
1208
|
+
const enabling = !state.service.publicApiEnabled
|
|
1209
|
+
openConfirm({
|
|
1210
|
+
title: enabling ? '开启远程知识库 API?' : '关闭远程知识库 API?',
|
|
1211
|
+
message: enabling
|
|
1212
|
+
? '开启后,持有有效令牌的其他 DSH 客户端可以连接这台中央知识库。'
|
|
1213
|
+
: '关闭后,所有远程客户端会立即断开;本地知识库和管理台不受影响。',
|
|
1214
|
+
confirmLabel: enabling ? '确认开启' : '确认关闭', danger: !enabling,
|
|
1215
|
+
onConfirm: async () => {
|
|
1216
|
+
state.service = await api('service', { method: 'PUT', body: { publicApiEnabled: enabling } })
|
|
1217
|
+
showToast(enabling ? '远程 API 已开启。' : '远程 API 已关闭。')
|
|
1218
|
+
renderShell()
|
|
1219
|
+
},
|
|
1220
|
+
})
|
|
1221
|
+
}
|
|
1222
|
+
|
|
1171
1223
|
function emptyState(title, description, actionLabel, action) {
|
|
1172
1224
|
return element('div', { class: 'empty-state' }, element('div', {}, element('strong', {}, title), element('p', {}, description), actionLabel ? actionButton(actionLabel, action, 'primary') : null))
|
|
1173
1225
|
}
|
|
@@ -1605,6 +1657,15 @@ function confirmRevokeToken(token) {
|
|
|
1605
1657
|
} })
|
|
1606
1658
|
}
|
|
1607
1659
|
|
|
1660
|
+
function confirmDeleteToken(token) {
|
|
1661
|
+
openConfirm({ title: `永久删除“${token.name}”?`, message: '这条已撤销令牌记录会被永久删除,此操作无法撤销。', confirmLabel: '永久删除', danger: true, onConfirm: async () => {
|
|
1662
|
+
await api(`tokens/${encodeURIComponent(token.id)}`, { method: 'DELETE' })
|
|
1663
|
+
showToast('已撤销令牌已永久删除。')
|
|
1664
|
+
await loadTokens()
|
|
1665
|
+
renderShell()
|
|
1666
|
+
} })
|
|
1667
|
+
}
|
|
1668
|
+
|
|
1608
1669
|
function openConfirm({ title, message, confirmLabel, danger, onConfirm }) {
|
|
1609
1670
|
return openModal({ title, body: element('p', {}, message), primaryLabel: confirmLabel, primaryVariant: danger ? 'danger' : 'primary', onPrimary: async () => { await onConfirm(); return true } })
|
|
1610
1671
|
}
|
package/web/index.html
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
<meta name="color-scheme" content="light dark">
|
|
7
7
|
<meta name="theme-color" content="#f4f5f7">
|
|
8
8
|
<meta name="dsh-knowledge-api" content="__DSH_KNOWLEDGE_API_PREFIX__">
|
|
9
|
+
<meta name="dsh-knowledge-auth-mode" content="__DSH_KNOWLEDGE_AUTH_MODE__">
|
|
9
10
|
<meta name="dsh-knowledge-web" content="__DSH_KNOWLEDGE_WEB_PATH__">
|
|
10
11
|
<title>DSH 知识库</title>
|
|
11
12
|
<link rel="stylesheet" href="__DSH_KNOWLEDGE_WEB_PATH__/styles.css?v=__DSH_KNOWLEDGE_ASSET_VERSION__">
|
package/web/styles.css
CHANGED
|
@@ -628,6 +628,15 @@ button:disabled { cursor: not-allowed; opacity: .56; }
|
|
|
628
628
|
.token-row strong { display: block; }
|
|
629
629
|
.token-row small { color: var(--text-tertiary); }
|
|
630
630
|
.permissions { display: flex; flex-wrap: wrap; gap: 5px; margin-top: 6px; }
|
|
631
|
+
.api-access-card { display: grid; gap: 14px; margin-bottom: 20px; border: 1px solid var(--border); border-radius: var(--radius); padding: 18px; background: var(--surface); }
|
|
632
|
+
.api-access-heading, .api-access-actions { display: flex; align-items: flex-start; justify-content: space-between; gap: 16px; }
|
|
633
|
+
.api-access-heading strong { font-size: 15px; }
|
|
634
|
+
.api-access-heading p { margin: 4px 0 0; color: var(--text-tertiary); }
|
|
635
|
+
.api-address-label { color: var(--text-secondary); font-size: 12px; font-weight: 600; }
|
|
636
|
+
.api-address-row { display: flex; align-items: center; gap: 8px; }
|
|
637
|
+
.api-address-row .input { min-width: 0; flex: 1; font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; }
|
|
638
|
+
.api-access-actions { align-items: center; border-top: 1px solid var(--border); padding-top: 13px; }
|
|
639
|
+
.api-access-actions small { color: var(--text-tertiary); }
|
|
631
640
|
|
|
632
641
|
.dialog-backdrop { position: fixed; inset: 0; z-index: 100; display: grid; place-items: center; padding: 20px; background: rgb(8 10 15 / 52%); backdrop-filter: blur(4px); }
|
|
633
642
|
.dialog {
|
|
@@ -755,6 +764,7 @@ button:disabled { cursor: not-allowed; opacity: .56; }
|
|
|
755
764
|
}
|
|
756
765
|
|
|
757
766
|
@media (max-width: 520px) {
|
|
767
|
+
.api-access-heading, .api-access-actions, .api-address-row { align-items: stretch; flex-direction: column; }
|
|
758
768
|
body { font-size: 13px; }
|
|
759
769
|
.login-page { padding: 16px; }
|
|
760
770
|
.login-card { padding: 24px; }
|